Skip to content

Commit cf1b2e0

Browse files
committed
refactor: Simplify User-Agent format to product-based
Use augment.ctxc.{product}/{version} format: - augment.ctxc.cli/{version} for all CLI commands (search, agent, index) - augment.ctxc.mcp/{version} for MCP server - augment.ctxc.sdk/{version} for SDK/programmatic usage MCP with client info: augment.ctxc.mcp/{version}/{clientName}/{clientVersion}
1 parent 7065f6d commit cf1b2e0

File tree

6 files changed

+107
-43
lines changed

6 files changed

+107
-43
lines changed

src/bin/cmd-agent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const agentCommand = new Command("agent")
5252

5353
// Create multi-index runner
5454
// Build User-Agent for analytics tracking
55-
const clientUserAgent = buildClientUserAgent("cli-agent");
55+
const clientUserAgent = buildClientUserAgent("cli");
5656

5757
const runner = await MultiIndexRunner.create({
5858
store,

src/bin/cmd-index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ async function runIndex(
5151
) {
5252
console.log(`Indexing ${sourceType} source...`);
5353
const indexer = new Indexer({
54-
clientUserAgent: buildClientUserAgent("cli-index"),
54+
clientUserAgent: buildClientUserAgent("cli"),
5555
});
5656
const result = await indexer.index(source, store, indexKey);
5757

src/bin/cmd-search.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export const searchCommand = new Command("search")
8484
const client = new SearchClient({
8585
store,
8686
indexName: indexKey,
87-
clientUserAgent: buildClientUserAgent("cli-search"),
87+
clientUserAgent: buildClientUserAgent("cli"),
8888
});
8989

9090
await client.initialize();

src/core/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export {
2121
} from "./file-filter.js";
2222

2323
export { sanitizeKey, isoTimestamp, buildClientUserAgent } from "./utils.js";
24-
export type { ClientInterface, MCPClientInfo } from "./utils.js";
24+
export type { ClientProduct, MCPClientInfo } from "./utils.js";
2525

2626
export { Indexer } from "./indexer.js";
2727
export type { IndexerConfig } from "./indexer.js";

src/core/utils.test.ts

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,91 @@
1-
import { describe, expect, it } from "vitest";
2-
import { buildClientUserAgent } from "./utils.js";
1+
import { describe, it, expect } from "vitest";
2+
import { sanitizeKey, normalizePath, buildClientUserAgent } from "./utils.js";
3+
4+
describe("sanitizeKey", () => {
5+
it("replaces unsafe characters with underscores", () => {
6+
expect(sanitizeKey("foo/bar/baz")).toBe("foo_bar_baz");
7+
expect(sanitizeKey("foo:bar")).toBe("foo_bar");
8+
expect(sanitizeKey("foo@bar.com")).toBe("foo_bar_com");
9+
});
10+
11+
it("collapses multiple underscores", () => {
12+
expect(sanitizeKey("foo//bar")).toBe("foo_bar");
13+
expect(sanitizeKey("foo___bar")).toBe("foo_bar");
14+
});
15+
16+
it("strips leading and trailing underscores", () => {
17+
expect(sanitizeKey("_foo_")).toBe("foo");
18+
expect(sanitizeKey("__foo__")).toBe("foo");
19+
});
20+
21+
it("preserves safe characters", () => {
22+
expect(sanitizeKey("foo-bar_baz123")).toBe("foo-bar_baz123");
23+
});
24+
});
25+
26+
describe("normalizePath", () => {
27+
it("removes leading ./", () => {
28+
expect(normalizePath("./src")).toBe("src");
29+
expect(normalizePath("./foo/bar")).toBe("foo/bar");
30+
});
31+
32+
it("removes leading slashes", () => {
33+
expect(normalizePath("/src")).toBe("src");
34+
expect(normalizePath("//src")).toBe("src");
35+
});
36+
37+
it("removes trailing slashes", () => {
38+
expect(normalizePath("src/")).toBe("src");
39+
expect(normalizePath("src//")).toBe("src");
40+
});
41+
42+
it("collapses multiple slashes", () => {
43+
expect(normalizePath("src//lib")).toBe("src/lib");
44+
expect(normalizePath("a///b//c")).toBe("a/b/c");
45+
});
46+
47+
it("returns empty string for root representations", () => {
48+
expect(normalizePath("./")).toBe("");
49+
expect(normalizePath("/")).toBe("");
50+
expect(normalizePath("")).toBe("");
51+
});
52+
});
353

454
describe("buildClientUserAgent", () => {
5-
it("should build User-Agent for CLI search", () => {
6-
const ua = buildClientUserAgent("cli-search");
7-
expect(ua).toMatch(/^augment.ctxc\/[0-9]+\.[0-9]+\.[0-9]+\/cli-search$/);
55+
it("builds CLI user agent", () => {
56+
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+/);
863
});
964

10-
it("should build User-Agent for MCP without client info", () => {
65+
it("builds MCP user agent without client info", () => {
1166
const ua = buildClientUserAgent("mcp");
12-
expect(ua).toMatch(/^augment.ctxc\/[0-9]+\.[0-9]+\.[0-9]+\/mcp$/);
67+
expect(ua).toMatch(/^augment\.ctxc\.mcp\/\d+\.\d+\.\d+$/);
1368
});
1469

15-
it("should build User-Agent for MCP with client info", () => {
70+
it("builds MCP user agent with client info", () => {
1671
const ua = buildClientUserAgent("mcp", { name: "claude-desktop", version: "1.0.0" });
17-
expect(ua).toMatch(/^augment.ctxc\/[0-9]+\.[0-9]+\.[0-9]+\/mcp\/claude-desktop\/1\.0\.0$/);
72+
expect(ua).toMatch(/^augment\.ctxc\.mcp\/\d+\.\d+\.\d+\/claude-desktop\/1\.0\.0$/);
1873
});
1974

20-
it("should build User-Agent for MCP with client name only", () => {
75+
it("builds MCP user agent with client name only", () => {
2176
const ua = buildClientUserAgent("mcp", { name: "cursor" });
22-
expect(ua).toMatch(/^augment.ctxc\/[0-9]+\.[0-9]+\.[0-9]+\/mcp\/cursor$/);
77+
expect(ua).toMatch(/^augment\.ctxc\.mcp\/\d+\.\d+\.\d+\/cursor$/);
78+
});
79+
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$/);
2384
});
2485

25-
it("should sanitize MCP client info", () => {
26-
const ua = buildClientUserAgent("mcp", { name: "My App 2.0", version: "1.2.3-beta" });
27-
// Spaces and other chars should be replaced with -
28-
expect(ua).toMatch(/^augment.ctxc\/[0-9]+\.[0-9]+\.[0-9]+\/mcp\/My-App-2\.0\/1\.2\.3-be$/);
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$/);
2990
});
3091
});

src/core/utils.ts

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,15 @@ export function normalizePath(path: string): string {
5252
// ============================================================================
5353

5454
/**
55-
* Supported client interface types for User-Agent tracking.
55+
* Client product types for User-Agent tracking.
5656
*
57-
* Format matches auggie CLI: augment.ctxc/{version}/{interface}
57+
* Format: augment.ctxc.{product}/{version}
58+
*
59+
* - cli: CLI commands (ctxc search, ctxc agent, ctxc index)
60+
* - mcp: MCP server mode (ctxc mcp)
61+
* - sdk: SDK/programmatic usage
5862
*/
59-
export type ClientInterface =
60-
| 'cli-search' // ctxc search command
61-
| 'sdk-search' // SearchClient programmatic use
62-
| 'cli-index' // ctxc index command
63-
| 'sdk-index' // Indexer programmatic use
64-
| 'mcp' // ctxc mcp command (MCP server mode)
65-
| 'cli-agent' // ctxc agent command
66-
| 'sdk-agent-provider'; // Vercel AI SDK interface
63+
export type ClientProduct = 'cli' | 'mcp' | 'sdk';
6764

6865
/**
6966
* MCP client information from the initialize request.
@@ -90,44 +87,50 @@ function getVersion(): string {
9087

9188
/**
9289
* Sanitize a string for use in User-Agent per RFC 9110.
93-
* Only allows: a-z A-Z 0-9 ! # $ % & ' * + . ^ _ ` | ~ -
90+
* Only allows: a-z A-Z 0-9 \! # $ % & ' * + . ^ _ ` | ~ -
9491
*/
9592
function sanitizeUserAgentToken(s: string, maxLen: number): string {
96-
return s.replace(/[^a-zA-Z0-9!#$%&'*+.^_`|~-]/g, "-").slice(0, maxLen);
93+
return s.replace(/[^a-zA-Z0-9\!#$%&'*+.^_`|~-]/g, "-").slice(0, maxLen);
9794
}
9895

9996
/**
10097
* Build a User-Agent string for analytics tracking.
10198
*
102-
* Format matches auggie CLI style: augment.ctxc/{version}/{interface}
103-
* With MCP client: augment.ctxc/{version}/mcp/{clientName}
99+
* Simplified format:
100+
* - CLI: augment.ctxc.cli/{version}
101+
* - MCP: augment.ctxc.mcp/{version}
102+
* - SDK: augment.ctxc.sdk/{version}
103+
*
104+
* With MCP client info:
105+
* - augment.ctxc.mcp/{version}/{clientName}/{clientVersion}
104106
*
105-
* @param clientInterface - The interface being used
106-
* @param mcpClientInfo - Optional MCP client info for mcp interface
107+
* @param product - The product being used (cli, mcp, sdk)
108+
* @param mcpClientInfo - Optional MCP client info
107109
* @returns User-Agent string for the request
108110
*
109111
* @example
110-
* buildClientUserAgent('cli-search')
111-
* // => 'augment.ctxc/0.1.3/cli-search'
112+
* buildClientUserAgent('cli')
113+
* // => 'augment.ctxc.cli/0.1.3'
112114
*
113115
* buildClientUserAgent('mcp', { name: 'claude-desktop', version: '1.0.0' })
114-
* // => 'augment.ctxc/0.1.3/mcp/claude-desktop/1.0.0'
116+
* // => 'augment.ctxc.mcp/0.1.3/claude-desktop/1.0.0'
115117
*/
116118
export function buildClientUserAgent(
117-
clientInterface: ClientInterface,
119+
product: ClientProduct,
118120
mcpClientInfo?: MCPClientInfo
119121
): string {
120122
const version = getVersion();
123+
const base = `augment.ctxc.${product}/${version}`;
121124

122-
if (clientInterface === 'mcp' && mcpClientInfo) {
125+
if (product === 'mcp' && mcpClientInfo) {
123126
// Sanitize MCP client info per RFC 9110 (same as auggie CLI)
124127
const name = sanitizeUserAgentToken(mcpClientInfo.name, 32);
125128
const clientVersion = mcpClientInfo.version
126129
? sanitizeUserAgentToken(mcpClientInfo.version, 8)
127130
: undefined;
128-
const clientName = clientVersion ? `${name}/${clientVersion}` : name;
129-
return `augment.ctxc/${version}/mcp/${clientName}`;
131+
const clientSuffix = clientVersion ? `${name}/${clientVersion}` : name;
132+
return `${base}/${clientSuffix}`;
130133
}
131134

132-
return `augment.ctxc/${version}/${clientInterface}`;
135+
return base;
133136
}

0 commit comments

Comments
 (0)