@khalilgharbaoui/opencode-claude-code-plugin 0.46.1 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -154,7 +154,16 @@ function translateStreamForHost(stream, dialect = "v1") {
154
154
  import { homedir as homedir2 } from "os";
155
155
 
156
156
  // src/accounts.ts
157
- import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
157
+ import {
158
+ chmod,
159
+ copyFile,
160
+ link,
161
+ lstat,
162
+ mkdir,
163
+ readlink,
164
+ symlink,
165
+ writeFile
166
+ } from "fs/promises";
158
167
  import path from "path";
159
168
 
160
169
  // src/logger.ts
@@ -401,8 +410,9 @@ function resolveAccounts(value) {
401
410
  function accountProviderId(account) {
402
411
  return `${BASE_PROVIDER_ID}-${normalizeAccountName(account)}`;
403
412
  }
404
- function accountDisplayName(account) {
405
- return `Claude Code (${titleizeAccount(account)})`;
413
+ function accountDisplayName(account, tier) {
414
+ const name = titleizeAccount(account);
415
+ return `Claude Code (${tier ? `${name}, ${tier}` : name})`;
406
416
  }
407
417
  function accountModelSuffix(account) {
408
418
  const normalized = normalizeAccountName(account);
@@ -425,13 +435,14 @@ function expandHome(value) {
425
435
  }
426
436
  return value;
427
437
  }
428
- async function ensureAccountRuntime(account, baseCliPath) {
438
+ async function ensureAccountRuntime(account, baseCliPath, deps = {}) {
439
+ const platform2 = deps.platform ?? process.platform;
429
440
  const configDir = accountConfigDir(account);
430
441
  if (!configDir) return { cliPath: baseCliPath };
431
442
  const expandedConfigDir = expandHome(configDir);
432
443
  await mkdir(expandedConfigDir, { recursive: true });
433
444
  try {
434
- await ensureSharedCapabilities(expandedConfigDir);
445
+ await ensureSharedCapabilities(expandedConfigDir, platform2);
435
446
  } catch (err) {
436
447
  log.warn("failed to symlink shared capabilities; continuing anyway", {
437
448
  account,
@@ -439,6 +450,13 @@ async function ensureAccountRuntime(account, baseCliPath) {
439
450
  error: String(err)
440
451
  });
441
452
  }
453
+ if (platform2 === "win32") {
454
+ return {
455
+ cliPath: baseCliPath,
456
+ configDir: expandedConfigDir,
457
+ accountInProcess: true
458
+ };
459
+ }
442
460
  const cliPath = await writeAccountWrapper(
443
461
  normalizeAccountName(account),
444
462
  baseCliPath,
@@ -446,13 +464,21 @@ async function ensureAccountRuntime(account, baseCliPath) {
446
464
  );
447
465
  return { cliPath, configDir: expandedConfigDir };
448
466
  }
449
- async function ensureSharedCapabilities(targetRoot) {
467
+ async function ensureSharedCapabilities(targetRoot, platform2) {
450
468
  const sourceRoot = expandHome("~/.claude");
451
469
  for (const item of SHARED_CAPABILITY_ITEMS) {
452
- await ensureSharedCapabilityItem(sourceRoot, targetRoot, item);
470
+ try {
471
+ await ensureSharedCapabilityItem(sourceRoot, targetRoot, item, platform2);
472
+ } catch (err) {
473
+ log.warn("could not share a Claude capability with the account", {
474
+ item,
475
+ target: path.join(targetRoot, item),
476
+ error: String(err)
477
+ });
478
+ }
453
479
  }
454
480
  }
455
- async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
481
+ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item, platform2) {
456
482
  const source = path.join(sourceRoot, item);
457
483
  const target = path.join(targetRoot, item);
458
484
  let sourceStat;
@@ -477,8 +503,29 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
477
503
  return;
478
504
  } catch {
479
505
  }
480
- const type = sourceStat.isDirectory() ? process.platform === "win32" ? "junction" : "dir" : "file";
481
- await symlink(source, target, type);
506
+ if (sourceStat.isDirectory()) {
507
+ await symlink(source, target, platform2 === "win32" ? "junction" : "dir");
508
+ return;
509
+ }
510
+ if (platform2 !== "win32") {
511
+ await symlink(source, target, "file");
512
+ return;
513
+ }
514
+ try {
515
+ await symlink(source, target, "file");
516
+ return;
517
+ } catch {
518
+ }
519
+ try {
520
+ await link(source, target);
521
+ return;
522
+ } catch {
523
+ }
524
+ await copyFile(source, target);
525
+ log.warn(
526
+ "copied a shared Claude capability into the account config dir instead of linking it; later edits to the original will not be picked up",
527
+ { item, target }
528
+ );
482
529
  }
483
530
  async function writeAccountWrapper(account, baseCliPath, configDir) {
484
531
  const cacheRoot = path.join(
@@ -1490,7 +1537,7 @@ function createAccountFailoverQuestionCall(sessionKey2, input, toolCallId = `${A
1490
1537
  options: [
1491
1538
  ...candidates.map((candidate) => ({
1492
1539
  label: candidate,
1493
- description: `Run on "${candidate}" until ${until}. The conversation is replayed as a fresh Claude session (a session cannot resume across accounts), and any MCP server configured only in "${source}"'s Claude profile will be missing.`
1540
+ description: input.carryTranscript === false ? `Run on "${candidate}" until ${until}. The conversation is replayed as a fresh Claude session, and any MCP server configured only in "${source}"'s Claude profile will be missing.` : `Run on "${candidate}" until ${until}. The conversation continues where it stopped if its Claude transcript can be carried across, and is replayed as a fresh Claude session if it cannot; either way, any MCP server configured only in "${source}"'s Claude profile will be missing.`
1494
1541
  })),
1495
1542
  {
1496
1543
  label: STOP_ANSWER,
@@ -1601,20 +1648,20 @@ function stripAccountFailoverParts(prompt) {
1601
1648
  }
1602
1649
  return changed ? out : prompt;
1603
1650
  }
1604
- function failoverContinuationText(target) {
1651
+ function failoverContinuationText(target, resumed = false) {
1605
1652
  return [
1606
- `The Claude account this conversation was running on hit its usage limit, so it has been moved to the "${target}" account and you are now in a fresh Claude session.`,
1607
- "The conversation so far is above. Continue the task from where it stopped: do not start over, do not re-plan, and do not repeat work that is already done.",
1653
+ resumed ? `The Claude account this conversation was running on hit its usage limit, so it has been moved to the "${target}" account. This is the same conversation, continued: everything you already know still stands.` : `The Claude account this conversation was running on hit its usage limit, so it has been moved to the "${target}" account and you are now in a fresh Claude session. The conversation so far is above.`,
1654
+ "Continue the task from where it stopped: do not start over, do not re-plan, and do not repeat work that is already done.",
1608
1655
  "Do not mention the account switch unless you are asked about it."
1609
1656
  ].join(" ");
1610
1657
  }
1611
- function buildFailoverContinuationPrompt(prompt, target) {
1658
+ function buildFailoverContinuationPrompt(prompt, target, resumed = false) {
1612
1659
  const stripped = stripAccountFailoverParts(prompt);
1613
1660
  return [
1614
1661
  ...stripped,
1615
1662
  {
1616
1663
  role: "user",
1617
- content: [{ type: "text", text: failoverContinuationText(target) }]
1664
+ content: [{ type: "text", text: failoverContinuationText(target, resumed) }]
1618
1665
  }
1619
1666
  ];
1620
1667
  }
@@ -5067,6 +5114,9 @@ function claudeSpawnEnv(opts) {
5067
5114
  if (opts?.promptCacheTtl) {
5068
5115
  env.CLAUDE_CODE_PROMPT_CACHE_TTL = opts.promptCacheTtl;
5069
5116
  }
5117
+ if (opts?.configDir) {
5118
+ env.CLAUDE_CONFIG_DIR = opts.configDir;
5119
+ }
5070
5120
  if (opts?.ignoreAnthropicApiKey) {
5071
5121
  delete env.ANTHROPIC_API_KEY;
5072
5122
  delete env.ANTHROPIC_AUTH_TOKEN;
@@ -5184,6 +5234,11 @@ function getActiveProcess(key) {
5184
5234
  }
5185
5235
  return ap;
5186
5236
  }
5237
+ function processBelongsToAnotherAccount(active, cliPath, configDir) {
5238
+ if (!active) return false;
5239
+ if (active.cliPath && active.cliPath !== cliPath) return true;
5240
+ return !!active.configDir && active.configDir !== configDir;
5241
+ }
5187
5242
  function setActiveProcess(key, ap) {
5188
5243
  cancelIdleProcessEviction(key);
5189
5244
  activeProcesses.set(key, ap);
@@ -5399,7 +5454,7 @@ function invalidateOtherEffortSessions(baseKey, effort) {
5399
5454
  clearBackgroundTasks(key);
5400
5455
  }
5401
5456
  }
5402
- function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey, effort, promptCacheTtl) {
5457
+ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey, effort, promptCacheTtl, configDir) {
5403
5458
  evictIfNeeded();
5404
5459
  log.info("spawning new claude process", {
5405
5460
  cliPath,
@@ -5407,13 +5462,14 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
5407
5462
  cwd,
5408
5463
  sessionKey: sessionKey2,
5409
5464
  effort,
5410
- promptCacheTtl
5465
+ promptCacheTtl,
5466
+ configDir
5411
5467
  });
5412
5468
  const plan = planClaudeSpawn(cliPath, cliArgs);
5413
5469
  const proc = spawn(plan.file, plan.args, {
5414
5470
  cwd,
5415
5471
  stdio: ["pipe", "pipe", "pipe"],
5416
- env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort, promptCacheTtl }),
5472
+ env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort, promptCacheTtl, configDir }),
5417
5473
  windowsVerbatimArguments: plan.windowsVerbatimArguments
5418
5474
  });
5419
5475
  const lineEmitter = new EventEmitter3();
@@ -5428,6 +5484,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
5428
5484
  startedAt: Date.now(),
5429
5485
  cliPath,
5430
5486
  cliArgs: [...cliArgs],
5487
+ configDir,
5431
5488
  unattendedLines: [],
5432
5489
  unattendedDropped: 0
5433
5490
  };
@@ -5553,7 +5610,8 @@ function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropi
5553
5610
  old.systemPromptFile,
5554
5611
  ignoreAnthropicApiKey,
5555
5612
  old.effort,
5556
- old.promptCacheTtl
5613
+ old.promptCacheTtl,
5614
+ old.configDir
5557
5615
  );
5558
5616
  replacement.mcpServers = old.mcpServers;
5559
5617
  replacement.pendingProxyCompletions = old.pendingProxyCompletions;
@@ -5984,6 +6042,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5984
6042
  "ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN detected; stripping it from claude spawns (ignoreAnthropicApiKey) so requests use your subscription auth, not pay-as-you-go API billing.",
5985
6043
  "Code Mode execute not forwarded: no model-visible input schema",
5986
6044
  "Code Mode execute not forwarded: read-only permission preset",
6045
+ "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.",
5987
6046
  "MCP tool not routed through the proxy: another proxy tool already holds that name, and it keeps it",
5988
6047
  "account failover declined; ending the turn",
5989
6048
  "agent effort override refused: unknown level",
@@ -5994,6 +6053,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5994
6053
  "btw: a turn is running but no claude process appeared for it",
5995
6054
  "btw: early aside failed; the message will ask again",
5996
6055
  "cannot interrupt this transport; waiting for the turn to end",
6056
+ "carried this conversation's claude transcript to the other account",
5997
6057
  "carrying the claude conversation over to the new session key",
5998
6058
  "claude --version output unparseable",
5999
6059
  "claude account is out of usage; the turn ends with a note",
@@ -6019,6 +6079,8 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
6019
6079
  "cleanup-stale: error processing cache root",
6020
6080
  "cleanup-stale: rmSync failed",
6021
6081
  "continuing this conversation's claude session under the new model or effort instead of replaying it",
6082
+ "copied a shared Claude capability into the account config dir instead of linking it; later edits to the original will not be picked up",
6083
+ "could not share a Claude capability with the account",
6022
6084
  "delivering proxy results after interrupted continuation",
6023
6085
  "draining proxy calls queued between turns",
6024
6086
  "empty user content; sending sentinel to satisfy CLI",
@@ -6091,6 +6153,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
6091
6153
  "remote URL images are not supported by Claude CLI, skipping",
6092
6154
  "replaying stdout the child emitted between turns",
6093
6155
  "replaying the conversation as text: no claude session was available to continue",
6156
+ "replaying this conversation as text on the other account",
6094
6157
  "result fallback timer fired \u2014 closing stream without result event",
6095
6158
  "resuming the claude session from before the restart instead of replaying it",
6096
6159
  "shared Claude capability already exists; leaving untouched",
@@ -6135,6 +6198,8 @@ var BUNDLE_DATA_ALLOWLIST = {
6135
6198
  emitted: "flag",
6136
6199
  enabled: "flag",
6137
6200
  executed: "flag",
6201
+ failedOver: "flag",
6202
+ renamed: "flag",
6138
6203
  isUsingOverage: "flag",
6139
6204
  hadProxyActivity: "flag",
6140
6205
  hadReasoning: "flag",
@@ -6204,6 +6269,8 @@ var BUNDLE_DATA_ALLOWLIST = {
6204
6269
  finishReason: "enum",
6205
6270
  hash: "enum",
6206
6271
  hostApi: "enum",
6272
+ // One of `SHARED_CAPABILITY_ITEMS`: a fixed list this package defines.
6273
+ item: "enum",
6207
6274
  kind: "enum",
6208
6275
  level: "enum",
6209
6276
  mode: "enum",
@@ -6513,8 +6580,8 @@ function redactLogLine(raw, context) {
6513
6580
  if (!parsed.data || Object.keys(parsed.data).length === 0) return head;
6514
6581
  return `${head} ${JSON.stringify(sanitizeLogData(parsed.data, context))}`;
6515
6582
  }
6516
- function readLogTail(path12, maxBytes = BUNDLE_TAIL_BYTES) {
6517
- const fd = openSync(path12, "r");
6583
+ function readLogTail(path14, maxBytes = BUNDLE_TAIL_BYTES) {
6584
+ const fd = openSync(path14, "r");
6518
6585
  try {
6519
6586
  const size = fstatSync(fd).size;
6520
6587
  const length = Math.min(size, maxBytes);
@@ -6694,7 +6761,10 @@ async function fetchPlanUsage(cliPath, options = {}) {
6694
6761
  if (support && await support(cliPath) === "unsupported") {
6695
6762
  return { status: "failed", error: "Plan usage is unavailable: this Claude CLI does not support headless output. Check usage in the Claude CLI directly." };
6696
6763
  }
6697
- const env = claudeSpawnEnv({ ignoreAnthropicApiKey: options.ignoreAnthropicApiKey });
6764
+ const env = claudeSpawnEnv({
6765
+ ignoreAnthropicApiKey: options.ignoreAnthropicApiKey,
6766
+ configDir: options.configDir
6767
+ });
6698
6768
  const stdout = await run(
6699
6769
  cliPath,
6700
6770
  ["-p", "/cost", "--output-format", "json"],
@@ -7365,7 +7435,8 @@ async function gatherDoctorReport(options) {
7365
7435
  });
7366
7436
  }
7367
7437
  const planUsage = wantsPlanUsage(options.argument ?? "") ? await (options.planUsageImpl ?? fetchPlanUsage)(cliPath, {
7368
- ignoreAnthropicApiKey: options.ignoreAnthropicApiKey
7438
+ ignoreAnthropicApiKey: options.ignoreAnthropicApiKey,
7439
+ configDir: options.configDir
7369
7440
  }) : { status: "not-requested" };
7370
7441
  return {
7371
7442
  plugin: base.plugin,
@@ -9127,6 +9198,14 @@ function classifyScreen(text) {
9127
9198
  }
9128
9199
  return null;
9129
9200
  }
9201
+ function interactiveTranscriptPath(opts) {
9202
+ return path10.join(
9203
+ resolveConfigDir(opts.configDir),
9204
+ "projects",
9205
+ encodeCwd(path10.resolve(opts.cwd)),
9206
+ `${opts.sessionId}.jsonl`
9207
+ );
9208
+ }
9130
9209
  function configDirEnv(configDir) {
9131
9210
  return configDir ? { CLAUDE_CONFIG_DIR: configDir } : {};
9132
9211
  }
@@ -10637,14 +10716,74 @@ function spawnInteractiveProcess(opts) {
10637
10716
  return ap;
10638
10717
  }
10639
10718
 
10719
+ // src/account-transcript.ts
10720
+ import { randomUUID as randomUUID6 } from "crypto";
10721
+ import { constants as fsConstants } from "fs";
10722
+ import { chmod as chmod2, copyFile as copyFile2, lstat as lstat2, mkdir as mkdir2 } from "fs/promises";
10723
+ import path11 from "path";
10724
+ var transcriptPath = interactiveTranscriptPath;
10725
+ function configDirForAccount(account) {
10726
+ const normalized = normalizeAccountName(account || DEFAULT_ACCOUNT);
10727
+ return resolveConfigDir(
10728
+ normalized === DEFAULT_ACCOUNT ? void 0 : accountConfigDirPath(normalized)
10729
+ );
10730
+ }
10731
+ async function carryTranscriptToAccount(input) {
10732
+ const { sessionId, cwd, targetConfigDir } = input;
10733
+ const targetOwn = transcriptPath({ configDir: targetConfigDir, cwd, sessionId });
10734
+ const targetOwnStat = await statTranscript(targetOwn);
10735
+ if (targetOwnStat.kind === "other") {
10736
+ return { kind: "refused", reason: "target-not-a-file", detail: targetOwn };
10737
+ }
10738
+ let live;
10739
+ for (const dir of dedupe2([targetConfigDir, ...input.accountConfigDirs])) {
10740
+ const candidate = transcriptPath({ configDir: dir, cwd, sessionId });
10741
+ const stat = candidate === targetOwn ? targetOwnStat : await statTranscript(candidate);
10742
+ if (stat.kind !== "file") continue;
10743
+ if (!live || stat.size > live.size) live = { file: candidate, size: stat.size };
10744
+ }
10745
+ if (!live) return { kind: "refused", reason: "no-transcript-anywhere" };
10746
+ if (live.file === targetOwn) return { kind: "already-there", sessionId };
10747
+ const renamed = targetOwnStat.kind === "file";
10748
+ const carriedId = renamed ? (input.newSessionId ?? randomUUID6)() : sessionId;
10749
+ const target = renamed ? transcriptPath({ configDir: targetConfigDir, cwd, sessionId: carriedId }) : targetOwn;
10750
+ try {
10751
+ await mkdir2(path11.dirname(target), { recursive: true, mode: 448 });
10752
+ await copyFile2(live.file, target, fsConstants.COPYFILE_EXCL);
10753
+ await chmod2(target, 384);
10754
+ } catch (err) {
10755
+ return { kind: "refused", reason: "copy-failed", detail: String(err) };
10756
+ }
10757
+ return { kind: "carried", sessionId: carriedId, from: live.file, to: target, renamed };
10758
+ }
10759
+ async function statTranscript(file) {
10760
+ try {
10761
+ const stat = await lstat2(file);
10762
+ return stat.isFile() ? { kind: "file", size: stat.size } : { kind: "other", size: 0 };
10763
+ } catch {
10764
+ return { kind: "none", size: 0 };
10765
+ }
10766
+ }
10767
+ function dedupe2(dirs) {
10768
+ const seen = /* @__PURE__ */ new Set();
10769
+ const out = [];
10770
+ for (const dir of dirs) {
10771
+ const resolved = path11.resolve(dir);
10772
+ if (seen.has(resolved)) continue;
10773
+ seen.add(resolved);
10774
+ out.push(resolved);
10775
+ }
10776
+ return out;
10777
+ }
10778
+
10640
10779
  // src/prompts.ts
10641
10780
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
10642
10781
  import { homedir as homedir8 } from "os";
10643
- import { randomUUID as randomUUID6 } from "crypto";
10782
+ import { randomUUID as randomUUID7 } from "crypto";
10644
10783
  import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join11 } from "path";
10645
- function readPromptFileIfPresent(path12) {
10784
+ function readPromptFileIfPresent(path14) {
10646
10785
  try {
10647
- const content = readFileSync6(path12, "utf8").trim();
10786
+ const content = readFileSync6(path14, "utf8").trim();
10648
10787
  return content || void 0;
10649
10788
  } catch {
10650
10789
  return void 0;
@@ -10749,9 +10888,9 @@ function extractSystemMessages(prompt) {
10749
10888
  }
10750
10889
  var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md"]);
10751
10890
  function userAuthoredInstructions(forwarded, opts = {}) {
10752
- const readFile2 = opts.readFile ?? ((path12) => {
10891
+ const readFile2 = opts.readFile ?? ((path14) => {
10753
10892
  try {
10754
- return readFileSync6(path12, "utf8");
10893
+ return readFileSync6(path14, "utf8");
10755
10894
  } catch {
10756
10895
  return null;
10757
10896
  }
@@ -10763,16 +10902,16 @@ function userAuthoredInstructions(forwarded, opts = {}) {
10763
10902
  }
10764
10903
  const seen = /* @__PURE__ */ new Set();
10765
10904
  for (const match of forwarded.matchAll(/^Instructions from: (.+)$/gm)) {
10766
- const path12 = match[1].trim();
10767
- if (seen.has(path12) || !isAbsolute3(path12)) continue;
10768
- if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path12))) continue;
10769
- seen.add(path12);
10770
- const file = readFile2(path12);
10905
+ const path14 = match[1].trim();
10906
+ if (seen.has(path14) || !isAbsolute3(path14)) continue;
10907
+ if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path14))) continue;
10908
+ seen.add(path14);
10909
+ const file = readFile2(path14);
10771
10910
  if (!file?.trim()) continue;
10772
10911
  const start = match.index + match[0].length + 1;
10773
10912
  const content = forwarded.startsWith(file, start) ? file : forwarded.startsWith(file.trimEnd(), start) ? file.trimEnd() : null;
10774
10913
  if (content === null) continue;
10775
- out.push({ text: `Instructions from: ${path12}
10914
+ out.push({ text: `Instructions from: ${path14}
10776
10915
  ${content}`, content });
10777
10916
  }
10778
10917
  return out;
@@ -10809,10 +10948,10 @@ ${options.compressionSummary.trim()}`
10809
10948
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
10810
10949
  const content = parts.join("\n\n");
10811
10950
  if (!content) return void 0;
10812
- const path12 = join11(pluginTmpDir(), `opencode-cc-sys-${randomUUID6()}.md`);
10951
+ const path14 = join11(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
10813
10952
  try {
10814
- writeFileSync6(path12, content, { encoding: "utf8", mode: 384 });
10815
- return path12;
10953
+ writeFileSync6(path14, content, { encoding: "utf8", mode: 384 });
10954
+ return path14;
10816
10955
  } catch (err) {
10817
10956
  log.warn("failed to write system prompt file", { error: String(err) });
10818
10957
  return void 0;
@@ -11143,6 +11282,8 @@ _Reply with your answer to continue._
11143
11282
  }
11144
11283
 
11145
11284
  // src/control-request.ts
11285
+ var ELICITATION_DECLINE = { action: "decline" };
11286
+ var warnedElicitationServers = /* @__PURE__ */ new Set();
11146
11287
  function controlRequestBehaviorForTool(config, toolName) {
11147
11288
  const configured = config.controlRequestToolBehaviors;
11148
11289
  if (configured && toolName) {
@@ -11211,6 +11352,18 @@ function handleControlRequest(config, msg, proc) {
11211
11352
  }
11212
11353
  return true;
11213
11354
  }
11355
+ if (request.subtype === "elicitation") {
11356
+ const server2 = request.mcp_server_name || "unknown";
11357
+ writeControlResponse(proc, requestId, { ...ELICITATION_DECLINE });
11358
+ if (!warnedElicitationServers.has(server2)) {
11359
+ warnedElicitationServers.add(server2);
11360
+ log.warn(
11361
+ "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.",
11362
+ { server: server2, mode: request.mode ?? "form" }
11363
+ );
11364
+ }
11365
+ return true;
11366
+ }
11214
11367
  writeControlResponse(proc, requestId, {});
11215
11368
  log.debug("control request acknowledged", {
11216
11369
  requestId,
@@ -11929,14 +12082,18 @@ var FAST_MODE_REASONS = {
11929
12082
  pending: "the CLI is still checking availability"
11930
12083
  };
11931
12084
  var warnedFastModeReasons = /* @__PURE__ */ new Set();
11932
- function reportFastModeState(msg, requested) {
12085
+ function reportFastModeState(msg, requested, phase = "result") {
11933
12086
  const state = msg.fast_mode_state;
11934
12087
  if (!state) return;
11935
12088
  if (!requested) {
11936
- log.debug("fast mode state", { state });
12089
+ log.debug("fast mode state", { state, phase });
11937
12090
  return;
11938
12091
  }
11939
12092
  if (state === "on") {
12093
+ if (phase === "init") {
12094
+ log.debug("fast mode reported on at session start", { state, phase });
12095
+ return;
12096
+ }
11940
12097
  log.info("fast mode active", { state });
11941
12098
  return;
11942
12099
  }
@@ -12431,7 +12588,7 @@ function createLineHandler(state, ctx) {
12431
12588
  claudeSessionId: msg.session_id
12432
12589
  });
12433
12590
  }
12434
- reportFastModeState(msg, ctx.fastMode);
12591
+ reportFastModeState(msg, ctx.fastMode, "init");
12435
12592
  reportSystemInit(msg, {
12436
12593
  ignoreAnthropicApiKey: ctx.config.ignoreAnthropicApiKey
12437
12594
  });
@@ -12997,6 +13154,7 @@ ${plan}
12997
13154
  }
12998
13155
  if (msg.type === "result") {
12999
13156
  clearFallbackTimer(state);
13157
+ reportFastModeState(msg, ctx.fastMode, "result");
13000
13158
  if (msg.session_id) {
13001
13159
  setClaudeSessionId(state.sessionKey, msg.session_id);
13002
13160
  }
@@ -13619,6 +13777,11 @@ var ClaudeCodeLanguageModel = class {
13619
13777
  interactive: !!useInteractive,
13620
13778
  turnStats: this.config.turnStats === true,
13621
13779
  ignoreAnthropicApiKey: this.config.ignoreAnthropicApiKey === true,
13780
+ // Where the plugin applies the account itself rather than through a
13781
+ // wrapper (`accountInProcess`, Windows), the `/cost` spawn needs the
13782
+ // same config dir a turn gets, or it reports the default account's
13783
+ // plan usage instead of this provider's.
13784
+ configDir: this.config.accountInProcess ? this.config.configDir : void 0,
13622
13785
  // `/claude-code-doctor usage` opts into the CLI's own plan-usage
13623
13786
  // report; anything else here is ignored, as it always has been.
13624
13787
  argument: doctor.rest
@@ -13819,21 +13982,61 @@ var ClaudeCodeLanguageModel = class {
13819
13982
  resetsAt: failoverAnswer.resetsAt
13820
13983
  });
13821
13984
  }
13985
+ const spawnConfigDir = this.config.accountInProcess ? this.skillBridgeSpawn(failover).configDir : void 0;
13822
13986
  const processForAccount = getActiveProcess(sk);
13823
- if (!compactionMode && processForAccount?.cliPath && processForAccount.cliPath !== cliPath) {
13987
+ if (!compactionMode && processBelongsToAnotherAccount(processForAccount, cliPath, spawnConfigDir)) {
13824
13988
  log.notice("claude process belongs to another account; starting fresh", {
13825
13989
  sessionKey: sk,
13826
- was: processForAccount.cliPath,
13990
+ was: processForAccount?.cliPath,
13827
13991
  now: cliPath,
13828
13992
  failedOver: failover.failedOver
13829
13993
  });
13830
13994
  deleteActiveProcess(sk);
13995
+ }
13996
+ let conversationOnTarget = false;
13997
+ const multiAccount = (this.config.failoverAccounts?.length ?? 0) > 1;
13998
+ const sessionBeforeAccountCheck = getClaudeSessionId(sk);
13999
+ if (!compactionMode && multiAccount && this.config.crossAccountResume !== false && sessionBeforeAccountCheck) {
14000
+ const carry = await carryTranscriptToAccount({
14001
+ sessionId: sessionBeforeAccountCheck,
14002
+ cwd,
14003
+ targetConfigDir: resolveConfigDir(this.skillBridgeSpawn(failover).configDir),
14004
+ // Every account's own directory, plus this model's configured one:
14005
+ // `configDir` is an option, so an account's directory is not always
14006
+ // the `~/.claude-<name>` the name alone would build.
14007
+ accountConfigDirs: [
14008
+ ...(this.config.failoverAccounts ?? []).map(configDirForAccount),
14009
+ resolveConfigDir(this.config.configDir)
14010
+ ]
14011
+ });
14012
+ if (carry.kind === "carried" || carry.kind === "already-there") {
14013
+ if (carry.sessionId !== sessionBeforeAccountCheck) {
14014
+ setClaudeSessionId(sk, carry.sessionId);
14015
+ }
14016
+ conversationOnTarget = true;
14017
+ if (carry.kind === "carried") {
14018
+ log.notice("carried this conversation's claude transcript to the other account", {
14019
+ sessionKey: sk,
14020
+ renamed: carry.renamed,
14021
+ sessionId: carry.sessionId,
14022
+ failedOver: failover.failedOver
14023
+ });
14024
+ }
14025
+ } else if (!getActiveProcess(sk)) {
14026
+ log.notice("replaying this conversation as text on the other account", {
14027
+ sessionKey: sk,
14028
+ reason: carry.reason,
14029
+ failedOver: failover.failedOver
14030
+ });
14031
+ deleteClaudeSessionId(sk);
14032
+ }
14033
+ } else if (!compactionMode && sessionBeforeAccountCheck && processBelongsToAnotherAccount(processForAccount, cliPath, spawnConfigDir)) {
13831
14034
  deleteClaudeSessionId(sk);
13832
14035
  }
13833
14036
  const hasExistingSession = !!getClaudeSessionId(sk);
13834
14037
  const hasActiveProcess = !!getActiveProcess(sk);
13835
14038
  let includeHistoryContext = !hasExistingSession && !hasActiveProcess && hasPriorConversation;
13836
- if (failoverAnswer?.kind === "switch" && hasPriorConversation) {
14039
+ if (failoverAnswer?.kind === "switch" && hasPriorConversation && !conversationOnTarget) {
13837
14040
  includeHistoryContext = true;
13838
14041
  }
13839
14042
  let forkFromClaudeSessionId;
@@ -13879,12 +14082,12 @@ var ClaudeCodeLanguageModel = class {
13879
14082
  let replayReason = "no-session-for-key";
13880
14083
  if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
13881
14084
  const configDir = resolveConfigDir(this.config.configDir);
13882
- const transcriptPath = (id) => join12(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
14085
+ const transcriptPath2 = (id) => join12(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
13883
14086
  const resumePoint = findResumePoint({
13884
14087
  sessionKey: sk,
13885
14088
  prompt: options.prompt,
13886
14089
  cliPath,
13887
- transcriptPath,
14090
+ transcriptPath: transcriptPath2,
13888
14091
  onRefused: (reason) => {
13889
14092
  replayReason = reason;
13890
14093
  }
@@ -13901,7 +14104,7 @@ var ClaudeCodeLanguageModel = class {
13901
14104
  sessionKey: sk,
13902
14105
  prompt: options.prompt,
13903
14106
  cliPath,
13904
- transcriptPath,
14107
+ transcriptPath: transcriptPath2,
13905
14108
  // `claudeSessionIsWriting`, not `getActiveProcess`: this walks every
13906
14109
  // sibling key, and that one refreshes LRU order and cancels idle
13907
14110
  // timers on everything it touches (the same reason the fork lookup
@@ -13940,13 +14143,19 @@ var ClaudeCodeLanguageModel = class {
13940
14143
  if (this.config.forkSessions === true && !compactionMode) {
13941
14144
  recordForkFingerprint(sk, options.prompt, cliPath);
13942
14145
  }
13943
- const { model: spawnModelId, fast: fastMode } = parseModelId(failover.modelId);
14146
+ const { model: spawnModelId, fast: fastMode } = parseModelId(
14147
+ this.config.accountInProcess ? stripAccountSuffix(failover.modelId) : failover.modelId
14148
+ );
13944
14149
  const exitPlanModeQuestionResult = compactionMode ? null : consumeExitPlanModeQuestionResult(sk, options.prompt);
13945
14150
  if (exitPlanModeQuestionResult) {
13946
14151
  log.info("sending plan approval decision to claude", { sk });
13947
14152
  }
13948
14153
  const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
13949
- const effectivePrompt = failoverAnswer?.kind === "switch" ? buildFailoverContinuationPrompt(options.prompt, failoverAnswer.target) : options.prompt;
14154
+ const effectivePrompt = failoverAnswer?.kind === "switch" ? buildFailoverContinuationPrompt(
14155
+ options.prompt,
14156
+ failoverAnswer.target,
14157
+ conversationOnTarget
14158
+ ) : options.prompt;
13950
14159
  const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(effectivePrompt, includeHistoryContext, {
13951
14160
  compactionMode,
13952
14161
  compactionInstructions: useInteractive && compactionMode ? extractSystemMessages(effectivePrompt).join("\n\n") : void 0,
@@ -14566,7 +14775,8 @@ var ClaudeCodeLanguageModel = class {
14566
14775
  spawnSystemPromptFile,
14567
14776
  self.config.ignoreAnthropicApiKey,
14568
14777
  reasoningEffort,
14569
- promptCacheTtl
14778
+ promptCacheTtl,
14779
+ spawnConfigDir
14570
14780
  );
14571
14781
  ap.mcpServers = spawnMcpServers;
14572
14782
  state.proc = ap.proc;
@@ -14628,7 +14838,8 @@ var ClaudeCodeLanguageModel = class {
14628
14838
  candidates: failoverAccounts,
14629
14839
  resetsAt: state.accountLimitHit?.resetsAt,
14630
14840
  window: state.accountLimitHit?.window,
14631
- reason: state.accountLimitHit || !state.accountBlock ? void 0 : describeAccountBlock(state.accountBlock)
14841
+ reason: state.accountLimitHit || !state.accountBlock ? void 0 : describeAccountBlock(state.accountBlock),
14842
+ carryTranscript: self.config.crossAccountResume !== false
14632
14843
  });
14633
14844
  log.warn(
14634
14845
  `Claude account "${sourceAccount}" ${state.accountLimitHit || !state.accountBlock ? "is out of usage" : `cannot serve (${state.accountBlock})`}; asking which account to continue on.`,
@@ -15126,11 +15337,80 @@ var ClaudeCodeLanguageModel = class {
15126
15337
  }
15127
15338
  };
15128
15339
 
15340
+ // src/account-tier.ts
15341
+ import { readFileSync as readFileSync7 } from "fs";
15342
+ import path12 from "path";
15343
+ var ORGANIZATION_TYPE_LABELS = {
15344
+ claude_max: "Max",
15345
+ claude_pro: "Pro",
15346
+ claude_team: "Team",
15347
+ claude_enterprise: "Enterprise",
15348
+ claude_free: "Free"
15349
+ };
15350
+ var RATE_LIMIT_TIER_LABELS = {
15351
+ default_claude_max_5x: "5x",
15352
+ default_claude_max_20x: "20x"
15353
+ };
15354
+ var tierCache = /* @__PURE__ */ new Map();
15355
+ function accountTierConfigDir(account) {
15356
+ const configured = accountConfigDirPath(account);
15357
+ if (configured) return configured;
15358
+ const fromEnv = process.env.CLAUDE_CONFIG_DIR;
15359
+ return fromEnv && fromEnv.trim() ? expandHome(fromEnv.trim()) : expandHome("~/.claude");
15360
+ }
15361
+ function readAccountTier(account) {
15362
+ const normalized = normalizeAccountName(account) || DEFAULT_ACCOUNT;
15363
+ const configDir = accountTierConfigDir(normalized);
15364
+ const cached = tierCache.get(configDir);
15365
+ if (cached !== void 0) return cached ?? void 0;
15366
+ const label = readTierFromConfigDir(configDir, normalized === DEFAULT_ACCOUNT);
15367
+ tierCache.set(configDir, label ?? null);
15368
+ return label;
15369
+ }
15370
+ function readTierFromConfigDir(configDir, allowLegacyFallback) {
15371
+ const candidates = [path12.join(configDir, ".claude.json")];
15372
+ if (allowLegacyFallback) candidates.push(expandHome("~/.claude.json"));
15373
+ for (const candidate of candidates) {
15374
+ const oauth = readOauthAccount(candidate);
15375
+ if (!oauth) continue;
15376
+ const label = tierLabel(oauth);
15377
+ if (label) return label;
15378
+ }
15379
+ return void 0;
15380
+ }
15381
+ function readOauthAccount(file) {
15382
+ let raw;
15383
+ try {
15384
+ raw = readFileSync7(file, "utf8");
15385
+ } catch {
15386
+ return void 0;
15387
+ }
15388
+ try {
15389
+ const parsed = JSON.parse(raw);
15390
+ if (!parsed || typeof parsed !== "object") return void 0;
15391
+ const oauth = parsed.oauthAccount;
15392
+ return oauth && typeof oauth === "object" ? oauth : void 0;
15393
+ } catch {
15394
+ log.debug("could not parse a Claude config file for its plan tier", { file });
15395
+ return void 0;
15396
+ }
15397
+ }
15398
+ function tierLabel(oauthAccount) {
15399
+ const organizationType = oauthAccount.organizationType;
15400
+ if (typeof organizationType !== "string") return void 0;
15401
+ const base = ORGANIZATION_TYPE_LABELS[organizationType];
15402
+ if (!base) return void 0;
15403
+ if (base !== "Max") return base;
15404
+ const rateLimitTier = oauthAccount.organizationRateLimitTier;
15405
+ const multiple = typeof rateLimitTier === "string" ? RATE_LIMIT_TIER_LABELS[rateLimitTier] : void 0;
15406
+ return multiple ? `${base} ${multiple}` : base;
15407
+ }
15408
+
15129
15409
  // src/cleanup-stale.ts
15130
15410
  import {
15131
15411
  existsSync as existsSync5,
15132
15412
  mkdirSync as mkdirSync5,
15133
- readFileSync as readFileSync7,
15413
+ readFileSync as readFileSync8,
15134
15414
  realpathSync as realpathSync4,
15135
15415
  rmSync as rmSync3,
15136
15416
  writeFileSync as writeFileSync7
@@ -15157,7 +15437,7 @@ function userIntendsToUseUnscoped() {
15157
15437
  const cfg = userOpencodeJsonPath();
15158
15438
  if (!existsSync5(cfg)) return false;
15159
15439
  try {
15160
- const json = JSON.parse(readFileSync7(cfg, "utf8"));
15440
+ const json = JSON.parse(readFileSync8(cfg, "utf8"));
15161
15441
  const plugins = json.plugin;
15162
15442
  if (!Array.isArray(plugins)) return false;
15163
15443
  return plugins.some(
@@ -15178,7 +15458,7 @@ function ourLoadedDir() {
15178
15458
  function ourVersion(dir) {
15179
15459
  if (!dir) return null;
15180
15460
  try {
15181
- const pkg = JSON.parse(readFileSync7(join13(dir, "package.json"), "utf8"));
15461
+ const pkg = JSON.parse(readFileSync8(join13(dir, "package.json"), "utf8"));
15182
15462
  return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
15183
15463
  } catch {
15184
15464
  return null;
@@ -15190,7 +15470,7 @@ function markerPath() {
15190
15470
  }
15191
15471
  function markerVersion() {
15192
15472
  try {
15193
- const marker = JSON.parse(readFileSync7(markerPath(), "utf8"));
15473
+ const marker = JSON.parse(readFileSync8(markerPath(), "utf8"));
15194
15474
  return typeof marker.version === "string" ? marker.version : null;
15195
15475
  } catch {
15196
15476
  return null;
@@ -15247,7 +15527,7 @@ function cleanupOne(cacheRoot, ourDir) {
15247
15527
  if (!existsSync5(pkgJsonPath)) return;
15248
15528
  let pkg = {};
15249
15529
  try {
15250
- pkg = JSON.parse(readFileSync7(pkgJsonPath, "utf8"));
15530
+ pkg = JSON.parse(readFileSync8(pkgJsonPath, "utf8"));
15251
15531
  } catch {
15252
15532
  return;
15253
15533
  }
@@ -15266,7 +15546,7 @@ function cleanupOne(cacheRoot, ourDir) {
15266
15546
  const cachePkgJson = join13(cacheRoot, "package.json");
15267
15547
  if (!existsSync5(cachePkgJson)) return;
15268
15548
  try {
15269
- const cfg = JSON.parse(readFileSync7(cachePkgJson, "utf8"));
15549
+ const cfg = JSON.parse(readFileSync8(cachePkgJson, "utf8"));
15270
15550
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
15271
15551
  delete cfg.dependencies[STALE_PACKAGE_NAME];
15272
15552
  writeFileSync7(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
@@ -15346,7 +15626,7 @@ function registerOpencodeLogSink(client) {
15346
15626
  }
15347
15627
 
15348
15628
  // src/v2-client.ts
15349
- import path11 from "path";
15629
+ import path13 from "path";
15350
15630
  function formatAgentTypeList(agents) {
15351
15631
  const lines = agents.flatMap((agent) => {
15352
15632
  if (agent.hidden === true || agent.mode === "primary") return [];
@@ -15375,7 +15655,7 @@ function toV1Session(info) {
15375
15655
  if (!info || typeof info !== "object") return void 0;
15376
15656
  const session = info;
15377
15657
  const root = session.location?.directory;
15378
- const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path11.resolve(root, session.subpath) : root : void 0;
15658
+ const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path13.resolve(root, session.subpath) : root : void 0;
15379
15659
  return { ...info, directory, parentID: session.parentID };
15380
15660
  }
15381
15661
  function toV1Messages(messages) {
@@ -15567,7 +15847,7 @@ function planV2Providers(seedSettings, defaultProxyTools) {
15567
15847
  return {
15568
15848
  info: {
15569
15849
  id: providerID,
15570
- name: accountDisplayName(account),
15850
+ name: accountDisplayName(account, readAccountTier(account)),
15571
15851
  activation: "enabled",
15572
15852
  package: V2_PLUGIN_PACKAGE,
15573
15853
  settings: {
@@ -15934,6 +16214,7 @@ function createClaudeCode(settings = {}) {
15934
16214
  forkSessions: settings.forkSessions === true,
15935
16215
  resumeAfterRestart: settings.resumeAfterRestart !== false,
15936
16216
  resumeAcrossModelChanges: settings.resumeAcrossModelChanges !== false,
16217
+ crossAccountResume: settings.crossAccountResume !== false,
15937
16218
  transport: settings.transport,
15938
16219
  interactive: settings.interactive,
15939
16220
  interactiveBypass: settings.interactiveBypass,
@@ -16104,7 +16385,7 @@ async function expandAccountProviders(config) {
16104
16385
  // stripped by cleanProviderOptions.
16105
16386
  failoverAccounts: accounts
16106
16387
  },
16107
- accountDisplayName(account)
16388
+ accountDisplayName(account, readAccountTier(account))
16108
16389
  ),
16109
16390
  models: configModelsForProvider(
16110
16391
  existing?.models ?? seed?.models ?? {},