@indigoai-us/hq-cli 5.345.143 → 5.345.144
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 +71 -0
- package/dist/command-catalog.generated.d.ts +278 -6
- package/dist/command-catalog.generated.js +341 -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-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/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/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
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* writes to stdout. Env off-switch: `HQ_NO_PLAN_LIMIT_NAG=1` silences all
|
|
33
33
|
* three surfaces, the threshold notice included.
|
|
34
34
|
*/
|
|
35
|
+
import { type PricingPlan } from "./billing/usage-only.js";
|
|
35
36
|
export declare const PLAN_LIMIT_UPGRADE_URL = "https://hq.computer/billing/upgrade";
|
|
36
37
|
/**
|
|
37
38
|
* Cadence of the over-limit per-turn line (US-036). Was once a day; the nag
|
|
@@ -67,11 +68,6 @@ export interface PlanLimitThresholdCrossing {
|
|
|
67
68
|
used: number;
|
|
68
69
|
limit: number;
|
|
69
70
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Record plan-limit status from a decoded JSON response body.
|
|
72
|
-
* Never throws. Overwrites the last-seen cell when a well-formed
|
|
73
|
-
* `planLimits` object is present; ignores malformed / absent payloads.
|
|
74
|
-
*/
|
|
75
71
|
export declare function recordPlanLimitStatus(body: unknown, opts?: {
|
|
76
72
|
write?: (s: string) => void;
|
|
77
73
|
statePath?: string;
|
|
@@ -104,7 +100,9 @@ export declare const INDIVIDUAL_PRICE_LABEL = "$50/mo";
|
|
|
104
100
|
* own checkout target; an older hq-pro sends none, and the line then names the
|
|
105
101
|
* plan without a link rather than inventing one.
|
|
106
102
|
*/
|
|
107
|
-
export declare function renderPersonalPlanFeatureNotice(feature: string, upgradeUrl?: string | null
|
|
103
|
+
export declare function renderPersonalPlanFeatureNotice(feature: string, upgradeUrl?: string | null, opts?: {
|
|
104
|
+
pricingPlan?: PricingPlan | null;
|
|
105
|
+
}): string;
|
|
108
106
|
/**
|
|
109
107
|
* One line per crossing.
|
|
110
108
|
*
|
|
@@ -39,6 +39,7 @@ import * as path from "node:path";
|
|
|
39
39
|
import { resolveProcessFlagCompanyContext } from "./flag-registry.js";
|
|
40
40
|
import { isCliLimitResourceKind, queueCliLimitNoticeExposure, } from "../utils/cli-telemetry.js";
|
|
41
41
|
import { planLimitUpgradeFallbackUrl, selectPlanLimitUpgradeUrl, } from "./billing/plan-limit-upgrade-url.js";
|
|
42
|
+
import { isUsageOnly, pricingPlanFromBody, usageOnlyPersonalFeatureNotice, } from "./billing/usage-only.js";
|
|
42
43
|
export const PLAN_LIMIT_UPGRADE_URL = "https://hq.computer/billing/upgrade";
|
|
43
44
|
/**
|
|
44
45
|
* Cadence of the over-limit per-turn line (US-036). Was once a day; the nag
|
|
@@ -220,8 +221,21 @@ function parseThresholdCrossings(body) {
|
|
|
220
221
|
* Never throws. Overwrites the last-seen cell when a well-formed
|
|
221
222
|
* `planLimits` object is present; ignores malformed / absent payloads.
|
|
222
223
|
*/
|
|
224
|
+
/** Set once a body in this process says the account is on usage pricing. */
|
|
225
|
+
let usageOnlySeen = false;
|
|
223
226
|
export function recordPlanLimitStatus(body, opts = {}) {
|
|
224
227
|
try {
|
|
228
|
+
// Usage pricing (US-012): a body that says the account is on usage
|
|
229
|
+
// pricing carries no Starter caps to nag about. Drop any earlier Starter
|
|
230
|
+
// status so the completion nag stays silent too.
|
|
231
|
+
// The flag is sticky for the process: a later 2xx body that carries
|
|
232
|
+
// `planLimits` without `pricingPlan` (an older route) must not bring the
|
|
233
|
+
// Starter nag back for the same command.
|
|
234
|
+
if (usageOnlySeen || isUsageOnly(pricingPlanFromBody(body))) {
|
|
235
|
+
usageOnlySeen = true;
|
|
236
|
+
lastSeen = null;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
225
239
|
const status = parsePlanLimits(body);
|
|
226
240
|
const crossings = parseThresholdCrossings(body);
|
|
227
241
|
let resolvedUpgradeUrl = null;
|
|
@@ -384,8 +398,11 @@ const PERSONAL_FEATURE_LABELS = {
|
|
|
384
398
|
* own checkout target; an older hq-pro sends none, and the line then names the
|
|
385
399
|
* plan without a link rather than inventing one.
|
|
386
400
|
*/
|
|
387
|
-
export function renderPersonalPlanFeatureNotice(feature, upgradeUrl) {
|
|
401
|
+
export function renderPersonalPlanFeatureNotice(feature, upgradeUrl, opts = {}) {
|
|
388
402
|
const label = PERSONAL_FEATURE_LABELS[feature] ?? feature;
|
|
403
|
+
// A person on usage pricing has no Individual plan to buy (US-012).
|
|
404
|
+
if (isUsageOnly(opts.pricingPlan))
|
|
405
|
+
return usageOnlyPersonalFeatureNotice(label);
|
|
389
406
|
const headline = `${label} is included with the HQ ${INDIVIDUAL_PLAN_NAME} plan (${INDIVIDUAL_PRICE_LABEL}).`;
|
|
390
407
|
const action = typeof upgradeUrl === "string" && upgradeUrl.trim().length > 0
|
|
391
408
|
? `Upgrade: ${upgradeUrl.trim()}`
|
|
@@ -517,6 +534,7 @@ export function emitPlanLimitNag(opts = {}) {
|
|
|
517
534
|
/** Test-only helper — clears last-seen status and session dedupe flags. */
|
|
518
535
|
export function _resetForTests() {
|
|
519
536
|
lastSeen = null;
|
|
537
|
+
usageOnlySeen = false;
|
|
520
538
|
warningShownThisSession = false;
|
|
521
539
|
overShownThisSession = false;
|
|
522
540
|
thresholdsShownThisSession.clear();
|
|
@@ -8,4 +8,26 @@ export interface ProviderLauncherDependencies {
|
|
|
8
8
|
}
|
|
9
9
|
export declare function providerLoginGuidance(provider: ProviderName, output: string): string | null;
|
|
10
10
|
export declare function runProviderLauncher(providerValue: string, args: readonly string[], dependencies?: ProviderLauncherDependencies): Promise<number>;
|
|
11
|
+
export interface ProviderCaptureOptions {
|
|
12
|
+
spawn?: typeof nodeSpawn;
|
|
13
|
+
cwd?: string;
|
|
14
|
+
/** Kill the child and report `timedOut` after this many milliseconds. */
|
|
15
|
+
timeoutMs: number;
|
|
16
|
+
}
|
|
17
|
+
export type ProviderCaptureResult = {
|
|
18
|
+
kind: "ok";
|
|
19
|
+
stdout: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "not-installed";
|
|
22
|
+
} | {
|
|
23
|
+
kind: "not-signed-in";
|
|
24
|
+
guidance: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "timed-out";
|
|
27
|
+
} | {
|
|
28
|
+
kind: "error";
|
|
29
|
+
code: number;
|
|
30
|
+
stderrTail: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function captureProviderRun(provider: "claude", args: readonly string[], options: ProviderCaptureOptions): Promise<ProviderCaptureResult>;
|
|
11
33
|
//# sourceMappingURL=provider-launcher.d.ts.map
|
|
@@ -53,6 +53,71 @@ export async function runProviderLauncher(providerValue, args, dependencies = {}
|
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
|
+
const CAPTURE_LIMIT = 1024 * 1024;
|
|
57
|
+
/**
|
|
58
|
+
* Run a noninteractive Claude Code command (`claude -p ...`) and capture its
|
|
59
|
+
* output instead of streaming it. stdin is closed so the child never waits on
|
|
60
|
+
* the terminal, and the run is bounded by `timeoutMs`. Never throws.
|
|
61
|
+
*/
|
|
62
|
+
const KILL_GRACE_MS = 5_000;
|
|
63
|
+
export async function captureProviderRun(provider, args, options) {
|
|
64
|
+
if (!args.some((arg) => arg === "-p" || arg === "--print")) {
|
|
65
|
+
throw new Error("captureProviderRun needs a noninteractive Claude Code command (-p or --print).");
|
|
66
|
+
}
|
|
67
|
+
let child;
|
|
68
|
+
try {
|
|
69
|
+
child = (options.spawn ?? nodeSpawn)(provider, [...args], {
|
|
70
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const code = err.code;
|
|
77
|
+
if (code === "ENOENT")
|
|
78
|
+
return { kind: "not-installed" };
|
|
79
|
+
return { kind: "error", code: 1, stderrTail: err.message };
|
|
80
|
+
}
|
|
81
|
+
let stdout = "";
|
|
82
|
+
let stderr = "";
|
|
83
|
+
child.stdout?.on("data", (chunk) => {
|
|
84
|
+
stdout = `${stdout}${String(chunk)}`.slice(-CAPTURE_LIMIT);
|
|
85
|
+
});
|
|
86
|
+
child.stderr?.on("data", (chunk) => {
|
|
87
|
+
stderr = `${stderr}${String(chunk)}`.slice(-16_384);
|
|
88
|
+
});
|
|
89
|
+
return await new Promise((resolve) => {
|
|
90
|
+
let settled = false;
|
|
91
|
+
const finish = (result) => {
|
|
92
|
+
if (settled)
|
|
93
|
+
return;
|
|
94
|
+
settled = true;
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
resolve(result);
|
|
97
|
+
};
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
child.kill("SIGTERM");
|
|
100
|
+
const hardKill = setTimeout(() => {
|
|
101
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
102
|
+
child.kill("SIGKILL");
|
|
103
|
+
}, KILL_GRACE_MS);
|
|
104
|
+
hardKill.unref();
|
|
105
|
+
child.unref();
|
|
106
|
+
finish({ kind: "timed-out" });
|
|
107
|
+
}, options.timeoutMs);
|
|
108
|
+
child.once("error", (err) => {
|
|
109
|
+
finish(err.code === "ENOENT" ? { kind: "not-installed" } : { kind: "error", code: 1, stderrTail: err.message });
|
|
110
|
+
});
|
|
111
|
+
child.once("close", (code) => {
|
|
112
|
+
const guidance = providerLoginGuidance(provider, `${stdout.slice(-16_384)}\n${stderr}`);
|
|
113
|
+
if (guidance)
|
|
114
|
+
return finish({ kind: "not-signed-in", guidance });
|
|
115
|
+
if (code === 0)
|
|
116
|
+
return finish({ kind: "ok", stdout });
|
|
117
|
+
finish({ kind: "error", code: code ?? 1, stderrTail: stderr.trim().slice(-500) });
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
56
121
|
function write(stream, message) {
|
|
57
122
|
(stream ?? process.stderr).write(message);
|
|
58
123
|
}
|
|
@@ -544,6 +544,8 @@ export type SearchCollection = {
|
|
|
544
544
|
mask: string;
|
|
545
545
|
context: string;
|
|
546
546
|
};
|
|
547
|
+
/** Mask of the per-company `<co>-assets` collection: library asset placeholders. */
|
|
548
|
+
export declare const ASSET_COLLECTION_MASK = "**/*.hqcloud";
|
|
547
549
|
/** Derive the local qmd collection policy for one HQ tree. */
|
|
548
550
|
export declare function deriveCollections(hqRoot: string): SearchCollection[];
|
|
549
551
|
export type ReconcileCollectionsOptions = {
|
|
@@ -1353,6 +1353,24 @@ function containsProjectSource(directory) {
|
|
|
1353
1353
|
}
|
|
1354
1354
|
return false;
|
|
1355
1355
|
}
|
|
1356
|
+
/** Mask of the per-company `<co>-assets` collection: library asset placeholders. */
|
|
1357
|
+
export const ASSET_COLLECTION_MASK = '**/*.hqcloud';
|
|
1358
|
+
const ASSET_SCAN_SKIP_DIRS = new Set(['.git', 'node_modules']);
|
|
1359
|
+
function containsAssetPlaceholder(directory) {
|
|
1360
|
+
if (!fs.existsSync(directory))
|
|
1361
|
+
return false;
|
|
1362
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
1363
|
+
const child = path.join(directory, entry.name);
|
|
1364
|
+
if (entry.isDirectory()) {
|
|
1365
|
+
if (!ASSET_SCAN_SKIP_DIRS.has(entry.name) && containsAssetPlaceholder(child))
|
|
1366
|
+
return true;
|
|
1367
|
+
}
|
|
1368
|
+
else if (entry.isFile() && entry.name.endsWith('.hqcloud') && entry.name !== '.hqcloud') {
|
|
1369
|
+
return true;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1356
1374
|
/** Derive the local qmd collection policy for one HQ tree. */
|
|
1357
1375
|
export function deriveCollections(hqRoot) {
|
|
1358
1376
|
const root = path.resolve(hqRoot);
|
|
@@ -1385,6 +1403,22 @@ export function deriveCollections(hqRoot) {
|
|
|
1385
1403
|
context: `Project PRDs and documentation for ${entry.name}.`,
|
|
1386
1404
|
});
|
|
1387
1405
|
}
|
|
1406
|
+
// Library asset placeholders can sit anywhere under a company (offload
|
|
1407
|
+
// replaces a file in place), so the collection is rooted at the company and
|
|
1408
|
+
// its mask admits only `.hqcloud` files. A separate collection, rather than
|
|
1409
|
+
// widening the knowledge/projects masks, because reconcileCollections adds
|
|
1410
|
+
// missing collections but never rewrites an existing collection's mask.
|
|
1411
|
+
for (const entry of companies) {
|
|
1412
|
+
const companyDir = path.join(companiesDir, entry.name);
|
|
1413
|
+
if (!containsAssetPlaceholder(companyDir))
|
|
1414
|
+
continue;
|
|
1415
|
+
collections.push({
|
|
1416
|
+
name: `${entry.name}-assets`,
|
|
1417
|
+
path: companyDir,
|
|
1418
|
+
mask: ASSET_COLLECTION_MASK,
|
|
1419
|
+
context: `Library asset placeholders (.hqcloud) for ${entry.name}.`,
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1388
1422
|
const personalKnowledge = path.join(root, 'personal', 'knowledge');
|
|
1389
1423
|
if (containsIndexedMarkdown(personalKnowledge)) {
|
|
1390
1424
|
collections.push({
|
|
@@ -101,7 +101,8 @@ export declare function resolveIndexMaxDocBytes(opts?: {
|
|
|
101
101
|
/**
|
|
102
102
|
* Extensions a qmd mask selects, lowercased and without the dot.
|
|
103
103
|
*
|
|
104
|
-
* HQ registers
|
|
104
|
+
* HQ registers single-extension (`**\/*.md`, `**\/*.hqcloud`) and brace-list
|
|
105
|
+
* (`**\/*.{md,json}`) masks today (see `deriveCollections`), but an
|
|
105
106
|
* operator's own collection may carry anything. An unrecognised mask returns
|
|
106
107
|
* `null`, meaning "match every file" — the cap then applies to the whole
|
|
107
108
|
* collection, which is the safe direction: we may ignore a file qmd would not
|
|
@@ -177,7 +177,8 @@ function parseNonNegativeInt(value) {
|
|
|
177
177
|
/**
|
|
178
178
|
* Extensions a qmd mask selects, lowercased and without the dot.
|
|
179
179
|
*
|
|
180
|
-
* HQ registers
|
|
180
|
+
* HQ registers single-extension (`**\/*.md`, `**\/*.hqcloud`) and brace-list
|
|
181
|
+
* (`**\/*.{md,json}`) masks today (see `deriveCollections`), but an
|
|
181
182
|
* operator's own collection may carry anything. An unrecognised mask returns
|
|
182
183
|
* `null`, meaning "match every file" — the cap then applies to the whole
|
|
183
184
|
* collection, which is the safe direction: we may ignore a file qmd would not
|
package/dist/main.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ import "./node-network-compat.js";
|
|
|
7
7
|
import { Sentry } from "./sentry.js";
|
|
8
8
|
export { handleStreamError } from "./stream-error-boundary.js";
|
|
9
9
|
export type { StreamErrorDependencies } from "./stream-error-boundary.js";
|
|
10
|
+
import { type PlanGateError } from "./utils/plan-gate-error.js";
|
|
11
|
+
import { type PricingPlan } from "./lib/billing/usage-only.js";
|
|
10
12
|
import { upgradeToTeam } from "./utils/team-upgrade.js";
|
|
11
13
|
/**
|
|
12
14
|
* Total time one CLI invocation may await foreground network work. This spans
|
|
@@ -43,7 +45,14 @@ export type TopLevelErrorDependencies = {
|
|
|
43
45
|
ask?: (question: string) => Promise<string>;
|
|
44
46
|
openUrl?: (url: string) => Promise<void>;
|
|
45
47
|
upgradeToTeam?: typeof upgradeToTeam;
|
|
48
|
+
/**
|
|
49
|
+
* Usage pricing (US-012): resolve the account's pricing plan for a plan gate
|
|
50
|
+
* whose 402 body did not carry one. Absent = use the body only.
|
|
51
|
+
*/
|
|
52
|
+
resolvePricingPlan?: (planGate: PlanGateError) => Promise<PricingPlan | null>;
|
|
46
53
|
};
|
|
54
|
+
/** Production resolver: membership row / usage-limits / summary, never interactive. */
|
|
55
|
+
export declare function resolvePlanGatePricingPlan(planGate: PlanGateError): Promise<PricingPlan | null>;
|
|
47
56
|
export declare const MAX_OWN_DIST_RELATIVE_PATH_BYTES = 256;
|
|
48
57
|
/** Return a bounded path only when targetPath is beneath packageRoot/dist. */
|
|
49
58
|
export declare function relativeOwnDistModulePath(packageRootPath: string, targetPath: string): string | undefined;
|
package/dist/main.js
CHANGED
|
@@ -40,7 +40,8 @@ import { browserLoginAbandonedMessage } from "./utils/browser-login-abandoned.js
|
|
|
40
40
|
import { callbackPortBusyMessage, DEFAULT_CALLBACK_PORT } from "./utils/callback-port-busy.js";
|
|
41
41
|
import { DEFAULT_COGNITO, isMachineIdentity, loadMachineCachedTokens, } from "./utils/cognito-session.js";
|
|
42
42
|
import { isCompanySelectionError } from "./utils/company-selection-error.js";
|
|
43
|
-
import { canOfferTeamUpgrade, formatPlanGateError, offerTeamUpgrade, offerUpgradeLink, planGateErrorFromVaultError, } from "./utils/plan-gate-error.js";
|
|
43
|
+
import { canOfferTeamUpgrade, formatPlanGateError, offerTeamUpgrade, offerUpgradeLink, planGateErrorFromVaultError, withPricingPlan, } from "./utils/plan-gate-error.js";
|
|
44
|
+
import { isUsageOnly, resolveCompanyPricingPlan, resolvePersonPricingPlan, } from "./lib/billing/usage-only.js";
|
|
44
45
|
import { upgradeToTeam } from "./utils/team-upgrade.js";
|
|
45
46
|
import { printCachedVersionNudge, printVersionAndNudge, refreshVersionCache, staleAgainstCachedLatest, } from "./utils/version-check.js";
|
|
46
47
|
import { enforceVersionGate, getVersionGatePhase, isLocalDependencyInstall, resolveRunningInstall, shouldSkipGate, } from "./utils/version-gate.js";
|
|
@@ -375,8 +376,19 @@ export async function runCli() {
|
|
|
375
376
|
}
|
|
376
377
|
}
|
|
377
378
|
}
|
|
379
|
+
/** Production resolver: membership row / usage-limits / summary, never interactive. */
|
|
380
|
+
export async function resolvePlanGatePricingPlan(planGate) {
|
|
381
|
+
if (planGate.code === "PERSONAL_PLAN_REQUIRED")
|
|
382
|
+
return resolvePersonPricingPlan({ timeoutMs: 3000 });
|
|
383
|
+
const companyUid = planGate.details.companyUid;
|
|
384
|
+
if (typeof companyUid === "string" && /^cmp_[A-Za-z0-9_-]+$/.test(companyUid)) {
|
|
385
|
+
return resolveCompanyPricingPlan(companyUid, { timeoutMs: 3000 });
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
378
389
|
const defaultTopLevelErrorDependencies = {
|
|
379
390
|
sentry: Sentry,
|
|
391
|
+
resolvePricingPlan: resolvePlanGatePricingPlan,
|
|
380
392
|
stderr: process.stderr,
|
|
381
393
|
setExitCode: (code) => {
|
|
382
394
|
process.exitCode = code;
|
|
@@ -562,7 +574,11 @@ function ownDistModuleMissingCaptureContext(err) {
|
|
|
562
574
|
export async function handleTopLevelError(err, deps = defaultTopLevelErrorDependencies) {
|
|
563
575
|
// A typed plan gate, or an hq-cloud vault error carrying a plan-limit
|
|
564
576
|
// refusal (402 / PLAN_LIMIT_* from a sync, share or skill upload).
|
|
565
|
-
const
|
|
577
|
+
const decodedPlanGate = planGateErrorFromVaultError(err);
|
|
578
|
+
const planGate = decodedPlanGate && !decodedPlanGate.details.pricingPlan && deps.resolvePricingPlan
|
|
579
|
+
? withPricingPlan(decodedPlanGate, await deps.resolvePricingPlan(decodedPlanGate).catch(() => null))
|
|
580
|
+
: decodedPlanGate;
|
|
581
|
+
const usageOnlyGate = isUsageOnly(planGate?.details.pricingPlan);
|
|
566
582
|
// A broken pipe (EPIPE) means the reader of `hq`'s output closed it early
|
|
567
583
|
// (`hq … | head`, `source <(hq …)`, a parent that exited). That is normal
|
|
568
584
|
// Unix behavior with no user-facing degradation — exit cleanly (0) and
|
|
@@ -654,7 +670,8 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
|
|
|
654
670
|
? queueCliLimitNoticeExposure(planGate.details.resource, "refusal", planGate.details.companyUid)
|
|
655
671
|
: null;
|
|
656
672
|
deps.setExitCode(1);
|
|
657
|
-
|
|
673
|
+
// A usage-only account has no plan to buy: no checkout or upgrade offer.
|
|
674
|
+
if (interactive && planGate.details.upgradeUrl && !usageOnlyGate) {
|
|
658
675
|
const companyUid = planGate.details.companyUid;
|
|
659
676
|
const isCompanyWorkforceCheckout = typeof companyUid === "string" &&
|
|
660
677
|
/^cmp_[A-Za-z0-9_-]+$/.test(companyUid) &&
|
|
@@ -15,14 +15,29 @@
|
|
|
15
15
|
* 3. `hq billing checkout` (see commands/billing.ts) reuses `mintPaymentLink`
|
|
16
16
|
* to hand out the same link proactively.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* Legacy prices mirror the console's authoritative constants
|
|
19
19
|
* (hq-console `src/lib/billing-pricing.ts`): amounts are in the smallest
|
|
20
|
-
* currency unit (US cents) to match Stripe's wire shape.
|
|
20
|
+
* currency unit (US cents) to match Stripe's wire shape. They apply to
|
|
21
|
+
* accounts on the plans that existed before usage pricing.
|
|
22
|
+
*
|
|
23
|
+
* Usage pricing (US-012): boxes are quoted by size from the usage card
|
|
24
|
+
* (`src/lib/billing/usage-card.ts`, `GET /v1/pricing` with a bundled
|
|
25
|
+
* fallback): small t3.small, medium t3.medium, large t3.large, xlarge
|
|
26
|
+
* t3.xlarge. Use {@link usageBoxQuotes} / {@link usageBoxQuoteCents}, never
|
|
27
|
+
* the legacy constants, for a usage-only account.
|
|
21
28
|
*/
|
|
22
|
-
|
|
29
|
+
import { type UsageCard } from "../lib/billing/usage-card.js";
|
|
30
|
+
/** Legacy per-Agent monthly list price, in cents ($100.00 / agent / month). */
|
|
23
31
|
export declare const AGENT_PRICE_CENTS = 10000;
|
|
24
|
-
/**
|
|
32
|
+
/** Legacy per-Outpost monthly list price, in cents ($80.00 / Outpost / month). */
|
|
25
33
|
export declare const OUTPOST_PRICE_CENTS = 8000;
|
|
34
|
+
/** Usage-pricing box sizes, in card order ("small (t3.small) $100/month, ..."). */
|
|
35
|
+
export declare function usageBoxQuotes(card?: UsageCard): string;
|
|
36
|
+
/**
|
|
37
|
+
* Monthly price of one usage-pricing box, in cents, by size or instance type.
|
|
38
|
+
* Null for a size the card does not list.
|
|
39
|
+
*/
|
|
40
|
+
export declare function usageBoxQuoteCents(sizeOrInstanceType: string, card?: UsageCard): number | null;
|
|
26
41
|
/** Render a cents amount as a `$X.YZ` USD string. */
|
|
27
42
|
export declare function formatUsd(cents: number): string;
|
|
28
43
|
/**
|
|
@@ -15,16 +15,37 @@
|
|
|
15
15
|
* 3. `hq billing checkout` (see commands/billing.ts) reuses `mintPaymentLink`
|
|
16
16
|
* to hand out the same link proactively.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* Legacy prices mirror the console's authoritative constants
|
|
19
19
|
* (hq-console `src/lib/billing-pricing.ts`): amounts are in the smallest
|
|
20
|
-
* currency unit (US cents) to match Stripe's wire shape.
|
|
20
|
+
* currency unit (US cents) to match Stripe's wire shape. They apply to
|
|
21
|
+
* accounts on the plans that existed before usage pricing.
|
|
22
|
+
*
|
|
23
|
+
* Usage pricing (US-012): boxes are quoted by size from the usage card
|
|
24
|
+
* (`src/lib/billing/usage-card.ts`, `GET /v1/pricing` with a bundled
|
|
25
|
+
* fallback): small t3.small, medium t3.medium, large t3.large, xlarge
|
|
26
|
+
* t3.xlarge. Use {@link usageBoxQuotes} / {@link usageBoxQuoteCents}, never
|
|
27
|
+
* the legacy constants, for a usage-only account.
|
|
21
28
|
*/
|
|
22
29
|
import chalk from "chalk";
|
|
23
30
|
import { vaultApiFetch } from "./vault-api.js";
|
|
24
|
-
|
|
31
|
+
import { BUNDLED_USAGE_CARD, formatBoxQuotes, } from "../lib/billing/usage-card.js";
|
|
32
|
+
/** Legacy per-Agent monthly list price, in cents ($100.00 / agent / month). */
|
|
25
33
|
export const AGENT_PRICE_CENTS = 10_000;
|
|
26
|
-
/**
|
|
34
|
+
/** Legacy per-Outpost monthly list price, in cents ($80.00 / Outpost / month). */
|
|
27
35
|
export const OUTPOST_PRICE_CENTS = 8_000;
|
|
36
|
+
/** Usage-pricing box sizes, in card order ("small (t3.small) $100/month, ..."). */
|
|
37
|
+
export function usageBoxQuotes(card = BUNDLED_USAGE_CARD) {
|
|
38
|
+
return formatBoxQuotes(card);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Monthly price of one usage-pricing box, in cents, by size or instance type.
|
|
42
|
+
* Null for a size the card does not list.
|
|
43
|
+
*/
|
|
44
|
+
export function usageBoxQuoteCents(sizeOrInstanceType, card = BUNDLED_USAGE_CARD) {
|
|
45
|
+
const ref = sizeOrInstanceType.trim().toLowerCase();
|
|
46
|
+
const box = card.boxes.find((b) => b.size.toLowerCase() === ref || b.instanceType.toLowerCase() === ref);
|
|
47
|
+
return box ? Math.round(box.monthlyUsd * 100) : null;
|
|
48
|
+
}
|
|
28
49
|
/** Render a cents amount as a `$X.YZ` USD string. */
|
|
29
50
|
export function formatUsd(cents) {
|
|
30
51
|
return `$${(Math.max(0, Math.round(cents)) / 100).toFixed(2)}`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type PlanLimitFreeMonth } from "../lib/billing/free-month.js";
|
|
2
|
+
import { type PricingPlan } from "../lib/billing/usage-only.js";
|
|
2
3
|
export type PlanGateCode = "PLAN_LIMIT_EXCEEDED" | "PLAN_REQUIRED" | "MEETING_PLAN_REQUIRED"
|
|
3
4
|
/** A PERSONAL scope asked for a feature the paid Individual plan includes. */
|
|
4
5
|
| "PERSONAL_PLAN_REQUIRED"
|
|
@@ -27,7 +28,16 @@ export interface PlanGateDetails {
|
|
|
27
28
|
fixOptions?: Record<string, number>;
|
|
28
29
|
/** Hard-stop 402: the free-month offer, present only when it stands. */
|
|
29
30
|
freeMonth?: PlanLimitFreeMonth;
|
|
31
|
+
/**
|
|
32
|
+
* Usage pricing (US-012): from the 402 body, or attached by the caller from
|
|
33
|
+
* /membership/me, usage-limits or the billing summary. `"usage-only"`
|
|
34
|
+
* renders no free month, Starter, Workforce or Individual copy and no plan
|
|
35
|
+
* upgrade link. Absent keeps the legacy copy.
|
|
36
|
+
*/
|
|
37
|
+
pricingPlan?: PricingPlan;
|
|
30
38
|
}
|
|
39
|
+
/** Copy a plan gate with the caller's pricing plan attached. */
|
|
40
|
+
export declare function withPricingPlan(err: PlanGateError, pricingPlan: PricingPlan | null | undefined): PlanGateError;
|
|
31
41
|
/** One renderer for server-selected upgrade links across CLI plan refusals. */
|
|
32
42
|
export declare function formatUpgradeRefusal(reason: string, upgradeUrl: string | undefined, options?: {
|
|
33
43
|
urlOnly?: boolean;
|
|
@@ -6,6 +6,48 @@
|
|
|
6
6
|
import { PLAN_LIMIT_DIMENSION_LABELS, renderPersonalPlanFeatureNotice, } from "../lib/plan-limit-nag.js";
|
|
7
7
|
import { withCliLimitEntrySurface } from "../lib/billing/plan-limit-upgrade-url.js";
|
|
8
8
|
import { parseFreeMonth, } from "../lib/billing/free-month.js";
|
|
9
|
+
import { isUsageOnly, pricingPlanFromBody, usageOnlyCardRequiredLines, USAGE_PRICING_POINTER, } from "../lib/billing/usage-only.js";
|
|
10
|
+
/** Legacy plan names and prices a usage-only account must never be shown. */
|
|
11
|
+
const LEGACY_PLAN_COPY = /Starter|Workforce|Individual|\$500|\$50\b|free month|upgrade/i;
|
|
12
|
+
/** Copy a plan gate with the caller's pricing plan attached. */
|
|
13
|
+
export function withPricingPlan(err, pricingPlan) {
|
|
14
|
+
if (!pricingPlan || err.details.pricingPlan === pricingPlan)
|
|
15
|
+
return err;
|
|
16
|
+
return new PlanGateError(err.code, { ...err.details, pricingPlan });
|
|
17
|
+
}
|
|
18
|
+
/** The usage-only rendering of a plan gate. Never names a legacy plan. */
|
|
19
|
+
function usageOnlyPlanGateReason(code, details) {
|
|
20
|
+
switch (code) {
|
|
21
|
+
case "PERSONAL_PLAN_REQUIRED":
|
|
22
|
+
return renderPersonalPlanFeatureNotice(details.feature ?? "this feature", null, {
|
|
23
|
+
pricingPlan: "usage-only",
|
|
24
|
+
});
|
|
25
|
+
case "MEETING_PLAN_REQUIRED":
|
|
26
|
+
return usageOnlyCardRequiredLines("A meeting notetaker bot").join("\n");
|
|
27
|
+
case "BILLING_REQUIRED":
|
|
28
|
+
return usageOnlyCardRequiredLines("An external agent").join("\n");
|
|
29
|
+
case "PLAN_REQUIRED":
|
|
30
|
+
return usageOnlyCardRequiredLines("This feature").join("\n");
|
|
31
|
+
case "PLAN_LIMIT_EXCEEDED": {
|
|
32
|
+
const server = details.reason?.trim();
|
|
33
|
+
const head = server && !LEGACY_PLAN_COPY.test(server) ? server : "This account has reached a usage limit.";
|
|
34
|
+
const lines = [head];
|
|
35
|
+
for (const row of details.resources ?? []) {
|
|
36
|
+
lines.push(` ${resourceLabel(row.resource)}: ` +
|
|
37
|
+
`${formatResourceAmount(row.resource, row.used)} of ` +
|
|
38
|
+
`${formatResourceAmount(row.resource, row.limit)} used.`);
|
|
39
|
+
}
|
|
40
|
+
const fixes = Object.entries(details.fixOptions ?? {}).map(([resource, target]) => `${resourceFixAction(resource)} to make room (the limit is ` +
|
|
41
|
+
`${formatResourceAmount(resource, target)}).`);
|
|
42
|
+
if (fixes.length > 0) {
|
|
43
|
+
lines.push(" Ways to sort it:");
|
|
44
|
+
fixes.forEach((fix, index) => lines.push(` ${index + 1}. ${fix}`));
|
|
45
|
+
}
|
|
46
|
+
lines.push(USAGE_PRICING_POINTER);
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
9
51
|
const GIB = 1024 ** 3;
|
|
10
52
|
const MIB = 1024 ** 2;
|
|
11
53
|
const KIB = 1024;
|
|
@@ -75,6 +117,8 @@ function planLimitRefusalText(head, details) {
|
|
|
75
117
|
return lines.join("\n");
|
|
76
118
|
}
|
|
77
119
|
function planGateReason(code, details) {
|
|
120
|
+
if (isUsageOnly(details.pricingPlan))
|
|
121
|
+
return usageOnlyPlanGateReason(code, details);
|
|
78
122
|
if (code === "PLAN_LIMIT_EXCEEDED") {
|
|
79
123
|
const head = details.reason?.trim()
|
|
80
124
|
? details.reason.trim()
|
|
@@ -143,6 +187,8 @@ export function companyUidFromPlanUpgradeUrl(value) {
|
|
|
143
187
|
return match?.[1];
|
|
144
188
|
}
|
|
145
189
|
function formatPlanGateErrorText(code, details) {
|
|
190
|
+
if (isUsageOnly(details.pricingPlan))
|
|
191
|
+
return planGateReason(code, details);
|
|
146
192
|
return formatUpgradeRefusal(planGateReason(code, details), details.upgradeUrl);
|
|
147
193
|
}
|
|
148
194
|
export class PlanGateError extends Error {
|
|
@@ -160,6 +206,9 @@ export function isPlanGateError(err) {
|
|
|
160
206
|
}
|
|
161
207
|
/** Plain, stable CLI copy for plan-gated resource creation. */
|
|
162
208
|
export function formatPlanGateError(err, options = {}) {
|
|
209
|
+
// A usage-only account has no plan to upgrade to: no upgrade link.
|
|
210
|
+
if (isUsageOnly(err.details.pricingPlan))
|
|
211
|
+
return planGateReason(err.code, err.details);
|
|
163
212
|
return formatUpgradeRefusal(planGateReason(err.code, err.details), err.details.upgradeUrl, options);
|
|
164
213
|
}
|
|
165
214
|
export function canOfferTeamUpgrade(argv = process.argv) {
|
|
@@ -229,6 +278,11 @@ export function planGateErrorFromPayload(status, body, companyUid) {
|
|
|
229
278
|
if (status !== 402 || !body || typeof body !== "object")
|
|
230
279
|
return null;
|
|
231
280
|
const payload = body;
|
|
281
|
+
const decoded = decodePlanGatePayload(payload, companyUid);
|
|
282
|
+
const pricingPlan = pricingPlanFromBody(payload);
|
|
283
|
+
return decoded && pricingPlan ? withPricingPlan(decoded, pricingPlan) : decoded;
|
|
284
|
+
}
|
|
285
|
+
function decodePlanGatePayload(payload, companyUid) {
|
|
232
286
|
// US-054: the personal feature denial. hq-pro identifies it with
|
|
233
287
|
// `error: "plan_feature_unavailable"` plus `scope: "personal"`; both the
|
|
234
288
|
// `scope` and `requiredPlan` fields are OPTIONAL on the wire, so an hq-pro
|
package/dist/utils/vault-api.js
CHANGED
|
@@ -172,7 +172,10 @@ function hasPlanLimitStatusPayload(body) {
|
|
|
172
172
|
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
173
173
|
return false;
|
|
174
174
|
}
|
|
175
|
-
return Object.hasOwn(body, 'planLimits') ||
|
|
175
|
+
return (Object.hasOwn(body, 'planLimits') ||
|
|
176
|
+
Object.hasOwn(body, 'thresholdCrossings') ||
|
|
177
|
+
// Usage pricing (US-012): a usage-only body silences the Starter nag.
|
|
178
|
+
body.pricingPlan === 'usage-only');
|
|
176
179
|
}
|
|
177
180
|
/**
|
|
178
181
|
* Map Cognito JWT paths onto parallel Bearer `hqk_` routes under `/v1/keys/…`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.144",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
49
49
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
50
50
|
"@aws-sdk/dsql-signer": "^3.1146.0",
|
|
51
|
-
"@indigoai-us/hq-cloud": "~6.18.
|
|
51
|
+
"@indigoai-us/hq-cloud": "~6.18.82",
|
|
52
52
|
"@indigoai-us/hq-flags-client": "^0.1.2",
|
|
53
53
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
54
54
|
"@sentry/node": "^10.49.0",
|