skillbox 0.1.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Christian Anagnostou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # skillbox
2
+
3
+ Local-first, agent-agnostic skills manager. Track, update, and sync skills across popular AI coding agents with one CLI.
4
+
5
+ ## Installation
6
+
7
+ ### npm (recommended)
8
+
9
+ ```bash
10
+ npm install -g skillbox
11
+ ```
12
+
13
+ TODO: Publish initial npm release
14
+
15
+ ### From Source
16
+
17
+ ```bash
18
+ git clone https://github.com/christiananagnostou/skillbox
19
+ cd skillbox
20
+ npm install
21
+ npm run build
22
+ npm link --global
23
+ ```
24
+
25
+ ### Homebrew
26
+
27
+ ```bash
28
+ brew tap christiananagnostou/skillbox
29
+ brew install skillbox
30
+ ```
31
+
32
+ TODO: Homebrew formula publishing
33
+
34
+ ## CI
35
+
36
+ ```bash
37
+ npm run lint:ci
38
+ npm run format:check
39
+ npm run build
40
+ ```
41
+
42
+ TODO: Publish GitHub Actions badge
43
+
44
+ ## Quick Start
45
+
46
+ ```bash
47
+ skillbox add https://example.com/skills/linting/SKILL.md
48
+ skillbox list
49
+ skillbox status
50
+ skillbox update linting
51
+ ```
52
+
53
+ ## Commands
54
+
55
+ ### Core Commands
56
+
57
+ ```bash
58
+ skillbox add <url> [--name <name>] [--global] [--agents ...]
59
+ skillbox convert <url> [--name <name>] [--output <dir>] [--agent]
60
+ skillbox list [--group=category|namespace|source|project] [--json]
61
+ skillbox status [--group=project|source] [--json]
62
+ skillbox update [name] [--system] [--project <path>]
63
+ skillbox import <path>
64
+ skillbox meta set <name> --category foo --tag bar --namespace baz
65
+ skillbox agent
66
+ ```
67
+
68
+ ### Project Commands
69
+
70
+ ```bash
71
+ skillbox project add <path> [--agent-path agent=path]
72
+ skillbox project list
73
+ skillbox project inspect <path>
74
+ skillbox project sync <path>
75
+ ```
76
+
77
+ ### Config
78
+
79
+ ```bash
80
+ skillbox config get
81
+ skillbox config set --default-scope user
82
+ skillbox config set --default-agent claude --default-agent cursor
83
+ skillbox config set --manage-system
84
+ ```
85
+
86
+ TODO: Config option reference (defaults, values, validation)
87
+
88
+ ## Agent Mode
89
+
90
+ Use `--json` for machine-readable output.
91
+
92
+ ```bash
93
+ skillbox list --json
94
+ skillbox status --json
95
+ skillbox update linting --json
96
+ ```
97
+
98
+ ### Agent Usage Snippet
99
+
100
+ ```text
101
+ Use skillbox for skill management.
102
+
103
+ Common workflow:
104
+ 1) skillbox list --json
105
+ 2) skillbox status --json
106
+ 3) skillbox update <name> --json
107
+
108
+ If you need to install a new skill from a URL, run:
109
+ skillbox add <url> [--name <name>]
110
+
111
+ If a URL is not a valid skill, run:
112
+ skillbox convert <url> --agent
113
+ ```
114
+
115
+ ## Skill Locations
116
+
117
+ Skillbox maintains a canonical store and syncs into agent-native folders.
118
+
119
+ Canonical store:
120
+
121
+ - `~/.config/skillbox/skills/<name>/`
122
+
123
+ Index + config:
124
+
125
+ - `~/.config/skillbox/index.json`
126
+ - `~/.config/skillbox/projects.json`
127
+ - `~/.config/skillbox/config.json`
128
+
129
+ Agent paths (default):
130
+
131
+ - OpenCode: `.opencode/skills/`, `~/.config/opencode/skills/` (Claude-compatible `.claude/skills/` also supported)
132
+ - Claude: `.claude/skills/`, `~/.claude/skills/`
133
+ - Cursor: `.cursor/skills/`, `.claude/skills/`, `~/.cursor/skills/`, `~/.claude/skills/`
134
+ - Codex: `$REPO_ROOT/.codex/skills/`, `~/.codex/skills/`, `/etc/codex/skills` (system)
135
+ - Amp: `.agents/skills/`, `~/.config/agents/skills/` (Claude-compatible `.claude/skills/` also supported)
136
+ - Antigravity: `.agent/skills/`, `~/.gemini/antigravity/skills/`
137
+
138
+ TODO: Validate agent path list against upstream docs before release
139
+
140
+ ## Usage with AI Agents
141
+
142
+ ### Just ask the agent
143
+
144
+ The simplest approach is to instruct your agent to use Skillbox:
145
+
146
+ ```
147
+ Use skillbox to manage skills for this repo. Run skillbox --help for all commands.
148
+ ```
149
+
150
+ ### AGENTS.md / CLAUDE.md
151
+
152
+ Add this to your project instructions for more consistent results:
153
+
154
+ ```markdown
155
+ ## Skills
156
+
157
+ Use `skillbox` to manage skills. Run `skillbox --help` for all commands.
158
+
159
+ Core workflow:
160
+
161
+ 1. `skillbox list --json`
162
+ 2. `skillbox status --json`
163
+ 3. `skillbox update <name> --json`
164
+ ```
165
+
166
+ ### Claude Code Skill
167
+
168
+ TODO: Provide a Skillbox skill for Claude Code (SKILL.md)
169
+
170
+ ## License
171
+
172
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { registerAdd } from "./commands/add.js";
4
+ import { registerConvert } from "./commands/convert.js";
5
+ import { registerList } from "./commands/list.js";
6
+ import { registerStatus } from "./commands/status.js";
7
+ import { registerUpdate } from "./commands/update.js";
8
+ import { registerImport } from "./commands/import.js";
9
+ import { registerMeta } from "./commands/meta.js";
10
+ import { registerProject } from "./commands/project.js";
11
+ import { registerAgent } from "./commands/agent.js";
12
+ import { registerConfig } from "./commands/config.js";
13
+ const program = new Command();
14
+ program.name("skillbox").description("Local-first, agent-agnostic skills manager").version("0.1.0");
15
+ registerAdd(program);
16
+ registerConvert(program);
17
+ registerList(program);
18
+ registerStatus(program);
19
+ registerUpdate(program);
20
+ registerImport(program);
21
+ registerMeta(program);
22
+ registerProject(program);
23
+ registerAgent(program);
24
+ registerConfig(program);
25
+ program.parse();
@@ -0,0 +1,105 @@
1
+ import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
2
+ import { fetchText } from "../lib/fetcher.js";
3
+ import { parseSkillMarkdown, inferNameFromUrl, buildMetadata } from "../lib/skill-parser.js";
4
+ import { handleCommandError } from "../lib/command.js";
5
+ import { ensureSkillsDir, writeSkillFiles } from "../lib/skill-store.js";
6
+ import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
7
+ import { buildTargets, copySkillToTargets } from "../lib/sync.js";
8
+ import { buildProjectAgentPaths } from "../lib/project-paths.js";
9
+ import { resolveRuntime, ensureProjectRegistered } from "../lib/runtime.js";
10
+ export const registerAdd = (program) => {
11
+ program
12
+ .command("add")
13
+ .argument("<url>", "Skill URL")
14
+ .option("--name <name>", "Override skill name")
15
+ .option("--global", "Install to user scope")
16
+ .option("--agents <list>", "Comma-separated agent list")
17
+ .option("--json", "JSON output")
18
+ .action(async (url, options) => {
19
+ try {
20
+ const skillMarkdown = await fetchText(url);
21
+ const parsed = parseSkillMarkdown(skillMarkdown);
22
+ const inferred = inferNameFromUrl(url);
23
+ const skillName = options.name ?? inferred ?? parsed.name;
24
+ if (!skillName) {
25
+ throw new Error("Unable to infer skill name. Use --name to specify it.");
26
+ }
27
+ if (!parsed.name && !options.name) {
28
+ throw new Error("Skill frontmatter missing name. Provide --name to continue.");
29
+ }
30
+ const metadata = buildMetadata(parsed, { type: "url", url }, skillName);
31
+ if (!parsed.description) {
32
+ throw new Error("Skill frontmatter missing description. Convert the source into a valid skill.");
33
+ }
34
+ await ensureSkillsDir();
35
+ await writeSkillFiles(skillName, skillMarkdown, metadata);
36
+ const index = await loadIndex();
37
+ const updated = upsertSkill(index, {
38
+ name: skillName,
39
+ source: { type: "url", url },
40
+ checksum: parsed.checksum,
41
+ updatedAt: metadata.updatedAt,
42
+ });
43
+ const { projectRoot, scope, agentList } = await resolveRuntime({
44
+ global: options.global,
45
+ agents: options.agents,
46
+ });
47
+ const projectEntry = await ensureProjectRegistered(projectRoot, scope);
48
+ const paths = buildProjectAgentPaths(projectRoot, projectEntry);
49
+ const installed = [];
50
+ const installs = [];
51
+ for (const agent of agentList) {
52
+ const map = paths[agent];
53
+ if (!map) {
54
+ continue;
55
+ }
56
+ const targets = buildTargets(agent, map, scope).map((target) => target.path);
57
+ const written = await copySkillToTargets(skillName, targets);
58
+ installed.push({ agent, scope, targets: written });
59
+ for (const target of written) {
60
+ installs.push({
61
+ scope,
62
+ agent,
63
+ path: target,
64
+ projectRoot: scope === "project" ? projectRoot : undefined,
65
+ });
66
+ }
67
+ }
68
+ const nextIndex = upsertSkill(updated, {
69
+ name: skillName,
70
+ source: { type: "url", url },
71
+ checksum: parsed.checksum,
72
+ updatedAt: metadata.updatedAt,
73
+ installs,
74
+ });
75
+ await saveIndex(sortIndex(nextIndex));
76
+ if (isJsonEnabled(options)) {
77
+ printJson({
78
+ ok: true,
79
+ command: "add",
80
+ data: {
81
+ name: skillName,
82
+ url,
83
+ scope,
84
+ agents: installed,
85
+ },
86
+ });
87
+ return;
88
+ }
89
+ printInfo(`Installed skill: ${skillName}`);
90
+ printInfo(`Source: ${url}`);
91
+ printInfo(`Scope: ${scope}`);
92
+ if (installed.length === 0) {
93
+ printInfo("No agent targets were updated.");
94
+ }
95
+ else {
96
+ for (const entry of installed) {
97
+ printInfo(`Updated ${entry.agent}: ${entry.targets.join(", ")}`);
98
+ }
99
+ }
100
+ }
101
+ catch (error) {
102
+ handleCommandError(options, "add", error);
103
+ }
104
+ });
105
+ };
@@ -0,0 +1,33 @@
1
+ import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
2
+ const agentSnippet = `Use skillbox for skill management.
3
+
4
+ Common workflow:
5
+ 1) skillbox list --json
6
+ 2) skillbox status --json
7
+ 3) skillbox update <name> --json
8
+
9
+ If you need to install a new skill from a URL, run:
10
+ skillbox add <url> [--name <name>]
11
+
12
+ If a URL is not a valid skill, run:
13
+ skillbox convert <url> --agent
14
+ `;
15
+ export const registerAgent = (program) => {
16
+ program
17
+ .command("agent")
18
+ .description("Print agent-friendly usage")
19
+ .option("--json", "JSON output")
20
+ .action((options) => {
21
+ if (isJsonEnabled(options)) {
22
+ printJson({
23
+ ok: true,
24
+ command: "agent",
25
+ data: {
26
+ snippet: agentSnippet,
27
+ },
28
+ });
29
+ return;
30
+ }
31
+ printInfo(agentSnippet);
32
+ });
33
+ };
@@ -0,0 +1,55 @@
1
+ import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
2
+ import { loadConfig, saveConfig } from "../lib/config.js";
3
+ import { handleCommandError } from "../lib/command.js";
4
+ const collect = (value, previous = []) => {
5
+ return [...previous, value];
6
+ };
7
+ export const registerConfig = (program) => {
8
+ const config = program.command("config").description("View or edit skillbox config");
9
+ config
10
+ .command("get")
11
+ .option("--json", "JSON output")
12
+ .action(async (options) => {
13
+ try {
14
+ const current = await loadConfig();
15
+ if (isJsonEnabled(options)) {
16
+ printJson({ ok: true, command: "config get", data: current });
17
+ return;
18
+ }
19
+ printInfo(JSON.stringify(current, null, 2));
20
+ }
21
+ catch (error) {
22
+ handleCommandError(options, "config get", error);
23
+ }
24
+ });
25
+ config
26
+ .command("set")
27
+ .option("--default-agent <agent>", "Default agent", collect)
28
+ .option("--default-scope <scope>", "Default scope: project or user")
29
+ .option("--manage-system", "Enable system scope operations")
30
+ .option("--json", "JSON output")
31
+ .action(async (options) => {
32
+ try {
33
+ const current = await loadConfig();
34
+ const nextScope = options.defaultScope ?? current.defaultScope;
35
+ if (nextScope !== "project" && nextScope !== "user") {
36
+ throw new Error("defaultScope must be 'project' or 'user'.");
37
+ }
38
+ const next = {
39
+ ...current,
40
+ defaultAgents: options.defaultAgent ?? current.defaultAgents,
41
+ defaultScope: nextScope,
42
+ manageSystem: options.manageSystem ?? current.manageSystem,
43
+ };
44
+ await saveConfig(next);
45
+ if (isJsonEnabled(options)) {
46
+ printJson({ ok: true, command: "config set", data: next });
47
+ return;
48
+ }
49
+ printInfo("Config updated.");
50
+ }
51
+ catch (error) {
52
+ handleCommandError(options, "config set", error);
53
+ }
54
+ });
55
+ };
@@ -0,0 +1,64 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
4
+ import { fetchText } from "../lib/fetcher.js";
5
+ import { inferNameFromUrl } from "../lib/skill-parser.js";
6
+ import { handleCommandError } from "../lib/command.js";
7
+ export const registerConvert = (program) => {
8
+ program
9
+ .command("convert")
10
+ .argument("<url>", "Source URL to convert")
11
+ .option("--name <name>", "Override skill name")
12
+ .option("--output <dir>", "Output directory")
13
+ .option("--agent", "Delegate conversion to agent")
14
+ .option("--json", "JSON output")
15
+ .action(async (url, options) => {
16
+ try {
17
+ const sourceText = await fetchText(url);
18
+ const inferred = inferNameFromUrl(url);
19
+ const skillName = options.name ?? inferred;
20
+ if (!skillName) {
21
+ throw new Error("Unable to infer skill name. Use --name to specify it.");
22
+ }
23
+ const outputDir = options.output
24
+ ? path.resolve(options.output)
25
+ : path.join(process.cwd(), "skillbox-convert", skillName);
26
+ const description = "Draft skill generated from source content.";
27
+ const draftMarkdown = `---\nname: ${skillName}\ndescription: ${description}\n---\n\n# ${skillName}\n\n## Source\n- See source.txt for the raw content.\n\n## When to use\n- TODO\n\n## Instructions\n- TODO\n`;
28
+ const metadata = {
29
+ name: skillName,
30
+ version: "0.1.0",
31
+ description,
32
+ entry: "SKILL.md",
33
+ source: { type: "url", url },
34
+ checksum: "draft",
35
+ updatedAt: new Date().toISOString(),
36
+ };
37
+ await fs.mkdir(outputDir, { recursive: true });
38
+ await fs.writeFile(path.join(outputDir, "source.txt"), sourceText, "utf8");
39
+ await fs.writeFile(path.join(outputDir, "SKILL.md"), draftMarkdown, "utf8");
40
+ await fs.writeFile(path.join(outputDir, "skill.json"), `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
41
+ if (isJsonEnabled(options)) {
42
+ printJson({
43
+ ok: true,
44
+ command: "convert",
45
+ data: {
46
+ url,
47
+ name: skillName,
48
+ outputDir,
49
+ agent: Boolean(options.agent),
50
+ sourceLength: sourceText.length,
51
+ },
52
+ });
53
+ return;
54
+ }
55
+ printInfo(`Draft created: ${outputDir}`);
56
+ if (options.agent) {
57
+ printInfo("Agent mode enabled. Use the source.txt content to refine SKILL.md.");
58
+ }
59
+ }
60
+ catch (error) {
61
+ handleCommandError(options, "convert", error);
62
+ }
63
+ });
64
+ };
@@ -0,0 +1,50 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
4
+ import { parseSkillMarkdown, buildMetadata } from "../lib/skill-parser.js";
5
+ import { ensureSkillsDir, writeSkillFiles } from "../lib/skill-store.js";
6
+ import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
7
+ import { handleCommandError } from "../lib/command.js";
8
+ export const registerImport = (program) => {
9
+ program
10
+ .command("import")
11
+ .argument("<path>", "Path to skill directory")
12
+ .option("--json", "JSON output")
13
+ .action(async (inputPath, options) => {
14
+ try {
15
+ const resolved = path.resolve(inputPath);
16
+ const skillPath = path.join(resolved, "SKILL.md");
17
+ const markdown = await fs.readFile(skillPath, "utf8");
18
+ const parsed = parseSkillMarkdown(markdown);
19
+ const metadata = buildMetadata(parsed, { type: "local" });
20
+ if (!parsed.description) {
21
+ throw new Error("Skill frontmatter missing description.");
22
+ }
23
+ await ensureSkillsDir();
24
+ await writeSkillFiles(metadata.name, markdown, metadata);
25
+ const index = await loadIndex();
26
+ const updated = upsertSkill(index, {
27
+ name: metadata.name,
28
+ source: { type: "local" },
29
+ checksum: parsed.checksum,
30
+ updatedAt: metadata.updatedAt,
31
+ });
32
+ await saveIndex(sortIndex(updated));
33
+ if (isJsonEnabled(options)) {
34
+ printJson({
35
+ ok: true,
36
+ command: "import",
37
+ data: {
38
+ name: metadata.name,
39
+ path: resolved,
40
+ },
41
+ });
42
+ return;
43
+ }
44
+ printInfo(`Imported skill: ${metadata.name}`);
45
+ }
46
+ catch (error) {
47
+ handleCommandError(options, "import", error);
48
+ }
49
+ });
50
+ };
@@ -0,0 +1,95 @@
1
+ import { isJsonEnabled, printInfo, printJson, printGroupList } from "../lib/output.js";
2
+ import { loadIndex } from "../lib/index.js";
3
+ import { groupNamesByKey } from "../lib/grouping.js";
4
+ export const registerList = (program) => {
5
+ program
6
+ .command("list")
7
+ .option("--group <group>", "Group by category, namespace, source, project")
8
+ .option("--json", "JSON output")
9
+ .action(async (options) => {
10
+ const index = await loadIndex();
11
+ const skills = index.skills;
12
+ const groupedProjects = groupByProject(skills);
13
+ const groupedSources = groupBySource(skills);
14
+ const groupedNamespaces = groupByNamespace(skills);
15
+ const groupedCategories = groupByCategory(skills);
16
+ if (isJsonEnabled(options)) {
17
+ printJson({
18
+ ok: true,
19
+ command: "list",
20
+ data: {
21
+ group: options.group ?? null,
22
+ skills,
23
+ projects: options.group === "project" ? groupedProjects : undefined,
24
+ sources: options.group === "source" ? groupedSources : undefined,
25
+ namespaces: options.group === "namespace" ? groupedNamespaces : undefined,
26
+ categories: options.group === "category" ? groupedCategories : undefined,
27
+ },
28
+ });
29
+ return;
30
+ }
31
+ if (options.group === "project") {
32
+ printGroupList("Projects", groupedProjects.map((project) => ({ key: project.root, items: project.skills })));
33
+ return;
34
+ }
35
+ if (options.group === "source") {
36
+ printGroupList("Sources", groupedSources.map((source) => ({ key: source.source, items: source.skills })));
37
+ return;
38
+ }
39
+ if (options.group === "namespace") {
40
+ printGroupList("Namespaces", groupedNamespaces.map((namespace) => ({
41
+ key: namespace.namespace,
42
+ items: namespace.skills,
43
+ })));
44
+ return;
45
+ }
46
+ if (options.group === "category") {
47
+ printGroupList("Categories", groupedCategories.map((category) => ({ key: category.category, items: category.skills })));
48
+ return;
49
+ }
50
+ printInfo(`Skills: ${skills.length}`);
51
+ for (const skill of skills) {
52
+ const source = skill.source.type;
53
+ const namespace = skill.namespace ? ` (${skill.namespace})` : "";
54
+ printInfo(`- ${skill.name}${namespace} [${source}]`);
55
+ }
56
+ });
57
+ };
58
+ const groupByProject = (skills) => {
59
+ const grouped = groupNamesByKey(skills, (skill) => skill.name, (skill) => (skill.installs ?? [])
60
+ .filter((install) => install.scope === "project" && install.projectRoot)
61
+ .map((install) => install.projectRoot));
62
+ return grouped.map((group) => ({ root: group.key, skills: group.skills }));
63
+ };
64
+ const groupBySource = (skills) => {
65
+ const grouped = groupNamesByKey(skills, (skill) => skill.name, (skill) => [skill.source.type]);
66
+ return grouped.map((group) => ({ source: group.key, skills: group.skills }));
67
+ };
68
+ const groupByNamespace = (skills) => {
69
+ const grouped = groupNamesByKey(skills, (skill) => skill.name, (skill) => [skill.namespace ?? "(none)"]);
70
+ return grouped
71
+ .map((group) => ({ namespace: group.key, skills: group.skills }))
72
+ .sort((a, b) => {
73
+ if (a.namespace === "(none)") {
74
+ return 1;
75
+ }
76
+ if (b.namespace === "(none)") {
77
+ return -1;
78
+ }
79
+ return a.namespace.localeCompare(b.namespace);
80
+ });
81
+ };
82
+ const groupByCategory = (skills) => {
83
+ const grouped = groupNamesByKey(skills, (skill) => skill.name, (skill) => skill.categories ?? ["(uncategorized)"]);
84
+ return grouped
85
+ .map((group) => ({ category: group.key, skills: group.skills }))
86
+ .sort((a, b) => {
87
+ if (a.category === "(uncategorized)") {
88
+ return 1;
89
+ }
90
+ if (b.category === "(uncategorized)") {
91
+ return -1;
92
+ }
93
+ return a.category.localeCompare(b.category);
94
+ });
95
+ };
@@ -0,0 +1,60 @@
1
+ import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
2
+ import { readSkillMetadata, writeSkillMetadata } from "../lib/skill-store.js";
3
+ import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
4
+ import { handleCommandError } from "../lib/command.js";
5
+ export const registerMeta = (program) => {
6
+ const meta = program.command("meta").description("Manage skill metadata");
7
+ meta
8
+ .command("set")
9
+ .argument("<name>", "Skill name")
10
+ .option("--category <category>", "Category", collect)
11
+ .option("--tag <tag>", "Tag", collect)
12
+ .option("--namespace <namespace>", "Namespace")
13
+ .option("--json", "JSON output")
14
+ .action(async (name, options) => {
15
+ try {
16
+ const metadata = await readSkillMetadata(name);
17
+ const categories = options.category ?? metadata.categories ?? [];
18
+ const tags = options.tag ?? metadata.tags ?? [];
19
+ const namespace = options.namespace ?? metadata.namespace;
20
+ const nextMetadata = {
21
+ ...metadata,
22
+ categories: categories.length > 0 ? categories : undefined,
23
+ tags: tags.length > 0 ? tags : undefined,
24
+ namespace,
25
+ };
26
+ await writeSkillMetadata(name, nextMetadata);
27
+ const index = await loadIndex();
28
+ const updated = upsertSkill(index, {
29
+ name,
30
+ source: metadata.source,
31
+ checksum: metadata.checksum,
32
+ updatedAt: metadata.updatedAt,
33
+ categories: nextMetadata.categories,
34
+ tags: nextMetadata.tags,
35
+ namespace: nextMetadata.namespace,
36
+ });
37
+ await saveIndex(sortIndex(updated));
38
+ if (isJsonEnabled(options)) {
39
+ printJson({
40
+ ok: true,
41
+ command: "meta set",
42
+ data: {
43
+ name,
44
+ categories: nextMetadata.categories ?? [],
45
+ tags: nextMetadata.tags ?? [],
46
+ namespace: nextMetadata.namespace ?? null,
47
+ },
48
+ });
49
+ return;
50
+ }
51
+ printInfo(`Updated metadata for ${name}`);
52
+ }
53
+ catch (error) {
54
+ handleCommandError(options, "meta set", error);
55
+ }
56
+ });
57
+ };
58
+ const collect = (value, previous = []) => {
59
+ return [...previous, value];
60
+ };