@tangle-network/agent-profile-materialize 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,11 @@
1
+ Copyright (c) 2025 Tangle Network
2
+
3
+ All rights reserved.
4
+
5
+ This software and associated documentation files (the "Software") are proprietary
6
+ and confidential. No part of this Software may be reproduced, distributed, or
7
+ transmitted in any form or by any means, including photocopying, recording, or
8
+ other electronic or mechanical methods, without the prior written permission of
9
+ Tangle Network.
10
+
11
+ For licensing inquiries, contact: hello@tangle.tools
@@ -0,0 +1,69 @@
1
+ /**
2
+ * materializeProfile — THE ONE per-harness profile materializer, shared by every
3
+ * layer (cli-bridge host spawn, agent-dev-container box adapter, VB). It turns a
4
+ * structured {@link AgentProfile} into a {@link WorkspacePlan}: the exact native
5
+ * files to write into the run workspace (cwd), plus the env/flags some harnesses
6
+ * need because they have NO cwd auto-discovery for certain dimensions.
7
+ *
8
+ * Routed per the VERIFIED capability matrix (harness-capability-matrix, 2026-06-14)
9
+ * across every dimension (context, skills, mcp, hooks, subagents, commands).
10
+ *
11
+ * FAIL-CLOSED, not silent-drop: when a requested dimension has no path on the
12
+ * chosen harness (e.g. kimi MCP is flag-only, hermes hooks are opt-in global), it
13
+ * is recorded in `plan.unsupported` with the reason — never silently ignored,
14
+ * which is exactly the "looks provisioned, loads nothing" trap.
15
+ *
16
+ * The ONLY trustworthy proof a dimension works is the in-session effect (skill
17
+ * secret word, MCP tool called n>0, hook sentinel file) — see the canary tests.
18
+ *
19
+ * Types come from `@tangle-network/agent-interface` (the canonical AgentProfile
20
+ * contract); this package adds only the harness-specific materialization + the
21
+ * `node:fs` IO consumer. It was previously copied byte-for-byte into both
22
+ * cli-bridge and agent-dev-container — this is the single source those re-export.
23
+ */
24
+ import type { AgentProfile } from '@tangle-network/agent-interface';
25
+ export type HarnessId = 'claude-code' | 'claude' | 'claudish' | 'nanoclaw' | 'codex' | 'opencode' | 'kimi-code' | 'kimi' | 'pi' | 'gemini' | 'hermes' | 'openclaw';
26
+ export interface PlanFile {
27
+ relPath: string;
28
+ content: string;
29
+ /** chmod mode (e.g. 0o755 for hook scripts); default 0o644. */
30
+ mode?: number;
31
+ }
32
+ export interface Unsupported {
33
+ dimension: 'skills' | 'mcp' | 'hooks' | 'subagents' | 'commands' | 'instructions' | 'systemPrompt' | 'permissions' | 'tools';
34
+ reason: string;
35
+ }
36
+ /** The plan a caller writes into the run cwd, then applies to the spawn. */
37
+ export interface WorkspacePlan {
38
+ harness: HarnessId;
39
+ /** Files to write relative to the run workspace (cwd). */
40
+ files: PlanFile[];
41
+ /** Env vars to set on the spawned process (for harnesses needing them). */
42
+ env: Record<string, string>;
43
+ /** Extra CLI flags to pass (e.g. kimi --mcp-config-file <path>). */
44
+ flags: string[];
45
+ /** Dimensions requested but with NO cwd path on this harness — fail-closed record. */
46
+ unsupported: Unsupported[];
47
+ }
48
+ /** Normalize any skill markdown (VB frontmatter `skill:`/`description:`, or raw) into a
49
+ * harness SKILL.md with `name:`/`description:` frontmatter the harnesses require. */
50
+ export declare function normalizeSkillMd(name: string, raw: string): string;
51
+ /**
52
+ * Build the WorkspacePlan for `profile` on `harness`. Pure — no IO. The caller
53
+ * writes plan.files into the run cwd and applies plan.env/plan.flags to the spawn.
54
+ */
55
+ export declare function materializeProfile(profile: AgentProfile, harness: HarnessId, opts?: {
56
+ skip?: Unsupported['dimension'][];
57
+ }): WorkspacePlan;
58
+ /**
59
+ * Apply a WorkspacePlan to a real workspace dir: write every file (creating parent
60
+ * dirs), and return the env + flags the caller must hand the spawn. This is the IO
61
+ * consumer of the pure `materializeProfile` — used by the cli-bridge host spawn, the
62
+ * agent-dev-container box, and VB. Returns `unsupported` so the caller can log/fail.
63
+ */
64
+ export declare function applyWorkspacePlan(plan: WorkspacePlan, workspaceDir: string): {
65
+ env: Record<string, string>;
66
+ flags: string[];
67
+ unsupported: Unsupported[];
68
+ written: string[];
69
+ };
package/dist/index.js ADDED
@@ -0,0 +1,381 @@
1
+ /**
2
+ * materializeProfile — THE ONE per-harness profile materializer, shared by every
3
+ * layer (cli-bridge host spawn, agent-dev-container box adapter, VB). It turns a
4
+ * structured {@link AgentProfile} into a {@link WorkspacePlan}: the exact native
5
+ * files to write into the run workspace (cwd), plus the env/flags some harnesses
6
+ * need because they have NO cwd auto-discovery for certain dimensions.
7
+ *
8
+ * Routed per the VERIFIED capability matrix (harness-capability-matrix, 2026-06-14)
9
+ * across every dimension (context, skills, mcp, hooks, subagents, commands).
10
+ *
11
+ * FAIL-CLOSED, not silent-drop: when a requested dimension has no path on the
12
+ * chosen harness (e.g. kimi MCP is flag-only, hermes hooks are opt-in global), it
13
+ * is recorded in `plan.unsupported` with the reason — never silently ignored,
14
+ * which is exactly the "looks provisioned, loads nothing" trap.
15
+ *
16
+ * The ONLY trustworthy proof a dimension works is the in-session effect (skill
17
+ * secret word, MCP tool called n>0, hook sentinel file) — see the canary tests.
18
+ *
19
+ * Types come from `@tangle-network/agent-interface` (the canonical AgentProfile
20
+ * contract); this package adds only the harness-specific materialization + the
21
+ * `node:fs` IO consumer. It was previously copied byte-for-byte into both
22
+ * cli-bridge and agent-dev-container — this is the single source those re-export.
23
+ */
24
+ import { mkdirSync, writeFileSync } from 'node:fs';
25
+ import { dirname, join } from 'node:path';
26
+ /** Resolve an inline resource ref to {name, content}. github refs need an async fetch
27
+ * (the providers' resolveProfileResource does this); the sync materializer can't, so a
28
+ * github ref is reported unsupported and the caller pre-resolves it. A ref with
29
+ * `content` (or kind 'inline') is inline. */
30
+ function resolveInlineRef(ref) {
31
+ const r = ref;
32
+ if (r.kind === 'inline' || typeof r.content === 'string') {
33
+ return { name: String(r.name ?? r.path ?? 'resource'), content: String(r.content ?? '') };
34
+ }
35
+ return null; // github / unresolved
36
+ }
37
+ // ─── Verified per-harness conventions (capability matrix) ───────────────────
38
+ /** Project context file each harness actually reads from cwd. NEVER a constant. */
39
+ const CONTEXT_FILE = {
40
+ 'claude-code': 'CLAUDE.md', claude: 'CLAUDE.md', claudish: 'CLAUDE.md', nanoclaw: 'CLAUDE.md',
41
+ codex: 'AGENTS.md', opencode: 'AGENTS.md', 'kimi-code': 'AGENTS.md', kimi: 'AGENTS.md', pi: 'AGENTS.md',
42
+ gemini: 'GEMINI.md', // current default; AGENTS.md only via .gemini/settings.json context.fileName
43
+ hermes: 'HERMES.md', // highest-priority native name; only ONE context loads (first match)
44
+ openclaw: 'AGENTS.md',
45
+ };
46
+ /** cwd-relative native skill dir, or null if the harness has no cwd skill primitive. */
47
+ const SKILL_DIR = {
48
+ 'claude-code': '.claude/skills', claude: '.claude/skills', claudish: '.claude/skills', nanoclaw: '.claude/skills',
49
+ codex: '.codex/skills', opencode: '.opencode/skills', 'kimi-code': '.kimi/skills', kimi: '.kimi/skills',
50
+ pi: '.pi/skills', gemini: '.gemini/skills',
51
+ hermes: null, // skills are ~/.hermes/skills (user) only — no cwd discovery
52
+ openclaw: 'skills', // <workspace>/skills/<name>/SKILL.md
53
+ };
54
+ function canonicalHarness(h) {
55
+ if (h === 'claude' || h === 'claudish')
56
+ return 'claude-code';
57
+ if (h === 'kimi')
58
+ return 'kimi-code';
59
+ return h;
60
+ }
61
+ // ─── Helpers ────────────────────────────────────────────────────────────────
62
+ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
63
+ const collapse = (s) => s.replace(/\s+/g, ' ').trim();
64
+ /** Normalize any skill markdown (VB frontmatter `skill:`/`description:`, or raw) into a
65
+ * harness SKILL.md with `name:`/`description:` frontmatter the harnesses require. */
66
+ export function normalizeSkillMd(name, raw) {
67
+ const m = raw.match(FRONTMATTER_RE);
68
+ const fm = m?.[1] ?? '';
69
+ const body = (m?.[2] ?? raw).trim();
70
+ let desc = `Skill ${name}. Use when the task matches its domain.`;
71
+ if (fm) {
72
+ const lines = fm.split('\n');
73
+ const i = lines.findIndex((l) => /^description:/.test(l));
74
+ if (i >= 0) {
75
+ const inline = (lines[i] ?? '').replace(/^description:\s*/, '').trim();
76
+ if (inline && !['>', '|', '>-', '|-'].includes(inline))
77
+ desc = collapse(inline);
78
+ else {
79
+ const buf = [];
80
+ for (let j = i + 1; j < lines.length; j++) {
81
+ const ln = lines[j] ?? '';
82
+ if (/^\s+\S/.test(ln))
83
+ buf.push(ln.trim());
84
+ else if (ln.trim() === '')
85
+ continue;
86
+ else
87
+ break;
88
+ }
89
+ if (buf.length)
90
+ desc = collapse(buf.join(' '));
91
+ }
92
+ }
93
+ }
94
+ return `---\nname: ${name}\ndescription: ${JSON.stringify(desc)}\n---\n\n${body}\n`;
95
+ }
96
+ const tomlStr = (s) => JSON.stringify(s); // valid TOML basic string
97
+ // ─── The materializer ─────────────────────────────────────────────────────────
98
+ /**
99
+ * Build the WorkspacePlan for `profile` on `harness`. Pure — no IO. The caller
100
+ * writes plan.files into the run cwd and applies plan.env/plan.flags to the spawn.
101
+ */
102
+ export function materializeProfile(profile, harness, opts = {}) {
103
+ const h = canonicalHarness(harness);
104
+ const skip = new Set(opts.skip ?? []);
105
+ const plan = { harness: h, files: [], env: {}, flags: [], unsupported: [] };
106
+ const add = (relPath, content, mode) => plan.files.push({ relPath, content, ...(mode ? { mode } : {}) });
107
+ const unsupported = (dimension, reason) => plan.unsupported.push({ dimension, reason });
108
+ // 1) CONTEXT (instructions + systemPrompt → the harness's native context file).
109
+ const instr = [];
110
+ if (profile.prompt?.systemPrompt)
111
+ instr.push(profile.prompt.systemPrompt);
112
+ if (profile.prompt?.instructions?.length)
113
+ instr.push(...profile.prompt.instructions);
114
+ const resInstr = profile.resources?.instructions;
115
+ if (typeof resInstr === 'string')
116
+ instr.push(resInstr);
117
+ else if (resInstr && resInstr.kind === 'inline')
118
+ instr.push(resInstr.content);
119
+ if (instr.length) {
120
+ add(CONTEXT_FILE[h], `${instr.join('\n\n')}\n`);
121
+ // gemini reads AGENTS.md only via settings; we write GEMINI.md so no extra step.
122
+ }
123
+ // 2) SKILLS → native skill dir, from resources.skills refs (the box's shape), or
124
+ // fail-closed (hermes has no cwd skill dir; github refs need async pre-resolution).
125
+ for (const ref of profile.resources?.skills ?? []) {
126
+ const r = resolveInlineRef(ref);
127
+ if (!r) {
128
+ unsupported('skills', 'skill ref is not inline (github refs need async pre-resolution)');
129
+ continue;
130
+ }
131
+ const dir = SKILL_DIR[h];
132
+ if (!dir) {
133
+ unsupported('skills', `${h}: no cwd skill dir (skills live in a user/global dir); skill "${r.name}" not mounted`);
134
+ continue;
135
+ }
136
+ add(`${dir}/${r.name}/SKILL.md`, normalizeSkillMd(r.name, r.content));
137
+ }
138
+ // 3) MCP → per-harness format (the divergence the matrix names). Skippable so the
139
+ // additive host-wiring can leave MCP on cli-bridge's existing per-harness path.
140
+ const mcp = profile.mcp ?? {};
141
+ if (!skip.has('mcp') && Object.keys(mcp).length)
142
+ materializeMcp(h, mcp, plan, add, unsupported);
143
+ // 4) HOOKS → per-harness format; cwd-native only for claude/codex/gemini/opencode(plugin).
144
+ const hooks = profile.hooks ?? {};
145
+ if (Object.keys(hooks).length)
146
+ materializeHooks(h, hooks, add, unsupported, plan);
147
+ // 5) SUBAGENTS → cwd-native for claude/codex/opencode/gemini; flag/global elsewhere.
148
+ const subagents = profile.subagents ?? {};
149
+ for (const [name, sa] of Object.entries(subagents))
150
+ materializeSubagent(h, name, sa, add, unsupported);
151
+ // 6) COMMANDS → cwd-native (resources.commands refs); "commands are skills" on kimi.
152
+ for (const ref of profile.resources?.commands ?? []) {
153
+ const r = resolveInlineRef(ref);
154
+ if (!r) {
155
+ unsupported('commands', 'command ref is not inline (github needs pre-resolution)');
156
+ continue;
157
+ }
158
+ materializeCommand(h, r.name, r.content, add, unsupported);
159
+ }
160
+ // Merge multi-writer JSON files (e.g. .claude/settings.json from both mcp and hooks)
161
+ // so the second write can't clobber the first.
162
+ plan.files = mergeJsonFiles(plan.files);
163
+ return plan;
164
+ }
165
+ /** Collapse multiple PlanFiles with the same relPath: top-level-merge JSON, last-wins
166
+ * for non-JSON. settings.json contributors use disjoint top-level keys, so this unions them. */
167
+ function mergeJsonFiles(files) {
168
+ const byPath = new Map();
169
+ for (const f of files) {
170
+ const prev = byPath.get(f.relPath);
171
+ if (!prev) {
172
+ byPath.set(f.relPath, f);
173
+ continue;
174
+ }
175
+ if (f.relPath.endsWith('.json')) {
176
+ try {
177
+ const merged = { ...JSON.parse(prev.content), ...JSON.parse(f.content) };
178
+ byPath.set(f.relPath, { ...prev, content: JSON.stringify(merged, null, 2) });
179
+ continue;
180
+ }
181
+ catch { /* fall through to last-wins */ }
182
+ }
183
+ byPath.set(f.relPath, f); // non-JSON or parse-fail: last wins
184
+ }
185
+ return [...byPath.values()];
186
+ }
187
+ /**
188
+ * Apply a WorkspacePlan to a real workspace dir: write every file (creating parent
189
+ * dirs), and return the env + flags the caller must hand the spawn. This is the IO
190
+ * consumer of the pure `materializeProfile` — used by the cli-bridge host spawn, the
191
+ * agent-dev-container box, and VB. Returns `unsupported` so the caller can log/fail.
192
+ */
193
+ export function applyWorkspacePlan(plan, workspaceDir) {
194
+ const written = [];
195
+ for (const f of plan.files) {
196
+ const abs = join(workspaceDir, f.relPath);
197
+ mkdirSync(dirname(abs), { recursive: true });
198
+ writeFileSync(abs, f.content, { mode: f.mode ?? 0o644 });
199
+ written.push(f.relPath);
200
+ }
201
+ return { env: plan.env, flags: plan.flags, unsupported: plan.unsupported, written };
202
+ }
203
+ // ─── Per-dimension routing (verified matrix) ────────────────────────────────
204
+ function materializeMcp(h, mcp, plan, add, unsupported) {
205
+ switch (h) {
206
+ case 'claude-code':
207
+ case 'nanoclaw':
208
+ // claude reads ./.mcp.json but each server needs per-project APPROVAL; we also
209
+ // enable them via .claude/settings.json enabledMcpjsonServers to skip the prompt.
210
+ add('.mcp.json', JSON.stringify({ mcpServers: mcp }, null, 2));
211
+ add('.claude/settings.json', JSON.stringify({ enabledMcpjsonServers: Object.keys(mcp) }, null, 2));
212
+ if (h === 'nanoclaw')
213
+ unsupported('mcp', 'nanoclaw MCP is SDK-injected (options.mcpServers); .mcp.json may be ignored — inject via the SDK adapter');
214
+ break;
215
+ case 'codex': {
216
+ // codex reads [mcp_servers.<id>] in .codex/config.toml; trusted projects only.
217
+ const lines = ['[projects."."]', 'trust_level = "trusted"', ''];
218
+ for (const [name, s] of Object.entries(mcp)) {
219
+ lines.push(`[mcp_servers.${name}]`);
220
+ if (s.command)
221
+ lines.push(`command = ${tomlStr(s.command)}`);
222
+ if (s.args?.length)
223
+ lines.push(`args = [${s.args.map(tomlStr).join(', ')}]`);
224
+ if (s.url)
225
+ lines.push(`url = ${tomlStr(s.url)}`);
226
+ if (s.env)
227
+ lines.push(`env = { ${Object.entries(s.env).map(([k, v]) => `${k} = ${tomlStr(v)}`).join(', ')} }`);
228
+ lines.push('');
229
+ }
230
+ add('.codex/config.toml', lines.join('\n'));
231
+ break;
232
+ }
233
+ case 'opencode':
234
+ // opencode.json "mcp" block (local|remote).
235
+ add('opencode.json', JSON.stringify({ $schema: 'https://opencode.ai/config.json', mcp: opencodeMcp(mcp) }, null, 2));
236
+ break;
237
+ case 'gemini':
238
+ add('.gemini/settings.json', JSON.stringify({ mcpServers: mcp }, null, 2));
239
+ break;
240
+ case 'kimi-code': {
241
+ // NO cwd auto-discovery — kimi needs an explicit --mcp-config-file FLAG.
242
+ const rel = '.kimi/mcp.json';
243
+ add(rel, JSON.stringify({ mcpServers: mcp }, null, 2));
244
+ plan.flags.push('--mcp-config-file', rel);
245
+ break;
246
+ }
247
+ case 'pi':
248
+ unsupported('mcp', 'pi MCP is wired via a TS extension (.pi/extensions/*.ts), not a declarative cwd file — build an extension');
249
+ break;
250
+ case 'hermes':
251
+ unsupported('mcp', 'hermes MCP is global config.yaml (mcp_servers), no cwd discovery — write to the hermes global config or pass a config flag');
252
+ break;
253
+ case 'openclaw':
254
+ unsupported('mcp', 'openclaw MCP is central ~/.openclaw/openclaw.json (mcp.servers), no cwd discovery');
255
+ break;
256
+ }
257
+ }
258
+ function opencodeMcp(mcp) {
259
+ const out = {};
260
+ for (const [name, s] of Object.entries(mcp)) {
261
+ out[name] = s.url
262
+ ? { type: 'remote', url: s.url, enabled: s.enabled !== false, ...(s.headers ? { headers: s.headers } : {}) }
263
+ : { type: 'local', command: [s.command, ...(s.args ?? [])].filter(Boolean), enabled: s.enabled !== false, ...(s.env ? { environment: s.env } : {}) };
264
+ }
265
+ return out;
266
+ }
267
+ function materializeHooks(h, hooks, add, unsupported, plan) {
268
+ switch (h) {
269
+ case 'claude-code':
270
+ case 'nanoclaw': {
271
+ // .claude/settings.json hooks{} — each event → [{matcher, hooks:[{type:command,command}]}].
272
+ const settingsHooks = {};
273
+ for (const [event, cmds] of Object.entries(hooks)) {
274
+ settingsHooks[event] = cmds.map((c) => ({ matcher: c.matcher ?? '*', hooks: [{ type: 'command', command: c.command, ...(c.timeoutMs ? { timeout: Math.round(c.timeoutMs / 1000) } : {}) }] }));
275
+ }
276
+ add('.claude/settings.json', JSON.stringify({ hooks: settingsHooks }, null, 2));
277
+ break;
278
+ }
279
+ case 'gemini': {
280
+ const settingsHooks = {};
281
+ for (const [event, cmds] of Object.entries(hooks))
282
+ settingsHooks[event] = cmds.map((c) => ({ matcher: c.matcher ?? '*', hooks: [{ type: 'command', command: c.command }] }));
283
+ add('.gemini/settings.json', JSON.stringify({ hooks: settingsHooks }, null, 2));
284
+ break;
285
+ }
286
+ case 'codex': {
287
+ // codex hooks.json (event → commands).
288
+ add('.codex/hooks.json', JSON.stringify({ hooks }, null, 2));
289
+ break;
290
+ }
291
+ case 'opencode':
292
+ // opencode hooks are PLUGINS (JS/TS), not a settings block — emit a plugin stub.
293
+ for (const [event, cmds] of Object.entries(hooks)) {
294
+ const body = cmds.map((c) => ` // ${event}: ${c.command}`).join('\n');
295
+ add(`.opencode/plugin/vb-hooks-${event}.js`, `// auto-generated hook plugin for ${event}\nexport default async function () {\n${body}\n}\n`);
296
+ }
297
+ unsupported('hooks', 'opencode hooks are JS/TS plugins — generated a stub; a real handler must run the command (format differs from settings.json)');
298
+ break;
299
+ case 'kimi-code':
300
+ unsupported('hooks', 'kimi hooks are [[hooks]] in the config TOML loaded via --config-file, not cwd — pass --config-file');
301
+ break;
302
+ case 'pi':
303
+ unsupported('hooks', 'pi hooks are TS extensions (.pi/extensions/*.ts) — build an extension');
304
+ break;
305
+ case 'hermes': {
306
+ // opt-in project plugins; requires HERMES_ENABLE_PROJECT_PLUGINS=1.
307
+ for (const [event, cmds] of Object.entries(hooks))
308
+ add(`.hermes/plugins/vb-${event}/plugin.md`, cmds.map((c) => c.command).join('\n'));
309
+ plan.env.HERMES_ENABLE_PROJECT_PLUGINS = '1';
310
+ break;
311
+ }
312
+ case 'openclaw':
313
+ unsupported('hooks', 'openclaw hooks are DISABLED by default — enable in ~/.openclaw/openclaw.json; cwd hooks/ alone is a no-op');
314
+ break;
315
+ }
316
+ }
317
+ function materializeSubagent(h, name, sa, add, unsupported) {
318
+ switch (h) {
319
+ case 'claude-code':
320
+ case 'nanoclaw': {
321
+ const fm = ['---', `name: ${name}`];
322
+ if (sa.description)
323
+ fm.push(`description: ${JSON.stringify(sa.description)}`);
324
+ if (sa.model)
325
+ fm.push(`model: ${sa.model}`);
326
+ if (sa.tools)
327
+ fm.push(`tools: ${Object.keys(sa.tools).filter((t) => sa.tools[t]).join(', ')}`);
328
+ fm.push('---', '', sa.prompt ?? sa.description ?? name);
329
+ add(`.claude/agents/${name}.md`, fm.join('\n') + '\n');
330
+ break;
331
+ }
332
+ case 'opencode': {
333
+ const fm = ['---', `description: ${JSON.stringify(sa.description ?? name)}`];
334
+ if (sa.model)
335
+ fm.push(`model: ${sa.model}`);
336
+ fm.push('---', '', sa.prompt ?? '');
337
+ add(`.opencode/agent/${name}.md`, fm.join('\n') + '\n');
338
+ break;
339
+ }
340
+ case 'gemini':
341
+ add(`.gemini/agents/${name}.md`, `---\nname: ${name}\ndescription: ${JSON.stringify(sa.description ?? name)}\n---\n\n${sa.prompt ?? ''}\n`);
342
+ break;
343
+ case 'codex': {
344
+ const lines = [`name = ${tomlStr(name)}`];
345
+ if (sa.description)
346
+ lines.push(`description = ${tomlStr(sa.description)}`);
347
+ if (sa.model)
348
+ lines.push(`model = ${tomlStr(sa.model)}`);
349
+ if (sa.prompt)
350
+ lines.push(`prompt = ${tomlStr(sa.prompt)}`);
351
+ add(`.codex/agents/${name}.toml`, lines.join('\n') + '\n');
352
+ break;
353
+ }
354
+ default:
355
+ unsupported('subagents', `${h}: subagents have no cwd file (config/flag/global only); "${name}" not mounted`);
356
+ }
357
+ }
358
+ function materializeCommand(h, name, body, add, unsupported) {
359
+ switch (h) {
360
+ case 'claude-code':
361
+ case 'nanoclaw':
362
+ add(`.claude/commands/${name}.md`, body.endsWith('\n') ? body : body + '\n');
363
+ break;
364
+ case 'opencode':
365
+ add(`.opencode/command/${name}.md`, body.endsWith('\n') ? body : body + '\n');
366
+ break;
367
+ case 'gemini':
368
+ add(`.gemini/commands/${name}.toml`, `prompt = ${tomlStr(body)}\n`);
369
+ break;
370
+ case 'pi':
371
+ add(`.pi/prompts/${name}.md`, body.endsWith('\n') ? body : body + '\n');
372
+ break;
373
+ case 'kimi-code':
374
+ unsupported('commands', 'kimi commands ARE skills (/skill:<name>) — provide as a skill instead');
375
+ break;
376
+ case 'codex':
377
+ unsupported('commands', 'codex project commands unsupported (~/.codex/prompts is user-global only)');
378
+ break;
379
+ default: unsupported('commands', `${h}: no cwd command path`);
380
+ }
381
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@tangle-network/agent-profile-materialize",
3
+ "version": "0.1.0",
4
+ "description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-dev-container, and VB).",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@tangle-network/agent-interface": "0.5.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "25.6.0",
23
+ "typescript": "^6.0.3"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "clean": "rm -rf dist"
28
+ }
29
+ }