@almyty/skills 1.0.6 → 1.0.9

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
@@ -35,6 +35,64 @@ $ npx @almyty/skills install @acme/petstore
35
35
  $ npx @almyty/skills run @acme/petstore/get-pet --id 123
36
36
  ```
37
37
 
38
+ ## Where skills get installed
39
+
40
+ `install` writes a `SKILL.md` file per skill into one or more agent
41
+ directories. The CLI detects agents at two scopes:
42
+
43
+ - **Project scope** — a config dir exists in the current project
44
+ (e.g. `./.codex/`). Skills install to `./.codex/skills/`, only
45
+ this checkout sees them.
46
+ - **Home scope** — a config dir exists in your home directory
47
+ (e.g. `~/.codex/`). Skills install to `~/.codex/skills/`, every
48
+ project the agent opens picks them up.
49
+
50
+ Default behavior:
51
+
52
+ - **Interactive (TTY, no flags):** the picker lists every detected
53
+ agent at both scopes (each labeled `(project)` or `(home)`),
54
+ every other supported agent as opt-in, the universal
55
+ `.agents/skills/` convention, and a custom-path option. Pick any
56
+ combination.
57
+ - **`--yes` or non-TTY:** project-detected agents + `.agents/skills/`.
58
+ Home-detected agents are NOT installed automatically — pass
59
+ `--global` to opt in.
60
+ - **`--global` alone:** every home-detected agent. No project install.
61
+ - **`--all`:** every project-detected agent + universal. Combine
62
+ with `--global` to also include home-detected.
63
+ - **`--agent <name>`:** install to a specific agent. Picks the
64
+ detected scope (project preferred). With `--global`, prefers
65
+ home. If neither is detected, creates the project-scope dir
66
+ (the agent will pick it up on next scan).
67
+
68
+ | Flag | Meaning |
69
+ |------|---------|
70
+ | `--agent <name>`, `-a` | Install to the named agent. Repeatable. Partial-match. |
71
+ | `--agent '*'` | Every known agent at project scope, regardless of detection. |
72
+ | `--path <dir>`, `-p` | Custom skills directory. Repeatable. Bypasses detection. |
73
+ | `--all` | Every project-detected agent + `.agents/skills/`. |
74
+ | `--global`, `-G` | Use home scope (`~/.<agent>/skills/`). Modifier on `--agent`, or standalone for "every home-detected". |
75
+ | `--yes`, `-y` | Skip the picker; use the non-interactive defaults. |
76
+
77
+ Examples:
78
+
79
+ ```bash
80
+ $ npx @almyty/skills install @acme/petstore # interactive picker
81
+ $ npx @almyty/skills install @acme/petstore --all # every project-detected
82
+ $ npx @almyty/skills install @acme/petstore --all --global # project AND home detected
83
+ $ npx @almyty/skills install @acme/petstore --global # only home-detected agents
84
+ $ npx @almyty/skills install @acme/petstore -a codex # codex at whichever scope it lives
85
+ $ npx @almyty/skills install @acme/petstore -a codex --global # force codex at ~/.codex/skills
86
+ $ npx @almyty/skills install @acme/petstore --agent '*' -y # every known agent at project
87
+ $ npx @almyty/skills install @acme/petstore -p ./agents/skills # custom directory
88
+ ```
89
+
90
+ The 25+ supported agents include Claude Code, Codex, Cursor, Windsurf,
91
+ GitHub Copilot, Gemini CLI, Amp, Cline, Continue, Goose, Junie, Roo
92
+ Code, Trae, OpenHands, OpenCode, Augment, and others. See
93
+ `src/agents.ts` for the full registry — each entry maps a detection
94
+ directory to the `<dir>/skills` path that agent reads on session start.
95
+
38
96
  ## Configuration
39
97
 
40
98
  Create `.almytyrc` in your project or home directory:
package/dist/agents.d.ts CHANGED
@@ -7,15 +7,49 @@
7
7
  *
8
8
  * Agent list aligned with https://github.com/vercel-labs/skills
9
9
  */
10
+ /**
11
+ * Where an install ends up. `project` means the skill is bound to
12
+ * this directory's `.x/skills/` so only this checkout sees it;
13
+ * `home` means it's installed at the user's home-scope dir
14
+ * (e.g. `~/.codex/skills/`) and every project shares it. `custom`
15
+ * means the caller specified a `--path` and we don't track scope.
16
+ */
17
+ export type InstallScope = 'project' | 'home' | 'custom';
10
18
  export interface AgentTarget {
11
19
  name: string;
12
20
  configDir: string;
13
21
  skillsDir: string;
22
+ scope?: InstallScope;
14
23
  }
24
+ /**
25
+ * Known agent configurations (30+ agents).
26
+ * Aligned with Vercel's skills CLI.
27
+ * See: https://github.com/vercel-labs/skills/blob/main/src/agents.ts
28
+ */
29
+ export declare const AGENT_CONFIGS: Array<{
30
+ name: string;
31
+ detectDirs: string[];
32
+ skillsDir: string;
33
+ }>;
15
34
  /**
16
35
  * Detect which AI agents are configured in the given project directory.
36
+ * Project-scope only — for home-scope detection see detectHomeAgents.
17
37
  */
18
38
  export declare function detectAgents(projectDir: string): AgentTarget[];
39
+ /**
40
+ * Detect agents the user has installed globally (home-scope) — i.e.
41
+ * `~/.codex/`, `~/.claude/`, etc. exist regardless of cwd. The
42
+ * skillsDir returned is the home-scope path (`~/.codex/skills/`),
43
+ * which Codex and Claude Code both read for cross-project skills.
44
+ *
45
+ * Mirrors the project-scope detection: same detectDirs but anchored
46
+ * at $HOME instead of the project root. The shared `.agents/skills/`
47
+ * universal convention is intentionally NOT emitted here — that
48
+ * convention is repo-local (it lives next to the project's source
49
+ * so the team sees it under version control); if a user wants
50
+ * universal installs they can use --path explicitly.
51
+ */
52
+ export declare function detectHomeAgents(home?: string): AgentTarget[];
19
53
  /**
20
54
  * If no agents are detected, return defaults:
21
55
  * - .claude/skills/ (Claude Code native)
package/dist/agents.js CHANGED
@@ -8,19 +8,26 @@
8
8
  * Agent list aligned with https://github.com/vercel-labs/skills
9
9
  */
10
10
  import { existsSync, mkdirSync } from 'fs';
11
+ import { homedir } from 'os';
11
12
  import { join } from 'path';
12
13
  /**
13
14
  * Known agent configurations (30+ agents).
14
15
  * Aligned with Vercel's skills CLI.
15
16
  * See: https://github.com/vercel-labs/skills/blob/main/src/agents.ts
16
17
  */
17
- const AGENT_CONFIGS = [
18
+ export const AGENT_CONFIGS = [
18
19
  // --- Major agents ---
19
20
  { name: 'Claude Code', detectDirs: ['.claude'], skillsDir: '.claude/skills' },
20
21
  { name: 'Cursor', detectDirs: ['.cursor', '.cursorrc'], skillsDir: '.agents/skills' },
21
22
  { name: 'GitHub Copilot', detectDirs: ['.github/copilot'], skillsDir: '.agents/skills' },
22
23
  { name: 'Windsurf', detectDirs: ['.windsurf'], skillsDir: '.windsurf/skills' },
23
- { name: 'Codex', detectDirs: ['.codex'], skillsDir: '.agents/skills' },
24
+ // Codex reads skills from `.codex/skills/<skill>/SKILL.md` for repo-
25
+ // local skills (and `$CODEX_HOME/skills/...`, default ~/.codex/skills,
26
+ // for user-scoped). The agentskills.io universal `.agents/skills/`
27
+ // path is NOT consulted by Codex, so installs targeted at `.agents/`
28
+ // never appeared in the running agent. Source: Codex curated skill
29
+ // docs (~/.codex/vendor_imports/skills/skills/.curated/*/SKILL.md).
30
+ { name: 'Codex', detectDirs: ['.codex'], skillsDir: '.codex/skills' },
24
31
  // --- Additional agents ---
25
32
  { name: 'Amp', detectDirs: ['.amp'], skillsDir: '.agents/skills' },
26
33
  { name: 'Augment', detectDirs: ['.augment'], skillsDir: '.augment/skills' },
@@ -50,6 +57,7 @@ const AGENT_CONFIGS = [
50
57
  ];
51
58
  /**
52
59
  * Detect which AI agents are configured in the given project directory.
60
+ * Project-scope only — for home-scope detection see detectHomeAgents.
53
61
  */
54
62
  export function detectAgents(projectDir) {
55
63
  const agents = [];
@@ -65,12 +73,51 @@ export function detectAgents(projectDir) {
65
73
  name: config.name,
66
74
  configDir: config.detectDirs[0],
67
75
  skillsDir: fullSkillsDir,
76
+ scope: 'project',
68
77
  });
69
78
  }
70
79
  }
71
80
  }
72
81
  return agents;
73
82
  }
83
+ /**
84
+ * Detect agents the user has installed globally (home-scope) — i.e.
85
+ * `~/.codex/`, `~/.claude/`, etc. exist regardless of cwd. The
86
+ * skillsDir returned is the home-scope path (`~/.codex/skills/`),
87
+ * which Codex and Claude Code both read for cross-project skills.
88
+ *
89
+ * Mirrors the project-scope detection: same detectDirs but anchored
90
+ * at $HOME instead of the project root. The shared `.agents/skills/`
91
+ * universal convention is intentionally NOT emitted here — that
92
+ * convention is repo-local (it lives next to the project's source
93
+ * so the team sees it under version control); if a user wants
94
+ * universal installs they can use --path explicitly.
95
+ */
96
+ export function detectHomeAgents(home = homedir()) {
97
+ const agents = [];
98
+ const seenSkillsDirs = new Set();
99
+ for (const config of AGENT_CONFIGS) {
100
+ const detected = config.detectDirs.some(dir => existsSync(join(home, dir)));
101
+ if (!detected)
102
+ continue;
103
+ // Home-scope install path: replace the project-prefixed
104
+ // skillsDir with a home-prefixed equivalent. Most agents store
105
+ // skills under <configDir>/skills, so just join home with the
106
+ // skillsDir from AGENT_CONFIGS — that's already the right
107
+ // structure (e.g., `.codex/skills` → `<home>/.codex/skills`).
108
+ const fullSkillsDir = join(home, config.skillsDir);
109
+ if (seenSkillsDirs.has(fullSkillsDir))
110
+ continue;
111
+ seenSkillsDirs.add(fullSkillsDir);
112
+ agents.push({
113
+ name: config.name,
114
+ configDir: config.detectDirs[0],
115
+ skillsDir: fullSkillsDir,
116
+ scope: 'home',
117
+ });
118
+ }
119
+ return agents;
120
+ }
74
121
  /**
75
122
  * If no agents are detected, return defaults:
76
123
  * - .claude/skills/ (Claude Code native)
package/dist/auth.d.ts CHANGED
@@ -1,20 +1,6 @@
1
1
  /**
2
- * Credential resolver for @almyty/skills.
3
- *
4
- * Authentication itself lives in the dedicated @almyty/auth package
5
- * (`npx @almyty/auth login`). This module just READS the shared
6
- * credentials store and surfaces them to the rest of the skills CLI.
7
- *
8
- * Lookup order:
9
- * 1. ALMYTY_TOKEN environment variable (CI / scripts)
10
- * 2. ~/.almyty/credentials.json (interactive use, written
11
- * by `npx @almyty/auth login`)
2
+ * Thin re-export from the shared @almyty/client credential resolver.
3
+ * Keeps `./auth.js` imports working across the skills-cli codebase.
12
4
  */
13
- /**
14
- * Resolve token: env var > stored credentials. Exits with a clear hint
15
- * if no credentials are available.
16
- */
17
- export declare function resolveAuth(): {
18
- url: string;
19
- token: string;
20
- };
5
+ export { resolveCredentials, resolveCredentialsOrExit, resolveCredentialsOrExit as resolveAuth, loadCredentials, CREDENTIALS_FILE, } from '@almyty/client';
6
+ export type { StoredCredentials } from '@almyty/client';
package/dist/auth.js CHANGED
@@ -1,57 +1,5 @@
1
1
  /**
2
- * Credential resolver for @almyty/skills.
3
- *
4
- * Authentication itself lives in the dedicated @almyty/auth package
5
- * (`npx @almyty/auth login`). This module just READS the shared
6
- * credentials store and surfaces them to the rest of the skills CLI.
7
- *
8
- * Lookup order:
9
- * 1. ALMYTY_TOKEN environment variable (CI / scripts)
10
- * 2. ~/.almyty/credentials.json (interactive use, written
11
- * by `npx @almyty/auth login`)
2
+ * Thin re-export from the shared @almyty/client credential resolver.
3
+ * Keeps `./auth.js` imports working across the skills-cli codebase.
12
4
  */
13
- import { readFileSync, existsSync } from 'fs';
14
- import { homedir } from 'os';
15
- import { join } from 'path';
16
- const CREDENTIALS_FILE = join(homedir(), '.almyty', 'credentials.json');
17
- function loadCredentials() {
18
- try {
19
- if (!existsSync(CREDENTIALS_FILE))
20
- return null;
21
- return JSON.parse(readFileSync(CREDENTIALS_FILE, 'utf-8'));
22
- }
23
- catch {
24
- return null;
25
- }
26
- }
27
- /**
28
- * Resolve token: env var > stored credentials. Exits with a clear hint
29
- * if no credentials are available.
30
- */
31
- export function resolveAuth() {
32
- const envToken = process.env.ALMYTY_TOKEN;
33
- // Read URL from: env > config file > default
34
- let configUrl;
35
- try {
36
- const { readFileSync, existsSync } = require('fs');
37
- const { join } = require('path');
38
- const { homedir } = require('os');
39
- const configPath = join(homedir(), '.almyty', 'config.json');
40
- if (existsSync(configPath)) {
41
- configUrl = JSON.parse(readFileSync(configPath, 'utf-8')).apiUrl;
42
- }
43
- }
44
- catch { }
45
- const envUrl = process.env.ALMYTY_URL || configUrl || 'https://api.almyty.com';
46
- if (envToken) {
47
- return { url: envUrl, token: envToken };
48
- }
49
- const creds = loadCredentials();
50
- if (creds?.token) {
51
- return { url: creds.url, token: creds.token };
52
- }
53
- console.error('Not authenticated. Run one of:');
54
- console.error(' npx @almyty/auth login # browser-based login');
55
- console.error(' export ALMYTY_TOKEN=<token> # for CI');
56
- process.exit(1);
57
- }
5
+ export { resolveCredentials, resolveCredentialsOrExit, resolveCredentialsOrExit as resolveAuth, loadCredentials, CREDENTIALS_FILE, } from '@almyty/client';
package/dist/index.js CHANGED
@@ -5,7 +5,8 @@ import { getAllTargets } from './agents.js';
5
5
  import { installSkills, removeSkills, listInstalledSkills } from './installer.js';
6
6
  import { loadConfig, resolveTargets } from './config.js';
7
7
  import { generateMetaSkill } from './meta-skill.js';
8
- const VERSION = '1.0.0';
8
+ import { selectInstallTargetsAuto, selectInstallTargetsInteractive, } from './target-selector.js';
9
+ const VERSION = '1.0.9';
9
10
  function printHelp() {
10
11
  console.log(`
11
12
  almyty Skills CLI v${VERSION}
@@ -40,19 +41,65 @@ Config:
40
41
  ALMYTY_TOKEN Override auth token
41
42
 
42
43
  Options:
44
+ --agent, -a <name> Install target by name (repeatable; e.g.
45
+ -a codex -a claude). Accepts '*' or 'all'
46
+ for every known agent regardless of
47
+ detection. Partial match.
48
+ --path, -p <dir> Custom skills dir, repeatable. Bypasses
49
+ agent detection.
50
+ --all Install to every PROJECT-detected agent
51
+ (plus the universal .agents/skills/).
52
+ Combine with --global to also include
53
+ home-detected. Skips the picker.
54
+ --global, -G Use home-scope (~/.codex/skills/, etc.)
55
+ instead of project-scope. With --all,
56
+ includes home-detected agents alongside
57
+ project-detected. With --agent <name>,
58
+ prefers home-scope when the agent is
59
+ detected at $HOME.
60
+ --yes, -y Skip the interactive picker. Falls back
61
+ to detected agents (or defaults).
43
62
  --interval, -i <seconds> Daemon poll interval in seconds (default: 60)
44
63
  --url <url> almyty API URL (default: https://api.almyty.com)
45
64
  --dir <path> Project directory (default: current directory)
46
65
  --help, -h Show help
47
66
  --version, -v Show version
48
67
 
68
+ Target selection (install):
69
+ Without flags in a TTY, install shows a multi-select picker of all
70
+ known agents (detected ones pre-checked) plus a "custom path…" option.
71
+ In a non-TTY, the picker is skipped: install writes to detected agents
72
+ and the universal .agents/skills/ directory.
73
+
49
74
  Examples:
50
75
  npx @almyty/skills daemon
51
76
  npx @almyty/skills install myorg/petstore/get-pet
77
+ npx @almyty/skills install @org/gw -a codex -a claude
78
+ npx @almyty/skills install @org/gw --path ./tmp/skills --yes
79
+ npx @almyty/skills install @org/gw --all
52
80
  npx @almyty/skills search "weather"
53
81
  npx @almyty/skills run myorg/petstore/get-pet --petId 123
54
82
  `);
55
83
  }
84
+ /**
85
+ * Repeatable flags accumulate into a string[] when supplied more
86
+ * than once. Used by `--agent` and `--path` so callers can pick
87
+ * multiple targets without inventing comma syntax (the selector
88
+ * also splits on comma/space, so both styles work).
89
+ */
90
+ const REPEATABLE_FLAGS = new Set(['agent', 'path']);
91
+ function appendRepeatable(flags, key, value) {
92
+ const existing = flags[key];
93
+ if (existing === undefined || existing === true || existing === false) {
94
+ flags[key] = value;
95
+ }
96
+ else if (typeof existing === 'string') {
97
+ flags[key] = [existing, value];
98
+ }
99
+ else {
100
+ existing.push(value);
101
+ }
102
+ }
56
103
  function parseArgs(argv) {
57
104
  const result = { positional: [], flags: {} };
58
105
  let i = 0;
@@ -70,6 +117,23 @@ function parseArgs(argv) {
70
117
  else if (arg === '--interval' || arg === '-i') {
71
118
  result.flags.interval = argv[++i] || '60';
72
119
  }
120
+ else if (arg === '--agent' || arg === '-a') {
121
+ appendRepeatable(result.flags, 'agent', argv[++i] || '');
122
+ }
123
+ else if (arg === '--path' || arg === '-p') {
124
+ appendRepeatable(result.flags, 'path', argv[++i] || '');
125
+ }
126
+ else if (arg === '--all') {
127
+ result.flags.all = true;
128
+ }
129
+ else if (arg === '--yes' || arg === '-y') {
130
+ result.flags.yes = true;
131
+ }
132
+ else if (arg === '--global' || arg === '-G') {
133
+ // --global / -G installs at home scope (~/.codex/skills/, etc.)
134
+ // -g remains aliased to --gateway for back-compat.
135
+ result.flags.global = true;
136
+ }
73
137
  else if (arg === '--help' || arg === '-h') {
74
138
  result.flags.help = true;
75
139
  }
@@ -83,7 +147,12 @@ function parseArgs(argv) {
83
147
  const key = arg.slice(2);
84
148
  const next = argv[i + 1];
85
149
  if (next && !next.startsWith('--')) {
86
- result.flags[key] = next;
150
+ if (REPEATABLE_FLAGS.has(key)) {
151
+ appendRepeatable(result.flags, key, next);
152
+ }
153
+ else {
154
+ result.flags[key] = next;
155
+ }
87
156
  i++;
88
157
  }
89
158
  else {
@@ -121,9 +190,12 @@ function requireRef(args, command) {
121
190
  }
122
191
  function parseRunParams(args) {
123
192
  const params = {};
124
- const entries = Object.entries(args.flags);
125
- for (const [key, value] of entries) {
126
- if (['url', 'dir', 'help', 'version', 'interval', 'gateway'].includes(key))
193
+ const reserved = new Set([
194
+ 'url', 'dir', 'help', 'version', 'interval', 'gateway',
195
+ 'agent', 'path', 'all', 'yes', 'global',
196
+ ]);
197
+ for (const [key, value] of Object.entries(args.flags)) {
198
+ if (reserved.has(key))
127
199
  continue;
128
200
  params[key] = value;
129
201
  }
@@ -311,7 +383,24 @@ async function main() {
311
383
  return;
312
384
  }
313
385
  console.log(`\n${gwName} (${skills.length} skill(s))`);
314
- const targets = resolveTargets(projectDir, config);
386
+ const selection = {
387
+ projectDir,
388
+ config,
389
+ agentFlag: args.flags.agent,
390
+ pathFlag: args.flags.path,
391
+ all: !!args.flags.all,
392
+ yes: !!args.flags.yes,
393
+ global: !!args.flags.global,
394
+ };
395
+ let targets = selectInstallTargetsAuto(selection);
396
+ if (targets === null) {
397
+ // Interactive picker — TTY + no flags + no .almytyrc.
398
+ targets = await selectInstallTargetsInteractive(selection);
399
+ }
400
+ if (targets.length === 0) {
401
+ console.error('No install targets resolved. Pass --agent / --path / --all or run without flags in a TTY.');
402
+ process.exit(1);
403
+ }
315
404
  const results = targets.map(target => installSkills(skills, target));
316
405
  console.log('');
317
406
  for (const result of results) {
@@ -1,5 +1,13 @@
1
1
  /**
2
2
  * Skill installer — writes SKILL.md files to agent directories.
3
+ *
4
+ * Naming: the skill's own name is used directly for both the
5
+ * directory and the SKILL.md frontmatter `name:` field. We used to
6
+ * prefix `almyty-` to flag installs as ours; that turned the agent-
7
+ * visible label into `$almyty-open-meteo-weather-get-v1-forecast`,
8
+ * which is noisy and uninformative. Identification for `remove`
9
+ * and `installed` now reads the `metadata.author: almyty` line
10
+ * already present in every SKILL.md we generate.
3
11
  */
4
12
  import type { SkillFile } from './client.js';
5
13
  import type { AgentTarget } from './agents.js';
@@ -11,14 +19,10 @@ export interface InstallResult {
11
19
  }
12
20
  /**
13
21
  * Install skill files into an agent's skills directory.
14
- * Each skill gets its own directory: <skillsDir>/almyty-<name>/SKILL.md
22
+ * Each skill lives at `<skillsDir>/<skill-name>/SKILL.md`.
15
23
  */
16
24
  export declare function installSkills(skills: SkillFile[], target: AgentTarget): InstallResult;
17
- /**
18
- * Remove all almyty-installed skills from an agent's skills directory.
19
- */
25
+ /** Remove every almyty-installed skill from an agent's skills directory. */
20
26
  export declare function removeSkills(target: AgentTarget): number;
21
- /**
22
- * List installed almyty skills in an agent's skills directory.
23
- */
27
+ /** List almyty-installed skills in an agent's skills directory. */
24
28
  export declare function listInstalledSkills(target: AgentTarget): string[];
package/dist/installer.js CHANGED
@@ -1,22 +1,68 @@
1
1
  /**
2
2
  * Skill installer — writes SKILL.md files to agent directories.
3
+ *
4
+ * Naming: the skill's own name is used directly for both the
5
+ * directory and the SKILL.md frontmatter `name:` field. We used to
6
+ * prefix `almyty-` to flag installs as ours; that turned the agent-
7
+ * visible label into `$almyty-open-meteo-weather-get-v1-forecast`,
8
+ * which is noisy and uninformative. Identification for `remove`
9
+ * and `installed` now reads the `metadata.author: almyty` line
10
+ * already present in every SKILL.md we generate.
3
11
  */
4
- import { writeFileSync, mkdirSync, existsSync, readdirSync, rmSync } from 'fs';
12
+ import { writeFileSync, mkdirSync, existsSync, readdirSync, rmSync, readFileSync } from 'fs';
5
13
  import { join } from 'path';
6
- const SKILL_PREFIX = 'almyty-';
14
+ /** Legacy prefix from <=v1.0.9 — older installs still use this dir
15
+ * name. Kept here so `remove`/`installed` continue to find them. */
16
+ const LEGACY_SKILL_PREFIX = 'almyty-';
17
+ /** Marker line we expect inside every SKILL.md frontmatter we wrote. */
18
+ const ALMYTY_MARKER = /^\s*author:\s*almyty\s*$/m;
19
+ /**
20
+ * Strip the legacy `name: almyty-<x>` line in the SKILL.md frontmatter
21
+ * to `name: <x>` so the agent-visible identifier is the skill's own
22
+ * slug. Backend currently writes the prefixed form; once that's
23
+ * cleaned up upstream this becomes a no-op.
24
+ */
25
+ function rewriteNameInFrontmatter(content) {
26
+ return content.replace(/^name:\s*almyty-/m, 'name: ');
27
+ }
28
+ /**
29
+ * Read SKILL.md and decide whether it was installed by this CLI
30
+ * (or its predecessor). Two signals: the legacy `almyty-` directory
31
+ * prefix, OR the `metadata.author: almyty` line in the frontmatter.
32
+ */
33
+ function isAlmytyInstall(skillsDir, dirName) {
34
+ if (dirName.startsWith(LEGACY_SKILL_PREFIX))
35
+ return true;
36
+ const skillFile = join(skillsDir, dirName, 'SKILL.md');
37
+ if (!existsSync(skillFile))
38
+ return false;
39
+ try {
40
+ return ALMYTY_MARKER.test(readFileSync(skillFile, 'utf-8'));
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
7
46
  /**
8
47
  * Install skill files into an agent's skills directory.
9
- * Each skill gets its own directory: <skillsDir>/almyty-<name>/SKILL.md
48
+ * Each skill lives at `<skillsDir>/<skill-name>/SKILL.md`.
10
49
  */
11
50
  export function installSkills(skills, target) {
12
51
  const files = [];
13
52
  mkdirSync(target.skillsDir, { recursive: true });
14
53
  for (const skill of skills) {
15
- const dirName = `${SKILL_PREFIX}${skill.name}`;
54
+ const dirName = skill.name;
16
55
  const skillDir = join(target.skillsDir, dirName);
17
56
  const skillFile = join(skillDir, 'SKILL.md');
57
+ // Cleanup: if a previous install used the `almyty-<name>` dir
58
+ // shape, remove it before writing the new shape so the agent
59
+ // doesn't see two copies of the same skill.
60
+ const legacyDir = join(target.skillsDir, `${LEGACY_SKILL_PREFIX}${skill.name}`);
61
+ if (legacyDir !== skillDir && existsSync(legacyDir)) {
62
+ rmSync(legacyDir, { recursive: true, force: true });
63
+ }
18
64
  mkdirSync(skillDir, { recursive: true });
19
- writeFileSync(skillFile, skill.content, 'utf-8');
65
+ writeFileSync(skillFile, rewriteNameInFrontmatter(skill.content), 'utf-8');
20
66
  files.push(skillFile);
21
67
  }
22
68
  return {
@@ -26,30 +72,28 @@ export function installSkills(skills, target) {
26
72
  files,
27
73
  };
28
74
  }
29
- /**
30
- * Remove all almyty-installed skills from an agent's skills directory.
31
- */
75
+ /** Remove every almyty-installed skill from an agent's skills directory. */
32
76
  export function removeSkills(target) {
33
77
  if (!existsSync(target.skillsDir))
34
78
  return 0;
35
79
  const entries = readdirSync(target.skillsDir, { withFileTypes: true });
36
80
  let removed = 0;
37
81
  for (const entry of entries) {
38
- if (entry.isDirectory() && entry.name.startsWith(SKILL_PREFIX)) {
39
- rmSync(join(target.skillsDir, entry.name), { recursive: true, force: true });
40
- removed++;
41
- }
82
+ if (!entry.isDirectory())
83
+ continue;
84
+ if (!isAlmytyInstall(target.skillsDir, entry.name))
85
+ continue;
86
+ rmSync(join(target.skillsDir, entry.name), { recursive: true, force: true });
87
+ removed++;
42
88
  }
43
89
  return removed;
44
90
  }
45
- /**
46
- * List installed almyty skills in an agent's skills directory.
47
- */
91
+ /** List almyty-installed skills in an agent's skills directory. */
48
92
  export function listInstalledSkills(target) {
49
93
  if (!existsSync(target.skillsDir))
50
94
  return [];
51
95
  const entries = readdirSync(target.skillsDir, { withFileTypes: true });
52
96
  return entries
53
- .filter(e => e.isDirectory() && e.name.startsWith(SKILL_PREFIX))
54
- .map(e => e.name.replace(SKILL_PREFIX, ''));
97
+ .filter((e) => e.isDirectory() && isAlmytyInstall(target.skillsDir, e.name))
98
+ .map((e) => e.name.replace(LEGACY_SKILL_PREFIX, ''));
55
99
  }
@@ -0,0 +1,30 @@
1
+ import { AgentTarget } from './agents.js';
2
+ import type { AlmytyConfig } from './config.js';
3
+ export interface SelectionInput {
4
+ projectDir: string;
5
+ config: AlmytyConfig;
6
+ /** `--agent <name>` (repeatable, comma/space split, supports `*` / `all`). */
7
+ agentFlag?: string | string[];
8
+ /** `--path <dir>` (repeatable, comma split). */
9
+ pathFlag?: string | string[];
10
+ /** `--all`: every project-detected agent + universal. */
11
+ all?: boolean;
12
+ /** `--yes` / `-y`: skip the interactive picker. */
13
+ yes?: boolean;
14
+ /** `--global` / `-G`: include / prefer home-scope installs. */
15
+ global?: boolean;
16
+ /** Override $HOME (test seam). */
17
+ home?: string;
18
+ }
19
+ /**
20
+ * Resolve targets for non-interactive paths. Returns null when the
21
+ * caller must fall through to the interactive picker (TTY + no
22
+ * resolving flag/config).
23
+ */
24
+ export declare function selectInstallTargetsAuto(input: SelectionInput): AgentTarget[] | null;
25
+ /**
26
+ * Drive the multi-select picker. Lists every detected agent at
27
+ * BOTH scopes, with detection scope shown as a hint, plus a custom
28
+ * path option. Returns the chosen targets.
29
+ */
30
+ export declare function selectInstallTargetsInteractive(input: SelectionInput): Promise<AgentTarget[]>;
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Target selection for the install path.
3
+ *
4
+ * Two scopes:
5
+ * - **project**: writes to `<cwd>/.<agent>/skills/` so the skill
6
+ * is part of this checkout. Detected by an `.x/` config dir in
7
+ * `projectDir`.
8
+ * - **home**: writes to `~/.<agent>/skills/`, shared across every
9
+ * project the user opens with that agent. Detected by an `.x/`
10
+ * config dir in `$HOME`.
11
+ *
12
+ * Order of precedence (highest first):
13
+ * 1. CLI `--path` — explicit custom dir(s); detection ignored.
14
+ * 2. CLI `--all` — every PROJECT-detected agent + universal
15
+ * CLI `--all --global` — every PROJECT-detected + every HOME-detected
16
+ * CLI `--global` (alone) — every HOME-detected agent
17
+ * 3. CLI `--agent foo` — the named agent at whichever scope it's
18
+ * detected in (project preferred); home if `--global` is set.
19
+ * `--agent '*'` / `--agent all` — every known agent dir at project scope
20
+ * regardless of detection.
21
+ * 4. `.almytyrc` skillsDir / agents whitelist.
22
+ * 5. Interactive picker (TTY) — multi-select listing every detected
23
+ * agent in both scopes plus a "custom path…" option.
24
+ * 6. Non-TTY fallback — project-detected + universal, or defaults.
25
+ */
26
+ import { resolve, join } from 'path';
27
+ import { homedir } from 'os';
28
+ import { AGENT_CONFIGS, detectAgents, detectHomeAgents, getDefaultTargets, } from './agents.js';
29
+ function toList(v) {
30
+ if (!v)
31
+ return [];
32
+ const arr = Array.isArray(v) ? v : [v];
33
+ return arr
34
+ .flatMap((s) => s.split(/[,\s]+/))
35
+ .map((s) => s.trim())
36
+ .filter(Boolean);
37
+ }
38
+ function customPathTarget(projectDir, p) {
39
+ const abs = resolve(projectDir, p);
40
+ return {
41
+ name: `custom (${p})`,
42
+ configDir: p,
43
+ skillsDir: abs,
44
+ scope: 'custom',
45
+ };
46
+ }
47
+ function projectAgentTarget(projectDir, name) {
48
+ const cfg = AGENT_CONFIGS.find((c) => c.name === name);
49
+ if (!cfg)
50
+ return null;
51
+ return {
52
+ name: cfg.name,
53
+ configDir: cfg.detectDirs[0],
54
+ skillsDir: join(projectDir, cfg.skillsDir),
55
+ scope: 'project',
56
+ };
57
+ }
58
+ function homeAgentTarget(home, name) {
59
+ const cfg = AGENT_CONFIGS.find((c) => c.name === name);
60
+ if (!cfg)
61
+ return null;
62
+ return {
63
+ name: cfg.name,
64
+ configDir: cfg.detectDirs[0],
65
+ skillsDir: join(home, cfg.skillsDir),
66
+ scope: 'home',
67
+ };
68
+ }
69
+ function universalTarget(projectDir) {
70
+ return {
71
+ name: 'Universal (.agents/skills)',
72
+ configDir: '.agents',
73
+ skillsDir: join(projectDir, '.agents/skills'),
74
+ scope: 'project',
75
+ };
76
+ }
77
+ function dedupeBySkillsDir(targets) {
78
+ const seen = new Set();
79
+ const out = [];
80
+ for (const t of targets) {
81
+ if (seen.has(t.skillsDir))
82
+ continue;
83
+ seen.add(t.skillsDir);
84
+ out.push(t);
85
+ }
86
+ return out;
87
+ }
88
+ function matchAgentName(filter, candidate) {
89
+ const norm = (s) => s.toLowerCase().replace(/[\s-]/g, '');
90
+ return norm(candidate).includes(norm(filter));
91
+ }
92
+ /**
93
+ * Resolve targets for non-interactive paths. Returns null when the
94
+ * caller must fall through to the interactive picker (TTY + no
95
+ * resolving flag/config).
96
+ */
97
+ export function selectInstallTargetsAuto(input) {
98
+ const { projectDir, config, all, yes, global } = input;
99
+ const home = input.home ?? homedir();
100
+ const agentFlag = toList(input.agentFlag);
101
+ const pathFlag = toList(input.pathFlag);
102
+ // 1. Explicit `--path` wins outright. Detection ignored.
103
+ if (pathFlag.length > 0) {
104
+ return dedupeBySkillsDir(pathFlag.map((p) => customPathTarget(projectDir, p)));
105
+ }
106
+ // 2. Bulk install via `--all` and/or `--global`. Only fires when
107
+ // no `--agent` filter is set — otherwise `--global` is a
108
+ // *modifier* on the agent-by-name resolution below, not a
109
+ // standalone "every home-detected" command.
110
+ if ((all || global) && agentFlag.length === 0) {
111
+ const projectDetected = all ? detectAgents(projectDir) : [];
112
+ const homeDetected = global ? detectHomeAgents(home) : [];
113
+ const universal = all ? [universalTarget(projectDir)] : [];
114
+ const combined = [...projectDetected, ...homeDetected, ...universal];
115
+ if (combined.length === 0) {
116
+ throw new Error(global && !all
117
+ ? 'No agents detected at home scope (~/.<agent>/). Run `npx @almyty/skills install` interactively or pass `--agent <name>`.'
118
+ : 'No agents detected in this project. Try --agent <name> or --path <dir>.');
119
+ }
120
+ return dedupeBySkillsDir(combined);
121
+ }
122
+ // 3. `--agent foo,bar`. Wildcard expands to every known agent at
123
+ // project scope. Otherwise: prefer project scope when detected,
124
+ // else fall through to home scope when detected, else
125
+ // project-scope at the default path (the agent's not installed
126
+ // anywhere yet; the user is opting in by name).
127
+ if (agentFlag.length > 0) {
128
+ if (agentFlag.some((f) => f === '*' || f.toLowerCase() === 'all')) {
129
+ const everyKnown = AGENT_CONFIGS
130
+ .map((c) => projectAgentTarget(projectDir, c.name))
131
+ .filter(Boolean);
132
+ return dedupeBySkillsDir([...everyKnown, universalTarget(projectDir)]);
133
+ }
134
+ const projectDetectedNames = new Set(detectAgents(projectDir).map((a) => a.name));
135
+ const homeDetectedNames = new Set(detectHomeAgents(home).map((a) => a.name));
136
+ const targets = [];
137
+ const unmatched = [];
138
+ for (const filter of agentFlag) {
139
+ const cfg = AGENT_CONFIGS.find((c) => matchAgentName(filter, c.name));
140
+ if (!cfg) {
141
+ unmatched.push(filter);
142
+ continue;
143
+ }
144
+ // Pick the right scope. With `--global` always take home if
145
+ // available; otherwise prefer project, fall through to home.
146
+ if (global && homeDetectedNames.has(cfg.name)) {
147
+ targets.push(homeAgentTarget(home, cfg.name));
148
+ }
149
+ else if (projectDetectedNames.has(cfg.name)) {
150
+ targets.push(projectAgentTarget(projectDir, cfg.name));
151
+ }
152
+ else if (homeDetectedNames.has(cfg.name)) {
153
+ targets.push(homeAgentTarget(home, cfg.name));
154
+ }
155
+ else {
156
+ // Not detected anywhere — install at project scope by name
157
+ // request. The agent picks it up next time it scans.
158
+ targets.push(projectAgentTarget(projectDir, cfg.name));
159
+ }
160
+ }
161
+ if (unmatched.length > 0) {
162
+ throw new Error(`No known agents matched --agent ${unmatched.join(',')}. ` +
163
+ `Run with --help to see supported names.`);
164
+ }
165
+ return dedupeBySkillsDir([...targets, universalTarget(projectDir)]);
166
+ }
167
+ // 4. Project `.almytyrc` skillsDir override.
168
+ if (config.skillsDir) {
169
+ return [
170
+ {
171
+ name: 'custom (.almytyrc)',
172
+ configDir: config.skillsDir,
173
+ skillsDir: resolve(projectDir, config.skillsDir),
174
+ scope: 'custom',
175
+ },
176
+ ];
177
+ }
178
+ // 5. Project `.almytyrc` agents whitelist (project-scope only).
179
+ if (config.agents && config.agents.length > 0) {
180
+ const matched = [];
181
+ for (const filter of config.agents) {
182
+ const cfg = AGENT_CONFIGS.find((c) => matchAgentName(filter, c.name));
183
+ if (cfg)
184
+ matched.push(projectAgentTarget(projectDir, cfg.name));
185
+ }
186
+ if (matched.length > 0) {
187
+ return dedupeBySkillsDir([...matched, universalTarget(projectDir)]);
188
+ }
189
+ }
190
+ // 6. `--yes` / non-TTY: pick a sensible default WITHOUT prompting.
191
+ // Project-detected + universal first; if nothing project-side,
192
+ // show home-detected as a hint (the user probably wants the
193
+ // interactive picker for this; here we just return defaults
194
+ // so scripted use doesn't hang).
195
+ if (yes || !process.stdin.isTTY) {
196
+ const projectDetected = detectAgents(projectDir);
197
+ if (projectDetected.length > 0) {
198
+ return dedupeBySkillsDir([
199
+ ...projectDetected,
200
+ universalTarget(projectDir),
201
+ ]);
202
+ }
203
+ return dedupeBySkillsDir([
204
+ ...getDefaultTargets(projectDir),
205
+ universalTarget(projectDir),
206
+ ]);
207
+ }
208
+ // Interactive picker required.
209
+ return null;
210
+ }
211
+ /**
212
+ * Drive the multi-select picker. Lists every detected agent at
213
+ * BOTH scopes, with detection scope shown as a hint, plus a custom
214
+ * path option. Returns the chosen targets.
215
+ */
216
+ export async function selectInstallTargetsInteractive(input) {
217
+ const { projectDir } = input;
218
+ const home = input.home ?? homedir();
219
+ const clack = await import('@clack/prompts');
220
+ const projectDetected = detectAgents(projectDir);
221
+ const homeDetected = detectHomeAgents(home);
222
+ // Build options: project entries first (preferred), then home,
223
+ // then known-but-not-detected as a separate group, then universal,
224
+ // then custom-path. We use synthetic value tokens so we can
225
+ // resolve back to AgentTargets after the multiselect returns.
226
+ const seen = new Set();
227
+ const options = [];
228
+ for (const a of projectDetected) {
229
+ options.push({
230
+ value: `project:${a.name}`,
231
+ label: `${a.name} (project)`,
232
+ hint: a.skillsDir.replace(projectDir, '.'),
233
+ });
234
+ seen.add(`${a.scope}:${a.name}`);
235
+ }
236
+ for (const a of homeDetected) {
237
+ if (seen.has(`project:${a.name}`)) {
238
+ // Same agent also detected locally — already shown above; the
239
+ // user can still install both scopes by adding the home line
240
+ // here, so keep it.
241
+ }
242
+ options.push({
243
+ value: `home:${a.name}`,
244
+ label: `${a.name} (home)`,
245
+ hint: a.skillsDir.replace(home, '~'),
246
+ });
247
+ }
248
+ options.push({
249
+ value: '__universal__',
250
+ label: 'Universal (.agents/skills)',
251
+ hint: 'cross-client convention; project scope',
252
+ });
253
+ // List the agents that are recognized but not detected anywhere
254
+ // so users can opt in by checkbox without retyping the name.
255
+ const detectedNames = new Set([...projectDetected, ...homeDetected].map((a) => a.name));
256
+ for (const cfg of AGENT_CONFIGS) {
257
+ if (detectedNames.has(cfg.name))
258
+ continue;
259
+ options.push({
260
+ value: `project:${cfg.name}`,
261
+ label: `${cfg.name}`,
262
+ hint: `not detected — would create ${cfg.skillsDir}`,
263
+ });
264
+ }
265
+ options.push({
266
+ value: '__custom__',
267
+ label: 'Custom path…',
268
+ hint: 'enter a directory after this prompt',
269
+ });
270
+ const initialValues = projectDetected.map((a) => `project:${a.name}`);
271
+ if (initialValues.length === 0)
272
+ initialValues.push('__universal__');
273
+ const picked = await clack.multiselect({
274
+ message: 'Where should the skills be installed?',
275
+ options,
276
+ initialValues,
277
+ required: true,
278
+ });
279
+ if (clack.isCancel(picked)) {
280
+ clack.cancel('Install cancelled.');
281
+ process.exit(0);
282
+ }
283
+ const values = picked;
284
+ const targets = [];
285
+ for (const v of values) {
286
+ if (v === '__universal__') {
287
+ targets.push(universalTarget(projectDir));
288
+ continue;
289
+ }
290
+ if (v === '__custom__') {
291
+ const path = await clack.text({
292
+ message: 'Custom skills directory path:',
293
+ placeholder: 'e.g. .my-agent/skills',
294
+ validate: (s) => (s && s.trim() ? undefined : 'Required'),
295
+ });
296
+ if (clack.isCancel(path)) {
297
+ clack.cancel('Install cancelled.');
298
+ process.exit(0);
299
+ }
300
+ targets.push(customPathTarget(projectDir, String(path).trim()));
301
+ continue;
302
+ }
303
+ const [scope, name] = v.split(':');
304
+ const t = scope === 'home'
305
+ ? homeAgentTarget(home, name)
306
+ : projectAgentTarget(projectDir, name);
307
+ if (t)
308
+ targets.push(t);
309
+ }
310
+ return dedupeBySkillsDir(targets);
311
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@almyty/skills",
3
- "version": "1.0.6",
4
- "description": "Install API skills into AI coding agents — Claude Code, Cursor, Windsurf, Copilot, Codex",
3
+ "version": "1.0.9",
4
+ "description": "Install API skills into AI coding agents \u2014 Claude Code, Cursor, Windsurf, Copilot, Codex",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "dist"
12
12
  ],
13
13
  "scripts": {
14
- "build": "tsc",
14
+ "build": "tsc && chmod +x dist/index.js",
15
15
  "dev": "tsx src/index.ts",
16
16
  "test": "vitest run",
17
17
  "test:watch": "vitest",
@@ -31,10 +31,14 @@
31
31
  ],
32
32
  "author": "almyty",
33
33
  "license": "BSL-1.1",
34
+ "dependencies": {
35
+ "@almyty/client": "^0.1.0",
36
+ "@clack/prompts": "^1.2.0"
37
+ },
34
38
  "devDependencies": {
35
39
  "@types/node": "^25.4.0",
36
40
  "tsx": "^4.7.0",
37
41
  "typescript": "^5.3.0",
38
42
  "vitest": "^4.1.0"
39
43
  }
40
- }
44
+ }