@botconnector/bccli 0.4.0-beta.3 → 0.4.0-beta.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,16 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- HELP_TEXT,
4
- parseCliArgs
5
- } from "./chunk-UN7QXPCB.js";
6
- import {
7
- addProviderKey,
8
- createProvider,
9
- listAllModels
10
- } from "./chunk-2JBRLGKD.js";
11
- import {
12
- runLogin
13
- } from "./chunk-L7L4RGKG.js";
14
2
  import {
15
3
  BUILTIN_AGENTS,
16
4
  buildSystemPrompt,
@@ -18,15 +6,33 @@ import {
18
6
  loadAgentDefs,
19
7
  loadCommands,
20
8
  loadSkills,
9
+ normalizeGlobPath,
21
10
  parseFrontmatter,
22
- runPrint
23
- } from "./chunk-OSWQPSA4.js";
11
+ runPrint,
12
+ validateGlobPattern
13
+ } from "./chunk-NWVFCTTO.js";
14
+ import {
15
+ helpText,
16
+ parseCliArgs
17
+ } from "./chunk-NM3C75BT.js";
24
18
  import {
25
19
  Permissions
26
20
  } from "./chunk-ELML4KVQ.js";
27
21
  import {
28
- providerName
29
- } from "./chunk-U7DLMVPZ.js";
22
+ addProviderKey,
23
+ completionAssistantMessage,
24
+ createProvider,
25
+ hasPendingProgrammaticReplay,
26
+ listAllModels
27
+ } from "./chunk-CX5U4EHX.js";
28
+ import {
29
+ providerName,
30
+ readGlobalConfigFile
31
+ } from "./chunk-AOVIQXXO.js";
32
+ import "./chunk-UJH2JT6H.js";
33
+ import {
34
+ runLogin
35
+ } from "./chunk-Z37BR5H4.js";
30
36
  import {
31
37
  Session,
32
38
  pruneSessions
@@ -35,8 +41,76 @@ import {
35
41
  ConfigError,
36
42
  bccliHome,
37
43
  loadConfig,
38
- resolveModel
39
- } from "./chunk-RZUJYAQV.js";
44
+ resolveLanguage,
45
+ resolveModel,
46
+ setLanguage,
47
+ t
48
+ } from "./chunk-IJGTX7RC.js";
49
+
50
+ // src/textToolCalls.ts
51
+ var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
52
+ var CALL_START = /<tool_call>|\n?[^\n]*\bTool call list\b[^\n]*|\n\s*\d+\.\s*Tool:/g;
53
+ function coerce(value, type) {
54
+ const types = Array.isArray(type) ? type : [type];
55
+ const v = value.trim();
56
+ if ((types.includes("integer") || types.includes("number")) && v !== "" && !Number.isNaN(Number(v))) return Number(v);
57
+ if (types.includes("boolean") && /^(true|false)$/i.test(v)) return v.toLowerCase() === "true";
58
+ if ((types.includes("object") || types.includes("array")) && /^[[{]/.test(v)) {
59
+ try {
60
+ return JSON.parse(v);
61
+ } catch {
62
+ }
63
+ }
64
+ return value.replace(/^\n+|\n+$/g, "");
65
+ }
66
+ function pickTool(name, keys, tools) {
67
+ const named = name && tools.find((t2) => t2.function.name === name);
68
+ if (named) return named;
69
+ let best;
70
+ let bestScore = 0;
71
+ let tie = false;
72
+ for (const t2 of tools) {
73
+ const params = t2.function.parameters;
74
+ const props = Object.keys(params.properties ?? {});
75
+ if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
76
+ const score = keys.filter((k) => props.includes(k)).length;
77
+ if (score > bestScore) [best, bestScore, tie] = [t2, score, false];
78
+ else if (score === bestScore && score > 0) tie = true;
79
+ }
80
+ return tie ? void 0 : best;
81
+ }
82
+ function recoverTextToolCalls(text, tools) {
83
+ if (!/<\/arg_value>|<tool_call>/.test(text)) return null;
84
+ const starts = [...text.matchAll(CALL_START)].map((m) => m.index ?? 0);
85
+ const firstPair = text.search(PAIR);
86
+ const cut = Math.min(...[...starts, firstPair].filter((i) => i >= 0));
87
+ const bounds = starts.filter((i) => i >= cut);
88
+ if (!bounds.length || bounds[0] > cut) bounds.unshift(cut);
89
+ const toolCalls = [];
90
+ bounds.forEach((start, n) => {
91
+ const segment = text.slice(start, bounds[n + 1] ?? text.length);
92
+ const name = /<tool_call>\s*([A-Za-z_][\w.-]*)/.exec(segment)?.[1] ?? /Tool:\s*`?([A-Za-z_][\w.-]*)`?/.exec(segment)?.[1];
93
+ let args = [];
94
+ const flush = () => {
95
+ if (!args.length) return;
96
+ const keys = args.map(([k]) => k);
97
+ const tool = pickTool(name, keys, tools);
98
+ if (tool) {
99
+ const props = tool.function.parameters.properties ?? {};
100
+ const input = Object.fromEntries(args.filter(([k]) => k in props).map(([k, v]) => [k, coerce(v, props[k].type)]));
101
+ toolCalls.push({ id: `text_call_${toolCalls.length}`, name: tool.function.name, arguments: JSON.stringify(input) });
102
+ }
103
+ args = [];
104
+ };
105
+ for (const m of segment.matchAll(PAIR)) {
106
+ if (args.some(([k]) => k === m[1])) flush();
107
+ args.push([m[1], m[2]]);
108
+ }
109
+ flush();
110
+ });
111
+ if (!toolCalls.length) return null;
112
+ return { text: text.slice(0, cut).trim(), toolCalls };
113
+ }
40
114
 
41
115
  // src/tools/index.ts
42
116
  import { z as z8 } from "zod";
@@ -136,7 +210,7 @@ function runCommand(command, opts) {
136
210
  clearTimeout(timer);
137
211
  opts.signal.removeEventListener("abort", onAbort);
138
212
  const output = dropped ? `${head}
139
- \u2026 [${dropped} karakter dipotong] \u2026
213
+ \u2026 [${dropped} characters truncated] \u2026
140
214
  ${tail}` : head + tail;
141
215
  resolve2({ output: output + extra, exitCode, timedOut, aborted });
142
216
  };
@@ -155,7 +229,7 @@ var bashTool = defineTool({
155
229
  target: (input) => input.command,
156
230
  async run(input, ctx) {
157
231
  const r = await runCommand(input.command, { cwd: ctx.cwd, timeoutMs: input.timeout_ms ?? 12e4, signal: ctx.signal });
158
- const status = r.timedOut ? `[dihentikan: timeout ${Math.round((input.timeout_ms ?? 12e4) / 1e3)} detik]` : r.aborted ? "[dibatalkan user]" : `[exit code ${r.exitCode}]`;
232
+ const status = r.timedOut ? `[stopped: timeout after ${Math.round((input.timeout_ms ?? 12e4) / 1e3)} seconds]` : r.aborted ? "[cancelled by the user]" : `[exit code ${r.exitCode}]`;
159
233
  return { output: `${r.output.trimEnd()}
160
234
  ${status}`.trimStart(), isError: r.exitCode !== 0 };
161
235
  }
@@ -214,7 +288,7 @@ function countOccurrences(haystack, needle) {
214
288
  }
215
289
  function applyEdit(content, input) {
216
290
  if (input.old_string === input.new_string) {
217
- return { output: "old_string dan new_string sama; tidak ada perubahan.", isError: true };
291
+ return { output: "old_string and new_string are identical; nothing to change.", isError: true };
218
292
  }
219
293
  const eol = content.includes("\r\n") ? "\r\n" : "\n";
220
294
  const toEol = (s) => s.replace(/\r?\n/g, eol);
@@ -225,13 +299,13 @@ function applyEdit(content, input) {
225
299
  if (count === 0) continue;
226
300
  if (count > 1 && !input.replace_all) {
227
301
  return {
228
- output: `old_string muncul ${count} kali di ${input.path}. Tambah konteks agar unik, atau pakai replace_all.`,
302
+ output: `old_string appears ${count} times in ${input.path}. Add more context to make it unique, or use replace_all.`,
229
303
  isError: true
230
304
  };
231
305
  }
232
306
  return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
233
307
  }
234
- return { output: `old_string tidak ditemukan di ${input.path}. Baca ulang file-nya.`, isError: true };
308
+ return { output: `old_string not found in ${input.path}. Re-read the file.`, isError: true };
235
309
  }
236
310
  var editTool = defineTool({
237
311
  name: "edit",
@@ -241,12 +315,12 @@ var editTool = defineTool({
241
315
  target: (input) => input.path,
242
316
  async validate(input, ctx) {
243
317
  const abs = resolvePath(ctx.cwd, input.path);
244
- if (!ctx.readFiles.has(abs)) return `Baca ${input.path} dulu dengan read sebelum mengedit.`;
318
+ if (!ctx.readFiles.has(abs)) return `Read ${input.path} with read before editing it.`;
245
319
  let content;
246
320
  try {
247
321
  content = await readFile(abs, "utf8");
248
322
  } catch (error) {
249
- return `Tidak bisa membaca ${input.path}: ${errorCode(error)}`;
323
+ return `Cannot read ${input.path}: ${errorCode(error)}`;
250
324
  }
251
325
  const next = applyEdit(content, input);
252
326
  return typeof next === "string" ? void 0 : next.output;
@@ -260,25 +334,30 @@ var editTool = defineTool({
260
334
  async run(input, ctx) {
261
335
  const abs = resolvePath(ctx.cwd, input.path);
262
336
  if (!ctx.readFiles.has(abs)) {
263
- return { output: `Baca ${input.path} dulu dengan read sebelum mengedit.`, isError: true };
337
+ return { output: `Read ${input.path} with read before editing it.`, isError: true };
264
338
  }
265
339
  let content;
266
340
  try {
267
341
  content = await readFile(abs, "utf8");
268
342
  } catch (error) {
269
- return { output: `Tidak bisa membaca ${input.path}: ${errorCode(error)}`, isError: true };
343
+ return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
270
344
  }
271
345
  const next = applyEdit(content, input);
272
346
  if (typeof next !== "string") return next;
273
347
  await ctx.checkpoint?.(abs);
274
348
  await writeFile(abs, next);
275
- return { output: `Mengedit ${input.path}.`, display: formatDiff(diffLines(content, next)) };
349
+ return { output: `Edited ${input.path}.`, display: formatDiff(diffLines(content, next)) };
276
350
  }
277
351
  });
278
352
 
279
353
  // src/tools/fetch.ts
280
354
  import { z as z3 } from "zod";
281
- var MAX_CHARS = 5e4;
355
+ var MAX_CHARS = 12e3;
356
+ var FOCUS_CHUNKS = 3;
357
+ var FOCUS_SIZE = 800;
358
+ var CACHE_MAX = 16;
359
+ var CACHE_TTL_MS = 15 * 6e4;
360
+ var CACHE_MAX_CHARS = 2e6;
282
361
  var MAX_BYTES = 5 * 1024 * 1024;
283
362
  async function readCapped(res) {
284
363
  const reader = res.body?.getReader();
@@ -300,49 +379,238 @@ async function readCapped(res) {
300
379
  function htmlToText(html) {
301
380
  return html.replace(/<(script|style|noscript)[\s\S]*?<\/\1>/gi, "").replace(/<(br|\/p|\/div|\/h[1-6]|\/li|\/tr)\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&").split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
302
381
  }
303
- var fetchTool = defineTool({
304
- name: "fetch",
305
- description: "Fetch a URL (http/https). HTML is converted to plain text. Max 50k characters.",
306
- schema: z3.object({ url: z3.string().describe("Absolute http(s) URL") }),
307
- kind: "fetch",
308
- target: (input) => input.url,
309
- async run(input, ctx) {
310
- if (!/^https?:\/\//i.test(input.url)) return { output: "URL harus diawali http:// atau https://", isError: true };
311
- let url = new URL(input.url);
312
- let res;
313
- try {
314
- for (let hop = 0; hop < 5; hop++) {
315
- res = await fetch(url, { signal: AbortSignal.any([ctx.signal, AbortSignal.timeout(3e4)]), redirect: "manual" });
316
- const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
317
- if (!location) break;
318
- const next = new URL(location, url);
319
- if (next.host !== url.host) {
320
- await res.body?.cancel();
321
- return {
322
- output: `Redirect ke host lain: ${next.href}. Panggil fetch lagi dengan URL itu kalau memang perlu (akan diminta izin).`,
323
- isError: true
324
- };
382
+ function describeFetchError(error) {
383
+ const cause = error.cause;
384
+ const code = cause?.code ?? "";
385
+ const detail = cause?.message ?? error.message;
386
+ if (error.name === "TimeoutError") return "no response within 30 seconds";
387
+ if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `${detail} \u2014 DNS could not find the host (typo, offline, or blocked by DNS/ISP)`;
388
+ if (/CERT|SELF_SIGNED|UNABLE_TO/.test(code)) {
389
+ return `${detail} \u2014 TLS certificate rejected; often caused by an antivirus or proxy that inspects HTTPS. Try running with NODE_OPTIONS=--use-system-ca`;
390
+ }
391
+ if (/ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR_CONNECT_TIMEOUT|UND_ERR_SOCKET/.test(code)) {
392
+ return `${detail} \u2014 connection failed (firewall, proxy, or the site is blocked by the network)`;
393
+ }
394
+ return code ? `${detail} (${code})` : detail;
395
+ }
396
+ var tokensOf = (text) => Math.ceil(text.length / 4);
397
+ function isPrivateHost(host) {
398
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "");
399
+ if (h === "localhost" || /\.(local|internal|lan|localdomain|home\.arpa)$/.test(h)) return true;
400
+ if (h.includes(":")) return /^(::1?$|f[cd]|fe[89ab]|::ffff:)/.test(h);
401
+ const v4 = h.match(/^(\d+)\.(\d+)\.\d+\.\d+$/);
402
+ if (v4) {
403
+ const [a, b] = [Number(v4[1]), Number(v4[2])];
404
+ return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127;
405
+ }
406
+ return !h.includes(".");
407
+ }
408
+ var words = (s) => s.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [];
409
+ function pickRelevant(text, query, count = FOCUS_CHUNKS, size = FOCUS_SIZE) {
410
+ const chunks = [];
411
+ let cur = "";
412
+ for (const line of text.split("\n")) {
413
+ for (let i = 0; i < Math.max(line.length, 1); i += size) {
414
+ cur += `${line.slice(i, i + size)}
415
+ `;
416
+ if (cur.length >= size) {
417
+ chunks.push(cur.trimEnd());
418
+ cur = "";
419
+ }
420
+ }
421
+ }
422
+ if (cur.trim()) chunks.push(cur.trimEnd());
423
+ const q = [...new Set(words(query))];
424
+ const docs = chunks.map(words);
425
+ const avg = docs.reduce((n, d) => n + d.length, 0) / (docs.length || 1) || 1;
426
+ const idf = q.map((t2) => {
427
+ const df = docs.filter((d) => d.includes(t2)).length;
428
+ return Math.log(1 + (docs.length - df + 0.5) / (df + 0.5));
429
+ });
430
+ const scored = docs.map((d, i) => ({
431
+ i,
432
+ score: q.reduce((sum, t2, k) => {
433
+ const tf = d.filter((w) => w === t2).length;
434
+ return sum + idf[k] * tf * 2.2 / (tf + 1.2 * (0.25 + 0.75 * d.length / avg));
435
+ }, 0)
436
+ })).filter((c) => c.score > 0).sort((a, b) => b.score - a.score).slice(0, count).sort((a, b) => a.i - b.i);
437
+ return scored.length ? scored.map((c) => chunks[c.i]).join("\n\u2026\n") : null;
438
+ }
439
+ var cache = /* @__PURE__ */ new Map();
440
+ function remember(key, value, size) {
441
+ if (size > CACHE_MAX_CHARS) return;
442
+ cache.delete(key);
443
+ cache.set(key, value);
444
+ if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
445
+ }
446
+ function recall(key) {
447
+ const hit = cache.get(key);
448
+ if (typeof hit === "object" && Date.now() - hit.at > CACHE_TTL_MS) {
449
+ cache.delete(key);
450
+ return void 0;
451
+ }
452
+ return hit;
453
+ }
454
+ var ACCEPT = "text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.5";
455
+ var withCost = (text, what) => `${what} \xB7 ${t("~{n} tokens", { n: tokensOf(text) })}`;
456
+ function createFetchTool(opts = {}) {
457
+ const keenableURL = opts.keenableURL === void 0 ? "https://api.keenable.ai/v1/fetch/public" : opts.keenableURL;
458
+ const isPrivate = opts.isPrivate ?? isPrivateHost;
459
+ return defineTool({
460
+ name: "fetch",
461
+ description: `Fetch a URL (http/https) as text/markdown. Costs the user tokens, so ask for as little as you need:
462
+ - Pass \`prompt\` (what you want to know from the page) to get only the relevant excerpt instead of the whole page. Prefer this whenever you are looking for specific facts.
463
+ - Without \`prompt\` you get the first ${MAX_CHARS} characters; the reply tells you the \`offset\` to continue from.
464
+ - Fetching a page that is already in this conversation is wasteful; reuse what you have.`,
465
+ schema: z3.object({
466
+ url: z3.string().describe("Absolute http(s) URL"),
467
+ prompt: z3.string().max(2e3).optional().describe("What to look for in the page; returns only the relevant excerpt"),
468
+ offset: z3.number().int().min(0).optional().describe("Start reading at this character (to continue a truncated page)")
469
+ }),
470
+ kind: "fetch",
471
+ target: (input) => input.url,
472
+ async run(input, ctx) {
473
+ if (!/^https?:\/\//i.test(input.url)) return { output: "URL must start with http:// or https://", isError: true };
474
+ const target = new URL(input.url);
475
+ const prompt = input.prompt?.trim();
476
+ const askKeenable = !!prompt && !input.offset && !!keenableURL && !target.username && !target.password && !isPrivate(target.hostname);
477
+ if (askKeenable) {
478
+ const key = `x
479
+ ${input.url}
480
+ ${prompt}`;
481
+ const hit = recall(key);
482
+ if (hit) {
483
+ ctx.refundFetch?.();
484
+ return { output: hit, display: withCost(hit, t("focused excerpt \xB7 cache")) };
485
+ }
486
+ const out = await extractWithKeenable(keenableURL, input.url, prompt, ctx.signal);
487
+ if (out) {
488
+ const output2 = `[Excerpt from ${input.url} for "${prompt}" (extracted by Keenable). If it says the answer is not there, try fetch without a prompt.]
489
+ ${out}`;
490
+ remember(key, output2, output2.length);
491
+ return { output: output2, display: withCost(output2, t("focused excerpt (Keenable)")) };
492
+ }
493
+ }
494
+ let page = recall(`p
495
+ ${input.url}`);
496
+ const cached = !!page;
497
+ if (cached) ctx.refundFetch?.();
498
+ if (!page) {
499
+ const loaded = await loadPage(input.url, ctx.signal);
500
+ if ("error" in loaded) return { output: loaded.error, isError: true };
501
+ page = loaded;
502
+ remember(`p
503
+ ${input.url}`, page, page.text.length);
504
+ }
505
+ const { text, truncated } = page;
506
+ const via = cached ? " \xB7 cache" : "";
507
+ if (prompt && !input.offset) {
508
+ const picked = pickRelevant(text, prompt);
509
+ if (picked) {
510
+ const output2 = `[Most relevant excerpt from ${input.url} for "${prompt}" (${text.length} characters total). Full page: fetch without a prompt.]
511
+ ${picked}`;
512
+ return { output: output2, display: withCost(output2, `${t("focused excerpt (local)")}${via}`) };
325
513
  }
514
+ }
515
+ const offset = input.offset ?? 0;
516
+ if (offset >= text.length && text.length > 0) {
517
+ return { output: `offset ${offset} is past the end of the page (${text.length} characters).`, isError: true };
518
+ }
519
+ const end = Math.min(offset + MAX_CHARS, text.length);
520
+ let output = text.slice(offset, end);
521
+ if (end < text.length || truncated) {
522
+ const total = truncated ? `${text.length}+` : text.length;
523
+ output += `
524
+ \u2026 [truncated: characters ${offset}-${end} of ${total}. Continue with offset=${end}, or use prompt for a focused excerpt.]`;
525
+ }
526
+ const size = truncated ? t("over 5 MB, truncated") : t("{n} characters", { n: text.length });
527
+ return { output, display: withCost(output, `${size}${via}`) };
528
+ }
529
+ });
530
+ }
531
+ async function extractWithKeenable(endpoint, url, prompt, signal) {
532
+ try {
533
+ const u = new URL(endpoint);
534
+ u.searchParams.set("url", url);
535
+ u.searchParams.set("prompt", prompt);
536
+ u.searchParams.set("live", "true");
537
+ u.searchParams.set("max_chars", String(MAX_CHARS));
538
+ const res = await fetch(u, { headers: { "X-Keenable-Title": "BotConnector" }, signal: AbortSignal.any([signal, AbortSignal.timeout(25e3)]) });
539
+ if (!res.ok) return void 0;
540
+ const content = (await res.json()).content;
541
+ return typeof content === "string" && content.trim() ? content.slice(0, MAX_CHARS) : void 0;
542
+ } catch {
543
+ return void 0;
544
+ }
545
+ }
546
+ async function loadPage(inputUrl, signal) {
547
+ let url = new URL(inputUrl);
548
+ let res;
549
+ try {
550
+ for (let hop = 0; hop < 5; hop++) {
551
+ res = await fetch(url, { headers: { accept: ACCEPT }, signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)]), redirect: "manual" });
552
+ const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
553
+ if (!location) break;
554
+ const next = new URL(location, url);
555
+ if (next.host !== url.host) {
326
556
  await res.body?.cancel();
327
- url = next;
557
+ return { error: `Redirect to another host: ${next.href}. Call fetch again with that URL if needed (permission will be requested).` };
328
558
  }
329
- } catch (error) {
330
- return { output: `Gagal mengambil ${input.url}: ${error.message}`, isError: true };
559
+ await res.body?.cancel();
560
+ url = next;
331
561
  }
332
- if (!res) return { output: `Gagal mengambil ${input.url}`, isError: true };
333
- const { text: body, truncated } = await readCapped(res);
334
- if (!res.ok) return { output: `HTTP ${res.status} dari ${input.url}: ${body.slice(0, 500)}`, isError: true };
335
- const text = (res.headers.get("content-type") ?? "").includes("html") ? htmlToText(body) : body;
336
- const clipped = text.length > MAX_CHARS || truncated ? `${text.slice(0, MAX_CHARS)}
337
- \u2026 [dipotong]` : text;
338
- const display = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
339
- return { output: clipped, display };
562
+ } catch (error) {
563
+ return { error: `Failed to fetch ${inputUrl}: ${describeFetchError(error)}` };
340
564
  }
341
- });
565
+ if (!res) return { error: `Failed to fetch ${inputUrl}` };
566
+ const { text: body, truncated } = await readCapped(res);
567
+ const type = res.headers.get("content-type") ?? "";
568
+ const text = type.includes("html") ? htmlToText(body) : body;
569
+ if (!res.ok) return { error: `HTTP ${res.status} from ${inputUrl}: ${text.slice(0, 500)}` };
570
+ return { text, truncated, at: Date.now() };
571
+ }
572
+ var fetchTool = createFetchTool();
342
573
 
343
574
  // src/tools/glob.ts
344
- import fg from "fast-glob";
575
+ import { glob as nativeGlob } from "fs/promises";
345
576
  import { z as z4 } from "zod";
577
+
578
+ // src/abort.ts
579
+ var ABORTED = /* @__PURE__ */ Symbol("aborted");
580
+ function raceAbort(promise, signal) {
581
+ if (signal.aborted) return Promise.resolve(ABORTED);
582
+ return new Promise((resolve2, reject) => {
583
+ const onAbort = () => resolve2(ABORTED);
584
+ signal.addEventListener("abort", onAbort, { once: true });
585
+ promise.then(
586
+ (value) => {
587
+ signal.removeEventListener("abort", onAbort);
588
+ resolve2(value);
589
+ },
590
+ (error) => {
591
+ signal.removeEventListener("abort", onAbort);
592
+ reject(error);
593
+ }
594
+ );
595
+ });
596
+ }
597
+ async function collectUntilAbort(iterable, signal, onItem) {
598
+ const iterator = iterable[Symbol.asyncIterator]();
599
+ try {
600
+ for (; ; ) {
601
+ const next = signal ? await raceAbort(iterator.next(), signal) : await iterator.next();
602
+ if (next === ABORTED) return false;
603
+ if (next.done) return true;
604
+ onItem(next.value);
605
+ }
606
+ } finally {
607
+ Promise.resolve(iterator.return?.(void 0)).catch(() => {
608
+ });
609
+ }
610
+ }
611
+
612
+ // src/tools/glob.ts
613
+ var MAX_OUTPUT_CHARS = 24e3;
346
614
  var globTool = defineTool({
347
615
  name: "glob",
348
616
  description: 'Find files by glob pattern, e.g. "src/**/*.ts". Respects .gitignore. Max 500 results.',
@@ -353,12 +621,31 @@ var globTool = defineTool({
353
621
  kind: "read",
354
622
  target: (input) => input.pattern,
355
623
  async run(input, ctx) {
624
+ const invalid = validateGlobPattern(input.pattern);
625
+ if (invalid) return { output: invalid, isError: true };
356
626
  const root = resolvePath(ctx.cwd, input.path ?? ".");
357
- const files = (await fg(input.pattern, { cwd: root, ignore: ignorePatterns(root), onlyFiles: true, dot: true })).sort();
358
- if (!files.length) return { output: "Tidak ada file yang cocok." };
359
- const shown = files.slice(0, 500);
360
- const more = files.length > shown.length ? `
361
- \u2026 ${files.length - shown.length} file lagi` : "";
627
+ const files = [];
628
+ const finished = await collectUntilAbort(
629
+ nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root) }),
630
+ ctx.signal,
631
+ (file) => files.push(normalizeGlobPath(file))
632
+ );
633
+ if (!finished) return { output: "Cancelled by the user.", isError: true };
634
+ files.sort();
635
+ if (!files.length) return { output: "No matching files." };
636
+ const first = files.slice(0, 500);
637
+ const shown = [];
638
+ let size = 0;
639
+ for (const file of first) {
640
+ const extra = file.length + (shown.length ? 1 : 0);
641
+ if (shown.length && size + extra > MAX_OUTPUT_CHARS) break;
642
+ shown.push(file);
643
+ size += extra;
644
+ if (size >= MAX_OUTPUT_CHARS) break;
645
+ }
646
+ const omitted = files.length - shown.length;
647
+ const more = omitted > 0 ? `
648
+ \u2026 ${omitted} more files omitted (narrow pattern/path)` : "";
362
649
  return { output: shown.join("\n") + more, display: `${files.length} file` };
363
650
  }
364
651
  });
@@ -366,14 +653,22 @@ var globTool = defineTool({
366
653
  // src/tools/grep.ts
367
654
  import { execFile } from "child_process";
368
655
  import { readFile as readFile2 } from "fs/promises";
369
- import fg2 from "fast-glob";
656
+ import { glob as nativeGlob2 } from "fs/promises";
370
657
  import { z as z5 } from "zod";
371
658
  var MAX_MATCHES = 200;
659
+ var MAX_OUTPUT_CHARS2 = 24e3;
372
660
  async function grepJs(pattern, opts) {
373
661
  const regex = new RegExp(pattern, opts.ignoreCase ? "i" : "");
374
- const files = await fg2(opts.glob ?? "**/*", { cwd: opts.root, ignore: ignorePatterns(opts.root), onlyFiles: true, dot: true });
662
+ const files = [];
663
+ const finished = await collectUntilAbort(
664
+ nativeGlob2(opts.glob ?? "**/*", { cwd: opts.root, exclude: ignorePatterns(opts.root) }),
665
+ opts.signal,
666
+ (file) => files.push(normalizeGlobPath(file))
667
+ );
375
668
  const out = [];
669
+ if (!finished) return out;
376
670
  for (const file of files.sort()) {
671
+ if (opts.signal?.aborted) break;
377
672
  let text;
378
673
  try {
379
674
  const buffer = await readFile2(resolvePath(opts.root, file));
@@ -396,7 +691,8 @@ function grepRg(pattern, opts) {
396
691
  if (opts.glob) args.push("-g", opts.glob);
397
692
  args.push(".");
398
693
  return new Promise((resolve2) => {
399
- execFile("rg", args, { cwd: opts.root, maxBuffer: 10 * 1024 * 1024 }, (error, stdout) => {
694
+ execFile("rg", args, { cwd: opts.root, maxBuffer: 10 * 1024 * 1024, signal: opts.signal }, (error, stdout) => {
695
+ if (opts.signal?.aborted) return resolve2([]);
400
696
  const code = error?.code;
401
697
  if (code === "ENOENT") return resolve2(void 0);
402
698
  if (error && error.code !== 1) return resolve2(void 0);
@@ -416,15 +712,32 @@ var grepTool = defineTool({
416
712
  kind: "read",
417
713
  target: (input) => input.pattern,
418
714
  async run(input, ctx) {
419
- const opts = { root: resolvePath(ctx.cwd, input.path ?? "."), glob: input.glob, ignoreCase: input.ignore_case };
715
+ if (input.glob) {
716
+ const invalid = validateGlobPattern(input.glob);
717
+ if (invalid) return { output: invalid, isError: true };
718
+ }
719
+ const opts = { root: resolvePath(ctx.cwd, input.path ?? "."), glob: input.glob, ignoreCase: input.ignore_case, signal: ctx.signal };
420
720
  try {
421
721
  new RegExp(input.pattern);
422
722
  } catch (error) {
423
- return { output: `Regex tidak valid: ${error.message}`, isError: true };
723
+ return { output: `Invalid regex: ${error.message}`, isError: true };
424
724
  }
425
725
  const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
426
- if (!matches.length) return { output: "Tidak ada yang cocok." };
427
- return { output: matches.join("\n"), display: `${matches.length} hasil` };
726
+ if (ctx.signal.aborted) return { output: "Cancelled by the user.", isError: true };
727
+ if (!matches.length) return { output: "No matches." };
728
+ const shown = [];
729
+ let size = 0;
730
+ for (const match of matches) {
731
+ const extra = match.length + (shown.length ? 1 : 0);
732
+ if (shown.length && size + extra > MAX_OUTPUT_CHARS2) break;
733
+ shown.push(match);
734
+ size += extra;
735
+ if (size >= MAX_OUTPUT_CHARS2) break;
736
+ }
737
+ const omitted = matches.length - shown.length;
738
+ const suffix = omitted > 0 ? `
739
+ \u2026 ${omitted} more matches omitted (narrow pattern/path/glob)` : "";
740
+ return { output: shown.join("\n") + suffix, display: t("{n} results", { n: matches.length }) };
428
741
  }
429
742
  });
430
743
 
@@ -432,6 +745,7 @@ var grepTool = defineTool({
432
745
  import { readFile as readFile3 } from "fs/promises";
433
746
  import { z as z6 } from "zod";
434
747
  var MAX_LINE = 2e3;
748
+ var MAX_OUTPUT_CHARS3 = 4e4;
435
749
  var readTool = defineTool({
436
750
  name: "read",
437
751
  description: "Read a text file. Returns numbered lines (default first 2000). Use offset/limit for large files. Always read a file before editing it.",
@@ -448,25 +762,37 @@ var readTool = defineTool({
448
762
  try {
449
763
  buffer = await readFile3(abs);
450
764
  } catch (error) {
451
- return { output: `Tidak bisa membaca ${input.path}: ${errorCode(error)}`, isError: true };
765
+ return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
452
766
  }
453
767
  if (buffer.subarray(0, 8e3).includes(0)) {
454
- return { output: `${input.path} adalah file biner, tidak dibaca.`, isError: true };
768
+ return { output: `${input.path} is a binary file; not read.`, isError: true };
455
769
  }
456
770
  const lines = buffer.toString("utf8").split(/\r?\n/);
457
771
  const start = (input.offset ?? 1) - 1;
458
772
  const limit = input.limit ?? 2e3;
459
- const slice = lines.slice(start, start + limit);
460
- const body = slice.map((line, i) => {
461
- const text = line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}\u2026 [baris dipotong]` : line;
462
- return `${String(start + i + 1).padStart(6)} ${text}`;
463
- }).join("\n");
464
- const remaining = lines.length - start - slice.length;
773
+ const requested = lines.slice(start, start + limit);
774
+ const shown = [];
775
+ let size = 0;
776
+ for (let i = 0; i < requested.length; i++) {
777
+ const line = requested[i];
778
+ const text = line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}\u2026 [line truncated]` : line;
779
+ const rendered = `${String(start + i + 1).padStart(6)} ${text}`;
780
+ const extra = rendered.length + (shown.length ? 1 : 0);
781
+ if (shown.length && size + extra > MAX_OUTPUT_CHARS3) break;
782
+ shown.push(rendered);
783
+ size += extra;
784
+ if (size >= MAX_OUTPUT_CHARS3) break;
785
+ }
786
+ const body = shown.join("\n");
787
+ const remaining = lines.length - start - shown.length;
788
+ const tokenBudgetHit = shown.length < requested.length;
789
+ const nextOffset = start + shown.length + 1;
465
790
  ctx.readFiles.add(abs);
466
791
  return {
467
- output: remaining > 0 ? `${body}
468
- \u2026 ${remaining} baris lagi (pakai offset)` : body,
469
- display: `${slice.length} baris`
792
+ output: remaining > 0 ? tokenBudgetHit ? `${body}
793
+ \u2026 ${remaining} more lines (token budget reached; continue with offset=${nextOffset})` : `${body}
794
+ \u2026 ${remaining} more lines (use offset)` : body,
795
+ display: t("{n} lines", { n: shown.length })
470
796
  };
471
797
  }
472
798
  });
@@ -487,7 +813,7 @@ var writeTool = defineTool({
487
813
  target: (input) => input.path,
488
814
  async validate(input, ctx) {
489
815
  const abs = resolvePath(ctx.cwd, input.path);
490
- return existsSync(abs) && !ctx.readFiles.has(abs) ? `${input.path} sudah ada. Baca dulu dengan read sebelum menimpanya.` : void 0;
816
+ return existsSync(abs) && !ctx.readFiles.has(abs) ? `${input.path} already exists. Read it with read before overwriting it.` : void 0;
491
817
  },
492
818
  async preview(input, ctx) {
493
819
  const abs = resolvePath(ctx.cwd, input.path);
@@ -497,18 +823,18 @@ var writeTool = defineTool({
497
823
  async run(input, ctx) {
498
824
  const abs = resolvePath(ctx.cwd, input.path);
499
825
  if (existsSync(abs) && !ctx.readFiles.has(abs)) {
500
- return { output: `${input.path} sudah ada. Baca dulu dengan read sebelum menimpanya.`, isError: true };
826
+ return { output: `${input.path} already exists. Read it with read before overwriting it.`, isError: true };
501
827
  }
502
828
  try {
503
829
  await ctx.checkpoint?.(abs);
504
830
  await mkdir(dirname(abs), { recursive: true });
505
831
  await writeFile2(abs, input.content);
506
832
  } catch (error) {
507
- return { output: `Gagal menulis ${input.path}: ${errorCode(error)}`, isError: true };
833
+ return { output: `Failed to write ${input.path}: ${errorCode(error)}`, isError: true };
508
834
  }
509
835
  ctx.readFiles.add(abs);
510
836
  const lines = input.content.split("\n").length;
511
- return { output: `Menulis ${input.path} (${lines} baris).`, display: `${lines} baris ditulis` };
837
+ return { output: `Wrote ${input.path} (${lines} lines).`, display: t("{lines} lines written", { lines }) };
512
838
  }
513
839
  });
514
840
 
@@ -517,76 +843,86 @@ var ALL_TOOLS = [readTool, writeTool, editTool, bashTool, grepTool, globTool, fe
517
843
  function toolDefinitions(tools) {
518
844
  return tools.map((tool) => {
519
845
  const { $schema: _ignored, ...parameters } = tool.jsonSchema ?? z8.toJSONSchema(tool.schema);
520
- return { type: "function", function: { name: tool.name, description: tool.description, parameters } };
846
+ return { type: "function", function: { name: tool.name, description: tool.description, parameters, ...tool.outputJsonSchema ? { output_schema: tool.outputJsonSchema } : {} } };
521
847
  });
522
848
  }
523
849
 
524
- // src/textToolCalls.ts
525
- var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
526
- var CALL_START = /<tool_call>|\n?[^\n]*\bTool call list\b[^\n]*|\n\s*\d+\.\s*Tool:/g;
527
- function coerce(value, type) {
528
- const types = Array.isArray(type) ? type : [type];
529
- const v = value.trim();
530
- if ((types.includes("integer") || types.includes("number")) && v !== "" && !Number.isNaN(Number(v))) return Number(v);
531
- if (types.includes("boolean") && /^(true|false)$/i.test(v)) return v.toLowerCase() === "true";
532
- if ((types.includes("object") || types.includes("array")) && /^[[{]/.test(v)) {
533
- try {
534
- return JSON.parse(v);
535
- } catch {
850
+ // src/webBudget.ts
851
+ var KEEP_TOKENS = 4e4;
852
+ var PRUNE_MIN_TOKENS = 2e4;
853
+ var STUB = "[old tool result removed:";
854
+ var tokens = (text) => Math.ceil(text.length / 4);
855
+ var isStub = (content) => content.startsWith(STUB);
856
+ function pruneOldFetches(messages) {
857
+ const urls = /* @__PURE__ */ new Map();
858
+ for (const m of messages) {
859
+ if (m.role !== "assistant") continue;
860
+ for (const c of m.tool_calls ?? []) {
861
+ if (c.function.name !== "fetch") continue;
862
+ try {
863
+ urls.set(c.id, String(JSON.parse(c.function.arguments).url ?? ""));
864
+ } catch {
865
+ urls.set(c.id, "");
866
+ }
536
867
  }
537
868
  }
538
- return value.replace(/^\n+|\n+$/g, "");
539
- }
540
- function pickTool(name, keys, tools) {
541
- const named = name && tools.find((t) => t.function.name === name);
542
- if (named) return named;
543
- let best;
544
- let bestScore = 0;
545
- let tie = false;
546
- for (const t of tools) {
547
- const params = t.function.parameters;
548
- const props = Object.keys(params.properties ?? {});
549
- if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
550
- const score = keys.filter((k) => props.includes(k)).length;
551
- if (score > bestScore) [best, bestScore, tie] = [t, score, false];
552
- else if (score === bestScore && score > 0) tie = true;
869
+ let kept = 0;
870
+ let oldTokens = 0;
871
+ const old = [];
872
+ for (let i = messages.length - 1; i >= 0; i--) {
873
+ const m = messages[i];
874
+ if (m.role !== "tool" || !urls.has(m.tool_call_id) || isStub(m.content)) continue;
875
+ kept += tokens(m.content);
876
+ if (kept > KEEP_TOKENS) {
877
+ old.push(i);
878
+ oldTokens += tokens(m.content);
879
+ }
553
880
  }
554
- return tie ? void 0 : best;
881
+ if (oldTokens <= PRUNE_MIN_TOKENS) return false;
882
+ for (const i of old) {
883
+ const m = messages[i];
884
+ messages[i] = { ...m, content: `${STUB} fetch ${urls.get(m.tool_call_id)}. Fetch it again (use prompt for a focused excerpt) if still needed.]` };
885
+ }
886
+ return true;
555
887
  }
556
- function recoverTextToolCalls(text, tools) {
557
- if (!/<\/arg_value>|<tool_call>/.test(text)) return null;
558
- const starts = [...text.matchAll(CALL_START)].map((m) => m.index ?? 0);
559
- const firstPair = text.search(PAIR);
560
- const cut = Math.min(...[...starts, firstPair].filter((i) => i >= 0));
561
- const bounds = starts.filter((i) => i >= cut);
562
- if (!bounds.length || bounds[0] > cut) bounds.unshift(cut);
563
- const toolCalls = [];
564
- bounds.forEach((start, n) => {
565
- const segment = text.slice(start, bounds[n + 1] ?? text.length);
566
- const name = /<tool_call>\s*([A-Za-z_][\w.-]*)/.exec(segment)?.[1] ?? /Tool:\s*`?([A-Za-z_][\w.-]*)`?/.exec(segment)?.[1];
567
- let args = [];
568
- const flush = () => {
569
- if (!args.length) return;
570
- const keys = args.map(([k]) => k);
571
- const tool = pickTool(name, keys, tools);
572
- if (tool) {
573
- const props = tool.function.parameters.properties ?? {};
574
- const input = Object.fromEntries(args.filter(([k]) => k in props).map(([k, v]) => [k, coerce(v, props[k].type)]));
575
- toolCalls.push({ id: `text_call_${toolCalls.length}`, name: tool.function.name, arguments: JSON.stringify(input) });
576
- }
577
- args = [];
578
- };
579
- for (const m of segment.matchAll(PAIR)) {
580
- if (args.some(([k]) => k === m[1])) flush();
581
- args.push([m[1], m[2]]);
888
+ var WebBudget = class {
889
+ fetches = 0;
890
+ chars = 0;
891
+ perTurn;
892
+ sessionChars;
893
+ constructor(limits = {}) {
894
+ this.perTurn = limits.perTurn ?? 8;
895
+ this.sessionChars = limits.sessionChars ?? 3e5;
896
+ }
897
+ startTurn() {
898
+ this.fetches = 0;
899
+ }
900
+ /** Gives back a reservation, e.g. when the fetch was served from cache and cost nothing. */
901
+ refund() {
902
+ if (this.fetches > 0) this.fetches--;
903
+ }
904
+ reset() {
905
+ this.fetches = 0;
906
+ this.chars = 0;
907
+ }
908
+ /** Reserves one fetch; returns the refusal to show the model when over budget. */
909
+ take() {
910
+ if (this.fetches >= this.perTurn) {
911
+ return `Limit of ${this.perTurn} fetches per question reached (each fetch uses the user's tokens). Answer from the results you already have; if that is not enough, tell the user what is still missing.`;
582
912
  }
583
- flush();
584
- });
585
- if (!toolCalls.length) return null;
586
- return { text: text.slice(0, cut).trim(), toolCalls };
587
- }
913
+ if (this.chars >= this.sessionChars) {
914
+ return `Total web page limit for this session reached (~${Math.round(this.sessionChars / 4e3)}k tokens). Answer from the results you already have and ask the user whether to search more.`;
915
+ }
916
+ this.fetches++;
917
+ return void 0;
918
+ }
919
+ record(chars) {
920
+ this.chars += chars;
921
+ }
922
+ };
588
923
 
589
924
  // src/agent.ts
925
+ var CANCELLED = "Cancelled by the user.";
590
926
  var estimateTokens = (text) => Math.ceil(text.length / 4);
591
927
  var COMPACT_PROMPT = "Summarize the conversation so far for yourself so you can continue the work with no other context. Include: the user goals, decisions made, files touched and their current state, commands run and results, the task in progress and the next step. Be concise but complete.";
592
928
  var Agent = class {
@@ -602,6 +938,7 @@ var Agent = class {
602
938
  this.maxSteps = opts.maxSteps ?? 50;
603
939
  this.contextWindow = opts.contextWindow ?? 128e3;
604
940
  this.systemPrompt = opts.systemPrompt;
941
+ this.reasoning = opts.reasoning ?? "auto";
605
942
  }
606
943
  opts;
607
944
  provider;
@@ -611,6 +948,7 @@ var Agent = class {
611
948
  lastInputTokens = 0;
612
949
  onEvent = () => {
613
950
  };
951
+ reasoning;
614
952
  askPermission = async () => "no";
615
953
  tools;
616
954
  definitions;
@@ -618,6 +956,7 @@ var Agent = class {
618
956
  turnTools;
619
957
  turnDefinitions;
620
958
  readFiles = /* @__PURE__ */ new Set();
959
+ webBudget = new WebBudget();
621
960
  maxSteps;
622
961
  // ponytail: one window for every model; read it from /v1/models metadata when providers expose it.
623
962
  contextWindow;
@@ -630,7 +969,7 @@ var Agent = class {
630
969
  this.definitions = toolDefinitions(tools);
631
970
  }
632
971
  isParallelSafe(call) {
633
- const tool = this.turnTools.find((t) => t.name === call.name);
972
+ const tool = this.turnTools.find((t2) => t2.name === call.name);
634
973
  if (!tool?.parallelSafe) return false;
635
974
  try {
636
975
  const parsed = tool.schema.safeParse(JSON.parse(call.arguments || "{}"));
@@ -647,11 +986,13 @@ var Agent = class {
647
986
  load(messages) {
648
987
  this.messages = messages;
649
988
  this.readFiles.clear();
989
+ this.webBudget.reset();
650
990
  this.lastInputTokens = 0;
651
991
  }
652
992
  clear() {
653
993
  this.messages = [];
654
994
  this.readFiles.clear();
995
+ this.webBudget.reset();
655
996
  this.lastInputTokens = 0;
656
997
  this.opts.onReset?.();
657
998
  }
@@ -660,31 +1001,47 @@ var Agent = class {
660
1001
  messages: [{ role: "system", content: this.systemPrompt }, ...this.messages, { role: "user", content: COMPACT_PROMPT }],
661
1002
  // Some gateways reject tool_calls in history when no tools are declared.
662
1003
  tools: this.definitions,
663
- signal
1004
+ signal,
1005
+ reasoning: this.reasoning
664
1006
  });
665
1007
  this.clear();
666
- this.push({ role: "user", content: `Ringkasan percakapan sebelumnya:
1008
+ this.push({ role: "user", content: `Summary of the previous conversation:
667
1009
  ${completion.text}` });
668
- this.push({ role: "assistant", content: "Oke, saya lanjutkan dari ringkasan ini." });
1010
+ this.push({ role: "assistant", content: "OK, I will continue from this summary." });
669
1011
  this.onEvent({ type: "compacted" });
670
1012
  }
1013
+ /**
1014
+ * Events of one tool execution. A cancelled turn gives up on a tool that does not stop, and whatever that tool
1015
+ * reports later must not reach the screen or a newer turn, so this is per execution and not per call id (ids repeat).
1016
+ */
1017
+ report(ticket, event) {
1018
+ if (ticket.abandoned) return;
1019
+ if (event.type === "toolStart") ticket.open = true;
1020
+ if (event.type === "toolEnd") ticket.open = false;
1021
+ this.onEvent(event);
1022
+ }
671
1023
  async run(text, signal) {
672
1024
  this.turnTools = this.tools;
673
1025
  this.turnDefinitions = this.definitions;
674
1026
  this.opts.onTurnStart?.();
1027
+ this.webBudget.startTurn();
675
1028
  try {
676
- if (this.lastInputTokens > this.contextWindow * 0.8) await this.compact(signal);
1029
+ if (this.lastInputTokens > this.contextWindow * 0.8 && !hasPendingProgrammaticReplay(this.messages)) await this.compact(signal);
677
1030
  this.push({ role: "user", content: text });
678
1031
  for (let step = 0; step < this.maxSteps; step++) {
679
- if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8) {
1032
+ if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8 && !hasPendingProgrammaticReplay(this.messages)) {
680
1033
  await this.compact(signal);
681
- this.push({ role: "user", content: `Lanjutkan tugas ini sesuai ringkasan di atas: ${text}` });
1034
+ this.push({ role: "user", content: `Continue this task using the summary above: ${text}` });
682
1035
  }
1036
+ pruneOldFetches(this.messages);
683
1037
  let completion = await this.provider.chat({
684
1038
  messages: [{ role: "system", content: this.systemPrompt }, ...this.messages],
685
1039
  tools: this.turnDefinitions,
1040
+ programmaticToolNames: this.turnTools.filter((tool) => tool.programmaticSafe).map((tool) => tool.name),
686
1041
  signal,
687
- onText: (delta) => this.onEvent({ type: "text", delta })
1042
+ onText: (delta) => this.onEvent({ type: "text", delta }),
1043
+ onThinking: (delta) => this.onEvent({ type: "thinking", delta }),
1044
+ reasoning: this.reasoning
688
1045
  });
689
1046
  if (!completion.toolCalls.length && completion.finishReason !== "repetition") {
690
1047
  const recovered = recoverTextToolCalls(completion.text, this.turnDefinitions);
@@ -701,26 +1058,17 @@ ${completion.text}` });
701
1058
  this.totalUsage.outputTokens += usage.outputTokens;
702
1059
  this.lastInputTokens = usage.inputTokens;
703
1060
  this.onEvent({ type: "usage", ...this.totalUsage });
704
- this.push({
705
- role: "assistant",
706
- content: completion.text || null,
707
- ...completion.toolCalls.length ? {
708
- tool_calls: completion.toolCalls.map((c) => ({
709
- id: c.id,
710
- type: "function",
711
- function: { name: c.name, arguments: c.arguments }
712
- }))
713
- } : {}
714
- });
1061
+ this.push(completionAssistantMessage(completion));
715
1062
  if (completion.finishReason === "repetition") {
716
1063
  this.onEvent({ type: "textReplace", text: completion.text });
717
1064
  this.onEvent({
718
1065
  type: "error",
719
- message: "Model terjebak mengulang teks, jawaban dihentikan. Coba ulangi atau ganti model dengan /model."
1066
+ message: t("The model got stuck repeating text, so the answer was stopped. Try again or switch models with /model.")
720
1067
  });
721
1068
  return;
722
1069
  }
723
1070
  if (!completion.toolCalls.length) {
1071
+ if (completion.finishReason === "continue") continue;
724
1072
  this.onEvent({ type: "done" });
725
1073
  return;
726
1074
  }
@@ -728,16 +1076,35 @@ ${completion.text}` });
728
1076
  let i = 0;
729
1077
  while (i < calls.length) {
730
1078
  if (signal.aborted) {
731
- for (const pending of calls.slice(i)) this.push({ role: "tool", tool_call_id: pending.id, content: "Dibatalkan oleh user." });
1079
+ this.cancelCalls(calls.slice(i));
732
1080
  this.onEvent({ type: "aborted" });
733
1081
  return;
734
1082
  }
735
1083
  let j = i + 1;
736
1084
  if (this.isParallelSafe(calls[i])) while (j < calls.length && this.isParallelSafe(calls[j])) j++;
737
1085
  const batch = calls.slice(i, j);
738
- const results = await Promise.all(batch.map((c) => this.runTool(c, signal)));
1086
+ const tickets = batch.map(() => ({ abandoned: false, open: false }));
1087
+ const running = Promise.all(batch.map((c, k) => this.runTool(c, signal, tickets[k])));
1088
+ const outcome = await raceAbort(running, signal);
1089
+ if (outcome === ABORTED) {
1090
+ batch.forEach((c, k) => {
1091
+ const ticket = tickets[k];
1092
+ ticket.abandoned = true;
1093
+ if (ticket.open) this.onEvent({ type: "toolEnd", id: c.id, tool: c.name, output: CANCELLED, isError: true });
1094
+ });
1095
+ running.catch(() => {
1096
+ });
1097
+ this.cancelCalls(calls.slice(i));
1098
+ this.onEvent({ type: "aborted" });
1099
+ return;
1100
+ }
739
1101
  batch.forEach((c, k) => {
740
- this.push({ role: "tool", tool_call_id: c.id, content: results[k] });
1102
+ this.push({
1103
+ role: "tool",
1104
+ tool_call_id: c.id,
1105
+ content: outcome[k],
1106
+ ...c.caller ? { responses_caller: c.caller } : {}
1107
+ });
741
1108
  });
742
1109
  i = j;
743
1110
  }
@@ -757,48 +1124,62 @@ ${completion.text}` });
757
1124
  this.onEvent({
758
1125
  type: "error",
759
1126
  message: noTools ? `${message}
760
- Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model.` : message
1127
+ ${t("This model may not support tool calling. Try another model with /model.")}` : message
761
1128
  });
762
1129
  }
763
1130
  }
764
- async runTool(call, signal) {
1131
+ /** Every tool call needs an answer in the history, or the next request to the model is rejected. */
1132
+ cancelCalls(calls) {
1133
+ for (const pending of calls)
1134
+ this.push({
1135
+ role: "tool",
1136
+ tool_call_id: pending.id,
1137
+ content: CANCELLED,
1138
+ ...pending.caller ? { responses_caller: pending.caller } : {}
1139
+ });
1140
+ }
1141
+ async runTool(call, signal, ticket) {
1142
+ const emit = (event) => this.report(ticket, event);
765
1143
  const fail = (output, target2 = "") => {
766
- this.onEvent({ type: "toolStart", id: call.id, tool: call.name, target: target2 });
767
- this.onEvent({ type: "toolEnd", id: call.id, tool: call.name, output, isError: true });
1144
+ emit({ type: "toolStart", id: call.id, tool: call.name, target: target2 });
1145
+ emit({ type: "toolEnd", id: call.id, tool: call.name, output, isError: true });
768
1146
  return output;
769
1147
  };
770
- const tool = this.turnTools.find((t) => t.name === call.name);
771
- if (!tool) return fail(`Alat "${call.name}" tidak ada. Alat yang tersedia: ${this.turnTools.map((t) => t.name).join(", ")}`);
1148
+ const tool = this.turnTools.find((t2) => t2.name === call.name);
1149
+ if (!tool) return fail(`Tool "${call.name}" does not exist. Available tools: ${this.turnTools.map((t2) => t2.name).join(", ")}`);
772
1150
  let args;
773
1151
  try {
774
1152
  args = JSON.parse(call.arguments || "{}");
775
1153
  } catch {
776
- return fail(`Argumen bukan JSON valid: ${call.arguments.slice(0, 200)}`);
1154
+ return fail(`Arguments are not valid JSON: ${call.arguments.slice(0, 200)}`);
777
1155
  }
778
1156
  const parsed = tool.schema.safeParse(args);
779
1157
  if (!parsed.success) {
780
- return fail(`Argumen tidak valid: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
1158
+ return fail(`Invalid arguments: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
781
1159
  }
782
1160
  const input = parsed.data;
783
1161
  const target = tool.target(input);
1162
+ const overBudget = tool.name === "fetch" ? this.webBudget.take() : void 0;
1163
+ if (overBudget) return fail(overBudget, target);
784
1164
  const ctx = {
785
1165
  cwd: this.opts.cwd,
786
1166
  signal,
787
1167
  readFiles: this.readFiles,
788
1168
  callId: call.id,
789
- emit: (event) => this.onEvent(event),
1169
+ emit,
790
1170
  ask: (req) => this.askPermission(req),
791
1171
  checkpoint: this.opts.checkpoint,
1172
+ refundFetch: () => this.webBudget.refund(),
792
1173
  addUsage: (u) => {
793
1174
  this.totalUsage.inputTokens += u.inputTokens;
794
1175
  this.totalUsage.outputTokens += u.outputTokens;
795
1176
  this.onEvent({ type: "usage", ...this.totalUsage });
796
1177
  }
797
1178
  };
798
- this.onEvent({ type: "toolStart", id: call.id, tool: tool.name, target });
1179
+ emit({ type: "toolStart", id: call.id, tool: tool.name, target });
799
1180
  const invalid = await tool.validate?.(input, ctx).catch((error) => error.message);
800
1181
  if (invalid) {
801
- this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output: invalid, isError: true });
1182
+ emit({ type: "toolEnd", id: call.id, tool: tool.name, output: invalid, isError: true });
802
1183
  return invalid;
803
1184
  }
804
1185
  const request = { tool: tool.name, kind: tool.kind, target };
@@ -816,8 +1197,8 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
816
1197
  decision = answer === "no" ? "deny" : "allow";
817
1198
  }
818
1199
  if (decision === "deny") {
819
- const output = this.permissions.mode === "plan" ? "Ditolak: mode plan hanya boleh membaca dan mencari. Susun rencana untuk user tanpa mengubah apa pun." : "User menolak menjalankan alat ini. Tanyakan ke user apa yang diinginkan, atau coba cara lain.";
820
- this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output, isError: true });
1200
+ const output = this.permissions.mode === "plan" ? "Denied: plan mode only allows reading and searching. Write the plan for the user without changing anything." : "The user declined to run this tool. Ask the user what they want, or try another approach.";
1201
+ emit({ type: "toolEnd", id: call.id, tool: tool.name, output, isError: true });
821
1202
  return output;
822
1203
  }
823
1204
  let result;
@@ -826,7 +1207,8 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
826
1207
  } catch (error) {
827
1208
  result = { output: `Error: ${error.message}`, isError: true };
828
1209
  }
829
- this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output: result.output, display: result.display, isError: !!result.isError });
1210
+ if (tool.name === "fetch" && !result.isError) this.webBudget.record(result.output.length);
1211
+ emit({ type: "toolEnd", id: call.id, tool: tool.name, output: result.output, display: result.display, isError: !!result.isError });
830
1212
  return result.output;
831
1213
  }
832
1214
  };
@@ -855,7 +1237,7 @@ var CheckpointStore = class {
855
1237
  while (this.turns() > MAX_TURNS) this.history.shift();
856
1238
  }
857
1239
  turns() {
858
- return this.history.filter((t) => t.size).length;
1240
+ return this.history.filter((t2) => t2.size).length;
859
1241
  }
860
1242
  async undo() {
861
1243
  while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
@@ -890,9 +1272,10 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
890
1272
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
891
1273
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
892
1274
  import { z as z9 } from "zod";
1275
+ var MCP_OUTPUT_MAX_CHARS = 32e3;
893
1276
  function mcpToolName(server, tool, taken) {
894
- const clean = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
895
- const base = `mcp__${clean(server)}__${clean(tool)}`.slice(0, 64);
1277
+ const clean2 = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
1278
+ const base = `mcp__${clean2(server)}__${clean2(tool)}`.slice(0, 64);
896
1279
  let name = base;
897
1280
  for (let n = 2; taken.has(name); n++) name = `${base.slice(0, 64 - String(n).length - 1)}_${n}`;
898
1281
  taken.add(name);
@@ -901,7 +1284,7 @@ function mcpToolName(server, tool, taken) {
901
1284
  function withTimeout(promise, ms) {
902
1285
  let timer;
903
1286
  const timeout = new Promise((_, reject) => {
904
- timer = setTimeout(() => reject(new Error(`timeout ${Math.round(ms / 1e3)} detik`)), ms);
1287
+ timer = setTimeout(() => reject(new Error(t("timeout after {n} seconds", { n: Math.round(ms / 1e3) }))), ms);
905
1288
  });
906
1289
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
907
1290
  }
@@ -938,7 +1321,7 @@ var McpManager = class {
938
1321
  });
939
1322
  return;
940
1323
  }
941
- entry.tools = tools.map((t) => this.wrap(spec.name, client, t));
1324
+ entry.tools = tools.map((t2) => this.wrap(spec.name, client, t2));
942
1325
  entry.state = { ...entry.state, status: "ready", tools: entry.tools.length };
943
1326
  } catch (error) {
944
1327
  entry.state = { ...entry.state, status: "error", error: error.message };
@@ -954,7 +1337,9 @@ var McpManager = class {
954
1337
  description: `[MCP ${server}] ${tool.description ?? tool.name}`.slice(0, 1024),
955
1338
  schema: z9.record(z9.string(), z9.unknown()),
956
1339
  jsonSchema: tool.inputSchema,
1340
+ ...tool.outputSchema ? { outputJsonSchema: tool.outputSchema } : {},
957
1341
  kind: "mcp",
1342
+ programmaticSafe: tool.annotations?.readOnlyHint === true,
958
1343
  target: (input) => {
959
1344
  const text = JSON.stringify(input);
960
1345
  return text.length > 80 ? `${text.slice(0, 77)}\u2026` : text;
@@ -962,9 +1347,12 @@ var McpManager = class {
962
1347
  async run(input, ctx) {
963
1348
  try {
964
1349
  const result = await client.callTool({ name: tool.name, arguments: input }, void 0, { signal: ctx.signal });
965
- const output = (result.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n") || "(kosong)";
966
- return { output: output.length > 5e4 ? `${output.slice(0, 5e4)}
967
- \u2026 [dipotong]` : output, isError: !!result.isError };
1350
+ const output = (result.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n") || "(empty)";
1351
+ return {
1352
+ output: output.length > MCP_OUTPUT_MAX_CHARS ? `${output.slice(0, MCP_OUTPUT_MAX_CHARS)}
1353
+ \u2026 [truncated at ${MCP_OUTPUT_MAX_CHARS} characters; narrow the MCP request if possible]` : output,
1354
+ isError: !!result.isError
1355
+ };
968
1356
  } catch (error) {
969
1357
  return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
970
1358
  }
@@ -975,14 +1363,14 @@ var McpManager = class {
975
1363
  const entry = this.servers.get(name);
976
1364
  if (!entry) return;
977
1365
  this.servers.delete(name);
978
- for (const t of entry.tools) this.taken.delete(t.name);
979
- if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((t) => t.name));
1366
+ for (const t2 of entry.tools) this.taken.delete(t2.name);
1367
+ if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((t2) => t2.name));
980
1368
  await entry.client?.close().catch(() => {
981
1369
  });
982
1370
  this.opts.onChange?.();
983
1371
  }
984
1372
  tools() {
985
- return [...this.servers.values()].flatMap((s) => s.tools);
1373
+ return [...this.servers.values()].flatMap((s) => s.tools).sort((a, b) => a.name.localeCompare(b.name));
986
1374
  }
987
1375
  states() {
988
1376
  return [...this.servers.values()].map((s) => s.state);
@@ -1003,15 +1391,15 @@ function createExitPlanTool(opts) {
1003
1391
  description: "Plan mode only: present the finished plan to the user for approval. If approved you may start implementing it.",
1004
1392
  schema: z10.object({ plan: z10.string().describe("The plan, in markdown") }),
1005
1393
  kind: "read",
1006
- target: () => "rencana",
1394
+ target: () => t("plan"),
1007
1395
  async run(input) {
1008
- if (opts.permissions.mode !== "plan") return { output: "exit_plan hanya dipakai di mode plan.", isError: true };
1396
+ if (opts.permissions.mode !== "plan") return { output: "exit_plan is only used in plan mode.", isError: true };
1009
1397
  const decision = await opts.interaction.approvePlan(input.plan);
1010
1398
  if (decision === "no") {
1011
- return { output: "User belum menyetujui. Tanyakan apa yang perlu diperbaiki, perbaiki rencananya, lalu panggil exit_plan lagi." };
1399
+ return { output: "The user has not approved yet. Ask what needs to change, revise the plan, then call exit_plan again." };
1012
1400
  }
1013
1401
  opts.permissions.mode = decision;
1014
- return { output: `Rencana disetujui. Mode sekarang ${decision}. Mulai kerjakan rencananya sekarang.` };
1402
+ return { output: `Plan approved. The mode is now ${decision}. Start carrying out the plan now.` };
1015
1403
  }
1016
1404
  });
1017
1405
  }
@@ -1028,7 +1416,7 @@ function createSkillTool(skills) {
1028
1416
  target: (input) => input.name,
1029
1417
  async run(input) {
1030
1418
  const skill = skills.find((s) => s.name === input.name);
1031
- if (!skill) return { output: `Skill "${input.name}" tidak ada. Tersedia: ${skills.map((s) => s.name).join(", ") || "(tidak ada)"}`, isError: true };
1419
+ if (!skill) return { output: `Skill "${input.name}" does not exist. Available: ${skills.map((s) => s.name).join(", ") || "(none)"}`, isError: true };
1032
1420
  const { body } = parseFrontmatter(readFileSync(skill.file, "utf8"));
1033
1421
  return { output: `Base directory for this skill: ${skill.dir}
1034
1422
 
@@ -1037,8 +1425,69 @@ ${body}`, display: skill.description.slice(0, 60) };
1037
1425
  });
1038
1426
  }
1039
1427
 
1040
- // src/tools/task.ts
1428
+ // src/tools/websearch.ts
1041
1429
  import { z as z12 } from "zod";
1430
+ var clean = (items, max) => (Array.isArray(items) ? items : []).filter((r) => typeof r?.url === "string" && /^https?:\/\//.test(r.url)).slice(0, max).map((r) => ({
1431
+ title: String(r.title || r.url).slice(0, 200),
1432
+ url: r.url,
1433
+ snippet: String(r.snippet || r.description || "").replace(/\s+/g, " ").trim().slice(0, 600)
1434
+ }));
1435
+ async function postJson(url, body, headers, signal) {
1436
+ const res = await fetch(url, {
1437
+ method: "POST",
1438
+ headers: { "content-type": "application/json", ...headers },
1439
+ body: JSON.stringify(body),
1440
+ signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)])
1441
+ });
1442
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1443
+ return res.json();
1444
+ }
1445
+ function createWebSearchTool(opts) {
1446
+ const keenableURL = opts.keenableURL ?? "https://api.keenable.ai/v1/search/public";
1447
+ return defineTool({
1448
+ name: "web_search",
1449
+ description: "Search the web. Returns titles, URLs and short snippets. Use it to find the right pages instead of guessing URLs; then fetch a result URL when you need the full page.",
1450
+ schema: z12.object({
1451
+ query: z12.string().min(1).max(500).describe("Search query"),
1452
+ max_results: z12.number().int().min(1).max(10).optional().describe("Number of results (default 6)")
1453
+ }),
1454
+ kind: "read",
1455
+ target: (input) => input.query,
1456
+ parallelSafe: () => true,
1457
+ async run(input, ctx) {
1458
+ const max = input.max_results ?? 6;
1459
+ const errors = [];
1460
+ let results = [];
1461
+ const bc = opts.botconnector();
1462
+ if (bc) {
1463
+ try {
1464
+ const data = await postJson(`${bc.baseURL.replace(/\/+$/, "")}/web/search`, { query: input.query, max_results: max }, { authorization: `Bearer ${bc.apiKey}` }, ctx.signal);
1465
+ results = clean(data.results, max);
1466
+ } catch (error) {
1467
+ errors.push(`BotConnector: ${error.message}`);
1468
+ }
1469
+ }
1470
+ if (!results.length) {
1471
+ try {
1472
+ const data = await postJson(keenableURL, { query: input.query }, { "x-keenable-title": "BotConnector" }, ctx.signal);
1473
+ results = clean(data.results, max);
1474
+ } catch (error) {
1475
+ errors.push(`Keenable: ${error.message}`);
1476
+ }
1477
+ }
1478
+ if (!results.length) {
1479
+ return { output: errors.length ? `Web search failed (${errors.join("; ")}).` : "No results.", isError: errors.length > 0 };
1480
+ }
1481
+ const output = results.map((r, i) => `${i + 1}. ${r.title}
1482
+ ${r.url}${r.snippet ? `
1483
+ ${r.snippet}` : ""}`).join("\n");
1484
+ return { output, display: t("{n} results", { n: results.length }) };
1485
+ }
1486
+ });
1487
+ }
1488
+
1489
+ // src/tools/task.ts
1490
+ import { z as z13 } from "zod";
1042
1491
  function createTaskTool(opts) {
1043
1492
  const byName = new Map(opts.agents.map((a) => [a.name, a]));
1044
1493
  const list = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
@@ -1046,10 +1495,10 @@ function createTaskTool(opts) {
1046
1495
  name: "task",
1047
1496
  description: `Delegate a self-contained sub-task to a subagent. It starts with an empty context and returns only its final report. Available agents:
1048
1497
  ${list}`,
1049
- schema: z12.object({
1050
- agent: z12.string().describe("Agent name"),
1051
- description: z12.string().describe("3-6 word summary shown to the user"),
1052
- prompt: z12.string().describe("Full instructions; the subagent sees nothing else")
1498
+ schema: z13.object({
1499
+ agent: z13.string().describe("Agent name"),
1500
+ description: z13.string().describe("3-6 word summary shown to the user"),
1501
+ prompt: z13.string().describe("Full instructions; the subagent sees nothing else")
1053
1502
  }),
1054
1503
  kind: "read",
1055
1504
  target: (input) => `[${input.agent}] ${input.description}`,
@@ -1058,16 +1507,16 @@ ${list}`,
1058
1507
  const allowed = byName.get(input.agent)?.tools;
1059
1508
  if (!allowed) return false;
1060
1509
  const base = opts.baseTools();
1061
- return allowed.every((name) => base.find((t) => t.name === name)?.kind === "read");
1510
+ return allowed.every((name) => base.find((t2) => t2.name === name)?.kind === "read");
1062
1511
  },
1063
1512
  async run(input, ctx) {
1064
1513
  const def = byName.get(input.agent);
1065
- if (!def) return { output: `Agent "${input.agent}" tidak ada. Tersedia: ${[...byName.keys()].join(", ")}`, isError: true };
1066
- const base = opts.baseTools().filter((t) => t.name !== "task" && t.name !== "exit_plan");
1067
- const unknown = (def.tools ?? []).filter((n) => !base.some((t) => t.name === n));
1068
- if (unknown.length) return { output: `Agent "${def.name}": alat tidak dikenal: ${unknown.join(", ")}`, isError: true };
1514
+ if (!def) return { output: `Agent "${input.agent}" does not exist. Available: ${[...byName.keys()].join(", ")}`, isError: true };
1515
+ const base = opts.baseTools().filter((t2) => t2.name !== "task" && t2.name !== "exit_plan");
1516
+ const unknown = (def.tools ?? []).filter((n) => !base.some((t2) => t2.name === n));
1517
+ if (unknown.length) return { output: `Agent "${def.name}": unknown tools: ${unknown.join(", ")}`, isError: true };
1069
1518
  const allowed = def.tools;
1070
- const tools = allowed ? base.filter((t) => allowed.includes(t.name)) : base;
1519
+ const tools = allowed ? base.filter((t2) => allowed.includes(t2.name)) : base;
1071
1520
  let provider;
1072
1521
  try {
1073
1522
  provider = def.model ? opts.providerFor(def.model) : opts.provider();
@@ -1085,6 +1534,7 @@ ${def.prompt}`,
1085
1534
  cwd: opts.cwd,
1086
1535
  maxSteps: def.maxSteps ?? 50,
1087
1536
  label: def.name,
1537
+ reasoning: opts.reasoning?.() ?? "auto",
1088
1538
  // Subagent edits belong to the parent's turn so /undo reverts them too.
1089
1539
  checkpoint: ctx.checkpoint
1090
1540
  });
@@ -1098,22 +1548,22 @@ ${def.prompt}`,
1098
1548
  if (ctx.ask) child.askPermission = ctx.ask;
1099
1549
  await child.run(input.prompt, ctx.signal);
1100
1550
  ctx.addUsage?.(child.totalUsage);
1101
- const tokens = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
1102
- const display = `${steps} langkah \xB7 ${tokens >= 1e3 ? `${Math.round(tokens / 1e3)}k` : tokens} token`;
1103
- if (outcome?.type === "error") return { output: `Subagent ${def.name} gagal: ${outcome.message}`, isError: true, display };
1104
- if (outcome?.type === "aborted") return { output: `Subagent ${def.name} dibatalkan.`, isError: true, display };
1551
+ const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
1552
+ const display = t("{steps} steps \xB7 {tokens} tokens", { steps, tokens: tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2 });
1553
+ if (outcome?.type === "error") return { output: `Subagent ${def.name} failed: ${outcome.message}`, isError: true, display };
1554
+ if (outcome?.type === "aborted") return { output: `Subagent ${def.name} was cancelled.`, isError: true, display };
1105
1555
  const last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
1106
- const report = last && typeof last.content === "string" ? last.content : "(tidak ada laporan)";
1556
+ const report = last && typeof last.content === "string" ? last.content : "(no report)";
1107
1557
  const suffix = outcome?.type === "stepLimit" ? `
1108
1558
 
1109
- (Subagent berhenti di batas ${def.maxSteps ?? 50} langkah.)` : "";
1559
+ (The subagent stopped at its ${def.maxSteps ?? 50}-step limit.)` : "";
1110
1560
  return { output: report + suffix, display };
1111
1561
  }
1112
1562
  });
1113
1563
  }
1114
1564
 
1115
1565
  // src/tools/todo.ts
1116
- import { z as z13 } from "zod";
1566
+ import { z as z14 } from "zod";
1117
1567
  var TodoStore = class {
1118
1568
  items = [];
1119
1569
  listeners = /* @__PURE__ */ new Set();
@@ -1130,36 +1580,47 @@ function createTodoTool(store) {
1130
1580
  return defineTool({
1131
1581
  name: "todo_write",
1132
1582
  description: "Write the task list for multi-step work (3+ steps). Send the whole list every time; exactly one item in_progress while working. Mark items completed as soon as they are done.",
1133
- schema: z13.object({
1134
- todos: z13.array(z13.object({ content: z13.string(), status: z13.enum(["pending", "in_progress", "completed"]) }))
1583
+ schema: z14.object({
1584
+ todos: z14.array(z14.object({ content: z14.string(), status: z14.enum(["pending", "in_progress", "completed"]) }))
1135
1585
  }),
1136
1586
  kind: "read",
1137
1587
  target: (input) => `${input.todos.length} item`,
1138
1588
  async run(input) {
1139
- if (input.todos.filter((t) => t.status === "in_progress").length > 1) {
1140
- return { output: "Hanya boleh satu item in_progress.", isError: true };
1589
+ if (input.todos.filter((t2) => t2.status === "in_progress").length > 1) {
1590
+ return { output: "Only one item may be in_progress.", isError: true };
1141
1591
  }
1142
1592
  store.set(input.todos);
1143
- const done = input.todos.filter((t) => t.status === "completed").length;
1144
- return { output: `Todo diperbarui: ${done}/${input.todos.length} selesai.` };
1593
+ const done = input.todos.filter((t2) => t2.status === "completed").length;
1594
+ return { output: `Todo updated: ${done}/${input.todos.length} completed.` };
1145
1595
  }
1146
1596
  });
1147
1597
  }
1148
1598
 
1149
1599
  // src/setup.ts
1150
1600
  var TOOL_RULES = { bash: "bash", edit: "edit", write: "edit", fetch: "fetch" };
1601
+ function lunaPtcAutoEnabled(env) {
1602
+ const value = String(env.BCCLI_LUNA_PTC ?? "").trim().toLowerCase();
1603
+ return !["0", "false", "off", "no"].includes(value);
1604
+ }
1151
1605
  function createRuntime(opts) {
1152
1606
  const env = opts.env ?? process.env;
1153
1607
  const home = bccliHome(env);
1154
1608
  let config = loadConfig(opts.cwd, env);
1155
1609
  let modelRef = opts.args.model ?? config.model;
1610
+ let reasoning = opts.args.reasoning ?? config.reasoning;
1156
1611
  const makeProvider = (ref) => {
1157
1612
  const resolved = resolveModel(config, ref, env);
1158
- return opts.provider ?? createProvider({ baseURL: resolved.baseURL, apiKey: resolved.apiKey, model: resolved.model });
1613
+ return opts.provider ?? createProvider({
1614
+ baseURL: resolved.baseURL,
1615
+ apiKey: resolved.apiKey,
1616
+ model: resolved.model,
1617
+ providerId: resolved.providerId,
1618
+ enableProgrammaticToolCalling: lunaPtcAutoEnabled(env)
1619
+ });
1159
1620
  };
1160
1621
  const provider = makeProvider(modelRef);
1161
1622
  const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
1162
- const rules = [...config.allow, ...opts.args.allowedTools.map((t) => TOOL_RULES[t] ?? t)];
1623
+ const rules = [...config.allow, ...opts.args.allowedTools.map((t2) => TOOL_RULES[t2] ?? t2)];
1163
1624
  const permissions = new Permissions(mode, rules, opts.cwd);
1164
1625
  const roots = { cwd: opts.cwd, home, userHome: opts.userHome ?? homedir() };
1165
1626
  const skills = loadSkills(roots);
@@ -1179,16 +1640,27 @@ function createRuntime(opts) {
1179
1640
  }
1180
1641
  }
1181
1642
  const systemPrompt = buildSystemPrompt({ cwd: opts.cwd, home, model: modelRef, skills });
1182
- const baseTools = [...ALL_TOOLS, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
1643
+ const webSearch = createWebSearchTool({
1644
+ botconnector: () => {
1645
+ try {
1646
+ const bc = resolveModel(config, "bc-cloud/search", env);
1647
+ return bc.apiKey ? { baseURL: bc.baseURL, apiKey: bc.apiKey } : void 0;
1648
+ } catch {
1649
+ return void 0;
1650
+ }
1651
+ }
1652
+ });
1653
+ const baseTools = [...ALL_TOOLS, webSearch, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
1183
1654
  const task = createTaskTool({
1184
1655
  agents: [...BUILTIN_AGENTS, ...agentDefs],
1185
1656
  // Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
1186
- baseTools: () => agent.tools.filter((t) => t.name !== "task"),
1657
+ baseTools: () => agent.tools.filter((t2) => t2.name !== "task"),
1187
1658
  permissions,
1188
1659
  provider: () => agent.provider,
1189
1660
  providerFor: makeProvider,
1190
1661
  systemPrompt,
1191
- cwd: opts.cwd
1662
+ cwd: opts.cwd,
1663
+ reasoning: () => reasoning
1192
1664
  });
1193
1665
  const agent = new Agent({
1194
1666
  provider,
@@ -1197,6 +1669,7 @@ function createRuntime(opts) {
1197
1669
  systemPrompt,
1198
1670
  cwd: opts.cwd,
1199
1671
  history,
1672
+ reasoning,
1200
1673
  onMessage: (m) => session.append(m),
1201
1674
  onReset: () => session.reset(),
1202
1675
  onTurnStart: () => checkpoints.beginTurn(),
@@ -1238,6 +1711,9 @@ function createRuntime(opts) {
1238
1711
  get modelRef() {
1239
1712
  return modelRef;
1240
1713
  },
1714
+ get reasoning() {
1715
+ return reasoning;
1716
+ },
1241
1717
  agent,
1242
1718
  get session() {
1243
1719
  return session;
@@ -1267,36 +1743,59 @@ function createRuntime(opts) {
1267
1743
  setModel(ref) {
1268
1744
  agent.provider = makeProvider(ref);
1269
1745
  modelRef = ref;
1746
+ },
1747
+ setReasoning(level) {
1748
+ reasoning = level;
1749
+ agent.reasoning = level;
1270
1750
  }
1271
1751
  };
1272
1752
  }
1273
1753
 
1274
1754
  // src/version.ts
1275
- var VERSION = true ? "0.4.0-beta.3" : "0.0.0-dev";
1755
+ var VERSION = true ? "0.4.0-beta.31" : "0.0.0-dev";
1276
1756
 
1277
1757
  // src/cli.ts
1758
+ function chooseLanguage(argv) {
1759
+ let fromConfig;
1760
+ try {
1761
+ fromConfig = readGlobalConfigFile(process.env).language;
1762
+ } catch {
1763
+ }
1764
+ setLanguage(resolveLanguage(argv, process.env, fromConfig));
1765
+ }
1278
1766
  async function main() {
1767
+ chooseLanguage(process.argv.slice(2));
1279
1768
  const args = parseCliArgs(process.argv.slice(2));
1280
1769
  if (args.version) {
1281
1770
  console.log(VERSION);
1282
1771
  return 0;
1283
1772
  }
1284
1773
  if (args.help) {
1285
- console.log(HELP_TEXT);
1774
+ console.log(helpText());
1286
1775
  return 0;
1287
1776
  }
1288
1777
  const cwd = process.cwd();
1289
1778
  if (args.command === "login") return runLogin(args.loginProvider ?? "bc-cloud", cwd);
1290
1779
  if (args.command === "mcp") {
1291
- const { runMcpCommand } = await import("./mcpCli-Q2H63ISP.js");
1292
- const { readSecret } = await import("./login-KHU7ONSR.js");
1780
+ const { runMcpCommand } = await import("./mcpCli-UGNEDBMU.js");
1781
+ const { readSecret } = await import("./login-AP2ODLJX.js");
1293
1782
  return runMcpCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
1294
1783
  }
1295
1784
  if (args.command === "provider") {
1296
- const { runProviderCommand } = await import("./providerCli-G3X7KENO.js");
1297
- const { readSecret } = await import("./login-KHU7ONSR.js");
1785
+ const { runProviderCommand } = await import("./providerCli-J4Y7D6PF.js");
1786
+ const { readSecret } = await import("./login-AP2ODLJX.js");
1298
1787
  return runProviderCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
1299
1788
  }
1789
+ if (args.command === "connect" || args.command === "disconnect" || args.command === "integrations") {
1790
+ const { runIntegrationCommand } = await import("./integrationCli-G4C6XQUE.js");
1791
+ return runIntegrationCommand(args, {
1792
+ env: process.env,
1793
+ cwd,
1794
+ out: (s) => console.log(s),
1795
+ err: (s) => console.error(s),
1796
+ fetch: globalThis.fetch
1797
+ });
1798
+ }
1300
1799
  if (args.command === "models") {
1301
1800
  const config = loadConfig(cwd);
1302
1801
  const resolved = resolveModel(config, args.model ?? config.model);
@@ -1310,11 +1809,11 @@ async function main() {
1310
1809
  } catch {
1311
1810
  }
1312
1811
  if (args.print) {
1313
- if (!args.prompt) throw new ConfigError('Mode -p butuh tugas, contoh: bccli -p "jelaskan repo ini"');
1314
- const { serversToStart } = await import("./config-NPSYFAFD.js");
1812
+ if (!args.prompt) throw new ConfigError(t('-p mode needs a task, e.g. bccli -p "explain this repo"'));
1813
+ const { serversToStart } = await import("./config-DMF4AO7E.js");
1315
1814
  const plan = serversToStart(rt.home, cwd);
1316
1815
  for (const s of plan.needTrust) {
1317
- console.error(`Server MCP project "${s.name}" dilewati (belum diizinkan; jalankan bccli interaktif sekali untuk menyetujuinya).`);
1816
+ console.error(t('Project MCP server "{name}" skipped (not approved yet; run interactive bccli once to approve it).', { name: s.name }));
1318
1817
  }
1319
1818
  await rt.startMcp(plan.start);
1320
1819
  try {
@@ -1323,8 +1822,8 @@ async function main() {
1323
1822
  await rt.mcp.stop();
1324
1823
  }
1325
1824
  }
1326
- if (!process.stdin.isTTY) throw new ConfigError('Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"');
1327
- const { startInteractive } = await import("./ui-A32PTF4F.js");
1825
+ if (!process.stdin.isTTY) throw new ConfigError(t('Interactive mode needs a terminal. For scripts/CI use: bccli -p "task"'));
1826
+ const { startInteractive } = await import("./ui-EFY7SNIW.js");
1328
1827
  return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
1329
1828
  }
1330
1829
  main().then(