@botconnector/bccli 0.4.0-beta.37 → 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.
- package/README.md +8 -0
- package/dist/acp-4XE2KOIJ.js +114 -0
- package/dist/{args-MNVI2OJI.js → args-BD2PHEKG.js} +4 -4
- package/dist/{chunk-TASYQW7B.js → chunk-AVB6M4JE.js} +2 -2
- package/dist/{chunk-FCQCZCKO.js → chunk-EONFDWBE.js} +2 -2
- package/dist/{chunk-7TABTC32.js → chunk-GGNXHJL6.js} +4 -3
- package/dist/chunk-HE33XAPZ.js +341 -0
- package/dist/{chunk-HPZSXX4W.js → chunk-ISCT3PN3.js} +2 -2
- package/dist/{chunk-BB4CVWHS.js → chunk-JROUQ5YS.js} +6 -45
- package/dist/chunk-LN32JIZR.js +56 -0
- package/dist/{chunk-4RMP4CNT.js → chunk-M23QBX3V.js} +52 -12
- package/dist/chunk-M7LLJQ7M.js +310 -0
- package/dist/{chunk-YURAIW7A.js → chunk-NI5JZUZ3.js} +1 -1
- package/dist/chunk-PDZPPSLU.js +8 -0
- package/dist/chunk-PSO7R5AD.js +10 -0
- package/dist/chunk-QETGQXPL.js +2125 -0
- package/dist/chunk-RV74TCEP.js +231 -0
- package/dist/{chunk-U66JJ6RN.js → chunk-SESBH44L.js} +1 -1
- package/dist/{chunk-3JRBXJVB.js → chunk-VM7HYAJS.js} +1 -1
- package/dist/{chunk-ASTNW3X2.js → chunk-ZG7HCXYA.js} +89 -9
- package/dist/{chunk-HXSREA7S.js → chunk-ZMU7MXCH.js} +184 -295
- package/dist/cli.js +65 -2097
- package/dist/{config-JRO5EOKZ.js → config-C4KLLDC6.js} +3 -3
- package/dist/{integrationCli-ZTZTQD2I.js → integrationCli-HR7CDUSI.js} +2 -2
- package/dist/{login-MHVCON5R.js → login-IWBRUNT4.js} +1 -1
- package/dist/{mcpCli-5ZSAMBJS.js → mcpCli-5QTXH7XJ.js} +9 -5
- package/dist/modelsCli-DKTT3UZD.js +36 -0
- package/dist/providerCli-DDH4ZZUY.js +15 -0
- package/dist/sdk.js +5114 -0
- package/dist/serve-7BP5BVX3.js +514 -0
- package/dist/serve-I2VQUTHR.js +178 -0
- package/dist/session-H2YL5RSB.js +12 -0
- package/dist/setup-FGCFKC7Z.js +23 -0
- package/dist/{ui-YLIKFLGN.js → ui-MLOXPAHT.js} +292 -54
- package/dist/update-73WI27VM.js +109 -0
- package/package.json +5 -2
- package/dist/chunk-H5N4ST6B.js +0 -149
- package/dist/providerCli-Z3TZ5XSZ.js +0 -14
- package/dist/session-IYVTCUBC.js +0 -12
|
@@ -1,262 +1,65 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
} from "./chunk-
|
|
3
|
+
defineTool
|
|
4
|
+
} from "./chunk-PSO7R5AD.js";
|
|
5
5
|
import {
|
|
6
6
|
t
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-M23QBX3V.js";
|
|
8
8
|
|
|
9
|
-
// src/
|
|
9
|
+
// src/tools/bash.ts
|
|
10
10
|
import { spawn } from "child_process";
|
|
11
|
-
|
|
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/
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
"",
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
550
|
-
import { dirname
|
|
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
|
|
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 =
|
|
368
|
+
for (let dir = cwd; ; dir = dirname(dir)) {
|
|
562
369
|
dirs2.unshift(dir);
|
|
563
|
-
if (
|
|
370
|
+
if (dirname(dir) === dir) break;
|
|
564
371
|
}
|
|
565
|
-
return [
|
|
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 =
|
|
578
|
-
if (
|
|
579
|
-
if (
|
|
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
|
|
619
|
-
import { basename
|
|
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 [
|
|
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 =
|
|
653
|
-
if (!
|
|
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:
|
|
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 =
|
|
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:
|
|
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(
|
|
682
|
-
const name = str(data.name) ||
|
|
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
|
|
593
|
+
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
701
594
|
function instructionFiles(cwd, home) {
|
|
702
|
-
return instructionPaths(cwd, home).filter((path) =>
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
};
|