@botconnector/bccli 0.4.0-beta.12 → 0.4.0-beta.13

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
@@ -39,6 +39,71 @@ import {
39
39
  resolveModel
40
40
  } from "./chunk-2MGINW4O.js";
41
41
 
42
+ // src/textToolCalls.ts
43
+ var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
44
+ var CALL_START = /<tool_call>|\n?[^\n]*\bTool call list\b[^\n]*|\n\s*\d+\.\s*Tool:/g;
45
+ function coerce(value, type) {
46
+ const types = Array.isArray(type) ? type : [type];
47
+ const v = value.trim();
48
+ if ((types.includes("integer") || types.includes("number")) && v !== "" && !Number.isNaN(Number(v))) return Number(v);
49
+ if (types.includes("boolean") && /^(true|false)$/i.test(v)) return v.toLowerCase() === "true";
50
+ if ((types.includes("object") || types.includes("array")) && /^[[{]/.test(v)) {
51
+ try {
52
+ return JSON.parse(v);
53
+ } catch {
54
+ }
55
+ }
56
+ return value.replace(/^\n+|\n+$/g, "");
57
+ }
58
+ function pickTool(name, keys, tools) {
59
+ const named = name && tools.find((t) => t.function.name === name);
60
+ if (named) return named;
61
+ let best;
62
+ let bestScore = 0;
63
+ let tie = false;
64
+ for (const t of tools) {
65
+ const params = t.function.parameters;
66
+ const props = Object.keys(params.properties ?? {});
67
+ if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
68
+ const score = keys.filter((k) => props.includes(k)).length;
69
+ if (score > bestScore) [best, bestScore, tie] = [t, score, false];
70
+ else if (score === bestScore && score > 0) tie = true;
71
+ }
72
+ return tie ? void 0 : best;
73
+ }
74
+ function recoverTextToolCalls(text, tools) {
75
+ if (!/<\/arg_value>|<tool_call>/.test(text)) return null;
76
+ const starts = [...text.matchAll(CALL_START)].map((m) => m.index ?? 0);
77
+ const firstPair = text.search(PAIR);
78
+ const cut = Math.min(...[...starts, firstPair].filter((i) => i >= 0));
79
+ const bounds = starts.filter((i) => i >= cut);
80
+ if (!bounds.length || bounds[0] > cut) bounds.unshift(cut);
81
+ const toolCalls = [];
82
+ bounds.forEach((start, n) => {
83
+ const segment = text.slice(start, bounds[n + 1] ?? text.length);
84
+ const name = /<tool_call>\s*([A-Za-z_][\w.-]*)/.exec(segment)?.[1] ?? /Tool:\s*`?([A-Za-z_][\w.-]*)`?/.exec(segment)?.[1];
85
+ let args = [];
86
+ const flush = () => {
87
+ if (!args.length) return;
88
+ const keys = args.map(([k]) => k);
89
+ const tool = pickTool(name, keys, tools);
90
+ if (tool) {
91
+ const props = tool.function.parameters.properties ?? {};
92
+ const input = Object.fromEntries(args.filter(([k]) => k in props).map(([k, v]) => [k, coerce(v, props[k].type)]));
93
+ toolCalls.push({ id: `text_call_${toolCalls.length}`, name: tool.function.name, arguments: JSON.stringify(input) });
94
+ }
95
+ args = [];
96
+ };
97
+ for (const m of segment.matchAll(PAIR)) {
98
+ if (args.some(([k]) => k === m[1])) flush();
99
+ args.push([m[1], m[2]]);
100
+ }
101
+ flush();
102
+ });
103
+ if (!toolCalls.length) return null;
104
+ return { text: text.slice(0, cut).trim(), toolCalls };
105
+ }
106
+
42
107
  // src/tools/index.ts
43
108
  import { z as z8 } from "zod";
44
109
 
@@ -279,7 +344,12 @@ var editTool = defineTool({
279
344
 
280
345
  // src/tools/fetch.ts
281
346
  import { z as z3 } from "zod";
282
- var MAX_CHARS = 5e4;
347
+ var MAX_CHARS = 12e3;
348
+ var FOCUS_CHUNKS = 3;
349
+ var FOCUS_SIZE = 800;
350
+ var CACHE_MAX = 16;
351
+ var CACHE_TTL_MS = 15 * 6e4;
352
+ var CACHE_MAX_CHARS = 2e6;
283
353
  var MAX_BYTES = 5 * 1024 * 1024;
284
354
  async function readCapped(res) {
285
355
  const reader = res.body?.getReader();
@@ -315,45 +385,179 @@ function describeFetchError(error) {
315
385
  }
316
386
  return code ? `${detail} (${code})` : detail;
317
387
  }
318
- var fetchTool = defineTool({
319
- name: "fetch",
320
- description: "Fetch a URL (http/https). HTML is converted to plain text. Max 50k characters.",
321
- schema: z3.object({ url: z3.string().describe("Absolute http(s) URL") }),
322
- kind: "fetch",
323
- target: (input) => input.url,
324
- async run(input, ctx) {
325
- if (!/^https?:\/\//i.test(input.url)) return { output: "URL harus diawali http:// atau https://", isError: true };
326
- let url = new URL(input.url);
327
- let res;
328
- try {
329
- for (let hop = 0; hop < 5; hop++) {
330
- res = await fetch(url, { signal: AbortSignal.any([ctx.signal, AbortSignal.timeout(3e4)]), redirect: "manual" });
331
- const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
332
- if (!location) break;
333
- const next = new URL(location, url);
334
- if (next.host !== url.host) {
335
- await res.body?.cancel();
336
- return {
337
- output: `Redirect ke host lain: ${next.href}. Panggil fetch lagi dengan URL itu kalau memang perlu (akan diminta izin).`,
338
- isError: true
339
- };
388
+ var tokensOf = (text) => Math.ceil(text.length / 4);
389
+ function isPrivateHost(host) {
390
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "");
391
+ if (h === "localhost" || /\.(local|internal|lan|localdomain|home\.arpa)$/.test(h)) return true;
392
+ if (h.includes(":")) return /^(::1?$|f[cd]|fe[89ab]|::ffff:)/.test(h);
393
+ const v4 = h.match(/^(\d+)\.(\d+)\.\d+\.\d+$/);
394
+ if (v4) {
395
+ const [a, b] = [Number(v4[1]), Number(v4[2])];
396
+ 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;
397
+ }
398
+ return !h.includes(".");
399
+ }
400
+ var words = (s) => s.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [];
401
+ function pickRelevant(text, query, count = FOCUS_CHUNKS, size = FOCUS_SIZE) {
402
+ const chunks = [];
403
+ let cur = "";
404
+ for (const line of text.split("\n")) {
405
+ for (let i = 0; i < Math.max(line.length, 1); i += size) {
406
+ cur += `${line.slice(i, i + size)}
407
+ `;
408
+ if (cur.length >= size) {
409
+ chunks.push(cur.trimEnd());
410
+ cur = "";
411
+ }
412
+ }
413
+ }
414
+ if (cur.trim()) chunks.push(cur.trimEnd());
415
+ const q = [...new Set(words(query))];
416
+ const docs = chunks.map(words);
417
+ const avg = docs.reduce((n, d) => n + d.length, 0) / (docs.length || 1) || 1;
418
+ const idf = q.map((t) => {
419
+ const df = docs.filter((d) => d.includes(t)).length;
420
+ return Math.log(1 + (docs.length - df + 0.5) / (df + 0.5));
421
+ });
422
+ const scored = docs.map((d, i) => ({
423
+ i,
424
+ score: q.reduce((sum, t, k) => {
425
+ const tf = d.filter((w) => w === t).length;
426
+ return sum + idf[k] * tf * 2.2 / (tf + 1.2 * (0.25 + 0.75 * d.length / avg));
427
+ }, 0)
428
+ })).filter((c) => c.score > 0).sort((a, b) => b.score - a.score).slice(0, count).sort((a, b) => a.i - b.i);
429
+ return scored.length ? scored.map((c) => chunks[c.i]).join("\n\u2026\n") : null;
430
+ }
431
+ var cache = /* @__PURE__ */ new Map();
432
+ function remember(key, value, size) {
433
+ if (size > CACHE_MAX_CHARS) return;
434
+ cache.delete(key);
435
+ cache.set(key, value);
436
+ if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
437
+ }
438
+ function recall(key) {
439
+ const hit = cache.get(key);
440
+ if (typeof hit === "object" && Date.now() - hit.at > CACHE_TTL_MS) {
441
+ cache.delete(key);
442
+ return void 0;
443
+ }
444
+ return hit;
445
+ }
446
+ var ACCEPT = "text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.5";
447
+ var withCost = (text, what) => `${what} \xB7 ~${tokensOf(text)} token`;
448
+ function createFetchTool(opts = {}) {
449
+ const keenableURL = opts.keenableURL === void 0 ? "https://api.keenable.ai/v1/fetch/public" : opts.keenableURL;
450
+ const isPrivate = opts.isPrivate ?? isPrivateHost;
451
+ return defineTool({
452
+ name: "fetch",
453
+ description: `Fetch a URL (http/https) as text/markdown. Costs the user tokens, so ask for as little as you need:
454
+ - 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.
455
+ - Without \`prompt\` you get the first ${MAX_CHARS} characters; the reply tells you the \`offset\` to continue from.
456
+ - Fetching a page that is already in this conversation is wasteful; reuse what you have.`,
457
+ schema: z3.object({
458
+ url: z3.string().describe("Absolute http(s) URL"),
459
+ prompt: z3.string().max(2e3).optional().describe("What to look for in the page; returns only the relevant excerpt"),
460
+ offset: z3.number().int().min(0).optional().describe("Start reading at this character (to continue a truncated page)")
461
+ }),
462
+ kind: "fetch",
463
+ target: (input) => input.url,
464
+ async run(input, ctx) {
465
+ if (!/^https?:\/\//i.test(input.url)) return { output: "URL harus diawali http:// atau https://", isError: true };
466
+ const target = new URL(input.url);
467
+ const prompt = input.prompt?.trim();
468
+ const askKeenable = !!prompt && !input.offset && !!keenableURL && !target.username && !target.password && !isPrivate(target.hostname);
469
+ if (askKeenable) {
470
+ const key = `x
471
+ ${input.url}
472
+ ${prompt}`;
473
+ const hit = recall(key);
474
+ if (hit) return { output: hit, display: withCost(hit, "kutipan fokus \xB7 cache") };
475
+ const out = await extractWithKeenable(keenableURL, input.url, prompt, ctx.signal);
476
+ if (out) {
477
+ const output2 = `[Kutipan dari ${input.url} untuk "${prompt}" (diekstrak Keenable). Kalau jawabannya "tidak ada", coba fetch tanpa prompt.]
478
+ ${out}`;
479
+ remember(key, output2, output2.length);
480
+ return { output: output2, display: withCost(output2, "kutipan fokus (Keenable)") };
340
481
  }
482
+ }
483
+ let page = recall(`p
484
+ ${input.url}`);
485
+ const cached = !!page;
486
+ if (!page) {
487
+ const loaded = await loadPage(input.url, ctx.signal);
488
+ if ("error" in loaded) return { output: loaded.error, isError: true };
489
+ page = loaded;
490
+ remember(`p
491
+ ${input.url}`, page, page.text.length);
492
+ }
493
+ const { text, truncated } = page;
494
+ const via = cached ? " \xB7 cache" : "";
495
+ if (prompt && !input.offset) {
496
+ const picked = pickRelevant(text, prompt);
497
+ if (picked) {
498
+ const output2 = `[Kutipan paling relevan dari ${input.url} untuk "${prompt}" (${text.length} karakter total). Halaman penuh: fetch tanpa prompt.]
499
+ ${picked}`;
500
+ return { output: output2, display: withCost(output2, `kutipan fokus (lokal)${via}`) };
501
+ }
502
+ }
503
+ const offset = input.offset ?? 0;
504
+ if (offset >= text.length && text.length > 0) {
505
+ return { output: `offset ${offset} melewati akhir halaman (${text.length} karakter).`, isError: true };
506
+ }
507
+ const end = Math.min(offset + MAX_CHARS, text.length);
508
+ let output = text.slice(offset, end);
509
+ if (end < text.length || truncated) {
510
+ const total = truncated ? `${text.length}+` : text.length;
511
+ output += `
512
+ \u2026 [dipotong: karakter ${offset}-${end} dari ${total}. Lanjut dengan offset=${end}, atau pakai prompt untuk kutipan terfokus.]`;
513
+ }
514
+ const size = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
515
+ return { output, display: withCost(output, `${size}${via}`) };
516
+ }
517
+ });
518
+ }
519
+ async function extractWithKeenable(endpoint, url, prompt, signal) {
520
+ try {
521
+ const u = new URL(endpoint);
522
+ u.searchParams.set("url", url);
523
+ u.searchParams.set("prompt", prompt);
524
+ u.searchParams.set("live", "true");
525
+ u.searchParams.set("max_chars", String(MAX_CHARS));
526
+ const res = await fetch(u, { headers: { "X-Keenable-Title": "BotConnector" }, signal: AbortSignal.any([signal, AbortSignal.timeout(25e3)]) });
527
+ if (!res.ok) return void 0;
528
+ const content = (await res.json()).content;
529
+ return typeof content === "string" && content.trim() ? content.slice(0, MAX_CHARS) : void 0;
530
+ } catch {
531
+ return void 0;
532
+ }
533
+ }
534
+ async function loadPage(inputUrl, signal) {
535
+ let url = new URL(inputUrl);
536
+ let res;
537
+ try {
538
+ for (let hop = 0; hop < 5; hop++) {
539
+ res = await fetch(url, { headers: { accept: ACCEPT }, signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)]), redirect: "manual" });
540
+ const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
541
+ if (!location) break;
542
+ const next = new URL(location, url);
543
+ if (next.host !== url.host) {
341
544
  await res.body?.cancel();
342
- url = next;
545
+ return { error: `Redirect ke host lain: ${next.href}. Panggil fetch lagi dengan URL itu kalau memang perlu (akan diminta izin).` };
343
546
  }
344
- } catch (error) {
345
- return { output: `Gagal mengambil ${input.url}: ${describeFetchError(error)}`, isError: true };
547
+ await res.body?.cancel();
548
+ url = next;
346
549
  }
347
- if (!res) return { output: `Gagal mengambil ${input.url}`, isError: true };
348
- const { text: body, truncated } = await readCapped(res);
349
- const text = (res.headers.get("content-type") ?? "").includes("html") ? htmlToText(body) : body;
350
- if (!res.ok) return { output: `HTTP ${res.status} dari ${input.url}: ${text.slice(0, 500)}`, isError: true };
351
- const clipped = text.length > MAX_CHARS || truncated ? `${text.slice(0, MAX_CHARS)}
352
- \u2026 [dipotong]` : text;
353
- const display = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
354
- return { output: clipped, display };
550
+ } catch (error) {
551
+ return { error: `Gagal mengambil ${inputUrl}: ${describeFetchError(error)}` };
355
552
  }
356
- });
553
+ if (!res) return { error: `Gagal mengambil ${inputUrl}` };
554
+ const { text: body, truncated } = await readCapped(res);
555
+ const type = res.headers.get("content-type") ?? "";
556
+ const text = type.includes("html") ? htmlToText(body) : body;
557
+ if (!res.ok) return { error: `HTTP ${res.status} dari ${inputUrl}: ${text.slice(0, 500)}` };
558
+ return { text, truncated, at: Date.now() };
559
+ }
560
+ var fetchTool = createFetchTool();
357
561
 
358
562
  // src/tools/glob.ts
359
563
  import fg from "fast-glob";
@@ -536,70 +740,75 @@ function toolDefinitions(tools) {
536
740
  });
537
741
  }
538
742
 
539
- // src/textToolCalls.ts
540
- var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
541
- var CALL_START = /<tool_call>|\n?[^\n]*\bTool call list\b[^\n]*|\n\s*\d+\.\s*Tool:/g;
542
- function coerce(value, type) {
543
- const types = Array.isArray(type) ? type : [type];
544
- const v = value.trim();
545
- if ((types.includes("integer") || types.includes("number")) && v !== "" && !Number.isNaN(Number(v))) return Number(v);
546
- if (types.includes("boolean") && /^(true|false)$/i.test(v)) return v.toLowerCase() === "true";
547
- if ((types.includes("object") || types.includes("array")) && /^[[{]/.test(v)) {
548
- try {
549
- return JSON.parse(v);
550
- } catch {
743
+ // src/webBudget.ts
744
+ var KEEP_TOKENS = 4e4;
745
+ var PRUNE_MIN_TOKENS = 2e4;
746
+ var STUB = "[hasil tool lama dihapus:";
747
+ var tokens = (text) => Math.ceil(text.length / 4);
748
+ var isStub = (content) => content.startsWith(STUB);
749
+ function pruneOldFetches(messages) {
750
+ const urls = /* @__PURE__ */ new Map();
751
+ for (const m of messages) {
752
+ if (m.role !== "assistant") continue;
753
+ for (const c of m.tool_calls ?? []) {
754
+ if (c.function.name !== "fetch") continue;
755
+ try {
756
+ urls.set(c.id, String(JSON.parse(c.function.arguments).url ?? ""));
757
+ } catch {
758
+ urls.set(c.id, "");
759
+ }
551
760
  }
552
761
  }
553
- return value.replace(/^\n+|\n+$/g, "");
554
- }
555
- function pickTool(name, keys, tools) {
556
- const named = name && tools.find((t) => t.function.name === name);
557
- if (named) return named;
558
- let best;
559
- let bestScore = 0;
560
- let tie = false;
561
- for (const t of tools) {
562
- const params = t.function.parameters;
563
- const props = Object.keys(params.properties ?? {});
564
- if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
565
- const score = keys.filter((k) => props.includes(k)).length;
566
- if (score > bestScore) [best, bestScore, tie] = [t, score, false];
567
- else if (score === bestScore && score > 0) tie = true;
762
+ let kept = 0;
763
+ let oldTokens = 0;
764
+ const old = [];
765
+ for (let i = messages.length - 1; i >= 0; i--) {
766
+ const m = messages[i];
767
+ if (m.role !== "tool" || !urls.has(m.tool_call_id) || isStub(m.content)) continue;
768
+ kept += tokens(m.content);
769
+ if (kept > KEEP_TOKENS) {
770
+ old.push(i);
771
+ oldTokens += tokens(m.content);
772
+ }
568
773
  }
569
- return tie ? void 0 : best;
774
+ if (oldTokens <= PRUNE_MIN_TOKENS) return false;
775
+ for (const i of old) {
776
+ const m = messages[i];
777
+ messages[i] = { ...m, content: `${STUB} fetch ${urls.get(m.tool_call_id)}. Ambil lagi dengan fetch (pakai prompt untuk kutipan terfokus) kalau masih perlu.]` };
778
+ }
779
+ return true;
570
780
  }
571
- function recoverTextToolCalls(text, tools) {
572
- if (!/<\/arg_value>|<tool_call>/.test(text)) return null;
573
- const starts = [...text.matchAll(CALL_START)].map((m) => m.index ?? 0);
574
- const firstPair = text.search(PAIR);
575
- const cut = Math.min(...[...starts, firstPair].filter((i) => i >= 0));
576
- const bounds = starts.filter((i) => i >= cut);
577
- if (!bounds.length || bounds[0] > cut) bounds.unshift(cut);
578
- const toolCalls = [];
579
- bounds.forEach((start, n) => {
580
- const segment = text.slice(start, bounds[n + 1] ?? text.length);
581
- const name = /<tool_call>\s*([A-Za-z_][\w.-]*)/.exec(segment)?.[1] ?? /Tool:\s*`?([A-Za-z_][\w.-]*)`?/.exec(segment)?.[1];
582
- let args = [];
583
- const flush = () => {
584
- if (!args.length) return;
585
- const keys = args.map(([k]) => k);
586
- const tool = pickTool(name, keys, tools);
587
- if (tool) {
588
- const props = tool.function.parameters.properties ?? {};
589
- const input = Object.fromEntries(args.filter(([k]) => k in props).map(([k, v]) => [k, coerce(v, props[k].type)]));
590
- toolCalls.push({ id: `text_call_${toolCalls.length}`, name: tool.function.name, arguments: JSON.stringify(input) });
591
- }
592
- args = [];
593
- };
594
- for (const m of segment.matchAll(PAIR)) {
595
- if (args.some(([k]) => k === m[1])) flush();
596
- args.push([m[1], m[2]]);
781
+ var WebBudget = class {
782
+ fetches = 0;
783
+ chars = 0;
784
+ perTurn;
785
+ sessionChars;
786
+ constructor(limits = {}) {
787
+ this.perTurn = limits.perTurn ?? 8;
788
+ this.sessionChars = limits.sessionChars ?? 3e5;
789
+ }
790
+ startTurn() {
791
+ this.fetches = 0;
792
+ }
793
+ reset() {
794
+ this.fetches = 0;
795
+ this.chars = 0;
796
+ }
797
+ /** Reserves one fetch; returns the refusal to show the model when over budget. */
798
+ take() {
799
+ if (this.fetches >= this.perTurn) {
800
+ return `Batas ${this.perTurn} fetch per pertanyaan tercapai (tiap fetch memakai token user). Rangkum jawaban dari hasil yang sudah ada; kalau memang kurang, jelaskan ke user apa yang belum ketemu.`;
597
801
  }
598
- flush();
599
- });
600
- if (!toolCalls.length) return null;
601
- return { text: text.slice(0, cut).trim(), toolCalls };
602
- }
802
+ if (this.chars >= this.sessionChars) {
803
+ return `Batas total halaman web sesi ini tercapai (~${Math.round(this.sessionChars / 4e3)}k token). Rangkum dari hasil yang sudah ada dan tanyakan ke user apakah perlu mencari lebih banyak.`;
804
+ }
805
+ this.fetches++;
806
+ return void 0;
807
+ }
808
+ record(chars) {
809
+ this.chars += chars;
810
+ }
811
+ };
603
812
 
604
813
  // src/agent.ts
605
814
  var estimateTokens = (text) => Math.ceil(text.length / 4);
@@ -635,6 +844,7 @@ var Agent = class {
635
844
  turnTools;
636
845
  turnDefinitions;
637
846
  readFiles = /* @__PURE__ */ new Set();
847
+ webBudget = new WebBudget();
638
848
  maxSteps;
639
849
  // ponytail: one window for every model; read it from /v1/models metadata when providers expose it.
640
850
  contextWindow;
@@ -664,11 +874,13 @@ var Agent = class {
664
874
  load(messages) {
665
875
  this.messages = messages;
666
876
  this.readFiles.clear();
877
+ this.webBudget.reset();
667
878
  this.lastInputTokens = 0;
668
879
  }
669
880
  clear() {
670
881
  this.messages = [];
671
882
  this.readFiles.clear();
883
+ this.webBudget.reset();
672
884
  this.lastInputTokens = 0;
673
885
  this.opts.onReset?.();
674
886
  }
@@ -690,6 +902,7 @@ ${completion.text}` });
690
902
  this.turnTools = this.tools;
691
903
  this.turnDefinitions = this.definitions;
692
904
  this.opts.onTurnStart?.();
905
+ this.webBudget.startTurn();
693
906
  try {
694
907
  if (this.lastInputTokens > this.contextWindow * 0.8) await this.compact(signal);
695
908
  this.push({ role: "user", content: text });
@@ -698,6 +911,7 @@ ${completion.text}` });
698
911
  await this.compact(signal);
699
912
  this.push({ role: "user", content: `Lanjutkan tugas ini sesuai ringkasan di atas: ${text}` });
700
913
  }
914
+ pruneOldFetches(this.messages);
701
915
  let completion = await this.provider.chat({
702
916
  messages: [{ role: "system", content: this.systemPrompt }, ...this.messages],
703
917
  tools: this.turnDefinitions,
@@ -801,6 +1015,8 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
801
1015
  }
802
1016
  const input = parsed.data;
803
1017
  const target = tool.target(input);
1018
+ const overBudget = tool.name === "fetch" ? this.webBudget.take() : void 0;
1019
+ if (overBudget) return fail(overBudget, target);
804
1020
  const ctx = {
805
1021
  cwd: this.opts.cwd,
806
1022
  signal,
@@ -846,6 +1062,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
846
1062
  } catch (error) {
847
1063
  result = { output: `Error: ${error.message}`, isError: true };
848
1064
  }
1065
+ if (tool.name === "fetch" && !result.isError) this.webBudget.record(result.output.length);
849
1066
  this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output: result.output, display: result.display, isError: !!result.isError });
850
1067
  return result.output;
851
1068
  }
@@ -1180,8 +1397,8 @@ ${def.prompt}`,
1180
1397
  if (ctx.ask) child.askPermission = ctx.ask;
1181
1398
  await child.run(input.prompt, ctx.signal);
1182
1399
  ctx.addUsage?.(child.totalUsage);
1183
- const tokens = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
1184
- const display = `${steps} langkah \xB7 ${tokens >= 1e3 ? `${Math.round(tokens / 1e3)}k` : tokens} token`;
1400
+ const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
1401
+ const display = `${steps} langkah \xB7 ${tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2} token`;
1185
1402
  if (outcome?.type === "error") return { output: `Subagent ${def.name} gagal: ${outcome.message}`, isError: true, display };
1186
1403
  if (outcome?.type === "aborted") return { output: `Subagent ${def.name} dibatalkan.`, isError: true, display };
1187
1404
  const last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
@@ -1374,7 +1591,7 @@ function createRuntime(opts) {
1374
1591
  }
1375
1592
 
1376
1593
  // src/version.ts
1377
- var VERSION = true ? "0.4.0-beta.12" : "0.0.0-dev";
1594
+ var VERSION = true ? "0.4.0-beta.13" : "0.0.0-dev";
1378
1595
 
1379
1596
  // src/cli.ts
1380
1597
  async function main() {
@@ -1436,7 +1653,7 @@ async function main() {
1436
1653
  }
1437
1654
  }
1438
1655
  if (!process.stdin.isTTY) throw new ConfigError('Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"');
1439
- const { startInteractive } = await import("./ui-6GTQUJOE.js");
1656
+ const { startInteractive } = await import("./ui-JD5YWGPE.js");
1440
1657
  return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
1441
1658
  }
1442
1659
  main().then(
@@ -248,6 +248,11 @@ function Markdown({ text }) {
248
248
  // src/ui/LinePrompt.tsx
249
249
  import { Box as Box2, Text as Text2, useInput } from "ink";
250
250
  import { useState } from "react";
251
+
252
+ // src/ui/focusReport.ts
253
+ var isFocusReport = (input) => input === "[I" || input === "[O";
254
+
255
+ // src/ui/LinePrompt.tsx
251
256
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
252
257
  function LinePrompt({ label, mask, onSubmit, onCancel }) {
253
258
  const [value, setValue] = useState("");
@@ -255,7 +260,7 @@ function LinePrompt({ label, mask, onSubmit, onCancel }) {
255
260
  if (key.return) onSubmit(value.trim());
256
261
  else if (key.escape) onCancel();
257
262
  else if (key.backspace || key.delete) setValue((v) => v.slice(0, -1));
258
- else if (!key.ctrl && !key.meta && !key.tab && !key.upArrow && !key.downArrow) setValue((v) => v + input.replace(/[\r\n]/g, ""));
263
+ else if (!key.ctrl && !key.meta && !key.tab && !key.upArrow && !key.downArrow && !isFocusReport(input)) setValue((v) => v + input.replace(/[\r\n]/g, ""));
259
264
  });
260
265
  const shown = mask ? "\u2022".repeat(value.length) : value;
261
266
  return /* @__PURE__ */ jsx2(Box2, { borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: /* @__PURE__ */ jsxs(Text2, { children: [
@@ -284,7 +289,7 @@ function ListPicker({ title, items, onPick }) {
284
289
  else if (key.backspace || key.delete) {
285
290
  setFilter((f) => f.slice(0, -1));
286
291
  setIndex(0);
287
- } else if (!key.ctrl && !key.meta && !key.tab && input) {
292
+ } else if (!key.ctrl && !key.meta && !key.tab && input && !isFocusReport(input)) {
288
293
  setFilter((f) => f + input);
289
294
  setIndex(0);
290
295
  }
@@ -329,7 +334,7 @@ function ModelPicker({ groups, current, onPick }) {
329
334
  else if (key.backspace || key.delete) {
330
335
  setFilter((f) => f.slice(0, -1));
331
336
  setIndex(0);
332
- } else if (!key.ctrl && !key.meta && !key.tab && input) {
337
+ } else if (!key.ctrl && !key.meta && !key.tab && input && !isFocusReport(input)) {
333
338
  setFilter((f) => f + input);
334
339
  setIndex(0);
335
340
  }
@@ -498,7 +503,7 @@ function PromptInput({ disabled, history, cwd, onSubmit, extraCommands }) {
498
503
  edit((v) => completeFile(v, cwd));
499
504
  return;
500
505
  }
501
- if (key.ctrl || key.meta || key.escape || key.tab || key.leftArrow || key.rightArrow) return;
506
+ if (key.ctrl || key.meta || key.escape || key.tab || key.leftArrow || key.rightArrow || isFocusReport(input)) return;
502
507
  edit((v) => v + input.replace(/\r/g, "\n"));
503
508
  },
504
509
  { isActive: !disabled }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botconnector/bccli",
3
- "version": "0.4.0-beta.12",
3
+ "version": "0.4.0-beta.13",
4
4
  "description": "BCCLI — BotConnector AI coding agent for the terminal",
5
5
  "type": "module",
6
6
  "bin": {