@rayrun/cli 0.4.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,20 +1,64 @@
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';
3
12
  import { readFile, writeFile } from 'node:fs/promises';
4
13
  import { stripVTControlCharacters } from 'node:util';
5
14
 
6
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]
7
22
  rayrun connect mcp <url> --name <name> [--transport <streamable-http|sse>] [--json]
8
23
  rayrun connect openapi <spec-url> [--name <name>] [--base-url <url>] [--json]
9
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]
10
49
  rayrun clients list [--limit <1-100>] [--cursor <cursor>] [--json]
11
50
  rayrun tools search <query> [--connection <uid>] [--limit <1-100>] [--cursor <cursor>] [--json]
12
51
  rayrun policy inspect <client-uid> [--query <query>] [--limit <1-100>] [--cursor <cursor>] [--json]
13
52
  rayrun approvals list [--limit <1-100>] [--cursor <cursor>] [--json]
14
53
  rayrun activity list [--limit <1-100>] [--cursor <cursor>] [--json]
15
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]
16
56
  rayrun hooks test <connection-uid> <tool-uid> --arguments <json> [--file <file>] [--mock-result <json>] [--config <json>] [--json]
17
- rayrun hooks deploy <connection-uid> <tool-uid> [--file <file>] [--config <json>] [--shadow] [--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>
18
62
  rayrun hooks rollback <connection-uid> <tool-uid> <revision-uid> [--shadow] [--json]
19
63
  rayrun hooks deactivate <connection-uid> <tool-uid> [--shadow] [--json]
20
64
  rayrun hooks logs <connection-uid> <tool-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
@@ -24,14 +68,19 @@ Environment:
24
68
  RAYRUN_API_URL Optional API origin (defaults to https://ray.run)`;
25
69
 
26
70
  const managementCommands = new Set([
71
+ 'access-profiles',
27
72
  'activity',
28
73
  'approvals',
29
74
  'clients',
30
75
  'connect',
31
76
  'connections',
77
+ 'deploy',
78
+ 'deployments',
32
79
  'hooks',
33
80
  'policy',
81
+ 'skills',
34
82
  'tools',
83
+ 'webhooks',
35
84
  ]);
36
85
 
37
86
  export const isManagementCommand = (command) => managementCommands.has(command);
@@ -40,20 +89,27 @@ const optionNames = new Map([
40
89
  ['--base-url', 'baseUrl'],
41
90
  ['--arguments', 'argumentsJson'],
42
91
  ['--config', 'configJson'],
92
+ ['--confirm', 'confirmationHookUid'],
43
93
  ['--connection', 'connectionUid'],
94
+ ['--confirm-name', 'confirmationName'],
44
95
  ['--cursor', 'cursor'],
45
96
  ['--limit', 'limit'],
46
97
  ['--file', 'filePath'],
47
98
  ['--mock-result', 'mockResultJson'],
48
99
  ['--name', 'name'],
100
+ ['--mode', 'mode'],
101
+ ['--profiles', 'profiles'],
49
102
  ['--query', 'query'],
103
+ ['--reason', 'reason'],
50
104
  ['--output', 'outputPath'],
51
105
  ['--transport', 'transport'],
106
+ ['--secret', 'secret'],
107
+ ['--version', 'versionUid'],
52
108
  ['--types-output', 'typesOutputPath'],
53
109
  ]);
54
110
 
55
111
  const parseCommandArguments = (arguments_, allowedOptions) => {
56
- const options = { json: false };
112
+ const options = { json: false, secrets: [] };
57
113
  const positional = [];
58
114
 
59
115
  for (let index = 0; index < arguments_.length; index += 1) {
@@ -69,14 +125,38 @@ const parseCommandArguments = (arguments_, allowedOptions) => {
69
125
  options.shadow = true;
70
126
  continue;
71
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
+ }
72
148
 
73
149
  const optionName = optionNames.get(argument);
74
150
  if (optionName) {
75
151
  if (!allowedOptions.has(optionName)) throw new Error(`Unknown option: ${argument}`);
76
152
  const value = arguments_[index + 1];
77
153
  if (!value || value.startsWith('--')) throw new Error(`${argument} requires a value.`);
78
- if (options[optionName] !== undefined) throw new Error(`${argument} can only be used once.`);
79
- 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
+ }
80
160
  index += 1;
81
161
  continue;
82
162
  }
@@ -123,12 +203,31 @@ const formatCell = (value) => {
123
203
  if (typeof value === 'boolean') return value ? 'yes' : 'no';
124
204
  const sanitized = stripVTControlCharacters(String(value))
125
205
  .replaceAll(/\p{Cc}+/gu, ' ')
206
+ .replaceAll(
207
+ /\p{Bidi_Control}/gu,
208
+ (character) => `\\u{${character.codePointAt(0).toString(16).toUpperCase()}}`,
209
+ )
126
210
  .replaceAll(/\s+/gu, ' ')
127
211
  .trim();
128
212
 
129
213
  return sanitized || '-';
130
214
  };
131
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
+
132
231
  export const renderTable = (items, columns) => {
133
232
  if (items.length === 0) return 'No results.\n';
134
233
 
@@ -181,10 +280,491 @@ const parseJsonOption = (value, option, fallback) => {
181
280
 
182
281
  const hookMode = (options) => (options.shadow ? 'shadow' : 'active');
183
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
+
184
388
  const runCommand = async ({ arguments_, client, output }) => {
185
389
  const command = arguments_[0];
186
390
  const action = arguments_[1];
187
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
+
188
768
  if (command === 'connect' && action === 'mcp') {
189
769
  const { options, positional } = parseCommandArguments(
190
770
  arguments_.slice(2),
@@ -244,6 +824,306 @@ const runCommand = async ({ arguments_, client, output }) => {
244
824
  return;
245
825
  }
246
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
+
247
1127
  if (command === 'clients' && action === 'list') {
248
1128
  const { options, positional } = parseCommandArguments(
249
1129
  arguments_.slice(2),
@@ -413,10 +1293,142 @@ const runCommand = async ({ arguments_, client, output }) => {
413
1293
  return;
414
1294
  }
415
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
+
416
1428
  if (command === 'hooks' && action === 'deploy') {
417
1429
  const { options, positional } = parseCommandArguments(
418
1430
  arguments_.slice(2),
419
- new Set(['configJson', 'filePath', 'json', 'shadow']),
1431
+ new Set(['configJson', 'filePath', 'json', 'reason', 'shadow']),
420
1432
  );
421
1433
  requireShape(positional.length === 2);
422
1434
  const connectionUid = positional[0];
@@ -428,6 +1440,7 @@ const runCommand = async ({ arguments_, client, output }) => {
428
1440
  const saved = await client.hooks.saveDraft(connectionUid, toolUid, {
429
1441
  config: parseJsonOption(options.configJson, '--config', hook.draftConfig),
430
1442
  expectedVersion: hook.version,
1443
+ reason: options.reason,
431
1444
  source,
432
1445
  });
433
1446
  hook = saved.hook;
@@ -538,10 +1551,11 @@ export const runManagementCommand = async (
538
1551
  } catch (error) {
539
1552
  if (!(error instanceof RayrunApiError)) throw error;
540
1553
  const requestId = error.requestId ? `, request ${error.requestId}` : '';
1554
+ const diagnosticTarget = error.code === 'invalid_source' ? 'source' : 'hook.ts';
541
1555
  const diagnostics = error.diagnostics
542
1556
  .map(
543
1557
  (entry) =>
544
- `hook.ts${entry.line === undefined ? '' : `:${String(entry.line)}${entry.column === undefined ? '' : `:${String(entry.column)}`}`}: ${entry.message}`,
1558
+ `${diagnosticTarget}${entry.line === undefined ? '' : `:${String(entry.line)}${entry.column === undefined ? '' : `:${String(entry.column)}`}`}: ${entry.message}`,
545
1559
  )
546
1560
  .join('\n');
547
1561
  throw new Error(