@phaseo/cli 0.1.1 → 0.1.2

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/dist/index.js ADDED
@@ -0,0 +1,1814 @@
1
+ #!/usr/bin/env node
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { spawn } from "node:child_process";
4
+ import { createServer } from "node:http";
5
+ import { clearScreenDown, cursorTo, moveCursor } from "node:readline";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { stdin as input, stdout as output, platform } from "node:process";
8
+ import { apiFetch, authorizeUrl, exchangeAuthorizationCode, getSessionAccessToken, normalizeApiRoot, parseScopeArgument, pollDeviceToken, revokeRefreshToken, startDeviceLogin, } from "./api.js";
9
+ import { clearSession, readSession, writeSession } from "./session.js";
10
+ import { printError, printJson, sanitizeTerminalText } from "./output.js";
11
+ import { CLI_VERSION } from "./generated/meta.js";
12
+ import { getVersionInfo } from "./release.js";
13
+ const LOGIN_METHOD_OPTIONS = [
14
+ {
15
+ value: "browser",
16
+ label: "Sign in with Phaseo",
17
+ description: "Open a browser, approve access, and return here automatically.",
18
+ },
19
+ {
20
+ value: "device",
21
+ label: "Sign in with Device Code",
22
+ description: "Best for SSH, remote shells, and terminals without browser callback support.",
23
+ },
24
+ ];
25
+ export function parseArgs(argv) {
26
+ const command = [];
27
+ const flags = {};
28
+ for (let i = 0; i < argv.length; i++) {
29
+ const arg = argv[i];
30
+ if (arg === "-h") {
31
+ flags.help = true;
32
+ continue;
33
+ }
34
+ if (arg === "-v") {
35
+ flags.version = true;
36
+ continue;
37
+ }
38
+ if (arg.startsWith("--")) {
39
+ const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
40
+ if (inlineValue !== undefined) {
41
+ flags[rawKey] = inlineValue;
42
+ continue;
43
+ }
44
+ const next = argv[i + 1];
45
+ if (next && !next.startsWith("-")) {
46
+ flags[rawKey] = next;
47
+ i += 1;
48
+ }
49
+ else {
50
+ flags[rawKey] = true;
51
+ }
52
+ continue;
53
+ }
54
+ command.push(arg);
55
+ }
56
+ return { command, flags };
57
+ }
58
+ function flagString(flags, key) {
59
+ const value = flags[key];
60
+ return typeof value === "string" ? value : undefined;
61
+ }
62
+ function flagBool(flags, key) {
63
+ return flags[key] === true || flags[key] === "true" || flags[key] === "1";
64
+ }
65
+ function flagNumber(flags, key) {
66
+ const value = flagString(flags, key);
67
+ if (value === undefined)
68
+ return undefined;
69
+ const parsed = Number(value);
70
+ if (!Number.isFinite(parsed))
71
+ throw new Error(`--${key} must be a number`);
72
+ return parsed;
73
+ }
74
+ function sleep(ms) {
75
+ return new Promise((resolve) => setTimeout(resolve, ms));
76
+ }
77
+ function base64Url(input) {
78
+ return input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
79
+ }
80
+ function randomBase64Url(byteLength) {
81
+ return base64Url(randomBytes(byteLength));
82
+ }
83
+ function sha256Base64Url(value) {
84
+ return base64Url(createHash("sha256").update(value).digest());
85
+ }
86
+ function appendQuery(path, params) {
87
+ const [base, existingQuery] = path.split("?", 2);
88
+ const query = new URLSearchParams(existingQuery ?? "");
89
+ for (const [key, value] of Object.entries(params)) {
90
+ if (value === undefined || value === "")
91
+ continue;
92
+ query.set(key, String(value));
93
+ }
94
+ const text = query.toString();
95
+ return text ? `${base}?${text}` : base;
96
+ }
97
+ function parseJsonFlag(flags, key, fallback = {}) {
98
+ const raw = flagString(flags, key);
99
+ if (!raw)
100
+ return fallback;
101
+ const parsed = JSON.parse(raw);
102
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
103
+ throw new Error(`--${key} must be a JSON object`);
104
+ }
105
+ return parsed;
106
+ }
107
+ function compact(value) {
108
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
109
+ }
110
+ function parseCsvFlag(flags, key) {
111
+ return (flagString(flags, key) ?? "")
112
+ .split(",")
113
+ .map((value) => value.trim())
114
+ .filter(Boolean);
115
+ }
116
+ function formatMoneyFromNanos(nanos) {
117
+ const value = Number(nanos ?? 0);
118
+ if (!Number.isFinite(value))
119
+ return "$0.000000";
120
+ return `$${(value / 1_000_000_000).toFixed(6)}`;
121
+ }
122
+ const HELP_ENTRIES = {
123
+ root: {
124
+ description: "Phaseo CLI",
125
+ usage: [
126
+ "phaseo login [--api-url <url>] [--method browser|device] [--browser] [--device-code] [--scopes <csv>] [--json]",
127
+ "phaseo logout [--json]",
128
+ "phaseo whoami [--json]",
129
+ "phaseo version [--json]",
130
+ "",
131
+ "phaseo keys --help",
132
+ "phaseo workspaces --help",
133
+ "phaseo presets --help",
134
+ "phaseo settings --help",
135
+ "phaseo guardrails --help",
136
+ "phaseo oauth-clients --help",
137
+ "phaseo management-keys --help",
138
+ "phaseo models --help",
139
+ "phaseo providers --help",
140
+ "phaseo organisations --help",
141
+ "phaseo endpoints --help",
142
+ "phaseo pricing --help",
143
+ "phaseo credits --help",
144
+ "phaseo activity --help",
145
+ "phaseo logs --help",
146
+ "phaseo analytics --help",
147
+ "phaseo generation --help",
148
+ "phaseo webhooks --help",
149
+ "phaseo api --help",
150
+ "",
151
+ "Login scopes:",
152
+ " Default login requests full first-party CLI access across the control plane.",
153
+ " Use --scopes workspaces:read,keys:write,... when a narrower token is preferred.",
154
+ "",
155
+ "Environment:",
156
+ " PHASEO_API_URL Override API root, defaults to https://api.phaseo.app",
157
+ ],
158
+ },
159
+ login: {
160
+ usage: [
161
+ "phaseo login [--api-url <url>] [--method browser|device] [--browser] [--device-code] [--scopes <csv>] [--json]",
162
+ ],
163
+ },
164
+ version: { usage: ["phaseo version [--json]"] },
165
+ logout: { usage: ["phaseo logout [--json]"] },
166
+ whoami: { usage: ["phaseo whoami [--json]"] },
167
+ keys: {
168
+ usage: [
169
+ "phaseo keys current [--json]",
170
+ "phaseo keys list [--limit <n>] [--offset <n>] [--include-disabled] [--json]",
171
+ "phaseo keys create --name <name> [--limit <usd>] [--limit-reset daily|weekly|monthly] [--expires-at <iso>] [--show-secret] [--json]",
172
+ "phaseo keys get <id-or-hash> [--json]",
173
+ "phaseo keys update <id-or-hash> [--name <name>] [--disabled true|false] [--limit <usd>] [--limit-reset daily|weekly|monthly] [--json]",
174
+ "phaseo keys delete <id-or-hash> [--json]",
175
+ ],
176
+ },
177
+ "keys current": { usage: ["phaseo keys current [--json]"] },
178
+ "keys list": { usage: ["phaseo keys list [--limit <n>] [--offset <n>] [--include-disabled] [--json]"] },
179
+ "keys create": { usage: ["phaseo keys create --name <name> [--limit <usd>] [--limit-reset daily|weekly|monthly] [--expires-at <iso>] [--show-secret] [--json]"] },
180
+ "keys get": { usage: ["phaseo keys get <id-or-hash> [--json]"] },
181
+ "keys update": { usage: ["phaseo keys update <id-or-hash> [--name <name>] [--disabled true|false] [--limit <usd>] [--limit-reset daily|weekly|monthly] [--json]"] },
182
+ "keys delete": { usage: ["phaseo keys delete <id-or-hash> [--json]"] },
183
+ workspaces: {
184
+ usage: [
185
+ "phaseo workspaces list [--json]",
186
+ "phaseo workspaces create --name <name> [--slug <slug>] [--json]",
187
+ "phaseo workspaces get <id-or-slug> [--json]",
188
+ "phaseo workspaces update <id-or-slug> [--name <name>] [--slug <slug>] [--json]",
189
+ "phaseo workspaces delete <id-or-slug> [--json]",
190
+ "phaseo workspaces members <id-or-slug> [--json]",
191
+ "phaseo workspaces add-members <id-or-slug> --user-ids <id,id> [--role member|admin] [--json]",
192
+ "phaseo workspaces remove-members <id-or-slug> --user-ids <id,id> [--json]",
193
+ ],
194
+ },
195
+ "workspaces list": { usage: ["phaseo workspaces list [--json]"] },
196
+ "workspaces create": { usage: ["phaseo workspaces create --name <name> [--slug <slug>] [--json]"] },
197
+ "workspaces get": { usage: ["phaseo workspaces get <id-or-slug> [--json]"] },
198
+ "workspaces update": { usage: ["phaseo workspaces update <id-or-slug> [--name <name>] [--slug <slug>] [--json]"] },
199
+ "workspaces delete": { usage: ["phaseo workspaces delete <id-or-slug> [--json]"] },
200
+ "workspaces members": { usage: ["phaseo workspaces members <id-or-slug> [--json]"] },
201
+ "workspaces add-members": { usage: ["phaseo workspaces add-members <id-or-slug> --user-ids <id,id> [--role member|admin] [--json]"] },
202
+ "workspaces remove-members": { usage: ["phaseo workspaces remove-members <id-or-slug> --user-ids <id,id> [--json]"] },
203
+ presets: {
204
+ usage: [
205
+ "phaseo presets list [--json]",
206
+ "phaseo presets create --name <@name> [--model <model>] [--system-prompt <text>] [--config-json <json>] [--json]",
207
+ "phaseo presets get <id-or-slug-or-name> [--json]",
208
+ "phaseo presets update <id-or-slug-or-name> [--config-json <json>] [--json]",
209
+ "phaseo presets delete <id-or-slug-or-name> [--json]",
210
+ ],
211
+ },
212
+ "presets list": { usage: ["phaseo presets list [--json]"] },
213
+ "presets create": { usage: ["phaseo presets create --name <@name> [--model <model>] [--system-prompt <text>] [--config-json <json>] [--json]"] },
214
+ "presets get": { usage: ["phaseo presets get <id-or-slug-or-name> [--json]"] },
215
+ "presets update": { usage: ["phaseo presets update <id-or-slug-or-name> [--config-json <json>] [--json]"] },
216
+ "presets delete": { usage: ["phaseo presets delete <id-or-slug-or-name> [--json]"] },
217
+ settings: {
218
+ usage: [
219
+ "phaseo settings get [--json]",
220
+ "phaseo settings update [--routing-mode balanced|price|latency|throughput] [--body-json <json>] [--json]",
221
+ ],
222
+ },
223
+ "settings get": { usage: ["phaseo settings get [--json]"] },
224
+ "settings update": { usage: ["phaseo settings update [--routing-mode balanced|price|latency|throughput] [--body-json <json>] [--json]"] },
225
+ guardrails: {
226
+ usage: [
227
+ "phaseo guardrails list [--json]",
228
+ "phaseo guardrails create --name <name> [--body-json <json>] [--json]",
229
+ "phaseo guardrails get <id> [--json]",
230
+ "phaseo guardrails update <id> --body-json <json> [--json]",
231
+ "phaseo guardrails delete <id> [--json]",
232
+ "phaseo guardrails list-keys <id> [--json]",
233
+ "phaseo guardrails add-keys <id> --key-ids <id,id> [--json]",
234
+ "phaseo guardrails remove-keys <id> --key-ids <id,id> [--json]",
235
+ "phaseo guardrails list-members <id> [--json]",
236
+ "phaseo guardrails add-members <id> --user-ids <id,id> [--json]",
237
+ "phaseo guardrails remove-members <id> --user-ids <id,id> [--json]",
238
+ "phaseo guardrails set-keys <id> --key-ids <id,id> [--json]",
239
+ ],
240
+ },
241
+ "guardrails list": { usage: ["phaseo guardrails list [--json]"] },
242
+ "guardrails create": { usage: ["phaseo guardrails create --name <name> [--body-json <json>] [--json]"] },
243
+ "guardrails get": { usage: ["phaseo guardrails get <id> [--json]"] },
244
+ "guardrails update": { usage: ["phaseo guardrails update <id> --body-json <json> [--json]"] },
245
+ "guardrails delete": { usage: ["phaseo guardrails delete <id> [--json]"] },
246
+ "guardrails list-keys": { usage: ["phaseo guardrails list-keys <id> [--json]"] },
247
+ "guardrails add-keys": { usage: ["phaseo guardrails add-keys <id> --key-ids <id,id> [--json]"] },
248
+ "guardrails remove-keys": { usage: ["phaseo guardrails remove-keys <id> --key-ids <id,id> [--json]"] },
249
+ "guardrails list-members": { usage: ["phaseo guardrails list-members <id> [--json]"] },
250
+ "guardrails add-members": { usage: ["phaseo guardrails add-members <id> --user-ids <id,id> [--json]"] },
251
+ "guardrails remove-members": { usage: ["phaseo guardrails remove-members <id> --user-ids <id,id> [--json]"] },
252
+ "guardrails set-keys": { usage: ["phaseo guardrails set-keys <id> --key-ids <id,id> [--json]"] },
253
+ "oauth-clients": {
254
+ usage: [
255
+ "phaseo oauth-clients list [--json]",
256
+ "phaseo oauth-clients create --name <name> --redirect-uri <uri>|--redirect-uris <uri,uri> [--client-type public|confidential] [--scopes scope_a,scope_b] [--show-secret] [--json]",
257
+ "phaseo oauth-clients get <client-id> [--json]",
258
+ "phaseo oauth-clients update <client-id> [--name <name>] [--redirect-uri <uri>|--redirect-uris <uri,uri>] [--scopes scope_a,scope_b] [--json]",
259
+ "phaseo oauth-clients delete <client-id> [--json]",
260
+ "phaseo oauth-clients regenerate-secret <client-id> [--show-secret] [--json]",
261
+ ],
262
+ },
263
+ "oauth-clients list": { usage: ["phaseo oauth-clients list [--json]"] },
264
+ "oauth-clients create": { usage: ["phaseo oauth-clients create --name <name> --redirect-uri <uri>|--redirect-uris <uri,uri> [--client-type public|confidential] [--scopes scope_a,scope_b] [--show-secret] [--json]"] },
265
+ "oauth-clients get": { usage: ["phaseo oauth-clients get <client-id> [--json]"] },
266
+ "oauth-clients update": { usage: ["phaseo oauth-clients update <client-id> [--name <name>] [--redirect-uri <uri>|--redirect-uris <uri,uri>] [--scopes scope_a,scope_b] [--json]"] },
267
+ "oauth-clients delete": { usage: ["phaseo oauth-clients delete <client-id> [--json]"] },
268
+ "oauth-clients regenerate-secret": { usage: ["phaseo oauth-clients regenerate-secret <client-id> [--show-secret] [--json]"] },
269
+ "management-keys": {
270
+ usage: [
271
+ "phaseo management-keys list [--json]",
272
+ "phaseo management-keys create --name <name> [--template raycast-readonly|read-only|read-write|full-control] [--scopes scope_a,scope_b] [--show-secret] [--json]",
273
+ "phaseo management-keys get <id> [--json]",
274
+ "phaseo management-keys update <id> [--name <name>] [--paused true|false] [--json]",
275
+ "phaseo management-keys delete <id> [--json]",
276
+ ],
277
+ },
278
+ "management-keys list": { usage: ["phaseo management-keys list [--json]"] },
279
+ "management-keys create": { usage: ["phaseo management-keys create --name <name> [--show-secret] [--json]"] },
280
+ "management-keys get": { usage: ["phaseo management-keys get <id> [--json]"] },
281
+ "management-keys update": { usage: ["phaseo management-keys update <id> [--name <name>] [--template raycast-readonly|read-only|read-write|full-control] [--paused true|false] [--json]"] },
282
+ "management-keys delete": { usage: ["phaseo management-keys delete <id> [--json]"] },
283
+ models: { usage: [
284
+ "phaseo models list [--limit <n>] [--offset <n>] [--all] [--json]",
285
+ "phaseo models get <model-id> [--json]",
286
+ ] },
287
+ "models list": { usage: ["phaseo models list [--limit <n>] [--offset <n>] [--all] [--json]"] },
288
+ "models get": { usage: ["phaseo models get <model-id> [--json]"] },
289
+ providers: { usage: ["phaseo providers list [--json]"] },
290
+ "providers list": { usage: ["phaseo providers list [--json]"] },
291
+ organisations: { usage: ["phaseo organisations list [--limit <n>] [--offset <n>] [--json]"] },
292
+ "organisations list": { usage: ["phaseo organisations list [--limit <n>] [--offset <n>] [--json]"] },
293
+ endpoints: { usage: ["phaseo endpoints list [--json]"] },
294
+ "endpoints list": { usage: ["phaseo endpoints list [--json]"] },
295
+ pricing: {
296
+ usage: [
297
+ "phaseo pricing models [--json]",
298
+ "phaseo pricing calculate --provider <provider> --model <model> --endpoint <endpoint> --usage-json <json>",
299
+ ],
300
+ },
301
+ "pricing models": { usage: ["phaseo pricing models [--json]"] },
302
+ "pricing calculate": { usage: ["phaseo pricing calculate --provider <provider> --model <model> --endpoint <endpoint> --usage-json <json>"] },
303
+ credits: { usage: ["phaseo credits get [--json]"] },
304
+ "credits get": { usage: ["phaseo credits get [--json]"] },
305
+ activity: { usage: ["phaseo activity list [--days <n>] [--limit <n>] [--offset <n>] [--json]"] },
306
+ "activity list": { usage: ["phaseo activity list [--days <n>] [--limit <n>] [--offset <n>] [--json]"] },
307
+ logs: {
308
+ usage: [
309
+ "phaseo logs list [--since <15m|1h|7d>] [--from <iso>] [--to <iso>] [--status <success|error|2xx|4xx|5xx|code>] [--provider <id>] [--model <id>] [--endpoint <path>] [--request-id <id>] [--key-id <id>] [--session-id <id>] [--error-code <code>] [--workspace <id>] [--limit <n>] [--offset <n>] [--json]",
310
+ "phaseo logs get <request-id> [--workspace <id>] [--json]",
311
+ ],
312
+ },
313
+ "logs list": { usage: ["phaseo logs list [--since <15m|1h|7d>] [--from <iso>] [--to <iso>] [--status <success|error|2xx|4xx|5xx|code>] [--provider <id>] [--model <id>] [--endpoint <path>] [--request-id <id>] [--key-id <id>] [--session-id <id>] [--error-code <code>] [--workspace <id>] [--limit <n>] [--offset <n>] [--json]"] },
314
+ "logs get": { usage: ["phaseo logs get <request-id> [--workspace <id>] [--json]"] },
315
+ analytics: { usage: ["phaseo analytics get [--date YYYY-MM-DD] [--json]"] },
316
+ "analytics get": { usage: ["phaseo analytics get [--date YYYY-MM-DD] [--json]"] },
317
+ generation: { usage: ["phaseo generation get --id <request-id> [--json]"] },
318
+ "generation get": { usage: ["phaseo generation get --id <request-id> [--json]"] },
319
+ webhooks: {
320
+ usage: [
321
+ "phaseo webhooks list [--limit <n>] [--offset <n>] [--include-deleted] [--json]",
322
+ "phaseo webhooks create --url <https-url> [--name <name>] [--events <event,event>] [--show-secret] [--json]",
323
+ "phaseo webhooks get <id> [--json]",
324
+ "phaseo webhooks update <id> [--url <https-url>] [--name <name>] [--events <event,event>] [--status active|disabled] [--json]",
325
+ "phaseo webhooks rotate-secret <id> [--show-secret] [--json]",
326
+ "phaseo webhooks delete <id> [--json]",
327
+ ],
328
+ },
329
+ "webhooks list": { usage: ["phaseo webhooks list [--limit <n>] [--offset <n>] [--include-deleted] [--json]"] },
330
+ "webhooks create": { usage: ["phaseo webhooks create --url <https-url> [--name <name>] [--events <event,event>] [--show-secret] [--json]"] },
331
+ "webhooks get": { usage: ["phaseo webhooks get <id> [--json]"] },
332
+ "webhooks update": { usage: ["phaseo webhooks update <id> [--url <https-url>] [--name <name>] [--events <event,event>] [--status active|disabled] [--json]"] },
333
+ "webhooks rotate-secret": { usage: ["phaseo webhooks rotate-secret <id> [--show-secret] [--json]"] },
334
+ "webhooks delete": { usage: ["phaseo webhooks delete <id> [--json]"] },
335
+ api: {
336
+ usage: [
337
+ "phaseo api get <v1-path> [--json]",
338
+ "phaseo api post <v1-path> --body-json <json> [--json]",
339
+ "phaseo api put <v1-path> --body-json <json> [--json]",
340
+ "phaseo api patch <v1-path> --body-json <json> [--json]",
341
+ "phaseo api delete <v1-path> [--json]",
342
+ ],
343
+ },
344
+ "api get": { usage: ["phaseo api get <v1-path> [--json]"] },
345
+ "api post": { usage: ["phaseo api post <v1-path> --body-json <json> [--json]"] },
346
+ "api put": { usage: ["phaseo api put <v1-path> --body-json <json> [--json]"] },
347
+ "api patch": { usage: ["phaseo api patch <v1-path> --body-json <json> [--json]"] },
348
+ "api delete": { usage: ["phaseo api delete <v1-path> [--json]"] },
349
+ };
350
+ export function helpKeyForCommand(command) {
351
+ if (command.length >= 2) {
352
+ const twoPart = `${command[0]} ${command[1]}`;
353
+ if (twoPart in HELP_ENTRIES)
354
+ return twoPart;
355
+ }
356
+ if (command.length >= 1 && command[0] in HELP_ENTRIES)
357
+ return command[0];
358
+ return "root";
359
+ }
360
+ export function renderHelp(command) {
361
+ const key = helpKeyForCommand(command);
362
+ const entry = HELP_ENTRIES[key] ?? HELP_ENTRIES.root;
363
+ const lines = [];
364
+ if (key === "root") {
365
+ lines.push(entry.description ?? "Phaseo CLI", "", "Usage:");
366
+ }
367
+ else {
368
+ lines.push(`Phaseo CLI Help: ${key}`, "", "Usage:");
369
+ }
370
+ for (const usageLine of entry.usage) {
371
+ if (usageLine === "") {
372
+ lines.push("");
373
+ continue;
374
+ }
375
+ if (usageLine.endsWith(":")) {
376
+ lines.push(usageLine);
377
+ continue;
378
+ }
379
+ lines.push(` ${usageLine}`);
380
+ }
381
+ return `${lines.join("\n")}\n`;
382
+ }
383
+ function printHelp(command = []) {
384
+ process.stdout.write(renderHelp(command));
385
+ }
386
+ export function renderVersionText(details) {
387
+ const lines = [
388
+ `Phaseo CLI ${details.version}`,
389
+ `Package manager: ${details.packageManager}`,
390
+ `Install: ${details.installCommand}`,
391
+ `Update: ${details.updateCommand}`,
392
+ ];
393
+ if (details.latestVersion) {
394
+ lines.push(details.updateAvailable
395
+ ? `Latest: ${details.latestVersion} (update available)`
396
+ : `Latest: ${details.latestVersion}`);
397
+ }
398
+ return `${lines.join("\n")}\n`;
399
+ }
400
+ async function printVersion(flags, options = {}) {
401
+ if (options.short) {
402
+ const info = await getVersionInfo();
403
+ process.stdout.write(`${CLI_VERSION} update: ${info.updateCommand}\n`);
404
+ return;
405
+ }
406
+ const refreshVersion = flagBool(flags, "refresh-version") || flagBool(flags, "check-update");
407
+ const info = await getVersionInfo({
408
+ lookupLatest: refreshVersion,
409
+ forceLatestLookup: refreshVersion,
410
+ });
411
+ if (flagBool(flags, "json")) {
412
+ printJson(info);
413
+ return;
414
+ }
415
+ process.stdout.write(renderVersionText(info));
416
+ }
417
+ async function maybePrintUpdateNotice(json) {
418
+ if (json || process.env.PHASEO_DISABLE_UPDATE_CHECK === "1")
419
+ return;
420
+ const info = await getVersionInfo({ lookupLatest: true });
421
+ if (!info.updateAvailable || !info.latestVersion)
422
+ return;
423
+ process.stdout.write(`\nUpdate available: ${CLI_VERSION} -> ${info.latestVersion}\nRun: ${info.updateCommand}\n`);
424
+ }
425
+ async function request(path, options = {}) {
426
+ const session = await getSessionAccessToken();
427
+ return apiFetch(session.apiUrl, path, {
428
+ method: options.method ?? "GET",
429
+ accessToken: session.accessToken,
430
+ ...(options.body ? { body: JSON.stringify(options.body) } : {}),
431
+ });
432
+ }
433
+ function printList(items, render) {
434
+ for (const item of items)
435
+ process.stdout.write(`${render(item)}\n`);
436
+ }
437
+ function countRenderedLines(text) {
438
+ const normalized = text.replace(/\r/g, "");
439
+ const trimmed = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
440
+ return trimmed ? trimmed.split("\n").length : 0;
441
+ }
442
+ function clearRenderedBlock(lineCount) {
443
+ if (!output.isTTY || lineCount <= 0)
444
+ return;
445
+ moveCursor(output, 0, -lineCount);
446
+ cursorTo(output, 0);
447
+ clearScreenDown(output);
448
+ }
449
+ function repaintBlock(text, previousLineCount) {
450
+ clearRenderedBlock(previousLineCount);
451
+ output.write(text);
452
+ return countRenderedLines(text);
453
+ }
454
+ function isInteractiveTerminal() {
455
+ return Boolean(input.isTTY && output.isTTY);
456
+ }
457
+ export function prefersDeviceCodeByEnvironment(env = process.env) {
458
+ return Boolean(env.SSH_CONNECTION ||
459
+ env.SSH_CLIENT ||
460
+ env.SSH_TTY ||
461
+ env.CI);
462
+ }
463
+ export function windowsBrowserOpenArgs(url) {
464
+ return ["url.dll,FileProtocolHandler", url];
465
+ }
466
+ export function renderOneTimeClientSecret(secret, showSecret) {
467
+ if (typeof secret !== "string" || !secret)
468
+ return "";
469
+ return showSecret
470
+ ? `Client secret: ${secret}\n`
471
+ : "Client secret hidden. Re-run with --json or --show-secret to capture it once.\n";
472
+ }
473
+ function loginOptionIndex(method) {
474
+ return LOGIN_METHOD_OPTIONS.findIndex((option) => option.value === method);
475
+ }
476
+ export function renderLoginBanner() {
477
+ return [
478
+ " _____ _ ",
479
+ " | __ \\| | ",
480
+ " | |__) | |__ __ _ ___ ___ ___ ",
481
+ " | ___/| '_ \\ / _` / __|/ _ \\/ _ \\ ",
482
+ " | | | | | | (_| \\__ \\ __/ (_) |",
483
+ " |_| |_| |_|\\__,_|___/\\___|\\___/ ",
484
+ " ",
485
+ " ",
486
+ "",
487
+ "Phaseo CLI",
488
+ "Workspace control, keys, and routing tools from your terminal.",
489
+ ].join("\n");
490
+ }
491
+ export function renderLoginMenu(selectedIndex, defaultMethod) {
492
+ const lines = [
493
+ renderLoginBanner(),
494
+ "",
495
+ "Choose a login method.",
496
+ "Use Up/Down arrows and Enter to continue.",
497
+ "",
498
+ ];
499
+ for (const [index, option] of LOGIN_METHOD_OPTIONS.entries()) {
500
+ const isSelected = index === selectedIndex;
501
+ const isDefault = option.value === defaultMethod;
502
+ lines.push(`${isSelected ? ">" : " "} ${option.label}${isDefault ? " [default]" : ""}`);
503
+ lines.push(` ${option.description}`);
504
+ lines.push("");
505
+ }
506
+ lines.push("Tip: Device Code is the safer choice in SSH, remote devboxes, and CI.");
507
+ lines.push("Press Ctrl+C to cancel.");
508
+ return lines.join("\n");
509
+ }
510
+ export function nextLoginMenuIndex(currentIndex, rawInput, optionCount) {
511
+ if (optionCount <= 0)
512
+ return 0;
513
+ switch (rawInput) {
514
+ case "\u001b[A":
515
+ return Math.max(0, currentIndex - 1);
516
+ case "\u001b[B":
517
+ return Math.min(optionCount - 1, currentIndex + 1);
518
+ case "1":
519
+ return 0;
520
+ case "2":
521
+ return Math.min(optionCount - 1, 1);
522
+ default:
523
+ return currentIndex;
524
+ }
525
+ }
526
+ export function inspectCallbackRequest(requestUrl, expectedState) {
527
+ const error = requestUrl.searchParams.get("error") || undefined;
528
+ const errorDescription = requestUrl.searchParams.get("error_description") || undefined;
529
+ const state = requestUrl.searchParams.get("state") || undefined;
530
+ const code = requestUrl.searchParams.get("code") || undefined;
531
+ if ((error || code) && state !== expectedState) {
532
+ return { ok: false, pending: true };
533
+ }
534
+ if (error) {
535
+ return { ok: false, error, errorDescription, state };
536
+ }
537
+ if (!code) {
538
+ return { ok: false, pending: true };
539
+ }
540
+ return { ok: true, code, state };
541
+ }
542
+ export function validateLoopbackRedirectUri(value) {
543
+ const url = new URL(value);
544
+ const loopback = url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]" || url.hostname === "localhost";
545
+ if (url.protocol !== "http:" || !loopback || url.pathname !== "/callback" || url.username || url.password || url.search || url.hash) {
546
+ throw new Error("Browser redirect URI must be an HTTP loopback /callback URL");
547
+ }
548
+ return url.toString();
549
+ }
550
+ export function callbackListenHost(redirectUri) {
551
+ const hostname = new URL(redirectUri).hostname;
552
+ return hostname === "[::1]" ? "::1" : hostname;
553
+ }
554
+ function openUrl(url) {
555
+ if (prefersDeviceCodeByEnvironment()) {
556
+ return false;
557
+ }
558
+ try {
559
+ if (platform === "win32") {
560
+ // Pass the authorization URL as data, never through a command interpreter.
561
+ spawn("rundll32.exe", windowsBrowserOpenArgs(url), {
562
+ detached: true,
563
+ stdio: "ignore",
564
+ }).unref();
565
+ return true;
566
+ }
567
+ if (platform === "darwin") {
568
+ spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
569
+ return true;
570
+ }
571
+ spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
572
+ return true;
573
+ }
574
+ catch {
575
+ return false;
576
+ }
577
+ }
578
+ function resolveLoginMethod(flags, json) {
579
+ if (flagBool(flags, "browser"))
580
+ return "browser";
581
+ if (flagBool(flags, "device-code"))
582
+ return "device";
583
+ const raw = flagString(flags, "method");
584
+ if (raw) {
585
+ if (raw === "browser" || raw === "device")
586
+ return raw;
587
+ throw new Error("--method must be browser or device");
588
+ }
589
+ if (json || !isInteractiveTerminal() || prefersDeviceCodeByEnvironment()) {
590
+ return "device";
591
+ }
592
+ return "browser";
593
+ }
594
+ async function chooseLoginMethod(flags, json) {
595
+ const preset = flagString(flags, "method") ?? (flagBool(flags, "browser") ? "browser" : flagBool(flags, "device-code") ? "device" : undefined);
596
+ if (preset || json || !isInteractiveTerminal() || prefersDeviceCodeByEnvironment()) {
597
+ return resolveLoginMethod(flags, json);
598
+ }
599
+ const defaultMethod = resolveLoginMethod(flags, json);
600
+ if (typeof input.setRawMode === "function") {
601
+ return new Promise((resolve, reject) => {
602
+ let selectedIndex = Math.max(0, loginOptionIndex(defaultMethod));
603
+ let renderedLines = 0;
604
+ const finish = (result) => {
605
+ input.off("data", handleKeypress);
606
+ if (typeof input.setRawMode === "function") {
607
+ input.setRawMode(false);
608
+ }
609
+ input.pause();
610
+ output.write("\x1b[?25h");
611
+ clearRenderedBlock(renderedLines);
612
+ if (result.error) {
613
+ reject(result.error);
614
+ return;
615
+ }
616
+ resolve(result.method ?? defaultMethod);
617
+ };
618
+ const render = () => {
619
+ renderedLines = repaintBlock(`${renderLoginMenu(selectedIndex, defaultMethod)}\n`, renderedLines);
620
+ };
621
+ const handleKeypress = (chunk) => {
622
+ const rawInput = chunk.toString("utf8");
623
+ if (rawInput === "\u0003") {
624
+ finish({ error: new Error("Login cancelled.") });
625
+ return;
626
+ }
627
+ if (rawInput === "\r" || rawInput === "\n") {
628
+ finish({ method: LOGIN_METHOD_OPTIONS[selectedIndex]?.value ?? defaultMethod });
629
+ return;
630
+ }
631
+ const nextIndex = nextLoginMenuIndex(selectedIndex, rawInput, LOGIN_METHOD_OPTIONS.length);
632
+ if (nextIndex !== selectedIndex) {
633
+ selectedIndex = nextIndex;
634
+ render();
635
+ }
636
+ };
637
+ input.setRawMode(true);
638
+ input.resume();
639
+ output.write("\x1b[?25l");
640
+ render();
641
+ input.on("data", handleKeypress);
642
+ });
643
+ }
644
+ const prompt = createInterface({ input, output });
645
+ try {
646
+ const defaultChoice = defaultMethod === "device" ? "2" : "1";
647
+ const answer = (await prompt.question(`Login method: [1] Sign in with Phaseo [2] Sign in with Device Code (default ${defaultChoice}): `)).trim();
648
+ if (!answer)
649
+ return defaultMethod;
650
+ if (answer === "2" || /^device$/i.test(answer))
651
+ return "device";
652
+ return "browser";
653
+ }
654
+ finally {
655
+ prompt.close();
656
+ }
657
+ }
658
+ function createCallbackServer(args) {
659
+ const redirect = new URL(args.redirectUri);
660
+ const host = callbackListenHost(args.redirectUri);
661
+ const port = redirect.port ? Number(redirect.port) : 8976;
662
+ const callbackPath = redirect.pathname || "/";
663
+ let settled = false;
664
+ let activeRedirectUri = redirect.toString();
665
+ let resolveCallback = () => undefined;
666
+ const waitForCallback = new Promise((resolve) => {
667
+ resolveCallback = resolve;
668
+ });
669
+ const timeout = setTimeout(() => {
670
+ finish({ ok: false, error: "login_timeout", errorDescription: "Browser login timed out after 10 minutes" });
671
+ }, 10 * 60 * 1000);
672
+ timeout.unref();
673
+ function finish(value) {
674
+ if (settled)
675
+ return;
676
+ settled = true;
677
+ clearTimeout(timeout);
678
+ resolveCallback(value);
679
+ }
680
+ const server = createServer((req, res) => {
681
+ const requestUrl = new URL(req.url || "/", args.redirectUri);
682
+ if (requestUrl.pathname !== callbackPath) {
683
+ res.statusCode = 404;
684
+ res.end("Not found");
685
+ return;
686
+ }
687
+ const outcome = inspectCallbackRequest(requestUrl, args.expectedState);
688
+ if ("pending" in outcome) {
689
+ res.statusCode = 200;
690
+ res.setHeader("content-type", "text/html; charset=utf-8");
691
+ res.end("<!doctype html><html><body style=\"font-family:sans-serif;padding:24px\"><h1>Waiting for Phaseo authorization</h1><p>This browser window can stay open until authorization completes.</p></body></html>");
692
+ return;
693
+ }
694
+ finish(outcome);
695
+ res.statusCode = 200;
696
+ res.setHeader("content-type", "text/html; charset=utf-8");
697
+ res.end("<!doctype html><html><body style=\"font-family:sans-serif;padding:24px\"><h1>Phaseo login complete</h1><p>You can return to your terminal now.</p></body></html>");
698
+ });
699
+ return {
700
+ start: async () => new Promise((resolve, reject) => {
701
+ server.once("error", reject);
702
+ server.listen(port, host, () => {
703
+ server.off("error", reject);
704
+ const address = server.address();
705
+ if (address && typeof address === "object") {
706
+ const active = new URL(redirect);
707
+ active.port = String(address.port);
708
+ activeRedirectUri = active.toString();
709
+ }
710
+ resolve();
711
+ });
712
+ }),
713
+ getRedirectUri: () => activeRedirectUri,
714
+ waitForCallback,
715
+ close: async () => new Promise((resolve) => {
716
+ server.close(() => resolve());
717
+ }),
718
+ };
719
+ }
720
+ async function completeLogin(apiRoot, tokens, json) {
721
+ await writeSession({
722
+ accessToken: tokens.access_token,
723
+ refreshToken: tokens.refresh_token,
724
+ expiresAt: Date.now() + Number(tokens.expires_in ?? 900) * 1000,
725
+ apiUrl: apiRoot,
726
+ scope: tokens.scope,
727
+ });
728
+ if (json) {
729
+ printJson({ ok: true, logged_in: true, api_url: apiRoot, scope: tokens.scope ?? null });
730
+ }
731
+ else {
732
+ process.stdout.write("Logged in to Phaseo.\n");
733
+ }
734
+ }
735
+ function requestedLoginScope(flags) {
736
+ return parseScopeArgument(flagString(flags, "scopes"));
737
+ }
738
+ async function loginWithDeviceCode(apiRoot, flags, json) {
739
+ const scope = requestedLoginScope(flags);
740
+ const device = await startDeviceLogin(apiRoot, scope);
741
+ if (json) {
742
+ printJson({
743
+ status: "authorization_required",
744
+ user_code: device.user_code,
745
+ verification_uri: device.verification_uri,
746
+ verification_uri_complete: device.verification_uri_complete,
747
+ expires_in: device.expires_in,
748
+ });
749
+ }
750
+ else {
751
+ process.stdout.write("Authorize Phaseo CLI\n\n");
752
+ process.stdout.write(`Open: ${device.verification_uri_complete}\n`);
753
+ process.stdout.write(`Code: ${device.user_code}\n\n`);
754
+ process.stdout.write("Waiting for approval...\n");
755
+ }
756
+ const deadline = Date.now() + Number(device.expires_in) * 1000;
757
+ let intervalMs = Math.max(1, Number(device.interval ?? 5)) * 1000;
758
+ while (Date.now() < deadline) {
759
+ await sleep(intervalMs);
760
+ try {
761
+ const tokens = await pollDeviceToken(apiRoot, device.device_code);
762
+ await completeLogin(apiRoot, tokens, json);
763
+ return;
764
+ }
765
+ catch (error) {
766
+ if (error?.code === "authorization_pending")
767
+ continue;
768
+ if (error?.code === "slow_down") {
769
+ intervalMs += 5_000;
770
+ continue;
771
+ }
772
+ throw error;
773
+ }
774
+ }
775
+ throw new Error("Device login expired. Run `phaseo login` again.");
776
+ }
777
+ async function loginWithBrowser(apiRoot, flags) {
778
+ const redirectUri = validateLoopbackRedirectUri(flagString(flags, "redirect-uri") ?? "http://127.0.0.1:0/callback");
779
+ const state = randomBase64Url(24);
780
+ const codeVerifier = randomBase64Url(32);
781
+ const codeChallenge = sha256Base64Url(codeVerifier);
782
+ const callbackServer = createCallbackServer({ redirectUri, expectedState: state });
783
+ await callbackServer.start();
784
+ try {
785
+ const activeRedirectUri = callbackServer.getRedirectUri();
786
+ const scope = requestedLoginScope(flags);
787
+ const authUrl = authorizeUrl(apiRoot, {
788
+ clientId: "phaseo_cli",
789
+ redirectUri: activeRedirectUri,
790
+ scope,
791
+ state,
792
+ codeChallenge,
793
+ });
794
+ const didOpen = openUrl(authUrl);
795
+ process.stdout.write("Sign in with Phaseo\n\n");
796
+ process.stdout.write(`Open: ${authUrl}\n\n`);
797
+ if (!didOpen) {
798
+ process.stdout.write("Browser auto-open was unavailable, so use the URL above.\n");
799
+ }
800
+ process.stdout.write("Waiting for browser sign-in...\n");
801
+ const callback = await callbackServer.waitForCallback;
802
+ if (!callback.ok) {
803
+ const failure = callback;
804
+ throw new Error(failure.errorDescription ?? failure.error);
805
+ }
806
+ const tokens = await exchangeAuthorizationCode(apiRoot, {
807
+ code: callback.code,
808
+ redirectUri: activeRedirectUri,
809
+ codeVerifier,
810
+ });
811
+ await completeLogin(apiRoot, tokens, false);
812
+ }
813
+ finally {
814
+ await callbackServer.close();
815
+ }
816
+ }
817
+ async function login(flags) {
818
+ const apiRoot = normalizeApiRoot(flagString(flags, "api-url"));
819
+ const json = flagBool(flags, "json");
820
+ const method = await chooseLoginMethod(flags, json);
821
+ if (json && method === "browser") {
822
+ throw new Error("Browser login is interactive and requires user action in the browser, so it cannot produce immediate JSON output with --json. Use --method device instead.");
823
+ }
824
+ if (method === "browser") {
825
+ return loginWithBrowser(apiRoot, flags);
826
+ }
827
+ return loginWithDeviceCode(apiRoot, flags, json);
828
+ }
829
+ async function logout(flags) {
830
+ const session = await readSession();
831
+ let revoked = false;
832
+ if (session?.refreshToken) {
833
+ try {
834
+ await revokeRefreshToken(session.apiUrl, session.refreshToken);
835
+ revoked = true;
836
+ }
837
+ catch {
838
+ revoked = false;
839
+ }
840
+ }
841
+ await clearSession();
842
+ if (flagBool(flags, "json")) {
843
+ printJson({ ok: true, logged_in: false, refresh_token_revoked: revoked });
844
+ }
845
+ else {
846
+ process.stdout.write(revoked ? "Logged out of Phaseo and revoked the session.\n" : "Logged out of Phaseo.\n");
847
+ }
848
+ }
849
+ async function whoami(flags) {
850
+ const body = await request("/me");
851
+ if (flagBool(flags, "json"))
852
+ return printJson(body);
853
+ const data = body.data;
854
+ process.stdout.write(`${data.user.email ?? data.user.id}\n`);
855
+ process.stdout.write(`Current workspace: ${data.current_workspace_id}\n`);
856
+ if (Array.isArray(data.workspaces) && data.workspaces.length) {
857
+ process.stdout.write("\nWorkspaces:\n");
858
+ for (const workspace of data.workspaces) {
859
+ const marker = workspace.current ? "*" : " ";
860
+ process.stdout.write(`${marker} ${workspace.name ?? workspace.id} (${workspace.role}) ${workspace.id}\n`);
861
+ }
862
+ }
863
+ }
864
+ async function listKeys(flags) {
865
+ const body = await request(appendQuery("/keys", {
866
+ limit: flagString(flags, "limit"),
867
+ offset: flagString(flags, "offset"),
868
+ include_disabled: flagBool(flags, "include-disabled") || undefined,
869
+ }));
870
+ if (flagBool(flags, "json"))
871
+ return printJson(body);
872
+ printList(body.data ?? [], (key) => `${key.disabled ? "paused" : "active"} ${key.name ?? key.id} ${key.id} ${key.prefix ?? ""}`.trim());
873
+ }
874
+ async function createKey(flags) {
875
+ const name = flagString(flags, "name");
876
+ if (!name)
877
+ throw new Error("`--name` is required");
878
+ const body = await request("/keys", {
879
+ method: "POST",
880
+ body: compact({
881
+ name,
882
+ workspace_id: flagString(flags, "workspace"),
883
+ limit: flagNumber(flags, "limit"),
884
+ limit_reset: flagString(flags, "limit-reset"),
885
+ expires_at: flagString(flags, "expires-at"),
886
+ disabled: flagBool(flags, "disabled") || undefined,
887
+ }),
888
+ });
889
+ if (flagBool(flags, "json"))
890
+ return printJson(body);
891
+ const key = body.data;
892
+ process.stdout.write(`Created API key: ${key.name ?? name}\n`);
893
+ process.stdout.write(`ID: ${key.id}\n`);
894
+ process.stdout.write(`Prefix: ${key.prefix ?? "unknown"}\n`);
895
+ if (flagBool(flags, "show-secret"))
896
+ process.stdout.write(`Key: ${key.key}\n`);
897
+ else
898
+ process.stdout.write("Secret hidden. Re-run with --json or --show-secret if an agent needs to capture it.\n");
899
+ }
900
+ async function getKey(id, flags) {
901
+ if (!id)
902
+ throw new Error("Key id or hash is required");
903
+ const body = await request(`/keys/${encodeURIComponent(id)}`);
904
+ if (flagBool(flags, "json"))
905
+ return printJson(body);
906
+ const key = body.data;
907
+ process.stdout.write(`${key.name ?? key.id}\nStatus: ${key.disabled ? "disabled" : "active"}\nID: ${key.id}\nPrefix: ${key.prefix ?? "unknown"}\n`);
908
+ }
909
+ async function updateKey(id, flags) {
910
+ if (!id)
911
+ throw new Error("Key id or hash is required");
912
+ const body = await request(`/keys/${encodeURIComponent(id)}`, {
913
+ method: "PATCH",
914
+ body: compact({
915
+ name: flagString(flags, "name"),
916
+ disabled: flags.disabled === undefined ? undefined : flagBool(flags, "disabled"),
917
+ limit: flagNumber(flags, "limit"),
918
+ limit_reset: flagString(flags, "limit-reset"),
919
+ expires_at: flagString(flags, "expires-at"),
920
+ }),
921
+ });
922
+ if (flagBool(flags, "json"))
923
+ return printJson(body);
924
+ process.stdout.write(`Updated API key: ${body.data.name ?? body.data.id}\n`);
925
+ }
926
+ async function deleteKey(id, flags) {
927
+ if (!id)
928
+ throw new Error("Key id or hash is required");
929
+ const body = await request(`/keys/${encodeURIComponent(id)}`, { method: "DELETE" });
930
+ if (flagBool(flags, "json"))
931
+ return printJson(body);
932
+ process.stdout.write("Deleted API key.\n");
933
+ }
934
+ async function currentKey(flags) {
935
+ const body = await request("/key");
936
+ if (flagBool(flags, "json"))
937
+ return printJson(body);
938
+ process.stdout.write(`${body.data.name ?? body.data.prefix ?? body.data.id}\n`);
939
+ process.stdout.write(`ID: ${body.data.id}\n`);
940
+ process.stdout.write(`Workspace: ${body.data.workspace_id}\n`);
941
+ process.stdout.write(`Status: ${body.data.status}\n`);
942
+ process.stdout.write(`Expires: ${body.data.expires_at ?? "never"}\n`);
943
+ }
944
+ async function listWorkspaces(flags) {
945
+ const body = await request(appendQuery("/workspaces", { limit: flagString(flags, "limit"), offset: flagString(flags, "offset") }));
946
+ if (flagBool(flags, "json"))
947
+ return printJson(body);
948
+ printList(body.data ?? [], (workspace) => `${workspace.name ?? workspace.id} ${workspace.slug ?? ""} ${workspace.id}`.trim());
949
+ }
950
+ async function createWorkspace(flags) {
951
+ const name = flagString(flags, "name");
952
+ if (!name)
953
+ throw new Error("`--name` is required");
954
+ const body = await request("/workspaces", { method: "POST", body: compact({ name, slug: flagString(flags, "slug") }) });
955
+ if (flagBool(flags, "json"))
956
+ return printJson(body);
957
+ process.stdout.write(`Created workspace: ${body.data.name} (${body.data.id})\n`);
958
+ }
959
+ async function getWorkspace(id, flags) {
960
+ if (!id)
961
+ throw new Error("Workspace id or slug is required");
962
+ const body = await request(`/workspaces/${encodeURIComponent(id)}`);
963
+ if (flagBool(flags, "json"))
964
+ return printJson(body);
965
+ process.stdout.write(`${body.data.name ?? body.data.id}\nSlug: ${body.data.slug ?? ""}\nID: ${body.data.id}\n`);
966
+ }
967
+ async function updateWorkspace(id, flags) {
968
+ if (!id)
969
+ throw new Error("Workspace id or slug is required");
970
+ const body = await request(`/workspaces/${encodeURIComponent(id)}`, {
971
+ method: "PATCH",
972
+ body: compact({ name: flagString(flags, "name"), slug: flagString(flags, "slug") }),
973
+ });
974
+ if (flagBool(flags, "json"))
975
+ return printJson(body);
976
+ process.stdout.write(`Updated workspace: ${body.data.name ?? body.data.id}\n`);
977
+ }
978
+ async function deleteWorkspace(id, flags) {
979
+ if (!id)
980
+ throw new Error("Workspace id or slug is required");
981
+ const body = await request(`/workspaces/${encodeURIComponent(id)}`, { method: "DELETE" });
982
+ if (flagBool(flags, "json"))
983
+ return printJson(body);
984
+ process.stdout.write("Deleted workspace.\n");
985
+ }
986
+ async function listWorkspaceMembers(id, flags) {
987
+ if (!id)
988
+ throw new Error("Workspace id or slug is required");
989
+ const body = await request(`/workspaces/${encodeURIComponent(id)}/members`);
990
+ if (flagBool(flags, "json"))
991
+ return printJson(body);
992
+ printList(body.data ?? [], (member) => `${member.role ?? "member"} ${member.user_id}`);
993
+ }
994
+ async function addWorkspaceMembers(id, flags) {
995
+ if (!id)
996
+ throw new Error("Workspace id or slug is required");
997
+ const userIds = (flagString(flags, "user-ids") ?? "")
998
+ .split(",")
999
+ .map((value) => value.trim())
1000
+ .filter(Boolean);
1001
+ if (!userIds.length)
1002
+ throw new Error("--user-ids is required");
1003
+ const body = await request(`/workspaces/${encodeURIComponent(id)}/members/add`, {
1004
+ method: "POST",
1005
+ body: compact({
1006
+ user_ids: userIds,
1007
+ role: flagString(flags, "role"),
1008
+ }),
1009
+ });
1010
+ if (flagBool(flags, "json"))
1011
+ return printJson(body);
1012
+ process.stdout.write(`Added ${body.added_count ?? userIds.length} member(s) to workspace.\n`);
1013
+ }
1014
+ async function removeWorkspaceMembers(id, flags) {
1015
+ if (!id)
1016
+ throw new Error("Workspace id or slug is required");
1017
+ const userIds = (flagString(flags, "user-ids") ?? "")
1018
+ .split(",")
1019
+ .map((value) => value.trim())
1020
+ .filter(Boolean);
1021
+ if (!userIds.length)
1022
+ throw new Error("--user-ids is required");
1023
+ const body = await request(`/workspaces/${encodeURIComponent(id)}/members/remove`, {
1024
+ method: "POST",
1025
+ body: { user_ids: userIds },
1026
+ });
1027
+ if (flagBool(flags, "json"))
1028
+ return printJson(body);
1029
+ process.stdout.write(`Removed ${body.removed_count ?? 0} member(s) from workspace.\n`);
1030
+ }
1031
+ function buildPresetConfig(flags) {
1032
+ const config = parseJsonFlag(flags, "config-json");
1033
+ const model = flagString(flags, "model");
1034
+ if (model)
1035
+ config.models = [model];
1036
+ const systemPrompt = flagString(flags, "system-prompt");
1037
+ if (systemPrompt)
1038
+ config.system_prompt = systemPrompt;
1039
+ const routingMode = flagString(flags, "routing-mode");
1040
+ if (routingMode)
1041
+ config.routing_mode = routingMode;
1042
+ return config;
1043
+ }
1044
+ async function listPresets(flags) {
1045
+ const body = await request(appendQuery("/presets", { limit: flagString(flags, "limit"), offset: flagString(flags, "offset"), visibility: flagString(flags, "visibility") }));
1046
+ if (flagBool(flags, "json"))
1047
+ return printJson(body);
1048
+ printList(body.data ?? [], (preset) => `${preset.name ?? preset.id} ${preset.visibility ?? "team"} ${preset.id}`);
1049
+ }
1050
+ async function createPreset(flags) {
1051
+ const name = flagString(flags, "name");
1052
+ if (!name)
1053
+ throw new Error("`--name` is required");
1054
+ const body = await request("/presets", {
1055
+ method: "POST",
1056
+ body: compact({
1057
+ name,
1058
+ slug: flagString(flags, "slug"),
1059
+ description: flagString(flags, "description"),
1060
+ visibility: flagString(flags, "visibility"),
1061
+ config: buildPresetConfig(flags),
1062
+ }),
1063
+ });
1064
+ if (flagBool(flags, "json"))
1065
+ return printJson(body);
1066
+ process.stdout.write(`Created preset: ${body.data.name} (${body.data.id})\n`);
1067
+ }
1068
+ async function getPreset(id, flags) {
1069
+ if (!id)
1070
+ throw new Error("Preset id, slug, or name is required");
1071
+ const body = await request(`/presets/${encodeURIComponent(id)}`);
1072
+ if (flagBool(flags, "json"))
1073
+ return printJson(body);
1074
+ process.stdout.write(`${body.data.name ?? body.data.id}\nVisibility: ${body.data.visibility}\nID: ${body.data.id}\n`);
1075
+ }
1076
+ async function updatePreset(id, flags) {
1077
+ if (!id)
1078
+ throw new Error("Preset id, slug, or name is required");
1079
+ const body = await request(`/presets/${encodeURIComponent(id)}`, {
1080
+ method: "PATCH",
1081
+ body: compact({
1082
+ name: flagString(flags, "name"),
1083
+ slug: flagString(flags, "slug"),
1084
+ description: flagString(flags, "description"),
1085
+ visibility: flagString(flags, "visibility"),
1086
+ config: buildPresetConfig(flags),
1087
+ }),
1088
+ });
1089
+ if (flagBool(flags, "json"))
1090
+ return printJson(body);
1091
+ process.stdout.write(`Updated preset: ${body.data.name ?? body.data.id}\n`);
1092
+ }
1093
+ async function deletePreset(id, flags) {
1094
+ if (!id)
1095
+ throw new Error("Preset id, slug, or name is required");
1096
+ const body = await request(`/presets/${encodeURIComponent(id)}`, { method: "DELETE" });
1097
+ if (flagBool(flags, "json"))
1098
+ return printJson(body);
1099
+ process.stdout.write("Deleted preset.\n");
1100
+ }
1101
+ async function settingsGet(flags) {
1102
+ const body = await request("/settings");
1103
+ if (flagBool(flags, "json"))
1104
+ return printJson(body);
1105
+ const settings = body.data ?? {};
1106
+ process.stdout.write(`Routing: ${settings.routing_mode ?? "balanced"}\n`);
1107
+ process.stdout.write(`Beta channel: ${Boolean(settings.beta_channel_enabled)}\n`);
1108
+ process.stdout.write(`Alpha channel: ${Boolean(settings.alpha_channel_enabled)}\n`);
1109
+ process.stdout.write(`Response healing: ${Boolean(settings.response_healing_enabled)}\n`);
1110
+ process.stdout.write(`BYOK fallback: ${Boolean(settings.byok_fallback_enabled)}\n`);
1111
+ }
1112
+ async function settingsUpdate(flags) {
1113
+ const patch = parseJsonFlag(flags, "body-json");
1114
+ const routingMode = flagString(flags, "routing-mode");
1115
+ if (routingMode)
1116
+ patch.routing_mode = routingMode;
1117
+ if (flags["beta-channel"] !== undefined)
1118
+ patch.beta_channel_enabled = flagBool(flags, "beta-channel");
1119
+ if (flags["alpha-channel"] !== undefined)
1120
+ patch.alpha_channel_enabled = flagBool(flags, "alpha-channel");
1121
+ if (flags["response-healing"] !== undefined)
1122
+ patch.response_healing_enabled = flagBool(flags, "response-healing");
1123
+ if (flags["byok-fallback"] !== undefined)
1124
+ patch.byok_fallback_enabled = flagBool(flags, "byok-fallback");
1125
+ if (Object.keys(patch).length === 0)
1126
+ throw new Error("Provide --routing-mode, another settings flag, or --body-json");
1127
+ const body = await request("/settings", { method: "PATCH", body: patch });
1128
+ if (flagBool(flags, "json"))
1129
+ return printJson(body);
1130
+ process.stdout.write("Updated workspace settings.\n");
1131
+ }
1132
+ async function listGuardrails(flags) {
1133
+ const body = await request(appendQuery("/guardrails", { limit: flagString(flags, "limit"), offset: flagString(flags, "offset") }));
1134
+ if (flagBool(flags, "json"))
1135
+ return printJson(body);
1136
+ printList(body.data ?? [], (guardrail) => `${guardrail.enabled === false ? "off" : "on"} ${guardrail.name ?? guardrail.id} ${guardrail.id}`);
1137
+ }
1138
+ async function createGuardrail(flags) {
1139
+ const name = flagString(flags, "name");
1140
+ if (!name)
1141
+ throw new Error("`--name` is required");
1142
+ const payload = parseJsonFlag(flags, "body-json");
1143
+ payload.name = name;
1144
+ if (flagString(flags, "description"))
1145
+ payload.description = flagString(flags, "description");
1146
+ const body = await request("/guardrails", { method: "POST", body: payload });
1147
+ if (flagBool(flags, "json"))
1148
+ return printJson(body);
1149
+ process.stdout.write(`Created guardrail: ${body.data.name ?? body.data.id} (${body.data.id})\n`);
1150
+ }
1151
+ async function getGuardrail(id, flags) {
1152
+ if (!id)
1153
+ throw new Error("Guardrail id is required");
1154
+ const body = await request(`/guardrails/${encodeURIComponent(id)}`);
1155
+ if (flagBool(flags, "json"))
1156
+ return printJson(body);
1157
+ process.stdout.write(`${body.data.name ?? body.data.id}\nEnabled: ${body.data.enabled !== false}\nID: ${body.data.id}\nKeys: ${(body.data.key_ids ?? []).join(", ")}\n`);
1158
+ }
1159
+ async function updateGuardrail(id, flags) {
1160
+ if (!id)
1161
+ throw new Error("Guardrail id is required");
1162
+ const payload = parseJsonFlag(flags, "body-json");
1163
+ if (flagString(flags, "name"))
1164
+ payload.name = flagString(flags, "name");
1165
+ if (flags.enabled !== undefined)
1166
+ payload.enabled = flagBool(flags, "enabled");
1167
+ if (Object.keys(payload).length === 0)
1168
+ throw new Error("Provide --body-json or a supported flag");
1169
+ const body = await request(`/guardrails/${encodeURIComponent(id)}`, { method: "PATCH", body: payload });
1170
+ if (flagBool(flags, "json"))
1171
+ return printJson(body);
1172
+ process.stdout.write(`Updated guardrail: ${body.data.name ?? body.data.id}\n`);
1173
+ }
1174
+ async function deleteGuardrail(id, flags) {
1175
+ if (!id)
1176
+ throw new Error("Guardrail id is required");
1177
+ const body = await request(`/guardrails/${encodeURIComponent(id)}`, { method: "DELETE" });
1178
+ if (flagBool(flags, "json"))
1179
+ return printJson(body);
1180
+ process.stdout.write("Deleted guardrail.\n");
1181
+ }
1182
+ async function setGuardrailKeys(id, flags) {
1183
+ if (!id)
1184
+ throw new Error("Guardrail id is required");
1185
+ const keyIds = (flagString(flags, "key-ids") ?? "")
1186
+ .split(",")
1187
+ .map((value) => value.trim())
1188
+ .filter(Boolean);
1189
+ const body = await request(`/guardrails/${encodeURIComponent(id)}/keys`, { method: "PUT", body: { key_ids: keyIds } });
1190
+ if (flagBool(flags, "json"))
1191
+ return printJson(body);
1192
+ process.stdout.write(`Assigned ${keyIds.length} key(s) to guardrail.\n`);
1193
+ }
1194
+ async function listGuardrailKeys(id, flags) {
1195
+ if (!id)
1196
+ throw new Error("Guardrail id is required");
1197
+ const body = await request(`/guardrails/${encodeURIComponent(id)}/keys`);
1198
+ if (flagBool(flags, "json"))
1199
+ return printJson(body);
1200
+ printList(body.data ?? [], (assignment) => `${assignment.name ?? assignment.prefix ?? assignment.key_id} ${assignment.key_id}`);
1201
+ }
1202
+ async function addGuardrailKeys(id, flags) {
1203
+ if (!id)
1204
+ throw new Error("Guardrail id is required");
1205
+ const keyIds = (flagString(flags, "key-ids") ?? "")
1206
+ .split(",")
1207
+ .map((value) => value.trim())
1208
+ .filter(Boolean);
1209
+ if (!keyIds.length)
1210
+ throw new Error("--key-ids is required");
1211
+ const body = await request(`/guardrails/${encodeURIComponent(id)}/keys/add`, {
1212
+ method: "POST",
1213
+ body: { key_ids: keyIds },
1214
+ });
1215
+ if (flagBool(flags, "json"))
1216
+ return printJson(body);
1217
+ process.stdout.write(`Added ${body.added_count ?? keyIds.length} key assignment(s).\n`);
1218
+ }
1219
+ async function removeGuardrailKeys(id, flags) {
1220
+ if (!id)
1221
+ throw new Error("Guardrail id is required");
1222
+ const keyIds = (flagString(flags, "key-ids") ?? "")
1223
+ .split(",")
1224
+ .map((value) => value.trim())
1225
+ .filter(Boolean);
1226
+ if (!keyIds.length)
1227
+ throw new Error("--key-ids is required");
1228
+ const body = await request(`/guardrails/${encodeURIComponent(id)}/keys/remove`, {
1229
+ method: "POST",
1230
+ body: { key_ids: keyIds },
1231
+ });
1232
+ if (flagBool(flags, "json"))
1233
+ return printJson(body);
1234
+ process.stdout.write(`Removed ${body.removed_count ?? 0} key assignment(s).\n`);
1235
+ }
1236
+ async function listGuardrailMembers(id, flags) {
1237
+ if (!id)
1238
+ throw new Error("Guardrail id is required");
1239
+ const body = await request(`/guardrails/${encodeURIComponent(id)}/members`);
1240
+ if (flagBool(flags, "json"))
1241
+ return printJson(body);
1242
+ printList(body.data ?? [], (assignment) => `${assignment.display_name ?? assignment.user_id} ${assignment.role ?? "member"} ${assignment.user_id}`);
1243
+ }
1244
+ async function addGuardrailMembers(id, flags) {
1245
+ if (!id)
1246
+ throw new Error("Guardrail id is required");
1247
+ const userIds = parseCsvFlag(flags, "user-ids");
1248
+ if (!userIds.length)
1249
+ throw new Error("--user-ids is required");
1250
+ const body = await request(`/guardrails/${encodeURIComponent(id)}/members/add`, {
1251
+ method: "POST",
1252
+ body: { user_ids: userIds },
1253
+ });
1254
+ if (flagBool(flags, "json"))
1255
+ return printJson(body);
1256
+ process.stdout.write(`Added ${body.added_count ?? userIds.length} member assignment(s).\n`);
1257
+ }
1258
+ async function removeGuardrailMembers(id, flags) {
1259
+ if (!id)
1260
+ throw new Error("Guardrail id is required");
1261
+ const userIds = parseCsvFlag(flags, "user-ids");
1262
+ if (!userIds.length)
1263
+ throw new Error("--user-ids is required");
1264
+ const body = await request(`/guardrails/${encodeURIComponent(id)}/members/remove`, {
1265
+ method: "POST",
1266
+ body: { user_ids: userIds },
1267
+ });
1268
+ if (flagBool(flags, "json"))
1269
+ return printJson(body);
1270
+ process.stdout.write(`Removed ${body.removed_count ?? 0} member assignment(s).\n`);
1271
+ }
1272
+ function buildOauthClientPayload(flags) {
1273
+ const redirectUris = [
1274
+ ...parseCsvFlag(flags, "redirect-uris"),
1275
+ ...parseCsvFlag(flags, "redirect-uri"),
1276
+ ];
1277
+ const uniqueRedirectUris = Array.from(new Set(redirectUris));
1278
+ const scopes = parseCsvFlag(flags, "scopes");
1279
+ return compact({
1280
+ name: flagString(flags, "name"),
1281
+ client_type: flagString(flags, "client-type"),
1282
+ allowed_scopes: scopes.length ? scopes : undefined,
1283
+ description: flagString(flags, "description"),
1284
+ homepage_url: flagString(flags, "homepage-url"),
1285
+ logo_url: flagString(flags, "logo-url"),
1286
+ privacy_policy_url: flagString(flags, "privacy-policy-url"),
1287
+ terms_of_service_url: flagString(flags, "terms-of-service-url"),
1288
+ redirect_uris: uniqueRedirectUris.length ? uniqueRedirectUris : undefined,
1289
+ });
1290
+ }
1291
+ async function listOauthClients(flags) {
1292
+ const body = await request("/oauth-clients");
1293
+ if (flagBool(flags, "json"))
1294
+ return printJson(body);
1295
+ printList(body.data ?? [], (client) => `${client.name ?? client.client_id} ${client.client_id}`);
1296
+ }
1297
+ async function createOauthClient(flags) {
1298
+ const payload = buildOauthClientPayload(flags);
1299
+ if (!payload.name)
1300
+ throw new Error("--name is required");
1301
+ if (!Array.isArray(payload.redirect_uris) || payload.redirect_uris.length === 0) {
1302
+ throw new Error("--redirect-uri or --redirect-uris is required");
1303
+ }
1304
+ const body = await request("/oauth-clients", { method: "POST", body: payload });
1305
+ if (flagBool(flags, "json"))
1306
+ return printJson(body);
1307
+ process.stdout.write(`Created OAuth client: ${body.name ?? payload.name}\nClient ID: ${body.client_id}\n`);
1308
+ process.stdout.write(renderOneTimeClientSecret(body.client_secret, flagBool(flags, "show-secret")));
1309
+ }
1310
+ async function getOauthClient(id, flags) {
1311
+ if (!id)
1312
+ throw new Error("OAuth client id is required");
1313
+ const body = await request(`/oauth-clients/${encodeURIComponent(id)}`);
1314
+ if (flagBool(flags, "json"))
1315
+ return printJson(body);
1316
+ process.stdout.write(`${body.name ?? body.client_id}\nClient ID: ${body.client_id}\nRedirect URIs: ${(body.redirect_uris ?? []).join(", ")}\n`);
1317
+ }
1318
+ async function updateOauthClient(id, flags) {
1319
+ if (!id)
1320
+ throw new Error("OAuth client id is required");
1321
+ const payload = buildOauthClientPayload(flags);
1322
+ if (Object.keys(payload).length === 0)
1323
+ throw new Error("Provide at least one field to update");
1324
+ const body = await request(`/oauth-clients/${encodeURIComponent(id)}`, { method: "PATCH", body: payload });
1325
+ if (flagBool(flags, "json"))
1326
+ return printJson(body);
1327
+ process.stdout.write(`Updated OAuth client: ${body.name ?? body.client_id}\n`);
1328
+ }
1329
+ async function deleteOauthClient(id, flags) {
1330
+ if (!id)
1331
+ throw new Error("OAuth client id is required");
1332
+ const body = await request(`/oauth-clients/${encodeURIComponent(id)}`, { method: "DELETE" });
1333
+ if (flagBool(flags, "json"))
1334
+ return printJson(body);
1335
+ process.stdout.write(`${body.message ?? "Deleted OAuth client."}\n`);
1336
+ }
1337
+ async function regenerateOauthClientSecret(id, flags) {
1338
+ if (!id)
1339
+ throw new Error("OAuth client id is required");
1340
+ const body = await request(`/oauth-clients/${encodeURIComponent(id)}/regenerate-secret`, { method: "POST" });
1341
+ if (flagBool(flags, "json"))
1342
+ return printJson(body);
1343
+ process.stdout.write(`Regenerated secret for ${body.client_id}\n`);
1344
+ process.stdout.write(renderOneTimeClientSecret(body.client_secret, flagBool(flags, "show-secret")));
1345
+ }
1346
+ async function listManagementKeys(flags) {
1347
+ const body = await request(appendQuery("/management-keys", { limit: flagString(flags, "limit"), offset: flagString(flags, "offset") }));
1348
+ if (flagBool(flags, "json"))
1349
+ return printJson(body);
1350
+ printList(body.data ?? [], (key) => `${key.status ?? "unknown"} ${key.name ?? key.id} ${key.id} ${key.prefix ?? ""}`.trim());
1351
+ }
1352
+ async function createManagementKey(flags) {
1353
+ const name = flagString(flags, "name");
1354
+ if (!name)
1355
+ throw new Error("`--name` is required");
1356
+ const body = await request("/management-keys", {
1357
+ method: "POST",
1358
+ body: compact({
1359
+ name,
1360
+ template: flagString(flags, "template"),
1361
+ scopes: flagString(flags, "scopes")?.split(",").map((scope) => scope.trim()).filter(Boolean),
1362
+ expires_at: flagString(flags, "expires-at"),
1363
+ paused: flagBool(flags, "paused") || undefined,
1364
+ }),
1365
+ });
1366
+ if (flagBool(flags, "json"))
1367
+ return printJson(body);
1368
+ process.stdout.write(`Created management key: ${body.data.name ?? name}\nID: ${body.data.id}\nPrefix: ${body.data.prefix ?? "unknown"}\n`);
1369
+ if (flagBool(flags, "show-secret"))
1370
+ process.stdout.write(`Key: ${body.data.key}\n`);
1371
+ else
1372
+ process.stdout.write("Secret hidden. Re-run with --json or --show-secret if an agent needs to capture it.\n");
1373
+ }
1374
+ async function getManagementKey(id, flags) {
1375
+ if (!id)
1376
+ throw new Error("Management key id is required");
1377
+ const body = await request(`/management-keys/${encodeURIComponent(id)}`);
1378
+ if (flagBool(flags, "json"))
1379
+ return printJson(body);
1380
+ process.stdout.write(`${body.data.name ?? body.data.id}\nStatus: ${body.data.status}\nID: ${body.data.id}\nPrefix: ${body.data.prefix ?? "unknown"}\n`);
1381
+ }
1382
+ async function updateManagementKey(id, flags) {
1383
+ if (!id)
1384
+ throw new Error("Management key id is required");
1385
+ const payload = {};
1386
+ if (flagString(flags, "name"))
1387
+ payload.name = flagString(flags, "name");
1388
+ if (flagString(flags, "template"))
1389
+ payload.template = flagString(flags, "template");
1390
+ if (flags.paused !== undefined)
1391
+ payload.paused = flagBool(flags, "paused");
1392
+ if (flagString(flags, "expires-at"))
1393
+ payload.expires_at = flagString(flags, "expires-at");
1394
+ Object.assign(payload, parseJsonFlag(flags, "body-json"));
1395
+ if (Object.keys(payload).length === 0)
1396
+ throw new Error("Provide a supported flag or --body-json");
1397
+ const body = await request(`/management-keys/${encodeURIComponent(id)}`, { method: "PATCH", body: payload });
1398
+ if (flagBool(flags, "json"))
1399
+ return printJson(body);
1400
+ process.stdout.write(`Updated management key: ${body.data.name ?? body.data.id}\n`);
1401
+ }
1402
+ async function deleteManagementKey(id, flags) {
1403
+ if (!id)
1404
+ throw new Error("Management key id is required");
1405
+ const body = await request(`/management-keys/${encodeURIComponent(id)}`, { method: "DELETE" });
1406
+ if (flagBool(flags, "json"))
1407
+ return printJson(body);
1408
+ process.stdout.write("Deleted management key.\n");
1409
+ }
1410
+ export function buildModelsListPath(flags) {
1411
+ const mine = flagBool(flags, "mine");
1412
+ return appendQuery(mine ? "/models/me" : "/models", {
1413
+ limit: flagString(flags, "limit"),
1414
+ offset: flagString(flags, "offset"),
1415
+ availability: flagBool(flags, "all") ? "all" : undefined,
1416
+ });
1417
+ }
1418
+ async function listModels(flags) {
1419
+ const body = await request(buildModelsListPath(flags));
1420
+ if (flagBool(flags, "json"))
1421
+ return printJson(body);
1422
+ printList(body.models ?? body.data ?? [], (model) => `${model.id ?? model.model_id} ${model.name ?? ""}`.trim());
1423
+ }
1424
+ async function listProviders(flags) {
1425
+ const body = await request(appendQuery("/providers", { limit: flagString(flags, "limit"), offset: flagString(flags, "offset") }));
1426
+ if (flagBool(flags, "json"))
1427
+ return printJson(body);
1428
+ printList(body.providers ?? [], (provider) => `${provider.api_provider_id} ${provider.api_provider_name ?? ""}`.trim());
1429
+ }
1430
+ async function getModel(id, flags) {
1431
+ if (!id)
1432
+ throw new Error("Model id is required");
1433
+ const body = await request(appendQuery("/models", { id, limit: 1 }));
1434
+ const model = (body.models ?? body.data ?? [])[0];
1435
+ if (!model)
1436
+ throw new Error(`Model not found: ${id}`);
1437
+ if (flagBool(flags, "json"))
1438
+ return printJson(model);
1439
+ process.stdout.write(`${model.id ?? model.model_id}\n${model.name ?? ""}\n`);
1440
+ }
1441
+ async function listOrganisations(flags) {
1442
+ const body = await request(appendQuery("/organisations", {
1443
+ limit: flagString(flags, "limit"),
1444
+ offset: flagString(flags, "offset"),
1445
+ }));
1446
+ if (flagBool(flags, "json"))
1447
+ return printJson(body);
1448
+ printList(body.organisations ?? [], (organisation) => `${organisation.organisation_id} ${organisation.name ?? ""}`.trim());
1449
+ }
1450
+ async function listEndpoints(flags) {
1451
+ const body = await request("/endpoints");
1452
+ if (flagBool(flags, "json"))
1453
+ return printJson(body);
1454
+ printList(body.endpoints ?? [], (endpoint) => String(endpoint));
1455
+ }
1456
+ async function pricingModels(flags) {
1457
+ const body = await request("/pricing/models");
1458
+ if (flagBool(flags, "json"))
1459
+ return printJson(body);
1460
+ printList(body.models ?? [], (model) => `${model.provider}/${model.model} ${model.endpoint} ${model.meters?.length ?? 0} meters`);
1461
+ }
1462
+ async function pricingCalculate(flags) {
1463
+ const provider = flagString(flags, "provider");
1464
+ const model = flagString(flags, "model");
1465
+ const endpoint = flagString(flags, "endpoint");
1466
+ if (!provider || !model || !endpoint)
1467
+ throw new Error("--provider, --model, and --endpoint are required");
1468
+ const body = await request("/pricing/calculate", {
1469
+ method: "POST",
1470
+ body: {
1471
+ provider,
1472
+ model,
1473
+ endpoint,
1474
+ usage: parseJsonFlag(flags, "usage-json"),
1475
+ },
1476
+ });
1477
+ return printJson(body);
1478
+ }
1479
+ async function creditsGet(flags) {
1480
+ const body = await request(appendQuery("/credits", { workspace_id: flagString(flags, "workspace") }));
1481
+ if (flagBool(flags, "json"))
1482
+ return printJson(body);
1483
+ process.stdout.write(`Available: ${formatMoneyFromNanos(body.credits?.available_nanos)}\nReserved: ${formatMoneyFromNanos(body.credits?.reserved_nanos)}\n30d requests: ${body.credits?.thirty_day_requests ?? 0}\n`);
1484
+ }
1485
+ async function activityList(flags) {
1486
+ const body = await request(appendQuery("/activity", {
1487
+ workspace_id: flagString(flags, "workspace"),
1488
+ days: flagString(flags, "days"),
1489
+ limit: flagString(flags, "limit"),
1490
+ offset: flagString(flags, "offset"),
1491
+ }));
1492
+ if (flagBool(flags, "json"))
1493
+ return printJson(body);
1494
+ printList(body.activity ?? [], (item) => `${item.timestamp} ${item.model} ${item.provider} ${item.cost_cents ?? 0}c ${item.request_id}`);
1495
+ }
1496
+ export function buildLogsListPath(flags) {
1497
+ return appendQuery("/logs", {
1498
+ workspace_id: flagString(flags, "workspace"),
1499
+ since: flagString(flags, "since"),
1500
+ from: flagString(flags, "from"),
1501
+ to: flagString(flags, "to"),
1502
+ status: flagString(flags, "status"),
1503
+ provider: flagString(flags, "provider"),
1504
+ model: flagString(flags, "model"),
1505
+ endpoint: flagString(flags, "endpoint"),
1506
+ request_id: flagString(flags, "request-id"),
1507
+ key_id: flagString(flags, "key-id"),
1508
+ session_id: flagString(flags, "session-id"),
1509
+ error_code: flagString(flags, "error-code"),
1510
+ limit: flagString(flags, "limit"),
1511
+ offset: flagString(flags, "offset"),
1512
+ });
1513
+ }
1514
+ async function logsList(flags) {
1515
+ const body = await request(buildLogsListPath(flags));
1516
+ if (flagBool(flags, "json"))
1517
+ return printJson(body);
1518
+ printList(body.data ?? [], (item) => {
1519
+ const status = item.status_code ?? (item.success ? "success" : "error");
1520
+ return `${item.created_at} ${status} ${item.model_id ?? "unknown"} ${item.provider ?? "unknown"} ${item.latency_ms ?? 0}ms ${item.request_id}`;
1521
+ });
1522
+ }
1523
+ async function logsGet(id, flags) {
1524
+ if (!id)
1525
+ throw new Error("Request id is required");
1526
+ const body = await request(appendQuery(`/logs/${encodeURIComponent(id)}`, {
1527
+ workspace_id: flagString(flags, "workspace"),
1528
+ }));
1529
+ if (flagBool(flags, "json"))
1530
+ return printJson(body);
1531
+ const item = body.data ?? {};
1532
+ const errorMessage = item.error_message ? ` - ${sanitizeTerminalText(String(item.error_message))}` : "";
1533
+ process.stdout.write(`${item.request_id ?? id}\nStatus: ${item.status_code ?? (item.success ? "success" : "error")}\nModel: ${item.model_id ?? "unknown"}\nProvider: ${item.provider ?? "unknown"}\nEndpoint: ${item.endpoint ?? "unknown"}\nLatency: ${item.latency_ms ?? 0}ms\nError: ${item.error_code ?? "none"}${errorMessage}\n`);
1534
+ }
1535
+ async function analyticsGet(flags) {
1536
+ const body = await request(appendQuery("/analytics", { workspace_id: flagString(flags, "workspace"), date: flagString(flags, "date") }));
1537
+ if (flagBool(flags, "json"))
1538
+ return printJson(body);
1539
+ printList(body.data ?? [], (item) => `${item.date} ${item.model_permaslug} ${item.provider_name} ${item.requests} req $${item.usage}`);
1540
+ }
1541
+ async function generationGet(flags) {
1542
+ const id = flagString(flags, "id");
1543
+ if (!id)
1544
+ throw new Error("--id is required");
1545
+ const body = await request(appendQuery("/generations", { id }));
1546
+ if (flagBool(flags, "json"))
1547
+ return printJson(body);
1548
+ process.stdout.write(`${body.request_id ?? id}\nModel: ${body.model_id ?? body.model ?? "unknown"}\nProvider: ${body.provider ?? "unknown"}\nCost: ${formatMoneyFromNanos(body.cost_nanos)}\nReplay supported: ${Boolean(body.replay_supported)}\n`);
1549
+ }
1550
+ async function listWebhooks(flags) {
1551
+ const body = await request(appendQuery("/webhook-endpoints", {
1552
+ limit: flagString(flags, "limit"),
1553
+ offset: flagString(flags, "offset"),
1554
+ include_deleted: flagBool(flags, "include-deleted") || undefined,
1555
+ }));
1556
+ if (flagBool(flags, "json"))
1557
+ return printJson(body);
1558
+ printList(body.data ?? [], (endpoint) => `${endpoint.status ?? "unknown"} ${endpoint.name ?? endpoint.id} ${endpoint.url ?? ""} ${endpoint.id}`.trim());
1559
+ }
1560
+ function renderWebhookSecret(secret, showSecret) {
1561
+ if (typeof secret !== "string" || !secret)
1562
+ return "";
1563
+ return showSecret
1564
+ ? `Signing secret: ${secret}\n`
1565
+ : "Signing secret hidden. Re-run with --json or --show-secret to capture it once.\n";
1566
+ }
1567
+ async function createWebhook(flags) {
1568
+ const url = flagString(flags, "url");
1569
+ if (!url)
1570
+ throw new Error("--url is required");
1571
+ const body = await request("/webhook-endpoints", {
1572
+ method: "POST",
1573
+ body: compact({
1574
+ name: flagString(flags, "name"),
1575
+ url,
1576
+ events: flagString(flags, "events") ? parseCsvFlag(flags, "events") : undefined,
1577
+ }),
1578
+ });
1579
+ if (flagBool(flags, "json"))
1580
+ return printJson(body);
1581
+ process.stdout.write(`Created webhook endpoint: ${body.name ?? body.id}\nID: ${body.id}\n`);
1582
+ process.stdout.write(renderWebhookSecret(body.signing_secret, flagBool(flags, "show-secret")));
1583
+ }
1584
+ async function getWebhook(id, flags) {
1585
+ if (!id)
1586
+ throw new Error("Webhook endpoint id is required");
1587
+ const body = await request(`/webhook-endpoints/${encodeURIComponent(id)}`);
1588
+ if (flagBool(flags, "json"))
1589
+ return printJson(body);
1590
+ process.stdout.write(`${body.name ?? body.id}\nStatus: ${body.status ?? "unknown"}\nURL: ${body.url ?? "unknown"}\nID: ${body.id}\n`);
1591
+ }
1592
+ async function updateWebhook(id, flags) {
1593
+ if (!id)
1594
+ throw new Error("Webhook endpoint id is required");
1595
+ const payload = compact({
1596
+ name: flagString(flags, "name"),
1597
+ url: flagString(flags, "url"),
1598
+ events: flagString(flags, "events") ? parseCsvFlag(flags, "events") : undefined,
1599
+ status: flagString(flags, "status"),
1600
+ });
1601
+ if (Object.keys(payload).length === 0)
1602
+ throw new Error("Provide --name, --url, --events, or --status");
1603
+ const body = await request(`/webhook-endpoints/${encodeURIComponent(id)}`, { method: "PATCH", body: payload });
1604
+ if (flagBool(flags, "json"))
1605
+ return printJson(body);
1606
+ process.stdout.write(`Updated webhook endpoint: ${body.name ?? body.id}\n`);
1607
+ }
1608
+ async function rotateWebhookSecret(id, flags) {
1609
+ if (!id)
1610
+ throw new Error("Webhook endpoint id is required");
1611
+ const body = await request(`/webhook-endpoints/${encodeURIComponent(id)}/rotate-secret`, { method: "POST", body: {} });
1612
+ if (flagBool(flags, "json"))
1613
+ return printJson(body);
1614
+ process.stdout.write(`Rotated webhook signing secret: ${body.name ?? body.id}\n`);
1615
+ process.stdout.write(renderWebhookSecret(body.signing_secret, flagBool(flags, "show-secret")));
1616
+ }
1617
+ async function deleteWebhook(id, flags) {
1618
+ if (!id)
1619
+ throw new Error("Webhook endpoint id is required");
1620
+ const body = await request(`/webhook-endpoints/${encodeURIComponent(id)}`, { method: "DELETE" });
1621
+ if (flagBool(flags, "json"))
1622
+ return printJson(body);
1623
+ process.stdout.write("Deleted webhook endpoint.\n");
1624
+ }
1625
+ async function rawApi(method, path, flags) {
1626
+ if (!path)
1627
+ throw new Error("API path is required");
1628
+ const normalizedPath = path.startsWith("/v1/") ? path.slice(3) : path;
1629
+ const body = method === "GET" || method === "DELETE" ? undefined : parseJsonFlag(flags, "body-json");
1630
+ const response = await request(normalizedPath, { method, body });
1631
+ printJson(response);
1632
+ }
1633
+ async function main() {
1634
+ const parsed = parseArgs(process.argv.slice(2));
1635
+ const json = flagBool(parsed.flags, "json");
1636
+ try {
1637
+ const [first, second, third] = parsed.command;
1638
+ if (flagBool(parsed.flags, "version")) {
1639
+ await printVersion(parsed.flags, { short: true });
1640
+ return;
1641
+ }
1642
+ if (!first) {
1643
+ printHelp();
1644
+ return;
1645
+ }
1646
+ if (first === "help") {
1647
+ printHelp(parsed.command.slice(1));
1648
+ return;
1649
+ }
1650
+ if (flagBool(parsed.flags, "help")) {
1651
+ printHelp(parsed.command);
1652
+ return;
1653
+ }
1654
+ if (first === "version") {
1655
+ await printVersion(parsed.flags);
1656
+ return;
1657
+ }
1658
+ let action = null;
1659
+ if (first === "login")
1660
+ action = login(parsed.flags);
1661
+ else if (first === "logout")
1662
+ action = logout(parsed.flags);
1663
+ else if (first === "whoami")
1664
+ action = whoami(parsed.flags);
1665
+ else if (first === "keys" && second === "current")
1666
+ action = currentKey(parsed.flags);
1667
+ else if (first === "keys" && second === "list")
1668
+ action = listKeys(parsed.flags);
1669
+ else if (first === "keys" && second === "create")
1670
+ action = createKey(parsed.flags);
1671
+ else if (first === "keys" && second === "get")
1672
+ action = getKey(third, parsed.flags);
1673
+ else if (first === "keys" && second === "update")
1674
+ action = updateKey(third, parsed.flags);
1675
+ else if (first === "keys" && second === "delete")
1676
+ action = deleteKey(third, parsed.flags);
1677
+ else if (first === "workspaces" && second === "list")
1678
+ action = listWorkspaces(parsed.flags);
1679
+ else if (first === "workspaces" && second === "create")
1680
+ action = createWorkspace(parsed.flags);
1681
+ else if (first === "workspaces" && second === "get")
1682
+ action = getWorkspace(third, parsed.flags);
1683
+ else if (first === "workspaces" && second === "update")
1684
+ action = updateWorkspace(third, parsed.flags);
1685
+ else if (first === "workspaces" && second === "delete")
1686
+ action = deleteWorkspace(third, parsed.flags);
1687
+ else if (first === "workspaces" && second === "members")
1688
+ action = listWorkspaceMembers(third, parsed.flags);
1689
+ else if (first === "workspaces" && second === "add-members")
1690
+ action = addWorkspaceMembers(third, parsed.flags);
1691
+ else if (first === "workspaces" && second === "remove-members")
1692
+ action = removeWorkspaceMembers(third, parsed.flags);
1693
+ else if (first === "presets" && second === "list")
1694
+ action = listPresets(parsed.flags);
1695
+ else if (first === "presets" && second === "create")
1696
+ action = createPreset(parsed.flags);
1697
+ else if (first === "presets" && second === "get")
1698
+ action = getPreset(third, parsed.flags);
1699
+ else if (first === "presets" && second === "update")
1700
+ action = updatePreset(third, parsed.flags);
1701
+ else if (first === "presets" && second === "delete")
1702
+ action = deletePreset(third, parsed.flags);
1703
+ else if (first === "settings" && second === "get")
1704
+ action = settingsGet(parsed.flags);
1705
+ else if (first === "settings" && second === "update")
1706
+ action = settingsUpdate(parsed.flags);
1707
+ else if (first === "guardrails" && second === "list")
1708
+ action = listGuardrails(parsed.flags);
1709
+ else if (first === "guardrails" && second === "create")
1710
+ action = createGuardrail(parsed.flags);
1711
+ else if (first === "guardrails" && second === "get")
1712
+ action = getGuardrail(third, parsed.flags);
1713
+ else if (first === "guardrails" && second === "update")
1714
+ action = updateGuardrail(third, parsed.flags);
1715
+ else if (first === "guardrails" && second === "delete")
1716
+ action = deleteGuardrail(third, parsed.flags);
1717
+ else if (first === "guardrails" && second === "list-keys")
1718
+ action = listGuardrailKeys(third, parsed.flags);
1719
+ else if (first === "guardrails" && second === "add-keys")
1720
+ action = addGuardrailKeys(third, parsed.flags);
1721
+ else if (first === "guardrails" && second === "remove-keys")
1722
+ action = removeGuardrailKeys(third, parsed.flags);
1723
+ else if (first === "guardrails" && second === "list-members")
1724
+ action = listGuardrailMembers(third, parsed.flags);
1725
+ else if (first === "guardrails" && second === "add-members")
1726
+ action = addGuardrailMembers(third, parsed.flags);
1727
+ else if (first === "guardrails" && second === "remove-members")
1728
+ action = removeGuardrailMembers(third, parsed.flags);
1729
+ else if (first === "guardrails" && second === "set-keys")
1730
+ action = setGuardrailKeys(third, parsed.flags);
1731
+ else if (first === "oauth-clients" && second === "list")
1732
+ action = listOauthClients(parsed.flags);
1733
+ else if (first === "oauth-clients" && second === "create")
1734
+ action = createOauthClient(parsed.flags);
1735
+ else if (first === "oauth-clients" && second === "get")
1736
+ action = getOauthClient(third, parsed.flags);
1737
+ else if (first === "oauth-clients" && second === "update")
1738
+ action = updateOauthClient(third, parsed.flags);
1739
+ else if (first === "oauth-clients" && second === "delete")
1740
+ action = deleteOauthClient(third, parsed.flags);
1741
+ else if (first === "oauth-clients" && second === "regenerate-secret")
1742
+ action = regenerateOauthClientSecret(third, parsed.flags);
1743
+ else if (first === "management-keys" && second === "list")
1744
+ action = listManagementKeys(parsed.flags);
1745
+ else if (first === "management-keys" && second === "create")
1746
+ action = createManagementKey(parsed.flags);
1747
+ else if (first === "management-keys" && second === "get")
1748
+ action = getManagementKey(third, parsed.flags);
1749
+ else if (first === "management-keys" && second === "update")
1750
+ action = updateManagementKey(third, parsed.flags);
1751
+ else if (first === "management-keys" && second === "delete")
1752
+ action = deleteManagementKey(third, parsed.flags);
1753
+ else if (first === "models" && second === "list")
1754
+ action = listModels(parsed.flags);
1755
+ else if (first === "models" && second === "get")
1756
+ action = getModel(third, parsed.flags);
1757
+ else if (first === "providers" && second === "list")
1758
+ action = listProviders(parsed.flags);
1759
+ else if (first === "organisations" && second === "list")
1760
+ action = listOrganisations(parsed.flags);
1761
+ else if (first === "endpoints" && second === "list")
1762
+ action = listEndpoints(parsed.flags);
1763
+ else if (first === "pricing" && second === "models")
1764
+ action = pricingModels(parsed.flags);
1765
+ else if (first === "pricing" && second === "calculate")
1766
+ action = pricingCalculate(parsed.flags);
1767
+ else if (first === "credits" && second === "get")
1768
+ action = creditsGet(parsed.flags);
1769
+ else if (first === "activity" && second === "list")
1770
+ action = activityList(parsed.flags);
1771
+ else if (first === "logs" && second === "list")
1772
+ action = logsList(parsed.flags);
1773
+ else if (first === "logs" && second === "get")
1774
+ action = logsGet(third, parsed.flags);
1775
+ else if (first === "analytics" && second === "get")
1776
+ action = analyticsGet(parsed.flags);
1777
+ else if (first === "generation" && second === "get")
1778
+ action = generationGet(parsed.flags);
1779
+ else if (first === "webhooks" && second === "list")
1780
+ action = listWebhooks(parsed.flags);
1781
+ else if (first === "webhooks" && second === "create")
1782
+ action = createWebhook(parsed.flags);
1783
+ else if (first === "webhooks" && second === "get")
1784
+ action = getWebhook(third, parsed.flags);
1785
+ else if (first === "webhooks" && second === "update")
1786
+ action = updateWebhook(third, parsed.flags);
1787
+ else if (first === "webhooks" && second === "rotate-secret")
1788
+ action = rotateWebhookSecret(third, parsed.flags);
1789
+ else if (first === "webhooks" && second === "delete")
1790
+ action = deleteWebhook(third, parsed.flags);
1791
+ else if (first === "api" && second === "get")
1792
+ action = rawApi("GET", third, parsed.flags);
1793
+ else if (first === "api" && second === "post")
1794
+ action = rawApi("POST", third, parsed.flags);
1795
+ else if (first === "api" && second === "put")
1796
+ action = rawApi("PUT", third, parsed.flags);
1797
+ else if (first === "api" && second === "patch")
1798
+ action = rawApi("PATCH", third, parsed.flags);
1799
+ else if (first === "api" && second === "delete")
1800
+ action = rawApi("DELETE", third, parsed.flags);
1801
+ if (action) {
1802
+ await action;
1803
+ await maybePrintUpdateNotice(json);
1804
+ return;
1805
+ }
1806
+ throw new Error(`Unknown command: ${parsed.command.join(" ")}`);
1807
+ }
1808
+ catch (error) {
1809
+ printError(error, { json });
1810
+ process.exitCode = 1;
1811
+ }
1812
+ }
1813
+ await main();
1814
+ //# sourceMappingURL=index.js.map