@botconnector/bccli 0.4.0-beta.5 → 0.4.0-beta.7

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/README.md CHANGED
@@ -80,6 +80,13 @@ per provider; ketik untuk menyaring. Pilihan tersimpan sebagai default.
80
80
  izin (`permissionMode`, `allow`) dan provider yang sudah ada hanya diatur dari `~/.bccli/config.json`,
81
81
  supaya repo yang kamu clone tidak bisa memberi dirinya izin penuh atau mencuri API key.
82
82
 
83
+ ## Internet
84
+
85
+ - `web_search`: mencari di web tanpa perlu izin. Memakai akun BotConnector Cloud
86
+ bila ada key, kalau tidak langsung ke Keenable (gratis, tanpa key). Hasilnya
87
+ judul, URL, dan cuplikan singkat, jadi hemat token.
88
+ - `fetch`: membuka satu URL dari komputer ini (butuh izin per host).
89
+
83
90
  ## MCP
84
91
 
85
92
  `/mcp` (atau `bccli mcp add <nama>`) memasang server dari katalog:
@@ -35,7 +35,7 @@ function readSecret(prompt) {
35
35
  });
36
36
  }
37
37
  async function runLogin(providerId, cwd) {
38
- const { runProviderCommand } = await import("./providerCli-MXV6PD6F.js");
38
+ const { runProviderCommand } = await import("./providerCli-6ON7QEVN.js");
39
39
  const { parseCliArgs } = await import("./args-FCLPQ7TO.js");
40
40
  return runProviderCommand(parseCliArgs(["provider", "add", providerId]), {
41
41
  env: process.env,
@@ -191,6 +191,10 @@ async function readJson(res) {
191
191
  function createProvider({ baseURL, apiKey, model, fetch: doFetch = fetch, retryDelayMs = 1e3 }) {
192
192
  const headers = { "content-type": "application/json" };
193
193
  if (apiKey) headers.authorization = `Bearer ${apiKey}`;
194
+ if (/^https:\/\/api\.botconnector\.id\/v1\/?$/.test(baseURL) && apiKey?.startsWith("bc_live_")) {
195
+ headers["x-botconnector-client"] = "bccli";
196
+ headers["x-botconnector-client-version"] = "0.4.0";
197
+ }
194
198
  async function post(path, body, signal) {
195
199
  let lastError;
196
200
  for (let attempt = 0; attempt < 4; attempt++) {
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/print.ts
4
4
  function toolLabel(tool) {
5
+ if (tool === "web_search") return "Search";
5
6
  return tool.charAt(0).toUpperCase() + tool.slice(1);
6
7
  }
7
8
  async function runPrint(rt, prompt, io = { out: process.stdout, err: process.stderr }, opts = {}) {
package/dist/cli.js CHANGED
@@ -7,10 +7,10 @@ import {
7
7
  addProviderKey,
8
8
  createProvider,
9
9
  listAllModels
10
- } from "./chunk-C5AFWX7H.js";
10
+ } from "./chunk-FMM53YSU.js";
11
11
  import {
12
12
  runLogin
13
- } from "./chunk-GW36UMHZ.js";
13
+ } from "./chunk-BRTSYM5O.js";
14
14
  import {
15
15
  BUILTIN_AGENTS,
16
16
  buildSystemPrompt,
@@ -20,7 +20,7 @@ import {
20
20
  loadSkills,
21
21
  parseFrontmatter,
22
22
  runPrint
23
- } from "./chunk-5O4HD7IU.js";
23
+ } from "./chunk-KV27KTOX.js";
24
24
  import {
25
25
  Permissions
26
26
  } from "./chunk-ELML4KVQ.js";
@@ -300,6 +300,20 @@ async function readCapped(res) {
300
300
  function htmlToText(html) {
301
301
  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
302
  }
303
+ function describeFetchError(error) {
304
+ const cause = error.cause;
305
+ const code = cause?.code ?? "";
306
+ const detail = cause?.message ?? error.message;
307
+ if (error.name === "TimeoutError") return "tidak ada jawaban dalam 30 detik";
308
+ if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `${detail} \u2014 DNS tidak menemukan host (salah ketik, offline, atau diblokir DNS/ISP)`;
309
+ if (/CERT|SELF_SIGNED|UNABLE_TO/.test(code)) {
310
+ return `${detail} \u2014 sertifikat TLS ditolak; sering karena antivirus/proxy yang memeriksa HTTPS. Coba jalankan dengan NODE_OPTIONS=--use-system-ca`;
311
+ }
312
+ if (/ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR_CONNECT_TIMEOUT|UND_ERR_SOCKET/.test(code)) {
313
+ return `${detail} \u2014 koneksi gagal (firewall, proxy, atau situs diblokir jaringan)`;
314
+ }
315
+ return code ? `${detail} (${code})` : detail;
316
+ }
303
317
  var fetchTool = defineTool({
304
318
  name: "fetch",
305
319
  description: "Fetch a URL (http/https). HTML is converted to plain text. Max 50k characters.",
@@ -327,12 +341,12 @@ var fetchTool = defineTool({
327
341
  url = next;
328
342
  }
329
343
  } catch (error) {
330
- return { output: `Gagal mengambil ${input.url}: ${error.message}`, isError: true };
344
+ return { output: `Gagal mengambil ${input.url}: ${describeFetchError(error)}`, isError: true };
331
345
  }
332
346
  if (!res) return { output: `Gagal mengambil ${input.url}`, isError: true };
333
347
  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
348
  const text = (res.headers.get("content-type") ?? "").includes("html") ? htmlToText(body) : body;
349
+ if (!res.ok) return { output: `HTTP ${res.status} dari ${input.url}: ${text.slice(0, 500)}`, isError: true };
336
350
  const clipped = text.length > MAX_CHARS || truncated ? `${text.slice(0, MAX_CHARS)}
337
351
  \u2026 [dipotong]` : text;
338
352
  const display = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
@@ -892,8 +906,8 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
892
906
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
893
907
  import { z as z9 } from "zod";
894
908
  function mcpToolName(server, tool, taken) {
895
- const clean = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
896
- const base = `mcp__${clean(server)}__${clean(tool)}`.slice(0, 64);
909
+ const clean2 = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
910
+ const base = `mcp__${clean2(server)}__${clean2(tool)}`.slice(0, 64);
897
911
  let name = base;
898
912
  for (let n = 2; taken.has(name); n++) name = `${base.slice(0, 64 - String(n).length - 1)}_${n}`;
899
913
  taken.add(name);
@@ -1038,8 +1052,69 @@ ${body}`, display: skill.description.slice(0, 60) };
1038
1052
  });
1039
1053
  }
1040
1054
 
1041
- // src/tools/task.ts
1055
+ // src/tools/websearch.ts
1042
1056
  import { z as z12 } from "zod";
1057
+ var clean = (items, max) => (Array.isArray(items) ? items : []).filter((r) => typeof r?.url === "string" && /^https?:\/\//.test(r.url)).slice(0, max).map((r) => ({
1058
+ title: String(r.title || r.url).slice(0, 200),
1059
+ url: r.url,
1060
+ snippet: String(r.snippet || r.description || "").replace(/\s+/g, " ").trim().slice(0, 600)
1061
+ }));
1062
+ async function postJson(url, body, headers, signal) {
1063
+ const res = await fetch(url, {
1064
+ method: "POST",
1065
+ headers: { "content-type": "application/json", ...headers },
1066
+ body: JSON.stringify(body),
1067
+ signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)])
1068
+ });
1069
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1070
+ return res.json();
1071
+ }
1072
+ function createWebSearchTool(opts) {
1073
+ const keenableURL = opts.keenableURL ?? "https://api.keenable.ai/v1/search/public";
1074
+ return defineTool({
1075
+ name: "web_search",
1076
+ 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.",
1077
+ schema: z12.object({
1078
+ query: z12.string().min(1).max(500).describe("Search query"),
1079
+ max_results: z12.number().int().min(1).max(10).optional().describe("Number of results (default 6)")
1080
+ }),
1081
+ kind: "read",
1082
+ target: (input) => input.query,
1083
+ parallelSafe: () => true,
1084
+ async run(input, ctx) {
1085
+ const max = input.max_results ?? 6;
1086
+ const errors = [];
1087
+ let results = [];
1088
+ const bc = opts.botconnector();
1089
+ if (bc) {
1090
+ try {
1091
+ const data = await postJson(`${bc.baseURL.replace(/\/+$/, "")}/web/search`, { query: input.query, max_results: max }, { authorization: `Bearer ${bc.apiKey}` }, ctx.signal);
1092
+ results = clean(data.results, max);
1093
+ } catch (error) {
1094
+ errors.push(`BotConnector: ${error.message}`);
1095
+ }
1096
+ }
1097
+ if (!results.length) {
1098
+ try {
1099
+ const data = await postJson(keenableURL, { query: input.query }, { "x-keenable-title": "BotConnector" }, ctx.signal);
1100
+ results = clean(data.results, max);
1101
+ } catch (error) {
1102
+ errors.push(`Keenable: ${error.message}`);
1103
+ }
1104
+ }
1105
+ if (!results.length) {
1106
+ return { output: errors.length ? `Web search gagal (${errors.join("; ")}).` : "Tidak ada hasil.", isError: errors.length > 0 };
1107
+ }
1108
+ const output = results.map((r, i) => `${i + 1}. ${r.title}
1109
+ ${r.url}${r.snippet ? `
1110
+ ${r.snippet}` : ""}`).join("\n");
1111
+ return { output, display: `${results.length} hasil` };
1112
+ }
1113
+ });
1114
+ }
1115
+
1116
+ // src/tools/task.ts
1117
+ import { z as z13 } from "zod";
1043
1118
  function createTaskTool(opts) {
1044
1119
  const byName = new Map(opts.agents.map((a) => [a.name, a]));
1045
1120
  const list = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
@@ -1047,10 +1122,10 @@ function createTaskTool(opts) {
1047
1122
  name: "task",
1048
1123
  description: `Delegate a self-contained sub-task to a subagent. It starts with an empty context and returns only its final report. Available agents:
1049
1124
  ${list}`,
1050
- schema: z12.object({
1051
- agent: z12.string().describe("Agent name"),
1052
- description: z12.string().describe("3-6 word summary shown to the user"),
1053
- prompt: z12.string().describe("Full instructions; the subagent sees nothing else")
1125
+ schema: z13.object({
1126
+ agent: z13.string().describe("Agent name"),
1127
+ description: z13.string().describe("3-6 word summary shown to the user"),
1128
+ prompt: z13.string().describe("Full instructions; the subagent sees nothing else")
1054
1129
  }),
1055
1130
  kind: "read",
1056
1131
  target: (input) => `[${input.agent}] ${input.description}`,
@@ -1114,7 +1189,7 @@ ${def.prompt}`,
1114
1189
  }
1115
1190
 
1116
1191
  // src/tools/todo.ts
1117
- import { z as z13 } from "zod";
1192
+ import { z as z14 } from "zod";
1118
1193
  var TodoStore = class {
1119
1194
  items = [];
1120
1195
  listeners = /* @__PURE__ */ new Set();
@@ -1131,8 +1206,8 @@ function createTodoTool(store) {
1131
1206
  return defineTool({
1132
1207
  name: "todo_write",
1133
1208
  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.",
1134
- schema: z13.object({
1135
- todos: z13.array(z13.object({ content: z13.string(), status: z13.enum(["pending", "in_progress", "completed"]) }))
1209
+ schema: z14.object({
1210
+ todos: z14.array(z14.object({ content: z14.string(), status: z14.enum(["pending", "in_progress", "completed"]) }))
1136
1211
  }),
1137
1212
  kind: "read",
1138
1213
  target: (input) => `${input.todos.length} item`,
@@ -1180,7 +1255,17 @@ function createRuntime(opts) {
1180
1255
  }
1181
1256
  }
1182
1257
  const systemPrompt = buildSystemPrompt({ cwd: opts.cwd, home, model: modelRef, skills });
1183
- const baseTools = [...ALL_TOOLS, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
1258
+ const webSearch = createWebSearchTool({
1259
+ botconnector: () => {
1260
+ try {
1261
+ const bc = resolveModel(config, "bc-cloud/search", env);
1262
+ return bc.apiKey ? { baseURL: bc.baseURL, apiKey: bc.apiKey } : void 0;
1263
+ } catch {
1264
+ return void 0;
1265
+ }
1266
+ }
1267
+ });
1268
+ const baseTools = [...ALL_TOOLS, webSearch, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
1184
1269
  const task = createTaskTool({
1185
1270
  agents: [...BUILTIN_AGENTS, ...agentDefs],
1186
1271
  // Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
@@ -1273,7 +1358,7 @@ function createRuntime(opts) {
1273
1358
  }
1274
1359
 
1275
1360
  // src/version.ts
1276
- var VERSION = true ? "0.4.0-beta.5" : "0.0.0-dev";
1361
+ var VERSION = true ? "0.4.0-beta.7" : "0.0.0-dev";
1277
1362
 
1278
1363
  // src/cli.ts
1279
1364
  async function main() {
@@ -1290,12 +1375,12 @@ async function main() {
1290
1375
  if (args.command === "login") return runLogin(args.loginProvider ?? "bc-cloud", cwd);
1291
1376
  if (args.command === "mcp") {
1292
1377
  const { runMcpCommand } = await import("./mcpCli-Q2H63ISP.js");
1293
- const { readSecret } = await import("./login-F4G5QXOU.js");
1378
+ const { readSecret } = await import("./login-4277DGKX.js");
1294
1379
  return runMcpCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
1295
1380
  }
1296
1381
  if (args.command === "provider") {
1297
- const { runProviderCommand } = await import("./providerCli-MXV6PD6F.js");
1298
- const { readSecret } = await import("./login-F4G5QXOU.js");
1382
+ const { runProviderCommand } = await import("./providerCli-6ON7QEVN.js");
1383
+ const { readSecret } = await import("./login-4277DGKX.js");
1299
1384
  return runProviderCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
1300
1385
  }
1301
1386
  if (args.command === "models") {
@@ -1325,7 +1410,7 @@ async function main() {
1325
1410
  }
1326
1411
  }
1327
1412
  if (!process.stdin.isTTY) throw new ConfigError('Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"');
1328
- const { startInteractive } = await import("./ui-RYTSKEIA.js");
1413
+ const { startInteractive } = await import("./ui-4EBFPASA.js");
1329
1414
  return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
1330
1415
  }
1331
1416
  main().then(
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  readSecret,
4
4
  runLogin
5
- } from "./chunk-GW36UMHZ.js";
5
+ } from "./chunk-BRTSYM5O.js";
6
6
  export {
7
7
  readSecret,
8
8
  runLogin
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  addProviderKey,
4
4
  runProviderCommand
5
- } from "./chunk-C5AFWX7H.js";
5
+ } from "./chunk-FMM53YSU.js";
6
6
  import "./chunk-U7DLMVPZ.js";
7
7
  import "./chunk-RZUJYAQV.js";
8
8
  export {
@@ -5,7 +5,7 @@ import {
5
5
  instructionPaths,
6
6
  parseFrontmatter,
7
7
  toolLabel
8
- } from "./chunk-5O4HD7IU.js";
8
+ } from "./chunk-KV27KTOX.js";
9
9
  import {
10
10
  nextMode
11
11
  } from "./chunk-ELML4KVQ.js";
@@ -628,15 +628,40 @@ var NOTICES = {
628
628
  stepLimit: ['Batas 50 langkah tercapai. Ketik "lanjut" untuk meneruskan.', "warn"],
629
629
  aborted: ["Dibatalkan.", "warn"]
630
630
  };
631
+ var isEmptyCont = (e) => e.kind === "assistant" && e.cont && !e.text;
631
632
  function endTurn(t) {
632
- return { done: [...t.done, ...t.live], live: [] };
633
+ return { done: [...t.done, ...t.live.filter((e) => !isEmptyCont(e))], live: [] };
634
+ }
635
+ var TOOL_MARKUP = /<\/?tool_call>|<\/?arg_(?:key|value)>|Tool call list|^\s*\d+\.\s*Tool:|`\s*=\s*`/;
636
+ function finishedCut(text) {
637
+ let inFence = false;
638
+ let pos = 0;
639
+ let cut = -1;
640
+ const lines = text.split("\n");
641
+ for (const line of lines.slice(0, -1)) {
642
+ if (TOOL_MARKUP.test(line)) break;
643
+ if (line.trimStart().startsWith("```")) inFence = !inFence;
644
+ pos += line.length + 1;
645
+ if (!inFence) cut = pos - 1;
646
+ }
647
+ return cut;
648
+ }
649
+ function flushLines(t) {
650
+ const last = t.live.at(-1);
651
+ if (last?.kind !== "assistant") return t;
652
+ const cut = finishedCut(last.text);
653
+ if (cut < 0) return t;
654
+ const chunk = last.text.slice(0, cut);
655
+ if (!last.cont && !chunk.trim()) return t;
656
+ const rest = entry({ kind: "assistant", text: last.text.slice(cut + 1), cont: true, flushed: (last.flushed ?? 0) + cut + 1 });
657
+ return { done: [...t.done, ...t.live.slice(0, -1).filter((e) => !isEmptyCont(e)), { ...last, text: chunk || " " }], live: [rest] };
633
658
  }
634
659
  function applyEvent(t, event) {
635
660
  switch (event.type) {
636
661
  case "text": {
637
662
  const last = t.live.at(-1);
638
- if (last?.kind === "assistant") return { ...t, live: [...t.live.slice(0, -1), { ...last, text: last.text + event.delta }] };
639
- return { ...t, live: [...t.live, entry({ kind: "assistant", text: event.delta })] };
663
+ if (last?.kind === "assistant") return flushLines({ ...t, live: [...t.live.slice(0, -1), { ...last, text: last.text + event.delta }] });
664
+ return flushLines({ ...t, live: [...t.live, entry({ kind: "assistant", text: event.delta })] });
640
665
  }
641
666
  case "thinking": {
642
667
  const last = t.live.at(-1);
@@ -646,12 +671,12 @@ function applyEvent(t, event) {
646
671
  case "textReplace": {
647
672
  const last = t.live.at(-1);
648
673
  if (last?.kind !== "assistant") return t;
649
- return { ...t, live: [...t.live.slice(0, -1), { ...last, text: event.text }] };
674
+ return { ...t, live: [...t.live.slice(0, -1), { ...last, text: event.text.slice(last.flushed ?? 0) }] };
650
675
  }
651
676
  case "toolStart": {
652
677
  const running = t.live.some((e) => e.kind === "tool" && !e.done);
653
678
  const tool = entry({ kind: "tool", callId: event.id, tool: event.tool, target: event.target, done: false });
654
- return running ? { ...t, live: [...t.live, tool] } : { done: [...t.done, ...t.live], live: [tool] };
679
+ return running ? { ...t, live: [...t.live, tool] } : { done: [...t.done, ...t.live.filter((e) => !isEmptyCont(e))], live: [tool] };
655
680
  }
656
681
  case "toolEnd": {
657
682
  const live = t.live.map(
@@ -1226,6 +1251,9 @@ ${t.output}`);
1226
1251
  case "user":
1227
1252
  return /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: `> ${e.text}` }) }, e.id);
1228
1253
  case "assistant":
1254
+ if (e.cont) {
1255
+ return e.text ? /* @__PURE__ */ jsx15(Box13, { paddingLeft: 2, children: /* @__PURE__ */ jsx15(Markdown, { text: e.text }) }, e.id) : null;
1256
+ }
1229
1257
  if (!e.text.trim()) return null;
1230
1258
  return /* @__PURE__ */ jsxs10(Box13, { marginTop: 1, children: [
1231
1259
  /* @__PURE__ */ jsx15(Text15, { color: color(ACCENT), children: "\u25CF " }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botconnector/bccli",
3
- "version": "0.4.0-beta.5",
3
+ "version": "0.4.0-beta.7",
4
4
  "description": "BCCLI — BotConnector AI coding agent for the terminal",
5
5
  "type": "module",
6
6
  "bin": {