@almyty/skills 1.0.16 → 1.3.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 +143 -57
- package/dist/auth.d.ts +10 -3
- package/dist/auth.js +10 -3
- package/dist/cli-args.d.ts +16 -0
- package/dist/cli-args.js +152 -0
- package/dist/exit-codes.d.ts +26 -0
- package/dist/exit-codes.js +32 -0
- package/dist/help.d.ts +2 -0
- package/dist/help.js +111 -0
- package/dist/index.js +282 -350
- package/dist/installer.d.ts +24 -1
- package/dist/installer.js +59 -4
- package/dist/meta-skill.d.ts +9 -0
- package/dist/meta-skill.js +46 -10
- package/dist/target-selector.d.ts +7 -0
- package/dist/target-selector.js +8 -6
- package/dist/tty.d.ts +22 -0
- package/dist/tty.js +35 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +22 -0
- package/package.json +19 -4
package/dist/installer.d.ts
CHANGED
|
@@ -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
|
@@ -10,12 +10,36 @@
|
|
|
10
10
|
* already present in every SKILL.md we generate.
|
|
11
11
|
*/
|
|
12
12
|
import { writeFileSync, mkdirSync, existsSync, readdirSync, rmSync, readFileSync } from 'fs';
|
|
13
|
-
import { join } from 'path';
|
|
13
|
+
import { join, resolve, sep } from 'path';
|
|
14
14
|
/** Legacy prefix from <=v1.0.9 — older installs still use this dir
|
|
15
15
|
* name. Kept here so `remove`/`installed` continue to find them. */
|
|
16
16
|
const LEGACY_SKILL_PREFIX = 'almyty-';
|
|
17
17
|
/** Marker line we expect inside every SKILL.md frontmatter we wrote. */
|
|
18
18
|
const ALMYTY_MARKER = /^\s*author:\s*almyty\s*$/m;
|
|
19
|
+
/**
|
|
20
|
+
* Skill names come from the backend/gateway and are used to build
|
|
21
|
+
* filesystem paths under skillsDir. A malicious or compromised gateway
|
|
22
|
+
* could return `name: "../../../.ssh/authorized_keys"` and have the
|
|
23
|
+
* installer write — or, via the legacy-dir cleanup, recursively delete —
|
|
24
|
+
* arbitrary files on the user's machine (and the daemon/watch modes do
|
|
25
|
+
* this automatically on a timer). Restrict names to a single safe path
|
|
26
|
+
* segment.
|
|
27
|
+
*/
|
|
28
|
+
const SAFE_SKILL_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
29
|
+
function isSafeSkillName(name) {
|
|
30
|
+
return (typeof name === 'string' &&
|
|
31
|
+
name !== '.' &&
|
|
32
|
+
name !== '..' &&
|
|
33
|
+
!name.includes('/') &&
|
|
34
|
+
!name.includes('\\') &&
|
|
35
|
+
SAFE_SKILL_NAME.test(name));
|
|
36
|
+
}
|
|
37
|
+
/** Defence in depth: assert a built path stays inside skillsDir. */
|
|
38
|
+
function isInside(baseDir, candidate) {
|
|
39
|
+
const base = resolve(baseDir);
|
|
40
|
+
const target = resolve(candidate);
|
|
41
|
+
return target === base || target.startsWith(base + sep);
|
|
42
|
+
}
|
|
19
43
|
/**
|
|
20
44
|
* Strip the legacy `name: almyty-<x>` line in the SKILL.md frontmatter
|
|
21
45
|
* to `name: <x>` so the agent-visible identifier is the skill's own
|
|
@@ -46,11 +70,24 @@ function isAlmytyInstall(skillsDir, dirName) {
|
|
|
46
70
|
/**
|
|
47
71
|
* Install skill files into an agent's skills directory.
|
|
48
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.
|
|
49
76
|
*/
|
|
50
|
-
export function installSkills(skills, target) {
|
|
77
|
+
export function installSkills(skills, target, options = {}) {
|
|
78
|
+
const dryRun = options.dryRun === true;
|
|
51
79
|
const files = [];
|
|
52
|
-
|
|
80
|
+
let overwritten = 0;
|
|
81
|
+
if (!dryRun)
|
|
82
|
+
mkdirSync(target.skillsDir, { recursive: true });
|
|
53
83
|
for (const skill of skills) {
|
|
84
|
+
// Reject backend-supplied names that aren't a single safe path
|
|
85
|
+
// segment — skip rather than throw so one bad entry can't break the
|
|
86
|
+
// whole install (e.g. in daemon/watch mode).
|
|
87
|
+
if (!isSafeSkillName(skill.name)) {
|
|
88
|
+
console.warn(`Skipping skill with unsafe name: ${JSON.stringify(skill.name)}`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
54
91
|
const dirName = skill.name;
|
|
55
92
|
const skillDir = join(target.skillsDir, dirName);
|
|
56
93
|
const skillFile = join(skillDir, 'SKILL.md');
|
|
@@ -58,6 +95,17 @@ export function installSkills(skills, target) {
|
|
|
58
95
|
// shape, remove it before writing the new shape so the agent
|
|
59
96
|
// doesn't see two copies of the same skill.
|
|
60
97
|
const legacyDir = join(target.skillsDir, `${LEGACY_SKILL_PREFIX}${skill.name}`);
|
|
98
|
+
// Containment guard (defence in depth) before any write/delete.
|
|
99
|
+
if (!isInside(target.skillsDir, skillDir) || !isInside(target.skillsDir, legacyDir)) {
|
|
100
|
+
console.warn(`Skipping skill whose path escapes the skills directory: ${skill.name}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (existsSync(skillFile))
|
|
104
|
+
overwritten++;
|
|
105
|
+
if (dryRun) {
|
|
106
|
+
files.push(skillFile);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
61
109
|
if (legacyDir !== skillDir && existsSync(legacyDir)) {
|
|
62
110
|
rmSync(legacyDir, { recursive: true, force: true });
|
|
63
111
|
}
|
|
@@ -68,7 +116,14 @@ export function installSkills(skills, target) {
|
|
|
68
116
|
return {
|
|
69
117
|
agent: target.name,
|
|
70
118
|
skillsDir: target.skillsDir,
|
|
71
|
-
|
|
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,
|
|
72
127
|
files,
|
|
73
128
|
};
|
|
74
129
|
}
|
package/dist/meta-skill.d.ts
CHANGED
|
@@ -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;
|
package/dist/meta-skill.js
CHANGED
|
@@ -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
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
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
|
-
|
|
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
|
|
56
|
+
### Install skills (writes SKILL.md into this project's agent dirs)
|
|
34
57
|
\`\`\`bash
|
|
35
|
-
npx @almyty/skills install
|
|
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
|
|
64
|
+
npx @almyty/skills run acme/petstore/get-pet --petId 123
|
|
41
65
|
\`\`\`
|
|
42
66
|
|
|
43
|
-
###
|
|
67
|
+
### What is installed here, and undo it
|
|
44
68
|
\`\`\`bash
|
|
45
|
-
npx @almyty/skills
|
|
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
|
}
|
package/dist/target-selector.js
CHANGED
|
@@ -187,12 +187,14 @@ export function selectInstallTargetsAuto(input) {
|
|
|
187
187
|
return dedupeBySkillsDir([...matched, universalTarget(projectDir)]);
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
-
// 6. `--yes
|
|
191
|
-
// Project-detected + universal first; if nothing
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
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
|
+
}
|
package/dist/version.js
ADDED
|
@@ -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,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@almyty/skills",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
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.",
|
|
5
8
|
"type": "module",
|
|
6
9
|
"main": "dist/index.js",
|
|
7
10
|
"bin": {
|
|
@@ -30,9 +33,9 @@
|
|
|
30
33
|
"almyty"
|
|
31
34
|
],
|
|
32
35
|
"author": "almyty",
|
|
33
|
-
"license": "
|
|
36
|
+
"license": "Apache-2.0",
|
|
34
37
|
"dependencies": {
|
|
35
|
-
"@almyty/client": "^
|
|
38
|
+
"@almyty/client": "^1.2.0",
|
|
36
39
|
"@clack/prompts": "^1.2.0"
|
|
37
40
|
},
|
|
38
41
|
"devDependencies": {
|
|
@@ -40,5 +43,17 @@
|
|
|
40
43
|
"tsx": "^4.7.0",
|
|
41
44
|
"typescript": "^5.3.0",
|
|
42
45
|
"vitest": "^4.1.0"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://almyty.com",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/almyty-inc/almyty.git",
|
|
51
|
+
"directory": "packages/skills-cli"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/almyty-inc/almyty/issues"
|
|
55
|
+
},
|
|
56
|
+
"overrides": {
|
|
57
|
+
"postcss": "^8.5.23"
|
|
43
58
|
}
|
|
44
59
|
}
|