@gravity-rail/cli 0.6.0 → 0.7.1

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
@@ -305,6 +305,25 @@ var AdminApi = class extends BaseApiClient {
305
305
  method: "POST"
306
306
  });
307
307
  }
308
+ // Proactive Support: outbound (us→customer) requests + resources
309
+ async createOutboundSupportRequestAsAdmin(data) {
310
+ return this.fetch("admin/support-requests/outbound", {
311
+ method: "POST",
312
+ body: JSON.stringify(data)
313
+ });
314
+ }
315
+ async attachSupportRequestResourceAsAdmin(uuid, data) {
316
+ return this.fetch(`admin/support-requests/${uuid}/resources`, {
317
+ method: "POST",
318
+ body: JSON.stringify(data)
319
+ });
320
+ }
321
+ async revokeSupportRequestResourceAsAdmin(uuid, resourceUuid) {
322
+ return this.fetch(
323
+ `admin/support-requests/${uuid}/resources/${resourceUuid}/revoke`,
324
+ { method: "POST" }
325
+ );
326
+ }
308
327
  // Admin Accounts API
309
328
  async getAccounts(options) {
310
329
  const params = new URLSearchParams();
@@ -318,6 +337,8 @@ var AdminApi = class extends BaseApiClient {
318
337
  if (options.isActive !== void 0) params.append("isActive", options.isActive.toString());
319
338
  if (options.organizationId !== void 0)
320
339
  params.append("organizationId", options.organizationId.toString());
340
+ if (options.workspaceId !== void 0)
341
+ params.append("workspaceId", options.workspaceId.toString());
321
342
  if (options.pendingDeletion !== void 0)
322
343
  params.append("pendingDeletion", options.pendingDeletion.toString());
323
344
  return this.fetch(`admin/accounts?${params.toString()}`);
@@ -1431,6 +1452,45 @@ var AdminApi = class extends BaseApiClient {
1431
1452
  `admin/temporal/workflows/${encoded}${qs ? `?${qs}` : ""}`
1432
1453
  );
1433
1454
  }
1455
+ // ── Customer activity events (cross-workspace operator feed) ──────────────
1456
+ async listCustomerActivityEvents(params = {}) {
1457
+ const qs = new URLSearchParams();
1458
+ if (params.page) qs.set("page", String(params.page));
1459
+ if (params.pageSize) qs.set("pageSize", String(params.pageSize));
1460
+ if (params.status) qs.set("status", params.status);
1461
+ if (params.subjectType) qs.set("subjectType", params.subjectType);
1462
+ if (params.source) qs.set("source", params.source);
1463
+ if (params.severity) qs.set("severity", params.severity);
1464
+ if (params.disposition) qs.set("disposition", params.disposition);
1465
+ if (params.workspaceId !== void 0) qs.set("workspaceId", String(params.workspaceId));
1466
+ if (params.organizationId !== void 0)
1467
+ qs.set("organizationId", String(params.organizationId));
1468
+ if (params.accountId !== void 0) qs.set("accountId", String(params.accountId));
1469
+ const query = qs.toString();
1470
+ return this.fetch(
1471
+ `admin/customer-activity${query ? `?${query}` : ""}`
1472
+ );
1473
+ }
1474
+ async getCustomerActivityEvent(uuid) {
1475
+ return this.fetch(`admin/customer-activity/${uuid}`);
1476
+ }
1477
+ async setCustomerActivityEventDisposition(uuid, data) {
1478
+ return this.fetch(`admin/customer-activity/${uuid}/disposition`, {
1479
+ method: "POST",
1480
+ body: JSON.stringify(data)
1481
+ });
1482
+ }
1483
+ async setCustomerActivityEventStatus(uuid, data) {
1484
+ return this.fetch(`admin/customer-activity/${uuid}/status`, {
1485
+ method: "POST",
1486
+ body: JSON.stringify(data)
1487
+ });
1488
+ }
1489
+ async reclassifyCustomerActivityEvent(uuid) {
1490
+ return this.fetch(`admin/customer-activity/${uuid}/reclassify`, {
1491
+ method: "POST"
1492
+ });
1493
+ }
1434
1494
  };
1435
1495
  function appendTemporalWorkspaceParams(params, options) {
1436
1496
  const seen = /* @__PURE__ */ new Set();
@@ -1454,6 +1514,14 @@ function bindAdminMethods(target, admin) {
1454
1514
  target.updateSupportRequestStatusAsAdmin = admin.updateSupportRequestStatusAsAdmin.bind(admin);
1455
1515
  target.updateSupportRequestAttributesAsAdmin = admin.updateSupportRequestAttributesAsAdmin.bind(admin);
1456
1516
  target.createSupportRequestLinearIssueAsAdmin = admin.createSupportRequestLinearIssueAsAdmin.bind(admin);
1517
+ target.createOutboundSupportRequestAsAdmin = admin.createOutboundSupportRequestAsAdmin.bind(admin);
1518
+ target.attachSupportRequestResourceAsAdmin = admin.attachSupportRequestResourceAsAdmin.bind(admin);
1519
+ target.revokeSupportRequestResourceAsAdmin = admin.revokeSupportRequestResourceAsAdmin.bind(admin);
1520
+ target.listCustomerActivityEvents = admin.listCustomerActivityEvents.bind(admin);
1521
+ target.getCustomerActivityEvent = admin.getCustomerActivityEvent.bind(admin);
1522
+ target.setCustomerActivityEventDisposition = admin.setCustomerActivityEventDisposition.bind(admin);
1523
+ target.setCustomerActivityEventStatus = admin.setCustomerActivityEventStatus.bind(admin);
1524
+ target.reclassifyCustomerActivityEvent = admin.reclassifyCustomerActivityEvent.bind(admin);
1457
1525
  target.getAccounts = admin.getAccounts.bind(admin);
1458
1526
  target.getAccountDetails = admin.getAccountDetails.bind(admin);
1459
1527
  target.getAccountByUuid = admin.getAccountByUuid.bind(admin);
@@ -1626,6 +1694,14 @@ var ApiClient = class extends GravityRailClient {
1626
1694
  updateSupportRequestStatusAsAdmin;
1627
1695
  updateSupportRequestAttributesAsAdmin;
1628
1696
  createSupportRequestLinearIssueAsAdmin;
1697
+ createOutboundSupportRequestAsAdmin;
1698
+ attachSupportRequestResourceAsAdmin;
1699
+ revokeSupportRequestResourceAsAdmin;
1700
+ listCustomerActivityEvents;
1701
+ getCustomerActivityEvent;
1702
+ setCustomerActivityEventDisposition;
1703
+ setCustomerActivityEventStatus;
1704
+ reclassifyCustomerActivityEvent;
1629
1705
  getAccounts;
1630
1706
  getAccountDetails;
1631
1707
  getAccountByUuid;
@@ -2101,9 +2177,78 @@ This workspace requires organization SSO authentication.`);
2101
2177
  });
2102
2178
  }
2103
2179
 
2180
+ // src/lib/pinned-fetch.ts
2181
+ import { Agent, fetch as undiciFetch } from "undici";
2182
+ function createPinnedFetch(apiUrl, apiIp) {
2183
+ if (!apiIp) return fetch;
2184
+ let parsed;
2185
+ try {
2186
+ parsed = new URL(apiUrl);
2187
+ } catch {
2188
+ return fetch;
2189
+ }
2190
+ const hostname = parsed.hostname;
2191
+ const dispatcher = new Agent({
2192
+ connect: {
2193
+ servername: hostname
2194
+ }
2195
+ });
2196
+ return async (input, init) => {
2197
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
2198
+ let reqUrl;
2199
+ try {
2200
+ reqUrl = new URL(url, apiUrl);
2201
+ } catch {
2202
+ return fetch(input, init);
2203
+ }
2204
+ const headers2 = new Headers(init?.headers);
2205
+ if (reqUrl.hostname === hostname) {
2206
+ headers2.set("Host", hostname);
2207
+ reqUrl = new URL(reqUrl.href);
2208
+ reqUrl.hostname = apiIp;
2209
+ }
2210
+ return undiciFetch(reqUrl, {
2211
+ ...init,
2212
+ headers: headers2,
2213
+ dispatcher
2214
+ });
2215
+ };
2216
+ }
2217
+ function withAuditHeaders(fetchFn, headers2) {
2218
+ const { chatId, sessionId, toolCallId } = headers2;
2219
+ if (chatId === void 0 && !sessionId && !toolCallId) {
2220
+ return fetchFn;
2221
+ }
2222
+ return async (input, init) => {
2223
+ const reqHeaders = new Headers(init?.headers);
2224
+ if (chatId !== void 0) {
2225
+ reqHeaders.set("X-GR-Chat-Id", String(chatId));
2226
+ }
2227
+ if (sessionId) {
2228
+ reqHeaders.set("X-GR-Session-Id", sessionId);
2229
+ }
2230
+ if (toolCallId) {
2231
+ reqHeaders.set("X-GR-Tool-Call-Id", toolCallId);
2232
+ }
2233
+ return fetchFn(input, { ...init, headers: reqHeaders });
2234
+ };
2235
+ }
2236
+ function createSandboxFetchFromEnv(apiUrl) {
2237
+ const apiIp = process.env.GRAVITY_RAIL_API_IP;
2238
+ let fetchFn = createPinnedFetch(apiUrl, apiIp);
2239
+ const chatIdRaw = process.env.GRAVITY_RAIL_CHAT_ID;
2240
+ const chatId = chatIdRaw ? Number(chatIdRaw) : void 0;
2241
+ return withAuditHeaders(fetchFn, {
2242
+ chatId: chatId !== void 0 && Number.isFinite(chatId) ? chatId : void 0,
2243
+ sessionId: process.env.GRAVITY_RAIL_SESSION_ID,
2244
+ toolCallId: process.env.GRAVITY_RAIL_TOOL_CALL_ID
2245
+ });
2246
+ }
2247
+
2104
2248
  // src/lib/context.ts
2105
2249
  function makeApiClient(accessToken, apiUrl) {
2106
- const apiClient = new ApiClient(accessToken, apiUrl);
2250
+ const fetchFn = createSandboxFetchFromEnv(apiUrl);
2251
+ const apiClient = new ApiClient(accessToken, apiUrl, fetchFn);
2107
2252
  registerOrgAuthHandler(apiClient, apiUrl, accessToken);
2108
2253
  return apiClient;
2109
2254
  }
@@ -2120,7 +2265,9 @@ async function tryTokenRefresh(ctx, creds) {
2120
2265
  return { ...ctx, accessToken: refreshed.accessToken, apiClient };
2121
2266
  }
2122
2267
  function createContext(opts) {
2123
- const envConfig = getEnvironmentConfig(opts.env);
2268
+ const baseConfig = getEnvironmentConfig(opts.env);
2269
+ const apiUrl = opts.apiUrl || process.env.GRAVITY_RAIL_API_URL || baseConfig.apiUrl;
2270
+ const envConfig = apiUrl === baseConfig.apiUrl ? baseConfig : { ...baseConfig, apiUrl };
2124
2271
  const credentialsManager = new CredentialsManager(homedir(), envConfig.apiUrl);
2125
2272
  return {
2126
2273
  env: opts.env,
@@ -2206,6 +2353,40 @@ async function createPublicAuthenticatedContext(opts) {
2206
2353
 
2207
2354
  // src/lib/input.ts
2208
2355
  import { readFileSync as readFileSync2 } from "fs";
2356
+ var JSON_BODY_ACTIONS = /* @__PURE__ */ new Set([
2357
+ "create",
2358
+ "update",
2359
+ "import",
2360
+ "enroll",
2361
+ "unenroll",
2362
+ "reenroll",
2363
+ "advance",
2364
+ "skip",
2365
+ "send",
2366
+ "review",
2367
+ "submit-answer",
2368
+ "start-scenario",
2369
+ "update-status"
2370
+ ]);
2371
+ function commandNeedsJsonBody(cmd, parsedArgs) {
2372
+ if (parsedArgs.data !== void 0 || parsedArgs.file !== void 0) {
2373
+ return true;
2374
+ }
2375
+ if (cmd.args?.some((a) => a.name === "data" || a.name === "file")) {
2376
+ return true;
2377
+ }
2378
+ if (!cmd.mutates) {
2379
+ return false;
2380
+ }
2381
+ const action = cmd.path[cmd.path.length - 1] ?? "";
2382
+ if (JSON_BODY_ACTIONS.has(action)) {
2383
+ return true;
2384
+ }
2385
+ if (action === "send" || action === "cancel" && cmd.path.length > 1) {
2386
+ return true;
2387
+ }
2388
+ return false;
2389
+ }
2209
2390
  async function readJsonInput(args) {
2210
2391
  const dataStr = args.data;
2211
2392
  const filePath = args.file;
@@ -2236,18 +2417,18 @@ function readStdinIfPiped() {
2236
2417
  return new Promise((resolve, reject) => {
2237
2418
  let data = "";
2238
2419
  let settled = false;
2239
- const finish = (value) => {
2420
+ const finish2 = (value) => {
2240
2421
  if (settled) return;
2241
2422
  settled = true;
2242
2423
  resolve(value);
2243
2424
  };
2244
2425
  const flush = () => {
2245
2426
  if (!data.trim()) {
2246
- finish(void 0);
2427
+ finish2(void 0);
2247
2428
  return;
2248
2429
  }
2249
2430
  try {
2250
- finish(JSON.parse(data));
2431
+ finish2(JSON.parse(data));
2251
2432
  } catch {
2252
2433
  reject(new CLIError("Invalid JSON from stdin"));
2253
2434
  }
@@ -2267,11 +2448,104 @@ function readStdinIfPiped() {
2267
2448
  flush();
2268
2449
  return;
2269
2450
  }
2270
- finish(void 0);
2451
+ finish2(void 0);
2271
2452
  });
2272
2453
  });
2273
2454
  }
2274
2455
 
2456
+ // src/lib/phone-voice.ts
2457
+ var FAILED_TERMINATION_REASONS = /* @__PURE__ */ new Set([
2458
+ "failed",
2459
+ "busy",
2460
+ "no-answer",
2461
+ "canceled",
2462
+ "timeout",
2463
+ "error"
2464
+ ]);
2465
+ var SEED_PHONE_PATTERN = /^\+1212555010\d$/;
2466
+ function sleep(ms) {
2467
+ return new Promise((resolve) => setTimeout(resolve, ms));
2468
+ }
2469
+ function latestOutboundCall(calls) {
2470
+ return calls.filter((c) => !c.inbound).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
2471
+ }
2472
+ function isTerminalFailure(call) {
2473
+ if (call.status === "ERROR") {
2474
+ return true;
2475
+ }
2476
+ const reason = call.terminationReason ?? "";
2477
+ return FAILED_TERMINATION_REASONS.has(reason);
2478
+ }
2479
+ function formatOutboundCallFailure(call, memberId) {
2480
+ const reason = call.terminationReason ?? call.status;
2481
+ const lines = [`Outbound call failed (phoneCallId=${call.id}, terminationReason=${reason}).`];
2482
+ if (reason === "failed") {
2483
+ lines.push(
2484
+ "Twilio rejected the dial \u2014 common causes: invalid/unverified callee number, trial account restrictions, or unreachable DEV_TUNNEL_URL for the answer URL."
2485
+ );
2486
+ }
2487
+ lines.push(
2488
+ `Update the callee phone: gr members update --id ${memberId} --data '{"phone":"+1YOUR_MOBILE"}' --allow-write`
2489
+ );
2490
+ return lines.join("\n");
2491
+ }
2492
+ function isLikelySeedPhoneNumber(phone) {
2493
+ return SEED_PHONE_PATTERN.test(phone);
2494
+ }
2495
+ async function waitForOutboundPhoneCallOutcome(client, workspaceUuid, chatId, memberId, options) {
2496
+ const timeoutMs = options?.timeoutMs ?? 2e4;
2497
+ const intervalMs = options?.intervalMs ?? 500;
2498
+ const deadline = Date.now() + timeoutMs;
2499
+ while (Date.now() < deadline) {
2500
+ const calls2 = await client.getChatPhoneCalls(workspaceUuid, chatId);
2501
+ const outbound2 = latestOutboundCall(calls2);
2502
+ if (!outbound2) {
2503
+ await sleep(intervalMs);
2504
+ continue;
2505
+ }
2506
+ if (isTerminalFailure(outbound2)) {
2507
+ throw new CLIError(formatOutboundCallFailure(outbound2, memberId));
2508
+ }
2509
+ if (outbound2.status === "STARTED") {
2510
+ return outbound2;
2511
+ }
2512
+ if (outbound2.status === "FINISHED") {
2513
+ return outbound2;
2514
+ }
2515
+ await sleep(intervalMs);
2516
+ }
2517
+ const calls = await client.getChatPhoneCalls(workspaceUuid, chatId);
2518
+ const outbound = latestOutboundCall(calls);
2519
+ if (!outbound) {
2520
+ throw new CLIError(
2521
+ `Outbound call was not initiated for chat ${chatId}. Check API logs, DEV_TUNNEL_URL, and that ngrok points at https://api.gravityrail.test:5054.`
2522
+ );
2523
+ }
2524
+ if (isTerminalFailure(outbound)) {
2525
+ throw new CLIError(formatOutboundCallFailure(outbound, memberId));
2526
+ }
2527
+ throw new CLIError(
2528
+ `Timed out after ${timeoutMs / 1e3}s waiting for outbound call result (phoneCallId=${outbound.id}, status=${outbound.status}). The call may still be ringing \u2014 check Twilio console or gr chats phone-calls.`
2529
+ );
2530
+ }
2531
+ async function resolveVoicePhoneNumberId(client, workspaceUuid, explicitId) {
2532
+ if (explicitId !== void 0) {
2533
+ return explicitId;
2534
+ }
2535
+ const defaults = await client.getWorkspaceDefaults(workspaceUuid);
2536
+ if (defaults.defaultPhoneNumberId) {
2537
+ return defaults.defaultPhoneNumberId;
2538
+ }
2539
+ const numbers = await client.getPhoneNumbers(workspaceUuid);
2540
+ const voiceLine = numbers.find((n) => n.enableVoice !== false && !n.isArchived);
2541
+ if (!voiceLine) {
2542
+ throw new CLIError(
2543
+ "No voice-enabled phone number in workspace \u2014 run: yarn dev:twilio-ngrok dev.<you> -w <slug> or pass --phone-number-id"
2544
+ );
2545
+ }
2546
+ return voiceLine.id;
2547
+ }
2548
+
2275
2549
  // src/lib/crud.ts
2276
2550
  function crudCommands(config) {
2277
2551
  const {
@@ -2379,6 +2653,7 @@ function crudCommands(config) {
2379
2653
  mutates: true,
2380
2654
  args: [{ name: "id", description: `${entityName} ID`, required: true }],
2381
2655
  requiresWorkspace,
2656
+ ...config.updateExamples ? { examples: config.updateExamples } : {},
2382
2657
  handler: async (hctx) => {
2383
2658
  if (!hctx.data) throw new CLIError("JSON data required (--data, --file, or stdin)");
2384
2659
  const id = resolveId(hctx);
@@ -2801,11 +3076,13 @@ var CommandRegistry = class {
2801
3076
  const lines = [`${cmd.description}
2802
3077
  `, `Usage: gr ${cmdStr} [options]`];
2803
3078
  if (cmd.requiresWorkspace) {
2804
- lines.push(" Requires: -w <workspace-uuid>");
3079
+ lines.push(" Requires: -w <workspace-uuid> (or --realm <workspace>/<member> in dev)");
2805
3080
  }
2806
3081
  if (cmd.mutates) {
2807
3082
  lines.push(" Requires: --allow-write");
2808
3083
  }
3084
+ const action = cmd.path[cmd.path.length - 1] ?? "";
3085
+ const showsJsonHints = action === "create" || action === "update" || cmd.args?.some((a) => a.name === "data" || a.name === "file");
2809
3086
  if (cmd.args && cmd.args.length > 0) {
2810
3087
  lines.push("", "Arguments:");
2811
3088
  for (const arg of cmd.args) {
@@ -2814,6 +3091,12 @@ var CommandRegistry = class {
2814
3091
  lines.push(` ${flag.padEnd(28)} ${arg.description}${req}`);
2815
3092
  }
2816
3093
  }
3094
+ if (showsJsonHints) {
3095
+ lines.push("", "JSON body (one of):");
3096
+ lines.push(" -d, --data <json> Inline JSON");
3097
+ lines.push(" -f, --file <path> JSON file");
3098
+ lines.push(" stdin Pipe JSON when not a TTY");
3099
+ }
2817
3100
  if (cmd.examples && cmd.examples.length > 0) {
2818
3101
  lines.push("", "Examples:");
2819
3102
  for (const ex of cmd.examples) {
@@ -2822,6 +3105,14 @@ var CommandRegistry = class {
2822
3105
  }
2823
3106
  return lines.join("\n") + "\n";
2824
3107
  }
3108
+ /** All registered domains (for docs generation and introspection). */
3109
+ getDomains() {
3110
+ return this.domains;
3111
+ }
3112
+ /** All registered commands (for docs generation and introspection). */
3113
+ getAllCommands() {
3114
+ return [...this.commands.values()];
3115
+ }
2825
3116
  getTopLevelHelp() {
2826
3117
  const lines = ["Usage: gr <command> [options]\n", "Commands:"];
2827
3118
  const seen = /* @__PURE__ */ new Set();
@@ -2842,6 +3133,12 @@ var CommandRegistry = class {
2842
3133
  "",
2843
3134
  "Global Options:",
2844
3135
  " --env <env> Environment: dev, staging, prod (default: dev)",
3136
+ " --realm <ws>/<member> Dev auth shortcut (dev only; sets -w automatically)",
3137
+ " --api-url <url> API base URL (or GRAVITY_RAIL_API_URL)",
3138
+ " -w, --workspace <uuid> Workspace UUID",
3139
+ " -d, --data <json> JSON body (create/update and other write commands)",
3140
+ " -f, --file <path> JSON body from file",
3141
+ " --allow-write Required for create/update/delete/archive",
2845
3142
  " -o, --output-format <fmt> Output format: json, jsonl, table (default: auto)",
2846
3143
  " -v, --verbose Verbose output to stderr",
2847
3144
  " --help Show help",
@@ -3368,7 +3665,11 @@ var membersDomain = {
3368
3665
  deleteMethod: "deleteMember",
3369
3666
  archiveMethod: "archiveMember",
3370
3667
  unarchiveMethod: "unarchiveMember",
3371
- columns: COLUMNS3
3668
+ columns: COLUMNS3,
3669
+ updateExamples: [
3670
+ `yarn gr members update -w $UUID --id 2 --data '{"phone":"+15551234567"}' --allow-write`,
3671
+ "yarn gr members update --realm acme-demo/acme-admin --id 2 --file /tmp/member.json --allow-write"
3672
+ ]
3372
3673
  }),
3373
3674
  // Current member
3374
3675
  {
@@ -3522,6 +3823,31 @@ var membersDomain = {
3522
3823
  ]
3523
3824
  };
3524
3825
 
3826
+ // src/domains/member-roles.ts
3827
+ var ROLE_COLUMNS2 = [
3828
+ { header: "ID", getValue: (item) => String(item.id ?? "") },
3829
+ { header: "NAME", getValue: (item) => item.name ?? "" },
3830
+ { header: "DESCRIPTION", getValue: (item) => String(item.description ?? "").slice(0, 40) }
3831
+ ];
3832
+ var memberRolesDomain = {
3833
+ name: "member-roles",
3834
+ description: "Workspace member roles (alias for gr members roles). Use when resolving anonymousMemberRoleId / signupMemberRoleId.",
3835
+ commands: crudCommands({
3836
+ prefix: ["member-roles"],
3837
+ entityName: "member role",
3838
+ listMethod: "getMemberRoles",
3839
+ getMethod: "getMemberRole",
3840
+ createMethod: "createMemberRole",
3841
+ updateMethod: "updateMemberRole",
3842
+ deleteMethod: "deleteMemberRole",
3843
+ columns: ROLE_COLUMNS2,
3844
+ createExamples: [
3845
+ "gr member-roles list -w $WID",
3846
+ "gr member-roles get -w $WID --id 2"
3847
+ ]
3848
+ })
3849
+ };
3850
+
3525
3851
  // src/domains/tasks.ts
3526
3852
  var COLUMNS4 = [
3527
3853
  COL.id(),
@@ -4188,8 +4514,18 @@ var workspacesDomain = {
4188
4514
  `organizationId is required in --data (e.g. --data '{"organizationId": 1, "name": "My Workspace"}')`
4189
4515
  );
4190
4516
  }
4191
- const result = await requireAuth(ctx).apiClient.createWorkspace(payload);
4192
- outputSingle(result, ctx, COLUMNS7);
4517
+ try {
4518
+ const result = await requireAuth(ctx).apiClient.createWorkspace(payload);
4519
+ outputSingle(result, ctx, COLUMNS7);
4520
+ } catch (err) {
4521
+ const statusCode = err?.statusCode;
4522
+ if (statusCode === 405) {
4523
+ throw new CLIError(
4524
+ '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)'
4525
+ );
4526
+ }
4527
+ throw err;
4528
+ }
4193
4529
  }
4194
4530
  },
4195
4531
  // Defaults
@@ -5226,11 +5562,14 @@ var RULE_COLUMNS = [
5226
5562
  { header: "EVENT", getValue: (item) => item.eventType ?? "" },
5227
5563
  { header: "ACTIVE", getValue: (item) => String(item.active ?? "") }
5228
5564
  ];
5229
- var EVENT_COLUMNS2 = [
5565
+ var ROUTINE_COLUMNS = [
5230
5566
  COL.id(),
5231
5567
  COL.name(),
5232
- { header: "SLUG", getValue: (item) => item.slug ?? "" },
5233
- COL.status()
5568
+ { header: "MODE", getValue: (item) => item.scheduleMode ?? "" },
5569
+ { header: "CRON", getValue: (item) => item.cronString ?? "" },
5570
+ { header: "TZ", getValue: (item) => item.timezone ?? "UTC" },
5571
+ { header: "ENABLED", getValue: (item) => String(item.scheduleEnabled ?? "") },
5572
+ { header: "NEXT RUN", getValue: (item) => item.nextRun ?? "" }
5234
5573
  ];
5235
5574
  var eventRulesCrudNoList = crudCommands({
5236
5575
  prefix: ["events", "rules"],
@@ -5399,7 +5738,15 @@ var eventsDomain = {
5399
5738
  updateMethod: "updateRoutine",
5400
5739
  deleteMethod: "deleteRoutine",
5401
5740
  paginationSupport: "full",
5402
- columns: EVENT_COLUMNS2
5741
+ columns: ROUTINE_COLUMNS,
5742
+ createExamples: [
5743
+ "Daily 7am check-in in the member\u2019s timezone (cron is interpreted in `timezone`):",
5744
+ `gr routines create -w $WID --allow-write --data '{"name":"AM check-in",`,
5745
+ ' "scheduleEnabled":true,"scheduleMode":"CRON","cronString":"0 7 * * *",',
5746
+ ` "timezone":"America/Los_Angeles","targetMemberId":1,"workflowId":2}'`,
5747
+ " (timezone is an IANA name; omit it to default to the workspace timezone, else UTC.",
5748
+ ' Without it, "0 7 * * *" fires at 07:00 UTC, not 7am local.)'
5749
+ ]
5403
5750
  }),
5404
5751
  {
5405
5752
  path: ["routines", "run"],
@@ -5446,6 +5793,232 @@ var eventsDomain = {
5446
5793
  ]
5447
5794
  };
5448
5795
 
5796
+ // src/domains/experiments.ts
5797
+ var EXPERIMENT_COLUMNS = [
5798
+ COL.id(),
5799
+ { header: "UUID", getValue: (item) => (item.uuid ?? "").slice(0, 8) },
5800
+ { header: "SLUG", getValue: (item) => item.slug ?? "" },
5801
+ COL.name(),
5802
+ COL.status(),
5803
+ {
5804
+ header: "ASSIGNED",
5805
+ getValue: (item) => item.assignmentCount != null ? String(item.assignmentCount) : "0"
5806
+ },
5807
+ COL.created()
5808
+ ];
5809
+ var GROUP_COLUMNS = [
5810
+ COL.id(),
5811
+ { header: "SLUG", getValue: (item) => item.slug ?? "" },
5812
+ COL.name(),
5813
+ {
5814
+ header: "WEIGHT",
5815
+ getValue: (item) => item.allocationWeight != null ? String(item.allocationWeight) : ""
5816
+ },
5817
+ {
5818
+ header: "CONTROL",
5819
+ getValue: (item) => item.isControl ? "yes" : ""
5820
+ },
5821
+ { header: "ORDER", getValue: (item) => String(item.order ?? "") }
5822
+ ];
5823
+ var STATS_COLUMNS = [
5824
+ { header: "GROUP_ID", getValue: (item) => String(item.groupId ?? "") },
5825
+ { header: "SLUG", getValue: (item) => item.groupSlug ?? "" },
5826
+ { header: "NAME", getValue: (item) => item.groupName ?? "" },
5827
+ {
5828
+ header: "CONTROL",
5829
+ getValue: (item) => item.isControl ? "yes" : ""
5830
+ },
5831
+ {
5832
+ header: "ASSIGNED",
5833
+ getValue: (item) => item.assignedCount != null ? String(item.assignedCount) : "0"
5834
+ },
5835
+ {
5836
+ header: "EXPOSED",
5837
+ getValue: (item) => item.exposedCount != null ? String(item.exposedCount) : "0"
5838
+ }
5839
+ ];
5840
+ var MEMBER_ASSIGNMENT_COLUMNS = [
5841
+ COL.id(),
5842
+ {
5843
+ header: "EXPERIMENT",
5844
+ getValue: (item) => item.experimentSlug ?? String(item.experimentId ?? "")
5845
+ },
5846
+ {
5847
+ header: "GROUP",
5848
+ getValue: (item) => item.groupSlug ?? String(item.groupId ?? "")
5849
+ },
5850
+ { header: "ASSIGNED", getValue: (item) => item.assignedAt ?? "" },
5851
+ { header: "SOURCE", getValue: (item) => item.assignmentSource ?? "" }
5852
+ ];
5853
+ var experimentsDomain = {
5854
+ name: "experiments",
5855
+ description: "Manage member A/B experiments, groups, and reporting",
5856
+ commands: [
5857
+ {
5858
+ path: ["experiments", "list"],
5859
+ description: "List experiments",
5860
+ requiresWorkspace: true,
5861
+ args: [
5862
+ ...LIST_PAGINATION_ARGS,
5863
+ {
5864
+ name: "include-archived",
5865
+ description: "Include archived experiments",
5866
+ required: false
5867
+ }
5868
+ ],
5869
+ handler: async ({ ctx, workspace, args }) => {
5870
+ const includeArchived = args["include-archived"] === "true";
5871
+ const pag = parsePaginationArgs(args);
5872
+ const result = await requireAuth(ctx).apiClient.getExperiments(workspace, {
5873
+ ...pag.page ? { page: pag.page } : {},
5874
+ ...pag.pageSize ? { pageSize: pag.pageSize } : {},
5875
+ includeArchived
5876
+ });
5877
+ outputData(listResult(result, true), ctx, EXPERIMENT_COLUMNS);
5878
+ }
5879
+ },
5880
+ {
5881
+ path: ["experiments", "get"],
5882
+ description: "Get an experiment by ID",
5883
+ requiresWorkspace: true,
5884
+ args: [{ name: "id", description: "Experiment ID", required: true }],
5885
+ handler: async ({ ctx, workspace, args }) => {
5886
+ const result = await requireAuth(ctx).apiClient.getExperiment(
5887
+ workspace,
5888
+ requireNumber(args, "id")
5889
+ );
5890
+ outputSingle(result, ctx);
5891
+ }
5892
+ },
5893
+ {
5894
+ path: ["experiments", "create"],
5895
+ description: "Create a draft experiment (pass JSON via --data)",
5896
+ mutates: true,
5897
+ requiresWorkspace: true,
5898
+ handler: async ({ ctx, workspace, data }) => {
5899
+ if (!data) throw new CLIError("JSON data required (--data, --file, or stdin)");
5900
+ const result = await requireAuth(ctx).apiClient.createExperiment(
5901
+ workspace,
5902
+ data
5903
+ );
5904
+ outputSingle(result, ctx, EXPERIMENT_COLUMNS);
5905
+ }
5906
+ },
5907
+ {
5908
+ path: ["experiments", "update"],
5909
+ description: "Update an experiment (pass JSON via --data)",
5910
+ mutates: true,
5911
+ requiresWorkspace: true,
5912
+ args: [{ name: "id", description: "Experiment ID", required: true }],
5913
+ handler: async ({ ctx, workspace, args, data }) => {
5914
+ if (!data) throw new CLIError("JSON data required (--data, --file, or stdin)");
5915
+ const result = await requireAuth(ctx).apiClient.updateExperiment(
5916
+ workspace,
5917
+ requireNumber(args, "id"),
5918
+ data
5919
+ );
5920
+ outputSingle(result, ctx, EXPERIMENT_COLUMNS);
5921
+ }
5922
+ },
5923
+ {
5924
+ path: ["experiments", "delete"],
5925
+ description: "Delete a draft experiment or archive a running one",
5926
+ mutates: true,
5927
+ requiresWorkspace: true,
5928
+ args: [{ name: "id", description: "Experiment ID", required: true }],
5929
+ handler: async ({ ctx, workspace, args }) => {
5930
+ const result = await requireAuth(ctx).apiClient.deleteExperiment(
5931
+ workspace,
5932
+ requireNumber(args, "id")
5933
+ );
5934
+ process.stdout.write(`${result.status} experiment ${args["id"]}
5935
+ `);
5936
+ }
5937
+ },
5938
+ {
5939
+ path: ["experiments", "stats"],
5940
+ description: "Get assignment and exposure counts by group",
5941
+ requiresWorkspace: true,
5942
+ args: [{ name: "id", description: "Experiment ID", required: true }],
5943
+ handler: async ({ ctx, workspace, args }) => {
5944
+ const result = await requireAuth(ctx).apiClient.getExperimentStats(
5945
+ workspace,
5946
+ requireNumber(args, "id")
5947
+ );
5948
+ outputData(result.groups, ctx, STATS_COLUMNS);
5949
+ }
5950
+ },
5951
+ {
5952
+ path: ["experiments", "groups", "create"],
5953
+ description: "Create a group on an experiment (pass JSON via --data)",
5954
+ mutates: true,
5955
+ requiresWorkspace: true,
5956
+ args: [{ name: "experiment-id", description: "Experiment ID", required: true }],
5957
+ handler: async ({ ctx, workspace, args, data }) => {
5958
+ if (!data) throw new CLIError("JSON data required (--data, --file, or stdin)");
5959
+ const result = await requireAuth(ctx).apiClient.createExperimentGroup(
5960
+ workspace,
5961
+ requireNumber(args, "experiment-id"),
5962
+ data
5963
+ );
5964
+ outputSingle(result, ctx, GROUP_COLUMNS);
5965
+ }
5966
+ },
5967
+ {
5968
+ path: ["experiments", "groups", "update"],
5969
+ description: "Update a group (pass JSON via --data)",
5970
+ mutates: true,
5971
+ requiresWorkspace: true,
5972
+ args: [
5973
+ { name: "experiment-id", description: "Experiment ID", required: true },
5974
+ { name: "group-id", description: "Group ID", required: true }
5975
+ ],
5976
+ handler: async ({ ctx, workspace, args, data }) => {
5977
+ if (!data) throw new CLIError("JSON data required (--data, --file, or stdin)");
5978
+ const result = await requireAuth(ctx).apiClient.updateExperimentGroup(
5979
+ workspace,
5980
+ requireNumber(args, "experiment-id"),
5981
+ requireNumber(args, "group-id"),
5982
+ data
5983
+ );
5984
+ outputSingle(result, ctx, GROUP_COLUMNS);
5985
+ }
5986
+ },
5987
+ {
5988
+ path: ["experiments", "groups", "delete"],
5989
+ description: "Delete a group with no member assignments",
5990
+ mutates: true,
5991
+ requiresWorkspace: true,
5992
+ args: [
5993
+ { name: "experiment-id", description: "Experiment ID", required: true },
5994
+ { name: "group-id", description: "Group ID", required: true }
5995
+ ],
5996
+ handler: async ({ ctx, workspace, args }) => {
5997
+ await requireAuth(ctx).apiClient.deleteExperimentGroup(
5998
+ workspace,
5999
+ requireNumber(args, "experiment-id"),
6000
+ requireNumber(args, "group-id")
6001
+ );
6002
+ process.stdout.write(`Deleted group ${args["group-id"]}
6003
+ `);
6004
+ }
6005
+ },
6006
+ {
6007
+ path: ["experiments", "members", "list"],
6008
+ description: "List experiment assignments for a member",
6009
+ requiresWorkspace: true,
6010
+ args: [{ name: "member-id", description: "Member ID", required: true }],
6011
+ handler: async ({ ctx, workspace, args }) => {
6012
+ const result = await requireAuth(ctx).apiClient.getMemberExperiments(
6013
+ workspace,
6014
+ requireNumber(args, "member-id")
6015
+ );
6016
+ outputData(result, ctx, MEMBER_ASSIGNMENT_COLUMNS);
6017
+ }
6018
+ }
6019
+ ]
6020
+ };
6021
+
5449
6022
  // src/domains/files.ts
5450
6023
  var COLUMNS11 = [
5451
6024
  COL.id(),
@@ -7477,9 +8050,9 @@ function toCamelMessage(raw) {
7477
8050
  id: raw.id,
7478
8051
  role: raw.role,
7479
8052
  content: raw.content ?? "",
7480
- createdAt: raw.created_at ?? null,
7481
- toolCalls: raw.tool_calls ?? null,
7482
- toolCallId: raw.tool_call_id ?? null
8053
+ createdAt: raw.createdAt ?? raw.created_at ?? null,
8054
+ toolCalls: raw.toolCalls ?? raw.tool_calls ?? null,
8055
+ toolCallId: raw.toolCallId ?? raw.tool_call_id ?? null
7483
8056
  };
7484
8057
  }
7485
8058
  async function listConciergeChats(ctx) {
@@ -7502,20 +8075,72 @@ async function getConciergeChat(ctx, chatId) {
7502
8075
  if (!res.ok) throw new Error(`Failed to get concierge chat ${chatId}: ${res.status}`);
7503
8076
  return toCamelChat(await res.json());
7504
8077
  }
7505
- async function getConciergeChatMessages(ctx, chatId) {
7506
- const res = await fetch(`${baseUrl2(ctx)}/chats/${chatId}/messages`, {
7507
- headers: authHeaders(ctx)
8078
+ async function originateConciergeCall(ctx, kind = "concierge_setup") {
8079
+ const res = await fetch(`${baseUrl2(ctx)}/calls`, {
8080
+ method: "POST",
8081
+ headers: authHeaders(ctx),
8082
+ body: JSON.stringify({ kind })
7508
8083
  });
7509
- if (!res.ok)
7510
- throw new Error(`Failed to get concierge messages for chat ${chatId}: ${res.status}`);
7511
- const data = await res.json();
7512
- return data.map(toCamelMessage);
8084
+ if (res.status === 412) {
8085
+ throw new Error("Your account phone is not verified. Run `gr account verify-phone` first.");
8086
+ }
8087
+ if (!res.ok) {
8088
+ const detail = await res.text();
8089
+ throw new Error(`Failed to start concierge call: ${res.status} ${detail}`);
8090
+ }
8091
+ return await res.json();
7513
8092
  }
7514
-
7515
- // src/domains/concierge.ts
7516
- var CHAT_COLUMNS = [
7517
- COL.id(),
7518
- COL.uuid(),
8093
+ async function originateConciergeSms(ctx, kind = "concierge_setup") {
8094
+ const res = await fetch(`${baseUrl2(ctx)}/sms`, {
8095
+ method: "POST",
8096
+ headers: authHeaders(ctx),
8097
+ body: JSON.stringify({ kind })
8098
+ });
8099
+ if (res.status === 412) {
8100
+ throw new Error("Your account phone is not verified. Run `gr account verify-phone` first.");
8101
+ }
8102
+ if (!res.ok) {
8103
+ const detail = await res.text();
8104
+ throw new Error(`Failed to start concierge SMS: ${res.status} ${detail}`);
8105
+ }
8106
+ return await res.json();
8107
+ }
8108
+ async function startAccountPhoneVerify(ctx, phone) {
8109
+ const res = await fetch(`${ctx.envConfig.apiUrl}/api/v2/account/phone/verify/start`, {
8110
+ method: "POST",
8111
+ headers: authHeaders(ctx),
8112
+ body: JSON.stringify({ phone })
8113
+ });
8114
+ if (!res.ok) {
8115
+ const detail = await res.text();
8116
+ throw new Error(`Failed to send verification code: ${res.status} ${detail}`);
8117
+ }
8118
+ }
8119
+ async function confirmAccountPhoneVerify(ctx, code) {
8120
+ const res = await fetch(`${ctx.envConfig.apiUrl}/api/v2/account/phone/verify/confirm`, {
8121
+ method: "POST",
8122
+ headers: authHeaders(ctx),
8123
+ body: JSON.stringify({ code })
8124
+ });
8125
+ if (!res.ok) {
8126
+ const detail = await res.text();
8127
+ throw new Error(`Failed to confirm verification code: ${res.status} ${detail}`);
8128
+ }
8129
+ }
8130
+ async function getConciergeChatMessages(ctx, chatId) {
8131
+ const res = await fetch(`${baseUrl2(ctx)}/chats/${chatId}/messages`, {
8132
+ headers: authHeaders(ctx)
8133
+ });
8134
+ if (!res.ok)
8135
+ throw new Error(`Failed to get concierge messages for chat ${chatId}: ${res.status}`);
8136
+ const data = await res.json();
8137
+ return data.map(toCamelMessage);
8138
+ }
8139
+
8140
+ // src/domains/concierge.ts
8141
+ var CHAT_COLUMNS = [
8142
+ COL.id(),
8143
+ COL.uuid(),
7519
8144
  { header: "TITLE", getValue: (c) => c.title ?? "" },
7520
8145
  { header: "UPDATED", getValue: (c) => c.updatedAt ?? "" }
7521
8146
  ];
@@ -8658,27 +9283,780 @@ var orgPhoneNumbersDomain = {
8658
9283
  commands: commands2
8659
9284
  };
8660
9285
 
9286
+ // src/lib/phone-numbers/defaults.ts
9287
+ var PHONE_NUMBER_CREATE_DEFAULTS = {
9288
+ allowSignup: false,
9289
+ allowAnonymous: false,
9290
+ restartDelay: 900,
9291
+ enableSms: true,
9292
+ enableVoice: true,
9293
+ assistantEnabled: true,
9294
+ voiceUnavailableMessage: "This phone number is not available for voice calls right now. Please try again later or send a text message.",
9295
+ smsUnavailableMessage: "This phone number is not available for text messages right now. Please try again later.",
9296
+ anonymousRejectionMessage: "Hello. Unfortunately we could not find your phone number in our system. Goodbye.",
9297
+ voicemailGreeting: "Please leave a message after the tone."
9298
+ };
9299
+ function mergePhoneNumberCreateDefaults(data) {
9300
+ return { ...PHONE_NUMBER_CREATE_DEFAULTS, ...data };
9301
+ }
9302
+
9303
+ // src/lib/phone-numbers/field-help.ts
9304
+ var PHONE_NUMBER_FIELD_REFERENCE = `
9305
+ Field reference for --data JSON (create/update):
9306
+
9307
+ Identity & branding
9308
+ name (string) Display label
9309
+ brandName, brandDescription, brandWebsite (string, optional)
9310
+
9311
+ Inbound routing (phone_numbers.defaultWorkflowId = INBOUND workflow)
9312
+ defaultWorkflowId (number|null) Workflow for inbound voice/SMS chats (required for voice)
9313
+ allowSignup (bool, default false) Unknown callers can start signup flow
9314
+ signupMemberRoleId (number) Required when allowSignup=true
9315
+ allowAnonymous (bool, default false) Unknown callers get anonymous member per call
9316
+ anonymousMemberRoleId (number) Required when allowAnonymous=true
9317
+ restartDelay (number|null, default 900) Seconds before a new inbound conversation starts
9318
+ restartPrompt (string|null)
9319
+
9320
+ Channels
9321
+ enableVoice (bool, default true)
9322
+ enableSms (bool, default true)
9323
+ enableWhatsApp (bool, default false)
9324
+ assistantEnabled (bool, default true)
9325
+
9326
+ Caller-facing messages
9327
+ voiceUnavailableMessage (string) Played when voice cannot be handled
9328
+ voiceGreetingMessage (string) Played before AI connects
9329
+ anonymousRejectionMessage (string) Unknown caller denied (TTS)
9330
+ smsUnavailableMessage (string)
9331
+ voicemailGreeting (string)
9332
+
9333
+ Work modes (voiceWorkMode / smsWorkMode)
9334
+ always_forward | forward_off_hours | always_message | message_off_hours | voicemail | voicemail_off_hours
9335
+ voiceForwardNumber, smsForwardNumber, voiceOffHoursMessage, smsOffHoursMessage
9336
+
9337
+ Provider link
9338
+ providerPhoneUuid (string) Org provider phone UUID \u2014 use gr phone-numbers link after org assign
9339
+
9340
+ Constraints
9341
+ allowAnonymous=true \u2192 anonymousMemberRoleId required
9342
+ allowSignup=true \u2192 signupMemberRoleId required
9343
+
9344
+ Outbound (different field on workflows, not this resource):
9345
+ workflows.defaultPhoneNumberId = OUTBOUND origin for that workflow
9346
+ Event rules may reference phoneNumberUUID (workspace phone uuid string)
9347
+
9348
+ List roles: gr member-roles list -w $WID
9349
+ Provisioning: gr phone-numbers setup --help
9350
+ `.trim();
9351
+
9352
+ // src/lib/phone-numbers/validation-hints.ts
9353
+ var HINT_RULES = [
9354
+ {
9355
+ pattern: /anonymousMemberRoleId.*required/i,
9356
+ hint: "Fix: set anonymousMemberRoleId (gr member-roles list -w $WID) or set allowAnonymous=false."
9357
+ },
9358
+ {
9359
+ pattern: /signupMemberRoleId/i,
9360
+ hint: "Fix: set signupMemberRoleId from gr member-roles list -w $WID, or disable allowSignup."
9361
+ },
9362
+ {
9363
+ pattern: /memberRoleAccess/i,
9364
+ hint: "Fix: memberRoleAccess entries need { memberRoleId, accessMode }. List roles: gr member-roles list -w $WID"
9365
+ },
9366
+ {
9367
+ pattern: /defaultWorkflowId/i,
9368
+ hint: "Fix: defaultWorkflowId is the INBOUND workflow id (gr workflows list -w $WID). Outbound uses workflows.defaultPhoneNumberId."
9369
+ },
9370
+ {
9371
+ pattern: /providerPhoneUuid|provider_phone/i,
9372
+ hint: "Fix: link org number \u2014 gr org phone-numbers assign \u2026 then gr phone-numbers link --provider-phone-uuid <uuid> --allow-write"
9373
+ }
9374
+ ];
9375
+ function enrichPhoneNumberApiError(err) {
9376
+ const message = err instanceof Error ? err.message : String(err);
9377
+ const extra = [];
9378
+ for (const rule of HINT_RULES) {
9379
+ if (rule.pattern.test(message)) {
9380
+ extra.push(rule.hint);
9381
+ }
9382
+ }
9383
+ if ("errors" in err && Array.isArray(err.errors)) {
9384
+ for (const e of err.errors) {
9385
+ for (const rule of HINT_RULES) {
9386
+ if (rule.pattern.test(e)) {
9387
+ extra.push(rule.hint);
9388
+ }
9389
+ }
9390
+ }
9391
+ }
9392
+ const unique = [...new Set(extra)];
9393
+ const body = unique.length > 0 ? `${message}
9394
+
9395
+ ${unique.map((h) => `${h}`).join("\n")}` : message;
9396
+ throw new CLIError(body);
9397
+ }
9398
+
9399
+ // src/lib/phone-numbers/warnings.ts
9400
+ function collectPhoneNumberWarnings(phone) {
9401
+ const warnings = [];
9402
+ const enableVoice = phone.enableVoice === true;
9403
+ const defaultWorkflowId = phone.defaultWorkflowId;
9404
+ const isMapped = phone.isMapped === true;
9405
+ const allowAnonymous = phone.allowAnonymous === true;
9406
+ const anonymousMemberRoleId = phone.anonymousMemberRoleId;
9407
+ if (!isMapped) {
9408
+ warnings.push({
9409
+ severity: "warn",
9410
+ code: "not_mapped",
9411
+ message: "Phone line is not linked to an org provider number (isMapped=false). Inbound PSTN will not reach this workspace until linked.",
9412
+ fixCommand: "gr org phone-numbers assign \u2026 then gr phone-numbers link -w $WID --id <id> --provider-phone-uuid <uuid> --allow-write"
9413
+ });
9414
+ }
9415
+ if (enableVoice && (defaultWorkflowId === null || defaultWorkflowId === void 0)) {
9416
+ warnings.push({
9417
+ severity: "error",
9418
+ code: "voice_no_inbound_workflow",
9419
+ message: "Voice is enabled but defaultWorkflowId (inbound route) is unset. Known callers may hear a rejection message; misconfiguration can drop calls without a call log.",
9420
+ fixCommand: `gr phone-numbers update --id <id> --data '{"defaultWorkflowId":<workflowId>}' --allow-write`
9421
+ });
9422
+ }
9423
+ if (enableVoice && !phone.voiceUnavailableMessage) {
9424
+ warnings.push({
9425
+ severity: "warn",
9426
+ code: "missing_voice_unavailable_message",
9427
+ message: "voiceUnavailableMessage is unset. If voice cannot be handled, callers may get a generic or empty response."
9428
+ });
9429
+ }
9430
+ if (allowAnonymous && (anonymousMemberRoleId === null || anonymousMemberRoleId === void 0)) {
9431
+ warnings.push({
9432
+ severity: "error",
9433
+ code: "anonymous_missing_role",
9434
+ message: "allowAnonymous=true requires anonymousMemberRoleId.",
9435
+ fixCommand: "gr member-roles list -w $WID # then set anonymousMemberRoleId in update JSON"
9436
+ });
9437
+ }
9438
+ if (!allowAnonymous && !phone.allowSignup && enableVoice && !phone.anonymousRejectionMessage) {
9439
+ warnings.push({
9440
+ severity: "warn",
9441
+ code: "missing_anonymous_rejection_message",
9442
+ message: "Unknown callers are rejected (allowAnonymous=false, allowSignup=false) but anonymousRejectionMessage is unset (platform default TTS will be used)."
9443
+ });
9444
+ }
9445
+ if (phone.allowSignup === true && !phone.signupMemberRoleId) {
9446
+ warnings.push({
9447
+ severity: "error",
9448
+ code: "signup_missing_role",
9449
+ message: "allowSignup=true requires signupMemberRoleId.",
9450
+ fixCommand: "gr member-roles list -w $WID"
9451
+ });
9452
+ }
9453
+ return warnings;
9454
+ }
9455
+
9456
+ // src/lib/phone-numbers/routing-simulate.ts
9457
+ var VALID_CALLER_ROLES = /* @__PURE__ */ new Set([
9458
+ "anonymous",
9459
+ "member",
9460
+ "operator",
9461
+ "signup",
9462
+ "unknown"
9463
+ ]);
9464
+ function parseCallerRole(value) {
9465
+ if (typeof value !== "string" || value === "") {
9466
+ return void 0;
9467
+ }
9468
+ return VALID_CALLER_ROLES.has(value) ? value : void 0;
9469
+ }
9470
+ function inferCallerRole(phone, opts) {
9471
+ if (opts.asRole) {
9472
+ return opts.asRole;
9473
+ }
9474
+ if (opts.hasMemberMatch) {
9475
+ return "member";
9476
+ }
9477
+ if (phone.allowAnonymous === true) {
9478
+ return "anonymous";
9479
+ }
9480
+ if (phone.allowSignup === true && phone.signupMemberRoleId) {
9481
+ return "signup";
9482
+ }
9483
+ return "anonymous";
9484
+ }
9485
+ function simulateInboundRouting(phone, opts = {}) {
9486
+ const steps = [];
9487
+ const recommendations = [];
9488
+ const isBusinessHours = opts.isBusinessHours ?? true;
9489
+ if (!phone.isMapped) {
9490
+ steps.push({
9491
+ step: "provider_mapping",
9492
+ outcome: "reject",
9493
+ detail: "Number not linked to org provider (isMapped=false)"
9494
+ });
9495
+ return finish("reject_unmapped", false, false, steps, recommendations, opts.asRole);
9496
+ }
9497
+ if (!phone.enableVoice) {
9498
+ steps.push({
9499
+ step: "channel_voice",
9500
+ outcome: "reject",
9501
+ detail: "enableVoice=false \u2014 plays voiceUnavailableMessage or default"
9502
+ });
9503
+ return finish(
9504
+ "voice_disabled_message",
9505
+ false,
9506
+ false,
9507
+ steps,
9508
+ recommendations,
9509
+ opts.asRole
9510
+ );
9511
+ }
9512
+ steps.push({ step: "channel_voice", outcome: "ok" });
9513
+ const callerRole = inferCallerRole(phone, opts);
9514
+ steps.push({ step: "caller_resolution", outcome: callerRole });
9515
+ if (callerRole === "member") {
9516
+ steps.push({ step: "member_access", outcome: "proceed" });
9517
+ } else if (callerRole === "anonymous") {
9518
+ if (!phone.anonymousMemberRoleId) {
9519
+ steps.push({
9520
+ step: "anonymous_role",
9521
+ outcome: "reject",
9522
+ detail: "allowAnonymous without anonymousMemberRoleId"
9523
+ });
9524
+ recommendations.push(
9525
+ "Set anonymousMemberRoleId or disable allowAnonymous (gr member-roles list -w $WID)"
9526
+ );
9527
+ return finish("misconfigured_anonymous", false, false, steps, recommendations, callerRole);
9528
+ }
9529
+ steps.push({ step: "anonymous_member", outcome: "create_ephemeral_member" });
9530
+ } else if (callerRole === "signup") {
9531
+ steps.push({ step: "signup_flow", outcome: "redirect_voice_consent" });
9532
+ return finish("signup_consent_redirect", false, false, steps, recommendations, callerRole);
9533
+ } else if (callerRole === "unknown" || callerRole === "operator") {
9534
+ steps.push({
9535
+ step: "caller_access",
9536
+ outcome: "reject",
9537
+ detail: "Unknown caller; anonymous and signup disabled"
9538
+ });
9539
+ return finish("anonymous_rejection_tts", false, false, steps, recommendations, callerRole);
9540
+ }
9541
+ const voiceWorkMode = phone.voiceWorkMode;
9542
+ if (voiceWorkMode === "always_forward" && phone.voiceForwardNumber) {
9543
+ steps.push({ step: "voice_work_mode", outcome: "forward", detail: voiceWorkMode });
9544
+ return finish("forward_pstn", false, true, steps, recommendations, callerRole);
9545
+ }
9546
+ if (voiceWorkMode === "forward_off_hours" && !isBusinessHours && phone.voiceForwardNumber) {
9547
+ steps.push({ step: "voice_work_mode", outcome: "forward_off_hours" });
9548
+ return finish("forward_pstn", false, true, steps, recommendations, callerRole);
9549
+ }
9550
+ if (voiceWorkMode === "voicemail" || voiceWorkMode === "voicemail_off_hours" && !isBusinessHours) {
9551
+ steps.push({ step: "voice_work_mode", outcome: "voicemail" });
9552
+ return finish("voicemail_record", true, true, steps, recommendations, callerRole);
9553
+ }
9554
+ if ((voiceWorkMode === "always_message" || voiceWorkMode === "message_off_hours") && phone.voiceOffHoursMessage) {
9555
+ steps.push({ step: "voice_work_mode", outcome: "play_message" });
9556
+ return finish("play_message_hangup", false, false, steps, recommendations, callerRole);
9557
+ }
9558
+ steps.push({ step: "voice_work_mode", outcome: "ai_path" });
9559
+ const workflowId = phone.defaultWorkflowId;
9560
+ if (workflowId === null || workflowId === void 0) {
9561
+ steps.push({
9562
+ step: "inbound_workflow",
9563
+ outcome: "reject",
9564
+ detail: "defaultWorkflowId unset \u2014 plays unavailable message; call log should be created"
9565
+ });
9566
+ recommendations.push(
9567
+ `Set defaultWorkflowId: gr phone-numbers update --id <id> --data '{"defaultWorkflowId":<wf>}' --allow-write`
9568
+ );
9569
+ return finish("no_inbound_workflow", false, true, steps, recommendations, callerRole);
9570
+ }
9571
+ steps.push({ step: "inbound_workflow", outcome: "ok", detail: `workflowId=${workflowId}` });
9572
+ steps.push({ step: "chat_create", outcome: "create_phone_voice_chat" });
9573
+ steps.push({ step: "assistant", outcome: "connect_media_stream" });
9574
+ return finish("ai_voice_chat", true, true, steps, recommendations, callerRole);
9575
+ }
9576
+ function finish(code, createsChat, createsPhoneCall, steps, recommendations, callerRole) {
9577
+ const labels = {
9578
+ reject_unmapped: "Reject \u2014 not mapped to provider",
9579
+ voice_disabled_message: "Play voice-unavailable message",
9580
+ misconfigured_anonymous: "Reject \u2014 anonymous misconfigured",
9581
+ signup_consent_redirect: "Redirect to voice signup consent",
9582
+ anonymous_rejection_tts: "Play anonymous rejection TTS, hang up",
9583
+ forward_pstn: "Forward to voiceForwardNumber",
9584
+ voicemail_record: "Record voicemail",
9585
+ play_message_hangup: "Play off-hours message, hang up",
9586
+ no_inbound_workflow: "Play unavailable message (no inbound workflow)",
9587
+ ai_voice_chat: "Create chat + connect AI voice"
9588
+ };
9589
+ return {
9590
+ callerRole: callerRole ?? "unknown",
9591
+ steps,
9592
+ finalBehavior: labels[code] ?? code,
9593
+ createsChat,
9594
+ createsPhoneCall,
9595
+ recommendations
9596
+ };
9597
+ }
9598
+ function formatRoutingSimulation(sim, phone) {
9599
+ const lines = [];
9600
+ lines.push(`Phone ${phone.id} (${String(phone.number ?? "").slice(0, 6)}\u2026)`);
9601
+ lines.push(`Caller role (simulated): ${sim.callerRole}`);
9602
+ lines.push("");
9603
+ lines.push("Routing trace:");
9604
+ for (const s of sim.steps) {
9605
+ const detail = s.detail ? ` \u2014 ${s.detail}` : "";
9606
+ lines.push(` \u2022 ${s.step}: ${s.outcome}${detail}`);
9607
+ }
9608
+ lines.push("");
9609
+ lines.push(`Final: ${sim.finalBehavior}`);
9610
+ lines.push(`Creates chat: ${sim.createsChat ? "yes" : "no"}`);
9611
+ lines.push(`Creates phone call record: ${sim.createsPhoneCall ? "yes" : "no"}`);
9612
+ if (sim.recommendations.length > 0) {
9613
+ lines.push("");
9614
+ lines.push("Recommendations:");
9615
+ for (const r of sim.recommendations) {
9616
+ lines.push(` \u2192 ${r}`);
9617
+ }
9618
+ }
9619
+ return lines.join("\n");
9620
+ }
9621
+
9622
+ // src/lib/phone-numbers/behavior-matrix.ts
9623
+ var ROLE_ROWS = ["anonymous", "member", "signup", "operator"];
9624
+ function roleExperience(phone, role) {
9625
+ const sim = simulateInboundRouting(phone, {
9626
+ asRole: role,
9627
+ hasMemberMatch: role === "member"
9628
+ });
9629
+ return sim.finalBehavior;
9630
+ }
9631
+ function formatPhoneNumberBehaviorMatrix(phone, outboundUsers, warnings = collectPhoneNumberWarnings(phone)) {
9632
+ const lines = [];
9633
+ const maskedNumber = typeof phone.number === "string" && phone.number.length > 4 ? `${phone.number.slice(0, 6)}\u2026` : "(no number)";
9634
+ lines.push(`Phone number ${phone.id} \u2014 ${phone.name ?? "(unnamed)"} \u2014 ${maskedNumber}`);
9635
+ lines.push(`UUID: ${phone.uuid} | Mapped: ${phone.isMapped ? "yes" : "no"}`);
9636
+ lines.push("");
9637
+ lines.push("## Channels");
9638
+ lines.push(` Voice: ${phone.enableVoice ? "on" : "off"} SMS: ${phone.enableSms ? "on" : "off"} WhatsApp: ${phone.enableWhatsApp ? "on" : "off"}`);
9639
+ lines.push(` Assistant: ${phone.assistantEnabled ? "on" : "off"}`);
9640
+ lines.push("");
9641
+ lines.push("## Inbound route");
9642
+ lines.push(
9643
+ ` defaultWorkflowId (inbound): ${phone.defaultWorkflowId ?? "null"} restartDelay: ${phone.restartDelay ?? "null"}s`
9644
+ );
9645
+ lines.push(
9646
+ ` allowAnonymous: ${phone.allowAnonymous} (role ${phone.anonymousMemberRoleId ?? "\u2014"}) allowSignup: ${phone.allowSignup} (role ${phone.signupMemberRoleId ?? "\u2014"})`
9647
+ );
9648
+ if (phone.voiceWorkMode) {
9649
+ lines.push(` voiceWorkMode: ${phone.voiceWorkMode}`);
9650
+ }
9651
+ lines.push("");
9652
+ lines.push("## Outbound users");
9653
+ if (outboundUsers.length === 0) {
9654
+ lines.push(" (none \u2014 no workflow defaultPhoneNumberId or event rule phoneNumberUUID)");
9655
+ } else {
9656
+ for (const u of outboundUsers) {
9657
+ lines.push(` \u2022 [${u.kind}] ${u.name} (id ${u.id})${u.detail ? ` \u2014 ${u.detail}` : ""}`);
9658
+ }
9659
+ }
9660
+ lines.push("");
9661
+ lines.push("## Role experience (simulated inbound)");
9662
+ for (const role of ROLE_ROWS) {
9663
+ lines.push(` ${role.padEnd(10)} \u2192 ${roleExperience(phone, role)}`);
9664
+ }
9665
+ lines.push("");
9666
+ if (warnings.length > 0) {
9667
+ lines.push("## Warnings");
9668
+ for (const w of warnings) {
9669
+ const tag = w.severity === "error" ? "ERROR" : "WARN";
9670
+ lines.push(` [${tag}] ${w.message}`);
9671
+ if (w.fixCommand) {
9672
+ lines.push(` Fix: ${w.fixCommand}`);
9673
+ }
9674
+ }
9675
+ lines.push("");
9676
+ }
9677
+ lines.push("Tip: gr phone-numbers diagnose --id <id> --as +1\u2026 | --raw for full JSON");
9678
+ return lines.join("\n");
9679
+ }
9680
+
9681
+ // src/lib/phone-numbers/outbound-users.ts
9682
+ async function findOutboundPhoneUsers(client, workspace, phone) {
9683
+ const phoneId = phone.id;
9684
+ const phoneUuid = String(phone.uuid ?? "");
9685
+ const users = [];
9686
+ const workflows = await client.getWorkflows(workspace, false);
9687
+ for (const wf of workflows) {
9688
+ if (wf.defaultPhoneNumberId === phoneId) {
9689
+ users.push({
9690
+ kind: "workflow_default",
9691
+ id: wf.id,
9692
+ name: String(wf.name ?? `workflow ${wf.id}`),
9693
+ detail: "workflows.defaultPhoneNumberId (outbound origin)"
9694
+ });
9695
+ }
9696
+ }
9697
+ let page = 1;
9698
+ const pageSize = 100;
9699
+ for (; ; ) {
9700
+ const rulesPage = await client.getEventRules(workspace, { page, pageSize });
9701
+ const list = rulesPage.items;
9702
+ for (const rule of list) {
9703
+ const params = rule.actionParams ?? rule.action_params;
9704
+ if (!params) continue;
9705
+ const pnUuid = params.phoneNumberUUID ?? params.phone_number_uuid;
9706
+ const pnId = params.phoneNumberId ?? params.phone_number_id;
9707
+ if (pnUuid === phoneUuid || pnId === phoneId) {
9708
+ users.push({
9709
+ kind: "event_rule",
9710
+ id: rule.id,
9711
+ name: String(rule.name ?? `rule ${rule.id}`),
9712
+ detail: `action=${String(rule.actionType ?? rule.action_type ?? "")}`
9713
+ });
9714
+ }
9715
+ }
9716
+ if (!rulesPage.totalPages || page >= rulesPage.totalPages || list.length < pageSize) {
9717
+ break;
9718
+ }
9719
+ page += 1;
9720
+ if (page > 50) {
9721
+ process.stderr.write(
9722
+ "Warning: event-rules lookup truncated at 5000 results (50 pages)\n"
9723
+ );
9724
+ break;
9725
+ }
9726
+ }
9727
+ return users;
9728
+ }
9729
+
8661
9730
  // src/domains/phone-numbers.ts
9731
+ function asPhoneRecord(value) {
9732
+ return value;
9733
+ }
9734
+ function requireCallerRole(args, flag) {
9735
+ const role = parseCallerRole(args[flag]);
9736
+ if (!role) {
9737
+ throw new CLIError(
9738
+ `Invalid --${flag}. Use one of: anonymous, member, operator, signup, unknown`
9739
+ );
9740
+ }
9741
+ return role;
9742
+ }
8662
9743
  var COLUMNS25 = [
8663
9744
  COL.id(),
8664
- { header: "NUMBER", getValue: (item) => item.phone_number ?? "" },
8665
- { header: "LABEL", getValue: (item) => item.label ?? "" },
9745
+ { header: "NUMBER", getValue: (item) => item.phone_number ?? item.number ?? "" },
9746
+ { header: "LABEL", getValue: (item) => item.label ?? item.name ?? "" },
8666
9747
  COL.status()
8667
9748
  ];
9749
+ var PROVISIONING_HELP = `
9750
+ Provisioning chain (org \u2192 workspace):
9751
+ 1. gr org phone-numbers search --org <org-uuid> [--area-code 775]
9752
+ 2. gr org phone-numbers purchase --org <org-uuid> --data '{"phoneNumber":"+1\u2026"}' --allow-write
9753
+ 3. gr org phone-numbers assign --org <org-uuid> --data '{"providerPhoneUuid":"\u2026","workspaceUuid":"\u2026"}' --allow-write
9754
+ 4. gr phone-numbers create -w $WID --data '{"name":"\u2026","providerPhoneUuid":"\u2026"}' --allow-write
9755
+ OR gr phone-numbers link -w $WID --id <id> --provider-phone-uuid <uuid> --allow-write
9756
+ 5. gr phone-numbers update --id <id> --data '{"defaultWorkflowId":<wf>}' --allow-write
9757
+
9758
+ IDs: workspace phone id (numeric) vs uuid (string, used in event rules phoneNumberUUID).
9759
+ Inbound: phone_numbers.defaultWorkflowId | Outbound: workflows.defaultPhoneNumberId
9760
+ `.trim();
8668
9761
  var phoneNumbersDomain = {
8669
9762
  name: "phone-numbers",
8670
- description: "Manage phone numbers",
9763
+ description: `Workspace phone numbers (inbound/outbound voice & SMS). ${PROVISIONING_HELP}`,
8671
9764
  commands: [
8672
9765
  ...crudCommands({
8673
9766
  prefix: ["phone-numbers"],
8674
9767
  entityName: "phone number",
8675
9768
  listMethod: "getPhoneNumbers",
8676
- getMethod: "getPhoneNumber",
8677
- createMethod: "createPhoneNumber",
8678
- updateMethod: "updatePhoneNumber",
8679
9769
  archiveMethod: "archivePhoneNumber",
8680
9770
  columns: COLUMNS25
8681
9771
  }),
9772
+ {
9773
+ path: ["phone-numbers", "get"],
9774
+ description: "Get a phone number (behavior matrix; use --raw for JSON)",
9775
+ requiresWorkspace: true,
9776
+ args: [
9777
+ { name: "id", description: "Phone number ID", required: true },
9778
+ { name: "raw", description: "Output full JSON instead of behavior matrix", isFlag: true }
9779
+ ],
9780
+ handler: async ({ ctx, workspace, args }) => {
9781
+ const client = requireAuth(ctx).apiClient;
9782
+ const id = requireNumber(args, "id");
9783
+ const phone = asPhoneRecord(await client.getPhoneNumber(workspace, id));
9784
+ if (args.raw) {
9785
+ process.stdout.write(`${JSON.stringify(phone, null, 2)}
9786
+ `);
9787
+ return;
9788
+ }
9789
+ const outboundUsers = await findOutboundPhoneUsers(client, workspace, phone);
9790
+ process.stdout.write(
9791
+ `${formatPhoneNumberBehaviorMatrix(phone, outboundUsers)}
9792
+ `
9793
+ );
9794
+ }
9795
+ },
9796
+ {
9797
+ path: ["phone-numbers", "create"],
9798
+ description: `Create a phone number
9799
+
9800
+ ${PHONE_NUMBER_FIELD_REFERENCE}`,
9801
+ mutates: true,
9802
+ requiresWorkspace: true,
9803
+ examples: [
9804
+ "gr phone-numbers create -w $WID --allow-write -f /tmp/phone.json",
9805
+ `gr phone-numbers create -w $WID --allow-write --data '{"name":"Line 1","defaultWorkflowId":1}'`
9806
+ ],
9807
+ handler: async ({ ctx, workspace, data }) => {
9808
+ if (!data || typeof data !== "object") {
9809
+ throw new CLIError("JSON data required (--data, --file, or stdin)");
9810
+ }
9811
+ const payload = mergePhoneNumberCreateDefaults(data);
9812
+ try {
9813
+ const result = await requireAuth(ctx).apiClient.createPhoneNumber(
9814
+ workspace,
9815
+ payload
9816
+ );
9817
+ const record = asPhoneRecord(result);
9818
+ const warnings = collectPhoneNumberWarnings(record);
9819
+ outputData([record], ctx, COLUMNS25);
9820
+ if (warnings.length > 0) {
9821
+ process.stderr.write("\nWarnings after create:\n");
9822
+ for (const w of warnings) {
9823
+ process.stderr.write(` [${w.severity}] ${w.message}
9824
+ `);
9825
+ }
9826
+ }
9827
+ } catch (err) {
9828
+ return enrichPhoneNumberApiError(err);
9829
+ }
9830
+ }
9831
+ },
9832
+ {
9833
+ path: ["phone-numbers", "update"],
9834
+ description: `Update a phone number
9835
+
9836
+ ${PHONE_NUMBER_FIELD_REFERENCE}`,
9837
+ mutates: true,
9838
+ requiresWorkspace: true,
9839
+ args: [{ name: "id", description: "Phone number ID", required: true }],
9840
+ handler: async ({ ctx, workspace, args, data }) => {
9841
+ if (!data || typeof data !== "object") {
9842
+ throw new CLIError("JSON data required (--data, --file, or stdin)");
9843
+ }
9844
+ const id = requireNumber(args, "id");
9845
+ try {
9846
+ const result = await requireAuth(ctx).apiClient.updatePhoneNumber(
9847
+ workspace,
9848
+ id,
9849
+ data
9850
+ );
9851
+ outputData([result], ctx, COLUMNS25);
9852
+ } catch (err) {
9853
+ return enrichPhoneNumberApiError(err);
9854
+ }
9855
+ }
9856
+ },
9857
+ {
9858
+ path: ["phone-numbers", "setup"],
9859
+ description: "Provision/link a workspace phone line with inbound defaults (happy-path wizard)",
9860
+ mutates: true,
9861
+ requiresWorkspace: true,
9862
+ args: [
9863
+ { name: "name", description: "Display name for the line" },
9864
+ {
9865
+ name: "inbound-workflow",
9866
+ aliases: ["inbound-workflow-id"],
9867
+ description: "Inbound defaultWorkflowId"
9868
+ },
9869
+ { name: "area-code", description: "Area code when requesting a new number" },
9870
+ { name: "provider-phone-uuid", description: "Existing org provider phone UUID to link" },
9871
+ { name: "enable-voice", description: "Enable voice (default true)", isFlag: true },
9872
+ { name: "enable-sms", description: "Enable SMS (default true)", isFlag: true },
9873
+ { name: "allow-anonymous", description: "Allow anonymous inbound callers", isFlag: true },
9874
+ {
9875
+ name: "anonymous-member-role-id",
9876
+ description: "Role id when --allow-anonymous"
9877
+ }
9878
+ ],
9879
+ examples: [
9880
+ 'gr phone-numbers setup -w $WID --allow-write --name "Main" --inbound-workflow 1',
9881
+ "gr phone-numbers setup -w $WID --allow-write --provider-phone-uuid <uuid> --inbound-workflow 2"
9882
+ ],
9883
+ handler: async ({ ctx, workspace, args, data }) => {
9884
+ const client = requireAuth(ctx).apiClient;
9885
+ const payload = mergePhoneNumberCreateDefaults({
9886
+ name: typeof args.name === "string" ? args.name : "Voice line",
9887
+ enableVoice: args["enable-voice"] === false ? false : true,
9888
+ enableSms: args["enable-sms"] === false ? false : true,
9889
+ ...typeof args["inbound-workflow"] === "string" ? { defaultWorkflowId: Number(args["inbound-workflow"]) } : {},
9890
+ ...args["allow-anonymous"] ? { allowAnonymous: true } : {},
9891
+ ...typeof args["anonymous-member-role-id"] === "string" ? { anonymousMemberRoleId: Number(args["anonymous-member-role-id"]) } : {},
9892
+ ...typeof args["provider-phone-uuid"] === "string" ? { providerPhoneUuid: args["provider-phone-uuid"] } : {},
9893
+ ...data && typeof data === "object" ? data : {}
9894
+ });
9895
+ let phone;
9896
+ try {
9897
+ if (payload.providerPhoneUuid) {
9898
+ phone = asPhoneRecord(
9899
+ await client.createPhoneNumber(workspace, payload)
9900
+ );
9901
+ } else if (typeof args["area-code"] === "string") {
9902
+ phone = asPhoneRecord(
9903
+ await client.requestPhoneNumber(workspace, {
9904
+ areaCode: args["area-code"],
9905
+ name: payload.name
9906
+ })
9907
+ );
9908
+ if (payload.defaultWorkflowId) {
9909
+ phone = asPhoneRecord(
9910
+ await client.updatePhoneNumber(workspace, phone.id, {
9911
+ defaultWorkflowId: payload.defaultWorkflowId,
9912
+ ...payload
9913
+ })
9914
+ );
9915
+ }
9916
+ } else {
9917
+ throw new CLIError(
9918
+ "Provide --provider-phone-uuid (link existing org number) or --area-code (request new number)"
9919
+ );
9920
+ }
9921
+ } catch (err) {
9922
+ return enrichPhoneNumberApiError(err);
9923
+ }
9924
+ process.stdout.write(`
9925
+ Setup complete: id=${phone.id} uuid=${phone.uuid}
9926
+
9927
+ `);
9928
+ const outboundUsers = await findOutboundPhoneUsers(client, workspace, phone);
9929
+ process.stdout.write(
9930
+ `${formatPhoneNumberBehaviorMatrix(phone, outboundUsers)}
9931
+ `
9932
+ );
9933
+ }
9934
+ },
9935
+ {
9936
+ path: ["phone-numbers", "diagnose"],
9937
+ description: "Simulate inbound routing for a caller number (no PSTN)",
9938
+ requiresWorkspace: true,
9939
+ args: [
9940
+ { name: "id", description: "Phone number ID", required: true },
9941
+ { name: "as", description: "Caller E.164 (e.g. +15551234567)" },
9942
+ {
9943
+ name: "as-role",
9944
+ description: "Caller role: anonymous|member|operator|signup (skips number lookup)"
9945
+ }
9946
+ ],
9947
+ examples: ["gr phone-numbers diagnose -w $WID --id 2 --as +15551234567"],
9948
+ handler: async ({ ctx, workspace, args }) => {
9949
+ const client = requireAuth(ctx).apiClient;
9950
+ const id = requireNumber(args, "id");
9951
+ const phone = asPhoneRecord(await client.getPhoneNumber(workspace, id));
9952
+ const asRole = parseCallerRole(args["as-role"]);
9953
+ let hasMemberMatch = asRole === "member";
9954
+ if (!asRole && typeof args.as === "string") {
9955
+ hasMemberMatch = false;
9956
+ }
9957
+ const sim = simulateInboundRouting(phone, { asRole, hasMemberMatch });
9958
+ process.stdout.write(`${formatRoutingSimulation(sim, phone)}
9959
+ `);
9960
+ }
9961
+ },
9962
+ {
9963
+ path: ["phone-numbers", "experience"],
9964
+ description: "Preview caller experience by role (no PSTN)",
9965
+ requiresWorkspace: true,
9966
+ args: [
9967
+ { name: "id", description: "Phone number ID", required: true },
9968
+ {
9969
+ name: "as-role",
9970
+ description: "anonymous|member|operator|signup",
9971
+ required: true
9972
+ }
9973
+ ],
9974
+ handler: async ({ ctx, workspace, args }) => {
9975
+ const client = requireAuth(ctx).apiClient;
9976
+ const id = requireNumber(args, "id");
9977
+ const role = requireCallerRole(args, "as-role");
9978
+ const phone = asPhoneRecord(await client.getPhoneNumber(workspace, id));
9979
+ const sim = simulateInboundRouting(phone, {
9980
+ asRole: role,
9981
+ hasMemberMatch: role === "member"
9982
+ });
9983
+ process.stdout.write(
9984
+ `If a caller in role "${role}" dials this line right now:
9985
+ \u2192 ${sim.finalBehavior}
9986
+
9987
+ ${formatRoutingSimulation(sim, phone)}
9988
+ `
9989
+ );
9990
+ }
9991
+ },
9992
+ {
9993
+ path: ["phone-numbers", "test-inbound"],
9994
+ description: "Server-side inbound routing simulation (JSON; no Twilio call)",
9995
+ requiresWorkspace: true,
9996
+ args: [
9997
+ { name: "id", description: "Phone number ID", required: true },
9998
+ { name: "as", description: "Caller E.164" },
9999
+ { name: "as-role", description: "anonymous|member|signup" }
10000
+ ],
10001
+ handler: async ({ ctx, workspace, args }) => {
10002
+ const client = requireAuth(ctx).apiClient;
10003
+ const id = requireNumber(args, "id");
10004
+ const phone = asPhoneRecord(await client.getPhoneNumber(workspace, id));
10005
+ const asRole = parseCallerRole(args["as-role"]);
10006
+ const sim = simulateInboundRouting(phone, {
10007
+ asRole,
10008
+ hasMemberMatch: asRole === "member"
10009
+ });
10010
+ process.stdout.write(`${JSON.stringify({ phoneNumberId: id, simulation: sim }, null, 2)}
10011
+ `);
10012
+ }
10013
+ },
10014
+ {
10015
+ path: ["phone-numbers", "users"],
10016
+ description: "List workflows and event rules using this number for outbound",
10017
+ requiresWorkspace: true,
10018
+ args: [{ name: "id", description: "Phone number ID", required: true }],
10019
+ handler: async ({ ctx, workspace, args }) => {
10020
+ const client = requireAuth(ctx).apiClient;
10021
+ const id = requireNumber(args, "id");
10022
+ const phone = asPhoneRecord(await client.getPhoneNumber(workspace, id));
10023
+ const users = await findOutboundPhoneUsers(client, workspace, phone);
10024
+ outputData(users, ctx);
10025
+ }
10026
+ },
10027
+ {
10028
+ path: ["phone-numbers", "inbound-log"],
10029
+ description: "Recent inbound call activity for this line (use calls list; routing detail in status)",
10030
+ requiresWorkspace: true,
10031
+ args: [
10032
+ {
10033
+ name: "phone-number-id",
10034
+ aliases: ["id"],
10035
+ description: "Phone number ID",
10036
+ required: true
10037
+ },
10038
+ { name: "page", description: "Page number" },
10039
+ { name: "page-size", description: "Page size" }
10040
+ ],
10041
+ handler: async ({ ctx, workspace, args }) => {
10042
+ const result = await requireAuth(ctx).apiClient.getPhoneNumberCalls(
10043
+ workspace,
10044
+ requireNumberFrom(args, "phone-number-id", "id"),
10045
+ args.page ? Number(args.page) : 1,
10046
+ args["page-size"] ? Number(args["page-size"]) : 20,
10047
+ { inbound: true }
10048
+ );
10049
+ const inbound = result.items ?? [];
10050
+ process.stdout.write(
10051
+ `Inbound calls (most recent page): ${inbound.length} shown
10052
+ `
10053
+ );
10054
+ process.stdout.write(
10055
+ "Tip: gr phone-numbers diagnose --id <id> --as +1\u2026 simulates routing without a live call.\n"
10056
+ );
10057
+ outputData(inbound, ctx);
10058
+ }
10059
+ },
8682
10060
  {
8683
10061
  path: ["phone-numbers", "available"],
8684
10062
  description: "List available phone numbers for purchase",
@@ -8715,7 +10093,7 @@ var phoneNumbersDomain = {
8715
10093
  },
8716
10094
  {
8717
10095
  path: ["phone-numbers", "request"],
8718
- description: "Request a phone number",
10096
+ description: "Request a phone number from Twilio",
8719
10097
  mutates: true,
8720
10098
  requiresWorkspace: true,
8721
10099
  handler: async ({ ctx, workspace, data }) => {
@@ -9234,6 +10612,64 @@ var supportDomain = {
9234
10612
  const result = wid ? await client.commentOnWorkspaceSupportRequest(wid, id, payload) : orgUuid ? await client.commentOnOrgSupportRequest(orgUuid, id, payload) : await client.commentOnSupportRequest(id, payload);
9235
10613
  outputSingle(result, ctx, SUPPORT_COLUMNS);
9236
10614
  }
10615
+ },
10616
+ {
10617
+ path: ["support", "close"],
10618
+ description: "Close one of your support requests (workspace-scoped)",
10619
+ mutates: true,
10620
+ args: [
10621
+ { name: "workspace", aliases: ["w"], description: "Workspace UUID", required: true },
10622
+ { name: "id", aliases: ["uuid"], description: "Support request UUID", required: true },
10623
+ { name: "reason", description: "Optional close reason" }
10624
+ ],
10625
+ handler: async ({ ctx, args, workspace, data }) => {
10626
+ const client = requireAuth(ctx).apiClient;
10627
+ const id = requireString(args, "id");
10628
+ const wid = args.workspace ?? workspace;
10629
+ if (!wid) throw new Error("--workspace is required to close a support request");
10630
+ const input = data ?? {};
10631
+ const reason = input.reason ?? args.reason;
10632
+ const result = await client.closeWorkspaceSupportRequest(
10633
+ wid,
10634
+ id,
10635
+ reason ? { reason } : {}
10636
+ );
10637
+ outputSingle(result, ctx, SUPPORT_COLUMNS);
10638
+ }
10639
+ },
10640
+ {
10641
+ path: ["support", "resource-click"],
10642
+ description: "Record a click on a pushed resource (consume + track usefulness)",
10643
+ mutates: true,
10644
+ args: [
10645
+ { name: "workspace", aliases: ["w"], description: "Workspace UUID", required: true },
10646
+ { name: "id", aliases: ["uuid"], description: "Support request UUID", required: true },
10647
+ {
10648
+ name: "resource-uuid",
10649
+ description: "UUID of the resource that was opened",
10650
+ required: true
10651
+ },
10652
+ {
10653
+ name: "resulting-chat-uuid",
10654
+ description: "Manager Chat UUID a manager_prompt opened (links resource\u2192chat)"
10655
+ }
10656
+ ],
10657
+ handler: async ({ ctx, args, workspace, data }) => {
10658
+ const client = requireAuth(ctx).apiClient;
10659
+ const id = requireString(args, "id");
10660
+ const resourceUuid = requireString(args, "resource-uuid");
10661
+ const wid = args.workspace ?? workspace;
10662
+ if (!wid) throw new Error("--workspace is required to click a resource");
10663
+ const input = data ?? {};
10664
+ const resultingChatUuid = input.resultingChatUuid ?? args["resulting-chat-uuid"];
10665
+ const result = await client.clickWorkspaceSupportRequestResource(
10666
+ wid,
10667
+ id,
10668
+ resourceUuid,
10669
+ resultingChatUuid ? { resultingChatUuid } : {}
10670
+ );
10671
+ outputSingle(result, ctx, SUPPORT_COLUMNS);
10672
+ }
9237
10673
  }
9238
10674
  ]
9239
10675
  };
@@ -10795,6 +12231,7 @@ function registerPublicDomains(registry) {
10795
12231
  registry.register(tasksDomain);
10796
12232
  registry.register(chatsDomain);
10797
12233
  registry.register(membersDomain);
12234
+ registry.register(memberRolesDomain);
10798
12235
  registry.register(workspacesDomain);
10799
12236
  registry.register(workflowsDomain);
10800
12237
  registry.register(workflowVersionsDomain);
@@ -10805,6 +12242,7 @@ function registerPublicDomains(registry) {
10805
12242
  registry.register(sitesDomain);
10806
12243
  registry.register(filesDomain);
10807
12244
  registry.register(eventsDomain);
12245
+ registry.register(experimentsDomain);
10808
12246
  registry.register(calendarsDomain);
10809
12247
  registry.register(qualificationsDomain);
10810
12248
  registry.register(personasDomain);
@@ -13018,9 +14456,45 @@ async function listAudioDevices() {
13018
14456
  }
13019
14457
 
13020
14458
  // src/commands/concierge.ts
14459
+ async function conciergeOutboundCommand(hctx) {
14460
+ const ctx = requireAuth(hctx.ctx);
14461
+ const { args } = hctx;
14462
+ if (args.call && args.sms) {
14463
+ throw new CLIError("--call and --sms are mutually exclusive");
14464
+ }
14465
+ if (args.call) {
14466
+ const result = await originateConciergeCall(ctx);
14467
+ process.stderr.write(
14468
+ `Concierge call started. callSid=${result.callSid} startedAt=${result.startedAt}
14469
+ `
14470
+ );
14471
+ return;
14472
+ }
14473
+ if (args.sms) {
14474
+ const result = await originateConciergeSms(ctx);
14475
+ process.stderr.write(
14476
+ `Concierge SMS started. messageSid=${result.messageSid} conversationId=${result.conversationId}
14477
+ `
14478
+ );
14479
+ return;
14480
+ }
14481
+ throw new CLIError(
14482
+ "Use `gr concierge chat --call`, `gr concierge chat --sms`, or `gr concierge chat` for interactive mode."
14483
+ );
14484
+ }
13021
14485
  async function conciergeCommand(hctx) {
13022
14486
  const ctx = requireAuth(hctx.ctx);
13023
14487
  const { args } = hctx;
14488
+ if (args.call || args.sms) {
14489
+ if (args.call && args.sms) {
14490
+ throw new CLIError("--call and --sms are mutually exclusive");
14491
+ }
14492
+ if (args.voice || args.realtime) {
14493
+ throw new CLIError("--call/--sms cannot be combined with --voice or --realtime");
14494
+ }
14495
+ await conciergeOutboundCommand(hctx);
14496
+ return;
14497
+ }
13024
14498
  const chatIdArg = typeof args["chat-id"] === "string" ? args["chat-id"] : void 0;
13025
14499
  const message = typeof args.message === "string" ? args.message : void 0;
13026
14500
  const chat = await getOrCreateConciergeChat(ctx, chatIdArg, !!message);
@@ -13154,6 +14628,16 @@ After directory trust + \`--allow-write\`, the concierge can use \`read_local_fi
13154
14628
 
13155
14629
  ## Verify success
13156
14630
  \`gr workspaces list\` then \`gr workflows list -w <uuid>\` or \`gr agents list -w <uuid>\`
14631
+
14632
+ ## Local phone / voice QA (monorepo dev)
14633
+ \`\`\`bash
14634
+ yarn dev:twilio-ngrok --start-ngrok --url gravityrail.ngrok.io dev.<you> -w acme-demo
14635
+ yarn gr phone setup
14636
+ yarn gr account verify-phone --phone +1\u2026 --allow-write --realm superadmin
14637
+ yarn gr concierge chat --call --allow-write --realm superadmin
14638
+ yarn gr phone outbound --realm acme-demo/acme-admin --workflow-id 1 --allow-write
14639
+ \`\`\`
14640
+ See \`.claude/skills/phone-voice-qa/SKILL.md\` and \`./scripts/e2e-voice-phone-matrix.sh\`.
13157
14641
  `;
13158
14642
  async function getStartedCommand(hctx) {
13159
14643
  const format = hctx.args.format ?? hctx.args.agent;
@@ -13418,7 +14902,10 @@ export {
13418
14902
  authBrowserCommand,
13419
14903
  buildCliConciergeAppContext,
13420
14904
  chatCommand,
14905
+ commandNeedsJsonBody,
13421
14906
  conciergeCommand,
14907
+ conciergeOutboundCommand,
14908
+ confirmAccountPhoneVerify,
13422
14909
  createAuthenticatedContext,
13423
14910
  createConciergeChat,
13424
14911
  createContext,
@@ -13437,6 +14924,7 @@ export {
13437
14924
  handleError,
13438
14925
  isCursorPaginatedResponse,
13439
14926
  isDirectoryTrusted,
14927
+ isLikelySeedPhoneNumber,
13440
14928
  isListResult,
13441
14929
  isPagePaginatedResponse,
13442
14930
  isPaginatedResponse,
@@ -13446,6 +14934,8 @@ export {
13446
14934
  logoutCommand,
13447
14935
  manageCommand,
13448
14936
  organizationsListCommand,
14937
+ originateConciergeCall,
14938
+ originateConciergeSms,
13449
14939
  outputData,
13450
14940
  outputSingle,
13451
14941
  parseArgs,
@@ -13456,9 +14946,12 @@ export {
13456
14946
  requireDirectoryTrust,
13457
14947
  requireNumber,
13458
14948
  requireString,
14949
+ resolveVoicePhoneNumberId,
13459
14950
  simpleCrudCommands,
14951
+ startAccountPhoneVerify,
13460
14952
  stderr,
13461
14953
  unwrapItems,
14954
+ waitForOutboundPhoneCallOutcome,
13462
14955
  whoamiCommand
13463
14956
  };
13464
14957
  //# sourceMappingURL=lib.js.map