@indigoai-us/hq-cli 5.108.5 → 5.108.7
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 +134 -0
- package/dist/commands/auth.js +11 -4
- package/dist/commands/feedback.d.ts +38 -0
- package/dist/commands/feedback.js +80 -0
- package/dist/commands/signals.js +12 -0
- package/dist/commands/sources.js +12 -0
- package/dist/commands/whoami.js +7 -2
- package/dist/utils/cognito-session.d.ts +58 -16
- package/dist/utils/cognito-session.js +229 -39
- package/dist/utils/feedback-diagnostics.d.ts +11 -0
- package/dist/utils/feedback-log-bundle.d.ts +149 -0
- package/dist/utils/feedback-log-bundle.js +430 -0
- package/dist/utils/feedback-logs.d.ts +329 -0
- package/dist/utils/feedback-logs.js +669 -0
- package/package.json +2 -2
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug-log and state capture for `hq feedback` submissions.
|
|
3
|
+
*
|
|
4
|
+
* A bug report is far cheaper to triage when the submitter's recent log output
|
|
5
|
+
* travels with it. This module collects two bounded, redacted classes of
|
|
6
|
+
* evidence from the user's `~/.hq` directory and hands them to the feedback
|
|
7
|
+
* command, which attaches them to the `diagnostics` blob on the submission:
|
|
8
|
+
*
|
|
9
|
+
* - LOG TAILS (`files`) — the end of each rolling log. One file covers more
|
|
10
|
+
* than its name suggests: `logs/hq-sync.log` is the shared diagnostic log
|
|
11
|
+
* for BOTH the hq-cloud sync engine and the desktop sync app, which write
|
|
12
|
+
* to it through the same path, and its ROTATED generations
|
|
13
|
+
* (`hq-sync.log.1`, `.2`, `.3`) are eligible too — just after a rotation
|
|
14
|
+
* the active file is nearly empty and the history lives in `.1`.
|
|
15
|
+
* `logs/mcp-registry.log` is the CLI's own append-only MCP audit trail.
|
|
16
|
+
* Any future `*.log`/`*.jsonl` dropped into `~/.hq/logs/` is picked up
|
|
17
|
+
* automatically.
|
|
18
|
+
*
|
|
19
|
+
* - STATE SNAPSHOTS (`state`) — small JSON files that describe where sync
|
|
20
|
+
* and client health currently stand: the in-flight sync phase and conflict
|
|
21
|
+
* count, the client-health failure streak, the per-company sync journal
|
|
22
|
+
* locators. These are tiny and answer the questions a log tail usually
|
|
23
|
+
* cannot ("was it mid-pull?", "how many conflicts?", "how long since the
|
|
24
|
+
* last successful sync?"), so they are collected FIRST and the log budget
|
|
25
|
+
* takes what remains.
|
|
26
|
+
*
|
|
27
|
+
* Contrast with `local-files-overview.ts`, which deliberately sends only
|
|
28
|
+
* metadata (existence, size, age, error-line counts) and NEVER a line of log
|
|
29
|
+
* text. That module feeds the BACKGROUND client-health heartbeat, where the
|
|
30
|
+
* user has not asked for anything and content would be surveillance. This
|
|
31
|
+
* module runs only on an EXPLICIT, user-initiated `hq feedback` invocation,
|
|
32
|
+
* where the user is asking us to look at their problem, so content is both
|
|
33
|
+
* expected and useful. The two must not be merged.
|
|
34
|
+
*
|
|
35
|
+
* Three hard safety properties, each with a regression test:
|
|
36
|
+
*
|
|
37
|
+
* 1. ALLOWLIST, NOT A SCAN. `~/.hq` also holds `cognito-tokens.json`,
|
|
38
|
+
* `deploy-passwords.json`, and `secrets-cache/`. Only `*.log` files
|
|
39
|
+
* directly inside `~/.hq/logs/` plus the two known root-level boot logs
|
|
40
|
+
* are ever eligible. A recursive walk over `~/.hq` is a credential leak
|
|
41
|
+
* and must never be introduced here.
|
|
42
|
+
*
|
|
43
|
+
* 2. NO SYMLINKS. Eligibility is decided with `lstat`, and a candidate must
|
|
44
|
+
* be a REGULAR file. Without this, a symlink named `anything.log` inside
|
|
45
|
+
* `~/.hq/logs/` pointing at `~/.hq/cognito-tokens.json` would satisfy the
|
|
46
|
+
* allowlist and exfiltrate the user's tokens. `stat` would follow the link
|
|
47
|
+
* and report a regular file — `lstat` is the whole guard.
|
|
48
|
+
*
|
|
49
|
+
* 3. REDACTION IS APPLIED BEFORE BUDGETING. Every tail passes through
|
|
50
|
+
* `redactLogText` before it is measured or returned, so no code path can
|
|
51
|
+
* emit an unredacted span. Redaction is defence in depth, not the primary
|
|
52
|
+
* control — properties 1 and 2 are what keep secrets out.
|
|
53
|
+
*
|
|
54
|
+
* Every filesystem operation is independently best-effort: a missing file, a
|
|
55
|
+
* permission error, or a file that disappears mid-read degrades that one entry
|
|
56
|
+
* and never throws. Collecting diagnostics must never break a bug report.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Ceiling on the total redacted log text attached to one submission.
|
|
60
|
+
*
|
|
61
|
+
* The server caps the WHOLE feedback request body at 64 KiB, so this is only
|
|
62
|
+
* an upper bound — the caller passes a smaller `budgetBytes` computed from the
|
|
63
|
+
* headroom actually left after the title, body, and the rest of diagnostics.
|
|
64
|
+
* See `submitFeedback` in `../commands/feedback.ts`.
|
|
65
|
+
*/
|
|
66
|
+
export declare const FEEDBACK_LOGS_MAX_TOTAL_BYTES: number;
|
|
67
|
+
/** Per-file ceiling, so one noisy log cannot consume the entire budget. */
|
|
68
|
+
export declare const FEEDBACK_LOGS_MAX_FILE_TAIL_BYTES: number;
|
|
69
|
+
/**
|
|
70
|
+
* Below this much remaining budget a tail is too short to be worth reading,
|
|
71
|
+
* so the file is recorded as skipped rather than truncated into noise.
|
|
72
|
+
*/
|
|
73
|
+
export declare const FEEDBACK_LOGS_MIN_USEFUL_BYTES = 1024;
|
|
74
|
+
/**
|
|
75
|
+
* Ceiling on how many skipped entries are recorded.
|
|
76
|
+
*
|
|
77
|
+
* `skipped` is metadata, not payload, so it was originally left outside the
|
|
78
|
+
* byte budget — which made it unbounded. An installation with hundreds of log
|
|
79
|
+
* files produced hundreds of `{name, reason}` objects and a blob several times
|
|
80
|
+
* the requested budget; `attachDebugLogs` then dropped the whole thing to stay
|
|
81
|
+
* under the request cap, so the user with the MOST log history got the LEAST
|
|
82
|
+
* evidence. The overflow count preserves the signal without the bytes.
|
|
83
|
+
*/
|
|
84
|
+
export declare const FEEDBACK_MAX_SKIPPED_ENTRIES = 25;
|
|
85
|
+
/** Subdirectory of `~/.hq` whose log files are eligible. Not recursive. */
|
|
86
|
+
export declare const FEEDBACK_LOGS_DIRNAME = "logs";
|
|
87
|
+
/**
|
|
88
|
+
* Eligible extensions inside `~/.hq/logs/`. `.jsonl` is included because the
|
|
89
|
+
* CLI's MCP registry audit trail is JSON-lines, not plain text.
|
|
90
|
+
*/
|
|
91
|
+
export declare const FEEDBACK_LOG_EXTENSIONS: readonly string[];
|
|
92
|
+
/**
|
|
93
|
+
* True for a log filename, including a ROTATED generation.
|
|
94
|
+
*
|
|
95
|
+
* The desktop/sync logger keeps 32 MiB x 3 generations named `hq-sync.log.1`,
|
|
96
|
+
* `.2`, `.3` (see hq-desktop-core `logfile.rs`). A plain "ends with .log"
|
|
97
|
+
* test misses every one of them — and they matter most in the worst case: just
|
|
98
|
+
* after a rotation the active log is nearly empty and all the history a
|
|
99
|
+
* triager needs sits in `.1`.
|
|
100
|
+
*/
|
|
101
|
+
export declare function isEligibleLogName(name: string): boolean;
|
|
102
|
+
/** Per-state-file ceiling. These are status documents, not streams. */
|
|
103
|
+
export declare const FEEDBACK_STATE_MAX_FILE_BYTES: number;
|
|
104
|
+
/**
|
|
105
|
+
* Ceiling on all state snapshots combined. Deliberately a small slice of the
|
|
106
|
+
* overall budget: state is dense but finite, and the rest belongs to logs.
|
|
107
|
+
*/
|
|
108
|
+
export declare const FEEDBACK_STATE_MAX_TOTAL_BYTES: number;
|
|
109
|
+
/**
|
|
110
|
+
* State files at the `~/.hq` ROOT that are eligible, by EXACT name.
|
|
111
|
+
*
|
|
112
|
+
* The root is where `cognito-tokens.json`, `deploy-passwords.json`, and
|
|
113
|
+
* `secrets-cache/` live, so it is never globbed. Adding a name here is a
|
|
114
|
+
* deliberate act: confirm the file carries no credential before listing it.
|
|
115
|
+
*/
|
|
116
|
+
export declare const FEEDBACK_STATE_FILENAMES: readonly string[];
|
|
117
|
+
/**
|
|
118
|
+
* Per-company sync journal LOCATORS (`sync-journal.<slug>.json`) are also
|
|
119
|
+
* eligible. Matched by an exact prefix AND a `.json` suffix, which is what
|
|
120
|
+
* excludes the multi-hundred-megabyte `…json.last-good` snapshots sitting
|
|
121
|
+
* beside them. No credential file at the root carries this prefix.
|
|
122
|
+
*/
|
|
123
|
+
export declare const FEEDBACK_STATE_JOURNAL_PREFIX = "sync-journal.";
|
|
124
|
+
/**
|
|
125
|
+
* `~/.hq/locks/` — held operation locks and background-work claims.
|
|
126
|
+
*
|
|
127
|
+
* The highest-signal thing in the whole collection, and the one place where the
|
|
128
|
+
* FILENAME is the diagnosis: a `…stale-claim…` entry names the operation, the
|
|
129
|
+
* process that abandoned it, and the fact that the claim went stale. That is
|
|
130
|
+
* the answer to "sync/reindex is stuck", which is otherwise invisible in a log
|
|
131
|
+
* tail because a hung process writes nothing. Contents are tiny (tens to a
|
|
132
|
+
* couple hundred bytes) and included too.
|
|
133
|
+
*/
|
|
134
|
+
export declare const FEEDBACK_LOCKS_DIRNAME = "locks";
|
|
135
|
+
/**
|
|
136
|
+
* `~/.hq/jobs/` — scheduled-job status, reconcile results, and probe attempts.
|
|
137
|
+
* Nested one or two levels (`jobs/probes/<job>/last-attempt.json`), so this is
|
|
138
|
+
* the only source needing a directory walk. See {@link walkJsonFiles} for the
|
|
139
|
+
* bounds that walk carries.
|
|
140
|
+
*/
|
|
141
|
+
export declare const FEEDBACK_JOBS_DIRNAME = "jobs";
|
|
142
|
+
/** Per-entry ceiling for a lock file. Their value is the name plus a little context. */
|
|
143
|
+
export declare const FEEDBACK_LOCK_MAX_FILE_BYTES = 512;
|
|
144
|
+
/**
|
|
145
|
+
* Bounds on the `jobs/` walk. Depth 3 reaches
|
|
146
|
+
* `jobs/probes/<job>/last-attempt.json`; the file cap stops a pathological
|
|
147
|
+
* tree from crowding out logs.
|
|
148
|
+
*/
|
|
149
|
+
export declare const FEEDBACK_JOBS_MAX_DEPTH = 3;
|
|
150
|
+
export declare const FEEDBACK_JOBS_MAX_FILES = 20;
|
|
151
|
+
/** Ceiling on how many lock entries are collected. */
|
|
152
|
+
export declare const FEEDBACK_LOCKS_MAX_FILES = 20;
|
|
153
|
+
/**
|
|
154
|
+
* Log files that live at the root of `~/.hq` rather than under `logs/`.
|
|
155
|
+
* Listed explicitly — the root directory holds credentials and must never be
|
|
156
|
+
* enumerated by pattern.
|
|
157
|
+
*/
|
|
158
|
+
export declare const FEEDBACK_ROOT_LOG_FILENAMES: readonly string[];
|
|
159
|
+
/** Why a discovered log file contributed nothing to the submission. */
|
|
160
|
+
export type SkipReason = "budget" | "empty" | "unreadable";
|
|
161
|
+
export interface LogTail {
|
|
162
|
+
/** Path relative to `~/.hq`, e.g. `logs/hq-sync.log`. Never absolute. */
|
|
163
|
+
name: string;
|
|
164
|
+
/** Full on-disk size, even when only a tail was included. */
|
|
165
|
+
sizeBytes: number;
|
|
166
|
+
/** Last-modified time, or null when it could not be read. */
|
|
167
|
+
modifiedIso: string | null;
|
|
168
|
+
/** Byte length of `tail` after redaction. */
|
|
169
|
+
includedBytes: number;
|
|
170
|
+
/** True when `tail` is a suffix of a larger file. */
|
|
171
|
+
truncated: boolean;
|
|
172
|
+
/** How many spans redaction replaced. Non-zero is expected and fine. */
|
|
173
|
+
redactions: number;
|
|
174
|
+
/** The redacted tail itself. */
|
|
175
|
+
tail: string;
|
|
176
|
+
}
|
|
177
|
+
/** A small JSON status document included whole (or head-truncated). */
|
|
178
|
+
export interface StateSnapshot {
|
|
179
|
+
/** Path relative to `~/.hq`, e.g. `sync-progress.json`. Never absolute. */
|
|
180
|
+
name: string;
|
|
181
|
+
sizeBytes: number;
|
|
182
|
+
modifiedIso: string | null;
|
|
183
|
+
includedBytes: number;
|
|
184
|
+
/** True when the file exceeded the per-file ceiling and was cut. */
|
|
185
|
+
truncated: boolean;
|
|
186
|
+
redactions: number;
|
|
187
|
+
/** The redacted document text. */
|
|
188
|
+
content: string;
|
|
189
|
+
}
|
|
190
|
+
export interface FeedbackLogsBlob {
|
|
191
|
+
/** The budget this collection was given, for interpreting truncation. */
|
|
192
|
+
budgetBytes: number;
|
|
193
|
+
/** Included tails, newest-modified first. */
|
|
194
|
+
files: LogTail[];
|
|
195
|
+
/** Sync and client-health status documents. Collected before the tails. */
|
|
196
|
+
state: StateSnapshot[];
|
|
197
|
+
/**
|
|
198
|
+
* Discovered files that contributed nothing, and why. Capped at
|
|
199
|
+
* {@link FEEDBACK_MAX_SKIPPED_ENTRIES}; see `skippedOverflow`.
|
|
200
|
+
*/
|
|
201
|
+
skipped: Array<{
|
|
202
|
+
name: string;
|
|
203
|
+
reason: SkipReason;
|
|
204
|
+
}>;
|
|
205
|
+
/**
|
|
206
|
+
* How many further files were skipped but not listed individually. Absent
|
|
207
|
+
* when nothing overflowed.
|
|
208
|
+
*/
|
|
209
|
+
skippedOverflow?: number;
|
|
210
|
+
}
|
|
211
|
+
export interface LogCandidate {
|
|
212
|
+
name: string;
|
|
213
|
+
absPath: string;
|
|
214
|
+
sizeBytes: number;
|
|
215
|
+
modifiedMs: number;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Replace every secret-shaped span in `text`. Returns the redacted text and a
|
|
219
|
+
* count of replacements, so triage can tell "no secrets present" from
|
|
220
|
+
* "redaction never ran".
|
|
221
|
+
*/
|
|
222
|
+
export declare function redactLogText(text: string): {
|
|
223
|
+
text: string;
|
|
224
|
+
redactions: number;
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* `true` unless the user disabled log capture via `HQ_FEEDBACK_LOGS`.
|
|
228
|
+
*
|
|
229
|
+
* Uses `?.trim() ||` rather than `??` on purpose: an env var that is SET BUT
|
|
230
|
+
* EMPTY must fall back to the default, and `??` would pass `""` through as a
|
|
231
|
+
* deliberate choice.
|
|
232
|
+
*/
|
|
233
|
+
export declare function feedbackLogsEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
234
|
+
/**
|
|
235
|
+
* Enumerate eligible log files under `hqDir`.
|
|
236
|
+
*
|
|
237
|
+
* Eligibility is the allowlist described in the module header: `*.log` files
|
|
238
|
+
* directly inside `hqDir/logs/`, plus {@link FEEDBACK_ROOT_LOG_FILENAMES} at
|
|
239
|
+
* the root. Every candidate must be a REGULAR file by `lstat` — directories
|
|
240
|
+
* and, critically, symlinks are rejected. Returned newest-modified first so
|
|
241
|
+
* the most relevant log is offered the budget before older ones.
|
|
242
|
+
*/
|
|
243
|
+
export declare function discoverLogFiles(hqDir: string): LogCandidate[];
|
|
244
|
+
/**
|
|
245
|
+
* Read the last `maxBytes` of a file.
|
|
246
|
+
*
|
|
247
|
+
* When the file is larger than the window, the first (partial) line is dropped
|
|
248
|
+
* so the tail always starts at a line boundary. Returns null if the file
|
|
249
|
+
* cannot be read.
|
|
250
|
+
*/
|
|
251
|
+
export declare function readLogTail(absPath: string, maxBytes: number): {
|
|
252
|
+
text: string;
|
|
253
|
+
truncated: boolean;
|
|
254
|
+
} | null;
|
|
255
|
+
/**
|
|
256
|
+
* Enumerate eligible state files at the `~/.hq` root.
|
|
257
|
+
*
|
|
258
|
+
* Two ways in, both narrow: an exact name in {@link FEEDBACK_STATE_FILENAMES},
|
|
259
|
+
* or the `sync-journal.<slug>.json` shape. As with logs, every candidate must
|
|
260
|
+
* be a REGULAR file by `lstat`, so a symlink cannot be used to reach a
|
|
261
|
+
* credential file through an eligible-looking name.
|
|
262
|
+
*/
|
|
263
|
+
export declare function discoverStateFiles(hqDir: string): LogCandidate[];
|
|
264
|
+
/**
|
|
265
|
+
* Enumerate flat, non-recursive entries of a `~/.hq` subdirectory.
|
|
266
|
+
*
|
|
267
|
+
* Used for `locks/`, where every entry is eligible regardless of extension —
|
|
268
|
+
* the filename itself carries the diagnosis. As everywhere else in this module,
|
|
269
|
+
* `lstat` decides eligibility, so a symlink dropped into the directory is
|
|
270
|
+
* skipped rather than followed.
|
|
271
|
+
*
|
|
272
|
+
* Results are sorted NEWEST-FIRST and only then truncated to `maxFiles`, which
|
|
273
|
+
* is load-bearing rather than cosmetic. Sorting by name and cutting at the cap
|
|
274
|
+
* silently drops the most diagnostic entry: ordinary `operation-*.lock` files
|
|
275
|
+
* sort ahead of a `qmd-reindex-bg.claim…stale-claim…`, so an installation with
|
|
276
|
+
* more than `maxFiles` locks would have excluded exactly the entry this source
|
|
277
|
+
* exists to capture. Recency is the right proxy for relevance here, and it
|
|
278
|
+
* matches how log files are prioritised.
|
|
279
|
+
*/
|
|
280
|
+
export declare function discoverFlatDirFiles(hqDir: string, dirName: string, maxFiles: number): LogCandidate[];
|
|
281
|
+
/**
|
|
282
|
+
* Walk a `~/.hq` subdirectory for `.json` files, bounded in BOTH depth and
|
|
283
|
+
* count.
|
|
284
|
+
*
|
|
285
|
+
* This is the module's only recursive traversal, so it is deliberately the
|
|
286
|
+
* most constrained. Three bounds, each load-bearing:
|
|
287
|
+
*
|
|
288
|
+
* - `lstat` per entry, and recursion only into a REAL directory. A symlinked
|
|
289
|
+
* directory is never followed, so a link at `jobs/x -> /home/user` cannot
|
|
290
|
+
* turn a two-level walk into a scan of the home directory — where, as it
|
|
291
|
+
* happens, `~/.codex/auth.json` and `~/.hq-agent/machine-creds.json` live.
|
|
292
|
+
* This is the same guard the flat enumerations use, applied to directories
|
|
293
|
+
* rather than files, and it is what keeps "walk a subtree" from being a
|
|
294
|
+
* categorically riskier operation than the allowlists around it.
|
|
295
|
+
* - `maxDepth`, so a deep tree cannot cost unbounded stat calls.
|
|
296
|
+
* - `maxFiles`, so a wide tree cannot crowd the log budget.
|
|
297
|
+
*
|
|
298
|
+
* Entries are returned sorted by name for a stable, reviewable order.
|
|
299
|
+
*/
|
|
300
|
+
export declare function walkJsonFiles(hqDir: string, dirName: string, maxDepth: number, maxFiles: number): LogCandidate[];
|
|
301
|
+
/**
|
|
302
|
+
* Read up to `maxBytes` from the START of a file. State documents are JSON,
|
|
303
|
+
* whose meaningful keys sit at the top, so a head is more useful than a tail.
|
|
304
|
+
* Returns null if the file cannot be read.
|
|
305
|
+
*/
|
|
306
|
+
export declare function readFileHead(absPath: string, maxBytes: number): {
|
|
307
|
+
text: string;
|
|
308
|
+
truncated: boolean;
|
|
309
|
+
} | null;
|
|
310
|
+
export interface CollectFeedbackLogsOptions {
|
|
311
|
+
/** Total redacted bytes this collection may emit. Clamped to the max. */
|
|
312
|
+
budgetBytes?: number;
|
|
313
|
+
/** Override `~/.hq` (tests). */
|
|
314
|
+
hqDir?: string;
|
|
315
|
+
/** Override the home directory used to derive `~/.hq` (tests). */
|
|
316
|
+
homeDir?: string;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Collect redacted `~/.hq` evidence within `budgetBytes`.
|
|
320
|
+
*
|
|
321
|
+
* State snapshots are taken first, capped by
|
|
322
|
+
* {@link FEEDBACK_STATE_MAX_TOTAL_BYTES}, because they are small and answer
|
|
323
|
+
* questions a log tail cannot. Log tails then take whatever budget remains.
|
|
324
|
+
*
|
|
325
|
+
* Returns `undefined` when nothing eligible exists at all, so the caller can
|
|
326
|
+
* omit the field entirely rather than attach an empty object. Never throws.
|
|
327
|
+
*/
|
|
328
|
+
export declare function collectFeedbackLogs(opts?: CollectFeedbackLogsOptions): FeedbackLogsBlob | undefined;
|
|
329
|
+
//# sourceMappingURL=feedback-logs.d.ts.map
|