@indigoai-us/hq-cli 5.182.0 → 5.184.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/command-catalog.generated.d.ts +3 -0
  3. package/dist/command-catalog.generated.js +4 -0
  4. package/dist/commands/doctor.d.ts +2 -0
  5. package/dist/commands/doctor.js +100 -6
  6. package/dist/commands/mesh.js +18 -1
  7. package/dist/lib/core-utils/hook-timeout-debug-context-flag.d.ts +20 -0
  8. package/dist/lib/core-utils/hook-timeout-debug-context-flag.js +80 -0
  9. package/dist/lib/core-utils/sentry-report.d.ts +24 -2
  10. package/dist/lib/core-utils/sentry-report.js +254 -16
  11. package/dist/lib/doctor/checks/sync-health.js +13 -0
  12. package/dist/lib/doctor/client-health-reason-codes-flag.d.ts +26 -0
  13. package/dist/lib/doctor/client-health-reason-codes-flag.js +94 -0
  14. package/dist/lib/doctor/fix/apply.d.ts +5 -0
  15. package/dist/lib/doctor/fix/apply.js +28 -3
  16. package/dist/lib/doctor/fix/json-output.d.ts +20 -0
  17. package/dist/lib/doctor/fix/json-output.js +22 -0
  18. package/dist/lib/doctor/fix/refresh-sync-outcome.d.ts +12 -0
  19. package/dist/lib/doctor/fix/refresh-sync-outcome.js +25 -0
  20. package/dist/lib/doctor/json-output.d.ts +7 -1
  21. package/dist/lib/doctor/json-output.js +6 -2
  22. package/dist/lib/doctor/types.d.ts +5 -0
  23. package/dist/lib/doctor/types.js +15 -0
  24. package/dist/lib/mesh/live/daemon/index.d.ts +1 -1
  25. package/dist/lib/mesh/live/daemon/index.js +1 -1
  26. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +7 -0
  27. package/dist/lib/mesh/live/daemon/transcript-watch.js +25 -3
  28. package/dist/lib/mesh/live/emit-client.d.ts +6 -0
  29. package/dist/lib/mesh/live/emit-client.js +8 -0
  30. package/dist/lib/mesh/live/enqueue.d.ts +5 -0
  31. package/dist/lib/mesh/live/enqueue.js +19 -0
  32. package/dist/lib/mesh/live/flush.js +5 -3
  33. package/dist/lib/mesh/live/format-spool-line.d.ts +5 -2
  34. package/dist/lib/mesh/live/format-spool-line.js +10 -0
  35. package/dist/lib/mesh/live/report-summary.d.ts +45 -0
  36. package/dist/lib/mesh/live/report-summary.js +222 -0
  37. package/dist/lib/mesh/live/session-event.schema.json +24 -1
  38. package/dist/lib/mesh/live/validate-session-event.d.ts +1 -1
  39. package/dist/lib/mesh/live/validate-session-event.js +1 -1
  40. package/package.json +1 -1
@@ -0,0 +1,222 @@
1
+ /**
2
+ * US-051 — one-sentence quiet-report fields for session_end.
3
+ *
4
+ * Built locally at the quiet trigger / SessionEnd flush. Never includes file
5
+ * contents. Tokens, secret-looking strings, and secrets-file paths are redacted.
6
+ */
7
+ import { spawnSync } from "node:child_process";
8
+ import * as fs from "node:fs";
9
+ import * as os from "node:os";
10
+ import * as path from "node:path";
11
+ export const REPORT_SUMMARY_MAX_CHARS = 140;
12
+ export const REPORT_SUMMARY_SCHEMA_MAX = 280;
13
+ export const REPORT_TRANSCRIPT_TAIL_BYTES = 256 * 1024;
14
+ const SECRET_RE = /(?:Bearer\s+[A-Za-z0-9._\-+=/]+)|(?:sk-[A-Za-z0-9]{8,})|(?:ghp_[A-Za-z0-9]{8,})|(?:github_pat_[A-Za-z0-9_]{8,})|(?:xox[baprs]-[A-Za-z0-9-]{8,})|(?:AKIA[0-9A-Z]{8,})|(?:op:\/\/[^\s"]+)/gi;
15
+ const SECRETS_PATH_RE = /(?:^|[\s"'`])((?:~|\/|\.\/|\.\.\/)?(?:[\w.-]+\/)*(?:\.env(?:\.[A-Za-z0-9._-]+)?|secrets(?:\/[\w.-]+)*|id_rsa|credentials(?:\.json)?|\.netrc|\.npmrc))/gi;
16
+ const PR_URL_RE = /https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/\d+/g;
17
+ export function redactReportSummary(text) {
18
+ let out = text.replace(SECRET_RE, "[redacted]");
19
+ out = out.replace(SECRETS_PATH_RE, (full, match) => full.replace(match, "[redacted-path]"));
20
+ // eslint-disable-next-line no-control-regex -- strip C0 controls from report text
21
+ return out.replace(/[\u0000-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim();
22
+ }
23
+ export function firstSentence(text, max = REPORT_SUMMARY_MAX_CHARS) {
24
+ const cleaned = redactReportSummary(text);
25
+ if (!cleaned)
26
+ return "";
27
+ const cut = cleaned.split(/(?<=[.!?])\s+/)[0] ?? cleaned;
28
+ const sentence = cut.trim();
29
+ if (sentence.length <= max)
30
+ return sentence;
31
+ const slice = sentence.slice(0, max);
32
+ const lastSpace = slice.lastIndexOf(" ");
33
+ return (lastSpace > 40 ? slice.slice(0, lastSpace) : slice).trim();
34
+ }
35
+ function textFromUnknown(value) {
36
+ if (typeof value === "string")
37
+ return value;
38
+ if (Array.isArray(value)) {
39
+ return value
40
+ .map((item) => {
41
+ if (typeof item === "string")
42
+ return item;
43
+ if (item && typeof item === "object") {
44
+ const rec = item;
45
+ if (rec.type === "text" && typeof rec.text === "string")
46
+ return rec.text;
47
+ }
48
+ return "";
49
+ })
50
+ .filter(Boolean)
51
+ .join("\n");
52
+ }
53
+ if (value && typeof value === "object") {
54
+ const rec = value;
55
+ if (typeof rec.text === "string")
56
+ return rec.text;
57
+ if (rec.content !== undefined)
58
+ return textFromUnknown(rec.content);
59
+ }
60
+ return "";
61
+ }
62
+ export function extractLastAssistantText(jsonl) {
63
+ let last;
64
+ for (const rawLine of jsonl.split(/\r?\n/)) {
65
+ const line = rawLine.trim();
66
+ if (!line.startsWith("{"))
67
+ continue;
68
+ let obj;
69
+ try {
70
+ obj = JSON.parse(line);
71
+ }
72
+ catch {
73
+ continue;
74
+ }
75
+ const type = typeof obj.type === "string" ? obj.type : "";
76
+ const role = typeof obj.role === "string" ? obj.role : "";
77
+ const payload = obj.payload;
78
+ let text = "";
79
+ if (type === "assistant" || role === "assistant") {
80
+ text = textFromUnknown(obj.message ?? obj.content);
81
+ }
82
+ else if (payload &&
83
+ (payload.type === "agent_message" || payload.type === "assistant")) {
84
+ text = textFromUnknown(payload.message ?? payload.content ?? payload.text);
85
+ }
86
+ const trimmed = text.trim();
87
+ if (trimmed)
88
+ last = trimmed;
89
+ }
90
+ return last;
91
+ }
92
+ export function readTranscriptTail(transcriptPath, maxBytes = REPORT_TRANSCRIPT_TAIL_BYTES) {
93
+ try {
94
+ const st = fs.statSync(transcriptPath);
95
+ if (!st.isFile() || st.size <= 0)
96
+ return undefined;
97
+ const size = Math.min(st.size, maxBytes);
98
+ const fd = fs.openSync(transcriptPath, "r");
99
+ try {
100
+ const buf = Buffer.alloc(size);
101
+ fs.readSync(fd, buf, 0, size, Math.max(0, st.size - size));
102
+ let text = buf.toString("utf8");
103
+ if (st.size > maxBytes) {
104
+ const nl = text.indexOf("\n");
105
+ if (nl >= 0)
106
+ text = text.slice(nl + 1);
107
+ }
108
+ return text;
109
+ }
110
+ finally {
111
+ fs.closeSync(fd);
112
+ }
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ }
118
+ function defaultGit(cwd, args) {
119
+ try {
120
+ const r = spawnSync("git", ["-C", cwd, ...args], {
121
+ timeout: 800,
122
+ encoding: "utf8",
123
+ stdio: ["ignore", "pipe", "ignore"],
124
+ });
125
+ if (r.status !== 0)
126
+ return undefined;
127
+ const out = (r.stdout ?? "").trim();
128
+ return out || undefined;
129
+ }
130
+ catch {
131
+ return undefined;
132
+ }
133
+ }
134
+ export function collectGitContext(cwd, git = defaultGit) {
135
+ const branch = git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
136
+ const top = git(cwd, ["rev-parse", "--show-toplevel"]);
137
+ const repo = top ? path.basename(top) : undefined;
138
+ const out = {};
139
+ if (repo && repo.length <= 128 && !repo.includes("\0"))
140
+ out.repo = repo;
141
+ if (branch && branch !== "HEAD" && branch.length <= 128)
142
+ out.gitBranch = branch;
143
+ return out;
144
+ }
145
+ export function extractPrUrls(text, max = 5) {
146
+ const seen = new Set();
147
+ const urls = [];
148
+ for (const match of text.match(PR_URL_RE) ?? []) {
149
+ if (seen.has(match))
150
+ continue;
151
+ seen.add(match);
152
+ urls.push(match.slice(0, 256));
153
+ if (urls.length >= max)
154
+ break;
155
+ }
156
+ return urls;
157
+ }
158
+ export function claudeTranscriptPathFor(sessionId, cwd, home = os.homedir()) {
159
+ const encoded = cwd.startsWith("/") ? cwd.replace(/\//g, "-") : `-${cwd.replace(/\//g, "-")}`;
160
+ return path.join(home, ".claude", "projects", encoded, `${sessionId}.jsonl`);
161
+ }
162
+ export function resolveTranscriptPath(input) {
163
+ const explicit = input.transcriptPath?.trim();
164
+ if (explicit)
165
+ return explicit;
166
+ const envPath = process.env.CLAUDE_TRANSCRIPT_PATH?.trim();
167
+ if (envPath)
168
+ return envPath;
169
+ const sid = input.sessionId?.trim();
170
+ const cwd = input.cwd?.trim();
171
+ if (sid && cwd)
172
+ return claudeTranscriptPathFor(sid, cwd, input.home);
173
+ return undefined;
174
+ }
175
+ export function buildSessionEndReportFields(input) {
176
+ const fields = {};
177
+ const git = input.git ?? defaultGit;
178
+ const cwd = input.cwd?.trim();
179
+ if (cwd)
180
+ Object.assign(fields, collectGitContext(cwd, git));
181
+ const transcriptPath = resolveTranscriptPath(input);
182
+ const read = input.readTranscript ?? ((p) => readTranscriptTail(p));
183
+ let assistant;
184
+ if (transcriptPath) {
185
+ const tail = read(transcriptPath);
186
+ if (tail)
187
+ assistant = extractLastAssistantText(tail);
188
+ }
189
+ if (assistant) {
190
+ const summary = firstSentence(assistant);
191
+ if (summary)
192
+ fields.summary = summary.slice(0, REPORT_SUMMARY_SCHEMA_MAX);
193
+ const prs = extractPrUrls(assistant);
194
+ if (prs.length > 0)
195
+ fields.prUrls = prs;
196
+ }
197
+ return fields;
198
+ }
199
+ /** Merge report fields onto a session_end event object (no-op for other kinds). */
200
+ export function enrichSessionEndEvent(event, extra) {
201
+ if (event.kind !== "session_end")
202
+ return event;
203
+ const built = buildSessionEndReportFields({
204
+ sessionId: typeof event.sessionId === "string" ? event.sessionId : undefined,
205
+ cwd: typeof event.cwd === "string" ? event.cwd : undefined,
206
+ transcriptPath: extra?.transcriptPath,
207
+ home: extra?.home,
208
+ git: extra?.git,
209
+ readTranscript: extra?.readTranscript,
210
+ });
211
+ const out = { ...event };
212
+ if (!out.summary && built.summary)
213
+ out.summary = built.summary;
214
+ if (!out.repo && built.repo)
215
+ out.repo = built.repo;
216
+ if (!out.gitBranch && built.gitBranch)
217
+ out.gitBranch = built.gitBranch;
218
+ if (!out.prUrls && built.prUrls)
219
+ out.prUrls = built.prUrls;
220
+ return out;
221
+ }
222
+ //# sourceMappingURL=report-summary.js.map
@@ -109,7 +109,30 @@
109
109
  "type": "string",
110
110
  "minLength": 1,
111
111
  "maxLength": 280,
112
- "description": "Optional short summary allowed only on note, blocked, and task_status. Never prompt-derived content."
112
+ "description": "Optional short summary on note, blocked, task_status, and session_end (US-051 quiet report). Redacted; never raw prompts, tokens, or file contents."
113
+ },
114
+ "repo": {
115
+ "type": "string",
116
+ "minLength": 1,
117
+ "maxLength": 128,
118
+ "description": "Optional git repository basename on session_end for the quiet work report (US-051)."
119
+ },
120
+ "gitBranch": {
121
+ "type": "string",
122
+ "minLength": 1,
123
+ "maxLength": 128,
124
+ "description": "Optional git branch of the session cwd on session_end (US-051)."
125
+ },
126
+ "prUrls": {
127
+ "type": "array",
128
+ "maxItems": 5,
129
+ "items": {
130
+ "type": "string",
131
+ "minLength": 1,
132
+ "maxLength": 256,
133
+ "pattern": "^https://github\\.com/"
134
+ },
135
+ "description": "Optional pull-request URLs seen in the quiet window (US-051)."
113
136
  },
114
137
  "cwd": {
115
138
  "type": "string",
@@ -8,7 +8,7 @@
8
8
  import type { ValidateFunction } from "ajv";
9
9
  export declare const SESSION_EVENT_SCHEMA_PATH: string;
10
10
  /** Expected SHA-256 of the canonical schema bytes (hq-pro + hq-cli must match). */
11
- export declare const SESSION_EVENT_SCHEMA_SHA256 = "4284bcc3718b8c9008c26156c1f79ae5cfd70fdd248beb2b1c776988f109e415";
11
+ export declare const SESSION_EVENT_SCHEMA_SHA256 = "e3a9b05daf0037688809e8825a286f9dc89d69aa9266696270d776b64bf99dda";
12
12
  export declare const PROHIBITED_CONTENT_CLASSES: readonly ["prompts", "model_output", "transcripts", "message_bodies", "tokens", "credentials"];
13
13
  export type ProhibitedContentClass = (typeof PROHIBITED_CONTENT_CLASSES)[number];
14
14
  /** Fixture field name → prohibited class. */
@@ -13,7 +13,7 @@ import Ajv2020 from "ajv/dist/2020.js";
13
13
  const HERE = dirname(fileURLToPath(import.meta.url));
14
14
  export const SESSION_EVENT_SCHEMA_PATH = join(HERE, "session-event.schema.json");
15
15
  /** Expected SHA-256 of the canonical schema bytes (hq-pro + hq-cli must match). */
16
- export const SESSION_EVENT_SCHEMA_SHA256 = "4284bcc3718b8c9008c26156c1f79ae5cfd70fdd248beb2b1c776988f109e415";
16
+ export const SESSION_EVENT_SCHEMA_SHA256 = "e3a9b05daf0037688809e8825a286f9dc89d69aa9266696270d776b64bf99dda";
17
17
  export const PROHIBITED_CONTENT_CLASSES = [
18
18
  "prompts",
19
19
  "model_output",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.182.0",
3
+ "version": "5.184.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {