@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/gr.js +39561 -3306
- package/dist/gr.js.map +1 -1
- package/dist/lib.d.ts +4 -0
- package/dist/lib.js +920 -15
- package/dist/lib.js.map +1 -1
- package/dist/register-special-commands.d.ts +4 -0
- package/dist/register-special-commands.js +5525 -0
- package/dist/register-special-commands.js.map +1 -0
- package/package.json +9 -3
- package/dist/account-verify-phone-4LRJZT5I.js +0 -36
- package/dist/account-verify-phone-4LRJZT5I.js.map +0 -1
- package/dist/chunk-3EO4COHB.js +0 -14202
- package/dist/chunk-3EO4COHB.js.map +0 -1
- package/dist/chunk-QVG4L6BE.js +0 -2199
- package/dist/chunk-QVG4L6BE.js.map +0 -1
- package/dist/chunk-XRBVAFCB.js +0 -47
- package/dist/chunk-XRBVAFCB.js.map +0 -1
- package/dist/concierge-ZOG7YFWQ.js +0 -6
- package/dist/concierge-ZOG7YFWQ.js.map +0 -1
- package/dist/errors-DJILV5RW.js +0 -4
- package/dist/errors-DJILV5RW.js.map +0 -1
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
|
|
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
|
|
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
|
-
|
|
2427
|
+
finish2(void 0);
|
|
2310
2428
|
return;
|
|
2311
2429
|
}
|
|
2312
2430
|
try {
|
|
2313
|
-
|
|
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
|
-
|
|
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:
|
|
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);
|