@koriit/opencode-claude-bridge 0.1.2 → 0.1.7
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 -4
- package/src/index.ts +2 -7
- package/src/logger.ts +64 -17
- package/src/opencode-builtins.ts +1 -15
- package/src/skill-inject.ts +7 -5
- package/src/version.ts +0 -114
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.7",
|
|
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",
|
|
@@ -42,9 +42,6 @@
|
|
|
42
42
|
"engines": {
|
|
43
43
|
"bun": ">=1.2.0"
|
|
44
44
|
},
|
|
45
|
-
"opencode": {
|
|
46
|
-
"supportedRange": ">=1.15.0 <1.16.0"
|
|
47
|
-
},
|
|
48
45
|
"devDependencies": {
|
|
49
46
|
"@opencode-ai/plugin": "1.15.13",
|
|
50
47
|
"@opencode-ai/sdk": "1.15.13",
|
package/src/index.ts
CHANGED
|
@@ -5,10 +5,9 @@ import { injectCommandsAndAgents } from "./inject.js"
|
|
|
5
5
|
import { injectSkills } from "./skill-inject.js"
|
|
6
6
|
import { injectMcp } from "./mcp-inject.js"
|
|
7
7
|
import { injectLsp } from "./lsp-inject.js"
|
|
8
|
-
import { createLogger } from "./logger.js"
|
|
8
|
+
import { createLogger, type LoggingClient } from "./logger.js"
|
|
9
9
|
import { listClaudePlugins, selectEnabledPlugins } from "./selection.js"
|
|
10
10
|
import { collectExistingSkillNames } from "./skill-scan.js"
|
|
11
|
-
import { checkVersion, fetchOpencodeVersion } from "./version.js"
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Parse an environment-variable value as a boolean, matching the set of truthy
|
|
@@ -52,15 +51,11 @@ export const server: Plugin = async (_input, options) => {
|
|
|
52
51
|
|
|
53
52
|
return {
|
|
54
53
|
config: async (cfg) => {
|
|
55
|
-
const logger = createLogger(bridge.strict)
|
|
54
|
+
const logger = createLogger(bridge.strict, _input.client as unknown as LoggingClient)
|
|
56
55
|
try {
|
|
57
56
|
// Replay parse-time validation warnings (strict-promotable).
|
|
58
57
|
for (const w of warnings) logger.warn(w)
|
|
59
58
|
|
|
60
|
-
// §9 version-compat advisory (always soft — still attempts injection).
|
|
61
|
-
const version = await fetchOpencodeVersion(_input.serverUrl)
|
|
62
|
-
checkVersion(version, logger)
|
|
63
|
-
|
|
64
59
|
// §5 mirror-claude resolution.
|
|
65
60
|
const all = await listClaudePlugins(_input.$, logger)
|
|
66
61
|
if (all === null) return // CLI missing/failed — already warned; inject nothing.
|
package/src/logger.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
/** Minimal duck-type for the OpenCode client — only the log() method we use. */
|
|
2
|
+
export interface LoggingClient {
|
|
3
|
+
log(params: { service?: string; level?: "debug" | "info" | "warn" | "error"; message?: string }): unknown
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
/** Thrown when a soft warning is promoted to a hard error under `strict` mode. */
|
|
2
7
|
export class BridgeError extends Error {
|
|
3
8
|
override name = "BridgeError"
|
|
4
9
|
}
|
|
5
10
|
|
|
6
|
-
/** Prefix every line the bridge emits so its output is greppable in OpenCode's logs. */
|
|
7
|
-
export const LOG_PREFIX = "[opencode-claude-bridge]"
|
|
8
|
-
|
|
9
11
|
export interface WarnOptions {
|
|
10
12
|
/**
|
|
11
13
|
* Whether this warning should be promoted to a hard error under `strict`.
|
|
12
14
|
* Defaults to `true` (parse failures, missing CLI). Set `false` for advisory
|
|
13
|
-
* warnings that must never abort the hook even in strict mode
|
|
14
|
-
* version-range notice, which always still attempts injection).
|
|
15
|
+
* warnings that must never abort the hook even in strict mode.
|
|
15
16
|
*/
|
|
16
17
|
fatalInStrict?: boolean
|
|
17
18
|
}
|
|
@@ -28,26 +29,72 @@ export interface Logger {
|
|
|
28
29
|
hadWarnings(): boolean
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
|
|
31
33
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
34
|
+
* Fallback output logger used when no real OpenCode client is available (tests,
|
|
35
|
+
* edge-case early errors). Writes to process.stderr in OpenCode's structured
|
|
36
|
+
* format and only when --print-logs is in argv — matching OpenCode's own
|
|
37
|
+
* log-visibility behaviour.
|
|
35
38
|
*/
|
|
36
|
-
|
|
39
|
+
const fallbackLog = (() => {
|
|
40
|
+
const enabled = process.argv.includes("--print-logs")
|
|
41
|
+
let last = Date.now()
|
|
42
|
+
|
|
43
|
+
function write(level: "INFO" | "WARN", msg: string): void {
|
|
44
|
+
if (!enabled) return
|
|
45
|
+
const now = Date.now()
|
|
46
|
+
const ts = new Date(now).toISOString().split(".")[0]
|
|
47
|
+
const diff = now - last
|
|
48
|
+
last = now
|
|
49
|
+
process.stderr.write(`${level.padEnd(5)} ${ts} +${diff}ms service=opencode-claude-bridge ${msg}\n`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
info: (msg: string) => write("INFO", msg),
|
|
54
|
+
warn: (msg: string) => write("WARN", msg),
|
|
55
|
+
}
|
|
56
|
+
})()
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a logger bound to the resolved `strict` flag and the OpenCode client.
|
|
60
|
+
*
|
|
61
|
+
* When a client is provided, log entries are posted to the server via
|
|
62
|
+
* `client.log()` — they flow through OpenCode's own log pipeline, appear in
|
|
63
|
+
* the log file, and respect `--print-logs` automatically.
|
|
64
|
+
*
|
|
65
|
+
* When no client is provided (tests, early-startup errors) the fallback logger
|
|
66
|
+
* writes to process.stderr in the same format, gated on `--print-logs`.
|
|
67
|
+
*/
|
|
68
|
+
export function createLogger(strict: boolean, client?: LoggingClient): Logger {
|
|
37
69
|
let warningCount = 0
|
|
70
|
+
|
|
71
|
+
function logInfo(msg: string): void {
|
|
72
|
+
if (typeof client?.log === "function") {
|
|
73
|
+
void client.log({ service: "opencode-claude-bridge", level: "info", message: msg })
|
|
74
|
+
} else {
|
|
75
|
+
fallbackLog.info(msg)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function logWarn(msg: string): void {
|
|
80
|
+
if (typeof client?.log === "function") {
|
|
81
|
+
void client.log({ service: "opencode-claude-bridge", level: "warn", message: msg })
|
|
82
|
+
} else {
|
|
83
|
+
fallbackLog.warn(msg)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
38
87
|
return {
|
|
39
|
-
info(msg
|
|
40
|
-
|
|
88
|
+
info(msg) {
|
|
89
|
+
logInfo(msg)
|
|
41
90
|
},
|
|
42
|
-
warn(msg
|
|
91
|
+
warn(msg, opts) {
|
|
43
92
|
const fatalInStrict = opts?.fatalInStrict ?? true
|
|
44
93
|
warningCount++
|
|
45
|
-
if (strict && fatalInStrict)
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
console.warn(`${LOG_PREFIX} warning: ${msg}`)
|
|
94
|
+
if (strict && fatalInStrict) throw new BridgeError(msg)
|
|
95
|
+
logWarn(msg)
|
|
49
96
|
},
|
|
50
|
-
hadWarnings()
|
|
97
|
+
hadWarnings() {
|
|
51
98
|
return warningCount > 0
|
|
52
99
|
},
|
|
53
100
|
}
|
package/src/opencode-builtins.ts
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* The runtime OpenCode version verified by the spec is **1.15.10**; the source
|
|
5
|
-
* checkout on this machine is **1.15.13**. The constants were re-confirmed
|
|
6
|
-
* unchanged against the 1.15.13 source — the cited line numbers and values are
|
|
7
|
-
* identical in both versions. The pin below records the runtime version (what
|
|
8
|
-
* users run and what the spec validated against); re-validate on the next upgrade.
|
|
9
|
-
*
|
|
10
|
-
* MUST be re-validated on every OpenCode upgrade — these are part of what the
|
|
11
|
-
* §9 compatibility check guards.
|
|
2
|
+
* OpenCode built-in name lists and skill-discovery constants.
|
|
12
3
|
*
|
|
13
4
|
* Source locations (all in `packages/opencode/src/`):
|
|
14
5
|
* - Built-in agents: `agent/agent.ts:127-248`
|
|
@@ -17,11 +8,6 @@
|
|
|
17
8
|
* - Skill scan dirs: `skill/index.ts:22-26, 173-233`
|
|
18
9
|
*/
|
|
19
10
|
|
|
20
|
-
// The single source of truth for the verified OpenCode version is VERIFIED_OPENCODE_VERSION
|
|
21
|
-
// in src/version.ts. Re-export it here so callers in this module don't need a cross-module
|
|
22
|
-
// import, and so the value is never duplicated.
|
|
23
|
-
export { VERIFIED_OPENCODE_VERSION as PINNED_OPENCODE_VERSION } from "./version.js"
|
|
24
|
-
|
|
25
11
|
// ── Built-in agents ─────────────────────────────────────────────────────────
|
|
26
12
|
//
|
|
27
13
|
// Registered as hard-coded entries in the `agents` object before `cfg.agent`
|
package/src/skill-inject.ts
CHANGED
|
@@ -353,12 +353,14 @@ async function injectPluginSkills(
|
|
|
353
353
|
continue
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
356
|
+
const extractedName = extractSkillName(content)
|
|
357
|
+
// Fall back to the directory name when the SKILL.md has no `name:` field —
|
|
358
|
+
// the directory name is the conventional skill identifier and is always present.
|
|
359
|
+
const bareName = extractedName ?? subdir
|
|
360
|
+
if (extractedName === null) {
|
|
361
|
+
logger.info(
|
|
362
|
+
`SKILL.md at "${skillMdPath}" from plugin "${plugin.id}" has no frontmatter name; using directory name "${subdir}"`,
|
|
360
363
|
)
|
|
361
|
-
continue
|
|
362
364
|
}
|
|
363
365
|
|
|
364
366
|
const { name: allocatedName, renamed } = allocator.claim(plugin.id, bareName)
|
package/src/version.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import type { Logger } from "./logger.js"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Supported OpenCode version range. The injection approach relies on OpenCode-*internal*
|
|
5
|
-
* behavior (a shared mutable config object + lazy-after-`plugin.init()` service init) that
|
|
6
|
-
* is not a documented public contract, so the bridge pins a conservative same-minor window
|
|
7
|
-
* and warns outside it. Widen only when the e2e canary passes against a new version (§9).
|
|
8
|
-
*/
|
|
9
|
-
export const SUPPORTED_OPENCODE_RANGE = ">=1.15.0 <1.16.0"
|
|
10
|
-
|
|
11
|
-
/** The OpenCode version the design and range were verified against (§9). */
|
|
12
|
-
export const VERIFIED_OPENCODE_VERSION = "1.15.10"
|
|
13
|
-
|
|
14
|
-
interface Semver {
|
|
15
|
-
major: number
|
|
16
|
-
minor: number
|
|
17
|
-
patch: number
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Parse a `major.minor.patch` string, tolerating a leading `v` and ignoring any
|
|
22
|
-
* prerelease/build suffix (`-rc.1`, `+build`). Returns `null` if the core triple is
|
|
23
|
-
* not three integers.
|
|
24
|
-
*/
|
|
25
|
-
export function parseSemver(version: string): Semver | null {
|
|
26
|
-
// Anchored end so trailing garbage ("1.2.3abc", "1.2.3.4") is rejected rather than
|
|
27
|
-
// silently truncated to a bogus triple; only a `-prerelease` / `+build` suffix is allowed.
|
|
28
|
-
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version.trim())
|
|
29
|
-
if (!match) return null
|
|
30
|
-
return {
|
|
31
|
-
major: Number(match[1]),
|
|
32
|
-
minor: Number(match[2]),
|
|
33
|
-
patch: Number(match[3]),
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function compare(a: Semver, b: Semver): number {
|
|
38
|
-
return a.major - b.major || a.minor - b.minor || a.patch - b.patch
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const COMPARATORS: Record<string, (cmp: number) => boolean> = {
|
|
42
|
-
">=": (c) => c >= 0,
|
|
43
|
-
"<=": (c) => c <= 0,
|
|
44
|
-
">": (c) => c > 0,
|
|
45
|
-
"<": (c) => c < 0,
|
|
46
|
-
"=": (c) => c === 0,
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Test `version` against a space-separated AND range of simple comparator clauses
|
|
51
|
-
* (e.g. `">=1.15.0 <1.16.0"`). Returns `false` if `version` is unparseable or any
|
|
52
|
-
* clause is malformed — callers treat that as "outside the supported range".
|
|
53
|
-
*/
|
|
54
|
-
export function satisfiesRange(version: string, range: string): boolean {
|
|
55
|
-
const parsed = parseSemver(version)
|
|
56
|
-
if (!parsed) return false
|
|
57
|
-
|
|
58
|
-
const clauses = range.trim().split(/\s+/).filter(Boolean)
|
|
59
|
-
for (const clause of clauses) {
|
|
60
|
-
const match = /^(>=|<=|>|<|=)?\s*(.+)$/.exec(clause)
|
|
61
|
-
if (!match) return false
|
|
62
|
-
const op = match[2] === undefined ? null : (match[1] ?? "=")
|
|
63
|
-
const bound = parseSemver(match[2]!)
|
|
64
|
-
if (op === null || !bound) return false
|
|
65
|
-
if (!COMPARATORS[op]!(compare(parsed, bound))) return false
|
|
66
|
-
}
|
|
67
|
-
return true
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Read the running OpenCode version from the live HTTP server's health endpoint.
|
|
72
|
-
*
|
|
73
|
-
* This is the only mechanism available to an external plugin: the v1 `input.client`
|
|
74
|
-
* exposes no version method, there is no `OPENCODE_VERSION` env var at runtime, and the
|
|
75
|
-
* build-time define global is not visible outside OpenCode's own bundle. The endpoint is
|
|
76
|
-
* mounted at the server root (`GET /global/health` → `{ healthy, version }`) and the HTTP
|
|
77
|
-
* server is already running before `plugin.init()` fires, so calling it from the config
|
|
78
|
-
* hook is safe and touches none of the lazy component services.
|
|
79
|
-
*
|
|
80
|
-
* Returns `null` on any failure (network, parse, missing field) — the caller degrades to
|
|
81
|
-
* "version unknown" rather than blocking injection.
|
|
82
|
-
*/
|
|
83
|
-
export async function fetchOpencodeVersion(serverUrl: URL): Promise<string | null> {
|
|
84
|
-
try {
|
|
85
|
-
const res = await fetch(new URL("/global/health", serverUrl))
|
|
86
|
-
if (!res.ok) return null
|
|
87
|
-
const body = (await res.json()) as { version?: unknown }
|
|
88
|
-
return typeof body.version === "string" ? body.version : null
|
|
89
|
-
} catch {
|
|
90
|
-
return null
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Compare the running version against {@link SUPPORTED_OPENCODE_RANGE} and emit the §9
|
|
96
|
-
* advisory warning when it falls outside. This warning is **always soft** — even under
|
|
97
|
-
* `strict`, the bridge still attempts injection (the e2e suite is the real canary). A
|
|
98
|
-
* `null` version (health unreachable) is reported as an inability to verify, not a failure.
|
|
99
|
-
*/
|
|
100
|
-
export function checkVersion(version: string | null, logger: Logger): void {
|
|
101
|
-
if (version === null) {
|
|
102
|
-
logger.warn(
|
|
103
|
-
`could not determine the running OpenCode version; proceeding (supported range ${SUPPORTED_OPENCODE_RANGE})`,
|
|
104
|
-
{ fatalInStrict: false },
|
|
105
|
-
)
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
if (!satisfiesRange(version, SUPPORTED_OPENCODE_RANGE)) {
|
|
109
|
-
logger.warn(
|
|
110
|
-
`untested OpenCode version ${version} (supported range ${SUPPORTED_OPENCODE_RANGE}) — bridge may misbehave; attempting injection anyway`,
|
|
111
|
-
{ fatalInStrict: false },
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
}
|