@rayrun/cli 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -1
- package/package.json +11 -6
- package/src/execution.js +862 -0
- package/src/main.js +16 -2
- package/src/management.js +377 -0
- package/src/oauth.js +599 -0
- package/src/setup.js +2 -2
package/src/main.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { clientDefinitions, detectClients } from './clients.js';
|
|
2
|
+
import { executionUsage, isExecutionCommand, runExecutionCommand } from './execution.js';
|
|
3
|
+
import { isManagementCommand, managementUsage, runManagementCommand } from './management.js';
|
|
2
4
|
import { applySetup, rollbackSetup } from './setup.js';
|
|
3
5
|
import { createRequire } from 'node:module';
|
|
4
6
|
import { createInterface } from 'node:readline/promises';
|
|
@@ -9,7 +11,11 @@ const usage = `Usage:
|
|
|
9
11
|
rayrun setup <endpoint> [--client <id> ... | --all] [--project] [--yes] [--dry-run] [--login | --no-login]
|
|
10
12
|
rayrun setup rollback <backup-id>
|
|
11
13
|
|
|
12
|
-
Clients: claude-code, codex, cursor, vscode, windsurf, devin, opencode
|
|
14
|
+
Clients: claude-code, codex, cursor, vscode, windsurf, devin, opencode
|
|
15
|
+
|
|
16
|
+
${executionUsage}
|
|
17
|
+
|
|
18
|
+
${managementUsage}`;
|
|
13
19
|
|
|
14
20
|
export const validateEndpoint = (value) => {
|
|
15
21
|
let endpoint;
|
|
@@ -97,7 +103,7 @@ const selectInteractively = async (clients) => {
|
|
|
97
103
|
return clients.filter((_client, index) => indexes.has(index));
|
|
98
104
|
};
|
|
99
105
|
|
|
100
|
-
export const main = async (arguments_) => {
|
|
106
|
+
export const main = async (arguments_, dependencies) => {
|
|
101
107
|
if (arguments_.includes('--help') || arguments_.includes('-h')) {
|
|
102
108
|
process.stdout.write(`${usage}\n`);
|
|
103
109
|
return;
|
|
@@ -106,6 +112,14 @@ export const main = async (arguments_) => {
|
|
|
106
112
|
process.stdout.write(`${String(packageVersion)}\n`);
|
|
107
113
|
return;
|
|
108
114
|
}
|
|
115
|
+
if (isExecutionCommand(arguments_)) {
|
|
116
|
+
await runExecutionCommand(arguments_, { ...dependencies, version: packageVersion });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (isManagementCommand(arguments_[0])) {
|
|
120
|
+
await runManagementCommand(arguments_, dependencies);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
109
123
|
const { options, positional } = parseArguments(arguments_);
|
|
110
124
|
|
|
111
125
|
if (positional[0] !== 'setup') {
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/* eslint-disable node/no-process-env -- management intentionally reads the invoking user's Rayrun environment */
|
|
2
|
+
import { Rayrun, RayrunApiError } from '@rayrun/sdk';
|
|
3
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
4
|
+
|
|
5
|
+
export const managementUsage = `Management:
|
|
6
|
+
rayrun connect mcp <url> --name <name> [--transport <streamable-http|sse>] [--json]
|
|
7
|
+
rayrun connect openapi <spec-url> [--name <name>] [--base-url <url>] [--json]
|
|
8
|
+
rayrun connections list [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
9
|
+
rayrun clients list [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
10
|
+
rayrun tools search <query> [--connection <uid>] [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
11
|
+
rayrun policy inspect <client-uid> [--query <query>] [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
12
|
+
rayrun approvals list [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
13
|
+
rayrun activity list [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
14
|
+
|
|
15
|
+
Environment:
|
|
16
|
+
RAYRUN_API_KEY API key created in Dashboard -> Settings -> API keys
|
|
17
|
+
RAYRUN_API_URL Optional API origin (defaults to https://ray.run)`;
|
|
18
|
+
|
|
19
|
+
const managementCommands = new Set([
|
|
20
|
+
'activity',
|
|
21
|
+
'approvals',
|
|
22
|
+
'clients',
|
|
23
|
+
'connect',
|
|
24
|
+
'connections',
|
|
25
|
+
'policy',
|
|
26
|
+
'tools',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const isManagementCommand = (command) => managementCommands.has(command);
|
|
30
|
+
|
|
31
|
+
const optionNames = new Map([
|
|
32
|
+
['--base-url', 'baseUrl'],
|
|
33
|
+
['--connection', 'connectionUid'],
|
|
34
|
+
['--cursor', 'cursor'],
|
|
35
|
+
['--limit', 'limit'],
|
|
36
|
+
['--name', 'name'],
|
|
37
|
+
['--query', 'query'],
|
|
38
|
+
['--transport', 'transport'],
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const parseCommandArguments = (arguments_, allowedOptions) => {
|
|
42
|
+
const options = { json: false };
|
|
43
|
+
const positional = [];
|
|
44
|
+
|
|
45
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
46
|
+
const argument = arguments_[index];
|
|
47
|
+
|
|
48
|
+
if (argument === '--json') {
|
|
49
|
+
if (!allowedOptions.has('json')) throw new Error(`Unknown option: ${argument}`);
|
|
50
|
+
options.json = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const optionName = optionNames.get(argument);
|
|
55
|
+
if (optionName) {
|
|
56
|
+
if (!allowedOptions.has(optionName)) throw new Error(`Unknown option: ${argument}`);
|
|
57
|
+
const value = arguments_[index + 1];
|
|
58
|
+
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;
|
|
61
|
+
index += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (argument?.startsWith('--')) throw new Error(`Unknown option: ${argument}`);
|
|
66
|
+
if (argument) positional.push(argument);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (options.limit !== undefined) {
|
|
70
|
+
const limit = Number(options.limit);
|
|
71
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
72
|
+
throw new Error('--limit must be an integer from 1 to 100.');
|
|
73
|
+
}
|
|
74
|
+
options.limit = limit;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { options, positional };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const validateApiBaseUrl = (value) => {
|
|
81
|
+
let url;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
url = new URL(value);
|
|
85
|
+
} catch {
|
|
86
|
+
throw new Error('RAYRUN_API_URL must be an absolute HTTP or HTTPS URL.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
90
|
+
throw new Error('RAYRUN_API_URL must use HTTP or HTTPS.');
|
|
91
|
+
}
|
|
92
|
+
if (url.protocol === 'http:' && !['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname)) {
|
|
93
|
+
throw new Error('RAYRUN_API_URL may use HTTP only on localhost.');
|
|
94
|
+
}
|
|
95
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
96
|
+
throw new Error('RAYRUN_API_URL cannot contain credentials, a query string, or a fragment.');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return url.href.replace(/\/$/u, '');
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const formatCell = (value) => {
|
|
103
|
+
if (value === null || value === undefined || value === '') return '-';
|
|
104
|
+
if (typeof value === 'boolean') return value ? 'yes' : 'no';
|
|
105
|
+
const sanitized = stripVTControlCharacters(String(value))
|
|
106
|
+
.replaceAll(/\p{Cc}+/gu, ' ')
|
|
107
|
+
.replaceAll(/\s+/gu, ' ')
|
|
108
|
+
.trim();
|
|
109
|
+
|
|
110
|
+
return sanitized || '-';
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const renderTable = (items, columns) => {
|
|
114
|
+
if (items.length === 0) return 'No results.\n';
|
|
115
|
+
|
|
116
|
+
const rows = items.map((item) => columns.map(({ value }) => formatCell(value(item))));
|
|
117
|
+
const widths = columns.map(({ label }, index) =>
|
|
118
|
+
Math.max(label.length, ...rows.map((row) => row[index].length)),
|
|
119
|
+
);
|
|
120
|
+
const line = (cells) =>
|
|
121
|
+
cells
|
|
122
|
+
.map((cell, index) => cell.padEnd(widths[index], ' '))
|
|
123
|
+
.join(' ')
|
|
124
|
+
.trimEnd();
|
|
125
|
+
|
|
126
|
+
return `${line(columns.map(({ label }) => label))}\n${line(widths.map((width) => '-'.repeat(width)))}\n${rows.map(line).join('\n')}\n`;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const pageQuery = (options, extra = {}) => ({
|
|
130
|
+
...extra,
|
|
131
|
+
cursor: options.cursor,
|
|
132
|
+
limit: options.limit,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const writeValue = (output, value) => {
|
|
136
|
+
output.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const writePage = ({ columns, options, output, page }) => {
|
|
140
|
+
if (options.json) {
|
|
141
|
+
writeValue(output, page);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
output.write(renderTable(page.items, columns));
|
|
146
|
+
if (page.page.nextCursor) output.write(`Next cursor: ${page.page.nextCursor}\n`);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const requireShape = (condition) => {
|
|
150
|
+
if (!condition) throw new Error(managementUsage);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const runCommand = async ({ arguments_, client, output }) => {
|
|
154
|
+
const command = arguments_[0];
|
|
155
|
+
const action = arguments_[1];
|
|
156
|
+
|
|
157
|
+
if (command === 'connect' && action === 'mcp') {
|
|
158
|
+
const { options, positional } = parseCommandArguments(
|
|
159
|
+
arguments_.slice(2),
|
|
160
|
+
new Set(['json', 'name', 'transport']),
|
|
161
|
+
);
|
|
162
|
+
requireShape(positional.length === 1 && typeof options.name === 'string');
|
|
163
|
+
if (options.transport && !['sse', 'streamable-http'].includes(options.transport)) {
|
|
164
|
+
throw new Error('--transport must be streamable-http or sse.');
|
|
165
|
+
}
|
|
166
|
+
const result = await client.connections.create({
|
|
167
|
+
kind: 'mcp',
|
|
168
|
+
name: options.name,
|
|
169
|
+
transport: options.transport,
|
|
170
|
+
url: positional[0],
|
|
171
|
+
});
|
|
172
|
+
if (options.json) writeValue(output, result);
|
|
173
|
+
else output.write(`Connected ${result.connection.uid}.\n`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (command === 'connect' && action === 'openapi') {
|
|
178
|
+
const { options, positional } = parseCommandArguments(
|
|
179
|
+
arguments_.slice(2),
|
|
180
|
+
new Set(['baseUrl', 'json', 'name']),
|
|
181
|
+
);
|
|
182
|
+
requireShape(positional.length === 1);
|
|
183
|
+
const result = await client.connections.create({
|
|
184
|
+
baseUrl: options.baseUrl,
|
|
185
|
+
kind: 'openapi',
|
|
186
|
+
name: options.name,
|
|
187
|
+
specificationUrl: positional[0],
|
|
188
|
+
});
|
|
189
|
+
if (options.json) writeValue(output, result);
|
|
190
|
+
else output.write(`Connected ${result.connection.uid}.\n`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (command === 'connections' && action === 'list') {
|
|
195
|
+
const { options, positional } = parseCommandArguments(
|
|
196
|
+
arguments_.slice(2),
|
|
197
|
+
new Set(['cursor', 'json', 'limit']),
|
|
198
|
+
);
|
|
199
|
+
requireShape(positional.length === 0);
|
|
200
|
+
const page = await client.connections.list(pageQuery(options));
|
|
201
|
+
writePage({
|
|
202
|
+
columns: [
|
|
203
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
204
|
+
{ label: 'NAME', value: (item) => item.displayName },
|
|
205
|
+
{ label: 'KIND', value: (item) => item.kind },
|
|
206
|
+
{ label: 'STATUS', value: (item) => item.status },
|
|
207
|
+
{ label: 'POLICY', value: (item) => item.toolAccessMode },
|
|
208
|
+
],
|
|
209
|
+
options,
|
|
210
|
+
output,
|
|
211
|
+
page,
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (command === 'clients' && action === 'list') {
|
|
217
|
+
const { options, positional } = parseCommandArguments(
|
|
218
|
+
arguments_.slice(2),
|
|
219
|
+
new Set(['cursor', 'json', 'limit']),
|
|
220
|
+
);
|
|
221
|
+
requireShape(positional.length === 0);
|
|
222
|
+
const page = await client.clients.list(pageQuery(options));
|
|
223
|
+
writePage({
|
|
224
|
+
columns: [
|
|
225
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
226
|
+
{ label: 'CLIENT', value: (item) => item.clientName },
|
|
227
|
+
{ label: 'USER', value: (item) => item.userName },
|
|
228
|
+
{ label: 'POLICY', value: (item) => item.toolAccessMode },
|
|
229
|
+
{ label: 'PROFILE', value: (item) => item.accessProfile?.name },
|
|
230
|
+
{ label: 'LAST USED', value: (item) => item.lastUsedAt },
|
|
231
|
+
],
|
|
232
|
+
options,
|
|
233
|
+
output,
|
|
234
|
+
page,
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (command === 'tools' && action === 'search') {
|
|
240
|
+
const { options, positional } = parseCommandArguments(
|
|
241
|
+
arguments_.slice(2),
|
|
242
|
+
new Set(['connectionUid', 'cursor', 'json', 'limit']),
|
|
243
|
+
);
|
|
244
|
+
const query = positional.join(' ').trim();
|
|
245
|
+
requireShape(query.length > 0);
|
|
246
|
+
const page = await client.tools.list(
|
|
247
|
+
pageQuery(options, { connectionUid: options.connectionUid, query }),
|
|
248
|
+
);
|
|
249
|
+
writePage({
|
|
250
|
+
columns: [
|
|
251
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
252
|
+
{ label: 'NAME', value: (item) => item.name },
|
|
253
|
+
{ label: 'CONNECTION', value: (item) => item.connectionUid },
|
|
254
|
+
{ label: 'RISK', value: (item) => item.risk },
|
|
255
|
+
{ label: 'APPROVED', value: (item) => item.approved },
|
|
256
|
+
{ label: 'RULE', value: (item) => item.rule },
|
|
257
|
+
],
|
|
258
|
+
options,
|
|
259
|
+
output,
|
|
260
|
+
page,
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (command === 'policy' && action === 'inspect') {
|
|
266
|
+
const { options, positional } = parseCommandArguments(
|
|
267
|
+
arguments_.slice(2),
|
|
268
|
+
new Set(['cursor', 'json', 'limit', 'query']),
|
|
269
|
+
);
|
|
270
|
+
requireShape(positional.length === 1);
|
|
271
|
+
const page = await client.clients.listTools(
|
|
272
|
+
positional[0],
|
|
273
|
+
pageQuery(options, { query: options.query }),
|
|
274
|
+
);
|
|
275
|
+
writePage({
|
|
276
|
+
columns: [
|
|
277
|
+
{ label: 'SERVICE', value: (item) => item.serviceName },
|
|
278
|
+
{ label: 'TOOL', value: (item) => item.name },
|
|
279
|
+
{ label: 'EFFECTIVE', value: (item) => item.effectiveDecision },
|
|
280
|
+
{ label: 'REASON', value: (item) => item.effectiveReason },
|
|
281
|
+
{ label: 'CLIENT', value: (item) => item.clientDecision },
|
|
282
|
+
{ label: 'WORKSPACE', value: (item) => item.workspaceDecision },
|
|
283
|
+
],
|
|
284
|
+
options,
|
|
285
|
+
output,
|
|
286
|
+
page,
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (command === 'approvals' && action === 'list') {
|
|
292
|
+
const { options, positional } = parseCommandArguments(
|
|
293
|
+
arguments_.slice(2),
|
|
294
|
+
new Set(['cursor', 'json', 'limit']),
|
|
295
|
+
);
|
|
296
|
+
requireShape(positional.length === 0);
|
|
297
|
+
const page = await client.reviews.list(pageQuery(options));
|
|
298
|
+
writePage({
|
|
299
|
+
columns: [
|
|
300
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
301
|
+
{ label: 'SERVICE', value: (item) => item.connectionName },
|
|
302
|
+
{ label: 'TOOL', value: (item) => item.toolName },
|
|
303
|
+
{ label: 'STATUS', value: (item) => item.status },
|
|
304
|
+
{ label: 'CREATED', value: (item) => item.createdAt },
|
|
305
|
+
{ label: 'EXPIRES', value: (item) => item.expiresAt },
|
|
306
|
+
],
|
|
307
|
+
options,
|
|
308
|
+
output,
|
|
309
|
+
page,
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (command === 'activity' && action === 'list') {
|
|
315
|
+
const { options, positional } = parseCommandArguments(
|
|
316
|
+
arguments_.slice(2),
|
|
317
|
+
new Set(['cursor', 'json', 'limit']),
|
|
318
|
+
);
|
|
319
|
+
requireShape(positional.length === 0);
|
|
320
|
+
const page = await client.activity.list(pageQuery(options));
|
|
321
|
+
writePage({
|
|
322
|
+
columns: [
|
|
323
|
+
{ label: 'ID', value: (item) => item.id },
|
|
324
|
+
{ label: 'WHEN', value: (item) => item.calledAt },
|
|
325
|
+
{ label: 'TOOL', value: (item) => item.toolName },
|
|
326
|
+
{ label: 'SERVICE', value: (item) => item.connectionName },
|
|
327
|
+
{ label: 'CLIENT', value: (item) => item.clientName },
|
|
328
|
+
{ label: 'DECISION', value: (item) => item.policyDecision },
|
|
329
|
+
{
|
|
330
|
+
label: 'RESULT',
|
|
331
|
+
value: (item) =>
|
|
332
|
+
item.succeeded === null
|
|
333
|
+
? item.errorCode === 'outcome_unknown'
|
|
334
|
+
? 'outcome unknown'
|
|
335
|
+
: 'pending'
|
|
336
|
+
: item.succeeded
|
|
337
|
+
? 'success'
|
|
338
|
+
: 'failed',
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
options,
|
|
342
|
+
output,
|
|
343
|
+
page,
|
|
344
|
+
});
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
throw new Error(managementUsage);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
export const runManagementCommand = async (
|
|
352
|
+
arguments_,
|
|
353
|
+
{
|
|
354
|
+
environment = process.env,
|
|
355
|
+
fetchImplementation = globalThis.fetch,
|
|
356
|
+
output = process.stdout,
|
|
357
|
+
} = {},
|
|
358
|
+
) => {
|
|
359
|
+
const apiKey = environment.RAYRUN_API_KEY;
|
|
360
|
+
if (!apiKey?.trim()) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
'RAYRUN_API_KEY is required. Create a scoped key in Dashboard -> Settings -> API keys.',
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
const baseUrl = environment.RAYRUN_API_URL
|
|
366
|
+
? validateApiBaseUrl(environment.RAYRUN_API_URL)
|
|
367
|
+
: undefined;
|
|
368
|
+
const client = new Rayrun({ apiKey, baseUrl, fetch: fetchImplementation });
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
await runCommand({ arguments_, client, output });
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (!(error instanceof RayrunApiError)) throw error;
|
|
374
|
+
const requestId = error.requestId ? `, request ${error.requestId}` : '';
|
|
375
|
+
throw new Error(`${error.message} (${error.code}${requestId})`, { cause: error });
|
|
376
|
+
}
|
|
377
|
+
};
|