@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
@@ -1,262 +1,65 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- ConfigError
4
- } from "./chunk-H5N4ST6B.js";
3
+ defineTool
4
+ } from "./chunk-PSO7R5AD.js";
5
5
  import {
6
6
  t
7
- } from "./chunk-4RMP4CNT.js";
7
+ } from "./chunk-M23QBX3V.js";
8
8
 
9
- // src/hooks.ts
9
+ // src/tools/bash.ts
10
10
  import { spawn } from "child_process";
11
- var DEFAULT_TIMEOUT_MS = 1e4;
12
- var MAX_CAPTURE = 8192;
13
- function globToRegExp(pattern) {
14
- const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
15
- return new RegExp(`^${escaped}$`);
16
- }
17
- function runOne(command, event, payload, opts) {
18
- const env = {
19
- ...process.env,
20
- ...opts.env,
21
- BCCLI_HOOK_EVENT: event,
22
- ...payload.tool !== void 0 ? { BCCLI_HOOK_TOOL: payload.tool } : {},
23
- ...payload.input !== void 0 ? { BCCLI_HOOK_INPUT: JSON.stringify(payload.input) } : {},
24
- ...payload.output !== void 0 ? { BCCLI_HOOK_OUTPUT: payload.output } : {}
25
- };
26
- return new Promise((resolve) => {
27
- let stdout = "";
28
- let stderr = "";
29
- let timedOut = false;
30
- let settled = false;
31
- const finish = (result) => {
32
- if (settled) return;
33
- settled = true;
34
- clearTimeout(timer);
35
- resolve(result);
36
- };
37
- let child;
38
- try {
39
- child = spawn(command, { shell: true, env, cwd: opts.cwd ?? process.cwd(), stdio: ["ignore", "pipe", "pipe"] });
40
- } catch (error) {
41
- resolve({ failure: error.message });
42
- return;
43
- }
44
- const timer = setTimeout(() => {
45
- timedOut = true;
46
- child.kill();
47
- }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
48
- child.stdout?.on("data", (chunk) => {
49
- if (stdout.length < MAX_CAPTURE) stdout += String(chunk);
50
- });
51
- child.stderr?.on("data", (chunk) => {
52
- if (stderr.length < MAX_CAPTURE) stderr += String(chunk);
53
- });
54
- child.on("error", (error) => finish({ failure: error.message }));
55
- child.on("close", (code) => {
56
- if (timedOut) {
57
- finish({ failure: "timed out" });
58
- return;
59
- }
60
- if (event === "PreToolUse" && code === 2) {
61
- finish({ blocked: stderr.trim() || stdout.trim() || "Blocked by a PreToolUse hook." });
62
- return;
63
- }
64
- if (code !== 0) {
65
- const detail = stderr.trim().split("\n")[0];
66
- finish({ failure: detail ? `exit ${code}: ${detail}` : `exit ${code}` });
67
- return;
68
- }
69
- finish({});
70
- });
71
- });
72
- }
73
- async function runHooks(hooks, event, payload = {}, opts = {}) {
74
- const rules = hooks?.[event];
75
- if (!Array.isArray(rules) || rules.length === 0) return { warnings: [] };
76
- const warnings = [];
77
- let blocked;
78
- for (const rule of rules) {
79
- if (!rule || typeof rule.command !== "string" || rule.command.length === 0) continue;
80
- if (payload.tool !== void 0 && rule.match && !globToRegExp(rule.match).test(payload.tool)) continue;
81
- const result = await runOne(rule.command, event, payload, opts);
82
- if (result.blocked) {
83
- blocked = result.blocked;
84
- break;
85
- }
86
- if (result.failure) warnings.push(t("Hook {event} failed: {reason}", { event, reason: result.failure }));
87
- }
88
- return { blocked, warnings };
89
- }
90
-
91
- // src/print.ts
92
- function toolLabel(tool) {
93
- if (tool === "web_search") return "Search";
94
- return tool.charAt(0).toUpperCase() + tool.slice(1);
95
- }
96
- async function runPrint(rt, prompt, io = { out: process.stdout, err: process.stderr }, opts = {}) {
97
- const format = opts.outputFormat ?? "text";
98
- const emit = (line) => io.out.write(`${JSON.stringify(line)}
99
- `);
100
- let result = "";
101
- const toolNames = /* @__PURE__ */ new Map();
102
- rt.interaction.approvePlan = async () => {
103
- if (opts.allowAll) return "allowAll";
104
- io.err.write(`${t("Plan needs approval; run with --allow-all.")}
105
- `);
106
- return "no";
107
- };
108
- let failed = false;
109
- let wroteText = false;
110
- rt.agent.onEvent = (event) => {
111
- switch (event.type) {
112
- case "text":
113
- if (format === "text") {
114
- io.out.write(event.delta);
115
- wroteText = true;
116
- } else {
117
- result += event.delta;
118
- }
119
- break;
120
- case "toolStart":
121
- toolNames.set(event.id, event.tool);
122
- io.err.write(`\u23BF ${toolLabel(event.tool)} ${event.target}
123
- `);
124
- if (format === "stream-json") emit({ type: "tool_use", tool: event.tool, target: event.target });
125
- break;
126
- case "toolEnd":
127
- if (event.isError) io.err.write(` \u2717 ${event.output.split("\n")[0]}
128
- `);
129
- if (format === "stream-json") emit({ type: "tool_result", tool: toolNames.get(event.id), isError: !!event.isError, output: event.output });
130
- break;
131
- case "stepLimit":
132
- io.err.write(`${t("Step limit reached.")}
133
- `);
134
- failed = true;
135
- break;
136
- case "aborted":
137
- io.err.write(`${t("Cancelled.")}
138
- `);
139
- failed = true;
140
- break;
141
- case "error":
142
- io.err.write(`Error: ${event.message}
143
- `);
144
- if (format === "stream-json") emit({ type: "error", message: event.message });
145
- failed = true;
146
- break;
147
- }
148
- };
149
- rt.agent.askPermission = async (req) => {
150
- io.err.write(` ${t("Needs permission for {tool}. Run with --allow-all or --allowed-tools {kind}.", { tool: req.tool, kind: req.kind })}
151
- `);
152
- return "no";
153
- };
154
- const controller = new AbortController();
155
- const onSigint = () => controller.abort();
156
- process.once("SIGINT", onSigint);
157
- const start = await runHooks(rt.config.hooks, "SessionStart", {}, { env: rt.env, cwd: rt.cwd });
158
- for (const warning of start.warnings) io.err.write(`${warning}
159
- `);
160
- await rt.emitPlugins("SessionStart");
161
- try {
162
- await rt.agent.run(prompt, controller.signal);
163
- } finally {
164
- process.off("SIGINT", onSigint);
165
- }
166
- const stop = await runHooks(rt.config.hooks, "Stop", {}, { env: rt.env, cwd: rt.cwd });
167
- for (const warning of stop.warnings) io.err.write(`${warning}
168
- `);
169
- await rt.emitPlugins("Stop");
170
- if (format === "json") {
171
- io.out.write(`${JSON.stringify({ status: failed ? "failed" : "ok", model: rt.modelRef, result })}
172
- `);
173
- } else if (format === "stream-json") {
174
- if (result) emit({ type: "message", text: result });
175
- emit({ type: "result", status: failed ? "failed" : "ok", model: rt.modelRef });
176
- } else if (wroteText) {
177
- io.out.write("\n");
178
- }
179
- return failed ? 1 : 0;
180
- }
11
+ import { z } from "zod";
181
12
 
182
- // src/worktree.ts
183
- import { execFile } from "child_process";
184
- import { existsSync } from "fs";
185
- import { basename, dirname, join } from "path";
186
- function git(cwd, args, bin = "git") {
187
- return new Promise((resolve) => {
188
- execFile(
189
- bin,
190
- args,
191
- { cwd, maxBuffer: 20 * 1024 * 1024 },
192
- (error, stdout, stderr) => resolve({ ok: !error, out: error ? stderr : stdout, missing: error?.code === "ENOENT" })
193
- );
194
- });
195
- }
196
- var NAME_RE = /^[A-Za-z0-9._-]+$/;
197
- function samePath(a, b) {
198
- const norm = (p) => process.platform === "win32" ? p.replace(/\//g, "\\").toLowerCase() : p.replace(/\\/g, "/");
199
- return norm(a) === norm(b);
200
- }
201
- function worktreeEntries(porcelain) {
202
- return porcelain.split(/\r?\n\r?\n/).map((block) => {
203
- const path = /^worktree (.+)$/m.exec(block)?.[1]?.trim();
204
- if (!path) return void 0;
205
- const branch = /^branch refs\/heads\/(.+)$/m.exec(block)?.[1];
206
- return branch ? { path, branch } : { path };
207
- }).filter((entry) => !!entry);
208
- }
209
- async function resolveWorktree(cwd, name, bin = "git") {
210
- if (!NAME_RE.test(name)) {
211
- throw new ConfigError(t('Invalid worktree name "{name}": use letters, digits, dot, dash, or underscore (no slashes).', { name }));
212
- }
213
- const inside = await git(cwd, ["rev-parse", "--is-inside-work-tree"], bin);
214
- if (inside.missing) throw new ConfigError(t("git is not installed or not on PATH, so -w cannot be used."));
215
- if (!inside.ok || inside.out.trim() !== "true") throw new ConfigError(t("This folder is not a git repository, so -w cannot create a worktree."));
216
- const list2 = await git(cwd, ["worktree", "list", "--porcelain"], bin);
217
- if (!list2.ok) throw new ConfigError(t("This folder is not a git repository, so -w cannot create a worktree."));
218
- const entries = worktreeEntries(list2.out);
219
- const main = entries[0]?.path;
220
- if (!main) throw new ConfigError(t("This folder is not a git repository, so -w cannot create a worktree."));
221
- const path = join(dirname(main), `${basename(main)}.worktrees`, name);
222
- if (entries.some((entry) => samePath(entry.path, path))) return { path, created: false };
223
- if (existsSync(path)) {
224
- throw new ConfigError(t("Worktree path {path} already exists but is not a registered worktree; remove or rename it first.", { path }));
225
- }
226
- const branch = `bccli/${name}`;
227
- const hasBranch = (await git(main, ["rev-parse", "--verify", "-q", `refs/heads/${branch}`], bin)).ok;
228
- const add = hasBranch ? await git(main, ["worktree", "add", path, branch], bin) : await git(main, ["worktree", "add", "-b", branch, path], bin);
229
- if (!add.ok) throw new ConfigError(add.out.trim() || t("This folder is not a git repository, so -w cannot create a worktree."));
230
- return { path, created: true };
231
- }
232
- async function worktreeListText(cwd, bin = "git") {
233
- const inside = await git(cwd, ["rev-parse", "--is-inside-work-tree"], bin);
234
- if (inside.missing) return t("git is not installed or not on PATH, so /worktree cannot be used.");
235
- if (!inside.ok || inside.out.trim() !== "true") return t("This folder is not a git repository, so there is no diff.");
236
- const list2 = await git(cwd, ["worktree", "list", "--porcelain"], bin);
237
- if (!list2.ok) return t("This folder is not a git repository, so there is no diff.");
238
- const rows = worktreeEntries(list2.out).map((entry) => ` ${entry.path}${entry.branch ? ` (${entry.branch})` : ""}`);
13
+ // src/tools/sandbox.ts
14
+ function darwinProfile(cwd, network) {
15
+ const writable = [cwd, "/tmp", "/private/tmp", "/dev", "/var/folders"].map((p) => `(subpath "${p}")`).join(" ");
239
16
  return [
240
- t("Git worktrees:"),
241
- ...rows,
242
- "",
243
- t("Create one with: bccli -w <name>"),
244
- t("Remove with: git worktree remove <path>")
17
+ "(version 1)",
18
+ "(deny default)",
19
+ "(allow process*)",
20
+ "(allow signal (target self))",
21
+ "(allow sysctl-read)",
22
+ "(allow file-read*)",
23
+ `(allow file-write* ${writable})`,
24
+ "(allow mach-lookup)",
25
+ "(allow ipc-posix*)",
26
+ network ? "(allow network*)" : "(deny network*)"
245
27
  ].join("\n");
246
28
  }
247
-
248
- // src/tools/bash.ts
249
- import { spawn as spawn2 } from "child_process";
250
- import { z } from "zod";
251
-
252
- // src/tools/types.ts
253
- function defineTool(tool) {
254
- return tool;
29
+ function sandboxArgv(command, opts) {
30
+ if (opts.platform === "darwin") {
31
+ return ["sandbox-exec", "-p", darwinProfile(opts.cwd, opts.network), "/bin/bash", "-c", command];
32
+ }
33
+ if (opts.platform === "linux") {
34
+ return [
35
+ "bwrap",
36
+ "--ro-bind",
37
+ "/",
38
+ "/",
39
+ "--dev-bind",
40
+ "/dev",
41
+ "/dev",
42
+ "--proc",
43
+ "/proc",
44
+ "--bind",
45
+ opts.cwd,
46
+ opts.cwd,
47
+ "--tmpfs",
48
+ "/tmp",
49
+ "--die-with-parent",
50
+ ...opts.network ? [] : ["--unshare-net"],
51
+ "--",
52
+ "/bin/bash",
53
+ "-c",
54
+ command
55
+ ];
56
+ }
57
+ return void 0;
255
58
  }
256
59
 
257
60
  // src/tools/bash.ts
258
61
  var HALF = 15e3;
259
- function killProcessTree(pid, platform = process.platform, spawnFn = spawn2) {
62
+ function killProcessTree(pid, platform = process.platform, spawnFn = spawn) {
260
63
  if (platform === "win32") {
261
64
  const killer = spawnFn("taskkill", ["/pid", String(pid), "/T", "/F"]);
262
65
  killer.on("error", () => {
@@ -285,16 +88,10 @@ function killAllCommands() {
285
88
  var backgrounds = /* @__PURE__ */ new Map();
286
89
  var nextBackgroundId = 1;
287
90
  function startBackground(command, cwd, env) {
288
- const child = spawn2(command, {
289
- cwd,
290
- shell: true,
291
- detached: process.platform !== "win32",
292
- stdio: ["ignore", "pipe", "pipe"],
293
- env
294
- });
91
+ const child = spawnChild(command, cwd, env);
295
92
  const task = {
296
93
  id: nextBackgroundId++,
297
- command,
94
+ command: Array.isArray(command) ? command.join(" ") : command,
298
95
  startedAt: Date.now(),
299
96
  status: "running",
300
97
  killed: false,
@@ -362,6 +159,9 @@ function renderBackgroundTasks() {
362
159
  return `Background tasks:
363
160
  ${lines.join("\n")}`;
364
161
  }
162
+ function spawnChild(command, cwd, env) {
163
+ return Array.isArray(command) ? spawn(command[0], command.slice(1), { cwd, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"], env }) : spawn(command, { cwd, shell: true, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"], env });
164
+ }
365
165
  var NETWORK_PATTERNS = [
366
166
  /\b(curl|wget|ftp|scp|sftp|rsync|lynx|telnet)\b/i,
367
167
  /\bssh\b/i,
@@ -396,13 +196,7 @@ function offlineEnv(env = process.env) {
396
196
  }
397
197
  function runCommand(command, opts) {
398
198
  return new Promise((resolve) => {
399
- const child = spawn2(command, {
400
- cwd: opts.cwd,
401
- shell: true,
402
- detached: process.platform !== "win32",
403
- stdio: ["ignore", "pipe", "pipe"],
404
- env: opts.env
405
- });
199
+ const child = spawnChild(command, opts.cwd, opts.env);
406
200
  if (!exitHookInstalled) {
407
201
  process.on("exit", killAllCommands);
408
202
  exitHookInstalled = true;
@@ -472,9 +266,11 @@ var BASH_SCHEMA = z.object({
472
266
  });
473
267
  function createBashTool(options = {}) {
474
268
  const offline = options.networkPolicy === "offline";
269
+ const sandbox = options.sandbox === true;
270
+ const platform = options.platform ?? process.platform;
475
271
  const childEnv = offline ? offlineEnv() : void 0;
476
- const description = offline ? `${BASH_DESCRIPTION}
477
- Network access is off (networkPolicy "offline"): internet commands such as curl, wget, git fetch/push/clone, package installs (npm, pip, ...), gh, and docker pull are blocked.` : BASH_DESCRIPTION;
272
+ const description = `${offline ? `${BASH_DESCRIPTION}
273
+ Network access is off (networkPolicy "offline"): internet commands such as curl, wget, git fetch/push/clone, package installs (npm, pip, ...), gh, and docker pull are blocked.` : BASH_DESCRIPTION}${sandbox ? "\nCommands run inside an OS sandbox: file writes are limited to the working directory and temp areas." : ""}`;
478
274
  return defineTool({
479
275
  name: "bash",
480
276
  description,
@@ -501,9 +297,20 @@ Network access is off (networkPolicy "offline"): internet commands such as curl,
501
297
  isError: true
502
298
  };
503
299
  }
300
+ let exec = input.command ?? "";
301
+ if (input.command && sandbox) {
302
+ const argv = sandboxArgv(input.command, { platform, cwd: ctx.cwd, network: !offline });
303
+ if (!argv) {
304
+ return {
305
+ output: `The bash sandbox is not supported on ${platform}; disable "sandbox" in ~/.bccli/config.json or run on macOS (sandbox-exec) / Linux (bwrap).`,
306
+ isError: true
307
+ };
308
+ }
309
+ exec = argv;
310
+ }
504
311
  if (input.background) {
505
312
  if (!input.command) return { output: 'The "command" argument is required to start a background task.', isError: true };
506
- const task = startBackground(input.command, ctx.cwd, childEnv);
313
+ const task = startBackground(exec, ctx.cwd, childEnv);
507
314
  return {
508
315
  output: `Background task #${task.id} started: ${input.command}
509
316
  Poll it with {task_id: ${task.id}}; stop it with {task_id: ${task.id}, kill: true}.`
@@ -512,7 +319,7 @@ Poll it with {task_id: ${task.id}}; stop it with {task_id: ${task.id}, kill: tru
512
319
  if (!input.command) {
513
320
  return { output: 'The "command" argument is required (or set task_id to poll a background task).', isError: true };
514
321
  }
515
- const r = await runCommand(input.command, {
322
+ const r = await runCommand(exec, {
516
323
  cwd: ctx.cwd,
517
324
  timeoutMs: input.timeout_ms ?? 12e4,
518
325
  signal: ctx.signal,
@@ -546,23 +353,23 @@ var BUILTIN_AGENTS = [
546
353
  ];
547
354
 
548
355
  // src/context.ts
549
- import { existsSync as existsSync2, readFileSync } from "fs";
550
- import { dirname as dirname2, join as join2 } from "path";
356
+ import { existsSync, readFileSync } from "fs";
357
+ import { dirname, join } from "path";
551
358
  var FILES = ["AGENTS.md", "BCCLI.md"];
552
359
  function readIf(path) {
553
360
  try {
554
- return existsSync2(path) ? readFileSync(path, "utf8").trim() : void 0;
361
+ return existsSync(path) ? readFileSync(path, "utf8").trim() : void 0;
555
362
  } catch {
556
363
  return void 0;
557
364
  }
558
365
  }
559
366
  function instructionPaths(cwd, home) {
560
367
  const dirs2 = [];
561
- for (let dir = cwd; ; dir = dirname2(dir)) {
368
+ for (let dir = cwd; ; dir = dirname(dir)) {
562
369
  dirs2.unshift(dir);
563
- if (dirname2(dir) === dir) break;
370
+ if (dirname(dir) === dir) break;
564
371
  }
565
- return [join2(home, "BCCLI.md"), ...dirs2.flatMap((dir) => FILES.map((name) => join2(dir, name)))];
372
+ return [join(home, "BCCLI.md"), ...dirs2.flatMap((dir) => FILES.map((name) => join(dir, name)))];
566
373
  }
567
374
  function loadInstructions(cwd, home) {
568
375
  const parts = [];
@@ -574,9 +381,9 @@ ${text}`);
574
381
  return parts.join("\n\n");
575
382
  }
576
383
  function isGitRepo(cwd) {
577
- for (let dir = cwd; ; dir = dirname2(dir)) {
578
- if (existsSync2(join2(dir, ".git"))) return true;
579
- if (dirname2(dir) === dir) return false;
384
+ for (let dir = cwd; ; dir = dirname(dir)) {
385
+ if (existsSync(join(dir, ".git"))) return true;
386
+ if (dirname(dir) === dir) return false;
580
387
  }
581
388
  }
582
389
  function buildSystemPrompt(opts) {
@@ -586,6 +393,10 @@ function buildSystemPrompt(opts) {
586
393
 
587
394
  Skills (load one with the skill tool when it matches the task):
588
395
  ${skills.map((s) => `- ${s.name}: ${s.description.slice(0, 200)}`).join("\n")}` : "";
396
+ const mapText = opts.repoMap ? `
397
+
398
+ Repository map (files and top-level symbols):
399
+ ${opts.repoMap}` : "";
589
400
  return `You are a coding agent running in the user's terminal, inside BCCLI (BotConnector's terminal app). You help with software engineering tasks by reading code, editing files, and running commands with the tools provided.
590
401
 
591
402
  How to work:
@@ -607,7 +418,7 @@ Environment:
607
418
  - Git repository: ${isGitRepo(opts.cwd) ? "yes" : "no"}
608
419
  - Platform: ${opts.platform ?? process.platform}
609
420
  - Date: ${opts.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
610
- - Model: ${opts.model}${skillText}${instructions ? `
421
+ - Model: ${opts.model}${skillText}${mapText}${instructions ? `
611
422
 
612
423
  Project and user instructions (follow them):
613
424
 
@@ -615,8 +426,8 @@ ${instructions}` : ""}`;
615
426
  }
616
427
 
617
428
  // src/extensions.ts
618
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
619
- import { basename as basename2, join as join3 } from "path";
429
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
430
+ import { basename, join as join2 } from "path";
620
431
  function parseFrontmatter(text) {
621
432
  const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
622
433
  if (!match) return { data: {}, body: text };
@@ -636,7 +447,7 @@ function parseFrontmatter(text) {
636
447
  }
637
448
  var str = (v) => Array.isArray(v) ? v.join(", ") : v;
638
449
  function dirs(r, kind) {
639
- return [join3(r.userHome, ".claude", kind), join3(r.home, kind), join3(r.cwd, ".claude", kind), join3(r.cwd, ".bccli", kind)];
450
+ return [join2(r.userHome, ".claude", kind), join2(r.home, kind), join2(r.cwd, ".claude", kind), join2(r.cwd, ".bccli", kind)];
640
451
  }
641
452
  function list(dir) {
642
453
  try {
@@ -649,11 +460,11 @@ function loadSkills(r) {
649
460
  const byName = /* @__PURE__ */ new Map();
650
461
  for (const dir of dirs(r, "skills")) {
651
462
  for (const entry of list(dir)) {
652
- const file = join3(dir, entry, "SKILL.md");
653
- if (!existsSync3(file)) continue;
463
+ const file = join2(dir, entry, "SKILL.md");
464
+ if (!existsSync2(file)) continue;
654
465
  const { data } = parseFrontmatter(readFileSync2(file, "utf8"));
655
466
  const name = str(data.name) || entry;
656
- byName.set(name, { name, description: str(data.description) ?? "", dir: join3(dir, entry), file });
467
+ byName.set(name, { name, description: str(data.description) ?? "", dir: join2(dir, entry), file });
657
468
  }
658
469
  }
659
470
  return [...byName.values()];
@@ -662,10 +473,10 @@ function loadCommands(r) {
662
473
  const byName = /* @__PURE__ */ new Map();
663
474
  for (const dir of dirs(r, "commands")) {
664
475
  for (const entry of list(dir)) {
665
- const file = join3(dir, entry);
476
+ const file = join2(dir, entry);
666
477
  if (!entry.endsWith(".md") || !statSync(file).isFile()) continue;
667
478
  const { data, body } = parseFrontmatter(readFileSync2(file, "utf8"));
668
- const def = { name: basename2(entry, ".md"), body: body.trim() };
479
+ const def = { name: basename(entry, ".md"), body: body.trim() };
669
480
  if (data.description) def.description = str(data.description);
670
481
  if (data["argument-hint"]) def.argumentHint = str(data["argument-hint"]);
671
482
  byName.set(def.name, def);
@@ -678,8 +489,8 @@ function loadAgentDefs(r) {
678
489
  for (const dir of dirs(r, "agents")) {
679
490
  for (const entry of list(dir)) {
680
491
  if (!entry.endsWith(".md")) continue;
681
- const { data, body } = parseFrontmatter(readFileSync2(join3(dir, entry), "utf8"));
682
- const name = str(data.name) || basename2(entry, ".md");
492
+ const { data, body } = parseFrontmatter(readFileSync2(join2(dir, entry), "utf8"));
493
+ const name = str(data.name) || basename(entry, ".md");
683
494
  const def = { name, description: str(data.description) ?? "", prompt: body.trim() };
684
495
  if (data.tools)
685
496
  def.tools = Array.isArray(data.tools) ? data.tools : String(data.tools).split(",").map((s) => s.trim()).filter(Boolean);
@@ -696,13 +507,95 @@ function loadAgentDefs(r) {
696
507
  return [...byName.values()];
697
508
  }
698
509
 
510
+ // src/hooks.ts
511
+ import { spawn as spawn2 } from "child_process";
512
+ var DEFAULT_TIMEOUT_MS = 1e4;
513
+ var MAX_CAPTURE = 8192;
514
+ function globToRegExp(pattern) {
515
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
516
+ return new RegExp(`^${escaped}$`);
517
+ }
518
+ function runOne(command, event, payload, opts) {
519
+ const env = {
520
+ ...process.env,
521
+ ...opts.env,
522
+ BCCLI_HOOK_EVENT: event,
523
+ ...payload.tool !== void 0 ? { BCCLI_HOOK_TOOL: payload.tool } : {},
524
+ ...payload.input !== void 0 ? { BCCLI_HOOK_INPUT: JSON.stringify(payload.input) } : {},
525
+ ...payload.output !== void 0 ? { BCCLI_HOOK_OUTPUT: payload.output } : {}
526
+ };
527
+ return new Promise((resolve) => {
528
+ let stdout = "";
529
+ let stderr = "";
530
+ let timedOut = false;
531
+ let settled = false;
532
+ const finish = (result) => {
533
+ if (settled) return;
534
+ settled = true;
535
+ clearTimeout(timer);
536
+ resolve(result);
537
+ };
538
+ let child;
539
+ try {
540
+ child = spawn2(command, { shell: true, env, cwd: opts.cwd ?? process.cwd(), stdio: ["ignore", "pipe", "pipe"] });
541
+ } catch (error) {
542
+ resolve({ failure: error.message });
543
+ return;
544
+ }
545
+ const timer = setTimeout(() => {
546
+ timedOut = true;
547
+ child.kill();
548
+ }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
549
+ child.stdout?.on("data", (chunk) => {
550
+ if (stdout.length < MAX_CAPTURE) stdout += String(chunk);
551
+ });
552
+ child.stderr?.on("data", (chunk) => {
553
+ if (stderr.length < MAX_CAPTURE) stderr += String(chunk);
554
+ });
555
+ child.on("error", (error) => finish({ failure: error.message }));
556
+ child.on("close", (code) => {
557
+ if (timedOut) {
558
+ finish({ failure: "timed out" });
559
+ return;
560
+ }
561
+ if (event === "PreToolUse" && code === 2) {
562
+ finish({ blocked: stderr.trim() || stdout.trim() || "Blocked by a PreToolUse hook." });
563
+ return;
564
+ }
565
+ if (code !== 0) {
566
+ const detail = stderr.trim().split("\n")[0];
567
+ finish({ failure: detail ? `exit ${code}: ${detail}` : `exit ${code}` });
568
+ return;
569
+ }
570
+ finish({});
571
+ });
572
+ });
573
+ }
574
+ async function runHooks(hooks, event, payload = {}, opts = {}) {
575
+ const rules = hooks?.[event];
576
+ if (!Array.isArray(rules) || rules.length === 0) return { warnings: [] };
577
+ const warnings = [];
578
+ let blocked;
579
+ for (const rule of rules) {
580
+ if (!rule || typeof rule.command !== "string" || rule.command.length === 0) continue;
581
+ if (payload.tool !== void 0 && rule.match && !globToRegExp(rule.match).test(payload.tool)) continue;
582
+ const result = await runOne(rule.command, event, payload, opts);
583
+ if (result.blocked) {
584
+ blocked = result.blocked;
585
+ break;
586
+ }
587
+ if (result.failure) warnings.push(t("Hook {event} failed: {reason}", { event, reason: result.failure }));
588
+ }
589
+ return { blocked, warnings };
590
+ }
591
+
699
592
  // src/slash/memory.ts
700
- import { appendFileSync, existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
593
+ import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
701
594
  function instructionFiles(cwd, home) {
702
- return instructionPaths(cwd, home).filter((path) => existsSync4(path)).map((path) => ({ path, lines: readFileSync3(path, "utf8").split("\n").filter(Boolean).length }));
595
+ return instructionPaths(cwd, home).filter((path) => existsSync3(path)).map((path) => ({ path, lines: readFileSync3(path, "utf8").split("\n").filter(Boolean).length }));
703
596
  }
704
597
  function appendMemory(file, text) {
705
- const prefix = existsSync4(file) && !readFileSync3(file, "utf8").endsWith("\n") ? "\n" : "";
598
+ const prefix = existsSync3(file) && !readFileSync3(file, "utf8").endsWith("\n") ? "\n" : "";
706
599
  appendFileSync(file, `${prefix}- ${text.trim()}
707
600
  `);
708
601
  }
@@ -713,12 +606,12 @@ function parseMemoryArgs(args) {
713
606
 
714
607
  // src/tools/ignore.ts
715
608
  import { readFileSync as readFileSync4 } from "fs";
716
- import { join as join4 } from "path";
609
+ import { join as join3 } from "path";
717
610
  function ignorePatterns(root) {
718
611
  const patterns = ["**/node_modules/**", "**/.git/**"];
719
612
  let lines = [];
720
613
  try {
721
- lines = readFileSync4(join4(root, ".gitignore"), "utf8").split(/\r?\n/);
614
+ lines = readFileSync4(join3(root, ".gitignore"), "utf8").split(/\r?\n/);
722
615
  } catch {
723
616
  return patterns;
724
617
  }
@@ -762,10 +655,8 @@ function normalizeGlobPath(path) {
762
655
 
763
656
  export {
764
657
  runHooks,
765
- toolLabel,
766
- runPrint,
767
- defineTool,
768
658
  renderBackgroundTasks,
659
+ runCommand,
769
660
  createBashTool,
770
661
  bashTool,
771
662
  ignorePatterns,
@@ -779,7 +670,5 @@ export {
779
670
  loadAgentDefs,
780
671
  instructionFiles,
781
672
  appendMemory,
782
- parseMemoryArgs,
783
- resolveWorktree,
784
- worktreeListText
673
+ parseMemoryArgs
785
674
  };