@tanstack/ai-claude-code 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/README.md +18 -0
- package/dist/esm/adapters/policy-map.d.ts +8 -0
- package/dist/esm/adapters/policy-map.js +51 -0
- package/dist/esm/adapters/policy-map.js.map +1 -0
- package/dist/esm/adapters/projection.d.ts +14 -0
- package/dist/esm/adapters/projection.js +101 -0
- package/dist/esm/adapters/projection.js.map +1 -0
- package/dist/esm/adapters/text.d.ts +75 -0
- package/dist/esm/adapters/text.js +306 -0
- package/dist/esm/adapters/text.js.map +1 -0
- package/dist/esm/index.d.ts +10 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/messages/prompt.d.ts +17 -0
- package/dist/esm/messages/prompt.js +37 -0
- package/dist/esm/messages/prompt.js.map +1 -0
- package/dist/esm/model-meta.d.ts +10 -0
- package/dist/esm/model-meta.js +14 -0
- package/dist/esm/model-meta.js.map +1 -0
- package/dist/esm/provider-options.d.ts +30 -0
- package/dist/esm/stream/sdk-types.d.ts +124 -0
- package/dist/esm/stream/translate.d.ts +44 -0
- package/dist/esm/stream/translate.js +368 -0
- package/dist/esm/stream/translate.js.map +1 -0
- package/package.json +62 -0
- package/src/adapters/policy-map.ts +83 -0
- package/src/adapters/projection.ts +226 -0
- package/src/adapters/text.ts +539 -0
- package/src/index.ts +21 -0
- package/src/messages/prompt.ts +68 -0
- package/src/model-meta.ts +21 -0
- package/src/provider-options.ts +30 -0
- package/src/stream/sdk-types.ts +135 -0
- package/src/stream/translate.ts +483 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code workspace projector — the reference implementation the other
|
|
3
|
+
* harness projectors (codex, opencode) mirror.
|
|
4
|
+
*
|
|
5
|
+
* `withSandbox` surfaces a portable `WorkspaceProjection` (skills, plugins, a
|
|
6
|
+
* secret resolver, and a one-time marker path) via a capability. Each harness
|
|
7
|
+
* adapter reads it in its `chatStream` setup and projects those inputs into the
|
|
8
|
+
* CLI's native format. For Claude Code that means:
|
|
9
|
+
*
|
|
10
|
+
* - MCP servers → a project-scoped `.mcp.json` at the workspace root.
|
|
11
|
+
* - gitSkill repos → linked under `.claude/skills/<basename>`.
|
|
12
|
+
* - agentSkill → no reliable claude primitive pulls a public skill by
|
|
13
|
+
* bare name, so we warn and skip rather than invent one.
|
|
14
|
+
* - plugins → `claude plugin install <name>` (best-effort).
|
|
15
|
+
*
|
|
16
|
+
* The secret-bearing `.mcp.json` is (re)written on EVERY call, re-resolving
|
|
17
|
+
* secrets each time, so claude always reads current values and a snapshot can
|
|
18
|
+
* never serve a stale or rotated secret. Only the safe, idempotent, non-secret
|
|
19
|
+
* operations (gitSkill links, plugin installs, agentSkill handling) are guarded
|
|
20
|
+
* by a one-time marker file under the workspace.
|
|
21
|
+
*
|
|
22
|
+
* External-convention caveat: the `.mcp.json` location/shape, the skills dir,
|
|
23
|
+
* and the plugin-install command are verified against the installed `claude`
|
|
24
|
+
* CLI. Where claude has no clean primitive (agentSkill by bare name) we no-op
|
|
25
|
+
* with a warning instead of fabricating a command.
|
|
26
|
+
*/
|
|
27
|
+
import { isSecretRef, resolveGitSkillDir } from '@tanstack/ai-sandbox'
|
|
28
|
+
import type {
|
|
29
|
+
BearerRef,
|
|
30
|
+
SandboxHandle,
|
|
31
|
+
SecretRef,
|
|
32
|
+
WorkspaceProjection,
|
|
33
|
+
WorkspaceSkill,
|
|
34
|
+
} from '@tanstack/ai-sandbox'
|
|
35
|
+
|
|
36
|
+
/** POSIX single-quote escape for embedding a value in a shell command. */
|
|
37
|
+
function shellQuote(value: string): string {
|
|
38
|
+
return `'${value.replace(/'/g, `'\\''`)}'`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Last path segment of a `gitSkill` clone dir, used as the skills-dir name. */
|
|
42
|
+
function basenameOf(path: string): string {
|
|
43
|
+
const segments = path.split('/').filter((segment) => segment !== '')
|
|
44
|
+
return segments[segments.length - 1] ?? path
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True when `value` is a `bearer(ref)` marker created by `@tanstack/ai-sandbox`. */
|
|
48
|
+
function isBearerMarker(value: unknown): value is BearerRef {
|
|
49
|
+
return (
|
|
50
|
+
typeof value === 'object' &&
|
|
51
|
+
value !== null &&
|
|
52
|
+
isSecretRef((value as { __bearerRef?: unknown }).__bearerRef)
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a single MCP header value: a `SecretRef` resolves to its plaintext, a
|
|
58
|
+
* `bearer(ref)` marker resolves to `Bearer <plaintext>`, and a plain string is
|
|
59
|
+
* passed through unchanged.
|
|
60
|
+
*/
|
|
61
|
+
function resolveHeaderValue(
|
|
62
|
+
value: string | SecretRef | BearerRef,
|
|
63
|
+
resolveSecret: (ref: SecretRef) => string,
|
|
64
|
+
): string {
|
|
65
|
+
if (isSecretRef(value)) return resolveSecret(value)
|
|
66
|
+
if (isBearerMarker(value)) return `Bearer ${resolveSecret(value.__bearerRef)}`
|
|
67
|
+
return value
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A claude-format HTTP MCP server entry. */
|
|
71
|
+
interface ClaudeMcpServer {
|
|
72
|
+
type: 'http'
|
|
73
|
+
url: string
|
|
74
|
+
headers: Record<string, string>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build claude's project-scoped MCP config from the `{ kind: 'mcp' }` skills,
|
|
79
|
+
* resolving every header value (SecretRef / bearer / string). Returns
|
|
80
|
+
* `undefined` when there are no MCP skills so the caller can skip the write.
|
|
81
|
+
*/
|
|
82
|
+
function buildMcpConfig(
|
|
83
|
+
skills: Array<WorkspaceSkill>,
|
|
84
|
+
resolveSecret: (ref: SecretRef) => string,
|
|
85
|
+
): { mcpServers: Record<string, ClaudeMcpServer> } | undefined {
|
|
86
|
+
const mcpServers: Record<string, ClaudeMcpServer> = {}
|
|
87
|
+
let count = 0
|
|
88
|
+
for (const skill of skills) {
|
|
89
|
+
if (skill.kind !== 'mcp') continue
|
|
90
|
+
count += 1
|
|
91
|
+
const headers: Record<string, string> = {}
|
|
92
|
+
const rawHeaders = skill.config.headers ?? {}
|
|
93
|
+
for (const [name, value] of Object.entries(rawHeaders)) {
|
|
94
|
+
headers[name] = resolveHeaderValue(value, resolveSecret)
|
|
95
|
+
}
|
|
96
|
+
const rawUrl = skill.config['url']
|
|
97
|
+
const url = typeof rawUrl === 'string' ? rawUrl : ''
|
|
98
|
+
mcpServers[skill.name] = { type: 'http', url, headers }
|
|
99
|
+
}
|
|
100
|
+
return count > 0 ? { mcpServers } : undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Write the project-scoped `.mcp.json`, re-resolving every secret. This runs on
|
|
105
|
+
* EVERY projection call (never gated by the marker) so claude always reads the
|
|
106
|
+
* current secret values and a snapshot can never serve a stale or rotated one.
|
|
107
|
+
* With `snapshot:'after-run'` the file may still be captured in the image, so
|
|
108
|
+
* secret-bearing MCP material is best used with the default `after-setup`
|
|
109
|
+
* strategy. When there are no MCP skills the write is skipped.
|
|
110
|
+
*/
|
|
111
|
+
async function projectMcpServers(
|
|
112
|
+
handle: SandboxHandle,
|
|
113
|
+
projection: WorkspaceProjection,
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
const config = buildMcpConfig(projection.skills, projection.resolveSecret)
|
|
116
|
+
if (config === undefined) return
|
|
117
|
+
const target = `${projection.root}/.mcp.json`
|
|
118
|
+
await handle.fs.write(target, JSON.stringify(config, null, 2))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Ensure each cloned `gitSkill` repo is available under claude's project skills
|
|
123
|
+
* dir (`<root>/.claude/skills/<basename>`) via a symlink, falling back to a
|
|
124
|
+
* recursive copy on platforms without `ln -s`.
|
|
125
|
+
*/
|
|
126
|
+
async function projectGitSkills(
|
|
127
|
+
handle: SandboxHandle,
|
|
128
|
+
projection: WorkspaceProjection,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
const skillsDir = `${projection.root}/.claude/skills`
|
|
131
|
+
let madeDir = false
|
|
132
|
+
for (const skill of projection.skills) {
|
|
133
|
+
if (skill.kind !== 'git') continue
|
|
134
|
+
if (!madeDir) {
|
|
135
|
+
await handle.fs.mkdir(skillsDir)
|
|
136
|
+
madeDir = true
|
|
137
|
+
}
|
|
138
|
+
const source = skill.into ?? resolveGitSkillDir(projection.root, skill)
|
|
139
|
+
const target = `${skillsDir}/${basenameOf(source)}`
|
|
140
|
+
const lnCmd = `ln -s ${shellQuote(source)} ${shellQuote(target)}`
|
|
141
|
+
const result = await handle.process.exec(lnCmd, { cwd: projection.root })
|
|
142
|
+
if (result.exitCode !== 0) {
|
|
143
|
+
const cpCmd = `cp -r ${shellQuote(source)} ${shellQuote(target)}`
|
|
144
|
+
const copied = await handle.process.exec(cpCmd, { cwd: projection.root })
|
|
145
|
+
if (copied.exitCode !== 0) {
|
|
146
|
+
console.warn(
|
|
147
|
+
`[claude-code] failed to link gitSkill "${skill.repo}" into ${target}: ${copied.stderr.trim()}`,
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `agentSkill` references a public skill by bare name. Claude Code has no
|
|
156
|
+
* primitive to fetch a skill from a bare name (skills resolve from local
|
|
157
|
+
* `.claude/skills/` dirs and plugin marketplaces), so we warn and skip rather
|
|
158
|
+
* than fabricate a command.
|
|
159
|
+
*/
|
|
160
|
+
function projectAgentSkills(projection: WorkspaceProjection): void {
|
|
161
|
+
for (const skill of projection.skills) {
|
|
162
|
+
if (skill.kind !== 'agent-skill') continue
|
|
163
|
+
console.warn(
|
|
164
|
+
`[claude-code] agentSkill "${skill.name}" cannot be projected: Claude Code has ` +
|
|
165
|
+
'no command to install a public skill by bare name. Provide it as a gitSkill ' +
|
|
166
|
+
'or a plugin instead. Skipping.',
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Install each declared plugin via `claude plugin install <name>`. Plugin
|
|
173
|
+
* installs are best-effort: a failure (no marketplace, network, …) warns but
|
|
174
|
+
* never throws, so a missing plugin can't break the run.
|
|
175
|
+
*/
|
|
176
|
+
async function projectPlugins(
|
|
177
|
+
handle: SandboxHandle,
|
|
178
|
+
projection: WorkspaceProjection,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
for (const name of projection.plugins) {
|
|
181
|
+
const cmd = `claude plugin install ${shellQuote(name)}`
|
|
182
|
+
try {
|
|
183
|
+
const result = await handle.process.exec(cmd, { cwd: projection.root })
|
|
184
|
+
if (result.exitCode !== 0) {
|
|
185
|
+
console.warn(
|
|
186
|
+
`[claude-code] "claude plugin install ${name}" exited ${result.exitCode}: ${result.stderr.trim()}`,
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
} catch (error: unknown) {
|
|
190
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
191
|
+
console.warn(
|
|
192
|
+
`[claude-code] failed to install plugin "${name}": ${message}`,
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Project a `WorkspaceProjection` into the Claude Code sandbox. Safe to call on
|
|
200
|
+
* every `chatStream`. The secret-bearing `.mcp.json` is (re)written on every
|
|
201
|
+
* call, re-resolving secrets, so claude always reads current values and a
|
|
202
|
+
* snapshot can never serve a stale or rotated secret. The safe, idempotent,
|
|
203
|
+
* non-secret operations (gitSkill links, plugin installs, agentSkill handling)
|
|
204
|
+
* are guarded by a one-time marker so they run only on the first call after
|
|
205
|
+
* create/restore.
|
|
206
|
+
*
|
|
207
|
+
* @param handle - The sandbox handle (`fs` + `process`).
|
|
208
|
+
* @param projection - The portable workspace inputs from `withSandbox`.
|
|
209
|
+
*/
|
|
210
|
+
export async function projectClaudeWorkspace(
|
|
211
|
+
handle: SandboxHandle,
|
|
212
|
+
projection: WorkspaceProjection,
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
// Always re-resolve and rewrite the secret-bearing MCP config so rotated
|
|
215
|
+
// secrets re-apply and snapshots can't serve stale values.
|
|
216
|
+
await projectMcpServers(handle, projection)
|
|
217
|
+
|
|
218
|
+
// Gate only the safe, idempotent, non-secret operations on the marker.
|
|
219
|
+
if (await handle.fs.exists(projection.markerPath)) return
|
|
220
|
+
|
|
221
|
+
await projectGitSkills(handle, projection)
|
|
222
|
+
projectAgentSkills(projection)
|
|
223
|
+
await projectPlugins(handle, projection)
|
|
224
|
+
|
|
225
|
+
await handle.fs.write(projection.markerPath, '')
|
|
226
|
+
}
|