@bike4mind/cli 0.18.5 → 0.20.1

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 (41) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +204 -35
  3. package/bin/bike4mind-cli.mjs +137 -24
  4. package/bin/hearth-hook.mjs +292 -0
  5. package/dist/AgentHistoryStore-BQiATPsQ.mjs +35755 -0
  6. package/dist/ApiClient-BPmlalut.mjs +277 -0
  7. package/dist/{ConfigStore-D39UqFnY.mjs → ConfigStore-CNfbeaJf.mjs} +6702 -4122
  8. package/dist/{ImageStore-BVmEG1xc.mjs → ImageStore-kVo-oHoS.mjs} +2 -2
  9. package/dist/PluginStore-DwvOJ-G3.mjs +206 -0
  10. package/dist/ProxyManager-Bqr7Lmsd.mjs +3 -0
  11. package/dist/{ProxyManager-CV94yZUW.mjs → ProxyManager-C5H0pUyK.mjs} +2 -2
  12. package/dist/{SandboxOrchestrator-BS6gALNq.mjs → SandboxOrchestrator-BFPVpmB5.mjs} +1 -1
  13. package/dist/{SandboxOrchestrator-BoINxbX4.mjs → SandboxOrchestrator-C8uleDn2.mjs} +7 -7
  14. package/dist/ShellSessionManager-6o8KZzl1-vrbPAUTq.mjs +252 -0
  15. package/dist/{ViolationLogStore-B-plqJfn.mjs → ViolationLogStore-byEhxa2A.mjs} +1 -1
  16. package/dist/WorkItemsClient-Cow6nXx7.mjs +382 -0
  17. package/dist/{bashExecute-B1N1lMOS-TZVDbcQ4.mjs → bashExecute-CrdPpBqk-DCATrE-D.mjs} +116 -16
  18. package/dist/buildAgent-DwPvcTpz.mjs +824 -0
  19. package/dist/commands/acpCommand.mjs +798 -0
  20. package/dist/commands/apiCommand.mjs +14 -16
  21. package/dist/commands/doctorCommand.mjs +5 -5
  22. package/dist/commands/envCommand.mjs +1 -1
  23. package/dist/commands/headlessCommand.mjs +272 -76
  24. package/dist/commands/mcpCommand.mjs +14 -1
  25. package/dist/commands/pluginCommand.mjs +232 -0
  26. package/dist/commands/updateCommand.mjs +10 -9
  27. package/dist/{grepSearch-DJs-cubo-Bm0Y8oS3.mjs → grepSearch-BaYUfIYs-C-fxWc9G.mjs} +3 -3
  28. package/dist/index.mjs +3284 -2307
  29. package/dist/{package-I_v_WFUn.mjs → package-CxHSRXdp.mjs} +1 -1
  30. package/dist/serve-Du3HiqAH.mjs +772 -0
  31. package/dist/store-BG3e54c8.mjs +3 -0
  32. package/dist/{store-DV5s-qni.mjs → store-CvjTpQPs.mjs} +70 -3
  33. package/dist/{terminalSetup-BbJt04ZG.mjs → terminalSetup-DjXAwpDy.mjs} +2 -3
  34. package/dist/{treeSitterEngine-BRbQ9b7I.mjs → treeSitterEngine-QBE3YkmG.mjs} +51 -1
  35. package/dist/{updateChecker-C8xsNY2L.mjs → updateChecker-CQW8bxo6.mjs} +10 -10
  36. package/package.json +48 -43
  37. package/dist/BackgroundAgentManager-D-xsWd3C.mjs +0 -27303
  38. package/dist/ProxyManager-ByuAHFMq.mjs +0 -3
  39. package/dist/store-DgzCTRkN.mjs +0 -3
  40. package/dist/utils-Cdktpk_k.mjs +0 -158
  41. package/dist/utils-DEizxshI.mjs +0 -3
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { a as getDefaultApiUrl, t as ConfigStore } from "../ConfigStore-D39UqFnY.mjs";
2
+ import { l as resolveApiEndpoint, s as parseApiUrl, t as ConfigStore } from "../ConfigStore-CNfbeaJf.mjs";
3
3
  //#region src/commands/apiCommand.ts
4
4
  /**
5
5
  * External API config command (--api-url / --reset-api)
6
6
  * Runs outside the interactive CLI session, before any auth flow.
7
7
  *
8
- * - `--reset-api`: clears customUrl, falling back to the build-time default service
8
+ * - `--reset-api`: clears customUrl, falling back to whatever the CLI resolves
9
+ * without one (build-time default, the source-mode local dev server, or - for
10
+ * a published unbranded fork - nothing, in which case `b4m` prompts for one)
9
11
  * - `--api-url <url>`: sets a custom API URL (e.g. http://localhost:3000)
10
12
  *
11
13
  * Both clear auth tokens because they're bound to the old origin, and both
@@ -14,20 +16,13 @@ import { a as getDefaultApiUrl, t as ConfigStore } from "../ConfigStore-D39UqFnY
14
16
  async function handleApiCommand(options) {
15
17
  const configStore = new ConfigStore();
16
18
  if (options.mode === "set") {
17
- const url = options.url.trim().replace(/\/+$/, "");
18
- let parsed;
19
- try {
20
- parsed = new URL(url);
21
- } catch {
22
- console.error(`❌ Invalid URL: ${url}`);
23
- console.error(" Example: --api-url http://localhost:3000");
24
- process.exit(1);
25
- }
26
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
27
- console.error(`❌ Only http:// and https:// URLs are supported (got ${parsed.protocol}//)`);
19
+ const result = parseApiUrl(options.url);
20
+ if ("error" in result) {
21
+ console.error(`❌ ${result.error}`);
28
22
  console.error(" Example: --api-url http://localhost:3000");
29
23
  process.exit(1);
30
24
  }
25
+ const { url } = result;
31
26
  await configStore.setCustomApiUrl(url);
32
27
  await configStore.clearAuthTokens();
33
28
  console.log(`\n✅ API URL set to ${url}`);
@@ -37,10 +32,13 @@ async function handleApiCommand(options) {
37
32
  }
38
33
  await configStore.setCustomApiUrl(null);
39
34
  await configStore.clearAuthTokens();
40
- const defaultUrl = getDefaultApiUrl();
41
- console.log(`\n✅ API URL reset to the default service${defaultUrl ? ` (${defaultUrl})` : ""}`);
35
+ const endpoint = resolveApiEndpoint();
36
+ console.log("\n✅ Custom API URL cleared");
42
37
  console.log("🔓 Authentication cleared");
43
- console.log("💡 Run `b4m` to authenticate.\n");
38
+ if (endpoint.status === "configured") {
39
+ console.log(`🌍 The CLI will now use ${endpoint.url}`);
40
+ console.log("💡 Run `b4m` to authenticate.\n");
41
+ } else console.log("💡 Run `b4m` and you'll be prompted to choose a backend.\n");
44
42
  }
45
43
  //#endregion
46
44
  export { handleApiCommand };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "../package-I_v_WFUn.mjs";
3
- import { a as fetchLatestVersion, c as isNpmPrefixWritable, i as compareSemver } from "../updateChecker-C8xsNY2L.mjs";
2
+ import { t as version } from "../package-CxHSRXdp.mjs";
3
+ import { a as fetchLatestVersion, c as isNpmPrefixWritable, i as compareSemver } from "../updateChecker-CQW8bxo6.mjs";
4
4
  import { t as checkRipgrep } from "../ripgrepCheck-BmkyTK2i.mjs";
5
5
  import { execSync } from "child_process";
6
6
  import { existsSync } from "fs";
@@ -17,15 +17,15 @@ async function handleDoctorCommand() {
17
17
  console.log("Running diagnostics...\n");
18
18
  const results = [];
19
19
  const nodeVersion = process.version;
20
- if (parseInt(nodeVersion.slice(1).split(".")[0], 10) >= 18) results.push({
20
+ if (parseInt(nodeVersion.slice(1).split(".")[0], 10) >= 24) results.push({
21
21
  name: "Node.js version",
22
22
  status: "pass",
23
- message: `${nodeVersion} (>= 18 required)`
23
+ message: `${nodeVersion} (>= 24 required)`
24
24
  });
25
25
  else results.push({
26
26
  name: "Node.js version",
27
27
  status: "fail",
28
- message: `${nodeVersion} (>= 18 required, please upgrade)`
28
+ message: `${nodeVersion} (>= 24 required, please upgrade)`
29
29
  });
30
30
  const currentVersion = version;
31
31
  const latestVersion = await fetchLatestVersion();
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as ConfigStore } from "../ConfigStore-D39UqFnY.mjs";
2
+ import { t as ConfigStore } from "../ConfigStore-CNfbeaJf.mjs";
3
3
  //#region src/commands/envCommand.ts
4
4
  /**
5
5
  * Environment switching for the `--dev` / `--prod` launch flags.
@@ -1,12 +1,180 @@
1
1
  #!/usr/bin/env node
2
- import { C as createCoordinateTaskTool, D as FallbackLlmBackend, E as ApiClient, F as generateCliTools, G as ReActAgent, N as loadContextFiles, O as ServerLlmBackend, P as PermissionManager, Q as CheckpointStore, T as createAgentDelegateTool, W as setWebSocketToolExecutor, X as RemoteSkillSource, Y as isReadOnlyTool, Z as CustomCommandStore, _ as createGetFileStructureTool, a as WebSocketToolExecutor, b as createWriteTodosTool, et as SessionStore, i as McpManager, n as SubagentOrchestrator, o as WebSocketConnectionManager, q as buildSystemPrompt, r as AgentStore, s as WebSocketLlmBackend, t as BackgroundAgentManager, v as createFindDefinitionTool, w as createBackgroundAgentTools, x as createSkillTool, y as createTodoStore } from "../BackgroundAgentManager-D-xsWd3C.mjs";
3
- import { n as logger, r as getApiUrl, t as ConfigStore } from "../ConfigStore-D39UqFnY.mjs";
2
+ import { $ as RemoteSkillSource, A as FallbackLlmBackend, B as classifyCommandRisk, C as createWriteTodosTool, D as createResumeAgentTool, E as createCoordinateTaskTool, F as loadContextFiles, I as PermissionManager, J as setWebSocketToolExecutor, L as generateCliTools, M as ReActAgent, O as createBackgroundAgentTools, Q as isReadOnlyTool, S as createTodoStore, X as buildSystemPrompt, a as McpManager, b as createWorkItemTools, et as CustomCommandStore, i as AgentStore, k as createAgentDelegateTool, n as BackgroundAgentManager, nt as SessionStore, o as createSseBackend, r as SubagentOrchestrator, t as AgentHistoryStore, tt as CheckpointStore, w as createSkillTool, x as createFindDefinitionTool, y as createGetFileStructureTool, z as SHELL_LIKE_TOOL_COMMAND_FIELDS } from "../AgentHistoryStore-BQiATPsQ.mjs";
3
+ import { c as requireApiUrl, n as logger, t as ConfigStore } from "../ConfigStore-CNfbeaJf.mjs";
4
+ import { t as ApiClient } from "../ApiClient-BPmlalut.mjs";
4
5
  import { t as DEFAULT_SANDBOX_CONFIG } from "../types-LyRNHOiS.mjs";
6
+ import { r as reconstructTurnBlocks, t as WorkItemsClient } from "../WorkItemsClient-Cow6nXx7.mjs";
5
7
  import { t as createSandboxRuntime } from "../SandboxRuntimeAdapter-ChGlxSGQ.mjs";
6
- import { t as SandboxOrchestrator } from "../SandboxOrchestrator-BoINxbX4.mjs";
7
- import { t as ProxyManager } from "../ProxyManager-CV94yZUW.mjs";
8
+ import { t as SandboxOrchestrator } from "../SandboxOrchestrator-C8uleDn2.mjs";
9
+ import { t as ProxyManager } from "../ProxyManager-C5H0pUyK.mjs";
8
10
  import { randomBytes } from "crypto";
9
11
  import { v4 } from "uuid";
12
+ import { readFile } from "fs/promises";
13
+ //#region src/commands/headlessProtocol.ts
14
+ /**
15
+ * Headless stream-JSON protocol contract.
16
+ *
17
+ * `b4m -p "..." --output-format stream-json` emits one JSON object per line
18
+ * (NDJSON). This module owns that wire contract so CI and other tooling can
19
+ * depend on it: the schema version, the event shapes, and (later milestones)
20
+ * the permission protocol and strict input validation.
21
+ *
22
+ * Human-readable spec: packages/cli/docs/headless-protocol.md. Keep the two in
23
+ * sync - every event type and field documented there must be produced here.
24
+ */
25
+ /**
26
+ * Protocol schema version (semver). Bump the MAJOR when removing/renaming a
27
+ * field or event type (breaking change); bump the MINOR when adding an optional
28
+ * field or a new event type (backward-compatible). Consumers should reject a
29
+ * MAJOR they do not understand.
30
+ */
31
+ const HEADLESS_SCHEMA_VERSION = "1.0.0";
32
+ /** Serialize an event with the protocol envelope stamped on. Exposed for tests. */
33
+ function stampEvent(runId, event) {
34
+ return {
35
+ ...event,
36
+ schemaVersion: HEADLESS_SCHEMA_VERSION,
37
+ runId
38
+ };
39
+ }
40
+ /**
41
+ * Build an NDJSON emitter that stamps schemaVersion + runId onto every event.
42
+ * `write` receives one serialized line including its trailing newline.
43
+ */
44
+ function createHeadlessEmitter(runId, write) {
45
+ return (event) => {
46
+ write(JSON.stringify(stampEvent(runId, event)) + "\n");
47
+ };
48
+ }
49
+ /**
50
+ * Raised when a structured headless input is malformed or carries a field
51
+ * outside its allowlist. Headless input fails loud on protocol drift rather
52
+ * than silently ignoring unknown fields.
53
+ */
54
+ var HeadlessInputError = class extends Error {
55
+ constructor(message) {
56
+ super(message);
57
+ this.name = "HeadlessInputError";
58
+ }
59
+ };
60
+ /**
61
+ * Decode a JSON string to a plain object and reject any key outside
62
+ * `allowedKeys`. `inputType` names the input in error messages. Throws
63
+ * HeadlessInputError on invalid JSON, a non-object payload, or an unknown key.
64
+ */
65
+ function parseStrictObject(raw, allowedKeys, inputType) {
66
+ let decoded;
67
+ try {
68
+ decoded = JSON.parse(raw);
69
+ } catch (e) {
70
+ throw new HeadlessInputError(`${inputType}: invalid JSON (${e instanceof Error ? e.message : String(e)})`);
71
+ }
72
+ if (decoded === null || typeof decoded !== "object" || Array.isArray(decoded)) throw new HeadlessInputError(`${inputType}: expected a JSON object`);
73
+ const allowed = new Set(allowedKeys);
74
+ const unknownKeys = Object.keys(decoded).filter((k) => !allowed.has(k));
75
+ if (unknownKeys.length > 0) throw new HeadlessInputError(`${inputType}: unknown field(s): ${unknownKeys.join(", ")}. Allowed: ${allowedKeys.join(", ")}`);
76
+ return decoded;
77
+ }
78
+ /**
79
+ * Decode a JSON string to an array of strings, rejecting any other shape.
80
+ * Used for list-shaped inputs such as the B4M_ADDITIONAL_DIRS bridge.
81
+ */
82
+ function parseStringArray(raw, inputType) {
83
+ let decoded;
84
+ try {
85
+ decoded = JSON.parse(raw);
86
+ } catch (e) {
87
+ throw new HeadlessInputError(`${inputType}: invalid JSON (${e instanceof Error ? e.message : String(e)})`);
88
+ }
89
+ if (!Array.isArray(decoded) || decoded.some((v) => typeof v !== "string")) throw new HeadlessInputError(`${inputType}: expected a JSON array of strings`);
90
+ return decoded;
91
+ }
92
+ /**
93
+ * Classify the risk of a single tool invocation for the permission protocol.
94
+ * Shell-like tools are classified from their actual command text (reusing the
95
+ * shared command-risk tokenizer); every other tool falls back to a level derived
96
+ * from its permission category (auto_approve -> low, prompt_always -> high, else
97
+ * medium). Never throws: a classifier failure fails closed at `high`.
98
+ */
99
+ function classifyToolRisk(toolName, args, category) {
100
+ const field = SHELL_LIKE_TOOL_COMMAND_FIELDS[toolName];
101
+ const command = field && args !== null && typeof args === "object" ? args[field] : void 0;
102
+ if (typeof command === "string") try {
103
+ return classifyCommandRisk(command);
104
+ } catch {
105
+ return {
106
+ level: "high",
107
+ reasons: ["command risk analysis failed (fail closed)"]
108
+ };
109
+ }
110
+ return {
111
+ level: category === "auto_approve" ? "low" : category === "prompt_always" ? "high" : "medium",
112
+ reasons: []
113
+ };
114
+ }
115
+ const HEADLESS_PERMISSION_POLICY_KEYS = [
116
+ "allow",
117
+ "deny",
118
+ "maxAutoAllowRisk",
119
+ "defaultAction"
120
+ ];
121
+ const RISK_RANK = {
122
+ low: 0,
123
+ medium: 1,
124
+ high: 2
125
+ };
126
+ function readOptionalStringArray(value, label) {
127
+ if (value === void 0) return [];
128
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) throw new HeadlessInputError(`${label} must be an array of strings`);
129
+ return value;
130
+ }
131
+ /**
132
+ * Parse and strictly validate a permission policy from its JSON text. Rejects
133
+ * unknown fields and malformed values with HeadlessInputError. Missing lists
134
+ * default to empty and a missing defaultAction defaults to 'deny'.
135
+ */
136
+ function parsePermissionPolicy(raw) {
137
+ const obj = parseStrictObject(raw, HEADLESS_PERMISSION_POLICY_KEYS, "permission policy");
138
+ const allow = readOptionalStringArray(obj.allow, "permission policy: allow");
139
+ const deny = readOptionalStringArray(obj.deny, "permission policy: deny");
140
+ let maxAutoAllowRisk;
141
+ if (obj.maxAutoAllowRisk !== void 0) {
142
+ if (obj.maxAutoAllowRisk !== "low" && obj.maxAutoAllowRisk !== "medium" && obj.maxAutoAllowRisk !== "high") throw new HeadlessInputError("permission policy: maxAutoAllowRisk must be one of low|medium|high");
143
+ maxAutoAllowRisk = obj.maxAutoAllowRisk;
144
+ }
145
+ let defaultAction = "deny";
146
+ if (obj.defaultAction !== void 0) {
147
+ if (obj.defaultAction !== "allow" && obj.defaultAction !== "deny") throw new HeadlessInputError("permission policy: defaultAction must be one of allow|deny");
148
+ defaultAction = obj.defaultAction;
149
+ }
150
+ if (maxAutoAllowRisk !== void 0 && defaultAction === "allow") throw new HeadlessInputError("permission policy: maxAutoAllowRisk has no effect with defaultAction 'allow' (tools above the threshold are allowed anyway); set defaultAction to 'deny' or remove maxAutoAllowRisk");
151
+ return {
152
+ allow,
153
+ deny,
154
+ maxAutoAllowRisk,
155
+ defaultAction
156
+ };
157
+ }
158
+ /** Evaluate a policy against a tool and its classified risk. Never throws. */
159
+ function evaluatePermissionPolicy(policy, toolName, riskLevel) {
160
+ if (policy.deny.includes(toolName)) return {
161
+ action: "deny",
162
+ reason: "tool in policy deny list"
163
+ };
164
+ if (policy.allow.includes(toolName)) return {
165
+ action: "allow",
166
+ reason: "tool in policy allow list"
167
+ };
168
+ if (policy.maxAutoAllowRisk && RISK_RANK[riskLevel] <= RISK_RANK[policy.maxAutoAllowRisk]) return {
169
+ action: "allow",
170
+ reason: `risk ${riskLevel} <= maxAutoAllowRisk ${policy.maxAutoAllowRisk}`
171
+ };
172
+ return {
173
+ action: policy.defaultAction,
174
+ reason: `policy default (${policy.defaultAction})`
175
+ };
176
+ }
177
+ //#endregion
10
178
  //#region src/commands/headlessCommand.ts
11
179
  /**
12
180
  * Headless/programmatic mode command (b4m -p "query")
@@ -32,22 +200,33 @@ const silentLogger = {
32
200
  debug: () => {}
33
201
  };
34
202
  async function handleHeadlessCommand(options) {
35
- const { prompt, outputFormat, dangerouslySkipPermissions, addDirs } = options;
203
+ const { prompt, outputFormat, dangerouslySkipPermissions, addDirs, permissionPolicyPath } = options;
36
204
  logger.setVerbose(options.verbose);
37
205
  const stdinContent = await readStdin();
38
206
  const fullPrompt = stdinContent ? `${prompt}\n\n<stdin>\n${stdinContent}\n</stdin>` : prompt;
39
207
  const configStore = new ConfigStore();
40
208
  const sessionStore = new SessionStore();
41
209
  const customCommandStore = new CustomCommandStore();
210
+ const runId = v4();
42
211
  try {
43
212
  const config = await configStore.load();
44
213
  const configDirs = await configStore.getAdditionalDirectories();
45
- const flagDirs = process.env.B4M_ADDITIONAL_DIRS ? JSON.parse(process.env.B4M_ADDITIONAL_DIRS) : [];
46
- const additionalDirectories = [...new Set([
214
+ const flagDirs = process.env.B4M_ADDITIONAL_DIRS ? parseStringArray(process.env.B4M_ADDITIONAL_DIRS, "B4M_ADDITIONAL_DIRS") : [];
215
+ const additionalDirectories = [.../* @__PURE__ */ new Set([
47
216
  ...configDirs,
48
217
  ...flagDirs,
49
218
  ...addDirs
50
219
  ])];
220
+ let permissionPolicy = null;
221
+ if (permissionPolicyPath) {
222
+ let policyRaw;
223
+ try {
224
+ policyRaw = await readFile(permissionPolicyPath, "utf-8");
225
+ } catch (e) {
226
+ throw new Error(`Cannot read permission policy at "${permissionPolicyPath}": ${e instanceof Error ? e.message : String(e)}`);
227
+ }
228
+ permissionPolicy = parsePermissionPolicy(policyRaw);
229
+ }
51
230
  try {
52
231
  await customCommandStore.loadCommands();
53
232
  } catch {}
@@ -61,45 +240,22 @@ async function handleHeadlessCommand(options) {
61
240
  process.stderr.write("Error: Authentication token expired. Run `b4m /login` to re-authenticate.\n");
62
241
  process.exit(1);
63
242
  }
64
- const apiClient = new ApiClient(getApiUrl(config.apiConfig), configStore);
243
+ let apiBaseURL;
244
+ try {
245
+ apiBaseURL = requireApiUrl(config.apiConfig);
246
+ } catch (error) {
247
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
248
+ process.exit(1);
249
+ }
250
+ const apiClient = new ApiClient(apiBaseURL, configStore);
65
251
  if (process.env.B4M_NO_REMOTE_SKILLS !== "1" && config.preferences.enableRemoteSkills !== false) try {
66
252
  customCommandStore.setRemoteSource(new RemoteSkillSource(apiClient));
67
253
  await customCommandStore.mergeRemoteCommands();
68
254
  } catch {}
69
- const tokenGetter = async () => {
70
- return (await configStore.getAuthTokens())?.accessToken ?? null;
71
- };
72
- let wsManager = null;
73
- let llm;
74
- let completionsUrl;
75
- try {
76
- const serverConfig = await apiClient.get("/api/settings/serverConfig");
77
- const wsUrl = serverConfig?.websocketUrl;
78
- const wsCompletionUrl = serverConfig?.wsCompletionUrl;
79
- completionsUrl = serverConfig?.completionsUrl;
80
- if (wsUrl && wsCompletionUrl) {
81
- wsManager = new WebSocketConnectionManager(wsUrl, tokenGetter);
82
- await wsManager.connect();
83
- setWebSocketToolExecutor(new WebSocketToolExecutor(wsManager, tokenGetter));
84
- llm = new WebSocketLlmBackend({
85
- wsManager,
86
- apiClient,
87
- model: config.defaultModel,
88
- tokenGetter,
89
- wsCompletionUrl
90
- });
91
- logger.debug("[headless] Using WebSocket transport");
92
- } else throw new Error("No websocketUrl or wsCompletionUrl in server config");
93
- } catch {
94
- wsManager = null;
95
- setWebSocketToolExecutor(null);
96
- llm = new ServerLlmBackend({
97
- apiClient,
98
- model: config.defaultModel,
99
- completionsUrl
100
- });
101
- logger.debug("[headless] Using SSE transport fallback");
102
- }
255
+ const { llm } = await createSseBackend({
256
+ apiClient,
257
+ model: config.defaultModel
258
+ });
103
259
  const models = await llm.getModelInfo();
104
260
  if (models.length === 0) throw new Error("No models available from server.");
105
261
  const modelInfo = models.find((m) => m.id === config.defaultModel) ?? models[0];
@@ -122,22 +278,51 @@ async function handleHeadlessCommand(options) {
122
278
  };
123
279
  await logger.initialize(session.id);
124
280
  const permissionManager = new PermissionManager(config.trustedTools ?? [], void 0, config.tools.disabled);
125
- const promptFn = (toolName, _args, _preview) => {
126
- if (dangerouslySkipPermissions) {
127
- logger.debug(`[headless] Auto-allowing tool: ${toolName}`);
128
- return Promise.resolve({ action: "allow-once" });
129
- }
130
- process.stderr.write(`Warning: Tool "${toolName}" requires permission and was denied. Use --dangerously-skip-permissions to auto-allow tools in headless mode.\n`);
131
- return Promise.resolve({ action: "deny" });
281
+ const emit = createHeadlessEmitter(runId, (line) => process.stdout.write(line));
282
+ const streaming = outputFormat === "stream-json";
283
+ const promptFn = (toolName, args, _preview) => {
284
+ const risk = classifyToolRisk(toolName, args, permissionManager.getCategory(toolName));
285
+ if (streaming) emit({
286
+ type: "permission_request",
287
+ toolName,
288
+ risk
289
+ });
290
+ let decision;
291
+ if (dangerouslySkipPermissions) decision = {
292
+ action: "allow-once",
293
+ reason: "dangerously-skip-permissions"
294
+ };
295
+ else if (permissionPolicy) {
296
+ const verdict = evaluatePermissionPolicy(permissionPolicy, toolName, risk.level);
297
+ decision = {
298
+ action: verdict.action === "allow" ? "allow-once" : "deny",
299
+ reason: verdict.reason
300
+ };
301
+ } else decision = {
302
+ action: "deny",
303
+ reason: "no permission policy; default deny"
304
+ };
305
+ if (streaming) emit({
306
+ type: "permission_decision",
307
+ toolName,
308
+ action: decision.action,
309
+ reason: decision.reason,
310
+ risk: risk.level
311
+ });
312
+ else if (decision.action === "deny") process.stderr.write(`Warning: Tool "${toolName}" requires permission and was denied (${decision.reason}). Grant it via --permission-policy, or --dangerously-skip-permissions to allow all tools.\n`);
313
+ if (decision.action === "allow-once") logger.debug(`[headless] Auto-allowing tool: ${toolName}`);
314
+ return Promise.resolve({ action: decision.action });
132
315
  };
133
316
  const userQuestionFn = (_payload) => {
134
317
  process.stderr.write("Warning: Agent requested user input; headless mode cannot respond interactively. Answering with empty response.\n");
135
318
  return Promise.resolve({ answers: [] });
136
319
  };
137
320
  const sandboxConfig = config.sandbox ?? DEFAULT_SANDBOX_CONFIG;
138
- const checkpointStore = new CheckpointStore(configStore.getProjectConfigDir() ?? process.cwd());
321
+ const checkpointProjectDir = configStore.getProjectConfigDir() ?? process.cwd();
322
+ const checkpointStore = new CheckpointStore(checkpointProjectDir);
139
323
  const [sandboxRuntime] = await Promise.all([createSandboxRuntime(), checkpointStore.init(session.id).catch(() => {})]);
140
- const sandboxOrchestrator = new SandboxOrchestrator(sandboxConfig, sandboxRuntime, new ProxyManager(sandboxConfig.network));
324
+ const proxyManager = new ProxyManager(sandboxConfig.network);
325
+ const sandboxOrchestrator = new SandboxOrchestrator(sandboxConfig, sandboxRuntime, proxyManager);
141
326
  permissionManager.setSandboxState(sandboxConfig.mode, sandboxOrchestrator.isActive());
142
327
  const agentContext = {
143
328
  currentAgent: null,
@@ -154,6 +339,7 @@ async function handleHeadlessCommand(options) {
154
339
  loadContextFiles(projectConfigDir)
155
340
  ]);
156
341
  const mcpTools = mcpManager.getTools();
342
+ const historyStore = new AgentHistoryStore(config.preferences.subagentHistoryTtlMs ?? 36e5);
157
343
  const orchestrator = new SubagentOrchestrator({
158
344
  userId: config.userId,
159
345
  llm,
@@ -166,14 +352,18 @@ async function handleHeadlessCommand(options) {
166
352
  customCommandStore,
167
353
  enableParallelToolExecution: config.preferences.enableParallelToolExecution === true,
168
354
  showUserQuestion: userQuestionFn,
169
- checkpointStore
355
+ checkpointStore,
356
+ historyStore
170
357
  });
171
358
  const backgroundManager = new BackgroundAgentManager(orchestrator);
172
359
  const agentDelegateTool = createAgentDelegateTool(orchestrator, agentStore, session.id, backgroundManager);
173
360
  const backgroundTools = createBackgroundAgentTools(backgroundManager);
174
- const writeTodosTool = createWriteTodosTool(createTodoStore());
361
+ const resumeAgentTool = createResumeAgentTool(orchestrator, historyStore, backgroundManager);
362
+ const todoStore = createTodoStore();
363
+ const writeTodosTool = createWriteTodosTool(todoStore);
175
364
  const findDefinitionTool = createFindDefinitionTool();
176
365
  const getFileStructureTool = createGetFileStructureTool();
366
+ const workItemTools = config.preferences.enableWorkItemTools ? createWorkItemTools(new WorkItemsClient(apiClient)) : [];
177
367
  const enableSkillTool = config.preferences.enableSkillTool !== false;
178
368
  const skillTool = enableSkillTool ? createSkillTool({
179
369
  customCommandStore,
@@ -183,9 +373,11 @@ async function handleHeadlessCommand(options) {
183
373
  const cliTools = [
184
374
  agentDelegateTool,
185
375
  ...backgroundTools,
376
+ resumeAgentTool,
186
377
  writeTodosTool,
187
378
  findDefinitionTool,
188
- getFileStructureTool
379
+ getFileStructureTool,
380
+ ...workItemTools
189
381
  ];
190
382
  if (skillTool) cliTools.push(skillTool);
191
383
  if (config.preferences.enableCoordinatorMode === true) {
@@ -219,16 +411,15 @@ async function handleHeadlessCommand(options) {
219
411
  });
220
412
  agentContext.currentAgent = agent;
221
413
  agent.observationQueue = agentContext.observationQueue;
222
- if (outputFormat === "stream-json") {
223
- const emitNdjson = (obj) => process.stdout.write(JSON.stringify(obj) + "\n");
414
+ if (streaming) {
224
415
  agent.on("thought", (step) => {
225
- emitNdjson({
416
+ emit({
226
417
  type: "thought",
227
418
  content: step.content
228
419
  });
229
420
  });
230
421
  agent.on("action", (step) => {
231
- emitNdjson({
422
+ emit({
232
423
  type: "action",
233
424
  content: step.content,
234
425
  toolName: step.metadata?.toolName,
@@ -236,7 +427,7 @@ async function handleHeadlessCommand(options) {
236
427
  });
237
428
  });
238
429
  agent.on("observation", (step) => {
239
- emitNdjson({
430
+ emit({
240
431
  type: "observation",
241
432
  content: step.content,
242
433
  toolName: step.metadata?.toolName
@@ -255,6 +446,7 @@ async function handleHeadlessCommand(options) {
255
446
  } finally {
256
447
  backgroundManager.setCurrentTurn(null);
257
448
  }
449
+ const richContent = reconstructTurnBlocks(result.steps, result.finalAnswer);
258
450
  const finalSession = {
259
451
  ...session,
260
452
  messages: [{
@@ -266,6 +458,7 @@ async function handleHeadlessCommand(options) {
266
458
  id: v4(),
267
459
  role: "assistant",
268
460
  content: result.finalAnswer,
461
+ ...richContent ? { richContent } : {},
269
462
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
270
463
  }],
271
464
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -282,6 +475,8 @@ async function handleHeadlessCommand(options) {
282
475
  break;
283
476
  case "json": {
284
477
  const jsonResult = {
478
+ schemaVersion: HEADLESS_SCHEMA_VERSION,
479
+ runId,
285
480
  result: result.finalAnswer,
286
481
  steps: result.steps.map((s) => ({
287
482
  type: s.type,
@@ -300,32 +495,33 @@ async function handleHeadlessCommand(options) {
300
495
  process.stdout.write(JSON.stringify(jsonResult, null, 2) + "\n");
301
496
  break;
302
497
  }
303
- case "stream-json":
304
- process.stdout.write(JSON.stringify({
305
- type: "result",
306
- content: result.finalAnswer,
307
- tokenUsage: {
308
- totalTokens: result.completionInfo.totalTokens,
309
- inputTokens: result.completionInfo.totalInputTokens,
310
- outputTokens: result.completionInfo.totalOutputTokens
311
- },
312
- iterations: result.completionInfo.iterations,
313
- toolCalls: result.completionInfo.toolCalls
314
- }) + "\n");
315
- break;
498
+ case "stream-json": emit({
499
+ type: "result",
500
+ content: result.finalAnswer,
501
+ tokenUsage: {
502
+ totalTokens: result.completionInfo.totalTokens,
503
+ inputTokens: result.completionInfo.totalInputTokens,
504
+ outputTokens: result.completionInfo.totalOutputTokens
505
+ },
506
+ iterations: result.completionInfo.iterations,
507
+ toolCalls: result.completionInfo.toolCalls
508
+ });
316
509
  }
317
510
  await mcpManager.disconnect().catch(() => {});
318
- if (wsManager) wsManager.disconnect();
319
511
  setWebSocketToolExecutor(null);
320
512
  agent.removeAllListeners();
321
513
  process.exit(0);
322
514
  } catch (error) {
323
515
  const message = error instanceof Error ? error.message : String(error);
324
- if (outputFormat === "json") process.stdout.write(JSON.stringify({ error: message }) + "\n");
325
- else if (outputFormat === "stream-json") process.stdout.write(JSON.stringify({
326
- type: "error",
516
+ if (outputFormat === "json") process.stdout.write(JSON.stringify({
517
+ schemaVersion: HEADLESS_SCHEMA_VERSION,
518
+ runId,
327
519
  error: message
328
520
  }) + "\n");
521
+ else if (outputFormat === "stream-json") createHeadlessEmitter(runId, (line) => process.stdout.write(line))({
522
+ type: "error",
523
+ error: message
524
+ });
329
525
  else process.stderr.write(`Error: ${message}\n`);
330
526
  process.exit(1);
331
527
  }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { t as ConfigStore } from "../ConfigStore-D39UqFnY.mjs";
2
+ import { t as ConfigStore } from "../ConfigStore-CNfbeaJf.mjs";
3
+ import { t as version } from "../package-CxHSRXdp.mjs";
3
4
  //#region src/commands/mcpCommand.ts
4
5
  /**
5
6
  * External MCP commands (b4m mcp list, b4m mcp add, etc.)
@@ -68,6 +69,17 @@ async function handleMcpCommand(subcommand, argv) {
68
69
  }
69
70
  await handleDisable(config, argv.name, configStore);
70
71
  break;
72
+ case "serve": {
73
+ const { handleMcpServeCommand } = await import("../serve-Du3HiqAH.mjs");
74
+ await handleMcpServeCommand({
75
+ http: Boolean(argv.http),
76
+ port: typeof argv.port === "number" ? argv.port : void 0,
77
+ apiKey: typeof argv.apiKey === "string" ? argv.apiKey : void 0,
78
+ apiUrl: typeof argv.apiUrl === "string" ? argv.apiUrl : void 0,
79
+ version
80
+ });
81
+ break;
82
+ }
71
83
  default:
72
84
  console.error(`❌ Unknown MCP subcommand: ${subcommand}`);
73
85
  console.error("");
@@ -77,6 +89,7 @@ async function handleMcpCommand(subcommand, argv) {
77
89
  console.error(" b4m mcp remove <name> - Remove an MCP server");
78
90
  console.error(" b4m mcp enable <name> - Enable an MCP server");
79
91
  console.error(" b4m mcp disable <name> - Disable an MCP server");
92
+ console.error(" b4m mcp serve [--http] [--port] - Run Bike4Mind as an MCP server");
80
93
  process.exit(1);
81
94
  }
82
95
  }