@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/index.js
CHANGED
|
@@ -1,182 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { resolveCredentials } from './auth.js';
|
|
3
3
|
import { AlmytyClient, parseRef } from './client.js';
|
|
4
4
|
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
8
|
import { selectInstallTargetsAuto, selectInstallTargetsInteractive, } from './target-selector.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Commands:
|
|
18
|
-
daemon Start skill daemon (syncs all skills)
|
|
19
|
-
install <ref> Install skills
|
|
20
|
-
list [ref] List available skills
|
|
21
|
-
search <query> Search for skills
|
|
22
|
-
run <ref> [--key value ...] Execute a skill
|
|
23
|
-
installed Show locally installed skills
|
|
24
|
-
remove Remove all installed skills
|
|
25
|
-
gateways List your gateways
|
|
26
|
-
|
|
27
|
-
Authentication:
|
|
28
|
-
Run \`npx @almyty/auth login\` once. Credentials at ~/.almyty/credentials.json
|
|
29
|
-
are shared across every almyty CLI.
|
|
30
|
-
|
|
31
|
-
References:
|
|
32
|
-
org/gateway All skills from a gateway
|
|
33
|
-
org/gateway/skill A specific skill
|
|
34
|
-
skill-name Search by name
|
|
35
|
-
<uuid> Direct ID reference
|
|
36
|
-
|
|
37
|
-
Config:
|
|
38
|
-
.almytyrc JSON config file (project or home dir)
|
|
39
|
-
ALMYTY_SKILLS_DIR Override skill installation directory
|
|
40
|
-
ALMYTY_URL Override API URL
|
|
41
|
-
ALMYTY_TOKEN Override auth token
|
|
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).
|
|
62
|
-
--interval, -i <seconds> Daemon poll interval in seconds (default: 60)
|
|
63
|
-
--url <url> almyty API URL (default: https://api.almyty.com)
|
|
64
|
-
--dir <path> Project directory (default: current directory)
|
|
65
|
-
--help, -h Show help
|
|
66
|
-
--version, -v Show version
|
|
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
|
-
|
|
74
|
-
Examples:
|
|
75
|
-
npx @almyty/skills daemon
|
|
76
|
-
npx @almyty/skills install myorg/petstore/get-pet
|
|
77
|
-
npx @almyty/skills install acme/petstore -a codex -a claude
|
|
78
|
-
npx @almyty/skills install acme/petstore --path ./tmp/skills --yes
|
|
79
|
-
npx @almyty/skills install acme/petstore --all
|
|
80
|
-
npx @almyty/skills search "weather"
|
|
81
|
-
npx @almyty/skills run myorg/petstore/get-pet --petId 123
|
|
82
|
-
`);
|
|
9
|
+
import { EXIT } from './exit-codes.js';
|
|
10
|
+
import { VERSION } from './version.js';
|
|
11
|
+
import { isInteractive } from './tty.js';
|
|
12
|
+
import { printHelp } from './help.js';
|
|
13
|
+
import { getRef, parseArgs, parseRunParams, } from './cli-args.js';
|
|
14
|
+
function emitJson(value) {
|
|
15
|
+
console.log(JSON.stringify(value, null, 2));
|
|
83
16
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
}
|
|
103
|
-
function parseArgs(argv) {
|
|
104
|
-
const result = { positional: [], flags: {} };
|
|
105
|
-
let i = 0;
|
|
106
|
-
while (i < argv.length) {
|
|
107
|
-
const arg = argv[i];
|
|
108
|
-
if (arg === '--gateway' || arg === '-g') {
|
|
109
|
-
result.flags.gateway = argv[++i] || '';
|
|
110
|
-
}
|
|
111
|
-
else if (arg === '--url') {
|
|
112
|
-
result.flags.url = argv[++i] || '';
|
|
113
|
-
}
|
|
114
|
-
else if (arg === '--dir') {
|
|
115
|
-
result.flags.dir = argv[++i] || '';
|
|
116
|
-
}
|
|
117
|
-
else if (arg === '--interval' || arg === '-i') {
|
|
118
|
-
result.flags.interval = argv[++i] || '60';
|
|
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
|
-
}
|
|
137
|
-
else if (arg === '--help' || arg === '-h') {
|
|
138
|
-
result.flags.help = true;
|
|
139
|
-
}
|
|
140
|
-
else if (arg === '--version' || arg === '-v') {
|
|
141
|
-
result.flags.version = true;
|
|
142
|
-
}
|
|
143
|
-
else if (arg.includes('/')) {
|
|
144
|
-
result.ref = arg;
|
|
145
|
-
}
|
|
146
|
-
else if (arg.startsWith('--')) {
|
|
147
|
-
const key = arg.slice(2);
|
|
148
|
-
const next = argv[i + 1];
|
|
149
|
-
if (next && !next.startsWith('--')) {
|
|
150
|
-
if (REPEATABLE_FLAGS.has(key)) {
|
|
151
|
-
appendRepeatable(result.flags, key, next);
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
result.flags[key] = next;
|
|
155
|
-
}
|
|
156
|
-
i++;
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
result.flags[key] = true;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
else if (!result.command) {
|
|
163
|
-
result.command = arg;
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
result.positional.push(arg);
|
|
167
|
-
}
|
|
168
|
-
i++;
|
|
169
|
-
}
|
|
170
|
-
return result;
|
|
171
|
-
}
|
|
172
|
-
function getRef(args) {
|
|
173
|
-
if (args.ref)
|
|
174
|
-
return args.ref;
|
|
175
|
-
if (args.flags.gateway)
|
|
176
|
-
return args.flags.gateway;
|
|
177
|
-
if (args.positional.length > 0)
|
|
178
|
-
return args.positional[0];
|
|
179
|
-
return null;
|
|
17
|
+
function fail(message, code) {
|
|
18
|
+
console.error(message);
|
|
19
|
+
process.exit(code);
|
|
180
20
|
}
|
|
181
21
|
function requireRef(args, command) {
|
|
182
22
|
const ref = getRef(args);
|
|
@@ -184,22 +24,43 @@ function requireRef(args, command) {
|
|
|
184
24
|
console.error('Error: reference required');
|
|
185
25
|
console.error(` npx @almyty/skills ${command} <org>/<gateway>`);
|
|
186
26
|
console.error(` npx @almyty/skills ${command} <skill-name>`);
|
|
187
|
-
process.exit(
|
|
27
|
+
process.exit(EXIT.USAGE);
|
|
188
28
|
}
|
|
189
29
|
return ref;
|
|
190
30
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
31
|
+
/**
|
|
32
|
+
* The credential, or the one instruction that fixes it.
|
|
33
|
+
*
|
|
34
|
+
* This used to call the shared resolver, which exits 1 — the same code
|
|
35
|
+
* as an unexpected crash. Every almyty CLI answers a missing credential
|
|
36
|
+
* with 3 so a script can retry the login instead of guessing.
|
|
37
|
+
*/
|
|
38
|
+
function requireAuth(args) {
|
|
39
|
+
const creds = resolveCredentials();
|
|
40
|
+
if (!creds?.token) {
|
|
41
|
+
if (args.flags.json) {
|
|
42
|
+
emitJson({
|
|
43
|
+
error: 'NOT_AUTHENTICATED',
|
|
44
|
+
message: 'Run `npx @almyty/auth login`, or set ALMYTY_TOKEN.',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
console.error('Not authenticated. Run one of:');
|
|
48
|
+
console.error(' npx @almyty/auth login');
|
|
49
|
+
console.error(' export ALMYTY_TOKEN=<your-token>');
|
|
50
|
+
process.exit(EXIT.AUTH);
|
|
51
|
+
}
|
|
52
|
+
return { url: creds.url, token: creds.token };
|
|
53
|
+
}
|
|
54
|
+
function newClient(args, urlOverride) {
|
|
55
|
+
const { url, token } = requireAuth(args);
|
|
56
|
+
return new AlmytyClient(urlOverride || url, token);
|
|
57
|
+
}
|
|
58
|
+
/** `org/gateway/skill — description`, the label used everywhere. */
|
|
59
|
+
function skillLabel(skill) {
|
|
60
|
+
if (skill.orgSlug && skill.gatewaySlug) {
|
|
61
|
+
return `${skill.orgSlug}/${skill.gatewaySlug}/${skill.name}`;
|
|
201
62
|
}
|
|
202
|
-
return
|
|
63
|
+
return skill.skillRef || skill.name || skill.toolName;
|
|
203
64
|
}
|
|
204
65
|
async function main() {
|
|
205
66
|
const args = parseArgs(process.argv.slice(2));
|
|
@@ -215,6 +76,7 @@ async function main() {
|
|
|
215
76
|
const projectDir = args.flags.dir || process.cwd();
|
|
216
77
|
const config = loadConfig(projectDir);
|
|
217
78
|
const urlOverride = args.flags.url || config.url;
|
|
79
|
+
const json = args.flags.json === true;
|
|
218
80
|
switch (command) {
|
|
219
81
|
case 'login':
|
|
220
82
|
case 'logout':
|
|
@@ -222,14 +84,17 @@ async function main() {
|
|
|
222
84
|
// Auth has moved to its own dedicated package. Redirect rather
|
|
223
85
|
// than silently doing nothing — users typing the old commands
|
|
224
86
|
// should see the new entry point.
|
|
225
|
-
console.error(
|
|
87
|
+
console.error('Authentication moved to @almyty/auth.');
|
|
226
88
|
console.error(` npx @almyty/auth ${command}`);
|
|
227
|
-
process.exit(
|
|
89
|
+
process.exit(EXIT.USAGE);
|
|
228
90
|
}
|
|
229
91
|
case 'gateways': {
|
|
230
|
-
const
|
|
231
|
-
const client = new AlmytyClient(urlOverride || url, token);
|
|
92
|
+
const client = newClient(args, urlOverride);
|
|
232
93
|
const gateways = await client.listGateways();
|
|
94
|
+
if (json) {
|
|
95
|
+
emitJson(gateways);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
233
98
|
if (gateways.length === 0) {
|
|
234
99
|
console.log('No gateways found. Create one at https://app.almyty.com/gateways');
|
|
235
100
|
return;
|
|
@@ -246,47 +111,46 @@ async function main() {
|
|
|
246
111
|
}
|
|
247
112
|
case 'list': {
|
|
248
113
|
const ref = getRef(args);
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
if (
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
254
|
-
|
|
114
|
+
const client = newClient(args, urlOverride);
|
|
115
|
+
const parsed = ref ? parseRef(ref) : null;
|
|
116
|
+
if (parsed && (parsed.type === 'gateway' || parsed.type === 'uuid')) {
|
|
117
|
+
const skills = await client.fetchSkills(ref);
|
|
118
|
+
if (json) {
|
|
119
|
+
emitJson(skills.map((s) => ({ name: s.name, fileName: s.fileName })));
|
|
255
120
|
return;
|
|
256
121
|
}
|
|
257
|
-
console.log(`\n${allSkills.length} skills available:\n`);
|
|
258
|
-
for (const skill of allSkills) {
|
|
259
|
-
const label = skill.gateway ? `${skill.orgSlug}/${skill.gatewaySlug}/${skill.name}` : skill.name;
|
|
260
|
-
const desc = skill.description ? ` — ${skill.description}` : '';
|
|
261
|
-
console.log(` ${label}${desc}`);
|
|
262
|
-
}
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
const parsed = parseRef(ref);
|
|
266
|
-
if (parsed.type === 'gateway' || parsed.type === 'uuid') {
|
|
267
|
-
const skills = await client.fetchSkills(ref);
|
|
268
122
|
if (skills.length === 0) {
|
|
269
123
|
console.log('No skills available. Assign tools to your gateway first.');
|
|
270
124
|
return;
|
|
271
125
|
}
|
|
272
126
|
console.log(`\n${skills.length} skills available:\n`);
|
|
273
|
-
for (const skill of skills)
|
|
127
|
+
for (const skill of skills)
|
|
274
128
|
console.log(` ${skill.name}`);
|
|
275
|
-
}
|
|
276
129
|
console.log(`\nInstall: npx @almyty/skills install ${ref}`);
|
|
130
|
+
break;
|
|
277
131
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
132
|
+
// No ref, or a name that is not a gateway ref: list everything.
|
|
133
|
+
const allSkills = (await client.fetchAllSkills());
|
|
134
|
+
if (json) {
|
|
135
|
+
emitJson(allSkills.map((s) => ({
|
|
136
|
+
ref: skillLabel(s),
|
|
137
|
+
name: s.name,
|
|
138
|
+
description: s.description ?? null,
|
|
139
|
+
gateway: s.gateway ?? null,
|
|
140
|
+
gatewayId: s.gatewayId ?? null,
|
|
141
|
+
orgSlug: s.orgSlug ?? null,
|
|
142
|
+
gatewaySlug: s.gatewaySlug ?? null,
|
|
143
|
+
})));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (allSkills.length === 0) {
|
|
147
|
+
console.log('No skills available. Assign tools to your gateways first.');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
console.log(`\n${allSkills.length} skills available:\n`);
|
|
151
|
+
for (const skill of allSkills) {
|
|
152
|
+
const desc = skill.description ? ` — ${skill.description}` : '';
|
|
153
|
+
console.log(` ${skillLabel(skill)}${desc}`);
|
|
290
154
|
}
|
|
291
155
|
break;
|
|
292
156
|
}
|
|
@@ -295,11 +159,24 @@ async function main() {
|
|
|
295
159
|
if (!query) {
|
|
296
160
|
console.error('Error: search query required');
|
|
297
161
|
console.error(' npx @almyty/skills search <query>');
|
|
298
|
-
process.exit(
|
|
162
|
+
process.exit(EXIT.USAGE);
|
|
299
163
|
}
|
|
300
|
-
|
|
301
|
-
|
|
164
|
+
// Search is org-scoped: it looks through the gateways YOUR
|
|
165
|
+
// account can see, so it needs a credential. There is no public
|
|
166
|
+
// skill index to search without one.
|
|
167
|
+
const client = newClient(args, urlOverride);
|
|
302
168
|
const results = await client.searchSkills(query);
|
|
169
|
+
if (json) {
|
|
170
|
+
emitJson((results ?? []).map((r) => ({
|
|
171
|
+
ref: r.skillRef ?? null,
|
|
172
|
+
name: r.toolName ?? r.name ?? null,
|
|
173
|
+
description: r.toolDescription ?? null,
|
|
174
|
+
gatewayId: r.gatewayId ?? null,
|
|
175
|
+
gatewayName: r.gatewayName ?? null,
|
|
176
|
+
toolId: r.toolId ?? null,
|
|
177
|
+
})));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
303
180
|
if (!results || results.length === 0) {
|
|
304
181
|
console.log(`No skills found for "${query}".`);
|
|
305
182
|
return;
|
|
@@ -310,19 +187,20 @@ async function main() {
|
|
|
310
187
|
const desc = skill.toolDescription ? ` — ${skill.toolDescription}` : '';
|
|
311
188
|
console.log(` ${label}${desc}`);
|
|
312
189
|
}
|
|
313
|
-
console.log(
|
|
314
|
-
console.log(
|
|
190
|
+
console.log('\nInstall: npx @almyty/skills install <ref>');
|
|
191
|
+
console.log('Run: npx @almyty/skills run <ref>');
|
|
315
192
|
break;
|
|
316
193
|
}
|
|
317
194
|
case 'install': {
|
|
318
195
|
const ref = requireRef(args, 'install');
|
|
319
|
-
const
|
|
320
|
-
const client = new AlmytyClient(urlOverride || url, token);
|
|
196
|
+
const client = newClient(args, urlOverride);
|
|
321
197
|
const parsed = parseRef(ref);
|
|
198
|
+
const dryRun = args.flags['dry-run'] === true;
|
|
322
199
|
let skills = [];
|
|
323
200
|
let gwName = ref;
|
|
324
201
|
if (parsed.type === 'gateway' || parsed.type === 'uuid') {
|
|
325
|
-
|
|
202
|
+
if (!json)
|
|
203
|
+
console.log('Fetching skills...');
|
|
326
204
|
const [gateway, fetched] = await Promise.all([
|
|
327
205
|
client.fetchGateway(ref).catch(() => null),
|
|
328
206
|
client.fetchSkills(ref),
|
|
@@ -331,33 +209,37 @@ async function main() {
|
|
|
331
209
|
gwName = gateway?.name || ref;
|
|
332
210
|
}
|
|
333
211
|
else if (parsed.type === 'skill') {
|
|
334
|
-
|
|
212
|
+
if (!json)
|
|
213
|
+
console.log('Fetching skill...');
|
|
335
214
|
const gatewayRef = `${parsed.orgSlug}/${parsed.gatewaySlug}`;
|
|
336
215
|
const fetched = await client.fetchSkills(gatewayRef);
|
|
337
|
-
const match = fetched.find(s => s.name === parsed.skillName ||
|
|
216
|
+
const match = fetched.find((s) => s.name === parsed.skillName ||
|
|
338
217
|
s.fileName === `almyty-${parsed.skillName}` ||
|
|
339
218
|
s.fileName === parsed.skillName);
|
|
340
219
|
if (!match) {
|
|
341
220
|
console.error(`Skill "${parsed.skillName}" not found in ${gatewayRef}`);
|
|
342
|
-
const available = fetched.map(s => s.name).join(', ');
|
|
221
|
+
const available = fetched.map((s) => s.name).join(', ');
|
|
343
222
|
if (available)
|
|
344
223
|
console.error(`Available: ${available}`);
|
|
345
|
-
process.exit(
|
|
224
|
+
process.exit(EXIT.NOT_FOUND);
|
|
346
225
|
}
|
|
347
226
|
skills = [match];
|
|
348
227
|
gwName = `${gatewayRef}/${parsed.skillName}`;
|
|
349
228
|
}
|
|
350
229
|
else if (parsed.type === 'search') {
|
|
351
|
-
|
|
230
|
+
if (!json)
|
|
231
|
+
console.log(`Searching for "${ref}"...`);
|
|
352
232
|
const results = await client.searchSkills(ref);
|
|
353
233
|
if (!results || results.length === 0) {
|
|
354
|
-
|
|
355
|
-
process.exit(1);
|
|
234
|
+
fail(`No skills found for "${ref}".`, EXIT.NOT_FOUND);
|
|
356
235
|
}
|
|
357
236
|
if (results.length === 1) {
|
|
358
237
|
const match = results[0];
|
|
359
238
|
const fetched = await client.fetchSkills(match.gatewayId);
|
|
360
|
-
const toolSlug = match.toolName
|
|
239
|
+
const toolSlug = match.toolName
|
|
240
|
+
?.toLowerCase()
|
|
241
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
242
|
+
.replace(/^-|-$/g, '');
|
|
361
243
|
const skill = fetched.find((s) => s.name === toolSlug || s.name === match.toolName);
|
|
362
244
|
if (skill) {
|
|
363
245
|
skills = [skill];
|
|
@@ -369,29 +251,41 @@ async function main() {
|
|
|
369
251
|
}
|
|
370
252
|
}
|
|
371
253
|
else {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
254
|
+
// Ambiguous: refuse rather than guess, and exit non-zero so a
|
|
255
|
+
// script does not treat "installed nothing" as success.
|
|
256
|
+
if (json) {
|
|
257
|
+
emitJson({
|
|
258
|
+
error: 'AMBIGUOUS_REF',
|
|
259
|
+
query: ref,
|
|
260
|
+
matches: results.map((r) => r.skillRef || r.toolName),
|
|
261
|
+
});
|
|
377
262
|
}
|
|
378
|
-
|
|
379
|
-
|
|
263
|
+
else {
|
|
264
|
+
console.error(`\nMultiple matches for "${ref}":\n`);
|
|
265
|
+
for (const r of results) {
|
|
266
|
+
const label = r.skillRef || r.toolName;
|
|
267
|
+
const desc = r.toolDescription ? ` — ${r.toolDescription}` : '';
|
|
268
|
+
console.error(` ${label}${desc}`);
|
|
269
|
+
}
|
|
270
|
+
console.error('\nBe more specific: npx @almyty/skills install org/gateway/skill');
|
|
271
|
+
}
|
|
272
|
+
process.exit(EXIT.USAGE);
|
|
380
273
|
}
|
|
381
274
|
}
|
|
382
275
|
if (skills.length === 0) {
|
|
383
|
-
|
|
384
|
-
return;
|
|
276
|
+
fail('No skills found.', EXIT.NOT_FOUND);
|
|
385
277
|
}
|
|
386
|
-
console.log(`\n${gwName} (${skills.length} skill(s))`);
|
|
387
278
|
const selection = {
|
|
388
279
|
projectDir,
|
|
389
280
|
config,
|
|
390
281
|
agentFlag: args.flags.agent,
|
|
391
282
|
pathFlag: args.flags.path,
|
|
392
|
-
all:
|
|
393
|
-
|
|
394
|
-
|
|
283
|
+
all: args.flags.all === true,
|
|
284
|
+
// A dry run must never sit on a picker in a pipe, and --json has
|
|
285
|
+
// no way to render one.
|
|
286
|
+
yes: args.flags.yes === true || json,
|
|
287
|
+
global: args.flags.global === true,
|
|
288
|
+
interactive: isInteractive(),
|
|
395
289
|
};
|
|
396
290
|
let targets = selectInstallTargetsAuto(selection);
|
|
397
291
|
if (targets === null) {
|
|
@@ -399,23 +293,70 @@ async function main() {
|
|
|
399
293
|
targets = await selectInstallTargetsInteractive(selection);
|
|
400
294
|
}
|
|
401
295
|
if (targets.length === 0) {
|
|
402
|
-
|
|
403
|
-
|
|
296
|
+
fail('No install targets resolved. Pass --agent / --path / --all, or run without flags in a terminal.', EXIT.USAGE);
|
|
297
|
+
}
|
|
298
|
+
// Say where, before writing. Installing edits directories an
|
|
299
|
+
// editor reads on every session, so the paths belong on screen
|
|
300
|
+
// whether or not the user asked for a dry run.
|
|
301
|
+
if (!json) {
|
|
302
|
+
console.log('');
|
|
303
|
+
console.log(dryRun
|
|
304
|
+
? `${gwName} (${skills.length} skill(s)) — dry run, nothing will be written:`
|
|
305
|
+
: `${gwName} (${skills.length} skill(s)) — installing to:`);
|
|
306
|
+
for (const target of targets) {
|
|
307
|
+
console.log(` ${target.name}: ${target.skillsDir}`);
|
|
308
|
+
}
|
|
404
309
|
}
|
|
405
|
-
const results = targets.map(target => installSkills(skills, target));
|
|
310
|
+
const results = targets.map((target) => installSkills(skills, target, { dryRun }));
|
|
311
|
+
if (json) {
|
|
312
|
+
emitJson({
|
|
313
|
+
ref,
|
|
314
|
+
gateway: gwName,
|
|
315
|
+
dryRun,
|
|
316
|
+
skills: skills.map((s) => s.name),
|
|
317
|
+
targets: results.map((r) => ({
|
|
318
|
+
agent: r.agent,
|
|
319
|
+
skillsDir: r.skillsDir,
|
|
320
|
+
installed: r.installed,
|
|
321
|
+
skipped: r.skipped,
|
|
322
|
+
overwritten: r.overwritten,
|
|
323
|
+
files: r.files,
|
|
324
|
+
})),
|
|
325
|
+
});
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const totalInstalled = results.reduce((sum, r) => sum + r.installed, 0);
|
|
329
|
+
const totalSkipped = results.reduce((sum, r) => sum + r.skipped, 0);
|
|
330
|
+
const totalOverwritten = results.reduce((sum, r) => sum + r.overwritten, 0);
|
|
406
331
|
console.log('');
|
|
407
332
|
for (const result of results) {
|
|
408
|
-
console.log(` ${result.agent}: ${result.installed}
|
|
333
|
+
console.log(` ${result.agent}: ${result.installed} skill file(s) ${dryRun ? 'would go to' : '->'} ${result.skillsDir}`);
|
|
334
|
+
if (dryRun)
|
|
335
|
+
for (const file of result.files)
|
|
336
|
+
console.log(` ${file}`);
|
|
337
|
+
}
|
|
338
|
+
console.log('');
|
|
339
|
+
if (dryRun) {
|
|
340
|
+
console.log(`Dry run: ${totalInstalled} skill file(s) across ${results.length} target(s), ${totalOverwritten} of them replacing an existing file.`);
|
|
341
|
+
console.log('Re-run without --dry-run to write them.');
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
console.log(`Installed ${totalInstalled} skill file(s) across ${results.length} target(s).`);
|
|
345
|
+
if (totalOverwritten > 0) {
|
|
346
|
+
console.log(`${totalOverwritten} replaced a SKILL.md that was already there.`);
|
|
347
|
+
}
|
|
348
|
+
console.log('Your AI coding agent picks them up on its next session.');
|
|
349
|
+
}
|
|
350
|
+
// Said out loud: a skill refused for an unsafe name used to be
|
|
351
|
+
// counted as installed, so the total was the number offered.
|
|
352
|
+
if (totalSkipped > 0) {
|
|
353
|
+
console.log(`${totalSkipped} skill(s) were skipped — see the warnings above.`);
|
|
409
354
|
}
|
|
410
|
-
const totalInstalled = results.reduce((sum, r) => sum + r.installed, 0);
|
|
411
|
-
console.log(`\nInstalled ${totalInstalled} skill files across ${results.length} agent(s).`);
|
|
412
|
-
console.log('Skills will be automatically loaded by your AI coding agent.');
|
|
413
355
|
break;
|
|
414
356
|
}
|
|
415
357
|
case 'run': {
|
|
416
358
|
const ref = requireRef(args, 'run');
|
|
417
|
-
const
|
|
418
|
-
const client = new AlmytyClient(urlOverride || url, token);
|
|
359
|
+
const client = newClient(args, urlOverride);
|
|
419
360
|
const parsed = parseRef(ref);
|
|
420
361
|
let gatewayId;
|
|
421
362
|
let toolId;
|
|
@@ -427,144 +368,134 @@ async function main() {
|
|
|
427
368
|
else if (parsed.type === 'search') {
|
|
428
369
|
const results = await client.searchSkills(ref);
|
|
429
370
|
if (!results || results.length === 0) {
|
|
430
|
-
|
|
431
|
-
process.exit(1);
|
|
371
|
+
fail(`No skill found for "${ref}".`, EXIT.NOT_FOUND);
|
|
432
372
|
}
|
|
433
373
|
if (results.length > 1) {
|
|
434
374
|
console.error(`Multiple matches for "${ref}". Be more specific:`);
|
|
435
|
-
for (const r of results)
|
|
375
|
+
for (const r of results)
|
|
436
376
|
console.error(` ${r.skillRef || r.toolName}`);
|
|
437
|
-
|
|
438
|
-
process.exit(1);
|
|
377
|
+
process.exit(EXIT.USAGE);
|
|
439
378
|
}
|
|
440
379
|
gatewayId = results[0].gatewayId;
|
|
441
380
|
toolId = results[0].toolId;
|
|
442
381
|
}
|
|
443
382
|
else {
|
|
444
|
-
|
|
445
|
-
process.exit(1);
|
|
383
|
+
fail('Error: run needs a skill reference (org/gateway/skill, or a skill name)', EXIT.USAGE);
|
|
446
384
|
}
|
|
447
385
|
const params = parseRunParams(args);
|
|
448
386
|
const result = await client.executeSkill(gatewayId, toolId, params);
|
|
387
|
+
// A skill's result IS data, so JSON is the default here rather
|
|
388
|
+
// than an opt-in. --json is accepted and means the same thing.
|
|
449
389
|
console.log(JSON.stringify(result, null, 2));
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
console.log(`almyty skill daemon (every ${intervalSec}s)`);
|
|
462
|
-
console.log(`Syncing to ${targets.length} agent target(s):`);
|
|
463
|
-
for (const t of targets) {
|
|
464
|
-
console.log(` ${t.name}: ${t.skillsDir}`);
|
|
390
|
+
// The gateway answers 200 with a body that reports the tool's own
|
|
391
|
+
// failure, so the body decides the exit code.
|
|
392
|
+
if (result && typeof result === 'object') {
|
|
393
|
+
const body = result;
|
|
394
|
+
const failed = body.success === false ||
|
|
395
|
+
body.status === 'failed' ||
|
|
396
|
+
body.status === 'error' ||
|
|
397
|
+
(body.data && body.data.success === false);
|
|
398
|
+
if (failed)
|
|
399
|
+
process.exitCode = EXIT.FAILED;
|
|
465
400
|
}
|
|
466
|
-
console.log('\nPress Ctrl+C to stop.\n');
|
|
467
|
-
let lastHash = '';
|
|
468
|
-
const sync = async () => {
|
|
469
|
-
try {
|
|
470
|
-
const allSkills = await client.fetchAllSkills();
|
|
471
|
-
const metaSkill = generateMetaSkill();
|
|
472
|
-
const skills = [metaSkill, ...(allSkills || [])];
|
|
473
|
-
const currentHash = skills.map(s => `${s.name}:${s.content.length}`).join('|');
|
|
474
|
-
if (currentHash !== lastHash) {
|
|
475
|
-
lastHash = currentHash;
|
|
476
|
-
const ts = new Date().toLocaleTimeString();
|
|
477
|
-
for (const target of targets) {
|
|
478
|
-
installSkills(skills, target);
|
|
479
|
-
}
|
|
480
|
-
console.log(`[${ts}] Synced ${skills.length} skills to ${targets.length} agent(s).`);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
catch (err) {
|
|
484
|
-
const ts = new Date().toLocaleTimeString();
|
|
485
|
-
console.error(`[${ts}] Sync error: ${err.message}`);
|
|
486
|
-
}
|
|
487
|
-
};
|
|
488
|
-
await sync();
|
|
489
|
-
const interval = setInterval(sync, intervalSec * 1000);
|
|
490
|
-
const shutdown = () => {
|
|
491
|
-
clearInterval(interval);
|
|
492
|
-
console.log('\nDaemon stopped.');
|
|
493
|
-
process.exit(0);
|
|
494
|
-
};
|
|
495
|
-
process.on('SIGINT', shutdown);
|
|
496
|
-
process.on('SIGTERM', shutdown);
|
|
497
|
-
await new Promise(() => { });
|
|
498
401
|
break;
|
|
499
402
|
}
|
|
500
403
|
case 'installed': {
|
|
501
404
|
const targets = resolveTargets(projectDir, config);
|
|
502
|
-
|
|
405
|
+
const found = [];
|
|
503
406
|
for (const target of targets) {
|
|
504
407
|
const installed = listInstalledSkills(target);
|
|
505
408
|
if (installed.length > 0) {
|
|
506
|
-
|
|
507
|
-
for (const name of installed) {
|
|
508
|
-
console.log(` ${name}`);
|
|
509
|
-
}
|
|
510
|
-
totalFound += installed.length;
|
|
409
|
+
found.push({ agent: target.name, skillsDir: target.skillsDir, skills: installed });
|
|
511
410
|
}
|
|
512
411
|
}
|
|
513
|
-
if (
|
|
412
|
+
if (json) {
|
|
413
|
+
emitJson(found);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (found.length === 0) {
|
|
514
417
|
console.log('No almyty skills installed in this directory.');
|
|
515
418
|
console.log('Install: npx @almyty/skills install <org>/<gateway>');
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
for (const entry of found) {
|
|
422
|
+
console.log(`\n${entry.agent} (${entry.skillsDir}):`);
|
|
423
|
+
for (const name of entry.skills)
|
|
424
|
+
console.log(` ${name}`);
|
|
516
425
|
}
|
|
517
426
|
break;
|
|
518
427
|
}
|
|
519
428
|
case 'remove': {
|
|
520
429
|
const targets = resolveTargets(projectDir, config);
|
|
521
|
-
|
|
430
|
+
const removedPer = [];
|
|
522
431
|
for (const target of targets) {
|
|
523
432
|
const removed = removeSkills(target);
|
|
524
433
|
if (removed > 0) {
|
|
525
|
-
|
|
526
|
-
totalRemoved += removed;
|
|
434
|
+
removedPer.push({ agent: target.name, skillsDir: target.skillsDir, removed });
|
|
527
435
|
}
|
|
528
436
|
}
|
|
437
|
+
const totalRemoved = removedPer.reduce((sum, r) => sum + r.removed, 0);
|
|
438
|
+
if (json) {
|
|
439
|
+
emitJson({ removed: totalRemoved, targets: removedPer });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
529
442
|
if (totalRemoved === 0) {
|
|
530
443
|
console.log('No almyty skills found to remove.');
|
|
444
|
+
return;
|
|
531
445
|
}
|
|
532
|
-
|
|
533
|
-
console.log(
|
|
446
|
+
for (const entry of removedPer) {
|
|
447
|
+
console.log(` Removed ${entry.removed} skill(s) from ${entry.skillsDir}`);
|
|
534
448
|
}
|
|
449
|
+
console.log(`\nRemoved ${totalRemoved} skill(s) total.`);
|
|
535
450
|
break;
|
|
536
451
|
}
|
|
452
|
+
case 'daemon':
|
|
537
453
|
case 'watch': {
|
|
538
|
-
const ref = requireRef(args, 'watch');
|
|
539
454
|
const intervalSec = parseInt(args.flags.interval, 10) || config.interval || 60;
|
|
540
|
-
const
|
|
541
|
-
const
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
455
|
+
const client = newClient(args, urlOverride);
|
|
456
|
+
const watchRef = command === 'watch' ? requireRef(args, 'watch') : null;
|
|
457
|
+
const targets = watchRef
|
|
458
|
+
? getAllTargets(projectDir)
|
|
459
|
+
: resolveTargets(projectDir, config);
|
|
460
|
+
if (targets.length === 0) {
|
|
461
|
+
fail('No agent targets found. Pass --agent or --path.', EXIT.USAGE);
|
|
462
|
+
}
|
|
463
|
+
let label = 'every skill';
|
|
464
|
+
if (watchRef) {
|
|
465
|
+
const gateway = await client.fetchGateway(watchRef).catch(() => null);
|
|
466
|
+
label = gateway?.name || watchRef;
|
|
549
467
|
}
|
|
468
|
+
console.log(`almyty skill sync: ${label}, every ${intervalSec}s`);
|
|
469
|
+
console.log(`Syncing to ${targets.length} target(s):`);
|
|
470
|
+
for (const t of targets)
|
|
471
|
+
console.log(` ${t.name}: ${t.skillsDir}`);
|
|
550
472
|
console.log('\nPress Ctrl+C to stop.\n');
|
|
551
473
|
let lastHash = '';
|
|
552
474
|
const sync = async () => {
|
|
553
475
|
try {
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
476
|
+
const fetched = watchRef
|
|
477
|
+
? await client.fetchSkills(watchRef)
|
|
478
|
+
: [generateMetaSkill(), ...((await client.fetchAllSkills()) || [])];
|
|
479
|
+
const currentHash = fetched
|
|
480
|
+
.map((s) => `${s.name}:${s.content.length}`)
|
|
481
|
+
.join('|');
|
|
482
|
+
if (currentHash === lastHash)
|
|
483
|
+
return;
|
|
484
|
+
const ts = new Date().toLocaleTimeString();
|
|
485
|
+
if (fetched.length === 0) {
|
|
486
|
+
console.log(`[${ts}] No skills available.`);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
let written = 0;
|
|
490
|
+
for (const target of targets) {
|
|
491
|
+
written += installSkills(fetched, target).installed;
|
|
567
492
|
}
|
|
493
|
+
// Only once the install actually happened. Stamping the hash
|
|
494
|
+
// first meant a throw inside installSkills logged one "Sync
|
|
495
|
+
// error" and then every later tick matched the hash and
|
|
496
|
+
// printed nothing -- indistinguishable from "up to date".
|
|
497
|
+
lastHash = currentHash;
|
|
498
|
+
console.log(`[${ts}] Synced ${written} skill file(s) to ${targets.length} target(s).`);
|
|
568
499
|
}
|
|
569
500
|
catch (err) {
|
|
570
501
|
const ts = new Date().toLocaleTimeString();
|
|
@@ -575,8 +506,8 @@ async function main() {
|
|
|
575
506
|
const interval = setInterval(sync, intervalSec * 1000);
|
|
576
507
|
const shutdown = () => {
|
|
577
508
|
clearInterval(interval);
|
|
578
|
-
console.log('\
|
|
579
|
-
process.exit(
|
|
509
|
+
console.log('\nStopped.');
|
|
510
|
+
process.exit(EXIT.OK);
|
|
580
511
|
};
|
|
581
512
|
process.on('SIGINT', shutdown);
|
|
582
513
|
process.on('SIGTERM', shutdown);
|
|
@@ -585,11 +516,12 @@ async function main() {
|
|
|
585
516
|
}
|
|
586
517
|
default:
|
|
587
518
|
console.error(`Unknown command: ${command}`);
|
|
588
|
-
|
|
589
|
-
process.exit(
|
|
519
|
+
console.error('Commands: install, list, search, run, installed, remove, gateways, daemon, watch. Run --help for detail.');
|
|
520
|
+
process.exit(EXIT.USAGE);
|
|
590
521
|
}
|
|
591
522
|
}
|
|
592
523
|
main().catch((err) => {
|
|
593
524
|
console.error(`Error: ${err.message}`);
|
|
594
|
-
|
|
525
|
+
const authFailure = /Authentication failed|\(401\)|API error 401/.test(err.message ?? '');
|
|
526
|
+
process.exit(authFailure ? EXIT.AUTH : EXIT.ERROR);
|
|
595
527
|
});
|