@indigoai-us/hq-cli 5.119.10 → 5.119.11

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,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.119.11] — 2026-09-18
6
+
7
+ ### Added
8
+
9
+ - On a personal account, My Telemetry is part of the paid Individual plan
10
+ ($50/mo). When the server withholds it, the CLI now prints a short notice
11
+ naming the plan and where to upgrade, instead of the raw refusal. Company
12
+ plans are unaffected, and the notice has nothing to do with the plan-limit
13
+ nags — it is a plan fact, printed once, where it happened.
14
+
15
+ ### Fixed
16
+
17
+ - `hq files … --company prs_<person-uid>` now reaches that person's vault
18
+ instead of failing with "Company slug ... was not found". This is how a DM
19
+ recipient reads a file attached to a message: the attachment lives in the
20
+ sender's personal vault, so naming the sender is the only scope that works.
21
+
5
22
  ## [5.119.10] — 2026-09-18
6
23
 
7
24
  ### Added
@@ -83,6 +83,28 @@ export declare function recordPlanLimitStatus(body: unknown, opts?: {
83
83
  * never claims the workspace has stopped working.
84
84
  */
85
85
  export declare function buildOverLine(overEntries: Array<[string, PlanLimitEntry]>, upgradeUrl: string): string;
86
+ /**
87
+ * My Telemetry is an Individual-plan feature on a personal scope (owner
88
+ * decision 9 / us-044-individual-plan-design.md §11). When hq-pro refuses it,
89
+ * the CLI prints this notice INSTEAD of telemetry output.
90
+ *
91
+ * It lives beside the nag copy so plan wording stays in one file, but it is
92
+ * NOT part of the nag model: no persistence, no throttle, no episode state. It
93
+ * is rendered once, on the spot, by whoever received the refusal. The refusal
94
+ * is a plan fact, so it neither counts toward the nag bands nor is silenced by
95
+ * the nag cadence.
96
+ *
97
+ * Copy names the plan and its price only — no Outposts, no bot pricing (owner
98
+ * decisions 10 and 11).
99
+ */
100
+ export declare const INDIVIDUAL_PLAN_NAME = "Individual";
101
+ export declare const INDIVIDUAL_PRICE_LABEL = "$50/mo";
102
+ /**
103
+ * The notice a personal feature refusal prints. `upgradeUrl` is the server's
104
+ * own checkout target; an older hq-pro sends none, and the line then names the
105
+ * plan without a link rather than inventing one.
106
+ */
107
+ export declare function renderPersonalPlanFeatureNotice(feature: string, upgradeUrl?: string | null): string;
86
108
  /**
87
109
  * One line per crossing.
88
110
  *
@@ -326,6 +326,42 @@ export function buildOverLine(overEntries, upgradeUrl) {
326
326
  return (`⚠ HQ Starter: ${facts}. New files and new secrets are paused. ` +
327
327
  `Upgrade: ${upgradeUrl}`);
328
328
  }
329
+ // ---------------------------------------------------------------------------
330
+ // Individual-plan feature notice (US-054)
331
+ // ---------------------------------------------------------------------------
332
+ /**
333
+ * My Telemetry is an Individual-plan feature on a personal scope (owner
334
+ * decision 9 / us-044-individual-plan-design.md §11). When hq-pro refuses it,
335
+ * the CLI prints this notice INSTEAD of telemetry output.
336
+ *
337
+ * It lives beside the nag copy so plan wording stays in one file, but it is
338
+ * NOT part of the nag model: no persistence, no throttle, no episode state. It
339
+ * is rendered once, on the spot, by whoever received the refusal. The refusal
340
+ * is a plan fact, so it neither counts toward the nag bands nor is silenced by
341
+ * the nag cadence.
342
+ *
343
+ * Copy names the plan and its price only — no Outposts, no bot pricing (owner
344
+ * decisions 10 and 11).
345
+ */
346
+ export const INDIVIDUAL_PLAN_NAME = "Individual";
347
+ export const INDIVIDUAL_PRICE_LABEL = "$50/mo";
348
+ /** Human names for the features a personal scope can be refused. */
349
+ const PERSONAL_FEATURE_LABELS = {
350
+ telemetry: "My Telemetry",
351
+ };
352
+ /**
353
+ * The notice a personal feature refusal prints. `upgradeUrl` is the server's
354
+ * own checkout target; an older hq-pro sends none, and the line then names the
355
+ * plan without a link rather than inventing one.
356
+ */
357
+ export function renderPersonalPlanFeatureNotice(feature, upgradeUrl) {
358
+ const label = PERSONAL_FEATURE_LABELS[feature] ?? feature;
359
+ const headline = `${label} is included with the HQ ${INDIVIDUAL_PLAN_NAME} plan (${INDIVIDUAL_PRICE_LABEL}).`;
360
+ const action = typeof upgradeUrl === "string" && upgradeUrl.trim().length > 0
361
+ ? `Upgrade: ${upgradeUrl.trim()}`
362
+ : "Upgrade in your HQ billing settings.";
363
+ return `${headline} Your personal account is on the free tier. ${action}`;
364
+ }
329
365
  /** `"{resource}:{band}"` — the persisted key, matching the server's own. */
330
366
  function crossingKey(crossing) {
331
367
  return `${crossing.resource}:${crossing.band}`;
@@ -1,11 +1,15 @@
1
+ export type PlanGateCode = "PLAN_LIMIT_EXCEEDED" | "PLAN_REQUIRED"
1
2
  /**
2
- * A deliberate subscription denial returned by hq-pro. Keeping its structured
3
- * fields on a typed error lets the CLI boundary render one safe, consistent
4
- * message instead of every command parsing and printing a server response.
3
+ * US-054: a PERSONAL scope asked for a feature the paid Individual plan
4
+ * includes and the free tier does not. A different plan, a different price
5
+ * and a different remedy from the company codes above, so it is its own
6
+ * code rather than a reuse of `PLAN_REQUIRED` with company copy.
5
7
  */
6
- export type PlanGateCode = "PLAN_LIMIT_EXCEEDED" | "PLAN_REQUIRED";
8
+ | "PERSONAL_PLAN_REQUIRED";
7
9
  export interface PlanGateDetails {
8
10
  resource?: string;
11
+ /** The refused feature, on a `PERSONAL_PLAN_REQUIRED` denial. */
12
+ feature?: string;
9
13
  used?: number;
10
14
  limit?: number;
11
15
  upgradeUrl?: string;
@@ -1,3 +1,9 @@
1
+ /**
2
+ * A deliberate subscription denial returned by hq-pro. Keeping its structured
3
+ * fields on a typed error lets the CLI boundary render one safe, consistent
4
+ * message instead of every command parsing and printing a server response.
5
+ */
6
+ import { renderPersonalPlanFeatureNotice } from "../lib/plan-limit-nag.js";
1
7
  const TEAM_UPGRADE_HINT = "Run `hq billing upgrade` to move to HQ Workforce.";
2
8
  export class PlanGateError extends Error {
3
9
  code;
@@ -6,7 +12,11 @@ export class PlanGateError extends Error {
6
12
  // Commands with their own expected-error boundary commonly print
7
13
  // `err.message`. Keeping the friendly copy here means those boundaries
8
14
  // retain the same plan-gate voice as main.ts without per-command handling.
9
- super([formatPlanGateDetails(code, details), TEAM_UPGRADE_HINT].join("\n"));
15
+ // The HQ Workforce hint is company advice; a personal denial ends at its
16
+ // own notice, which already names the Individual plan and its checkout.
17
+ super(code === "PERSONAL_PLAN_REQUIRED"
18
+ ? formatPlanGateDetails(code, details)
19
+ : [formatPlanGateDetails(code, details), TEAM_UPGRADE_HINT].join("\n"));
10
20
  this.code = code;
11
21
  this.details = details;
12
22
  this.name = "PlanGateError";
@@ -31,6 +41,12 @@ function formatPlanGateDetails(code, details) {
31
41
  existingResourcesNote,
32
42
  ].join("\n");
33
43
  }
44
+ // A personal scope is not a workspace: it has no members to trim, no $500
45
+ // tier to buy, and nothing of its own is paused. One line, the plan's own
46
+ // copy, and no company remedy.
47
+ if (code === "PERSONAL_PLAN_REQUIRED") {
48
+ return renderPersonalPlanFeatureNotice(details.feature ?? "this feature", details.upgradeUrl ?? null);
49
+ }
34
50
  const upgrade = typeof details.upgradeUrl === "string"
35
51
  ? `Upgrade to HQ Workforce ($500/mo) to remove limits: ${details.upgradeUrl}`
36
52
  : "Upgrade to HQ Workforce ($500/mo) to remove limits.";
@@ -41,6 +57,9 @@ function formatPlanGateDetails(code, details) {
41
57
  ].join("\n");
42
58
  }
43
59
  export function formatPlanGateError(err) {
60
+ if (err.code === "PERSONAL_PLAN_REQUIRED") {
61
+ return formatPlanGateDetails(err.code, err.details);
62
+ }
44
63
  return [
45
64
  formatPlanGateDetails(err.code, err.details),
46
65
  TEAM_UPGRADE_HINT,
@@ -80,6 +99,22 @@ export function planGateErrorFromPayload(status, body, companyUid) {
80
99
  if (status !== 402 || !body || typeof body !== "object")
81
100
  return null;
82
101
  const payload = body;
102
+ // US-054: the personal feature denial. hq-pro identifies it with
103
+ // `error: "plan_feature_unavailable"` plus `scope: "personal"`; both the
104
+ // `scope` and `requiredPlan` fields are OPTIONAL on the wire, so an hq-pro
105
+ // that predates US-054 sends a company-shaped denial and keeps falling
106
+ // through to the company path below (defensive consumer).
107
+ if (payload.error === "plan_feature_unavailable" &&
108
+ payload.scope === "personal") {
109
+ return new PlanGateError("PERSONAL_PLAN_REQUIRED", {
110
+ ...(typeof payload.feature === "string"
111
+ ? { feature: payload.feature }
112
+ : {}),
113
+ ...(typeof payload.upgradeUrl === "string"
114
+ ? { upgradeUrl: payload.upgradeUrl }
115
+ : {}),
116
+ });
117
+ }
83
118
  if (payload.code !== "PLAN_LIMIT_EXCEEDED" && payload.code !== "PLAN_REQUIRED") {
84
119
  return null;
85
120
  }
@@ -59,6 +59,7 @@ export declare function vaultApiFetchPublic(opts: {
59
59
  query?: Record<string, string>;
60
60
  }): Promise<Response>;
61
61
  export declare function looksLikeCompanyUid(ref: string): boolean;
62
+ export declare function looksLikePersonVaultUid(ref: string): boolean;
62
63
  export declare function getCompanyUid(token: string, companySlug: string | undefined): Promise<string>;
63
64
  export declare function resolveCallerPersonUid(token: string, baseUrl?: string): Promise<string>;
64
65
  export declare function getEntityUid(token: string, opts: {
@@ -285,6 +285,24 @@ const COMPANY_UID_PREFIX = 'cmp_';
285
285
  export function looksLikeCompanyUid(ref) {
286
286
  return ref.startsWith(COMPANY_UID_PREFIX);
287
287
  }
288
+ /**
289
+ * A `--company` value beginning with `prs_` names a PERSON'S vault, not a
290
+ * company. It is the only way to reach a DM attachment: the file lives in the
291
+ * SENDER's personal vault under
292
+ * `chat/attachments/dm/<uidA>--<uidB>/…`, and the vault API authorizes either
293
+ * member of that pair to `get` it (hq-pro #3571). The recipient has no
294
+ * personal vault of their own (so `--personal` cannot work) and the key is not
295
+ * in any company bucket (so a slug 404s on the object), which left the read
296
+ * with no expressible scope at all.
297
+ *
298
+ * Routed straight through as the scope uid: person entities have no slug
299
+ * namespace, so there is nothing to resolve, and the server's own per-key gate
300
+ * — not the CLI — decides whether the caller may read it.
301
+ */
302
+ const PERSON_VAULT_UID_PREFIX = 'prs_';
303
+ export function looksLikePersonVaultUid(ref) {
304
+ return ref.startsWith(PERSON_VAULT_UID_PREFIX);
305
+ }
288
306
  // A 401 from ANY vault resolution call means the caller's HQ session is
289
307
  // expired or missing — an expected auth state fixed by `hq login`, not a
290
308
  // code defect. Raise a typed AuthError so the top-level handler prints an
@@ -351,6 +369,12 @@ async function resolveCompanyUid(token, ref) {
351
369
  if (looksLikeCompanyUid(ref)) {
352
370
  return resolveCompanyByUid(token, ref);
353
371
  }
372
+ // A `prs_` ref names a person's vault (DM attachments). There is no slug to
373
+ // resolve and no company lookup that could succeed; pass it through as the
374
+ // scope and let the vault API's per-key authorization answer.
375
+ if (looksLikePersonVaultUid(ref)) {
376
+ return ref;
377
+ }
354
378
  // PRIMARY PATH — caller-scoped slug resolution. Resolves the slug to the
355
379
  // caller's OWN company (unique within their namespace by the invariant
356
380
  // above), making a stranger's same-slug company invisible.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.119.10",
3
+ "version": "5.119.11",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {