@tanstack/ai-opencode 0.2.3 → 0.2.5
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/dist/esm/adapters/projection.js +140 -67
- package/dist/esm/adapters/projection.js.map +1 -1
- package/dist/esm/adapters/text.js +194 -220
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/index.js +6 -21
- package/dist/esm/messages/prompt.js +27 -33
- package/dist/esm/messages/prompt.js.map +1 -1
- package/dist/esm/model-meta.js +23 -12
- package/dist/esm/model-meta.js.map +1 -1
- package/dist/esm/process/permissions.js +56 -44
- package/dist/esm/process/permissions.js.map +1 -1
- package/dist/esm/process/sandbox-server.js +52 -68
- package/dist/esm/process/sandbox-server.js.map +1 -1
- package/dist/esm/process/server.js +162 -154
- package/dist/esm/process/server.js.map +1 -1
- package/dist/esm/stream/queue.js +60 -55
- package/dist/esm/stream/queue.js.map +1 -1
- package/dist/esm/stream/translate.js +311 -296
- package/dist/esm/stream/translate.js.map +1 -1
- package/package.json +7 -7
- package/src/adapters/text.ts +50 -1
- package/dist/esm/index.js.map +0 -1
|
@@ -1,83 +1,156 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isSecretRef, resolveGitSkillDir } from "@tanstack/ai-sandbox";
|
|
2
|
+
//#region src/adapters/projection.ts
|
|
3
|
+
/**
|
|
4
|
+
* OpenCode workspace projector — mirrors the Claude Code reference
|
|
5
|
+
* implementation at `packages/ai-claude-code/src/adapters/projection.ts`.
|
|
6
|
+
*
|
|
7
|
+
* `withSandbox` surfaces a portable `WorkspaceProjection` (skills, plugins, a
|
|
8
|
+
* secret resolver, and a one-time marker path) via a capability. This adapter
|
|
9
|
+
* reads it in `chatStream` setup and projects workspace inputs into OpenCode's
|
|
10
|
+
* native format:
|
|
11
|
+
*
|
|
12
|
+
* - MCP servers → `opencode.json` at the workspace root (mcp section).
|
|
13
|
+
* Written on EVERY call (never marker-gated) so rotated
|
|
14
|
+
* secrets always re-apply.
|
|
15
|
+
* - gitSkill repos → OpenCode has no recognised skills directory; we
|
|
16
|
+
* warn-and-skip rather than invent a path.
|
|
17
|
+
* - agentSkill → no bare-name primitive in opencode; warn-and-skip.
|
|
18
|
+
* - plugins → opencode has no `opencode plugin install` command;
|
|
19
|
+
* warn-and-skip.
|
|
20
|
+
*
|
|
21
|
+
* The `opencode.json` mcp-section shape mirrors the `OPENCODE_CONFIG_CONTENT`
|
|
22
|
+
* shape already used by the adapter's host-tool-bridge, ensuring both
|
|
23
|
+
* mechanisms use the same server-entry schema:
|
|
24
|
+
*
|
|
25
|
+
* ```json
|
|
26
|
+
* { "mcp": { "<name>": { "type": "remote", "url": "...", "enabled": true,
|
|
27
|
+
* "headers": { ... } } } }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* OpenCode merges project-scoped `opencode.json` with its environment config,
|
|
31
|
+
* so the workspace file coexists safely with the runtime `OPENCODE_CONFIG_CONTENT`
|
|
32
|
+
* env written for the host-tool-bridge.
|
|
33
|
+
*
|
|
34
|
+
* External-convention caveat: the `opencode.json` location and the `remote`
|
|
35
|
+
* MCP-entry shape are derived from the adapter's existing `OPENCODE_CONFIG_CONTENT`
|
|
36
|
+
* usage. Where OpenCode has no clean primitive (gitSkill dir, agentSkill,
|
|
37
|
+
* plugins) we no-op with a warning instead of fabricating a command.
|
|
38
|
+
*/
|
|
39
|
+
/** True when `value` is a `bearer(ref)` marker created by `@tanstack/ai-sandbox`. */
|
|
2
40
|
function isBearerMarker(value) {
|
|
3
|
-
|
|
41
|
+
return typeof value === "object" && value !== null && isSecretRef(value.__bearerRef);
|
|
4
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a single MCP header value: a `SecretRef` resolves to its plaintext,
|
|
45
|
+
* a `bearer(ref)` marker resolves to `Bearer <plaintext>`, and a plain string
|
|
46
|
+
* is passed through unchanged.
|
|
47
|
+
*/
|
|
5
48
|
function resolveHeaderValue(value, resolveSecret) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
49
|
+
if (isSecretRef(value)) return resolveSecret(value);
|
|
50
|
+
if (isBearerMarker(value)) return `Bearer ${resolveSecret(value.__bearerRef)}`;
|
|
51
|
+
return value;
|
|
9
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the `mcp` section of OpenCode's `opencode.json` from the
|
|
55
|
+
* `{ kind: 'mcp' }` skills, resolving every header value (SecretRef / bearer
|
|
56
|
+
* / string). Returns `undefined` when there are no MCP skills so the caller
|
|
57
|
+
* can skip the write.
|
|
58
|
+
*/
|
|
10
59
|
function buildMcpSection(skills, resolveSecret) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
60
|
+
const mcp = {};
|
|
61
|
+
let count = 0;
|
|
62
|
+
for (const skill of skills) {
|
|
63
|
+
if (skill.kind !== "mcp") continue;
|
|
64
|
+
count += 1;
|
|
65
|
+
const headers = {};
|
|
66
|
+
const rawHeaders = skill.config.headers ?? {};
|
|
67
|
+
for (const [name, value] of Object.entries(rawHeaders)) headers[name] = resolveHeaderValue(value, resolveSecret);
|
|
68
|
+
const rawUrl = skill.config["url"];
|
|
69
|
+
const url = typeof rawUrl === "string" ? rawUrl : "";
|
|
70
|
+
mcp[skill.name] = {
|
|
71
|
+
type: "remote",
|
|
72
|
+
url,
|
|
73
|
+
enabled: true,
|
|
74
|
+
headers
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return count > 0 ? mcp : void 0;
|
|
26
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Write (or merge-update) `opencode.json` at the workspace root with the
|
|
81
|
+
* project-scoped MCP servers, re-resolving every secret. Runs on EVERY
|
|
82
|
+
* projection call (never marker-gated) so rotated secrets always re-apply.
|
|
83
|
+
*
|
|
84
|
+
* If `opencode.json` already exists (e.g. committed to the repo) we read it,
|
|
85
|
+
* merge only the `mcp` key, and rewrite, preserving other settings. When there
|
|
86
|
+
* are no MCP skills the write is skipped entirely.
|
|
87
|
+
*/
|
|
27
88
|
async function projectMcpServers(handle, projection) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
} catch {
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const merged = { ...existing, mcp: mcpSection };
|
|
46
|
-
await handle.fs.write(target, JSON.stringify(merged, null, 2));
|
|
89
|
+
const mcpSection = buildMcpSection(projection.skills, projection.resolveSecret);
|
|
90
|
+
if (mcpSection === void 0) return;
|
|
91
|
+
const target = `${projection.root}/opencode.json`;
|
|
92
|
+
let existing = {};
|
|
93
|
+
if (await handle.fs.exists(target)) try {
|
|
94
|
+
const raw = await handle.fs.read(target);
|
|
95
|
+
const parsed = JSON.parse(raw);
|
|
96
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) existing = parsed;
|
|
97
|
+
} catch {}
|
|
98
|
+
const merged = {
|
|
99
|
+
...existing,
|
|
100
|
+
mcp: mcpSection
|
|
101
|
+
};
|
|
102
|
+
await handle.fs.write(target, JSON.stringify(merged, null, 2));
|
|
47
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* OpenCode has no recognised skills directory where we can drop gitSkill repos.
|
|
106
|
+
* AGENTS.md is already handled by the bootstrap layer (OpenCode reads it
|
|
107
|
+
* natively). We warn-and-skip instead of inventing a path.
|
|
108
|
+
*/
|
|
48
109
|
function projectGitSkills(projection) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
55
|
-
}
|
|
110
|
+
for (const skill of projection.skills) {
|
|
111
|
+
if (skill.kind !== "git") continue;
|
|
112
|
+
const dir = skill.into ?? resolveGitSkillDir(projection.root, skill);
|
|
113
|
+
console.warn(`[opencode] gitSkill "${skill.repo}" cloned to ${dir} but OpenCode has no recognised skills directory to link it into. The skill is available at that path — add an AGENTS.md reference to it manually if needed. Skipping.`);
|
|
114
|
+
}
|
|
56
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* `agentSkill` references a public skill by bare name. OpenCode has no command
|
|
118
|
+
* to fetch a skill from a bare name, so we warn and skip.
|
|
119
|
+
*/
|
|
57
120
|
function projectAgentSkills(projection) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
);
|
|
63
|
-
}
|
|
121
|
+
for (const skill of projection.skills) {
|
|
122
|
+
if (skill.kind !== "agent-skill") continue;
|
|
123
|
+
console.warn(`[opencode] agentSkill "${skill.name}" cannot be projected: OpenCode has no command to install a public skill by bare name. Provide it as a gitSkill instead. Skipping.`);
|
|
124
|
+
}
|
|
64
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* OpenCode has no plugin install command. We warn-and-skip each declared plugin
|
|
128
|
+
* rather than fabricating a command.
|
|
129
|
+
*/
|
|
65
130
|
function projectPlugins(projection) {
|
|
66
|
-
|
|
67
|
-
console.warn(
|
|
68
|
-
`[opencode] plugin "${name}" cannot be installed: OpenCode has no plugin install command. Skipping.`
|
|
69
|
-
);
|
|
70
|
-
}
|
|
131
|
+
for (const name of projection.plugins) console.warn(`[opencode] plugin "${name}" cannot be installed: OpenCode has no plugin install command. Skipping.`);
|
|
71
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Project a `WorkspaceProjection` into the OpenCode sandbox. Safe to call on
|
|
135
|
+
* every `chatStream`. The secret-bearing `opencode.json` mcp section is
|
|
136
|
+
* (re)written on every call, re-resolving secrets, so OpenCode always reads
|
|
137
|
+
* current values and a snapshot can never serve a stale or rotated secret.
|
|
138
|
+
* The safe, idempotent, non-secret operations (gitSkill, agentSkill, plugins
|
|
139
|
+
* — all warn-and-skip for OpenCode) are guarded by a one-time marker so they
|
|
140
|
+
* produce at most one warning per sandbox lifetime.
|
|
141
|
+
*
|
|
142
|
+
* @param handle - The sandbox handle (`fs` + `process`).
|
|
143
|
+
* @param projection - The portable workspace inputs from `withSandbox`.
|
|
144
|
+
*/
|
|
72
145
|
async function projectOpencodeWorkspace(handle, projection) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
146
|
+
await projectMcpServers(handle, projection);
|
|
147
|
+
if (await handle.fs.exists(projection.markerPath)) return;
|
|
148
|
+
projectGitSkills(projection);
|
|
149
|
+
projectAgentSkills(projection);
|
|
150
|
+
projectPlugins(projection);
|
|
151
|
+
await handle.fs.write(projection.markerPath, "");
|
|
79
152
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//# sourceMappingURL=projection.js.map
|
|
153
|
+
//#endregion
|
|
154
|
+
export { projectOpencodeWorkspace };
|
|
155
|
+
|
|
156
|
+
//# sourceMappingURL=projection.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projection.js","sources":["../../../src/adapters/projection.ts"],"sourcesContent":["/**\n * OpenCode workspace projector — mirrors the Claude Code reference\n * implementation at `packages/ai-claude-code/src/adapters/projection.ts`.\n *\n * `withSandbox` surfaces a portable `WorkspaceProjection` (skills, plugins, a\n * secret resolver, and a one-time marker path) via a capability. This adapter\n * reads it in `chatStream` setup and projects workspace inputs into OpenCode's\n * native format:\n *\n * - MCP servers → `opencode.json` at the workspace root (mcp section).\n * Written on EVERY call (never marker-gated) so rotated\n * secrets always re-apply.\n * - gitSkill repos → OpenCode has no recognised skills directory; we\n * warn-and-skip rather than invent a path.\n * - agentSkill → no bare-name primitive in opencode; warn-and-skip.\n * - plugins → opencode has no `opencode plugin install` command;\n * warn-and-skip.\n *\n * The `opencode.json` mcp-section shape mirrors the `OPENCODE_CONFIG_CONTENT`\n * shape already used by the adapter's host-tool-bridge, ensuring both\n * mechanisms use the same server-entry schema:\n *\n * ```json\n * { \"mcp\": { \"<name>\": { \"type\": \"remote\", \"url\": \"...\", \"enabled\": true,\n * \"headers\": { ... } } } }\n * ```\n *\n * OpenCode merges project-scoped `opencode.json` with its environment config,\n * so the workspace file coexists safely with the runtime `OPENCODE_CONFIG_CONTENT`\n * env written for the host-tool-bridge.\n *\n * External-convention caveat: the `opencode.json` location and the `remote`\n * MCP-entry shape are derived from the adapter's existing `OPENCODE_CONFIG_CONTENT`\n * usage. Where OpenCode has no clean primitive (gitSkill dir, agentSkill,\n * plugins) we no-op with a warning instead of fabricating a command.\n */\nimport { isSecretRef, resolveGitSkillDir } from '@tanstack/ai-sandbox'\nimport type {\n BearerRef,\n SandboxHandle,\n SecretRef,\n WorkspaceProjection,\n WorkspaceSkill,\n} from '@tanstack/ai-sandbox'\n\n/** True when `value` is a `bearer(ref)` marker created by `@tanstack/ai-sandbox`. */\nfunction isBearerMarker(value: unknown): value is BearerRef {\n return (\n typeof value === 'object' &&\n value !== null &&\n isSecretRef((value as { __bearerRef?: unknown }).__bearerRef)\n )\n}\n\n/**\n * Resolve a single MCP header value: a `SecretRef` resolves to its plaintext,\n * a `bearer(ref)` marker resolves to `Bearer <plaintext>`, and a plain string\n * is passed through unchanged.\n */\nfunction resolveHeaderValue(\n value: string | SecretRef | BearerRef,\n resolveSecret: (ref: SecretRef) => string,\n): string {\n if (isSecretRef(value)) return resolveSecret(value)\n if (isBearerMarker(value)) return `Bearer ${resolveSecret(value.__bearerRef)}`\n return value\n}\n\n/** An OpenCode `remote`-type MCP server entry (mirrors OPENCODE_CONFIG_CONTENT shape). */\ninterface OpencodeMcpServer {\n type: 'remote'\n url: string\n enabled: boolean\n headers: Record<string, string>\n}\n\n/**\n * Build the `mcp` section of OpenCode's `opencode.json` from the\n * `{ kind: 'mcp' }` skills, resolving every header value (SecretRef / bearer\n * / string). Returns `undefined` when there are no MCP skills so the caller\n * can skip the write.\n */\nfunction buildMcpSection(\n skills: Array<WorkspaceSkill>,\n resolveSecret: (ref: SecretRef) => string,\n): Record<string, OpencodeMcpServer> | undefined {\n const mcp: Record<string, OpencodeMcpServer> = {}\n let count = 0\n for (const skill of skills) {\n if (skill.kind !== 'mcp') continue\n count += 1\n const headers: Record<string, string> = {}\n const rawHeaders = skill.config.headers ?? {}\n for (const [name, value] of Object.entries(rawHeaders)) {\n headers[name] = resolveHeaderValue(value, resolveSecret)\n }\n const rawUrl = skill.config['url']\n const url = typeof rawUrl === 'string' ? rawUrl : ''\n mcp[skill.name] = { type: 'remote', url, enabled: true, headers }\n }\n return count > 0 ? mcp : undefined\n}\n\n/**\n * Write (or merge-update) `opencode.json` at the workspace root with the\n * project-scoped MCP servers, re-resolving every secret. Runs on EVERY\n * projection call (never marker-gated) so rotated secrets always re-apply.\n *\n * If `opencode.json` already exists (e.g. committed to the repo) we read it,\n * merge only the `mcp` key, and rewrite, preserving other settings. When there\n * are no MCP skills the write is skipped entirely.\n */\nasync function projectMcpServers(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n): Promise<void> {\n const mcpSection = buildMcpSection(\n projection.skills,\n projection.resolveSecret,\n )\n if (mcpSection === undefined) return\n\n const target = `${projection.root}/opencode.json`\n\n // Read the existing file if present so we can preserve non-mcp settings.\n let existing: Record<string, unknown> = {}\n if (await handle.fs.exists(target)) {\n try {\n const raw = await handle.fs.read(target)\n const parsed: unknown = JSON.parse(raw)\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n !Array.isArray(parsed)\n ) {\n existing = parsed as Record<string, unknown>\n }\n } catch {\n // Unreadable or invalid JSON — start fresh so the MCP config lands cleanly.\n }\n }\n\n const merged = { ...existing, mcp: mcpSection }\n await handle.fs.write(target, JSON.stringify(merged, null, 2))\n}\n\n/**\n * OpenCode has no recognised skills directory where we can drop gitSkill repos.\n * AGENTS.md is already handled by the bootstrap layer (OpenCode reads it\n * natively). We warn-and-skip instead of inventing a path.\n */\nfunction projectGitSkills(projection: WorkspaceProjection): void {\n for (const skill of projection.skills) {\n if (skill.kind !== 'git') continue\n const dir = skill.into ?? resolveGitSkillDir(projection.root, skill)\n console.warn(\n `[opencode] gitSkill \"${skill.repo}\" cloned to ${dir} but OpenCode has no ` +\n 'recognised skills directory to link it into. The skill is available at that ' +\n 'path — add an AGENTS.md reference to it manually if needed. Skipping.',\n )\n }\n}\n\n/**\n * `agentSkill` references a public skill by bare name. OpenCode has no command\n * to fetch a skill from a bare name, so we warn and skip.\n */\nfunction projectAgentSkills(projection: WorkspaceProjection): void {\n for (const skill of projection.skills) {\n if (skill.kind !== 'agent-skill') continue\n console.warn(\n `[opencode] agentSkill \"${skill.name}\" cannot be projected: OpenCode has no ` +\n 'command to install a public skill by bare name. Provide it as a gitSkill ' +\n 'instead. Skipping.',\n )\n }\n}\n\n/**\n * OpenCode has no plugin install command. We warn-and-skip each declared plugin\n * rather than fabricating a command.\n */\nfunction projectPlugins(projection: WorkspaceProjection): void {\n for (const name of projection.plugins) {\n console.warn(\n `[opencode] plugin \"${name}\" cannot be installed: OpenCode has no plugin ` +\n 'install command. Skipping.',\n )\n }\n}\n\n/**\n * Project a `WorkspaceProjection` into the OpenCode sandbox. Safe to call on\n * every `chatStream`. The secret-bearing `opencode.json` mcp section is\n * (re)written on every call, re-resolving secrets, so OpenCode always reads\n * current values and a snapshot can never serve a stale or rotated secret.\n * The safe, idempotent, non-secret operations (gitSkill, agentSkill, plugins\n * — all warn-and-skip for OpenCode) are guarded by a one-time marker so they\n * produce at most one warning per sandbox lifetime.\n *\n * @param handle - The sandbox handle (`fs` + `process`).\n * @param projection - The portable workspace inputs from `withSandbox`.\n */\nexport async function projectOpencodeWorkspace(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n): Promise<void> {\n // Always re-resolve and rewrite the secret-bearing MCP config so rotated\n // secrets re-apply and snapshots can't serve stale values.\n await projectMcpServers(handle, projection)\n\n // Gate only the safe, idempotent, non-secret operations on the marker.\n if (await handle.fs.exists(projection.markerPath)) return\n\n projectGitSkills(projection)\n projectAgentSkills(projection)\n projectPlugins(projection)\n\n await handle.fs.write(projection.markerPath, '')\n}\n"],"names":[],"mappings":";AA8CA,SAAS,eAAe,OAAoC;AAC1D,SACE,OAAO,UAAU,YACjB,UAAU,QACV,YAAa,MAAoC,WAAW;AAEhE;AAOA,SAAS,mBACP,OACA,eACQ;AACR,MAAI,YAAY,KAAK,EAAG,QAAO,cAAc,KAAK;AAClD,MAAI,eAAe,KAAK,EAAG,QAAO,UAAU,cAAc,MAAM,WAAW,CAAC;AAC5E,SAAO;AACT;AAgBA,SAAS,gBACP,QACA,eAC+C;AAC/C,QAAM,MAAyC,CAAA;AAC/C,MAAI,QAAQ;AACZ,aAAW,SAAS,QAAQ;AAC1B,QAAI,MAAM,SAAS,MAAO;AAC1B,aAAS;AACT,UAAM,UAAkC,CAAA;AACxC,UAAM,aAAa,MAAM,OAAO,WAAW,CAAA;AAC3C,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,UAAU,GAAG;AACtD,cAAQ,IAAI,IAAI,mBAAmB,OAAO,aAAa;AAAA,IACzD;AACA,UAAM,SAAS,MAAM,OAAO,KAAK;AACjC,UAAM,MAAM,OAAO,WAAW,WAAW,SAAS;AAClD,QAAI,MAAM,IAAI,IAAI,EAAE,MAAM,UAAU,KAAK,SAAS,MAAM,QAAA;AAAA,EAC1D;AACA,SAAO,QAAQ,IAAI,MAAM;AAC3B;AAWA,eAAe,kBACb,QACA,YACe;AACf,QAAM,aAAa;AAAA,IACjB,WAAW;AAAA,IACX,WAAW;AAAA,EAAA;AAEb,MAAI,eAAe,OAAW;AAE9B,QAAM,SAAS,GAAG,WAAW,IAAI;AAGjC,MAAI,WAAoC,CAAA;AACxC,MAAI,MAAM,OAAO,GAAG,OAAO,MAAM,GAAG;AAClC,QAAI;AACF,YAAM,MAAM,MAAM,OAAO,GAAG,KAAK,MAAM;AACvC,YAAM,SAAkB,KAAK,MAAM,GAAG;AACtC,UACE,OAAO,WAAW,YAClB,WAAW,QACX,CAAC,MAAM,QAAQ,MAAM,GACrB;AACA,mBAAW;AAAA,MACb;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,SAAS,EAAE,GAAG,UAAU,KAAK,WAAA;AACnC,QAAM,OAAO,GAAG,MAAM,QAAQ,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAC/D;AAOA,SAAS,iBAAiB,YAAuC;AAC/D,aAAW,SAAS,WAAW,QAAQ;AACrC,QAAI,MAAM,SAAS,MAAO;AAC1B,UAAM,MAAM,MAAM,QAAQ,mBAAmB,WAAW,MAAM,KAAK;AACnE,YAAQ;AAAA,MACN,wBAAwB,MAAM,IAAI,eAAe,GAAG;AAAA,IAAA;AAAA,EAIxD;AACF;AAMA,SAAS,mBAAmB,YAAuC;AACjE,aAAW,SAAS,WAAW,QAAQ;AACrC,QAAI,MAAM,SAAS,cAAe;AAClC,YAAQ;AAAA,MACN,0BAA0B,MAAM,IAAI;AAAA,IAAA;AAAA,EAIxC;AACF;AAMA,SAAS,eAAe,YAAuC;AAC7D,aAAW,QAAQ,WAAW,SAAS;AACrC,YAAQ;AAAA,MACN,sBAAsB,IAAI;AAAA,IAAA;AAAA,EAG9B;AACF;AAcA,eAAsB,yBACpB,QACA,YACe;AAGf,QAAM,kBAAkB,QAAQ,UAAU;AAG1C,MAAI,MAAM,OAAO,GAAG,OAAO,WAAW,UAAU,EAAG;AAEnD,mBAAiB,UAAU;AAC3B,qBAAmB,UAAU;AAC7B,iBAAe,UAAU;AAEzB,QAAM,OAAO,GAAG,MAAM,WAAW,YAAY,EAAE;AACjD;"}
|
|
1
|
+
{"version":3,"file":"projection.js","names":[],"sources":["../../../src/adapters/projection.ts"],"sourcesContent":["/**\n * OpenCode workspace projector — mirrors the Claude Code reference\n * implementation at `packages/ai-claude-code/src/adapters/projection.ts`.\n *\n * `withSandbox` surfaces a portable `WorkspaceProjection` (skills, plugins, a\n * secret resolver, and a one-time marker path) via a capability. This adapter\n * reads it in `chatStream` setup and projects workspace inputs into OpenCode's\n * native format:\n *\n * - MCP servers → `opencode.json` at the workspace root (mcp section).\n * Written on EVERY call (never marker-gated) so rotated\n * secrets always re-apply.\n * - gitSkill repos → OpenCode has no recognised skills directory; we\n * warn-and-skip rather than invent a path.\n * - agentSkill → no bare-name primitive in opencode; warn-and-skip.\n * - plugins → opencode has no `opencode plugin install` command;\n * warn-and-skip.\n *\n * The `opencode.json` mcp-section shape mirrors the `OPENCODE_CONFIG_CONTENT`\n * shape already used by the adapter's host-tool-bridge, ensuring both\n * mechanisms use the same server-entry schema:\n *\n * ```json\n * { \"mcp\": { \"<name>\": { \"type\": \"remote\", \"url\": \"...\", \"enabled\": true,\n * \"headers\": { ... } } } }\n * ```\n *\n * OpenCode merges project-scoped `opencode.json` with its environment config,\n * so the workspace file coexists safely with the runtime `OPENCODE_CONFIG_CONTENT`\n * env written for the host-tool-bridge.\n *\n * External-convention caveat: the `opencode.json` location and the `remote`\n * MCP-entry shape are derived from the adapter's existing `OPENCODE_CONFIG_CONTENT`\n * usage. Where OpenCode has no clean primitive (gitSkill dir, agentSkill,\n * plugins) we no-op with a warning instead of fabricating a command.\n */\nimport { isSecretRef, resolveGitSkillDir } from '@tanstack/ai-sandbox'\nimport type {\n BearerRef,\n SandboxHandle,\n SecretRef,\n WorkspaceProjection,\n WorkspaceSkill,\n} from '@tanstack/ai-sandbox'\n\n/** True when `value` is a `bearer(ref)` marker created by `@tanstack/ai-sandbox`. */\nfunction isBearerMarker(value: unknown): value is BearerRef {\n return (\n typeof value === 'object' &&\n value !== null &&\n isSecretRef((value as { __bearerRef?: unknown }).__bearerRef)\n )\n}\n\n/**\n * Resolve a single MCP header value: a `SecretRef` resolves to its plaintext,\n * a `bearer(ref)` marker resolves to `Bearer <plaintext>`, and a plain string\n * is passed through unchanged.\n */\nfunction resolveHeaderValue(\n value: string | SecretRef | BearerRef,\n resolveSecret: (ref: SecretRef) => string,\n): string {\n if (isSecretRef(value)) return resolveSecret(value)\n if (isBearerMarker(value)) return `Bearer ${resolveSecret(value.__bearerRef)}`\n return value\n}\n\n/** An OpenCode `remote`-type MCP server entry (mirrors OPENCODE_CONFIG_CONTENT shape). */\ninterface OpencodeMcpServer {\n type: 'remote'\n url: string\n enabled: boolean\n headers: Record<string, string>\n}\n\n/**\n * Build the `mcp` section of OpenCode's `opencode.json` from the\n * `{ kind: 'mcp' }` skills, resolving every header value (SecretRef / bearer\n * / string). Returns `undefined` when there are no MCP skills so the caller\n * can skip the write.\n */\nfunction buildMcpSection(\n skills: Array<WorkspaceSkill>,\n resolveSecret: (ref: SecretRef) => string,\n): Record<string, OpencodeMcpServer> | undefined {\n const mcp: Record<string, OpencodeMcpServer> = {}\n let count = 0\n for (const skill of skills) {\n if (skill.kind !== 'mcp') continue\n count += 1\n const headers: Record<string, string> = {}\n const rawHeaders = skill.config.headers ?? {}\n for (const [name, value] of Object.entries(rawHeaders)) {\n headers[name] = resolveHeaderValue(value, resolveSecret)\n }\n const rawUrl = skill.config['url']\n const url = typeof rawUrl === 'string' ? rawUrl : ''\n mcp[skill.name] = { type: 'remote', url, enabled: true, headers }\n }\n return count > 0 ? mcp : undefined\n}\n\n/**\n * Write (or merge-update) `opencode.json` at the workspace root with the\n * project-scoped MCP servers, re-resolving every secret. Runs on EVERY\n * projection call (never marker-gated) so rotated secrets always re-apply.\n *\n * If `opencode.json` already exists (e.g. committed to the repo) we read it,\n * merge only the `mcp` key, and rewrite, preserving other settings. When there\n * are no MCP skills the write is skipped entirely.\n */\nasync function projectMcpServers(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n): Promise<void> {\n const mcpSection = buildMcpSection(\n projection.skills,\n projection.resolveSecret,\n )\n if (mcpSection === undefined) return\n\n const target = `${projection.root}/opencode.json`\n\n // Read the existing file if present so we can preserve non-mcp settings.\n let existing: Record<string, unknown> = {}\n if (await handle.fs.exists(target)) {\n try {\n const raw = await handle.fs.read(target)\n const parsed: unknown = JSON.parse(raw)\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n !Array.isArray(parsed)\n ) {\n existing = parsed as Record<string, unknown>\n }\n } catch {\n // Unreadable or invalid JSON — start fresh so the MCP config lands cleanly.\n }\n }\n\n const merged = { ...existing, mcp: mcpSection }\n await handle.fs.write(target, JSON.stringify(merged, null, 2))\n}\n\n/**\n * OpenCode has no recognised skills directory where we can drop gitSkill repos.\n * AGENTS.md is already handled by the bootstrap layer (OpenCode reads it\n * natively). We warn-and-skip instead of inventing a path.\n */\nfunction projectGitSkills(projection: WorkspaceProjection): void {\n for (const skill of projection.skills) {\n if (skill.kind !== 'git') continue\n const dir = skill.into ?? resolveGitSkillDir(projection.root, skill)\n console.warn(\n `[opencode] gitSkill \"${skill.repo}\" cloned to ${dir} but OpenCode has no ` +\n 'recognised skills directory to link it into. The skill is available at that ' +\n 'path — add an AGENTS.md reference to it manually if needed. Skipping.',\n )\n }\n}\n\n/**\n * `agentSkill` references a public skill by bare name. OpenCode has no command\n * to fetch a skill from a bare name, so we warn and skip.\n */\nfunction projectAgentSkills(projection: WorkspaceProjection): void {\n for (const skill of projection.skills) {\n if (skill.kind !== 'agent-skill') continue\n console.warn(\n `[opencode] agentSkill \"${skill.name}\" cannot be projected: OpenCode has no ` +\n 'command to install a public skill by bare name. Provide it as a gitSkill ' +\n 'instead. Skipping.',\n )\n }\n}\n\n/**\n * OpenCode has no plugin install command. We warn-and-skip each declared plugin\n * rather than fabricating a command.\n */\nfunction projectPlugins(projection: WorkspaceProjection): void {\n for (const name of projection.plugins) {\n console.warn(\n `[opencode] plugin \"${name}\" cannot be installed: OpenCode has no plugin ` +\n 'install command. Skipping.',\n )\n }\n}\n\n/**\n * Project a `WorkspaceProjection` into the OpenCode sandbox. Safe to call on\n * every `chatStream`. The secret-bearing `opencode.json` mcp section is\n * (re)written on every call, re-resolving secrets, so OpenCode always reads\n * current values and a snapshot can never serve a stale or rotated secret.\n * The safe, idempotent, non-secret operations (gitSkill, agentSkill, plugins\n * — all warn-and-skip for OpenCode) are guarded by a one-time marker so they\n * produce at most one warning per sandbox lifetime.\n *\n * @param handle - The sandbox handle (`fs` + `process`).\n * @param projection - The portable workspace inputs from `withSandbox`.\n */\nexport async function projectOpencodeWorkspace(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n): Promise<void> {\n // Always re-resolve and rewrite the secret-bearing MCP config so rotated\n // secrets re-apply and snapshots can't serve stale values.\n await projectMcpServers(handle, projection)\n\n // Gate only the safe, idempotent, non-secret operations on the marker.\n if (await handle.fs.exists(projection.markerPath)) return\n\n projectGitSkills(projection)\n projectAgentSkills(projection)\n projectPlugins(projection)\n\n await handle.fs.write(projection.markerPath, '')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,SAAS,eAAe,OAAoC;CAC1D,OACE,OAAO,UAAU,YACjB,UAAU,QACV,YAAa,MAAoC,WAAW;AAEhE;;;;;;AAOA,SAAS,mBACP,OACA,eACQ;CACR,IAAI,YAAY,KAAK,GAAG,OAAO,cAAc,KAAK;CAClD,IAAI,eAAe,KAAK,GAAG,OAAO,UAAU,cAAc,MAAM,WAAW;CAC3E,OAAO;AACT;;;;;;;AAgBA,SAAS,gBACP,QACA,eAC+C;CAC/C,MAAM,MAAyC,CAAC;CAChD,IAAI,QAAQ;CACZ,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,MAAM,SAAS,OAAO;EAC1B,SAAS;EACT,MAAM,UAAkC,CAAC;EACzC,MAAM,aAAa,MAAM,OAAO,WAAW,CAAC;EAC5C,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,UAAU,GACnD,QAAQ,QAAQ,mBAAmB,OAAO,aAAa;EAEzD,MAAM,SAAS,MAAM,OAAO;EAC5B,MAAM,MAAM,OAAO,WAAW,WAAW,SAAS;EAClD,IAAI,MAAM,QAAQ;GAAE,MAAM;GAAU;GAAK,SAAS;GAAM;EAAQ;CAClE;CACA,OAAO,QAAQ,IAAI,MAAM,KAAA;AAC3B;;;;;;;;;;AAWA,eAAe,kBACb,QACA,YACe;CACf,MAAM,aAAa,gBACjB,WAAW,QACX,WAAW,aACb;CACA,IAAI,eAAe,KAAA,GAAW;CAE9B,MAAM,SAAS,GAAG,WAAW,KAAK;CAGlC,IAAI,WAAoC,CAAC;CACzC,IAAI,MAAM,OAAO,GAAG,OAAO,MAAM,GAC/B,IAAI;EACF,MAAM,MAAM,MAAM,OAAO,GAAG,KAAK,MAAM;EACvC,MAAM,SAAkB,KAAK,MAAM,GAAG;EACtC,IACE,OAAO,WAAW,YAClB,WAAW,QACX,CAAC,MAAM,QAAQ,MAAM,GAErB,WAAW;CAEf,QAAQ,CAER;CAGF,MAAM,SAAS;EAAE,GAAG;EAAU,KAAK;CAAW;CAC9C,MAAM,OAAO,GAAG,MAAM,QAAQ,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAC/D;;;;;;AAOA,SAAS,iBAAiB,YAAuC;CAC/D,KAAK,MAAM,SAAS,WAAW,QAAQ;EACrC,IAAI,MAAM,SAAS,OAAO;EAC1B,MAAM,MAAM,MAAM,QAAQ,mBAAmB,WAAW,MAAM,KAAK;EACnE,QAAQ,KACN,wBAAwB,MAAM,KAAK,cAAc,IAAI,uKAGvD;CACF;AACF;;;;;AAMA,SAAS,mBAAmB,YAAuC;CACjE,KAAK,MAAM,SAAS,WAAW,QAAQ;EACrC,IAAI,MAAM,SAAS,eAAe;EAClC,QAAQ,KACN,0BAA0B,MAAM,KAAK,mIAGvC;CACF;AACF;;;;;AAMA,SAAS,eAAe,YAAuC;CAC7D,KAAK,MAAM,QAAQ,WAAW,SAC5B,QAAQ,KACN,sBAAsB,KAAK,yEAE7B;AAEJ;;;;;;;;;;;;;AAcA,eAAsB,yBACpB,QACA,YACe;CAGf,MAAM,kBAAkB,QAAQ,UAAU;CAG1C,IAAI,MAAM,OAAO,GAAG,OAAO,WAAW,UAAU,GAAG;CAEnD,iBAAiB,UAAU;CAC3B,mBAAmB,UAAU;CAC7B,eAAe,UAAU;CAEzB,MAAM,OAAO,GAAG,MAAM,WAAW,YAAY,EAAE;AACjD"}
|