@khalilgharbaoui/opencode-claude-code-plugin 0.46.1 → 0.47.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.d.ts CHANGED
@@ -435,6 +435,8 @@ interface ClaudeCodeConfig {
435
435
  resumeAfterRestart?: boolean;
436
436
  /** Carry a conversation's Claude session over when its model or effort changes. Default true. */
437
437
  resumeAcrossModelChanges?: boolean;
438
+ /** Carry a conversation's Claude transcript over on an account switch. Default true. */
439
+ crossAccountResume?: boolean;
438
440
  logging?: LoggingConfig;
439
441
  }
440
442
  interface LoggingConfig {
@@ -856,6 +858,30 @@ interface ClaudeCodeProviderSettings {
856
858
  * false` turns this off too.
857
859
  */
858
860
  resumeAcrossModelChanges?: boolean;
861
+ /**
862
+ * Continue the same Claude conversation when it moves to another account,
863
+ * instead of replaying the whole thread as text into a fresh session there.
864
+ * Default `true`; it does nothing at all on a single-account install.
865
+ *
866
+ * An account is a `CLAUDE_CONFIG_DIR`, and a conversation is one transcript
867
+ * file inside it, so the move is a file copy into the target account's own
868
+ * `projects/<encoded cwd>/` plus the `--resume` that already exists. Claude
869
+ * Code 2.1.288 resumes a copied transcript with its context intact, resolves
870
+ * a session by FILENAME, and appends to the copy without touching the
871
+ * original (h #g218).
872
+ *
873
+ * Every refusal falls back to the replay and says which in the log: no
874
+ * transcript to carry, something that is not a plain file at the target
875
+ * path, or a copy that failed. The source transcript is never moved,
876
+ * deleted or written to, and an existing file is never overwritten (the
877
+ * carry takes a fresh session id instead).
878
+ *
879
+ * What was measured is the file layout, with a second config dir holding a
880
+ * copy of the same credentials; whether a different Anthropic account's
881
+ * token answers a conversation produced under another one is not yet
882
+ * verified live. Set `false` to keep the replay.
883
+ */
884
+ crossAccountResume?: boolean;
859
885
  /**
860
886
  * Routing for Claude's built-in `WebSearch` tool.
861
887
  *
package/dist/index.js CHANGED
@@ -1490,7 +1490,7 @@ function createAccountFailoverQuestionCall(sessionKey2, input, toolCallId = `${A
1490
1490
  options: [
1491
1491
  ...candidates.map((candidate) => ({
1492
1492
  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.`
1493
+ 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
1494
  })),
1495
1495
  {
1496
1496
  label: STOP_ANSWER,
@@ -1601,20 +1601,20 @@ function stripAccountFailoverParts(prompt) {
1601
1601
  }
1602
1602
  return changed ? out : prompt;
1603
1603
  }
1604
- function failoverContinuationText(target) {
1604
+ function failoverContinuationText(target, resumed = false) {
1605
1605
  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.",
1606
+ 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.`,
1607
+ "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
1608
  "Do not mention the account switch unless you are asked about it."
1609
1609
  ].join(" ");
1610
1610
  }
1611
- function buildFailoverContinuationPrompt(prompt, target) {
1611
+ function buildFailoverContinuationPrompt(prompt, target, resumed = false) {
1612
1612
  const stripped = stripAccountFailoverParts(prompt);
1613
1613
  return [
1614
1614
  ...stripped,
1615
1615
  {
1616
1616
  role: "user",
1617
- content: [{ type: "text", text: failoverContinuationText(target) }]
1617
+ content: [{ type: "text", text: failoverContinuationText(target, resumed) }]
1618
1618
  }
1619
1619
  ];
1620
1620
  }
@@ -5994,6 +5994,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5994
5994
  "btw: a turn is running but no claude process appeared for it",
5995
5995
  "btw: early aside failed; the message will ask again",
5996
5996
  "cannot interrupt this transport; waiting for the turn to end",
5997
+ "carried this conversation's claude transcript to the other account",
5997
5998
  "carrying the claude conversation over to the new session key",
5998
5999
  "claude --version output unparseable",
5999
6000
  "claude account is out of usage; the turn ends with a note",
@@ -6091,6 +6092,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
6091
6092
  "remote URL images are not supported by Claude CLI, skipping",
6092
6093
  "replaying stdout the child emitted between turns",
6093
6094
  "replaying the conversation as text: no claude session was available to continue",
6095
+ "replaying this conversation as text on the other account",
6094
6096
  "result fallback timer fired \u2014 closing stream without result event",
6095
6097
  "resuming the claude session from before the restart instead of replaying it",
6096
6098
  "shared Claude capability already exists; leaving untouched",
@@ -6135,6 +6137,8 @@ var BUNDLE_DATA_ALLOWLIST = {
6135
6137
  emitted: "flag",
6136
6138
  enabled: "flag",
6137
6139
  executed: "flag",
6140
+ failedOver: "flag",
6141
+ renamed: "flag",
6138
6142
  isUsingOverage: "flag",
6139
6143
  hadProxyActivity: "flag",
6140
6144
  hadReasoning: "flag",
@@ -6513,8 +6517,8 @@ function redactLogLine(raw, context) {
6513
6517
  if (!parsed.data || Object.keys(parsed.data).length === 0) return head;
6514
6518
  return `${head} ${JSON.stringify(sanitizeLogData(parsed.data, context))}`;
6515
6519
  }
6516
- function readLogTail(path12, maxBytes = BUNDLE_TAIL_BYTES) {
6517
- const fd = openSync(path12, "r");
6520
+ function readLogTail(path13, maxBytes = BUNDLE_TAIL_BYTES) {
6521
+ const fd = openSync(path13, "r");
6518
6522
  try {
6519
6523
  const size = fstatSync(fd).size;
6520
6524
  const length = Math.min(size, maxBytes);
@@ -9127,6 +9131,14 @@ function classifyScreen(text) {
9127
9131
  }
9128
9132
  return null;
9129
9133
  }
9134
+ function interactiveTranscriptPath(opts) {
9135
+ return path10.join(
9136
+ resolveConfigDir(opts.configDir),
9137
+ "projects",
9138
+ encodeCwd(path10.resolve(opts.cwd)),
9139
+ `${opts.sessionId}.jsonl`
9140
+ );
9141
+ }
9130
9142
  function configDirEnv(configDir) {
9131
9143
  return configDir ? { CLAUDE_CONFIG_DIR: configDir } : {};
9132
9144
  }
@@ -10637,14 +10649,74 @@ function spawnInteractiveProcess(opts) {
10637
10649
  return ap;
10638
10650
  }
10639
10651
 
10652
+ // src/account-transcript.ts
10653
+ import { randomUUID as randomUUID6 } from "crypto";
10654
+ import { constants as fsConstants } from "fs";
10655
+ import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir2 } from "fs/promises";
10656
+ import path11 from "path";
10657
+ var transcriptPath = interactiveTranscriptPath;
10658
+ function configDirForAccount(account) {
10659
+ const normalized = normalizeAccountName(account || DEFAULT_ACCOUNT);
10660
+ return resolveConfigDir(
10661
+ normalized === DEFAULT_ACCOUNT ? void 0 : accountConfigDirPath(normalized)
10662
+ );
10663
+ }
10664
+ async function carryTranscriptToAccount(input) {
10665
+ const { sessionId, cwd, targetConfigDir } = input;
10666
+ const targetOwn = transcriptPath({ configDir: targetConfigDir, cwd, sessionId });
10667
+ const targetOwnStat = await statTranscript(targetOwn);
10668
+ if (targetOwnStat.kind === "other") {
10669
+ return { kind: "refused", reason: "target-not-a-file", detail: targetOwn };
10670
+ }
10671
+ let live;
10672
+ for (const dir of dedupe2([targetConfigDir, ...input.accountConfigDirs])) {
10673
+ const candidate = transcriptPath({ configDir: dir, cwd, sessionId });
10674
+ const stat = candidate === targetOwn ? targetOwnStat : await statTranscript(candidate);
10675
+ if (stat.kind !== "file") continue;
10676
+ if (!live || stat.size > live.size) live = { file: candidate, size: stat.size };
10677
+ }
10678
+ if (!live) return { kind: "refused", reason: "no-transcript-anywhere" };
10679
+ if (live.file === targetOwn) return { kind: "already-there", sessionId };
10680
+ const renamed = targetOwnStat.kind === "file";
10681
+ const carriedId = renamed ? (input.newSessionId ?? randomUUID6)() : sessionId;
10682
+ const target = renamed ? transcriptPath({ configDir: targetConfigDir, cwd, sessionId: carriedId }) : targetOwn;
10683
+ try {
10684
+ await mkdir2(path11.dirname(target), { recursive: true, mode: 448 });
10685
+ await copyFile(live.file, target, fsConstants.COPYFILE_EXCL);
10686
+ await chmod2(target, 384);
10687
+ } catch (err) {
10688
+ return { kind: "refused", reason: "copy-failed", detail: String(err) };
10689
+ }
10690
+ return { kind: "carried", sessionId: carriedId, from: live.file, to: target, renamed };
10691
+ }
10692
+ async function statTranscript(file) {
10693
+ try {
10694
+ const stat = await lstat2(file);
10695
+ return stat.isFile() ? { kind: "file", size: stat.size } : { kind: "other", size: 0 };
10696
+ } catch {
10697
+ return { kind: "none", size: 0 };
10698
+ }
10699
+ }
10700
+ function dedupe2(dirs) {
10701
+ const seen = /* @__PURE__ */ new Set();
10702
+ const out = [];
10703
+ for (const dir of dirs) {
10704
+ const resolved = path11.resolve(dir);
10705
+ if (seen.has(resolved)) continue;
10706
+ seen.add(resolved);
10707
+ out.push(resolved);
10708
+ }
10709
+ return out;
10710
+ }
10711
+
10640
10712
  // src/prompts.ts
10641
10713
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
10642
10714
  import { homedir as homedir8 } from "os";
10643
- import { randomUUID as randomUUID6 } from "crypto";
10715
+ import { randomUUID as randomUUID7 } from "crypto";
10644
10716
  import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join11 } from "path";
10645
- function readPromptFileIfPresent(path12) {
10717
+ function readPromptFileIfPresent(path13) {
10646
10718
  try {
10647
- const content = readFileSync6(path12, "utf8").trim();
10719
+ const content = readFileSync6(path13, "utf8").trim();
10648
10720
  return content || void 0;
10649
10721
  } catch {
10650
10722
  return void 0;
@@ -10749,9 +10821,9 @@ function extractSystemMessages(prompt) {
10749
10821
  }
10750
10822
  var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md"]);
10751
10823
  function userAuthoredInstructions(forwarded, opts = {}) {
10752
- const readFile2 = opts.readFile ?? ((path12) => {
10824
+ const readFile2 = opts.readFile ?? ((path13) => {
10753
10825
  try {
10754
- return readFileSync6(path12, "utf8");
10826
+ return readFileSync6(path13, "utf8");
10755
10827
  } catch {
10756
10828
  return null;
10757
10829
  }
@@ -10763,16 +10835,16 @@ function userAuthoredInstructions(forwarded, opts = {}) {
10763
10835
  }
10764
10836
  const seen = /* @__PURE__ */ new Set();
10765
10837
  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);
10838
+ const path13 = match[1].trim();
10839
+ if (seen.has(path13) || !isAbsolute3(path13)) continue;
10840
+ if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path13))) continue;
10841
+ seen.add(path13);
10842
+ const file = readFile2(path13);
10771
10843
  if (!file?.trim()) continue;
10772
10844
  const start = match.index + match[0].length + 1;
10773
10845
  const content = forwarded.startsWith(file, start) ? file : forwarded.startsWith(file.trimEnd(), start) ? file.trimEnd() : null;
10774
10846
  if (content === null) continue;
10775
- out.push({ text: `Instructions from: ${path12}
10847
+ out.push({ text: `Instructions from: ${path13}
10776
10848
  ${content}`, content });
10777
10849
  }
10778
10850
  return out;
@@ -10809,10 +10881,10 @@ ${options.compressionSummary.trim()}`
10809
10881
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
10810
10882
  const content = parts.join("\n\n");
10811
10883
  if (!content) return void 0;
10812
- const path12 = join11(pluginTmpDir(), `opencode-cc-sys-${randomUUID6()}.md`);
10884
+ const path13 = join11(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
10813
10885
  try {
10814
- writeFileSync6(path12, content, { encoding: "utf8", mode: 384 });
10815
- return path12;
10886
+ writeFileSync6(path13, content, { encoding: "utf8", mode: 384 });
10887
+ return path13;
10816
10888
  } catch (err) {
10817
10889
  log.warn("failed to write system prompt file", { error: String(err) });
10818
10890
  return void 0;
@@ -13828,12 +13900,51 @@ var ClaudeCodeLanguageModel = class {
13828
13900
  failedOver: failover.failedOver
13829
13901
  });
13830
13902
  deleteActiveProcess(sk);
13903
+ }
13904
+ let conversationOnTarget = false;
13905
+ const multiAccount = (this.config.failoverAccounts?.length ?? 0) > 1;
13906
+ const sessionBeforeAccountCheck = getClaudeSessionId(sk);
13907
+ if (!compactionMode && multiAccount && this.config.crossAccountResume !== false && sessionBeforeAccountCheck) {
13908
+ const carry = await carryTranscriptToAccount({
13909
+ sessionId: sessionBeforeAccountCheck,
13910
+ cwd,
13911
+ targetConfigDir: resolveConfigDir(this.skillBridgeSpawn(failover).configDir),
13912
+ // Every account's own directory, plus this model's configured one:
13913
+ // `configDir` is an option, so an account's directory is not always
13914
+ // the `~/.claude-<name>` the name alone would build.
13915
+ accountConfigDirs: [
13916
+ ...(this.config.failoverAccounts ?? []).map(configDirForAccount),
13917
+ resolveConfigDir(this.config.configDir)
13918
+ ]
13919
+ });
13920
+ if (carry.kind === "carried" || carry.kind === "already-there") {
13921
+ if (carry.sessionId !== sessionBeforeAccountCheck) {
13922
+ setClaudeSessionId(sk, carry.sessionId);
13923
+ }
13924
+ conversationOnTarget = true;
13925
+ if (carry.kind === "carried") {
13926
+ log.notice("carried this conversation's claude transcript to the other account", {
13927
+ sessionKey: sk,
13928
+ renamed: carry.renamed,
13929
+ sessionId: carry.sessionId,
13930
+ failedOver: failover.failedOver
13931
+ });
13932
+ }
13933
+ } else if (!getActiveProcess(sk)) {
13934
+ log.notice("replaying this conversation as text on the other account", {
13935
+ sessionKey: sk,
13936
+ reason: carry.reason,
13937
+ failedOver: failover.failedOver
13938
+ });
13939
+ deleteClaudeSessionId(sk);
13940
+ }
13941
+ } else if (!compactionMode && sessionBeforeAccountCheck && processForAccount?.cliPath && processForAccount.cliPath !== cliPath) {
13831
13942
  deleteClaudeSessionId(sk);
13832
13943
  }
13833
13944
  const hasExistingSession = !!getClaudeSessionId(sk);
13834
13945
  const hasActiveProcess = !!getActiveProcess(sk);
13835
13946
  let includeHistoryContext = !hasExistingSession && !hasActiveProcess && hasPriorConversation;
13836
- if (failoverAnswer?.kind === "switch" && hasPriorConversation) {
13947
+ if (failoverAnswer?.kind === "switch" && hasPriorConversation && !conversationOnTarget) {
13837
13948
  includeHistoryContext = true;
13838
13949
  }
13839
13950
  let forkFromClaudeSessionId;
@@ -13879,12 +13990,12 @@ var ClaudeCodeLanguageModel = class {
13879
13990
  let replayReason = "no-session-for-key";
13880
13991
  if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
13881
13992
  const configDir = resolveConfigDir(this.config.configDir);
13882
- const transcriptPath = (id) => join12(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
13993
+ const transcriptPath2 = (id) => join12(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
13883
13994
  const resumePoint = findResumePoint({
13884
13995
  sessionKey: sk,
13885
13996
  prompt: options.prompt,
13886
13997
  cliPath,
13887
- transcriptPath,
13998
+ transcriptPath: transcriptPath2,
13888
13999
  onRefused: (reason) => {
13889
14000
  replayReason = reason;
13890
14001
  }
@@ -13901,7 +14012,7 @@ var ClaudeCodeLanguageModel = class {
13901
14012
  sessionKey: sk,
13902
14013
  prompt: options.prompt,
13903
14014
  cliPath,
13904
- transcriptPath,
14015
+ transcriptPath: transcriptPath2,
13905
14016
  // `claudeSessionIsWriting`, not `getActiveProcess`: this walks every
13906
14017
  // sibling key, and that one refreshes LRU order and cancels idle
13907
14018
  // timers on everything it touches (the same reason the fork lookup
@@ -13946,7 +14057,11 @@ var ClaudeCodeLanguageModel = class {
13946
14057
  log.info("sending plan approval decision to claude", { sk });
13947
14058
  }
13948
14059
  const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
13949
- const effectivePrompt = failoverAnswer?.kind === "switch" ? buildFailoverContinuationPrompt(options.prompt, failoverAnswer.target) : options.prompt;
14060
+ const effectivePrompt = failoverAnswer?.kind === "switch" ? buildFailoverContinuationPrompt(
14061
+ options.prompt,
14062
+ failoverAnswer.target,
14063
+ conversationOnTarget
14064
+ ) : options.prompt;
13950
14065
  const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(effectivePrompt, includeHistoryContext, {
13951
14066
  compactionMode,
13952
14067
  compactionInstructions: useInteractive && compactionMode ? extractSystemMessages(effectivePrompt).join("\n\n") : void 0,
@@ -14628,7 +14743,8 @@ var ClaudeCodeLanguageModel = class {
14628
14743
  candidates: failoverAccounts,
14629
14744
  resetsAt: state.accountLimitHit?.resetsAt,
14630
14745
  window: state.accountLimitHit?.window,
14631
- reason: state.accountLimitHit || !state.accountBlock ? void 0 : describeAccountBlock(state.accountBlock)
14746
+ reason: state.accountLimitHit || !state.accountBlock ? void 0 : describeAccountBlock(state.accountBlock),
14747
+ carryTranscript: self.config.crossAccountResume !== false
14632
14748
  });
14633
14749
  log.warn(
14634
14750
  `Claude account "${sourceAccount}" ${state.accountLimitHit || !state.accountBlock ? "is out of usage" : `cannot serve (${state.accountBlock})`}; asking which account to continue on.`,
@@ -15346,7 +15462,7 @@ function registerOpencodeLogSink(client) {
15346
15462
  }
15347
15463
 
15348
15464
  // src/v2-client.ts
15349
- import path11 from "path";
15465
+ import path12 from "path";
15350
15466
  function formatAgentTypeList(agents) {
15351
15467
  const lines = agents.flatMap((agent) => {
15352
15468
  if (agent.hidden === true || agent.mode === "primary") return [];
@@ -15375,7 +15491,7 @@ function toV1Session(info) {
15375
15491
  if (!info || typeof info !== "object") return void 0;
15376
15492
  const session = info;
15377
15493
  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;
15494
+ const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path12.resolve(root, session.subpath) : root : void 0;
15379
15495
  return { ...info, directory, parentID: session.parentID };
15380
15496
  }
15381
15497
  function toV1Messages(messages) {
@@ -15934,6 +16050,7 @@ function createClaudeCode(settings = {}) {
15934
16050
  forkSessions: settings.forkSessions === true,
15935
16051
  resumeAfterRestart: settings.resumeAfterRestart !== false,
15936
16052
  resumeAcrossModelChanges: settings.resumeAcrossModelChanges !== false,
16053
+ crossAccountResume: settings.crossAccountResume !== false,
15937
16054
  transport: settings.transport,
15938
16055
  interactive: settings.interactive,
15939
16056
  interactiveBypass: settings.interactiveBypass,