@datename/fastcode-termux 0.0.0-stage → 0.1.6-beta
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/LICENSE +21 -0
- package/README.md +418 -2
- package/bin/fastcode.mjs +59 -0
- package/dist/agent/approval.d.ts +50 -0
- package/dist/agent/approval.js +7 -0
- package/dist/agent/compaction.d.ts +39 -0
- package/dist/agent/compaction.js +227 -0
- package/dist/agent/context.d.ts +29 -0
- package/dist/agent/context.js +62 -0
- package/dist/agent/guards.d.ts +43 -0
- package/dist/agent/guards.js +269 -0
- package/dist/agent/ignore.d.ts +11 -0
- package/dist/agent/ignore.js +129 -0
- package/dist/agent/limits.d.ts +24 -0
- package/dist/agent/limits.js +71 -0
- package/dist/agent/process-tree.d.ts +2 -0
- package/dist/agent/process-tree.js +31 -0
- package/dist/agent/protocol-mode.d.ts +6 -0
- package/dist/agent/protocol-mode.js +51 -0
- package/dist/agent/protocol.d.ts +4 -0
- package/dist/agent/protocol.js +129 -0
- package/dist/agent/registry.d.ts +17 -0
- package/dist/agent/registry.js +90 -0
- package/dist/agent/runner.d.ts +106 -0
- package/dist/agent/runner.js +352 -0
- package/dist/agent/shell-session.d.ts +37 -0
- package/dist/agent/shell-session.js +400 -0
- package/dist/agent/stream.d.ts +29 -0
- package/dist/agent/stream.js +103 -0
- package/dist/agent/tool-schemas.d.ts +2 -0
- package/dist/agent/tool-schemas.js +63 -0
- package/dist/agent/tools/append_file.d.ts +2 -0
- package/dist/agent/tools/append_file.js +26 -0
- package/dist/agent/tools/apply_patch.d.ts +10 -0
- package/dist/agent/tools/apply_patch.js +52 -0
- package/dist/agent/tools/copy_file.d.ts +2 -0
- package/dist/agent/tools/copy_file.js +30 -0
- package/dist/agent/tools/delete_file.d.ts +2 -0
- package/dist/agent/tools/delete_file.js +24 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +23 -0
- package/dist/agent/tools/index.d.ts +2 -0
- package/dist/agent/tools/index.js +32 -0
- package/dist/agent/tools/list_files.d.ts +2 -0
- package/dist/agent/tools/list_files.js +17 -0
- package/dist/agent/tools/make_dir.d.ts +2 -0
- package/dist/agent/tools/make_dir.js +24 -0
- package/dist/agent/tools/move_path.d.ts +2 -0
- package/dist/agent/tools/move_path.js +47 -0
- package/dist/agent/tools/paths.d.ts +7 -0
- package/dist/agent/tools/paths.js +84 -0
- package/dist/agent/tools/pwd.d.ts +2 -0
- package/dist/agent/tools/pwd.js +10 -0
- package/dist/agent/tools/read_file.d.ts +2 -0
- package/dist/agent/tools/read_file.js +21 -0
- package/dist/agent/tools/read_many.d.ts +2 -0
- package/dist/agent/tools/read_many.js +25 -0
- package/dist/agent/tools/readers.d.ts +6 -0
- package/dist/agent/tools/readers.js +216 -0
- package/dist/agent/tools/replace_in_file.d.ts +2 -0
- package/dist/agent/tools/replace_in_file.js +30 -0
- package/dist/agent/tools/shell.d.ts +2 -0
- package/dist/agent/tools/shell.js +48 -0
- package/dist/agent/tools/stat_path.d.ts +2 -0
- package/dist/agent/tools/stat_path.js +31 -0
- package/dist/agent/tools/types.d.ts +39 -0
- package/dist/agent/tools/types.js +1 -0
- package/dist/agent/tools/validate.d.ts +6 -0
- package/dist/agent/tools/validate.js +29 -0
- package/dist/agent/tools/write_file.d.ts +2 -0
- package/dist/agent/tools/write_file.js +26 -0
- package/dist/agent/tools.d.ts +3 -0
- package/dist/agent/tools.js +2 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.js +708 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +569 -0
- package/dist/commands/about.d.ts +8 -0
- package/dist/commands/about.js +16 -0
- package/dist/commands/agent.d.ts +11 -0
- package/dist/commands/agent.js +18 -0
- package/dist/commands/attach.d.ts +9 -0
- package/dist/commands/attach.js +15 -0
- package/dist/commands/bg.d.ts +7 -0
- package/dist/commands/bg.js +9 -0
- package/dist/commands/cancel.d.ts +10 -0
- package/dist/commands/cancel.js +10 -0
- package/dist/commands/clear.d.ts +11 -0
- package/dist/commands/clear.js +9 -0
- package/dist/commands/compact.d.ts +10 -0
- package/dist/commands/compact.js +22 -0
- package/dist/commands/config.d.ts +7 -0
- package/dist/commands/config.js +24 -0
- package/dist/commands/context.d.ts +7 -0
- package/dist/commands/context.js +24 -0
- package/dist/commands/cost.d.ts +9 -0
- package/dist/commands/cost.js +13 -0
- package/dist/commands/cwd.d.ts +7 -0
- package/dist/commands/cwd.js +7 -0
- package/dist/commands/edit.d.ts +10 -0
- package/dist/commands/edit.js +36 -0
- package/dist/commands/effort.d.ts +25 -0
- package/dist/commands/effort.js +48 -0
- package/dist/commands/exit.d.ts +9 -0
- package/dist/commands/exit.js +8 -0
- package/dist/commands/expand.d.ts +10 -0
- package/dist/commands/expand.js +8 -0
- package/dist/commands/help.d.ts +6 -0
- package/dist/commands/help.js +18 -0
- package/dist/commands/index.d.ts +9 -0
- package/dist/commands/index.js +80 -0
- package/dist/commands/jobs.d.ts +7 -0
- package/dist/commands/jobs.js +16 -0
- package/dist/commands/kill.d.ts +7 -0
- package/dist/commands/kill.js +11 -0
- package/dist/commands/login.d.ts +18 -0
- package/dist/commands/login.js +63 -0
- package/dist/commands/logout.d.ts +9 -0
- package/dist/commands/logout.js +45 -0
- package/dist/commands/model.d.ts +24 -0
- package/dist/commands/model.js +50 -0
- package/dist/commands/plugin.d.ts +11 -0
- package/dist/commands/plugin.js +74 -0
- package/dist/commands/provider.d.ts +24 -0
- package/dist/commands/provider.js +39 -0
- package/dist/commands/reset.d.ts +9 -0
- package/dist/commands/reset.js +13 -0
- package/dist/commands/skills.d.ts +11 -0
- package/dist/commands/skills.js +106 -0
- package/dist/commands/steer.d.ts +11 -0
- package/dist/commands/steer.js +13 -0
- package/dist/commands/system.d.ts +11 -0
- package/dist/commands/system.js +20 -0
- package/dist/commands/theme.d.ts +11 -0
- package/dist/commands/theme.js +22 -0
- package/dist/commands/tools.d.ts +7 -0
- package/dist/commands/tools.js +29 -0
- package/dist/commands/whoami.d.ts +9 -0
- package/dist/commands/whoami.js +52 -0
- package/dist/config/paths.d.ts +4 -0
- package/dist/config/paths.js +59 -0
- package/dist/config/store.d.ts +89 -0
- package/dist/config/store.js +260 -0
- package/dist/config/themes.d.ts +73 -0
- package/dist/config/themes.js +77 -0
- package/dist/config/trust.d.ts +22 -0
- package/dist/config/trust.js +153 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +31 -0
- package/dist/lib/diagnostics.d.ts +66 -0
- package/dist/lib/diagnostics.js +195 -0
- package/dist/lib/http.d.ts +23 -0
- package/dist/lib/http.js +128 -0
- package/dist/lib/markdown.d.ts +1 -0
- package/dist/lib/markdown.js +32 -0
- package/dist/lib/model-meta.d.ts +19 -0
- package/dist/lib/model-meta.js +113 -0
- package/dist/lib/native-tools.d.ts +12 -0
- package/dist/lib/native-tools.js +21 -0
- package/dist/lib/net.d.ts +33 -0
- package/dist/lib/net.js +168 -0
- package/dist/lib/pkg.d.ts +5 -0
- package/dist/lib/pkg.js +27 -0
- package/dist/lib/platform.d.ts +7 -0
- package/dist/lib/platform.js +57 -0
- package/dist/lib/startup.d.ts +6 -0
- package/dist/lib/startup.js +25 -0
- package/dist/lib/uiMode.d.ts +2 -0
- package/dist/lib/uiMode.js +7 -0
- package/dist/patch/apply.d.ts +15 -0
- package/dist/patch/apply.js +172 -0
- package/dist/patch/parse.d.ts +29 -0
- package/dist/patch/parse.js +112 -0
- package/dist/patch/plan.d.ts +49 -0
- package/dist/patch/plan.js +273 -0
- package/dist/plugins/manager.d.ts +23 -0
- package/dist/plugins/manager.js +101 -0
- package/dist/providers/anthropic.d.ts +22 -0
- package/dist/providers/anthropic.js +119 -0
- package/dist/providers/custom.d.ts +33 -0
- package/dist/providers/custom.js +25 -0
- package/dist/providers/deepseek.d.ts +34 -0
- package/dist/providers/deepseek.js +16 -0
- package/dist/providers/google.d.ts +33 -0
- package/dist/providers/google.js +19 -0
- package/dist/providers/groq.d.ts +34 -0
- package/dist/providers/groq.js +21 -0
- package/dist/providers/index.d.ts +10 -0
- package/dist/providers/index.js +67 -0
- package/dist/providers/mistral.d.ts +34 -0
- package/dist/providers/mistral.js +20 -0
- package/dist/providers/moonshot.d.ts +34 -0
- package/dist/providers/moonshot.js +23 -0
- package/dist/providers/ollama.d.ts +33 -0
- package/dist/providers/ollama.js +22 -0
- package/dist/providers/openai.d.ts +34 -0
- package/dist/providers/openai.js +26 -0
- package/dist/providers/openaiCompatible.d.ts +26 -0
- package/dist/providers/openaiCompatible.js +158 -0
- package/dist/providers/openrouter.d.ts +34 -0
- package/dist/providers/openrouter.js +27 -0
- package/dist/providers/qwen.d.ts +34 -0
- package/dist/providers/qwen.js +24 -0
- package/dist/providers/xai.d.ts +34 -0
- package/dist/providers/xai.js +19 -0
- package/dist/providers/zhipu.d.ts +34 -0
- package/dist/providers/zhipu.js +24 -0
- package/dist/session/log.d.ts +52 -0
- package/dist/session/log.js +331 -0
- package/dist/session/projection.d.ts +43 -0
- package/dist/session/projection.js +257 -0
- package/dist/skills/manager.d.ts +18 -0
- package/dist/skills/manager.js +201 -0
- package/dist/skills/parser.d.ts +18 -0
- package/dist/skills/parser.js +65 -0
- package/dist/types.d.ts +205 -0
- package/dist/types.js +1 -0
- package/dist/ui/Banner.d.ts +5 -0
- package/dist/ui/Banner.js +65 -0
- package/dist/ui/Message.d.ts +1 -0
- package/dist/ui/Message.js +24 -0
- package/dist/ui/Picker.d.ts +8 -0
- package/dist/ui/Picker.js +24 -0
- package/dist/ui/Prompt.d.ts +9 -0
- package/dist/ui/Prompt.js +38 -0
- package/dist/ui/Spinner.d.ts +5 -0
- package/dist/ui/Spinner.js +39 -0
- package/dist/ui/StatusBar.d.ts +11 -0
- package/dist/ui/StatusBar.js +20 -0
- package/dist/ui/TrustGate.d.ts +5 -0
- package/dist/ui/TrustGate.js +26 -0
- package/package.json +96 -4
- package/src/agent/approval.ts +47 -0
- package/src/agent/compaction.ts +276 -0
- package/src/agent/context.ts +97 -0
- package/src/agent/guards.ts +270 -0
- package/src/agent/ignore.ts +147 -0
- package/src/agent/limits.ts +71 -0
- package/src/agent/process-tree.ts +32 -0
- package/src/agent/protocol-mode.ts +59 -0
- package/src/agent/protocol.ts +129 -0
- package/src/agent/registry.ts +98 -0
- package/src/agent/runner.ts +443 -0
- package/src/agent/shell-session.ts +449 -0
- package/src/agent/stream.ts +143 -0
- package/src/agent/tool-schemas.ts +67 -0
- package/src/agent/tools/append_file.ts +28 -0
- package/src/agent/tools/apply_patch.ts +54 -0
- package/src/agent/tools/copy_file.ts +32 -0
- package/src/agent/tools/delete_file.ts +26 -0
- package/src/agent/tools/grep.ts +25 -0
- package/src/agent/tools/index.ts +34 -0
- package/src/agent/tools/list_files.ts +19 -0
- package/src/agent/tools/make_dir.ts +26 -0
- package/src/agent/tools/move_path.ts +50 -0
- package/src/agent/tools/paths.ts +92 -0
- package/src/agent/tools/pwd.ts +12 -0
- package/src/agent/tools/read_file.ts +23 -0
- package/src/agent/tools/read_many.ts +27 -0
- package/src/agent/tools/readers.ts +228 -0
- package/src/agent/tools/replace_in_file.ts +32 -0
- package/src/agent/tools/shell.ts +53 -0
- package/src/agent/tools/stat_path.ts +33 -0
- package/src/agent/tools/types.ts +42 -0
- package/src/agent/tools/validate.ts +35 -0
- package/src/agent/tools/write_file.ts +28 -0
- package/src/agent/tools.ts +3 -0
- package/src/app.ts +708 -0
- package/src/cli.ts +335 -0
- package/src/commands/about.ts +16 -0
- package/src/commands/agent.ts +18 -0
- package/src/commands/attach.ts +16 -0
- package/src/commands/bg.ts +9 -0
- package/src/commands/cancel.ts +10 -0
- package/src/commands/clear.ts +9 -0
- package/src/commands/compact.ts +23 -0
- package/src/commands/config.ts +24 -0
- package/src/commands/context.ts +25 -0
- package/src/commands/cost.ts +13 -0
- package/src/commands/cwd.ts +7 -0
- package/src/commands/edit.ts +37 -0
- package/src/commands/effort.ts +49 -0
- package/src/commands/exit.ts +8 -0
- package/src/commands/expand.ts +8 -0
- package/src/commands/help.ts +18 -0
- package/src/commands/index.ts +80 -0
- package/src/commands/jobs.ts +17 -0
- package/src/commands/kill.ts +12 -0
- package/src/commands/login.ts +63 -0
- package/src/commands/logout.ts +45 -0
- package/src/commands/model.ts +50 -0
- package/src/commands/plugin.ts +74 -0
- package/src/commands/provider.ts +39 -0
- package/src/commands/reset.ts +13 -0
- package/src/commands/skills.ts +106 -0
- package/src/commands/steer.ts +13 -0
- package/src/commands/system.ts +20 -0
- package/src/commands/theme.ts +22 -0
- package/src/commands/tools.ts +29 -0
- package/src/commands/whoami.ts +52 -0
- package/src/config/paths.ts +59 -0
- package/src/config/store.ts +293 -0
- package/src/config/themes.ts +77 -0
- package/src/config/trust.ts +183 -0
- package/src/index.ts +31 -0
- package/src/lib/diagnostics.ts +195 -0
- package/src/lib/http.ts +132 -0
- package/src/lib/markdown.ts +32 -0
- package/src/lib/model-meta.ts +138 -0
- package/src/lib/native-tools.ts +32 -0
- package/src/lib/net.ts +204 -0
- package/src/lib/pkg.ts +35 -0
- package/src/lib/platform.ts +57 -0
- package/src/lib/startup.ts +25 -0
- package/src/lib/uiMode.ts +7 -0
- package/src/patch/apply.ts +194 -0
- package/src/patch/parse.ts +131 -0
- package/src/patch/plan.ts +317 -0
- package/src/plugins/manager.ts +101 -0
- package/src/providers/anthropic.ts +119 -0
- package/src/providers/custom.ts +25 -0
- package/src/providers/deepseek.ts +16 -0
- package/src/providers/google.ts +19 -0
- package/src/providers/groq.ts +21 -0
- package/src/providers/index.ts +68 -0
- package/src/providers/mistral.ts +20 -0
- package/src/providers/moonshot.ts +23 -0
- package/src/providers/ollama.ts +22 -0
- package/src/providers/openai.ts +26 -0
- package/src/providers/openaiCompatible.ts +158 -0
- package/src/providers/openrouter.ts +27 -0
- package/src/providers/qwen.ts +24 -0
- package/src/providers/xai.ts +19 -0
- package/src/providers/zhipu.ts +24 -0
- package/src/session/log.ts +374 -0
- package/src/session/projection.ts +308 -0
- package/src/skills/manager.ts +201 -0
- package/src/skills/parser.ts +65 -0
- package/src/types.ts +205 -0
- package/src/ui/Banner.ts +68 -0
- package/src/ui/Message.ts +24 -0
- package/src/ui/Picker.ts +24 -0
- package/src/ui/Prompt.ts +38 -0
- package/src/ui/Spinner.ts +39 -0
- package/src/ui/StatusBar.ts +20 -0
- package/src/ui/TrustGate.ts +26 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { killProcessTree } from "./process-tree.js";
|
|
3
|
+
const OUTPUT_LIMIT = 1024 * 1024;
|
|
4
|
+
const DEFAULT_BACKGROUND_MS = 2000;
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
6
|
+
const MAX_POOL = 2;
|
|
7
|
+
const jobs = new Map();
|
|
8
|
+
const pool = [];
|
|
9
|
+
let spawns = 0;
|
|
10
|
+
let spawnTurn = Number.NaN;
|
|
11
|
+
let spawnsThisTurn = 0;
|
|
12
|
+
class ShellJob {
|
|
13
|
+
id;
|
|
14
|
+
key;
|
|
15
|
+
command;
|
|
16
|
+
cwd;
|
|
17
|
+
startedAt = Date.now();
|
|
18
|
+
output = "";
|
|
19
|
+
delivered = 0;
|
|
20
|
+
status = "running";
|
|
21
|
+
exitCode;
|
|
22
|
+
nextPollMs = 1000;
|
|
23
|
+
child;
|
|
24
|
+
resident;
|
|
25
|
+
donePromise;
|
|
26
|
+
resolveDone;
|
|
27
|
+
waiters = [];
|
|
28
|
+
constructor(command, cwd) {
|
|
29
|
+
this.id = "sh_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 7);
|
|
30
|
+
this.key = this.id.replace(/[^a-z0-9]/gi, "");
|
|
31
|
+
this.command = command;
|
|
32
|
+
this.cwd = cwd;
|
|
33
|
+
this.donePromise = new Promise((resolve) => {
|
|
34
|
+
this.resolveDone = resolve;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
done() {
|
|
38
|
+
return this.donePromise;
|
|
39
|
+
}
|
|
40
|
+
append(text) {
|
|
41
|
+
if (!text)
|
|
42
|
+
return;
|
|
43
|
+
this.output += text;
|
|
44
|
+
if (this.output.length > OUTPUT_LIMIT) {
|
|
45
|
+
const over = this.output.length - OUTPUT_LIMIT;
|
|
46
|
+
this.output = this.output.slice(over);
|
|
47
|
+
this.delivered = Math.max(0, this.delivered - over);
|
|
48
|
+
}
|
|
49
|
+
this.notify();
|
|
50
|
+
}
|
|
51
|
+
takeOutput() {
|
|
52
|
+
const next = this.output.slice(this.delivered);
|
|
53
|
+
this.delivered = this.output.length;
|
|
54
|
+
return next;
|
|
55
|
+
}
|
|
56
|
+
finish(status, exitCode) {
|
|
57
|
+
if (this.status !== "running")
|
|
58
|
+
return;
|
|
59
|
+
this.status = status;
|
|
60
|
+
this.exitCode = exitCode;
|
|
61
|
+
this.resolveDone();
|
|
62
|
+
this.notify();
|
|
63
|
+
}
|
|
64
|
+
waitForChange(ms) {
|
|
65
|
+
if (this.status !== "running" || this.delivered < this.output.length)
|
|
66
|
+
return Promise.resolve();
|
|
67
|
+
if (ms <= 0)
|
|
68
|
+
return Promise.resolve();
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
let timer;
|
|
71
|
+
const wake = () => {
|
|
72
|
+
if (timer)
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
this.waiters = this.waiters.filter((entry) => entry !== wake);
|
|
75
|
+
resolve();
|
|
76
|
+
};
|
|
77
|
+
this.waiters.push(wake);
|
|
78
|
+
timer = setTimeout(wake, ms);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
notify() {
|
|
82
|
+
const waiters = this.waiters;
|
|
83
|
+
this.waiters = [];
|
|
84
|
+
for (const waiter of waiters)
|
|
85
|
+
waiter();
|
|
86
|
+
}
|
|
87
|
+
view() {
|
|
88
|
+
return {
|
|
89
|
+
id: this.id,
|
|
90
|
+
command: this.command,
|
|
91
|
+
cwd: this.cwd,
|
|
92
|
+
status: this.status,
|
|
93
|
+
exitCode: this.exitCode,
|
|
94
|
+
bytes: this.output.length,
|
|
95
|
+
startedAt: this.startedAt,
|
|
96
|
+
pid: this.child?.pid,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
kill(reason) {
|
|
100
|
+
if (this.status !== "running")
|
|
101
|
+
return;
|
|
102
|
+
this.append(reason === "timeout" ? "\n[command timed out: process tree killed]" : "\n[command aborted: process tree killed]");
|
|
103
|
+
this.status = reason;
|
|
104
|
+
if (this.resident)
|
|
105
|
+
this.resident.kill();
|
|
106
|
+
else if (this.child)
|
|
107
|
+
killProcessTree(this.child, "SIGTERM");
|
|
108
|
+
this.resolveDone();
|
|
109
|
+
this.notify();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
class ResidentShell {
|
|
113
|
+
child;
|
|
114
|
+
job;
|
|
115
|
+
dead = false;
|
|
116
|
+
buffer = "";
|
|
117
|
+
constructor(cwd) {
|
|
118
|
+
const env = { ...process.env, FASTCODE_SANDBOX: "1", FASTCODE_SANDBOX_ROOT: cwd };
|
|
119
|
+
this.child = spawn("sh", ["-s"], {
|
|
120
|
+
cwd,
|
|
121
|
+
env,
|
|
122
|
+
detached: process.platform !== "win32",
|
|
123
|
+
windowsHide: true,
|
|
124
|
+
});
|
|
125
|
+
this.child.stdout.setEncoding("utf8");
|
|
126
|
+
this.child.stderr.setEncoding("utf8");
|
|
127
|
+
this.child.stdout.on("data", (chunk) => this.consume(chunk));
|
|
128
|
+
this.child.stderr.on("data", (chunk) => this.consume(chunk));
|
|
129
|
+
this.child.on("error", (err) => {
|
|
130
|
+
this.fail(err.message);
|
|
131
|
+
});
|
|
132
|
+
this.child.on("close", (code) => {
|
|
133
|
+
this.dead = true;
|
|
134
|
+
this.fail(`shell exited (${code ?? "unknown"})`, code);
|
|
135
|
+
const index = pool.indexOf(this);
|
|
136
|
+
if (index >= 0)
|
|
137
|
+
pool.splice(index, 1);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
run(job, timeoutMs) {
|
|
141
|
+
this.job = job;
|
|
142
|
+
job.resident = this;
|
|
143
|
+
const marker = `__FC_END_${job.key}__`;
|
|
144
|
+
const script = [
|
|
145
|
+
`if cd ${shellQuote(job.cwd)}; then`,
|
|
146
|
+
job.command,
|
|
147
|
+
"__fc_status=$?",
|
|
148
|
+
"else",
|
|
149
|
+
"__fc_status=1",
|
|
150
|
+
"fi",
|
|
151
|
+
`printf '\\n${marker}:%s\\n' "$__fc_status"`,
|
|
152
|
+
"",
|
|
153
|
+
].join("\n");
|
|
154
|
+
try {
|
|
155
|
+
this.child.stdin.write(script);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
job.append(err?.message ?? String(err));
|
|
159
|
+
job.finish("failed", 1);
|
|
160
|
+
}
|
|
161
|
+
void timeoutMs;
|
|
162
|
+
}
|
|
163
|
+
consume(chunk) {
|
|
164
|
+
if (this.dead)
|
|
165
|
+
return;
|
|
166
|
+
this.buffer += chunk;
|
|
167
|
+
if (!this.job)
|
|
168
|
+
return;
|
|
169
|
+
const marker = `__FC_END_${this.job.key}__:`;
|
|
170
|
+
const index = this.buffer.indexOf(marker);
|
|
171
|
+
if (index < 0) {
|
|
172
|
+
const sentinel = "\n" + marker;
|
|
173
|
+
let keep = 0;
|
|
174
|
+
const max = Math.min(this.buffer.length, sentinel.length);
|
|
175
|
+
for (let length = max; length > 0; length -= 1) {
|
|
176
|
+
if (sentinel.startsWith(this.buffer.slice(this.buffer.length - length))) {
|
|
177
|
+
keep = length;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const flush = this.buffer.length - keep;
|
|
182
|
+
if (flush > 0) {
|
|
183
|
+
this.job.append(this.buffer.slice(0, flush));
|
|
184
|
+
this.buffer = this.buffer.slice(flush);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const rest = this.buffer.slice(index + marker.length);
|
|
189
|
+
const newline = rest.indexOf("\n");
|
|
190
|
+
if (newline < 0)
|
|
191
|
+
return;
|
|
192
|
+
const job = this.job;
|
|
193
|
+
job.append(this.buffer.slice(0, index));
|
|
194
|
+
const code = Number(rest.slice(0, newline).trim());
|
|
195
|
+
this.buffer = rest.slice(newline + 1);
|
|
196
|
+
this.job = undefined;
|
|
197
|
+
job.finish(Number.isFinite(code) && code === 0 ? "done" : "failed", Number.isFinite(code) ? code : 1);
|
|
198
|
+
}
|
|
199
|
+
fail(message, code) {
|
|
200
|
+
if (!this.job)
|
|
201
|
+
return;
|
|
202
|
+
const job = this.job;
|
|
203
|
+
this.job = undefined;
|
|
204
|
+
job.append(`\n${message}`);
|
|
205
|
+
job.finish("failed", code ?? 1);
|
|
206
|
+
}
|
|
207
|
+
kill() {
|
|
208
|
+
this.dead = true;
|
|
209
|
+
killProcessTree(this.child, "SIGTERM");
|
|
210
|
+
setTimeout(() => killProcessTree(this.child, "SIGKILL"), 2000);
|
|
211
|
+
const index = pool.indexOf(this);
|
|
212
|
+
if (index >= 0)
|
|
213
|
+
pool.splice(index, 1);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export async function executeShellCommand(command, cwd, options = {}) {
|
|
217
|
+
pruneJobs();
|
|
218
|
+
if (options.signal?.aborted)
|
|
219
|
+
return { ok: false, status: "killed", output: "command aborted before start" };
|
|
220
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
221
|
+
const backgroundAfterMs = options.backgroundAfterMs ?? DEFAULT_BACKGROUND_MS;
|
|
222
|
+
const useResident = process.platform !== "win32" && !needsOneShot(command);
|
|
223
|
+
let job;
|
|
224
|
+
if (useResident) {
|
|
225
|
+
const resident = acquireResident(options.turn, cwd);
|
|
226
|
+
if (resident === "busy")
|
|
227
|
+
return { ok: false, status: "failed", output: "both pool shells are busy; poll or kill a session first" };
|
|
228
|
+
if (resident === "limit")
|
|
229
|
+
return { ok: false, status: "failed", output: spawnLimitMessage() };
|
|
230
|
+
job = new ShellJob(command, cwd);
|
|
231
|
+
resident.run(job, timeoutMs);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
if (!maySpawn(options.turn))
|
|
235
|
+
return { ok: false, status: "failed", output: spawnLimitMessage() };
|
|
236
|
+
job = startOneShot(command, cwd);
|
|
237
|
+
}
|
|
238
|
+
jobs.set(job.id, job);
|
|
239
|
+
const timer = setTimeout(() => job.kill("timeout"), timeoutMs);
|
|
240
|
+
const onAbort = () => job.kill("killed");
|
|
241
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
242
|
+
const cleanup = () => {
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
245
|
+
};
|
|
246
|
+
await Promise.race([job.done(), delay(backgroundAfterMs)]);
|
|
247
|
+
if (job.status === "running") {
|
|
248
|
+
cleanup();
|
|
249
|
+
return {
|
|
250
|
+
ok: true,
|
|
251
|
+
status: "running",
|
|
252
|
+
output: job.takeOutput(),
|
|
253
|
+
sessionId: job.id,
|
|
254
|
+
nextPollMs: job.nextPollMs,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
cleanup();
|
|
258
|
+
return syncResult(job);
|
|
259
|
+
}
|
|
260
|
+
export async function pollShellSession(id, waitMs = 0) {
|
|
261
|
+
pruneJobs();
|
|
262
|
+
const job = jobs.get(id);
|
|
263
|
+
if (!job)
|
|
264
|
+
return undefined;
|
|
265
|
+
return await pollOnce(job, waitMs);
|
|
266
|
+
}
|
|
267
|
+
async function pollOnce(job, waitMs) {
|
|
268
|
+
await job.waitForChange(waitMs);
|
|
269
|
+
const output = job.takeOutput();
|
|
270
|
+
if (output)
|
|
271
|
+
job.nextPollMs = 1000;
|
|
272
|
+
else if (job.status === "running")
|
|
273
|
+
job.nextPollMs = Math.min(30_000, job.nextPollMs * 2);
|
|
274
|
+
return {
|
|
275
|
+
ok: job.status === "done" || job.status === "running",
|
|
276
|
+
status: job.status,
|
|
277
|
+
output,
|
|
278
|
+
exitCode: job.exitCode,
|
|
279
|
+
sessionId: job.id,
|
|
280
|
+
nextPollMs: job.nextPollMs,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
export function killShellSession(id) {
|
|
284
|
+
const job = jobs.get(id);
|
|
285
|
+
if (!job || job.status !== "running")
|
|
286
|
+
return false;
|
|
287
|
+
job.kill("killed");
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
export function listShellSessions() {
|
|
291
|
+
pruneJobs();
|
|
292
|
+
return Array.from(jobs.values())
|
|
293
|
+
.sort((a, b) => b.startedAt - a.startedAt)
|
|
294
|
+
.map((job) => job.view());
|
|
295
|
+
}
|
|
296
|
+
export function shutdownShells() {
|
|
297
|
+
for (const job of jobs.values()) {
|
|
298
|
+
if (job.status === "running")
|
|
299
|
+
job.kill("killed");
|
|
300
|
+
}
|
|
301
|
+
for (const resident of [...pool])
|
|
302
|
+
resident.kill();
|
|
303
|
+
pool.length = 0;
|
|
304
|
+
jobs.clear();
|
|
305
|
+
}
|
|
306
|
+
export function shellPoolStats() {
|
|
307
|
+
return {
|
|
308
|
+
spawns,
|
|
309
|
+
active: Array.from(jobs.values()).filter((job) => job.status === "running").length,
|
|
310
|
+
pool: pool.filter((resident) => !resident.dead).length,
|
|
311
|
+
maxSpawnPerTurn: maxSpawnPerTurn(),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function startOneShot(command, cwd) {
|
|
315
|
+
const job = new ShellJob(command, cwd);
|
|
316
|
+
const child = spawn(command, {
|
|
317
|
+
cwd,
|
|
318
|
+
shell: true,
|
|
319
|
+
env: { ...process.env, FASTCODE_SANDBOX: "1", FASTCODE_SANDBOX_ROOT: cwd },
|
|
320
|
+
windowsHide: true,
|
|
321
|
+
detached: process.platform !== "win32",
|
|
322
|
+
});
|
|
323
|
+
job.child = child;
|
|
324
|
+
child.stdout?.on("data", (chunk) => job.append(chunk.toString("utf8")));
|
|
325
|
+
child.stderr?.on("data", (chunk) => job.append(chunk.toString("utf8")));
|
|
326
|
+
child.on("error", (err) => {
|
|
327
|
+
job.append("\n" + err.message);
|
|
328
|
+
job.finish("failed", 1);
|
|
329
|
+
});
|
|
330
|
+
child.on("close", (code) => {
|
|
331
|
+
if (job.status === "running")
|
|
332
|
+
job.finish(code === 0 ? "done" : "failed", code);
|
|
333
|
+
});
|
|
334
|
+
return job;
|
|
335
|
+
}
|
|
336
|
+
function acquireResident(turn, cwd) {
|
|
337
|
+
const idle = pool.find((resident) => !resident.dead && !resident.job);
|
|
338
|
+
if (idle)
|
|
339
|
+
return idle;
|
|
340
|
+
if (pool.filter((resident) => !resident.dead).length >= MAX_POOL)
|
|
341
|
+
return "busy";
|
|
342
|
+
if (!maySpawn(turn))
|
|
343
|
+
return "limit";
|
|
344
|
+
const resident = new ResidentShell(cwd);
|
|
345
|
+
pool.push(resident);
|
|
346
|
+
return resident;
|
|
347
|
+
}
|
|
348
|
+
function needsOneShot(command) {
|
|
349
|
+
return /(^|\s)(vim|nano|less|more|ssh|su|login|read|top|htop|tmux|screen)(\s|$)/.test(command)
|
|
350
|
+
|| /(^|\s)(python|python3|node|bash|zsh|fish|irb|sqlite3)(\s|$)/.test(command) && !/\s-(c|e|m)\s/.test(command);
|
|
351
|
+
}
|
|
352
|
+
function maySpawn(turn) {
|
|
353
|
+
const current = typeof turn === "number" ? turn : 0;
|
|
354
|
+
if (current !== spawnTurn) {
|
|
355
|
+
spawnTurn = current;
|
|
356
|
+
spawnsThisTurn = 0;
|
|
357
|
+
}
|
|
358
|
+
if (spawnsThisTurn >= maxSpawnPerTurn())
|
|
359
|
+
return false;
|
|
360
|
+
spawnsThisTurn += 1;
|
|
361
|
+
spawns += 1;
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
function maxSpawnPerTurn() {
|
|
365
|
+
const raw = Number(process.env["FASTCODE_MAX_SPAWN_PER_TURN"]);
|
|
366
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 8;
|
|
367
|
+
}
|
|
368
|
+
function spawnLimitMessage() {
|
|
369
|
+
return `spawn limit reached (FASTCODE_MAX_SPAWN_PER_TURN=${maxSpawnPerTurn()}); batch commands or poll an existing session`;
|
|
370
|
+
}
|
|
371
|
+
function syncResult(job) {
|
|
372
|
+
return {
|
|
373
|
+
ok: job.status === "done",
|
|
374
|
+
status: job.status,
|
|
375
|
+
output: job.takeOutput(),
|
|
376
|
+
exitCode: job.exitCode,
|
|
377
|
+
sessionId: job.id,
|
|
378
|
+
nextPollMs: job.nextPollMs,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function pruneJobs() {
|
|
382
|
+
const now = Date.now();
|
|
383
|
+
for (const [id, job] of jobs) {
|
|
384
|
+
if (job.status !== "running" && now - job.startedAt > 10 * 60 * 1000)
|
|
385
|
+
jobs.delete(id);
|
|
386
|
+
}
|
|
387
|
+
if (jobs.size > 50) {
|
|
388
|
+
const finished = Array.from(jobs.values())
|
|
389
|
+
.filter((job) => job.status !== "running")
|
|
390
|
+
.sort((a, b) => a.startedAt - b.startedAt);
|
|
391
|
+
for (const job of finished.slice(0, jobs.size - 50))
|
|
392
|
+
jobs.delete(job.id);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function shellQuote(value) {
|
|
396
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
397
|
+
}
|
|
398
|
+
function delay(ms) {
|
|
399
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
400
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming request runner with an idle timeout and prefix-safe retry.
|
|
3
|
+
*
|
|
4
|
+
* `provider.stream()` only exposes the caller's AbortSignal, so a connection
|
|
5
|
+
* that stalls after the headers arrive can wait forever. This wrapper adds:
|
|
6
|
+
* • an idle timer reset on every chunk (default 60s),
|
|
7
|
+
* • retries routed through `retryStream`, which skips the already delivered
|
|
8
|
+
* prefix instead of duplicating tokens,
|
|
9
|
+
* • `incomplete` detection so the chat loop can tell "server closed early"
|
|
10
|
+
* from a normal finish.
|
|
11
|
+
*/
|
|
12
|
+
import type { ChatChunk, ChatRequest, Provider } from "../types.js";
|
|
13
|
+
export interface RunStreamOptions {
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
idleTimeoutMs?: number;
|
|
16
|
+
maxRetries?: number;
|
|
17
|
+
onDelta?: (delta: string, text: string) => void;
|
|
18
|
+
onReset?: () => void;
|
|
19
|
+
clientRequestId?: string;
|
|
20
|
+
retryBackoffMs?: number[];
|
|
21
|
+
}
|
|
22
|
+
export interface RunStreamResult {
|
|
23
|
+
text: string;
|
|
24
|
+
usage?: ChatChunk["usage"];
|
|
25
|
+
toolCalls?: ChatChunk["toolCalls"];
|
|
26
|
+
incomplete: boolean;
|
|
27
|
+
retried: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function runStream(provider: Provider, req: ChatRequest, apiKey: string | undefined, options?: RunStreamOptions): Promise<RunStreamResult>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { IncompleteStreamError, retryStream } from "../lib/net.js";
|
|
2
|
+
export async function runStream(provider, req, apiKey, options = {}) {
|
|
3
|
+
const idleTimeoutMs = positive(options.idleTimeoutMs, 60_000);
|
|
4
|
+
const maxRetries = Math.max(0, Math.floor(options.maxRetries ?? 1));
|
|
5
|
+
let retried = 0;
|
|
6
|
+
let text = "";
|
|
7
|
+
let usage;
|
|
8
|
+
let toolCalls;
|
|
9
|
+
let sawDone = false;
|
|
10
|
+
const send = async function* ({ clientRequestId }) {
|
|
11
|
+
yield* attemptChunks(provider, { ...req, clientRequestId }, apiKey, {
|
|
12
|
+
signal: options.signal,
|
|
13
|
+
idleTimeoutMs,
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
try {
|
|
17
|
+
for await (const chunk of retryStream(send, {
|
|
18
|
+
clientRequestId: options.clientRequestId,
|
|
19
|
+
retries: maxRetries,
|
|
20
|
+
signal: options.signal,
|
|
21
|
+
backoffMs: options.retryBackoffMs,
|
|
22
|
+
onRetry: () => {
|
|
23
|
+
retried += 1;
|
|
24
|
+
},
|
|
25
|
+
onReset: options.onReset,
|
|
26
|
+
})) {
|
|
27
|
+
if (chunk.reset)
|
|
28
|
+
text = chunk.delta ?? "";
|
|
29
|
+
else if (chunk.delta)
|
|
30
|
+
text += chunk.delta;
|
|
31
|
+
if (chunk.delta)
|
|
32
|
+
options.onDelta?.(chunk.delta, text);
|
|
33
|
+
if (chunk.usage)
|
|
34
|
+
usage = chunk.usage;
|
|
35
|
+
if (chunk.toolCalls)
|
|
36
|
+
toolCalls = chunk.toolCalls;
|
|
37
|
+
if (chunk.done)
|
|
38
|
+
sawDone = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
if (err instanceof IncompleteStreamError) {
|
|
43
|
+
return { text: err.partialText || text, usage, toolCalls, incomplete: true, retried };
|
|
44
|
+
}
|
|
45
|
+
if (err && typeof err === "object" && !err.partialText && text)
|
|
46
|
+
err.partialText = text;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
return { text, usage, toolCalls, incomplete: !sawDone, retried };
|
|
50
|
+
}
|
|
51
|
+
async function* attemptChunks(provider, req, apiKey, options) {
|
|
52
|
+
const parent = options.signal;
|
|
53
|
+
const child = new AbortController();
|
|
54
|
+
let timedOut = false;
|
|
55
|
+
const forwardAbort = () => child.abort();
|
|
56
|
+
if (parent) {
|
|
57
|
+
if (parent.aborted)
|
|
58
|
+
child.abort();
|
|
59
|
+
else
|
|
60
|
+
parent.addEventListener("abort", forwardAbort, { once: true });
|
|
61
|
+
}
|
|
62
|
+
let timer;
|
|
63
|
+
const resetIdle = () => {
|
|
64
|
+
if (timer)
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
timer = setTimeout(() => {
|
|
67
|
+
timedOut = true;
|
|
68
|
+
child.abort();
|
|
69
|
+
}, options.idleTimeoutMs);
|
|
70
|
+
};
|
|
71
|
+
resetIdle();
|
|
72
|
+
try {
|
|
73
|
+
for await (const chunk of provider.stream({ ...req, signal: child.signal }, apiKey)) {
|
|
74
|
+
resetIdle();
|
|
75
|
+
yield chunk;
|
|
76
|
+
if (chunk.done)
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
if (parent?.aborted)
|
|
82
|
+
throw abortError();
|
|
83
|
+
if (timedOut)
|
|
84
|
+
throw new Error(`stream idle timeout after ${options.idleTimeoutMs}ms (no data received)`);
|
|
85
|
+
throw toError(err);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
if (timer)
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
parent?.removeEventListener("abort", forwardAbort);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function abortError() {
|
|
94
|
+
const error = new Error("Aborted");
|
|
95
|
+
error.name = "AbortError";
|
|
96
|
+
return error;
|
|
97
|
+
}
|
|
98
|
+
function toError(value) {
|
|
99
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
100
|
+
}
|
|
101
|
+
function positive(value, fallback) {
|
|
102
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
103
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const CWD_NOTE = "Paths are relative to the session working directory unless absolute.";
|
|
2
|
+
export function toolSchemas() {
|
|
3
|
+
return [
|
|
4
|
+
schema("shell", "Run a shell command. Long commands return a session id for polling.", {
|
|
5
|
+
command: { type: "string" },
|
|
6
|
+
timeoutMs: { type: "number" },
|
|
7
|
+
session_id: { type: "string", description: "Poll an existing background session." },
|
|
8
|
+
waitMs: { type: "number", description: "Milliseconds to wait for new session output." },
|
|
9
|
+
}),
|
|
10
|
+
schema("pwd", "Print the current working directory.", {}),
|
|
11
|
+
schema("read_file", CWD_NOTE, { path: { type: "string" } }, ["path"]),
|
|
12
|
+
schema("read_many", CWD_NOTE, { paths: { type: "array", items: { type: "string" } } }, ["paths"]),
|
|
13
|
+
schema("write_file", "Replace a file with full contents.", {
|
|
14
|
+
path: { type: "string" },
|
|
15
|
+
content: { type: "string" },
|
|
16
|
+
}, ["path", "content"]),
|
|
17
|
+
schema("append_file", "Append text to a file.", {
|
|
18
|
+
path: { type: "string" },
|
|
19
|
+
content: { type: "string" },
|
|
20
|
+
}, ["path", "content"]),
|
|
21
|
+
schema("apply_patch", "Apply a multi-file patch (*** Begin Patch ... *** End Patch).", {
|
|
22
|
+
patch: { type: "string" },
|
|
23
|
+
}, ["patch"]),
|
|
24
|
+
schema("copy_file", CWD_NOTE, { from: { type: "string" }, to: { type: "string" } }, ["from", "to"]),
|
|
25
|
+
schema("delete_file", "Delete a file or directory.", { path: { type: "string" } }, ["path"]),
|
|
26
|
+
schema("list_files", "List files under a directory.", {
|
|
27
|
+
path: { type: "string" },
|
|
28
|
+
maxDepth: { type: "number" },
|
|
29
|
+
detail: { type: "boolean" },
|
|
30
|
+
}),
|
|
31
|
+
schema("make_dir", "Create a directory.", { path: { type: "string" } }, ["path"]),
|
|
32
|
+
schema("move_path", "Move/rename a path; refuses an existing target.", {
|
|
33
|
+
from: { type: "string" },
|
|
34
|
+
to: { type: "string" },
|
|
35
|
+
}, ["from", "to"]),
|
|
36
|
+
schema("grep", "Search file contents with a regular expression.", {
|
|
37
|
+
pattern: { type: "string" },
|
|
38
|
+
path: { type: "string" },
|
|
39
|
+
maxDepth: { type: "number" },
|
|
40
|
+
}, ["pattern"]),
|
|
41
|
+
schema("replace_in_file", "Replace an exact text occurrence.", {
|
|
42
|
+
path: { type: "string" },
|
|
43
|
+
find: { type: "string" },
|
|
44
|
+
replace: { type: "string" },
|
|
45
|
+
}, ["path", "find", "replace"]),
|
|
46
|
+
schema("stat_path", "Show file metadata.", { path: { type: "string" } }, ["path"]),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
function schema(name, description, properties, required) {
|
|
50
|
+
return {
|
|
51
|
+
type: "function",
|
|
52
|
+
function: {
|
|
53
|
+
name,
|
|
54
|
+
description,
|
|
55
|
+
parameters: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties,
|
|
58
|
+
...(required && required.length > 0 ? { required } : {}),
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { resolvePath } from "./paths.js";
|
|
4
|
+
import { invalid, preparePath, requiredString } from "./validate.js";
|
|
5
|
+
export const appendFileTool = {
|
|
6
|
+
name: "append_file",
|
|
7
|
+
requiresTrust: true,
|
|
8
|
+
stateScope: "turn",
|
|
9
|
+
revalidate: ["trust", "sandboxRoots"],
|
|
10
|
+
prepare(call, ctx) {
|
|
11
|
+
try {
|
|
12
|
+
requiredString(call, "path");
|
|
13
|
+
requiredString(call, "content");
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
return invalid(err.message);
|
|
17
|
+
}
|
|
18
|
+
return preparePath(ctx.cwd, requiredString(call, "path"));
|
|
19
|
+
},
|
|
20
|
+
async execute(call, ctx) {
|
|
21
|
+
const file = resolvePath(ctx.cwd, call);
|
|
22
|
+
await mkdir(dirname(file), { recursive: true });
|
|
23
|
+
await appendFile(file, requiredString(call, "content"), "utf8");
|
|
24
|
+
return { ok: true, output: `appended ${file}`, sideEffects: { touched: [file] } };
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Tool } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Atomic multi-file patch.
|
|
4
|
+
*
|
|
5
|
+
* Applies Add / Delete / Update / Move only after every file plan succeeds.
|
|
6
|
+
* Symbolic links are refused, CRLF and mode bits are preserved, and per-file
|
|
7
|
+
* diffs are returned. xattr / ACL are NOT preserved and the inode changes
|
|
8
|
+
* (tmp+rename); restart file watchers if they miss the change.
|
|
9
|
+
*/
|
|
10
|
+
export declare const applyPatchTool: Tool;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { parsePatch } from "../../patch/parse.js";
|
|
2
|
+
import { planPatch, reviewPatchPlan } from "../../patch/plan.js";
|
|
3
|
+
import { applyPatchPlan } from "../../patch/apply.js";
|
|
4
|
+
import { invalid, requiredString } from "./validate.js";
|
|
5
|
+
/**
|
|
6
|
+
* Atomic multi-file patch.
|
|
7
|
+
*
|
|
8
|
+
* Applies Add / Delete / Update / Move only after every file plan succeeds.
|
|
9
|
+
* Symbolic links are refused, CRLF and mode bits are preserved, and per-file
|
|
10
|
+
* diffs are returned. xattr / ACL are NOT preserved and the inode changes
|
|
11
|
+
* (tmp+rename); restart file watchers if they miss the change.
|
|
12
|
+
*/
|
|
13
|
+
export const applyPatchTool = {
|
|
14
|
+
name: "apply_patch",
|
|
15
|
+
requiresTrust: true,
|
|
16
|
+
stateScope: "turn",
|
|
17
|
+
revalidate: ["trust", "sandboxRoots"],
|
|
18
|
+
approvalHints: { mayRequireApproval: true },
|
|
19
|
+
prepare(call) {
|
|
20
|
+
let patch;
|
|
21
|
+
try {
|
|
22
|
+
patch = requiredString(call, "patch");
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return invalid(err.message);
|
|
26
|
+
}
|
|
27
|
+
const parsed = parsePatch(patch);
|
|
28
|
+
return parsed.ok === false ? invalid(parsed.output) : { ok: true };
|
|
29
|
+
},
|
|
30
|
+
async execute(call, ctx) {
|
|
31
|
+
const patch = requiredString(call, "patch");
|
|
32
|
+
const planned = planPatch(patch, ctx.cwd);
|
|
33
|
+
if (planned.ok === false)
|
|
34
|
+
return { ok: false, output: planned.output };
|
|
35
|
+
const review = reviewPatchPlan(planned.plan);
|
|
36
|
+
if (review.kind === "deny")
|
|
37
|
+
return { ok: false, output: `patch denied: ${review.reason}` };
|
|
38
|
+
if (review.kind === "pending") {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
output: `patch needs approval (${review.reason}), but no approval resolver is registered; nothing was written.`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const applied = applyPatchPlan(planned.plan);
|
|
45
|
+
const warnings = applied.warnings.length > 0 ? "\n\nwarnings:\n" + applied.warnings.map((line) => "- " + line).join("\n") : "";
|
|
46
|
+
return {
|
|
47
|
+
ok: applied.ok,
|
|
48
|
+
output: applied.output + warnings,
|
|
49
|
+
sideEffects: { touched: applied.touched, removed: applied.removed },
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|