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