Skip to content

Commit e806b63

Browse files
committed
refactor: simplify version loading to match auggie-sdk-typescript
- Read version from package.json at runtime (like auggie-sdk) - Remove generated version file approach - Remove prebuild/prepare scripts - Single source of truth for version
1 parent eed9a67 commit e806b63

5 files changed

Lines changed: 121 additions & 114 deletions

File tree

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
node_modules
22
dist
3-
src/generated/

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323
"test:integration": "tsx test/augment-provider.ts && tsx test/cli-agent.ts",
2424
"format": "biome format --write .",
2525
"lint": "biome check .",
26-
"lint:fix": "biome check --write .",
27-
"prebuild": "node scripts/generate-version.js",
28-
"prepare": "node scripts/generate-version.js"
26+
"lint:fix": "biome check --write ."
2927
},
3028
"keywords": [
3129
"augment",

scripts/generate-version.js

Lines changed: 0 additions & 38 deletions
This file was deleted.

src/core/utils.test.ts

Lines changed: 89 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
import { describe, it, expect } from "vitest";
2-
import { sanitizeKey, normalizePath, buildClientUserAgent } from "./utils.js";
2+
import {
3+
sanitizeKey,
4+
normalizePath,
5+
buildClientUserAgent,
6+
type MCPClientInfo
7+
} from "./utils.js";
38

49
describe("sanitizeKey", () => {
5-
it("replaces unsafe characters with underscores", () => {
6-
expect(sanitizeKey("foo/bar/baz")).toBe("foo_bar_baz");
10+
it("should replace unsafe characters with underscores", () => {
11+
expect(sanitizeKey("foo/bar")).toBe("foo_bar");
712
expect(sanitizeKey("foo:bar")).toBe("foo_bar");
8-
expect(sanitizeKey("foo@bar.com")).toBe("foo_bar_com");
13+
expect(sanitizeKey("foo@bar")).toBe("foo_bar");
914
});
1015

11-
it("collapses multiple underscores", () => {
16+
it("should collapse multiple underscores", () => {
1217
expect(sanitizeKey("foo//bar")).toBe("foo_bar");
13-
expect(sanitizeKey("foo___bar")).toBe("foo_bar");
18+
expect(sanitizeKey("foo:::bar")).toBe("foo_bar");
1419
});
1520

16-
it("strips leading and trailing underscores", () => {
17-
expect(sanitizeKey("_foo_")).toBe("foo");
18-
expect(sanitizeKey("__foo__")).toBe("foo");
21+
it("should trim leading/trailing underscores", () => {
22+
expect(sanitizeKey("/foo")).toBe("foo");
23+
expect(sanitizeKey("foo/")).toBe("foo");
1924
});
2025

21-
it("preserves safe characters", () => {
22-
expect(sanitizeKey("foo-bar_baz123")).toBe("foo-bar_baz123");
26+
it("should keep valid characters", () => {
27+
expect(sanitizeKey("foo-bar_baz")).toBe("foo-bar_baz");
28+
expect(sanitizeKey("FooBar123")).toBe("FooBar123");
2329
});
2430
});
2531

@@ -29,63 +35,102 @@ describe("normalizePath", () => {
2935
expect(normalizePath("./foo/bar")).toBe("foo/bar");
3036
});
3137

32-
it("removes leading slashes", () => {
38+
it("removes leading /", () => {
3339
expect(normalizePath("/src")).toBe("src");
34-
expect(normalizePath("//src")).toBe("src");
40+
expect(normalizePath("///src")).toBe("src");
3541
});
3642

37-
it("removes trailing slashes", () => {
43+
it("removes trailing /", () => {
3844
expect(normalizePath("src/")).toBe("src");
39-
expect(normalizePath("src//")).toBe("src");
45+
expect(normalizePath("src///")).toBe("src");
4046
});
4147

4248
it("collapses multiple slashes", () => {
4349
expect(normalizePath("src//lib")).toBe("src/lib");
44-
expect(normalizePath("a///b//c")).toBe("a/b/c");
50+
expect(normalizePath("a//b//c")).toBe("a/b/c");
4551
});
4652

47-
it("returns empty string for root representations", () => {
53+
it("handles root paths", () => {
4854
expect(normalizePath("./")).toBe("");
4955
expect(normalizePath("/")).toBe("");
5056
expect(normalizePath("")).toBe("");
5157
});
58+
59+
it("handles complex paths", () => {
60+
expect(normalizePath("./src//lib/")).toBe("src/lib");
61+
expect(normalizePath("///a//b//c///")).toBe("a/b/c");
62+
});
5263
});
5364

5465
describe("buildClientUserAgent", () => {
55-
it("builds CLI user agent", () => {
66+
it("builds CLI User-Agent", () => {
5667
const ua = buildClientUserAgent("cli");
57-
expect(ua).toMatch(/^augment\.ctxc\.cli\/\d+\.\d+\.\d+/);
58-
});
59-
60-
it("builds SDK user agent", () => {
61-
const ua = buildClientUserAgent("sdk");
62-
expect(ua).toMatch(/^augment\.ctxc\.sdk\/\d+\.\d+\.\d+/);
68+
expect(ua).toMatch(/^augment\.ctxc\.cli\/[\d.]+/);
6369
});
6470

65-
it("builds MCP user agent without client info", () => {
71+
it("builds MCP User-Agent", () => {
6672
const ua = buildClientUserAgent("mcp");
67-
expect(ua).toMatch(/^augment\.ctxc\.mcp\/\d+\.\d+\.\d+$/);
68-
});
69-
70-
it("builds MCP user agent with client info", () => {
71-
const ua = buildClientUserAgent("mcp", { name: "claude-desktop", version: "1.0.0" });
72-
expect(ua).toMatch(/^augment\.ctxc\.mcp\/\d+\.\d+\.\d+\/claude-desktop\/1\.0\.0$/);
73-
});
74-
75-
it("builds MCP user agent with client name only", () => {
76-
const ua = buildClientUserAgent("mcp", { name: "cursor" });
77-
expect(ua).toMatch(/^augment\.ctxc\.mcp\/\d+\.\d+\.\d+\/cursor$/);
73+
expect(ua).toMatch(/^augment\.ctxc\.mcp\/[\d.]+/);
7874
});
7975

80-
it("sanitizes MCP client info - spaces replaced with dashes", () => {
81-
const ua = buildClientUserAgent("mcp", { name: "My App", version: "1.2.3" });
82-
// Space replaced with -
83-
expect(ua).toMatch(/\/My-App\/1\.2\.3$/);
76+
it("builds SDK User-Agent", () => {
77+
const ua = buildClientUserAgent("sdk");
78+
expect(ua).toMatch(/^augment\.ctxc\.sdk\/[\d.]+/);
79+
});
80+
81+
it("appends MCP client info with name and version", () => {
82+
const mcpClientInfo: MCPClientInfo = {
83+
name: "claude-desktop",
84+
version: "1.0.0"
85+
};
86+
const ua = buildClientUserAgent("mcp", mcpClientInfo);
87+
expect(ua).toMatch(/^augment\.ctxc\.mcp\/[\d.]+\/claude-desktop\/1\.0\.0$/);
88+
});
89+
90+
it("appends MCP client info with name only", () => {
91+
const mcpClientInfo: MCPClientInfo = {
92+
name: "cursor"
93+
};
94+
const ua = buildClientUserAgent("mcp", mcpClientInfo);
95+
expect(ua).toMatch(/^augment\.ctxc\.mcp\/[\d.]+\/cursor$/);
96+
});
97+
98+
it("sanitizes MCP client name per RFC 9110", () => {
99+
const mcpClientInfo: MCPClientInfo = {
100+
name: "My App", // space not allowed
101+
version: "1.0"
102+
};
103+
const ua = buildClientUserAgent("mcp", mcpClientInfo);
104+
expect(ua).toContain("/My-App/"); // space replaced with -
105+
});
106+
107+
it("truncates long client names to 32 chars", () => {
108+
const mcpClientInfo: MCPClientInfo = {
109+
name: "a".repeat(50),
110+
version: "1.0"
111+
};
112+
const ua = buildClientUserAgent("mcp", mcpClientInfo);
113+
// Should contain 32 'a's, not 50
114+
expect(ua).toContain("/" + "a".repeat(32) + "/");
115+
});
116+
117+
it("truncates long versions to 8 chars", () => {
118+
const mcpClientInfo: MCPClientInfo = {
119+
name: "app",
120+
version: "1.2.3-beta.4" // 12 chars
121+
};
122+
const ua = buildClientUserAgent("mcp", mcpClientInfo);
123+
// Version should be truncated to 8 chars: "1.2.3-be"
124+
expect(ua).toMatch(/\/app\/1\.2\.3-be$/);
84125
});
85126

86-
it("truncates long version strings", () => {
87-
const ua = buildClientUserAgent("mcp", { name: "app", version: "1.2.3-beta.1" });
88-
// Version truncated to 8 chars: "1.2.3-be"
89-
expect(ua).toMatch(/\/app\/1\.2\.3-be$/);
127+
it("ignores MCP client info for non-MCP products", () => {
128+
const mcpClientInfo: MCPClientInfo = {
129+
name: "should-be-ignored",
130+
version: "1.0"
131+
};
132+
const ua = buildClientUserAgent("cli", mcpClientInfo);
133+
expect(ua).not.toContain("should-be-ignored");
134+
expect(ua).toMatch(/^augment\.ctxc\.cli\/[\d.]+$/);
90135
});
91136
});

src/core/utils.ts

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,40 @@
11
/**
22
* Shared utility functions
33
*/
4-
import { createRequire } from "node:module";
5-
import { existsSync } from "node:fs";
4+
import { readFileSync } from "node:fs";
65
import { dirname, join } from "node:path";
76
import { fileURLToPath } from "node:url";
87

9-
// Load VERSION with fallback for resilience
10-
// In production/bundled environments, the generated file is always present
11-
// In development edge cases (direct tsx without prebuild), falls back to "unknown"
12-
let VERSION = "unknown";
13-
try {
14-
// Use createRequire for synchronous import that can be caught
15-
const require = createRequire(import.meta.url);
16-
const __dirname = dirname(fileURLToPath(import.meta.url));
17-
18-
// Try .js first (compiled), then .ts (source/dev)
19-
const jsPath = join(__dirname, "../generated/version.js");
20-
const tsPath = join(__dirname, "../generated/version.ts");
8+
// ============================================================================
9+
// Version utilities (matching auggie-sdk-typescript approach)
10+
// ============================================================================
11+
12+
let cachedVersion: string | undefined;
13+
14+
/**
15+
* Get the package version from package.json.
16+
* Matches auggie-sdk-typescript's getSDKVersion() approach.
17+
*/
18+
function getVersion(): string {
19+
if (cachedVersion) return cachedVersion!;
2120

22-
if (existsSync(jsPath)) {
23-
const versionModule = require("../generated/version.js");
24-
VERSION = versionModule.VERSION ?? "unknown";
25-
} else if (existsSync(tsPath)) {
26-
// In development/test with tsx, load the .ts file
27-
const versionModule = require("../generated/version.ts");
28-
VERSION = versionModule.VERSION ?? "unknown";
21+
try {
22+
const __dirname = dirname(fileURLToPath(import.meta.url));
23+
// In src/core/utils.ts -> ../.. to reach package.json
24+
// In dist/core/utils.js -> ../.. to reach package.json
25+
const packageJsonPath = join(__dirname, "..", "..", "package.json");
26+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
27+
cachedVersion = pkg.version || "unknown";
28+
} catch {
29+
cachedVersion = "unknown";
2930
}
30-
} catch {
31-
// Generated file doesn't exist or failed to load - use fallback
32-
// User-Agent will still identify the product: augment.ctxc.cli/unknown
31+
return cachedVersion!;
3332
}
3433

34+
// ============================================================================
35+
// Path/string utilities
36+
// ============================================================================
37+
3538
/**
3639
* Sanitize a key for use in filenames/paths.
3740
* Replaces unsafe characters with underscores.
@@ -101,10 +104,10 @@ export interface MCPClientInfo {
101104

102105
/**
103106
* Sanitize a string for use in User-Agent per RFC 9110.
104-
* Only allows: a-z A-Z 0-9 \! # $ % & ' * + . ^ _ \` | ~ -
107+
* Only allows: a-z A-Z 0-9 \! # $ % & ' * + . ^ _ ` | ~ -
105108
*/
106109
function sanitizeUserAgentToken(s: string, maxLen: number): string {
107-
return s.replace(/[^a-zA-Z0-9\!#$%&'*+.^_\`|~-]/g, "-").slice(0, maxLen);
110+
return s.replace(/[^a-zA-Z0-9\!#$%&'*+.^_`|~-]/g, "-").slice(0, maxLen);
108111
}
109112

110113
/**
@@ -133,8 +136,8 @@ export function buildClientUserAgent(
133136
product: ClientProduct,
134137
mcpClientInfo?: MCPClientInfo
135138
): string {
136-
137-
const base = `augment.ctxc.${product}/${VERSION}`;
139+
const version = getVersion();
140+
const base = `augment.ctxc.${product}/${version}`;
138141

139142
if (product === 'mcp' && mcpClientInfo) {
140143
// Sanitize MCP client info per RFC 9110 (same as auggie CLI)

0 commit comments

Comments
 (0)