@botconnector/bccli 0.3.0-beta.3 → 0.4.0-beta.10

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,40 +1,43 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ BUILTIN_AGENTS,
4
+ buildSystemPrompt,
5
+ ignorePatterns,
6
+ loadAgentDefs,
7
+ loadCommands,
8
+ loadSkills,
9
+ parseFrontmatter,
10
+ runPrint
11
+ } from "./chunk-KV27KTOX.js";
2
12
  import {
3
13
  HELP_TEXT,
4
14
  parseCliArgs
5
- } from "./chunk-RDQ7HPWS.js";
15
+ } from "./chunk-BJ337ABK.js";
16
+ import {
17
+ Permissions
18
+ } from "./chunk-ELML4KVQ.js";
6
19
  import {
7
20
  addProviderKey,
8
21
  createProvider,
9
22
  listAllModels
10
- } from "./chunk-5ISYVAXI.js";
23
+ } from "./chunk-U7GK73CK.js";
24
+ import {
25
+ providerName
26
+ } from "./chunk-7S6JXQCH.js";
27
+ import "./chunk-CLN4Z5E3.js";
11
28
  import {
12
29
  runLogin
13
- } from "./chunk-R37QWDTL.js";
30
+ } from "./chunk-IOWDWOXS.js";
14
31
  import {
15
32
  Session,
16
33
  pruneSessions
17
34
  } from "./chunk-SJH3YLW4.js";
18
- import {
19
- ignorePatterns,
20
- loadAgentDefs,
21
- loadCommands,
22
- loadSkills,
23
- parseFrontmatter,
24
- runPrint
25
- } from "./chunk-TBPPXOOZ.js";
26
- import {
27
- Permissions
28
- } from "./chunk-5XN44Q55.js";
29
- import {
30
- providerName
31
- } from "./chunk-5C6NQFSI.js";
32
35
  import {
33
36
  ConfigError,
34
37
  bccliHome,
35
38
  loadConfig,
36
39
  resolveModel
37
- } from "./chunk-Z5JM76EL.js";
40
+ } from "./chunk-2MGINW4O.js";
38
41
 
39
42
  // src/tools/index.ts
40
43
  import { z as z8 } from "zod";
@@ -268,6 +271,7 @@ var editTool = defineTool({
268
271
  }
269
272
  const next = applyEdit(content, input);
270
273
  if (typeof next !== "string") return next;
274
+ await ctx.checkpoint?.(abs);
271
275
  await writeFile(abs, next);
272
276
  return { output: `Mengedit ${input.path}.`, display: formatDiff(diffLines(content, next)) };
273
277
  }
@@ -297,6 +301,20 @@ async function readCapped(res) {
297
301
  function htmlToText(html) {
298
302
  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");
299
303
  }
304
+ function describeFetchError(error) {
305
+ const cause = error.cause;
306
+ const code = cause?.code ?? "";
307
+ const detail = cause?.message ?? error.message;
308
+ if (error.name === "TimeoutError") return "tidak ada jawaban dalam 30 detik";
309
+ if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `${detail} \u2014 DNS tidak menemukan host (salah ketik, offline, atau diblokir DNS/ISP)`;
310
+ if (/CERT|SELF_SIGNED|UNABLE_TO/.test(code)) {
311
+ return `${detail} \u2014 sertifikat TLS ditolak; sering karena antivirus/proxy yang memeriksa HTTPS. Coba jalankan dengan NODE_OPTIONS=--use-system-ca`;
312
+ }
313
+ if (/ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR_CONNECT_TIMEOUT|UND_ERR_SOCKET/.test(code)) {
314
+ return `${detail} \u2014 koneksi gagal (firewall, proxy, atau situs diblokir jaringan)`;
315
+ }
316
+ return code ? `${detail} (${code})` : detail;
317
+ }
300
318
  var fetchTool = defineTool({
301
319
  name: "fetch",
302
320
  description: "Fetch a URL (http/https). HTML is converted to plain text. Max 50k characters.",
@@ -324,12 +342,12 @@ var fetchTool = defineTool({
324
342
  url = next;
325
343
  }
326
344
  } catch (error) {
327
- return { output: `Gagal mengambil ${input.url}: ${error.message}`, isError: true };
345
+ return { output: `Gagal mengambil ${input.url}: ${describeFetchError(error)}`, isError: true };
328
346
  }
329
347
  if (!res) return { output: `Gagal mengambil ${input.url}`, isError: true };
330
348
  const { text: body, truncated } = await readCapped(res);
331
- if (!res.ok) return { output: `HTTP ${res.status} dari ${input.url}: ${body.slice(0, 500)}`, isError: true };
332
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 };
333
351
  const clipped = text.length > MAX_CHARS || truncated ? `${text.slice(0, MAX_CHARS)}
334
352
  \u2026 [dipotong]` : text;
335
353
  const display = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
@@ -497,6 +515,7 @@ var writeTool = defineTool({
497
515
  return { output: `${input.path} sudah ada. Baca dulu dengan read sebelum menimpanya.`, isError: true };
498
516
  }
499
517
  try {
518
+ await ctx.checkpoint?.(abs);
500
519
  await mkdir(dirname(abs), { recursive: true });
501
520
  await writeFile2(abs, input.content);
502
521
  } catch (error) {
@@ -517,6 +536,71 @@ function toolDefinitions(tools) {
517
536
  });
518
537
  }
519
538
 
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 {
551
+ }
552
+ }
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;
568
+ }
569
+ return tie ? void 0 : best;
570
+ }
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]]);
597
+ }
598
+ flush();
599
+ });
600
+ if (!toolCalls.length) return null;
601
+ return { text: text.slice(0, cut).trim(), toolCalls };
602
+ }
603
+
520
604
  // src/agent.ts
521
605
  var estimateTokens = (text) => Math.ceil(text.length / 4);
522
606
  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.";
@@ -532,6 +616,8 @@ var Agent = class {
532
616
  this.messages = [...opts.history ?? []];
533
617
  this.maxSteps = opts.maxSteps ?? 50;
534
618
  this.contextWindow = opts.contextWindow ?? 128e3;
619
+ this.systemPrompt = opts.systemPrompt;
620
+ this.reasoning = opts.reasoning ?? "auto";
535
621
  }
536
622
  opts;
537
623
  provider;
@@ -541,6 +627,7 @@ var Agent = class {
541
627
  lastInputTokens = 0;
542
628
  onEvent = () => {
543
629
  };
630
+ reasoning;
544
631
  askPermission = async () => "no";
545
632
  tools;
546
633
  definitions;
@@ -551,6 +638,10 @@ var Agent = class {
551
638
  maxSteps;
552
639
  // ponytail: one window for every model; read it from /v1/models metadata when providers expose it.
553
640
  contextWindow;
641
+ systemPrompt;
642
+ setSystemPrompt(text) {
643
+ this.systemPrompt = text;
644
+ }
554
645
  setTools(tools) {
555
646
  this.tools = tools;
556
647
  this.definitions = toolDefinitions(tools);
@@ -569,6 +660,12 @@ var Agent = class {
569
660
  this.messages.push(message);
570
661
  this.opts.onMessage?.(message);
571
662
  }
663
+ /** Swaps in another conversation (resume, new session) without writing to the session log. */
664
+ load(messages) {
665
+ this.messages = messages;
666
+ this.readFiles.clear();
667
+ this.lastInputTokens = 0;
668
+ }
572
669
  clear() {
573
670
  this.messages = [];
574
671
  this.readFiles.clear();
@@ -577,10 +674,11 @@ var Agent = class {
577
674
  }
578
675
  async compact(signal) {
579
676
  const completion = await this.provider.chat({
580
- messages: [{ role: "system", content: this.opts.systemPrompt }, ...this.messages, { role: "user", content: COMPACT_PROMPT }],
677
+ messages: [{ role: "system", content: this.systemPrompt }, ...this.messages, { role: "user", content: COMPACT_PROMPT }],
581
678
  // Some gateways reject tool_calls in history when no tools are declared.
582
679
  tools: this.definitions,
583
- signal
680
+ signal,
681
+ reasoning: this.reasoning
584
682
  });
585
683
  this.clear();
586
684
  this.push({ role: "user", content: `Ringkasan percakapan sebelumnya:
@@ -591,6 +689,7 @@ ${completion.text}` });
591
689
  async run(text, signal) {
592
690
  this.turnTools = this.tools;
593
691
  this.turnDefinitions = this.definitions;
692
+ this.opts.onTurnStart?.();
594
693
  try {
595
694
  if (this.lastInputTokens > this.contextWindow * 0.8) await this.compact(signal);
596
695
  this.push({ role: "user", content: text });
@@ -599,14 +698,23 @@ ${completion.text}` });
599
698
  await this.compact(signal);
600
699
  this.push({ role: "user", content: `Lanjutkan tugas ini sesuai ringkasan di atas: ${text}` });
601
700
  }
602
- const completion = await this.provider.chat({
603
- messages: [{ role: "system", content: this.opts.systemPrompt }, ...this.messages],
701
+ let completion = await this.provider.chat({
702
+ messages: [{ role: "system", content: this.systemPrompt }, ...this.messages],
604
703
  tools: this.turnDefinitions,
605
704
  signal,
606
- onText: (delta) => this.onEvent({ type: "text", delta })
705
+ onText: (delta) => this.onEvent({ type: "text", delta }),
706
+ onThinking: (delta) => this.onEvent({ type: "thinking", delta }),
707
+ reasoning: this.reasoning
607
708
  });
709
+ if (!completion.toolCalls.length && completion.finishReason !== "repetition") {
710
+ const recovered = recoverTextToolCalls(completion.text, this.turnDefinitions);
711
+ if (recovered) {
712
+ completion = { ...completion, ...recovered };
713
+ this.onEvent({ type: "textReplace", text: recovered.text });
714
+ }
715
+ }
608
716
  const usage = completion.usage ?? {
609
- inputTokens: estimateTokens(this.opts.systemPrompt) + estimateTokens(JSON.stringify(this.messages)),
717
+ inputTokens: estimateTokens(this.systemPrompt) + estimateTokens(JSON.stringify(this.messages)),
610
718
  outputTokens: estimateTokens(completion.text) + (completion.toolCalls.length ? estimateTokens(JSON.stringify(completion.toolCalls)) : 0)
611
719
  };
612
720
  this.totalUsage.inputTokens += usage.inputTokens;
@@ -700,6 +808,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
700
808
  callId: call.id,
701
809
  emit: (event) => this.onEvent(event),
702
810
  ask: (req) => this.askPermission(req),
811
+ checkpoint: this.opts.checkpoint,
703
812
  addUsage: (u) => {
704
813
  this.totalUsage.inputTokens += u.inputTokens;
705
814
  this.totalUsage.outputTokens += u.outputTokens;
@@ -742,24 +851,59 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
742
851
  }
743
852
  };
744
853
 
745
- // src/agents.ts
746
- var EXPLORE_PROMPT = "You are a read-only research agent working for another agent. Search and read the codebase to answer the request. You cannot edit files or run commands. Reply with a concise report: the answer, the relevant file paths with line numbers, and nothing else.";
747
- var GENERAL_PROMPT = "You are an agent handling a delegated task for another agent. Complete the task end to end with the tools you have, verify the result, and reply with a short report of what you did and what you found.";
748
- var BUILTIN_AGENTS = [
749
- {
750
- name: "explore",
751
- description: "Read-only: search and read code to answer a question (fast, safe, can run in parallel).",
752
- tools: ["read", "grep", "glob"],
753
- prompt: EXPLORE_PROMPT,
754
- maxSteps: 30
755
- },
756
- {
757
- name: "general",
758
- description: "Full tools: carry out a self-contained sub-task (edits and commands still need user permission).",
759
- prompt: GENERAL_PROMPT,
760
- maxSteps: 50
854
+ // src/checkpoints.ts
855
+ import { existsSync as existsSync2, statSync } from "fs";
856
+ import { readFile as readFile5, rm, writeFile as writeFile3 } from "fs/promises";
857
+ var MAX_TURNS = 20;
858
+ var MAX_BYTES2 = 2 * 1024 * 1024;
859
+ var CheckpointStore = class {
860
+ history = [];
861
+ current;
862
+ beginTurn() {
863
+ if (this.current?.size === 0) return;
864
+ this.current = /* @__PURE__ */ new Map();
865
+ this.history.push(this.current);
866
+ }
867
+ /** Call before writing a file; only the first snapshot per file per turn is kept. */
868
+ async snapshot(path) {
869
+ if (!this.current) this.beginTurn();
870
+ const turn = this.current;
871
+ if (turn.has(path)) return;
872
+ if (!existsSync2(path)) turn.set(path, null);
873
+ else if (statSync(path).size > MAX_BYTES2) turn.set(path, "skipped");
874
+ else turn.set(path, await readFile5(path));
875
+ while (this.turns() > MAX_TURNS) this.history.shift();
876
+ }
877
+ turns() {
878
+ return this.history.filter((t) => t.size).length;
879
+ }
880
+ async undo() {
881
+ while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
882
+ const turn = this.history.pop();
883
+ this.current = void 0;
884
+ if (!turn) return void 0;
885
+ const result = { restored: [], deleted: [], skipped: [], failed: [] };
886
+ for (const [path, original] of turn) {
887
+ try {
888
+ if (original === "skipped") result.skipped.push(path);
889
+ else if (original === null) {
890
+ await rm(path, { force: true });
891
+ result.deleted.push(path);
892
+ } else {
893
+ await writeFile3(path, original);
894
+ result.restored.push(path);
895
+ }
896
+ } catch {
897
+ result.failed.push(path);
898
+ }
899
+ }
900
+ return result;
761
901
  }
762
- ];
902
+ clear() {
903
+ this.history.length = 0;
904
+ this.current = void 0;
905
+ }
906
+ };
763
907
 
764
908
  // src/mcp/manager.ts
765
909
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -767,8 +911,8 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
767
911
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
768
912
  import { z as z9 } from "zod";
769
913
  function mcpToolName(server, tool, taken) {
770
- const clean = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
771
- const base = `mcp__${clean(server)}__${clean(tool)}`.slice(0, 64);
914
+ const clean2 = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
915
+ const base = `mcp__${clean2(server)}__${clean2(tool)}`.slice(0, 64);
772
916
  let name = base;
773
917
  for (let n = 2; taken.has(name); n++) name = `${base.slice(0, 64 - String(n).length - 1)}_${n}`;
774
918
  taken.add(name);
@@ -871,73 +1015,6 @@ var McpManager = class {
871
1015
  // src/setup.ts
872
1016
  import { homedir } from "os";
873
1017
 
874
- // src/context.ts
875
- import { existsSync as existsSync2, readFileSync } from "fs";
876
- import { dirname as dirname2, join } from "path";
877
- var FILES = ["AGENTS.md", "BCCLI.md"];
878
- function readIf(path) {
879
- try {
880
- return existsSync2(path) ? readFileSync(path, "utf8").trim() : void 0;
881
- } catch {
882
- return void 0;
883
- }
884
- }
885
- function loadInstructions(cwd, home) {
886
- const dirs = [];
887
- for (let dir = cwd; ; dir = dirname2(dir)) {
888
- dirs.unshift(dir);
889
- if (dirname2(dir) === dir) break;
890
- }
891
- const parts = [];
892
- const global = readIf(join(home, "BCCLI.md"));
893
- if (global) parts.push(`# ${join(home, "BCCLI.md")}
894
- ${global}`);
895
- for (const dir of dirs) {
896
- for (const name of FILES) {
897
- const text = readIf(join(dir, name));
898
- if (text) parts.push(`# ${join(dir, name)}
899
- ${text}`);
900
- }
901
- }
902
- return parts.join("\n\n");
903
- }
904
- function isGitRepo(cwd) {
905
- for (let dir = cwd; ; dir = dirname2(dir)) {
906
- if (existsSync2(join(dir, ".git"))) return true;
907
- if (dirname2(dir) === dir) return false;
908
- }
909
- }
910
- function buildSystemPrompt(opts) {
911
- const instructions = loadInstructions(opts.cwd, opts.home);
912
- const skills = (opts.skills ?? []).slice(0, 50);
913
- const skillText = skills.length ? `
914
-
915
- Skills (load one with the skill tool when it matches the task):
916
- ${skills.map((s) => `- ${s.name}: ${s.description.slice(0, 200)}`).join("\n")}` : "";
917
- return `You are BCCLI, a coding agent by BotConnector running in the user's terminal. You help with software engineering tasks by reading code, editing files, and running commands with the tools provided.
918
-
919
- How to work:
920
- - Understand before changing: read the relevant files and search the codebase first. Never guess file contents.
921
- - Always read a file before editing it. Make the smallest correct change; match the existing style.
922
- - For tasks with 3 or more steps, keep a todo list with todo_write and update it as you go.
923
- - In plan mode you can only read and search. When the plan is ready, call exit_plan with it and wait for approval.
924
- - Verify your work: run the tests, build or the command that proves the change works, and report the real result.
925
- - The user must approve edits and commands. If they decline, ask what they want instead of retrying.
926
- - Be concise. Reply in the user's language. No preamble; lead with the answer or the action.
927
- - Never expose secrets, never run destructive commands (rm -rf, force push, dropping data) unless the user explicitly asks.
928
-
929
- Environment:
930
- - Working directory: ${opts.cwd}
931
- - Git repository: ${isGitRepo(opts.cwd) ? "yes" : "no"}
932
- - Platform: ${opts.platform ?? process.platform}
933
- - Date: ${opts.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
934
- - Model: ${opts.model}${skillText}${instructions ? `
935
-
936
- Project and user instructions (follow them):
937
-
938
- ${instructions}` : ""}`;
939
- }
940
-
941
1018
  // src/tools/plan.ts
942
1019
  import { z as z10 } from "zod";
943
1020
  function createExitPlanTool(opts) {
@@ -960,7 +1037,7 @@ function createExitPlanTool(opts) {
960
1037
  }
961
1038
 
962
1039
  // src/tools/skill.ts
963
- import { readFileSync as readFileSync2 } from "fs";
1040
+ import { readFileSync } from "fs";
964
1041
  import { z as z11 } from "zod";
965
1042
  function createSkillTool(skills) {
966
1043
  return defineTool({
@@ -972,7 +1049,7 @@ function createSkillTool(skills) {
972
1049
  async run(input) {
973
1050
  const skill = skills.find((s) => s.name === input.name);
974
1051
  if (!skill) return { output: `Skill "${input.name}" tidak ada. Tersedia: ${skills.map((s) => s.name).join(", ") || "(tidak ada)"}`, isError: true };
975
- const { body } = parseFrontmatter(readFileSync2(skill.file, "utf8"));
1052
+ const { body } = parseFrontmatter(readFileSync(skill.file, "utf8"));
976
1053
  return { output: `Base directory for this skill: ${skill.dir}
977
1054
 
978
1055
  ${body}`, display: skill.description.slice(0, 60) };
@@ -980,8 +1057,69 @@ ${body}`, display: skill.description.slice(0, 60) };
980
1057
  });
981
1058
  }
982
1059
 
983
- // src/tools/task.ts
1060
+ // src/tools/websearch.ts
984
1061
  import { z as z12 } from "zod";
1062
+ var clean = (items, max) => (Array.isArray(items) ? items : []).filter((r) => typeof r?.url === "string" && /^https?:\/\//.test(r.url)).slice(0, max).map((r) => ({
1063
+ title: String(r.title || r.url).slice(0, 200),
1064
+ url: r.url,
1065
+ snippet: String(r.snippet || r.description || "").replace(/\s+/g, " ").trim().slice(0, 600)
1066
+ }));
1067
+ async function postJson(url, body, headers, signal) {
1068
+ const res = await fetch(url, {
1069
+ method: "POST",
1070
+ headers: { "content-type": "application/json", ...headers },
1071
+ body: JSON.stringify(body),
1072
+ signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)])
1073
+ });
1074
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1075
+ return res.json();
1076
+ }
1077
+ function createWebSearchTool(opts) {
1078
+ const keenableURL = opts.keenableURL ?? "https://api.keenable.ai/v1/search/public";
1079
+ return defineTool({
1080
+ name: "web_search",
1081
+ 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.",
1082
+ schema: z12.object({
1083
+ query: z12.string().min(1).max(500).describe("Search query"),
1084
+ max_results: z12.number().int().min(1).max(10).optional().describe("Number of results (default 6)")
1085
+ }),
1086
+ kind: "read",
1087
+ target: (input) => input.query,
1088
+ parallelSafe: () => true,
1089
+ async run(input, ctx) {
1090
+ const max = input.max_results ?? 6;
1091
+ const errors = [];
1092
+ let results = [];
1093
+ const bc = opts.botconnector();
1094
+ if (bc) {
1095
+ try {
1096
+ const data = await postJson(`${bc.baseURL.replace(/\/+$/, "")}/web/search`, { query: input.query, max_results: max }, { authorization: `Bearer ${bc.apiKey}` }, ctx.signal);
1097
+ results = clean(data.results, max);
1098
+ } catch (error) {
1099
+ errors.push(`BotConnector: ${error.message}`);
1100
+ }
1101
+ }
1102
+ if (!results.length) {
1103
+ try {
1104
+ const data = await postJson(keenableURL, { query: input.query }, { "x-keenable-title": "BotConnector" }, ctx.signal);
1105
+ results = clean(data.results, max);
1106
+ } catch (error) {
1107
+ errors.push(`Keenable: ${error.message}`);
1108
+ }
1109
+ }
1110
+ if (!results.length) {
1111
+ return { output: errors.length ? `Web search gagal (${errors.join("; ")}).` : "Tidak ada hasil.", isError: errors.length > 0 };
1112
+ }
1113
+ const output = results.map((r, i) => `${i + 1}. ${r.title}
1114
+ ${r.url}${r.snippet ? `
1115
+ ${r.snippet}` : ""}`).join("\n");
1116
+ return { output, display: `${results.length} hasil` };
1117
+ }
1118
+ });
1119
+ }
1120
+
1121
+ // src/tools/task.ts
1122
+ import { z as z13 } from "zod";
985
1123
  function createTaskTool(opts) {
986
1124
  const byName = new Map(opts.agents.map((a) => [a.name, a]));
987
1125
  const list = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
@@ -989,10 +1127,10 @@ function createTaskTool(opts) {
989
1127
  name: "task",
990
1128
  description: `Delegate a self-contained sub-task to a subagent. It starts with an empty context and returns only its final report. Available agents:
991
1129
  ${list}`,
992
- schema: z12.object({
993
- agent: z12.string().describe("Agent name"),
994
- description: z12.string().describe("3-6 word summary shown to the user"),
995
- prompt: z12.string().describe("Full instructions; the subagent sees nothing else")
1130
+ schema: z13.object({
1131
+ agent: z13.string().describe("Agent name"),
1132
+ description: z13.string().describe("3-6 word summary shown to the user"),
1133
+ prompt: z13.string().describe("Full instructions; the subagent sees nothing else")
996
1134
  }),
997
1135
  kind: "read",
998
1136
  target: (input) => `[${input.agent}] ${input.description}`,
@@ -1027,7 +1165,10 @@ ${list}`,
1027
1165
  ${def.prompt}`,
1028
1166
  cwd: opts.cwd,
1029
1167
  maxSteps: def.maxSteps ?? 50,
1030
- label: def.name
1168
+ label: def.name,
1169
+ reasoning: opts.reasoning?.() ?? "auto",
1170
+ // Subagent edits belong to the parent's turn so /undo reverts them too.
1171
+ checkpoint: ctx.checkpoint
1031
1172
  });
1032
1173
  let outcome;
1033
1174
  let steps = 0;
@@ -1054,7 +1195,7 @@ ${def.prompt}`,
1054
1195
  }
1055
1196
 
1056
1197
  // src/tools/todo.ts
1057
- import { z as z13 } from "zod";
1198
+ import { z as z14 } from "zod";
1058
1199
  var TodoStore = class {
1059
1200
  items = [];
1060
1201
  listeners = /* @__PURE__ */ new Set();
@@ -1071,8 +1212,8 @@ function createTodoTool(store) {
1071
1212
  return defineTool({
1072
1213
  name: "todo_write",
1073
1214
  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.",
1074
- schema: z13.object({
1075
- todos: z13.array(z13.object({ content: z13.string(), status: z13.enum(["pending", "in_progress", "completed"]) }))
1215
+ schema: z14.object({
1216
+ todos: z14.array(z14.object({ content: z14.string(), status: z14.enum(["pending", "in_progress", "completed"]) }))
1076
1217
  }),
1077
1218
  kind: "read",
1078
1219
  target: (input) => `${input.todos.length} item`,
@@ -1094,9 +1235,10 @@ function createRuntime(opts) {
1094
1235
  const home = bccliHome(env);
1095
1236
  let config = loadConfig(opts.cwd, env);
1096
1237
  let modelRef = opts.args.model ?? config.model;
1238
+ let reasoning = opts.args.reasoning ?? config.reasoning;
1097
1239
  const makeProvider = (ref) => {
1098
1240
  const resolved = resolveModel(config, ref, env);
1099
- return opts.provider ?? createProvider({ baseURL: resolved.baseURL, apiKey: resolved.apiKey, model: resolved.model });
1241
+ return opts.provider ?? createProvider({ baseURL: resolved.baseURL, apiKey: resolved.apiKey, model: resolved.model, providerId: resolved.providerId });
1100
1242
  };
1101
1243
  const provider = makeProvider(modelRef);
1102
1244
  const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
@@ -1107,6 +1249,8 @@ function createRuntime(opts) {
1107
1249
  const commands = loadCommands(roots);
1108
1250
  const agentDefs = loadAgentDefs(roots);
1109
1251
  const todos = new TodoStore();
1252
+ const checkpoints = new CheckpointStore();
1253
+ let startedAt = /* @__PURE__ */ new Date();
1110
1254
  const interaction = { approvePlan: async () => "no" };
1111
1255
  let session = Session.create(home, opts.cwd);
1112
1256
  let history = opts.history;
@@ -1118,7 +1262,17 @@ function createRuntime(opts) {
1118
1262
  }
1119
1263
  }
1120
1264
  const systemPrompt = buildSystemPrompt({ cwd: opts.cwd, home, model: modelRef, skills });
1121
- const baseTools = [...ALL_TOOLS, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
1265
+ const webSearch = createWebSearchTool({
1266
+ botconnector: () => {
1267
+ try {
1268
+ const bc = resolveModel(config, "bc-cloud/search", env);
1269
+ return bc.apiKey ? { baseURL: bc.baseURL, apiKey: bc.apiKey } : void 0;
1270
+ } catch {
1271
+ return void 0;
1272
+ }
1273
+ }
1274
+ });
1275
+ const baseTools = [...ALL_TOOLS, webSearch, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
1122
1276
  const task = createTaskTool({
1123
1277
  agents: [...BUILTIN_AGENTS, ...agentDefs],
1124
1278
  // Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
@@ -1127,7 +1281,8 @@ function createRuntime(opts) {
1127
1281
  provider: () => agent.provider,
1128
1282
  providerFor: makeProvider,
1129
1283
  systemPrompt,
1130
- cwd: opts.cwd
1284
+ cwd: opts.cwd,
1285
+ reasoning: () => reasoning
1131
1286
  });
1132
1287
  const agent = new Agent({
1133
1288
  provider,
@@ -1136,8 +1291,11 @@ function createRuntime(opts) {
1136
1291
  systemPrompt,
1137
1292
  cwd: opts.cwd,
1138
1293
  history,
1294
+ reasoning,
1139
1295
  onMessage: (m) => session.append(m),
1140
- onReset: () => session.reset()
1296
+ onReset: () => session.reset(),
1297
+ onTurnStart: () => checkpoints.beginTurn(),
1298
+ checkpoint: (path) => checkpoints.snapshot(path)
1141
1299
  });
1142
1300
  const mcp = new McpManager({
1143
1301
  onChange: () => agent.setTools([...baseTools, task, ...mcp.tools()]),
@@ -1175,23 +1333,48 @@ function createRuntime(opts) {
1175
1333
  get modelRef() {
1176
1334
  return modelRef;
1177
1335
  },
1336
+ get reasoning() {
1337
+ return reasoning;
1338
+ },
1178
1339
  agent,
1179
1340
  get session() {
1180
1341
  return session;
1181
1342
  },
1182
1343
  resume(previous) {
1183
1344
  session = previous;
1184
- agent.messages = previous.load();
1345
+ agent.load(previous.load());
1346
+ agent.totalUsage = { inputTokens: 0, outputTokens: 0 };
1347
+ todos.set([]);
1348
+ checkpoints.clear();
1349
+ },
1350
+ newSession() {
1351
+ session = Session.create(home, opts.cwd);
1352
+ agent.load([]);
1353
+ agent.totalUsage = { inputTokens: 0, outputTokens: 0 };
1354
+ todos.set([]);
1355
+ checkpoints.clear();
1356
+ startedAt = /* @__PURE__ */ new Date();
1357
+ },
1358
+ rebuildSystemPrompt() {
1359
+ agent.setSystemPrompt(buildSystemPrompt({ cwd: opts.cwd, home, model: modelRef, skills }));
1360
+ },
1361
+ checkpoints,
1362
+ get startedAt() {
1363
+ return startedAt;
1185
1364
  },
1186
1365
  setModel(ref) {
1187
1366
  agent.provider = makeProvider(ref);
1188
1367
  modelRef = ref;
1368
+ },
1369
+ setReasoning(level) {
1370
+ reasoning = level;
1371
+ agent.reasoning = level;
1189
1372
  }
1190
1373
  };
1191
1374
  }
1192
1375
 
1193
1376
  // src/version.ts
1194
- var VERSION = true ? "0.3.0-beta.3" : "0.0.0-dev";
1377
+ var VERSION = true ? "0.4.0-beta.10" : "0.0.0-dev";
1195
1378
 
1196
1379
  // src/cli.ts
1197
1380
  async function main() {
@@ -1207,15 +1390,25 @@ async function main() {
1207
1390
  const cwd = process.cwd();
1208
1391
  if (args.command === "login") return runLogin(args.loginProvider ?? "bc-cloud", cwd);
1209
1392
  if (args.command === "mcp") {
1210
- const { runMcpCommand } = await import("./mcpCli-BV3IGR47.js");
1211
- const { readSecret } = await import("./login-WJMGVDRM.js");
1393
+ const { runMcpCommand } = await import("./mcpCli-DZWQPTOS.js");
1394
+ const { readSecret } = await import("./login-4C262MEI.js");
1212
1395
  return runMcpCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
1213
1396
  }
1214
1397
  if (args.command === "provider") {
1215
- const { runProviderCommand } = await import("./providerCli-SJRCBSRE.js");
1216
- const { readSecret } = await import("./login-WJMGVDRM.js");
1398
+ const { runProviderCommand } = await import("./providerCli-Q7LRM2NP.js");
1399
+ const { readSecret } = await import("./login-4C262MEI.js");
1217
1400
  return runProviderCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
1218
1401
  }
1402
+ if (args.command === "connect" || args.command === "disconnect" || args.command === "integrations") {
1403
+ const { runIntegrationCommand } = await import("./integrationCli-GEIMUL3M.js");
1404
+ return runIntegrationCommand(args, {
1405
+ env: process.env,
1406
+ cwd,
1407
+ out: (s) => console.log(s),
1408
+ err: (s) => console.error(s),
1409
+ fetch: globalThis.fetch
1410
+ });
1411
+ }
1219
1412
  if (args.command === "models") {
1220
1413
  const config = loadConfig(cwd);
1221
1414
  const resolved = resolveModel(config, args.model ?? config.model);
@@ -1230,7 +1423,7 @@ async function main() {
1230
1423
  }
1231
1424
  if (args.print) {
1232
1425
  if (!args.prompt) throw new ConfigError('Mode -p butuh tugas, contoh: bccli -p "jelaskan repo ini"');
1233
- const { serversToStart } = await import("./config-XPWPGNZJ.js");
1426
+ const { serversToStart } = await import("./config-YUNKPCNB.js");
1234
1427
  const plan = serversToStart(rt.home, cwd);
1235
1428
  for (const s of plan.needTrust) {
1236
1429
  console.error(`Server MCP project "${s.name}" dilewati (belum diizinkan; jalankan bccli interaktif sekali untuk menyetujuinya).`);
@@ -1243,7 +1436,7 @@ async function main() {
1243
1436
  }
1244
1437
  }
1245
1438
  if (!process.stdin.isTTY) throw new ConfigError('Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"');
1246
- const { startInteractive } = await import("./ui-HAW5NMFA.js");
1439
+ const { startInteractive } = await import("./ui-6GTQUJOE.js");
1247
1440
  return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
1248
1441
  }
1249
1442
  main().then(