skillbox 0.1.2 → 0.2.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.
package/README.md CHANGED
@@ -13,26 +13,13 @@
13
13
  npm install -g skillbox
14
14
  ```
15
15
 
16
- ### From Source
17
16
 
18
- ```bash
19
- git clone https://github.com/christiananagnostou/skillbox
20
- cd skillbox
21
- npm install
22
- npm run build
23
- npm link --global
24
- ```
25
-
26
- ## CI
27
17
 
28
- ```bash
29
- npm run lint:ci
30
- npm run format:check
31
- npm run build
32
- ```
18
+ ## Quick Start
33
19
 
20
+ Skillbox will detect installed agents on your machine. If detection succeeds, `enter` accepts the detected list; if nothing is found, `enter` defaults to all agents or you can type a comma-separated list.
34
21
 
35
- ## Quick Start
22
+ Tip: run `skillbox list` right after install to see existing skills.
36
23
 
37
24
  ```bash
38
25
  skillbox add https://example.com/skills/linting/SKILL.md
@@ -71,10 +58,15 @@ skillbox project sync <path>
71
58
  skillbox config get
72
59
  skillbox config set --default-scope user
73
60
  skillbox config set --default-agent claude --default-agent cursor
61
+ skillbox config set --add-agent codex
74
62
  skillbox config set --manage-system
75
63
  ```
76
64
 
77
- TODO: Config option reference (defaults, values, validation)
65
+ Config defaults live in `~/.config/skillbox/config.json`:
66
+
67
+ - `defaultScope`: `project` (default) or `user`
68
+ - `defaultAgents`: empty array means all agents
69
+ - `manageSystem`: `false` by default
78
70
 
79
71
  ## Agent Mode
80
72
 
@@ -126,7 +118,7 @@ Agent paths (default):
126
118
  - Amp: `.agents/skills/`, `~/.config/agents/skills/` (Claude-compatible `.claude/skills/` also supported)
127
119
  - Antigravity: `.agent/skills/`, `~/.gemini/antigravity/skills/`
128
120
 
129
- TODO: Validate agent path list against upstream docs before release
121
+ Note: only Codex defines a system scope path.
130
122
 
131
123
  ## Usage with AI Agents
132
124
 
@@ -154,9 +146,15 @@ Core workflow:
154
146
  3. `skillbox update <name> --json`
155
147
  ```
156
148
 
157
- ### Claude Code Skill
149
+ ## Development
158
150
 
159
- TODO: Provide a Skillbox skill for Claude Code (SKILL.md)
151
+ ```bash
152
+ git clone https://github.com/christiananagnostou/skillbox
153
+ cd skillbox
154
+ npm install
155
+ npm run build
156
+ npm link --global
157
+ ```
160
158
 
161
159
  ## License
162
160
 
package/dist/cli.js CHANGED
@@ -22,4 +22,9 @@ registerMeta(program);
22
22
  registerProject(program);
23
23
  registerAgent(program);
24
24
  registerConfig(program);
25
- program.parse();
25
+ const run = async () => {
26
+ const { runOnboarding } = await import("./lib/onboarding.js");
27
+ await runOnboarding();
28
+ program.parse();
29
+ };
30
+ void run();
@@ -25,6 +25,7 @@ export const registerConfig = (program) => {
25
25
  config
26
26
  .command("set")
27
27
  .option("--default-agent <agent>", "Default agent", collect)
28
+ .option("--add-agent <agent>", "Add to default agents", collect)
28
29
  .option("--default-scope <scope>", "Default scope: project or user")
29
30
  .option("--manage-system", "Enable system scope operations")
30
31
  .option("--json", "JSON output")
@@ -35,9 +36,12 @@ export const registerConfig = (program) => {
35
36
  if (nextScope !== "project" && nextScope !== "user") {
36
37
  throw new Error("defaultScope must be 'project' or 'user'.");
37
38
  }
39
+ const addedAgents = options.addAgent ?? [];
40
+ const nextAgents = options.defaultAgent ?? current.defaultAgents;
41
+ const mergedAgents = Array.from(new Set([...(nextAgents ?? []), ...addedAgents].filter((agent) => agent.length > 0)));
38
42
  const next = {
39
43
  ...current,
40
- defaultAgents: options.defaultAgent ?? current.defaultAgents,
44
+ defaultAgents: mergedAgents,
41
45
  defaultScope: nextScope,
42
46
  manageSystem: options.manageSystem ?? current.manageSystem,
43
47
  };
@@ -5,13 +5,36 @@ import { parseSkillMarkdown, buildMetadata } from "../lib/skill-parser.js";
5
5
  import { ensureSkillsDir, writeSkillFiles } from "../lib/skill-store.js";
6
6
  import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
7
7
  import { handleCommandError } from "../lib/command.js";
8
+ import { discoverSkills } from "../lib/discovery.js";
9
+ import { getSystemAgentPaths, getUserAgentPaths } from "../lib/agents.js";
8
10
  export const registerImport = (program) => {
9
11
  program
10
12
  .command("import")
11
- .argument("<path>", "Path to skill directory")
13
+ .argument("[path]", "Path to skill directory")
14
+ .option("--global", "Import skills from user agent folders")
15
+ .option("--system", "Import skills from system agent folders")
12
16
  .option("--json", "JSON output")
13
17
  .action(async (inputPath, options) => {
14
18
  try {
19
+ if (!inputPath && !options.global && !options.system) {
20
+ throw new Error("Provide a path or use --global/--system.");
21
+ }
22
+ if (options.global || options.system) {
23
+ const summary = await importGlobalSkills({
24
+ includeUser: Boolean(options.global),
25
+ includeSystem: Boolean(options.system),
26
+ });
27
+ if (isJsonEnabled(options)) {
28
+ printJson({
29
+ ok: true,
30
+ command: "import",
31
+ data: summary,
32
+ });
33
+ return;
34
+ }
35
+ printInfo(`Imported ${summary.imported.length} skill(s).`);
36
+ return;
37
+ }
15
38
  const resolved = path.resolve(inputPath);
16
39
  const skillPath = path.join(resolved, "SKILL.md");
17
40
  const markdown = await fs.readFile(skillPath, "utf8");
@@ -48,3 +71,50 @@ export const registerImport = (program) => {
48
71
  }
49
72
  });
50
73
  };
74
+ const importGlobalSkills = async (options) => {
75
+ const projectRoot = process.cwd();
76
+ const paths = [
77
+ ...(options.includeUser ? getUserAgentPaths(projectRoot) : []),
78
+ ...(options.includeSystem ? getSystemAgentPaths(projectRoot) : []),
79
+ ];
80
+ const discovered = await discoverSkills(paths);
81
+ const index = await loadIndex();
82
+ const seen = new Set(index.skills.map((skill) => skill.name));
83
+ const imported = [];
84
+ const skipped = [];
85
+ for (const skill of discovered) {
86
+ if (seen.has(skill.name)) {
87
+ skipped.push(skill.name);
88
+ continue;
89
+ }
90
+ const markdown = await fs.readFile(skill.skillFile, "utf8");
91
+ const parsed = parseSkillMarkdown(markdown);
92
+ const metadata = buildMetadata(parsed, { type: "local" });
93
+ if (!parsed.description) {
94
+ skipped.push(skill.name);
95
+ continue;
96
+ }
97
+ await ensureSkillsDir();
98
+ await writeSkillFiles(metadata.name, markdown, metadata);
99
+ const next = upsertSkill(index, {
100
+ name: metadata.name,
101
+ source: { type: "local" },
102
+ checksum: parsed.checksum,
103
+ updatedAt: metadata.updatedAt,
104
+ installs: [
105
+ {
106
+ scope: options.includeSystem ? "system" : "user",
107
+ agent: "unknown",
108
+ path: skill.skillDir,
109
+ },
110
+ ],
111
+ });
112
+ index.skills = next.skills;
113
+ imported.push(metadata.name);
114
+ }
115
+ await saveIndex(sortIndex(index));
116
+ return {
117
+ imported: imported.sort(),
118
+ skipped: skipped.sort(),
119
+ };
120
+ };
@@ -1,14 +1,17 @@
1
1
  import { isJsonEnabled, printInfo, printJson, printGroupList } from "../lib/output.js";
2
2
  import { loadIndex } from "../lib/index.js";
3
3
  import { groupNamesByKey } from "../lib/grouping.js";
4
+ import { discoverGlobalSkills } from "../lib/global-skills.js";
4
5
  export const registerList = (program) => {
5
6
  program
6
7
  .command("list")
7
8
  .option("--group <group>", "Group by category, namespace, source, project")
9
+ .option("--project-only", "Only show project-indexed skills")
8
10
  .option("--json", "JSON output")
9
11
  .action(async (options) => {
10
12
  const index = await loadIndex();
11
- const skills = index.skills;
13
+ const globalSkills = options.projectOnly ? [] : await listGlobalSkills(index.skills);
14
+ const skills = [...index.skills, ...globalSkills];
12
15
  const groupedProjects = groupByProject(skills);
13
16
  const groupedSources = groupBySource(skills);
14
17
  const groupedNamespaces = groupByNamespace(skills);
@@ -61,6 +64,21 @@ const groupByProject = (skills) => {
61
64
  .map((install) => install.projectRoot));
62
65
  return grouped.map((group) => ({ root: group.key, skills: group.skills }));
63
66
  };
67
+ const listGlobalSkills = async (existing) => {
68
+ const projectRoot = process.cwd();
69
+ const seen = new Set(existing.map((skill) => skill.name));
70
+ const discovered = await discoverGlobalSkills(projectRoot);
71
+ return discovered
72
+ .filter((skill) => !seen.has(skill.name))
73
+ .map((skill) => ({
74
+ name: skill.name,
75
+ source: { type: "local" },
76
+ installs: skill.installs,
77
+ namespace: undefined,
78
+ categories: undefined,
79
+ tags: undefined,
80
+ }));
81
+ };
64
82
  const groupBySource = (skills) => {
65
83
  const grouped = groupNamesByKey(skills, (skill) => skill.name, (skill) => [skill.source.type]);
66
84
  return grouped.map((group) => ({ source: group.key, skills: group.skills }));
@@ -16,9 +16,9 @@ export const registerStatus = (program) => {
16
16
  const config = await loadConfig();
17
17
  const results = [];
18
18
  for (const skill of index.skills) {
19
- const projects = (skill.installs ?? [])
19
+ const projects = Array.from(new Set((skill.installs ?? [])
20
20
  .filter((install) => install.scope === "project" && install.projectRoot)
21
- .map((install) => install.projectRoot);
21
+ .map((install) => install.projectRoot)));
22
22
  const allowSystem = config.manageSystem;
23
23
  const isSystem = (skill.installs ?? []).some((install) => install.scope === "system");
24
24
  if (isSystem && !allowSystem) {
@@ -0,0 +1,31 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ const home = os.homedir();
5
+ const agentRoots = {
6
+ opencode: [path.join(home, ".config", "opencode")],
7
+ claude: [path.join(home, ".claude")],
8
+ cursor: [path.join(home, ".cursor")],
9
+ codex: [path.join(home, ".codex")],
10
+ amp: [path.join(home, ".config", "agents")],
11
+ antigravity: [path.join(home, ".gemini", "antigravity")],
12
+ };
13
+ const exists = async (target) => {
14
+ try {
15
+ await fs.access(target);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ };
22
+ export const detectAgents = async () => {
23
+ const detected = [];
24
+ for (const [agent, roots] of Object.entries(agentRoots)) {
25
+ const matches = await Promise.all(roots.map((root) => exists(root)));
26
+ if (matches.some(Boolean)) {
27
+ detected.push(agent);
28
+ }
29
+ }
30
+ return detected;
31
+ };
@@ -38,6 +38,14 @@ export const agentPaths = (projectRoot) => ({
38
38
  },
39
39
  });
40
40
  export const allAgents = ["opencode", "claude", "cursor", "codex", "amp", "antigravity"];
41
+ export const getUserAgentPaths = (projectRoot) => {
42
+ const paths = agentPaths(projectRoot);
43
+ return Object.values(paths).flatMap((entry) => entry.user);
44
+ };
45
+ export const getSystemAgentPaths = (projectRoot) => {
46
+ const paths = agentPaths(projectRoot);
47
+ return Object.values(paths).flatMap((entry) => entry.system ?? []);
48
+ };
41
49
  const agentSet = new Set(allAgents);
42
50
  export const isAgentId = (value) => {
43
51
  return agentSet.has(value);
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ const exists = async (target) => {
4
+ try {
5
+ await fs.access(target);
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ };
12
+ export const discoverSkills = async (paths) => {
13
+ const results = [];
14
+ for (const root of paths) {
15
+ if (!(await exists(root))) {
16
+ continue;
17
+ }
18
+ const entries = await fs.readdir(root, { withFileTypes: true });
19
+ for (const entry of entries) {
20
+ if (!entry.isDirectory()) {
21
+ continue;
22
+ }
23
+ const skillDir = path.join(root, entry.name);
24
+ const skillFile = path.join(skillDir, "SKILL.md");
25
+ if (!(await exists(skillFile))) {
26
+ continue;
27
+ }
28
+ results.push({ name: entry.name, skillDir, skillFile });
29
+ }
30
+ }
31
+ return results;
32
+ };
@@ -0,0 +1,36 @@
1
+ import { getUserAgentPaths, getSystemAgentPaths } from "./agents.js";
2
+ import { discoverSkills } from "./discovery.js";
3
+ export const discoverGlobalSkills = async (projectRoot) => {
4
+ const userPaths = getUserAgentPaths(projectRoot);
5
+ const systemPaths = getSystemAgentPaths(projectRoot);
6
+ const [userSkills, systemSkills] = await Promise.all([
7
+ discoverSkills(userPaths),
8
+ discoverSkills(systemPaths),
9
+ ]);
10
+ const entries = [];
11
+ for (const skill of userSkills) {
12
+ entries.push({
13
+ name: skill.name,
14
+ installs: [
15
+ {
16
+ scope: "user",
17
+ agent: "unknown",
18
+ path: skill.skillDir,
19
+ },
20
+ ],
21
+ });
22
+ }
23
+ for (const skill of systemSkills) {
24
+ entries.push({
25
+ name: skill.name,
26
+ installs: [
27
+ {
28
+ scope: "system",
29
+ agent: "unknown",
30
+ path: skill.skillDir,
31
+ },
32
+ ],
33
+ });
34
+ }
35
+ return entries;
36
+ };
@@ -0,0 +1,39 @@
1
+ import readline from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { allAgents, isAgentId } from "./agents.js";
4
+ import { loadConfig, saveConfig } from "./config.js";
5
+ import { detectAgents } from "./agent-detect.js";
6
+ const formatPrompt = (detected) => {
7
+ if (detected.length === 0) {
8
+ return `Which agents do you use? (comma-separated) [${allAgents.join(", ")}]: `;
9
+ }
10
+ return `Detected agents: ${detected.join(", ")}. Press enter to accept or edit: `;
11
+ };
12
+ const promptAgents = async () => {
13
+ const detected = await detectAgents();
14
+ const rl = readline.createInterface({ input, output });
15
+ const answer = await rl.question(formatPrompt(detected));
16
+ rl.close();
17
+ const raw = answer.trim().length > 0 ? answer : detected.join(",");
18
+ const selected = raw
19
+ .split(",")
20
+ .map((agent) => agent.trim())
21
+ .filter((agent) => agent.length > 0)
22
+ .filter(isAgentId);
23
+ if (selected.length > 0) {
24
+ return selected;
25
+ }
26
+ return detected.length > 0 ? detected : allAgents;
27
+ };
28
+ export const runOnboarding = async () => {
29
+ const config = await loadConfig();
30
+ if (config.defaultAgents.length > 0) {
31
+ return;
32
+ }
33
+ const selected = await promptAgents();
34
+ const next = {
35
+ ...config,
36
+ defaultAgents: selected,
37
+ };
38
+ await saveConfig(next);
39
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillbox",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Local-first, agent-agnostic skills manager",
5
5
  "license": "MIT",
6
6
  "author": "Christian Anagnostou",