@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.
- package/CHANGELOG.md +70 -0
- package/dist/commands/files.d.ts +11 -0
- package/dist/commands/files.js +206 -30
- package/dist/commands/integrations-api.d.ts +15 -0
- package/dist/commands/integrations-connect.js +84 -3
- package/dist/commands/integrations-oauth.js +62 -3
- package/dist/commands/mcp-registration.d.ts +17 -7
- package/dist/commands/mcp-registration.js +16 -27
- package/dist/commands/mesh.js +174 -50
- package/dist/commands/pack-install.js +5 -5
- package/dist/commands/secrets.d.ts +7 -0
- package/dist/commands/secrets.js +26 -2
- package/dist/lib/mesh/live/backfill-held.d.ts +42 -1
- package/dist/lib/mesh/live/backfill-held.js +95 -13
- package/dist/lib/mesh/live/daemon/doctor.d.ts +15 -0
- package/dist/lib/mesh/live/daemon/doctor.js +41 -10
- package/dist/lib/mesh/live/daemon/mode.d.ts +37 -0
- package/dist/lib/mesh/live/daemon/mode.js +88 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +8 -0
- package/dist/lib/mesh/live/daemon/run.js +39 -28
- package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
- package/dist/lib/mesh/live/emit-client.d.ts +99 -0
- package/dist/lib/mesh/live/emit-client.js +193 -0
- package/dist/lib/mesh/live/emit-evidence.d.ts +49 -0
- package/dist/lib/mesh/live/emit-evidence.js +77 -0
- package/dist/lib/mesh/live/emit-replay.d.ts +26 -0
- package/dist/lib/mesh/live/emit-replay.js +157 -0
- package/dist/lib/mesh/live/emit-retry.d.ts +25 -0
- package/dist/lib/mesh/live/emit-retry.js +79 -0
- package/dist/lib/mesh/live/emit.d.ts +54 -0
- package/dist/lib/mesh/live/emit.js +153 -0
- package/dist/lib/narrow-hint-banner.d.ts +3 -7
- package/dist/lib/narrow-hint-banner.js +13 -34
- package/dist/lib/plan-limit-nag.d.ts +0 -3
- package/dist/lib/plan-limit-nag.js +10 -20
- 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
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
237
|
+
export function isStrictRefusal(syncMode, level) {
|
|
250
238
|
if (syncMode !== "all")
|
|
251
239
|
return false;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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(
|
|
174
|
+
if (!isPlanLimitNagEnabled())
|
|
185
175
|
return;
|
|
186
176
|
if (lastSeen === null)
|
|
187
177
|
return;
|