@indigoai-us/hq-cli 5.94.0 → 5.94.2

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.
@@ -13,12 +13,23 @@ import { homedir } from "node:os";
13
13
  import { resolveLiveRoot } from "../utils/hq-roots.js";
14
14
  import { peekIdToken } from "../utils/id-token.js";
15
15
  const DEFAULT_TRIGGER = "stop-gate";
16
- const BACKENDS = new Set(["auto", "claude", "codex", "none"]);
16
+ const BACKENDS = new Set(["auto", "claude", "codex", "grok", "none"]);
17
17
  // Pinned by operator directive 2026-07-31; change defaults here deliberately.
18
18
  const CODEX_SIBLING_MODEL = "gpt-5.6-terra";
19
19
  const CODEX_SIBLING_REASONING_EFFORT = "high";
20
20
  const CLAUDE_SIBLING_MODEL = "claude-opus-5";
21
21
  const CLAUDE_SIBLING_EFFORT = "medium";
22
+ const GROK_SIBLING_MODEL = "grok-4.5";
23
+ const GROK_SIBLING_EFFORT = "high";
24
+ /**
25
+ * `grok -p` is single-turn by default: without a turn budget it answers with a
26
+ * plan and exits, having done nothing. The maintenance flow needs many turns.
27
+ */
28
+ const GROK_SIBLING_MAX_TURNS = "100";
29
+ /** Preference order for `--backend auto`, first healthy candidate wins. */
30
+ const AUTO_BACKEND_ORDER = ["codex", "grok", "claude"];
31
+ /** A backend that cannot answer `--version` this fast is treated as broken. */
32
+ const BACKEND_PROBE_TIMEOUT_MS = 10_000;
22
33
  /** A sibling still holding the lock after this long is treated as abandoned. */
23
34
  const SIBLING_LOCK_TTL_MS = 60 * 60 * 1000;
24
35
  /** Newest-wins cap so a wedged sibling cannot grow an unbounded queue. */
@@ -402,21 +413,150 @@ function backendOnPath(name) {
402
413
  }
403
414
  });
404
415
  }
405
- function resolveBackend(requested) {
416
+ /**
417
+ * An installed binary is not necessarily a working one: a partial install, a
418
+ * broken wrapper or a missing runtime all leave an executable on PATH that
419
+ * fails the moment it is asked to do anything. Ask it for its version — cheap,
420
+ * bounded, and enough to reject a backend before handing it a checkpoint.
421
+ */
422
+ function backendResponds(name) {
423
+ try {
424
+ execFileSync(name, ["--version"], {
425
+ timeout: BACKEND_PROBE_TIMEOUT_MS,
426
+ stdio: "ignore",
427
+ });
428
+ return true;
429
+ }
430
+ catch {
431
+ return false;
432
+ }
433
+ }
434
+ function processAlive(pid) {
435
+ try {
436
+ process.kill(pid, 0);
437
+ return true;
438
+ }
439
+ catch {
440
+ return false;
441
+ }
442
+ }
443
+ /**
444
+ * Backends whose most recent run died without producing a report.
445
+ *
446
+ * A version probe cannot catch a backend that starts cleanly and then fails at
447
+ * runtime — an expired login, a sandbox it cannot write in, a model it is not
448
+ * entitled to. The evidence for that is a finished run directory with no
449
+ * report.md in it, so prefer a different backend for the next checkpoint. Only
450
+ * the newest run per backend counts: one that has since succeeded is healthy.
451
+ */
452
+ function crashedBackends(siblingRoot) {
453
+ const crashed = new Set();
454
+ let runDirs;
455
+ try {
456
+ runDirs = fs
457
+ .readdirSync(siblingRoot)
458
+ .filter((entry) => entry !== "pending.jsonl")
459
+ .sort()
460
+ .reverse();
461
+ }
462
+ catch {
463
+ return crashed;
464
+ }
465
+ const seen = new Set();
466
+ for (const runDir of runDirs) {
467
+ const absolute = path.join(siblingRoot, runDir);
468
+ let backend;
469
+ try {
470
+ const payload = JSON.parse(fs.readFileSync(path.join(absolute, "payload.json"), "utf8"));
471
+ backend = payload.backend;
472
+ }
473
+ catch {
474
+ continue;
475
+ }
476
+ if (typeof backend !== "string" || !backend || seen.has(backend))
477
+ continue;
478
+ seen.add(backend);
479
+ if (fs.existsSync(path.join(absolute, "report.md")))
480
+ continue;
481
+ // A missing report only means a crash once the process itself is gone.
482
+ let pid = Number.NaN;
483
+ try {
484
+ pid = Number.parseInt(fs.readFileSync(path.join(absolute, "sibling.pid"), "utf8").trim(), 10);
485
+ }
486
+ catch {
487
+ // A run that never recorded a PID cannot still be running.
488
+ }
489
+ if (Number.isSafeInteger(pid) && pid > 0 && processAlive(pid))
490
+ continue;
491
+ crashed.add(backend);
492
+ }
493
+ return crashed;
494
+ }
495
+ function resolveBackend(requested, liveRoot) {
406
496
  const value = requested ?? "auto";
407
497
  if (!BACKENDS.has(value))
408
498
  usage(`checkpoint: unknown backend: ${value}`);
409
- if (value === "auto") {
410
- if (backendOnPath("codex"))
411
- return "codex";
412
- if (backendOnPath("claude"))
413
- return "claude";
499
+ // An explicitly named backend is honoured as given; only `auto` shops around.
500
+ if (value !== "auto")
501
+ return value;
502
+ const available = AUTO_BACKEND_ORDER.filter((name) => backendOnPath(name) && backendResponds(name));
503
+ if (available.length === 0)
414
504
  return "none";
505
+ const crashed = crashedBackends(path.join(liveRoot, "workspace", "checkpoints", "sibling"));
506
+ // If every candidate failed last time, still try the preferred one: a stale
507
+ // failure is a weaker signal than running no maintenance sibling at all.
508
+ return available.find((name) => !crashed.has(name)) ?? available[0];
509
+ }
510
+ function siblingArgs(backend, prompt) {
511
+ switch (backend) {
512
+ case "claude":
513
+ return [
514
+ "-p",
515
+ prompt,
516
+ "--model",
517
+ CLAUDE_SIBLING_MODEL,
518
+ "--effort",
519
+ CLAUDE_SIBLING_EFFORT,
520
+ "--permission-mode",
521
+ "acceptEdits",
522
+ ];
523
+ case "grok":
524
+ return [
525
+ "-p",
526
+ prompt,
527
+ "-m",
528
+ GROK_SIBLING_MODEL,
529
+ "--reasoning-effort",
530
+ GROK_SIBLING_EFFORT,
531
+ // `acceptEdits` auto-approves edits only. A terminal command — the git
532
+ // scan, the journal helper — then hits an approval gate with no human
533
+ // attached, and grok cancels the whole run: `stopReason: cancelled,
534
+ // cancellationCategory: PermissionCancelled`. Unattended approval plus
535
+ // a sandbox is the same posture codex gets from `-s workspace-write`.
536
+ "--always-approve",
537
+ "--sandbox",
538
+ "workspace",
539
+ "--max-turns",
540
+ GROK_SIBLING_MAX_TURNS,
541
+ ];
542
+ case "codex":
543
+ return [
544
+ "exec",
545
+ "--skip-git-repo-check",
546
+ "-s",
547
+ "workspace-write",
548
+ "--dangerously-bypass-hook-trust",
549
+ "-m",
550
+ CODEX_SIBLING_MODEL,
551
+ "-c",
552
+ `model_reasoning_effort=${CODEX_SIBLING_REASONING_EFFORT}`,
553
+ prompt,
554
+ ];
415
555
  }
416
- return value;
417
556
  }
418
- function siblingPayload(input, threadPath) {
557
+ function siblingPayload(input, threadPath, backend) {
419
558
  return {
559
+ backend,
420
560
  summary: input.summary ?? "",
421
561
  files: input.files,
422
562
  learnings: input.learnings,
@@ -525,7 +665,7 @@ function startSibling(liveRoot, input, threadPath, backend) {
525
665
  const siblingRoot = path.join(liveRoot, "workspace", "checkpoints", "sibling");
526
666
  const pendingPath = path.join(siblingRoot, "pending.jsonl");
527
667
  const lockPath = path.join(stateDir, "checkpoint-sibling.lock");
528
- const payload = siblingPayload(input, threadPath);
668
+ const payload = siblingPayload(input, threadPath, backend);
529
669
  const activePid = existingSiblingPid(lockPath, Date.now());
530
670
  fs.mkdirSync(siblingRoot, { recursive: true });
531
671
  if (activePid !== null) {
@@ -542,29 +682,7 @@ function startSibling(liveRoot, input, threadPath, backend) {
542
682
  fs.writeFileSync(path.join(runDir, "prompt.md"), prompt);
543
683
  const logPath = path.join(runDir, "run.log");
544
684
  const logFd = fs.openSync(logPath, "a");
545
- const args = backend === "claude"
546
- ? [
547
- "-p",
548
- prompt,
549
- "--model",
550
- CLAUDE_SIBLING_MODEL,
551
- "--effort",
552
- CLAUDE_SIBLING_EFFORT,
553
- "--permission-mode",
554
- "acceptEdits",
555
- ]
556
- : [
557
- "exec",
558
- "--skip-git-repo-check",
559
- "-s",
560
- "workspace-write",
561
- "--dangerously-bypass-hook-trust",
562
- "-m",
563
- CODEX_SIBLING_MODEL,
564
- "-c",
565
- `model_reasoning_effort=${CODEX_SIBLING_REASONING_EFFORT}`,
566
- prompt,
567
- ];
685
+ const args = siblingArgs(backend, prompt);
568
686
  let child;
569
687
  try {
570
688
  child = spawn(backend, args, {
@@ -587,7 +705,10 @@ function startSibling(liveRoot, input, threadPath, backend) {
587
705
  if (!child.pid)
588
706
  throw new Error("checkpoint: sibling did not return a PID");
589
707
  fs.writeFileSync(lockPath, String(child.pid));
590
- printResult(`checkpoint: sibling started (pid ${child.pid})`);
708
+ // Recorded per run so a later checkpoint can tell "died without a report"
709
+ // apart from "still working" without consulting the shared lock.
710
+ fs.writeFileSync(path.join(runDir, "sibling.pid"), String(child.pid));
711
+ printResult(`checkpoint: sibling started (${backend}, pid ${child.pid})`);
591
712
  }
592
713
  function hasGateProbeConflict(options, command) {
593
714
  return [
@@ -641,7 +762,7 @@ function runCheckpoint(options, command, group) {
641
762
  if (!input.summary?.trim()) {
642
763
  usage("checkpoint: --summary is required unless --idle or --gate-probe is used");
643
764
  }
644
- const backend = resolveBackend(options.backend);
765
+ const backend = resolveBackend(options.backend, liveRoot);
645
766
  const now = new Date();
646
767
  const threadId = `T-${formatTimestamp(now)}-auto-${summarySlug(input.summary)}`;
647
768
  const threadPath = path.join(liveRoot, "workspace", "threads", `${threadId}.json`);
@@ -730,7 +851,7 @@ export function registerCoreCheckpointCommand(core) {
730
851
  .option("--payload <file|->", "JSON payload file, or - for stdin")
731
852
  .option("--idle", "touch stamps without a checkpoint")
732
853
  .option("--no-agent", "do not spawn the maintenance sibling")
733
- .option("--backend <auto|claude|codex|none>", "sibling backend", "auto")
854
+ .option("--backend <auto|claude|codex|grok|none>", "sibling backend", "auto")
734
855
  .option("--gate-probe", "write the local Stop-hook eligibility verdict")
735
856
  .option("--hq-root <path>", "HQ installation to operate on")
736
857
  .option("--dry-run", "print the planned checkpoint without writing")
@@ -23,26 +23,38 @@ function relay(result) {
23
23
  process.stderr.write(result.stderr);
24
24
  }
25
25
  export function registerSearchCommand(program) {
26
- const search = program.command('search').description('Search the local HQ qmd index');
27
- search
28
- .command('get <document>')
29
- .description('Retrieve a qmd document by path or document id')
30
- .option('-c, --collection <collection>', 'Restrict retrieval to a collection')
31
- .action((document, options) => {
32
- relay(runQmd(buildGetArgs(document, options)));
33
- });
34
- search
35
- .command('<query>')
26
+ // The query is an ARGUMENT on `search` itself, not a subcommand. It was
27
+ // previously registered as `.command('<query>')`, which makes commander look
28
+ // for a subcommand literally named `<query>` — so every real query failed
29
+ // with "unknown command '<the query>'" and plain `hq search` never worked.
30
+ // Variadic so `hq search foo bar` needs no quoting; `search get` stays a
31
+ // subcommand and still wins when it is the first token.
32
+ const search = program
33
+ .command('search')
36
34
  .description('Search the local HQ qmd index')
35
+ .argument('[query...]', 'Query terms (joined with spaces)')
37
36
  .option('--mode <mode>', 'Search mode: keyword, semantic, or hybrid', 'keyword')
38
37
  .option('-c, --collection <collection>', 'Restrict search to a collection')
39
38
  .option('-n, --count <count>', 'Maximum result count', (value) => Number(value))
40
39
  .option('--json', 'Request machine-readable qmd output')
41
- .action((query, options) => {
40
+ .action((queryParts = [], options) => {
41
+ const query = queryParts.join(' ').trim();
42
+ if (!query) {
43
+ search.outputHelp();
44
+ process.exitCode = 1;
45
+ return;
46
+ }
42
47
  if (!['keyword', 'semantic', 'hybrid'].includes(options.mode ?? 'keyword')) {
43
48
  throw new Error(`Unknown search mode '${options.mode}'. Expected keyword, semantic, or hybrid.`);
44
49
  }
45
50
  relay(runQmd(buildSearchArgs(query, options)));
46
51
  });
52
+ search
53
+ .command('get <document>')
54
+ .description('Retrieve a qmd document by path or document id')
55
+ .option('-c, --collection <collection>', 'Restrict retrieval to a collection')
56
+ .action((document, options) => {
57
+ relay(runQmd(buildGetArgs(document, options)));
58
+ });
47
59
  }
48
60
  //# sourceMappingURL=search.js.map
@@ -27,8 +27,15 @@ export type ResolveQmdBinOptions = {
27
27
  isExecutable?: (candidate: string) => boolean;
28
28
  packageBin?: () => string | undefined;
29
29
  pathBin?: () => string | undefined;
30
+ isUsable?: (bin: string) => boolean;
30
31
  };
31
- /** Return the pinned package version when qmd is supplied by this CLI. */
32
+ /**
33
+ * Return the pinned package version when qmd is supplied by this CLI.
34
+ *
35
+ * Read from disk rather than `require('@tobilu/qmd/package.json')`: qmd 2.x does
36
+ * not export package.json, so the require form throws and the version silently
37
+ * disappeared from `hq index status`. Same root cause as packageLocalBin.
38
+ */
32
39
  export declare function resolveQmdVersion(): string | undefined;
33
40
  /** Resolve qmd without relying on a globally installed copy. */
34
41
  export declare function resolveQmdBin(options?: ResolveQmdBinOptions): string;
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
2
2
  import * as fs from 'node:fs';
3
3
  import { createRequire } from 'node:module';
4
4
  import * as path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  const require = createRequire(import.meta.url);
6
7
  export class QmdBinaryMissingError extends Error {
7
8
  name = 'QmdBinaryMissingError';
@@ -32,24 +33,68 @@ function isExecutable(candidate) {
32
33
  return false;
33
34
  }
34
35
  }
36
+ /**
37
+ * Locate the bundled qmd through the dependency-bin link the package manager
38
+ * installs, walking up from this module.
39
+ *
40
+ * Two earlier approaches were wrong and both failed silently, so every host fell
41
+ * through to PATH and a machine without a global qmd reported "skipped" — which
42
+ * defeats the point of depending on qmd at all:
43
+ *
44
+ * - `<pkg>/qmd` guessed a filename; the package declares `bin/qmd`.
45
+ * - `require.resolve('@tobilu/qmd/package.json')` throws
46
+ * ERR_PACKAGE_PATH_NOT_EXPORTED on qmd 2.x, whose `exports` map does not
47
+ * expose package.json.
48
+ *
49
+ * `node_modules/.bin/qmd` is the contract every package manager honours and is
50
+ * independent of the dependency's own exports map.
51
+ */
35
52
  function packageLocalBin() {
36
- try {
37
- const packageJson = require.resolve('@tobilu/qmd/package.json');
38
- return path.join(path.dirname(packageJson), 'qmd');
39
- }
40
- catch {
41
- return undefined;
53
+ const names = process.platform === 'win32' ? ['qmd.cmd', 'qmd.exe', 'qmd'] : ['qmd'];
54
+ let directory = path.dirname(fileURLToPath(import.meta.url));
55
+ for (let depth = 0; depth < 10; depth++) {
56
+ for (const name of names) {
57
+ const candidate = path.join(directory, 'node_modules', '.bin', name);
58
+ if (fs.existsSync(candidate))
59
+ return candidate;
60
+ }
61
+ const parent = path.dirname(directory);
62
+ if (parent === directory)
63
+ break;
64
+ directory = parent;
42
65
  }
66
+ return undefined;
43
67
  }
44
- /** Return the pinned package version when qmd is supplied by this CLI. */
68
+ /**
69
+ * Return the pinned package version when qmd is supplied by this CLI.
70
+ *
71
+ * Read from disk rather than `require('@tobilu/qmd/package.json')`: qmd 2.x does
72
+ * not export package.json, so the require form throws and the version silently
73
+ * disappeared from `hq index status`. Same root cause as packageLocalBin.
74
+ */
45
75
  export function resolveQmdVersion() {
46
- try {
47
- const packageJson = require('@tobilu/qmd/package.json');
48
- return typeof packageJson.version === 'string' ? packageJson.version : undefined;
49
- }
50
- catch {
51
- return undefined;
76
+ // Walk for the package directory itself. Neither require() nor the .bin entry
77
+ // works: qmd 2.x does not export package.json, and .bin/qmd is a generated
78
+ // shim (not a symlink) under pnpm's hoisted linker, so resolving it lands back
79
+ // in this package rather than qmd's.
80
+ let directory = path.dirname(fileURLToPath(import.meta.url));
81
+ for (let depth = 0; depth < 10; depth++) {
82
+ const manifest = path.join(directory, 'node_modules', '@tobilu', 'qmd', 'package.json');
83
+ if (fs.existsSync(manifest)) {
84
+ try {
85
+ const parsed = JSON.parse(fs.readFileSync(manifest, 'utf8'));
86
+ if (typeof parsed.version === 'string')
87
+ return parsed.version;
88
+ }
89
+ catch { /* unreadable manifest: report no version rather than throwing */ }
90
+ return undefined;
91
+ }
92
+ const parent = path.dirname(directory);
93
+ if (parent === directory)
94
+ break;
95
+ directory = parent;
52
96
  }
97
+ return undefined;
53
98
  }
54
99
  function pathBin() {
55
100
  const paths = (process.env.PATH ?? '').split(path.delimiter).filter(Boolean);
@@ -63,6 +108,27 @@ function pathBin() {
63
108
  }
64
109
  return undefined;
65
110
  }
111
+ const usableQmdCache = new Map();
112
+ /**
113
+ * Does this qmd binary actually run? `--version` is the cheapest subcommand
114
+ * that still loads the native module, which is exactly what fails when the
115
+ * bindings are missing. Cached per path so resolution stays a single spawn.
116
+ */
117
+ function isUsableQmd(bin) {
118
+ const cached = usableQmdCache.get(bin);
119
+ if (cached !== undefined)
120
+ return cached;
121
+ let usable = false;
122
+ try {
123
+ const probe = spawnSync(bin, ['--version'], { encoding: 'utf8', timeout: 10_000 });
124
+ usable = !probe.error && probe.status === 0;
125
+ }
126
+ catch {
127
+ usable = false;
128
+ }
129
+ usableQmdCache.set(bin, usable);
130
+ return usable;
131
+ }
66
132
  /** Resolve qmd without relying on a globally installed copy. */
67
133
  export function resolveQmdBin(options = {}) {
68
134
  const env = options.env ?? process.env;
@@ -78,12 +144,27 @@ export function resolveQmdBin(options = {}) {
78
144
  probes.push('HQ_QMD_BIN (not set)');
79
145
  }
80
146
  const installed = (options.packageBin ?? packageLocalBin)();
81
- if (installed && executable(installed))
82
- return installed;
83
- probes.push(`package-local @tobilu/qmd (${installed ?? 'not found'})`);
84
147
  const onPath = (options.pathBin ?? pathBin)();
85
- if (onPath && executable(onPath))
86
- return onPath;
148
+ const fallback = onPath && executable(onPath) ? onPath : undefined;
149
+ if (installed && executable(installed)) {
150
+ // Executable is not the same as usable: a global install can ship a qmd
151
+ // whose native better-sqlite3 bindings were never compiled, so every
152
+ // invocation crashes. Preferring it blindly turned a dormant packaging
153
+ // problem into broken search on hosts that had a working qmd on PATH.
154
+ //
155
+ // The runnability probe costs a ~0.3s spawn, so only pay it when there is
156
+ // something to fall back TO. With no alternative the answer is the bundled
157
+ // binary either way, and a failure there now surfaces loudly rather than
158
+ // silently, so probing would buy nothing but latency on the hot path.
159
+ if (!fallback || (options.isUsable ?? isUsableQmd)(installed))
160
+ return installed;
161
+ probes.push(`package-local @tobilu/qmd (${installed}, present but not runnable)`);
162
+ }
163
+ else {
164
+ probes.push(`package-local @tobilu/qmd (${installed ?? 'not found'})`);
165
+ }
166
+ if (fallback)
167
+ return fallback;
87
168
  probes.push(`qmd on PATH (${onPath ?? 'not found'})`);
88
169
  throw new QmdBinaryMissingError(`Unable to resolve qmd. Probed ${probes.join('; ')}. Install @tobilu/qmd or set HQ_QMD_BIN to an executable qmd binary.`);
89
170
  }
package/dist/main.js CHANGED
@@ -73,7 +73,7 @@ import { emitCliSessionStarted } from "./utils/cli-telemetry.js";
73
73
  import { settleWithin } from "./utils/settle-with-timeout.js";
74
74
  import { emitPlanLimitNag } from "./lib/plan-limit-nag.js";
75
75
  import { isPackageRootResolutionError, packageRootCaptureContext, } from "./utils/package-root-diagnostics.js";
76
- import { unexpectedCliErrorMessage } from "./utils/unexpected-cli-error.js";
76
+ import { fallbackOperatorMessage, unexpectedCliErrorMessage } from "./utils/unexpected-cli-error.js";
77
77
  /** Hard upper bound for non-user-visible release-health finalization. */
78
78
  const RELEASE_HEALTH_SETTLE_TIMEOUT_MS = 3_000;
79
79
  // Swallow EPIPE when a downstream reader (e.g. `source <(…)`, `| head`) closes
@@ -348,9 +348,13 @@ export function handleTopLevelError(err, deps = defaultTopLevelErrorDependencies
348
348
  }
349
349
  else {
350
350
  deps.sentry.captureException(err);
351
- const userMessage = unexpectedCliErrorMessage(err);
352
- if (userMessage)
353
- deps.stderr.write(`hq: ${userMessage}\n`);
351
+ // Always emit something. Printing only when unexpectedCliErrorMessage()
352
+ // returned a value meant every error class except IntegrationsCliError
353
+ // exited 1 with zero bytes on stdout AND stderr — a silent failure the
354
+ // caller cannot distinguish from success. The fallback runs through the
355
+ // same redaction chain, so this widens visibility, not disclosure.
356
+ const userMessage = unexpectedCliErrorMessage(err) ?? fallbackOperatorMessage(err);
357
+ deps.stderr.write(`hq: ${userMessage}\n`);
354
358
  }
355
359
  deps.setExitCode(1);
356
360
  }
@@ -5,4 +5,20 @@
5
5
  * are not printed indiscriminately.
6
6
  */
7
7
  export declare function unexpectedCliErrorMessage(err: unknown): string | null;
8
+ /**
9
+ * Apply the same redaction chain to an arbitrary error message.
10
+ *
11
+ * Exists so the top-level handler can ALWAYS emit a diagnostic. Previously it
12
+ * printed only when unexpectedCliErrorMessage() returned a value, and that
13
+ * returns null for everything except IntegrationsCliError — so any other
14
+ * failure (a qmd crash, for instance) exited 1 with zero bytes on both
15
+ * streams, which is indistinguishable from success-with-no-output.
16
+ */
17
+ export declare function redactForOperator(raw: string): string;
18
+ /**
19
+ * Last-resort operator message for an error the CLI has no specific handling
20
+ * for. Redacted through the same chain as the integrations path; falls back to
21
+ * a fixed string when the value carries no usable message.
22
+ */
23
+ export declare function fallbackOperatorMessage(err: unknown): string;
8
24
  //# sourceMappingURL=unexpected-cli-error.d.ts.map
@@ -8,7 +8,19 @@ import { IntegrationsCliError } from "../commands/integrations.js";
8
8
  export function unexpectedCliErrorMessage(err) {
9
9
  if (!(err instanceof IntegrationsCliError) || err.expected)
10
10
  return null;
11
- const message = err.message
11
+ return redactForOperator(err.message) || "Integration request failed";
12
+ }
13
+ /**
14
+ * Apply the same redaction chain to an arbitrary error message.
15
+ *
16
+ * Exists so the top-level handler can ALWAYS emit a diagnostic. Previously it
17
+ * printed only when unexpectedCliErrorMessage() returned a value, and that
18
+ * returns null for everything except IntegrationsCliError — so any other
19
+ * failure (a qmd crash, for instance) exited 1 with zero bytes on both
20
+ * streams, which is indistinguishable from success-with-no-output.
21
+ */
22
+ export function redactForOperator(raw) {
23
+ return raw
12
24
  .replace(/-----BEGIN[^-]+PRIVATE KEY-----[\s\S]*?-----END[^-]+PRIVATE KEY-----/g, "[REDACTED]")
13
25
  .replace(/\p{Cc}/gu, " ")
14
26
  .replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "[REDACTED]")
@@ -19,6 +31,18 @@ export function unexpectedCliErrorMessage(err) {
19
31
  .replace(/\s+/g, " ")
20
32
  .trim()
21
33
  .slice(0, 1_000);
22
- return message || "Integration request failed";
34
+ }
35
+ /**
36
+ * Last-resort operator message for an error the CLI has no specific handling
37
+ * for. Redacted through the same chain as the integrations path; falls back to
38
+ * a fixed string when the value carries no usable message.
39
+ */
40
+ export function fallbackOperatorMessage(err) {
41
+ const raw = err instanceof Error
42
+ ? `${err.name}: ${err.message}`
43
+ : typeof err === "string"
44
+ ? err
45
+ : "";
46
+ return redactForOperator(raw) || "command failed with an unreported error";
23
47
  }
24
48
  //# sourceMappingURL=unexpected-cli-error.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.94.0",
3
+ "version": "5.94.2",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {