@botconnector/bccli 0.4.0-beta.35 → 0.4.0-beta.37

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.
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  addGlobalServer,
4
+ expandEnvVars,
4
5
  globalMcpPath,
5
6
  projectMcpPath,
6
7
  projectTrust,
@@ -8,10 +9,12 @@ import {
8
9
  removeGlobalServer,
9
10
  serversToStart,
10
11
  setProjectTrust
11
- } from "./chunk-CDUPYHNL.js";
12
- import "./chunk-IJGTX7RC.js";
12
+ } from "./chunk-TASYQW7B.js";
13
+ import "./chunk-H5N4ST6B.js";
14
+ import "./chunk-4RMP4CNT.js";
13
15
  export {
14
16
  addGlobalServer,
17
+ expandEnvVars,
15
18
  globalMcpPath,
16
19
  projectMcpPath,
17
20
  projectTrust,
@@ -4,9 +4,11 @@ import {
4
4
  bccliHome,
5
5
  loadConfig,
6
6
  readCredentials,
7
- resolveModel,
7
+ resolveModel
8
+ } from "./chunk-H5N4ST6B.js";
9
+ import {
8
10
  t
9
- } from "./chunk-IJGTX7RC.js";
11
+ } from "./chunk-4RMP4CNT.js";
10
12
 
11
13
  // src/integrationCli.ts
12
14
  import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
@@ -23,11 +25,15 @@ function statePath(target, env) {
23
25
  function keyPath(env) {
24
26
  return join(integrationHome(env), "bc-cloud.key");
25
27
  }
28
+ var OPENCLAW_ENV_ID = "BOTCONNECTOR_API_KEY";
29
+ var HERMES_ENV_ID = "BOTCONNECTOR_API_KEY";
30
+ var HERMES_PROVIDER_ID = "botconnector-cloud";
31
+ var FALLBACK_DEFAULT_MODEL = "glm-5.3-flash";
26
32
  function parseTarget(raw) {
27
- if (raw === "opencode" || raw === "aider" || raw === "cline" || raw === "deepseek-harness" || raw === "cursor" || raw === "openai-cli" || raw === "openai-sdk" || raw === "openai-compatible" || raw === "codex" || raw === "claude-code") return raw;
33
+ if (raw === "opencode" || raw === "aider" || raw === "cline" || raw === "deepseek-harness" || raw === "cursor" || raw === "openai-cli" || raw === "openai-sdk" || raw === "openai-compatible" || raw === "codex" || raw === "claude-code" || raw === "openclaw" || raw === "hermes") return raw;
28
34
  if (raw === "dsh") return "deepseek-harness";
29
35
  throw new ConfigError(
30
- t("Agent must be one of: opencode, aider, cline, deepseek-harness (dsh), cursor, openai-cli, openai-sdk, openai-compatible, codex, claude-code")
36
+ t("Agent must be one of: opencode, aider, cline, deepseek-harness (dsh), cursor, openai-cli, openai-sdk, openai-compatible, codex, claude-code, openclaw, hermes")
31
37
  );
32
38
  }
33
39
  function homeDir(env) {
@@ -83,7 +89,9 @@ async function botConnectorModels(deps) {
83
89
  });
84
90
  if (!response.ok) throw new ConfigError(t("Could not load the BotConnector model catalog (HTTP {status}).", { status: response.status }));
85
91
  const payload = await response.json();
86
- return (payload.data ?? []).map((item) => item.id).filter((id) => typeof id === "string" && !!id);
92
+ const models = (payload.data ?? []).map((item) => item.id).filter((id) => typeof id === "string" && !!id);
93
+ if (!models.length) throw new ConfigError(t("The BotConnector model catalog is empty; cannot configure external agents."));
94
+ return models;
87
95
  }
88
96
  async function connectOpenCode(deps) {
89
97
  const home = homeDir(deps.env);
@@ -135,6 +143,15 @@ function stripManagedBlock(input) {
135
143
  return right ? `${left}
136
144
  ${right}` : left;
137
145
  }
146
+ function externalAgentDefaultModel(models, deps) {
147
+ try {
148
+ const config = loadConfig(deps.cwd, deps.env);
149
+ const preferred = config.model.startsWith("bc-cloud/") ? config.model.slice("bc-cloud/".length) : "";
150
+ if (preferred && models.includes(preferred)) return preferred;
151
+ } catch {
152
+ }
153
+ return models.includes(FALLBACK_DEFAULT_MODEL) ? FALLBACK_DEFAULT_MODEL : models[0];
154
+ }
138
155
  function yamlQuote(value) {
139
156
  return JSON.stringify(value.replace(/\\/g, "/"));
140
157
  }
@@ -537,6 +554,86 @@ async function connectClaudeCode(deps) {
537
554
  deps.out(`Launcher: ${launcher}`);
538
555
  deps.out(`Default coding model: ${model}`);
539
556
  }
557
+ async function connectOpenClaw(deps) {
558
+ const home = homeDir(deps.env);
559
+ const clawHome = deps.env.OPENCLAW_CONFIG_DIR || join(home, ".openclaw");
560
+ const path = join(clawHome, "openclaw.json");
561
+ const backup = safeBackup(path, deps.env);
562
+ ensureSecret(deps.env);
563
+ const models = await botConnectorModels(deps);
564
+ const model = externalAgentDefaultModel(models, deps);
565
+ let doc = {};
566
+ if (existsSync(path)) {
567
+ try {
568
+ doc = JSON.parse(readFileSync(path, "utf8"));
569
+ } catch {
570
+ throw new ConfigError(t("The OpenClaw config is not valid JSON: {path}", { path }));
571
+ }
572
+ }
573
+ const modelsRoot = doc.models && typeof doc.models === "object" && !Array.isArray(doc.models) ? { ...doc.models } : {};
574
+ const providers = modelsRoot.providers && typeof modelsRoot.providers === "object" && !Array.isArray(modelsRoot.providers) ? { ...modelsRoot.providers } : {};
575
+ const existing = providers[PROVIDER_ID];
576
+ if (existing?.baseUrl && existing.baseUrl.replace(/\/+$/, "") !== BASE_URL) {
577
+ throw new ConfigError(t('OpenClaw already has a provider "{id}" with a different endpoint ({base}). BCCLI will not overwrite it; disconnect or change that provider first.', { id: PROVIDER_ID, base: existing.baseUrl }));
578
+ }
579
+ const secretRef = { source: "env", provider: PROVIDER_ID, id: OPENCLAW_ENV_ID };
580
+ providers[PROVIDER_ID] = {
581
+ baseUrl: BASE_URL,
582
+ api: "openai-completions",
583
+ apiKey: secretRef,
584
+ // Live catalog ids only; OpenClaw resolves provider capabilities itself.
585
+ models: models.map((id) => ({ id, name: id }))
586
+ };
587
+ modelsRoot.providers = providers;
588
+ doc.models = modelsRoot;
589
+ mkdirSync(dirname(path), { recursive: true });
590
+ writeFileSync(path, `${JSON.stringify(doc, null, 2)}
591
+ `);
592
+ writeState("openclaw", {
593
+ target: "openclaw",
594
+ files: [{ path, ...backup }],
595
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
596
+ }, deps.env);
597
+ deps.out(t("OpenClaw is connected to BotConnector ({n} models; default {model}).", { n: models.length, model }));
598
+ deps.out(`Config: ${path}`);
599
+ deps.out(t("API key: env {env} (SecretRef, not stored in the config).", { env: OPENCLAW_ENV_ID }));
600
+ deps.out(t("Set the default model in OpenClaw with: openclaw models set {provider}/{model}", { provider: PROVIDER_ID, model }));
601
+ }
602
+ async function connectHermes(deps) {
603
+ const home = homeDir(deps.env);
604
+ const hermesHome = deps.env.HERMES_HOME || join(home, ".hermes");
605
+ const path = join(hermesHome, "config.yaml");
606
+ const backup = safeBackup(path, deps.env);
607
+ ensureSecret(deps.env);
608
+ const models = await botConnectorModels(deps);
609
+ const model = externalAgentDefaultModel(models, deps);
610
+ const source = existsSync(path) ? readFileSync(path, "utf8") : "";
611
+ const doc = parseDocument(source);
612
+ if (doc.errors.length) throw new ConfigError(t("The Hermes config is not valid YAML: {path}", { path }));
613
+ const previousProvider = doc.getIn(["providers", HERMES_PROVIDER_ID]);
614
+ if (previousProvider?.api && previousProvider.api.replace(/\/+$/, "") !== BASE_URL) {
615
+ throw new ConfigError(t('Hermes already has a provider "{id}" with a different endpoint ({base}). BCCLI will not overwrite it; disconnect or change that provider first.', { id: HERMES_PROVIDER_ID, base: previousProvider.api }));
616
+ }
617
+ doc.setIn(["providers", HERMES_PROVIDER_ID], {
618
+ api: BASE_URL,
619
+ key_env: HERMES_ENV_ID,
620
+ transport: "chat_completions",
621
+ default_model: model,
622
+ discover_models: true,
623
+ models: Object.fromEntries(models.map((id) => [id, {}]))
624
+ });
625
+ doc.setIn(["model"], { default: model, provider: `custom:${HERMES_PROVIDER_ID}` });
626
+ mkdirSync(hermesHome, { recursive: true });
627
+ writeFileSync(path, doc.toString());
628
+ writeState("hermes", {
629
+ target: "hermes",
630
+ files: [{ path, ...backup }],
631
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
632
+ }, deps.env);
633
+ deps.out(t("Hermes is connected to BotConnector ({n} models; default {model}).", { n: models.length, model }));
634
+ deps.out(`Config: ${path}`);
635
+ deps.out(t("API key: env {env} only (via key_env; a plaintext key in config.yaml is never written).", { env: HERMES_ENV_ID }));
636
+ }
540
637
  function disconnect(target, deps) {
541
638
  const state = readState(target, deps.env);
542
639
  if (!state) {
@@ -555,7 +652,7 @@ function disconnect(target, deps) {
555
652
  deps.out(t("{target}: the BotConnector integration was removed and the config restored.", { target }));
556
653
  }
557
654
  function listIntegrations(deps) {
558
- for (const target of ["opencode", "aider", "cline", "deepseek-harness", "cursor", "openai-cli", "openai-sdk", "openai-compatible", "codex", "claude-code"]) {
655
+ for (const target of ["opencode", "aider", "cline", "deepseek-harness", "cursor", "openai-cli", "openai-sdk", "openai-compatible", "codex", "claude-code", "openclaw", "hermes"]) {
559
656
  deps.out(`${target} ${readState(target, deps.env) ? "connected" : "not connected"}`);
560
657
  }
561
658
  }
@@ -578,6 +675,8 @@ async function runIntegrationCommand(args, deps) {
578
675
  else if (target === "openai-sdk") await connectOpenAiSdk(deps);
579
676
  else if (target === "openai-compatible") await connectOpenAiCompatible(deps);
580
677
  else if (target === "codex") await connectCodex(deps);
678
+ else if (target === "openclaw") await connectOpenClaw(deps);
679
+ else if (target === "hermes") await connectHermes(deps);
581
680
  else await connectClaudeCode(deps);
582
681
  return 0;
583
682
  }
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  readSecret,
4
4
  runLogin
5
- } from "./chunk-FNBTY56Z.js";
5
+ } from "./chunk-FCQCZCKO.js";
6
6
  export {
7
7
  readSecret,
8
8
  runLogin
@@ -2,17 +2,23 @@
2
2
  import {
3
3
  CATALOG,
4
4
  fillTemplate
5
- } from "./chunk-GEG6QT5H.js";
5
+ } from "./chunk-HPZSXX4W.js";
6
+ import {
7
+ authorizeMcpServer,
8
+ clearTokens
9
+ } from "./chunk-3HCOAJEQ.js";
6
10
  import {
7
11
  addGlobalServer,
8
12
  globalMcpPath,
9
13
  readMcpFile,
10
14
  removeGlobalServer
11
- } from "./chunk-CDUPYHNL.js";
15
+ } from "./chunk-TASYQW7B.js";
16
+ import {
17
+ bccliHome
18
+ } from "./chunk-H5N4ST6B.js";
12
19
  import {
13
- bccliHome,
14
20
  t
15
- } from "./chunk-IJGTX7RC.js";
21
+ } from "./chunk-4RMP4CNT.js";
16
22
 
17
23
  // src/mcpCli.ts
18
24
  async function runMcpCommand(args, deps) {
@@ -33,8 +39,30 @@ async function runMcpCommand(args, deps) {
33
39
  deps.out(t("{name} removed.", { name }));
34
40
  return 0;
35
41
  }
42
+ if (action === "auth" || action === "logout") {
43
+ const config = installed[name];
44
+ if (config?.type !== "http") {
45
+ deps.err(
46
+ t("{name} is not an installed HTTP MCP server. Add it first with: bccli mcp add {name} --url <url>", { name })
47
+ );
48
+ return 1;
49
+ }
50
+ if (action === "logout") {
51
+ const removed = clearTokens(home, config.url);
52
+ deps.out(removed ? t("{name} logged out.", { name }) : t("{name} has no stored OAuth tokens.", { name }));
53
+ return 0;
54
+ }
55
+ try {
56
+ await authorizeMcpServer({ name, config }, { home });
57
+ deps.out(t("{name} authorized. Active in the next bccli session.", { name }));
58
+ return 0;
59
+ } catch (error) {
60
+ deps.err(t("OAuth login failed for {name}: {reason}", { name, reason: error.message }));
61
+ return 1;
62
+ }
63
+ }
36
64
  if (action !== "add") {
37
- deps.err(t("Unknown action: {action}. Use list, add, or remove.", { action }));
65
+ deps.err(t("Unknown action: {action}. Use list, add, remove, auth, or logout.", { action }));
38
66
  return 1;
39
67
  }
40
68
  if (args.url) {
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ addProviderKey,
4
+ runProviderCommand
5
+ } from "./chunk-BB4CVWHS.js";
6
+ import "./chunk-U66JJ6RN.js";
7
+ import "./chunk-7TABTC32.js";
8
+ import "./chunk-YURAIW7A.js";
9
+ import "./chunk-H5N4ST6B.js";
10
+ import "./chunk-4RMP4CNT.js";
11
+ export {
12
+ addProviderKey,
13
+ runProviderCommand
14
+ };
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ Session,
4
+ pruneSessions
5
+ } from "./chunk-3JRBXJVB.js";
6
+ import "./chunk-7TABTC32.js";
7
+ import "./chunk-YURAIW7A.js";
8
+ import "./chunk-4RMP4CNT.js";
9
+ export {
10
+ Session,
11
+ pruneSessions
12
+ };
@@ -1,56 +1,66 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  BUILTIN_AGENTS,
4
+ appendMemory,
4
5
  ignorePatterns,
5
- instructionPaths,
6
+ instructionFiles,
6
7
  normalizeGlobPath,
7
8
  parseFrontmatter,
9
+ parseMemoryArgs,
10
+ renderBackgroundTasks,
11
+ runHooks,
8
12
  toolLabel,
9
- validateGlobPattern
10
- } from "./chunk-LRMC4AOV.js";
13
+ validateGlobPattern,
14
+ worktreeListText
15
+ } from "./chunk-HXSREA7S.js";
11
16
  import {
12
17
  nextMode
13
- } from "./chunk-ELML4KVQ.js";
18
+ } from "./chunk-R3ND76LW.js";
14
19
  import {
15
20
  hasKey,
16
21
  providerName,
17
22
  writeGlobalConfig
18
- } from "./chunk-AOVIQXXO.js";
23
+ } from "./chunk-U66JJ6RN.js";
24
+ import {
25
+ Session
26
+ } from "./chunk-3JRBXJVB.js";
27
+ import {
28
+ contentText
29
+ } from "./chunk-7TABTC32.js";
19
30
  import {
20
31
  REASONING_LEVELS,
21
32
  assertReasoningSupported,
22
33
  parseReasoningLevel,
23
34
  supportedReasoningLevels
24
- } from "./chunk-UJH2JT6H.js";
25
- import {
26
- Session
27
- } from "./chunk-SJH3YLW4.js";
35
+ } from "./chunk-YURAIW7A.js";
28
36
  import {
29
37
  CATALOG,
30
38
  fillTemplate
31
- } from "./chunk-GEG6QT5H.js";
39
+ } from "./chunk-HPZSXX4W.js";
32
40
  import {
33
41
  addGlobalServer,
34
42
  globalMcpPath,
35
43
  readMcpFile,
36
44
  removeGlobalServer
37
- } from "./chunk-CDUPYHNL.js";
45
+ } from "./chunk-TASYQW7B.js";
38
46
  import {
39
47
  PRESETS,
48
+ removeCredential
49
+ } from "./chunk-H5N4ST6B.js";
50
+ import {
40
51
  getLanguage,
41
52
  locale,
42
53
  parseLang,
43
- removeCredential,
44
54
  setLanguage,
45
55
  t
46
- } from "./chunk-IJGTX7RC.js";
56
+ } from "./chunk-4RMP4CNT.js";
47
57
 
48
58
  // src/ui/index.tsx
49
59
  import { render } from "ink";
50
60
 
51
61
  // src/ui/App.tsx
52
62
  import { execFile as execFile2 } from "child_process";
53
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
63
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
54
64
  import { homedir } from "os";
55
65
  import { dirname, join as join2, relative, resolve } from "path";
56
66
  import { Box as Box13, Static, Text as Text16, useApp, useInput as useInput9 } from "ink";
@@ -69,7 +79,10 @@ var SLASH_COMMANDS = [
69
79
  { name: "status", description: "version, model, permission mode, MCP, context" },
70
80
  { name: "permissions", description: "view/revoke permissions" },
71
81
  { name: "undo", description: "undo the file edits of the last turn" },
82
+ { name: "rewind", description: "step back turns: conversation and file edits (/rewind 2)" },
72
83
  { name: "diff", description: "git diff of the project" },
84
+ { name: "worktree", description: "list git worktrees; create one with bccli -w <name>" },
85
+ { name: "pr", description: "review a pull request (/pr <n|url|branch>)" },
73
86
  { name: "copy", description: "copy the last answer" },
74
87
  { name: "export", description: "save the conversation as markdown" },
75
88
  { name: "memory", description: "project instructions (/memory <text>, /memory global <text>)" },
@@ -83,6 +96,7 @@ var SLASH_COMMANDS = [
83
96
  { name: "clear", description: "start a new conversation" },
84
97
  { name: "compact", description: "summarize the conversation" },
85
98
  { name: "cost", description: "token usage of this session" },
99
+ { name: "tasks", description: "list background tasks" },
86
100
  { name: "exit", description: "quit" }
87
101
  ];
88
102
  function parseSlash(text) {
@@ -96,6 +110,14 @@ function expandCommand(def, args) {
96
110
 
97
111
  ${args}` : def.body;
98
112
  }
113
+ function prReviewPrompt(selector) {
114
+ const target = selector.trim();
115
+ const refs = target ? `Use "${target}" as the PR selector: run gh pr view ${target} --json url,title,body,baseRefName,headRefName,files,additions,deletions and gh pr diff ${target}.` : "Start with `gh pr view --json url,title,baseRefName,headRefName` for the PR of the current branch, then fetch the change with `gh pr diff`. If the branch has no open PR, review its changes against the default branch instead: find the default branch (`gh repo view --json defaultBranchRef` or `git remote show origin`), then `git diff origin/<default-branch>...HEAD`.";
116
+ return `Review this pull request like a senior reviewer.
117
+ ${refs}
118
+ Read the changed files in the repository when the diff alone is not enough to judge the change.
119
+ Write the review as: a 2-3 sentence summary; concrete problems (bugs, security, race conditions, error handling), each with file:line and why it matters; missing or weak tests; smaller style/nit issues; then a final verdict \u2014 approve, request changes, or comment \u2014 with the single most important reason.`;
120
+ }
99
121
 
100
122
  // src/slash/copy.ts
101
123
  function osc52(text, inTmux) {
@@ -156,7 +178,7 @@ async function doctorText(input, deps) {
156
178
  function exportMarkdown(messages, title) {
157
179
  const parts = [`# ${title}`, ""];
158
180
  for (const m of messages) {
159
- if (m.role === "user") parts.push("## User", "", String(m.content), "");
181
+ if (m.role === "user") parts.push("## User", "", contentText(m.content), "");
160
182
  else if (m.role === "assistant") {
161
183
  for (const c of m.tool_calls ?? []) parts.push(`\u23BF ${c.function.name} ${c.function.arguments.slice(0, 200)}`, "");
162
184
  if (m.content) parts.push("## BCCLI", "", m.content, "");
@@ -202,21 +224,6 @@ function sessionText(s) {
202
224
  ].join("\n");
203
225
  }
204
226
 
205
- // src/slash/memory.ts
206
- import { appendFileSync, existsSync, readFileSync } from "fs";
207
- function instructionFiles(cwd, home) {
208
- return instructionPaths(cwd, home).filter((path) => existsSync(path)).map((path) => ({ path, lines: readFileSync(path, "utf8").split("\n").filter(Boolean).length }));
209
- }
210
- function appendMemory(file, text) {
211
- const prefix = existsSync(file) && !readFileSync(file, "utf8").endsWith("\n") ? "\n" : "";
212
- appendFileSync(file, `${prefix}- ${text.trim()}
213
- `);
214
- }
215
- function parseMemoryArgs(args) {
216
- const match = /^global(?:\s+|$)/i.exec(args.trim());
217
- return match ? { global: true, text: args.trim().slice(match[0].length).trim() } : { global: false, text: args.trim() };
218
- }
219
-
220
227
  // src/ui/Markdown.tsx
221
228
  import { Box, Text, useStdout } from "ink";
222
229
 
@@ -1180,7 +1187,7 @@ var nextId = 0;
1180
1187
  var entry = (e) => ({ ...e, id: nextId++ });
1181
1188
  var NOTICES = {
1182
1189
  compacted: ["Conversation summarized to fit the model context.", "info"],
1183
- stepLimit: ['Step limit of 50 reached. Type "continue" to keep going.', "warn"],
1190
+ stepLimit: ['Step limit of {n} reached. Type "continue" to keep going.', "warn"],
1184
1191
  aborted: ["Cancelled.", "warn"]
1185
1192
  };
1186
1193
  var isEmptyCont = (e) => e.kind === "assistant" && e.cont && !e.text;
@@ -1255,6 +1262,11 @@ function applyEvent(t2, event) {
1255
1262
  }
1256
1263
  case "error":
1257
1264
  return { ...t2, live: [...t2.live, entry({ kind: "notice", text: `Error: ${event.message}`, tone: "error" })] };
1265
+ case "stepLimit": {
1266
+ const limit = event.maxSteps;
1267
+ const notice = NOTICES.stepLimit;
1268
+ return notice ? { ...t2, live: [...t2.live, entry({ kind: "notice", text: t(notice[0], { n: limit }), tone: notice[1] })] } : t2;
1269
+ }
1258
1270
  default: {
1259
1271
  const notice = NOTICES[event.type];
1260
1272
  return notice ? { ...t2, live: [...t2.live, entry({ kind: "notice", text: t(notice[0]), tone: notice[1] })] } : t2;
@@ -1326,7 +1338,13 @@ function App({ runtime, initialPrompt, version }) {
1326
1338
  runtime.agent.onEvent = onEvent;
1327
1339
  runtime.agent.askPermission = (request) => new Promise((resolve2) => setAsks((q) => [...q, { id: askId.current++, request, resolve: resolve2 }]));
1328
1340
  runtime.interaction.approvePlan = (plan) => new Promise((resolve2) => setPlanAsk({ plan, resolve: resolve2 }));
1329
- }, [runtime, onEvent]);
1341
+ runHooks(runtime.config.hooks, "SessionStart", {}, { env: runtime.env, cwd: runtime.cwd }).then((outcome) => {
1342
+ for (const w of outcome.warnings) notice(w, "warn");
1343
+ }).catch(() => {
1344
+ });
1345
+ runtime.emitPlugins("SessionStart").catch(() => {
1346
+ });
1347
+ }, [runtime, onEvent, notice]);
1330
1348
  const runTurn = useCallback(
1331
1349
  async (text) => {
1332
1350
  setTranscript((t2) => ({ ...t2, done: [...t2.done, entry({ kind: "user", text })] }));
@@ -1335,11 +1353,17 @@ function App({ runtime, initialPrompt, version }) {
1335
1353
  controller.current = new AbortController();
1336
1354
  await runtime.agent.run(text, controller.current.signal);
1337
1355
  controller.current = null;
1356
+ runHooks(runtime.config.hooks, "Stop", {}, { env: runtime.env, cwd: runtime.cwd }).then((outcome) => {
1357
+ for (const w of outcome.warnings) notice(w, "warn");
1358
+ }).catch(() => {
1359
+ });
1360
+ runtime.emitPlugins("Stop").catch(() => {
1361
+ });
1338
1362
  setTranscript(endTurn);
1339
1363
  setBusy(false);
1340
1364
  setCancelling(false);
1341
1365
  },
1342
- [runtime]
1366
+ [runtime, notice]
1343
1367
  );
1344
1368
  const runSlashCommand = useCallback(
1345
1369
  async (name, args) => {
@@ -1360,6 +1384,9 @@ function App({ runtime, initialPrompt, version }) {
1360
1384
  notice(t("{input} tokens in \xB7 {output} tokens out", { input: u.inputTokens.toLocaleString(locale()), output: u.outputTokens.toLocaleString(locale()) }));
1361
1385
  return;
1362
1386
  }
1387
+ case "tasks":
1388
+ notice(renderBackgroundTasks());
1389
+ return;
1363
1390
  case "compact": {
1364
1391
  setBusy(true);
1365
1392
  setStartedAt(Date.now());
@@ -1476,7 +1503,7 @@ function App({ runtime, initialPrompt, version }) {
1476
1503
  if (!chosen) return;
1477
1504
  runtime.resume(chosen.session);
1478
1505
  setTokens(0);
1479
- const chat = runtime.agent.messages.filter(
1506
+ const chat = runtime.agent.messages.map((m) => m.role === "user" && Array.isArray(m.content) ? { ...m, content: contentText(m.content) } : m).filter(
1480
1507
  (m) => (m.role === "user" || m.role === "assistant") && typeof m.content === "string" && !!m.content.trim()
1481
1508
  );
1482
1509
  setTranscript({
@@ -1650,6 +1677,12 @@ function App({ runtime, initialPrompt, version }) {
1650
1677
  case "diff":
1651
1678
  notice(await gitDiff(runtime.cwd));
1652
1679
  return;
1680
+ case "worktree":
1681
+ notice(await worktreeListText(runtime.cwd));
1682
+ return;
1683
+ case "pr":
1684
+ void runTurn(prReviewPrompt(args));
1685
+ return;
1653
1686
  case "undo": {
1654
1687
  const result = await runtime.checkpoints.undo();
1655
1688
  if (!result) {
@@ -1668,6 +1701,25 @@ function App({ runtime, initialPrompt, version }) {
1668
1701
  );
1669
1702
  return;
1670
1703
  }
1704
+ case "rewind": {
1705
+ const parsed = Number.parseInt(args.trim(), 10);
1706
+ const result = await runtime.rewindTurns(Number.isFinite(parsed) && parsed > 0 ? parsed : 1);
1707
+ if (!result) {
1708
+ notice(t("Nothing to rewind."));
1709
+ return;
1710
+ }
1711
+ const rel = (p) => relative(runtime.cwd, p) || p;
1712
+ notice(
1713
+ [
1714
+ result.failed.length ? t("Could not restore: {files}", { files: result.failed.map(rel).join(", ") }) : "",
1715
+ result.restored.length ? t("Restored: {files}", { files: result.restored.map(rel).join(", ") }) : "",
1716
+ result.deleted.length ? t("Deleted (new files): {files}", { files: result.deleted.map(rel).join(", ") }) : "",
1717
+ result.skipped.length ? t("Too large to back up, left unchanged: {files}", { files: result.skipped.map(rel).join(", ") }) : "",
1718
+ t("Rewound {n} turn(s): the conversation continues from before them.", { n: result.turns })
1719
+ ].filter(Boolean).join("\n")
1720
+ );
1721
+ return;
1722
+ }
1671
1723
  case "copy": {
1672
1724
  const last = runtime.agent.messages.findLast((m) => m.role === "assistant" && typeof m.content === "string" && m.content.trim());
1673
1725
  if (!last || typeof last.content !== "string") {
@@ -1684,7 +1736,7 @@ function App({ runtime, initialPrompt, version }) {
1684
1736
  case "export": {
1685
1737
  const name2 = args || `bccli-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[:T]/g, "-")}.md`;
1686
1738
  const file = resolve(runtime.cwd, name2);
1687
- if (existsSync2(file)) {
1739
+ if (existsSync(file)) {
1688
1740
  const answer2 = await ask(t("{file} already exists. Overwrite? (y/N)", { file }));
1689
1741
  if (!answer2 || !/^y(a|es)?$/i.test(answer2.trim())) {
1690
1742
  notice(t("Export cancelled."));
@@ -1732,7 +1784,7 @@ function App({ runtime, initialPrompt, version }) {
1732
1784
  else if (skill) {
1733
1785
  let body;
1734
1786
  try {
1735
- body = parseFrontmatter(readFileSync2(skill.file, "utf8")).body;
1787
+ body = parseFrontmatter(readFileSync(skill.file, "utf8")).body;
1736
1788
  } catch (error) {
1737
1789
  notice(t("Skill {name} cannot be read: {error}", { name: skill.name, error: error.code ?? error.message }), "error");
1738
1790
  return;
@@ -1949,7 +2001,7 @@ ${t2.output}`);
1949
2001
  import { jsx as jsx17 } from "react/jsx-runtime";
1950
2002
  async function startInteractive(rt, opts) {
1951
2003
  if (opts.resume) {
1952
- const { Session: Session2 } = await import("./session-7UB77CEG.js");
2004
+ const { Session: Session2 } = await import("./session-IYVTCUBC.js");
1953
2005
  const list = Session2.list(rt.home, rt.cwd).slice(0, 20);
1954
2006
  if (!list.length) console.log(t("No sessions in this folder yet; starting a new one."));
1955
2007
  else {
@@ -1964,7 +2016,7 @@ async function startInteractive(rt, opts) {
1964
2016
  if (chosen) rt.resume(chosen.session);
1965
2017
  }
1966
2018
  }
1967
- const { serversToStart, setProjectTrust } = await import("./config-DMF4AO7E.js");
2019
+ const { serversToStart, setProjectTrust } = await import("./config-JRO5EOKZ.js");
1968
2020
  const plan = serversToStart(rt.home, rt.cwd);
1969
2021
  if (plan.needTrust.length) {
1970
2022
  const { createInterface } = await import("readline/promises");
@@ -1977,7 +2029,9 @@ async function startInteractive(rt, opts) {
1977
2029
  }
1978
2030
  rl.close();
1979
2031
  }
1980
- void rt.startMcp(plan.start);
2032
+ void rt.startMcp(plan.start).catch((error) => {
2033
+ console.error(t("Could not start MCP servers: {error}", { error: error instanceof Error ? error.message : String(error) }));
2034
+ });
1981
2035
  const instance = render(/* @__PURE__ */ jsx17(App, { runtime: rt, initialPrompt: opts.initialPrompt, version: opts.version }), { exitOnCtrlC: true });
1982
2036
  await instance.waitUntilExit();
1983
2037
  await rt.mcp.stop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botconnector/bccli",
3
- "version": "0.4.0-beta.35",
3
+ "version": "0.4.0-beta.37",
4
4
  "description": "BCCLI — BotConnector AI coding agent for the terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,6 +29,7 @@
29
29
  "ink": "^7.1.1",
30
30
  "openai": "^7.27.0",
31
31
  "react": "^19.3.0",
32
+ "undici": "^7.28.0",
32
33
  "ws": "^8.22.0",
33
34
  "yaml": "^2.9.1",
34
35
  "zod": "^4.6.5"
@@ -1,12 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- helpText,
4
- parseCliArgs
5
- } from "./chunk-NM3C75BT.js";
6
- import "./chunk-ELML4KVQ.js";
7
- import "./chunk-UJH2JT6H.js";
8
- import "./chunk-IJGTX7RC.js";
9
- export {
10
- helpText,
11
- parseCliArgs
12
- };