@indigoai-us/hq-cli 5.122.2 → 5.123.0

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.
@@ -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