@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 +27 -0
- package/package.json +2 -2
- package/src/management.js +176 -1
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
|
+
"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.
|
|
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
|
-
|
|
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
|
};
|