@indigoai-us/hq-cli 5.122.2 → 5.123.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +46 -0
  3. package/dist/command-catalog.generated.d.ts +6 -3
  4. package/dist/command-catalog.generated.js +7 -3
  5. package/dist/commands/dm.d.ts +54 -1
  6. package/dist/commands/dm.js +204 -8
  7. package/dist/commands/mesh.js +7 -2
  8. package/dist/commands/update.js +6 -0
  9. package/dist/commands/whoami.d.ts +2 -2
  10. package/dist/commands/whoami.js +18 -19
  11. package/dist/lib/billing/plan-lock.d.ts +64 -69
  12. package/dist/lib/billing/plan-lock.js +230 -161
  13. package/dist/lib/mesh/api.js +96 -28
  14. package/dist/lib/mesh/live/backfill-held.d.ts +43 -7
  15. package/dist/lib/mesh/live/backfill-held.js +323 -42
  16. package/dist/lib/mesh/live/daemon/board-refresh.d.ts +4 -1
  17. package/dist/lib/mesh/live/daemon/board-refresh.js +71 -30
  18. package/dist/lib/mesh/live/daemon/config.d.ts +9 -0
  19. package/dist/lib/mesh/live/daemon/config.js +45 -0
  20. package/dist/lib/mesh/live/daemon/credentials.d.ts +6 -1
  21. package/dist/lib/mesh/live/daemon/credentials.js +20 -4
  22. package/dist/lib/mesh/live/daemon/doctor.d.ts +6 -0
  23. package/dist/lib/mesh/live/daemon/doctor.js +29 -2
  24. package/dist/lib/mesh/live/daemon/index.d.ts +4 -2
  25. package/dist/lib/mesh/live/daemon/index.js +3 -2
  26. package/dist/lib/mesh/live/daemon/install.d.ts +27 -0
  27. package/dist/lib/mesh/live/daemon/install.js +54 -4
  28. package/dist/lib/mesh/live/daemon/run.d.ts +6 -0
  29. package/dist/lib/mesh/live/daemon/run.js +50 -3
  30. package/dist/lib/mesh/live/daemon/state.d.ts +3 -0
  31. package/dist/lib/mesh/live/daemon/version-drift.d.ts +5 -0
  32. package/dist/lib/mesh/live/daemon/version-drift.js +47 -0
  33. package/dist/lib/mesh/live/paths.d.ts +2 -0
  34. package/dist/lib/mesh/live/paths.js +4 -0
  35. package/dist/lib/work-context/reconcile.js +67 -6
  36. package/dist/lib/work-context/state.d.ts +5 -3
  37. package/dist/lib/work-context/state.js +28 -4
  38. package/dist/utils/cli-telemetry.d.ts +21 -0
  39. package/dist/utils/cli-telemetry.js +77 -1
  40. package/dist/utils/self-update.d.ts +3 -0
  41. package/dist/utils/self-update.js +11 -0
  42. package/package.json +3 -2
@@ -5,7 +5,7 @@ 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
7
  import { ensureCognitoToken, loadMachineCachedTokens, resolveCognitoTokenSource, } from "../utils/cognito-session.js";
8
- import { colorizePlanLimitNotice, fetchPlanLockStatus, renderPlanLine, renderPlanLimitNotice, } from "../lib/billing/plan-lock.js";
8
+ import { colorizePlanLimitNotice, fetchPlanLimitStatus, renderPlanLine, renderPlanLimitNotice, } from "../lib/billing/plan-lock.js";
9
9
  /**
10
10
  * Resolves the person represented by an ID token without pairing a delegated
11
11
  * email with the token subject of the delegating machine.
@@ -42,27 +42,27 @@ function peekIdToken(idToken) {
42
42
  };
43
43
  }
44
44
  /**
45
- * starter-plan-hard-limits / US-011 — read the workspace plan lock for
46
- * `--company <slug>`.
45
+ * starter-plan-hard-limits / US-011 — read the workspace plan-limit nag for
46
+ * `--company <slug>` (the bind notice).
47
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.
48
+ * hq-pro owns the numbers (`GET /v1/billing/usage-limits`); this only reads
49
+ * them. Best-effort by construction: no session, a network failure, or a
50
+ * company the caller is not a member of all resolve to null — reported as
51
+ * UNKNOWN and rendered as nothing, never as a guessed over-limit or a guessed
52
+ * all-clear, and never as a command failure. The command exists to answer
53
+ * "who am I"; the plan line is decoration on top.
54
54
  */
55
- async function readPlanLock(company) {
55
+ async function readPlanLimits(company) {
56
56
  const slug = company?.trim();
57
57
  if (!slug)
58
58
  return null;
59
59
  try {
60
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
61
+ // check) and inside hq-core's per-turn plan-limit refresh hook. With the
62
62
  // default `interactive: true`, a box with no cached session would launch a
63
63
  // browser sign-in and block the caller instead of resolving to UNKNOWN.
64
64
  const token = await ensureCognitoToken({ interactive: false });
65
- return await fetchPlanLockStatus(token, slug);
65
+ return await fetchPlanLimitStatus(token, slug);
66
66
  }
67
67
  catch {
68
68
  return null;
@@ -74,13 +74,13 @@ async function readPlanLock(company) {
74
74
  * orientation when it is Starter and healthy, and nothing at all otherwise.
75
75
  */
76
76
  export function renderWhoamiPlanBlock(status) {
77
- if (status.lock.locked) {
77
+ if (status.over) {
78
78
  return colorizePlanLimitNotice(renderPlanLimitNotice(status));
79
79
  }
80
80
  return renderPlanLine(status);
81
81
  }
82
82
  async function printPlanBlock(company) {
83
- const status = await readPlanLock(company);
83
+ const status = await readPlanLimits(company);
84
84
  if (!status)
85
85
  return;
86
86
  const block = renderWhoamiPlanBlock(status);
@@ -92,7 +92,7 @@ export function registerWhoamiCommand(program) {
92
92
  .command('whoami')
93
93
  .description('Show the currently authenticated user')
94
94
  .option('--json', 'Output identity metadata as JSON (never tokens)')
95
- .option('--company <slug>', 'Also report this company\'s workspace plan state (Starter lock)')
95
+ .option('--company <slug>', 'Also report this company\'s workspace plan state (Starter nag)')
96
96
  .action(async (opts) => {
97
97
  try {
98
98
  // Effective source (same as auth status / ensureCognitoToken): env
@@ -115,9 +115,8 @@ export function registerWhoamiCommand(program) {
115
115
  const agentUid = machineCreds?.entityUid !== undefined
116
116
  ? (machineCreds.entityUid.startsWith('agt_') ? machineCreds.entityUid : null)
117
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);
118
+ // planLimits is additive: absent-or-null means UNKNOWN.
119
+ const planLimits = await readPlanLimits(opts.company);
121
120
  console.log(JSON.stringify({
122
121
  schemaVersion: 1,
123
122
  authenticated: machine ? Boolean(machineCreds) : Boolean(cached && !isExpiring(cached, 0)),
@@ -126,7 +125,7 @@ export function registerWhoamiCommand(program) {
126
125
  personUid: !machine && identity.entityType === 'person' ? identity.entityUid ?? null : null,
127
126
  agentUid,
128
127
  username: machineCreds?.username ?? null,
129
- planLock,
128
+ planLimits,
130
129
  }, null, 2));
131
130
  return;
132
131
  }
@@ -1,65 +1,58 @@
1
1
  /**
2
- * `plan-lock` (starter-plan-hard-limits / US-011, rewritten for US-036) — the
3
- * CLI-side read + render of a workspace's Starter plan-limit state.
2
+ * CLI-side read + render of a workspace's Starter plan-limit nag
3
+ * (starter-plan-hard-limits / US-011, rewritten for US-036, then US-040).
4
4
  *
5
5
  * Starter (free) workspaces are capped on members, integrations, secrets and
6
6
  * agents (deployments and storage nag but never arm a stop). Going over does
7
7
  * NOT stop the workspace working (US-033 owner decision 12): everything that
8
8
  * exists keeps working, and the only two things that pause are adding new
9
9
  * files to the vault and adding new secrets (owner decision 13). Every string
10
- * this module renders is a nag, never a claim that HQ has stopped. The state
11
- * is NOT decided here — hq-pro is the single source of truth and ships the
12
- * answer on `GET /membership/me` as a per-company `planLock` object. This
13
- * module only reads that field and renders it.
10
+ * this module renders is a nag, never a claim that HQ has stopped.
11
+ *
12
+ * US-040: hq-pro no longer attaches `planLock` on `/membership/me` or
13
+ * `/v1/billing/usage-limits`. This module reads the nag payload from
14
+ * usage-limits (`planLimits` resource rows, top-level `freeMonth`,
15
+ * `upgradeUrl`) and never a lock object.
14
16
  *
15
17
  * Copy rule (US-036): no string a customer reads may contain "locked" or
16
18
  * "read-only". `plan-lock.test.ts` scans every rendered string for both.
17
19
  *
18
- * Member counts are decoration, never a second opinion: the count comes from
19
- * `GET /v1/billing/usage-limits` on a best-effort basis and its absence only
20
- * removes the "n of 5" detail from the notice. An absent field is UNKNOWN, so
21
- * nothing here ever infers a lock (or an unlock) from missing data
22
- * (hq-absent-field-never-means-constraining-value).
20
+ * An absent field is UNKNOWN, so nothing here ever infers over-limit from
21
+ * missing data (hq-absent-field-never-means-constraining-value).
23
22
  */
24
23
  /**
25
- * Mirror of hq-pro's `PlanLockReason`. Owner decision 7 (2026-09-17) fixes the
26
- * nagging set at these four: `deployments` and `storageBytes` are nag-only and
27
- * never appear here. An unrecognised reason is dropped by `parsePlanLock`, so a
28
- * server that adds a fifth dimension renders as the generic line rather than as
29
- * a false claim about members.
24
+ * Dimensions the bind notice names with a remedy. Owner decision 7 (2026-09-17)
25
+ * fixes the nagging set at these four: `deployments` and `storageBytes` are
26
+ * nag-only and never appear here.
30
27
  */
28
+ export declare const PLAN_LIMIT_REASONS: readonly ["users", "integrations", "secrets", "agents"];
29
+ /** @deprecated US-040 — alias of {@link PLAN_LIMIT_REASONS}. */
31
30
  export declare const PLAN_LOCK_REASONS: readonly ["users", "integrations", "secrets", "agents"];
32
- export type PlanLockReason = (typeof PLAN_LOCK_REASONS)[number];
33
- /** Mirror of hq-pro's `PlanLock` wire shape (see src/billing/plan-lock.ts). */
34
- export interface PlanLock {
35
- locked: boolean;
36
- reasons: PlanLockReason[];
37
- upgradeUrl: string;
38
- fixOptions: PlanLockFixOptions;
39
- }
40
- /**
41
- * The remedies hq-pro ships with the lock. The two original fields are always
42
- * present (defaulted when the server omits them) because shipped copy reads
43
- * them unconditionally; every dimension added since is optional, so an older
44
- * server simply leaves it absent and the renderer quotes the Starter cap.
45
- */
46
- export interface PlanLockFixOptions {
47
- removeMembersTo: number;
48
- disconnectIntegrations: boolean;
49
- disconnectIntegrationsTo?: number;
50
- removeSecretsTo?: number;
51
- deprovisionAgentsTo?: number;
52
- }
31
+ export type PlanLimitReason = (typeof PLAN_LIMIT_REASONS)[number];
32
+ /** @deprecated US-040 — alias of {@link PlanLimitReason}. */
33
+ export type PlanLockReason = PlanLimitReason;
53
34
  /** Optional member decoration for the notice ("7 of 5 members"). */
54
- export interface PlanLockMembers {
35
+ export interface PlanLimitMembers {
55
36
  used: number;
56
37
  limit: number;
57
38
  }
58
- export interface PlanLockStatus {
39
+ /** @deprecated US-040 — alias of {@link PlanLimitMembers}. */
40
+ export type PlanLockMembers = PlanLimitMembers;
41
+ export interface PlanLimitFreeMonth {
42
+ eligible: boolean;
43
+ trialDays?: number;
44
+ url?: string;
45
+ }
46
+ export interface PlanLimitStatus {
59
47
  companySlug: string;
60
48
  companyUid?: string;
61
- lock: PlanLock;
62
- members?: PlanLockMembers;
49
+ /** True when any named dimension is over its cap. */
50
+ over: boolean;
51
+ reasons: PlanLimitReason[];
52
+ members?: PlanLimitMembers;
53
+ usages: Partial<Record<PlanLimitReason, PlanLimitMembers>>;
54
+ upgradeUrl: string;
55
+ freeMonth?: PlanLimitFreeMonth;
63
56
  /**
64
57
  * Resolved plan id, when the usage endpoint answered. UNKNOWN (absent) when
65
58
  * it did not — surfaces that gate on "is this Starter?" must then stay
@@ -68,51 +61,53 @@ export interface PlanLockStatus {
68
61
  plan?: "free" | "paid" | "enterprise";
69
62
  checkedAt: string;
70
63
  }
71
- /** Starter member cap quoted when the server did not send `removeMembersTo`. */
64
+ /** @deprecated US-040 — alias of {@link PlanLimitStatus}. */
65
+ export type PlanLockStatus = PlanLimitStatus;
66
+ /** Starter member cap quoted when the server did not send a users.limit. */
72
67
  export declare const STARTER_MEMBER_TARGET = 5;
73
68
  /**
74
- * Starter integration cap quoted when the server did not send
75
- * `disconnectIntegrationsTo`. One: Starter includes a single integration
76
- * (US-020 owner decision 11, 2026-09-17 — it was zero before that).
69
+ * Starter integration cap quoted when the server did not send a limit.
70
+ * One: Starter includes a single integration (US-020 owner decision 11).
77
71
  */
78
72
  export declare const STARTER_INTEGRATION_TARGET = 1;
79
- /** Starter secret cap quoted when the server did not send `removeSecretsTo`. */
73
+ /** Starter secret cap quoted when the server did not send a secrets.limit. */
80
74
  export declare const STARTER_SECRET_TARGET = 10;
81
- /** Starter agent cap quoted when the server did not send `deprovisionAgentsTo`. */
75
+ /** Starter agent cap quoted when the server did not send an agents.limit. */
82
76
  export declare const STARTER_AGENT_TARGET = 0;
83
77
  /** Upgrade destination quoted when the server did not send one. */
84
78
  export declare const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
85
- /** The paid plan the lock wall sends owners to. Copy lives in ONE place. */
79
+ /** The paid plan the nag sends owners to. Copy lives in ONE place. */
86
80
  export declare const WORKFORCE_PLAN_LABEL = "HQ Workforce ($500/mo)";
87
- /**
88
- * Defensively parse a `planLock` payload. Anything malformed returns null —
89
- * the caller then behaves exactly as if the server had sent nothing, which is
90
- * "unknown", not "locked".
91
- */
92
- export declare function parsePlanLock(value: unknown): PlanLock | null;
81
+ export declare function parseFreeMonth(value: unknown): PlanLimitFreeMonth | undefined;
93
82
  /**
94
83
  * Pick the membership row for `companySlug` (or `cmp_…` uid) out of a decoded
95
- * `/membership/me` body and return its parsed lock. No matching row — or no
96
- * `planLock` on it — is UNKNOWN, so this returns null rather than guessing.
84
+ * `/membership/me` body. No matching row is UNKNOWN. Does not read `planLock`.
97
85
  */
98
- export declare function selectPlanLock(body: unknown, companyRef: string): {
99
- lock: PlanLock;
86
+ export declare function selectCompanyMembership(body: unknown, companyRef: string): {
100
87
  companyUid?: string;
101
88
  } | null;
102
89
  /** Parse the resolved plan id out of a usage-limits body. Absent → null. */
103
90
  export declare function selectPlan(body: unknown): "free" | "paid" | "enterprise" | null;
104
91
  /** Parse the `users` dimension out of a usage-limits body. Absent → null. */
105
- export declare function selectMemberUsage(body: unknown): PlanLockMembers | null;
92
+ export declare function selectMemberUsage(body: unknown): PlanLimitMembers | null;
93
+ /**
94
+ * Read over-limit dimensions from a usage-limits body. `planLimits` nested
95
+ * map is preferred when present; otherwise the top-level resource rows.
96
+ */
97
+ export declare function selectOverReasons(body: unknown): PlanLimitReason[];
98
+ export declare function selectUsages(body: unknown): Partial<Record<PlanLimitReason, PlanLimitMembers>>;
106
99
  /**
107
- * Read the live lock for one company: `planLock` from `/membership/me`, plus a
108
- * best-effort member count for the notice. Returns null when the server did not
109
- * answer with a lock for this company — callers must then say nothing.
100
+ * Read the live nag for one company from usage-limits. `/membership/me` is
101
+ * used only to resolve slug → companyUid. Returns null when the server did
102
+ * not answer for this company — callers must then say nothing.
110
103
  */
111
- export declare function fetchPlanLockStatus(token: string, companyRef: string, opts?: {
104
+ export declare function fetchPlanLimitStatus(token: string, companyRef: string, opts?: {
112
105
  timeoutMs?: number;
113
- }): Promise<PlanLockStatus | null>;
106
+ }): Promise<PlanLimitStatus | null>;
107
+ /** @deprecated US-040 — alias of {@link fetchPlanLimitStatus}. */
108
+ export declare const fetchPlanLockStatus: typeof fetchPlanLimitStatus;
114
109
  /** Why the workspace is over, one clause per reason. */
115
- export declare function reasonLabel(reason: PlanLockReason): string;
110
+ export declare function reasonLabel(reason: PlanLimitReason): string;
116
111
  /**
117
112
  * The one sentence every over-limit surface says about the two hard stops
118
113
  * (US-033 §3.3). Written once so the CLI, the console and the emails cannot
@@ -124,22 +119,22 @@ export declare const HARD_STOP_SENTENCE: string;
124
119
  * against the cap, and the ways out. Plain text — colour is applied by the
125
120
  * caller so scripts capturing stdout get a clean block.
126
121
  *
127
- * Every line is derived from `lock.reasons`. A workspace over on secrets is
122
+ * Every line is derived from `status.reasons`. A workspace over on secrets is
128
123
  * never told it has too many members, and an empty reason list (a server
129
124
  * dimension this CLI does not know) renders a generic line rather than a claim
130
125
  * about a dimension nobody measured.
131
126
  */
132
- export declare function renderPlanLimitNotice(status: PlanLockStatus): string;
127
+ export declare function renderPlanLimitNotice(status: PlanLimitStatus): string;
133
128
  /**
134
129
  * The one-line form injected on every turn while the workspace stays over.
135
130
  * Kept to a single line on purpose — it repeats each turn.
136
131
  */
137
- export declare function renderPlanLimitLine(status: PlanLockStatus): string;
132
+ export declare function renderPlanLimitLine(status: PlanLimitStatus): string;
138
133
  /**
139
134
  * The `Plan: …` orientation line. Starter only: a paid or enterprise
140
135
  * workspace — and an UNKNOWN plan — gets no line at all.
141
136
  */
142
- export declare function renderPlanLine(status: PlanLockStatus): string | null;
137
+ export declare function renderPlanLine(status: PlanLimitStatus): string | null;
143
138
  /**
144
139
  * Colourised block for interactive output. Yellow, not red: this is a nag, and
145
140
  * an error colour would read as "HQ has stopped working", which is the exact