@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koriit/opencode-claude-bridge",
3
- "version": "0.1.2",
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 (e.g. the §9
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
- * Create a logger bound to the resolved `strict` flag. The hook itself is responsible
33
- * for catching {@link BridgeError} in non-strict paths; in strict mode it lets the
34
- * error propagate so OpenCode surfaces a hard failure (design §10).
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
- export function createLogger(strict: boolean): Logger {
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: string): void {
40
- console.log(`${LOG_PREFIX} ${msg}`)
88
+ info(msg) {
89
+ logInfo(msg)
41
90
  },
42
- warn(msg: string, opts?: WarnOptions): void {
91
+ warn(msg, opts) {
43
92
  const fatalInStrict = opts?.fatalInStrict ?? true
44
93
  warningCount++
45
- if (strict && fatalInStrict) {
46
- throw new BridgeError(msg)
47
- }
48
- console.warn(`${LOG_PREFIX} warning: ${msg}`)
94
+ if (strict && fatalInStrict) throw new BridgeError(msg)
95
+ logWarn(msg)
49
96
  },
50
- hadWarnings(): boolean {
97
+ hadWarnings() {
51
98
  return warningCount > 0
52
99
  },
53
100
  }
@@ -1,14 +1,5 @@
1
1
  /**
2
- * Version-pinned OpenCode built-in name lists and skill-discovery constants.
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`
@@ -353,12 +353,14 @@ async function injectPluginSkills(
353
353
  continue
354
354
  }
355
355
 
356
- const bareName = extractSkillName(content)
357
- if (bareName === null) {
358
- logger.warn(
359
- `SKILL.md at "${skillMdPath}" from plugin "${plugin.id}" has no frontmatter name; skipping`,
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
- }