@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
@@ -131,12 +131,6 @@ export function agentCreatePlanLimitFromPayload(status, body) {
131
131
  : {}),
132
132
  };
133
133
  }
134
- function formatServerPrice(amountMinor, currency) {
135
- return new Intl.NumberFormat("en-US", {
136
- style: "currency",
137
- currency,
138
- }).format(amountMinor / 100);
139
- }
140
134
  async function offerAgentCheckout(url) {
141
135
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
142
136
  try {
@@ -146,7 +140,7 @@ async function offerAgentCheckout(url) {
146
140
  });
147
141
  }
148
142
  catch {
149
- console.error("Could not open the upgrade page automatically. Use the URL above.");
143
+ console.error("Could not open the billing page automatically. Use the URL above.");
150
144
  return false;
151
145
  }
152
146
  finally {
@@ -156,7 +150,7 @@ async function offerAgentCheckout(url) {
156
150
  /** Print a server-priced plan block without minting or rewriting its URL. */
157
151
  export async function surfaceAgentCreatePlanLimit(planLimit, opts = {}) {
158
152
  if (isUsageOnly(opts.pricingPlan)) {
159
- // Usage pricing (US-012): no Workforce plan to buy. The company adds a
153
+ // Usage pricing (US-012): no flat plan to buy. The company adds a
160
154
  // card and each agent box bills by size from the usage card.
161
155
  const card = opts.usageCard ?? BUNDLED_USAGE_CARD;
162
156
  for (const line of usageOnlyCardRequiredLines("An agent box", card)) {
@@ -166,13 +160,13 @@ export async function surfaceAgentCreatePlanLimit(planLimit, opts = {}) {
166
160
  queueCliLimitNoticeExposure("agents", "refusal", opts.companyUid);
167
161
  return;
168
162
  }
169
- const price = `${formatServerPrice(planLimit.amountMinor, planLimit.currency)}/month`;
170
- const reason = `HQ Workforce plan required to create an agent (${price}).`;
163
+ // The server's price was the flat plan's, which is closed: name no plan or price.
164
+ const reason = "Agents are not included on this company's current billing.";
171
165
  const argv = opts.json ? [...process.argv, "--json"] : process.argv;
172
166
  const interactive = canOfferTeamUpgrade(argv);
173
167
  if (!planLimit.checkoutUrl) {
174
168
  console.error(reason);
175
- console.error("Ask a company owner to upgrade before creating an agent.");
169
+ console.error("Ask a company owner to check the company's Billing page before creating an agent.");
176
170
  queueCliLimitNoticeExposure("agents", "refusal", opts.companyUid);
177
171
  return;
178
172
  }
@@ -385,7 +385,10 @@ export function registerAuthCommands(program) {
385
385
  if (!options.retry) {
386
386
  const state = keychain.readKeychainAccessState();
387
387
  if (state.deniedAt) {
388
- console.log("Keychain access wasn't allowed, so HQ uses ~/.hq/cognito-tokens.json. Run `hq auth keychain --retry` to ask again.");
388
+ console.log("Keychain access did not complete, so HQ uses ~/.hq/cognito-tokens.json. Run `hq auth keychain --retry` to ask again.");
389
+ }
390
+ else if (state.lastFailureAt) {
391
+ console.log("Keychain access did not complete. HQ uses your saved sign-in instead and can ask again from a terminal. Run `hq auth keychain --retry` to retry now.");
389
392
  }
390
393
  else if (state.allowedAt) {
391
394
  console.log(chalk.green("HQ can read your sign-in from the Keychain."));
@@ -395,9 +398,7 @@ export function registerAuthCommands(program) {
395
398
  }
396
399
  return;
397
400
  }
398
- const state = keychain.readKeychainAccessState();
399
- if (!state.allowedAt)
400
- console.error(keychain.KEYCHAIN_FIRST_READ_NOTE);
401
+ console.error(keychain.KEYCHAIN_FIRST_READ_NOTE);
401
402
  const result = keychain.readKeychainTokensDetailed({ retry: true, interactive: true, notify: () => { } });
402
403
  if (result.outcome === "hit") {
403
404
  console.log(result.tokens
@@ -408,7 +409,7 @@ export function registerAuthCommands(program) {
408
409
  console.log("No HQ sign-in is saved in the Keychain yet. HQ will use ~/.hq/cognito-tokens.json.");
409
410
  }
410
411
  else {
411
- console.log(chalk.yellow("Keychain access wasn't allowed. HQ will use ~/.hq/cognito-tokens.json. Run `hq auth keychain --retry` to ask again."));
412
+ console.log(chalk.yellow("Keychain access did not complete. HQ will use ~/.hq/cognito-tokens.json. Run `hq auth keychain --retry` to ask again."));
412
413
  }
413
414
  });
414
415
  authCmd
@@ -2,9 +2,9 @@
2
2
  * `hq billing sponsor` — the individual-pull half of plan sponsorship, from the
3
3
  * terminal ("ask a company to sponsor me", hq-pro #3609).
4
4
  *
5
- * A person who is over a personal limit has exactly three ways out: pay $50
6
- * themselves, reduce what they store, or ask a company to cover the $50 plan
7
- * line. This command is the third one, plus the admin side that answers it.
5
+ * A person who is over a personal limit has exactly three ways out: pay for
6
+ * the personal plan themselves, reduce what they store, or ask a company to
7
+ * cover it. This command is the third one, plus the admin side that answers it.
8
8
  *
9
9
  * hq billing sponsor request <company> — ask that company to cover my plan
10
10
  * hq billing sponsor list — the queue (admins), or my own
@@ -36,7 +36,7 @@ export declare const PLAN_RESOURCE: "plan";
36
36
  /** What a person is told when hq-pro's plan-sponsorship switch is off. */
37
37
  export declare const FEATURE_OFF_MESSAGE = "Asking a company to sponsor your plan isn't available yet.";
38
38
  /** What v1 sponsorship covers, stated wherever a person confirms or reads it. */
39
- export declare const COVERAGE_SENTENCE = "Your company pays your $50 plan. Storage above your plan's allowance is still yours.";
39
+ export declare const COVERAGE_SENTENCE = "Your company pays your personal plan. Storage above your plan's allowance is still yours.";
40
40
  /** What a person is told while an approval waits on the sponsor's card. */
41
41
  export declare const HELD_FOR_CARD_SENTENCE = "Your company said yes and is adding a card. Your limits still apply until then.";
42
42
  /** One request row as hq-pro persists and returns it. */
@@ -2,9 +2,9 @@
2
2
  * `hq billing sponsor` — the individual-pull half of plan sponsorship, from the
3
3
  * terminal ("ask a company to sponsor me", hq-pro #3609).
4
4
  *
5
- * A person who is over a personal limit has exactly three ways out: pay $50
6
- * themselves, reduce what they store, or ask a company to cover the $50 plan
7
- * line. This command is the third one, plus the admin side that answers it.
5
+ * A person who is over a personal limit has exactly three ways out: pay for
6
+ * the personal plan themselves, reduce what they store, or ask a company to
7
+ * cover it. This command is the third one, plus the admin side that answers it.
8
8
  *
9
9
  * hq billing sponsor request <company> — ask that company to cover my plan
10
10
  * hq billing sponsor list — the queue (admins), or my own
@@ -41,7 +41,7 @@ export const PLAN_RESOURCE = "plan";
41
41
  /** What a person is told when hq-pro's plan-sponsorship switch is off. */
42
42
  export const FEATURE_OFF_MESSAGE = "Asking a company to sponsor your plan isn't available yet.";
43
43
  /** What v1 sponsorship covers, stated wherever a person confirms or reads it. */
44
- export const COVERAGE_SENTENCE = "Your company pays your $50 plan. Storage above your plan's allowance is still yours.";
44
+ export const COVERAGE_SENTENCE = "Your company pays your personal plan. Storage above your plan's allowance is still yours.";
45
45
  /** What a person is told while an approval waits on the sponsor's card. */
46
46
  export const HELD_FOR_CARD_SENTENCE = "Your company said yes and is adding a card. Your limits still apply until then.";
47
47
  /**
@@ -85,7 +85,7 @@ export function messageForError(status, body) {
85
85
  if (status === 401)
86
86
  return "Your sign-in has expired. Run `hq login` and try again.";
87
87
  if (status === 403 && code === "SPONSOR_PLAN_REQUIRED") {
88
- return "That company's plan can't cover an individual plan. Only HQ Workforce and Enterprise companies can.";
88
+ return "That company can't cover a personal plan on its current billing. Ask an owner of that company to check its Billing page.";
89
89
  }
90
90
  if (status === 403) {
91
91
  return "You're not allowed to do that. Only an owner or admin of the company can answer a sponsorship request.";
@@ -209,7 +209,7 @@ export function registerBillingSponsorCommands(billing) {
209
209
  process.stdout.write(JSON.stringify(data.request, null, 2) + "\n");
210
210
  return;
211
211
  }
212
- console.log(`Asked ${company} to cover your $50 HQ plan.`);
212
+ console.log(`Asked ${company} to cover your personal HQ plan.`);
213
213
  console.log(COVERAGE_SENTENCE);
214
214
  console.log(`An owner or admin there answers next. Check with \`hq billing sponsor list\`.`);
215
215
  }
@@ -276,11 +276,11 @@ export function registerBillingSponsorCommands(billing) {
276
276
  }
277
277
  const where = named ?? "that company";
278
278
  if (!state.active) {
279
- console.log(`No company is covering your $50 HQ plan right now.`);
279
+ console.log(`No company is covering your personal HQ plan right now.`);
280
280
  console.log(`If you asked ${where}, they haven't answered yet — an owner or admin there answers next.`);
281
281
  return;
282
282
  }
283
- console.log(`Your $50 HQ plan: ${statusLabel(state.status ?? "active")}`);
283
+ console.log(`Your personal HQ plan: ${statusLabel(state.status ?? "active")}`);
284
284
  if (state.requestId)
285
285
  console.log(` ${chalk.bold(state.requestId)}`);
286
286
  console.log(COVERAGE_SENTENCE);
@@ -328,7 +328,7 @@ export function registerBillingSponsorCommands(billing) {
328
328
  }
329
329
  console.log((await companyIsUsageOnly(result.companyUid))
330
330
  ? "Approved. Their personal plan is now on the company bill."
331
- : "Approved. Their $50 plan is now on the company bill.");
331
+ : "Approved. Their personal plan is now on the company bill.");
332
332
  }
333
333
  catch (err) {
334
334
  fail(err);
@@ -370,7 +370,7 @@ export function registerBillingSponsorCommands(billing) {
370
370
  }
371
371
  console.log((await companyIsUsageOnly(result.companyUid))
372
372
  ? "Ended. Their personal plan goes back to them from the next bill."
373
- : "Ended. Their $50 plan goes back to them from the next bill.");
373
+ : "Ended. Their personal plan goes back to them from the next bill.");
374
374
  }
375
375
  catch (err) {
376
376
  fail(err);
@@ -396,7 +396,7 @@ export function registerBillingSponsorCommands(billing) {
396
396
  console.log(usageOnlySponsorLines()[2]);
397
397
  return;
398
398
  }
399
- console.log("Withdrawn. You can pay the $50 yourself, reduce what you store, or ask another company.");
399
+ console.log("Withdrawn. You can pay for your plan yourself, reduce what you store, or ask another company.");
400
400
  }
401
401
  catch (err) {
402
402
  fail(err);
@@ -12,18 +12,18 @@
12
12
  *
13
13
  * Tenant boundary: the company comes from the caller's own active memberships
14
14
  * (`/membership/me`); a slug outside them is refused before any read. The
15
- * usage-limits routes are member-gated on the server as well. With no cloud
15
+ * company statement is owner-gated on the server as well. With no cloud
16
16
  * company (or `--personal`) it reads the caller's personal scope.
17
17
  *
18
- * Usage numbers come through `readMonthUsage` (src/lib/billing/usage-cost.ts),
19
- * the single seam to switch to the US-005 daily rollup once it has a read route.
18
+ * Usage numbers come through `readMonthUsage` (src/lib/billing/usage-cost.ts)
19
+ * from the usage statement, `GET /v1/billing/usage` (hq-pro US-006). Only a
20
+ * server without that route (404) falls back to the usage-limits readings.
20
21
  *
21
22
  * Only an account on usage pricing gets costs. The plan comes from the
22
- * server: `pricingPlan` / `usageOnly` on the usage-limits body (hq-pro #5042,
23
- * computed with `isOnUsagePricing`). Only when that body does not say, the
24
- * billing summary the command already reads for the trial. A flat-plan account (for example a Workforce
25
- * company that subscribed before the 2026-10-16 cutover) and an account the
26
- * server says nothing about get readings only, with no rate, cost or
23
+ * server's shared plan status (`billingState`, hq-pro #5088) on the billing
24
+ * summary, with the legacy `pricingPlan` / `usageOnly` fields for a server
25
+ * that does not send it. An account on an earlier flat plan and an account
26
+ * the server says nothing about get readings only, with no rate, cost or
27
27
  * projected bill.
28
28
  */
29
29
  import type { Command } from "commander";
@@ -34,8 +34,19 @@ export interface MembershipRef {
34
34
  slug: string;
35
35
  role?: string;
36
36
  }
37
+ /** One `GET /v1/billing/usage` answer: the HTTP status and the decoded body. */
38
+ export interface UsageStatementResponse {
39
+ status: number;
40
+ body: unknown | null;
41
+ }
37
42
  export interface BillingUsageDeps {
38
43
  listCompanies(): Promise<MembershipRef[]>;
44
+ /**
45
+ * `GET /v1/billing/usage` for the scope (US-015). Absent, or a 404 from a
46
+ * server without the route, falls back to {@link fetchUsageLimits}.
47
+ */
48
+ fetchUsageStatement?: (scope: UsageScope) => Promise<UsageStatementResponse>;
49
+ /** Legacy readings for a server without the statement route. */
39
50
  fetchUsageLimits: ReadMonthUsageDeps["fetchUsageLimits"];
40
51
  readStorageSplit?: (company: MembershipRef, opts: {
41
52
  fresh: boolean;
@@ -77,6 +88,9 @@ export declare function pricingPlanFromSummary(body: unknown): PricingPlan | nul
77
88
  * only when usage-limits does not say.
78
89
  */
79
90
  export declare function resolveOnUsagePricing(limitsPlan: PricingPlan | null, summaryPlan: PricingPlan | null): boolean | null;
91
+ /** Thrown when the company statement is refused to a caller who is not its owner. */
92
+ export declare class UsageStatementForbiddenError extends Error {
93
+ }
80
94
  export declare function runBillingUsage(opts: BillingUsageOptions, deps: BillingUsageDeps): Promise<BillingUsageReport>;
81
95
  /**
82
96
  * `hq billing usage --card`: the card from GET /v1/pricing, or the bundled
@@ -12,27 +12,27 @@
12
12
  *
13
13
  * Tenant boundary: the company comes from the caller's own active memberships
14
14
  * (`/membership/me`); a slug outside them is refused before any read. The
15
- * usage-limits routes are member-gated on the server as well. With no cloud
15
+ * company statement is owner-gated on the server as well. With no cloud
16
16
  * company (or `--personal`) it reads the caller's personal scope.
17
17
  *
18
- * Usage numbers come through `readMonthUsage` (src/lib/billing/usage-cost.ts),
19
- * the single seam to switch to the US-005 daily rollup once it has a read route.
18
+ * Usage numbers come through `readMonthUsage` (src/lib/billing/usage-cost.ts)
19
+ * from the usage statement, `GET /v1/billing/usage` (hq-pro US-006). Only a
20
+ * server without that route (404) falls back to the usage-limits readings.
20
21
  *
21
22
  * Only an account on usage pricing gets costs. The plan comes from the
22
- * server: `pricingPlan` / `usageOnly` on the usage-limits body (hq-pro #5042,
23
- * computed with `isOnUsagePricing`). Only when that body does not say, the
24
- * billing summary the command already reads for the trial. A flat-plan account (for example a Workforce
25
- * company that subscribed before the 2026-10-16 cutover) and an account the
26
- * server says nothing about get readings only, with no rate, cost or
23
+ * server's shared plan status (`billingState`, hq-pro #5088) on the billing
24
+ * summary, with the legacy `pricingPlan` / `usageOnly` fields for a server
25
+ * that does not send it. An account on an earlier flat plan and an account
26
+ * the server says nothing about get readings only, with no rate, cost or
27
27
  * projected bill.
28
28
  */
29
29
  import chalk from "chalk";
30
30
  import { ensureCognitoToken } from "../utils/cognito-session.js";
31
31
  import { vaultApiFetch, vaultApiFetchPublic } from "../utils/vault-api.js";
32
- import { buildReadingsReport, buildReport, formatReport, readMonthUsage, } from "../lib/billing/usage-cost.js";
32
+ import { buildReadingsReport, buildReport, formatReport, readMonthUsage, usageLimitsSource, usageStatementSource, } from "../lib/billing/usage-cost.js";
33
33
  import { ratesFromPricingStatement } from "../lib/billing/usage-rate-card.js";
34
34
  import { fetchUsageCard, formatUsageCardLines } from "../lib/billing/usage-card.js";
35
- import { fetchCompanyBillingSummary, pricingPlanFromBody, usageTrialActiveFromBody, } from "../lib/billing/usage-only.js";
35
+ import { fetchCompanyBillingSummary, pricingPlanFromBody, hasBillingStateField, usageTrialActiveFromBody, } from "../lib/billing/usage-only.js";
36
36
  export class UsageScopeError extends Error {
37
37
  }
38
38
  /** Pick the scope. Never reaches outside the caller's own memberships. */
@@ -67,7 +67,7 @@ function asRecord(value) {
67
67
  /** The plan on a usage-limits body: `pricingPlan`, else the `usageOnly` / `onUsagePricing` boolean. */
68
68
  export function pricingPlanFromUsageLimits(body) {
69
69
  const plan = pricingPlanFromBody(body);
70
- if (plan)
70
+ if (plan || hasBillingStateField(body))
71
71
  return plan;
72
72
  const rec = asRecord(body);
73
73
  const flag = typeof rec?.onUsagePricing === "boolean" ? rec.onUsagePricing : rec?.usageOnly;
@@ -85,9 +85,12 @@ export function pricingPlanFromSummary(body) {
85
85
  const rec = asRecord(body);
86
86
  if (!rec)
87
87
  return null;
88
- // `plan: "usage"` only means "no Workforce subscription", not usage pricing,
89
- // so it never turns pricing on. Without pricingPlan, show no cost.
90
- return pricingPlanFromBody(rec) ?? "current";
88
+ // `plan: "usage"` only means "no flat-plan subscription", not usage pricing,
89
+ // so it never turns pricing on. A server that sends billingState answers
90
+ // from it alone (unknown stays unknown); an older one without pricingPlan
91
+ // shows no cost.
92
+ const plan = pricingPlanFromBody(rec);
93
+ return hasBillingStateField(rec) ? plan : plan ?? "current";
91
94
  }
92
95
  /**
93
96
  * True: price the meters. False: flat plan. Null: unknown, show no cost.
@@ -101,20 +104,44 @@ export function resolveOnUsagePricing(limitsPlan, summaryPlan) {
101
104
  return null;
102
105
  return plan === "usage-only";
103
106
  }
107
+ /** Thrown when the company statement is refused to a caller who is not its owner. */
108
+ export class UsageStatementForbiddenError extends Error {
109
+ }
110
+ /**
111
+ * Read the statement, or say the caller should use the legacy readings: null
112
+ * means the server has no statement route (404) or no reader was given.
113
+ */
114
+ async function readStatementBody(scope, deps) {
115
+ if (!deps.fetchUsageStatement)
116
+ return null;
117
+ const res = await deps.fetchUsageStatement(scope);
118
+ if (res.status === 404)
119
+ return null;
120
+ if (res.status === 403 && scope.kind === "company") {
121
+ throw new UsageStatementForbiddenError(`Only the owner of '${scope.companySlug}' can see its usage statement. ` +
122
+ "Run `hq billing usage --personal` for your own usage.");
123
+ }
124
+ return { body: res.status >= 200 && res.status < 300 ? res.body : null };
125
+ }
104
126
  export async function runBillingUsage(opts, deps) {
105
127
  const memberships = await deps.listCompanies();
106
128
  const scope = resolveUsageScope(memberships, opts);
107
129
  const membership = scope.kind === "company" ? memberships.find((m) => m.uid === scope.companyUid) : undefined;
108
130
  let limitsBody = null;
131
+ const statement = await readStatementBody(scope, deps);
132
+ const usageDeps = {
133
+ fetchUsageLimits: async (s) => (limitsBody = await deps.fetchUsageLimits(s)),
134
+ now: deps.now,
135
+ ...(deps.readTrialActive ? { readTrialActive: deps.readTrialActive } : {}),
136
+ ...(opts.storageScan !== false && deps.readStorageSplit && membership
137
+ ? { readStorageSplit: () => deps.readStorageSplit(membership, { fresh: opts.fresh === true }) }
138
+ : {}),
139
+ };
140
+ const source = statement
141
+ ? usageStatementSource(async () => statement.body)
142
+ : usageLimitsSource(usageDeps);
109
143
  const [usage, pricing] = await Promise.all([
110
- readMonthUsage(scope, {
111
- fetchUsageLimits: async (s) => (limitsBody = await deps.fetchUsageLimits(s)),
112
- now: deps.now,
113
- ...(deps.readTrialActive ? { readTrialActive: deps.readTrialActive } : {}),
114
- ...(opts.storageScan !== false && deps.readStorageSplit && membership
115
- ? { readStorageSplit: () => deps.readStorageSplit(membership, { fresh: opts.fresh === true }) }
116
- : {}),
117
- }),
144
+ readMonthUsage(scope, usageDeps, source),
118
145
  deps.fetchPricing().catch(() => null),
119
146
  ]);
120
147
  const limitsPlan = pricingPlanFromUsageLimits(limitsBody);
@@ -142,6 +169,14 @@ function productionDeps() {
142
169
  const { companiesFromMemberships } = await import("./storage.js");
143
170
  return companiesFromMemberships(await res.json());
144
171
  },
172
+ async fetchUsageStatement(scope) {
173
+ const res = await vaultApiFetch(scope.kind === "company"
174
+ ? { token: await token(), path: "/v1/billing/usage", query: { companyUid: scope.companyUid } }
175
+ : { token: await token(), path: "/v1/billing/usage", query: { scope: "person" } });
176
+ if (!res.ok)
177
+ return { status: res.status, body: null };
178
+ return { status: res.status, body: await res.json().catch(() => null) };
179
+ },
145
180
  async fetchUsageLimits(scope) {
146
181
  const res = await vaultApiFetch(scope.kind === "company"
147
182
  ? { token: await token(), path: "/v1/billing/usage-limits", query: { companyUid: scope.companyUid } }
@@ -13,16 +13,16 @@
13
13
  * hq billing checkout [--company <slug>] [--personal] — mint a card-capture link
14
14
  * hq billing usage [--company <slug>] [--personal] — usage, projected bill,
15
15
  * drivers (read-only)
16
- * hq billing upgrade [--company <slug>] — Workforce checkout
17
- * (legacy plans only)
16
+ * hq billing upgrade [--company <slug>] — the switch to usage
17
+ * pricing (earlier plans)
18
18
  * hq billing sponsor … — ask a company to cover
19
- * your $50 plan, or
19
+ * your personal plan, or
20
20
  * answer such a request
21
21
  *
22
- * Usage pricing (US-012): a company whose billing summary says
23
- * `pricingPlan: "usage-only"` has no plan to upgrade to. `hq billing upgrade`
24
- * tells it so and points at `hq billing usage` instead of opening Workforce
25
- * checkout. Legacy companies keep the Workforce flow unchanged.
22
+ * Usage pricing (US-012, US-024): a company on usage pricing has no plan to
23
+ * upgrade to; `hq billing upgrade` tells it so and points at
24
+ * `hq billing usage`. Every other company is pointed at the switch to usage
25
+ * pricing: the flat-plan checkout is closed, so the CLI never opens it.
26
26
  */
27
27
  import { Command } from "commander";
28
28
  interface DatabaseBilling {
@@ -13,22 +13,22 @@
13
13
  * hq billing checkout [--company <slug>] [--personal] — mint a card-capture link
14
14
  * hq billing usage [--company <slug>] [--personal] — usage, projected bill,
15
15
  * drivers (read-only)
16
- * hq billing upgrade [--company <slug>] — Workforce checkout
17
- * (legacy plans only)
16
+ * hq billing upgrade [--company <slug>] — the switch to usage
17
+ * pricing (earlier plans)
18
18
  * hq billing sponsor … — ask a company to cover
19
- * your $50 plan, or
19
+ * your personal plan, or
20
20
  * answer such a request
21
21
  *
22
- * Usage pricing (US-012): a company whose billing summary says
23
- * `pricingPlan: "usage-only"` has no plan to upgrade to. `hq billing upgrade`
24
- * tells it so and points at `hq billing usage` instead of opening Workforce
25
- * checkout. Legacy companies keep the Workforce flow unchanged.
22
+ * Usage pricing (US-012, US-024): a company on usage pricing has no plan to
23
+ * upgrade to; `hq billing upgrade` tells it so and points at
24
+ * `hq billing usage`. Every other company is pointed at the switch to usage
25
+ * pricing: the flat-plan checkout is closed, so the CLI never opens it.
26
26
  */
27
27
  import chalk from "chalk";
28
28
  import { ensureCognitoToken } from "../utils/cognito-session.js";
29
29
  import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
30
30
  import { mintPaymentLink } from "../utils/billing-gate.js";
31
- import { upgradeToTeam } from "../utils/team-upgrade.js";
31
+ import { openUsageSwitch } from "../utils/team-upgrade.js";
32
32
  import { registerBillingSponsorCommands } from "./billing-sponsor.js";
33
33
  import { fetchCompanyPricingPlan, isUsageOnly, usageOnlyUpgradeLines, } from "../lib/billing/usage-only.js";
34
34
  import { fetchUsageCard } from "../lib/billing/usage-card.js";
@@ -157,10 +157,10 @@ export function registerBillingCommand(program) {
157
157
  });
158
158
  billing
159
159
  .command("upgrade")
160
- .description("Open Stripe Checkout to upgrade a company on an earlier plan (usage-pricing companies have no plan to upgrade)")
160
+ .description("Open the switch to usage pricing for a company on an earlier plan (usage-pricing companies have nothing to change)")
161
161
  .option("--company <slug>", "Company slug (resolves to companyUid)")
162
- .option("--no-browser", "Print the Stripe Checkout URL instead of opening it")
163
- .option("--json", "Emit the Checkout URL as JSON")
162
+ .option("--no-browser", "Print the URL instead of opening it")
163
+ .option("--json", "Emit the URL as JSON")
164
164
  .action(async function (opts) {
165
165
  try {
166
166
  const token = await ensureCognitoToken();
@@ -177,8 +177,8 @@ export function registerBillingCommand(program) {
177
177
  console.log(line);
178
178
  return;
179
179
  }
180
- await upgradeToTeam({
181
- companyUid,
180
+ await openUsageSwitch({
181
+ companySlug: companyOf(this),
182
182
  noBrowser: opts.browser === false,
183
183
  json: opts.json,
184
184
  });
@@ -484,8 +484,8 @@ async function defaultRunInitialSync(args) {
484
484
  if (event.type === "plan-limit") {
485
485
  failureCount += Math.max(1, event.count ?? 0, event.paths?.length ?? 0);
486
486
  firstFailure ??= {
487
- path: event.paths?.[0] ?? "plan limit",
488
- message: event.message ?? "plan limit refused upload",
487
+ path: event.paths?.[0] ?? "new files",
488
+ message: event.message ?? "new files are paused for this account",
489
489
  };
490
490
  return;
491
491
  }
@@ -1119,7 +1119,7 @@ export function registerCloudCommands(program) {
1119
1119
  // and keep their exit status.
1120
1120
  log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
1121
1121
  `${result.filesSkipped} skipped), but ${describeRefusedFiles(planLimitRefused)} ` +
1122
- "refused by your plan limit."));
1122
+ "refused: new files are paused for this account."));
1123
1123
  for (const line of renderPlanLimitRefusalLines(planLimitRefused)) {
1124
1124
  log(chalk.yellow(line));
1125
1125
  }
@@ -1811,7 +1811,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
1811
1811
  `${r.conflictPaths.length} conflict(s)` +
1812
1812
  (r.aborted ? " — aborted" : "") +
1813
1813
  (legRefused
1814
- ? ` — ${describeRefusedCount(legRefused)} refused by plan limit`
1814
+ ? ` — ${describeRefusedCount(legRefused)} refused (new files paused)`
1815
1815
  : ""));
1816
1816
  if (legRefused) {
1817
1817
  printPlanLimitRefusal(legRefused);
@@ -1828,7 +1828,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
1828
1828
  `${result.filesRefusedStale ?? 0} stale-refused; ` +
1829
1829
  `${result.filesUploadFailed ?? 0} failed` +
1830
1830
  (refusedTargets > 0
1831
- ? `; new files refused by a plan limit on ${refusedTargets} target(s)`
1831
+ ? `; new files paused on ${refusedTargets} target(s)`
1832
1832
  : "");
1833
1833
  console.log(errored > 0 ||
1834
1834
  result.filesExcludedByScope > 0 ||
@@ -1973,7 +1973,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1973
1973
  `${pushResult.filesDeleted} deleted, ${pushResult.filesExcludedByScope} scope-excluded` +
1974
1974
  (pushResult.aborted ? " — aborted" : "") +
1975
1975
  (pushRefused
1976
- ? ` — ${describeRefusedCount(pushRefused)} refused by plan limit`
1976
+ ? ` — ${describeRefusedCount(pushRefused)} refused (new files paused)`
1977
1977
  : ""));
1978
1978
  if (pushRefused)
1979
1979
  printPlanLimitRefusal(pushRefused, " ");
@@ -2105,7 +2105,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
2105
2105
  if (pushRefused) {
2106
2106
  // The pull ran, but the push did not upload everything: no success line.
2107
2107
  console.log(chalk.yellow(`\n⚠ Sync now finished, but ${describeRefusedFiles(pushRefused)} ` +
2108
- "refused by your plan limit (listed above)."));
2108
+ "refused: new files are paused for this account (listed above)."));
2109
2109
  throw new SyncExitError(1, { health: "succeeded" });
2110
2110
  }
2111
2111
  if (pushResult.filesExcludedByScope > 0 ||
@@ -2159,9 +2159,9 @@ async function runNowAll(hqRoot, message, onConflict, modeAllOverride, skipPerso
2159
2159
  // like a clean sync. Its per-target details were printed above.
2160
2160
  if (refused.planLimitRefused) {
2161
2161
  console.log(chalk.yellow(`\n⚠ Sync finished, but ${describeRefusedFiles(refused.planLimitRefused)} refused ` +
2162
- "by your plan limit (listed under push --all above)."));
2162
+ "because new files are paused for this account (listed under push --all above)."));
2163
2163
  if (refused.planLimitRefused.upgradeUrl) {
2164
- console.log(chalk.yellow(` Upgrade: ${refused.planLimitRefused.upgradeUrl}`));
2164
+ console.log(chalk.yellow(` Billing: ${refused.planLimitRefused.upgradeUrl}`));
2165
2165
  }
2166
2166
  }
2167
2167
  console.log(chalk.yellow("\n⚠ Sync finished, but one or more requested uploads were refused."));
@@ -1,6 +1,7 @@
1
1
  export type SessionHelperRuntimeOverrides = {
2
2
  env?: NodeJS.ProcessEnv;
3
3
  now?: () => Date;
4
+ beforeCounterAction?: () => void;
4
5
  };
5
6
  export type SessionHelperRuntime = {
6
7
  env: NodeJS.ProcessEnv;
@@ -1,6 +1,7 @@
1
1
  type SessionHelperRuntimeOverrides = {
2
2
  env?: NodeJS.ProcessEnv;
3
3
  now?: () => Date;
4
+ beforeCounterAction?: () => void;
4
5
  };
5
6
  export type SessionJournalOperationResult = {
6
7
  code: number;