@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.
package/dist/lib.js CHANGED
@@ -1,157 +1,48 @@
1
+ import {
2
+ COL,
3
+ LIST_PAGINATION_ARGS,
4
+ confirmAccountPhoneVerify,
5
+ createAuthenticatedContext,
6
+ createConciergeChat,
7
+ createContext,
8
+ createPublicAuthenticatedContext,
9
+ createPublicContext,
10
+ detectOutputFormat,
11
+ extractPaginationMeta,
12
+ fetchAllCursorPages,
13
+ fetchAllPages,
14
+ formatPaginationWarning,
15
+ getConciergeChat,
16
+ getConciergeChatMessages,
17
+ isCursorPaginatedResponse,
18
+ isListResult,
19
+ isPagePaginatedResponse,
20
+ isPaginatedResponse,
21
+ listConciergeChats,
22
+ listResult,
23
+ originateConciergeCall,
24
+ originateConciergeSms,
25
+ outputData,
26
+ outputSingle,
27
+ parsePaginationArgs,
28
+ requireAuth,
29
+ startAccountPhoneVerify,
30
+ stderr,
31
+ unwrapItems
32
+ } from "./chunk-L5YCPUAM.js";
1
33
  import {
2
34
  AGUIClient,
3
- CredentialsManager,
4
- clearScreen,
5
35
  formatTimestamp,
6
- getEnvironmentConfig,
7
36
  getErrorMessage,
8
37
  getTextDelta,
9
38
  performOAuthFlow,
10
- printMessage,
11
- printTable,
12
- refreshAccessToken,
13
39
  selectFromList
14
40
  } from "./chunk-H62EC2CF.js";
15
41
  import {
16
42
  AuthRequiredError,
17
43
  CLIError,
18
44
  handleError
19
- } from "./chunk-OPWCTOHP.js";
20
-
21
- // src/lib/pagination.ts
22
- function isListResult(data) {
23
- if (!data || typeof data !== "object" || Array.isArray(data)) return false;
24
- const obj = data;
25
- return Array.isArray(obj.items) && ("supportsAll" in obj || "meta" in obj && (obj.meta === void 0 || typeof obj.meta === "object"));
26
- }
27
- function isPagePaginatedResponse(data) {
28
- if (!data || typeof data !== "object") return false;
29
- const obj = data;
30
- if (!Array.isArray(obj.items) || typeof obj.page !== "number") return false;
31
- if (typeof obj.totalPages === "number") return true;
32
- if (typeof obj.total === "number" && typeof obj.pageSize === "number") return true;
33
- return false;
34
- }
35
- function isCursorPaginatedResponse(data) {
36
- if (!data || typeof data !== "object") return false;
37
- const obj = data;
38
- return Array.isArray(obj.items) && typeof obj.hasMore === "boolean";
39
- }
40
- function isPaginatedResponse(data) {
41
- return isPagePaginatedResponse(data) || isCursorPaginatedResponse(data);
42
- }
43
- function extractPaginationMeta(data) {
44
- if (isPagePaginatedResponse(data)) {
45
- const totalPages = data.totalPages ?? (data.pageSize ? Math.ceil(data.total / data.pageSize) : void 0);
46
- return {
47
- total: data.total,
48
- page: data.page,
49
- totalPages
50
- };
51
- }
52
- if (isCursorPaginatedResponse(data)) {
53
- return {
54
- hasMore: data.hasMore,
55
- nextCursor: data.nextCursor
56
- };
57
- }
58
- return void 0;
59
- }
60
- function formatPaginationWarning(meta, supportsAll = true) {
61
- if (!meta) return void 0;
62
- const allHint = supportsAll ? " Use --all to fetch all pages." : "";
63
- if (meta.totalPages !== void 0 && meta.totalPages > 1) {
64
- const pageNum = meta.page ?? 1;
65
- return `Showing page ${pageNum} of ${meta.totalPages} (${meta.total} total items).${allHint}`;
66
- }
67
- if (meta.hasMore === true) {
68
- return `More results available.${allHint}`;
69
- }
70
- return void 0;
71
- }
72
- function unwrapItems(data) {
73
- if (Array.isArray(data)) return data;
74
- if (data && typeof data === "object" && "items" in data && Array.isArray(data.items)) {
75
- return data.items;
76
- }
77
- return [data];
78
- }
79
- var LIST_PAGINATION_ARGS = [
80
- { name: "page", description: "Page number" },
81
- { name: "page-size", description: "Page size" },
82
- { name: "all", description: "Fetch all pages and return combined results", isFlag: true }
83
- ];
84
- function parsePaginationArgs(args) {
85
- const result2 = {};
86
- if (args.all === true) {
87
- result2.all = true;
88
- }
89
- if (args.page !== void 0) {
90
- const num = Number(args.page);
91
- if (isNaN(num) || num < 1) throw new CLIError("--page must be a positive integer");
92
- result2.page = num;
93
- }
94
- if (args["page-size"] !== void 0) {
95
- const num = Number(args["page-size"]);
96
- if (isNaN(num) || num < 1) throw new CLIError("--page-size must be a positive integer");
97
- if (num > 100) throw new CLIError("--page-size must be at most 100 (API limit)");
98
- result2.pageSize = num;
99
- }
100
- return result2;
101
- }
102
- var MAX_PAGE_SIZE = 100;
103
- var MAX_PAGES = 200;
104
- async function fetchAllPages(fetcher) {
105
- const allItems = [];
106
- let page = 1;
107
- while (page <= MAX_PAGES) {
108
- const result2 = await fetcher({ page, pageSize: MAX_PAGE_SIZE });
109
- const items = unwrapItems(result2);
110
- allItems.push(...items);
111
- const meta = extractPaginationMeta(result2);
112
- if (meta?.totalPages !== void 0) {
113
- if (page >= meta.totalPages) break;
114
- } else if (!isPagePaginatedResponse(result2)) {
115
- if (items.length < MAX_PAGE_SIZE) break;
116
- }
117
- if (page === MAX_PAGES) {
118
- throw new CLIError(
119
- `Reached maximum page limit (${MAX_PAGES}). Results may be incomplete.`
120
- );
121
- }
122
- page++;
123
- }
124
- return allItems;
125
- }
126
- async function fetchAllCursorPages(fetcher) {
127
- const allItems = [];
128
- let cursor;
129
- let pages = 0;
130
- while (pages < MAX_PAGES) {
131
- const result2 = await fetcher({ cursor });
132
- const items = unwrapItems(result2);
133
- allItems.push(...items);
134
- pages++;
135
- if (isCursorPaginatedResponse(result2) && result2.hasMore && result2.nextCursor) {
136
- cursor = result2.nextCursor;
137
- } else {
138
- break;
139
- }
140
- if (pages === MAX_PAGES) {
141
- throw new CLIError(
142
- `Reached maximum page limit (${MAX_PAGES}). Results may be incomplete.`
143
- );
144
- }
145
- }
146
- return allItems;
147
- }
148
- function listResult(data, supportsAll = false) {
149
- return {
150
- items: unwrapItems(data),
151
- meta: extractPaginationMeta(data),
152
- supportsAll
153
- };
154
- }
45
+ } from "./chunk-7VZ4JLAJ.js";
155
46
 
156
47
  // src/lib/args.ts
157
48
  var PAGINATION_ARGS = [
@@ -229,475 +120,8 @@ function findDef(defs, arg) {
229
120
  return void 0;
230
121
  }
231
122
 
232
- // src/lib/context.ts
233
- import { GravityRailClient } from "@gravity-rail/sdk";
234
- import { homedir } from "os";
235
-
236
- // src/lib/output.ts
237
- function detectOutputFormat(explicit) {
238
- if (explicit) return explicit;
239
- return process.stdout.isTTY ? "table" : "json";
240
- }
241
- function outputData(data, opts, columns, paginationMeta, supportsAll) {
242
- let items;
243
- let meta;
244
- let allSupported;
245
- if (isListResult(data)) {
246
- items = data.items;
247
- meta = data.meta;
248
- allSupported = data.supportsAll ?? false;
249
- } else {
250
- items = unwrapItems(data);
251
- meta = paginationMeta ?? extractPaginationMeta(data);
252
- allSupported = supportsAll ?? false;
253
- }
254
- const warning = formatPaginationWarning(meta, allSupported);
255
- if (warning) {
256
- process.stderr.write(warning + "\n");
257
- }
258
- switch (opts.outputFormat) {
259
- case "json":
260
- process.stdout.write(JSON.stringify(items, null, 2) + "\n");
261
- break;
262
- case "jsonl":
263
- for (const item of items) {
264
- process.stdout.write(JSON.stringify(item) + "\n");
265
- }
266
- break;
267
- case "table":
268
- if (columns) {
269
- printTable(items, columns);
270
- } else {
271
- process.stdout.write(JSON.stringify(items, null, 2) + "\n");
272
- }
273
- break;
274
- }
275
- }
276
- function outputSingle(data, opts, columns) {
277
- switch (opts.outputFormat) {
278
- case "json":
279
- process.stdout.write(JSON.stringify(data, null, 2) + "\n");
280
- break;
281
- case "jsonl":
282
- process.stdout.write(JSON.stringify(data) + "\n");
283
- break;
284
- case "table":
285
- if (columns) {
286
- printTable([data], columns);
287
- } else {
288
- process.stdout.write(JSON.stringify(data, null, 2) + "\n");
289
- }
290
- break;
291
- }
292
- }
293
- var COL = {
294
- id: () => ({ header: "ID", getValue: (item) => String(item.id ?? "") }),
295
- uuid: () => ({ header: "UUID", getValue: (item) => item.uuid ?? "" }),
296
- name: () => ({ header: "NAME", getValue: (item) => item.name ?? "" }),
297
- status: () => ({ header: "STATUS", getValue: (item) => item.status ?? "" }),
298
- created: () => ({
299
- header: "CREATED",
300
- getValue: (item) => item.created_at ?? ""
301
- }),
302
- updated: () => ({
303
- header: "UPDATED",
304
- getValue: (item) => item.updated_at ?? ""
305
- }),
306
- description: () => ({
307
- header: "DESCRIPTION",
308
- getValue: (item) => item.description ?? ""
309
- }),
310
- enabled: () => ({
311
- header: "ENABLED",
312
- getValue: (item) => String(item.enabled ?? "")
313
- })
314
- };
315
- function stderr(msg) {
316
- process.stderr.write(msg + "\n");
317
- }
318
-
319
- // src/lib/realm.ts
320
- import { execSync } from "child_process";
321
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
322
- import { dirname, join } from "path";
323
- function findDevRealmsFile() {
324
- let dir = process.cwd();
325
- while (dir !== dirname(dir)) {
326
- const candidate = join(dir, "dev-realms.json");
327
- if (existsSync(candidate)) return candidate;
328
- dir = dirname(dir);
329
- }
330
- throw new Error("Cannot find dev-realms.json in any parent directory");
331
- }
332
- var DEV_CLOUD_CACHE_DIR = ".dev-cloud";
333
- var CACHE_MAX_AGE_MS = 60 * 60 * 1e3;
334
- function findRepoRoot() {
335
- let dir = process.cwd();
336
- while (dir !== dirname(dir)) {
337
- if (existsSync(join(dir, ".git"))) {
338
- return dir;
339
- }
340
- dir = dirname(dir);
341
- }
342
- return void 0;
343
- }
344
- function getDevCloudCacheDir(namespace) {
345
- const repoRoot = findRepoRoot();
346
- const base = repoRoot ?? process.cwd();
347
- return join(base, DEV_CLOUD_CACHE_DIR, namespace);
348
- }
349
- function fetchDevCloudRealms(namespace) {
350
- const k8sNamespace = `dev-cloud-${namespace}`;
351
- const cmd = `kubectl exec -n ${k8sNamespace} deploy/env -c devbox -- cat /workspace/dev-realms.json`;
352
- try {
353
- return execSync(cmd, { encoding: "utf-8", timeout: 3e4 });
354
- } catch (err) {
355
- const message = err instanceof Error ? err.message : String(err);
356
- throw new Error(
357
- `Failed to fetch dev-realms.json from dev-cloud pod "${namespace}": ${message}`
358
- );
359
- }
360
- }
361
- function loadDevCloudRealmFile(namespace) {
362
- const cacheDir = getDevCloudCacheDir(namespace);
363
- const cachePath = join(cacheDir, "dev-realms.json");
364
- if (existsSync(cachePath)) {
365
- const stat = statSync(cachePath);
366
- const age = Date.now() - stat.mtimeMs;
367
- if (age < CACHE_MAX_AGE_MS) {
368
- return cachePath;
369
- }
370
- }
371
- const content = fetchDevCloudRealms(namespace);
372
- mkdirSync(cacheDir, { recursive: true });
373
- writeFileSync(cachePath, content, "utf-8");
374
- return cachePath;
375
- }
376
- function loadRealmCredentials(realm, devRealmsPath) {
377
- const filePath = devRealmsPath ?? findDevRealmsFile();
378
- let data;
379
- try {
380
- data = JSON.parse(readFileSync(filePath, "utf-8"));
381
- } catch {
382
- throw new Error(`Cannot read dev-realms.json at ${filePath}`);
383
- }
384
- const parts = realm.split("/");
385
- if (parts.length === 1) {
386
- const accounts = data._accounts;
387
- const account = accounts?.[parts[0]];
388
- if (!account?.token) {
389
- throw new Error(`Realm account "${parts[0]}" not found in dev-realms.json`);
390
- }
391
- return { token: account.token };
392
- } else {
393
- const [wsName, memberName] = parts;
394
- const ws = data[wsName];
395
- if (!ws) {
396
- throw new Error(`Realm workspace "${wsName}" not found in dev-realms.json`);
397
- }
398
- const member = ws.members?.[memberName];
399
- if (!member?.token) {
400
- throw new Error(`Realm member "${memberName}" not found in workspace "${wsName}"`);
401
- }
402
- return { token: member.token, workspaceUuid: ws.workspaceUuid };
403
- }
404
- }
405
-
406
- // src/lib/org-auth.ts
407
- import { execFile } from "child_process";
408
- var ORG_AUTH_TIMEOUT_MS = 5 * 60 * 1e3;
409
- function extractOrgTokenFromSetCookies(setCookieValues, preferredCookieName) {
410
- let fallback;
411
- for (const value of setCookieValues) {
412
- const nameValue = value.split(";")[0]?.trim() ?? "";
413
- const eqIdx = nameValue.indexOf("=");
414
- if (eqIdx === -1) continue;
415
- const name = nameValue.slice(0, eqIdx).trim();
416
- const tokenValue = nameValue.slice(eqIdx + 1).trim() || void 0;
417
- if (!tokenValue) continue;
418
- if (name === preferredCookieName) return tokenValue;
419
- if (!fallback && (name.startsWith("orgToken") || name.startsWith("orgSession"))) {
420
- fallback = tokenValue;
421
- }
422
- }
423
- return fallback;
424
- }
425
- async function tryDirectJoin(apiUrl, accessToken, orgAuthInfo) {
426
- const response = await fetch(`${apiUrl}/api/v2/o/${orgAuthInfo.orgUuid}/join`, {
427
- method: "POST",
428
- headers: {
429
- "Content-Type": "application/json",
430
- Authorization: `Bearer ${accessToken}`
431
- },
432
- body: "{}"
433
- });
434
- if (response.status === 400) {
435
- return null;
436
- }
437
- if (!response.ok) {
438
- throw new Error(`Org join failed (HTTP ${response.status})`);
439
- }
440
- const rawHeaders = response.headers;
441
- const setCookies = rawHeaders.getSetCookie?.() ?? (rawHeaders.get("set-cookie") ? [rawHeaders.get("set-cookie")] : []);
442
- if (setCookies.length === 0) {
443
- throw new Error("Org join succeeded but no Set-Cookie header returned");
444
- }
445
- const orgToken = extractOrgTokenFromSetCookies(setCookies, orgAuthInfo.cookieName);
446
- if (!orgToken) {
447
- throw new Error("Org join succeeded but org token cookie not found in Set-Cookie");
448
- }
449
- return orgToken;
450
- }
451
- function isAllowedAuthUrl(url, apiUrl) {
452
- try {
453
- const parsed = new URL(url);
454
- const api = new URL(apiUrl);
455
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return false;
456
- if (parsed.hostname === api.hostname) return true;
457
- const lastTwo = (host) => {
458
- const parts = host.split(".").filter(Boolean);
459
- return parts.length >= 2 ? parts.slice(-2).join(".") : host;
460
- };
461
- const isIp = /^[0-9.]+$|^\[.*\]$/.test(parsed.hostname);
462
- if (isIp) return false;
463
- return lastTwo(parsed.hostname) === lastTwo(api.hostname);
464
- } catch {
465
- return false;
466
- }
467
- }
468
- function openBrowser(url) {
469
- let command;
470
- let args;
471
- if (process.platform === "darwin") {
472
- command = "open";
473
- args = [url];
474
- } else if (process.platform === "win32") {
475
- command = "cmd.exe";
476
- args = ["/c", "start", "", url];
477
- } else {
478
- command = "xdg-open";
479
- args = [url];
480
- }
481
- execFile(command, args, (_err) => {
482
- });
483
- }
484
- function registerOrgAuthHandler(apiClient, apiUrl, accessToken) {
485
- apiClient.setOrgAuthHandler(async (orgAuthInfo) => {
486
- try {
487
- const orgToken = await tryDirectJoin(apiUrl, accessToken, orgAuthInfo);
488
- if (orgToken !== null) {
489
- apiClient.setOrgToken(orgToken, orgAuthInfo.orgUuid);
490
- return;
491
- }
492
- } catch (err) {
493
- const msg = err instanceof Error ? err.message : String(err);
494
- stderr(`Warning: org direct-join failed (${msg}); trying browser auth`);
495
- }
496
- if (!isAllowedAuthUrl(orgAuthInfo.authUrl, apiUrl)) {
497
- throw new Error(
498
- `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.`
499
- );
500
- }
501
- stderr(`
502
- This workspace requires organization SSO authentication.`);
503
- stderr(`Opening browser to complete authentication...`);
504
- stderr(` ${orgAuthInfo.authUrl}
505
- `);
506
- stderr(`If the browser does not open, visit the URL above manually.`);
507
- stderr(
508
- `After signing in, re-run the command \u2014 the CLI cannot receive the SSO token directly.`
509
- );
510
- openBrowser(orgAuthInfo.authUrl);
511
- await new Promise((_, reject) => {
512
- const t = setTimeout(
513
- () => reject(
514
- new Error(
515
- "Organization authentication timed out.\nPlease complete authentication in the browser, then re-run the command."
516
- )
517
- ),
518
- ORG_AUTH_TIMEOUT_MS
519
- );
520
- t.unref?.();
521
- });
522
- });
523
- }
524
-
525
- // src/lib/pinned-fetch.ts
526
- import { Agent, fetch as undiciFetch } from "undici";
527
- function createPinnedFetch(apiUrl, apiIp) {
528
- if (!apiIp) return fetch;
529
- let parsed;
530
- try {
531
- parsed = new URL(apiUrl);
532
- } catch {
533
- return fetch;
534
- }
535
- const hostname = parsed.hostname;
536
- const dispatcher = new Agent({
537
- connect: {
538
- servername: hostname
539
- }
540
- });
541
- return async (input, init) => {
542
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
543
- let reqUrl;
544
- try {
545
- reqUrl = new URL(url, apiUrl);
546
- } catch {
547
- return fetch(input, init);
548
- }
549
- const headers2 = new Headers(init?.headers);
550
- if (reqUrl.hostname === hostname) {
551
- headers2.set("Host", hostname);
552
- reqUrl = new URL(reqUrl.href);
553
- reqUrl.hostname = apiIp;
554
- }
555
- return undiciFetch(reqUrl, {
556
- ...init,
557
- headers: headers2,
558
- dispatcher
559
- });
560
- };
561
- }
562
- function withAuditHeaders(fetchFn, headers2) {
563
- const { chatId, sessionId, toolCallId } = headers2;
564
- if (chatId === void 0 && !sessionId && !toolCallId) {
565
- return fetchFn;
566
- }
567
- return async (input, init) => {
568
- const reqHeaders = new Headers(init?.headers);
569
- if (chatId !== void 0) {
570
- reqHeaders.set("X-GR-Chat-Id", String(chatId));
571
- }
572
- if (sessionId) {
573
- reqHeaders.set("X-GR-Session-Id", sessionId);
574
- }
575
- if (toolCallId) {
576
- reqHeaders.set("X-GR-Tool-Call-Id", toolCallId);
577
- }
578
- return fetchFn(input, { ...init, headers: reqHeaders });
579
- };
580
- }
581
- function createSandboxFetchFromEnv(apiUrl) {
582
- const apiIp = process.env.GRAVITY_RAIL_API_IP;
583
- let fetchFn = createPinnedFetch(apiUrl, apiIp);
584
- const chatIdRaw = process.env.GRAVITY_RAIL_CHAT_ID;
585
- const chatId = chatIdRaw ? Number(chatIdRaw) : void 0;
586
- return withAuditHeaders(fetchFn, {
587
- chatId: chatId !== void 0 && Number.isFinite(chatId) ? chatId : void 0,
588
- sessionId: process.env.GRAVITY_RAIL_SESSION_ID,
589
- toolCallId: process.env.GRAVITY_RAIL_TOOL_CALL_ID
590
- });
591
- }
592
-
593
- // src/lib/context.ts
594
- function makeApiClient(accessToken, apiUrl) {
595
- const fetchFn = createSandboxFetchFromEnv(apiUrl);
596
- const apiClient = new GravityRailClient(accessToken, apiUrl, fetchFn);
597
- registerOrgAuthHandler(apiClient, apiUrl, accessToken);
598
- return apiClient;
599
- }
600
- async function tryTokenRefresh(ctx, creds) {
601
- if (!creds.refreshToken) return null;
602
- const refreshed = await refreshAccessToken(
603
- ctx.envConfig,
604
- creds,
605
- ctx.credentialsManager,
606
- ctx.env
607
- );
608
- if (!refreshed?.accessToken) return null;
609
- const apiClient = makeApiClient(refreshed.accessToken, ctx.envConfig.apiUrl);
610
- return { ...ctx, accessToken: refreshed.accessToken, apiClient };
611
- }
612
- function createContext(opts) {
613
- const baseConfig = getEnvironmentConfig(opts.env);
614
- const apiUrl = opts.apiUrl || process.env.GRAVITY_RAIL_API_URL || baseConfig.apiUrl;
615
- const envConfig = apiUrl === baseConfig.apiUrl ? baseConfig : { ...baseConfig, apiUrl };
616
- const credentialsManager = new CredentialsManager(homedir(), envConfig.apiUrl);
617
- return {
618
- env: opts.env,
619
- envConfig,
620
- outputFormat: detectOutputFormat(opts.outputFormat),
621
- verbose: opts.verbose ?? false,
622
- allowWrite: opts.allowWrite ?? false,
623
- credentialsManager
624
- };
625
- }
626
- async function createAuthenticatedContext(opts) {
627
- const ctx = createContext(opts);
628
- if (opts.realm) {
629
- let devRealmsPath;
630
- if (opts.env.startsWith("dev-cloud:")) {
631
- const namespace = opts.env.slice("dev-cloud:".length);
632
- devRealmsPath = loadDevCloudRealmFile(namespace);
633
- }
634
- const { token, workspaceUuid } = loadRealmCredentials(opts.realm, devRealmsPath);
635
- const apiClient = makeApiClient(token, ctx.envConfig.apiUrl);
636
- return { ...ctx, accessToken: token, apiClient, realmWorkspace: workspaceUuid };
637
- }
638
- const apiKey = process.env.GRAVITY_RAIL_API_KEY;
639
- if (apiKey) {
640
- const apiClient = makeApiClient(apiKey, ctx.envConfig.apiUrl);
641
- return { ...ctx, accessToken: apiKey, apiClient };
642
- }
643
- const creds = ctx.credentialsManager.load(ctx.env);
644
- if (creds?.accessToken) {
645
- const buffer = 5 * 60 * 1e3;
646
- const notExpired = !creds.expiresAt || new Date(creds.expiresAt) > new Date(Date.now() + buffer);
647
- if (notExpired) {
648
- const apiClient = makeApiClient(creds.accessToken, ctx.envConfig.apiUrl);
649
- return { ...ctx, accessToken: creds.accessToken, apiClient };
650
- }
651
- const refreshed = await tryTokenRefresh(ctx, creds);
652
- if (refreshed) return refreshed;
653
- }
654
- throw new AuthRequiredError();
655
- }
656
- function requireAuth(ctx) {
657
- if (!("apiClient" in ctx)) {
658
- throw new Error("Command handler accessed apiClient without declaring requiresAuth");
659
- }
660
- return ctx;
661
- }
662
- function createPublicContext(opts) {
663
- const env = opts.env ?? "prod";
664
- const envConfig = opts.apiUrl ? {
665
- apiUrl: opts.apiUrl,
666
- frontendUrl: opts.frontendUrl || process.env.GRAVITY_RAIL_FRONTEND_URL || "https://app.gravityrail.com"
667
- } : getEnvironmentConfig(env);
668
- const credentialsManager = new CredentialsManager(homedir(), envConfig.apiUrl);
669
- return {
670
- env,
671
- envConfig,
672
- outputFormat: detectOutputFormat(opts.outputFormat),
673
- verbose: opts.verbose ?? false,
674
- allowWrite: opts.allowWrite ?? false,
675
- credentialsManager
676
- };
677
- }
678
- async function createPublicAuthenticatedContext(opts) {
679
- const ctx = createPublicContext(opts);
680
- const apiKey = process.env.GRAVITY_RAIL_API_KEY;
681
- if (apiKey) {
682
- const apiClient = makeApiClient(apiKey, ctx.envConfig.apiUrl);
683
- return { ...ctx, accessToken: apiKey, apiClient };
684
- }
685
- const creds = ctx.credentialsManager.load(ctx.env);
686
- if (creds?.accessToken) {
687
- const buffer = 5 * 60 * 1e3;
688
- const notExpired = !creds.expiresAt || new Date(creds.expiresAt) > new Date(Date.now() + buffer);
689
- if (notExpired) {
690
- const apiClient = makeApiClient(creds.accessToken, ctx.envConfig.apiUrl);
691
- return { ...ctx, accessToken: creds.accessToken, apiClient };
692
- }
693
- const refreshed = await tryTokenRefresh(ctx, creds);
694
- if (refreshed) return refreshed;
695
- }
696
- throw new AuthRequiredError();
697
- }
698
-
699
123
  // src/lib/input.ts
700
- import { readFileSync as readFileSync2 } from "fs";
124
+ import { readFileSync } from "fs";
701
125
  var JSON_BODY_ACTIONS = /* @__PURE__ */ new Set([
702
126
  "create",
703
127
  "update",
@@ -744,7 +168,7 @@ async function readJsonInput(args) {
744
168
  }
745
169
  if (filePath) {
746
170
  try {
747
- const content = readFileSync2(filePath, "utf-8");
171
+ const content = readFileSync(filePath, "utf-8");
748
172
  return JSON.parse(content);
749
173
  } catch (err) {
750
174
  if (err instanceof SyntaxError) {
@@ -1344,6 +768,11 @@ var CommandRegistry = class {
1344
768
  const key = cmd.path.join(" ");
1345
769
  this.commands.set(key, cmd);
1346
770
  }
771
+ registerCommands(commands3) {
772
+ for (const cmd of commands3) {
773
+ this.registerCommand(cmd);
774
+ }
775
+ }
1347
776
  /** Resolve command words to a CommandDef using longest-match.
1348
777
  * Supports {param} path segments that match any word and extract as pathParams. */
1349
778
  resolve(words) {
@@ -1376,6 +805,7 @@ var CommandRegistry = class {
1376
805
  }
1377
806
  /** Generate help text. If prefix given, show commands under that prefix. */
1378
807
  getHelp(prefix) {
808
+ const inSandbox = process.env.GRAVITY_RAIL_SANDBOX === "1";
1379
809
  if (!prefix || prefix.length === 0) {
1380
810
  return this.getTopLevelHelp();
1381
811
  }
@@ -1412,18 +842,21 @@ var CommandRegistry = class {
1412
842
  `, "Commands:"];
1413
843
  for (const cmd of matching.sort((a, b) => a.path.join(" ").localeCompare(b.path.join(" ")))) {
1414
844
  const subPath = cmd.path.slice(prefix.length).join(" ") || cmd.path[cmd.path.length - 1];
1415
- lines.push(` ${subPath.padEnd(28)} ${cmd.description}`);
845
+ const description = inSandbox ? sandboxHelpText(cmd.description) : cmd.description;
846
+ lines.push(` ${subPath.padEnd(28)} ${description}`);
1416
847
  }
1417
848
  return lines.join("\n") + "\n";
1418
849
  }
1419
850
  getCommandHelp(cmd) {
1420
851
  const cmdStr = cmd.path.join(" ");
1421
- const lines = [`${cmd.description}
852
+ const inSandbox = process.env.GRAVITY_RAIL_SANDBOX === "1";
853
+ const description = inSandbox ? sandboxHelpText(cmd.description) : cmd.description;
854
+ const lines = [`${description}
1422
855
  `, `Usage: gr ${cmdStr} [options]`];
1423
- if (cmd.requiresWorkspace) {
856
+ if (cmd.requiresWorkspace && !inSandbox) {
1424
857
  lines.push(" Requires: -w <workspace-uuid> (or --realm <workspace>/<member> in dev)");
1425
858
  }
1426
- if (cmd.mutates) {
859
+ if (cmd.mutates && !inSandbox) {
1427
860
  lines.push(" Requires: --allow-write");
1428
861
  }
1429
862
  const action = cmd.path[cmd.path.length - 1] ?? "";
@@ -1445,7 +878,7 @@ var CommandRegistry = class {
1445
878
  if (cmd.examples && cmd.examples.length > 0) {
1446
879
  lines.push("", "Examples:");
1447
880
  for (const ex of cmd.examples) {
1448
- lines.push(` ${ex}`);
881
+ lines.push(` ${inSandbox ? sandboxExample(ex) : ex}`);
1449
882
  }
1450
883
  }
1451
884
  return lines.join("\n") + "\n";
@@ -1460,38 +893,50 @@ var CommandRegistry = class {
1460
893
  }
1461
894
  getTopLevelHelp() {
1462
895
  const lines = ["Usage: gr <command> [options]\n", "Commands:"];
896
+ const inSandbox = process.env.GRAVITY_RAIL_SANDBOX === "1";
1463
897
  const seen = /* @__PURE__ */ new Set();
1464
898
  for (const domain of this.domains) {
1465
899
  if (!seen.has(domain.name)) {
1466
900
  seen.add(domain.name);
1467
- lines.push(` ${domain.name.padEnd(28)} ${domain.description}`);
901
+ const description = inSandbox ? sandboxHelpText(domain.description) : domain.description;
902
+ lines.push(` ${domain.name.padEnd(28)} ${description}`);
1468
903
  }
1469
904
  }
1470
905
  for (const [, cmd] of this.commands) {
1471
906
  const topWord = cmd.path[0];
1472
907
  if (!seen.has(topWord) && cmd.path.length === 1) {
1473
908
  seen.add(topWord);
1474
- lines.push(` ${topWord.padEnd(28)} ${cmd.description}`);
909
+ const description = inSandbox ? sandboxHelpText(cmd.description) : cmd.description;
910
+ lines.push(` ${topWord.padEnd(28)} ${description}`);
1475
911
  }
1476
912
  }
1477
- lines.push(
913
+ const globalOptions = [
1478
914
  "",
1479
915
  "Global Options:",
1480
- " --env <env> Environment: dev, staging, prod (default: dev)",
1481
- " --realm <ws>/<member> Dev auth shortcut (dev only; sets -w automatically)",
1482
- " --api-url <url> API base URL (or GRAVITY_RAIL_API_URL)",
1483
- " -w, --workspace <uuid> Workspace UUID",
916
+ ...!inSandbox ? [
917
+ " --env <env> Environment: dev, staging, prod (default: dev)",
918
+ " --realm <ws>/<member> Dev auth shortcut (dev only; sets -w automatically)",
919
+ " --api-url <url> API base URL (or GRAVITY_RAIL_API_URL)",
920
+ " -w, --workspace <uuid> Workspace UUID"
921
+ ] : [],
1484
922
  " -d, --data <json> JSON body (create/update and other write commands)",
1485
923
  " -f, --file <path> JSON body from file",
1486
- " --allow-write Required for create/update/delete/archive",
924
+ ...!inSandbox ? [" --allow-write Required for create/update/delete/archive"] : [],
1487
925
  " -o, --output-format <fmt> Output format: json, jsonl, table (default: auto)",
1488
926
  " -v, --verbose Verbose output to stderr",
1489
927
  " --help Show help",
1490
928
  " --version Show version"
1491
- );
929
+ ];
930
+ lines.push(...globalOptions);
1492
931
  return lines.join("\n") + "\n";
1493
932
  }
1494
933
  };
934
+ function sandboxHelpText(text) {
935
+ return text.replace(/\byarn gr\b/g, "gr").replace(/\s--allow-write\b/g, "").replace(/\s--realm\s+(?:"[^"]+"|'[^']+'|<[^>]+>|\S+)/g, "").replace(/\s-w\s+(?:"[^"]+"|'[^']+'|<[^>]+>|\S+)/g, "");
936
+ }
937
+ function sandboxExample(example) {
938
+ return sandboxHelpText(example);
939
+ }
1495
940
 
1496
941
  // src/lib/types.ts
1497
942
  function requireString(args, key) {
@@ -1514,8 +959,8 @@ function requireNumberFrom(args, ...keys) {
1514
959
  if (!isNaN(num)) return num;
1515
960
  }
1516
961
  }
1517
- const hint = keys.map((k) => `--${k}`).join(" or ");
1518
- throw new Error(`Missing required argument: ${hint}`);
962
+ const hint2 = keys.map((k) => `--${k}`).join(" or ");
963
+ throw new Error(`Missing required argument: ${hint2}`);
1519
964
  }
1520
965
 
1521
966
  // src/domains/agent-tasks.ts
@@ -1653,6 +1098,42 @@ var agentTasksDomain = {
1653
1098
 
1654
1099
  // src/domains/agents.ts
1655
1100
  var COLUMNS = [COL.id(), COL.uuid(), COL.name(), COL.status(), COL.created()];
1101
+ var TOOL_COLUMNS = [
1102
+ COL.name(),
1103
+ { header: "DESCRIPTION", getValue: (item) => (item.description ?? "").slice(0, 60) }
1104
+ ];
1105
+ var ABILITY_COLUMNS = [
1106
+ COL.uuid(),
1107
+ { header: "ABILITY-TYPE", getValue: (item) => item.abilityType ?? "" },
1108
+ COL.enabled(),
1109
+ { header: "CEL-CONDITION", getValue: (item) => item.celCondition ?? "" }
1110
+ ];
1111
+ function requireAbilityScope(args) {
1112
+ const hasWorkflow = typeof args["workflow-id"] === "string";
1113
+ const hasTask = typeof args["task-id"] === "string";
1114
+ if (hasWorkflow === hasTask) {
1115
+ throw new CLIError(
1116
+ "Specify exactly one of --workflow-id or --task-id. Abilities are scoped to a workflow or task, not to the agent row itself."
1117
+ );
1118
+ }
1119
+ if (hasTask) {
1120
+ const taskId = Number(args["task-id"]);
1121
+ if (isNaN(taskId)) throw new CLIError("--task-id must be a number");
1122
+ return { taskId };
1123
+ }
1124
+ const workflowId = Number(args["workflow-id"]);
1125
+ if (isNaN(workflowId)) throw new CLIError("--workflow-id must be a number");
1126
+ return { workflowId };
1127
+ }
1128
+ function normalizeChannel(channel) {
1129
+ const aliases = {
1130
+ phone: "phone-voice",
1131
+ voice: "phone-voice",
1132
+ sms: "phone-sms",
1133
+ web: "web-chat"
1134
+ };
1135
+ return aliases[channel] ?? channel;
1136
+ }
1656
1137
  var agentsDomain = {
1657
1138
  name: "agents",
1658
1139
  description: "Manage agents",
@@ -1668,52 +1149,207 @@ var agentsDomain = {
1668
1149
  archiveMethod: "archiveAgent",
1669
1150
  unarchiveMethod: "unarchiveAgent",
1670
1151
  columns: COLUMNS,
1671
- paginationSupport: "full"
1672
- })
1673
- ]
1674
- };
1675
-
1676
- // src/domains/chats.ts
1677
- var COLUMNS2 = [
1678
- COL.id(),
1679
- COL.uuid(),
1680
- { header: "TYPE", getValue: (item) => item.chatType ?? item.chat_type ?? "" },
1681
- { header: "MEMBER", getValue: (item) => item.member_name ?? String(item.member_id ?? "") },
1682
- COL.status(),
1683
- { header: "CHANNEL", getValue: (item) => item.channel ?? "" },
1684
- COL.created()
1685
- ];
1686
- var LABEL_COLUMNS = [
1687
- COL.id(),
1688
- COL.uuid(),
1689
- COL.name(),
1690
- { header: "COLOR", getValue: (item) => item.color ?? "" }
1691
- ];
1692
- var FILTER_COLUMNS = [COL.id(), COL.uuid(), COL.name()];
1693
- var MESSAGE_COLUMNS2 = [
1694
- COL.id(),
1695
- { header: "ROLE", getValue: (item) => item.role ?? "" },
1696
- {
1697
- header: "CONTENT",
1698
- getValue: (item) => String(item.content ?? "").slice(0, 80),
1699
- maxWidth: 80
1700
- },
1701
- {
1702
- header: "TOOL_CALLS",
1703
- getValue: (item) => Array.isArray(item.tool_calls) && item.tool_calls.length > 0 ? item.tool_calls.map((t) => t.function?.name ?? "?").join(",") : ""
1704
- },
1705
- COL.created()
1706
- ];
1707
- var chatsDomain = {
1708
- name: "chats",
1709
- description: "Manage chats",
1710
- commands: [
1711
- ...crudCommands({
1712
- prefix: ["chats"],
1713
- entityName: "chat",
1714
- listMethod: "getChats",
1715
- getMethod: "getChat",
1716
- createMethod: "createChat",
1152
+ paginationSupport: "full",
1153
+ updateHelp: "Update an agent. Body schema (JSON via --data/--file/stdin), all fields optional:\n name string display name\n description string short summary shown in listings\n systemPrompt string base instructions for the agent\n model string AI model slug (see `gr ai-models list`)\n temperature number sampling temperature (0-2)\n avatarFileId number workspace file ID for the avatar\n config object AgentConfig overrides (advanced)\nAbilities are NOT set here \u2014 they live on a workflow/task scope; use `gr agents abilities` to manage them.",
1154
+ updateExamples: [
1155
+ `gr agents update --id 42 --data '{"name":"Scheduler","temperature":0.4}'`,
1156
+ "gr agents update --id 42 --file agent.json"
1157
+ ]
1158
+ }),
1159
+ {
1160
+ path: ["agents", "resolve-tools"],
1161
+ description: "Show resolved runtime tools for an agent in a channel context. Channel accepts canonical values (phone-voice, web-voice, phone-sms, web-chat) or shorthand aliases: phone -> phone-voice, voice -> phone-voice, sms -> phone-sms, web -> web-chat. Unknown values are passed through unchanged. When neither --task-id nor --task-uuid is supplied the agent's workflow start task is used.",
1162
+ requiresWorkspace: true,
1163
+ args: [
1164
+ { name: "id", description: "Agent ID", required: true },
1165
+ {
1166
+ name: "channel",
1167
+ description: "Channel to simulate. Canonical: phone-voice, web-voice, phone-sms, web-chat. Aliases: phone|voice -> phone-voice, sms -> phone-sms, web -> web-chat.",
1168
+ required: true
1169
+ },
1170
+ {
1171
+ name: "task-id",
1172
+ description: "Numeric task ID whose workflow abilities to resolve. Omit to use the workflow start task (or supply --task-uuid instead).",
1173
+ required: false
1174
+ },
1175
+ {
1176
+ name: "task-uuid",
1177
+ description: "Task UUID as an alternative to --task-id. Omit to use the workflow start task.",
1178
+ required: false
1179
+ }
1180
+ ],
1181
+ handler: async ({ ctx, workspace, args }) => {
1182
+ const rawTaskId = args["task-id"];
1183
+ const rawTaskUuid = args["task-uuid"];
1184
+ if (rawTaskId !== void 0 && rawTaskUuid !== void 0) {
1185
+ throw new Error("Provide at most one of --task-id or --task-uuid, not both");
1186
+ }
1187
+ const options = {
1188
+ channel: normalizeChannel(String(args.channel))
1189
+ };
1190
+ if (typeof rawTaskId === "string") {
1191
+ const n = Number(rawTaskId);
1192
+ if (isNaN(n)) throw new Error("Argument --task-id must be a number");
1193
+ options.taskId = n;
1194
+ }
1195
+ if (typeof rawTaskUuid === "string") {
1196
+ options.taskUuid = rawTaskUuid;
1197
+ }
1198
+ const result2 = await requireAuth(ctx).apiClient.resolveAgentTools(
1199
+ workspace,
1200
+ requireNumber(args, "id"),
1201
+ options
1202
+ );
1203
+ const tools = result2.tools ?? result2;
1204
+ outputData(Array.isArray(tools) ? tools : [tools], ctx, TOOL_COLUMNS);
1205
+ }
1206
+ },
1207
+ {
1208
+ path: ["agents", "abilities", "list"],
1209
+ description: "Show the abilities effective for an agent on a task (read-only). Requires --task-id because abilities are resolved through the task's workflow, not stored on the agent row.",
1210
+ requiresWorkspace: true,
1211
+ columns: ABILITY_COLUMNS,
1212
+ args: [
1213
+ { name: "id", description: "Agent ID", required: true },
1214
+ {
1215
+ name: "task-id",
1216
+ description: "Task ID whose workflow abilities to list",
1217
+ required: true
1218
+ }
1219
+ ],
1220
+ handler: async ({ ctx, workspace, args }) => {
1221
+ const result2 = await requireAuth(ctx).apiClient.listAgentAbilities(
1222
+ workspace,
1223
+ requireNumber(args, "id"),
1224
+ { taskId: requireNumber(args, "task-id") }
1225
+ );
1226
+ outputData(result2, ctx, ABILITY_COLUMNS);
1227
+ }
1228
+ },
1229
+ {
1230
+ path: ["agents", "abilities", "enable"],
1231
+ description: "Enable (create) an ability on a workflow or task scope. Requires exactly one of --workflow-id or --task-id \u2014 abilities are scoped to a workflow/task, never to the agent row. Provide initial --config JSON inline or via --data/--file.",
1232
+ mutates: true,
1233
+ requiresWorkspace: true,
1234
+ args: [
1235
+ { name: "ability-type", description: "Ability type slug (e.g. 'communication:send_sms')", required: true },
1236
+ { name: "workflow-id", description: "Workflow scope (mutually exclusive with --task-id)" },
1237
+ { name: "task-id", description: "Task scope (mutually exclusive with --workflow-id)" }
1238
+ ],
1239
+ examples: [
1240
+ "gr agents abilities enable --workflow-id 7 --ability-type tools:web_search",
1241
+ `gr agents abilities enable --task-id 12 --ability-type integration:data_access --data '{"dataTypeUuid":"..."}'`
1242
+ ],
1243
+ handler: async ({ ctx, workspace, args, data }) => {
1244
+ const abilityType = args["ability-type"];
1245
+ if (typeof abilityType !== "string") {
1246
+ throw new CLIError("--ability-type is required");
1247
+ }
1248
+ const scope = requireAbilityScope(args);
1249
+ const config = data && typeof data === "object" ? data : {};
1250
+ const body = {
1251
+ abilityType,
1252
+ config,
1253
+ ...scope
1254
+ };
1255
+ const result2 = await requireAuth(ctx).apiClient.createAbility(workspace, body);
1256
+ outputSingle(result2, ctx, ABILITY_COLUMNS);
1257
+ }
1258
+ },
1259
+ {
1260
+ path: ["agents", "abilities", "disable"],
1261
+ description: "Disable an ability instance by UUID (sets enabled=false, keeps config). Use `gr agents abilities list` to find the UUID.",
1262
+ mutates: true,
1263
+ requiresWorkspace: true,
1264
+ args: [{ name: "uuid", description: "Ability instance UUID", required: true }],
1265
+ handler: async ({ ctx, workspace, args }) => {
1266
+ const uuid = args.uuid;
1267
+ if (typeof uuid !== "string") throw new CLIError("--uuid is required");
1268
+ const result2 = await requireAuth(ctx).apiClient.updateAbility(workspace, uuid, {
1269
+ enabled: false
1270
+ });
1271
+ outputSingle(result2, ctx, ABILITY_COLUMNS);
1272
+ }
1273
+ },
1274
+ {
1275
+ path: ["agents", "abilities", "configure"],
1276
+ description: "Update an ability instance by UUID. Pass --data/--file JSON to merge into config, and/or --enabled true|false to toggle. config is merged with the existing config server-side.",
1277
+ mutates: true,
1278
+ requiresWorkspace: true,
1279
+ args: [
1280
+ { name: "uuid", description: "Ability instance UUID", required: true },
1281
+ { name: "enabled", description: "Set enabled state (true|false)" }
1282
+ ],
1283
+ examples: [
1284
+ `gr agents abilities configure --uuid <uuid> --data '{"canForward":true}'`,
1285
+ "gr agents abilities configure --uuid <uuid> --enabled true"
1286
+ ],
1287
+ handler: async ({ ctx, workspace, args, data }) => {
1288
+ const uuid = args.uuid;
1289
+ if (typeof uuid !== "string") throw new CLIError("--uuid is required");
1290
+ const body = {};
1291
+ if (data && typeof data === "object") {
1292
+ body.config = data;
1293
+ }
1294
+ if (typeof args.enabled === "string") {
1295
+ if (args.enabled !== "true" && args.enabled !== "false") {
1296
+ throw new CLIError("--enabled must be true or false");
1297
+ }
1298
+ body.enabled = args.enabled === "true";
1299
+ }
1300
+ if (Object.keys(body).length === 0) {
1301
+ throw new CLIError(
1302
+ "Nothing to update: provide --data config and/or --enabled"
1303
+ );
1304
+ }
1305
+ const result2 = await requireAuth(ctx).apiClient.updateAbility(workspace, uuid, body);
1306
+ outputSingle(result2, ctx, ABILITY_COLUMNS);
1307
+ }
1308
+ }
1309
+ ]
1310
+ };
1311
+
1312
+ // src/domains/chats.ts
1313
+ var COLUMNS2 = [
1314
+ COL.id(),
1315
+ COL.uuid(),
1316
+ { header: "TYPE", getValue: (item) => item.chatType ?? item.chat_type ?? "" },
1317
+ { header: "MEMBER", getValue: (item) => item.member_name ?? String(item.member_id ?? "") },
1318
+ COL.status(),
1319
+ { header: "CHANNEL", getValue: (item) => item.channel ?? "" },
1320
+ COL.created()
1321
+ ];
1322
+ var LABEL_COLUMNS = [
1323
+ COL.id(),
1324
+ COL.uuid(),
1325
+ COL.name(),
1326
+ { header: "COLOR", getValue: (item) => item.color ?? "" }
1327
+ ];
1328
+ var FILTER_COLUMNS = [COL.id(), COL.uuid(), COL.name()];
1329
+ var MESSAGE_COLUMNS2 = [
1330
+ COL.id(),
1331
+ { header: "ROLE", getValue: (item) => item.role ?? "" },
1332
+ {
1333
+ header: "CONTENT",
1334
+ getValue: (item) => String(item.content ?? "").slice(0, 80),
1335
+ maxWidth: 80
1336
+ },
1337
+ {
1338
+ header: "TOOL_CALLS",
1339
+ getValue: (item) => Array.isArray(item.tool_calls) && item.tool_calls.length > 0 ? item.tool_calls.map((t) => t.function?.name ?? "?").join(",") : ""
1340
+ },
1341
+ COL.created()
1342
+ ];
1343
+ var chatsDomain = {
1344
+ name: "chats",
1345
+ description: "Manage chats",
1346
+ commands: [
1347
+ ...crudCommands({
1348
+ prefix: ["chats"],
1349
+ entityName: "chat",
1350
+ listMethod: "getChats",
1351
+ getMethod: "getChat",
1352
+ createMethod: "createChat",
1717
1353
  updateMethod: "updateChat",
1718
1354
  deleteMethod: "deleteChat",
1719
1355
  archiveMethod: "archiveChat",
@@ -1894,21 +1530,47 @@ var chatsDomain = {
1894
1530
  // Inspect (aggregated debug view)
1895
1531
  {
1896
1532
  path: ["chats", "inspect"],
1897
- description: "Inspect a chat with messages, assignment state, and data records",
1898
- requiresWorkspace: true,
1533
+ description: "Inspect a chat with messages, assignment state, and data records. Omit -w for concierge (account-level) chats.",
1534
+ requiresWorkspace: false,
1899
1535
  args: [{ name: "id", description: "Chat ID", required: true }],
1900
1536
  handler: async ({ ctx, workspace, args }) => {
1901
1537
  const chatId = requireNumber(args, "id");
1902
- const [chat, messages, ws] = await Promise.all([
1903
- requireAuth(ctx).apiClient.getChat(workspace, chatId),
1904
- requireAuth(ctx).apiClient.getChatMessages(workspace, chatId),
1905
- requireAuth(ctx).apiClient.getWorkspace(workspace)
1906
- ]);
1907
- const isHipaa = ws.features?.includes("hipaa_compliance");
1538
+ const authed = requireAuth(ctx);
1908
1539
  const truncate2 = (content) => {
1909
1540
  const text = typeof content === "string" ? content : JSON.stringify(content);
1910
1541
  return text.slice(0, 200);
1911
1542
  };
1543
+ if (!workspace) {
1544
+ const [chat2, messages2] = await Promise.all([
1545
+ getConciergeChat(authed, chatId),
1546
+ getConciergeChatMessages(authed, chatId)
1547
+ ]);
1548
+ outputSingle(
1549
+ {
1550
+ chat: {
1551
+ id: chat2.id,
1552
+ uuid: chat2.uuid,
1553
+ title: chat2.title,
1554
+ chatType: chat2.chatType,
1555
+ threadId: chat2.threadId,
1556
+ createdAt: chat2.createdAt
1557
+ },
1558
+ messages: messages2.map((m) => ({
1559
+ id: m.id,
1560
+ role: m.role,
1561
+ content: truncate2(m.content)
1562
+ }))
1563
+ },
1564
+ ctx
1565
+ );
1566
+ return;
1567
+ }
1568
+ const [chat, messages, ws] = await Promise.all([
1569
+ authed.apiClient.getChat(workspace, chatId),
1570
+ authed.apiClient.getChatMessages(workspace, chatId),
1571
+ authed.apiClient.getWorkspace(workspace)
1572
+ ]);
1573
+ const isHipaa = ws.features?.includes("hipaa_compliance");
1912
1574
  const result2 = {
1913
1575
  chat: {
1914
1576
  id: chat.id,
@@ -1928,10 +1590,7 @@ var chatsDomain = {
1928
1590
  };
1929
1591
  const assignmentId = chat.assignmentId;
1930
1592
  if (assignmentId) {
1931
- const assignment = await requireAuth(ctx).apiClient.getAssignment(
1932
- workspace,
1933
- assignmentId
1934
- );
1593
+ const assignment = await authed.apiClient.getAssignment(workspace, assignmentId);
1935
1594
  result2.assignment = {
1936
1595
  id: assignment.id,
1937
1596
  status: assignment.status,
@@ -2186,10 +1845,7 @@ var memberRolesDomain = {
2186
1845
  updateMethod: "updateMemberRole",
2187
1846
  deleteMethod: "deleteMemberRole",
2188
1847
  columns: ROLE_COLUMNS2,
2189
- createExamples: [
2190
- "gr member-roles list -w $WID",
2191
- "gr member-roles get -w $WID --id 2"
2192
- ]
1848
+ createExamples: ["gr member-roles list -w $WID", "gr member-roles get -w $WID --id 2"]
2193
1849
  })
2194
1850
  };
2195
1851
 
@@ -2198,10 +1854,13 @@ var COLUMNS4 = [
2198
1854
  COL.id(),
2199
1855
  COL.uuid(),
2200
1856
  COL.name(),
2201
- COL.status(),
1857
+ // TaskResponse has no `status` field — it has `archived`. Surface that
1858
+ // directly so active vs archived is visible in the default table (relevant
1859
+ // once --include-archived mixes both into the listing).
1860
+ { header: "STATUS", getValue: (item) => item.archived ? "archived" : "active" },
2202
1861
  {
2203
1862
  header: "WORKFLOW",
2204
- getValue: (item) => item.workflow_name ?? String(item.workflow_id ?? "")
1863
+ getValue: (item) => String(item.workflowId ?? "")
2205
1864
  },
2206
1865
  COL.created()
2207
1866
  ];
@@ -2219,7 +1878,21 @@ var tasksDomain = {
2219
1878
  deleteMethod: "deleteTask",
2220
1879
  archiveMethod: "archiveTask",
2221
1880
  unarchiveMethod: "unarchiveTask",
2222
- columns: COLUMNS4
1881
+ columns: COLUMNS4,
1882
+ // `tasks list` is workspace-wide by default. Filter to one workflow
1883
+ // with --workflow-id, and opt into archived tasks with
1884
+ // --include-archived (active-only by default, matching the backend).
1885
+ listArgs: [
1886
+ {
1887
+ name: "workflow-id",
1888
+ description: "Filter tasks to a single workflow by ID"
1889
+ },
1890
+ {
1891
+ name: "include-archived",
1892
+ description: "Include archived tasks in the listing",
1893
+ isFlag: true
1894
+ }
1895
+ ]
2223
1896
  }),
2224
1897
  {
2225
1898
  path: ["tasks", "clone"],
@@ -2274,9 +1947,18 @@ var COLUMNS5 = [
2274
1947
  COL.id(),
2275
1948
  COL.uuid(),
2276
1949
  COL.name(),
2277
- { header: "TASKS", getValue: (item) => String(item.taskCount ?? "") },
1950
+ // taskCount is the total (active + archived). Show the active count as the
1951
+ // headline number — matching `tasks list` — and call out archived tasks
1952
+ // separately so the table never overstates how many tasks actually run.
1953
+ { header: "TASKS", getValue: (item) => formatTaskCount(item) },
2278
1954
  COL.created()
2279
1955
  ];
1956
+ function formatTaskCount(item) {
1957
+ const total = Number(item.taskCount ?? 0);
1958
+ const archived = Number(item.archivedTaskCount ?? 0);
1959
+ const active = total - archived;
1960
+ return archived > 0 ? `${active} (+${archived} archived)` : String(active);
1961
+ }
2280
1962
  var NOTE_COLUMNS = [
2281
1963
  COL.id(),
2282
1964
  { header: "TITLE", getValue: (item) => item.title ?? "" },
@@ -2377,13 +2059,21 @@ var workflowsDomain = {
2377
2059
  // Diagram
2378
2060
  {
2379
2061
  path: ["workflows", "diagram"],
2380
- description: "Get workflow diagram",
2062
+ description: "Get workflow diagram (active tasks only by default)",
2381
2063
  requiresWorkspace: true,
2382
- args: [{ name: "id", description: "Workflow ID", required: true }],
2064
+ args: [
2065
+ { name: "id", description: "Workflow ID", required: true },
2066
+ {
2067
+ name: "include-archived",
2068
+ description: "Render archived tasks (distinctly styled) too",
2069
+ isFlag: true
2070
+ }
2071
+ ],
2383
2072
  handler: async ({ ctx, workspace, args }) => {
2384
2073
  const result2 = await requireAuth(ctx).apiClient.getWorkflowDiagram(
2385
2074
  workspace,
2386
- requireNumber(args, "id")
2075
+ requireNumber(args, "id"),
2076
+ { includeArchived: args["include-archived"] === true }
2387
2077
  );
2388
2078
  outputSingle(result2, ctx);
2389
2079
  }
@@ -2900,28 +2590,64 @@ var workspacesDomain = {
2900
2590
  outputSingle(result2, ctx, COLUMNS7);
2901
2591
  }
2902
2592
  },
2903
- // Create a workspace via the public endpoint
2593
+ // Provision a workspace under an organization's active subscription.
2904
2594
  {
2905
2595
  path: ["workspaces", "create"],
2906
- description: "Create a workspace",
2596
+ description: "Provision a workspace in an organization (requires an active subscription)",
2907
2597
  mutates: true,
2908
2598
  requiresWorkspace: false,
2909
- handler: async ({ ctx, data }) => {
2910
- if (!data) throw new CLIError("JSON data required (--data, --file, or stdin)");
2911
- const payload = data;
2912
- if (!payload.organizationId) {
2599
+ args: [
2600
+ {
2601
+ name: "org",
2602
+ aliases: ["org-uuid"],
2603
+ description: "Organization UUID",
2604
+ required: true
2605
+ },
2606
+ { name: "product", description: "Workspace plan product ID", required: true },
2607
+ { name: "name", description: "Workspace name", required: true },
2608
+ { name: "period", description: "Billing period: 'month' or 'year'", default: "month" },
2609
+ { name: "slug", description: "Workspace slug (optional)" },
2610
+ { name: "template", description: "Template workspace UUID to clone (optional)" }
2611
+ ],
2612
+ examples: ['gr workspaces create --org <org-uuid> --product 14 --name "Front Desk"'],
2613
+ handler: async ({ ctx, args }) => {
2614
+ const orgUuid = args.org ?? args["org-uuid"];
2615
+ const name = args.name;
2616
+ const productRaw = args.product;
2617
+ if (!orgUuid) throw new CLIError("Missing required argument: --org <org-uuid>");
2618
+ if (!name) throw new CLIError("Missing required argument: --name");
2619
+ if (!productRaw) {
2913
2620
  throw new CLIError(
2914
- `organizationId is required in --data (e.g. --data '{"organizationId": 1, "name": "My Workspace"}')`
2621
+ "Missing required argument: --product\nList workspace plans with: gr billing products --type workspace"
2915
2622
  );
2916
2623
  }
2624
+ const payload = {
2625
+ name,
2626
+ productId: Number(productRaw),
2627
+ planPeriod: args.period || "month"
2628
+ };
2629
+ if (args.slug) payload.slug = args.slug;
2630
+ if (args.template) payload.templateWorkspaceUuid = args.template;
2917
2631
  try {
2918
- const result2 = await requireAuth(ctx).apiClient.createWorkspace(payload);
2632
+ const result2 = await requireAuth(ctx).apiClient.createOrganizationWorkspace(
2633
+ orgUuid,
2634
+ payload
2635
+ );
2919
2636
  outputSingle(result2, ctx, COLUMNS7);
2637
+ if ((ctx.outputFormat ?? "table") === "table") {
2638
+ const wsUuid = result2?.uuid ?? "<workspace-uuid>";
2639
+ process.stdout.write(
2640
+ `
2641
+ Provisioning started. Watch it come up with:
2642
+ gr workspaces get -w ${wsUuid}
2643
+ `
2644
+ );
2645
+ }
2920
2646
  } catch (err) {
2921
2647
  const statusCode = err?.statusCode;
2922
- if (statusCode === 405) {
2648
+ if (statusCode === 400) {
2923
2649
  throw new CLIError(
2924
- 'Creating a workspace directly is not supported \u2014 provisioning runs through the concierge (it handles product selection and billing).\nNext: gr concierge chat --allow-write --message "Create a new workspace"\nThen: gr workspaces list (to get the new workspace UUID for -w)'
2650
+ "Could not create the workspace. The organization needs an active, paid subscription first.\n gr billing products --type org # pick an org plan\n gr billing checkout --product <id> # pay up front\n gr billing status # confirm it is active"
2925
2651
  );
2926
2652
  }
2927
2653
  throw err;
@@ -3421,7 +3147,7 @@ var MESSAGE_COLUMNS4 = [
3421
3147
  COL.created()
3422
3148
  ];
3423
3149
  var TASK_COLUMNS = [COL.id(), COL.name(), COL.status()];
3424
- var TOOL_COLUMNS = [
3150
+ var TOOL_COLUMNS2 = [
3425
3151
  COL.name(),
3426
3152
  { header: "DESCRIPTION", getValue: (item) => (item.description ?? "").slice(0, 60) }
3427
3153
  ];
@@ -3529,7 +3255,7 @@ var assignmentsDomain = {
3529
3255
  requireNumber(args, "assignment-id")
3530
3256
  );
3531
3257
  const tools = result2.tools ?? result2;
3532
- outputData(Array.isArray(tools) ? tools : [tools], ctx, TOOL_COLUMNS);
3258
+ outputData(Array.isArray(tools) ? tools : [tools], ctx, TOOL_COLUMNS2);
3533
3259
  }
3534
3260
  }
3535
3261
  ]
@@ -3975,35 +3701,24 @@ Common optional
3975
3701
  Scope (set exactly one when objectType requires it)
3976
3702
  routineId, workflowId, taskId, dataTypeId, journeyStepId, calendarId, ...
3977
3703
 
3978
- chat:create actionParams (outbound voice/SMS)
3979
- {"workflowId":<id>,"channel":"phone-voice"|"phone-sms","phoneNumberId":<id>,
3980
- "inboxId":<optional>,"assistantEnabled":<bool>}
3704
+ chat:create actionParams (outbound voice/SMS \u2014 starts a workflow chat)
3705
+ {"workflowUUID":"<workflow-uuid>","channel":"phone-voice"|"phone-sms",
3706
+ "phoneNumberUUID":"<uuid>","inboxUUID":"<optional-uuid>","assistantEnabled":<bool>}
3707
+ Keys are UUID strings: workflowUUID (not workflowId), phoneNumberUUID (not
3708
+ phoneNumberId), inboxUUID (not inboxId). Unknown keys are rejected.
3709
+
3710
+ sms:send actionParams (send an SMS, or start an SMS workflow chat)
3711
+ One-off text: {"phoneNumberUUID":"<from-number-uuid>","messageTemplate":"Hi {{name}} \u2026"}
3712
+ Workflow chat: {"phoneNumberUUID":"<from-number-uuid>","workflowUUID":"<workflow-uuid>",
3713
+ "initialUserMessage":"<optional seed>"}
3714
+ phoneNumberUUID (the from-number) is required. The message body is messageTemplate
3715
+ (supports {{record/member}} template vars) \u2014 there is no "body"/"message" key.
3716
+ Provide messageTemplate for a one-off text, OR workflowUUID to start a chat.
3981
3717
 
3982
3718
  member:label actionParams
3983
3719
  {"addLabelIds":["<uuid>"],"removeLabelIds":["<uuid>"]}
3984
3720
  `;
3985
3721
 
3986
- // src/lib/routines/field-help.ts
3987
- var ROUTINE_FIELD_REFERENCE = `
3988
- Field reference for --data JSON (create/update):
3989
-
3990
- Core
3991
- name (string) Display name
3992
- workflowId (number) Workflow run when the routine fires
3993
- targetMemberId (number) Member who receives the scheduled outreach
3994
-
3995
- Schedule
3996
- scheduleEnabled (bool) Master on/off for the cron trigger
3997
- scheduleMode (string) "CRON" (only mode today)
3998
- cronString (string) Standard cron, e.g. "0 7 * * *"
3999
- timezone (string, optional) IANA name, e.g. "America/Los_Angeles"
4000
- Defaults to workspace timezone, else UTC
4001
-
4002
- Visibility / lifecycle
4003
- archived (bool) Archive flag on update
4004
- event rules Use gr events rules create \u2014 not inline here
4005
- `;
4006
-
4007
3722
  // src/domains/events.ts
4008
3723
  var RULE_COLUMNS = [
4009
3724
  COL.id(),
@@ -4011,15 +3726,6 @@ var RULE_COLUMNS = [
4011
3726
  { header: "EVENT", getValue: (item) => item.eventType ?? "" },
4012
3727
  { header: "ACTIVE", getValue: (item) => String(item.active ?? "") }
4013
3728
  ];
4014
- var ROUTINE_COLUMNS = [
4015
- COL.id(),
4016
- COL.name(),
4017
- { header: "MODE", getValue: (item) => item.scheduleMode ?? "" },
4018
- { header: "CRON", getValue: (item) => item.cronString ?? "" },
4019
- { header: "TZ", getValue: (item) => item.timezone ?? "UTC" },
4020
- { header: "ENABLED", getValue: (item) => String(item.scheduleEnabled ?? "") },
4021
- { header: "NEXT RUN", getValue: (item) => item.nextRun ?? "" }
4022
- ];
4023
3729
  var eventRulesCrudNoList = crudCommands({
4024
3730
  prefix: ["events", "rules"],
4025
3731
  entityName: "event rule",
@@ -4043,8 +3749,8 @@ ${EVENT_RULE_FIELD_REFERENCE}`,
4043
3749
  `gr events rules create -w $WID --allow-write --data '{"name":"AM voice check-in",`,
4044
3750
  ' "objectType":"routine","eventType":"routine:triggered","actionType":"chat:create",',
4045
3751
  ' "routineId":<routine-id>,',
4046
- ' "actionParams":{"workflowId":<workflow-id>,"channel":"phone-voice",',
4047
- ` "phoneNumberId":<workspace-phone-id>}}'`,
3752
+ ' "actionParams":{"workflowUUID":"<workflow-uuid>","channel":"phone-voice",',
3753
+ ` "phoneNumberUUID":"<workspace-phone-uuid>"}}'`,
4048
3754
  "",
4049
3755
  ' PM SMS variant: use "channel":"phone-sms" and a second rule on the same routineId.',
4050
3756
  " Prereq: org phone assigned + gr phone-numbers link; workflow published with start task.",
@@ -4182,77 +3888,9 @@ var eventsDomain = {
4182
3888
  const result2 = await requireAuth(ctx).apiClient.searchEventRules(workspace, opts);
4183
3889
  outputData(result2, ctx, RULE_COLUMNS);
4184
3890
  }
4185
- },
4186
- // Routines
4187
- ...crudCommands({
4188
- prefix: ["routines"],
4189
- entityName: "routine",
4190
- listMethod: "getRoutines",
4191
- getMethod: "getRoutine",
4192
- createMethod: "createRoutine",
4193
- updateMethod: "updateRoutine",
4194
- deleteMethod: "deleteRoutine",
4195
- paginationSupport: "full",
4196
- columns: ROUTINE_COLUMNS,
4197
- createHelp: `Create a routine
4198
-
4199
- ${ROUTINE_FIELD_REFERENCE}`,
4200
- updateHelp: `Update a routine
4201
-
4202
- ${ROUTINE_FIELD_REFERENCE}`,
4203
- createExamples: [
4204
- "Daily 7am check-in in the member\u2019s timezone (cron is interpreted in `timezone`):",
4205
- `gr routines create -w $WID --allow-write --data '{"name":"AM check-in",`,
4206
- ' "scheduleEnabled":true,"scheduleMode":"CRON","cronString":"0 7 * * *",',
4207
- ` "timezone":"America/Los_Angeles","targetMemberId":1,"workflowId":2}'`,
4208
- " (timezone is an IANA name; omit it to default to the workspace timezone, else UTC.",
4209
- ' Without it, "0 7 * * *" fires at 07:00 UTC, not 7am local.)'
4210
- ]
4211
- }),
4212
- {
4213
- path: ["routines", "run"],
4214
- description: "Run a routine now",
4215
- mutates: true,
4216
- requiresWorkspace: true,
4217
- args: [{ name: "id", description: "Routine ID", required: true }],
4218
- handler: async ({ ctx, workspace, args, data }) => {
4219
- const result2 = await requireAuth(ctx).apiClient.runRoutine(
4220
- workspace,
4221
- requireNumber(args, "id"),
4222
- data ?? {}
4223
- );
4224
- outputSingle(result2, ctx);
4225
- }
4226
- },
4227
- {
4228
- path: ["routines", "runs", "list"],
4229
- description: "List runs for a routine",
4230
- requiresWorkspace: true,
4231
- args: [
4232
- {
4233
- name: "id",
4234
- description: "Routine ID (aligns with routines get/run/update/delete)"
4235
- },
4236
- { name: "status", description: "Filter by run status" },
4237
- { name: "page", description: "Page number (1-indexed)" },
4238
- { name: "page-size", description: "Page size (max 100)" }
4239
- ],
4240
- handler: async ({ ctx, workspace, args }) => {
4241
- const routineId = args.id ? requireNumber(args, "id") : void 0;
4242
- const page = args.page ? requireNumber(args, "page") : void 0;
4243
- const pageSize = args["page-size"] ? requireNumber(args, "page-size") : void 0;
4244
- const status = args.status ?? void 0;
4245
- const result2 = await requireAuth(ctx).apiClient.getRoutineRuns(workspace, {
4246
- ...routineId !== void 0 ? { routineId } : {},
4247
- ...page !== void 0 ? { page } : {},
4248
- ...pageSize !== void 0 ? { pageSize } : {},
4249
- ...status ? { status } : {}
4250
- });
4251
- outputData(result2, ctx);
4252
- }
4253
- }
4254
- ]
4255
- };
3891
+ }
3892
+ ]
3893
+ };
4256
3894
 
4257
3895
  // src/domains/experiments.ts
4258
3896
  var EXPERIMENT_COLUMNS = [
@@ -5866,6 +5504,178 @@ var sitesDomain = {
5866
5504
  ]
5867
5505
  };
5868
5506
 
5507
+ // src/domains/abilities.ts
5508
+ import { randomUUID } from "crypto";
5509
+ var ATTACHED_COLUMNS = [
5510
+ { header: "UUID", getValue: (item) => item.uuid ?? "" },
5511
+ { header: "TYPE", getValue: (item) => item.abilityType ?? "" },
5512
+ { header: "ENABLED", getValue: (item) => item.enabled === false ? "" : "yes" },
5513
+ { header: "CEL", getValue: (item) => item.celCondition ?? "" }
5514
+ ];
5515
+ function toAbilityData(a) {
5516
+ return {
5517
+ uuid: a.uuid,
5518
+ abilityType: a.abilityType,
5519
+ config: a.config ?? {},
5520
+ enabled: a.enabled ?? true,
5521
+ ...a.celCondition ? { celCondition: a.celCondition } : {}
5522
+ };
5523
+ }
5524
+ var TYPE_COLUMNS = [
5525
+ { header: "SLUG", getValue: (item) => item.slug ?? "" },
5526
+ { header: "TITLE", getValue: (item) => item.title ?? "" },
5527
+ { header: "CATEGORY", getValue: (item) => item.category ?? "" },
5528
+ {
5529
+ header: "CHANNELS",
5530
+ // null channels means "all channels" — show that explicitly so an empty
5531
+ // cell never reads as "no channels".
5532
+ getValue: (item) => Array.isArray(item.channels) ? item.channels.join(",") : "all"
5533
+ },
5534
+ { header: "SINGLETON", getValue: (item) => item.singleton ? "yes" : "" },
5535
+ { header: "FEATURE-GATE", getValue: (item) => item.requiredFeature ?? "" }
5536
+ ];
5537
+ var abilitiesDomain = {
5538
+ name: "abilities",
5539
+ description: "Inspect ability types and attach/detach them on a task",
5540
+ commands: [
5541
+ {
5542
+ path: ["abilities", "list"],
5543
+ description: "List builtin ability types with their channel constraints and param specs. Table shows SLUG, TITLE, CATEGORY, CHANNELS (all = every channel), SINGLETON, FEATURE-GATE; use -o json for full specs including the params JSON Schema.",
5544
+ requiresWorkspace: true,
5545
+ columns: TYPE_COLUMNS,
5546
+ args: [
5547
+ {
5548
+ name: "include-system",
5549
+ description: "Also list auto-injected system abilities (hidden by default)",
5550
+ isFlag: true
5551
+ }
5552
+ ],
5553
+ handler: async (hctx) => {
5554
+ const includeSystem = hctx.args["include-system"] === true;
5555
+ const result2 = await requireAuth(hctx.ctx).apiClient.getAbilityTypes(
5556
+ hctx.workspace,
5557
+ includeSystem ? { includeSystem: true } : {}
5558
+ );
5559
+ outputData(result2.types ?? [], hctx.ctx, TYPE_COLUMNS);
5560
+ }
5561
+ },
5562
+ {
5563
+ path: ["abilities", "get"],
5564
+ description: "Show the full spec for one ability type: title, description, category, channels, singleton/system flags, required feature, and the JSON Schema of its params model.",
5565
+ requiresWorkspace: true,
5566
+ args: [
5567
+ {
5568
+ name: "name",
5569
+ aliases: ["slug"],
5570
+ description: "Ability type slug (e.g. 'communication:phone_call_tools')",
5571
+ required: true
5572
+ }
5573
+ ],
5574
+ handler: async (hctx) => {
5575
+ const slug = hctx.args.name ?? hctx.args.slug;
5576
+ if (!slug) throw new CLIError("--name is required");
5577
+ const result2 = await requireAuth(hctx.ctx).apiClient.getAbilityType(
5578
+ hctx.workspace,
5579
+ slug
5580
+ );
5581
+ outputSingle(result2, hctx.ctx);
5582
+ }
5583
+ },
5584
+ {
5585
+ path: ["abilities", "attached"],
5586
+ description: "List a task's task-specific abilities (excludes workflow-inherited).",
5587
+ requiresWorkspace: true,
5588
+ columns: ATTACHED_COLUMNS,
5589
+ args: [{ name: "task", description: "Task ID", required: true }],
5590
+ handler: async (hctx) => {
5591
+ const task = await requireAuth(hctx.ctx).apiClient.getTask(
5592
+ hctx.workspace,
5593
+ requireNumber(hctx.args, "task")
5594
+ );
5595
+ outputData(task.abilities ?? [], hctx.ctx, ATTACHED_COLUMNS);
5596
+ }
5597
+ },
5598
+ {
5599
+ path: ["abilities", "attach"],
5600
+ description: "Attach an ability to a task. Reads the task, appends the new ability (generating its uuid), and saves \u2014 so existing abilities aren't lost. Find slugs via 'gr abilities list' and the config schema via 'gr abilities get'.",
5601
+ mutates: true,
5602
+ requiresWorkspace: true,
5603
+ args: [
5604
+ { name: "task", description: "Task ID to attach to", required: true },
5605
+ {
5606
+ name: "type",
5607
+ aliases: ["ability-type", "slug"],
5608
+ description: "Ability type slug, e.g. 'integration:data_access'",
5609
+ required: true
5610
+ },
5611
+ {
5612
+ name: "config",
5613
+ description: `Ability config as a JSON object, e.g. '{"pauseOnEscalate":true}'`
5614
+ },
5615
+ { name: "cel", description: "Optional CEL activation condition" },
5616
+ {
5617
+ name: "disabled",
5618
+ description: "Attach in a disabled state (default: enabled)",
5619
+ isFlag: true
5620
+ }
5621
+ ],
5622
+ handler: async (hctx) => {
5623
+ const api = requireAuth(hctx.ctx).apiClient;
5624
+ const taskId = requireNumber(hctx.args, "task");
5625
+ const abilityType = hctx.args.type ?? hctx.args["ability-type"] ?? hctx.args.slug;
5626
+ if (!abilityType) throw new CLIError("--type is required (an ability type slug)");
5627
+ let config = {};
5628
+ if (hctx.args.config) {
5629
+ try {
5630
+ config = JSON.parse(hctx.args.config);
5631
+ } catch {
5632
+ throw new CLIError("--config must be a valid JSON object");
5633
+ }
5634
+ }
5635
+ const task = await api.getTask(hctx.workspace, taskId);
5636
+ const existing = (task.abilities ?? []).map(toAbilityData);
5637
+ const next = {
5638
+ uuid: randomUUID(),
5639
+ abilityType,
5640
+ config,
5641
+ enabled: hctx.args.disabled !== true,
5642
+ ...hctx.args.cel ? { celCondition: hctx.args.cel } : {}
5643
+ };
5644
+ const updated = await api.updateTask(hctx.workspace, taskId, {
5645
+ abilities: [...existing, next]
5646
+ });
5647
+ outputData(updated.abilities ?? [], hctx.ctx, ATTACHED_COLUMNS);
5648
+ }
5649
+ },
5650
+ {
5651
+ path: ["abilities", "detach"],
5652
+ description: "Detach an ability from a task by its uuid (from `gr abilities attached`). Re-saves the remaining task-specific abilities so the rest are preserved.",
5653
+ mutates: true,
5654
+ requiresWorkspace: true,
5655
+ args: [
5656
+ { name: "task", description: "Task ID", required: true },
5657
+ { name: "uuid", description: "UUID of the attached ability to remove", required: true }
5658
+ ],
5659
+ handler: async (hctx) => {
5660
+ const api = requireAuth(hctx.ctx).apiClient;
5661
+ const taskId = requireNumber(hctx.args, "task");
5662
+ const uuid = hctx.args.uuid;
5663
+ if (!uuid) throw new CLIError("--uuid is required");
5664
+ const task = await api.getTask(hctx.workspace, taskId);
5665
+ const existing = task.abilities ?? [];
5666
+ const remaining = existing.filter((a) => a.uuid !== uuid).map(toAbilityData);
5667
+ if (remaining.length === existing.length) {
5668
+ throw new CLIError(`No attached ability with uuid '${uuid}' on task ${taskId}`);
5669
+ }
5670
+ const updated = await api.updateTask(hctx.workspace, taskId, {
5671
+ abilities: remaining
5672
+ });
5673
+ outputData(updated.abilities ?? [], hctx.ctx, ATTACHED_COLUMNS);
5674
+ }
5675
+ }
5676
+ ]
5677
+ };
5678
+
5869
5679
  // src/domains/access-grants.ts
5870
5680
  var COLUMNS14 = [
5871
5681
  COL.id(),
@@ -5938,6 +5748,7 @@ var accessGrantsDomain = {
5938
5748
  {
5939
5749
  path: ["access-grants", "resolve"],
5940
5750
  description: "Resolve an access grant",
5751
+ mutates: true,
5941
5752
  requiresWorkspace: true,
5942
5753
  args: [{ name: "id", description: "Access grant ID", required: true }],
5943
5754
  handler: async ({ ctx, workspace, args, data }) => {
@@ -6386,6 +6197,149 @@ var authManageDomain = {
6386
6197
  ]
6387
6198
  };
6388
6199
 
6200
+ // src/domains/billing.ts
6201
+ function priceSummary(product) {
6202
+ const plans = product.plans ?? {};
6203
+ const abbr = { month: "mo", year: "yr", quarter: "qtr" };
6204
+ const parts = [];
6205
+ for (const [period, plan] of Object.entries(plans)) {
6206
+ const cents = plan?.costPerPeriod ?? 0;
6207
+ if (!plan?.stripePriceId) continue;
6208
+ parts.push(`$${(cents / 100).toFixed(0)}/${abbr[period] ?? period}`);
6209
+ }
6210
+ return parts.join(", ") || "\u2014";
6211
+ }
6212
+ var PRODUCT_COLUMNS2 = [
6213
+ { header: "ID", getValue: (p) => String(p.id ?? "") },
6214
+ { header: "NAME", getValue: (p) => p.name ?? "" },
6215
+ { header: "TYPE", getValue: (p) => String(p.productType ?? "") },
6216
+ { header: "PRICE", getValue: (p) => priceSummary(p) },
6217
+ {
6218
+ header: "TRIAL",
6219
+ getValue: (p) => {
6220
+ const days = p.trialPeriodDays ?? 0;
6221
+ return days > 0 ? `${days}d` : "\u2014";
6222
+ }
6223
+ }
6224
+ ];
6225
+ var SUBSCRIPTION_COLUMNS = [
6226
+ COL.id(),
6227
+ COL.status(),
6228
+ { header: "PLAN", getValue: (s) => String(s.plan_name ?? s.productName ?? s.plan ?? "") },
6229
+ { header: "PERIOD", getValue: (s) => String(s.planPeriod ?? s.plan_period ?? "") },
6230
+ {
6231
+ header: "ORG",
6232
+ getValue: (s) => String(s.organizationUuid ?? s.customerOrganizationUuid ?? "")
6233
+ }
6234
+ ];
6235
+ function isTable(ctx) {
6236
+ return (ctx.outputFormat ?? "table") === "table";
6237
+ }
6238
+ function hint(ctx, lines) {
6239
+ if (!isTable(ctx)) return;
6240
+ process.stdout.write("\n" + lines.join("\n") + "\n");
6241
+ }
6242
+ function productType(args) {
6243
+ const t = args.type ?? args["product-type"];
6244
+ if (t === "org" || t === "workspace") return t;
6245
+ return void 0;
6246
+ }
6247
+ var billingDomain = {
6248
+ name: "billing",
6249
+ description: "Browse plans, start checkout, and check subscription status",
6250
+ commands: [
6251
+ {
6252
+ path: ["billing", "products"],
6253
+ description: "List self-service plans you can purchase",
6254
+ requiresWorkspace: false,
6255
+ args: [
6256
+ {
6257
+ name: "type",
6258
+ description: "Filter by plan type: 'org' or 'workspace'"
6259
+ }
6260
+ ],
6261
+ examples: ["gr billing products --type org"],
6262
+ handler: async ({ ctx, args }) => {
6263
+ const products = await requireAuth(ctx).apiClient.listSelfServeProducts(
6264
+ productType(args)
6265
+ );
6266
+ outputData(products, ctx, PRODUCT_COLUMNS2);
6267
+ if (products.length === 0) {
6268
+ hint(ctx, ["No purchasable plans found. (Free plans are not sold via the CLI.)"]);
6269
+ } else {
6270
+ hint(ctx, [
6271
+ "Next \u2014 buy an organization plan to get started:",
6272
+ " gr billing checkout --product <id> [--period month|year]"
6273
+ ]);
6274
+ }
6275
+ }
6276
+ },
6277
+ {
6278
+ path: ["billing", "checkout"],
6279
+ description: "Get a Stripe payment link to purchase an organization plan",
6280
+ mutates: true,
6281
+ requiresWorkspace: false,
6282
+ args: [
6283
+ { name: "product", description: "Product ID of the org plan to buy", required: true },
6284
+ { name: "period", description: "Billing period: 'month' or 'year'", default: "month" }
6285
+ ],
6286
+ examples: ["gr billing checkout --product 12 --period month"],
6287
+ handler: async ({ ctx, args }) => {
6288
+ const productId = Number(requireString(args, "product"));
6289
+ const period = args.period || "month";
6290
+ const result2 = await requireAuth(ctx).apiClient.createCheckoutSession(
6291
+ productId,
6292
+ period
6293
+ );
6294
+ if (isTable(ctx)) {
6295
+ process.stdout.write(
6296
+ `
6297
+ \u{1F4B3} Open this link in a browser to complete payment:
6298
+
6299
+ ${result2.checkoutUrl}
6300
+
6301
+ Once payment succeeds, your organization and subscription are created automatically.
6302
+ Then check it landed:
6303
+ gr billing status
6304
+ `
6305
+ );
6306
+ } else {
6307
+ outputSingle(result2, ctx);
6308
+ }
6309
+ }
6310
+ },
6311
+ {
6312
+ path: ["billing", "status"],
6313
+ description: "Show your subscriptions and what to do next",
6314
+ requiresWorkspace: false,
6315
+ examples: ["gr billing status"],
6316
+ handler: async ({ ctx }) => {
6317
+ const subs = await requireAuth(ctx).apiClient.getSubscriptions();
6318
+ outputData(subs, ctx, SUBSCRIPTION_COLUMNS);
6319
+ const active = subs.find(
6320
+ (s) => ["active", "trialing"].includes(String(s.status ?? s.subscriptionStatus ?? ""))
6321
+ );
6322
+ if (!active) {
6323
+ hint(ctx, [
6324
+ "No active subscription yet. To get started:",
6325
+ " gr billing products --type org # pick a plan",
6326
+ " gr billing checkout --product <id> # pay up front"
6327
+ ]);
6328
+ return;
6329
+ }
6330
+ const orgUuid = String(
6331
+ active.organizationUuid ?? active.customerOrganizationUuid ?? "<org-uuid>"
6332
+ );
6333
+ hint(ctx, [
6334
+ "You have an active subscription. Add a workspace:",
6335
+ " gr billing products --type workspace # pick a workspace plan",
6336
+ ` gr workspaces create --org ${orgUuid} --product <id> --name "My Workspace"`
6337
+ ]);
6338
+ }
6339
+ }
6340
+ ]
6341
+ };
6342
+
6389
6343
  // src/domains/billing-portal.ts
6390
6344
  var INVOICE_COLUMNS = [
6391
6345
  { header: "ID", getValue: (i) => String(i.id) },
@@ -6485,119 +6439,6 @@ var billingPortalDomain = {
6485
6439
  commands
6486
6440
  };
6487
6441
 
6488
- // src/lib/concierge-api.ts
6489
- function baseUrl2(ctx) {
6490
- return `${ctx.envConfig.apiUrl}/api/v2/concierge`;
6491
- }
6492
- function authHeaders(ctx) {
6493
- return {
6494
- Authorization: `Bearer ${ctx.accessToken}`,
6495
- "Content-Type": "application/json"
6496
- };
6497
- }
6498
- function toCamelChat(raw) {
6499
- return {
6500
- id: raw.id,
6501
- uuid: raw.uuid,
6502
- chatType: raw.chat_type,
6503
- title: raw.title,
6504
- threadId: raw.thread_id,
6505
- createdAt: raw.created_at,
6506
- updatedAt: raw.updated_at
6507
- };
6508
- }
6509
- function toCamelMessage(raw) {
6510
- return {
6511
- id: raw.id,
6512
- role: raw.role,
6513
- content: raw.content ?? "",
6514
- createdAt: raw.createdAt ?? raw.created_at ?? null,
6515
- toolCalls: raw.toolCalls ?? raw.tool_calls ?? null,
6516
- toolCallId: raw.toolCallId ?? raw.tool_call_id ?? null
6517
- };
6518
- }
6519
- async function listConciergeChats(ctx) {
6520
- const res = await fetch(`${baseUrl2(ctx)}/chats`, { headers: authHeaders(ctx) });
6521
- if (!res.ok) throw new Error(`Failed to list concierge chats: ${res.status}`);
6522
- const data = await res.json();
6523
- return data.map(toCamelChat);
6524
- }
6525
- async function createConciergeChat(ctx, title) {
6526
- const res = await fetch(`${baseUrl2(ctx)}/chats`, {
6527
- method: "POST",
6528
- headers: authHeaders(ctx),
6529
- body: JSON.stringify({ title: title ?? null })
6530
- });
6531
- if (!res.ok) throw new Error(`Failed to create concierge chat: ${res.status}`);
6532
- return toCamelChat(await res.json());
6533
- }
6534
- async function getConciergeChat(ctx, chatId) {
6535
- const res = await fetch(`${baseUrl2(ctx)}/chats/${chatId}`, { headers: authHeaders(ctx) });
6536
- if (!res.ok) throw new Error(`Failed to get concierge chat ${chatId}: ${res.status}`);
6537
- return toCamelChat(await res.json());
6538
- }
6539
- async function originateConciergeCall(ctx, kind = "concierge_setup") {
6540
- const res = await fetch(`${baseUrl2(ctx)}/calls`, {
6541
- method: "POST",
6542
- headers: authHeaders(ctx),
6543
- body: JSON.stringify({ kind })
6544
- });
6545
- if (res.status === 412) {
6546
- throw new Error("Your account phone is not verified. Run `gr account verify-phone` first.");
6547
- }
6548
- if (!res.ok) {
6549
- const detail = await res.text();
6550
- throw new Error(`Failed to start concierge call: ${res.status} ${detail}`);
6551
- }
6552
- return await res.json();
6553
- }
6554
- async function originateConciergeSms(ctx, kind = "concierge_setup") {
6555
- const res = await fetch(`${baseUrl2(ctx)}/sms`, {
6556
- method: "POST",
6557
- headers: authHeaders(ctx),
6558
- body: JSON.stringify({ kind })
6559
- });
6560
- if (res.status === 412) {
6561
- throw new Error("Your account phone is not verified. Run `gr account verify-phone` first.");
6562
- }
6563
- if (!res.ok) {
6564
- const detail = await res.text();
6565
- throw new Error(`Failed to start concierge SMS: ${res.status} ${detail}`);
6566
- }
6567
- return await res.json();
6568
- }
6569
- async function startAccountPhoneVerify(ctx, phone) {
6570
- const res = await fetch(`${ctx.envConfig.apiUrl}/api/v2/account/phone/verify/start`, {
6571
- method: "POST",
6572
- headers: authHeaders(ctx),
6573
- body: JSON.stringify({ phone })
6574
- });
6575
- if (!res.ok) {
6576
- const detail = await res.text();
6577
- throw new Error(`Failed to send verification code: ${res.status} ${detail}`);
6578
- }
6579
- }
6580
- async function confirmAccountPhoneVerify(ctx, code) {
6581
- const res = await fetch(`${ctx.envConfig.apiUrl}/api/v2/account/phone/verify/confirm`, {
6582
- method: "POST",
6583
- headers: authHeaders(ctx),
6584
- body: JSON.stringify({ code })
6585
- });
6586
- if (!res.ok) {
6587
- const detail = await res.text();
6588
- throw new Error(`Failed to confirm verification code: ${res.status} ${detail}`);
6589
- }
6590
- }
6591
- async function getConciergeChatMessages(ctx, chatId) {
6592
- const res = await fetch(`${baseUrl2(ctx)}/chats/${chatId}/messages`, {
6593
- headers: authHeaders(ctx)
6594
- });
6595
- if (!res.ok)
6596
- throw new Error(`Failed to get concierge messages for chat ${chatId}: ${res.status}`);
6597
- const data = await res.json();
6598
- return data.map(toCamelMessage);
6599
- }
6600
-
6601
6442
  // src/domains/concierge.ts
6602
6443
  var CHAT_COLUMNS = [
6603
6444
  COL.id(),
@@ -6642,7 +6483,7 @@ var conciergeDomain = {
6642
6483
 
6643
6484
  // src/domains/custom-toolkits.ts
6644
6485
  var COLUMNS17 = [COL.id(), COL.name(), COL.description(), COL.enabled()];
6645
- var TOOL_COLUMNS2 = [COL.id(), COL.name(), COL.description()];
6486
+ var TOOL_COLUMNS3 = [COL.id(), COL.name(), COL.description()];
6646
6487
  var customToolkitsDomain = {
6647
6488
  name: "custom-toolkits",
6648
6489
  description: "Manage custom toolkits and their tools",
@@ -6700,7 +6541,7 @@ var customToolkitsDomain = {
6700
6541
  toolkitId,
6701
6542
  data
6702
6543
  );
6703
- outputSingle(result2, ctx, TOOL_COLUMNS2);
6544
+ outputSingle(result2, ctx, TOOL_COLUMNS3);
6704
6545
  }
6705
6546
  },
6706
6547
  {
@@ -6722,7 +6563,7 @@ var customToolkitsDomain = {
6722
6563
  toolId,
6723
6564
  data
6724
6565
  );
6725
- outputSingle(result2, ctx, TOOL_COLUMNS2);
6566
+ outputSingle(result2, ctx, TOOL_COLUMNS3);
6726
6567
  }
6727
6568
  },
6728
6569
  {
@@ -7935,6 +7776,50 @@ function parseCallerRole(value) {
7935
7776
  }
7936
7777
  return VALID_CALLER_ROLES.has(value) ? value : void 0;
7937
7778
  }
7779
+ var PERMISSION_USE = 1;
7780
+ function evaluateWorkflowMemberRoleAccess(memberRoleId, memberRoleAccess) {
7781
+ if (memberRoleAccess === void 0) {
7782
+ return { outcome: "skip", detail: "workflow memberRoleAccess not available" };
7783
+ }
7784
+ if (memberRoleAccess.length === 0) {
7785
+ return { outcome: "allow", detail: "no workflow role restrictions configured" };
7786
+ }
7787
+ if (memberRoleId === void 0) {
7788
+ return {
7789
+ outcome: "skip",
7790
+ detail: "caller member role unknown \u2014 cannot evaluate role gate"
7791
+ };
7792
+ }
7793
+ const hasUse = memberRoleAccess.some(
7794
+ (entry) => entry.memberRoleId === memberRoleId && (entry.accessMode & PERMISSION_USE) === PERMISSION_USE
7795
+ );
7796
+ if (hasUse) {
7797
+ return { outcome: "allow", detail: `memberRoleId=${memberRoleId} has USE on workflow` };
7798
+ }
7799
+ return {
7800
+ outcome: "deny",
7801
+ detail: `memberRoleId=${memberRoleId} lacks USE in workflow memberRoleAccess`
7802
+ };
7803
+ }
7804
+ function parseWorkflowMemberRoleAccess(workflow) {
7805
+ const raw = workflow.memberRoleAccess;
7806
+ if (!Array.isArray(raw)) {
7807
+ return [];
7808
+ }
7809
+ const entries = [];
7810
+ for (const item of raw) {
7811
+ if (!item || typeof item !== "object") {
7812
+ continue;
7813
+ }
7814
+ const row = item;
7815
+ const memberRoleId = typeof row.memberRoleId === "number" ? row.memberRoleId : Number(row.memberRoleId);
7816
+ const accessMode = typeof row.accessMode === "number" ? row.accessMode : Number(row.accessMode);
7817
+ if (Number.isFinite(memberRoleId) && Number.isFinite(accessMode)) {
7818
+ entries.push({ memberRoleId, accessMode });
7819
+ }
7820
+ }
7821
+ return entries;
7822
+ }
7938
7823
  function inferCallerRole(phone, opts) {
7939
7824
  if (opts.asRole) {
7940
7825
  return opts.asRole;
@@ -8061,6 +7946,32 @@ function simulateInboundRouting(phone, opts = {}) {
8061
7946
  return finish("no_inbound_workflow", false, true, steps, recommendations, callerRole);
8062
7947
  }
8063
7948
  steps.push({ step: "inbound_workflow", outcome: "ok", detail: `workflowId=${workflowId}` });
7949
+ const callerMemberRoleId = callerRole === "member" ? opts.matchedMemberRoleId : callerRole === "anonymous" ? phone.anonymousMemberRoleId ?? void 0 : void 0;
7950
+ const roleAccess = evaluateWorkflowMemberRoleAccess(
7951
+ callerMemberRoleId,
7952
+ opts.workflowMemberRoleAccess
7953
+ );
7954
+ if (roleAccess.outcome === "deny") {
7955
+ steps.push({
7956
+ step: "workflow_member_role_access",
7957
+ outcome: "reject",
7958
+ detail: roleAccess.detail
7959
+ });
7960
+ recommendations.push(
7961
+ "Grant the caller's member role USE access on the inbound workflow (workflow memberRoleAccess or `gr member-roles update` workflowAccess)"
7962
+ );
7963
+ return finish("workflow_member_role_denied", false, true, steps, recommendations, callerRole);
7964
+ }
7965
+ steps.push({
7966
+ step: "workflow_member_role_access",
7967
+ outcome: roleAccess.outcome === "skip" ? "skipped" : "ok",
7968
+ detail: roleAccess.detail
7969
+ });
7970
+ if (roleAccess.outcome === "skip" && callerMemberRoleId === void 0 && opts.workflowMemberRoleAccess !== void 0 && opts.workflowMemberRoleAccess.length > 0) {
7971
+ recommendations.push(
7972
+ "Workflow has member role restrictions but caller memberRoleId was not resolved \u2014 re-run with --as +E.164 (caller phone) so the role gate can be evaluated against real data"
7973
+ );
7974
+ }
8064
7975
  steps.push({ step: "chat_create", outcome: "create_phone_voice_chat" });
8065
7976
  steps.push({ step: "assistant", outcome: "connect_media_stream" });
8066
7977
  return finish("ai_voice_chat", true, true, steps, recommendations, callerRole);
@@ -8077,6 +7988,7 @@ function finish(code, createsChat, createsPhoneCall, steps, recommendations, cal
8077
7988
  voicemail_record: "Record voicemail",
8078
7989
  play_message_hangup: "Play off-hours message, hang up",
8079
7990
  no_inbound_workflow: "Play unavailable message (no inbound workflow)",
7991
+ workflow_member_role_denied: "Reject \u2014 caller member role lacks workflow USE (memberRoleAccess)",
8080
7992
  ai_voice_chat: "Create chat + connect AI voice"
8081
7993
  };
8082
7994
  return {
@@ -8128,9 +8040,22 @@ function formatPhoneNumberBehaviorMatrix(phone, outboundUsers, warnings = collec
8128
8040
  lines.push(`UUID: ${phone.uuid} | Mapped: ${phone.isMapped ? "yes" : "no"}`);
8129
8041
  lines.push("");
8130
8042
  lines.push("## Channels");
8131
- lines.push(` Voice: ${phone.enableVoice ? "on" : "off"} SMS: ${phone.enableSms ? "on" : "off"} WhatsApp: ${phone.enableWhatsApp ? "on" : "off"}`);
8043
+ lines.push(
8044
+ ` Voice: ${phone.enableVoice ? "on" : "off"} SMS: ${phone.enableSms ? "on" : "off"} WhatsApp: ${phone.enableWhatsApp ? "on" : "off"}`
8045
+ );
8132
8046
  lines.push(` Assistant: ${phone.assistantEnabled ? "on" : "off"}`);
8133
8047
  lines.push("");
8048
+ if (phone.enableVoice) {
8049
+ lines.push("## Channel defaults (auto-injected on voice)");
8050
+ lines.push(
8051
+ " Voice channels auto-add the hang_up_call tool (system:voice_channel);"
8052
+ );
8053
+ lines.push(
8054
+ " it is not listed by `gr agents abilities list`. See `gr abilities get"
8055
+ );
8056
+ lines.push(" --name communication:phone_call_tools` for the voice ability spec.");
8057
+ lines.push("");
8058
+ }
8134
8059
  lines.push("## Inbound route");
8135
8060
  lines.push(
8136
8061
  ` defaultWorkflowId (inbound): ${phone.defaultWorkflowId ?? "null"} restartDelay: ${phone.restartDelay ?? "null"}s`
@@ -8211,9 +8136,7 @@ async function findOutboundPhoneUsers(client, workspace, phone) {
8211
8136
  }
8212
8137
  page += 1;
8213
8138
  if (page > 50) {
8214
- process.stderr.write(
8215
- "Warning: event-rules lookup truncated at 5000 results (50 pages)\n"
8216
- );
8139
+ process.stderr.write("Warning: event-rules lookup truncated at 5000 results (50 pages)\n");
8217
8140
  break;
8218
8141
  }
8219
8142
  }
@@ -10488,7 +10411,9 @@ function normalizeCallerE164(phone) {
10488
10411
  var MEMBER_SEARCH_PAGE_SIZE = 100;
10489
10412
  async function resolveMemberByCallerPhone(client, workspace, callerE164) {
10490
10413
  const normalized = normalizeCallerE164(callerE164);
10491
- const candidates = [normalized, callerE164.trim()].filter(
10414
+ const digits = callerE164.replace(/\D/g, "");
10415
+ const nsn = digits.length === 11 && digits.startsWith("1") ? digits.slice(1) : digits;
10416
+ const candidates = [normalized, callerE164.trim(), digits, nsn].filter(
10492
10417
  (v, i, arr) => Boolean(v) && arr.indexOf(v) === i
10493
10418
  );
10494
10419
  const matchInRows = (rows) => {
@@ -10533,6 +10458,25 @@ async function resolveMemberByCallerPhone(client, workspace, callerE164) {
10533
10458
  return void 0;
10534
10459
  }
10535
10460
 
10461
+ // src/lib/phone-numbers/workflow-role-access.ts
10462
+ async function loadInboundWorkflowMemberRoleAccess(client, workspace, defaultWorkflowId) {
10463
+ const workflowId = typeof defaultWorkflowId === "number" ? defaultWorkflowId : defaultWorkflowId === null || defaultWorkflowId === void 0 ? void 0 : Number(defaultWorkflowId);
10464
+ if (workflowId === void 0 || !Number.isFinite(workflowId)) {
10465
+ return void 0;
10466
+ }
10467
+ try {
10468
+ const workflow = await client.getWorkflow(workspace, workflowId);
10469
+ return parseWorkflowMemberRoleAccess(workflow);
10470
+ } catch (err) {
10471
+ const reason = err instanceof Error ? err.message : String(err);
10472
+ process.stderr.write(
10473
+ `[diagnose] Could not fetch workflow ${workflowId} memberRoleAccess \u2014 role gate will be skipped (${reason})
10474
+ `
10475
+ );
10476
+ return void 0;
10477
+ }
10478
+ }
10479
+
10536
10480
  // src/domains/phone-numbers.ts
10537
10481
  function asPhoneRecord(value) {
10538
10482
  return value;
@@ -10591,9 +10535,8 @@ var phoneNumbersDomain = {
10591
10535
  const client = requireAuth(ctx).apiClient;
10592
10536
  const id = requireNumber(args, "id");
10593
10537
  const phone = asPhoneRecord(await client.getPhoneNumber(workspace, id));
10594
- if (args.raw) {
10595
- process.stdout.write(`${JSON.stringify(phone, null, 2)}
10596
- `);
10538
+ if (args.raw || ctx.outputFormat !== "table") {
10539
+ outputSingle(phone, ctx);
10597
10540
  return;
10598
10541
  }
10599
10542
  const outboundUsers = await findOutboundPhoneUsers(client, workspace, phone);
@@ -10767,21 +10710,34 @@ Setup complete: id=${phone.id} uuid=${phone.uuid}
10767
10710
  const asRole = parseCallerRole(args["as-role"]);
10768
10711
  let hasMemberMatch = asRole === "member";
10769
10712
  let matchedMemberId;
10713
+ let matchedMemberRoleId;
10770
10714
  let matchedMemberPhoneType;
10771
10715
  if (!asRole && typeof args.as === "string") {
10772
10716
  const match = await resolveMemberByCallerPhone(client, workspace, args.as);
10773
10717
  if (match) {
10774
10718
  hasMemberMatch = true;
10775
10719
  matchedMemberId = match.memberId;
10720
+ matchedMemberRoleId = match.memberRoleId;
10776
10721
  matchedMemberPhoneType = match.phoneType;
10777
10722
  }
10778
10723
  }
10724
+ const workflowMemberRoleAccess = await loadInboundWorkflowMemberRoleAccess(
10725
+ client,
10726
+ workspace,
10727
+ phone.defaultWorkflowId
10728
+ );
10779
10729
  const sim = simulateInboundRouting(phone, {
10780
10730
  asRole,
10781
10731
  hasMemberMatch,
10782
10732
  matchedMemberId,
10783
- matchedMemberPhoneType
10733
+ matchedMemberRoleId,
10734
+ matchedMemberPhoneType,
10735
+ workflowMemberRoleAccess
10784
10736
  });
10737
+ if (ctx.outputFormat !== "table") {
10738
+ outputSingle({ phoneNumberId: id, simulation: sim }, ctx);
10739
+ return;
10740
+ }
10785
10741
  process.stdout.write(`${formatRoutingSimulation(sim, phone)}
10786
10742
  `);
10787
10743
  }
@@ -10803,10 +10759,28 @@ Setup complete: id=${phone.id} uuid=${phone.uuid}
10803
10759
  const id = requireNumber(args, "id");
10804
10760
  const role = requireCallerRole(args, "as-role");
10805
10761
  const phone = asPhoneRecord(await client.getPhoneNumber(workspace, id));
10762
+ const workflowMemberRoleAccess = await loadInboundWorkflowMemberRoleAccess(
10763
+ client,
10764
+ workspace,
10765
+ phone.defaultWorkflowId
10766
+ );
10806
10767
  const sim = simulateInboundRouting(phone, {
10807
10768
  asRole: role,
10808
- hasMemberMatch: role === "member"
10769
+ hasMemberMatch: role === "member",
10770
+ workflowMemberRoleAccess
10809
10771
  });
10772
+ if (ctx.outputFormat !== "table") {
10773
+ outputSingle(
10774
+ {
10775
+ phoneNumberId: id,
10776
+ callerRole: role,
10777
+ finalBehavior: sim.finalBehavior,
10778
+ simulation: sim
10779
+ },
10780
+ ctx
10781
+ );
10782
+ return;
10783
+ }
10810
10784
  process.stdout.write(
10811
10785
  `If a caller in role "${role}" dials this line right now:
10812
10786
  \u2192 ${sim.finalBehavior}
@@ -10832,17 +10806,26 @@ ${formatRoutingSimulation(sim, phone)}
10832
10806
  const asRole = parseCallerRole(args["as-role"]);
10833
10807
  let hasMemberMatch = asRole === "member";
10834
10808
  let matchedMemberId;
10809
+ let matchedMemberRoleId;
10835
10810
  if (!asRole && typeof args.as === "string") {
10836
10811
  const match = await resolveMemberByCallerPhone(client, workspace, args.as);
10837
10812
  if (match) {
10838
10813
  hasMemberMatch = true;
10839
10814
  matchedMemberId = match.memberId;
10815
+ matchedMemberRoleId = match.memberRoleId;
10840
10816
  }
10841
10817
  }
10818
+ const workflowMemberRoleAccess = await loadInboundWorkflowMemberRoleAccess(
10819
+ client,
10820
+ workspace,
10821
+ phone.defaultWorkflowId
10822
+ );
10842
10823
  const sim = simulateInboundRouting(phone, {
10843
10824
  asRole,
10844
10825
  hasMemberMatch,
10845
- matchedMemberId
10826
+ matchedMemberId,
10827
+ matchedMemberRoleId,
10828
+ workflowMemberRoleAccess
10846
10829
  });
10847
10830
  process.stdout.write(
10848
10831
  `${JSON.stringify({ phoneNumberId: id, simulation: sim }, null, 2)}
@@ -10886,11 +10869,15 @@ ${formatRoutingSimulation(sim, phone)}
10886
10869
  { inbound: true }
10887
10870
  );
10888
10871
  const inbound = result2.items ?? [];
10889
- process.stdout.write(`Inbound calls (most recent page): ${inbound.length} shown
10872
+ if (ctx.outputFormat === "table") {
10873
+ process.stdout.write(`Inbound calls (most recent page): ${inbound.length} shown
10890
10874
  `);
10891
- process.stdout.write(
10892
- "Tip: gr phone-numbers diagnose --id <id> --as +1\u2026 simulates routing without a live call.\n"
10893
- );
10875
+ process.stdout.write(
10876
+ "Tip: gr phone-numbers diagnose --id <id> --as +1\u2026 simulates routing without a live call.\n"
10877
+ );
10878
+ } else {
10879
+ stderr(`Inbound calls (most recent page): ${inbound.length} shown`);
10880
+ }
10894
10881
  outputData(inbound, ctx);
10895
10882
  }
10896
10883
  },
@@ -11264,6 +11251,115 @@ var reportsDomain = {
11264
11251
  ]
11265
11252
  };
11266
11253
 
11254
+ // src/lib/routines/field-help.ts
11255
+ var ROUTINE_FIELD_REFERENCE = `
11256
+ Field reference for --data JSON (create/update):
11257
+
11258
+ Core
11259
+ name (string) Display name
11260
+ workflowId (number) Workflow run when the routine fires
11261
+ targetMemberId (number) Member who receives the scheduled outreach
11262
+
11263
+ Schedule
11264
+ scheduleEnabled (bool) Master on/off for the cron trigger
11265
+ scheduleMode (string) "CRON" (only mode today)
11266
+ cronString (string) Standard cron, e.g. "0 7 * * *"
11267
+ timezone (string, optional) IANA name, e.g. "America/Los_Angeles"
11268
+ Defaults to workspace timezone, else UTC
11269
+
11270
+ Visibility / lifecycle
11271
+ archived (bool) Archive flag on update
11272
+ event rules Use gr events rules create \u2014 not inline here
11273
+ `;
11274
+
11275
+ // src/domains/routines.ts
11276
+ var ROUTINE_COLUMNS = [
11277
+ COL.id(),
11278
+ COL.name(),
11279
+ { header: "MODE", getValue: (item) => item.scheduleMode ?? "" },
11280
+ { header: "CRON", getValue: (item) => item.cronString ?? "" },
11281
+ { header: "TZ", getValue: (item) => item.timezone ?? "UTC" },
11282
+ { header: "ENABLED", getValue: (item) => String(item.scheduleEnabled ?? "") },
11283
+ { header: "NEXT RUN", getValue: (item) => item.nextRun ?? "" }
11284
+ ];
11285
+ var routinesDomain = {
11286
+ name: "routines",
11287
+ description: "Schedule recurring outbound (e.g. a daily SMS/voice check-in via cron)",
11288
+ commands: [
11289
+ ...crudCommands({
11290
+ prefix: ["routines"],
11291
+ entityName: "routine",
11292
+ listMethod: "getRoutines",
11293
+ getMethod: "getRoutine",
11294
+ createMethod: "createRoutine",
11295
+ updateMethod: "updateRoutine",
11296
+ deleteMethod: "deleteRoutine",
11297
+ paginationSupport: "full",
11298
+ columns: ROUTINE_COLUMNS,
11299
+ createHelp: `Create a routine
11300
+
11301
+ ${ROUTINE_FIELD_REFERENCE}`,
11302
+ updateHelp: `Update a routine
11303
+
11304
+ ${ROUTINE_FIELD_REFERENCE}`,
11305
+ createExamples: [
11306
+ "Daily 7am check-in in the member\u2019s timezone (cron is interpreted in `timezone`):",
11307
+ `gr routines create -w $WID --allow-write --data '{"name":"AM check-in",`,
11308
+ ' "scheduleEnabled":true,"scheduleMode":"CRON","cronString":"0 7 * * *",',
11309
+ ` "timezone":"America/Los_Angeles","targetMemberId":1,"workflowId":2}'`,
11310
+ " (timezone is an IANA name; omit it to default to the workspace timezone, else UTC.",
11311
+ ' Without it, "0 7 * * *" fires at 07:00 UTC, not 7am local.)',
11312
+ "",
11313
+ "To actually send via SMS/voice, pair the routine with an event rule:",
11314
+ ' gr events rules create \u2014 objectType "routine", eventType "routine:triggered",',
11315
+ ' actionType "chat:create". See `gr events --help`.'
11316
+ ]
11317
+ }),
11318
+ {
11319
+ path: ["routines", "run"],
11320
+ description: "Run a routine now",
11321
+ mutates: true,
11322
+ requiresWorkspace: true,
11323
+ args: [{ name: "id", description: "Routine ID", required: true }],
11324
+ handler: async ({ ctx, workspace, args, data }) => {
11325
+ const result2 = await requireAuth(ctx).apiClient.runRoutine(
11326
+ workspace,
11327
+ requireNumber(args, "id"),
11328
+ data ?? {}
11329
+ );
11330
+ outputSingle(result2, ctx);
11331
+ }
11332
+ },
11333
+ {
11334
+ path: ["routines", "runs", "list"],
11335
+ description: "List runs for a routine",
11336
+ requiresWorkspace: true,
11337
+ args: [
11338
+ {
11339
+ name: "id",
11340
+ description: "Routine ID (aligns with routines get/run/update/delete)"
11341
+ },
11342
+ { name: "status", description: "Filter by run status" },
11343
+ { name: "page", description: "Page number (1-indexed)" },
11344
+ { name: "page-size", description: "Page size (max 100)" }
11345
+ ],
11346
+ handler: async ({ ctx, workspace, args }) => {
11347
+ const routineId = args.id ? requireNumber(args, "id") : void 0;
11348
+ const page = args.page ? requireNumber(args, "page") : void 0;
11349
+ const pageSize = args["page-size"] ? requireNumber(args, "page-size") : void 0;
11350
+ const status = args.status ?? void 0;
11351
+ const result2 = await requireAuth(ctx).apiClient.getRoutineRuns(workspace, {
11352
+ ...routineId !== void 0 ? { routineId } : {},
11353
+ ...page !== void 0 ? { page } : {},
11354
+ ...pageSize !== void 0 ? { pageSize } : {},
11355
+ ...status ? { status } : {}
11356
+ });
11357
+ outputData(result2, ctx);
11358
+ }
11359
+ }
11360
+ ]
11361
+ };
11362
+
11267
11363
  // src/domains/subscriptions.ts
11268
11364
  var COLUMNS27 = [
11269
11365
  COL.id(),
@@ -13126,11 +13222,13 @@ function registerPublicDomains(registry) {
13126
13222
  registry.register(sitesDomain);
13127
13223
  registry.register(filesDomain);
13128
13224
  registry.register(eventsDomain);
13225
+ registry.register(routinesDomain);
13129
13226
  registry.register(experimentsDomain);
13130
13227
  registry.register(calendarsDomain);
13131
13228
  registry.register(qualificationsDomain);
13132
13229
  registry.register(personasDomain);
13133
13230
  registry.register(journeysDomain);
13231
+ registry.register(abilitiesDomain);
13134
13232
  registry.register(phoneNumbersDomain);
13135
13233
  registry.register(inboxesDomain);
13136
13234
  registry.register(operatorGroupsDomain);
@@ -13143,6 +13241,7 @@ function registerPublicDomains(registry) {
13143
13241
  registry.register(mcpServersDomain);
13144
13242
  registry.register(apiKeysDomain);
13145
13243
  registry.register(subscriptionsDomain);
13244
+ registry.register(billingDomain);
13146
13245
  registry.register(supportDomain);
13147
13246
  registry.register(reportsDomain);
13148
13247
  registry.register(aiModelsDomain);
@@ -13169,7 +13268,7 @@ function registerPublicDomains(registry) {
13169
13268
  }
13170
13269
 
13171
13270
  // src/commands/auth-browser.ts
13172
- import { execFile as execFile2 } from "child_process";
13271
+ import { execFile } from "child_process";
13173
13272
  async function authBrowserCommand(ctx, args) {
13174
13273
  const noOpen = Boolean(args["no-open"]);
13175
13274
  const account = await ctx.apiClient.getUserAccount();
@@ -13205,7 +13304,7 @@ function openUrl(url) {
13205
13304
  command = "xdg-open";
13206
13305
  args = [url];
13207
13306
  }
13208
- execFile2(command, args, (err) => {
13307
+ execFile(command, args, (err) => {
13209
13308
  if (err) {
13210
13309
  process.stderr.write("Warning: Could not open browser automatically\n");
13211
13310
  }
@@ -13418,7 +13517,7 @@ async function resolveDirectoryTrust(opts) {
13418
13517
  return "trusted";
13419
13518
  }
13420
13519
  async function requireDirectoryTrust(opts) {
13421
- const { CLIError: CLIError2 } = await import("./errors-SMY7LZJB.js");
13520
+ const { CLIError: CLIError2 } = await import("./errors-VK4QO3QZ.js");
13422
13521
  const dir = path3.resolve(opts.trustDir ?? opts.cwd ?? process.cwd());
13423
13522
  const status = await resolveDirectoryTrust(opts);
13424
13523
  if (status === "trusted") return;
@@ -13541,27 +13640,42 @@ async function runConciergeAguiLoop(client, initialMessages, onEvent, opts) {
13541
13640
  if (!hitlPayload) {
13542
13641
  break;
13543
13642
  }
13544
- const next = await handleCliHitlInterrupt(hitlPayload, {
13545
- allowWrite: opts.allowWrite,
13546
- autoApprove: opts.autoApprove,
13547
- trustCwd: opts.trustCwd,
13548
- trustDir: opts.trustDir,
13549
- nonInteractive: opts.nonInteractive
13550
- });
13643
+ opts.onHitlPromptStart?.();
13644
+ let next;
13645
+ try {
13646
+ next = await handleCliHitlInterrupt(hitlPayload, {
13647
+ allowWrite: opts.allowWrite,
13648
+ autoApprove: opts.autoApprove,
13649
+ trustCwd: opts.trustCwd,
13650
+ trustDir: opts.trustDir,
13651
+ nonInteractive: opts.nonInteractive
13652
+ });
13653
+ } finally {
13654
+ opts.onHitlPromptEnd?.();
13655
+ }
13551
13656
  if (!next) {
13552
13657
  process.stderr.write(
13553
13658
  " [HITL] Unsupported interrupt in CLI \u2014 open app.gravityrail.com/setup to continue.\n"
13554
13659
  );
13555
- break;
13660
+ return {
13661
+ streamText,
13662
+ hasToolCalls,
13663
+ unsupportedHitl: normalizeUnsupportedHitl(hitlPayload)
13664
+ };
13556
13665
  }
13557
13666
  resume = next;
13558
13667
  streamText = "";
13559
13668
  }
13560
13669
  return { streamText, hasToolCalls };
13561
13670
  }
13562
-
13563
- // src/lib/chat-session.ts
13564
- import * as readline4 from "readline";
13671
+ function normalizeUnsupportedHitl(raw) {
13672
+ const payload = raw && typeof raw === "object" ? raw : {};
13673
+ return {
13674
+ type: typeof payload.type === "string" ? payload.type : void 0,
13675
+ interruptId: typeof payload.interrupt_id === "string" ? payload.interrupt_id : void 0,
13676
+ payload
13677
+ };
13678
+ }
13565
13679
 
13566
13680
  // src/lib/realtime-client.ts
13567
13681
  import { EventEmitter } from "events";
@@ -13822,6 +13936,19 @@ function isToolCall(item) {
13822
13936
  function isToolResult(item) {
13823
13937
  return "kind" in item && item.kind === "tool_result";
13824
13938
  }
13939
+ function toolResultLooksFailed(content) {
13940
+ const trimmed = content.trim();
13941
+ if (!trimmed) return false;
13942
+ if (/^error[:\s]/i.test(trimmed)) return true;
13943
+ try {
13944
+ const parsed = JSON.parse(trimmed);
13945
+ if (parsed.ok === false) return true;
13946
+ if (typeof parsed.error === "string" && parsed.error.length > 0) return true;
13947
+ if (typeof parsed.status === "string" && parsed.status.toLowerCase() === "error") return true;
13948
+ } catch {
13949
+ }
13950
+ return false;
13951
+ }
13825
13952
  function getTruncationLabel(reason, truncatedAtMs) {
13826
13953
  const timeStr = truncatedAtMs ? ` at ${(truncatedAtMs / 1e3).toFixed(1)}s` : "";
13827
13954
  switch (reason) {
@@ -13855,6 +13982,8 @@ var CLEAR_EOL = `${CSI}K`;
13855
13982
  var RESET = `${CSI}0m`;
13856
13983
  var MOUSE_ON = `${CSI}?1000h${CSI}?1006h`;
13857
13984
  var MOUSE_OFF = `${CSI}?1000l${CSI}?1006l`;
13985
+ var BRACKETED_PASTE_ON = `${CSI}?2004h`;
13986
+ var BRACKETED_PASTE_OFF = `${CSI}?2004l`;
13858
13987
  var BLUE = `${CSI}34m`;
13859
13988
  var GREEN = `${CSI}32m`;
13860
13989
  var GRAY = `${CSI}90m`;
@@ -13863,6 +13992,11 @@ var ORANGE = `${CSI}38;5;208m`;
13863
13992
  var LIGHT_BLUE = `${CSI}38;5;117m`;
13864
13993
  var DIM = `${CSI}2m`;
13865
13994
  var DIM_ITALIC_GRAY = `${CSI}2;3;90m`;
13995
+ var RED = `${CSI}31m`;
13996
+ var CYAN = `${CSI}36m`;
13997
+ var MAX_INPUT_ROWS = 6;
13998
+ var INPUT_LABEL = "You: ";
13999
+ var INPUT_LABEL_WIDTH = 5;
13866
14000
  function moveTo(row, col) {
13867
14001
  return `${CSI}${row};${col}H`;
13868
14002
  }
@@ -13887,6 +14021,11 @@ var ChatTUI = class {
13887
14021
  // Escape sequence accumulator for raw stdin parsing
13888
14022
  escBuf = "";
13889
14023
  escTimer = null;
14024
+ // Run / inference status (title bar)
14025
+ runState = "idle";
14026
+ activeToolName = "";
14027
+ pausedForPrompt = false;
14028
+ turnInProgress = false;
13890
14029
  constructor(options) {
13891
14030
  this.options = options;
13892
14031
  this.debugPanelVisible = !!options.debug;
@@ -13898,8 +14037,8 @@ var ChatTUI = class {
13898
14037
  // ─── Lifecycle ──────────────────────────────────────────────────────
13899
14038
  start() {
13900
14039
  this.updateDimensions();
13901
- process.stdout.write(ALT_SCREEN_ON + MOUSE_ON);
13902
- process.stdout.write(`${CSI}1;${Math.max(1, this.rows - 2)}r`);
14040
+ process.stdout.write(ALT_SCREEN_ON + MOUSE_ON + BRACKETED_PASTE_ON);
14041
+ this.applyScrollRegion();
13903
14042
  process.stdin.setEncoding("utf8");
13904
14043
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
13905
14044
  process.stdin.on("data", this.onData);
@@ -13942,6 +14081,7 @@ var ChatTUI = class {
13942
14081
  streamStart() {
13943
14082
  this.isStreaming = true;
13944
14083
  this.streamContent = "";
14084
+ this.runState = "streaming";
13945
14085
  this.scrollOffset = 0;
13946
14086
  this.dirty();
13947
14087
  }
@@ -13957,8 +14097,42 @@ var ChatTUI = class {
13957
14097
  this.streamContent = "";
13958
14098
  this.dirty();
13959
14099
  }
14100
+ runStarted() {
14101
+ this.turnInProgress = true;
14102
+ this.runState = "thinking";
14103
+ this.activeToolName = "";
14104
+ this.dirty();
14105
+ }
14106
+ runFinished() {
14107
+ this.turnInProgress = false;
14108
+ this.runState = "idle";
14109
+ this.activeToolName = "";
14110
+ this.dirty();
14111
+ }
14112
+ /** Temporarily leave alt-screen for an external prompt (HITL). */
14113
+ pauseForPrompt() {
14114
+ if (!this.started || this.pausedForPrompt) return;
14115
+ this.pausedForPrompt = true;
14116
+ process.stdin.removeListener("data", this.onData);
14117
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
14118
+ process.stdout.write(
14119
+ `${MOUSE_OFF}${BRACKETED_PASTE_OFF}${CSI}r${ALT_SCREEN_OFF}${SHOW_CURSOR}`
14120
+ );
14121
+ }
14122
+ /** Re-enter alt-screen after an external prompt. */
14123
+ resumeAfterPrompt() {
14124
+ if (!this.started || !this.pausedForPrompt) return;
14125
+ this.pausedForPrompt = false;
14126
+ process.stdout.write(ALT_SCREEN_ON + MOUSE_ON + BRACKETED_PASTE_ON);
14127
+ this.applyScrollRegion();
14128
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
14129
+ process.stdin.on("data", this.onData);
14130
+ this.render();
14131
+ }
13960
14132
  toolCallStart(id, name) {
13961
14133
  this.items.push({ kind: "tool_call", id, name, args: "", done: false });
14134
+ this.runState = "tool_running";
14135
+ this.activeToolName = name;
13962
14136
  this.dirty();
13963
14137
  }
13964
14138
  toolCallArgs(id, delta) {
@@ -14011,7 +14185,12 @@ var ChatTUI = class {
14011
14185
  this.dirty();
14012
14186
  }
14013
14187
  toolCallResult(toolCallId, toolName, content) {
14014
- this.items.push({ kind: "tool_result", toolCallId, toolName, content });
14188
+ const failed = toolResultLooksFailed(content);
14189
+ this.items.push({ kind: "tool_result", toolCallId, toolName, content, failed });
14190
+ if (!this.isStreaming) {
14191
+ this.runState = "thinking";
14192
+ this.activeToolName = "";
14193
+ }
14015
14194
  this.dirty();
14016
14195
  }
14017
14196
  /** Find the tool name for a given tool call ID */
@@ -14064,7 +14243,9 @@ var ChatTUI = class {
14064
14243
  this.scheduleRender();
14065
14244
  }
14066
14245
  restoreTerminal = () => {
14067
- process.stdout.write(`${MOUSE_OFF}${CSI}r${ALT_SCREEN_OFF}${SHOW_CURSOR}`);
14246
+ process.stdout.write(
14247
+ `${MOUSE_OFF}${BRACKETED_PASTE_OFF}${CSI}r${ALT_SCREEN_OFF}${SHOW_CURSOR}`
14248
+ );
14068
14249
  };
14069
14250
  updateDimensions() {
14070
14251
  this.rows = process.stdout.rows || 24;
@@ -14080,11 +14261,32 @@ var ChatTUI = class {
14080
14261
  if (!this.debugVisible) return 0;
14081
14262
  return this.cols - this.chatWidth() - 1;
14082
14263
  }
14264
+ titleRows() {
14265
+ return this.options.title || this.options.subtitle ? 1 : 0;
14266
+ }
14267
+ inputRows() {
14268
+ const lines = this.inputBuffer.split("\n").length;
14269
+ return Math.min(MAX_INPUT_ROWS, Math.max(1, lines));
14270
+ }
14271
+ footerRows() {
14272
+ return 1 + this.inputRows();
14273
+ }
14274
+ messageAreaTop() {
14275
+ return 1 + this.titleRows();
14276
+ }
14277
+ messageAreaBottom() {
14278
+ return this.rows - this.footerRows();
14279
+ }
14280
+ applyScrollRegion() {
14281
+ const top = this.messageAreaTop();
14282
+ const bottom = Math.max(top, this.messageAreaBottom());
14283
+ process.stdout.write(`${CSI}${top};${bottom}r`);
14284
+ }
14083
14285
  scrollHeight() {
14084
- return Math.max(1, this.rows - 2);
14286
+ return Math.max(1, this.messageAreaBottom() - this.messageAreaTop() + 1);
14085
14287
  }
14086
14288
  pageSize() {
14087
- return Math.max(1, this.rows - 4);
14289
+ return Math.max(1, this.scrollHeight() - 2);
14088
14290
  }
14089
14291
  // ─── Scroll ───────────────────────────────────────────────────────
14090
14292
  scrollUp(lines) {
@@ -14150,6 +14352,11 @@ var ChatTUI = class {
14150
14352
  }
14151
14353
  /** Handle a parsed CSI sequence: params is everything between \x1B[ and the final char */
14152
14354
  handleCSI(params, code) {
14355
+ if (params === "201~") {
14356
+ this.scrollToBottom();
14357
+ this.dirty();
14358
+ return;
14359
+ }
14153
14360
  if (params.startsWith("<") && (code === "M" || code === "m")) {
14154
14361
  if (code === "m") return;
14155
14362
  const parts = params.slice(1).split(";");
@@ -14231,22 +14438,31 @@ var ChatTUI = class {
14231
14438
  this.handleEnter();
14232
14439
  return;
14233
14440
  }
14441
+ if (code === 10) {
14442
+ this.inputBuffer += "\n";
14443
+ this.dirty();
14444
+ return;
14445
+ }
14234
14446
  if (code === 127 || code === 8) {
14235
14447
  if (this.inputBuffer.length > 0) {
14236
14448
  this.inputBuffer = this.inputBuffer.slice(0, -1);
14237
- this.renderInputLine();
14449
+ this.dirty();
14238
14450
  }
14239
14451
  return;
14240
14452
  }
14241
14453
  if (code < 32) return;
14242
14454
  this.inputBuffer += ch;
14243
- this.renderInputLine();
14455
+ this.dirty();
14244
14456
  }
14245
14457
  handleEnter() {
14458
+ if (this.turnInProgress) {
14459
+ this.dirty();
14460
+ return;
14461
+ }
14246
14462
  const text = this.inputBuffer.trim();
14247
14463
  this.inputBuffer = "";
14248
14464
  if (!text) {
14249
- this.renderInputLine();
14465
+ this.dirty();
14250
14466
  return;
14251
14467
  }
14252
14468
  if (text.toLowerCase() === "exit" || text.toLowerCase() === "quit") {
@@ -14263,7 +14479,7 @@ var ChatTUI = class {
14263
14479
  // ─── Events ───────────────────────────────────────────────────────
14264
14480
  onResize = () => {
14265
14481
  this.updateDimensions();
14266
- process.stdout.write(`${CSI}1;${Math.max(1, this.rows - 2)}r`);
14482
+ this.applyScrollRegion();
14267
14483
  this.displayLinesCache = null;
14268
14484
  this.debugLinesCache = null;
14269
14485
  this.render();
@@ -14305,19 +14521,24 @@ var ChatTUI = class {
14305
14521
  visible = displayLines.slice(start, end);
14306
14522
  }
14307
14523
  const buf = [HIDE_CURSOR];
14524
+ const msgTop = this.messageAreaTop();
14525
+ if (this.titleRows()) {
14526
+ buf.push(moveTo(1, 1), this.buildTitleBar(), CLEAR_EOL);
14527
+ }
14308
14528
  if (this.debugVisible) {
14309
- this.renderSplitScreen(buf, visible, sh);
14529
+ this.renderSplitScreen(buf, visible, sh, msgTop);
14310
14530
  } else {
14311
14531
  for (let i = 0; i < sh; i++) {
14312
- buf.push(moveTo(i + 1, 1), visible[i] || "", CLEAR_EOL);
14532
+ buf.push(moveTo(msgTop + i, 1), visible[i] || "", CLEAR_EOL);
14313
14533
  }
14314
14534
  }
14315
- buf.push(moveTo(this.rows - 1, 1), this.buildSeparator(), CLEAR_EOL);
14316
- buf.push(moveTo(this.rows, 1), `${BLUE}You:${RESET} ${this.inputBuffer}${CLEAR_EOL}`);
14535
+ const sepRow = this.rows - this.inputRows();
14536
+ buf.push(moveTo(sepRow, 1), this.buildSeparator(), CLEAR_EOL);
14537
+ this.renderInputArea(buf, sepRow + 1);
14317
14538
  buf.push(SHOW_CURSOR);
14318
14539
  process.stdout.write(buf.join(""));
14319
14540
  }
14320
- renderSplitScreen(buf, chatVisible, sh) {
14541
+ renderSplitScreen(buf, chatVisible, sh, msgTop = 1) {
14321
14542
  const cw = this.chatWidth();
14322
14543
  const dw = this.debugWidth();
14323
14544
  const divCol = cw + 1;
@@ -14333,21 +14554,70 @@ var ChatTUI = class {
14333
14554
  const chatLine = truncateAnsi(chatVisible[i] || "", cw);
14334
14555
  const debugLine = truncateAnsi(debugVisible[i] || "", dw);
14335
14556
  buf.push(
14336
- moveTo(i + 1, 1),
14557
+ moveTo(msgTop + i, 1),
14337
14558
  chatLine,
14338
14559
  CLEAR_EOL,
14339
14560
  // clear to end handles rest, but we need to position divider
14340
- moveTo(i + 1, divCol),
14561
+ moveTo(msgTop + i, divCol),
14341
14562
  `${DIM}\u2502${RESET}`,
14342
14563
  debugLine
14343
14564
  );
14344
14565
  buf.push(CLEAR_EOL);
14345
14566
  }
14346
14567
  }
14347
- renderInputLine() {
14348
- process.stdout.write(
14349
- `${moveTo(this.rows, 1)}${BLUE}You:${RESET} ${this.inputBuffer}${CLEAR_EOL}`
14350
- );
14568
+ renderInputArea(buf, startRow) {
14569
+ const inputRows = this.inputRows();
14570
+ const width = this.chatWidth();
14571
+ const contentWidth = Math.max(1, width - INPUT_LABEL_WIDTH);
14572
+ const inputLines = this.wrapInputLines(this.inputBuffer, contentWidth, inputRows);
14573
+ for (let i = 0; i < inputRows; i++) {
14574
+ const row = startRow + i;
14575
+ const line = inputLines[i] ?? "";
14576
+ const prefix = i === 0 ? `${BLUE}${INPUT_LABEL}${RESET} ` : " ".repeat(INPUT_LABEL_WIDTH);
14577
+ buf.push(moveTo(row, 1), prefix, line, CLEAR_EOL);
14578
+ }
14579
+ }
14580
+ wrapInputLines(text, maxWidth, maxRows) {
14581
+ const rawLines = text.split("\n");
14582
+ const wrapped = [];
14583
+ for (const raw of rawLines) {
14584
+ for (const wl of this.wrapText(raw, maxWidth)) {
14585
+ wrapped.push(wl);
14586
+ }
14587
+ }
14588
+ if (!wrapped.length) wrapped.push("");
14589
+ if (wrapped.length > maxRows) {
14590
+ const trimmed = wrapped.slice(wrapped.length - maxRows);
14591
+ trimmed[0] = "\u2026" + trimmed[0].slice(1);
14592
+ return trimmed;
14593
+ }
14594
+ return wrapped;
14595
+ }
14596
+ buildTitleBar() {
14597
+ const parts = [];
14598
+ if (this.options.title) {
14599
+ parts.push(`${CYAN}${this.options.title}${RESET}`);
14600
+ }
14601
+ if (this.options.subtitle) {
14602
+ parts.push(`${DIM}${this.options.subtitle}${RESET}`);
14603
+ }
14604
+ let status = "";
14605
+ switch (this.runState) {
14606
+ case "thinking":
14607
+ status = `${YELLOW}Thinking\u2026${RESET}`;
14608
+ break;
14609
+ case "tool_running":
14610
+ status = `${YELLOW}Running ${this.activeToolName}\u2026${RESET}`;
14611
+ break;
14612
+ case "streaming":
14613
+ status = `${GREEN}Writing\u2026${RESET}`;
14614
+ break;
14615
+ default:
14616
+ break;
14617
+ }
14618
+ if (status) parts.push(status);
14619
+ const line = parts.join(` ${DIM}|${RESET} `);
14620
+ return truncateAnsi(line, this.cols);
14351
14621
  }
14352
14622
  // ─── Separator ────────────────────────────────────────────────────
14353
14623
  buildSeparator() {
@@ -14362,6 +14632,7 @@ var ChatTUI = class {
14362
14632
  if (this.options.debug) {
14363
14633
  hints.push(`^D ${this.debugPanelVisible ? "hide" : "show"} debug`);
14364
14634
  }
14635
+ hints.push("Enter send");
14365
14636
  const lineWidth = Math.min(this.cols, 60);
14366
14637
  if (!hints.length) {
14367
14638
  return `${GRAY}${"\u2500".repeat(lineWidth)}${RESET}`;
@@ -14392,8 +14663,9 @@ var ChatTUI = class {
14392
14663
  return lines;
14393
14664
  }
14394
14665
  appendToolCallLines(lines, tc, width) {
14395
- const status = tc.done ? "Called" : "Calling";
14396
- const summary = ` ${GRAY}${status} ${tc.name}${tc.done ? "" : "..."}${RESET}`;
14666
+ const status = tc.done ? "\u2713" : "\u2026";
14667
+ const verb = tc.done ? "Called" : "Calling";
14668
+ const summary = ` ${tc.done ? GREEN : YELLOW}${status}${RESET} ${GRAY}${verb} ${tc.name}${RESET}`;
14397
14669
  if (!this.expandDetails) {
14398
14670
  lines.push(summary);
14399
14671
  return;
@@ -14414,14 +14686,15 @@ var ChatTUI = class {
14414
14686
  }
14415
14687
  }
14416
14688
  appendToolResultLines(lines, tr, width) {
14689
+ const mark = tr.failed ? `${RED}\u2717${RESET}` : `${GREEN}\u2713${RESET}`;
14417
14690
  if (!this.expandDetails) {
14418
- const maxLen = width - 10;
14691
+ const maxLen = width - 12;
14419
14692
  const preview = tr.content.replace(/\n/g, " ").slice(0, maxLen);
14420
14693
  const truncated = preview.length < tr.content.length ? "..." : "";
14421
- lines.push(` ${GRAY}\u2192 ${tr.toolName}: ${preview}${truncated}${RESET}`);
14694
+ lines.push(` ${mark} ${GRAY}${tr.toolName}: ${preview}${truncated}${RESET}`);
14422
14695
  return;
14423
14696
  }
14424
- lines.push(` ${GRAY}\u2192 ${tr.toolName} result:${RESET}`);
14697
+ lines.push(` ${mark} ${GRAY}${tr.toolName} result:${RESET}`);
14425
14698
  let formatted = tr.content;
14426
14699
  try {
14427
14700
  formatted = JSON.stringify(JSON.parse(tr.content), null, 2);
@@ -14473,7 +14746,12 @@ var ChatTUI = class {
14473
14746
  const labelWidth = 12;
14474
14747
  const indent = " ".repeat(labelWidth);
14475
14748
  if (!this.streamContent) {
14476
- lines.push(label);
14749
+ if (this.runState === "thinking" || this.runState === "tool_running") {
14750
+ const hint2 = this.runState === "tool_running" && this.activeToolName ? `Running ${this.activeToolName}\u2026` : "Thinking\u2026";
14751
+ lines.push(`${label}${YELLOW}${hint2}${RESET}`);
14752
+ } else {
14753
+ lines.push(label);
14754
+ }
14477
14755
  return;
14478
14756
  }
14479
14757
  let first = true;
@@ -14591,15 +14869,68 @@ function buildDebugSummary(_direction, msg) {
14591
14869
  return type;
14592
14870
  }
14593
14871
 
14872
+ // src/lib/agui-tui-events.ts
14873
+ function handleAguiEvent(tui, event) {
14874
+ const type = event.type?.toLowerCase() ?? "";
14875
+ switch (type) {
14876
+ case "run_started":
14877
+ tui.runStarted();
14878
+ break;
14879
+ case "run_finished":
14880
+ tui.runFinished();
14881
+ break;
14882
+ case "text_message_start":
14883
+ tui.streamStart();
14884
+ break;
14885
+ case "text_message_content": {
14886
+ const delta = getTextDelta(event);
14887
+ if (delta) tui.streamDelta(delta);
14888
+ break;
14889
+ }
14890
+ case "text_message_end":
14891
+ tui.streamEnd();
14892
+ break;
14893
+ case "tool_call_start":
14894
+ tui.toolCallStart(
14895
+ event.toolCallId ?? "",
14896
+ event.toolCallName ?? event.name ?? "tool"
14897
+ );
14898
+ break;
14899
+ case "tool_call_args":
14900
+ if (event.toolCallId && event.delta) {
14901
+ tui.toolCallArgs(event.toolCallId, event.delta);
14902
+ }
14903
+ break;
14904
+ case "tool_call_end":
14905
+ if (event.toolCallId) tui.toolCallEnd(event.toolCallId);
14906
+ break;
14907
+ case "tool_call_result": {
14908
+ const toolCallId = event.toolCallId ?? "";
14909
+ const toolName = tui.findToolCallName(toolCallId);
14910
+ const content = typeof event.content === "string" ? event.content : typeof event.result === "string" ? event.result : JSON.stringify(event.result ?? event.content ?? "");
14911
+ tui.toolCallResult(toolCallId, toolName, content);
14912
+ break;
14913
+ }
14914
+ case "run_error": {
14915
+ const errorMsg = getErrorMessage(event);
14916
+ if (errorMsg) tui.showError(errorMsg);
14917
+ tui.runFinished();
14918
+ break;
14919
+ }
14920
+ default:
14921
+ break;
14922
+ }
14923
+ }
14924
+
14594
14925
  // src/lib/audio.ts
14595
- import { spawn, execSync as execSync2, spawnSync } from "child_process";
14926
+ import { spawn, execSync, spawnSync } from "child_process";
14596
14927
  import { EventEmitter as EventEmitter2 } from "events";
14597
14928
  import * as readline3 from "readline";
14598
14929
  var SAMPLE_RATE = 24e3;
14599
14930
  var MIC_CHUNK_BYTES = 4800;
14600
14931
  function checkSoxAvailable() {
14601
14932
  try {
14602
- execSync2("which sox", { stdio: "pipe" });
14933
+ execSync("which sox", { stdio: "pipe" });
14603
14934
  return true;
14604
14935
  } catch {
14605
14936
  return false;
@@ -14910,6 +15241,7 @@ var AudioPlayer = class extends EventEmitter2 {
14910
15241
  };
14911
15242
 
14912
15243
  // src/lib/chat-session.ts
15244
+ var HITL_INTERRUPT_EXIT_CODE = 3;
14913
15245
  async function runChatSession(session, args) {
14914
15246
  const debug = !!args.debug;
14915
15247
  const isVoice = !!args.voice;
@@ -14939,6 +15271,14 @@ async function runChatSession(session, args) {
14939
15271
  function vadMode(args) {
14940
15272
  return typeof args.vad === "string" ? args.vad : "server";
14941
15273
  }
15274
+ function sanitizeInterruptForStdout(result2) {
15275
+ const url = typeof result2.payload.url === "string" ? result2.payload.url : void 0;
15276
+ return {
15277
+ type: result2.type,
15278
+ interrupt_id: result2.interruptId,
15279
+ url
15280
+ };
15281
+ }
14942
15282
  async function singleTurnChatSession(session, message, debug, autoApprove = false, allowWrite = false) {
14943
15283
  const aguiClient = new AGUIClient({
14944
15284
  baseUrl: session.ctx.envConfig.apiUrl,
@@ -14958,7 +15298,7 @@ async function singleTurnChatSession(session, message, debug, autoApprove = fals
14958
15298
  `);
14959
15299
  }
14960
15300
  };
14961
- const { streamText, hasToolCalls } = await runConciergeAguiLoop(
15301
+ const { streamText, hasToolCalls, unsupportedHitl } = await runConciergeAguiLoop(
14962
15302
  aguiClient,
14963
15303
  [{ role: "user", content: message }],
14964
15304
  onEvent,
@@ -14971,6 +15311,21 @@ async function singleTurnChatSession(session, message, debug, autoApprove = fals
14971
15311
  nonInteractive: true
14972
15312
  }
14973
15313
  );
15314
+ if (unsupportedHitl) {
15315
+ const jsonPayload = JSON.stringify({
15316
+ error: "hitl_interrupt",
15317
+ message: "A human-in-the-loop interrupt requires out-of-band action before the operation can continue.",
15318
+ resume: {
15319
+ chatId: session.chatId,
15320
+ chatUuid: session.chatUuid,
15321
+ interruptId: unsupportedHitl.interruptId
15322
+ },
15323
+ interrupt: sanitizeInterruptForStdout(unsupportedHitl)
15324
+ });
15325
+ process.stdout.write(`${jsonPayload}
15326
+ `);
15327
+ throw new CLIError("HITL interrupt requires out-of-band handling", HITL_INTERRUPT_EXIT_CODE);
15328
+ }
14974
15329
  if (streamText) {
14975
15330
  process.stdout.write(`\x1B[32mAssistant:\x1B[0m ${streamText}
14976
15331
  `);
@@ -15015,113 +15370,95 @@ async function interactiveChatSession(session, debug, autoApprove, allowWrite) {
15015
15370
  model: session.model,
15016
15371
  appContext: session.appContext
15017
15372
  });
15018
- clearScreen();
15019
- console.log("=".repeat(60));
15020
- console.log(session.label);
15021
- console.log("=".repeat(60));
15022
- console.log(`Chat: ${session.chatTitle}`);
15023
- console.log("Type your message and press Enter. Type 'exit' or Ctrl+C to quit.");
15024
- console.log("=".repeat(60));
15025
- console.log("");
15026
15373
  const existingMessages = await session.loadMessages();
15027
- if (existingMessages.length > 0) {
15028
- console.log("\x1B[90m--- Previous messages ---\x1B[0m\n");
15029
- for (const msg of existingMessages.slice(-10)) {
15030
- const text = typeof msg.content === "string" ? msg.content : "";
15031
- if (text && text.trim()) {
15032
- printMessage(text, msg.role);
15374
+ const history = buildHistory(existingMessages);
15375
+ const conversationMessages = buildConversationMessages(existingMessages);
15376
+ let tui;
15377
+ const sendTurn = async (text) => {
15378
+ conversationMessages.push({ role: "user", content: text });
15379
+ let assistantContent = "";
15380
+ try {
15381
+ tui.runStarted();
15382
+ const { streamText } = await runConciergeAguiLoop(
15383
+ aguiClient,
15384
+ conversationMessages,
15385
+ (event) => {
15386
+ handleAguiEvent(tui, event);
15387
+ const type = event.type?.toLowerCase() ?? "";
15388
+ if (type === "text_message_content" && event.delta) {
15389
+ assistantContent += event.delta;
15390
+ }
15391
+ const errorMsg = getErrorMessage(event);
15392
+ if (errorMsg) tui.showError(errorMsg);
15393
+ },
15394
+ {
15395
+ autoApprove,
15396
+ allowWrite: allowWrite || !!session.allowWrite,
15397
+ debug,
15398
+ trustCwd: session.trustCwd,
15399
+ trustDir: session.trustDir,
15400
+ onHitlPromptStart: () => tui.pauseForPrompt(),
15401
+ onHitlPromptEnd: () => tui.resumeAfterPrompt()
15402
+ }
15403
+ );
15404
+ if (streamText && !assistantContent) {
15405
+ assistantContent = streamText;
15033
15406
  }
15407
+ if (assistantContent) {
15408
+ conversationMessages.push({ role: "assistant", content: assistantContent });
15409
+ }
15410
+ } catch (error) {
15411
+ tui.showError(error instanceof Error ? error.message : String(error));
15412
+ } finally {
15413
+ tui.runFinished();
15034
15414
  }
15035
- console.log("\x1B[90m--- End of history ---\x1B[0m\n");
15036
- }
15037
- const conversationMessages = [];
15038
- for (const msg of existingMessages.slice(-10)) {
15415
+ };
15416
+ await new Promise((resolve) => {
15417
+ tui = new ChatTUI({
15418
+ title: session.label,
15419
+ subtitle: session.chatTitle,
15420
+ onSend: (text) => {
15421
+ void sendTurn(text);
15422
+ },
15423
+ onExit: () => {
15424
+ tui.stop();
15425
+ resolve();
15426
+ },
15427
+ debug,
15428
+ env: session.ctx.env
15429
+ });
15430
+ tui.loadHistory(history);
15431
+ tui.start();
15432
+ });
15433
+ }
15434
+ function buildConversationMessages(messages) {
15435
+ const out = [];
15436
+ for (const msg of messages) {
15039
15437
  const text = typeof msg.content === "string" ? msg.content : "";
15040
- if (text && text.trim() && (msg.role === "user" || msg.role === "assistant")) {
15041
- conversationMessages.push({ role: msg.role, content: text });
15438
+ if (text.trim() && (msg.role === "user" || msg.role === "assistant")) {
15439
+ out.push({ role: msg.role, content: text });
15042
15440
  }
15043
15441
  }
15044
- const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
15045
- return new Promise((resolve) => {
15046
- const prompt = () => {
15047
- rl.question("\x1B[34mYou:\x1B[0m ", async (input) => {
15048
- const trimmed = input.trim();
15049
- if (!trimmed) {
15050
- prompt();
15051
- return;
15052
- }
15053
- if (trimmed.toLowerCase() === "exit" || trimmed.toLowerCase() === "quit") {
15054
- console.log("\nGoodbye!");
15055
- rl.close();
15056
- return;
15057
- }
15058
- conversationMessages.push({ role: "user", content: trimmed });
15059
- process.stdout.write("\n\x1B[32mAssistant:\x1B[0m ");
15060
- let assistantContent = "";
15061
- try {
15062
- const { streamText } = await runConciergeAguiLoop(
15063
- aguiClient,
15064
- conversationMessages,
15065
- (event) => {
15066
- const delta = getTextDelta(event);
15067
- if (delta) {
15068
- process.stdout.write(delta);
15069
- assistantContent += delta;
15070
- }
15071
- const errorMsg = getErrorMessage(event);
15072
- if (errorMsg) console.log(`
15073
- \x1B[31mError: ${errorMsg}\x1B[0m`);
15074
- },
15075
- {
15076
- autoApprove,
15077
- allowWrite: allowWrite || !!session.allowWrite,
15078
- debug,
15079
- trustCwd: session.trustCwd,
15080
- trustDir: session.trustDir
15081
- }
15082
- );
15083
- if (streamText && !assistantContent) {
15084
- assistantContent = streamText;
15085
- process.stdout.write(streamText);
15086
- }
15087
- if (assistantContent) {
15088
- conversationMessages.push({ role: "assistant", content: assistantContent });
15089
- }
15090
- console.log("\n");
15091
- } catch (error) {
15092
- console.log(
15093
- `
15094
- \x1B[31mError: ${error instanceof Error ? error.message : error}\x1B[0m
15095
- `
15096
- );
15097
- }
15098
- prompt();
15099
- });
15100
- };
15101
- rl.on("close", () => {
15102
- console.log("\nGoodbye!");
15103
- resolve();
15104
- });
15105
- prompt();
15106
- });
15107
- }
15108
- function buildHistory(messages) {
15109
- const history = [];
15110
- for (const msg of messages.slice(-30)) {
15111
- const text = typeof msg.content === "string" ? msg.content : "";
15112
- if (msg.role === "user" || msg.role === "assistant") {
15113
- if (text.trim()) {
15114
- history.push({ role: msg.role, content: text });
15115
- }
15116
- if (msg.role === "assistant" && msg.tool_calls) {
15117
- for (const tc of msg.tool_calls) {
15118
- history.push({
15119
- kind: "tool_call",
15120
- id: tc.id,
15121
- name: tc.function.name,
15122
- args: tc.function.arguments || "",
15123
- done: true
15124
- });
15442
+ return out;
15443
+ }
15444
+ function buildHistory(messages, limit) {
15445
+ const history = [];
15446
+ const slice = limit ? messages.slice(-limit) : messages;
15447
+ for (const msg of slice) {
15448
+ const text = typeof msg.content === "string" ? msg.content : "";
15449
+ if (msg.role === "user" || msg.role === "assistant") {
15450
+ if (text.trim()) {
15451
+ history.push({ role: msg.role, content: text });
15452
+ }
15453
+ if (msg.role === "assistant" && msg.tool_calls) {
15454
+ for (const tc of msg.tool_calls) {
15455
+ history.push({
15456
+ kind: "tool_call",
15457
+ id: tc.id,
15458
+ name: tc.function.name,
15459
+ args: tc.function.arguments || "",
15460
+ done: true
15461
+ });
15125
15462
  }
15126
15463
  }
15127
15464
  } else if (msg.role === "tool") {
@@ -15179,7 +15516,7 @@ async function realtimeChatSession(session, vad, debug) {
15179
15516
  throw new CLIError(`Failed to connect: ${err instanceof Error ? err.message : err}`);
15180
15517
  }
15181
15518
  const existingMessages = await session.loadMessages();
15182
- const history = buildHistory(existingMessages);
15519
+ const history = buildHistory(existingMessages, 30);
15183
15520
  const tui = new ChatTUI({
15184
15521
  onSend: (text) => {
15185
15522
  client.sendTextMessage(text);
@@ -15244,7 +15581,7 @@ Connecting to ${session.label.toLowerCase()}...
15244
15581
  throw new CLIError(`Failed to connect: ${err instanceof Error ? err.message : err}`);
15245
15582
  }
15246
15583
  const existingMessages = await session.loadMessages();
15247
- const history = buildHistory(existingMessages);
15584
+ const history = buildHistory(existingMessages, 30);
15248
15585
  const mic = new MicCapture((pcm16) => {
15249
15586
  if (!client.isConnected()) return;
15250
15587
  client.sendAudio(pcm16);
@@ -15339,15 +15676,169 @@ async function listAudioDevices() {
15339
15676
  }
15340
15677
  }
15341
15678
 
15679
+ // src/lib/chat-history.ts
15680
+ var PAGE_SIZE = 100;
15681
+ var HARD_CAP = 2e3;
15682
+ async function loadAllChatMessages(ctx, workspaceUuid, chatId) {
15683
+ const items = [];
15684
+ let offset = 0;
15685
+ while (items.length < HARD_CAP) {
15686
+ const batch = await ctx.apiClient.getChatMessages(workspaceUuid, chatId, PAGE_SIZE, offset).catch(() => []);
15687
+ if (!batch.length) break;
15688
+ items.push(...batch);
15689
+ if (batch.length < PAGE_SIZE) break;
15690
+ offset += batch.length;
15691
+ }
15692
+ return items;
15693
+ }
15694
+
15695
+ // src/commands/manage.ts
15696
+ async function manageCommand(hctx) {
15697
+ const ctx = requireAuth(hctx.ctx);
15698
+ const { workspace, args } = hctx;
15699
+ const asMember = typeof args["as-member"] === "string" ? args["as-member"] : void 0;
15700
+ const chatIdArg = typeof args["chat-id"] === "string" ? args["chat-id"] : void 0;
15701
+ const message = typeof args.message === "string" ? args.message : void 0;
15702
+ const chat = await getOrCreateChat(ctx, workspace, chatIdArg, asMember, "manager", !!message);
15703
+ const session = buildWorkspaceSession(ctx, workspace, chat, "Workspace Manager", {
15704
+ appContext: buildCliConciergeAppContext(),
15705
+ allowWrite: true,
15706
+ trustCwd: !!args["trust-cwd"],
15707
+ trustDir: typeof args["trust-dir"] === "string" ? args["trust-dir"] : void 0
15708
+ });
15709
+ process.stderr.write(`Chat #${session.chatId}
15710
+ `);
15711
+ await runChatSession(session, args);
15712
+ process.stderr.write(`Chat #${session.chatId}
15713
+ `);
15714
+ }
15715
+ async function chatCommand(hctx) {
15716
+ const ctx = requireAuth(hctx.ctx);
15717
+ const { workspace, args } = hctx;
15718
+ const chatIdArg = typeof args["chat-id"] === "string" ? args["chat-id"] : void 0;
15719
+ const workflowId = typeof args.workflow === "string" ? args.workflow : void 0;
15720
+ const memberId = typeof args.member === "string" ? args.member : void 0;
15721
+ let chat;
15722
+ if (chatIdArg) {
15723
+ const id = Number(chatIdArg);
15724
+ if (isNaN(id)) throw new CLIError("--chat-id must be a number");
15725
+ chat = await ctx.apiClient.getChat(workspace, id);
15726
+ } else if (workflowId) {
15727
+ chat = await createAssignmentChat(ctx.apiClient, workspace, Number(workflowId), memberId);
15728
+ } else if (memberId) {
15729
+ chat = await createMemberChat(ctx.apiClient, workspace, memberId);
15730
+ } else {
15731
+ throw new CLIError(
15732
+ 'Specify --workflow <id>, --member <id>, or --chat-id <id>.\nUse "gr manage" for workspace manager chats.'
15733
+ );
15734
+ }
15735
+ const session = buildWorkspaceSession(ctx, workspace, chat, "Chat");
15736
+ await runChatSession(session, args);
15737
+ }
15738
+ function buildWorkspaceSession(ctx, workspace, chat, label, extra) {
15739
+ const wsBase = ctx.envConfig.apiUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
15740
+ return {
15741
+ ctx,
15742
+ workspaceUuid: workspace,
15743
+ chatId: chat.id,
15744
+ chatUuid: chat.uuid,
15745
+ chatTitle: chat.title || chat.uuid,
15746
+ aguiEndpointUrl: `${ctx.envConfig.apiUrl}/api/v2/w/${workspace}/web-chat/${chat.id}/chat/agui`,
15747
+ wsUrl: `${wsBase}/api/v2/w/${workspace}/realtime/stream-ws`,
15748
+ loadMessages: () => loadAllChatMessages(ctx, workspace, chat.id),
15749
+ label,
15750
+ model: "manager",
15751
+ ...extra
15752
+ };
15753
+ }
15754
+ async function getOrCreateChat(ctx, workspace, chatIdArg, asMemberId, chatType, skipSelection = false) {
15755
+ if (chatIdArg) {
15756
+ const chatId = Number(chatIdArg);
15757
+ if (isNaN(chatId)) throw new CLIError("--chat-id must be a number");
15758
+ return ctx.apiClient.getChat(workspace, chatId);
15759
+ }
15760
+ if (skipSelection) {
15761
+ return createManagerChat(ctx.apiClient, workspace, asMemberId);
15762
+ }
15763
+ const chatsResponse = await ctx.apiClient.getChats(workspace, {
15764
+ chatType,
15765
+ sortBy: "updatedAt",
15766
+ sortOrder: "desc",
15767
+ pageSize: 20
15768
+ });
15769
+ const chats = chatsResponse.items || [];
15770
+ const { item: selectedChat, isNew } = await selectFromList(chats, {
15771
+ title: "Select a chat or create a new one",
15772
+ labelFn: (chat) => chat.title || `Chat ${chat.uuid.slice(0, 8)}...`,
15773
+ detailFn: (chat) => {
15774
+ const time = formatTimestamp(chat.updatedAt);
15775
+ const msgCount = chat.messageCount || 0;
15776
+ return `${msgCount} msg${msgCount !== 1 ? "s" : ""} - ${time}`;
15777
+ },
15778
+ emptyMessage: "No existing chats.",
15779
+ allowNew: true,
15780
+ newLabel: "+ Start new chat"
15781
+ });
15782
+ if (!selectedChat && !isNew) {
15783
+ throw new CLIError("No chat selected.");
15784
+ }
15785
+ if (isNew) {
15786
+ return createManagerChat(ctx.apiClient, workspace, asMemberId);
15787
+ }
15788
+ return selectedChat;
15789
+ }
15790
+ async function createManagerChat(apiClient, workspace, asMemberId) {
15791
+ const data = {
15792
+ chatType: "manager",
15793
+ channel: "web-chat"
15794
+ };
15795
+ if (asMemberId) {
15796
+ data.memberId = Number(asMemberId);
15797
+ }
15798
+ const chat = await apiClient.createChat(workspace, data);
15799
+ process.stderr.write(`Created chat #${chat.id}
15800
+ `);
15801
+ return chat;
15802
+ }
15803
+ async function createAssignmentChat(apiClient, workspace, workflowId, asMemberId) {
15804
+ if (isNaN(workflowId)) throw new CLIError("--workflow must be a number");
15805
+ const data = {
15806
+ chatType: "assignment",
15807
+ channel: "web-chat",
15808
+ workflowId
15809
+ };
15810
+ if (asMemberId) {
15811
+ data.memberId = Number(asMemberId);
15812
+ }
15813
+ const chat = await apiClient.createChat(workspace, data);
15814
+ process.stderr.write(`Created assignment chat #${chat.id} (workflow ${workflowId})
15815
+ `);
15816
+ return chat;
15817
+ }
15818
+ async function createMemberChat(apiClient, workspace, memberId) {
15819
+ const id = Number(memberId);
15820
+ if (isNaN(id)) throw new CLIError("--member must be a number");
15821
+ const data = {
15822
+ chatType: "member",
15823
+ channel: "web-chat",
15824
+ memberId: id
15825
+ };
15826
+ const chat = await apiClient.createChat(workspace, data);
15827
+ process.stderr.write(`Created DM chat #${chat.id} (member ${memberId})
15828
+ `);
15829
+ return chat;
15830
+ }
15831
+
15342
15832
  // src/commands/concierge.ts
15343
15833
  async function conciergeOutboundCommand(hctx) {
15344
15834
  const ctx = requireAuth(hctx.ctx);
15345
15835
  const { args } = hctx;
15836
+ const message = typeof args.message === "string" ? args.message : void 0;
15346
15837
  if (args.call && args.sms) {
15347
15838
  throw new CLIError("--call and --sms are mutually exclusive");
15348
15839
  }
15349
15840
  if (args.call) {
15350
- const result2 = await originateConciergeCall(ctx);
15841
+ const result2 = await originateConciergeCall(ctx, "concierge_setup", message, "cli");
15351
15842
  process.stderr.write(
15352
15843
  `Concierge call started. callSid=${result2.callSid} startedAt=${result2.startedAt}
15353
15844
  `
@@ -15438,6 +15929,64 @@ async function getOrCreateConciergeChat(ctx, chatIdArg, skipSelection) {
15438
15929
  return selectedChat;
15439
15930
  }
15440
15931
 
15932
+ // src/commands/dev.ts
15933
+ import { existsSync, mkdirSync, writeFileSync, readFileSync as readFileSync2 } from "fs";
15934
+ import { execSync as execSync2 } from "child_process";
15935
+ import { join } from "path";
15936
+ async function devImportLocalCommand(ctx, workspace) {
15937
+ stderr(`Exporting workspace ${workspace}...`);
15938
+ const exportData = await ctx.apiClient.exportWorkspace(workspace);
15939
+ stderr("Export successful!");
15940
+ stderr(` Workflows: ${(exportData.workflows || []).length}`);
15941
+ stderr(` Tasks: ${(exportData.tasks || []).length}`);
15942
+ stderr(` Data Types: ${(exportData.dataTypes || []).length}`);
15943
+ stderr(` Sites: ${(exportData.sites || []).length}`);
15944
+ const slug = workspace.slice(0, 8);
15945
+ const importCacheDir = join(ctx.credentialsManager.getCacheDir(), "imports");
15946
+ if (!existsSync(importCacheDir)) {
15947
+ mkdirSync(importCacheDir, { recursive: true });
15948
+ }
15949
+ const filename = `ws-${slug}-${workspace}.json`;
15950
+ const filepath = join(importCacheDir, filename);
15951
+ writeFileSync(filepath, JSON.stringify(exportData, null, 2));
15952
+ stderr(`Saved to: ${filepath}`);
15953
+ stderr("Running dev:restore...");
15954
+ const rootDir = findProjectRoot();
15955
+ const relativePath = filepath.replace(rootDir + "/", "");
15956
+ try {
15957
+ const result2 = execSync2(`yarn dev:restore ${relativePath}`, {
15958
+ cwd: rootDir,
15959
+ stdio: "pipe",
15960
+ encoding: "utf-8"
15961
+ });
15962
+ process.stderr.write(result2);
15963
+ const failed = result2.includes("Failed/Skipped: ") && !result2.includes("Failed/Skipped: 0") || result2.includes("Successfully restored: 0");
15964
+ if (failed) throw new CLIError("Import failed");
15965
+ } catch (error) {
15966
+ if (error instanceof CLIError) throw error;
15967
+ const execError = error;
15968
+ if (execError.stdout) process.stderr.write(execError.stdout);
15969
+ if (execError.stderr) process.stderr.write(execError.stderr);
15970
+ throw new CLIError("dev:restore failed");
15971
+ }
15972
+ stderr(`Import complete: ${workspace}`);
15973
+ }
15974
+ function findProjectRoot() {
15975
+ let dir = process.cwd();
15976
+ while (dir !== "/") {
15977
+ const pkgPath = join(dir, "package.json");
15978
+ if (existsSync(pkgPath)) {
15979
+ try {
15980
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
15981
+ if (pkg.workspaces) return dir;
15982
+ } catch {
15983
+ }
15984
+ }
15985
+ dir = join(dir, "..");
15986
+ }
15987
+ return process.cwd();
15988
+ }
15989
+
15441
15990
  // src/commands/get-started.ts
15442
15991
  var WEB_SETUP = "https://app.gravityrail.com/setup";
15443
15992
  var WEB_GET_STARTED = "https://app.gravityrail.com/get-started";
@@ -15537,73 +16086,15 @@ async function getStartedCommand(hctx) {
15537
16086
  process.stdout.write(LLM_GUIDE);
15538
16087
  }
15539
16088
 
15540
- // src/commands/dev.ts
15541
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync3 } from "fs";
15542
- import { execSync as execSync3 } from "child_process";
15543
- import { join as join2 } from "path";
15544
- async function devImportLocalCommand(ctx, workspace) {
15545
- stderr(`Exporting workspace ${workspace}...`);
15546
- const exportData = await ctx.apiClient.exportWorkspace(workspace);
15547
- stderr("Export successful!");
15548
- stderr(` Workflows: ${(exportData.workflows || []).length}`);
15549
- stderr(` Tasks: ${(exportData.tasks || []).length}`);
15550
- stderr(` Data Types: ${(exportData.dataTypes || []).length}`);
15551
- stderr(` Sites: ${(exportData.sites || []).length}`);
15552
- const slug = workspace.slice(0, 8);
15553
- const importCacheDir = join2(ctx.credentialsManager.getCacheDir(), "imports");
15554
- if (!existsSync2(importCacheDir)) {
15555
- mkdirSync2(importCacheDir, { recursive: true });
15556
- }
15557
- const filename = `ws-${slug}-${workspace}.json`;
15558
- const filepath = join2(importCacheDir, filename);
15559
- writeFileSync2(filepath, JSON.stringify(exportData, null, 2));
15560
- stderr(`Saved to: ${filepath}`);
15561
- stderr("Running dev:restore...");
15562
- const rootDir = findProjectRoot();
15563
- const relativePath = filepath.replace(rootDir + "/", "");
15564
- try {
15565
- const result2 = execSync3(`yarn dev:restore ${relativePath}`, {
15566
- cwd: rootDir,
15567
- stdio: "pipe",
15568
- encoding: "utf-8"
15569
- });
15570
- process.stderr.write(result2);
15571
- const failed = result2.includes("Failed/Skipped: ") && !result2.includes("Failed/Skipped: 0") || result2.includes("Successfully restored: 0");
15572
- if (failed) throw new CLIError("Import failed");
15573
- } catch (error) {
15574
- if (error instanceof CLIError) throw error;
15575
- const execError = error;
15576
- if (execError.stdout) process.stderr.write(execError.stdout);
15577
- if (execError.stderr) process.stderr.write(execError.stderr);
15578
- throw new CLIError("dev:restore failed");
15579
- }
15580
- stderr(`Import complete: ${workspace}`);
15581
- }
15582
- function findProjectRoot() {
15583
- let dir = process.cwd();
15584
- while (dir !== "/") {
15585
- const pkgPath = join2(dir, "package.json");
15586
- if (existsSync2(pkgPath)) {
15587
- try {
15588
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
15589
- if (pkg.workspaces) return dir;
15590
- } catch {
15591
- }
15592
- }
15593
- dir = join2(dir, "..");
15594
- }
15595
- return process.cwd();
15596
- }
15597
-
15598
16089
  // src/commands/login.ts
15599
- import { GravityRailClient as GravityRailClient2 } from "@gravity-rail/sdk";
16090
+ import { GravityRailClient } from "@gravity-rail/sdk";
15600
16091
  async function loginCommand(ctx) {
15601
16092
  const { accessToken } = await performOAuthFlow({
15602
16093
  env: ctx.env,
15603
16094
  envConfig: ctx.envConfig,
15604
16095
  credentialsManager: ctx.credentialsManager
15605
16096
  });
15606
- const apiClient = new GravityRailClient2(accessToken, ctx.envConfig.apiUrl);
16097
+ const apiClient = new GravityRailClient(accessToken, ctx.envConfig.apiUrl);
15607
16098
  const account = await apiClient.getUserAccount();
15608
16099
  stderr(`Logged in as ${account.name} (${account.email})`);
15609
16100
  }
@@ -15612,143 +16103,6 @@ async function logoutCommand(ctx) {
15612
16103
  stderr(`Credentials cleared for ${ctx.env}`);
15613
16104
  }
15614
16105
 
15615
- // src/commands/manage.ts
15616
- async function manageCommand(hctx) {
15617
- const ctx = requireAuth(hctx.ctx);
15618
- const { workspace, args } = hctx;
15619
- const asMember = typeof args["as-member"] === "string" ? args["as-member"] : void 0;
15620
- const chatIdArg = typeof args["chat-id"] === "string" ? args["chat-id"] : void 0;
15621
- const message = typeof args.message === "string" ? args.message : void 0;
15622
- const chat = await getOrCreateChat(ctx, workspace, chatIdArg, asMember, "manager", !!message);
15623
- const session = buildWorkspaceSession(ctx, workspace, chat, "Workspace Manager", {
15624
- appContext: buildCliConciergeAppContext(),
15625
- allowWrite: true,
15626
- trustCwd: !!args["trust-cwd"],
15627
- trustDir: typeof args["trust-dir"] === "string" ? args["trust-dir"] : void 0
15628
- });
15629
- process.stderr.write(`Chat #${session.chatId}
15630
- `);
15631
- await runChatSession(session, args);
15632
- process.stderr.write(`Chat #${session.chatId}
15633
- `);
15634
- }
15635
- async function chatCommand(hctx) {
15636
- const ctx = requireAuth(hctx.ctx);
15637
- const { workspace, args } = hctx;
15638
- const chatIdArg = typeof args["chat-id"] === "string" ? args["chat-id"] : void 0;
15639
- const workflowId = typeof args.workflow === "string" ? args.workflow : void 0;
15640
- const memberId = typeof args.member === "string" ? args.member : void 0;
15641
- let chat;
15642
- if (chatIdArg) {
15643
- const id = Number(chatIdArg);
15644
- if (isNaN(id)) throw new CLIError("--chat-id must be a number");
15645
- chat = await ctx.apiClient.getChat(workspace, id);
15646
- } else if (workflowId) {
15647
- chat = await createAssignmentChat(ctx.apiClient, workspace, Number(workflowId), memberId);
15648
- } else if (memberId) {
15649
- chat = await createMemberChat(ctx.apiClient, workspace, memberId);
15650
- } else {
15651
- throw new CLIError(
15652
- 'Specify --workflow <id>, --member <id>, or --chat-id <id>.\nUse "gr manage" for workspace manager chats.'
15653
- );
15654
- }
15655
- const session = buildWorkspaceSession(ctx, workspace, chat, "Chat");
15656
- await runChatSession(session, args);
15657
- }
15658
- function buildWorkspaceSession(ctx, workspace, chat, label, extra) {
15659
- const wsBase = ctx.envConfig.apiUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
15660
- return {
15661
- ctx,
15662
- workspaceUuid: workspace,
15663
- chatId: chat.id,
15664
- chatUuid: chat.uuid,
15665
- chatTitle: chat.title || chat.uuid,
15666
- aguiEndpointUrl: `${ctx.envConfig.apiUrl}/api/v2/w/${workspace}/web-chat/${chat.id}/chat/agui`,
15667
- wsUrl: `${wsBase}/api/v2/w/${workspace}/realtime/stream-ws`,
15668
- loadMessages: () => ctx.apiClient.getChatMessages(workspace, chat.id, 50).catch(() => []),
15669
- label,
15670
- model: "manager",
15671
- ...extra
15672
- };
15673
- }
15674
- async function getOrCreateChat(ctx, workspace, chatIdArg, asMemberId, chatType, skipSelection = false) {
15675
- if (chatIdArg) {
15676
- const chatId = Number(chatIdArg);
15677
- if (isNaN(chatId)) throw new CLIError("--chat-id must be a number");
15678
- return ctx.apiClient.getChat(workspace, chatId);
15679
- }
15680
- if (skipSelection) {
15681
- return createManagerChat(ctx.apiClient, workspace, asMemberId);
15682
- }
15683
- const chatsResponse = await ctx.apiClient.getChats(workspace, {
15684
- chatType,
15685
- sortBy: "updatedAt",
15686
- sortOrder: "desc",
15687
- pageSize: 20
15688
- });
15689
- const chats = chatsResponse.items || [];
15690
- const { item: selectedChat, isNew } = await selectFromList(chats, {
15691
- title: "Select a chat or create a new one",
15692
- labelFn: (chat) => chat.title || `Chat ${chat.uuid.slice(0, 8)}...`,
15693
- detailFn: (chat) => {
15694
- const time = formatTimestamp(chat.updatedAt);
15695
- const msgCount = chat.messageCount || 0;
15696
- return `${msgCount} msg${msgCount !== 1 ? "s" : ""} - ${time}`;
15697
- },
15698
- emptyMessage: "No existing chats.",
15699
- allowNew: true,
15700
- newLabel: "+ Start new chat"
15701
- });
15702
- if (!selectedChat && !isNew) {
15703
- throw new CLIError("No chat selected.");
15704
- }
15705
- if (isNew) {
15706
- return createManagerChat(ctx.apiClient, workspace, asMemberId);
15707
- }
15708
- return selectedChat;
15709
- }
15710
- async function createManagerChat(apiClient, workspace, asMemberId) {
15711
- const data = {
15712
- chatType: "manager",
15713
- channel: "web-chat"
15714
- };
15715
- if (asMemberId) {
15716
- data.memberId = Number(asMemberId);
15717
- }
15718
- const chat = await apiClient.createChat(workspace, data);
15719
- process.stderr.write(`Created chat #${chat.id}
15720
- `);
15721
- return chat;
15722
- }
15723
- async function createAssignmentChat(apiClient, workspace, workflowId, asMemberId) {
15724
- if (isNaN(workflowId)) throw new CLIError("--workflow must be a number");
15725
- const data = {
15726
- chatType: "assignment",
15727
- channel: "web-chat",
15728
- workflowId
15729
- };
15730
- if (asMemberId) {
15731
- data.memberId = Number(asMemberId);
15732
- }
15733
- const chat = await apiClient.createChat(workspace, data);
15734
- process.stderr.write(`Created assignment chat #${chat.id} (workflow ${workflowId})
15735
- `);
15736
- return chat;
15737
- }
15738
- async function createMemberChat(apiClient, workspace, memberId) {
15739
- const id = Number(memberId);
15740
- if (isNaN(id)) throw new CLIError("--member must be a number");
15741
- const data = {
15742
- chatType: "member",
15743
- channel: "web-chat",
15744
- memberId: id
15745
- };
15746
- const chat = await apiClient.createChat(workspace, data);
15747
- process.stderr.write(`Created DM chat #${chat.id} (member ${memberId})
15748
- `);
15749
- return chat;
15750
- }
15751
-
15752
16106
  // src/commands/organizations.ts
15753
16107
  var COLUMNS34 = [
15754
16108
  { header: "NAME", getValue: (o) => o.organizationName || "", maxWidth: 35 },
@@ -15776,6 +16130,230 @@ async function whoamiCommand(ctx) {
15776
16130
  );
15777
16131
  outputSingle(account, ctx, columns);
15778
16132
  }
16133
+
16134
+ // src/top-level-commands.ts
16135
+ var topLevelCommands = [
16136
+ {
16137
+ path: ["login"],
16138
+ description: "Authenticate via OAuth",
16139
+ requiresAuth: false,
16140
+ handler: async ({ ctx }) => {
16141
+ await loginCommand(ctx);
16142
+ }
16143
+ },
16144
+ {
16145
+ path: ["logout"],
16146
+ description: "Clear cached credentials",
16147
+ requiresAuth: false,
16148
+ handler: async ({ ctx }) => {
16149
+ await logoutCommand(ctx);
16150
+ }
16151
+ },
16152
+ {
16153
+ path: ["auth", "browser"],
16154
+ description: "Open an authenticated browser session using CLI credentials",
16155
+ args: [
16156
+ { name: "no-open", description: "Print the URL instead of opening browser", isFlag: true }
16157
+ ],
16158
+ handler: async ({ ctx, args }) => {
16159
+ await authBrowserCommand(requireAuth(ctx), args);
16160
+ }
16161
+ },
16162
+ {
16163
+ path: ["manage"],
16164
+ description: "Interactive workspace manager chat (text, realtime, or voice)",
16165
+ requiresWorkspace: true,
16166
+ mutates: true,
16167
+ args: [
16168
+ { name: "realtime", description: "Use WebSocket realtime connection", isFlag: true },
16169
+ { name: "voice", description: "Enable voice mode (microphone + speaker)", isFlag: true },
16170
+ { name: "message", alias: "m", description: "Single-turn: send one message and exit" },
16171
+ { name: "as-member", description: "Chat as a specific member (by numeric ID)" },
16172
+ { name: "chat-id", description: "Resume an existing chat by numeric ID" },
16173
+ { name: "vad", description: "Voice activity detection: server (default) or client" },
16174
+ { name: "list-devices", description: "List audio devices and exit", isFlag: true },
16175
+ { name: "input-device", description: "Microphone device name or ID" },
16176
+ { name: "output-device", description: "Speaker device name or ID" },
16177
+ { name: "debug", description: "Enable debug logging", isFlag: true },
16178
+ {
16179
+ name: "auto-approve",
16180
+ description: "Auto-approve all tool calls (skip HITL)",
16181
+ isFlag: true
16182
+ },
16183
+ {
16184
+ name: "trust-cwd",
16185
+ description: "Trust files in the current working directory (non-interactive)",
16186
+ isFlag: true
16187
+ },
16188
+ {
16189
+ name: "trust-dir",
16190
+ description: "Trust files in this directory (non-interactive; absolute path)"
16191
+ }
16192
+ ],
16193
+ handler: async (hctx) => {
16194
+ await manageCommand(hctx);
16195
+ }
16196
+ },
16197
+ {
16198
+ path: ["chat"],
16199
+ description: "Chat with a workflow, member, or existing chat (text, realtime, or voice)",
16200
+ requiresWorkspace: true,
16201
+ mutates: true,
16202
+ args: [
16203
+ { name: "workflow", description: "Start assignment chat with workflow (by numeric ID)" },
16204
+ { name: "member", description: "Start DM with a member (by numeric ID)" },
16205
+ { name: "chat-id", description: "Resume an existing chat by numeric ID" },
16206
+ { name: "realtime", description: "Use WebSocket realtime connection", isFlag: true },
16207
+ { name: "voice", description: "Enable voice mode (microphone + speaker)", isFlag: true },
16208
+ { name: "message", alias: "m", description: "Single-turn: send one message and exit" },
16209
+ { name: "vad", description: "Voice activity detection: server (default) or client" },
16210
+ { name: "list-devices", description: "List audio devices and exit", isFlag: true },
16211
+ { name: "input-device", description: "Microphone device name or ID" },
16212
+ { name: "output-device", description: "Speaker device name or ID" },
16213
+ { name: "debug", description: "Enable debug logging", isFlag: true },
16214
+ {
16215
+ name: "auto-approve",
16216
+ description: "Auto-approve all tool calls (skip HITL)",
16217
+ isFlag: true
16218
+ }
16219
+ ],
16220
+ handler: async (hctx) => {
16221
+ await chatCommand(hctx);
16222
+ }
16223
+ },
16224
+ {
16225
+ path: ["get-started"],
16226
+ description: "Bootstrap instructions for new users and LLM agents",
16227
+ requiresAuth: false,
16228
+ args: [
16229
+ {
16230
+ name: "format",
16231
+ description: "Output format: llm (agent-readable) or default (human)"
16232
+ },
16233
+ {
16234
+ name: "agent",
16235
+ description: "Alias for --format llm",
16236
+ isFlag: true
16237
+ }
16238
+ ],
16239
+ handler: async (hctx) => {
16240
+ await getStartedCommand(hctx);
16241
+ }
16242
+ },
16243
+ {
16244
+ path: ["setup"],
16245
+ description: "Alias for get-started \u2014 bootstrap instructions",
16246
+ requiresAuth: false,
16247
+ args: [
16248
+ { name: "format", description: "Output format: llm or default" },
16249
+ { name: "agent", description: "Alias for --format llm", isFlag: true }
16250
+ ],
16251
+ handler: async (hctx) => {
16252
+ await getStartedCommand(hctx);
16253
+ }
16254
+ },
16255
+ {
16256
+ path: ["concierge"],
16257
+ description: "Call or SMS your verified phone via the Concierge (server-side)",
16258
+ requiresWorkspace: false,
16259
+ mutates: true,
16260
+ args: [
16261
+ { name: "call", description: "Ring your verified account phone", isFlag: true },
16262
+ { name: "sms", description: "Start an SMS thread on your verified phone", isFlag: true }
16263
+ ],
16264
+ handler: async (hctx) => {
16265
+ await conciergeOutboundCommand(hctx);
16266
+ }
16267
+ },
16268
+ {
16269
+ path: ["account", "verify-phone"],
16270
+ description: "Verify your account phone for concierge chat --call / --sms",
16271
+ requiresWorkspace: false,
16272
+ mutates: true,
16273
+ args: [{ name: "phone", description: "E.164 phone number (optional; prompts if omitted)" }],
16274
+ handler: async (hctx) => {
16275
+ const { accountVerifyPhoneCommand } = await import("./account-verify-phone-TC73QIIE.js");
16276
+ await accountVerifyPhoneCommand(hctx);
16277
+ }
16278
+ },
16279
+ {
16280
+ path: ["concierge", "chat"],
16281
+ description: "Interactive concierge chat (text, realtime, or voice)",
16282
+ requiresWorkspace: false,
16283
+ mutates: true,
16284
+ args: [
16285
+ { name: "realtime", description: "Use WebSocket realtime connection", isFlag: true },
16286
+ { name: "voice", description: "Enable voice mode (microphone + speaker)", isFlag: true },
16287
+ {
16288
+ name: "call",
16289
+ description: "Ring your verified account phone (server-side)",
16290
+ isFlag: true
16291
+ },
16292
+ {
16293
+ name: "sms",
16294
+ description: "SMS your verified account phone (server-side)",
16295
+ isFlag: true
16296
+ },
16297
+ {
16298
+ name: "message",
16299
+ alias: "m",
16300
+ description: 'Single-turn: send one message and exit. With --call: the "why I am calling" anchor for the opening self-introduction.'
16301
+ },
16302
+ { name: "chat-id", description: "Resume an existing chat by numeric ID" },
16303
+ { name: "vad", description: "Voice activity detection: server (default) or client" },
16304
+ { name: "list-devices", description: "List audio devices and exit", isFlag: true },
16305
+ { name: "input-device", description: "Microphone device name or ID" },
16306
+ { name: "output-device", description: "Speaker device name or ID" },
16307
+ { name: "debug", description: "Enable debug logging", isFlag: true },
16308
+ {
16309
+ name: "auto-approve",
16310
+ description: "Auto-approve all tool calls (skip HITL)",
16311
+ isFlag: true
16312
+ },
16313
+ {
16314
+ name: "trust-cwd",
16315
+ description: "Trust files in the current working directory (non-interactive)",
16316
+ isFlag: true
16317
+ },
16318
+ {
16319
+ name: "trust-dir",
16320
+ description: "Trust files in this directory (non-interactive; absolute path)"
16321
+ }
16322
+ ],
16323
+ handler: async (hctx) => {
16324
+ await conciergeCommand(hctx);
16325
+ }
16326
+ },
16327
+ {
16328
+ path: ["dev", "import-local"],
16329
+ description: "Import remote workspace locally",
16330
+ requiresWorkspace: true,
16331
+ handler: async ({ ctx, workspace }) => {
16332
+ await devImportLocalCommand(requireAuth(ctx), workspace);
16333
+ }
16334
+ },
16335
+ {
16336
+ path: ["whoami"],
16337
+ description: "Show current user info",
16338
+ handler: async ({ ctx }) => {
16339
+ await whoamiCommand(requireAuth(ctx));
16340
+ }
16341
+ },
16342
+ {
16343
+ path: ["organizations", "list"],
16344
+ description: "List organizations",
16345
+ handler: async ({ ctx }) => {
16346
+ await organizationsListCommand(requireAuth(ctx));
16347
+ }
16348
+ },
16349
+ {
16350
+ path: ["orgs", "list"],
16351
+ description: "List organizations",
16352
+ handler: async ({ ctx }) => {
16353
+ await organizationsListCommand(requireAuth(ctx));
16354
+ }
16355
+ }
16356
+ ];
15779
16357
  export {
15780
16358
  AuthRequiredError,
15781
16359
  CLIError,
@@ -15834,6 +16412,7 @@ export {
15834
16412
  simpleCrudCommands,
15835
16413
  startAccountPhoneVerify,
15836
16414
  stderr,
16415
+ topLevelCommands,
15837
16416
  unwrapItems,
15838
16417
  waitForOutboundPhoneCallOutcome,
15839
16418
  whoamiCommand