@michaelfromyeg/loom-adapter-opencode 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael DeMarco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ import { HarnessAdapter } from '@michaelfromyeg/loom-adapter-kit';
2
+
3
+ declare const opencodeAdapter: HarnessAdapter;
4
+
5
+ export { opencodeAdapter as default, opencodeAdapter };
package/dist/index.js ADDED
@@ -0,0 +1,322 @@
1
+ import { homedir } from 'os';
2
+ import { join, basename, extname, relative } from 'path';
3
+ import { artifact } from '@michaelfromyeg/loom-adapter-kit';
4
+ import { kindOf, refOf, leafNameOf } from '@michaelfromyeg/loom-schema';
5
+ import { existsSync, readFileSync, statSync, readdirSync } from 'fs';
6
+
7
+ // src/index.ts
8
+ var MCP_SCHEMA = "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json";
9
+ var json = (o) => `${JSON.stringify(o, null, 2)}
10
+ `;
11
+ function readJson(path) {
12
+ try {
13
+ return JSON.parse(readFileSync(path, "utf8"));
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+ function subdirsWith(dir, marker) {
19
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
20
+ return readdirSync(dir).filter((n) => statSync(join(dir, n)).isDirectory() && existsSync(join(dir, n, marker))).sort();
21
+ }
22
+ function filesWithExt(dir, ext) {
23
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
24
+ return readdirSync(dir).filter((n) => n.endsWith(ext) && statSync(join(dir, n)).isFile()).sort();
25
+ }
26
+ function copyTree(srcDir, destPrefix, kind) {
27
+ const out = [];
28
+ const walk = (d) => {
29
+ for (const n of readdirSync(d).sort()) {
30
+ const abs = join(d, n);
31
+ if (statSync(abs).isDirectory()) walk(abs);
32
+ else
33
+ out.push(artifact(`${destPrefix}/${relative(srcDir, abs)}`, readFileSync(abs), { kind }));
34
+ }
35
+ };
36
+ walk(srcDir);
37
+ return out;
38
+ }
39
+ function sourceToString(source) {
40
+ if (typeof source === "string") return source;
41
+ const s = source;
42
+ switch (s?.source) {
43
+ case "github":
44
+ return `github:${s.repo}${s.ref ? `#${s.ref}` : ""}`;
45
+ case "url":
46
+ case "git-subdir":
47
+ return String(s.url);
48
+ case "npm":
49
+ return `npm:${s.package}${s.version ? `@${s.version}` : ""}`;
50
+ default:
51
+ return String(source);
52
+ }
53
+ }
54
+ function synthesizeServerJson(namespace, name, cfg) {
55
+ const base = {
56
+ $schema: MCP_SCHEMA,
57
+ name: `${namespace}/${name}`,
58
+ description: `Imported ${name} MCP server.`,
59
+ version: "0.0.0"
60
+ };
61
+ if (cfg.type === "remote") {
62
+ const headers = cfg.headers ? Object.entries(cfg.headers).map(([n, value]) => ({ name: n, value })) : void 0;
63
+ return {
64
+ ...base,
65
+ remotes: [{ type: "streamable-http", url: cfg.url, ...headers?.length ? { headers } : {} }]
66
+ };
67
+ }
68
+ const command = cfg.command ?? [];
69
+ if (command[0] === "npx") {
70
+ const ident = command.slice(1).find((a) => a !== "-y" && !a.startsWith("-"));
71
+ if (ident) {
72
+ const at = ident.lastIndexOf("@");
73
+ const id = at > 0 ? ident.slice(0, at) : ident;
74
+ const version = at > 0 ? ident.slice(at + 1) : void 0;
75
+ return {
76
+ ...base,
77
+ packages: [
78
+ {
79
+ registryType: "npm",
80
+ identifier: id,
81
+ ...version ? { version } : {},
82
+ transport: { type: "stdio" }
83
+ }
84
+ ]
85
+ };
86
+ }
87
+ }
88
+ return {
89
+ ...base,
90
+ ...command.length ? { command: command[0], args: command.slice(1) } : {},
91
+ ...cfg.environment ? { env: cfg.environment } : {}
92
+ };
93
+ }
94
+ function importPlugin(dir, namespace) {
95
+ const components = [];
96
+ const files = [];
97
+ for (const sk of subdirsWith(join(dir, "skills"), "SKILL.md")) {
98
+ components.push({ skill: `skills/${sk}` });
99
+ files.push(...copyTree(join(dir, "skills", sk), `skills/${sk}`, "skill"));
100
+ }
101
+ for (const f of filesWithExt(join(dir, "agents"), ".md")) {
102
+ components.push({ agent: `agents/${f}` });
103
+ files.push(artifact(`agents/${f}`, readFileSync(join(dir, "agents", f)), { kind: "agent" }));
104
+ }
105
+ for (const f of filesWithExt(join(dir, "commands"), ".md")) {
106
+ components.push({ command: `commands/${f}` });
107
+ files.push(
108
+ artifact(`commands/${f}`, readFileSync(join(dir, "commands", f)), { kind: "command" })
109
+ );
110
+ }
111
+ const mcpDir = join(dir, "mcp");
112
+ const verbatim = subdirsWith(mcpDir, "server.json");
113
+ if (verbatim.length > 0) {
114
+ for (const leaf of verbatim) {
115
+ components.push({ mcp: `mcp/${leaf}` });
116
+ files.push(
117
+ artifact(`mcp/${leaf}/server.json`, readFileSync(join(mcpDir, leaf, "server.json")), {
118
+ kind: "mcp"
119
+ })
120
+ );
121
+ }
122
+ } else {
123
+ const config2 = readJson(join(dir, "opencode.json"));
124
+ const servers = config2?.mcp ?? {};
125
+ for (const [serverName, cfg] of Object.entries(servers)) {
126
+ components.push({ mcp: `mcp/${serverName}` });
127
+ files.push(
128
+ artifact(
129
+ `mcp/${serverName}/server.json`,
130
+ json(synthesizeServerJson(namespace, serverName, cfg)),
131
+ { kind: "mcp" }
132
+ )
133
+ );
134
+ }
135
+ }
136
+ const config = readJson(join(dir, "opencode.json"));
137
+ const name = config?.name ? String(config.name) : basename(dir);
138
+ const plugin = {
139
+ name,
140
+ version: "0.1.0",
141
+ owner: { name, namespace },
142
+ components
143
+ };
144
+ return { kind: "plugin", plugin, files };
145
+ }
146
+ function importMarketplace(manifest, namespace) {
147
+ const owner = manifest.owner;
148
+ const plugins = (manifest.plugins ?? []).map((p) => ({
149
+ plugin: sourceToString(p.source),
150
+ ...p.version ? { version: String(p.version) } : {},
151
+ ...p.category ? { category: String(p.category) } : {},
152
+ ...Array.isArray(p.tags) ? { tags: p.tags } : {}
153
+ }));
154
+ const marketplace = {
155
+ name: String(manifest.name),
156
+ owner: {
157
+ name: owner?.name ?? String(manifest.name),
158
+ namespace,
159
+ ...owner?.email ? { email: owner.email } : {}
160
+ },
161
+ ...manifest.description ? { description: String(manifest.description) } : {},
162
+ plugins
163
+ };
164
+ return { kind: "marketplace", marketplace };
165
+ }
166
+ function importOpencode(dir, opts) {
167
+ const namespace = opts?.namespace ?? "com.imported";
168
+ const marketplace = readJson(join(dir, "loom-marketplace.json"));
169
+ if (marketplace) return importMarketplace(marketplace, namespace);
170
+ if (existsSync(join(dir, "skills")) || existsSync(join(dir, "agents")) || existsSync(join(dir, "commands")) || existsSync(join(dir, "mcp")) || existsSync(join(dir, "opencode.json"))) {
171
+ return importPlugin(dir, namespace);
172
+ }
173
+ return null;
174
+ }
175
+
176
+ // src/mcp.ts
177
+ function mcpServerName(server) {
178
+ const raw = server.name ?? "server";
179
+ const afterSlash = raw.includes("/") ? raw.slice(raw.lastIndexOf("/") + 1) : raw;
180
+ return afterSlash || "server";
181
+ }
182
+ var RUNNERS = { npm: "npx", pypi: "uvx" };
183
+ function mcpRunConfig(server) {
184
+ const pkg = server.packages?.[0];
185
+ if (pkg?.identifier) {
186
+ if (pkg.registryType === "oci") {
187
+ return {
188
+ type: "local",
189
+ command: ["docker", "run", "-i", "--rm", pkg.identifier],
190
+ enabled: true
191
+ };
192
+ }
193
+ const runner = RUNNERS[pkg.registryType ?? "npm"] ?? "npx";
194
+ const ident = pkg.version ? `${pkg.identifier}@${pkg.version}` : pkg.identifier;
195
+ const command = runner === "npx" ? ["npx", "-y", ident] : [runner, ident];
196
+ return { type: "local", command, enabled: true };
197
+ }
198
+ const remote = server.remotes?.[0];
199
+ if (remote?.url) {
200
+ const config = { type: "remote", url: remote.url, enabled: true };
201
+ if (remote.headers?.length) {
202
+ config.headers = Object.fromEntries(remote.headers.map((h) => [h.name, h.value]));
203
+ }
204
+ return config;
205
+ }
206
+ if (server.command) {
207
+ const config = {
208
+ type: "local",
209
+ command: [server.command, ...server.args ?? []],
210
+ enabled: true
211
+ };
212
+ if (server.env) config.environment = server.env;
213
+ return config;
214
+ }
215
+ return {
216
+ type: "local",
217
+ command: ["echo", "server.json declares no runnable transport"],
218
+ enabled: true
219
+ };
220
+ }
221
+
222
+ // src/index.ts
223
+ var TARGET_SCHEMA = "opencode/1";
224
+ var json2 = (o) => `${JSON.stringify(o, null, 2)}
225
+ `;
226
+ function copyDir(ctx, ref, destPrefix, kind) {
227
+ return ctx.list(ref).map((file) => {
228
+ const within = file.startsWith(`${ref}/`) ? file.slice(ref.length + 1) : basename(file);
229
+ return artifact(`${destPrefix}/${within}`, ctx.read(file), { kind });
230
+ });
231
+ }
232
+ function copyFileOrDir(ctx, ref, destPrefix, kind) {
233
+ const files = ctx.list(ref);
234
+ if (files.length === 0) {
235
+ return [artifact(`${destPrefix}${extname(ref) || ".md"}`, ctx.read(ref), { kind })];
236
+ }
237
+ return copyDir(ctx, ref, destPrefix, kind);
238
+ }
239
+ var opencodeAdapter = {
240
+ target: "opencode",
241
+ version: "0.1.0",
242
+ targetSchema: TARGET_SCHEMA,
243
+ detect(scope, cwd) {
244
+ const root = scope === "user" ? join(homedir(), ".config", "opencode") : join(cwd, ".opencode");
245
+ return {
246
+ root,
247
+ // Executable plugins (.ts) live in plugins/; that is also where passthrough lands.
248
+ plugins: join(root, "plugins"),
249
+ skills: join(root, "skills"),
250
+ // opencode.json (with the mcp block) lives at the config root.
251
+ mcp: root,
252
+ agents: join(root, "agents"),
253
+ commands: join(root, "commands"),
254
+ hooks: join(root, "plugins"),
255
+ catalog: root
256
+ };
257
+ },
258
+ transform(component, ctx) {
259
+ const ref = refOf(component);
260
+ const leaf = leafNameOf(component);
261
+ switch (kindOf(component)) {
262
+ case "skill":
263
+ return copyDir(ctx, ref, `skills/${leaf}`, "skill");
264
+ case "agent":
265
+ return copyFileOrDir(ctx, ref, `agents/${leaf}`, "agent");
266
+ case "command":
267
+ return copyFileOrDir(ctx, ref, `commands/${leaf}`, "command");
268
+ case "hook":
269
+ return [
270
+ artifact(`plugins/${basename(ref)}`, ctx.read(ref), { kind: "hook", executable: true })
271
+ ];
272
+ case "mcp":
273
+ return copyDir(ctx, ref, `mcp/${leaf}`, "mcp");
274
+ case "passthrough":
275
+ return [
276
+ artifact(`plugins/${basename(ref)}`, ctx.read(ref), { kind: "hook", executable: true })
277
+ ];
278
+ default:
279
+ return [];
280
+ }
281
+ },
282
+ emitManifest(plugin, ctx) {
283
+ const mcp = {};
284
+ for (const c of plugin.components) {
285
+ if (kindOf(c) !== "mcp") continue;
286
+ try {
287
+ const server = JSON.parse(ctx.read(`${refOf(c)}/server.json`).toString("utf8"));
288
+ mcp[mcpServerName(server)] = mcpRunConfig(server);
289
+ } catch {
290
+ }
291
+ }
292
+ if (Object.keys(mcp).length === 0) return [];
293
+ const manifest = { mcp };
294
+ return [artifact("opencode.json", json2(manifest), { kind: "manifest" })];
295
+ },
296
+ emitCatalog(marketplace) {
297
+ const plugins = marketplace.entries.map((entry) => {
298
+ const entryPlugin = {
299
+ name: entry.name,
300
+ source: entry.source.startsWith("./") ? entry.source : `./${entry.source}`
301
+ };
302
+ if (entry.description) entryPlugin.description = entry.description;
303
+ if (entry.version) entryPlugin.version = entry.version;
304
+ if (entry.category) entryPlugin.category = entry.category;
305
+ if (entry.tags) entryPlugin.tags = entry.tags;
306
+ return entryPlugin;
307
+ });
308
+ const catalog = {
309
+ name: marketplace.name,
310
+ owner: marketplace.owner,
311
+ plugins
312
+ };
313
+ if (marketplace.description) catalog.description = marketplace.description;
314
+ return [artifact("loom-marketplace.json", json2(catalog), { kind: "catalog" })];
315
+ },
316
+ importNative: importOpencode
317
+ };
318
+ var index_default = opencodeAdapter;
319
+
320
+ export { index_default as default, opencodeAdapter };
321
+ //# sourceMappingURL=index.js.map
322
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/import.ts","../src/mcp.ts","../src/index.ts"],"names":["config","json","basename","artifact","join"],"mappings":"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","file":"index.js","sourcesContent":["import { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { basename, join, relative } from \"node:path\";\nimport {\n artifact,\n type CompiledArtifact,\n type ImportedMarketplace,\n type ImportedPlugin,\n type ImportOptions,\n type ImportResult,\n} from \"@michaelfromyeg/loom-adapter-kit\";\nimport type { Component, Marketplace, Plugin } from \"@michaelfromyeg/loom-schema\";\nimport type { OpencodeMcpServer } from \"./mcp\";\n\nconst MCP_SCHEMA = \"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json\";\nconst json = (o: unknown): string => `${JSON.stringify(o, null, 2)}\\n`;\n\nfunction readJson(path: string): Record<string, unknown> | null {\n try {\n return JSON.parse(readFileSync(path, \"utf8\"));\n } catch {\n return null;\n }\n}\n\n/** Subdirectories of `dir` that contain `marker`. */\nfunction subdirsWith(dir: string, marker: string): string[] {\n if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];\n return readdirSync(dir)\n .filter((n) => statSync(join(dir, n)).isDirectory() && existsSync(join(dir, n, marker)))\n .sort();\n}\n\nfunction filesWithExt(dir: string, ext: string): string[] {\n if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];\n return readdirSync(dir)\n .filter((n) => n.endsWith(ext) && statSync(join(dir, n)).isFile())\n .sort();\n}\n\nfunction copyTree(\n srcDir: string,\n destPrefix: string,\n kind: CompiledArtifact[\"kind\"],\n): CompiledArtifact[] {\n const out: CompiledArtifact[] = [];\n const walk = (d: string) => {\n for (const n of readdirSync(d).sort()) {\n const abs = join(d, n);\n if (statSync(abs).isDirectory()) walk(abs);\n else\n out.push(artifact(`${destPrefix}/${relative(srcDir, abs)}`, readFileSync(abs), { kind }));\n }\n };\n walk(srcDir);\n return out;\n}\n\n/** A Loom source string from a Loom-marketplace `source` (string or object). */\nfunction sourceToString(source: unknown): string {\n if (typeof source === \"string\") return source;\n const s = source as Record<string, unknown>;\n switch (s?.source) {\n case \"github\":\n return `github:${s.repo}${s.ref ? `#${s.ref}` : \"\"}`;\n case \"url\":\n case \"git-subdir\":\n return String(s.url);\n case \"npm\":\n return `npm:${s.package}${s.version ? `@${s.version}` : \"\"}`;\n default:\n return String(source);\n }\n}\n\n/**\n * Reconstruct an MCP-standard server.json from OpenCode's runnable `mcp` entry\n * (lossy but functional). OpenCode's local `command` is a STRING ARRAY (argv)\n * and the env key is `environment`, not `env`.\n */\nfunction synthesizeServerJson(namespace: string, name: string, cfg: OpencodeMcpServer): unknown {\n const base = {\n $schema: MCP_SCHEMA,\n name: `${namespace}/${name}`,\n description: `Imported ${name} MCP server.`,\n version: \"0.0.0\",\n };\n if (cfg.type === \"remote\") {\n const headers = cfg.headers\n ? Object.entries(cfg.headers).map(([n, value]) => ({ name: n, value }))\n : undefined;\n return {\n ...base,\n remotes: [{ type: \"streamable-http\", url: cfg.url, ...(headers?.length ? { headers } : {}) }],\n };\n }\n const command = cfg.command ?? [];\n // npm-style: [\"npx\",\"-y\",\"<ident>\"] -> npm package (parse the ident after -y).\n if (command[0] === \"npx\") {\n const ident = command.slice(1).find((a) => a !== \"-y\" && !a.startsWith(\"-\"));\n if (ident) {\n const at = ident.lastIndexOf(\"@\");\n const id = at > 0 ? ident.slice(0, at) : ident;\n const version = at > 0 ? ident.slice(at + 1) : undefined;\n return {\n ...base,\n packages: [\n {\n registryType: \"npm\",\n identifier: id,\n ...(version ? { version } : {}),\n transport: { type: \"stdio\" },\n },\n ],\n };\n }\n }\n // Bare command: first element is the executable, the rest are args.\n return {\n ...base,\n ...(command.length ? { command: command[0], args: command.slice(1) } : {}),\n ...(cfg.environment ? { env: cfg.environment } : {}),\n };\n}\n\nfunction importPlugin(dir: string, namespace: string): ImportedPlugin {\n const components: Component[] = [];\n const files: CompiledArtifact[] = [];\n\n for (const sk of subdirsWith(join(dir, \"skills\"), \"SKILL.md\")) {\n components.push({ skill: `skills/${sk}` });\n files.push(...copyTree(join(dir, \"skills\", sk), `skills/${sk}`, \"skill\"));\n }\n for (const f of filesWithExt(join(dir, \"agents\"), \".md\")) {\n components.push({ agent: `agents/${f}` });\n files.push(artifact(`agents/${f}`, readFileSync(join(dir, \"agents\", f)), { kind: \"agent\" }));\n }\n for (const f of filesWithExt(join(dir, \"commands\"), \".md\")) {\n components.push({ command: `commands/${f}` });\n files.push(\n artifact(`commands/${f}`, readFileSync(join(dir, \"commands\", f)), { kind: \"command\" }),\n );\n }\n\n // Prefer verbatim mcp/<leaf>/server.json copies the adapter wrote; only if there\n // is NO mcp/ dir do we reconstruct server.json from opencode.json's mcp block.\n const mcpDir = join(dir, \"mcp\");\n const verbatim = subdirsWith(mcpDir, \"server.json\");\n if (verbatim.length > 0) {\n for (const leaf of verbatim) {\n components.push({ mcp: `mcp/${leaf}` });\n files.push(\n artifact(`mcp/${leaf}/server.json`, readFileSync(join(mcpDir, leaf, \"server.json\")), {\n kind: \"mcp\",\n }),\n );\n }\n } else {\n const config = readJson(join(dir, \"opencode.json\"));\n const servers = (config?.mcp as Record<string, OpencodeMcpServer> | undefined) ?? {};\n for (const [serverName, cfg] of Object.entries(servers)) {\n components.push({ mcp: `mcp/${serverName}` });\n files.push(\n artifact(\n `mcp/${serverName}/server.json`,\n json(synthesizeServerJson(namespace, serverName, cfg)),\n { kind: \"mcp\" },\n ),\n );\n }\n }\n\n // OpenCode has no plugin manifest; the name is the directory basename, with the\n // opencode.json \"name\" (if any) preferred.\n const config = readJson(join(dir, \"opencode.json\"));\n const name = config?.name ? String(config.name) : basename(dir);\n const plugin: Plugin = {\n name,\n version: \"0.1.0\",\n owner: { name, namespace },\n components,\n };\n return { kind: \"plugin\", plugin, files };\n}\n\nfunction importMarketplace(\n manifest: Record<string, unknown>,\n namespace: string,\n): ImportedMarketplace {\n const owner = manifest.owner as { name?: string; email?: string } | undefined;\n const plugins = ((manifest.plugins as Array<Record<string, unknown>>) ?? []).map((p) => ({\n plugin: sourceToString(p.source),\n ...(p.version ? { version: String(p.version) } : {}),\n ...(p.category ? { category: String(p.category) } : {}),\n ...(Array.isArray(p.tags) ? { tags: p.tags as string[] } : {}),\n }));\n const marketplace: Marketplace = {\n name: String(manifest.name),\n owner: {\n name: owner?.name ?? String(manifest.name),\n namespace,\n ...(owner?.email ? { email: owner.email } : {}),\n },\n ...(manifest.description ? { description: String(manifest.description) } : {}),\n plugins,\n };\n return { kind: \"marketplace\", marketplace };\n}\n\n/** Reverse-compile an OpenCode plugin or Loom-marketplace dir into the Loom model. */\nexport function importOpencode(dir: string, opts?: ImportOptions): ImportResult | null {\n const namespace = opts?.namespace ?? \"com.imported\";\n\n // OpenCode has no native marketplace; the adapter emits a Loom-only catalog.\n const marketplace = readJson(join(dir, \"loom-marketplace.json\"));\n if (marketplace) return importMarketplace(marketplace, namespace);\n\n // Directory convention: any plural component dir makes this an importable plugin.\n if (\n existsSync(join(dir, \"skills\")) ||\n existsSync(join(dir, \"agents\")) ||\n existsSync(join(dir, \"commands\")) ||\n existsSync(join(dir, \"mcp\")) ||\n existsSync(join(dir, \"opencode.json\"))\n ) {\n return importPlugin(dir, namespace);\n }\n return null;\n}\n","/**\n * Derives a runnable OpenCode `mcp` entry from an MCP-standard `server.json`\n * (the registry's ServerJSON shape: packages[] / remotes[] / a bare command).\n * Stored standards stay verbatim in the plugin; this is the tool-specific view.\n *\n * OpenCode's mcp shape differs from most harnesses (see docs/harness-research.md):\n * local -> { type: \"local\", command: [ ...string ARRAY... ], environment: {…}, enabled: true }\n * remote -> { type: \"remote\", url, headers: {…}, enabled: true }\n * Note the local invocation is a single `command` STRING ARRAY (argv) and the\n * env key is `environment`, NOT `env`.\n */\n\nexport interface OpencodeLocalServer {\n type: \"local\";\n command: string[];\n environment?: Record<string, string>;\n enabled: boolean;\n}\nexport interface OpencodeRemoteServer {\n type: \"remote\";\n url: string;\n headers?: Record<string, string>;\n enabled: boolean;\n}\nexport type OpencodeMcpServer = OpencodeLocalServer | OpencodeRemoteServer;\n\ninterface McpPackage {\n registryType?: string;\n identifier?: string;\n version?: string;\n transport?: { type?: string };\n}\ninterface McpRemote {\n type?: string;\n url?: string;\n headers?: Array<{ name: string; value: string }>;\n}\ninterface ServerJson {\n name?: string;\n packages?: McpPackage[];\n remotes?: McpRemote[];\n command?: string;\n args?: string[];\n env?: Record<string, string>;\n}\n\n/** The short server key OpenCode uses (the part after the reverse-DNS namespace). */\nexport function mcpServerName(server: ServerJson): string {\n const raw = server.name ?? \"server\";\n const afterSlash = raw.includes(\"/\") ? raw.slice(raw.lastIndexOf(\"/\") + 1) : raw;\n return afterSlash || \"server\";\n}\n\n// TODO(verify): the research doc only documents npx/local examples for OpenCode's\n// command array. pypi/oci runner choice (uvx/docker) is a best-effort mirror of\n// the other adapters and not confirmed against OpenCode docs.\nconst RUNNERS: Record<string, string> = { npm: \"npx\", pypi: \"uvx\" };\n\nexport function mcpRunConfig(server: ServerJson): OpencodeMcpServer {\n const pkg = server.packages?.[0];\n if (pkg?.identifier) {\n if (pkg.registryType === \"oci\") {\n return {\n type: \"local\",\n command: [\"docker\", \"run\", \"-i\", \"--rm\", pkg.identifier],\n enabled: true,\n };\n }\n const runner = RUNNERS[pkg.registryType ?? \"npm\"] ?? \"npx\";\n const ident = pkg.version ? `${pkg.identifier}@${pkg.version}` : pkg.identifier;\n const command = runner === \"npx\" ? [\"npx\", \"-y\", ident] : [runner, ident];\n return { type: \"local\", command, enabled: true };\n }\n\n const remote = server.remotes?.[0];\n if (remote?.url) {\n const config: OpencodeRemoteServer = { type: \"remote\", url: remote.url, enabled: true };\n if (remote.headers?.length) {\n config.headers = Object.fromEntries(remote.headers.map((h) => [h.name, h.value]));\n }\n return config;\n }\n\n if (server.command) {\n const config: OpencodeLocalServer = {\n type: \"local\",\n command: [server.command, ...(server.args ?? [])],\n enabled: true,\n };\n if (server.env) config.environment = server.env;\n return config;\n }\n\n return {\n type: \"local\",\n command: [\"echo\", \"server.json declares no runnable transport\"],\n enabled: true,\n };\n}\n","import { homedir } from \"node:os\";\nimport { basename, extname, join } from \"node:path\";\nimport {\n artifact,\n type CompiledArtifact,\n type HarnessAdapter,\n type InstallPaths,\n type PluginCtx,\n type ResolvedMarketplace,\n} from \"@michaelfromyeg/loom-adapter-kit\";\nimport {\n type Component,\n kindOf,\n leafNameOf,\n type Plugin,\n refOf,\n type Scope,\n} from \"@michaelfromyeg/loom-schema\";\nimport { importOpencode } from \"./import\";\nimport { mcpRunConfig, mcpServerName, type OpencodeMcpServer } from \"./mcp\";\n\n/** Bump on any change to OpenCode's directory-convention / opencode.json shape (spec §5). */\nconst TARGET_SCHEMA = \"opencode/1\";\n\ninterface OpencodeManifest {\n mcp: Record<string, OpencodeMcpServer>;\n}\ninterface LoomMarketplacePlugin {\n name: string;\n source: string;\n description?: string;\n version?: string;\n category?: string;\n tags?: string[];\n}\ninterface LoomMarketplace {\n name: string;\n owner: { name: string; namespace: string; email?: string };\n description?: string;\n plugins: LoomMarketplacePlugin[];\n}\n\nconst json = (o: unknown): string => `${JSON.stringify(o, null, 2)}\\n`;\n\n/** Copy every file under a plugin dir into `destPrefix/`, preserving structure. */\nfunction copyDir(\n ctx: PluginCtx,\n ref: string,\n destPrefix: string,\n kind: CompiledArtifact[\"kind\"],\n): CompiledArtifact[] {\n return ctx.list(ref).map((file) => {\n const within = file.startsWith(`${ref}/`) ? file.slice(ref.length + 1) : basename(file);\n return artifact(`${destPrefix}/${within}`, ctx.read(file), { kind });\n });\n}\n\n/**\n * Place a component that may be a single Markdown file or a directory. OpenCode\n * wants agents/commands as a flat `<leaf>.md`; a bare-file source falls back to\n * `.md`, while a directory source is copied verbatim under `destPrefix/`.\n */\nfunction copyFileOrDir(\n ctx: PluginCtx,\n ref: string,\n destPrefix: string,\n kind: CompiledArtifact[\"kind\"],\n): CompiledArtifact[] {\n const files = ctx.list(ref);\n if (files.length === 0) {\n // ref is a single file (e.g. agents/code-review.md).\n return [artifact(`${destPrefix}${extname(ref) || \".md\"}`, ctx.read(ref), { kind })];\n }\n return copyDir(ctx, ref, destPrefix, kind);\n}\n\nexport const opencodeAdapter: HarnessAdapter = {\n target: \"opencode\",\n version: \"0.1.0\",\n targetSchema: TARGET_SCHEMA,\n\n detect(scope: Scope, cwd: string): InstallPaths {\n // User config lives under XDG `~/.config/opencode`; project under `<cwd>/.opencode`.\n const root = scope === \"user\" ? join(homedir(), \".config\", \"opencode\") : join(cwd, \".opencode\");\n return {\n root,\n // Executable plugins (.ts) live in plugins/; that is also where passthrough lands.\n plugins: join(root, \"plugins\"),\n skills: join(root, \"skills\"),\n // opencode.json (with the mcp block) lives at the config root.\n mcp: root,\n agents: join(root, \"agents\"),\n commands: join(root, \"commands\"),\n hooks: join(root, \"plugins\"),\n catalog: root,\n };\n },\n\n transform(component: Component, ctx: PluginCtx): CompiledArtifact[] {\n const ref = refOf(component);\n const leaf = leafNameOf(component);\n switch (kindOf(component)) {\n case \"skill\":\n return copyDir(ctx, ref, `skills/${leaf}`, \"skill\");\n case \"agent\":\n return copyFileOrDir(ctx, ref, `agents/${leaf}`, \"agent\");\n case \"command\":\n return copyFileOrDir(ctx, ref, `commands/${leaf}`, \"command\");\n case \"hook\":\n // OpenCode has no standalone hooks dir; hooks are executable plugins.\n return [\n artifact(`plugins/${basename(ref)}`, ctx.read(ref), { kind: \"hook\", executable: true }),\n ];\n case \"mcp\":\n // Verbatim provenance copy; the runnable config goes into opencode.json's mcp block.\n return copyDir(ctx, ref, `mcp/${leaf}`, \"mcp\");\n case \"passthrough\":\n // Executable plugins/scripts land in plugins/, placed DISABLED (spec §11).\n return [\n artifact(`plugins/${basename(ref)}`, ctx.read(ref), { kind: \"hook\", executable: true }),\n ];\n default:\n return [];\n }\n },\n\n emitManifest(plugin: Plugin, ctx: PluginCtx): CompiledArtifact[] {\n // OpenCode has NO central plugin manifest. The only thing to emit is the\n // aggregated `mcp` block; on install this block is MERGED into the user's\n // existing opencode.json rather than replacing it.\n const mcp: Record<string, OpencodeMcpServer> = {};\n for (const c of plugin.components) {\n if (kindOf(c) !== \"mcp\") continue;\n try {\n const server = JSON.parse(ctx.read(`${refOf(c)}/server.json`).toString(\"utf8\"));\n mcp[mcpServerName(server)] = mcpRunConfig(server);\n } catch {\n // validate.ts already surfaced an error for an unparsable server.json.\n }\n }\n if (Object.keys(mcp).length === 0) return [];\n\n const manifest: OpencodeManifest = { mcp };\n return [artifact(\"opencode.json\", json(manifest), { kind: \"manifest\" })];\n },\n\n emitCatalog(marketplace: ResolvedMarketplace): CompiledArtifact[] {\n // OpenCode reads no marketplace catalog -- do NOT synthesize a manifest it\n // ignores. Emit a Loom-only index so the build output is inspectable; OpenCode\n // does not consume this file.\n const plugins: LoomMarketplacePlugin[] = marketplace.entries.map((entry) => {\n const entryPlugin: LoomMarketplacePlugin = {\n name: entry.name,\n source: entry.source.startsWith(\"./\") ? entry.source : `./${entry.source}`,\n };\n if (entry.description) entryPlugin.description = entry.description;\n if (entry.version) entryPlugin.version = entry.version;\n if (entry.category) entryPlugin.category = entry.category;\n if (entry.tags) entryPlugin.tags = entry.tags;\n return entryPlugin;\n });\n\n const catalog: LoomMarketplace = {\n name: marketplace.name,\n owner: marketplace.owner,\n plugins,\n };\n if (marketplace.description) catalog.description = marketplace.description;\n\n return [artifact(\"loom-marketplace.json\", json(catalog), { kind: \"catalog\" })];\n },\n\n importNative: importOpencode,\n};\n\nexport default opencodeAdapter;\n"]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@michaelfromyeg/loom-adapter-opencode",
3
+ "version": "0.1.0",
4
+ "description": "Loom adapter for OpenCode: directory-convention placement + opencode.json mcp block.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@michaelfromyeg/loom-adapter-kit": "0.1.0",
20
+ "@michaelfromyeg/loom-schema": "0.1.0"
21
+ },
22
+ "devDependencies": {
23
+ "@michaelfromyeg/loom-core": "0.1.0"
24
+ },
25
+ "license": "MIT",
26
+ "author": "Michael DeMarco",
27
+ "homepage": "https://github.com/michaelfromyeg/loom#readme",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/michaelfromyeg/loom.git",
31
+ "directory": "packages/adapter-opencode"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/michaelfromyeg/loom/issues"
35
+ },
36
+ "keywords": [
37
+ "loom",
38
+ "coding-agent",
39
+ "ai-agent",
40
+ "claude-code",
41
+ "codex",
42
+ "cursor",
43
+ "copilot",
44
+ "opencode",
45
+ "mcp",
46
+ "plugin",
47
+ "skill",
48
+ "compiler"
49
+ ],
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "scripts": {
54
+ "build": "tsup"
55
+ }
56
+ }