skillbox 0.3.2 → 0.3.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.
package/README.md CHANGED
@@ -67,8 +67,9 @@ skillbox update [name] # update skills
67
67
 
68
68
  | Command | Description |
69
69
  |---------|-------------|
70
- | `skillbox project add <path>` | Register a project |
70
+ | `skillbox project add <path>` | Register a project and auto-import skills from `skills/` and agent directories |
71
71
  | `skillbox project list` | List registered projects |
72
+ | `skillbox project inspect <path>` | Show project details and skills |
72
73
  | `skillbox project sync <path>` | Re-sync skills to a project |
73
74
 
74
75
  ## Supported Agents
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import { registerRemove } from "./commands/remove.js";
12
12
  import { registerStatus } from "./commands/status.js";
13
13
  import { registerUpdate } from "./commands/update.js";
14
14
  const program = new Command();
15
- program.name("skillbox").description("Local-first, agent-agnostic skills manager").version("0.3.2");
15
+ program.name("skillbox").description("Local-first, agent-agnostic skills manager").version("0.3.3");
16
16
  registerAdd(program);
17
17
  registerAgent(program);
18
18
  registerConfig(program);
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import { getErrorMessage } from "../lib/command.js";
2
3
  import { loadConfig } from "../lib/config.js";
3
4
  import { parseRepoRef } from "../lib/github.js";
@@ -38,25 +39,22 @@ async function installSkillTargets(skillName, options, installs) {
38
39
  if (!map) {
39
40
  continue;
40
41
  }
41
- const targets = buildTargets(agent, map, scope).map((target) => target.path);
42
+ const targets = buildTargets(agent, map, scope).map((target) => path.join(target.path, skillName));
42
43
  const results = await installSkillToTargets(skillName, targets, config);
43
- const written = results
44
- .filter((result) => result.mode !== "skipped")
45
- .map((result) => result.path);
46
44
  const warnings = buildSymlinkWarning(agent, results);
47
45
  for (const warning of warnings) {
48
46
  printInfo(warning);
49
47
  }
50
- const deduped = recordInstallPaths(written, recordedPaths);
51
- if (deduped.length > 0) {
52
- for (const target of deduped) {
53
- installs.push({
54
- scope,
55
- agent,
56
- path: target,
57
- projectRoot: scope === "project" ? projectRoot : undefined,
58
- });
59
- }
48
+ // Record all targets, not just successfully written ones
49
+ // The warning tells users about symlink issues, but we still track the install intent
50
+ const deduped = recordInstallPaths(targets, recordedPaths);
51
+ for (const target of deduped) {
52
+ installs.push({
53
+ scope,
54
+ agent,
55
+ path: target,
56
+ projectRoot: scope === "project" ? projectRoot : undefined,
57
+ });
60
58
  }
61
59
  }
62
60
  }
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import { handleCommandError } from "../lib/command.js";
2
3
  import { loadConfig } from "../lib/config.js";
3
4
  import { fetchText } from "../lib/fetcher.js";
@@ -71,16 +72,15 @@ export function registerAdd(program) {
71
72
  if (!map) {
72
73
  continue;
73
74
  }
74
- const targets = buildTargets(agent, map, scope).map((target) => target.path);
75
+ const targets = buildTargets(agent, map, scope).map((target) => path.join(target.path, skillName));
75
76
  const results = await installSkillToTargets(skillName, targets, config);
76
- const written = results
77
- .filter((result) => result.mode !== "skipped")
78
- .map((result) => result.path);
79
77
  const warnings = buildSymlinkWarning(agent, results);
80
78
  for (const warning of warnings) {
81
79
  printInfo(warning);
82
80
  }
83
- const deduped = recordInstallPaths(written, recordedPaths);
81
+ // Record all targets, not just successfully written ones
82
+ // The warning tells users about symlink issues, but we still track the install intent
83
+ const deduped = recordInstallPaths(targets, recordedPaths);
84
84
  if (deduped.length > 0) {
85
85
  installed.push({ agent, scope, targets: deduped });
86
86
  for (const target of deduped) {
@@ -1,4 +1,3 @@
1
- import fs from "node:fs/promises";
2
1
  import path from "node:path";
3
2
  import { getUserPathsForAgents } from "../lib/agents.js";
4
3
  import { handleCommandError } from "../lib/command.js";
@@ -6,8 +5,7 @@ import { discoverSkills } from "../lib/discovery.js";
6
5
  import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
7
6
  import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
8
7
  import { resolveRuntime } from "../lib/runtime.js";
9
- import { buildMetadata, parseSkillMarkdown } from "../lib/skill-parser.js";
10
- import { ensureSkillsDir, writeSkillFiles } from "../lib/skill-store.js";
8
+ import { importSkillFromDir } from "../lib/skill-store.js";
11
9
  async function importGlobalSkills(agents) {
12
10
  const projectRoot = process.cwd();
13
11
  const agentPaths = getUserPathsForAgents(projectRoot, agents);
@@ -32,24 +30,20 @@ async function importGlobalSkills(agents) {
32
30
  skipped.add(skill.name);
33
31
  continue;
34
32
  }
35
- const markdown = await fs.readFile(skill.skillFile, "utf8");
36
- const parsed = parseSkillMarkdown(markdown);
37
- if (!parsed.description) {
33
+ const data = await importSkillFromDir(skill.skillFile);
34
+ if (!data) {
38
35
  skipped.add(skill.name);
39
36
  continue;
40
37
  }
41
- const metadata = buildMetadata(parsed, { type: "local" });
42
- await ensureSkillsDir();
43
- await writeSkillFiles(metadata.name, markdown, metadata);
44
38
  const next = upsertSkill(index, {
45
- name: metadata.name,
39
+ name: data.name,
46
40
  source: { type: "local" },
47
- checksum: parsed.checksum,
48
- updatedAt: metadata.updatedAt,
41
+ checksum: data.checksum,
42
+ updatedAt: data.updatedAt,
49
43
  installs: [{ scope: "user", agent: skill.agent, path: skill.skillDir }],
50
44
  });
51
45
  index.skills = next.skills;
52
- imported.add(metadata.name);
46
+ imported.add(data.name);
53
47
  }
54
48
  await saveIndex(sortIndex(index));
55
49
  return {
@@ -81,27 +75,23 @@ export function registerImport(program) {
81
75
  }
82
76
  const resolved = path.resolve(inputPath);
83
77
  const skillPath = path.join(resolved, "SKILL.md");
84
- const markdown = await fs.readFile(skillPath, "utf8");
85
- const parsed = parseSkillMarkdown(markdown);
86
- if (!parsed.description) {
78
+ const data = await importSkillFromDir(skillPath);
79
+ if (!data) {
87
80
  throw new Error("Skill frontmatter missing description.");
88
81
  }
89
- const metadata = buildMetadata(parsed, { type: "local" });
90
- await ensureSkillsDir();
91
- await writeSkillFiles(metadata.name, markdown, metadata);
92
82
  const index = await loadIndex();
93
83
  const updated = upsertSkill(index, {
94
- name: metadata.name,
84
+ name: data.name,
95
85
  source: { type: "local" },
96
- checksum: parsed.checksum,
97
- updatedAt: metadata.updatedAt,
86
+ checksum: data.checksum,
87
+ updatedAt: data.updatedAt,
98
88
  });
99
89
  await saveIndex(sortIndex(updated));
100
90
  if (isJsonEnabled(options)) {
101
- printJson({ ok: true, command: "import", data: { name: metadata.name, path: resolved } });
91
+ printJson({ ok: true, command: "import", data: { name: data.name, path: resolved } });
102
92
  return;
103
93
  }
104
- printInfo(`Imported skill: ${metadata.name}`);
94
+ printInfo(`Imported skill: ${data.name}`);
105
95
  }
106
96
  catch (error) {
107
97
  handleCommandError(options, "import", error);
@@ -1,9 +1,34 @@
1
+ import chalk from "chalk";
1
2
  import fs from "node:fs/promises";
3
+ import terminalLink from "terminal-link";
2
4
  import { discoverGlobalSkills } from "../lib/global-skills.js";
3
5
  import { loadIndex } from "../lib/index.js";
4
6
  import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
5
7
  import { resolveRuntime } from "../lib/runtime.js";
6
8
  import { groupAndSort, sortByName } from "../lib/source-grouping.js";
9
+ function getSkillUrl(skill) {
10
+ if (skill.source.type === "url" && skill.source.url) {
11
+ return skill.source.url;
12
+ }
13
+ if (skill.source.type === "git" && skill.source.repo) {
14
+ const repo = skill.source.repo;
15
+ // If already a full URL, use it directly
16
+ if (repo.startsWith("http://") || repo.startsWith("https://")) {
17
+ return repo;
18
+ }
19
+ // Convert shorthand (user/repo) to full GitHub URL
20
+ return `https://github.com/${repo}`;
21
+ }
22
+ return undefined;
23
+ }
24
+ function linkSkillName(skill) {
25
+ const url = getSkillUrl(skill);
26
+ if (url && terminalLink.isSupported) {
27
+ const linkIcon = terminalLink("‹↗›", url);
28
+ return `${skill.name} ${chalk.dim(linkIcon)}`;
29
+ }
30
+ return skill.name;
31
+ }
7
32
  async function detectSubcommands(skillPath) {
8
33
  try {
9
34
  const entries = await fs.readdir(skillPath);
@@ -108,7 +133,7 @@ function printScopeGroup(group) {
108
133
  for (const sourceGroup of projectGroup.sourceGroups) {
109
134
  printInfo(` ${sourceGroup.source}`);
110
135
  for (const skill of sourceGroup.skills) {
111
- printInfo(` ${skill.name}`);
136
+ printInfo(` ${linkSkillName(skill)}`);
112
137
  if (skill.subcommands.length > 0) {
113
138
  printInfo(` → ${skill.subcommands.join(", ")}`);
114
139
  }
@@ -121,7 +146,7 @@ function printScopeGroup(group) {
121
146
  printInfo("");
122
147
  printInfo(`${sourceGroup.source}`);
123
148
  for (const skill of sourceGroup.skills) {
124
- printInfo(` ${skill.name}`);
149
+ printInfo(` ${linkSkillName(skill)}`);
125
150
  if (skill.subcommands.length > 0) {
126
151
  printInfo(` → ${skill.subcommands.join(", ")}`);
127
152
  }
@@ -1,11 +1,53 @@
1
1
  import path from "node:path";
2
+ import { allAgents, getProjectPathsForAgents } from "../lib/agents.js";
2
3
  import { handleCommandError } from "../lib/command.js";
4
+ import { discoverSkills } from "../lib/discovery.js";
3
5
  import { collect } from "../lib/fs-utils.js";
4
- import { loadIndex } from "../lib/index.js";
6
+ import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
5
7
  import { collectProjectSkills, getProjectInstallPaths, getProjectSkills } from "../lib/installs.js";
6
8
  import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
7
9
  import { loadProjects, saveProjects, upsertProject } from "../lib/projects.js";
10
+ import { importSkillFromDir } from "../lib/skill-store.js";
8
11
  import { copySkillToInstallPaths } from "../lib/sync.js";
12
+ function getProjectSkillPaths(projectRoot) {
13
+ const agentPaths = getProjectPathsForAgents(projectRoot, allAgents);
14
+ const seen = new Set();
15
+ // Add generic skills/ directory
16
+ seen.add(path.join(projectRoot, "skills"));
17
+ // Add all agent-specific project paths
18
+ for (const { path: agentPath } of agentPaths) {
19
+ seen.add(agentPath);
20
+ }
21
+ return Array.from(seen);
22
+ }
23
+ async function discoverProjectSkills(projectRoot) {
24
+ const skillPaths = getProjectSkillPaths(projectRoot);
25
+ const discovered = await discoverSkills(skillPaths);
26
+ if (discovered.length === 0) {
27
+ return [];
28
+ }
29
+ const index = await loadIndex();
30
+ const imported = [];
31
+ for (const skill of discovered) {
32
+ const data = await importSkillFromDir(skill.skillFile);
33
+ if (!data) {
34
+ continue;
35
+ }
36
+ const updated = upsertSkill(index, {
37
+ name: data.name,
38
+ source: { type: "local" },
39
+ checksum: data.checksum,
40
+ updatedAt: data.updatedAt,
41
+ installs: [{ scope: "project", agent: "claude", path: skill.skillDir, projectRoot }],
42
+ });
43
+ index.skills = updated.skills;
44
+ imported.push(data.name);
45
+ }
46
+ if (imported.length > 0) {
47
+ await saveIndex(sortIndex(index));
48
+ }
49
+ return imported;
50
+ }
9
51
  function parseAgentPaths(entries) {
10
52
  const overrides = {};
11
53
  for (const entry of entries) {
@@ -22,6 +64,7 @@ export function registerProject(program) {
22
64
  const project = program.command("project").description("Manage projects");
23
65
  project
24
66
  .command("add")
67
+ .description("Register a project and import skills from its skills/ directory")
25
68
  .argument("<path>", "Project path")
26
69
  .option("--agent-path <agentPath>", "Agent path override (agent=path)", collect)
27
70
  .option("--json", "JSON output")
@@ -45,6 +88,7 @@ export function registerProject(program) {
45
88
  }),
46
89
  };
47
90
  await saveProjects(merged);
91
+ const importedSkills = await discoverProjectSkills(resolved);
48
92
  if (isJsonEnabled(options)) {
49
93
  printJson({
50
94
  ok: true,
@@ -52,11 +96,15 @@ export function registerProject(program) {
52
96
  data: {
53
97
  path: resolved,
54
98
  agentPaths: merged.projects.find((p) => p.root === resolved)?.agentPaths ?? {},
99
+ skills: importedSkills,
55
100
  },
56
101
  });
57
102
  return;
58
103
  }
59
104
  printInfo(`Project registered: ${resolved}`);
105
+ if (importedSkills.length > 0) {
106
+ printInfo(`Discovered ${importedSkills.length} skill(s): ${importedSkills.join(", ")}`);
107
+ }
60
108
  }
61
109
  catch (error) {
62
110
  handleCommandError(options, "project add", error);
@@ -48,6 +48,12 @@ export function getUserAgentPaths(projectRoot) {
48
48
  return Object.values(paths).flatMap((entry) => entry.user);
49
49
  }
50
50
  export function getUserPathsForAgents(projectRoot, agents) {
51
+ return getPathsForAgents(projectRoot, agents, "user");
52
+ }
53
+ export function getProjectPathsForAgents(projectRoot, agents) {
54
+ return getPathsForAgents(projectRoot, agents, "project");
55
+ }
56
+ function getPathsForAgents(projectRoot, agents, scope) {
51
57
  const paths = agentPaths(projectRoot);
52
58
  const seen = new Set();
53
59
  const results = [];
@@ -56,12 +62,12 @@ export function getUserPathsForAgents(projectRoot, agents) {
56
62
  if (!agentEntry) {
57
63
  continue;
58
64
  }
59
- for (const userPath of agentEntry.user) {
60
- if (seen.has(userPath)) {
65
+ for (const pathValue of agentEntry[scope]) {
66
+ if (seen.has(pathValue)) {
61
67
  continue;
62
68
  }
63
- seen.add(userPath);
64
- results.push({ agent, path: userPath });
69
+ seen.add(pathValue);
70
+ results.push({ agent, path: pathValue });
65
71
  }
66
72
  }
67
73
  return results;
@@ -1,5 +1,44 @@
1
+ import { execSync } from "node:child_process";
2
+ const GITHUB_HOSTS = ["api.github.com", "raw.githubusercontent.com"];
3
+ let cachedToken;
4
+ function getGitHubToken() {
5
+ if (cachedToken !== undefined)
6
+ return cachedToken;
7
+ // Check environment variables first, then fall back to gh CLI
8
+ const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
9
+ if (envToken) {
10
+ cachedToken = envToken;
11
+ }
12
+ else {
13
+ try {
14
+ cachedToken =
15
+ execSync("gh auth token", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim() ||
16
+ null;
17
+ }
18
+ catch {
19
+ cachedToken = null;
20
+ }
21
+ }
22
+ return cachedToken;
23
+ }
24
+ function isGitHubUrl(url) {
25
+ try {
26
+ const { host } = new URL(url);
27
+ return GITHUB_HOSTS.includes(host);
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
1
33
  export async function fetchText(url) {
2
- const response = await fetch(url);
34
+ const headers = {};
35
+ if (isGitHubUrl(url)) {
36
+ const token = getGitHubToken();
37
+ if (token) {
38
+ headers.Authorization = `Bearer ${token}`;
39
+ }
40
+ }
41
+ const response = await fetch(url, { headers });
3
42
  if (!response.ok) {
4
43
  throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
5
44
  }
package/dist/lib/index.js CHANGED
@@ -29,9 +29,30 @@ export function upsertSkill(index, skill) {
29
29
  next.skills.push(skill);
30
30
  return next;
31
31
  }
32
- next.skills[existingIndex] = { ...next.skills[existingIndex], ...skill };
32
+ const existing = next.skills[existingIndex];
33
+ const mergedInstalls = mergeInstalls(existing.installs, skill.installs);
34
+ next.skills[existingIndex] = { ...existing, ...skill, installs: mergedInstalls };
33
35
  return next;
34
36
  }
37
+ function mergeInstalls(existing, incoming) {
38
+ if (!existing && !incoming)
39
+ return undefined;
40
+ if (!existing)
41
+ return incoming;
42
+ if (!incoming)
43
+ return existing;
44
+ // Dedupe by scope + agent + projectRoot combination
45
+ const seen = new Set();
46
+ const merged = [];
47
+ for (const install of [...existing, ...incoming]) {
48
+ const key = `${install.scope}:${install.agent}:${install.projectRoot ?? ""}`;
49
+ if (seen.has(key))
50
+ continue;
51
+ seen.add(key);
52
+ merged.push(install);
53
+ }
54
+ return merged;
55
+ }
35
56
  export function sortIndex(index) {
36
57
  const skills = [...index.skills].sort((a, b) => a.name.localeCompare(b.name));
37
58
  return { ...index, skills };
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { skillboxSkillsDir } from "./paths.js";
5
+ import { buildMetadata, parseSkillMarkdown } from "./skill-parser.js";
5
6
  export async function ensureSkillsDir() {
6
7
  await fs.mkdir(skillboxSkillsDir(), { recursive: true });
7
8
  }
@@ -26,3 +27,23 @@ export async function writeSkillMetadata(name, metadata) {
26
27
  export function hashContent(content) {
27
28
  return crypto.createHash("sha256").update(content).digest("hex");
28
29
  }
30
+ /**
31
+ * Import a skill from a local directory. Reads the markdown, parses it,
32
+ * and writes to the skill store. Returns the data needed for index updates,
33
+ * or null if the skill is invalid (missing description).
34
+ */
35
+ export async function importSkillFromDir(skillFile) {
36
+ const markdown = await fs.readFile(skillFile, "utf8");
37
+ const parsed = parseSkillMarkdown(markdown);
38
+ if (!parsed.description) {
39
+ return null;
40
+ }
41
+ const metadata = buildMetadata(parsed, { type: "local" });
42
+ await ensureSkillsDir();
43
+ await writeSkillFiles(metadata.name, markdown, metadata);
44
+ return {
45
+ name: metadata.name,
46
+ checksum: parsed.checksum,
47
+ updatedAt: metadata.updatedAt,
48
+ };
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillbox",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Local-first, agent-agnostic skills manager",
5
5
  "license": "MIT",
6
6
  "author": "Christian Anagnostou",
@@ -38,6 +38,7 @@
38
38
  "dependencies": {
39
39
  "chalk": "^5.3.0",
40
40
  "commander": "^12.0.0",
41
+ "terminal-link": "^3.0.0",
41
42
  "zod": "^3.23.8"
42
43
  },
43
44
  "devDependencies": {