@indigoai-us/hq-cli 5.108.4 → 5.108.6
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 +126 -0
- package/dist/commands/feedback.d.ts +38 -0
- package/dist/commands/feedback.js +80 -0
- package/dist/commands/meetings.js +55 -1
- package/dist/commands/signals.js +12 -0
- package/dist/commands/sources.js +12 -0
- package/dist/utils/client-health.js +17 -1
- 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/dist/utils/self-update.d.ts +20 -0
- package/dist/utils/self-update.js +22 -1
- package/dist/utils/version-check.d.ts +7 -0
- package/dist/utils/version-check.js +46 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,132 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.108.6] — 2026-09-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `hq feedback bug|feature` now uploads a full log bundle alongside the report,
|
|
10
|
+
so a bug report carries the submitter's whole recent log history rather than a
|
|
11
|
+
16 KiB sliver of it. Measured against a real `~/.hq`: 21 MB of logs across 21
|
|
12
|
+
files, compressed to 1.3 MB — roughly 500x what the inline path can hold.
|
|
13
|
+
|
|
14
|
+
The inline `diagnostics.logs` summary is unchanged and still ships in the
|
|
15
|
+
request body. It cannot be grown: the server rejects a request over 64 KiB and
|
|
16
|
+
stores `diagnostics` in a DynamoDB item, which caps at 400 KB. So the bundle
|
|
17
|
+
goes out of band — gzipped and uploaded direct to S3 through a presigned URL,
|
|
18
|
+
the same path screenshots already use — and the report carries only a
|
|
19
|
+
reference to it.
|
|
20
|
+
|
|
21
|
+
Three properties are load-bearing and are covered by tests that fail if any is
|
|
22
|
+
removed:
|
|
23
|
+
|
|
24
|
+
- **Same allowlist.** Discovery is reused from the inline collector verbatim.
|
|
25
|
+
The bundle carries more OF the same files, never more files. Credential
|
|
26
|
+
material lives outside `~/.hq` (`~/.codex/auth.json`,
|
|
27
|
+
`~/.hq-agent/machine-creds.json`), so widening discovery — not raising the
|
|
28
|
+
size — is what would turn this into an exfiltration bug.
|
|
29
|
+
- **Same redaction.** Every chunk passes through the existing redactor before
|
|
30
|
+
compression. A chunk boundary is held open rather than cut through a PEM
|
|
31
|
+
block, and an over-long line is cut on whitespace, so a split can never
|
|
32
|
+
bisect a token and let the halves escape a pattern that would have matched
|
|
33
|
+
the whole.
|
|
34
|
+
- **Bounded memory.** 50 MB compressed is roughly a gigabyte of raw text.
|
|
35
|
+
Nothing is fully materialised: files are read in slices, redacted, and
|
|
36
|
+
streamed into gzip, retaining only the compressed output.
|
|
37
|
+
|
|
38
|
+
Ordering is the truncation policy — status documents first, then logs newest
|
|
39
|
+
first, so a capped run keeps what a triager reads first. Oversized files are
|
|
40
|
+
read from the tail, because the end of a log is what explains a failure.
|
|
41
|
+
|
|
42
|
+
`--no-logs` and `HQ_FEEDBACK_LOGS=0` disable the bundle and the inline logs
|
|
43
|
+
together. Every failure is silent and non-fatal: a missing endpoint, a
|
|
44
|
+
disabled bucket, a refused presign, or a failed upload all degrade to
|
|
45
|
+
submitting without the bundle rather than costing the user their bug report.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- `hq feedback` debug capture now also collects three more `~/.hq` sources, all
|
|
50
|
+
small and all high-signal for the failures they describe:
|
|
51
|
+
|
|
52
|
+
- **`locks/`** — held operation locks and background-work claims. This is the
|
|
53
|
+
one place where the FILENAME is the diagnosis: a `…stale-claim…` entry names
|
|
54
|
+
the operation, the process that abandoned it, and the fact that the claim
|
|
55
|
+
went stale, which is the answer to "sync/reindex is stuck". A hung process
|
|
56
|
+
writes nothing to a log, so this is otherwise invisible. Zero-byte lock
|
|
57
|
+
files are kept for their names rather than skipped as empty.
|
|
58
|
+
- **`jobs/`** — scheduled-job status, last reconcile, and probe attempts,
|
|
59
|
+
answering "my scheduled job never ran". Nested, so this is the collector's
|
|
60
|
+
only directory walk; it is bounded in depth (3) and file count (20), and
|
|
61
|
+
refuses to traverse a symlinked directory so it cannot escape `~/.hq`.
|
|
62
|
+
- Three more root status documents: `outpost-session-heartbeat.json` (Outpost
|
|
63
|
+
liveness), `version-check.json` (why an update is not offered), and
|
|
64
|
+
`plan-limit-nag.json`.
|
|
65
|
+
|
|
66
|
+
Deliberately still excluded: the two multi-megabyte telemetry cursors, the
|
|
67
|
+
`sync-state-v3/` internals, `backups/`, and everything outside `~/.hq` —
|
|
68
|
+
notably Claude Code session transcripts, which are conversation content and
|
|
69
|
+
do not belong on a bug report.
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
|
|
73
|
+
- Runtime bumped to `@indigoai-us/hq-cloud` ~6.16.11: personal-vault sync no
|
|
74
|
+
longer downloads `workspace/.session-logs/` from other machines (push-only),
|
|
75
|
+
and `hq reindex` now prunes local session-log copies 7 days after the vault
|
|
76
|
+
confirms them (`HQ_SESSION_LOG_LOCAL_RETENTION_DAYS`, `off` to disable).
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
## [5.108.5] — 2026-09-04
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- `hq meetings import <file>` imports normalized historical meeting transcripts
|
|
84
|
+
through HQ's managed source pipeline, with immutable idempotent replay and
|
|
85
|
+
explicit conflict detection when an external source ID is reused with
|
|
86
|
+
different content.
|
|
87
|
+
- `hq feedback bug|feature` now attaches bounded, redacted debug evidence from
|
|
88
|
+
the submitter's `~/.hq` directory to the submission's `diagnostics`, so a bug
|
|
89
|
+
report carries the context a triager would otherwise have to ask for. Two
|
|
90
|
+
classes are collected:
|
|
91
|
+
|
|
92
|
+
- **Log tails** — `*.log` and `*.jsonl` directly inside `~/.hq/logs/`,
|
|
93
|
+
including rotated generations (`hq-sync.log.1`, `.2`, `.3`), plus
|
|
94
|
+
`boot-sync.log` and `boot-capture.log` at the `~/.hq` root. This covers the
|
|
95
|
+
hq-cloud sync engine and the desktop sync app (which share
|
|
96
|
+
`logs/hq-sync.log`) and the CLI's MCP registry audit trail
|
|
97
|
+
(`logs/mcp-registry.log`). Rotated generations matter most right after a
|
|
98
|
+
rotation, when the active file is nearly empty and the history a triager
|
|
99
|
+
needs sits in `.1`. Any future log dropped into `~/.hq/logs/` is picked up
|
|
100
|
+
automatically.
|
|
101
|
+
- **State snapshots** — the small JSON status documents that say where sync
|
|
102
|
+
and client health stand: `sync-progress.json` (phase, file counts, conflict
|
|
103
|
+
count), `sync-version.json`, `cli-client-health.json` (failure streak),
|
|
104
|
+
`cli-client-health.observation.json`, and the per-company
|
|
105
|
+
`sync-journal.<slug>.json` locators. These are collected first, since they
|
|
106
|
+
answer questions a log tail cannot, and the log budget takes what remains.
|
|
107
|
+
|
|
108
|
+
Eligibility is an explicit allowlist in both classes. The rest of `~/.hq`
|
|
109
|
+
(`cognito-tokens.json`, `deploy-passwords.json`, `secrets-cache/`) is never
|
|
110
|
+
read, the enormous `sync-journal.*.json.last-good` snapshots are excluded by
|
|
111
|
+
suffix, and eligibility is decided with `lstat` so a symlink wearing an
|
|
112
|
+
eligible name cannot be used to reach a credential file.
|
|
113
|
+
|
|
114
|
+
Every tail is redacted before it is measured or sent (JWTs, AWS key ids,
|
|
115
|
+
GitHub and Slack tokens, `Bearer` values, SigV4 presign signatures, PEM
|
|
116
|
+
private-key blocks, and `key=value` pairs for secret-shaped names). The total
|
|
117
|
+
is budgeted against the headroom left under the server's 64 KiB request-body
|
|
118
|
+
cap and re-measured against the real serialized envelope after attaching, so
|
|
119
|
+
a large report can never be turned into a 413 by its own diagnostics.
|
|
120
|
+
|
|
121
|
+
Opt out per invocation with `--no-logs`, or globally with
|
|
122
|
+
`HQ_FEEDBACK_LOGS=0`.
|
|
123
|
+
|
|
124
|
+
### Fixed
|
|
125
|
+
|
|
126
|
+
- `hq` self-update no longer loops. After the CLI updates to `@latest` and a
|
|
127
|
+
re-run produces no effective version change, the version check records that
|
|
128
|
+
outcome and converges instead of re-triggering the updater, so 5.105.x (and
|
|
129
|
+
later) installs stop repeatedly self-updating (#494).
|
|
130
|
+
|
|
5
131
|
## [5.108.4] — 2026-09-04
|
|
6
132
|
|
|
7
133
|
### Fixed
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { type DiagnosticsBlob } from "../utils/feedback-diagnostics.js";
|
|
3
|
+
import { collectFeedbackLogs, feedbackLogsEnabled } from "../utils/feedback-logs.js";
|
|
4
|
+
import { type LogBundleRef } from "../utils/feedback-log-bundle.js";
|
|
2
5
|
export declare const BODY_MAX_BYTES: number;
|
|
6
|
+
/**
|
|
7
|
+
* Mirror of the server's `MAX_BODY_BYTES` in hq-pro
|
|
8
|
+
* `src/vault-service/handlers/feedback.ts`, which rejects the WHOLE request
|
|
9
|
+
* body over this size with a 413. Note this is the same number as
|
|
10
|
+
* `BODY_MAX_BYTES` above but a different measurement: that one bounds the
|
|
11
|
+
* markdown body alone, this one bounds the serialized JSON envelope. Debug
|
|
12
|
+
* logs are budgeted against whatever headroom is left between them.
|
|
13
|
+
*/
|
|
14
|
+
export declare const REQUEST_MAX_BYTES: number;
|
|
3
15
|
export interface FeedbackResult {
|
|
4
16
|
id: string;
|
|
5
17
|
}
|
|
@@ -11,8 +23,34 @@ export interface FeedbackSubmitOptions {
|
|
|
11
23
|
token: string;
|
|
12
24
|
/** S3 object keys of already-uploaded screenshots (see uploadScreenshots). */
|
|
13
25
|
screenshots?: string[];
|
|
26
|
+
/**
|
|
27
|
+
* Attach redacted tails of the submitter's `~/.hq` logs. Defaults to true;
|
|
28
|
+
* `false` (from `--no-logs`) skips collection entirely. Even when true, the
|
|
29
|
+
* user's `HQ_FEEDBACK_LOGS` env override still applies.
|
|
30
|
+
*/
|
|
31
|
+
includeLogs?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Reference to an already-uploaded log bundle (see uploadLogBundle). Carries
|
|
34
|
+
* the full log history that the inline `diagnostics.logs` blob cannot — the
|
|
35
|
+
* inline blob is bounded by the request cap and DynamoDB's item limit, so it
|
|
36
|
+
* stays a summary and this carries the rest.
|
|
37
|
+
*/
|
|
38
|
+
logBundle?: LogBundleRef;
|
|
14
39
|
}
|
|
15
40
|
export declare function readBodyFile(bodyFile: string, stdin?: NodeJS.ReadableStream): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Attach redacted `~/.hq` log tails to `diagnostics` if, and only if, they fit
|
|
43
|
+
* inside the server's request-body cap.
|
|
44
|
+
*
|
|
45
|
+
* Mutates `diagnostics` in place (it is already referenced by `requestBody`,
|
|
46
|
+
* so the measurement below sees the attached logs). Best-effort throughout: a
|
|
47
|
+
* collection failure leaves the submission untouched rather than failing a bug
|
|
48
|
+
* report over its own diagnostics.
|
|
49
|
+
*/
|
|
50
|
+
export declare function attachDebugLogs(requestBody: Record<string, unknown>, diagnostics: DiagnosticsBlob, enabled: boolean, deps?: {
|
|
51
|
+
collect?: typeof collectFeedbackLogs;
|
|
52
|
+
envEnabled?: typeof feedbackLogsEnabled;
|
|
53
|
+
}): void;
|
|
16
54
|
export declare function submitFeedback(opts: FeedbackSubmitOptions): Promise<FeedbackResult>;
|
|
17
55
|
export declare function registerFeedbackCommand(program: Command): void;
|
|
18
56
|
//# sourceMappingURL=feedback.d.ts.map
|
|
@@ -4,7 +4,24 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
4
4
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
5
5
|
import { collectDiagnostics } from "../utils/feedback-diagnostics.js";
|
|
6
6
|
import { MAX_SCREENSHOTS, uploadScreenshots } from "../utils/feedback-screenshots.js";
|
|
7
|
+
import { FEEDBACK_LOGS_MAX_TOTAL_BYTES, FEEDBACK_LOGS_MIN_USEFUL_BYTES, collectFeedbackLogs, feedbackLogsEnabled, } from "../utils/feedback-logs.js";
|
|
8
|
+
import { uploadLogBundle } from "../utils/feedback-log-bundle.js";
|
|
7
9
|
export const BODY_MAX_BYTES = 64 * 1024;
|
|
10
|
+
/**
|
|
11
|
+
* Mirror of the server's `MAX_BODY_BYTES` in hq-pro
|
|
12
|
+
* `src/vault-service/handlers/feedback.ts`, which rejects the WHOLE request
|
|
13
|
+
* body over this size with a 413. Note this is the same number as
|
|
14
|
+
* `BODY_MAX_BYTES` above but a different measurement: that one bounds the
|
|
15
|
+
* markdown body alone, this one bounds the serialized JSON envelope. Debug
|
|
16
|
+
* logs are budgeted against whatever headroom is left between them.
|
|
17
|
+
*/
|
|
18
|
+
export const REQUEST_MAX_BYTES = 64 * 1024;
|
|
19
|
+
/**
|
|
20
|
+
* Headroom held back for the `"logs":{...}` JSON envelope itself — its keys,
|
|
21
|
+
* braces, and the escaping of the tail text. The final size is re-measured
|
|
22
|
+
* after attaching, so this reserve only needs to be roughly right.
|
|
23
|
+
*/
|
|
24
|
+
const LOGS_ENVELOPE_RESERVE_BYTES = 1024;
|
|
8
25
|
export async function readBodyFile(bodyFile, stdin) {
|
|
9
26
|
if (bodyFile === "-") {
|
|
10
27
|
const stream = stdin ?? process.stdin;
|
|
@@ -23,6 +40,44 @@ export async function readBodyFile(bodyFile, stdin) {
|
|
|
23
40
|
}
|
|
24
41
|
return fs.promises.readFile(bodyFile, "utf-8");
|
|
25
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Attach redacted `~/.hq` log tails to `diagnostics` if, and only if, they fit
|
|
45
|
+
* inside the server's request-body cap.
|
|
46
|
+
*
|
|
47
|
+
* Mutates `diagnostics` in place (it is already referenced by `requestBody`,
|
|
48
|
+
* so the measurement below sees the attached logs). Best-effort throughout: a
|
|
49
|
+
* collection failure leaves the submission untouched rather than failing a bug
|
|
50
|
+
* report over its own diagnostics.
|
|
51
|
+
*/
|
|
52
|
+
export function attachDebugLogs(requestBody, diagnostics, enabled, deps = {}) {
|
|
53
|
+
const collect = deps.collect ?? collectFeedbackLogs;
|
|
54
|
+
const envEnabled = deps.envEnabled ?? feedbackLogsEnabled;
|
|
55
|
+
if (!enabled || !envEnabled())
|
|
56
|
+
return;
|
|
57
|
+
try {
|
|
58
|
+
const baseBytes = Buffer.byteLength(JSON.stringify(requestBody), "utf8");
|
|
59
|
+
const available = REQUEST_MAX_BYTES - baseBytes - LOGS_ENVELOPE_RESERVE_BYTES;
|
|
60
|
+
if (available < FEEDBACK_LOGS_MIN_USEFUL_BYTES)
|
|
61
|
+
return;
|
|
62
|
+
const logs = collect({
|
|
63
|
+
budgetBytes: Math.min(available, FEEDBACK_LOGS_MAX_TOTAL_BYTES),
|
|
64
|
+
});
|
|
65
|
+
if (!logs)
|
|
66
|
+
return;
|
|
67
|
+
diagnostics.logs = logs;
|
|
68
|
+
// Final authority: re-measure the ACTUAL envelope. JSON escaping of the
|
|
69
|
+
// tail text (quotes, newlines, control chars) can expand it well past the
|
|
70
|
+
// raw byte count the budget counted, so the estimate above is necessary
|
|
71
|
+
// but not sufficient. If we overshot, drop the logs rather than 413.
|
|
72
|
+
if (Buffer.byteLength(JSON.stringify(requestBody), "utf8") > REQUEST_MAX_BYTES) {
|
|
73
|
+
delete diagnostics.logs;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Never let diagnostics collection break the submission.
|
|
78
|
+
delete diagnostics.logs;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
26
81
|
export async function submitFeedback(opts) {
|
|
27
82
|
// Validate the title locally, symmetric with the body check below. Commander's
|
|
28
83
|
// `requiredOption("--title")` only requires the flag to be PRESENT — an empty
|
|
@@ -54,6 +109,18 @@ export async function submitFeedback(opts) {
|
|
|
54
109
|
if (opts.screenshots && opts.screenshots.length > 0) {
|
|
55
110
|
requestBody.screenshots = opts.screenshots;
|
|
56
111
|
}
|
|
112
|
+
// The bundle reference is four small fields, so unlike the inline logs below
|
|
113
|
+
// it needs no budgeting — but it is added BEFORE attachDebugLogs so those
|
|
114
|
+
// bytes are counted against the inline budget rather than overrunning it.
|
|
115
|
+
if (opts.logBundle) {
|
|
116
|
+
requestBody.logBundle = opts.logBundle;
|
|
117
|
+
}
|
|
118
|
+
// Debug logs are attached LAST and sized against the headroom the rest of
|
|
119
|
+
// the request left behind. The server 413s the whole body over 64 KiB, so a
|
|
120
|
+
// fixed log budget would turn a previously-working large-body submission
|
|
121
|
+
// into a hard failure. Measuring the real serialized envelope — the exact
|
|
122
|
+
// bytes vaultApiFetch will send — is what makes that impossible.
|
|
123
|
+
attachDebugLogs(requestBody, diagnostics, opts.includeLogs !== false);
|
|
57
124
|
const res = await vaultApiFetch({
|
|
58
125
|
token: opts.token,
|
|
59
126
|
path: "/v1/feedback",
|
|
@@ -81,6 +148,9 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
81
148
|
.requiredOption("--body-file <path>", "Path to a markdown file with the body; use - to read from stdin")
|
|
82
149
|
.option("--company <slug>", "Company slug to associate with the report")
|
|
83
150
|
.option("--screenshot <path>", `Attach a screenshot (repeatable, up to ${MAX_SCREENSHOTS}; .png/.jpg/.jpeg/.webp/.gif)`, (value, prev) => [...prev, value], [])
|
|
151
|
+
// Commander turns `--no-logs` into a boolean `logs` that defaults to true,
|
|
152
|
+
// so the default is "attach logs" and the flag is the opt-out.
|
|
153
|
+
.option("--no-logs", "Do not attach redacted tails of your ~/.hq log files (also: HQ_FEEDBACK_LOGS=0)")
|
|
84
154
|
.action(async (opts) => {
|
|
85
155
|
try {
|
|
86
156
|
const token = await ensureCognitoToken({ interactive: false });
|
|
@@ -91,6 +161,14 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
91
161
|
paths: opts.screenshot ?? [],
|
|
92
162
|
token,
|
|
93
163
|
});
|
|
164
|
+
// Full-history logs go direct to S3 before the submission that
|
|
165
|
+
// references them. Best-effort throughout: if the endpoint is absent
|
|
166
|
+
// (older server), the bucket is unconfigured, or the upload fails,
|
|
167
|
+
// this resolves undefined and the report still carries inline logs.
|
|
168
|
+
const logBundle = await uploadLogBundle({
|
|
169
|
+
token,
|
|
170
|
+
enabled: opts.logs !== false && feedbackLogsEnabled(),
|
|
171
|
+
});
|
|
94
172
|
const result = await submitFeedback({
|
|
95
173
|
type,
|
|
96
174
|
title: opts.title,
|
|
@@ -98,6 +176,8 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
98
176
|
company: opts.company,
|
|
99
177
|
token,
|
|
100
178
|
screenshots,
|
|
179
|
+
includeLogs: opts.logs !== false,
|
|
180
|
+
logBundle,
|
|
101
181
|
});
|
|
102
182
|
console.log(`Submitted: ${result.id}`);
|
|
103
183
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
3
|
-
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
4
|
+
import { vaultApiFetch, getCompanyUid, resolveCallerPersonUid, } from "../utils/vault-api.js";
|
|
4
5
|
function formatDuration(seconds) {
|
|
5
6
|
const h = Math.floor(seconds / 3600);
|
|
6
7
|
const m = Math.floor((seconds % 3600) / 60);
|
|
@@ -216,6 +217,59 @@ export function registerMeetingsCommand(program) {
|
|
|
216
217
|
}
|
|
217
218
|
});
|
|
218
219
|
// ── hq meetings invite <meeting-url> ──────────────────────────────
|
|
220
|
+
meetings
|
|
221
|
+
.command("import <file>")
|
|
222
|
+
.description("Import a normalized historical transcript JSON file")
|
|
223
|
+
.action(async (file) => {
|
|
224
|
+
try {
|
|
225
|
+
const companySlug = meetings.opts().company;
|
|
226
|
+
if (!companySlug) {
|
|
227
|
+
throw new Error("--company <slug> is required for historical imports");
|
|
228
|
+
}
|
|
229
|
+
const bytes = await readFile(file);
|
|
230
|
+
if (bytes.byteLength > 5 * 1024 * 1024) {
|
|
231
|
+
throw new Error("Historical meeting import exceeds 5 MiB");
|
|
232
|
+
}
|
|
233
|
+
let parsed;
|
|
234
|
+
try {
|
|
235
|
+
parsed = JSON.parse(bytes.toString("utf8"));
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
throw new Error("Historical meeting import file must contain valid JSON");
|
|
239
|
+
}
|
|
240
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
241
|
+
throw new Error("Historical meeting import file must contain a JSON object");
|
|
242
|
+
}
|
|
243
|
+
const token = await ensureCognitoToken();
|
|
244
|
+
const companyId = await getCompanyUid(token, companySlug);
|
|
245
|
+
const recorderPersonUid = await resolveCallerPersonUid(token);
|
|
246
|
+
const res = await vaultApiFetch({
|
|
247
|
+
token,
|
|
248
|
+
method: "POST",
|
|
249
|
+
path: "/v1/meetings/import",
|
|
250
|
+
body: {
|
|
251
|
+
...parsed,
|
|
252
|
+
companyId,
|
|
253
|
+
recorderPersonUid,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
if (!res.ok)
|
|
257
|
+
await handleApiError(res, meetings.opts().json);
|
|
258
|
+
const result = (await res.json());
|
|
259
|
+
if (meetings.opts().json) {
|
|
260
|
+
console.log(JSON.stringify(result, null, 2));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
console.log(chalk.green(`\n✓ Historical meeting ${result.outcome}: ${result.meetingId}`));
|
|
264
|
+
if (result.state)
|
|
265
|
+
console.log(chalk.dim(` State: ${result.state}`));
|
|
266
|
+
console.log();
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
219
273
|
meetings
|
|
220
274
|
.command("invite <meetingUrl>")
|
|
221
275
|
.description("Invite the meeting bot to a Google Meet, Zoom, or Teams URL")
|
package/dist/commands/signals.js
CHANGED
|
@@ -101,6 +101,12 @@ async function runList(options) {
|
|
|
101
101
|
const format = options.format ?? defaultListFormat();
|
|
102
102
|
const accessToken = await resolveAccessToken();
|
|
103
103
|
const vaultConfig = buildVaultConfig(accessToken);
|
|
104
|
+
// HQ-59: every company (cmp_) read below rides the presign VaultClient
|
|
105
|
+
// (`usePresign` keys on the vault client + cmp_ prefix), so tell entity
|
|
106
|
+
// resolution to skip the company `POST /sts/vend` — the vended creds were
|
|
107
|
+
// never consumed here, but each call re-entered the packed FILE_ACL fit
|
|
108
|
+
// ladder (policyTruncated noise). Personal vaults still vend-self.
|
|
109
|
+
vaultConfig.companyVaultUsesPresign = true;
|
|
104
110
|
const entity = await resolveEntity({ slug, vaultConfig });
|
|
105
111
|
const result = await listSignals({
|
|
106
112
|
entity,
|
|
@@ -134,6 +140,12 @@ async function runGet(options) {
|
|
|
134
140
|
const format = options.format ?? defaultGetFormat();
|
|
135
141
|
const accessToken = await resolveAccessToken();
|
|
136
142
|
const vaultConfig = buildVaultConfig(accessToken);
|
|
143
|
+
// HQ-59: every company (cmp_) read below rides the presign VaultClient
|
|
144
|
+
// (`usePresign` keys on the vault client + cmp_ prefix), so tell entity
|
|
145
|
+
// resolution to skip the company `POST /sts/vend` — the vended creds were
|
|
146
|
+
// never consumed here, but each call re-entered the packed FILE_ACL fit
|
|
147
|
+
// ladder (policyTruncated noise). Personal vaults still vend-self.
|
|
148
|
+
vaultConfig.companyVaultUsesPresign = true;
|
|
137
149
|
const entity = await resolveEntity({ slug, vaultConfig });
|
|
138
150
|
const doc = await getSignal({
|
|
139
151
|
entity,
|
package/dist/commands/sources.js
CHANGED
|
@@ -109,6 +109,12 @@ async function runList(options) {
|
|
|
109
109
|
const format = options.format ?? defaultListFormat();
|
|
110
110
|
const accessToken = await resolveAccessToken();
|
|
111
111
|
const vaultConfig = buildVaultConfig(accessToken);
|
|
112
|
+
// HQ-59: every company (cmp_) read below rides the presign VaultClient
|
|
113
|
+
// (`usePresign` keys on the vault client + cmp_ prefix), so tell entity
|
|
114
|
+
// resolution to skip the company `POST /sts/vend` — the vended creds were
|
|
115
|
+
// never consumed here, but each call re-entered the packed FILE_ACL fit
|
|
116
|
+
// ladder (policyTruncated noise). Personal vaults still vend-self.
|
|
117
|
+
vaultConfig.companyVaultUsesPresign = true;
|
|
112
118
|
const entity = await resolveEntity({ slug, vaultConfig });
|
|
113
119
|
const result = await listSources({
|
|
114
120
|
entity,
|
|
@@ -142,6 +148,12 @@ async function runGet(options) {
|
|
|
142
148
|
const format = options.format ?? defaultGetFormat();
|
|
143
149
|
const accessToken = await resolveAccessToken();
|
|
144
150
|
const vaultConfig = buildVaultConfig(accessToken);
|
|
151
|
+
// HQ-59: every company (cmp_) read below rides the presign VaultClient
|
|
152
|
+
// (`usePresign` keys on the vault client + cmp_ prefix), so tell entity
|
|
153
|
+
// resolution to skip the company `POST /sts/vend` — the vended creds were
|
|
154
|
+
// never consumed here, but each call re-entered the packed FILE_ACL fit
|
|
155
|
+
// ladder (policyTruncated noise). Personal vaults still vend-self.
|
|
156
|
+
vaultConfig.companyVaultUsesPresign = true;
|
|
145
157
|
const entity = await resolveEntity({ slug, vaultConfig });
|
|
146
158
|
const doc = await getSource({
|
|
147
159
|
entity,
|
|
@@ -345,7 +345,23 @@ export function buildCliHeartbeat(input) {
|
|
|
345
345
|
syncState = "error";
|
|
346
346
|
break;
|
|
347
347
|
default:
|
|
348
|
-
|
|
348
|
+
// An UNRESOLVED failure streak is a live error state, not idleness.
|
|
349
|
+
// Reporting `idle` here is what put "BROKEN — Runner failed" next to
|
|
350
|
+
// "Sync state: idle" on the support view: every later `hq` invocation
|
|
351
|
+
// overwrote the `error` state the failing sync had reported while
|
|
352
|
+
// leaving the streak that earned it untouched.
|
|
353
|
+
//
|
|
354
|
+
// Only this CLI's own `sync_success` clears the streak. The journal
|
|
355
|
+
// `lastSync` folded into `lastSyncSuccessAt` below deliberately does
|
|
356
|
+
// NOT: the engine stamps it per FILE update (hq-cloud journal.ts
|
|
357
|
+
// `updateEntry`), and a push stamps it before throwing its upload worker
|
|
358
|
+
// errors — so it means "some file moved", not "a run succeeded", and
|
|
359
|
+
// must never clear an alarm counter. That is why this branch keys off
|
|
360
|
+
// the streak rather than off the success timestamp.
|
|
361
|
+
if (input.state.consecutiveFailures > 0)
|
|
362
|
+
syncState = "error";
|
|
363
|
+
else
|
|
364
|
+
syncState = lastSyncSuccessAt ? "idle" : "never_synced";
|
|
349
365
|
break;
|
|
350
366
|
}
|
|
351
367
|
const heartbeat = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type VersionInfo } from "./feedback-versions.js";
|
|
2
|
+
import type { FeedbackLogsBlob } from "./feedback-logs.js";
|
|
2
3
|
export interface GitContext {
|
|
3
4
|
branch: string | null;
|
|
4
5
|
head: string | null;
|
|
@@ -23,6 +24,16 @@ export interface DiagnosticsBlob {
|
|
|
23
24
|
cwd: string;
|
|
24
25
|
git: GitContext;
|
|
25
26
|
recentSentryBreadcrumbs: unknown[];
|
|
27
|
+
/**
|
|
28
|
+
* Redacted tails of the submitter's `~/.hq` log files.
|
|
29
|
+
*
|
|
30
|
+
* Attached by `submitFeedback`, NOT by `collectDiagnostics`, because the
|
|
31
|
+
* size budget can only be computed once the rest of the request body is
|
|
32
|
+
* known (the server caps the whole body at 64 KiB). Absent when the user
|
|
33
|
+
* opted out (`--no-logs` / `HQ_FEEDBACK_LOGS=0`), when no eligible log file
|
|
34
|
+
* exists, or when the body left no headroom.
|
|
35
|
+
*/
|
|
36
|
+
logs?: FeedbackLogsBlob;
|
|
26
37
|
}
|
|
27
38
|
export declare function sanitizeArgv(argv: string[]): string[];
|
|
28
39
|
export declare function collectDiagnostics(): DiagnosticsBlob;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-history log bundle for `hq feedback` — the out-of-band companion to the
|
|
3
|
+
* inline `diagnostics.logs` blob in `feedback-logs.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Inline logs are capped twice over: the feedback endpoint rejects a request
|
|
6
|
+
* body above 64 KiB, and `diagnostics` is written into a DynamoDB item, which
|
|
7
|
+
* caps at 400 KB. Neither can be tuned into carrying a real log history, so the
|
|
8
|
+
* inline blob is deliberately a ~40 KiB summary of 16 KiB tails. This module
|
|
9
|
+
* produces the other half: a gzipped bundle uploaded direct to S3, carrying the
|
|
10
|
+
* files whole rather than in tail-sized slivers.
|
|
11
|
+
*
|
|
12
|
+
* Three properties are load-bearing and none may be traded away:
|
|
13
|
+
*
|
|
14
|
+
* 1. SAME ALLOWLIST. Discovery reuses `feedback-logs.ts` verbatim —
|
|
15
|
+
* `discoverLogFiles`, `discoverStateFiles`, `discoverFlatDirFiles`,
|
|
16
|
+
* `walkJsonFiles`. The bundle carries MORE OF the same files, never more
|
|
17
|
+
* files. Credential material lives outside `~/.hq` (`~/.codex/auth.json`,
|
|
18
|
+
* `~/.hq-agent/machine-creds.json`), so widening discovery here — not
|
|
19
|
+
* raising the size — is what would turn this into an exfiltration bug.
|
|
20
|
+
*
|
|
21
|
+
* 2. SAME REDACTION. Every line goes through `redactLogText` before it is
|
|
22
|
+
* compressed. Shipping a raw archive would be far simpler and would
|
|
23
|
+
* silently undo the entire security model of the inline path.
|
|
24
|
+
*
|
|
25
|
+
* 3. BOUNDED MEMORY. 50 MB compressed is roughly a gigabyte of raw log text
|
|
26
|
+
* at the ratio these files compress at. Nothing is ever fully materialised:
|
|
27
|
+
* files are read in slices, redacted a line at a time, and streamed into
|
|
28
|
+
* gzip, with only the compressed output retained.
|
|
29
|
+
*
|
|
30
|
+
* Output format — gzipped NDJSON, one JSON record per line:
|
|
31
|
+
* {"kind":"manifest","version":1,...} exactly one, first
|
|
32
|
+
* {"kind":"file","name":"logs/hq-sync.log",...} one per file
|
|
33
|
+
* {"kind":"chunk","name":"logs/hq-sync.log","seq":0,...} many per file
|
|
34
|
+
* {"kind":"summary","fileCount":12,...} exactly one, last
|
|
35
|
+
*
|
|
36
|
+
* NDJSON rather than tar so there is no archive dependency, so a truncated
|
|
37
|
+
* bundle is still parseable line-by-line up to the cut, and so the records
|
|
38
|
+
* carry the same redaction metadata the inline blob already reports.
|
|
39
|
+
*/
|
|
40
|
+
import { type LogCandidate } from "./feedback-logs.js";
|
|
41
|
+
import { vaultApiFetch } from "./vault-api.js";
|
|
42
|
+
/**
|
|
43
|
+
* Compressed ceiling. Mirrors `MAX_LOG_BUNDLE_BYTES` in the hq-pro handler
|
|
44
|
+
* `feedback-log-bundles.ts`, which refuses to presign above it — the two must
|
|
45
|
+
* stay in step or the CLI will build bundles the server will not accept.
|
|
46
|
+
*/
|
|
47
|
+
export declare const LOG_BUNDLE_MAX_BYTES: number;
|
|
48
|
+
/**
|
|
49
|
+
* Headroom between the size we stop feeding at and the hard cap.
|
|
50
|
+
*
|
|
51
|
+
* gzip reports compressed bytes only as its internal buffer flushes, so the
|
|
52
|
+
* running total lags the bytes actually consumed. The lag is bounded by that
|
|
53
|
+
* buffer (tens of KiB); a 1 MiB margin covers it with three orders of magnitude
|
|
54
|
+
* to spare, and the final size is asserted against the real cap regardless.
|
|
55
|
+
*/
|
|
56
|
+
export declare const LOG_BUNDLE_SAFETY_MARGIN_BYTES: number;
|
|
57
|
+
/**
|
|
58
|
+
* Per-file raw ceiling. The desktop logger rotates at 32 MiB (hq-desktop-core
|
|
59
|
+
* `logfile.rs`), so this admits a full generation with headroom while stopping
|
|
60
|
+
* one pathological file from consuming the whole bundle. Files above it are
|
|
61
|
+
* read from the TAIL — the end of a log is what explains a failure.
|
|
62
|
+
*/
|
|
63
|
+
export declare const LOG_BUNDLE_MAX_FILE_RAW_BYTES: number;
|
|
64
|
+
export interface LogBundleResult {
|
|
65
|
+
/** The gzipped NDJSON bytes, ready to PUT. */
|
|
66
|
+
gzip: Buffer;
|
|
67
|
+
/** `gzip.byteLength` — what the presign request must declare. */
|
|
68
|
+
sizeBytes: number;
|
|
69
|
+
/** How many files contributed at least one chunk. */
|
|
70
|
+
fileCount: number;
|
|
71
|
+
/** True when the size cap stopped collection before every file was read. */
|
|
72
|
+
truncated: boolean;
|
|
73
|
+
/** Raw (pre-compression, post-redaction) bytes read into the bundle. */
|
|
74
|
+
rawBytes: number;
|
|
75
|
+
/** Total redacted spans across every file. Non-zero is expected and fine. */
|
|
76
|
+
redactions: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* What a submission records about its uploaded bundle. Must stay in step with
|
|
80
|
+
* `LogBundleRef` in the hq-pro handler `feedback-log-bundles.ts`, which
|
|
81
|
+
* re-validates every field before persisting it.
|
|
82
|
+
*/
|
|
83
|
+
export interface LogBundleRef {
|
|
84
|
+
key: string;
|
|
85
|
+
sizeBytes: number;
|
|
86
|
+
fileCount: number;
|
|
87
|
+
truncated: boolean;
|
|
88
|
+
}
|
|
89
|
+
export interface BuildLogBundleOptions {
|
|
90
|
+
/** Compressed ceiling. Clamped to {@link LOG_BUNDLE_MAX_BYTES}. */
|
|
91
|
+
maxBytes?: number;
|
|
92
|
+
/** Override `~/.hq` (tests). */
|
|
93
|
+
hqDir?: string;
|
|
94
|
+
/** Override the home directory used to derive `~/.hq` (tests). */
|
|
95
|
+
homeDir?: string;
|
|
96
|
+
/** Override the per-file raw ceiling (tests). */
|
|
97
|
+
maxFileRawBytes?: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Order candidates so that, when the cap truncates collection, what survives is
|
|
101
|
+
* what a triager reads first.
|
|
102
|
+
*
|
|
103
|
+
* State documents lead: they are tiny and answer questions a log cannot (which
|
|
104
|
+
* operation is claimed, what the sync cursor is). Log files follow, newest
|
|
105
|
+
* first. Reversing this would let one large old log crowd out every status
|
|
106
|
+
* file — the same class of ordering defect that let ordinary lock entries
|
|
107
|
+
* crowd out a stale claim in the inline collector.
|
|
108
|
+
*/
|
|
109
|
+
export declare function orderBundleCandidates(hqDir: string): LogCandidate[];
|
|
110
|
+
/**
|
|
111
|
+
* Read `absPath` in slices, redacting whole lines, invoking `onChunk` with
|
|
112
|
+
* roughly {@link CHUNK_TEXT_BYTES} of redacted text at a time.
|
|
113
|
+
*
|
|
114
|
+
* Reads from the tail when the file exceeds `maxRawBytes`, and drops the first
|
|
115
|
+
* partial line after seeking so a chunk never begins mid-record. `onChunk`
|
|
116
|
+
* returns false to stop early (the cap was hit).
|
|
117
|
+
*/
|
|
118
|
+
export declare function streamRedactedFile(absPath: string, sizeBytes: number, maxRawBytes: number, chunkBytes: number, onChunk: (text: string, redactions: number) => Promise<boolean>): Promise<{
|
|
119
|
+
fromTail: boolean;
|
|
120
|
+
stopped: boolean;
|
|
121
|
+
}>;
|
|
122
|
+
/**
|
|
123
|
+
* Build a gzipped NDJSON bundle of the submitter's redacted `~/.hq` logs.
|
|
124
|
+
*
|
|
125
|
+
* Returns `undefined` when nothing eligible exists, so the caller can skip the
|
|
126
|
+
* upload entirely. Never throws: a bug report must not fail over its own
|
|
127
|
+
* diagnostics, so any unexpected error yields `undefined` and the submission
|
|
128
|
+
* proceeds with inline logs alone.
|
|
129
|
+
*/
|
|
130
|
+
export declare function buildLogBundle(opts?: BuildLogBundleOptions): Promise<LogBundleResult | undefined>;
|
|
131
|
+
/**
|
|
132
|
+
* Build, presign, and upload a log bundle; return the reference the submission
|
|
133
|
+
* should carry, or `undefined` if anything at all did not work out.
|
|
134
|
+
*
|
|
135
|
+
* Every failure path is silent and non-fatal by design. The bundle is an
|
|
136
|
+
* enrichment on top of the inline logs that already ship in the request body,
|
|
137
|
+
* so a missing endpoint, a disabled bucket, a refused presign, or a failed PUT
|
|
138
|
+
* must all degrade to "submit without it" rather than cost the user their bug
|
|
139
|
+
* report. In particular a 404 is expected and unremarkable while a CLI that
|
|
140
|
+
* knows about bundles is running against a server that does not yet.
|
|
141
|
+
*/
|
|
142
|
+
export declare function uploadLogBundle(opts: {
|
|
143
|
+
token: string;
|
|
144
|
+
enabled: boolean;
|
|
145
|
+
fetchImpl?: typeof fetch;
|
|
146
|
+
build?: typeof buildLogBundle;
|
|
147
|
+
apiFetch?: typeof vaultApiFetch;
|
|
148
|
+
}): Promise<LogBundleRef | undefined>;
|
|
149
|
+
//# sourceMappingURL=feedback-log-bundle.d.ts.map
|