@indigoai-us/hq-cli 5.162.0 → 5.163.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/command-catalog.generated.d.ts +188 -0
  3. package/dist/command-catalog.generated.js +235 -0
  4. package/dist/command-registration-plan.d.ts +7 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/agent-enroll.d.ts +6 -1
  7. package/dist/commands/agent-enroll.js +30 -3
  8. package/dist/commands/agents.js +15 -22
  9. package/dist/commands/bot.js +56 -6
  10. package/dist/commands/daemon.d.ts +53 -0
  11. package/dist/commands/daemon.js +554 -0
  12. package/dist/commands/lanes.js +7 -0
  13. package/dist/commands/meetings.js +18 -16
  14. package/dist/commands/mesh.js +9 -0
  15. package/dist/lib/daemon/catalog.d.ts +85 -0
  16. package/dist/lib/daemon/catalog.js +274 -0
  17. package/dist/lib/daemon/config.d.ts +26 -0
  18. package/dist/lib/daemon/config.js +85 -0
  19. package/dist/lib/daemon/handoff.d.ts +53 -0
  20. package/dist/lib/daemon/handoff.js +136 -0
  21. package/dist/lib/daemon/install.d.ts +76 -0
  22. package/dist/lib/daemon/install.js +299 -0
  23. package/dist/lib/daemon/legacy.d.ts +35 -0
  24. package/dist/lib/daemon/legacy.js +120 -0
  25. package/dist/lib/daemon/lock.d.ts +47 -0
  26. package/dist/lib/daemon/lock.js +138 -0
  27. package/dist/lib/daemon/machine-type.d.ts +48 -0
  28. package/dist/lib/daemon/machine-type.js +139 -0
  29. package/dist/lib/daemon/migrate.d.ts +61 -0
  30. package/dist/lib/daemon/migrate.js +238 -0
  31. package/dist/lib/daemon/paths.d.ts +22 -0
  32. package/dist/lib/daemon/paths.js +42 -0
  33. package/dist/lib/daemon/remote-control.d.ts +35 -0
  34. package/dist/lib/daemon/remote-control.js +47 -0
  35. package/dist/lib/daemon/requests.d.ts +24 -0
  36. package/dist/lib/daemon/requests.js +63 -0
  37. package/dist/lib/daemon/run.d.ts +41 -0
  38. package/dist/lib/daemon/run.js +319 -0
  39. package/dist/lib/daemon/supervisor.d.ts +87 -0
  40. package/dist/lib/daemon/supervisor.js +523 -0
  41. package/dist/lib/daemon/updater.d.ts +39 -0
  42. package/dist/lib/daemon/updater.js +103 -0
  43. package/dist/lib/flag-registry.d.ts +7 -1
  44. package/dist/lib/flag-registry.js +8 -0
  45. package/dist/lib/mesh/live/backoff.d.ts +2 -2
  46. package/dist/lib/mesh/live/backoff.js +16 -3
  47. package/dist/lib/mesh/live/daemon/run.d.ts +2 -2
  48. package/dist/lib/mesh/live/daemon/run.js +5 -2
  49. package/dist/lib/mesh/live/flush.d.ts +3 -2
  50. package/dist/lib/mesh/live/flush.js +34 -7
  51. package/dist/lib/mesh/live/session-events-client.d.ts +2 -0
  52. package/dist/lib/mesh/live/session-events-client.js +25 -0
  53. package/dist/lib/service-manager/systemd.d.ts +2 -2
  54. package/dist/lib/service-manager/systemd.js +1 -1
  55. package/dist/lib/service-manager/types.d.ts +2 -0
  56. package/dist/main.d.ts +6 -0
  57. package/dist/main.js +53 -13
  58. package/dist/utils/cognito-session.d.ts +4 -0
  59. package/dist/utils/cognito-session.js +11 -10
  60. package/dist/utils/plan-gate-error.d.ts +27 -13
  61. package/dist/utils/plan-gate-error.js +133 -58
  62. package/dist/utils/self-update.d.ts +9 -0
  63. package/dist/utils/self-update.js +15 -2
  64. package/package.json +1 -1
@@ -1,30 +1,44 @@
1
- export type PlanGateCode = "PLAN_LIMIT_EXCEEDED" | "PLAN_REQUIRED"
2
- /**
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.
7
- */
8
- | "PERSONAL_PLAN_REQUIRED";
1
+ export type PlanGateCode = "PLAN_LIMIT_EXCEEDED" | "PLAN_REQUIRED" | "MEETING_PLAN_REQUIRED"
2
+ /** A PERSONAL scope asked for a feature the paid Individual plan includes. */
3
+ | "PERSONAL_PLAN_REQUIRED"
4
+ /** External agent enrollment was denied by the company plan. */
5
+ | "BILLING_REQUIRED";
9
6
  export interface PlanGateDetails {
10
7
  resource?: string;
11
- /** The refused feature, on a `PERSONAL_PLAN_REQUIRED` denial. */
12
8
  feature?: string;
13
9
  used?: number;
14
10
  limit?: number;
15
11
  upgradeUrl?: string;
16
- /** Best-effort target captured by the shared HTTP client for interactive checkout. */
12
+ reason?: string;
13
+ /** Company target from request context or the server's UID-keyed billing URL. */
17
14
  companyUid?: string;
18
15
  }
16
+ /** One renderer for server-selected upgrade links across CLI plan refusals. */
17
+ export declare function formatUpgradeRefusal(reason: string, upgradeUrl: string | undefined, options?: {
18
+ urlOnly?: boolean;
19
+ }): string;
20
+ /** Recover a company UID only from the server's UID-keyed billing callback. */
21
+ export declare function companyUidFromPlanUpgradeUrl(value: unknown): string | undefined;
19
22
  export declare class PlanGateError extends Error {
20
23
  readonly code: PlanGateCode;
21
24
  readonly details: PlanGateDetails;
22
25
  constructor(code: PlanGateCode, details: PlanGateDetails);
23
26
  }
24
27
  export declare function isPlanGateError(err: unknown): err is PlanGateError;
25
- export declare function formatPlanGateError(err: PlanGateError): string;
28
+ /** Plain, stable CLI copy for plan-gated resource creation. */
29
+ export declare function formatPlanGateError(err: PlanGateError, options?: {
30
+ urlOnly?: boolean;
31
+ }): string;
26
32
  export declare function canOfferTeamUpgrade(argv?: readonly string[]): boolean;
27
- /** Ask once, only on a real interactive terminal. */
33
+ export interface UpgradeLinkOfferDependencies {
34
+ ask?: (question: string) => Promise<string>;
35
+ openUrl?: (url: string) => Promise<void>;
36
+ argv?: readonly string[];
37
+ interactive?: boolean;
38
+ }
39
+ /** Ask before opening the exact URL returned by the server. */
40
+ export declare function offerUpgradeLink(url: string, deps?: UpgradeLinkOfferDependencies): Promise<boolean>;
41
+ /** Legacy checkout callback retained for callers outside the shared refusal paths. */
28
42
  export declare function offerTeamUpgrade(onConfirm: () => Promise<void>, deps?: {
29
43
  ask?: (question: string) => Promise<string>;
30
44
  argv?: readonly string[];
@@ -33,7 +47,7 @@ export declare function offerTeamUpgrade(onConfirm: () => Promise<void>, deps?:
33
47
  /** Decode an already-read JSON envelope from either shared HTTP client. */
34
48
  export declare function planGateErrorFromPayload(status: number, body: unknown, companyUid?: string): PlanGateError | null;
35
49
  /**
36
- * Decode only the two deliberate plan-gate envelopes. Malformed, unrelated,
50
+ * Decode the supported plan-gate envelopes. Malformed, unrelated,
37
51
  * or non-402 responses retain their existing command-specific handling.
38
52
  */
39
53
  export declare function planGateErrorFromResponse(response: Response, companyUid?: string): Promise<{
@@ -4,19 +4,74 @@
4
4
  * message instead of every command parsing and printing a server response.
5
5
  */
6
6
  import { renderPersonalPlanFeatureNotice } from "../lib/plan-limit-nag.js";
7
- const TEAM_UPGRADE_HINT = "Run `hq billing upgrade` to move to HQ Workforce.";
7
+ function planGateReason(code, details) {
8
+ if (details.reason?.trim())
9
+ return details.reason.trim();
10
+ if (code === "PLAN_LIMIT_EXCEEDED" &&
11
+ typeof details.resource === "string" &&
12
+ typeof details.used === "number" &&
13
+ typeof details.limit === "number") {
14
+ return `Starter plan limit reached: ${details.resource} ${details.used}/${details.limit} used.`;
15
+ }
16
+ if (code === "PERSONAL_PLAN_REQUIRED") {
17
+ return renderPersonalPlanFeatureNotice(details.feature ?? "this feature", null);
18
+ }
19
+ if (code === "MEETING_PLAN_REQUIRED") {
20
+ return "HQ Workforce plan required to invite a meeting bot.";
21
+ }
22
+ if (code === "BILLING_REQUIRED") {
23
+ return "Upgrade the company plan before enrolling an external agent.";
24
+ }
25
+ return "HQ Workforce plan required for this feature.";
26
+ }
27
+ /** One renderer for server-selected upgrade links across CLI plan refusals. */
28
+ export function formatUpgradeRefusal(reason, upgradeUrl, options = {}) {
29
+ const safeUrl = safeUpgradeUrl(upgradeUrl);
30
+ if (options.urlOnly && safeUrl)
31
+ return safeUrl;
32
+ if (!safeUrl)
33
+ return reason;
34
+ return `${reason}\nUpgrade: ${safeUrl}`;
35
+ }
36
+ function safeUpgradeUrl(value) {
37
+ if (typeof value !== "string" ||
38
+ value.trim() !== value ||
39
+ !/^https:\/\//i.test(value)) {
40
+ return undefined;
41
+ }
42
+ try {
43
+ const url = new URL(value);
44
+ if (url.protocol !== "https:" ||
45
+ !url.hostname ||
46
+ url.username ||
47
+ url.password) {
48
+ return undefined;
49
+ }
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ return value;
55
+ }
56
+ /** Recover a company UID only from the server's UID-keyed billing callback. */
57
+ export function companyUidFromPlanUpgradeUrl(value) {
58
+ const safeUrl = safeUpgradeUrl(value);
59
+ if (!safeUrl)
60
+ return undefined;
61
+ const callbackUrl = new URL(safeUrl).searchParams.get("callbackUrl");
62
+ if (!callbackUrl)
63
+ return undefined;
64
+ const match = /^\/api\/companies\/(cmp_[A-Za-z0-9_-]+)\/billing\/upgrade\/?$/.exec(callbackUrl);
65
+ return match?.[1];
66
+ }
67
+ function formatPlanGateErrorText(code, details) {
68
+ return formatUpgradeRefusal(planGateReason(code, details), details.upgradeUrl);
69
+ }
8
70
  export class PlanGateError extends Error {
9
71
  code;
10
72
  details;
11
73
  constructor(code, details) {
12
- // Commands with their own expected-error boundary commonly print
13
- // `err.message`. Keeping the friendly copy here means those boundaries
14
- // retain the same plan-gate voice as main.ts without per-command handling.
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"));
74
+ super(formatPlanGateErrorText(code, details));
20
75
  this.code = code;
21
76
  this.details = details;
22
77
  this.name = "PlanGateError";
@@ -26,56 +81,52 @@ export function isPlanGateError(err) {
26
81
  return err instanceof PlanGateError;
27
82
  }
28
83
  /** Plain, stable CLI copy for plan-gated resource creation. */
29
- function formatPlanGateDetails(code, details) {
30
- const existingResourcesNote = "Existing resources will keep working.";
31
- if (code === "PLAN_LIMIT_EXCEEDED" &&
32
- typeof details.resource === "string" &&
33
- typeof details.used === "number" &&
34
- typeof details.limit === "number") {
35
- const upgrade = typeof details.upgradeUrl === "string"
36
- ? `Upgrade to HQ Workforce ($500/mo) to remove limits: ${details.upgradeUrl}`
37
- : "Upgrade to HQ Workforce ($500/mo) to remove limits.";
38
- return [
39
- `Starter plan limit reached: ${details.resource} ${details.used}/${details.limit} used.`,
40
- upgrade,
41
- existingResourcesNote,
42
- ].join("\n");
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
- }
50
- const upgrade = typeof details.upgradeUrl === "string"
51
- ? `Upgrade to HQ Workforce ($500/mo) to remove limits: ${details.upgradeUrl}`
52
- : "Upgrade to HQ Workforce ($500/mo) to remove limits.";
53
- return [
54
- "HQ Workforce plan required for this feature.",
55
- upgrade,
56
- existingResourcesNote,
57
- ].join("\n");
58
- }
59
- export function formatPlanGateError(err) {
60
- if (err.code === "PERSONAL_PLAN_REQUIRED") {
61
- return formatPlanGateDetails(err.code, err.details);
62
- }
63
- return [
64
- formatPlanGateDetails(err.code, err.details),
65
- TEAM_UPGRADE_HINT,
66
- ].join("\n");
84
+ export function formatPlanGateError(err, options = {}) {
85
+ return formatUpgradeRefusal(planGateReason(err.code, err.details), err.details.upgradeUrl, options);
67
86
  }
68
87
  export function canOfferTeamUpgrade(argv = process.argv) {
69
88
  return Boolean(process.stdin.isTTY && process.stderr.isTTY) &&
70
89
  !process.env.CI &&
71
90
  !argv.includes("--json");
72
91
  }
73
- /** Ask once, only on a real interactive terminal. */
92
+ /** Ask before opening the exact URL returned by the server. */
93
+ export async function offerUpgradeLink(url, deps = {}) {
94
+ const safeUrl = safeUpgradeUrl(url);
95
+ if (!safeUrl)
96
+ return false;
97
+ if (!(deps.interactive ?? canOfferTeamUpgrade(deps.argv)))
98
+ return false;
99
+ let answer;
100
+ if (deps.ask) {
101
+ answer = await deps.ask("Open upgrade link now? [y/N] ");
102
+ }
103
+ else {
104
+ const { createInterface } = await import("node:readline/promises");
105
+ const readline = createInterface({ input: process.stdin, output: process.stderr });
106
+ try {
107
+ answer = await readline.question("Open upgrade link now? [y/N] ");
108
+ }
109
+ finally {
110
+ readline.close();
111
+ }
112
+ }
113
+ if (!/^y(?:es)?$/i.test(answer.trim()))
114
+ return false;
115
+ if (deps.openUrl) {
116
+ await deps.openUrl(safeUrl);
117
+ }
118
+ else {
119
+ const { default: open } = await import("open");
120
+ await open(safeUrl);
121
+ }
122
+ return true;
123
+ }
124
+ /** Legacy checkout callback retained for callers outside the shared refusal paths. */
74
125
  export async function offerTeamUpgrade(onConfirm, deps = {}) {
75
126
  if (!(deps.interactive ?? canOfferTeamUpgrade(deps.argv)))
76
127
  return false;
77
128
  if (deps.ask) {
78
- const answer = await deps.ask("Upgrade to HQ Workforce ($500/mo) now? [y/N] ");
129
+ const answer = await deps.ask("Upgrade now? (opens checkout) [y/N] ");
79
130
  if (!/^y(?:es)?$/i.test(answer.trim()))
80
131
  return false;
81
132
  await onConfirm();
@@ -84,7 +135,7 @@ export async function offerTeamUpgrade(onConfirm, deps = {}) {
84
135
  const { createInterface } = await import("node:readline/promises");
85
136
  const readline = createInterface({ input: process.stdin, output: process.stderr });
86
137
  try {
87
- const answer = await readline.question("Upgrade to HQ Workforce ($500/mo) now? [y/N] ");
138
+ const answer = await readline.question("Upgrade now? (opens checkout) [y/N] ");
88
139
  if (!/^y(?:es)?$/i.test(answer.trim()))
89
140
  return false;
90
141
  await onConfirm();
@@ -106,28 +157,52 @@ export function planGateErrorFromPayload(status, body, companyUid) {
106
157
  // through to the company path below (defensive consumer).
107
158
  if (payload.error === "plan_feature_unavailable" &&
108
159
  payload.scope === "personal") {
160
+ const upgradeUrl = safeUpgradeUrl(payload.upgradeUrl);
109
161
  return new PlanGateError("PERSONAL_PLAN_REQUIRED", {
110
162
  ...(typeof payload.feature === "string"
111
163
  ? { feature: payload.feature }
112
164
  : {}),
113
- ...(typeof payload.upgradeUrl === "string"
114
- ? { upgradeUrl: payload.upgradeUrl }
115
- : {}),
165
+ ...(upgradeUrl ? { upgradeUrl } : {}),
116
166
  });
117
167
  }
118
- if (payload.code !== "PLAN_LIMIT_EXCEEDED" && payload.code !== "PLAN_REQUIRED") {
168
+ const code = payload.code;
169
+ const upgradeUrl = safeUpgradeUrl(payload.upgradeUrl);
170
+ const targetCompanyUid = companyUid ?? companyUidFromPlanUpgradeUrl(upgradeUrl);
171
+ const planLimit = payload.planLimit !== null &&
172
+ typeof payload.planLimit === "object" &&
173
+ !Array.isArray(payload.planLimit)
174
+ ? payload.planLimit
175
+ : undefined;
176
+ const isTeamPlanBillingRequired = code === "BILLING_REQUIRED" && planLimit?.requiredPlan === "team";
177
+ if (code === "BILLING_REQUIRED") {
178
+ if (!isTeamPlanBillingRequired ||
179
+ !targetCompanyUid ||
180
+ !/^cmp_[A-Za-z0-9_-]+$/.test(targetCompanyUid)) {
181
+ return null;
182
+ }
183
+ }
184
+ else if (code !== "PLAN_LIMIT_EXCEEDED" &&
185
+ code !== "PLAN_REQUIRED" &&
186
+ code !== "MEETING_PLAN_REQUIRED") {
119
187
  return null;
120
188
  }
121
- return new PlanGateError(payload.code, {
189
+ const reason = typeof payload.message === "string"
190
+ ? payload.message
191
+ : typeof payload.error === "string" && payload.error !== payload.code
192
+ ? payload.error
193
+ : undefined;
194
+ return new PlanGateError(code, {
122
195
  ...(typeof payload.resource === "string" ? { resource: payload.resource } : {}),
196
+ ...(typeof payload.feature === "string" ? { feature: payload.feature } : {}),
123
197
  ...(typeof payload.used === "number" ? { used: payload.used } : {}),
124
198
  ...(typeof payload.limit === "number" ? { limit: payload.limit } : {}),
125
- ...(typeof payload.upgradeUrl === "string" ? { upgradeUrl: payload.upgradeUrl } : {}),
126
- ...(companyUid ? { companyUid } : {}),
199
+ ...(upgradeUrl ? { upgradeUrl } : {}),
200
+ ...(reason ? { reason } : {}),
201
+ ...(targetCompanyUid ? { companyUid: targetCompanyUid } : {}),
127
202
  });
128
203
  }
129
204
  /**
130
- * Decode only the two deliberate plan-gate envelopes. Malformed, unrelated,
205
+ * Decode the supported plan-gate envelopes. Malformed, unrelated,
131
206
  * or non-402 responses retain their existing command-specific handling.
132
207
  */
133
208
  export async function planGateErrorFromResponse(response, companyUid) {
@@ -83,6 +83,8 @@ export type SelfUpdateAction =
83
83
  | "update-ineffective"
84
84
  /** Updated and the command re-ran on the new version; exit with `reexecStatus`. */
85
85
  | "reexec"
86
+ /** Updated with no re-exec (the `hq daemon` updater restarts the daemon instead). */
87
+ | "updated"
86
88
  /**
87
89
  * Unattended caller: replacing the CLI in place is unsafe here, so nothing
88
90
  * was attempted and the command runs on the current version. An attended
@@ -171,4 +173,11 @@ export declare function autoUpdateAndReexec(argv: readonly string[], latest: str
171
173
  * are preserved.
172
174
  */
173
175
  export declare function selfUpdateAndReexec(argv: readonly string[], deps?: SelfUpdateDeps): Promise<SelfUpdateOutcome>;
176
+ /**
177
+ * `hq daemon` updater service: update to npm latest with nobody at a terminal
178
+ * and no re-exec. It runs as its own short-lived child of the daemon, so no
179
+ * user command is waiting on the package being replaced; after an "updated"
180
+ * outcome the daemon restarts itself onto the new version.
181
+ */
182
+ export declare function daemonSelfUpdate(deps?: SelfUpdateDeps): Promise<SelfUpdateOutcome>;
174
183
  //# sourceMappingURL=self-update.d.ts.map
@@ -292,6 +292,10 @@ async function updateAndReexec(argv, flavor, known, deps) {
292
292
  catch {
293
293
  // Defensive only; the default helper already absorbs manager failures.
294
294
  }
295
+ if (!flavor.reexec) {
296
+ console.error(chalk.dim(`✓ hq-cli updated to ${latest}.`));
297
+ return { action: "updated", latest };
298
+ }
295
299
  const childEnv = { ...env, [REEXEC_GUARD_ENV]: "1" };
296
300
  const status = (deps.reexec ?? reexecHq)([...argv.slice(2)], childEnv);
297
301
  if (status === null) {
@@ -309,7 +313,7 @@ async function updateAndReexec(argv, flavor, known, deps) {
309
313
  * this resolve it from the registry.
310
314
  */
311
315
  export async function autoUpdateAndReexec(argv, latest, deps = {}) {
312
- return updateAndReexec(argv, { noun: "command", verbose: false, lock: true, onlyWhenAttended: true }, latest, deps);
316
+ return updateAndReexec(argv, { noun: "command", verbose: false, lock: true, onlyWhenAttended: true, reexec: true }, latest, deps);
313
317
  }
314
318
  /**
315
319
  * Rescue path: update to npm latest and re-exec the rescue on the new version.
@@ -318,6 +322,15 @@ export async function autoUpdateAndReexec(argv, latest, deps = {}) {
318
322
  * are preserved.
319
323
  */
320
324
  export async function selfUpdateAndReexec(argv, deps = {}) {
321
- return updateAndReexec(argv, { noun: "rescue", verbose: true, lock: true, onlyWhenAttended: false }, null, deps);
325
+ return updateAndReexec(argv, { noun: "rescue", verbose: true, lock: true, onlyWhenAttended: false, reexec: true }, null, deps);
326
+ }
327
+ /**
328
+ * `hq daemon` updater service: update to npm latest with nobody at a terminal
329
+ * and no re-exec. It runs as its own short-lived child of the daemon, so no
330
+ * user command is waiting on the package being replaced; after an "updated"
331
+ * outcome the daemon restarts itself onto the new version.
332
+ */
333
+ export async function daemonSelfUpdate(deps = {}) {
334
+ return updateAndReexec([], { noun: "daemon update", verbose: false, lock: true, onlyWhenAttended: false, reexec: false }, null, deps);
322
335
  }
323
336
  //# sourceMappingURL=self-update.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.162.0",
3
+ "version": "5.163.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {