@mrpatronz/nexusflow 2.2.1 → 2.3.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 (130) hide show
  1. package/README.md +80 -17
  2. package/dist/commands/ui.d.ts +1 -0
  3. package/dist/commands/ui.d.ts.map +1 -1
  4. package/dist/commands/ui.js +24 -13
  5. package/dist/commands/ui.js.map +1 -1
  6. package/dist/commands/ui.test.d.ts +2 -0
  7. package/dist/commands/ui.test.d.ts.map +1 -0
  8. package/dist/commands/ui.test.js +58 -0
  9. package/dist/commands/ui.test.js.map +1 -0
  10. package/dist/core/workspace-skills-workflow.integration.test.d.ts +2 -0
  11. package/dist/core/workspace-skills-workflow.integration.test.d.ts.map +1 -0
  12. package/dist/core/workspace-skills-workflow.integration.test.js +162 -0
  13. package/dist/core/workspace-skills-workflow.integration.test.js.map +1 -0
  14. package/dist/core/worktree.test.js +1 -1
  15. package/dist/core/worktree.test.js.map +1 -1
  16. package/dist/generators/index.d.ts.map +1 -1
  17. package/dist/generators/index.js +4 -8
  18. package/dist/generators/index.js.map +1 -1
  19. package/dist/generators/skills-generator.d.ts +2 -2
  20. package/dist/generators/skills-generator.d.ts.map +1 -1
  21. package/dist/generators/skills-generator.js +84 -89
  22. package/dist/generators/skills-generator.js.map +1 -1
  23. package/dist/generators/skills-generator.test.d.ts +2 -0
  24. package/dist/generators/skills-generator.test.d.ts.map +1 -0
  25. package/dist/generators/skills-generator.test.js +79 -0
  26. package/dist/generators/skills-generator.test.js.map +1 -0
  27. package/dist/gui/assets/AgentsPage-BtrTgIyU.js +3 -0
  28. package/dist/gui/assets/{DashboardPage-CUspfCap.js → DashboardPage-CeUkMdxc.js} +1 -1
  29. package/dist/gui/assets/{GettingStartedPage-BqtgARZW.js → GettingStartedPage-YwJXpsc6.js} +1 -1
  30. package/dist/gui/assets/ProjectsPage-D8kEHb-m.js +1 -0
  31. package/dist/gui/assets/ScaffoldRepoInline-ij7JLzmx.js +1 -0
  32. package/dist/gui/assets/SettingsPage-BQhAP4go.js +1 -0
  33. package/dist/gui/assets/SkillsPage-DJ-dXfcC.js +6 -0
  34. package/dist/gui/assets/StartWorkPage-DGWZzF4H.js +1 -0
  35. package/dist/gui/assets/{StrategiesPage-D0Y6A4Tw.js → StrategiesPage-CLOjDRbX.js} +2 -2
  36. package/dist/gui/assets/WorkspaceLauncher-CjNI4vqn.js +1 -0
  37. package/dist/gui/assets/WorkspacesPage-MDzm2JUh.js +6 -0
  38. package/dist/gui/assets/alert-dialog-DjwFcE6b.js +1 -0
  39. package/dist/gui/assets/bot-wH0FKlic.js +1 -0
  40. package/dist/gui/assets/chevron-down-DDmhvRLr.js +1 -0
  41. package/dist/gui/assets/code-xml-Chu1p81J.js +1 -0
  42. package/dist/gui/assets/index-CfQEnn52.js +52 -0
  43. package/dist/gui/assets/index-k7inKaui.css +2 -0
  44. package/dist/gui/assets/{spinner-DXt6mnJk.js → spinner-BCEaGbib.js} +1 -1
  45. package/dist/gui/assets/terminal-gK6BMl3E.js +1 -0
  46. package/dist/gui/assets/{textarea-BLU5HXmZ.js → textarea-D3-elOAa.js} +1 -1
  47. package/dist/gui/assets/{useAriaLabelledBy-CjNlctb9.js → useAriaLabelledBy-DmwmZIqb.js} +1 -1
  48. package/dist/gui/assets/useCompositeListItem-CZnlZ-eF.js +1 -0
  49. package/dist/gui/assets/useValueChanged-BfI2s7NX.js +1 -0
  50. package/dist/gui/assets/zap-FQrDnrgb.js +1 -0
  51. package/dist/gui/index.html +6 -4
  52. package/dist/resources/agents-catalog.d.ts +7 -0
  53. package/dist/resources/agents-catalog.d.ts.map +1 -0
  54. package/dist/resources/agents-catalog.js +141 -0
  55. package/dist/resources/agents-catalog.js.map +1 -0
  56. package/dist/resources/agents-catalog.test.d.ts +2 -0
  57. package/dist/resources/agents-catalog.test.d.ts.map +1 -0
  58. package/dist/resources/agents-catalog.test.js +50 -0
  59. package/dist/resources/agents-catalog.test.js.map +1 -0
  60. package/dist/resources/codex-agent.d.ts +6 -0
  61. package/dist/resources/codex-agent.d.ts.map +1 -0
  62. package/dist/resources/codex-agent.js +55 -0
  63. package/dist/resources/codex-agent.js.map +1 -0
  64. package/dist/resources/codex-agent.test.d.ts +2 -0
  65. package/dist/resources/codex-agent.test.d.ts.map +1 -0
  66. package/dist/resources/codex-agent.test.js +27 -0
  67. package/dist/resources/codex-agent.test.js.map +1 -0
  68. package/dist/resources/contracts.d.ts +116 -0
  69. package/dist/resources/contracts.d.ts.map +1 -0
  70. package/dist/resources/contracts.js +103 -0
  71. package/dist/resources/contracts.js.map +1 -0
  72. package/dist/resources/fs-safety.d.ts +11 -0
  73. package/dist/resources/fs-safety.d.ts.map +1 -0
  74. package/dist/resources/fs-safety.js +77 -0
  75. package/dist/resources/fs-safety.js.map +1 -0
  76. package/dist/resources/materializer.d.ts +17 -0
  77. package/dist/resources/materializer.d.ts.map +1 -0
  78. package/dist/resources/materializer.js +439 -0
  79. package/dist/resources/materializer.js.map +1 -0
  80. package/dist/resources/materializer.test.d.ts +2 -0
  81. package/dist/resources/materializer.test.d.ts.map +1 -0
  82. package/dist/resources/materializer.test.js +149 -0
  83. package/dist/resources/materializer.test.js.map +1 -0
  84. package/dist/resources/service.d.ts +8 -0
  85. package/dist/resources/service.d.ts.map +1 -0
  86. package/dist/resources/service.js +45 -0
  87. package/dist/resources/service.js.map +1 -0
  88. package/dist/resources/service.test.d.ts +2 -0
  89. package/dist/resources/service.test.d.ts.map +1 -0
  90. package/dist/resources/service.test.js +58 -0
  91. package/dist/resources/service.test.js.map +1 -0
  92. package/dist/server.d.ts +5 -0
  93. package/dist/server.d.ts.map +1 -1
  94. package/dist/server.js +310 -4
  95. package/dist/server.js.map +1 -1
  96. package/dist/server.test.js +349 -1
  97. package/dist/server.test.js.map +1 -1
  98. package/dist/types.d.ts +107 -0
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/utils/resources.d.ts +2 -1
  101. package/dist/utils/resources.d.ts.map +1 -1
  102. package/dist/utils/resources.js +4 -3
  103. package/dist/utils/resources.js.map +1 -1
  104. package/dist/utils/session-finder.d.ts +6 -0
  105. package/dist/utils/session-finder.d.ts.map +1 -1
  106. package/dist/utils/session-finder.js +56 -0
  107. package/dist/utils/session-finder.js.map +1 -1
  108. package/dist/utils/session-finder.test.js +34 -1
  109. package/dist/utils/session-finder.test.js.map +1 -1
  110. package/dist/utils/skills-catalog.d.ts +77 -0
  111. package/dist/utils/skills-catalog.d.ts.map +1 -0
  112. package/dist/utils/skills-catalog.js +871 -0
  113. package/dist/utils/skills-catalog.js.map +1 -0
  114. package/dist/utils/skills-catalog.test.d.ts +2 -0
  115. package/dist/utils/skills-catalog.test.d.ts.map +1 -0
  116. package/dist/utils/skills-catalog.test.js +281 -0
  117. package/dist/utils/skills-catalog.test.js.map +1 -0
  118. package/package.json +4 -1
  119. package/dist/gui/assets/ProjectsPage-UBoQxI2G.js +0 -1
  120. package/dist/gui/assets/ScaffoldRepoInline-DqoourJe.js +0 -1
  121. package/dist/gui/assets/SettingsPage-BZl24tqD.js +0 -1
  122. package/dist/gui/assets/StartWorkPage-BGNFdBfj.js +0 -1
  123. package/dist/gui/assets/WorkspaceLauncher-Cxv0O_wt.js +0 -1
  124. package/dist/gui/assets/WorkspacesPage-Cvb1rOeh.js +0 -6
  125. package/dist/gui/assets/index-CllPiiFL.js +0 -52
  126. package/dist/gui/assets/index-DGCBhMAn.css +0 -2
  127. package/dist/gui/assets/useCompositeListItem-DWGFLKqA.js +0 -1
  128. /package/dist/{resources → static-resources}/workflows/plan-implement-review.md +0 -0
  129. /package/dist/{resources → static-resources}/workflows/research-verify.md +0 -0
  130. /package/dist/{resources → static-resources}/workflows/solo-developer.md +0 -0
@@ -0,0 +1,871 @@
1
+ /**
2
+ * @module utils/skills-catalog
3
+ * Manages built-in template categories, user-defined custom categories,
4
+ * portable skills packages (SKILL.md), and workspace skills assignments.
5
+ */
6
+ import * as fs from 'node:fs/promises';
7
+ import * as path from 'node:path';
8
+ import * as os from 'node:os';
9
+ import { randomUUID } from 'node:crypto';
10
+ import fse from 'fs-extra';
11
+ import { load as yamlLoad, dump as yamlDump } from 'js-yaml';
12
+ import { slugify } from './slug.js';
13
+ import { acquireLock, createMutationQueue } from '../core/locks.js';
14
+ import { formatValidationError, resourceIdSchema, skillCategorySchema, skillFrontmatterSchema, workspaceResourcesConfigSchema, } from '../resources/contracts.js';
15
+ import { assertNoLinkedPathComponents, assertPathWithin, assertPathIsNotLink, atomicWriteJson, } from '../resources/fs-safety.js';
16
+ /**
17
+ * Parses YAML frontmatter delimited by `---`, handling CRLF line endings.
18
+ */
19
+ export function parseSkillMarkdown(raw) {
20
+ // Normalize CRLF to LF
21
+ const normalized = raw.replace(/\r\n/g, '\n').trim();
22
+ if (!normalized.startsWith('---')) {
23
+ return { metadata: {}, content: raw };
24
+ }
25
+ const endIndex = normalized.indexOf('\n---', 3);
26
+ if (endIndex === -1) {
27
+ return { metadata: {}, content: raw };
28
+ }
29
+ const yamlBlock = normalized.substring(3, endIndex).trim();
30
+ const content = normalized.substring(endIndex + 4).trim();
31
+ let metadata = {};
32
+ try {
33
+ const loaded = yamlLoad(yamlBlock);
34
+ if (loaded && typeof loaded === 'object') {
35
+ metadata = loaded;
36
+ }
37
+ }
38
+ catch (err) {
39
+ console.error('Failed to parse YAML frontmatter:', err);
40
+ }
41
+ return { metadata, content };
42
+ }
43
+ /**
44
+ * Serializes metadata and content into YAML frontmatter markdown.
45
+ */
46
+ export function serializeSkillMarkdown(metadata, content) {
47
+ const cleanMeta = {};
48
+ for (const [k, v] of Object.entries(metadata)) {
49
+ if (v !== undefined && v !== null && v !== '') {
50
+ cleanMeta[k] = v;
51
+ }
52
+ }
53
+ const yamlString = yamlDump(cleanMeta, { lineWidth: -1 }).trim();
54
+ return `---\n${yamlString}\n---\n\n${content.trim()}\n`;
55
+ }
56
+ // ─── Security Helpers ─────────────────────────────────────────────────────
57
+ const runCatalogMutation = createMutationQueue();
58
+ const runWorkspaceConfigMutation = createMutationQueue();
59
+ export class WorkspaceResourceRevisionError extends Error {
60
+ constructor(expected, current) {
61
+ super(`Workspace resource configuration changed (expected revision ${expected}, current ${current}).`);
62
+ this.name = 'WorkspaceResourceRevisionError';
63
+ }
64
+ }
65
+ async function withCatalogLock(operation) {
66
+ return runCatalogMutation(async () => {
67
+ const release = await acquireLock(path.join(getNexusFlowHome(), '.locks', 'resource-catalog.lock'), {
68
+ staleMs: 60_000,
69
+ timeoutMs: 10_000,
70
+ timeoutMessage: 'Timed out waiting for the resource catalog lock.',
71
+ });
72
+ try {
73
+ return await operation();
74
+ }
75
+ finally {
76
+ await release();
77
+ }
78
+ });
79
+ }
80
+ // ─── Default Template Categories & Skills ─────────────────────────────────
81
+ export const DEFAULT_CATEGORIES = [
82
+ {
83
+ id: 'pull-requests',
84
+ name: 'Pull Requests & Review',
85
+ description: 'Workflows for authoring, analyzing, and reviewing pull requests and merge readiness.',
86
+ icon: 'git-pull-request',
87
+ color: '#3b82f6',
88
+ custom: false,
89
+ isTemplate: true,
90
+ skills: ['pr-review-toolkit', 'pr-description-gen', 'merge-conflict-resolver'],
91
+ },
92
+ {
93
+ id: 'testing-qa',
94
+ name: 'Testing & Quality Assurance',
95
+ description: 'Test automation, coverage verification, and local runtime verifier recipes.',
96
+ icon: 'flask-conical',
97
+ color: '#10b981',
98
+ custom: false,
99
+ isTemplate: true,
100
+ skills: ['verifier-workspace', 'e2e-runner', 'unit-test-coverage'],
101
+ },
102
+ {
103
+ id: 'cross-repo-release',
104
+ name: 'Cross-Repo & Release Ordering',
105
+ description: 'Managing multi-repo dependency loops, package packing, and merge ordering.',
106
+ icon: 'package',
107
+ color: '#8b5cf6',
108
+ custom: false,
109
+ isTemplate: true,
110
+ skills: ['nexusflow-local-package-loop', 'nexusflow-release-ordering'],
111
+ },
112
+ {
113
+ id: 'database-migrations',
114
+ name: 'Database & Migrations',
115
+ description: 'Safe schema migrations, rollback procedures, and SQL performance checks.',
116
+ icon: 'database',
117
+ color: '#f59e0b',
118
+ custom: false,
119
+ isTemplate: true,
120
+ skills: ['schema-migration-validator', 'sql-fluff-linter'],
121
+ },
122
+ {
123
+ id: 'security-auditing',
124
+ name: 'Security & Auditing',
125
+ description: 'OWASP vulnerability scanning, secret leak detection, and compliance auditing.',
126
+ icon: 'shield-check',
127
+ color: '#ef4444',
128
+ custom: false,
129
+ isTemplate: true,
130
+ skills: ['secret-scanner', 'security-auditor'],
131
+ },
132
+ ];
133
+ export const DEFAULT_SKILLS = [
134
+ {
135
+ id: 'pr-review-toolkit',
136
+ name: 'pr-review-toolkit',
137
+ title: 'Pull Request Review Toolkit',
138
+ category: 'pull-requests',
139
+ description: 'Reviews pull requests for breaking changes, code style, edge cases, and test coverage. Use when user asks to "review PR", "audit changes", or "check diff".',
140
+ tags: ['git', 'pr', 'review', 'quality'],
141
+ allowedTools: ['run_command', 'view_file', 'grep_search'],
142
+ custom: false,
143
+ content: `# Pull Request Review Toolkit
144
+
145
+ This skill guides the AI assistant through a structured, multi-file pull request review.
146
+
147
+ ## Review Steps
148
+ 1. **Analyze Diff Scope**:
149
+ - Inspect uncommitted changes or branch diff using \`git diff origin/main...HEAD\`.
150
+ - Identify affected modules, interfaces, and public API boundaries.
151
+ 2. **Contract & Regression Check**:
152
+ - Verify that existing function signatures and data contracts remain backwards-compatible.
153
+ - Check error handling, boundary conditions, and null safety.
154
+ 3. **Test Coverage & Verification**:
155
+ - Confirm unit or integration tests exist for newly added logic.
156
+ - Run verification commands (\`npm test\`, \`dotnet test\`, or project test runner).
157
+ 4. **Structured Feedback**:
158
+ - Return findings organized by Severity (Critical, High, Medium, Minor) with exact line references and remediation suggestions.
159
+ `,
160
+ },
161
+ {
162
+ id: 'pr-description-gen',
163
+ name: 'pr-description-gen',
164
+ title: 'PR Description Generator',
165
+ category: 'pull-requests',
166
+ description: 'Generates conventional, well-structured Pull Request descriptions with summaries, diff breakdowns, and test checklists.',
167
+ tags: ['git', 'pr', 'documentation'],
168
+ allowedTools: ['run_command'],
169
+ custom: false,
170
+ content: `# Pull Request Description Generator
171
+
172
+ Automatically writes standardized, clear PR descriptions based on git history and repository changes.
173
+
174
+ ## Procedure
175
+ 1. Extract commit messages on the feature branch via \`git log origin/main..HEAD --oneline\`.
176
+ 2. Inspect the file change stats via \`git diff --stat origin/main..HEAD\`.
177
+ 3. Format output with:
178
+ - **Summary**: 2-3 sentences explaining the "why" and "what".
179
+ - **Key Changes**: Bulleted list of architectural and implementation updates.
180
+ - **Testing Plan**: Exact verification commands run and outcomes.
181
+ - **Breaking Changes**: Explicit flag if public APIs or database schemas were altered.
182
+ `,
183
+ },
184
+ {
185
+ id: 'merge-conflict-resolver',
186
+ name: 'merge-conflict-resolver',
187
+ title: 'Merge Conflict Resolver',
188
+ category: 'pull-requests',
189
+ description: 'Guides step-by-step resolution of git merge conflicts, ensuring neither upstream updates nor local feature intent are lost.',
190
+ tags: ['git', 'merge', 'conflict'],
191
+ allowedTools: ['run_command', 'view_file', 'replace_file_content'],
192
+ custom: false,
193
+ content: `# Merge Conflict Resolver
194
+
195
+ Guidelines for resolving complex git merge and rebase conflicts across workspace repositories.
196
+
197
+ ## Workflow
198
+ 1. Run \`git status\` to locate unmerged paths (\`UU\` status).
199
+ 2. For each conflicted file, inspect both the incoming (HEAD) and current versions.
200
+ 3. Identify semantic intent of both sides before removing conflict markers (\`<<<<<<<\`, \`=======\`, \`>>>>>>>\`).
201
+ 4. Re-run project tests and type-checks to verify resolution.
202
+ `,
203
+ },
204
+ {
205
+ id: 'verifier-workspace',
206
+ name: 'verifier-workspace',
207
+ title: 'Local Runtime Verifier',
208
+ category: 'testing-qa',
209
+ description: 'Guidelines and recipes to safely launch, mock, and verify services locally across workspace repositories.',
210
+ tags: ['testing', 'runtime', 'verifier', 'ports'],
211
+ allowedTools: ['run_command'],
212
+ custom: false,
213
+ content: `# Local Runtime Verifier
214
+
215
+ Guidelines to safely launch, mock, and verify services locally in this workspace.
216
+
217
+ ## Verification Recipe
218
+ 1. **Check local ports**: Ensure target ports do not conflict with running services.
219
+ 2. **Run mocks**: Spin up local databases/caches before starting services.
220
+ 3. **Watch out for shared staging environment**: Never publish messages or write data to staging infrastructure while testing locally unless explicitly requested.
221
+ `,
222
+ },
223
+ {
224
+ id: 'e2e-runner',
225
+ name: 'e2e-runner',
226
+ title: 'End-to-End Test Runner',
227
+ category: 'testing-qa',
228
+ description: 'Orchestrates end-to-end testing (Playwright, Cypress) with failure triage and trace analysis.',
229
+ tags: ['e2e', 'playwright', 'testing'],
230
+ allowedTools: ['run_command', 'view_file'],
231
+ custom: false,
232
+ content: `# End-to-End Test Runner
233
+
234
+ Guides executing and debugging end-to-end browser and API tests.
235
+
236
+ ## Instructions
237
+ 1. Run headless test suite: \`npx playwright test\` or \`npm run test:e2e\`.
238
+ 2. On failure, inspect generated screenshots, videos, or trace logs in the test results directory.
239
+ 3. Fix underlying selector mismatches, timing issues, or backend regressions.
240
+ `,
241
+ },
242
+ {
243
+ id: 'unit-test-coverage',
244
+ name: 'unit-test-coverage',
245
+ title: 'Unit Test Coverage & TDD',
246
+ category: 'testing-qa',
247
+ description: 'Implements comprehensive unit tests for new or modified modules, adhering to test-driven development best practices.',
248
+ tags: ['unit-test', 'tdd', 'coverage'],
249
+ allowedTools: ['run_command', 'view_file', 'write_to_file', 'replace_file_content'],
250
+ custom: false,
251
+ content: `# Unit Test Coverage & TDD
252
+
253
+ Guides writing high-coverage, maintainable unit tests.
254
+
255
+ ## Guidelines
256
+ - Test behavior, not implementation details.
257
+ - Mock external network calls, database connections, and file system I/O.
258
+ - Verify edge cases: empty collections, null inputs, unexpected exceptions, timeouts.
259
+ - Target >80% statement and branch coverage on critical business logic.
260
+ `,
261
+ },
262
+ {
263
+ id: 'nexusflow-local-package-loop',
264
+ name: 'nexusflow-local-package-loop',
265
+ title: 'Local Package Development Loop',
266
+ category: 'cross-repo-release',
267
+ description: 'Guides testing cross-repo package dependencies locally without publishing to external registries.',
268
+ tags: ['cross-repo', 'npm', 'nuget', 'packages'],
269
+ allowedTools: ['run_command', 'view_file'],
270
+ custom: false,
271
+ content: `# Local Package Development Loop
272
+
273
+ This skill guides the AI assistant through local package testing across repositories in this workspace.
274
+
275
+ ## Workflow
276
+ When modifying a shared package in one repository, you must test its effect on downstream consumer repositories before pushing.
277
+
278
+ ### npm / JS/TS:
279
+ 1. Run \`npm pack\` inside the producing package directory.
280
+ 2. Copy the generated \`.tgz\` file to \`local-packages/\`.
281
+ 3. Reference the local tarball in consumer \`package.json\`.
282
+
283
+ ### NuGet / .NET:
284
+ 1. Run \`dotnet pack -c Release -o ./local-packages\` in producer.
285
+ 2. Reference local package version in consumer \`.csproj\`.
286
+ `,
287
+ },
288
+ {
289
+ id: 'nexusflow-release-ordering',
290
+ name: 'nexusflow-release-ordering',
291
+ title: 'Release & Merge Ordering',
292
+ category: 'cross-repo-release',
293
+ description: 'Computes and explains the correct topological merge and release order when cross-repo dependencies change.',
294
+ tags: ['release', 'dependencies', 'ordering'],
295
+ allowedTools: ['view_file'],
296
+ custom: false,
297
+ content: `# Release and Merge Ordering Guidelines
298
+
299
+ Answers what repositories must be merged and released in what order when cross-repo dependencies are modified.
300
+
301
+ ## Principles
302
+ 1. **Producer First**: Repositories producing shared packages must be merged, tagged, and published first.
303
+ 2. **Consumer Bump**: Downstream consumer repositories must update their version reference and be merged next.
304
+ 3. **Reversion Check**: Ensure all temporary local package references are reverted before merging consumer branches.
305
+ `,
306
+ },
307
+ {
308
+ id: 'schema-migration-validator',
309
+ name: 'schema-migration-validator',
310
+ title: 'Schema Migration Validator',
311
+ category: 'database-migrations',
312
+ description: 'Validates database schema migrations for safety, lock contention risks, and backwards compatibility.',
313
+ tags: ['database', 'migrations', 'sql', 'prisma'],
314
+ allowedTools: ['run_command', 'view_file'],
315
+ custom: false,
316
+ content: `# Database Schema Migration Validator
317
+
318
+ Guidelines for evaluating database migrations before running them against shared environments.
319
+
320
+ ## Checklist
321
+ - **No Table Locks**: Avoid adding non-null columns without default values to large existing tables.
322
+ - **Index Creation**: Use concurrent index creation (e.g. \`CREATE INDEX CONCURRENTLY\`) on production databases.
323
+ - **Rollback Safety**: Verify a corresponding down/revert migration script exists and works.
324
+ `,
325
+ },
326
+ {
327
+ id: 'sql-fluff-linter',
328
+ name: 'sql-fluff-linter',
329
+ title: 'SQL Quality & Linter',
330
+ category: 'database-migrations',
331
+ description: 'Lints SQL queries and migration scripts for formatting, performance traps, and ANSI SQL standards.',
332
+ tags: ['sql', 'lint', 'database'],
333
+ allowedTools: ['run_command', 'view_file'],
334
+ custom: false,
335
+ content: `# SQL Quality & Linter
336
+
337
+ Enforces clean, performant SQL syntax across migrations and query templates.
338
+
339
+ ## Rules
340
+ - Use explicit column names in SELECT queries (avoid \`SELECT *\`).
341
+ - Ensure all JOIN conditions use indexed foreign keys.
342
+ - Uppercase SQL keywords (\`SELECT\`, \`WHERE\`, \`GROUP BY\`, \`ORDER BY\`).
343
+ `,
344
+ },
345
+ {
346
+ id: 'secret-scanner',
347
+ name: 'secret-scanner',
348
+ title: 'Secret & Credential Scanner',
349
+ category: 'security-auditing',
350
+ description: 'Scans source files, commit history, and configuration files for exposed API keys, private certificates, and secrets.',
351
+ tags: ['security', 'secrets', 'credentials'],
352
+ allowedTools: ['grep_search', 'view_file'],
353
+ custom: false,
354
+ content: `# Secret & Credential Scanner
355
+
356
+ Proactively prevents committing confidential credentials to version control.
357
+
358
+ ## High-Risk Patterns
359
+ - AWS access keys (\`AKIA...\`), GitHub PATs (\`ghp_...\`), OpenAI keys (\`sk-...\`).
360
+ - RSA / SSH private keys (\`-----BEGIN PRIVATE KEY-----\`).
361
+ - Hardcoded connection strings with passwords in source code.
362
+ `,
363
+ },
364
+ {
365
+ id: 'security-auditor',
366
+ name: 'security-auditor',
367
+ title: 'Security Audit Playbook',
368
+ category: 'security-auditing',
369
+ description: 'Use when auditing code for OWASP Top 10 vulnerabilities, access-control gaps, injection, or cryptographic failures.',
370
+ tags: ['security', 'audit', 'owasp'],
371
+ allowedTools: ['view_file', 'grep_search'],
372
+ custom: false,
373
+ content: `# Security Audit Playbook
374
+
375
+ Instructions for conducting deep application security audits.
376
+
377
+ ## Focus Areas
378
+ 1. **Injection Vectors**: SQL, Command, LDAP, and XSS vulnerabilities.
379
+ 2. **Broken Access Control**: Missing authorization checks on sensitive REST / RPC routes.
380
+ 3. **Cryptographic Failures**: Deprecated hashing algorithms (MD5/SHA1) or unencrypted tokens.
381
+ `,
382
+ },
383
+ ];
384
+ // ─── Categories Management ────────────────────────────────────────────────
385
+ export function getNexusFlowHome() {
386
+ if (process.env.NEXUSFLOW_HOME && process.env.NEXUSFLOW_HOME !== 'undefined') {
387
+ return process.env.NEXUSFLOW_HOME;
388
+ }
389
+ return path.join(os.homedir(), '.nexusflow');
390
+ }
391
+ export function getUserCategoriesPath() {
392
+ return path.join(getNexusFlowHome(), 'categories.json');
393
+ }
394
+ export function getUserSkillsDir() {
395
+ return path.join(getNexusFlowHome(), 'skills');
396
+ }
397
+ const PORTABLE_SKILL_SUPPORT_DIRECTORIES = new Set(['scripts', 'references', 'assets', 'agents']);
398
+ async function copySkillSupportTree(sourceDir, targetDir) {
399
+ await fse.ensureDir(targetDir);
400
+ for (const entry of await fs.readdir(sourceDir, { withFileTypes: true })) {
401
+ if (entry.isSymbolicLink()) {
402
+ throw new Error(`Linked skill support files are not allowed: ${entry.name}`);
403
+ }
404
+ const source = path.join(sourceDir, entry.name);
405
+ const target = path.join(targetDir, entry.name);
406
+ if (entry.isDirectory()) {
407
+ await copySkillSupportTree(source, target);
408
+ }
409
+ else if (entry.isFile()) {
410
+ await fs.copyFile(source, target);
411
+ }
412
+ else {
413
+ throw new Error(`Unsupported skill support entry: ${entry.name}`);
414
+ }
415
+ }
416
+ }
417
+ async function readUserSkillCategories(userPath) {
418
+ if (!(await fse.pathExists(userPath))) {
419
+ return [];
420
+ }
421
+ const data = await fse.readJson(userPath);
422
+ if (!Array.isArray(data)) {
423
+ throw new Error('Custom skill categories must be stored as an array.');
424
+ }
425
+ const categories = [];
426
+ for (const item of data) {
427
+ const parsed = skillCategorySchema.safeParse(item);
428
+ if (!parsed.success) {
429
+ console.warn(`Ignoring invalid custom skill category: ${formatValidationError(parsed.error)}`);
430
+ continue;
431
+ }
432
+ categories.push(parsed.data);
433
+ }
434
+ return categories;
435
+ }
436
+ /**
437
+ * Loads all skill categories (merging built-in templates and user custom categories).
438
+ */
439
+ export async function getSkillCategories() {
440
+ const categoryMap = new Map();
441
+ // 1. Seed with default template categories
442
+ for (const cat of DEFAULT_CATEGORIES) {
443
+ categoryMap.set(cat.id, { ...cat });
444
+ }
445
+ // 2. Load user categories from ~/.nexusflow/categories.json if present
446
+ try {
447
+ const userPath = getUserCategoriesPath();
448
+ if (await fse.pathExists(userPath)) {
449
+ const data = await readUserSkillCategories(userPath);
450
+ for (const item of data) {
451
+ categoryMap.set(item.id, {
452
+ ...item,
453
+ custom: item.custom !== undefined ? item.custom : true,
454
+ isTemplate: item.isTemplate !== undefined ? item.isTemplate : false,
455
+ });
456
+ }
457
+ }
458
+ }
459
+ catch (err) {
460
+ console.error('Failed to read user categories:', err);
461
+ }
462
+ return Array.from(categoryMap.values());
463
+ }
464
+ /**
465
+ * Saves or updates a custom skill category.
466
+ */
467
+ export async function saveSkillCategory(category) {
468
+ const rawId = category.id || slugify(category.name);
469
+ const id = slugify(rawId);
470
+ if (!id) {
471
+ throw new Error('Category name cannot be empty');
472
+ }
473
+ return withCatalogLock(async () => {
474
+ const userPath = getUserCategoriesPath();
475
+ await fse.ensureDir(path.dirname(userPath));
476
+ const userItems = await readUserSkillCategories(userPath);
477
+ const existing = userItems.find((item) => item.id === id)
478
+ ?? DEFAULT_CATEGORIES.find((item) => item.id === id);
479
+ const candidate = {
480
+ id,
481
+ name: category.name.trim(),
482
+ description: category.description?.trim() || '',
483
+ icon: category.icon || existing?.icon || 'folder',
484
+ color: category.color || existing?.color || '#3b82f6',
485
+ custom: true,
486
+ isTemplate: existing?.isTemplate || false,
487
+ skills: category.skills || existing?.skills || [],
488
+ };
489
+ const parsed = skillCategorySchema.safeParse(candidate);
490
+ if (!parsed.success) {
491
+ throw new Error(`Invalid skill category: ${formatValidationError(parsed.error)}`);
492
+ }
493
+ const filtered = userItems.filter((item) => item.id !== id);
494
+ filtered.push(parsed.data);
495
+ await atomicWriteJson(userPath, filtered);
496
+ return parsed.data;
497
+ });
498
+ }
499
+ /**
500
+ * Deletes a custom skill category or resets a customized template override.
501
+ */
502
+ export async function deleteSkillCategory(id) {
503
+ const sanitizedId = slugify(id);
504
+ if (!sanitizedId) {
505
+ throw new Error('Invalid category ID');
506
+ }
507
+ await withCatalogLock(async () => {
508
+ const userPath = getUserCategoriesPath();
509
+ const userItems = await readUserSkillCategories(userPath);
510
+ const isCustomized = userItems.some((item) => item.id === sanitizedId);
511
+ const defaultTemplate = DEFAULT_CATEGORIES.find((item) => item.id === sanitizedId);
512
+ if (!isCustomized && defaultTemplate) {
513
+ throw new Error('Cannot delete built-in template categories');
514
+ }
515
+ if (!isCustomized && !defaultTemplate) {
516
+ throw new Error('Category not found');
517
+ }
518
+ // Removing an override resets it back to the built-in category.
519
+ const filtered = userItems.filter((item) => item.id !== sanitizedId);
520
+ await atomicWriteJson(userPath, filtered);
521
+ });
522
+ }
523
+ // ─── Skills Management ────────────────────────────────────────────────────
524
+ /**
525
+ * Reads a skill package from a directory containing SKILL.md.
526
+ */
527
+ async function loadSkillFromDir(skillDir, custom, catalogRoot = path.dirname(skillDir)) {
528
+ await assertNoLinkedPathComponents(catalogRoot, skillDir);
529
+ const skillFile = path.join(skillDir, 'SKILL.md');
530
+ if (!(await fse.pathExists(skillFile))) {
531
+ return null;
532
+ }
533
+ const raw = await fs.readFile(skillFile, 'utf-8');
534
+ const { metadata, content } = parseSkillMarkdown(raw);
535
+ const parsedMetadata = skillFrontmatterSchema.safeParse(metadata);
536
+ if (!parsedMetadata.success) {
537
+ throw new Error(`Invalid SKILL.md: ${formatValidationError(parsedMetadata.error)}`);
538
+ }
539
+ const id = parsedMetadata.data.name;
540
+ const directoryId = path.basename(skillDir);
541
+ if (id !== directoryId) {
542
+ throw new Error(`Skill name "${id}" must match directory identity "${directoryId}".`);
543
+ }
544
+ const name = id;
545
+ const nexusflowMetadata = parsedMetadata.data.metadata &&
546
+ typeof parsedMetadata.data.metadata.nexusflow === 'object' &&
547
+ parsedMetadata.data.metadata.nexusflow !== null
548
+ ? parsedMetadata.data.metadata.nexusflow
549
+ : {};
550
+ const title = parsedMetadata.data.title ||
551
+ (typeof nexusflowMetadata.title === 'string' ? nexusflowMetadata.title : undefined) ||
552
+ name
553
+ .split('-')
554
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
555
+ .join(' ');
556
+ const category = parsedMetadata.data.category ||
557
+ (typeof nexusflowMetadata.category === 'string' ? nexusflowMetadata.category : undefined) ||
558
+ 'general';
559
+ const description = parsedMetadata.data.description;
560
+ const tags = parsedMetadata.data.tags ||
561
+ (Array.isArray(nexusflowMetadata.tags)
562
+ ? nexusflowMetadata.tags.filter((tag) => typeof tag === 'string')
563
+ : []);
564
+ const rawAllowedTools = parsedMetadata.data['allowed-tools'];
565
+ const allowedTools = Array.isArray(rawAllowedTools)
566
+ ? rawAllowedTools
567
+ : rawAllowedTools
568
+ ? rawAllowedTools.split(/\s+/).filter(Boolean)
569
+ : [];
570
+ // Inspect references/ and scripts/ if present
571
+ const referencesDir = path.join(skillDir, 'references');
572
+ const scriptsDir = path.join(skillDir, 'scripts');
573
+ const references = [];
574
+ const scripts = [];
575
+ if (await fse.pathExists(referencesDir)) {
576
+ try {
577
+ await assertNoLinkedPathComponents(skillDir, referencesDir);
578
+ const files = await fs.readdir(referencesDir, { withFileTypes: true });
579
+ for (const file of files) {
580
+ if (file.isSymbolicLink())
581
+ throw new Error(`Linked skill files are not allowed: ${file.name}`);
582
+ if (file.isFile()) {
583
+ references.push({ name: file.name, relativePath: path.join('references', file.name) });
584
+ }
585
+ }
586
+ }
587
+ catch (error) {
588
+ if (error instanceof Error && error.message.includes('Linked skill files'))
589
+ throw error;
590
+ }
591
+ }
592
+ if (await fse.pathExists(scriptsDir)) {
593
+ try {
594
+ await assertNoLinkedPathComponents(skillDir, scriptsDir);
595
+ const files = await fs.readdir(scriptsDir, { withFileTypes: true });
596
+ for (const file of files) {
597
+ if (file.isSymbolicLink())
598
+ throw new Error(`Linked skill files are not allowed: ${file.name}`);
599
+ if (file.isFile()) {
600
+ scripts.push({ name: file.name, relativePath: path.join('scripts', file.name) });
601
+ }
602
+ }
603
+ }
604
+ catch (error) {
605
+ if (error instanceof Error && error.message.includes('Linked skill files'))
606
+ throw error;
607
+ }
608
+ }
609
+ return {
610
+ id,
611
+ name,
612
+ title,
613
+ category,
614
+ description,
615
+ tags,
616
+ allowedTools,
617
+ content,
618
+ custom,
619
+ sourcePath: skillDir,
620
+ references: references.length > 0 ? references : undefined,
621
+ scripts: scripts.length > 0 ? scripts : undefined,
622
+ };
623
+ }
624
+ /**
625
+ * Retrieves all available skills (built-in templates + user directory + optional workspace directory).
626
+ */
627
+ export async function getAllSkills(_workspacePath) {
628
+ const skillMap = new Map();
629
+ // 1. Built-in template skills
630
+ for (const s of DEFAULT_SKILLS) {
631
+ skillMap.set(s.id, { ...s });
632
+ }
633
+ // 2. User directory (~/.nexusflow/skills/)
634
+ const userSkillsDir = getUserSkillsDir();
635
+ if (await fse.pathExists(userSkillsDir)) {
636
+ try {
637
+ await assertPathIsNotLink(userSkillsDir);
638
+ const entries = await fs.readdir(userSkillsDir, { withFileTypes: true });
639
+ if (Array.isArray(entries)) {
640
+ for (const entry of entries) {
641
+ const isDir = typeof entry === 'string' ? true : entry.isDirectory ? entry.isDirectory() : true;
642
+ const entryName = typeof entry === 'string' ? entry : entry.name;
643
+ if (isDir) {
644
+ try {
645
+ const loaded = await loadSkillFromDir(path.join(userSkillsDir, entryName), true, userSkillsDir);
646
+ if (loaded) {
647
+ if (skillMap.has(loaded.id)) {
648
+ throw new Error(`A resource named "${loaded.id}" already exists in the built-in catalog.`);
649
+ }
650
+ skillMap.set(loaded.id, loaded);
651
+ }
652
+ }
653
+ catch (error) {
654
+ const message = error instanceof Error ? error.message : String(error);
655
+ console.warn(`Skipping invalid skill "${entryName}": ${message}`);
656
+ }
657
+ }
658
+ }
659
+ }
660
+ }
661
+ catch (err) {
662
+ console.error('Failed to load user skills:', err);
663
+ }
664
+ }
665
+ return Array.from(skillMap.values());
666
+ }
667
+ /**
668
+ * Saves or updates a portable skill package.
669
+ */
670
+ export async function saveSkill(skill) {
671
+ const rawId = skill.id || skill.name;
672
+ const idResult = resourceIdSchema.safeParse(rawId);
673
+ if (!idResult.success) {
674
+ throw new Error(`Invalid skill ID: ${formatValidationError(idResult.error)}`);
675
+ }
676
+ if (idResult.data !== rawId)
677
+ throw new Error('Invalid skill ID format.');
678
+ const id = idResult.data;
679
+ const nameResult = resourceIdSchema.safeParse(skill.name);
680
+ if (!nameResult.success || nameResult.data !== skill.name || nameResult.data !== id) {
681
+ throw new Error('Skill id and name must match.');
682
+ }
683
+ if (DEFAULT_SKILLS.some((builtIn) => builtIn.id === id)) {
684
+ throw new Error('Built-in skills cannot be overwritten. Create a custom skill with a new identifier.');
685
+ }
686
+ const description = skill.description?.trim();
687
+ if (!description)
688
+ throw new Error('Skill description is required.');
689
+ if (!skill.content.trim())
690
+ throw new Error('Skill content is required.');
691
+ return withCatalogLock(async () => {
692
+ const userSkillsDir = path.resolve(getUserSkillsDir());
693
+ await fse.ensureDir(userSkillsDir);
694
+ await assertPathIsNotLink(userSkillsDir);
695
+ const targetDir = assertPathWithin(userSkillsDir, path.join(userSkillsDir, id));
696
+ await assertNoLinkedPathComponents(userSkillsDir, targetDir);
697
+ const stagingDir = await fs.mkdtemp(path.join(userSkillsDir, `.staging-${id}-`));
698
+ const backupDir = path.join(userSkillsDir, `.backup-${id}-${randomUUID()}`);
699
+ let movedExisting = false;
700
+ try {
701
+ let existingFrontmatter;
702
+ if (await fse.pathExists(targetDir)) {
703
+ await assertNoLinkedPathComponents(userSkillsDir, targetDir);
704
+ for (const entry of await fs.readdir(targetDir, { withFileTypes: true })) {
705
+ if (entry.isSymbolicLink()) {
706
+ throw new Error(`Linked skill package entries are not allowed: ${entry.name}`);
707
+ }
708
+ if (entry.name === 'SKILL.md')
709
+ continue;
710
+ if (!entry.isDirectory() || !PORTABLE_SKILL_SUPPORT_DIRECTORIES.has(entry.name)) {
711
+ throw new Error(`Unsupported top-level skill package entry: ${entry.name}`);
712
+ }
713
+ await copySkillSupportTree(path.join(targetDir, entry.name), path.join(stagingDir, entry.name));
714
+ }
715
+ const currentSkillMarkdown = parseSkillMarkdown(await fs.readFile(path.join(targetDir, 'SKILL.md'), 'utf-8'));
716
+ const parsedCurrentFrontmatter = skillFrontmatterSchema.safeParse(currentSkillMarkdown.metadata);
717
+ if (parsedCurrentFrontmatter.success)
718
+ existingFrontmatter = parsedCurrentFrontmatter.data;
719
+ }
720
+ const existingNexusFlowMetadata = existingFrontmatter?.metadata &&
721
+ typeof existingFrontmatter.metadata.nexusflow === 'object' &&
722
+ existingFrontmatter.metadata.nexusflow !== null
723
+ ? existingFrontmatter.metadata.nexusflow
724
+ : {};
725
+ const metadata = {
726
+ name: id,
727
+ description,
728
+ license: existingFrontmatter?.license,
729
+ compatibility: existingFrontmatter?.compatibility,
730
+ metadata: {
731
+ ...(existingFrontmatter?.metadata ?? {}),
732
+ nexusflow: {
733
+ ...existingNexusFlowMetadata,
734
+ title: skill.title || id,
735
+ category: skill.category || 'general',
736
+ tags: skill.tags || [],
737
+ },
738
+ },
739
+ };
740
+ const allowedTools = skill.allowedTools === undefined
741
+ ? existingFrontmatter?.['allowed-tools']
742
+ : skill.allowedTools;
743
+ if (allowedTools?.length)
744
+ metadata['allowed-tools'] = allowedTools;
745
+ await fs.writeFile(path.join(stagingDir, 'SKILL.md'), serializeSkillMarkdown(metadata, skill.content), 'utf-8');
746
+ for (const [directory, files] of [
747
+ ['references', skill.references],
748
+ ['scripts', skill.scripts],
749
+ ]) {
750
+ if (files === undefined)
751
+ continue;
752
+ const supportDir = path.join(stagingDir, directory);
753
+ await fse.remove(supportDir);
754
+ if (!files.length)
755
+ continue;
756
+ await fse.ensureDir(supportDir);
757
+ for (const file of files) {
758
+ if (!file.name || path.basename(file.name) !== file.name || file.content === undefined) {
759
+ throw new Error(`Invalid ${directory} file name: ${file.name || '(empty)'}`);
760
+ }
761
+ await fs.writeFile(path.join(supportDir, file.name), file.content, 'utf-8');
762
+ }
763
+ }
764
+ if (await fse.pathExists(targetDir)) {
765
+ await assertNoLinkedPathComponents(userSkillsDir, targetDir);
766
+ await fs.rename(targetDir, backupDir);
767
+ movedExisting = true;
768
+ }
769
+ await fs.rename(stagingDir, targetDir);
770
+ if (movedExisting)
771
+ await fse.remove(backupDir).catch(() => { });
772
+ }
773
+ catch (error) {
774
+ await fse.remove(stagingDir).catch(() => { });
775
+ if (movedExisting && !(await fse.pathExists(targetDir)) && (await fse.pathExists(backupDir))) {
776
+ await fs.rename(backupDir, targetDir).catch(() => { });
777
+ }
778
+ throw error;
779
+ }
780
+ const loaded = await loadSkillFromDir(targetDir, true, userSkillsDir);
781
+ if (!loaded)
782
+ throw new Error('Saved skill could not be loaded.');
783
+ return loaded;
784
+ });
785
+ }
786
+ /**
787
+ * Deletes a user custom skill safely.
788
+ */
789
+ export async function deleteSkill(id) {
790
+ const idResult = resourceIdSchema.safeParse(id);
791
+ if (!idResult.success || idResult.data !== id) {
792
+ throw new Error('Invalid skill ID format.');
793
+ }
794
+ await withCatalogLock(async () => {
795
+ const userSkillsDir = path.resolve(getUserSkillsDir());
796
+ await fse.ensureDir(userSkillsDir);
797
+ await assertPathIsNotLink(userSkillsDir);
798
+ const targetDir = assertPathWithin(userSkillsDir, path.join(userSkillsDir, id));
799
+ if (!(await fse.pathExists(targetDir)))
800
+ throw new Error('Skill not found.');
801
+ await assertNoLinkedPathComponents(userSkillsDir, targetDir);
802
+ await fse.remove(targetDir);
803
+ });
804
+ }
805
+ // ─── Workspace Skills Assignment Config ────────────────────────────────────
806
+ /**
807
+ * Loads workspace skills assignment config from `.nexusflow/skills.json` or returns defaults.
808
+ */
809
+ export async function getWorkspaceSkillsConfig(workspacePath) {
810
+ const canonicalWorkspace = await fs.realpath(workspacePath);
811
+ const configFile = path.join(canonicalWorkspace, '.nexusflow', 'skills.json');
812
+ if (await fse.pathExists(configFile)) {
813
+ await assertNoLinkedPathComponents(canonicalWorkspace, configFile);
814
+ const rawData = await fse.readJson(configFile);
815
+ const legacyData = typeof rawData === 'object' && rawData !== null && !('schemaVersion' in rawData)
816
+ ? { schemaVersion: 1, revision: 0, ...rawData }
817
+ : rawData;
818
+ const result = workspaceResourcesConfigSchema.safeParse(legacyData);
819
+ if (!result.success) {
820
+ throw new Error(`Invalid workspace resource configuration: ${formatValidationError(result.error)}`);
821
+ }
822
+ return result.data;
823
+ }
824
+ return {
825
+ schemaVersion: 1,
826
+ revision: 0,
827
+ enabledSkills: [],
828
+ enabledAgents: [],
829
+ enabledCategories: [],
830
+ };
831
+ }
832
+ /**
833
+ * Saves workspace skills assignment config.
834
+ */
835
+ export async function saveWorkspaceSkillsConfig(workspacePath, config, expectedRevision) {
836
+ return runWorkspaceConfigMutation(async () => {
837
+ const canonicalWorkspace = await fs.realpath(workspacePath);
838
+ const configDir = path.join(canonicalWorkspace, '.nexusflow');
839
+ await fse.ensureDir(configDir);
840
+ await assertNoLinkedPathComponents(canonicalWorkspace, configDir);
841
+ await assertPathIsNotLink(configDir);
842
+ const release = await acquireLock(path.join(configDir, 'resource-config.lock'), {
843
+ staleMs: 60_000,
844
+ timeoutMs: 10_000,
845
+ timeoutMessage: 'Timed out waiting for the workspace resource configuration lock.',
846
+ });
847
+ try {
848
+ const current = await getWorkspaceSkillsConfig(canonicalWorkspace);
849
+ const currentRevision = current.revision ?? 0;
850
+ if (expectedRevision !== undefined && expectedRevision !== currentRevision) {
851
+ throw new WorkspaceResourceRevisionError(expectedRevision, currentRevision);
852
+ }
853
+ const parsed = workspaceResourcesConfigSchema.safeParse({
854
+ schemaVersion: 1,
855
+ revision: currentRevision + 1,
856
+ enabledSkills: config.enabledSkills,
857
+ enabledAgents: config.enabledAgents ?? current.enabledAgents ?? [],
858
+ enabledCategories: config.enabledCategories ?? [],
859
+ });
860
+ if (!parsed.success) {
861
+ throw new Error(`Invalid workspace resource configuration: ${formatValidationError(parsed.error)}`);
862
+ }
863
+ await atomicWriteJson(path.join(configDir, 'skills.json'), parsed.data);
864
+ return parsed.data;
865
+ }
866
+ finally {
867
+ await release();
868
+ }
869
+ });
870
+ }
871
+ //# sourceMappingURL=skills-catalog.js.map