@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/dist/index.js CHANGED
@@ -1,182 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import { resolveAuth } from './auth.js';
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
- const VERSION = '1.0.12';
10
- function printHelp() {
11
- console.log(`
12
- almyty Skills CLI v${VERSION}
13
-
14
- Usage:
15
- npx @almyty/skills <command> [options]
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
- * 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
- }
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(1);
27
+ process.exit(EXIT.USAGE);
188
28
  }
189
29
  return ref;
190
30
  }
191
- function parseRunParams(args) {
192
- const params = {};
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))
199
- continue;
200
- params[key] = value;
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 params;
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(`Authentication moved to @almyty/auth.`);
87
+ console.error('Authentication moved to @almyty/auth.');
226
88
  console.error(` npx @almyty/auth ${command}`);
227
- process.exit(1);
89
+ process.exit(EXIT.USAGE);
228
90
  }
229
91
  case 'gateways': {
230
- const { url, token } = resolveAuth();
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 { url, token } = resolveAuth();
250
- const client = new AlmytyClient(urlOverride || url, token);
251
- if (!ref) {
252
- const allSkills = await client.fetchAllSkills();
253
- if (!allSkills || allSkills.length === 0) {
254
- console.log('No skills available. Assign tools to your gateways first.');
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
- else {
279
- const allSkills = await client.fetchAllSkills();
280
- if (!allSkills || allSkills.length === 0) {
281
- console.log('No skills available.');
282
- return;
283
- }
284
- console.log(`\n${allSkills.length} skills available:\n`);
285
- for (const skill of allSkills) {
286
- const label = skill.gateway ? `${skill.orgSlug}/${skill.gatewaySlug}/${skill.name}` : skill.name;
287
- const desc = skill.description ? ` — ${skill.description}` : '';
288
- console.log(` ${label}${desc}`);
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(1);
162
+ process.exit(EXIT.USAGE);
299
163
  }
300
- const { url, token } = resolveAuth();
301
- const client = new AlmytyClient(urlOverride || url, token);
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(`\nInstall: npx @almyty/skills install <ref>`);
314
- console.log(`Run: npx @almyty/skills run <ref>`);
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 { url, token } = resolveAuth();
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
- console.log('Fetching skills...');
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
- console.log('Fetching skill...');
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(1);
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
- console.log(`Searching for "${ref}"...`);
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
- console.error(`No skills found for "${ref}".`);
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?.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
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
- console.log(`\nMultiple matches for "${ref}":\n`);
373
- for (const r of results) {
374
- const label = r.skillRef || r.toolName;
375
- const desc = r.toolDescription ? ` — ${r.toolDescription}` : '';
376
- console.log(` ${label}${desc}`);
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
- console.log(`\nBe more specific: npx @almyty/skills install org/gateway/skill`);
379
- return;
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
- console.log('No skills found.');
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: !!args.flags.all,
393
- yes: !!args.flags.yes,
394
- global: !!args.flags.global,
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
- console.error('No install targets resolved. Pass --agent / --path / --all or run without flags in a TTY.');
403
- process.exit(1);
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} skills -> ${result.skillsDir}`);
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 { url, token } = resolveAuth();
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
- console.error(`No skill found for "${ref}".`);
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
- console.error('Error: run requires a skill reference (org/gateway/skill or skill-name)');
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
- break;
451
- }
452
- case 'daemon': {
453
- const intervalSec = parseInt(args.flags.interval, 10) || config.interval || 60;
454
- const { url, token } = resolveAuth();
455
- const client = new AlmytyClient(urlOverride || url, token);
456
- const targets = resolveTargets(projectDir, config);
457
- if (targets.length === 0) {
458
- console.error('No agent targets found.');
459
- process.exit(1);
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
- let totalFound = 0;
405
+ const found = [];
503
406
  for (const target of targets) {
504
407
  const installed = listInstalledSkills(target);
505
408
  if (installed.length > 0) {
506
- console.log(`\n${target.name} (${target.skillsDir}):`);
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 (totalFound === 0) {
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
- let totalRemoved = 0;
430
+ const removedPer = [];
522
431
  for (const target of targets) {
523
432
  const removed = removeSkills(target);
524
433
  if (removed > 0) {
525
- console.log(` Removed ${removed} skills from ${target.skillsDir}`);
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
- else {
533
- console.log(`\nRemoved ${totalRemoved} skill(s) total.`);
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 { url, token } = resolveAuth();
541
- const client = new AlmytyClient(urlOverride || url, token);
542
- const gateway = await client.fetchGateway(ref).catch(() => null);
543
- const gwName = gateway?.name || ref;
544
- const targets = getAllTargets(projectDir);
545
- console.log(`Watching ${gwName} (every ${intervalSec}s)`);
546
- console.log(`Syncing to ${targets.length} agent target(s):`);
547
- for (const t of targets) {
548
- console.log(` ${t.name}: ${t.skillsDir}`);
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 skills = await client.fetchSkills(ref);
555
- const currentHash = skills.map(s => `${s.name}:${s.content.length}`).join('|');
556
- if (currentHash !== lastHash) {
557
- lastHash = currentHash;
558
- const ts = new Date().toLocaleTimeString();
559
- if (skills.length === 0) {
560
- console.log(`[${ts}] No skills available.`);
561
- return;
562
- }
563
- for (const target of targets) {
564
- installSkills(skills, target);
565
- }
566
- console.log(`[${ts}] Synced ${skills.length} skills to ${targets.length} agent(s).`);
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('\nWatch stopped.');
579
- process.exit(0);
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
- printHelp();
589
- process.exit(1);
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
- process.exit(1);
525
+ const authFailure = /Authentication failed|\(401\)|API error 401/.test(err.message ?? '');
526
+ process.exit(authFailure ? EXIT.AUTH : EXIT.ERROR);
595
527
  });