@tangle-network/agent-profile-materialize 0.1.0 → 0.2.3

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/index.d.ts CHANGED
@@ -21,8 +21,8 @@
21
21
  * `node:fs` IO consumer. It was previously copied byte-for-byte into both
22
22
  * cli-bridge and agent-dev-container — this is the single source those re-export.
23
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';
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
26
  export interface PlanFile {
27
27
  relPath: string;
28
28
  content: string;
@@ -30,7 +30,7 @@ export interface PlanFile {
30
30
  mode?: number;
31
31
  }
32
32
  export interface Unsupported {
33
- dimension: 'skills' | 'mcp' | 'hooks' | 'subagents' | 'commands' | 'instructions' | 'systemPrompt' | 'permissions' | 'tools';
33
+ dimension: "skills" | "mcp" | "hooks" | "subagents" | "commands" | "instructions" | "systemPrompt" | "permissions" | "tools";
34
34
  reason: string;
35
35
  }
36
36
  /** The plan a caller writes into the run cwd, then applies to the spawn. */
@@ -53,7 +53,7 @@ export declare function normalizeSkillMd(name: string, raw: string): string;
53
53
  * writes plan.files into the run cwd and applies plan.env/plan.flags to the spawn.
54
54
  */
55
55
  export declare function materializeProfile(profile: AgentProfile, harness: HarnessId, opts?: {
56
- skip?: Unsupported['dimension'][];
56
+ skip?: Unsupported["dimension"][];
57
57
  }): WorkspacePlan;
58
58
  /**
59
59
  * Apply a WorkspacePlan to a real workspace dir: write every file (creating parent
package/dist/index.js CHANGED
@@ -21,73 +21,90 @@
21
21
  * `node:fs` IO consumer. It was previously copied byte-for-byte into both
22
22
  * cli-bridge and agent-dev-container — this is the single source those re-export.
23
23
  */
24
- import { mkdirSync, writeFileSync } from 'node:fs';
25
- import { dirname, join } from 'node:path';
24
+ import { mkdirSync, writeFileSync } from "node:fs";
25
+ import { dirname, join } from "node:path";
26
26
  /** Resolve an inline resource ref to {name, content}. github refs need an async fetch
27
27
  * (the providers' resolveProfileResource does this); the sync materializer can't, so a
28
28
  * github ref is reported unsupported and the caller pre-resolves it. A ref with
29
29
  * `content` (or kind 'inline') is inline. */
30
30
  function resolveInlineRef(ref) {
31
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 ?? '') };
32
+ if (r.kind === "inline" || typeof r.content === "string") {
33
+ return {
34
+ name: String(r.name ?? r.path ?? "resource"),
35
+ content: String(r.content ?? ""),
36
+ };
34
37
  }
35
38
  return null; // github / unresolved
36
39
  }
37
40
  // ─── Verified per-harness conventions (capability matrix) ───────────────────
38
41
  /** Project context file each harness actually reads from cwd. NEVER a constant. */
39
42
  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',
43
+ "claude-code": "CLAUDE.md",
44
+ claude: "CLAUDE.md",
45
+ claudish: "CLAUDE.md",
46
+ nanoclaw: "CLAUDE.md",
47
+ codex: "AGENTS.md",
48
+ opencode: "AGENTS.md",
49
+ "kimi-code": "AGENTS.md",
50
+ kimi: "AGENTS.md",
51
+ pi: "AGENTS.md",
52
+ gemini: "GEMINI.md", // current default; AGENTS.md only via .gemini/settings.json context.fileName
53
+ hermes: "HERMES.md", // highest-priority native name; only ONE context loads (first match)
54
+ openclaw: "AGENTS.md",
45
55
  };
46
56
  /** cwd-relative native skill dir, or null if the harness has no cwd skill primitive. */
47
57
  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',
58
+ "claude-code": ".claude/skills",
59
+ claude: ".claude/skills",
60
+ claudish: ".claude/skills",
61
+ nanoclaw: ".claude/skills",
62
+ codex: ".codex/skills",
63
+ opencode: ".opencode/skills",
64
+ "kimi-code": ".kimi/skills",
65
+ kimi: ".kimi/skills",
66
+ pi: ".pi/skills",
67
+ gemini: ".gemini/skills",
51
68
  hermes: null, // skills are ~/.hermes/skills (user) only — no cwd discovery
52
- openclaw: 'skills', // <workspace>/skills/<name>/SKILL.md
69
+ openclaw: "skills", // <workspace>/skills/<name>/SKILL.md
53
70
  };
54
71
  function canonicalHarness(h) {
55
- if (h === 'claude' || h === 'claudish')
56
- return 'claude-code';
57
- if (h === 'kimi')
58
- return 'kimi-code';
72
+ if (h === "claude" || h === "claudish")
73
+ return "claude-code";
74
+ if (h === "kimi")
75
+ return "kimi-code";
59
76
  return h;
60
77
  }
61
78
  // ─── Helpers ────────────────────────────────────────────────────────────────
62
79
  const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
63
- const collapse = (s) => s.replace(/\s+/g, ' ').trim();
80
+ const collapse = (s) => s.replace(/\s+/g, " ").trim();
64
81
  /** Normalize any skill markdown (VB frontmatter `skill:`/`description:`, or raw) into a
65
82
  * harness SKILL.md with `name:`/`description:` frontmatter the harnesses require. */
66
83
  export function normalizeSkillMd(name, raw) {
67
84
  const m = raw.match(FRONTMATTER_RE);
68
- const fm = m?.[1] ?? '';
85
+ const fm = m?.[1] ?? "";
69
86
  const body = (m?.[2] ?? raw).trim();
70
87
  let desc = `Skill ${name}. Use when the task matches its domain.`;
71
88
  if (fm) {
72
- const lines = fm.split('\n');
89
+ const lines = fm.split("\n");
73
90
  const i = lines.findIndex((l) => /^description:/.test(l));
74
91
  if (i >= 0) {
75
- const inline = (lines[i] ?? '').replace(/^description:\s*/, '').trim();
76
- if (inline && !['>', '|', '>-', '|-'].includes(inline))
92
+ const inline = (lines[i] ?? "").replace(/^description:\s*/, "").trim();
93
+ if (inline && ![">", "|", ">-", "|-"].includes(inline))
77
94
  desc = collapse(inline);
78
95
  else {
79
96
  const buf = [];
80
97
  for (let j = i + 1; j < lines.length; j++) {
81
- const ln = lines[j] ?? '';
98
+ const ln = lines[j] ?? "";
82
99
  if (/^\s+\S/.test(ln))
83
100
  buf.push(ln.trim());
84
- else if (ln.trim() === '')
101
+ else if (ln.trim() === "")
85
102
  continue;
86
103
  else
87
104
  break;
88
105
  }
89
106
  if (buf.length)
90
- desc = collapse(buf.join(' '));
107
+ desc = collapse(buf.join(" "));
91
108
  }
92
109
  }
93
110
  }
@@ -102,7 +119,13 @@ const tomlStr = (s) => JSON.stringify(s); // valid TOML basic string
102
119
  export function materializeProfile(profile, harness, opts = {}) {
103
120
  const h = canonicalHarness(harness);
104
121
  const skip = new Set(opts.skip ?? []);
105
- const plan = { harness: h, files: [], env: {}, flags: [], unsupported: [] };
122
+ const plan = {
123
+ harness: h,
124
+ files: [],
125
+ env: {},
126
+ flags: [],
127
+ unsupported: [],
128
+ };
106
129
  const add = (relPath, content, mode) => plan.files.push({ relPath, content, ...(mode ? { mode } : {}) });
107
130
  const unsupported = (dimension, reason) => plan.unsupported.push({ dimension, reason });
108
131
  // 1) CONTEXT (instructions + systemPrompt → the harness's native context file).
@@ -112,12 +135,12 @@ export function materializeProfile(profile, harness, opts = {}) {
112
135
  if (profile.prompt?.instructions?.length)
113
136
  instr.push(...profile.prompt.instructions);
114
137
  const resInstr = profile.resources?.instructions;
115
- if (typeof resInstr === 'string')
138
+ if (typeof resInstr === "string")
116
139
  instr.push(resInstr);
117
- else if (resInstr && resInstr.kind === 'inline')
140
+ else if (resInstr && resInstr.kind === "inline")
118
141
  instr.push(resInstr.content);
119
142
  if (instr.length) {
120
- add(CONTEXT_FILE[h], `${instr.join('\n\n')}\n`);
143
+ add(CONTEXT_FILE[h], `${instr.join("\n\n")}\n`);
121
144
  // gemini reads AGENTS.md only via settings; we write GEMINI.md so no extra step.
122
145
  }
123
146
  // 2) SKILLS → native skill dir, from resources.skills refs (the box's shape), or
@@ -125,12 +148,12 @@ export function materializeProfile(profile, harness, opts = {}) {
125
148
  for (const ref of profile.resources?.skills ?? []) {
126
149
  const r = resolveInlineRef(ref);
127
150
  if (!r) {
128
- unsupported('skills', 'skill ref is not inline (github refs need async pre-resolution)');
151
+ unsupported("skills", "skill ref is not inline (github refs need async pre-resolution)");
129
152
  continue;
130
153
  }
131
154
  const dir = SKILL_DIR[h];
132
155
  if (!dir) {
133
- unsupported('skills', `${h}: no cwd skill dir (skills live in a user/global dir); skill "${r.name}" not mounted`);
156
+ unsupported("skills", `${h}: no cwd skill dir (skills live in a user/global dir); skill "${r.name}" not mounted`);
134
157
  continue;
135
158
  }
136
159
  add(`${dir}/${r.name}/SKILL.md`, normalizeSkillMd(r.name, r.content));
@@ -138,7 +161,7 @@ export function materializeProfile(profile, harness, opts = {}) {
138
161
  // 3) MCP → per-harness format (the divergence the matrix names). Skippable so the
139
162
  // additive host-wiring can leave MCP on cli-bridge's existing per-harness path.
140
163
  const mcp = profile.mcp ?? {};
141
- if (!skip.has('mcp') && Object.keys(mcp).length)
164
+ if (!skip.has("mcp") && Object.keys(mcp).length)
142
165
  materializeMcp(h, mcp, plan, add, unsupported);
143
166
  // 4) HOOKS → per-harness format; cwd-native only for claude/codex/gemini/opencode(plugin).
144
167
  const hooks = profile.hooks ?? {};
@@ -152,7 +175,7 @@ export function materializeProfile(profile, harness, opts = {}) {
152
175
  for (const ref of profile.resources?.commands ?? []) {
153
176
  const r = resolveInlineRef(ref);
154
177
  if (!r) {
155
- unsupported('commands', 'command ref is not inline (github needs pre-resolution)');
178
+ unsupported("commands", "command ref is not inline (github needs pre-resolution)");
156
179
  continue;
157
180
  }
158
181
  materializeCommand(h, r.name, r.content, add, unsupported);
@@ -172,13 +195,21 @@ function mergeJsonFiles(files) {
172
195
  byPath.set(f.relPath, f);
173
196
  continue;
174
197
  }
175
- if (f.relPath.endsWith('.json')) {
198
+ if (f.relPath.endsWith(".json")) {
176
199
  try {
177
- const merged = { ...JSON.parse(prev.content), ...JSON.parse(f.content) };
178
- byPath.set(f.relPath, { ...prev, content: JSON.stringify(merged, null, 2) });
200
+ const merged = {
201
+ ...JSON.parse(prev.content),
202
+ ...JSON.parse(f.content),
203
+ };
204
+ byPath.set(f.relPath, {
205
+ ...prev,
206
+ content: JSON.stringify(merged, null, 2),
207
+ });
179
208
  continue;
180
209
  }
181
- catch { /* fall through to last-wins */ }
210
+ catch {
211
+ /* fall through to last-wins */
212
+ }
182
213
  }
183
214
  byPath.set(f.relPath, f); // non-JSON or parse-fail: last wins
184
215
  }
@@ -198,60 +229,67 @@ export function applyWorkspacePlan(plan, workspaceDir) {
198
229
  writeFileSync(abs, f.content, { mode: f.mode ?? 0o644 });
199
230
  written.push(f.relPath);
200
231
  }
201
- return { env: plan.env, flags: plan.flags, unsupported: plan.unsupported, written };
232
+ return {
233
+ env: plan.env,
234
+ flags: plan.flags,
235
+ unsupported: plan.unsupported,
236
+ written,
237
+ };
202
238
  }
203
239
  // ─── Per-dimension routing (verified matrix) ────────────────────────────────
204
240
  function materializeMcp(h, mcp, plan, add, unsupported) {
205
241
  switch (h) {
206
- case 'claude-code':
207
- case 'nanoclaw':
242
+ case "claude-code":
243
+ case "nanoclaw":
208
244
  // claude reads ./.mcp.json but each server needs per-project APPROVAL; we also
209
245
  // 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');
246
+ add(".mcp.json", JSON.stringify({ mcpServers: mcp }, null, 2));
247
+ add(".claude/settings.json", JSON.stringify({ enabledMcpjsonServers: Object.keys(mcp) }, null, 2));
248
+ if (h === "nanoclaw")
249
+ unsupported("mcp", "nanoclaw MCP is SDK-injected (options.mcpServers); .mcp.json may be ignored — inject via the SDK adapter");
214
250
  break;
215
- case 'codex': {
251
+ case "codex": {
216
252
  // codex reads [mcp_servers.<id>] in .codex/config.toml; trusted projects only.
217
- const lines = ['[projects."."]', 'trust_level = "trusted"', ''];
253
+ const lines = ['[projects."."]', 'trust_level = "trusted"', ""];
218
254
  for (const [name, s] of Object.entries(mcp)) {
219
255
  lines.push(`[mcp_servers.${name}]`);
220
256
  if (s.command)
221
257
  lines.push(`command = ${tomlStr(s.command)}`);
222
258
  if (s.args?.length)
223
- lines.push(`args = [${s.args.map(tomlStr).join(', ')}]`);
259
+ lines.push(`args = [${s.args.map(tomlStr).join(", ")}]`);
224
260
  if (s.url)
225
261
  lines.push(`url = ${tomlStr(s.url)}`);
226
262
  if (s.env)
227
- lines.push(`env = { ${Object.entries(s.env).map(([k, v]) => `${k} = ${tomlStr(v)}`).join(', ')} }`);
228
- lines.push('');
263
+ lines.push(`env = { ${Object.entries(s.env)
264
+ .map(([k, v]) => `${k} = ${tomlStr(v)}`)
265
+ .join(", ")} }`);
266
+ lines.push("");
229
267
  }
230
- add('.codex/config.toml', lines.join('\n'));
268
+ add(".codex/config.toml", lines.join("\n"));
231
269
  break;
232
270
  }
233
- case 'opencode':
271
+ case "opencode":
234
272
  // opencode.json "mcp" block (local|remote).
235
- add('opencode.json', JSON.stringify({ $schema: 'https://opencode.ai/config.json', mcp: opencodeMcp(mcp) }, null, 2));
273
+ add("opencode.json", JSON.stringify({ $schema: "https://opencode.ai/config.json", mcp: opencodeMcp(mcp) }, null, 2));
236
274
  break;
237
- case 'gemini':
238
- add('.gemini/settings.json', JSON.stringify({ mcpServers: mcp }, null, 2));
275
+ case "gemini":
276
+ add(".gemini/settings.json", JSON.stringify({ mcpServers: mcp }, null, 2));
239
277
  break;
240
- case 'kimi-code': {
278
+ case "kimi-code": {
241
279
  // NO cwd auto-discovery — kimi needs an explicit --mcp-config-file FLAG.
242
- const rel = '.kimi/mcp.json';
280
+ const rel = ".kimi/mcp.json";
243
281
  add(rel, JSON.stringify({ mcpServers: mcp }, null, 2));
244
- plan.flags.push('--mcp-config-file', rel);
282
+ plan.flags.push("--mcp-config-file", rel);
245
283
  break;
246
284
  }
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');
285
+ case "pi":
286
+ unsupported("mcp", "pi MCP is wired via a TS extension (.pi/extensions/*.ts), not a declarative cwd file — build an extension");
249
287
  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');
288
+ case "hermes":
289
+ 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
290
  break;
253
- case 'openclaw':
254
- unsupported('mcp', 'openclaw MCP is central ~/.openclaw/openclaw.json (mcp.servers), no cwd discovery');
291
+ case "openclaw":
292
+ unsupported("mcp", "openclaw MCP is central ~/.openclaw/openclaw.json (mcp.servers), no cwd discovery");
255
293
  break;
256
294
  }
257
295
  }
@@ -259,88 +297,117 @@ function opencodeMcp(mcp) {
259
297
  const out = {};
260
298
  for (const [name, s] of Object.entries(mcp)) {
261
299
  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 } : {}) };
300
+ ? {
301
+ type: "remote",
302
+ url: s.url,
303
+ enabled: s.enabled !== false,
304
+ ...(s.headers ? { headers: s.headers } : {}),
305
+ }
306
+ : {
307
+ type: "local",
308
+ command: [s.command, ...(s.args ?? [])].filter(Boolean),
309
+ enabled: s.enabled !== false,
310
+ ...(s.env ? { environment: s.env } : {}),
311
+ };
264
312
  }
265
313
  return out;
266
314
  }
267
315
  function materializeHooks(h, hooks, add, unsupported, plan) {
268
316
  switch (h) {
269
- case 'claude-code':
270
- case 'nanoclaw': {
317
+ case "claude-code":
318
+ case "nanoclaw": {
271
319
  // .claude/settings.json hooks{} — each event → [{matcher, hooks:[{type:command,command}]}].
272
320
  const settingsHooks = {};
273
321
  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) } : {}) }] }));
322
+ settingsHooks[event] = cmds.map((c) => ({
323
+ matcher: c.matcher ?? "*",
324
+ hooks: [
325
+ {
326
+ type: "command",
327
+ command: c.command,
328
+ ...(c.timeoutMs
329
+ ? { timeout: Math.round(c.timeoutMs / 1000) }
330
+ : {}),
331
+ },
332
+ ],
333
+ }));
275
334
  }
276
- add('.claude/settings.json', JSON.stringify({ hooks: settingsHooks }, null, 2));
335
+ add(".claude/settings.json", JSON.stringify({ hooks: settingsHooks }, null, 2));
277
336
  break;
278
337
  }
279
- case 'gemini': {
338
+ case "gemini": {
280
339
  const settingsHooks = {};
281
340
  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));
341
+ settingsHooks[event] = cmds.map((c) => ({
342
+ matcher: c.matcher ?? "*",
343
+ hooks: [{ type: "command", command: c.command }],
344
+ }));
345
+ add(".gemini/settings.json", JSON.stringify({ hooks: settingsHooks }, null, 2));
284
346
  break;
285
347
  }
286
- case 'codex': {
348
+ case "codex": {
287
349
  // codex hooks.json (event → commands).
288
- add('.codex/hooks.json', JSON.stringify({ hooks }, null, 2));
350
+ add(".codex/hooks.json", JSON.stringify({ hooks }, null, 2));
289
351
  break;
290
352
  }
291
- case 'opencode':
353
+ case "opencode":
292
354
  // opencode hooks are PLUGINS (JS/TS), not a settings block — emit a plugin stub.
293
355
  for (const [event, cmds] of Object.entries(hooks)) {
294
- const body = cmds.map((c) => ` // ${event}: ${c.command}`).join('\n');
356
+ const body = cmds.map((c) => ` // ${event}: ${c.command}`).join("\n");
295
357
  add(`.opencode/plugin/vb-hooks-${event}.js`, `// auto-generated hook plugin for ${event}\nexport default async function () {\n${body}\n}\n`);
296
358
  }
297
- unsupported('hooks', 'opencode hooks are JS/TS plugins — generated a stub; a real handler must run the command (format differs from settings.json)');
359
+ unsupported("hooks", "opencode hooks are JS/TS plugins — generated a stub; a real handler must run the command (format differs from settings.json)");
298
360
  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');
361
+ case "kimi-code":
362
+ unsupported("hooks", "kimi hooks are [[hooks]] in the config TOML loaded via --config-file, not cwd — pass --config-file");
301
363
  break;
302
- case 'pi':
303
- unsupported('hooks', 'pi hooks are TS extensions (.pi/extensions/*.ts) — build an extension');
364
+ case "pi":
365
+ unsupported("hooks", "pi hooks are TS extensions (.pi/extensions/*.ts) — build an extension");
304
366
  break;
305
- case 'hermes': {
367
+ case "hermes": {
306
368
  // opt-in project plugins; requires HERMES_ENABLE_PROJECT_PLUGINS=1.
307
369
  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';
370
+ add(`.hermes/plugins/vb-${event}/plugin.md`, cmds.map((c) => c.command).join("\n"));
371
+ plan.env.HERMES_ENABLE_PROJECT_PLUGINS = "1";
310
372
  break;
311
373
  }
312
- case 'openclaw':
313
- unsupported('hooks', 'openclaw hooks are DISABLED by default — enable in ~/.openclaw/openclaw.json; cwd hooks/ alone is a no-op');
374
+ case "openclaw":
375
+ unsupported("hooks", "openclaw hooks are DISABLED by default — enable in ~/.openclaw/openclaw.json; cwd hooks/ alone is a no-op");
314
376
  break;
315
377
  }
316
378
  }
317
379
  function materializeSubagent(h, name, sa, add, unsupported) {
318
380
  switch (h) {
319
- case 'claude-code':
320
- case 'nanoclaw': {
321
- const fm = ['---', `name: ${name}`];
381
+ case "claude-code":
382
+ case "nanoclaw": {
383
+ const fm = ["---", `name: ${name}`];
322
384
  if (sa.description)
323
385
  fm.push(`description: ${JSON.stringify(sa.description)}`);
324
386
  if (sa.model)
325
387
  fm.push(`model: ${sa.model}`);
326
388
  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');
389
+ fm.push(`tools: ${Object.keys(sa.tools)
390
+ .filter((t) => sa.tools?.[t])
391
+ .join(", ")}`);
392
+ fm.push("---", "", sa.prompt ?? sa.description ?? name);
393
+ add(`.claude/agents/${name}.md`, `${fm.join("\n")}\n`);
330
394
  break;
331
395
  }
332
- case 'opencode': {
333
- const fm = ['---', `description: ${JSON.stringify(sa.description ?? name)}`];
396
+ case "opencode": {
397
+ const fm = [
398
+ "---",
399
+ `description: ${JSON.stringify(sa.description ?? name)}`,
400
+ ];
334
401
  if (sa.model)
335
402
  fm.push(`model: ${sa.model}`);
336
- fm.push('---', '', sa.prompt ?? '');
337
- add(`.opencode/agent/${name}.md`, fm.join('\n') + '\n');
403
+ fm.push("---", "", sa.prompt ?? "");
404
+ add(`.opencode/agent/${name}.md`, `${fm.join("\n")}\n`);
338
405
  break;
339
406
  }
340
- case 'gemini':
341
- add(`.gemini/agents/${name}.md`, `---\nname: ${name}\ndescription: ${JSON.stringify(sa.description ?? name)}\n---\n\n${sa.prompt ?? ''}\n`);
407
+ case "gemini":
408
+ add(`.gemini/agents/${name}.md`, `---\nname: ${name}\ndescription: ${JSON.stringify(sa.description ?? name)}\n---\n\n${sa.prompt ?? ""}\n`);
342
409
  break;
343
- case 'codex': {
410
+ case "codex": {
344
411
  const lines = [`name = ${tomlStr(name)}`];
345
412
  if (sa.description)
346
413
  lines.push(`description = ${tomlStr(sa.description)}`);
@@ -348,34 +415,35 @@ function materializeSubagent(h, name, sa, add, unsupported) {
348
415
  lines.push(`model = ${tomlStr(sa.model)}`);
349
416
  if (sa.prompt)
350
417
  lines.push(`prompt = ${tomlStr(sa.prompt)}`);
351
- add(`.codex/agents/${name}.toml`, lines.join('\n') + '\n');
418
+ add(`.codex/agents/${name}.toml`, `${lines.join("\n")}\n`);
352
419
  break;
353
420
  }
354
421
  default:
355
- unsupported('subagents', `${h}: subagents have no cwd file (config/flag/global only); "${name}" not mounted`);
422
+ unsupported("subagents", `${h}: subagents have no cwd file (config/flag/global only); "${name}" not mounted`);
356
423
  }
357
424
  }
358
425
  function materializeCommand(h, name, body, add, unsupported) {
359
426
  switch (h) {
360
- case 'claude-code':
361
- case 'nanoclaw':
362
- add(`.claude/commands/${name}.md`, body.endsWith('\n') ? body : body + '\n');
427
+ case "claude-code":
428
+ case "nanoclaw":
429
+ add(`.claude/commands/${name}.md`, body.endsWith("\n") ? body : `${body}\n`);
363
430
  break;
364
- case 'opencode':
365
- add(`.opencode/command/${name}.md`, body.endsWith('\n') ? body : body + '\n');
431
+ case "opencode":
432
+ add(`.opencode/command/${name}.md`, body.endsWith("\n") ? body : `${body}\n`);
366
433
  break;
367
- case 'gemini':
434
+ case "gemini":
368
435
  add(`.gemini/commands/${name}.toml`, `prompt = ${tomlStr(body)}\n`);
369
436
  break;
370
- case 'pi':
371
- add(`.pi/prompts/${name}.md`, body.endsWith('\n') ? body : body + '\n');
437
+ case "pi":
438
+ add(`.pi/prompts/${name}.md`, body.endsWith("\n") ? body : `${body}\n`);
372
439
  break;
373
- case 'kimi-code':
374
- unsupported('commands', 'kimi commands ARE skills (/skill:<name>) — provide as a skill instead');
440
+ case "kimi-code":
441
+ unsupported("commands", "kimi commands ARE skills (/skill:<name>) — provide as a skill instead");
375
442
  break;
376
- case 'codex':
377
- unsupported('commands', 'codex project commands unsupported (~/.codex/prompts is user-global only)');
443
+ case "codex":
444
+ unsupported("commands", "codex project commands unsupported (~/.codex/prompts is user-global only)");
378
445
  break;
379
- default: unsupported('commands', `${h}: no cwd command path`);
446
+ default:
447
+ unsupported("commands", `${h}: no cwd command path`);
380
448
  }
381
449
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.1.0",
3
+ "version": "0.2.3",
4
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
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -16,7 +16,7 @@
16
16
  "dist"
17
17
  ],
18
18
  "dependencies": {
19
- "@tangle-network/agent-interface": "0.5.0"
19
+ "@tangle-network/agent-interface": "^0.18.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "25.6.0",