@khalilgharbaoui/opencode-claude-code-plugin 0.26.2 → 0.27.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
@@ -514,6 +514,9 @@ function translateStreamForHost(stream, dialect = "v1") {
514
514
  );
515
515
  }
516
516
 
517
+ // src/account-failover.ts
518
+ import { homedir as homedir2 } from "os";
519
+
517
520
  // src/accounts.ts
518
521
  import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
519
522
  import path from "path";
@@ -550,6 +553,10 @@ function accountConfigDir(account) {
550
553
  if (!normalized || normalized === DEFAULT_ACCOUNT) return void 0;
551
554
  return `~/.claude-${normalized}`;
552
555
  }
556
+ function accountConfigDirPath(account) {
557
+ const dir = accountConfigDir(account);
558
+ return dir ? expandHome(dir) : void 0;
559
+ }
553
560
  function expandHome(value) {
554
561
  const home = process.env.HOME ?? process.env.USERPROFILE;
555
562
  if (value === "~") return home ?? value;
@@ -875,6 +882,27 @@ function reportCompactBoundary(msg) {
875
882
  });
876
883
  return formatCompactBoundaryNote(boundary);
877
884
  }
885
+ var CONVERSATION_RESET_MARKER = "\u258C **claude code reset:**";
886
+ function parseConversationReset(msg) {
887
+ if (msg.type !== "conversation_reset") return null;
888
+ const newConversationId = str(msg.new_conversation_id);
889
+ if (!newConversationId) return null;
890
+ return { newConversationId, previousSessionId: str(msg.session_id) };
891
+ }
892
+ function formatConversationResetNote() {
893
+ return `
894
+ ${CONVERSATION_RESET_MARKER} Claude Code cleared its conversation, so from here on Claude does not see the earlier messages this chat still shows. Start a new opencode session for a clean slate, or restate what matters.
895
+ `;
896
+ }
897
+ function reportConversationReset(msg) {
898
+ const reset = parseConversationReset(msg);
899
+ if (!reset) return null;
900
+ log.notice("claude code reset its conversation", {
901
+ newConversationId: reset.newConversationId,
902
+ previousSessionId: reset.previousSessionId ?? null
903
+ });
904
+ return formatConversationResetNote();
905
+ }
878
906
  var RESULT_ERROR_MARKER = "\u258C **claude code error:**";
879
907
  var RESULT_SUBTYPES = {
880
908
  error_max_turns: "it hit its internal turn limit before finishing",
@@ -1082,6 +1110,35 @@ var ACCOUNT_LIMIT_PATTERNS = [
1082
1110
  /third-party apps now draw from your extra usage/i,
1083
1111
  /you[’'`]?ve hit your individual spend limit/i
1084
1112
  ];
1113
+ var ACCOUNT_BLOCK_MARKER = "\u258C **claude account:**";
1114
+ var ACCOUNT_BLOCKS = {
1115
+ authentication_failed: { what: "is not logged in (its login expired or was revoked)", login: true },
1116
+ oauth_org_not_allowed: { what: "belongs to an organization that does not allow this login", login: false },
1117
+ account_on_hold: { what: "is on hold", login: false },
1118
+ verification_required: { what: "needs to be verified at claude.ai", login: false },
1119
+ billing_error: { what: "has a billing problem", login: false }
1120
+ };
1121
+ function accountBlockKind(msg) {
1122
+ if (msg.type !== "assistant" || typeof msg.error !== "string") return null;
1123
+ return Object.prototype.hasOwnProperty.call(ACCOUNT_BLOCKS, msg.error) ? msg.error : null;
1124
+ }
1125
+ function describeAccountBlock(kind) {
1126
+ return ACCOUNT_BLOCKS[kind].what;
1127
+ }
1128
+ function loginCommandFor(configDir, home = homedir2()) {
1129
+ if (!configDir) return "claude auth login";
1130
+ const shown = configDir === home || configDir.startsWith(`${home}/`) ? `~${configDir.slice(home.length)}` : configDir;
1131
+ return `CLAUDE_CONFIG_DIR=${shown} claude auth login`;
1132
+ }
1133
+ function formatAccountBlockNote(input) {
1134
+ const block = ACCOUNT_BLOCKS[input.kind];
1135
+ const account = normalizeAccountName(input.account || DEFAULT_ACCOUNT);
1136
+ const fix = block.login ? `Log in again with \`${loginCommandFor(input.configDir)}\`, then resend your message.` : "Check the account at claude.ai, then resend your message.";
1137
+ const then = input.offeringSwitch ? " Or pick another account below." : "";
1138
+ return `
1139
+ ${ACCOUNT_BLOCK_MARKER} the Claude account "${account}" ${block.what}. ${fix}${then}
1140
+ `;
1141
+ }
1085
1142
  function isAccountLimitError(input) {
1086
1143
  if (input.rateLimit && isRateLimitRejected(input.rateLimit)) return true;
1087
1144
  const text = input.resultText;
@@ -1187,7 +1244,7 @@ function createAccountFailoverQuestionCall(sessionKey2, input, toolCallId = `${A
1187
1244
  const candidates = input.candidates.map((c) => normalizeAccountName(c));
1188
1245
  const resets = describeReset(input.resetsAt);
1189
1246
  const until = resets ?? "opencode restarts";
1190
- const question = [
1247
+ const question = input.reason ? `The Claude account "${source}" ${input.reason}. Continue this task on another configured account? Leaving this unanswered waits, at no cost.` : [
1191
1248
  `The Claude account "${source}" is out of usage`,
1192
1249
  input.window ? ` in ${input.window}` : "",
1193
1250
  resets ? `, which resets at ${resets}` : "",
@@ -4689,11 +4746,13 @@ var PLUGIN_NOTE_MARKERS = [
4689
4746
  ...LEGACY_INLINE_ASIDE_MARKERS,
4690
4747
  TURN_STATS_MARKER,
4691
4748
  COMPACT_BOUNDARY_MARKER,
4749
+ CONVERSATION_RESET_MARKER,
4692
4750
  RATE_LIMIT_MARKER,
4693
4751
  RESULT_ERROR_MARKER,
4694
4752
  DOCTOR_MARKER,
4695
4753
  STREAM_TIMEOUT_MARKER,
4696
- FAILOVER_MARKER
4754
+ FAILOVER_MARKER,
4755
+ ACCOUNT_BLOCK_MARKER
4697
4756
  ];
4698
4757
  function isPluginNote(part) {
4699
4758
  if (!part || part.type !== "text" || typeof part.text !== "string") return false;
@@ -5539,13 +5598,48 @@ function dirExists2(p) {
5539
5598
  return false;
5540
5599
  }
5541
5600
  }
5542
- function fileExists2(p) {
5601
+ function realPath(p) {
5543
5602
  try {
5544
- return fs5.statSync(p).isFile();
5603
+ return fs5.realpathSync(p);
5545
5604
  } catch {
5546
- return false;
5605
+ return path7.resolve(p);
5547
5606
  }
5548
5607
  }
5608
+ function envEnabled(name) {
5609
+ const value = process.env[name];
5610
+ if (value === void 0) return false;
5611
+ const normalized = value.trim().toLowerCase();
5612
+ return normalized !== "" && normalized !== "0" && normalized !== "false";
5613
+ }
5614
+ var SAFE_SKILL_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
5615
+ var FRONTMATTER_BLOCK = /^---\r?\n([\s\S]*?)\r?\n---/;
5616
+ var FRONTMATTER_NAME = /^name:[ \t]*(.+?)[ \t]*$/m;
5617
+ function declaredSkillName(source) {
5618
+ const block = FRONTMATTER_BLOCK.exec(source);
5619
+ if (!block) return null;
5620
+ const line = FRONTMATTER_NAME.exec(block[1]);
5621
+ if (!line) return null;
5622
+ const name = line[1].trim().replace(/^["']|["']$/g, "").trim();
5623
+ return SAFE_SKILL_NAME.test(name) ? name : null;
5624
+ }
5625
+ function readSkill(root, entry) {
5626
+ const dir = path7.join(root, entry);
5627
+ let source;
5628
+ try {
5629
+ source = fs5.readFileSync(path7.join(dir, "SKILL.md"), "utf8");
5630
+ } catch {
5631
+ return null;
5632
+ }
5633
+ return {
5634
+ name: declaredSkillName(source) ?? entry,
5635
+ dir,
5636
+ realDir: realPath(dir),
5637
+ contentHash: crypto3.createHash("sha256").update(source).digest("hex")
5638
+ };
5639
+ }
5640
+ function sameSkill(a, b) {
5641
+ return a.realDir === b.realDir || a.contentHash === b.contentHash;
5642
+ }
5549
5643
  function skillRoots(cwd) {
5550
5644
  const roots = [];
5551
5645
  const seen = /* @__PURE__ */ new Set();
@@ -5555,19 +5649,36 @@ function skillRoots(cwd) {
5555
5649
  seen.add(abs);
5556
5650
  if (dirExists2(abs)) roots.push(abs);
5557
5651
  };
5652
+ const home = os3.homedir();
5653
+ const homeDir = home ? path7.resolve(home) : null;
5558
5654
  let current = path7.resolve(cwd);
5559
5655
  while (true) {
5560
5656
  push(path7.join(current, ".opencode", "skills"));
5657
+ if (current !== homeDir) {
5658
+ push(path7.join(current, ".claude", "skills"));
5659
+ push(path7.join(current, ".agents", "skills"));
5660
+ }
5561
5661
  const parent = path7.dirname(current);
5562
5662
  if (parent === current) break;
5563
5663
  current = parent;
5564
5664
  }
5565
- const home = os3.homedir();
5566
- if (home) push(path7.join(home, ".opencode", "skills"));
5665
+ if (homeDir) push(path7.join(homeDir, ".opencode", "skills"));
5567
5666
  const envDir = process.env.OPENCODE_CONFIG_DIR;
5568
- if (envDir) push(path7.join(envDir, "skills"));
5569
- const xdg = process.env.XDG_CONFIG_HOME ?? (home ? path7.join(home, ".config") : null);
5570
- if (xdg) push(path7.join(xdg, "opencode", "skills"));
5667
+ if (envDir) {
5668
+ push(path7.join(envDir, "skills"));
5669
+ push(path7.join(envDir, "skill"));
5670
+ }
5671
+ const xdg = process.env.XDG_CONFIG_HOME ?? (homeDir ? path7.join(homeDir, ".config") : null);
5672
+ if (xdg) {
5673
+ push(path7.join(xdg, "opencode", "skills"));
5674
+ push(path7.join(xdg, "opencode", "skill"));
5675
+ }
5676
+ if (homeDir && !envEnabled("OPENCODE_DISABLE_EXTERNAL_SKILLS")) {
5677
+ if (!envEnabled("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")) {
5678
+ push(path7.join(homeDir, ".claude", "skills"));
5679
+ }
5680
+ push(path7.join(homeDir, ".agents", "skills"));
5681
+ }
5571
5682
  return roots;
5572
5683
  }
5573
5684
  function collectSkills(root, claimed, found) {
@@ -5579,19 +5690,28 @@ function collectSkills(root, claimed, found) {
5579
5690
  }
5580
5691
  for (const entry of entries) {
5581
5692
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
5582
- const name = entry.name;
5583
- if (name.startsWith(".")) continue;
5584
- if (claimed.has(name)) continue;
5585
- const dir = path7.join(root, name);
5586
- if (!fileExists2(path7.join(dir, "SKILL.md"))) continue;
5587
- claimed.add(name);
5588
- found.push({ name, dir });
5693
+ if (entry.name.startsWith(".")) continue;
5694
+ const skill = readSkill(root, entry.name);
5695
+ if (!skill) continue;
5696
+ const winner = claimed.get(skill.name);
5697
+ if (winner) {
5698
+ if (!sameSkill(winner, skill)) {
5699
+ log.warn("two different skills share a name; bridging the one the nearest root claimed", {
5700
+ name: skill.name,
5701
+ bridged: winner.dir,
5702
+ ignored: skill.dir
5703
+ });
5704
+ }
5705
+ continue;
5706
+ }
5707
+ claimed.set(skill.name, skill);
5708
+ found.push(skill);
5589
5709
  }
5590
5710
  }
5591
5711
  var byName = (a, b) => a.name.localeCompare(b.name);
5592
5712
  function discoverOpencodeSkills(cwd) {
5593
5713
  const found = [];
5594
- const claimed = /* @__PURE__ */ new Set();
5714
+ const claimed = /* @__PURE__ */ new Map();
5595
5715
  for (const root of skillRoots(cwd)) collectSkills(root, claimed, found);
5596
5716
  return found.sort(byName);
5597
5717
  }
@@ -5599,17 +5719,105 @@ function discoverBundledSkills() {
5599
5719
  const root = bundledSkillsDir();
5600
5720
  if (!root) return [];
5601
5721
  const found = [];
5602
- collectSkills(root, /* @__PURE__ */ new Set(), found);
5722
+ collectSkills(root, /* @__PURE__ */ new Map(), found);
5603
5723
  return found.sort(byName);
5604
5724
  }
5605
- function registerBundledSkillPath(config) {
5606
- const dir = bundledSkillsDir();
5607
- if (!dir) return false;
5608
- config.skills ??= {};
5609
- const paths = config.skills.paths ??= [];
5610
- if (paths.some((p) => path7.resolve(p) === dir)) return false;
5611
- paths.push(dir);
5612
- return true;
5725
+ function claudeConfigDir(configDir) {
5726
+ const explicit = configDir ?? process.env.CLAUDE_CONFIG_DIR;
5727
+ if (explicit) return path7.resolve(expandHome(explicit));
5728
+ const home = os3.homedir();
5729
+ return home ? path7.resolve(home, ".claude") : null;
5730
+ }
5731
+ function installedPluginSkillRoots(configDir, cwd) {
5732
+ if (!configDir) return [];
5733
+ let parsed;
5734
+ try {
5735
+ parsed = JSON.parse(
5736
+ fs5.readFileSync(path7.join(configDir, "plugins", "installed_plugins.json"), "utf8")
5737
+ );
5738
+ } catch {
5739
+ return [];
5740
+ }
5741
+ const plugins = parsed?.plugins;
5742
+ if (!plugins || typeof plugins !== "object") return [];
5743
+ const workspace = path7.resolve(cwd);
5744
+ const roots = [];
5745
+ for (const installs of Object.values(plugins)) {
5746
+ if (!Array.isArray(installs)) continue;
5747
+ for (const install of installs) {
5748
+ const entry = install;
5749
+ if (typeof entry?.installPath !== "string" || !entry.installPath) continue;
5750
+ if (entry.scope !== "user" && !coversWorkspace(entry.projectPath, workspace)) continue;
5751
+ roots.push(path7.join(entry.installPath, "skills"));
5752
+ }
5753
+ }
5754
+ return roots;
5755
+ }
5756
+ function coversWorkspace(projectPath, workspace) {
5757
+ if (typeof projectPath !== "string" || !projectPath) return false;
5758
+ const root = path7.resolve(projectPath);
5759
+ if (root === workspace) return true;
5760
+ const rel = path7.relative(root, workspace);
5761
+ return rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
5762
+ }
5763
+ function discoverNativeClaudeSkills(opts) {
5764
+ const found = [];
5765
+ const scanned = /* @__PURE__ */ new Set();
5766
+ const scan = (root, scope) => {
5767
+ const abs = path7.resolve(root);
5768
+ if (scanned.has(abs)) return;
5769
+ scanned.add(abs);
5770
+ let entries;
5771
+ try {
5772
+ entries = fs5.readdirSync(abs, { withFileTypes: true });
5773
+ } catch {
5774
+ return;
5775
+ }
5776
+ for (const entry of entries) {
5777
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
5778
+ if (entry.name.startsWith(".")) continue;
5779
+ const skill = readSkill(abs, entry.name);
5780
+ if (skill) found.push({ ...skill, scope });
5781
+ }
5782
+ };
5783
+ const configDir = claudeConfigDir(opts.configDir);
5784
+ if (configDir) scan(path7.join(configDir, "skills"), "user");
5785
+ const home = os3.homedir();
5786
+ const homeDir = home ? path7.resolve(home) : null;
5787
+ let current = path7.resolve(opts.cwd);
5788
+ while (true) {
5789
+ if (current !== homeDir) scan(path7.join(current, ".claude", "skills"), "project");
5790
+ const parent = path7.dirname(current);
5791
+ if (parent === current) break;
5792
+ current = parent;
5793
+ }
5794
+ for (const root of installedPluginSkillRoots(configDir, opts.cwd)) scan(root, "plugin");
5795
+ return found;
5796
+ }
5797
+ function dropNativelyLoadedSkills(skills, native) {
5798
+ if (native.length === 0) return { bridged: skills, skipped: [] };
5799
+ const bridged = [];
5800
+ const skipped = [];
5801
+ for (const skill of skills) {
5802
+ const sameCopy = native.find((n) => n.realDir === skill.realDir);
5803
+ const sameContent = sameCopy ?? native.find((n) => n.contentHash === skill.contentHash);
5804
+ const nameTaken = sameContent ?? native.find((n) => n.scope !== "plugin" && n.name === skill.name);
5805
+ if (!nameTaken) {
5806
+ bridged.push(skill);
5807
+ continue;
5808
+ }
5809
+ const reason = sameCopy ? "same-copy" : sameContent ? "same-content" : "name-taken";
5810
+ skipped.push({ skill, native: nameTaken, reason });
5811
+ if (reason === "name-taken") {
5812
+ log.warn("claude already registers a different skill under this name; not bridging ours", {
5813
+ name: skill.name,
5814
+ claudeLoads: nameTaken.dir,
5815
+ notBridged: skill.dir,
5816
+ scope: nameTaken.scope
5817
+ });
5818
+ }
5819
+ }
5820
+ return { bridged, skipped };
5613
5821
  }
5614
5822
  function readBundledSkillInfos() {
5615
5823
  return discoverBundledSkills().flatMap((skill) => {
@@ -5646,6 +5854,13 @@ function linkSkill(source, target) {
5646
5854
  fs5.cpSync(source, target, { recursive: true, dereference: true });
5647
5855
  }
5648
5856
  }
5857
+ function fileExists2(p) {
5858
+ try {
5859
+ return fs5.statSync(p).isFile();
5860
+ } catch {
5861
+ return false;
5862
+ }
5863
+ }
5649
5864
  function buildSkillPluginDir(skills) {
5650
5865
  if (skills.length === 0) return null;
5651
5866
  const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
@@ -5681,11 +5896,36 @@ function buildSkillPluginDir(skills) {
5681
5896
  }
5682
5897
  return root;
5683
5898
  }
5899
+ function registerBundledSkillPath(config) {
5900
+ const dir = bundledSkillsDir();
5901
+ if (!dir) return false;
5902
+ config.skills ??= {};
5903
+ const paths = config.skills.paths ??= [];
5904
+ if (paths.some((p) => path7.resolve(p) === dir)) return false;
5905
+ paths.push(dir);
5906
+ return true;
5907
+ }
5684
5908
  async function resolveSkillPluginDirs(opts) {
5685
5909
  const user = opts.enabled ? discoverOpencodeSkills(opts.cwd) : [];
5686
5910
  const claimed = new Set(user.map((s) => s.name));
5687
5911
  const bundled = discoverBundledSkills().filter((s) => !claimed.has(s.name));
5688
- const skills = [...user, ...bundled].sort(byName);
5912
+ let skills = [...user, ...bundled].sort(byName);
5913
+ if (opts.skipNative !== false && skills.length > 0) {
5914
+ const native = discoverNativeClaudeSkills({ cwd: opts.cwd, configDir: opts.configDir });
5915
+ const { bridged, skipped } = dropNativelyLoadedSkills(skills, native);
5916
+ if (skipped.length > 0) {
5917
+ log.info("skills claude code already loads; left unbridged", {
5918
+ count: skipped.length,
5919
+ skipped: skipped.map((s) => ({
5920
+ name: s.skill.name,
5921
+ reason: s.reason,
5922
+ opencode: s.skill.dir,
5923
+ claude: s.native.dir
5924
+ }))
5925
+ });
5926
+ }
5927
+ skills = bridged;
5928
+ }
5689
5929
  if (skills.length === 0) return [];
5690
5930
  const supported = await detectCliSupportsFlag(opts.cliPath, "--plugin-dir");
5691
5931
  if (!supported) {
@@ -6289,7 +6529,7 @@ function spawnInteractiveProcess(opts) {
6289
6529
  // src/claude-code-language-model.ts
6290
6530
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
6291
6531
  import { unlink as unlink3 } from "fs/promises";
6292
- import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
6532
+ import { homedir as homedir6, tmpdir as tmpdir2 } from "os";
6293
6533
  import { randomUUID as randomUUID5 } from "crypto";
6294
6534
  import { dirname as dirname5, join as join8 } from "path";
6295
6535
  async function isProxyCallStillServed(callId) {
@@ -6653,7 +6893,7 @@ ${options.compressionSummary.trim()}`
6653
6893
  for (const s of extraSystemContent) {
6654
6894
  if (s.trim()) parts.push(s.trim());
6655
6895
  }
6656
- const configRoot = process.env.XDG_CONFIG_HOME ?? join8(homedir5(), ".config");
6896
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join8(homedir6(), ".config");
6657
6897
  const globalAgents = readPromptFileIfPresent(join8(configRoot, "opencode", "AGENTS.md"));
6658
6898
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
6659
6899
  const forwarded = extraSystemContent.join("\n\n");
@@ -6927,6 +7167,18 @@ var ClaudeCodeLanguageModel = class {
6927
7167
  proxyOpencodeTools: this.config.proxyOpencodeTools
6928
7168
  });
6929
7169
  }
7170
+ /**
7171
+ * Arguments the skill bridge needs beyond `cwd` / `cliPath`: which
7172
+ * `CLAUDE_CONFIG_DIR` this spawn reads its native skills from, and whether
7173
+ * to drop the ones it already loads. A failover moves the spawn to another
7174
+ * account, and therefore to that account's config dir.
7175
+ */
7176
+ skillBridgeSpawn(failover) {
7177
+ return {
7178
+ configDir: failover.failedOver && failover.target ? accountConfigDirPath(failover.target) : this.config.configDir,
7179
+ skipNative: this.config.bridgeSkipNativeSkills !== false
7180
+ };
7181
+ }
6930
7182
  /** Share one lazy registry request within a turn without making it stale. */
6931
7183
  createLiveToolInfoLoader() {
6932
7184
  let pending;
@@ -7445,7 +7697,8 @@ var ClaudeCodeLanguageModel = class {
7445
7697
  const skillPluginDirs = await resolveSkillPluginDirs({
7446
7698
  cwd,
7447
7699
  cliPath,
7448
- enabled: this.config.bridgeOpencodeSkills === true
7700
+ enabled: this.config.bridgeOpencodeSkills === true,
7701
+ ...this.skillBridgeSpawn(failover)
7449
7702
  });
7450
7703
  const cliArgs = buildCliArgs({
7451
7704
  sessionKey: sk,
@@ -8175,7 +8428,8 @@ ${plan}
8175
8428
  const skillPluginDirs = await resolveSkillPluginDirs({
8176
8429
  cwd,
8177
8430
  cliPath,
8178
- enabled: self.config.bridgeOpencodeSkills === true
8431
+ enabled: self.config.bridgeOpencodeSkills === true,
8432
+ ...self.skillBridgeSpawn(failover)
8179
8433
  });
8180
8434
  const ap = spawnInteractiveProcess({
8181
8435
  cwd,
@@ -8331,7 +8585,8 @@ ${plan}
8331
8585
  const skillPluginDirs = await resolveSkillPluginDirs({
8332
8586
  cwd,
8333
8587
  cliPath,
8334
- enabled: self.config.bridgeOpencodeSkills === true
8588
+ enabled: self.config.bridgeOpencodeSkills === true,
8589
+ ...self.skillBridgeSpawn(failover)
8335
8590
  });
8336
8591
  cliArgs = buildCliArgs({
8337
8592
  sessionKey: sk,
@@ -8591,6 +8846,7 @@ ${plan}
8591
8846
  let resultMeta = {};
8592
8847
  let resultFailure;
8593
8848
  let accountLimitHit = null;
8849
+ let accountBlock = null;
8594
8850
  const drainBuffer = [];
8595
8851
  let drainTimer = null;
8596
8852
  const DRAIN_QUIET_MS = 100;
@@ -8752,20 +9008,21 @@ ${plan}
8752
9008
  });
8753
9009
  }
8754
9010
  activeProcess?.pendingProxyCompletions?.clear();
8755
- if (accountLimitHit && failoverAskActive && msg.is_error === true) {
9011
+ if ((accountLimitHit || accountBlock) && failoverAskActive && msg.is_error === true) {
8756
9012
  const call = createAccountFailoverQuestionCall(sk, {
8757
9013
  sourceAccount,
8758
9014
  candidates: failoverAccounts,
8759
- resetsAt: accountLimitHit.resetsAt,
8760
- window: accountLimitHit.window
9015
+ resetsAt: accountLimitHit?.resetsAt,
9016
+ window: accountLimitHit?.window,
9017
+ reason: accountLimitHit || !accountBlock ? void 0 : describeAccountBlock(accountBlock)
8761
9018
  });
8762
9019
  log.warn(
8763
- `Claude account "${sourceAccount}" is out of usage; asking which account to continue on.`,
9020
+ `Claude account "${sourceAccount}" ${accountLimitHit || !accountBlock ? "is out of usage" : `cannot serve (${accountBlock})`}; asking which account to continue on.`,
8764
9021
  {
8765
9022
  sessionKey: sk,
8766
9023
  candidates: failoverAccounts,
8767
9024
  toolCallId: call.toolCallId,
8768
- resetsAt: accountLimitHit.resetsAt ?? null
9025
+ resetsAt: accountLimitHit?.resetsAt ?? null
8769
9026
  }
8770
9027
  );
8771
9028
  finishWithQuestionCall(call);
@@ -8903,6 +9160,20 @@ ${plan}
8903
9160
  endTextBlock();
8904
9161
  }
8905
9162
  }
9163
+ if (msg.type === "conversation_reset") {
9164
+ const note = reportConversationReset(msg);
9165
+ if (note) {
9166
+ toolCallMap.clear();
9167
+ reasoningIds.clear();
9168
+ reasoningStarted.clear();
9169
+ textBlockIndices.clear();
9170
+ controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: note });
9171
+ endTextBlock();
9172
+ }
9173
+ return;
9174
+ }
9175
+ const block = accountBlockKind(msg);
9176
+ if (block) accountBlock = block;
8906
9177
  if (msg.type === "rate_limit_event") {
8907
9178
  const info = parseRateLimitEvent(msg);
8908
9179
  if (info && isRateLimitRejected(info)) {
@@ -8919,58 +9190,58 @@ ${plan}
8919
9190
  return;
8920
9191
  }
8921
9192
  if (msg.type === "content_block_start" && msg.content_block && msg.index !== void 0) {
8922
- const block = msg.content_block;
9193
+ const block2 = msg.content_block;
8923
9194
  const idx = msg.index;
8924
- if (block.type === "thinking") {
9195
+ if (block2.type === "thinking") {
8925
9196
  noteReasoning();
8926
9197
  const reasoningId = generateId();
8927
9198
  reasoningIds.set(idx, reasoningId);
8928
9199
  }
8929
- if (block.type === "text") {
9200
+ if (block2.type === "text") {
8930
9201
  textBlockIndices.add(idx);
8931
9202
  resetLastVisibleTextBlock();
8932
- if (block.text) {
9203
+ if (block2.text) {
8933
9204
  if (!currentTextId) startTextBlock();
8934
9205
  controller.enqueue({
8935
9206
  type: "text-delta",
8936
9207
  id: currentTextId,
8937
- delta: block.text
9208
+ delta: block2.text
8938
9209
  });
8939
- noteVisibleText(block.text);
9210
+ noteVisibleText(block2.text);
8940
9211
  hasReceivedContent = true;
8941
9212
  }
8942
9213
  }
8943
- if (block.type === "tool_use" && block.id && block.name) {
9214
+ if (block2.type === "tool_use" && block2.id && block2.name) {
8944
9215
  noteToolActivity();
8945
9216
  const entry = {
8946
- id: block.id,
8947
- name: block.name,
9217
+ id: block2.id,
9218
+ name: block2.name,
8948
9219
  inputJson: "",
8949
9220
  started: false
8950
9221
  };
8951
9222
  toolCallMap.set(idx, entry);
8952
- if (block.name !== "AskUserQuestion" && block.name !== "ask_user_question" && block.name !== "ExitPlanMode" && !block.name.startsWith(PROXY_TOOL_PREFIX)) {
9223
+ if (block2.name !== "AskUserQuestion" && block2.name !== "ask_user_question" && block2.name !== "ExitPlanMode" && !block2.name.startsWith(PROXY_TOOL_PREFIX)) {
8953
9224
  const { name: mappedName, skip, executed } = mapTool(
8954
- block.name,
9225
+ block2.name,
8955
9226
  void 0,
8956
9227
  {
8957
9228
  webSearch: self.config.webSearch,
8958
9229
  sessionId: getClaudeSessionId(sk),
8959
- toolUseId: block.id
9230
+ toolUseId: block2.id
8960
9231
  }
8961
9232
  );
8962
9233
  if (!skip) {
8963
9234
  entry.started = true;
8964
9235
  controller.enqueue({
8965
9236
  type: "tool-input-start",
8966
- id: block.id,
9237
+ id: block2.id,
8967
9238
  toolName: mappedName,
8968
9239
  providerExecuted: executed
8969
9240
  });
8970
9241
  log.info("tool started", {
8971
- name: block.name,
9242
+ name: block2.name,
8972
9243
  mappedName,
8973
- id: block.id
9244
+ id: block2.id
8974
9245
  });
8975
9246
  }
8976
9247
  }
@@ -9161,8 +9432,8 @@ ${plan}
9161
9432
  hadStreamThinking: hadThinkingTextFromStream
9162
9433
  });
9163
9434
  if (!hadThinkingTextFromStream) {
9164
- for (const block of thinkingBlocks) {
9165
- if (block.thinking && block.thinking.length > 0) {
9435
+ for (const block2 of thinkingBlocks) {
9436
+ if (block2.thinking && block2.thinking.length > 0) {
9166
9437
  noteReasoning();
9167
9438
  hadThinkingTextFromStream = true;
9168
9439
  const thinkingId = generateId();
@@ -9173,7 +9444,7 @@ ${plan}
9173
9444
  controller.enqueue({
9174
9445
  type: "reasoning-delta",
9175
9446
  id: thinkingId,
9176
- delta: block.thinking
9447
+ delta: block2.thinking
9177
9448
  });
9178
9449
  controller.enqueue({
9179
9450
  type: "reasoning-end",
@@ -9200,20 +9471,20 @@ ${plan}
9200
9471
  if (hasToolUse) {
9201
9472
  clearFallbackTimer();
9202
9473
  }
9203
- for (const block of msg.message.content) {
9204
- if (block.type === "text" && block.text) {
9474
+ for (const block2 of msg.message.content) {
9475
+ if (block2.type === "text" && block2.text) {
9205
9476
  resetLastVisibleTextBlock();
9206
9477
  const blockId = startTextBlock();
9207
9478
  controller.enqueue({
9208
9479
  type: "text-delta",
9209
9480
  id: blockId,
9210
- delta: block.text
9481
+ delta: block2.text
9211
9482
  });
9212
9483
  endTextBlock();
9213
- noteVisibleText(block.text);
9484
+ noteVisibleText(block2.text);
9214
9485
  hasReceivedContent = true;
9215
9486
  }
9216
- if (block.type === "thinking" && block.thinking) {
9487
+ if (block2.type === "thinking" && block2.thinking) {
9217
9488
  noteReasoning();
9218
9489
  const thinkingId = generateId();
9219
9490
  controller.enqueue({
@@ -9223,17 +9494,17 @@ ${plan}
9223
9494
  controller.enqueue({
9224
9495
  type: "reasoning-delta",
9225
9496
  id: thinkingId,
9226
- delta: block.thinking
9497
+ delta: block2.thinking
9227
9498
  });
9228
9499
  controller.enqueue({
9229
9500
  type: "reasoning-end",
9230
9501
  id: thinkingId
9231
9502
  });
9232
9503
  }
9233
- if (block.type === "tool_use" && block.id && block.name) {
9504
+ if (block2.type === "tool_use" && block2.id && block2.name) {
9234
9505
  noteToolActivity();
9235
- const parsedInput = block.input ?? {};
9236
- if (isAskUserQuestionTool(block.name)) {
9506
+ const parsedInput = block2.input ?? {};
9507
+ if (isAskUserQuestionTool(block2.name)) {
9237
9508
  const askId = startTextBlock();
9238
9509
  controller.enqueue({
9239
9510
  type: "text-delta",
@@ -9241,12 +9512,12 @@ ${plan}
9241
9512
  delta: formatAskUserQuestion(parsedInput)
9242
9513
  });
9243
9514
  endTextBlock();
9244
- } else if (block.name === "ExitPlanMode") {
9515
+ } else if (block2.name === "ExitPlanMode") {
9245
9516
  const plan = parsedInput?.plan || "";
9246
9517
  if (planModeQuestionActive) {
9247
9518
  const questionCall = createExitPlanModeQuestionCall(
9248
9519
  sk,
9249
- block.id,
9520
+ block2.id,
9250
9521
  plan
9251
9522
  );
9252
9523
  const planId2 = startTextBlock();
@@ -9271,8 +9542,8 @@ ${plan}
9271
9542
  `
9272
9543
  });
9273
9544
  endTextBlock();
9274
- } else if (isWebSearchTool(block.name) && isWebSearchHandledByCli(self.config.webSearch)) {
9275
- toolCallsById.delete(block.id);
9545
+ } else if (isWebSearchTool(block2.name) && isWebSearchHandledByCli(self.config.webSearch)) {
9546
+ toolCallsById.delete(block2.id);
9276
9547
  const query = typeof parsedInput?.query === "string" ? parsedInput.query : JSON.stringify(parsedInput);
9277
9548
  const searchId = startTextBlock();
9278
9549
  controller.enqueue({
@@ -9283,11 +9554,11 @@ ${plan}
9283
9554
  `
9284
9555
  });
9285
9556
  endTextBlock();
9286
- } else if (block.name.startsWith(PROXY_TOOL_PREFIX)) {
9557
+ } else if (block2.name.startsWith(PROXY_TOOL_PREFIX)) {
9287
9558
  noteProxyActivity();
9288
9559
  log.debug("ignoring proxy tool_use from assistant message", {
9289
- name: block.name,
9290
- id: block.id
9560
+ name: block2.name,
9561
+ id: block2.id
9291
9562
  });
9292
9563
  } else {
9293
9564
  const {
@@ -9295,61 +9566,61 @@ ${plan}
9295
9566
  input: mappedInput,
9296
9567
  executed,
9297
9568
  skip
9298
- } = mapTool(block.name, parsedInput, {
9569
+ } = mapTool(block2.name, parsedInput, {
9299
9570
  webSearch: self.config.webSearch,
9300
9571
  sessionId: getClaudeSessionId(sk),
9301
- toolUseId: block.id
9572
+ toolUseId: block2.id
9302
9573
  });
9303
9574
  if (!skip) {
9304
- toolCallsById.set(block.id, {
9305
- id: block.id,
9306
- name: block.name,
9575
+ toolCallsById.set(block2.id, {
9576
+ id: block2.id,
9577
+ name: block2.name,
9307
9578
  input: parsedInput
9308
9579
  });
9309
- if (!executed) skipResultForIds.add(block.id);
9580
+ if (!executed) skipResultForIds.add(block2.id);
9310
9581
  controller.enqueue({
9311
9582
  type: "tool-input-start",
9312
- id: block.id,
9583
+ id: block2.id,
9313
9584
  toolName: mappedName,
9314
9585
  providerExecuted: executed
9315
9586
  });
9316
9587
  controller.enqueue({
9317
9588
  type: "tool-call",
9318
- toolCallId: block.id,
9589
+ toolCallId: block2.id,
9319
9590
  toolName: mappedName,
9320
9591
  input: JSON.stringify(mappedInput),
9321
9592
  providerExecuted: executed
9322
9593
  });
9323
9594
  }
9324
9595
  log.info("tool_use from assistant message", {
9325
- name: block.name,
9596
+ name: block2.name,
9326
9597
  mappedName,
9327
- id: block.id,
9598
+ id: block2.id,
9328
9599
  executed
9329
9600
  });
9330
9601
  }
9331
9602
  }
9332
- if (block.type === "tool_result") {
9603
+ if (block2.type === "tool_result") {
9333
9604
  log.debug("tool_result", {
9334
- toolUseId: block.tool_use_id
9605
+ toolUseId: block2.tool_use_id
9335
9606
  });
9336
9607
  }
9337
9608
  }
9338
9609
  }
9339
9610
  if (msg.type === "user" && msg.message?.content) {
9340
- for (const block of msg.message.content) {
9341
- if (block.type === "tool_result" && block.tool_use_id) {
9342
- if (skipResultForIds.has(block.tool_use_id)) {
9611
+ for (const block2 of msg.message.content) {
9612
+ if (block2.type === "tool_result" && block2.tool_use_id) {
9613
+ if (skipResultForIds.has(block2.tool_use_id)) {
9343
9614
  log.debug("skipping tool-result (opencode runs it)", {
9344
- toolUseId: block.tool_use_id
9615
+ toolUseId: block2.tool_use_id
9345
9616
  });
9346
9617
  continue;
9347
9618
  }
9348
9619
  let resultText = "";
9349
- if (typeof block.content === "string") {
9350
- resultText = block.content;
9351
- } else if (Array.isArray(block.content)) {
9352
- resultText = block.content.filter(
9620
+ if (typeof block2.content === "string") {
9621
+ resultText = block2.content;
9622
+ } else if (Array.isArray(block2.content)) {
9623
+ resultText = block2.content.filter(
9353
9624
  (c) => c.type === "text" && typeof c.text === "string"
9354
9625
  ).map((c) => c.text).join("\n");
9355
9626
  }
@@ -9357,11 +9628,11 @@ ${plan}
9357
9628
  if (claudeSessionId) {
9358
9629
  const list2 = applyTaskCreateToolResult(
9359
9630
  claudeSessionId,
9360
- block.tool_use_id,
9631
+ block2.tool_use_id,
9361
9632
  resultText
9362
9633
  );
9363
9634
  if (list2) {
9364
- const synthId = `todowrite_${block.tool_use_id}`;
9635
+ const synthId = `todowrite_${block2.tool_use_id}`;
9365
9636
  controller.enqueue({
9366
9637
  type: "tool-input-start",
9367
9638
  id: synthId,
@@ -9385,12 +9656,12 @@ ${plan}
9385
9656
  noteToolActivity();
9386
9657
  }
9387
9658
  }
9388
- const toolCall = toolCallsById.get(block.tool_use_id);
9659
+ const toolCall = toolCallsById.get(block2.tool_use_id);
9389
9660
  if (toolCall) {
9390
- const isError = block.is_error === true;
9661
+ const isError = block2.is_error === true;
9391
9662
  controller.enqueue({
9392
9663
  type: "tool-result",
9393
- toolCallId: block.tool_use_id,
9664
+ toolCallId: block2.tool_use_id,
9394
9665
  toolName: toolCall.name,
9395
9666
  result: {
9396
9667
  output: resultText,
@@ -9402,11 +9673,11 @@ ${plan}
9402
9673
  });
9403
9674
  noteToolActivity();
9404
9675
  log.info("tool result emitted", {
9405
- toolUseId: block.tool_use_id,
9676
+ toolUseId: block2.tool_use_id,
9406
9677
  name: toolCall.name,
9407
9678
  isError
9408
9679
  });
9409
- toolCallsById.delete(block.tool_use_id);
9680
+ toolCallsById.delete(block2.tool_use_id);
9410
9681
  }
9411
9682
  }
9412
9683
  }
@@ -9432,6 +9703,26 @@ ${plan}
9432
9703
  })) {
9433
9704
  accountLimitHit = {};
9434
9705
  }
9706
+ if (accountBlock && msg.is_error) {
9707
+ resultFailure ??= accountBlock;
9708
+ const offeringSwitch = failoverAskActive;
9709
+ controller.enqueue({
9710
+ type: "text-delta",
9711
+ id: startTextBlock(),
9712
+ delta: formatAccountBlockNote({
9713
+ kind: accountBlock,
9714
+ account: sourceAccount,
9715
+ configDir: self.config.configDir,
9716
+ offeringSwitch
9717
+ })
9718
+ });
9719
+ endTextBlock();
9720
+ log.warn(`Claude account "${sourceAccount}" cannot serve requests`, {
9721
+ sessionKey: sk,
9722
+ kind: accountBlock,
9723
+ offeringSwitch
9724
+ });
9725
+ }
9435
9726
  const failure = describeResultFailure(msg);
9436
9727
  if (failure) {
9437
9728
  resultFailure = msg.subtype;
@@ -9873,11 +10164,11 @@ ${plan}
9873
10164
  import {
9874
10165
  existsSync as existsSync4,
9875
10166
  readFileSync as readFileSync6,
9876
- realpathSync,
10167
+ realpathSync as realpathSync2,
9877
10168
  rmSync as rmSync3,
9878
10169
  writeFileSync as writeFileSync5
9879
10170
  } from "fs";
9880
- import { homedir as homedir6 } from "os";
10171
+ import { homedir as homedir7 } from "os";
9881
10172
  import { join as join9, resolve as resolve4 } from "path";
9882
10173
  import { fileURLToPath as fileURLToPath3 } from "url";
9883
10174
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
@@ -9887,12 +10178,12 @@ function candidateCacheRoots() {
9887
10178
  const xdg = process.env.XDG_CACHE_HOME;
9888
10179
  return [
9889
10180
  xdg ? join9(xdg, "opencode") : null,
9890
- join9(homedir6(), ".cache", "opencode"),
9891
- join9(homedir6(), "Library", "Caches", "opencode")
10181
+ join9(homedir7(), ".cache", "opencode"),
10182
+ join9(homedir7(), "Library", "Caches", "opencode")
9892
10183
  ].filter((p) => Boolean(p));
9893
10184
  }
9894
10185
  function userOpencodeJsonPath() {
9895
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join9(homedir6(), ".config");
10186
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join9(homedir7(), ".config");
9896
10187
  return join9(xdgConfig, "opencode", "opencode.json");
9897
10188
  }
9898
10189
  function userIntendsToUseUnscoped() {
@@ -9912,7 +10203,7 @@ function userIntendsToUseUnscoped() {
9912
10203
  function ourLoadedDir() {
9913
10204
  try {
9914
10205
  const filePath = fileURLToPath3(import.meta.url);
9915
- return realpathSync(resolve4(filePath, "..", ".."));
10206
+ return realpathSync2(resolve4(filePath, "..", ".."));
9916
10207
  } catch {
9917
10208
  return null;
9918
10209
  }
@@ -9940,7 +10231,7 @@ function cleanupOne(cacheRoot, ourDir) {
9940
10231
  if (!existsSync4(stalePath)) return;
9941
10232
  let realStalePath = stalePath;
9942
10233
  try {
9943
- realStalePath = realpathSync(stalePath);
10234
+ realStalePath = realpathSync2(stalePath);
9944
10235
  } catch {
9945
10236
  }
9946
10237
  if (ourDir && realStalePath === ourDir) return;
@@ -10499,6 +10790,7 @@ function createClaudeCode(settings = {}) {
10499
10790
  ignoreAnthropicApiKey: settings.ignoreAnthropicApiKey,
10500
10791
  idleProcessTimeoutMs: settings.idleProcessTimeoutMs,
10501
10792
  bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
10793
+ bridgeSkipNativeSkills: settings.bridgeSkipNativeSkills !== false,
10502
10794
  turnStats: settings.turnStats === true,
10503
10795
  interactive: settings.interactive,
10504
10796
  interactiveBypass: settings.interactiveBypass,