@rayrun/cli 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,3 +84,30 @@ List commands show a compact table. Add `--json` for stable machine-readable out
84
84
  request up to 100 rows, and `--cursor` with the printed next cursor to continue. The CLI calls the
85
85
  same hosted API and policy evaluator as the dashboard; it does not run a local gateway or store API
86
86
  keys.
87
+
88
+ ## Author hosted tool hooks
89
+
90
+ Use a full-control API key. Hook source executes in Rayrun’s hosted sandbox, not in the CLI process.
91
+
92
+ ```sh
93
+ rayrun hooks pull <connection-uid> <tool-uid> \
94
+ --output hook.ts --types-output rayrun-hooks.d.ts
95
+
96
+ rayrun hooks test <connection-uid> <tool-uid> \
97
+ --file hook.ts --arguments '{"query":"release"}' --mock-result '{"items":[]}'
98
+
99
+ rayrun hooks deploy <connection-uid> <tool-uid> --file hook.ts --shadow
100
+ rayrun hooks logs <connection-uid> <tool-uid>
101
+ rayrun hooks rollback <connection-uid> <tool-uid> <revision-uid>
102
+ rayrun hooks deactivate <connection-uid> <tool-uid> --shadow
103
+ ```
104
+
105
+ `pull` can write the draft and generated TypeScript declarations separately. `test` compiles and
106
+ runs before plus an optional after stage using your mock result; it never contacts the upstream.
107
+ `deploy` saves `--file` and optional `--config` before creating an immutable revision. Omit `--shadow`
108
+ to deploy active. Rollback and deactivate default to active and accept `--shadow` for the shadow
109
+ pointer. Hook log payloads appear only when the service’s payload-capture setting is enabled; timing,
110
+ outcome, revision, request correlation, and the shadow changed/unchanged signal remain available
111
+ either way. Captured messages, structured data, and errors require a full-control key. Use
112
+ `hooks logs --json` for the full-fidelity retained record. Compiler errors include hook.ts line and
113
+ column diagnostics.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rayrun/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Set up MCP clients, execute tools, and manage Rayrun from the command line",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@modelcontextprotocol/client": "2.0.0",
32
- "@rayrun/sdk": "^0.4.0",
32
+ "@rayrun/sdk": "^0.5.0",
33
33
  "jsonc-parser": "^3.3.1",
34
34
  "smol-toml": "^1.8.0"
35
35
  },
package/src/management.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable node/no-process-env -- management intentionally reads the invoking user's Rayrun environment */
2
2
  import { Rayrun, RayrunApiError } from '@rayrun/sdk';
3
+ import { readFile, writeFile } from 'node:fs/promises';
3
4
  import { stripVTControlCharacters } from 'node:util';
4
5
 
5
6
  export const managementUsage = `Management:
@@ -11,6 +12,12 @@ export const managementUsage = `Management:
11
12
  rayrun policy inspect <client-uid> [--query <query>] [--limit <1-100>] [--cursor <cursor>] [--json]
12
13
  rayrun approvals list [--limit <1-100>] [--cursor <cursor>] [--json]
13
14
  rayrun activity list [--limit <1-100>] [--cursor <cursor>] [--json]
15
+ rayrun hooks pull <connection-uid> <tool-uid> [--output <file>] [--types-output <file>] [--json]
16
+ 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]
18
+ rayrun hooks rollback <connection-uid> <tool-uid> <revision-uid> [--shadow] [--json]
19
+ rayrun hooks deactivate <connection-uid> <tool-uid> [--shadow] [--json]
20
+ rayrun hooks logs <connection-uid> <tool-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
14
21
 
15
22
  Environment:
16
23
  RAYRUN_API_KEY API key created in Dashboard -> Settings -> API keys
@@ -22,6 +29,7 @@ const managementCommands = new Set([
22
29
  'clients',
23
30
  'connect',
24
31
  'connections',
32
+ 'hooks',
25
33
  'policy',
26
34
  'tools',
27
35
  ]);
@@ -30,12 +38,18 @@ export const isManagementCommand = (command) => managementCommands.has(command);
30
38
 
31
39
  const optionNames = new Map([
32
40
  ['--base-url', 'baseUrl'],
41
+ ['--arguments', 'argumentsJson'],
42
+ ['--config', 'configJson'],
33
43
  ['--connection', 'connectionUid'],
34
44
  ['--cursor', 'cursor'],
35
45
  ['--limit', 'limit'],
46
+ ['--file', 'filePath'],
47
+ ['--mock-result', 'mockResultJson'],
36
48
  ['--name', 'name'],
37
49
  ['--query', 'query'],
50
+ ['--output', 'outputPath'],
38
51
  ['--transport', 'transport'],
52
+ ['--types-output', 'typesOutputPath'],
39
53
  ]);
40
54
 
41
55
  const parseCommandArguments = (arguments_, allowedOptions) => {
@@ -50,6 +64,11 @@ const parseCommandArguments = (arguments_, allowedOptions) => {
50
64
  options.json = true;
51
65
  continue;
52
66
  }
67
+ if (argument === '--shadow') {
68
+ if (!allowedOptions.has('shadow')) throw new Error(`Unknown option: ${argument}`);
69
+ options.shadow = true;
70
+ continue;
71
+ }
53
72
 
54
73
  const optionName = optionNames.get(argument);
55
74
  if (optionName) {
@@ -150,6 +169,18 @@ const requireShape = (condition) => {
150
169
  if (!condition) throw new Error(managementUsage);
151
170
  };
152
171
 
172
+ const parseJsonOption = (value, option, fallback) => {
173
+ if (value === undefined) return fallback;
174
+
175
+ try {
176
+ return JSON.parse(value);
177
+ } catch {
178
+ throw new Error(`${option} must be valid JSON.`);
179
+ }
180
+ };
181
+
182
+ const hookMode = (options) => (options.shadow ? 'shadow' : 'active');
183
+
153
184
  const runCommand = async ({ arguments_, client, output }) => {
154
185
  const command = arguments_[0];
155
186
  const action = arguments_[1];
@@ -345,6 +376,141 @@ const runCommand = async ({ arguments_, client, output }) => {
345
376
  return;
346
377
  }
347
378
 
379
+ if (command === 'hooks' && action === 'pull') {
380
+ const { options, positional } = parseCommandArguments(
381
+ arguments_.slice(2),
382
+ new Set(['json', 'outputPath', 'typesOutputPath']),
383
+ );
384
+ requireShape(positional.length === 2);
385
+ const { hook } = await client.hooks.get(positional[0], positional[1]);
386
+
387
+ if (options.outputPath) await writeFile(options.outputPath, hook.draftSource, 'utf8');
388
+ if (options.typesOutputPath) await writeFile(options.typesOutputPath, hook.types, 'utf8');
389
+ if (options.json) writeValue(output, { hook });
390
+ else if (!options.outputPath) output.write(hook.draftSource);
391
+ else output.write(`Wrote hook source to ${options.outputPath}.\n`);
392
+ return;
393
+ }
394
+
395
+ if (command === 'hooks' && action === 'test') {
396
+ const { options, positional } = parseCommandArguments(
397
+ arguments_.slice(2),
398
+ new Set(['argumentsJson', 'configJson', 'filePath', 'json', 'mockResultJson']),
399
+ );
400
+ requireShape(positional.length === 2 && options.argumentsJson !== undefined);
401
+ const { hook } = await client.hooks.get(positional[0], positional[1]);
402
+ const source = options.filePath ? await readFile(options.filePath, 'utf8') : hook.draftSource;
403
+ const body = {
404
+ arguments: parseJsonOption(options.argumentsJson, '--arguments'),
405
+ config: parseJsonOption(options.configJson, '--config', hook.draftConfig),
406
+ source,
407
+ ...(options.mockResultJson === undefined
408
+ ? {}
409
+ : { mockResult: parseJsonOption(options.mockResultJson, '--mock-result') }),
410
+ };
411
+ const result = await client.hooks.test(positional[0], positional[1], body);
412
+ writeValue(output, result);
413
+ return;
414
+ }
415
+
416
+ if (command === 'hooks' && action === 'deploy') {
417
+ const { options, positional } = parseCommandArguments(
418
+ arguments_.slice(2),
419
+ new Set(['configJson', 'filePath', 'json', 'shadow']),
420
+ );
421
+ requireShape(positional.length === 2);
422
+ const connectionUid = positional[0];
423
+ const toolUid = positional[1];
424
+ let { hook } = await client.hooks.get(connectionUid, toolUid);
425
+
426
+ if (hook.hookUid === null || options.filePath || options.configJson !== undefined) {
427
+ const source = options.filePath ? await readFile(options.filePath, 'utf8') : hook.draftSource;
428
+ const saved = await client.hooks.saveDraft(connectionUid, toolUid, {
429
+ config: parseJsonOption(options.configJson, '--config', hook.draftConfig),
430
+ expectedVersion: hook.version,
431
+ source,
432
+ });
433
+ hook = saved.hook;
434
+ }
435
+
436
+ const result = await client.hooks.deploy(connectionUid, toolUid, {
437
+ expectedVersion: hook.version,
438
+ mode: hookMode(options),
439
+ });
440
+ if (options.json) writeValue(output, result);
441
+ else {
442
+ const revisionUid =
443
+ hookMode(options) === 'shadow'
444
+ ? result.hook.shadowRevisionUid
445
+ : result.hook.activeRevisionUid;
446
+ output.write(`Deployed ${String(revisionUid)} in ${hookMode(options)} mode.\n`);
447
+ }
448
+ return;
449
+ }
450
+
451
+ if (command === 'hooks' && ['deactivate', 'rollback'].includes(action)) {
452
+ const { options, positional } = parseCommandArguments(
453
+ arguments_.slice(2),
454
+ new Set(['json', 'shadow']),
455
+ );
456
+ requireShape(positional.length === (action === 'rollback' ? 3 : 2));
457
+ const { hook } = await client.hooks.get(positional[0], positional[1]);
458
+ const result = await client.hooks.setDeployment(positional[0], positional[1], {
459
+ expectedVersion: hook.version,
460
+ mode: hookMode(options),
461
+ revisionUid: action === 'rollback' ? positional[2] : null,
462
+ });
463
+ if (options.json) writeValue(output, result);
464
+ else {
465
+ output.write(
466
+ action === 'rollback'
467
+ ? `Deployed ${positional[2]} in ${hookMode(options)} mode.\n`
468
+ : `Deactivated ${hookMode(options)} mode.\n`,
469
+ );
470
+ }
471
+ return;
472
+ }
473
+
474
+ if (command === 'hooks' && action === 'logs') {
475
+ const { options, positional } = parseCommandArguments(
476
+ arguments_.slice(2),
477
+ new Set(['cursor', 'json', 'limit']),
478
+ );
479
+ requireShape(positional.length === 2);
480
+ const page = await client.hooks.listRuns(positional[0], positional[1], pageQuery(options));
481
+ writePage({
482
+ columns: [
483
+ { label: 'WHEN', value: (item) => item.createdAt },
484
+ { label: 'REQUEST', value: (item) => item.requestId },
485
+ { label: 'REVISION', value: (item) => item.revisionUid },
486
+ { label: 'MODE', value: (item) => (item.shadow ? 'shadow' : 'active') },
487
+ { label: 'STAGE', value: (item) => item.stage },
488
+ { label: 'OUTCOME', value: (item) => item.outcome },
489
+ {
490
+ label: 'CHANGED',
491
+ value: (item) =>
492
+ item.differsFromActive === null ? undefined : item.differsFromActive ? 'yes' : 'no',
493
+ },
494
+ { label: 'DURATION', value: (item) => `${String(item.durationMs)}ms` },
495
+ {
496
+ label: 'LOGS',
497
+ value: (item) =>
498
+ item.logs
499
+ ?.map(
500
+ (entry) =>
501
+ `[${entry.level}] ${entry.message}${entry.data === undefined ? '' : ` ${JSON.stringify(entry.data)}`}`,
502
+ )
503
+ .join(' | ') ?? 'not captured',
504
+ },
505
+ { label: 'ERROR', value: (item) => item.errorMessage },
506
+ ],
507
+ options,
508
+ output,
509
+ page,
510
+ });
511
+ return;
512
+ }
513
+
348
514
  throw new Error(managementUsage);
349
515
  };
350
516
 
@@ -372,6 +538,15 @@ export const runManagementCommand = async (
372
538
  } catch (error) {
373
539
  if (!(error instanceof RayrunApiError)) throw error;
374
540
  const requestId = error.requestId ? `, request ${error.requestId}` : '';
375
- throw new Error(`${error.message} (${error.code}${requestId})`, { cause: error });
541
+ const diagnostics = error.diagnostics
542
+ .map(
543
+ (entry) =>
544
+ `hook.ts${entry.line === undefined ? '' : `:${String(entry.line)}${entry.column === undefined ? '' : `:${String(entry.column)}`}`}: ${entry.message}`,
545
+ )
546
+ .join('\n');
547
+ throw new Error(
548
+ `${error.message} (${error.code}${requestId})${diagnostics ? `\n${diagnostics}` : ''}`,
549
+ { cause: error },
550
+ );
376
551
  }
377
552
  };