@botconnector/bccli 0.4.0-beta.2 → 0.4.0-beta.4

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.
@@ -12,6 +12,54 @@ import {
12
12
  saveCredential
13
13
  } from "./chunk-RZUJYAQV.js";
14
14
 
15
+ // src/thinking.ts
16
+ var OPEN = "<think>";
17
+ var CLOSE = "</think>";
18
+ function partialTag(text, tag) {
19
+ for (let n = Math.min(tag.length - 1, text.length); n > 0; n--) if (tag.startsWith(text.slice(-n))) return n;
20
+ return 0;
21
+ }
22
+ var ThinkSplitter = class {
23
+ inside = false;
24
+ held = "";
25
+ push(delta) {
26
+ let rest = this.held + delta;
27
+ this.held = "";
28
+ let text = "";
29
+ let thinking = "";
30
+ while (rest) {
31
+ const tag = this.inside ? CLOSE : OPEN;
32
+ const at = rest.indexOf(tag);
33
+ if (at >= 0) {
34
+ if (this.inside) thinking += rest.slice(0, at);
35
+ else text += rest.slice(0, at);
36
+ rest = rest.slice(at + tag.length);
37
+ this.inside = !this.inside;
38
+ continue;
39
+ }
40
+ const keep = partialTag(rest, tag);
41
+ const out = rest.slice(0, rest.length - keep);
42
+ if (this.inside) thinking += out;
43
+ else text += out;
44
+ this.held = rest.slice(rest.length - keep);
45
+ break;
46
+ }
47
+ return { text, thinking };
48
+ }
49
+ /** Whatever was held back as a possible tag start is plain text after all. */
50
+ flush() {
51
+ const held = this.held;
52
+ this.held = "";
53
+ return this.inside ? { text: "", thinking: held } : { text: held, thinking: "" };
54
+ }
55
+ };
56
+ function splitThinking(text) {
57
+ const s = new ThinkSplitter();
58
+ const a = s.push(text);
59
+ const b = s.flush();
60
+ return { text: a.text + b.text, thinking: a.thinking + b.thinking };
61
+ }
62
+
15
63
  // src/provider.ts
16
64
  var ProviderError = class extends Error {
17
65
  constructor(message, status) {
@@ -58,8 +106,21 @@ function errorMessage(body) {
58
106
  }
59
107
  }
60
108
  var toUsage = (u) => u ? { inputTokens: u.prompt_tokens ?? 0, outputTokens: u.completion_tokens ?? 0 } : void 0;
61
- async function readStream(res, onText) {
109
+ async function readStream(res, onText, onThinking) {
62
110
  let text = "";
111
+ let thinking = "";
112
+ const splitter = new ThinkSplitter();
113
+ const emit = (part) => {
114
+ if (part.thinking) {
115
+ thinking += part.thinking;
116
+ onThinking?.(part.thinking);
117
+ }
118
+ if (part.text) {
119
+ text += part.text;
120
+ onText?.(part.text);
121
+ if (loopAt < 0) loopAt = detectRepetition(text);
122
+ }
123
+ };
63
124
  let usage;
64
125
  let finishReason;
65
126
  const calls = [];
@@ -84,11 +145,9 @@ async function readStream(res, onText) {
84
145
  if (!choice) return;
85
146
  if (choice.finish_reason) finishReason = choice.finish_reason;
86
147
  const delta = choice.delta ?? {};
87
- if (delta.content) {
88
- text += delta.content;
89
- onText?.(delta.content);
90
- if (loopAt < 0) loopAt = detectRepetition(text);
91
- }
148
+ const reasoning = delta.reasoning_content || delta.reasoning;
149
+ if (reasoning) emit({ text: "", thinking: reasoning });
150
+ if (delta.content) emit(splitter.push(delta.content));
92
151
  for (const tc of delta.tool_calls ?? []) {
93
152
  let target = tc.index !== void 0 ? byIndex.get(tc.index) : current;
94
153
  if (!target || tc.index === void 0 && tc.id && target.id && target.id !== tc.id) {
@@ -110,11 +169,12 @@ async function readStream(res, onText) {
110
169
  buffer = buffer.slice(newline + 1);
111
170
  newline = buffer.indexOf("\n");
112
171
  }
113
- if (loopAt >= 0) return { text: text.slice(0, loopAt), toolCalls: [], usage, finishReason: "repetition" };
172
+ if (loopAt >= 0) return { text: text.slice(0, loopAt), toolCalls: [], thinking: thinking || void 0, usage, finishReason: "repetition" };
114
173
  }
115
174
  handle(buffer.trim());
175
+ emit(splitter.flush());
116
176
  const toolCalls = calls.map((call, index) => ({ ...call, id: call.id || `call_${index}` }));
117
- return { text, toolCalls, usage, finishReason };
177
+ return { text, toolCalls, thinking: thinking || void 0, usage, finishReason };
118
178
  }
119
179
  async function readJson(res) {
120
180
  const body = await res.json();
@@ -124,7 +184,9 @@ async function readJson(res) {
124
184
  name: tc.function?.name ?? "",
125
185
  arguments: tc.function?.arguments ?? ""
126
186
  }));
127
- return { text: choice?.message?.content ?? "", toolCalls, usage: toUsage(body.usage), finishReason: choice?.finish_reason };
187
+ const split = splitThinking(choice?.message?.content ?? "");
188
+ const thinking = (choice?.message?.reasoning_content || choice?.message?.reasoning || "") + split.thinking;
189
+ return { text: split.text, toolCalls, thinking: thinking || void 0, usage: toUsage(body.usage), finishReason: choice?.finish_reason };
128
190
  }
129
191
  function createProvider({ baseURL, apiKey, model, fetch: doFetch = fetch, retryDelayMs = 1e3 }) {
130
192
  const headers = { "content-type": "application/json" };
@@ -149,7 +211,7 @@ function createProvider({ baseURL, apiKey, model, fetch: doFetch = fetch, retryD
149
211
  throw lastError ?? new ProviderError("Permintaan gagal");
150
212
  }
151
213
  return {
152
- async chat({ messages, tools, signal, onText }) {
214
+ async chat({ messages, tools, signal, onText, onThinking }) {
153
215
  const res = await post(
154
216
  "/chat/completions",
155
217
  { model, messages, stream: true, stream_options: { include_usage: true }, ...tools?.length ? { tools } : {} },
@@ -157,7 +219,7 @@ function createProvider({ baseURL, apiKey, model, fetch: doFetch = fetch, retryD
157
219
  );
158
220
  if ((res.headers.get("content-type") ?? "").includes("application/json")) return readJson(res);
159
221
  if (!res.body) throw new ProviderError("Respons provider kosong");
160
- return readStream(res, onText);
222
+ return readStream(res, onText, onThinking);
161
223
  },
162
224
  async listModels() {
163
225
  const res = await doFetch(`${baseURL}/models`, { headers });
@@ -35,7 +35,7 @@ function readSecret(prompt) {
35
35
  });
36
36
  }
37
37
  async function runLogin(providerId, cwd) {
38
- const { runProviderCommand } = await import("./providerCli-G3X7KENO.js");
38
+ const { runProviderCommand } = await import("./providerCli-MXV6PD6F.js");
39
39
  const { parseCliArgs } = await import("./args-FCLPQ7TO.js");
40
40
  return runProviderCommand(parseCliArgs(["provider", "add", providerId]), {
41
41
  env: process.env,
package/dist/cli.js CHANGED
@@ -7,10 +7,10 @@ import {
7
7
  addProviderKey,
8
8
  createProvider,
9
9
  listAllModels
10
- } from "./chunk-2JBRLGKD.js";
10
+ } from "./chunk-C5AFWX7H.js";
11
11
  import {
12
12
  runLogin
13
- } from "./chunk-L7L4RGKG.js";
13
+ } from "./chunk-GW36UMHZ.js";
14
14
  import {
15
15
  BUILTIN_AGENTS,
16
16
  buildSystemPrompt,
@@ -521,6 +521,71 @@ function toolDefinitions(tools) {
521
521
  });
522
522
  }
523
523
 
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 {
536
+ }
537
+ }
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;
553
+ }
554
+ return tie ? void 0 : best;
555
+ }
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]]);
582
+ }
583
+ flush();
584
+ });
585
+ if (!toolCalls.length) return null;
586
+ return { text: text.slice(0, cut).trim(), toolCalls };
587
+ }
588
+
524
589
  // src/agent.ts
525
590
  var estimateTokens = (text) => Math.ceil(text.length / 4);
526
591
  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.";
@@ -615,12 +680,20 @@ ${completion.text}` });
615
680
  await this.compact(signal);
616
681
  this.push({ role: "user", content: `Lanjutkan tugas ini sesuai ringkasan di atas: ${text}` });
617
682
  }
618
- const completion = await this.provider.chat({
683
+ let completion = await this.provider.chat({
619
684
  messages: [{ role: "system", content: this.systemPrompt }, ...this.messages],
620
685
  tools: this.turnDefinitions,
621
686
  signal,
622
- onText: (delta) => this.onEvent({ type: "text", delta })
687
+ onText: (delta) => this.onEvent({ type: "text", delta }),
688
+ onThinking: (delta) => this.onEvent({ type: "thinking", delta })
623
689
  });
690
+ if (!completion.toolCalls.length && completion.finishReason !== "repetition") {
691
+ const recovered = recoverTextToolCalls(completion.text, this.turnDefinitions);
692
+ if (recovered) {
693
+ completion = { ...completion, ...recovered };
694
+ this.onEvent({ type: "textReplace", text: recovered.text });
695
+ }
696
+ }
624
697
  const usage = completion.usage ?? {
625
698
  inputTokens: estimateTokens(this.systemPrompt) + estimateTokens(JSON.stringify(this.messages)),
626
699
  outputTokens: estimateTokens(completion.text) + (completion.toolCalls.length ? estimateTokens(JSON.stringify(completion.toolCalls)) : 0)
@@ -1200,7 +1273,7 @@ function createRuntime(opts) {
1200
1273
  }
1201
1274
 
1202
1275
  // src/version.ts
1203
- var VERSION = true ? "0.4.0-beta.2" : "0.0.0-dev";
1276
+ var VERSION = true ? "0.4.0-beta.4" : "0.0.0-dev";
1204
1277
 
1205
1278
  // src/cli.ts
1206
1279
  async function main() {
@@ -1217,12 +1290,12 @@ async function main() {
1217
1290
  if (args.command === "login") return runLogin(args.loginProvider ?? "bc-cloud", cwd);
1218
1291
  if (args.command === "mcp") {
1219
1292
  const { runMcpCommand } = await import("./mcpCli-Q2H63ISP.js");
1220
- const { readSecret } = await import("./login-KHU7ONSR.js");
1293
+ const { readSecret } = await import("./login-F4G5QXOU.js");
1221
1294
  return runMcpCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
1222
1295
  }
1223
1296
  if (args.command === "provider") {
1224
- const { runProviderCommand } = await import("./providerCli-G3X7KENO.js");
1225
- const { readSecret } = await import("./login-KHU7ONSR.js");
1297
+ const { runProviderCommand } = await import("./providerCli-MXV6PD6F.js");
1298
+ const { readSecret } = await import("./login-F4G5QXOU.js");
1226
1299
  return runProviderCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
1227
1300
  }
1228
1301
  if (args.command === "models") {
@@ -1252,7 +1325,7 @@ async function main() {
1252
1325
  }
1253
1326
  }
1254
1327
  if (!process.stdin.isTTY) throw new ConfigError('Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"');
1255
- const { startInteractive } = await import("./ui-A32PTF4F.js");
1328
+ const { startInteractive } = await import("./ui-7FXPXXGN.js");
1256
1329
  return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
1257
1330
  }
1258
1331
  main().then(
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  readSecret,
4
4
  runLogin
5
- } from "./chunk-L7L4RGKG.js";
5
+ } from "./chunk-GW36UMHZ.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-2JBRLGKD.js";
5
+ } from "./chunk-C5AFWX7H.js";
6
6
  import "./chunk-U7DLMVPZ.js";
7
7
  import "./chunk-RZUJYAQV.js";
8
8
  export {
@@ -638,6 +638,11 @@ function applyEvent(t, event) {
638
638
  if (last?.kind === "assistant") return { ...t, live: [...t.live.slice(0, -1), { ...last, text: last.text + event.delta }] };
639
639
  return { ...t, live: [...t.live, entry({ kind: "assistant", text: event.delta })] };
640
640
  }
641
+ case "thinking": {
642
+ const last = t.live.at(-1);
643
+ if (last?.kind === "thinking") return { ...t, live: [...t.live.slice(0, -1), { ...last, text: last.text + event.delta }] };
644
+ return { ...t, live: [...t.live, entry({ kind: "thinking", text: event.delta })] };
645
+ }
641
646
  case "textReplace": {
642
647
  const last = t.live.at(-1);
643
648
  if (last?.kind !== "assistant") return t;
@@ -687,6 +692,7 @@ function helpText(runtime) {
687
692
  "",
688
693
  "shift+tab ganti mode izin esc batalkan giliran",
689
694
  "ctrl+o output alat terakhir lengkap \\ + enter baris baru",
695
+ "ctrl+t buka/tutup thinking model",
690
696
  "@file + tab lengkapi nama file \u2191\u2193 riwayat input"
691
697
  ].join("\n");
692
698
  }
@@ -713,6 +719,8 @@ function App({ runtime, initialPrompt, version }) {
713
719
  const [mcpMenu, setMcpMenu] = useState8(null);
714
720
  const [listPicker, setListPicker] = useState8(null);
715
721
  const [planAsk, setPlanAsk] = useState8(null);
722
+ const [queued, setQueued] = useState8([]);
723
+ const [showThinking, setShowThinking] = useState8(false);
716
724
  useEffect2(() => runtime.todos.subscribe(setTodos), [runtime]);
717
725
  const extraCommands = [
718
726
  ...runtime.commands.filter((c) => !SLASH_COMMANDS.some((b) => b.name === c.name.toLowerCase())).map((c) => ({ name: c.name.toLowerCase(), description: c.description ?? "perintah custom" })),
@@ -1086,6 +1094,19 @@ ARGUMENTS: ${slash.args}`);
1086
1094
  },
1087
1095
  [runSlash, runTurn, runtime, notice]
1088
1096
  );
1097
+ const onPrompt = useCallback(
1098
+ (text) => {
1099
+ if (busy) setQueued((q) => [...q, text]);
1100
+ else submit(text);
1101
+ },
1102
+ [busy, submit]
1103
+ );
1104
+ useEffect2(() => {
1105
+ if (busy || pending || planAsk || !queued.length) return;
1106
+ const [next, ...rest] = queued;
1107
+ setQueued(rest);
1108
+ submit(next);
1109
+ }, [busy, pending, planAsk, queued, submit]);
1089
1110
  const initialSent = useRef(false);
1090
1111
  useEffect2(() => {
1091
1112
  if (initialPrompt && !initialSent.current) {
@@ -1100,6 +1121,25 @@ ARGUMENTS: ${slash.args}`);
1100
1121
  setMode(next);
1101
1122
  } else if (key.escape && busy && !pending) {
1102
1123
  controller.current?.abort();
1124
+ if (queued.length) {
1125
+ setQueued([]);
1126
+ notice(`${queued.length} pesan antrian dibatalkan.`, "warn");
1127
+ }
1128
+ } else if (key.ctrl && input === "t") {
1129
+ const next = !showThinking;
1130
+ setShowThinking(next);
1131
+ setTranscript((t) => {
1132
+ const last = [...t.done, ...t.live].findLast((e) => e.kind === "thinking");
1133
+ const reprint = next && last?.kind === "thinking" && t.done.includes(last);
1134
+ return {
1135
+ ...t,
1136
+ done: [
1137
+ ...t.done,
1138
+ ...reprint ? [entry({ kind: "thinking", text: last.text, open: true })] : [],
1139
+ entry({ kind: "notice", text: next ? "Thinking ditampilkan (ctrl+t untuk menutup)." : "Thinking disembunyikan (ctrl+t untuk membuka).", tone: "info" })
1140
+ ]
1141
+ };
1142
+ });
1103
1143
  } else if (key.ctrl && input === "o" && lastTool.current) {
1104
1144
  const t = lastTool.current;
1105
1145
  notice(`\u23BF ${t.tool} ${t.target}
@@ -1180,6 +1220,18 @@ ${t.output}`);
1180
1220
  /* @__PURE__ */ jsx15(Text15, { color: color(ACCENT), children: "\u25CF " }),
1181
1221
  /* @__PURE__ */ jsx15(Markdown, { text: e.text.replace(/^\s+/, "") })
1182
1222
  ] }, e.id);
1223
+ case "thinking": {
1224
+ const text = e.text.trim();
1225
+ if (!text) return null;
1226
+ if (!(e.open ?? showThinking)) {
1227
+ const lines = text.split("\n").length;
1228
+ return /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: `\u273B Berpikir \xB7 ${lines} baris \xB7 ctrl+t buka` }) }, e.id);
1229
+ }
1230
+ return /* @__PURE__ */ jsxs10(Box13, { marginTop: 1, flexDirection: "column", children: [
1231
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "\u273B Berpikir (ctrl+t tutup)" }),
1232
+ /* @__PURE__ */ jsx15(Box13, { paddingLeft: 2, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, italic: true, children: text }) })
1233
+ ] }, e.id);
1234
+ }
1183
1235
  case "tool":
1184
1236
  return /* @__PURE__ */ jsx15(ToolBlock, { tool: e.tool, target: e.target, output: e.output, display: e.display, isError: e.isError, done: e.done, sub: e.sub }, e.id);
1185
1237
  case "notice":
@@ -1232,7 +1284,8 @@ ${t.output}`);
1232
1284
  }
1233
1285
  ) : null,
1234
1286
  /* @__PURE__ */ jsx15(TodoList, { items: todos }),
1235
- /* @__PURE__ */ jsx15(PromptInput, { disabled: busy || !!pending || !!picker || !!providerMenu || !!prompt || !!planAsk || !!mcpMenu || !!listPicker, history, cwd: runtime.cwd, onSubmit: submit, extraCommands }),
1287
+ queued.map((q, i) => /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: ` \u23F3 antri: ${q}` }, `${i}-${q}`)),
1288
+ /* @__PURE__ */ jsx15(PromptInput, { disabled: !!pending || !!picker || !!providerMenu || !!prompt || !!planAsk || !!mcpMenu || !!listPicker, history, cwd: runtime.cwd, onSubmit: onPrompt, extraCommands }),
1236
1289
  /* @__PURE__ */ jsx15(StatusBar, { mode, tokens, busy, model: modelLabel })
1237
1290
  ] });
1238
1291
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botconnector/bccli",
3
- "version": "0.4.0-beta.2",
3
+ "version": "0.4.0-beta.4",
4
4
  "description": "BCCLI — BotConnector AI coding agent for the terminal",
5
5
  "type": "module",
6
6
  "bin": {