@indigoai-us/hq-cli 5.345.159 → 5.345.161

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 (104) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/assets/scaffold/core/scripts/session-journal.sh +116 -24
  3. package/dist/command-catalog.generated.d.ts +222 -9
  4. package/dist/command-catalog.generated.js +273 -9
  5. package/dist/commands/access.d.ts +6 -0
  6. package/dist/commands/access.js +18 -1
  7. package/dist/commands/acl-lock.d.ts +41 -0
  8. package/dist/commands/acl-lock.js +28 -0
  9. package/dist/commands/agent-enroll.d.ts +2 -0
  10. package/dist/commands/agent-enroll.js +9 -4
  11. package/dist/commands/agents.js +5 -11
  12. package/dist/commands/auth.js +6 -5
  13. package/dist/commands/billing-sponsor.d.ts +4 -4
  14. package/dist/commands/billing-sponsor.js +11 -11
  15. package/dist/commands/billing-usage.d.ts +22 -8
  16. package/dist/commands/billing-usage.js +57 -22
  17. package/dist/commands/billing.d.ts +7 -7
  18. package/dist/commands/billing.js +13 -13
  19. package/dist/commands/cloud-provision.js +2 -2
  20. package/dist/commands/cloud.js +7 -7
  21. package/dist/commands/core-session-helper-runtime.d.ts +1 -0
  22. package/dist/commands/core-session-journal-native.d.ts +1 -0
  23. package/dist/commands/core-session-journal-native.js +158 -34
  24. package/dist/commands/db-remote-deps.js +12 -1
  25. package/dist/commands/feedback.d.ts +12 -0
  26. package/dist/commands/feedback.js +52 -0
  27. package/dist/commands/files-browse.d.ts +8 -0
  28. package/dist/commands/files-browse.js +83 -1
  29. package/dist/commands/files.js +19 -7
  30. package/dist/commands/install-global.d.ts +2 -0
  31. package/dist/commands/install-global.js +24 -1
  32. package/dist/commands/integrations-connect.js +1 -0
  33. package/dist/commands/integrations-manage.d.ts +2 -0
  34. package/dist/commands/integrations-manage.js +6 -4
  35. package/dist/commands/lanes-link.js +5 -3
  36. package/dist/commands/lanes-session-start.d.ts +12 -0
  37. package/dist/commands/lanes-session-start.js +93 -12
  38. package/dist/commands/meetings.d.ts +15 -4
  39. package/dist/commands/meetings.js +58 -19
  40. package/dist/commands/skill.js +6 -6
  41. package/dist/commands/whoami.js +1 -1
  42. package/dist/hook-engine-path.js +13 -2
  43. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3236 -1988
  44. package/dist/hq-cloud-sync-runner/package.json +1 -1
  45. package/dist/index.js +21 -0
  46. package/dist/lanes-session-start-argv.d.ts +7 -0
  47. package/dist/lanes-session-start-argv.js +9 -0
  48. package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
  49. package/dist/lib/billing/plan-limit-refusals.js +26 -3
  50. package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
  51. package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
  52. package/dist/lib/billing/plan-lock.d.ts +22 -6
  53. package/dist/lib/billing/plan-lock.js +58 -30
  54. package/dist/lib/billing/usage-cost.d.ts +24 -2
  55. package/dist/lib/billing/usage-cost.js +103 -3
  56. package/dist/lib/billing/usage-only.d.ts +37 -1
  57. package/dist/lib/billing/usage-only.js +89 -4
  58. package/dist/lib/billing/usage-pause.d.ts +27 -0
  59. package/dist/lib/billing/usage-pause.js +50 -0
  60. package/dist/lib/bot/log.d.ts +3 -2
  61. package/dist/lib/bot/progress.d.ts +23 -1
  62. package/dist/lib/bot/progress.js +43 -5
  63. package/dist/lib/bot/run.js +2 -0
  64. package/dist/lib/bot/runtime/claude.d.ts +12 -0
  65. package/dist/lib/bot/runtime/claude.js +20 -0
  66. package/dist/lib/bot/runtime/index.d.ts +10 -1
  67. package/dist/lib/bot/runtime/index.js +14 -2
  68. package/dist/lib/bot/runtime/types.d.ts +8 -0
  69. package/dist/lib/db/control-plane.d.ts +5 -0
  70. package/dist/lib/db/control-plane.js +10 -0
  71. package/dist/lib/doctor/checks/ps1-shims.d.ts +21 -0
  72. package/dist/lib/doctor/checks/ps1-shims.js +82 -0
  73. package/dist/lib/doctor/fix/apply.d.ts +13 -1
  74. package/dist/lib/doctor/fix/apply.js +38 -3
  75. package/dist/lib/doctor/fix/remediation.d.ts +9 -1
  76. package/dist/lib/doctor/fix/remediation.js +22 -0
  77. package/dist/lib/doctor/registry.js +2 -0
  78. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  79. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  80. package/dist/lib/hook-engine/resolve.d.ts +14 -0
  81. package/dist/lib/hook-engine/resolve.js +55 -1
  82. package/dist/lib/lanes/links.d.ts +2 -1
  83. package/dist/lib/lanes/links.js +235 -17
  84. package/dist/lib/lanes/registry-flag.d.ts +24 -0
  85. package/dist/lib/lanes/registry-flag.js +72 -6
  86. package/dist/lib/lanes/store.d.ts +4 -2
  87. package/dist/lib/lanes/store.js +8 -8
  88. package/dist/lib/plan-limit-nag.d.ts +0 -16
  89. package/dist/lib/plan-limit-nag.js +17 -18
  90. package/dist/lib/windows/ps1-shims.d.ts +60 -0
  91. package/dist/lib/windows/ps1-shims.js +152 -0
  92. package/dist/main.d.ts +0 -2
  93. package/dist/main.js +19 -57
  94. package/dist/utils/plan-gate-error.d.ts +30 -14
  95. package/dist/utils/plan-gate-error.js +60 -46
  96. package/dist/utils/self-update.d.ts +2 -0
  97. package/dist/utils/self-update.js +8 -0
  98. package/dist/utils/staged-npm-install.js +13 -2
  99. package/dist/utils/team-upgrade.d.ts +19 -6
  100. package/dist/utils/team-upgrade.js +37 -39
  101. package/dist/utils/token-keychain.d.ts +11 -8
  102. package/dist/utils/token-keychain.js +36 -15
  103. package/package.json +8 -8
  104. package/scripts/install-hook-engine.mjs +12 -0
@@ -76,6 +76,8 @@ export function parsePrincipal(raw) {
76
76
  }
77
77
  throw new IntegrationsCliError(`Could not tell what '${raw}' refers to. Use an email address, a uid (prs_… / agt_… / grp_…), \`everyone\`, or an explicit \`person:<id>\` / \`group:<id>\` form.`, { expected: true });
78
78
  }
79
+ /** `--with` / `--to` / `--from` help. Groups resolve membership at call time. */
80
+ export const PRINCIPAL_HELP = "Email, uid, vault group (grp_…), or `everyone`";
79
81
  /** How a principal reads back in output, without leaking a raw uid as a name. */
80
82
  function describePrincipal(entry) {
81
83
  if (entry.granteeType === "company-wide")
@@ -379,7 +381,7 @@ export function registerManageCommands(integrations) {
379
381
  .option("--provider <slug>", "Connected app (e.g. linear)")
380
382
  .option("--connection <id>", "Connection id (acct_…)")
381
383
  .requiredOption("--tool <name>", "The tool to authorize")
382
- .requiredOption("--to <principal>", "Email, uid, or `everyone`")
384
+ .requiredOption("--to <principal>", PRINCIPAL_HELP)
383
385
  .option("--permission <level>", "read, write, or admin", "write")
384
386
  .option("--json", "Machine-readable output")
385
387
  .action(async (app, opts) => {
@@ -418,7 +420,7 @@ export function registerManageCommands(integrations) {
418
420
  .option("--provider <slug>", "Connected app (e.g. linear)")
419
421
  .option("--connection <id>", "Connection id (acct_…)")
420
422
  .requiredOption("--tool <name>", "The tool to stop authorizing")
421
- .requiredOption("--from <principal>", "Email, uid, or `everyone`")
423
+ .requiredOption("--from <principal>", PRINCIPAL_HELP)
422
424
  .option("--json", "Machine-readable output")
423
425
  .action(async (app, opts) => {
424
426
  const principal = parsePrincipal(opts.from);
@@ -465,7 +467,7 @@ export function registerManageCommands(integrations) {
465
467
  .option("--company <slug>", "Company slug, e.g. indigo")
466
468
  .option("--provider <slug>", "Connected app (e.g. linear)")
467
469
  .option("--connection <id>", "Connection id (acct_…)")
468
- .requiredOption("--with <principal>", "Email, uid, or `everyone`")
470
+ .requiredOption("--with <principal>", PRINCIPAL_HELP)
469
471
  .option("--permission <level>", "read, write, or admin", "write")
470
472
  .option("--json", "Machine-readable output")
471
473
  .action(async (app, opts) => {
@@ -495,7 +497,7 @@ export function registerManageCommands(integrations) {
495
497
  .option("--company <slug>", "Company slug, e.g. indigo")
496
498
  .option("--provider <slug>", "Connected app (e.g. linear)")
497
499
  .option("--connection <id>", "Connection id (acct_…)")
498
- .requiredOption("--from <principal>", "Email, uid, or `everyone`")
500
+ .requiredOption("--from <principal>", PRINCIPAL_HELP)
499
501
  .option("--json", "Machine-readable output")
500
502
  .action(async (app, opts) => {
501
503
  const principal = parsePrincipal(opts.from);
@@ -14,6 +14,7 @@ function addLinkOptions(command) {
14
14
  .option("--engine <engine>", "Session engine")
15
15
  .option("--title <title>", "Session title")
16
16
  .option("--host-session <id>", "Host session id")
17
+ .option("--claims <ids>", "Exact comma-separated recipient receipt IDs")
17
18
  .option("--text <message>", "Message text")
18
19
  .option("--text-file <path>", "Read message text from a file")
19
20
  .option("--state <state>", "Child state")
@@ -35,7 +36,7 @@ function commandError(error) {
35
36
  }
36
37
  export function registerLanesLinkCommand(lanes) {
37
38
  const link = lanes.command("link").description("Link live sessions to a conductor through session inboxes");
38
- const commands = ["open", "join", "send", "report", "status", "inbox", "read", "wait", "list", "leave", "close"];
39
+ const commands = ["open", "join", "send", "ack", "capabilities", "report", "status", "inbox", "read", "wait", "list", "leave", "close"];
39
40
  for (const subcommand of commands) {
40
41
  addLinkOptions(link.command(subcommand)).action(async (options) => {
41
42
  try {
@@ -56,7 +57,8 @@ export function registerLanesLinkCommand(lanes) {
56
57
  }
57
58
  const deliver = link.command("_deliver", { hidden: true })
58
59
  .description("Internal linked-session hook delivery")
59
- .requiredOption("--event <event>", "Hook event");
60
+ .requiredOption("--event <event>", "Hook event")
61
+ .option("--ack-mode <mode>", "Internal explicit recipient acknowledgement");
60
62
  deliver.action(async (options) => {
61
63
  // Hook commands must not update/re-exec the CLI inside the provider's time budget.
62
64
  process.env.HQ_NO_UPDATE_CHECK = "1";
@@ -70,7 +72,7 @@ export function registerLanesLinkCommand(lanes) {
70
72
  env: process.env,
71
73
  defaultValue: true,
72
74
  });
73
- const result = await runLaneLinkWithFlag("_deliver", { hqRoot: root, env: process.env, event: options.event, input }, enabled);
75
+ const result = await runLaneLinkWithFlag("_deliver", { hqRoot: root, env: process.env, event: options.event, ackMode: options.ackMode, input }, enabled);
74
76
  if (result.output)
75
77
  writeJson(result.output);
76
78
  }
@@ -8,9 +8,21 @@ export interface SessionStartOptions {
8
8
  stderr?: Pick<NodeJS.WriteStream, "write">;
9
9
  resolveRoot?: (env: NodeJS.ProcessEnv, cwd?: string) => string;
10
10
  startSweep?: (root: string, env: NodeJS.ProcessEnv) => void;
11
+ /** Starts the off-hot-path registry read when an offline flag value is missing or old. */
12
+ refreshFlags?: (env: NodeJS.ProcessEnv) => void;
11
13
  }
14
+ /** Offline flag values older than this trigger one background registry read. */
15
+ export declare const SESSION_START_FLAG_REFRESH_AFTER_MS: number;
12
16
  /** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
13
17
  export declare function startSessionStartOrphanSweep(root: string, env?: NodeJS.ProcessEnv, spawnDetached?: typeof spawn, rootScoped?: boolean): void;
18
+ /**
19
+ * Start a detached `hq lanes session-start --refresh-flags` so the registry
20
+ * read never runs inside the SessionStart hook. A short lease keeps a burst of
21
+ * session starts to one refresh. An env without a state dir never spawns.
22
+ */
23
+ export declare function startSessionStartFlagRefresh(env: NodeJS.ProcessEnv, spawnDetached?: typeof spawn, now?: number): boolean;
24
+ /** Read the session-start flags from the registry, refreshing the offline cache. */
25
+ export declare function refreshSessionStartFlags(env?: NodeJS.ProcessEnv): Promise<void>;
14
26
  /** SessionStart-compatible, fail-open conduct bootstrap. */
15
27
  export declare function runLaneSessionStart(options?: SessionStartOptions): Promise<void>;
16
28
  export declare function registerLaneSessionStartCommand(lanes: Command): void;
@@ -4,8 +4,17 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
7
- import { readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
7
+ import { readLanesRegistryFlagOffline, readLanesRegistryFlagResult } from "../lib/lanes/registry-flag.js";
8
8
  import { LANE_CONDUCT_SESSION_START_FLAG, LANE_ORPHAN_SWEEP_CONTEXT_FLAG, LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG } from "../lib/lanes/flag-keys.js";
9
+ const SESSION_START_FLAGS = [
10
+ LANE_CONDUCT_SESSION_START_FLAG,
11
+ LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG,
12
+ LANE_ORPHAN_SWEEP_CONTEXT_FLAG,
13
+ ];
14
+ /** Offline flag values older than this trigger one background registry read. */
15
+ export const SESSION_START_FLAG_REFRESH_AFTER_MS = 10 * 60 * 1000;
16
+ const SESSION_START_FLAG_REFRESH_LEASE_MS = 60 * 1000;
17
+ const SESSION_START_FLAG_REFRESH_LEASE = "lanes-session-start-flag-refresh.lock";
9
18
  const CODEX_DEFAULT_KEYS = ["conduct_engine", "conduct_child_model", "conduct_child_effort", "conduct_default_source"];
10
19
  function readSessionMetaValue(text, key) {
11
20
  const line = text.split(/\r?\n/).find((entry) => new RegExp(`^\\s*${key}:`).test(entry));
@@ -247,6 +256,58 @@ export function startSessionStartOrphanSweep(root, env = process.env, spawnDetac
247
256
  // SessionStart's orphan report is advisory and must never block startup.
248
257
  }
249
258
  }
259
+ function flagRefreshLeaseDir(env) {
260
+ const stateDir = env.HQ_STATE_DIR?.trim();
261
+ if (stateDir)
262
+ return stateDir;
263
+ const home = env.HOME?.trim() || env.USERPROFILE?.trim();
264
+ return home ? path.join(home, ".hq") : null;
265
+ }
266
+ /**
267
+ * Start a detached `hq lanes session-start --refresh-flags` so the registry
268
+ * read never runs inside the SessionStart hook. A short lease keeps a burst of
269
+ * session starts to one refresh. An env without a state dir never spawns.
270
+ */
271
+ export function startSessionStartFlagRefresh(env, spawnDetached = spawn, now = Date.now()) {
272
+ const dir = flagRefreshLeaseDir(env);
273
+ const cli = process.argv[1];
274
+ if (!dir || !cli)
275
+ return false;
276
+ try {
277
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
278
+ const lease = path.join(dir, SESSION_START_FLAG_REFRESH_LEASE);
279
+ try {
280
+ if (now - fs.statSync(lease).mtimeMs < SESSION_START_FLAG_REFRESH_LEASE_MS)
281
+ return false;
282
+ fs.rmSync(lease, { force: true });
283
+ }
284
+ catch (error) {
285
+ if (error.code !== "ENOENT")
286
+ return false;
287
+ }
288
+ try {
289
+ fs.closeSync(fs.openSync(lease, "wx", 0o600));
290
+ }
291
+ catch {
292
+ return false;
293
+ }
294
+ const child = spawnDetached(process.execPath, [cli, "lanes", "session-start", "--refresh-flags"], {
295
+ detached: true,
296
+ stdio: "ignore",
297
+ env: { ...env, HQ_NO_UPDATE_CHECK: "1" },
298
+ });
299
+ child.on("error", () => { });
300
+ child.unref();
301
+ return true;
302
+ }
303
+ catch {
304
+ return false;
305
+ }
306
+ }
307
+ /** Read the session-start flags from the registry, refreshing the offline cache. */
308
+ export async function refreshSessionStartFlags(env = process.env) {
309
+ await Promise.all(SESSION_START_FLAGS.map((flag) => readLanesRegistryFlagResult(flag, { env, defaultValue: true, reportFailure: () => undefined }).catch(() => undefined)));
310
+ }
250
311
  /** SessionStart-compatible, fail-open conduct bootstrap. */
251
312
  export async function runLaneSessionStart(options = {}) {
252
313
  const env = options.env ?? process.env;
@@ -278,19 +339,33 @@ export async function runLaneSessionStart(options = {}) {
278
339
  if (/(^|,)auto-conduct(,|$)/.test(disabledHooks))
279
340
  return;
280
341
  const root = (options.resolveRoot ?? ((e, cwd) => resolveLanesHqRoot({ env: e, cwd })))(env, options.cwd);
281
- const flagEnabled = await readLanesRegistryFlagFailOpen(LANE_CONDUCT_SESSION_START_FLAG, {
282
- env,
283
- defaultValue: true,
284
- reportFailure: (error) => {
285
- const kind = error instanceof Error ? error.name : "Error";
286
- debug(`hq lanes session-start: flag read failed (${kind})`);
287
- },
288
- });
342
+ // Flags come from offline state only. A missing or old value fails open and
343
+ // schedules one background registry read for the next session start.
344
+ let refreshNeeded = false;
345
+ const readFlag = (flag) => {
346
+ let read = null;
347
+ try {
348
+ read = readLanesRegistryFlagOffline(flag, { env, defaultValue: true });
349
+ }
350
+ catch {
351
+ read = null;
352
+ }
353
+ if (!read || (read.source !== "local-override" && read.ageMs > SESSION_START_FLAG_REFRESH_AFTER_MS))
354
+ refreshNeeded = true;
355
+ return read?.value ?? true;
356
+ };
357
+ const flagEnabled = readFlag(LANE_CONDUCT_SESSION_START_FLAG);
358
+ const rootScoped = readFlag(LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG);
359
+ const contextEnabled = readFlag(LANE_ORPHAN_SWEEP_CONTEXT_FLAG);
360
+ if (refreshNeeded) {
361
+ try {
362
+ (options.refreshFlags ?? ((e) => { startSessionStartFlagRefresh(e); }))(env);
363
+ }
364
+ catch { /* advisory */ }
365
+ }
289
366
  if (!flagEnabled)
290
367
  return;
291
- const rootScoped = await readLanesRegistryFlagFailOpen(LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG, { env, defaultValue: true });
292
368
  (options.startSweep ?? ((r, e) => startSessionStartOrphanSweep(r, e, spawn, rootScoped)))(root, env);
293
- const contextEnabled = await readLanesRegistryFlagFailOpen(LANE_ORPHAN_SWEEP_CONTEXT_FLAG, { env, defaultValue: true });
294
369
  if (contextEnabled) {
295
370
  const count = readOrphanCount(root, env, rootScoped, (error) => debug(`hq lanes session-start: orphan report read failed (${error instanceof Error ? error.name : "Error"})`));
296
371
  if (count > 0)
@@ -324,6 +399,12 @@ export async function runLaneSessionStart(options = {}) {
324
399
  export function registerLaneSessionStartCommand(lanes) {
325
400
  lanes.command("session-start", { hidden: true })
326
401
  .description("Internal SessionStart bootstrap")
327
- .action(async () => runLaneSessionStart());
402
+ .option("--refresh-flags", "Refresh the offline session-start flag values from the registry")
403
+ .action(async (opts) => {
404
+ if (opts.refreshFlags)
405
+ await refreshSessionStartFlags();
406
+ else
407
+ await runLaneSessionStart();
408
+ });
328
409
  }
329
410
  //# sourceMappingURL=lanes-session-start.js.map
@@ -3,11 +3,22 @@ import { type PricingPlan } from "../lib/billing/usage-only.js";
3
3
  /** Upper bound on list pages one short-id lookup reads (per cursor chain). */
4
4
  export declare const SHORT_ID_MAX_PAGES = 50;
5
5
  /**
6
- * The 402 copy for meeting bots. A usage-only company (US-012: the body or the
7
- * company's billing summary says `pricingPlan: "usage-only"`) has no Workforce
8
- * plan to buy, so it is told to add a card. Every other company keeps the
9
- * Workforce copy.
6
+ * The 402 copy for meeting bots. A usage pause (US-024) prints the server's
7
+ * message and Billing link. A usage-only company (US-012) is told to add a
8
+ * card. Every other company gets the server's sentence under a line that
9
+ * names no plan: the flat plans are closed.
10
10
  */
11
11
  export declare function meetingPaymentRequiredLines(body: Record<string, unknown>, resolvePlan?: () => Promise<PricingPlan | null>): Promise<string[]>;
12
+ /**
13
+ * The company a `hq meetings` call is about: the explicit `--company` value,
14
+ * otherwise the company an explicitly named HQ session is bound to (the session's company
15
+ * lock, from `scope-capability.json` or `meta.yaml`). A session bound to
16
+ * Personal, an unbound session, or an unreadable lock gives undefined so the
17
+ * server's own default applies. Before this, every subcommand sent no
18
+ * company at all without `--company`, so the list, get, transcript and
19
+ * search views silently showed only the caller's personal recordings.
20
+ * Feedback 04771691-fcbc-4847-899a-6c966c561b52.
21
+ */
22
+ export declare function sessionCompanySlug(env?: NodeJS.ProcessEnv): string | undefined;
12
23
  export declare function registerMeetingsCommand(program: Command): void;
13
24
  //# sourceMappingURL=meetings.d.ts.map
@@ -1,10 +1,12 @@
1
1
  import chalk from "chalk";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { ensureCognitoToken } from "../utils/cognito-session.js";
4
+ import { readSessionCompanyLock } from "../lib/session-company-lock.js";
5
+ import { sessionIdFromEnv } from "../lib/core-utils/session-id.js";
4
6
  import { vaultApiFetch, getCompanyUid, resolveCallerPersonUid, } from "../utils/vault-api.js";
5
7
  import { isPlanGateError, planGateErrorFromPayload, } from "../utils/plan-gate-error.js";
6
8
  import { mapWithConcurrency } from "./sync-manifest.js";
7
- import { isUsageOnly, pricingPlanFromBody, resolveCompanyPricingPlan, usageOnlyCardRequiredLines, } from "../lib/billing/usage-only.js";
9
+ import { isUsageOnly, pricingPlanFromBody, resolveCompanyPricingPlan, usageOnlyCardRequiredLines, usagePauseFromBody, } from "../lib/billing/usage-only.js";
8
10
  const LIVE_SNAPSHOT_STATUSES = new Set([
9
11
  "joining",
10
12
  "recording",
@@ -318,23 +320,32 @@ async function resolveShortId(token, prefix, query) {
318
320
  process.exit(1);
319
321
  }
320
322
  /**
321
- * The 402 copy for meeting bots. A usage-only company (US-012: the body or the
322
- * company's billing summary says `pricingPlan: "usage-only"`) has no Workforce
323
- * plan to buy, so it is told to add a card. Every other company keeps the
324
- * Workforce copy.
323
+ * The 402 copy for meeting bots. A usage pause (US-024) prints the server's
324
+ * message and Billing link. A usage-only company (US-012) is told to add a
325
+ * card. Every other company gets the server's sentence under a line that
326
+ * names no plan: the flat plans are closed.
325
327
  */
326
328
  export async function meetingPaymentRequiredLines(body, resolvePlan = () => resolveCompanyPricingPlan()) {
327
329
  const plan = pricingPlanFromBody(body) ?? (await resolvePlan().catch(() => null));
328
330
  if (isUsageOnly(plan)) {
329
331
  return usageOnlyCardRequiredLines("A meeting notetaker bot").map((line, i) => i === 0 ? chalk.red(line) : chalk.dim(line));
330
332
  }
333
+ // US-024: a usage pause is the server's own answer: its message and link.
334
+ const pause = usagePauseFromBody(body);
335
+ if (pause) {
336
+ return [
337
+ chalk.red(pause.message),
338
+ ...(pause.actionUrl ? [chalk.dim(`Billing: ${pause.actionUrl}`)] : []),
339
+ ];
340
+ }
341
+ // The server's sentence when it sent one; otherwise copy that names no plan.
331
342
  const detail = typeof body.message === "string"
332
343
  ? body.message
333
344
  : typeof body.error === "string"
334
345
  ? body.error
335
- : "Upgrade your company to HQ Workforce to record meetings.";
346
+ : "Run `hq billing status` or open the company's Billing page to check it.";
336
347
  return [
337
- chalk.red("Meeting notetaker bots require the $500/mo HQ Workforce plan."),
348
+ chalk.red("Meeting notetaker bots are not available on this company's current billing."),
338
349
  chalk.dim(detail),
339
350
  ];
340
351
  }
@@ -425,11 +436,39 @@ function printMeetingTable(meetings) {
425
436
  ].join(" "));
426
437
  }
427
438
  }
439
+ /**
440
+ * The company a `hq meetings` call is about: the explicit `--company` value,
441
+ * otherwise the company an explicitly named HQ session is bound to (the session's company
442
+ * lock, from `scope-capability.json` or `meta.yaml`). A session bound to
443
+ * Personal, an unbound session, or an unreadable lock gives undefined so the
444
+ * server's own default applies. Before this, every subcommand sent no
445
+ * company at all without `--company`, so the list, get, transcript and
446
+ * search views silently showed only the caller's personal recordings.
447
+ * Feedback 04771691-fcbc-4847-899a-6c966c561b52.
448
+ */
449
+ export function sessionCompanySlug(env = process.env) {
450
+ // The shared reader also supports the last-writer-wins .current pointer.
451
+ // It cannot identify a plain terminal or cron caller's session.
452
+ if (!sessionIdFromEnv(env))
453
+ return undefined;
454
+ let lock;
455
+ try {
456
+ lock = readSessionCompanyLock(env);
457
+ }
458
+ catch (err) {
459
+ // Fall through to the server default, but say why so an unbound call is
460
+ // never mistaken for a working company scope.
461
+ console.error(chalk.dim(` (session company not applied: ${err instanceof Error ? err.message : String(err)})`));
462
+ return undefined;
463
+ }
464
+ const primary = lock?.companies[0];
465
+ return primary && primary !== "personal" ? primary : undefined;
466
+ }
428
467
  export function registerMeetingsCommand(program) {
429
468
  const meetings = program
430
469
  .command("meetings")
431
470
  .description("View and search meeting recordings, transcripts, and notes")
432
- .option("--company <slug>", "Company slug (for multi-company users)")
471
+ .option("--company <slug>", "Company slug (defaults to the company this HQ session is bound to)")
433
472
  .option("--json", "Output raw JSON instead of formatted text");
434
473
  // ── hq meetings list ──────────────────────────────────────────────
435
474
  meetings
@@ -441,7 +480,7 @@ export function registerMeetingsCommand(program) {
441
480
  try {
442
481
  const token = await ensureCognitoToken();
443
482
  const query = {};
444
- const companySlug = meetings.opts().company;
483
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
445
484
  if (opts.limit)
446
485
  query.limit = opts.limit;
447
486
  if (opts.next)
@@ -479,9 +518,9 @@ export function registerMeetingsCommand(program) {
479
518
  .action(async (recallBotId, opts) => {
480
519
  try {
481
520
  const token = await ensureCognitoToken();
482
- const companySlug = meetings.opts().company;
521
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
483
522
  if (!companySlug)
484
- throw new Error("--company <slug|uid> is required for live transcripts");
523
+ throw new Error("--company <slug|uid> is required for live transcripts (this session is not bound to a company)");
485
524
  const companyId = companySlug.startsWith("cmp_") ? companySlug : await getCompanyUid(token, companySlug);
486
525
  if (!recallBotId) {
487
526
  if (opts.follow || opts.since)
@@ -601,9 +640,9 @@ export function registerMeetingsCommand(program) {
601
640
  .description("Import a normalized historical transcript JSON file")
602
641
  .action(async (file) => {
603
642
  try {
604
- const companySlug = meetings.opts().company;
643
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
605
644
  if (!companySlug) {
606
- throw new Error("--company <slug> is required for historical imports");
645
+ throw new Error("--company <slug> is required for historical imports (this session is not bound to a company)");
607
646
  }
608
647
  const bytes = await readFile(file);
609
648
  if (bytes.byteLength > 5 * 1024 * 1024) {
@@ -657,10 +696,10 @@ export function registerMeetingsCommand(program) {
657
696
  .action(async (meetingUrl) => {
658
697
  try {
659
698
  const token = await ensureCognitoToken();
660
- const companySlug = meetings.opts().company;
699
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
661
700
  // Always name a company. With no companyId the backend files the bot
662
701
  // under Personal, which can never record, so an entitled user would
663
- // get the HQ Workforce upgrade prompt. Without --company this uses the
702
+ // get the payment-required refusal. Without --company this uses the
664
703
  // caller's only active company, or asks them to pick one.
665
704
  const query = {
666
705
  companyId: await getCompanyUid(token, companySlug),
@@ -698,7 +737,7 @@ export function registerMeetingsCommand(program) {
698
737
  try {
699
738
  const token = await ensureCognitoToken();
700
739
  const query = {};
701
- const companySlug = meetings.opts().company;
740
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
702
741
  if (companySlug)
703
742
  query.companyId = await getCompanyUid(token, companySlug);
704
743
  const meetingId = await resolveShortId(token, rawId, query);
@@ -823,7 +862,7 @@ export function registerMeetingsCommand(program) {
823
862
  try {
824
863
  const token = await ensureCognitoToken();
825
864
  const params = { q: query };
826
- const companySlug = meetings.opts().company;
865
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
827
866
  if (companySlug)
828
867
  params.companyId = await getCompanyUid(token, companySlug);
829
868
  const res = await vaultApiFetch({
@@ -854,7 +893,7 @@ export function registerMeetingsCommand(program) {
854
893
  try {
855
894
  const token = await ensureCognitoToken();
856
895
  const query = {};
857
- const companySlug = meetings.opts().company;
896
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
858
897
  if (companySlug)
859
898
  query.companyId = await getCompanyUid(token, companySlug);
860
899
  const meetingId = await resolveShortId(token, rawId, query);
@@ -923,7 +962,7 @@ export function registerMeetingsCommand(program) {
923
962
  try {
924
963
  const token = await ensureCognitoToken();
925
964
  const query = {};
926
- const companySlug = meetings.opts().company;
965
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
927
966
  if (companySlug)
928
967
  query.companyId = await getCompanyUid(token, companySlug);
929
968
  const meetingId = await resolveShortId(token, rawId, query);
@@ -28,7 +28,7 @@ import { AuthError } from "../utils/auth-error.js";
28
28
  import { redactErrorText } from "../utils/redact-error-text.js";
29
29
  import { stampVaultAccessDenied, vaultAccessDeniedDiagnostics, } from "../utils/vault-access-denied-error.js";
30
30
  import { PlanGateError } from "../utils/plan-gate-error.js";
31
- import { createPlanLimitRefusalTracker, trackingProgressRecorder, } from "../lib/billing/plan-limit-refusals.js";
31
+ import { createPlanLimitRefusalTracker, REFUSAL_FALLBACK_SENTENCE, trackingProgressRecorder, } from "../lib/billing/plan-limit-refusals.js";
32
32
  export const SKILL_UID_PATTERN = /^skl_[A-Za-z0-9]+$/;
33
33
  export const SKILL_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
34
34
  const COMPANY_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
@@ -54,16 +54,16 @@ async function defaultSyncFile(input) {
54
54
  };
55
55
  }
56
56
  /**
57
- * The upload of a stamped skill file was refused by a plan limit. Registration
57
+ * The upload of a stamped skill file was refused (new files paused). Registration
58
58
  * and the local stamp already succeeded, so say that on stderr, then return a
59
- * typed plan gate for main.ts to render (reason and upgrade link) and exit
59
+ * typed plan gate for main.ts to render (reason and billing link) and exit
60
60
  * non-zero. Never "Skill ready … Sync: complete".
61
61
  */
62
62
  function skillUploadRefused(skillUid, filePath, companySlug, refused) {
63
- console.warn(chalk.yellow(`⚠ Skill ${skillUid} is stamped locally at '${filePath}', but the upload was refused by your plan limit.`));
63
+ console.warn(chalk.yellow(`⚠ Skill ${skillUid} is stamped locally at '${filePath}', but the upload was refused: new files are paused for this account.`));
64
64
  return new PlanGateError("PLAN_LIMIT_EXCEEDED", {
65
- reason: `${refused.message ?? "New files are paused by your plan limit."} ` +
66
- "The skill file was not uploaded. Once the limit is sorted, upload it with: " +
65
+ reason: `${refused.message ?? REFUSAL_FALLBACK_SENTENCE} ` +
66
+ "The skill file was not uploaded. Once uploads resume, upload it with: " +
67
67
  `hq sync push --company ${companySlug} '${filePath}'`,
68
68
  ...(refused.upgradeUrl ? { upgradeUrl: refused.upgradeUrl } : {}),
69
69
  });
@@ -123,7 +123,7 @@ export function registerWhoamiCommand(program) {
123
123
  .command('whoami')
124
124
  .description('Show the currently authenticated user')
125
125
  .option('--json', 'Output identity metadata as JSON (never tokens)')
126
- .option('--company <slug>', 'Also report this company\'s workspace plan state (Starter nag)')
126
+ .option('--company <slug>', 'Also report this company\'s billing and usage notice')
127
127
  .action(async (opts) => {
128
128
  try {
129
129
  // Effective source (same as auth status / ensureCognitoToken): env
@@ -18,7 +18,7 @@ export async function runHookEnginePath(args) {
18
18
  console.log('Usage: hq core hook-engine-path [--repair] [--for-registration]\nResolve and pin the installed native hook engine offline.\n--for-registration prints a path only when the launcher flag is enabled.');
19
19
  return;
20
20
  }
21
- const { installHookEngine, hookEngineRequired, HOOK_ENGINE_REPAIR } = await import('./lib/hook-engine/resolve.js');
21
+ const { installHookEngine, hookEngineRequired, removeHookEnginePointer, verifyHookEngine, HOOK_ENGINE_REPAIR } = await import('./lib/hook-engine/resolve.js');
22
22
  if (args.includes('--for-registration')) {
23
23
  const { launcherEnabled } = await import('./lib/hook-engine/registration.js');
24
24
  if (!launcherEnabled()) {
@@ -27,8 +27,19 @@ export async function runHookEnginePath(args) {
27
27
  }
28
28
  }
29
29
  const binary = installHookEngine({ repair: args.includes('--repair') });
30
- if (binary)
30
+ if (binary) {
31
+ // The printed path is one that runs and reports as an hq-hook engine.
32
+ // A binary that cannot answer its self-check must not be bridged, so the
33
+ // shell pointer is removed and the command exits non-zero.
34
+ const problem = verifyHookEngine(binary);
35
+ if (problem) {
36
+ removeHookEnginePointer();
37
+ console.error(`${problem}. Existing bash hooks remain active. Repair: ${HOOK_ENGINE_REPAIR}`);
38
+ process.exitCode = 1;
39
+ return;
40
+ }
31
41
  console.log(binary);
42
+ }
32
43
  else {
33
44
  console.error(`Native hook engine missing; ${hookEngineRequired() ? 'repair before running hooks' : 'existing bash hooks remain active'}. Repair: ${HOOK_ENGINE_REPAIR}`);
34
45
  process.exitCode = 1;