agent-skillboard 0.1.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 (94) hide show
  1. package/CONTRIBUTING.md +83 -0
  2. package/LICENSE +21 -0
  3. package/README.md +645 -0
  4. package/bin/skillboard.mjs +4 -0
  5. package/docs/adapters.md +127 -0
  6. package/docs/capabilities.md +107 -0
  7. package/docs/install.md +346 -0
  8. package/docs/plans/20260625-080025-skillboard-mvp-review.md +189 -0
  9. package/docs/plans/README.md +20 -0
  10. package/docs/policy-model.md +251 -0
  11. package/docs/positioning.md +94 -0
  12. package/docs/profiles.md +166 -0
  13. package/docs/rollout-runbook.md +60 -0
  14. package/docs/user-flow.md +231 -0
  15. package/docs/versioning.md +201 -0
  16. package/examples/multi-source-skills/anthropic/docx/SKILL.md +8 -0
  17. package/examples/multi-source-skills/anthropic/skill-creator/SKILL.md +8 -0
  18. package/examples/multi-source-skills/matt/grill-me/SKILL.md +8 -0
  19. package/examples/multi-source-skills/matt/tdd/SKILL.md +8 -0
  20. package/examples/multi-source-skills/omo/review-work/SKILL.md +8 -0
  21. package/examples/multi-source-skills/omo/ulw-plan/SKILL.md +8 -0
  22. package/examples/multi-source-skills/private/tdd-work-continuity/SKILL.md +8 -0
  23. package/examples/multi-source-skills/private/workflow-router/SKILL.md +8 -0
  24. package/examples/multi-source-skills/wshobson/python-testing/SKILL.md +8 -0
  25. package/examples/multi-source-skills/wshobson/security-review/SKILL.md +8 -0
  26. package/examples/multi-source.config.yaml +271 -0
  27. package/examples/skillboard.config.yaml +194 -0
  28. package/examples/skills/grill-me/SKILL.md +9 -0
  29. package/examples/skills/grill-with-docs/SKILL.md +9 -0
  30. package/examples/skills/requirement-intake/SKILL.md +9 -0
  31. package/examples/skills/tdd/SKILL.md +8 -0
  32. package/package.json +58 -0
  33. package/profiles/anthropics-skills.yaml +17 -0
  34. package/profiles/mattpocock-skills.yaml +26 -0
  35. package/profiles/oh-my-openagent.yaml +32 -0
  36. package/profiles/voltagent-awesome-agent-skills.yaml +18 -0
  37. package/profiles/wshobson-agents.yaml +19 -0
  38. package/src/advisor/action-core.mjs +74 -0
  39. package/src/advisor/actions.mjs +256 -0
  40. package/src/advisor/application-commands.mjs +59 -0
  41. package/src/advisor/apply-action.mjs +183 -0
  42. package/src/advisor/schema.mjs +112 -0
  43. package/src/advisor/setup-actions.mjs +42 -0
  44. package/src/advisor/skills.mjs +191 -0
  45. package/src/advisor/sort.mjs +15 -0
  46. package/src/advisor/sources.mjs +160 -0
  47. package/src/advisor/trust-policy.mjs +65 -0
  48. package/src/advisor/workflow.mjs +41 -0
  49. package/src/advisor.mjs +134 -0
  50. package/src/agent-inventory-platforms.mjs +100 -0
  51. package/src/agent-inventory.mjs +804 -0
  52. package/src/brief-cli.mjs +40 -0
  53. package/src/brief-renderer.mjs +362 -0
  54. package/src/change-plan.mjs +169 -0
  55. package/src/cli.mjs +1171 -0
  56. package/src/config-helpers.mjs +72 -0
  57. package/src/control/can-use-guard.mjs +91 -0
  58. package/src/control/config-write.mjs +163 -0
  59. package/src/control/skill-crud.mjs +394 -0
  60. package/src/control/source-trust.mjs +161 -0
  61. package/src/control/workflow-crud.mjs +104 -0
  62. package/src/control.mjs +197 -0
  63. package/src/doctor.mjs +299 -0
  64. package/src/domain/constants.mjs +47 -0
  65. package/src/domain/indexes.mjs +29 -0
  66. package/src/domain/rules/capabilities.mjs +33 -0
  67. package/src/domain/rules/harnesses.mjs +16 -0
  68. package/src/domain/rules/install-units.mjs +95 -0
  69. package/src/domain/rules/skills.mjs +105 -0
  70. package/src/domain/rules/workflows.mjs +105 -0
  71. package/src/domain/skill-state-matrix.mjs +79 -0
  72. package/src/domain/source-classes.mjs +99 -0
  73. package/src/hook-plan.mjs +152 -0
  74. package/src/impact.mjs +41 -0
  75. package/src/index.mjs +82 -0
  76. package/src/init.mjs +136 -0
  77. package/src/install-output-detector.mjs +337 -0
  78. package/src/install-units.mjs +62 -0
  79. package/src/inventory-refresh.mjs +45 -0
  80. package/src/lifecycle-cli.mjs +166 -0
  81. package/src/lifecycle-content.mjs +87 -0
  82. package/src/policy.mjs +42 -0
  83. package/src/reconcile.mjs +111 -0
  84. package/src/report.mjs +151 -0
  85. package/src/review.mjs +88 -0
  86. package/src/rollout.mjs +453 -0
  87. package/src/skill-paths.mjs +17 -0
  88. package/src/source-cache.mjs +178 -0
  89. package/src/source-profile-loader.mjs +160 -0
  90. package/src/source-profiles.mjs +299 -0
  91. package/src/source-verification.mjs +252 -0
  92. package/src/uninstall.mjs +258 -0
  93. package/src/workspace.mjs +219 -0
  94. package/tsconfig.lsp.json +15 -0
@@ -0,0 +1,160 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { isAbsolute, join, resolve } from "node:path";
4
+ import YAML from "yaml";
5
+ import { readBoolean, readOptionalNumber, readOptionalString, readString, readStringList, requireRecord } from "./config-helpers.mjs";
6
+ import {
7
+ EXPOSURE_VALUES,
8
+ INSTALL_UNIT_KIND_VALUES,
9
+ INVOCATION_VALUES,
10
+ PERMISSION_RISK_VALUES,
11
+ STATUS_VALUES,
12
+ TRUST_LEVEL_VALUES
13
+ } from "./domain/constants.mjs";
14
+
15
+ const PROFILE_DIR = fileURLToPath(new URL("../profiles", import.meta.url));
16
+
17
+ export async function loadSourceProfile(profileRef, options = {}) {
18
+ const path = await resolveProfilePath(profileRef, options.profileDirs ?? []);
19
+ const raw = requireRecord(YAML.parse(await readFile(path, "utf8")), `source profile ${profileRef}`);
20
+ return parseSourceProfile(raw, path);
21
+ }
22
+
23
+ async function resolveProfilePath(profileRef, profileDirs) {
24
+ if (looksLikePath(profileRef)) {
25
+ return resolve(profileRef);
26
+ }
27
+ const dirs = [...profileDirs, PROFILE_DIR];
28
+ for (const dir of dirs) {
29
+ const entries = await readdir(dir).catch(() => []);
30
+ for (const entry of entries) {
31
+ if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) {
32
+ continue;
33
+ }
34
+ const path = join(dir, entry);
35
+ const profile = requireRecord(YAML.parse(await readFile(path, "utf8")), `source profile ${path}`);
36
+ if (profile.id === profileRef) {
37
+ return path;
38
+ }
39
+ }
40
+ }
41
+ throw new Error(`Unknown source profile: ${profileRef}`);
42
+ }
43
+
44
+ function looksLikePath(value) {
45
+ return isAbsolute(value) || value.startsWith(".") || value.includes("/") || value.includes("\\");
46
+ }
47
+
48
+ function parseSourceProfile(raw, path) {
49
+ const profile = {
50
+ id: readString(raw, "id", ""),
51
+ source: readString(raw, "source", ""),
52
+ kind: readString(raw, "kind", "skill"),
53
+ sourceClass: readOptionalString(raw, "source_class"),
54
+ priority: readOptionalNumber(raw, "priority"),
55
+ trustLevel: readString(raw, "trust_level", "unreviewed"),
56
+ sourceDigest: readOptionalString(raw, "source_digest"),
57
+ signature: readOptionalString(raw, "signature"),
58
+ publicKey: readOptionalString(raw, "public_key"),
59
+ verifiedAt: readOptionalString(raw, "verified_at"),
60
+ namespace: readString(raw, "namespace", ""),
61
+ targetPathPrefix: readString(raw, "target_path_prefix", readString(raw, "namespace", "")),
62
+ idStrategy: readString(raw, "id_strategy", "path"),
63
+ scope: readString(raw, "scope", "local"),
64
+ manifestPath: readString(raw, "manifest_path", ""),
65
+ cachePath: readString(raw, "cache_path", ""),
66
+ defaultStatus: readString(raw, "default_status", "vendor"),
67
+ defaultInvocation: readString(raw, "default_invocation", "manual-only"),
68
+ defaultExposure: readString(raw, "default_exposure", "exported"),
69
+ defaultCategory: readString(raw, "default_category", "uncategorized"),
70
+ categoryPathSegment: readOptionalNumber(raw, "category_path_segment"),
71
+ pathRules: parsePathRules(raw.path_rules, `source profile ${path}.path_rules`),
72
+ providedComponents: readStringList(raw, "provided_components"),
73
+ skillPaths: readStringList(raw, "skill_paths"),
74
+ components: parseProfileComponents(raw.components, `source profile ${path}.components`),
75
+ modifiedConfigFiles: readStringList(raw, "modified_config_files"),
76
+ autoUpdate: readBoolean(raw, "auto_update", false),
77
+ enabled: readBoolean(raw, "enabled", true),
78
+ workflowDependencies: readStringList(raw, "workflow_dependencies"),
79
+ permissionRisk: readString(raw, "permission_risk", "unknown"),
80
+ rollback: readString(raw, "rollback", "unknown")
81
+ };
82
+ if (profile.id.length === 0) {
83
+ throw new Error(`Source profile ${path} is missing id`);
84
+ }
85
+ validateProfileValues(profile);
86
+ return profile;
87
+ }
88
+
89
+ function parseProfileComponents(value, label) {
90
+ const raw = requireRecord(value ?? {}, label);
91
+ return {
92
+ commands: readStringList(raw, "commands"),
93
+ hooks: readStringList(raw, "hooks"),
94
+ mcpServers: readStringList(raw, "mcp_servers")
95
+ };
96
+ }
97
+
98
+ function parsePathRules(value, label) {
99
+ if (value === undefined) {
100
+ return [];
101
+ }
102
+ if (!Array.isArray(value)) {
103
+ throw new Error(`${label} must be a list of mappings`);
104
+ }
105
+ return value.map((entry, index) => {
106
+ const raw = requireRecord(entry, `${label}[${index}]`);
107
+ const rule = {
108
+ pattern: readString(raw, "pattern", ""),
109
+ status: readOptionalString(raw, "status"),
110
+ invocation: readOptionalString(raw, "invocation"),
111
+ exposure: readOptionalString(raw, "exposure"),
112
+ category: readOptionalString(raw, "category")
113
+ };
114
+ if (rule.pattern.trim() === "") {
115
+ throw new Error(`${label}[${index}].pattern must be a non-empty string`);
116
+ }
117
+ return rule;
118
+ });
119
+ }
120
+
121
+ function validateProfileValues(profile) {
122
+ if (!INSTALL_UNIT_KIND_VALUES.has(profile.kind)) {
123
+ throw new Error(`Unsupported source profile kind for ${profile.id}: ${profile.kind}`);
124
+ }
125
+ if (!STATUS_VALUES.has(profile.defaultStatus)) {
126
+ throw new Error(`Unsupported source profile default_status for ${profile.id}: ${profile.defaultStatus}`);
127
+ }
128
+ if (!INVOCATION_VALUES.has(profile.defaultInvocation)) {
129
+ throw new Error(`Unsupported source profile default_invocation for ${profile.id}: ${profile.defaultInvocation}`);
130
+ }
131
+ if (!EXPOSURE_VALUES.has(profile.defaultExposure)) {
132
+ throw new Error(`Unsupported source profile default_exposure for ${profile.id}: ${profile.defaultExposure}`);
133
+ }
134
+ if (!PERMISSION_RISK_VALUES.has(profile.permissionRisk)) {
135
+ throw new Error(`Unsupported source profile permission_risk for ${profile.id}: ${profile.permissionRisk}`);
136
+ }
137
+ if (!TRUST_LEVEL_VALUES.has(profile.trustLevel)) {
138
+ throw new Error(`Unsupported source profile trust_level for ${profile.id}: ${profile.trustLevel}`);
139
+ }
140
+ if (
141
+ profile.categoryPathSegment !== undefined
142
+ && (!Number.isInteger(profile.categoryPathSegment) || profile.categoryPathSegment < 0)
143
+ ) {
144
+ throw new Error(`Source profile ${profile.id} category_path_segment must be a non-negative integer`);
145
+ }
146
+ for (const rule of profile.pathRules) {
147
+ if (rule.status !== undefined && !STATUS_VALUES.has(rule.status)) {
148
+ throw new Error(`Unsupported source profile path_rules status for ${profile.id}: ${rule.status}`);
149
+ }
150
+ if (rule.invocation !== undefined && !INVOCATION_VALUES.has(rule.invocation)) {
151
+ throw new Error(`Unsupported source profile path_rules invocation for ${profile.id}: ${rule.invocation}`);
152
+ }
153
+ if (rule.exposure !== undefined && !EXPOSURE_VALUES.has(rule.exposure)) {
154
+ throw new Error(`Unsupported source profile path_rules exposure for ${profile.id}: ${rule.exposure}`);
155
+ }
156
+ }
157
+ if (profile.defaultInvocation === "global-auto" && profile.defaultExposure !== "global-meta") {
158
+ throw new Error(`Source profile ${profile.id} cannot grant global-auto to non-global-meta skills`);
159
+ }
160
+ }
@@ -0,0 +1,299 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { basename, dirname, join, relative, resolve } from "node:path";
3
+ import YAML from "yaml";
4
+ import { readString, requireRecord } from "./config-helpers.mjs";
5
+ import { textChangePlan } from "./change-plan.mjs";
6
+ import { loadSourceProfile } from "./source-profile-loader.mjs";
7
+
8
+ export { loadSourceProfile };
9
+
10
+ export async function importSource(options) {
11
+ const sourceRoot = resolve(options.sourceRoot);
12
+ const profile = options.profile;
13
+ const files = await findSkillFiles(sourceRoot);
14
+ const matchedFiles = files
15
+ .map((file) => ({
16
+ file,
17
+ relativeFile: relative(sourceRoot, file).replaceAll("\\", "/")
18
+ }))
19
+ .filter((entry) => matchesAnyProfilePattern(entry.relativeFile, profile.skillPaths))
20
+ .sort((left, right) => left.relativeFile.localeCompare(right.relativeFile));
21
+ const skills = [];
22
+ const warnings = [];
23
+
24
+ for (const entry of matchedFiles) {
25
+ const frontmatter = parseSkillFrontmatter(await readFile(entry.file, "utf8"));
26
+ const pathRule = matchingPathRule(entry.relativeFile, profile);
27
+ const slug = skillSlug(entry.relativeFile, frontmatter, profile);
28
+ const id = skillId(slug, frontmatter, profile);
29
+ const path = skillTargetPath(slug, profile);
30
+ skills.push({
31
+ id,
32
+ path,
33
+ status: pathRule?.status ?? profile.defaultStatus,
34
+ invocation: safeDefaultInvocation(pathRule?.invocation ?? profile.defaultInvocation),
35
+ exposure: pathRule?.exposure ?? profile.defaultExposure,
36
+ category: pathRule?.category ?? categoryFromPath(entry.relativeFile, profile) ?? profile.defaultCategory,
37
+ ownerInstallUnit: profile.id,
38
+ description: frontmatter.description
39
+ });
40
+ }
41
+
42
+ if (skills.length === 0) {
43
+ warnings.push(`No SKILL.md files matched profile ${profile.id} under ${sourceRoot}`);
44
+ }
45
+
46
+ return {
47
+ profile,
48
+ sourceRoot,
49
+ skills,
50
+ installUnit: {
51
+ id: profile.id,
52
+ kind: profile.kind,
53
+ sourceClass: profile.sourceClass,
54
+ priority: profile.priority,
55
+ trustLevel: profile.trustLevel,
56
+ sourceDigest: profile.sourceDigest,
57
+ signature: profile.signature,
58
+ publicKey: profile.publicKey,
59
+ verifiedAt: profile.verifiedAt,
60
+ source: profile.source || sourceRoot,
61
+ scope: profile.scope,
62
+ manifestPath: profile.manifestPath,
63
+ cachePath: profile.cachePath,
64
+ providedComponents: profile.providedComponents,
65
+ components: {
66
+ skills: skills.map((skill) => skill.id),
67
+ commands: profile.components.commands,
68
+ hooks: profile.components.hooks,
69
+ mcpServers: profile.components.mcpServers
70
+ },
71
+ modifiedConfigFiles: profile.modifiedConfigFiles,
72
+ autoUpdate: profile.autoUpdate,
73
+ enabled: profile.enabled,
74
+ workflowDependencies: profile.workflowDependencies,
75
+ permissionRisk: profile.permissionRisk,
76
+ rollback: profile.rollback
77
+ },
78
+ warnings
79
+ };
80
+ }
81
+
82
+ export function renderImportFragment(imported) {
83
+ return YAML.stringify(importFragment(imported));
84
+ }
85
+
86
+ export function mergeImportFragment(configText, imported, options = {}) {
87
+ const document = YAML.parseDocument(configText);
88
+ if (document.errors.length > 0) {
89
+ throw new Error(`Invalid YAML config: ${document.errors.map((error) => error.message).join("; ")}`);
90
+ }
91
+ requireYamlMap(document.contents, "config root");
92
+ const fragment = importFragment(imported);
93
+ const replace = options.replace === true;
94
+ const skillIds = Object.keys(fragment.skills);
95
+ const unitIds = Object.keys(fragment.install_units);
96
+ const existingSkills = ensureMap(document, "skills");
97
+ const existingUnits = ensureMap(document, "install_units");
98
+ const duplicateSkills = skillIds.filter((id) => existingSkills.get(id, true) !== undefined);
99
+ const duplicateUnits = unitIds.filter((id) => existingUnits.get(id, true) !== undefined);
100
+
101
+ if (!replace && (duplicateSkills.length > 0 || duplicateUnits.length > 0)) {
102
+ throw new Error(duplicateMessage(duplicateSkills, duplicateUnits));
103
+ }
104
+
105
+ for (const skillId of skillIds) {
106
+ existingSkills.set(skillId, document.createNode(fragment.skills[skillId]));
107
+ }
108
+ for (const unitId of unitIds) {
109
+ existingUnits.set(unitId, document.createNode(fragment.install_units[unitId]));
110
+ }
111
+ const text = preserveLineEndings(String(document), configText);
112
+ const plan = textChangePlan(configText, text);
113
+ return {
114
+ text,
115
+ changed: plan.changed,
116
+ plan,
117
+ addedSkills: skillIds,
118
+ addedInstallUnits: unitIds,
119
+ replacedSkills: duplicateSkills,
120
+ replacedInstallUnits: duplicateUnits
121
+ };
122
+ }
123
+
124
+ function ensureMap(document, key) {
125
+ const existing = document.get(key, true);
126
+ if (existing === undefined) {
127
+ const next = document.createNode({});
128
+ document.set(key, next);
129
+ return next;
130
+ }
131
+ return requireYamlMap(existing, key);
132
+ }
133
+
134
+ function requireYamlMap(value, label) {
135
+ if (!YAML.isMap(value)) {
136
+ throw new Error(`${label} must be a mapping`);
137
+ }
138
+ return value;
139
+ }
140
+
141
+ function preserveLineEndings(text, reference) {
142
+ return reference.includes("\r\n") ? text.replace(/\n/g, "\r\n") : text;
143
+ }
144
+
145
+ function importFragment(imported) {
146
+ const skills = {};
147
+ for (const skill of imported.skills) {
148
+ skills[skill.id] = {
149
+ path: skill.path,
150
+ status: skill.status,
151
+ invocation: skill.invocation,
152
+ exposure: skill.exposure,
153
+ category: skill.category,
154
+ owner_install_unit: skill.ownerInstallUnit
155
+ };
156
+ }
157
+ const unit = imported.installUnit;
158
+ return stripUndefined({
159
+ skills,
160
+ install_units: {
161
+ [unit.id]: {
162
+ kind: unit.kind,
163
+ source_class: unit.sourceClass,
164
+ priority: unit.priority,
165
+ trust_level: unit.trustLevel,
166
+ source_digest: unit.sourceDigest,
167
+ signature: unit.signature,
168
+ public_key: unit.publicKey,
169
+ verified_at: unit.verifiedAt,
170
+ source: unit.source,
171
+ scope: unit.scope,
172
+ manifest_path: unit.manifestPath,
173
+ cache_path: unit.cachePath,
174
+ provided_components: unit.providedComponents,
175
+ components: {
176
+ skills: unit.components.skills,
177
+ commands: unit.components.commands,
178
+ hooks: unit.components.hooks,
179
+ mcp_servers: unit.components.mcpServers
180
+ },
181
+ modified_config_files: unit.modifiedConfigFiles,
182
+ auto_update: unit.autoUpdate,
183
+ enabled: unit.enabled,
184
+ workflow_dependencies: unit.workflowDependencies,
185
+ permission_risk: unit.permissionRisk,
186
+ rollback: unit.rollback
187
+ }
188
+ }
189
+ });
190
+ }
191
+
192
+ function duplicateMessage(skillIds, unitIds) {
193
+ const parts = [];
194
+ if (skillIds.length > 0) {
195
+ parts.push(`skills already exist: ${skillIds.join(", ")}`);
196
+ }
197
+ if (unitIds.length > 0) {
198
+ parts.push(`install units already exist: ${unitIds.join(", ")}`);
199
+ }
200
+ return `${parts.join("; ")}. Re-run with --replace to overwrite.`;
201
+ }
202
+
203
+ async function findSkillFiles(root) {
204
+ const files = [];
205
+ const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
206
+ for (const entry of entries) {
207
+ const path = join(root, entry.name);
208
+ if (entry.isDirectory()) {
209
+ files.push(...(await findSkillFiles(path)));
210
+ } else if (entry.isFile() && entry.name === "SKILL.md") {
211
+ files.push(path);
212
+ }
213
+ }
214
+ return files;
215
+ }
216
+
217
+ function parseSkillFrontmatter(text) {
218
+ const match = /^---[ \t]*\r?\n(?<body>[\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
219
+ if (match?.groups === undefined) {
220
+ throw new Error("SKILL.md is missing YAML frontmatter");
221
+ }
222
+ const raw = requireRecord(YAML.parse(match.groups.body), "SKILL.md frontmatter");
223
+ return {
224
+ name: readString(raw, "name", ""),
225
+ description: readString(raw, "description", "")
226
+ };
227
+ }
228
+
229
+ function matchesAnyProfilePattern(file, patterns) {
230
+ const activePatterns = patterns.length === 0 ? ["**/SKILL.md"] : patterns;
231
+ return activePatterns.some((pattern) => patternToRegExp(pattern).test(file));
232
+ }
233
+
234
+ function matchingPathRule(file, profile) {
235
+ return (profile.pathRules ?? []).find((rule) => patternToRegExp(rule.pattern).test(file));
236
+ }
237
+
238
+ function patternToRegExp(pattern) {
239
+ const escaped = pattern
240
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
241
+ .replaceAll("**", "\0")
242
+ .replaceAll("*", "[^/]*")
243
+ .replaceAll("\0", ".*");
244
+ return new RegExp(`^${escaped}$`);
245
+ }
246
+
247
+ function skillSlug(relativeFile, frontmatter, profile) {
248
+ if (profile.idStrategy === "frontmatter-name" && frontmatter.name.length > 0) {
249
+ return normalizeSlug(frontmatter.name.split(".").at(-1));
250
+ }
251
+ return normalizeSlug(basename(dirname(relativeFile)));
252
+ }
253
+
254
+ function skillId(slug, frontmatter, profile) {
255
+ if (profile.idStrategy === "frontmatter-name" && frontmatter.name.includes(".")) {
256
+ return frontmatter.name;
257
+ }
258
+ return profile.namespace.length === 0 ? slug : `${profile.namespace}.${slug}`;
259
+ }
260
+
261
+ function skillTargetPath(slug, profile) {
262
+ return profile.targetPathPrefix.length === 0 ? slug : `${profile.targetPathPrefix}/${slug}`;
263
+ }
264
+
265
+ function categoryFromPath(relativeFile, profile) {
266
+ if (profile.categoryPathSegment === undefined) {
267
+ return undefined;
268
+ }
269
+ const segment = relativeFile.split("/")[profile.categoryPathSegment];
270
+ if (segment === undefined || segment.endsWith(".md")) {
271
+ return undefined;
272
+ }
273
+ const category = normalizeSlug(segment);
274
+ return category.length === 0 ? undefined : category;
275
+ }
276
+
277
+ function normalizeSlug(value) {
278
+ return value
279
+ .trim()
280
+ .toLowerCase()
281
+ .replace(/[^a-z0-9._-]+/g, "-")
282
+ .replace(/^-+|-+$/g, "");
283
+ }
284
+
285
+ function safeDefaultInvocation(invocation) {
286
+ return invocation === "global-auto" ? "blocked" : invocation;
287
+ }
288
+
289
+ function stripUndefined(value) {
290
+ if (Array.isArray(value)) {
291
+ return value.map((item) => stripUndefined(item));
292
+ }
293
+ if (value !== null && typeof value === "object") {
294
+ return Object.fromEntries(Object.entries(value)
295
+ .filter((entry) => entry[1] !== undefined)
296
+ .map(([key, entryValue]) => [key, stripUndefined(entryValue)]));
297
+ }
298
+ return value;
299
+ }