@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
@@ -1,18 +1,82 @@
1
- const TOOL_RE = /<fastcode_tool>([\s\S]*?)<\/fastcode_tool>/g;
2
1
  // Some models (notably DeepSeek) emit their native "DSML" tool-call markup
3
2
  // instead of <fastcode_tool>. The markers look like `<|DSML|invoke ...>`
4
3
  // (U+FF5C fullwidth bar, sometimes doubled or spaced). We canonicalize those
5
4
  // markers back to plain angle-bracket tags and then parse the
6
5
  // calls -> invoke -> parameter structure as a second dialect.
6
+ const FASTCODE_TOOL_OPEN = "<fastcode_tool>";
7
+ const FASTCODE_TOOL_CLOSE_RE = /<\/\s*fastcode_tool\s*>/gi;
8
+ const RESIDUAL_CLOSE_RE = /<\/\s*(?:fastcode_tool|parameter|invoke|calls)\s*>/i;
7
9
  const DSML_OPEN_RE = /<\s*(?:[||]\s*)+DSML\s*(?:[||]\s*)+/gi;
8
10
  const DSML_CLOSE_RE = /<\/\s*(?:[||]\s*)+DSML\s*(?:[||]\s*)+/gi;
9
- const DSML_BLOCK_RE = /<([a-z_]*calls?)>([\s\S]*?)<\/\1>/gi;
10
- const DSML_INVOKE_RE = /<invoke\s+name="([^"]+)"\s*>([\s\S]*?)<\/invoke>/gi;
11
- const DSML_PARAM_RE = /<parameter\s+name="([^"]+)"([^>]*)>([\s\S]*?)<\/parameter>/gi;
11
+ const DSML_BLOCK_RE = /<([a-z_]*calls?)>([\s\S]*?)<\/\s*\1\s*>/gi;
12
+ const DSML_INVOKE_RE = /<invoke\s+name="([^"]+)"\s*>([\s\S]*?)<\/\s*invoke\s*>/gi;
13
+ const DSML_PARAM_RE = /<parameter\s+name="([^"]+)"([^>]*)>([\s\S]*?)<\/\s*parameter\s*>/gi;
14
+ const INVOKE_OPEN_RE = /<invoke\s+name="([^"]+)"\s*>/gi;
15
+
16
+ export interface ToolCallRecord {
17
+ tool: string;
18
+ [key: string]: any;
19
+ }
20
+
21
+ export interface ToolCallParseResult {
22
+ calls: ToolCallRecord[];
23
+ /** End offset of the last complete construct that was consumed. */
24
+ consumedEnd: number;
25
+ /** True parse failures. Salvaged calls do not add an error. */
26
+ errors: string[];
27
+ }
28
+
12
29
  /** Turn the DSML glyph markers into ordinary `<tag>` / `</tag>` tags. */
13
30
  function canonicalizeDsml(text) {
14
31
  return text.replace(DSML_CLOSE_RE, "</").replace(DSML_OPEN_RE, "<");
15
32
  }
33
+ function invokeToCall(invoke): ToolCallRecord {
34
+ const call: ToolCallRecord = { tool: invoke[1] };
35
+ for (const param of (invoke[2] ?? "").matchAll(DSML_PARAM_RE))
36
+ call[param[1]] = coerceDsmlValue(param[2] ?? "", param[3] ?? "");
37
+ return call;
38
+ }
39
+ function insideSpan(start: number, spans: Array<{ start: number; end: number }>): boolean {
40
+ return spans.some((span) => start >= span.start && start < span.end);
41
+ }
42
+ function matchFrom(text: string, from: number, re: RegExp): { start: number; end: number } | undefined {
43
+ re.lastIndex = from;
44
+ const match = re.exec(text);
45
+ if (!match || match.index === undefined)
46
+ return undefined;
47
+ return { start: match.index, end: match.index + match[0].length };
48
+ }
49
+ function parseJsonCall(body: string): ToolCallRecord | undefined {
50
+ try {
51
+ const parsed = JSON.parse(body.trim());
52
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.tool === "string")
53
+ return parsed as ToolCallRecord;
54
+ }
55
+ catch {
56
+ // Malformed JSON is reported by the caller.
57
+ }
58
+ return undefined;
59
+ }
60
+ /**
61
+ * Recover a JSON object that starts at the beginning of `text` but may be
62
+ * followed by more markup (for example a drifted close tag). Returns the
63
+ * consumed prefix length so the caller can continue after it.
64
+ */
65
+ function salvageJsonPrefix(text: string): { call: ToolCallRecord; consumed: number } | undefined {
66
+ let searchFrom = 0;
67
+ while (searchFrom < text.length) {
68
+ const end = text.indexOf("}", searchFrom);
69
+ if (end < 0)
70
+ return undefined;
71
+ const call = parseJsonCall(text.slice(0, end + 1));
72
+ if (call) {
73
+ const close = matchFrom(text, end + 1, FASTCODE_TOOL_CLOSE_RE);
74
+ return { call, consumed: close ? close.end : end + 1 };
75
+ }
76
+ searchFrom = end + 1;
77
+ }
78
+ return undefined;
79
+ }
16
80
  function coerceDsmlValue(attrs, raw) {
17
81
  const value = raw.replace(/^\n/, "").replace(/\n$/, "");
18
82
  if (/\bstring\s*=\s*"true"/i.test(attrs))
@@ -32,44 +96,126 @@ function coerceDsmlValue(attrs, raw) {
32
96
  return Number.isNaN(n) ? value : n;
33
97
  }
34
98
  }
35
- function parseDsmlCalls(text) {
36
- const canonical = canonicalizeDsml(text);
37
- const bodies = [];
38
- for (const block of canonical.matchAll(DSML_BLOCK_RE))
39
- bodies.push(block[2] ?? "");
40
- if (bodies.length === 0)
41
- bodies.push(canonical);
42
- const calls = [];
43
- for (const body of bodies) {
44
- for (const invoke of body.matchAll(DSML_INVOKE_RE)) {
45
- const call = { tool: invoke[1] };
46
- for (const param of (invoke[2] ?? "").matchAll(DSML_PARAM_RE))
47
- call[param[1]] = coerceDsmlValue(param[2] ?? "", param[3] ?? "");
48
- calls.push(call);
99
+ /**
100
+ * Parse all complete tool calls, then salvage only the *remaining* text.
101
+ *
102
+ * Invariants (regression-tested):
103
+ * - a complete construct is consumed exactly once and never rescanned;
104
+ * - the salvage pass receives `raw.slice(consumedEnd)`, never the full text;
105
+ * - two adjacent normal calls plus a stray close tag stays at two calls.
106
+ */
107
+ export function parseToolCalls(text): ToolCallParseResult {
108
+ const raw = canonicalizeDsml(String(text ?? ""));
109
+ const calls: ToolCallRecord[] = [];
110
+ const errors: string[] = [];
111
+ const spans: Array<{ start: number; end: number }> = [];
112
+ let cursor = 0;
113
+ while (cursor < raw.length) {
114
+ const open = raw.indexOf(FASTCODE_TOOL_OPEN, cursor);
115
+ if (open < 0)
116
+ break;
117
+ const innerStart = open + FASTCODE_TOOL_OPEN.length;
118
+ const close = matchFrom(raw, innerStart, FASTCODE_TOOL_CLOSE_RE);
119
+ if (!close) {
120
+ const salvaged = salvageJsonPrefix(raw.slice(innerStart));
121
+ if (salvaged) {
122
+ calls.push(salvaged.call);
123
+ cursor = innerStart + salvaged.consumed;
124
+ spans.push({ start: open, end: cursor });
125
+ continue;
126
+ }
127
+ errors.push("unclosed <fastcode_tool> block; re-emit a complete tool call");
128
+ break;
129
+ }
130
+ const body = raw.slice(innerStart, close.start);
131
+ const full = parseJsonCall(body);
132
+ if (full) {
133
+ calls.push(full);
134
+ spans.push({ start: open, end: close.end });
135
+ cursor = close.end;
136
+ continue;
49
137
  }
138
+ // Closing drift: the JSON may be valid even when extra markup (or a
139
+ // second opener) sits before the matched close tag.
140
+ const prefix = salvageJsonPrefix(body);
141
+ if (prefix) {
142
+ calls.push(prefix.call);
143
+ cursor = innerStart + prefix.consumed;
144
+ spans.push({ start: open, end: cursor });
145
+ continue;
146
+ }
147
+ errors.push("invalid JSON inside <fastcode_tool>; re-emit a complete tool call");
148
+ spans.push({ start: open, end: close.end });
149
+ cursor = close.end;
50
150
  }
51
- return calls;
52
- }
53
- export function parseToolCalls(text) {
54
- const calls = [];
55
- for (const match of text.matchAll(TOOL_RE)) {
56
- const raw = match[1]?.trim();
57
- if (!raw)
151
+
152
+ // DSML dialect: parse each complete block once, then complete invokes that
153
+ // did not belong to a block. Spans prevent a second parse of the same text.
154
+ for (const block of raw.matchAll(DSML_BLOCK_RE)) {
155
+ const start = block.index ?? 0;
156
+ const end = start + block[0].length;
157
+ if (insideSpan(start, spans))
58
158
  continue;
59
- try {
60
- const parsed = JSON.parse(raw);
61
- if (typeof parsed.tool === "string")
62
- calls.push(parsed);
159
+ for (const invoke of (block[2] ?? "").matchAll(DSML_INVOKE_RE))
160
+ calls.push(invokeToCall(invoke));
161
+ spans.push({ start, end });
162
+ }
163
+ for (const invoke of raw.matchAll(DSML_INVOKE_RE)) {
164
+ const start = invoke.index ?? 0;
165
+ if (insideSpan(start, spans))
166
+ continue;
167
+ calls.push(invokeToCall(invoke));
168
+ spans.push({ start, end: start + invoke[0].length });
169
+ }
170
+
171
+ const consumedEnd = spans.reduce((max, span) => Math.max(max, span.end), 0);
172
+ salvageResidual(raw.slice(consumedEnd), calls, errors);
173
+ return { calls, consumedEnd, errors };
174
+ }
175
+
176
+ function salvageResidual(tail: string, calls: ToolCallRecord[], errors: string[]): void {
177
+ if (!tail.trim())
178
+ return;
179
+ let salvaged = false;
180
+
181
+ const toolOpen = tail.indexOf(FASTCODE_TOOL_OPEN);
182
+ if (toolOpen >= 0) {
183
+ const recovered = salvageJsonPrefix(tail.slice(toolOpen + FASTCODE_TOOL_OPEN.length));
184
+ if (recovered) {
185
+ calls.push(recovered.call);
186
+ salvaged = true;
63
187
  }
64
- catch {
65
- // Ignore malformed tool blocks; the model can correct itself next turn.
188
+ else {
189
+ errors.push("unclosed <fastcode_tool> block; re-emit a complete tool call");
66
190
  }
67
191
  }
68
- calls.push(...parseDsmlCalls(text));
69
- return calls;
192
+
193
+ const openers = Array.from(tail.matchAll(INVOKE_OPEN_RE));
194
+ openers.forEach((opener, index) => {
195
+ const start = opener.index ?? 0;
196
+ const bodyStart = start + opener[0].length;
197
+ const nextStart = openers[index + 1]?.index ?? tail.length;
198
+ const call = invokeToCall({ 1: opener[1], 2: tail.slice(bodyStart, nextStart) });
199
+ if (Object.keys(call).length > 1) {
200
+ calls.push(call);
201
+ salvaged = true;
202
+ }
203
+ else {
204
+ errors.push(`incomplete invoke for "${opener[1]}"; re-emit a complete tool call`);
205
+ }
206
+ });
207
+
208
+ if (!salvaged && openers.length === 0 && toolOpen < 0 && RESIDUAL_CLOSE_RE.test(tail))
209
+ errors.push("stray closing tag without an opening tool call; re-emit a complete tool call");
70
210
  }
211
+
71
212
  export function stripToolCalls(text) {
72
- return canonicalizeDsml(text).replace(TOOL_RE, "").replace(DSML_BLOCK_RE, "").trim();
213
+ return canonicalizeDsml(String(text ?? ""))
214
+ .replace(/<fastcode_tool>[\s\S]*?<\/\s*fastcode_tool\s*>/gi, "")
215
+ .replace(DSML_BLOCK_RE, "")
216
+ .replace(/<fastcode_tool>[\s\S]*$/i, "")
217
+ .replace(/<\/\s*(?:fastcode_tool|parameter|invoke|calls)\s*>/gi, "")
218
+ .trim();
73
219
  }
74
220
  export function formatToolResults(results) {
75
221
  const blocks = results.map((r, index) => {
@@ -97,20 +243,25 @@ You can inspect and change the user's machine by emitting exactly one or more to
97
243
  Available tools:
98
244
  - shell: {"tool":"shell","command":"command to run"}
99
245
  - shell with timeout: {"tool":"shell","command":"npm test","timeoutMs":120000}
246
+ - shell poll (long commands return a session id): {"tool":"shell","session_id":"sh_..."}
247
+ Polling returns only new output; use waitMs to wait briefly for more.
100
248
  - pwd: {"tool":"pwd"}
101
249
  - read_file: {"tool":"read_file","path":"path/to/file"}
102
- - read_many: {"tool":"read_many","paths":["file1","file2"]}
250
+ - read_file range: {"tool":"read_file","path":"large.ts","offset":800,"limit":50}
251
+ offset is 1-based; use it instead of reading a large file from the start.
252
+ - read_many: {"tool":"read_many","paths":["file1","file2"],"maxBytes":300000}
103
253
  - write_file: {"tool":"write_file","path":"path/to/file","content":"full file contents"}
104
254
  - append_file: {"tool":"append_file","path":"path/to/file","content":"text to append"}
105
255
  - apply_patch: {"tool":"apply_patch","patch":"*** Begin Patch\\n*** Update File: path\\n@@\\n-old\\n+new\\n*** End Patch\\n"}
106
- - list_files: {"tool":"list_files","path":"optional/path","maxDepth":2}
107
- - grep: {"tool":"grep","pattern":"text or regex","path":"optional/path"}
108
- - replace_in_file: {"tool":"replace_in_file","path":"path/to/file","find":"exact text","replace":"replacement"}
256
+ - list_files: {"tool":"list_files","path":"optional/path","maxDepth":2,"detail":false}
257
+ - read_file is capped (default 200KB); grep skips files over 1MB. .fastcodeignore is honored.
258
+ - grep: {"tool":"grep","pattern":"text or regex","path":"optional/path","caseSensitive":false,"glob":"*.ts","context":2,"maxResults":200}
259
+ - replace_in_file: {"tool":"replace_in_file","path":"path/to/file","find":"exact text","replace":"replacement","expectedMatches":1}
109
260
  - stat_path: {"tool":"stat_path","path":"path/to/file"}
110
261
  - make_dir: {"tool":"make_dir","path":"path/to/dir"}
111
- - copy_file: {"tool":"copy_file","from":"src","to":"dest"}
262
+ - copy_file: {"tool":"copy_file","from":"src","to":"dest","overwrite":false}
112
263
  - move_path: {"tool":"move_path","from":"src","to":"dest"}
113
- - delete_file: {"tool":"delete_file","path":"path/to/file"}
264
+ - delete_file: {"tool":"delete_file","path":"path/to/file","recursive":false}
114
265
 
115
266
  Rules:
116
267
  - Prefer inspecting files before editing.
@@ -0,0 +1,270 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isTrusted } from "../config/trust.js";
3
+ import {
4
+ DEFAULT_ALLOW_OPTIONS,
5
+ approvalRequestId,
6
+ type ApprovalDecision,
7
+ type ApprovalOption,
8
+ type ApprovalRequest,
9
+ } from "./approval.js";
10
+ import { approvalKey, isPreApproved, rememberApproval } from "./approval-store.js";
11
+ import { evaluateShellCommand } from "./guards.js";
12
+ import { TOOLS } from "./tools/index.js";
13
+ import type { RevalidateKey, SideEffects, StateScope, Tool, ToolCall, ToolCtx, ToolErrorCode, ToolResult } from "./tools/types.js";
14
+ import { loadConfig } from "../config/store.js";
15
+ import {
16
+ egressAudit,
17
+ evaluateEgressCall,
18
+ evaluateEgressOutput,
19
+ formatEgressDenial,
20
+ normalizeEgressConfig,
21
+ } from "./policy/egress.js";
22
+
23
+ const STATE_SCOPES = new Set<StateScope>(["call", "turn", "session"]);
24
+ const REVALIDATE_KEYS = new Set<RevalidateKey>(["trust", "sandboxRoots", "schema", "modelCaps"]);
25
+ const TOOL_ERROR_CODES = new Set<ToolErrorCode>([
26
+ "ENOENT", "EACCES", "EEXIST", "ENOTDIR", "EISDIR", "EXDEV", "INVALID_ARGS",
27
+ "SANDBOX_DENY", "APPROVAL_DENY", "EGRESS_DENY", "NOT_FOUND", "ABORTED", "UNKNOWN",
28
+ ]);
29
+
30
+ function errorCode(error: any): ToolErrorCode {
31
+ const code = String(error?.code ?? "");
32
+ if (TOOL_ERROR_CODES.has(code as ToolErrorCode))
33
+ return code as ToolErrorCode;
34
+ const message = String(error?.message ?? error ?? "");
35
+ if (/outside sandbox/i.test(message))
36
+ return "SANDBOX_DENY";
37
+ if (/not trusted/i.test(message))
38
+ return "APPROVAL_DENY";
39
+ if (/aborted/i.test(message))
40
+ return "ABORTED";
41
+ return "UNKNOWN";
42
+ }
43
+
44
+ function unique(values: string[] | undefined): string[] {
45
+ return Array.from(new Set((values ?? []).filter(Boolean)));
46
+ }
47
+
48
+ function normalizeSideEffects(sideEffects: SideEffects | undefined): SideEffects | undefined {
49
+ if (!sideEffects)
50
+ return undefined;
51
+ const touched = unique(sideEffects.touched);
52
+ const created = unique(sideEffects.created);
53
+ const removed = unique(sideEffects.removed);
54
+ if (touched.length === 0 && created.length === 0 && removed.length === 0)
55
+ return undefined;
56
+ return {
57
+ ...(touched.length > 0 ? { touched } : {}),
58
+ ...(created.length > 0 ? { created } : {}),
59
+ ...(removed.length > 0 ? { removed } : {}),
60
+ };
61
+ }
62
+
63
+ export function assertToolContract(tools: Tool[] = TOOLS): void {
64
+ const names = new Set<string>();
65
+ for (const tool of tools) {
66
+ if (!tool.name || typeof tool.name !== "string")
67
+ throw new Error("tool contract: every tool needs a name");
68
+ if (names.has(tool.name))
69
+ throw new Error(`tool contract: duplicate tool name ${tool.name}`);
70
+ names.add(tool.name);
71
+ if (!STATE_SCOPES.has(tool.stateScope))
72
+ throw new Error(`tool contract: ${tool.name} has an invalid stateScope`);
73
+ for (const key of tool.revalidate ?? []) {
74
+ if (!REVALIDATE_KEYS.has(key))
75
+ throw new Error(`tool contract: ${tool.name} has an invalid revalidate key ${String(key)}`);
76
+ }
77
+ if (tool.requiresTrust && !(tool.revalidate ?? []).includes("trust")) {
78
+ throw new Error(`tool contract: ${tool.name} requiresTrust but does not revalidate "trust"`);
79
+ }
80
+ }
81
+ }
82
+
83
+ export function toolCatalogHash(tools: Tool[] = TOOLS): string {
84
+ const catalog = tools
85
+ .map((tool) => ({
86
+ name: tool.name,
87
+ trust: Boolean(tool.requiresTrust),
88
+ scope: tool.stateScope,
89
+ revalidate: [...(tool.revalidate ?? [])].sort(),
90
+ }))
91
+ .sort((a, b) => a.name.localeCompare(b.name));
92
+ return createHash("sha1").update(JSON.stringify(catalog)).digest("hex").slice(0, 8);
93
+ }
94
+
95
+ export class ToolRegistry {
96
+ private readonly tools = new Map<string, Tool>();
97
+ private readonly catalogHash: string;
98
+ constructor(tools: Tool[] = TOOLS) {
99
+ assertToolContract(tools);
100
+ for (const tool of tools)
101
+ this.tools.set(tool.name, tool);
102
+ this.catalogHash = toolCatalogHash(tools);
103
+ }
104
+ get(name: string): Tool | undefined {
105
+ return this.tools.get(name);
106
+ }
107
+ list(): Tool[] {
108
+ return Array.from(this.tools.values());
109
+ }
110
+ approve(call: ToolCall, ctx: ToolCtx): ApprovalDecision {
111
+ const tool = this.get(call.tool);
112
+ if (!tool)
113
+ return { kind: "deny", reason: `unknown tool: ${String(call.tool)}` };
114
+ if (tool.name === "shell") {
115
+ const verdict = evaluateShellCommand(call.command);
116
+ if (verdict.action === "hard_block")
117
+ return { kind: "deny", reason: verdict.output, code: verdict.code, target: verdict.target };
118
+ if (verdict.action === "soft_gate") {
119
+ const key = approvalKey(tool.name, call, ctx.cwd);
120
+ if (isPreApproved(key, this.catalogHash))
121
+ return { kind: "allow", scope: "once" };
122
+ return { kind: "pending", request: shellApprovalRequest(call, verdict) };
123
+ }
124
+ if (!verdict.allow)
125
+ return { kind: "deny", reason: verdict.output };
126
+ }
127
+ if ((tool.revalidate ?? []).includes("trust") && !isTrusted(ctx.cwd)) {
128
+ return {
129
+ kind: "deny",
130
+ reason: `project is not trusted: ${ctx.cwd}. Run \`fastcode trust ${ctx.cwd}\` or accept the trust prompt.`,
131
+ };
132
+ }
133
+ return { kind: "allow", scope: "once" };
134
+ }
135
+ async execute(call: ToolCall, ctx: ToolCtx): Promise<ToolResult> {
136
+ const tool = this.get(call.tool);
137
+ if (!tool)
138
+ return { ok: false, output: `unknown tool: ${String(call.tool)}`, code: "INVALID_ARGS" };
139
+ const toolCtx: ToolCtx = { ...ctx, catalogHash: this.catalogHash };
140
+ const prepared = tool.prepare(call, toolCtx);
141
+ if (prepared.ok === false)
142
+ return { ok: false, output: prepared.output, code: prepared.code ?? "INVALID_ARGS", hint: prepared.hint };
143
+ const egressConfig = normalizeEgressConfig(loadConfig().egress);
144
+ const preEgress = evaluateEgressCall(call, ctx.cwd, egressConfig);
145
+ if (preEgress.action === "deny") {
146
+ const egressDecision = preEgress.decision!;
147
+ return {
148
+ ok: false,
149
+ output: formatEgressDenial(egressDecision),
150
+ code: "EGRESS_DENY",
151
+ hint: "Egress policy blocked this call before execution; no approval request was created.",
152
+ egress: egressAudit(egressDecision),
153
+ };
154
+ }
155
+ const preAudit = preEgress.decision ? egressAudit(preEgress.decision) : undefined;
156
+ const decision = this.approve(call, toolCtx);
157
+ if (decision.kind === "deny") {
158
+ const output = decision.reason ?? "denied by approval policy";
159
+ if (tool.name === "shell" && (decision.code === "sensitive_file_egress" || decision.code === "credential_read")) {
160
+ return {
161
+ ok: false,
162
+ output,
163
+ code: "EGRESS_DENY",
164
+ egress: {
165
+ outlet: "shell.command",
166
+ ...(decision.target ? { target: decision.target } : {}),
167
+ rule: decision.code,
168
+ mode: "block",
169
+ wouldBlock: true,
170
+ code: "EGRESS_DENY",
171
+ reason: decision.code,
172
+ },
173
+ };
174
+ }
175
+ return { ok: false, output, code: "APPROVAL_DENY" };
176
+ }
177
+ if (decision.kind === "pending") {
178
+ if (!ctx.requestApproval)
179
+ return { ok: false, output: "approval required, but no approval resolver is registered (fail closed)", code: "APPROVAL_DENY" };
180
+ let resolved: ApprovalDecision | undefined;
181
+ try {
182
+ resolved = await ctx.requestApproval(decision.request);
183
+ }
184
+ catch (err: any) {
185
+ return { ok: false, output: `approval failed (fail closed): ${err?.message ?? String(err)}`, code: "APPROVAL_DENY" };
186
+ }
187
+ if (!resolved || resolved.kind === "pending")
188
+ return { ok: false, output: "approval unresolved; nothing was executed (fail closed)", code: "APPROVAL_DENY" };
189
+ if (resolved.kind !== "allow") {
190
+ const reason = resolved.kind === "deny" ? resolved.reason : resolved.kind === "feedback" ? resolved.text : `decision ${resolved.kind} requires a later batch`;
191
+ return { ok: false, output: `approval denied: ${reason ?? "no reason"}`, code: "APPROVAL_DENY" };
192
+ }
193
+ rememberApproval(approvalKey(tool.name, call, ctx.cwd), this.catalogHash, resolved.scope);
194
+ }
195
+ try {
196
+ let result: ToolResult = await tool.execute(call, toolCtx);
197
+ result = {
198
+ ...result,
199
+ sideEffects: normalizeSideEffects(result.sideEffects),
200
+ ...(preAudit && !result.egress ? { egress: preAudit } : {}),
201
+ };
202
+ const postEgress = evaluateEgressOutput(call.tool, result.output, egressConfig);
203
+ if (postEgress.action === "deny") {
204
+ const egressDecision = postEgress.decision!;
205
+ return {
206
+ ok: false,
207
+ output: formatEgressDenial(egressDecision),
208
+ code: "EGRESS_DENY",
209
+ hint: "Egress policy blocked this tool output before it reached the model.",
210
+ sideEffects: result.sideEffects,
211
+ egress: egressAudit(egressDecision),
212
+ };
213
+ }
214
+ if (postEgress.decision)
215
+ result = { ...result, egress: egressAudit(postEgress.decision) };
216
+ return result;
217
+ }
218
+ catch (err: any) {
219
+ const output = err?.message ?? String(err);
220
+ const postEgress = evaluateEgressOutput(call.tool, output, egressConfig);
221
+ if (postEgress.action === "deny") {
222
+ const egressDecision = postEgress.decision!;
223
+ return {
224
+ ok: false,
225
+ output: formatEgressDenial(egressDecision),
226
+ code: "EGRESS_DENY",
227
+ hint: "Egress policy blocked this error output before it reached the model.",
228
+ egress: egressAudit(egressDecision),
229
+ };
230
+ }
231
+ return {
232
+ ok: false,
233
+ output,
234
+ code: errorCode(err),
235
+ ...(preAudit ? { egress: preAudit } : {}),
236
+ ...(postEgress.decision ? { egress: egressAudit(postEgress.decision) } : {}),
237
+ };
238
+ }
239
+ }
240
+ }
241
+
242
+ function shellApprovalRequest(call: ToolCall, verdict: { code: string; target?: string; reason?: string; output?: string }): ApprovalRequest {
243
+ const options = DEFAULT_ALLOW_OPTIONS.map((option) => ({ ...option })) as ApprovalOption[];
244
+ const denyIndex = options.findIndex((option) => option.kind === "deny");
245
+ const target = verdict.target ?? "";
246
+ const prompt = `Shell command needs approval: ${verdict.code}${target ? ` (${target})` : ""}`;
247
+ return {
248
+ id: approvalRequestId(),
249
+ tool: "shell",
250
+ args: call,
251
+ reason: "destructive",
252
+ options,
253
+ tier: 2,
254
+ prompt,
255
+ source: "system",
256
+ defaultIndex: denyIndex >= 0 ? denyIndex : 0,
257
+ timeoutMs: 60_000,
258
+ };
259
+ }
260
+
261
+ export const registry = new ToolRegistry();
262
+
263
+ export async function runAgentTool(call: ToolCall, cwd = process.cwd(), options: { signal?: AbortSignal; turn?: number; requestApproval?: import("./approval.js").ApprovalResolver } = {}): Promise<ToolResult> {
264
+ return registry.execute(call, {
265
+ cwd,
266
+ signal: options.signal,
267
+ turn: options.turn,
268
+ requestApproval: options.requestApproval,
269
+ });
270
+ }