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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
const CONFIG_DIRECTORY = join(homedir(), ".config", "agent-dir");
|
|
5
|
+
export const TELEMETRY_DIRECTORY = join(CONFIG_DIRECTORY, "telemetry");
|
|
6
|
+
export const LEGACY_TELEMETRY_FILE = join(CONFIG_DIRECTORY, "telemetry.jsonl");
|
|
7
|
+
const LEGACY_ARCHIVE_FILE = join(CONFIG_DIRECTORY, "telemetry.legacy-archive.jsonl");
|
|
8
|
+
function safeSegment(value) {
|
|
9
|
+
const safe = value
|
|
10
|
+
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
11
|
+
.replace(/^\.+$/, "_")
|
|
12
|
+
.slice(0, 100);
|
|
13
|
+
return safe || "unknown";
|
|
14
|
+
}
|
|
15
|
+
export function telemetrySessionPath(configId, sessionId) {
|
|
16
|
+
return join(TELEMETRY_DIRECTORY, safeSegment(configId), `session-${safeSegment(sessionId)}.jsonl`);
|
|
17
|
+
}
|
|
18
|
+
function parseEvents(content) {
|
|
19
|
+
const events = [];
|
|
20
|
+
for (const line of content.split("\n")) {
|
|
21
|
+
if (!line.trim())
|
|
22
|
+
continue;
|
|
23
|
+
try {
|
|
24
|
+
const value = JSON.parse(line);
|
|
25
|
+
if (value &&
|
|
26
|
+
value.schemaVersion === 1 &&
|
|
27
|
+
typeof value.configId === "string" &&
|
|
28
|
+
typeof value.sessionId === "string" &&
|
|
29
|
+
typeof value.timestamp === "string" &&
|
|
30
|
+
value.event) {
|
|
31
|
+
events.push(value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Ignore malformed lines without discarding other valid events.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return events;
|
|
39
|
+
}
|
|
40
|
+
function readFileEvents(path) {
|
|
41
|
+
try {
|
|
42
|
+
return parseEvents(readFileSync(path, "utf8"));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function collectJsonlFiles(directory) {
|
|
49
|
+
if (!existsSync(directory))
|
|
50
|
+
return [];
|
|
51
|
+
const files = [];
|
|
52
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
53
|
+
const path = join(directory, entry.name);
|
|
54
|
+
if (entry.isDirectory())
|
|
55
|
+
files.push(...collectJsonlFiles(path));
|
|
56
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
57
|
+
files.push(path);
|
|
58
|
+
}
|
|
59
|
+
return files;
|
|
60
|
+
}
|
|
61
|
+
/** Migrate the old shared file once, preserving an archive and avoiding duplicate lines. */
|
|
62
|
+
export function migrateLegacyTelemetry() {
|
|
63
|
+
if (!existsSync(LEGACY_TELEMETRY_FILE))
|
|
64
|
+
return;
|
|
65
|
+
const legacyEvents = readFileEvents(LEGACY_TELEMETRY_FILE);
|
|
66
|
+
mkdirSync(TELEMETRY_DIRECTORY, { recursive: true, mode: 0o700 });
|
|
67
|
+
const groups = new Map();
|
|
68
|
+
for (const event of legacyEvents) {
|
|
69
|
+
const path = telemetrySessionPath(event.configId, event.sessionId);
|
|
70
|
+
const group = groups.get(path) ?? [];
|
|
71
|
+
group.push(event);
|
|
72
|
+
groups.set(path, group);
|
|
73
|
+
}
|
|
74
|
+
for (const [path, events] of groups) {
|
|
75
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
76
|
+
let existing = new Set();
|
|
77
|
+
try {
|
|
78
|
+
existing = new Set(readFileSync(path, "utf8").split("\n").filter(Boolean));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// New session file.
|
|
82
|
+
}
|
|
83
|
+
const missing = events
|
|
84
|
+
.map((event) => JSON.stringify(event))
|
|
85
|
+
.filter((line) => {
|
|
86
|
+
if (existing.has(line))
|
|
87
|
+
return false;
|
|
88
|
+
existing.add(line);
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
91
|
+
if (missing.length) {
|
|
92
|
+
appendFileSync(path, `${missing.join("\n")}\n`, { encoding: "utf8", mode: 0o600 });
|
|
93
|
+
chmodSync(path, 0o600);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
let archive = LEGACY_ARCHIVE_FILE;
|
|
97
|
+
if (existsSync(archive))
|
|
98
|
+
archive = `${LEGACY_ARCHIVE_FILE}.${Date.now()}`;
|
|
99
|
+
renameSync(LEGACY_TELEMETRY_FILE, archive);
|
|
100
|
+
}
|
|
101
|
+
export function readAllTelemetryEvents() {
|
|
102
|
+
migrateLegacyTelemetry();
|
|
103
|
+
return collectJsonlFiles(TELEMETRY_DIRECTORY)
|
|
104
|
+
.flatMap(readFileEvents)
|
|
105
|
+
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
106
|
+
}
|
|
107
|
+
export function resetTelemetry(profile, all = false) {
|
|
108
|
+
if (all) {
|
|
109
|
+
rmSync(TELEMETRY_DIRECTORY, { recursive: true, force: true });
|
|
110
|
+
rmSync(LEGACY_TELEMETRY_FILE, { force: true });
|
|
111
|
+
rmSync(LEGACY_ARCHIVE_FILE, { force: true });
|
|
112
|
+
for (const entry of existsSync(CONFIG_DIRECTORY)
|
|
113
|
+
? readdirSync(CONFIG_DIRECTORY, { withFileTypes: true })
|
|
114
|
+
: []) {
|
|
115
|
+
if (entry.isFile() && entry.name.startsWith("telemetry.legacy-archive.jsonl.")) {
|
|
116
|
+
rmSync(join(CONFIG_DIRECTORY, entry.name), { force: true });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!profile)
|
|
122
|
+
throw new Error("Specify a profile name or use --all.");
|
|
123
|
+
rmSync(join(TELEMETRY_DIRECTORY, safeSegment(profile)), { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
export function ensureTelemetryDirectory() {
|
|
126
|
+
mkdirSync(TELEMETRY_DIRECTORY, { recursive: true, mode: 0o700 });
|
|
127
|
+
try {
|
|
128
|
+
chmodSync(TELEMETRY_DIRECTORY, 0o700);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Best-effort permission hardening.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -20,11 +20,31 @@ export interface TelemetryMcpRequestEvent {
|
|
|
20
20
|
method: string;
|
|
21
21
|
success: boolean;
|
|
22
22
|
durationMs: number;
|
|
23
|
+
responseBytes?: number;
|
|
24
|
+
estimatedResponseTokens?: number;
|
|
23
25
|
errorCategory?: string;
|
|
24
26
|
}
|
|
27
|
+
export interface TelemetryFileMetric {
|
|
28
|
+
pathHash: string;
|
|
29
|
+
contentHash?: string;
|
|
30
|
+
bytes: number;
|
|
31
|
+
}
|
|
25
32
|
export interface TelemetryToolEvent {
|
|
26
33
|
event: "tool_call";
|
|
27
34
|
tool: string;
|
|
35
|
+
requestIdHash?: string;
|
|
36
|
+
inputFingerprint?: string;
|
|
37
|
+
outputFingerprint?: string;
|
|
38
|
+
/** Full local-only troubleshooting payload, emitted only at diagnostic level. */
|
|
39
|
+
diagnosticDetails?: Record<string, unknown>;
|
|
40
|
+
estimatedInputTokens?: number;
|
|
41
|
+
estimatedOutputTokens?: number;
|
|
42
|
+
cumulativeOutputBytes?: number;
|
|
43
|
+
cumulativeEstimatedOutputTokens?: number;
|
|
44
|
+
duplicateOutputBytes?: number;
|
|
45
|
+
textOutputBytes?: number;
|
|
46
|
+
uniqueFileCount?: number;
|
|
47
|
+
files?: TelemetryFileMetric[];
|
|
28
48
|
success: boolean;
|
|
29
49
|
durationMs: number;
|
|
30
50
|
inputBytes?: number;
|
|
@@ -71,6 +91,10 @@ export interface TelemetryRuntimeContext {
|
|
|
71
91
|
mcpClientName?: string;
|
|
72
92
|
mcpClientVersion?: string;
|
|
73
93
|
nodeVersion?: string;
|
|
94
|
+
distroId?: string;
|
|
95
|
+
distroName?: string;
|
|
96
|
+
distroVersion?: string;
|
|
97
|
+
kernelVersion?: string;
|
|
74
98
|
hostname?: string;
|
|
75
99
|
username?: string;
|
|
76
100
|
processId?: number;
|
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(?:[^"'\\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";
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
export declare function appendBoundedOutput(current: string, chunk: Buffer | string): string;
|
|
3
|
+
export declare function commandEnvironment(): NodeJS.ProcessEnv;
|
|
4
|
+
export declare function terminateProcessTree(child: ChildProcess): void;
|
|
5
|
+
export declare function processTreeSpawnOptions(): {
|
|
6
|
+
detached: boolean;
|
|
7
|
+
};
|
|
1
8
|
export interface CommandResult {
|
|
2
9
|
command: string;
|
|
3
10
|
exitCode: number;
|
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
3
|
+
const MAX_CAPTURE_BYTES = 512_000;
|
|
4
|
+
const TRUNCATION_MARKER = "\n…[output truncated at 512 KB]";
|
|
5
|
+
export function appendBoundedOutput(current, chunk) {
|
|
6
|
+
if (current.endsWith(TRUNCATION_MARKER) ||
|
|
7
|
+
Buffer.byteLength(current, "utf8") >= MAX_CAPTURE_BYTES)
|
|
8
|
+
return current;
|
|
9
|
+
const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
10
|
+
const available = MAX_CAPTURE_BYTES - Buffer.byteLength(current, "utf8");
|
|
11
|
+
if (incoming.byteLength <= available)
|
|
12
|
+
return current + incoming.toString("utf8");
|
|
13
|
+
const markerBytes = Buffer.byteLength(TRUNCATION_MARKER, "utf8");
|
|
14
|
+
const payloadBytes = Math.max(0, available - markerBytes);
|
|
15
|
+
let payload = incoming.subarray(0, payloadBytes).toString("utf8");
|
|
16
|
+
while (Buffer.byteLength(payload, "utf8") > payloadBytes)
|
|
17
|
+
payload = payload.slice(0, -1);
|
|
18
|
+
return current + payload + TRUNCATION_MARKER;
|
|
19
|
+
}
|
|
20
|
+
export function commandEnvironment() {
|
|
21
|
+
return Object.fromEntries(Object.entries(process.env).filter(([key]) => !/(?:TOKEN|SECRET|PASSWORD|PASSWD|API[_-]?KEY|PRIVATE[_-]?KEY|CREDENTIAL|AUTHORIZATION|ACCESS[_-]?KEY)/i.test(key)));
|
|
22
|
+
}
|
|
23
|
+
const FORCE_KILL_DELAY_MS = 1_000;
|
|
24
|
+
export function terminateProcessTree(child) {
|
|
25
|
+
const pid = child.pid;
|
|
26
|
+
if (!pid || child.killed || child.exitCode !== null)
|
|
27
|
+
return;
|
|
28
|
+
if (process.platform === "win32") {
|
|
29
|
+
const killer = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
30
|
+
stdio: "ignore",
|
|
31
|
+
windowsHide: true,
|
|
32
|
+
});
|
|
33
|
+
killer.on("error", () => child.kill("SIGKILL"));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
process.kill(-pid, "SIGTERM");
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
child.kill("SIGTERM");
|
|
41
|
+
}
|
|
42
|
+
const forceTimer = setTimeout(() => {
|
|
43
|
+
try {
|
|
44
|
+
process.kill(-pid, "SIGKILL");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
child.kill("SIGKILL");
|
|
48
|
+
}
|
|
49
|
+
}, FORCE_KILL_DELAY_MS);
|
|
50
|
+
forceTimer.unref();
|
|
51
|
+
child.once("close", () => clearTimeout(forceTimer));
|
|
52
|
+
}
|
|
53
|
+
export function processTreeSpawnOptions() {
|
|
54
|
+
return { detached: process.platform !== "win32" };
|
|
55
|
+
}
|
|
3
56
|
export function isCommandBlacklisted(command, args = [], blacklistedCommands = []) {
|
|
4
57
|
const invocation = [command, ...args];
|
|
5
58
|
return blacklistedCommands.some((entry) => {
|
|
@@ -17,7 +70,12 @@ export function runCommand(root, command, args = [], allowedCommands = [], black
|
|
|
17
70
|
if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string"))
|
|
18
71
|
throw new Error("Command arguments must be strings.");
|
|
19
72
|
return new Promise((resolve, reject) => {
|
|
20
|
-
const child = spawn(command, args, {
|
|
73
|
+
const child = spawn(command, args, {
|
|
74
|
+
cwd: root,
|
|
75
|
+
shell: false,
|
|
76
|
+
env: commandEnvironment(),
|
|
77
|
+
...processTreeSpawnOptions(),
|
|
78
|
+
});
|
|
21
79
|
let stdout = "";
|
|
22
80
|
let stderr = "";
|
|
23
81
|
let finished = false;
|
|
@@ -27,14 +85,14 @@ export function runCommand(root, command, args = [], allowedCommands = [], black
|
|
|
27
85
|
return;
|
|
28
86
|
finished = true;
|
|
29
87
|
clearTimeout(timer);
|
|
30
|
-
child
|
|
88
|
+
terminateProcessTree(child);
|
|
31
89
|
reject(new Error(`Command '${command}' was aborted.`));
|
|
32
90
|
};
|
|
33
91
|
timer = setTimeout(() => {
|
|
34
92
|
if (finished)
|
|
35
93
|
return;
|
|
36
94
|
finished = true;
|
|
37
|
-
child
|
|
95
|
+
terminateProcessTree(child);
|
|
38
96
|
signal?.removeEventListener("abort", abort);
|
|
39
97
|
reject(new Error(`Command '${command}' timed out after ${timeoutMs}ms.`));
|
|
40
98
|
}, timeoutMs);
|
|
@@ -44,10 +102,10 @@ export function runCommand(root, command, args = [], allowedCommands = [], black
|
|
|
44
102
|
}
|
|
45
103
|
signal?.addEventListener("abort", abort, { once: true });
|
|
46
104
|
child.stdout.on("data", (chunk) => {
|
|
47
|
-
stdout
|
|
105
|
+
stdout = appendBoundedOutput(stdout, chunk);
|
|
48
106
|
});
|
|
49
107
|
child.stderr.on("data", (chunk) => {
|
|
50
|
-
stderr
|
|
108
|
+
stderr = appendBoundedOutput(stderr, chunk);
|
|
51
109
|
});
|
|
52
110
|
child.on("error", (error) => {
|
|
53
111
|
if (finished)
|
|
@@ -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>;
|