@almyty/skills 1.2.0 → 1.5.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.
@@ -14,14 +14,37 @@ import type { AgentTarget } from './agents.js';
14
14
  export interface InstallResult {
15
15
  agent: string;
16
16
  skillsDir: string;
17
+ /** How many skill files were actually written. */
17
18
  installed: number;
19
+ /** How many were refused (unsafe name, or a path outside skillsDir). */
20
+ skipped: number;
21
+ /**
22
+ * How many of the written files replaced an existing SKILL.md.
23
+ * Installing is a write into someone's editor config, so the count of
24
+ * files it overwrote is part of the answer, not a detail.
25
+ */
26
+ overwritten: number;
27
+ /** True when nothing was written because this was a dry run. */
28
+ dryRun: boolean;
18
29
  files: string[];
19
30
  }
31
+ export interface InstallOptions {
32
+ /**
33
+ * Report what would be written and touch nothing. `install` writes
34
+ * into directories an editor reads on every session; being able to
35
+ * see the exact paths first is the difference between a tool you
36
+ * trust and one you run in a scratch clone.
37
+ */
38
+ dryRun?: boolean;
39
+ }
20
40
  /**
21
41
  * Install skill files into an agent's skills directory.
22
42
  * Each skill lives at `<skillsDir>/<skill-name>/SKILL.md`.
43
+ *
44
+ * With `dryRun`, resolves and validates every path and reports what it
45
+ * would write, without creating a directory or touching a file.
23
46
  */
24
- export declare function installSkills(skills: SkillFile[], target: AgentTarget): InstallResult;
47
+ export declare function installSkills(skills: SkillFile[], target: AgentTarget, options?: InstallOptions): InstallResult;
25
48
  /** Remove every almyty-installed skill from an agent's skills directory. */
26
49
  export declare function removeSkills(target: AgentTarget): number;
27
50
  /** List almyty-installed skills in an agent's skills directory. */
package/dist/installer.js CHANGED
@@ -70,10 +70,16 @@ function isAlmytyInstall(skillsDir, dirName) {
70
70
  /**
71
71
  * Install skill files into an agent's skills directory.
72
72
  * Each skill lives at `<skillsDir>/<skill-name>/SKILL.md`.
73
+ *
74
+ * With `dryRun`, resolves and validates every path and reports what it
75
+ * would write, without creating a directory or touching a file.
73
76
  */
74
- export function installSkills(skills, target) {
77
+ export function installSkills(skills, target, options = {}) {
78
+ const dryRun = options.dryRun === true;
75
79
  const files = [];
76
- mkdirSync(target.skillsDir, { recursive: true });
80
+ let overwritten = 0;
81
+ if (!dryRun)
82
+ mkdirSync(target.skillsDir, { recursive: true });
77
83
  for (const skill of skills) {
78
84
  // Reject backend-supplied names that aren't a single safe path
79
85
  // segment — skip rather than throw so one bad entry can't break the
@@ -94,6 +100,12 @@ export function installSkills(skills, target) {
94
100
  console.warn(`Skipping skill whose path escapes the skills directory: ${skill.name}`);
95
101
  continue;
96
102
  }
103
+ if (existsSync(skillFile))
104
+ overwritten++;
105
+ if (dryRun) {
106
+ files.push(skillFile);
107
+ continue;
108
+ }
97
109
  if (legacyDir !== skillDir && existsSync(legacyDir)) {
98
110
  rmSync(legacyDir, { recursive: true, force: true });
99
111
  }
@@ -104,7 +116,14 @@ export function installSkills(skills, target) {
104
116
  return {
105
117
  agent: target.name,
106
118
  skillsDir: target.skillsDir,
107
- installed: skills.length,
119
+ // What was written, not what was offered. The loop skips skills with
120
+ // an unsafe name or a path that escapes the skills directory, and
121
+ // counting the input meant "Installed 12 skill files" for ten files
122
+ // on disk -- the two that were refused were reported as installed.
123
+ installed: files.length,
124
+ skipped: skills.length - files.length,
125
+ overwritten,
126
+ dryRun,
108
127
  files,
109
128
  };
110
129
  }
@@ -1,2 +1,11 @@
1
1
  import type { SkillFile } from './client.js';
2
+ /**
3
+ * The one skill the daemon always installs: how to reach the rest.
4
+ *
5
+ * It is read by a coding agent, so every command in it has to exist.
6
+ * It documented `install`, `list`, `search`, `run` and `daemon` only,
7
+ * and wrote refs with a mandatory leading `@` the CLI treats as
8
+ * optional — so an agent copying it out learned a surface that was
9
+ * both smaller and subtly wrong.
10
+ */
2
11
  export declare function generateMetaSkill(): SkillFile;
@@ -1,3 +1,12 @@
1
+ /**
2
+ * The one skill the daemon always installs: how to reach the rest.
3
+ *
4
+ * It is read by a coding agent, so every command in it has to exist.
5
+ * It documented `install`, `list`, `search`, `run` and `daemon` only,
6
+ * and wrote refs with a mandatory leading `@` the CLI treats as
7
+ * optional — so an agent copying it out learned a surface that was
8
+ * both smaller and subtly wrong.
9
+ */
1
10
  export function generateMetaSkill() {
2
11
  const content = `---
3
12
  name: almyty-skills
@@ -13,16 +22,30 @@ Manage API skills powered by almyty — a universal API-to-AI tool gateway.
13
22
 
14
23
  ## When to use
15
24
 
16
- - User wants to discover available API tools
17
- - User wants to find a specific API capability
18
- - User wants to run an API tool directly
19
- - User needs to list what skills are installed
25
+ - The user wants to discover available API tools
26
+ - The user wants to find a specific API capability
27
+ - The user wants to run an API tool directly
28
+ - The user needs to know which skills are installed here
29
+
30
+ ## References
31
+
32
+ A skill is addressed as \`org/gateway/skill\`, a whole gateway as
33
+ \`org/gateway\`. A leading \`@\` is optional. A bare name is treated as a
34
+ search and installs only when it matches exactly one skill.
20
35
 
21
36
  ## Commands
22
37
 
23
- ### List all available skills
38
+ Add \`--json\` to any read command for parseable output.
39
+
40
+ ### What gateways exist
41
+ \`\`\`bash
42
+ npx @almyty/skills gateways
43
+ \`\`\`
44
+
45
+ ### List available skills
24
46
  \`\`\`bash
25
47
  npx @almyty/skills list
48
+ npx @almyty/skills list acme/petstore
26
49
  \`\`\`
27
50
 
28
51
  ### Search for skills
@@ -30,20 +53,33 @@ npx @almyty/skills list
30
53
  npx @almyty/skills search <query>
31
54
  \`\`\`
32
55
 
33
- ### Install a specific skill
56
+ ### Install skills (writes SKILL.md into this project's agent dirs)
34
57
  \`\`\`bash
35
- npx @almyty/skills install @<org>/<gateway>/<skill-name>
58
+ npx @almyty/skills install acme/petstore --dry-run # show the exact files first
59
+ npx @almyty/skills install acme/petstore/get-pet
36
60
  \`\`\`
37
61
 
38
62
  ### Run a skill directly
39
63
  \`\`\`bash
40
- npx @almyty/skills run @<org>/<gateway>/<skill-name> --param1 value1 --param2 value2
64
+ npx @almyty/skills run acme/petstore/get-pet --petId 123
41
65
  \`\`\`
42
66
 
43
- ### Start the skill daemon (auto-syncs all skills)
67
+ ### What is installed here, and undo it
44
68
  \`\`\`bash
45
- npx @almyty/skills daemon
69
+ npx @almyty/skills installed
70
+ npx @almyty/skills remove
46
71
  \`\`\`
72
+
73
+ ### Keep skills in sync on a timer
74
+ \`\`\`bash
75
+ npx @almyty/skills daemon # every gateway
76
+ npx @almyty/skills watch acme/petstore # one gateway
77
+ \`\`\`
78
+
79
+ ## Exit codes
80
+
81
+ \`2\` usage error, \`3\` not authenticated (run \`npx @almyty/auth login\`),
82
+ \`4\` no such gateway or skill, \`5\` the skill ran and failed.
47
83
  `;
48
84
  return {
49
85
  name: 'skills',
@@ -13,6 +13,13 @@ export interface SelectionInput {
13
13
  yes?: boolean;
14
14
  /** `--global` / `-G`: include / prefer home-scope installs. */
15
15
  global?: boolean;
16
+ /**
17
+ * Whether prompting is allowed. The caller decides (see tty.ts):
18
+ * reading `process.stdin.isTTY` here meant a CI runner's pseudo-tty
19
+ * counted as interactive, and a scripted install could hang on a
20
+ * picker with nobody to answer it.
21
+ */
22
+ interactive?: boolean;
16
23
  /** Override $HOME (test seam). */
17
24
  home?: string;
18
25
  }
@@ -187,12 +187,14 @@ export function selectInstallTargetsAuto(input) {
187
187
  return dedupeBySkillsDir([...matched, universalTarget(projectDir)]);
188
188
  }
189
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) {
190
+ // 6. `--yes`, or nowhere to prompt: pick a sensible default WITHOUT
191
+ // prompting. Project-detected + universal first; if nothing
192
+ // project-side, fall back to the defaults so scripted use never
193
+ // hangs on a picker.
194
+ //
195
+ // `interactive` comes from the caller rather than from
196
+ // process.stdin.isTTY, which is true inside most CI runners.
197
+ if (yes || input.interactive === false) {
196
198
  const projectDetected = detectAgents(projectDir);
197
199
  if (projectDetected.length > 0) {
198
200
  return dedupeBySkillsDir([
package/dist/tty.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Whether this process may prompt, and whether it may colour.
3
+ *
4
+ * `install` used to decide interactivity from `process.stdin.isTTY`
5
+ * alone. That is true inside most CI runners' pseudo-terminals, so a
6
+ * scripted install could stop dead on a multi-select picker nobody was
7
+ * there to answer. Both streams and the CI marker have to agree before
8
+ * a prompt is allowed.
9
+ */
10
+ /** Whether both stdio streams are terminals. A test seam. */
11
+ export interface TtyState {
12
+ stdin: boolean;
13
+ stdout: boolean;
14
+ }
15
+ export declare function isInteractive(env?: NodeJS.ProcessEnv, tty?: TtyState): boolean;
16
+ /**
17
+ * Whether decoration is wanted. Honours NO_COLOR (any value) and
18
+ * FORCE_COLOR, in that order of surprise — see https://no-color.org.
19
+ * The prompt library reads the same variables, so setting NO_COLOR
20
+ * gives a plain picker rather than a half-coloured one.
21
+ */
22
+ export declare function useColor(env?: NodeJS.ProcessEnv, tty?: TtyState): boolean;
package/dist/tty.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Whether this process may prompt, and whether it may colour.
3
+ *
4
+ * `install` used to decide interactivity from `process.stdin.isTTY`
5
+ * alone. That is true inside most CI runners' pseudo-terminals, so a
6
+ * scripted install could stop dead on a multi-select picker nobody was
7
+ * there to answer. Both streams and the CI marker have to agree before
8
+ * a prompt is allowed.
9
+ */
10
+ function currentTty() {
11
+ return {
12
+ stdin: Boolean(process.stdin.isTTY),
13
+ stdout: Boolean(process.stdout.isTTY),
14
+ };
15
+ }
16
+ export function isInteractive(env = process.env, tty = currentTty()) {
17
+ if (env.ALMYTY_NON_INTERACTIVE === '1')
18
+ return false;
19
+ if (env.CI && env.CI !== 'false' && env.CI !== '0')
20
+ return false;
21
+ return tty.stdin && tty.stdout;
22
+ }
23
+ /**
24
+ * Whether decoration is wanted. Honours NO_COLOR (any value) and
25
+ * FORCE_COLOR, in that order of surprise — see https://no-color.org.
26
+ * The prompt library reads the same variables, so setting NO_COLOR
27
+ * gives a plain picker rather than a half-coloured one.
28
+ */
29
+ export function useColor(env = process.env, tty = currentTty()) {
30
+ if (env.NO_COLOR !== undefined && env.NO_COLOR !== '')
31
+ return false;
32
+ if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== '0')
33
+ return true;
34
+ return tty.stdout;
35
+ }
@@ -0,0 +1,2 @@
1
+ export declare function readVersion(fallback?: string): string;
2
+ export declare const VERSION: string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The CLI's own version, read from its package.json at startup.
3
+ *
4
+ * It used to be a hardcoded string, which drifted: `--version`
5
+ * answered 0.1.0 while the published package was 1.2.0, so a bug
6
+ * report never identified the build it came from. Both `dist/index.js`
7
+ * and `src/index.ts` sit one directory below the package root, so the
8
+ * same relative path resolves for the built bin and for `tsx src/index.ts`.
9
+ */
10
+ import { readFileSync } from 'node:fs';
11
+ export function readVersion(fallback = '0.0.0') {
12
+ try {
13
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
14
+ return typeof pkg.version === 'string' && pkg.version.length > 0
15
+ ? pkg.version
16
+ : fallback;
17
+ }
18
+ catch {
19
+ return fallback;
20
+ }
21
+ }
22
+ export const VERSION = readVersion();
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@almyty/skills",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "Install any API as a skill into Claude Code, Cursor, Windsurf, Copilot, or Codex \u2014 almyty turns OpenAPI and GraphQL into agent-ready skills.",
7
+ "description": "Install any API as a skill into Claude Code, Cursor, Windsurf, Copilot, or Codex — almyty turns OpenAPI and GraphQL into agent-ready skills.",
8
8
  "type": "module",
9
9
  "main": "dist/index.js",
10
10
  "bin": {
@@ -52,5 +52,8 @@
52
52
  },
53
53
  "bugs": {
54
54
  "url": "https://github.com/almyty-inc/almyty/issues"
55
+ },
56
+ "overrides": {
57
+ "postcss": "^8.5.23"
55
58
  }
56
59
  }