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

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.
Files changed (58) hide show
  1. package/README.md +97 -2
  2. package/dist/acp-NWP4FFTG.js +182 -0
  3. package/dist/args-2R45YZ42.js +13 -0
  4. package/dist/{chunk-U7DLMVPZ.js → chunk-2QYHSESB.js} +2 -1
  5. package/dist/{chunk-UESQNU4A.js → chunk-4WVIPZFV.js} +14 -9
  6. package/dist/chunk-7F3IWBPQ.js +191 -0
  7. package/dist/chunk-DP7EBW53.js +341 -0
  8. package/dist/{chunk-SJH3YLW4.js → chunk-F6ACN47V.js} +60 -6
  9. package/dist/chunk-FTFVW7XD.js +235 -0
  10. package/dist/{chunk-ELML4KVQ.js → chunk-GAFJEQTJ.js} +32 -2
  11. package/dist/{chunk-NGLKANMD.js → chunk-H73FO5YK.js} +24 -2
  12. package/dist/chunk-KD2LMIS4.js +145 -0
  13. package/dist/chunk-KIZC5XV4.js +2141 -0
  14. package/dist/chunk-N7KP6RNP.js +76 -0
  15. package/dist/chunk-NDEJUL44.js +361 -0
  16. package/dist/chunk-PSO7R5AD.js +10 -0
  17. package/dist/chunk-QXOV4DUZ.js +56 -0
  18. package/dist/chunk-QZKLZELZ.js +64 -0
  19. package/dist/chunk-R4BJBTWC.js +940 -0
  20. package/dist/chunk-SNKVS26H.js +63 -0
  21. package/dist/chunk-TQWCM5RO.js +231 -0
  22. package/dist/chunk-VU5HNQHX.js +8 -0
  23. package/dist/chunk-XLC6T4SX.js +500 -0
  24. package/dist/chunk-YIYX3FFS.js +733 -0
  25. package/dist/cli.js +157 -1281
  26. package/dist/completion-OV3MW2N4.js +125 -0
  27. package/dist/{config-NPSYFAFD.js → config-AZZFOXKV.js} +5 -2
  28. package/dist/config-H5A7XHHK.js +30 -0
  29. package/dist/ghBotCli-7WDYF4L7.js +677 -0
  30. package/dist/integrationCli-ZMTSILYF.js +685 -0
  31. package/dist/login-W4Y6WHLV.js +16 -0
  32. package/dist/{mcpCli-Q2H63ISP.js → mcpCli-5QISTOFK.js} +42 -9
  33. package/dist/modelsCli-FZMBD4OE.js +36 -0
  34. package/dist/npmRunner-LU43GM3W.js +46 -0
  35. package/dist/oauthLogin-VESMXG4M.js +210 -0
  36. package/dist/pluginsCli-GEHR7FQR.js +284 -0
  37. package/dist/prCli-JXA7DJW4.js +72 -0
  38. package/dist/providerCli-EUZGKP5F.js +15 -0
  39. package/dist/sdk.js +5486 -0
  40. package/dist/serve-6KX4MXHC.js +178 -0
  41. package/dist/serve-YSUJD57D.js +515 -0
  42. package/dist/session-F6MG3UCU.js +12 -0
  43. package/dist/setup-LLWREJQH.js +24 -0
  44. package/dist/statsCli-H4HEY5ID.js +353 -0
  45. package/dist/transferCli-2XUKBG2L.js +71 -0
  46. package/dist/ui-6HJVYI46.js +2370 -0
  47. package/dist/update-O4WCLKOO.js +109 -0
  48. package/package.json +24 -3
  49. package/dist/args-FCLPQ7TO.js +0 -11
  50. package/dist/chunk-5O4HD7IU.js +0 -253
  51. package/dist/chunk-C5AFWX7H.js +0 -328
  52. package/dist/chunk-GW36UMHZ.js +0 -52
  53. package/dist/chunk-RZUJYAQV.js +0 -112
  54. package/dist/chunk-UN7QXPCB.js +0 -82
  55. package/dist/login-F4G5QXOU.js +0 -9
  56. package/dist/providerCli-MXV6PD6F.js +0 -11
  57. package/dist/session-7UB77CEG.js +0 -9
  58. package/dist/ui-RYTSKEIA.js +0 -1344
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ defineTool
4
+ } from "./chunk-PSO7R5AD.js";
5
+ import {
6
+ t
7
+ } from "./chunk-XLC6T4SX.js";
8
+
9
+ // src/tools/websearch.ts
10
+ import { z } from "zod";
11
+ var clean = (items, max) => (Array.isArray(items) ? items : []).filter((r) => typeof r?.url === "string" && /^https?:\/\//.test(r.url)).slice(0, max).map((r) => ({
12
+ title: String(r.title || r.url).slice(0, 200),
13
+ url: r.url,
14
+ snippet: String(r.snippet || r.description || "").replace(/\s+/g, " ").trim().slice(0, 600)
15
+ }));
16
+ async function postJson(url, body, headers, signal) {
17
+ const res = await fetch(url, {
18
+ method: "POST",
19
+ headers: { "content-type": "application/json", ...headers },
20
+ body: JSON.stringify(body),
21
+ signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)])
22
+ });
23
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
24
+ return res.json();
25
+ }
26
+ function createWebSearchTool(opts) {
27
+ const keenableURL = opts.keenableURL ?? "https://api.keenable.ai/v1/search/public";
28
+ return defineTool({
29
+ name: "web_search",
30
+ 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.",
31
+ schema: z.object({
32
+ query: z.string().min(1).max(500).describe("Search query"),
33
+ max_results: z.number().int().min(1).max(10).optional().describe("Number of results (default 6)")
34
+ }),
35
+ kind: "read",
36
+ target: (input) => input.query,
37
+ parallelSafe: () => true,
38
+ async run(input, ctx) {
39
+ const max = input.max_results ?? 6;
40
+ const errors = [];
41
+ let results = [];
42
+ const bc = opts.botconnector();
43
+ if (bc) {
44
+ try {
45
+ const data = await postJson(`${bc.baseURL.replace(/\/+$/, "")}/web/search`, { query: input.query, max_results: max }, { authorization: `Bearer ${bc.apiKey}` }, ctx.signal);
46
+ results = clean(data.results, max);
47
+ } catch (error) {
48
+ errors.push(`BotConnector: ${error.message}`);
49
+ }
50
+ }
51
+ if (!results.length) {
52
+ try {
53
+ const data = await postJson(keenableURL, { query: input.query }, { "x-keenable-title": "BotConnector" }, ctx.signal);
54
+ results = clean(data.results, max);
55
+ } catch (error) {
56
+ errors.push(`Keenable: ${error.message}`);
57
+ }
58
+ }
59
+ if (!results.length) {
60
+ return { output: errors.length ? `Web search failed (${errors.join("; ")}).` : "No results.", isError: errors.length > 0 };
61
+ }
62
+ const output = results.map((r, i) => `${i + 1}. ${r.title}
63
+ ${r.url}${r.snippet ? `
64
+ ${r.snippet}` : ""}`).join("\n");
65
+ return { output, display: t("{n} results", { n: results.length }) };
66
+ }
67
+ });
68
+ }
69
+
70
+ // src/tools/fetch.ts
71
+ import { lookup as dnsCbLookup } from "dns";
72
+ import { lookup as dnsLookup } from "dns/promises";
73
+ import { isIP } from "net";
74
+ import { Agent, fetch as undiciFetch } from "undici";
75
+ import { z as z2 } from "zod";
76
+ var MAX_CHARS = 12e3;
77
+ var FOCUS_CHUNKS = 3;
78
+ var FOCUS_SIZE = 800;
79
+ var CACHE_MAX = 16;
80
+ var CACHE_TTL_MS = 15 * 6e4;
81
+ var CACHE_MAX_CHARS = 2e6;
82
+ var MAX_BYTES = 5 * 1024 * 1024;
83
+ async function readCapped(res) {
84
+ const reader = res.body?.getReader();
85
+ if (!reader) return { text: "", truncated: false };
86
+ const chunks = [];
87
+ let size = 0;
88
+ for (; ; ) {
89
+ const { done, value } = await reader.read();
90
+ if (done) return { text: Buffer.concat(chunks).toString("utf8"), truncated: false };
91
+ size += value.length;
92
+ if (size > MAX_BYTES) {
93
+ chunks.push(value.subarray(0, value.length - (size - MAX_BYTES)));
94
+ await reader.cancel();
95
+ return { text: Buffer.concat(chunks).toString("utf8"), truncated: true };
96
+ }
97
+ chunks.push(value);
98
+ }
99
+ }
100
+ function htmlToText(html) {
101
+ 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");
102
+ }
103
+ function describeFetchError(error) {
104
+ const cause = error.cause;
105
+ const code = cause?.code ?? "";
106
+ const detail = cause?.message ?? error.message;
107
+ if (error.name === "TimeoutError") return "no response within 30 seconds";
108
+ if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `${detail} \u2014 DNS could not find the host (typo, offline, or blocked by DNS/ISP)`;
109
+ if (/CERT|SELF_SIGNED|UNABLE_TO/.test(code)) {
110
+ return `${detail} \u2014 TLS certificate rejected; often caused by an antivirus or proxy that inspects HTTPS. Try running with NODE_OPTIONS=--use-system-ca`;
111
+ }
112
+ if (/ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR_CONNECT_TIMEOUT|UND_ERR_SOCKET/.test(code)) {
113
+ return `${detail} \u2014 connection failed (firewall, proxy, or the site is blocked by the network)`;
114
+ }
115
+ return code ? `${detail} (${code})` : detail;
116
+ }
117
+ var tokensOf = (text) => Math.ceil(text.length / 4);
118
+ function isPrivateHost(host) {
119
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "");
120
+ if (h === "localhost" || /\.(local|internal|lan|localdomain|home\.arpa)$/.test(h)) return true;
121
+ if (h.includes(":")) return /^(::1?$|f[cd]|fe[89ab]|::ffff:)/.test(h);
122
+ const v4 = h.match(/^(\d+)\.(\d+)\.\d+\.\d+$/);
123
+ if (v4) {
124
+ const [a, b] = [Number(v4[1]), Number(v4[2])];
125
+ 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;
126
+ }
127
+ return !h.includes(".");
128
+ }
129
+ var words = (s) => s.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [];
130
+ function pinLookup(pinned) {
131
+ return (hostname, options, callback) => {
132
+ const opts = typeof options === "object" && options !== null ? options : {};
133
+ const family = typeof options === "number" ? options : opts.family;
134
+ const realDns = () => dnsCbLookup(hostname, typeof options === "number" ? { family: options } : opts, callback);
135
+ if (!pinned.length) return realDns();
136
+ const list = family ? pinned.filter((a) => a.family === family) : pinned;
137
+ if (!list.length) {
138
+ callback(Object.assign(new Error(`no pinned address for family ${family}`), { code: "ENOTFOUND" }), "");
139
+ return;
140
+ }
141
+ if (opts.all) callback(null, list);
142
+ else callback(null, list[0].address, list[0].family);
143
+ };
144
+ }
145
+ function pickRelevant(text, query, count = FOCUS_CHUNKS, size = FOCUS_SIZE) {
146
+ const chunks = [];
147
+ let cur = "";
148
+ for (const line of text.split("\n")) {
149
+ for (let i = 0; i < Math.max(line.length, 1); i += size) {
150
+ cur += `${line.slice(i, i + size)}
151
+ `;
152
+ if (cur.length >= size) {
153
+ chunks.push(cur.trimEnd());
154
+ cur = "";
155
+ }
156
+ }
157
+ }
158
+ if (cur.trim()) chunks.push(cur.trimEnd());
159
+ const q = [...new Set(words(query))];
160
+ const docs = chunks.map(words);
161
+ const avg = docs.reduce((n, d) => n + d.length, 0) / (docs.length || 1) || 1;
162
+ const idf = q.map((t2) => {
163
+ const df = docs.filter((d) => d.includes(t2)).length;
164
+ return Math.log(1 + (docs.length - df + 0.5) / (df + 0.5));
165
+ });
166
+ const scored = docs.map((d, i) => ({
167
+ i,
168
+ score: q.reduce((sum, t2, k) => {
169
+ const tf = d.filter((w) => w === t2).length;
170
+ return sum + idf[k] * tf * 2.2 / (tf + 1.2 * (0.25 + 0.75 * d.length / avg));
171
+ }, 0)
172
+ })).filter((c) => c.score > 0).sort((a, b) => b.score - a.score).slice(0, count).sort((a, b) => a.i - b.i);
173
+ return scored.length ? scored.map((c) => chunks[c.i]).join("\n\u2026\n") : null;
174
+ }
175
+ var cache = /* @__PURE__ */ new Map();
176
+ function remember(key, value, size) {
177
+ if (size > CACHE_MAX_CHARS) return;
178
+ cache.delete(key);
179
+ cache.set(key, value);
180
+ if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
181
+ }
182
+ function recall(key) {
183
+ const hit = cache.get(key);
184
+ if (typeof hit === "object" && Date.now() - hit.at > CACHE_TTL_MS) {
185
+ cache.delete(key);
186
+ return void 0;
187
+ }
188
+ return hit;
189
+ }
190
+ var ACCEPT = "text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.5";
191
+ var withCost = (text, what) => `${what} \xB7 ${t("~{n} tokens", { n: tokensOf(text) })}`;
192
+ function createFetchTool(opts = {}) {
193
+ const keenableURL = opts.keenableURL === void 0 ? "https://api.keenable.ai/v1/fetch/public" : opts.keenableURL;
194
+ const isPrivate = opts.isPrivate ?? isPrivateHost;
195
+ const lookup = opts.lookup ?? ((hostname) => dnsLookup(hostname, { all: true }));
196
+ return defineTool({
197
+ name: "fetch",
198
+ description: `Fetch a URL (http/https) as text/markdown. Costs the user tokens, so ask for as little as you need:
199
+ - 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.
200
+ - Without \`prompt\` you get the first ${MAX_CHARS} characters; the reply tells you the \`offset\` to continue from.
201
+ - Fetching a page that is already in this conversation is wasteful; reuse what you have.`,
202
+ schema: z2.object({
203
+ url: z2.string().describe("Absolute http(s) URL"),
204
+ prompt: z2.string().max(2e3).optional().describe("What to look for in the page; returns only the relevant excerpt"),
205
+ offset: z2.number().int().min(0).optional().describe("Start reading at this character (to continue a truncated page)")
206
+ }),
207
+ kind: "fetch",
208
+ target: (input) => input.url,
209
+ async run(input, ctx) {
210
+ if (!/^https?:\/\//i.test(input.url)) return { output: "URL must start with http:// or https://", isError: true };
211
+ const target = new URL(input.url);
212
+ let pinned;
213
+ if (!isPrivate(target.hostname) && isIP(target.hostname) === 0) {
214
+ try {
215
+ const addresses = await lookup(target.hostname);
216
+ const internal = addresses.find((a) => isPrivate(a.address));
217
+ if (internal) {
218
+ return {
219
+ output: t("Blocked: {host} resolves to a private address ({addr}). Fetching an internal host through a public DNS name is not allowed.", { host: target.hostname, addr: internal.address }),
220
+ isError: true
221
+ };
222
+ }
223
+ pinned = addresses;
224
+ } catch {
225
+ }
226
+ }
227
+ const prompt = input.prompt?.trim();
228
+ const askKeenable = !!prompt && !input.offset && !!keenableURL && !target.username && !target.password && !isPrivate(target.hostname);
229
+ if (askKeenable) {
230
+ const key = `x
231
+ ${input.url}
232
+ ${prompt}`;
233
+ const hit = recall(key);
234
+ if (hit) {
235
+ ctx.refundFetch?.();
236
+ return { output: hit, display: withCost(hit, t("focused excerpt \xB7 cache")) };
237
+ }
238
+ const out = await extractWithKeenable(keenableURL, input.url, prompt, ctx.signal);
239
+ if (out) {
240
+ const output2 = `[Excerpt from ${input.url} for "${prompt}" (extracted by Keenable). If it says the answer is not there, try fetch without a prompt.]
241
+ ${out}`;
242
+ remember(key, output2, output2.length);
243
+ return { output: output2, display: withCost(output2, t("focused excerpt (Keenable)")) };
244
+ }
245
+ }
246
+ let page = recall(`p
247
+ ${input.url}`);
248
+ const cached = !!page;
249
+ if (cached) ctx.refundFetch?.();
250
+ if (!page) {
251
+ const loaded = await loadPage(input.url, ctx.signal, pinned, opts.fetchPage);
252
+ if ("error" in loaded) return { output: loaded.error, isError: true };
253
+ page = loaded;
254
+ remember(`p
255
+ ${input.url}`, page, page.text.length);
256
+ }
257
+ const { text, truncated } = page;
258
+ const via = cached ? " \xB7 cache" : "";
259
+ if (prompt && !input.offset) {
260
+ const picked = pickRelevant(text, prompt);
261
+ if (picked) {
262
+ const output2 = `[Most relevant excerpt from ${input.url} for "${prompt}" (${text.length} characters total). Full page: fetch without a prompt.]
263
+ ${picked}`;
264
+ return { output: output2, display: withCost(output2, `${t("focused excerpt (local)")}${via}`) };
265
+ }
266
+ }
267
+ const offset = input.offset ?? 0;
268
+ if (offset >= text.length && text.length > 0) {
269
+ return { output: `offset ${offset} is past the end of the page (${text.length} characters).`, isError: true };
270
+ }
271
+ const end = Math.min(offset + MAX_CHARS, text.length);
272
+ let output = text.slice(offset, end);
273
+ if (end < text.length || truncated) {
274
+ const total = truncated ? `${text.length}+` : text.length;
275
+ output += `
276
+ \u2026 [truncated: characters ${offset}-${end} of ${total}. Continue with offset=${end}, or use prompt for a focused excerpt.]`;
277
+ }
278
+ const size = truncated ? t("over 5 MB, truncated") : t("{n} characters", { n: text.length });
279
+ return { output, display: withCost(output, `${size}${via}`) };
280
+ }
281
+ });
282
+ }
283
+ async function extractWithKeenable(endpoint, url, prompt, signal) {
284
+ try {
285
+ const u = new URL(endpoint);
286
+ u.searchParams.set("url", url);
287
+ u.searchParams.set("prompt", prompt);
288
+ u.searchParams.set("live", "true");
289
+ u.searchParams.set("max_chars", String(MAX_CHARS));
290
+ const res = await fetch(u, { headers: { "X-Keenable-Title": "BotConnector" }, signal: AbortSignal.any([signal, AbortSignal.timeout(25e3)]) });
291
+ if (!res.ok) return void 0;
292
+ const content = (await res.json()).content;
293
+ return typeof content === "string" && content.trim() ? content.slice(0, MAX_CHARS) : void 0;
294
+ } catch {
295
+ return void 0;
296
+ }
297
+ }
298
+ async function loadPage(inputUrl, signal, pinned, fetchPage) {
299
+ let url = new URL(inputUrl);
300
+ const dispatcher = pinned?.length ? new Agent({ connect: { lookup: pinLookup(pinned) } }) : void 0;
301
+ const doFetch = fetchPage ?? (dispatcher ? undiciFetch : fetch);
302
+ try {
303
+ let res;
304
+ for (let hop = 0; hop < 5; hop++) {
305
+ res = await doFetch(url, {
306
+ headers: { accept: ACCEPT },
307
+ signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)]),
308
+ redirect: "manual",
309
+ ...dispatcher ? { dispatcher } : {}
310
+ });
311
+ const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
312
+ if (!location) break;
313
+ const next = new URL(location, url);
314
+ if (next.host !== url.host) {
315
+ await res.body?.cancel();
316
+ return { error: `Redirect to another host: ${next.href}. Call fetch again with that URL if needed (permission will be requested).` };
317
+ }
318
+ await res.body?.cancel();
319
+ url = next;
320
+ }
321
+ if (!res) return { error: `Failed to fetch ${inputUrl}` };
322
+ const { text: body, truncated } = await readCapped(res);
323
+ const type = res.headers.get("content-type") ?? "";
324
+ const text = type.includes("html") ? htmlToText(body) : body;
325
+ if (!res.ok) return { error: `HTTP ${res.status} from ${inputUrl}: ${text.slice(0, 500)}` };
326
+ return { text, truncated, at: Date.now() };
327
+ } catch (error) {
328
+ return { error: `Failed to fetch ${inputUrl}: ${describeFetchError(error)}` };
329
+ } finally {
330
+ await dispatcher?.close().catch(() => {
331
+ });
332
+ }
333
+ }
334
+ var fetchTool = createFetchTool();
335
+
336
+ export {
337
+ isPrivateHost,
338
+ createFetchTool,
339
+ fetchTool,
340
+ createWebSearchTool
341
+ };
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ contentText
4
+ } from "./chunk-R4BJBTWC.js";
2
5
 
3
6
  // src/session.ts
4
7
  import { randomBytes } from "crypto";
@@ -36,7 +39,7 @@ var Session = class _Session {
36
39
  return readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
37
40
  const session = new _Session(join(dir, f));
38
41
  const first = session.load().find((m) => m.role === "user");
39
- return { session, mtime: statSync(session.file).mtime, preview: first ? String(first.content).slice(0, 80) : "" };
42
+ return { session, mtime: statSync(session.file).mtime, preview: first ? contentText(first.content).slice(0, 80) : "" };
40
43
  }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime() || b.session.file.localeCompare(a.session.file));
41
44
  }
42
45
  static latest(home, cwd) {
@@ -49,26 +52,77 @@ var Session = class _Session {
49
52
  append(message) {
50
53
  this.write({ t: "msg", m: message });
51
54
  }
55
+ appendUsage(usage) {
56
+ this.write({ t: "usage", ...usage });
57
+ }
52
58
  reset() {
53
59
  this.write({ t: "reset" });
54
60
  }
55
- load() {
61
+ records() {
56
62
  if (!existsSync(this.file)) return [];
57
- let messages = [];
63
+ const out = [];
58
64
  for (const line of readFileSync(this.file, "utf8").split("\n")) {
59
65
  if (!line.trim()) continue;
60
- let record;
61
66
  try {
62
- record = JSON.parse(line);
67
+ out.push(JSON.parse(line));
63
68
  } catch {
64
- continue;
65
69
  }
70
+ }
71
+ return out;
72
+ }
73
+ /** What the model continues from: everything after the last reset (after a compaction that is the summary and the recent turns). */
74
+ load() {
75
+ let messages = [];
76
+ for (const record of this.records()) {
66
77
  if (record.t === "reset") messages = [];
67
78
  else if (record.t === "msg") messages.push(record.m);
68
79
  }
69
80
  return repairTail(messages);
70
81
  }
82
+ /**
83
+ * What the user sees on resume: the whole conversation. Compaction only shrinks what the model is sent, so
84
+ * its reset does not hide the older turns here. A real reset (/clear, rewind) still starts over, the
85
+ * synthetic summary pair is never shown, and the recent turns a compaction re-writes are not shown twice.
86
+ */
87
+ loadHistory() {
88
+ const segments = [[]];
89
+ for (const record of this.records()) {
90
+ if (record.t === "reset") segments.push([]);
91
+ else if (record.t === "msg") segments[segments.length - 1].push(record.m);
92
+ }
93
+ let shown = segments[0];
94
+ for (const segment of segments.slice(1)) {
95
+ if (!isCompactionSummary(segment[0])) {
96
+ shown = segment;
97
+ continue;
98
+ }
99
+ const kept = segment.slice(2);
100
+ let at = 0;
101
+ while (at <= shown.length && !endsWithPrefixOf(shown, at, kept)) at++;
102
+ shown = [...shown, ...kept.slice(shown.length - at)];
103
+ }
104
+ return repairTail(shown.filter((m) => !isCompactionSummary(m) && !isCompactionAck(m)));
105
+ }
71
106
  };
107
+ var SUMMARY_PREFIX = "Summary of the previous conversation:";
108
+ var SUMMARY_ACK = "OK, I will continue from this summary.";
109
+ function isCompactionSummary(m) {
110
+ return m?.role === "user" && typeof m.content === "string" && m.content.startsWith(SUMMARY_PREFIX);
111
+ }
112
+ function isCompactionAck(m) {
113
+ return m.role === "assistant" && m.content === SUMMARY_ACK;
114
+ }
115
+ function sameMessage(a, b) {
116
+ if (a.role !== b.role) return false;
117
+ if (a.role === "tool" && b.role === "tool") return a.tool_call_id === b.tool_call_id;
118
+ return JSON.stringify(a.content) === JSON.stringify(b.content) && JSON.stringify("tool_calls" in a && a.tool_calls) === JSON.stringify("tool_calls" in b && b.tool_calls);
119
+ }
120
+ function endsWithPrefixOf(shown, at, kept) {
121
+ const n = shown.length - at;
122
+ if (n > kept.length) return false;
123
+ for (let j = 0; j < n; j++) if (!sameMessage(shown[at + j], kept[j])) return false;
124
+ return true;
125
+ }
72
126
  function pruneSessions(home, now = Date.now(), maxAgeDays = 30) {
73
127
  const root = join(home, "sessions");
74
128
  if (!existsSync(root)) return 0;
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ MODE_ORDER
4
+ } from "./chunk-GAFJEQTJ.js";
5
+ import {
6
+ REASONING_LEVELS,
7
+ isReasoningLevel
8
+ } from "./chunk-SNKVS26H.js";
9
+ import {
10
+ ConfigError
11
+ } from "./chunk-NDEJUL44.js";
12
+ import {
13
+ parseLang,
14
+ t
15
+ } from "./chunk-XLC6T4SX.js";
16
+
17
+ // src/args.ts
18
+ import { parseArgs } from "util";
19
+
20
+ // src/serveProtocol.ts
21
+ import { randomBytes, timingSafeEqual } from "crypto";
22
+ var MAX_FRAME_BYTES = 262144;
23
+ function parseClientFrame(raw) {
24
+ let value;
25
+ try {
26
+ value = JSON.parse(raw);
27
+ } catch {
28
+ return { error: "invalid JSON frame" };
29
+ }
30
+ const f = value;
31
+ if (typeof f?.streamId !== "string" || f.streamId === "") return { error: "frame requires a streamId" };
32
+ if (f.type === "open") {
33
+ if (typeof f.target !== "string" || f.target === "") return { error: "open requires a target" };
34
+ return { type: "open", streamId: f.streamId, target: f.target };
35
+ }
36
+ if (f.type === "cancel") return { type: "cancel", streamId: f.streamId };
37
+ return { error: `unknown frame type: ${String(f?.type)}` };
38
+ }
39
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
40
+ var LOOPBACK_BIND = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
41
+ function trustFence(headers, port) {
42
+ const host = headers.host;
43
+ if (!host) return "missing Host header";
44
+ const sep = host.lastIndexOf(":");
45
+ const hostname = sep > -1 ? host.slice(0, sep) : host;
46
+ const hostPort = sep > -1 ? Number(host.slice(sep + 1)) : 80;
47
+ if (!LOOPBACK_HOSTS.has(hostname)) return `non-loopback Host: ${hostname}`;
48
+ if (hostPort !== port) return `Host port ${hostPort} does not match listener ${port}`;
49
+ if (headers.origin) {
50
+ let origin;
51
+ try {
52
+ origin = new URL(headers.origin);
53
+ } catch {
54
+ return "malformed Origin header";
55
+ }
56
+ const originHost = origin.hostname === "::1" ? "[::1]" : origin.hostname;
57
+ const originPort = origin.port ? Number(origin.port) : origin.protocol === "https:" ? 443 : 80;
58
+ if (!LOOPBACK_HOSTS.has(originHost)) return `cross-origin Origin: ${headers.origin}`;
59
+ if (originPort !== port) return "Origin port mismatch";
60
+ }
61
+ if (headers["sec-fetch-site"] === "cross-site") return "cross-site request";
62
+ return null;
63
+ }
64
+ function bearerOk(header, token) {
65
+ if (!header?.startsWith("Bearer ")) return false;
66
+ const got = Buffer.from(header.slice(7));
67
+ const want = Buffer.from(token);
68
+ return got.length === want.length && timingSafeEqual(got, want);
69
+ }
70
+ function randomToken() {
71
+ return randomBytes(32).toString("hex");
72
+ }
73
+
74
+ // src/args.ts
75
+ var HELP = `BCCLI \u2014 BotConnector's AI coding agent for the terminal
76
+
77
+ Usage:
78
+ bccli [task] interactive mode (optionally start with a task)
79
+ bccli -p "task" run one task without interaction (scripts/CI)
80
+ bccli login [provider] OAuth login (or --key paste API key, --logout remove)
81
+ bccli models [provider] list models (default: the active provider)
82
+ bccli update install the newest bccli from npm
83
+ bccli acp run as an ACP agent for editors (Zed, ...)
84
+ bccli serve [--port N] serve sessions over HTTP+WS for IDEs/bots
85
+ bccli plugins list, install, remove, or update plugins
86
+ bccli gh-bot [serve] GitHub comment bot (off-by-default; needs config)
87
+ bccli provider list|add <id>|remove <id> manage providers (custom: --url <url> [--name N] [--key-env ENV])
88
+ bccli mcp list|add <name>|remove <name>|auth <name>|logout <name>|serve [--base-url <url>] manage MCP servers, or serve this account's tools over stdio
89
+ bccli completion <shell> print a shell completion script (bash, zsh, fish, powershell)
90
+ bccli stats [--days N] [--tools N] [--models [N|all]] [--project slug]
91
+ show token usage from local sessions (default: last 7 days)
92
+ bccli export [session] [file] copy a session JSONL to a file (default: last session here)
93
+ bccli import <file> import a session JSONL into the current project
94
+ bccli pr <number> [--repo owner/name] [task] check out a GitHub PR and run a task on it
95
+ bccli integrations show the status of external agent integrations
96
+ bccli connect <agent> opencode, aider, cline, dsh, codex, claude-code, cursor, openai-cli, openai-sdk, openai-compatible, openclaw, hermes
97
+ bccli disconnect <agent> remove an integration and restore the previous config
98
+
99
+ Options:
100
+ -m, --model <provider/model> choose a model, e.g. bc-cloud/glm-5.3-flash
101
+ --reasoning <level> auto | off | low | medium | high | max
102
+ -c, --continue continue the last session in this folder
103
+ -r, --resume pick a session to continue
104
+ --allow-all run all tools without asking for permission
105
+ --allowed-tools <a,b> tools allowed without asking: bash, edit, fetch
106
+ -w, --worktree <name> run in git worktree ../<repo>.worktrees/<name> (created, or reused if it exists)
107
+ --output-format <fmt> text | json | stream-json for -p (machine-readable output)
108
+ --permission-mode <mode> default | acceptEdits | plan | allowAll
109
+ --lang <en|id> interface language (default: en; or BCCLI_LANG, or "language" in config)
110
+ -v, --version version
111
+ -h, --help this help`;
112
+ var helpText = () => t(HELP);
113
+ function parseCliArgs(argv) {
114
+ const expanded = argv.map(
115
+ (a, i) => a === "--models" && (i + 1 >= argv.length || argv[i + 1].startsWith("-")) ? "--models=all" : a
116
+ );
117
+ const { values, positionals } = parseArgs({
118
+ args: expanded,
119
+ allowPositionals: true,
120
+ options: {
121
+ print: { type: "boolean", short: "p" },
122
+ model: { type: "string", short: "m" },
123
+ reasoning: { type: "string" },
124
+ lang: { type: "string" },
125
+ continue: { type: "boolean", short: "c" },
126
+ resume: { type: "boolean", short: "r" },
127
+ "allow-all": { type: "boolean" },
128
+ "allowed-tools": { type: "string" },
129
+ worktree: { type: "string", short: "w" },
130
+ "output-format": { type: "string" },
131
+ "permission-mode": { type: "string" },
132
+ help: { type: "boolean", short: "h" },
133
+ version: { type: "boolean", short: "v" },
134
+ url: { type: "string" },
135
+ name: { type: "string" },
136
+ "key-env": { type: "string" },
137
+ value: { type: "string", multiple: true },
138
+ key: { type: "boolean" },
139
+ logout: { type: "boolean" },
140
+ port: { type: "string" },
141
+ host: { type: "string" },
142
+ token: { type: "string" },
143
+ "base-url": { type: "string" },
144
+ days: { type: "string" },
145
+ tools: { type: "string" },
146
+ models: { type: "string" },
147
+ project: { type: "string" },
148
+ repo: { type: "string" }
149
+ }
150
+ });
151
+ const reasoning = values.reasoning;
152
+ if (reasoning && !isReasoningLevel(reasoning)) {
153
+ throw new ConfigError(t("--reasoning must be one of: {levels}", { levels: REASONING_LEVELS.join(", ") }));
154
+ }
155
+ const mode = values["permission-mode"];
156
+ if (mode && !MODE_ORDER.includes(mode)) {
157
+ throw new ConfigError(t("--permission-mode must be one of: {modes}", { modes: MODE_ORDER.join(", ") }));
158
+ }
159
+ if (values.lang !== void 0 && !parseLang(values.lang)) throw new ConfigError(t("--lang must be one of: en, id"));
160
+ const outputFormat = values["output-format"];
161
+ if (outputFormat !== void 0 && !["text", "json", "stream-json"].includes(outputFormat)) {
162
+ throw new ConfigError(t("--output-format must be one of: {formats}", { formats: "text, json, stream-json" }));
163
+ }
164
+ const [first, ...rest] = positionals;
165
+ const SUBCOMMANDS = ["login", "models", "update", "acp", "serve", "plugins", "gh-bot", "provider", "mcp", "connect", "disconnect", "integrations", "completion", "stats", "export", "import", "pr"];
166
+ const command = SUBCOMMANDS.includes(first) ? first : "run";
167
+ const words = command === "run" ? positionals : rest;
168
+ const port = values.port !== void 0 ? Number(values.port) : void 0;
169
+ if (port !== void 0 && (!Number.isInteger(port) || port < 0 || port > 65535)) {
170
+ throw new ConfigError(t("--port must be a number between 0 and 65535"));
171
+ }
172
+ const days = values.days !== void 0 ? Number(values.days) : void 0;
173
+ if (days !== void 0 && (!Number.isInteger(days) || days < 1)) {
174
+ throw new ConfigError(t("--days must be a positive integer"));
175
+ }
176
+ const tools = values.tools !== void 0 ? Number(values.tools) : void 0;
177
+ if (tools !== void 0 && (!Number.isInteger(tools) || tools < 1)) {
178
+ throw new ConfigError(t("--tools must be a positive integer"));
179
+ }
180
+ let models;
181
+ if (values.models !== void 0) {
182
+ models = values.models === "" || values.models === "all" ? "all" : values.models;
183
+ if (models !== "all" && (!Number.isInteger(Number(models)) || Number(models) < 1)) {
184
+ throw new ConfigError(t('--models must be a positive integer or "all"'));
185
+ }
186
+ }
187
+ if (values.host !== void 0 && !LOOPBACK_BIND.has(values.host)) {
188
+ throw new ConfigError(t("serve only accepts loopback --host (127.0.0.1, localhost, ::1)"));
189
+ }
190
+ return {
191
+ command,
192
+ prompt: command === "run" && words.length ? words.join(" ") : void 0,
193
+ print: !!values.print,
194
+ model: values.model,
195
+ reasoning,
196
+ lang: parseLang(values.lang),
197
+ continue: !!values.continue,
198
+ resume: !!values.resume,
199
+ allowAll: !!values["allow-all"],
200
+ allowedTools: (values["allowed-tools"] ?? "").split(",").map((s) => s.trim()).filter(Boolean),
201
+ worktree: values.worktree,
202
+ outputFormat,
203
+ permissionMode: mode,
204
+ help: !!values.help,
205
+ version: !!values.version,
206
+ loginProvider: command === "login" ? rest[0] ?? "bc-cloud" : void 0,
207
+ key: !!values.key,
208
+ logout: !!values.logout,
209
+ subArgs: ["models", "plugins", "gh-bot", "provider", "mcp", "connect", "disconnect", "integrations", "completion", "stats", "export", "import", "pr"].includes(command) ? rest : [],
210
+ url: values.url,
211
+ name: values.name,
212
+ keyEnv: values["key-env"],
213
+ values: values.value ?? [],
214
+ port,
215
+ host: values.host,
216
+ token: values.token,
217
+ baseUrl: values["base-url"],
218
+ days,
219
+ tools,
220
+ models,
221
+ project: values.project,
222
+ repo: values.repo
223
+ };
224
+ }
225
+
226
+ export {
227
+ MAX_FRAME_BYTES,
228
+ parseClientFrame,
229
+ LOOPBACK_BIND,
230
+ trustFence,
231
+ bearerOk,
232
+ randomToken,
233
+ helpText,
234
+ parseCliArgs
235
+ };