@gravity-rail/cli 0.7.3 → 0.8.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
@@ -29,7 +29,7 @@ import {
29
29
  startAccountPhoneVerify,
30
30
  stderr,
31
31
  unwrapItems
32
- } from "./chunk-KTTHOL3B.js";
32
+ } from "./chunk-L5YCPUAM.js";
33
33
  import {
34
34
  AGUIClient,
35
35
  formatTimestamp,
@@ -959,8 +959,8 @@ function requireNumberFrom(args, ...keys) {
959
959
  if (!isNaN(num)) return num;
960
960
  }
961
961
  }
962
- const hint = keys.map((k) => `--${k}`).join(" or ");
963
- throw new Error(`Missing required argument: ${hint}`);
962
+ const hint2 = keys.map((k) => `--${k}`).join(" or ");
963
+ throw new Error(`Missing required argument: ${hint2}`);
964
964
  }
965
965
 
966
966
  // src/domains/agent-tasks.ts
@@ -1102,6 +1102,29 @@ var TOOL_COLUMNS = [
1102
1102
  COL.name(),
1103
1103
  { header: "DESCRIPTION", getValue: (item) => (item.description ?? "").slice(0, 60) }
1104
1104
  ];
1105
+ var ABILITY_COLUMNS = [
1106
+ COL.uuid(),
1107
+ { header: "ABILITY-TYPE", getValue: (item) => item.abilityType ?? "" },
1108
+ COL.enabled(),
1109
+ { header: "CEL-CONDITION", getValue: (item) => item.celCondition ?? "" }
1110
+ ];
1111
+ function requireAbilityScope(args) {
1112
+ const hasWorkflow = typeof args["workflow-id"] === "string";
1113
+ const hasTask = typeof args["task-id"] === "string";
1114
+ if (hasWorkflow === hasTask) {
1115
+ throw new CLIError(
1116
+ "Specify exactly one of --workflow-id or --task-id. Abilities are scoped to a workflow or task, not to the agent row itself."
1117
+ );
1118
+ }
1119
+ if (hasTask) {
1120
+ const taskId = Number(args["task-id"]);
1121
+ if (isNaN(taskId)) throw new CLIError("--task-id must be a number");
1122
+ return { taskId };
1123
+ }
1124
+ const workflowId = Number(args["workflow-id"]);
1125
+ if (isNaN(workflowId)) throw new CLIError("--workflow-id must be a number");
1126
+ return { workflowId };
1127
+ }
1105
1128
  function normalizeChannel(channel) {
1106
1129
  const aliases = {
1107
1130
  phone: "phone-voice",
@@ -1126,11 +1149,16 @@ var agentsDomain = {
1126
1149
  archiveMethod: "archiveAgent",
1127
1150
  unarchiveMethod: "unarchiveAgent",
1128
1151
  columns: COLUMNS,
1129
- paginationSupport: "full"
1152
+ paginationSupport: "full",
1153
+ updateHelp: "Update an agent. Body schema (JSON via --data/--file/stdin), all fields optional:\n name string display name\n description string short summary shown in listings\n systemPrompt string base instructions for the agent\n model string AI model slug (see `gr ai-models list`)\n temperature number sampling temperature (0-2)\n avatarFileId number workspace file ID for the avatar\n config object AgentConfig overrides (advanced)\nAbilities are NOT set here \u2014 they live on a workflow/task scope; use `gr agents abilities` to manage them.",
1154
+ updateExamples: [
1155
+ `gr agents update --id 42 --data '{"name":"Scheduler","temperature":0.4}'`,
1156
+ "gr agents update --id 42 --file agent.json"
1157
+ ]
1130
1158
  }),
1131
1159
  {
1132
1160
  path: ["agents", "resolve-tools"],
1133
- description: "Show resolved runtime tools for an agent in a channel context. Channel accepts canonical values (phone-voice, web-voice, phone-sms, web-chat) or shorthand aliases: phone -> phone-voice, voice -> phone-voice, sms -> phone-sms, web -> web-chat. Unknown values are passed through unchanged.",
1161
+ description: "Show resolved runtime tools for an agent in a channel context. Channel accepts canonical values (phone-voice, web-voice, phone-sms, web-chat) or shorthand aliases: phone -> phone-voice, voice -> phone-voice, sms -> phone-sms, web -> web-chat. Unknown values are passed through unchanged. When neither --task-id nor --task-uuid is supplied the agent's workflow start task is used.",
1134
1162
  requiresWorkspace: true,
1135
1163
  args: [
1136
1164
  { name: "id", description: "Agent ID", required: true },
@@ -1141,22 +1169,142 @@ var agentsDomain = {
1141
1169
  },
1142
1170
  {
1143
1171
  name: "task-id",
1144
- description: "Task ID whose workflow abilities to resolve",
1145
- required: true
1172
+ description: "Numeric task ID whose workflow abilities to resolve. Omit to use the workflow start task (or supply --task-uuid instead).",
1173
+ required: false
1174
+ },
1175
+ {
1176
+ name: "task-uuid",
1177
+ description: "Task UUID as an alternative to --task-id. Omit to use the workflow start task.",
1178
+ required: false
1146
1179
  }
1147
1180
  ],
1148
1181
  handler: async ({ ctx, workspace, args }) => {
1182
+ const rawTaskId = args["task-id"];
1183
+ const rawTaskUuid = args["task-uuid"];
1184
+ if (rawTaskId !== void 0 && rawTaskUuid !== void 0) {
1185
+ throw new Error("Provide at most one of --task-id or --task-uuid, not both");
1186
+ }
1187
+ const options = {
1188
+ channel: normalizeChannel(String(args.channel))
1189
+ };
1190
+ if (typeof rawTaskId === "string") {
1191
+ const n = Number(rawTaskId);
1192
+ if (isNaN(n)) throw new Error("Argument --task-id must be a number");
1193
+ options.taskId = n;
1194
+ }
1195
+ if (typeof rawTaskUuid === "string") {
1196
+ options.taskUuid = rawTaskUuid;
1197
+ }
1149
1198
  const result2 = await requireAuth(ctx).apiClient.resolveAgentTools(
1150
1199
  workspace,
1151
1200
  requireNumber(args, "id"),
1152
- {
1153
- channel: normalizeChannel(String(args.channel)),
1154
- taskId: requireNumber(args, "task-id")
1155
- }
1201
+ options
1156
1202
  );
1157
1203
  const tools = result2.tools ?? result2;
1158
1204
  outputData(Array.isArray(tools) ? tools : [tools], ctx, TOOL_COLUMNS);
1159
1205
  }
1206
+ },
1207
+ {
1208
+ path: ["agents", "abilities", "list"],
1209
+ description: "Show the abilities effective for an agent on a task (read-only). Requires --task-id because abilities are resolved through the task's workflow, not stored on the agent row.",
1210
+ requiresWorkspace: true,
1211
+ columns: ABILITY_COLUMNS,
1212
+ args: [
1213
+ { name: "id", description: "Agent ID", required: true },
1214
+ {
1215
+ name: "task-id",
1216
+ description: "Task ID whose workflow abilities to list",
1217
+ required: true
1218
+ }
1219
+ ],
1220
+ handler: async ({ ctx, workspace, args }) => {
1221
+ const result2 = await requireAuth(ctx).apiClient.listAgentAbilities(
1222
+ workspace,
1223
+ requireNumber(args, "id"),
1224
+ { taskId: requireNumber(args, "task-id") }
1225
+ );
1226
+ outputData(result2, ctx, ABILITY_COLUMNS);
1227
+ }
1228
+ },
1229
+ {
1230
+ path: ["agents", "abilities", "enable"],
1231
+ description: "Enable (create) an ability on a workflow or task scope. Requires exactly one of --workflow-id or --task-id \u2014 abilities are scoped to a workflow/task, never to the agent row. Provide initial --config JSON inline or via --data/--file.",
1232
+ mutates: true,
1233
+ requiresWorkspace: true,
1234
+ args: [
1235
+ { name: "ability-type", description: "Ability type slug (e.g. 'communication:send_sms')", required: true },
1236
+ { name: "workflow-id", description: "Workflow scope (mutually exclusive with --task-id)" },
1237
+ { name: "task-id", description: "Task scope (mutually exclusive with --workflow-id)" }
1238
+ ],
1239
+ examples: [
1240
+ "gr agents abilities enable --workflow-id 7 --ability-type tools:web_search",
1241
+ `gr agents abilities enable --task-id 12 --ability-type integration:data_access --data '{"dataTypeUuid":"..."}'`
1242
+ ],
1243
+ handler: async ({ ctx, workspace, args, data }) => {
1244
+ const abilityType = args["ability-type"];
1245
+ if (typeof abilityType !== "string") {
1246
+ throw new CLIError("--ability-type is required");
1247
+ }
1248
+ const scope = requireAbilityScope(args);
1249
+ const config = data && typeof data === "object" ? data : {};
1250
+ const body = {
1251
+ abilityType,
1252
+ config,
1253
+ ...scope
1254
+ };
1255
+ const result2 = await requireAuth(ctx).apiClient.createAbility(workspace, body);
1256
+ outputSingle(result2, ctx, ABILITY_COLUMNS);
1257
+ }
1258
+ },
1259
+ {
1260
+ path: ["agents", "abilities", "disable"],
1261
+ description: "Disable an ability instance by UUID (sets enabled=false, keeps config). Use `gr agents abilities list` to find the UUID.",
1262
+ mutates: true,
1263
+ requiresWorkspace: true,
1264
+ args: [{ name: "uuid", description: "Ability instance UUID", required: true }],
1265
+ handler: async ({ ctx, workspace, args }) => {
1266
+ const uuid = args.uuid;
1267
+ if (typeof uuid !== "string") throw new CLIError("--uuid is required");
1268
+ const result2 = await requireAuth(ctx).apiClient.updateAbility(workspace, uuid, {
1269
+ enabled: false
1270
+ });
1271
+ outputSingle(result2, ctx, ABILITY_COLUMNS);
1272
+ }
1273
+ },
1274
+ {
1275
+ path: ["agents", "abilities", "configure"],
1276
+ description: "Update an ability instance by UUID. Pass --data/--file JSON to merge into config, and/or --enabled true|false to toggle. config is merged with the existing config server-side.",
1277
+ mutates: true,
1278
+ requiresWorkspace: true,
1279
+ args: [
1280
+ { name: "uuid", description: "Ability instance UUID", required: true },
1281
+ { name: "enabled", description: "Set enabled state (true|false)" }
1282
+ ],
1283
+ examples: [
1284
+ `gr agents abilities configure --uuid <uuid> --data '{"canForward":true}'`,
1285
+ "gr agents abilities configure --uuid <uuid> --enabled true"
1286
+ ],
1287
+ handler: async ({ ctx, workspace, args, data }) => {
1288
+ const uuid = args.uuid;
1289
+ if (typeof uuid !== "string") throw new CLIError("--uuid is required");
1290
+ const body = {};
1291
+ if (data && typeof data === "object") {
1292
+ body.config = data;
1293
+ }
1294
+ if (typeof args.enabled === "string") {
1295
+ if (args.enabled !== "true" && args.enabled !== "false") {
1296
+ throw new CLIError("--enabled must be true or false");
1297
+ }
1298
+ body.enabled = args.enabled === "true";
1299
+ }
1300
+ if (Object.keys(body).length === 0) {
1301
+ throw new CLIError(
1302
+ "Nothing to update: provide --data config and/or --enabled"
1303
+ );
1304
+ }
1305
+ const result2 = await requireAuth(ctx).apiClient.updateAbility(workspace, uuid, body);
1306
+ outputSingle(result2, ctx, ABILITY_COLUMNS);
1307
+ }
1160
1308
  }
1161
1309
  ]
1162
1310
  };
@@ -1382,21 +1530,47 @@ var chatsDomain = {
1382
1530
  // Inspect (aggregated debug view)
1383
1531
  {
1384
1532
  path: ["chats", "inspect"],
1385
- description: "Inspect a chat with messages, assignment state, and data records",
1386
- requiresWorkspace: true,
1533
+ description: "Inspect a chat with messages, assignment state, and data records. Omit -w for concierge (account-level) chats.",
1534
+ requiresWorkspace: false,
1387
1535
  args: [{ name: "id", description: "Chat ID", required: true }],
1388
1536
  handler: async ({ ctx, workspace, args }) => {
1389
1537
  const chatId = requireNumber(args, "id");
1390
- const [chat, messages, ws] = await Promise.all([
1391
- requireAuth(ctx).apiClient.getChat(workspace, chatId),
1392
- requireAuth(ctx).apiClient.getChatMessages(workspace, chatId),
1393
- requireAuth(ctx).apiClient.getWorkspace(workspace)
1394
- ]);
1395
- const isHipaa = ws.features?.includes("hipaa_compliance");
1538
+ const authed = requireAuth(ctx);
1396
1539
  const truncate2 = (content) => {
1397
1540
  const text = typeof content === "string" ? content : JSON.stringify(content);
1398
1541
  return text.slice(0, 200);
1399
1542
  };
1543
+ if (!workspace) {
1544
+ const [chat2, messages2] = await Promise.all([
1545
+ getConciergeChat(authed, chatId),
1546
+ getConciergeChatMessages(authed, chatId)
1547
+ ]);
1548
+ outputSingle(
1549
+ {
1550
+ chat: {
1551
+ id: chat2.id,
1552
+ uuid: chat2.uuid,
1553
+ title: chat2.title,
1554
+ chatType: chat2.chatType,
1555
+ threadId: chat2.threadId,
1556
+ createdAt: chat2.createdAt
1557
+ },
1558
+ messages: messages2.map((m) => ({
1559
+ id: m.id,
1560
+ role: m.role,
1561
+ content: truncate2(m.content)
1562
+ }))
1563
+ },
1564
+ ctx
1565
+ );
1566
+ return;
1567
+ }
1568
+ const [chat, messages, ws] = await Promise.all([
1569
+ authed.apiClient.getChat(workspace, chatId),
1570
+ authed.apiClient.getChatMessages(workspace, chatId),
1571
+ authed.apiClient.getWorkspace(workspace)
1572
+ ]);
1573
+ const isHipaa = ws.features?.includes("hipaa_compliance");
1400
1574
  const result2 = {
1401
1575
  chat: {
1402
1576
  id: chat.id,
@@ -1416,10 +1590,7 @@ var chatsDomain = {
1416
1590
  };
1417
1591
  const assignmentId = chat.assignmentId;
1418
1592
  if (assignmentId) {
1419
- const assignment = await requireAuth(ctx).apiClient.getAssignment(
1420
- workspace,
1421
- assignmentId
1422
- );
1593
+ const assignment = await authed.apiClient.getAssignment(workspace, assignmentId);
1423
1594
  result2.assignment = {
1424
1595
  id: assignment.id,
1425
1596
  status: assignment.status,
@@ -1674,10 +1845,7 @@ var memberRolesDomain = {
1674
1845
  updateMethod: "updateMemberRole",
1675
1846
  deleteMethod: "deleteMemberRole",
1676
1847
  columns: ROLE_COLUMNS2,
1677
- createExamples: [
1678
- "gr member-roles list -w $WID",
1679
- "gr member-roles get -w $WID --id 2"
1680
- ]
1848
+ createExamples: ["gr member-roles list -w $WID", "gr member-roles get -w $WID --id 2"]
1681
1849
  })
1682
1850
  };
1683
1851
 
@@ -1686,10 +1854,13 @@ var COLUMNS4 = [
1686
1854
  COL.id(),
1687
1855
  COL.uuid(),
1688
1856
  COL.name(),
1689
- COL.status(),
1857
+ // TaskResponse has no `status` field — it has `archived`. Surface that
1858
+ // directly so active vs archived is visible in the default table (relevant
1859
+ // once --include-archived mixes both into the listing).
1860
+ { header: "STATUS", getValue: (item) => item.archived ? "archived" : "active" },
1690
1861
  {
1691
1862
  header: "WORKFLOW",
1692
- getValue: (item) => item.workflow_name ?? String(item.workflow_id ?? "")
1863
+ getValue: (item) => String(item.workflowId ?? "")
1693
1864
  },
1694
1865
  COL.created()
1695
1866
  ];
@@ -1707,7 +1878,21 @@ var tasksDomain = {
1707
1878
  deleteMethod: "deleteTask",
1708
1879
  archiveMethod: "archiveTask",
1709
1880
  unarchiveMethod: "unarchiveTask",
1710
- columns: COLUMNS4
1881
+ columns: COLUMNS4,
1882
+ // `tasks list` is workspace-wide by default. Filter to one workflow
1883
+ // with --workflow-id, and opt into archived tasks with
1884
+ // --include-archived (active-only by default, matching the backend).
1885
+ listArgs: [
1886
+ {
1887
+ name: "workflow-id",
1888
+ description: "Filter tasks to a single workflow by ID"
1889
+ },
1890
+ {
1891
+ name: "include-archived",
1892
+ description: "Include archived tasks in the listing",
1893
+ isFlag: true
1894
+ }
1895
+ ]
1711
1896
  }),
1712
1897
  {
1713
1898
  path: ["tasks", "clone"],
@@ -1762,9 +1947,18 @@ var COLUMNS5 = [
1762
1947
  COL.id(),
1763
1948
  COL.uuid(),
1764
1949
  COL.name(),
1765
- { header: "TASKS", getValue: (item) => String(item.taskCount ?? "") },
1950
+ // taskCount is the total (active + archived). Show the active count as the
1951
+ // headline number — matching `tasks list` — and call out archived tasks
1952
+ // separately so the table never overstates how many tasks actually run.
1953
+ { header: "TASKS", getValue: (item) => formatTaskCount(item) },
1766
1954
  COL.created()
1767
1955
  ];
1956
+ function formatTaskCount(item) {
1957
+ const total = Number(item.taskCount ?? 0);
1958
+ const archived = Number(item.archivedTaskCount ?? 0);
1959
+ const active = total - archived;
1960
+ return archived > 0 ? `${active} (+${archived} archived)` : String(active);
1961
+ }
1768
1962
  var NOTE_COLUMNS = [
1769
1963
  COL.id(),
1770
1964
  { header: "TITLE", getValue: (item) => item.title ?? "" },
@@ -1865,13 +2059,21 @@ var workflowsDomain = {
1865
2059
  // Diagram
1866
2060
  {
1867
2061
  path: ["workflows", "diagram"],
1868
- description: "Get workflow diagram",
2062
+ description: "Get workflow diagram (active tasks only by default)",
1869
2063
  requiresWorkspace: true,
1870
- args: [{ name: "id", description: "Workflow ID", required: true }],
2064
+ args: [
2065
+ { name: "id", description: "Workflow ID", required: true },
2066
+ {
2067
+ name: "include-archived",
2068
+ description: "Render archived tasks (distinctly styled) too",
2069
+ isFlag: true
2070
+ }
2071
+ ],
1871
2072
  handler: async ({ ctx, workspace, args }) => {
1872
2073
  const result2 = await requireAuth(ctx).apiClient.getWorkflowDiagram(
1873
2074
  workspace,
1874
- requireNumber(args, "id")
2075
+ requireNumber(args, "id"),
2076
+ { includeArchived: args["include-archived"] === true }
1875
2077
  );
1876
2078
  outputSingle(result2, ctx);
1877
2079
  }
@@ -2388,28 +2590,64 @@ var workspacesDomain = {
2388
2590
  outputSingle(result2, ctx, COLUMNS7);
2389
2591
  }
2390
2592
  },
2391
- // Create a workspace via the public endpoint
2593
+ // Provision a workspace under an organization's active subscription.
2392
2594
  {
2393
2595
  path: ["workspaces", "create"],
2394
- description: "Create a workspace",
2596
+ description: "Provision a workspace in an organization (requires an active subscription)",
2395
2597
  mutates: true,
2396
2598
  requiresWorkspace: false,
2397
- handler: async ({ ctx, data }) => {
2398
- if (!data) throw new CLIError("JSON data required (--data, --file, or stdin)");
2399
- const payload = data;
2400
- if (!payload.organizationId) {
2599
+ args: [
2600
+ {
2601
+ name: "org",
2602
+ aliases: ["org-uuid"],
2603
+ description: "Organization UUID",
2604
+ required: true
2605
+ },
2606
+ { name: "product", description: "Workspace plan product ID", required: true },
2607
+ { name: "name", description: "Workspace name", required: true },
2608
+ { name: "period", description: "Billing period: 'month' or 'year'", default: "month" },
2609
+ { name: "slug", description: "Workspace slug (optional)" },
2610
+ { name: "template", description: "Template workspace UUID to clone (optional)" }
2611
+ ],
2612
+ examples: ['gr workspaces create --org <org-uuid> --product 14 --name "Front Desk"'],
2613
+ handler: async ({ ctx, args }) => {
2614
+ const orgUuid = args.org ?? args["org-uuid"];
2615
+ const name = args.name;
2616
+ const productRaw = args.product;
2617
+ if (!orgUuid) throw new CLIError("Missing required argument: --org <org-uuid>");
2618
+ if (!name) throw new CLIError("Missing required argument: --name");
2619
+ if (!productRaw) {
2401
2620
  throw new CLIError(
2402
- `organizationId is required in --data (e.g. --data '{"organizationId": 1, "name": "My Workspace"}')`
2621
+ "Missing required argument: --product\nList workspace plans with: gr billing products --type workspace"
2403
2622
  );
2404
2623
  }
2624
+ const payload = {
2625
+ name,
2626
+ productId: Number(productRaw),
2627
+ planPeriod: args.period || "month"
2628
+ };
2629
+ if (args.slug) payload.slug = args.slug;
2630
+ if (args.template) payload.templateWorkspaceUuid = args.template;
2405
2631
  try {
2406
- const result2 = await requireAuth(ctx).apiClient.createWorkspace(payload);
2632
+ const result2 = await requireAuth(ctx).apiClient.createOrganizationWorkspace(
2633
+ orgUuid,
2634
+ payload
2635
+ );
2407
2636
  outputSingle(result2, ctx, COLUMNS7);
2637
+ if ((ctx.outputFormat ?? "table") === "table") {
2638
+ const wsUuid = result2?.uuid ?? "<workspace-uuid>";
2639
+ process.stdout.write(
2640
+ `
2641
+ Provisioning started. Watch it come up with:
2642
+ gr workspaces get -w ${wsUuid}
2643
+ `
2644
+ );
2645
+ }
2408
2646
  } catch (err) {
2409
2647
  const statusCode = err?.statusCode;
2410
- if (statusCode === 405) {
2648
+ if (statusCode === 400) {
2411
2649
  throw new CLIError(
2412
- '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)'
2650
+ "Could not create the workspace. The organization needs an active, paid subscription first.\n gr billing products --type org # pick an org plan\n gr billing checkout --product <id> # pay up front\n gr billing status # confirm it is active"
2413
2651
  );
2414
2652
  }
2415
2653
  throw err;
@@ -3463,35 +3701,24 @@ Common optional
3463
3701
  Scope (set exactly one when objectType requires it)
3464
3702
  routineId, workflowId, taskId, dataTypeId, journeyStepId, calendarId, ...
3465
3703
 
3466
- chat:create actionParams (outbound voice/SMS)
3467
- {"workflowId":<id>,"channel":"phone-voice"|"phone-sms","phoneNumberId":<id>,
3468
- "inboxId":<optional>,"assistantEnabled":<bool>}
3704
+ chat:create actionParams (outbound voice/SMS \u2014 starts a workflow chat)
3705
+ {"workflowUUID":"<workflow-uuid>","channel":"phone-voice"|"phone-sms",
3706
+ "phoneNumberUUID":"<uuid>","inboxUUID":"<optional-uuid>","assistantEnabled":<bool>}
3707
+ Keys are UUID strings: workflowUUID (not workflowId), phoneNumberUUID (not
3708
+ phoneNumberId), inboxUUID (not inboxId). Unknown keys are rejected.
3709
+
3710
+ sms:send actionParams (send an SMS, or start an SMS workflow chat)
3711
+ One-off text: {"phoneNumberUUID":"<from-number-uuid>","messageTemplate":"Hi {{name}} \u2026"}
3712
+ Workflow chat: {"phoneNumberUUID":"<from-number-uuid>","workflowUUID":"<workflow-uuid>",
3713
+ "initialUserMessage":"<optional seed>"}
3714
+ phoneNumberUUID (the from-number) is required. The message body is messageTemplate
3715
+ (supports {{record/member}} template vars) \u2014 there is no "body"/"message" key.
3716
+ Provide messageTemplate for a one-off text, OR workflowUUID to start a chat.
3469
3717
 
3470
3718
  member:label actionParams
3471
3719
  {"addLabelIds":["<uuid>"],"removeLabelIds":["<uuid>"]}
3472
3720
  `;
3473
3721
 
3474
- // src/lib/routines/field-help.ts
3475
- var ROUTINE_FIELD_REFERENCE = `
3476
- Field reference for --data JSON (create/update):
3477
-
3478
- Core
3479
- name (string) Display name
3480
- workflowId (number) Workflow run when the routine fires
3481
- targetMemberId (number) Member who receives the scheduled outreach
3482
-
3483
- Schedule
3484
- scheduleEnabled (bool) Master on/off for the cron trigger
3485
- scheduleMode (string) "CRON" (only mode today)
3486
- cronString (string) Standard cron, e.g. "0 7 * * *"
3487
- timezone (string, optional) IANA name, e.g. "America/Los_Angeles"
3488
- Defaults to workspace timezone, else UTC
3489
-
3490
- Visibility / lifecycle
3491
- archived (bool) Archive flag on update
3492
- event rules Use gr events rules create \u2014 not inline here
3493
- `;
3494
-
3495
3722
  // src/domains/events.ts
3496
3723
  var RULE_COLUMNS = [
3497
3724
  COL.id(),
@@ -3499,15 +3726,6 @@ var RULE_COLUMNS = [
3499
3726
  { header: "EVENT", getValue: (item) => item.eventType ?? "" },
3500
3727
  { header: "ACTIVE", getValue: (item) => String(item.active ?? "") }
3501
3728
  ];
3502
- var ROUTINE_COLUMNS = [
3503
- COL.id(),
3504
- COL.name(),
3505
- { header: "MODE", getValue: (item) => item.scheduleMode ?? "" },
3506
- { header: "CRON", getValue: (item) => item.cronString ?? "" },
3507
- { header: "TZ", getValue: (item) => item.timezone ?? "UTC" },
3508
- { header: "ENABLED", getValue: (item) => String(item.scheduleEnabled ?? "") },
3509
- { header: "NEXT RUN", getValue: (item) => item.nextRun ?? "" }
3510
- ];
3511
3729
  var eventRulesCrudNoList = crudCommands({
3512
3730
  prefix: ["events", "rules"],
3513
3731
  entityName: "event rule",
@@ -3531,8 +3749,8 @@ ${EVENT_RULE_FIELD_REFERENCE}`,
3531
3749
  `gr events rules create -w $WID --allow-write --data '{"name":"AM voice check-in",`,
3532
3750
  ' "objectType":"routine","eventType":"routine:triggered","actionType":"chat:create",',
3533
3751
  ' "routineId":<routine-id>,',
3534
- ' "actionParams":{"workflowId":<workflow-id>,"channel":"phone-voice",',
3535
- ` "phoneNumberId":<workspace-phone-id>}}'`,
3752
+ ' "actionParams":{"workflowUUID":"<workflow-uuid>","channel":"phone-voice",',
3753
+ ` "phoneNumberUUID":"<workspace-phone-uuid>"}}'`,
3536
3754
  "",
3537
3755
  ' PM SMS variant: use "channel":"phone-sms" and a second rule on the same routineId.',
3538
3756
  " Prereq: org phone assigned + gr phone-numbers link; workflow published with start task.",
@@ -3670,74 +3888,6 @@ var eventsDomain = {
3670
3888
  const result2 = await requireAuth(ctx).apiClient.searchEventRules(workspace, opts);
3671
3889
  outputData(result2, ctx, RULE_COLUMNS);
3672
3890
  }
3673
- },
3674
- // Routines
3675
- ...crudCommands({
3676
- prefix: ["routines"],
3677
- entityName: "routine",
3678
- listMethod: "getRoutines",
3679
- getMethod: "getRoutine",
3680
- createMethod: "createRoutine",
3681
- updateMethod: "updateRoutine",
3682
- deleteMethod: "deleteRoutine",
3683
- paginationSupport: "full",
3684
- columns: ROUTINE_COLUMNS,
3685
- createHelp: `Create a routine
3686
-
3687
- ${ROUTINE_FIELD_REFERENCE}`,
3688
- updateHelp: `Update a routine
3689
-
3690
- ${ROUTINE_FIELD_REFERENCE}`,
3691
- createExamples: [
3692
- "Daily 7am check-in in the member\u2019s timezone (cron is interpreted in `timezone`):",
3693
- `gr routines create -w $WID --allow-write --data '{"name":"AM check-in",`,
3694
- ' "scheduleEnabled":true,"scheduleMode":"CRON","cronString":"0 7 * * *",',
3695
- ` "timezone":"America/Los_Angeles","targetMemberId":1,"workflowId":2}'`,
3696
- " (timezone is an IANA name; omit it to default to the workspace timezone, else UTC.",
3697
- ' Without it, "0 7 * * *" fires at 07:00 UTC, not 7am local.)'
3698
- ]
3699
- }),
3700
- {
3701
- path: ["routines", "run"],
3702
- description: "Run a routine now",
3703
- mutates: true,
3704
- requiresWorkspace: true,
3705
- args: [{ name: "id", description: "Routine ID", required: true }],
3706
- handler: async ({ ctx, workspace, args, data }) => {
3707
- const result2 = await requireAuth(ctx).apiClient.runRoutine(
3708
- workspace,
3709
- requireNumber(args, "id"),
3710
- data ?? {}
3711
- );
3712
- outputSingle(result2, ctx);
3713
- }
3714
- },
3715
- {
3716
- path: ["routines", "runs", "list"],
3717
- description: "List runs for a routine",
3718
- requiresWorkspace: true,
3719
- args: [
3720
- {
3721
- name: "id",
3722
- description: "Routine ID (aligns with routines get/run/update/delete)"
3723
- },
3724
- { name: "status", description: "Filter by run status" },
3725
- { name: "page", description: "Page number (1-indexed)" },
3726
- { name: "page-size", description: "Page size (max 100)" }
3727
- ],
3728
- handler: async ({ ctx, workspace, args }) => {
3729
- const routineId = args.id ? requireNumber(args, "id") : void 0;
3730
- const page = args.page ? requireNumber(args, "page") : void 0;
3731
- const pageSize = args["page-size"] ? requireNumber(args, "page-size") : void 0;
3732
- const status = args.status ?? void 0;
3733
- const result2 = await requireAuth(ctx).apiClient.getRoutineRuns(workspace, {
3734
- ...routineId !== void 0 ? { routineId } : {},
3735
- ...page !== void 0 ? { page } : {},
3736
- ...pageSize !== void 0 ? { pageSize } : {},
3737
- ...status ? { status } : {}
3738
- });
3739
- outputData(result2, ctx);
3740
- }
3741
3891
  }
3742
3892
  ]
3743
3893
  };
@@ -5354,6 +5504,178 @@ var sitesDomain = {
5354
5504
  ]
5355
5505
  };
5356
5506
 
5507
+ // src/domains/abilities.ts
5508
+ import { randomUUID } from "crypto";
5509
+ var ATTACHED_COLUMNS = [
5510
+ { header: "UUID", getValue: (item) => item.uuid ?? "" },
5511
+ { header: "TYPE", getValue: (item) => item.abilityType ?? "" },
5512
+ { header: "ENABLED", getValue: (item) => item.enabled === false ? "" : "yes" },
5513
+ { header: "CEL", getValue: (item) => item.celCondition ?? "" }
5514
+ ];
5515
+ function toAbilityData(a) {
5516
+ return {
5517
+ uuid: a.uuid,
5518
+ abilityType: a.abilityType,
5519
+ config: a.config ?? {},
5520
+ enabled: a.enabled ?? true,
5521
+ ...a.celCondition ? { celCondition: a.celCondition } : {}
5522
+ };
5523
+ }
5524
+ var TYPE_COLUMNS = [
5525
+ { header: "SLUG", getValue: (item) => item.slug ?? "" },
5526
+ { header: "TITLE", getValue: (item) => item.title ?? "" },
5527
+ { header: "CATEGORY", getValue: (item) => item.category ?? "" },
5528
+ {
5529
+ header: "CHANNELS",
5530
+ // null channels means "all channels" — show that explicitly so an empty
5531
+ // cell never reads as "no channels".
5532
+ getValue: (item) => Array.isArray(item.channels) ? item.channels.join(",") : "all"
5533
+ },
5534
+ { header: "SINGLETON", getValue: (item) => item.singleton ? "yes" : "" },
5535
+ { header: "FEATURE-GATE", getValue: (item) => item.requiredFeature ?? "" }
5536
+ ];
5537
+ var abilitiesDomain = {
5538
+ name: "abilities",
5539
+ description: "Inspect ability types and attach/detach them on a task",
5540
+ commands: [
5541
+ {
5542
+ path: ["abilities", "list"],
5543
+ description: "List builtin ability types with their channel constraints and param specs. Table shows SLUG, TITLE, CATEGORY, CHANNELS (all = every channel), SINGLETON, FEATURE-GATE; use -o json for full specs including the params JSON Schema.",
5544
+ requiresWorkspace: true,
5545
+ columns: TYPE_COLUMNS,
5546
+ args: [
5547
+ {
5548
+ name: "include-system",
5549
+ description: "Also list auto-injected system abilities (hidden by default)",
5550
+ isFlag: true
5551
+ }
5552
+ ],
5553
+ handler: async (hctx) => {
5554
+ const includeSystem = hctx.args["include-system"] === true;
5555
+ const result2 = await requireAuth(hctx.ctx).apiClient.getAbilityTypes(
5556
+ hctx.workspace,
5557
+ includeSystem ? { includeSystem: true } : {}
5558
+ );
5559
+ outputData(result2.types ?? [], hctx.ctx, TYPE_COLUMNS);
5560
+ }
5561
+ },
5562
+ {
5563
+ path: ["abilities", "get"],
5564
+ description: "Show the full spec for one ability type: title, description, category, channels, singleton/system flags, required feature, and the JSON Schema of its params model.",
5565
+ requiresWorkspace: true,
5566
+ args: [
5567
+ {
5568
+ name: "name",
5569
+ aliases: ["slug"],
5570
+ description: "Ability type slug (e.g. 'communication:phone_call_tools')",
5571
+ required: true
5572
+ }
5573
+ ],
5574
+ handler: async (hctx) => {
5575
+ const slug = hctx.args.name ?? hctx.args.slug;
5576
+ if (!slug) throw new CLIError("--name is required");
5577
+ const result2 = await requireAuth(hctx.ctx).apiClient.getAbilityType(
5578
+ hctx.workspace,
5579
+ slug
5580
+ );
5581
+ outputSingle(result2, hctx.ctx);
5582
+ }
5583
+ },
5584
+ {
5585
+ path: ["abilities", "attached"],
5586
+ description: "List a task's task-specific abilities (excludes workflow-inherited).",
5587
+ requiresWorkspace: true,
5588
+ columns: ATTACHED_COLUMNS,
5589
+ args: [{ name: "task", description: "Task ID", required: true }],
5590
+ handler: async (hctx) => {
5591
+ const task = await requireAuth(hctx.ctx).apiClient.getTask(
5592
+ hctx.workspace,
5593
+ requireNumber(hctx.args, "task")
5594
+ );
5595
+ outputData(task.abilities ?? [], hctx.ctx, ATTACHED_COLUMNS);
5596
+ }
5597
+ },
5598
+ {
5599
+ path: ["abilities", "attach"],
5600
+ description: "Attach an ability to a task. Reads the task, appends the new ability (generating its uuid), and saves \u2014 so existing abilities aren't lost. Find slugs via 'gr abilities list' and the config schema via 'gr abilities get'.",
5601
+ mutates: true,
5602
+ requiresWorkspace: true,
5603
+ args: [
5604
+ { name: "task", description: "Task ID to attach to", required: true },
5605
+ {
5606
+ name: "type",
5607
+ aliases: ["ability-type", "slug"],
5608
+ description: "Ability type slug, e.g. 'integration:data_access'",
5609
+ required: true
5610
+ },
5611
+ {
5612
+ name: "config",
5613
+ description: `Ability config as a JSON object, e.g. '{"pauseOnEscalate":true}'`
5614
+ },
5615
+ { name: "cel", description: "Optional CEL activation condition" },
5616
+ {
5617
+ name: "disabled",
5618
+ description: "Attach in a disabled state (default: enabled)",
5619
+ isFlag: true
5620
+ }
5621
+ ],
5622
+ handler: async (hctx) => {
5623
+ const api = requireAuth(hctx.ctx).apiClient;
5624
+ const taskId = requireNumber(hctx.args, "task");
5625
+ const abilityType = hctx.args.type ?? hctx.args["ability-type"] ?? hctx.args.slug;
5626
+ if (!abilityType) throw new CLIError("--type is required (an ability type slug)");
5627
+ let config = {};
5628
+ if (hctx.args.config) {
5629
+ try {
5630
+ config = JSON.parse(hctx.args.config);
5631
+ } catch {
5632
+ throw new CLIError("--config must be a valid JSON object");
5633
+ }
5634
+ }
5635
+ const task = await api.getTask(hctx.workspace, taskId);
5636
+ const existing = (task.abilities ?? []).map(toAbilityData);
5637
+ const next = {
5638
+ uuid: randomUUID(),
5639
+ abilityType,
5640
+ config,
5641
+ enabled: hctx.args.disabled !== true,
5642
+ ...hctx.args.cel ? { celCondition: hctx.args.cel } : {}
5643
+ };
5644
+ const updated = await api.updateTask(hctx.workspace, taskId, {
5645
+ abilities: [...existing, next]
5646
+ });
5647
+ outputData(updated.abilities ?? [], hctx.ctx, ATTACHED_COLUMNS);
5648
+ }
5649
+ },
5650
+ {
5651
+ path: ["abilities", "detach"],
5652
+ description: "Detach an ability from a task by its uuid (from `gr abilities attached`). Re-saves the remaining task-specific abilities so the rest are preserved.",
5653
+ mutates: true,
5654
+ requiresWorkspace: true,
5655
+ args: [
5656
+ { name: "task", description: "Task ID", required: true },
5657
+ { name: "uuid", description: "UUID of the attached ability to remove", required: true }
5658
+ ],
5659
+ handler: async (hctx) => {
5660
+ const api = requireAuth(hctx.ctx).apiClient;
5661
+ const taskId = requireNumber(hctx.args, "task");
5662
+ const uuid = hctx.args.uuid;
5663
+ if (!uuid) throw new CLIError("--uuid is required");
5664
+ const task = await api.getTask(hctx.workspace, taskId);
5665
+ const existing = task.abilities ?? [];
5666
+ const remaining = existing.filter((a) => a.uuid !== uuid).map(toAbilityData);
5667
+ if (remaining.length === existing.length) {
5668
+ throw new CLIError(`No attached ability with uuid '${uuid}' on task ${taskId}`);
5669
+ }
5670
+ const updated = await api.updateTask(hctx.workspace, taskId, {
5671
+ abilities: remaining
5672
+ });
5673
+ outputData(updated.abilities ?? [], hctx.ctx, ATTACHED_COLUMNS);
5674
+ }
5675
+ }
5676
+ ]
5677
+ };
5678
+
5357
5679
  // src/domains/access-grants.ts
5358
5680
  var COLUMNS14 = [
5359
5681
  COL.id(),
@@ -5875,6 +6197,149 @@ var authManageDomain = {
5875
6197
  ]
5876
6198
  };
5877
6199
 
6200
+ // src/domains/billing.ts
6201
+ function priceSummary(product) {
6202
+ const plans = product.plans ?? {};
6203
+ const abbr = { month: "mo", year: "yr", quarter: "qtr" };
6204
+ const parts = [];
6205
+ for (const [period, plan] of Object.entries(plans)) {
6206
+ const cents = plan?.costPerPeriod ?? 0;
6207
+ if (!plan?.stripePriceId) continue;
6208
+ parts.push(`$${(cents / 100).toFixed(0)}/${abbr[period] ?? period}`);
6209
+ }
6210
+ return parts.join(", ") || "\u2014";
6211
+ }
6212
+ var PRODUCT_COLUMNS2 = [
6213
+ { header: "ID", getValue: (p) => String(p.id ?? "") },
6214
+ { header: "NAME", getValue: (p) => p.name ?? "" },
6215
+ { header: "TYPE", getValue: (p) => String(p.productType ?? "") },
6216
+ { header: "PRICE", getValue: (p) => priceSummary(p) },
6217
+ {
6218
+ header: "TRIAL",
6219
+ getValue: (p) => {
6220
+ const days = p.trialPeriodDays ?? 0;
6221
+ return days > 0 ? `${days}d` : "\u2014";
6222
+ }
6223
+ }
6224
+ ];
6225
+ var SUBSCRIPTION_COLUMNS = [
6226
+ COL.id(),
6227
+ COL.status(),
6228
+ { header: "PLAN", getValue: (s) => String(s.plan_name ?? s.productName ?? s.plan ?? "") },
6229
+ { header: "PERIOD", getValue: (s) => String(s.planPeriod ?? s.plan_period ?? "") },
6230
+ {
6231
+ header: "ORG",
6232
+ getValue: (s) => String(s.organizationUuid ?? s.customerOrganizationUuid ?? "")
6233
+ }
6234
+ ];
6235
+ function isTable(ctx) {
6236
+ return (ctx.outputFormat ?? "table") === "table";
6237
+ }
6238
+ function hint(ctx, lines) {
6239
+ if (!isTable(ctx)) return;
6240
+ process.stdout.write("\n" + lines.join("\n") + "\n");
6241
+ }
6242
+ function productType(args) {
6243
+ const t = args.type ?? args["product-type"];
6244
+ if (t === "org" || t === "workspace") return t;
6245
+ return void 0;
6246
+ }
6247
+ var billingDomain = {
6248
+ name: "billing",
6249
+ description: "Browse plans, start checkout, and check subscription status",
6250
+ commands: [
6251
+ {
6252
+ path: ["billing", "products"],
6253
+ description: "List self-service plans you can purchase",
6254
+ requiresWorkspace: false,
6255
+ args: [
6256
+ {
6257
+ name: "type",
6258
+ description: "Filter by plan type: 'org' or 'workspace'"
6259
+ }
6260
+ ],
6261
+ examples: ["gr billing products --type org"],
6262
+ handler: async ({ ctx, args }) => {
6263
+ const products = await requireAuth(ctx).apiClient.listSelfServeProducts(
6264
+ productType(args)
6265
+ );
6266
+ outputData(products, ctx, PRODUCT_COLUMNS2);
6267
+ if (products.length === 0) {
6268
+ hint(ctx, ["No purchasable plans found. (Free plans are not sold via the CLI.)"]);
6269
+ } else {
6270
+ hint(ctx, [
6271
+ "Next \u2014 buy an organization plan to get started:",
6272
+ " gr billing checkout --product <id> [--period month|year]"
6273
+ ]);
6274
+ }
6275
+ }
6276
+ },
6277
+ {
6278
+ path: ["billing", "checkout"],
6279
+ description: "Get a Stripe payment link to purchase an organization plan",
6280
+ mutates: true,
6281
+ requiresWorkspace: false,
6282
+ args: [
6283
+ { name: "product", description: "Product ID of the org plan to buy", required: true },
6284
+ { name: "period", description: "Billing period: 'month' or 'year'", default: "month" }
6285
+ ],
6286
+ examples: ["gr billing checkout --product 12 --period month"],
6287
+ handler: async ({ ctx, args }) => {
6288
+ const productId = Number(requireString(args, "product"));
6289
+ const period = args.period || "month";
6290
+ const result2 = await requireAuth(ctx).apiClient.createCheckoutSession(
6291
+ productId,
6292
+ period
6293
+ );
6294
+ if (isTable(ctx)) {
6295
+ process.stdout.write(
6296
+ `
6297
+ \u{1F4B3} Open this link in a browser to complete payment:
6298
+
6299
+ ${result2.checkoutUrl}
6300
+
6301
+ Once payment succeeds, your organization and subscription are created automatically.
6302
+ Then check it landed:
6303
+ gr billing status
6304
+ `
6305
+ );
6306
+ } else {
6307
+ outputSingle(result2, ctx);
6308
+ }
6309
+ }
6310
+ },
6311
+ {
6312
+ path: ["billing", "status"],
6313
+ description: "Show your subscriptions and what to do next",
6314
+ requiresWorkspace: false,
6315
+ examples: ["gr billing status"],
6316
+ handler: async ({ ctx }) => {
6317
+ const subs = await requireAuth(ctx).apiClient.getSubscriptions();
6318
+ outputData(subs, ctx, SUBSCRIPTION_COLUMNS);
6319
+ const active = subs.find(
6320
+ (s) => ["active", "trialing"].includes(String(s.status ?? s.subscriptionStatus ?? ""))
6321
+ );
6322
+ if (!active) {
6323
+ hint(ctx, [
6324
+ "No active subscription yet. To get started:",
6325
+ " gr billing products --type org # pick a plan",
6326
+ " gr billing checkout --product <id> # pay up front"
6327
+ ]);
6328
+ return;
6329
+ }
6330
+ const orgUuid = String(
6331
+ active.organizationUuid ?? active.customerOrganizationUuid ?? "<org-uuid>"
6332
+ );
6333
+ hint(ctx, [
6334
+ "You have an active subscription. Add a workspace:",
6335
+ " gr billing products --type workspace # pick a workspace plan",
6336
+ ` gr workspaces create --org ${orgUuid} --product <id> --name "My Workspace"`
6337
+ ]);
6338
+ }
6339
+ }
6340
+ ]
6341
+ };
6342
+
5878
6343
  // src/domains/billing-portal.ts
5879
6344
  var INVOICE_COLUMNS = [
5880
6345
  { header: "ID", getValue: (i) => String(i.id) },
@@ -7575,9 +8040,22 @@ function formatPhoneNumberBehaviorMatrix(phone, outboundUsers, warnings = collec
7575
8040
  lines.push(`UUID: ${phone.uuid} | Mapped: ${phone.isMapped ? "yes" : "no"}`);
7576
8041
  lines.push("");
7577
8042
  lines.push("## Channels");
7578
- lines.push(` Voice: ${phone.enableVoice ? "on" : "off"} SMS: ${phone.enableSms ? "on" : "off"} WhatsApp: ${phone.enableWhatsApp ? "on" : "off"}`);
8043
+ lines.push(
8044
+ ` Voice: ${phone.enableVoice ? "on" : "off"} SMS: ${phone.enableSms ? "on" : "off"} WhatsApp: ${phone.enableWhatsApp ? "on" : "off"}`
8045
+ );
7579
8046
  lines.push(` Assistant: ${phone.assistantEnabled ? "on" : "off"}`);
7580
8047
  lines.push("");
8048
+ if (phone.enableVoice) {
8049
+ lines.push("## Channel defaults (auto-injected on voice)");
8050
+ lines.push(
8051
+ " Voice channels auto-add the hang_up_call tool (system:voice_channel);"
8052
+ );
8053
+ lines.push(
8054
+ " it is not listed by `gr agents abilities list`. See `gr abilities get"
8055
+ );
8056
+ lines.push(" --name communication:phone_call_tools` for the voice ability spec.");
8057
+ lines.push("");
8058
+ }
7581
8059
  lines.push("## Inbound route");
7582
8060
  lines.push(
7583
8061
  ` defaultWorkflowId (inbound): ${phone.defaultWorkflowId ?? "null"} restartDelay: ${phone.restartDelay ?? "null"}s`
@@ -7658,9 +8136,7 @@ async function findOutboundPhoneUsers(client, workspace, phone) {
7658
8136
  }
7659
8137
  page += 1;
7660
8138
  if (page > 50) {
7661
- process.stderr.write(
7662
- "Warning: event-rules lookup truncated at 5000 results (50 pages)\n"
7663
- );
8139
+ process.stderr.write("Warning: event-rules lookup truncated at 5000 results (50 pages)\n");
7664
8140
  break;
7665
8141
  }
7666
8142
  }
@@ -10775,6 +11251,115 @@ var reportsDomain = {
10775
11251
  ]
10776
11252
  };
10777
11253
 
11254
+ // src/lib/routines/field-help.ts
11255
+ var ROUTINE_FIELD_REFERENCE = `
11256
+ Field reference for --data JSON (create/update):
11257
+
11258
+ Core
11259
+ name (string) Display name
11260
+ workflowId (number) Workflow run when the routine fires
11261
+ targetMemberId (number) Member who receives the scheduled outreach
11262
+
11263
+ Schedule
11264
+ scheduleEnabled (bool) Master on/off for the cron trigger
11265
+ scheduleMode (string) "CRON" (only mode today)
11266
+ cronString (string) Standard cron, e.g. "0 7 * * *"
11267
+ timezone (string, optional) IANA name, e.g. "America/Los_Angeles"
11268
+ Defaults to workspace timezone, else UTC
11269
+
11270
+ Visibility / lifecycle
11271
+ archived (bool) Archive flag on update
11272
+ event rules Use gr events rules create \u2014 not inline here
11273
+ `;
11274
+
11275
+ // src/domains/routines.ts
11276
+ var ROUTINE_COLUMNS = [
11277
+ COL.id(),
11278
+ COL.name(),
11279
+ { header: "MODE", getValue: (item) => item.scheduleMode ?? "" },
11280
+ { header: "CRON", getValue: (item) => item.cronString ?? "" },
11281
+ { header: "TZ", getValue: (item) => item.timezone ?? "UTC" },
11282
+ { header: "ENABLED", getValue: (item) => String(item.scheduleEnabled ?? "") },
11283
+ { header: "NEXT RUN", getValue: (item) => item.nextRun ?? "" }
11284
+ ];
11285
+ var routinesDomain = {
11286
+ name: "routines",
11287
+ description: "Schedule recurring outbound (e.g. a daily SMS/voice check-in via cron)",
11288
+ commands: [
11289
+ ...crudCommands({
11290
+ prefix: ["routines"],
11291
+ entityName: "routine",
11292
+ listMethod: "getRoutines",
11293
+ getMethod: "getRoutine",
11294
+ createMethod: "createRoutine",
11295
+ updateMethod: "updateRoutine",
11296
+ deleteMethod: "deleteRoutine",
11297
+ paginationSupport: "full",
11298
+ columns: ROUTINE_COLUMNS,
11299
+ createHelp: `Create a routine
11300
+
11301
+ ${ROUTINE_FIELD_REFERENCE}`,
11302
+ updateHelp: `Update a routine
11303
+
11304
+ ${ROUTINE_FIELD_REFERENCE}`,
11305
+ createExamples: [
11306
+ "Daily 7am check-in in the member\u2019s timezone (cron is interpreted in `timezone`):",
11307
+ `gr routines create -w $WID --allow-write --data '{"name":"AM check-in",`,
11308
+ ' "scheduleEnabled":true,"scheduleMode":"CRON","cronString":"0 7 * * *",',
11309
+ ` "timezone":"America/Los_Angeles","targetMemberId":1,"workflowId":2}'`,
11310
+ " (timezone is an IANA name; omit it to default to the workspace timezone, else UTC.",
11311
+ ' Without it, "0 7 * * *" fires at 07:00 UTC, not 7am local.)',
11312
+ "",
11313
+ "To actually send via SMS/voice, pair the routine with an event rule:",
11314
+ ' gr events rules create \u2014 objectType "routine", eventType "routine:triggered",',
11315
+ ' actionType "chat:create". See `gr events --help`.'
11316
+ ]
11317
+ }),
11318
+ {
11319
+ path: ["routines", "run"],
11320
+ description: "Run a routine now",
11321
+ mutates: true,
11322
+ requiresWorkspace: true,
11323
+ args: [{ name: "id", description: "Routine ID", required: true }],
11324
+ handler: async ({ ctx, workspace, args, data }) => {
11325
+ const result2 = await requireAuth(ctx).apiClient.runRoutine(
11326
+ workspace,
11327
+ requireNumber(args, "id"),
11328
+ data ?? {}
11329
+ );
11330
+ outputSingle(result2, ctx);
11331
+ }
11332
+ },
11333
+ {
11334
+ path: ["routines", "runs", "list"],
11335
+ description: "List runs for a routine",
11336
+ requiresWorkspace: true,
11337
+ args: [
11338
+ {
11339
+ name: "id",
11340
+ description: "Routine ID (aligns with routines get/run/update/delete)"
11341
+ },
11342
+ { name: "status", description: "Filter by run status" },
11343
+ { name: "page", description: "Page number (1-indexed)" },
11344
+ { name: "page-size", description: "Page size (max 100)" }
11345
+ ],
11346
+ handler: async ({ ctx, workspace, args }) => {
11347
+ const routineId = args.id ? requireNumber(args, "id") : void 0;
11348
+ const page = args.page ? requireNumber(args, "page") : void 0;
11349
+ const pageSize = args["page-size"] ? requireNumber(args, "page-size") : void 0;
11350
+ const status = args.status ?? void 0;
11351
+ const result2 = await requireAuth(ctx).apiClient.getRoutineRuns(workspace, {
11352
+ ...routineId !== void 0 ? { routineId } : {},
11353
+ ...page !== void 0 ? { page } : {},
11354
+ ...pageSize !== void 0 ? { pageSize } : {},
11355
+ ...status ? { status } : {}
11356
+ });
11357
+ outputData(result2, ctx);
11358
+ }
11359
+ }
11360
+ ]
11361
+ };
11362
+
10778
11363
  // src/domains/subscriptions.ts
10779
11364
  var COLUMNS27 = [
10780
11365
  COL.id(),
@@ -12637,11 +13222,13 @@ function registerPublicDomains(registry) {
12637
13222
  registry.register(sitesDomain);
12638
13223
  registry.register(filesDomain);
12639
13224
  registry.register(eventsDomain);
13225
+ registry.register(routinesDomain);
12640
13226
  registry.register(experimentsDomain);
12641
13227
  registry.register(calendarsDomain);
12642
13228
  registry.register(qualificationsDomain);
12643
13229
  registry.register(personasDomain);
12644
13230
  registry.register(journeysDomain);
13231
+ registry.register(abilitiesDomain);
12645
13232
  registry.register(phoneNumbersDomain);
12646
13233
  registry.register(inboxesDomain);
12647
13234
  registry.register(operatorGroupsDomain);
@@ -12654,6 +13241,7 @@ function registerPublicDomains(registry) {
12654
13241
  registry.register(mcpServersDomain);
12655
13242
  registry.register(apiKeysDomain);
12656
13243
  registry.register(subscriptionsDomain);
13244
+ registry.register(billingDomain);
12657
13245
  registry.register(supportDomain);
12658
13246
  registry.register(reportsDomain);
12659
13247
  registry.register(aiModelsDomain);
@@ -13069,13 +13657,25 @@ async function runConciergeAguiLoop(client, initialMessages, onEvent, opts) {
13069
13657
  process.stderr.write(
13070
13658
  " [HITL] Unsupported interrupt in CLI \u2014 open app.gravityrail.com/setup to continue.\n"
13071
13659
  );
13072
- break;
13660
+ return {
13661
+ streamText,
13662
+ hasToolCalls,
13663
+ unsupportedHitl: normalizeUnsupportedHitl(hitlPayload)
13664
+ };
13073
13665
  }
13074
13666
  resume = next;
13075
13667
  streamText = "";
13076
13668
  }
13077
13669
  return { streamText, hasToolCalls };
13078
13670
  }
13671
+ function normalizeUnsupportedHitl(raw) {
13672
+ const payload = raw && typeof raw === "object" ? raw : {};
13673
+ return {
13674
+ type: typeof payload.type === "string" ? payload.type : void 0,
13675
+ interruptId: typeof payload.interrupt_id === "string" ? payload.interrupt_id : void 0,
13676
+ payload
13677
+ };
13678
+ }
13079
13679
 
13080
13680
  // src/lib/realtime-client.ts
13081
13681
  import { EventEmitter } from "events";
@@ -14147,8 +14747,8 @@ var ChatTUI = class {
14147
14747
  const indent = " ".repeat(labelWidth);
14148
14748
  if (!this.streamContent) {
14149
14749
  if (this.runState === "thinking" || this.runState === "tool_running") {
14150
- const hint = this.runState === "tool_running" && this.activeToolName ? `Running ${this.activeToolName}\u2026` : "Thinking\u2026";
14151
- lines.push(`${label}${YELLOW}${hint}${RESET}`);
14750
+ const hint2 = this.runState === "tool_running" && this.activeToolName ? `Running ${this.activeToolName}\u2026` : "Thinking\u2026";
14751
+ lines.push(`${label}${YELLOW}${hint2}${RESET}`);
14152
14752
  } else {
14153
14753
  lines.push(label);
14154
14754
  }
@@ -14641,6 +15241,7 @@ var AudioPlayer = class extends EventEmitter2 {
14641
15241
  };
14642
15242
 
14643
15243
  // src/lib/chat-session.ts
15244
+ var HITL_INTERRUPT_EXIT_CODE = 3;
14644
15245
  async function runChatSession(session, args) {
14645
15246
  const debug = !!args.debug;
14646
15247
  const isVoice = !!args.voice;
@@ -14670,6 +15271,14 @@ async function runChatSession(session, args) {
14670
15271
  function vadMode(args) {
14671
15272
  return typeof args.vad === "string" ? args.vad : "server";
14672
15273
  }
15274
+ function sanitizeInterruptForStdout(result2) {
15275
+ const url = typeof result2.payload.url === "string" ? result2.payload.url : void 0;
15276
+ return {
15277
+ type: result2.type,
15278
+ interrupt_id: result2.interruptId,
15279
+ url
15280
+ };
15281
+ }
14673
15282
  async function singleTurnChatSession(session, message, debug, autoApprove = false, allowWrite = false) {
14674
15283
  const aguiClient = new AGUIClient({
14675
15284
  baseUrl: session.ctx.envConfig.apiUrl,
@@ -14689,7 +15298,7 @@ async function singleTurnChatSession(session, message, debug, autoApprove = fals
14689
15298
  `);
14690
15299
  }
14691
15300
  };
14692
- const { streamText, hasToolCalls } = await runConciergeAguiLoop(
15301
+ const { streamText, hasToolCalls, unsupportedHitl } = await runConciergeAguiLoop(
14693
15302
  aguiClient,
14694
15303
  [{ role: "user", content: message }],
14695
15304
  onEvent,
@@ -14702,6 +15311,21 @@ async function singleTurnChatSession(session, message, debug, autoApprove = fals
14702
15311
  nonInteractive: true
14703
15312
  }
14704
15313
  );
15314
+ if (unsupportedHitl) {
15315
+ const jsonPayload = JSON.stringify({
15316
+ error: "hitl_interrupt",
15317
+ message: "A human-in-the-loop interrupt requires out-of-band action before the operation can continue.",
15318
+ resume: {
15319
+ chatId: session.chatId,
15320
+ chatUuid: session.chatUuid,
15321
+ interruptId: unsupportedHitl.interruptId
15322
+ },
15323
+ interrupt: sanitizeInterruptForStdout(unsupportedHitl)
15324
+ });
15325
+ process.stdout.write(`${jsonPayload}
15326
+ `);
15327
+ throw new CLIError("HITL interrupt requires out-of-band handling", HITL_INTERRUPT_EXIT_CODE);
15328
+ }
14705
15329
  if (streamText) {
14706
15330
  process.stdout.write(`\x1B[32mAssistant:\x1B[0m ${streamText}
14707
15331
  `);
@@ -15209,11 +15833,12 @@ async function createMemberChat(apiClient, workspace, memberId) {
15209
15833
  async function conciergeOutboundCommand(hctx) {
15210
15834
  const ctx = requireAuth(hctx.ctx);
15211
15835
  const { args } = hctx;
15836
+ const message = typeof args.message === "string" ? args.message : void 0;
15212
15837
  if (args.call && args.sms) {
15213
15838
  throw new CLIError("--call and --sms are mutually exclusive");
15214
15839
  }
15215
15840
  if (args.call) {
15216
- const result2 = await originateConciergeCall(ctx);
15841
+ const result2 = await originateConciergeCall(ctx, "concierge_setup", message, "cli");
15217
15842
  process.stderr.write(
15218
15843
  `Concierge call started. callSid=${result2.callSid} startedAt=${result2.startedAt}
15219
15844
  `
@@ -15647,7 +16272,7 @@ var topLevelCommands = [
15647
16272
  mutates: true,
15648
16273
  args: [{ name: "phone", description: "E.164 phone number (optional; prompts if omitted)" }],
15649
16274
  handler: async (hctx) => {
15650
- const { accountVerifyPhoneCommand } = await import("./account-verify-phone-TDQNZ5XN.js");
16275
+ const { accountVerifyPhoneCommand } = await import("./account-verify-phone-TC73QIIE.js");
15651
16276
  await accountVerifyPhoneCommand(hctx);
15652
16277
  }
15653
16278
  },
@@ -15669,7 +16294,11 @@ var topLevelCommands = [
15669
16294
  description: "SMS your verified account phone (server-side)",
15670
16295
  isFlag: true
15671
16296
  },
15672
- { name: "message", alias: "m", description: "Single-turn: send one message and exit" },
16297
+ {
16298
+ name: "message",
16299
+ alias: "m",
16300
+ description: 'Single-turn: send one message and exit. With --call: the "why I am calling" anchor for the opening self-introduction.'
16301
+ },
15673
16302
  { name: "chat-id", description: "Resume an existing chat by numeric ID" },
15674
16303
  { name: "vad", description: "Voice activity detection: server (default) or client" },
15675
16304
  { name: "list-devices", description: "List audio devices and exit", isFlag: true },