@indigoai-us/hq-cli 5.117.1 → 5.117.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,30 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.117.2] — 2026-09-16
6
+
7
+ ### Fixed
8
+
9
+ - External agents: the work-mesh listener no longer fails every refresh with
10
+ "Agent callers may only use DM, channel comms, and reaction routes". It now
11
+ re-reads the agent's own inbox on each doorbell instead of the person-only
12
+ conversation routes.
13
+ - External agents: `hq agent probe` no longer waits for a DM to itself to show up
14
+ in the inbox, which the server never delivers. The dm check passes when the
15
+ send is accepted and the agent inbox is readable.
16
+ - External agents: kit service logs no longer write every line twice when the
17
+ services run under a supervisor.
18
+
19
+ ### Changed
20
+
21
+ - HQ now calls the paid plan by its real name everywhere you see it. If your
22
+ workspace went over its Starter limit, the notice, the error you got when you
23
+ tried to write something, and the upgrade prompt used to give three different
24
+ names for the same plan — "HQ Workforce", "HQ Team", and "Team" — which made
25
+ it genuinely unclear what you were being asked to buy. They all say HQ
26
+ Workforce now. The free plan is called Starter rather than "free plan", to
27
+ match the pricing page.
28
+
5
29
  ## [5.117.1] — 2026-09-16
6
30
 
7
31
  ### Fixed
@@ -109,9 +109,9 @@ Structured storage for agent and app state — not a replacement for markdown kn
109
109
  | `hq db status --company {co}` | Create/open local SQLite at `~/.hq/db/{co}/vault.db` (WAL) |
110
110
  | `hq db sql --company {co} -- 'SELECT …'` | Query the company local DB (read-only by default) |
111
111
  | `hq db migrate --company {co} --hq-root {HQ}` | Apply reviewable SQL under `companies/{co}/db/migrations/` |
112
- | `hq db provision --company {co}` | Remote DB on **HQ Team** plan only (secrets never printed) |
112
+ | `hq db provision --company {co}` | Remote DB on **HQ Workforce** ($500/mo) or Enterprise only (secrets never printed) |
113
113
 
114
- Use local for single-machine agent skills and implementer standards. Use Team remote for multi-machine shared data and deploy SecretBinding. Never commit `*.db` files into the vault tree.
114
+ Use local for single-machine agent skills and implementer standards. Use the remote DB for multi-machine shared data and deploy SecretBinding. Never commit `*.db` files into the vault tree.
115
115
 
116
116
  ### HQ CLI: Direct messages (`hq dm`)
117
117
 
@@ -318,6 +318,47 @@ HQ/
318
318
  └── threads/ # Session threads + handoff.json
319
319
  ```
320
320
 
321
+ ## Plans and limits
322
+
323
+ HQ has three plans: **Starter**, **HQ Workforce** ($500/month per workspace), and
324
+ **Enterprise**.
325
+
326
+ Starter is free. It holds up to 5 members, and it does not include integrations
327
+ or MCP, Atlas, or telemetry. Everything else — knowledge, projects, policies,
328
+ workers, secrets, vault storage, `hq deploy`, and `hq share` — works the same as
329
+ on the paid plans.
330
+
331
+ ### When a workspace goes over
332
+
333
+ A Starter workspace locks as soon as it goes over 5 members. There is no grace
334
+ period and no countdown.
335
+
336
+ A locked workspace is read-only. Nothing is deleted or hidden, and everyone can
337
+ still read everything. Writes are refused until the workspace is back inside the
338
+ limits.
339
+
340
+ There are two ways out, and only the workspace owner can do either:
341
+
342
+ 1. Remove members until there are 5 or fewer. Removing someone from a workspace
343
+ does not delete their personal HQ or the work they saved into the workspace.
344
+ 2. Upgrade to HQ Workforce at $500/month from the workspace billing page.
345
+
346
+ Either one unlocks the workspace right away.
347
+
348
+ Existing integration connections on a locked Starter workspace are disabled, not
349
+ deleted. Upgrading turns them back on without reconnecting anything.
350
+
351
+ ### What you'll see
352
+
353
+ The owner gets one email when the workspace locks, a reminder at most once every
354
+ 7 days while it stays locked, and one confirmation when it unlocks. Billing mail
355
+ comes from billing@hqforwork.com, and billing links go to hq.computer, which is
356
+ the console.
357
+
358
+ In the console you'll see a lock banner, an upgrade wall, and a team roster that
359
+ counts members against the limit. In a Claude Code session, `/startwork` shows a
360
+ short notice on every turn while the workspace is over its limits.
361
+
321
362
  ## Meeting notes, signals & ontology
322
363
 
323
364
  HQ captures these **natively, per company** — check HQ first, not your email or a third-party notetaker.
@@ -334,7 +375,7 @@ HQ captures these **natively, per company** — check HQ first, not your email o
334
375
 
335
376
  **Your preference for "meeting notes":** defaults to HQ-native. To point a company at email instead, set `meeting_notes_source: email` in `companies/{co}/settings/knowledge/preferences.yaml` (global default lives in `personal/settings/knowledge-preferences.yaml`).
336
377
 
337
- > Signals extraction and the ontology gardener run on HQ cloud and will require HQ Pro once billing ships. Billing isn't live yet today these are provisioned per-company when you cloud-back it via `/designate-team`. Reference: `core/knowledge/public/hq-core/native-knowledge-stores.md`.
378
+ > Signals extraction and the ontology gardener run on HQ cloud and are part of HQ Workforce ($500/mo) and Enterprise. They are not included in the free Starter plan. Cloud-back a company with `/designate-team` to turn them on. Reference: `core/knowledge/public/hq-core/native-knowledge-stores.md`.
338
379
 
339
380
  ## Typical Session
340
381
 
@@ -148,7 +148,7 @@ Share-session URLs are encrypted single-use 15-minute capabilities — never per
148
148
 
149
149
  ## CLI: `hq db` (vault databases)
150
150
 
151
- Local SQLite per company (always) + optional remote Postgres-class on **HQ Team** ($500/mo). Guide: `core/knowledge/public/hq-core/vault-databases.md`. Requires `@indigoai-us/hq-cli` ≥ 5.62.0.
151
+ Local SQLite per company (always, Starter included) + optional remote Postgres-class on **HQ Workforce** ($500/mo) or Enterprise. Guide: `core/knowledge/public/hq-core/vault-databases.md`. Requires `@indigoai-us/hq-cli` ≥ 5.62.0.
152
152
 
153
153
  | Command | Use |
154
154
  |---------|-----|
@@ -800,6 +800,9 @@ export declare const COMMAND_CATALOG: readonly [{
800
800
  readonly options: readonly [{
801
801
  readonly flags: "--json";
802
802
  readonly description: "Output identity metadata as JSON (never tokens)";
803
+ }, {
804
+ readonly flags: "--company <slug>";
805
+ readonly description: "Also report this company's workspace plan state (Starter lock)";
803
806
  }];
804
807
  readonly subcommands: readonly [];
805
808
  }, {
@@ -1276,7 +1279,7 @@ export declare const COMMAND_CATALOG: readonly [{
1276
1279
  readonly subcommands: readonly [];
1277
1280
  }, {
1278
1281
  readonly name: "provision";
1279
- readonly description: "Provision (or re-bind) the company remote vault DB via HQ control plane (Team plan)";
1282
+ readonly description: "Provision (or re-bind) the company remote vault DB via HQ control plane (HQ Workforce plan)";
1280
1283
  readonly aliases: readonly [];
1281
1284
  readonly hidden: false;
1282
1285
  readonly usage: "[options]";
@@ -4603,7 +4606,7 @@ export declare const COMMAND_CATALOG: readonly [{
4603
4606
  readonly subcommands: readonly [];
4604
4607
  }, {
4605
4608
  readonly name: "upgrade";
4606
- readonly description: "Open Stripe Checkout to upgrade this company to HQ Team ($500/mo)";
4609
+ readonly description: "Open Stripe Checkout to upgrade this company to HQ Workforce ($500/mo)";
4607
4610
  readonly aliases: readonly [];
4608
4611
  readonly hidden: false;
4609
4612
  readonly usage: "[options]";
@@ -1021,6 +1021,10 @@ export const COMMAND_CATALOG = [
1021
1021
  {
1022
1022
  "flags": "--json",
1023
1023
  "description": "Output identity metadata as JSON (never tokens)"
1024
+ },
1025
+ {
1026
+ "flags": "--company <slug>",
1027
+ "description": "Also report this company's workspace plan state (Starter lock)"
1024
1028
  }
1025
1029
  ],
1026
1030
  "subcommands": []
@@ -1628,7 +1632,7 @@ export const COMMAND_CATALOG = [
1628
1632
  },
1629
1633
  {
1630
1634
  "name": "provision",
1631
- "description": "Provision (or re-bind) the company remote vault DB via HQ control plane (Team plan)",
1635
+ "description": "Provision (or re-bind) the company remote vault DB via HQ control plane (HQ Workforce plan)",
1632
1636
  "aliases": [],
1633
1637
  "hidden": false,
1634
1638
  "usage": "[options]",
@@ -5951,7 +5955,7 @@ export const COMMAND_CATALOG = [
5951
5955
  },
5952
5956
  {
5953
5957
  "name": "upgrade",
5954
- "description": "Open Stripe Checkout to upgrade this company to HQ Team ($500/mo)",
5958
+ "description": "Open Stripe Checkout to upgrade this company to HQ Workforce ($500/mo)",
5955
5959
  "aliases": [],
5956
5960
  "hidden": false,
5957
5961
  "usage": "[options]",
@@ -124,7 +124,7 @@ export function buildKitRuntime(service) {
124
124
  process.env.HQ_MACHINE_CREDS_FILE = paths.machineCredsPath;
125
125
  process.env.HQ_REQUIRE_MACHINE_IDENTITY = "1";
126
126
  process.env.HQ_VAULT_API_URL = creds.apiBaseUrl;
127
- const log = createKitLogger(paths, service, { echo: true });
127
+ const log = createKitLogger(paths, service, { echo: process.stdout.isTTY === true });
128
128
  const getToken = () => ensureCognitoToken({ tokenSource: "machine", interactive: false });
129
129
  return { paths, creds, config, getToken, log };
130
130
  }
@@ -168,6 +168,21 @@ export async function runKitService(service, rt) {
168
168
  refreshMs: rt.config.meshRefreshMs,
169
169
  getToken: rt.getToken,
170
170
  log: rt.log,
171
+ // Any doorbell (and the periodic refresh) re-polls the agent inbox, the
172
+ // only conversation source agent callers are authorized to read.
173
+ refetch: async () => {
174
+ const r = await pollInboxOnce({
175
+ paths: rt.paths,
176
+ agentUid: rt.creds.entityUid,
177
+ apiBaseUrl: rt.creds.apiBaseUrl,
178
+ pollMs: rt.config.inboxPollMs,
179
+ ack: rt.config.inboxAck,
180
+ getToken: rt.getToken,
181
+ log: rt.log,
182
+ });
183
+ if (!r.ok)
184
+ throw new Error("agent inbox poll failed; see inbox.log");
185
+ },
171
186
  // An inbox doorbell means "poll now" rather than waiting for the loop.
172
187
  onInboxDoorbell: () => pollInboxOnce({
173
188
  paths: rt.paths,
@@ -9,7 +9,7 @@
9
9
  * presence is live: a fresh last-heartbeat.json plus an ok
10
10
  * component-mesh stamp from the doorbell listener. (The company
11
11
  * roster route is owner/admin-only and 404s for agt_ callers.)
12
- * dm send a DM to self via POST /v1/notify/dm and read it back
12
+ * dm send a DM to self via POST /v1/notify/dm (accepted) and read the agent inbox (200)
13
13
  * through GET /v1/agents/{uid}/inbox (the agent read surface;
14
14
  * /v1/notify/thread is intentionally closed to agents)
15
15
  * secrets `GET /secrets/{companyUid}` answers 200
@@ -38,8 +38,6 @@ export interface ProbeResult {
38
38
  /** Seconds since the kit's last successful heartbeat, or null if never. */
39
39
  heartbeatAgeSeconds: number | null;
40
40
  }
41
- export declare const DM_ROUNDTRIP_TIMEOUT_MS = 20000;
42
- export declare const DM_ROUNDTRIP_POLL_MS = 2000;
43
41
  /** A heartbeat older than this does not count as live presence (3 missed 60 s beats). */
44
42
  export declare const PRESENCE_FRESH_SECONDS = 180;
45
43
  export interface ProbeDeps {
@@ -9,7 +9,7 @@
9
9
  * presence is live: a fresh last-heartbeat.json plus an ok
10
10
  * component-mesh stamp from the doorbell listener. (The company
11
11
  * roster route is owner/admin-only and 404s for agt_ callers.)
12
- * dm send a DM to self via POST /v1/notify/dm and read it back
12
+ * dm send a DM to self via POST /v1/notify/dm (accepted) and read the agent inbox (200)
13
13
  * through GET /v1/agents/{uid}/inbox (the agent read surface;
14
14
  * /v1/notify/thread is intentionally closed to agents)
15
15
  * secrets `GET /secrets/{companyUid}` answers 200
@@ -41,8 +41,6 @@ import { readKitConfig } from "../lib/agent-kit/kit-config.js";
41
41
  import { readLastHeartbeat } from "../lib/agent-kit/run/heartbeat.js";
42
42
  import { defaultRunPull, syncPullArgs } from "../lib/agent-kit/run/sync.js";
43
43
  import { requireExternalCreds } from "./agent-kit.js";
44
- export const DM_ROUNDTRIP_TIMEOUT_MS = 20_000;
45
- export const DM_ROUNDTRIP_POLL_MS = 2_000;
46
44
  /** A heartbeat older than this does not count as live presence (3 missed 60 s beats). */
47
45
  export const PRESENCE_FRESH_SECONDS = 180;
48
46
  function errText(err) {
@@ -59,7 +57,6 @@ export function heartbeatAgeSeconds(paths, now = () => new Date()) {
59
57
  }
60
58
  export async function runProbe(deps) {
61
59
  const now = deps.now ?? (() => new Date());
62
- const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
63
60
  const nonce = (deps.nonce ?? (() => randomBytes(6).toString("hex")))();
64
61
  const self = deps.creds.entityUid;
65
62
  const checks = [];
@@ -153,37 +150,22 @@ export async function runProbe(deps) {
153
150
  else {
154
151
  checks.push({ name: "work-mesh", ok: false, detail: "skipped: no token/company" });
155
152
  }
156
- // 4. dm round-trip: send via notify, read back via the agent inbox
153
+ // 4. dm: the send is accepted and the agent inbox is readable.
154
+ // The server never delivers a self-DM into the sender's own inbox
155
+ // (hq-pro-core notify-dm skips inbox delivery when recipient === sender),
156
+ // so a self round-trip can never succeed. Inbound delivery is proven by a
157
+ // teammate's DM, which the kit's inbox poller mirrors to inbox.jsonl.
157
158
  if (token) {
158
159
  try {
159
160
  const body = `hq agent probe ${nonce}`;
160
161
  await deps.sendDm(token, self, body);
161
- const deadline = now().getTime() + (deps.dmTimeoutMs ?? DM_ROUNDTRIP_TIMEOUT_MS);
162
- let found = false;
163
- let lastStatus = 0;
164
- for (;;) {
165
- const res = await deps.readInbox(token);
166
- lastStatus = res.status;
167
- if (res.status === 200 && inboxContainsNonce(res.body, nonce)) {
168
- found = true;
169
- break;
170
- }
171
- // A 4xx will not heal by polling (auth/route refusal): stop now.
172
- if (res.status >= 400 && res.status < 500)
173
- break;
174
- if (now().getTime() >= deadline)
175
- break;
176
- await sleep(DM_ROUNDTRIP_POLL_MS);
177
- }
178
- const secs = Math.round((deps.dmTimeoutMs ?? DM_ROUNDTRIP_TIMEOUT_MS) / 1000);
162
+ const res = await deps.readInbox(token);
179
163
  checks.push({
180
164
  name: "dm",
181
- ok: found,
182
- detail: found
183
- ? "sent to self via /v1/notify/dm and read back from /v1/agents/{uid}/inbox"
184
- : lastStatus === 200
185
- ? `sent but not visible in /v1/agents/{uid}/inbox after ${secs}s`
186
- : `sent but /v1/agents/{uid}/inbox answered ${lastStatus}`,
165
+ ok: res.status === 200,
166
+ detail: res.status === 200
167
+ ? "send accepted by /v1/notify/dm; /v1/agents/{uid}/inbox → 200 (self-DMs are not delivered to the inbox; a DM from a teammate confirms inbound)"
168
+ : `send accepted but /v1/agents/{uid}/inbox answered ${res.status}`,
187
169
  });
188
170
  }
189
171
  catch (err) {
@@ -110,7 +110,7 @@ export function registerBillingCommand(program) {
110
110
  });
111
111
  billing
112
112
  .command("upgrade")
113
- .description("Open Stripe Checkout to upgrade this company to HQ Team ($500/mo)")
113
+ .description("Open Stripe Checkout to upgrade this company to HQ Workforce ($500/mo)")
114
114
  .option("--company <slug>", "Company slug (resolves to companyUid)")
115
115
  .option("--no-browser", "Print the Stripe Checkout URL instead of opening it")
116
116
  .option("--json", "Emit the Checkout URL as JSON")
@@ -28,7 +28,7 @@ const defaultDeps = () => ({
28
28
  });
29
29
  export function registerDbProvisionCommand(db, depsFactory = defaultDeps) {
30
30
  db.command("provision")
31
- .description("Provision (or re-bind) the company remote vault DB via HQ control plane (Team plan)")
31
+ .description("Provision (or re-bind) the company remote vault DB via HQ control plane (HQ Workforce plan)")
32
32
  .requiredOption("--company <slug>", "Company slug")
33
33
  .option("--region <region>", "AWS region", "us-east-1")
34
34
  .action(async (opts) => {
@@ -65,9 +65,9 @@ export function registerDbProvisionCommand(db, depsFactory = defaultDeps) {
65
65
  throw error;
66
66
  const msg = error instanceof Error ? error.message : "Unknown error";
67
67
  const status = error.status;
68
- if (status === 402 || /PLAN_REQUIRED|Team plan|\$500/i.test(msg)) {
69
- console.error(chalk.red("Error:"), "Remote vault DB requires the HQ Team plan ($500/mo).");
70
- console.error(chalk.dim("Local databases still work: hq db status|sql|migrate — no Team plan required."));
68
+ if (status === 402 || /PLAN_REQUIRED|Team plan|Workforce|\$500/i.test(msg)) {
69
+ console.error(chalk.red("Error:"), "Remote vault DB requires the HQ Workforce plan ($500/mo).");
70
+ console.error(chalk.dim("Local databases still work: hq db status|sql|migrate — no HQ Workforce plan required."));
71
71
  }
72
72
  else if (status === 404 || /Unknown route|Not Found/i.test(msg)) {
73
73
  console.error(chalk.red("Error:"), "Remote DB control plane is not available yet (routes not deployed).");
@@ -111,8 +111,8 @@ async function handleApiError(res, json = false) {
111
111
  console.error(chalk.red(body.error ?? "Not found"));
112
112
  }
113
113
  else if (res.status === 402) {
114
- console.error(chalk.red("Meeting notetaker bots require the $500/mo HQ Team plan."));
115
- console.error(chalk.dim(body.message ?? body.error ?? "Upgrade your company to HQ Team to record meetings."));
114
+ console.error(chalk.red("Meeting notetaker bots require the $500/mo HQ Workforce plan."));
115
+ console.error(chalk.dim(body.message ?? body.error ?? "Upgrade your company to HQ Workforce to record meetings."));
116
116
  }
117
117
  else {
118
118
  console.error(chalk.red(`API error (${res.status}): ${body.error ?? res.statusText}`));
@@ -2,6 +2,7 @@
2
2
  * hq whoami — displays current user or 'not logged in'
3
3
  */
4
4
  import { Command } from 'commander';
5
+ import { type PlanLockStatus } from "../lib/billing/plan-lock.js";
5
6
  export interface WhoamiTokenIdentity {
6
7
  email?: string;
7
8
  sub?: string;
@@ -19,5 +20,11 @@ export interface WhoamiDisplayIdentity {
19
20
  * email with the token subject of the delegating machine.
20
21
  */
21
22
  export declare function resolveWhoamiIdentity(identity: WhoamiTokenIdentity): WhoamiDisplayIdentity;
23
+ /**
24
+ * Human rendering of the plan state: the full WORKSPACE LOCKED block when the
25
+ * workspace is locked, the one-line `Plan: Starter — …` orientation when it is
26
+ * Starter and healthy, and nothing at all otherwise.
27
+ */
28
+ export declare function renderWhoamiPlanBlock(status: PlanLockStatus): string | null;
22
29
  export declare function registerWhoamiCommand(program: Command): void;
23
30
  //# sourceMappingURL=whoami.d.ts.map
@@ -4,7 +4,8 @@
4
4
  import chalk from 'chalk';
5
5
  import { loadCachedTokens, isExpiring, loadMachineCreds, } from '@indigoai-us/hq-cloud';
6
6
  import { peekIdToken as decodeIdToken } from "../utils/id-token.js";
7
- import { loadMachineCachedTokens, resolveCognitoTokenSource } from "../utils/cognito-session.js";
7
+ import { ensureCognitoToken, loadMachineCachedTokens, resolveCognitoTokenSource, } from "../utils/cognito-session.js";
8
+ import { colorizePlanLockNotice, fetchPlanLockStatus, renderPlanLine, renderPlanLockNotice, } from "../lib/billing/plan-lock.js";
8
9
  /**
9
10
  * Resolves the person represented by an ID token without pairing a delegated
10
11
  * email with the token subject of the delegating machine.
@@ -40,11 +41,58 @@ function peekIdToken(idToken) {
40
41
  : undefined,
41
42
  };
42
43
  }
44
+ /**
45
+ * starter-plan-hard-limits / US-011 — read the workspace plan lock for
46
+ * `--company <slug>`.
47
+ *
48
+ * hq-pro owns the decision (`planLock` on GET /membership/me); this only reads
49
+ * it. Best-effort by construction: no session, a network failure, an older
50
+ * backend that sends no `planLock`, or a company the caller is not a member of
51
+ * all resolve to null — reported as UNKNOWN and rendered as nothing, never as
52
+ * a guessed lock or a guessed all-clear, and never as a command failure. The
53
+ * command exists to answer "who am I"; the plan line is decoration on top.
54
+ */
55
+ async function readPlanLock(company) {
56
+ const slug = company?.trim();
57
+ if (!slug)
58
+ return null;
59
+ try {
60
+ // NEVER interactive: this runs inside `--json` (before the authenticated
61
+ // check) and inside hq-core's per-turn plan-lock refresh hook. With the
62
+ // default `interactive: true`, a box with no cached session would launch a
63
+ // browser sign-in and block the caller instead of resolving to UNKNOWN.
64
+ const token = await ensureCognitoToken({ interactive: false });
65
+ return await fetchPlanLockStatus(token, slug);
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ /**
72
+ * Human rendering of the plan state: the full WORKSPACE LOCKED block when the
73
+ * workspace is locked, the one-line `Plan: Starter — …` orientation when it is
74
+ * Starter and healthy, and nothing at all otherwise.
75
+ */
76
+ export function renderWhoamiPlanBlock(status) {
77
+ if (status.lock.locked) {
78
+ return colorizePlanLockNotice(renderPlanLockNotice(status));
79
+ }
80
+ return renderPlanLine(status);
81
+ }
82
+ async function printPlanBlock(company) {
83
+ const status = await readPlanLock(company);
84
+ if (!status)
85
+ return;
86
+ const block = renderWhoamiPlanBlock(status);
87
+ if (block)
88
+ console.log(block);
89
+ }
43
90
  export function registerWhoamiCommand(program) {
44
91
  program
45
92
  .command('whoami')
46
93
  .description('Show the currently authenticated user')
47
94
  .option('--json', 'Output identity metadata as JSON (never tokens)')
95
+ .option('--company <slug>', 'Also report this company\'s workspace plan state (Starter lock)')
48
96
  .action(async (opts) => {
49
97
  try {
50
98
  // Effective source (same as auth status / ensureCognitoToken): env
@@ -67,6 +115,9 @@ export function registerWhoamiCommand(program) {
67
115
  const agentUid = machineCreds?.entityUid !== undefined
68
116
  ? (machineCreds.entityUid.startsWith('agt_') ? machineCreds.entityUid : null)
69
117
  : boundLegacyAgent;
118
+ // planLock is additive: absent-or-null means UNKNOWN, which is what
119
+ // every consumer already sees from a backend that predates it.
120
+ const planLock = await readPlanLock(opts.company);
70
121
  console.log(JSON.stringify({
71
122
  schemaVersion: 1,
72
123
  authenticated: machine ? Boolean(machineCreds) : Boolean(cached && !isExpiring(cached, 0)),
@@ -75,6 +126,7 @@ export function registerWhoamiCommand(program) {
75
126
  personUid: !machine && identity.entityType === 'person' ? identity.entityUid ?? null : null,
76
127
  agentUid,
77
128
  username: machineCreds?.username ?? null,
129
+ planLock,
78
130
  }, null, 2));
79
131
  return;
80
132
  }
@@ -88,6 +140,7 @@ export function registerWhoamiCommand(program) {
88
140
  : undefined;
89
141
  console.log(`Machine identity ${username}${entityUid ? ` (agent ${entityUid})` : ''} — sessions mint automatically`);
90
142
  console.log(`token source: machine`);
143
+ await printPlanBlock(opts.company);
91
144
  return;
92
145
  }
93
146
  if (!cached) {
@@ -101,6 +154,7 @@ export function registerWhoamiCommand(program) {
101
154
  }
102
155
  const { email, sub } = resolveWhoamiIdentity(peekIdToken(cached.idToken));
103
156
  console.log(`Logged in as ${email ?? 'unknown'}${sub ? ` (${sub})` : ''}`);
157
+ await printPlanBlock(opts.company);
104
158
  }
105
159
  catch (error) {
106
160
  console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error');
@@ -18,7 +18,6 @@
18
18
  import * as fs from "node:fs";
19
19
  import * as path from "node:path";
20
20
  import { vaultApiFetch } from "../../../utils/vault-api.js";
21
- import { warmMeshConversationCache } from "../../mesh/api.js";
22
21
  import { writeComponentStatus } from "../creds.js";
23
22
  export const SEEN_IDS_NAME = "seen-ids.json";
24
23
  export const INBOX_JSONL_NAME = "inbox.jsonl";
@@ -115,7 +114,8 @@ export function defaultAckItem(agentUid, apiBaseUrl) {
115
114
  export async function pollInboxOnce(deps) {
116
115
  const fetchInbox = deps.fetchInbox ?? defaultFetchInbox(deps.agentUid, deps.apiBaseUrl);
117
116
  const ackItem = deps.ackItem ?? defaultAckItem(deps.agentUid, deps.apiBaseUrl);
118
- const warmCache = deps.warmCache ?? ((token, uid) => warmMeshConversationCache(token, uid));
117
+ // No default cache warm: the human conversation routes 403 for agent callers.
118
+ const warmCache = deps.warmCache ?? (async () => { });
119
119
  let token;
120
120
  try {
121
121
  token = await deps.getToken();
@@ -20,7 +20,7 @@
20
20
  * is stamped ok while subscribed and refreshing, error otherwise.
21
21
  */
22
22
  import mqtt from "mqtt";
23
- import { warmMeshConversationCache } from "../../mesh/api.js";
23
+ import { defaultFetchInbox } from "./inbox.js";
24
24
  import { createPersonalRealtimeFetcher, MQTT_KEEPALIVE_SECONDS, renewalDelayMs, } from "../../mesh/live/daemon/credentials.js";
25
25
  import { presignIotWssUrl } from "../../mesh/live/daemon/presign.js";
26
26
  import { writeComponentStatus } from "../creds.js";
@@ -54,7 +54,16 @@ export async function startMeshListener(deps) {
54
54
  const setT = deps.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
55
55
  const clearT = deps.clearTimeout ?? ((h) => clearTimeout(h));
56
56
  const connect = deps.connect ?? ((url, opts) => mqtt.connect(url, opts));
57
- const refetch = deps.refetch ?? ((token, uid) => warmMeshConversationCache(token, uid));
57
+ // Agent callers are refused by the human conversation routes
58
+ // (/v1/notify/inbox|contacts|thread → 403 AGENT_ROUTE_NOT_SUPPORTED), so the
59
+ // default refetch reads the agent's own inbox, the agent-authorized source.
60
+ const refetch = deps.refetch ??
61
+ (async (token, uid) => {
62
+ const res = await defaultFetchInbox(uid, deps.apiBaseUrl)(token);
63
+ if (res.status !== 200) {
64
+ throw new Error(`GET ${res.url ?? "agent inbox"} → ${res.status}${res.bodyText ? ` body=${res.bodyText}` : ""}`);
65
+ }
66
+ });
58
67
  const fetchCredentials = deps.fetchCredentials ??
59
68
  (async () => {
60
69
  const token = await deps.getToken();
@@ -114,8 +114,8 @@ Rules:
114
114
  body: `# Work mesh status
115
115
 
116
116
  The work mesh is HQ's live board of who is working on what. It is the source
117
- of truth for project stories and active sessions; the kit's listener keeps a
118
- local cache under \`~/.hq/work-mesh/cache/\`.
117
+ of truth for project stories and active sessions. The kit's listener re-polls
118
+ this agent's inbox whenever a doorbell rings.
119
119
 
120
120
  \`\`\`bash
121
121
  hq mesh session status --company <slug> # who is working on what right now
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `plan-lock` (starter-plan-hard-limits / US-011) — the CLI-side read + render
3
+ * of the workspace plan lock.
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
7
+ * trims back under the caps or upgrades to HQ Workforce. The lock decision is
8
+ * NOT made here — hq-pro's `src/billing/plan-lock.ts` is the single source of
9
+ * truth and ships the answer on `GET /membership/me` as a per-company
10
+ * `planLock` object. This module only reads that field and renders it.
11
+ *
12
+ * Member counts are decoration, never a second opinion: the count comes from
13
+ * `GET /v1/billing/usage-limits` on a best-effort basis and its absence only
14
+ * removes the "n of 5" detail from the notice. An absent field is UNKNOWN, so
15
+ * nothing here ever infers a lock (or an unlock) from missing data
16
+ * (hq-absent-field-never-means-constraining-value).
17
+ */
18
+ /** Mirror of hq-pro's `PlanLockReason`. */
19
+ export type PlanLockReason = "users" | "integrations";
20
+ /** Mirror of hq-pro's `PlanLock` wire shape (see src/billing/plan-lock.ts). */
21
+ export interface PlanLock {
22
+ locked: boolean;
23
+ reasons: PlanLockReason[];
24
+ upgradeUrl: string;
25
+ fixOptions: {
26
+ removeMembersTo: number;
27
+ disconnectIntegrations: boolean;
28
+ };
29
+ }
30
+ /** Optional member decoration for the notice ("7 of 5 members"). */
31
+ export interface PlanLockMembers {
32
+ used: number;
33
+ limit: number;
34
+ }
35
+ export interface PlanLockStatus {
36
+ companySlug: string;
37
+ companyUid?: string;
38
+ lock: PlanLock;
39
+ members?: PlanLockMembers;
40
+ /**
41
+ * Resolved plan id, when the usage endpoint answered. UNKNOWN (absent) when
42
+ * it did not — surfaces that gate on "is this Starter?" must then stay
43
+ * silent rather than assume either way.
44
+ */
45
+ plan?: "free" | "paid" | "enterprise";
46
+ checkedAt: string;
47
+ }
48
+ /** Starter member cap quoted when the server did not send `removeMembersTo`. */
49
+ export declare const STARTER_MEMBER_TARGET = 5;
50
+ /** Upgrade destination quoted when the server did not send one. */
51
+ export declare const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
52
+ /** The paid plan the lock wall sends owners to. Copy lives in ONE place. */
53
+ export declare const WORKFORCE_PLAN_LABEL = "HQ Workforce ($500/mo)";
54
+ /**
55
+ * Defensively parse a `planLock` payload. Anything malformed returns null —
56
+ * the caller then behaves exactly as if the server had sent nothing, which is
57
+ * "unknown", not "locked".
58
+ */
59
+ export declare function parsePlanLock(value: unknown): PlanLock | null;
60
+ /**
61
+ * Pick the membership row for `companySlug` (or `cmp_…` uid) out of a decoded
62
+ * `/membership/me` body and return its parsed lock. No matching row — or no
63
+ * `planLock` on it — is UNKNOWN, so this returns null rather than guessing.
64
+ */
65
+ export declare function selectPlanLock(body: unknown, companyRef: string): {
66
+ lock: PlanLock;
67
+ companyUid?: string;
68
+ } | null;
69
+ /** Parse the resolved plan id out of a usage-limits body. Absent → null. */
70
+ export declare function selectPlan(body: unknown): "free" | "paid" | "enterprise" | null;
71
+ /** Parse the `users` dimension out of a usage-limits body. Absent → null. */
72
+ export declare function selectMemberUsage(body: unknown): PlanLockMembers | null;
73
+ /**
74
+ * Read the live lock for one company: `planLock` from `/membership/me`, plus a
75
+ * best-effort member count for the notice. Returns null when the server did not
76
+ * answer with a lock for this company — callers must then say nothing.
77
+ */
78
+ export declare function fetchPlanLockStatus(token: string, companyRef: string, opts?: {
79
+ timeoutMs?: number;
80
+ }): Promise<PlanLockStatus | null>;
81
+ /**
82
+ * 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
84
+ * caller so scripts capturing stdout get a clean block.
85
+ */
86
+ export declare function renderPlanLockNotice(status: PlanLockStatus): string;
87
+ /**
88
+ * The one-line form injected on every turn while the workspace stays locked.
89
+ * Kept to a single line on purpose — it repeats each turn.
90
+ */
91
+ export declare function renderPlanLockLine(status: PlanLockStatus): string;
92
+ /**
93
+ * The `Plan: …` orientation line. Starter only: a paid or enterprise
94
+ * workspace — and an UNKNOWN plan — gets no line at all.
95
+ */
96
+ export declare function renderPlanLine(status: PlanLockStatus): string | null;
97
+ /** Colourised block for interactive output. */
98
+ export declare function colorizePlanLockNotice(notice: string): string;
99
+ //# sourceMappingURL=plan-lock.d.ts.map
@@ -0,0 +1,230 @@
1
+ /**
2
+ * `plan-lock` (starter-plan-hard-limits / US-011) — the CLI-side read + render
3
+ * of the workspace plan lock.
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
7
+ * trims back under the caps or upgrades to HQ Workforce. The lock decision is
8
+ * NOT made here — hq-pro's `src/billing/plan-lock.ts` is the single source of
9
+ * truth and ships the answer on `GET /membership/me` as a per-company
10
+ * `planLock` object. This module only reads that field and renders it.
11
+ *
12
+ * Member counts are decoration, never a second opinion: the count comes from
13
+ * `GET /v1/billing/usage-limits` on a best-effort basis and its absence only
14
+ * removes the "n of 5" detail from the notice. An absent field is UNKNOWN, so
15
+ * nothing here ever infers a lock (or an unlock) from missing data
16
+ * (hq-absent-field-never-means-constraining-value).
17
+ */
18
+ import chalk from "chalk";
19
+ import { vaultApiFetch } from "../../utils/vault-api.js";
20
+ /** Starter member cap quoted when the server did not send `removeMembersTo`. */
21
+ export const STARTER_MEMBER_TARGET = 5;
22
+ /** Upgrade destination quoted when the server did not send one. */
23
+ export const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
24
+ /** The paid plan the lock wall sends owners to. Copy lives in ONE place. */
25
+ export const WORKFORCE_PLAN_LABEL = "HQ Workforce ($500/mo)";
26
+ function asRecord(value) {
27
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
28
+ return null;
29
+ }
30
+ return value;
31
+ }
32
+ /**
33
+ * Defensively parse a `planLock` payload. Anything malformed returns null —
34
+ * the caller then behaves exactly as if the server had sent nothing, which is
35
+ * "unknown", not "locked".
36
+ */
37
+ export function parsePlanLock(value) {
38
+ const rec = asRecord(value);
39
+ if (!rec)
40
+ return null;
41
+ if (typeof rec.locked !== "boolean")
42
+ return null;
43
+ const reasons = [];
44
+ if (Array.isArray(rec.reasons)) {
45
+ for (const reason of rec.reasons) {
46
+ if (reason === "users" || reason === "integrations")
47
+ reasons.push(reason);
48
+ }
49
+ }
50
+ const fix = asRecord(rec.fixOptions);
51
+ const removeMembersTo = typeof fix?.removeMembersTo === "number" &&
52
+ Number.isFinite(fix.removeMembersTo)
53
+ ? fix.removeMembersTo
54
+ : STARTER_MEMBER_TARGET;
55
+ return {
56
+ locked: rec.locked,
57
+ reasons,
58
+ upgradeUrl: typeof rec.upgradeUrl === "string" && rec.upgradeUrl.trim().length > 0
59
+ ? rec.upgradeUrl.trim()
60
+ : DEFAULT_UPGRADE_URL,
61
+ fixOptions: {
62
+ removeMembersTo,
63
+ disconnectIntegrations: fix?.disconnectIntegrations === true,
64
+ },
65
+ };
66
+ }
67
+ /**
68
+ * Pick the membership row for `companySlug` (or `cmp_…` uid) out of a decoded
69
+ * `/membership/me` body and return its parsed lock. No matching row — or no
70
+ * `planLock` on it — is UNKNOWN, so this returns null rather than guessing.
71
+ */
72
+ export function selectPlanLock(body, companyRef) {
73
+ const rec = asRecord(body);
74
+ const rows = Array.isArray(rec?.memberships) ? rec.memberships : [];
75
+ const ref = companyRef.trim().toLowerCase();
76
+ for (const raw of rows) {
77
+ const row = asRecord(raw);
78
+ if (!row)
79
+ continue;
80
+ if (typeof row.status === "string" && row.status !== "active")
81
+ continue;
82
+ const slug = typeof row.companySlug === "string" ? row.companySlug.toLowerCase() : "";
83
+ const uid = typeof row.companyUid === "string" ? row.companyUid.toLowerCase() : "";
84
+ if (slug !== ref && uid !== ref)
85
+ continue;
86
+ const lock = parsePlanLock(row.planLock);
87
+ if (!lock)
88
+ return null;
89
+ return {
90
+ lock,
91
+ companyUid: typeof row.companyUid === "string" ? row.companyUid : undefined,
92
+ };
93
+ }
94
+ return null;
95
+ }
96
+ /** Parse the resolved plan id out of a usage-limits body. Absent → null. */
97
+ export function selectPlan(body) {
98
+ const plan = asRecord(body)?.plan;
99
+ return plan === "free" || plan === "paid" || plan === "enterprise"
100
+ ? plan
101
+ : null;
102
+ }
103
+ /** Parse the `users` dimension out of a usage-limits body. Absent → null. */
104
+ export function selectMemberUsage(body) {
105
+ const users = asRecord(asRecord(body)?.users);
106
+ if (!users)
107
+ return null;
108
+ if (typeof users.used !== "number" || !Number.isFinite(users.used)) {
109
+ return null;
110
+ }
111
+ const limit = typeof users.limit === "number" && Number.isFinite(users.limit)
112
+ ? users.limit
113
+ : STARTER_MEMBER_TARGET;
114
+ return { used: users.used, limit };
115
+ }
116
+ /**
117
+ * Read the live lock for one company: `planLock` from `/membership/me`, plus a
118
+ * best-effort member count for the notice. Returns null when the server did not
119
+ * answer with a lock for this company — callers must then say nothing.
120
+ */
121
+ export async function fetchPlanLockStatus(token, companyRef, opts = {}) {
122
+ const timeoutMs = opts.timeoutMs ?? 8000;
123
+ const res = await vaultApiFetch({
124
+ token,
125
+ path: "/membership/me",
126
+ signal: AbortSignal.timeout(timeoutMs),
127
+ });
128
+ if (!res.ok)
129
+ return null;
130
+ const selected = selectPlanLock(await res.json().catch(() => null), companyRef);
131
+ if (!selected)
132
+ return null;
133
+ let members;
134
+ let plan;
135
+ // Decoration only. The lock answer above is authoritative; the usage read
136
+ // adds the member count and the plan id that the notices and the `Plan:`
137
+ // orientation line quote. A failure here must never suppress — or invent —
138
+ // a lock, so every field it feeds stays optional.
139
+ if (selected.companyUid) {
140
+ try {
141
+ const usage = await vaultApiFetch({
142
+ token,
143
+ path: "/v1/billing/usage-limits",
144
+ query: { companyUid: selected.companyUid },
145
+ signal: AbortSignal.timeout(timeoutMs),
146
+ });
147
+ if (usage.ok) {
148
+ const body = await usage.json().catch(() => null);
149
+ members = selectMemberUsage(body) ?? undefined;
150
+ plan = selectPlan(body) ?? undefined;
151
+ }
152
+ }
153
+ catch {
154
+ // Network/timeout: leave both unknown.
155
+ }
156
+ }
157
+ return {
158
+ companySlug: companyRef,
159
+ companyUid: selected.companyUid,
160
+ lock: selected.lock,
161
+ members,
162
+ plan,
163
+ checkedAt: new Date().toISOString(),
164
+ };
165
+ }
166
+ function reasonLabel(reason) {
167
+ return reason === "users"
168
+ ? "too many members"
169
+ : "integrations are not included on Starter";
170
+ }
171
+ /**
172
+ * 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
174
+ * caller so scripts capturing stdout get a clean block.
175
+ */
176
+ export function renderPlanLockNotice(status) {
177
+ const { lock, members, companySlug } = status;
178
+ const target = lock.fixOptions.removeMembersTo;
179
+ const lines = [];
180
+ lines.push(`WORKSPACE LOCKED — ${companySlug} is over its Starter plan.`);
181
+ const reasons = lock.reasons.length
182
+ ? lock.reasons.map(reasonLabel).join("; ")
183
+ : "over the Starter plan limits";
184
+ lines.push(` Why: ${reasons}.`);
185
+ if (members) {
186
+ lines.push(` Members: ${members.used} of ${target}.`);
187
+ }
188
+ else {
189
+ lines.push(` Members allowed on Starter: ${target}.`);
190
+ }
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
+ : ""}.`);
195
+ lines.push(` 2. Upgrade to ${WORKFORCE_PLAN_LABEL}.`);
196
+ lines.push(` Upgrade: ${lock.upgradeUrl}`);
197
+ return lines.join("\n");
198
+ }
199
+ /**
200
+ * The one-line form injected on every turn while the workspace stays locked.
201
+ * Kept to a single line on purpose — it repeats each turn.
202
+ */
203
+ 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}). ` +
209
+ `Writes to HQ cloud will fail until fixed: ${status.lock.upgradeUrl}`);
210
+ }
211
+ /**
212
+ * The `Plan: …` orientation line. Starter only: a paid or enterprise
213
+ * workspace — and an UNKNOWN plan — gets no line at all.
214
+ */
215
+ export function renderPlanLine(status) {
216
+ if (status.lock.locked)
217
+ return "Plan: Starter — LOCKED";
218
+ if (status.plan !== "free")
219
+ return null;
220
+ const target = status.lock.fixOptions.removeMembersTo;
221
+ if (status.members) {
222
+ return `Plan: Starter — ${status.members.used} of ${target} members`;
223
+ }
224
+ return `Plan: Starter — ${target} members included`;
225
+ }
226
+ /** Colourised block for interactive output. */
227
+ export function colorizePlanLockNotice(notice) {
228
+ return chalk.red(notice);
229
+ }
230
+ //# sourceMappingURL=plan-lock.js.map
@@ -224,7 +224,7 @@ export function emitPlanLimitNag(opts = {}) {
224
224
  if (worst === null)
225
225
  return;
226
226
  warningShownThisSession = true;
227
- const line = `⚠ HQ free plan: ${formatEntryLine(worst.key, worst.entry)}. Upgrade: ${resolvedUpgradeUrl}`;
227
+ const line = `⚠ HQ Starter plan: ${formatEntryLine(worst.key, worst.entry)}. Upgrade: ${resolvedUpgradeUrl}`;
228
228
  write(chalk.yellow(line) + "\n");
229
229
  }
230
230
  catch {
@@ -1,4 +1,4 @@
1
- const TEAM_UPGRADE_HINT = "Run `hq billing upgrade` to move to Team.";
1
+ const TEAM_UPGRADE_HINT = "Run `hq billing upgrade` to move to HQ Workforce.";
2
2
  export class PlanGateError extends Error {
3
3
  code;
4
4
  details;
@@ -23,19 +23,19 @@ function formatPlanGateDetails(code, details) {
23
23
  typeof details.used === "number" &&
24
24
  typeof details.limit === "number") {
25
25
  const upgrade = typeof details.upgradeUrl === "string"
26
- ? `Upgrade to HQ Team ($500/mo) to remove limits: ${details.upgradeUrl}`
27
- : "Upgrade to HQ Team ($500/mo) to remove limits.";
26
+ ? `Upgrade to HQ Workforce ($500/mo) to remove limits: ${details.upgradeUrl}`
27
+ : "Upgrade to HQ Workforce ($500/mo) to remove limits.";
28
28
  return [
29
- `Free plan limit reached: ${details.resource} ${details.used}/${details.limit} used.`,
29
+ `Starter plan limit reached: ${details.resource} ${details.used}/${details.limit} used.`,
30
30
  upgrade,
31
31
  existingResourcesNote,
32
32
  ].join("\n");
33
33
  }
34
34
  const upgrade = typeof details.upgradeUrl === "string"
35
- ? `Upgrade to HQ Team ($500/mo) to remove limits: ${details.upgradeUrl}`
36
- : "Upgrade to HQ Team ($500/mo) to remove limits.";
35
+ ? `Upgrade to HQ Workforce ($500/mo) to remove limits: ${details.upgradeUrl}`
36
+ : "Upgrade to HQ Workforce ($500/mo) to remove limits.";
37
37
  return [
38
- "HQ Team plan required for this feature.",
38
+ "HQ Workforce plan required for this feature.",
39
39
  upgrade,
40
40
  existingResourcesNote,
41
41
  ].join("\n");
@@ -56,7 +56,7 @@ export async function offerTeamUpgrade(onConfirm, deps = {}) {
56
56
  if (!(deps.interactive ?? canOfferTeamUpgrade(deps.argv)))
57
57
  return false;
58
58
  if (deps.ask) {
59
- const answer = await deps.ask("Upgrade to HQ Team ($500/mo) now? [y/N] ");
59
+ const answer = await deps.ask("Upgrade to HQ Workforce ($500/mo) now? [y/N] ");
60
60
  if (!/^y(?:es)?$/i.test(answer.trim()))
61
61
  return false;
62
62
  await onConfirm();
@@ -65,7 +65,7 @@ export async function offerTeamUpgrade(onConfirm, deps = {}) {
65
65
  const { createInterface } = await import("node:readline/promises");
66
66
  const readline = createInterface({ input: process.stdin, output: process.stderr });
67
67
  try {
68
- const answer = await readline.question("Upgrade to HQ Team ($500/mo) now? [y/N] ");
68
+ const answer = await readline.question("Upgrade to HQ Workforce ($500/mo) now? [y/N] ");
69
69
  if (!/^y(?:es)?$/i.test(answer.trim()))
70
70
  return false;
71
71
  await onConfirm();
@@ -10,7 +10,7 @@ export interface TeamUpgradeOptions {
10
10
  error?: (text: string) => void;
11
11
  }
12
12
  /**
13
- * Start the owner-gated HQ Team Checkout flow. Stripe collects payment details
13
+ * Start the owner-gated HQ Workforce Checkout flow. Stripe collects payment details
14
14
  * in its hosted browser page; the CLI never receives card data.
15
15
  */
16
16
  export declare function upgradeToTeam(opts?: TeamUpgradeOptions): Promise<string>;
@@ -14,7 +14,7 @@ function ownerUpgradeError() {
14
14
  return Object.assign(new Error("Only a company owner can upgrade. Ask a company owner to upgrade."), { expected: true });
15
15
  }
16
16
  /**
17
- * Start the owner-gated HQ Team Checkout flow. Stripe collects payment details
17
+ * Start the owner-gated HQ Workforce Checkout flow. Stripe collects payment details
18
18
  * in its hosted browser page; the CLI never receives card data.
19
19
  */
20
20
  export async function upgradeToTeam(opts = {}) {
@@ -35,7 +35,7 @@ export async function upgradeToTeam(opts = {}) {
35
35
  if (response.status === 403 || /owner/i.test(`${body.error ?? ""} ${body.message ?? ""}`)) {
36
36
  throw ownerUpgradeError();
37
37
  }
38
- throw Object.assign(new Error(`Could not start HQ Team checkout: ${body.error ?? body.message ?? response.statusText}`), { expected: true });
38
+ throw Object.assign(new Error(`Could not start HQ Workforce checkout: ${body.error ?? body.message ?? response.statusText}`), { expected: true });
39
39
  }
40
40
  const { url } = (await response.json());
41
41
  if (typeof url !== "string" || !url) {
@@ -46,7 +46,7 @@ export async function upgradeToTeam(opts = {}) {
46
46
  write(`${JSON.stringify({ url })}\n`);
47
47
  return url;
48
48
  }
49
- write("Opening HQ Team checkout ($500/mo) in your browser — finish there and you're upgraded.\n");
49
+ write("Opening HQ Workforce checkout ($500/mo) in your browser — finish there and you're upgraded.\n");
50
50
  const skipBrowser = opts.noBrowser || (opts.headless ?? isHeadless());
51
51
  if (skipBrowser) {
52
52
  write(`Open this URL to continue: ${url}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.117.1",
3
+ "version": "5.117.2",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {