dsh-packycode-api 1.1.3 → 1.1.5

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/lib/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
2
2
  import { homedir, tmpdir } from "node:os";
3
- import { isAbsolute, join, posix, sep, win32 } from "node:path";
3
+ import { dirname, isAbsolute, join, posix, resolve, sep, win32 } from "node:path";
4
4
  import z from "@deepseek-ai/schemastery";
5
5
  import { HarnessError, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
6
6
  import { spawn } from "node:child_process";
7
- import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
7
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
8
8
  import { mkdtemp, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
9
9
  import { createServer } from "node:http";
10
10
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -544,7 +544,8 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now
544
544
  * model does not support is refused before the request is sent: Claude Opus
545
545
  * 5.5 (platform.claude.com effort docs), GPT-6.1 Sol and GPT-6 Astra
546
546
  * `reasoning.effort` (developers.openai.com model pages), DeepSeek thinking
547
- * mode (`thinking` toggle plus `low|high|max` effort).
547
+ * mode (`thinking` toggle plus `low|high|max` effort), and Grok 4.7
548
+ * (docs.x.ai/developers/grok-4-7).
548
549
  *
549
550
  * @module dsh-packycode-api/catalog
550
551
  */
@@ -586,6 +587,11 @@ const ALWAYS_THINKING_LEVELS = Object.freeze({
586
587
  xhigh: "xhigh",
587
588
  max: "max"
588
589
  });
590
+ /** Grok 4.7 supports low | medium | high | xhigh, with no off or max. */
591
+ const GROK_LEVELS = Object.freeze({
592
+ ...ALWAYS_THINKING_LEVELS,
593
+ max: null
594
+ });
589
595
  const PACKYCODE_MODELS = Object.freeze([
590
596
  Object.freeze({
591
597
  id: "deepseek-flash",
@@ -632,6 +638,20 @@ const PACKYCODE_MODELS = Object.freeze([
632
638
  maxTokens: 128e3,
633
639
  thinkingLevelMap: ALWAYS_THINKING_LEVELS
634
640
  }),
641
+ Object.freeze({
642
+ id: "grok-4.7",
643
+ name: "Grok 4.7",
644
+ api: "openai-responses",
645
+ provider: PROVIDER,
646
+ baseUrl: RESPONSES_BASE,
647
+ reasoning: true,
648
+ input: Object.freeze(["text", "image"]),
649
+ cost: NO_COST,
650
+ contextWindow: 5e5,
651
+ maxTokens: 5e5,
652
+ thinkingLevelMap: GROK_LEVELS,
653
+ compat: Object.freeze({ supportsLongCacheRetention: false })
654
+ }),
635
655
  Object.freeze({
636
656
  id: "claude-opus-5-5",
637
657
  name: "Claude Opus 5.5",
@@ -1275,12 +1295,104 @@ function hostPath(lookup) {
1275
1295
  return;
1276
1296
  }
1277
1297
  }
1298
+ /** The only PTC program this adapter emits; its JSON literal also works in Python. */
1299
+ const PLAN_REVIEW_PROGRAM_PREFIX = "return await tools.exit_plan_mode(";
1300
+ /** Parse durable arguments without evaluating model-authored code. */
1301
+ function parsedArguments(value) {
1302
+ try {
1303
+ return typeof value === "string" ? JSON.parse(value) : value;
1304
+ } catch {
1305
+ return;
1306
+ }
1307
+ }
1308
+ /**
1309
+ * Recover only the public plan, never arbitrary tool arguments or PTC code.
1310
+ * The PTC allowlist is deliberately the exact, single-call program we emit.
1311
+ */
1312
+ function planOfCall(block) {
1313
+ if (block?.type !== "tool-call") return void 0;
1314
+ const args = parsedArguments(block.arguments);
1315
+ let plan;
1316
+ if (block.name === "exit_plan_mode") plan = args?.plan;
1317
+ else if (block.name === "run_code" && typeof args?.code === "string") {
1318
+ const code = args.code;
1319
+ if (!code.startsWith(PLAN_REVIEW_PROGRAM_PREFIX) || !code.endsWith(")")) return void 0;
1320
+ const review = parsedArguments(code.slice(34, -1));
1321
+ if (review === null || typeof review !== "object" || Object.keys(review).length !== 1) return void 0;
1322
+ plan = review.plan;
1323
+ }
1324
+ return typeof plan === "string" && plan.trim() !== "" ? plan : void 0;
1325
+ }
1326
+ /** Keep the latest complete plan and its review from the persisted model transcript. */
1327
+ function planReviewContext(messages) {
1328
+ let latest;
1329
+ for (const message of messages) {
1330
+ if (message?.role !== "assistant") continue;
1331
+ for (const block of Array.isArray(message.content) ? message.content : []) {
1332
+ const plan = planOfCall(block);
1333
+ if (plan !== void 0) latest = {
1334
+ plan,
1335
+ id: block.id
1336
+ };
1337
+ }
1338
+ }
1339
+ if (latest === void 0) return void 0;
1340
+ let review;
1341
+ for (const message of messages) {
1342
+ if (message?.role !== "user") continue;
1343
+ for (const block of Array.isArray(message.content) ? message.content : []) {
1344
+ if (block?.type !== "tool-result" || latest.id === void 0 || block.toolCallId !== latest.id) continue;
1345
+ const text = typeof block.content === "string" ? block.content : (Array.isArray(block.content) ? block.content : []).filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
1346
+ review = `[计划审阅${block.isError === true ? "未批准/失败" : "结果"}]\n${text}`;
1347
+ }
1348
+ }
1349
+ return {
1350
+ id: latest.id,
1351
+ turn: {
1352
+ label: "计划审阅上下文",
1353
+ parts: [{
1354
+ kind: "text",
1355
+ text: `[最近提交的完整计划;提交本身不代表已批准]\n${latest.plan}\n\n${review ?? "[尚无计划审阅结果;不要把提交视为实施许可]"}`
1356
+ }]
1357
+ }
1358
+ };
1359
+ }
1360
+ /**
1361
+ * Select the review transport from this request's actual direct-tool catalog.
1362
+ * `run_code` binds `tools` in both supported host language flavors.
1363
+ */
1364
+ function planReviewCall(options, plan) {
1365
+ if (plan.trim() === "") throw new Error("Claude Code submitted an empty plan; no review or implementation was authorized.");
1366
+ const context = planReviewContext([{
1367
+ role: "assistant",
1368
+ content: [{
1369
+ type: "tool-call",
1370
+ name: "exit_plan_mode",
1371
+ arguments: { plan }
1372
+ }]
1373
+ }]).turn;
1374
+ if (textLength(context.parts) + context.label.length + 3 >= MAX_PROMPT_CHARS$1) throw new Error("The submitted plan exceeds the CLI delegation budget (48,000 characters). Shorten or split it before review; no plan constraints were truncated and no implementation was authorized.");
1375
+ const tools = Array.isArray(options?.tools) ? options.tools : [];
1376
+ if (tools.some((tool) => tool?.name === "exit_plan_mode")) return {
1377
+ name: "exit_plan_mode",
1378
+ args: JSON.stringify({ plan })
1379
+ };
1380
+ if (tools.some((tool) => tool?.name === "run_code")) return {
1381
+ name: "run_code",
1382
+ args: JSON.stringify({
1383
+ code: `${PLAN_REVIEW_PROGRAM_PREFIX}${JSON.stringify({ plan })})`,
1384
+ description: "Present completed plan for user review"
1385
+ })
1386
+ };
1387
+ throw new Error("Claude Code submitted a plan, but this request has neither exit_plan_mode nor run_code available for host review; no implementation was authorized.");
1388
+ }
1278
1389
  /**
1279
1390
  * Flatten one message's content into ordered parts: text, image, and file.
1280
1391
  * Tool calls and results become short text so a turn that started on another
1281
- * model still reads coherently; reasoning is not carried.
1392
+ * model still reads coherently; reasoning is not carried. The latest review
1393
+ * result is carried in full separately, not through the general 8k tool cap.
1282
1394
  */
1283
- function collectParts(blocks, parts) {
1395
+ function collectParts(blocks, parts, reviewId) {
1284
1396
  for (const block of Array.isArray(blocks) ? blocks : []) {
1285
1397
  if (block === null || typeof block !== "object") continue;
1286
1398
  switch (block.type) {
@@ -1314,6 +1426,7 @@ function collectParts(blocks, parts) {
1314
1426
  });
1315
1427
  break;
1316
1428
  case "tool-result": {
1429
+ if (reviewId !== void 0 && block.toolCallId === reviewId) break;
1317
1430
  if (typeof block.content === "string") {
1318
1431
  parts.push({
1319
1432
  kind: "text",
@@ -1355,10 +1468,12 @@ const textLength = (parts) => parts.reduce((sum, part) => sum + (part.kind === "
1355
1468
  */
1356
1469
  function delegatedTurns(options) {
1357
1470
  const messages = Array.isArray(options?.messages) ? options.messages : [];
1471
+ const review = planReviewContext(messages);
1472
+ const reserved = review === void 0 ? 0 : textLength(review.turn.parts) + review.turn.label.length + 3;
1358
1473
  const turns = [];
1359
1474
  for (const message of messages) {
1360
1475
  if (message === null || typeof message !== "object" || message.role === "system") continue;
1361
- const parts = collectParts(message.content, []);
1476
+ const parts = collectParts(message.content, [], review?.id);
1362
1477
  if (parts.length === 0 || textLength(parts) === 0 && !parts.some((part) => part.kind !== "text")) continue;
1363
1478
  const label = message.role === "user" ? "用户" : message.role === "assistant" ? "助手" : String(message.role);
1364
1479
  turns.push({
@@ -1366,8 +1481,11 @@ function delegatedTurns(options) {
1366
1481
  parts
1367
1482
  });
1368
1483
  }
1484
+ const newest = turns.at(-1);
1485
+ const newestSize = newest === void 0 ? 0 : textLength(newest.parts) + newest.label.length + 3;
1486
+ if (review !== void 0 && reserved + newestSize > MAX_PROMPT_CHARS$1) throw new Error("The complete plan, its review, and the latest request exceed the CLI delegation budget (48,000 characters). Shorten or split the plan/request before continuing; plan constraints were not truncated.");
1369
1487
  const kept = [];
1370
- let used = 0;
1488
+ let used = reserved;
1371
1489
  for (let index = turns.length - 1; index >= 0; index -= 1) {
1372
1490
  const turn = turns[index];
1373
1491
  const size = textLength(turn.parts) + turn.label.length + 3;
@@ -1383,9 +1501,11 @@ function delegatedTurns(options) {
1383
1501
  });
1384
1502
  break;
1385
1503
  }
1504
+ const truncated = kept.length < turns.length || kept[0]?.clipped === true;
1505
+ if (review !== void 0) kept.unshift(review.turn);
1386
1506
  return {
1387
1507
  turns: kept,
1388
- truncated: kept.length < turns.length || kept[0]?.clipped === true
1508
+ truncated
1389
1509
  };
1390
1510
  }
1391
1511
  /** Keep the last `budget` characters of a turn's text, and every part after them. */
@@ -1898,7 +2018,8 @@ function createClaudeCliAdapter({ inner, runner, modelIds = CLI_BACKED_MODEL_IDS
1898
2018
  default: break;
1899
2019
  }
1900
2020
  if (submittedPlan !== void 0) {
1901
- yield* blocks.toolCall(`plan-${randomUUID()}`, "exit_plan_mode", JSON.stringify({ plan: submittedPlan }));
2021
+ const review = planReviewCall(options, submittedPlan);
2022
+ yield* blocks.toolCall(`plan-${randomUUID()}`, review.name, review.args);
1902
2023
  yield {
1903
2024
  type: "usage",
1904
2025
  usage: turnUsage(lastRequest, finished)
@@ -2304,10 +2425,11 @@ function resolveSpawnCommand(binary, { platform = process.platform, exists = exi
2304
2425
  * consumes the streaming protocol, and instructions appended to the CLI's
2305
2426
  * own system prompt ({@link DEFAULT_APPEND_SYSTEM_PROMPT} when blank). The
2306
2427
  * CLI's system prompt and tool set are never replaced: they are part of
2307
- * what the official client sends.
2428
+ * what the official client sends. Plan runs supply an absolute private
2429
+ * planDirectory below the child's working directory.
2308
2430
  * @returns argv after the binary name.
2309
2431
  */
2310
- function buildClaudeArgs({ mode = DEFAULT_CLAUDE_MODE, model, stream = false, appendSystemPrompt, searchBridge, disableNativeSearch = false } = {}) {
2432
+ function buildClaudeArgs({ mode = DEFAULT_CLAUDE_MODE, model, stream = false, appendSystemPrompt, searchBridge, disableNativeSearch = false, planDirectory } = {}) {
2311
2433
  const spec = CLAUDE_CLI_MODES[mode];
2312
2434
  if (spec === void 0) throw new Error(`claude_code mode must be one of ${Object.keys(CLAUDE_CLI_MODES).join(", ")}`);
2313
2435
  const args = stream ? [
@@ -2334,16 +2456,31 @@ function buildClaudeArgs({ mode = DEFAULT_CLAUDE_MODE, model, stream = false, ap
2334
2456
  args.push("--permission-mode", spec.permissionMode);
2335
2457
  if (searchBridge !== void 0) args.push("--allowedTools", searchBridge.toolName);
2336
2458
  } else args.push("--allowedTools", ...spec.allowedTools, ...searchBridge === void 0 ? [] : [searchBridge.toolName]);
2459
+ if (mode === "plan") {
2460
+ args.push("--permission-prompt-tool", "stdio");
2461
+ if (planDirectory !== void 0 && (typeof planDirectory !== "string" || !isAbsolute(planDirectory))) throw new Error("claude_code plan directory must be an absolute path");
2462
+ args.push("--settings", JSON.stringify({
2463
+ ...planDirectory === void 0 ? {} : { plansDirectory: planDirectory },
2464
+ permissions: { ask: ["ExitPlanMode"] }
2465
+ }));
2466
+ }
2337
2467
  if (disableNativeSearch || searchBridge !== void 0) args.push("--disallowedTools", "WebSearch");
2338
2468
  if (searchBridge !== void 0) args.push("--mcp-config", searchBridge.configPath);
2339
2469
  return args;
2340
2470
  }
2341
- /** How many permission prompts one plan-mode run will refuse before stopping. */
2471
+ /** How many permission denials one plan-mode run may recover from. */
2342
2472
  const MAX_PLAN_DENIALS = 8;
2343
- /** A plan file larger than this is not read into the turn. */
2473
+ /** A plan file or submitted plan larger than this is not returned into the turn. */
2344
2474
  const PLAN_FILE_MAX_BYTES = 2e5;
2345
- const PLAN_FILE_CLOCK_SKEW_MS = 2e3;
2475
+ const PLAN_DIRECTORY_PREFIX = ".dsh-packycode-plan-";
2476
+ const PLAN_FS = Object.freeze({
2477
+ mkdtempSync,
2478
+ realpathSync,
2479
+ lstatSync,
2480
+ rmSync
2481
+ });
2346
2482
  const PLAN_EXIT_DENY = "Write the plan to the plan file first, starting with a # heading, then call ExitPlanMode again.";
2483
+ const PLAN_TOO_LARGE_DENY = "The plan exceeds the 200000-byte review limit. Write a complete, shorter plan without dropping constraints, then call ExitPlanMode again.";
2347
2484
  const PLAN_ACTION_DENY = "Plan mode is active. Do not edit files or run commands that change the system. Continue read-only exploration, write the plan file, and call ExitPlanMode.";
2348
2485
  const PLAN_DIALOG_ERROR = "Plan mode cannot ask the user from the CLI. Decide and continue, or call ExitPlanMode.";
2349
2486
  const PLAN_DENY_LIMIT_MESSAGE = "claude_code plan mode stopped after too many denied permission prompts";
@@ -2357,11 +2494,58 @@ function planMarkdown(value) {
2357
2494
  if (typeof value !== "string") return void 0;
2358
2495
  const trimmed = value.trim();
2359
2496
  if (trimmed === "") return void 0;
2360
- const bounded = trimmed.length > PLAN_FILE_MAX_BYTES ? trimmed.slice(0, PLAN_FILE_MAX_BYTES) : trimmed;
2361
- return /^#\s+\S/u.test(bounded) ? bounded : `# Plan\n\n${bounded}`;
2497
+ const markdown = /^#\s+\S/u.test(trimmed) ? trimmed : `# Plan\n\n${trimmed}`;
2498
+ return Buffer.byteLength(markdown) > PLAN_FILE_MAX_BYTES ? void 0 : markdown;
2499
+ }
2500
+ /** Own exactly one newly created, owner-only directory, never a shared CLI home. */
2501
+ function createOwnedPlanDirectory(fs, cwd) {
2502
+ const parent = fs.realpathSync(cwd ?? process.cwd());
2503
+ const directory = fs.mkdtempSync(join(parent, PLAN_DIRECTORY_PREFIX));
2504
+ if (!isAbsolute(directory) || dirname(directory) !== parent || !directory.startsWith(join(parent, PLAN_DIRECTORY_PREFIX))) throw new Error("PackyCode plan directory ownership could not be established");
2505
+ const identity = fs.lstatSync(directory);
2506
+ if (!identity.isDirectory() || identity.isSymbolicLink()) throw new Error("PackyCode plan directory ownership could not be established");
2507
+ const canonical = directory;
2508
+ let removed = false;
2509
+ const isOwned = () => {
2510
+ try {
2511
+ const current = fs.lstatSync(directory);
2512
+ return current.isDirectory() && !current.isSymbolicLink() && current.dev === identity.dev && current.ino === identity.ino && fs.realpathSync(directory) === canonical;
2513
+ } catch {
2514
+ return false;
2515
+ }
2516
+ };
2517
+ const owned = {
2518
+ directory: canonical,
2519
+ isOwned,
2520
+ close() {
2521
+ if (removed) return;
2522
+ let current;
2523
+ try {
2524
+ current = fs.lstatSync(directory);
2525
+ } catch (error) {
2526
+ if (error?.code === "ENOENT") {
2527
+ removed = true;
2528
+ return;
2529
+ }
2530
+ throw error;
2531
+ }
2532
+ if (!current.isDirectory() || current.isSymbolicLink() || !isOwned()) throw new Error("PackyCode plan directory ownership changed; refusing to remove it");
2533
+ fs.rmSync(canonical, {
2534
+ recursive: true,
2535
+ force: true
2536
+ });
2537
+ removed = true;
2538
+ }
2539
+ };
2540
+ if (!isOwned()) {
2541
+ owned.close();
2542
+ throw new Error("PackyCode plan directory ownership could not be established");
2543
+ }
2544
+ return owned;
2362
2545
  }
2363
- /** Whether `file` is the root itself or a path inside it, after resolving links. */
2546
+ /** Whether a file stays inside the immutable canonical directory bound to the run. */
2364
2547
  function pathInside(root, file, realpath) {
2548
+ if (typeof root !== "string" || !isAbsolute(root) || typeof file !== "string" || !isAbsolute(file)) return false;
2365
2549
  let rootReal;
2366
2550
  let fileReal;
2367
2551
  try {
@@ -2370,87 +2554,104 @@ function pathInside(root, file, realpath) {
2370
2554
  } catch {
2371
2555
  return false;
2372
2556
  }
2557
+ if (rootReal !== resolve(root)) return false;
2373
2558
  const prefix = rootReal.endsWith(sep) ? rootReal : `${rootReal}${sep}`;
2374
- return fileReal === rootReal || fileReal.startsWith(prefix);
2559
+ return fileReal.startsWith(prefix);
2560
+ }
2561
+ /** A descriptor-based bounded read also rejects a replaced leaf before reading it. */
2562
+ function readBoundedPlanFile(file, expected) {
2563
+ const fd = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
2564
+ try {
2565
+ const info = fstatSync(fd);
2566
+ if (!info.isFile() || expected !== void 0 && (info.dev !== expected.dev || info.ino !== expected.ino)) return void 0;
2567
+ if (info.size > PLAN_FILE_MAX_BYTES) throw Object.assign(/* @__PURE__ */ new Error(PLAN_TOO_LARGE_DENY), { code: "CLAUDE_PLAN_TOO_LARGE" });
2568
+ const bytes = Buffer.allocUnsafe(200001);
2569
+ let length = 0;
2570
+ while (length < bytes.length) {
2571
+ const count = readSync(fd, bytes, length, bytes.length - length, null);
2572
+ if (count === 0) break;
2573
+ length += count;
2574
+ }
2575
+ if (length > PLAN_FILE_MAX_BYTES) throw Object.assign(/* @__PURE__ */ new Error(PLAN_TOO_LARGE_DENY), { code: "CLAUDE_PLAN_TOO_LARGE" });
2576
+ return bytes.subarray(0, length).toString("utf8");
2577
+ } finally {
2578
+ closeSync(fd);
2579
+ }
2375
2580
  }
2376
2581
  /**
2377
- * The plan a CLI `ExitPlanMode` request is submitting.
2378
- *
2379
- * The tool's own schema does not carry the plan; the CLI injects `plan` and
2380
- * `planFilePath` from the file it told the model to write. Text on the request
2381
- * wins. A path is read only when it resolves inside this run's config home or
2382
- * `~/.claude/plans`, is a markdown file written for this run, and is bounded.
2383
- * Anything else is ignored — a model-supplied path is not a licence to read
2384
- * the disk.
2385
- * @param input - the control request's `input`.
2386
- * @param options - config home, when the run started, and file-system seams.
2387
- * @returns plan markdown, or undefined when the request has none yet.
2388
- */
2389
- function readSubmittedPlan(input, { configDir, startedAt = Date.now(), home = homedir(), readFile = (file) => readFileSync(file, "utf8"), stat = statSync, realpath = realpathSync } = {}) {
2390
- const inline = planMarkdown(input?.plan);
2391
- if (inline !== void 0) return inline;
2582
+ * The plan a CLI ExitPlanMode request submits. Inline text remains valid; a
2583
+ * file must resolve inside this invocation's explicitly bound canonical
2584
+ * planDirectory. Shared config homes and ~/.claude/plans are never searched.
2585
+ * The caller supplies scrub so secrets are removed before the size check.
2586
+ * @param input - the control request's input.
2587
+ * @param options - the bound private plan directory, redactor, and filesystem seams.
2588
+ * @returns bounded plan markdown, or undefined when the request has none yet.
2589
+ */
2590
+ function readSubmittedPlan(input, { planDirectory, scrub = (value) => value, onRejected = () => void 0, readFile = readBoundedPlanFile, stat = statSync, realpath = realpathSync } = {}) {
2591
+ if (typeof input?.plan === "string") {
2592
+ const text = scrub(input.plan);
2593
+ const inline = planMarkdown(text);
2594
+ if (inline !== void 0) return inline;
2595
+ if (text.trim() !== "") {
2596
+ onRejected("too-large");
2597
+ return;
2598
+ }
2599
+ }
2392
2600
  const requested = typeof input?.planFilePath === "string" ? input.planFilePath : "";
2393
- if (requested === "") return void 0;
2394
- if (![configDir, join(home, ".claude", "plans")].filter((root) => typeof root === "string" && root !== "").some((root) => pathInside(root, requested, realpath))) return void 0;
2395
- let file;
2601
+ if (!pathInside(planDirectory, requested, realpath)) return void 0;
2396
2602
  try {
2397
- file = realpath(requested);
2603
+ const file = realpath(requested);
2604
+ if (!file.toLowerCase().endsWith(".md")) return void 0;
2398
2605
  const info = stat(file);
2399
- if (!info.isFile() || info.size > PLAN_FILE_MAX_BYTES || info.mtimeMs < startedAt - PLAN_FILE_CLOCK_SKEW_MS) return void 0;
2400
- } catch {
2606
+ if (!info.isFile()) return void 0;
2607
+ if (info.size > PLAN_FILE_MAX_BYTES) {
2608
+ onRejected("too-large");
2609
+ return;
2610
+ }
2611
+ const text = readFile(file, info);
2612
+ if (typeof text !== "string") return void 0;
2613
+ const safe = scrub(text);
2614
+ const plan = planMarkdown(safe);
2615
+ if (plan === void 0 && safe.trim() !== "") onRejected("too-large");
2616
+ return plan;
2617
+ } catch (error) {
2618
+ if (error?.code === "CLAUDE_PLAN_TOO_LARGE") onRejected("too-large");
2401
2619
  return;
2402
2620
  }
2403
- if (!file.toLowerCase().endsWith(".md")) return void 0;
2621
+ }
2622
+ /**
2623
+ * A file-only completion from the CLI is accepted only from this run's new
2624
+ * private plans directory. There is no shared-directory/mtime fallback. If
2625
+ * more than one eligible file exists, do not guess which session submitted it.
2626
+ * @param options - the bound private plan directory, redactor, and filesystem seams.
2627
+ * @returns plan markdown, or undefined when absent or ambiguous.
2628
+ */
2629
+ function findPlanWrittenDuringRun({ planDirectory, scrub = (value) => value, onRejected = () => void 0, readdir = readdirSync, readFile = readBoundedPlanFile, stat = statSync, realpath = realpathSync } = {}) {
2630
+ if (typeof planDirectory !== "string" || !isAbsolute(planDirectory)) return void 0;
2631
+ let names;
2404
2632
  try {
2405
- return planMarkdown(readFile(file));
2633
+ names = readdir(planDirectory);
2406
2634
  } catch {
2407
2635
  return;
2408
2636
  }
2409
- }
2410
- /**
2411
- * The plan file a print-mode CLI wrote and then returned without calling
2412
- * `ExitPlanMode`. `-p` often has no interactive plan tool, so the file is
2413
- * what DSH reviews. Only a markdown file under this run's config `plans`
2414
- * directory or `~/.claude/plans`, newer than the run, is eligible. The newest
2415
- * one wins.
2416
- * @param options - config home, when the run started, and file-system seams.
2417
- * @returns plan markdown, or undefined when this run wrote none.
2418
- */
2419
- function findPlanWrittenDuringRun({ configDir, startedAt = Date.now(), home = homedir(), readdir = readdirSync, readFile = (file) => readFileSync(file, "utf8"), stat = statSync, realpath = realpathSync } = {}) {
2420
- const roots = [typeof configDir === "string" && configDir !== "" ? join(configDir, "plans") : "", join(home, ".claude", "plans")].filter((root) => root !== "");
2421
- let newest;
2422
- let newestMtime = -Infinity;
2423
- for (const root of roots) {
2424
- let names;
2425
- try {
2426
- names = readdir(root);
2427
- } catch {
2428
- continue;
2429
- }
2430
- for (const name of names) {
2431
- if (typeof name !== "string" || !name.toLowerCase().endsWith(".md")) continue;
2432
- const candidate = join(root, name);
2433
- let info;
2434
- try {
2435
- info = stat(candidate);
2436
- } catch {
2437
- continue;
2438
- }
2439
- if (!info.isFile() || info.mtimeMs < startedAt - PLAN_FILE_CLOCK_SKEW_MS || info.mtimeMs < newestMtime) continue;
2440
- const text = readSubmittedPlan({ planFilePath: candidate }, {
2441
- configDir,
2442
- startedAt,
2443
- home,
2444
- readFile,
2445
- stat,
2446
- realpath
2447
- });
2448
- if (text === void 0) continue;
2449
- newest = text;
2450
- newestMtime = info.mtimeMs;
2451
- }
2637
+ let found;
2638
+ for (const name of names) {
2639
+ if (typeof name !== "string" || !name.toLowerCase().endsWith(".md")) continue;
2640
+ const candidate = join(planDirectory, name);
2641
+ if (dirname(candidate) !== planDirectory) continue;
2642
+ const text = readSubmittedPlan({ planFilePath: candidate }, {
2643
+ planDirectory,
2644
+ scrub,
2645
+ onRejected,
2646
+ readFile,
2647
+ stat,
2648
+ realpath
2649
+ });
2650
+ if (text === void 0) continue;
2651
+ if (found !== void 0) return void 0;
2652
+ found = text;
2452
2653
  }
2453
- return newest;
2654
+ return found;
2454
2655
  }
2455
2656
  /**
2456
2657
  * Build what the CLI reads from stdin for one run.
@@ -2579,6 +2780,22 @@ function toolResultText(content) {
2579
2780
  return content.map((part) => part?.type === "text" && typeof part.text === "string" ? part.text : "").join(" ");
2580
2781
  }
2581
2782
  /**
2783
+ * Native plan/permission gates sometimes produce no control_request at all.
2784
+ * Recognize their specific messages, never generic is_error, ENOENT, EACCES,
2785
+ * a failed search, or a command's nonzero exit. An associated assistant tool
2786
+ * use is required, so arbitrary user text cannot spend the denial budget.
2787
+ */
2788
+ function nativePermissionDenial(name, text) {
2789
+ if (typeof name !== "string") return false;
2790
+ if ([
2791
+ "Write",
2792
+ "Edit",
2793
+ "NotebookEdit"
2794
+ ].includes(name) && /^Cannot (?:write to|edit|modify) [\s\S]+ while in plan mode\.(?:\s|$)/u.test(text)) return true;
2795
+ if (["Bash", "PowerShell"].includes(name) && text.startsWith("Auto mode could not evaluate this action and is blocking it for safety")) return true;
2796
+ return text.startsWith(PLAN_ACTION_DENY) || text.startsWith(PLAN_EXIT_DENY) || /^Permission to use [^\n]+ (?:has been|was) denied\b/u.test(text) || /^Claude requested permissions to use [^\n]+, but you haven['’]t granted it yet\b/u.test(text) || /^The user doesn['’]t want to proceed with this tool use\b/u.test(text);
2797
+ }
2798
+ /**
2582
2799
  * Read one `--output-format stream-json` line.
2583
2800
  *
2584
2801
  * The CLI prints both partial deltas (`stream_event`) and each finished
@@ -2701,7 +2918,7 @@ function describeRunKey(credential) {
2701
2918
  * seams.
2702
2919
  * @returns `{ run(options), stream(options) }`.
2703
2920
  */
2704
- function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_CLAUDE_CONFIG_DIR, resolveCredential, spawnImpl = spawn, timeoutMs = DEFAULT_CLAUDE_TIMEOUT_MS, startupTimeoutMs = DEFAULT_CLAUDE_STARTUP_TIMEOUT_MS, platform = process.platform, createSearchBridge } = {}) {
2921
+ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_CLAUDE_CONFIG_DIR, resolveCredential, spawnImpl = spawn, timeoutMs = DEFAULT_CLAUDE_TIMEOUT_MS, startupTimeoutMs = DEFAULT_CLAUDE_STARTUP_TIMEOUT_MS, platform = process.platform, createSearchBridge, planFs = PLAN_FS } = {}) {
2705
2922
  if (typeof resolveCredential !== "function") throw new Error("the Claude Code CLI runner needs resolveCredential");
2706
2923
  const owners = /* @__PURE__ */ new Set();
2707
2924
  let disposed = false;
@@ -2725,7 +2942,6 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
2725
2942
  if (disposed || signal?.aborted) throw new Error("claude_code was aborted");
2726
2943
  const cancellation = new AbortController();
2727
2944
  const runSignal = signal === void 0 ? cancellation.signal : AbortSignal.any([signal, cancellation.signal]);
2728
- const startedAt = Date.now();
2729
2945
  const state = {
2730
2946
  stdout: "",
2731
2947
  stderr: "",
@@ -2733,10 +2949,10 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
2733
2949
  silent: false,
2734
2950
  sawOutput: false,
2735
2951
  settled: false,
2736
- denies: 0,
2737
- startedAt
2952
+ denies: 0
2738
2953
  };
2739
2954
  let child;
2955
+ let plans;
2740
2956
  let bridge;
2741
2957
  let bridgeClosing;
2742
2958
  let pendingBridge;
@@ -2789,13 +3005,18 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
2789
3005
  owners.delete(owner);
2790
3006
  done();
2791
3007
  });
3008
+ plans?.close();
2792
3009
  return;
2793
3010
  }
2794
3011
  try {
2795
3012
  await closeBridge();
2796
3013
  } finally {
2797
- owners.delete(owner);
2798
- done();
3014
+ try {
3015
+ plans?.close();
3016
+ } finally {
3017
+ owners.delete(owner);
3018
+ done();
3019
+ }
2799
3020
  }
2800
3021
  };
2801
3022
  const checkAborted = () => {
@@ -2812,6 +3033,10 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
2812
3033
  }
2813
3034
  };
2814
3035
  try {
3036
+ if (mode === "plan") {
3037
+ plans = createOwnedPlanDirectory(planFs, cwd);
3038
+ state.planDirectory = plans.directory;
3039
+ }
2815
3040
  const credential = await setup(resolveCredential({
2816
3041
  model,
2817
3042
  signal: runSignal
@@ -2846,7 +3071,8 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
2846
3071
  stream,
2847
3072
  appendSystemPrompt,
2848
3073
  searchBridge: bridge,
2849
- disableNativeSearch: typeof createSearchBridge === "function"
3074
+ disableNativeSearch: typeof createSearchBridge === "function",
3075
+ planDirectory: plans?.directory
2850
3076
  });
2851
3077
  child = spawnImpl(command.command, [...command.args, ...args], {
2852
3078
  cwd,
@@ -2877,17 +3103,26 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
2877
3103
  child.stderr?.setEncoding?.("utf8");
2878
3104
  let controlBuffer = "";
2879
3105
  let stdinEnded = false;
3106
+ const onStdinError = () => {
3107
+ if (state.settled || runSignal.aborted || typeof state.plan === "string") return;
3108
+ state.stdinFailed = true;
3109
+ kill();
3110
+ };
2880
3111
  const endStdin = () => {
2881
3112
  if (stdinEnded) return;
2882
3113
  stdinEnded = true;
2883
3114
  try {
2884
3115
  child.stdin?.end?.();
2885
- } catch {}
3116
+ } catch {
3117
+ onStdinError();
3118
+ }
2886
3119
  };
2887
3120
  const writeStdin = (frame) => {
2888
3121
  try {
2889
3122
  child.stdin?.write?.(frame);
2890
- } catch {}
3123
+ } catch {
3124
+ onStdinError();
3125
+ }
2891
3126
  };
2892
3127
  const controlResponse = (requestId, response) => {
2893
3128
  writeStdin(`${JSON.stringify({
@@ -2904,12 +3139,22 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
2904
3139
  endStdin();
2905
3140
  kill();
2906
3141
  };
2907
- const refuseControl = (requestId, kind) => {
2908
- state.denies += 1;
3142
+ const toolUses = /* @__PURE__ */ new Map();
3143
+ const deniedTools = /* @__PURE__ */ new Set();
3144
+ const deniedRequests = /* @__PURE__ */ new Set();
3145
+ const countDenial = (requestId, toolId) => {
3146
+ const duplicate = requestId !== void 0 && deniedRequests.has(requestId) || toolId !== void 0 && deniedTools.has(toolId);
3147
+ if (requestId !== void 0) deniedRequests.add(requestId);
3148
+ if (toolId !== void 0) deniedTools.add(toolId);
3149
+ if (!duplicate) state.denies += 1;
2909
3150
  if (state.denies > MAX_PLAN_DENIALS) {
2910
3151
  stopForDenials();
2911
- return;
3152
+ return false;
2912
3153
  }
3154
+ return true;
3155
+ };
3156
+ const refuseControl = (requestId, kind, toolId) => {
3157
+ if (!countDenial(requestId, toolId)) return;
2913
3158
  if (kind === "dialog") {
2914
3159
  writeStdin(`${JSON.stringify({
2915
3160
  type: "control_response",
@@ -2928,10 +3173,12 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
2928
3173
  };
2929
3174
  /**
2930
3175
  * Plan mode keeps stdin open so a permission prompt can be answered.
2931
- * `ExitPlanMode` with a plan stops the child; it is never allowed,
2932
- * because allowing it would make the CLI start editing in this process.
3176
+ * ExitPlanMode is never allowed: stop with the submitted plan instead.
3177
+ * Native hard denials share the same budget, keyed by tool_use_id so the
3178
+ * tool_result following a control denial cannot charge it a second time.
2933
3179
  */
2934
3180
  const handlePlanLine = (line) => {
3181
+ if (state.denyExhausted === true || typeof state.plan === "string") return;
2935
3182
  const trimmed = line.trim();
2936
3183
  if (!trimmed.startsWith("{")) return;
2937
3184
  let message;
@@ -2941,44 +3188,64 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
2941
3188
  return;
2942
3189
  }
2943
3190
  if (message?.type === "result") {
3191
+ state.resultError = message.is_error === true;
2944
3192
  endStdin();
2945
3193
  return;
2946
3194
  }
3195
+ if (state.resultError !== void 0) return;
3196
+ if (message?.type === "assistant") {
3197
+ for (const block of Array.isArray(message.message?.content) ? message.message.content : []) {
3198
+ if (block?.type !== "tool_use" || typeof block.id !== "string") continue;
3199
+ toolUses.set(block.id, {
3200
+ name: block.name,
3201
+ input: block.input
3202
+ });
3203
+ if (toolUses.size > 512) toolUses.delete(toolUses.keys().next().value);
3204
+ }
3205
+ return;
3206
+ }
3207
+ if (message?.type === "user") {
3208
+ for (const block of Array.isArray(message.message?.content) ? message.message.content : []) {
3209
+ if (block?.type !== "tool_result" || block.is_error !== true || typeof block.tool_use_id !== "string") continue;
3210
+ if (nativePermissionDenial(toolUses.get(block.tool_use_id)?.name, toolResultText(block.content).trim()) && !countDenial(void 0, block.tool_use_id)) return;
3211
+ }
3212
+ return;
3213
+ }
2947
3214
  if (message?.type !== "control_request") return;
2948
3215
  const requestId = message.request_id;
2949
3216
  const request = message.request;
2950
3217
  if (typeof requestId !== "string" || request === null || typeof request !== "object") {
2951
- state.denies += 1;
2952
- if (state.denies > MAX_PLAN_DENIALS) stopForDenials();
3218
+ countDenial(typeof requestId === "string" ? requestId : void 0, void 0);
2953
3219
  return;
2954
3220
  }
3221
+ const toolId = typeof request.tool_use_id === "string" ? request.tool_use_id : [...toolUses].reverse().find(([, tool]) => tool.name === request.tool_name && JSON.stringify(tool.input) === JSON.stringify(request.input))?.[0];
2955
3222
  if (request.subtype !== "can_use_tool") {
2956
- refuseControl(requestId, "dialog");
3223
+ refuseControl(requestId, "dialog", toolId);
2957
3224
  return;
2958
3225
  }
2959
3226
  if (request.tool_name === "ExitPlanMode") {
2960
- const plan = readSubmittedPlan(request.input, {
2961
- configDir,
2962
- startedAt
2963
- });
3227
+ let tooLarge = false;
3228
+ const plan = typeof request.input?.plan === "string" || plans?.isOwned() ? readSubmittedPlan(request.input, {
3229
+ planDirectory: plans?.directory,
3230
+ scrub,
3231
+ onRejected: (reason) => {
3232
+ tooLarge = reason === "too-large";
3233
+ }
3234
+ }) : void 0;
2964
3235
  if (plan !== void 0) {
2965
- state.plan = scrub(plan);
3236
+ state.plan = plan;
2966
3237
  endStdin();
2967
3238
  kill();
2968
3239
  return;
2969
3240
  }
2970
- state.denies += 1;
2971
- if (state.denies > MAX_PLAN_DENIALS) {
2972
- stopForDenials();
2973
- return;
2974
- }
3241
+ if (!countDenial(requestId, toolId)) return;
2975
3242
  controlResponse(requestId, {
2976
3243
  behavior: "deny",
2977
- message: PLAN_EXIT_DENY
3244
+ message: tooLarge ? PLAN_TOO_LARGE_DENY : PLAN_EXIT_DENY
2978
3245
  });
2979
3246
  return;
2980
3247
  }
2981
- refuseControl(requestId, "tool");
3248
+ refuseControl(requestId, "tool", toolId);
2982
3249
  };
2983
3250
  child.stdout?.on("data", (chunk) => {
2984
3251
  state.sawOutput = true;
@@ -3008,18 +3275,32 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
3008
3275
  child.on("close", (code, killSignal) => {
3009
3276
  if (state.settled) return;
3010
3277
  state.settled = true;
3278
+ if (mode === "plan" && controlBuffer.trim() !== "") handlePlanLine(controlBuffer);
3011
3279
  if (state.timedOut) return reject(/* @__PURE__ */ new Error(`claude_code timed out after ${Math.round(timeoutMs / 1e3)}s`));
3012
3280
  if (state.silent) {
3013
3281
  const detail = state.stderr.trim().split("\n").slice(-4).join(" ").slice(0, 400);
3014
3282
  return reject(/* @__PURE__ */ new Error(`Claude Code CLI printed nothing within ${Math.round(startupTimeoutMs / 1e3)}s of starting and was stopped${detail === "" ? "" : `: ${detail}`}`));
3015
3283
  }
3016
3284
  if (signal?.aborted) return reject(/* @__PURE__ */ new Error("claude_code was aborted"));
3285
+ if (state.stdinFailed === true) return reject(/* @__PURE__ */ new Error("claude_code could not write to CLI input"));
3017
3286
  if (state.denyExhausted === true) return reject(/* @__PURE__ */ new Error(PLAN_DENY_LIMIT_MESSAGE));
3018
3287
  if (typeof state.plan === "string") return resolve({
3019
3288
  code,
3020
3289
  killSignal
3021
3290
  });
3022
3291
  if (runSignal.aborted) return reject(/* @__PURE__ */ new Error("claude_code was aborted"));
3292
+ if (mode === "plan" && code === 0 && state.resultError === false && plans?.isOwned()) {
3293
+ let tooLarge = false;
3294
+ const written = findPlanWrittenDuringRun({
3295
+ planDirectory: plans.directory,
3296
+ scrub,
3297
+ onRejected: (reason) => {
3298
+ tooLarge ||= reason === "too-large";
3299
+ }
3300
+ });
3301
+ if (tooLarge) return reject(/* @__PURE__ */ new Error(PLAN_TOO_LARGE_DENY));
3302
+ if (written !== void 0) state.plan = written;
3303
+ }
3023
3304
  resolve({
3024
3305
  code,
3025
3306
  killSignal
@@ -3033,7 +3314,7 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
3033
3314
  throw error;
3034
3315
  });
3035
3316
  closed.catch(() => void 0);
3036
- child.stdin?.on?.("error", () => void 0);
3317
+ child.stdin?.on?.("error", onStdinError);
3037
3318
  if (mode === "plan") writeStdin(input);
3038
3319
  else child.stdin?.end?.(input);
3039
3320
  if (runSignal.aborted) kill();
@@ -3217,13 +3498,6 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
3217
3498
  const event = readClaudeStreamEvents(JSON.parse(scrub(buffer.trim()))).find((item) => item.type === "result");
3218
3499
  if (event !== void 0) sawResult = event.result;
3219
3500
  } catch {}
3220
- if (mode === "plan" && typeof state.plan !== "string" && exitStatus?.code === 0) {
3221
- const written = findPlanWrittenDuringRun({
3222
- configDir,
3223
- startedAt: state.startedAt
3224
- });
3225
- if (written !== void 0) state.plan = scrub(written);
3226
- }
3227
3501
  if (typeof state.plan === "string") {
3228
3502
  if (deltaKind !== void 0) {
3229
3503
  const tail = deltaRedactor.flush({ boundary: true });
@@ -3272,7 +3546,11 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
3272
3546
  };
3273
3547
  } finally {
3274
3548
  stopIfAbandoned();
3275
- await closeBridge();
3549
+ try {
3550
+ await closed.catch(() => void 0);
3551
+ } finally {
3552
+ await closeBridge();
3553
+ }
3276
3554
  }
3277
3555
  },
3278
3556
  /** Revoke bridges and stop every invocation when the plugin unloads. */
@@ -4084,13 +4362,15 @@ function claudeModeFor(sandbox, approval) {
4084
4362
  }
4085
4363
  }
4086
4364
  /**
4087
- * Whether this session should run the CLI in plan mode.
4365
+ * Whether a prepared model request should run the CLI in plan mode.
4088
4366
  *
4089
- * The model request runs inside `agent/pre-step`, before the plan-mode service
4090
- * appends `plan/mode`. `pending` is that not-yet-logged selection and wins over
4091
- * the logged `active` bit, matching the `plan:policy` section. Without the
4092
- * service, the logged projection is the only signal. A missing session, a
4093
- * missing service, or a throw means plan mode is off — never a failed turn.
4367
+ * This selection is for request preparation only, not an executing tool batch.
4368
+ * `pending` is the selection used for the proposed request assembly and wins
4369
+ * over logged `active`, matching `plan:policy`. An accepted pre-step commits
4370
+ * it; reading after that boundary sees the same selection as logged `active`.
4371
+ * Without the service, the logged projection is the only signal. A missing
4372
+ * session, a missing service, or a throw means plan mode is off — never a
4373
+ * failed turn.
4094
4374
  * @param ctx - the plugin context; services are read without an inject requirement.
4095
4375
  * @param session - the session whose plan state is read.
4096
4376
  * @returns true when the CLI must plan instead of acting.
@@ -4108,7 +4388,30 @@ function planModeForSession(ctx, session) {
4108
4388
  return false;
4109
4389
  }
4110
4390
  }
4111
- /** `pending` is the selection the in-flight request was assembled with. */
4391
+ /**
4392
+ * Whether a tool in the current batch must plan, using logged active state.
4393
+ *
4394
+ * Approval queues `pending: false`; it does not authorize remaining tools in
4395
+ * that batch to implement. Likewise, pending entry must not change a tool's
4396
+ * current mode before the next accepted pre-step commits it.
4397
+ * @param ctx - the plugin context.
4398
+ * @param session - the executing tool's session.
4399
+ * @param agent - the trusted executing agent, when provided by the tool runtime.
4400
+ */
4401
+ function planModeForToolExecution(ctx, session, agent) {
4402
+ if (session === void 0 || session === null) return false;
4403
+ try {
4404
+ const caller = agent ?? ctx.get?.("agents")?.currentInitiator?.();
4405
+ if (caller?.session === session) {
4406
+ const state = ctx.get?.("planMode")?.get?.(caller);
4407
+ if (state !== void 0 && state !== null) return state.active === true;
4408
+ }
4409
+ return ctx.get?.("sessionProjections")?.stateOf?.(session, "plan")?.active === true;
4410
+ } catch {
4411
+ return false;
4412
+ }
4413
+ }
4414
+ /** `pending` is the selection the proposed request was assembled with. */
4112
4415
  function planSelected(state) {
4113
4416
  if (typeof state?.pending === "boolean") return state.pending;
4114
4417
  return state?.active === true;
@@ -4161,7 +4464,8 @@ const CLAUDE_TOOL_INSTRUCTIONS = "This task was delegated to Claude Code by anot
4161
4464
  const CLAUDE_TOOL_PLAN_INSTRUCTIONS = "This task was delegated to Claude Code while DeepSeek Harness is in plan mode. Explore read-only, write a complete markdown plan that starts with a # heading to the CLI plan file, then call ExitPlanMode. Do not edit the project or start implementing. Reply with the plan text only.";
4162
4465
  /**
4163
4466
  * @param options - the CLI runner, the most a run may do for a session, and
4164
- * whether that session is in DSH plan mode.
4467
+ * whether the executing batch's logged state is DSH plan mode (not a
4468
+ * pending selection for the next model request).
4165
4469
  * @returns the tool definition to register.
4166
4470
  */
4167
4471
  function createClaudeCodeTool({ runner, modeCeiling, planActive }) {
@@ -4225,7 +4529,7 @@ function createClaudeCodeTool({ runner, modeCeiling, planActive }) {
4225
4529
  const requested = typeof args.cwd === "string" ? args.cwd.trim() : "";
4226
4530
  const cwd = requested === "" ? sessionCwd : isAbsolute(requested) ? requested : sessionCwd === void 0 ? requested : join(sessionCwd, requested);
4227
4531
  const session = exec?.agent?.session;
4228
- const planning = planActive?.(session) === true;
4532
+ const planning = planActive?.(session, exec?.agent) === true;
4229
4533
  return await runner.run({
4230
4534
  prompt: args.prompt,
4231
4535
  mode: planning ? "plan" : capClaudeMode(args.mode, modeCeiling(session)) ?? "edit",
@@ -4612,6 +4916,134 @@ var PackyCodeKeyRing = class {
4612
4916
  }
4613
4917
  };
4614
4918
  //#endregion
4919
+ //#region src/preferences.js
4920
+ /**
4921
+ * Where the plugin's preferences are read from and written to, on both
4922
+ * settings generations DSH has shipped.
4923
+ *
4924
+ * - DSH 0.1.5 installs a settings *section*: the section hands back a live
4925
+ * source and writes go through `settings.update(namespace, patch)`.
4926
+ * - DSH 0.2 has no sections: a preference is a *volatile* Config field of the
4927
+ * plugin's own row, read through a live reference, written through
4928
+ * `settings.update(rowId, patch)`, and announced by `loader/volatile-update`.
4929
+ * Only volatile fields are writable there, so the host overrides stay
4930
+ * read-only while web search and model visibility are editable.
4931
+ *
4932
+ * @module dsh-packycode-api/preferences
4933
+ */
4934
+ /**
4935
+ * Mark a preference editable without restarting the plugin. DSH 0.2 stores
4936
+ * plugin preferences as volatile Config fields and writes only those. The
4937
+ * peer dependency requires the schema version that implements `volatile()`.
4938
+ */
4939
+ const live = (schema) => schema.volatile();
4940
+ /** Read a Config field that may arrive as a volatile reference. */
4941
+ const fieldValue = (value) => value !== null && typeof value === "object" && typeof value.get === "function" ? value.get() : value;
4942
+ /** A preference target nothing can write to yet. */
4943
+ function unwritable(message) {
4944
+ return {
4945
+ writable: false,
4946
+ set: async () => {
4947
+ throw new Error(message);
4948
+ }
4949
+ };
4950
+ }
4951
+ /**
4952
+ * Connect the preferences to the settings service the host provides.
4953
+ * @param settingsCtx - the scope the `settings` inject handed over.
4954
+ * @param options - the plugin context and Config, the section namespace, the
4955
+ * current source and how to replace it, what to do when the section changes,
4956
+ * and the preference targets this fills in (host overrides, search, models).
4957
+ */
4958
+ function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSource, onChange, hostPreference, searchPreference, modelPreference }) {
4959
+ const settings = settingsCtx.settings;
4960
+ if (typeof settings?.installSection === "function") {
4961
+ try {
4962
+ settings.installSection(ctx, namespace, Config, source(), {
4963
+ setSource: (current) => setSource(typeof current === "function" ? () => current() ?? {} : () => current ?? {}),
4964
+ onChange
4965
+ });
4966
+ if (typeof settings.update === "function") {
4967
+ const write = (patch) => settings.update(namespace, patch);
4968
+ const target = {
4969
+ writable: settings.writable !== false,
4970
+ set: write
4971
+ };
4972
+ Object.assign(hostPreference, target);
4973
+ Object.assign(searchPreference, target);
4974
+ Object.assign(modelPreference, target);
4975
+ }
4976
+ } catch (error) {
4977
+ ctx.logger?.warn?.("packycode-api: host overrides unavailable, defaults stand: %s", error.message);
4978
+ }
4979
+ return;
4980
+ }
4981
+ if (typeof settings?.update !== "function") return;
4982
+ const rowId = ctx.fiber?.entry?.options?.id ?? namespace;
4983
+ const target = {
4984
+ writable: settings.writable !== false,
4985
+ set: (patch) => settings.update(rowId, patch)
4986
+ };
4987
+ Object.assign(searchPreference, target);
4988
+ Object.assign(modelPreference, target);
4989
+ if (typeof settings.configure === "function") {
4990
+ const scope = typeof settingsCtx.effect === "function" ? settingsCtx : ctx;
4991
+ try {
4992
+ scope.effect(() => settings.configure({ auto: false }, ctx.fiber), "packycode-api: settings page policy");
4993
+ } catch (error) {
4994
+ ctx.logger?.warn?.("packycode-api: could not hide the generated settings page: %s", error.message);
4995
+ }
4996
+ }
4997
+ }
4998
+ //#endregion
4999
+ //#region src/model-selection.js
5000
+ /** Profile preferences for the models advertised in PackyCode's picker. */
5001
+ /** Store exclusions so new catalog models are selected by default as well. */
5002
+ function readHiddenModels(preference) {
5003
+ const value = fieldValue(preference?.hiddenModels);
5004
+ if (!Array.isArray(value)) return [];
5005
+ const hidden = new Set(value);
5006
+ return PACKYCODE_MODEL_IDS.filter((id) => hidden.has(id));
5007
+ }
5008
+ function createModelSelectionController({ readPreference, writePreference, isWritable, onChange }) {
5009
+ const hidden = () => readHiddenModels(readPreference());
5010
+ let advertised = hidden().join(",");
5011
+ const reconcile = () => {
5012
+ const next = hidden().join(",");
5013
+ if (next === advertised) return;
5014
+ onChange?.();
5015
+ advertised = next;
5016
+ };
5017
+ const status = () => {
5018
+ const excluded = new Set(hidden());
5019
+ return {
5020
+ writable: isWritable() === true,
5021
+ models: PACKYCODE_MODELS.map(({ id, name }) => ({
5022
+ id,
5023
+ name,
5024
+ selected: !excluded.has(id)
5025
+ }))
5026
+ };
5027
+ };
5028
+ return Object.freeze({
5029
+ status,
5030
+ reconcile,
5031
+ isVisible: (id) => !hidden().includes(id),
5032
+ async update({ selectedIds } = {}) {
5033
+ if (!Array.isArray(selectedIds) || selectedIds.some((id) => !PACKYCODE_MODEL_IDS.includes(id))) throw new Error("That PackyCode model selection is not supported");
5034
+ if (isWritable() !== true) throw new Error("PackyCode settings are not writable in this profile");
5035
+ const selected = new Set(selectedIds);
5036
+ try {
5037
+ await writePreference({ hiddenModels: PACKYCODE_MODEL_IDS.filter((id) => !selected.has(id)) });
5038
+ } catch {
5039
+ throw new Error("PackyCode could not save the model selection");
5040
+ }
5041
+ reconcile();
5042
+ return status();
5043
+ }
5044
+ });
5045
+ }
5046
+ //#endregion
4615
5047
  //#region src/plugin-version.js
4616
5048
  /**
4617
5049
  * Version self-knowledge for the settings panel.
@@ -4934,85 +5366,6 @@ function createPackyCodePluginManager({ fetchImpl = ambientFetch, runCommand = s
4934
5366
  });
4935
5367
  }
4936
5368
  //#endregion
4937
- //#region src/preferences.js
4938
- /**
4939
- * Where the plugin's preferences are read from and written to, on both
4940
- * settings generations DSH has shipped.
4941
- *
4942
- * - DSH 0.1.5 installs a settings *section*: the section hands back a live
4943
- * source and writes go through `settings.update(namespace, patch)`.
4944
- * - DSH 0.2 has no sections: a preference is a *volatile* Config field of the
4945
- * plugin's own row, read through a live reference, written through
4946
- * `settings.update(rowId, patch)`, and announced by `loader/volatile-update`.
4947
- * Only volatile fields are writable there, so the host overrides stay
4948
- * read-only while the web search preference is editable.
4949
- *
4950
- * @module dsh-packycode-api/preferences
4951
- */
4952
- /**
4953
- * Mark a preference editable without restarting the plugin. DSH 0.2 stores
4954
- * plugin preferences as volatile Config fields and writes only those; an older
4955
- * schema library has no `volatile()` and keeps a plain field.
4956
- */
4957
- const live = (schema) => typeof schema.volatile === "function" ? schema.volatile() : schema;
4958
- /** Read a Config field that may arrive as a volatile reference. */
4959
- const fieldValue = (value) => value !== null && typeof value === "object" && typeof value.get === "function" ? value.get() : value;
4960
- /** A preference target nothing can write to yet. */
4961
- function unwritable(message) {
4962
- return {
4963
- writable: false,
4964
- set: async () => {
4965
- throw new Error(message);
4966
- }
4967
- };
4968
- }
4969
- /**
4970
- * Connect the preferences to the settings service the host provides.
4971
- * @param settingsCtx - the scope the `settings` inject handed over.
4972
- * @param options - the plugin context and Config, the section namespace, the
4973
- * current source and how to replace it, what to do when the section changes,
4974
- * and the two preference targets this fills in (host overrides, search).
4975
- */
4976
- function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSource, onChange, hostPreference, searchPreference }) {
4977
- const settings = settingsCtx.settings;
4978
- if (typeof settings?.installSection === "function") {
4979
- try {
4980
- settings.installSection(ctx, namespace, Config, source(), {
4981
- setSource: (current) => setSource(typeof current === "function" ? () => current() ?? {} : () => current ?? {}),
4982
- onChange
4983
- });
4984
- if (typeof settings.update === "function") {
4985
- const write = (patch) => settings.update(namespace, patch);
4986
- Object.assign(hostPreference, {
4987
- writable: true,
4988
- set: write
4989
- });
4990
- Object.assign(searchPreference, {
4991
- writable: true,
4992
- set: write
4993
- });
4994
- }
4995
- } catch (error) {
4996
- ctx.logger?.warn?.("packycode-api: host overrides unavailable, defaults stand: %s", error.message);
4997
- }
4998
- return;
4999
- }
5000
- if (typeof settings?.update !== "function") return;
5001
- const rowId = ctx.fiber?.entry?.options?.id ?? namespace;
5002
- Object.assign(searchPreference, {
5003
- writable: settings.writable !== false,
5004
- set: (patch) => settings.update(rowId, patch)
5005
- });
5006
- if (typeof settings.configure === "function") {
5007
- const scope = typeof settingsCtx.effect === "function" ? settingsCtx : ctx;
5008
- try {
5009
- scope.effect(() => settings.configure({ auto: false }, ctx.fiber), "packycode-api: settings page policy");
5010
- } catch (error) {
5011
- ctx.logger?.warn?.("packycode-api: could not hide the generated settings page: %s", error.message);
5012
- }
5013
- }
5014
- }
5015
- //#endregion
5016
5369
  //#region src/provider.js
5017
5370
  /**
5018
5371
  * The PackyCode pi-ai provider, built in code so the user configures no route.
@@ -5042,7 +5395,7 @@ function keyRingAuth() {
5042
5395
  }
5043
5396
  /** Build the route's pi-ai provider (unguarded; the plugin wraps its streams). */
5044
5397
  function createPackyCodeProvider() {
5045
- return createProvider({
5398
+ const provider = createProvider({
5046
5399
  id: PROVIDER,
5047
5400
  name: DISPLAY_NAME,
5048
5401
  baseUrl: DEFAULT_API_HOST,
@@ -5054,6 +5407,15 @@ function createPackyCodeProvider() {
5054
5407
  "anthropic-messages": anthropicMessagesApi()
5055
5408
  }
5056
5409
  });
5410
+ const requestOptions = (model, options) => model.api === "openai-responses" && !model.id.startsWith("gpt-") ? {
5411
+ ...options,
5412
+ cacheRetention: "none"
5413
+ } : options;
5414
+ return Object.freeze({
5415
+ ...provider,
5416
+ stream: (model, context, options) => provider.stream(model, context, requestOptions(model, options)),
5417
+ streamSimple: (model, context, options) => provider.streamSimple(model, context, requestOptions(model, options))
5418
+ });
5057
5419
  }
5058
5420
  /**
5059
5421
  * Observe a pi-ai event stream and hand a key-level failure to the plugin.
@@ -5134,7 +5496,7 @@ function guardKeyRotation(stream, { onKeyFailure, key } = {}) {
5134
5496
  //#region src/route.js
5135
5497
  /**
5136
5498
  * The HTTP model route: the pi-ai provider profile and the adapter that
5137
- * serves the catalog's HTTP models (DeepSeek and GPT) with the ring's keys.
5499
+ * serves the catalog's HTTP models (DeepSeek, GPT and Grok) with the ring's keys.
5138
5500
  *
5139
5501
  * @module dsh-packycode-api/route
5140
5502
  */
@@ -5224,7 +5586,7 @@ function createPackyCodeProfile(provider) {
5224
5586
  * supplies none, and the attachment store image input is read from.
5225
5587
  * @returns a `PiAiAdapter` serving the `packycode` route.
5226
5588
  */
5227
- function createRouteAdapter({ ring, keys, fallbackSessionId, resolveAttachments }) {
5589
+ function createRouteAdapter({ ring, keys, fallbackSessionId, resolveAttachments, isModelVisible = () => true }) {
5228
5590
  const raw = createPackyCodeProvider();
5229
5591
  /**
5230
5592
  * The gateway's Codex-group check refuses a Responses request that carries
@@ -5257,7 +5619,11 @@ function createRouteAdapter({ ring, keys, fallbackSessionId, resolveAttachments
5257
5619
  streamSimple: guarded("streamSimple")
5258
5620
  });
5259
5621
  const profiles = /* @__PURE__ */ new Map([[PROVIDER, createPackyCodeProfile(provider)]]);
5260
- return new PiAiAdapter({
5622
+ return new class extends PiAiAdapter {
5623
+ async listModels(provider) {
5624
+ return (await super.listModels(provider)).filter((model) => isModelVisible(model.id));
5625
+ }
5626
+ }({
5261
5627
  profiles: () => profiles,
5262
5628
  resolveApiKey: async () => {
5263
5629
  const active = await keys.active();
@@ -5282,7 +5648,7 @@ function createRouteAdapter({ ring, keys, fallbackSessionId, resolveAttachments
5282
5648
  * Every reply carries only what this plugin owns: key labels and state, the
5283
5649
  * balance and group projections the host built, host overrides, whether this
5284
5650
  * device has the Claude Code CLI, the plugin version, and the web search
5285
- * preference. Key material never crosses this boundary.
5651
+ * preference and model visibility. Key material never crosses this boundary.
5286
5652
  *
5287
5653
  * @module dsh-packycode-api/rpc
5288
5654
  */
@@ -5292,7 +5658,11 @@ const ok = (value) => ({
5292
5658
  });
5293
5659
  const failed = (message) => ({
5294
5660
  ok: false,
5295
- error: { message }
5661
+ error: {
5662
+ code: "packycode/request-failed",
5663
+ message,
5664
+ details: {}
5665
+ }
5296
5666
  });
5297
5667
  /** Only sentences this plugin authored may reach the browser. */
5298
5668
  const SAFE_MESSAGE = /^(?:PackyCode|That |Unknown |PackyCode's |add |the )[\s\S]{0,180}$/u;
@@ -5304,7 +5674,7 @@ const keyId = (payload) => String(payload?.id ?? "");
5304
5674
  * web search controller (without one, its endpoints refuse).
5305
5675
  * @returns a handler for `connection.rpc.handle(CHANNEL, handler)`.
5306
5676
  */
5307
- function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCli, pluginManager, search }) {
5677
+ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCli, pluginManager, search, modelSelection }) {
5308
5678
  const need = (service, message) => {
5309
5679
  if (service === void 0) throw new Error(message);
5310
5680
  return service;
@@ -5313,6 +5683,7 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCl
5313
5683
  const manager = () => need(pluginManager, "PackyCode plugin version is unavailable");
5314
5684
  const webSearch = () => need(search, "PackyCode web search is unavailable in this runtime");
5315
5685
  const groupReader = () => need(groups, "PackyCode key groups are unavailable in this runtime");
5686
+ const models = () => need(modelSelection, "PackyCode model selection is unavailable in this runtime");
5316
5687
  const status = async () => ({
5317
5688
  ...await ring.list(),
5318
5689
  hosts: hosts(),
@@ -5377,7 +5748,9 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCl
5377
5748
  "search/update": (payload) => webSearch().update({
5378
5749
  searchProvider: payload?.searchProvider,
5379
5750
  searchBackend: payload?.searchBackend
5380
- })
5751
+ }),
5752
+ "models/status": () => models().status(),
5753
+ "models/update": (payload) => models().update({ selectedIds: payload?.selectedIds })
5381
5754
  };
5382
5755
  return async (endpoint, payload, signal) => {
5383
5756
  try {
@@ -6122,9 +6495,11 @@ const FOREIGN_ROUTERS = Object.freeze({
6122
6495
  });
6123
6496
  /** Normalize a stored preference; anything unknown reads as the default. */
6124
6497
  function readSearchChoice(value) {
6498
+ const provider = fieldValue(value?.searchProvider);
6499
+ const selectedBackend = fieldValue(value?.searchBackend);
6125
6500
  return {
6126
- mode: SEARCH_MODES.includes(value?.searchProvider) ? value.searchProvider : SEARCH_MODE_DEFAULT,
6127
- backend: SEARCH_BACKENDS.includes(value?.searchBackend) ? value.searchBackend : DEFAULT_SEARCH_BACKEND
6501
+ mode: SEARCH_MODES.includes(provider) ? provider : SEARCH_MODE_DEFAULT,
6502
+ backend: SEARCH_BACKENDS.includes(selectedBackend) ? selectedBackend : DEFAULT_SEARCH_BACKEND
6128
6503
  };
6129
6504
  }
6130
6505
  /** Only sentences this plugin wrote reach the browser. */
@@ -6308,7 +6683,9 @@ const Config = z.object({
6308
6683
  */
6309
6684
  searchProvider: live(z.union([...SEARCH_MODES]).default(SEARCH_MODE_DEFAULT)),
6310
6685
  /** Which PackyCode model runs a search: `deepseek` or `gpt`. */
6311
- searchBackend: live(z.union([...SEARCH_BACKENDS]).default(DEFAULT_SEARCH_BACKEND))
6686
+ searchBackend: live(z.union([...SEARCH_BACKENDS]).default(DEFAULT_SEARCH_BACKEND)),
6687
+ /** Only the picker is filtered; an existing session can keep its model. */
6688
+ hiddenModels: live(z.array(z.string()).default([]))
6312
6689
  });
6313
6690
  function apply(ctx, config) {
6314
6691
  const ring = new PackyCodeKeyRing(ctx.credentials);
@@ -6316,7 +6693,8 @@ function apply(ctx, config) {
6316
6693
  apiHost: normalizeHost(config?.apiHost, DEFAULT_API_HOST),
6317
6694
  statusHost: normalizeHost(config?.statusHost, DEFAULT_STATUS_HOST),
6318
6695
  searchProvider: fieldValue(config?.searchProvider),
6319
- searchBackend: fieldValue(config?.searchBackend)
6696
+ searchBackend: fieldValue(config?.searchBackend),
6697
+ hiddenModels: fieldValue(config?.hiddenModels)
6320
6698
  });
6321
6699
  const hosts = () => {
6322
6700
  const current = source() ?? {};
@@ -6362,6 +6740,17 @@ function apply(ctx, config) {
6362
6740
  };
6363
6741
  const fallbackSessionId = `${PLUGIN_ID}-${randomUUID()}`;
6364
6742
  const attachments = () => ctx.get?.("attachments") ?? ctx.attachments;
6743
+ const modelPreference = {
6744
+ writable: false,
6745
+ set: void 0
6746
+ };
6747
+ let refreshModels = () => {};
6748
+ const modelSelection = createModelSelectionController({
6749
+ readPreference: () => source() ?? {},
6750
+ writePreference: (patch) => modelPreference.set(patch),
6751
+ isWritable: () => modelPreference.writable === true && typeof modelPreference.set === "function",
6752
+ onChange: () => refreshModels()
6753
+ });
6365
6754
  /**
6366
6755
  * The Claude Code CLI runner. PackyCode serves its Claude models only to the
6367
6756
  * official CLI, so CLI-backed models are run by that CLI itself — with this
@@ -6420,12 +6809,13 @@ function apply(ctx, config) {
6420
6809
  const configured = config?.claudeCliMode;
6421
6810
  return configured === void 0 || configured === "auto" ? claudeModeForSession(ctx, session) : configured;
6422
6811
  };
6423
- ctx.llm.registerAdapter([PROVIDER], createClaudeCliAdapter({
6812
+ const registration = ctx.llm.registerAdapter([PROVIDER], createClaudeCliAdapter({
6424
6813
  inner: createRouteAdapter({
6425
6814
  ring,
6426
6815
  keys,
6427
6816
  fallbackSessionId,
6428
- resolveAttachments: attachments
6817
+ resolveAttachments: attachments,
6818
+ isModelVisible: modelSelection.isVisible
6429
6819
  }),
6430
6820
  runner: claudeRunner,
6431
6821
  resolveMode: () => {
@@ -6436,6 +6826,10 @@ function apply(ctx, config) {
6436
6826
  resolveAttachments: attachments,
6437
6827
  explainKey: (runKey) => keys.describe(runKey)
6438
6828
  }));
6829
+ refreshModels = () => {
6830
+ if (typeof registration?.replace === "function") registration.replace([PROVIDER]);
6831
+ else ctx.emit?.("llm/adapters-updated");
6832
+ };
6439
6833
  ctx.llm.registerConfigurableProviders([{
6440
6834
  provider: PROVIDER,
6441
6835
  displayName: DISPLAY_NAME,
@@ -6487,12 +6881,15 @@ function apply(ctx, config) {
6487
6881
  onChange: () => {
6488
6882
  balance.invalidate();
6489
6883
  groups.invalidate();
6884
+ modelSelection.reconcile();
6490
6885
  search.reconcile();
6491
6886
  },
6492
6887
  hostPreference,
6493
- searchPreference
6888
+ searchPreference,
6889
+ modelPreference
6494
6890
  }));
6495
6891
  ctx.on?.("loader/volatile-update", () => {
6892
+ modelSelection.reconcile();
6496
6893
  search.reconcile();
6497
6894
  });
6498
6895
  /**
@@ -6514,7 +6911,8 @@ function apply(ctx, config) {
6514
6911
  preference: hostPreference,
6515
6912
  claudeCli,
6516
6913
  pluginManager: createPackyCodePluginManager(),
6517
- search
6914
+ search,
6915
+ modelSelection
6518
6916
  });
6519
6917
  /**
6520
6918
  * Connection registers a channel through `owner.webServer`, read on its own
@@ -6531,9 +6929,9 @@ function apply(ctx, config) {
6531
6929
  ctx.effect(() => toolsCtx.tools.register(createClaudeCodeTool({
6532
6930
  runner: claudeRunner,
6533
6931
  modeCeiling: cliModeCeiling,
6534
- planActive: (session) => planModeForSession(ctx, session)
6932
+ planActive: (session, agent) => planModeForToolExecution(ctx, session, agent)
6535
6933
  })), "packycode-api: claude_code tool");
6536
6934
  });
6537
6935
  }
6538
6936
  //#endregion
6539
- export { AUTO_CLAUDE_MODE, CHANNEL, CLAUDE_CLI_MODES, CLAUDE_INSTALL_COMMAND, CLAUDE_MISSING_MESSAGE, CLAUDE_NPM_PACKAGE, CLAUDE_TOOL_INSTRUCTIONS, CLI_BACKED_MODEL_IDS, CLI_IMAGE_LIMITS, Config, DEFAULT_API_HOST, DEFAULT_APPEND_SYSTEM_PROMPT, DEFAULT_SEARCH_BACKEND, DEFAULT_STATUS_HOST, DISPLAY_NAME, LEGACY_STATUS_HOSTS, PACKYCODE_AUTO_SEARCH_PROVIDER_ID, PACKYCODE_MODELS, PACKYCODE_MODEL_IDS, PACKYCODE_SEARCH_PROVIDER_ID, PLUGIN_ID, PROVIDER, PackyCodeKeyRing, SEARCH_BACKENDS, SEARCH_BACKEND_MODELS, SEARCH_MODES, SETTINGS_NAMESPACE, TITLE_MODEL_ID, TITLE_MODEL_ROUTES, accountHostOrder, apply, assertApiKey, buildClaudeArgs, buildClaudeEnvironment, buildClaudeInput, buildDelegatedContent, buildSearchRequest, capClaudeMode, catalogCoverage, classifyClientPolicy, classifyGatewayFailure, claudeBinaryCandidates, claudeModeFor, claudeModeForSession, cliSearchInstructions, createBalanceReader, createClaudeCliAdapter, createClaudeCliManager, createClaudeCodeTool, createClaudeRunner, createClaudeSearchBridge, createGroupReader, createKeyChooser, createPackyCodeAutoSearchProvider, createPackyCodeProfile, createPackyCodeProvider, createPackyCodeRpc, createPackyCodeSearchProvider, createProfileSearchComposition, createRouteAdapter, createSearchComposition, createSearchController, delegationInstructions, describeRunKey, explainCliClientPolicyFailure, explainCliGroupFailure, explainClientPolicyFailure, explainNpmFailure, findClaudeBinary, guardKeyRotation, inject, keyNote, locateClaudeBinary, locateNpm, name, normalizeFailureMessage, normalizeHost, npmCandidates, parseBillingSubscription, parseBillingUsage, parseClaudeResult, parseClaudeVersion, parseMessagesSearchResponse, parsePricing, parseResponsesSearchResponse, parseSelectedGroups, parseTokenUsage, pickKeyForModel, planModeForSession, readClaudeStreamEvents, readSearchChoice, renderAuxiliaryPrompt, renderDelegatedPrompt, resolveSpawnCommand, statusHostOrder, summarizeToolUse };
6937
+ export { AUTO_CLAUDE_MODE, CHANNEL, CLAUDE_CLI_MODES, CLAUDE_INSTALL_COMMAND, CLAUDE_MISSING_MESSAGE, CLAUDE_NPM_PACKAGE, CLAUDE_TOOL_INSTRUCTIONS, CLI_BACKED_MODEL_IDS, CLI_IMAGE_LIMITS, Config, DEFAULT_API_HOST, DEFAULT_APPEND_SYSTEM_PROMPT, DEFAULT_SEARCH_BACKEND, DEFAULT_STATUS_HOST, DISPLAY_NAME, LEGACY_STATUS_HOSTS, PACKYCODE_AUTO_SEARCH_PROVIDER_ID, PACKYCODE_MODELS, PACKYCODE_MODEL_IDS, PACKYCODE_SEARCH_PROVIDER_ID, PLUGIN_ID, PROVIDER, PackyCodeKeyRing, SEARCH_BACKENDS, SEARCH_BACKEND_MODELS, SEARCH_MODES, SETTINGS_NAMESPACE, TITLE_MODEL_ID, TITLE_MODEL_ROUTES, accountHostOrder, apply, assertApiKey, buildClaudeArgs, buildClaudeEnvironment, buildClaudeInput, buildDelegatedContent, buildSearchRequest, capClaudeMode, catalogCoverage, classifyClientPolicy, classifyGatewayFailure, claudeBinaryCandidates, claudeModeFor, claudeModeForSession, cliSearchInstructions, createBalanceReader, createClaudeCliAdapter, createClaudeCliManager, createClaudeCodeTool, createClaudeRunner, createClaudeSearchBridge, createGroupReader, createKeyChooser, createModelSelectionController, createPackyCodeAutoSearchProvider, createPackyCodeProfile, createPackyCodeProvider, createPackyCodeRpc, createPackyCodeSearchProvider, createProfileSearchComposition, createRouteAdapter, createSearchComposition, createSearchController, delegationInstructions, describeRunKey, explainCliClientPolicyFailure, explainCliGroupFailure, explainClientPolicyFailure, explainNpmFailure, findClaudeBinary, guardKeyRotation, inject, keyNote, locateClaudeBinary, locateNpm, name, normalizeFailureMessage, normalizeHost, npmCandidates, parseBillingSubscription, parseBillingUsage, parseClaudeResult, parseClaudeVersion, parseMessagesSearchResponse, parsePricing, parseResponsesSearchResponse, parseSelectedGroups, parseTokenUsage, pickKeyForModel, planModeForSession, planModeForToolExecution, readClaudeStreamEvents, readHiddenModels, readSearchChoice, renderAuxiliaryPrompt, renderDelegatedPrompt, resolveSpawnCommand, statusHostOrder, summarizeToolUse };