@indigoai-us/hq-cli 5.248.0 → 5.249.1

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 CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.249.1] — 2026-09-27
6
+
7
+ ## [5.249.0] — 2026-09-27
8
+
9
+ ### Fixed
10
+
11
+ - Lanes flags with no registry row now quietly resolve off. Actual registry
12
+ read failures still emit their `flag_read_failed` event.
13
+ - Plan-limit notices prefer the current usage-limits upgrade URL and can use
14
+ the company console billing page as a default-off hq-flags fallback.
15
+
5
16
  ## [5.248.0] — 2026-09-27
6
17
 
7
18
  ### Changed
@@ -17,6 +17,8 @@
17
17
  * "key" but is really OAuth-protected still connects on the first try.
18
18
  */
19
19
  import { Command } from "commander";
20
+ import { consoleOrigin } from "../utils/console-origin.js";
21
+ export { consoleOrigin };
20
22
  import type { AdminConnection } from "./integrations-core.js";
21
23
  import { type InstallInput } from "./integrations-api.js";
22
24
  interface ConnectOptions {
@@ -57,19 +59,6 @@ interface ConnectTarget {
57
59
  /** Human label for prompts and success lines. */
58
60
  label: string;
59
61
  }
60
- /**
61
- * The console that fronts the control plane this CLI is pointed at.
62
- *
63
- * A handoff must land on the console for the SAME backend, because that
64
- * console starts its own OAuth flow against its own backend. Sending a staging
65
- * session to the production console would connect the app in the wrong place —
66
- * so when the control plane is overridden and its console cannot be derived,
67
- * return undefined and say so rather than guess a host.
68
- *
69
- * `HQ_CONSOLE_URL` is the explicit override; otherwise the console is the
70
- * control-plane host with its `hqapi.` prefix removed.
71
- */
72
- export declare function consoleOrigin(vaultApiUrl?: string): string | undefined;
73
62
  /**
74
63
  * The console's Integrations page for a company, the console root when the
75
64
  * caller never named one (a single-membership session resolves the company
@@ -95,5 +84,4 @@ export declare function bringYourOwnClientHandoff(target: ConnectTarget, opts: C
95
84
  export declare function googleClientTypeHint(clientId: string, authorizationUrl: string): string | null;
96
85
  export declare function findAlreadyConnected(connections: readonly AdminConnection[], app: string): AdminConnection | undefined;
97
86
  export declare function registerConnectCommands(integrations: Command): void;
98
- export {};
99
87
  //# sourceMappingURL=integrations-connect.d.ts.map
@@ -18,7 +18,9 @@
18
18
  */
19
19
  import chalk from "chalk";
20
20
  import open from "open";
21
- import { DEFAULT_VAULT_API_URL, ensureCognitoIdToken, } from "../utils/cognito-session.js";
21
+ import { ensureCognitoIdToken } from "../utils/cognito-session.js";
22
+ import { consoleOrigin } from "../utils/console-origin.js";
23
+ export { consoleOrigin };
22
24
  import { getCompanyUid } from "../utils/vault-api.js";
23
25
  import { redactSensitiveUrl, redactUrlsInText } from "../utils/redact-url.js";
24
26
  import { IntegrationsCliError, bareProvider, connectionDomain, fetchAdminSurface, printJson, revokedConnectionDetails, resolveConnection, } from "./integrations-core.js";
@@ -536,37 +538,6 @@ async function connectViaOAuth(token, companyUid, target, opts) {
536
538
  listener.close();
537
539
  }
538
540
  }
539
- const PRODUCTION_CONSOLE_ORIGIN = "https://hq.computer";
540
- const PRODUCTION_VAULT_API_HOST = "hqapi.hq.computer";
541
- /**
542
- * The console that fronts the control plane this CLI is pointed at.
543
- *
544
- * A handoff must land on the console for the SAME backend, because that
545
- * console starts its own OAuth flow against its own backend. Sending a staging
546
- * session to the production console would connect the app in the wrong place —
547
- * so when the control plane is overridden and its console cannot be derived,
548
- * return undefined and say so rather than guess a host.
549
- *
550
- * `HQ_CONSOLE_URL` is the explicit override; otherwise the console is the
551
- * control-plane host with its `hqapi.` prefix removed.
552
- */
553
- export function consoleOrigin(vaultApiUrl = DEFAULT_VAULT_API_URL) {
554
- const explicit = process.env.HQ_CONSOLE_URL?.trim();
555
- if (explicit)
556
- return explicit.replace(/\/+$/, "");
557
- let api;
558
- try {
559
- api = new URL(vaultApiUrl);
560
- }
561
- catch {
562
- return undefined;
563
- }
564
- if (api.host === PRODUCTION_VAULT_API_HOST)
565
- return PRODUCTION_CONSOLE_ORIGIN;
566
- if (!api.hostname.startsWith("hqapi."))
567
- return undefined;
568
- return `${api.protocol}//${api.host.slice("hqapi.".length)}`;
569
- }
570
541
  /**
571
542
  * The console's Integrations page for a company, the console root when the
572
543
  * caller never named one (a single-membership session resolves the company
@@ -0,0 +1,13 @@
1
+ export declare const PLAN_LIMIT_UPGRADE_FALLBACK_FLAG = "cli.plan-limit-upgrade-fallback";
2
+ export declare const PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT = false;
3
+ export interface PlanLimitUpgradeUrlChoices {
4
+ topLevelUrl?: string | null;
5
+ nestedUrl?: string | null;
6
+ legacyUrl: string;
7
+ }
8
+ export declare function isPlanLimitUpgradeFallbackEnabled(): boolean;
9
+ /** Resolve a server URL without changing legacy behavior while the flag is off. */
10
+ export declare function selectPlanLimitUpgradeUrl(companySlug: string | undefined, choices: PlanLimitUpgradeUrlChoices): string;
11
+ /** Keep a caller's previous URL until the hq-flags rollout is enabled. */
12
+ export declare function planLimitUpgradeFallbackUrl(companySlug: string | undefined, previousFallbackUrl: string): string;
13
+ //# sourceMappingURL=plan-limit-upgrade-url.d.ts.map
@@ -0,0 +1,38 @@
1
+ import { resolveProcessFlagGate } from "../flag-registry.js";
2
+ import { DEFAULT_VAULT_API_URL } from "../../utils/cognito-session.js";
3
+ import { consoleOrigin } from "../../utils/console-origin.js";
4
+ export const PLAN_LIMIT_UPGRADE_FALLBACK_FLAG = "cli.plan-limit-upgrade-fallback";
5
+ export const PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT = false;
6
+ export function isPlanLimitUpgradeFallbackEnabled() {
7
+ return resolveProcessFlagGate(PLAN_LIMIT_UPGRADE_FALLBACK_FLAG, { fallback: PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT }, () => PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT);
8
+ }
9
+ function companyBillingUrl(companySlug) {
10
+ const controlPlaneUrl = process.env.HQ_VAULT_API_URL?.trim() || DEFAULT_VAULT_API_URL;
11
+ const origin = consoleOrigin(controlPlaneUrl);
12
+ if (!origin)
13
+ return null;
14
+ const slug = companySlug?.trim();
15
+ const route = slug
16
+ ? `/companies/${encodeURIComponent(slug)}/billing?upgrade=1`
17
+ : "/billing";
18
+ return `${origin}${route}`;
19
+ }
20
+ /** Resolve a server URL without changing legacy behavior while the flag is off. */
21
+ export function selectPlanLimitUpgradeUrl(companySlug, choices) {
22
+ if (!isPlanLimitUpgradeFallbackEnabled())
23
+ return choices.legacyUrl;
24
+ const topLevelUrl = choices.topLevelUrl?.trim() || undefined;
25
+ const nestedUrl = choices.nestedUrl?.trim() || undefined;
26
+ const generatedUrl = companyBillingUrl(companySlug);
27
+ if (companySlug?.trim()) {
28
+ return generatedUrl ?? topLevelUrl ?? nestedUrl ?? choices.legacyUrl;
29
+ }
30
+ return topLevelUrl ?? nestedUrl ?? generatedUrl ?? choices.legacyUrl;
31
+ }
32
+ /** Keep a caller's previous URL until the hq-flags rollout is enabled. */
33
+ export function planLimitUpgradeFallbackUrl(companySlug, previousFallbackUrl) {
34
+ return selectPlanLimitUpgradeUrl(companySlug, {
35
+ legacyUrl: previousFallbackUrl,
36
+ });
37
+ }
38
+ //# sourceMappingURL=plan-limit-upgrade-url.js.map
@@ -85,6 +85,7 @@ export declare function parseFreeMonth(value: unknown): PlanLimitFreeMonth | und
85
85
  */
86
86
  export declare function selectCompanyMembership(body: unknown, companyRef: string): {
87
87
  companyUid?: string;
88
+ companySlug?: string;
88
89
  } | null;
89
90
  /** Parse the resolved plan id out of a usage-limits body. Absent → null. */
90
91
  export declare function selectPlan(body: unknown): "free" | "paid" | "enterprise" | null;
@@ -22,6 +22,7 @@
22
22
  */
23
23
  import chalk from "chalk";
24
24
  import { vaultApiFetch } from "../../utils/vault-api.js";
25
+ import { selectPlanLimitUpgradeUrl } from "./plan-limit-upgrade-url.js";
25
26
  /**
26
27
  * Dimensions the bind notice names with a remedy. Owner decision 7 (2026-09-17)
27
28
  * fixes the nagging set at these four: `deployments` and `storageBytes` are
@@ -133,6 +134,7 @@ export function selectCompanyMembership(body, companyRef) {
133
134
  continue;
134
135
  return {
135
136
  companyUid: typeof row.companyUid === "string" ? row.companyUid : undefined,
137
+ companySlug: typeof row.companySlug === "string" ? row.companySlug : undefined,
136
138
  };
137
139
  }
138
140
  return null;
@@ -157,12 +159,20 @@ export function selectMemberUsage(body) {
157
159
  : STARTER_MEMBER_TARGET;
158
160
  return { used: users.used, limit };
159
161
  }
160
- function selectUpgradeUrl(body) {
162
+ function selectUpgradeUrl(body, companySlug) {
161
163
  const rec = asRecord(body);
162
- const raw = rec?.upgradeUrl;
163
- if (typeof raw === "string" && raw.trim().length > 0)
164
- return raw.trim();
165
- return DEFAULT_UPGRADE_URL;
164
+ const nested = asRecord(rec?.planLimits);
165
+ const topLevelUrl = typeof rec?.upgradeUrl === "string" && rec.upgradeUrl.trim().length > 0
166
+ ? rec.upgradeUrl.trim()
167
+ : undefined;
168
+ const nestedUrl = typeof nested?.upgradeUrl === "string" && nested.upgradeUrl.trim().length > 0
169
+ ? nested.upgradeUrl.trim()
170
+ : undefined;
171
+ return selectPlanLimitUpgradeUrl(companySlug, {
172
+ topLevelUrl,
173
+ nestedUrl,
174
+ legacyUrl: topLevelUrl ?? DEFAULT_UPGRADE_URL,
175
+ });
166
176
  }
167
177
  /**
168
178
  * Read over-limit dimensions from a usage-limits body. `planLimits` nested
@@ -195,7 +205,7 @@ export function selectUsages(body) {
195
205
  }
196
206
  return out;
197
207
  }
198
- function statusFromUsage(companySlug, companyUid, body) {
208
+ function statusFromUsage(companySlug, companyUid, body, upgradeCompanySlug) {
199
209
  const usages = selectUsages(body);
200
210
  const reasons = selectOverReasons(body);
201
211
  const rec = asRecord(body);
@@ -207,7 +217,7 @@ function statusFromUsage(companySlug, companyUid, body) {
207
217
  reasons,
208
218
  members: selectMemberUsage(body) ?? usages.users,
209
219
  usages,
210
- upgradeUrl: selectUpgradeUrl(body),
220
+ upgradeUrl: selectUpgradeUrl(body, upgradeCompanySlug),
211
221
  freeMonth: parseFreeMonth(rec?.freeMonth),
212
222
  plan: selectPlan(body) ?? undefined,
213
223
  checkedAt: new Date().toISOString(),
@@ -242,7 +252,9 @@ export async function fetchPlanLimitStatus(token, companyRef, opts = {}) {
242
252
  const body = await usage.json().catch(() => null);
243
253
  if (!body)
244
254
  return null;
245
- return statusFromUsage(companyRef, selected.companyUid, body);
255
+ const selectedSlug = selected.companySlug ??
256
+ (!companyRef.startsWith("cmp_") ? companyRef : undefined);
257
+ return statusFromUsage(companyRef, selected.companyUid, body, selectedSlug);
246
258
  }
247
259
  catch {
248
260
  return null;
@@ -10,12 +10,6 @@ class MissingFlagTokenError extends Error {
10
10
  this.name = "MissingFlagTokenError";
11
11
  }
12
12
  }
13
- class MissingFlagRowError extends Error {
14
- constructor(flagKey) {
15
- super(`The hq-flags registry did not resolve ${flagKey}`);
16
- this.name = "MissingFlagRowError";
17
- }
18
- }
19
13
  function errorClass(error) {
20
14
  return error instanceof Error ? error.name : "UnknownError";
21
15
  }
@@ -104,10 +98,7 @@ async function readRegistryValue(flagKey, endpoint, options) {
104
98
  const resolution = client.explain(flagKey, { fallback: false });
105
99
  if (failed)
106
100
  return false;
107
- if (resolution.source !== "snapshot" && resolution.source !== "local-override") {
108
- reportFailure(new MissingFlagRowError(flagKey));
109
- }
110
- else {
101
+ if (resolution.source === "snapshot" || resolution.source === "local-override") {
111
102
  enabled = resolution.value;
112
103
  }
113
104
  }
@@ -36,6 +36,8 @@ import chalk from "chalk";
36
36
  import * as fs from "node:fs";
37
37
  import * as os from "node:os";
38
38
  import * as path from "node:path";
39
+ import { resolveProcessFlagCompanyContext } from "./flag-registry.js";
40
+ import { isPlanLimitUpgradeFallbackEnabled, planLimitUpgradeFallbackUrl, selectPlanLimitUpgradeUrl, } from "./billing/plan-limit-upgrade-url.js";
39
41
  export const PLAN_LIMIT_UPGRADE_URL = "https://hq.computer/billing/upgrade";
40
42
  /**
41
43
  * Cadence of the over-limit per-turn line (US-036). Was once a day; the nag
@@ -155,6 +157,18 @@ function parsePlanLimits(body) {
155
157
  }
156
158
  : null;
157
159
  }
160
+ function parseNestedUpgradeUrl(body) {
161
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
162
+ return null;
163
+ }
164
+ const planLimits = body.planLimits;
165
+ if (planLimits === null ||
166
+ typeof planLimits !== "object" ||
167
+ Array.isArray(planLimits)) {
168
+ return null;
169
+ }
170
+ return parseUpgradeUrl(planLimits.upgradeUrl);
171
+ }
158
172
  /**
159
173
  * Defensively parse the top-level `thresholdCrossings` array.
160
174
  *
@@ -208,14 +222,31 @@ function parseThresholdCrossings(body) {
208
222
  export function recordPlanLimitStatus(body, opts = {}) {
209
223
  try {
210
224
  const status = parsePlanLimits(body);
225
+ const crossings = parseThresholdCrossings(body);
226
+ let resolvedUpgradeUrl = null;
227
+ if (status !== null || crossings.length > 0) {
228
+ const bodyRecord = body !== null && typeof body === "object" && !Array.isArray(body)
229
+ ? body
230
+ : null;
231
+ const topLevelUpgradeUrl = parseUpgradeUrl(bodyRecord?.upgradeUrl);
232
+ const nestedUpgradeUrl = parseNestedUpgradeUrl(body);
233
+ const companySlug = isPlanLimitUpgradeFallbackEnabled()
234
+ ? resolveProcessFlagCompanyContext().companySlug
235
+ : undefined;
236
+ resolvedUpgradeUrl = selectPlanLimitUpgradeUrl(companySlug, {
237
+ topLevelUrl: topLevelUpgradeUrl,
238
+ nestedUrl: nestedUpgradeUrl,
239
+ legacyUrl: status?.upgradeUrl ?? PLAN_LIMIT_UPGRADE_URL,
240
+ });
241
+ }
211
242
  if (status !== null) {
212
243
  const anyOver = Object.values(status.limits).some((e) => e.over);
213
- lastSeen = { ...status, anyOver };
244
+ lastSeen = { ...status, anyOver, upgradeUrl: resolvedUpgradeUrl };
214
245
  }
215
246
  // Independent of `planLimits`: the server can announce a crossing on a
216
247
  // response whose nag map this CLI could not parse, and the notice is the
217
248
  // one surface that must land inside a session that is already running.
218
- emitPlanLimitThresholdNotice(parseThresholdCrossings(body), status?.upgradeUrl ?? null, opts);
249
+ emitPlanLimitThresholdNotice(crossings, resolvedUpgradeUrl, opts);
219
250
  }
220
251
  catch {
221
252
  // Never throw from record path.
@@ -402,7 +433,8 @@ export function emitPlanLimitThresholdNotice(crossings, upgradeUrl, opts = {}) {
402
433
  const write = opts.write ?? ((s) => process.stderr.write(s));
403
434
  const statePath = opts.statePath ?? defaultStatePath();
404
435
  const stored = readNagState(statePath).notifiedThresholds ?? {};
405
- const resolvedUpgradeUrl = upgradeUrl ?? PLAN_LIMIT_UPGRADE_URL;
436
+ const resolvedUpgradeUrl = upgradeUrl ??
437
+ planLimitUpgradeFallbackUrl(resolveProcessFlagCompanyContext().companySlug, PLAN_LIMIT_UPGRADE_URL);
406
438
  const printed = {};
407
439
  for (const crossing of crossings) {
408
440
  const key = crossingKey(crossing);
@@ -440,7 +472,8 @@ export function emitPlanLimitNag(opts = {}) {
440
472
  const now = opts.now ?? (() => new Date());
441
473
  const statePath = opts.statePath ?? defaultStatePath();
442
474
  const { limits, anyOver, upgradeUrl } = lastSeen;
443
- const resolvedUpgradeUrl = upgradeUrl ?? PLAN_LIMIT_UPGRADE_URL;
475
+ const resolvedUpgradeUrl = upgradeUrl ??
476
+ planLimitUpgradeFallbackUrl(resolveProcessFlagCompanyContext().companySlug, PLAN_LIMIT_UPGRADE_URL);
444
477
  const entries = Object.entries(limits);
445
478
  if (entries.length === 0)
446
479
  return;
@@ -0,0 +1,3 @@
1
+ /** Keep process-wide flag tests independent of the developer machine binding. */
2
+ export declare function isolateFlagCompanyContext(): () => void;
3
+ //# sourceMappingURL=isolated-flag-company-context.d.ts.map
@@ -0,0 +1,18 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { vi } from "vitest";
5
+ /** Keep process-wide flag tests independent of the developer machine binding. */
6
+ export function isolateFlagCompanyContext() {
7
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), "hq-flag-company-context-"));
8
+ vi.stubEnv("HOME", home);
9
+ vi.stubEnv("HQ_COMPANY_UID", undefined);
10
+ vi.stubEnv("HQ_COMPANY_SLUG", undefined);
11
+ vi.stubEnv("HQ_SPAWN_COMPANY", undefined);
12
+ vi.stubEnv("HQ_SESSION_ID", undefined);
13
+ vi.stubEnv("HQ_AGENT_IDENTITY_FILE", "");
14
+ vi.stubEnv("HQ_ROOT", undefined);
15
+ vi.stubEnv("HQ_WORK_CONTEXT_ROOT", undefined);
16
+ return () => fs.rmSync(home, { recursive: true, force: true });
17
+ }
18
+ //# sourceMappingURL=isolated-flag-company-context.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The console that fronts the control plane this CLI is pointed at.
3
+ *
4
+ * A handoff must land on the console for the same backend. When an override is
5
+ * active, derive its console from the control-plane host instead of assuming
6
+ * production. `HQ_CONSOLE_URL` remains an explicit override.
7
+ */
8
+ export declare function consoleOrigin(vaultApiUrl?: string): string | undefined;
9
+ //# sourceMappingURL=console-origin.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { DEFAULT_VAULT_API_URL } from "./cognito-session.js";
2
+ const PRODUCTION_CONSOLE_ORIGIN = "https://hq.computer";
3
+ const PRODUCTION_VAULT_API_HOST = "hqapi.hq.computer";
4
+ /**
5
+ * The console that fronts the control plane this CLI is pointed at.
6
+ *
7
+ * A handoff must land on the console for the same backend. When an override is
8
+ * active, derive its console from the control-plane host instead of assuming
9
+ * production. `HQ_CONSOLE_URL` remains an explicit override.
10
+ */
11
+ export function consoleOrigin(vaultApiUrl = DEFAULT_VAULT_API_URL) {
12
+ const explicit = process.env.HQ_CONSOLE_URL?.trim();
13
+ if (explicit)
14
+ return explicit.replace(/\/+$/, "");
15
+ let api;
16
+ try {
17
+ api = new URL(vaultApiUrl);
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ if (api.host === PRODUCTION_VAULT_API_HOST)
23
+ return PRODUCTION_CONSOLE_ORIGIN;
24
+ if (!api.hostname.startsWith("hqapi."))
25
+ return undefined;
26
+ return `${api.protocol}//${api.host.slice("hqapi.".length)}`;
27
+ }
28
+ //# sourceMappingURL=console-origin.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.248.0",
3
+ "version": "5.249.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {