@koriit/opencode-claude-bridge 0.1.11 → 0.1.15
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/package.json +1 -1
- package/src/index.ts +30 -1
- package/src/inject.ts +46 -5
- package/src/logger.ts +23 -27
- package/src/opencode-core.d.ts +18 -0
- package/src/skill-inject.ts +100 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koriit/opencode-claude-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "An OpenCode plugin that bridges enabled Claude Code plugins (commands, agents, skills, MCP, LSP) into OpenCode at runtime, namespaced so they never shadow your existing items.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -68,6 +68,7 @@ export const server: Plugin = async (_input, options) => {
|
|
|
68
68
|
|
|
69
69
|
// §6.1 commands, §6.2 agents — inline injection into the shared cfg.
|
|
70
70
|
const cmdAgentSummary = await injectCommandsAndAgents(selected, cfg, logger)
|
|
71
|
+
const { commandAllocator } = cmdAgentSummary
|
|
71
72
|
|
|
72
73
|
// §6.3 skills — cfg.skills.paths injection (with bridge-cache copy on collision).
|
|
73
74
|
const home = os.homedir()
|
|
@@ -85,6 +86,7 @@ export const server: Plugin = async (_input, options) => {
|
|
|
85
86
|
home,
|
|
86
87
|
projectDir: _input.directory,
|
|
87
88
|
cacheRoot: process.env["OPENCODE_CLAUDE_BRIDGE_CACHE_ROOT"],
|
|
89
|
+
commandAllocator,
|
|
88
90
|
}, logger)
|
|
89
91
|
|
|
90
92
|
// §6.4 MCP — cfg.mcp injection (opt-in via allowMcp).
|
|
@@ -93,10 +95,32 @@ export const server: Plugin = async (_input, options) => {
|
|
|
93
95
|
// §6.5 LSP — cfg.lsp injection (opt-in via allowLsp; respects cfg.lsp === false).
|
|
94
96
|
const lspSummary = await injectLsp(selected, cfg, bridge.allowLsp, logger)
|
|
95
97
|
|
|
98
|
+
// Remove commands that OpenCode's native Claude integration may have
|
|
99
|
+
// auto-loaded from blocked plugins. Runs after all bridge injection so
|
|
100
|
+
// OpenCode's own loading has had time to run during the async awaits above.
|
|
101
|
+
const selectedIds = new Set(selected.map((p) => p.id))
|
|
102
|
+
const blockedPlugins = all.filter((p) => !selectedIds.has(p.id))
|
|
103
|
+
if (blockedPlugins.length > 0) {
|
|
104
|
+
const mutableCmd = (cfg as unknown as { command?: Record<string, { description?: string }> }).command
|
|
105
|
+
if (mutableCmd && typeof mutableCmd === "object") {
|
|
106
|
+
for (const [name, entry] of Object.entries(mutableCmd)) {
|
|
107
|
+
const desc = entry?.description
|
|
108
|
+
if (typeof desc === "string") {
|
|
109
|
+
for (const bp of blockedPlugins) {
|
|
110
|
+
if (desc.endsWith(`[${bp.id}]`)) {
|
|
111
|
+
delete mutableCmd[name]
|
|
112
|
+
break
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
96
120
|
// §10 concise per-run summary.
|
|
97
121
|
const renamed = cmdAgentSummary.renamed + skillSummary.renamed + mcpSummary.renamed + lspSummary.renamed
|
|
98
122
|
const summaryParts: string[] = [
|
|
99
|
-
`injected ${cmdAgentSummary.commands} command(s), ${cmdAgentSummary.agents} agent(s), ${skillSummary.skills} skill(s), ${mcpSummary.servers} MCP server(s), ${lspSummary.servers} LSP server(s)`,
|
|
123
|
+
`injected ${cmdAgentSummary.commands + skillSummary.commandsAdded} command(s), ${cmdAgentSummary.agents} agent(s), ${skillSummary.skills} skill(s), ${mcpSummary.servers} MCP server(s), ${lspSummary.servers} LSP server(s)`,
|
|
100
124
|
]
|
|
101
125
|
if (renamed > 0) summaryParts.push(`renamed ${renamed} (collision)`)
|
|
102
126
|
if (mcpSummary.skippedPolicy > 0) summaryParts.push(`skipped ${mcpSummary.skippedPolicy} MCP (policy)`)
|
|
@@ -120,6 +144,11 @@ export const server: Plugin = async (_input, options) => {
|
|
|
120
144
|
}
|
|
121
145
|
},
|
|
122
146
|
|
|
147
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
148
|
+
if (input.sessionID)
|
|
149
|
+
output.system.push(`Session ID: ${input.sessionID}`)
|
|
150
|
+
},
|
|
151
|
+
|
|
123
152
|
"chat.message": async () => {
|
|
124
153
|
// Show a one-time toast on the first chat message if any warnings were emitted
|
|
125
154
|
// during the config hook. The toast is best-effort — a failure must never throw.
|
package/src/inject.ts
CHANGED
|
@@ -80,6 +80,11 @@ export interface InjectionSummary {
|
|
|
80
80
|
renamed: number
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** Full result of {@link injectCommandsAndAgents}, including the populated command allocator. */
|
|
84
|
+
export interface InjectionResult extends InjectionSummary {
|
|
85
|
+
commandAllocator: NameAllocator
|
|
86
|
+
}
|
|
87
|
+
|
|
83
88
|
// ── File collection helpers ────────────────────────────────────────────────────
|
|
84
89
|
|
|
85
90
|
/**
|
|
@@ -405,6 +410,41 @@ export function sanitizeAgentColor(value: string): string | null {
|
|
|
405
410
|
return null
|
|
406
411
|
}
|
|
407
412
|
|
|
413
|
+
// ── Single-entry command injection ───────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Insert one command entry into `cfg.command` using the allocator, appending `[pluginId]`
|
|
417
|
+
* to the description. Returns the allocated name and whether it was renamed.
|
|
418
|
+
*
|
|
419
|
+
* The `entry.template` must already be fully resolved by the caller (e.g. via
|
|
420
|
+
* `resolvePluginRoot` for commands from the plugin's commands/ dir, or verbatim for
|
|
421
|
+
* skill-derived commands whose body does not use `${CLAUDE_PLUGIN_ROOT}`).
|
|
422
|
+
*/
|
|
423
|
+
export function injectCommandEntry(
|
|
424
|
+
bareName: string,
|
|
425
|
+
entry: CommandEntry,
|
|
426
|
+
cfg: InjectableConfig,
|
|
427
|
+
allocator: NameAllocator,
|
|
428
|
+
pluginId: string,
|
|
429
|
+
logger: Logger,
|
|
430
|
+
): { allocatedName: string; renamed: boolean } {
|
|
431
|
+
if (cfg.command === undefined || cfg.command === null || typeof cfg.command !== "object") {
|
|
432
|
+
cfg.command = {}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const { name: allocatedName, renamed } = allocator.claim(pluginId, bareName)
|
|
436
|
+
|
|
437
|
+
const rawDescription = entry.description
|
|
438
|
+
const tracedDescription = rawDescription
|
|
439
|
+
? `${rawDescription} [${pluginId}]`
|
|
440
|
+
: `${bareName} [${pluginId}]`
|
|
441
|
+
|
|
442
|
+
warnUnsupportedPlaceholders(entry.template, allocatedName, pluginId, logger)
|
|
443
|
+
|
|
444
|
+
cfg.command[allocatedName] = { ...entry, description: tracedDescription }
|
|
445
|
+
return { allocatedName, renamed }
|
|
446
|
+
}
|
|
447
|
+
|
|
408
448
|
// ── Command injection for one plugin ─────────────────────────────────────────
|
|
409
449
|
|
|
410
450
|
/**
|
|
@@ -666,11 +706,8 @@ export async function injectCommandsAndAgents(
|
|
|
666
706
|
plugins: ClaudePlugin[],
|
|
667
707
|
cfg: Config,
|
|
668
708
|
logger: Logger,
|
|
669
|
-
): Promise<
|
|
709
|
+
): Promise<InjectionResult> {
|
|
670
710
|
const mutableCfg = cfg as unknown as InjectableConfig
|
|
671
|
-
const summary: InjectionSummary = { commands: 0, agents: 0, renamed: 0 }
|
|
672
|
-
|
|
673
|
-
if (plugins.length === 0) return summary
|
|
674
711
|
|
|
675
712
|
// Seed the command allocator with existing cfg keys ∪ built-in command names.
|
|
676
713
|
const existingCommands = new Set<string>([
|
|
@@ -679,6 +716,10 @@ export async function injectCommandsAndAgents(
|
|
|
679
716
|
])
|
|
680
717
|
const commandAlloc = new NameAllocator(existingCommands)
|
|
681
718
|
|
|
719
|
+
const summary: InjectionSummary = { commands: 0, agents: 0, renamed: 0 }
|
|
720
|
+
|
|
721
|
+
if (plugins.length === 0) return { ...summary, commandAllocator: commandAlloc }
|
|
722
|
+
|
|
682
723
|
// Seed the agent allocator with existing cfg keys ∪ built-in agent names.
|
|
683
724
|
const existingAgents = new Set<string>([
|
|
684
725
|
...BUILTIN_AGENT_NAMES,
|
|
@@ -691,5 +732,5 @@ export async function injectCommandsAndAgents(
|
|
|
691
732
|
await injectPluginAgents(plugin, mutableCfg, agentAlloc, summary, logger)
|
|
692
733
|
}
|
|
693
734
|
|
|
694
|
-
return summary
|
|
735
|
+
return { ...summary, commandAllocator: commandAlloc }
|
|
695
736
|
}
|
package/src/logger.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
|
|
1
3
|
/** Thrown when a soft warning is promoted to a hard error under `strict` mode. */
|
|
2
4
|
export class BridgeError extends Error {
|
|
3
5
|
override name = "BridgeError"
|
|
@@ -25,48 +27,42 @@ export interface Logger {
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
30
|
+
* Detect whether --print-logs was passed to the parent OpenCode process.
|
|
31
|
+
*
|
|
32
|
+
* process.argv is stripped in Bun Workers (only ["bun", "<worker_script>"] is
|
|
33
|
+
* present), so we cannot check it directly. However, Workers run in the same
|
|
34
|
+
* OS process as the host, so /proc/self/cmdline contains the real command line.
|
|
35
|
+
* Falls back to false on non-Linux platforms or if the file is unreadable.
|
|
31
36
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
function detectPrintLogs(): boolean {
|
|
38
|
+
try {
|
|
39
|
+
const args = readFileSync("/proc/self/cmdline").toString().split("\0")
|
|
40
|
+
return args.includes("--print-logs")
|
|
41
|
+
} catch {
|
|
42
|
+
return false
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
const printLogs = detectPrintLogs()
|
|
47
|
+
|
|
39
48
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* configured instance. A dynamic import of the same specifier returns it.
|
|
45
|
-
*
|
|
46
|
-
* Falls back to a plain stderr writer if the import fails (tests, non-OpenCode
|
|
47
|
-
* environments) — in that case output is always emitted so tests can capture it.
|
|
49
|
+
* Try to import @opencode-ai/core/util/log from the host Bun Worker's module
|
|
50
|
+
* registry. If available, its already-initialized logger routes output to
|
|
51
|
+
* stderr (with --print-logs) or the log file (default) without any extra
|
|
52
|
+
* argv inspection. Falls back to null when the module is unavailable.
|
|
48
53
|
*/
|
|
49
|
-
async function resolveCoreLog()
|
|
54
|
+
async function resolveCoreLog() {
|
|
50
55
|
try {
|
|
51
|
-
|
|
52
|
-
// The dynamic import resolves against Bun's module registry at runtime —
|
|
53
|
-
// the host worker already loaded and initialized it.
|
|
54
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
55
|
-
// @ts-ignore — not in node_modules; resolved from the Bun bundle at runtime
|
|
56
|
-
return await import("@opencode-ai/core/util/log") as CoreLog
|
|
56
|
+
return await import("@opencode-ai/core/util/log")
|
|
57
57
|
} catch {
|
|
58
58
|
return null
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
// Kick off resolution immediately so it's ready before the first log call.
|
|
63
62
|
const coreLogPromise = resolveCoreLog()
|
|
64
63
|
|
|
65
|
-
/**
|
|
66
|
-
* Fallback writer used when the core log module is unavailable.
|
|
67
|
-
* Always writes to stderr — correct for test environments.
|
|
68
|
-
*/
|
|
69
64
|
function fallbackWrite(level: "INFO" | "WARN", msg: string): void {
|
|
65
|
+
if (!printLogs) return
|
|
70
66
|
const ts = new Date().toISOString().split(".")[0]
|
|
71
67
|
process.stderr.write(`${level.padEnd(5)} ${ts} service=opencode-claude-bridge ${msg}\n`)
|
|
72
68
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for @opencode-ai/core — a private package bundled into the
|
|
3
|
+
* OpenCode binary and not published to npm. Only the subset the bridge actually
|
|
4
|
+
* uses is declared here; the full API is larger.
|
|
5
|
+
*
|
|
6
|
+
* Resolved at runtime via dynamic import against Bun's module registry, which
|
|
7
|
+
* already holds the host worker's pre-initialized instance.
|
|
8
|
+
*/
|
|
9
|
+
declare module "@opencode-ai/core/util/log" {
|
|
10
|
+
export interface Logger {
|
|
11
|
+
debug(message?: unknown, extra?: Record<string, unknown>): void
|
|
12
|
+
info(message?: unknown, extra?: Record<string, unknown>): void
|
|
13
|
+
warn(message?: unknown, extra?: Record<string, unknown>): void
|
|
14
|
+
error(message?: unknown, extra?: Record<string, unknown>): void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function create(tags?: Record<string, unknown>): Logger
|
|
18
|
+
}
|
package/src/skill-inject.ts
CHANGED
|
@@ -29,6 +29,8 @@ import path from "node:path"
|
|
|
29
29
|
import type { Config } from "@opencode-ai/plugin"
|
|
30
30
|
import { extractSkillName } from "./skill-scan.js"
|
|
31
31
|
import { NameAllocator, splitPluginId } from "./naming.js"
|
|
32
|
+
import { parseFrontmatter, FRONTMATTER_PARSE_ERROR } from "./frontmatter.js"
|
|
33
|
+
import { injectCommandEntry } from "./inject.js"
|
|
32
34
|
import type { Logger } from "./logger.js"
|
|
33
35
|
import type { ClaudePlugin } from "./types.js"
|
|
34
36
|
|
|
@@ -55,6 +57,7 @@ interface NormalizedSkillsConfig {
|
|
|
55
57
|
|
|
56
58
|
interface InjectableConfig {
|
|
57
59
|
skills?: SkillsConfig | boolean | undefined
|
|
60
|
+
command?: Record<string, unknown> | undefined
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
/**
|
|
@@ -317,14 +320,20 @@ export function patchSkillName(content: string, newName: string): string {
|
|
|
317
320
|
|
|
318
321
|
/**
|
|
319
322
|
* Scan `<installPath>/skills/` for skill subdirectories and inject each one into
|
|
320
|
-
* `cfg.skills.paths
|
|
323
|
+
* `cfg.skills.paths` and/or `cfg.command` based on the skill's frontmatter flags.
|
|
321
324
|
*
|
|
322
|
-
*
|
|
325
|
+
* Routing table (from `user-invocable` and `disable-model-invocation` frontmatter):
|
|
326
|
+
* - neither flag → inject as skill AND command
|
|
327
|
+
* - user-invocable: false → inject as skill only (no command)
|
|
328
|
+
* - disable-model-invocation: true → inject as command only (no skill)
|
|
329
|
+
* - both flags set → skip entirely with a WARN
|
|
323
330
|
*/
|
|
324
331
|
async function injectPluginSkills(
|
|
325
332
|
plugin: ClaudePlugin,
|
|
326
333
|
skillsCfg: NormalizedSkillsConfig,
|
|
327
|
-
|
|
334
|
+
mutableCfg: InjectableConfig,
|
|
335
|
+
skillAllocator: NameAllocator,
|
|
336
|
+
commandAllocator: NameAllocator,
|
|
328
337
|
cacheRoot: string,
|
|
329
338
|
summary: SkillInjectionSummary,
|
|
330
339
|
logger: Logger,
|
|
@@ -363,36 +372,81 @@ async function injectPluginSkills(
|
|
|
363
372
|
)
|
|
364
373
|
}
|
|
365
374
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
375
|
+
// Parse additional frontmatter flags that control injection routing.
|
|
376
|
+
const parsed = parseFrontmatter(content)
|
|
377
|
+
let userInvocable = true
|
|
378
|
+
let disableModelInvocation = false
|
|
379
|
+
let description: string | undefined
|
|
380
|
+
let body = ""
|
|
381
|
+
|
|
382
|
+
if (parsed !== null && parsed !== FRONTMATTER_PARSE_ERROR) {
|
|
383
|
+
const fm = parsed.data
|
|
384
|
+
if (fm["user-invocable"] === false) userInvocable = false
|
|
385
|
+
if (fm["disable-model-invocation"] === true) disableModelInvocation = true
|
|
386
|
+
if (typeof fm["description"] === "string") description = fm["description"]
|
|
387
|
+
body = parsed.body
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const asSkill = !disableModelInvocation
|
|
391
|
+
const asCommand = userInvocable
|
|
392
|
+
|
|
393
|
+
if (!asSkill && !asCommand) {
|
|
394
|
+
logger.warn(
|
|
395
|
+
`skill "${bareName}" from plugin "${plugin.id}" has both "disable-model-invocation: true" and "user-invocable: false"; skipping`,
|
|
396
|
+
{ fatalInStrict: false },
|
|
397
|
+
)
|
|
398
|
+
continue
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (asSkill) {
|
|
402
|
+
const { name: allocatedName, renamed } = skillAllocator.claim(plugin.id, bareName)
|
|
403
|
+
|
|
404
|
+
if (!renamed) {
|
|
405
|
+
// No collision — point OpenCode directly at the plugin's skill dir.
|
|
406
|
+
skillsCfg.paths.push(skillDir)
|
|
407
|
+
summary.skills++
|
|
408
|
+
} else {
|
|
409
|
+
// Collision — copy the skill dir into the bridge cache and patch the name.
|
|
410
|
+
const cachedSkillDir = cacheDirForSkill(cacheRoot, plugin, allocatedName)
|
|
411
|
+
const cachedSkillMd = path.join(cachedSkillDir, "SKILL.md")
|
|
412
|
+
|
|
413
|
+
const stale = await isCacheStale(skillMdPath, cachedSkillMd)
|
|
414
|
+
if (stale) {
|
|
415
|
+
try {
|
|
416
|
+
await copyDirRecursive(skillDir, cachedSkillDir, logger)
|
|
417
|
+
// Patch using the content already in memory (read above for extractSkillName)
|
|
418
|
+
// rather than re-reading the just-copied file — same bytes, avoids a round-trip.
|
|
419
|
+
const patched = patchSkillName(content, allocatedName)
|
|
420
|
+
await fs.writeFile(cachedSkillMd, patched, "utf8")
|
|
421
|
+
} catch (err) {
|
|
422
|
+
logger.warn(
|
|
423
|
+
`failed to create bridge-cache copy for skill "${bareName}" from plugin "${plugin.id}" (${err instanceof Error ? err.message : String(err)}); skipping`,
|
|
424
|
+
)
|
|
425
|
+
continue
|
|
426
|
+
}
|
|
390
427
|
}
|
|
428
|
+
|
|
429
|
+
skillsCfg.paths.push(cachedSkillDir)
|
|
430
|
+
summary.skills++
|
|
431
|
+
summary.renamed++
|
|
391
432
|
}
|
|
433
|
+
}
|
|
392
434
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
435
|
+
if (asCommand) {
|
|
436
|
+
if (!body) continue
|
|
437
|
+
const fmData = (parsed !== null && parsed !== FRONTMATTER_PARSE_ERROR) ? parsed.data : {}
|
|
438
|
+
const modelRaw = fmData["model"]
|
|
439
|
+
const model = typeof modelRaw === "string" && modelRaw.includes("/") ? modelRaw : undefined
|
|
440
|
+
const { renamed } = injectCommandEntry(
|
|
441
|
+
bareName,
|
|
442
|
+
{ template: body, description, model },
|
|
443
|
+
mutableCfg as unknown as { command?: Record<string, import("./inject.js").CommandEntry> },
|
|
444
|
+
commandAllocator,
|
|
445
|
+
plugin.id,
|
|
446
|
+
logger,
|
|
447
|
+
)
|
|
448
|
+
summary.commandsAdded++
|
|
449
|
+
if (renamed) summary.renamed++
|
|
396
450
|
}
|
|
397
451
|
}
|
|
398
452
|
}
|
|
@@ -403,6 +457,7 @@ async function injectPluginSkills(
|
|
|
403
457
|
export interface SkillInjectionSummary {
|
|
404
458
|
skills: number
|
|
405
459
|
renamed: number
|
|
460
|
+
commandsAdded: number
|
|
406
461
|
}
|
|
407
462
|
|
|
408
463
|
/**
|
|
@@ -424,6 +479,13 @@ export interface SkillInjectOptions {
|
|
|
424
479
|
* to `~/.cache/opencode-claude-bridge/skills/`.
|
|
425
480
|
*/
|
|
426
481
|
cacheRoot?: string
|
|
482
|
+
/**
|
|
483
|
+
* The fully-populated command name allocator from `injectCommandsAndAgents`.
|
|
484
|
+
* Required to participate in the same command namespace so skill-derived
|
|
485
|
+
* commands never collide with plugin commands or built-in commands.
|
|
486
|
+
* When omitted (e.g. in legacy callers), a fresh allocator is created.
|
|
487
|
+
*/
|
|
488
|
+
commandAllocator?: NameAllocator
|
|
427
489
|
}
|
|
428
490
|
|
|
429
491
|
/**
|
|
@@ -451,7 +513,7 @@ export async function injectSkills(
|
|
|
451
513
|
logger: Logger,
|
|
452
514
|
): Promise<SkillInjectionSummary> {
|
|
453
515
|
const mutableCfg = cfg as unknown as InjectableConfig
|
|
454
|
-
const summary: SkillInjectionSummary = { skills: 0, renamed: 0 }
|
|
516
|
+
const summary: SkillInjectionSummary = { skills: 0, renamed: 0, commandsAdded: 0 }
|
|
455
517
|
|
|
456
518
|
if (plugins.length === 0) return summary
|
|
457
519
|
|
|
@@ -469,13 +531,17 @@ export async function injectSkills(
|
|
|
469
531
|
const cacheRoot =
|
|
470
532
|
opts.cacheRoot ?? path.join(opts.home, ".cache", "opencode-claude-bridge", "skills")
|
|
471
533
|
|
|
472
|
-
// Seed the allocator with every name OpenCode already knows about (native + built-ins).
|
|
534
|
+
// Seed the skill allocator with every name OpenCode already knows about (native + built-ins).
|
|
473
535
|
// `existingSkillNames` is provided by the caller (from `collectExistingSkillNames`) to
|
|
474
536
|
// avoid calling the Skill service from the hook.
|
|
475
|
-
const
|
|
537
|
+
const skillAllocator = new NameAllocator(existingSkillNames)
|
|
538
|
+
|
|
539
|
+
// Use the caller-provided command allocator so skill-derived commands share the
|
|
540
|
+
// same namespace as plugin commands. Fall back to a fresh one if not provided.
|
|
541
|
+
const commandAllocator = opts.commandAllocator ?? new NameAllocator(new Set<string>())
|
|
476
542
|
|
|
477
543
|
for (const plugin of plugins) {
|
|
478
|
-
await injectPluginSkills(plugin, skillsCfg,
|
|
544
|
+
await injectPluginSkills(plugin, skillsCfg, mutableCfg, skillAllocator, commandAllocator, cacheRoot, summary, logger)
|
|
479
545
|
}
|
|
480
546
|
|
|
481
547
|
return summary
|