@khalilgharbaoui/opencode-claude-code-plugin 0.52.0 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/claude-code-language-model.ts
2
- import { join as join13 } from "path";
2
+ import { join as join15 } from "path";
3
3
 
4
4
  // src/ids.ts
5
5
  import { randomInt } from "crypto";
@@ -1844,6 +1844,9 @@ function failoverUntil(resetsAt) {
1844
1844
  return resetsAtToMs(resetsAt);
1845
1845
  }
1846
1846
 
1847
+ // src/subagent-dispatch.ts
1848
+ import { join as join4 } from "path";
1849
+
1847
1850
  // src/agent-models.ts
1848
1851
  import { readFile, readdir } from "fs/promises";
1849
1852
  import path2 from "path";
@@ -2441,6 +2444,252 @@ function agentDirectories(home, projectDirectory) {
2441
2444
  return directories;
2442
2445
  }
2443
2446
 
2447
+ // src/dispatch-choice-store.ts
2448
+ import { mkdirSync as mkdirSync2, readFileSync, renameSync as renameSync2, writeFileSync } from "fs";
2449
+ import { homedir as homedir3 } from "os";
2450
+ import { join as join2 } from "path";
2451
+ var MAX_DISPATCH_CHOICE_SESSIONS = 128;
2452
+ var MAX_DISPATCH_CHOICES_PER_SESSION = 32;
2453
+ var DISPATCH_CHOICE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
2454
+ var sessions = null;
2455
+ var pathOverride = null;
2456
+ function dispatchChoiceStorePath() {
2457
+ if (pathOverride) return pathOverride;
2458
+ const stateRoot = process.env.XDG_STATE_HOME || join2(homedir3(), ".local", "state");
2459
+ return join2(stateRoot, "opencode-claude-code-plugin", "subagent-dispatch.json");
2460
+ }
2461
+ function isChoice(value) {
2462
+ const choice = value;
2463
+ if (!choice || typeof choice !== "object" || Array.isArray(choice)) return false;
2464
+ const ok = (field) => field === void 0 || typeof field === "string";
2465
+ if (!ok(choice.model) || !ok(choice.effort) || !ok(choice.account)) return false;
2466
+ return !!(choice.model || choice.effort || choice.account);
2467
+ }
2468
+ function isSessionRecord(value) {
2469
+ const record = value;
2470
+ if (!record || typeof record !== "object" || Array.isArray(record)) return false;
2471
+ if (typeof record.updatedAt !== "number" || !Number.isFinite(record.updatedAt)) {
2472
+ return false;
2473
+ }
2474
+ const choices = record.choices;
2475
+ if (!choices || typeof choices !== "object" || Array.isArray(choices)) return false;
2476
+ return Object.values(choices).every(isChoice);
2477
+ }
2478
+ function trimChoices(choices) {
2479
+ const entries = Object.entries(choices).filter(([, choice]) => isChoice(choice));
2480
+ return Object.fromEntries(entries.slice(-MAX_DISPATCH_CHOICES_PER_SESSION));
2481
+ }
2482
+ function readFile2(file) {
2483
+ const out = /* @__PURE__ */ new Map();
2484
+ try {
2485
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
2486
+ const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
2487
+ if (!entries || typeof entries !== "object") return out;
2488
+ const cutoff = Date.now() - DISPATCH_CHOICE_MAX_AGE_MS;
2489
+ const valid = Object.entries(entries).filter((entry) => isSessionRecord(entry[1])).filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt);
2490
+ for (const [key, record] of valid) out.set(key, record);
2491
+ } catch {
2492
+ }
2493
+ return out;
2494
+ }
2495
+ function load() {
2496
+ if (!sessions) sessions = readFile2(dispatchChoiceStorePath());
2497
+ return sessions;
2498
+ }
2499
+ function save(update) {
2500
+ const file = dispatchChoiceStorePath();
2501
+ const merged = readFile2(file);
2502
+ update(merged);
2503
+ const cutoff = Date.now() - DISPATCH_CHOICE_MAX_AGE_MS;
2504
+ const kept = [...merged.entries()].filter(([, record]) => record.updatedAt >= cutoff).filter(([, record]) => Object.keys(record.choices).length > 0).sort((a, b) => a[1].updatedAt - b[1].updatedAt).slice(-MAX_DISPATCH_CHOICE_SESSIONS);
2505
+ sessions = new Map(kept);
2506
+ try {
2507
+ mkdirSync2(join2(file, ".."), { recursive: true, mode: 448 });
2508
+ const tmp = `${file}.${process.pid}.tmp`;
2509
+ writeFileSync(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
2510
+ mode: 384
2511
+ });
2512
+ renameSync2(tmp, file);
2513
+ } catch (err) {
2514
+ log.debug("could not write the subagent dispatch choice store", {
2515
+ error: err instanceof Error ? err.message : String(err)
2516
+ });
2517
+ }
2518
+ }
2519
+ function readDispatchChoice(opencodeSessionId, agent) {
2520
+ if (!opencodeSessionId || opencodeSessionId === "default") return void 0;
2521
+ const record = load().get(opencodeSessionId);
2522
+ const choice = record?.choices[agent];
2523
+ return choice ? { ...choice } : void 0;
2524
+ }
2525
+ function writeDispatchChoice(opencodeSessionId, agent, choice) {
2526
+ if (!opencodeSessionId || opencodeSessionId === "default" || !agent) return;
2527
+ const stored = {
2528
+ ...choice.model ? { model: choice.model } : {},
2529
+ ...choice.effort ? { effort: choice.effort } : {},
2530
+ ...choice.account ? { account: choice.account } : {}
2531
+ };
2532
+ if (!isChoice(stored)) return;
2533
+ const current = load().get(opencodeSessionId)?.choices[agent];
2534
+ if (current && current.model === stored.model && current.effort === stored.effort && current.account === stored.account) {
2535
+ return;
2536
+ }
2537
+ save((merged) => {
2538
+ const record = merged.get(opencodeSessionId) ?? { choices: {}, updatedAt: 0 };
2539
+ const choices = { ...record.choices };
2540
+ delete choices[agent];
2541
+ choices[agent] = stored;
2542
+ merged.delete(opencodeSessionId);
2543
+ merged.set(opencodeSessionId, {
2544
+ choices: trimChoices(choices),
2545
+ updatedAt: Date.now()
2546
+ });
2547
+ });
2548
+ }
2549
+ function forgetDispatchChoices(opencodeSessionId) {
2550
+ if (!load().has(opencodeSessionId)) return;
2551
+ save((merged) => {
2552
+ merged.delete(opencodeSessionId);
2553
+ });
2554
+ }
2555
+
2556
+ // src/tmp.ts
2557
+ import * as fs from "fs";
2558
+ import * as os from "os";
2559
+ import * as path3 from "path";
2560
+ var PLUGIN_TMP_PREFIX = "opencode-claude-code-";
2561
+ var PID_TMP_DIR = path3.join(
2562
+ os.tmpdir(),
2563
+ `${PLUGIN_TMP_PREFIX}${process.pid}`
2564
+ );
2565
+ var currentDir = null;
2566
+ var createdDirs = /* @__PURE__ */ new Set();
2567
+ var registered = false;
2568
+ function currentUid() {
2569
+ return typeof process.getuid === "function" ? process.getuid() : void 0;
2570
+ }
2571
+ function untrustworthyReason(dir) {
2572
+ let stat;
2573
+ try {
2574
+ stat = fs.lstatSync(dir);
2575
+ } catch {
2576
+ return null;
2577
+ }
2578
+ if (stat.isSymbolicLink()) return "path is a symlink";
2579
+ if (!stat.isDirectory()) return "path is not a directory";
2580
+ const uid = currentUid();
2581
+ if (uid !== void 0 && stat.uid !== uid) {
2582
+ return `directory is owned by uid ${stat.uid}, not ${uid}`;
2583
+ }
2584
+ return null;
2585
+ }
2586
+ function ensureDir(dir) {
2587
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
2588
+ try {
2589
+ fs.chmodSync(dir, 448);
2590
+ } catch {
2591
+ }
2592
+ }
2593
+ function chooseDir() {
2594
+ const reason = untrustworthyReason(PID_TMP_DIR);
2595
+ if (!reason) return PID_TMP_DIR;
2596
+ const fallback = fs.mkdtempSync(path3.join(os.tmpdir(), PLUGIN_TMP_PREFIX));
2597
+ log.warn("refusing the pid-named scratch directory", {
2598
+ path: PID_TMP_DIR,
2599
+ reason,
2600
+ fallback
2601
+ });
2602
+ return fallback;
2603
+ }
2604
+ function registerExitCleanup() {
2605
+ if (registered) return;
2606
+ registered = true;
2607
+ process.on("exit", () => {
2608
+ for (const dir of createdDirs) {
2609
+ try {
2610
+ fs.rmSync(dir, { recursive: true, force: true });
2611
+ } catch {
2612
+ }
2613
+ }
2614
+ });
2615
+ }
2616
+ var PID_DIR_NAME = /^opencode-claude-code-(\d+)$/;
2617
+ function isProcessAlive(pid) {
2618
+ try {
2619
+ process.kill(pid, 0);
2620
+ return true;
2621
+ } catch (err) {
2622
+ return err.code === "EPERM";
2623
+ }
2624
+ }
2625
+ function sweepStalePluginTmpDirs(keep) {
2626
+ const removed = [];
2627
+ if (process.env.OPENCODE_CLAUDE_CODE_NO_TMP_SWEEP === "1") return removed;
2628
+ const root = os.tmpdir();
2629
+ let entries;
2630
+ try {
2631
+ entries = fs.readdirSync(root);
2632
+ } catch {
2633
+ return removed;
2634
+ }
2635
+ const uid = currentUid();
2636
+ const keepPath = keep ? path3.resolve(keep) : null;
2637
+ for (const entry of entries) {
2638
+ const match = PID_DIR_NAME.exec(entry);
2639
+ if (!match) continue;
2640
+ const pid = Number(match[1]);
2641
+ if (!Number.isSafeInteger(pid) || pid <= 0) continue;
2642
+ if (pid === process.pid) continue;
2643
+ const dir = path3.join(root, entry);
2644
+ if (keepPath && path3.resolve(dir) === keepPath) continue;
2645
+ let stat;
2646
+ try {
2647
+ stat = fs.lstatSync(dir);
2648
+ } catch {
2649
+ continue;
2650
+ }
2651
+ if (stat.isSymbolicLink() || !stat.isDirectory()) continue;
2652
+ if (uid !== void 0 && stat.uid !== uid) continue;
2653
+ if (isProcessAlive(pid)) continue;
2654
+ try {
2655
+ fs.rmSync(dir, { recursive: true, force: true });
2656
+ removed.push(dir);
2657
+ } catch (err) {
2658
+ log.debug("could not remove a stale scratch directory", {
2659
+ dir,
2660
+ error: String(err)
2661
+ });
2662
+ }
2663
+ }
2664
+ if (removed.length > 0) {
2665
+ log.info("removed stale plugin scratch directories", {
2666
+ count: removed.length,
2667
+ dirs: removed
2668
+ });
2669
+ }
2670
+ return removed;
2671
+ }
2672
+ var swept = false;
2673
+ function sweepOnce(keep) {
2674
+ if (swept) return;
2675
+ swept = true;
2676
+ try {
2677
+ sweepStalePluginTmpDirs(keep);
2678
+ } catch (err) {
2679
+ log.warn("stale scratch directory sweep failed", { error: String(err) });
2680
+ }
2681
+ }
2682
+ function pluginTmpDir() {
2683
+ if (currentDir === null || untrustworthyReason(currentDir) !== null) {
2684
+ currentDir = chooseDir();
2685
+ }
2686
+ ensureDir(currentDir);
2687
+ createdDirs.add(currentDir);
2688
+ registerExitCleanup();
2689
+ sweepOnce(currentDir);
2690
+ return currentDir;
2691
+ }
2692
+
2444
2693
  // src/subagent-dispatch.ts
2445
2694
  var SUBAGENT_DISPATCH_MARKER = "\u258C **subagent dispatch:**";
2446
2695
  var SUBAGENT_DISPATCH_TOOL_CALL_PREFIX = "subagent_dispatch_";
@@ -2449,6 +2698,7 @@ var DEFAULT_ANSWER = "Default";
2449
2698
  var LAST_ANSWER_PREFIX = "Same as last time";
2450
2699
  var CUSTOMISE_ANSWER = "Customise\u2026";
2451
2700
  var PER_TASK_ANSWER = "Per task\u2026";
2701
+ var PER_TYPE_ANSWER = "Per type\u2026";
2452
2702
  var DISPATCH_COMBOS = [
2453
2703
  {
2454
2704
  model: "claude-haiku-5-5",
@@ -2487,30 +2737,49 @@ function isSubagentDispatchActive(input) {
2487
2737
  if (input.configured !== "ask") return false;
2488
2738
  return input.opencodeHasQuestion;
2489
2739
  }
2490
- var lastChoices = /* @__PURE__ */ new Map();
2491
- var MAX_REMEMBERED_CHOICES = 256;
2492
- function lastChoiceKey(parentSessionId, agent) {
2493
- return `${parentSessionId}${KEY_SEPARATOR3}${agent}`;
2494
- }
2495
2740
  function rememberDispatchChoice(parentSessionId, agent, choice) {
2496
- if (!parentSessionId || parentSessionId === "default") return;
2497
- const key = lastChoiceKey(parentSessionId, agent);
2498
- lastChoices.delete(key);
2499
- lastChoices.set(key, { ...choice });
2500
- while (lastChoices.size > MAX_REMEMBERED_CHOICES) {
2501
- const oldest = lastChoices.keys().next().value;
2502
- if (oldest === void 0) break;
2503
- lastChoices.delete(oldest);
2504
- }
2741
+ writeDispatchChoice(parentSessionId, agent, choice);
2505
2742
  }
2506
2743
  function recallDispatchChoice(parentSessionId, agent) {
2507
- return lastChoices.get(lastChoiceKey(parentSessionId, agent));
2744
+ return readDispatchChoice(parentSessionId, agent);
2745
+ }
2746
+ function forgetDispatchChoicesForSession(opencodeSessionId) {
2747
+ forgetDispatchChoices(opencodeSessionId);
2748
+ }
2749
+ function usableRemembered(context, agent) {
2750
+ const remembered = recallDispatchChoice(context.parentSessionId, agent);
2751
+ if (!remembered) return void 0;
2752
+ const choice = {};
2753
+ if (remembered.model && Object.hasOwn(defaultModels, remembered.model)) {
2754
+ choice.model = remembered.model;
2755
+ }
2756
+ if (remembered.effort && REASONING_EFFORTS.includes(remembered.effort)) {
2757
+ choice.effort = remembered.effort;
2758
+ }
2759
+ let droppedAccount;
2760
+ if (remembered.account) {
2761
+ if (dispatchAccountCandidates(context).includes(remembered.account)) {
2762
+ choice.account = remembered.account;
2763
+ } else {
2764
+ droppedAccount = remembered.account;
2765
+ }
2766
+ }
2767
+ if (!choice.model && !choice.effort && !choice.account) {
2768
+ return droppedAccount ? { choice, droppedAccount } : void 0;
2769
+ }
2770
+ return droppedAccount ? { choice, droppedAccount } : { choice };
2508
2771
  }
2509
2772
  var CLAIM_TTL_MS = 15 * 6e4;
2510
2773
  var MAX_CLAIMS = 512;
2511
2774
  var claims = [];
2512
2775
  var sessionChoices = /* @__PURE__ */ new Map();
2513
2776
  var MAX_SESSION_CHOICES = 512;
2777
+ function promptMatchesClaim(claimPrompt, childPrompt) {
2778
+ if (childPrompt === claimPrompt) return true;
2779
+ if (claimPrompt.length === 0) return false;
2780
+ return childPrompt.endsWith(`
2781
+ ${claimPrompt}`);
2782
+ }
2514
2783
  function pruneClaims(now) {
2515
2784
  for (let i = claims.length - 1; i >= 0; i--) {
2516
2785
  if (now - claims[i].createdAt > CLAIM_TTL_MS) claims.splice(i, 1);
@@ -2541,7 +2810,9 @@ function lookupSessionChoice(sessionId) {
2541
2810
  function hasCandidateClaim(agent, prompt, now = Date.now()) {
2542
2811
  if (!agent || prompt === void 0) return false;
2543
2812
  pruneClaims(now);
2544
- return claims.some((claim) => claim.agent === agent && claim.prompt === prompt);
2813
+ return claims.some(
2814
+ (claim) => claim.agent === agent && promptMatchesClaim(claim.prompt, prompt)
2815
+ );
2545
2816
  }
2546
2817
  function claimDispatchChoice(input) {
2547
2818
  const existing = sessionChoices.get(input.sessionId);
@@ -2550,9 +2821,14 @@ function claimDispatchChoice(input) {
2550
2821
  return void 0;
2551
2822
  }
2552
2823
  pruneClaims(input.now ?? Date.now());
2553
- const index = claims.findIndex(
2554
- (claim2) => claim2.parentSessionId === input.parentSessionId && claim2.agent === input.agent && claim2.prompt === input.prompt
2555
- );
2824
+ const belongsHere = (claim2) => claim2.parentSessionId === input.parentSessionId && claim2.agent === input.agent;
2825
+ const childPrompt = input.prompt;
2826
+ let index = claims.findIndex((claim2) => belongsHere(claim2) && claim2.prompt === childPrompt);
2827
+ if (index === -1) {
2828
+ index = claims.findIndex(
2829
+ (claim2) => belongsHere(claim2) && promptMatchesClaim(claim2.prompt, childPrompt)
2830
+ );
2831
+ }
2556
2832
  if (index === -1) return void 0;
2557
2833
  const [claim] = claims.splice(index, 1);
2558
2834
  sessionChoices.set(input.sessionId, claim.choice);
@@ -2636,7 +2912,11 @@ function comboLabel(model, effort) {
2636
2912
  return effort ? `${model} / ${effort}` : model;
2637
2913
  }
2638
2914
  function describeChoice(choice, fallback) {
2639
- return comboLabel(choice.model ?? fallback.model, choice.effort ?? fallback.effort);
2915
+ const combo = comboLabel(choice.model ?? fallback.model, choice.effort ?? fallback.effort);
2916
+ return choice.account ? `${combo} @${choice.account}` : combo;
2917
+ }
2918
+ function droppedAccountPhrase(account) {
2919
+ return ` The account "${account}" you picked last time is no longer available to this conversation, so this stays on the account you are on.`;
2640
2920
  }
2641
2921
  function availableCombos() {
2642
2922
  return DISPATCH_COMBOS.filter((combo) => Object.hasOwn(defaultModels, combo.model));
@@ -2654,16 +2934,25 @@ function dispatchAccountCandidates(context) {
2654
2934
  }
2655
2935
  return out;
2656
2936
  }
2657
- function accountQuestion(context) {
2937
+ function accountQuestion(context, scope) {
2658
2938
  const candidates = dispatchAccountCandidates(context);
2659
2939
  if (candidates.length < 2) return null;
2660
2940
  const source = candidates[0];
2941
+ const perType = "agent" in scope;
2942
+ const subject = perType ? `the "${scope.agent}" subagents` : "these subagents";
2943
+ const options = candidates.map((account) => ({
2944
+ label: account,
2945
+ description: account === source ? `Stay on this conversation's own account. This is what happens today.` : `Spawn ${subject} with "${account}"'s Claude config dir, so the work draws on that account's usage window.`
2946
+ }));
2947
+ if (!perType && scope.types > 1) {
2948
+ options.push({
2949
+ label: PER_TYPE_ANSWER,
2950
+ description: `Choose the account separately for each of these ${scope.types} agent types on the next screen.`
2951
+ });
2952
+ }
2661
2953
  return {
2662
- question: "Which Claude account should these subagents run on?",
2663
- options: candidates.map((account) => ({
2664
- label: account,
2665
- description: account === source ? `Stay on this conversation's own account. This is what happens today.` : `Spawn each subagent with "${account}"'s Claude config dir, so its work draws on that account's usage window.`
2666
- }))
2954
+ question: perType ? `Which Claude account should the "${scope.agent}" subagents run on?` : "Which Claude account should these subagents run on?",
2955
+ options
2667
2956
  };
2668
2957
  }
2669
2958
  function summaryQuestion(tasks, context) {
@@ -2674,13 +2963,14 @@ function summaryQuestion(tasks, context) {
2674
2963
  });
2675
2964
  const question = tasks.length === 1 ? `About to dispatch the "${tasks[0].agent}" subagent (${tasks[0].description || "no description"}). How should it run?` : `About to dispatch ${tasks.length} subagents (${counts.join(", ")}). How should they run?`;
2676
2965
  const options = [];
2677
- const remembered = types.map((agent) => recallDispatchChoice(context.parentSessionId, agent));
2678
- if (remembered.every((choice) => choice !== void 0)) {
2966
+ const remembered = types.map((agent) => usableRemembered(context, agent));
2967
+ if (remembered.every((entry) => entry !== void 0)) {
2968
+ const dropped = remembered.map((entry) => entry.droppedAccount).find((account) => !!account);
2679
2969
  options.push({
2680
2970
  label: LAST_ANSWER_PREFIX,
2681
2971
  description: types.map(
2682
- (agent, index) => `${agent}: ${describeChoice(remembered[index], context.describeDefault(agent))}`
2683
- ).join(". ")
2972
+ (agent, index) => `${agent}: ${describeChoice(remembered[index].choice, context.describeDefault(agent))}`
2973
+ ).join(". ") + (dropped ? `.${droppedAccountPhrase(dropped)}` : "")
2684
2974
  });
2685
2975
  }
2686
2976
  options.push({
@@ -2698,20 +2988,20 @@ function summaryQuestion(tasks, context) {
2698
2988
  }
2699
2989
  options.push({
2700
2990
  label: CUSTOMISE_ANSWER,
2701
- description: tasks.length === 1 ? "Choose the account, or type a model and effort of your own." : "Choose per agent type on the next screen, and per task after that."
2991
+ description: tasks.length === 1 ? "Choose the account, or type a model and effort of your own." : "Choose per agent type on the next screen, and per account or per task after that."
2702
2992
  });
2703
2993
  return { question, options };
2704
2994
  }
2705
2995
  function typeQuestion(agent, tasks, context) {
2706
2996
  const mine = tasks.filter((task) => task.agent === agent);
2707
2997
  const fallback = context.describeDefault(agent);
2708
- const question = mine.length === 1 ? `"${agent}" (${mine[0].description || "no description"}): how should it run?` : `"${agent}" (${mine.length} subagents): how should they run?`;
2998
+ const question = (mine.length === 1 ? `"${agent}" (${mine[0].description || "no description"}): how should it run?` : `"${agent}" (${mine.length} subagents): how should they run?`) + accountHint(context);
2709
2999
  const options = [];
2710
- const remembered = recallDispatchChoice(context.parentSessionId, agent);
3000
+ const remembered = usableRemembered(context, agent);
2711
3001
  if (remembered) {
2712
3002
  options.push({
2713
3003
  label: LAST_ANSWER_PREFIX,
2714
- description: `${describeChoice(remembered, fallback)}, the last thing you picked for "${agent}" in this conversation.`
3004
+ description: `${describeChoice(remembered.choice, fallback)}, the last thing you picked for "${agent}" in this conversation.` + (remembered.droppedAccount ? droppedAccountPhrase(remembered.droppedAccount) : "")
2715
3005
  });
2716
3006
  }
2717
3007
  options.push({
@@ -2735,7 +3025,7 @@ function typeQuestion(agent, tasks, context) {
2735
3025
  function taskQuestion(task, typeChoice, context) {
2736
3026
  const fallback = context.describeDefault(task.agent);
2737
3027
  const options = [];
2738
- if (typeChoice && (typeChoice.model || typeChoice.effort)) {
3028
+ if (typeChoice && (typeChoice.model || typeChoice.effort || typeChoice.account)) {
2739
3029
  options.push({
2740
3030
  label: `Same as the rest (${describeChoice(typeChoice, fallback)})`,
2741
3031
  description: `What you picked for every "${task.agent}" subagent in this dispatch.`
@@ -2749,10 +3039,15 @@ function taskQuestion(task, typeChoice, context) {
2749
3039
  options.push({ label: comboLabel(combo.model, combo.effort), description: combo.description });
2750
3040
  }
2751
3041
  return {
2752
- question: `"${task.agent}": ${task.description || task.prompt.slice(0, 60)}`,
3042
+ question: `"${task.agent}": ${task.description || task.prompt.slice(0, 60)}${accountHint(context)}`,
2753
3043
  options
2754
3044
  };
2755
3045
  }
3046
+ function accountHint(context) {
3047
+ const candidates = dispatchAccountCandidates(context);
3048
+ if (candidates.length < 2) return "";
3049
+ return ` Add "@${candidates[1]}" to a typed answer to send it to that account.`;
3050
+ }
2756
3051
  function header(text) {
2757
3052
  return text.length <= 30 ? text : `${text.slice(0, 29)}\u2026`;
2758
3053
  }
@@ -2787,17 +3082,26 @@ function createSubagentDispatchQuestion(sessionKey2, heldCallIds, tasks, context
2787
3082
  fields: [{ kind: "all" }],
2788
3083
  choices: /* @__PURE__ */ new Map(),
2789
3084
  account: void 0,
3085
+ typeAccounts: /* @__PURE__ */ new Map(),
3086
+ typeChoices: /* @__PURE__ */ new Map(),
3087
+ droppedAccounts: [],
2790
3088
  context
2791
3089
  };
2792
3090
  const summary = summaryQuestion(tasks, context);
2793
3091
  return buildCall(entry, [{ header: "Subagents", ...summary }], toolCallId);
2794
3092
  }
2795
- function parseCustomChoice(answer) {
3093
+ function parseCustomChoice(answer, candidates = []) {
2796
3094
  const words2 = answer.trim().split(/[\s,/]+/).map((word) => word.trim()).filter(Boolean);
2797
3095
  if (words2.length === 0) return null;
2798
3096
  const choice = {};
2799
3097
  for (const word of words2) {
2800
3098
  const lower = word.toLowerCase();
3099
+ if (lower.startsWith("@")) {
3100
+ const account = normalizeAccountName(lower.slice(1));
3101
+ if (!account || !candidates.includes(account)) return null;
3102
+ choice.account = account;
3103
+ continue;
3104
+ }
2801
3105
  if (REASONING_EFFORTS.includes(lower)) {
2802
3106
  choice.effort = lower;
2803
3107
  continue;
@@ -2808,7 +3112,7 @@ function parseCustomChoice(answer) {
2808
3112
  }
2809
3113
  return null;
2810
3114
  }
2811
- return choice.model || choice.effort ? choice : null;
3115
+ return choice.model || choice.effort || choice.account ? choice : null;
2812
3116
  }
2813
3117
  function parseComboLabel(answer) {
2814
3118
  for (const combo of availableCombos()) {
@@ -2818,7 +3122,7 @@ function parseComboLabel(answer) {
2818
3122
  }
2819
3123
  return null;
2820
3124
  }
2821
- function classifyAnswer(answer) {
3125
+ function classifyAnswer(answer, candidates = []) {
2822
3126
  const trimmed = answer.trim();
2823
3127
  if (!trimmed) return { kind: "default" };
2824
3128
  if (trimmed === DEFAULT_ANSWER) return { kind: "default" };
@@ -2828,7 +3132,7 @@ function classifyAnswer(answer) {
2828
3132
  if (trimmed.startsWith("Same as the rest")) return { kind: "sameAsRest" };
2829
3133
  const combo = parseComboLabel(trimmed);
2830
3134
  if (combo) return { kind: "choice", choice: combo };
2831
- const custom = parseCustomChoice(trimmed);
3135
+ const custom = parseCustomChoice(trimmed, candidates);
2832
3136
  if (custom) return { kind: "choice", choice: custom };
2833
3137
  return { kind: "unknown", answer: trimmed };
2834
3138
  }
@@ -2869,19 +3173,28 @@ function advance(entry, part) {
2869
3173
  case "types":
2870
3174
  return advanceTypes(entry, answers);
2871
3175
  default:
2872
- return advanceTasks(entry, answers);
3176
+ return advanceDetail(entry, answers);
2873
3177
  }
2874
3178
  }
2875
3179
  function applyToAll(entry, choice) {
2876
3180
  for (const index of entry.tasks.keys()) entry.choices.set(index, { ...choice });
2877
3181
  }
3182
+ function noteDroppedAccount(entry, agent, account) {
3183
+ if (entry.droppedAccounts.some((dropped) => dropped.agent === agent)) return;
3184
+ entry.droppedAccounts.push({ agent, account });
3185
+ }
2878
3186
  function advanceSummary(entry, answers) {
2879
- const answer = classifyAnswer(answers.get(entry.questions[0]) ?? "");
3187
+ const candidates = dispatchAccountCandidates(entry.context);
3188
+ const answer = classifyAnswer(answers.get(entry.questions[0]) ?? "", candidates);
2880
3189
  if (answer.kind === "customise") return typesForm(entry);
2881
3190
  if (answer.kind === "last") {
2882
3191
  for (const [index, task] of entry.tasks.entries()) {
2883
- const remembered = recallDispatchChoice(entry.context.parentSessionId, task.agent);
2884
- if (remembered) entry.choices.set(index, { ...remembered });
3192
+ const remembered = usableRemembered(entry.context, task.agent);
3193
+ if (!remembered) continue;
3194
+ if (remembered.droppedAccount) {
3195
+ noteDroppedAccount(entry, task.agent, remembered.droppedAccount);
3196
+ }
3197
+ entry.choices.set(index, { ...remembered.choice });
2885
3198
  }
2886
3199
  return release(entry, null);
2887
3200
  }
@@ -2908,7 +3221,7 @@ function typesForm(entry) {
2908
3221
  ...typeQuestion(agent, entry.tasks, entry.context)
2909
3222
  }));
2910
3223
  const fields = types.map((agent) => ({ kind: "type", agent }));
2911
- const account = accountQuestion(entry.context);
3224
+ const account = accountQuestion(entry.context, { types: types.length });
2912
3225
  if (account) {
2913
3226
  prompts.push({ header: "Account", ...account });
2914
3227
  fields.push({ kind: "account" });
@@ -2920,25 +3233,30 @@ function typesForm(entry) {
2920
3233
  call: buildCall(entry, prompts, questionCallId("types"))
2921
3234
  };
2922
3235
  }
3236
+ function readAccountAnswer(entry, raw, agent) {
3237
+ const trimmed = raw.trim();
3238
+ if (!trimmed || trimmed === DEFAULT_ANSWER) return void 0;
3239
+ const picked = normalizeAccountName(trimmed.startsWith("@") ? trimmed.slice(1) : trimmed);
3240
+ const candidates = dispatchAccountCandidates(entry.context);
3241
+ if (picked && candidates.includes(picked)) return picked;
3242
+ log.notice("subagent dispatch account answer refused", {
3243
+ sessionKey: entry.sessionKey,
3244
+ agent,
3245
+ answer: trimmed,
3246
+ candidates
3247
+ });
3248
+ return void 0;
3249
+ }
2923
3250
  function advanceTypes(entry, answers) {
3251
+ const candidates = dispatchAccountCandidates(entry.context);
2924
3252
  const perTask = [];
2925
- const typeChoices = /* @__PURE__ */ new Map();
3253
+ let perTypeAccounts = false;
2926
3254
  for (const [position, field] of entry.fields.entries()) {
2927
3255
  const raw = answers.get(entry.questions[position]) ?? "";
2928
- const answer = classifyAnswer(raw);
3256
+ const answer = classifyAnswer(raw, candidates);
2929
3257
  if (field.kind === "account") {
2930
- if (answer.kind === "unknown") {
2931
- const picked = normalizeAccountName(answer.answer);
2932
- const candidates = dispatchAccountCandidates(entry.context);
2933
- if (candidates.includes(picked)) entry.account = picked;
2934
- else {
2935
- log.notice("subagent dispatch account answer refused", {
2936
- sessionKey: entry.sessionKey,
2937
- answer: answer.answer,
2938
- candidates
2939
- });
2940
- }
2941
- }
3258
+ if (raw.trim() === PER_TYPE_ANSWER) perTypeAccounts = true;
3259
+ else entry.account = readAccountAnswer(entry, raw, null) ?? entry.account;
2942
3260
  continue;
2943
3261
  }
2944
3262
  if (field.kind !== "type") continue;
@@ -2947,12 +3265,15 @@ function advanceTypes(entry, answers) {
2947
3265
  continue;
2948
3266
  }
2949
3267
  if (answer.kind === "last") {
2950
- const remembered = recallDispatchChoice(entry.context.parentSessionId, field.agent);
2951
- if (remembered) typeChoices.set(field.agent, { ...remembered });
3268
+ const remembered = usableRemembered(entry.context, field.agent);
3269
+ if (remembered?.droppedAccount) {
3270
+ noteDroppedAccount(entry, field.agent, remembered.droppedAccount);
3271
+ }
3272
+ if (remembered) applyTypeChoice(entry, field.agent, remembered.choice);
2952
3273
  continue;
2953
3274
  }
2954
3275
  if (answer.kind === "choice") {
2955
- typeChoices.set(field.agent, answer.choice);
3276
+ applyTypeChoice(entry, field.agent, answer.choice);
2956
3277
  continue;
2957
3278
  }
2958
3279
  if (answer.kind === "unknown") {
@@ -2964,31 +3285,60 @@ function advanceTypes(entry, answers) {
2964
3285
  }
2965
3286
  }
2966
3287
  for (const [index, task] of entry.tasks.entries()) {
2967
- const choice = typeChoices.get(task.agent);
3288
+ const choice = entry.typeChoices.get(task.agent);
2968
3289
  if (choice) entry.choices.set(index, { ...choice });
2969
3290
  }
2970
- if (perTask.length > 0) return tasksForm(entry, perTask, typeChoices);
3291
+ if (perTask.length > 0 || perTypeAccounts) {
3292
+ return detailForm(entry, perTask, perTypeAccounts);
3293
+ }
2971
3294
  return release(entry, null);
2972
3295
  }
2973
- function tasksForm(entry, agents, typeChoices) {
3296
+ function applyTypeChoice(entry, agent, choice) {
3297
+ const { account, ...rest } = choice;
3298
+ if (account) entry.typeAccounts.set(agent, account);
3299
+ entry.typeChoices.set(agent, rest);
3300
+ }
3301
+ function detailForm(entry, perTaskAgents, perTypeAccounts) {
2974
3302
  const prompts = [];
2975
3303
  const fields = [];
3304
+ if (perTypeAccounts) {
3305
+ for (const agent of dispatchAgentTypes(entry.tasks)) {
3306
+ const account = accountQuestion(entry.context, { agent });
3307
+ if (!account) continue;
3308
+ prompts.push({ header: `${agent} account`, ...account });
3309
+ fields.push({ kind: "typeAccount", agent });
3310
+ }
3311
+ }
2976
3312
  for (const [index, task] of entry.tasks.entries()) {
2977
- if (!agents.includes(task.agent)) continue;
3313
+ if (!perTaskAgents.includes(task.agent)) continue;
3314
+ const typeChoice = entry.typeChoices.get(task.agent);
3315
+ const typeAccount = entry.typeAccounts.get(task.agent);
2978
3316
  prompts.push({
2979
3317
  header: task.description || task.agent,
2980
- ...taskQuestion(task, typeChoices.get(task.agent), entry.context)
3318
+ ...taskQuestion(
3319
+ task,
3320
+ typeChoice || typeAccount ? { ...typeChoice, ...typeAccount ? { account: typeAccount } : {} } : void 0,
3321
+ entry.context
3322
+ )
2981
3323
  });
2982
3324
  fields.push({ kind: "task", taskIndex: index });
2983
3325
  }
2984
- entry.stage = "tasks";
3326
+ if (prompts.length === 0) return release(entry, null);
3327
+ entry.stage = "detail";
2985
3328
  entry.fields = fields;
2986
- return { kind: "form", call: buildCall(entry, prompts, questionCallId("tasks")) };
3329
+ return { kind: "form", call: buildCall(entry, prompts, questionCallId("detail")) };
2987
3330
  }
2988
- function advanceTasks(entry, answers) {
3331
+ function advanceDetail(entry, answers) {
3332
+ const candidates = dispatchAccountCandidates(entry.context);
2989
3333
  for (const [position, field] of entry.fields.entries()) {
3334
+ const raw = answers.get(entry.questions[position]) ?? "";
3335
+ if (field.kind === "typeAccount") {
3336
+ const picked = readAccountAnswer(entry, raw, field.agent);
3337
+ if (picked) entry.typeAccounts.set(field.agent, picked);
3338
+ continue;
3339
+ }
2990
3340
  if (field.kind !== "task") continue;
2991
- const answer = classifyAnswer(answers.get(entry.questions[position]) ?? "");
3341
+ const answer = classifyAnswer(raw, candidates);
2992
3342
  if (answer.kind === "choice") {
2993
3343
  entry.choices.set(field.taskIndex, answer.choice);
2994
3344
  continue;
@@ -3008,13 +3358,18 @@ function advanceTasks(entry, answers) {
3008
3358
  }
3009
3359
  return release(entry, null);
3010
3360
  }
3361
+ function droppedAccountNote(dropped) {
3362
+ const list2 = dropped.map((entry) => `"${entry.agent}" (was "${entry.account}")`).join(", ");
3363
+ return `
3364
+ ${SUBAGENT_DISPATCH_MARKER} The account you last picked for ${list2} is no longer configured for this conversation, or no longer in its account group, so that work stays on the account you are on. Everything else you picked still applies.
3365
+ `;
3366
+ }
3011
3367
  function release(entry, note) {
3012
- if (entry.account) {
3013
- for (const [index, task] of entry.tasks.entries()) {
3014
- const current = entry.choices.get(index) ?? {};
3015
- entry.choices.set(index, { ...current, account: entry.account });
3016
- void task;
3017
- }
3368
+ for (const [index, task] of entry.tasks.entries()) {
3369
+ const current = entry.choices.get(index);
3370
+ const account = current?.account ?? entry.typeAccounts.get(task.agent) ?? entry.account;
3371
+ if (!account) continue;
3372
+ entry.choices.set(index, { ...current ?? {}, account });
3018
3373
  }
3019
3374
  const seen = /* @__PURE__ */ new Set();
3020
3375
  for (const [index, task] of entry.tasks.entries()) {
@@ -3024,6 +3379,15 @@ function release(entry, note) {
3024
3379
  seen.add(task.agent);
3025
3380
  rememberDispatchChoice(entry.context.parentSessionId, task.agent, choice);
3026
3381
  }
3382
+ if (!note && entry.droppedAccounts.length > 0) {
3383
+ log.notice("a remembered subagent dispatch account is no longer available", {
3384
+ sessionKey: entry.sessionKey,
3385
+ // `agent` rather than a key of its own: `dropped` already means a count
3386
+ // in the bundle allowlist, and these are agent names like every other.
3387
+ agent: entry.droppedAccounts.map((dropped) => dropped.agent)
3388
+ });
3389
+ note = droppedAccountNote(entry.droppedAccounts);
3390
+ }
3027
3391
  return {
3028
3392
  kind: "release",
3029
3393
  heldCallIds: entry.heldCallIds,
@@ -3060,8 +3424,8 @@ import { execFile } from "child_process";
3060
3424
  import { promisify } from "util";
3061
3425
 
3062
3426
  // src/windows-spawn.ts
3063
- import * as fs from "fs";
3064
- import * as path3 from "path";
3427
+ import * as fs2 from "fs";
3428
+ import * as path4 from "path";
3065
3429
  var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
3066
3430
  function envValue(env, name) {
3067
3431
  const direct = env[name];
@@ -3071,7 +3435,7 @@ function envValue(env, name) {
3071
3435
  }
3072
3436
  function defaultIsFile(candidate) {
3073
3437
  try {
3074
- return fs.statSync(candidate).isFile();
3438
+ return fs2.statSync(candidate).isFile();
3075
3439
  } catch {
3076
3440
  return false;
3077
3441
  }
@@ -3081,7 +3445,7 @@ function executableExtensions(env = process.env) {
3081
3445
  return raw.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean).map((ext) => ext.startsWith(".") ? ext : `.${ext}`);
3082
3446
  }
3083
3447
  function isBatchFile(file) {
3084
- const ext = path3.win32.extname(file).toLowerCase();
3448
+ const ext = path4.win32.extname(file).toLowerCase();
3085
3449
  return ext === ".cmd" || ext === ".bat";
3086
3450
  }
3087
3451
  function resolveWindowsCommand(command, deps = {}) {
@@ -3089,7 +3453,7 @@ function resolveWindowsCommand(command, deps = {}) {
3089
3453
  const isFile = deps.isFile ?? defaultIsFile;
3090
3454
  const extensions = executableExtensions(env);
3091
3455
  const candidatesFor = (base) => {
3092
- const ext = path3.win32.extname(base).toLowerCase();
3456
+ const ext = path4.win32.extname(base).toLowerCase();
3093
3457
  if (ext && extensions.includes(ext)) return [base];
3094
3458
  return [...extensions.map((candidate) => base + candidate), base];
3095
3459
  };
@@ -3100,10 +3464,10 @@ function resolveWindowsCommand(command, deps = {}) {
3100
3464
  return null;
3101
3465
  }
3102
3466
  const pathValue = envValue(env, "PATH") ?? "";
3103
- for (const entry of pathValue.split(path3.win32.delimiter)) {
3467
+ for (const entry of pathValue.split(path4.win32.delimiter)) {
3104
3468
  const directory = entry.trim().replace(/^"(.*)"$/, "$1");
3105
3469
  if (!directory) continue;
3106
- for (const candidate of candidatesFor(path3.win32.join(directory, command))) {
3470
+ for (const candidate of candidatesFor(path4.win32.join(directory, command))) {
3107
3471
  if (isFile(candidate)) return candidate;
3108
3472
  }
3109
3473
  }
@@ -3132,7 +3496,7 @@ function quoteBatchArgument(arg) {
3132
3496
  }
3133
3497
  function planCmdInvocation(file, args, env = process.env) {
3134
3498
  const comspec = envValue(env, "ComSpec") ?? "cmd.exe";
3135
- const line = [`"${path3.win32.normalize(file)}"`, ...args.map(quoteBatchArgument)];
3499
+ const line = [`"${path4.win32.normalize(file)}"`, ...args.map(quoteBatchArgument)];
3136
3500
  return {
3137
3501
  file: comspec,
3138
3502
  args: ["/d", "/s", "/c", `"${line.join(" ")}"`],
@@ -3376,145 +3740,6 @@ import * as fs3 from "fs";
3376
3740
  import * as path5 from "path";
3377
3741
  import * as crypto from "crypto";
3378
3742
  import { EventEmitter } from "events";
3379
-
3380
- // src/tmp.ts
3381
- import * as fs2 from "fs";
3382
- import * as os from "os";
3383
- import * as path4 from "path";
3384
- var PLUGIN_TMP_PREFIX = "opencode-claude-code-";
3385
- var PID_TMP_DIR = path4.join(
3386
- os.tmpdir(),
3387
- `${PLUGIN_TMP_PREFIX}${process.pid}`
3388
- );
3389
- var currentDir = null;
3390
- var createdDirs = /* @__PURE__ */ new Set();
3391
- var registered = false;
3392
- function currentUid() {
3393
- return typeof process.getuid === "function" ? process.getuid() : void 0;
3394
- }
3395
- function untrustworthyReason(dir) {
3396
- let stat;
3397
- try {
3398
- stat = fs2.lstatSync(dir);
3399
- } catch {
3400
- return null;
3401
- }
3402
- if (stat.isSymbolicLink()) return "path is a symlink";
3403
- if (!stat.isDirectory()) return "path is not a directory";
3404
- const uid = currentUid();
3405
- if (uid !== void 0 && stat.uid !== uid) {
3406
- return `directory is owned by uid ${stat.uid}, not ${uid}`;
3407
- }
3408
- return null;
3409
- }
3410
- function ensureDir(dir) {
3411
- fs2.mkdirSync(dir, { recursive: true, mode: 448 });
3412
- try {
3413
- fs2.chmodSync(dir, 448);
3414
- } catch {
3415
- }
3416
- }
3417
- function chooseDir() {
3418
- const reason = untrustworthyReason(PID_TMP_DIR);
3419
- if (!reason) return PID_TMP_DIR;
3420
- const fallback = fs2.mkdtempSync(path4.join(os.tmpdir(), PLUGIN_TMP_PREFIX));
3421
- log.warn("refusing the pid-named scratch directory", {
3422
- path: PID_TMP_DIR,
3423
- reason,
3424
- fallback
3425
- });
3426
- return fallback;
3427
- }
3428
- function registerExitCleanup() {
3429
- if (registered) return;
3430
- registered = true;
3431
- process.on("exit", () => {
3432
- for (const dir of createdDirs) {
3433
- try {
3434
- fs2.rmSync(dir, { recursive: true, force: true });
3435
- } catch {
3436
- }
3437
- }
3438
- });
3439
- }
3440
- var PID_DIR_NAME = /^opencode-claude-code-(\d+)$/;
3441
- function isProcessAlive(pid) {
3442
- try {
3443
- process.kill(pid, 0);
3444
- return true;
3445
- } catch (err) {
3446
- return err.code === "EPERM";
3447
- }
3448
- }
3449
- function sweepStalePluginTmpDirs(keep) {
3450
- const removed = [];
3451
- if (process.env.OPENCODE_CLAUDE_CODE_NO_TMP_SWEEP === "1") return removed;
3452
- const root = os.tmpdir();
3453
- let entries;
3454
- try {
3455
- entries = fs2.readdirSync(root);
3456
- } catch {
3457
- return removed;
3458
- }
3459
- const uid = currentUid();
3460
- const keepPath = keep ? path4.resolve(keep) : null;
3461
- for (const entry of entries) {
3462
- const match = PID_DIR_NAME.exec(entry);
3463
- if (!match) continue;
3464
- const pid = Number(match[1]);
3465
- if (!Number.isSafeInteger(pid) || pid <= 0) continue;
3466
- if (pid === process.pid) continue;
3467
- const dir = path4.join(root, entry);
3468
- if (keepPath && path4.resolve(dir) === keepPath) continue;
3469
- let stat;
3470
- try {
3471
- stat = fs2.lstatSync(dir);
3472
- } catch {
3473
- continue;
3474
- }
3475
- if (stat.isSymbolicLink() || !stat.isDirectory()) continue;
3476
- if (uid !== void 0 && stat.uid !== uid) continue;
3477
- if (isProcessAlive(pid)) continue;
3478
- try {
3479
- fs2.rmSync(dir, { recursive: true, force: true });
3480
- removed.push(dir);
3481
- } catch (err) {
3482
- log.debug("could not remove a stale scratch directory", {
3483
- dir,
3484
- error: String(err)
3485
- });
3486
- }
3487
- }
3488
- if (removed.length > 0) {
3489
- log.info("removed stale plugin scratch directories", {
3490
- count: removed.length,
3491
- dirs: removed
3492
- });
3493
- }
3494
- return removed;
3495
- }
3496
- var swept = false;
3497
- function sweepOnce(keep) {
3498
- if (swept) return;
3499
- swept = true;
3500
- try {
3501
- sweepStalePluginTmpDirs(keep);
3502
- } catch (err) {
3503
- log.warn("stale scratch directory sweep failed", { error: String(err) });
3504
- }
3505
- }
3506
- function pluginTmpDir() {
3507
- if (currentDir === null || untrustworthyReason(currentDir) !== null) {
3508
- currentDir = chooseDir();
3509
- }
3510
- ensureDir(currentDir);
3511
- createdDirs.add(currentDir);
3512
- registerExitCleanup();
3513
- sweepOnce(currentDir);
3514
- return currentDir;
3515
- }
3516
-
3517
- // src/proxy-mcp.ts
3518
3743
  var PROXY_KEEPALIVE_MS = 15e3;
3519
3744
  function acceptsEventStream(acceptHeader) {
3520
3745
  return typeof acceptHeader === "string" && acceptHeader.toLowerCase().includes("text/event-stream");
@@ -6023,17 +6248,17 @@ function tailIsReplyOnly(history, from) {
6023
6248
  }
6024
6249
 
6025
6250
  // src/session-resume-store.ts
6026
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
6027
- import { homedir as homedir5 } from "os";
6028
- import { join as join6 } from "path";
6251
+ import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
6252
+ import { homedir as homedir6 } from "os";
6253
+ import { join as join8 } from "path";
6029
6254
  var MAX_RESUME_RECORDS = 256;
6030
6255
  var RESUME_RECORD_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
6031
6256
  var records = null;
6032
- var pathOverride = null;
6257
+ var pathOverride2 = null;
6033
6258
  function resumeStorePath() {
6034
- if (pathOverride) return pathOverride;
6035
- const stateRoot = process.env.XDG_STATE_HOME || join6(homedir5(), ".local", "state");
6036
- return join6(stateRoot, "opencode-claude-code-plugin", "claude-sessions.json");
6259
+ if (pathOverride2) return pathOverride2;
6260
+ const stateRoot = process.env.XDG_STATE_HOME || join8(homedir6(), ".local", "state");
6261
+ return join8(stateRoot, "opencode-claude-code-plugin", "claude-sessions.json");
6037
6262
  }
6038
6263
  function isDigest(value) {
6039
6264
  const entry = value;
@@ -6043,11 +6268,11 @@ function isRecord2(value) {
6043
6268
  const record = value;
6044
6269
  return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest) && (record.shape === void 0 || Array.isArray(record.shape) && record.shape.every(isDigest)) && (record.configDir === void 0 || typeof record.configDir === "string") && (record.account === void 0 || typeof record.account === "string");
6045
6270
  }
6046
- function load() {
6271
+ function load2() {
6047
6272
  if (records) return records;
6048
6273
  records = /* @__PURE__ */ new Map();
6049
6274
  try {
6050
- const parsed = JSON.parse(readFileSync3(resumeStorePath(), "utf8"));
6275
+ const parsed = JSON.parse(readFileSync4(resumeStorePath(), "utf8"));
6051
6276
  const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
6052
6277
  if (entries && typeof entries === "object") {
6053
6278
  const cutoff = Date.now() - RESUME_RECORD_MAX_AGE_MS;
@@ -6058,11 +6283,11 @@ function load() {
6058
6283
  }
6059
6284
  return records;
6060
6285
  }
6061
- function save(update) {
6286
+ function save2(update) {
6062
6287
  const file = resumeStorePath();
6063
6288
  const merged = /* @__PURE__ */ new Map();
6064
6289
  try {
6065
- const onDisk = JSON.parse(readFileSync3(file, "utf8"))?.sessions;
6290
+ const onDisk = JSON.parse(readFileSync4(file, "utf8"))?.sessions;
6066
6291
  if (onDisk && typeof onDisk === "object") {
6067
6292
  for (const [key, record] of Object.entries(onDisk)) {
6068
6293
  if (isRecord2(record)) merged.set(key, record);
@@ -6075,12 +6300,12 @@ function save(update) {
6075
6300
  const kept = [...merged.entries()].filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt).slice(-MAX_RESUME_RECORDS);
6076
6301
  records = new Map(kept);
6077
6302
  try {
6078
- mkdirSync3(join6(file, ".."), { recursive: true, mode: 448 });
6303
+ mkdirSync4(join8(file, ".."), { recursive: true, mode: 448 });
6079
6304
  const tmp = `${file}.${process.pid}.tmp`;
6080
- writeFileSync3(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
6305
+ writeFileSync4(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
6081
6306
  mode: 384
6082
6307
  });
6083
- renameSync2(tmp, file);
6308
+ renameSync3(tmp, file);
6084
6309
  } catch (err) {
6085
6310
  log.debug("could not write the claude session resume store", {
6086
6311
  error: err instanceof Error ? err.message : String(err)
@@ -6093,11 +6318,11 @@ function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath, where)
6093
6318
  const shape2 = conversationDigests(prompt, { assistantContent: false });
6094
6319
  const configDir = where?.configDir;
6095
6320
  const account = where?.account;
6096
- const current = load().get(sessionKey2);
6321
+ const current = load2().get(sessionKey2);
6097
6322
  if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.shape !== void 0 && current.configDir === configDir && current.account === account && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
6098
6323
  return;
6099
6324
  }
6100
- save((merged) => {
6325
+ save2((merged) => {
6101
6326
  merged.delete(sessionKey2);
6102
6327
  merged.set(sessionKey2, {
6103
6328
  claudeSessionId,
@@ -6111,22 +6336,22 @@ function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath, where)
6111
6336
  });
6112
6337
  }
6113
6338
  function forgetResumePoint(sessionKey2) {
6114
- if (!load().has(sessionKey2)) return;
6115
- save((merged) => {
6339
+ if (!load2().has(sessionKey2)) return;
6340
+ save2((merged) => {
6116
6341
  merged.delete(sessionKey2);
6117
6342
  });
6118
6343
  }
6119
6344
  function moveResumePoint(fromKey, toKey) {
6120
- const record = load().get(fromKey);
6345
+ const record = load2().get(fromKey);
6121
6346
  if (!record) return;
6122
- save((merged) => {
6347
+ save2((merged) => {
6123
6348
  merged.delete(fromKey);
6124
6349
  if (merged.has(toKey)) return;
6125
6350
  merged.set(toKey, { ...record, updatedAt: Date.now() });
6126
6351
  });
6127
6352
  }
6128
6353
  function findResumePoint(opts) {
6129
- const record = load().get(opts.sessionKey);
6354
+ const record = load2().get(opts.sessionKey);
6130
6355
  if (!record) {
6131
6356
  opts.onRefused?.("no-session-recorded");
6132
6357
  return void 0;
@@ -6159,7 +6384,7 @@ function findSiblingResumePoint(opts) {
6159
6384
  let best;
6160
6385
  let bestUpdatedAt = -1;
6161
6386
  let refusal;
6162
- for (const [key, record] of load()) {
6387
+ for (const [key, record] of load2()) {
6163
6388
  if (key === opts.sessionKey) continue;
6164
6389
  if (modelSiblingSignature(key, providers) !== signature) continue;
6165
6390
  const siblingAccount = providers ? sessionKeyAccount(key, providers) : void 0;
@@ -6228,7 +6453,7 @@ function findForeignAccountSibling(opts) {
6228
6453
  }
6229
6454
  let found;
6230
6455
  let foundUpdatedAt = -1;
6231
- for (const [key, record] of load()) {
6456
+ for (const [key, record] of load2()) {
6232
6457
  const hit = consider(key);
6233
6458
  if (hit && record.updatedAt >= foundUpdatedAt) {
6234
6459
  found = hit;
@@ -6848,6 +7073,7 @@ function deleteActiveProcessesForSession(sessionID) {
6848
7073
  }
6849
7074
  for (const key of released) clearBackgroundTasks(key);
6850
7075
  forgetSessionChoice(sessionID);
7076
+ forgetDispatchChoicesForSession(sessionID);
6851
7077
  return released;
6852
7078
  }
6853
7079
  function killAllActiveProcesses() {
@@ -7583,7 +7809,7 @@ async function handleBtwCommand(client, input, options = {}) {
7583
7809
  // src/diagnostic-bundle.ts
7584
7810
  import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
7585
7811
  import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
7586
- import { homedir as homedir6 } from "os";
7812
+ import { homedir as homedir7 } from "os";
7587
7813
 
7588
7814
  // src/log-messages.ts
7589
7815
  var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
@@ -7593,6 +7819,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
7593
7819
  "Code Mode execute not forwarded: read-only permission preset",
7594
7820
  "MCP server asked for operator input and was declined: a headless Claude Code session cannot prompt, so its elicitation can only be refused. Run that server's flow in Claude Code directly, or configure it not to elicit.",
7595
7821
  "MCP tool not routed through the proxy: another proxy tool already holds that name, and it keeps it",
7822
+ "a remembered subagent dispatch account is no longer available",
7596
7823
  "account failover declined; ending the turn",
7597
7824
  "accountGroups must be an object mapping an account name to a group name; ignoring it",
7598
7825
  "accountGroups names an account that is not configured; ignoring it",
@@ -7974,7 +8201,7 @@ function createIdHasher(salt = randomBytes2(16).toString("hex")) {
7974
8201
  return (raw) => createHash4("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
7975
8202
  }
7976
8203
  function createRedactionContext(options = {}) {
7977
- const home = options.home ?? homedir6();
8204
+ const home = options.home ?? homedir7();
7978
8205
  const homes = /* @__PURE__ */ new Set();
7979
8206
  if (home) homes.add(home);
7980
8207
  if (home) {
@@ -10116,9 +10343,9 @@ async function resolveSkillPluginDirs(opts) {
10116
10343
  // src/claude-session-wrapper.ts
10117
10344
  import { EventEmitter as EventEmitter4 } from "events";
10118
10345
  import { randomUUID as randomUUID5 } from "crypto";
10119
- import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "fs";
10346
+ import { existsSync as existsSync4, writeFileSync as writeFileSync6 } from "fs";
10120
10347
  import { unlink as unlink2 } from "fs/promises";
10121
- import { join as join11 } from "path";
10348
+ import { join as join13 } from "path";
10122
10349
 
10123
10350
  // src/claude-session-bun.ts
10124
10351
  import * as os5 from "os";
@@ -11299,8 +11526,8 @@ ${text2}`
11299
11526
  }
11300
11527
  function stageImage(data, extension) {
11301
11528
  try {
11302
- const file = join11(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
11303
- writeFileSync5(file, data, { mode: 384, flag: "wx" });
11529
+ const file = join13(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
11530
+ writeFileSync6(file, data, { mode: 384, flag: "wx" });
11304
11531
  return file;
11305
11532
  } catch (err) {
11306
11533
  log.warn("interactive transport could not stage an image", {
@@ -11841,13 +12068,13 @@ function dedupe2(dirs) {
11841
12068
  }
11842
12069
 
11843
12070
  // src/prompts.ts
11844
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
11845
- import { homedir as homedir9 } from "os";
12071
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
12072
+ import { homedir as homedir10 } from "os";
11846
12073
  import { randomUUID as randomUUID7 } from "crypto";
11847
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join12 } from "path";
12074
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join14 } from "path";
11848
12075
  function readPromptFileIfPresent(path15) {
11849
12076
  try {
11850
- const content = readFileSync7(path15, "utf8").trim();
12077
+ const content = readFileSync8(path15, "utf8").trim();
11851
12078
  return content || void 0;
11852
12079
  } catch {
11853
12080
  return void 0;
@@ -11856,7 +12083,7 @@ function readPromptFileIfPresent(path15) {
11856
12083
  function nearestWorkspaceAgentsPrompt(cwd) {
11857
12084
  let dir = cwd;
11858
12085
  while (true) {
11859
- const content = readPromptFileIfPresent(join12(dir, "AGENTS.md"));
12086
+ const content = readPromptFileIfPresent(join14(dir, "AGENTS.md"));
11860
12087
  if (content) return content;
11861
12088
  const parent = dirname6(dir);
11862
12089
  if (parent === dir) return void 0;
@@ -11952,9 +12179,9 @@ function extractSystemMessages(prompt) {
11952
12179
  }
11953
12180
  var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md"]);
11954
12181
  function userAuthoredInstructions(forwarded, opts = {}) {
11955
- const readFile2 = opts.readFile ?? ((path15) => {
12182
+ const readFile3 = opts.readFile ?? ((path15) => {
11956
12183
  try {
11957
- return readFileSync7(path15, "utf8");
12184
+ return readFileSync8(path15, "utf8");
11958
12185
  } catch {
11959
12186
  return null;
11960
12187
  }
@@ -11970,7 +12197,7 @@ function userAuthoredInstructions(forwarded, opts = {}) {
11970
12197
  if (seen.has(path15) || !isAbsolute3(path15)) continue;
11971
12198
  if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path15))) continue;
11972
12199
  seen.add(path15);
11973
- const file = readFile2(path15);
12200
+ const file = readFile3(path15);
11974
12201
  if (!file?.trim()) continue;
11975
12202
  const start = match.index + match[0].length + 1;
11976
12203
  const content = forwarded.startsWith(file, start) ? file : forwarded.startsWith(file.trimEnd(), start) ? file.trimEnd() : null;
@@ -11995,8 +12222,8 @@ ${options.compressionSummary.trim()}`
11995
12222
  for (const s of extraSystemContent) {
11996
12223
  if (s.trim()) parts.push(s.trim());
11997
12224
  }
11998
- const configRoot = process.env.XDG_CONFIG_HOME ?? join12(homedir9(), ".config");
11999
- const globalAgents = readPromptFileIfPresent(join12(configRoot, "opencode", "AGENTS.md"));
12225
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join14(homedir10(), ".config");
12226
+ const globalAgents = readPromptFileIfPresent(join14(configRoot, "opencode", "AGENTS.md"));
12000
12227
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
12001
12228
  const forwarded = extraSystemContent.join("\n\n");
12002
12229
  const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
@@ -12012,9 +12239,9 @@ ${options.compressionSummary.trim()}`
12012
12239
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
12013
12240
  const content = parts.join("\n\n");
12014
12241
  if (!content) return void 0;
12015
- const path15 = join12(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
12242
+ const path15 = join14(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
12016
12243
  try {
12017
- writeFileSync6(path15, content, { encoding: "utf8", mode: 384 });
12244
+ writeFileSync7(path15, content, { encoding: "utf8", mode: 384 });
12018
12245
  return path15;
12019
12246
  } catch (err) {
12020
12247
  log.warn("failed to write system prompt file", { error: String(err) });
@@ -15346,7 +15573,7 @@ var ClaudeCodeLanguageModel = class {
15346
15573
  let replayReason = "no-session-for-key";
15347
15574
  if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
15348
15575
  const configDir = resolveConfigDir(this.config.configDir);
15349
- const transcriptPath2 = (id, dir) => join13(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
15576
+ const transcriptPath2 = (id, dir) => join15(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
15350
15577
  const resumePoint = findResumePoint({
15351
15578
  sessionKey: sk,
15352
15579
  prompt: options.prompt,
@@ -16693,7 +16920,7 @@ var ClaudeCodeLanguageModel = class {
16693
16920
  };
16694
16921
 
16695
16922
  // src/account-tier.ts
16696
- import { readFileSync as readFileSync8 } from "fs";
16923
+ import { readFileSync as readFileSync9 } from "fs";
16697
16924
  import path13 from "path";
16698
16925
  var ORGANIZATION_TYPE_LABELS = {
16699
16926
  claude_max: "Max",
@@ -16736,7 +16963,7 @@ function readTierFromConfigDir(configDir, allowLegacyFallback) {
16736
16963
  function readOauthAccount(file) {
16737
16964
  let raw;
16738
16965
  try {
16739
- raw = readFileSync8(file, "utf8");
16966
+ raw = readFileSync9(file, "utf8");
16740
16967
  } catch {
16741
16968
  return void 0;
16742
16969
  }
@@ -16764,14 +16991,14 @@ function tierLabel(oauthAccount) {
16764
16991
  // src/cleanup-stale.ts
16765
16992
  import {
16766
16993
  existsSync as existsSync5,
16767
- mkdirSync as mkdirSync5,
16768
- readFileSync as readFileSync9,
16994
+ mkdirSync as mkdirSync6,
16995
+ readFileSync as readFileSync10,
16769
16996
  realpathSync as realpathSync4,
16770
16997
  rmSync as rmSync3,
16771
- writeFileSync as writeFileSync7
16998
+ writeFileSync as writeFileSync8
16772
16999
  } from "fs";
16773
- import { homedir as homedir10 } from "os";
16774
- import { dirname as dirname7, join as join14, resolve as resolve5 } from "path";
17000
+ import { homedir as homedir11 } from "os";
17001
+ import { dirname as dirname7, join as join16, resolve as resolve5 } from "path";
16775
17002
  import { fileURLToPath as fileURLToPath4 } from "url";
16776
17003
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
16777
17004
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
@@ -16779,20 +17006,20 @@ var alreadyRan = false;
16779
17006
  function candidateCacheRoots() {
16780
17007
  const xdg = process.env.XDG_CACHE_HOME;
16781
17008
  return [
16782
- xdg ? join14(xdg, "opencode") : null,
16783
- join14(homedir10(), ".cache", "opencode"),
16784
- join14(homedir10(), "Library", "Caches", "opencode")
17009
+ xdg ? join16(xdg, "opencode") : null,
17010
+ join16(homedir11(), ".cache", "opencode"),
17011
+ join16(homedir11(), "Library", "Caches", "opencode")
16785
17012
  ].filter((p) => Boolean(p));
16786
17013
  }
16787
17014
  function userOpencodeJsonPath() {
16788
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join14(homedir10(), ".config");
16789
- return join14(xdgConfig, "opencode", "opencode.json");
17015
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join16(homedir11(), ".config");
17016
+ return join16(xdgConfig, "opencode", "opencode.json");
16790
17017
  }
16791
17018
  function userIntendsToUseUnscoped() {
16792
17019
  const cfg = userOpencodeJsonPath();
16793
17020
  if (!existsSync5(cfg)) return false;
16794
17021
  try {
16795
- const json = JSON.parse(readFileSync9(cfg, "utf8"));
17022
+ const json = JSON.parse(readFileSync10(cfg, "utf8"));
16796
17023
  const plugins = json.plugin;
16797
17024
  if (!Array.isArray(plugins)) return false;
16798
17025
  return plugins.some(
@@ -16813,19 +17040,19 @@ function ourLoadedDir() {
16813
17040
  function ourVersion(dir) {
16814
17041
  if (!dir) return null;
16815
17042
  try {
16816
- const pkg = JSON.parse(readFileSync9(join14(dir, "package.json"), "utf8"));
17043
+ const pkg = JSON.parse(readFileSync10(join16(dir, "package.json"), "utf8"));
16817
17044
  return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
16818
17045
  } catch {
16819
17046
  return null;
16820
17047
  }
16821
17048
  }
16822
17049
  function markerPath() {
16823
- const stateRoot = process.env.XDG_STATE_HOME ?? join14(homedir10(), ".local", "state");
16824
- return join14(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
17050
+ const stateRoot = process.env.XDG_STATE_HOME ?? join16(homedir11(), ".local", "state");
17051
+ return join16(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
16825
17052
  }
16826
17053
  function markerVersion() {
16827
17054
  try {
16828
- const marker = JSON.parse(readFileSync9(markerPath(), "utf8"));
17055
+ const marker = JSON.parse(readFileSync10(markerPath(), "utf8"));
16829
17056
  return typeof marker.version === "string" ? marker.version : null;
16830
17057
  } catch {
16831
17058
  return null;
@@ -16834,8 +17061,8 @@ function markerVersion() {
16834
17061
  function writeMarker(version) {
16835
17062
  const file = markerPath();
16836
17063
  try {
16837
- mkdirSync5(dirname7(file), { recursive: true });
16838
- writeFileSync7(
17064
+ mkdirSync6(dirname7(file), { recursive: true });
17065
+ writeFileSync8(
16839
17066
  file,
16840
17067
  JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
16841
17068
  "utf8"
@@ -16870,7 +17097,7 @@ function cleanupStaleUnscopedInstall() {
16870
17097
  }
16871
17098
  function cleanupOne(cacheRoot, ourDir) {
16872
17099
  if (!existsSync5(cacheRoot)) return;
16873
- const stalePath = join14(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
17100
+ const stalePath = join16(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
16874
17101
  if (!existsSync5(stalePath)) return;
16875
17102
  let realStalePath = stalePath;
16876
17103
  try {
@@ -16878,11 +17105,11 @@ function cleanupOne(cacheRoot, ourDir) {
16878
17105
  } catch {
16879
17106
  }
16880
17107
  if (ourDir && realStalePath === ourDir) return;
16881
- const pkgJsonPath = join14(stalePath, "package.json");
17108
+ const pkgJsonPath = join16(stalePath, "package.json");
16882
17109
  if (!existsSync5(pkgJsonPath)) return;
16883
17110
  let pkg = {};
16884
17111
  try {
16885
- pkg = JSON.parse(readFileSync9(pkgJsonPath, "utf8"));
17112
+ pkg = JSON.parse(readFileSync10(pkgJsonPath, "utf8"));
16886
17113
  } catch {
16887
17114
  return;
16888
17115
  }
@@ -16898,13 +17125,13 @@ function cleanupOne(cacheRoot, ourDir) {
16898
17125
  });
16899
17126
  return;
16900
17127
  }
16901
- const cachePkgJson = join14(cacheRoot, "package.json");
17128
+ const cachePkgJson = join16(cacheRoot, "package.json");
16902
17129
  if (!existsSync5(cachePkgJson)) return;
16903
17130
  try {
16904
- const cfg = JSON.parse(readFileSync9(cachePkgJson, "utf8"));
17131
+ const cfg = JSON.parse(readFileSync10(cachePkgJson, "utf8"));
16905
17132
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
16906
17133
  delete cfg.dependencies[STALE_PACKAGE_NAME];
16907
- writeFileSync7(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
17134
+ writeFileSync8(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
16908
17135
  log.info("cleanup-stale: pruned dep from cache package.json");
16909
17136
  }
16910
17137
  } catch (err) {