@datename/fastcode-termux 0.1.6 → 0.1.8-beta.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.
Files changed (303) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +67 -16
  3. package/dist/agent/approval-json.d.ts +43 -0
  4. package/dist/agent/approval-json.js +69 -0
  5. package/dist/agent/approval-native.d.ts +3 -0
  6. package/dist/agent/approval-native.js +165 -0
  7. package/dist/agent/approval-store.d.ts +16 -0
  8. package/dist/agent/approval-store.js +21 -0
  9. package/dist/agent/approval.d.ts +83 -0
  10. package/dist/agent/approval.js +42 -0
  11. package/dist/agent/compaction.d.ts +41 -0
  12. package/dist/agent/compaction.js +270 -0
  13. package/dist/agent/context.d.ts +29 -0
  14. package/dist/agent/context.js +62 -0
  15. package/dist/agent/guards.d.ts +24 -16
  16. package/dist/agent/guards.js +1167 -18
  17. package/dist/agent/ignore.d.ts +11 -0
  18. package/dist/agent/ignore.js +129 -0
  19. package/dist/agent/policy/defaults.d.ts +16 -0
  20. package/dist/agent/policy/defaults.js +17 -0
  21. package/dist/agent/policy/egress.d.ts +32 -0
  22. package/dist/agent/policy/egress.js +280 -0
  23. package/dist/agent/process-tree.d.ts +22 -0
  24. package/dist/agent/process-tree.js +102 -0
  25. package/dist/agent/protocol-mode.d.ts +6 -0
  26. package/dist/agent/protocol-mode.js +51 -0
  27. package/dist/agent/protocol.d.ts +20 -1
  28. package/dist/agent/protocol.js +169 -39
  29. package/dist/agent/registry.d.ts +19 -0
  30. package/dist/agent/registry.js +247 -0
  31. package/dist/agent/runner.d.ts +127 -0
  32. package/dist/agent/runner.js +391 -0
  33. package/dist/agent/shell-ast.d.ts +11 -0
  34. package/dist/agent/shell-ast.js +204 -0
  35. package/dist/agent/shell-session.d.ts +51 -0
  36. package/dist/agent/shell-session.js +611 -0
  37. package/dist/agent/stream.d.ts +29 -0
  38. package/dist/agent/stream.js +103 -0
  39. package/dist/agent/tool-schemas.d.ts +2 -0
  40. package/dist/agent/tool-schemas.js +82 -0
  41. package/dist/agent/tools/append_file.d.ts +2 -0
  42. package/dist/agent/tools/append_file.js +28 -0
  43. package/dist/agent/tools/apply_patch.d.ts +10 -0
  44. package/dist/agent/tools/apply_patch.js +92 -0
  45. package/dist/agent/tools/copy_file.d.ts +2 -0
  46. package/dist/agent/tools/copy_file.js +53 -0
  47. package/dist/agent/tools/delete_file.d.ts +2 -0
  48. package/dist/agent/tools/delete_file.js +43 -0
  49. package/dist/agent/tools/grep.d.ts +2 -0
  50. package/dist/agent/tools/grep.js +54 -0
  51. package/dist/agent/tools/index.d.ts +2 -0
  52. package/dist/agent/tools/index.js +32 -0
  53. package/dist/agent/tools/list_files.d.ts +2 -0
  54. package/dist/agent/tools/list_files.js +17 -0
  55. package/dist/agent/tools/make_dir.d.ts +2 -0
  56. package/dist/agent/tools/make_dir.js +26 -0
  57. package/dist/agent/tools/move_path.d.ts +9 -0
  58. package/dist/agent/tools/move_path.js +73 -0
  59. package/dist/agent/tools/paths.d.ts +7 -0
  60. package/dist/agent/tools/paths.js +84 -0
  61. package/dist/agent/tools/pwd.d.ts +2 -0
  62. package/dist/agent/tools/pwd.js +10 -0
  63. package/dist/agent/tools/read_file.d.ts +2 -0
  64. package/dist/agent/tools/read_file.js +81 -0
  65. package/dist/agent/tools/read_many.d.ts +2 -0
  66. package/dist/agent/tools/read_many.js +43 -0
  67. package/dist/agent/tools/readers.d.ts +58 -0
  68. package/dist/agent/tools/readers.js +426 -0
  69. package/dist/agent/tools/replace_in_file.d.ts +2 -0
  70. package/dist/agent/tools/replace_in_file.js +63 -0
  71. package/dist/agent/tools/shell.d.ts +2 -0
  72. package/dist/agent/tools/shell.js +48 -0
  73. package/dist/agent/tools/stat_path.d.ts +2 -0
  74. package/dist/agent/tools/stat_path.js +31 -0
  75. package/dist/agent/tools/types.d.ts +65 -0
  76. package/dist/agent/tools/types.js +1 -0
  77. package/dist/agent/tools/validate.d.ts +19 -0
  78. package/dist/agent/tools/validate.js +92 -0
  79. package/dist/agent/tools/write_file.d.ts +2 -0
  80. package/dist/agent/tools/write_file.js +28 -0
  81. package/dist/agent/tools.d.ts +3 -1
  82. package/dist/agent/tools.js +2 -377
  83. package/dist/app.d.ts +2 -1
  84. package/dist/app.js +846 -173
  85. package/dist/cli-main.js +363 -0
  86. package/dist/cli.js +444 -333
  87. package/dist/commands/about.js +1 -1
  88. package/dist/commands/attach.d.ts +9 -0
  89. package/dist/commands/attach.js +15 -0
  90. package/dist/commands/bg.d.ts +7 -0
  91. package/dist/commands/bg.js +9 -0
  92. package/dist/commands/cancel.d.ts +10 -0
  93. package/dist/commands/cancel.js +10 -0
  94. package/dist/commands/clear.d.ts +1 -0
  95. package/dist/commands/clear.js +1 -1
  96. package/dist/commands/compact.d.ts +22 -0
  97. package/dist/commands/compact.js +34 -0
  98. package/dist/commands/context.d.ts +7 -0
  99. package/dist/commands/context.js +25 -0
  100. package/dist/commands/edit.d.ts +10 -0
  101. package/dist/commands/edit.js +36 -0
  102. package/dist/commands/effort.d.ts +25 -0
  103. package/dist/commands/effort.js +48 -0
  104. package/dist/commands/expand.d.ts +10 -0
  105. package/dist/commands/expand.js +8 -0
  106. package/dist/commands/index.js +24 -0
  107. package/dist/commands/jobs.d.ts +7 -0
  108. package/dist/commands/jobs.js +17 -0
  109. package/dist/commands/kill.d.ts +7 -0
  110. package/dist/commands/kill.js +13 -0
  111. package/dist/commands/login.d.ts +6 -4
  112. package/dist/commands/login.js +24 -22
  113. package/dist/commands/logout.js +8 -16
  114. package/dist/commands/model.d.ts +1 -0
  115. package/dist/commands/model.js +10 -22
  116. package/dist/commands/provider.d.ts +1 -2
  117. package/dist/commands/provider.js +4 -11
  118. package/dist/commands/skills.js +34 -4
  119. package/dist/commands/steer.d.ts +11 -0
  120. package/dist/commands/steer.js +13 -0
  121. package/dist/commands/threads.d.ts +9 -0
  122. package/dist/commands/threads.js +26 -0
  123. package/dist/commands/whoami.js +6 -5
  124. package/dist/config/store.d.ts +53 -12
  125. package/dist/config/store.js +163 -73
  126. package/dist/config/trust.d.ts +30 -8
  127. package/dist/config/trust.js +139 -14
  128. package/dist/headless.d.ts +7 -0
  129. package/dist/headless.js +213 -0
  130. package/dist/index.js +19 -3
  131. package/dist/lib/debug.d.ts +2 -0
  132. package/dist/lib/debug.js +22 -0
  133. package/dist/lib/diagnostics.d.ts +48 -4
  134. package/dist/lib/diagnostics.js +90 -8
  135. package/dist/lib/highlight-subset.d.ts +21 -0
  136. package/dist/lib/highlight-subset.js +88 -0
  137. package/dist/lib/http.d.ts +11 -1
  138. package/dist/lib/http.js +62 -4
  139. package/dist/lib/lifecycle.d.ts +5 -0
  140. package/dist/lib/lifecycle.js +62 -0
  141. package/dist/lib/markdown.js +22 -4
  142. package/dist/lib/model-meta.d.ts +19 -0
  143. package/dist/lib/model-meta.js +113 -0
  144. package/dist/lib/native-tools.d.ts +12 -0
  145. package/dist/lib/native-tools.js +21 -0
  146. package/dist/lib/net.d.ts +33 -0
  147. package/dist/lib/net.js +168 -0
  148. package/dist/patch/apply.d.ts +15 -0
  149. package/dist/patch/apply.js +172 -0
  150. package/dist/patch/parse.d.ts +29 -0
  151. package/dist/patch/parse.js +112 -0
  152. package/dist/patch/plan.d.ts +49 -0
  153. package/dist/patch/plan.js +273 -0
  154. package/dist/providers/anthropic.d.ts +1 -0
  155. package/dist/providers/anthropic.js +12 -0
  156. package/dist/providers/custom.d.ts +9 -0
  157. package/dist/providers/deepseek.d.ts +10 -0
  158. package/dist/providers/deepseek.js +3 -2
  159. package/dist/providers/google.d.ts +9 -0
  160. package/dist/providers/groq.d.ts +10 -0
  161. package/dist/providers/groq.js +2 -1
  162. package/dist/providers/index.d.ts +10 -201
  163. package/dist/providers/index.js +30 -0
  164. package/dist/providers/mistral.d.ts +10 -0
  165. package/dist/providers/mistral.js +2 -1
  166. package/dist/providers/moonshot.d.ts +10 -0
  167. package/dist/providers/moonshot.js +2 -1
  168. package/dist/providers/ollama.d.ts +9 -0
  169. package/dist/providers/openai.d.ts +10 -0
  170. package/dist/providers/openai.js +2 -1
  171. package/dist/providers/openaiCompatible.d.ts +33 -2
  172. package/dist/providers/openaiCompatible.js +78 -16
  173. package/dist/providers/openrouter.d.ts +10 -0
  174. package/dist/providers/openrouter.js +2 -1
  175. package/dist/providers/qwen.d.ts +10 -0
  176. package/dist/providers/qwen.js +2 -1
  177. package/dist/providers/spec.d.ts +12 -0
  178. package/dist/providers/spec.js +29 -0
  179. package/dist/providers/xai.d.ts +10 -0
  180. package/dist/providers/xai.js +2 -1
  181. package/dist/providers/zhipu.d.ts +10 -0
  182. package/dist/providers/zhipu.js +2 -1
  183. package/dist/session/log.d.ts +49 -14
  184. package/dist/session/log.js +236 -11
  185. package/dist/session/projection.d.ts +76 -0
  186. package/dist/session/projection.js +526 -0
  187. package/dist/skills/manager.d.ts +15 -0
  188. package/dist/skills/manager.js +135 -18
  189. package/dist/types.d.ts +75 -1
  190. package/dist/ui/Message.d.ts +1 -7
  191. package/dist/ui/Message.js +13 -2
  192. package/dist/ui/Picker.d.ts +8 -0
  193. package/dist/ui/Picker.js +24 -0
  194. package/dist/ui/Prompt.d.ts +1 -2
  195. package/dist/ui/Prompt.js +2 -2
  196. package/dist/ui/Spinner.js +11 -5
  197. package/dist/ui/StatusBar.d.ts +2 -1
  198. package/dist/ui/StatusBar.js +6 -5
  199. package/package.json +3 -2
  200. package/src/agent/approval-json.ts +103 -0
  201. package/src/agent/approval-native.ts +168 -0
  202. package/src/agent/approval-store.ts +38 -0
  203. package/src/agent/approval.ts +94 -0
  204. package/src/agent/compaction.ts +321 -0
  205. package/src/agent/context.ts +97 -0
  206. package/src/agent/guards.ts +1239 -19
  207. package/src/agent/ignore.ts +147 -0
  208. package/src/agent/policy/defaults.ts +34 -0
  209. package/src/agent/policy/egress.ts +335 -0
  210. package/src/agent/process-tree.ts +114 -0
  211. package/src/agent/protocol-mode.ts +59 -0
  212. package/src/agent/protocol.ts +191 -40
  213. package/src/agent/registry.ts +270 -0
  214. package/src/agent/runner.ts +486 -0
  215. package/src/agent/shell-ast.ts +227 -0
  216. package/src/agent/shell-session.ts +673 -0
  217. package/src/agent/stream.ts +143 -0
  218. package/src/agent/tool-schemas.ts +86 -0
  219. package/src/agent/tools/append_file.ts +30 -0
  220. package/src/agent/tools/apply_patch.ts +95 -0
  221. package/src/agent/tools/copy_file.ts +56 -0
  222. package/src/agent/tools/delete_file.ts +45 -0
  223. package/src/agent/tools/grep.ts +57 -0
  224. package/src/agent/tools/index.ts +34 -0
  225. package/src/agent/tools/list_files.ts +19 -0
  226. package/src/agent/tools/make_dir.ts +28 -0
  227. package/src/agent/tools/move_path.ts +83 -0
  228. package/src/agent/tools/paths.ts +92 -0
  229. package/src/agent/tools/pwd.ts +12 -0
  230. package/src/agent/tools/read_file.ts +84 -0
  231. package/src/agent/tools/read_many.ts +45 -0
  232. package/src/agent/tools/readers.ts +491 -0
  233. package/src/agent/tools/replace_in_file.ts +66 -0
  234. package/src/agent/tools/shell.ts +53 -0
  235. package/src/agent/tools/stat_path.ts +33 -0
  236. package/src/agent/tools/types.ts +84 -0
  237. package/src/agent/tools/validate.ts +104 -0
  238. package/src/agent/tools/write_file.ts +30 -0
  239. package/src/agent/tools.ts +3 -377
  240. package/src/app.ts +853 -170
  241. package/src/{cli.ts → cli-main.ts} +64 -9
  242. package/src/commands/about.ts +1 -1
  243. package/src/commands/attach.ts +16 -0
  244. package/src/commands/bg.ts +9 -0
  245. package/src/commands/cancel.ts +10 -0
  246. package/src/commands/clear.ts +1 -1
  247. package/src/commands/compact.ts +35 -0
  248. package/src/commands/context.ts +26 -0
  249. package/src/commands/edit.ts +37 -0
  250. package/src/commands/effort.ts +49 -0
  251. package/src/commands/expand.ts +8 -0
  252. package/src/commands/index.ts +24 -0
  253. package/src/commands/jobs.ts +18 -0
  254. package/src/commands/kill.ts +14 -0
  255. package/src/commands/login.ts +24 -22
  256. package/src/commands/logout.ts +8 -16
  257. package/src/commands/model.ts +10 -22
  258. package/src/commands/provider.ts +4 -11
  259. package/src/commands/skills.ts +34 -4
  260. package/src/commands/steer.ts +13 -0
  261. package/src/commands/threads.ts +28 -0
  262. package/src/commands/whoami.ts +6 -5
  263. package/src/config/store.ts +168 -77
  264. package/src/config/trust.ts +181 -22
  265. package/src/headless.ts +225 -0
  266. package/src/index.ts +19 -3
  267. package/src/lib/debug.ts +24 -0
  268. package/src/lib/diagnostics.ts +90 -8
  269. package/src/lib/highlight-subset.ts +93 -0
  270. package/src/lib/http.ts +66 -4
  271. package/src/lib/lifecycle.ts +69 -0
  272. package/src/lib/markdown.ts +22 -4
  273. package/src/lib/model-meta.ts +138 -0
  274. package/src/lib/native-tools.ts +32 -0
  275. package/src/lib/net.ts +204 -0
  276. package/src/patch/apply.ts +194 -0
  277. package/src/patch/parse.ts +131 -0
  278. package/src/patch/plan.ts +317 -0
  279. package/src/providers/anthropic.ts +13 -1
  280. package/src/providers/deepseek.ts +3 -2
  281. package/src/providers/groq.ts +2 -1
  282. package/src/providers/index.ts +32 -1
  283. package/src/providers/mistral.ts +2 -1
  284. package/src/providers/moonshot.ts +2 -1
  285. package/src/providers/openai.ts +2 -1
  286. package/src/providers/openaiCompatible.ts +104 -19
  287. package/src/providers/openrouter.ts +2 -1
  288. package/src/providers/qwen.ts +2 -1
  289. package/src/providers/spec.ts +42 -0
  290. package/src/providers/xai.ts +2 -1
  291. package/src/providers/zhipu.ts +2 -1
  292. package/src/session/log.ts +311 -29
  293. package/src/session/projection.ts +621 -0
  294. package/src/skills/manager.ts +140 -18
  295. package/src/types/highlight-shims.d.ts +14 -0
  296. package/src/types.ts +79 -2
  297. package/src/ui/Message.ts +13 -2
  298. package/src/ui/Picker.ts +24 -0
  299. package/src/ui/Prompt.ts +2 -2
  300. package/src/ui/Spinner.ts +11 -5
  301. package/src/ui/StatusBar.ts +6 -5
  302. package/tsconfig.json +1 -0
  303. /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
@@ -0,0 +1,611 @@
1
+ import { spawn } from "node:child_process";
2
+ import parse from "bash-parser";
3
+ import { terminateProcessTree } from "./process-tree.js";
4
+ import { normalizeTimeoutMs } from "./tools/validate.js";
5
+ const OUTPUT_LIMIT = 1024 * 1024;
6
+ const DEFAULT_BACKGROUND_MS = 2000;
7
+ const DEFAULT_TIMEOUT_MS = 120_000;
8
+ export const DEFAULT_SHELL_POOL_SIZE = 2;
9
+ export const MAX_SHELL_POOL_SIZE = 6;
10
+ let poolLimit = DEFAULT_SHELL_POOL_SIZE;
11
+ const jobs = new Map();
12
+ const pool = [];
13
+ let spawns = 0;
14
+ let spawnTurn = Number.NaN;
15
+ let spawnsThisTurn = 0;
16
+ class ShellJob {
17
+ id;
18
+ key;
19
+ command;
20
+ cwd;
21
+ startedAt = Date.now();
22
+ output = "";
23
+ delivered = 0;
24
+ status = "running";
25
+ exitCode;
26
+ nextPollMs = 1000;
27
+ child;
28
+ resident;
29
+ donePromise;
30
+ resolveDone;
31
+ waiters = [];
32
+ timeoutTimer;
33
+ abortSignal;
34
+ abortListener;
35
+ cleaned = false;
36
+ constructor(command, cwd) {
37
+ this.id = "sh_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 7);
38
+ this.key = this.id.replace(/[^a-z0-9]/gi, "");
39
+ this.command = command;
40
+ this.cwd = cwd;
41
+ this.donePromise = new Promise((resolve) => {
42
+ this.resolveDone = resolve;
43
+ });
44
+ }
45
+ done() {
46
+ return this.donePromise;
47
+ }
48
+ append(text) {
49
+ if (!text)
50
+ return;
51
+ this.output += text;
52
+ if (this.output.length > OUTPUT_LIMIT) {
53
+ const over = this.output.length - OUTPUT_LIMIT;
54
+ this.output = this.output.slice(over);
55
+ this.delivered = Math.max(0, this.delivered - over);
56
+ }
57
+ this.notify();
58
+ }
59
+ takeOutput() {
60
+ const next = this.output.slice(this.delivered);
61
+ this.delivered = this.output.length;
62
+ return next;
63
+ }
64
+ finish(status, exitCode) {
65
+ if (this.status !== "running")
66
+ return;
67
+ this.status = status;
68
+ this.exitCode = exitCode;
69
+ this.cleanupLifecycle();
70
+ this.resolveDone();
71
+ this.notify();
72
+ }
73
+ /**
74
+ * Task-level timeout lives until the job actually finishes. Returning to
75
+ * the foreground only detaches the AbortSignal; it must not disarm this.
76
+ */
77
+ armTimeout(ms) {
78
+ if (this.timeoutTimer || this.status !== "running")
79
+ return;
80
+ this.timeoutTimer = setTimeout(() => this.kill("timeout"), ms);
81
+ this.timeoutTimer.unref?.();
82
+ }
83
+ attachAbort(signal) {
84
+ if (!signal || this.abortListener)
85
+ return;
86
+ const listener = () => this.kill("killed");
87
+ this.abortSignal = signal;
88
+ this.abortListener = listener;
89
+ signal.addEventListener("abort", listener, { once: true });
90
+ }
91
+ /** Detach Esc/foreground cancellation but keep the timeout armed. */
92
+ detachForeground() {
93
+ if (this.abortSignal && this.abortListener)
94
+ this.abortSignal.removeEventListener("abort", this.abortListener);
95
+ this.abortSignal = undefined;
96
+ this.abortListener = undefined;
97
+ }
98
+ cleanupLifecycle() {
99
+ if (this.cleaned)
100
+ return;
101
+ this.cleaned = true;
102
+ if (this.timeoutTimer) {
103
+ clearTimeout(this.timeoutTimer);
104
+ this.timeoutTimer = undefined;
105
+ }
106
+ this.detachForeground();
107
+ }
108
+ waitForChange(ms) {
109
+ if (this.status !== "running" || this.delivered < this.output.length)
110
+ return Promise.resolve();
111
+ if (ms <= 0)
112
+ return Promise.resolve();
113
+ return new Promise((resolve) => {
114
+ let timer;
115
+ const wake = () => {
116
+ if (timer)
117
+ clearTimeout(timer);
118
+ this.waiters = this.waiters.filter((entry) => entry !== wake);
119
+ resolve();
120
+ };
121
+ this.waiters.push(wake);
122
+ timer = setTimeout(wake, ms);
123
+ });
124
+ }
125
+ notify() {
126
+ const waiters = this.waiters;
127
+ this.waiters = [];
128
+ for (const waiter of waiters)
129
+ waiter();
130
+ }
131
+ view() {
132
+ return {
133
+ id: this.id,
134
+ command: this.command,
135
+ cwd: this.cwd,
136
+ status: this.status,
137
+ exitCode: this.exitCode,
138
+ bytes: this.output.length,
139
+ startedAt: this.startedAt,
140
+ pid: this.child?.pid ?? this.resident?.child.pid,
141
+ timeoutArmed: Boolean(this.timeoutTimer),
142
+ foregroundAttached: Boolean(this.abortListener),
143
+ };
144
+ }
145
+ kill(reason) {
146
+ if (this.status !== "running")
147
+ return;
148
+ this.append(reason === "timeout"
149
+ ? "\n[command timed out: process group killed; setsid descendants may escape]"
150
+ : "\n[command aborted: process group killed; setsid descendants may escape]");
151
+ this.status = reason;
152
+ this.cleanupLifecycle();
153
+ if (this.resident)
154
+ this.resident.kill();
155
+ else if (this.child)
156
+ terminateProcessTree(this.child);
157
+ this.resolveDone();
158
+ this.notify();
159
+ }
160
+ }
161
+ class ResidentShell {
162
+ child;
163
+ job;
164
+ dead = false;
165
+ buffer = "";
166
+ background = false;
167
+ claimed = false;
168
+ constructor(cwd) {
169
+ const env = { ...process.env, FASTCODE_SANDBOX: "1", FASTCODE_SANDBOX_ROOT: cwd };
170
+ this.child = spawn("sh", ["-s"], {
171
+ cwd,
172
+ env,
173
+ detached: process.platform !== "win32",
174
+ windowsHide: true,
175
+ });
176
+ this.child.stdout.setEncoding("utf8");
177
+ this.child.stderr.setEncoding("utf8");
178
+ this.child.stdout.on("data", (chunk) => this.consume(chunk));
179
+ this.child.stderr.on("data", (chunk) => this.consume(chunk));
180
+ this.child.on("error", (err) => {
181
+ this.fail(err.message);
182
+ });
183
+ this.child.on("close", (code) => {
184
+ this.dead = true;
185
+ this.fail(`shell exited (${code ?? "unknown"})`, code);
186
+ const index = pool.indexOf(this);
187
+ if (index >= 0)
188
+ pool.splice(index, 1);
189
+ });
190
+ }
191
+ claimIfIdle() {
192
+ if (this.dead || this.job || this.claimed)
193
+ return false;
194
+ this.claimed = true;
195
+ return true;
196
+ }
197
+ run(job, timeoutMs, background = false) {
198
+ this.claimed = false;
199
+ this.job = job;
200
+ this.background = background;
201
+ this.buffer = "";
202
+ job.resident = this;
203
+ const marker = `__FC_END_${job.key}__`;
204
+ const body = job.command.trim() ? ["(", job.command, ")"] : [":"];
205
+ const script = [
206
+ `if cd ${shellQuote(job.cwd)}; then`,
207
+ ...body,
208
+ "__fc_status=$?",
209
+ "else",
210
+ "__fc_status=1",
211
+ "fi",
212
+ `printf '\\n${marker}:%s\\n' "$__fc_status"`,
213
+ "",
214
+ ].join("\n");
215
+ try {
216
+ this.child.stdin.write(script);
217
+ }
218
+ catch (err) {
219
+ job.append(err?.message ?? String(err));
220
+ job.finish("failed", 1);
221
+ }
222
+ void timeoutMs;
223
+ }
224
+ consume(chunk) {
225
+ if (this.dead)
226
+ return;
227
+ // Output that arrives while no job owns the shell belongs to an
228
+ // already-finished background child. Never carry it into the next job.
229
+ if (!this.job) {
230
+ this.buffer = "";
231
+ return;
232
+ }
233
+ this.buffer += chunk;
234
+ const marker = `__FC_END_${this.job.key}__:`;
235
+ const index = this.buffer.indexOf(marker);
236
+ if (index < 0) {
237
+ const sentinel = "\n" + marker;
238
+ let keep = 0;
239
+ const max = Math.min(this.buffer.length, sentinel.length);
240
+ for (let length = max; length > 0; length -= 1) {
241
+ if (sentinel.startsWith(this.buffer.slice(this.buffer.length - length))) {
242
+ keep = length;
243
+ break;
244
+ }
245
+ }
246
+ const flush = this.buffer.length - keep;
247
+ if (flush > 0) {
248
+ this.job.append(this.buffer.slice(0, flush));
249
+ this.buffer = this.buffer.slice(flush);
250
+ }
251
+ return;
252
+ }
253
+ const rest = this.buffer.slice(index + marker.length);
254
+ const newline = rest.indexOf("\n");
255
+ if (newline < 0)
256
+ return;
257
+ const job = this.job;
258
+ job.append(this.buffer.slice(0, index));
259
+ const code = Number(rest.slice(0, newline).trim());
260
+ this.buffer = rest.slice(newline + 1);
261
+ this.job = undefined;
262
+ const background = this.background;
263
+ this.background = false;
264
+ job.finish(Number.isFinite(code) && code === 0 ? "done" : "failed", Number.isFinite(code) ? code : 1);
265
+ // A background operator leaves a detached child writing into this
266
+ // shell's stdout pipe. Retire the resident so late output cannot be
267
+ // attributed to the next command.
268
+ if (background)
269
+ this.kill();
270
+ }
271
+ fail(message, code) {
272
+ this.claimed = false;
273
+ if (!this.job)
274
+ return;
275
+ const job = this.job;
276
+ this.job = undefined;
277
+ this.background = false;
278
+ if (this.buffer) {
279
+ job.append(this.buffer);
280
+ this.buffer = "";
281
+ }
282
+ if (code === 0) {
283
+ job.finish("done", 0);
284
+ return;
285
+ }
286
+ job.append(`\n${message}`);
287
+ job.finish("failed", code ?? 1);
288
+ }
289
+ kill() {
290
+ this.dead = true;
291
+ this.claimed = false;
292
+ this.background = false;
293
+ this.buffer = "";
294
+ terminateProcessTree(this.child);
295
+ const index = pool.indexOf(this);
296
+ if (index >= 0)
297
+ pool.splice(index, 1);
298
+ }
299
+ }
300
+ export async function executeShellCommand(command, cwd, options = {}) {
301
+ pruneJobs();
302
+ if (options.signal?.aborted)
303
+ return { ok: false, status: "killed", output: "command aborted before start" };
304
+ const timeoutMs = normalizeTimeoutMs(options.timeoutMs, DEFAULT_TIMEOUT_MS);
305
+ const backgroundAfterMs = options.backgroundAfterMs ?? DEFAULT_BACKGROUND_MS;
306
+ const shape = analyzeShellShape(command);
307
+ const useResident = process.platform !== "win32" && !shape.oneShot;
308
+ let job;
309
+ if (useResident) {
310
+ const resident = await acquireResident(options.turn, cwd, timeoutMs, options.signal);
311
+ if (resident === "limit")
312
+ return { ok: false, status: "failed", output: spawnLimitMessage() };
313
+ if (resident === "timeout")
314
+ return { ok: false, status: "timeout", output: "shell pool is busy; timed out waiting for a free resident shell" };
315
+ if (resident === "killed")
316
+ return { ok: false, status: "killed", output: "command aborted before start" };
317
+ job = new ShellJob(command, cwd);
318
+ resident.run(job, timeoutMs, shape.background);
319
+ }
320
+ else {
321
+ if (!maySpawn(options.turn))
322
+ return { ok: false, status: "failed", output: spawnLimitMessage() };
323
+ job = startOneShot(command, cwd);
324
+ }
325
+ jobs.set(job.id, job);
326
+ job.armTimeout(timeoutMs);
327
+ job.attachAbort(options.signal);
328
+ if (options.signal?.aborted)
329
+ job.kill("killed");
330
+ await Promise.race([job.done(), delay(backgroundAfterMs)]);
331
+ if (job.status === "running") {
332
+ // Task keeps running: only the foreground Esc/abort association is
333
+ // released. The timeout stays armed until the job really finishes.
334
+ job.detachForeground();
335
+ return {
336
+ ok: true,
337
+ status: "running",
338
+ output: job.takeOutput(),
339
+ sessionId: job.id,
340
+ nextPollMs: job.nextPollMs,
341
+ };
342
+ }
343
+ return syncResult(job);
344
+ }
345
+ export async function pollShellSession(id, waitMs = 0) {
346
+ pruneJobs();
347
+ const job = jobs.get(id);
348
+ if (!job)
349
+ return undefined;
350
+ return await pollOnce(job, waitMs);
351
+ }
352
+ async function pollOnce(job, waitMs) {
353
+ await job.waitForChange(waitMs);
354
+ const output = job.takeOutput();
355
+ if (output)
356
+ job.nextPollMs = 1000;
357
+ else if (job.status === "running")
358
+ job.nextPollMs = Math.min(30_000, job.nextPollMs * 2);
359
+ return {
360
+ ok: job.status === "done" || job.status === "running",
361
+ status: job.status,
362
+ output,
363
+ exitCode: job.exitCode,
364
+ sessionId: job.id,
365
+ nextPollMs: job.nextPollMs,
366
+ };
367
+ }
368
+ export function killShellSession(id) {
369
+ const job = jobs.get(id);
370
+ if (!job || job.status !== "running")
371
+ return false;
372
+ job.kill("killed");
373
+ return true;
374
+ }
375
+ export function listShellSessions() {
376
+ pruneJobs();
377
+ return Array.from(jobs.values())
378
+ .sort((a, b) => b.startedAt - a.startedAt)
379
+ .map((job) => job.view());
380
+ }
381
+ export function shutdownShells() {
382
+ for (const job of jobs.values()) {
383
+ if (job.status === "running")
384
+ job.kill("killed");
385
+ }
386
+ for (const resident of [...pool])
387
+ resident.kill();
388
+ pool.length = 0;
389
+ jobs.clear();
390
+ }
391
+ /**
392
+ * Exit-safe stop: send SIGTERM, wait briefly for close, then force SIGKILL.
393
+ * Use this when the process is about to exit and must not leave children.
394
+ */
395
+ export async function shutdownShellsAsync(graceMs = 400) {
396
+ const running = Array.from(jobs.values()).filter((job) => job.status === "running");
397
+ const residents = pool.filter((resident) => !resident.dead);
398
+ for (const job of running)
399
+ job.kill("killed");
400
+ for (const resident of residents)
401
+ resident.kill();
402
+ pool.length = 0;
403
+ jobs.clear();
404
+ const children = [];
405
+ for (const job of running) {
406
+ if (job.child)
407
+ children.push(job.child);
408
+ }
409
+ for (const resident of residents)
410
+ children.push(resident.child);
411
+ if (children.length === 0)
412
+ return;
413
+ await Promise.race([
414
+ Promise.all(children.map((child) => waitForChildClose(child, graceMs))),
415
+ delay(graceMs + 50),
416
+ ]);
417
+ for (const child of children) {
418
+ if (child.exitCode === null && child.signalCode === null)
419
+ terminateProcessTree(child, { graceMs: 0 });
420
+ }
421
+ await delay(50);
422
+ }
423
+ function waitForChildClose(child, ms) {
424
+ if (child.exitCode !== null || child.signalCode !== null)
425
+ return Promise.resolve();
426
+ return new Promise((resolve) => {
427
+ let timer;
428
+ const done = () => {
429
+ if (timer)
430
+ clearTimeout(timer);
431
+ child.removeListener("close", done);
432
+ resolve();
433
+ };
434
+ timer = setTimeout(done, ms);
435
+ child.once("close", done);
436
+ });
437
+ }
438
+ export function shellPoolSize() {
439
+ return poolLimit;
440
+ }
441
+ /** Clamp and apply the resident shell concurrency (1..6, default 2). */
442
+ export function setShellPoolSize(size) {
443
+ const parsed = Number(size);
444
+ poolLimit = Number.isFinite(parsed)
445
+ ? Math.min(MAX_SHELL_POOL_SIZE, Math.max(1, Math.floor(parsed)))
446
+ : DEFAULT_SHELL_POOL_SIZE;
447
+ return poolLimit;
448
+ }
449
+ export function shellPoolStats() {
450
+ return {
451
+ spawns,
452
+ active: Array.from(jobs.values()).filter((job) => job.status === "running").length,
453
+ pool: pool.filter((resident) => !resident.dead).length,
454
+ maxPool: poolLimit,
455
+ maxSpawnPerTurn: maxSpawnPerTurn(),
456
+ };
457
+ }
458
+ function startOneShot(command, cwd) {
459
+ const job = new ShellJob(command, cwd);
460
+ const child = spawn(command, {
461
+ cwd,
462
+ shell: true,
463
+ env: { ...process.env, FASTCODE_SANDBOX: "1", FASTCODE_SANDBOX_ROOT: cwd },
464
+ windowsHide: true,
465
+ detached: process.platform !== "win32",
466
+ });
467
+ job.child = child;
468
+ child.stdout?.on("data", (chunk) => job.append(chunk.toString("utf8")));
469
+ child.stderr?.on("data", (chunk) => job.append(chunk.toString("utf8")));
470
+ child.on("error", (err) => {
471
+ job.append("\n" + err.message);
472
+ job.finish("failed", 1);
473
+ });
474
+ child.on("close", (code) => {
475
+ if (job.status === "running")
476
+ job.finish(code === 0 ? "done" : "failed", code);
477
+ });
478
+ return job;
479
+ }
480
+ async function acquireResident(turn, cwd, waitMs, signal) {
481
+ const deadline = Date.now() + waitMs;
482
+ while (true) {
483
+ for (const resident of pool) {
484
+ if (resident.claimIfIdle())
485
+ return resident;
486
+ }
487
+ const live = pool.filter((resident) => !resident.dead);
488
+ if (live.length < poolLimit && maySpawn(turn)) {
489
+ const resident = new ResidentShell(cwd);
490
+ resident.claimIfIdle();
491
+ pool.push(resident);
492
+ return resident;
493
+ }
494
+ // No free shell and no spawn budget: wait for a running command to
495
+ // finish instead of failing the third call with "both shells busy".
496
+ if (live.length === 0)
497
+ return "limit";
498
+ if (signal?.aborted)
499
+ return "killed";
500
+ const remaining = deadline - Date.now();
501
+ if (remaining <= 0)
502
+ return "timeout";
503
+ await delay(Math.min(remaining, 50));
504
+ }
505
+ }
506
+ const INTERACTIVE_COMMANDS = new Set(["vim", "nano", "less", "more", "ssh", "su", "login", "read", "top", "htop", "tmux", "screen"]);
507
+ const REPL_COMMANDS = new Set(["python", "python3", "node", "bash", "zsh", "fish", "irb", "sqlite3"]);
508
+ const WRAPPER_COMMANDS = new Set(["sudo", "env", "nohup", "command", "time", "exec"]);
509
+ function analyzeShellShape(command) {
510
+ let ast;
511
+ try {
512
+ ast = parse(command, { mode: "bash" });
513
+ }
514
+ catch {
515
+ // Fall back to the historical heuristic only when the parser cannot
516
+ // classify the command at all.
517
+ return {
518
+ oneShot: /(^|\s)(vim|nano|less|more|ssh|su|login|read|top|htop|tmux|screen)(\s|$)/.test(command)
519
+ || /(^|\s)(python|python3|node|bash|zsh|fish|irb|sqlite3)(\s|$)/.test(command) && !/\s-(c|e|m)\s/.test(command),
520
+ background: /(^|[^&>])&([^&>]|$)/.test(command),
521
+ };
522
+ }
523
+ let oneShot = false;
524
+ let background = false;
525
+ const walk = (node, depth = 0) => {
526
+ if (!node || typeof node !== "object" || depth > 32)
527
+ return;
528
+ if (Array.isArray(node)) {
529
+ for (const entry of node)
530
+ walk(entry, depth + 1);
531
+ return;
532
+ }
533
+ // bash-parser also marks `cmd &> file` as async; only retire the
534
+ // resident for a real background operator, not a redirect.
535
+ if (node.async === true && /(^|[^&>])&([^&>]|$)/.test(command))
536
+ background = true;
537
+ if (node.type === "Command" && typeof node.name?.text === "string") {
538
+ const name = node.name.text;
539
+ const args = Array.isArray(node.suffix)
540
+ ? node.suffix.map((word) => typeof word?.text === "string" ? word.text : "").filter(Boolean)
541
+ : [];
542
+ const replOptions = (values) => values.some((arg) => /^(-c|-e|-m|-p|--eval|--print|--command)$/.test(arg));
543
+ if (INTERACTIVE_COMMANDS.has(name) || REPL_COMMANDS.has(name) && !replOptions(args))
544
+ oneShot = true;
545
+ if (WRAPPER_COMMANDS.has(name)) {
546
+ const wrappedIndex = args.findIndex((arg) => INTERACTIVE_COMMANDS.has(arg) || REPL_COMMANDS.has(arg));
547
+ if (wrappedIndex >= 0) {
548
+ const wrappedName = args[wrappedIndex];
549
+ if (INTERACTIVE_COMMANDS.has(wrappedName) ||
550
+ REPL_COMMANDS.has(wrappedName) && !replOptions(args.slice(wrappedIndex + 1)))
551
+ oneShot = true;
552
+ }
553
+ }
554
+ }
555
+ for (const value of Object.values(node)) {
556
+ if (value && typeof value === "object")
557
+ walk(value, depth + 1);
558
+ }
559
+ };
560
+ walk(ast);
561
+ return { oneShot, background };
562
+ }
563
+ function maySpawn(turn) {
564
+ const current = typeof turn === "number" ? turn : 0;
565
+ if (current !== spawnTurn) {
566
+ spawnTurn = current;
567
+ spawnsThisTurn = 0;
568
+ }
569
+ if (spawnsThisTurn >= maxSpawnPerTurn())
570
+ return false;
571
+ spawnsThisTurn += 1;
572
+ spawns += 1;
573
+ return true;
574
+ }
575
+ function maxSpawnPerTurn() {
576
+ const raw = Number(process.env["FASTCODE_MAX_SPAWN_PER_TURN"]);
577
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 8;
578
+ }
579
+ function spawnLimitMessage() {
580
+ return `spawn limit reached (FASTCODE_MAX_SPAWN_PER_TURN=${maxSpawnPerTurn()}); batch commands or poll an existing session`;
581
+ }
582
+ function syncResult(job) {
583
+ return {
584
+ ok: job.status === "done",
585
+ status: job.status,
586
+ output: job.takeOutput(),
587
+ exitCode: job.exitCode,
588
+ sessionId: job.id,
589
+ nextPollMs: job.nextPollMs,
590
+ };
591
+ }
592
+ function pruneJobs() {
593
+ const now = Date.now();
594
+ for (const [id, job] of jobs) {
595
+ if (job.status !== "running" && now - job.startedAt > 10 * 60 * 1000)
596
+ jobs.delete(id);
597
+ }
598
+ if (jobs.size > 50) {
599
+ const finished = Array.from(jobs.values())
600
+ .filter((job) => job.status !== "running")
601
+ .sort((a, b) => a.startedAt - b.startedAt);
602
+ for (const job of finished.slice(0, jobs.size - 50))
603
+ jobs.delete(job.id);
604
+ }
605
+ }
606
+ function shellQuote(value) {
607
+ return "'" + value.replace(/'/g, "'\\''") + "'";
608
+ }
609
+ function delay(ms) {
610
+ return new Promise((resolve) => setTimeout(resolve, ms));
611
+ }
@@ -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>;