agent-dir 0.5.0 → 1.1.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 -421
- package/SECURITY.md +39 -0
- package/assets/agent-dir-banner.svg +38 -0
- package/dist/bin/agent-dir.js +9 -7
- package/dist/src/auth.js +3 -1
- package/dist/src/codegraph.d.ts +1 -0
- package/dist/src/codegraph.js +12 -0
- package/dist/src/config.js +0 -2
- package/dist/src/logging.d.ts +1 -1
- package/dist/src/logging.js +9 -69
- package/dist/src/mcp.d.ts +2 -1
- package/dist/src/mcp.js +191 -653
- package/dist/src/server.d.ts +2 -2
- package/dist/src/server.js +45 -146
- package/dist/src/telemetry/privacy.js +2 -0
- package/dist/src/telemetry/types.d.ts +2 -0
- package/dist/src/tools/code.d.ts +0 -12
- package/dist/src/tools/code.js +9 -29
- 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 +0 -64
- package/dist/src/tools/metadata.d.ts +0 -1
- package/dist/src/tools/metadata.js +0 -22
- package/dist/src/tools/skills.d.ts +1 -0
- package/dist/src/tools/skills.js +65 -0
- package/dist/src/types.d.ts +1 -2
- package/docs/CONFIGURATION.md +154 -0
- package/docs/MCP.md +106 -0
- package/docs/RELEASING.md +204 -0
- package/docs/telemetry.md +109 -0
- package/package.json +4 -1
package/dist/src/server.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import type { ServerOptions } from "./types.js";
|
|
|
2
2
|
export interface ServerHandle {
|
|
3
3
|
close: () => Promise<void>;
|
|
4
4
|
}
|
|
5
|
-
export declare function startServer({ root, port, token, commandConfig, telemetry, }: ServerOptions): Promise<ServerHandle>;
|
|
5
|
+
export declare function startServer({ root, port, token, commandConfig, telemetry, codeGraph, }: 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,15 +3,14 @@ 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;
|
|
10
9
|
const HEADERS_TIMEOUT_MS = 30_000;
|
|
11
10
|
const KEEP_ALIVE_TIMEOUT_MS = 5_000;
|
|
12
11
|
const HEALTH_PATH = "/__health";
|
|
13
|
-
export function startServer({ root, port, token, commandConfig = {}, telemetry = { level: "none" }, }) {
|
|
14
|
-
const mcp = createMcpHandler(root, commandConfig, telemetry);
|
|
12
|
+
export function startServer({ root, port, token, commandConfig = {}, telemetry = { level: "none" }, codeGraph, }) {
|
|
13
|
+
const mcp = createMcpHandler(root, commandConfig, telemetry, codeGraph);
|
|
15
14
|
const httpTelemetry = new TelemetryRecorder({
|
|
16
15
|
level: telemetry.level,
|
|
17
16
|
persist: telemetry.persist ?? false,
|
|
@@ -83,9 +82,13 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
|
|
|
83
82
|
return;
|
|
84
83
|
}
|
|
85
84
|
const response = await mcp(request);
|
|
85
|
+
const mcpResponseBody = await response
|
|
86
|
+
.clone()
|
|
87
|
+
.json()
|
|
88
|
+
.catch(() => undefined);
|
|
86
89
|
const errorDetail = await getResponseErrorDetail(response);
|
|
90
|
+
const mcpLog = getMcpLog(body, mcpResponseBody);
|
|
87
91
|
await sendResponse(res, response);
|
|
88
|
-
const mcpLog = getMcpLog(body);
|
|
89
92
|
const mcpErrorContext = response.status >= 400 ? getMcpErrorContext(body, request) : undefined;
|
|
90
93
|
logRequest({
|
|
91
94
|
method: req.method,
|
|
@@ -99,50 +102,6 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
|
|
|
99
102
|
recordHttpTelemetry(httpTelemetry, req.method, url.pathname, response.status, authenticated, startedAt);
|
|
100
103
|
return;
|
|
101
104
|
}
|
|
102
|
-
if (url.pathname === "/__tree" && req.method === "GET") {
|
|
103
|
-
const tree = await listFiles(root);
|
|
104
|
-
sendText(res, 200, JSON.stringify(tree, null, 2), "application/json; charset=utf-8");
|
|
105
|
-
logRequest({ method: req.method, path: url.pathname, status: 200, detail: "directory" });
|
|
106
|
-
recordHttpTelemetry(httpTelemetry, req.method, url.pathname, 200, authenticated, startedAt);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const relative = decodeURIComponent(url.pathname.slice(1));
|
|
110
|
-
if (!relative) {
|
|
111
|
-
sendText(res, 200, "agent-dir\n");
|
|
112
|
-
logRequest({
|
|
113
|
-
method: req.method ?? "UNKNOWN",
|
|
114
|
-
path: url.pathname,
|
|
115
|
-
status: 200,
|
|
116
|
-
detail: "server",
|
|
117
|
-
});
|
|
118
|
-
recordHttpTelemetry(httpTelemetry, req.method ?? "UNKNOWN", url.pathname, 200, authenticated, startedAt);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
if (req.method === "GET") {
|
|
122
|
-
sendText(res, 200, await readFile(root, relative), "text/plain; charset=utf-8");
|
|
123
|
-
logRequest({ method: req.method, path: url.pathname, status: 200, detail: "REST GET" });
|
|
124
|
-
recordHttpTelemetry(httpTelemetry, req.method, url.pathname, 200, authenticated, startedAt);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (req.method === "PUT") {
|
|
128
|
-
await writeFile(root, relative, body.toString("utf8"));
|
|
129
|
-
sendText(res, 200, "File updated successfully\n");
|
|
130
|
-
logRequest({ method: req.method, path: url.pathname, status: 200, detail: "REST PUT" });
|
|
131
|
-
recordHttpTelemetry(httpTelemetry, req.method, url.pathname, 200, authenticated, startedAt);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (req.method === "DELETE") {
|
|
135
|
-
await deleteFile(root, relative);
|
|
136
|
-
sendText(res, 200, "File deleted successfully\n");
|
|
137
|
-
logRequest({
|
|
138
|
-
method: req.method,
|
|
139
|
-
path: url.pathname,
|
|
140
|
-
status: 200,
|
|
141
|
-
detail: "REST DELETE",
|
|
142
|
-
});
|
|
143
|
-
recordHttpTelemetry(httpTelemetry, req.method, url.pathname, 200, authenticated, startedAt);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
105
|
sendText(res, 404, "Not found\n");
|
|
147
106
|
logRequest({
|
|
148
107
|
method: req.method ?? "UNKNOWN",
|
|
@@ -179,10 +138,15 @@ export function startServer({ root, port, token, commandConfig = {}, telemetry =
|
|
|
179
138
|
return new Promise((resolve, reject) => {
|
|
180
139
|
server.once("error", reject);
|
|
181
140
|
server.listen(port, "127.0.0.1", () => resolve({
|
|
182
|
-
close:
|
|
141
|
+
close: async () => {
|
|
183
142
|
mcp.closeSubscriptions();
|
|
184
|
-
|
|
185
|
-
|
|
143
|
+
try {
|
|
144
|
+
await new Promise((done) => server.close(() => done()));
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
await codeGraph?.close();
|
|
148
|
+
}
|
|
149
|
+
},
|
|
186
150
|
}));
|
|
187
151
|
});
|
|
188
152
|
}
|
|
@@ -324,7 +288,7 @@ async function getResponseErrorDetail(response) {
|
|
|
324
288
|
}
|
|
325
289
|
return undefined;
|
|
326
290
|
}
|
|
327
|
-
export function getMcpLog(body) {
|
|
291
|
+
export function getMcpLog(body, responseBody) {
|
|
328
292
|
try {
|
|
329
293
|
const message = JSON.parse(body.toString("utf8"));
|
|
330
294
|
if (typeof message.method !== "string")
|
|
@@ -337,20 +301,7 @@ export function getMcpLog(body) {
|
|
|
337
301
|
const name = message.params.name;
|
|
338
302
|
const args = isRecordValue(message.params.arguments) ? message.params.arguments : {};
|
|
339
303
|
const detail = (() => {
|
|
340
|
-
const paths = Array.isArray(args.paths)
|
|
341
|
-
? args.paths.filter((value) => typeof value === "string")
|
|
342
|
-
: [];
|
|
343
|
-
const files = Array.isArray(args.files)
|
|
344
|
-
? args.files
|
|
345
|
-
.filter((value) => typeof value === "object" && value !== null)
|
|
346
|
-
.map((value) => value.path)
|
|
347
|
-
.filter((value) => typeof value === "string")
|
|
348
|
-
: [];
|
|
349
304
|
switch (name) {
|
|
350
|
-
case "list_files":
|
|
351
|
-
return "all project files";
|
|
352
|
-
case "list_dirs":
|
|
353
|
-
return paths.join(", ");
|
|
354
305
|
case "read_range": {
|
|
355
306
|
const path = typeof args.path === "string" ? args.path : "";
|
|
356
307
|
const startLine = typeof args.startLine === "number" ? args.startLine : undefined;
|
|
@@ -359,21 +310,12 @@ export function getMcpLog(body) {
|
|
|
359
310
|
? `${path}:${startLine}-${endLine}`
|
|
360
311
|
: path;
|
|
361
312
|
}
|
|
362
|
-
case "read_files":
|
|
363
|
-
case "delete_files":
|
|
364
|
-
case "git_stage":
|
|
365
|
-
case "git_unstage":
|
|
366
|
-
return paths.join(", ");
|
|
367
|
-
case "write_files":
|
|
368
|
-
case "patch_files":
|
|
369
|
-
return files.join(", ");
|
|
370
313
|
case "run_npm_batch": {
|
|
371
314
|
const scripts = Array.isArray(args.scripts)
|
|
372
315
|
? args.scripts.filter((value) => typeof value === "string")
|
|
373
316
|
: [];
|
|
374
317
|
return scripts.map((script) => `npm run ${script}`).join(", ");
|
|
375
318
|
}
|
|
376
|
-
case "search_files":
|
|
377
319
|
case "search_code": {
|
|
378
320
|
const query = typeof args.query === "string" ? args.query : "";
|
|
379
321
|
const maxResults = typeof args.maxResults === "number" ? args.maxResults : undefined;
|
|
@@ -397,15 +339,6 @@ export function getMcpLog(body) {
|
|
|
397
339
|
: `symbol: ${symbol}`
|
|
398
340
|
: "";
|
|
399
341
|
}
|
|
400
|
-
case "find_definition": {
|
|
401
|
-
const symbol = typeof args.symbol === "string" ? args.symbol : "";
|
|
402
|
-
const maxResults = typeof args.maxResults === "number" ? args.maxResults : undefined;
|
|
403
|
-
return symbol
|
|
404
|
-
? maxResults !== undefined
|
|
405
|
-
? `definition: ${symbol} (max ${maxResults})`
|
|
406
|
-
: `definition: ${symbol}`
|
|
407
|
-
: "";
|
|
408
|
-
}
|
|
409
342
|
case "find_references": {
|
|
410
343
|
const symbol = typeof args.symbol === "string" ? args.symbol : "";
|
|
411
344
|
const maxResults = typeof args.maxResults === "number" ? args.maxResults : undefined;
|
|
@@ -415,36 +348,36 @@ export function getMcpLog(body) {
|
|
|
415
348
|
: `references: ${symbol}`
|
|
416
349
|
: "";
|
|
417
350
|
}
|
|
418
|
-
case "git_commit":
|
|
419
|
-
return "commit (message omitted)";
|
|
420
|
-
case "find_imports": {
|
|
421
|
-
const maxResults = typeof args.maxResults === "number" ? args.maxResults : undefined;
|
|
422
|
-
return maxResults !== undefined ? `all imports (max ${maxResults})` : "all imports";
|
|
423
|
-
}
|
|
424
|
-
case "find_exports": {
|
|
425
|
-
const maxResults = typeof args.maxResults === "number" ? args.maxResults : undefined;
|
|
426
|
-
return maxResults !== undefined ? `all exports (max ${maxResults})` : "all exports";
|
|
427
|
-
}
|
|
428
|
-
case "diagnostics": {
|
|
429
|
-
const maxResults = typeof args.maxResults === "number" ? args.maxResults : undefined;
|
|
430
|
-
return maxResults !== undefined ? `max ${maxResults}` : "";
|
|
431
|
-
}
|
|
432
|
-
case "git_restore":
|
|
433
|
-
return paths.join(", ");
|
|
434
|
-
case "git_push":
|
|
435
|
-
return "push (remote/branch omitted)";
|
|
436
|
-
case "git_diff": {
|
|
437
|
-
const staged = args.staged === true ? "staged" : "working tree";
|
|
438
|
-
const path = typeof args.path === "string" ? args.path : "";
|
|
439
|
-
return path ? `${staged}: ${path}` : staged;
|
|
440
|
-
}
|
|
441
|
-
case "git_log": {
|
|
442
|
-
const limit = typeof args.limit === "number" ? args.limit : 20;
|
|
443
|
-
const path = typeof args.path === "string" ? args.path : "";
|
|
444
|
-
return path ? `last ${limit}: ${path}` : `last ${limit}`;
|
|
445
|
-
}
|
|
446
351
|
case "file_info":
|
|
447
352
|
return typeof args.path === "string" ? args.path : "";
|
|
353
|
+
case "apply_changes": {
|
|
354
|
+
const response = isRecordValue(responseBody) ? responseBody : undefined;
|
|
355
|
+
const result = response && isRecordValue(response.result) ? response.result : undefined;
|
|
356
|
+
if (result?.isError === true || response?.error !== undefined)
|
|
357
|
+
return "change failed";
|
|
358
|
+
const applied = Array.isArray(result?.structuredContent) ? result.structuredContent : [];
|
|
359
|
+
if (applied.length) {
|
|
360
|
+
const entries = applied
|
|
361
|
+
.filter((item) => isRecordValue(item) && item.changed === true)
|
|
362
|
+
.map((item) => {
|
|
363
|
+
const kind = item.kind === "write" || item.kind === "patch" || item.kind === "delete"
|
|
364
|
+
? item.kind
|
|
365
|
+
: "change";
|
|
366
|
+
const path = typeof item.path === "string" ? item.path : "(unknown path)";
|
|
367
|
+
return `${kind} ${path}`;
|
|
368
|
+
});
|
|
369
|
+
if (!entries.length)
|
|
370
|
+
return args.dryRun === true ? "preview: no file changes" : "no file changes";
|
|
371
|
+
const shown = entries.slice(0, 5);
|
|
372
|
+
const suffix = entries.length > shown.length ? ` • +${entries.length - shown.length} more` : "";
|
|
373
|
+
return `${args.dryRun === true ? "preview" : "changed"}: ${shown.join(" • ")}${suffix}`;
|
|
374
|
+
}
|
|
375
|
+
const changes = Array.isArray(args.changes) ? args.changes : [];
|
|
376
|
+
const paths = changes
|
|
377
|
+
.filter((item) => isRecordValue(item))
|
|
378
|
+
.map((item) => (typeof item.path === "string" ? item.path : "(unknown path)"));
|
|
379
|
+
return `requested: ${paths.slice(0, 5).join(" • ")}${paths.length > 5 ? ` • +${paths.length - 5} more` : ""}`;
|
|
380
|
+
}
|
|
448
381
|
case "run_command_batch": {
|
|
449
382
|
const commands = Array.isArray(args.commands) ? args.commands : [];
|
|
450
383
|
return commands
|
|
@@ -464,33 +397,7 @@ export function getMcpLog(body) {
|
|
|
464
397
|
}
|
|
465
398
|
function getMcpMethodDetail(method, params) {
|
|
466
399
|
const value = params ?? {};
|
|
467
|
-
const capabilities = isRecordValue(value.capabilities)
|
|
468
|
-
? Object.keys(value.capabilities).sort()
|
|
469
|
-
: [];
|
|
470
|
-
const clientInfo = isRecordValue(value.clientInfo) ? value.clientInfo : undefined;
|
|
471
|
-
const clientName = typeof clientInfo?.name === "string" ? clientInfo.name : undefined;
|
|
472
|
-
const clientVersion = typeof clientInfo?.version === "string" ? clientInfo.version : undefined;
|
|
473
|
-
const protocol = typeof value.protocolVersion === "string"
|
|
474
|
-
? value.protocolVersion
|
|
475
|
-
: isRecordValue(value._meta) &&
|
|
476
|
-
typeof value._meta["io.modelcontextprotocol/protocolVersion"] === "string"
|
|
477
|
-
? value._meta["io.modelcontextprotocol/protocolVersion"]
|
|
478
|
-
: undefined;
|
|
479
400
|
switch (method) {
|
|
480
|
-
case "initialize": {
|
|
481
|
-
const client = clientName
|
|
482
|
-
? clientVersion
|
|
483
|
-
? `${clientName} v${clientVersion}`
|
|
484
|
-
: clientName
|
|
485
|
-
: undefined;
|
|
486
|
-
return ([
|
|
487
|
-
protocol ? `protocol: ${protocol}` : undefined,
|
|
488
|
-
client ? `client: ${client}` : undefined,
|
|
489
|
-
capabilities.length ? `capabilities: ${capabilities.join(", ")}` : undefined,
|
|
490
|
-
]
|
|
491
|
-
.filter((part) => Boolean(part))
|
|
492
|
-
.join(" • ") || "initialization handshake");
|
|
493
|
-
}
|
|
494
401
|
case "notifications/initialized":
|
|
495
402
|
return "initialization complete";
|
|
496
403
|
case "tools/list":
|
|
@@ -535,15 +442,7 @@ function formatCommand(command, args) {
|
|
|
535
442
|
return count ? `${command} (args: ${count})` : command;
|
|
536
443
|
}
|
|
537
444
|
function recordHttpTelemetry(recorder, method, pathname, status, authenticated, startedAt) {
|
|
538
|
-
const route = pathname === "/mcp"
|
|
539
|
-
? "mcp"
|
|
540
|
-
: pathname === "/__tree"
|
|
541
|
-
? "tree"
|
|
542
|
-
: pathname === "/"
|
|
543
|
-
? "root"
|
|
544
|
-
: pathname.startsWith("/") && pathname.length > 1
|
|
545
|
-
? "file"
|
|
546
|
-
: "other";
|
|
445
|
+
const route = pathname === "/mcp" ? "mcp" : "other";
|
|
547
446
|
recorder.record({
|
|
548
447
|
event: "http_request",
|
|
549
448
|
method,
|
|
@@ -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) }),
|
package/dist/src/tools/code.d.ts
CHANGED
|
@@ -8,19 +8,7 @@ export interface SymbolMatch extends SearchMatch {
|
|
|
8
8
|
symbol: string;
|
|
9
9
|
kind: string;
|
|
10
10
|
}
|
|
11
|
-
export interface ImportMatch extends SearchMatch {
|
|
12
|
-
module: string;
|
|
13
|
-
kind: "import" | "require";
|
|
14
|
-
}
|
|
15
|
-
export interface ExportMatch extends SearchMatch {
|
|
16
|
-
symbol: string;
|
|
17
|
-
kind: "export";
|
|
18
|
-
}
|
|
19
|
-
export declare function searchFiles(root: string, query: string, regex?: boolean, maxResults?: number, maxBytes?: number): Promise<SearchMatch[]>;
|
|
20
11
|
export declare function searchCode(root: string, query: string, regex?: boolean, maxResults?: number, maxBytes?: number): Promise<SearchMatch[]>;
|
|
21
12
|
export declare function findFiles(root: string, pattern: string, maxResults?: number): Promise<string[]>;
|
|
22
13
|
export declare function findSymbol(root: string, symbol: string, maxResults?: number, maxBytes?: number): Promise<SymbolMatch[]>;
|
|
23
|
-
export declare function findDefinition(root: string, symbol: string, maxResults?: number, maxBytes?: number): Promise<SymbolMatch[]>;
|
|
24
14
|
export declare function findReferences(root: string, symbol: string, maxResults?: number, maxBytes?: number): Promise<SearchMatch[]>;
|
|
25
|
-
export declare function findImports(root: string, maxResults?: number, maxBytes?: number): Promise<ImportMatch[]>;
|
|
26
|
-
export declare function findExports(root: string, maxResults?: number, maxBytes?: number): Promise<ExportMatch[]>;
|
package/dist/src/tools/code.js
CHANGED
|
@@ -34,7 +34,8 @@ const CODE_EXTENSIONS = new Set([
|
|
|
34
34
|
".yaml",
|
|
35
35
|
".yml",
|
|
36
36
|
]);
|
|
37
|
-
const DEFAULT_OUTPUT_BYTES =
|
|
37
|
+
const DEFAULT_OUTPUT_BYTES = 8_000;
|
|
38
|
+
const MAX_SEARCH_FILE_BYTES = 1_000_000;
|
|
38
39
|
function truncateUtf8(value, maxBytes) {
|
|
39
40
|
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
40
41
|
return value;
|
|
@@ -84,7 +85,13 @@ async function readMatches(root, query, options = {}) {
|
|
|
84
85
|
break;
|
|
85
86
|
let content;
|
|
86
87
|
try {
|
|
87
|
-
|
|
88
|
+
const target = await safePath(root, relativePath);
|
|
89
|
+
const stat = await fs.stat(target);
|
|
90
|
+
if (!stat.isFile() || stat.size > MAX_SEARCH_FILE_BYTES)
|
|
91
|
+
continue;
|
|
92
|
+
content = await fs.readFile(target, "utf8");
|
|
93
|
+
if (content.includes(String.fromCharCode(0)))
|
|
94
|
+
continue;
|
|
88
95
|
}
|
|
89
96
|
catch {
|
|
90
97
|
continue;
|
|
@@ -101,11 +108,6 @@ async function readMatches(root, query, options = {}) {
|
|
|
101
108
|
}
|
|
102
109
|
return matches;
|
|
103
110
|
}
|
|
104
|
-
export async function searchFiles(root, query, regex = false, maxResults = 200, maxBytes = DEFAULT_OUTPUT_BYTES) {
|
|
105
|
-
if (!query.trim())
|
|
106
|
-
throw new Error("Search query cannot be empty.");
|
|
107
|
-
return boundMatches(await readMatches(root, query, { regex, maxResults }), maxBytes);
|
|
108
|
-
}
|
|
109
111
|
export async function searchCode(root, query, regex = false, maxResults = 200, maxBytes = DEFAULT_OUTPUT_BYTES) {
|
|
110
112
|
if (!query.trim())
|
|
111
113
|
throw new Error("Search query cannot be empty.");
|
|
@@ -132,34 +134,12 @@ export async function findSymbol(root, symbol, maxResults = 100, maxBytes = DEFA
|
|
|
132
134
|
const matches = (await readMatches(root, pattern.source, { codeOnly: true, regex: true, maxResults })).map((match) => ({ ...match, symbol, kind: symbolKind(match.text) }));
|
|
133
135
|
return boundMatches(matches, maxBytes);
|
|
134
136
|
}
|
|
135
|
-
export function findDefinition(root, symbol, maxResults = 100, maxBytes = DEFAULT_OUTPUT_BYTES) {
|
|
136
|
-
return findSymbol(root, symbol, maxResults, maxBytes);
|
|
137
|
-
}
|
|
138
137
|
export function findReferences(root, symbol, maxResults = 200, maxBytes = DEFAULT_OUTPUT_BYTES) {
|
|
139
138
|
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(symbol))
|
|
140
139
|
throw new Error("Symbol must be a valid identifier.");
|
|
141
140
|
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
142
141
|
return searchCode(root, `\\b${escaped}\\b`, true, maxResults, maxBytes);
|
|
143
142
|
}
|
|
144
|
-
export async function findImports(root, maxResults = 200, maxBytes = DEFAULT_OUTPUT_BYTES) {
|
|
145
|
-
const matches = await readMatches(root, String.raw `(?:^|[;{}])\s*import\s*(?:[^"'\n]*?from\s*)?["']([^"']+)["']|require\(\s*["']([^"']+)["']\s*\)`, { codeOnly: true, regex: true, maxResults });
|
|
146
|
-
return boundMatches(matches.map((match) => {
|
|
147
|
-
const captured = match.text.match(/(?:from\s*)?["']([^"']+)["']|require\(\s*["']([^"']+)["']\s*\)/);
|
|
148
|
-
return {
|
|
149
|
-
...match,
|
|
150
|
-
module: captured?.[1] ?? captured?.[2] ?? match.text,
|
|
151
|
-
kind: /require\s*\(/.test(match.text) ? "require" : "import",
|
|
152
|
-
};
|
|
153
|
-
}), maxBytes);
|
|
154
|
-
}
|
|
155
|
-
export async function findExports(root, maxResults = 200, maxBytes = DEFAULT_OUTPUT_BYTES) {
|
|
156
|
-
const matches = await readMatches(root, String.raw `\bexport\s+(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][A-Za-z0-9_$]*)`, { codeOnly: true, regex: true, maxResults });
|
|
157
|
-
return boundMatches(matches.map((match) => ({
|
|
158
|
-
...match,
|
|
159
|
-
symbol: match.text.match(/(?:function|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][A-Za-z0-9_$]*)/)?.[1] ?? "default",
|
|
160
|
-
kind: "export",
|
|
161
|
-
})), maxBytes);
|
|
162
|
-
}
|
|
163
143
|
function symbolKind(line) {
|
|
164
144
|
if (/\bclass\b/i.test(line))
|
|
165
145
|
return "class";
|
|
@@ -8,7 +8,9 @@ export async function diagnostics(root, maxResults = 200) {
|
|
|
8
8
|
const entries = await listFiles(root);
|
|
9
9
|
const results = [];
|
|
10
10
|
for (const entry of entries) {
|
|
11
|
-
if (results.length >= maxResults
|
|
11
|
+
if (results.length >= maxResults)
|
|
12
|
+
break;
|
|
13
|
+
if (entry.type !== "file")
|
|
12
14
|
continue;
|
|
13
15
|
const target = path.join(resolvedRoot, entry.path);
|
|
14
16
|
let stat;
|
|
@@ -9,33 +9,14 @@ export interface FilePatch {
|
|
|
9
9
|
count?: number;
|
|
10
10
|
}
|
|
11
11
|
export declare function listFiles(root: string): Promise<FileEntry[]>;
|
|
12
|
-
export declare function listDir(root: string, relativePath?: string): Promise<FileEntry[]>;
|
|
13
|
-
export declare function listDirs(root: string, relativePaths: string[]): Promise<Array<{
|
|
14
|
-
path: string;
|
|
15
|
-
entries: FileEntry[];
|
|
16
|
-
}>>;
|
|
17
12
|
export declare function readFile(root: string, relativePath: string): Promise<string>;
|
|
18
|
-
export declare function readRange(root: string, relativePath: string, startLine: number, endLine: number): Promise<{
|
|
13
|
+
export declare function readRange(root: string, relativePath: string, startLine: number, endLine: number, maxBytes?: number): Promise<{
|
|
19
14
|
path: string;
|
|
20
15
|
startLine: number;
|
|
21
16
|
endLine: number;
|
|
22
17
|
content: string;
|
|
18
|
+
truncated: boolean;
|
|
23
19
|
}>;
|
|
24
|
-
export declare function writeFile(root: string, relativePath: string, content: string): Promise<void>;
|
|
25
|
-
export declare function patchFiles(root: string, files: Array<{
|
|
26
|
-
path: string;
|
|
27
|
-
patches: FilePatch[];
|
|
28
|
-
}>): Promise<Array<{
|
|
29
|
-
path: string;
|
|
30
|
-
applied: number;
|
|
31
|
-
content: string;
|
|
32
|
-
}>>;
|
|
33
|
-
export declare function patchFile(root: string, relativePath: string, patches: FilePatch[]): Promise<{
|
|
34
|
-
path: string;
|
|
35
|
-
applied: number;
|
|
36
|
-
content: string;
|
|
37
|
-
}>;
|
|
38
|
-
export declare function deleteFile(root: string, relativePath: string): Promise<void>;
|
|
39
20
|
export type FileChange = {
|
|
40
21
|
kind: "write";
|
|
41
22
|
path: string;
|
package/dist/src/tools/files.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
1
|
+
import { createReadStream, promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3
4
|
const IGNORED = new Set([".git", "node_modules", ".next", "dist", "build", "coverage"]);
|
|
4
5
|
export async function safePath(root, relativePath = ".", allowMissing = false) {
|
|
5
6
|
const resolvedRoot = await fs.realpath(root);
|
|
@@ -55,30 +56,10 @@ export async function listFiles(root) {
|
|
|
55
56
|
};
|
|
56
57
|
return walk(resolvedRoot);
|
|
57
58
|
}
|
|
58
|
-
export async function listDir(root, relativePath = ".") {
|
|
59
|
-
const resolvedRoot = await fs.realpath(root);
|
|
60
|
-
const directory = await safePath(resolvedRoot, relativePath);
|
|
61
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
62
|
-
return entries
|
|
63
|
-
.filter((entry) => !IGNORED.has(entry.name))
|
|
64
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
65
|
-
.map((entry) => ({
|
|
66
|
-
path: path.relative(resolvedRoot, path.join(directory, entry.name)),
|
|
67
|
-
type: entry.isDirectory() ? "directory" : "file",
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
70
|
-
export async function listDirs(root, relativePaths) {
|
|
71
|
-
if (relativePaths.length === 0)
|
|
72
|
-
throw new Error("At least one directory is required.");
|
|
73
|
-
return Promise.all(relativePaths.map(async (relativePath) => ({
|
|
74
|
-
path: relativePath,
|
|
75
|
-
entries: await listDir(root, relativePath),
|
|
76
|
-
})));
|
|
77
|
-
}
|
|
78
59
|
export function readFile(root, relativePath) {
|
|
79
60
|
return safePath(root, relativePath).then((target) => fs.readFile(target, "utf8"));
|
|
80
61
|
}
|
|
81
|
-
export async function readRange(root, relativePath, startLine, endLine) {
|
|
62
|
+
export async function readRange(root, relativePath, startLine, endLine, maxBytes = 8_000) {
|
|
82
63
|
if (!Number.isInteger(startLine) ||
|
|
83
64
|
!Number.isInteger(endLine) ||
|
|
84
65
|
startLine < 1 ||
|
|
@@ -86,58 +67,92 @@ export async function readRange(root, relativePath, startLine, endLine) {
|
|
|
86
67
|
throw new Error("Line range must use positive integers with endLine >= startLine.");
|
|
87
68
|
if (endLine - startLine > 2000)
|
|
88
69
|
throw new Error("A read range may contain at most 2000 lines.");
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
path: relativePath,
|
|
93
|
-
startLine,
|
|
94
|
-
endLine: Math.min(endLine, lines.length),
|
|
95
|
-
content: lines.slice(startLine - 1, endLine).join("\n"),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
export async function writeFile(root, relativePath, content) {
|
|
99
|
-
const target = await safePath(root, relativePath, true);
|
|
100
|
-
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
101
|
-
await fs.writeFile(target, content, "utf8");
|
|
102
|
-
}
|
|
103
|
-
export async function patchFiles(root, files) {
|
|
104
|
-
if (files.length === 0)
|
|
105
|
-
throw new Error("At least one file is required.");
|
|
106
|
-
return Promise.all(files.map(({ path: relativePath, patches }) => patchFile(root, relativePath, patches)));
|
|
107
|
-
}
|
|
108
|
-
export async function patchFile(root, relativePath, patches) {
|
|
109
|
-
if (patches.length === 0)
|
|
110
|
-
throw new Error("At least one patch is required.");
|
|
70
|
+
if (!Number.isInteger(maxBytes) || maxBytes < 1)
|
|
71
|
+
throw new Error("maxBytes must be a positive integer.");
|
|
111
72
|
const target = await safePath(root, relativePath);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
73
|
+
const decoder = new StringDecoder("utf8");
|
|
74
|
+
const stream = createReadStream(target, { highWaterMark: 64 * 1024 });
|
|
75
|
+
const output = [];
|
|
76
|
+
let line = "";
|
|
77
|
+
let lineNumber = 1;
|
|
78
|
+
let outputBytes = 0;
|
|
79
|
+
let truncated = false;
|
|
80
|
+
const marker = "…[truncated]";
|
|
81
|
+
const finishLine = () => {
|
|
82
|
+
if (lineNumber >= startLine && lineNumber <= endLine) {
|
|
83
|
+
if (truncated && line.endsWith(marker)) {
|
|
84
|
+
output.push(line);
|
|
85
|
+
outputBytes = maxBytes;
|
|
86
|
+
lineNumber++;
|
|
87
|
+
line = "";
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const value = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
91
|
+
const separatorBytes = output.length ? 1 : 0;
|
|
92
|
+
const remaining = maxBytes - outputBytes - separatorBytes;
|
|
93
|
+
if (Buffer.byteLength(value, "utf8") > remaining) {
|
|
94
|
+
const allowance = Math.max(0, remaining - Buffer.byteLength(marker, "utf8") - 1);
|
|
95
|
+
let clipped = Buffer.from(value, "utf8").subarray(0, allowance).toString("utf8");
|
|
96
|
+
while (clipped.endsWith("\uFFFD"))
|
|
97
|
+
clipped = clipped.slice(0, -1);
|
|
98
|
+
output.push(`${clipped}\n${marker}`);
|
|
99
|
+
outputBytes = maxBytes;
|
|
100
|
+
truncated = true;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
output.push(value);
|
|
104
|
+
outputBytes += separatorBytes + Buffer.byteLength(value, "utf8");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
lineNumber++;
|
|
108
|
+
line = "";
|
|
109
|
+
};
|
|
110
|
+
for await (const chunk of stream) {
|
|
111
|
+
const text = decoder.write(chunk);
|
|
124
112
|
let offset = 0;
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
113
|
+
while (offset < text.length) {
|
|
114
|
+
const newline = text.indexOf("\n", offset);
|
|
115
|
+
const end = newline === -1 ? text.length : newline;
|
|
116
|
+
if (lineNumber >= startLine && lineNumber <= endLine && !truncated) {
|
|
117
|
+
const piece = text.slice(offset, end);
|
|
118
|
+
const remaining = maxBytes - outputBytes - Buffer.byteLength(line, "utf8");
|
|
119
|
+
if (remaining <= 0) {
|
|
120
|
+
line = "";
|
|
121
|
+
truncated = true;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
if (Buffer.byteLength(piece, "utf8") > remaining) {
|
|
125
|
+
const allowance = Math.max(0, remaining - Buffer.byteLength(marker, "utf8") - 1);
|
|
126
|
+
let clipped = Buffer.from(piece, "utf8").subarray(0, allowance).toString("utf8");
|
|
127
|
+
while (clipped.endsWith("\uFFFD"))
|
|
128
|
+
clipped = clipped.slice(0, -1);
|
|
129
|
+
line += `${clipped}\n${marker}`;
|
|
130
|
+
truncated = true;
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
line += piece;
|
|
134
|
+
}
|
|
135
|
+
if (newline === -1)
|
|
136
|
+
break;
|
|
137
|
+
finishLine();
|
|
138
|
+
offset = newline + 1;
|
|
130
139
|
}
|
|
140
|
+
if (lineNumber > endLine || truncated)
|
|
141
|
+
break;
|
|
131
142
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
stream.destroy();
|
|
144
|
+
const tail = decoder.end();
|
|
145
|
+
if (tail && lineNumber >= startLine && lineNumber <= endLine && !truncated)
|
|
146
|
+
line += tail;
|
|
147
|
+
if (lineNumber >= startLine && lineNumber <= endLine && !(truncated && line.length === 0))
|
|
148
|
+
finishLine();
|
|
149
|
+
return {
|
|
150
|
+
path: relativePath,
|
|
151
|
+
startLine,
|
|
152
|
+
endLine: Math.min(endLine, lineNumber - 1),
|
|
153
|
+
content: output.join("\n"),
|
|
154
|
+
truncated,
|
|
155
|
+
};
|
|
141
156
|
}
|
|
142
157
|
export async function applyFileChanges(root, changes, dryRun = false) {
|
|
143
158
|
if (changes.length === 0)
|
package/dist/src/tools/git.d.ts
CHANGED
|
@@ -4,11 +4,4 @@ export interface GitResult {
|
|
|
4
4
|
stdout: string;
|
|
5
5
|
stderr: string;
|
|
6
6
|
}
|
|
7
|
-
export declare function gitStage(root: string, paths: string[]): Promise<GitResult>;
|
|
8
|
-
export declare function gitUnstage(root: string, paths: string[]): Promise<GitResult>;
|
|
9
|
-
export declare function gitCommit(root: string, message: string): Promise<GitResult>;
|
|
10
|
-
export declare function gitPush(root: string, remote?: string, branch?: string): Promise<GitResult>;
|
|
11
7
|
export declare function gitStatus(root: string): Promise<GitResult>;
|
|
12
|
-
export declare function gitDiff(root: string, staged?: boolean, pathspec?: string): Promise<GitResult>;
|
|
13
|
-
export declare function gitRestore(root: string, paths: string[], staged?: boolean): Promise<GitResult>;
|
|
14
|
-
export declare function gitLog(root: string, limit?: number, pathspec?: string): Promise<GitResult>;
|