@gravity-rail/cli 0.7.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
@@ -1452,6 +1452,45 @@ var AdminApi = class extends BaseApiClient {
1452
1452
  `admin/temporal/workflows/${encoded}${qs ? `?${qs}` : ""}`
1453
1453
  );
1454
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
+ }
1455
1494
  };
1456
1495
  function appendTemporalWorkspaceParams(params, options) {
1457
1496
  const seen = /* @__PURE__ */ new Set();
@@ -1478,6 +1517,11 @@ function bindAdminMethods(target, admin) {
1478
1517
  target.createOutboundSupportRequestAsAdmin = admin.createOutboundSupportRequestAsAdmin.bind(admin);
1479
1518
  target.attachSupportRequestResourceAsAdmin = admin.attachSupportRequestResourceAsAdmin.bind(admin);
1480
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);
1481
1525
  target.getAccounts = admin.getAccounts.bind(admin);
1482
1526
  target.getAccountDetails = admin.getAccountDetails.bind(admin);
1483
1527
  target.getAccountByUuid = admin.getAccountByUuid.bind(admin);
@@ -1653,6 +1697,11 @@ var ApiClient = class extends GravityRailClient {
1653
1697
  createOutboundSupportRequestAsAdmin;
1654
1698
  attachSupportRequestResourceAsAdmin;
1655
1699
  revokeSupportRequestResourceAsAdmin;
1700
+ listCustomerActivityEvents;
1701
+ getCustomerActivityEvent;
1702
+ setCustomerActivityEventDisposition;
1703
+ setCustomerActivityEventStatus;
1704
+ reclassifyCustomerActivityEvent;
1656
1705
  getAccounts;
1657
1706
  getAccountDetails;
1658
1707
  getAccountByUuid;
@@ -2128,9 +2177,78 @@ This workspace requires organization SSO authentication.`);
2128
2177
  });
2129
2178
  }
2130
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
+
2131
2248
  // src/lib/context.ts
2132
2249
  function makeApiClient(accessToken, apiUrl) {
2133
- const apiClient = new ApiClient(accessToken, apiUrl);
2250
+ const fetchFn = createSandboxFetchFromEnv(apiUrl);
2251
+ const apiClient = new ApiClient(accessToken, apiUrl, fetchFn);
2134
2252
  registerOrgAuthHandler(apiClient, apiUrl, accessToken);
2135
2253
  return apiClient;
2136
2254
  }
@@ -2299,18 +2417,18 @@ function readStdinIfPiped() {
2299
2417
  return new Promise((resolve, reject) => {
2300
2418
  let data = "";
2301
2419
  let settled = false;
2302
- const finish = (value) => {
2420
+ const finish2 = (value) => {
2303
2421
  if (settled) return;
2304
2422
  settled = true;
2305
2423
  resolve(value);
2306
2424
  };
2307
2425
  const flush = () => {
2308
2426
  if (!data.trim()) {
2309
- finish(void 0);
2427
+ finish2(void 0);
2310
2428
  return;
2311
2429
  }
2312
2430
  try {
2313
- finish(JSON.parse(data));
2431
+ finish2(JSON.parse(data));
2314
2432
  } catch {
2315
2433
  reject(new CLIError("Invalid JSON from stdin"));
2316
2434
  }
@@ -2330,7 +2448,7 @@ function readStdinIfPiped() {
2330
2448
  flush();
2331
2449
  return;
2332
2450
  }
2333
- finish(void 0);
2451
+ finish2(void 0);
2334
2452
  });
2335
2453
  });
2336
2454
  }
@@ -2987,6 +3105,14 @@ var CommandRegistry = class {
2987
3105
  }
2988
3106
  return lines.join("\n") + "\n";
2989
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
+ }
2990
3116
  getTopLevelHelp() {
2991
3117
  const lines = ["Usage: gr <command> [options]\n", "Commands:"];
2992
3118
  const seen = /* @__PURE__ */ new Set();
@@ -3697,6 +3823,31 @@ var membersDomain = {
3697
3823
  ]
3698
3824
  };
3699
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
+
3700
3851
  // src/domains/tasks.ts
3701
3852
  var COLUMNS4 = [
3702
3853
  COL.id(),
@@ -7899,9 +8050,9 @@ function toCamelMessage(raw) {
7899
8050
  id: raw.id,
7900
8051
  role: raw.role,
7901
8052
  content: raw.content ?? "",
7902
- createdAt: raw.created_at ?? null,
7903
- toolCalls: raw.tool_calls ?? null,
7904
- 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
7905
8056
  };
7906
8057
  }
7907
8058
  async function listConciergeChats(ctx) {
@@ -9132,27 +9283,780 @@ var orgPhoneNumbersDomain = {
9132
9283
  commands: commands2
9133
9284
  };
9134
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
+
9135
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
+ }
9136
9743
  var COLUMNS25 = [
9137
9744
  COL.id(),
9138
- { header: "NUMBER", getValue: (item) => item.phone_number ?? "" },
9139
- { 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 ?? "" },
9140
9747
  COL.status()
9141
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();
9142
9761
  var phoneNumbersDomain = {
9143
9762
  name: "phone-numbers",
9144
- description: "Manage phone numbers",
9763
+ description: `Workspace phone numbers (inbound/outbound voice & SMS). ${PROVISIONING_HELP}`,
9145
9764
  commands: [
9146
9765
  ...crudCommands({
9147
9766
  prefix: ["phone-numbers"],
9148
9767
  entityName: "phone number",
9149
9768
  listMethod: "getPhoneNumbers",
9150
- getMethod: "getPhoneNumber",
9151
- createMethod: "createPhoneNumber",
9152
- updateMethod: "updatePhoneNumber",
9153
9769
  archiveMethod: "archivePhoneNumber",
9154
9770
  columns: COLUMNS25
9155
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
+ },
9156
10060
  {
9157
10061
  path: ["phone-numbers", "available"],
9158
10062
  description: "List available phone numbers for purchase",
@@ -9189,7 +10093,7 @@ var phoneNumbersDomain = {
9189
10093
  },
9190
10094
  {
9191
10095
  path: ["phone-numbers", "request"],
9192
- description: "Request a phone number",
10096
+ description: "Request a phone number from Twilio",
9193
10097
  mutates: true,
9194
10098
  requiresWorkspace: true,
9195
10099
  handler: async ({ ctx, workspace, data }) => {
@@ -11327,6 +12231,7 @@ function registerPublicDomains(registry) {
11327
12231
  registry.register(tasksDomain);
11328
12232
  registry.register(chatsDomain);
11329
12233
  registry.register(membersDomain);
12234
+ registry.register(memberRolesDomain);
11330
12235
  registry.register(workspacesDomain);
11331
12236
  registry.register(workflowsDomain);
11332
12237
  registry.register(workflowVersionsDomain);