@pentoshi/clai 2.0.28 → 2.0.29

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 (105) hide show
  1. package/dist/agent/confirm-port.d.ts +30 -0
  2. package/dist/agent/confirm-port.js +89 -0
  3. package/dist/agent/confirm-port.js.map +1 -0
  4. package/dist/agent/plan-tool.d.ts +26 -0
  5. package/dist/agent/plan-tool.js +188 -0
  6. package/dist/agent/plan-tool.js.map +1 -0
  7. package/dist/agent/runner.d.ts +6 -174
  8. package/dist/agent/runner.js +25 -1606
  9. package/dist/agent/runner.js.map +1 -1
  10. package/dist/agent/session-policy.d.ts +19 -0
  11. package/dist/agent/session-policy.js +40 -0
  12. package/dist/agent/session-policy.js.map +1 -0
  13. package/dist/agent/stop-summary.d.ts +8 -0
  14. package/dist/agent/stop-summary.js +53 -0
  15. package/dist/agent/stop-summary.js.map +1 -0
  16. package/dist/agent/tool-call-parser.d.ts +155 -0
  17. package/dist/agent/tool-call-parser.js +1107 -0
  18. package/dist/agent/tool-call-parser.js.map +1 -0
  19. package/dist/agent/tool-output-formatting.d.ts +7 -0
  20. package/dist/agent/tool-output-formatting.js +127 -0
  21. package/dist/agent/tool-output-formatting.js.map +1 -0
  22. package/dist/commands/update.js +1 -1
  23. package/dist/prompts/index.d.ts +2 -2
  24. package/dist/prompts/index.js +103 -59
  25. package/dist/prompts/index.js.map +1 -1
  26. package/dist/repl/prompt-line.d.ts +19 -0
  27. package/dist/repl/prompt-line.js +555 -0
  28. package/dist/repl/prompt-line.js.map +1 -0
  29. package/dist/repl/slash-commands.d.ts +24 -0
  30. package/dist/repl/slash-commands.js +317 -0
  31. package/dist/repl/slash-commands.js.map +1 -0
  32. package/dist/repl.d.ts +2 -25
  33. package/dist/repl.js +41 -898
  34. package/dist/repl.js.map +1 -1
  35. package/dist/safety/classifier.js +1 -1
  36. package/dist/safety/classifier.js.map +1 -1
  37. package/dist/store/keys.js +0 -4
  38. package/dist/store/keys.js.map +1 -1
  39. package/dist/store/plan.js +1 -1
  40. package/dist/store/plan.js.map +1 -1
  41. package/dist/tools/nmap-runner.d.ts +19 -0
  42. package/dist/tools/nmap-runner.js +177 -0
  43. package/dist/tools/nmap-runner.js.map +1 -0
  44. package/dist/tools/package-binary.d.ts +1 -0
  45. package/dist/tools/package-binary.js +30 -0
  46. package/dist/tools/package-binary.js.map +1 -0
  47. package/dist/tools/pdf.js +2 -2
  48. package/dist/tools/pdf.js.map +1 -1
  49. package/dist/tools/registry.d.ts +2 -11
  50. package/dist/tools/registry.js +3 -203
  51. package/dist/tools/registry.js.map +1 -1
  52. package/dist/tools/tool-types.d.ts +12 -0
  53. package/dist/tools/tool-types.js +2 -0
  54. package/dist/tools/tool-types.js.map +1 -0
  55. package/dist/tools/web/audit.d.ts +7 -29
  56. package/dist/tools/web/audit.js +7 -29
  57. package/dist/tools/web/audit.js.map +1 -1
  58. package/dist/tools/web/budget.d.ts +5 -28
  59. package/dist/tools/web/budget.js +5 -28
  60. package/dist/tools/web/budget.js.map +1 -1
  61. package/dist/tools/web/capture.d.ts +8 -35
  62. package/dist/tools/web/capture.js +8 -35
  63. package/dist/tools/web/capture.js.map +1 -1
  64. package/dist/tools/web/fetch-core.d.ts +6 -26
  65. package/dist/tools/web/fetch-core.js +6 -26
  66. package/dist/tools/web/fetch-core.js.map +1 -1
  67. package/dist/tools/web/readable.d.ts +7 -13
  68. package/dist/tools/web/readable.js +7 -13
  69. package/dist/tools/web/readable.js.map +1 -1
  70. package/dist/tools/web/search.d.ts +7 -20
  71. package/dist/tools/web/search.js +7 -20
  72. package/dist/tools/web/search.js.map +1 -1
  73. package/dist/tools/web/ssrf-guard.d.ts +8 -27
  74. package/dist/tools/web/ssrf-guard.js +8 -27
  75. package/dist/tools/web/ssrf-guard.js.map +1 -1
  76. package/dist/tui/App.js +19 -88
  77. package/dist/tui/App.js.map +1 -1
  78. package/dist/tui/key-intent.d.ts +36 -0
  79. package/dist/tui/key-intent.js +54 -0
  80. package/dist/tui/key-intent.js.map +1 -0
  81. package/dist/tui/render-lines.js +125 -37
  82. package/dist/tui/render-lines.js.map +1 -1
  83. package/dist/tui/state.js +2 -2
  84. package/dist/tui/state.js.map +1 -1
  85. package/dist/tui/text-format.d.ts +11 -0
  86. package/dist/tui/text-format.js +85 -0
  87. package/dist/tui/text-format.js.map +1 -0
  88. package/dist/ui/ansi-box.d.ts +7 -0
  89. package/dist/ui/ansi-box.js +18 -0
  90. package/dist/ui/ansi-box.js.map +1 -0
  91. package/dist/ui/banner.d.ts +0 -9
  92. package/dist/ui/banner.js +1 -43
  93. package/dist/ui/banner.js.map +1 -1
  94. package/dist/ui/intro-card.d.ts +10 -0
  95. package/dist/ui/intro-card.js +39 -0
  96. package/dist/ui/intro-card.js.map +1 -0
  97. package/dist/ui/markdown.js +1 -1
  98. package/dist/ui/markdown.js.map +1 -1
  99. package/dist/ui/output-pane.js.map +1 -1
  100. package/dist/ui/spinner.js +6 -17
  101. package/dist/ui/spinner.js.map +1 -1
  102. package/dist/ui/wordmark.d.ts +7 -0
  103. package/dist/ui/wordmark.js +42 -0
  104. package/dist/ui/wordmark.js.map +1 -0
  105. package/package.json +1 -1
@@ -0,0 +1,1107 @@
1
+ import { currentDateTimeContext } from "../prompts/index.js";
2
+ import { isCompactionMemoryMessage } from "./context-manager.js";
3
+ import { safeCwd } from "../os/cwd.js";
4
+ export function preprocessJson(raw) {
5
+ let inString = false;
6
+ let escaped = false;
7
+ let result = "";
8
+ for (let i = 0; i < raw.length; i++) {
9
+ const char = raw[i];
10
+ if (char === '"' && !escaped) {
11
+ inString = !inString;
12
+ result += char;
13
+ }
14
+ else if (inString) {
15
+ if (char === "\n") {
16
+ result += "\\n";
17
+ }
18
+ else if (char === "\r") {
19
+ result += "\\r";
20
+ }
21
+ else if (char === "\t") {
22
+ result += "\\t";
23
+ }
24
+ else {
25
+ result += char;
26
+ }
27
+ }
28
+ else {
29
+ if (char === "," && i + 1 < raw.length) {
30
+ let nextNonWs = "";
31
+ for (let j = i + 1; j < raw.length; j++) {
32
+ if (!/\s/.test(raw[j])) {
33
+ nextNonWs = raw[j];
34
+ break;
35
+ }
36
+ }
37
+ if (nextNonWs === "}" || nextNonWs === "]") {
38
+ continue;
39
+ }
40
+ }
41
+ result += char;
42
+ }
43
+ if (char === "\\" && inString) {
44
+ escaped = !escaped;
45
+ }
46
+ else {
47
+ escaped = false;
48
+ }
49
+ }
50
+ return result;
51
+ }
52
+ /**
53
+ * Last-resort lenient repair for tool-call JSON that strict JSON.parse
54
+ * rejected. Models frequently emit "almost JSON": smart/curly quotes from a
55
+ * copy-paste, Python-style True/False/None literals, or an object that is
56
+ * wholly single-quoted. We only apply these transforms when a strict parse
57
+ * has already failed, so well-formed JSON is never touched.
58
+ */
59
+ function lenientJsonParse(text) {
60
+ const candidates = [];
61
+ // 1. Normalize unicode/smart quotes to ASCII quotes.
62
+ const deSmart = text
63
+ .replace(/[\u201C\u201D\u201E\u2033]/g, '"')
64
+ .replace(/[\u2018\u2019\u201A\u2032]/g, "'");
65
+ candidates.push(deSmart);
66
+ // 2. Python/JS literals → JSON literals (outside of double-quoted strings).
67
+ candidates.push(replaceOutsideStrings(deSmart));
68
+ // 3. Single-quoted object → double-quoted (only when there are no double
69
+ // quotes already, so we don't corrupt strings that contain apostrophes).
70
+ if (!deSmart.includes('"') && deSmart.includes("'")) {
71
+ candidates.push(deSmart.replace(/'/g, '"'));
72
+ }
73
+ for (const candidate of candidates) {
74
+ try {
75
+ return JSON.parse(preprocessJson(candidate).trim());
76
+ }
77
+ catch {
78
+ // try the next repair
79
+ }
80
+ }
81
+ return undefined;
82
+ }
83
+ /** Replace bare Python/JS literals (True/False/None/NaN) with JSON equivalents,
84
+ * skipping anything inside a double-quoted string. */
85
+ function replaceOutsideStrings(text) {
86
+ let inString = false;
87
+ let escaped = false;
88
+ let out = "";
89
+ let i = 0;
90
+ while (i < text.length) {
91
+ const ch = text[i];
92
+ if (inString) {
93
+ out += ch;
94
+ if (escaped)
95
+ escaped = false;
96
+ else if (ch === "\\")
97
+ escaped = true;
98
+ else if (ch === '"')
99
+ inString = false;
100
+ i += 1;
101
+ continue;
102
+ }
103
+ if (ch === '"') {
104
+ inString = true;
105
+ out += ch;
106
+ i += 1;
107
+ continue;
108
+ }
109
+ const rest = text.slice(i);
110
+ const m = /^(True|False|None|NaN|undefined)\b/.exec(rest);
111
+ if (m) {
112
+ const word = m[1];
113
+ out +=
114
+ word === "True"
115
+ ? "true"
116
+ : word === "False"
117
+ ? "false"
118
+ : word === "NaN"
119
+ ? "0"
120
+ : "null"; // None / undefined
121
+ i += word.length;
122
+ continue;
123
+ }
124
+ out += ch;
125
+ i += 1;
126
+ }
127
+ return out;
128
+ }
129
+ function tryParseCall(raw) {
130
+ let parsed;
131
+ try {
132
+ parsed = JSON.parse(preprocessJson(raw).trim());
133
+ }
134
+ catch {
135
+ // Strict parse failed — try lenient repairs before giving up so a model
136
+ // that emits smart quotes / single quotes / Python literals still works.
137
+ const repaired = lenientJsonParse(raw.trim());
138
+ if (repaired && typeof repaired === "object" && !Array.isArray(repaired)) {
139
+ parsed = repaired;
140
+ }
141
+ }
142
+ if (!parsed)
143
+ return undefined;
144
+ const anyParsed = parsed;
145
+ // Accept name under several keys models commonly use.
146
+ const nameRaw = typeof parsed.name === "string"
147
+ ? parsed.name
148
+ : typeof anyParsed.tool_name === "string"
149
+ ? anyParsed.tool_name
150
+ : undefined;
151
+ if (typeof nameRaw === "string" && nameRaw.length > 0) {
152
+ // Strip a leading "functions." namespace some models add.
153
+ const name = nameRaw.replace(/^functions\./, "");
154
+ // Many OpenAI/Hermes/Qwen-trained models emit {"name","arguments"}
155
+ // (or "parameters"/"input") instead of {"name","args"} — accept any.
156
+ const argsSrc = pickObject(parsed.args) ??
157
+ pickObject(parsed.arguments) ??
158
+ pickObject(anyParsed.parameters) ??
159
+ pickObject(anyParsed.input);
160
+ if (argsSrc) {
161
+ return { name, args: argsSrc };
162
+ }
163
+ // Allow an empty args object explicitly written as {} or null (common for
164
+ // sysinfo), but do NOT invent args for objects that merely happen to
165
+ // contain a "name" key (e.g. {"name":"shell.exec"} with no command).
166
+ if (parsed.args === null || parsed.arguments === null) {
167
+ return { name, args: {} };
168
+ }
169
+ // Flattened form: the args are emitted as SIBLINGS of `name` rather than
170
+ // nested (e.g. {"name":"web.fetch","url":"…","responseMode":"raw"}). Treat
171
+ // the non-reserved keys as args, but only when at least one is a known
172
+ // tool-arg key so plain data objects carrying a `name` are not misread.
173
+ const flat = {};
174
+ for (const key of Object.keys(anyParsed)) {
175
+ if (!FLATTENED_RESERVED_KEYS.has(key))
176
+ flat[key] = anyParsed[key];
177
+ }
178
+ const flatKeys = Object.keys(flat);
179
+ if (flatKeys.length > 0 && flatKeys.some((k) => TOOL_ARG_KEYS.has(k))) {
180
+ return { name, args: flat };
181
+ }
182
+ }
183
+ return undefined;
184
+ }
185
+ // Keys that name the tool or wrap its arguments — excluded when recovering a
186
+ // flattened tool call whose args sit alongside `name`.
187
+ const FLATTENED_RESERVED_KEYS = new Set([
188
+ "name",
189
+ "tool_name",
190
+ "args",
191
+ "arguments",
192
+ "parameters",
193
+ "input",
194
+ ]);
195
+ function pickObject(value) {
196
+ return value && typeof value === "object" && !Array.isArray(value)
197
+ ? value
198
+ : undefined;
199
+ }
200
+ // Kimi K2 / Moonshot models on NVIDIA NIM emit tool calls using a
201
+ // sentinel-token format that looks like:
202
+ // <|tool_calls_section_begin|>
203
+ // <|tool_call_begin|>functions.shell.exec:0<|tool_call_argument_begin|>
204
+ // {"command":"ls"}
205
+ // <|tool_call_end|>
206
+ // <|tool_calls_section_end|>
207
+ // The `functions.` prefix is optional, the trailing `:N` index is optional,
208
+ // and the surrounding section markers may be absent on truncated streams.
209
+ const KIMI_TOOL_CALL_RE = /<\|tool_call_begin\|>\s*(?:functions\.)?([A-Za-z][\w.]*?)(?::\d+)?\s*<\|tool_call_argument_begin\|>\s*(\{[\s\S]*?\})\s*<\|tool_call_end\|>/i;
210
+ function parseKimiToolCall(text) {
211
+ const match = text.match(KIMI_TOOL_CALL_RE);
212
+ if (!match)
213
+ return undefined;
214
+ const name = match[1];
215
+ return tryParseCall(JSON.stringify({ name, args: tryJson(match[2]) ?? {} }));
216
+ }
217
+ // Recognized XML-ish tool-call wrappers some models emit (with or without a
218
+ // matching close tag). Used so we can recover a call even when the model
219
+ // forgot the closing tag, while plain prose never matches.
220
+ const XML_BLOCK_OPENERS = /<tool_call>|<function_calls>|<invoke>|<ant:invoke>/i;
221
+ /**
222
+ * Scan `text` starting at the index of the first `{`, returning the balanced
223
+ * JSON substring (respecting strings) or undefined if braces never balance.
224
+ * Lets us recover a tool-call JSON object the model emitted without a closing
225
+ * wrapper tag, where a non-greedy regex would stop at the first inner brace.
226
+ */
227
+ function extractBalancedJson(text) {
228
+ const start = text.indexOf("{");
229
+ if (start < 0)
230
+ return undefined;
231
+ let depth = 0;
232
+ let inString = false;
233
+ let escaped = false;
234
+ for (let i = start; i < text.length; i += 1) {
235
+ const ch = text[i];
236
+ if (escaped) {
237
+ escaped = false;
238
+ continue;
239
+ }
240
+ if (ch === "\\") {
241
+ escaped = true;
242
+ continue;
243
+ }
244
+ if (ch === '"') {
245
+ inString = !inString;
246
+ continue;
247
+ }
248
+ if (inString)
249
+ continue;
250
+ if (ch === "{")
251
+ depth += 1;
252
+ else if (ch === "}") {
253
+ depth -= 1;
254
+ if (depth === 0)
255
+ return text.slice(start, i + 1);
256
+ }
257
+ }
258
+ return undefined;
259
+ }
260
+ function parseXmlToolCall(text) {
261
+ // Pattern 1 (name + args/arguments/parameters JSON):
262
+ // <tool_call>
263
+ // <name>tool.name</name>
264
+ // <args>{...}</args> (or <arguments> / <parameters>)
265
+ // <tool_call>
266
+ const xmlNameArgs = text.match(/<tool_call>[\s\S]*?<name>\s*([\w.]+?)\s*<\/name>\s*<(?:args|arguments|parameters)>\s*(\{[\s\S]*?\})\s*<\/(?:args|arguments|parameters)>[\s\S]*?<\/tool_call>/i);
267
+ if (xmlNameArgs?.[1] && xmlNameArgs?.[2]) {
268
+ try {
269
+ const args = JSON.parse(preprocessJson(xmlNameArgs[2]));
270
+ return {
271
+ name: xmlNameArgs[1],
272
+ args: args,
273
+ };
274
+ }
275
+ catch { }
276
+ }
277
+ // Pattern 1b (MiMo alternative):
278
+ // <tool_call>
279
+ // <tool_name>tool.name</tool_name>
280
+ // <parameters>{...}</parameters> (or <arguments> / <args>)
281
+ // <tool_call>
282
+ const xmlToolNameParams = text.match(/<tool_call>[\s\S]*?<tool_name>\s*([\w.]+?)\s*<\/tool_name>\s*<(?:parameters|arguments|args)>\s*(\{[\s\S]*?\})\s*<\/(?:parameters|arguments|args)>[\s\S]*?<\/tool_call>/i);
283
+ if (xmlToolNameParams?.[1] && xmlToolNameParams?.[2]) {
284
+ try {
285
+ const args = JSON.parse(preprocessJson(xmlToolNameParams[2]));
286
+ return {
287
+ name: xmlToolNameParams[1],
288
+ args: args,
289
+ };
290
+ }
291
+ catch { }
292
+ }
293
+ // Pattern 1c (MiMo function/parameter format), with or WITHOUT a
294
+ // surrounding <tool_call> wrapper (some models emit the bare function block):
295
+ // <function=tool.name>
296
+ // <parameter=name>value</parameter>
297
+ // </function>
298
+ const xmlFunctionBlock = text.match(/<function=([\w.]+?)>([\s\S]*?)<\/function>/i);
299
+ if (xmlFunctionBlock?.[1] && xmlFunctionBlock?.[2]) {
300
+ const name = xmlFunctionBlock[1];
301
+ const inner = xmlFunctionBlock[2];
302
+ const args = {};
303
+ const paramRegex = /<parameter=([\w.]+?)>([\s\S]*?)<\/parameter>/gi;
304
+ let paramMatch;
305
+ while ((paramMatch = paramRegex.exec(inner)) !== null) {
306
+ const paramName = paramMatch[1];
307
+ const paramValueStr = paramMatch[2].trim();
308
+ let paramValue = paramValueStr;
309
+ try {
310
+ if (/^(?:\[|\{|true|false|null|\d+(\.\d+)?$)/i.test(paramValueStr)) {
311
+ paramValue = JSON.parse(preprocessJson(paramValueStr));
312
+ }
313
+ }
314
+ catch { }
315
+ args[paramName] = paramValue;
316
+ }
317
+ return { name, args };
318
+ }
319
+ // Pattern 2: JSON object inside a recognized wrapper (closed). The wrapper
320
+ // may be the tool_call sentinel, <function_calls>, or <invoke>/<ant:invoke>.
321
+ // Backtracking off the closing tag handles nested {} in the args.
322
+ const wrappers = [
323
+ "<tool_call>",
324
+ "<function_calls>",
325
+ "<invoke>",
326
+ "<ant:invoke>",
327
+ ];
328
+ for (const open of wrappers) {
329
+ const close = open === "<function_calls>"
330
+ ? "</function_calls>"
331
+ : open === "<invoke>"
332
+ ? "</invoke>"
333
+ : open === "<ant:invoke>"
334
+ ? "</ant:invoke>"
335
+ : "</tool_call>";
336
+ const re = new RegExp(open.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +
337
+ "[\\s\\S]*?(?:<tool>)?\\s*(\\{[\\s\\S]*?\\})\\s*" +
338
+ close.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
339
+ const match = text.match(re);
340
+ if (match?.[1]) {
341
+ const call = tryParseCall(match[1]);
342
+ if (call)
343
+ return call;
344
+ }
345
+ }
346
+ // Pattern 3: a recognized opener is present but there is no matching close
347
+ // (model forgot it, or the stream was cut). Use a balanced brace scan from
348
+ // the first `{` after the opener so nested args survive, then try to parse.
349
+ const openerMatch = XML_BLOCK_OPENERS.exec(text);
350
+ if (openerMatch) {
351
+ const after = text.slice(openerMatch.index + openerMatch[0].length);
352
+ const json = extractBalancedJson(after);
353
+ if (json) {
354
+ const call = tryParseCall(json);
355
+ if (call)
356
+ return call;
357
+ }
358
+ // Also try the <name>...</name><arguments>{...}</arguments> tag shape
359
+ // without a closing wrapper (Hermes/Qwen often omit it).
360
+ const tagShape = after.match(/<name>\s*([\w.]+?)\s*<\/name>\s*<(?:args|arguments|parameters)>\s*(\{[\s\S]*?\})\s*<\/(?:args|arguments|parameters)>/i);
361
+ if (tagShape?.[1] && tagShape?.[2]) {
362
+ try {
363
+ const args = JSON.parse(preprocessJson(tagShape[2]));
364
+ return { name: tagShape[1], args: args };
365
+ }
366
+ catch { }
367
+ }
368
+ }
369
+ return undefined;
370
+ }
371
+ function tryJson(raw) {
372
+ try {
373
+ const preprocessed = preprocessJson(raw);
374
+ const parsed = JSON.parse(preprocessed);
375
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
376
+ return parsed;
377
+ }
378
+ }
379
+ catch {
380
+ // ignore
381
+ }
382
+ return undefined;
383
+ }
384
+ /** Strip any leftover Kimi/Moonshot sentinel tokens from final answers
385
+ * so a model that mixes prose and tool-call markers never bleeds raw
386
+ * `<|tool_call_begin|>` strings to the terminal. */
387
+ export function stripSentinelTokens(text) {
388
+ return text
389
+ .replace(/<\|tool_calls_section_begin\|>[\s\S]*?<\|tool_calls_section_end\|>/gi, "")
390
+ .replace(/<\|tool_call_begin\|>[\s\S]*?<\|tool_call_end\|>/gi, "")
391
+ .replace(/<\|tool_calls?(?:_section)?_(?:begin|end)\|>/gi, "")
392
+ .replace(/<\|tool_call_argument_begin\|>/gi, "")
393
+ .replace(/<\|tool_[a-z_]*\|>/gi, "")
394
+ .trim();
395
+ }
396
+ export function parseToolCall(text, options = {}) {
397
+ // 1. ```tool ... ``` (standard format)
398
+ const fenced = text.match(/```tool\s*\n?([\s\S]*?)```/i);
399
+ if (fenced?.[1]) {
400
+ const call = tryParseCall(fenced[1]);
401
+ if (call)
402
+ return call;
403
+ }
404
+ // 2. <tool_call>...</tool_call> (XML formats)
405
+ const xmlCall = parseXmlToolCall(text);
406
+ if (xmlCall)
407
+ return xmlCall;
408
+ // 3. Kimi/Moonshot sentinel format (used by kimi-k2 family on NIM).
409
+ const kimi = parseKimiToolCall(text);
410
+ if (kimi)
411
+ return kimi;
412
+ // In strict mode, stop here. Headings, generic fenced blocks, and trailing
413
+ // JSON are too easy to accidentally trigger when the model is showing a
414
+ // worked example.
415
+ if (options.strict)
416
+ return undefined;
417
+ // 4. ### tool / ## tool / # tool heading + JSON
418
+ const heading = text.match(/#{1,3}\s*tool\s*\n\s*(\{[\s\S]*\})/i);
419
+ if (heading?.[1]) {
420
+ const call = tryParseCall(heading[1]);
421
+ if (call)
422
+ return call;
423
+ }
424
+ // 5. **tool** heading + JSON
425
+ const bold = text.match(/\*\*tool\*\*\s*\n\s*(\{[\s\S]*\})/i);
426
+ if (bold?.[1]) {
427
+ const call = tryParseCall(bold[1]);
428
+ if (call)
429
+ return call;
430
+ }
431
+ // 6. Any fenced block (```json, ```, etc.) containing name+args
432
+ const anyFenced = text.match(/```\w*\s*\n?([\s\S]*?)```/);
433
+ if (anyFenced?.[1]) {
434
+ const call = tryParseCall(anyFenced[1]);
435
+ if (call)
436
+ return call;
437
+ }
438
+ // 7. Trailing JSON object with "name" and "args"
439
+ const trailingJson = text.match(/(\{"name"\s*:\s*"[^"]+"\s*,\s*"args"\s*:\s*\{[\s\S]*?\}\s*\})\s*$/);
440
+ if (trailingJson?.[1]) {
441
+ const call = tryParseCall(trailingJson[1]);
442
+ if (call)
443
+ return call;
444
+ }
445
+ return undefined;
446
+ }
447
+ // Argument keys that the built-in tools accept. Used to recognize when a
448
+ // model emitted a bare args object (e.g. {"path":"file.pdf"}) — intending a
449
+ // tool call but forgetting the {"name","args"} wrapper and the ```tool fence.
450
+ const TOOL_ARG_KEYS = new Set([
451
+ "command",
452
+ "path",
453
+ "paths",
454
+ "url",
455
+ "query",
456
+ "target",
457
+ "pattern",
458
+ "tool",
459
+ "tools",
460
+ "files",
461
+ "content",
462
+ "calls",
463
+ "record",
464
+ "ports",
465
+ "profile",
466
+ "id",
467
+ "lang",
468
+ "dpi",
469
+ "psm",
470
+ "recursive",
471
+ "oldText",
472
+ "newText",
473
+ "expectedReplacements",
474
+ "goal",
475
+ "tasks",
476
+ "taskId",
477
+ "state",
478
+ "method",
479
+ "body",
480
+ "headers",
481
+ "maxBytes",
482
+ "maxResults",
483
+ "cwd",
484
+ "name",
485
+ "concurrency",
486
+ // Extra optional keys that commonly ride along with a bare args object so
487
+ // it is still recognized (and its tool inferred) instead of leaking to the
488
+ // screen — e.g. shell.exec with {"command":"…","timeoutMs":300000}.
489
+ "timeoutMs",
490
+ "flags",
491
+ "iOwnThis",
492
+ "own",
493
+ "note",
494
+ "kind",
495
+ "detail",
496
+ "maxEntries",
497
+ "checkBinary",
498
+ "scanType",
499
+ "whois",
500
+ "dns",
501
+ "nmap",
502
+ "bytes",
503
+ "responseMode",
504
+ "includeHeaders",
505
+ "includeTls",
506
+ "includeTiming",
507
+ "includeRedirectChain",
508
+ "redactSensitive",
509
+ ]);
510
+ /**
511
+ * When a model emits a bare args object with no {"name", "args"} wrapper and
512
+ * no ```tool fence, infer which tool it MEANT from the argument keys so we
513
+ * can run it directly instead of nudging the model to re-emit (the user
514
+ * should not have to type "run"). Only unambiguous key signatures map to a
515
+ * tool; genuinely ambiguous shapes (a lone `path` could be fs.read / fs.list
516
+ * / pdf.read / image.ocr; a lone `target` could be whois / dns / scan) return
517
+ * undefined so the caller falls back to a re-emit nudge. Inferred calls still
518
+ * pass through the normal safety classifier + confirmation, so inference can
519
+ * never bypass a confirm/block gate.
520
+ */
521
+ export function inferToolFromArgs(obj) {
522
+ const has = (key) => Object.prototype.hasOwnProperty.call(obj, key);
523
+ if (has("command"))
524
+ return "shell.exec";
525
+ if (has("files"))
526
+ return "fs.writeMany";
527
+ if (has("calls"))
528
+ return "tool.batch";
529
+ if (has("oldText") || has("newText"))
530
+ return "fs.edit";
531
+ if (has("content") && has("path"))
532
+ return "fs.write";
533
+ if (has("pattern"))
534
+ return "fs.search";
535
+ if (has("query"))
536
+ return "web.search";
537
+ if (has("tools"))
538
+ return "tool.check";
539
+ if (has("goal") && has("tasks"))
540
+ return "plan.create";
541
+ if (has("taskId") || has("state"))
542
+ return "task.update";
543
+ if (has("tool"))
544
+ return "pkg.install";
545
+ if (has("record") && has("target"))
546
+ return "dns.lookup";
547
+ if (has("ports") && has("target"))
548
+ return "net.scan";
549
+ if (has("url")) {
550
+ // A url with an explicit method/body is a raw HTTP request (http.fetch);
551
+ // a lone url is a content read (web.fetch).
552
+ return has("method") || has("body") ? "http.fetch" : "web.fetch";
553
+ }
554
+ return undefined;
555
+ }
556
+ /**
557
+ * Strip a single wrapping ```json / ``` fence (if any) and return the inner
558
+ * text trimmed. Leaves un-fenced text unchanged.
559
+ */
560
+ function stripLoneFence(text) {
561
+ const fenced = text.trim().match(/^```[a-zA-Z]*\s*\n?([\s\S]*?)\n?```$/);
562
+ return (fenced?.[1] ?? text).trim();
563
+ }
564
+ /**
565
+ * Try to recover a bare-args tool call from a single candidate text snippet.
566
+ * Returns the recognized result or undefined if the text isn't a recoverable
567
+ * tool call. Used by both the whole-text path and the embedded-fence path.
568
+ */
569
+ function tryRecognizeBareArgs(inner) {
570
+ const trimmed = inner.trim();
571
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
572
+ return undefined;
573
+ let parsed;
574
+ try {
575
+ parsed = JSON.parse(trimmed);
576
+ }
577
+ catch {
578
+ return undefined;
579
+ }
580
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
581
+ return undefined;
582
+ }
583
+ const obj = parsed;
584
+ // Complete {name, args} call the earlier matchers didn't catch.
585
+ const direct = tryParseCall(trimmed);
586
+ if (direct)
587
+ return { call: direct };
588
+ // Bare args object: every key is a known tool-arg key.
589
+ const keys = Object.keys(obj);
590
+ if (keys.length === 0 || keys.length > 6)
591
+ return undefined;
592
+ const allKnown = keys.every((key) => TOOL_ARG_KEYS.has(key));
593
+ if (!allKnown)
594
+ return undefined;
595
+ const inferred = inferToolFromArgs(obj);
596
+ if (inferred) {
597
+ return { call: { name: inferred, args: obj } };
598
+ }
599
+ return { argsOnly: true };
600
+ }
601
+ /**
602
+ * When a model means to call a tool but emits ONLY a bare JSON object —
603
+ * either a proper {"name","args"} that the strict matchers missed, or a bare
604
+ * args object like {"path":"file.pdf"} with the wrapper/fence dropped — this
605
+ * recognizes it. Returns:
606
+ * - { call } when the object is a complete {name, args} tool call, or
607
+ * - { argsOnly: true } when it looks like a bare args object (so the caller
608
+ * can nudge the model to re-emit a properly named, fenced tool call).
609
+ * Returns undefined for anything that is plainly a normal prose/JSON answer.
610
+ *
611
+ * Also handles the case where a model emits prose followed by a non-`tool`
612
+ * fenced code block (e.g. ```web\n{"url":"..."}\n```) that contains a bare
613
+ * args object — the fence is scanned even when it's not the sole content.
614
+ */
615
+ export function recognizeBareToolJson(text) {
616
+ // Primary path: the whole (de-fenced) text is a bare JSON object
617
+ const inner = stripLoneFence(text);
618
+ const primary = tryRecognizeBareArgs(inner);
619
+ if (primary)
620
+ return primary;
621
+ // Secondary path: scan for any fenced block embedded in the text
622
+ // This catches models that prepend prose before emitting a bare-args fence,
623
+ // e.g. "Let me fetch it.\n\n```web\n{\"url\":\"https://...\"}\n```"
624
+ // We skip ```tool fences — those are handled by parseToolCall already.
625
+ const embeddedFenceRe = /```([a-zA-Z]*)\s*\n?([\s\S]*?)```/g;
626
+ let m;
627
+ while ((m = embeddedFenceRe.exec(text)) !== null) {
628
+ const lang = m[1] ?? "";
629
+ const body = (m[2] ?? "").trim();
630
+ // Skip ```tool blocks — parseToolCall owns those.
631
+ if (lang.toLowerCase() === "tool")
632
+ continue;
633
+ // Skip empty or multi-line JSON that spans more than a simple object.
634
+ if (!body.startsWith("{") || !body.endsWith("}"))
635
+ continue;
636
+ const result = tryRecognizeBareArgs(body);
637
+ if (result)
638
+ return result;
639
+ }
640
+ return undefined;
641
+ }
642
+ /**
643
+ * Detect an opened-but-unparseable tool call. This happens when the model's
644
+ * output is truncated by the token limit mid-JSON: we see the ```tool fence
645
+ * (or a bare {"name":"...","args" prefix) open, but parseToolCall returns
646
+ * undefined because the JSON never closed. Without this, the broken block
647
+ * leaks to the screen as a "final answer" and the requested action (e.g. a
648
+ * multi-file fs.writeMany scaffold) silently never runs.
649
+ */
650
+ export function looksLikeTruncatedToolCall(text) {
651
+ // An opened ```tool fence with no closing fence.
652
+ const openFence = /```tool\s*\n?/i.test(text);
653
+ const closeFence = /```tool[\s\S]*?```/i.test(text);
654
+ if (openFence && !closeFence)
655
+ return true;
656
+ // A tool-call JSON object that started but whose braces never balanced.
657
+ const jsonStart = text.search(/\{\s*"name"\s*:\s*"[A-Za-z][\w.]*"\s*,\s*"args"/);
658
+ if (jsonStart >= 0) {
659
+ const slice = text.slice(jsonStart);
660
+ let depth = 0;
661
+ let inString = false;
662
+ let escaped = false;
663
+ let balanced = false;
664
+ for (const ch of slice) {
665
+ if (escaped) {
666
+ escaped = false;
667
+ continue;
668
+ }
669
+ if (ch === "\\") {
670
+ escaped = true;
671
+ continue;
672
+ }
673
+ if (ch === '"')
674
+ inString = !inString;
675
+ if (inString)
676
+ continue;
677
+ if (ch === "{")
678
+ depth += 1;
679
+ else if (ch === "}") {
680
+ depth -= 1;
681
+ if (depth === 0) {
682
+ balanced = true;
683
+ break;
684
+ }
685
+ }
686
+ }
687
+ if (!balanced)
688
+ return true;
689
+ }
690
+ return false;
691
+ }
692
+ /**
693
+ * Count the number of ```tool fenced blocks in a message. Models sometimes
694
+ * emit MULTIPLE tool calls in one response (e.g. fs.writeMany + npm install +
695
+ * npm run dev). Only the FIRST is parsed and executed; the rest are silently
696
+ * dropped and leak to the screen as code fences, while the model believes it
697
+ * ran all of them — a major cause of "everything is done" fabrications. We
698
+ * detect this so the runner can run the first and explicitly tell the model
699
+ * the others did NOT run and must be re-sent one at a time.
700
+ */
701
+ export function countToolFences(text) {
702
+ const matches = text.match(/```tool\s*\n[\s\S]*?```/gi);
703
+ return matches ? matches.length : 0;
704
+ }
705
+ /**
706
+ * Parse every explicitly-delimited tool call in a message (```tool fences,
707
+ * <tool_call> XML, Kimi sentinel blocks), in document order, so the runner
708
+ * can execute a batch emitted in one turn instead of only the first call.
709
+ */
710
+ export function parseAllToolCalls(text) {
711
+ const found = [];
712
+ let m;
713
+ const fenceRe = /```tool\s*\n?([\s\S]*?)```/gi;
714
+ while ((m = fenceRe.exec(text)) !== null) {
715
+ const call = tryParseCall(m[1] ?? "");
716
+ if (call)
717
+ found.push({ index: m.index, call });
718
+ }
719
+ const xmlRe = /<tool_call>([\s\S]*?)<\/tool_call>/gi;
720
+ while ((m = xmlRe.exec(text)) !== null) {
721
+ const call = parseXmlToolCall(m[0]);
722
+ if (call)
723
+ found.push({ index: m.index, call });
724
+ }
725
+ const kimiRe = new RegExp(KIMI_TOOL_CALL_RE.source, "gi");
726
+ while ((m = kimiRe.exec(text)) !== null) {
727
+ const call = tryParseCall(JSON.stringify({ name: m[1], args: tryJson(m[2] ?? "{}") ?? {} }));
728
+ if (call)
729
+ found.push({ index: m.index, call });
730
+ }
731
+ // Bare <function=name>…</function> blocks (no <tool_call> wrapper) — some
732
+ // models emit one or several of these. Route each through parseXmlToolCall
733
+ // so the <parameter=…> args are decoded. Skip any that overlap a
734
+ // <tool_call> block already captured above (avoid double-counting).
735
+ const fnRe = /<function=[\w.]+?>[\s\S]*?<\/function>/gi;
736
+ while ((m = fnRe.exec(text)) !== null) {
737
+ const alreadyCaptured = found.some((f) => m.index >= f.index && m.index < f.index + 12);
738
+ const overlapsToolCall = /<tool_call>/i.test(text.slice(Math.max(0, m.index - 24), m.index));
739
+ if (alreadyCaptured || overlapsToolCall)
740
+ continue;
741
+ const call = parseXmlToolCall(m[0]);
742
+ if (call)
743
+ found.push({ index: m.index, call });
744
+ }
745
+ found.sort((a, b) => a.index - b.index);
746
+ // De-duplicate calls that two different matchers picked up at (nearly) the
747
+ // same spot so a single XML call isn't executed twice.
748
+ const deduped = [];
749
+ for (const entry of found) {
750
+ if (deduped.some((d) => sameToolCall(d.call, entry.call) &&
751
+ Math.abs(d.index - entry.index) < 64)) {
752
+ continue;
753
+ }
754
+ deduped.push(entry);
755
+ }
756
+ return deduped.map((f) => f.call);
757
+ }
758
+ /** Structural equality for two tool calls (name + canonical args JSON). */
759
+ export function sameToolCall(a, b) {
760
+ if (a.name !== b.name)
761
+ return false;
762
+ try {
763
+ return JSON.stringify(a.args) === JSON.stringify(b.args);
764
+ }
765
+ catch {
766
+ return false;
767
+ }
768
+ }
769
+ /**
770
+ * Partition a batch of tool calls (in document order) into execution groups.
771
+ * A run of consecutive parallel-safe calls forms one group to be run
772
+ * concurrently (bounded by maxGroupSize); every non-parallel-safe call is its
773
+ * own single-element group, i.e. a sequential barrier. Because plan updates
774
+ * and side-effecting tools are never parallel-safe, they always split the
775
+ * batch — which keeps parallelism scoped within a single task and prevents
776
+ * plan-state races and overlapping writes.
777
+ */
778
+ export function groupToolCallsForExecution(calls, isParallelSafe, maxGroupSize = 4) {
779
+ const groups = [];
780
+ let cursor = 0;
781
+ while (cursor < calls.length) {
782
+ const group = [calls[cursor]];
783
+ if (isParallelSafe(calls[cursor])) {
784
+ let j = cursor + 1;
785
+ while (j < calls.length &&
786
+ group.length < maxGroupSize &&
787
+ isParallelSafe(calls[j])) {
788
+ group.push(calls[j]);
789
+ j += 1;
790
+ }
791
+ }
792
+ groups.push(group);
793
+ cursor += group.length;
794
+ }
795
+ return groups;
796
+ }
797
+ /**
798
+ * Build the conversation to hand back to the caller at turn end. Strips system
799
+ * prompts (they're re-added each turn) but keeps the user turn plus every
800
+ * assistant tool-call and tool result, then appends the final answer if it
801
+ * isn't already the last message. Persisting this is what lets a resumed
802
+ * session give the model back what it actually did — commands, outputs, and
803
+ * results — instead of only its prose answers.
804
+ */
805
+ export function buildTurnHistory(messages, answer) {
806
+ // Drop system messages (the main prompt, plan context, and reflections are
807
+ // all re-injected each turn) EXCEPT compacted session memory, which is the
808
+ // only record of summarized older turns and must survive a resume.
809
+ const convo = messages.filter((m) => m.role !== "system" || isCompactionMemoryMessage(m));
810
+ const last = convo[convo.length - 1];
811
+ if (answer &&
812
+ !(last && last.role === "assistant" && last.content === answer)) {
813
+ convo.push({ role: "assistant", content: answer });
814
+ }
815
+ return convo;
816
+ }
817
+ /**
818
+ * Collapse pathological repetition before a message is stored in history.
819
+ * Some models degenerate into emitting the same short phrase hundreds of
820
+ * times ("We need to wait.We need to wait.…"), which otherwise bloats the
821
+ * context window and wastes tokens on every subsequent turn. We keep a few
822
+ * copies and note the collapse so the meaning is preserved without the bulk.
823
+ */
824
+ export function collapseRepeatedText(text) {
825
+ if (!text || text.length < 1500)
826
+ return text;
827
+ try {
828
+ return text.replace(/(.{3,80}?)\1{6,}/gs, (match, unit) => `${unit.repeat(3)} …[repeated ~${Math.round(match.length / Math.max(1, unit.length))}× — collapsed]`);
829
+ }
830
+ catch {
831
+ return text;
832
+ }
833
+ }
834
+ /** Extract the text before the tool call block for display purposes */
835
+ export function textBeforeToolCall(text) {
836
+ const patterns = [
837
+ /```tool\s*\n?[\s\S]*$/i,
838
+ /<tool_call>[\s\S]*$/i,
839
+ // Kimi/Moonshot sentinel block — strip from the section opener
840
+ // (or the first call opener if the section header is missing).
841
+ /<\|tool_calls_section_begin\|>[\s\S]*$/i,
842
+ /<\|tool_call_begin\|>[\s\S]*$/i,
843
+ /#{1,3}\s*tool\s*\n\s*\{[\s\S]*$/i,
844
+ /\*\*tool\*\*\s*\n\s*\{[\s\S]*$/i,
845
+ /```\w*\s*\n?\{[\s\S]*?"name"[\s\S]*$/i,
846
+ /\{"name"\s*:\s*"[^"]+"\s*,\s*"args"\s*:\s*\{[\s\S]*$/i,
847
+ ];
848
+ for (const pattern of patterns) {
849
+ const idx = text.search(pattern);
850
+ if (idx >= 0) {
851
+ return text.slice(0, idx).trim();
852
+ }
853
+ }
854
+ return text.trim();
855
+ }
856
+ export function formatToolArgs(call) {
857
+ if (call.name === "shell.exec")
858
+ return String(call.args.command ?? "");
859
+ if (call.name === "net.scan")
860
+ return `${call.args.target ?? ""}${call.args.ports ? ` -p ${call.args.ports}` : ""}${call.args.flags ? ` ${call.args.flags}` : ""}`;
861
+ if (call.name === "pentest.recon")
862
+ return String(call.args.target ?? "");
863
+ if (call.name === "dns.lookup")
864
+ return `${call.args.target ?? ""}${call.args.record ? ` ${call.args.record}` : " A"}`;
865
+ if (call.name === "whois.lookup")
866
+ return String(call.args.target ?? "");
867
+ if (call.name === "fs.read" || call.name === "fs.write")
868
+ return String(call.args.path ?? "");
869
+ if (call.name === "fs.writeMany") {
870
+ const files = Array.isArray(call.args.files) ? call.args.files : [];
871
+ const names = files
872
+ .map((f) => f && typeof f === "object"
873
+ ? String(f.path ?? "")
874
+ : "")
875
+ .filter(Boolean);
876
+ const preview = names.slice(0, 4).join(", ");
877
+ return `${names.length} file(s)${preview ? `: ${preview}${names.length > 4 ? ", …" : ""}` : ""}`;
878
+ }
879
+ if (call.name === "fs.search")
880
+ return String(call.args.pattern ?? "");
881
+ if (call.name === "image.ocr" || call.name === "pdf.read")
882
+ return String(call.args.path ?? "");
883
+ if (call.name === "http.fetch" || call.name === "web.fetch")
884
+ return String(call.args.url ?? "");
885
+ if (call.name === "web.search")
886
+ return String(call.args.query ?? "");
887
+ if (call.name === "pkg.install")
888
+ return String(call.args.tool ?? "");
889
+ if (call.name === "fs.list")
890
+ return String(call.args.path ?? safeCwd());
891
+ return JSON.stringify(call.args);
892
+ }
893
+ const VOLATILE_SIGNAL_RE = /\b(?:current(?:ly)?|latest|newest|today|now|right now|live|recent|breaking|news|release[sd]?|version|prices?|stocks?|market|rates?|weather|forecast|elections?|results?|rankings?|standings?|stats?|cve|advis(?:ory|ories)|vulnerabilit(?:y|ies))\b/i;
894
+ const VOLATILE_ROLE_QUERY_RE = /\b(?:who(?:\s+is|'s)?|whos|name|tell\s+me|what(?:\s+is|'s)?)\b[\s\S]{0,120}\b(?:cm|chief\s+minister|prime\s+minister|president|governor|mayor|ministers?|cabinet|leader|head\s+of|ceo|cto|cfo|coo|chair(?:man|woman|person)?|coach|captain)\b/i;
895
+ const ROLE_OF_ENTITY_RE = /\b(?:cm|chief\s+minister|prime\s+minister|president|governor|mayor|ministers?|ceo|cto|cfo|coo|chair(?:man|woman|person)?|coach|captain)\s+(?:of|for|in)\b/i;
896
+ const EXPLICIT_WEB_LOOKUP_RE = /\b(?:search\s+(?:the\s+)?(?:web|internet|online)|look\s*up|google|verify\s+(?:online|on\s+the\s+web)|check\s+(?:online|the\s+web|internet))\b/i;
897
+ const STATIC_DISAMBIGUATION_RE = /\b(?:stand\s+for|stands\s+for|meaning|definition|define|abbreviation|centimeters?|centimetres?)\b/i;
898
+ const LOCAL_RUNTIME_RE = /\b(?:current\s+(?:directory|dir|cwd|working\s+directory|folder|path|user|shell|process(?:es)?|branch|git\s+branch|network|ip|interfaces?|working\s+tree)|pwd|whoami)\b/i;
899
+ // Signals that the current turn is (or continues) a coding / scaffolding
900
+ // task. These are intentionally broad — over-budgeting a build is cheap
901
+ // (the loop still stops as soon as the model gives a final answer) while
902
+ // under-budgeting silently truncates a half-built project.
903
+ const BUILD_TASK_RE = /\b(?:build|create|scaffold|generate|make|set\s*up|setup|bootstrap|init(?:ialize)?|implement|add|write|develop|code|refactor|migrate|convert|wire\s*up|integrate)\b[\s\S]{0,80}\b(?:app|application|project|site|website|web\s*app|server|api|service|component|page|module|feature|cli|script|library|package|frontend|backend|fullstack|game|bot|dashboard|form|endpoint|database|schema|test|tests|suite)\b/i;
904
+ const BUILD_STACK_RE = /\b(?:react|next(?:\.?js)?|vue|svelte|angular|vite|webpack|express|fastify|nest(?:js)?|django|flask|fastapi|rails|laravel|spring|node(?:\.?js)?|typescript|tailwind|redux|prisma|mongoose|graphql|docker|kubernetes)\b/i;
905
+ // Pentest / security keywords — these tasks are inherently multi-step and
906
+ // always deserve the full step budget, just like build tasks.
907
+ const PENTEST_TASK_RE = /\b(?:pentest|pen[\s-]?test|penetration|security\s*(?:test|audit|scan|assess)|csrf|xss|sqli|sql[\s-]?inject|rce|lfi|rfi|ssrf|idor|xxe|brute[\s-]?force|enumerat|exploit|vulnerabilit|recon|bug[\s-]?bounty|ctf|capture[\s-]?the[\s-]?flag|red[\s-]?team|offensive|nmap|nikto|nuclei|ffuf|gobuster|sqlmap|hydra|metasploit)\b/i;
908
+ /**
909
+ * Detect pentest/security tasks that need the full step budget.
910
+ * Mirrors looksLikeBuildTask but for security work.
911
+ */
912
+ export function looksLikePentestTask(prompt, history) {
913
+ if (PENTEST_TASK_RE.test(prompt))
914
+ return true;
915
+ if (history && history.length > 0) {
916
+ const recent = history.slice(-6);
917
+ for (const msg of recent) {
918
+ if (msg.role === "user" && PENTEST_TASK_RE.test(msg.content))
919
+ return true;
920
+ }
921
+ }
922
+ return false;
923
+ }
924
+ // Short continuation prompts that, on their own, carry no build signal but
925
+ // clearly mean "keep going with what we were doing".
926
+ const CONTINUATION_RE = /^(?:do\s+it|build\s+it|build\s+fully|build\s+it\s+fully|go\s+ahead|continue|proceed|keep\s+going|finish(?:\s+it)?|complete(?:\s+it)?|yes|ok(?:ay)?|make\s+it|run\s+it|next|on\s+your\s+own|build\s+(?:fully\s+)?on\s+your\s+own)\b/i;
927
+ const INCOMPLETE_RE = /\b(?:not\s+complete|incomplete|isn'?t\s+(?:done|complete|working|finished)|doesn'?t\s+work|still\s+(?:broken|missing|failing)|missing\s+(?:files?|parts?)|finish\s+(?:the|it)|complete\s+(?:the|it))\b/i;
928
+ // The synthetic message injected when the user runs /implement to approve a
929
+ // plan ("I approve the plan. Execute it now, task by task…"). It must always
930
+ // count as a build/continuation turn — it contains the word "now", which
931
+ // would otherwise trip the volatile-info freshness guard and divert the run
932
+ // into a pointless web.search instead of executing the plan.
933
+ const PLAN_EXECUTION_RE = /\b(?:approve the plan|execute it (?:now|task by task)|task by task|execute the plan|implement the plan)\b/i;
934
+ // Informational / comparison / explanation intent. These questions want an
935
+ // ANSWER, not a build — even when they mention a framework or an install
936
+ // step (e.g. "compare installation steps in react vite", "how do I set up
937
+ // tailwind", "tailwind 3 vs 4"). They must NOT trigger the explore→plan
938
+ // build workflow.
939
+ const INFORMATIONAL_SIGNAL_RE = /\b(?:compare|comparison|contrast|differ(?:ence|ences|s)?|pros\s+and\s+cons|trade-?offs?|versus|vs\.?|cheat\s*sheet|explain|describe|summari[sz]e|overview)\b/i;
940
+ const INTERROGATIVE_LEAD_RE = /^(?:what|which|why|how|when|who|where|is|are|do|does|did|can|could|should|would|will)\b/i;
941
+ /**
942
+ * Does a single message imply an actual build/scaffold task (as opposed to a
943
+ * question about one)? Comparison/explanation signals and plain questions are
944
+ * treated as informational and return false even when they name a stack.
945
+ */
946
+ function messageImpliesBuild(text) {
947
+ if (!text)
948
+ return false;
949
+ if (INFORMATIONAL_SIGNAL_RE.test(text))
950
+ return false;
951
+ // Explicit "build/create/scaffold … <thing>" is always a build.
952
+ if (BUILD_TASK_RE.test(text))
953
+ return true;
954
+ // A bare question (interrogative lead or trailing "?") that merely mentions
955
+ // a stack is informational, not a build.
956
+ if (text.endsWith("?") || INTERROGATIVE_LEAD_RE.test(text))
957
+ return false;
958
+ return BUILD_STACK_RE.test(text);
959
+ }
960
+ /**
961
+ * Decide whether this turn should get the build workflow (explore → plan →
962
+ * implement) and a generous step budget. Looks at the current prompt first,
963
+ * then falls back to recent USER turns so a terse follow-up inherits an
964
+ * ongoing build — but NOT the agent's own (possibly mistaken) plan narration.
965
+ */
966
+ export function looksLikeBuildTask(prompt, history) {
967
+ const text = prompt.replace(/\s+/g, " ").trim();
968
+ // Continuation / "not done yet" / plan-execution always count as build.
969
+ if (CONTINUATION_RE.test(text) ||
970
+ INCOMPLETE_RE.test(text) ||
971
+ PLAN_EXECUTION_RE.test(text)) {
972
+ return true;
973
+ }
974
+ if (messageImpliesBuild(text)) {
975
+ return true;
976
+ }
977
+ // Inspect recent USER turns only: if the user was already building
978
+ // something, treat a terse follow-up as part of that build. (Assistant
979
+ // turns are excluded so a misfired plan can't keep re-triggering build.)
980
+ if (history && history.length > 0) {
981
+ const recent = history.slice(-6);
982
+ for (const msg of recent) {
983
+ if (msg.role !== "user")
984
+ continue;
985
+ if (messageImpliesBuild(msg.content.replace(/\s+/g, " ").trim())) {
986
+ return true;
987
+ }
988
+ }
989
+ }
990
+ return false;
991
+ }
992
+ // Matrix of action-verb narration: the model says it is *about to* do
993
+ // something but hasn't. Used to detect "narrate, don't act" stalls.
994
+ const ACTION_NARRATION_RE = /\b(?:let me|let's|i'?ll|i will|i'?m going to|i am going to|i need to|i should|i'?m about to|going to|now i'?ll|first[,]?\s*i'?ll)\s+(?:now\s+|first\s+|quickly\s+|just\s+|go\s+ahead\s+and\s+)?(?:explore|list|read|fetch|browse|check|inspect|examine|look|create|run|start|write|build|add|scaffold|set\s*up|setup|install|initialize|init|generate|make|review|open|find|search|verify|update|edit|modify|fix|implement|gather|assess|scan|audit)\b/i;
995
+ /**
996
+ * Detect a message that narrates an *upcoming* action ("let me explore the
997
+ * directory", "I'll create the components") rather than an actual answer or
998
+ * tool call. Used to catch models that describe intent but emit no tool call,
999
+ * which would otherwise end the turn with nothing done. A real completion
1000
+ * summary (past tense, longer, or containing a code block) is NOT flagged.
1001
+ */
1002
+ export function looksLikeActionNarration(text) {
1003
+ const t = text.trim();
1004
+ if (t.length === 0 || t.length > 600)
1005
+ return false;
1006
+ if (t.includes("```"))
1007
+ return false;
1008
+ return ACTION_NARRATION_RE.test(t);
1009
+ }
1010
+ /**
1011
+ * Detect a message that narrates a PLAN as prose ("Goal: … Tasks: 1. … Please
1012
+ * approve the plan") instead of calling plan.create. Such a turn leaves no
1013
+ * real plan, so the user can't /implement it — we nudge the model to emit the
1014
+ * plan.create tool call instead.
1015
+ */
1016
+ export function looksLikePlanNarration(text) {
1017
+ const t = text.trim();
1018
+ if (t.length < 40)
1019
+ return false;
1020
+ const approval = /\b(?:approve|approval|once approved|request changes|await(?:ing)?\s+(?:your\s+)?approval)\b/i.test(t);
1021
+ const goal = /\bgoal\b/i.test(t);
1022
+ const tasks = /\b(?:tasks?|steps?)\b/i.test(t) ||
1023
+ /(?:^|\n)\s*(?:t?1[.)]|step\s*1)\b/im.test(t);
1024
+ return approval || (goal && tasks);
1025
+ }
1026
+ export function requiresFreshWebSearch(prompt) {
1027
+ const text = prompt.replace(/\s+/g, " ").trim();
1028
+ if (!text)
1029
+ return false;
1030
+ if (STATIC_DISAMBIGUATION_RE.test(text) || LOCAL_RUNTIME_RE.test(text)) {
1031
+ return false;
1032
+ }
1033
+ // Plan-execution and terse continuation turns are never "fetch current
1034
+ // info" turns, even when they contain words like "now". (We intentionally
1035
+ // do NOT exclude on build-stack keywords here — "latest vite version" is a
1036
+ // legitimate version lookup. The runAgentLoop caller additionally gates the
1037
+ // guard on looksLikeBuildTask so a real scaffold turn never searches.)
1038
+ if (PLAN_EXECUTION_RE.test(text) || CONTINUATION_RE.test(text)) {
1039
+ return false;
1040
+ }
1041
+ return (VOLATILE_SIGNAL_RE.test(text) ||
1042
+ VOLATILE_ROLE_QUERY_RE.test(text) ||
1043
+ ROLE_OF_ENTITY_RE.test(text) ||
1044
+ EXPLICIT_WEB_LOOKUP_RE.test(text));
1045
+ }
1046
+ /**
1047
+ * Detect a low-quality "everything in one step" plan task. A single task that
1048
+ * itself enumerates many files/actions (multiple commas, an "and", several
1049
+ * slashes, or an overlong title) means the model lumped the whole build into
1050
+ * one checkbox instead of producing a real ordered checklist.
1051
+ */
1052
+ export function isLumpedSingleTask(taskTitles) {
1053
+ if (taskTitles.length !== 1)
1054
+ return false;
1055
+ const only = taskTitles[0];
1056
+ return ((only.match(/,/g)?.length ?? 0) >= 2 ||
1057
+ /\band\b/i.test(only) ||
1058
+ (only.match(/\//g)?.length ?? 0) >= 2 ||
1059
+ only.length > 90);
1060
+ }
1061
+ export function freshnessGuardMessage(now = new Date()) {
1062
+ return (`Freshness guard for this turn: the latest user prompt appears to ask for current, volatile, or externally verifiable information. The present moment is ${currentDateTimeContext(now)}. ` +
1063
+ "Before answering, call web.search FIRST with a concise query derived from the user prompt. " +
1064
+ "Shape the search query for the newest timeline by including current/latest or the current year/month when useful. " +
1065
+ "Do not answer from the snippets alone when detail matters — set fetchTop (e.g. fetchTop:2) to read the top result pages, or follow up with web.fetch on the most relevant URL, then answer from what the pages actually say and cite them. " +
1066
+ "If web.search fails or has no results, say that current information is unavailable instead of guessing from memory.");
1067
+ }
1068
+ /**
1069
+ * Directive injected for build/scaffold turns. Forces the careful
1070
+ * explore → understand → plan → implement loop instead of a one-shot dump,
1071
+ * and forbids stopping before the goal is reached.
1072
+ */
1073
+ export function buildWorkflowDirective() {
1074
+ return [
1075
+ "BUILD WORKFLOW (this is a build/scaffold/feature task — follow this order EXACTLY; deviation is a failure):",
1076
+ "1. EXPLORE: fs.list the working directory (and key subdirs) to see what already exists. Use tool.batch to parallelize reads.",
1077
+ "2. UNDERSTAND: fs.read the files that matter (like package.json for js related and same for other languages too, config, entry points, existing components). Detect the existing stack/tooling and MATCH it. If the dir is empty or only has a stub, start fresh with a sensible modern default and say so.",
1078
+ "3. PLAN: call plan.create with a COMPREHENSIVE plan — a detailed `detail` (stack chosen and WHY, architecture, how you'll verify) and 4-8 SEPARATE, ordered, high-quality tasks. The FIRST task initializes the project (scaffolder); the MIDDLE tasks MUST implement the ACTUAL FEATURE the user asked for by REPLACING the scaffolder's boilerplate (e.g. rewrite src/App.jsx into the real todo/blog/etc. UI, add components, state, styles); the LAST task verifies with a build. Scaffolding + install + run ALONE is NOT acceptable — that just leaves the Vite starter page. Each task is one distinct, verifiable action. Then STOP and wait for the user to /implement.",
1079
+ "4. IMPLEMENT: once approved, work task by task in STRICT ORDER. For each task: call task.update {taskId, state:'in_progress'} → do the real work → VERIFY it actually succeeded (read a file you wrote, check the command's exit/output) → call task.update {taskId, state:'done'}, then move to the NEXT task. You MAY emit several tool calls in one message; they run in document order (independent read-only lookups run in parallel, while task.update and any write/command runs one at a time), and the batch STOPS if one fails. Keep every batch scoped to ONE task. A clean rhythm is: task.update in_progress + the work + task.update done together. Keep going until EVERY task is done. Do NOT claim work you didn't actually run.",
1080
+ "",
1081
+ "INITIALIZE WITH THE OFFICIAL SCAFFOLDER FIRST (do NOT hand-write build configs):",
1082
+ '- React/Vue/Svelte/vanilla → `npm create vite@latest <appname> -- --template react` (templates: react, react-ts, vue, vue-ts, svelte, vanilla). Next.js → `npx --yes create-next-app@latest <appname> --yes --eslint --no-tailwind --app --src-dir --import-alias "@/*"`. Node API → `npm init -y`.',
1083
+ "- GET THE TEMPLATE FLAG RIGHT. With `npm create vite@latest NAME -- --template react` the `--` IS required (it forwards --template to create-vite). With `npx create-vite@latest NAME --template react` do NOT add `--` (npx passes args straight through, so `-- --template react` makes npx DROP the flag and you silently get the WRONG, vanilla template). Pick ONE form and keep the template flag attached. After scaffolding, fs.read the generated index.html / src entry to CONFIRM you got React (a src/main.jsx + App.jsx, not a vanilla main.js/counter.js). If it's the wrong template, delete the folder and re-run with the correct command.",
1084
+ "- RUN SCAFFOLDERS NON-INTERACTIVELY and into a NEW SUBFOLDER (`<appname>`). Scaffolders REFUSE to run in a non-empty directory and then print 'Operation cancelled' — and the current dir frequently already has a file like .DS_Store. So scaffold into a subfolder (always works). `--yes` does NOT fix the non-empty-dir cancel; a subfolder does. NEVER background a scaffolder with `&` or pipe `yes |` into it.",
1085
+ '- If a scaffolder cannot be driven non-interactively or keeps failing, FALL BACK to hand-writing a minimal Vite setup (package.json with "type":"module", @vitejs/plugin-react, index.html that loads /src/main.jsx, src/main.jsx, src/App.jsx) then `npm install`. That never prompts and you control every file.',
1086
+ "- VERIFY the init actually worked before marking the task done: fs.read package.json (it must now exist AND list react + react-dom) and fs.read index.html (it must reference your jsx entry). 'Operation cancelled' / non-zero exit means the task FAILED — do not proceed as if it succeeded.",
1087
+ "",
1088
+ "CRITICAL RULES during IMPLEMENTATION:",
1089
+ "- You may batch tool calls: emit one or several ```tool blocks in a message. They run in document order — independent READ-ONLY lookups (fs.read/list/search, dns/whois, http.fetch GET, web.search/fetch, sysinfo) run in parallel, while task.update and any write/command (fs.write*, shell.exec, pkg.install, net.scan) run one at a time. If any call fails, the rest of that batch is cancelled so you can react — so order dependent steps correctly and keep every batch scoped to ONE task. A good batch is task.update(in_progress) + the work + task.update(done) for ONE task.",
1090
+ "- Do NOT re-explore. Step 1 (EXPLORE) was already completed during planning. Start executing the first pending task immediately.",
1091
+ "- ONE task at a time, in ORDER. Do NOT skip ahead to task 3 before task 2 is done.",
1092
+ "- KEEP EACH FILE SMALL ENOUGH TO WRITE IN ONE CALL. If a fs.write is reported as 'cut off (output too long)', the file was NOT fully written and is likely broken/invalid — re-write it, splitting a large component into smaller files if needed. NEVER leave a half-written file and move on.",
1093
+ "- VERIFY each step before marking it done: after writing files, fs.read the file back and confirm it is COMPLETE and syntactically valid (balanced braces/parens/JSX tags); after an install, check it exited 0. Marking a task done without a successful, verified tool call is the worst failure.",
1094
+ "- VERIFY THE BUILD, not just the dev server. `vite` / `npm run dev` reports 'ready' even when your App.jsx has syntax errors (the error only shows in the browser). To actually confirm the app works, run `npm run build` (it fails on real syntax/JSX errors) and check it exits 0. Seeing 'VITE ready' is NOT proof the app renders.",
1095
+ "- If a tool call FAILS (error output, non-zero exit, file missing), the task is NOT done. Mark it 'failed', diagnose WHY, fix it, and retry until it succeeds.",
1096
+ "- NEVER claim a task is done, files were created, a dependency is installed, or a server is running unless the tool call ACTUALLY succeeded and you saw the success output. If you have not run it, say so.",
1097
+ "- Start a dev server with shell.start (background job), NOT `npm run dev &` via shell.exec.",
1098
+ "- THE DELIVERABLE IS THE WORKING FEATURE, NOT THE SCAFFOLD. After scaffolding you MUST replace the starter boilerplate (Vite's default App.jsx counter, Next's starter page, etc.) with the actual app the user asked for. If the user asked for a todo app, src/App.jsx must contain a real todo UI with state — finishing with the untouched Vite starter page is a FAILURE even if the build passes.",
1099
+ "",
1100
+ "FORBIDDEN before plan approval (/implement): you MUST NOT use fs.write, fs.writeMany, fs.edit, shell.exec, shell.start, pkg.install, or pkg.uninstall. The ONLY tool allowed before approval is plan.create (and the read/list tools for exploration). If you are nudged to 'take action' before a plan exists, your action MUST be plan.create.",
1101
+ "If the task is genuinely trivial (a single tiny file), you may skip the plan — but for an app/feature, ALWAYS plan first.",
1102
+ ].join("\n");
1103
+ }
1104
+ export function shouldDimToolChatter(call) {
1105
+ return call.name === "web.search";
1106
+ }
1107
+ //# sourceMappingURL=tool-call-parser.js.map