@khalilgharbaoui/opencode-claude-code-plugin 0.26.3 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -366,7 +366,8 @@ model: claude-code-work/claude-opus-5@work
366
366
  | `compactionModel` | string | `"claude-haiku-4-5"` | Model used when opencode invokes `/compact`. Override per-process via the `CLAUDE_CODE_COMPACTION_MODEL` env var (env wins over config). See [Compaction](#compaction). |
367
367
  | `ignoreAnthropicApiKey` | boolean | `false` | Strip `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` from every spawned `claude` process so it authenticates with your logged-in subscription instead of pay-as-you-go API billing. The plugin warns once at startup whenever an API key is detected, regardless of this setting. See [Billing](#billing). |
368
368
  | `idleProcessTimeoutMs` | number | – | Kill a retained headless Claude worker after this many idle milliseconds following a completed turn. The timer starts when a turn finishes, a new turn cancels it, a worker that is mid-turn when it fires is left alone and re-timed, and the session id is preserved for `--resume`. Values above Node's maximum timer delay (`2147483647`) are ignored. Omit or set `0` to retain workers until LRU eviction (16 processes). Interactive transport is excluded. Contributed by [@bernardofortes](https://github.com/bernardofortes). |
369
- | `bridgeOpencodeSkills` | boolean | `false` | Expose your opencode skills to Claude's native `Skill` tool. Off by default because every bridged skill is also in the system prompt opencode forwards, so a large set is paid for twice per turn; the bundled configuration skill is staged either way. See [Skill bridge](#skill-bridge). Written by [@broskees](https://github.com/broskees). |
369
+ | `bridgeOpencodeSkills` | boolean | `false` | Expose your opencode skills to Claude's native `Skill` tool, from every root opencode itself reads. Off by default because every bridged skill is also in the system prompt opencode forwards, so a large set is paid for twice per turn; the bundled configuration skill is staged either way. See [Skill bridge](#skill-bridge). Written by [@broskees](https://github.com/broskees). |
370
+ | `bridgeSkipNativeSkills` | boolean | `true` | Leave a skill unbridged when the Claude session already loads it from `$CLAUDE_CONFIG_DIR/skills`, the project's `.claude/skills`, or an installed plugin, so one skill does not reach the model twice. Matched by resolved path, by identical `SKILL.md`, or by name. `false` bridges everything and reinstates the duplicates. See [Skills Claude already has](#skills-claude-already-has). |
370
371
  | `logging` | object | all defaults | The plugin's own logger, four independent fields: `file` (boolean, default `false`), `dir` (string, default `~/.local/share/opencode-claude-code/`), `mode` (`"silent"` \| `"debug"`, default `"silent"`) and `level` (`"debug"` \| `"info"` \| `"notice"` \| `"warn"` \| `"error"`, default `"info"`). Goes under `provider.claude-code.options` like every other row here. See [Logging](#logging). |
371
372
  | `turnStats` | boolean | `false` | Append a one-line cost / duration / cache footer to each finished turn. See [Per-turn stats](#per-turn-stats). |
372
373
  | `interactive` | boolean | `false` | **Experimental.** Drive the interactive `claude` TUI (subscription billing) instead of headless `--print`. Requires opencode running under Bun with PTY support; silently falls back to headless otherwise. The tool proxy, `permissionMode` and `/btw` are all unavailable on it, so read [What it does not support](#what-it-does-not-support) before enabling. Env: `CLAUDE_CODE_INTERACTIVE_TRANSPORT=1`. |
@@ -778,6 +779,7 @@ Four Claude Code stream events used to reach nothing but a debug log:
778
779
 
779
780
  - **A rate-limit rejection.** When the CLI reports `status: "rejected"` (or a rejected extra-usage state), the turn now carries a `▌ **rate limit:**` line naming the window, the reason extra usage is unavailable, when it resets, and the four things that can be done about it. Warned once per identity per process. See [Billing](#billing-change-june-15-2026-agent-sdk-credit).
780
781
  - **A context compaction Claude Code did on its own.** A `▌ **context compacted:**` note says so, with the before and after token counts, so an answer that suddenly forgets the start of the conversation has a visible cause.
782
+ - **A conversation Claude Code cleared.** Sending `/clear` as a message, or a plan-mode exit that clears context, makes Claude Code start a fresh conversation while opencode still shows the old messages. A `▌ **claude code reset:**` note says so. The plugin deliberately does not replay the earlier messages, since that would undo the clear. Start a new opencode session if you want the two to match.
781
783
  - **A `result` whose subtype is not `success`** (`error_max_turns`, `error_during_execution`, …). The subtype is named in the transcript and the turn finishes as an error instead of an ordinary reply.
782
784
  - **A CLI-executed tool that failed.** Its result is forwarded with the AI SDK's error flag, so opencode renders the row as failed rather than as a success whose output happens to be an error message.
783
785
 
@@ -799,7 +801,7 @@ No separate skill installation or copying is needed. It ships with each package
799
801
 
800
802
  ## Skill bridge
801
803
 
802
- opencode and Claude Code use the same on-disk skill format, a `<name>/SKILL.md` whose frontmatter carries `name` and `description`, but they read from different directories. opencode looks in `.opencode/skills/` and `~/.config/opencode/skills/`; the Claude CLI looks in `~/.claude/skills/` and its own plugins. So opencode advertises your skills in the system prompt it forwards, the model calls `Skill("browser-automation")`, and Claude answers `Unknown skill`.
804
+ opencode and Claude Code use the same on-disk skill format, a `<name>/SKILL.md` whose frontmatter carries `name` and `description`, but they read from overlapping, not identical, directories. opencode looks in `.opencode/skills/`, `~/.config/opencode/skills/`, `~/.agents/skills/` and more; the Claude CLI looks in `~/.claude/skills/`, the project's `.claude/skills/` and its own plugins. Where they differ, opencode advertises a skill in the system prompt it forwards, the model calls `Skill("browser-automation")`, and Claude answers `Unknown skill`. Where they overlap, the same skill reaches one session twice.
803
805
 
804
806
  By default the plugin discovers your opencode skills, stages a throwaway Claude Code plugin directory that links them, and passes it as `claude --plugin-dir`. They register natively, prefixed with the plugin name:
805
807
 
@@ -810,9 +812,33 @@ opencode-skills:rtk
810
812
 
811
813
  Claude can invoke them with the Skill tool or as `/opencode-skills:<name>`. `--plugin-dir` is scoped to the spawned session, so nothing is written into your `~/.claude`.
812
814
 
813
- Discovery order, first match wins: `.opencode/skills/` walking up from the working directory, then `~/.opencode/skills/`, then `$OPENCODE_CONFIG_DIR/skills/`, then `~/.config/opencode/skills/`. A project skill shadows a global one of the same name. If the skill set is unchanged the staged directory is reused between spawns.
815
+ Discovery covers every root opencode itself reads, first match wins:
814
816
 
815
- The bridge is **off by default**: every bridged skill's name and description is also in the system prompt opencode already forwards, so a large skill set is paid for twice on every turn. Set `bridgeOpencodeSkills: true` when the model tries `Skill("<name>")` for a skill opencode advertises and gets `Unknown skill`; the bundled configuration skill is staged either way. When on, the bridge applies to the headless, interactive and direct `doGenerate` spawns alike, never to compaction, and it is skipped on a Claude CLI without `--plugin-dir` (the plugin probes `claude --help` and logs a notice).
817
+ 1. Walking up from the working directory: `.opencode/skills/`, `.claude/skills/`, `.agents/skills/` at each level.
818
+ 2. `~/.opencode/skills/`.
819
+ 3. `$OPENCODE_CONFIG_DIR/skills/` and `.../skill/`.
820
+ 4. `~/.config/opencode/skills/` and `.../skill/` (or `$XDG_CONFIG_HOME`).
821
+ 5. `~/.claude/skills/` and `~/.agents/skills/`.
822
+
823
+ A project skill shadows a global one of the same name, and an opencode-managed copy shadows an external one. Step 5 is opencode's own "external" scan and honours its `OPENCODE_DISABLE_EXTERNAL_SKILLS` and `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` variables. A skill is known by the `name:` its `SKILL.md` frontmatter declares, falling back to the directory name, which is what opencode advertises. If the skill set is unchanged the staged directory is reused between spawns.
824
+
825
+ ### Skills Claude already has
826
+
827
+ Those roots overlap Claude Code's own, which reads `$CLAUDE_CONFIG_DIR/skills/` (`~/.claude/skills/` by default), the project's `.claude/skills/`, and the `skills/` folder of every installed plugin. Without care one skill reaches a single session twice, costing prompt tokens on every turn and leaving it ambiguous which copy answers.
828
+
829
+ So `bridgeSkipNativeSkills` (**on by default**) leaves a skill unbridged when Claude already loads it. A skill counts as already loaded when:
830
+
831
+ - it is literally the same directory, symlinks resolved;
832
+ - its `SKILL.md` is byte-identical to a native one, wherever that one lives (this is the case for a skill installed as a Claude plugin *and* symlinked into `~/.agents/skills`);
833
+ - a **different** skill of the same name is registered under user or project scope. Plugin skills are namespaced `<plugin>:<name>` and so never take a bridged name, only duplicate its content.
834
+
835
+ Only that last case changes which copy answers `Skill("<name>")`, so it is logged at WARN naming both paths; the others are logged at INFO. Set `bridgeSkipNativeSkills: false` to bridge everything regardless and get the duplicates back.
836
+
837
+ One limitation worth knowing: the plugin scan reads `installed_plugins.json` and does not check whether that plugin is actually enabled, so a skill from a disabled plugin can be treated as native. If a skill disappears, grep `plugin.log` for `skills claude code already loads`: one line names both paths and the reason.
838
+
839
+ ### Enabling it
840
+
841
+ The bridge itself is **off by default**: every bridged skill's name and description is also in the system prompt opencode already forwards, so a large skill set is paid for twice on every turn. Set `bridgeOpencodeSkills: true` when the model tries `Skill("<name>")` for a skill opencode advertises and gets `Unknown skill`; the bundled configuration skill is staged either way. When on, the bridge applies to the headless, interactive and direct `doGenerate` spawns alike, never to compaction, and it is skipped on a Claude CLI without `--plugin-dir` (the plugin probes `claude --help` and logs a notice).
816
842
 
817
843
  This bridge was written by [@broskees](https://github.com/broskees) (Joseph Roberts) on his fork and absorbed here with credit; see [Credits](#credits).
818
844
 
package/dist/index.d.ts CHANGED
@@ -407,6 +407,8 @@ interface ClaudeCodeConfig {
407
407
  idleProcessTimeoutMs?: number;
408
408
  /** Stage opencode skills as a `--plugin-dir` so Claude's Skill tool can run them. */
409
409
  bridgeOpencodeSkills?: boolean;
410
+ /** Leave out the skills the Claude session already loads natively. Default true. */
411
+ bridgeSkipNativeSkills?: boolean;
410
412
  /** Append a one-line cost / duration / cache footer to each finished turn. */
411
413
  turnStats?: boolean;
412
414
  logging?: LoggingConfig;
@@ -662,17 +664,41 @@ interface ClaudeCodeProviderSettings {
662
664
  */
663
665
  idleProcessTimeoutMs?: number;
664
666
  /**
665
- * Expose your opencode skills (`.opencode/skills`, `~/.config/opencode/skills`)
666
- * to Claude Code's native Skill tool by staging them as a session-scoped
667
- * `--plugin-dir`, so a `Skill("<name>")` call for a skill opencode advertises
668
- * does not fail with `Unknown skill`. Off by default: every bridged skill
669
- * is also listed in the system prompt opencode forwards, so a large skill
670
- * set costs prompt tokens twice per turn. When on it applies to the
671
- * headless, interactive and direct `doGenerate` spawns alike; compaction
672
- * never loads it, and the bundled configuration skill is staged either way.
673
- * No-op on CLIs without `--plugin-dir`.
667
+ * Expose your opencode skills to Claude Code's native Skill tool by staging
668
+ * them as a session-scoped `--plugin-dir`, so a `Skill("<name>")` call for a
669
+ * skill opencode advertises does not fail with `Unknown skill`. Covers every
670
+ * root opencode itself reads: project `.opencode/`, `.claude/` and `.agents/`
671
+ * walking up from the workspace, the opencode config dirs, and the global
672
+ * `~/.claude/skills` and `~/.agents/skills` (those last two behind the same
673
+ * `OPENCODE_DISABLE_EXTERNAL_SKILLS` / `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS`
674
+ * switches opencode honours).
675
+ *
676
+ * Off by default: every bridged skill is also listed in the system prompt
677
+ * opencode forwards, so a large skill set costs prompt tokens twice per turn.
678
+ * When on it applies to the headless, interactive and direct `doGenerate`
679
+ * spawns alike; compaction never loads it, and the bundled configuration
680
+ * skill is staged either way. No-op on CLIs without `--plugin-dir`.
674
681
  */
675
682
  bridgeOpencodeSkills?: boolean;
683
+ /**
684
+ * Leave a skill unbridged when the Claude Code session already loads it:
685
+ * from `<CLAUDE_CONFIG_DIR>/skills` (`~/.claude/skills` by default), from the
686
+ * project's own `.claude/skills`, or from an installed plugin's `skills/`.
687
+ * On by default, because those roots overlap opencode's and the duplicate
688
+ * costs prompt tokens on every turn for nothing.
689
+ *
690
+ * A skill is treated as already loaded when it is literally the same
691
+ * directory (symlinks resolved), when its SKILL.md is byte-identical to a
692
+ * native one, or when a *different* skill of the same name is registered
693
+ * under user or project scope. That last case is the only one that changes
694
+ * behaviour, because `Skill("<name>")` then answers from Claude's copy
695
+ * rather than opencode's, so it is logged at WARN naming both paths. Plugin
696
+ * skills are namespaced `<plugin>:<name>` and so only ever match by content.
697
+ *
698
+ * Set `false` to bridge everything regardless, which restores the pre-0.25
699
+ * behaviour of advertising a shared skill twice.
700
+ */
701
+ bridgeSkipNativeSkills?: boolean;
676
702
  /**
677
703
  * Append one compact line to the end of every finished (non-compaction,
678
704
  * non-error) turn with what that turn cost: dollars, wall duration, how many
@@ -810,6 +836,8 @@ interface ClaudeStreamMessage {
810
836
  * `accountBlockKind`; schema confirmed on Claude Code 2.1.280.
811
837
  */
812
838
  error?: string;
839
+ /** On a `conversation_reset`: the conversation Claude Code started. */
840
+ new_conversation_id?: string;
813
841
  message?: {
814
842
  role?: string;
815
843
  model?: string;
@@ -993,6 +1021,13 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
993
1021
  * spawn block resolves anything: `userMsg` is built well ahead of it.
994
1022
  */
995
1023
  private stripContextRemindersEnabled;
1024
+ /**
1025
+ * Arguments the skill bridge needs beyond `cwd` / `cliPath`: which
1026
+ * `CLAUDE_CONFIG_DIR` this spawn reads its native skills from, and whether
1027
+ * to drop the ones it already loads. A failover moves the spawn to another
1028
+ * account, and therefore to that account's config dir.
1029
+ */
1030
+ private skillBridgeSpawn;
996
1031
  /** Share one lazy registry request within a turn without making it stale. */
997
1032
  private createLiveToolInfoLoader;
998
1033
  /**
package/dist/index.js CHANGED
@@ -553,6 +553,10 @@ function accountConfigDir(account) {
553
553
  if (!normalized || normalized === DEFAULT_ACCOUNT) return void 0;
554
554
  return `~/.claude-${normalized}`;
555
555
  }
556
+ function accountConfigDirPath(account) {
557
+ const dir = accountConfigDir(account);
558
+ return dir ? expandHome(dir) : void 0;
559
+ }
556
560
  function expandHome(value) {
557
561
  const home = process.env.HOME ?? process.env.USERPROFILE;
558
562
  if (value === "~") return home ?? value;
@@ -878,6 +882,27 @@ function reportCompactBoundary(msg) {
878
882
  });
879
883
  return formatCompactBoundaryNote(boundary);
880
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
+ }
881
906
  var RESULT_ERROR_MARKER = "\u258C **claude code error:**";
882
907
  var RESULT_SUBTYPES = {
883
908
  error_max_turns: "it hit its internal turn limit before finishing",
@@ -4721,6 +4746,7 @@ var PLUGIN_NOTE_MARKERS = [
4721
4746
  ...LEGACY_INLINE_ASIDE_MARKERS,
4722
4747
  TURN_STATS_MARKER,
4723
4748
  COMPACT_BOUNDARY_MARKER,
4749
+ CONVERSATION_RESET_MARKER,
4724
4750
  RATE_LIMIT_MARKER,
4725
4751
  RESULT_ERROR_MARKER,
4726
4752
  DOCTOR_MARKER,
@@ -5572,13 +5598,48 @@ function dirExists2(p) {
5572
5598
  return false;
5573
5599
  }
5574
5600
  }
5575
- function fileExists2(p) {
5601
+ function realPath(p) {
5576
5602
  try {
5577
- return fs5.statSync(p).isFile();
5603
+ return fs5.realpathSync(p);
5578
5604
  } catch {
5579
- return false;
5605
+ return path7.resolve(p);
5580
5606
  }
5581
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
+ }
5582
5643
  function skillRoots(cwd) {
5583
5644
  const roots = [];
5584
5645
  const seen = /* @__PURE__ */ new Set();
@@ -5588,19 +5649,36 @@ function skillRoots(cwd) {
5588
5649
  seen.add(abs);
5589
5650
  if (dirExists2(abs)) roots.push(abs);
5590
5651
  };
5652
+ const home = os3.homedir();
5653
+ const homeDir = home ? path7.resolve(home) : null;
5591
5654
  let current = path7.resolve(cwd);
5592
5655
  while (true) {
5593
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
+ }
5594
5661
  const parent = path7.dirname(current);
5595
5662
  if (parent === current) break;
5596
5663
  current = parent;
5597
5664
  }
5598
- const home = os3.homedir();
5599
- if (home) push(path7.join(home, ".opencode", "skills"));
5665
+ if (homeDir) push(path7.join(homeDir, ".opencode", "skills"));
5600
5666
  const envDir = process.env.OPENCODE_CONFIG_DIR;
5601
- if (envDir) push(path7.join(envDir, "skills"));
5602
- const xdg = process.env.XDG_CONFIG_HOME ?? (home ? path7.join(home, ".config") : null);
5603
- 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
+ }
5604
5682
  return roots;
5605
5683
  }
5606
5684
  function collectSkills(root, claimed, found) {
@@ -5612,19 +5690,28 @@ function collectSkills(root, claimed, found) {
5612
5690
  }
5613
5691
  for (const entry of entries) {
5614
5692
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
5615
- const name = entry.name;
5616
- if (name.startsWith(".")) continue;
5617
- if (claimed.has(name)) continue;
5618
- const dir = path7.join(root, name);
5619
- if (!fileExists2(path7.join(dir, "SKILL.md"))) continue;
5620
- claimed.add(name);
5621
- 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);
5622
5709
  }
5623
5710
  }
5624
5711
  var byName = (a, b) => a.name.localeCompare(b.name);
5625
5712
  function discoverOpencodeSkills(cwd) {
5626
5713
  const found = [];
5627
- const claimed = /* @__PURE__ */ new Set();
5714
+ const claimed = /* @__PURE__ */ new Map();
5628
5715
  for (const root of skillRoots(cwd)) collectSkills(root, claimed, found);
5629
5716
  return found.sort(byName);
5630
5717
  }
@@ -5632,17 +5719,105 @@ function discoverBundledSkills() {
5632
5719
  const root = bundledSkillsDir();
5633
5720
  if (!root) return [];
5634
5721
  const found = [];
5635
- collectSkills(root, /* @__PURE__ */ new Set(), found);
5722
+ collectSkills(root, /* @__PURE__ */ new Map(), found);
5636
5723
  return found.sort(byName);
5637
5724
  }
5638
- function registerBundledSkillPath(config) {
5639
- const dir = bundledSkillsDir();
5640
- if (!dir) return false;
5641
- config.skills ??= {};
5642
- const paths = config.skills.paths ??= [];
5643
- if (paths.some((p) => path7.resolve(p) === dir)) return false;
5644
- paths.push(dir);
5645
- 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 };
5646
5821
  }
5647
5822
  function readBundledSkillInfos() {
5648
5823
  return discoverBundledSkills().flatMap((skill) => {
@@ -5679,6 +5854,13 @@ function linkSkill(source, target) {
5679
5854
  fs5.cpSync(source, target, { recursive: true, dereference: true });
5680
5855
  }
5681
5856
  }
5857
+ function fileExists2(p) {
5858
+ try {
5859
+ return fs5.statSync(p).isFile();
5860
+ } catch {
5861
+ return false;
5862
+ }
5863
+ }
5682
5864
  function buildSkillPluginDir(skills) {
5683
5865
  if (skills.length === 0) return null;
5684
5866
  const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
@@ -5714,11 +5896,36 @@ function buildSkillPluginDir(skills) {
5714
5896
  }
5715
5897
  return root;
5716
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
+ }
5717
5908
  async function resolveSkillPluginDirs(opts) {
5718
5909
  const user = opts.enabled ? discoverOpencodeSkills(opts.cwd) : [];
5719
5910
  const claimed = new Set(user.map((s) => s.name));
5720
5911
  const bundled = discoverBundledSkills().filter((s) => !claimed.has(s.name));
5721
- 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
+ }
5722
5929
  if (skills.length === 0) return [];
5723
5930
  const supported = await detectCliSupportsFlag(opts.cliPath, "--plugin-dir");
5724
5931
  if (!supported) {
@@ -6960,6 +7167,18 @@ var ClaudeCodeLanguageModel = class {
6960
7167
  proxyOpencodeTools: this.config.proxyOpencodeTools
6961
7168
  });
6962
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
+ }
6963
7182
  /** Share one lazy registry request within a turn without making it stale. */
6964
7183
  createLiveToolInfoLoader() {
6965
7184
  let pending;
@@ -7478,7 +7697,8 @@ var ClaudeCodeLanguageModel = class {
7478
7697
  const skillPluginDirs = await resolveSkillPluginDirs({
7479
7698
  cwd,
7480
7699
  cliPath,
7481
- enabled: this.config.bridgeOpencodeSkills === true
7700
+ enabled: this.config.bridgeOpencodeSkills === true,
7701
+ ...this.skillBridgeSpawn(failover)
7482
7702
  });
7483
7703
  const cliArgs = buildCliArgs({
7484
7704
  sessionKey: sk,
@@ -8208,7 +8428,8 @@ ${plan}
8208
8428
  const skillPluginDirs = await resolveSkillPluginDirs({
8209
8429
  cwd,
8210
8430
  cliPath,
8211
- enabled: self.config.bridgeOpencodeSkills === true
8431
+ enabled: self.config.bridgeOpencodeSkills === true,
8432
+ ...self.skillBridgeSpawn(failover)
8212
8433
  });
8213
8434
  const ap = spawnInteractiveProcess({
8214
8435
  cwd,
@@ -8364,7 +8585,8 @@ ${plan}
8364
8585
  const skillPluginDirs = await resolveSkillPluginDirs({
8365
8586
  cwd,
8366
8587
  cliPath,
8367
- enabled: self.config.bridgeOpencodeSkills === true
8588
+ enabled: self.config.bridgeOpencodeSkills === true,
8589
+ ...self.skillBridgeSpawn(failover)
8368
8590
  });
8369
8591
  cliArgs = buildCliArgs({
8370
8592
  sessionKey: sk,
@@ -8938,6 +9160,18 @@ ${plan}
8938
9160
  endTextBlock();
8939
9161
  }
8940
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
+ }
8941
9175
  const block = accountBlockKind(msg);
8942
9176
  if (block) accountBlock = block;
8943
9177
  if (msg.type === "rate_limit_event") {
@@ -9930,7 +10164,7 @@ ${plan}
9930
10164
  import {
9931
10165
  existsSync as existsSync4,
9932
10166
  readFileSync as readFileSync6,
9933
- realpathSync,
10167
+ realpathSync as realpathSync2,
9934
10168
  rmSync as rmSync3,
9935
10169
  writeFileSync as writeFileSync5
9936
10170
  } from "fs";
@@ -9969,7 +10203,7 @@ function userIntendsToUseUnscoped() {
9969
10203
  function ourLoadedDir() {
9970
10204
  try {
9971
10205
  const filePath = fileURLToPath3(import.meta.url);
9972
- return realpathSync(resolve4(filePath, "..", ".."));
10206
+ return realpathSync2(resolve4(filePath, "..", ".."));
9973
10207
  } catch {
9974
10208
  return null;
9975
10209
  }
@@ -9997,7 +10231,7 @@ function cleanupOne(cacheRoot, ourDir) {
9997
10231
  if (!existsSync4(stalePath)) return;
9998
10232
  let realStalePath = stalePath;
9999
10233
  try {
10000
- realStalePath = realpathSync(stalePath);
10234
+ realStalePath = realpathSync2(stalePath);
10001
10235
  } catch {
10002
10236
  }
10003
10237
  if (ourDir && realStalePath === ourDir) return;
@@ -10556,6 +10790,7 @@ function createClaudeCode(settings = {}) {
10556
10790
  ignoreAnthropicApiKey: settings.ignoreAnthropicApiKey,
10557
10791
  idleProcessTimeoutMs: settings.idleProcessTimeoutMs,
10558
10792
  bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
10793
+ bridgeSkipNativeSkills: settings.bridgeSkipNativeSkills !== false,
10559
10794
  turnStats: settings.turnStats === true,
10560
10795
  interactive: settings.interactive,
10561
10796
  interactiveBypass: settings.interactiveBypass,