@indigoai-us/hq-cli 5.103.26 → 5.103.27

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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.103.27] — 2026-08-28
6
+
5
7
  ## [5.103.26] — 2026-08-27
6
8
 
7
9
  ### Changed
@@ -272,15 +272,11 @@ async function runGroupSend(recipients, message) {
272
272
  for (const r of recipients) {
273
273
  const rc = detectRecipient(r);
274
274
  if (!rc) {
275
- console.error(chalk.red(`Invalid recipient '${r}': each must be an email address or a personUid (prs_…).`));
276
- process.exit(1);
277
- }
278
- // Group DMs are channels — agents don't participate in channels (their
279
- // DM surface is 1:1 via the durable box inbox). DM an agent directly.
280
- if (rc.toPersonUid?.startsWith("agt_")) {
281
- console.error(chalk.red(`Agents can't join group DMs yet — DM '${r}' directly: hq dm ${r} "<message>".`));
275
+ console.error(chalk.red(`Invalid recipient '${r}': each must be an email address, a personUid (prs_…), or an agentUid (agt_…).`));
282
276
  process.exit(1);
283
277
  }
278
+ // People (prs_) and agents (agt_) are both valid group participants —
279
+ // the server (handleCreateGroupDm) accepts agt_* uids like any other.
284
280
  participants.push(rc.toEmail ?? rc.toPersonUid);
285
281
  }
286
282
  if (participants.length < 2) {
@@ -26,7 +26,7 @@ export interface IntegrationsDoctorDeps {
26
26
  resolveCompany?: (token: string, company: string | undefined) => Promise<string>;
27
27
  listConnections?: (token: string, companyUid: string) => Promise<IntegrationConnection[]>;
28
28
  }
29
- type FindingKind = "reconnect" | "re-add" | "contact-admin" | "provider-blocked" | "retryable" | "hq-configuration";
29
+ type FindingKind = "reconnect" | "re-add" | "contact-admin" | "wait" | "provider-blocked" | "retryable" | "hq-configuration";
30
30
  interface Finding {
31
31
  provider: string;
32
32
  connectionId: string;
@@ -25,6 +25,7 @@ const RETRYABLE_REASON_CODES = new Set([
25
25
  "oauth_refresh_write_conflict",
26
26
  ]);
27
27
  const HQ_CONFIGURATION_REASON_CODE = "oauth_client_secret_unavailable";
28
+ const UNSPECIFIED_REASON_CODE = "unspecified";
28
29
  /**
29
30
  * Expected session/company prerequisites degrade to NA instead of crashing or
30
31
  * becoming a false connection failure. An unreadable inventory remains UNKNOWN.
@@ -101,11 +102,18 @@ export function classifyConnection(connection) {
101
102
  const reason = recordedReason(connection);
102
103
  const connectedWithRecordedFailure = connection.status === "connected" && reason !== "";
103
104
  const knownReasonCode = knownReasonCodeFor(connection);
104
- if (knownReasonCode) {
105
+ // `unspecified` is a recognised member of the server vocabulary, but it is an
106
+ // absence of diagnosis rather than one. Short-circuiting on it here would skip
107
+ // the status-aware branches below and report a degraded connection as an error
108
+ // needing a reconnect. It is deliberately allowed to fall through, where the
109
+ // server's role- and credential-aware remediation is applied like any other
110
+ // undiagnosed row.
111
+ if (knownReasonCode && knownReasonCode !== UNSPECIFIED_REASON_CODE) {
105
112
  const finding = findingForKnownReasonCode(connection, provider, knownReasonCode);
106
113
  // Retryable and HQ-configuration reason codes diagnose conditions that a
107
114
  // caller-specific remediation cannot change. Reconnect-class codes defer
108
- // to the server's credential/role-aware remediation classification.
115
+ // to the server's credential/role-aware remediation classification, so an
116
+ // install in progress is never interrupted by a reconnect.
109
117
  return [withServerRemediation(connection, RECONNECT_REASON_CODES.has(knownReasonCode)
110
118
  ? findingForServerFixKind(finding, connection.fix_kind)
111
119
  : finding)];
@@ -179,6 +187,12 @@ function findingForServerFixKind(fallback, fixKind) {
179
187
  kind: "contact-admin",
180
188
  message: "must be repaired by a company owner or admin",
181
189
  };
190
+ case "wait":
191
+ return {
192
+ ...fallback,
193
+ kind: "wait",
194
+ message: "is waiting for the Factory installation to complete",
195
+ };
182
196
  case "reconnect":
183
197
  default:
184
198
  return fallback;
@@ -199,11 +213,13 @@ function withServerRemediation(connection, finding) {
199
213
  * the legacy text heuristics, which remain below for old and unknown rows.
200
214
  */
201
215
  function knownReasonCodeFor(connection) {
202
- return [connection.errorReason, connection.needsReauthReason, connection.degradedReason]
203
- .find((value) => (typeof value === "string" &&
216
+ const values = [connection.errorReason, connection.needsReauthReason, connection.degradedReason];
217
+ if (values.includes(HQ_CONFIGURATION_REASON_CODE))
218
+ return HQ_CONFIGURATION_REASON_CODE;
219
+ return values.find((value) => (typeof value === "string" &&
204
220
  (RECONNECT_REASON_CODES.has(value) ||
205
221
  RETRYABLE_REASON_CODES.has(value) ||
206
- value === HQ_CONFIGURATION_REASON_CODE)));
222
+ value === UNSPECIFIED_REASON_CODE)));
207
223
  }
208
224
  function findingForKnownReasonCode(connection, provider, code) {
209
225
  if (RECONNECT_REASON_CODES.has(code)) {
@@ -288,6 +304,15 @@ function resultForGroup(entries, company) {
288
304
  ...(serverRemediation ? { remediation: serverRemediation } : {}),
289
305
  };
290
306
  }
307
+ if (first.kind === "wait") {
308
+ return {
309
+ status: "WARN",
310
+ checkId: `${INTEGRATIONS_PREFIX}.wait.${first.provider}`,
311
+ target: namedConnections,
312
+ message: `${first.provider}: ${count} ${plural} ${first.message}. Wait for it to complete, then retry the operation.`,
313
+ remediation: serverRemediation ?? "Wait for the Factory installation to complete, then retry the operation.",
314
+ };
315
+ }
291
316
  if (first.kind === "hq-configuration") {
292
317
  return {
293
318
  status: "FAIL",
@@ -108,6 +108,28 @@ export declare class QmdTerminatedError extends QmdExitError {
108
108
  name: string;
109
109
  constructor(message: string, args: string[], status: number | null, stdout: string, stderr: string, signal: string);
110
110
  }
111
+ /**
112
+ * qmd refused an LLM-backed operation because its local LLM is DISABLED IN CI:
113
+ * qmd hard-disables every LLM operation whenever `CI` is truthy in the
114
+ * environment and throws `Error: LLM operations are disabled in CI (set
115
+ * CI=true)`. Its semantic/hybrid reads (`vsearch`/`query`) expand the query
116
+ * through that gate, and the index build (`embed`) generates vectors through it,
117
+ * so any of them dies immediately when CI is set. hq-cli forwards the caller's
118
+ * environment — including CI — to the qmd child, so this is the caller's
119
+ * ENVIRONMENT, not an hq-cli defect.
120
+ *
121
+ * Before this class the refusal rendered as `qmd <full argv> exited with 1:
122
+ * <qmd's stderr>` and, because that message interpolated the caller's whole
123
+ * argv, every distinct search query minted a brand-new permanent Sentry issue —
124
+ * the same unbounded-fingerprint failure fixed for HQ-CLI-S and HQ-CLI
125
+ * 7677702704. This typed subclass lets the boundary classify the condition as
126
+ * the caller's environment and stop fingerprinting on the query text: it is
127
+ * raised only from qmd's OWN captured streams and its message names the
128
+ * SUBCOMMAND only. Sentry HQ-CLI 7688850003.
129
+ */
130
+ export declare class QmdLlmDisabledError extends QmdExitError {
131
+ name: string;
132
+ }
111
133
  export type ResolveQmdBinOptions = {
112
134
  env?: Record<string, string | undefined>;
113
135
  isExecutable?: (candidate: string) => boolean;
@@ -86,6 +86,28 @@ export class QmdTerminatedError extends QmdExitError {
86
86
  this.signal = signal;
87
87
  }
88
88
  }
89
+ /**
90
+ * qmd refused an LLM-backed operation because its local LLM is DISABLED IN CI:
91
+ * qmd hard-disables every LLM operation whenever `CI` is truthy in the
92
+ * environment and throws `Error: LLM operations are disabled in CI (set
93
+ * CI=true)`. Its semantic/hybrid reads (`vsearch`/`query`) expand the query
94
+ * through that gate, and the index build (`embed`) generates vectors through it,
95
+ * so any of them dies immediately when CI is set. hq-cli forwards the caller's
96
+ * environment — including CI — to the qmd child, so this is the caller's
97
+ * ENVIRONMENT, not an hq-cli defect.
98
+ *
99
+ * Before this class the refusal rendered as `qmd <full argv> exited with 1:
100
+ * <qmd's stderr>` and, because that message interpolated the caller's whole
101
+ * argv, every distinct search query minted a brand-new permanent Sentry issue —
102
+ * the same unbounded-fingerprint failure fixed for HQ-CLI-S and HQ-CLI
103
+ * 7677702704. This typed subclass lets the boundary classify the condition as
104
+ * the caller's environment and stop fingerprinting on the query text: it is
105
+ * raised only from qmd's OWN captured streams and its message names the
106
+ * SUBCOMMAND only. Sentry HQ-CLI 7688850003.
107
+ */
108
+ export class QmdLlmDisabledError extends QmdExitError {
109
+ name = 'QmdLlmDisabledError';
110
+ }
89
111
  function isExecutable(candidate) {
90
112
  try {
91
113
  fs.accessSync(candidate, fs.constants.X_OK);
@@ -730,6 +752,21 @@ function finishRunQmd(result, bin, args) {
730
752
  }
731
753
  const detail = normalized.stderr || normalized.stdout || 'qmd returned no diagnostic output';
732
754
  const message = `qmd ${args.join(' ')} exited with ${normalized.status ?? 'an unknown status'}: ${detail}`;
755
+ // qmd refused because its local LLM is disabled: whenever CI is set in the
756
+ // environment, qmd hard-disables every LLM operation, so `vsearch`/`query`
757
+ // (query expansion) and `embed` (vector build) throw `Error: LLM operations
758
+ // are disabled in CI (set CI=true)` before doing any work. hq-cli inherits the
759
+ // caller's CI into the qmd child (runQmd forwards process.env), so this is the
760
+ // caller's ENVIRONMENT, not an hq-cli defect. Match qmd's OWN captured streams
761
+ // (stderr AND stdout) — never the synthesized `message`, which embeds the
762
+ // caller's whole argv — and name the SUBCOMMAND only, so the Sentry group can
763
+ // no longer fingerprint per search query (HQ-CLI 7688850003). Checked ahead of
764
+ // the collection wordings: the refusal text matches none of them, so order is
765
+ // safe, and the narrower typed signature stays first.
766
+ if (/LLM operations are disabled/i.test(`${normalized.stderr}\n${normalized.stdout}`)) {
767
+ const subcommand = typeof args[0] === 'string' && args[0].length > 0 ? redactErrorText(args[0]) || 'qmd' : 'qmd';
768
+ throw new QmdLlmDisabledError(`qmd ${subcommand} could not run: its local LLM is disabled because CI is set in the environment`, args, normalized.status, normalized.stdout, normalized.stderr);
769
+ }
733
770
  if (/(?:collection|qmd:\/\/).*(?:not found|does not exist|unknown)|(?:not found|does not exist).*collection/i.test(detail)) {
734
771
  throw new QmdCollectionMissingError(message, args, normalized.status, normalized.stdout, normalized.stderr);
735
772
  }
package/dist/main.js CHANGED
@@ -68,6 +68,7 @@ import { networkTransportErrorMessage } from "./utils/network-transport-error.js
68
68
  import { qmdNativeBindingErrorMessage } from "./utils/qmd-native-binding-error.js";
69
69
  import { qmdMissingCollectionMessage } from "./utils/qmd-collection-missing-error.js";
70
70
  import { qmdTerminatedMessage } from "./utils/qmd-terminated-error.js";
71
+ import { qmdLlmDisabledMessage } from "./utils/qmd-llm-disabled-error.js";
71
72
  import { isExpectedUserError } from "./utils/expected-cli-error.js";
72
73
  import { isEpipe } from "./utils/epipe.js";
73
74
  import { isInterceptedProcessExit } from "./utils/intercepted-process-exit.js";
@@ -486,7 +487,23 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
486
487
  // collection-missing checks (both narrower typed signatures) and BEFORE the
487
488
  // environmental / transport / generic branches.
488
489
  const terminatedMsg = qmdMsg || collectionMsg ? null : qmdTerminatedMessage(err);
489
- const envMsg = qmdMsg || collectionMsg || terminatedMsg ? null : environmentalFsErrorMessage(err);
490
+ // A qmd LLM-backed operation refused because qmd's local LLM is DISABLED
491
+ // whenever CI is set in the environment (`vsearch`/`query` expand the
492
+ // query through it; `embed` builds vectors through it). hq-cli forwards the
493
+ // caller's CI to the qmd child, so this is the caller's ENVIRONMENT, not an
494
+ // hq-cli defect. Before this branch the refusal reached the final else and,
495
+ // because the synthesized message embedded the caller's whole argv, minted
496
+ // a brand-new permanent issue per query (HQ-CLI 7688850003). finishRunQmd
497
+ // now types it QmdLlmDisabledError; print the query-free remedy and skip
498
+ // capture. Evaluated AFTER the native-binding / collection-missing /
499
+ // terminated checks (narrower typed signatures) and BEFORE the
500
+ // environmental / transport / generic branches. Scoped to the subcommands
501
+ // that legitimately need the LLM (vsearch/query/embed); an LLM-disabled
502
+ // failure from hq's OWN reconciliation stays a captured internal error.
503
+ const llmDisabledMsg = qmdMsg || collectionMsg || terminatedMsg ? null : qmdLlmDisabledMessage(err);
504
+ const envMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg
505
+ ? null
506
+ : environmentalFsErrorMessage(err);
490
507
  // A raw network transport failure (undici's `TypeError: fetch failed`
491
508
  // with a ConnectTimeoutError / ECONNREFUSED / ENOTFOUND cause) is the
492
509
  // caller's connectivity, not an hq-cli defect. Before this branch it fell
@@ -497,7 +514,9 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
497
514
  // message that names the unreachable host, exit 1, and skip Sentry.
498
515
  // Ordered after the environmental check so a full disk keeps its exact
499
516
  // existing message.
500
- const transportMsg = qmdMsg || collectionMsg || terminatedMsg || envMsg ? null : networkTransportErrorMessage(err);
517
+ const transportMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || envMsg
518
+ ? null
519
+ : networkTransportErrorMessage(err);
501
520
  if (qmdMsg) {
502
521
  deps.stderr.write(`hq: ${qmdMsg}\n`);
503
522
  }
@@ -507,6 +526,9 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
507
526
  else if (terminatedMsg) {
508
527
  deps.stderr.write(`hq: ${terminatedMsg}\n`);
509
528
  }
529
+ else if (llmDisabledMsg) {
530
+ deps.stderr.write(`hq: ${llmDisabledMsg}\n`);
531
+ }
510
532
  else if (envMsg) {
511
533
  deps.stderr.write(`hq: ${envMsg}\n`);
512
534
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * If `err` is a qmd LLM-disabled-in-CI failure on a subcommand that legitimately
3
+ * needs the LLM, return an actionable, query-free remedy; otherwise return
4
+ * `null`. Mirrors qmdTerminatedMessage / qmdMissingCollectionMessage /
5
+ * qmdNativeBindingErrorMessage so the top-level handler branches the same way: a
6
+ * non-null result means print-and-skip-Sentry, null means "handle as usual
7
+ * (capture to Sentry)".
8
+ */
9
+ export declare function qmdLlmDisabledMessage(err: unknown): string | null;
10
+ //# sourceMappingURL=qmd-llm-disabled-error.d.ts.map
@@ -0,0 +1,87 @@
1
+ // src/utils/qmd-llm-disabled-error.ts
2
+ //
3
+ // Classify a qmd failure caused by qmd's local LLM being DISABLED IN CI — not an
4
+ // hq-cli code defect. qmd hard-disables every LLM operation whenever `CI` is
5
+ // truthy in the environment (`Error: LLM operations are disabled in CI (set
6
+ // CI=true)`); its semantic/hybrid reads (`vsearch`/`query`) expand the query
7
+ // through that gate and the index build (`embed`) generates embeddings through
8
+ // it, so each dies immediately when CI is set. hq-cli forwards the caller's
9
+ // environment — including CI — to the qmd child, so this is the caller's
10
+ // ENVIRONMENT, not a bug HQ can fix in code: surface an actionable remedy and
11
+ // SKIP Sentry capture. Sibling of qmd-terminated-error.ts (HQ-CLI 7677702704),
12
+ // qmd-collection-missing-error.ts (HQ-CLI-S), qmd-native-binding-error.ts
13
+ // (HQ-CLI-J), environmental-error.ts (HQ-CLI-2) and network-transport-error.ts
14
+ // (HQ-CLI-G): a failure that is NOT an hq-cli defect is printed with an
15
+ // actionable message and never filed as a crash.
16
+ //
17
+ // HQ-CLI 7688850003: `hq search <query> --mode semantic ...` spawned `qmd
18
+ // vsearch`, which refused because CI was set on the host (an automation
19
+ // harness). hq-cli had no classifier for that wording, so finishRunQmd raised a
20
+ // plain QmdExitError whose synthesized message interpolates the caller's whole
21
+ // argv; nothing caught it, so it reached the top-level handler's final else and
22
+ // was captured — and because the message embeds the query, every distinct query
23
+ // minted a brand-new permanent Sentry issue. finishRunQmd now types the
24
+ // condition QmdLlmDisabledError (raised only from qmd's OWN captured streams,
25
+ // message naming the subcommand only); this classifier closes the boundary.
26
+ //
27
+ // The gate is deliberately narrow on TWO axes so it can neither be tripped by
28
+ // user input nor silence a real bug:
29
+ // 1. CLASS: only a QmdLlmDisabledError (the typed subclass finishRunQmd raises
30
+ // for the LLM-disabled wording) qualifies — never a plain QmdExitError, a
31
+ // native-binding failure, or any other error, even one carrying the same
32
+ // wording in a user-controlled field.
33
+ // 2. INVOCATION: only the qmd subcommands that legitimately need the LLM — the
34
+ // caller-supplied semantic/hybrid reads `vsearch`/`query`, plus the index
35
+ // build `embed`. Any other subcommand (e.g. an LLM-disabled failure surfaced
36
+ // by hq's OWN `collection list` reconciliation) returns null and stays on
37
+ // the captured-error path, so a condition hq did not expect is still
38
+ // reported — now grouped per subcommand rather than per query.
39
+ //
40
+ // The remedy is entirely query-free: only the SUBCOMMAND — drawn from a closed
41
+ // allow-list — selects the wording, and NOTHING is interpolated from user input,
42
+ // so there is no injection surface at all. This preserves the bounded-
43
+ // fingerprint doctrine established by HQ-CLI-S and HQ-CLI 7677702704.
44
+ /** Semantic/hybrid reads whose query is EXPANDED through qmd's LLM gate. */
45
+ const LLM_SEARCH_READS = new Set(["vsearch", "query"]);
46
+ /** The index build that generates embeddings through the same LLM gate. */
47
+ const LLM_INDEX_BUILD = "embed";
48
+ /**
49
+ * Remedy for a semantic/hybrid SEARCH read. Leads with `--mode keyword`, which
50
+ * needs no LLM and is correct in EVERY environment (including a genuine CI
51
+ * pipeline where clearing CI is not an option), and offers clearing CI only as
52
+ * the secondary option for a non-CI host that merely has the variable set.
53
+ */
54
+ const SEARCH_REMEDY = "Semantic and hybrid search need qmd's local LLM, which is switched off " +
55
+ "because CI is set in this environment. Re-run with '--mode keyword' (it needs " +
56
+ "no LLM and works everywhere), or clear CI for the command and try again.";
57
+ /**
58
+ * Remedy for the `embed` index build, which cannot run at all without the LLM —
59
+ * so `--mode keyword` does not apply and the only fix is to clear CI.
60
+ */
61
+ const EMBED_REMEDY = "Embeddings can't be built while CI is set in this environment, because qmd " +
62
+ "disables its local LLM there. Clear CI for the command and run it again.";
63
+ /**
64
+ * If `err` is a qmd LLM-disabled-in-CI failure on a subcommand that legitimately
65
+ * needs the LLM, return an actionable, query-free remedy; otherwise return
66
+ * `null`. Mirrors qmdTerminatedMessage / qmdMissingCollectionMessage /
67
+ * qmdNativeBindingErrorMessage so the top-level handler branches the same way: a
68
+ * non-null result means print-and-skip-Sentry, null means "handle as usual
69
+ * (capture to Sentry)".
70
+ */
71
+ export function qmdLlmDisabledMessage(err) {
72
+ if (err === null || typeof err !== "object")
73
+ return null;
74
+ const record = err;
75
+ if (record.name !== "QmdLlmDisabledError")
76
+ return null;
77
+ const args = Array.isArray(record.args) ? record.args : [];
78
+ const subcommand = args[0];
79
+ if (typeof subcommand !== "string")
80
+ return null;
81
+ if (LLM_SEARCH_READS.has(subcommand))
82
+ return SEARCH_REMEDY;
83
+ if (subcommand === LLM_INDEX_BUILD)
84
+ return EMBED_REMEDY;
85
+ return null;
86
+ }
87
+ //# sourceMappingURL=qmd-llm-disabled-error.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.103.26",
3
+ "version": "5.103.27",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {