@indigoai-us/hq-cli 5.108.24 → 5.108.26

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 (36) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/commands/files.d.ts +11 -0
  3. package/dist/commands/files.js +206 -30
  4. package/dist/commands/integrations-api.d.ts +15 -0
  5. package/dist/commands/integrations-connect.js +84 -3
  6. package/dist/commands/integrations-oauth.js +62 -3
  7. package/dist/commands/mcp-registration.d.ts +17 -7
  8. package/dist/commands/mcp-registration.js +16 -27
  9. package/dist/commands/mesh.js +174 -50
  10. package/dist/commands/pack-install.js +5 -5
  11. package/dist/commands/secrets.d.ts +7 -0
  12. package/dist/commands/secrets.js +26 -2
  13. package/dist/lib/mesh/live/backfill-held.d.ts +42 -1
  14. package/dist/lib/mesh/live/backfill-held.js +95 -13
  15. package/dist/lib/mesh/live/daemon/doctor.d.ts +15 -0
  16. package/dist/lib/mesh/live/daemon/doctor.js +41 -10
  17. package/dist/lib/mesh/live/daemon/mode.d.ts +37 -0
  18. package/dist/lib/mesh/live/daemon/mode.js +88 -0
  19. package/dist/lib/mesh/live/daemon/run.d.ts +8 -0
  20. package/dist/lib/mesh/live/daemon/run.js +39 -28
  21. package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
  22. package/dist/lib/mesh/live/emit-client.d.ts +99 -0
  23. package/dist/lib/mesh/live/emit-client.js +193 -0
  24. package/dist/lib/mesh/live/emit-evidence.d.ts +49 -0
  25. package/dist/lib/mesh/live/emit-evidence.js +77 -0
  26. package/dist/lib/mesh/live/emit-replay.d.ts +26 -0
  27. package/dist/lib/mesh/live/emit-replay.js +157 -0
  28. package/dist/lib/mesh/live/emit-retry.d.ts +25 -0
  29. package/dist/lib/mesh/live/emit-retry.js +79 -0
  30. package/dist/lib/mesh/live/emit.d.ts +54 -0
  31. package/dist/lib/mesh/live/emit.js +153 -0
  32. package/dist/lib/narrow-hint-banner.d.ts +3 -7
  33. package/dist/lib/narrow-hint-banner.js +13 -34
  34. package/dist/lib/plan-limit-nag.d.ts +0 -3
  35. package/dist/lib/plan-limit-nag.js +10 -20
  36. package/package.json +1 -1
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Direct-emit orchestration (owner decision 2026-09-08). One invocation:
3
+ * 1. drains the local retry file (prior failures),
4
+ * 2. appends the new event(s),
5
+ * 3. POSTs /v1/mesh/events in batches with a short bounded retry,
6
+ * 4. keeps only network/5xx/429-failed or unaccounted events for next time,
7
+ * 5. records lastPostAt + per-event status counts for `hq mesh doctor`.
8
+ *
9
+ * accepted / unassigned / rejected are all terminal (rejected is dropped with a
10
+ * count — never re-posted). No long-lived process, spool, or held queue.
11
+ */
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import { MESH_EVENTS_BATCH_MAX, parseEmitResults, } from "./emit-client.js";
15
+ import { EMIT_RETRY_MAX, readEmitRetry, writeEmitRetry, } from "./emit-retry.js";
16
+ import { defaultSleep, fullJitterDelayMs, } from "./backoff.js";
17
+ export function emitStatePath(workMeshRoot) {
18
+ return path.join(workMeshRoot, "emit-state.json");
19
+ }
20
+ export function readEmitState(workMeshRoot) {
21
+ try {
22
+ const raw = fs.readFileSync(emitStatePath(workMeshRoot), "utf8");
23
+ const v = JSON.parse(raw);
24
+ if (v && typeof v === "object" && !Array.isArray(v))
25
+ return v;
26
+ }
27
+ catch {
28
+ /* absent / malformed */
29
+ }
30
+ return null;
31
+ }
32
+ function writeEmitState(workMeshRoot, state) {
33
+ const p = emitStatePath(workMeshRoot);
34
+ try {
35
+ fs.mkdirSync(path.dirname(p), { recursive: true, mode: 0o700 });
36
+ const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
37
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 0o600 });
38
+ fs.renameSync(tmp, p);
39
+ }
40
+ catch {
41
+ /* best-effort */
42
+ }
43
+ }
44
+ function eventIdOf(e) {
45
+ return typeof e.eventId === "string" && e.eventId.trim() ? e.eventId.trim() : null;
46
+ }
47
+ /** Drain the retry file, post pending + new events, persist failures. */
48
+ export async function emitEvents(deps) {
49
+ const now = deps.now ?? (() => new Date());
50
+ const sleep = deps.sleep ?? defaultSleep;
51
+ const random = deps.random ?? Math.random;
52
+ const maxAttempts = deps.maxAttempts ?? 3;
53
+ const retryMax = deps.retryMax ?? EMIT_RETRY_MAX;
54
+ // Combine pending retry + new events, deduped by eventId (first wins).
55
+ const pending = readEmitRetry(deps.workMeshRoot);
56
+ const combined = [];
57
+ const seen = new Set();
58
+ for (const e of [...pending, ...(deps.newEvents ?? [])]) {
59
+ const id = eventIdOf(e);
60
+ if (id) {
61
+ if (seen.has(id))
62
+ continue;
63
+ seen.add(id);
64
+ }
65
+ combined.push(e);
66
+ }
67
+ const summary = {
68
+ attempted: combined.length,
69
+ accepted: 0,
70
+ unassigned: 0,
71
+ rejected: 0,
72
+ retained: 0,
73
+ droppedOverflow: 0,
74
+ retryDepth: 0,
75
+ batches: 0,
76
+ statuses: { accepted: 0, unassigned: 0, rejected: 0 },
77
+ };
78
+ const keep = [];
79
+ let anyPosted = false;
80
+ for (let i = 0; i < combined.length; i += MESH_EVENTS_BATCH_MAX) {
81
+ const chunk = combined.slice(i, i + MESH_EVENTS_BATCH_MAX);
82
+ summary.batches += 1;
83
+ let result = await deps.poster(chunk);
84
+ for (let attempt = 1; attempt < maxAttempts && !result.ok && result.retryable; attempt += 1) {
85
+ await sleep(fullJitterDelayMs(attempt - 1, { random }));
86
+ result = await deps.poster(chunk);
87
+ }
88
+ if (!result.ok) {
89
+ if (result.retryable) {
90
+ // Network/5xx/429 after retries → keep the whole chunk for next time.
91
+ keep.push(...chunk);
92
+ deps.log?.(`emit batch retained (${chunk.length}) status=${result.status}`);
93
+ }
94
+ else {
95
+ // Non-retryable 4xx (e.g. 401/400) → drop with a log (never user data).
96
+ summary.rejected += chunk.length;
97
+ summary.statuses.rejected += chunk.length;
98
+ deps.log?.(`emit batch dropped (${chunk.length}) non-retryable status=${result.status}`);
99
+ }
100
+ continue;
101
+ }
102
+ anyPosted = true;
103
+ const results = parseEmitResults(result.body);
104
+ if (!results) {
105
+ // Unparseable 2xx → retain rather than pretend posted.
106
+ keep.push(...chunk);
107
+ deps.log?.(`emit batch retained (${chunk.length}) unparseable 2xx`);
108
+ continue;
109
+ }
110
+ const byId = new Map(results.map((r) => [r.eventId, r]));
111
+ for (const e of chunk) {
112
+ const id = eventIdOf(e);
113
+ const r = id ? byId.get(id) : undefined;
114
+ if (!r) {
115
+ // Unaccounted event in a 2xx → retain (don't lose it).
116
+ keep.push(e);
117
+ continue;
118
+ }
119
+ summary.statuses[r.status] += 1;
120
+ if (r.status === "accepted")
121
+ summary.accepted += 1;
122
+ else if (r.status === "unassigned")
123
+ summary.unassigned += 1;
124
+ else
125
+ summary.rejected += 1;
126
+ }
127
+ }
128
+ const written = writeEmitRetry(deps.workMeshRoot, keep, retryMax);
129
+ summary.retained = written.written;
130
+ summary.droppedOverflow = written.dropped;
131
+ summary.retryDepth = written.written;
132
+ const nowIso = now().toISOString();
133
+ const prior = readEmitState(deps.workMeshRoot) ?? {};
134
+ const nextState = {
135
+ ...prior,
136
+ lastAttemptAt: nowIso,
137
+ lastAccepted: summary.accepted,
138
+ lastUnassigned: summary.unassigned,
139
+ lastRejected: summary.rejected,
140
+ lastRetryDepth: summary.retryDepth,
141
+ };
142
+ if (anyPosted) {
143
+ nextState.lastPostAt = nowIso;
144
+ summary.lastPostAt = nowIso;
145
+ delete nextState.lastError;
146
+ }
147
+ else if (combined.length > 0) {
148
+ nextState.lastError = "emit failed (network/server); retained for retry";
149
+ }
150
+ writeEmitState(deps.workMeshRoot, nextState);
151
+ return summary;
152
+ }
153
+ //# sourceMappingURL=emit.js.map
@@ -22,7 +22,6 @@
22
22
  * - the env var `HQ_SYNC_NARROW_HINT=off` is set,
23
23
  * - the per-hqRoot CLI config (`<hqRoot>/.hq/config.json`) has
24
24
  * `syncNarrowHint: 'off'`,
25
- * - the synced flag registry disables `sync.narrow-hint`,
26
25
  * - or the same `{companyUid, level}` pair has already been shown this
27
26
  * process (module-singleton dedupe; the runner imports the same module
28
27
  * once per `hq` invocation so a single invocation prints at most one
@@ -47,7 +46,6 @@
47
46
  * `syncNarrowHintMinBytes` — see `resolveNarrowHintMinBytes`.
48
47
  */
49
48
  import * as fs from "node:fs";
50
- import { type FlagReader } from "./flag-registry.js";
51
49
  export type BannerLevel = "hint" | "warning" | "strict";
52
50
  /**
53
51
  * Default size gate for the narrow-mode nudge: 5 GiB. A local company folder
@@ -87,8 +85,6 @@ export interface ShouldShowBannerOpts {
87
85
  readFile?: (p: string) => string;
88
86
  /** Test seam: override `fs.existsSync`. */
89
87
  existsFile?: (p: string) => boolean;
90
- /** Test seam: held registry snapshot reader. */
91
- flagReader?: FlagReader;
92
88
  }
93
89
  /**
94
90
  * Decides whether a banner should be printed AT ALL — independent of
@@ -154,12 +150,12 @@ export declare function companyFolderExceedsThreshold(companyDir: string, thresh
154
150
  * `companyFolderExceedsThreshold`). A strict-level all-mode membership whose
155
151
  * folder is under the threshold is never refused.
156
152
  */
157
- export declare function isStrictRefusal(syncMode: BannerInput["syncMode"], level: BannerLevel, flagReader?: FlagReader): boolean;
153
+ export declare function isStrictRefusal(syncMode: BannerInput["syncMode"], level: BannerLevel): boolean;
158
154
  /**
159
155
  * Keep the rendered banner honest about the decision that this invocation
160
156
  * actually made. Hint and warning remain local presentation choices. `strict`
161
- * is reserved for a real refusal, so a registry opt-out of an old strict level
162
- * degrades to the local warning presentation instead of claiming a block.
157
+ * is reserved for a real refusal, so a strict level that did NOT produce a
158
+ * refusal degrades to the local warning presentation instead of claiming a block.
163
159
  */
164
160
  export declare function resolveNarrowHintPresentationLevel(level: BannerLevel, strictRefusal: boolean): BannerLevel;
165
161
  /**
@@ -22,7 +22,6 @@
22
22
  * - the env var `HQ_SYNC_NARROW_HINT=off` is set,
23
23
  * - the per-hqRoot CLI config (`<hqRoot>/.hq/config.json`) has
24
24
  * `syncNarrowHint: 'off'`,
25
- * - the synced flag registry disables `sync.narrow-hint`,
26
25
  * - or the same `{companyUid, level}` pair has already been shown this
27
26
  * process (module-singleton dedupe; the runner imports the same module
28
27
  * once per `hq` invocation so a single invocation prints at most one
@@ -49,7 +48,6 @@
49
48
  import chalk from "chalk";
50
49
  import * as fs from "node:fs";
51
50
  import * as path from "node:path";
52
- import { resolveFlagGate, resolveProcessFlagGate, } from "./flag-registry.js";
53
51
  /**
54
52
  * Default size gate for the narrow-mode nudge: 5 GiB. A local company folder
55
53
  * smaller than this is cheap to keep in full, so all-mode is left alone and no
@@ -101,21 +99,11 @@ export function shouldShowBanner(opts = {}) {
101
99
  }
102
100
  }
103
101
  }
104
- const lookup = {
105
- globalEnvVar: "HQ_SYNC_NARROW_HINT",
106
- globalValueSemantics: {
107
- onValues: [],
108
- offValues: ["off"],
109
- unrecognizedValue: true,
110
- unsetValue: true,
111
- // Deliberately no `trim`: the legacy parser only case-folds.
112
- caseInsensitive: true,
113
- },
114
- fallback: true,
115
- };
116
- return opts.flagReader
117
- ? resolveFlagGate(opts.flagReader, "sync.narrow-hint", lookup, () => true)
118
- : resolveProcessFlagGate("sync.narrow-hint", lookup, () => true);
102
+ // The env var (`HQ_SYNC_NARROW_HINT=off`, evaluated above) and the
103
+ // `.hq/config.json` off-switch are the ONLY ways to suppress the banner. This
104
+ // is a personal preference, not a rollout flag, so nothing else gates it:
105
+ // reaching here means neither off-switch fired, so the banner shows.
106
+ return true;
119
107
  }
120
108
  /**
121
109
  * Resolve the banner level from environment overrides. Defaults to
@@ -246,29 +234,20 @@ export function companyFolderExceedsThreshold(companyDir, thresholdBytes, deps =
246
234
  * `companyFolderExceedsThreshold`). A strict-level all-mode membership whose
247
235
  * folder is under the threshold is never refused.
248
236
  */
249
- export function isStrictRefusal(syncMode, level, flagReader) {
237
+ export function isStrictRefusal(syncMode, level) {
250
238
  if (syncMode !== "all")
251
239
  return false;
252
- const lookup = {
253
- globalEnvVar: "HQ_SYNC_NARROW_HINT_LEVEL",
254
- globalValueSemantics: {
255
- onValues: ["strict"],
256
- offValues: ["hint", "warning"],
257
- unrecognizedValue: false,
258
- unsetValue: false,
259
- caseInsensitive: true,
260
- },
261
- fallback: level === "strict",
262
- };
263
- return flagReader
264
- ? resolveFlagGate(flagReader, "sync.narrow-hint-strict", lookup, () => level === "strict")
265
- : resolveProcessFlagGate("sync.narrow-hint-strict", lookup, () => level === "strict");
240
+ // The refusal is driven purely by the caller's `level`, which is itself
241
+ // resolved from the operator's own `HQ_SYNC_NARROW_HINT_LEVEL` (see
242
+ // `resolveBannerLevel`). This is a personal escalation choice, not a rollout
243
+ // flag, so no registry can independently force or clear the strict refusal.
244
+ return level === "strict";
266
245
  }
267
246
  /**
268
247
  * Keep the rendered banner honest about the decision that this invocation
269
248
  * actually made. Hint and warning remain local presentation choices. `strict`
270
- * is reserved for a real refusal, so a registry opt-out of an old strict level
271
- * degrades to the local warning presentation instead of claiming a block.
249
+ * is reserved for a real refusal, so a strict level that did NOT produce a
250
+ * refusal degrades to the local warning presentation instead of claiming a block.
272
251
  */
273
252
  export function resolveNarrowHintPresentationLevel(level, strictRefusal) {
274
253
  if (strictRefusal)
@@ -15,7 +15,6 @@
15
15
  * Additive only — never throws, never touches `process.exitCode`, never
16
16
  * writes to stdout. Env off-switch: `HQ_NO_PLAN_LIMIT_NAG=1`.
17
17
  */
18
- import { type FlagReader } from "./flag-registry.js";
19
18
  export declare const PLAN_LIMIT_UPGRADE_URL = "https://app.indigo-hq.com/billing/upgrade";
20
19
  export interface PlanLimitEntry {
21
20
  used: number;
@@ -40,8 +39,6 @@ export declare function emitPlanLimitNag(opts?: {
40
39
  write?: (s: string) => void;
41
40
  now?: () => Date;
42
41
  statePath?: string;
43
- /** Test seam: held registry snapshot reader. */
44
- flagReader?: FlagReader;
45
42
  }): void;
46
43
  /** Test-only helper — clears last-seen status and session dedupe flags. */
47
44
  export declare function _resetForTests(): void;
@@ -19,7 +19,6 @@ import chalk from "chalk";
19
19
  import * as fs from "node:fs";
20
20
  import * as os from "node:os";
21
21
  import * as path from "node:path";
22
- import { resolveFlagGate, resolveProcessFlagGate, } from "./flag-registry.js";
23
22
  export const PLAN_LIMIT_UPGRADE_URL = "https://app.indigo-hq.com/billing/upgrade";
24
23
  const DAY_MS = 24 * 60 * 60 * 1000;
25
24
  /** Module-level last-seen cell — overwritten by each successful parse. */
@@ -31,24 +30,15 @@ let overShownThisSession = false;
31
30
  function defaultStatePath() {
32
31
  return path.join(os.homedir(), ".hq", "plan-limit-nag.json");
33
32
  }
34
- function isOptedOut() {
35
- return process.env.HQ_NO_PLAN_LIMIT_NAG === "1";
36
- }
37
- function isPlanLimitNagEnabled(flagReader) {
38
- const lookup = {
39
- globalEnvVar: "HQ_NO_PLAN_LIMIT_NAG",
40
- globalValueSemantics: {
41
- onValues: [],
42
- offValues: ["1"],
43
- unrecognizedValue: true,
44
- unsetValue: true,
45
- },
46
- fallback: true,
47
- };
48
- const legacyFallback = () => !isOptedOut();
49
- return flagReader
50
- ? resolveFlagGate(flagReader, "cli.plan-limit-nag", lookup, legacyFallback)
51
- : resolveProcessFlagGate("cli.plan-limit-nag", lookup, legacyFallback);
33
+ function isPlanLimitNagEnabled() {
34
+ // Personal opt-out only: `HQ_NO_PLAN_LIMIT_NAG=1` silences the nag on THIS
35
+ // machine. This is a user preference, not a rollout flag, so it is read
36
+ // straight from the environment — no registry lookup, so no future
37
+ // registration can revoke the opt-out. The value match is asymmetric on
38
+ // purpose: ONLY the exact value "1" turns the nag off; unset and every other
39
+ // value (including "0" and "false") keep it on. Do not "tidy" this into a
40
+ // boolean parse — that would silently silence anyone who wrote "false".
41
+ return process.env.HQ_NO_PLAN_LIMIT_NAG !== "1";
52
42
  }
53
43
  /**
54
44
  * Defensively parse a single planLimits entry. Returns null if the shape is
@@ -181,7 +171,7 @@ function buildOverBox(overEntries) {
181
171
  */
182
172
  export function emitPlanLimitNag(opts = {}) {
183
173
  try {
184
- if (!isPlanLimitNagEnabled(opts.flagReader))
174
+ if (!isPlanLimitNagEnabled())
185
175
  return;
186
176
  if (lastSeen === null)
187
177
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.108.24",
3
+ "version": "5.108.26",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {