@mrpatronz/nexusflow 2.11.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +151 -12
- package/dist/analyzers/detect-deps.js +1 -1
- package/dist/analyzers/detect-deps.js.map +1 -1
- package/dist/commands/commit.d.ts.map +1 -1
- package/dist/commands/commit.js +15 -0
- package/dist/commands/commit.js.map +1 -1
- package/dist/commands/create.d.ts +10 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +44 -23
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/finish.d.ts.map +1 -1
- package/dist/commands/finish.js +15 -0
- package/dist/commands/finish.js.map +1 -1
- package/dist/commands/flow.d.ts +13 -0
- package/dist/commands/flow.d.ts.map +1 -0
- package/dist/commands/flow.js +91 -0
- package/dist/commands/flow.js.map +1 -0
- package/dist/commands/skill.d.ts +28 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +250 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +14 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/tag.d.ts +13 -0
- package/dist/commands/tag.d.ts.map +1 -0
- package/dist/commands/tag.js +179 -0
- package/dist/commands/tag.js.map +1 -0
- package/dist/commands/verify.d.ts +16 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +91 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/core/adapters/local-storage.d.ts.map +1 -1
- package/dist/core/adapters/local-storage.js +2 -2
- package/dist/core/adapters/local-storage.js.map +1 -1
- package/dist/core/commit.d.ts +2 -0
- package/dist/core/commit.d.ts.map +1 -1
- package/dist/core/commit.js +15 -0
- package/dist/core/commit.js.map +1 -1
- package/dist/core/domain-catalog.d.ts +9 -0
- package/dist/core/domain-catalog.d.ts.map +1 -0
- package/dist/core/domain-catalog.js +47 -0
- package/dist/core/domain-catalog.js.map +1 -0
- package/dist/core/domain-packs.d.ts +81 -0
- package/dist/core/domain-packs.d.ts.map +1 -0
- package/dist/core/domain-packs.js +443 -0
- package/dist/core/domain-packs.js.map +1 -0
- package/dist/core/generation-lock.d.ts.map +1 -1
- package/dist/core/generation-lock.js +1 -3
- package/dist/core/generation-lock.js.map +1 -1
- package/dist/core/lifecycle.d.ts +28 -0
- package/dist/core/lifecycle.d.ts.map +1 -0
- package/dist/core/lifecycle.js +411 -0
- package/dist/core/lifecycle.js.map +1 -0
- package/dist/core/verify.d.ts +39 -0
- package/dist/core/verify.d.ts.map +1 -0
- package/dist/core/verify.js +397 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/core/work-guidance.d.ts +201 -0
- package/dist/core/work-guidance.d.ts.map +1 -0
- package/dist/core/work-guidance.js +225 -0
- package/dist/core/work-guidance.js.map +1 -0
- package/dist/core/workspace-state.d.ts +24 -3
- package/dist/core/workspace-state.d.ts.map +1 -1
- package/dist/core/workspace-state.js +93 -31
- package/dist/core/workspace-state.js.map +1 -1
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +7 -0
- package/dist/core/workspace.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +38 -4
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +4 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/plan-generator.d.ts.map +1 -1
- package/dist/generators/plan-generator.js +6 -1
- package/dist/generators/plan-generator.js.map +1 -1
- package/dist/generators/skills-generator.d.ts.map +1 -1
- package/dist/generators/skills-generator.js +36 -3
- package/dist/generators/skills-generator.js.map +1 -1
- package/dist/gui/assets/AgentsPage-GCFiyNSm.js +3 -0
- package/dist/gui/assets/DashboardPage-51JHl4Hg.js +1 -0
- package/dist/gui/assets/{GettingStartedPage-1MOTng3B.js → GettingStartedPage-DH-zXYgU.js} +1 -1
- package/dist/gui/assets/ProjectsPage-DUv2fhI8.js +1 -0
- package/dist/gui/assets/ScaffoldRepoInline-Asao9IqG.js +1 -0
- package/dist/gui/assets/SettingsPage--pnZKpmm.js +1 -0
- package/dist/gui/assets/SkillsPage-BPq8arO5.js +7 -0
- package/dist/gui/assets/StartWorkPage-CypGEQZ0.js +1 -0
- package/dist/gui/assets/{StrategiesPage-CdCbbIu-.js → StrategiesPage-BHAzXMnc.js} +2 -2
- package/dist/gui/assets/WorkroomsPage-BYC8c-rP.js +4 -0
- package/dist/gui/assets/WorkspacesPage-DRASo1aL.js +13 -0
- package/dist/gui/assets/{alert-dialog-u7qgb9Hq.js → alert-dialog-BD3wT0w3.js} +1 -1
- package/dist/gui/assets/badge-DTBzXOWn.js +1 -0
- package/dist/gui/assets/index-er-mkc2S.css +2 -0
- package/dist/gui/assets/index-jcGHcXQ1.js +57 -0
- package/dist/gui/assets/square-check-big-Bj8Rf_dH.js +1 -0
- package/dist/gui/assets/tabs-DfbdubLX.js +1 -0
- package/dist/gui/assets/{useAriaLabelledBy-C4zV-1Ra.js → useAriaLabelledBy-BmJErI5a.js} +1 -1
- package/dist/gui/assets/vsc-Dn-smJni.js +1 -0
- package/dist/gui/index.html +3 -5
- package/dist/http/work-guidance-routes.d.ts +4 -0
- package/dist/http/work-guidance-routes.d.ts.map +1 -0
- package/dist/http/work-guidance-routes.js +44 -0
- package/dist/http/work-guidance-routes.js.map +1 -0
- package/dist/index.js +169 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +183 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/resources/contracts.d.ts.map +1 -1
- package/dist/resources/contracts.js +2 -1
- package/dist/resources/contracts.js.map +1 -1
- package/dist/resources/fs-safety.d.ts.map +1 -1
- package/dist/resources/fs-safety.js +2 -1
- package/dist/resources/fs-safety.js.map +1 -1
- package/dist/resources/materializer.d.ts +3 -0
- package/dist/resources/materializer.d.ts.map +1 -1
- package/dist/resources/materializer.js +45 -19
- package/dist/resources/materializer.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +327 -4
- package/dist/server.js.map +1 -1
- package/dist/static-resources/skills/contextspace-verify-release/SKILL.md +51 -0
- package/dist/static-resources/skills/contextspace-verify-release/references/evidence.md +21 -0
- package/dist/static-resources/skills/nexusflow-dev/SKILL.md +49 -0
- package/dist/static-resources/skills/nexusflow-dev/references/cli-conventions.md +15 -0
- package/dist/static-resources/skills/nexusflow-dev/references/verification-checklist.md +28 -0
- package/dist/static-resources/skills/nexusflow-lifecycle/SKILL.md +62 -0
- package/dist/static-resources/skills/nexusflow-lifecycle/references/epic-deliverables.md +22 -0
- package/dist/static-resources/skills/nexusflow-lifecycle/references/flow-selection-matrix.md +21 -0
- package/dist/static-resources/skills/nexusflow-lifecycle/references/stage-outcomes.md +23 -0
- package/dist/static-resources/workflows/epic-multi-slice.md +21 -0
- package/dist/static-resources/workflows/plan-implement-review.md +17 -6
- package/dist/static-resources/workflows/research-verify.md +15 -4
- package/dist/static-resources/workflows/solo-developer.md +11 -2
- package/dist/test-setup.d.ts +2 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +8 -0
- package/dist/test-setup.js.map +1 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/resolve-workspace.d.ts.map +1 -1
- package/dist/utils/resolve-workspace.js +15 -8
- package/dist/utils/resolve-workspace.js.map +1 -1
- package/dist/utils/skills-catalog.d.ts +13 -9
- package/dist/utils/skills-catalog.d.ts.map +1 -1
- package/dist/utils/skills-catalog.js +241 -341
- package/dist/utils/skills-catalog.js.map +1 -1
- package/dist/utils/workflow-advisor.d.ts +1 -1
- package/dist/utils/workflow-advisor.d.ts.map +1 -1
- package/dist/utils/workflow-advisor.js +16 -4
- package/dist/utils/workflow-advisor.js.map +1 -1
- package/dist/utils/workflows.d.ts.map +1 -1
- package/dist/utils/workflows.js +5 -2
- package/dist/utils/workflows.js.map +1 -1
- package/dist/workrooms/contracts.d.ts +3 -3
- package/dist/workrooms/local-resources.d.ts +4 -1
- package/dist/workrooms/local-resources.d.ts.map +1 -1
- package/dist/workrooms/manager.d.ts +5 -2
- package/dist/workrooms/manager.d.ts.map +1 -1
- package/dist/workrooms/store.d.ts +2 -2
- package/package.json +3 -2
- package/resources/skills/contextspace-verify-release/SKILL.md +51 -0
- package/resources/skills/contextspace-verify-release/references/evidence.md +21 -0
- package/resources/skills/nexusflow-dev/SKILL.md +49 -0
- package/resources/skills/nexusflow-dev/references/cli-conventions.md +15 -0
- package/resources/skills/nexusflow-dev/references/verification-checklist.md +28 -0
- package/resources/skills/nexusflow-lifecycle/SKILL.md +62 -0
- package/resources/skills/nexusflow-lifecycle/references/epic-deliverables.md +22 -0
- package/resources/skills/nexusflow-lifecycle/references/flow-selection-matrix.md +21 -0
- package/resources/skills/nexusflow-lifecycle/references/stage-outcomes.md +23 -0
- package/resources/workflows/epic-multi-slice.md +21 -0
- package/resources/workflows/plan-implement-review.md +17 -6
- package/resources/workflows/research-verify.md +15 -4
- package/resources/workflows/solo-developer.md +11 -2
- package/dist/gui/assets/AgentsPage-DbivhmK0.js +0 -3
- package/dist/gui/assets/DashboardPage-D50Qrm3e.js +0 -1
- package/dist/gui/assets/NullStore-tdPen1kp.js +0 -1
- package/dist/gui/assets/ProjectsPage-srKgf7VN.js +0 -1
- package/dist/gui/assets/ScaffoldRepoInline-CP0yn5mw.js +0 -1
- package/dist/gui/assets/SettingsPage-DWmrLV8o.js +0 -1
- package/dist/gui/assets/SkillsPage-CKjTXdrd.js +0 -6
- package/dist/gui/assets/StartWorkPage-BCUnJ9k8.js +0 -1
- package/dist/gui/assets/WorkroomsPage-BcT-mGtT.js +0 -4
- package/dist/gui/assets/WorkspacesPage-CYFCm-7_.js +0 -2
- package/dist/gui/assets/badge-DwXvB44H.js +0 -1
- package/dist/gui/assets/chunk-BV7QT456-DfUBKG-z.js +0 -3
- package/dist/gui/assets/index-3tD_IeyQ.js +0 -55
- package/dist/gui/assets/index-CK5YRfPV.css +0 -2
- package/dist/gui/assets/square-check-big-BSIiYXPI.js +0 -1
- package/dist/gui/assets/tabs-BrHKp5bD.js +0 -1
- package/dist/gui/assets/vsc-D80c4gBT.js +0 -1
|
@@ -15,11 +15,11 @@ import { resolveBrandHomeDir, resolveGlobalDurablePath, resolveWorkspaceConfigDi
|
|
|
15
15
|
import { formatValidationError, resourceIdSchema, skillCategorySchema, skillFrontmatterSchema, workspaceResourcesConfigSchema, } from '../resources/contracts.js';
|
|
16
16
|
import { assertNoLinkedPathComponents, assertPathWithin, assertPathIsNotLink, atomicWriteJson, } from '../resources/fs-safety.js';
|
|
17
17
|
/**
|
|
18
|
-
* Parses YAML frontmatter delimited by `---`, handling CRLF line endings.
|
|
18
|
+
* Parses YAML frontmatter delimited by `---`, handling CRLF line endings and UTF-8 BOM.
|
|
19
19
|
*/
|
|
20
20
|
export function parseSkillMarkdown(raw) {
|
|
21
|
-
//
|
|
22
|
-
const normalized = raw.replace(/\r\n/g, '\n').trim();
|
|
21
|
+
// Strip UTF-8 BOM and normalize CRLF to LF
|
|
22
|
+
const normalized = raw.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').trim();
|
|
23
23
|
if (!normalized.startsWith('---')) {
|
|
24
24
|
return { metadata: {}, content: raw };
|
|
25
25
|
}
|
|
@@ -79,309 +79,8 @@ async function withCatalogLock(operation) {
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
// ─── Default Template Categories & Skills ─────────────────────────────────
|
|
82
|
-
export const DEFAULT_CATEGORIES = [
|
|
83
|
-
|
|
84
|
-
id: 'pull-requests',
|
|
85
|
-
name: 'Pull Requests & Review',
|
|
86
|
-
description: 'Workflows for authoring, analyzing, and reviewing pull requests and merge readiness.',
|
|
87
|
-
icon: 'git-pull-request',
|
|
88
|
-
color: '#3b82f6',
|
|
89
|
-
custom: false,
|
|
90
|
-
isTemplate: true,
|
|
91
|
-
skills: ['pr-review-toolkit', 'pr-description-gen', 'merge-conflict-resolver'],
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
id: 'testing-qa',
|
|
95
|
-
name: 'Testing & Quality Assurance',
|
|
96
|
-
description: 'Test automation, coverage verification, and local runtime verifier recipes.',
|
|
97
|
-
icon: 'flask-conical',
|
|
98
|
-
color: '#10b981',
|
|
99
|
-
custom: false,
|
|
100
|
-
isTemplate: true,
|
|
101
|
-
skills: ['verifier-workspace', 'e2e-runner', 'unit-test-coverage'],
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
id: 'cross-repo-release',
|
|
105
|
-
name: 'Cross-Repo & Release Ordering',
|
|
106
|
-
description: 'Managing multi-repo dependency loops, package packing, and merge ordering.',
|
|
107
|
-
icon: 'package',
|
|
108
|
-
color: '#8b5cf6',
|
|
109
|
-
custom: false,
|
|
110
|
-
isTemplate: true,
|
|
111
|
-
skills: ['nexusflow-local-package-loop', 'nexusflow-release-ordering'],
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
id: 'database-migrations',
|
|
115
|
-
name: 'Database & Migrations',
|
|
116
|
-
description: 'Safe schema migrations, rollback procedures, and SQL performance checks.',
|
|
117
|
-
icon: 'database',
|
|
118
|
-
color: '#f59e0b',
|
|
119
|
-
custom: false,
|
|
120
|
-
isTemplate: true,
|
|
121
|
-
skills: ['schema-migration-validator', 'sql-fluff-linter'],
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
id: 'security-auditing',
|
|
125
|
-
name: 'Security & Auditing',
|
|
126
|
-
description: 'OWASP vulnerability scanning, secret leak detection, and compliance auditing.',
|
|
127
|
-
icon: 'shield-check',
|
|
128
|
-
color: '#ef4444',
|
|
129
|
-
custom: false,
|
|
130
|
-
isTemplate: true,
|
|
131
|
-
skills: ['secret-scanner', 'security-auditor'],
|
|
132
|
-
},
|
|
133
|
-
];
|
|
134
|
-
export const DEFAULT_SKILLS = [
|
|
135
|
-
{
|
|
136
|
-
id: 'pr-review-toolkit',
|
|
137
|
-
name: 'pr-review-toolkit',
|
|
138
|
-
title: 'Pull Request Review Toolkit',
|
|
139
|
-
category: 'pull-requests',
|
|
140
|
-
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".',
|
|
141
|
-
tags: ['git', 'pr', 'review', 'quality'],
|
|
142
|
-
allowedTools: ['run_command', 'view_file', 'grep_search'],
|
|
143
|
-
custom: false,
|
|
144
|
-
content: `# Pull Request Review Toolkit
|
|
145
|
-
|
|
146
|
-
This skill guides the AI assistant through a structured, multi-file pull request review.
|
|
147
|
-
|
|
148
|
-
## Review Steps
|
|
149
|
-
1. **Analyze Diff Scope**:
|
|
150
|
-
- Inspect uncommitted changes or branch diff using \`git diff origin/main...HEAD\`.
|
|
151
|
-
- Identify affected modules, interfaces, and public API boundaries.
|
|
152
|
-
2. **Contract & Regression Check**:
|
|
153
|
-
- Verify that existing function signatures and data contracts remain backwards-compatible.
|
|
154
|
-
- Check error handling, boundary conditions, and null safety.
|
|
155
|
-
3. **Test Coverage & Verification**:
|
|
156
|
-
- Confirm unit or integration tests exist for newly added logic.
|
|
157
|
-
- Run verification commands (\`npm test\`, \`dotnet test\`, or project test runner).
|
|
158
|
-
4. **Structured Feedback**:
|
|
159
|
-
- Return findings organized by Severity (Critical, High, Medium, Minor) with exact line references and remediation suggestions.
|
|
160
|
-
`,
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
id: 'pr-description-gen',
|
|
164
|
-
name: 'pr-description-gen',
|
|
165
|
-
title: 'PR Description Generator',
|
|
166
|
-
category: 'pull-requests',
|
|
167
|
-
description: 'Generates conventional, well-structured Pull Request descriptions with summaries, diff breakdowns, and test checklists.',
|
|
168
|
-
tags: ['git', 'pr', 'documentation'],
|
|
169
|
-
allowedTools: ['run_command'],
|
|
170
|
-
custom: false,
|
|
171
|
-
content: `# Pull Request Description Generator
|
|
172
|
-
|
|
173
|
-
Automatically writes standardized, clear PR descriptions based on git history and repository changes.
|
|
174
|
-
|
|
175
|
-
## Procedure
|
|
176
|
-
1. Extract commit messages on the feature branch via \`git log origin/main..HEAD --oneline\`.
|
|
177
|
-
2. Inspect the file change stats via \`git diff --stat origin/main..HEAD\`.
|
|
178
|
-
3. Format output with:
|
|
179
|
-
- **Summary**: 2-3 sentences explaining the "why" and "what".
|
|
180
|
-
- **Key Changes**: Bulleted list of architectural and implementation updates.
|
|
181
|
-
- **Testing Plan**: Exact verification commands run and outcomes.
|
|
182
|
-
- **Breaking Changes**: Explicit flag if public APIs or database schemas were altered.
|
|
183
|
-
`,
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
id: 'merge-conflict-resolver',
|
|
187
|
-
name: 'merge-conflict-resolver',
|
|
188
|
-
title: 'Merge Conflict Resolver',
|
|
189
|
-
category: 'pull-requests',
|
|
190
|
-
description: 'Guides step-by-step resolution of git merge conflicts, ensuring neither upstream updates nor local feature intent are lost.',
|
|
191
|
-
tags: ['git', 'merge', 'conflict'],
|
|
192
|
-
allowedTools: ['run_command', 'view_file', 'replace_file_content'],
|
|
193
|
-
custom: false,
|
|
194
|
-
content: `# Merge Conflict Resolver
|
|
195
|
-
|
|
196
|
-
Guidelines for resolving complex git merge and rebase conflicts across workspace repositories.
|
|
197
|
-
|
|
198
|
-
## Workflow
|
|
199
|
-
1. Run \`git status\` to locate unmerged paths (\`UU\` status).
|
|
200
|
-
2. For each conflicted file, inspect both the incoming (HEAD) and current versions.
|
|
201
|
-
3. Identify semantic intent of both sides before removing conflict markers (\`<<<<<<<\`, \`=======\`, \`>>>>>>>\`).
|
|
202
|
-
4. Re-run project tests and type-checks to verify resolution.
|
|
203
|
-
`,
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
id: 'verifier-workspace',
|
|
207
|
-
name: 'verifier-workspace',
|
|
208
|
-
title: 'Local Runtime Verifier',
|
|
209
|
-
category: 'testing-qa',
|
|
210
|
-
description: 'Guidelines and recipes to safely launch, mock, and verify services locally across workspace repositories.',
|
|
211
|
-
tags: ['testing', 'runtime', 'verifier', 'ports'],
|
|
212
|
-
allowedTools: ['run_command'],
|
|
213
|
-
custom: false,
|
|
214
|
-
content: `# Local Runtime Verifier
|
|
215
|
-
|
|
216
|
-
Guidelines to safely launch, mock, and verify services locally in this workspace.
|
|
217
|
-
|
|
218
|
-
## Verification Recipe
|
|
219
|
-
1. **Check local ports**: Ensure target ports do not conflict with running services.
|
|
220
|
-
2. **Run mocks**: Spin up local databases/caches before starting services.
|
|
221
|
-
3. **Watch out for shared staging environment**: Never publish messages or write data to staging infrastructure while testing locally unless explicitly requested.
|
|
222
|
-
`,
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
id: 'e2e-runner',
|
|
226
|
-
name: 'e2e-runner',
|
|
227
|
-
title: 'End-to-End Test Runner',
|
|
228
|
-
category: 'testing-qa',
|
|
229
|
-
description: 'Orchestrates end-to-end testing (Playwright, Cypress) with failure triage and trace analysis.',
|
|
230
|
-
tags: ['e2e', 'playwright', 'testing'],
|
|
231
|
-
allowedTools: ['run_command', 'view_file'],
|
|
232
|
-
custom: false,
|
|
233
|
-
content: `# End-to-End Test Runner
|
|
234
|
-
|
|
235
|
-
Guides executing and debugging end-to-end browser and API tests.
|
|
236
|
-
|
|
237
|
-
## Instructions
|
|
238
|
-
1. Run headless test suite: \`npx playwright test\` or \`npm run test:e2e\`.
|
|
239
|
-
2. On failure, inspect generated screenshots, videos, or trace logs in the test results directory.
|
|
240
|
-
3. Fix underlying selector mismatches, timing issues, or backend regressions.
|
|
241
|
-
`,
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
id: 'unit-test-coverage',
|
|
245
|
-
name: 'unit-test-coverage',
|
|
246
|
-
title: 'Unit Test Coverage & TDD',
|
|
247
|
-
category: 'testing-qa',
|
|
248
|
-
description: 'Implements comprehensive unit tests for new or modified modules, adhering to test-driven development best practices.',
|
|
249
|
-
tags: ['unit-test', 'tdd', 'coverage'],
|
|
250
|
-
allowedTools: ['run_command', 'view_file', 'write_to_file', 'replace_file_content'],
|
|
251
|
-
custom: false,
|
|
252
|
-
content: `# Unit Test Coverage & TDD
|
|
253
|
-
|
|
254
|
-
Guides writing high-coverage, maintainable unit tests.
|
|
255
|
-
|
|
256
|
-
## Guidelines
|
|
257
|
-
- Test behavior, not implementation details.
|
|
258
|
-
- Mock external network calls, database connections, and file system I/O.
|
|
259
|
-
- Verify edge cases: empty collections, null inputs, unexpected exceptions, timeouts.
|
|
260
|
-
- Target >80% statement and branch coverage on critical business logic.
|
|
261
|
-
`,
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
id: 'nexusflow-local-package-loop',
|
|
265
|
-
name: 'nexusflow-local-package-loop',
|
|
266
|
-
title: 'Local Package Development Loop',
|
|
267
|
-
category: 'cross-repo-release',
|
|
268
|
-
description: 'Guides testing cross-repo package dependencies locally without publishing to external registries.',
|
|
269
|
-
tags: ['cross-repo', 'npm', 'nuget', 'packages'],
|
|
270
|
-
allowedTools: ['run_command', 'view_file'],
|
|
271
|
-
custom: false,
|
|
272
|
-
content: `# Local Package Development Loop
|
|
273
|
-
|
|
274
|
-
This skill guides the AI assistant through local package testing across repositories in this workspace.
|
|
275
|
-
|
|
276
|
-
## Workflow
|
|
277
|
-
When modifying a shared package in one repository, you must test its effect on downstream consumer repositories before pushing.
|
|
278
|
-
|
|
279
|
-
### npm / JS/TS:
|
|
280
|
-
1. Run \`npm pack\` inside the producing package directory.
|
|
281
|
-
2. Copy the generated \`.tgz\` file to \`local-packages/\`.
|
|
282
|
-
3. Reference the local tarball in consumer \`package.json\`.
|
|
283
|
-
|
|
284
|
-
### NuGet / .NET:
|
|
285
|
-
1. Run \`dotnet pack -c Release -o ./local-packages\` in producer.
|
|
286
|
-
2. Reference local package version in consumer \`.csproj\`.
|
|
287
|
-
`,
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
id: 'nexusflow-release-ordering',
|
|
291
|
-
name: 'nexusflow-release-ordering',
|
|
292
|
-
title: 'Release & Merge Ordering',
|
|
293
|
-
category: 'cross-repo-release',
|
|
294
|
-
description: 'Computes and explains the correct topological merge and release order when cross-repo dependencies change.',
|
|
295
|
-
tags: ['release', 'dependencies', 'ordering'],
|
|
296
|
-
allowedTools: ['view_file'],
|
|
297
|
-
custom: false,
|
|
298
|
-
content: `# Release and Merge Ordering Guidelines
|
|
299
|
-
|
|
300
|
-
Answers what repositories must be merged and released in what order when cross-repo dependencies are modified.
|
|
301
|
-
|
|
302
|
-
## Principles
|
|
303
|
-
1. **Producer First**: Repositories producing shared packages must be merged, tagged, and published first.
|
|
304
|
-
2. **Consumer Bump**: Downstream consumer repositories must update their version reference and be merged next.
|
|
305
|
-
3. **Reversion Check**: Ensure all temporary local package references are reverted before merging consumer branches.
|
|
306
|
-
`,
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
id: 'schema-migration-validator',
|
|
310
|
-
name: 'schema-migration-validator',
|
|
311
|
-
title: 'Schema Migration Validator',
|
|
312
|
-
category: 'database-migrations',
|
|
313
|
-
description: 'Validates database schema migrations for safety, lock contention risks, and backwards compatibility.',
|
|
314
|
-
tags: ['database', 'migrations', 'sql', 'prisma'],
|
|
315
|
-
allowedTools: ['run_command', 'view_file'],
|
|
316
|
-
custom: false,
|
|
317
|
-
content: `# Database Schema Migration Validator
|
|
318
|
-
|
|
319
|
-
Guidelines for evaluating database migrations before running them against shared environments.
|
|
320
|
-
|
|
321
|
-
## Checklist
|
|
322
|
-
- **No Table Locks**: Avoid adding non-null columns without default values to large existing tables.
|
|
323
|
-
- **Index Creation**: Use concurrent index creation (e.g. \`CREATE INDEX CONCURRENTLY\`) on production databases.
|
|
324
|
-
- **Rollback Safety**: Verify a corresponding down/revert migration script exists and works.
|
|
325
|
-
`,
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
id: 'sql-fluff-linter',
|
|
329
|
-
name: 'sql-fluff-linter',
|
|
330
|
-
title: 'SQL Quality & Linter',
|
|
331
|
-
category: 'database-migrations',
|
|
332
|
-
description: 'Lints SQL queries and migration scripts for formatting, performance traps, and ANSI SQL standards.',
|
|
333
|
-
tags: ['sql', 'lint', 'database'],
|
|
334
|
-
allowedTools: ['run_command', 'view_file'],
|
|
335
|
-
custom: false,
|
|
336
|
-
content: `# SQL Quality & Linter
|
|
337
|
-
|
|
338
|
-
Enforces clean, performant SQL syntax across migrations and query templates.
|
|
339
|
-
|
|
340
|
-
## Rules
|
|
341
|
-
- Use explicit column names in SELECT queries (avoid \`SELECT *\`).
|
|
342
|
-
- Ensure all JOIN conditions use indexed foreign keys.
|
|
343
|
-
- Uppercase SQL keywords (\`SELECT\`, \`WHERE\`, \`GROUP BY\`, \`ORDER BY\`).
|
|
344
|
-
`,
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
id: 'secret-scanner',
|
|
348
|
-
name: 'secret-scanner',
|
|
349
|
-
title: 'Secret & Credential Scanner',
|
|
350
|
-
category: 'security-auditing',
|
|
351
|
-
description: 'Scans source files, commit history, and configuration files for exposed API keys, private certificates, and secrets.',
|
|
352
|
-
tags: ['security', 'secrets', 'credentials'],
|
|
353
|
-
allowedTools: ['grep_search', 'view_file'],
|
|
354
|
-
custom: false,
|
|
355
|
-
content: `# Secret & Credential Scanner
|
|
356
|
-
|
|
357
|
-
Proactively prevents committing confidential credentials to version control.
|
|
358
|
-
|
|
359
|
-
## High-Risk Patterns
|
|
360
|
-
- AWS access keys (\`AKIA...\`), GitHub PATs (\`ghp_...\`), OpenAI keys (\`sk-...\`).
|
|
361
|
-
- RSA / SSH private keys (\`-----BEGIN PRIVATE KEY-----\`).
|
|
362
|
-
- Hardcoded connection strings with passwords in source code.
|
|
363
|
-
`,
|
|
364
|
-
},
|
|
365
|
-
{
|
|
366
|
-
id: 'security-auditor',
|
|
367
|
-
name: 'security-auditor',
|
|
368
|
-
title: 'Security Audit Playbook',
|
|
369
|
-
category: 'security-auditing',
|
|
370
|
-
description: 'Use when auditing code for OWASP Top 10 vulnerabilities, access-control gaps, injection, or cryptographic failures.',
|
|
371
|
-
tags: ['security', 'audit', 'owasp'],
|
|
372
|
-
allowedTools: ['view_file', 'grep_search'],
|
|
373
|
-
custom: false,
|
|
374
|
-
content: `# Security Audit Playbook
|
|
375
|
-
|
|
376
|
-
Instructions for conducting deep application security audits.
|
|
377
|
-
|
|
378
|
-
## Focus Areas
|
|
379
|
-
1. **Injection Vectors**: SQL, Command, LDAP, and XSS vulnerabilities.
|
|
380
|
-
2. **Broken Access Control**: Missing authorization checks on sensitive REST / RPC routes.
|
|
381
|
-
3. **Cryptographic Failures**: Deprecated hashing algorithms (MD5/SHA1) or unencrypted tokens.
|
|
382
|
-
`,
|
|
383
|
-
},
|
|
384
|
-
];
|
|
82
|
+
export const DEFAULT_CATEGORIES = [];
|
|
83
|
+
export const DEFAULT_SKILLS = [];
|
|
385
84
|
// ─── Categories Management ────────────────────────────────────────────────
|
|
386
85
|
export function getContextSpaceHome() {
|
|
387
86
|
return resolveBrandHomeDir();
|
|
@@ -441,11 +140,15 @@ export async function getSkillCategories() {
|
|
|
441
140
|
for (const cat of DEFAULT_CATEGORIES) {
|
|
442
141
|
categoryMap.set(cat.id, { ...cat });
|
|
443
142
|
}
|
|
444
|
-
// 2. Load user categories (legacy fallback then primary)
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
143
|
+
// 2. Load user categories (legacy fallback then primary, or isolated custom home)
|
|
144
|
+
const csHome = process.env.CONTEXTSPACE_HOME?.trim();
|
|
145
|
+
const nfHome = process.env.NEXUSFLOW_HOME?.trim();
|
|
146
|
+
const candidateFiles = (csHome || nfHome)
|
|
147
|
+
? [getUserCategoriesPath()]
|
|
148
|
+
: [
|
|
149
|
+
path.join(os.homedir(), LEGACY_CONFIG_DIR_NAME, 'categories.json'),
|
|
150
|
+
path.join(os.homedir(), PRIMARY_CONFIG_DIR_NAME, 'categories.json'),
|
|
151
|
+
];
|
|
449
152
|
const userPath = getUserCategoriesPath();
|
|
450
153
|
if (!candidateFiles.includes(userPath)) {
|
|
451
154
|
candidateFiles.push(userPath);
|
|
@@ -560,31 +263,42 @@ async function loadSkillFromDir(skillDir, custom, catalogRoot = path.dirname(ski
|
|
|
560
263
|
metadataObj[LEGACY_RESOURCE_METADATA_KEY] !== null
|
|
561
264
|
? metadataObj[LEGACY_RESOURCE_METADATA_KEY]
|
|
562
265
|
: {};
|
|
266
|
+
const rawMetaTitle = metadataObj && typeof metadataObj.title === 'string' ? metadataObj.title : undefined;
|
|
563
267
|
const title = parsedMetadata.data.title ||
|
|
564
268
|
(typeof brandMetadata.title === 'string' ? brandMetadata.title : undefined) ||
|
|
269
|
+
rawMetaTitle ||
|
|
565
270
|
name
|
|
566
271
|
.split('-')
|
|
567
272
|
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
568
273
|
.join(' ');
|
|
274
|
+
const rawMetaCategory = metadataObj && typeof metadataObj.category === 'string' ? metadataObj.category : undefined;
|
|
569
275
|
const category = parsedMetadata.data.category ||
|
|
570
276
|
(typeof brandMetadata.category === 'string' ? brandMetadata.category : undefined) ||
|
|
277
|
+
rawMetaCategory ||
|
|
571
278
|
'general';
|
|
572
279
|
const description = parsedMetadata.data.description;
|
|
280
|
+
const rawMetaTags = metadataObj && Array.isArray(metadataObj.tags)
|
|
281
|
+
? metadataObj.tags.filter((t) => typeof t === 'string')
|
|
282
|
+
: metadataObj && typeof metadataObj.tags === 'string'
|
|
283
|
+
? metadataObj.tags.split(',').map((s) => s.trim()).filter(Boolean)
|
|
284
|
+
: [];
|
|
573
285
|
const tags = parsedMetadata.data.tags ||
|
|
574
286
|
(Array.isArray(brandMetadata.tags)
|
|
575
287
|
? brandMetadata.tags.filter((tag) => typeof tag === 'string')
|
|
576
|
-
:
|
|
288
|
+
: rawMetaTags);
|
|
577
289
|
const rawAllowedTools = parsedMetadata.data['allowed-tools'];
|
|
578
290
|
const allowedTools = Array.isArray(rawAllowedTools)
|
|
579
291
|
? rawAllowedTools
|
|
580
292
|
: rawAllowedTools
|
|
581
293
|
? rawAllowedTools.split(/\s+/).filter(Boolean)
|
|
582
294
|
: [];
|
|
583
|
-
// Inspect references
|
|
295
|
+
// Inspect references/, scripts/, and assets/ if present (per Agent Skills spec)
|
|
584
296
|
const referencesDir = path.join(skillDir, 'references');
|
|
585
297
|
const scriptsDir = path.join(skillDir, 'scripts');
|
|
298
|
+
const assetsDir = path.join(skillDir, 'assets');
|
|
586
299
|
const references = [];
|
|
587
300
|
const scripts = [];
|
|
301
|
+
const assets = [];
|
|
588
302
|
if (await fse.pathExists(referencesDir)) {
|
|
589
303
|
try {
|
|
590
304
|
await assertNoLinkedPathComponents(skillDir, referencesDir);
|
|
@@ -619,6 +333,25 @@ async function loadSkillFromDir(skillDir, custom, catalogRoot = path.dirname(ski
|
|
|
619
333
|
throw error;
|
|
620
334
|
}
|
|
621
335
|
}
|
|
336
|
+
if (await fse.pathExists(assetsDir)) {
|
|
337
|
+
try {
|
|
338
|
+
await assertNoLinkedPathComponents(skillDir, assetsDir);
|
|
339
|
+
const files = await fs.readdir(assetsDir, { withFileTypes: true });
|
|
340
|
+
for (const file of files) {
|
|
341
|
+
if (file.isSymbolicLink())
|
|
342
|
+
throw new Error(`Linked skill files are not allowed: ${file.name}`);
|
|
343
|
+
if (file.isFile()) {
|
|
344
|
+
assets.push({ name: file.name, relativePath: path.join('assets', file.name) });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
if (error instanceof Error && error.message.includes('Linked skill files'))
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const rawScope = parsedMetadata.data.scope ?? brandMetadata.scope;
|
|
354
|
+
const scope = rawScope === 'workspace' || rawScope === 'global' ? rawScope : undefined;
|
|
622
355
|
return {
|
|
623
356
|
id,
|
|
624
357
|
name,
|
|
@@ -632,22 +365,32 @@ async function loadSkillFromDir(skillDir, custom, catalogRoot = path.dirname(ski
|
|
|
632
365
|
sourcePath: skillDir,
|
|
633
366
|
references: references.length > 0 ? references : undefined,
|
|
634
367
|
scripts: scripts.length > 0 ? scripts : undefined,
|
|
368
|
+
assets: assets.length > 0 ? assets : undefined,
|
|
369
|
+
scope,
|
|
370
|
+
metadata: metadataObj,
|
|
371
|
+
license: parsedMetadata.data.license,
|
|
372
|
+
compatibility: parsedMetadata.data.compatibility,
|
|
635
373
|
};
|
|
636
374
|
}
|
|
637
375
|
/**
|
|
638
376
|
* Retrieves all available skills (built-in templates + user directory + optional workspace directory).
|
|
639
377
|
*/
|
|
640
|
-
export async function getAllSkills(
|
|
378
|
+
export async function getAllSkills(workspacePath) {
|
|
641
379
|
const skillMap = new Map();
|
|
642
380
|
// 1. Built-in template skills
|
|
643
381
|
for (const s of DEFAULT_SKILLS) {
|
|
644
|
-
skillMap.set(s.id, { ...s });
|
|
382
|
+
skillMap.set(s.id, { ...s, scope: 'global' });
|
|
645
383
|
}
|
|
646
|
-
// 2. User directory (~/.nexusflow/skills/ and ~/.contextspace/skills
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
384
|
+
// 2. User directory (~/.nexusflow/skills/ and ~/.contextspace/skills/, or isolated custom home)
|
|
385
|
+
const csHome = process.env.CONTEXTSPACE_HOME?.trim();
|
|
386
|
+
const nfHome = process.env.NEXUSFLOW_HOME?.trim();
|
|
387
|
+
const candidateDirs = (csHome || nfHome)
|
|
388
|
+
? [getUserSkillsDir()]
|
|
389
|
+
: [
|
|
390
|
+
path.join(os.homedir(), LEGACY_CONFIG_DIR_NAME, 'skills'),
|
|
391
|
+
path.join(os.homedir(), PRIMARY_CONFIG_DIR_NAME, 'skills'),
|
|
392
|
+
path.join(os.homedir(), '.agents', 'skills'),
|
|
393
|
+
];
|
|
651
394
|
const activeSkillsDir = getUserSkillsDir();
|
|
652
395
|
if (!candidateDirs.includes(activeSkillsDir)) {
|
|
653
396
|
candidateDirs.push(activeSkillsDir);
|
|
@@ -669,6 +412,7 @@ export async function getAllSkills(_workspacePath) {
|
|
|
669
412
|
if (isBuiltIn) {
|
|
670
413
|
throw new Error(`A resource named "${loaded.id}" already exists in the built-in catalog.`);
|
|
671
414
|
}
|
|
415
|
+
loaded.scope = loaded.scope || 'global';
|
|
672
416
|
skillMap.set(loaded.id, loaded);
|
|
673
417
|
}
|
|
674
418
|
}
|
|
@@ -685,12 +429,93 @@ export async function getAllSkills(_workspacePath) {
|
|
|
685
429
|
}
|
|
686
430
|
}
|
|
687
431
|
}
|
|
432
|
+
// 3. Workspace-local skills (<workspace>/.agents/skills/)
|
|
433
|
+
if (workspacePath) {
|
|
434
|
+
try {
|
|
435
|
+
const canonicalWorkspace = await fs.realpath(workspacePath).catch(() => path.resolve(workspacePath));
|
|
436
|
+
const workspaceSkillsDir = path.join(canonicalWorkspace, '.agents', 'skills');
|
|
437
|
+
if (await fse.pathExists(workspaceSkillsDir)) {
|
|
438
|
+
await assertPathIsNotLink(workspaceSkillsDir);
|
|
439
|
+
const materializedGlobalSkillIds = new Set();
|
|
440
|
+
const lockPathPrimary = path.join(canonicalWorkspace, PRIMARY_CONFIG_DIR_NAME, 'resources.lock.json');
|
|
441
|
+
const lockPathLegacy = path.join(canonicalWorkspace, LEGACY_CONFIG_DIR_NAME, 'resources.lock.json');
|
|
442
|
+
const lockPath = (await fse.pathExists(lockPathPrimary))
|
|
443
|
+
? lockPathPrimary
|
|
444
|
+
: (await fse.pathExists(lockPathLegacy))
|
|
445
|
+
? lockPathLegacy
|
|
446
|
+
: null;
|
|
447
|
+
if (lockPath) {
|
|
448
|
+
try {
|
|
449
|
+
const lockJson = await fse.readJson(lockPath);
|
|
450
|
+
const outputs = Array.isArray(lockJson?.outputs)
|
|
451
|
+
? lockJson.outputs
|
|
452
|
+
: Array.isArray(lockJson?.managedFiles)
|
|
453
|
+
? lockJson.managedFiles
|
|
454
|
+
: [];
|
|
455
|
+
for (const out of outputs) {
|
|
456
|
+
if (out && out.kind === 'skill') {
|
|
457
|
+
const outPath = typeof out.path === 'string' ? out.path.replaceAll('\\', '/') : '';
|
|
458
|
+
if (out.adapter === 'agent-skill-v1' || outPath.startsWith('.agents/skills/')) {
|
|
459
|
+
if (typeof out.resourceId === 'string') {
|
|
460
|
+
materializedGlobalSkillIds.add(out.resourceId);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// Ignore unreadable or corrupt lock file
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const entries = await fs.readdir(workspaceSkillsDir, { withFileTypes: true });
|
|
471
|
+
if (Array.isArray(entries)) {
|
|
472
|
+
for (const entry of entries) {
|
|
473
|
+
const isDir = typeof entry === 'string' ? true : entry.isDirectory ? entry.isDirectory() : true;
|
|
474
|
+
const entryName = typeof entry === 'string' ? entry : entry.name;
|
|
475
|
+
if (isDir) {
|
|
476
|
+
try {
|
|
477
|
+
const skillDir = path.join(workspaceSkillsDir, entryName);
|
|
478
|
+
const loaded = await loadSkillFromDir(skillDir, true, workspaceSkillsDir);
|
|
479
|
+
if (loaded) {
|
|
480
|
+
const isMaterializedGlobal = loaded.scope === 'global' ||
|
|
481
|
+
(loaded.scope !== 'workspace' && materializedGlobalSkillIds.has(loaded.id));
|
|
482
|
+
if (isMaterializedGlobal) {
|
|
483
|
+
// Materialized global skill: do NOT mark its scope as 'workspace'.
|
|
484
|
+
// If its global counterpart was deleted machine-wide, do NOT resurrect it as an active workspace skill.
|
|
485
|
+
if (skillMap.has(loaded.id)) {
|
|
486
|
+
const globalSkill = skillMap.get(loaded.id);
|
|
487
|
+
globalSkill.scope = 'global';
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
loaded.scope = 'workspace';
|
|
492
|
+
skillMap.set(loaded.id, loaded);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
498
|
+
console.warn(`Skipping invalid workspace skill "${entryName}": ${message}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
console.error('Failed to load workspace skills from', workspacePath, err);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
688
509
|
return Array.from(skillMap.values());
|
|
689
510
|
}
|
|
690
511
|
/**
|
|
691
512
|
* Saves or updates a portable skill package.
|
|
692
513
|
*/
|
|
693
514
|
export async function saveSkill(skill, options = {}) {
|
|
515
|
+
const scope = options.scope ?? 'global';
|
|
516
|
+
if (scope === 'workspace' && !options.workspacePath) {
|
|
517
|
+
throw new Error('workspacePath is required when saving a workspace-scoped skill.');
|
|
518
|
+
}
|
|
694
519
|
const rawId = skill.id || skill.name;
|
|
695
520
|
const idResult = resourceIdSchema.safeParse(rawId);
|
|
696
521
|
if (!idResult.success) {
|
|
@@ -711,20 +536,19 @@ export async function saveSkill(skill, options = {}) {
|
|
|
711
536
|
throw new Error('Skill description is required.');
|
|
712
537
|
if (!skill.content.trim())
|
|
713
538
|
throw new Error('Skill content is required.');
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
await
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
await
|
|
720
|
-
const
|
|
721
|
-
const backupDir = path.join(userSkillsDir, `.backup-${id}-${randomUUID()}`);
|
|
539
|
+
const executeSave = async (skillsBaseDir) => {
|
|
540
|
+
await fse.ensureDir(skillsBaseDir);
|
|
541
|
+
await assertPathIsNotLink(skillsBaseDir);
|
|
542
|
+
const targetDir = assertPathWithin(skillsBaseDir, path.join(skillsBaseDir, id));
|
|
543
|
+
await assertNoLinkedPathComponents(skillsBaseDir, targetDir);
|
|
544
|
+
const stagingDir = await fs.mkdtemp(path.join(skillsBaseDir, `.staging-${id}-`));
|
|
545
|
+
const backupDir = path.join(skillsBaseDir, `.backup-${id}-${randomUUID()}`);
|
|
722
546
|
let movedExisting = false;
|
|
723
547
|
let installedStaging = false;
|
|
724
548
|
try {
|
|
725
549
|
let existingFrontmatter;
|
|
726
550
|
if (await fse.pathExists(targetDir)) {
|
|
727
|
-
await assertNoLinkedPathComponents(
|
|
551
|
+
await assertNoLinkedPathComponents(skillsBaseDir, targetDir);
|
|
728
552
|
for (const entry of await fs.readdir(targetDir, { withFileTypes: true })) {
|
|
729
553
|
if (entry.isSymbolicLink()) {
|
|
730
554
|
throw new Error(`Linked skill package entries are not allowed: ${entry.name}`);
|
|
@@ -742,6 +566,9 @@ export async function saveSkill(skill, options = {}) {
|
|
|
742
566
|
existingFrontmatter = parsedCurrentFrontmatter.data;
|
|
743
567
|
}
|
|
744
568
|
const existingMetaObj = existingFrontmatter?.metadata;
|
|
569
|
+
const callerMetadata = skill.metadata && typeof skill.metadata === 'object' && !Array.isArray(skill.metadata)
|
|
570
|
+
? skill.metadata
|
|
571
|
+
: undefined;
|
|
745
572
|
const existingBrandMetadata = existingMetaObj &&
|
|
746
573
|
typeof existingMetaObj[RESOURCE_METADATA_KEY] === 'object' &&
|
|
747
574
|
existingMetaObj[RESOURCE_METADATA_KEY] !== null
|
|
@@ -751,19 +578,59 @@ export async function saveSkill(skill, options = {}) {
|
|
|
751
578
|
existingMetaObj[LEGACY_RESOURCE_METADATA_KEY] !== null
|
|
752
579
|
? existingMetaObj[LEGACY_RESOURCE_METADATA_KEY]
|
|
753
580
|
: {};
|
|
581
|
+
const callerBrandMetadata = callerMetadata &&
|
|
582
|
+
typeof callerMetadata[RESOURCE_METADATA_KEY] === 'object' &&
|
|
583
|
+
callerMetadata[RESOURCE_METADATA_KEY] !== null
|
|
584
|
+
? callerMetadata[RESOURCE_METADATA_KEY]
|
|
585
|
+
: callerMetadata &&
|
|
586
|
+
typeof callerMetadata[LEGACY_RESOURCE_METADATA_KEY] === 'object' &&
|
|
587
|
+
callerMetadata[LEGACY_RESOURCE_METADATA_KEY] !== null
|
|
588
|
+
? callerMetadata[LEGACY_RESOURCE_METADATA_KEY]
|
|
589
|
+
: {};
|
|
590
|
+
const inferredTitle = skill.title ||
|
|
591
|
+
(callerMetadata && typeof callerMetadata.title === 'string' ? callerMetadata.title : undefined) ||
|
|
592
|
+
(typeof existingBrandMetadata.title === 'string' ? existingBrandMetadata.title : undefined) ||
|
|
593
|
+
id;
|
|
594
|
+
const inferredCategory = options.category ||
|
|
595
|
+
skill.category ||
|
|
596
|
+
(callerMetadata && typeof callerMetadata.category === 'string' ? callerMetadata.category : undefined) ||
|
|
597
|
+
(typeof existingBrandMetadata.category === 'string' ? existingBrandMetadata.category : undefined) ||
|
|
598
|
+
'general';
|
|
599
|
+
const callerMetaTags = callerMetadata && Array.isArray(callerMetadata.tags)
|
|
600
|
+
? callerMetadata.tags.filter((t) => typeof t === 'string')
|
|
601
|
+
: undefined;
|
|
602
|
+
const existingBrandTags = Array.isArray(existingBrandMetadata.tags)
|
|
603
|
+
? existingBrandMetadata.tags.filter((t) => typeof t === 'string')
|
|
604
|
+
: undefined;
|
|
605
|
+
const inferredTags = skill.tags || callerMetaTags || existingBrandTags || [];
|
|
754
606
|
const metadataPayload = {
|
|
755
607
|
...existingBrandMetadata,
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
608
|
+
...callerBrandMetadata,
|
|
609
|
+
title: inferredTitle,
|
|
610
|
+
category: inferredCategory,
|
|
611
|
+
tags: inferredTags,
|
|
612
|
+
scope,
|
|
759
613
|
};
|
|
614
|
+
const mergedCustomMetadata = {
|
|
615
|
+
...(existingFrontmatter?.metadata ?? {}),
|
|
616
|
+
...(callerMetadata ?? {}),
|
|
617
|
+
};
|
|
618
|
+
delete mergedCustomMetadata[RESOURCE_METADATA_KEY];
|
|
619
|
+
delete mergedCustomMetadata[LEGACY_RESOURCE_METADATA_KEY];
|
|
620
|
+
delete mergedCustomMetadata['__proto__'];
|
|
621
|
+
delete mergedCustomMetadata['constructor'];
|
|
622
|
+
delete mergedCustomMetadata['prototype'];
|
|
760
623
|
const metadata = {
|
|
761
624
|
name: id,
|
|
625
|
+
title: inferredTitle,
|
|
626
|
+
category: inferredCategory,
|
|
762
627
|
description,
|
|
763
|
-
|
|
764
|
-
|
|
628
|
+
tags: inferredTags,
|
|
629
|
+
license: skill.license ?? existingFrontmatter?.license,
|
|
630
|
+
compatibility: skill.compatibility ?? existingFrontmatter?.compatibility,
|
|
631
|
+
scope,
|
|
765
632
|
metadata: {
|
|
766
|
-
...
|
|
633
|
+
...mergedCustomMetadata,
|
|
767
634
|
[RESOURCE_METADATA_KEY]: metadataPayload,
|
|
768
635
|
[LEGACY_RESOURCE_METADATA_KEY]: metadataPayload,
|
|
769
636
|
},
|
|
@@ -777,6 +644,7 @@ export async function saveSkill(skill, options = {}) {
|
|
|
777
644
|
for (const [directory, files] of [
|
|
778
645
|
['references', skill.references],
|
|
779
646
|
['scripts', skill.scripts],
|
|
647
|
+
['assets', skill.assets],
|
|
780
648
|
]) {
|
|
781
649
|
if (files === undefined)
|
|
782
650
|
continue;
|
|
@@ -806,18 +674,24 @@ export async function saveSkill(skill, options = {}) {
|
|
|
806
674
|
}
|
|
807
675
|
await options.beforeCommit?.();
|
|
808
676
|
if (await fse.pathExists(targetDir)) {
|
|
809
|
-
await assertNoLinkedPathComponents(
|
|
677
|
+
await assertNoLinkedPathComponents(skillsBaseDir, targetDir);
|
|
810
678
|
await fs.rename(targetDir, backupDir);
|
|
811
679
|
movedExisting = true;
|
|
812
680
|
}
|
|
813
681
|
await fs.rename(stagingDir, targetDir);
|
|
814
682
|
installedStaging = true;
|
|
815
|
-
const loaded = await loadSkillFromDir(targetDir, true,
|
|
683
|
+
const loaded = await loadSkillFromDir(targetDir, true, skillsBaseDir);
|
|
816
684
|
if (!loaded)
|
|
817
685
|
throw new Error('Saved skill could not be loaded.');
|
|
686
|
+
loaded.scope = scope;
|
|
818
687
|
if (movedExisting)
|
|
819
688
|
await fse.remove(backupDir).catch(() => { });
|
|
820
|
-
|
|
689
|
+
const skillFilePath = path.join(targetDir, 'SKILL.md');
|
|
690
|
+
return {
|
|
691
|
+
...loaded,
|
|
692
|
+
path: skillFilePath,
|
|
693
|
+
skill: loaded,
|
|
694
|
+
};
|
|
821
695
|
}
|
|
822
696
|
catch (error) {
|
|
823
697
|
await fse.remove(stagingDir).catch(() => { });
|
|
@@ -828,16 +702,42 @@ export async function saveSkill(skill, options = {}) {
|
|
|
828
702
|
}
|
|
829
703
|
throw error;
|
|
830
704
|
}
|
|
831
|
-
}
|
|
705
|
+
};
|
|
706
|
+
if (scope === 'workspace') {
|
|
707
|
+
const canonicalWorkspace = await fs.realpath(options.workspacePath).catch(() => path.resolve(options.workspacePath));
|
|
708
|
+
const workspaceSkillsDir = path.join(canonicalWorkspace, '.agents', 'skills');
|
|
709
|
+
return runWorkspaceConfigMutation(async () => {
|
|
710
|
+
await assertNoLinkedPathComponents(canonicalWorkspace, workspaceSkillsDir);
|
|
711
|
+
return executeSave(workspaceSkillsDir);
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
return withCatalogLock(async () => executeSave(path.resolve(getUserSkillsDir())));
|
|
832
715
|
}
|
|
833
716
|
/**
|
|
834
|
-
* Deletes a
|
|
717
|
+
* Deletes a skill safely from the global or workspace catalog.
|
|
835
718
|
*/
|
|
836
|
-
export async function deleteSkill(id) {
|
|
719
|
+
export async function deleteSkill(id, options) {
|
|
837
720
|
const idResult = resourceIdSchema.safeParse(id);
|
|
838
721
|
if (!idResult.success || idResult.data !== id) {
|
|
839
722
|
throw new Error('Invalid skill ID format.');
|
|
840
723
|
}
|
|
724
|
+
const scope = options?.scope ?? 'global';
|
|
725
|
+
if (scope === 'workspace') {
|
|
726
|
+
if (!options?.workspacePath) {
|
|
727
|
+
throw new Error('workspacePath is required when deleting a workspace-scoped skill.');
|
|
728
|
+
}
|
|
729
|
+
const wsPath = options.workspacePath;
|
|
730
|
+
const canonicalWorkspace = await fs.realpath(wsPath).catch(() => path.resolve(wsPath));
|
|
731
|
+
const workspaceSkillsDir = path.join(canonicalWorkspace, '.agents', 'skills');
|
|
732
|
+
await runWorkspaceConfigMutation(async () => {
|
|
733
|
+
const targetDir = assertPathWithin(canonicalWorkspace, path.join(workspaceSkillsDir, id));
|
|
734
|
+
await assertNoLinkedPathComponents(canonicalWorkspace, targetDir);
|
|
735
|
+
if (!(await fse.pathExists(targetDir)))
|
|
736
|
+
throw new Error('Skill not found.');
|
|
737
|
+
await fse.remove(targetDir);
|
|
738
|
+
});
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
841
741
|
await withCatalogLock(async () => {
|
|
842
742
|
const userSkillsDir = path.resolve(getUserSkillsDir());
|
|
843
743
|
await fse.ensureDir(userSkillsDir);
|