frely-cli 0.23.9 → 0.23.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +49 -20
  2. package/README.zh-CN.md +47 -19
  3. package/dist/agent/local/approvals.d.ts +24 -0
  4. package/dist/agent/local/approvals.js +79 -0
  5. package/dist/agent/local/approvals.js.map +1 -0
  6. package/dist/agent/local/command.d.ts +10 -0
  7. package/dist/agent/local/command.js +292 -0
  8. package/dist/agent/local/command.js.map +1 -0
  9. package/dist/agent/local/confine.d.ts +20 -0
  10. package/dist/agent/local/confine.js +52 -0
  11. package/dist/agent/local/confine.js.map +1 -0
  12. package/dist/agent/local/manifest.d.ts +17 -0
  13. package/dist/agent/local/manifest.js +55 -0
  14. package/dist/agent/local/manifest.js.map +1 -0
  15. package/dist/agent/local/model-proxy.d.ts +18 -0
  16. package/dist/agent/local/model-proxy.js +111 -0
  17. package/dist/agent/local/model-proxy.js.map +1 -0
  18. package/dist/agent/local/source.d.ts +28 -0
  19. package/dist/agent/local/source.js +64 -0
  20. package/dist/agent/local/source.js.map +1 -0
  21. package/dist/agent/local/store.d.ts +45 -0
  22. package/dist/agent/local/store.js +78 -0
  23. package/dist/agent/local/store.js.map +1 -0
  24. package/dist/agent/local/supervisor.d.ts +9 -0
  25. package/dist/agent/local/supervisor.js +62 -0
  26. package/dist/agent/local/supervisor.js.map +1 -0
  27. package/dist/agent/run-store.d.ts +80 -0
  28. package/dist/agent/run-store.js +154 -0
  29. package/dist/agent/run-store.js.map +1 -0
  30. package/dist/agent/run.d.ts +41 -0
  31. package/dist/agent/run.js +233 -0
  32. package/dist/agent/run.js.map +1 -0
  33. package/dist/agent/workspace-read.d.ts +78 -0
  34. package/dist/agent/workspace-read.js +292 -0
  35. package/dist/agent/workspace-read.js.map +1 -0
  36. package/dist/agent-help.d.ts +158 -14
  37. package/dist/agent-help.js +33 -8
  38. package/dist/agent-help.js.map +1 -1
  39. package/dist/cloud.d.ts +2 -42
  40. package/dist/cloud.js +8 -260
  41. package/dist/cloud.js.map +1 -1
  42. package/dist/controller/client.d.ts +6 -3
  43. package/dist/controller/client.js +7 -8
  44. package/dist/controller/client.js.map +1 -1
  45. package/dist/controller/session.d.ts +7 -2
  46. package/dist/controller/session.js +79 -22
  47. package/dist/controller/session.js.map +1 -1
  48. package/dist/index.js +114 -9
  49. package/dist/index.js.map +1 -1
  50. package/dist/provider/control.d.ts +10 -0
  51. package/dist/provider/control.js +19 -0
  52. package/dist/provider/control.js.map +1 -1
  53. package/dist/provider/share.js +4 -1
  54. package/dist/provider/share.js.map +1 -1
  55. package/dist/runtime/sandbox.d.ts +20 -0
  56. package/dist/runtime/sandbox.js +39 -1
  57. package/dist/runtime/sandbox.js.map +1 -1
  58. package/dist/share/allowance.d.ts +8 -0
  59. package/dist/share/allowance.js +22 -0
  60. package/dist/share/allowance.js.map +1 -0
  61. package/dist/share/control.d.ts +2 -0
  62. package/dist/share/control.js +14 -10
  63. package/dist/share/control.js.map +1 -1
  64. package/dist/share/service-config.d.ts +39 -0
  65. package/dist/share/service-config.js +85 -0
  66. package/dist/share/service-config.js.map +1 -0
  67. package/dist/share/service-control.d.ts +15 -0
  68. package/dist/share/service-control.js +31 -0
  69. package/dist/share/service-control.js.map +1 -0
  70. package/dist/share/service-run.d.ts +55 -0
  71. package/dist/share/service-run.js +285 -0
  72. package/dist/share/service-run.js.map +1 -0
  73. package/dist/share/service-state.d.ts +16 -0
  74. package/dist/share/service-state.js +66 -0
  75. package/dist/share/service-state.js.map +1 -0
  76. package/dist/skill/access.d.ts +25 -6
  77. package/dist/skill/access.js +59 -9
  78. package/dist/skill/access.js.map +1 -1
  79. package/dist/skill/cloud-item.d.ts +2 -2
  80. package/dist/skill/cloud-item.js.map +1 -1
  81. package/dist/skill/entry.d.ts +1 -1
  82. package/dist/skill/entry.js +1 -1
  83. package/dist/skill/remote-mcp.d.ts +3 -0
  84. package/dist/skill/remote-mcp.js +7 -1
  85. package/dist/skill/remote-mcp.js.map +1 -1
  86. package/dist/skill/router.d.ts +57 -2
  87. package/dist/skill/router.js +76 -40
  88. package/dist/skill/router.js.map +1 -1
  89. package/dist/uninstall.d.ts +4 -0
  90. package/dist/uninstall.js +14 -0
  91. package/dist/uninstall.js.map +1 -1
  92. package/package.json +3 -2
@@ -0,0 +1,233 @@
1
+ import { spawn } from "node:child_process";
2
+ import { cliLaunchArguments } from "../cli-launch.js";
3
+ import { ClientToolsUnsupportedError, invokeInstalledAgent, openInstalledAgentSession, SkillAccessError, } from "../skill/access.js";
4
+ import { addUsage, createRun, effectiveStatus, listRuns, processAlive, readRun, resolveRun, resumable, writeRun, } from "./run-store.js";
5
+ import { READ_TOOLS, WorkspaceReadBridge } from "./workspace-read.js";
6
+ const MAX_TASK_BYTES = 128 * 1024;
7
+ const OVERVIEW_HEADER = "\n\n---\nLocal workspace (read-only file tools are available: read what you need, do not guess). Readable files:\n";
8
+ export const defaultRunDeps = {
9
+ openSession: (distributionId) => openInstalledAgentSession({ distributionId }),
10
+ invokePlain: async (distributionId, task, signal) => {
11
+ const result = await invokeInstalledAgent({ distributionId, task, signal });
12
+ return { text: result.text, modelId: result.modelId, ...("usage" in result && result.usage ? { usage: result.usage } : {}) };
13
+ },
14
+ spawnWorker: (runId) => {
15
+ // Detached, so Ctrl-C, a host command timeout or a killed parent does not stop the call it is waiting for.
16
+ const launch = cliLaunchArguments(process.argv[1] ?? "", ["agent", "__worker", runId]);
17
+ spawn(launch[0], launch.slice(1), { detached: true, stdio: "ignore", windowsHide: true, env: process.env }).unref();
18
+ },
19
+ stderr: (text) => { process.stderr.write(text); },
20
+ pollMs: 400,
21
+ };
22
+ /** Create the run record, start the detached worker and wait for the end. The same call also serves `frely agent result --wait`. */
23
+ export async function startRun(input, deps = defaultRunDeps) {
24
+ if (!input.task.trim() || Buffer.byteLength(input.task, "utf8") > MAX_TASK_BYTES)
25
+ throw new SkillAccessError("input_invalid", "Agent input must be non-empty and at most 128 KiB.");
26
+ const record = await createRun({
27
+ distributionId: input.distributionId,
28
+ task: input.task,
29
+ ...(input.read === undefined ? {} : { readRoot: input.read.root, limits: input.read.limits }),
30
+ });
31
+ deps.stderr(`Run ${record.runId} started. If this command is cut off the run keeps going: frely agent result ${record.runId} --wait\n`);
32
+ deps.spawnWorker(record.runId);
33
+ return waitForRun(record.runId, deps);
34
+ }
35
+ /** Poll the local record until the run is no longer running; prints each newly read file to stderr. */
36
+ export async function waitForRun(runId, deps = defaultRunDeps) {
37
+ const seen = new Set();
38
+ while (true) {
39
+ const record = await readRun(runId);
40
+ if (record === null)
41
+ throw new SkillAccessError("remote_call_failed", "The run record is no longer on this computer.");
42
+ for (const file of record.filesRead) {
43
+ if (!seen.has(file.path)) {
44
+ seen.add(file.path);
45
+ deps.stderr(`read ${file.path}\n`);
46
+ }
47
+ }
48
+ if (effectiveStatus(record) !== "running")
49
+ return { ...record, status: effectiveStatus(record) };
50
+ await new Promise((resolve) => setTimeout(resolve, deps.pollMs));
51
+ }
52
+ }
53
+ /** A finished run as the foreground command reports it: the result, or the error the call would have thrown. */
54
+ export function runOutcome(record) {
55
+ if (record.status === "completed") {
56
+ return {
57
+ ok: true,
58
+ runId: record.runId,
59
+ distributionId: record.distributionId,
60
+ ...(record.modelId === undefined ? {} : { modelId: record.modelId }),
61
+ text: record.text ?? "",
62
+ ...(record.readRoot === undefined ? {} : { filesRead: record.filesRead, rounds: record.rounds.length }),
63
+ ...(record.usage.total_tokens > 0 ? { usage: record.usage } : {}),
64
+ ...(record.toolsUnsupported === true ? { toolsUnsupported: true } : {}),
65
+ };
66
+ }
67
+ throw runFailure(record);
68
+ }
69
+ export function runFailure(record) {
70
+ if (record.status === "interrupted") {
71
+ return new SkillAccessError("remote_call_failed", `The run was interrupted before it finished. Run \`frely agent result ${record.runId} --resume --wait\` to continue it; the interrupted round is billed again.`);
72
+ }
73
+ if (record.status === "cancelled")
74
+ return new SkillAccessError("remote_call_failed", `The run ${record.runId} was cancelled.`);
75
+ const code = (record.error?.code ?? "remote_call_failed");
76
+ return new SkillAccessError(code, record.error?.message ?? "The remote Frely Agent call failed.");
77
+ }
78
+ /** The state `frely agent result` and `frely agent runs` print for a run (no task text; the result text only when finished). */
79
+ export function runSummary(record) {
80
+ const status = effectiveStatus(record);
81
+ return {
82
+ ok: status === "running" || status === "completed",
83
+ runId: record.runId,
84
+ status,
85
+ distributionId: record.distributionId,
86
+ createdAt: record.createdAt,
87
+ updatedAt: record.updatedAt,
88
+ rounds: record.rounds.length,
89
+ filesRead: record.filesRead,
90
+ usage: record.usage,
91
+ ...(status === "completed" ? { text: record.text ?? "" } : {}),
92
+ ...(status === "completed" || record.error === undefined ? {} : { error: record.error }),
93
+ resumable: resumable(record),
94
+ ...(status === "running" ? { note: "Still running. Progress so far is listed; run `frely agent result <runId> --wait` to wait for the answer." } : {}),
95
+ };
96
+ }
97
+ export async function listRunSummaries() {
98
+ return (await listRuns()).slice(0, 20).map((record) => {
99
+ const { filesRead, usage, ...rest } = runSummary(record);
100
+ return { ...rest, filesRead: record.filesRead.length, totalTokens: usage.total_tokens };
101
+ });
102
+ }
103
+ /** Stop a run: ask its worker to abort the call (the server stops and settles what was used), or mark an orphaned record. */
104
+ export async function cancelRun(target, deps = defaultRunDeps) {
105
+ const record = await resolveRun(target);
106
+ if (record.status !== "running")
107
+ return record;
108
+ if (processAlive(record.workerPid)) {
109
+ try {
110
+ process.kill(record.workerPid, "SIGTERM");
111
+ }
112
+ catch { /* it just ended */ }
113
+ for (let waited = 0; waited < 5000; waited += deps.pollMs) {
114
+ await new Promise((resolve) => setTimeout(resolve, deps.pollMs));
115
+ const current = await readRun(record.runId);
116
+ if (current !== null && current.status !== "running")
117
+ return current;
118
+ }
119
+ }
120
+ const latest = await readRun(record.runId) ?? record;
121
+ if (latest.status !== "running")
122
+ return latest;
123
+ return writeRun({ ...latest, status: "cancelled" });
124
+ }
125
+ /** Prepare an interrupted or failed run to continue with the items it already has, and start a worker for it. */
126
+ export async function resumeRun(target, deps = defaultRunDeps) {
127
+ const record = await resolveRun(target);
128
+ if (!resumable(record))
129
+ throw new Error(effectiveStatus(record) === "running" ? "This run is still running." : "This run cannot be resumed.");
130
+ const { error: _error, workerPid: _pid, ...rest } = record;
131
+ const next = await writeRun({ ...rest, status: "running", resumeCount: record.resumeCount + 1 });
132
+ deps.stderr(`Resuming run ${record.runId}. The round that was interrupted is billed again.\n`);
133
+ deps.spawnWorker(record.runId);
134
+ return next;
135
+ }
136
+ /** The worker side: runs the call (and the tool rounds) of one record, saving after every step. */
137
+ export async function executeRun(runId, deps = defaultRunDeps, signal = new AbortController().signal) {
138
+ const loaded = await readRun(runId);
139
+ if (loaded === null || loaded.status !== "running")
140
+ return;
141
+ let record = await writeRun({ ...loaded, workerPid: process.pid });
142
+ const save = async (patch) => { record = await writeRun({ ...record, ...patch }); };
143
+ try {
144
+ if (record.readRoot === undefined) {
145
+ const result = await deps.invokePlain(record.distributionId, record.task, signal);
146
+ await save({ status: "completed", modelId: result.modelId, text: result.text });
147
+ return;
148
+ }
149
+ await runWithTools(record, save, () => record, deps, signal);
150
+ }
151
+ catch (error) {
152
+ if (signal.aborted) {
153
+ await save({ status: "cancelled" });
154
+ return;
155
+ }
156
+ const code = error instanceof SkillAccessError ? error.code : "remote_call_failed";
157
+ const message = error instanceof SkillAccessError ? error.message : "The remote Frely Agent call failed.";
158
+ await save({ status: "failed", error: { code, message } });
159
+ }
160
+ }
161
+ async function runWithTools(start, save, current, deps, signal) {
162
+ const limits = start.limits;
163
+ if (limits === undefined || start.readRoot === undefined)
164
+ throw new SkillAccessError("input_invalid", "The run has no workspace.");
165
+ const bridge = await WorkspaceReadBridge.open(start.readRoot, limits);
166
+ bridge.restore({ usedBytes: start.bridge?.usedBytes ?? 0, roundsAnswered: start.bridge?.roundsAnswered ?? 0, filesRead: start.filesRead });
167
+ const task = await withOverview(start.task, bridge);
168
+ const session = await deps.openSession(start.distributionId);
169
+ await save({ modelId: session.modelId });
170
+ const hardCap = limits.maxRounds + 3;
171
+ while (true) {
172
+ if (signal.aborted)
173
+ throw new Error("aborted");
174
+ const items = [...current().items];
175
+ const pending = unansweredCalls(items);
176
+ if (pending.length > 0) {
177
+ const outputs = await bridge.answerRound(pending);
178
+ for (const output of outputs)
179
+ items.push({ type: "function_call_output", call_id: output.callId, output: output.output });
180
+ await save({ items, filesRead: bridge.filesRead(), bridge: { usedBytes: bridge.state().usedBytes, roundsAnswered: bridge.state().roundsAnswered } });
181
+ }
182
+ if (current().rounds.length >= hardCap)
183
+ throw new SkillAccessError("remote_call_failed", "The Agent kept asking for files after the read budget was used up.");
184
+ const startedAt = new Date().toISOString();
185
+ let turn;
186
+ try {
187
+ turn = await session.turn({ task, tools: READ_TOOLS, items, signal });
188
+ }
189
+ catch (error) {
190
+ if (error instanceof ClientToolsUnsupportedError && current().rounds.length === 0) {
191
+ // The Agent takes no caller tools: do the plain single call, as if --read had not been given.
192
+ const result = await deps.invokePlain(start.distributionId, start.task, signal);
193
+ await save({ status: "completed", modelId: result.modelId, text: result.text, toolsUnsupported: true });
194
+ return;
195
+ }
196
+ throw error;
197
+ }
198
+ const round = {
199
+ n: current().rounds.length + 1,
200
+ ...(turn.requestId === undefined ? {} : { requestId: turn.requestId }),
201
+ startedAt,
202
+ endedAt: new Date().toISOString(),
203
+ toolCalls: turn.status === "tool_calls" ? turn.calls.map((call) => call.name) : [],
204
+ ...(turn.usage === undefined ? {} : { usage: turn.usage }),
205
+ };
206
+ const usage = addUsage(current().usage, turn.usage);
207
+ if (turn.status === "completed") {
208
+ await save({ status: "completed", text: turn.text, rounds: [...current().rounds, round], usage });
209
+ return;
210
+ }
211
+ await save({ rounds: [...current().rounds, round], usage, items: [...items, ...turn.items] });
212
+ }
213
+ }
214
+ function unansweredCalls(items) {
215
+ const answered = new Set(items.flatMap((item) => (item.type === "function_call_output" ? [item.call_id] : [])));
216
+ return items.flatMap((item) => (item.type === "function_call" && !answered.has(item.call_id) ? [{ callId: item.call_id, name: item.name, arguments: item.arguments }] : []));
217
+ }
218
+ /** Task plus the path tree of the readable files, kept inside the 128 KiB input limit. */
219
+ async function withOverview(task, bridge) {
220
+ const room = MAX_TASK_BYTES - Buffer.byteLength(task, "utf8") - Buffer.byteLength(OVERVIEW_HEADER, "utf8");
221
+ if (room < 256)
222
+ return task;
223
+ const overview = await bridge.overview(Math.min(8 * 1024, room));
224
+ return overview === "" ? task : `${task}${OVERVIEW_HEADER}${overview}`;
225
+ }
226
+ /** Entry of `frely agent __worker <runId>`: cancel on SIGTERM, ignore the terminal hang-up. */
227
+ export async function workerMain(runId, deps = defaultRunDeps) {
228
+ const controller = new AbortController();
229
+ process.on("SIGTERM", () => controller.abort());
230
+ process.on("SIGHUP", () => undefined);
231
+ await executeRun(runId, deps, controller.signal);
232
+ }
233
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/agent/run.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ import type { CallerToolDefinition } from "../skill/router.js";
2
+ /** One tool result is cut at this size (the Agent runtime rejects larger caller tool outputs). */
3
+ export declare const TOOL_OUTPUT_LIMIT_BYTES: number;
4
+ export declare const DEFAULT_TOTAL_READ_BYTES: number;
5
+ export declare const DEFAULT_MAX_ROUNDS = 16;
6
+ export interface ReadLimits {
7
+ /** Total tool output handed to the Agent in one run. */
8
+ readonly totalBytes: number;
9
+ /** Rounds in which tool calls are answered; later calls get "budget exhausted". */
10
+ readonly maxRounds: number;
11
+ }
12
+ export declare const DEFAULT_READ_LIMITS: ReadLimits;
13
+ export interface FileRead {
14
+ readonly path: string;
15
+ readonly bytes: number;
16
+ }
17
+ /**
18
+ * Read-only tools, with the same names and parameters as the FrelyMCP workspace tools. They run on this computer; the Agent only sees
19
+ * what a call returns.
20
+ */
21
+ export declare const READ_TOOLS: readonly CallerToolDefinition[];
22
+ /**
23
+ * Why a workspace path never leaves this computer, or null. Applies to every read-only tool result: git internals, environment files,
24
+ * key material, package-manager credentials. Gitignored files and binaries are checked separately (they need git and the file content).
25
+ */
26
+ export declare function localDenyReason(relativePath: string): string | null;
27
+ /** Which of these workspace-relative paths git ignores. Outside a git repository nothing is ignored. */
28
+ export declare function gitIgnoredPaths(root: string, paths: readonly string[]): Promise<Set<string>>;
29
+ /** The repository root for `--read` without a directory: the git top level of the current directory, else the directory itself. */
30
+ export declare function defaultReadRoot(cwd: string): Promise<string>;
31
+ export declare class WorkspaceReadBridge {
32
+ private readonly workspace;
33
+ readonly limits: ReadLimits;
34
+ private used;
35
+ private roundsAnswered;
36
+ private readonly reads;
37
+ private constructor();
38
+ static open(root: string, limits?: ReadLimits): Promise<WorkspaceReadBridge>;
39
+ get root(): string;
40
+ /** Restore the counters of an earlier part of the same run (resume). */
41
+ restore(state: {
42
+ readonly usedBytes: number;
43
+ readonly roundsAnswered: number;
44
+ readonly filesRead: readonly FileRead[];
45
+ }): void;
46
+ state(): {
47
+ usedBytes: number;
48
+ roundsAnswered: number;
49
+ filesRead: FileRead[];
50
+ };
51
+ filesRead(): FileRead[];
52
+ /** True once the Agent has used up the round budget; its next tool calls only get "budget exhausted". */
53
+ get roundBudgetSpent(): boolean;
54
+ /** Answer one round of calls. Never throws: a failure is the tool's output. Returns `onRead(path)` events through `log`. */
55
+ answerRound(calls: readonly {
56
+ callId: string;
57
+ name: string;
58
+ arguments: string;
59
+ }[], log?: (path: string) => void): Promise<{
60
+ callId: string;
61
+ output: string;
62
+ }[]>;
63
+ private exhaustedMessage;
64
+ private run;
65
+ private sanitize;
66
+ private noteRead;
67
+ private dispatch;
68
+ private ignored;
69
+ /** Drop denied and gitignored entries from a listing. */
70
+ private visible;
71
+ /**
72
+ * A path tree of the files the Agent may read, cut at `maxBytes`: the first thing it gets, so it can choose what to open. Uses
73
+ * `git ls-files` (tracked and untracked, not ignored) in a repository, else a bounded walk.
74
+ */
75
+ overview(maxBytes?: number): Promise<string>;
76
+ }
77
+ /** Cut at a byte limit on a character boundary, saying so, so the Agent knows to ask for a line range. */
78
+ export declare function limitBytes(text: string, limit: number): string;
@@ -0,0 +1,292 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { basename } from "node:path";
3
+ import { promisify } from "node:util";
4
+ import { Workspace } from "../runtime/workspace.js";
5
+ const execute = promisify(execFile);
6
+ /** One tool result is cut at this size (the Agent runtime rejects larger caller tool outputs). */
7
+ export const TOOL_OUTPUT_LIMIT_BYTES = 64 * 1024;
8
+ export const DEFAULT_TOTAL_READ_BYTES = 1024 * 1024;
9
+ export const DEFAULT_MAX_ROUNDS = 16;
10
+ const OVERVIEW_LIMIT_BYTES = 8 * 1024;
11
+ const DENIED = "denied by local policy";
12
+ export const DEFAULT_READ_LIMITS = Object.freeze({ totalBytes: DEFAULT_TOTAL_READ_BYTES, maxRounds: DEFAULT_MAX_ROUNDS });
13
+ /**
14
+ * Read-only tools, with the same names and parameters as the FrelyMCP workspace tools. They run on this computer; the Agent only sees
15
+ * what a call returns.
16
+ */
17
+ export const READ_TOOLS = Object.freeze([
18
+ tool("workspace_info", "Describe the workspace and the limits of these read-only tools.", {}),
19
+ tool("list_directory", "List a directory of the workspace (path relative to the workspace root).", { path: { type: "string" } }),
20
+ tool("find_files", "Find files by glob pattern, for example **/*.ts.", { path: { type: "string" }, pattern: { type: "string" }, maxResults: { type: "integer" } }, ["pattern"]),
21
+ tool("search_files", "Search file contents for text (or a regular expression).", {
22
+ path: { type: "string" }, query: { type: "string" }, regex: { type: "boolean" }, caseSensitive: { type: "boolean" }, maxResults: { type: "integer" }, contextLines: { type: "integer" },
23
+ }, ["query"]),
24
+ tool("read_file", "Read a text file (up to 64 KiB per call; use startLine/endLine for the rest).", { path: { type: "string" }, startLine: { type: "integer" }, endLine: { type: "integer" } }, ["path"]),
25
+ tool("read_file_lines", "Read a line range of a text file.", { path: { type: "string" }, startLine: { type: "integer" }, endLine: { type: "integer" } }, ["path", "startLine"]),
26
+ tool("stat_path", "Size and type of a file or directory.", { path: { type: "string" } }, ["path"]),
27
+ ]);
28
+ function tool(name, description, properties, required = []) {
29
+ return { type: "function", name, description, parameters: { type: "object", properties, required, additionalProperties: false } };
30
+ }
31
+ /**
32
+ * Why a workspace path never leaves this computer, or null. Applies to every read-only tool result: git internals, environment files,
33
+ * key material, package-manager credentials. Gitignored files and binaries are checked separately (they need git and the file content).
34
+ */
35
+ export function localDenyReason(relativePath) {
36
+ const segments = relativePath.split(/[\\/]+/u).filter((segment) => segment !== "" && segment !== ".");
37
+ if (segments.some((segment) => segment.toLowerCase() === ".git"))
38
+ return DENIED;
39
+ const name = (segments.at(-1) ?? "").toLowerCase();
40
+ if (name === ".env" || name.startsWith(".env.") || name.startsWith(".env-"))
41
+ return DENIED;
42
+ if (/\.(pem|key|p12)$/u.test(name))
43
+ return DENIED;
44
+ if (/^id_/u.test(name))
45
+ return DENIED;
46
+ if (name === ".npmrc" || name === ".netrc" || name === ".pypirc")
47
+ return DENIED;
48
+ return null;
49
+ }
50
+ /** Which of these workspace-relative paths git ignores. Outside a git repository nothing is ignored. */
51
+ export async function gitIgnoredPaths(root, paths) {
52
+ if (paths.length === 0)
53
+ return new Set();
54
+ return new Promise((resolve) => {
55
+ const child = spawn("git", ["-C", root, "check-ignore", "--stdin", "-z"], { stdio: ["pipe", "pipe", "ignore"], windowsHide: true });
56
+ const chunks = [];
57
+ child.stdout.on("data", (chunk) => chunks.push(chunk));
58
+ child.once("error", () => resolve(new Set()));
59
+ child.once("close", (code) => resolve(code === 0 ? new Set(Buffer.concat(chunks).toString("utf8").split("\0").filter(Boolean)) : new Set()));
60
+ child.stdin.once("error", () => undefined);
61
+ child.stdin.end(paths.join("\0") + "\0");
62
+ });
63
+ }
64
+ /** The repository root for `--read` without a directory: the git top level of the current directory, else the directory itself. */
65
+ export async function defaultReadRoot(cwd) {
66
+ try {
67
+ const { stdout } = await execute("git", ["-C", cwd, "rev-parse", "--show-toplevel"], { windowsHide: true });
68
+ const top = stdout.trim();
69
+ return top === "" ? cwd : top;
70
+ }
71
+ catch {
72
+ return cwd;
73
+ }
74
+ }
75
+ export class WorkspaceReadBridge {
76
+ workspace;
77
+ limits;
78
+ used = 0;
79
+ roundsAnswered = 0;
80
+ reads = new Map();
81
+ constructor(workspace, limits) {
82
+ this.workspace = workspace;
83
+ this.limits = limits;
84
+ }
85
+ static async open(root, limits = DEFAULT_READ_LIMITS) {
86
+ return new WorkspaceReadBridge(await Workspace.open(root), limits);
87
+ }
88
+ get root() { return this.workspace.root; }
89
+ /** Restore the counters of an earlier part of the same run (resume). */
90
+ restore(state) {
91
+ this.used = state.usedBytes;
92
+ this.roundsAnswered = state.roundsAnswered;
93
+ for (const file of state.filesRead)
94
+ this.reads.set(file.path, file.bytes);
95
+ }
96
+ state() {
97
+ return { usedBytes: this.used, roundsAnswered: this.roundsAnswered, filesRead: this.filesRead() };
98
+ }
99
+ filesRead() {
100
+ return [...this.reads].map(([path, bytes]) => ({ path, bytes }));
101
+ }
102
+ /** True once the Agent has used up the round budget; its next tool calls only get "budget exhausted". */
103
+ get roundBudgetSpent() { return this.roundsAnswered >= this.limits.maxRounds; }
104
+ /** Answer one round of calls. Never throws: a failure is the tool's output. Returns `onRead(path)` events through `log`. */
105
+ async answerRound(calls, log = () => undefined) {
106
+ const exhausted = this.roundBudgetSpent;
107
+ if (!exhausted)
108
+ this.roundsAnswered += 1;
109
+ const outputs = [];
110
+ for (const call of calls) {
111
+ outputs.push({ callId: call.callId, output: exhausted || this.used >= this.limits.totalBytes ? this.exhaustedMessage() : await this.run(call, log) });
112
+ }
113
+ return outputs;
114
+ }
115
+ exhaustedMessage() {
116
+ return "budget exhausted: no more files can be read in this run. Answer with what you have already read.";
117
+ }
118
+ async run(call, log) {
119
+ let args;
120
+ try {
121
+ const parsed = call.arguments.trim() === "" ? {} : JSON.parse(call.arguments);
122
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
123
+ throw new Error("not an object");
124
+ args = parsed;
125
+ }
126
+ catch {
127
+ return "error: arguments must be a JSON object";
128
+ }
129
+ let output;
130
+ try {
131
+ output = await this.dispatch(call.name, args, log);
132
+ }
133
+ catch (error) {
134
+ output = `error: ${this.sanitize(error instanceof Error ? error.message : "tool failed")}`;
135
+ }
136
+ output = limitBytes(output, Math.min(TOOL_OUTPUT_LIMIT_BYTES, Math.max(0, this.limits.totalBytes - this.used)));
137
+ this.used += Buffer.byteLength(output, "utf8");
138
+ return output;
139
+ }
140
+ sanitize(message) {
141
+ return message.split(this.workspace.root).join(".");
142
+ }
143
+ noteRead(path, bytes, log) {
144
+ if (!this.reads.has(path))
145
+ log(path);
146
+ this.reads.set(path, (this.reads.get(path) ?? 0) + bytes);
147
+ }
148
+ async dispatch(name, args, log) {
149
+ switch (name) {
150
+ case "workspace_info":
151
+ return JSON.stringify({
152
+ workspace: basename(this.workspace.root),
153
+ tools: READ_TOOLS.map((entry) => entry.name),
154
+ limits: { toolOutputBytes: TOOL_OUTPUT_LIMIT_BYTES, totalBytes: this.limits.totalBytes, rounds: this.limits.maxRounds },
155
+ note: "Read-only. Git internals, .env files, key files, files ignored by git and binary files are not readable.",
156
+ });
157
+ case "list_directory": {
158
+ const path = pathArg(args, "path", ".");
159
+ if (localDenyReason(path) !== null || await this.ignored(path))
160
+ return DENIED;
161
+ const info = await this.workspace.statPath(path);
162
+ if (info.type !== "directory")
163
+ return JSON.stringify(info);
164
+ const entries = await this.workspace.listDirectory(path);
165
+ const kept = await this.visible(entries.map((entry) => ({ entry, path: joinPath(path, entry.name) })));
166
+ return JSON.stringify(kept.map(({ entry }) => entry));
167
+ }
168
+ case "stat_path": {
169
+ const path = pathArg(args, "path");
170
+ if (localDenyReason(path) !== null || await this.ignored(path))
171
+ return DENIED;
172
+ return JSON.stringify(await this.workspace.statPath(path));
173
+ }
174
+ case "find_files": {
175
+ const path = pathArg(args, "path", ".");
176
+ if (localDenyReason(path) !== null)
177
+ return DENIED;
178
+ const found = await this.workspace.findFiles(path, stringArg(args, "pattern"), intArg(args, "maxResults", 100, 1, 1000));
179
+ return JSON.stringify((await this.visible(found.map((file) => ({ file, path: file })))).map(({ file }) => file));
180
+ }
181
+ case "search_files": {
182
+ const path = pathArg(args, "path", ".");
183
+ if (localDenyReason(path) !== null)
184
+ return DENIED;
185
+ const hits = await this.workspace.searchFiles(path, stringArg(args, "query"), {
186
+ regex: args.regex === true,
187
+ caseSensitive: args.caseSensitive === true,
188
+ maxResults: intArg(args, "maxResults", 50, 1, 500),
189
+ contextLines: intArg(args, "contextLines", 0, 0, 10),
190
+ });
191
+ const kept = await this.visible(hits.map((hit) => ({ hit, path: hit.path })));
192
+ for (const { hit } of kept)
193
+ this.noteRead(hit.path, Buffer.byteLength(hit.text, "utf8"), log);
194
+ return JSON.stringify(kept.map(({ hit }) => hit));
195
+ }
196
+ case "read_file":
197
+ case "read_file_lines": {
198
+ const path = pathArg(args, "path");
199
+ if (localDenyReason(path) !== null || await this.ignored(path))
200
+ return DENIED;
201
+ const file = await this.workspace.readFileBytes(path);
202
+ if (file.data.subarray(0, 8000).includes(0))
203
+ return `${DENIED} (binary file)`;
204
+ const ranged = name === "read_file_lines" || args.startLine !== undefined || args.endLine !== undefined;
205
+ const text = ranged
206
+ ? (await this.workspace.readFileLines(path, intArg(args, "startLine", 1, 1, Number.MAX_SAFE_INTEGER), args.endLine === undefined ? undefined : intArg(args, "endLine", 1, 1, Number.MAX_SAFE_INTEGER))).content
207
+ : file.data.toString("utf8");
208
+ const returned = limitBytes(text, TOOL_OUTPUT_LIMIT_BYTES);
209
+ this.noteRead(path, Buffer.byteLength(returned, "utf8"), log);
210
+ return returned;
211
+ }
212
+ default:
213
+ return `error: unknown tool ${name.slice(0, 64)}`;
214
+ }
215
+ }
216
+ async ignored(path) {
217
+ if (path === "." || path === "")
218
+ return false;
219
+ return (await gitIgnoredPaths(this.workspace.root, [normalize(path)])).size > 0;
220
+ }
221
+ /** Drop denied and gitignored entries from a listing. */
222
+ async visible(entries) {
223
+ const allowed = entries.filter((entry) => localDenyReason(entry.path) === null);
224
+ const ignored = await gitIgnoredPaths(this.workspace.root, allowed.map((entry) => normalize(entry.path)));
225
+ return allowed.filter((entry) => !ignored.has(normalize(entry.path)));
226
+ }
227
+ /**
228
+ * A path tree of the files the Agent may read, cut at `maxBytes`: the first thing it gets, so it can choose what to open. Uses
229
+ * `git ls-files` (tracked and untracked, not ignored) in a repository, else a bounded walk.
230
+ */
231
+ async overview(maxBytes = OVERVIEW_LIMIT_BYTES) {
232
+ let files;
233
+ try {
234
+ const { stdout } = await execute("git", ["-C", this.workspace.root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { maxBuffer: 16 * 1024 * 1024, windowsHide: true });
235
+ files = stdout.split("\0").filter(Boolean);
236
+ }
237
+ catch {
238
+ files = await this.workspace.findFiles(".", "*", 2000).catch(() => []);
239
+ }
240
+ files = [...new Set(files)].filter((file) => localDenyReason(file) === null).sort();
241
+ const lines = [];
242
+ let size = 0;
243
+ for (const file of files) {
244
+ size += Buffer.byteLength(file, "utf8") + 1;
245
+ if (size > maxBytes)
246
+ break;
247
+ lines.push(file);
248
+ }
249
+ const more = files.length - lines.length;
250
+ return `${lines.join("\n")}${more > 0 ? `\n... and ${more} more files` : ""}`;
251
+ }
252
+ }
253
+ function joinPath(base, name) {
254
+ return base === "." || base === "" ? name : `${base.replace(/[\\/]+$/u, "")}/${name}`;
255
+ }
256
+ function normalize(path) {
257
+ return path.split(/[\\/]+/u).filter((segment) => segment !== "" && segment !== ".").join("/");
258
+ }
259
+ function pathArg(args, name, fallback) {
260
+ const value = args[name];
261
+ if (value === undefined && fallback !== undefined)
262
+ return fallback;
263
+ if (typeof value !== "string" || value === "" || value.includes("\0"))
264
+ throw new Error(`${name} must be a non-empty path.`);
265
+ return value;
266
+ }
267
+ function stringArg(args, name) {
268
+ const value = args[name];
269
+ if (typeof value !== "string" || value === "")
270
+ throw new Error(`${name} is required.`);
271
+ return value;
272
+ }
273
+ function intArg(args, name, fallback, min, max) {
274
+ const value = args[name];
275
+ if (value === undefined)
276
+ return fallback;
277
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min || value > max)
278
+ throw new Error(`${name} must be an integer from ${min} to ${max}.`);
279
+ return value;
280
+ }
281
+ /** Cut at a byte limit on a character boundary, saying so, so the Agent knows to ask for a line range. */
282
+ export function limitBytes(text, limit) {
283
+ if (Buffer.byteLength(text, "utf8") <= limit)
284
+ return text;
285
+ const note = "\n[truncated: output limit reached; use read_file_lines for the rest]";
286
+ const room = Math.max(0, limit - Buffer.byteLength(note, "utf8"));
287
+ let cut = Buffer.from(text, "utf8").subarray(0, room).toString("utf8");
288
+ if (cut.endsWith("�"))
289
+ cut = cut.slice(0, -1);
290
+ return limit === 0 ? "" : cut + note;
291
+ }
292
+ //# sourceMappingURL=workspace-read.js.map