@moreih29/nexus-core 0.12.0 → 0.13.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.
Files changed (210) hide show
  1. package/README.md +48 -63
  2. package/assets/agents/architect/body.ko.md +177 -0
  3. package/{agents → assets/agents}/architect/body.md +16 -0
  4. package/assets/agents/designer/body.ko.md +125 -0
  5. package/{agents → assets/agents}/designer/body.md +16 -0
  6. package/assets/agents/engineer/body.ko.md +106 -0
  7. package/{agents → assets/agents}/engineer/body.md +14 -0
  8. package/assets/agents/lead/body.ko.md +70 -0
  9. package/assets/agents/lead/body.md +70 -0
  10. package/assets/agents/postdoc/body.ko.md +122 -0
  11. package/{agents → assets/agents}/postdoc/body.md +16 -0
  12. package/assets/agents/researcher/body.ko.md +137 -0
  13. package/{agents → assets/agents}/researcher/body.md +15 -0
  14. package/assets/agents/reviewer/body.ko.md +138 -0
  15. package/{agents → assets/agents}/reviewer/body.md +15 -0
  16. package/assets/agents/strategist/body.ko.md +116 -0
  17. package/{agents → assets/agents}/strategist/body.md +16 -0
  18. package/assets/agents/tester/body.ko.md +195 -0
  19. package/{agents → assets/agents}/tester/body.md +15 -0
  20. package/assets/agents/writer/body.ko.md +122 -0
  21. package/{agents → assets/agents}/writer/body.md +14 -0
  22. package/assets/capability-matrix.yml +198 -0
  23. package/assets/hooks/agent-bootstrap/handler.test.ts +368 -0
  24. package/assets/hooks/agent-bootstrap/handler.ts +119 -0
  25. package/assets/hooks/agent-bootstrap/meta.yml +10 -0
  26. package/assets/hooks/agent-finalize/handler.test.ts +368 -0
  27. package/assets/hooks/agent-finalize/handler.ts +76 -0
  28. package/assets/hooks/agent-finalize/meta.yml +10 -0
  29. package/assets/hooks/capability-matrix.yml +313 -0
  30. package/assets/hooks/post-tool-telemetry/handler.test.ts +302 -0
  31. package/assets/hooks/post-tool-telemetry/handler.ts +49 -0
  32. package/assets/hooks/post-tool-telemetry/meta.yml +11 -0
  33. package/assets/hooks/prompt-router/handler.test.ts +801 -0
  34. package/assets/hooks/prompt-router/handler.ts +261 -0
  35. package/assets/hooks/prompt-router/meta.yml +11 -0
  36. package/assets/hooks/session-init/handler.test.ts +274 -0
  37. package/assets/hooks/session-init/handler.ts +30 -0
  38. package/assets/hooks/session-init/meta.yml +9 -0
  39. package/assets/lsp-servers.json +55 -0
  40. package/assets/schema/lsp-servers.schema.json +67 -0
  41. package/assets/skills/nx-init/body.ko.md +197 -0
  42. package/{skills → assets/skills}/nx-init/body.md +11 -0
  43. package/assets/skills/nx-plan/body.ko.md +361 -0
  44. package/{skills → assets/skills}/nx-plan/body.md +13 -0
  45. package/assets/skills/nx-run/body.ko.md +161 -0
  46. package/{skills → assets/skills}/nx-run/body.md +11 -0
  47. package/assets/skills/nx-sync/body.ko.md +92 -0
  48. package/{skills → assets/skills}/nx-sync/body.md +10 -0
  49. package/assets/tools/tool-name-map.yml +353 -0
  50. package/dist/hooks/opencode-mount.d.ts +35 -0
  51. package/dist/hooks/opencode-mount.d.ts.map +1 -0
  52. package/dist/hooks/opencode-mount.js +332 -0
  53. package/dist/hooks/opencode-mount.js.map +1 -0
  54. package/dist/hooks/runtime.d.ts +37 -0
  55. package/dist/hooks/runtime.d.ts.map +1 -0
  56. package/dist/hooks/runtime.js +274 -0
  57. package/dist/hooks/runtime.js.map +1 -0
  58. package/dist/hooks/types.d.ts +196 -0
  59. package/dist/hooks/types.d.ts.map +1 -0
  60. package/dist/hooks/types.js +85 -0
  61. package/dist/hooks/types.js.map +1 -0
  62. package/dist/lsp/cache.d.ts +9 -0
  63. package/dist/lsp/cache.d.ts.map +1 -0
  64. package/dist/lsp/cache.js +216 -0
  65. package/dist/lsp/cache.js.map +1 -0
  66. package/dist/lsp/client.d.ts +24 -0
  67. package/dist/lsp/client.d.ts.map +1 -0
  68. package/dist/lsp/client.js +166 -0
  69. package/dist/lsp/client.js.map +1 -0
  70. package/dist/lsp/detect.d.ts +77 -0
  71. package/dist/lsp/detect.d.ts.map +1 -0
  72. package/dist/lsp/detect.js +116 -0
  73. package/dist/lsp/detect.js.map +1 -0
  74. package/dist/mcp/server.d.ts +5 -0
  75. package/dist/mcp/server.d.ts.map +1 -0
  76. package/dist/mcp/server.js +34 -0
  77. package/dist/mcp/server.js.map +1 -0
  78. package/dist/mcp/tools/artifact.d.ts +4 -0
  79. package/dist/mcp/tools/artifact.d.ts.map +1 -0
  80. package/dist/mcp/tools/artifact.js +36 -0
  81. package/dist/mcp/tools/artifact.js.map +1 -0
  82. package/dist/mcp/tools/history.d.ts +3 -0
  83. package/dist/mcp/tools/history.d.ts.map +1 -0
  84. package/dist/mcp/tools/history.js +29 -0
  85. package/dist/mcp/tools/history.js.map +1 -0
  86. package/dist/mcp/tools/lsp.d.ts +13 -0
  87. package/dist/mcp/tools/lsp.d.ts.map +1 -0
  88. package/dist/mcp/tools/lsp.js +225 -0
  89. package/dist/mcp/tools/lsp.js.map +1 -0
  90. package/dist/mcp/tools/plan.d.ts +3 -0
  91. package/dist/mcp/tools/plan.d.ts.map +1 -0
  92. package/dist/mcp/tools/plan.js +317 -0
  93. package/dist/mcp/tools/plan.js.map +1 -0
  94. package/dist/mcp/tools/task.d.ts +3 -0
  95. package/dist/mcp/tools/task.d.ts.map +1 -0
  96. package/dist/mcp/tools/task.js +252 -0
  97. package/dist/mcp/tools/task.js.map +1 -0
  98. package/dist/shared/invocations.d.ts +74 -0
  99. package/dist/shared/invocations.d.ts.map +1 -0
  100. package/dist/shared/invocations.js +247 -0
  101. package/dist/shared/invocations.js.map +1 -0
  102. package/dist/shared/json-store.d.ts +37 -0
  103. package/dist/shared/json-store.d.ts.map +1 -0
  104. package/dist/shared/json-store.js +163 -0
  105. package/dist/shared/json-store.js.map +1 -0
  106. package/dist/shared/mcp-utils.d.ts +3 -0
  107. package/dist/shared/mcp-utils.d.ts.map +1 -0
  108. package/dist/shared/mcp-utils.js +6 -0
  109. package/dist/shared/mcp-utils.js.map +1 -0
  110. package/dist/shared/paths.d.ts +21 -0
  111. package/dist/shared/paths.d.ts.map +1 -0
  112. package/dist/shared/paths.js +81 -0
  113. package/dist/shared/paths.js.map +1 -0
  114. package/dist/shared/tool-log.d.ts +8 -0
  115. package/dist/shared/tool-log.d.ts.map +1 -0
  116. package/dist/shared/tool-log.js +22 -0
  117. package/dist/shared/tool-log.js.map +1 -0
  118. package/dist/types/state.d.ts +862 -0
  119. package/dist/types/state.d.ts.map +1 -0
  120. package/dist/types/state.js +66 -0
  121. package/dist/types/state.js.map +1 -0
  122. package/docs/consuming/codex-lead-merge.md +106 -0
  123. package/docs/plugin-guide.md +360 -0
  124. package/docs/plugin-template/claude/.github/workflows/build.yml +60 -0
  125. package/docs/plugin-template/claude/README.md +110 -0
  126. package/docs/plugin-template/claude/package.json +16 -0
  127. package/docs/plugin-template/codex/.github/workflows/build.yml +51 -0
  128. package/docs/plugin-template/codex/README.md +147 -0
  129. package/docs/plugin-template/codex/package.json +17 -0
  130. package/docs/plugin-template/opencode/.github/workflows/build.yml +61 -0
  131. package/docs/plugin-template/opencode/README.md +121 -0
  132. package/docs/plugin-template/opencode/package.json +25 -0
  133. package/package.json +21 -21
  134. package/scripts/build-agents.test.ts +1279 -0
  135. package/scripts/build-agents.ts +978 -0
  136. package/scripts/build-hooks.test.ts +1385 -0
  137. package/scripts/build-hooks.ts +584 -0
  138. package/scripts/cli.test.ts +367 -0
  139. package/scripts/cli.ts +547 -0
  140. package/agents/architect/meta.yml +0 -13
  141. package/agents/designer/meta.yml +0 -13
  142. package/agents/engineer/meta.yml +0 -11
  143. package/agents/postdoc/meta.yml +0 -13
  144. package/agents/researcher/meta.yml +0 -12
  145. package/agents/reviewer/meta.yml +0 -12
  146. package/agents/strategist/meta.yml +0 -13
  147. package/agents/tester/meta.yml +0 -12
  148. package/agents/writer/meta.yml +0 -11
  149. package/conformance/README.md +0 -311
  150. package/conformance/examples/plan.extension.schema.example.json +0 -25
  151. package/conformance/lifecycle/README.md +0 -48
  152. package/conformance/lifecycle/agent-complete.json +0 -44
  153. package/conformance/lifecycle/agent-resume.json +0 -43
  154. package/conformance/lifecycle/agent-spawn.json +0 -36
  155. package/conformance/lifecycle/memory-access-record.json +0 -27
  156. package/conformance/lifecycle/session-end.json +0 -48
  157. package/conformance/scenarios/full-plan-cycle.json +0 -147
  158. package/conformance/scenarios/task-deps-ordering.json +0 -95
  159. package/conformance/schema/fixture.schema.json +0 -354
  160. package/conformance/state-schemas/agent-tracker.schema.json +0 -63
  161. package/conformance/state-schemas/history.schema.json +0 -134
  162. package/conformance/state-schemas/memory-access.schema.json +0 -36
  163. package/conformance/state-schemas/plan.schema.json +0 -77
  164. package/conformance/state-schemas/tasks.schema.json +0 -98
  165. package/conformance/tools/artifact-write.json +0 -97
  166. package/conformance/tools/context.json +0 -172
  167. package/conformance/tools/history-search.json +0 -219
  168. package/conformance/tools/plan-decide.json +0 -139
  169. package/conformance/tools/plan-start.json +0 -81
  170. package/conformance/tools/plan-status.json +0 -127
  171. package/conformance/tools/plan-update.json +0 -341
  172. package/conformance/tools/task-add.json +0 -156
  173. package/conformance/tools/task-close.json +0 -161
  174. package/conformance/tools/task-list.json +0 -177
  175. package/conformance/tools/task-update.json +0 -167
  176. package/docs/behavioral-contracts.md +0 -145
  177. package/docs/consumer-implementation-guide.md +0 -840
  178. package/docs/memory-lifecycle-contract.md +0 -119
  179. package/docs/nexus-layout.md +0 -224
  180. package/docs/nexus-outputs-contract.md +0 -344
  181. package/docs/nexus-state-overview.md +0 -170
  182. package/docs/nexus-tools-contract.md +0 -438
  183. package/manifest.json +0 -448
  184. package/schema/README.md +0 -69
  185. package/schema/agent.schema.json +0 -23
  186. package/schema/common.schema.json +0 -17
  187. package/schema/manifest.schema.json +0 -78
  188. package/schema/memory-policy.schema.json +0 -98
  189. package/schema/skill.schema.json +0 -54
  190. package/schema/task-exceptions.schema.json +0 -40
  191. package/schema/vocabulary.schema.json +0 -167
  192. package/scripts/.gitkeep +0 -0
  193. package/scripts/conformance-coverage.ts +0 -466
  194. package/scripts/import-from-claude-nexus.ts +0 -403
  195. package/scripts/lib/frontmatter.ts +0 -71
  196. package/scripts/lib/lint.ts +0 -348
  197. package/scripts/lib/structure.ts +0 -159
  198. package/scripts/lib/validate.ts +0 -796
  199. package/scripts/validate.ts +0 -90
  200. package/skills/nx-init/meta.yml +0 -8
  201. package/skills/nx-plan/meta.yml +0 -10
  202. package/skills/nx-run/meta.yml +0 -8
  203. package/skills/nx-sync/meta.yml +0 -7
  204. package/vocabulary/capabilities.yml +0 -65
  205. package/vocabulary/categories.yml +0 -11
  206. package/vocabulary/invocations.yml +0 -147
  207. package/vocabulary/memory_policy.yml +0 -88
  208. package/vocabulary/resume-tiers.yml +0 -11
  209. package/vocabulary/tags.yml +0 -60
  210. package/vocabulary/task-exceptions.yml +0 -29
@@ -1,403 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import path from 'node:path';
4
- import { readFile, writeFile, mkdir, readdir, rm, rename, stat } from 'node:fs/promises';
5
- import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
6
- import { execSync } from 'node:child_process';
7
- import { glob } from 'tinyglobby';
8
- import { parseFrontmatter } from './lib/frontmatter.ts';
9
-
10
- // Neutral layer allowed fields (bridge §2.1)
11
- const AGENT_ALLOW_FIELDS = new Set([
12
- 'id', 'name', 'alias_ko', 'description', 'task',
13
- 'category', 'capabilities', 'resume_tier', 'model_tier',
14
- ]);
15
- const SKILL_ALLOW_FIELDS = new Set([
16
- 'id', 'name', 'description', 'triggers', 'alias_ko', 'manual_only',
17
- ]);
18
-
19
- // Explicitly known drop list (warn + drop)
20
- const AGENT_DROP_FIELDS = new Set(['maxTurns', 'tags', 'disallowedTools', 'model']);
21
- // 'disable-model-invocation' is NOT in this set — it is mapped to manual_only below.
22
- const SKILL_DROP_FIELDS = new Set(['trigger_display', 'purpose']);
23
-
24
- // model name → model_tier abstraction
25
- const MODEL_TIER_MAP: Record<string, 'high' | 'standard'> = {
26
- opus: 'high',
27
- sonnet: 'standard',
28
- haiku: 'standard',
29
- };
30
-
31
- interface ImportOptions {
32
- source: string;
33
- apply: boolean;
34
- agentsOnly: boolean;
35
- skillsOnly: boolean;
36
- }
37
-
38
- function parseCli(): ImportOptions {
39
- const args = process.argv.slice(2);
40
- let source = process.env['NEXUS_CLAUDE_NEXUS_PATH'] ?? path.resolve('..', 'claude-nexus');
41
- let apply = false;
42
- let agentsOnly = false;
43
- let skillsOnly = false;
44
- for (let i = 0; i < args.length; i++) {
45
- if (args[i] === '--source' && args[i + 1]) { source = path.resolve(args[++i]!); }
46
- else if (args[i] === '--apply') { apply = true; }
47
- else if (args[i] === '--agents-only') { agentsOnly = true; }
48
- else if (args[i] === '--skills-only') { skillsOnly = true; }
49
- }
50
- if (agentsOnly && skillsOnly) {
51
- throw new Error('--agents-only and --skills-only are mutually exclusive');
52
- }
53
- return { source, apply, agentsOnly, skillsOnly };
54
- }
55
-
56
- async function verifySourceIsClaudeNexus(source: string): Promise<void> {
57
- const pkgPath = path.join(source, 'package.json');
58
- try {
59
- const pkg = JSON.parse(await readFile(pkgPath, 'utf8')) as { name?: string };
60
- if (pkg.name !== 'claude-nexus') {
61
- throw new Error(`source package.json.name='${pkg.name}', expected 'claude-nexus'`);
62
- }
63
- } catch (err) {
64
- throw new Error(`Failed to verify source at ${source}: ${(err as Error).message}`);
65
- }
66
- }
67
-
68
- function gitWorkingTreeClean(root: string, scopes: string[]): boolean {
69
- try {
70
- const out = execSync(`git status --porcelain ${scopes.join(' ')}`, { cwd: root, encoding: 'utf8' });
71
- return out.trim().length === 0;
72
- } catch {
73
- return true; // directory not yet created, treat as clean
74
- }
75
- }
76
-
77
- // ---- Body transformation ----
78
- // <role>...</role> → ## Role ... (top-level section marker only)
79
- // <constraints>...</constraints> → ## Constraints ...
80
- // <guidelines>...</guidelines> → ## Guidelines ...
81
- // inline <example>, <thinking> etc. sub-XML blocks are preserved
82
- function transformBody(rawBody: string): string {
83
- const sections: Array<[RegExp, string]> = [
84
- [/<role>\s*\n?/g, '## Role\n\n'],
85
- [/<\/role>\s*\n?/g, '\n\n'],
86
- [/<constraints>\s*\n?/g, '## Constraints\n\n'],
87
- [/<\/constraints>\s*\n?/g, '\n\n'],
88
- [/<guidelines>\s*\n?/g, '## Guidelines\n\n'],
89
- [/<\/guidelines>\s*\n?/g, '\n\n'],
90
- ];
91
- let result = rawBody;
92
- for (const [re, replacement] of sections) {
93
- result = result.replace(re, replacement);
94
- }
95
- // Trim excessive blank lines
96
- return result.replace(/\n{3,}/g, '\n\n').trim() + '\n';
97
- }
98
-
99
- // ---- Capability reverse mapping ----
100
- // disallowedTools → capabilities reverse mapping
101
- interface CapabilityEntry {
102
- id: string;
103
- harness_mapping: {
104
- claude_code: string[];
105
- opencode: string[];
106
- };
107
- }
108
-
109
- interface CapabilitiesFile {
110
- capabilities: CapabilityEntry[];
111
- }
112
-
113
- async function loadCapabilityMap(root: string): Promise<Map<string, string>> {
114
- const raw = await readFile(path.join(root, 'vocabulary/capabilities.yml'), 'utf8');
115
- const caps = parseYaml(raw) as CapabilitiesFile;
116
- const map = new Map<string, string>();
117
- for (const cap of caps.capabilities) {
118
- for (const tool of cap.harness_mapping.claude_code) {
119
- map.set(tool, cap.id);
120
- }
121
- for (const tool of cap.harness_mapping.opencode) {
122
- map.set(tool, cap.id);
123
- }
124
- }
125
- return map;
126
- }
127
-
128
- function reverseMapTools(disallowedTools: string[], capMap: Map<string, string>): string[] {
129
- const capabilitySet = new Set<string>();
130
- for (const tool of disallowedTools) {
131
- const cap = capMap.get(tool);
132
- if (cap) capabilitySet.add(cap);
133
- }
134
- return Array.from(capabilitySet).sort();
135
- }
136
-
137
- // ---- Agent transformation ----
138
- interface TransformedAgent {
139
- id: string;
140
- meta: Record<string, unknown>;
141
- body: string;
142
- }
143
-
144
- async function transformAgent(
145
- sourcePath: string,
146
- capMap: Map<string, string>,
147
- warnings: string[]
148
- ): Promise<TransformedAgent | null> {
149
- const source = await readFile(sourcePath, 'utf8');
150
- const { data, content } = parseFrontmatter(source);
151
- const fileName = path.basename(sourcePath, '.md');
152
-
153
- const meta: Record<string, unknown> = {};
154
- for (const [key, value] of Object.entries(data)) {
155
- if (AGENT_ALLOW_FIELDS.has(key)) {
156
- if (key === 'capabilities') continue; // will be computed from disallowedTools
157
- meta[key] = value;
158
- } else if (AGENT_DROP_FIELDS.has(key)) {
159
- warnings.push(` ${fileName}: dropping known field '${key}'`);
160
- } else {
161
- throw new Error(`${fileName}: unknown field '${key}' — add to allow list or drop list`);
162
- }
163
- }
164
-
165
- // model → model_tier
166
- if (typeof data['model'] === 'string') {
167
- const tier = MODEL_TIER_MAP[data['model']];
168
- if (!tier) throw new Error(`${fileName}: unknown model '${data['model']}'`);
169
- meta['model_tier'] = tier;
170
- }
171
-
172
- // disallowedTools → capabilities
173
- const disallowed = Array.isArray(data['disallowedTools']) ? data['disallowedTools'] : [];
174
- meta['capabilities'] = reverseMapTools(disallowed as string[], capMap);
175
-
176
- // Ensure id present (use filename if not in frontmatter)
177
- if (!meta['id']) meta['id'] = fileName;
178
-
179
- // name default to id
180
- if (!meta['name']) meta['name'] = meta['id'];
181
-
182
- const body = transformBody(content);
183
-
184
- return { id: meta['id'] as string, meta, body };
185
- }
186
-
187
- // ---- Skill transformation ----
188
- interface TransformedSkill {
189
- id: string;
190
- meta: Record<string, unknown>;
191
- body: string;
192
- }
193
-
194
- async function transformSkill(
195
- sourceDir: string,
196
- warnings: string[]
197
- ): Promise<TransformedSkill | null> {
198
- const skillFile = path.join(sourceDir, 'SKILL.md');
199
- const source = await readFile(skillFile, 'utf8');
200
- const { data, content } = parseFrontmatter(source);
201
- const skillId = path.basename(sourceDir);
202
-
203
- const meta: Record<string, unknown> = {};
204
- for (const [key, value] of Object.entries(data)) {
205
- if (SKILL_ALLOW_FIELDS.has(key)) {
206
- meta[key] = value;
207
- } else if (SKILL_DROP_FIELDS.has(key)) {
208
- warnings.push(` ${skillId}: dropping known field '${key}'`);
209
- } else if (key === 'disable-model-invocation') {
210
- // Map to manual_only
211
- meta['manual_only'] = Boolean(value);
212
- warnings.push(` ${skillId}: mapping disable-model-invocation → manual_only`);
213
- } else {
214
- throw new Error(`${skillId}: unknown field '${key}' — add to allow list or drop list`);
215
- }
216
- }
217
-
218
- // Normalize triggers: bracket strings → tag ids; slash commands → drop + manual_only
219
- if (Array.isArray(meta['triggers'])) {
220
- const rawTriggers = meta['triggers'] as string[];
221
- // Bracket triggers (e.g., [plan], [plan:auto]): strip brackets, split on ':' for parent id
222
- const bracketTriggers = rawTriggers
223
- .filter((t) => t.startsWith('[') && t.endsWith(']'))
224
- .map((t) => t.slice(1, -1).split(':')[0]!)
225
- .filter((t, i, arr) => arr.indexOf(t) === i); // dedupe
226
- // Slash command triggers (e.g., /claude-nexus:nx-init): harness-specific, drop them
227
- const slashTriggers = rawTriggers.filter((t) => t.startsWith('/'));
228
- if (slashTriggers.length > 0) {
229
- warnings.push(` ${skillId}: slash command triggers detected (${slashTriggers.join(', ')}) → dropping + setting manual_only: true`);
230
- meta['manual_only'] = true;
231
- }
232
- if (bracketTriggers.length > 0) {
233
- meta['triggers'] = bracketTriggers;
234
- } else {
235
- delete meta['triggers'];
236
- }
237
- }
238
-
239
- if (!meta['id']) meta['id'] = skillId;
240
- if (!meta['name']) meta['name'] = meta['id'];
241
-
242
- const body = transformBody(content);
243
-
244
- return { id: meta['id'] as string, meta, body };
245
- }
246
-
247
- // ---- Staging + atomic rename ----
248
- async function writeToStaging(
249
- root: string,
250
- kind: 'agents' | 'skills',
251
- items: Array<{ id: string; meta: Record<string, unknown>; body: string }>
252
- ): Promise<void> {
253
- const stagingDir = path.join(root, `${kind}.staging`);
254
- await rm(stagingDir, { recursive: true, force: true });
255
- await mkdir(stagingDir, { recursive: true });
256
- for (const item of items) {
257
- const dir = path.join(stagingDir, item.id);
258
- await mkdir(dir, { recursive: true });
259
- await writeFile(path.join(dir, 'meta.yml'), stringifyYaml(item.meta));
260
- await writeFile(path.join(dir, 'body.md'), item.body);
261
- }
262
- }
263
-
264
- async function atomicSwap(root: string, kind: 'agents' | 'skills'): Promise<void> {
265
- const live = path.join(root, kind);
266
- const staging = path.join(root, `${kind}.staging`);
267
- const backup = path.join(root, `${kind}.backup`);
268
- // If live exists, move it to backup
269
- try {
270
- await stat(live);
271
- await rm(backup, { recursive: true, force: true });
272
- await rename(live, backup);
273
- } catch {
274
- // live doesn't exist — first bootstrap
275
- }
276
- await rename(staging, live);
277
- }
278
-
279
- async function cleanupBackup(root: string, kind: 'agents' | 'skills'): Promise<void> {
280
- const backup = path.join(root, `${kind}.backup`);
281
- await rm(backup, { recursive: true, force: true });
282
- }
283
-
284
- async function restoreBackup(root: string, kind: 'agents' | 'skills'): Promise<void> {
285
- const live = path.join(root, kind);
286
- const backup = path.join(root, `${kind}.backup`);
287
- try {
288
- await rm(live, { recursive: true, force: true });
289
- await rename(backup, live);
290
- } catch {
291
- /* nothing to restore */
292
- }
293
- }
294
-
295
- // ---- Main ----
296
- async function main(): Promise<void> {
297
- const opts = parseCli();
298
- const root = process.cwd();
299
-
300
- console.log('# nexus-core import-from-claude-nexus');
301
- console.log(`Source: ${opts.source}`);
302
- console.log(`Target: ${root}`);
303
- console.log(`Mode: ${opts.apply ? 'APPLY' : 'DRY-RUN'}`);
304
- console.log('');
305
-
306
- // Verify source
307
- await verifySourceIsClaudeNexus(opts.source);
308
-
309
- // Git working tree dirty check
310
- if (opts.apply) {
311
- const scopes = ['agents/', 'skills/', 'manifest.json'];
312
- if (!gitWorkingTreeClean(root, scopes)) {
313
- throw new Error('Working tree has uncommitted changes in agents/, skills/, or manifest.json. Commit or stash first.');
314
- }
315
- }
316
-
317
- // Load capability map
318
- const capMap = await loadCapabilityMap(root);
319
-
320
- // Collect source files
321
- const agentFiles = opts.skillsOnly
322
- ? []
323
- : await glob(['agents/*.md'], { cwd: opts.source, absolute: true });
324
- const skillDirs = opts.agentsOnly
325
- ? []
326
- : await glob(['skills/*'], { cwd: opts.source, absolute: true, onlyDirectories: true });
327
-
328
- const warnings: string[] = [];
329
- const transformedAgents: TransformedAgent[] = [];
330
- const transformedSkills: TransformedSkill[] = [];
331
-
332
- // Transform agents
333
- for (const f of agentFiles) {
334
- const agent = await transformAgent(f, capMap, warnings);
335
- if (agent) transformedAgents.push(agent);
336
- }
337
-
338
- // Transform skills
339
- for (const d of skillDirs) {
340
- const skill = await transformSkill(d, warnings);
341
- if (skill) transformedSkills.push(skill);
342
- }
343
-
344
- console.log('# Transformation summary');
345
- console.log(`Agents: ${transformedAgents.length}`);
346
- console.log(`Skills: ${transformedSkills.length}`);
347
- if (warnings.length > 0) {
348
- console.log('# Warnings');
349
- for (const w of warnings) console.log(w);
350
- }
351
- console.log('');
352
-
353
- if (!opts.apply) {
354
- console.log('# Files that would be written (dry-run):');
355
- for (const a of transformedAgents) {
356
- console.log(` agents/${a.id}/meta.yml`);
357
- console.log(` agents/${a.id}/body.md`);
358
- }
359
- for (const s of transformedSkills) {
360
- console.log(` skills/${s.id}/meta.yml`);
361
- console.log(` skills/${s.id}/body.md`);
362
- }
363
- console.log('');
364
- console.log('Run with --apply to write.');
365
- return;
366
- }
367
-
368
- // Apply — all-or-nothing transaction via staging
369
- try {
370
- if (!opts.skillsOnly) {
371
- await writeToStaging(root, 'agents', transformedAgents);
372
- await atomicSwap(root, 'agents');
373
- }
374
- if (!opts.agentsOnly) {
375
- await writeToStaging(root, 'skills', transformedSkills);
376
- await atomicSwap(root, 'skills');
377
- }
378
- } catch (err) {
379
- console.error('Import failed during write phase. Attempting restore...');
380
- if (!opts.skillsOnly) await restoreBackup(root, 'agents');
381
- if (!opts.agentsOnly) await restoreBackup(root, 'skills');
382
- throw err;
383
- }
384
-
385
- // Cleanup backup on success
386
- if (!opts.skillsOnly) await cleanupBackup(root, 'agents');
387
- if (!opts.agentsOnly) await cleanupBackup(root, 'skills');
388
-
389
- console.log('');
390
- console.log('Import complete. Running validate...');
391
- // Auto-validate after apply — run validate via spawnSync
392
- const { spawnSync } = await import('node:child_process');
393
- const res = spawnSync('bun', ['run', 'validate'], { cwd: root, stdio: 'inherit' });
394
- if (res.status !== 0) {
395
- throw new Error('Validation failed after import. Check output above.');
396
- }
397
- console.log('Import + validation complete.');
398
- }
399
-
400
- main().catch((err) => {
401
- console.error('Error:', (err as Error).message);
402
- process.exit(1);
403
- });
@@ -1,71 +0,0 @@
1
- import { parse as parseYaml } from 'yaml';
2
-
3
- const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
4
-
5
- export interface ParsedFrontmatter {
6
- /** parsed YAML object */
7
- data: Record<string, unknown>;
8
- /** body content (after closing ---) */
9
- content: string;
10
- /** 1-based line number where frontmatter YAML starts (after opening ---) */
11
- frontmatterStartLine: number;
12
- /** 1-based line number where body content starts (after closing ---) */
13
- contentStartLine: number;
14
- }
15
-
16
- /**
17
- * Parses a markdown file with optional YAML frontmatter.
18
- *
19
- * Format:
20
- * ---
21
- * {frontmatter YAML}
22
- * ---
23
- * {body content}
24
- *
25
- * If no frontmatter is present, returns {data: {}, content: source, frontmatterStartLine: 0, contentStartLine: 1}.
26
- *
27
- * Line numbers are 1-based to match editor conventions.
28
- */
29
- export function parseFrontmatter(source: string): ParsedFrontmatter {
30
- const match = source.match(FRONTMATTER_RE);
31
- if (!match) {
32
- return {
33
- data: {},
34
- content: source,
35
- frontmatterStartLine: 0,
36
- contentStartLine: 1,
37
- };
38
- }
39
-
40
- const frontmatterText = match[1];
41
- const body = match[2];
42
- const parsed = parseYaml(frontmatterText);
43
-
44
- // Opening '---' is line 1, frontmatter YAML starts at line 2
45
- const frontmatterStartLine = 2;
46
- // Count lines in frontmatter text + 2 (opening --- and closing ---)
47
- const frontmatterLineCount = frontmatterText.split('\n').length;
48
- const contentStartLine = frontmatterStartLine + frontmatterLineCount + 1; // +1 for closing ---
49
-
50
- return {
51
- data: (parsed ?? {}) as Record<string, unknown>,
52
- content: body,
53
- frontmatterStartLine,
54
- contentStartLine,
55
- };
56
- }
57
-
58
- /**
59
- * Reverse: translates a line number within the frontmatter YAML (1-based, as reported
60
- * by a YAML parser operating on the frontmatter text alone) back to the line in the
61
- * original source file.
62
- */
63
- export function frontmatterLineToSourceLine(
64
- parsed: ParsedFrontmatter,
65
- frontmatterLine: number
66
- ): number {
67
- if (parsed.frontmatterStartLine === 0) {
68
- throw new Error('Source has no frontmatter');
69
- }
70
- return parsed.frontmatterStartLine + frontmatterLine - 1;
71
- }