skillbox 0.1.1 → 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
@@ -1,6 +1,9 @@
1
1
  # skillbox
2
2
 
3
- Local-first, agent-agnostic skills manager. Track, update, and sync skills across popular AI coding agents with one CLI.
3
+ > Local-first, agent-agnostic skills manager. Track, update, and sync skills across popular AI coding agents with one CLI.
4
+
5
+ [![CI](https://github.com/christiananagnostou/skillbox/actions/workflows/ci.yml/badge.svg)](https://github.com/christiananagnostou/skillbox/actions/workflows/ci.yml)
6
+ [![npm version](https://img.shields.io/npm/v/skillbox.svg)](https://www.npmjs.com/package/skillbox)
4
7
 
5
8
  ## Installation
6
9
 
@@ -10,38 +13,13 @@ Local-first, agent-agnostic skills manager. Track, update, and sync skills acros
10
13
  npm install -g skillbox
11
14
  ```
12
15
 
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
16
 
34
- ## CI
35
17
 
36
- ```bash
37
- npm run lint:ci
38
- npm run format:check
39
- npm run build
40
- ```
18
+ ## Quick Start
41
19
 
42
- TODO: Publish GitHub Actions badge
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.
43
21
 
44
- ## Quick Start
22
+ Tip: run `skillbox list` right after install to see existing skills.
45
23
 
46
24
  ```bash
47
25
  skillbox add https://example.com/skills/linting/SKILL.md
@@ -80,10 +58,15 @@ skillbox project sync <path>
80
58
  skillbox config get
81
59
  skillbox config set --default-scope user
82
60
  skillbox config set --default-agent claude --default-agent cursor
61
+ skillbox config set --add-agent codex
83
62
  skillbox config set --manage-system
84
63
  ```
85
64
 
86
- 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
87
70
 
88
71
  ## Agent Mode
89
72
 
@@ -135,7 +118,7 @@ Agent paths (default):
135
118
  - Amp: `.agents/skills/`, `~/.config/agents/skills/` (Claude-compatible `.claude/skills/` also supported)
136
119
  - Antigravity: `.agent/skills/`, `~/.gemini/antigravity/skills/`
137
120
 
138
- TODO: Validate agent path list against upstream docs before release
121
+ Note: only Codex defines a system scope path.
139
122
 
140
123
  ## Usage with AI Agents
141
124
 
@@ -163,9 +146,15 @@ Core workflow:
163
146
  3. `skillbox update <name> --json`
164
147
  ```
165
148
 
166
- ### Claude Code Skill
149
+ ## Development
167
150
 
168
- 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
+ ```
169
158
 
170
159
  ## License
171
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
+ };
@@ -1,4 +1,3 @@
1
- import fetch from "node-fetch";
2
1
  export const fetchText = async (url) => {
3
2
  const response = await fetch(url);
4
3
  if (!response.ok) {
@@ -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.1",
3
+ "version": "0.2.0",
4
4
  "description": "Local-first, agent-agnostic skills manager",
5
5
  "license": "MIT",
6
6
  "author": "Christian Anagnostou",
@@ -38,7 +38,6 @@
38
38
  "dependencies": {
39
39
  "chalk": "^5.3.0",
40
40
  "commander": "^12.0.0",
41
- "node-fetch": "^3.3.2",
42
41
  "zod": "^3.23.8"
43
42
  },
44
43
  "devDependencies": {