skillpm 0.0.4 → 0.0.5

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
@@ -21,12 +21,14 @@ npx skillpm list
21
21
  npx skillpm init
22
22
  ```
23
23
 
24
- Or install globally for convenience:
24
+ Or install the CLI globally:
25
25
 
26
26
  ```bash
27
27
  npm install -g skillpm
28
28
  ```
29
29
 
30
+ > **Note:** Skills are always workspace-local. This installs the `skillpm` CLI — not skills.
31
+
30
32
  ## How it works
31
33
 
32
34
  When you run `skillpm install <skill>`:
@@ -34,7 +36,8 @@ When you run `skillpm install <skill>`:
34
36
  1. **npm install** — npm handles resolution, download, lockfile, `node_modules/`
35
37
  2. **Scan** — skillpm scans `node_modules/` for packages containing `skills/*/SKILL.md`
36
38
  3. **Link** — for each skill found, skillpm calls [`skills`](https://www.npmjs.com/package/skills) to wire it into agent directories (Claude, Cursor, VS Code, Codex, and many more)
37
- 4. **MCP config** — skillpm collects `skillpm.mcpServers` from all skills (transitively) and configures each via [`add-mcp`](https://github.com/neondatabase/add-mcp)
39
+ 4. **Configs** — for each skill with a `configs/` directory, skillpm copies agent definitions, rules, and prompts into the workspace (auto-prefixed to avoid conflicts)
40
+ 5. **MCP config** — skillpm collects `skillpm.mcpServers` from all skills (transitively) and configures each via [`add-mcp`](https://github.com/neondatabase/add-mcp)
38
41
 
39
42
  That's it. Agents see the full skill tree with MCP servers configured.
40
43
 
@@ -49,6 +52,7 @@ skillpm doesn't reinvent anything. It orchestrates three battle-tested tools: np
49
52
  | Dependency management | Standard `package.json` `dependencies` — npm handles semver, lockfiles, audit |
50
53
  | Versioning | npm semver, `package-lock.json`, reproducible installs |
51
54
  | Agent wiring | Links skills into agent directories via [`skills`](https://www.npmjs.com/package/skills) |
55
+ | Config files | Copies agent definitions, rules, and prompts from `configs/` into the workspace |
52
56
  | MCP server config | Collects and configures MCP servers transitively via [`add-mcp`](https://github.com/neondatabase/add-mcp) |
53
57
 
54
58
  ## Commands
@@ -1,2 +1,2 @@
1
1
  export declare function install(args: string[], cwd: string): Promise<void>;
2
- export declare function wireSkills(scanRoot: string, wireTarget?: string): Promise<void>;
2
+ export declare function wireSkills(cwd: string): Promise<void>;
@@ -1,23 +1,13 @@
1
1
  import { npm, npx, log } from '../utils/index.js';
2
2
  import { scanNodeModules, collectMcpServers } from '../scanner/index.js';
3
- import { execFile } from 'node:child_process';
4
- import { promisify } from 'node:util';
5
- import { dirname } from 'node:path';
6
- import { homedir } from 'node:os';
7
- const execFileAsync = promisify(execFile);
8
- function isGlobalFlag(args) {
9
- return args.includes('-g') || args.includes('--global');
10
- }
11
- /**
12
- * Resolve the node_modules root to scan. For global installs, uses `npm root -g`.
13
- */
14
- async function resolveNodeModulesRoot(args, cwd) {
15
- if (!isGlobalFlag(args))
16
- return cwd;
17
- const { stdout } = await execFileAsync('npm', ['root', '-g']);
18
- return dirname(stdout.trim());
19
- }
3
+ import { copyConfigs } from '../configs/index.js';
20
4
  export async function install(args, cwd) {
5
+ // Reject global installs — skillpm is workspace-only
6
+ if (args.includes('-g') || args.includes('--global')) {
7
+ log.error('Global installs are not supported. skillpm works per-workspace with package.json and lockfiles.');
8
+ log.error('For global skills, use: npx skills add <path>');
9
+ process.exit(1);
10
+ }
21
11
  // Step 1: npm install
22
12
  const npmArgs = ['install', ...args];
23
13
  log.info(`Running npm ${npmArgs.join(' ')}`);
@@ -34,15 +24,11 @@ export async function install(args, cwd) {
34
24
  process.exit(1);
35
25
  }
36
26
  // Step 2: Scan for skills and wire them
37
- const scanRoot = await resolveNodeModulesRoot(args, cwd);
38
- // For global installs, wire skills into user's home directory (not the npm prefix)
39
- const wireTarget = isGlobalFlag(args) ? homedir() : cwd;
40
- await wireSkills(scanRoot, wireTarget);
27
+ await wireSkills(cwd);
41
28
  }
42
- export async function wireSkills(scanRoot, wireTarget) {
43
- const wireCwd = wireTarget ?? scanRoot;
29
+ export async function wireSkills(cwd) {
44
30
  // Scan node_modules/ for SKILL.md packages
45
- const skills = await scanNodeModules(scanRoot);
31
+ const skills = await scanNodeModules(cwd);
46
32
  if (skills.length === 0) {
47
33
  log.info('No skill packages found in node_modules/');
48
34
  return;
@@ -52,7 +38,7 @@ export async function wireSkills(scanRoot, wireTarget) {
52
38
  for (const skill of skills) {
53
39
  log.info(`Linking ${log.skill(skill.name, skill.version)} into agent directories`);
54
40
  try {
55
- await npx(['skills', 'add', skill.skillDir, '--all', '-y'], { cwd: wireCwd });
41
+ await npx(['skills', 'add', skill.skillDir, '--all', '-y'], { cwd });
56
42
  log.success(`Linked ${skill.name}`);
57
43
  }
58
44
  catch (err) {
@@ -70,7 +56,7 @@ export async function wireSkills(scanRoot, wireTarget) {
70
56
  for (const server of mcpServers) {
71
57
  log.info(`Configuring MCP server: ${server}`);
72
58
  try {
73
- await npx(['add-mcp', server, '-y'], { cwd: wireCwd });
59
+ await npx(['add-mcp', server, '-y'], { cwd });
74
60
  log.success(`Configured ${server}`);
75
61
  }
76
62
  catch (err) {
@@ -79,31 +65,17 @@ export async function wireSkills(scanRoot, wireTarget) {
79
65
  }
80
66
  }
81
67
  }
82
- // Wire agent definitions via add-agent
83
- for (const skill of skills) {
84
- for (const agentFile of skill.agents) {
85
- log.info(`Wiring agent from ${log.skill(skill.name, skill.version)}`);
86
- try {
87
- await npx(['add-agent', agentFile, '--package', skill.name], { cwd: wireCwd });
88
- log.success(`Wired agent ${agentFile}`);
89
- }
90
- catch (err) {
91
- const msg = err instanceof Error ? err.message : String(err);
92
- log.warn(`Failed to wire agent: ${msg}`);
93
- }
94
- }
95
- }
96
- // Wire prompts/instructions via add-prompt
68
+ // Copy configs/ files (agents, prompts, rules) into workspace
97
69
  for (const skill of skills) {
98
- for (const promptFile of skill.prompts) {
99
- log.info(`Wiring prompt from ${log.skill(skill.name, skill.version)}`);
70
+ if (skill.configsDir) {
71
+ log.info(`Copying config files from ${log.skill(skill.name, skill.version)}`);
100
72
  try {
101
- await npx(['add-prompt', promptFile, '--package', skill.name], { cwd: wireCwd });
102
- log.success(`Wired prompt ${promptFile}`);
73
+ const copied = await copyConfigs(skill.configsDir, cwd, skill.name);
74
+ log.success(`Copied ${copied.length} config file(s) from ${skill.name}`);
103
75
  }
104
76
  catch (err) {
105
77
  const msg = err instanceof Error ? err.message : String(err);
106
- log.warn(`Failed to wire prompt: ${msg}`);
78
+ log.warn(`Failed to copy config files from ${skill.name}: ${msg}`);
107
79
  }
108
80
  }
109
81
  }
@@ -1,23 +1,21 @@
1
- import { npm, npx, log } from '../utils/index.js';
1
+ import { npm, log } from '../utils/index.js';
2
2
  import { wireSkills } from './install.js';
3
+ import { removeConfigs } from '../configs/index.js';
3
4
  export async function uninstall(args, cwd) {
4
5
  if (args.length === 0) {
5
6
  log.error('Usage: skillpm uninstall <skill> [skill...]');
6
7
  process.exit(1);
7
8
  }
8
- // Clean up agents/prompts for removed packages before npm uninstall
9
+ // Clean up wired files before npm uninstall
9
10
  for (const pkg of args) {
10
11
  try {
11
- await npx(['add-agent', '--remove-package', pkg], { cwd });
12
+ const removed = await removeConfigs(cwd, pkg);
13
+ if (removed.length > 0) {
14
+ log.info(`Removed ${removed.length} config file(s) from ${pkg}`);
15
+ }
12
16
  }
13
17
  catch {
14
- // Ignore — package may not have had agents
15
- }
16
- try {
17
- await npx(['add-prompt', '--remove-package', pkg], { cwd });
18
- }
19
- catch {
20
- // Ignore — package may not have had prompts
18
+ // Ignore — package may not have had configs
21
19
  }
22
20
  }
23
21
  log.info(`Running npm uninstall ${args.join(' ')}`);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Copy all files from a skill's configs/ directory to the workspace,
3
+ * auto-prefixing filenames with the package name.
4
+ */
5
+ export declare function copyConfigs(configsDir: string, cwd: string, packageName: string): Promise<string[]>;
6
+ /**
7
+ * Remove all config files for a package using the manifest.
8
+ */
9
+ export declare function removeConfigs(cwd: string, packageName: string): Promise<string[]>;
@@ -0,0 +1,97 @@
1
+ import { readdir, copyFile, mkdir, unlink, readFile, writeFile, stat } from 'node:fs/promises';
2
+ import { join, relative, dirname, basename } from 'node:path';
3
+ const MANIFEST_DIR = '.skillpm';
4
+ const MANIFEST_FILE = 'manifest.json';
5
+ async function readManifest(cwd) {
6
+ try {
7
+ const raw = await readFile(join(cwd, MANIFEST_DIR, MANIFEST_FILE), 'utf-8');
8
+ return JSON.parse(raw);
9
+ }
10
+ catch {
11
+ return {};
12
+ }
13
+ }
14
+ async function writeManifest(cwd, manifest) {
15
+ const dir = join(cwd, MANIFEST_DIR);
16
+ await mkdir(dir, { recursive: true });
17
+ await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(manifest, null, 2) + '\n');
18
+ }
19
+ /**
20
+ * Recursively collect all files in a directory, returning paths relative to the root.
21
+ */
22
+ async function walkDir(dir, root) {
23
+ root ??= dir;
24
+ const files = [];
25
+ let entries;
26
+ try {
27
+ entries = await readdir(dir);
28
+ }
29
+ catch {
30
+ return files;
31
+ }
32
+ for (const name of entries) {
33
+ const full = join(dir, name);
34
+ const s = await stat(full);
35
+ if (s.isDirectory()) {
36
+ files.push(...(await walkDir(full, root)));
37
+ }
38
+ else {
39
+ files.push(relative(root, full));
40
+ }
41
+ }
42
+ return files;
43
+ }
44
+ /**
45
+ * Auto-prefix a filename with the package name to avoid conflicts.
46
+ * e.g. "reviewer.md" with package "my-skill" → "my-skill--reviewer.md"
47
+ */
48
+ function prefixFilename(relPath, packageName) {
49
+ const dir = dirname(relPath);
50
+ const file = basename(relPath);
51
+ const prefixed = `${packageName}--${file}`;
52
+ return dir === '.' ? prefixed : join(dir, prefixed);
53
+ }
54
+ /**
55
+ * Copy all files from a skill's configs/ directory to the workspace,
56
+ * auto-prefixing filenames with the package name.
57
+ */
58
+ export async function copyConfigs(configsDir, cwd, packageName) {
59
+ const files = await walkDir(configsDir);
60
+ const copied = [];
61
+ for (const relPath of files) {
62
+ const prefixed = prefixFilename(relPath, packageName);
63
+ const src = join(configsDir, relPath);
64
+ const dest = join(cwd, prefixed);
65
+ await mkdir(dirname(dest), { recursive: true });
66
+ await copyFile(src, dest);
67
+ copied.push(prefixed);
68
+ }
69
+ // Update manifest
70
+ const manifest = await readManifest(cwd);
71
+ manifest[packageName] = copied;
72
+ await writeManifest(cwd, manifest);
73
+ return copied;
74
+ }
75
+ /**
76
+ * Remove all config files for a package using the manifest.
77
+ */
78
+ export async function removeConfigs(cwd, packageName) {
79
+ const manifest = await readManifest(cwd);
80
+ const files = manifest[packageName];
81
+ if (!files || files.length === 0)
82
+ return [];
83
+ const removed = [];
84
+ for (const relPath of files) {
85
+ try {
86
+ await unlink(join(cwd, relPath));
87
+ removed.push(relPath);
88
+ }
89
+ catch {
90
+ // File already gone
91
+ }
92
+ }
93
+ delete manifest[packageName];
94
+ await writeManifest(cwd, manifest);
95
+ return removed;
96
+ }
97
+ //# sourceMappingURL=index.js.map
@@ -19,8 +19,6 @@ export interface SkillInfo {
19
19
  mcpServers: string[];
20
20
  /** True if SKILL.md is at package root instead of skills/<name>/ */
21
21
  legacy?: boolean;
22
- /** Paths to agent definition .md files in agents/ */
23
- agents: string[];
24
- /** Paths to prompt/instruction .md files in prompts/ */
25
- prompts: string[];
22
+ /** Path to configs/ directory if present (mirrors workspace layout) */
23
+ configsDir?: string;
26
24
  }
@@ -1,19 +1,6 @@
1
1
  import { readdir, access } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { readPackageJson, parseSkillpmField } from '../manifest/index.js';
4
- /**
5
- * Scan a directory for .md files (used for agents/ and prompts/ dirs).
6
- */
7
- async function scanMdFiles(dir) {
8
- let entries;
9
- try {
10
- entries = await readdir(dir);
11
- }
12
- catch {
13
- return [];
14
- }
15
- return entries.filter((e) => e.endsWith('.md')).map((e) => join(dir, e));
16
- }
17
4
  /**
18
5
  * Scan node_modules/ for packages that contain a skills/<name>/SKILL.md file.
19
6
  * Returns metadata for each discovered skill package.
@@ -57,10 +44,22 @@ export async function scanNodeModules(cwd) {
57
44
  }
58
45
  return skills;
59
46
  }
47
+ async function hasDir(dir) {
48
+ try {
49
+ await access(dir);
50
+ return true;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
60
56
  async function tryReadSkill(pkgDir) {
61
57
  const pkg = await readPackageJson(pkgDir);
62
58
  if (!pkg)
63
59
  return null;
60
+ // Check for configs/ directory
61
+ const configsDir = join(pkgDir, 'configs');
62
+ const hasConfigs = await hasDir(configsDir);
64
63
  // Preferred: look for skills/*/SKILL.md
65
64
  const skillsDir = join(pkgDir, 'skills');
66
65
  let skillSubdirs;
@@ -79,16 +78,13 @@ async function tryReadSkill(pkgDir) {
79
78
  continue;
80
79
  }
81
80
  const skillpm = parseSkillpmField(pkg);
82
- const agents = await scanMdFiles(join(pkgDir, 'agents'));
83
- const prompts = await scanMdFiles(join(pkgDir, 'prompts'));
84
81
  return {
85
82
  name: pkg.name,
86
83
  version: pkg.version,
87
84
  path: pkgDir,
88
85
  skillDir,
89
86
  mcpServers: skillpm?.mcpServers ?? [],
90
- agents,
91
- prompts,
87
+ configsDir: hasConfigs ? configsDir : undefined,
92
88
  };
93
89
  }
94
90
  // Fallback: root SKILL.md (legacy format)
@@ -99,8 +95,6 @@ async function tryReadSkill(pkgDir) {
99
95
  return null;
100
96
  }
101
97
  const skillpm = parseSkillpmField(pkg);
102
- const agents = await scanMdFiles(join(pkgDir, 'agents'));
103
- const prompts = await scanMdFiles(join(pkgDir, 'prompts'));
104
98
  return {
105
99
  name: pkg.name,
106
100
  version: pkg.version,
@@ -108,8 +102,7 @@ async function tryReadSkill(pkgDir) {
108
102
  skillDir: pkgDir,
109
103
  mcpServers: skillpm?.mcpServers ?? [],
110
104
  legacy: true,
111
- agents,
112
- prompts,
105
+ configsDir: hasConfigs ? configsDir : undefined,
113
106
  };
114
107
  }
115
108
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillpm",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Package manager for Agent Skills. Built on npm.",
5
5
  "type": "module",
6
6
  "bin": {