@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
@@ -183,7 +183,7 @@ function sameFile(a, b) {
183
183
  * (another process reclaimed it and took a fresh lock in between), the fresh
184
184
  * lock is linked back into place and nothing is reclaimed.
185
185
  */
186
- function recordLockIsStale(lockPath, deps) {
186
+ function recordLockIsStale(lockPath, deps, reclaimByAge = true) {
187
187
  const statSync = deps.statSync ?? ((p) => fs.statSync(p));
188
188
  const linkSync = deps.linkSync ?? fs.linkSync.bind(fs);
189
189
  const before = statSync(lockPath);
@@ -199,7 +199,7 @@ function recordLockIsStale(lockPath, deps) {
199
199
  const mtimeAge = now - before.mtimeMs;
200
200
  const age = Number.isFinite(ts) ? Math.max(now - ts, mtimeAge) : mtimeAge;
201
201
  const stale = (pidKnown && sameHost && pid !== process.pid && !lockOwnerAlive(pid)) ||
202
- age > lockStaleMs();
202
+ (reclaimByAge && age > lockStaleMs());
203
203
  if (!stale)
204
204
  return false;
205
205
  const parked = `${lockPath}.stale.${process.pid}.${randomBytes(6).toString("hex")}`;
@@ -243,7 +243,7 @@ function recordLockIsStale(lockPath, deps) {
243
243
  removeParked();
244
244
  return reclaimed;
245
245
  }
246
- function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS) {
246
+ function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS, reclaimByAge = true) {
247
247
  const lockPath = lockPathFor(recordPath);
248
248
  const statSync = deps.statSync ?? ((p) => fs.statSync(p));
249
249
  const fstatSync = deps.fstatSync ?? ((fd) => fs.fstatSync(fd));
@@ -295,7 +295,7 @@ function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS) {
295
295
  throw new LanesError("store_lock_timeout", `Timed out waiting for lock ${lockPath}`, { path: lockPath });
296
296
  }
297
297
  try {
298
- if (recordLockIsStale(lockPath, deps))
298
+ if (recordLockIsStale(lockPath, deps, reclaimByAge))
299
299
  continue;
300
300
  }
301
301
  catch (statErr) {
@@ -309,12 +309,12 @@ function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS) {
309
309
  }
310
310
  }
311
311
  /** Test seam: run withRecordLock with overridden deps and a shorter timeout. */
312
- export function withRecordLockForTests(recordPath, overrides, fn, timeoutMs) {
313
- return withRecordLock(recordPath, { ...defaultStoreDeps(), ...overrides }, fn, timeoutMs);
312
+ export function withRecordLockForTests(recordPath, overrides, fn, timeoutMs, reclaimByAge = true) {
313
+ return withRecordLock(recordPath, { ...defaultStoreDeps(), ...overrides }, fn, timeoutMs, reclaimByAge);
314
314
  }
315
315
  /** Run a lane-owned file read/modify/write transaction under its sidecar lock. */
316
- export function withLaneRecordFileLock(recordPath, fn) {
317
- return withRecordLock(recordPath, defaultStoreDeps(), fn);
316
+ export function withLaneRecordFileLock(recordPath, fn, options = {}) {
317
+ return withRecordLock(recordPath, defaultStoreDeps(), fn, LOCK_TIMEOUT_MS, options.reclaimByAge ?? true);
318
318
  }
319
319
  function readJsonFile(filePath, deps) {
320
320
  let text;
@@ -79,22 +79,6 @@ export declare function recordPlanLimitStatus(body: unknown, opts?: {
79
79
  * never claims the workspace has stopped working.
80
80
  */
81
81
  export declare function buildOverLine(overEntries: Array<[string, PlanLimitEntry]>, upgradeUrl: string): string;
82
- /**
83
- * My Telemetry is an Individual-plan feature on a personal scope (owner
84
- * decision 9 / us-044-individual-plan-design.md §11). When hq-pro refuses it,
85
- * the CLI prints this notice INSTEAD of telemetry output.
86
- *
87
- * It lives beside the nag copy so plan wording stays in one file, but it is
88
- * NOT part of the nag model: no persistence, no throttle, no episode state. It
89
- * is rendered once, on the spot, by whoever received the refusal. The refusal
90
- * is a plan fact, so it neither counts toward the nag bands nor is silenced by
91
- * the nag cadence.
92
- *
93
- * Copy names the plan and its price only — no Outposts, no bot pricing (owner
94
- * decisions 10 and 11).
95
- */
96
- export declare const INDIVIDUAL_PLAN_NAME = "Individual";
97
- export declare const INDIVIDUAL_PRICE_LABEL = "$50/mo";
98
82
  /**
99
83
  * The notice a personal feature refusal prints. `upgradeUrl` is the server's
100
84
  * own checkout target; an older hq-pro sends none, and the line then names the
@@ -367,16 +367,18 @@ export function buildOverLine(overEntries, upgradeUrl) {
367
367
  const facts = overEntries
368
368
  .map(([key, entry]) => `${PLAN_LIMIT_DIMENSION_LABELS[key] ?? key} ${entry.used} of ${entry.limit}`)
369
369
  .join(", ");
370
- return (`⚠ HQ Starter: ${facts}. New files and new secrets are paused. ` +
371
- `Upgrade: ${upgradeUrl}`);
370
+ // US-024: names no plan and offers no upgrade; the link is the Billing page.
371
+ return (`⚠ Over this account's limits: ${facts}. New files and new secrets are paused. ` +
372
+ `Billing: ${upgradeUrl}`);
372
373
  }
373
374
  // ---------------------------------------------------------------------------
374
- // Individual-plan feature notice (US-054)
375
+ // Personal feature notice (US-054, copy per US-024)
375
376
  // ---------------------------------------------------------------------------
376
377
  /**
377
- * My Telemetry is an Individual-plan feature on a personal scope (owner
378
- * decision 9 / us-044-individual-plan-design.md §11). When hq-pro refuses it,
379
- * the CLI prints this notice INSTEAD of telemetry output.
378
+ * My Telemetry is a paid feature on a personal scope (owner decision 9 /
379
+ * us-044-individual-plan-design.md §11). When hq-pro refuses it, the CLI
380
+ * prints this notice INSTEAD of telemetry output. The flat personal plan is
381
+ * closed (US-024), so the notice names no plan or price.
380
382
  *
381
383
  * It lives beside the nag copy so plan wording stays in one file, but it is
382
384
  * NOT part of the nag model: no persistence, no throttle, no episode state. It
@@ -384,11 +386,8 @@ export function buildOverLine(overEntries, upgradeUrl) {
384
386
  * is a plan fact, so it neither counts toward the nag bands nor is silenced by
385
387
  * the nag cadence.
386
388
  *
387
- * Copy names the plan and its price only — no Outposts, no bot pricing (owner
388
- * decisions 10 and 11).
389
+ * No Outposts and no bot pricing in the copy (owner decisions 10 and 11).
389
390
  */
390
- export const INDIVIDUAL_PLAN_NAME = "Individual";
391
- export const INDIVIDUAL_PRICE_LABEL = "$50/mo";
392
391
  /** Human names for the features a personal scope can be refused. */
393
392
  const PERSONAL_FEATURE_LABELS = {
394
393
  telemetry: "My Telemetry",
@@ -403,11 +402,11 @@ export function renderPersonalPlanFeatureNotice(feature, upgradeUrl, opts = {})
403
402
  // A person on usage pricing has no Individual plan to buy (US-012).
404
403
  if (isUsageOnly(opts.pricingPlan))
405
404
  return usageOnlyPersonalFeatureNotice(label);
406
- const headline = `${label} is included with the HQ ${INDIVIDUAL_PLAN_NAME} plan (${INDIVIDUAL_PRICE_LABEL}).`;
405
+ const headline = `${label} is not included on your personal account's current billing.`;
407
406
  const action = typeof upgradeUrl === "string" && upgradeUrl.trim().length > 0
408
- ? `Upgrade: ${upgradeUrl.trim()}`
409
- : "Upgrade in your HQ billing settings.";
410
- return `${headline} Your personal account is on the free tier. ${action}`;
407
+ ? `Billing: ${upgradeUrl.trim()}`
408
+ : "See your HQ billing settings.";
409
+ return `${headline} ${action}`;
411
410
  }
412
411
  /** `"{resource}:{band}"` — the persisted key, matching the server's own. */
413
412
  function crossingKey(crossing) {
@@ -429,9 +428,9 @@ export function renderThresholdNotice(crossing, upgradeUrl) {
429
428
  const name = dimensionName(crossing.resource);
430
429
  const counts = `${crossing.used} of ${crossing.limit}`;
431
430
  const headline = crossing.band === "over"
432
- ? `HQ Starter: you are past your ${name} limit (${counts}).`
433
- : `HQ Starter: you have reached 80% of your ${name} (${counts}).`;
434
- return `${headline} Upgrade: ${upgradeUrl}`;
431
+ ? `You are past your ${name} limit (${counts}).`
432
+ : `You have reached 80% of your ${name} limit (${counts}).`;
433
+ return `${headline} Billing: ${upgradeUrl}`;
435
434
  }
436
435
  /**
437
436
  * Print every crossing this machine has not already printed for this episode.
@@ -521,7 +520,7 @@ export function emitPlanLimitNag(opts = {}) {
521
520
  if (worst === null)
522
521
  return;
523
522
  warningShownThisSession = true;
524
- const line = `⚠ HQ Starter: ${formatEntryLine(worst.key, worst.entry)}. Add headroom before you hit the cap: ${resolvedUpgradeUrl}`;
523
+ const line = `⚠ Nearing this account's limits: ${formatEntryLine(worst.key, worst.entry)}. Make room before you hit the cap. Billing: ${resolvedUpgradeUrl}`;
525
524
  write(chalk.yellow(line) + "\n");
526
525
  if (isCliLimitResourceKind(worst.key)) {
527
526
  queueCliLimitNoticeExposure(worst.key, "advisory");
@@ -0,0 +1,60 @@
1
+ /** hq-cli's own bins and the package file each npm launcher runs. */
2
+ export declare const HQ_CLI_BINS: Readonly<Record<string, string>>;
3
+ /**
4
+ * True when `content` is npm's (cmd-shim) PowerShell launcher for the hq-cli
5
+ * bin `bin`. The launcher resolves its own folder, runs node on
6
+ * `$basedir/node_modules/@indigoai-us/hq-cli/<entry>` and exits with node's
7
+ * code. Either slash direction is accepted.
8
+ */
9
+ export declare function isNpmGeneratedHqCliPs1Shim(content: string, bin: string): boolean;
10
+ export interface Ps1ShimFs {
11
+ readFile: (file: string) => string;
12
+ remove: (file: string) => void;
13
+ }
14
+ /** The npm-generated hq-cli `.ps1` launchers present in `binDir`. */
15
+ export declare function findHqCliPs1Shims(binDir: string, io?: Ps1ShimFs): string[];
16
+ export interface RemovePs1ShimsResult {
17
+ /** Launchers removed by this call. */
18
+ removed: string[];
19
+ /** `.ps1` files with an hq-cli bin name that are not npm's launcher. */
20
+ kept: string[];
21
+ /** Launchers that could not be removed, with the reason. */
22
+ failed: Array<{
23
+ file: string;
24
+ reason: string;
25
+ }>;
26
+ }
27
+ /**
28
+ * Remove the npm-generated `.ps1` launchers for hq-cli's bins from `binDir`
29
+ * (the npm global prefix on Windows). Safe to run again: a missing file is
30
+ * not an error. Never touches another package's `.ps1` or a hand-written one.
31
+ */
32
+ export declare function removeHqCliPs1Shims(binDir: string, io?: Ps1ShimFs): RemovePs1ShimsResult;
33
+ /**
34
+ * True when the effective execution policy blocks npm's unsigned launcher.
35
+ * `Undefined` as the effective policy means no scope set one, which Windows
36
+ * client editions treat as `Restricted`.
37
+ */
38
+ export declare function policyBlocksUnsignedScripts(policy: string): boolean;
39
+ export type ExecutionPolicyRun = () => {
40
+ status: number | null;
41
+ stdout: string;
42
+ };
43
+ /**
44
+ * Read the current user's effective PowerShell execution policy. Read-only:
45
+ * it runs `Get-ExecutionPolicy` and never sets anything. Null when
46
+ * PowerShell could not be run or printed nothing usable.
47
+ */
48
+ export declare function readExecutionPolicy(run?: ExecutionPolicyRun): string | null;
49
+ /**
50
+ * Postinstall entry: after a global npm install on Windows, remove hq-cli's
51
+ * `.ps1` launchers from the prefix holding `packageDir`. Acts only when the
52
+ * package sits at `<prefix>/node_modules/@indigoai-us/hq-cli`, npm's Windows
53
+ * global layout. Null when it did nothing. npm 11.16+ may skip lifecycle
54
+ * scripts, so `hq doctor` reports any launcher left behind.
55
+ */
56
+ export declare function removePs1ShimsAfterNpmInstall(packageDir: string, opts: {
57
+ platform: NodeJS.Platform;
58
+ global: boolean;
59
+ }, io?: Ps1ShimFs): RemovePs1ShimsResult | null;
60
+ //# sourceMappingURL=ps1-shims.d.ts.map
@@ -0,0 +1,152 @@
1
+ /**
2
+ * npm writes three launchers for every global bin on Windows: `hq`, `hq.cmd`
3
+ * and `hq.ps1`. PowerShell prefers `hq.ps1`, and under the default
4
+ * `Restricted` execution policy it refuses to run it ("running scripts is
5
+ * disabled on this system", UnauthorizedAccess). `hq.cmd` works under every
6
+ * policy, so hq-cli removes its own `.ps1` launchers and PowerShell falls
7
+ * through to `hq.cmd`.
8
+ *
9
+ * Only a file whose content is npm's generated launcher for an hq-cli bin is
10
+ * removed. A `.ps1` written by a person or another package is left alone. The
11
+ * execution policy is read only, never changed.
12
+ */
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+ import { spawnSync } from "node:child_process";
16
+ /** hq-cli's own bins and the package file each npm launcher runs. */
17
+ export const HQ_CLI_BINS = {
18
+ hq: "dist/index.js",
19
+ "hq-auth-refresh": "dist/bin/hq-auth-refresh.js",
20
+ };
21
+ const PACKAGE_PATH = "node_modules/@indigoai-us/hq-cli/";
22
+ /**
23
+ * True when `content` is npm's (cmd-shim) PowerShell launcher for the hq-cli
24
+ * bin `bin`. The launcher resolves its own folder, runs node on
25
+ * `$basedir/node_modules/@indigoai-us/hq-cli/<entry>` and exits with node's
26
+ * code. Either slash direction is accepted.
27
+ */
28
+ export function isNpmGeneratedHqCliPs1Shim(content, bin) {
29
+ const entry = HQ_CLI_BINS[bin];
30
+ if (!entry)
31
+ return false;
32
+ const text = content.replace(/\r\n/g, "\n").replace(/\\/g, "/");
33
+ if (!text.startsWith("#!/usr/bin/env pwsh\n"))
34
+ return false;
35
+ if (!text.includes("$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent"))
36
+ return false;
37
+ if (!text.trimEnd().endsWith("exit $ret"))
38
+ return false;
39
+ return text.includes(`"$basedir/${PACKAGE_PATH}${entry}"`);
40
+ }
41
+ const realFs = {
42
+ readFile: (file) => fs.readFileSync(file, "utf8"),
43
+ remove: (file) => fs.rmSync(file, { force: true }),
44
+ };
45
+ function isMissing(error) {
46
+ const code = error?.code;
47
+ return code === "ENOENT" || code === "ENOTDIR";
48
+ }
49
+ /** The npm-generated hq-cli `.ps1` launchers present in `binDir`. */
50
+ export function findHqCliPs1Shims(binDir, io = realFs) {
51
+ const found = [];
52
+ for (const bin of Object.keys(HQ_CLI_BINS)) {
53
+ const file = path.join(binDir, `${bin}.ps1`);
54
+ let content;
55
+ try {
56
+ content = io.readFile(file);
57
+ }
58
+ catch {
59
+ continue;
60
+ }
61
+ if (isNpmGeneratedHqCliPs1Shim(content, bin))
62
+ found.push(file);
63
+ }
64
+ return found;
65
+ }
66
+ /**
67
+ * Remove the npm-generated `.ps1` launchers for hq-cli's bins from `binDir`
68
+ * (the npm global prefix on Windows). Safe to run again: a missing file is
69
+ * not an error. Never touches another package's `.ps1` or a hand-written one.
70
+ */
71
+ export function removeHqCliPs1Shims(binDir, io = realFs) {
72
+ const result = { removed: [], kept: [], failed: [] };
73
+ for (const bin of Object.keys(HQ_CLI_BINS)) {
74
+ const file = path.join(binDir, `${bin}.ps1`);
75
+ let content;
76
+ try {
77
+ content = io.readFile(file);
78
+ }
79
+ catch (error) {
80
+ if (!isMissing(error)) {
81
+ result.failed.push({ file, reason: error instanceof Error ? error.message : String(error) });
82
+ }
83
+ continue;
84
+ }
85
+ if (!isNpmGeneratedHqCliPs1Shim(content, bin)) {
86
+ result.kept.push(file);
87
+ continue;
88
+ }
89
+ try {
90
+ io.remove(file);
91
+ result.removed.push(file);
92
+ }
93
+ catch (error) {
94
+ if (isMissing(error))
95
+ continue;
96
+ result.failed.push({ file, reason: error instanceof Error ? error.message : String(error) });
97
+ }
98
+ }
99
+ return result;
100
+ }
101
+ /** Policies under which PowerShell refuses to run an unsigned local `.ps1`. */
102
+ const BLOCKING_POLICIES = new Set(["restricted", "allsigned", "undefined"]);
103
+ /**
104
+ * True when the effective execution policy blocks npm's unsigned launcher.
105
+ * `Undefined` as the effective policy means no scope set one, which Windows
106
+ * client editions treat as `Restricted`.
107
+ */
108
+ export function policyBlocksUnsignedScripts(policy) {
109
+ return BLOCKING_POLICIES.has(policy.trim().toLowerCase());
110
+ }
111
+ function defaultPolicyRun() {
112
+ const run = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Get-ExecutionPolicy"], { encoding: "utf8", windowsHide: true, timeout: 15_000 });
113
+ return { status: run.status, stdout: typeof run.stdout === "string" ? run.stdout : "" };
114
+ }
115
+ /**
116
+ * Read the current user's effective PowerShell execution policy. Read-only:
117
+ * it runs `Get-ExecutionPolicy` and never sets anything. Null when
118
+ * PowerShell could not be run or printed nothing usable.
119
+ */
120
+ export function readExecutionPolicy(run = defaultPolicyRun) {
121
+ try {
122
+ const { status, stdout } = run();
123
+ if (status !== 0)
124
+ return null;
125
+ const policy = stdout.trim().split(/\s+/).pop() ?? "";
126
+ return /^[A-Za-z]+$/.test(policy) ? policy : null;
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ /**
133
+ * Postinstall entry: after a global npm install on Windows, remove hq-cli's
134
+ * `.ps1` launchers from the prefix holding `packageDir`. Acts only when the
135
+ * package sits at `<prefix>/node_modules/@indigoai-us/hq-cli`, npm's Windows
136
+ * global layout. Null when it did nothing. npm 11.16+ may skip lifecycle
137
+ * scripts, so `hq doctor` reports any launcher left behind.
138
+ */
139
+ export function removePs1ShimsAfterNpmInstall(packageDir, opts, io = realFs) {
140
+ if (opts.platform !== "win32" || !opts.global)
141
+ return null;
142
+ const dir = path.resolve(packageDir);
143
+ const scope = path.dirname(dir);
144
+ const nodeModules = path.dirname(scope);
145
+ if (path.basename(dir).toLowerCase() !== "hq-cli" ||
146
+ path.basename(scope).toLowerCase() !== "@indigoai-us" ||
147
+ path.basename(nodeModules).toLowerCase() !== "node_modules") {
148
+ return null;
149
+ }
150
+ return removeHqCliPs1Shims(path.dirname(nodeModules), io);
151
+ }
152
+ //# sourceMappingURL=ps1-shims.js.map
package/dist/main.d.ts CHANGED
@@ -9,7 +9,6 @@ export { handleStreamError } from "./stream-error-boundary.js";
9
9
  export type { StreamErrorDependencies } from "./stream-error-boundary.js";
10
10
  import { type PlanGateError } from "./utils/plan-gate-error.js";
11
11
  import { type PricingPlan } from "./lib/billing/usage-only.js";
12
- import { upgradeToTeam } from "./utils/team-upgrade.js";
13
12
  /**
14
13
  * Total time one CLI invocation may await foreground network work. This spans
15
14
  * the hard version decision, pre-action observability, and release-health
@@ -44,7 +43,6 @@ export type TopLevelErrorDependencies = {
44
43
  argv?: readonly string[];
45
44
  ask?: (question: string) => Promise<string>;
46
45
  openUrl?: (url: string) => Promise<void>;
47
- upgradeToTeam?: typeof upgradeToTeam;
48
46
  /**
49
47
  * Usage pricing (US-012): resolve the account's pricing plan for a plan gate
50
48
  * whose 402 body did not carry one. Absent = use the body only.
package/dist/main.js CHANGED
@@ -40,9 +40,8 @@ import { browserLoginAbandonedMessage } from "./utils/browser-login-abandoned.js
40
40
  import { callbackPortBusyMessage, DEFAULT_CALLBACK_PORT } from "./utils/callback-port-busy.js";
41
41
  import { DEFAULT_COGNITO, isMachineIdentity, loadMachineCachedTokens, } from "./utils/cognito-session.js";
42
42
  import { isCompanySelectionError } from "./utils/company-selection-error.js";
43
- import { canOfferTeamUpgrade, formatPlanGateError, offerTeamUpgrade, offerUpgradeLink, planGateErrorFromVaultError, withPricingPlan, } from "./utils/plan-gate-error.js";
43
+ import { canOfferTeamUpgrade, formatPlanGateError, offerUpgradeLink, planGateActionUrl, planGateErrorFromVaultError, withPricingPlan, } from "./utils/plan-gate-error.js";
44
44
  import { isUsageOnly, resolveCompanyPricingPlan, resolvePersonPricingPlan, } from "./lib/billing/usage-only.js";
45
- import { upgradeToTeam } from "./utils/team-upgrade.js";
46
45
  import { printCachedVersionNudge, printVersionAndNudge, refreshVersionCache, staleAgainstCachedLatest, } from "./utils/version-check.js";
47
46
  import { enforceVersionGate, getVersionGatePhase, isLocalDependencyInstall, resolveRunningInstall, shouldSkipGate, } from "./utils/version-gate.js";
48
47
  import { autoUpdateAndReexec } from "./utils/self-update.js";
@@ -654,8 +653,8 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
654
653
  }
655
654
  else if (planGate) {
656
655
  // Plan denials are expected product limits, never a CLI crash. The
657
- // server URL is always shown. Company refusals that the Workforce
658
- // checkout satisfies retain the direct Stripe checkout action.
656
+ // server URL is always shown. The flat-plan checkout is closed, so the
657
+ // only offer is to open the server's billing link.
659
658
  //
660
659
  // A plan-limit refusal (a Starter hard stop) always prints its reason,
661
660
  // usage and fix options, even non-interactively: the user can fix it
@@ -671,60 +670,23 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
671
670
  ? queueCliLimitNoticeExposure(planGate.details.resource, "refusal", planGate.details.companyUid)
672
671
  : null;
673
672
  deps.setExitCode(1);
674
- // A usage-only account has no plan to buy: no checkout or upgrade offer.
675
- if (interactive && planGate.details.upgradeUrl && !usageOnlyGate) {
676
- const companyUid = planGate.details.companyUid;
677
- const isCompanyWorkforceCheckout = typeof companyUid === "string" &&
678
- /^cmp_[A-Za-z0-9_-]+$/.test(companyUid) &&
679
- (planGate.code === "PLAN_REQUIRED" ||
680
- planGate.code === "MEETING_PLAN_REQUIRED" ||
681
- planGate.code === "BILLING_REQUIRED");
682
- if (isCompanyWorkforceCheckout && companyUid) {
683
- try {
684
- const upgraded = await offerTeamUpgrade(async () => {
685
- await (deps.upgradeToTeam ?? upgradeToTeam)({ companyUid });
686
- }, {
687
- interactive: true,
688
- argv: deps.argv,
689
- ask: deps.ask,
690
- });
691
- if (upgraded)
692
- queueCliLimitNoticeEngagement(promptExposure);
693
- }
694
- catch (upgradeErr) {
695
- const message = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
696
- deps.stderr.write(`hq: ${message}\n`);
697
- try {
698
- const opened = await offerUpgradeLink(planGate.details.upgradeUrl, {
699
- interactive: true,
700
- argv: deps.argv,
701
- ask: deps.ask,
702
- openUrl: deps.openUrl,
703
- });
704
- if (opened)
705
- queueCliLimitNoticeEngagement(promptExposure);
706
- }
707
- catch (linkErr) {
708
- const linkMessage = linkErr instanceof Error ? linkErr.message : String(linkErr);
709
- deps.stderr.write(`hq: ${linkMessage}\n`);
710
- }
711
- }
673
+ // A usage-only account has no plan to buy: no upgrade offer. A usage
674
+ // pause offers its own Billing link.
675
+ const actionUrl = planGateActionUrl(planGate);
676
+ if (interactive && actionUrl && (!usageOnlyGate || planGate.details.usagePause)) {
677
+ try {
678
+ const opened = await offerUpgradeLink(actionUrl, {
679
+ interactive: true,
680
+ argv: deps.argv,
681
+ ask: deps.ask,
682
+ openUrl: deps.openUrl,
683
+ });
684
+ if (opened)
685
+ queueCliLimitNoticeEngagement(promptExposure);
712
686
  }
713
- else {
714
- try {
715
- const opened = await offerUpgradeLink(planGate.details.upgradeUrl, {
716
- interactive: true,
717
- argv: deps.argv,
718
- ask: deps.ask,
719
- openUrl: deps.openUrl,
720
- });
721
- if (opened)
722
- queueCliLimitNoticeEngagement(promptExposure);
723
- }
724
- catch (upgradeErr) {
725
- const message = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
726
- deps.stderr.write(`hq: ${message}\n`);
727
- }
687
+ catch (upgradeErr) {
688
+ const message = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
689
+ deps.stderr.write(`hq: ${message}\n`);
728
690
  }
729
691
  }
730
692
  }
@@ -1,7 +1,7 @@
1
1
  import { type PlanLimitFreeMonth } from "../lib/billing/free-month.js";
2
- import { type PricingPlan } from "../lib/billing/usage-only.js";
2
+ import { type PricingPlan, type UsagePause } from "../lib/billing/usage-only.js";
3
3
  export type PlanGateCode = "PLAN_LIMIT_EXCEEDED" | "PLAN_REQUIRED" | "MEETING_PLAN_REQUIRED"
4
- /** A PERSONAL scope asked for a feature the paid Individual plan includes. */
4
+ /** A PERSONAL scope asked for a feature its current plan does not include. */
5
5
  | "PERSONAL_PLAN_REQUIRED"
6
6
  /** External agent enrollment was denied by the company plan. */
7
7
  | "BILLING_REQUIRED";
@@ -29,19 +29,41 @@ export interface PlanGateDetails {
29
29
  /** Hard-stop 402: the free-month offer, present only when it stands. */
30
30
  freeMonth?: PlanLimitFreeMonth;
31
31
  /**
32
- * Usage pricing (US-012): from the 402 body, or attached by the caller from
33
- * /membership/me, usage-limits or the billing summary. `"usage-only"`
34
- * renders no free month, Starter, Workforce or Individual copy and no plan
35
- * upgrade link. Absent keeps the legacy copy.
32
+ * Usage pricing (US-012): from the 402 body (`billingState` first, then the
33
+ * legacy `pricingPlan`), or attached by the caller from the membership row,
34
+ * usage-limits or the billing summary. `"usage-only"` renders no retired
35
+ * plan copy and no plan upgrade link. Absent (unknown) renders neutral copy
36
+ * that names no plan.
36
37
  */
37
38
  pricingPlan?: PricingPlan;
39
+ /**
40
+ * The server's usage pause (`usagePause` on the 402 body, hq-pro
41
+ * src/billing/usage-pause.ts). When present its message is the whole
42
+ * refusal and its `actionUrl` the only link.
43
+ */
44
+ usagePause?: UsagePause;
45
+ /**
46
+ * Hard-stop 402: true when HQ Workforce is closed to the company (hq-pro
47
+ * `billing.legacy-plans-closed`, owner decision 2026-10-19). `upgradeUrl`
48
+ * is then the usage-pricing switch page; the refusal offers the switch and
49
+ * never the free Workforce month.
50
+ */
51
+ legacyPlansClosed?: boolean;
38
52
  }
39
53
  /** Copy a plan gate with the caller's pricing plan attached. */
40
54
  export declare function withPricingPlan(err: PlanGateError, pricingPlan: PricingPlan | null | undefined): PlanGateError;
41
- /** One renderer for server-selected upgrade links across CLI plan refusals. */
55
+ /**
56
+ * One renderer for server-selected billing links across CLI refusals. The
57
+ * link is labelled `Billing:`, or `Switch:` when the server said it is the
58
+ * usage-pricing switch page. It never says "Upgrade": the flat plans that
59
+ * word led to are closed.
60
+ */
42
61
  export declare function formatUpgradeRefusal(reason: string, upgradeUrl: string | undefined, options?: {
43
62
  urlOnly?: boolean;
63
+ legacyPlansClosed?: boolean;
44
64
  }): string;
65
+ /** The link a refusal offers: the usage pause's action, else the server's upgrade URL. */
66
+ export declare function planGateActionUrl(err: PlanGateError): string | undefined;
45
67
  /** Recover a company UID only from the server's UID-keyed billing callback. */
46
68
  export declare function companyUidFromPlanUpgradeUrl(value: unknown): string | undefined;
47
69
  export declare class PlanGateError extends Error {
@@ -61,14 +83,8 @@ export interface UpgradeLinkOfferDependencies {
61
83
  argv?: readonly string[];
62
84
  interactive?: boolean;
63
85
  }
64
- /** Ask before opening the exact URL returned by the server. */
86
+ /** Ask before opening the exact billing URL returned by the server. */
65
87
  export declare function offerUpgradeLink(url: string, deps?: UpgradeLinkOfferDependencies): Promise<boolean>;
66
- /** Legacy checkout callback retained for callers outside the shared refusal paths. */
67
- export declare function offerTeamUpgrade(onConfirm: () => Promise<void>, deps?: {
68
- ask?: (question: string) => Promise<string>;
69
- argv?: readonly string[];
70
- interactive?: boolean;
71
- }): Promise<boolean>;
72
88
  /** Decode an already-read JSON envelope from either shared HTTP client. */
73
89
  export declare function planGateErrorFromPayload(status: number, body: unknown, companyUid?: string): PlanGateError | null;
74
90
  /**