@bike4mind/cli 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { At as tokenEstimateMultiplier } from "./ConfigStore-RHqyXcSS.mjs";
2
+ import { Pt as tokenEstimateMultiplier } from "./ConfigStore-DUATzLxB.mjs";
3
3
  import { v4 } from "uuid";
4
4
  import { get_encoding } from "tiktoken";
5
5
  //#region ../../b4m-core/utils/dist/calibratedTokenizer.mjs
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { J as getPlanModeFilePath, L as generateCliTools, N as ReActAgent, R as wrapTools, Y as buildSystemPrompt, a as loadProjectContext, c as createSseBackend, i as buildProjectAgentStore, l as ServerLlmBackend, lt as Logger, n as BackgroundAgentManager, o as McpManager, q as setWebSocketToolExecutor, r as SubagentOrchestrator, t as AgentHistoryStore } from "./AgentHistoryStore-O2gKB5YG.mjs";
3
- import { D as ARTIFACT_REMOVED_PLACEHOLDER, Dt as stripDeliveredArtifactBlocks, E as ARTIFACT_DELIVERED_PLACEHOLDER, F as ChatModels, I as CompletionReasoningEffortSchema, K as ModelBackend, N as CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS, Ot as stripToolArtifactMarkup, W as MODEL_INFO_FIELD_GROUP_OF, Z as PermissionDeniedError, _ as logger, ct as createThinkMarkerEscaper, ht as hasDeliverablePinnedArtifact, lt as escapeThinkMarkers, pt as getQuestErrorCode, ut as filterToolArtifactMarkup, yt as isUserInitiatedAbort } from "./ConfigStore-RHqyXcSS.mjs";
2
+ import { F as generateCliTools, G as setWebSocketToolExecutor, J as DeferredToolRegistry, U as wrapTools, V as buildSystemPrompt, Y as ReActAgent, a as loadProjectContext, c as createSseBackend, i as buildProjectAgentStore, l as ServerLlmBackend, n as BackgroundAgentManager, o as McpManager, q as getPlanModeFilePath, r as SubagentOrchestrator, t as AgentHistoryStore, ut as Logger } from "./AgentHistoryStore-CLidbOHG.mjs";
3
+ import { A as ARTIFACT_REMOVED_PLACEHOLDER, Ct as isUserInitiatedAbort, I as CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS, Mt as stripToolArtifactMarkup, R as ChatModels, Y as ModelBackend, _t as getQuestErrorCode, b as logger, et as PermissionDeniedError, ft as createThinkMarkerEscaper, jt as stripDeliveredArtifactBlocks, k as ARTIFACT_DELIVERED_PLACEHOLDER, mt as filterToolArtifactMarkup, pt as escapeThinkMarkers, q as MODEL_INFO_FIELD_GROUP_OF, yt as hasDeliverablePinnedArtifact, z as CompletionReasoningEffortSchema } from "./ConfigStore-DUATzLxB.mjs";
4
4
  import "crypto";
5
5
  import { v4 } from "uuid";
6
- import { z as z$1 } from "zod";
7
6
  import "axios";
8
7
  import "@aws-sdk/client-bedrock-runtime";
9
8
  import "openai";
@@ -367,7 +366,8 @@ var AnthropicCachingAdapter = class {
367
366
  const messagesParam = modifiedParams.messages;
368
367
  if (strategy.cacheConversationHistory && Array.isArray(messagesParam) && messagesParam.length > 0) {
369
368
  const messages = [...messagesParam];
370
- const anchorIndex = messages.length - 1 - (strategy.historyCacheExcludeTrailingCount ?? 0);
369
+ let anchorIndex = messages.length - 1 - (strategy.historyCacheExcludeTrailingCount ?? 0);
370
+ while (anchorIndex >= 0 && messages[anchorIndex].role === "system") anchorIndex--;
371
371
  if (anchorIndex >= 0) {
372
372
  const anchorMsg = messages[anchorIndex];
373
373
  const msgContent = anchorMsg.content;
@@ -394,12 +394,17 @@ var AnthropicCachingAdapter = class {
394
394
  }
395
395
  }
396
396
  const tools = modifiedParams.tools;
397
- if (strategy.cacheTools && Array.isArray(tools) && tools.length > 0) {
397
+ const markIndex = Array.isArray(tools) ? tools.findLastIndex((t) => {
398
+ const tool = t;
399
+ const isServerTool = typeof tool.type === "string" && !("input_schema" in tool);
400
+ return tool.defer_loading !== true && !isServerTool;
401
+ }) : -1;
402
+ if (strategy.cacheTools && Array.isArray(tools) && markIndex >= 0) {
398
403
  const toolsCopy = [...tools];
399
- const lastTool = toolsCopy[toolsCopy.length - 1];
400
- if (claim("tools", hasMarker(lastTool))) {
401
- toolsCopy[toolsCopy.length - 1] = {
402
- ...lastTool,
404
+ const markTool = toolsCopy[markIndex];
405
+ if (claim("tools", hasMarker(markTool))) {
406
+ toolsCopy[markIndex] = {
407
+ ...markTool,
403
408
  cache_control: cacheControl
404
409
  };
405
410
  modifiedParams.tools = toolsCopy;
@@ -665,6 +670,7 @@ var NoOpCachingAdapter = class {
665
670
  ModelBackend.Anthropic, new AnthropicCachingAdapter(), ModelBackend.OpenAI, new OpenAICachingAdapter(), ModelBackend.Gemini, new GeminiCachingAdapter(), ModelBackend.Bedrock, new AnthropicCachingAdapter(), ModelBackend.XAI, new XAICachingAdapter(), ModelBackend.Kimi, new KimiCachingAdapter(), ModelBackend.DeepSeek, new DeepSeekCachingAdapter(), ModelBackend.Ollama, new NoOpCachingAdapter(), ModelBackend.BFL, new NoOpCachingAdapter(), ModelBackend.VoyageAI, new NoOpCachingAdapter(), ModelBackend.AWS, new NoOpCachingAdapter(), ModelBackend.LocalImage, new NoOpCachingAdapter();
666
671
  ChatModels.KIMI_K2_THINKING_BEDROCK, ChatModels.KIMI_K2_5_BEDROCK, ChatModels.DEEPSEEK_R1_BEDROCK, ChatModels.DEEPSEEK_FLASH, ChatModels.DEEPSEEK_V4_PRO, ChatModels.KIMI_K3;
667
672
  ChatModels.CLAUDE_4_7_OPUS, ChatModels.CLAUDE_4_7_OPUS_BEDROCK, ChatModels.CLAUDE_4_8_OPUS, ChatModels.CLAUDE_4_8_OPUS_BEDROCK, ChatModels.CLAUDE_5_SONNET, ChatModels.CLAUDE_5_SONNET_BEDROCK, ChatModels.CLAUDE_5_OPUS, ChatModels.CLAUDE_5_OPUS_BEDROCK, ChatModels.CLAUDE_5_5_HAIKU, ChatModels.CLAUDE_5_5_HAIKU_BEDROCK, ChatModels.CLAUDE_5_5_SONNET_BEDROCK;
673
+ ChatModels.CLAUDE_5_SONNET, ChatModels.CLAUDE_5_SONNET_BEDROCK, ChatModels.CLAUDE_5_5_SONNET_BEDROCK, ChatModels.CLAUDE_FABLE_5, ChatModels.CLAUDE_FABLE_5_BEDROCK, ChatModels.CLAUDE_FABLE_5_1_BEDROCK, ChatModels.CLAUDE_5_OPUS, ChatModels.CLAUDE_5_OPUS_BEDROCK, ChatModels.CLAUDE_5_5_OPUS, ChatModels.CLAUDE_5_5_OPUS_BEDROCK, ChatModels.CLAUDE_5_5_HAIKU, ChatModels.CLAUDE_5_5_HAIKU_BEDROCK;
668
674
  new Logger();
669
675
  ChatModels.CLAUDE_4_5_SONNET, ChatModels.CLAUDE_4_1_OPUS, ChatModels.CLAUDE_4_5_HAIKU, ChatModels.CLAUDE_4_5_OPUS, ChatModels.CLAUDE_4_6_SONNET, ChatModels.CLAUDE_4_6_OPUS;
670
676
  ChatModels.CLAUDE_4_5_SONNET_BEDROCK, ChatModels.CLAUDE_4_5_HAIKU_BEDROCK, ChatModels.CLAUDE_4_5_OPUS_BEDROCK, ChatModels.CLAUDE_4_6_SONNET_BEDROCK, ChatModels.CLAUDE_4_6_OPUS_BEDROCK;
@@ -753,6 +759,19 @@ ChatModels.KIMI_K2_7_CODE, ChatModels.KIMI_K2_7_CODE_HIGHSPEED, ChatModels.KIMI_
753
759
  ChatModels.KIMI_K2_7_CODE, ChatModels.KIMI_K2_7_CODE_HIGHSPEED;
754
760
  ChatModels.KIMI_K2_7_CODE, ChatModels.KIMI_K2_7_CODE_HIGHSPEED, ChatModels.KIMI_K2_6;
755
761
  ChatModels.KIMI_K3, ChatModels.KIMI_K2_7_CODE, ChatModels.KIMI_K2_7_CODE_HIGHSPEED, ChatModels.KIMI_K2_6, ChatModels.KIMI_K2_5;
762
+ /**
763
+ * Ollama's native /api/chat requires tool-call arguments as an object and rejects a JSON string
764
+ * with a 400. Unparseable or non-object input is kept under `_raw` rather than dropped.
765
+ */
766
+ function toOllamaToolArguments(args) {
767
+ if (args !== null && typeof args === "object" && !Array.isArray(args)) return args;
768
+ if (typeof args !== "string" || args.trim() === "") return {};
769
+ try {
770
+ const parsed = JSON.parse(args);
771
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
772
+ } catch {}
773
+ return { _raw: args };
774
+ }
756
775
  var OllamaBackend = class OllamaBackend {
757
776
  _host;
758
777
  _api;
@@ -1294,18 +1313,12 @@ var OllamaBackend = class OllamaBackend {
1294
1313
  };
1295
1314
  }
1296
1315
  pushToolMessages(messages, tool, result, _thinkingBlocks) {
1297
- let argumentsObj;
1298
- try {
1299
- argumentsObj = JSON.parse(tool.parameters);
1300
- } catch {
1301
- argumentsObj = { _raw: tool.parameters };
1302
- }
1303
1316
  messages.push({
1304
1317
  content: "",
1305
1318
  role: "assistant",
1306
1319
  tool_calls: [{ function: {
1307
1320
  name: tool.name,
1308
- arguments: argumentsObj
1321
+ arguments: toOllamaToolArguments(tool.parameters)
1309
1322
  } }]
1310
1323
  });
1311
1324
  messages.push({
@@ -1357,7 +1370,13 @@ var OllamaBackend = class OllamaBackend {
1357
1370
  mapped.content = texts.join("\n");
1358
1371
  if (images.length > 0) mapped.images = images;
1359
1372
  } else mapped.content = msg.content != null ? String(msg.content) : "";
1360
- if (Array.isArray(raw.tool_calls)) mapped.tool_calls = raw.tool_calls;
1373
+ if (Array.isArray(raw.tool_calls)) mapped.tool_calls = raw.tool_calls.map((call) => ({
1374
+ ...call,
1375
+ function: {
1376
+ ...call.function,
1377
+ arguments: toOllamaToolArguments(call.function?.arguments)
1378
+ }
1379
+ }));
1361
1380
  if (typeof raw.tool_name === "string") mapped.tool_name = raw.tool_name;
1362
1381
  return mapped;
1363
1382
  });
@@ -1379,7 +1398,7 @@ ChatModels.O1_PREVIEW, ChatModels.O1_MINI, ChatModels.O1, ChatModels.O3_MINI, Ch
1379
1398
  ChatModels.GPT5, ChatModels.GPT5_MINI, ChatModels.GPT5_NANO, ChatModels.GPT5_CHAT_LATEST;
1380
1399
  ChatModels.GPT5_1, ChatModels.GPT5_1_CHAT_LATEST;
1381
1400
  ChatModels.GPT5_2, ChatModels.GPT5_2_CHAT_LATEST;
1382
- ChatModels.GPT5_4, ChatModels.GPT5_4_MINI, ChatModels.GPT5_4_NANO;
1401
+ ChatModels.GPT5_4, ChatModels.GPT5_4_MINI, ChatModels.GPT5_4_MINI_2026_03_17, ChatModels.GPT5_4_NANO;
1383
1402
  ChatModels.GPT5_5;
1384
1403
  ChatModels.GPT5_6_SOL, ChatModels.GPT5_6_LUNA, ChatModels.GPT5_6_TERRA;
1385
1404
  ModelBackend.Bedrock, ModelBackend.AWS, ModelBackend.OpenAI, ModelBackend.Anthropic, ModelBackend.Gemini, ModelBackend.Ollama, ModelBackend.BFL, ModelBackend.XAI, ModelBackend.Kimi, ModelBackend.DeepSeek, ModelBackend.LocalImage, ModelBackend.VoyageAI;
@@ -1430,203 +1449,7 @@ var NotifyingLlmBackend = class {
1430
1449
  };
1431
1450
  //#endregion
1432
1451
  //#region src/tools/deferredToolRegistry.ts
1433
- /**
1434
- * Registry of tool schemas that are NOT loaded into the model's initial tool
1435
- * list. The model sees only the names (via the system prompt directory) and
1436
- * must call the `tool_search` meta-tool to load schemas on demand.
1437
- *
1438
- * This mirrors Claude Code's deferred-tool pattern. The win is large for
1439
- * heavy MCP integrations (e.g. 41 GitHub MCP tools at ~250-350 tokens of
1440
- * JSONSchema each = ~10-15k tokens per turn that's now ~1-1.5k of names).
1441
- */
1442
- var DeferredToolRegistry = class {
1443
- constructor() {
1444
- this.byName = /* @__PURE__ */ new Map();
1445
- this.directoryNames = [];
1446
- }
1447
- /** Replace registry contents with the supplied tools. Idempotent. */
1448
- register(tools) {
1449
- this.byName.clear();
1450
- for (const tool of tools) this.byName.set(tool.toolSchema.name, tool);
1451
- this.directoryNames = Object.freeze([...this.byName.keys()].sort());
1452
- logger.debug(`[DeferredToolRegistry] Registered ${tools.length} deferred tool(s)`);
1453
- }
1454
- clear() {
1455
- this.byName.clear();
1456
- this.directoryNames = Object.freeze([]);
1457
- }
1458
- size() {
1459
- return this.byName.size;
1460
- }
1461
- has(name) {
1462
- return this.byName.has(name);
1463
- }
1464
- get(name) {
1465
- return this.byName.get(name);
1466
- }
1467
- getAll() {
1468
- return Array.from(this.byName.values());
1469
- }
1470
- /** Return tools whose names appear in the supplied list, in input order. */
1471
- getByNames(names) {
1472
- const found = [];
1473
- for (const name of names) {
1474
- const tool = this.byName.get(name);
1475
- if (tool) found.push(tool);
1476
- }
1477
- return found;
1478
- }
1479
- /**
1480
- * Rank-search deferred tools by query terms. Name matches outrank
1481
- * description matches; exact substring on name wins ties.
1482
- */
1483
- searchByKeywords(query, maxResults) {
1484
- const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
1485
- if (terms.length === 0) return [];
1486
- const scored = [];
1487
- for (const tool of this.byName.values()) {
1488
- const name = tool.toolSchema.name.toLowerCase();
1489
- const desc = (tool.toolSchema.description || "").toLowerCase();
1490
- let score = 0;
1491
- for (const term of terms) {
1492
- if (name.includes(term)) score += 10;
1493
- if (desc.includes(term)) score += 1;
1494
- }
1495
- if (score > 0) scored.push({
1496
- tool,
1497
- score
1498
- });
1499
- }
1500
- scored.sort((a, b) => b.score - a.score);
1501
- return scored.slice(0, maxResults).map((s) => s.tool);
1502
- }
1503
- /**
1504
- * Directory entries rendered into the cache-stamped system-prompt reminder.
1505
- * Returns the frozen snapshot captured at `register()`, NOT live `byName`
1506
- * keys, so loading a tool mid-session can never change a byte of the cached
1507
- * system block (issue #213). A loaded tool remaining listed here is
1508
- * harmless: re-selecting it via `tool_search` is an idempotent no-op.
1509
- */
1510
- getDirectoryNames() {
1511
- return [...this.directoryNames];
1512
- }
1513
- };
1514
- const deferredToolRegistry = new DeferredToolRegistry();
1515
- //#endregion
1516
- //#region src/tools/toolSearchTool.ts
1517
- /**
1518
- * Default number of tools returned for a keyword search. Matches Claude
1519
- * Code's ToolSearch convention. 5 keeps the response payload small while
1520
- * surfacing enough alternatives for the model to refine its query.
1521
- */
1522
- const DEFAULT_MAX_RESULTS = 5;
1523
- /**
1524
- * Runtime validation for tool_search params. The LLM produces these
1525
- * values, so we validate at this boundary rather than trusting the
1526
- * shape. Coerces `max_results` from string->number for models that emit
1527
- * numeric-looking strings.
1528
- */
1529
- const ToolSearchParamsSchema = z$1.object({
1530
- query: z$1.string().min(1, "query must be a non-empty string"),
1531
- max_results: z$1.coerce.number().int().min(1).max(20).optional()
1532
- });
1533
- /**
1534
- * Parse the query string. Two forms:
1535
- * - `select:name1,name2,...` - exact-name selection
1536
- * - free text - keyword search across name + description
1537
- */
1538
- function parseQuery(query) {
1539
- const trimmed = query.trim();
1540
- const selectMatch = trimmed.match(/^select:(.+)$/i);
1541
- if (selectMatch) return {
1542
- mode: "select",
1543
- names: selectMatch[1].split(",").map((n) => n.trim()).filter((n) => n.length > 0)
1544
- };
1545
- return {
1546
- mode: "search",
1547
- text: trimmed
1548
- };
1549
- }
1550
- /**
1551
- * Format the loaded-tools response. Mirrors Claude Code's convention:
1552
- * one <function>{...}</function> line per matched tool. The model has
1553
- * already seen this format in its tool-registration system messages, so
1554
- * it parses without additional explanation.
1555
- *
1556
- * Note: the schemas are *also* injected into context.tools by the caller,
1557
- * so on the next iteration the model gets them as native tool definitions.
1558
- * The text response here is for in-turn awareness and audit trail.
1559
- */
1560
- function renderToolsBlock(tools) {
1561
- if (tools.length === 0) return "";
1562
- return `<functions>\n${tools.map((tool) => {
1563
- const schema = {
1564
- description: tool.toolSchema.description,
1565
- name: tool.toolSchema.name,
1566
- parameters: tool.toolSchema.parameters
1567
- };
1568
- return `<function>${JSON.stringify(schema)}</function>`;
1569
- }).join("\n")}\n</functions>`;
1570
- }
1571
- /**
1572
- * Build the tool_search meta-tool. The returned tool has a closure over
1573
- * the supplied `toolListAccessor`, which it uses to push newly-resolved
1574
- * tool schemas into the live agent context.
1575
- *
1576
- * Idempotent: re-loading a tool that's already in the context is a no-op.
1577
- */
1578
- function createToolSearchTool(toolListAccessor) {
1579
- return {
1580
- toolSchema: {
1581
- name: "tool_search",
1582
- description: "Fetches full schema definitions for deferred tools so they can be called. Deferred tools appear by name only in a system reminder; their parameter schemas are NOT loaded by default. Use this tool to load schemas on demand. Query forms: 'select:name1,name2' for exact selection, or free-text keywords to search by name and description. Once a tool's schema is returned, it becomes callable in subsequent turns.",
1583
- parameters: {
1584
- type: "object",
1585
- properties: {
1586
- query: {
1587
- type: "string",
1588
- description: "Either 'select:<comma-separated names>' to fetch specific tools, or free-text keywords (e.g. 'github pull request') to rank-search deferred tools."
1589
- },
1590
- max_results: {
1591
- type: "number",
1592
- description: `Maximum number of tools to return for keyword search. Defaults to ${DEFAULT_MAX_RESULTS}. Ignored for 'select:' queries.`
1593
- }
1594
- },
1595
- required: ["query"]
1596
- }
1597
- },
1598
- toolFn: async (params) => {
1599
- const parsedParams = ToolSearchParamsSchema.safeParse(params ?? {});
1600
- if (!parsedParams.success) {
1601
- const issue = parsedParams.error.issues[0];
1602
- return `tool_search: invalid parameters — ${issue.path.join(".") || "params"}: ${issue.message}`;
1603
- }
1604
- const { query, max_results } = parsedParams.data;
1605
- const parsed = parseQuery(query);
1606
- let matched;
1607
- let unmatched = [];
1608
- if (parsed.mode === "select") {
1609
- matched = deferredToolRegistry.getByNames(parsed.names);
1610
- const foundNames = new Set(matched.map((t) => t.toolSchema.name));
1611
- unmatched = parsed.names.filter((n) => !foundNames.has(n));
1612
- } else {
1613
- const max = max_results ?? DEFAULT_MAX_RESULTS;
1614
- matched = deferredToolRegistry.searchByKeywords(parsed.text, max);
1615
- }
1616
- if (matched.length === 0) return parsed.mode === "select" ? `tool_search: no deferred tools matched ${parsed.names.join(", ")}. Use a free-text query to search.` : `tool_search: no deferred tools matched query "${parsed.text}".`;
1617
- const liveTools = toolListAccessor();
1618
- const liveNames = new Set(liveTools.map((t) => t.toolSchema.name));
1619
- let added = 0;
1620
- for (const tool of matched) if (!liveNames.has(tool.toolSchema.name)) {
1621
- liveTools.push(tool);
1622
- added++;
1623
- }
1624
- logger.debug(`[tool_search] query="${query}" matched=${matched.length} added=${added} alreadyLoaded=${matched.length - added}`);
1625
- const block = renderToolsBlock(matched);
1626
- return `${`Loaded ${added} new tool schema(s)${added < matched.length ? ` (${matched.length - added} already loaded)` : ""}. These are now callable in your next message.${unmatched.length > 0 ? `\n\nNot found: ${unmatched.join(", ")}` : ""}`}\n\n${block}`;
1627
- }
1628
- };
1629
- }
1452
+ const deferredToolRegistry = new DeferredToolRegistry({ logger });
1630
1453
  //#endregion
1631
1454
  //#region src/llm/MultiLlmBackend.ts
1632
1455
  /**
@@ -2166,4 +1989,4 @@ function buildAgent(input) {
2166
1989
  };
2167
1990
  }
2168
1991
  //#endregion
2169
- export { deferredToolRegistry as a, createToolSearchTool as i, buildSupportingStores as n, NotifyingLlmBackend as o, buildLlmBackend as r, buildAgent as t };
1992
+ export { NotifyingLlmBackend as a, deferredToolRegistry as i, buildSupportingStores as n, buildLlmBackend as r, buildAgent as t };
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { A as createAgentDelegateTool, C as createTodoStore, D as createCoordinateTaskTool, F as PermissionManager, I as buildCliToolList, O as createResumeAgentTool, Q as CustomCommandStore, S as createFindDefinitionTool, T as createSkillTool, Z as RemoteSkillSource, b as createGetFileStructureTool, j as FallbackLlmBackend, k as createBackgroundAgentTools, nt as SessionStore, q as setWebSocketToolExecutor, s as buildSandbox, tt as CheckpointStore, w as createWriteTodosTool } from "../AgentHistoryStore-O2gKB5YG.mjs";
3
- import { _ as logger, o as isReadOnlyTool, t as ConfigStore, u as requireApiUrl } from "../ConfigStore-RHqyXcSS.mjs";
4
- import { a as deferredToolRegistry, i as createToolSearchTool, n as buildSupportingStores, o as NotifyingLlmBackend, r as buildLlmBackend, t as buildAgent } from "../buildAgent-C0jmyDL4.mjs";
5
- import { a as ApiClient } from "../dist-B1bGf-yn.mjs";
2
+ import { $ as CustomCommandStore, A as createAgentDelegateTool, C as createTodoStore, D as createCoordinateTaskTool, G as setWebSocketToolExecutor, N as PermissionManager, O as createResumeAgentTool, P as buildCliToolList, Q as RemoteSkillSource, S as createFindDefinitionTool, T as createSkillTool, X as createToolSearchTool, b as createGetFileStructureTool, j as FallbackLlmBackend, k as createBackgroundAgentTools, nt as CheckpointStore, rt as SessionStore, s as buildSandbox, w as createWriteTodosTool } from "../AgentHistoryStore-CLidbOHG.mjs";
3
+ import { b as logger, i as ToolCatalog, p as requireApiUrl, t as ConfigStore } from "../ConfigStore-DUATzLxB.mjs";
4
+ import { a as NotifyingLlmBackend, i as deferredToolRegistry, n as buildSupportingStores, r as buildLlmBackend, t as buildAgent } from "../buildAgent-DyZWK0LW.mjs";
5
+ import { a as ApiClient } from "../dist-DPP7UmK9.mjs";
6
6
  import { randomUUID } from "crypto";
7
7
  import { existsSync, realpathSync, statSync } from "fs";
8
8
  import { isAbsolute } from "path";
@@ -92,18 +92,19 @@ function truncate(text, max) {
92
92
  const PERMISSION_OPTION_ALLOW_ONCE = "allow-once";
93
93
  const PERMISSION_OPTION_ALLOW_ALWAYS = "allow-always";
94
94
  const PERMISSION_OPTION_REJECT = "deny";
95
- function buildPermissionOptions() {
95
+ function buildPermissionOptions(options = {}) {
96
+ const { allowAlways = true } = options;
96
97
  return [
97
98
  {
98
99
  optionId: PERMISSION_OPTION_ALLOW_ONCE,
99
100
  name: "Allow once",
100
101
  kind: "allow_once"
101
102
  },
102
- {
103
+ ...allowAlways ? [{
103
104
  optionId: PERMISSION_OPTION_ALLOW_ALWAYS,
104
105
  name: "Always allow",
105
106
  kind: "allow_always"
106
- },
107
+ }] : [],
107
108
  {
108
109
  optionId: PERMISSION_OPTION_REJECT,
109
110
  name: "Reject",
@@ -271,12 +272,13 @@ var AcpServer = class {
271
272
  this.stackPromise = null;
272
273
  this.stack = null;
273
274
  this.activeTurn = null;
274
- this.promptFn = async (toolName, args, preview) => {
275
+ this.promptFn = async (toolName, args, preview, kind) => {
276
+ const isSandboxEscalation = kind === "sandbox-escalation";
275
277
  const turn = this.activeTurn;
276
278
  if (!turn) return { action: "deny" };
277
279
  const toolCall = {
278
280
  toolCallId: `perm-${randomUUID()}`,
279
- title: toolCallTitle(toolName, args),
281
+ title: `${isSandboxEscalation ? "Run outside sandbox: " : ""}${toolCallTitle(toolName, args)}`,
280
282
  kind: toolKind(toolName),
281
283
  status: "pending",
282
284
  rawInput: args,
@@ -294,13 +296,15 @@ var AcpServer = class {
294
296
  turn.signal.addEventListener("abort", onClose);
295
297
  this.connectionSignal.addEventListener("abort", onClose);
296
298
  try {
297
- return { action: await Promise.race([turn.client.request(methods.client.session.requestPermission, {
299
+ const action = await Promise.race([turn.client.request(methods.client.session.requestPermission, {
298
300
  sessionId: turn.sessionId,
299
301
  toolCall,
300
- options: buildPermissionOptions()
302
+ options: buildPermissionOptions({ allowAlways: !isSandboxEscalation })
301
303
  }, { cancellationSignal: decisionController.signal }).then((res) => permissionResponseFromOutcome(res?.outcome)).catch(() => "deny"), new Promise((resolve) => {
302
304
  decisionController.signal.addEventListener("abort", () => resolve("deny"), { once: true });
303
- })]) };
305
+ })]);
306
+ if (isSandboxEscalation && action === "allow-always") return { action: "deny" };
307
+ return { action };
304
308
  } finally {
305
309
  clearTimeout(timer);
306
310
  turn.signal.removeEventListener("abort", onClose);
@@ -424,7 +428,7 @@ var AcpServer = class {
424
428
  result = await stack.agent.run(userText, {
425
429
  signal: abortController.signal,
426
430
  previousMessages: session.history,
427
- isReadOnlyTool,
431
+ isReadOnlyTool: (toolName) => stack.permissionManager.catalog.isReadOnly(toolName),
428
432
  maxHistoryIterations: 4
429
433
  });
430
434
  } catch (error) {
@@ -594,7 +598,7 @@ var AcpServer = class {
594
598
  tokenGetter,
595
599
  startupLog
596
600
  });
597
- const permissionManager = new PermissionManager(config.trustedTools ?? [], void 0, config.tools.disabled ?? []);
601
+ const permissionManager = new PermissionManager(new ToolCatalog(), config.trustedTools ?? [], config.tools.disabled ?? []);
598
602
  const checkpointStore = new CheckpointStore(this.configStore.getProjectConfigDir() ?? process.cwd());
599
603
  const stackSessionId = randomUUID();
600
604
  const { sandboxOrchestrator } = await buildSandbox({
@@ -627,9 +631,13 @@ var AcpServer = class {
627
631
  const llmWithFallback = config.fallbackModels && config.fallbackModels.length > 0 ? new FallbackLlmBackend(llm, config.fallbackModels, (from, to, error) => logger.debug(`[acp] Model "${from}" failed (${error.message}); falling back to "${to}"`)) : llm;
628
632
  const notifyingLlm = new NotifyingLlmBackend(llmWithFallback, backgroundManager);
629
633
  const agentToolsRef = { current: null };
630
- const toolSearchTool = deferredToolRegistry.size() > 0 ? createToolSearchTool(() => {
631
- if (!agentToolsRef.current) throw new Error("tool_search invoked before agent context was wired");
632
- return agentToolsRef.current;
634
+ const toolSearchTool = deferredToolRegistry.size() > 0 ? createToolSearchTool({
635
+ registry: deferredToolRegistry,
636
+ logger,
637
+ toolListAccessor: () => {
638
+ if (!agentToolsRef.current) throw new Error("tool_search invoked before agent context was wired");
639
+ return agentToolsRef.current;
640
+ }
633
641
  }) : null;
634
642
  const cliTools = this.buildCliTools({
635
643
  config,
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { d as resolveApiEndpoint, g as parseApiUrl, t as ConfigStore } from "../ConfigStore-RHqyXcSS.mjs";
2
+ import { m as resolveApiEndpoint, t as ConfigStore, y as parseApiUrl } from "../ConfigStore-DUATzLxB.mjs";
3
3
  //#region src/commands/apiCommand.ts
4
4
  /**
5
5
  * External API config command (--api-url / --reset-api)
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "../package-DsG8Tgvs.mjs";
2
+ import { t as version } from "../package-DYPVrada.mjs";
3
3
  import { c as isNpmPrefixWritable, i as fetchLatestVersion, r as compareSemver } from "../updateChecker-CCLVVdH0.mjs";
4
4
  import { t as checkRipgrep } from "../ripgrepCheck-BmkyTK2i.mjs";
5
5
  import { existsSync } from "fs";
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as ConfigStore } from "../ConfigStore-RHqyXcSS.mjs";
2
+ import { t as ConfigStore } from "../ConfigStore-DUATzLxB.mjs";
3
3
  //#region src/commands/envCommand.ts
4
4
  /**
5
5
  * Environment switching for the `--dev` / `--prod` launch flags.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { A as createAgentDelegateTool, B as SHELL_LIKE_TOOL_COMMAND_FIELDS, C as createTodoStore, D as createCoordinateTaskTool, F as PermissionManager, I as buildCliToolList, K as classifyCommandRisk, L as generateCliTools, N as ReActAgent, O as createResumeAgentTool, Q as CustomCommandStore, R as wrapTools, S as createFindDefinitionTool, T as createSkillTool, Y as buildSystemPrompt, Z as RemoteSkillSource, a as loadProjectContext, b as createGetFileStructureTool, c as createSseBackend, i as buildProjectAgentStore, j as FallbackLlmBackend, k as createBackgroundAgentTools, n as BackgroundAgentManager, nt as SessionStore, o as McpManager, q as setWebSocketToolExecutor, r as SubagentOrchestrator, s as buildSandbox, t as AgentHistoryStore, tt as CheckpointStore, w as createWriteTodosTool, x as createWorkItemTools } from "../AgentHistoryStore-O2gKB5YG.mjs";
3
- import { _ as logger, o as isReadOnlyTool, t as ConfigStore, u as requireApiUrl } from "../ConfigStore-RHqyXcSS.mjs";
4
- import { r as reconstructTurnBlocks, t as WorkItemsClient } from "../WorkItemsClient-cmw7WYBy.mjs";
5
- import { a as ApiClient } from "../dist-B1bGf-yn.mjs";
2
+ import { $ as CustomCommandStore, A as createAgentDelegateTool, C as createTodoStore, D as createCoordinateTaskTool, F as generateCliTools, G as setWebSocketToolExecutor, K as classifyCommandRisk, N as PermissionManager, O as createResumeAgentTool, P as buildCliToolList, Q as RemoteSkillSource, S as createFindDefinitionTool, T as createSkillTool, U as wrapTools, V as buildSystemPrompt, Y as ReActAgent, a as loadProjectContext, b as createGetFileStructureTool, c as createSseBackend, i as buildProjectAgentStore, j as FallbackLlmBackend, k as createBackgroundAgentTools, n as BackgroundAgentManager, nt as CheckpointStore, o as McpManager, r as SubagentOrchestrator, rt as SessionStore, s as buildSandbox, t as AgentHistoryStore, w as createWriteTodosTool, x as createWorkItemTools } from "../AgentHistoryStore-CLidbOHG.mjs";
3
+ import { a as commandTextOf, b as logger, c as toolCategory, i as ToolCatalog, p as requireApiUrl, t as ConfigStore } from "../ConfigStore-DUATzLxB.mjs";
4
+ import { r as reconstructTurnBlocks, t as WorkItemsClient } from "../WorkItemsClient-BSoOblGp.mjs";
5
+ import { a as ApiClient } from "../dist-DPP7UmK9.mjs";
6
6
  import { randomBytes } from "crypto";
7
7
  import { v4 } from "uuid";
8
8
  import { readFile } from "fs/promises";
@@ -24,7 +24,7 @@ import { readFile } from "fs/promises";
24
24
  * field or a new event type (backward-compatible). Consumers should reject a
25
25
  * MAJOR they do not understand.
26
26
  */
27
- const HEADLESS_SCHEMA_VERSION = "1.0.0";
27
+ const HEADLESS_SCHEMA_VERSION = "1.1.0";
28
28
  /** Serialize an event with the protocol envelope stamped on. Exposed for tests. */
29
29
  function stampEvent(runId, event) {
30
30
  return {
@@ -87,15 +87,15 @@ function parseStringArray(raw, inputType) {
87
87
  }
88
88
  /**
89
89
  * Classify the risk of a single tool invocation for the permission protocol.
90
- * Shell-like tools are classified from their actual command text (reusing the
91
- * shared command-risk tokenizer); every other tool falls back to a level derived
92
- * from its permission category (auto_approve -> low, prompt_always -> high, else
93
- * medium). Never throws: a classifier failure fails closed at `high`.
90
+ * Shell-like tools (a descriptor commandArg) are classified from their actual
91
+ * command text, reusing the shared command-risk tokenizer, exactly as the
92
+ * gateway's command-risk stage does; every other tool falls back to a level
93
+ * derived from its permission category (auto_approve -> low, prompt_always ->
94
+ * high, else medium). Never throws: a classifier failure fails closed at `high`.
94
95
  */
95
- function classifyToolRisk(toolName, args, category) {
96
- const field = SHELL_LIKE_TOOL_COMMAND_FIELDS[toolName];
97
- const command = field && args !== null && typeof args === "object" ? args[field] : void 0;
98
- if (typeof command === "string") try {
96
+ function classifyToolRisk(descriptor, args) {
97
+ const command = commandTextOf(descriptor, args);
98
+ if (command !== void 0) try {
99
99
  return classifyCommandRisk(command);
100
100
  } catch {
101
101
  return {
@@ -103,6 +103,7 @@ function classifyToolRisk(toolName, args, category) {
103
103
  reasons: ["command risk analysis failed (fail closed)"]
104
104
  };
105
105
  }
106
+ const category = toolCategory(descriptor);
106
107
  return {
107
108
  level: category === "auto_approve" ? "low" : category === "prompt_always" ? "high" : "medium",
108
109
  reasons: []
@@ -171,9 +172,21 @@ function evaluatePermissionPolicy(policy, toolName, riskLevel) {
171
172
  reason: `policy default (${policy.defaultAction})`
172
173
  };
173
174
  }
175
+ /** The permission_request event for a gated tool call; `kind` tells a consumer what is being asked. */
176
+ function buildPermissionRequestEvent(toolName, risk, kind = "tool") {
177
+ return {
178
+ type: "permission_request",
179
+ toolName,
180
+ risk,
181
+ kind
182
+ };
183
+ }
174
184
  /**
175
185
  * Resolve a headless permission decision from the fixed precedence ladder:
176
186
  * --dangerously-skip-permissions -> allow-once (explicit blanket override)
187
+ * a sandbox-escalation request -> deny (running outside the OS sandbox is its own
188
+ * decision; no policy allow-list or risk threshold
189
+ * approves it)
177
190
  * a directory-grant request -> deny (widening the filesystem allow-list is
178
191
  * its own decision; headless has no human to
179
192
  * grant it, so fail closed - never inherit the
@@ -183,11 +196,15 @@ function evaluatePermissionPolicy(policy, toolName, riskLevel) {
183
196
  * Extracted from the headless prompt closure so the ladder is unit-testable.
184
197
  */
185
198
  function resolveHeadlessPermissionDecision(params) {
186
- const { dangerouslySkipPermissions, isDirectoryGrant, toolName, riskLevel, permissionPolicy } = params;
199
+ const { dangerouslySkipPermissions, isDirectoryGrant, isSandboxEscalation, toolName, riskLevel, permissionPolicy } = params;
187
200
  if (dangerouslySkipPermissions) return {
188
201
  action: "allow-once",
189
202
  reason: "dangerously-skip-permissions"
190
203
  };
204
+ if (isSandboxEscalation) return {
205
+ action: "deny",
206
+ reason: "running outside the sandbox not approved in headless mode"
207
+ };
191
208
  if (isDirectoryGrant) return {
192
209
  action: "deny",
193
210
  reason: "directory access not granted in headless mode"
@@ -205,6 +222,39 @@ function resolveHeadlessPermissionDecision(params) {
205
222
  };
206
223
  }
207
224
  //#endregion
225
+ //#region src/commands/headlessPermissionPrompt.ts
226
+ /**
227
+ * The headless permission prompt. Every gated tool surfaces a structured permission_request +
228
+ * permission_decision pair on the stream (so decisions are never silent), then resolves the
229
+ * decision from resolveHeadlessPermissionDecision's precedence ladder.
230
+ */
231
+ function createHeadlessPromptFn(deps) {
232
+ const { catalog, dangerouslySkipPermissions, permissionPolicy, emit, warn } = deps;
233
+ return (toolName, args, _preview, kind) => {
234
+ const risk = classifyToolRisk(catalog.resolve(toolName), args);
235
+ emit?.(buildPermissionRequestEvent(toolName, risk, kind));
236
+ const decision = resolveHeadlessPermissionDecision({
237
+ dangerouslySkipPermissions,
238
+ isDirectoryGrant: kind === "directory-grant",
239
+ isSandboxEscalation: kind === "sandbox-escalation",
240
+ toolName,
241
+ riskLevel: risk.level,
242
+ permissionPolicy
243
+ });
244
+ if (emit) emit({
245
+ type: "permission_decision",
246
+ toolName,
247
+ action: decision.action,
248
+ reason: decision.reason,
249
+ risk: risk.level,
250
+ kind: kind ?? "tool"
251
+ });
252
+ else if (decision.action === "deny") warn(kind === "sandbox-escalation" ? `Warning: Tool "${toolName}" was denied by the OS sandbox, and retrying it outside the sandbox needs approval that headless mode cannot grant. Use --dangerously-skip-permissions to allow it.\n` : `Warning: Tool "${toolName}" requires permission and was denied (${decision.reason}). Grant it via --permission-policy, or --dangerously-skip-permissions to allow all tools.\n`);
253
+ if (decision.action === "allow-once") logger.debug(`[headless] Auto-allowing tool: ${toolName}`);
254
+ return Promise.resolve({ action: decision.action });
255
+ };
256
+ }
257
+ //#endregion
208
258
  //#region src/commands/headlessCommand.ts
209
259
  /**
210
260
  * Headless/programmatic mode command (b4m -p "query")
@@ -319,34 +369,16 @@ async function handleHeadlessCommand(options) {
319
369
  }
320
370
  };
321
371
  await logger.initialize(session.id);
322
- const permissionManager = new PermissionManager(config.trustedTools ?? [], void 0, config.tools.disabled);
372
+ const permissionManager = new PermissionManager(new ToolCatalog(), config.trustedTools ?? [], config.tools.disabled);
323
373
  const emit = createHeadlessEmitter(runId, (line) => process.stdout.write(line));
324
374
  const streaming = outputFormat === "stream-json";
325
- const promptFn = (toolName, args, _preview, kind) => {
326
- const risk = classifyToolRisk(toolName, args, permissionManager.getCategory(toolName));
327
- if (streaming) emit({
328
- type: "permission_request",
329
- toolName,
330
- risk
331
- });
332
- const decision = resolveHeadlessPermissionDecision({
333
- dangerouslySkipPermissions,
334
- isDirectoryGrant: kind === "directory-grant",
335
- toolName,
336
- riskLevel: risk.level,
337
- permissionPolicy
338
- });
339
- if (streaming) emit({
340
- type: "permission_decision",
341
- toolName,
342
- action: decision.action,
343
- reason: decision.reason,
344
- risk: risk.level
345
- });
346
- 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`);
347
- if (decision.action === "allow-once") logger.debug(`[headless] Auto-allowing tool: ${toolName}`);
348
- return Promise.resolve({ action: decision.action });
349
- };
375
+ const promptFn = createHeadlessPromptFn({
376
+ catalog: permissionManager.catalog,
377
+ dangerouslySkipPermissions,
378
+ permissionPolicy,
379
+ emit: streaming ? emit : null,
380
+ warn: (message) => process.stderr.write(message)
381
+ });
350
382
  const userQuestionFn = (_payload) => {
351
383
  process.stderr.write("Warning: Agent requested user input; headless mode cannot respond interactively. Answering with empty response.\n");
352
384
  return Promise.resolve({ answers: [] });
@@ -482,7 +514,7 @@ async function handleHeadlessCommand(options) {
482
514
  try {
483
515
  result = await agent.run(fullPrompt, {
484
516
  parallelExecution: config.preferences.enableParallelToolExecution === true,
485
- isReadOnlyTool,
517
+ isReadOnlyTool: (toolName) => permissionManager.catalog.isReadOnly(toolName),
486
518
  maxHistoryIterations: 4
487
519
  });
488
520
  } finally {