agent-dir 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +3 -2
  2. package/dist/bin/agent-dir.d.ts +0 -1
  3. package/dist/bin/agent-dir.js +11 -4
  4. package/dist/src/auth.d.ts +0 -1
  5. package/dist/src/auth.js +0 -1
  6. package/dist/src/codegraph.d.ts +0 -1
  7. package/dist/src/codegraph.js +0 -1
  8. package/dist/src/config.d.ts +0 -1
  9. package/dist/src/config.js +0 -1
  10. package/dist/src/logging.d.ts +0 -1
  11. package/dist/src/logging.js +1 -2
  12. package/dist/src/mcp.d.ts +0 -1
  13. package/dist/src/mcp.js +11 -2
  14. package/dist/src/server.d.ts +0 -1
  15. package/dist/src/server.js +120 -13
  16. package/dist/src/setup.d.ts +0 -1
  17. package/dist/src/setup.js +4 -3
  18. package/dist/src/telemetry/aggregate.d.ts +7 -1
  19. package/dist/src/telemetry/aggregate.js +39 -3
  20. package/dist/src/telemetry/config.d.ts +0 -1
  21. package/dist/src/telemetry/config.js +0 -1
  22. package/dist/src/telemetry/format.d.ts +0 -1
  23. package/dist/src/telemetry/format.js +1 -2
  24. package/dist/src/telemetry/privacy.d.ts +0 -1
  25. package/dist/src/telemetry/privacy.js +77 -3
  26. package/dist/src/telemetry/recorder.d.ts +0 -1
  27. package/dist/src/telemetry/recorder.js +0 -1
  28. package/dist/src/telemetry/types.d.ts +29 -3
  29. package/dist/src/telemetry/types.js +1 -2
  30. package/dist/src/tools/code.d.ts +0 -1
  31. package/dist/src/tools/code.js +0 -1
  32. package/dist/src/tools/commands.d.ts +0 -1
  33. package/dist/src/tools/commands.js +0 -1
  34. package/dist/src/tools/diagnostics.d.ts +0 -1
  35. package/dist/src/tools/diagnostics.js +0 -1
  36. package/dist/src/tools/files.d.ts +0 -1
  37. package/dist/src/tools/files.js +0 -1
  38. package/dist/src/tools/git.d.ts +0 -1
  39. package/dist/src/tools/git.js +0 -1
  40. package/dist/src/tools/metadata.d.ts +0 -1
  41. package/dist/src/tools/metadata.js +0 -1
  42. package/dist/src/tools/npm.d.ts +0 -1
  43. package/dist/src/tools/npm.js +0 -1
  44. package/dist/src/tools/skills.d.ts +0 -1
  45. package/dist/src/tools/skills.js +0 -1
  46. package/dist/src/tunnel.d.ts +1 -2
  47. package/dist/src/tunnel.js +266 -84
  48. package/dist/src/types.d.ts +27 -1
  49. package/dist/src/types.js +0 -1
  50. package/package.json +2 -2
  51. package/dist/bin/agent-dir.d.ts.map +0 -1
  52. package/dist/bin/agent-dir.js.map +0 -1
  53. package/dist/src/auth.d.ts.map +0 -1
  54. package/dist/src/auth.js.map +0 -1
  55. package/dist/src/codegraph.d.ts.map +0 -1
  56. package/dist/src/codegraph.js.map +0 -1
  57. package/dist/src/config.d.ts.map +0 -1
  58. package/dist/src/config.js.map +0 -1
  59. package/dist/src/logging.d.ts.map +0 -1
  60. package/dist/src/logging.js.map +0 -1
  61. package/dist/src/mcp.d.ts.map +0 -1
  62. package/dist/src/mcp.js.map +0 -1
  63. package/dist/src/server.d.ts.map +0 -1
  64. package/dist/src/server.js.map +0 -1
  65. package/dist/src/setup.d.ts.map +0 -1
  66. package/dist/src/setup.js.map +0 -1
  67. package/dist/src/telemetry/aggregate.d.ts.map +0 -1
  68. package/dist/src/telemetry/aggregate.js.map +0 -1
  69. package/dist/src/telemetry/config.d.ts.map +0 -1
  70. package/dist/src/telemetry/config.js.map +0 -1
  71. package/dist/src/telemetry/format.d.ts.map +0 -1
  72. package/dist/src/telemetry/format.js.map +0 -1
  73. package/dist/src/telemetry/privacy.d.ts.map +0 -1
  74. package/dist/src/telemetry/privacy.js.map +0 -1
  75. package/dist/src/telemetry/recorder.d.ts.map +0 -1
  76. package/dist/src/telemetry/recorder.js.map +0 -1
  77. package/dist/src/telemetry/types.d.ts.map +0 -1
  78. package/dist/src/telemetry/types.js.map +0 -1
  79. package/dist/src/tools/code.d.ts.map +0 -1
  80. package/dist/src/tools/code.js.map +0 -1
  81. package/dist/src/tools/commands.d.ts.map +0 -1
  82. package/dist/src/tools/commands.js.map +0 -1
  83. package/dist/src/tools/diagnostics.d.ts.map +0 -1
  84. package/dist/src/tools/diagnostics.js.map +0 -1
  85. package/dist/src/tools/files.d.ts.map +0 -1
  86. package/dist/src/tools/files.js.map +0 -1
  87. package/dist/src/tools/git.d.ts.map +0 -1
  88. package/dist/src/tools/git.js.map +0 -1
  89. package/dist/src/tools/metadata.d.ts.map +0 -1
  90. package/dist/src/tools/metadata.js.map +0 -1
  91. package/dist/src/tools/npm.d.ts.map +0 -1
  92. package/dist/src/tools/npm.js.map +0 -1
  93. package/dist/src/tools/skills.d.ts.map +0 -1
  94. package/dist/src/tools/skills.js.map +0 -1
  95. package/dist/src/tunnel.d.ts.map +0 -1
  96. package/dist/src/tunnel.js.map +0 -1
  97. package/dist/src/types.d.ts.map +0 -1
  98. package/dist/src/types.js.map +0 -1
package/README.md CHANGED
@@ -51,7 +51,7 @@ The Wormhole URL is only the transport endpoint. `agent-dir` authentication is e
51
51
 
52
52
  On the first interactive run, when no `agent-dir` config exists, the CLI walks you through creating your first saved profile. It asks for the profile name, directory, port, tunnel, optional Wormhole subdomain, optional npm scripts, optional allowed commands, and telemetry preference. The generated authentication token is saved with the profile.
53
53
 
54
- Telemetry is optional and stays local. During setup, `agent-dir` explains the available levels: **none**, **anonymous**, **basic**, and **detailed**. Telemetry is disabled by default if you choose the default option. The same choices can later be changed with `agent-dir telemetry enable|disable`.
54
+ Telemetry is optional and stays local. During setup, `agent-dir` explains the available levels: **none**, **anonymous**, **basic**, **detailed**, and **diagnostic**. Telemetry is disabled by default if you choose the default option. The same choices can later be changed with `agent-dir telemetry enable|disable`.
55
55
 
56
56
  You can also run the setup wizard explicitly at any time:
57
57
 
@@ -130,6 +130,7 @@ Enable a privacy level with:
130
130
  agent-dir telemetry enable anonymous
131
131
  agent-dir telemetry enable basic
132
132
  agent-dir telemetry enable detailed
133
+ agent-dir telemetry enable diagnostic
133
134
  ```
134
135
 
135
136
  Levels add bounded operational context:
@@ -138,7 +139,7 @@ Levels add bounded operational context:
138
139
  - **basic** — anonymous data plus bounded Agent Dir/Node/platform and MCP client version information.
139
140
  - **detailed** — basic data plus coarse project classification such as language, package manager, Git/CodeGraph availability, and project size.
140
141
 
141
- Telemetry never records file contents, command arguments, authentication tokens, environment variables, or project paths. Session telemetry separates wall-clock session lifetime from active MCP request time and the gaps between requests; those gaps may include agent reasoning, network delay, or other idle time and are not presented as agent thinking time. Persistence failures do not affect MCP requests.
142
+ Standard telemetry levels never record file contents, command arguments, authentication tokens, environment variables, or project paths. The opt-in diagnostic level additionally records local troubleshooting metadata such as project paths, hostname, username, process identifiers, Node version, memory usage, and process uptime; it remains local and is never uploaded by Agent Dir. Session telemetry separates wall-clock session lifetime from active MCP request time and the gaps between requests; those gaps may include agent reasoning, network delay, or other idle time and are not presented as agent thinking time. Persistence failures do not affect MCP requests.
142
143
 
143
144
  Manage telemetry with:
144
145
 
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env node
2
2
  export {};
3
- //# sourceMappingURL=agent-dir.d.ts.map
@@ -100,7 +100,7 @@ if (command === "telemetry") {
100
100
  if (action === "enable") {
101
101
  const level = positionals[2];
102
102
  if (!level || !isTelemetryLevel(level) || level === "none")
103
- fail("Usage: agent-dir telemetry enable <anonymous|basic|detailed>");
103
+ fail("Usage: agent-dir telemetry enable <anonymous|basic|detailed|diagnostic>");
104
104
  config.telemetry = { level };
105
105
  await saveConfig(config);
106
106
  console.log(`✓ Telemetry enabled at ${level} level.`);
@@ -125,7 +125,8 @@ if (command === "telemetry") {
125
125
  "commandCallCount",
126
126
  ],
127
127
  envelope: ["configId", "sessionId", "timestamp", "level", "event"],
128
- events: ["mcp_request", "tool_call", "command_call", "session"],
128
+ events: ["mcp_request", "tool_call", "command_call", "tunnel", "session"],
129
+ levels: ["none", "anonymous", "basic", "detailed", "diagnostic"],
129
130
  successMetrics: {
130
131
  mcp: "successful mcp_request events / mcp_request events",
131
132
  tool: "successful tool_call events / tool_call events",
@@ -406,6 +407,12 @@ if (tunnel !== "none") {
406
407
  tunnelResult = await startTunnel({
407
408
  provider: "wormhole",
408
409
  port,
410
+ token,
411
+ telemetry: {
412
+ ...(config.telemetry ?? { level: "none" }),
413
+ ...(telemetryConfigId ? { configId: telemetryConfigId } : {}),
414
+ persist: true,
415
+ },
409
416
  random: randomTunnel,
410
417
  ...(subdomain && !randomTunnel ? { subdomain } : {}),
411
418
  });
@@ -423,7 +430,8 @@ const shutdown = async () => {
423
430
  return;
424
431
  shuttingDown = true;
425
432
  console.log("\n Stopping agent-dir...");
426
- if (tunnelResult?.child && !tunnelResult.child.killed)
433
+ tunnelResult?.stop?.();
434
+ if (tunnelResult?.child && !tunnelResult.stop && !tunnelResult.child.killed)
427
435
  tunnelResult.child.kill("SIGTERM");
428
436
  await server.close();
429
437
  process.exit(0);
@@ -456,4 +464,3 @@ function readTelemetryEvents(path) {
456
464
  return [];
457
465
  }
458
466
  }
459
- //# sourceMappingURL=agent-dir.js.map
@@ -1,3 +1,2 @@
1
1
  export declare function authenticate(request: Request, token: string): boolean;
2
2
  export declare function unauthorized(): Response;
3
- //# sourceMappingURL=auth.d.ts.map
package/dist/src/auth.js CHANGED
@@ -23,4 +23,3 @@ export function unauthorized() {
23
23
  },
24
24
  });
25
25
  }
26
- //# sourceMappingURL=auth.js.map
@@ -30,4 +30,3 @@ export declare class CodeGraphIntegration {
30
30
  private handleProcessFailure;
31
31
  private rejectPending;
32
32
  }
33
- //# sourceMappingURL=codegraph.d.ts.map
@@ -262,4 +262,3 @@ function safeEnvironment() {
262
262
  }
263
263
  return environment;
264
264
  }
265
- //# sourceMappingURL=codegraph.js.map
@@ -3,4 +3,3 @@ export declare function loadConfig(): Promise<AgentConfig>;
3
3
  export declare function saveConfig(config: AgentConfig): Promise<void>;
4
4
  export declare function configPath(): string;
5
5
  export declare function mergeProfile(profile: Profile, overrides?: CommandConfig): CommandConfig;
6
- //# sourceMappingURL=config.d.ts.map
@@ -91,4 +91,3 @@ function isProfile(value) {
91
91
  return false;
92
92
  return true;
93
93
  }
94
- //# sourceMappingURL=config.js.map
@@ -15,4 +15,3 @@ export declare function printBanner(options: {
15
15
  export declare function printRequestLog(entry: RequestLog): void;
16
16
  export declare function logRequest(entry: RequestLog): void;
17
17
  export declare function printRequestHeader(): void;
18
- //# sourceMappingURL=logging.d.ts.map
@@ -132,7 +132,7 @@ export function printHelp() {
132
132
  writeLine("");
133
133
  writeLine("Telemetry:");
134
134
  writeLine(" agent-dir telemetry status");
135
- writeLine(" agent-dir telemetry enable <anonymous|basic|detailed>");
135
+ writeLine(" agent-dir telemetry enable <anonymous|basic|detailed|diagnostic>");
136
136
  writeLine(" agent-dir telemetry disable");
137
137
  writeLine(" agent-dir telemetry schema");
138
138
  writeLine(" agent-dir telemetry show [--follow]");
@@ -267,4 +267,3 @@ export function printRequestHeader() {
267
267
  writeLine(" [Request logs]");
268
268
  writeLine(" ──────────────────────────────────────────────────────────────");
269
269
  }
270
- //# sourceMappingURL=logging.js.map
package/dist/src/mcp.d.ts CHANGED
@@ -9,4 +9,3 @@ interface McpHandler {
9
9
  export declare function createMcpHandler(root: string, commandConfig?: CommandConfig, telemetry?: TelemetryConfig): McpHandler;
10
10
  export declare function validateMcpParamHeaders(inputSchema: Record<string, unknown>, args: Record<string, unknown>, headers: Headers): string | undefined;
11
11
  export {};
12
- //# sourceMappingURL=mcp.d.ts.map
package/dist/src/mcp.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createRequire } from "node:module";
3
+ import { hostname, userInfo } from "node:os";
3
4
  import { CodeGraphIntegration } from "./codegraph.js";
4
5
  import { TelemetryRecorder } from "./telemetry/recorder.js";
5
6
  import { findDefinition, findExports, findFiles, findImports, findReferences, findSymbol, searchCode, searchFiles, } from "./tools/code.js";
@@ -344,7 +345,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
344
345
  let sessionToolCallCount = 0;
345
346
  let sessionCommandCallCount = 0;
346
347
  let sessionRecorded = false;
347
- const telemetryProjectPromise = telemetry.level === "detailed"
348
+ const telemetryProjectPromise = telemetry.level === "detailed" || telemetry.level === "diagnostic"
348
349
  ? buildTelemetryProjectContext(root, codeGraph).catch(() => undefined)
349
350
  : undefined;
350
351
  const npmAllowed = commandConfig.npm?.allowedScripts ?? [];
@@ -1552,6 +1553,13 @@ function telemetryRuntime(params) {
1552
1553
  arch: process.arch,
1553
1554
  ...(typeof clientInfo?.name === "string" ? { mcpClientName: clientInfo.name } : {}),
1554
1555
  ...(typeof clientInfo?.version === "string" ? { mcpClientVersion: clientInfo.version } : {}),
1556
+ nodeVersion: process.versions.node,
1557
+ hostname: hostname(),
1558
+ username: userInfo().username,
1559
+ processId: process.pid,
1560
+ parentProcessId: process.ppid,
1561
+ processUptimeMs: Math.round(process.uptime() * 1000),
1562
+ memoryRssBytes: process.memoryUsage().rss,
1555
1563
  };
1556
1564
  }
1557
1565
  function toolTelemetryMetrics(toolName, result) {
@@ -1618,6 +1626,8 @@ async function buildTelemetryProjectContext(root, codeGraph) {
1618
1626
  const projectSize = overview.files < 100 ? "small" : overview.files < 1000 ? "medium" : "large";
1619
1627
  const capability = await codeGraph.capability();
1620
1628
  return {
1629
+ projectRoot: root,
1630
+ workingDirectory: process.cwd(),
1621
1631
  ...(language ? { language } : {}),
1622
1632
  ...(packageManager && ["npm", "pnpm", "yarn", "bun"].includes(packageManager)
1623
1633
  ? { packageManager: packageManager }
@@ -1871,4 +1881,3 @@ function json(value, status = 200) {
1871
1881
  headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" },
1872
1882
  });
1873
1883
  }
1874
- //# sourceMappingURL=mcp.js.map
@@ -8,4 +8,3 @@ export declare function getMcpLog(body: Buffer): {
8
8
  name: string;
9
9
  detail: string;
10
10
  } | undefined;
11
- //# sourceMappingURL=server.d.ts.map
@@ -2,22 +2,44 @@ import http, {} from "node:http";
2
2
  import { authenticate, unauthorized } from "./auth.js";
3
3
  import { logRequest } from "./logging.js";
4
4
  import { createMcpHandler } from "./mcp.js";
5
+ import { TelemetryRecorder } from "./telemetry/recorder.js";
5
6
  import { deleteFile, listFiles, readFile, writeFile } from "./tools/files.js";
6
7
  const MAX_BODY_BYTES = 10 * 1024 * 1024;
8
+ const REQUEST_BODY_TIMEOUT_MS = 30_000;
9
+ const REQUEST_TIMEOUT_MS = 120_000;
10
+ const HEADERS_TIMEOUT_MS = 30_000;
11
+ const KEEP_ALIVE_TIMEOUT_MS = 5_000;
12
+ const HEALTH_PATH = "/__health";
7
13
  export function startServer({ root, port, token, commandConfig = {}, telemetry = { level: "none" }, }) {
8
14
  const mcp = createMcpHandler(root, commandConfig, telemetry);
15
+ const httpTelemetry = new TelemetryRecorder({
16
+ level: telemetry.level,
17
+ persist: telemetry.persist ?? false,
18
+ ...(telemetry.configId ? { configId: telemetry.configId } : {}),
19
+ sessionId: `http-${Date.now()}`,
20
+ });
9
21
  const server = http.createServer(async (req, res) => {
22
+ const startedAt = performance.now();
23
+ const requestController = new AbortController();
24
+ req.once("aborted", () => requestController.abort());
25
+ res.once("close", () => {
26
+ if (!res.writableFinished)
27
+ requestController.abort();
28
+ });
10
29
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
30
+ let authenticated = false;
11
31
  try {
12
- const body = await readBody(req);
32
+ const body = await readBody(req, requestController.signal);
13
33
  const requestInit = {
14
34
  method: req.method ?? "GET",
15
35
  headers: toFetchHeaders(req.headers),
36
+ signal: requestController.signal,
16
37
  };
17
38
  if (body.length)
18
39
  requestInit.body = body;
19
40
  const request = new Request(url, requestInit);
20
- if (!authenticate(request, token)) {
41
+ authenticated = authenticate(request, token);
42
+ if (!authenticated) {
21
43
  const response = unauthorized();
22
44
  await sendResponse(res, response);
23
45
  logRequest({
@@ -26,6 +48,11 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
26
48
  status: 401,
27
49
  detail: "unauthorized",
28
50
  });
51
+ recordHttpTelemetry(httpTelemetry, req.method ?? "UNKNOWN", url.pathname, 401, authenticated, startedAt);
52
+ return;
53
+ }
54
+ if (url.pathname === HEALTH_PATH && req.method === "HEAD") {
55
+ sendText(res, 200, "ok\n");
29
56
  return;
30
57
  }
31
58
  if (url.pathname === "/mcp") {
@@ -38,6 +65,7 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
38
65
  status: 405,
39
66
  detail: "method not allowed",
40
67
  });
68
+ recordHttpTelemetry(httpTelemetry, req.method ?? "UNKNOWN", url.pathname, 405, authenticated, startedAt);
41
69
  return;
42
70
  }
43
71
  const headerError = validateMcpHeaders(request, messageBody(body));
@@ -50,6 +78,7 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
50
78
  status: headerError.status,
51
79
  detail: errorDetail ? `MCP header validation: ${errorDetail}` : "MCP header validation",
52
80
  });
81
+ recordHttpTelemetry(httpTelemetry, req.method, url.pathname, headerError.status, authenticated, startedAt);
53
82
  return;
54
83
  }
55
84
  const response = await mcp(request);
@@ -66,12 +95,14 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
66
95
  : (mcpLog?.detail ?? "MCP"),
67
96
  ...(mcpLog ? { tool: mcpLog.name } : {}),
68
97
  });
98
+ recordHttpTelemetry(httpTelemetry, req.method, url.pathname, response.status, authenticated, startedAt);
69
99
  return;
70
100
  }
71
101
  if (url.pathname === "/__tree" && req.method === "GET") {
72
102
  const tree = await listFiles(root);
73
103
  sendText(res, 200, JSON.stringify(tree, null, 2), "application/json; charset=utf-8");
74
104
  logRequest({ method: req.method, path: url.pathname, status: 200, detail: "directory" });
105
+ recordHttpTelemetry(httpTelemetry, req.method, url.pathname, 200, authenticated, startedAt);
75
106
  return;
76
107
  }
77
108
  const relative = decodeURIComponent(url.pathname.slice(1));
@@ -83,23 +114,32 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
83
114
  status: 200,
84
115
  detail: "server",
85
116
  });
117
+ recordHttpTelemetry(httpTelemetry, req.method ?? "UNKNOWN", url.pathname, 200, authenticated, startedAt);
86
118
  return;
87
119
  }
88
120
  if (req.method === "GET") {
89
121
  sendText(res, 200, await readFile(root, relative), "text/plain; charset=utf-8");
90
122
  logRequest({ method: req.method, path: url.pathname, status: 200, detail: "REST GET" });
123
+ recordHttpTelemetry(httpTelemetry, req.method, url.pathname, 200, authenticated, startedAt);
91
124
  return;
92
125
  }
93
126
  if (req.method === "PUT") {
94
127
  await writeFile(root, relative, body.toString("utf8"));
95
128
  sendText(res, 200, "File updated successfully\n");
96
129
  logRequest({ method: req.method, path: url.pathname, status: 200, detail: "REST PUT" });
130
+ recordHttpTelemetry(httpTelemetry, req.method, url.pathname, 200, authenticated, startedAt);
97
131
  return;
98
132
  }
99
133
  if (req.method === "DELETE") {
100
134
  await deleteFile(root, relative);
101
135
  sendText(res, 200, "File deleted successfully\n");
102
- logRequest({ method: req.method, path: url.pathname, status: 200, detail: "REST DELETE" });
136
+ logRequest({
137
+ method: req.method,
138
+ path: url.pathname,
139
+ status: 200,
140
+ detail: "REST DELETE",
141
+ });
142
+ recordHttpTelemetry(httpTelemetry, req.method, url.pathname, 200, authenticated, startedAt);
103
143
  return;
104
144
  }
105
145
  sendText(res, 404, "Not found\n");
@@ -109,10 +149,19 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
109
149
  status: 404,
110
150
  detail: "not found",
111
151
  });
152
+ recordHttpTelemetry(httpTelemetry, req.method ?? "UNKNOWN", url.pathname, 404, authenticated, startedAt);
112
153
  }
113
154
  catch (error) {
155
+ if (requestController.signal.aborted || res.destroyed)
156
+ return;
114
157
  const code = error.code;
115
- const status = code === "ENOENT" ? 404 : code === "PAYLOAD_TOO_LARGE" ? 413 : 500;
158
+ const status = code === "ENOENT"
159
+ ? 404
160
+ : code === "PAYLOAD_TOO_LARGE"
161
+ ? 413
162
+ : code === "REQUEST_TIMEOUT"
163
+ ? 408
164
+ : 500;
116
165
  sendText(res, status, `${error instanceof Error ? error.message : String(error)}\n`);
117
166
  logRequest({
118
167
  method: req.method ?? "UNKNOWN",
@@ -120,8 +169,12 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
120
169
  status,
121
170
  detail: error instanceof Error ? error.message : String(error),
122
171
  });
172
+ recordHttpTelemetry(httpTelemetry, req.method ?? "UNKNOWN", url.pathname, status, authenticated, startedAt);
123
173
  }
124
174
  });
175
+ server.requestTimeout = REQUEST_TIMEOUT_MS;
176
+ server.headersTimeout = HEADERS_TIMEOUT_MS;
177
+ server.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS;
125
178
  return new Promise((resolve, reject) => {
126
179
  server.once("error", reject);
127
180
  server.listen(port, "127.0.0.1", () => resolve({
@@ -480,6 +533,26 @@ function formatCommand(command, args) {
480
533
  const count = Array.isArray(args) ? args.length : 0;
481
534
  return count ? `${command} (args: ${count})` : command;
482
535
  }
536
+ function recordHttpTelemetry(recorder, method, pathname, status, authenticated, startedAt) {
537
+ const route = pathname === "/mcp"
538
+ ? "mcp"
539
+ : pathname === "/__tree"
540
+ ? "tree"
541
+ : pathname === "/"
542
+ ? "root"
543
+ : pathname.startsWith("/") && pathname.length > 1
544
+ ? "file"
545
+ : "other";
546
+ recorder.record({
547
+ event: "http_request",
548
+ method,
549
+ route,
550
+ status,
551
+ success: status < 400,
552
+ durationMs: performance.now() - startedAt,
553
+ authenticated,
554
+ });
555
+ }
483
556
  function toFetchHeaders(headers) {
484
557
  const result = new Headers();
485
558
  for (const [name, value] of Object.entries(headers)) {
@@ -490,12 +563,39 @@ function toFetchHeaders(headers) {
490
563
  }
491
564
  return result;
492
565
  }
493
- function readBody(req) {
566
+ function readBody(req, signal) {
494
567
  return new Promise((resolve, reject) => {
495
568
  const chunks = [];
496
569
  let size = 0;
497
570
  let tooLarge = false;
498
- req.on("data", (chunk) => {
571
+ let settled = false;
572
+ const timer = setTimeout(() => {
573
+ const error = new Error(`Request body timed out after ${REQUEST_BODY_TIMEOUT_MS / 1000}s.`);
574
+ error.code = "REQUEST_TIMEOUT";
575
+ finish(error);
576
+ }, REQUEST_BODY_TIMEOUT_MS);
577
+ const cleanup = () => {
578
+ clearTimeout(timer);
579
+ req.removeListener("aborted", onAborted);
580
+ req.removeListener("error", onError);
581
+ req.removeListener("data", onData);
582
+ req.removeListener("end", onEnd);
583
+ signal.removeEventListener("abort", onSignalAbort);
584
+ };
585
+ const finish = (error, value) => {
586
+ if (settled)
587
+ return;
588
+ settled = true;
589
+ cleanup();
590
+ if (error)
591
+ reject(error);
592
+ else
593
+ resolve(value ?? Buffer.alloc(0));
594
+ };
595
+ const onAborted = () => finish(new Error("Request aborted by the client."));
596
+ const onError = (error) => finish(error);
597
+ const onSignalAbort = () => finish(new Error("Request aborted by the client."));
598
+ const onData = (chunk) => {
499
599
  if (tooLarge)
500
600
  return;
501
601
  const buffer = Buffer.from(chunk);
@@ -505,17 +605,25 @@ function readBody(req) {
505
605
  return;
506
606
  }
507
607
  chunks.push(buffer);
508
- });
509
- req.on("end", () => {
608
+ };
609
+ const onEnd = () => {
510
610
  if (tooLarge) {
511
611
  const error = new Error(`Request body exceeds the ${MAX_BODY_BYTES / 1024 / 1024} MiB limit.`);
512
612
  error.code = "PAYLOAD_TOO_LARGE";
513
- reject(error);
613
+ finish(error);
514
614
  return;
515
615
  }
516
- resolve(Buffer.concat(chunks));
517
- });
518
- req.on("error", reject);
616
+ finish(undefined, Buffer.concat(chunks));
617
+ };
618
+ if (signal.aborted) {
619
+ onSignalAbort();
620
+ return;
621
+ }
622
+ req.once("aborted", onAborted);
623
+ req.once("error", onError);
624
+ signal.addEventListener("abort", onSignalAbort, { once: true });
625
+ req.on("data", onData);
626
+ req.on("end", onEnd);
519
627
  });
520
628
  }
521
629
  function sendText(res, status, text, contentType = "text/plain; charset=utf-8") {
@@ -556,4 +664,3 @@ async function sendResponse(res, response) {
556
664
  if (!res.destroyed)
557
665
  res.end();
558
666
  }
559
- //# sourceMappingURL=server.js.map
@@ -6,4 +6,3 @@ export declare function shouldOfferSetup(configExists: boolean, options: {
6
6
  tunnel?: string | undefined;
7
7
  }): boolean;
8
8
  export declare function runFirstTimeSetup(config: AgentConfig, defaultDirectory?: string): Promise<string | null | undefined>;
9
- //# sourceMappingURL=setup.d.ts.map
package/dist/src/setup.js CHANGED
@@ -119,16 +119,18 @@ export async function runFirstTimeSetup(config, defaultDirectory = ".") {
119
119
  bold("Privacy first"),
120
120
  dim("Telemetry is optional and stays local on this machine."),
121
121
  dim("It helps us understand usage, failures, and reliability improvements."),
122
- dim("Never records file contents, command arguments, tokens,"),
122
+ dim("Standard levels never record file contents, command arguments, tokens,"),
123
123
  dim("environment variables, or project paths."),
124
+ dim("Diagnostic mode is a separate opt-in local troubleshooting mode and records identifying metadata."),
124
125
  ]);
125
126
  console.log("");
126
127
  console.log(` ${green("●")} ${white("none")} ${dim("disabled")}`);
127
128
  console.log(` ${cyan("●")} ${white("anonymous")} ${dim("usage/tool metrics, success, duration, sizes, safe errors")}`);
128
129
  console.log(` ${cyan("●")} ${white("basic")} ${dim("anonymous + Agent Dir, Node, platform, MCP client version")}`);
129
130
  console.log(` ${cyan("●")} ${white("detailed")} ${dim("basic + coarse language, package manager, Git, CodeGraph, project size")}`);
131
+ console.log(` ${cyan("●")} ${white("diagnostic")} ${dim("detailed + local paths, host/user, PID, Node version, memory/uptime")}`);
130
132
  console.log("");
131
- const telemetry = await choose(rl, "Telemetry level", ["none", "anonymous", "basic", "detailed"], "none");
133
+ const telemetry = await choose(rl, "Telemetry level", ["none", "anonymous", "basic", "detailed", "diagnostic"], "none");
132
134
  const profile = { directory, port, tunnel, token: randomBytes(24).toString("hex") };
133
135
  if (subdomain)
134
136
  profile.subdomain = subdomain;
@@ -212,4 +214,3 @@ async function choose(rl, label, choices, defaultValue) {
212
214
  console.log(` ${error("✕")} ${dim(`Choose a number from 1 to ${choices.length}.`)}`);
213
215
  }
214
216
  }
215
- //# sourceMappingURL=setup.js.map
@@ -4,9 +4,16 @@ export interface TelemetrySummary {
4
4
  totalEvents: number;
5
5
  configs: Record<string, {
6
6
  eventCount: number;
7
+ httpRequests: number;
8
+ httpAuthFailures: number;
9
+ httpErrors: number;
7
10
  mcpRequests: number;
8
11
  toolCalls: number;
9
12
  commandCalls: number;
13
+ tunnelEvents: number;
14
+ tunnelDisconnects: number;
15
+ tunnelReconnects: number;
16
+ tunnelFailures: number;
10
17
  mcpSuccessRate: number | null;
11
18
  toolSuccessRate: number | null;
12
19
  commandSuccessRate: number | null;
@@ -55,4 +62,3 @@ export interface TelemetrySummary {
55
62
  }>;
56
63
  }
57
64
  export declare function summarizeTelemetry(events: TelemetryEnvelope[]): TelemetrySummary;
58
- //# sourceMappingURL=aggregate.d.ts.map
@@ -14,12 +14,15 @@ function rateForEvents(events, kind) {
14
14
  const matching = events.filter((item) => item.event.event === kind);
15
15
  return matching.length === 0
16
16
  ? null
17
- : rate(matching.filter((item) => item.event.event !== "session" && item.event.success).length, matching.length);
17
+ : rate(matching.filter((item) => "success" in item.event && item.event.success).length, matching.length);
18
18
  }
19
19
  function collectFailures(events, kind) {
20
20
  const failures = {};
21
21
  for (const envelope of events) {
22
- if (envelope.event.event !== kind || envelope.event.success || !envelope.event.errorCategory)
22
+ if (envelope.event.event !== kind ||
23
+ !("success" in envelope.event) ||
24
+ envelope.event.success ||
25
+ !envelope.event.errorCategory)
23
26
  continue;
24
27
  failures[envelope.event.errorCategory] = (failures[envelope.event.errorCategory] ?? 0) + 1;
25
28
  }
@@ -42,9 +45,16 @@ export function summarizeTelemetry(events) {
42
45
  const commands = new Map();
43
46
  const sessions = new Map();
44
47
  let legacySessionEvents = 0;
48
+ let httpRequests = 0;
49
+ let httpAuthFailures = 0;
50
+ let httpErrors = 0;
45
51
  let mcpRequests = 0;
46
52
  let toolCalls = 0;
47
53
  let commandCalls = 0;
54
+ let tunnelEvents = 0;
55
+ let tunnelDisconnects = 0;
56
+ let tunnelReconnects = 0;
57
+ let tunnelFailures = 0;
48
58
  for (const envelope of group) {
49
59
  const event = envelope.event;
50
60
  const session = sessions.get(envelope.sessionId) ?? {
@@ -57,6 +67,26 @@ export function summarizeTelemetry(events) {
57
67
  failuresByCategory: { mcp: {}, tool: {}, command: {} },
58
68
  };
59
69
  session.eventCount += 1;
70
+ if (event.event === "http_request") {
71
+ httpRequests += 1;
72
+ if (event.status === 401)
73
+ httpAuthFailures += 1;
74
+ if (event.status >= 400)
75
+ httpErrors += 1;
76
+ sessions.set(envelope.sessionId, session);
77
+ continue;
78
+ }
79
+ if (event.event === "tunnel") {
80
+ tunnelEvents += 1;
81
+ if (event.state === "disconnected")
82
+ tunnelDisconnects += 1;
83
+ if (event.state === "reconnected")
84
+ tunnelReconnects += 1;
85
+ if (event.state === "failed")
86
+ tunnelFailures += 1;
87
+ sessions.set(envelope.sessionId, session);
88
+ continue;
89
+ }
60
90
  if (event.event === "session") {
61
91
  if (Number.isFinite(event.wallClockDurationMs) &&
62
92
  Number.isFinite(event.activeRequestDurationMs) &&
@@ -130,9 +160,16 @@ export function summarizeTelemetry(events) {
130
160
  });
131
161
  configs[configId] = {
132
162
  eventCount: group.length,
163
+ httpRequests,
164
+ httpAuthFailures,
165
+ httpErrors,
133
166
  mcpRequests,
134
167
  toolCalls,
135
168
  commandCalls,
169
+ tunnelEvents,
170
+ tunnelDisconnects,
171
+ tunnelReconnects,
172
+ tunnelFailures,
136
173
  mcpSuccessRate: rateForEvents(group, "mcp_request"),
137
174
  toolSuccessRate: rateForEvents(group, "tool_call"),
138
175
  commandSuccessRate: rateForEvents(group, "command_call"),
@@ -181,4 +218,3 @@ export function summarizeTelemetry(events) {
181
218
  configs,
182
219
  };
183
220
  }
184
- //# sourceMappingURL=aggregate.js.map
@@ -2,4 +2,3 @@ import { type TelemetryConfig, type TelemetryLevel } from "./types.js";
2
2
  export declare const DEFAULT_TELEMETRY_CONFIG: TelemetryConfig;
3
3
  export declare function isTelemetryLevel(value: unknown): value is TelemetryLevel;
4
4
  export declare function parseTelemetryConfig(value: unknown): TelemetryConfig | undefined;
5
- //# sourceMappingURL=config.d.ts.map
@@ -13,4 +13,3 @@ export function parseTelemetryConfig(value) {
13
13
  return undefined;
14
14
  return { level };
15
15
  }
16
- //# sourceMappingURL=config.js.map
@@ -1,3 +1,2 @@
1
1
  import type { TelemetrySummary } from "./aggregate.js";
2
2
  export declare function formatTelemetrySummary(summary: TelemetrySummary, level: string): string;
3
- //# sourceMappingURL=format.d.ts.map
@@ -47,7 +47,7 @@ export function formatTelemetrySummary(summary, level) {
47
47
  return output.join("\n");
48
48
  }
49
49
  for (const [configId, config] of Object.entries(summary.configs)) {
50
- output.push("", ` CONFIGURATION ${configId}`, ` ${line("─", 66)}`, metric("Events", config.eventCount), metric("MCP requests", config.mcpRequests), metric("Tool calls", config.toolCalls), metric("Command calls", config.commandCalls), metric("MCP success", rate(config.mcpSuccessRate)), metric("Tool success", rate(config.toolSuccessRate)), metric("Command success", rate(config.commandSuccessRate)), "", " SESSIONS", metric("Sessions", Object.keys(config.sessions).length));
50
+ output.push("", ` CONFIGURATION ${configId}`, ` ${line("─", 66)}`, metric("Events", config.eventCount), metric("HTTP requests", config.httpRequests), metric("HTTP auth failures", config.httpAuthFailures), metric("HTTP errors", config.httpErrors), metric("MCP requests", config.mcpRequests), metric("Tool calls", config.toolCalls), metric("Command calls", config.commandCalls), metric("Tunnel events", config.tunnelEvents), metric("Tunnel disconnects", config.tunnelDisconnects), metric("Tunnel reconnects", config.tunnelReconnects), metric("Tunnel failures", config.tunnelFailures), metric("MCP success", rate(config.mcpSuccessRate)), metric("Tool success", rate(config.toolSuccessRate)), metric("Command success", rate(config.commandSuccessRate)), "", " SESSIONS", metric("Sessions", Object.keys(config.sessions).length));
51
51
  if (config.legacySessionEvents > 0) {
52
52
  output.push(metric("Legacy sessions", config.legacySessionEvents), " Legacy session records are excluded from session averages.");
53
53
  }
@@ -84,4 +84,3 @@ export function formatTelemetrySummary(summary, level) {
84
84
  output.push("", ` ${line()}`, " Success rates are per event type; MCP and tool events are related but distinct.", " Use 'telemetry show' for the raw event stream.", "");
85
85
  return output.join("\n");
86
86
  }
87
- //# sourceMappingURL=format.js.map
@@ -1,4 +1,3 @@
1
1
  import type { AnonymousTelemetryEvent, TelemetryEnvelope, TelemetryLevel, TelemetryProjectContext, TelemetryRuntimeContext } from "./types.js";
2
2
  export declare function sanitizeAnonymousEvent(event: AnonymousTelemetryEvent): AnonymousTelemetryEvent;
3
3
  export declare function createTelemetryEnvelope(level: Exclude<TelemetryLevel, "none">, event: AnonymousTelemetryEvent, runtime?: TelemetryRuntimeContext, project?: TelemetryProjectContext, configId?: string, sessionId?: string): TelemetryEnvelope;
4
- //# sourceMappingURL=privacy.d.ts.map