@moreih29/nexus-core 0.13.0 → 0.14.1

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.
Files changed (162) hide show
  1. package/README.md +34 -0
  2. package/dist/assets/hooks/agent-bootstrap/handler.d.ts +4 -0
  3. package/dist/assets/hooks/agent-bootstrap/handler.d.ts.map +1 -0
  4. package/dist/assets/hooks/agent-bootstrap/handler.js +100 -0
  5. package/dist/assets/hooks/agent-bootstrap/handler.js.map +1 -0
  6. package/dist/assets/hooks/agent-finalize/handler.d.ts +4 -0
  7. package/dist/assets/hooks/agent-finalize/handler.d.ts.map +1 -0
  8. package/dist/assets/hooks/agent-finalize/handler.js +63 -0
  9. package/dist/assets/hooks/agent-finalize/handler.js.map +1 -0
  10. package/dist/assets/hooks/post-tool-telemetry/handler.d.ts +4 -0
  11. package/dist/assets/hooks/post-tool-telemetry/handler.d.ts.map +1 -0
  12. package/dist/assets/hooks/post-tool-telemetry/handler.js +40 -0
  13. package/dist/assets/hooks/post-tool-telemetry/handler.js.map +1 -0
  14. package/dist/assets/hooks/prompt-router/handler.d.ts +4 -0
  15. package/dist/assets/hooks/prompt-router/handler.d.ts.map +1 -0
  16. package/dist/assets/hooks/prompt-router/handler.js +204 -0
  17. package/dist/assets/hooks/prompt-router/handler.js.map +1 -0
  18. package/dist/assets/hooks/session-init/handler.d.ts +4 -0
  19. package/dist/assets/hooks/session-init/handler.d.ts.map +1 -0
  20. package/dist/assets/hooks/session-init/handler.js +23 -0
  21. package/dist/assets/hooks/session-init/handler.js.map +1 -0
  22. package/dist/hooks/agent-bootstrap.js +105 -0
  23. package/dist/hooks/agent-finalize.js +164 -0
  24. package/dist/hooks/post-tool-telemetry.js +55 -0
  25. package/dist/hooks/prompt-router.js +7300 -0
  26. package/dist/hooks/session-init.js +21 -0
  27. package/dist/manifests/claude-hooks.json +52 -0
  28. package/dist/manifests/codex-hooks.json +28 -0
  29. package/dist/manifests/opencode-manifest.json +44 -0
  30. package/dist/manifests/portability-report.json +87 -0
  31. package/dist/scripts/build-agents.d.ts +157 -0
  32. package/dist/scripts/build-agents.d.ts.map +1 -0
  33. package/dist/scripts/build-agents.js +738 -0
  34. package/dist/scripts/build-agents.js.map +1 -0
  35. package/dist/scripts/build-hooks.d.ts +16 -0
  36. package/dist/scripts/build-hooks.d.ts.map +1 -0
  37. package/dist/scripts/build-hooks.js +389 -0
  38. package/dist/scripts/build-hooks.js.map +1 -0
  39. package/dist/scripts/cli.d.ts +54 -0
  40. package/dist/scripts/cli.d.ts.map +1 -0
  41. package/dist/scripts/cli.js +482 -0
  42. package/dist/scripts/cli.js.map +1 -0
  43. package/dist/src/hooks/opencode-mount.d.ts.map +1 -0
  44. package/dist/{hooks → src/hooks}/opencode-mount.js +26 -6
  45. package/dist/src/hooks/opencode-mount.js.map +1 -0
  46. package/dist/src/hooks/runtime.d.ts.map +1 -0
  47. package/dist/src/hooks/runtime.js.map +1 -0
  48. package/dist/src/hooks/types.d.ts.map +1 -0
  49. package/dist/src/hooks/types.js.map +1 -0
  50. package/dist/src/lsp/cache.d.ts.map +1 -0
  51. package/dist/src/lsp/cache.js.map +1 -0
  52. package/dist/src/lsp/client.d.ts.map +1 -0
  53. package/dist/src/lsp/client.js.map +1 -0
  54. package/dist/src/lsp/detect.d.ts.map +1 -0
  55. package/dist/src/lsp/detect.js.map +1 -0
  56. package/dist/src/mcp/server.d.ts.map +1 -0
  57. package/dist/src/mcp/server.js.map +1 -0
  58. package/dist/src/mcp/tools/artifact.d.ts.map +1 -0
  59. package/dist/src/mcp/tools/artifact.js.map +1 -0
  60. package/dist/src/mcp/tools/history.d.ts.map +1 -0
  61. package/dist/src/mcp/tools/history.js.map +1 -0
  62. package/dist/src/mcp/tools/lsp.d.ts.map +1 -0
  63. package/dist/src/mcp/tools/lsp.js.map +1 -0
  64. package/dist/src/mcp/tools/plan.d.ts.map +1 -0
  65. package/dist/src/mcp/tools/plan.js.map +1 -0
  66. package/dist/src/mcp/tools/task.d.ts.map +1 -0
  67. package/dist/src/mcp/tools/task.js.map +1 -0
  68. package/dist/src/shared/invocations.d.ts.map +1 -0
  69. package/dist/src/shared/invocations.js.map +1 -0
  70. package/dist/src/shared/json-store.d.ts.map +1 -0
  71. package/dist/src/shared/json-store.js.map +1 -0
  72. package/dist/src/shared/mcp-utils.d.ts.map +1 -0
  73. package/dist/src/shared/mcp-utils.js.map +1 -0
  74. package/dist/src/shared/package-root.d.ts +6 -0
  75. package/dist/src/shared/package-root.d.ts.map +1 -0
  76. package/dist/src/shared/package-root.js +19 -0
  77. package/dist/src/shared/package-root.js.map +1 -0
  78. package/dist/src/shared/paths.d.ts.map +1 -0
  79. package/dist/src/shared/paths.js.map +1 -0
  80. package/dist/src/shared/tool-log.d.ts.map +1 -0
  81. package/dist/src/shared/tool-log.js.map +1 -0
  82. package/dist/src/types/state.d.ts.map +1 -0
  83. package/dist/src/types/state.js.map +1 -0
  84. package/docs/plugin-guide.md +36 -0
  85. package/package.json +25 -17
  86. package/dist/hooks/opencode-mount.d.ts.map +0 -1
  87. package/dist/hooks/opencode-mount.js.map +0 -1
  88. package/dist/hooks/runtime.d.ts.map +0 -1
  89. package/dist/hooks/runtime.js.map +0 -1
  90. package/dist/hooks/types.d.ts.map +0 -1
  91. package/dist/hooks/types.js.map +0 -1
  92. package/dist/lsp/cache.d.ts.map +0 -1
  93. package/dist/lsp/cache.js.map +0 -1
  94. package/dist/lsp/client.d.ts.map +0 -1
  95. package/dist/lsp/client.js.map +0 -1
  96. package/dist/lsp/detect.d.ts.map +0 -1
  97. package/dist/lsp/detect.js.map +0 -1
  98. package/dist/mcp/server.d.ts.map +0 -1
  99. package/dist/mcp/server.js.map +0 -1
  100. package/dist/mcp/tools/artifact.d.ts.map +0 -1
  101. package/dist/mcp/tools/artifact.js.map +0 -1
  102. package/dist/mcp/tools/history.d.ts.map +0 -1
  103. package/dist/mcp/tools/history.js.map +0 -1
  104. package/dist/mcp/tools/lsp.d.ts.map +0 -1
  105. package/dist/mcp/tools/lsp.js.map +0 -1
  106. package/dist/mcp/tools/plan.d.ts.map +0 -1
  107. package/dist/mcp/tools/plan.js.map +0 -1
  108. package/dist/mcp/tools/task.d.ts.map +0 -1
  109. package/dist/mcp/tools/task.js.map +0 -1
  110. package/dist/shared/invocations.d.ts.map +0 -1
  111. package/dist/shared/invocations.js.map +0 -1
  112. package/dist/shared/json-store.d.ts.map +0 -1
  113. package/dist/shared/json-store.js.map +0 -1
  114. package/dist/shared/mcp-utils.d.ts.map +0 -1
  115. package/dist/shared/mcp-utils.js.map +0 -1
  116. package/dist/shared/paths.d.ts.map +0 -1
  117. package/dist/shared/paths.js.map +0 -1
  118. package/dist/shared/tool-log.d.ts.map +0 -1
  119. package/dist/shared/tool-log.js.map +0 -1
  120. package/dist/types/state.d.ts.map +0 -1
  121. package/dist/types/state.js.map +0 -1
  122. package/scripts/build-agents.test.ts +0 -1279
  123. package/scripts/build-agents.ts +0 -978
  124. package/scripts/build-hooks.test.ts +0 -1385
  125. package/scripts/build-hooks.ts +0 -584
  126. package/scripts/cli.test.ts +0 -367
  127. package/scripts/cli.ts +0 -547
  128. /package/dist/{hooks → src/hooks}/opencode-mount.d.ts +0 -0
  129. /package/dist/{hooks → src/hooks}/runtime.d.ts +0 -0
  130. /package/dist/{hooks → src/hooks}/runtime.js +0 -0
  131. /package/dist/{hooks → src/hooks}/types.d.ts +0 -0
  132. /package/dist/{hooks → src/hooks}/types.js +0 -0
  133. /package/dist/{lsp → src/lsp}/cache.d.ts +0 -0
  134. /package/dist/{lsp → src/lsp}/cache.js +0 -0
  135. /package/dist/{lsp → src/lsp}/client.d.ts +0 -0
  136. /package/dist/{lsp → src/lsp}/client.js +0 -0
  137. /package/dist/{lsp → src/lsp}/detect.d.ts +0 -0
  138. /package/dist/{lsp → src/lsp}/detect.js +0 -0
  139. /package/dist/{mcp → src/mcp}/server.d.ts +0 -0
  140. /package/dist/{mcp → src/mcp}/server.js +0 -0
  141. /package/dist/{mcp → src/mcp}/tools/artifact.d.ts +0 -0
  142. /package/dist/{mcp → src/mcp}/tools/artifact.js +0 -0
  143. /package/dist/{mcp → src/mcp}/tools/history.d.ts +0 -0
  144. /package/dist/{mcp → src/mcp}/tools/history.js +0 -0
  145. /package/dist/{mcp → src/mcp}/tools/lsp.d.ts +0 -0
  146. /package/dist/{mcp → src/mcp}/tools/lsp.js +0 -0
  147. /package/dist/{mcp → src/mcp}/tools/plan.d.ts +0 -0
  148. /package/dist/{mcp → src/mcp}/tools/plan.js +0 -0
  149. /package/dist/{mcp → src/mcp}/tools/task.d.ts +0 -0
  150. /package/dist/{mcp → src/mcp}/tools/task.js +0 -0
  151. /package/dist/{shared → src/shared}/invocations.d.ts +0 -0
  152. /package/dist/{shared → src/shared}/invocations.js +0 -0
  153. /package/dist/{shared → src/shared}/json-store.d.ts +0 -0
  154. /package/dist/{shared → src/shared}/json-store.js +0 -0
  155. /package/dist/{shared → src/shared}/mcp-utils.d.ts +0 -0
  156. /package/dist/{shared → src/shared}/mcp-utils.js +0 -0
  157. /package/dist/{shared → src/shared}/paths.d.ts +0 -0
  158. /package/dist/{shared → src/shared}/paths.js +0 -0
  159. /package/dist/{shared → src/shared}/tool-log.d.ts +0 -0
  160. /package/dist/{shared → src/shared}/tool-log.js +0 -0
  161. /package/dist/{types → src/types}/state.d.ts +0 -0
  162. /package/dist/{types → src/types}/state.js +0 -0
@@ -0,0 +1,738 @@
1
+ /**
2
+ * scripts/build-agents.ts
3
+ *
4
+ * Build pipeline for nexus agents and skills.
5
+ *
6
+ * Inputs:
7
+ * assets/agents/<n>/body.md × 9 agents
8
+ * assets/skills/<n>/body.md × 4 skills
9
+ * assets/capability-matrix.yml
10
+ * assets/tools/tool-name-map.yml (invocations section)
11
+ *
12
+ * Outputs per harness:
13
+ * dist/claude/
14
+ * .claude-plugin/plugin.json (Template — skip if exists, --force to overwrite)
15
+ * .claude-plugin/marketplace.json (Template — skip if exists, --force to overwrite)
16
+ * agents/<n>.md × N
17
+ * skills/<n>/SKILL.md × 4
18
+ * settings.json (Managed — primary agent injection, omitted if no primary)
19
+ *
20
+ * dist/opencode/
21
+ * package.json (Template — skip if exists)
22
+ * opencode.json.fragment (Managed — always overwrite)
23
+ * src/index.ts (Managed — always overwrite)
24
+ * src/agents/<n>.ts × N (Managed — always overwrite, mode:primary gets mode field)
25
+ * .opencode/skills/<n>/SKILL.md × 4 (Managed)
26
+ *
27
+ * dist/codex/
28
+ * plugin/.codex-plugin/plugin.json (Managed)
29
+ * plugin/skills/<n>/SKILL.md × 4 (Managed)
30
+ * agents/<n>.toml × N (Managed)
31
+ * prompts/<n>.md × N (Managed)
32
+ * install/config.fragment.toml (Managed)
33
+ * install/AGENTS.fragment.md (Managed — primary agents only, omitted if none)
34
+ *
35
+ * Overwrite policy:
36
+ * Managed paths — always overwrite (unless --dry-run)
37
+ * Template paths — skip if file exists (overwrite only with --force)
38
+ *
39
+ * CLI flags:
40
+ * --harness=claude|opencode|codex (default: all)
41
+ * --target=<dir> (default: dist/)
42
+ * --dry-run print affected files, no writes
43
+ * --force force Template file overwrite
44
+ * --strict error if Managed output has untracked modifications
45
+ * --only=<agent|skill name> restrict to a single asset
46
+ */
47
+ import { readFileSync, readdirSync, existsSync, mkdirSync, writeFileSync, } from "node:fs";
48
+ import { join, resolve, dirname } from "node:path";
49
+ import { fileURLToPath } from "node:url";
50
+ import { execSync } from "node:child_process";
51
+ import { parse as parseYaml } from "yaml";
52
+ import { expandInvocations } from "../src/shared/invocations.js";
53
+ import { findPackageRoot } from "../src/shared/package-root.js";
54
+ // ---------------------------------------------------------------------------
55
+ // Constants
56
+ // ---------------------------------------------------------------------------
57
+ const __dirname = dirname(fileURLToPath(import.meta.url));
58
+ export const ROOT = findPackageRoot(__dirname);
59
+ export const AGENTS_DIR = join(ROOT, "assets/agents");
60
+ export const SKILLS_DIR = join(ROOT, "assets/skills");
61
+ export const CAPABILITY_MATRIX_PATH = join(ROOT, "assets/capability-matrix.yml");
62
+ export const TOOL_NAME_MAP_PATH = join(ROOT, "assets/tools/tool-name-map.yml");
63
+ const HARNESSES = ["claude", "opencode", "codex"];
64
+ // Track dry-run affected files
65
+ const dryRunFiles = [];
66
+ // ---------------------------------------------------------------------------
67
+ // Frontmatter parsing
68
+ // ---------------------------------------------------------------------------
69
+ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
70
+ /**
71
+ * Split a body.md file into frontmatter object and body text.
72
+ * Throws on YAML parse failure.
73
+ */
74
+ export function parseFrontmatter(raw, filePath) {
75
+ const match = FRONTMATTER_RE.exec(raw);
76
+ if (!match) {
77
+ throw new Error(`[build-agents] Missing or malformed frontmatter in: ${filePath}`);
78
+ }
79
+ let fm;
80
+ try {
81
+ fm = parseYaml(match[1]);
82
+ }
83
+ catch (err) {
84
+ throw new Error(`[build-agents] YAML parse failure in frontmatter of: ${filePath}\n ${String(err)}`);
85
+ }
86
+ if (!fm.id || !fm.name) {
87
+ throw new Error(`[build-agents] Missing required frontmatter fields (id, name) in: ${filePath}`);
88
+ }
89
+ const VALID_MODES = ["primary", "subagent", "all"];
90
+ if (fm.mode !== undefined && !VALID_MODES.includes(fm.mode)) {
91
+ throw new Error(`[build-agents] Invalid mode "${fm.mode}" in: ${filePath}. Valid values: ${VALID_MODES.join(", ")}`);
92
+ }
93
+ return { fm, body: (match[2] ?? "").trimStart() };
94
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // Stage 1: Load assets
97
+ // ---------------------------------------------------------------------------
98
+ /**
99
+ * Load all agents and skills from assets/, whitelisting only body.md.
100
+ */
101
+ export function loadAssets(opts) {
102
+ const entries = [];
103
+ // Load agents
104
+ if (existsSync(AGENTS_DIR)) {
105
+ for (const entry of readdirSync(AGENTS_DIR, { withFileTypes: true })) {
106
+ if (!entry.isDirectory())
107
+ continue;
108
+ if (opts?.only && entry.name !== opts.only)
109
+ continue;
110
+ const bodyPath = join(AGENTS_DIR, entry.name, "body.md");
111
+ if (!existsSync(bodyPath)) {
112
+ throw new Error(`[build-agents] Missing body.md for agent: ${entry.name}`);
113
+ }
114
+ const raw = readFileSync(bodyPath, "utf-8");
115
+ const { fm, body } = parseFrontmatter(raw, bodyPath);
116
+ entries.push({ type: "agent", name: entry.name, frontmatter: fm, body, bodyPath });
117
+ }
118
+ }
119
+ // Load skills
120
+ if (existsSync(SKILLS_DIR)) {
121
+ for (const entry of readdirSync(SKILLS_DIR, { withFileTypes: true })) {
122
+ if (!entry.isDirectory())
123
+ continue;
124
+ if (opts?.only && entry.name !== opts.only)
125
+ continue;
126
+ const bodyPath = join(SKILLS_DIR, entry.name, "body.md");
127
+ if (!existsSync(bodyPath)) {
128
+ throw new Error(`[build-agents] Missing body.md for skill: ${entry.name}`);
129
+ }
130
+ const raw = readFileSync(bodyPath, "utf-8");
131
+ const { fm, body } = parseFrontmatter(raw, bodyPath);
132
+ entries.push({ type: "skill", name: entry.name, frontmatter: fm, body, bodyPath });
133
+ }
134
+ }
135
+ return entries;
136
+ }
137
+ // ---------------------------------------------------------------------------
138
+ // Stage 2: Load capability matrix
139
+ // ---------------------------------------------------------------------------
140
+ export function loadCapabilityMatrix() {
141
+ if (!existsSync(CAPABILITY_MATRIX_PATH)) {
142
+ throw new Error(`[build-agents] capability-matrix.yml not found at: ${CAPABILITY_MATRIX_PATH}`);
143
+ }
144
+ const raw = readFileSync(CAPABILITY_MATRIX_PATH, "utf-8");
145
+ return parseYaml(raw);
146
+ }
147
+ // ---------------------------------------------------------------------------
148
+ // Stage 3: Load invocations
149
+ // ---------------------------------------------------------------------------
150
+ export function loadInvocations() {
151
+ if (!existsSync(TOOL_NAME_MAP_PATH)) {
152
+ throw new Error(`[build-agents] tool-name-map.yml not found at: ${TOOL_NAME_MAP_PATH}`);
153
+ }
154
+ const raw = readFileSync(TOOL_NAME_MAP_PATH, "utf-8");
155
+ const parsed = parseYaml(raw);
156
+ if (!parsed.invocations) {
157
+ throw new Error(`[build-agents] tool-name-map.yml missing 'invocations' section`);
158
+ }
159
+ return parsed.invocations;
160
+ }
161
+ // ---------------------------------------------------------------------------
162
+ // Capability resolution helpers
163
+ // ---------------------------------------------------------------------------
164
+ /**
165
+ * Collect all Claude disallowedTools for an agent based on its capabilities[].
166
+ */
167
+ export function resolveClaudeDisallowedTools(capabilities, capMatrix) {
168
+ const tools = [];
169
+ for (const cap of capabilities) {
170
+ const entry = capMatrix.capabilities[cap];
171
+ if (!entry) {
172
+ throw new Error(`[build-agents] Unknown capability: ${cap}`);
173
+ }
174
+ if (entry.claude?.disallowedTools) {
175
+ for (const t of entry.claude.disallowedTools) {
176
+ if (!tools.includes(t))
177
+ tools.push(t);
178
+ }
179
+ }
180
+ }
181
+ return tools;
182
+ }
183
+ /**
184
+ * Collect merged OpenCode permission block for an agent.
185
+ */
186
+ export function resolveOpencodePermissions(capabilities, capMatrix) {
187
+ const perms = {};
188
+ for (const cap of capabilities) {
189
+ const entry = capMatrix.capabilities[cap];
190
+ if (!entry) {
191
+ throw new Error(`[build-agents] Unknown capability: ${cap}`);
192
+ }
193
+ if (entry.opencode?.permission) {
194
+ Object.assign(perms, entry.opencode.permission);
195
+ }
196
+ }
197
+ return perms;
198
+ }
199
+ /**
200
+ * Resolve codex sandbox_mode and disabled_tools for an agent.
201
+ * sandbox_mode: take the most restrictive non-null value ("read-only" wins).
202
+ */
203
+ export function resolveCodexConfig(capabilities, capMatrix) {
204
+ let sandboxMode = null;
205
+ const disabledTools = [];
206
+ for (const cap of capabilities) {
207
+ const entry = capMatrix.capabilities[cap];
208
+ if (!entry) {
209
+ throw new Error(`[build-agents] Unknown capability: ${cap}`);
210
+ }
211
+ if (entry.codex?.sandbox_mode) {
212
+ // "read-only" is the most restrictive
213
+ sandboxMode = entry.codex.sandbox_mode;
214
+ }
215
+ if (entry.codex?.disabled_tools) {
216
+ for (const t of entry.codex.disabled_tools) {
217
+ if (t && !disabledTools.includes(t))
218
+ disabledTools.push(t);
219
+ }
220
+ }
221
+ }
222
+ return { sandbox_mode: sandboxMode, disabled_tools: disabledTools };
223
+ }
224
+ /**
225
+ * Resolve model slug for a given model_tier and harness.
226
+ */
227
+ export function resolveModel(modelTier, harness, capMatrix) {
228
+ const tierEntry = capMatrix.model_tier[modelTier];
229
+ if (!tierEntry)
230
+ return null;
231
+ const val = tierEntry[harness];
232
+ return val === null || val === undefined ? null : val;
233
+ }
234
+ // ---------------------------------------------------------------------------
235
+ // Overwrite policy
236
+ // ---------------------------------------------------------------------------
237
+ /**
238
+ * Write a file according to the managed/template overwrite policy.
239
+ *
240
+ * Managed: always overwrite (unless --dry-run)
241
+ * Template: skip if exists (overwrite only with --force)
242
+ * --dry-run: record path, no write
243
+ * --strict: error if Managed path has untracked git modifications
244
+ */
245
+ export function applyOverwritePolicy(filePath, content, isManaged, opts) {
246
+ if (opts.dryRun) {
247
+ dryRunFiles.push(filePath);
248
+ return;
249
+ }
250
+ if (isManaged) {
251
+ if (opts.strict) {
252
+ // Check if the file is tracked by git and has local modifications
253
+ if (existsSync(filePath)) {
254
+ try {
255
+ const rel = filePath.startsWith(ROOT)
256
+ ? filePath.slice(ROOT.length + 1)
257
+ : filePath;
258
+ const result = execSync(`git status --short -- ${JSON.stringify(rel)}`, {
259
+ cwd: ROOT,
260
+ encoding: "utf-8",
261
+ stdio: ["pipe", "pipe", "pipe"],
262
+ }).trim();
263
+ if (result && !result.startsWith("?")) {
264
+ throw new Error(`[build-agents] --strict: managed file has untracked modifications: ${filePath}`);
265
+ }
266
+ }
267
+ catch (err) {
268
+ if (String(err).includes("--strict:"))
269
+ throw err;
270
+ // git not available or file not tracked — allow
271
+ }
272
+ }
273
+ }
274
+ mkdirSync(dirname(filePath), { recursive: true });
275
+ writeFileSync(filePath, content, "utf-8");
276
+ }
277
+ else {
278
+ // Template: skip if exists unless --force
279
+ if (existsSync(filePath) && !opts.force) {
280
+ return;
281
+ }
282
+ mkdirSync(dirname(filePath), { recursive: true });
283
+ writeFileSync(filePath, content, "utf-8");
284
+ }
285
+ }
286
+ // ---------------------------------------------------------------------------
287
+ // Harness: Claude
288
+ // ---------------------------------------------------------------------------
289
+ function claudeAgentMarkdown(asset, capMatrix, invocations) {
290
+ const fm = asset.frontmatter;
291
+ const disallowed = resolveClaudeDisallowedTools(fm.capabilities ?? [], capMatrix);
292
+ const model = resolveModel(fm.model_tier, "claude", capMatrix);
293
+ const fmLines = ["---"];
294
+ if (fm.description)
295
+ fmLines.push(`description: ${JSON.stringify(fm.description)}`);
296
+ if (model)
297
+ fmLines.push(`model: ${model}`);
298
+ if (disallowed.length > 0) {
299
+ fmLines.push(`disallowedTools:`);
300
+ for (const t of disallowed) {
301
+ fmLines.push(` - ${t}`);
302
+ }
303
+ }
304
+ fmLines.push("---");
305
+ fmLines.push("");
306
+ const expandedBody = expandInvocations(asset.body, "claude", invocations);
307
+ return fmLines.join("\n") + expandedBody;
308
+ }
309
+ function claudeSkillMarkdown(asset, invocations) {
310
+ const fm = asset.frontmatter;
311
+ const fmLines = ["---"];
312
+ if (fm.description)
313
+ fmLines.push(`description: ${JSON.stringify(fm.description)}`);
314
+ if (fm.triggers && fm.triggers.length > 0) {
315
+ fmLines.push(`triggers:`);
316
+ for (const t of fm.triggers) {
317
+ fmLines.push(` - ${t}`);
318
+ }
319
+ }
320
+ fmLines.push("---");
321
+ fmLines.push("");
322
+ const expandedBody = expandInvocations(asset.body, "claude", invocations);
323
+ return fmLines.join("\n") + expandedBody;
324
+ }
325
+ function buildPluginJson(agents) {
326
+ return JSON.stringify({
327
+ name: "claude-nexus",
328
+ version: "0.13.0",
329
+ description: "Nexus agent suite for Claude Code",
330
+ agents: agents.map((a) => ({
331
+ id: a.frontmatter.id,
332
+ name: a.frontmatter.name,
333
+ description: a.frontmatter.description,
334
+ file: `agents/${a.name}.md`,
335
+ })),
336
+ }, null, 2) + "\n";
337
+ }
338
+ function buildMarketplaceJson(agents) {
339
+ return JSON.stringify({
340
+ schema_version: "1.0",
341
+ agents: agents.map((a) => ({
342
+ id: a.frontmatter.id,
343
+ name: a.frontmatter.name,
344
+ description: a.frontmatter.description,
345
+ category: a.frontmatter.category,
346
+ model_tier: a.frontmatter.model_tier,
347
+ })),
348
+ }, null, 2) + "\n";
349
+ }
350
+ export function buildForClaude(assets, capMatrix, invocations, opts) {
351
+ const baseDir = join(opts.targetDir, "claude");
352
+ const agentAssets = assets.filter((a) => a.type === "agent");
353
+ const skillAssets = assets.filter((a) => a.type === "skill");
354
+ // Template files: .claude-plugin/plugin.json and marketplace.json
355
+ const pluginJsonPath = join(baseDir, ".claude-plugin", "plugin.json");
356
+ const marketplacePath = join(baseDir, ".claude-plugin", "marketplace.json");
357
+ applyOverwritePolicy(pluginJsonPath, buildPluginJson(agentAssets), false, opts);
358
+ applyOverwritePolicy(marketplacePath, buildMarketplaceJson(agentAssets), false, opts);
359
+ // Managed: agents/<n>.md
360
+ for (const agent of agentAssets) {
361
+ const outPath = join(baseDir, "agents", `${agent.name}.md`);
362
+ const content = claudeAgentMarkdown(agent, capMatrix, invocations);
363
+ applyOverwritePolicy(outPath, content, true, opts);
364
+ }
365
+ // Managed: skills/<n>/SKILL.md
366
+ for (const skill of skillAssets) {
367
+ const outPath = join(baseDir, "skills", skill.name, "SKILL.md");
368
+ const content = claudeSkillMarkdown(skill, invocations);
369
+ applyOverwritePolicy(outPath, content, true, opts);
370
+ }
371
+ // Managed: settings.json (primary agent injection)
372
+ const primaryAgents = agentAssets.filter((a) => (a.frontmatter.mode ?? "subagent") === "primary");
373
+ if (primaryAgents.length > 0) {
374
+ if (primaryAgents.length > 1) {
375
+ console.warn(`[build-agents] Warning: multiple primary agents found (${primaryAgents.map((a) => a.name).join(", ")}). Using first: ${primaryAgents[0].name}`);
376
+ }
377
+ const primaryAgent = primaryAgents[0];
378
+ const settingsPath = join(baseDir, "settings.json");
379
+ const settingsContent = JSON.stringify({ agent: primaryAgent.frontmatter.id }, null, 2) + "\n";
380
+ applyOverwritePolicy(settingsPath, settingsContent, true, opts);
381
+ }
382
+ }
383
+ // ---------------------------------------------------------------------------
384
+ // Harness: OpenCode
385
+ // ---------------------------------------------------------------------------
386
+ /**
387
+ * Generate OpenCode src/agents/<n>.ts content.
388
+ * Uses template literal inline. Backtick and ${ are escaped via string concatenation.
389
+ */
390
+ function opencodeAgentTs(asset, capMatrix, invocations) {
391
+ const fm = asset.frontmatter;
392
+ const perms = resolveOpencodePermissions(fm.capabilities ?? [], capMatrix);
393
+ const expandedBody = expandInvocations(asset.body, "opencode", invocations);
394
+ // Build permission block
395
+ const permEntries = Object.entries(perms);
396
+ const permBlock = permEntries.length > 0
397
+ ? ` permission: {\n${permEntries.map(([k, v]) => ` ${k}: "${v}",`).join("\n")}\n },`
398
+ : "";
399
+ // Escape content for embedding in a template literal
400
+ // We use string concatenation to avoid issues with backtick and ${ in the template
401
+ const escapedBody = expandedBody.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
402
+ const escapedDesc = fm.description.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
403
+ const lines = [
404
+ `// Auto-generated by build-agents.ts — do not edit`,
405
+ `// Source: assets/agents/${asset.name}/body.md`,
406
+ `import type { AgentConfig } from "opencode";`,
407
+ ``,
408
+ `export const ${camelCase(asset.name)}: AgentConfig = {`,
409
+ ` id: ${JSON.stringify(fm.id)},`,
410
+ ` name: ${JSON.stringify(fm.name)},`,
411
+ ` description: \`${escapedDesc}\`,`,
412
+ ];
413
+ if (permBlock)
414
+ lines.push(permBlock);
415
+ // Emit mode field only for primary agents (subagent is the OpenCode default)
416
+ if (fm.mode === "primary") {
417
+ lines.push(` mode: "primary",`);
418
+ }
419
+ lines.push(` system: \`${escapedBody}\`,`, `};`, ``);
420
+ return lines.join("\n");
421
+ }
422
+ function opencodeIndexTs(agents) {
423
+ const imports = agents
424
+ .map((a) => `import { ${camelCase(a.name)} } from "./agents/${a.name}.js";`)
425
+ .join("\n");
426
+ const exports = `export const agents = [\n${agents.map((a) => ` ${camelCase(a.name)},`).join("\n")}\n];`;
427
+ return [
428
+ `// Auto-generated by build-agents.ts — do not edit`,
429
+ ``,
430
+ imports,
431
+ ``,
432
+ exports,
433
+ ``,
434
+ ].join("\n");
435
+ }
436
+ function opencodePackageJson(agents) {
437
+ return (JSON.stringify({
438
+ name: "opencode-nexus",
439
+ version: "0.13.0",
440
+ description: "Nexus agent suite for OpenCode",
441
+ type: "module",
442
+ main: "./src/index.ts",
443
+ exports: {
444
+ ".": "./src/index.ts",
445
+ },
446
+ peerDependencies: {
447
+ opencode: "*",
448
+ },
449
+ }, null, 2) + "\n");
450
+ }
451
+ function opencodeJsonFragment(agents) {
452
+ // Fragment to be merged into opencode.json
453
+ return (JSON.stringify({
454
+ agents: agents.map((a) => ({
455
+ id: a.frontmatter.id,
456
+ module: `./src/agents/${a.name}.js`,
457
+ })),
458
+ }, null, 2) + "\n");
459
+ }
460
+ function opencodeSkillMarkdown(asset, invocations) {
461
+ const fm = asset.frontmatter;
462
+ const fmLines = ["---"];
463
+ if (fm.description)
464
+ fmLines.push(`description: ${JSON.stringify(fm.description)}`);
465
+ if (fm.triggers && fm.triggers.length > 0) {
466
+ fmLines.push(`triggers:`);
467
+ for (const t of fm.triggers) {
468
+ fmLines.push(` - ${t}`);
469
+ }
470
+ }
471
+ fmLines.push("---");
472
+ fmLines.push("");
473
+ const expandedBody = expandInvocations(asset.body, "opencode", invocations);
474
+ return fmLines.join("\n") + expandedBody;
475
+ }
476
+ export function buildForOpencode(assets, capMatrix, invocations, opts) {
477
+ const baseDir = join(opts.targetDir, "opencode");
478
+ const agentAssets = assets.filter((a) => a.type === "agent");
479
+ const skillAssets = assets.filter((a) => a.type === "skill");
480
+ // Template: package.json
481
+ const pkgPath = join(baseDir, "package.json");
482
+ applyOverwritePolicy(pkgPath, opencodePackageJson(agentAssets), false, opts);
483
+ // Managed: opencode.json.fragment
484
+ const fragmentPath = join(baseDir, "opencode.json.fragment");
485
+ applyOverwritePolicy(fragmentPath, opencodeJsonFragment(agentAssets), true, opts);
486
+ // Managed: src/index.ts
487
+ const indexPath = join(baseDir, "src", "index.ts");
488
+ applyOverwritePolicy(indexPath, opencodeIndexTs(agentAssets), true, opts);
489
+ // Managed: src/agents/<n>.ts
490
+ for (const agent of agentAssets) {
491
+ const outPath = join(baseDir, "src", "agents", `${agent.name}.ts`);
492
+ const content = opencodeAgentTs(agent, capMatrix, invocations);
493
+ applyOverwritePolicy(outPath, content, true, opts);
494
+ }
495
+ // Managed: .opencode/skills/<n>/SKILL.md
496
+ for (const skill of skillAssets) {
497
+ const outPath = join(baseDir, ".opencode", "skills", skill.name, "SKILL.md");
498
+ const content = opencodeSkillMarkdown(skill, invocations);
499
+ applyOverwritePolicy(outPath, content, true, opts);
500
+ }
501
+ }
502
+ // ---------------------------------------------------------------------------
503
+ // Harness: Codex
504
+ // ---------------------------------------------------------------------------
505
+ /**
506
+ * Escape a string for TOML multi-line literal string (''' ''').
507
+ * Literal strings do not allow escapes, so we must not include '''.
508
+ * Fallback: use basic multi-line string with minimal escaping.
509
+ */
510
+ function tomlMultilineString(value) {
511
+ // Use basic multi-line string: """ ... """
512
+ // Escape backslash and double-quote sequences
513
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
514
+ return `"""\n${escaped}\n"""`;
515
+ }
516
+ function codexAgentToml(asset, capMatrix, invocations) {
517
+ const fm = asset.frontmatter;
518
+ const { sandbox_mode, disabled_tools } = resolveCodexConfig(fm.capabilities ?? [], capMatrix);
519
+ const model = resolveModel(fm.model_tier, "codex", capMatrix);
520
+ const expandedBody = expandInvocations(asset.body, "codex", invocations);
521
+ const lines = [
522
+ `# Auto-generated by build-agents.ts — do not edit`,
523
+ `# Source: assets/agents/${asset.name}/body.md`,
524
+ ``,
525
+ `[agents.${fm.id}]`,
526
+ `description = ${JSON.stringify(fm.description)}`,
527
+ ];
528
+ if (model)
529
+ lines.push(`model = ${JSON.stringify(model)}`);
530
+ if (sandbox_mode)
531
+ lines.push(`sandbox_mode = ${JSON.stringify(sandbox_mode)}`);
532
+ if (disabled_tools.length > 0) {
533
+ lines.push(`disabled_tools = [${disabled_tools.map((t) => JSON.stringify(t)).join(", ")}]`);
534
+ }
535
+ lines.push(``, `[agents.${fm.id}.system]`, `content = ${tomlMultilineString(expandedBody)}`, ``);
536
+ return lines.join("\n");
537
+ }
538
+ function codexPromptMarkdown(asset, invocations) {
539
+ const expandedBody = expandInvocations(asset.body, "codex", invocations);
540
+ const fm = asset.frontmatter;
541
+ return [
542
+ `---`,
543
+ `name: ${JSON.stringify(fm.name)}`,
544
+ `description: ${JSON.stringify(fm.description)}`,
545
+ `---`,
546
+ ``,
547
+ expandedBody,
548
+ ].join("\n");
549
+ }
550
+ function codexSkillMarkdown(asset, invocations) {
551
+ const fm = asset.frontmatter;
552
+ const fmLines = ["---"];
553
+ if (fm.description)
554
+ fmLines.push(`description: ${JSON.stringify(fm.description)}`);
555
+ if (fm.triggers && fm.triggers.length > 0) {
556
+ fmLines.push(`triggers:`);
557
+ for (const t of fm.triggers) {
558
+ fmLines.push(` - ${t}`);
559
+ }
560
+ }
561
+ fmLines.push("---");
562
+ fmLines.push("");
563
+ const expandedBody = expandInvocations(asset.body, "codex", invocations);
564
+ return fmLines.join("\n") + expandedBody;
565
+ }
566
+ function codexPluginJson(agents) {
567
+ return (JSON.stringify({
568
+ name: "codex-nexus",
569
+ version: "0.13.0",
570
+ description: "Nexus agent suite for Codex",
571
+ agents: agents.map((a) => ({
572
+ id: a.frontmatter.id,
573
+ config: `agents/${a.name}.toml`,
574
+ prompt: `prompts/${a.name}.md`,
575
+ })),
576
+ }, null, 2) + "\n");
577
+ }
578
+ function codexConfigFragment(agents) {
579
+ const lines = [
580
+ `# Auto-generated by build-agents.ts — do not edit`,
581
+ `# Merge this fragment into your codex config.toml`,
582
+ ``,
583
+ `[mcp_servers.nx]`,
584
+ `command = "nexus-mcp"`,
585
+ ``,
586
+ ];
587
+ return lines.join("\n");
588
+ }
589
+ export function buildForCodex(assets, capMatrix, invocations, opts) {
590
+ const baseDir = join(opts.targetDir, "codex");
591
+ const agentAssets = assets.filter((a) => a.type === "agent");
592
+ const skillAssets = assets.filter((a) => a.type === "skill");
593
+ // Managed: plugin/.codex-plugin/plugin.json
594
+ const pluginJsonPath = join(baseDir, "plugin", ".codex-plugin", "plugin.json");
595
+ applyOverwritePolicy(pluginJsonPath, codexPluginJson(agentAssets), true, opts);
596
+ // Managed: plugin/skills/<n>/SKILL.md
597
+ for (const skill of skillAssets) {
598
+ const outPath = join(baseDir, "plugin", "skills", skill.name, "SKILL.md");
599
+ const content = codexSkillMarkdown(skill, invocations);
600
+ applyOverwritePolicy(outPath, content, true, opts);
601
+ }
602
+ // Managed: agents/<n>.toml
603
+ for (const agent of agentAssets) {
604
+ const outPath = join(baseDir, "agents", `${agent.name}.toml`);
605
+ const content = codexAgentToml(agent, capMatrix, invocations);
606
+ applyOverwritePolicy(outPath, content, true, opts);
607
+ }
608
+ // Managed: prompts/<n>.md
609
+ for (const agent of agentAssets) {
610
+ const outPath = join(baseDir, "prompts", `${agent.name}.md`);
611
+ const content = codexPromptMarkdown(agent, invocations);
612
+ applyOverwritePolicy(outPath, content, true, opts);
613
+ }
614
+ // Managed: install/config.fragment.toml
615
+ const fragmentPath = join(baseDir, "install", "config.fragment.toml");
616
+ applyOverwritePolicy(fragmentPath, codexConfigFragment(agentAssets), true, opts);
617
+ // Managed: install/AGENTS.fragment.md (primary agents only)
618
+ const primaryAgents = agentAssets.filter((a) => (a.frontmatter.mode ?? "subagent") === "primary");
619
+ if (primaryAgents.length > 0) {
620
+ const agentsFragmentPath = join(baseDir, "install", "AGENTS.fragment.md");
621
+ const blocks = primaryAgents.map((agent) => {
622
+ const expandedBody = expandInvocations(agent.body, "codex", invocations);
623
+ return [
624
+ `<!-- nexus-core:${agent.frontmatter.id}:start -->`,
625
+ `# ${agent.frontmatter.name}`,
626
+ ``,
627
+ expandedBody,
628
+ `<!-- nexus-core:${agent.frontmatter.id}:end -->`,
629
+ ].join("\n");
630
+ });
631
+ const agentsFragmentContent = blocks.join("\n\n") + "\n";
632
+ applyOverwritePolicy(agentsFragmentPath, agentsFragmentContent, true, opts);
633
+ }
634
+ }
635
+ // ---------------------------------------------------------------------------
636
+ // Utilities
637
+ // ---------------------------------------------------------------------------
638
+ function camelCase(str) {
639
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
640
+ }
641
+ // ---------------------------------------------------------------------------
642
+ // CLI arg parsing
643
+ // ---------------------------------------------------------------------------
644
+ export function parseArgs(argv) {
645
+ const args = argv.slice(2); // remove node and script path
646
+ let harnesses = [...HARNESSES];
647
+ let targetDir = join(ROOT, "dist");
648
+ let dryRun = false;
649
+ let force = false;
650
+ let strict = false;
651
+ let only;
652
+ for (const arg of args) {
653
+ if (arg.startsWith("--harness=")) {
654
+ const val = arg.slice("--harness=".length);
655
+ if (!HARNESSES.includes(val)) {
656
+ throw new Error(`[build-agents] Unknown harness: ${val}. Valid: ${HARNESSES.join(", ")}`);
657
+ }
658
+ harnesses = [val];
659
+ }
660
+ else if (arg.startsWith("--target=")) {
661
+ targetDir = resolve(arg.slice("--target=".length));
662
+ }
663
+ else if (arg === "--dry-run") {
664
+ dryRun = true;
665
+ }
666
+ else if (arg === "--force") {
667
+ force = true;
668
+ }
669
+ else if (arg === "--strict") {
670
+ strict = true;
671
+ }
672
+ else if (arg.startsWith("--only=")) {
673
+ only = arg.slice("--only=".length);
674
+ }
675
+ }
676
+ return { harnesses, targetDir, dryRun, force, strict, only };
677
+ }
678
+ // ---------------------------------------------------------------------------
679
+ // Main
680
+ // ---------------------------------------------------------------------------
681
+ export async function buildAgents(opts) {
682
+ // Stage 1: Load assets
683
+ const assets = loadAssets({ only: opts.only });
684
+ const agentCount = assets.filter((a) => a.type === "agent").length;
685
+ const skillCount = assets.filter((a) => a.type === "skill").length;
686
+ console.log(`[build-agents] Loaded ${agentCount} agents, ${skillCount} skills`);
687
+ // Stage 2: Load capability matrix
688
+ const capMatrix = loadCapabilityMatrix();
689
+ // Validate all capability IDs referenced by assets
690
+ const knownCapIds = new Set(Object.keys(capMatrix.capabilities));
691
+ for (const asset of assets) {
692
+ for (const cap of asset.frontmatter.capabilities ?? []) {
693
+ if (!knownCapIds.has(cap)) {
694
+ throw new Error(`[build-agents] "${asset.name}" references unknown capability: "${cap}". ` +
695
+ `Known: ${[...knownCapIds].join(", ")}`);
696
+ }
697
+ }
698
+ }
699
+ // Stage 3: Load invocations
700
+ const invocations = loadInvocations();
701
+ // Stage 4: Build per harness
702
+ if (opts.dryRun) {
703
+ console.log(`[build-agents] --dry-run mode: listing affected files only`);
704
+ }
705
+ for (const harness of opts.harnesses) {
706
+ console.log(`[build-agents] Building for harness: ${harness}`);
707
+ if (harness === "claude") {
708
+ buildForClaude(assets, capMatrix, invocations, opts);
709
+ }
710
+ else if (harness === "opencode") {
711
+ buildForOpencode(assets, capMatrix, invocations, opts);
712
+ }
713
+ else if (harness === "codex") {
714
+ buildForCodex(assets, capMatrix, invocations, opts);
715
+ }
716
+ }
717
+ if (opts.dryRun) {
718
+ console.log(`[build-agents] Affected files (${dryRunFiles.length}):`);
719
+ for (const f of dryRunFiles) {
720
+ console.log(` ${f}`);
721
+ }
722
+ // Clear for next run
723
+ dryRunFiles.length = 0;
724
+ return;
725
+ }
726
+ console.log(`[build-agents] Done`);
727
+ }
728
+ // Run when executed directly
729
+ if (import.meta.url === `file://${process.argv[1]}` ||
730
+ process.argv[1]?.endsWith("build-agents.ts") ||
731
+ process.argv[1]?.endsWith("build-agents.js")) {
732
+ const opts = parseArgs(process.argv);
733
+ buildAgents(opts).catch((err) => {
734
+ process.stderr.write(`[build-agents] FATAL: ${String(err)}\n`);
735
+ process.exit(1);
736
+ });
737
+ }
738
+ //# sourceMappingURL=build-agents.js.map