@indigoai-us/hq-cli 5.345.156 → 5.345.159

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.
Files changed (144) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
  3. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
  4. package/assets/scaffold/core/scripts/worktree.sh +4 -0
  5. package/dist/command-catalog.generated.d.ts +531 -6
  6. package/dist/command-catalog.generated.js +674 -27
  7. package/dist/command-registration-plan.d.ts +18 -0
  8. package/dist/command-registration-plan.js +4 -0
  9. package/dist/commands/_patterns.d.ts +17 -0
  10. package/dist/commands/_patterns.js +22 -0
  11. package/dist/commands/agent-kit.js +8 -6
  12. package/dist/commands/bot.js +7 -4
  13. package/dist/commands/channel-suggest.d.ts +56 -0
  14. package/dist/commands/channel-suggest.js +170 -0
  15. package/dist/commands/channels.js +7 -1
  16. package/dist/commands/cloud.d.ts +45 -0
  17. package/dist/commands/cloud.js +150 -10
  18. package/dist/commands/company.js +4 -3
  19. package/dist/commands/core-checkpoint-native.d.ts +18 -0
  20. package/dist/commands/core-checkpoint-native.js +67 -2
  21. package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
  22. package/dist/commands/core-pipeline-driver-native.js +131 -5
  23. package/dist/commands/core.js +12 -0
  24. package/dist/commands/crm.js +83 -6
  25. package/dist/commands/deploy-access.d.ts +33 -0
  26. package/dist/commands/deploy-access.js +322 -0
  27. package/dist/commands/dm-attach.d.ts +106 -0
  28. package/dist/commands/dm-attach.js +184 -0
  29. package/dist/commands/dm.d.ts +37 -0
  30. package/dist/commands/dm.js +172 -10
  31. package/dist/commands/feedback-show.d.ts +40 -0
  32. package/dist/commands/feedback-show.js +261 -0
  33. package/dist/commands/feedback.d.ts +21 -0
  34. package/dist/commands/feedback.js +152 -2
  35. package/dist/commands/files.d.ts +24 -0
  36. package/dist/commands/files.js +52 -1
  37. package/dist/commands/integrations-connect.js +7 -3
  38. package/dist/commands/lanes-accounts.js +16 -1
  39. package/dist/commands/lanes-loop.js +14 -4
  40. package/dist/commands/lanes-message.js +6 -4
  41. package/dist/commands/lanes-resume.js +36 -13
  42. package/dist/commands/lanes-shared.d.ts +5 -0
  43. package/dist/commands/lanes-shared.js +13 -3
  44. package/dist/commands/lanes-watch.js +1 -0
  45. package/dist/commands/lanes.d.ts +20 -1
  46. package/dist/commands/lanes.js +201 -51
  47. package/dist/commands/mesh.js +149 -1
  48. package/dist/commands/monitor.d.ts +1 -0
  49. package/dist/commands/monitor.js +4 -1
  50. package/dist/commands/repo.d.ts +41 -0
  51. package/dist/commands/repo.js +331 -0
  52. package/dist/commands/scan.d.ts +3 -1
  53. package/dist/commands/scan.js +7 -3
  54. package/dist/commands/secrets.d.ts +21 -0
  55. package/dist/commands/secrets.js +114 -14
  56. package/dist/commands/signals.js +17 -5
  57. package/dist/commands/staff/listings.d.ts +41 -0
  58. package/dist/commands/staff/listings.js +160 -0
  59. package/dist/commands/staff/reset-test-account.d.ts +3 -2
  60. package/dist/commands/staff/reset-test-account.js +4 -3
  61. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
  62. package/dist/hq-cloud-sync-runner/package.json +1 -1
  63. package/dist/lib/agent-kit/run/inbox.js +2 -1
  64. package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
  65. package/dist/lib/agent-kit/wake-secrets.js +32 -0
  66. package/dist/lib/agent-kit/wake.d.ts +4 -0
  67. package/dist/lib/agent-kit/wake.js +49 -3
  68. package/dist/lib/bot/inflight.d.ts +7 -1
  69. package/dist/lib/bot/inflight.js +10 -2
  70. package/dist/lib/bot/prompt.js +3 -0
  71. package/dist/lib/bot/run.js +72 -20
  72. package/dist/lib/bot/session.d.ts +14 -0
  73. package/dist/lib/bot/session.js +70 -0
  74. package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
  75. package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
  76. package/dist/lib/capacity/lane-login.d.ts +6 -0
  77. package/dist/lib/capacity/lane-login.js +5 -0
  78. package/dist/lib/core-utils/worktree.js +2 -0
  79. package/dist/lib/daemon/exit-cause.d.ts +43 -0
  80. package/dist/lib/daemon/exit-cause.js +88 -0
  81. package/dist/lib/daemon/handoff.d.ts +2 -0
  82. package/dist/lib/daemon/handoff.js +6 -1
  83. package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
  84. package/dist/lib/daemon/paths.d.ts +2 -0
  85. package/dist/lib/daemon/paths.js +1 -0
  86. package/dist/lib/daemon/run.js +13 -0
  87. package/dist/lib/daemon/supervisor.d.ts +54 -1
  88. package/dist/lib/daemon/supervisor.js +297 -21
  89. package/dist/lib/daemon/unit-holds.d.ts +22 -0
  90. package/dist/lib/daemon/unit-holds.js +86 -0
  91. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  92. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  93. package/dist/lib/index-render/company-knowledge.js +1 -3
  94. package/dist/lib/index-render/projects.js +1 -3
  95. package/dist/lib/lanes/account-company.d.ts +28 -0
  96. package/dist/lib/lanes/account-company.js +53 -0
  97. package/dist/lib/lanes/accounts.d.ts +22 -3
  98. package/dist/lib/lanes/accounts.js +38 -2
  99. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  100. package/dist/lib/lanes/claude-balance.js +6 -1
  101. package/dist/lib/lanes/claude-credential.d.ts +78 -0
  102. package/dist/lib/lanes/claude-credential.js +163 -0
  103. package/dist/lib/lanes/compaction.d.ts +2 -0
  104. package/dist/lib/lanes/compaction.js +9 -0
  105. package/dist/lib/lanes/config.js +6 -0
  106. package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
  107. package/dist/lib/lanes/hooks-install.js +2 -1
  108. package/dist/lib/lanes/host-capacity.d.ts +2 -0
  109. package/dist/lib/lanes/host-capacity.js +1 -0
  110. package/dist/lib/lanes/keep-alive.d.ts +1 -1
  111. package/dist/lib/lanes/keep-alive.js +3 -1
  112. package/dist/lib/lanes/links.js +10 -4
  113. package/dist/lib/lanes/loop.js +16 -1
  114. package/dist/lib/lanes/node-binary.d.ts +17 -0
  115. package/dist/lib/lanes/node-binary.js +30 -0
  116. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  117. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  118. package/dist/lib/lanes/reconcile.js +4 -0
  119. package/dist/lib/lanes/remote-control-lane.js +2 -0
  120. package/dist/lib/lanes/spawn.d.ts +3 -0
  121. package/dist/lib/lanes/spawn.js +119 -9
  122. package/dist/lib/lanes/store.d.ts +6 -0
  123. package/dist/lib/lanes/store.js +107 -18
  124. package/dist/lib/lanes/types.d.ts +2 -0
  125. package/dist/lib/lanes/watch.js +28 -5
  126. package/dist/lib/lanes/watcher-main.js +4 -3
  127. package/dist/lib/lanes/watcher.d.ts +4 -0
  128. package/dist/lib/lanes/watcher.js +110 -21
  129. package/dist/lib/mesh/api.d.ts +32 -0
  130. package/dist/lib/mesh/api.js +75 -0
  131. package/dist/lib/secret-scan/detect.js +9 -0
  132. package/dist/lib/secret-scan/repo-config.d.ts +41 -0
  133. package/dist/lib/secret-scan/repo-config.js +237 -0
  134. package/dist/lib/secret-scan/rules.d.ts +7 -1
  135. package/dist/lib/secret-scan/rules.js +3 -1
  136. package/dist/lib/secret-scan/staged.d.ts +5 -0
  137. package/dist/lib/secret-scan/staged.js +19 -2
  138. package/dist/lib/secret-scan/types.d.ts +2 -0
  139. package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
  140. package/dist/lib/sync/server-owned-local-only.js +75 -0
  141. package/dist/utils/package-use-lease.js +27 -9
  142. package/dist/utils/version-gate.d.ts +7 -0
  143. package/dist/utils/version-gate.js +66 -0
  144. package/package.json +8 -8
@@ -72,7 +72,7 @@ import { fastFlagEnabledDefaultOn } from "../native-fast-route.js";
72
72
  import { MULTI_COMPANY_SESSION_LOCK_FLAG } from "../lib/cli-flag-keys.js";
73
73
  import { secretsExecChildExitCode } from "../utils/child-wrapper-exit.js";
74
74
  import { isPlanGateError } from "../utils/plan-gate-error.js";
75
- import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN, OUTPOST_UID_PATTERN, } from "./_patterns.js";
75
+ import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN, OUTPOST_UID_PATTERN, AGENT_UID_PATTERN, agentMachineEmail, agentUidFromMachineEmail, } from "./_patterns.js";
76
76
  import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, formatSecretsListHeader, } from "./secrets-scope.js";
77
77
  import { vaultApiFetch, getCompanyUid, getEntityUid, looksLikeCompanyUid, } from "../utils/vault-api.js";
78
78
  import { formatApiKeyCapabilityDenial, peekHqApiKey, requireApiKeyCapability, resolveVaultCredentialForCapability, } from "../utils/resolve-vault-credential.js";
@@ -175,6 +175,18 @@ async function resolveSecretsTarget(capability, commandLabel, scopeOptions) {
175
175
  aclPath: (action) => action ? `${basePath}/acl/${action}` : `${basePath}/acl`,
176
176
  };
177
177
  }
178
+ /**
179
+ * Shown when `secrets share` / `unshare` gets a 403. Changing a secret's
180
+ * grants needs admin on that secret, which is a separate check from read.
181
+ * The old "Not authorized to share this secret" text led people and agents to
182
+ * conclude the secret itself was locked to them, so this names the missing
183
+ * permission and says read access is unchanged.
184
+ */
185
+ export const SECRET_SHARE_ADMIN_REQUIRED_MESSAGE = "You need admin permission on this secret to change who can see it. " +
186
+ "This does not affect read access: if you could read the secret before, you still can. " +
187
+ "Ask a company admin to change its sharing.";
188
+ /** hq-pro 409 code: the path inherits access from a broader pattern ACL row. */
189
+ const SECRET_ACL_COVERED_BY_BROADER_ROW_CODE = "ACL_COVERED_BY_BROADER_ROW";
178
190
  /**
179
191
  * Failure message for a secrets call. A 403 on an API-key route may carry
180
192
  * hq-pro's `{ granted, missing }` capability report — surface it verbatim so
@@ -303,7 +315,16 @@ export function parseSecretAclPrincipal(principal) {
303
315
  return { granteeType: "company-wide", granteeId: "" };
304
316
  }
305
317
  if (EMAIL_PATTERN.test(p)) {
306
- return { granteeType: "email", granteeId: p.toLowerCase() };
318
+ const email = p.toLowerCase();
319
+ if (email.endsWith(AGENT_EMAIL_DOMAIN)) {
320
+ const agentUid = agentUidFromMachineEmail(email);
321
+ if (!agentUid) {
322
+ console.error(chalk.red(`Invalid principal '${principal}': a fleet agent machine email must be agt-<26-character ULID>${AGENT_EMAIL_DOMAIN}`));
323
+ return null;
324
+ }
325
+ return { granteeType: "email", granteeId: email, agentUid };
326
+ }
327
+ return { granteeType: "email", granteeId: email };
307
328
  }
308
329
  if (GROUP_ID_PATTERN.test(p)) {
309
330
  return { granteeType: "group", granteeId: p };
@@ -313,12 +334,71 @@ export function parseSecretAclPrincipal(principal) {
313
334
  // that this id is the company's active Outpost.
314
335
  return { granteeType: "person", granteeId: p };
315
336
  }
316
- console.error(chalk.red(`Invalid principal '${principal}': must be '@all', an email address, a group id matching grp_<alphanumeric, underscore, hyphen>, or a company Outpost id (out_…)`));
337
+ if (AGENT_UID_PATTERN.test(p)) {
338
+ // /delegate passes the agt_… uid (feedback_68a50c16). The wire grantee is
339
+ // the agent's machine email; hq-pro canonicalizes it onto the agt_…
340
+ // principal for an active member of the company. `secrets share` checks
341
+ // that membership first (inactiveCompanyAgentReason), because for any
342
+ // other agent the server would store an inert email row and return 200.
343
+ return {
344
+ granteeType: "email",
345
+ granteeId: agentMachineEmail(p),
346
+ label: p,
347
+ agentUid: p,
348
+ };
349
+ }
350
+ if (/^agt_/i.test(p)) {
351
+ console.error(chalk.red(`Invalid principal '${principal}': a fleet agent uid must be agt_ followed by exactly 26 upper-case Crockford base32 characters (0-9, A-Z without I, L, O, U)`));
352
+ return null;
353
+ }
354
+ console.error(chalk.red(`Invalid principal '${principal}': must be '@all', an email address, a group id matching grp_<alphanumeric, underscore, hyphen>, a fleet agent uid (agt_…) or its machine email (agt-…@agents.getindigo.ai), or a company Outpost id (out_…)`));
355
+ return null;
356
+ }
357
+ const AGENT_EMAIL_DOMAIN = "@agents.getindigo.ai";
358
+ /** Agent membership states that can hold a secret grant. */
359
+ function isActiveRosterAgent(agent) {
360
+ if (agent.entityStatus !== undefined && agent.entityStatus !== "active") {
361
+ return false;
362
+ }
363
+ if (agent.setupPhase === "deprovisioning" || agent.setupPhase === "deprovisioned") {
364
+ return false;
365
+ }
366
+ return !["archived", "deleted", "removed", "deprovisioned", "suspended", "inactive"].includes(String(agent.agentStatus ?? ""));
367
+ }
368
+ /**
369
+ * Refuse a share to a fleet agent that is not an active agent of this
370
+ * company. hq-pro turns the machine-email grantee into an agt_… grant only
371
+ * for an active member; for any other agent it stores an email row the agent
372
+ * can never use and still returns 200. Reads the company roster
373
+ * (`GET /v1/agents?companyUid=…`, the same call as `hq agents list`) before
374
+ * any write. Returns an error message, or null when the agent is active here.
375
+ */
376
+ export async function inactiveCompanyAgentReason(token, companyUid, agentUid) {
377
+ const res = await vaultApiFetch({
378
+ token,
379
+ path: "/v1/agents",
380
+ query: { companyUid },
381
+ });
382
+ if (!res.ok) {
383
+ const body = (await res.json().catch(() => ({})));
384
+ return `Could not check that ${agentUid} is an agent of this company (${res.status} ${body.message ?? body.error ?? res.statusText}). Nothing was shared.`;
385
+ }
386
+ const data = (await res.json().catch(() => ({})));
387
+ const agent = (data.agents ?? []).find((a) => typeof a.uid === "string" && a.uid.toUpperCase() === agentUid.toUpperCase());
388
+ if (!agent) {
389
+ return `${agentUid} is not an agent of this company. Nothing was shared.`;
390
+ }
391
+ if (!isActiveRosterAgent(agent)) {
392
+ return `${agentUid} is not an active agent of this company. Nothing was shared.`;
393
+ }
317
394
  return null;
318
395
  }
319
396
  function describeSecretAclPrincipal(principal) {
320
- return principal.granteeType === "company-wide"
321
- ? "@all (entire company, including guests)"
397
+ if (principal.granteeType === "company-wide") {
398
+ return "@all (entire company, including guests)";
399
+ }
400
+ return principal.label
401
+ ? `${principal.label} (${principal.granteeId})`
322
402
  : principal.granteeId;
323
403
  }
324
404
  // Mirrors hq-pro's server-authoritative KNOWN_DESTINATION_REGISTRY
@@ -1040,11 +1120,12 @@ export function registerSecretsCommand(program) {
1040
1120
  .description("Manage approved scripts for script-locked secrets");
1041
1121
  script
1042
1122
  .command("approve <path>")
1043
- .description("Approve a script for a secret path")
1123
+ .description("Approve a script for a secret path. Each --id holds one approval: approving an id that is already approved for a different path or hash fails unless --replace is given. To let the same script run from several machines, approve one HQ-relative --remote-path instead of one absolute path per machine.")
1044
1124
  .requiredOption("--id <scriptId>", "Stable script identifier")
1045
1125
  .requiredOption("--script <path>", "Path to the local script file")
1046
1126
  .option("--remote-path <path>", "Script path to approve instead of the local path. A path relative to the HQ root (for example companies/<co>/skills/<skill>/<file>) matches on every HQ root; an absolute path must match the runtime's path exactly")
1047
1127
  .option("--attestation <level>", "Attestation level", "self-asserted-hash")
1128
+ .option("--replace", "Replace the approval already stored under this --id (a different path or hash). Without it the command fails and keeps the existing approval")
1048
1129
  .action(async (secretPath, opts) => {
1049
1130
  try {
1050
1131
  rejectIfPersonal(secrets.opts(), "script approve");
@@ -1074,10 +1155,19 @@ export function registerSecretsCommand(program) {
1074
1155
  scriptPath: approvedPath,
1075
1156
  sha256: usage.script?.sha256,
1076
1157
  attestationLevel: usage.script?.attestationLevel,
1158
+ ...(opts.replace ? { replace: true } : {}),
1077
1159
  },
1078
1160
  });
1079
1161
  if (!res.ok) {
1080
1162
  const body = (await res.json().catch(() => ({})));
1163
+ if (res.status === 409 && body.code === "script_id_exists") {
1164
+ // The server kept the existing approval. Say so, and show how to
1165
+ // proceed, instead of a bare "Failed to approve script".
1166
+ const existing = body.existing;
1167
+ console.error(chalk.red(`Script id '${opts.id}' is already approved for '${secretPath}'${existing?.scriptPath ? ` with path ${existing.scriptPath}` : ""}. The existing approval was kept.`));
1168
+ console.error(`Re-run with --replace to replace it, approve this script under a new --id, or approve one HQ-relative --remote-path (for example companies/<co>/skills/<skill>/<file>) so one approval matches on every machine.`);
1169
+ process.exit(1);
1170
+ }
1081
1171
  console.error(chalk.red(`Failed to approve script: ${extractApiMessage(body, res.statusText)}`));
1082
1172
  process.exit(1);
1083
1173
  }
@@ -1087,7 +1177,7 @@ export function registerSecretsCommand(program) {
1087
1177
  console.error(chalk.red(`Failed to approve script: the server did not store an active approval for '${opts.id}' on '${secretPath}'. If this id was revoked before, approve the script under a new --id and pass that id to --script-id.`));
1088
1178
  process.exit(1);
1089
1179
  }
1090
- console.log(chalk.green(`Approved script '${opts.id}' for '${secretPath}' (path: ${approvedPath}).`));
1180
+ console.log(chalk.green(`Approved script '${opts.id}' for '${secretPath}' (path: ${approvedPath})${opts.replace ? ", replacing the previous approval under that id" : ""}.`));
1091
1181
  }
1092
1182
  catch (err) {
1093
1183
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
@@ -1482,8 +1572,8 @@ export function registerSecretsCommand(program) {
1482
1572
  });
1483
1573
  secrets
1484
1574
  .command("share <path>")
1485
- .description("Share a secret with an email address, group, company Outpost (out_…), or the entire company (@all)")
1486
- .requiredOption("--with <principal>", "Email address, group id, out_ Outpost id, or @all (entire company) to share with")
1575
+ .description("Share a secret with an email address, group, fleet agent (agt_…), company Outpost (out_…), or the entire company (@all)")
1576
+ .requiredOption("--with <principal>", "Email address, group id, agt_ fleet agent uid, out_ Outpost id, or @all (entire company) to share with")
1487
1577
  .option("--permission <level>", "Permission level: read | write | admin (required, except for a company Outpost, which can only be given read and defaults to it)")
1488
1578
  .action(async (path, opts) => {
1489
1579
  try {
@@ -1512,6 +1602,13 @@ export function registerSecretsCommand(program) {
1512
1602
  process.exit(1);
1513
1603
  }
1514
1604
  const target = await resolveSecretsTarget("secrets:share", "secrets share", secrets.opts());
1605
+ if (principal.agentUid) {
1606
+ const reason = await inactiveCompanyAgentReason(target.token, target.companyUid, principal.agentUid);
1607
+ if (reason) {
1608
+ console.error(chalk.red(reason));
1609
+ process.exit(1);
1610
+ }
1611
+ }
1515
1612
  const res = await vaultApiFetch({
1516
1613
  token: target.token,
1517
1614
  path: target.aclPath("grant"),
@@ -1533,12 +1630,15 @@ export function registerSecretsCommand(program) {
1533
1630
  }
1534
1631
  else if (res.status === 403) {
1535
1632
  console.error(chalk.red(target.isApiKey
1536
- ? formatApiKeyCapabilityDenial(body, "Not authorized to share this secret")
1537
- : "Not authorized to share this secret"));
1633
+ ? formatApiKeyCapabilityDenial(body, SECRET_SHARE_ADMIN_REQUIRED_MESSAGE)
1634
+ : SECRET_SHARE_ADMIN_REQUIRED_MESSAGE));
1538
1635
  }
1539
1636
  else if (res.status === 404) {
1540
1637
  console.error(chalk.red("ACL record not found for this path — has the secret been created?"));
1541
1638
  }
1639
+ else if (res.status === 409 && body.code === SECRET_ACL_COVERED_BY_BROADER_ROW_CODE) {
1640
+ console.error(chalk.red(body.error ?? "This secret takes its access from a broader ACL. Share that path instead."));
1641
+ }
1542
1642
  else if (res.status === 409) {
1543
1643
  console.error(chalk.red("Concurrent modification — please retry"));
1544
1644
  }
@@ -1560,7 +1660,7 @@ export function registerSecretsCommand(program) {
1560
1660
  secrets
1561
1661
  .command("unshare <path>")
1562
1662
  .description("Remove a grant from a secret")
1563
- .requiredOption("--from <principal>", "Email address, group id, out_ Outpost id, or @all (entire company) to remove")
1663
+ .requiredOption("--from <principal>", "Email address, group id, agt_ fleet agent uid, out_ Outpost id, or @all (entire company) to remove")
1564
1664
  .action(async (path, opts) => {
1565
1665
  try {
1566
1666
  rejectIfPersonal(secrets.opts(), "unshare");
@@ -1590,8 +1690,8 @@ export function registerSecretsCommand(program) {
1590
1690
  }
1591
1691
  else if (res.status === 403) {
1592
1692
  console.error(chalk.red(target.isApiKey
1593
- ? formatApiKeyCapabilityDenial(body, "Not authorized to modify this secret's ACL")
1594
- : "Not authorized to modify this secret's ACL"));
1693
+ ? formatApiKeyCapabilityDenial(body, SECRET_SHARE_ADMIN_REQUIRED_MESSAGE)
1694
+ : SECRET_SHARE_ADMIN_REQUIRED_MESSAGE));
1595
1695
  }
1596
1696
  else if (res.status === 404) {
1597
1697
  console.log(chalk.green(`Grant already absent for '${path}' / ${describeSecretAclPrincipal(principal)}`));
@@ -49,10 +49,20 @@ function readActiveCompanySlug(hqRoot) {
49
49
  return undefined;
50
50
  }
51
51
  }
52
- function requireEntitySlug(flag, hqRoot) {
53
- const slug = flag ?? readActiveCompanySlug(hqRoot);
52
+ /**
53
+ * `--company` is accepted as a synonym for `--entity`, matching `hq meetings`
54
+ * and `hq integrations`. Passing both with different values is an error.
55
+ */
56
+ function pickEntityFlag(entity, company) {
57
+ if (entity !== undefined && company !== undefined && entity !== company) {
58
+ throw new Error(`--entity '${entity}' and --company '${company}' disagree. Pass one of them.`);
59
+ }
60
+ return entity ?? company;
61
+ }
62
+ function requireEntitySlug(options, hqRoot) {
63
+ const slug = pickEntityFlag(options.entity, options.company) ?? readActiveCompanySlug(hqRoot);
54
64
  if (!slug) {
55
- throw new Error("No entity specified. Pass --entity <slug> or run `hq signals entities` to see your options.");
65
+ throw new Error("No entity specified. Pass --entity <slug> (or --company <slug>) or run `hq signals entities` to see your options.");
56
66
  }
57
67
  return slug;
58
68
  }
@@ -93,7 +103,7 @@ async function runList(options) {
93
103
  // Throws InvalidSignalTypeError with the list of valid types if not in enum.
94
104
  assertSignalType(options.type);
95
105
  const signalType = options.type;
96
- const slug = requireEntitySlug(options.entity, options.hqRoot);
106
+ const slug = requireEntitySlug(options, options.hqRoot);
97
107
  const limit = options.limit ? Number.parseInt(options.limit, 10) : 50;
98
108
  if (!Number.isFinite(limit) || limit <= 0) {
99
109
  throw new Error(`--limit must be a positive integer (got '${options.limit}')`);
@@ -136,7 +146,7 @@ async function runGet(options) {
136
146
  }
137
147
  assertSignalType(options.type);
138
148
  const signalType = options.type;
139
- const slug = requireEntitySlug(options.entity, options.hqRoot);
149
+ const slug = requireEntitySlug(options, options.hqRoot);
140
150
  const format = options.format ?? defaultGetFormat();
141
151
  const accessToken = await resolveAccessToken();
142
152
  const vaultConfig = buildVaultConfig(accessToken);
@@ -225,6 +235,7 @@ export function registerSignalsCommand(program) {
225
235
  .command("list")
226
236
  .description("List signals of a given type for an entity")
227
237
  .option("--entity <slug>", "Entity slug (defaults to .hq/config.json activeCompany)")
238
+ .option("--company <slug>", "Company slug; same as --entity")
228
239
  .option("--type <signalType>", `Signal type: ${SIGNAL_TYPES.join(" | ")}`)
229
240
  .option("--limit <n>", "Max entries per page (default 50)")
230
241
  .option("--page-token <token>", "Continuation token from a prior page")
@@ -236,6 +247,7 @@ export function registerSignalsCommand(program) {
236
247
  .command("get")
237
248
  .description("Fetch a single signal by id")
238
249
  .option("--entity <slug>", "Entity slug (defaults to .hq/config.json activeCompany)")
250
+ .option("--company <slug>", "Company slug; same as --entity")
239
251
  .option("--type <signalType>", `Signal type: ${SIGNAL_TYPES.join(" | ")}`)
240
252
  .option("--id <signalId>", "Signal id (filename minus .md)")
241
253
  .option("--format <fmt>", "Output format: markdown | json (default: markdown for TTY, json piped)")
@@ -0,0 +1,41 @@
1
+ import { Command } from "commander";
2
+ /**
3
+ * `hq staff listings {queue|approve|reject}` — marketplace moderation from the
4
+ * CLI. Thin wrappers over the vault-service moderation routes:
5
+ *
6
+ * GET /v1/moderation/queue (queue)
7
+ * POST /v1/moderation/listings/{id} (approve | reject, body { action, note? })
8
+ *
9
+ * Every `hq publish` lands as `pending_review` and stays off the public
10
+ * marketplace until a staff member decides it. Before this command the only
11
+ * way to decide was the GTM admin web page. The server owns authorization
12
+ * (`isStaffEmail` on the id_token's email claim) and the state machine
13
+ * (pending_review -> approved | rejected, optimistic lock, superseding older
14
+ * approved versions). The moderation gate reads the `email` claim, which only
15
+ * the id_token carries, so this file uses `ensureCognitoIdToken`.
16
+ */
17
+ export interface QueueItem {
18
+ listingId: string;
19
+ type?: string;
20
+ status?: string;
21
+ creatorHandle?: string;
22
+ name?: string;
23
+ slug?: string;
24
+ version?: string;
25
+ summary?: string;
26
+ createdAt?: string;
27
+ injectionScan?: {
28
+ flagged?: boolean;
29
+ flags?: unknown[];
30
+ };
31
+ }
32
+ export declare function renderQueue(items: QueueItem[]): string;
33
+ export declare function runListingsQueue(): Promise<void>;
34
+ export interface RunDecisionParams {
35
+ listingId: string;
36
+ note?: string;
37
+ }
38
+ export declare function runApproveListing(params: RunDecisionParams): Promise<void>;
39
+ export declare function runRejectListing(params: RunDecisionParams): Promise<void>;
40
+ export declare function registerListingsCommand(staff: Command): Command;
41
+ //# sourceMappingURL=listings.d.ts.map
@@ -0,0 +1,160 @@
1
+ import chalk from "chalk";
2
+ import { ensureCognitoIdToken } from "../../utils/cognito-session.js";
3
+ import { vaultApiFetch } from "../../utils/vault-api.js";
4
+ import { callerEmailFromToken, isStaffEmail } from "./reset-test-account.js";
5
+ // ── Rendering ───────────────────────────────────────────────────────────────
6
+ function renderQueueLine(item) {
7
+ const pack = `${item.slug ?? item.name ?? "?"}@${item.version ?? "?"}`;
8
+ const creator = item.creatorHandle ? `@${item.creatorHandle}` : chalk.dim("(no handle)");
9
+ const flag = item.injectionScan?.flagged
10
+ ? chalk.yellow(` flagged (${item.injectionScan.flags?.length ?? 0})`)
11
+ : "";
12
+ const when = item.createdAt ? chalk.dim(` ${item.createdAt}`) : "";
13
+ return ` ${item.listingId} ${pack.padEnd(28)} ${(item.type ?? "").padEnd(6)} ${creator}${when}${flag}`;
14
+ }
15
+ export function renderQueue(items) {
16
+ if (items.length === 0) {
17
+ return chalk.dim("\n(no listings waiting for review)");
18
+ }
19
+ const lines = [chalk.bold(`\nListings waiting for review (${items.length}, oldest first)`)];
20
+ for (const item of items)
21
+ lines.push(renderQueueLine(item));
22
+ lines.push(chalk.dim("\nApprove with `hq staff listings approve <id>`, reject with `hq staff listings reject <id> --note <reason>`."));
23
+ return lines.join("\n");
24
+ }
25
+ // ── Shared helpers ──────────────────────────────────────────────────────────
26
+ async function readBody(res) {
27
+ try {
28
+ return (await res.json());
29
+ }
30
+ catch {
31
+ return {};
32
+ }
33
+ }
34
+ function renderApiError(status, body) {
35
+ const code = typeof body.code === "string" ? body.code : undefined;
36
+ const message = typeof body.error === "string" ? body.error : `request failed (${status})`;
37
+ return code ? `${code}: ${message}` : message;
38
+ }
39
+ /**
40
+ * Local staff preflight so a non-staff caller stops before the round trip.
41
+ * The server remains the authorization boundary.
42
+ */
43
+ async function requireStaffIdToken() {
44
+ const token = await ensureCognitoIdToken();
45
+ const callerEmail = callerEmailFromToken(token);
46
+ if (!callerEmail || !isStaffEmail(callerEmail)) {
47
+ throw new Error("hq staff listings requires a staff HQ session (@getindigo.ai, @hqforwork.com or @vyg.ai). " +
48
+ "Sign in with a staff account and retry.");
49
+ }
50
+ return token;
51
+ }
52
+ const LISTING_ID_RE = /^lst_[0-9A-Za-z]+$/;
53
+ function requireListingId(listingId) {
54
+ const id = listingId.trim();
55
+ if (!LISTING_ID_RE.test(id)) {
56
+ throw new Error(`"${listingId}" is not a listing id (expected lst_…). Run \`hq staff listings queue\` to see ids.`);
57
+ }
58
+ return id;
59
+ }
60
+ // ── Runners ─────────────────────────────────────────────────────────────────
61
+ export async function runListingsQueue() {
62
+ const token = await requireStaffIdToken();
63
+ const res = await vaultApiFetch({ token, path: "/v1/moderation/queue", method: "GET" });
64
+ const parsed = await readBody(res);
65
+ if (!res.ok)
66
+ throw new Error(renderApiError(res.status, parsed));
67
+ const queue = parsed.queue;
68
+ if (!Array.isArray(queue))
69
+ throw new Error("server did not return a moderation queue");
70
+ console.log(renderQueue(queue));
71
+ }
72
+ async function decide(action, params) {
73
+ const id = requireListingId(params.listingId);
74
+ const token = await requireStaffIdToken();
75
+ const body = { action };
76
+ if (params.note !== undefined && params.note.trim().length > 0)
77
+ body.note = params.note.trim();
78
+ const res = await vaultApiFetch({
79
+ token,
80
+ path: `/v1/moderation/listings/${encodeURIComponent(id)}`,
81
+ method: "POST",
82
+ body,
83
+ });
84
+ const parsed = await readBody(res);
85
+ if (!res.ok) {
86
+ const msg = renderApiError(res.status, parsed);
87
+ if (res.status === 409) {
88
+ throw new Error(`${msg} (the listing is no longer pending, or someone decided it at the same time)`);
89
+ }
90
+ throw new Error(msg);
91
+ }
92
+ return parsed;
93
+ }
94
+ function labelOf(listing, fallbackId) {
95
+ if (!listing)
96
+ return fallbackId;
97
+ return `${listing.slug ?? listing.name ?? fallbackId}@${listing.version ?? "?"} (${listing.listingId ?? fallbackId})`;
98
+ }
99
+ export async function runApproveListing(params) {
100
+ const result = await decide("approve", params);
101
+ console.log(chalk.green(`\n✓ approved ${labelOf(result.listing, params.listingId)}`));
102
+ const superseded = result.superseded ?? [];
103
+ if (superseded.length > 0) {
104
+ console.log(chalk.dim(` superseded older versions: ${superseded.join(", ")}`));
105
+ }
106
+ console.log(chalk.dim(" It is now live in the marketplace (the public pack page refreshes within ~10 minutes)."));
107
+ }
108
+ export async function runRejectListing(params) {
109
+ const result = await decide("reject", params);
110
+ console.log(chalk.yellow(`\n✗ rejected ${labelOf(result.listing, params.listingId)}`));
111
+ }
112
+ // ── Commander wiring ────────────────────────────────────────────────────────
113
+ function fail(err) {
114
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
115
+ process.exit(1);
116
+ }
117
+ export function registerListingsCommand(staff) {
118
+ const group = staff
119
+ .command("listings")
120
+ .description("Review marketplace listings waiting for approval (staff only). Wraps /v1/moderation.");
121
+ group
122
+ .command("queue")
123
+ .description("List published packs waiting for review, oldest first.")
124
+ .action(async () => {
125
+ try {
126
+ await runListingsQueue();
127
+ }
128
+ catch (err) {
129
+ fail(err);
130
+ }
131
+ });
132
+ group
133
+ .command("approve")
134
+ .description("Approve a pending listing so it goes live (older approved versions are superseded).")
135
+ .argument("<listingId>", "Listing id from `hq staff listings queue` (lst_…)")
136
+ .option("--note <text>", "Reviewer note recorded on the listing")
137
+ .action(async (listingId, opts) => {
138
+ try {
139
+ await runApproveListing({ listingId, note: opts.note });
140
+ }
141
+ catch (err) {
142
+ fail(err);
143
+ }
144
+ });
145
+ group
146
+ .command("reject")
147
+ .description("Reject a pending listing.")
148
+ .argument("<listingId>", "Listing id from `hq staff listings queue` (lst_…)")
149
+ .requiredOption("--note <reason>", "Why it was rejected (recorded on the listing)")
150
+ .action(async (listingId, opts) => {
151
+ try {
152
+ await runRejectListing({ listingId, note: opts.note });
153
+ }
154
+ catch (err) {
155
+ fail(err);
156
+ }
157
+ });
158
+ return group;
159
+ }
160
+ //# sourceMappingURL=listings.js.map
@@ -76,8 +76,9 @@ interface ExecuteResult {
76
76
  * non-staff caller stops before the round trip; the server remains the
77
77
  * authorization boundary (this file could not enforce it if it wanted to).
78
78
  *
79
- * Exact suffix match. `foo@getindigo.ai.evil.test` and `bar@xgetindigo.ai`
80
- * do not qualify.
79
+ * The domain list matches the server's `SUPER_ADMIN_DOMAINS` (getindigo.ai,
80
+ * hqforwork.com, vyg.ai). Exact domain match: `foo@getindigo.ai.evil.test`
81
+ * and `bar@xgetindigo.ai` do not qualify.
81
82
  */
82
83
  export declare function isStaffEmail(candidate: unknown): boolean;
83
84
  /** Pull the caller email out of a locally cached id_token. */
@@ -10,8 +10,9 @@ import { vaultApiFetch } from "../../utils/vault-api.js";
10
10
  * non-staff caller stops before the round trip; the server remains the
11
11
  * authorization boundary (this file could not enforce it if it wanted to).
12
12
  *
13
- * Exact suffix match. `foo@getindigo.ai.evil.test` and `bar@xgetindigo.ai`
14
- * do not qualify.
13
+ * The domain list matches the server's `SUPER_ADMIN_DOMAINS` (getindigo.ai,
14
+ * hqforwork.com, vyg.ai). Exact domain match: `foo@getindigo.ai.evil.test`
15
+ * and `bar@xgetindigo.ai` do not qualify.
15
16
  */
16
17
  export function isStaffEmail(candidate) {
17
18
  if (typeof candidate !== "string")
@@ -20,7 +21,7 @@ export function isStaffEmail(candidate) {
20
21
  if (parts.length !== 2 || parts[0].length === 0)
21
22
  return false;
22
23
  const domain = parts[1];
23
- return domain === "getindigo.ai" || domain === "hqforwork.com";
24
+ return domain === "getindigo.ai" || domain === "hqforwork.com" || domain === "vyg.ai";
24
25
  }
25
26
  /** Pull the caller email out of a locally cached id_token. */
26
27
  export function callerEmailFromToken(token) {