@indigoai-us/hq-cli 5.345.143 → 5.345.145

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 (69) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/dist/command-catalog.generated.d.ts +286 -6
  3. package/dist/command-catalog.generated.js +351 -6
  4. package/dist/command-registration-plan.d.ts +6 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/agent-enroll.d.ts +1 -0
  7. package/dist/commands/agent-enroll.js +7 -0
  8. package/dist/commands/agents.d.ts +25 -3
  9. package/dist/commands/agents.js +57 -4
  10. package/dist/commands/api-keys.js +66 -8
  11. package/dist/commands/billing-sponsor.d.ts +4 -0
  12. package/dist/commands/billing-sponsor.js +48 -5
  13. package/dist/commands/billing-usage.d.ts +59 -0
  14. package/dist/commands/billing-usage.js +162 -0
  15. package/dist/commands/billing.d.ts +9 -0
  16. package/dist/commands/billing.js +29 -2
  17. package/dist/commands/core-checkpoint-native.d.ts +30 -0
  18. package/dist/commands/core-checkpoint-native.js +167 -10
  19. package/dist/commands/core-job-notify-native.js +53 -3
  20. package/dist/commands/core-jobs-native.d.ts +8 -0
  21. package/dist/commands/core-jobs-native.js +34 -2
  22. package/dist/commands/core-policy.js +76 -13
  23. package/dist/commands/library-add.d.ts +120 -0
  24. package/dist/commands/library-add.js +434 -0
  25. package/dist/commands/library-caption.d.ts +112 -0
  26. package/dist/commands/library-caption.js +393 -0
  27. package/dist/commands/library-link.d.ts +32 -0
  28. package/dist/commands/library-link.js +95 -0
  29. package/dist/commands/library.d.ts +72 -0
  30. package/dist/commands/library.js +527 -0
  31. package/dist/commands/meetings.d.ts +8 -0
  32. package/dist/commands/meetings.js +25 -2
  33. package/dist/commands/members.d.ts +20 -0
  34. package/dist/commands/members.js +51 -2
  35. package/dist/commands/storage-offload.d.ts +16 -2
  36. package/dist/commands/storage-offload.js +55 -4
  37. package/dist/commands/storage.d.ts +8 -1
  38. package/dist/commands/storage.js +6 -2
  39. package/dist/commands/whoami.js +9 -0
  40. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2410 -2063
  41. package/dist/hq-cloud-sync-runner/package.json +1 -1
  42. package/dist/lib/billing/plan-lock.d.ts +15 -1
  43. package/dist/lib/billing/plan-lock.js +45 -5
  44. package/dist/lib/billing/usage-card.d.ts +82 -0
  45. package/dist/lib/billing/usage-card.js +198 -0
  46. package/dist/lib/billing/usage-cost.d.ts +214 -0
  47. package/dist/lib/billing/usage-cost.js +440 -0
  48. package/dist/lib/billing/usage-only.d.ts +87 -0
  49. package/dist/lib/billing/usage-only.js +262 -0
  50. package/dist/lib/billing/usage-rate-card.d.ts +51 -0
  51. package/dist/lib/billing/usage-rate-card.js +62 -0
  52. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  53. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  54. package/dist/lib/plan-limit-nag.d.ts +4 -6
  55. package/dist/lib/plan-limit-nag.js +19 -1
  56. package/dist/lib/provider-launcher.d.ts +22 -0
  57. package/dist/lib/provider-launcher.js +65 -0
  58. package/dist/lib/search-index/index.d.ts +2 -0
  59. package/dist/lib/search-index/index.js +34 -0
  60. package/dist/lib/search-index/max-doc-bytes.d.ts +2 -1
  61. package/dist/lib/search-index/max-doc-bytes.js +2 -1
  62. package/dist/main.d.ts +9 -0
  63. package/dist/main.js +20 -3
  64. package/dist/utils/billing-gate.d.ts +19 -4
  65. package/dist/utils/billing-gate.js +25 -4
  66. package/dist/utils/plan-gate-error.d.ts +10 -0
  67. package/dist/utils/plan-gate-error.js +54 -0
  68. package/dist/utils/vault-api.js +4 -1
  69. package/package.json +2 -2
@@ -10,6 +10,45 @@ async function requireCognitoForApiKeys(label) {
10
10
  requireApiKeyCapability(null, label);
11
11
  return ensureCognitoToken();
12
12
  }
13
+ /**
14
+ * Named capability bundles for `api-keys create --preset`. A preset grants
15
+ * exactly its listed capabilities — no secret scopes, no deploy block — so a
16
+ * headless agent's key cannot reach anything the preset does not name.
17
+ */
18
+ const API_KEY_PRESETS = {
19
+ /** Media library agent: search assets and mint download links. */
20
+ "library-agent": {
21
+ capabilities: ["search:query", "files:read"],
22
+ uploadCapability: "files:write",
23
+ },
24
+ };
25
+ function isApiKeyPresetName(value) {
26
+ return Object.prototype.hasOwnProperty.call(API_KEY_PRESETS, value);
27
+ }
28
+ /**
29
+ * Capabilities for `--preset`. Rejects an unknown preset and any flag that
30
+ * would widen or reshape the grant, so the minted key matches the preset.
31
+ */
32
+ function resolvePresetCapabilities(opts) {
33
+ const name = opts.preset ?? "";
34
+ if (!isApiKeyPresetName(name)) {
35
+ throw new Error(`Unknown preset '${name}'. Supported: ${Object.keys(API_KEY_PRESETS).join(", ")}`);
36
+ }
37
+ const conflicts = [
38
+ opts.capability.length > 0 ? "--capability" : null,
39
+ opts.scope.length > 0 ? "--scope" : null,
40
+ opts.deploy === true ? "--deploy" : null,
41
+ opts.deployApp.length > 0 ? "--deploy-app" : null,
42
+ opts.permission !== undefined ? "--permission" : null,
43
+ ].filter((flag) => flag !== null);
44
+ if (conflicts.length > 0) {
45
+ throw new Error(`--preset ${name} grants a fixed capability set; remove ${conflicts.join(", ")}.`);
46
+ }
47
+ const preset = API_KEY_PRESETS[name];
48
+ return opts.allowUpload === true
49
+ ? [...preset.capabilities, preset.uploadCapability]
50
+ : [...preset.capabilities];
51
+ }
13
52
  function collectRepeatedOption(value, previous) {
14
53
  return [...previous, value];
15
54
  }
@@ -160,9 +199,16 @@ export function registerApiKeysCommand(program) {
160
199
  .option("--deploy-app <id>", "Deploy app id/slug allowed for publish (repeatable); narrows the deploy grant", collectRepeatedOption, [])
161
200
  .option("--permission <level>", "Secret permission level: read | write | admin (applies to secret scopes; defaults to read)")
162
201
  .option("--expires <ISO8601>", "Optional ISO-8601 expiry timestamp")
202
+ .option("--preset <name>", `Grant a fixed capability bundle: ${Object.keys(API_KEY_PRESETS).join(" | ")} (library-agent = search:query, files:read)`)
203
+ .option("--allow-upload", "With --preset library-agent, also grant files:write (hq library add)")
163
204
  .action(async (opts) => {
164
205
  try {
165
- const capabilities = parseCapabilities(opts.capability);
206
+ if (opts.allowUpload === true && opts.preset === undefined) {
207
+ throw new Error("--allow-upload requires --preset library-agent.");
208
+ }
209
+ const capabilities = opts.preset !== undefined
210
+ ? resolvePresetCapabilities(opts)
211
+ : parseCapabilities(opts.capability);
166
212
  const wantsDeploy = hasDeployCapability(capabilities) ||
167
213
  opts.deploy === true ||
168
214
  opts.deployApp.length > 0;
@@ -250,13 +296,25 @@ export function registerApiKeysCommand(program) {
250
296
  }
251
297
  console.log(" Export it for automation (never falls back to a session):");
252
298
  console.log(`\n export HQ_API_KEY='${data.key.value}'\n`);
253
- console.log(" Vault secrets:");
254
- console.log(" hq secrets get <NAME> --reveal");
255
- console.log(" hq secrets exec --only <NAME> -- <command>");
256
- console.log(" hq secrets list --company <cmp_uid> (secrets:read)");
257
- console.log(" hq secrets set <NAME> --from-stdin --company <cmp_uid> (secrets:write)");
258
- console.log(" Or HTTP: POST /v1/keys/secrets/fetch with Authorization: Bearer <key>");
259
- console.log(chalk.dim(" Company-scoped commands need --company <cmp_uid> (or HQ_COMPANY_UID) — slug lookup needs a session."));
299
+ if (opts.preset === "library-agent") {
300
+ console.log(" Media library:");
301
+ console.log(` hq library search <query> --company ${companyRef ?? companyUid} (search:query)`);
302
+ console.log(` hq library link <vault-key> --company ${companyRef ?? companyUid} (files:read)`);
303
+ if (capabilities.includes("files:write")) {
304
+ console.log(` hq library add <file> --company ${companyRef ?? companyUid} (files:write)`);
305
+ }
306
+ console.log(" Or HTTP: POST /v1/keys/search and /v1/keys/files/presign with Authorization: Bearer <key>");
307
+ console.log(chalk.dim(" Download links are bearer URLs — never post them in Slack or shared channels."));
308
+ }
309
+ if (opts.preset === undefined) {
310
+ console.log(" Vault secrets:");
311
+ console.log(" hq secrets get <NAME> --reveal");
312
+ console.log(" hq secrets exec --only <NAME> -- <command>");
313
+ console.log(" hq secrets list --company <cmp_uid> (secrets:read)");
314
+ console.log(" hq secrets set <NAME> --from-stdin --company <cmp_uid> (secrets:write)");
315
+ console.log(" Or HTTP: POST /v1/keys/secrets/fetch with Authorization: Bearer <key>");
316
+ console.log(chalk.dim(" Company-scoped commands need --company <cmp_uid> (or HQ_COMPANY_UID) — slug lookup needs a session."));
317
+ }
260
318
  if (data.apiKey.scope.deploy?.apps?.length) {
261
319
  const deployApps = data.apiKey.scope.deploy.apps;
262
320
  console.log(deployApps.includes(DEPLOY_APP_ALL)
@@ -19,6 +19,10 @@
19
19
  * them: it resolves the caller's person id and the company's admin set
20
20
  * server-side, so nothing here is trusted from the client.
21
21
  *
22
+ * USAGE PRICING (US-012). A person whose person billing summary carries a
23
+ * `personalUsage` block is on usage pricing and has no personal plan to
24
+ * sponsor. `request` tells them so and files nothing.
25
+ *
22
26
  * FEATURE SWITCH. The `plan` request kind only exists while hq-pro's
23
27
  * `PLAN_SPONSORSHIP_REQUESTS_ENABLED` switch is on. With the switch off the
24
28
  * backend does not recognise `plan` and answers 400 `MISSING_FIELDS` — which
@@ -19,6 +19,10 @@
19
19
  * them: it resolves the caller's person id and the company's admin set
20
20
  * server-side, so nothing here is trusted from the client.
21
21
  *
22
+ * USAGE PRICING (US-012). A person whose person billing summary carries a
23
+ * `personalUsage` block is on usage pricing and has no personal plan to
24
+ * sponsor. `request` tells them so and files nothing.
25
+ *
22
26
  * FEATURE SWITCH. The `plan` request kind only exists while hq-pro's
23
27
  * `PLAN_SPONSORSHIP_REQUESTS_ENABLED` switch is on. With the switch off the
24
28
  * backend does not recognise `plan` and answers 400 `MISSING_FIELDS` — which
@@ -31,6 +35,7 @@ import { diagnosticsEnabled } from "../lib/diagnostics.js";
31
35
  import { ensureCognitoToken } from "../utils/cognito-session.js";
32
36
  import { isCompanySelectionError } from "../utils/company-selection-error.js";
33
37
  import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
38
+ import { fetchCompanyPricingPlan, fetchPersonPricingPlan, isUsageOnly, usageOnlySponsorLines, } from "../lib/billing/usage-only.js";
34
39
  /** The request kind this command files. Matches hq-pro's contract. */
35
40
  export const PLAN_RESOURCE = "plan";
36
41
  /** What a person is told when hq-pro's plan-sponsorship switch is off. */
@@ -163,18 +168,41 @@ async function transition(requestId, op) {
163
168
  });
164
169
  return { ...data.request, code: data.code, addCardUrl: data.addCardUrl };
165
170
  }
171
+ /** Whether the sponsoring company is on usage pricing. Unknown counts as no. */
172
+ async function companyIsUsageOnly(companyUid) {
173
+ if (!companyUid)
174
+ return false;
175
+ try {
176
+ return isUsageOnly(await fetchCompanyPricingPlan(await ensureCognitoToken(), companyUid));
177
+ }
178
+ catch {
179
+ return false;
180
+ }
181
+ }
166
182
  export function registerBillingSponsorCommands(billing) {
167
183
  const sponsor = billing
168
184
  .command("sponsor")
169
- .description("Ask a company to cover your $50 plan, or answer a request");
185
+ .description("Ask a company to cover your personal HQ plan, or answer a request");
170
186
  sponsor
171
187
  .command("request")
172
188
  .argument("<company>", "Company slug or uid to ask")
173
- .description("Ask a company to cover your $50 HQ plan")
189
+ .description("Ask a company to cover your personal HQ plan")
174
190
  .option("--json", "Emit raw JSON")
175
191
  .action(async function (company, opts) {
176
192
  try {
177
193
  const token = await ensureCognitoToken();
194
+ // Usage pricing (US-012): a person on usage pricing has no personal
195
+ // plan for a company to cover, so nothing is filed.
196
+ if (isUsageOnly(await fetchPersonPricingPlan(token))) {
197
+ const lines = usageOnlySponsorLines();
198
+ if (opts.json) {
199
+ process.stdout.write(JSON.stringify({ pricingPlan: "usage-only", request: null, message: lines.join(" ") }, null, 2) + "\n");
200
+ return;
201
+ }
202
+ for (const line of lines)
203
+ console.log(line);
204
+ return;
205
+ }
178
206
  const companyUid = await getCompanyUid(token, company);
179
207
  const data = await callApi(token, "/v1/billing/sponsorship/request", { method: "POST", body: { companyUid, resource: PLAN_RESOURCE } });
180
208
  if (opts.json) {
@@ -235,6 +263,12 @@ export function registerBillingSponsorCommands(billing) {
235
263
  console.log(line);
236
264
  return;
237
265
  }
266
+ // Usage pricing (US-012): no personal plan, so no plan status to show.
267
+ if (!opts.json && isUsageOnly(await fetchPersonPricingPlan(token))) {
268
+ for (const line of usageOnlySponsorLines())
269
+ console.log(line);
270
+ return;
271
+ }
238
272
  const state = await readSelfState(token, companyUid);
239
273
  if (opts.json) {
240
274
  process.stdout.write(JSON.stringify(state, null, 2) + "\n");
@@ -277,7 +311,7 @@ export function registerBillingSponsorCommands(billing) {
277
311
  sponsor
278
312
  .command("approve")
279
313
  .argument("<id>", "Request id")
280
- .description("Say yes — put someone's $50 plan on the company bill")
314
+ .description("Say yes — put someone's personal plan on the company bill")
281
315
  .option("--json", "Emit raw JSON")
282
316
  .action(async function (id, opts) {
283
317
  try {
@@ -292,7 +326,9 @@ export function registerBillingSponsorCommands(billing) {
292
326
  console.log(` ${chalk.cyan(result.addCardUrl)}`);
293
327
  return;
294
328
  }
295
- console.log("Approved. Their $50 plan is now on the company bill.");
329
+ console.log((await companyIsUsageOnly(result.companyUid))
330
+ ? "Approved. Their personal plan is now on the company bill."
331
+ : "Approved. Their $50 plan is now on the company bill.");
296
332
  }
297
333
  catch (err) {
298
334
  fail(err);
@@ -332,7 +368,9 @@ export function registerBillingSponsorCommands(billing) {
332
368
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
333
369
  return;
334
370
  }
335
- console.log("Ended. Their $50 plan goes back to them from the next bill.");
371
+ console.log((await companyIsUsageOnly(result.companyUid))
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.");
336
374
  }
337
375
  catch (err) {
338
376
  fail(err);
@@ -353,6 +391,11 @@ export function registerBillingSponsorCommands(billing) {
353
391
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
354
392
  return;
355
393
  }
394
+ if (isUsageOnly(await fetchPersonPricingPlan(await ensureCognitoToken()))) {
395
+ console.log("Withdrawn.");
396
+ console.log(usageOnlySponsorLines()[2]);
397
+ return;
398
+ }
356
399
  console.log("Withdrawn. You can pay the $50 yourself, reduce what you store, or ask another company.");
357
400
  }
358
401
  catch (err) {
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `hq billing usage` (usage-only-pricing US-015, rates per the 2026-10-16 card
3
+ * in US-012): this month's usage per meter, the projected bill, the included
4
+ * usage left, and the biggest drivers with a fix for each. Rates come from the
5
+ * `usage` block of `GET /v1/pricing`, or the bundled card
6
+ * (src/lib/billing/usage-card.ts) when the feed has none. The /hq-cost skill in hq-core runs this, previews a fix, and only
7
+ * applies it after the user says yes.
8
+ *
9
+ * Read-only. It never prunes, deletes, runs gc or reclaims space: the cloud
10
+ * storage split uses the same listing as `hq storage status`, which is
11
+ * read-only too.
12
+ *
13
+ * Tenant boundary: the company comes from the caller's own active memberships
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
16
+ * company (or `--personal`) it reads the caller's personal scope.
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.
20
+ */
21
+ import type { Command } from "commander";
22
+ import { type ReadMonthUsageDeps, type StorageSplit, type UsageReport, type UsageScope } from "../lib/billing/usage-cost.js";
23
+ export interface MembershipRef {
24
+ uid: string;
25
+ slug: string;
26
+ role?: string;
27
+ }
28
+ export interface BillingUsageDeps {
29
+ listCompanies(): Promise<MembershipRef[]>;
30
+ fetchUsageLimits: ReadMonthUsageDeps["fetchUsageLimits"];
31
+ readStorageSplit?: (company: MembershipRef, opts: {
32
+ fresh: boolean;
33
+ }) => Promise<StorageSplit | null>;
34
+ fetchPricing(): Promise<unknown | null>;
35
+ /** Whether the usage trial is running for the scope. Null = unknown. */
36
+ readTrialActive?: (scope: UsageScope) => Promise<boolean | null>;
37
+ now(): Date;
38
+ }
39
+ export interface BillingUsageOptions {
40
+ company?: string;
41
+ personal?: boolean;
42
+ storageScan?: boolean;
43
+ fresh?: boolean;
44
+ }
45
+ export declare class UsageScopeError extends Error {
46
+ }
47
+ /** Pick the scope. Never reaches outside the caller's own memberships. */
48
+ export declare function resolveUsageScope(memberships: MembershipRef[], opts: BillingUsageOptions): UsageScope;
49
+ export declare function runBillingUsage(opts: BillingUsageOptions, deps: BillingUsageDeps): Promise<UsageReport>;
50
+ /**
51
+ * `hq billing usage --card`: the card from GET /v1/pricing, or the bundled
52
+ * card when the feed has none. Needs no sign-in.
53
+ */
54
+ export declare function renderUsageCard(opts?: {
55
+ json?: boolean;
56
+ fetchPricing?: () => Promise<unknown | null>;
57
+ }): Promise<string>;
58
+ export declare function registerBillingUsageCommand(billing: Command): void;
59
+ //# sourceMappingURL=billing-usage.d.ts.map
@@ -0,0 +1,162 @@
1
+ /**
2
+ * `hq billing usage` (usage-only-pricing US-015, rates per the 2026-10-16 card
3
+ * in US-012): this month's usage per meter, the projected bill, the included
4
+ * usage left, and the biggest drivers with a fix for each. Rates come from the
5
+ * `usage` block of `GET /v1/pricing`, or the bundled card
6
+ * (src/lib/billing/usage-card.ts) when the feed has none. The /hq-cost skill in hq-core runs this, previews a fix, and only
7
+ * applies it after the user says yes.
8
+ *
9
+ * Read-only. It never prunes, deletes, runs gc or reclaims space: the cloud
10
+ * storage split uses the same listing as `hq storage status`, which is
11
+ * read-only too.
12
+ *
13
+ * Tenant boundary: the company comes from the caller's own active memberships
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
16
+ * company (or `--personal`) it reads the caller's personal scope.
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.
20
+ */
21
+ import chalk from "chalk";
22
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
23
+ import { vaultApiFetch, vaultApiFetchPublic } from "../utils/vault-api.js";
24
+ import { buildReport, formatReport, readMonthUsage, } from "../lib/billing/usage-cost.js";
25
+ import { ratesFromPricingStatement } from "../lib/billing/usage-rate-card.js";
26
+ import { fetchUsageCard, formatUsageCardLines } from "../lib/billing/usage-card.js";
27
+ import { fetchCompanyBillingSummary, usageTrialActiveFromBody } from "../lib/billing/usage-only.js";
28
+ export class UsageScopeError extends Error {
29
+ }
30
+ /** Pick the scope. Never reaches outside the caller's own memberships. */
31
+ export function resolveUsageScope(memberships, opts) {
32
+ if (opts.personal && opts.company) {
33
+ throw new UsageScopeError("Pass --company or --personal, not both.");
34
+ }
35
+ if (opts.personal)
36
+ return { kind: "person" };
37
+ if (opts.company) {
38
+ const match = memberships.find((m) => m.slug === opts.company || m.uid === opts.company);
39
+ if (!match) {
40
+ throw new UsageScopeError(`You are not an active member of '${opts.company}'. hq billing usage only shows your own companies.`);
41
+ }
42
+ return { kind: "company", companyUid: match.uid, companySlug: match.slug, ...(match.role ? { role: match.role } : {}) };
43
+ }
44
+ if (memberships.length === 0)
45
+ return { kind: "person" };
46
+ if (memberships.length === 1) {
47
+ const [m] = memberships;
48
+ return { kind: "company", companyUid: m.uid, companySlug: m.slug, ...(m.role ? { role: m.role } : {}) };
49
+ }
50
+ throw new UsageScopeError("You are in more than one company. Re-run with one of:\n" +
51
+ memberships.map((m) => ` --company ${m.slug}`).join("\n") +
52
+ "\n --personal");
53
+ }
54
+ export async function runBillingUsage(opts, deps) {
55
+ const memberships = await deps.listCompanies();
56
+ const scope = resolveUsageScope(memberships, opts);
57
+ const membership = scope.kind === "company" ? memberships.find((m) => m.uid === scope.companyUid) : undefined;
58
+ const [usage, pricing] = await Promise.all([
59
+ readMonthUsage(scope, {
60
+ fetchUsageLimits: deps.fetchUsageLimits,
61
+ now: deps.now,
62
+ ...(deps.readTrialActive ? { readTrialActive: deps.readTrialActive } : {}),
63
+ ...(opts.storageScan !== false && deps.readStorageSplit && membership
64
+ ? { readStorageSplit: () => deps.readStorageSplit(membership, { fresh: opts.fresh === true }) }
65
+ : {}),
66
+ }),
67
+ deps.fetchPricing().catch(() => null),
68
+ ]);
69
+ return buildReport(usage, ratesFromPricingStatement(pricing));
70
+ }
71
+ // ── Production deps ─────────────────────────────────────────────────────────
72
+ function productionDeps() {
73
+ let tokenP;
74
+ const token = () => (tokenP ??= ensureCognitoToken());
75
+ return {
76
+ async listCompanies() {
77
+ const res = await vaultApiFetch({ token: await token(), path: "/membership/me" });
78
+ if (!res.ok)
79
+ throw new Error(`Could not load your companies (HTTP ${res.status}). Run \`hq login\`.`);
80
+ const { companiesFromMemberships } = await import("./storage.js");
81
+ return companiesFromMemberships(await res.json());
82
+ },
83
+ async fetchUsageLimits(scope) {
84
+ const res = await vaultApiFetch(scope.kind === "company"
85
+ ? { token: await token(), path: "/v1/billing/usage-limits", query: { companyUid: scope.companyUid } }
86
+ : { token: await token(), path: "/v1/billing/person/usage-limits" });
87
+ if (!res.ok)
88
+ return null;
89
+ return res.json().catch(() => null);
90
+ },
91
+ async readStorageSplit(company, { fresh }) {
92
+ const storage = await import("./storage.js");
93
+ const deps = await storage.buildStorageDeps();
94
+ const hit = fresh ? undefined : deps.cloudCache?.read(company);
95
+ const status = hit ?? (await storage.getCloudStatus(company, deps));
96
+ if (!hit)
97
+ deps.cloudCache?.write(company, status);
98
+ if (!status.available)
99
+ return null;
100
+ return {
101
+ currentBytes: status.current_bytes,
102
+ oldVersionBytes: status.noncurrent_bytes,
103
+ oldVersionCount: status.noncurrent_count,
104
+ ...(status.listed_at ? { listedAt: status.listed_at } : {}),
105
+ canDelete: status.can_delete,
106
+ };
107
+ },
108
+ async fetchPricing() {
109
+ const res = await vaultApiFetchPublic({ path: "/v1/pricing" });
110
+ return res.ok ? res.json().catch(() => null) : null;
111
+ },
112
+ async readTrialActive(scope) {
113
+ // The trial is a company thing; the owner-gated summary says whether it
114
+ // runs. A member who is not an owner gets 403 and the answer is unknown.
115
+ if (scope.kind !== "company")
116
+ return null;
117
+ const body = await fetchCompanyBillingSummary(await token(), scope.companyUid);
118
+ return body === null ? null : usageTrialActiveFromBody(body);
119
+ },
120
+ now: () => new Date(),
121
+ };
122
+ }
123
+ /**
124
+ * `hq billing usage --card`: the card from GET /v1/pricing, or the bundled
125
+ * card when the feed has none. Needs no sign-in.
126
+ */
127
+ export async function renderUsageCard(opts = {}) {
128
+ const { card, source } = await fetchUsageCard(opts.fetchPricing ? { fetchPricing: opts.fetchPricing } : {});
129
+ if (opts.json)
130
+ return JSON.stringify({ source, card }, null, 2);
131
+ const from = source === "feed"
132
+ ? "from GET /v1/pricing"
133
+ : "bundled copy; the pricing feed has no usage card yet";
134
+ return [`HQ usage pricing (card ${card.version}, ${from})`, "", ...formatUsageCardLines(card)].join("\n");
135
+ }
136
+ export function registerBillingUsageCommand(billing) {
137
+ billing
138
+ .command("usage")
139
+ .description("Show this month's usage, projected bill, included usage left and what drives it (read-only)")
140
+ .option("--company <slug>", "One of your companies (default: your only company)")
141
+ .option("--personal", "Your personal HQ instead of a company")
142
+ .option("--no-storage-scan", "Skip listing cloud file versions (faster; no old-versions split)")
143
+ .option("--fresh", "List cloud file versions live instead of using figures from the last 15 minutes")
144
+ .option("--card", "Print the usage price card (meters, included usage, trial, boxes, domains) and exit")
145
+ .option("--json", "Print JSON")
146
+ .action(async function (opts) {
147
+ try {
148
+ if (opts.card) {
149
+ console.log(await renderUsageCard({ json: opts.json === true }));
150
+ return;
151
+ }
152
+ const parentCompany = this.parent?.opts().company ?? undefined;
153
+ const report = await runBillingUsage({ ...opts, company: opts.company ?? parentCompany }, productionDeps());
154
+ console.log(opts.json ? JSON.stringify(report, null, 2) : formatReport(report));
155
+ }
156
+ catch (err) {
157
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
158
+ process.exitCode = 1;
159
+ }
160
+ });
161
+ }
162
+ //# sourceMappingURL=billing-usage.js.map
@@ -11,9 +11,18 @@
11
11
  * Subcommands:
12
12
  * hq billing status [--company <slug>] — subscription + card on file
13
13
  * hq billing checkout [--company <slug>] [--personal] — mint a card-capture link
14
+ * hq billing usage [--company <slug>] [--personal] — usage, projected bill,
15
+ * drivers (read-only)
16
+ * hq billing upgrade [--company <slug>] — Workforce checkout
17
+ * (legacy plans only)
14
18
  * hq billing sponsor … — ask a company to cover
15
19
  * your $50 plan, or
16
20
  * answer such a request
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.
17
26
  */
18
27
  import { Command } from "commander";
19
28
  interface DatabaseBilling {
@@ -11,9 +11,18 @@
11
11
  * Subcommands:
12
12
  * hq billing status [--company <slug>] — subscription + card on file
13
13
  * hq billing checkout [--company <slug>] [--personal] — mint a card-capture link
14
+ * hq billing usage [--company <slug>] [--personal] — usage, projected bill,
15
+ * drivers (read-only)
16
+ * hq billing upgrade [--company <slug>] — Workforce checkout
17
+ * (legacy plans only)
14
18
  * hq billing sponsor … — ask a company to cover
15
19
  * your $50 plan, or
16
20
  * answer such a request
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.
17
26
  */
18
27
  import chalk from "chalk";
19
28
  import { ensureCognitoToken } from "../utils/cognito-session.js";
@@ -21,6 +30,9 @@ import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
21
30
  import { mintPaymentLink } from "../utils/billing-gate.js";
22
31
  import { upgradeToTeam } from "../utils/team-upgrade.js";
23
32
  import { registerBillingSponsorCommands } from "./billing-sponsor.js";
33
+ import { fetchCompanyPricingPlan, isUsageOnly, usageOnlyUpgradeLines, } from "../lib/billing/usage-only.js";
34
+ import { fetchUsageCard } from "../lib/billing/usage-card.js";
35
+ import { registerBillingUsageCommand } from "./billing-usage.js";
24
36
  const CHARGED_ON = {
25
37
  team: "the Team subscription",
26
38
  dedicated: "the separate HQ database apps subscription",
@@ -145,14 +157,28 @@ export function registerBillingCommand(program) {
145
157
  });
146
158
  billing
147
159
  .command("upgrade")
148
- .description("Open Stripe Checkout to upgrade this company to HQ Workforce ($500/mo)")
160
+ .description("Open Stripe Checkout to upgrade a company on an earlier plan (usage-pricing companies have no plan to upgrade)")
149
161
  .option("--company <slug>", "Company slug (resolves to companyUid)")
150
162
  .option("--no-browser", "Print the Stripe Checkout URL instead of opening it")
151
163
  .option("--json", "Emit the Checkout URL as JSON")
152
164
  .action(async function (opts) {
153
165
  try {
166
+ const token = await ensureCognitoToken();
167
+ const companyUid = await getCompanyUid(token, companyOf(this));
168
+ const plan = await fetchCompanyPricingPlan(token, companyUid);
169
+ if (isUsageOnly(plan)) {
170
+ const { card } = await fetchUsageCard();
171
+ const lines = usageOnlyUpgradeLines(card);
172
+ if (opts.json) {
173
+ process.stdout.write(JSON.stringify({ pricingPlan: "usage-only", url: null, message: lines.join(" ") }, null, 2) + "\n");
174
+ return;
175
+ }
176
+ for (const line of lines)
177
+ console.log(line);
178
+ return;
179
+ }
154
180
  await upgradeToTeam({
155
- company: companyOf(this),
181
+ companyUid,
156
182
  noBrowser: opts.browser === false,
157
183
  json: opts.json,
158
184
  });
@@ -162,5 +188,6 @@ export function registerBillingCommand(program) {
162
188
  }
163
189
  });
164
190
  registerBillingSponsorCommands(billing);
191
+ registerBillingUsageCommand(billing);
165
192
  }
166
193
  //# sourceMappingURL=billing.js.map
@@ -27,6 +27,14 @@ export declare const NO_BACKEND_RETRY_MS: number;
27
27
  * always releases the queue before the lock would be presumed abandoned.
28
28
  */
29
29
  export declare const SIBLING_MAX_RUNTIME_MS: number;
30
+ /**
31
+ * Queued payloads one sibling run may process, across its launch claim and its
32
+ * own queue drain. Each one runs the full upgrade flow, so a 50-deep queue in
33
+ * one run outlasts SIBLING_MAX_RUNTIME_MS and the grok turn cap; the run is
34
+ * then killed with work half done. The launcher re-queues the overflow for the
35
+ * next sibling instead.
36
+ */
37
+ export declare const MAX_QUEUED_PAYLOADS_PER_RUN = 5;
30
38
  type Backend = "claude" | "codex" | "grok" | "none";
31
39
  type SpawnableBackend = Exclude<Backend, "none">;
32
40
  /**
@@ -121,6 +129,28 @@ export declare function siblingLaunch(backend: SpawnableBackend, args: string[],
121
129
  command: string;
122
130
  args: string[];
123
131
  };
132
+ /**
133
+ * Keep the oldest {@link MAX_QUEUED_PAYLOADS_PER_RUN} claimed payloads for this
134
+ * run and append the rest back to the queue, so a deep queue is worked off
135
+ * across several runs instead of being lost when one run hits its cap. The
136
+ * claim file is rewritten to hold only what this run owns.
137
+ */
138
+ export declare function limitClaimedPayloads(entries: unknown[], claimPath: string, pendingPath: string, limit?: number): unknown[];
139
+ /** Times one queued record may be recovered from a finished run before it is dropped. */
140
+ export declare const MAX_CLAIM_RECOVERIES = 3;
141
+ /**
142
+ * Put back the queued records a finished sibling claimed but never processed.
143
+ *
144
+ * A sibling stopped at its wall-clock or turn cap leaves its claim files
145
+ * (pending-claimed*.jsonl) in its run directory. A record whose thread file
146
+ * still exists at its thread_path was not upgraded (the sibling renames the
147
+ * -auto- file when it is done), so it is appended back to pending.jsonl for
148
+ * this launch to claim. Only runs whose process is gone are read, records
149
+ * already queued are not duplicated, and each record is recovered at most
150
+ * {@link MAX_CLAIM_RECOVERIES} times so one the sibling keeps skipping cannot
151
+ * cycle forever. Each claim file read is renamed to *.recovered.
152
+ */
153
+ export declare function recoverOrphanedClaims(siblingRoot: string, pendingPath: string, liveRoot: string): number;
124
154
  /** Attach the native checkpoint command to the hidden `hq core` group. */
125
155
  export declare function registerCoreCheckpointCommand(core: Command, preResolvedHqRoot?: string): void;
126
156
  export {};