@indigoai-us/hq-cli 5.107.1 → 5.108.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.
@@ -20,7 +20,10 @@
20
20
  * caller-supplied identity outright).
21
21
  * - Sync state is read through the engine's `listJournals()` — the ONLY
22
22
  * correct enumeration of per-scope journals (single-path reconstruction
23
- * regressed before: feedback_9fbf1f82 / feedback_46288b7b).
23
+ * regressed before: feedback_9fbf1f82 / feedback_46288b7b). It stays the
24
+ * only enumeration, but it is now rate-limited rather than run on every
25
+ * command: it is expensive and synchronous, so it could not be bounded on
26
+ * the command path (see {@link JOURNAL_OBSERVATION_TTL_MS}).
24
27
  *
25
28
  * Local state (installation identity + monotonic sequence + sync outcome
26
29
  * counters) lives at `{stateDir}/cli-client-health.json` next to the sync
@@ -35,9 +38,42 @@ import { isExpiring, isMachineIdentity, loadCachedTokens, } from "./cognito-sess
35
38
  import { vaultApiFetch } from "./vault-api.js";
36
39
  import { collectVersions } from "./feedback-versions.js";
37
40
  import { CLIENT_HEALTH_CONTRACT_VERSION, parseClientHealthHeartbeat, } from "./client-health-contract.js";
41
+ import { collectLocalFilesOverview } from "./local-files-overview.js";
38
42
  /** Same bound as CLI telemetry: a heartbeat may never stall a command. */
39
43
  export const CLIENT_HEALTH_TIMEOUT_MS = 1_200;
40
44
  export const CLIENT_HEALTH_STATE_FILE = "cli-client-health.json";
45
+ /**
46
+ * Single-flight marker for the journal observation below. Held only for the
47
+ * duration of one `listJournals()` call.
48
+ */
49
+ export const CLIENT_HEALTH_OBSERVE_LOCK_FILE = "cli-client-health.observe.lock";
50
+ /** Cached journal observation — see {@link JournalObservation}. */
51
+ export const CLIENT_HEALTH_OBSERVATION_FILE = "cli-client-health.observation.json";
52
+ /**
53
+ * How long one journal `lastSync` observation stays usable before an
54
+ * `invocation` heartbeat re-reads it from the engine.
55
+ *
56
+ * `listJournals()` is the correct enumeration, but it is NOT cheap: it returns
57
+ * fully materialized journals, so the engine opens every area of the local
58
+ * sync state store, replays each area's write-ahead log, and deep-clones the
59
+ * whole aggregate file table (`AreaLedger.read` → `readCached` →
60
+ * `primeAggregateCache` → `materializeArea`). On a controller with a large,
61
+ * un-compacted store that measured 21 s of CPU per `hq` invocation — for the
62
+ * ONE field this module actually consumes, `journal.lastSync`.
63
+ *
64
+ * Worse, that work is fully SYNCHRONOUS, so the `settleWithin` bound below
65
+ * cannot preempt it: its timer cannot be serviced until the work it is meant
66
+ * to bound has already finished. Every `hq` command awaits the preAction hook,
67
+ * so an expensive store turned a best-effort telemetry heartbeat into a
68
+ * multi-second stall on every single command, secrets fetches included.
69
+ *
70
+ * The heartbeat only needs a coarse "when did this installation last sync"
71
+ * signal, so one observation is reused for this window instead. Sync commands
72
+ * always re-observe — they have opened the store anyway, so it costs nothing.
73
+ */
74
+ export const JOURNAL_OBSERVATION_TTL_MS = 15 * 60_000;
75
+ /** An observation lock older than this is treated as abandoned. */
76
+ export const JOURNAL_OBSERVATION_LOCK_STALE_MS = 5 * 60_000;
41
77
  /**
42
78
  * Pre-filter mirror of the contract's SemVer shape (the contract keeps its
43
79
  * validators private). A version failing this is DROPPED individually so one
@@ -107,6 +143,38 @@ export function loadClientHealthState(stateDir) {
107
143
  }
108
144
  return state;
109
145
  }
146
+ function observationFilePath(stateDir) {
147
+ return path.join(stateDir, CLIENT_HEALTH_OBSERVATION_FILE);
148
+ }
149
+ /** Load the cached journal observation; any problem reads as "none". */
150
+ export function loadJournalObservation(stateDir) {
151
+ let raw = {};
152
+ try {
153
+ const parsed = JSON.parse(fs.readFileSync(observationFilePath(stateDir), "utf-8"));
154
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
155
+ raw = parsed;
156
+ }
157
+ }
158
+ catch {
159
+ // Missing or corrupt: no usable observation.
160
+ }
161
+ const observation = {};
162
+ if (typeof raw.lastSyncAt === "string")
163
+ observation.lastSyncAt = raw.lastSyncAt;
164
+ if (typeof raw.observedAt === "string")
165
+ observation.observedAt = raw.observedAt;
166
+ return observation;
167
+ }
168
+ /** Best-effort persist — a read-only disk must never fail a command. */
169
+ export function persistJournalObservation(stateDir, observation) {
170
+ try {
171
+ fs.mkdirSync(stateDir, { recursive: true });
172
+ fs.writeFileSync(observationFilePath(stateDir), `${JSON.stringify(observation, null, 2)}\n`, "utf-8");
173
+ }
174
+ catch {
175
+ // Best effort only.
176
+ }
177
+ }
110
178
  /** Best-effort persist — a read-only disk must never fail a command. */
111
179
  export function persistClientHealthState(stateDir, state) {
112
180
  try {
@@ -165,6 +233,87 @@ export function latestJournalSyncTime(journals) {
165
233
  }
166
234
  return latest === null ? null : new Date(latest).toISOString();
167
235
  }
236
+ /**
237
+ * True when this event kind must pay for a fresh `listJournals()` read.
238
+ *
239
+ * Sync events always do: `hq sync` has already opened the state store, so the
240
+ * marginal cost is nil and the resulting heartbeat is the one that most needs
241
+ * an exact timestamp. Plain invocations reuse the last observation until it
242
+ * ages past `ttlMs`, which is what keeps the enumeration off the hot path of
243
+ * every unrelated command.
244
+ */
245
+ export function shouldObserveJournals(kind, observation, now, ttlMs) {
246
+ if (kind !== "invocation")
247
+ return true;
248
+ const observed = typeof observation.observedAt === "string"
249
+ ? Date.parse(observation.observedAt)
250
+ : NaN;
251
+ if (!Number.isFinite(observed))
252
+ return true;
253
+ const age = now.getTime() - observed;
254
+ // A stamp from the future (clock step, shared home directory) is not
255
+ // evidence of a recent read — re-observe rather than trust it forever.
256
+ return age < 0 || age >= ttlMs;
257
+ }
258
+ /**
259
+ * Best-effort exclusive claim on the journal observation, returning a release
260
+ * function, or null when another process already holds it.
261
+ *
262
+ * Fleet workers spawn many `hq` processes at once. Without this, a burst that
263
+ * all see the same expired observation would all pay the full enumeration
264
+ * simultaneously — precisely the CPU storm this change exists to remove. Any
265
+ * failure here resolves toward "do not observe", which is the cheap and safe
266
+ * direction for best-effort telemetry.
267
+ */
268
+ function readObservationLockOwner(lockPath) {
269
+ try {
270
+ return fs.readFileSync(lockPath, "utf-8");
271
+ }
272
+ catch {
273
+ return null;
274
+ }
275
+ }
276
+ function acquireJournalObservationLock(stateDir, nowMs, staleMs) {
277
+ const lockPath = path.join(stateDir, CLIENT_HEALTH_OBSERVE_LOCK_FILE);
278
+ // Reclaim an abandoned lock — but only the EXACT one just inspected. The
279
+ // owner token is re-read immediately before the unlink and the removal is
280
+ // dropped if it changed. Unfenced, two processes arriving together on the
281
+ // same aged lock could both decide to remove it, the second deleting the
282
+ // first's freshly created lock, and both would then run the enumeration this
283
+ // lock exists to serialise.
284
+ const abandoned = readObservationLockOwner(lockPath);
285
+ if (abandoned !== null) {
286
+ try {
287
+ const age = nowMs - fs.statSync(lockPath).mtimeMs;
288
+ const stale = age >= staleMs || age < -staleMs;
289
+ if (stale && readObservationLockOwner(lockPath) === abandoned) {
290
+ fs.rmSync(lockPath, { force: true });
291
+ }
292
+ }
293
+ catch {
294
+ // Vanished or unreadable mid-inspection: the exclusive create below is
295
+ // the real single-flight, so losing this race is safe.
296
+ }
297
+ }
298
+ const owner = `${process.pid}:${randomBytes(8).toString("hex")}`;
299
+ try {
300
+ fs.writeFileSync(lockPath, owner, { flag: "wx" });
301
+ }
302
+ catch {
303
+ return null;
304
+ }
305
+ return () => {
306
+ try {
307
+ // Never remove a lock this process no longer owns.
308
+ if (readObservationLockOwner(lockPath) === owner) {
309
+ fs.rmSync(lockPath, { force: true });
310
+ }
311
+ }
312
+ catch {
313
+ // Best effort; a leaked lock ages out via `staleMs`.
314
+ }
315
+ };
316
+ }
168
317
  /**
169
318
  * Build one contract-valid CLI heartbeat, or null when this environment
170
319
  * cannot be represented in the closed contract enums (unknown platform/arch)
@@ -217,6 +366,10 @@ export function buildCliHeartbeat(input) {
217
366
  if (lastSyncSuccessAt !== null) {
218
367
  heartbeat.lastSyncSuccessAt = lastSyncSuccessAt;
219
368
  }
369
+ if ((input.kind === "sync_success" || input.kind === "sync_failure") &&
370
+ input.localFilesOverview) {
371
+ heartbeat.localFilesOverview = input.localFilesOverview;
372
+ }
220
373
  return heartbeat;
221
374
  }
222
375
  function latestIso(a, b) {
@@ -310,12 +463,60 @@ async function contributeClientHealthUnbounded(kind, deps) {
310
463
  const sequence = nextHeartbeatSequence(state, now.getTime());
311
464
  state.sequence = sequence;
312
465
  persistClientHealthState(stateDir, state);
313
- let journalLastSyncAt = null;
314
- try {
315
- journalLastSyncAt = latestJournalSyncTime((deps.journals ?? listJournals)());
466
+ // Journal enumeration is the single most expensive thing this module can
467
+ // do — see JOURNAL_OBSERVATION_TTL_MS. Reuse the last observation unless
468
+ // this event kind requires a fresh one and no other process is taking it.
469
+ const observation = loadJournalObservation(stateDir);
470
+ let journalLastSyncAt = observation.lastSyncAt ?? null;
471
+ const ttlMs = deps.journalObservationTtlMs ?? JOURNAL_OBSERVATION_TTL_MS;
472
+ if (shouldObserveJournals(kind, observation, now, ttlMs)) {
473
+ const release = acquireJournalObservationLock(stateDir, now.getTime(), deps.journalObservationLockStaleMs ?? JOURNAL_OBSERVATION_LOCK_STALE_MS);
474
+ if (!release) {
475
+ // Another process is producing the authoritative observation right now.
476
+ // Emitting an invocation heartbeat here would carry a value already
477
+ // known to be stale under a HIGHER wall-clock sequence than the
478
+ // in-flight accurate one, and the server's monotonic gate would then
479
+ // discard the good heartbeat. Withhold instead — invocation heartbeats
480
+ // are best effort, and the next one reports the refreshed value. Sync
481
+ // OUTCOME heartbeats are never withheld; they carry a result.
482
+ if (kind === "invocation")
483
+ return;
484
+ }
485
+ else {
486
+ try {
487
+ const observed = latestJournalSyncTime((deps.journals ?? listJournals)());
488
+ if (observed !== null) {
489
+ journalLastSyncAt = observed;
490
+ observation.lastSyncAt = observed;
491
+ }
492
+ // Publish freshness only once the value behind it is ready. Stamping
493
+ // first would let a process that arrives mid-read treat the previous
494
+ // (or absent) value as current.
495
+ //
496
+ // A process that dies inside the engine therefore leaves no stamp —
497
+ // it leaves its LOCK, which holds contenders off until
498
+ // `journalObservationLockStaleMs`. That is what bounds retries of an
499
+ // enumeration already known to be pathological on this machine.
500
+ observation.observedAt = now.toISOString();
501
+ persistJournalObservation(stateDir, observation);
502
+ }
503
+ catch {
504
+ // Journal enumeration is best effort.
505
+ }
506
+ finally {
507
+ release();
508
+ }
509
+ }
316
510
  }
317
- catch {
318
- // Journal enumeration is best effort.
511
+ let localFilesOverview = null;
512
+ if (kind === "sync_success" || kind === "sync_failure") {
513
+ try {
514
+ localFilesOverview = (deps.localFilesOverview ?? collectLocalFilesOverview)();
515
+ }
516
+ catch {
517
+ // Collection is best effort — never blocks or drops the heartbeat.
518
+ localFilesOverview = null;
519
+ }
319
520
  }
320
521
  const heartbeat = buildCliHeartbeat({
321
522
  kind,
@@ -324,6 +525,7 @@ async function contributeClientHealthUnbounded(kind, deps) {
324
525
  versions: (deps.versions ?? collectVersions)(),
325
526
  journalLastSyncAt,
326
527
  now,
528
+ localFilesOverview,
327
529
  });
328
530
  if (!heartbeat)
329
531
  return;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Local log/journal FILE METADATA overview (client-sync-health-control-plane
3
+ * US-016, hq-cli only).
4
+ *
5
+ * Support asked for a bounded overview of a user's local log/journal files
6
+ * so they can debug sync issues without a screenshot. This module collects
7
+ * ONLY closed-enum booleans and bounded integers — existence, size, age, and
8
+ * a capped count of recent error-marker lines — and NEVER a path, NEVER file
9
+ * content, NEVER a line of log text. The returned
10
+ * {@link ClientHealthLocalFilesOverview} is the entire surface that can ever
11
+ * cross the wire; there is no string field on it for anything sensitive to
12
+ * hide inside.
13
+ *
14
+ * Sources (metadata only, via `fs.stat` — full content is read only for the
15
+ * bounded error-line count below, and even then only a capped tail window,
16
+ * and even then only an integer count is ever returned):
17
+ * - the hq-sync desktop app's log file, `~/.hq/logs/hq-sync.log`
18
+ * - the CLI's own per-company sync journals, enumerated via `listJournals()`
19
+ * (never a hand-reconstructed path — see client-health.ts's note on the
20
+ * single-path-reconstruction regression this mirrors).
21
+ *
22
+ * Every fs operation is independently best-effort: a missing file, a
23
+ * permission-denied stat, or a file that disappears mid-read (TOCTOU race)
24
+ * degrades that one field to its "absent" value and never throws. Collection
25
+ * must never break a heartbeat, let alone a CLI command.
26
+ */
27
+ import { type ClientHealthLocalFilesOverview } from "./client-health-contract.js";
28
+ /**
29
+ * Only the last N bytes of the sync log are read for error-line counting —
30
+ * this bounds memory/CPU regardless of how large the on-disk log has grown.
31
+ * Metadata (size/age) still reflects the FULL file via `fs.stat`.
32
+ */
33
+ export declare const LOCAL_FILES_OVERVIEW_TAIL_READ_BYTES: number;
34
+ /**
35
+ * Count lines that look like an error marker in the last
36
+ * {@link LOCAL_FILES_OVERVIEW_TAIL_READ_BYTES} of `filePath`, capped at
37
+ * {@link CLIENT_HEALTH_MAX_ERROR_LINE_COUNT}. Returns 0 on any read failure
38
+ * (missing file, permission denied, race). Only the resulting integer is
39
+ * ever returned — the read buffer never leaves this function.
40
+ */
41
+ export declare function countRecentErrorLines(filePath: string, maxBytes?: number, cap?: number): number;
42
+ export interface CollectLocalFilesOverviewDeps {
43
+ now?: () => Date;
44
+ homeDir?: () => string;
45
+ journals?: () => readonly {
46
+ path: string;
47
+ }[];
48
+ countErrorLines?: (filePath: string) => number;
49
+ }
50
+ /**
51
+ * Collect the bounded local-files overview for the current installation.
52
+ * NEVER throws. When per-company journals exist, `journalExists` is true if
53
+ * ANY journal file is present, sizes are summed (capped), and the age
54
+ * reflects the FRESHEST journal (smallest age) — the signal support cares
55
+ * about is "is this installation still writing journals at all."
56
+ */
57
+ export declare function collectLocalFilesOverview(deps?: CollectLocalFilesOverviewDeps): ClientHealthLocalFilesOverview;
58
+ //# sourceMappingURL=local-files-overview.d.ts.map
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Local log/journal FILE METADATA overview (client-sync-health-control-plane
3
+ * US-016, hq-cli only).
4
+ *
5
+ * Support asked for a bounded overview of a user's local log/journal files
6
+ * so they can debug sync issues without a screenshot. This module collects
7
+ * ONLY closed-enum booleans and bounded integers — existence, size, age, and
8
+ * a capped count of recent error-marker lines — and NEVER a path, NEVER file
9
+ * content, NEVER a line of log text. The returned
10
+ * {@link ClientHealthLocalFilesOverview} is the entire surface that can ever
11
+ * cross the wire; there is no string field on it for anything sensitive to
12
+ * hide inside.
13
+ *
14
+ * Sources (metadata only, via `fs.stat` — full content is read only for the
15
+ * bounded error-line count below, and even then only a capped tail window,
16
+ * and even then only an integer count is ever returned):
17
+ * - the hq-sync desktop app's log file, `~/.hq/logs/hq-sync.log`
18
+ * - the CLI's own per-company sync journals, enumerated via `listJournals()`
19
+ * (never a hand-reconstructed path — see client-health.ts's note on the
20
+ * single-path-reconstruction regression this mirrors).
21
+ *
22
+ * Every fs operation is independently best-effort: a missing file, a
23
+ * permission-denied stat, or a file that disappears mid-read (TOCTOU race)
24
+ * degrades that one field to its "absent" value and never throws. Collection
25
+ * must never break a heartbeat, let alone a CLI command.
26
+ */
27
+ import * as fs from "node:fs";
28
+ import * as os from "node:os";
29
+ import * as path from "node:path";
30
+ import { listJournals } from "@indigoai-us/hq-cloud";
31
+ import { CLIENT_HEALTH_MAX_ERROR_LINE_COUNT, CLIENT_HEALTH_MAX_FILE_AGE_SECONDS, CLIENT_HEALTH_MAX_FILE_SIZE_BYTES, } from "./client-health-contract.js";
32
+ /**
33
+ * Only the last N bytes of the sync log are read for error-line counting —
34
+ * this bounds memory/CPU regardless of how large the on-disk log has grown.
35
+ * Metadata (size/age) still reflects the FULL file via `fs.stat`.
36
+ */
37
+ export const LOCAL_FILES_OVERVIEW_TAIL_READ_BYTES = 2 * 1024 * 1024; // 2 MiB
38
+ /** Case-insensitive marker matched against each line (or JSON-lines `level`). */
39
+ const ERROR_MARKER = /error/i;
40
+ const JSON_LEVEL_ERROR = /"level"\s*:\s*"error"/i;
41
+ const ABSENT = { exists: false, sizeBytes: 0, ageSeconds: 0 };
42
+ function clampInt(value, max) {
43
+ if (!Number.isFinite(value) || value < 0)
44
+ return 0;
45
+ return Math.min(Math.floor(value), max);
46
+ }
47
+ /**
48
+ * Best-effort `fs.lstat`. Never throws: missing/denied/raced degrades to
49
+ * absent. Uses `lstat` (not `stat`) and rejects symlinks outright — a
50
+ * symlinked log/journal path could otherwise point anywhere on disk, and
51
+ * this module has no business following it (defense in depth: even though
52
+ * only bounded numbers ever leave this module, nothing here should read an
53
+ * arbitrary attacker- or misconfiguration-controlled file at all).
54
+ */
55
+ function statFacts(filePath, nowMs) {
56
+ try {
57
+ const stat = fs.lstatSync(filePath);
58
+ if (!stat.isFile())
59
+ return ABSENT;
60
+ const ageSeconds = (nowMs - stat.mtimeMs) / 1000;
61
+ return {
62
+ exists: true,
63
+ sizeBytes: clampInt(stat.size, CLIENT_HEALTH_MAX_FILE_SIZE_BYTES),
64
+ ageSeconds: clampInt(ageSeconds, CLIENT_HEALTH_MAX_FILE_AGE_SECONDS),
65
+ };
66
+ }
67
+ catch {
68
+ return ABSENT;
69
+ }
70
+ }
71
+ /** True only when `filePath` exists, is not a symlink, and is a regular file. */
72
+ function isSafeRegularFile(filePath) {
73
+ try {
74
+ return fs.lstatSync(filePath).isFile();
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ /**
81
+ * Count lines that look like an error marker in the last
82
+ * {@link LOCAL_FILES_OVERVIEW_TAIL_READ_BYTES} of `filePath`, capped at
83
+ * {@link CLIENT_HEALTH_MAX_ERROR_LINE_COUNT}. Returns 0 on any read failure
84
+ * (missing file, permission denied, race). Only the resulting integer is
85
+ * ever returned — the read buffer never leaves this function.
86
+ */
87
+ export function countRecentErrorLines(filePath, maxBytes = LOCAL_FILES_OVERVIEW_TAIL_READ_BYTES, cap = CLIENT_HEALTH_MAX_ERROR_LINE_COUNT) {
88
+ if (!isSafeRegularFile(filePath))
89
+ return 0;
90
+ let fd;
91
+ try {
92
+ fd = fs.openSync(filePath, "r");
93
+ const size = fs.fstatSync(fd).size;
94
+ const readSize = Math.min(size, maxBytes);
95
+ if (readSize <= 0)
96
+ return 0;
97
+ const start = size - readSize;
98
+ const buffer = Buffer.alloc(readSize);
99
+ fs.readSync(fd, buffer, 0, readSize, start);
100
+ const text = buffer.toString("utf-8");
101
+ let count = 0;
102
+ for (const line of text.split("\n")) {
103
+ if (count >= cap)
104
+ break;
105
+ if (ERROR_MARKER.test(line) || JSON_LEVEL_ERROR.test(line))
106
+ count += 1;
107
+ }
108
+ return Math.min(count, cap);
109
+ }
110
+ catch {
111
+ return 0;
112
+ }
113
+ finally {
114
+ if (fd !== undefined) {
115
+ try {
116
+ fs.closeSync(fd);
117
+ }
118
+ catch {
119
+ // Best effort only.
120
+ }
121
+ }
122
+ }
123
+ }
124
+ /**
125
+ * Collect the bounded local-files overview for the current installation.
126
+ * NEVER throws. When per-company journals exist, `journalExists` is true if
127
+ * ANY journal file is present, sizes are summed (capped), and the age
128
+ * reflects the FRESHEST journal (smallest age) — the signal support cares
129
+ * about is "is this installation still writing journals at all."
130
+ */
131
+ export function collectLocalFilesOverview(deps = {}) {
132
+ const now = (deps.now ?? (() => new Date()))();
133
+ const nowMs = now.getTime();
134
+ const homeDir = (deps.homeDir ?? os.homedir)();
135
+ const countErrorLines = deps.countErrorLines ?? countRecentErrorLines;
136
+ const syncLogPath = path.join(homeDir, ".hq", "logs", "hq-sync.log");
137
+ const syncLogFacts = statFacts(syncLogPath, nowMs);
138
+ const recentErrorLineCount = syncLogFacts.exists ? countErrorLines(syncLogPath) : 0;
139
+ let journalEntries;
140
+ try {
141
+ journalEntries = (deps.journals ?? listJournals)();
142
+ }
143
+ catch {
144
+ // Journal enumeration is best effort — mirrors client-health.ts.
145
+ journalEntries = [];
146
+ }
147
+ let journalExists = false;
148
+ let journalSizeBytesTotal = 0;
149
+ let journalAgeSecondsMin;
150
+ for (const entry of journalEntries) {
151
+ const facts = statFacts(entry.path, nowMs);
152
+ if (!facts.exists)
153
+ continue;
154
+ journalExists = true;
155
+ journalSizeBytesTotal += facts.sizeBytes;
156
+ if (journalAgeSecondsMin === undefined || facts.ageSeconds < journalAgeSecondsMin) {
157
+ journalAgeSecondsMin = facts.ageSeconds;
158
+ }
159
+ }
160
+ return {
161
+ syncLogExists: syncLogFacts.exists,
162
+ syncLogSizeBytes: syncLogFacts.sizeBytes,
163
+ syncLogAgeSeconds: syncLogFacts.ageSeconds,
164
+ journalExists,
165
+ journalSizeBytes: clampInt(journalSizeBytesTotal, CLIENT_HEALTH_MAX_FILE_SIZE_BYTES),
166
+ journalAgeSeconds: clampInt(journalAgeSecondsMin ?? 0, CLIENT_HEALTH_MAX_FILE_AGE_SECONDS),
167
+ recentErrorLineCount: clampInt(recentErrorLineCount, CLIENT_HEALTH_MAX_ERROR_LINE_COUNT),
168
+ };
169
+ }
170
+ //# sourceMappingURL=local-files-overview.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.107.1",
3
+ "version": "5.108.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -31,6 +31,7 @@
31
31
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
32
32
  "@aws-sdk/client-s3": "^3.1049.0",
33
33
  "@indigoai-us/hq-cloud": "~6.16.6",
34
+ "@indigoai-us/hq-flags-client": "^0.1.2",
34
35
  "@indigoai-us/hq-onboarding": "^0.1.0",
35
36
  "@sentry/node": "^10.49.0",
36
37
  "@tobilu/qmd": "2.5.3",