@rayrun/cli 0.3.0 → 0.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.
package/src/management.js CHANGED
@@ -1,45 +1,115 @@
1
+ import {
2
+ parseSecretBindings,
3
+ readDeploymentDirectory,
4
+ readDeploymentProjectState,
5
+ writeDeploymentProjectState,
6
+ } from './deployments.js';
7
+ import { readSkillDirectory, writeSkillDirectory } from './skills.js';
1
8
  /* eslint-disable node/no-process-env -- management intentionally reads the invoking user's Rayrun environment */
2
9
  import { Rayrun, RayrunApiError } from '@rayrun/sdk';
10
+ import { diffLines } from 'diff';
11
+ import { createHash } from 'node:crypto';
12
+ import { readFile, writeFile } from 'node:fs/promises';
3
13
  import { stripVTControlCharacters } from 'node:util';
4
14
 
5
15
  export const managementUsage = `Management:
16
+ rayrun deploy [directory] [--connection <uid>] [--name <name>] [--secret <NAME=secret-uid> ...] [--wait] [--json]
17
+ rayrun deployments list [--connection <uid>] [--limit <1-100>] [--json]
18
+ rayrun deployments get <build-uid> [--json]
19
+ rayrun deployments logs <build-uid> [--json]
20
+ rayrun deployments releases <connection-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
21
+ rayrun deployments rollback <connection-uid> <release-uid> [--wait] [--json]
6
22
  rayrun connect mcp <url> --name <name> [--transport <streamable-http|sse>] [--json]
7
23
  rayrun connect openapi <spec-url> [--name <name>] [--base-url <url>] [--json]
8
24
  rayrun connections list [--limit <1-100>] [--cursor <cursor>] [--json]
25
+ rayrun connections history <connection-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
26
+ rayrun connections diff <connection-uid> <version-uid> [--json]
27
+ rayrun connections restore <connection-uid> <version-uid> [--reason <note>] [--json]
28
+ rayrun access-profiles list [--limit <1-100>] [--cursor <cursor>] [--json]
29
+ rayrun access-profiles history <profile-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
30
+ rayrun access-profiles diff <profile-uid> <version-uid> [--json]
31
+ rayrun access-profiles restore <profile-uid> <version-uid> [--reason <note>] [--confirm-risk] [--json]
32
+ rayrun webhooks list [--json]
33
+ rayrun webhooks update <webhook-uid> --config <json> [--reason <note>] [--json]
34
+ rayrun webhooks history <webhook-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
35
+ rayrun webhooks diff <webhook-uid> <version-uid> [--json]
36
+ rayrun webhooks restore <webhook-uid> <version-uid> [--reason <note>] [--json]
37
+ rayrun skills validate <directory> [--json]
38
+ rayrun skills push <directory> [--publish] [--confirm-risk] [--mode <both|code|direct>] [--profiles <uid,uid>|--all-clients] [--reason <note>] [--json]
39
+ rayrun skills list [--json]
40
+ rayrun skills show <skill-uid> [--json]
41
+ rayrun skills history <skill-uid> [--json]
42
+ rayrun skills diff <skill-uid> <version-uid> [--json]
43
+ rayrun skills pull <skill-uid> --output <new-directory> [--version <version-uid>] [--json]
44
+ rayrun skills publish <skill-uid> [--confirm-risk] [--mode <both|code|direct>] [--profiles <uid,uid>|--all-clients] [--reason <note>] [--json]
45
+ rayrun skills audience <skill-uid> [--mode <both|code|direct>] [--profiles <uid,uid>|--all-clients] [--json]
46
+ rayrun skills enable|disable|archive <skill-uid> [--json]
47
+ rayrun skills restore <skill-uid> <version-uid> [--reason <note>] [--json]
48
+ rayrun skills delete <skill-uid> --confirm-name <name> [--json]
9
49
  rayrun clients list [--limit <1-100>] [--cursor <cursor>] [--json]
10
50
  rayrun tools search <query> [--connection <uid>] [--limit <1-100>] [--cursor <cursor>] [--json]
11
51
  rayrun policy inspect <client-uid> [--query <query>] [--limit <1-100>] [--cursor <cursor>] [--json]
12
52
  rayrun approvals list [--limit <1-100>] [--cursor <cursor>] [--json]
13
53
  rayrun activity list [--limit <1-100>] [--cursor <cursor>] [--json]
54
+ rayrun hooks pull <connection-uid> <tool-uid> [--output <file>] [--types-output <file>] [--json]
55
+ rayrun hooks save <connection-uid> <tool-uid> [--file <file>] [--config <json>] [--reason <note>] [--json]
56
+ rayrun hooks test <connection-uid> <tool-uid> --arguments <json> [--file <file>] [--mock-result <json>] [--config <json>] [--json]
57
+ rayrun hooks deploy <connection-uid> <tool-uid> [--file <file>] [--config <json>] [--reason <note>] [--shadow] [--json]
58
+ rayrun hooks history <connection-uid> <tool-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
59
+ rayrun hooks diff <connection-uid> <tool-uid> <version-uid> [--json]
60
+ rayrun hooks restore <connection-uid> <tool-uid> <version-uid> [--reason <note>] [--json]
61
+ rayrun hooks reset <connection-uid> <tool-uid> --confirm <hook-uid>@<version> --reason <note>
62
+ rayrun hooks rollback <connection-uid> <tool-uid> <revision-uid> [--shadow] [--json]
63
+ rayrun hooks deactivate <connection-uid> <tool-uid> [--shadow] [--json]
64
+ rayrun hooks logs <connection-uid> <tool-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
14
65
 
15
66
  Environment:
16
67
  RAYRUN_API_KEY API key created in Dashboard -> Settings -> API keys
17
68
  RAYRUN_API_URL Optional API origin (defaults to https://ray.run)`;
18
69
 
19
70
  const managementCommands = new Set([
71
+ 'access-profiles',
20
72
  'activity',
21
73
  'approvals',
22
74
  'clients',
23
75
  'connect',
24
76
  'connections',
77
+ 'deploy',
78
+ 'deployments',
79
+ 'hooks',
25
80
  'policy',
81
+ 'skills',
26
82
  'tools',
83
+ 'webhooks',
27
84
  ]);
28
85
 
29
86
  export const isManagementCommand = (command) => managementCommands.has(command);
30
87
 
31
88
  const optionNames = new Map([
32
89
  ['--base-url', 'baseUrl'],
90
+ ['--arguments', 'argumentsJson'],
91
+ ['--config', 'configJson'],
92
+ ['--confirm', 'confirmationHookUid'],
33
93
  ['--connection', 'connectionUid'],
94
+ ['--confirm-name', 'confirmationName'],
34
95
  ['--cursor', 'cursor'],
35
96
  ['--limit', 'limit'],
97
+ ['--file', 'filePath'],
98
+ ['--mock-result', 'mockResultJson'],
36
99
  ['--name', 'name'],
100
+ ['--mode', 'mode'],
101
+ ['--profiles', 'profiles'],
37
102
  ['--query', 'query'],
103
+ ['--reason', 'reason'],
104
+ ['--output', 'outputPath'],
38
105
  ['--transport', 'transport'],
106
+ ['--secret', 'secret'],
107
+ ['--version', 'versionUid'],
108
+ ['--types-output', 'typesOutputPath'],
39
109
  ]);
40
110
 
41
111
  const parseCommandArguments = (arguments_, allowedOptions) => {
42
- const options = { json: false };
112
+ const options = { json: false, secrets: [] };
43
113
  const positional = [];
44
114
 
45
115
  for (let index = 0; index < arguments_.length; index += 1) {
@@ -50,14 +120,43 @@ const parseCommandArguments = (arguments_, allowedOptions) => {
50
120
  options.json = true;
51
121
  continue;
52
122
  }
123
+ if (argument === '--shadow') {
124
+ if (!allowedOptions.has('shadow')) throw new Error(`Unknown option: ${argument}`);
125
+ options.shadow = true;
126
+ continue;
127
+ }
128
+ if (argument === '--confirm-risk') {
129
+ if (!allowedOptions.has('confirmRisk')) throw new Error(`Unknown option: ${argument}`);
130
+ options.confirmRisk = true;
131
+ continue;
132
+ }
133
+ if (argument === '--publish') {
134
+ if (!allowedOptions.has('publish')) throw new Error(`Unknown option: ${argument}`);
135
+ options.publish = true;
136
+ continue;
137
+ }
138
+ if (argument === '--wait') {
139
+ if (!allowedOptions.has('wait')) throw new Error(`Unknown option: ${argument}`);
140
+ options.wait = true;
141
+ continue;
142
+ }
143
+ if (argument === '--all-clients') {
144
+ if (!allowedOptions.has('allClients')) throw new Error(`Unknown option: ${argument}`);
145
+ options.allClients = true;
146
+ continue;
147
+ }
53
148
 
54
149
  const optionName = optionNames.get(argument);
55
150
  if (optionName) {
56
151
  if (!allowedOptions.has(optionName)) throw new Error(`Unknown option: ${argument}`);
57
152
  const value = arguments_[index + 1];
58
153
  if (!value || value.startsWith('--')) throw new Error(`${argument} requires a value.`);
59
- if (options[optionName] !== undefined) throw new Error(`${argument} can only be used once.`);
60
- options[optionName] = value;
154
+ if (optionName === 'secret') options.secrets.push(value);
155
+ else {
156
+ if (options[optionName] !== undefined)
157
+ throw new Error(`${argument} can only be used once.`);
158
+ options[optionName] = value;
159
+ }
61
160
  index += 1;
62
161
  continue;
63
162
  }
@@ -104,12 +203,31 @@ const formatCell = (value) => {
104
203
  if (typeof value === 'boolean') return value ? 'yes' : 'no';
105
204
  const sanitized = stripVTControlCharacters(String(value))
106
205
  .replaceAll(/\p{Cc}+/gu, ' ')
206
+ .replaceAll(
207
+ /\p{Bidi_Control}/gu,
208
+ (character) => `\\u{${character.codePointAt(0).toString(16).toUpperCase()}}`,
209
+ )
107
210
  .replaceAll(/\s+/gu, ' ')
108
211
  .trim();
109
212
 
110
213
  return sanitized || '-';
111
214
  };
112
215
 
216
+ const formatTerminalText = (value) => {
217
+ return stripVTControlCharacters(String(value))
218
+ .replaceAll(/\r\n?/gu, '\n')
219
+ .split('\n')
220
+ .map((line) =>
221
+ line
222
+ .replaceAll(/\p{Cc}+/gu, ' ')
223
+ .replaceAll(
224
+ /\p{Bidi_Control}/gu,
225
+ (character) => `\\u{${character.codePointAt(0).toString(16).toUpperCase()}}`,
226
+ ),
227
+ )
228
+ .join('\n');
229
+ };
230
+
113
231
  export const renderTable = (items, columns) => {
114
232
  if (items.length === 0) return 'No results.\n';
115
233
 
@@ -150,10 +268,503 @@ const requireShape = (condition) => {
150
268
  if (!condition) throw new Error(managementUsage);
151
269
  };
152
270
 
271
+ const parseJsonOption = (value, option, fallback) => {
272
+ if (value === undefined) return fallback;
273
+
274
+ try {
275
+ return JSON.parse(value);
276
+ } catch {
277
+ throw new Error(`${option} must be valid JSON.`);
278
+ }
279
+ };
280
+
281
+ const hookMode = (options) => (options.shadow ? 'shadow' : 'active');
282
+
283
+ const deploymentFinished = (deployment) =>
284
+ ['cancelled', 'failed'].includes(deployment.build.status) ||
285
+ (deployment.release &&
286
+ ['active', 'cancelled', 'failed', 'superseded'].includes(deployment.release.status));
287
+
288
+ const waitForDeployment = async (client, { buildUid, connectionUid, releaseUid }) => {
289
+ const deadline = Date.now() + 60 * 60 * 1_000;
290
+ while (Date.now() < deadline) {
291
+ const deployment = await client.deployments.get(buildUid);
292
+ if (releaseUid) {
293
+ const exactRelease = await client.deployments.getRelease(connectionUid, releaseUid);
294
+ const exactDeployment = { ...deployment, release: exactRelease };
295
+ if (deploymentFinished(exactDeployment)) return exactDeployment;
296
+ } else if (deploymentFinished(deployment)) {
297
+ return deployment;
298
+ }
299
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
300
+ }
301
+ throw new Error(
302
+ 'Deployment is still running after 60 minutes. Inspect it with rayrun deployments get.',
303
+ );
304
+ };
305
+
306
+ const writeDeployment = ({ deployment, options, output }) => {
307
+ if (options.json) {
308
+ writeValue(output, deployment);
309
+ return;
310
+ }
311
+ const status = deployment.release?.status ?? deployment.build.status;
312
+ const toolSummary = deployment.release?.toolNames.map(formatCell).join(', ');
313
+ output.write(
314
+ `${formatCell(deployment.connection.displayName)}: ${formatCell(status)} (build ${formatCell(deployment.build.uid)}, source r${String(deployment.source.revision)})${toolSummary ? ` — ${toolSummary}` : ''}\n`,
315
+ );
316
+ };
317
+
318
+ const renderLineDiff = (before, after, beforeLabel, afterLabel) => {
319
+ const parts = diffLines(before, after, { timeout: 75 });
320
+ if (!parts) {
321
+ return `--- ${beforeLabel}\n+++ ${afterLabel}\nDiff exceeded the 75 ms safety limit. Use --json to inspect both versions.\n`;
322
+ }
323
+
324
+ const lines = parts
325
+ .flatMap((part) => {
326
+ const prefix = part.added ? '+' : part.removed ? '-' : ' ';
327
+ const partLines = stripVTControlCharacters(part.value)
328
+ .split('\n')
329
+ .map((line) =>
330
+ line
331
+ .replaceAll(/\p{Cc}+/gu, ' ')
332
+ .replaceAll(
333
+ /\p{Bidi_Control}/gu,
334
+ (character) => `\\u{${character.codePointAt(0).toString(16).toUpperCase()}}`,
335
+ ),
336
+ );
337
+
338
+ if (partLines.at(-1) === '') partLines.pop();
339
+
340
+ return partLines.map((line) => `${prefix}${line}`);
341
+ })
342
+ .join('\n');
343
+
344
+ return `--- ${beforeLabel}\n+++ ${afterLabel}\n${lines}\n`;
345
+ };
346
+
347
+ const skillFileSummary = (files) => {
348
+ return files.map((file) => {
349
+ const bytes = Buffer.from(file.contentBase64, 'base64');
350
+ return {
351
+ digest: `sha256:${createHash('sha256').update(bytes).digest('hex')}`,
352
+ path: file.path,
353
+ size: bytes.byteLength,
354
+ };
355
+ });
356
+ };
357
+
358
+ const skillAudience = (options, fallback) => {
359
+ if (options.allClients && options.profiles !== undefined) {
360
+ throw new Error('Use either --profiles or --all-clients, not both.');
361
+ }
362
+ const deliveryMode = options.mode ?? fallback?.deliveryMode ?? 'both';
363
+ if (!['both', 'code', 'direct'].includes(deliveryMode)) {
364
+ throw new Error('--mode must be both, code, or direct.');
365
+ }
366
+ let profileUids;
367
+ if (options.allClients) {
368
+ profileUids = [];
369
+ } else if (options.profiles !== undefined) {
370
+ profileUids = options.profiles
371
+ .split(',')
372
+ .map((uid) => uid.trim())
373
+ .filter(Boolean);
374
+ if (profileUids.length === 0) {
375
+ throw new Error('--profiles requires at least one profile UID.');
376
+ }
377
+ } else if (fallback) {
378
+ profileUids = fallback.profileUids;
379
+ } else {
380
+ throw new Error('Choose an audience with --profiles <uid,uid> or --all-clients.');
381
+ }
382
+ return {
383
+ deliveryMode,
384
+ profileUids,
385
+ };
386
+ };
387
+
153
388
  const runCommand = async ({ arguments_, client, output }) => {
154
389
  const command = arguments_[0];
155
390
  const action = arguments_[1];
156
391
 
392
+ if (command === 'deploy') {
393
+ const { options, positional } = parseCommandArguments(
394
+ arguments_.slice(1),
395
+ new Set(['connectionUid', 'json', 'name', 'secret', 'wait']),
396
+ );
397
+ requireShape(positional.length <= 1);
398
+ const directory = positional[0] ?? '.';
399
+ const state = options.connectionUid ? null : await readDeploymentProjectState(directory);
400
+ const connectionUid = options.connectionUid ?? state?.connectionUid;
401
+ if (connectionUid && options.name) {
402
+ throw new Error('--name is only available when creating a source deployment.');
403
+ }
404
+ const files = await readDeploymentDirectory(directory);
405
+ await client.deployments.validate(files);
406
+ const created = await client.deployments.create({
407
+ connectionUid,
408
+ displayName: options.name,
409
+ files,
410
+ secretBindings: parseSecretBindings(options.secrets),
411
+ });
412
+ await writeDeploymentProjectState(directory, created.connectionUid);
413
+ if (options.wait) {
414
+ const deployment = await waitForDeployment(client, created);
415
+ writeDeployment({ deployment, options, output });
416
+ if (
417
+ ['cancelled', 'failed'].includes(deployment.build.status) ||
418
+ (deployment.release &&
419
+ ['cancelled', 'failed', 'superseded'].includes(deployment.release.status))
420
+ ) {
421
+ throw new Error(
422
+ `Deployment ${created.buildUid} failed. Run rayrun deployments logs ${created.buildUid}.`,
423
+ );
424
+ }
425
+ } else if (options.json) writeValue(output, created);
426
+ else {
427
+ output.write(
428
+ created.unchanged
429
+ ? `Already active as build ${created.buildUid}.\n`
430
+ : `Queued build ${created.buildUid}. Wait with: rayrun deployments get ${created.buildUid}\n`,
431
+ );
432
+ }
433
+ return;
434
+ }
435
+
436
+ if (command === 'deployments' && action === 'list') {
437
+ const { options, positional } = parseCommandArguments(
438
+ arguments_.slice(2),
439
+ new Set(['connectionUid', 'json', 'limit']),
440
+ );
441
+ requireShape(positional.length === 0);
442
+ const response = await client.deployments.list({
443
+ connectionUid: options.connectionUid,
444
+ limit: options.limit,
445
+ });
446
+ if (options.json) writeValue(output, response);
447
+ else {
448
+ output.write(
449
+ renderTable(response.items, [
450
+ { label: 'BUILD', value: (item) => item.build.uid },
451
+ { label: 'SERVICE', value: (item) => item.connection.displayName },
452
+ { label: 'SOURCE', value: (item) => `r${String(item.source.revision)}` },
453
+ { label: 'STATUS', value: (item) => item.release?.status ?? item.build.status },
454
+ { label: 'CREATED', value: (item) => item.build.createdAt },
455
+ ]),
456
+ );
457
+ }
458
+ return;
459
+ }
460
+
461
+ if (command === 'deployments' && action === 'get') {
462
+ const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
463
+ requireShape(positional.length === 1);
464
+ writeDeployment({ deployment: await client.deployments.get(positional[0]), options, output });
465
+ return;
466
+ }
467
+
468
+ if (command === 'deployments' && action === 'logs') {
469
+ const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
470
+ requireShape(positional.length === 1);
471
+ const buildLog = await client.deployments.getLog(positional[0]);
472
+ if (options.json) writeValue(output, buildLog);
473
+ else
474
+ output.write(
475
+ `${buildLog.truncated ? '[older output truncated]\n' : ''}${formatTerminalText(buildLog.text)}\n`,
476
+ );
477
+ return;
478
+ }
479
+
480
+ if (command === 'deployments' && action === 'releases') {
481
+ const { options, positional } = parseCommandArguments(
482
+ arguments_.slice(2),
483
+ new Set(['cursor', 'json', 'limit']),
484
+ );
485
+ requireShape(positional.length === 1);
486
+ const response = await client.deployments.listReleases(positional[0], pageQuery(options));
487
+ writePage({
488
+ columns: [
489
+ { label: 'RELEASE', value: (item) => item.uid },
490
+ { label: 'NUMBER', value: (item) => item.number },
491
+ { label: 'SOURCE', value: (item) => `r${String(item.sourceRevision)}` },
492
+ { label: 'STATUS', value: (item) => item.status },
493
+ { label: 'ACTIVATED', value: (item) => item.activatedAt },
494
+ ],
495
+ options,
496
+ output,
497
+ page: response,
498
+ });
499
+ return;
500
+ }
501
+
502
+ if (command === 'deployments' && action === 'rollback') {
503
+ const { options, positional } = parseCommandArguments(
504
+ arguments_.slice(2),
505
+ new Set(['json', 'wait']),
506
+ );
507
+ requireShape(positional.length === 2);
508
+ const created = await client.deployments.rollback(positional[0], positional[1]);
509
+ if (options.wait) {
510
+ const deadline = Date.now() + 20 * 60 * 1_000;
511
+ let release;
512
+ while (Date.now() < deadline) {
513
+ release = await client.deployments.getRelease(positional[0], created.releaseUid);
514
+ if (['active', 'cancelled', 'failed', 'superseded'].includes(release.status)) break;
515
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
516
+ }
517
+ if (!release || !['active', 'cancelled', 'failed', 'superseded'].includes(release.status)) {
518
+ throw new Error('Rollback is still running after 20 minutes.');
519
+ }
520
+ if (options.json) writeValue(output, release);
521
+ else output.write(`Release ${release.uid}: ${release.status}.\n`);
522
+ if (release.status !== 'active') throw new Error(`Rollback ${release.uid} failed.`);
523
+ } else if (options.json) writeValue(output, created);
524
+ else output.write(`Queued rollback release ${created.releaseUid}.\n`);
525
+ return;
526
+ }
527
+
528
+ if (command === 'skills' && action === 'validate') {
529
+ const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
530
+ requireShape(positional.length === 1);
531
+ const result = await client.skills.validate(await readSkillDirectory(positional[0]));
532
+ if (options.json) writeValue(output, result);
533
+ else {
534
+ output.write(
535
+ `Valid ${result.name}: ${String(result.fileCount)} files, ${String(result.byteCount)} bytes${result.riskFlags.length ? `; review ${result.riskFlags.join(', ')}` : ''}.\n`,
536
+ );
537
+ }
538
+ return;
539
+ }
540
+
541
+ if (command === 'skills' && action === 'push') {
542
+ const { options, positional } = parseCommandArguments(
543
+ arguments_.slice(2),
544
+ new Set(['allClients', 'confirmRisk', 'json', 'mode', 'profiles', 'publish', 'reason']),
545
+ );
546
+ requireShape(positional.length === 1);
547
+ const files = await readSkillDirectory(positional[0]);
548
+ const validation = await client.skills.validate(files);
549
+ const listed = await client.skills.list();
550
+ const existing = listed.items.find((item) => item.name === validation.name);
551
+ const audience = options.publish
552
+ ? skillAudience(
553
+ options,
554
+ existing
555
+ ? {
556
+ deliveryMode: existing.deliveryMode,
557
+ profileUids: existing.profiles.map((profile) => profile.uid),
558
+ }
559
+ : undefined,
560
+ )
561
+ : undefined;
562
+ let result = existing
563
+ ? await client.skills.saveDraft(existing.uid, {
564
+ changeReason: options.reason,
565
+ expectedVersion: existing.configurationVersion,
566
+ files,
567
+ source: 'cli',
568
+ })
569
+ : await client.skills.create({ changeReason: options.reason, files, source: 'cli' });
570
+ if (options.publish) {
571
+ result = await client.skills.publish(result.skillUid, {
572
+ ...audience,
573
+ changeReason: options.reason,
574
+ expectedVersion: result.configurationVersion,
575
+ riskConfirmed: options.confirmRisk,
576
+ });
577
+ }
578
+ if (options.json) writeValue(output, result);
579
+ else {
580
+ output.write(
581
+ `${options.publish ? 'Published' : result.unchanged ? 'Unchanged' : 'Saved'} ${validation.name} (${result.skillUid}) at configuration v${String(result.configurationVersion)}.\n`,
582
+ );
583
+ }
584
+ return;
585
+ }
586
+
587
+ if (command === 'skills' && action === 'list') {
588
+ const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
589
+ requireShape(positional.length === 0);
590
+ const result = await client.skills.list();
591
+ if (options.json) writeValue(output, result);
592
+ else {
593
+ output.write(
594
+ renderTable(result.items, [
595
+ { label: 'UID', value: (item) => item.uid },
596
+ { label: 'NAME', value: (item) => item.name },
597
+ { label: 'STATE', value: (item) => item.state },
598
+ { label: 'REVISION', value: (item) => item.publishedRevisionNumber },
599
+ { label: 'MODE', value: (item) => item.deliveryMode },
600
+ { label: 'FETCHES', value: (item) => item.fetchCount },
601
+ ]),
602
+ );
603
+ }
604
+ return;
605
+ }
606
+
607
+ if (command === 'skills' && ['history', 'show'].includes(action)) {
608
+ const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
609
+ requireShape(positional.length === 1);
610
+ const detail = await client.skills.get(positional[0]);
611
+ if (options.json || action === 'show') writeValue(output, detail);
612
+ else {
613
+ output.write(
614
+ renderTable(detail.versions, [
615
+ { label: 'REVISION', value: (item) => item.revisionNumber },
616
+ { label: 'UID', value: (item) => item.uid },
617
+ { label: 'WHEN', value: (item) => item.createdAt },
618
+ { label: 'ACTOR', value: (item) => item.actorName },
619
+ { label: 'DRAFT', value: (item) => item.isCurrentDraft },
620
+ { label: 'PUBLISHED', value: (item) => item.isPublished },
621
+ { label: 'NOTE', value: (item) => item.changeReason },
622
+ ]),
623
+ );
624
+ }
625
+ return;
626
+ }
627
+
628
+ if (command === 'skills' && action === 'diff') {
629
+ const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
630
+ requireShape(positional.length === 2);
631
+ const [draft, version] = await Promise.all([
632
+ client.skills.export(positional[0]),
633
+ client.skills.exportVersion(positional[0], positional[1]),
634
+ ]);
635
+ const comparison = {
636
+ draft: skillFileSummary(draft.files),
637
+ version: skillFileSummary(version.files),
638
+ };
639
+ if (options.json) writeValue(output, comparison);
640
+ else {
641
+ output.write(
642
+ renderLineDiff(
643
+ `${JSON.stringify(comparison.version, null, 2)}\n`,
644
+ `${JSON.stringify(comparison.draft, null, 2)}\n`,
645
+ positional[1],
646
+ 'current draft',
647
+ ),
648
+ );
649
+ }
650
+ return;
651
+ }
652
+
653
+ if (command === 'skills' && action === 'pull') {
654
+ const { options, positional } = parseCommandArguments(
655
+ arguments_.slice(2),
656
+ new Set(['json', 'outputPath', 'versionUid']),
657
+ );
658
+ requireShape(positional.length === 1 && typeof options.outputPath === 'string');
659
+ const result = options.versionUid
660
+ ? await client.skills.exportVersion(positional[0], options.versionUid)
661
+ : await client.skills.export(positional[0]);
662
+ await writeSkillDirectory(options.outputPath, result.files);
663
+ if (options.json)
664
+ writeValue(output, { fileCount: result.files.length, output: options.outputPath });
665
+ else output.write(`Wrote ${String(result.files.length)} files to ${options.outputPath}.\n`);
666
+ return;
667
+ }
668
+
669
+ if (command === 'skills' && action === 'publish') {
670
+ const { options, positional } = parseCommandArguments(
671
+ arguments_.slice(2),
672
+ new Set(['allClients', 'confirmRisk', 'json', 'mode', 'profiles', 'reason']),
673
+ );
674
+ requireShape(positional.length === 1);
675
+ const detail = await client.skills.get(positional[0]);
676
+ const result = await client.skills.publish(positional[0], {
677
+ ...skillAudience(options, {
678
+ deliveryMode: detail.skill.deliveryMode,
679
+ profileUids: detail.skill.profiles.map((profile) => profile.uid),
680
+ }),
681
+ changeReason: options.reason,
682
+ expectedVersion: detail.skill.configurationVersion,
683
+ riskConfirmed: options.confirmRisk,
684
+ });
685
+ if (options.json) writeValue(output, result);
686
+ else
687
+ output.write(
688
+ `Published ${detail.skill.name} at configuration v${String(result.configurationVersion)}.\n`,
689
+ );
690
+ return;
691
+ }
692
+
693
+ if (command === 'skills' && action === 'audience') {
694
+ const { options, positional } = parseCommandArguments(
695
+ arguments_.slice(2),
696
+ new Set(['allClients', 'json', 'mode', 'profiles']),
697
+ );
698
+ requireShape(positional.length === 1);
699
+ const detail = await client.skills.get(positional[0]);
700
+ const result = await client.skills.updateAudience(positional[0], {
701
+ ...skillAudience(options, {
702
+ deliveryMode: detail.skill.deliveryMode,
703
+ profileUids: detail.skill.profiles.map((profile) => profile.uid),
704
+ }),
705
+ expectedVersion: detail.skill.configurationVersion,
706
+ });
707
+ if (options.json) writeValue(output, result);
708
+ else {
709
+ output.write(
710
+ `${result.unchanged ? 'Kept' : 'Updated'} audience for ${detail.skill.name} at configuration v${String(result.configurationVersion)}.\n`,
711
+ );
712
+ }
713
+ return;
714
+ }
715
+
716
+ if (command === 'skills' && ['archive', 'disable', 'enable'].includes(action)) {
717
+ const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
718
+ requireShape(positional.length === 1);
719
+ const detail = await client.skills.get(positional[0]);
720
+ const result =
721
+ action === 'archive'
722
+ ? await client.skills.archive(positional[0], detail.skill.configurationVersion)
723
+ : await client.skills.setEnabled(
724
+ positional[0],
725
+ action === 'enable',
726
+ detail.skill.configurationVersion,
727
+ );
728
+ if (options.json) writeValue(output, result);
729
+ else output.write(`${action[0].toUpperCase()}${action.slice(1)}d ${detail.skill.name}.\n`);
730
+ return;
731
+ }
732
+
733
+ if (command === 'skills' && action === 'restore') {
734
+ const { options, positional } = parseCommandArguments(
735
+ arguments_.slice(2),
736
+ new Set(['json', 'reason']),
737
+ );
738
+ requireShape(positional.length === 2);
739
+ const detail = await client.skills.get(positional[0]);
740
+ const result = await client.skills.restoreVersion(positional[0], positional[1], {
741
+ changeReason: options.reason,
742
+ expectedVersion: detail.skill.configurationVersion,
743
+ });
744
+ if (options.json) writeValue(output, result);
745
+ else output.write(`Restored ${positional[1]} as a new draft.\n`);
746
+ return;
747
+ }
748
+
749
+ if (command === 'skills' && action === 'delete') {
750
+ const { options, positional } = parseCommandArguments(
751
+ arguments_.slice(2),
752
+ new Set(['confirmationName', 'json']),
753
+ );
754
+ requireShape(positional.length === 1 && typeof options.confirmationName === 'string');
755
+ const detail = await client.skills.get(positional[0]);
756
+ const result = await client.skills.delete(positional[0], {
757
+ confirmationName: options.confirmationName,
758
+ expectedVersion: detail.skill.configurationVersion,
759
+ });
760
+ if (options.json) writeValue(output, result);
761
+ else
762
+ output.write(
763
+ `Permanently deleted content for ${detail.skill.name}; its name remains reserved.\n`,
764
+ );
765
+ return;
766
+ }
767
+
157
768
  if (command === 'connect' && action === 'mcp') {
158
769
  const { options, positional } = parseCommandArguments(
159
770
  arguments_.slice(2),
@@ -213,6 +824,306 @@ const runCommand = async ({ arguments_, client, output }) => {
213
824
  return;
214
825
  }
215
826
 
827
+ if (command === 'connections' && action === 'history') {
828
+ const { options, positional } = parseCommandArguments(
829
+ arguments_.slice(2),
830
+ new Set(['cursor', 'json', 'limit']),
831
+ );
832
+ requireShape(positional.length === 1);
833
+ const page = await client.connections.listConfigurationVersions(
834
+ positional[0],
835
+ pageQuery(options),
836
+ );
837
+ writePage({
838
+ columns: [
839
+ { label: 'VERSION', value: (item) => item.revisionNumber },
840
+ { label: 'UID', value: (item) => item.uid },
841
+ { label: 'WHEN', value: (item) => item.createdAt },
842
+ { label: 'ACTOR', value: (item) => `${item.actor.name} (${item.actor.type})` },
843
+ { label: 'CHANGE', value: (item) => item.changeType },
844
+ { label: 'NOTE', value: (item) => item.changeReason },
845
+ { label: 'CURRENT', value: (item) => item.isCurrent },
846
+ ],
847
+ options,
848
+ output,
849
+ page,
850
+ });
851
+ return;
852
+ }
853
+
854
+ if (command === 'connections' && action === 'diff') {
855
+ const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
856
+ requireShape(positional.length === 2);
857
+ const result = await client.connections.getConfigurationVersion(positional[0], positional[1]);
858
+ if (options.json) {
859
+ writeValue(output, result);
860
+ return;
861
+ }
862
+ const previousLabel = result.version.previous
863
+ ? `configuration v${String(result.version.previous.revisionNumber)}`
864
+ : 'empty configuration';
865
+ output.write(
866
+ renderLineDiff(
867
+ `${JSON.stringify(result.version.previous?.snapshot ?? {}, null, 2)}\n`,
868
+ `${JSON.stringify(result.version.snapshot, null, 2)}\n`,
869
+ previousLabel,
870
+ `configuration v${String(result.version.revisionNumber)}`,
871
+ ),
872
+ );
873
+ return;
874
+ }
875
+
876
+ if (command === 'connections' && action === 'restore') {
877
+ const { options, positional } = parseCommandArguments(
878
+ arguments_.slice(2),
879
+ new Set(['json', 'reason']),
880
+ );
881
+ requireShape(positional.length === 2);
882
+ const history = await client.connections.listConfigurationVersions(positional[0], { limit: 1 });
883
+ const current = history.items[0];
884
+ if (!current?.isCurrent) {
885
+ throw new Error('The current connection configuration could not be read.');
886
+ }
887
+ const result = await client.connections.restoreConfigurationVersion(
888
+ positional[0],
889
+ positional[1],
890
+ { expectedVersion: current.revisionNumber, reason: options.reason },
891
+ );
892
+ if (options.json) writeValue(output, result);
893
+ else
894
+ output.write(
895
+ `Restored ${positional[1]} as connection configuration v${String(result.configurationVersion)} (${result.versionUid}).\n`,
896
+ );
897
+ return;
898
+ }
899
+
900
+ if (command === 'access-profiles' && action === 'list') {
901
+ const { options, positional } = parseCommandArguments(
902
+ arguments_.slice(2),
903
+ new Set(['cursor', 'json', 'limit']),
904
+ );
905
+ requireShape(positional.length === 0);
906
+ const page = await client.accessProfiles.list(pageQuery(options));
907
+ writePage({
908
+ columns: [
909
+ { label: 'UID', value: (item) => item.uid },
910
+ { label: 'NAME', value: (item) => item.name },
911
+ { label: 'POLICY', value: (item) => item.toolAccessMode },
912
+ { label: 'VERSION', value: (item) => item.toolPolicyVersion },
913
+ { label: 'CLIENTS', value: (item) => item.assignedClientCount },
914
+ { label: 'SKILLS', value: (item) => item.assignedSkillCount },
915
+ ],
916
+ options,
917
+ output,
918
+ page,
919
+ });
920
+ return;
921
+ }
922
+
923
+ if (command === 'access-profiles' && action === 'history') {
924
+ const { options, positional } = parseCommandArguments(
925
+ arguments_.slice(2),
926
+ new Set(['cursor', 'json', 'limit']),
927
+ );
928
+ requireShape(positional.length === 1);
929
+ const page = await client.accessProfiles.listConfigurationVersions(
930
+ positional[0],
931
+ pageQuery(options),
932
+ );
933
+ writePage({
934
+ columns: [
935
+ { label: 'VERSION', value: (item) => item.revisionNumber },
936
+ { label: 'UID', value: (item) => item.uid },
937
+ { label: 'WHEN', value: (item) => item.createdAt },
938
+ { label: 'ACTOR', value: (item) => `${item.actor.name} (${item.actor.type})` },
939
+ { label: 'CHANGE', value: (item) => item.changeType },
940
+ { label: 'NOTE', value: (item) => item.changeReason },
941
+ { label: 'CURRENT', value: (item) => item.isCurrent },
942
+ ],
943
+ options,
944
+ output,
945
+ page,
946
+ });
947
+ return;
948
+ }
949
+
950
+ if (command === 'access-profiles' && action === 'diff') {
951
+ const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
952
+ requireShape(positional.length === 2);
953
+ const result = await client.accessProfiles.getConfigurationVersion(
954
+ positional[0],
955
+ positional[1],
956
+ );
957
+ if (options.json) {
958
+ writeValue(output, result);
959
+ return;
960
+ }
961
+ const previousLabel = result.version.previous
962
+ ? `profile v${String(result.version.previous.revisionNumber)}`
963
+ : 'empty profile';
964
+ output.write(
965
+ renderLineDiff(
966
+ `${JSON.stringify(result.version.previous?.snapshot ?? {}, null, 2)}\n`,
967
+ `${JSON.stringify(result.version.snapshot, null, 2)}\n`,
968
+ previousLabel,
969
+ `profile v${String(result.version.revisionNumber)}`,
970
+ ),
971
+ );
972
+ return;
973
+ }
974
+
975
+ if (command === 'access-profiles' && action === 'restore') {
976
+ const { options, positional } = parseCommandArguments(
977
+ arguments_.slice(2),
978
+ new Set(['confirmRisk', 'json', 'reason']),
979
+ );
980
+ requireShape(positional.length === 2);
981
+ const history = await client.accessProfiles.listConfigurationVersions(positional[0], {
982
+ limit: 1,
983
+ });
984
+ const current = history.items[0];
985
+ if (!current?.isCurrent) {
986
+ throw new Error('The current access-profile configuration could not be read.');
987
+ }
988
+ const result = await client.accessProfiles.restoreConfigurationVersion(
989
+ positional[0],
990
+ positional[1],
991
+ {
992
+ expectedVersion: current.revisionNumber,
993
+ reason: options.reason,
994
+ riskConfirmed: options.confirmRisk === true,
995
+ },
996
+ );
997
+ if (options.json) writeValue(output, result);
998
+ else
999
+ output.write(
1000
+ `Restored ${positional[1]} as access-profile configuration v${String(result.toolPolicyVersion)} (${result.versionUid}).\n`,
1001
+ );
1002
+ return;
1003
+ }
1004
+
1005
+ if (command === 'webhooks' && action === 'list') {
1006
+ const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
1007
+ requireShape(positional.length === 0);
1008
+ const result = await client.webhooks.list();
1009
+ if (options.json) {
1010
+ writeValue(output, result);
1011
+ return;
1012
+ }
1013
+ output.write(
1014
+ renderTable(result.items, [
1015
+ { label: 'UID', value: (item) => item.uid },
1016
+ { label: 'DESCRIPTION', value: (item) => item.description },
1017
+ { label: 'DESTINATION', value: (item) => item.url },
1018
+ { label: 'ENABLED', value: (item) => item.enabled },
1019
+ { label: 'VERSION', value: (item) => item.configurationVersion },
1020
+ { label: 'HEALTH', value: (item) => item.health },
1021
+ ]),
1022
+ );
1023
+ return;
1024
+ }
1025
+
1026
+ if (command === 'webhooks' && action === 'update') {
1027
+ const { options, positional } = parseCommandArguments(
1028
+ arguments_.slice(2),
1029
+ new Set(['configJson', 'json', 'reason']),
1030
+ );
1031
+ requireShape(positional.length === 1 && options.configJson !== undefined);
1032
+ const configuration = parseJsonOption(options.configJson, '--config');
1033
+ if (!configuration || typeof configuration !== 'object' || Array.isArray(configuration)) {
1034
+ throw new Error('--config must be a JSON object.');
1035
+ }
1036
+ const listed = await client.webhooks.list();
1037
+ const current = listed.items.find((webhook) => webhook.uid === positional[0]);
1038
+ if (!current) {
1039
+ throw new Error(`Webhook ${positional[0]} was not found.`);
1040
+ }
1041
+ const result = await client.webhooks.update(positional[0], {
1042
+ ...configuration,
1043
+ expectedVersion: current.configurationVersion,
1044
+ reason: options.reason,
1045
+ });
1046
+ if (options.json) writeValue(output, result);
1047
+ else if (result.unchanged)
1048
+ output.write(
1049
+ `No changes; webhook configuration v${String(result.configurationVersion)} is current.\n`,
1050
+ );
1051
+ else
1052
+ output.write(
1053
+ `Saved webhook configuration v${String(result.configurationVersion)} (${result.versionUid}).\n`,
1054
+ );
1055
+ return;
1056
+ }
1057
+
1058
+ if (command === 'webhooks' && action === 'history') {
1059
+ const { options, positional } = parseCommandArguments(
1060
+ arguments_.slice(2),
1061
+ new Set(['cursor', 'json', 'limit']),
1062
+ );
1063
+ requireShape(positional.length === 1);
1064
+ const page = await client.webhooks.listConfigurationVersions(positional[0], pageQuery(options));
1065
+ writePage({
1066
+ columns: [
1067
+ { label: 'VERSION', value: (item) => item.revisionNumber },
1068
+ { label: 'UID', value: (item) => item.uid },
1069
+ { label: 'WHEN', value: (item) => item.createdAt },
1070
+ { label: 'ACTOR', value: (item) => `${item.actor.name} (${item.actor.type})` },
1071
+ { label: 'CHANGE', value: (item) => item.changeType },
1072
+ { label: 'NOTE', value: (item) => item.changeReason },
1073
+ { label: 'CURRENT', value: (item) => item.isCurrent },
1074
+ ],
1075
+ options,
1076
+ output,
1077
+ page,
1078
+ });
1079
+ return;
1080
+ }
1081
+
1082
+ if (command === 'webhooks' && action === 'diff') {
1083
+ const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
1084
+ requireShape(positional.length === 2);
1085
+ const result = await client.webhooks.getConfigurationVersion(positional[0], positional[1]);
1086
+ if (options.json) {
1087
+ writeValue(output, result);
1088
+ return;
1089
+ }
1090
+ const previousLabel = result.version.previous
1091
+ ? `webhook v${String(result.version.previous.revisionNumber)}`
1092
+ : 'empty webhook configuration';
1093
+ output.write(
1094
+ renderLineDiff(
1095
+ `${JSON.stringify(result.version.previous?.snapshot ?? {}, null, 2)}\n`,
1096
+ `${JSON.stringify(result.version.snapshot, null, 2)}\n`,
1097
+ previousLabel,
1098
+ `webhook v${String(result.version.revisionNumber)}`,
1099
+ ),
1100
+ );
1101
+ return;
1102
+ }
1103
+
1104
+ if (command === 'webhooks' && action === 'restore') {
1105
+ const { options, positional } = parseCommandArguments(
1106
+ arguments_.slice(2),
1107
+ new Set(['json', 'reason']),
1108
+ );
1109
+ requireShape(positional.length === 2);
1110
+ const history = await client.webhooks.listConfigurationVersions(positional[0], { limit: 1 });
1111
+ const current = history.items[0];
1112
+ if (!current?.isCurrent) {
1113
+ throw new Error('The current webhook configuration could not be read.');
1114
+ }
1115
+ const result = await client.webhooks.restoreConfigurationVersion(positional[0], positional[1], {
1116
+ expectedVersion: current.revisionNumber,
1117
+ reason: options.reason,
1118
+ });
1119
+ if (options.json) writeValue(output, result);
1120
+ else
1121
+ output.write(
1122
+ `Restored ${positional[1]} as webhook configuration v${String(result.configurationVersion)} (${result.versionUid}).\n`,
1123
+ );
1124
+ return;
1125
+ }
1126
+
216
1127
  if (command === 'clients' && action === 'list') {
217
1128
  const { options, positional } = parseCommandArguments(
218
1129
  arguments_.slice(2),
@@ -345,6 +1256,274 @@ const runCommand = async ({ arguments_, client, output }) => {
345
1256
  return;
346
1257
  }
347
1258
 
1259
+ if (command === 'hooks' && action === 'pull') {
1260
+ const { options, positional } = parseCommandArguments(
1261
+ arguments_.slice(2),
1262
+ new Set(['json', 'outputPath', 'typesOutputPath']),
1263
+ );
1264
+ requireShape(positional.length === 2);
1265
+ const { hook } = await client.hooks.get(positional[0], positional[1]);
1266
+
1267
+ if (options.outputPath) await writeFile(options.outputPath, hook.draftSource, 'utf8');
1268
+ if (options.typesOutputPath) await writeFile(options.typesOutputPath, hook.types, 'utf8');
1269
+ if (options.json) writeValue(output, { hook });
1270
+ else if (!options.outputPath) output.write(hook.draftSource);
1271
+ else output.write(`Wrote hook source to ${options.outputPath}.\n`);
1272
+ return;
1273
+ }
1274
+
1275
+ if (command === 'hooks' && action === 'test') {
1276
+ const { options, positional } = parseCommandArguments(
1277
+ arguments_.slice(2),
1278
+ new Set(['argumentsJson', 'configJson', 'filePath', 'json', 'mockResultJson']),
1279
+ );
1280
+ requireShape(positional.length === 2 && options.argumentsJson !== undefined);
1281
+ const { hook } = await client.hooks.get(positional[0], positional[1]);
1282
+ const source = options.filePath ? await readFile(options.filePath, 'utf8') : hook.draftSource;
1283
+ const body = {
1284
+ arguments: parseJsonOption(options.argumentsJson, '--arguments'),
1285
+ config: parseJsonOption(options.configJson, '--config', hook.draftConfig),
1286
+ source,
1287
+ ...(options.mockResultJson === undefined
1288
+ ? {}
1289
+ : { mockResult: parseJsonOption(options.mockResultJson, '--mock-result') }),
1290
+ };
1291
+ const result = await client.hooks.test(positional[0], positional[1], body);
1292
+ writeValue(output, result);
1293
+ return;
1294
+ }
1295
+
1296
+ if (command === 'hooks' && action === 'save') {
1297
+ const { options, positional } = parseCommandArguments(
1298
+ arguments_.slice(2),
1299
+ new Set(['configJson', 'filePath', 'json', 'reason']),
1300
+ );
1301
+ requireShape(positional.length === 2);
1302
+ const { hook } = await client.hooks.get(positional[0], positional[1]);
1303
+ const source = options.filePath ? await readFile(options.filePath, 'utf8') : hook.draftSource;
1304
+ const result = await client.hooks.saveDraft(positional[0], positional[1], {
1305
+ config: parseJsonOption(options.configJson, '--config', hook.draftConfig),
1306
+ expectedVersion: hook.version,
1307
+ reason: options.reason,
1308
+ source,
1309
+ });
1310
+
1311
+ if (options.json) writeValue(output, result);
1312
+ else if (result.draftVersion.unchanged)
1313
+ output.write(
1314
+ `No changes; draft v${String(result.draftVersion.revisionNumber)} is current.\n`,
1315
+ );
1316
+ else
1317
+ output.write(
1318
+ `Saved draft v${String(result.draftVersion.revisionNumber)} (${result.draftVersion.uid}).\n`,
1319
+ );
1320
+ return;
1321
+ }
1322
+
1323
+ if (command === 'hooks' && action === 'history') {
1324
+ const { options, positional } = parseCommandArguments(
1325
+ arguments_.slice(2),
1326
+ new Set(['cursor', 'json', 'limit']),
1327
+ );
1328
+ requireShape(positional.length === 2);
1329
+ const page = await client.hooks.listDraftVersions(
1330
+ positional[0],
1331
+ positional[1],
1332
+ pageQuery(options),
1333
+ );
1334
+ writePage({
1335
+ columns: [
1336
+ { label: 'VERSION', value: (item) => item.revisionNumber },
1337
+ { label: 'UID', value: (item) => item.uid },
1338
+ { label: 'WHEN', value: (item) => item.createdAt },
1339
+ { label: 'ACTOR', value: (item) => `${item.actor.name} (${item.actor.type})` },
1340
+ { label: 'CHANGE', value: (item) => item.changeType },
1341
+ { label: 'NOTE', value: (item) => item.changeReason },
1342
+ { label: 'CURRENT', value: (item) => item.isCurrent },
1343
+ ],
1344
+ options,
1345
+ output,
1346
+ page,
1347
+ });
1348
+ return;
1349
+ }
1350
+
1351
+ if (command === 'hooks' && action === 'diff') {
1352
+ const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
1353
+ requireShape(positional.length === 3);
1354
+ const result = await client.hooks.getDraftVersion(positional[0], positional[1], positional[2]);
1355
+
1356
+ if (options.json) {
1357
+ writeValue(output, result);
1358
+ return;
1359
+ }
1360
+ const previousLabel = result.version.previous
1361
+ ? `draft v${String(result.version.previous.revisionNumber)}`
1362
+ : 'empty draft';
1363
+ const currentLabel = `draft v${String(result.version.revisionNumber)}`;
1364
+ output.write(
1365
+ renderLineDiff(
1366
+ result.version.previous?.source ?? '',
1367
+ result.version.source,
1368
+ `${previousLabel} source`,
1369
+ `${currentLabel} source`,
1370
+ ),
1371
+ );
1372
+ output.write(
1373
+ renderLineDiff(
1374
+ `${JSON.stringify(result.version.previous?.config ?? {}, null, 2)}\n`,
1375
+ `${JSON.stringify(result.version.config, null, 2)}\n`,
1376
+ `${previousLabel} config`,
1377
+ `${currentLabel} config`,
1378
+ ),
1379
+ );
1380
+ return;
1381
+ }
1382
+
1383
+ if (command === 'hooks' && action === 'restore') {
1384
+ const { options, positional } = parseCommandArguments(
1385
+ arguments_.slice(2),
1386
+ new Set(['json', 'reason']),
1387
+ );
1388
+ requireShape(positional.length === 3);
1389
+ const { hook } = await client.hooks.get(positional[0], positional[1]);
1390
+ const result = await client.hooks.restoreDraft(positional[0], positional[1], positional[2], {
1391
+ expectedVersion: hook.version,
1392
+ reason: options.reason,
1393
+ });
1394
+
1395
+ if (options.json) writeValue(output, result);
1396
+ else
1397
+ output.write(
1398
+ `Restored ${positional[2]} as draft v${String(result.draftVersion.revisionNumber)} (${result.draftVersion.uid}).\n`,
1399
+ );
1400
+ return;
1401
+ }
1402
+
1403
+ if (command === 'hooks' && action === 'reset') {
1404
+ const { options, positional } = parseCommandArguments(
1405
+ arguments_.slice(2),
1406
+ new Set(['confirmationHookUid', 'reason']),
1407
+ );
1408
+ requireShape(
1409
+ positional.length === 2 &&
1410
+ typeof options.confirmationHookUid === 'string' &&
1411
+ typeof options.reason === 'string',
1412
+ );
1413
+ const separatorIndex = options.confirmationHookUid.lastIndexOf('@');
1414
+ const hookUid = options.confirmationHookUid.slice(0, separatorIndex);
1415
+ const expectedVersion = Number(options.confirmationHookUid.slice(separatorIndex + 1));
1416
+ if (separatorIndex <= 0 || !Number.isInteger(expectedVersion) || expectedVersion <= 0) {
1417
+ throw new Error('--confirm must be the current hook UID and version: <hook-uid>@<version>.');
1418
+ }
1419
+ await client.hooks.reset(positional[0], positional[1], {
1420
+ confirmationHookUid: hookUid,
1421
+ expectedVersion,
1422
+ reason: options.reason,
1423
+ });
1424
+ output.write(`Permanently reset hook ${hookUid} at version ${String(expectedVersion)}.\n`);
1425
+ return;
1426
+ }
1427
+
1428
+ if (command === 'hooks' && action === 'deploy') {
1429
+ const { options, positional } = parseCommandArguments(
1430
+ arguments_.slice(2),
1431
+ new Set(['configJson', 'filePath', 'json', 'reason', 'shadow']),
1432
+ );
1433
+ requireShape(positional.length === 2);
1434
+ const connectionUid = positional[0];
1435
+ const toolUid = positional[1];
1436
+ let { hook } = await client.hooks.get(connectionUid, toolUid);
1437
+
1438
+ if (hook.hookUid === null || options.filePath || options.configJson !== undefined) {
1439
+ const source = options.filePath ? await readFile(options.filePath, 'utf8') : hook.draftSource;
1440
+ const saved = await client.hooks.saveDraft(connectionUid, toolUid, {
1441
+ config: parseJsonOption(options.configJson, '--config', hook.draftConfig),
1442
+ expectedVersion: hook.version,
1443
+ reason: options.reason,
1444
+ source,
1445
+ });
1446
+ hook = saved.hook;
1447
+ }
1448
+
1449
+ const result = await client.hooks.deploy(connectionUid, toolUid, {
1450
+ expectedVersion: hook.version,
1451
+ mode: hookMode(options),
1452
+ });
1453
+ if (options.json) writeValue(output, result);
1454
+ else {
1455
+ const revisionUid =
1456
+ hookMode(options) === 'shadow'
1457
+ ? result.hook.shadowRevisionUid
1458
+ : result.hook.activeRevisionUid;
1459
+ output.write(`Deployed ${String(revisionUid)} in ${hookMode(options)} mode.\n`);
1460
+ }
1461
+ return;
1462
+ }
1463
+
1464
+ if (command === 'hooks' && ['deactivate', 'rollback'].includes(action)) {
1465
+ const { options, positional } = parseCommandArguments(
1466
+ arguments_.slice(2),
1467
+ new Set(['json', 'shadow']),
1468
+ );
1469
+ requireShape(positional.length === (action === 'rollback' ? 3 : 2));
1470
+ const { hook } = await client.hooks.get(positional[0], positional[1]);
1471
+ const result = await client.hooks.setDeployment(positional[0], positional[1], {
1472
+ expectedVersion: hook.version,
1473
+ mode: hookMode(options),
1474
+ revisionUid: action === 'rollback' ? positional[2] : null,
1475
+ });
1476
+ if (options.json) writeValue(output, result);
1477
+ else {
1478
+ output.write(
1479
+ action === 'rollback'
1480
+ ? `Deployed ${positional[2]} in ${hookMode(options)} mode.\n`
1481
+ : `Deactivated ${hookMode(options)} mode.\n`,
1482
+ );
1483
+ }
1484
+ return;
1485
+ }
1486
+
1487
+ if (command === 'hooks' && action === 'logs') {
1488
+ const { options, positional } = parseCommandArguments(
1489
+ arguments_.slice(2),
1490
+ new Set(['cursor', 'json', 'limit']),
1491
+ );
1492
+ requireShape(positional.length === 2);
1493
+ const page = await client.hooks.listRuns(positional[0], positional[1], pageQuery(options));
1494
+ writePage({
1495
+ columns: [
1496
+ { label: 'WHEN', value: (item) => item.createdAt },
1497
+ { label: 'REQUEST', value: (item) => item.requestId },
1498
+ { label: 'REVISION', value: (item) => item.revisionUid },
1499
+ { label: 'MODE', value: (item) => (item.shadow ? 'shadow' : 'active') },
1500
+ { label: 'STAGE', value: (item) => item.stage },
1501
+ { label: 'OUTCOME', value: (item) => item.outcome },
1502
+ {
1503
+ label: 'CHANGED',
1504
+ value: (item) =>
1505
+ item.differsFromActive === null ? undefined : item.differsFromActive ? 'yes' : 'no',
1506
+ },
1507
+ { label: 'DURATION', value: (item) => `${String(item.durationMs)}ms` },
1508
+ {
1509
+ label: 'LOGS',
1510
+ value: (item) =>
1511
+ item.logs
1512
+ ?.map(
1513
+ (entry) =>
1514
+ `[${entry.level}] ${entry.message}${entry.data === undefined ? '' : ` ${JSON.stringify(entry.data)}`}`,
1515
+ )
1516
+ .join(' | ') ?? 'not captured',
1517
+ },
1518
+ { label: 'ERROR', value: (item) => item.errorMessage },
1519
+ ],
1520
+ options,
1521
+ output,
1522
+ page,
1523
+ });
1524
+ return;
1525
+ }
1526
+
348
1527
  throw new Error(managementUsage);
349
1528
  };
350
1529
 
@@ -372,6 +1551,16 @@ export const runManagementCommand = async (
372
1551
  } catch (error) {
373
1552
  if (!(error instanceof RayrunApiError)) throw error;
374
1553
  const requestId = error.requestId ? `, request ${error.requestId}` : '';
375
- throw new Error(`${error.message} (${error.code}${requestId})`, { cause: error });
1554
+ const diagnosticTarget = error.code === 'invalid_source' ? 'source' : 'hook.ts';
1555
+ const diagnostics = error.diagnostics
1556
+ .map(
1557
+ (entry) =>
1558
+ `${diagnosticTarget}${entry.line === undefined ? '' : `:${String(entry.line)}${entry.column === undefined ? '' : `:${String(entry.column)}`}`}: ${entry.message}`,
1559
+ )
1560
+ .join('\n');
1561
+ throw new Error(
1562
+ `${error.message} (${error.code}${requestId})${diagnostics ? `\n${diagnostics}` : ''}`,
1563
+ { cause: error },
1564
+ );
376
1565
  }
377
1566
  };