@botconnector/bccli 0.4.0-beta.38 → 0.4.0-beta.39

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 (39) hide show
  1. package/README.md +8 -0
  2. package/dist/acp-4XE2KOIJ.js +114 -0
  3. package/dist/{args-MNVI2OJI.js → args-BD2PHEKG.js} +4 -4
  4. package/dist/{chunk-TASYQW7B.js → chunk-AVB6M4JE.js} +2 -2
  5. package/dist/{chunk-OB7FN66F.js → chunk-EONFDWBE.js} +2 -2
  6. package/dist/{chunk-H66PC2EB.js → chunk-GGNXHJL6.js} +2 -2
  7. package/dist/chunk-HE33XAPZ.js +341 -0
  8. package/dist/{chunk-HPZSXX4W.js → chunk-ISCT3PN3.js} +2 -2
  9. package/dist/{chunk-C6PKRYUX.js → chunk-JROUQ5YS.js} +6 -45
  10. package/dist/chunk-LN32JIZR.js +56 -0
  11. package/dist/{chunk-4RMP4CNT.js → chunk-M23QBX3V.js} +52 -12
  12. package/dist/chunk-M7LLJQ7M.js +310 -0
  13. package/dist/{chunk-YURAIW7A.js → chunk-NI5JZUZ3.js} +1 -1
  14. package/dist/chunk-PDZPPSLU.js +8 -0
  15. package/dist/chunk-PSO7R5AD.js +10 -0
  16. package/dist/chunk-QETGQXPL.js +2125 -0
  17. package/dist/chunk-RV74TCEP.js +231 -0
  18. package/dist/{chunk-U66JJ6RN.js → chunk-SESBH44L.js} +1 -1
  19. package/dist/{chunk-XFTBML3H.js → chunk-VM7HYAJS.js} +1 -1
  20. package/dist/{chunk-ASTNW3X2.js → chunk-ZG7HCXYA.js} +89 -9
  21. package/dist/{chunk-HXSREA7S.js → chunk-ZMU7MXCH.js} +184 -295
  22. package/dist/cli.js +65 -2097
  23. package/dist/{config-JRO5EOKZ.js → config-C4KLLDC6.js} +3 -3
  24. package/dist/{integrationCli-ZTZTQD2I.js → integrationCli-HR7CDUSI.js} +2 -2
  25. package/dist/{login-THRU4U4L.js → login-IWBRUNT4.js} +1 -1
  26. package/dist/{mcpCli-5ZSAMBJS.js → mcpCli-5QTXH7XJ.js} +9 -5
  27. package/dist/modelsCli-DKTT3UZD.js +36 -0
  28. package/dist/providerCli-DDH4ZZUY.js +15 -0
  29. package/dist/sdk.js +5114 -0
  30. package/dist/serve-7BP5BVX3.js +514 -0
  31. package/dist/serve-I2VQUTHR.js +178 -0
  32. package/dist/session-H2YL5RSB.js +12 -0
  33. package/dist/setup-FGCFKC7Z.js +23 -0
  34. package/dist/{ui-W4C6SRHA.js → ui-MLOXPAHT.js} +292 -54
  35. package/dist/update-73WI27VM.js +109 -0
  36. package/package.json +5 -2
  37. package/dist/chunk-H5N4ST6B.js +0 -149
  38. package/dist/providerCli-TGDE3633.js +0 -14
  39. package/dist/session-TVOWVP6Y.js +0 -12
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runCommand,
4
+ runHooks
5
+ } from "./chunk-ZMU7MXCH.js";
6
+ import {
7
+ ConfigError
8
+ } from "./chunk-M7LLJQ7M.js";
9
+ import {
10
+ t
11
+ } from "./chunk-M23QBX3V.js";
12
+
13
+ // src/verify.ts
14
+ var MAX_VERIFY_ROUNDS = 2;
15
+ async function runVerify(commands, opts) {
16
+ const run = opts.run ?? runCommand;
17
+ const failures = [];
18
+ for (const cmd of commands) {
19
+ const r = await run(cmd, { cwd: opts.cwd, timeoutMs: opts.timeoutMs ?? 3e5, signal: opts.signal, env: opts.env });
20
+ const exitCode = r.exitCode ?? -1;
21
+ if (exitCode !== 0) failures.push({ cmd, exitCode, output: r.output });
22
+ }
23
+ return failures;
24
+ }
25
+ function verifyFollowup(failures, maxBytes = 8e3) {
26
+ const blocks = failures.map((f) => {
27
+ const output = f.output.length > maxBytes ? `${f.output.slice(0, maxBytes)}
28
+ [truncated]` : f.output;
29
+ return `$ ${f.cmd}
30
+ [exit code ${f.exitCode}]
31
+ ${output}`;
32
+ });
33
+ return t("Automatic verification after your edits failed. Fix the problems so these commands pass, then stop.\n\n{blocks}", {
34
+ blocks: blocks.join("\n\n")
35
+ });
36
+ }
37
+
38
+ // src/print.ts
39
+ function toolLabel(tool) {
40
+ if (tool === "web_search") return "Search";
41
+ return tool.charAt(0).toUpperCase() + tool.slice(1);
42
+ }
43
+ async function runPrint(rt, prompt, io = { out: process.stdout, err: process.stderr }, opts = {}) {
44
+ const format = opts.outputFormat ?? "text";
45
+ const emit = (line) => io.out.write(`${JSON.stringify(line)}
46
+ `);
47
+ let result = "";
48
+ const toolNames = /* @__PURE__ */ new Map();
49
+ rt.interaction.approvePlan = async () => {
50
+ if (opts.allowAll) return "allowAll";
51
+ io.err.write(`${t("Plan needs approval; run with --allow-all.")}
52
+ `);
53
+ return "no";
54
+ };
55
+ let failed = false;
56
+ let wroteText = false;
57
+ let edited = false;
58
+ rt.agent.onEvent = (event) => {
59
+ switch (event.type) {
60
+ case "text":
61
+ if (format === "text") {
62
+ io.out.write(event.delta);
63
+ wroteText = true;
64
+ } else {
65
+ result += event.delta;
66
+ }
67
+ break;
68
+ case "toolStart":
69
+ toolNames.set(event.id, event.tool);
70
+ io.err.write(`\u23BF ${toolLabel(event.tool)} ${event.target}
71
+ `);
72
+ if (format === "stream-json") emit({ type: "tool_use", tool: event.tool, target: event.target });
73
+ break;
74
+ case "toolEnd":
75
+ if (event.isError) io.err.write(` \u2717 ${event.output.split("\n")[0]}
76
+ `);
77
+ if ((event.tool === "edit" || event.tool === "write") && !event.isError) edited = true;
78
+ if (format === "stream-json") emit({ type: "tool_result", tool: toolNames.get(event.id), isError: !!event.isError, output: event.output });
79
+ break;
80
+ case "stepLimit":
81
+ io.err.write(`${t("Step limit reached.")}
82
+ `);
83
+ failed = true;
84
+ break;
85
+ case "budgetExceeded":
86
+ io.err.write(`${t("Budget exceeded ({kind}): {used} of {limit}. Further model calls are blocked; raise usageCap in ~/.bccli/config.json.", { kind: event.kind, used: Math.round(event.used * 100) / 100, limit: event.limit })}
87
+ `);
88
+ if (format === "stream-json") emit({ type: "error", message: "budgetExceeded" });
89
+ failed = true;
90
+ break;
91
+ case "aborted":
92
+ io.err.write(`${t("Cancelled.")}
93
+ `);
94
+ failed = true;
95
+ break;
96
+ case "error":
97
+ io.err.write(`Error: ${event.message}
98
+ `);
99
+ if (format === "stream-json") emit({ type: "error", message: event.message });
100
+ failed = true;
101
+ break;
102
+ }
103
+ };
104
+ rt.agent.askPermission = async (req) => {
105
+ io.err.write(` ${t("Needs permission for {tool}. Run with --allow-all or --allowed-tools {kind}.", { tool: req.tool, kind: req.kind })}
106
+ `);
107
+ return "no";
108
+ };
109
+ const controller = new AbortController();
110
+ const onSigint = () => controller.abort();
111
+ process.once("SIGINT", onSigint);
112
+ const start = await runHooks(rt.config.hooks, "SessionStart", {}, { env: rt.env, cwd: rt.cwd });
113
+ for (const warning of start.warnings) io.err.write(`${warning}
114
+ `);
115
+ await rt.emitPlugins("SessionStart");
116
+ try {
117
+ await rt.agent.run(prompt, controller.signal);
118
+ const commands = rt.config.verifyCommands;
119
+ if (edited && commands.length && !controller.signal.aborted) {
120
+ for (let round = 0; round < MAX_VERIFY_ROUNDS; round++) {
121
+ io.err.write(`${t("Verifying edits: {cmds}", { cmds: commands.join(", ") })}
122
+ `);
123
+ const failures = await runVerify(commands, { cwd: rt.cwd, env: rt.env, signal: controller.signal });
124
+ if (!failures.length) {
125
+ io.err.write(`${t("Verification passed.")}
126
+ `);
127
+ break;
128
+ }
129
+ if (round === MAX_VERIFY_ROUNDS - 1 || controller.signal.aborted) {
130
+ io.err.write(`${t("Verification still failing after {n} fix round(s). Run the commands manually to see why.", { n: MAX_VERIFY_ROUNDS })}
131
+ `);
132
+ failed = true;
133
+ break;
134
+ }
135
+ await rt.agent.run(verifyFollowup(failures), controller.signal);
136
+ }
137
+ }
138
+ } finally {
139
+ process.off("SIGINT", onSigint);
140
+ }
141
+ const stop = await runHooks(rt.config.hooks, "Stop", {}, { env: rt.env, cwd: rt.cwd });
142
+ for (const warning of stop.warnings) io.err.write(`${warning}
143
+ `);
144
+ await rt.emitPlugins("Stop");
145
+ if (format === "json") {
146
+ io.out.write(`${JSON.stringify({ status: failed ? "failed" : "ok", model: rt.modelRef, result })}
147
+ `);
148
+ } else if (format === "stream-json") {
149
+ if (result) emit({ type: "message", text: result });
150
+ emit({ type: "result", status: failed ? "failed" : "ok", model: rt.modelRef });
151
+ } else if (wroteText) {
152
+ io.out.write("\n");
153
+ }
154
+ return failed ? 1 : 0;
155
+ }
156
+
157
+ // src/worktree.ts
158
+ import { execFile } from "child_process";
159
+ import { existsSync } from "fs";
160
+ import { basename, dirname, join } from "path";
161
+ function git(cwd, args, bin = "git") {
162
+ return new Promise((resolve) => {
163
+ execFile(
164
+ bin,
165
+ args,
166
+ { cwd, maxBuffer: 20 * 1024 * 1024 },
167
+ (error, stdout, stderr) => resolve({ ok: !error, out: error ? stderr : stdout, missing: error?.code === "ENOENT" })
168
+ );
169
+ });
170
+ }
171
+ var NAME_RE = /^[A-Za-z0-9._-]+$/;
172
+ function samePath(a, b) {
173
+ const norm = (p) => process.platform === "win32" ? p.replace(/\//g, "\\").toLowerCase() : p.replace(/\\/g, "/");
174
+ return norm(a) === norm(b);
175
+ }
176
+ function worktreeEntries(porcelain) {
177
+ return porcelain.split(/\r?\n\r?\n/).map((block) => {
178
+ const path = /^worktree (.+)$/m.exec(block)?.[1]?.trim();
179
+ if (!path) return void 0;
180
+ const branch = /^branch refs\/heads\/(.+)$/m.exec(block)?.[1];
181
+ return branch ? { path, branch } : { path };
182
+ }).filter((entry) => !!entry);
183
+ }
184
+ async function resolveWorktree(cwd, name, bin = "git") {
185
+ if (!NAME_RE.test(name)) {
186
+ throw new ConfigError(t('Invalid worktree name "{name}": use letters, digits, dot, dash, or underscore (no slashes).', { name }));
187
+ }
188
+ const inside = await git(cwd, ["rev-parse", "--is-inside-work-tree"], bin);
189
+ if (inside.missing) throw new ConfigError(t("git is not installed or not on PATH, so -w cannot be used."));
190
+ if (!inside.ok || inside.out.trim() !== "true") throw new ConfigError(t("This folder is not a git repository, so -w cannot create a worktree."));
191
+ const list = await git(cwd, ["worktree", "list", "--porcelain"], bin);
192
+ if (!list.ok) throw new ConfigError(t("This folder is not a git repository, so -w cannot create a worktree."));
193
+ const entries = worktreeEntries(list.out);
194
+ const main = entries[0]?.path;
195
+ if (!main) throw new ConfigError(t("This folder is not a git repository, so -w cannot create a worktree."));
196
+ const path = join(dirname(main), `${basename(main)}.worktrees`, name);
197
+ if (entries.some((entry) => samePath(entry.path, path))) return { path, created: false };
198
+ if (existsSync(path)) {
199
+ throw new ConfigError(t("Worktree path {path} already exists but is not a registered worktree; remove or rename it first.", { path }));
200
+ }
201
+ const branch = `bccli/${name}`;
202
+ const hasBranch = (await git(main, ["rev-parse", "--verify", "-q", `refs/heads/${branch}`], bin)).ok;
203
+ const add = hasBranch ? await git(main, ["worktree", "add", path, branch], bin) : await git(main, ["worktree", "add", "-b", branch, path], bin);
204
+ if (!add.ok) throw new ConfigError(add.out.trim() || t("This folder is not a git repository, so -w cannot create a worktree."));
205
+ return { path, created: true };
206
+ }
207
+ async function worktreeListText(cwd, bin = "git") {
208
+ const inside = await git(cwd, ["rev-parse", "--is-inside-work-tree"], bin);
209
+ if (inside.missing) return t("git is not installed or not on PATH, so /worktree cannot be used.");
210
+ if (!inside.ok || inside.out.trim() !== "true") return t("This folder is not a git repository, so there is no diff.");
211
+ const list = await git(cwd, ["worktree", "list", "--porcelain"], bin);
212
+ if (!list.ok) return t("This folder is not a git repository, so there is no diff.");
213
+ const rows = worktreeEntries(list.out).map((entry) => ` ${entry.path}${entry.branch ? ` (${entry.branch})` : ""}`);
214
+ return [
215
+ t("Git worktrees:"),
216
+ ...rows,
217
+ "",
218
+ t("Create one with: bccli -w <name>"),
219
+ t("Remove with: git worktree remove <path>")
220
+ ].join("\n");
221
+ }
222
+
223
+ export {
224
+ MAX_VERIFY_ROUNDS,
225
+ runVerify,
226
+ verifyFollowup,
227
+ toolLabel,
228
+ runPrint,
229
+ resolveWorktree,
230
+ worktreeListText
231
+ };
@@ -5,7 +5,7 @@ import {
5
5
  readCredentials,
6
6
  readJsonConfig,
7
7
  removeCredential
8
- } from "./chunk-H5N4ST6B.js";
8
+ } from "./chunk-M7LLJQ7M.js";
9
9
 
10
10
  // src/providers.ts
11
11
  import { mkdirSync, writeFileSync } from "fs";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  contentText
4
- } from "./chunk-H66PC2EB.js";
4
+ } from "./chunk-GGNXHJL6.js";
5
5
 
6
6
  // src/session.ts
7
7
  import { randomBytes } from "crypto";
@@ -5,26 +5,85 @@ import {
5
5
  import {
6
6
  REASONING_LEVELS,
7
7
  isReasoningLevel
8
- } from "./chunk-YURAIW7A.js";
8
+ } from "./chunk-NI5JZUZ3.js";
9
9
  import {
10
10
  ConfigError
11
- } from "./chunk-H5N4ST6B.js";
11
+ } from "./chunk-M7LLJQ7M.js";
12
12
  import {
13
13
  parseLang,
14
14
  t
15
- } from "./chunk-4RMP4CNT.js";
15
+ } from "./chunk-M23QBX3V.js";
16
16
 
17
17
  // src/args.ts
18
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
19
75
  var HELP = `BCCLI \u2014 BotConnector's AI coding agent for the terminal
20
76
 
21
77
  Usage:
22
78
  bccli [task] interactive mode (optionally start with a task)
23
79
  bccli -p "task" run one task without interaction (scripts/CI)
24
80
  bccli login [provider] save an API key (default: bc-cloud)
25
- bccli models list models from the active provider
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
26
85
  bccli provider list|add <id>|remove <id> manage providers (custom: --url <url> [--name N] [--key-env ENV])
27
- bccli mcp list|add <name>|remove <name>|auth <name>|logout <name> manage MCP servers (catalog, --url <url>, OAuth)
86
+ 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
28
87
  bccli integrations show the status of external agent integrations
29
88
  bccli connect <agent> opencode, aider, cline, dsh, codex, claude-code, cursor, openai-cli, openai-sdk, openai-compatible, openclaw, hermes
30
89
  bccli disconnect <agent> remove an integration and restore the previous config
@@ -64,7 +123,11 @@ function parseCliArgs(argv) {
64
123
  url: { type: "string" },
65
124
  name: { type: "string" },
66
125
  "key-env": { type: "string" },
67
- value: { type: "string", multiple: true }
126
+ value: { type: "string", multiple: true },
127
+ port: { type: "string" },
128
+ host: { type: "string" },
129
+ token: { type: "string" },
130
+ "base-url": { type: "string" }
68
131
  }
69
132
  });
70
133
  const reasoning = values.reasoning;
@@ -81,9 +144,16 @@ function parseCliArgs(argv) {
81
144
  throw new ConfigError(t("--output-format must be one of: {formats}", { formats: "text, json, stream-json" }));
82
145
  }
83
146
  const [first, ...rest] = positionals;
84
- const SUBCOMMANDS = ["login", "models", "provider", "mcp", "connect", "disconnect", "integrations"];
147
+ const SUBCOMMANDS = ["login", "models", "update", "acp", "serve", "provider", "mcp", "connect", "disconnect", "integrations"];
85
148
  const command = SUBCOMMANDS.includes(first) ? first : "run";
86
149
  const words = command === "run" ? positionals : rest;
150
+ const port = values.port !== void 0 ? Number(values.port) : void 0;
151
+ if (port !== void 0 && (!Number.isInteger(port) || port < 0 || port > 65535)) {
152
+ throw new ConfigError(t("--port must be a number between 0 and 65535"));
153
+ }
154
+ if (values.host !== void 0 && !LOOPBACK_BIND.has(values.host)) {
155
+ throw new ConfigError(t("serve only accepts loopback --host (127.0.0.1, localhost, ::1)"));
156
+ }
87
157
  return {
88
158
  command,
89
159
  prompt: command === "run" && words.length ? words.join(" ") : void 0,
@@ -101,15 +171,25 @@ function parseCliArgs(argv) {
101
171
  help: !!values.help,
102
172
  version: !!values.version,
103
173
  loginProvider: command === "login" ? rest[0] ?? "bc-cloud" : void 0,
104
- subArgs: ["provider", "mcp", "connect", "disconnect", "integrations"].includes(command) ? rest : [],
174
+ subArgs: ["models", "provider", "mcp", "connect", "disconnect", "integrations"].includes(command) ? rest : [],
105
175
  url: values.url,
106
176
  name: values.name,
107
177
  keyEnv: values["key-env"],
108
- values: values.value ?? []
178
+ values: values.value ?? [],
179
+ port,
180
+ host: values.host,
181
+ token: values.token,
182
+ baseUrl: values["base-url"]
109
183
  };
110
184
  }
111
185
 
112
186
  export {
187
+ MAX_FRAME_BYTES,
188
+ parseClientFrame,
189
+ LOOPBACK_BIND,
190
+ trustFence,
191
+ bearerOk,
192
+ randomToken,
113
193
  helpText,
114
194
  parseCliArgs
115
195
  };