@khalilgharbaoui/opencode-claude-code-plugin 0.26.3 → 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/README.md +30 -4
- package/dist/index.d.ts +44 -9
- package/dist/index.js +267 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +26 -9
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
|
|
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
|
|
815
|
+
Discovery covers every root opencode itself reads, first match wins:
|
|
814
816
|
|
|
815
|
-
|
|
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
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
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
|
|
5601
|
+
function realPath(p) {
|
|
5576
5602
|
try {
|
|
5577
|
-
return fs5.
|
|
5603
|
+
return fs5.realpathSync(p);
|
|
5578
5604
|
} catch {
|
|
5579
|
-
return
|
|
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
|
-
|
|
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)
|
|
5602
|
-
|
|
5603
|
-
|
|
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
|
-
|
|
5616
|
-
|
|
5617
|
-
if (
|
|
5618
|
-
const
|
|
5619
|
-
if (
|
|
5620
|
-
|
|
5621
|
-
|
|
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
|
|
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
|
|
5722
|
+
collectSkills(root, /* @__PURE__ */ new Map(), found);
|
|
5636
5723
|
return found.sort(byName);
|
|
5637
5724
|
}
|
|
5638
|
-
function
|
|
5639
|
-
const
|
|
5640
|
-
if (
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
return
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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,
|