@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.
- package/CHANGELOG.md +90 -0
- package/dist/command-catalog.generated.d.ts +286 -6
- package/dist/command-catalog.generated.js +351 -6
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +1 -0
- package/dist/commands/agent-enroll.js +7 -0
- package/dist/commands/agents.d.ts +25 -3
- package/dist/commands/agents.js +57 -4
- package/dist/commands/api-keys.js +66 -8
- package/dist/commands/billing-sponsor.d.ts +4 -0
- package/dist/commands/billing-sponsor.js +48 -5
- package/dist/commands/billing-usage.d.ts +59 -0
- package/dist/commands/billing-usage.js +162 -0
- package/dist/commands/billing.d.ts +9 -0
- package/dist/commands/billing.js +29 -2
- package/dist/commands/core-checkpoint-native.d.ts +30 -0
- package/dist/commands/core-checkpoint-native.js +167 -10
- package/dist/commands/core-job-notify-native.js +53 -3
- package/dist/commands/core-jobs-native.d.ts +8 -0
- package/dist/commands/core-jobs-native.js +34 -2
- package/dist/commands/core-policy.js +76 -13
- package/dist/commands/library-add.d.ts +120 -0
- package/dist/commands/library-add.js +434 -0
- package/dist/commands/library-caption.d.ts +112 -0
- package/dist/commands/library-caption.js +393 -0
- package/dist/commands/library-link.d.ts +32 -0
- package/dist/commands/library-link.js +95 -0
- package/dist/commands/library.d.ts +72 -0
- package/dist/commands/library.js +527 -0
- package/dist/commands/meetings.d.ts +8 -0
- package/dist/commands/meetings.js +25 -2
- package/dist/commands/members.d.ts +20 -0
- package/dist/commands/members.js +51 -2
- package/dist/commands/storage-offload.d.ts +16 -2
- package/dist/commands/storage-offload.js +55 -4
- package/dist/commands/storage.d.ts +8 -1
- package/dist/commands/storage.js +6 -2
- package/dist/commands/whoami.js +9 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2410 -2063
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/billing/plan-lock.d.ts +15 -1
- package/dist/lib/billing/plan-lock.js +45 -5
- package/dist/lib/billing/usage-card.d.ts +82 -0
- package/dist/lib/billing/usage-card.js +198 -0
- package/dist/lib/billing/usage-cost.d.ts +214 -0
- package/dist/lib/billing/usage-cost.js +440 -0
- package/dist/lib/billing/usage-only.d.ts +87 -0
- package/dist/lib/billing/usage-only.js +262 -0
- package/dist/lib/billing/usage-rate-card.d.ts +51 -0
- package/dist/lib/billing/usage-rate-card.js +62 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/plan-limit-nag.d.ts +4 -6
- package/dist/lib/plan-limit-nag.js +19 -1
- package/dist/lib/provider-launcher.d.ts +22 -0
- package/dist/lib/provider-launcher.js +65 -0
- package/dist/lib/search-index/index.d.ts +2 -0
- package/dist/lib/search-index/index.js +34 -0
- package/dist/lib/search-index/max-doc-bytes.d.ts +2 -1
- package/dist/lib/search-index/max-doc-bytes.js +2 -1
- package/dist/main.d.ts +9 -0
- package/dist/main.js +20 -3
- package/dist/utils/billing-gate.d.ts +19 -4
- package/dist/utils/billing-gate.js +25 -4
- package/dist/utils/plan-gate-error.d.ts +10 -0
- package/dist/utils/plan-gate-error.js +54 -0
- package/dist/utils/vault-api.js +4 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.
|
|
1
|
+
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.82","bundleSha256":"0bf6cd65150094c035cb4ee532c00bde78d030ad0d2132aa606c566f11997f41","formatVersion":1}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* missing data (hq-absent-field-never-means-constraining-value).
|
|
22
22
|
*/
|
|
23
23
|
import { parseFreeMonth, type PlanLimitFreeMonth } from "./free-month.js";
|
|
24
|
+
import { type PricingPlan } from "./usage-only.js";
|
|
24
25
|
export { parseFreeMonth, type PlanLimitFreeMonth };
|
|
25
26
|
/**
|
|
26
27
|
* Dimensions the bind notice names with a remedy. Owner decision 7 (2026-09-17)
|
|
@@ -50,6 +51,13 @@ export interface PlanLimitStatus {
|
|
|
50
51
|
* silent rather than assume either way.
|
|
51
52
|
*/
|
|
52
53
|
plan?: "free" | "paid" | "enterprise";
|
|
54
|
+
/**
|
|
55
|
+
* Usage pricing (US-012). `"usage-only"` when hq-pro says the company is on
|
|
56
|
+
* usage pricing: Starter caps, the free month and the Workforce upgrade do
|
|
57
|
+
* not apply, and every renderer below says so instead. Absent = unknown,
|
|
58
|
+
* which keeps the legacy copy.
|
|
59
|
+
*/
|
|
60
|
+
pricingPlan?: PricingPlan;
|
|
53
61
|
checkedAt: string;
|
|
54
62
|
}
|
|
55
63
|
/** Starter member cap quoted when the server did not send a users.limit. */
|
|
@@ -85,6 +93,11 @@ export declare function selectMemberUsage(body: unknown): PlanLimitMembers | nul
|
|
|
85
93
|
*/
|
|
86
94
|
export declare function selectOverReasons(body: unknown): PlanLimitReason[];
|
|
87
95
|
export declare function selectUsages(body: unknown): Partial<Record<PlanLimitReason, PlanLimitMembers>>;
|
|
96
|
+
/**
|
|
97
|
+
* Attach the pricing plan. A usage-only company is never "over its Starter
|
|
98
|
+
* limits" and is never offered the Starter free month: it has no Starter caps.
|
|
99
|
+
*/
|
|
100
|
+
export declare function applyPricingPlan(status: PlanLimitStatus, pricingPlan: PricingPlan | null | undefined): PlanLimitStatus;
|
|
88
101
|
/**
|
|
89
102
|
* Read the live nag for one company from usage-limits. `/membership/me` is
|
|
90
103
|
* used only to resolve slug → companyUid. Returns null when the server did
|
|
@@ -121,7 +134,8 @@ export declare function renderPlanLimitNotice(status: PlanLimitStatus, opts?: {
|
|
|
121
134
|
export declare function renderPlanLimitLine(status: PlanLimitStatus): string;
|
|
122
135
|
/**
|
|
123
136
|
* The `Plan: …` orientation line. Starter only: a paid or enterprise
|
|
124
|
-
* workspace — and an UNKNOWN plan — gets no line at all.
|
|
137
|
+
* workspace — and an UNKNOWN plan — gets no line at all. A usage-pricing
|
|
138
|
+
* company gets the usage line instead.
|
|
125
139
|
*/
|
|
126
140
|
export declare function renderPlanLine(status: PlanLimitStatus): string | null;
|
|
127
141
|
/**
|
|
@@ -25,6 +25,7 @@ import { diagnosticsEnabled } from "../diagnostics.js";
|
|
|
25
25
|
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
26
26
|
import { parseFreeMonth } from "./free-month.js";
|
|
27
27
|
import { selectPlanLimitUpgradeUrl } from "./plan-limit-upgrade-url.js";
|
|
28
|
+
import { fetchCompanyPricingPlan, isUsageOnly, pricingPlanFromBody, pricingPlanFromMembership, usageOnlyPlanLine, usageOnlyUpgradeLines, } from "./usage-only.js";
|
|
28
29
|
// The free-month parser lives in a leaf module so the plan-gate 402 decoder can
|
|
29
30
|
// share it without importing the HTTP layer; re-exported so existing callers
|
|
30
31
|
// keep importing it from here.
|
|
@@ -198,7 +199,8 @@ function statusFromUsage(companySlug, companyUid, body, upgradeCompanySlug) {
|
|
|
198
199
|
const reasons = selectOverReasons(body);
|
|
199
200
|
const rec = asRecord(body);
|
|
200
201
|
const anyOverFlag = rec?.anyOver === true;
|
|
201
|
-
|
|
202
|
+
const pricingPlan = pricingPlanFromBody(body);
|
|
203
|
+
return applyPricingPlan({
|
|
202
204
|
companySlug,
|
|
203
205
|
companyUid,
|
|
204
206
|
over: anyOverFlag || reasons.length > 0,
|
|
@@ -209,6 +211,23 @@ function statusFromUsage(companySlug, companyUid, body, upgradeCompanySlug) {
|
|
|
209
211
|
freeMonth: parseFreeMonth(rec?.freeMonth),
|
|
210
212
|
plan: selectPlan(body) ?? undefined,
|
|
211
213
|
checkedAt: new Date().toISOString(),
|
|
214
|
+
}, pricingPlan);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Attach the pricing plan. A usage-only company is never "over its Starter
|
|
218
|
+
* limits" and is never offered the Starter free month: it has no Starter caps.
|
|
219
|
+
*/
|
|
220
|
+
export function applyPricingPlan(status, pricingPlan) {
|
|
221
|
+
if (!pricingPlan)
|
|
222
|
+
return status;
|
|
223
|
+
if (!isUsageOnly(pricingPlan))
|
|
224
|
+
return { ...status, pricingPlan };
|
|
225
|
+
return {
|
|
226
|
+
...status,
|
|
227
|
+
pricingPlan,
|
|
228
|
+
over: false,
|
|
229
|
+
reasons: [],
|
|
230
|
+
freeMonth: undefined,
|
|
212
231
|
};
|
|
213
232
|
}
|
|
214
233
|
function isSilentPlanLimitBodyError(error) {
|
|
@@ -235,10 +254,11 @@ export async function fetchPlanLimitStatus(token, companyRef, opts = {}) {
|
|
|
235
254
|
});
|
|
236
255
|
if (!res.ok)
|
|
237
256
|
return null;
|
|
238
|
-
const
|
|
257
|
+
const membershipBody = await res.json().catch((error) => {
|
|
239
258
|
notePlanLimitBodyUnreadable(error);
|
|
240
259
|
return null;
|
|
241
|
-
})
|
|
260
|
+
});
|
|
261
|
+
const selected = selectCompanyMembership(membershipBody, companyRef);
|
|
242
262
|
if (!selected?.companyUid)
|
|
243
263
|
return null;
|
|
244
264
|
try {
|
|
@@ -258,7 +278,18 @@ export async function fetchPlanLimitStatus(token, companyRef, opts = {}) {
|
|
|
258
278
|
return null;
|
|
259
279
|
const selectedSlug = selected.companySlug ??
|
|
260
280
|
(!companyRef.startsWith("cmp_") ? companyRef : undefined);
|
|
261
|
-
|
|
281
|
+
const status = statusFromUsage(companyRef, selected.companyUid, body, selectedSlug);
|
|
282
|
+
if (status.pricingPlan)
|
|
283
|
+
return status;
|
|
284
|
+
// Older hq-pro: usage-limits has no pricingPlan. Try the membership row,
|
|
285
|
+
// then the (owner-gated) billing summary. Unknown keeps the legacy copy.
|
|
286
|
+
const fromRow = pricingPlanFromMembership(membershipBody, selected.companyUid);
|
|
287
|
+
if (fromRow)
|
|
288
|
+
return applyPricingPlan(status, fromRow);
|
|
289
|
+
const pricingPlan = await fetchCompanyPricingPlan(token, selected.companyUid, {
|
|
290
|
+
timeoutMs,
|
|
291
|
+
}).catch(() => null);
|
|
292
|
+
return applyPricingPlan(status, pricingPlan);
|
|
262
293
|
}
|
|
263
294
|
catch {
|
|
264
295
|
return null;
|
|
@@ -380,6 +411,9 @@ function capitalize(text) {
|
|
|
380
411
|
export function renderPlanLimitNotice(status, opts = {}) {
|
|
381
412
|
const { companySlug, freeMonth } = status;
|
|
382
413
|
const lines = [];
|
|
414
|
+
if (isUsageOnly(status.pricingPlan)) {
|
|
415
|
+
return [`${companySlug}:`, ...usageOnlyUpgradeLines().map((l) => ` ${l}`)].join("\n");
|
|
416
|
+
}
|
|
383
417
|
lines.push(`HQ Starter — ${companySlug} is over its plan limits.`);
|
|
384
418
|
const reasons = status.reasons.length
|
|
385
419
|
? status.reasons.map(reasonLabel).join("; ")
|
|
@@ -420,6 +454,9 @@ export function renderPlanLimitNotice(status, opts = {}) {
|
|
|
420
454
|
* Kept to a single line on purpose — it repeats each turn.
|
|
421
455
|
*/
|
|
422
456
|
export function renderPlanLimitLine(status) {
|
|
457
|
+
if (isUsageOnly(status.pricingPlan)) {
|
|
458
|
+
return `Company ${status.companySlug} is on usage pricing. ${usageOnlyUpgradeLines()[2]}`;
|
|
459
|
+
}
|
|
423
460
|
const detail = status.reasons.length
|
|
424
461
|
? status.reasons.map((reason) => reasonDetail(reason, status)).join("; ")
|
|
425
462
|
: "over the Starter plan limits";
|
|
@@ -428,9 +465,12 @@ export function renderPlanLimitLine(status) {
|
|
|
428
465
|
}
|
|
429
466
|
/**
|
|
430
467
|
* The `Plan: …` orientation line. Starter only: a paid or enterprise
|
|
431
|
-
* workspace — and an UNKNOWN plan — gets no line at all.
|
|
468
|
+
* workspace — and an UNKNOWN plan — gets no line at all. A usage-pricing
|
|
469
|
+
* company gets the usage line instead.
|
|
432
470
|
*/
|
|
433
471
|
export function renderPlanLine(status) {
|
|
472
|
+
if (isUsageOnly(status.pricingPlan))
|
|
473
|
+
return usageOnlyPlanLine();
|
|
434
474
|
if (status.over)
|
|
435
475
|
return "Plan: Starter — over its limits";
|
|
436
476
|
if (status.plan !== "free")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The usage pricing card (usage-only-pricing landing 2026-10-16, US-012).
|
|
3
|
+
*
|
|
4
|
+
* hq-pro owns the numbers and publishes them in the `usage` block of
|
|
5
|
+
* `GET /v1/pricing`. Every CLI surface that quotes a usage price (meters, the
|
|
6
|
+
* included amount, the trial, box sizes, domains) reads the card through this
|
|
7
|
+
* module and nowhere else:
|
|
8
|
+
*
|
|
9
|
+
* - {@link fetchUsageCard} reads the feed and falls back to the bundled card
|
|
10
|
+
* when the feed is offline, slow, or has no `usage` block yet.
|
|
11
|
+
* - {@link BUNDLED_USAGE_CARD} is the fallback. It equals the feed contract in
|
|
12
|
+
* companies/indigo/projects/usage-only-pricing/landing-2026-10-16/README.md.
|
|
13
|
+
*
|
|
14
|
+
* Field-level fallback: a feed that sends a `usage` block with one malformed
|
|
15
|
+
* field keeps the feed's other fields and takes the bundled value for that one
|
|
16
|
+
* field. Unknown meter ids are kept (they render with their own label and
|
|
17
|
+
* price); malformed meter or box rows are dropped.
|
|
18
|
+
*/
|
|
19
|
+
export interface UsageCardMeter {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly label: string;
|
|
22
|
+
readonly unit: string;
|
|
23
|
+
readonly unitUsd: number;
|
|
24
|
+
/** True when the meter is not billed while a trial runs (the vault fee). */
|
|
25
|
+
readonly waivedInTrial?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface UsageCardBox {
|
|
28
|
+
readonly size: string;
|
|
29
|
+
readonly instanceType: string;
|
|
30
|
+
readonly monthlyUsd: number;
|
|
31
|
+
}
|
|
32
|
+
export interface UsageCard {
|
|
33
|
+
readonly version: string;
|
|
34
|
+
readonly newSignupsFrom: string;
|
|
35
|
+
readonly includedUsd: number;
|
|
36
|
+
readonly trial: {
|
|
37
|
+
readonly days: number;
|
|
38
|
+
readonly capUsd: number;
|
|
39
|
+
};
|
|
40
|
+
readonly meters: readonly UsageCardMeter[];
|
|
41
|
+
readonly boxes: readonly UsageCardBox[];
|
|
42
|
+
readonly free: readonly string[];
|
|
43
|
+
readonly domains: {
|
|
44
|
+
readonly standardUsd: number;
|
|
45
|
+
readonly premiumUsd: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export type UsageCardSource = "feed" | "bundled";
|
|
49
|
+
export interface ResolvedUsageCard {
|
|
50
|
+
card: UsageCard;
|
|
51
|
+
source: UsageCardSource;
|
|
52
|
+
}
|
|
53
|
+
/** The feed contract, verbatim. Used whenever the feed has no usable `usage` block. */
|
|
54
|
+
export declare const BUNDLED_USAGE_CARD: UsageCard;
|
|
55
|
+
/** The self-hosted / on-prem guide the card links for teams that want no cloud bill. */
|
|
56
|
+
export declare const SELF_HOSTED_GUIDE_URL = "https://docs.hq.computer/hq/guide/use-hq-for-free/";
|
|
57
|
+
/**
|
|
58
|
+
* Parse the `usage` block of a `GET /v1/pricing` body. Returns null when the
|
|
59
|
+
* body has no `usage` object at all (the feed before hq-pro ships it).
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseUsageCard(body: unknown): UsageCard | null;
|
|
62
|
+
/** The card from a feed body, or the bundled card when the body has none. */
|
|
63
|
+
export declare function usageCardFromPricingFeed(body: unknown): ResolvedUsageCard;
|
|
64
|
+
export interface FetchUsageCardDeps {
|
|
65
|
+
/** GET /v1/pricing. Resolves the decoded body, or null when it did not answer. */
|
|
66
|
+
fetchPricing?: () => Promise<unknown | null>;
|
|
67
|
+
timeoutMs?: number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Read the card from the feed. Never throws: an offline feed, a non-2xx, or a
|
|
71
|
+
* body without `usage` all resolve to the bundled card.
|
|
72
|
+
*/
|
|
73
|
+
export declare function fetchUsageCard(deps?: FetchUsageCardDeps): Promise<ResolvedUsageCard>;
|
|
74
|
+
/** `$0.10`, `$2.00`, `$100`: cents only when the amount has them or is under $10. */
|
|
75
|
+
export declare function formatCardUsd(amount: number): string;
|
|
76
|
+
/** "small (t3.small) $100/month, medium (t3.medium) $200/month, ..." */
|
|
77
|
+
export declare function formatBoxQuotes(card?: UsageCard): string;
|
|
78
|
+
/** "deploys, DMs and background AI" */
|
|
79
|
+
export declare function formatFreeItems(card?: UsageCard): string;
|
|
80
|
+
/** The whole card as plain lines, for `hq billing usage --card` and help text. */
|
|
81
|
+
export declare function formatUsageCardLines(card?: UsageCard): string[];
|
|
82
|
+
//# sourceMappingURL=usage-card.d.ts.map
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The usage pricing card (usage-only-pricing landing 2026-10-16, US-012).
|
|
3
|
+
*
|
|
4
|
+
* hq-pro owns the numbers and publishes them in the `usage` block of
|
|
5
|
+
* `GET /v1/pricing`. Every CLI surface that quotes a usage price (meters, the
|
|
6
|
+
* included amount, the trial, box sizes, domains) reads the card through this
|
|
7
|
+
* module and nowhere else:
|
|
8
|
+
*
|
|
9
|
+
* - {@link fetchUsageCard} reads the feed and falls back to the bundled card
|
|
10
|
+
* when the feed is offline, slow, or has no `usage` block yet.
|
|
11
|
+
* - {@link BUNDLED_USAGE_CARD} is the fallback. It equals the feed contract in
|
|
12
|
+
* companies/indigo/projects/usage-only-pricing/landing-2026-10-16/README.md.
|
|
13
|
+
*
|
|
14
|
+
* Field-level fallback: a feed that sends a `usage` block with one malformed
|
|
15
|
+
* field keeps the feed's other fields and takes the bundled value for that one
|
|
16
|
+
* field. Unknown meter ids are kept (they render with their own label and
|
|
17
|
+
* price); malformed meter or box rows are dropped.
|
|
18
|
+
*/
|
|
19
|
+
import { vaultApiFetchPublic } from "../../utils/vault-api.js";
|
|
20
|
+
/** The feed contract, verbatim. Used whenever the feed has no usable `usage` block. */
|
|
21
|
+
export const BUNDLED_USAGE_CARD = {
|
|
22
|
+
version: "2026-10-16",
|
|
23
|
+
newSignupsFrom: "2026-10-16T18:00:00Z",
|
|
24
|
+
includedUsd: 10,
|
|
25
|
+
trial: { days: 30, capUsd: 500 },
|
|
26
|
+
meters: [
|
|
27
|
+
{ id: "member-sync", label: "Sync", unit: "sync", unitUsd: 0.1 },
|
|
28
|
+
{ id: "integration-call", label: "Gateway call", unit: "call", unitUsd: 0.05 },
|
|
29
|
+
{ id: "hq-mcp-call", label: "HQ MCP call", unit: "call", unitUsd: 0.05 },
|
|
30
|
+
{ id: "agent-run", label: "Agent run", unit: "run", unitUsd: 0.05 },
|
|
31
|
+
{ id: "storage", label: "Storage", unit: "GB-month", unitUsd: 2 },
|
|
32
|
+
{ id: "secret", label: "Secret", unit: "secret-month", unitUsd: 0.5 },
|
|
33
|
+
{ id: "vault", label: "Vault", unit: "vault-month", unitUsd: 5, waivedInTrial: true },
|
|
34
|
+
],
|
|
35
|
+
boxes: [
|
|
36
|
+
{ size: "small", instanceType: "t3.small", monthlyUsd: 100 },
|
|
37
|
+
{ size: "medium", instanceType: "t3.medium", monthlyUsd: 200 },
|
|
38
|
+
{ size: "large", instanceType: "t3.large", monthlyUsd: 400 },
|
|
39
|
+
{ size: "xlarge", instanceType: "t3.xlarge", monthlyUsd: 800 },
|
|
40
|
+
],
|
|
41
|
+
free: ["deploys", "dms", "background-ai"],
|
|
42
|
+
domains: { standardUsd: 20, premiumUsd: 500 },
|
|
43
|
+
};
|
|
44
|
+
/** The self-hosted / on-prem guide the card links for teams that want no cloud bill. */
|
|
45
|
+
export const SELF_HOSTED_GUIDE_URL = "https://docs.hq.computer/hq/guide/use-hq-for-free/";
|
|
46
|
+
function asRecord(value) {
|
|
47
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
48
|
+
? value
|
|
49
|
+
: null;
|
|
50
|
+
}
|
|
51
|
+
function nonNegative(value) {
|
|
52
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
53
|
+
}
|
|
54
|
+
function nonEmpty(value) {
|
|
55
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
56
|
+
}
|
|
57
|
+
function parseMeter(raw) {
|
|
58
|
+
const rec = asRecord(raw);
|
|
59
|
+
if (!rec)
|
|
60
|
+
return null;
|
|
61
|
+
const id = nonEmpty(rec.id);
|
|
62
|
+
const label = nonEmpty(rec.label);
|
|
63
|
+
const unit = nonEmpty(rec.unit);
|
|
64
|
+
const unitUsd = nonNegative(rec.unitUsd);
|
|
65
|
+
if (!id || !label || !unit || unitUsd === null)
|
|
66
|
+
return null;
|
|
67
|
+
return { id, label, unit, unitUsd, ...(rec.waivedInTrial === true ? { waivedInTrial: true } : {}) };
|
|
68
|
+
}
|
|
69
|
+
function parseBox(raw) {
|
|
70
|
+
const rec = asRecord(raw);
|
|
71
|
+
if (!rec)
|
|
72
|
+
return null;
|
|
73
|
+
const size = nonEmpty(rec.size);
|
|
74
|
+
const instanceType = nonEmpty(rec.instanceType);
|
|
75
|
+
const monthlyUsd = nonNegative(rec.monthlyUsd);
|
|
76
|
+
if (!size || !instanceType || monthlyUsd === null)
|
|
77
|
+
return null;
|
|
78
|
+
return { size, instanceType, monthlyUsd };
|
|
79
|
+
}
|
|
80
|
+
function rows(raw, parse) {
|
|
81
|
+
if (!Array.isArray(raw))
|
|
82
|
+
return null;
|
|
83
|
+
const out = raw.map(parse).filter((row) => row !== null);
|
|
84
|
+
return out.length > 0 ? out : null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse the `usage` block of a `GET /v1/pricing` body. Returns null when the
|
|
88
|
+
* body has no `usage` object at all (the feed before hq-pro ships it).
|
|
89
|
+
*/
|
|
90
|
+
export function parseUsageCard(body) {
|
|
91
|
+
const usage = asRecord(asRecord(body)?.usage);
|
|
92
|
+
if (!usage)
|
|
93
|
+
return null;
|
|
94
|
+
const bundled = BUNDLED_USAGE_CARD;
|
|
95
|
+
const trial = asRecord(usage.trial);
|
|
96
|
+
const trialDays = nonNegative(trial?.days);
|
|
97
|
+
const trialCap = nonNegative(trial?.capUsd);
|
|
98
|
+
const domains = asRecord(usage.domains);
|
|
99
|
+
const standardUsd = nonNegative(domains?.standardUsd);
|
|
100
|
+
const premiumUsd = nonNegative(domains?.premiumUsd);
|
|
101
|
+
const free = Array.isArray(usage.free)
|
|
102
|
+
? usage.free.filter((v) => typeof v === "string" && v.trim().length > 0)
|
|
103
|
+
: null;
|
|
104
|
+
return {
|
|
105
|
+
version: nonEmpty(usage.version) ?? bundled.version,
|
|
106
|
+
newSignupsFrom: nonEmpty(usage.newSignupsFrom) ?? bundled.newSignupsFrom,
|
|
107
|
+
includedUsd: nonNegative(usage.includedUsd) ?? bundled.includedUsd,
|
|
108
|
+
trial: {
|
|
109
|
+
days: trialDays ?? bundled.trial.days,
|
|
110
|
+
capUsd: trialCap ?? bundled.trial.capUsd,
|
|
111
|
+
},
|
|
112
|
+
meters: rows(usage.meters, parseMeter) ?? bundled.meters,
|
|
113
|
+
boxes: rows(usage.boxes, parseBox) ?? bundled.boxes,
|
|
114
|
+
free: free && free.length > 0 ? free : bundled.free,
|
|
115
|
+
domains: {
|
|
116
|
+
standardUsd: standardUsd ?? bundled.domains.standardUsd,
|
|
117
|
+
premiumUsd: premiumUsd ?? bundled.domains.premiumUsd,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** The card from a feed body, or the bundled card when the body has none. */
|
|
122
|
+
export function usageCardFromPricingFeed(body) {
|
|
123
|
+
const card = parseUsageCard(body);
|
|
124
|
+
return card ? { card, source: "feed" } : { card: BUNDLED_USAGE_CARD, source: "bundled" };
|
|
125
|
+
}
|
|
126
|
+
async function defaultFetchPricing(timeoutMs) {
|
|
127
|
+
let timer;
|
|
128
|
+
const timeout = new Promise((resolve) => {
|
|
129
|
+
timer = setTimeout(() => resolve(null), timeoutMs);
|
|
130
|
+
timer.unref?.();
|
|
131
|
+
});
|
|
132
|
+
const read = (async () => {
|
|
133
|
+
const res = await vaultApiFetchPublic({ path: "/v1/pricing" });
|
|
134
|
+
if (!res.ok)
|
|
135
|
+
return null;
|
|
136
|
+
return res.json().catch(() => null);
|
|
137
|
+
})();
|
|
138
|
+
try {
|
|
139
|
+
return await Promise.race([read, timeout]);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
if (timer)
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Read the card from the feed. Never throws: an offline feed, a non-2xx, or a
|
|
148
|
+
* body without `usage` all resolve to the bundled card.
|
|
149
|
+
*/
|
|
150
|
+
export async function fetchUsageCard(deps = {}) {
|
|
151
|
+
const timeoutMs = deps.timeoutMs ?? 5000;
|
|
152
|
+
try {
|
|
153
|
+
const body = await (deps.fetchPricing ? deps.fetchPricing() : defaultFetchPricing(timeoutMs));
|
|
154
|
+
return usageCardFromPricingFeed(body);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return { card: BUNDLED_USAGE_CARD, source: "bundled" };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ── Rendering ────────────────────────────────────────────────────────────────
|
|
161
|
+
/** `$0.10`, `$2.00`, `$100`: cents only when the amount has them or is under $10. */
|
|
162
|
+
export function formatCardUsd(amount) {
|
|
163
|
+
return Number.isInteger(amount) && amount >= 10 ? `$${amount}` : `$${amount.toFixed(2)}`;
|
|
164
|
+
}
|
|
165
|
+
/** "small (t3.small) $100/month, medium (t3.medium) $200/month, ..." */
|
|
166
|
+
export function formatBoxQuotes(card = BUNDLED_USAGE_CARD) {
|
|
167
|
+
return card.boxes
|
|
168
|
+
.map((b) => `${b.size} (${b.instanceType}) ${formatCardUsd(b.monthlyUsd)}/month`)
|
|
169
|
+
.join(", ");
|
|
170
|
+
}
|
|
171
|
+
const FREE_LABELS = {
|
|
172
|
+
deploys: "deploys",
|
|
173
|
+
dms: "DMs",
|
|
174
|
+
"background-ai": "background AI",
|
|
175
|
+
};
|
|
176
|
+
/** "deploys, DMs and background AI" */
|
|
177
|
+
export function formatFreeItems(card = BUNDLED_USAGE_CARD) {
|
|
178
|
+
const items = card.free.map((f) => FREE_LABELS[f] ?? f);
|
|
179
|
+
if (items.length <= 1)
|
|
180
|
+
return items.join("");
|
|
181
|
+
return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
182
|
+
}
|
|
183
|
+
/** The whole card as plain lines, for `hq billing usage --card` and help text. */
|
|
184
|
+
export function formatUsageCardLines(card = BUNDLED_USAGE_CARD) {
|
|
185
|
+
const lines = [];
|
|
186
|
+
const nameW = Math.max(...card.meters.map((m) => m.label.length));
|
|
187
|
+
for (const m of card.meters) {
|
|
188
|
+
const waived = m.waivedInTrial ? " (waived during the trial)" : "";
|
|
189
|
+
lines.push(` ${m.label.padEnd(nameW)} ${formatCardUsd(m.unitUsd)} per ${m.unit}${waived}`);
|
|
190
|
+
}
|
|
191
|
+
lines.push(`Included usage: ${formatCardUsd(card.includedUsd)} per month.`);
|
|
192
|
+
lines.push(`Trial: ${card.trial.days} days, capped at ${formatCardUsd(card.trial.capUsd)} of usage.`);
|
|
193
|
+
lines.push(`Boxes: ${formatBoxQuotes(card)}.`);
|
|
194
|
+
lines.push(`Free: ${formatFreeItems(card)}.`);
|
|
195
|
+
lines.push(`Domains: ${formatCardUsd(card.domains.standardUsd)} standard, ${formatCardUsd(card.domains.premiumUsd)} premium.`);
|
|
196
|
+
return lines;
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=usage-card.js.map
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq billing usage` core (usage-only-pricing US-015): this month's usage per
|
|
3
|
+
* meter, the projected bill, the allowance left, and the biggest drivers with
|
|
4
|
+
* a fix for each. Pure apart from the injected reads, so it is unit-tested
|
|
5
|
+
* without a network.
|
|
6
|
+
*
|
|
7
|
+
* ONE READ SEAM. Every usage number comes through {@link readMonthUsage}. Today
|
|
8
|
+
* it reads the sources the console and CLI already expose:
|
|
9
|
+
* - `GET /v1/billing/usage-limits?companyUid=` (company) or
|
|
10
|
+
* `GET /v1/billing/person/usage-limits` (solo, person scope) for storage
|
|
11
|
+
* bytes and secrets stored. Both routes are member-gated server-side, so a
|
|
12
|
+
* member only ever gets their own company's numbers.
|
|
13
|
+
* - optionally, the live cloud file-history listing `hq storage status`
|
|
14
|
+
* uses, to split storage into current files and old versions and to see a
|
|
15
|
+
* prune right away (Storage Lens, behind usage-limits, updates daily).
|
|
16
|
+
* usage-limits also answers `vaults` when the server counts them; without it
|
|
17
|
+
* the vault meter is `not-available`.
|
|
18
|
+
* The call-count meters (syncs, gateway calls, HQ MCP calls, agent runs) have
|
|
19
|
+
* no member-visible source yet. They are reported
|
|
20
|
+
* as `not-available`, never as zero, and the bill is marked incomplete.
|
|
21
|
+
*
|
|
22
|
+
* When the daily rollup (US-005, hq-pro `usage-meter-rollup.ts`) gets a read
|
|
23
|
+
* route, add it as a {@link MonthUsageSource} and make it the default here.
|
|
24
|
+
* Nothing else in this file or in the command has to change.
|
|
25
|
+
*
|
|
26
|
+
* Read-only: nothing here prunes, deletes, collects garbage or reclaims space.
|
|
27
|
+
* Fixes are returned as commands for the caller to preview and run with the
|
|
28
|
+
* user's yes (the /hq-cost skill does that).
|
|
29
|
+
*/
|
|
30
|
+
import { type UsageCard } from "./usage-card.js";
|
|
31
|
+
import { type UsageMeterKey, type UsageMeterRate } from "./usage-rate-card.js";
|
|
32
|
+
/** Bytes per billed GB. Storage is billed like S3: 1 GB = 2^30 bytes. */
|
|
33
|
+
export declare const BYTES_PER_GB: number;
|
|
34
|
+
export type UsageScope = {
|
|
35
|
+
kind: "company";
|
|
36
|
+
companyUid: string;
|
|
37
|
+
companySlug: string;
|
|
38
|
+
role?: string;
|
|
39
|
+
} | {
|
|
40
|
+
kind: "person";
|
|
41
|
+
};
|
|
42
|
+
export type MeterState = "measured" | "unknown" | "not-available";
|
|
43
|
+
export interface MeterReading {
|
|
44
|
+
key: UsageMeterKey;
|
|
45
|
+
/** In billed units (GB for storage). Null unless measured. */
|
|
46
|
+
value: number | null;
|
|
47
|
+
state: MeterState;
|
|
48
|
+
}
|
|
49
|
+
export interface StorageSplit {
|
|
50
|
+
/** Bytes in current files. */
|
|
51
|
+
currentBytes: number;
|
|
52
|
+
/** Bytes in old (noncurrent) versions. */
|
|
53
|
+
oldVersionBytes: number;
|
|
54
|
+
oldVersionCount: number;
|
|
55
|
+
/** When the listing was taken (ISO). */
|
|
56
|
+
listedAt?: string;
|
|
57
|
+
/** Whether the caller may prune old versions (owner or admin). */
|
|
58
|
+
canDelete: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface MonthUsage {
|
|
61
|
+
scope: UsageScope;
|
|
62
|
+
/** Which source answered; lets a reader tell interim numbers from the rollup. */
|
|
63
|
+
source: "usage-limits" | "daily-rollup";
|
|
64
|
+
/** Month-to-date window, UTC. */
|
|
65
|
+
period: {
|
|
66
|
+
start: string;
|
|
67
|
+
end: string;
|
|
68
|
+
asOf: string;
|
|
69
|
+
};
|
|
70
|
+
meters: MeterReading[];
|
|
71
|
+
storage?: StorageSplit;
|
|
72
|
+
/** True when the account's usage trial is running (meters waived in trial bill $0). Absent = unknown. */
|
|
73
|
+
trialActive?: boolean;
|
|
74
|
+
}
|
|
75
|
+
/** One way to read a month of usage. The rollup becomes a second implementation. */
|
|
76
|
+
export interface MonthUsageSource {
|
|
77
|
+
name: MonthUsage["source"];
|
|
78
|
+
read(scope: UsageScope, now: Date): Promise<Omit<MonthUsage, "storage">>;
|
|
79
|
+
}
|
|
80
|
+
export interface ReadMonthUsageDeps {
|
|
81
|
+
/** GET a usage-limits body for the scope. Null when the server did not answer. */
|
|
82
|
+
fetchUsageLimits(scope: UsageScope): Promise<unknown | null>;
|
|
83
|
+
/** Live cloud file-history split for a company. Absent or null skips the split. */
|
|
84
|
+
readStorageSplit?: (scope: Extract<UsageScope, {
|
|
85
|
+
kind: "company";
|
|
86
|
+
}>) => Promise<StorageSplit | null>;
|
|
87
|
+
/** Whether the usage trial is running. Null or absent = unknown. */
|
|
88
|
+
readTrialActive?: (scope: UsageScope) => Promise<boolean | null>;
|
|
89
|
+
now(): Date;
|
|
90
|
+
}
|
|
91
|
+
export declare function monthWindow(now: Date): {
|
|
92
|
+
start: Date;
|
|
93
|
+
end: Date;
|
|
94
|
+
};
|
|
95
|
+
/** Today's source: plan usage-limits (storage bytes, secrets). */
|
|
96
|
+
export declare function usageLimitsSource(deps: Pick<ReadMonthUsageDeps, "fetchUsageLimits">): MonthUsageSource;
|
|
97
|
+
/**
|
|
98
|
+
* One day of the US-005 rollup (`UsageDailyRow` in hq-pro
|
|
99
|
+
* `src/billing/usage-meter-rollup.ts`, read with `readCompanyRange`). Only the
|
|
100
|
+
* fields this reader needs. An absent meter key means unknown, never zero.
|
|
101
|
+
*/
|
|
102
|
+
export interface UsageDailyRowLike {
|
|
103
|
+
day: string;
|
|
104
|
+
meters: Partial<Record<RollupMeter, number>>;
|
|
105
|
+
unknownMeters?: string[];
|
|
106
|
+
}
|
|
107
|
+
type RollupMeter = "storage_bytes" | "secrets_stored" | "hq_mcp_calls" | "integration_calls" | "agent_runs" | "member_syncs" | "vaults";
|
|
108
|
+
/**
|
|
109
|
+
* Turn a month of daily rollup rows into meter readings: counters are summed
|
|
110
|
+
* across days, storage and secrets (daily levels) are averaged over the days
|
|
111
|
+
* that have a value. A meter with no value on any day is `not-available`,
|
|
112
|
+
* never zero. External MCP calls through HQ are counted as gateway calls
|
|
113
|
+
* (`integration_calls`).
|
|
114
|
+
*/
|
|
115
|
+
export declare function meterReadingsFromDailyRows(rows: readonly UsageDailyRowLike[]): MeterReading[];
|
|
116
|
+
/**
|
|
117
|
+
* The rollup as a source. `fetchRows` is the US-006 company-usage route (or
|
|
118
|
+
* `readCompanyRange` server-side) for the month to date. Pass this to
|
|
119
|
+
* {@link readMonthUsage} once that route ships; the company scope only.
|
|
120
|
+
*/
|
|
121
|
+
export declare function dailyRollupSource(fetchRows: (scope: UsageScope, from: string, to: string) => Promise<readonly UsageDailyRowLike[] | null>): MonthUsageSource;
|
|
122
|
+
/**
|
|
123
|
+
* The one function every caller uses to read a month of usage. Switch the
|
|
124
|
+
* default source here when the US-005 rollup has a read route.
|
|
125
|
+
*/
|
|
126
|
+
export declare function readMonthUsage(scope: UsageScope, deps: ReadMonthUsageDeps, source?: MonthUsageSource): Promise<MonthUsage>;
|
|
127
|
+
export interface MeterCost {
|
|
128
|
+
key: UsageMeterKey;
|
|
129
|
+
displayName: string;
|
|
130
|
+
unit: string;
|
|
131
|
+
unitUsd: number;
|
|
132
|
+
value: number | null;
|
|
133
|
+
state: MeterState;
|
|
134
|
+
/** Cost so far this month (gauges prorated by days elapsed). Null unless measured. */
|
|
135
|
+
monthToDateUsd: number | null;
|
|
136
|
+
/** Cost if the month ends at this pace (gauges at today's amount). Null unless measured. */
|
|
137
|
+
projectedUsd: number | null;
|
|
138
|
+
/** Not billed because the trial is running (vault fee). */
|
|
139
|
+
waivedInTrial?: boolean;
|
|
140
|
+
}
|
|
141
|
+
export interface UsageBill {
|
|
142
|
+
meters: MeterCost[];
|
|
143
|
+
monthToDateUsd: number;
|
|
144
|
+
projectedUsd: number;
|
|
145
|
+
allowanceUsd: number;
|
|
146
|
+
allowanceLeftUsd: number;
|
|
147
|
+
/** What would be invoiced at month end: projected cost above the allowance. */
|
|
148
|
+
projectedBillUsd: number;
|
|
149
|
+
/** True when any meter is not measured: the real bill can only be higher. */
|
|
150
|
+
incomplete: boolean;
|
|
151
|
+
daysElapsed: number;
|
|
152
|
+
daysInMonth: number;
|
|
153
|
+
}
|
|
154
|
+
export declare function computeBill(usage: MonthUsage, rates?: readonly UsageMeterRate[], allowanceUsd?: number): UsageBill;
|
|
155
|
+
export type FixId = "prune-old-versions" | "remove-unused-secrets" | "exclude-folders" | "turn-sync-off";
|
|
156
|
+
/**
|
|
157
|
+
* A fix is a pair of command lists. `preview` only reads (dry runs, listings);
|
|
158
|
+
* `apply` changes something and must only run after the user has seen the
|
|
159
|
+
* preview and said yes. This module never runs either.
|
|
160
|
+
*/
|
|
161
|
+
export interface Fix {
|
|
162
|
+
id: FixId;
|
|
163
|
+
title: string;
|
|
164
|
+
/** Plain words: what changes and what does not. */
|
|
165
|
+
detail: string;
|
|
166
|
+
preview: string[];
|
|
167
|
+
apply: string[];
|
|
168
|
+
/** Monthly saving if known, in USD. */
|
|
169
|
+
savesUsdPerMonth: number | null;
|
|
170
|
+
/** Set when the caller cannot apply this fix themselves. */
|
|
171
|
+
blockedReason?: string;
|
|
172
|
+
}
|
|
173
|
+
export interface Driver {
|
|
174
|
+
meter: UsageMeterKey | "storage-old-versions" | "storage-current";
|
|
175
|
+
/** One plain sentence naming what is costing money. */
|
|
176
|
+
summary: string;
|
|
177
|
+
projectedUsd: number;
|
|
178
|
+
fixes: Fix[];
|
|
179
|
+
}
|
|
180
|
+
/** ISO date `days` before `now`, for prune cutoffs. */
|
|
181
|
+
export declare function daysBefore(now: Date, days: number): string;
|
|
182
|
+
export declare function excludeFoldersFix(scope: UsageScope): Fix;
|
|
183
|
+
export declare function turnSyncOffFix(): Fix;
|
|
184
|
+
/**
|
|
185
|
+
* Biggest drivers first. Only measured meters with a cost become drivers; the
|
|
186
|
+
* turn-sync-off fix is always offered while a bill is projected.
|
|
187
|
+
*/
|
|
188
|
+
export declare function findDrivers(usage: MonthUsage, bill: UsageBill): Driver[];
|
|
189
|
+
/** The self-hosted / no-sync guide the usage card links (feed `signup.selfHosted.url`). */
|
|
190
|
+
export declare const NO_SYNC_GUIDE_URL = "https://docs.hq.computer/hq/guide/use-hq-for-free/";
|
|
191
|
+
export interface UsageReport {
|
|
192
|
+
scope: UsageScope;
|
|
193
|
+
source: MonthUsage["source"];
|
|
194
|
+
rateSource: "published" | "bundled";
|
|
195
|
+
period: MonthUsage["period"];
|
|
196
|
+
bill: UsageBill;
|
|
197
|
+
storage?: StorageSplit;
|
|
198
|
+
/** Whether the usage trial is running. Absent = unknown. */
|
|
199
|
+
trialActive?: boolean;
|
|
200
|
+
/** What the card lists as free, in words. */
|
|
201
|
+
freeItems: string;
|
|
202
|
+
drivers: Driver[];
|
|
203
|
+
noSyncGuideUrl: string;
|
|
204
|
+
readOnly: true;
|
|
205
|
+
}
|
|
206
|
+
export declare function buildReport(usage: MonthUsage, rates: {
|
|
207
|
+
rates: readonly UsageMeterRate[];
|
|
208
|
+
allowanceUsd: number;
|
|
209
|
+
source: "published" | "bundled";
|
|
210
|
+
card?: UsageCard;
|
|
211
|
+
}): UsageReport;
|
|
212
|
+
export declare function formatReport(r: UsageReport): string;
|
|
213
|
+
export {};
|
|
214
|
+
//# sourceMappingURL=usage-cost.d.ts.map
|