@indigoai-us/hq-cli 5.345.159 → 5.345.161
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/assets/scaffold/core/scripts/session-journal.sh +116 -24
- package/dist/command-catalog.generated.d.ts +222 -9
- package/dist/command-catalog.generated.js +273 -9
- package/dist/commands/access.d.ts +6 -0
- package/dist/commands/access.js +18 -1
- package/dist/commands/acl-lock.d.ts +41 -0
- package/dist/commands/acl-lock.js +28 -0
- package/dist/commands/agent-enroll.d.ts +2 -0
- package/dist/commands/agent-enroll.js +9 -4
- package/dist/commands/agents.js +5 -11
- package/dist/commands/auth.js +6 -5
- package/dist/commands/billing-sponsor.d.ts +4 -4
- package/dist/commands/billing-sponsor.js +11 -11
- package/dist/commands/billing-usage.d.ts +22 -8
- package/dist/commands/billing-usage.js +57 -22
- package/dist/commands/billing.d.ts +7 -7
- package/dist/commands/billing.js +13 -13
- package/dist/commands/cloud-provision.js +2 -2
- package/dist/commands/cloud.js +7 -7
- package/dist/commands/core-session-helper-runtime.d.ts +1 -0
- package/dist/commands/core-session-journal-native.d.ts +1 -0
- package/dist/commands/core-session-journal-native.js +158 -34
- package/dist/commands/db-remote-deps.js +12 -1
- package/dist/commands/feedback.d.ts +12 -0
- package/dist/commands/feedback.js +52 -0
- package/dist/commands/files-browse.d.ts +8 -0
- package/dist/commands/files-browse.js +83 -1
- package/dist/commands/files.js +19 -7
- package/dist/commands/install-global.d.ts +2 -0
- package/dist/commands/install-global.js +24 -1
- package/dist/commands/integrations-connect.js +1 -0
- package/dist/commands/integrations-manage.d.ts +2 -0
- package/dist/commands/integrations-manage.js +6 -4
- package/dist/commands/lanes-link.js +5 -3
- package/dist/commands/lanes-session-start.d.ts +12 -0
- package/dist/commands/lanes-session-start.js +93 -12
- package/dist/commands/meetings.d.ts +15 -4
- package/dist/commands/meetings.js +58 -19
- package/dist/commands/skill.js +6 -6
- package/dist/commands/whoami.js +1 -1
- package/dist/hook-engine-path.js +13 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3236 -1988
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +21 -0
- package/dist/lanes-session-start-argv.d.ts +7 -0
- package/dist/lanes-session-start-argv.js +9 -0
- package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
- package/dist/lib/billing/plan-limit-refusals.js +26 -3
- package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
- package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
- package/dist/lib/billing/plan-lock.d.ts +22 -6
- package/dist/lib/billing/plan-lock.js +58 -30
- package/dist/lib/billing/usage-cost.d.ts +24 -2
- package/dist/lib/billing/usage-cost.js +103 -3
- package/dist/lib/billing/usage-only.d.ts +37 -1
- package/dist/lib/billing/usage-only.js +89 -4
- package/dist/lib/billing/usage-pause.d.ts +27 -0
- package/dist/lib/billing/usage-pause.js +50 -0
- package/dist/lib/bot/log.d.ts +3 -2
- package/dist/lib/bot/progress.d.ts +23 -1
- package/dist/lib/bot/progress.js +43 -5
- package/dist/lib/bot/run.js +2 -0
- package/dist/lib/bot/runtime/claude.d.ts +12 -0
- package/dist/lib/bot/runtime/claude.js +20 -0
- package/dist/lib/bot/runtime/index.d.ts +10 -1
- package/dist/lib/bot/runtime/index.js +14 -2
- package/dist/lib/bot/runtime/types.d.ts +8 -0
- package/dist/lib/db/control-plane.d.ts +5 -0
- package/dist/lib/db/control-plane.js +10 -0
- package/dist/lib/doctor/checks/ps1-shims.d.ts +21 -0
- package/dist/lib/doctor/checks/ps1-shims.js +82 -0
- package/dist/lib/doctor/fix/apply.d.ts +13 -1
- package/dist/lib/doctor/fix/apply.js +38 -3
- package/dist/lib/doctor/fix/remediation.d.ts +9 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/doctor/registry.js +2 -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/hook-engine/resolve.d.ts +14 -0
- package/dist/lib/hook-engine/resolve.js +55 -1
- package/dist/lib/lanes/links.d.ts +2 -1
- package/dist/lib/lanes/links.js +235 -17
- package/dist/lib/lanes/registry-flag.d.ts +24 -0
- package/dist/lib/lanes/registry-flag.js +72 -6
- package/dist/lib/lanes/store.d.ts +4 -2
- package/dist/lib/lanes/store.js +8 -8
- package/dist/lib/plan-limit-nag.d.ts +0 -16
- package/dist/lib/plan-limit-nag.js +17 -18
- package/dist/lib/windows/ps1-shims.d.ts +60 -0
- package/dist/lib/windows/ps1-shims.js +152 -0
- package/dist/main.d.ts +0 -2
- package/dist/main.js +19 -57
- package/dist/utils/plan-gate-error.d.ts +30 -14
- package/dist/utils/plan-gate-error.js +60 -46
- package/dist/utils/self-update.d.ts +2 -0
- package/dist/utils/self-update.js +8 -0
- package/dist/utils/staged-npm-install.js +13 -2
- package/dist/utils/team-upgrade.d.ts +19 -6
- package/dist/utils/team-upgrade.js +37 -39
- package/dist/utils/token-keychain.d.ts +11 -8
- package/dist/utils/token-keychain.js +36 -15
- package/package.json +8 -8
- package/scripts/install-hook-engine.mjs +12 -0
|
@@ -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.99","bundleSha256":"05ece85b87c743511d74111934f420f24a47677e9e90a18dd45e6d3d1d8d0375","formatVersion":1}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { installFastPathStreamErrorHandlers } from "./utils/fast-stream-errors.j
|
|
|
15
15
|
import { installWindowsHideDefault } from "./utils/windows-hide-default.js";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { parseMonitorCheckFastRequest } from "./lanes-monitor-check-argv.js";
|
|
18
|
+
import { isLaneSessionStartFastRequest } from "./lanes-session-start-argv.js";
|
|
18
19
|
import { createRequire } from "node:module";
|
|
19
20
|
import { hookEnginePathArgs, runHookEnginePath } from "./hook-engine-path.js";
|
|
20
21
|
function cygpathMixedPath(value) {
|
|
@@ -156,6 +157,22 @@ async function runMonitorCheckFastWithRecovery(request) {
|
|
|
156
157
|
return;
|
|
157
158
|
await runMainWithInstallTreeRecovery();
|
|
158
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* The SessionStart hook runs `hq lanes session-start` on every new session
|
|
162
|
+
* under a 2 s cap. Load only that command's module, not the command graph.
|
|
163
|
+
*/
|
|
164
|
+
async function runLaneSessionStartFastWithRecovery() {
|
|
165
|
+
const loaded = await recoverFastImport(() => import("./commands/lanes-session-start.js"));
|
|
166
|
+
if (loaded.reexecStatus !== undefined) {
|
|
167
|
+
process.exitCode = loaded.reexecStatus;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (loaded.module) {
|
|
171
|
+
await loaded.module.runLaneSessionStart();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
await runMainWithInstallTreeRecovery();
|
|
175
|
+
}
|
|
159
176
|
/**
|
|
160
177
|
* Whether the install follow-up would read the registry or spawn the mesh
|
|
161
178
|
* daemon. False for fast routes' callers, and for commands that are not run
|
|
@@ -206,6 +223,10 @@ function dispatchCliRoutes() {
|
|
|
206
223
|
const helpRequest = !versionRequest && isCommandHelpRequest(process.argv);
|
|
207
224
|
const parsedFastCore = versionRequest || helpRequest ? null : parseFastCore(process.argv);
|
|
208
225
|
const monitorRequest = versionRequest || helpRequest ? null : parseMonitorCheckFastRequest(process.argv);
|
|
226
|
+
if (!versionRequest && !helpRequest && !monitorRequest && isLaneSessionStartFastRequest(process.argv)) {
|
|
227
|
+
void runLaneSessionStartFastWithRecovery();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
209
230
|
const secretsRequest = versionRequest || helpRequest || monitorRequest || parsedFastCore
|
|
210
231
|
? null
|
|
211
232
|
: parseSecretsEnvFastRequest(process.argv);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free argv matcher for the `hq lanes session-start` fast route.
|
|
3
|
+
* Imported statically by src/index.ts, so it must not import anything.
|
|
4
|
+
*/
|
|
5
|
+
/** Exactly `hq lanes session-start` with no options (the SessionStart hook form). */
|
|
6
|
+
export declare function isLaneSessionStartFastRequest(argv: readonly string[]): boolean;
|
|
7
|
+
//# sourceMappingURL=lanes-session-start-argv.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free argv matcher for the `hq lanes session-start` fast route.
|
|
3
|
+
* Imported statically by src/index.ts, so it must not import anything.
|
|
4
|
+
*/
|
|
5
|
+
/** Exactly `hq lanes session-start` with no options (the SessionStart hook form). */
|
|
6
|
+
export function isLaneSessionStartFastRequest(argv) {
|
|
7
|
+
return argv.length === 4 && argv[2] === "lanes" && argv[3] === "session-start";
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=lanes-session-start-argv.js.map
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* stops re-sending paused keys. The `path`/`paths`/`count` reads below consume
|
|
19
19
|
* that event; the `error`-message parsing stays for runners older than
|
|
20
20
|
* 6.18.17.
|
|
21
|
+
*
|
|
22
|
+
* US-024: the server's sentence wins. A `plan-limit` event carries the 402
|
|
23
|
+
* body's `message` (for a usage pause, the `usagePause` message hq-pro writes
|
|
24
|
+
* for the person who hit it), and a runner that forwards `usagePause` gives
|
|
25
|
+
* its message and `actionUrl` directly. The CLI adds no plan name and labels
|
|
26
|
+
* the link `Billing:`, never "Upgrade".
|
|
21
27
|
*/
|
|
22
28
|
import { type SyncProgressEvent } from "@indigoai-us/hq-cloud";
|
|
23
29
|
/** What one push leg was refused. `null` from the tracker means nothing was. */
|
|
@@ -29,10 +35,12 @@ export interface PlanLimitRefusalSummary {
|
|
|
29
35
|
count: number;
|
|
30
36
|
/** Vault-relative paths the runner named, de-duplicated, in report order. */
|
|
31
37
|
paths: string[];
|
|
32
|
-
/** Server sentence
|
|
38
|
+
/** Server sentence (usage pause message, event message, or a refused file's error). */
|
|
33
39
|
message?: string;
|
|
34
|
-
/** Server-selected (or company billing fallback)
|
|
40
|
+
/** Server-selected (or company billing fallback) billing link. */
|
|
35
41
|
upgradeUrl?: string;
|
|
42
|
+
/** True when the server said this is a usage pause (`usagePause`). */
|
|
43
|
+
usagePause?: boolean;
|
|
36
44
|
}
|
|
37
45
|
export interface PlanLimitRefusalTracker {
|
|
38
46
|
observe(event: SyncProgressEvent): void;
|
|
@@ -57,9 +65,11 @@ export declare function createPlanLimitRefusalTracker(opts?: {
|
|
|
57
65
|
export declare function trackingProgressRecorder(tracker: PlanLimitRefusalTracker, company: string | null): (event: SyncProgressEvent) => void;
|
|
58
66
|
/** Add a later leg's refusals to an earlier total. */
|
|
59
67
|
export declare function mergePlanLimitRefusals(a: PlanLimitRefusalSummary | null | undefined, b: PlanLimitRefusalSummary | null | undefined): PlanLimitRefusalSummary | null;
|
|
68
|
+
/** The sentence when the server sent none. Names no plan. */
|
|
69
|
+
export declare const REFUSAL_FALLBACK_SENTENCE = "New files are paused for this account. Open its Billing page to see why.";
|
|
60
70
|
/**
|
|
61
71
|
* Plain lines describing a refusal: what was not uploaded, why, and the
|
|
62
|
-
*
|
|
72
|
+
* billing link. Colour and the leading status line are the caller's.
|
|
63
73
|
*/
|
|
64
74
|
export declare function renderPlanLimitRefusalLines(summary: PlanLimitRefusalSummary, indent?: string): string[];
|
|
65
75
|
/** "1 new file" / "3 new files", or "new files" when only a notice said so. */
|
|
@@ -18,9 +18,16 @@
|
|
|
18
18
|
* stops re-sending paused keys. The `path`/`paths`/`count` reads below consume
|
|
19
19
|
* that event; the `error`-message parsing stays for runners older than
|
|
20
20
|
* 6.18.17.
|
|
21
|
+
*
|
|
22
|
+
* US-024: the server's sentence wins. A `plan-limit` event carries the 402
|
|
23
|
+
* body's `message` (for a usage pause, the `usagePause` message hq-pro writes
|
|
24
|
+
* for the person who hit it), and a runner that forwards `usagePause` gives
|
|
25
|
+
* its message and `actionUrl` directly. The CLI adds no plan name and labels
|
|
26
|
+
* the link `Billing:`, never "Upgrade".
|
|
21
27
|
*/
|
|
22
28
|
import { createSyncProgressRecorder, } from "@indigoai-us/hq-cloud";
|
|
23
29
|
import { planLimitUpgradeFallbackUrl } from "./plan-limit-upgrade-url.js";
|
|
30
|
+
import { usagePauseFromBody } from "./usage-pause.js";
|
|
24
31
|
/** Upgrade destination when neither the event nor the error named one. */
|
|
25
32
|
const FALLBACK_UPGRADE_URL = "https://hq.computer/billing";
|
|
26
33
|
const PLAN_LIMIT_CODE_TOKEN = /\bcode=(?:PLAN_LIMIT_EXCEEDED|PLAN_LIMIT_REACHED|plan_limit_reached)\b/;
|
|
@@ -72,6 +79,7 @@ export function createPlanLimitRefusalTracker(opts = {}) {
|
|
|
72
79
|
let reportedCount = 0;
|
|
73
80
|
let message;
|
|
74
81
|
let upgradeUrl;
|
|
82
|
+
let usagePaused = false;
|
|
75
83
|
const addPath = (value) => {
|
|
76
84
|
if (typeof value === "string" && value && !paths.includes(value)) {
|
|
77
85
|
paths.push(value);
|
|
@@ -82,7 +90,17 @@ export function createPlanLimitRefusalTracker(opts = {}) {
|
|
|
82
90
|
const record = event;
|
|
83
91
|
if (event.type === "plan-limit") {
|
|
84
92
|
noticeSeen = true;
|
|
93
|
+
const pause = usagePauseFromBody(record);
|
|
94
|
+
if (pause) {
|
|
95
|
+
usagePaused = true;
|
|
96
|
+
message = pause.message;
|
|
97
|
+
if (pause.actionUrl)
|
|
98
|
+
upgradeUrl = pause.actionUrl;
|
|
99
|
+
}
|
|
85
100
|
upgradeUrl ??= safeHttpsUrl(record.upgradeUrl);
|
|
101
|
+
if (typeof record.message === "string" && record.message.trim()) {
|
|
102
|
+
message ??= record.message.trim();
|
|
103
|
+
}
|
|
86
104
|
addPath(record.path);
|
|
87
105
|
if (Array.isArray(record.paths))
|
|
88
106
|
record.paths.forEach(addPath);
|
|
@@ -111,6 +129,7 @@ export function createPlanLimitRefusalTracker(opts = {}) {
|
|
|
111
129
|
paths: [...paths],
|
|
112
130
|
...(message ? { message } : {}),
|
|
113
131
|
...(url ? { upgradeUrl: url } : {}),
|
|
132
|
+
...(usagePaused ? { usagePause: true } : {}),
|
|
114
133
|
};
|
|
115
134
|
},
|
|
116
135
|
};
|
|
@@ -141,22 +160,26 @@ export function mergePlanLimitRefusals(a, b) {
|
|
|
141
160
|
paths.push(p);
|
|
142
161
|
const message = a.message ?? b.message;
|
|
143
162
|
const upgradeUrl = a.upgradeUrl ?? b.upgradeUrl;
|
|
163
|
+
const usagePause = a.usagePause === true || b.usagePause === true;
|
|
144
164
|
return {
|
|
145
165
|
count: a.count + b.count,
|
|
146
166
|
paths,
|
|
147
167
|
...(message ? { message } : {}),
|
|
148
168
|
...(upgradeUrl ? { upgradeUrl } : {}),
|
|
169
|
+
...(usagePause ? { usagePause } : {}),
|
|
149
170
|
};
|
|
150
171
|
}
|
|
151
172
|
/** How many named paths to print before summarising the rest. */
|
|
152
173
|
const MAX_LISTED_PATHS = 20;
|
|
174
|
+
/** The sentence when the server sent none. Names no plan. */
|
|
175
|
+
export const REFUSAL_FALLBACK_SENTENCE = "New files are paused for this account. Open its Billing page to see why.";
|
|
153
176
|
/**
|
|
154
177
|
* Plain lines describing a refusal: what was not uploaded, why, and the
|
|
155
|
-
*
|
|
178
|
+
* billing link. Colour and the leading status line are the caller's.
|
|
156
179
|
*/
|
|
157
180
|
export function renderPlanLimitRefusalLines(summary, indent = " ") {
|
|
158
181
|
const lines = [];
|
|
159
|
-
lines.push(`${indent}${summary.message ??
|
|
182
|
+
lines.push(`${indent}${summary.message ?? REFUSAL_FALLBACK_SENTENCE}`);
|
|
160
183
|
if (summary.paths.length > 0) {
|
|
161
184
|
lines.push(`${indent}Not uploaded:`);
|
|
162
185
|
for (const p of summary.paths.slice(0, MAX_LISTED_PATHS)) {
|
|
@@ -170,7 +193,7 @@ export function renderPlanLimitRefusalLines(summary, indent = " ") {
|
|
|
170
193
|
lines.push(`${indent}New files stay on this machine and were not uploaded.`);
|
|
171
194
|
}
|
|
172
195
|
if (summary.upgradeUrl)
|
|
173
|
-
lines.push(`${indent}
|
|
196
|
+
lines.push(`${indent}Billing: ${summary.upgradeUrl}`);
|
|
174
197
|
return lines;
|
|
175
198
|
}
|
|
176
199
|
/** "1 new file" / "3 new files", or "new files" when only a notice said so. */
|
|
@@ -3,6 +3,14 @@ export interface PlanLimitUpgradeUrlChoices {
|
|
|
3
3
|
nestedUrl?: string | null;
|
|
4
4
|
legacyUrl: string;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* The console's self-serve switch to usage pricing for one company. While HQ
|
|
8
|
+
* Workforce is closed to companies not already on it (hq-pro
|
|
9
|
+
* `billing.legacy-plans-closed`, owner decision 2026-10-19) this is where an
|
|
10
|
+
* upgrade leads; the Workforce checkout refuses. Null when no console origin
|
|
11
|
+
* can be derived; without a slug the generic billing page.
|
|
12
|
+
*/
|
|
13
|
+
export declare function usageSwitchPageUrl(companySlug?: string): string | null;
|
|
6
14
|
/** Resolve and attribute the final selected company billing destination. */
|
|
7
15
|
export declare function selectPlanLimitUpgradeUrl(companySlug: string | undefined, choices: PlanLimitUpgradeUrlChoices): string;
|
|
8
16
|
export declare function withCliLimitEntrySurface(value: string): string;
|
|
@@ -11,6 +11,24 @@ function companyBillingUrl(companySlug) {
|
|
|
11
11
|
: "/billing";
|
|
12
12
|
return `${origin}${route}`;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The console's self-serve switch to usage pricing for one company. While HQ
|
|
16
|
+
* Workforce is closed to companies not already on it (hq-pro
|
|
17
|
+
* `billing.legacy-plans-closed`, owner decision 2026-10-19) this is where an
|
|
18
|
+
* upgrade leads; the Workforce checkout refuses. Null when no console origin
|
|
19
|
+
* can be derived; without a slug the generic billing page.
|
|
20
|
+
*/
|
|
21
|
+
export function usageSwitchPageUrl(companySlug) {
|
|
22
|
+
const controlPlaneUrl = process.env.HQ_VAULT_API_URL?.trim() || DEFAULT_VAULT_API_URL;
|
|
23
|
+
const origin = consoleOrigin(controlPlaneUrl);
|
|
24
|
+
if (!origin)
|
|
25
|
+
return null;
|
|
26
|
+
const slug = companySlug?.trim();
|
|
27
|
+
const route = slug
|
|
28
|
+
? `/companies/${encodeURIComponent(slug)}/billing/usage-switch?entrySurface=cli_limit`
|
|
29
|
+
: "/billing";
|
|
30
|
+
return `${origin}${route}`;
|
|
31
|
+
}
|
|
14
32
|
/** Resolve and attribute the final selected company billing destination. */
|
|
15
33
|
export function selectPlanLimitUpgradeUrl(companySlug, choices) {
|
|
16
34
|
const topLevelUrl = choices.topLevelUrl?.trim() || undefined;
|
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
* Copy rule (US-036): no string a customer reads may contain "locked" or
|
|
18
18
|
* "read-only". `plan-lock.test.ts` scans every rendered string for both.
|
|
19
19
|
*
|
|
20
|
+
* Copy rule (US-024): the flat plans are closed, so nothing here offers the
|
|
21
|
+
* Workforce plan, its price or the free month, and nothing says "Upgrade".
|
|
22
|
+
* The plan is named (Starter) only when hq-pro said the company is on an
|
|
23
|
+
* earlier plan (`billingState` or `pricingPlan: "current"`); an unknown plan
|
|
24
|
+
* gets copy that names no plan, and the way out besides trimming is the
|
|
25
|
+
* switch to usage pricing or the company's Billing page.
|
|
26
|
+
*
|
|
20
27
|
* An absent field is UNKNOWN, so nothing here ever infers over-limit from
|
|
21
28
|
* missing data (hq-absent-field-never-means-constraining-value).
|
|
22
29
|
*/
|
|
@@ -58,8 +65,18 @@ export interface PlanLimitStatus {
|
|
|
58
65
|
* which keeps the legacy copy.
|
|
59
66
|
*/
|
|
60
67
|
pricingPlan?: PricingPlan;
|
|
68
|
+
/**
|
|
69
|
+
* True when hq-pro says HQ Workforce is closed to this company (hq-flags
|
|
70
|
+
* `billing.legacy-plans-closed`, owner decision 2026-10-19). `upgradeUrl`
|
|
71
|
+
* is then the console's usage-pricing switch page, the free month never
|
|
72
|
+
* stands, and the renderers offer the switch instead of a Workforce
|
|
73
|
+
* upgrade. Absent keeps the legacy copy (older servers).
|
|
74
|
+
*/
|
|
75
|
+
legacyPlansClosed?: boolean;
|
|
61
76
|
checkedAt: string;
|
|
62
77
|
}
|
|
78
|
+
/** The way out while HQ Workforce is closed. Copy lives in ONE place. */
|
|
79
|
+
export declare const USAGE_SWITCH_LABEL = "Switch to usage pricing";
|
|
63
80
|
/** Starter member cap quoted when the server did not send a users.limit. */
|
|
64
81
|
export declare const STARTER_MEMBER_TARGET = 5;
|
|
65
82
|
/**
|
|
@@ -71,10 +88,8 @@ export declare const STARTER_INTEGRATION_TARGET = 3;
|
|
|
71
88
|
export declare const STARTER_SECRET_TARGET = 10;
|
|
72
89
|
/** Starter agent cap quoted when the server did not send an agents.limit. */
|
|
73
90
|
export declare const STARTER_AGENT_TARGET = 0;
|
|
74
|
-
/**
|
|
91
|
+
/** Billing destination quoted when the server did not send one. */
|
|
75
92
|
export declare const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
|
|
76
|
-
/** The paid plan the nag sends owners to. Copy lives in ONE place. */
|
|
77
|
-
export declare const WORKFORCE_PLAN_LABEL = "HQ Workforce ($500/mo)";
|
|
78
93
|
/**
|
|
79
94
|
* Pick the membership row for `companySlug` (or `cmp_…` uid) out of a decoded
|
|
80
95
|
* `/membership/me` body. No matching row is UNKNOWN. Does not read `planLock`.
|
|
@@ -133,9 +148,10 @@ export declare function renderPlanLimitNotice(status: PlanLimitStatus, opts?: {
|
|
|
133
148
|
*/
|
|
134
149
|
export declare function renderPlanLimitLine(status: PlanLimitStatus): string;
|
|
135
150
|
/**
|
|
136
|
-
* The `Plan: …` orientation line. Starter only
|
|
137
|
-
*
|
|
138
|
-
*
|
|
151
|
+
* The `Plan: …` orientation line. Starter only, and only when hq-pro said the
|
|
152
|
+
* company is on an earlier plan: a paid or enterprise workspace gets no line,
|
|
153
|
+
* and an UNKNOWN pricing plan names no plan. A usage-pricing company gets the
|
|
154
|
+
* usage line instead.
|
|
139
155
|
*/
|
|
140
156
|
export declare function renderPlanLine(status: PlanLimitStatus): string | null;
|
|
141
157
|
/**
|
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
* Copy rule (US-036): no string a customer reads may contain "locked" or
|
|
18
18
|
* "read-only". `plan-lock.test.ts` scans every rendered string for both.
|
|
19
19
|
*
|
|
20
|
+
* Copy rule (US-024): the flat plans are closed, so nothing here offers the
|
|
21
|
+
* Workforce plan, its price or the free month, and nothing says "Upgrade".
|
|
22
|
+
* The plan is named (Starter) only when hq-pro said the company is on an
|
|
23
|
+
* earlier plan (`billingState` or `pricingPlan: "current"`); an unknown plan
|
|
24
|
+
* gets copy that names no plan, and the way out besides trimming is the
|
|
25
|
+
* switch to usage pricing or the company's Billing page.
|
|
26
|
+
*
|
|
20
27
|
* An absent field is UNKNOWN, so nothing here ever infers over-limit from
|
|
21
28
|
* missing data (hq-absent-field-never-means-constraining-value).
|
|
22
29
|
*/
|
|
@@ -24,7 +31,7 @@ import chalk from "chalk";
|
|
|
24
31
|
import { diagnosticsEnabled } from "../diagnostics.js";
|
|
25
32
|
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
26
33
|
import { parseFreeMonth } from "./free-month.js";
|
|
27
|
-
import { selectPlanLimitUpgradeUrl } from "./plan-limit-upgrade-url.js";
|
|
34
|
+
import { selectPlanLimitUpgradeUrl, usageSwitchPageUrl, } from "./plan-limit-upgrade-url.js";
|
|
28
35
|
import { fetchCompanyPricingPlan, isUsageOnly, pricingPlanFromBody, pricingPlanFromMembership, usageOnlyPlanLine, usageOnlyUpgradeLines, } from "./usage-only.js";
|
|
29
36
|
// The free-month parser lives in a leaf module so the plan-gate 402 decoder can
|
|
30
37
|
// share it without importing the HTTP layer; re-exported so existing callers
|
|
@@ -41,6 +48,8 @@ export const PLAN_LIMIT_REASONS = [
|
|
|
41
48
|
"secrets",
|
|
42
49
|
"agents",
|
|
43
50
|
];
|
|
51
|
+
/** The way out while HQ Workforce is closed. Copy lives in ONE place. */
|
|
52
|
+
export const USAGE_SWITCH_LABEL = "Switch to usage pricing";
|
|
44
53
|
/** Starter member cap quoted when the server did not send a users.limit. */
|
|
45
54
|
export const STARTER_MEMBER_TARGET = 5;
|
|
46
55
|
/**
|
|
@@ -52,10 +61,8 @@ export const STARTER_INTEGRATION_TARGET = 3;
|
|
|
52
61
|
export const STARTER_SECRET_TARGET = 10;
|
|
53
62
|
/** Starter agent cap quoted when the server did not send an agents.limit. */
|
|
54
63
|
export const STARTER_AGENT_TARGET = 0;
|
|
55
|
-
/**
|
|
64
|
+
/** Billing destination quoted when the server did not send one. */
|
|
56
65
|
export const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
|
|
57
|
-
/** The paid plan the nag sends owners to. Copy lives in ONE place. */
|
|
58
|
-
export const WORKFORCE_PLAN_LABEL = "HQ Workforce ($500/mo)";
|
|
59
66
|
function finiteNumber(value) {
|
|
60
67
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
61
68
|
}
|
|
@@ -157,6 +164,17 @@ function selectUpgradeUrl(body, companySlug) {
|
|
|
157
164
|
const nestedUrl = typeof nested?.upgradeUrl === "string" && nested.upgradeUrl.trim().length > 0
|
|
158
165
|
? nested.upgradeUrl.trim()
|
|
159
166
|
: undefined;
|
|
167
|
+
// While HQ Workforce is closed the way out is the usage-pricing switch, not
|
|
168
|
+
// the company billing page the generated URL would land on. Build it from
|
|
169
|
+
// the slug when known; otherwise keep the server's own switch URL.
|
|
170
|
+
if (rec?.legacyPlansClosed === true) {
|
|
171
|
+
const slug = companySlug?.trim();
|
|
172
|
+
return (slug ? usageSwitchPageUrl(slug) : undefined) ??
|
|
173
|
+
topLevelUrl ??
|
|
174
|
+
nestedUrl ??
|
|
175
|
+
usageSwitchPageUrl(undefined) ??
|
|
176
|
+
DEFAULT_UPGRADE_URL;
|
|
177
|
+
}
|
|
160
178
|
return selectPlanLimitUpgradeUrl(companySlug, {
|
|
161
179
|
topLevelUrl,
|
|
162
180
|
nestedUrl,
|
|
@@ -210,6 +228,7 @@ function statusFromUsage(companySlug, companyUid, body, upgradeCompanySlug) {
|
|
|
210
228
|
upgradeUrl: selectUpgradeUrl(body, upgradeCompanySlug),
|
|
211
229
|
freeMonth: parseFreeMonth(rec?.freeMonth),
|
|
212
230
|
plan: selectPlan(body) ?? undefined,
|
|
231
|
+
...(rec?.legacyPlansClosed === true ? { legacyPlansClosed: true } : {}),
|
|
213
232
|
checkedAt: new Date().toISOString(),
|
|
214
233
|
}, pricingPlan);
|
|
215
234
|
}
|
|
@@ -295,6 +314,10 @@ export async function fetchPlanLimitStatus(token, companyRef, opts = {}) {
|
|
|
295
314
|
return null;
|
|
296
315
|
}
|
|
297
316
|
}
|
|
317
|
+
/** True when hq-pro said the company is on an earlier (flat) plan. */
|
|
318
|
+
function knownEarlierPlan(status) {
|
|
319
|
+
return status.pricingPlan === "current";
|
|
320
|
+
}
|
|
298
321
|
/** Why the workspace is over, one clause per reason. */
|
|
299
322
|
export function reasonLabel(reason) {
|
|
300
323
|
switch (reason) {
|
|
@@ -305,7 +328,7 @@ export function reasonLabel(reason) {
|
|
|
305
328
|
case "secrets":
|
|
306
329
|
return "too many secrets";
|
|
307
330
|
case "agents":
|
|
308
|
-
return "agents are not included
|
|
331
|
+
return "agents are not included";
|
|
309
332
|
}
|
|
310
333
|
}
|
|
311
334
|
function usageTarget(reason, status) {
|
|
@@ -357,7 +380,7 @@ function reasonDetail(reason, status) {
|
|
|
357
380
|
? `${row.used} of ${target} secrets`
|
|
358
381
|
: `over its ${target}-secret limit`;
|
|
359
382
|
case "agents":
|
|
360
|
-
return "agents are not included
|
|
383
|
+
return "agents are not included";
|
|
361
384
|
}
|
|
362
385
|
}
|
|
363
386
|
function usageLine(reason, status) {
|
|
@@ -368,20 +391,20 @@ function usageLine(reason, status) {
|
|
|
368
391
|
const members = row ?? status.members;
|
|
369
392
|
return members
|
|
370
393
|
? ` Members: ${members.used} of ${target}.`
|
|
371
|
-
: ` Members allowed
|
|
394
|
+
: ` Members allowed: ${target}.`;
|
|
372
395
|
}
|
|
373
396
|
case "integrations":
|
|
374
397
|
return row
|
|
375
398
|
? ` Integrations: ${row.used} of ${target}.`
|
|
376
|
-
: ` Integrations allowed
|
|
399
|
+
: ` Integrations allowed: ${target}.`;
|
|
377
400
|
case "secrets":
|
|
378
401
|
return row
|
|
379
402
|
? ` Secrets: ${row.used} of ${target}.`
|
|
380
|
-
: ` Secrets allowed
|
|
403
|
+
: ` Secrets allowed: ${target}.`;
|
|
381
404
|
case "agents":
|
|
382
405
|
return row
|
|
383
406
|
? ` Agents: ${row.used} of ${target}.`
|
|
384
|
-
: ` Agents allowed
|
|
407
|
+
: ` Agents allowed: ${target}.`;
|
|
385
408
|
}
|
|
386
409
|
}
|
|
387
410
|
/**
|
|
@@ -409,15 +432,17 @@ function capitalize(text) {
|
|
|
409
432
|
* about a dimension nobody measured.
|
|
410
433
|
*/
|
|
411
434
|
export function renderPlanLimitNotice(status, opts = {}) {
|
|
412
|
-
const { companySlug
|
|
435
|
+
const { companySlug } = status;
|
|
413
436
|
const lines = [];
|
|
414
437
|
if (isUsageOnly(status.pricingPlan)) {
|
|
415
438
|
return [`${companySlug}:`, ...usageOnlyUpgradeLines().map((l) => ` ${l}`)].join("\n");
|
|
416
439
|
}
|
|
417
|
-
lines.push(
|
|
440
|
+
lines.push(knownEarlierPlan(status)
|
|
441
|
+
? `HQ Starter — ${companySlug} is over its Starter limits.`
|
|
442
|
+
: `${companySlug} is over its limits.`);
|
|
418
443
|
const reasons = status.reasons.length
|
|
419
444
|
? status.reasons.map(reasonLabel).join("; ")
|
|
420
|
-
: "over
|
|
445
|
+
: "over its limits";
|
|
421
446
|
lines.push(` Why: ${reasons}.`);
|
|
422
447
|
for (const reason of status.reasons) {
|
|
423
448
|
lines.push(usageLine(reason, status));
|
|
@@ -426,21 +451,20 @@ export function renderPlanLimitNotice(status, opts = {}) {
|
|
|
426
451
|
lines.push(HARD_STOP_SENTENCE_LINES[1]);
|
|
427
452
|
const remedy = status.reasons.length
|
|
428
453
|
? status.reasons.map((reason) => reasonRemedy(reason, status)).join(", and ")
|
|
429
|
-
: "come back under
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
454
|
+
: "come back under its limits";
|
|
455
|
+
// The flat plans are closed: no Workforce offer and no free month (it led
|
|
456
|
+
// to the closed checkout). The second way out is the switch to usage
|
|
457
|
+
// pricing when the server said the company is on an earlier plan or that
|
|
458
|
+
// the flat plans are closed to it; otherwise the company's Billing page.
|
|
459
|
+
lines.push(" Two ways to sort it:");
|
|
460
|
+
lines.push(` 1. ${capitalize(remedy)}.`);
|
|
461
|
+
if (status.legacyPlansClosed || knownEarlierPlan(status)) {
|
|
462
|
+
lines.push(` 2. ${USAGE_SWITCH_LABEL}: no plan fee, and these limits stop applying.`);
|
|
437
463
|
}
|
|
438
464
|
else {
|
|
439
|
-
lines.push("
|
|
440
|
-
lines.push(` 1. ${capitalize(remedy)}.`);
|
|
441
|
-
lines.push(` 2. Upgrade to ${WORKFORCE_PLAN_LABEL}.`);
|
|
465
|
+
lines.push(" 2. Open the company's Billing page.");
|
|
442
466
|
}
|
|
443
|
-
lines.push(`
|
|
467
|
+
lines.push(` ${status.legacyPlansClosed ? "Switch" : "Billing"}: ${status.upgradeUrl}`);
|
|
444
468
|
if (opts.inviteTeammateEnabled === true &&
|
|
445
469
|
status.plan === "free" &&
|
|
446
470
|
status.over &&
|
|
@@ -459,18 +483,22 @@ export function renderPlanLimitLine(status) {
|
|
|
459
483
|
}
|
|
460
484
|
const detail = status.reasons.length
|
|
461
485
|
? status.reasons.map((reason) => reasonDetail(reason, status)).join("; ")
|
|
462
|
-
: "over
|
|
463
|
-
|
|
486
|
+
: "over its limits";
|
|
487
|
+
const limits = knownEarlierPlan(status) ? "its Starter limits" : "its limits";
|
|
488
|
+
return (`Company ${status.companySlug} is over ${limits} (${detail}). ` +
|
|
464
489
|
`New files and new secrets are paused until this is sorted: ${status.upgradeUrl}`);
|
|
465
490
|
}
|
|
466
491
|
/**
|
|
467
|
-
* The `Plan: …` orientation line. Starter only
|
|
468
|
-
*
|
|
469
|
-
*
|
|
492
|
+
* The `Plan: …` orientation line. Starter only, and only when hq-pro said the
|
|
493
|
+
* company is on an earlier plan: a paid or enterprise workspace gets no line,
|
|
494
|
+
* and an UNKNOWN pricing plan names no plan. A usage-pricing company gets the
|
|
495
|
+
* usage line instead.
|
|
470
496
|
*/
|
|
471
497
|
export function renderPlanLine(status) {
|
|
472
498
|
if (isUsageOnly(status.pricingPlan))
|
|
473
499
|
return usageOnlyPlanLine();
|
|
500
|
+
if (!knownEarlierPlan(status))
|
|
501
|
+
return status.over ? "Plan: over its limits" : null;
|
|
474
502
|
if (status.over)
|
|
475
503
|
return "Plan: Starter — over its limits";
|
|
476
504
|
if (status.plan !== "free")
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* - optionally, the live cloud file-history listing `hq storage status`
|
|
14
14
|
* uses, to split storage into current files and old versions and to see a
|
|
15
15
|
* prune right away (Storage Lens, behind usage-limits, updates daily).
|
|
16
|
+
* With the usage statement, the listing is a separate current-storage
|
|
17
|
+
* breakdown only; the statement's month-average storage meter is billed.
|
|
16
18
|
* usage-limits also answers `vaults` when the server counts them; without it
|
|
17
19
|
* the vault meter is `not-available`.
|
|
18
20
|
* The call-count meters (syncs, gateway calls, HQ MCP calls, agent runs) have
|
|
@@ -45,6 +47,16 @@ export interface MeterReading {
|
|
|
45
47
|
/** In billed units (GB for storage). Null unless measured. */
|
|
46
48
|
value: number | null;
|
|
47
49
|
state: MeterState;
|
|
50
|
+
/**
|
|
51
|
+
* The server's own priced amounts for this line (usage statement only).
|
|
52
|
+
* When present they are the bill: the statement is what is invoiced to the
|
|
53
|
+
* cent, so the line is never re-priced from the rate card.
|
|
54
|
+
*/
|
|
55
|
+
accrued?: {
|
|
56
|
+
monthToDateUsd: number;
|
|
57
|
+
projectedUsd: number;
|
|
58
|
+
waived?: boolean;
|
|
59
|
+
};
|
|
48
60
|
}
|
|
49
61
|
export interface StorageSplit {
|
|
50
62
|
/** Bytes in current files. */
|
|
@@ -60,7 +72,7 @@ export interface StorageSplit {
|
|
|
60
72
|
export interface MonthUsage {
|
|
61
73
|
scope: UsageScope;
|
|
62
74
|
/** Which source answered; lets a reader tell interim numbers from the rollup. */
|
|
63
|
-
source: "usage-limits" | "daily-rollup";
|
|
75
|
+
source: "usage-statement" | "usage-limits" | "daily-rollup";
|
|
64
76
|
/** Month-to-date window, UTC. */
|
|
65
77
|
period: {
|
|
66
78
|
start: string;
|
|
@@ -71,6 +83,8 @@ export interface MonthUsage {
|
|
|
71
83
|
storage?: StorageSplit;
|
|
72
84
|
/** True when the account's usage trial is running (meters waived in trial bill $0). Absent = unknown. */
|
|
73
85
|
trialActive?: boolean;
|
|
86
|
+
/** The included usage the usage statement applied. Absent: use the rate card's. */
|
|
87
|
+
allowanceUsd?: number;
|
|
74
88
|
}
|
|
75
89
|
/** One way to read a month of usage. The rollup becomes a second implementation. */
|
|
76
90
|
export interface MonthUsageSource {
|
|
@@ -119,6 +133,14 @@ export declare function meterReadingsFromDailyRows(rows: readonly UsageDailyRowL
|
|
|
119
133
|
* {@link readMonthUsage} once that route ships; the company scope only.
|
|
120
134
|
*/
|
|
121
135
|
export declare function dailyRollupSource(fetchRows: (scope: UsageScope, from: string, to: string) => Promise<readonly UsageDailyRowLike[] | null>): MonthUsageSource;
|
|
136
|
+
export declare function meterReadingsFromStatement(body: unknown): MeterReading[] | null;
|
|
137
|
+
/**
|
|
138
|
+
* The usage statement as a source (`GET /v1/billing/usage`, US-015). The
|
|
139
|
+
* company statement is owner-gated on the server; a person statement is the
|
|
140
|
+
* caller's own. `fetchStatement` resolves the decoded body, or null when the
|
|
141
|
+
* server did not answer.
|
|
142
|
+
*/
|
|
143
|
+
export declare function usageStatementSource(fetchStatement: (scope: UsageScope) => Promise<unknown | null>): MonthUsageSource;
|
|
122
144
|
/**
|
|
123
145
|
* The one function every caller uses to read a month of usage. Switch the
|
|
124
146
|
* default source here when the US-005 rollup has a read route.
|
|
@@ -151,7 +173,7 @@ export interface UsageBill {
|
|
|
151
173
|
daysElapsed: number;
|
|
152
174
|
daysInMonth: number;
|
|
153
175
|
}
|
|
154
|
-
export declare function computeBill(usage: MonthUsage, rates?: readonly UsageMeterRate[],
|
|
176
|
+
export declare function computeBill(usage: MonthUsage, rates?: readonly UsageMeterRate[], cardAllowanceUsd?: number): UsageBill;
|
|
155
177
|
export type FixId = "prune-old-versions" | "remove-unused-secrets" | "exclude-folders" | "turn-sync-off";
|
|
156
178
|
/**
|
|
157
179
|
* A fix is a pair of command lists. `preview` only reads (dry runs, listings);
|