@gravity-rail/cli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib.js CHANGED
@@ -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()}`);
@@ -1454,6 +1475,9 @@ function bindAdminMethods(target, admin) {
1454
1475
  target.updateSupportRequestStatusAsAdmin = admin.updateSupportRequestStatusAsAdmin.bind(admin);
1455
1476
  target.updateSupportRequestAttributesAsAdmin = admin.updateSupportRequestAttributesAsAdmin.bind(admin);
1456
1477
  target.createSupportRequestLinearIssueAsAdmin = admin.createSupportRequestLinearIssueAsAdmin.bind(admin);
1478
+ target.createOutboundSupportRequestAsAdmin = admin.createOutboundSupportRequestAsAdmin.bind(admin);
1479
+ target.attachSupportRequestResourceAsAdmin = admin.attachSupportRequestResourceAsAdmin.bind(admin);
1480
+ target.revokeSupportRequestResourceAsAdmin = admin.revokeSupportRequestResourceAsAdmin.bind(admin);
1457
1481
  target.getAccounts = admin.getAccounts.bind(admin);
1458
1482
  target.getAccountDetails = admin.getAccountDetails.bind(admin);
1459
1483
  target.getAccountByUuid = admin.getAccountByUuid.bind(admin);
@@ -1626,6 +1650,9 @@ var ApiClient = class extends GravityRailClient {
1626
1650
  updateSupportRequestStatusAsAdmin;
1627
1651
  updateSupportRequestAttributesAsAdmin;
1628
1652
  createSupportRequestLinearIssueAsAdmin;
1653
+ createOutboundSupportRequestAsAdmin;
1654
+ attachSupportRequestResourceAsAdmin;
1655
+ revokeSupportRequestResourceAsAdmin;
1629
1656
  getAccounts;
1630
1657
  getAccountDetails;
1631
1658
  getAccountByUuid;
@@ -2120,7 +2147,9 @@ async function tryTokenRefresh(ctx, creds) {
2120
2147
  return { ...ctx, accessToken: refreshed.accessToken, apiClient };
2121
2148
  }
2122
2149
  function createContext(opts) {
2123
- const envConfig = getEnvironmentConfig(opts.env);
2150
+ const baseConfig = getEnvironmentConfig(opts.env);
2151
+ const apiUrl = opts.apiUrl || process.env.GRAVITY_RAIL_API_URL || baseConfig.apiUrl;
2152
+ const envConfig = apiUrl === baseConfig.apiUrl ? baseConfig : { ...baseConfig, apiUrl };
2124
2153
  const credentialsManager = new CredentialsManager(homedir(), envConfig.apiUrl);
2125
2154
  return {
2126
2155
  env: opts.env,
@@ -2206,6 +2235,40 @@ async function createPublicAuthenticatedContext(opts) {
2206
2235
 
2207
2236
  // src/lib/input.ts
2208
2237
  import { readFileSync as readFileSync2 } from "fs";
2238
+ var JSON_BODY_ACTIONS = /* @__PURE__ */ new Set([
2239
+ "create",
2240
+ "update",
2241
+ "import",
2242
+ "enroll",
2243
+ "unenroll",
2244
+ "reenroll",
2245
+ "advance",
2246
+ "skip",
2247
+ "send",
2248
+ "review",
2249
+ "submit-answer",
2250
+ "start-scenario",
2251
+ "update-status"
2252
+ ]);
2253
+ function commandNeedsJsonBody(cmd, parsedArgs) {
2254
+ if (parsedArgs.data !== void 0 || parsedArgs.file !== void 0) {
2255
+ return true;
2256
+ }
2257
+ if (cmd.args?.some((a) => a.name === "data" || a.name === "file")) {
2258
+ return true;
2259
+ }
2260
+ if (!cmd.mutates) {
2261
+ return false;
2262
+ }
2263
+ const action = cmd.path[cmd.path.length - 1] ?? "";
2264
+ if (JSON_BODY_ACTIONS.has(action)) {
2265
+ return true;
2266
+ }
2267
+ if (action === "send" || action === "cancel" && cmd.path.length > 1) {
2268
+ return true;
2269
+ }
2270
+ return false;
2271
+ }
2209
2272
  async function readJsonInput(args) {
2210
2273
  const dataStr = args.data;
2211
2274
  const filePath = args.file;
@@ -2272,6 +2335,99 @@ function readStdinIfPiped() {
2272
2335
  });
2273
2336
  }
2274
2337
 
2338
+ // src/lib/phone-voice.ts
2339
+ var FAILED_TERMINATION_REASONS = /* @__PURE__ */ new Set([
2340
+ "failed",
2341
+ "busy",
2342
+ "no-answer",
2343
+ "canceled",
2344
+ "timeout",
2345
+ "error"
2346
+ ]);
2347
+ var SEED_PHONE_PATTERN = /^\+1212555010\d$/;
2348
+ function sleep(ms) {
2349
+ return new Promise((resolve) => setTimeout(resolve, ms));
2350
+ }
2351
+ function latestOutboundCall(calls) {
2352
+ return calls.filter((c) => !c.inbound).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
2353
+ }
2354
+ function isTerminalFailure(call) {
2355
+ if (call.status === "ERROR") {
2356
+ return true;
2357
+ }
2358
+ const reason = call.terminationReason ?? "";
2359
+ return FAILED_TERMINATION_REASONS.has(reason);
2360
+ }
2361
+ function formatOutboundCallFailure(call, memberId) {
2362
+ const reason = call.terminationReason ?? call.status;
2363
+ const lines = [`Outbound call failed (phoneCallId=${call.id}, terminationReason=${reason}).`];
2364
+ if (reason === "failed") {
2365
+ lines.push(
2366
+ "Twilio rejected the dial \u2014 common causes: invalid/unverified callee number, trial account restrictions, or unreachable DEV_TUNNEL_URL for the answer URL."
2367
+ );
2368
+ }
2369
+ lines.push(
2370
+ `Update the callee phone: gr members update --id ${memberId} --data '{"phone":"+1YOUR_MOBILE"}' --allow-write`
2371
+ );
2372
+ return lines.join("\n");
2373
+ }
2374
+ function isLikelySeedPhoneNumber(phone) {
2375
+ return SEED_PHONE_PATTERN.test(phone);
2376
+ }
2377
+ async function waitForOutboundPhoneCallOutcome(client, workspaceUuid, chatId, memberId, options) {
2378
+ const timeoutMs = options?.timeoutMs ?? 2e4;
2379
+ const intervalMs = options?.intervalMs ?? 500;
2380
+ const deadline = Date.now() + timeoutMs;
2381
+ while (Date.now() < deadline) {
2382
+ const calls2 = await client.getChatPhoneCalls(workspaceUuid, chatId);
2383
+ const outbound2 = latestOutboundCall(calls2);
2384
+ if (!outbound2) {
2385
+ await sleep(intervalMs);
2386
+ continue;
2387
+ }
2388
+ if (isTerminalFailure(outbound2)) {
2389
+ throw new CLIError(formatOutboundCallFailure(outbound2, memberId));
2390
+ }
2391
+ if (outbound2.status === "STARTED") {
2392
+ return outbound2;
2393
+ }
2394
+ if (outbound2.status === "FINISHED") {
2395
+ return outbound2;
2396
+ }
2397
+ await sleep(intervalMs);
2398
+ }
2399
+ const calls = await client.getChatPhoneCalls(workspaceUuid, chatId);
2400
+ const outbound = latestOutboundCall(calls);
2401
+ if (!outbound) {
2402
+ throw new CLIError(
2403
+ `Outbound call was not initiated for chat ${chatId}. Check API logs, DEV_TUNNEL_URL, and that ngrok points at https://api.gravityrail.test:5054.`
2404
+ );
2405
+ }
2406
+ if (isTerminalFailure(outbound)) {
2407
+ throw new CLIError(formatOutboundCallFailure(outbound, memberId));
2408
+ }
2409
+ throw new CLIError(
2410
+ `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.`
2411
+ );
2412
+ }
2413
+ async function resolveVoicePhoneNumberId(client, workspaceUuid, explicitId) {
2414
+ if (explicitId !== void 0) {
2415
+ return explicitId;
2416
+ }
2417
+ const defaults = await client.getWorkspaceDefaults(workspaceUuid);
2418
+ if (defaults.defaultPhoneNumberId) {
2419
+ return defaults.defaultPhoneNumberId;
2420
+ }
2421
+ const numbers = await client.getPhoneNumbers(workspaceUuid);
2422
+ const voiceLine = numbers.find((n) => n.enableVoice !== false && !n.isArchived);
2423
+ if (!voiceLine) {
2424
+ throw new CLIError(
2425
+ "No voice-enabled phone number in workspace \u2014 run: yarn dev:twilio-ngrok dev.<you> -w <slug> or pass --phone-number-id"
2426
+ );
2427
+ }
2428
+ return voiceLine.id;
2429
+ }
2430
+
2275
2431
  // src/lib/crud.ts
2276
2432
  function crudCommands(config) {
2277
2433
  const {
@@ -2379,6 +2535,7 @@ function crudCommands(config) {
2379
2535
  mutates: true,
2380
2536
  args: [{ name: "id", description: `${entityName} ID`, required: true }],
2381
2537
  requiresWorkspace,
2538
+ ...config.updateExamples ? { examples: config.updateExamples } : {},
2382
2539
  handler: async (hctx) => {
2383
2540
  if (!hctx.data) throw new CLIError("JSON data required (--data, --file, or stdin)");
2384
2541
  const id = resolveId(hctx);
@@ -2801,11 +2958,13 @@ var CommandRegistry = class {
2801
2958
  const lines = [`${cmd.description}
2802
2959
  `, `Usage: gr ${cmdStr} [options]`];
2803
2960
  if (cmd.requiresWorkspace) {
2804
- lines.push(" Requires: -w <workspace-uuid>");
2961
+ lines.push(" Requires: -w <workspace-uuid> (or --realm <workspace>/<member> in dev)");
2805
2962
  }
2806
2963
  if (cmd.mutates) {
2807
2964
  lines.push(" Requires: --allow-write");
2808
2965
  }
2966
+ const action = cmd.path[cmd.path.length - 1] ?? "";
2967
+ const showsJsonHints = action === "create" || action === "update" || cmd.args?.some((a) => a.name === "data" || a.name === "file");
2809
2968
  if (cmd.args && cmd.args.length > 0) {
2810
2969
  lines.push("", "Arguments:");
2811
2970
  for (const arg of cmd.args) {
@@ -2814,6 +2973,12 @@ var CommandRegistry = class {
2814
2973
  lines.push(` ${flag.padEnd(28)} ${arg.description}${req}`);
2815
2974
  }
2816
2975
  }
2976
+ if (showsJsonHints) {
2977
+ lines.push("", "JSON body (one of):");
2978
+ lines.push(" -d, --data <json> Inline JSON");
2979
+ lines.push(" -f, --file <path> JSON file");
2980
+ lines.push(" stdin Pipe JSON when not a TTY");
2981
+ }
2817
2982
  if (cmd.examples && cmd.examples.length > 0) {
2818
2983
  lines.push("", "Examples:");
2819
2984
  for (const ex of cmd.examples) {
@@ -2842,6 +3007,12 @@ var CommandRegistry = class {
2842
3007
  "",
2843
3008
  "Global Options:",
2844
3009
  " --env <env> Environment: dev, staging, prod (default: dev)",
3010
+ " --realm <ws>/<member> Dev auth shortcut (dev only; sets -w automatically)",
3011
+ " --api-url <url> API base URL (or GRAVITY_RAIL_API_URL)",
3012
+ " -w, --workspace <uuid> Workspace UUID",
3013
+ " -d, --data <json> JSON body (create/update and other write commands)",
3014
+ " -f, --file <path> JSON body from file",
3015
+ " --allow-write Required for create/update/delete/archive",
2845
3016
  " -o, --output-format <fmt> Output format: json, jsonl, table (default: auto)",
2846
3017
  " -v, --verbose Verbose output to stderr",
2847
3018
  " --help Show help",
@@ -3368,7 +3539,11 @@ var membersDomain = {
3368
3539
  deleteMethod: "deleteMember",
3369
3540
  archiveMethod: "archiveMember",
3370
3541
  unarchiveMethod: "unarchiveMember",
3371
- columns: COLUMNS3
3542
+ columns: COLUMNS3,
3543
+ updateExamples: [
3544
+ `yarn gr members update -w $UUID --id 2 --data '{"phone":"+15551234567"}' --allow-write`,
3545
+ "yarn gr members update --realm acme-demo/acme-admin --id 2 --file /tmp/member.json --allow-write"
3546
+ ]
3372
3547
  }),
3373
3548
  // Current member
3374
3549
  {
@@ -4188,8 +4363,18 @@ var workspacesDomain = {
4188
4363
  `organizationId is required in --data (e.g. --data '{"organizationId": 1, "name": "My Workspace"}')`
4189
4364
  );
4190
4365
  }
4191
- const result = await requireAuth(ctx).apiClient.createWorkspace(payload);
4192
- outputSingle(result, ctx, COLUMNS7);
4366
+ try {
4367
+ const result = await requireAuth(ctx).apiClient.createWorkspace(payload);
4368
+ outputSingle(result, ctx, COLUMNS7);
4369
+ } catch (err) {
4370
+ const statusCode = err?.statusCode;
4371
+ if (statusCode === 405) {
4372
+ throw new CLIError(
4373
+ '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)'
4374
+ );
4375
+ }
4376
+ throw err;
4377
+ }
4193
4378
  }
4194
4379
  },
4195
4380
  // Defaults
@@ -5226,11 +5411,14 @@ var RULE_COLUMNS = [
5226
5411
  { header: "EVENT", getValue: (item) => item.eventType ?? "" },
5227
5412
  { header: "ACTIVE", getValue: (item) => String(item.active ?? "") }
5228
5413
  ];
5229
- var EVENT_COLUMNS2 = [
5414
+ var ROUTINE_COLUMNS = [
5230
5415
  COL.id(),
5231
5416
  COL.name(),
5232
- { header: "SLUG", getValue: (item) => item.slug ?? "" },
5233
- COL.status()
5417
+ { header: "MODE", getValue: (item) => item.scheduleMode ?? "" },
5418
+ { header: "CRON", getValue: (item) => item.cronString ?? "" },
5419
+ { header: "TZ", getValue: (item) => item.timezone ?? "UTC" },
5420
+ { header: "ENABLED", getValue: (item) => String(item.scheduleEnabled ?? "") },
5421
+ { header: "NEXT RUN", getValue: (item) => item.nextRun ?? "" }
5234
5422
  ];
5235
5423
  var eventRulesCrudNoList = crudCommands({
5236
5424
  prefix: ["events", "rules"],
@@ -5399,7 +5587,15 @@ var eventsDomain = {
5399
5587
  updateMethod: "updateRoutine",
5400
5588
  deleteMethod: "deleteRoutine",
5401
5589
  paginationSupport: "full",
5402
- columns: EVENT_COLUMNS2
5590
+ columns: ROUTINE_COLUMNS,
5591
+ createExamples: [
5592
+ "Daily 7am check-in in the member\u2019s timezone (cron is interpreted in `timezone`):",
5593
+ `gr routines create -w $WID --allow-write --data '{"name":"AM check-in",`,
5594
+ ' "scheduleEnabled":true,"scheduleMode":"CRON","cronString":"0 7 * * *",',
5595
+ ` "timezone":"America/Los_Angeles","targetMemberId":1,"workflowId":2}'`,
5596
+ " (timezone is an IANA name; omit it to default to the workspace timezone, else UTC.",
5597
+ ' Without it, "0 7 * * *" fires at 07:00 UTC, not 7am local.)'
5598
+ ]
5403
5599
  }),
5404
5600
  {
5405
5601
  path: ["routines", "run"],
@@ -5446,6 +5642,232 @@ var eventsDomain = {
5446
5642
  ]
5447
5643
  };
5448
5644
 
5645
+ // src/domains/experiments.ts
5646
+ var EXPERIMENT_COLUMNS = [
5647
+ COL.id(),
5648
+ { header: "UUID", getValue: (item) => (item.uuid ?? "").slice(0, 8) },
5649
+ { header: "SLUG", getValue: (item) => item.slug ?? "" },
5650
+ COL.name(),
5651
+ COL.status(),
5652
+ {
5653
+ header: "ASSIGNED",
5654
+ getValue: (item) => item.assignmentCount != null ? String(item.assignmentCount) : "0"
5655
+ },
5656
+ COL.created()
5657
+ ];
5658
+ var GROUP_COLUMNS = [
5659
+ COL.id(),
5660
+ { header: "SLUG", getValue: (item) => item.slug ?? "" },
5661
+ COL.name(),
5662
+ {
5663
+ header: "WEIGHT",
5664
+ getValue: (item) => item.allocationWeight != null ? String(item.allocationWeight) : ""
5665
+ },
5666
+ {
5667
+ header: "CONTROL",
5668
+ getValue: (item) => item.isControl ? "yes" : ""
5669
+ },
5670
+ { header: "ORDER", getValue: (item) => String(item.order ?? "") }
5671
+ ];
5672
+ var STATS_COLUMNS = [
5673
+ { header: "GROUP_ID", getValue: (item) => String(item.groupId ?? "") },
5674
+ { header: "SLUG", getValue: (item) => item.groupSlug ?? "" },
5675
+ { header: "NAME", getValue: (item) => item.groupName ?? "" },
5676
+ {
5677
+ header: "CONTROL",
5678
+ getValue: (item) => item.isControl ? "yes" : ""
5679
+ },
5680
+ {
5681
+ header: "ASSIGNED",
5682
+ getValue: (item) => item.assignedCount != null ? String(item.assignedCount) : "0"
5683
+ },
5684
+ {
5685
+ header: "EXPOSED",
5686
+ getValue: (item) => item.exposedCount != null ? String(item.exposedCount) : "0"
5687
+ }
5688
+ ];
5689
+ var MEMBER_ASSIGNMENT_COLUMNS = [
5690
+ COL.id(),
5691
+ {
5692
+ header: "EXPERIMENT",
5693
+ getValue: (item) => item.experimentSlug ?? String(item.experimentId ?? "")
5694
+ },
5695
+ {
5696
+ header: "GROUP",
5697
+ getValue: (item) => item.groupSlug ?? String(item.groupId ?? "")
5698
+ },
5699
+ { header: "ASSIGNED", getValue: (item) => item.assignedAt ?? "" },
5700
+ { header: "SOURCE", getValue: (item) => item.assignmentSource ?? "" }
5701
+ ];
5702
+ var experimentsDomain = {
5703
+ name: "experiments",
5704
+ description: "Manage member A/B experiments, groups, and reporting",
5705
+ commands: [
5706
+ {
5707
+ path: ["experiments", "list"],
5708
+ description: "List experiments",
5709
+ requiresWorkspace: true,
5710
+ args: [
5711
+ ...LIST_PAGINATION_ARGS,
5712
+ {
5713
+ name: "include-archived",
5714
+ description: "Include archived experiments",
5715
+ required: false
5716
+ }
5717
+ ],
5718
+ handler: async ({ ctx, workspace, args }) => {
5719
+ const includeArchived = args["include-archived"] === "true";
5720
+ const pag = parsePaginationArgs(args);
5721
+ const result = await requireAuth(ctx).apiClient.getExperiments(workspace, {
5722
+ ...pag.page ? { page: pag.page } : {},
5723
+ ...pag.pageSize ? { pageSize: pag.pageSize } : {},
5724
+ includeArchived
5725
+ });
5726
+ outputData(listResult(result, true), ctx, EXPERIMENT_COLUMNS);
5727
+ }
5728
+ },
5729
+ {
5730
+ path: ["experiments", "get"],
5731
+ description: "Get an experiment by ID",
5732
+ requiresWorkspace: true,
5733
+ args: [{ name: "id", description: "Experiment ID", required: true }],
5734
+ handler: async ({ ctx, workspace, args }) => {
5735
+ const result = await requireAuth(ctx).apiClient.getExperiment(
5736
+ workspace,
5737
+ requireNumber(args, "id")
5738
+ );
5739
+ outputSingle(result, ctx);
5740
+ }
5741
+ },
5742
+ {
5743
+ path: ["experiments", "create"],
5744
+ description: "Create a draft experiment (pass JSON via --data)",
5745
+ mutates: true,
5746
+ requiresWorkspace: true,
5747
+ handler: async ({ ctx, workspace, data }) => {
5748
+ if (!data) throw new CLIError("JSON data required (--data, --file, or stdin)");
5749
+ const result = await requireAuth(ctx).apiClient.createExperiment(
5750
+ workspace,
5751
+ data
5752
+ );
5753
+ outputSingle(result, ctx, EXPERIMENT_COLUMNS);
5754
+ }
5755
+ },
5756
+ {
5757
+ path: ["experiments", "update"],
5758
+ description: "Update an experiment (pass JSON via --data)",
5759
+ mutates: true,
5760
+ requiresWorkspace: true,
5761
+ args: [{ name: "id", description: "Experiment ID", required: true }],
5762
+ handler: async ({ ctx, workspace, args, data }) => {
5763
+ if (!data) throw new CLIError("JSON data required (--data, --file, or stdin)");
5764
+ const result = await requireAuth(ctx).apiClient.updateExperiment(
5765
+ workspace,
5766
+ requireNumber(args, "id"),
5767
+ data
5768
+ );
5769
+ outputSingle(result, ctx, EXPERIMENT_COLUMNS);
5770
+ }
5771
+ },
5772
+ {
5773
+ path: ["experiments", "delete"],
5774
+ description: "Delete a draft experiment or archive a running one",
5775
+ mutates: true,
5776
+ requiresWorkspace: true,
5777
+ args: [{ name: "id", description: "Experiment ID", required: true }],
5778
+ handler: async ({ ctx, workspace, args }) => {
5779
+ const result = await requireAuth(ctx).apiClient.deleteExperiment(
5780
+ workspace,
5781
+ requireNumber(args, "id")
5782
+ );
5783
+ process.stdout.write(`${result.status} experiment ${args["id"]}
5784
+ `);
5785
+ }
5786
+ },
5787
+ {
5788
+ path: ["experiments", "stats"],
5789
+ description: "Get assignment and exposure counts by group",
5790
+ requiresWorkspace: true,
5791
+ args: [{ name: "id", description: "Experiment ID", required: true }],
5792
+ handler: async ({ ctx, workspace, args }) => {
5793
+ const result = await requireAuth(ctx).apiClient.getExperimentStats(
5794
+ workspace,
5795
+ requireNumber(args, "id")
5796
+ );
5797
+ outputData(result.groups, ctx, STATS_COLUMNS);
5798
+ }
5799
+ },
5800
+ {
5801
+ path: ["experiments", "groups", "create"],
5802
+ description: "Create a group on an experiment (pass JSON via --data)",
5803
+ mutates: true,
5804
+ requiresWorkspace: true,
5805
+ args: [{ name: "experiment-id", description: "Experiment ID", required: true }],
5806
+ handler: async ({ ctx, workspace, args, data }) => {
5807
+ if (!data) throw new CLIError("JSON data required (--data, --file, or stdin)");
5808
+ const result = await requireAuth(ctx).apiClient.createExperimentGroup(
5809
+ workspace,
5810
+ requireNumber(args, "experiment-id"),
5811
+ data
5812
+ );
5813
+ outputSingle(result, ctx, GROUP_COLUMNS);
5814
+ }
5815
+ },
5816
+ {
5817
+ path: ["experiments", "groups", "update"],
5818
+ description: "Update a group (pass JSON via --data)",
5819
+ mutates: true,
5820
+ requiresWorkspace: true,
5821
+ args: [
5822
+ { name: "experiment-id", description: "Experiment ID", required: true },
5823
+ { name: "group-id", description: "Group ID", required: true }
5824
+ ],
5825
+ handler: async ({ ctx, workspace, args, data }) => {
5826
+ if (!data) throw new CLIError("JSON data required (--data, --file, or stdin)");
5827
+ const result = await requireAuth(ctx).apiClient.updateExperimentGroup(
5828
+ workspace,
5829
+ requireNumber(args, "experiment-id"),
5830
+ requireNumber(args, "group-id"),
5831
+ data
5832
+ );
5833
+ outputSingle(result, ctx, GROUP_COLUMNS);
5834
+ }
5835
+ },
5836
+ {
5837
+ path: ["experiments", "groups", "delete"],
5838
+ description: "Delete a group with no member assignments",
5839
+ mutates: true,
5840
+ requiresWorkspace: true,
5841
+ args: [
5842
+ { name: "experiment-id", description: "Experiment ID", required: true },
5843
+ { name: "group-id", description: "Group ID", required: true }
5844
+ ],
5845
+ handler: async ({ ctx, workspace, args }) => {
5846
+ await requireAuth(ctx).apiClient.deleteExperimentGroup(
5847
+ workspace,
5848
+ requireNumber(args, "experiment-id"),
5849
+ requireNumber(args, "group-id")
5850
+ );
5851
+ process.stdout.write(`Deleted group ${args["group-id"]}
5852
+ `);
5853
+ }
5854
+ },
5855
+ {
5856
+ path: ["experiments", "members", "list"],
5857
+ description: "List experiment assignments for a member",
5858
+ requiresWorkspace: true,
5859
+ args: [{ name: "member-id", description: "Member ID", required: true }],
5860
+ handler: async ({ ctx, workspace, args }) => {
5861
+ const result = await requireAuth(ctx).apiClient.getMemberExperiments(
5862
+ workspace,
5863
+ requireNumber(args, "member-id")
5864
+ );
5865
+ outputData(result, ctx, MEMBER_ASSIGNMENT_COLUMNS);
5866
+ }
5867
+ }
5868
+ ]
5869
+ };
5870
+
5449
5871
  // src/domains/files.ts
5450
5872
  var COLUMNS11 = [
5451
5873
  COL.id(),
@@ -7502,6 +7924,58 @@ async function getConciergeChat(ctx, chatId) {
7502
7924
  if (!res.ok) throw new Error(`Failed to get concierge chat ${chatId}: ${res.status}`);
7503
7925
  return toCamelChat(await res.json());
7504
7926
  }
7927
+ async function originateConciergeCall(ctx, kind = "concierge_setup") {
7928
+ const res = await fetch(`${baseUrl2(ctx)}/calls`, {
7929
+ method: "POST",
7930
+ headers: authHeaders(ctx),
7931
+ body: JSON.stringify({ kind })
7932
+ });
7933
+ if (res.status === 412) {
7934
+ throw new Error("Your account phone is not verified. Run `gr account verify-phone` first.");
7935
+ }
7936
+ if (!res.ok) {
7937
+ const detail = await res.text();
7938
+ throw new Error(`Failed to start concierge call: ${res.status} ${detail}`);
7939
+ }
7940
+ return await res.json();
7941
+ }
7942
+ async function originateConciergeSms(ctx, kind = "concierge_setup") {
7943
+ const res = await fetch(`${baseUrl2(ctx)}/sms`, {
7944
+ method: "POST",
7945
+ headers: authHeaders(ctx),
7946
+ body: JSON.stringify({ kind })
7947
+ });
7948
+ if (res.status === 412) {
7949
+ throw new Error("Your account phone is not verified. Run `gr account verify-phone` first.");
7950
+ }
7951
+ if (!res.ok) {
7952
+ const detail = await res.text();
7953
+ throw new Error(`Failed to start concierge SMS: ${res.status} ${detail}`);
7954
+ }
7955
+ return await res.json();
7956
+ }
7957
+ async function startAccountPhoneVerify(ctx, phone) {
7958
+ const res = await fetch(`${ctx.envConfig.apiUrl}/api/v2/account/phone/verify/start`, {
7959
+ method: "POST",
7960
+ headers: authHeaders(ctx),
7961
+ body: JSON.stringify({ phone })
7962
+ });
7963
+ if (!res.ok) {
7964
+ const detail = await res.text();
7965
+ throw new Error(`Failed to send verification code: ${res.status} ${detail}`);
7966
+ }
7967
+ }
7968
+ async function confirmAccountPhoneVerify(ctx, code) {
7969
+ const res = await fetch(`${ctx.envConfig.apiUrl}/api/v2/account/phone/verify/confirm`, {
7970
+ method: "POST",
7971
+ headers: authHeaders(ctx),
7972
+ body: JSON.stringify({ code })
7973
+ });
7974
+ if (!res.ok) {
7975
+ const detail = await res.text();
7976
+ throw new Error(`Failed to confirm verification code: ${res.status} ${detail}`);
7977
+ }
7978
+ }
7505
7979
  async function getConciergeChatMessages(ctx, chatId) {
7506
7980
  const res = await fetch(`${baseUrl2(ctx)}/chats/${chatId}/messages`, {
7507
7981
  headers: authHeaders(ctx)
@@ -9234,6 +9708,64 @@ var supportDomain = {
9234
9708
  const result = wid ? await client.commentOnWorkspaceSupportRequest(wid, id, payload) : orgUuid ? await client.commentOnOrgSupportRequest(orgUuid, id, payload) : await client.commentOnSupportRequest(id, payload);
9235
9709
  outputSingle(result, ctx, SUPPORT_COLUMNS);
9236
9710
  }
9711
+ },
9712
+ {
9713
+ path: ["support", "close"],
9714
+ description: "Close one of your support requests (workspace-scoped)",
9715
+ mutates: true,
9716
+ args: [
9717
+ { name: "workspace", aliases: ["w"], description: "Workspace UUID", required: true },
9718
+ { name: "id", aliases: ["uuid"], description: "Support request UUID", required: true },
9719
+ { name: "reason", description: "Optional close reason" }
9720
+ ],
9721
+ handler: async ({ ctx, args, workspace, data }) => {
9722
+ const client = requireAuth(ctx).apiClient;
9723
+ const id = requireString(args, "id");
9724
+ const wid = args.workspace ?? workspace;
9725
+ if (!wid) throw new Error("--workspace is required to close a support request");
9726
+ const input = data ?? {};
9727
+ const reason = input.reason ?? args.reason;
9728
+ const result = await client.closeWorkspaceSupportRequest(
9729
+ wid,
9730
+ id,
9731
+ reason ? { reason } : {}
9732
+ );
9733
+ outputSingle(result, ctx, SUPPORT_COLUMNS);
9734
+ }
9735
+ },
9736
+ {
9737
+ path: ["support", "resource-click"],
9738
+ description: "Record a click on a pushed resource (consume + track usefulness)",
9739
+ mutates: true,
9740
+ args: [
9741
+ { name: "workspace", aliases: ["w"], description: "Workspace UUID", required: true },
9742
+ { name: "id", aliases: ["uuid"], description: "Support request UUID", required: true },
9743
+ {
9744
+ name: "resource-uuid",
9745
+ description: "UUID of the resource that was opened",
9746
+ required: true
9747
+ },
9748
+ {
9749
+ name: "resulting-chat-uuid",
9750
+ description: "Manager Chat UUID a manager_prompt opened (links resource\u2192chat)"
9751
+ }
9752
+ ],
9753
+ handler: async ({ ctx, args, workspace, data }) => {
9754
+ const client = requireAuth(ctx).apiClient;
9755
+ const id = requireString(args, "id");
9756
+ const resourceUuid = requireString(args, "resource-uuid");
9757
+ const wid = args.workspace ?? workspace;
9758
+ if (!wid) throw new Error("--workspace is required to click a resource");
9759
+ const input = data ?? {};
9760
+ const resultingChatUuid = input.resultingChatUuid ?? args["resulting-chat-uuid"];
9761
+ const result = await client.clickWorkspaceSupportRequestResource(
9762
+ wid,
9763
+ id,
9764
+ resourceUuid,
9765
+ resultingChatUuid ? { resultingChatUuid } : {}
9766
+ );
9767
+ outputSingle(result, ctx, SUPPORT_COLUMNS);
9768
+ }
9237
9769
  }
9238
9770
  ]
9239
9771
  };
@@ -10805,6 +11337,7 @@ function registerPublicDomains(registry) {
10805
11337
  registry.register(sitesDomain);
10806
11338
  registry.register(filesDomain);
10807
11339
  registry.register(eventsDomain);
11340
+ registry.register(experimentsDomain);
10808
11341
  registry.register(calendarsDomain);
10809
11342
  registry.register(qualificationsDomain);
10810
11343
  registry.register(personasDomain);
@@ -13018,9 +13551,45 @@ async function listAudioDevices() {
13018
13551
  }
13019
13552
 
13020
13553
  // src/commands/concierge.ts
13554
+ async function conciergeOutboundCommand(hctx) {
13555
+ const ctx = requireAuth(hctx.ctx);
13556
+ const { args } = hctx;
13557
+ if (args.call && args.sms) {
13558
+ throw new CLIError("--call and --sms are mutually exclusive");
13559
+ }
13560
+ if (args.call) {
13561
+ const result = await originateConciergeCall(ctx);
13562
+ process.stderr.write(
13563
+ `Concierge call started. callSid=${result.callSid} startedAt=${result.startedAt}
13564
+ `
13565
+ );
13566
+ return;
13567
+ }
13568
+ if (args.sms) {
13569
+ const result = await originateConciergeSms(ctx);
13570
+ process.stderr.write(
13571
+ `Concierge SMS started. messageSid=${result.messageSid} conversationId=${result.conversationId}
13572
+ `
13573
+ );
13574
+ return;
13575
+ }
13576
+ throw new CLIError(
13577
+ "Use `gr concierge chat --call`, `gr concierge chat --sms`, or `gr concierge chat` for interactive mode."
13578
+ );
13579
+ }
13021
13580
  async function conciergeCommand(hctx) {
13022
13581
  const ctx = requireAuth(hctx.ctx);
13023
13582
  const { args } = hctx;
13583
+ if (args.call || args.sms) {
13584
+ if (args.call && args.sms) {
13585
+ throw new CLIError("--call and --sms are mutually exclusive");
13586
+ }
13587
+ if (args.voice || args.realtime) {
13588
+ throw new CLIError("--call/--sms cannot be combined with --voice or --realtime");
13589
+ }
13590
+ await conciergeOutboundCommand(hctx);
13591
+ return;
13592
+ }
13024
13593
  const chatIdArg = typeof args["chat-id"] === "string" ? args["chat-id"] : void 0;
13025
13594
  const message = typeof args.message === "string" ? args.message : void 0;
13026
13595
  const chat = await getOrCreateConciergeChat(ctx, chatIdArg, !!message);
@@ -13154,6 +13723,16 @@ After directory trust + \`--allow-write\`, the concierge can use \`read_local_fi
13154
13723
 
13155
13724
  ## Verify success
13156
13725
  \`gr workspaces list\` then \`gr workflows list -w <uuid>\` or \`gr agents list -w <uuid>\`
13726
+
13727
+ ## Local phone / voice QA (monorepo dev)
13728
+ \`\`\`bash
13729
+ yarn dev:twilio-ngrok --start-ngrok --url gravityrail.ngrok.io dev.<you> -w acme-demo
13730
+ yarn gr phone setup
13731
+ yarn gr account verify-phone --phone +1\u2026 --allow-write --realm superadmin
13732
+ yarn gr concierge chat --call --allow-write --realm superadmin
13733
+ yarn gr phone outbound --realm acme-demo/acme-admin --workflow-id 1 --allow-write
13734
+ \`\`\`
13735
+ See \`.claude/skills/phone-voice-qa/SKILL.md\` and \`./scripts/e2e-voice-phone-matrix.sh\`.
13157
13736
  `;
13158
13737
  async function getStartedCommand(hctx) {
13159
13738
  const format = hctx.args.format ?? hctx.args.agent;
@@ -13418,7 +13997,10 @@ export {
13418
13997
  authBrowserCommand,
13419
13998
  buildCliConciergeAppContext,
13420
13999
  chatCommand,
14000
+ commandNeedsJsonBody,
13421
14001
  conciergeCommand,
14002
+ conciergeOutboundCommand,
14003
+ confirmAccountPhoneVerify,
13422
14004
  createAuthenticatedContext,
13423
14005
  createConciergeChat,
13424
14006
  createContext,
@@ -13437,6 +14019,7 @@ export {
13437
14019
  handleError,
13438
14020
  isCursorPaginatedResponse,
13439
14021
  isDirectoryTrusted,
14022
+ isLikelySeedPhoneNumber,
13440
14023
  isListResult,
13441
14024
  isPagePaginatedResponse,
13442
14025
  isPaginatedResponse,
@@ -13446,6 +14029,8 @@ export {
13446
14029
  logoutCommand,
13447
14030
  manageCommand,
13448
14031
  organizationsListCommand,
14032
+ originateConciergeCall,
14033
+ originateConciergeSms,
13449
14034
  outputData,
13450
14035
  outputSingle,
13451
14036
  parseArgs,
@@ -13456,9 +14041,12 @@ export {
13456
14041
  requireDirectoryTrust,
13457
14042
  requireNumber,
13458
14043
  requireString,
14044
+ resolveVoicePhoneNumberId,
13459
14045
  simpleCrudCommands,
14046
+ startAccountPhoneVerify,
13460
14047
  stderr,
13461
14048
  unwrapItems,
14049
+ waitForOutboundPhoneCallOutcome,
13462
14050
  whoamiCommand
13463
14051
  };
13464
14052
  //# sourceMappingURL=lib.js.map