agent-skillboard 0.2.17 → 0.2.18

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.
@@ -1,26 +1,26 @@
1
- import { readdir, readFile } from "node:fs/promises";
2
- import { basename, dirname, join, relative, resolve } from "node:path";
3
- import YAML from "yaml";
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { basename, dirname, join, relative, resolve } from "node:path";
3
+ import YAML from "yaml";
4
4
  import { readString, requireRecord } from "./config-helpers.mjs";
5
5
  import { textChangePlan } from "./change-plan.mjs";
6
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,
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
17
  relativeFile: relative(sourceRoot, file).replace(/\\/g, "/")
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
-
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
24
  for (const entry of matchedFiles) {
25
25
  const frontmatter = parseSkillFrontmatter(await readFile(entry.file, "utf8"));
26
26
  const pathRule = matchingPathRule(entry.relativeFile, profile);
@@ -37,17 +37,17 @@ export async function importSource(options) {
37
37
  ownerInstallUnit: profile.id,
38
38
  description: frontmatter.description
39
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: {
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
51
  id: profile.id,
52
52
  kind: profile.kind,
53
53
  sourceClass: profile.sourceClass,
@@ -58,31 +58,31 @@ export async function importSource(options) {
58
58
  publicKey: profile.publicKey,
59
59
  verifiedAt: profile.verifiedAt,
60
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
-
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
86
  export function mergeImportFragment(configText, imported, options = {}) {
87
87
  const document = YAML.parseDocument(configText);
88
88
  if (document.errors.length > 0) {
@@ -117,7 +117,7 @@ export function mergeImportFragment(configText, imported, options = {}) {
117
117
  addedSkills: skillIds,
118
118
  addedInstallUnits: unitIds,
119
119
  replacedSkills: duplicateSkills,
120
- replacedInstallUnits: duplicateUnits
120
+ replacedInstallUnits: duplicateUnits
121
121
  };
122
122
  }
123
123
 
@@ -141,20 +141,20 @@ function requireYamlMap(value, label) {
141
141
  function preserveLineEndings(text, reference) {
142
142
  return reference.includes("\r\n") ? text.replace(/\n/g, "\r\n") : text;
143
143
  }
144
-
144
+
145
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;
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
158
  return stripUndefined({
159
159
  skills,
160
160
  install_units: {
@@ -168,64 +168,64 @@ function importFragment(imported) {
168
168
  public_key: unit.publicKey,
169
169
  verified_at: unit.verifiedAt,
170
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,
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
186
  rollback: unit.rollback
187
187
  }
188
188
  }
189
189
  });
190
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
-
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
229
  function matchesAnyProfilePattern(file, patterns) {
230
230
  const activePatterns = patterns.length === 0 ? ["**/SKILL.md"] : patterns;
231
231
  return activePatterns.some((pattern) => patternToRegExp(pattern).test(file));
@@ -234,16 +234,16 @@ function matchesAnyProfilePattern(file, patterns) {
234
234
  function matchingPathRule(file, profile) {
235
235
  return (profile.pathRules ?? []).find((rule) => patternToRegExp(rule.pattern).test(file));
236
236
  }
237
-
238
- function patternToRegExp(pattern) {
239
- const escaped = pattern
240
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
237
+
238
+ function patternToRegExp(pattern) {
239
+ const escaped = pattern
240
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
241
241
  .split("**").join("\0")
242
242
  .split("*").join("[^/]*")
243
243
  .split("\0").join(".*");
244
- return new RegExp(`^${escaped}$`);
245
- }
246
-
244
+ return new RegExp(`^${escaped}$`);
245
+ }
246
+
247
247
  function skillSlug(relativeFile, frontmatter, profile) {
248
248
  if (profile.idStrategy === "frontmatter-name" && frontmatter.name.length > 0) {
249
249
  const segments = frontmatter.name.split(".");
@@ -251,14 +251,14 @@ function skillSlug(relativeFile, frontmatter, profile) {
251
251
  }
252
252
  return normalizeSlug(basename(dirname(relativeFile)));
253
253
  }
254
-
255
- function skillId(slug, frontmatter, profile) {
256
- if (profile.idStrategy === "frontmatter-name" && frontmatter.name.includes(".")) {
257
- return frontmatter.name;
258
- }
259
- return profile.namespace.length === 0 ? slug : `${profile.namespace}.${slug}`;
260
- }
261
-
254
+
255
+ function skillId(slug, frontmatter, profile) {
256
+ if (profile.idStrategy === "frontmatter-name" && frontmatter.name.includes(".")) {
257
+ return frontmatter.name;
258
+ }
259
+ return profile.namespace.length === 0 ? slug : `${profile.namespace}.${slug}`;
260
+ }
261
+
262
262
  function skillTargetPath(slug, profile) {
263
263
  return profile.targetPathPrefix.length === 0 ? slug : `${profile.targetPathPrefix}/${slug}`;
264
264
  }
@@ -276,13 +276,13 @@ function categoryFromPath(relativeFile, profile) {
276
276
  }
277
277
 
278
278
  function normalizeSlug(value) {
279
- return value
280
- .trim()
281
- .toLowerCase()
282
- .replace(/[^a-z0-9._-]+/g, "-")
283
- .replace(/^-+|-+$/g, "");
284
- }
285
-
279
+ return value
280
+ .trim()
281
+ .toLowerCase()
282
+ .replace(/[^a-z0-9._-]+/g, "-")
283
+ .replace(/^-+|-+$/g, "");
284
+ }
285
+
286
286
  function safeDefaultInvocation(invocation) {
287
287
  return invocation === "global-auto" ? "blocked" : invocation;
288
288
  }
package/src/workspace.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readdir, readFile, realpath, stat } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
- import YAML from "yaml";
4
+ import YAML from "yaml";
5
5
  import {
6
6
  readBoolean,
7
7
  readOptionalRecord,
@@ -21,8 +21,8 @@ import { parseInstallUnits } from "./install-units.mjs";
21
21
  import { normalizeSkillPath } from "./skill-paths.mjs";
22
22
 
23
23
  export async function loadWorkspace(options) {
24
- const configText = await readFile(options.configPath, "utf8");
25
- const parsed = YAML.parse(configText);
24
+ const configText = await readFile(options.configPath, "utf8");
25
+ const parsed = YAML.parse(configText);
26
26
  const config = requireRecord(parsed, "config root");
27
27
  const version = parseVersion(config.version);
28
28
  const skills = parseSkills(config.skills);
@@ -123,7 +123,7 @@ function resolveStoredPath(value, options) {
123
123
  }
124
124
  return resolve(dirname(options.configPath ?? "."), value);
125
125
  }
126
-
126
+
127
127
  async function findSkillFiles(root, seen = new Set()) {
128
128
  const files = [];
129
129
  const resolvedRoot = await realpath(root).catch(() => root);
@@ -148,8 +148,8 @@ async function findSkillFiles(root, seen = new Set()) {
148
148
  }
149
149
  }
150
150
  return files;
151
- }
152
-
151
+ }
152
+
153
153
  function parseSkillFrontmatter(text) {
154
154
  const match = /^---[ \t]*\r?\n(?<body>[\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
155
155
  if (match?.groups === undefined) {
@@ -170,49 +170,49 @@ function parseSkillFrontmatter(text) {
170
170
  description: readString(raw, "description", "")
171
171
  };
172
172
  }
173
-
174
- function parseVersion(value) {
175
- if (value === undefined) {
176
- return 1;
177
- }
178
- if (value !== 1) {
179
- throw new Error(`Unsupported config version: ${value}`);
180
- }
181
- return value;
182
- }
183
-
184
- function parseDefaults(value) {
185
- const raw = requireRecord(value ?? {}, "defaults");
186
- return {
187
- invocationPolicy: readString(raw, "invocation_policy", "deny-by-default"),
188
- allowModelInvocation: readBoolean(raw, "allow_model_invocation", false),
189
- requireExplicitWorkflow: readBoolean(raw, "require_explicit_workflow", true)
190
- };
191
- }
192
-
193
- function parseSkills(value) {
194
- const raw = requireRecord(value ?? {}, "skills");
195
- return Object.entries(raw).map(([id, entry]) => {
196
- const skill = requireRecord(entry, `skills.${id}`);
197
- const status = readString(skill, "status", "vendor");
198
- const invocation = readString(skill, "invocation", "manual-only");
199
- const exposure = readString(skill, "exposure", "exported");
200
- if (!STATUS_VALUES.has(status)) {
201
- throw new Error(`Unsupported status for ${id}: ${status}`);
202
- }
203
- if (!INVOCATION_VALUES.has(invocation)) {
204
- throw new Error(`Unsupported invocation for ${id}: ${invocation}`);
205
- }
206
- if (!EXPOSURE_VALUES.has(exposure)) {
207
- throw new Error(`Unsupported exposure for ${id}: ${exposure}`);
208
- }
173
+
174
+ function parseVersion(value) {
175
+ if (value === undefined) {
176
+ return 1;
177
+ }
178
+ if (value !== 1) {
179
+ throw new Error(`Unsupported config version: ${value}`);
180
+ }
181
+ return value;
182
+ }
183
+
184
+ function parseDefaults(value) {
185
+ const raw = requireRecord(value ?? {}, "defaults");
186
+ return {
187
+ invocationPolicy: readString(raw, "invocation_policy", "deny-by-default"),
188
+ allowModelInvocation: readBoolean(raw, "allow_model_invocation", false),
189
+ requireExplicitWorkflow: readBoolean(raw, "require_explicit_workflow", true)
190
+ };
191
+ }
192
+
193
+ function parseSkills(value) {
194
+ const raw = requireRecord(value ?? {}, "skills");
195
+ return Object.entries(raw).map(([id, entry]) => {
196
+ const skill = requireRecord(entry, `skills.${id}`);
197
+ const status = readString(skill, "status", "vendor");
198
+ const invocation = readString(skill, "invocation", "manual-only");
199
+ const exposure = readString(skill, "exposure", "exported");
200
+ if (!STATUS_VALUES.has(status)) {
201
+ throw new Error(`Unsupported status for ${id}: ${status}`);
202
+ }
203
+ if (!INVOCATION_VALUES.has(invocation)) {
204
+ throw new Error(`Unsupported invocation for ${id}: ${invocation}`);
205
+ }
206
+ if (!EXPOSURE_VALUES.has(exposure)) {
207
+ throw new Error(`Unsupported exposure for ${id}: ${exposure}`);
208
+ }
209
209
  return {
210
210
  id,
211
211
  path: normalizeSkillPath(readString(skill, "path", id), `skills.${id}.path`),
212
- status,
213
- invocation,
214
- exposure,
215
- category: readString(skill, "category", "uncategorized"),
212
+ status,
213
+ invocation,
214
+ exposure,
215
+ category: readString(skill, "category", "uncategorized"),
216
216
  canonicalFor: readStringList(skill, "canonical_for"),
217
217
  conflictsWith: readStringList(skill, "conflicts_with"),
218
218
  replacedBy: readOptionalString(skill, "replaced_by"),
@@ -266,39 +266,39 @@ function parseCapabilities(value) {
266
266
  };
267
267
  });
268
268
  }
269
-
270
- function parseHarnesses(value) {
271
- const raw = requireRecord(value ?? {}, "harnesses");
272
- return Object.entries(raw).map(([name, entry]) => {
273
- const harness = requireRecord(entry, `harnesses.${name}`);
274
- const status = readString(harness, "status", "available");
275
- if (!HARNESS_STATUS_VALUES.has(status)) {
276
- throw new Error(`Unsupported harness status for ${name}: ${status}`);
277
- }
278
- return {
279
- name,
280
- status,
281
- workflows: readStringList(harness, "workflows"),
282
- commands: readStringList(harness, "commands")
283
- };
284
- });
285
- }
286
-
287
- function parseWorkflows(value) {
288
- const raw = requireRecord(value ?? {}, "workflows");
289
- return Object.entries(raw).map(([name, entry]) => {
290
- const workflow = requireRecord(entry, `workflows.${name}`);
291
- return {
292
- name,
293
- harness: readString(workflow, "harness", "unspecified"),
294
- activeSkills: readStringList(workflow, "active_skills"),
295
- blockedSkills: readStringList(workflow, "blocked_skills"),
296
- requiredOutputs: readStringList(workflow, "required_outputs"),
297
- requiredCapabilities: parseRequiredCapabilities(workflow.required_capabilities, `workflows.${name}.required_capabilities`)
298
- };
299
- });
300
- }
301
-
269
+
270
+ function parseHarnesses(value) {
271
+ const raw = requireRecord(value ?? {}, "harnesses");
272
+ return Object.entries(raw).map(([name, entry]) => {
273
+ const harness = requireRecord(entry, `harnesses.${name}`);
274
+ const status = readString(harness, "status", "available");
275
+ if (!HARNESS_STATUS_VALUES.has(status)) {
276
+ throw new Error(`Unsupported harness status for ${name}: ${status}`);
277
+ }
278
+ return {
279
+ name,
280
+ status,
281
+ workflows: readStringList(harness, "workflows"),
282
+ commands: readStringList(harness, "commands")
283
+ };
284
+ });
285
+ }
286
+
287
+ function parseWorkflows(value) {
288
+ const raw = requireRecord(value ?? {}, "workflows");
289
+ return Object.entries(raw).map(([name, entry]) => {
290
+ const workflow = requireRecord(entry, `workflows.${name}`);
291
+ return {
292
+ name,
293
+ harness: readString(workflow, "harness", "unspecified"),
294
+ activeSkills: readStringList(workflow, "active_skills"),
295
+ blockedSkills: readStringList(workflow, "blocked_skills"),
296
+ requiredOutputs: readStringList(workflow, "required_outputs"),
297
+ requiredCapabilities: parseRequiredCapabilities(workflow.required_capabilities, `workflows.${name}.required_capabilities`)
298
+ };
299
+ });
300
+ }
301
+
302
302
  function parseRequiredCapabilities(value, label) {
303
303
  const raw = requireRecord(value ?? {}, label);
304
304
  return Object.entries(raw).map(([name, entry]) => {