@alavida/agentpack 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +16 -2
  2. package/bin/intent.js +20 -0
  3. package/package.json +11 -5
  4. package/skills/agentpack-cli/SKILL.md +16 -1
  5. package/skills/agentpack-cli/references/skill-lifecycle.md +21 -1
  6. package/skills/authoring-skillgraphs-from-knowledge/SKILL.md +148 -0
  7. package/skills/authoring-skillgraphs-from-knowledge/references/authored-metadata.md +6 -0
  8. package/skills/developing-and-testing-skills/SKILL.md +109 -0
  9. package/skills/developing-and-testing-skills/references/local-workbench.md +7 -0
  10. package/skills/getting-started-skillgraphs/SKILL.md +115 -0
  11. package/skills/getting-started-skillgraphs/references/command-routing.md +7 -0
  12. package/skills/identifying-skill-opportunities/SKILL.md +119 -0
  13. package/skills/identifying-skill-opportunities/references/capability-boundaries.md +6 -0
  14. package/skills/maintaining-skillgraph-freshness/SKILL.md +110 -0
  15. package/skills/repairing-broken-skill-or-plugin-state/SKILL.md +112 -0
  16. package/skills/repairing-broken-skill-or-plugin-state/references/diagnostic-flows.md +6 -0
  17. package/skills/shipping-production-plugins-and-packages/SKILL.md +123 -0
  18. package/skills/shipping-production-plugins-and-packages/references/plugin-delivery.md +6 -0
  19. package/src/application/plugins/build-plugin.js +5 -0
  20. package/src/application/plugins/inspect-plugin-bundle.js +5 -0
  21. package/src/application/plugins/validate-plugin-bundle.js +5 -0
  22. package/src/application/skills/build-skill-workbench-model.js +194 -0
  23. package/src/application/skills/inspect-skill.js +5 -0
  24. package/src/application/skills/list-stale-skills.js +9 -0
  25. package/src/application/skills/run-skill-workbench-action.js +23 -0
  26. package/src/application/skills/start-skill-dev-workbench.js +192 -0
  27. package/src/application/skills/validate-skills.js +5 -0
  28. package/src/cli.js +1 -1
  29. package/src/commands/plugin.js +7 -4
  30. package/src/commands/skills.js +14 -9
  31. package/src/dashboard/App.jsx +343 -0
  32. package/src/dashboard/components/Breadcrumbs.jsx +45 -0
  33. package/src/dashboard/components/ControlStrip.jsx +153 -0
  34. package/src/dashboard/components/InspectorPanel.jsx +203 -0
  35. package/src/dashboard/components/SkillGraph.jsx +567 -0
  36. package/src/dashboard/components/Tooltip.jsx +111 -0
  37. package/src/dashboard/dist/dashboard.js +26692 -0
  38. package/src/dashboard/index.html +81 -0
  39. package/src/dashboard/lib/api.js +19 -0
  40. package/src/dashboard/lib/router.js +15 -0
  41. package/src/dashboard/main.jsx +4 -0
  42. package/src/domain/plugins/load-plugin-definition.js +163 -0
  43. package/src/domain/plugins/plugin-diagnostic-error.js +18 -0
  44. package/src/domain/plugins/plugin-requirements.js +15 -0
  45. package/src/domain/skills/skill-graph.js +137 -0
  46. package/src/domain/skills/skill-model.js +187 -0
  47. package/src/domain/skills/skill-provenance.js +69 -0
  48. package/src/infrastructure/fs/build-state-repository.js +16 -0
  49. package/src/infrastructure/fs/install-state-repository.js +16 -0
  50. package/src/infrastructure/runtime/materialize-skills.js +117 -0
  51. package/src/infrastructure/runtime/open-browser.js +20 -0
  52. package/src/infrastructure/runtime/skill-dev-workbench-server.js +96 -0
  53. package/src/infrastructure/runtime/watch-skill-workbench.js +68 -0
  54. package/src/infrastructure/runtime/watch-tree.js +44 -0
  55. package/src/lib/plugins.js +46 -117
  56. package/src/lib/skills.js +141 -459
  57. package/src/utils/errors.js +33 -1
@@ -0,0 +1,81 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Skill Dev Workbench</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Noto+Sans+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ color-scheme: dark;
13
+ --bg: #1a1916;
14
+ --surface: #252320;
15
+ --border: rgba(255, 255, 255, 0.06);
16
+ --border-bright: rgba(255, 255, 255, 0.12);
17
+ --text: #e8e4dc;
18
+ --text-dim: #9a9488;
19
+ --text-faint: #6b655c;
20
+
21
+ --status-current: #8fa67e;
22
+ --status-stale: #d4a45e;
23
+ --status-affected: #c4956e;
24
+ --status-unknown: #9a9488;
25
+
26
+ --edge-provenance: #7a9abb;
27
+ --edge-requires: #8fa67e;
28
+ --accent: #ffbf47;
29
+
30
+ --font-body: 'Crimson Pro', Georgia, 'Times New Roman', serif;
31
+ --font-mono: 'Noto Sans Mono', 'Courier New', monospace;
32
+ }
33
+
34
+ [data-theme="light"] {
35
+ color-scheme: light;
36
+ --bg: #f5f2ed;
37
+ --surface: #e8e4dc;
38
+ --border: rgba(0, 0, 0, 0.10);
39
+ --border-bright: rgba(0, 0, 0, 0.20);
40
+ --text: #1a1816;
41
+ --text-dim: #3d3830;
42
+ --text-faint: #6b655c;
43
+
44
+ --status-current: #3d6e2c;
45
+ --status-stale: #a07020;
46
+ --status-affected: #8c5530;
47
+ --status-unknown: #6b655c;
48
+
49
+ --edge-provenance: #2e6080;
50
+ --edge-requires: #3d6e2c;
51
+ --accent: #a07020;
52
+ }
53
+
54
+ * { box-sizing: border-box; margin: 0; padding: 0; }
55
+
56
+ body {
57
+ font-family: var(--font-body);
58
+ background: var(--bg);
59
+ color: var(--text);
60
+ overflow: hidden;
61
+ height: 100vh;
62
+ -webkit-font-smoothing: antialiased;
63
+ }
64
+
65
+ #app {
66
+ height: 100vh;
67
+ display: flex;
68
+ flex-direction: column;
69
+ }
70
+
71
+ @keyframes stale-pulse {
72
+ 0%, 100% { opacity: 0.4; }
73
+ 50% { opacity: 0.8; }
74
+ }
75
+ </style>
76
+ </head>
77
+ <body>
78
+ <div id="app" data-app-root></div>
79
+ <script type="module" src="/assets/dashboard.js"></script>
80
+ </body>
81
+ </html>
@@ -0,0 +1,19 @@
1
+ export async function fetchWorkbenchModel(skillPackageName) {
2
+ const params = skillPackageName ? `?skill=${encodeURIComponent(skillPackageName)}` : '';
3
+ const response = await fetch(`/api/model${params}`);
4
+ if (!response.ok) {
5
+ const body = await response.json().catch(() => ({}));
6
+ throw new Error(body.error || `Failed to load workbench model: ${response.status}`);
7
+ }
8
+ return response.json();
9
+ }
10
+
11
+ export async function runWorkbenchAction(action) {
12
+ const response = await fetch(`/api/actions/${action}`, {
13
+ method: 'POST',
14
+ });
15
+ if (!response.ok) {
16
+ throw new Error(`Failed to run ${action}: ${response.status}`);
17
+ }
18
+ return response.json();
19
+ }
@@ -0,0 +1,15 @@
1
+ export function getSkillFromHash() {
2
+ const hash = window.location.hash;
3
+ const match = hash.match(/^#\/skill\/(.+)$/);
4
+ return match ? decodeURIComponent(match[1]) : null;
5
+ }
6
+
7
+ export function setSkillHash(packageName) {
8
+ window.location.hash = `#/skill/${encodeURIComponent(packageName)}`;
9
+ }
10
+
11
+ export function onHashChange(callback) {
12
+ window.addEventListener('hashchange', () => {
13
+ callback(getSkillFromHash());
14
+ });
15
+ }
@@ -0,0 +1,4 @@
1
+ import { createRoot } from 'react-dom/client';
2
+ import { App } from './App.jsx';
3
+
4
+ createRoot(document.getElementById('app')).render(<App />);
@@ -0,0 +1,163 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { basename, join, resolve } from 'node:path';
3
+ import { normalizeRepoPath } from '../skills/skill-model.js';
4
+ import { findRepoRoot } from '../../lib/context.js';
5
+ import { NotFoundError } from '../../utils/errors.js';
6
+ import { PluginDiagnosticError } from './plugin-diagnostic-error.js';
7
+ import { getPluginRequirementLevel } from './plugin-requirements.js';
8
+
9
+ function resolvePluginDir(repoRoot, target) {
10
+ const absoluteTarget = resolve(repoRoot, target);
11
+ if (!existsSync(absoluteTarget)) {
12
+ throw new NotFoundError('plugin not found', {
13
+ code: 'plugin_not_found',
14
+ suggestion: `Target: ${target}`,
15
+ });
16
+ }
17
+
18
+ return absoluteTarget;
19
+ }
20
+
21
+ function inferPluginPackageName(pluginDir) {
22
+ return `@alavida-ai/plugin-${basename(pluginDir)}`;
23
+ }
24
+
25
+ function readPluginPackageJson(repoRoot, pluginDir, requirementLevel) {
26
+ const packageJsonPath = join(pluginDir, 'package.json');
27
+ const displayPath = normalizeRepoPath(repoRoot, packageJsonPath);
28
+ const example = {
29
+ name: inferPluginPackageName(pluginDir),
30
+ version: '0.1.0',
31
+ };
32
+
33
+ if (!existsSync(packageJsonPath)) {
34
+ throw new PluginDiagnosticError('No package.json found for plugin target', {
35
+ code: 'missing_plugin_package_json',
36
+ path: displayPath,
37
+ nextSteps: [
38
+ {
39
+ action: 'create_file',
40
+ path: displayPath,
41
+ reason: 'A plugin must declare package metadata before it can be inspected',
42
+ example,
43
+ },
44
+ ],
45
+ details: {
46
+ requirementLevel,
47
+ missing: ['package.json'],
48
+ },
49
+ });
50
+ }
51
+
52
+ let pkg;
53
+ try {
54
+ pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
55
+ } catch {
56
+ throw new PluginDiagnosticError('Plugin package.json contains invalid JSON', {
57
+ code: 'invalid_plugin_package_json',
58
+ path: displayPath,
59
+ nextSteps: [
60
+ {
61
+ action: 'edit_file',
62
+ path: displayPath,
63
+ reason: 'Inspect and validate require a readable package.json file',
64
+ example,
65
+ },
66
+ ],
67
+ details: {
68
+ requirementLevel,
69
+ },
70
+ });
71
+ }
72
+
73
+ const missingFields = ['name', 'version'].filter((field) => !pkg[field]);
74
+ if (missingFields.length > 0) {
75
+ throw new PluginDiagnosticError(`Plugin package.json missing required fields: ${missingFields.join(', ')}`, {
76
+ code: 'missing_plugin_package_fields',
77
+ path: displayPath,
78
+ nextSteps: [
79
+ {
80
+ action: 'edit_file',
81
+ path: displayPath,
82
+ reason: 'A plugin package.json must include required package metadata before inspection can continue',
83
+ example: {
84
+ ...example,
85
+ ...('name' in pkg ? { name: pkg.name } : {}),
86
+ ...('version' in pkg ? { version: pkg.version } : {}),
87
+ },
88
+ },
89
+ ],
90
+ details: {
91
+ requirementLevel,
92
+ missingFields,
93
+ },
94
+ });
95
+ }
96
+
97
+ return {
98
+ packageJsonPath,
99
+ packageJson: pkg,
100
+ packageName: pkg.name,
101
+ packageVersion: pkg.version,
102
+ };
103
+ }
104
+
105
+ function readPluginManifest(repoRoot, pluginDir, requirementLevel) {
106
+ const pluginManifestPath = join(pluginDir, '.claude-plugin', 'plugin.json');
107
+ const displayPath = normalizeRepoPath(repoRoot, pluginManifestPath);
108
+
109
+ if (!existsSync(pluginManifestPath)) {
110
+ throw new PluginDiagnosticError('Plugin manifest is missing', {
111
+ code: 'missing_plugin_manifest',
112
+ path: displayPath,
113
+ nextSteps: [
114
+ {
115
+ action: 'create_file',
116
+ path: displayPath,
117
+ reason: 'Plugin commands require .claude-plugin/plugin.json to describe the runtime plugin entrypoint',
118
+ example: {
119
+ name: basename(pluginDir),
120
+ description: 'Describe what this plugin provides.',
121
+ },
122
+ },
123
+ ],
124
+ details: {
125
+ requirementLevel,
126
+ missing: ['.claude-plugin/plugin.json'],
127
+ },
128
+ });
129
+ }
130
+
131
+ return {
132
+ pluginManifestPath,
133
+ pluginManifest: JSON.parse(readFileSync(pluginManifestPath, 'utf-8')),
134
+ };
135
+ }
136
+
137
+ export function loadPluginDefinition(target, {
138
+ cwd = process.cwd(),
139
+ requirementLevel = 'inspect',
140
+ } = {}) {
141
+ const repoRoot = findRepoRoot(cwd);
142
+ const pluginDir = resolvePluginDir(repoRoot, target);
143
+ const requirements = getPluginRequirementLevel(requirementLevel);
144
+
145
+ const packageData = readPluginPackageJson(repoRoot, pluginDir, requirementLevel);
146
+ const manifestData = requirements.pluginManifest
147
+ ? readPluginManifest(repoRoot, pluginDir, requirementLevel)
148
+ : {
149
+ pluginManifestPath: join(pluginDir, '.claude-plugin', 'plugin.json'),
150
+ pluginManifest: null,
151
+ };
152
+
153
+ return {
154
+ repoRoot,
155
+ pluginDir,
156
+ packageJsonPath: packageData.packageJsonPath,
157
+ packageJson: packageData.packageJson,
158
+ packageName: packageData.packageName,
159
+ packageVersion: packageData.packageVersion,
160
+ pluginManifestPath: manifestData.pluginManifestPath,
161
+ pluginManifest: manifestData.pluginManifest,
162
+ };
163
+ }
@@ -0,0 +1,18 @@
1
+ import { ValidationError } from '../../utils/errors.js';
2
+
3
+ export class PluginDiagnosticError extends ValidationError {
4
+ constructor(message, {
5
+ code,
6
+ path,
7
+ nextSteps = [],
8
+ details = {},
9
+ } = {}) {
10
+ super(message, {
11
+ code,
12
+ path,
13
+ nextSteps,
14
+ details,
15
+ });
16
+ this.name = 'PluginDiagnosticError';
17
+ }
18
+ }
@@ -0,0 +1,15 @@
1
+ export const PLUGIN_REQUIREMENTS = {
2
+ inspect: {
3
+ pluginManifest: true,
4
+ },
5
+ validate: {
6
+ pluginManifest: true,
7
+ },
8
+ build: {
9
+ pluginManifest: true,
10
+ },
11
+ };
12
+
13
+ export function getPluginRequirementLevel(level = 'inspect') {
14
+ return PLUGIN_REQUIREMENTS[level] || PLUGIN_REQUIREMENTS.inspect;
15
+ }
@@ -0,0 +1,137 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export function readSkillGraphNode(repoRoot, packageDir, {
5
+ directInstallNames = new Set(),
6
+ parseSkillFrontmatterFile,
7
+ readPackageMetadata,
8
+ findPackageDirByName,
9
+ normalizeDisplayPath,
10
+ } = {}) {
11
+ const skillFile = join(packageDir, 'SKILL.md');
12
+ if (!existsSync(skillFile)) return null;
13
+
14
+ const skillMetadata = parseSkillFrontmatterFile(skillFile);
15
+ const packageMetadata = readPackageMetadata(packageDir);
16
+ if (!packageMetadata.packageName) return null;
17
+
18
+ const dependencyNames = Object.keys(packageMetadata.dependencies || {})
19
+ .filter((dependencyName) => {
20
+ const localPackageDir = findPackageDirByName(repoRoot, dependencyName);
21
+ if (localPackageDir && existsSync(join(localPackageDir, 'SKILL.md'))) return true;
22
+ const installedPackageDir = join(repoRoot, 'node_modules', ...dependencyName.split('/'));
23
+ return existsSync(join(installedPackageDir, 'SKILL.md'));
24
+ })
25
+ .sort((a, b) => a.localeCompare(b));
26
+
27
+ return {
28
+ name: skillMetadata.name,
29
+ description: skillMetadata.description,
30
+ packageName: packageMetadata.packageName,
31
+ packageVersion: packageMetadata.packageVersion,
32
+ skillPath: normalizeDisplayPath(repoRoot, packageDir),
33
+ skillFile: normalizeDisplayPath(repoRoot, skillFile),
34
+ direct: directInstallNames.has(packageMetadata.packageName),
35
+ dependencies: dependencyNames,
36
+ };
37
+ }
38
+
39
+ export function buildSkillGraph(repoRoot, packageDirs, options) {
40
+ const nodes = new Map();
41
+
42
+ for (const packageDir of packageDirs) {
43
+ const node = readSkillGraphNode(repoRoot, packageDir, options);
44
+ if (!node) continue;
45
+ nodes.set(node.packageName, node);
46
+ }
47
+
48
+ return nodes;
49
+ }
50
+
51
+ export function buildReverseDependencies(nodes) {
52
+ const reverse = new Map();
53
+ for (const packageName of nodes.keys()) reverse.set(packageName, []);
54
+
55
+ for (const node of nodes.values()) {
56
+ for (const dependencyName of node.dependencies || []) {
57
+ if (!reverse.has(dependencyName)) continue;
58
+ reverse.get(dependencyName).push(node.packageName);
59
+ }
60
+ }
61
+
62
+ for (const values of reverse.values()) values.sort((a, b) => a.localeCompare(b));
63
+ return reverse;
64
+ }
65
+
66
+ export function buildSkillStatusMap(nodes, staleSkills = new Set()) {
67
+ const cache = new Map();
68
+
69
+ function resolveStatus(packageName, seen = new Set()) {
70
+ if (cache.has(packageName)) return cache.get(packageName);
71
+ if (staleSkills.has(packageName)) {
72
+ cache.set(packageName, 'stale');
73
+ return 'stale';
74
+ }
75
+
76
+ if (seen.has(packageName)) return 'current';
77
+ seen.add(packageName);
78
+
79
+ const node = nodes.get(packageName);
80
+ if (!node) {
81
+ cache.set(packageName, null);
82
+ return null;
83
+ }
84
+
85
+ const dependencyStatuses = (node.dependencies || [])
86
+ .map((dependencyName) => resolveStatus(dependencyName, new Set(seen)))
87
+ .filter(Boolean);
88
+
89
+ const status = dependencyStatuses.some((value) => value === 'stale' || value === 'affected')
90
+ ? 'affected'
91
+ : 'current';
92
+
93
+ cache.set(packageName, status);
94
+ return status;
95
+ }
96
+
97
+ for (const packageName of nodes.keys()) {
98
+ resolveStatus(packageName);
99
+ }
100
+
101
+ return cache;
102
+ }
103
+
104
+ export function readNodeStatus(statusMap, packageName) {
105
+ if (!statusMap) return null;
106
+ return statusMap.get(packageName) || null;
107
+ }
108
+
109
+ export function resolveDependencyClosure(initialRequires, { resolveNode }) {
110
+ const seen = new Set();
111
+ const queue = [...initialRequires];
112
+ const resolved = [];
113
+ const unresolved = [];
114
+
115
+ while (queue.length > 0) {
116
+ const packageName = queue.shift();
117
+ if (seen.has(packageName)) continue;
118
+ seen.add(packageName);
119
+
120
+ const node = resolveNode(packageName);
121
+ if (!node) {
122
+ unresolved.push(packageName);
123
+ continue;
124
+ }
125
+
126
+ resolved.push(node);
127
+
128
+ for (const requirement of node.requires || []) {
129
+ if (!seen.has(requirement)) queue.push(requirement);
130
+ }
131
+ }
132
+
133
+ resolved.sort((a, b) => a.packageName.localeCompare(b.packageName));
134
+ unresolved.sort((a, b) => a.localeCompare(b));
135
+
136
+ return { resolved, unresolved };
137
+ }
@@ -0,0 +1,187 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { NotFoundError, ValidationError } from '../../utils/errors.js';
4
+
5
+ function parseScalar(value) {
6
+ const trimmed = value.trim();
7
+ if (
8
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
9
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
10
+ ) {
11
+ return trimmed.slice(1, -1);
12
+ }
13
+ return trimmed;
14
+ }
15
+
16
+ function foldBlockScalar(lines, startIndex, baseIndent) {
17
+ const values = [];
18
+ let index = startIndex + 1;
19
+
20
+ while (index < lines.length) {
21
+ const rawLine = lines[index];
22
+ if (!rawLine.trim()) {
23
+ values.push('');
24
+ index += 1;
25
+ continue;
26
+ }
27
+
28
+ const indentMatch = rawLine.match(/^(\s*)/);
29
+ const indent = indentMatch ? indentMatch[1].length : 0;
30
+ if (indent <= baseIndent) break;
31
+
32
+ values.push(rawLine.slice(baseIndent + 2).trimEnd());
33
+ index += 1;
34
+ }
35
+
36
+ const folded = values
37
+ .join('\n')
38
+ .split('\n\n')
39
+ .map((chunk) => chunk.split('\n').join(' ').trim())
40
+ .filter((chunk, idx, arr) => chunk.length > 0 || idx < arr.length - 1)
41
+ .join('\n\n')
42
+ .trim();
43
+
44
+ return { value: folded, nextIndex: index };
45
+ }
46
+
47
+ function ensureContainer(target, key) {
48
+ if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
49
+ target[key] = {};
50
+ }
51
+ return target[key];
52
+ }
53
+
54
+ export function parseSkillFrontmatterFile(skillFilePath) {
55
+ if (!existsSync(skillFilePath)) {
56
+ throw new NotFoundError(`skill file not found: ${skillFilePath}`, { code: 'skill_not_found' });
57
+ }
58
+
59
+ const content = readFileSync(skillFilePath, 'utf-8');
60
+ if (!content.startsWith('---\n')) {
61
+ throw new ValidationError('SKILL.md missing frontmatter', { code: 'missing_frontmatter' });
62
+ }
63
+
64
+ const fmEnd = content.indexOf('\n---', 4);
65
+ if (fmEnd === -1) {
66
+ throw new ValidationError('SKILL.md has unclosed frontmatter', { code: 'unclosed_frontmatter' });
67
+ }
68
+
69
+ const lines = content.slice(4, fmEnd).split('\n');
70
+ const fields = {};
71
+ let activeArrayKey = null;
72
+ let activeArrayTarget = null;
73
+ let activeParentKey = null;
74
+
75
+ for (let index = 0; index < lines.length; index += 1) {
76
+ const rawLine = lines[index];
77
+ const line = rawLine.trimEnd();
78
+ if (!line.trim()) continue;
79
+
80
+ const listMatch = rawLine.match(/^(\s*)-\s+(.+)$/);
81
+ if (listMatch && activeArrayKey && activeArrayTarget) {
82
+ activeArrayTarget[activeArrayKey].push(parseScalar(listMatch[2]));
83
+ continue;
84
+ }
85
+
86
+ const nestedKeyMatch = rawLine.match(/^\s{2}([A-Za-z][\w-]*):\s*(.*)$/);
87
+ if (nestedKeyMatch && activeParentKey) {
88
+ const [, key, value] = nestedKeyMatch;
89
+ const parent = ensureContainer(fields, activeParentKey);
90
+ if (value === '') {
91
+ parent[key] = [];
92
+ activeArrayKey = key;
93
+ activeArrayTarget = parent;
94
+ continue;
95
+ }
96
+
97
+ parent[key] = parseScalar(value);
98
+ activeArrayKey = null;
99
+ activeArrayTarget = null;
100
+ continue;
101
+ }
102
+
103
+ const keyMatch = rawLine.match(/^([A-Za-z][\w-]*):\s*(.*)$/);
104
+ if (!keyMatch) continue;
105
+
106
+ const [, key, value] = keyMatch;
107
+ if (value === '>' || value === '|') {
108
+ const { value: blockValue, nextIndex } = foldBlockScalar(lines, index, 0);
109
+ fields[key] = blockValue;
110
+ activeParentKey = null;
111
+ activeArrayKey = null;
112
+ activeArrayTarget = null;
113
+ index = nextIndex - 1;
114
+ continue;
115
+ }
116
+
117
+ if (value === '') {
118
+ fields[key] = fields[key] && typeof fields[key] === 'object' && !Array.isArray(fields[key])
119
+ ? fields[key]
120
+ : [];
121
+ activeParentKey = key;
122
+ activeArrayKey = Array.isArray(fields[key]) ? key : null;
123
+ activeArrayTarget = Array.isArray(fields[key]) ? fields : null;
124
+ continue;
125
+ }
126
+
127
+ fields[key] = parseScalar(value);
128
+ activeParentKey = null;
129
+ activeArrayKey = null;
130
+ activeArrayTarget = null;
131
+ }
132
+
133
+ if (!fields.name) {
134
+ throw new ValidationError('SKILL.md frontmatter missing "name" field', { code: 'missing_name' });
135
+ }
136
+ if (!fields.description) {
137
+ throw new ValidationError('SKILL.md frontmatter missing "description" field', { code: 'missing_description' });
138
+ }
139
+
140
+ return {
141
+ name: fields.name,
142
+ description: fields.description,
143
+ sources: Array.isArray(fields.metadata?.sources)
144
+ ? fields.metadata.sources
145
+ : (Array.isArray(fields.sources) ? fields.sources : []),
146
+ requires: Array.isArray(fields.metadata?.requires)
147
+ ? fields.metadata.requires
148
+ : (Array.isArray(fields.requires) ? fields.requires : []),
149
+ status: typeof fields.metadata?.status === 'string' ? fields.metadata.status : null,
150
+ replacement: typeof fields.metadata?.replacement === 'string' ? fields.metadata.replacement : null,
151
+ message: typeof fields.metadata?.message === 'string' ? fields.metadata.message : null,
152
+ };
153
+ }
154
+
155
+ export function normalizeDisplayPath(repoRoot, absolutePath) {
156
+ return relative(repoRoot, absolutePath).split('\\').join('/');
157
+ }
158
+
159
+ export function normalizeRepoPath(repoRoot, absolutePath) {
160
+ return normalizeDisplayPath(repoRoot, absolutePath);
161
+ }
162
+
163
+ export function readPackageMetadata(packageDir) {
164
+ const packageJsonPath = join(packageDir, 'package.json');
165
+ if (!existsSync(packageJsonPath)) {
166
+ return {
167
+ packageName: null,
168
+ packageVersion: null,
169
+ dependencies: {},
170
+ devDependencies: {},
171
+ files: null,
172
+ repository: null,
173
+ publishConfigRegistry: null,
174
+ };
175
+ }
176
+
177
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
178
+ return {
179
+ packageName: pkg.name || null,
180
+ packageVersion: pkg.version || null,
181
+ dependencies: pkg.dependencies || {},
182
+ devDependencies: pkg.devDependencies || {},
183
+ files: Array.isArray(pkg.files) ? pkg.files : null,
184
+ repository: pkg.repository || null,
185
+ publishConfigRegistry: pkg.publishConfig?.registry || null,
186
+ };
187
+ }