@indigoai-us/hq-cli 5.118.0 → 5.118.2

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,41 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.118.2] — 2026-09-17
6
+
7
+ ### Changed
8
+
9
+ - The locked-workspace notice now says what you can still do. A locked
10
+ workspace is read-only for new things, but deleting still works — which
11
+ matters, because every fix the notice lists (remove members, disconnect
12
+ integrations, delete secrets, deprovision agents) is a delete. The notice
13
+ states that outright instead of leaving "read-only" to be guessed at.
14
+
15
+ - The plan-limit box names the plan: "HQ Starter plan limit exceeded" rather
16
+ than "HQ plan limit exceeded".
17
+
18
+ ### Added
19
+
20
+ - `hq agents provision --model` now sets the brain model during creation. A
21
+ model such as `gpt-5.5` or `grok-4.6` selects the agents-v2 runtime unless
22
+ `--provider` is specified, avoiding a later box recreation to change brains.
23
+
24
+ ## [5.118.1] — 2026-09-17
25
+
26
+ ### Fixed
27
+
28
+ - The locked-workspace notice now names the real reason. A workspace locked
29
+ because of secrets or agents no longer reads "over its 5-member limit"; each
30
+ reason gets its own explanation, its own Starter cap and its own fix, and a
31
+ lock with no recognised reason says only that the workspace is over the
32
+ Starter limits. The plan-limit warning also names the dimension it is quoting
33
+ ("users at 9/10 (90%) (members)").
34
+
35
+ - You can now remove an HQ Agents v2 agent from the CLI without bypassing its
36
+ safety check. `hq agents rm` accepts `--confirm-destroy-agents-v2
37
+ <instance-id>` alongside `--yes` and sends only the instance ID you name; HQ
38
+ still refuses an ID that does not match the live box.
39
+
5
40
  ## [5.118.0] — 2026-09-16
6
41
 
7
42
  ### Added
@@ -3812,7 +3812,10 @@ export declare const COMMAND_CATALOG: readonly [{
3812
3812
  readonly description: "Agent slug (defaults to a slug of <name>)";
3813
3813
  }, {
3814
3814
  readonly flags: "--provider <provider>";
3815
- readonly description: "Runtime: codex | grok | claude | agents-v2 (default codex). claude is subscription-only; agents-v2 boots its brain box (codex, or grok via a grok-* model) and receives the v2 runtime post-boot";
3815
+ readonly description: "Runtime: agents-v2 (recommended) | codex | grok | claude (legacy). A --model selects agents-v2 by default";
3816
+ }, {
3817
+ readonly flags: "--model <model>";
3818
+ readonly description: "Brain model, for example gpt-5.5 or grok-4.6";
3816
3819
  }, {
3817
3820
  readonly flags: "--auth-mode <mode>";
3818
3821
  readonly description: "Auth: subscription | apiKey (default subscription)";
@@ -4023,6 +4026,9 @@ export declare const COMMAND_CATALOG: readonly [{
4023
4026
  }, {
4024
4027
  readonly flags: "--yes";
4025
4028
  readonly description: "Confirm the irreversible teardown (required)";
4029
+ }, {
4030
+ readonly flags: "--confirm-destroy-agents-v2 <instance-id>";
4031
+ readonly description: "Confirm agents-v2 teardown by naming the live EC2 instance ID";
4026
4032
  }];
4027
4033
  readonly subcommands: readonly [];
4028
4034
  }, {
@@ -4926,7 +4926,11 @@ export const COMMAND_CATALOG = [
4926
4926
  },
4927
4927
  {
4928
4928
  "flags": "--provider <provider>",
4929
- "description": "Runtime: codex | grok | claude | agents-v2 (default codex). claude is subscription-only; agents-v2 boots its brain box (codex, or grok via a grok-* model) and receives the v2 runtime post-boot"
4929
+ "description": "Runtime: agents-v2 (recommended) | codex | grok | claude (legacy). A --model selects agents-v2 by default"
4930
+ },
4931
+ {
4932
+ "flags": "--model <model>",
4933
+ "description": "Brain model, for example gpt-5.5 or grok-4.6"
4930
4934
  },
4931
4935
  {
4932
4936
  "flags": "--auth-mode <mode>",
@@ -5205,6 +5209,10 @@ export const COMMAND_CATALOG = [
5205
5209
  {
5206
5210
  "flags": "--yes",
5207
5211
  "description": "Confirm the irreversible teardown (required)"
5212
+ },
5213
+ {
5214
+ "flags": "--confirm-destroy-agents-v2 <instance-id>",
5215
+ "description": "Confirm agents-v2 teardown by naming the live EC2 instance ID"
5208
5216
  }
5209
5217
  ],
5210
5218
  "subcommands": []
@@ -171,6 +171,7 @@ export interface ProvisionAgentInput {
171
171
  slug: string;
172
172
  codexAuthMode: "subscription" | "apiKey";
173
173
  provider?: "codex" | "grok" | "claude" | "agents-v2";
174
+ codexModel?: string;
174
175
  codexApiKey?: string;
175
176
  idempotencyKey: string;
176
177
  title?: string;
@@ -258,7 +259,7 @@ export type AgentTerminalResponse = ({
258
259
  };
259
260
  export declare function openAgentTerminal(token: string, agentUid: string, body: Record<string, unknown>): Promise<AgentTerminalResponse>;
260
261
  export declare function retryAgent(token: string, agentUid: string): Promise<Record<string, unknown>>;
261
- export declare function deprovisionAgent(token: string, agentUid: string): Promise<{
262
+ export declare function deprovisionAgent(token: string, agentUid: string, confirmDestroyAgentsV2?: string): Promise<{
262
263
  uid: string;
263
264
  setupState?: string;
264
265
  terminal?: boolean;
@@ -564,11 +564,14 @@ export async function retryAgent(token, agentUid) {
564
564
  method: "POST",
565
565
  });
566
566
  }
567
- export async function deprovisionAgent(token, agentUid) {
567
+ export async function deprovisionAgent(token, agentUid, confirmDestroyAgentsV2) {
568
568
  return agentsRequest({
569
569
  token,
570
570
  path: `/v1/agents/${encodeURIComponent(agentUid)}`,
571
571
  method: "DELETE",
572
+ body: confirmDestroyAgentsV2 === undefined
573
+ ? undefined
574
+ : { confirmDestroyAgentsV2 },
572
575
  });
573
576
  }
574
577
  export async function listAgentJobs(token, agentUid) {
@@ -1071,7 +1074,8 @@ export function registerAgentsCommand(program) {
1071
1074
  .description("Provision a new cloud agent (company-specific monthly price shown before creation)")
1072
1075
  .option("--company <slug>", "Company slug (resolves to companyUid)")
1073
1076
  .option("--slug <slug>", "Agent slug (defaults to a slug of <name>)")
1074
- .option("--provider <provider>", "Runtime: codex | grok | claude | agents-v2 (default codex). claude is subscription-only; agents-v2 boots its brain box (codex, or grok via a grok-* model) and receives the v2 runtime post-boot")
1077
+ .option("--provider <provider>", "Runtime: agents-v2 (recommended) | codex | grok | claude (legacy). A --model selects agents-v2 by default")
1078
+ .option("--model <model>", "Brain model, for example gpt-5.5 or grok-4.6")
1075
1079
  .option("--auth-mode <mode>", "Auth: subscription | apiKey (default subscription)", "subscription")
1076
1080
  .option("--api-key-env <VAR>", "Env var holding the API key for --auth-mode apiKey (never pass the key as a flag)")
1077
1081
  .option("--title <title>", "Org-chart job title")
@@ -1085,7 +1089,13 @@ export function registerAgentsCommand(program) {
1085
1089
  // unrecognised value, silently provisioning a billable box the operator
1086
1090
  // did not ask for. They now exit(1) instead — see parseEnumOption.
1087
1091
  const authMode = parseEnumOption(opts.authMode, VALID_AUTH_MODES, "--auth-mode") ?? "subscription";
1088
- const provider = parseEnumOption(opts.provider, VALID_PROVIDERS, "--provider");
1092
+ const explicitProvider = parseEnumOption(opts.provider, VALID_PROVIDERS, "--provider");
1093
+ if (opts.model !== undefined &&
1094
+ !/^[A-Za-z0-9._-]{1,64}$/.test(opts.model)) {
1095
+ console.error(chalk.red("--model must be a model identifier (letters, digits, dot, dash, underscore; max 64 chars)."));
1096
+ process.exit(1);
1097
+ }
1098
+ const provider = explicitProvider ?? (opts.model ? "agents-v2" : undefined);
1089
1099
  // claude is subscription-only on hq-pro (rejectIncompatibleProviderAuthMode
1090
1100
  // returns AGENT_PROVIDER_INCOMPATIBLE_WITH_AUTH_MODE). Catch it here so the
1091
1101
  // operator gets a direct message instead of a 400 from the control plane
@@ -1136,6 +1146,7 @@ export function registerAgentsCommand(program) {
1136
1146
  slug,
1137
1147
  codexAuthMode: authMode,
1138
1148
  ...(provider ? { provider } : {}),
1149
+ ...(opts.model ? { codexModel: opts.model } : {}),
1139
1150
  ...(codexApiKey ? { codexApiKey } : {}),
1140
1151
  idempotencyKey,
1141
1152
  ...(opts.title ? { title: opts.title } : {}),
@@ -1477,6 +1488,7 @@ export function registerAgentsCommand(program) {
1477
1488
  .description("Deprovision (permanently tear down) an agent")
1478
1489
  .option("--company <slug>", "Company slug (resolves to companyUid)")
1479
1490
  .option("--yes", "Confirm the irreversible teardown (required)")
1491
+ .option("--confirm-destroy-agents-v2 <instance-id>", "Confirm agents-v2 teardown by naming the live EC2 instance ID")
1480
1492
  .action(async function (agentUid, opts) {
1481
1493
  if (!opts.yes) {
1482
1494
  console.error(chalk.yellow(`This will permanently deprovision agent ${agentUid} and tear down its ` +
@@ -1486,7 +1498,7 @@ export function registerAgentsCommand(program) {
1486
1498
  }
1487
1499
  try {
1488
1500
  const token = (await resolveVaultCredential()).token;
1489
- const result = await deprovisionAgent(token, agentUid);
1501
+ const result = await deprovisionAgent(token, agentUid, opts.confirmDestroyAgentsV2);
1490
1502
  if (result.terminal === false) {
1491
1503
  console.log(chalk.yellow(`Deprovision in progress for agent ${agentUid} (not yet fully torn down — re-run to continue).`));
1492
1504
  }
@@ -2,8 +2,9 @@
2
2
  * `plan-lock` (starter-plan-hard-limits / US-011) — the CLI-side read + render
3
3
  * of the workspace plan lock.
4
4
  *
5
- * Starter (free) workspaces are capped at 5 members and 0 integrations. Going
6
- * over locks the workspace immediately: it becomes read-only until the owner
5
+ * Starter (free) workspaces are capped on four locking dimensions members,
6
+ * integrations, secrets and agents (owner decision 7, 2026-09-17; deployments
7
+ * and storage nag but never lock). Going over locks the workspace immediately: it becomes read-only until the owner
7
8
  * trims back under the caps or upgrades to HQ Workforce. The lock decision is
8
9
  * NOT made here — hq-pro's `src/billing/plan-lock.ts` is the single source of
9
10
  * truth and ships the answer on `GET /membership/me` as a per-company
@@ -15,17 +16,33 @@
15
16
  * nothing here ever infers a lock (or an unlock) from missing data
16
17
  * (hq-absent-field-never-means-constraining-value).
17
18
  */
18
- /** Mirror of hq-pro's `PlanLockReason`. */
19
- export type PlanLockReason = "users" | "integrations";
19
+ /**
20
+ * Mirror of hq-pro's `PlanLockReason`. Owner decision 7 (2026-09-17) fixes the
21
+ * locking set at these four: `deployments` and `storageBytes` are nag-only and
22
+ * never appear here. An unrecognised reason is dropped by `parsePlanLock`, so a
23
+ * server that adds a fifth dimension renders as the generic line rather than as
24
+ * a false claim about members.
25
+ */
26
+ export declare const PLAN_LOCK_REASONS: readonly ["users", "integrations", "secrets", "agents"];
27
+ export type PlanLockReason = (typeof PLAN_LOCK_REASONS)[number];
20
28
  /** Mirror of hq-pro's `PlanLock` wire shape (see src/billing/plan-lock.ts). */
21
29
  export interface PlanLock {
22
30
  locked: boolean;
23
31
  reasons: PlanLockReason[];
24
32
  upgradeUrl: string;
25
- fixOptions: {
26
- removeMembersTo: number;
27
- disconnectIntegrations: boolean;
28
- };
33
+ fixOptions: PlanLockFixOptions;
34
+ }
35
+ /**
36
+ * The remedies hq-pro ships with the lock. The two original fields are always
37
+ * present (defaulted when the server omits them) because shipped copy reads
38
+ * them unconditionally; every dimension added since is optional, so an older
39
+ * server simply leaves it absent and the renderer quotes the Starter cap.
40
+ */
41
+ export interface PlanLockFixOptions {
42
+ removeMembersTo: number;
43
+ disconnectIntegrations: boolean;
44
+ removeSecretsTo?: number;
45
+ deprovisionAgentsTo?: number;
29
46
  }
30
47
  /** Optional member decoration for the notice ("7 of 5 members"). */
31
48
  export interface PlanLockMembers {
@@ -47,6 +64,10 @@ export interface PlanLockStatus {
47
64
  }
48
65
  /** Starter member cap quoted when the server did not send `removeMembersTo`. */
49
66
  export declare const STARTER_MEMBER_TARGET = 5;
67
+ /** Starter secret cap quoted when the server did not send `removeSecretsTo`. */
68
+ export declare const STARTER_SECRET_TARGET = 10;
69
+ /** Starter agent cap quoted when the server did not send `deprovisionAgentsTo`. */
70
+ export declare const STARTER_AGENT_TARGET = 0;
50
71
  /** Upgrade destination quoted when the server did not send one. */
51
72
  export declare const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
52
73
  /** The paid plan the lock wall sends owners to. Copy lives in ONE place. */
@@ -78,10 +99,17 @@ export declare function selectMemberUsage(body: unknown): PlanLockMembers | null
78
99
  export declare function fetchPlanLockStatus(token: string, companyRef: string, opts?: {
79
100
  timeoutMs?: number;
80
101
  }): Promise<PlanLockStatus | null>;
102
+ /** Why the workspace locked, one clause per reason. */
103
+ export declare function reasonLabel(reason: PlanLockReason): string;
81
104
  /**
82
105
  * The full WORKSPACE LOCKED block: why it locked, where the workspace stands
83
- * against the cap, and the two fixes. Plain text — colour is applied by the
106
+ * against the cap, and the ways out. Plain text — colour is applied by the
84
107
  * caller so scripts capturing stdout get a clean block.
108
+ *
109
+ * Every line is derived from `lock.reasons`. A workspace locked on secrets is
110
+ * never told it has too many members, and an empty reason list (a server
111
+ * dimension this CLI does not know) renders a generic line rather than a claim
112
+ * about a dimension nobody measured.
85
113
  */
86
114
  export declare function renderPlanLockNotice(status: PlanLockStatus): string;
87
115
  /**
@@ -2,8 +2,9 @@
2
2
  * `plan-lock` (starter-plan-hard-limits / US-011) — the CLI-side read + render
3
3
  * of the workspace plan lock.
4
4
  *
5
- * Starter (free) workspaces are capped at 5 members and 0 integrations. Going
6
- * over locks the workspace immediately: it becomes read-only until the owner
5
+ * Starter (free) workspaces are capped on four locking dimensions members,
6
+ * integrations, secrets and agents (owner decision 7, 2026-09-17; deployments
7
+ * and storage nag but never lock). Going over locks the workspace immediately: it becomes read-only until the owner
7
8
  * trims back under the caps or upgrades to HQ Workforce. The lock decision is
8
9
  * NOT made here — hq-pro's `src/billing/plan-lock.ts` is the single source of
9
10
  * truth and ships the answer on `GET /membership/me` as a per-company
@@ -17,12 +18,35 @@
17
18
  */
18
19
  import chalk from "chalk";
19
20
  import { vaultApiFetch } from "../../utils/vault-api.js";
21
+ /**
22
+ * Mirror of hq-pro's `PlanLockReason`. Owner decision 7 (2026-09-17) fixes the
23
+ * locking set at these four: `deployments` and `storageBytes` are nag-only and
24
+ * never appear here. An unrecognised reason is dropped by `parsePlanLock`, so a
25
+ * server that adds a fifth dimension renders as the generic line rather than as
26
+ * a false claim about members.
27
+ */
28
+ export const PLAN_LOCK_REASONS = [
29
+ "users",
30
+ "integrations",
31
+ "secrets",
32
+ "agents",
33
+ ];
34
+ function isPlanLockReason(value) {
35
+ return PLAN_LOCK_REASONS.includes(value);
36
+ }
20
37
  /** Starter member cap quoted when the server did not send `removeMembersTo`. */
21
38
  export const STARTER_MEMBER_TARGET = 5;
39
+ /** Starter secret cap quoted when the server did not send `removeSecretsTo`. */
40
+ export const STARTER_SECRET_TARGET = 10;
41
+ /** Starter agent cap quoted when the server did not send `deprovisionAgentsTo`. */
42
+ export const STARTER_AGENT_TARGET = 0;
22
43
  /** Upgrade destination quoted when the server did not send one. */
23
44
  export const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
24
45
  /** The paid plan the lock wall sends owners to. Copy lives in ONE place. */
25
46
  export const WORKFORCE_PLAN_LABEL = "HQ Workforce ($500/mo)";
47
+ function finiteNumber(value) {
48
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
49
+ }
26
50
  function asRecord(value) {
27
51
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
28
52
  return null;
@@ -43,25 +67,32 @@ export function parsePlanLock(value) {
43
67
  const reasons = [];
44
68
  if (Array.isArray(rec.reasons)) {
45
69
  for (const reason of rec.reasons) {
46
- if (reason === "users" || reason === "integrations")
70
+ if (isPlanLockReason(reason))
47
71
  reasons.push(reason);
48
72
  }
49
73
  }
50
74
  const fix = asRecord(rec.fixOptions);
51
- const removeMembersTo = typeof fix?.removeMembersTo === "number" &&
52
- Number.isFinite(fix.removeMembersTo)
53
- ? fix.removeMembersTo
54
- : STARTER_MEMBER_TARGET;
75
+ const removeMembersTo = finiteNumber(fix?.removeMembersTo) ?? STARTER_MEMBER_TARGET;
76
+ const removeSecretsTo = finiteNumber(fix?.removeSecretsTo);
77
+ const deprovisionAgentsTo = finiteNumber(fix?.deprovisionAgentsTo);
78
+ const fixOptions = {
79
+ removeMembersTo,
80
+ disconnectIntegrations: fix?.disconnectIntegrations === true,
81
+ };
82
+ // Absent stays absent: an omitted remedy target is UNKNOWN, and the renderer
83
+ // quotes the published Starter cap rather than inventing a server answer.
84
+ if (removeSecretsTo !== null)
85
+ fixOptions.removeSecretsTo = removeSecretsTo;
86
+ if (deprovisionAgentsTo !== null) {
87
+ fixOptions.deprovisionAgentsTo = deprovisionAgentsTo;
88
+ }
55
89
  return {
56
90
  locked: rec.locked,
57
91
  reasons,
58
92
  upgradeUrl: typeof rec.upgradeUrl === "string" && rec.upgradeUrl.trim().length > 0
59
93
  ? rec.upgradeUrl.trim()
60
94
  : DEFAULT_UPGRADE_URL,
61
- fixOptions: {
62
- removeMembersTo,
63
- disconnectIntegrations: fix?.disconnectIntegrations === true,
64
- },
95
+ fixOptions,
65
96
  };
66
97
  }
67
98
  /**
@@ -163,35 +194,100 @@ export async function fetchPlanLockStatus(token, companyRef, opts = {}) {
163
194
  checkedAt: new Date().toISOString(),
164
195
  };
165
196
  }
166
- function reasonLabel(reason) {
167
- return reason === "users"
168
- ? "too many members"
169
- : "integrations are not included on Starter";
197
+ /** Why the workspace locked, one clause per reason. */
198
+ export function reasonLabel(reason) {
199
+ switch (reason) {
200
+ case "users":
201
+ return "too many members";
202
+ case "integrations":
203
+ return "integrations are not included on Starter";
204
+ case "secrets":
205
+ return "too many secrets";
206
+ case "agents":
207
+ return "agents are not included on Starter";
208
+ }
209
+ }
210
+ function secretTarget(lock) {
211
+ return lock.fixOptions.removeSecretsTo ?? STARTER_SECRET_TARGET;
212
+ }
213
+ function agentTarget(lock) {
214
+ return lock.fixOptions.deprovisionAgentsTo ?? STARTER_AGENT_TARGET;
215
+ }
216
+ /** The remedy sentence for one reason — what the owner does to clear it. */
217
+ function reasonRemedy(reason, lock) {
218
+ switch (reason) {
219
+ case "users":
220
+ return `remove members until you are at ${lock.fixOptions.removeMembersTo} or fewer`;
221
+ case "integrations":
222
+ return "disconnect the workspace's integrations";
223
+ case "secrets":
224
+ return `delete secrets until you are at ${secretTarget(lock)} or fewer`;
225
+ case "agents": {
226
+ const target = agentTarget(lock);
227
+ return target > 0
228
+ ? `deprovision agents until you are at ${target} or fewer`
229
+ : "deprovision the workspace's agents";
230
+ }
231
+ }
232
+ }
233
+ /** The short per-reason clause used inside the one-line turn notice. */
234
+ function reasonDetail(reason, status) {
235
+ const { lock, members } = status;
236
+ switch (reason) {
237
+ case "users": {
238
+ const target = lock.fixOptions.removeMembersTo;
239
+ return members
240
+ ? `${members.used} of ${target} members`
241
+ : `over its ${target}-member limit`;
242
+ }
243
+ case "integrations":
244
+ return "integrations are not included on Starter";
245
+ case "secrets":
246
+ return `over its ${secretTarget(status.lock)}-secret limit`;
247
+ case "agents":
248
+ return "agents are not included on Starter";
249
+ }
250
+ }
251
+ function capitalize(text) {
252
+ return text.charAt(0).toUpperCase() + text.slice(1);
170
253
  }
171
254
  /**
172
255
  * The full WORKSPACE LOCKED block: why it locked, where the workspace stands
173
- * against the cap, and the two fixes. Plain text — colour is applied by the
256
+ * against the cap, and the ways out. Plain text — colour is applied by the
174
257
  * caller so scripts capturing stdout get a clean block.
258
+ *
259
+ * Every line is derived from `lock.reasons`. A workspace locked on secrets is
260
+ * never told it has too many members, and an empty reason list (a server
261
+ * dimension this CLI does not know) renders a generic line rather than a claim
262
+ * about a dimension nobody measured.
175
263
  */
176
264
  export function renderPlanLockNotice(status) {
177
265
  const { lock, members, companySlug } = status;
178
- const target = lock.fixOptions.removeMembersTo;
179
266
  const lines = [];
180
267
  lines.push(`WORKSPACE LOCKED — ${companySlug} is over its Starter plan.`);
181
268
  const reasons = lock.reasons.length
182
269
  ? lock.reasons.map(reasonLabel).join("; ")
183
270
  : "over the Starter plan limits";
184
271
  lines.push(` Why: ${reasons}.`);
185
- if (members) {
186
- lines.push(` Members: ${members.used} of ${target}.`);
272
+ if (lock.reasons.includes("users")) {
273
+ const target = lock.fixOptions.removeMembersTo;
274
+ lines.push(members
275
+ ? ` Members: ${members.used} of ${target}.`
276
+ : ` Members allowed on Starter: ${target}.`);
277
+ }
278
+ if (lock.reasons.includes("secrets")) {
279
+ lines.push(` Secrets allowed on Starter: ${secretTarget(lock)}.`);
187
280
  }
188
- else {
189
- lines.push(` Members allowed on Starter: ${target}.`);
281
+ if (lock.reasons.includes("agents")) {
282
+ lines.push(` Agents allowed on Starter: ${agentTarget(lock)}.`);
190
283
  }
191
- lines.push(" This workspace is read-only until it is fixed. Two ways out:");
192
- lines.push(` 1. Remove members until you are at ${target} or fewer${lock.fixOptions.disconnectIntegrations
193
- ? ", and disconnect the workspace's integrations"
194
- : ""}.`);
284
+ lines.push(" Nothing has been deleted. This workspace is read-only until it is fixed:");
285
+ lines.push(" you can still delete things, but you cannot create, invite or upload.");
286
+ lines.push(" Two ways to fix it:");
287
+ const remedy = lock.reasons.length
288
+ ? lock.reasons.map((reason) => reasonRemedy(reason, lock)).join(", and ")
289
+ : "come back under the Starter plan limits";
290
+ lines.push(` 1. ${capitalize(remedy)}.`);
195
291
  lines.push(` 2. Upgrade to ${WORKFORCE_PLAN_LABEL}.`);
196
292
  lines.push(` Upgrade: ${lock.upgradeUrl}`);
197
293
  return lines.join("\n");
@@ -201,11 +297,12 @@ export function renderPlanLockNotice(status) {
201
297
  * Kept to a single line on purpose — it repeats each turn.
202
298
  */
203
299
  export function renderPlanLockLine(status) {
204
- const target = status.lock.fixOptions.removeMembersTo;
205
- const count = status.members
206
- ? `${status.members.used} of ${target} members`
207
- : `over its ${target}-member limit`;
208
- return (`Company ${status.companySlug} is locked on Starter (${count}). ` +
300
+ const detail = status.lock.reasons.length
301
+ ? status.lock.reasons
302
+ .map((reason) => reasonDetail(reason, status))
303
+ .join("; ")
304
+ : "over the Starter plan limits";
305
+ return (`Company ${status.companySlug} is locked on Starter (${detail}). ` +
209
306
  `Writes to HQ cloud will fail until fixed: ${status.lock.upgradeUrl}`);
210
307
  }
211
308
  /**
@@ -16,6 +16,14 @@
16
16
  * writes to stdout. Env off-switch: `HQ_NO_PLAN_LIMIT_NAG=1`.
17
17
  */
18
18
  export declare const PLAN_LIMIT_UPGRADE_URL = "https://hq.computer/billing/upgrade";
19
+ /**
20
+ * Plain-English name for each plan-limit resource key, so a nag line says which
21
+ * dimension is tight rather than only the wire key. The key itself stays in the
22
+ * copy — hooks and support transcripts match on it — and the label is appended
23
+ * only when it differs. An unknown key (a resource this CLI predates) renders
24
+ * as the bare key rather than a guess.
25
+ */
26
+ export declare const PLAN_LIMIT_DIMENSION_LABELS: Record<string, string>;
19
27
  export interface PlanLimitEntry {
20
28
  used: number;
21
29
  limit: number;
@@ -21,6 +21,25 @@ import * as os from "node:os";
21
21
  import * as path from "node:path";
22
22
  export const PLAN_LIMIT_UPGRADE_URL = "https://hq.computer/billing/upgrade";
23
23
  const DAY_MS = 24 * 60 * 60 * 1000;
24
+ /**
25
+ * Plain-English name for each plan-limit resource key, so a nag line says which
26
+ * dimension is tight rather than only the wire key. The key itself stays in the
27
+ * copy — hooks and support transcripts match on it — and the label is appended
28
+ * only when it differs. An unknown key (a resource this CLI predates) renders
29
+ * as the bare key rather than a guess.
30
+ */
31
+ export const PLAN_LIMIT_DIMENSION_LABELS = {
32
+ users: "members",
33
+ integrations: "integrations",
34
+ secrets: "secrets",
35
+ agents: "agents",
36
+ deployments: "deployments",
37
+ storageBytes: "storage",
38
+ };
39
+ function dimensionSuffix(key) {
40
+ const label = PLAN_LIMIT_DIMENSION_LABELS[key];
41
+ return label && label !== key ? ` (${label})` : "";
42
+ }
24
43
  /** Module-level last-seen cell — overwritten by each successful parse. */
25
44
  let lastSeen = null;
26
45
  /** Session dedupe for the ≥80% one-line warning. */
@@ -142,7 +161,7 @@ function formatPct(entry) {
142
161
  return `${Math.round((entry.used / entry.limit) * 100)}%`;
143
162
  }
144
163
  function formatEntryLine(key, entry) {
145
- return `${key} at ${entry.used}/${entry.limit} (${formatPct(entry)})`;
164
+ return `${key} at ${entry.used}/${entry.limit} (${formatPct(entry)})${dimensionSuffix(key)}`;
146
165
  }
147
166
  function readShownAt(statePath) {
148
167
  try {
@@ -171,9 +190,9 @@ function withinDayWindow(shownAt, nowMs) {
171
190
  return nowMs - shownAt < DAY_MS;
172
191
  }
173
192
  function buildOverBox(overEntries, upgradeUrl) {
174
- const title = "⚠ HQ plan limit exceeded";
193
+ const title = "⚠ HQ Starter plan limit exceeded";
175
194
  const upgrade = `Upgrade: ${upgradeUrl}`;
176
- const resourceLines = overEntries.map(([key, entry]) => ` ${key}: ${entry.used}/${entry.limit}`);
195
+ const resourceLines = overEntries.map(([key, entry]) => ` ${key}: ${entry.used}/${entry.limit}${dimensionSuffix(key)}`);
177
196
  const contentLines = [title, "", ...resourceLines, "", upgrade];
178
197
  const innerWidth = Math.max(...contentLines.map((l) => l.length), 40);
179
198
  const top = `┌${"─".repeat(innerWidth + 2)}┐`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.118.0",
3
+ "version": "5.118.2",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {