agent-dir 0.4.1 → 1.0.0
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.
- package/README.md +40 -374
- package/SECURITY.md +39 -0
- package/assets/agent-dir-banner.svg +38 -0
- package/dist/bin/agent-dir.js +105 -51
- package/dist/src/auth.js +3 -1
- package/dist/src/config.js +31 -3
- package/dist/src/logging.d.ts +1 -1
- package/dist/src/logging.js +13 -70
- package/dist/src/mcp.js +304 -665
- package/dist/src/server.d.ts +1 -1
- package/dist/src/server.js +41 -146
- package/dist/src/setup.js +67 -6
- package/dist/src/telemetry/privacy.js +48 -0
- package/dist/src/telemetry/recorder.js +8 -17
- package/dist/src/telemetry/storage.d.ts +9 -0
- package/dist/src/telemetry/storage.js +133 -0
- package/dist/src/telemetry/types.d.ts +24 -0
- package/dist/src/tools/code.d.ts +0 -12
- package/dist/src/tools/code.js +9 -29
- package/dist/src/tools/commands.d.ts +7 -0
- package/dist/src/tools/commands.js +63 -5
- package/dist/src/tools/diagnostics.js +3 -1
- package/dist/src/tools/files.d.ts +2 -21
- package/dist/src/tools/files.js +85 -70
- package/dist/src/tools/git.d.ts +0 -7
- package/dist/src/tools/git.js +10 -68
- package/dist/src/tools/metadata.d.ts +0 -1
- package/dist/src/tools/metadata.js +0 -22
- package/dist/src/tools/npm.js +7 -5
- package/dist/src/tools/skills.d.ts +1 -0
- package/dist/src/tools/skills.js +65 -0
- package/dist/src/tunnel.js +35 -18
- package/dist/src/types.d.ts +16 -4
- package/docs/CONFIGURATION.md +154 -0
- package/docs/MCP.md +106 -0
- package/docs/RELEASING.md +213 -0
- package/docs/telemetry.md +109 -0
- package/package.json +4 -1
package/dist/src/server.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface ServerHandle {
|
|
|
4
4
|
}
|
|
5
5
|
export declare function startServer({ root, port, token, commandConfig, telemetry, }: ServerOptions): Promise<ServerHandle>;
|
|
6
6
|
export declare function validateMcpHeaders(request: Request, message: Record<string, unknown> | null): Response | undefined;
|
|
7
|
-
export declare function getMcpLog(body: Buffer): {
|
|
7
|
+
export declare function getMcpLog(body: Buffer, responseBody?: unknown): {
|
|
8
8
|
name: string;
|
|
9
9
|
detail: string;
|
|
10
10
|
} | undefined;
|
package/dist/src/server.js
CHANGED
|
@@ -3,7 +3,6 @@ import { authenticate, unauthorized } from "./auth.js";
|
|
|
3
3
|
import { logRequest } from "./logging.js";
|
|
4
4
|
import { createMcpHandler } from "./mcp.js";
|
|
5
5
|
import { TelemetryRecorder } from "./telemetry/recorder.js";
|
|
6
|
-
import { deleteFile, listFiles, readFile, writeFile } from "./tools/files.js";
|
|
7
6
|
const MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
8
7
|
const REQUEST_BODY_TIMEOUT_MS = 30_000;
|
|
9
8
|
const REQUEST_TIMEOUT_MS = 120_000;
|
|
@@ -16,7 +15,7 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
|
|
|
16
15
|
level: telemetry.level,
|
|
17
16
|
persist: telemetry.persist ?? false,
|
|
18
17
|
...(telemetry.configId ? { configId: telemetry.configId } : {}),
|
|
19
|
-
sessionId: `http-${Date.now()}`,
|
|
18
|
+
sessionId: telemetry.sessionId ?? `http-${Date.now()}`,
|
|
20
19
|
});
|
|
21
20
|
const server = http.createServer(async (req, res) => {
|
|
22
21
|
const startedAt = performance.now();
|
|
@@ -29,6 +28,11 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
|
|
|
29
28
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
30
29
|
let authenticated = false;
|
|
31
30
|
try {
|
|
31
|
+
if (url.pathname === HEALTH_PATH && req.method === "HEAD") {
|
|
32
|
+
res.setHeader("cache-control", "no-store");
|
|
33
|
+
sendText(res, 200, "ok\n");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
32
36
|
const body = await readBody(req, requestController.signal);
|
|
33
37
|
const requestInit = {
|
|
34
38
|
method: req.method ?? "GET",
|
|
@@ -51,10 +55,6 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
|
|
|
51
55
|
recordHttpTelemetry(httpTelemetry, req.method ?? "UNKNOWN", url.pathname, 401, authenticated, startedAt);
|
|
52
56
|
return;
|
|
53
57
|
}
|
|
54
|
-
if (url.pathname === HEALTH_PATH && req.method === "HEAD") {
|
|
55
|
-
sendText(res, 200, "ok\n");
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
58
|
if (url.pathname === "/mcp") {
|
|
59
59
|
if (req.method !== "POST") {
|
|
60
60
|
res.setHeader("allow", "POST");
|
|
@@ -82,9 +82,13 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
|
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
const response = await mcp(request);
|
|
85
|
+
const mcpResponseBody = await response
|
|
86
|
+
.clone()
|
|
87
|
+
.json()
|
|
88
|
+
.catch(() => undefined);
|
|
85
89
|
const errorDetail = await getResponseErrorDetail(response);
|
|
90
|
+
const mcpLog = getMcpLog(body, mcpResponseBody);
|
|
86
91
|
await sendResponse(res, response);
|
|
87
|
-
const mcpLog = getMcpLog(body);
|
|
88
92
|
const mcpErrorContext = response.status >= 400 ? getMcpErrorContext(body, request) : undefined;
|
|
89
93
|
logRequest({
|
|
90
94
|
method: req.method,
|
|
@@ -98,50 +102,6 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
|
|
|
98
102
|
recordHttpTelemetry(httpTelemetry, req.method, url.pathname, response.status, authenticated, startedAt);
|
|
99
103
|
return;
|
|
100
104
|
}
|
|
101
|
-
if (url.pathname === "/__tree" && req.method === "GET") {
|
|
102
|
-
const tree = await listFiles(root);
|
|
103
|
-
sendText(res, 200, JSON.stringify(tree, null, 2), "application/json; charset=utf-8");
|
|
104
|
-
logRequest({ method: req.method, path: url.pathname, status: 200, detail: "directory" });
|
|
105
|
-
recordHttpTelemetry(httpTelemetry, req.method, url.pathname, 200, authenticated, startedAt);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
const relative = decodeURIComponent(url.pathname.slice(1));
|
|
109
|
-
if (!relative) {
|
|
110
|
-
sendText(res, 200, "agent-dir\n");
|
|
111
|
-
logRequest({
|
|
112
|
-
method: req.method ?? "UNKNOWN",
|
|
113
|
-
path: url.pathname,
|
|
114
|
-
status: 200,
|
|
115
|
-
detail: "server",
|
|
116
|
-
});
|
|
117
|
-
recordHttpTelemetry(httpTelemetry, req.method ?? "UNKNOWN", url.pathname, 200, authenticated, startedAt);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (req.method === "GET") {
|
|
121
|
-
sendText(res, 200, await readFile(root, relative), "text/plain; charset=utf-8");
|
|
122
|
-
logRequest({ method: req.method, path: url.pathname, status: 200, detail: "REST GET" });
|
|
123
|
-
recordHttpTelemetry(httpTelemetry, req.method, url.pathname, 200, authenticated, startedAt);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (req.method === "PUT") {
|
|
127
|
-
await writeFile(root, relative, body.toString("utf8"));
|
|
128
|
-
sendText(res, 200, "File updated successfully\n");
|
|
129
|
-
logRequest({ method: req.method, path: url.pathname, status: 200, detail: "REST PUT" });
|
|
130
|
-
recordHttpTelemetry(httpTelemetry, req.method, url.pathname, 200, authenticated, startedAt);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (req.method === "DELETE") {
|
|
134
|
-
await deleteFile(root, relative);
|
|
135
|
-
sendText(res, 200, "File deleted successfully\n");
|
|
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);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
105
|
sendText(res, 404, "Not found\n");
|
|
146
106
|
logRequest({
|
|
147
107
|
method: req.method ?? "UNKNOWN",
|
|
@@ -323,7 +283,7 @@ async function getResponseErrorDetail(response) {
|
|
|
323
283
|
}
|
|
324
284
|
return undefined;
|
|
325
285
|
}
|
|
326
|
-
export function getMcpLog(body) {
|
|
286
|
+
export function getMcpLog(body, responseBody) {
|
|
327
287
|
try {
|
|
328
288
|
const message = JSON.parse(body.toString("utf8"));
|
|
329
289
|
if (typeof message.method !== "string")
|
|
@@ -336,20 +296,7 @@ export function getMcpLog(body) {
|
|
|
336
296
|
const name = message.params.name;
|
|
337
297
|
const args = isRecordValue(message.params.arguments) ? message.params.arguments : {};
|
|
338
298
|
const detail = (() => {
|
|
339
|
-
const paths = Array.isArray(args.paths)
|
|
340
|
-
? args.paths.filter((value) => typeof value === "string")
|
|
341
|
-
: [];
|
|
342
|
-
const files = Array.isArray(args.files)
|
|
343
|
-
? args.files
|
|
344
|
-
.filter((value) => typeof value === "object" && value !== null)
|
|
345
|
-
.map((value) => value.path)
|
|
346
|
-
.filter((value) => typeof value === "string")
|
|
347
|
-
: [];
|
|
348
299
|
switch (name) {
|
|
349
|
-
case "list_files":
|
|
350
|
-
return "all project files";
|
|
351
|
-
case "list_dirs":
|
|
352
|
-
return paths.join(", ");
|
|
353
300
|
case "read_range": {
|
|
354
301
|
const path = typeof args.path === "string" ? args.path : "";
|
|
355
302
|
const startLine = typeof args.startLine === "number" ? args.startLine : undefined;
|
|
@@ -358,21 +305,12 @@ export function getMcpLog(body) {
|
|
|
358
305
|
? `${path}:${startLine}-${endLine}`
|
|
359
306
|
: path;
|
|
360
307
|
}
|
|
361
|
-
case "read_files":
|
|
362
|
-
case "delete_files":
|
|
363
|
-
case "git_stage":
|
|
364
|
-
case "git_unstage":
|
|
365
|
-
return paths.join(", ");
|
|
366
|
-
case "write_files":
|
|
367
|
-
case "patch_files":
|
|
368
|
-
return files.join(", ");
|
|
369
308
|
case "run_npm_batch": {
|
|
370
309
|
const scripts = Array.isArray(args.scripts)
|
|
371
310
|
? args.scripts.filter((value) => typeof value === "string")
|
|
372
311
|
: [];
|
|
373
312
|
return scripts.map((script) => `npm run ${script}`).join(", ");
|
|
374
313
|
}
|
|
375
|
-
case "search_files":
|
|
376
314
|
case "search_code": {
|
|
377
315
|
const query = typeof args.query === "string" ? args.query : "";
|
|
378
316
|
const maxResults = typeof args.maxResults === "number" ? args.maxResults : undefined;
|
|
@@ -396,15 +334,6 @@ export function getMcpLog(body) {
|
|
|
396
334
|
: `symbol: ${symbol}`
|
|
397
335
|
: "";
|
|
398
336
|
}
|
|
399
|
-
case "find_definition": {
|
|
400
|
-
const symbol = typeof args.symbol === "string" ? args.symbol : "";
|
|
401
|
-
const maxResults = typeof args.maxResults === "number" ? args.maxResults : undefined;
|
|
402
|
-
return symbol
|
|
403
|
-
? maxResults !== undefined
|
|
404
|
-
? `definition: ${symbol} (max ${maxResults})`
|
|
405
|
-
: `definition: ${symbol}`
|
|
406
|
-
: "";
|
|
407
|
-
}
|
|
408
337
|
case "find_references": {
|
|
409
338
|
const symbol = typeof args.symbol === "string" ? args.symbol : "";
|
|
410
339
|
const maxResults = typeof args.maxResults === "number" ? args.maxResults : undefined;
|
|
@@ -414,36 +343,36 @@ export function getMcpLog(body) {
|
|
|
414
343
|
: `references: ${symbol}`
|
|
415
344
|
: "";
|
|
416
345
|
}
|
|
417
|
-
case "git_commit":
|
|
418
|
-
return "commit (message omitted)";
|
|
419
|
-
case "find_imports": {
|
|
420
|
-
const maxResults = typeof args.maxResults === "number" ? args.maxResults : undefined;
|
|
421
|
-
return maxResults !== undefined ? `all imports (max ${maxResults})` : "all imports";
|
|
422
|
-
}
|
|
423
|
-
case "find_exports": {
|
|
424
|
-
const maxResults = typeof args.maxResults === "number" ? args.maxResults : undefined;
|
|
425
|
-
return maxResults !== undefined ? `all exports (max ${maxResults})` : "all exports";
|
|
426
|
-
}
|
|
427
|
-
case "diagnostics": {
|
|
428
|
-
const maxResults = typeof args.maxResults === "number" ? args.maxResults : undefined;
|
|
429
|
-
return maxResults !== undefined ? `max ${maxResults}` : "";
|
|
430
|
-
}
|
|
431
|
-
case "git_restore":
|
|
432
|
-
return paths.join(", ");
|
|
433
|
-
case "git_push":
|
|
434
|
-
return "push (remote/branch omitted)";
|
|
435
|
-
case "git_diff": {
|
|
436
|
-
const staged = args.staged === true ? "staged" : "working tree";
|
|
437
|
-
const path = typeof args.path === "string" ? args.path : "";
|
|
438
|
-
return path ? `${staged}: ${path}` : staged;
|
|
439
|
-
}
|
|
440
|
-
case "git_log": {
|
|
441
|
-
const limit = typeof args.limit === "number" ? args.limit : 20;
|
|
442
|
-
const path = typeof args.path === "string" ? args.path : "";
|
|
443
|
-
return path ? `last ${limit}: ${path}` : `last ${limit}`;
|
|
444
|
-
}
|
|
445
346
|
case "file_info":
|
|
446
347
|
return typeof args.path === "string" ? args.path : "";
|
|
348
|
+
case "apply_changes": {
|
|
349
|
+
const response = isRecordValue(responseBody) ? responseBody : undefined;
|
|
350
|
+
const result = response && isRecordValue(response.result) ? response.result : undefined;
|
|
351
|
+
if (result?.isError === true || response?.error !== undefined)
|
|
352
|
+
return "change failed";
|
|
353
|
+
const applied = Array.isArray(result?.structuredContent) ? result.structuredContent : [];
|
|
354
|
+
if (applied.length) {
|
|
355
|
+
const entries = applied
|
|
356
|
+
.filter((item) => isRecordValue(item) && item.changed === true)
|
|
357
|
+
.map((item) => {
|
|
358
|
+
const kind = item.kind === "write" || item.kind === "patch" || item.kind === "delete"
|
|
359
|
+
? item.kind
|
|
360
|
+
: "change";
|
|
361
|
+
const path = typeof item.path === "string" ? item.path : "(unknown path)";
|
|
362
|
+
return `${kind} ${path}`;
|
|
363
|
+
});
|
|
364
|
+
if (!entries.length)
|
|
365
|
+
return args.dryRun === true ? "preview: no file changes" : "no file changes";
|
|
366
|
+
const shown = entries.slice(0, 5);
|
|
367
|
+
const suffix = entries.length > shown.length ? ` • +${entries.length - shown.length} more` : "";
|
|
368
|
+
return `${args.dryRun === true ? "preview" : "changed"}: ${shown.join(" • ")}${suffix}`;
|
|
369
|
+
}
|
|
370
|
+
const changes = Array.isArray(args.changes) ? args.changes : [];
|
|
371
|
+
const paths = changes
|
|
372
|
+
.filter((item) => isRecordValue(item))
|
|
373
|
+
.map((item) => (typeof item.path === "string" ? item.path : "(unknown path)"));
|
|
374
|
+
return `requested: ${paths.slice(0, 5).join(" • ")}${paths.length > 5 ? ` • +${paths.length - 5} more` : ""}`;
|
|
375
|
+
}
|
|
447
376
|
case "run_command_batch": {
|
|
448
377
|
const commands = Array.isArray(args.commands) ? args.commands : [];
|
|
449
378
|
return commands
|
|
@@ -463,33 +392,7 @@ export function getMcpLog(body) {
|
|
|
463
392
|
}
|
|
464
393
|
function getMcpMethodDetail(method, params) {
|
|
465
394
|
const value = params ?? {};
|
|
466
|
-
const capabilities = isRecordValue(value.capabilities)
|
|
467
|
-
? Object.keys(value.capabilities).sort()
|
|
468
|
-
: [];
|
|
469
|
-
const clientInfo = isRecordValue(value.clientInfo) ? value.clientInfo : undefined;
|
|
470
|
-
const clientName = typeof clientInfo?.name === "string" ? clientInfo.name : undefined;
|
|
471
|
-
const clientVersion = typeof clientInfo?.version === "string" ? clientInfo.version : undefined;
|
|
472
|
-
const protocol = typeof value.protocolVersion === "string"
|
|
473
|
-
? value.protocolVersion
|
|
474
|
-
: isRecordValue(value._meta) &&
|
|
475
|
-
typeof value._meta["io.modelcontextprotocol/protocolVersion"] === "string"
|
|
476
|
-
? value._meta["io.modelcontextprotocol/protocolVersion"]
|
|
477
|
-
: undefined;
|
|
478
395
|
switch (method) {
|
|
479
|
-
case "initialize": {
|
|
480
|
-
const client = clientName
|
|
481
|
-
? clientVersion
|
|
482
|
-
? `${clientName} v${clientVersion}`
|
|
483
|
-
: clientName
|
|
484
|
-
: undefined;
|
|
485
|
-
return ([
|
|
486
|
-
protocol ? `protocol: ${protocol}` : undefined,
|
|
487
|
-
client ? `client: ${client}` : undefined,
|
|
488
|
-
capabilities.length ? `capabilities: ${capabilities.join(", ")}` : undefined,
|
|
489
|
-
]
|
|
490
|
-
.filter((part) => Boolean(part))
|
|
491
|
-
.join(" • ") || "initialization handshake");
|
|
492
|
-
}
|
|
493
396
|
case "notifications/initialized":
|
|
494
397
|
return "initialization complete";
|
|
495
398
|
case "tools/list":
|
|
@@ -534,15 +437,7 @@ function formatCommand(command, args) {
|
|
|
534
437
|
return count ? `${command} (args: ${count})` : command;
|
|
535
438
|
}
|
|
536
439
|
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";
|
|
440
|
+
const route = pathname === "/mcp" ? "mcp" : "other";
|
|
546
441
|
recorder.record({
|
|
547
442
|
event: "http_request",
|
|
548
443
|
method,
|
package/dist/src/setup.js
CHANGED
|
@@ -104,12 +104,26 @@ export async function runFirstTimeSetup(config, defaultDirectory = ".") {
|
|
|
104
104
|
const name = await requiredText(rl, "Profile name", "default");
|
|
105
105
|
const directory = resolve(await requiredText(rl, "Directory to expose", defaultDirectory));
|
|
106
106
|
const port = await requiredPort(rl, 3002);
|
|
107
|
-
|
|
107
|
+
let hostname;
|
|
108
|
+
let cloudflaredToken;
|
|
108
109
|
let subdomain;
|
|
109
|
-
|
|
110
|
+
console.log("");
|
|
111
|
+
console.log(`${section("◆")} ${section(bold("Cloudflare Tunnel"))}`);
|
|
112
|
+
console.log(rule());
|
|
113
|
+
const useCloudflared = await confirm(rl, "Set up Cloudflare Tunnel?", true);
|
|
114
|
+
if (useCloudflared) {
|
|
115
|
+
hostname = await requiredHostname(rl, "Public hostname (must be routed to this tunnel)");
|
|
116
|
+
cloudflaredToken = await requiredSecret(rl, "Cloudflare tunnel token");
|
|
117
|
+
}
|
|
118
|
+
console.log("");
|
|
119
|
+
console.log(`${section("◆")} ${section(bold("Wormhole"))}`);
|
|
120
|
+
console.log(rule());
|
|
121
|
+
const useWormhole = await confirm(rl, "Set up Wormhole too?", false);
|
|
122
|
+
if (useWormhole &&
|
|
110
123
|
(await confirm(rl, "Use a custom Wormhole subdomain? (No uses a random URL.)", false))) {
|
|
111
124
|
subdomain = await requiredText(rl, "Wormhole subdomain");
|
|
112
125
|
}
|
|
126
|
+
const tunnel = useCloudflared ? "cloudflared" : useWormhole ? "wormhole" : "none";
|
|
113
127
|
const npm = await optionalList(rl, "Allowed npm scripts (comma-separated, blank for none)");
|
|
114
128
|
const commands = await optionalList(rl, "Allowed commands (comma-separated, blank for none)");
|
|
115
129
|
console.log("");
|
|
@@ -119,9 +133,9 @@ export async function runFirstTimeSetup(config, defaultDirectory = ".") {
|
|
|
119
133
|
bold("Privacy first"),
|
|
120
134
|
dim("Telemetry is optional and stays local on this machine."),
|
|
121
135
|
dim("It helps us understand usage, failures, and reliability improvements."),
|
|
122
|
-
dim("Standard levels
|
|
123
|
-
dim("
|
|
124
|
-
dim("
|
|
136
|
+
dim("Standard levels avoid file contents, command arguments, tokens, and project paths."),
|
|
137
|
+
dim("Diagnostic mode records bounded raw tool inputs/outputs and identifying metadata."),
|
|
138
|
+
dim("It may contain source code or other sensitive project data; it stays local."),
|
|
125
139
|
]);
|
|
126
140
|
console.log("");
|
|
127
141
|
console.log(` ${green("●")} ${white("none")} ${dim("disabled")}`);
|
|
@@ -131,9 +145,25 @@ export async function runFirstTimeSetup(config, defaultDirectory = ".") {
|
|
|
131
145
|
console.log(` ${cyan("●")} ${white("diagnostic")} ${dim("detailed + local paths, host/user, PID, Node version, memory/uptime")}`);
|
|
132
146
|
console.log("");
|
|
133
147
|
const telemetry = await choose(rl, "Telemetry level", ["none", "anonymous", "basic", "detailed", "diagnostic"], "none");
|
|
134
|
-
const profile = {
|
|
148
|
+
const profile = {
|
|
149
|
+
directory,
|
|
150
|
+
port,
|
|
151
|
+
tunnel,
|
|
152
|
+
defaultTunnel: tunnel,
|
|
153
|
+
token: randomBytes(24).toString("hex"),
|
|
154
|
+
};
|
|
135
155
|
if (subdomain)
|
|
136
156
|
profile.subdomain = subdomain;
|
|
157
|
+
if (hostname)
|
|
158
|
+
profile.hostname = hostname;
|
|
159
|
+
if (useCloudflared || useWormhole) {
|
|
160
|
+
profile.tunnels = {
|
|
161
|
+
...(useWormhole ? { wormhole: { ...(subdomain ? { subdomain } : { random: true }) } } : {}),
|
|
162
|
+
...(useCloudflared && hostname
|
|
163
|
+
? { cloudflared: { hostname, ...(cloudflaredToken ? { token: cloudflaredToken } : {}) } }
|
|
164
|
+
: {}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
137
167
|
if (npm.length)
|
|
138
168
|
profile.npm = { allowedScripts: npm };
|
|
139
169
|
if (commands.length)
|
|
@@ -168,6 +198,37 @@ async function requiredText(rl, label, defaultValue) {
|
|
|
168
198
|
console.log(` ${error("✕")} ${dim("A value is required.")}`);
|
|
169
199
|
}
|
|
170
200
|
}
|
|
201
|
+
async function requiredSecret(rl, label) {
|
|
202
|
+
while (true) {
|
|
203
|
+
output.write(` ${bold(label)}: `);
|
|
204
|
+
const writableRl = rl;
|
|
205
|
+
const writeToOutput = writableRl._writeToOutput;
|
|
206
|
+
writableRl._writeToOutput = () => { };
|
|
207
|
+
let value;
|
|
208
|
+
try {
|
|
209
|
+
value = (await rl.question("")).trim();
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
writableRl._writeToOutput = writeToOutput;
|
|
213
|
+
output.write("\n");
|
|
214
|
+
}
|
|
215
|
+
if (value)
|
|
216
|
+
return value;
|
|
217
|
+
console.log(` ${error("✕")} ${dim("A value is required.")}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function requiredHostname(rl, label) {
|
|
221
|
+
while (true) {
|
|
222
|
+
const value = (await rl.question(` ${bold(label)}: `)).trim().toLowerCase();
|
|
223
|
+
const labels = value.split(".");
|
|
224
|
+
const valid = value.length <= 253 &&
|
|
225
|
+
labels.length >= 2 &&
|
|
226
|
+
labels.every((part) => part.length >= 1 && part.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(part));
|
|
227
|
+
if (valid)
|
|
228
|
+
return value;
|
|
229
|
+
console.log(` ${error("✕")} ${dim("Enter a hostname only, such as agent-dir-mcp.scriptr.uk (no spaces, https://, or path).")}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
171
232
|
async function requiredPort(rl, defaultValue) {
|
|
172
233
|
while (true) {
|
|
173
234
|
const value = (await rl.question(` ${bold("Port")} ${dim(`[${defaultValue}]`)}: `)).trim() ||
|
|
@@ -31,6 +31,8 @@ export function sanitizeAnonymousEvent(event) {
|
|
|
31
31
|
method: sanitizeMcpMethod(event.method),
|
|
32
32
|
success: event.success === true,
|
|
33
33
|
durationMs: boundedNumber(event.durationMs),
|
|
34
|
+
...optionalNumber("responseBytes", event.responseBytes),
|
|
35
|
+
...optionalNumber("estimatedResponseTokens", event.estimatedResponseTokens),
|
|
34
36
|
...(event.errorCategory === undefined
|
|
35
37
|
? {}
|
|
36
38
|
: { errorCategory: sanitizeErrorCategory(event.errorCategory) }),
|
|
@@ -40,6 +42,33 @@ export function sanitizeAnonymousEvent(event) {
|
|
|
40
42
|
return {
|
|
41
43
|
event: "tool_call",
|
|
42
44
|
tool: sanitizeIdentifier(event.tool, SAFE_TOOL_NAME),
|
|
45
|
+
...(event.requestIdHash === undefined
|
|
46
|
+
? {}
|
|
47
|
+
: { requestIdHash: sanitizeHash(event.requestIdHash) }),
|
|
48
|
+
...(event.inputFingerprint === undefined
|
|
49
|
+
? {}
|
|
50
|
+
: { inputFingerprint: sanitizeHash(event.inputFingerprint) }),
|
|
51
|
+
...(event.outputFingerprint === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: { outputFingerprint: sanitizeHash(event.outputFingerprint) }),
|
|
54
|
+
...optionalNumber("estimatedInputTokens", event.estimatedInputTokens),
|
|
55
|
+
...optionalNumber("estimatedOutputTokens", event.estimatedOutputTokens),
|
|
56
|
+
...optionalNumber("cumulativeOutputBytes", event.cumulativeOutputBytes),
|
|
57
|
+
...optionalNumber("cumulativeEstimatedOutputTokens", event.cumulativeEstimatedOutputTokens),
|
|
58
|
+
...optionalNumber("duplicateOutputBytes", event.duplicateOutputBytes),
|
|
59
|
+
...optionalNumber("textOutputBytes", event.textOutputBytes),
|
|
60
|
+
...optionalNumber("uniqueFileCount", event.uniqueFileCount),
|
|
61
|
+
...(event.files === undefined
|
|
62
|
+
? {}
|
|
63
|
+
: {
|
|
64
|
+
files: event.files.slice(0, 100).map((file) => ({
|
|
65
|
+
pathHash: sanitizeHash(file.pathHash),
|
|
66
|
+
...(file.contentHash === undefined
|
|
67
|
+
? {}
|
|
68
|
+
: { contentHash: sanitizeHash(file.contentHash) }),
|
|
69
|
+
bytes: boundedNumber(file.bytes),
|
|
70
|
+
})),
|
|
71
|
+
}),
|
|
43
72
|
success: event.success === true,
|
|
44
73
|
durationMs: boundedNumber(event.durationMs),
|
|
45
74
|
...optionalNumber("inputBytes", event.inputBytes),
|
|
@@ -100,6 +129,12 @@ export function createTelemetryEnvelope(level, event, runtime, project, configId
|
|
|
100
129
|
level,
|
|
101
130
|
event: sanitizeAnonymousEvent(event),
|
|
102
131
|
};
|
|
132
|
+
if (level === "diagnostic" &&
|
|
133
|
+
event.event === "tool_call" &&
|
|
134
|
+
event.diagnosticDetails &&
|
|
135
|
+
envelope.event.event === "tool_call") {
|
|
136
|
+
envelope.event.diagnosticDetails = event.diagnosticDetails;
|
|
137
|
+
}
|
|
103
138
|
if (level === "basic" || level === "detailed" || level === "diagnostic") {
|
|
104
139
|
if (runtime)
|
|
105
140
|
envelope.runtime = sanitizeRuntimeContext(runtime);
|
|
@@ -121,6 +156,16 @@ function sanitizeDiagnosticRuntimeContext(context) {
|
|
|
121
156
|
...(context.nodeVersion === undefined
|
|
122
157
|
? {}
|
|
123
158
|
: { nodeVersion: sanitizeVersion(context.nodeVersion) }),
|
|
159
|
+
...(context.distroId === undefined
|
|
160
|
+
? {}
|
|
161
|
+
: { distroId: sanitizeSensitiveIdentifier(context.distroId) }),
|
|
162
|
+
...(context.distroName === undefined ? {} : { distroName: context.distroName.slice(0, 128) }),
|
|
163
|
+
...(context.distroVersion === undefined
|
|
164
|
+
? {}
|
|
165
|
+
: { distroVersion: context.distroVersion.slice(0, 64) }),
|
|
166
|
+
...(context.kernelVersion === undefined
|
|
167
|
+
? {}
|
|
168
|
+
: { kernelVersion: context.kernelVersion.slice(0, 128) }),
|
|
124
169
|
...(context.hostname === undefined
|
|
125
170
|
? {}
|
|
126
171
|
: { hostname: sanitizeSensitiveIdentifier(context.hostname) }),
|
|
@@ -202,6 +247,9 @@ function sanitizeVersion(value) {
|
|
|
202
247
|
? normalized
|
|
203
248
|
: "unknown";
|
|
204
249
|
}
|
|
250
|
+
function sanitizeHash(value) {
|
|
251
|
+
return /^[a-f0-9]{64}$/.test(value) ? value : "unknown";
|
|
252
|
+
}
|
|
205
253
|
function sanitizeErrorCategory(value) {
|
|
206
254
|
const normalized = value.trim().toLowerCase();
|
|
207
255
|
return SAFE_ERROR_CATEGORIES.has(normalized) ? normalized : "unknown";
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { appendFileSync, chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
4
|
import { createTelemetryEnvelope } from "./privacy.js";
|
|
5
|
+
import { telemetrySessionPath } from "./storage.js";
|
|
5
6
|
const DEFAULT_MAX_EVENTS = 500;
|
|
6
|
-
const DEFAULT_PERSISTED_EVENTS = 500;
|
|
7
|
-
const TELEMETRY_FILE = join(homedir(), ".config", "agent-dir", "telemetry.jsonl");
|
|
8
7
|
export class TelemetryRecorder {
|
|
9
8
|
level;
|
|
10
9
|
maxEvents;
|
|
@@ -21,9 +20,9 @@ export class TelemetryRecorder {
|
|
|
21
20
|
this.runtime = options.runtime;
|
|
22
21
|
this.project = options.project;
|
|
23
22
|
this.persist = options.persist ?? false;
|
|
24
|
-
this.persistPath = options.persistPath ?? TELEMETRY_FILE;
|
|
25
23
|
this.configId = options.configId ?? "default";
|
|
26
|
-
this.sessionId = options.sessionId ??
|
|
24
|
+
this.sessionId = options.sessionId ?? randomUUID();
|
|
25
|
+
this.persistPath = options.persistPath ?? telemetrySessionPath(this.configId, this.sessionId);
|
|
27
26
|
if (this.persist && this.level !== "none")
|
|
28
27
|
this.loadPersisted();
|
|
29
28
|
}
|
|
@@ -64,8 +63,9 @@ export class TelemetryRecorder {
|
|
|
64
63
|
.filter((event) => event &&
|
|
65
64
|
event.schemaVersion === 1 &&
|
|
66
65
|
event.level === this.level &&
|
|
67
|
-
event.configId === this.configId
|
|
68
|
-
.
|
|
66
|
+
event.configId === this.configId &&
|
|
67
|
+
event.sessionId === this.sessionId)
|
|
68
|
+
.slice(-this.maxEvents);
|
|
69
69
|
this.events.push(...persisted);
|
|
70
70
|
}
|
|
71
71
|
catch {
|
|
@@ -80,15 +80,6 @@ export class TelemetryRecorder {
|
|
|
80
80
|
mode: 0o600,
|
|
81
81
|
});
|
|
82
82
|
chmodSync(this.persistPath, 0o600);
|
|
83
|
-
if (this.events.length === this.maxEvents) {
|
|
84
|
-
const lines = readFileSync(this.persistPath, "utf8").split("\n").filter(Boolean);
|
|
85
|
-
if (lines.length > this.maxEvents) {
|
|
86
|
-
writeFileSync(this.persistPath, `${lines.slice(-this.maxEvents).join("\n")}\n`, {
|
|
87
|
-
encoding: "utf8",
|
|
88
|
-
mode: 0o600,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
83
|
}
|
|
93
84
|
catch {
|
|
94
85
|
// Telemetry persistence must never affect the MCP request path.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TelemetryEnvelope } from "./types.js";
|
|
2
|
+
export declare const TELEMETRY_DIRECTORY: string;
|
|
3
|
+
export declare const LEGACY_TELEMETRY_FILE: string;
|
|
4
|
+
export declare function telemetrySessionPath(configId: string, sessionId: string): string;
|
|
5
|
+
/** Migrate the old shared file once, preserving an archive and avoiding duplicate lines. */
|
|
6
|
+
export declare function migrateLegacyTelemetry(): void;
|
|
7
|
+
export declare function readAllTelemetryEvents(): TelemetryEnvelope[];
|
|
8
|
+
export declare function resetTelemetry(profile?: string, all?: boolean): void;
|
|
9
|
+
export declare function ensureTelemetryDirectory(): void;
|