@gravity-rail/cli 0.7.2 → 0.8.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.
@@ -0,0 +1,761 @@
1
+ import {
2
+ CredentialsManager,
3
+ getEnvironmentConfig,
4
+ printTable,
5
+ refreshAccessToken
6
+ } from "./chunk-H62EC2CF.js";
7
+ import {
8
+ AuthRequiredError,
9
+ CLIError
10
+ } from "./chunk-7VZ4JLAJ.js";
11
+
12
+ // src/lib/pagination.ts
13
+ function isListResult(data) {
14
+ if (!data || typeof data !== "object" || Array.isArray(data)) return false;
15
+ const obj = data;
16
+ return Array.isArray(obj.items) && ("supportsAll" in obj || "meta" in obj && (obj.meta === void 0 || typeof obj.meta === "object"));
17
+ }
18
+ function isPagePaginatedResponse(data) {
19
+ if (!data || typeof data !== "object") return false;
20
+ const obj = data;
21
+ if (!Array.isArray(obj.items) || typeof obj.page !== "number") return false;
22
+ if (typeof obj.totalPages === "number") return true;
23
+ if (typeof obj.total === "number" && typeof obj.pageSize === "number") return true;
24
+ return false;
25
+ }
26
+ function isCursorPaginatedResponse(data) {
27
+ if (!data || typeof data !== "object") return false;
28
+ const obj = data;
29
+ return Array.isArray(obj.items) && typeof obj.hasMore === "boolean";
30
+ }
31
+ function isPaginatedResponse(data) {
32
+ return isPagePaginatedResponse(data) || isCursorPaginatedResponse(data);
33
+ }
34
+ function extractPaginationMeta(data) {
35
+ if (isPagePaginatedResponse(data)) {
36
+ const totalPages = data.totalPages ?? (data.pageSize ? Math.ceil(data.total / data.pageSize) : void 0);
37
+ return {
38
+ total: data.total,
39
+ page: data.page,
40
+ totalPages
41
+ };
42
+ }
43
+ if (isCursorPaginatedResponse(data)) {
44
+ return {
45
+ hasMore: data.hasMore,
46
+ nextCursor: data.nextCursor
47
+ };
48
+ }
49
+ return void 0;
50
+ }
51
+ function formatPaginationWarning(meta, supportsAll = true) {
52
+ if (!meta) return void 0;
53
+ const allHint = supportsAll ? " Use --all to fetch all pages." : "";
54
+ if (meta.totalPages !== void 0 && meta.totalPages > 1) {
55
+ const pageNum = meta.page ?? 1;
56
+ return `Showing page ${pageNum} of ${meta.totalPages} (${meta.total} total items).${allHint}`;
57
+ }
58
+ if (meta.hasMore === true) {
59
+ return `More results available.${allHint}`;
60
+ }
61
+ return void 0;
62
+ }
63
+ function unwrapItems(data) {
64
+ if (Array.isArray(data)) return data;
65
+ if (data && typeof data === "object" && "items" in data && Array.isArray(data.items)) {
66
+ return data.items;
67
+ }
68
+ return [data];
69
+ }
70
+ var LIST_PAGINATION_ARGS = [
71
+ { name: "page", description: "Page number" },
72
+ { name: "page-size", description: "Page size" },
73
+ { name: "all", description: "Fetch all pages and return combined results", isFlag: true }
74
+ ];
75
+ function parsePaginationArgs(args) {
76
+ const result = {};
77
+ if (args.all === true) {
78
+ result.all = true;
79
+ }
80
+ if (args.page !== void 0) {
81
+ const num = Number(args.page);
82
+ if (isNaN(num) || num < 1) throw new CLIError("--page must be a positive integer");
83
+ result.page = num;
84
+ }
85
+ if (args["page-size"] !== void 0) {
86
+ const num = Number(args["page-size"]);
87
+ if (isNaN(num) || num < 1) throw new CLIError("--page-size must be a positive integer");
88
+ if (num > 100) throw new CLIError("--page-size must be at most 100 (API limit)");
89
+ result.pageSize = num;
90
+ }
91
+ return result;
92
+ }
93
+ var MAX_PAGE_SIZE = 100;
94
+ var MAX_PAGES = 200;
95
+ async function fetchAllPages(fetcher) {
96
+ const allItems = [];
97
+ let page = 1;
98
+ while (page <= MAX_PAGES) {
99
+ const result = await fetcher({ page, pageSize: MAX_PAGE_SIZE });
100
+ const items = unwrapItems(result);
101
+ allItems.push(...items);
102
+ const meta = extractPaginationMeta(result);
103
+ if (meta?.totalPages !== void 0) {
104
+ if (page >= meta.totalPages) break;
105
+ } else if (!isPagePaginatedResponse(result)) {
106
+ if (items.length < MAX_PAGE_SIZE) break;
107
+ }
108
+ if (page === MAX_PAGES) {
109
+ throw new CLIError(
110
+ `Reached maximum page limit (${MAX_PAGES}). Results may be incomplete.`
111
+ );
112
+ }
113
+ page++;
114
+ }
115
+ return allItems;
116
+ }
117
+ async function fetchAllCursorPages(fetcher) {
118
+ const allItems = [];
119
+ let cursor;
120
+ let pages = 0;
121
+ while (pages < MAX_PAGES) {
122
+ const result = await fetcher({ cursor });
123
+ const items = unwrapItems(result);
124
+ allItems.push(...items);
125
+ pages++;
126
+ if (isCursorPaginatedResponse(result) && result.hasMore && result.nextCursor) {
127
+ cursor = result.nextCursor;
128
+ } else {
129
+ break;
130
+ }
131
+ if (pages === MAX_PAGES) {
132
+ throw new CLIError(
133
+ `Reached maximum page limit (${MAX_PAGES}). Results may be incomplete.`
134
+ );
135
+ }
136
+ }
137
+ return allItems;
138
+ }
139
+ function listResult(data, supportsAll = false) {
140
+ return {
141
+ items: unwrapItems(data),
142
+ meta: extractPaginationMeta(data),
143
+ supportsAll
144
+ };
145
+ }
146
+
147
+ // src/lib/output.ts
148
+ function detectOutputFormat(explicit) {
149
+ if (explicit) return explicit;
150
+ return process.stdout.isTTY ? "table" : "json";
151
+ }
152
+ function outputData(data, opts, columns, paginationMeta, supportsAll) {
153
+ let items;
154
+ let meta;
155
+ let allSupported;
156
+ if (isListResult(data)) {
157
+ items = data.items;
158
+ meta = data.meta;
159
+ allSupported = data.supportsAll ?? false;
160
+ } else {
161
+ items = unwrapItems(data);
162
+ meta = paginationMeta ?? extractPaginationMeta(data);
163
+ allSupported = supportsAll ?? false;
164
+ }
165
+ const warning = formatPaginationWarning(meta, allSupported);
166
+ if (warning) {
167
+ process.stderr.write(warning + "\n");
168
+ }
169
+ switch (opts.outputFormat) {
170
+ case "json":
171
+ process.stdout.write(JSON.stringify(items, null, 2) + "\n");
172
+ break;
173
+ case "jsonl":
174
+ for (const item of items) {
175
+ process.stdout.write(JSON.stringify(item) + "\n");
176
+ }
177
+ break;
178
+ case "table":
179
+ if (columns) {
180
+ printTable(items, columns);
181
+ } else {
182
+ process.stdout.write(JSON.stringify(items, null, 2) + "\n");
183
+ }
184
+ break;
185
+ }
186
+ }
187
+ function outputSingle(data, opts, columns) {
188
+ switch (opts.outputFormat) {
189
+ case "json":
190
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
191
+ break;
192
+ case "jsonl":
193
+ process.stdout.write(JSON.stringify(data) + "\n");
194
+ break;
195
+ case "table":
196
+ if (columns) {
197
+ printTable([data], columns);
198
+ } else {
199
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
200
+ }
201
+ break;
202
+ }
203
+ }
204
+ var COL = {
205
+ id: () => ({ header: "ID", getValue: (item) => String(item.id ?? "") }),
206
+ uuid: () => ({ header: "UUID", getValue: (item) => item.uuid ?? "" }),
207
+ name: () => ({ header: "NAME", getValue: (item) => item.name ?? "" }),
208
+ status: () => ({ header: "STATUS", getValue: (item) => item.status ?? "" }),
209
+ created: () => ({
210
+ header: "CREATED",
211
+ getValue: (item) => item.created_at ?? ""
212
+ }),
213
+ updated: () => ({
214
+ header: "UPDATED",
215
+ getValue: (item) => item.updated_at ?? ""
216
+ }),
217
+ description: () => ({
218
+ header: "DESCRIPTION",
219
+ getValue: (item) => item.description ?? ""
220
+ }),
221
+ enabled: () => ({
222
+ header: "ENABLED",
223
+ getValue: (item) => String(item.enabled ?? "")
224
+ })
225
+ };
226
+ function stderr(msg) {
227
+ process.stderr.write(msg + "\n");
228
+ }
229
+
230
+ // src/lib/context.ts
231
+ import { GravityRailClient } from "@gravity-rail/sdk";
232
+ import { homedir } from "os";
233
+
234
+ // src/lib/realm.ts
235
+ import { execSync } from "child_process";
236
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
237
+ import { dirname, join } from "path";
238
+ function findDevRealmsFile() {
239
+ let dir = process.cwd();
240
+ while (dir !== dirname(dir)) {
241
+ const candidate = join(dir, "dev-realms.json");
242
+ if (existsSync(candidate)) return candidate;
243
+ dir = dirname(dir);
244
+ }
245
+ throw new Error("Cannot find dev-realms.json in any parent directory");
246
+ }
247
+ var DEV_CLOUD_CACHE_DIR = ".dev-cloud";
248
+ var CACHE_MAX_AGE_MS = 60 * 60 * 1e3;
249
+ function findRepoRoot() {
250
+ let dir = process.cwd();
251
+ while (dir !== dirname(dir)) {
252
+ if (existsSync(join(dir, ".git"))) {
253
+ return dir;
254
+ }
255
+ dir = dirname(dir);
256
+ }
257
+ return void 0;
258
+ }
259
+ function getDevCloudCacheDir(namespace) {
260
+ const repoRoot = findRepoRoot();
261
+ const base = repoRoot ?? process.cwd();
262
+ return join(base, DEV_CLOUD_CACHE_DIR, namespace);
263
+ }
264
+ function fetchDevCloudRealms(namespace) {
265
+ const k8sNamespace = `dev-cloud-${namespace}`;
266
+ const cmd = `kubectl exec -n ${k8sNamespace} deploy/env -c devbox -- cat /workspace/dev-realms.json`;
267
+ try {
268
+ return execSync(cmd, { encoding: "utf-8", timeout: 3e4 });
269
+ } catch (err) {
270
+ const message = err instanceof Error ? err.message : String(err);
271
+ throw new Error(
272
+ `Failed to fetch dev-realms.json from dev-cloud pod "${namespace}": ${message}`
273
+ );
274
+ }
275
+ }
276
+ function loadDevCloudRealmFile(namespace) {
277
+ const cacheDir = getDevCloudCacheDir(namespace);
278
+ const cachePath = join(cacheDir, "dev-realms.json");
279
+ if (existsSync(cachePath)) {
280
+ const stat = statSync(cachePath);
281
+ const age = Date.now() - stat.mtimeMs;
282
+ if (age < CACHE_MAX_AGE_MS) {
283
+ return cachePath;
284
+ }
285
+ }
286
+ const content = fetchDevCloudRealms(namespace);
287
+ mkdirSync(cacheDir, { recursive: true });
288
+ writeFileSync(cachePath, content, "utf-8");
289
+ return cachePath;
290
+ }
291
+ function loadRealmCredentials(realm, devRealmsPath) {
292
+ const filePath = devRealmsPath ?? findDevRealmsFile();
293
+ let data;
294
+ try {
295
+ data = JSON.parse(readFileSync(filePath, "utf-8"));
296
+ } catch {
297
+ throw new Error(`Cannot read dev-realms.json at ${filePath}`);
298
+ }
299
+ const parts = realm.split("/");
300
+ if (parts.length === 1) {
301
+ const accounts = data._accounts;
302
+ const account = accounts?.[parts[0]];
303
+ if (!account?.token) {
304
+ throw new Error(`Realm account "${parts[0]}" not found in dev-realms.json`);
305
+ }
306
+ return { token: account.token };
307
+ } else {
308
+ const [wsName, memberName] = parts;
309
+ const ws = data[wsName];
310
+ if (!ws) {
311
+ throw new Error(`Realm workspace "${wsName}" not found in dev-realms.json`);
312
+ }
313
+ const member = ws.members?.[memberName];
314
+ if (!member?.token) {
315
+ throw new Error(`Realm member "${memberName}" not found in workspace "${wsName}"`);
316
+ }
317
+ return { token: member.token, workspaceUuid: ws.workspaceUuid };
318
+ }
319
+ }
320
+
321
+ // src/lib/org-auth.ts
322
+ import { execFile } from "child_process";
323
+ var ORG_AUTH_TIMEOUT_MS = 5 * 60 * 1e3;
324
+ function extractOrgTokenFromSetCookies(setCookieValues, preferredCookieName) {
325
+ let fallback;
326
+ for (const value of setCookieValues) {
327
+ const nameValue = value.split(";")[0]?.trim() ?? "";
328
+ const eqIdx = nameValue.indexOf("=");
329
+ if (eqIdx === -1) continue;
330
+ const name = nameValue.slice(0, eqIdx).trim();
331
+ const tokenValue = nameValue.slice(eqIdx + 1).trim() || void 0;
332
+ if (!tokenValue) continue;
333
+ if (name === preferredCookieName) return tokenValue;
334
+ if (!fallback && (name.startsWith("orgToken") || name.startsWith("orgSession"))) {
335
+ fallback = tokenValue;
336
+ }
337
+ }
338
+ return fallback;
339
+ }
340
+ async function tryDirectJoin(apiUrl, accessToken, orgAuthInfo) {
341
+ const response = await fetch(`${apiUrl}/api/v2/o/${orgAuthInfo.orgUuid}/join`, {
342
+ method: "POST",
343
+ headers: {
344
+ "Content-Type": "application/json",
345
+ Authorization: `Bearer ${accessToken}`
346
+ },
347
+ body: "{}"
348
+ });
349
+ if (response.status === 400) {
350
+ return null;
351
+ }
352
+ if (!response.ok) {
353
+ throw new Error(`Org join failed (HTTP ${response.status})`);
354
+ }
355
+ const rawHeaders = response.headers;
356
+ const setCookies = rawHeaders.getSetCookie?.() ?? (rawHeaders.get("set-cookie") ? [rawHeaders.get("set-cookie")] : []);
357
+ if (setCookies.length === 0) {
358
+ throw new Error("Org join succeeded but no Set-Cookie header returned");
359
+ }
360
+ const orgToken = extractOrgTokenFromSetCookies(setCookies, orgAuthInfo.cookieName);
361
+ if (!orgToken) {
362
+ throw new Error("Org join succeeded but org token cookie not found in Set-Cookie");
363
+ }
364
+ return orgToken;
365
+ }
366
+ function isAllowedAuthUrl(url, apiUrl) {
367
+ try {
368
+ const parsed = new URL(url);
369
+ const api = new URL(apiUrl);
370
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return false;
371
+ if (parsed.hostname === api.hostname) return true;
372
+ const lastTwo = (host) => {
373
+ const parts = host.split(".").filter(Boolean);
374
+ return parts.length >= 2 ? parts.slice(-2).join(".") : host;
375
+ };
376
+ const isIp = /^[0-9.]+$|^\[.*\]$/.test(parsed.hostname);
377
+ if (isIp) return false;
378
+ return lastTwo(parsed.hostname) === lastTwo(api.hostname);
379
+ } catch {
380
+ return false;
381
+ }
382
+ }
383
+ function openBrowser(url) {
384
+ let command;
385
+ let args;
386
+ if (process.platform === "darwin") {
387
+ command = "open";
388
+ args = [url];
389
+ } else if (process.platform === "win32") {
390
+ command = "cmd.exe";
391
+ args = ["/c", "start", "", url];
392
+ } else {
393
+ command = "xdg-open";
394
+ args = [url];
395
+ }
396
+ execFile(command, args, (_err) => {
397
+ });
398
+ }
399
+ function registerOrgAuthHandler(apiClient, apiUrl, accessToken) {
400
+ apiClient.setOrgAuthHandler(async (orgAuthInfo) => {
401
+ try {
402
+ const orgToken = await tryDirectJoin(apiUrl, accessToken, orgAuthInfo);
403
+ if (orgToken !== null) {
404
+ apiClient.setOrgToken(orgToken, orgAuthInfo.orgUuid);
405
+ return;
406
+ }
407
+ } catch (err) {
408
+ const msg = err instanceof Error ? err.message : String(err);
409
+ stderr(`Warning: org direct-join failed (${msg}); trying browser auth`);
410
+ }
411
+ if (!isAllowedAuthUrl(orgAuthInfo.authUrl, apiUrl)) {
412
+ throw new Error(
413
+ `Refusing to open organization auth URL: scheme/host does not match the configured API host. Re-run with the expected --api-url, or contact support.`
414
+ );
415
+ }
416
+ stderr(`
417
+ This workspace requires organization SSO authentication.`);
418
+ stderr(`Opening browser to complete authentication...`);
419
+ stderr(` ${orgAuthInfo.authUrl}
420
+ `);
421
+ stderr(`If the browser does not open, visit the URL above manually.`);
422
+ stderr(
423
+ `After signing in, re-run the command \u2014 the CLI cannot receive the SSO token directly.`
424
+ );
425
+ openBrowser(orgAuthInfo.authUrl);
426
+ await new Promise((_, reject) => {
427
+ const t = setTimeout(
428
+ () => reject(
429
+ new Error(
430
+ "Organization authentication timed out.\nPlease complete authentication in the browser, then re-run the command."
431
+ )
432
+ ),
433
+ ORG_AUTH_TIMEOUT_MS
434
+ );
435
+ t.unref?.();
436
+ });
437
+ });
438
+ }
439
+
440
+ // src/lib/pinned-fetch.ts
441
+ import { Agent, fetch as undiciFetch } from "undici";
442
+ function createPinnedFetch(apiUrl, apiIp) {
443
+ if (!apiIp) return fetch;
444
+ let parsed;
445
+ try {
446
+ parsed = new URL(apiUrl);
447
+ } catch {
448
+ return fetch;
449
+ }
450
+ const hostname = parsed.hostname;
451
+ const dispatcher = new Agent({
452
+ connect: {
453
+ servername: hostname
454
+ }
455
+ });
456
+ return async (input, init) => {
457
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
458
+ let reqUrl;
459
+ try {
460
+ reqUrl = new URL(url, apiUrl);
461
+ } catch {
462
+ return fetch(input, init);
463
+ }
464
+ const headers = new Headers(init?.headers);
465
+ if (reqUrl.hostname === hostname) {
466
+ headers.set("Host", hostname);
467
+ reqUrl = new URL(reqUrl.href);
468
+ reqUrl.hostname = apiIp;
469
+ }
470
+ return undiciFetch(reqUrl, {
471
+ ...init,
472
+ headers,
473
+ dispatcher
474
+ });
475
+ };
476
+ }
477
+ function withAuditHeaders(fetchFn, headers) {
478
+ const { chatId, sessionId, toolCallId } = headers;
479
+ return async (input, init) => {
480
+ const reqHeaders = new Headers(init?.headers);
481
+ const dynamicToolCallId = globalThis.__GRAVITY_RAIL_TOOL_CALL_ID__ || (typeof process !== "undefined" ? process.env.GRAVITY_RAIL_TOOL_CALL_ID : void 0);
482
+ if (chatId !== void 0) {
483
+ reqHeaders.set("X-GR-Chat-Id", String(chatId));
484
+ }
485
+ if (sessionId) {
486
+ reqHeaders.set("X-GR-Session-Id", sessionId);
487
+ }
488
+ if (dynamicToolCallId || toolCallId) {
489
+ reqHeaders.set("X-GR-Tool-Call-Id", dynamicToolCallId || toolCallId || "");
490
+ }
491
+ return fetchFn(input, { ...init, headers: reqHeaders });
492
+ };
493
+ }
494
+ function createSandboxFetchFromEnv(apiUrl) {
495
+ const env = typeof process !== "undefined" ? process.env : {};
496
+ const apiIp = env.GRAVITY_RAIL_API_IP;
497
+ let fetchFn = createPinnedFetch(apiUrl, apiIp);
498
+ const chatIdRaw = env.GRAVITY_RAIL_CHAT_ID;
499
+ const chatId = chatIdRaw ? Number(chatIdRaw) : void 0;
500
+ return withAuditHeaders(fetchFn, {
501
+ chatId: chatId !== void 0 && Number.isFinite(chatId) ? chatId : void 0,
502
+ sessionId: env.GRAVITY_RAIL_SESSION_ID,
503
+ toolCallId: env.GRAVITY_RAIL_TOOL_CALL_ID
504
+ });
505
+ }
506
+
507
+ // src/lib/context.ts
508
+ function makeApiClient(accessToken, apiUrl) {
509
+ const fetchFn = createSandboxFetchFromEnv(apiUrl);
510
+ const apiClient = new GravityRailClient(accessToken, apiUrl, fetchFn);
511
+ registerOrgAuthHandler(apiClient, apiUrl, accessToken);
512
+ return apiClient;
513
+ }
514
+ async function tryTokenRefresh(ctx, creds) {
515
+ if (!creds.refreshToken) return null;
516
+ const refreshed = await refreshAccessToken(
517
+ ctx.envConfig,
518
+ creds,
519
+ ctx.credentialsManager,
520
+ ctx.env
521
+ );
522
+ if (!refreshed?.accessToken) return null;
523
+ const apiClient = makeApiClient(refreshed.accessToken, ctx.envConfig.apiUrl);
524
+ return { ...ctx, accessToken: refreshed.accessToken, apiClient };
525
+ }
526
+ function createContext(opts) {
527
+ const baseConfig = getEnvironmentConfig(opts.env);
528
+ const apiUrl = opts.apiUrl || process.env.GRAVITY_RAIL_API_URL || baseConfig.apiUrl;
529
+ const envConfig = apiUrl === baseConfig.apiUrl ? baseConfig : { ...baseConfig, apiUrl };
530
+ const credentialsManager = new CredentialsManager(homedir(), envConfig.apiUrl);
531
+ return {
532
+ env: opts.env,
533
+ envConfig,
534
+ outputFormat: detectOutputFormat(opts.outputFormat),
535
+ verbose: opts.verbose ?? false,
536
+ allowWrite: opts.allowWrite ?? false,
537
+ credentialsManager
538
+ };
539
+ }
540
+ async function createAuthenticatedContext(opts) {
541
+ const ctx = createContext(opts);
542
+ if (opts.realm) {
543
+ let devRealmsPath;
544
+ if (opts.env.startsWith("dev-cloud:")) {
545
+ const namespace = opts.env.slice("dev-cloud:".length);
546
+ devRealmsPath = loadDevCloudRealmFile(namespace);
547
+ }
548
+ const { token, workspaceUuid } = loadRealmCredentials(opts.realm, devRealmsPath);
549
+ const apiClient = makeApiClient(token, ctx.envConfig.apiUrl);
550
+ return { ...ctx, accessToken: token, apiClient, realmWorkspace: workspaceUuid };
551
+ }
552
+ const apiKey = process.env.GRAVITY_RAIL_API_KEY;
553
+ if (apiKey) {
554
+ const apiClient = makeApiClient(apiKey, ctx.envConfig.apiUrl);
555
+ return { ...ctx, accessToken: apiKey, apiClient };
556
+ }
557
+ const creds = ctx.credentialsManager.load(ctx.env);
558
+ if (creds?.accessToken) {
559
+ const buffer = 5 * 60 * 1e3;
560
+ const notExpired = !creds.expiresAt || new Date(creds.expiresAt) > new Date(Date.now() + buffer);
561
+ if (notExpired) {
562
+ const apiClient = makeApiClient(creds.accessToken, ctx.envConfig.apiUrl);
563
+ return { ...ctx, accessToken: creds.accessToken, apiClient };
564
+ }
565
+ const refreshed = await tryTokenRefresh(ctx, creds);
566
+ if (refreshed) return refreshed;
567
+ }
568
+ throw new AuthRequiredError();
569
+ }
570
+ function requireAuth(ctx) {
571
+ if (!("apiClient" in ctx)) {
572
+ throw new Error("Command handler accessed apiClient without declaring requiresAuth");
573
+ }
574
+ return ctx;
575
+ }
576
+ function createPublicContext(opts) {
577
+ const env = opts.env ?? "prod";
578
+ const envConfig = opts.apiUrl ? {
579
+ apiUrl: opts.apiUrl,
580
+ frontendUrl: opts.frontendUrl || process.env.GRAVITY_RAIL_FRONTEND_URL || "https://app.gravityrail.com"
581
+ } : getEnvironmentConfig(env);
582
+ const credentialsManager = new CredentialsManager(homedir(), envConfig.apiUrl);
583
+ return {
584
+ env,
585
+ envConfig,
586
+ outputFormat: detectOutputFormat(opts.outputFormat),
587
+ verbose: opts.verbose ?? false,
588
+ allowWrite: opts.allowWrite ?? false,
589
+ credentialsManager
590
+ };
591
+ }
592
+ async function createPublicAuthenticatedContext(opts) {
593
+ const ctx = createPublicContext(opts);
594
+ const apiKey = process.env.GRAVITY_RAIL_API_KEY;
595
+ if (apiKey) {
596
+ const apiClient = makeApiClient(apiKey, ctx.envConfig.apiUrl);
597
+ return { ...ctx, accessToken: apiKey, apiClient };
598
+ }
599
+ const creds = ctx.credentialsManager.load(ctx.env);
600
+ if (creds?.accessToken) {
601
+ const buffer = 5 * 60 * 1e3;
602
+ const notExpired = !creds.expiresAt || new Date(creds.expiresAt) > new Date(Date.now() + buffer);
603
+ if (notExpired) {
604
+ const apiClient = makeApiClient(creds.accessToken, ctx.envConfig.apiUrl);
605
+ return { ...ctx, accessToken: creds.accessToken, apiClient };
606
+ }
607
+ const refreshed = await tryTokenRefresh(ctx, creds);
608
+ if (refreshed) return refreshed;
609
+ }
610
+ throw new AuthRequiredError();
611
+ }
612
+
613
+ // src/lib/concierge-api.ts
614
+ function baseUrl(ctx) {
615
+ return `${ctx.envConfig.apiUrl}/api/v2/concierge`;
616
+ }
617
+ function authHeaders(ctx) {
618
+ return {
619
+ Authorization: `Bearer ${ctx.accessToken}`,
620
+ "Content-Type": "application/json"
621
+ };
622
+ }
623
+ function toCamelChat(raw) {
624
+ return {
625
+ id: raw.id,
626
+ uuid: raw.uuid,
627
+ chatType: raw.chat_type,
628
+ title: raw.title,
629
+ threadId: raw.thread_id,
630
+ createdAt: raw.created_at,
631
+ updatedAt: raw.updated_at
632
+ };
633
+ }
634
+ function toCamelMessage(raw) {
635
+ return {
636
+ id: raw.id,
637
+ role: raw.role,
638
+ content: raw.content ?? "",
639
+ createdAt: raw.createdAt ?? raw.created_at ?? null,
640
+ toolCalls: raw.toolCalls ?? raw.tool_calls ?? null,
641
+ toolCallId: raw.toolCallId ?? raw.tool_call_id ?? null
642
+ };
643
+ }
644
+ async function listConciergeChats(ctx) {
645
+ const res = await fetch(`${baseUrl(ctx)}/chats`, { headers: authHeaders(ctx) });
646
+ if (!res.ok) throw new Error(`Failed to list concierge chats: ${res.status}`);
647
+ const data = await res.json();
648
+ return data.map(toCamelChat);
649
+ }
650
+ async function createConciergeChat(ctx, title) {
651
+ const res = await fetch(`${baseUrl(ctx)}/chats`, {
652
+ method: "POST",
653
+ headers: authHeaders(ctx),
654
+ body: JSON.stringify({ title: title ?? null })
655
+ });
656
+ if (!res.ok) throw new Error(`Failed to create concierge chat: ${res.status}`);
657
+ return toCamelChat(await res.json());
658
+ }
659
+ async function getConciergeChat(ctx, chatId) {
660
+ const res = await fetch(`${baseUrl(ctx)}/chats/${chatId}`, { headers: authHeaders(ctx) });
661
+ if (!res.ok) throw new Error(`Failed to get concierge chat ${chatId}: ${res.status}`);
662
+ return toCamelChat(await res.json());
663
+ }
664
+ async function originateConciergeCall(ctx, kind = "concierge_setup", outboundMessage, clientSurface) {
665
+ const body = { kind };
666
+ if (outboundMessage) body.outbound_message = outboundMessage;
667
+ if (clientSurface) body.client_surface = clientSurface;
668
+ const res = await fetch(`${baseUrl(ctx)}/calls`, {
669
+ method: "POST",
670
+ headers: authHeaders(ctx),
671
+ body: JSON.stringify(body)
672
+ });
673
+ if (res.status === 412) {
674
+ throw new Error("Your account phone is not verified. Run `gr account verify-phone` first.");
675
+ }
676
+ if (!res.ok) {
677
+ const detail = await res.text();
678
+ throw new Error(`Failed to start concierge call: ${res.status} ${detail}`);
679
+ }
680
+ return await res.json();
681
+ }
682
+ async function originateConciergeSms(ctx, kind = "concierge_setup") {
683
+ const res = await fetch(`${baseUrl(ctx)}/sms`, {
684
+ method: "POST",
685
+ headers: authHeaders(ctx),
686
+ body: JSON.stringify({ kind })
687
+ });
688
+ if (res.status === 412) {
689
+ throw new Error("Your account phone is not verified. Run `gr account verify-phone` first.");
690
+ }
691
+ if (!res.ok) {
692
+ const detail = await res.text();
693
+ throw new Error(`Failed to start concierge SMS: ${res.status} ${detail}`);
694
+ }
695
+ return await res.json();
696
+ }
697
+ async function startAccountPhoneVerify(ctx, phone) {
698
+ const res = await fetch(`${ctx.envConfig.apiUrl}/api/v2/account/phone/verify/start`, {
699
+ method: "POST",
700
+ headers: authHeaders(ctx),
701
+ body: JSON.stringify({ phone })
702
+ });
703
+ if (!res.ok) {
704
+ const detail = await res.text();
705
+ throw new Error(`Failed to send verification code: ${res.status} ${detail}`);
706
+ }
707
+ }
708
+ async function confirmAccountPhoneVerify(ctx, code) {
709
+ const res = await fetch(`${ctx.envConfig.apiUrl}/api/v2/account/phone/verify/confirm`, {
710
+ method: "POST",
711
+ headers: authHeaders(ctx),
712
+ body: JSON.stringify({ code })
713
+ });
714
+ if (!res.ok) {
715
+ const detail = await res.text();
716
+ throw new Error(`Failed to confirm verification code: ${res.status} ${detail}`);
717
+ }
718
+ }
719
+ async function getConciergeChatMessages(ctx, chatId) {
720
+ const res = await fetch(`${baseUrl(ctx)}/chats/${chatId}/messages`, {
721
+ headers: authHeaders(ctx)
722
+ });
723
+ if (!res.ok)
724
+ throw new Error(`Failed to get concierge messages for chat ${chatId}: ${res.status}`);
725
+ const data = await res.json();
726
+ return data.map(toCamelMessage);
727
+ }
728
+
729
+ export {
730
+ isListResult,
731
+ isPagePaginatedResponse,
732
+ isCursorPaginatedResponse,
733
+ isPaginatedResponse,
734
+ extractPaginationMeta,
735
+ formatPaginationWarning,
736
+ unwrapItems,
737
+ LIST_PAGINATION_ARGS,
738
+ parsePaginationArgs,
739
+ fetchAllPages,
740
+ fetchAllCursorPages,
741
+ listResult,
742
+ detectOutputFormat,
743
+ outputData,
744
+ outputSingle,
745
+ COL,
746
+ stderr,
747
+ createContext,
748
+ createAuthenticatedContext,
749
+ requireAuth,
750
+ createPublicContext,
751
+ createPublicAuthenticatedContext,
752
+ listConciergeChats,
753
+ createConciergeChat,
754
+ getConciergeChat,
755
+ originateConciergeCall,
756
+ originateConciergeSms,
757
+ startAccountPhoneVerify,
758
+ confirmAccountPhoneVerify,
759
+ getConciergeChatMessages
760
+ };
761
+ //# sourceMappingURL=chunk-L5YCPUAM.js.map