@indigoai-us/hq-cli 5.103.14 → 5.103.16

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,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.103.16] — 2026-08-22
6
+
7
+ ## [5.103.15] — 2026-08-21
8
+
9
+ ### Fixed
10
+
11
+ - The bundled sync engine now starts at `@indigoai-us/hq-cloud` 6.15.27, so
12
+ execution-time file decisions use fresh bytes, downloaded files immediately
13
+ journal their cloud hash, and unchanged local/cloud content no longer creates
14
+ false conflicts or gets uploaded again.
15
+
5
16
  ## [5.103.14] — 2026-08-21
6
17
 
7
18
  ### Fixed
@@ -129,11 +129,22 @@ export declare class IntegrationsCliError extends Error {
129
129
  * surface the error instead of switching to the browser sign-in.
130
130
  */
131
131
  readonly oauthProtected?: boolean;
132
+ /**
133
+ * The gateway's numeric JSON-RPC error code (e.g. -32603, -32009), when the
134
+ * failure came back as a JSON-RPC error body. Read at the throw site for the
135
+ * `expected` decision and, previously, discarded there — now retained as a
136
+ * BOUNDED machine discriminator so an error that still reaches Sentry can be
137
+ * grouped by its protocol code (HQ-CLI collision, Sentry 7642756130). Like
138
+ * `code`, branch on THIS, never on the human message. It never reaches a
139
+ * fingerprint as anything but the finite allowlist in sentry-fingerprint.ts.
140
+ */
141
+ readonly rpcCode?: number;
132
142
  constructor(message: string, opts?: {
133
143
  expected?: boolean;
134
144
  code?: string;
135
145
  status?: number;
136
146
  oauthProtected?: boolean;
147
+ rpcCode?: number;
137
148
  });
138
149
  }
139
150
  /**
@@ -37,6 +37,16 @@ export class IntegrationsCliError extends Error {
37
37
  * surface the error instead of switching to the browser sign-in.
38
38
  */
39
39
  oauthProtected;
40
+ /**
41
+ * The gateway's numeric JSON-RPC error code (e.g. -32603, -32009), when the
42
+ * failure came back as a JSON-RPC error body. Read at the throw site for the
43
+ * `expected` decision and, previously, discarded there — now retained as a
44
+ * BOUNDED machine discriminator so an error that still reaches Sentry can be
45
+ * grouped by its protocol code (HQ-CLI collision, Sentry 7642756130). Like
46
+ * `code`, branch on THIS, never on the human message. It never reaches a
47
+ * fingerprint as anything but the finite allowlist in sentry-fingerprint.ts.
48
+ */
49
+ rpcCode;
40
50
  constructor(message, opts = {}) {
41
51
  super(message);
42
52
  this.name = "IntegrationsCliError";
@@ -47,6 +57,8 @@ export class IntegrationsCliError extends Error {
47
57
  this.status = opts.status;
48
58
  if (opts.oauthProtected !== undefined)
49
59
  this.oauthProtected = opts.oauthProtected;
60
+ if (opts.rpcCode !== undefined)
61
+ this.rpcCode = opts.rpcCode;
50
62
  }
51
63
  }
52
64
  /**
@@ -281,7 +293,12 @@ export async function callGateway(token, params) {
281
293
  if (!res.ok || !message) {
282
294
  raiseIfUnauthorized(res);
283
295
  raiseIfUpstreamUnavailable(res);
284
- throw new IntegrationsCliError(`Integration gateway request failed (HTTP ${res.status}).`, { expected: isClientError(res.status) });
296
+ // Carry the HTTP status so a reportable transport failure (e.g. a 501 that
297
+ // is neither 401 nor an upstream-availability status, so it is captured)
298
+ // reaches the Sentry fingerprint's HTTP bucket instead of colliding under
299
+ // default grouping. Same reasoning as raiseForResponse, which already sets
300
+ // `status`. This does not change the `expected` decision above.
301
+ throw new IntegrationsCliError(`Integration gateway request failed (HTTP ${res.status}).`, { expected: isClientError(res.status), status: res.status });
285
302
  }
286
303
  if (message.error) {
287
304
  // The gateway's error text is minted UPSTREAM (hq-pro, the integration
@@ -293,7 +310,13 @@ export async function callGateway(token, params) {
293
310
  // no-op. This preserves PR #298's user-visible diagnostic; it only makes
294
311
  // it safe on the newly-expected path.
295
312
  throw new IntegrationsCliError(redactErrorText(message.error.message ?? "") ||
296
- "Integration gateway returned an error.", { expected: isExpectedGatewayError(message.error.code) });
313
+ "Integration gateway returned an error.", {
314
+ expected: isExpectedGatewayError(message.error.code),
315
+ // Carry the protocol code to the capture site as a bounded grouping
316
+ // discriminator (HQ-CLI collision, Sentry 7642756130). This does not
317
+ // alter the `expected` decision above; it only labels the error.
318
+ ...(message.error.code !== undefined ? { rpcCode: message.error.code } : {}),
319
+ });
297
320
  }
298
321
  return message;
299
322
  }
package/dist/sentry.js CHANGED
@@ -6,6 +6,7 @@ import { CLI_VERSION } from "./cli-version.js";
6
6
  import { getCachedSentryUser } from "./utils/sentry-identity.js";
7
7
  import { isEpipe } from "./utils/epipe.js";
8
8
  import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
9
+ import { sentryFingerprintFor } from "./utils/sentry-fingerprint.js";
9
10
  /**
10
11
  * Drop broken-pipe (EPIPE) crashes before scrubbing/send. A closed downstream
11
12
  * reader (`hq … | head`, `source <(hq …)`, a parent that exited) is normal
@@ -30,6 +31,18 @@ export function epipeAwareBeforeSend(event, hint) {
30
31
  // route, while the CLI still exits non-zero. HQ-CLI-R (Sentry 7671416365).
31
32
  if (environmentalFsErrorMessage(hint?.originalException))
32
33
  return null;
34
+ // Group an event that survives to send by a BOUNDED machine discriminator so
35
+ // unrelated gateway/HTTP failures stop colliding into one fungible issue
36
+ // (HQ-CLI collision, Sentry 7642756130). Placed here — path-independent,
37
+ // before the fleet scrubber — so it covers EVERY capture route (the top-level
38
+ // boundary, the command-level captureException sites, bin/hq-auth-refresh,
39
+ // any uncaughtException), mirroring the EPIPE and environmental-fs drops
40
+ // above. The classifier reads only a closed allowlist (never message/argv),
41
+ // and returns null for anything it cannot bound, in which case Sentry's
42
+ // default grouping is left untouched.
43
+ const fingerprint = sentryFingerprintFor(hint?.originalException);
44
+ if (fingerprint)
45
+ event.fingerprint = fingerprint;
33
46
  return beforeSend(event, hint);
34
47
  }
35
48
  export function initSentry() {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Return a bounded `event.fingerprint` array for `err`, or `null` when `err`
3
+ * carries no discriminator from the closed allowlist (in which case the caller
4
+ * must leave the fingerprint unset so Sentry's default grouping applies).
5
+ *
6
+ * The shape is always `[DEFAULT_GROUPING, errorName, discriminator]`:
7
+ * - a numeric `rpcCode` (only the gateway's IntegrationsCliError carries one)
8
+ * yields `rpc:<code>` for a known code, else `rpc:other`;
9
+ * - otherwise a numeric HTTP `status` yields `http:<status>` for a known
10
+ * status, else `http:other`.
11
+ *
12
+ * `rpcCode` is checked first so a gateway JSON-RPC fault groups by its protocol
13
+ * code; the two carriers are disjoint in practice (the JSON-RPC throw site sets
14
+ * only `rpcCode`, the REST throw sites set only `status`).
15
+ */
16
+ export declare function sentryFingerprintFor(err: unknown): string[] | null;
17
+ //# sourceMappingURL=sentry-fingerprint.d.ts.map
@@ -0,0 +1,118 @@
1
+ // src/utils/sentry-fingerprint.ts
2
+ //
3
+ // Compute a BOUNDED Sentry grouping fingerprint from an error, using only a
4
+ // CLOSED allowlist of enumerable discriminators — never any upstream- or
5
+ // user-minted text. Sibling in spirit to qmd-collection-missing-error.ts
6
+ // (HQ-CLI-S): the same unbounded-fingerprint discipline, applied to grouping
7
+ // instead of to a printed line.
8
+ //
9
+ // HQ-CLI collision (Sentry 7642756130): hq-cli captures every unclassified
10
+ // error with a bare `captureException(err)` and sets no `event.fingerprint`
11
+ // anywhere, so Sentry falls back to grouping on the exception type plus the
12
+ // bundled `callGateway` culprit frame — which is IDENTICAL for unrelated
13
+ // gateway failures. Two disjoint defects (a July confirm-queue enqueue fault
14
+ // and an August remote-MCP HTTP 403) therefore landed in ONE issue, and the
15
+ // issue's title silently rewrote itself to the latest event. That fungible
16
+ // bucket destroys the regression watermark for every lane keyed on the issue.
17
+ //
18
+ // The remedy is a client-side fingerprint that SPLITS by a bounded machine
19
+ // discriminator while preserving Sentry's default grouping. The returned array
20
+ // leads with the `{{ default }}` placeholder (Sentry expands it to the normal
21
+ // stack-based grouping hash) and appends the error's own name plus a single
22
+ // discriminator token drawn from a finite set — so genuinely different failure
23
+ // classes form distinct, PREDICTABLE groups, and everything else is left to
24
+ // default grouping untouched.
25
+ //
26
+ // Two hard rules, mirroring HQ-CLI-S:
27
+ // 1. CLOSED ALLOWLIST. The discriminator is one of a finite set of known
28
+ // JSON-RPC codes / HTTP statuses, or the literal `rpc:other` / `http:other`
29
+ // bucket. Total cardinality is therefore bounded — an over-wide key that
30
+ // minted a brand-new permanent issue per invocation is impossible.
31
+ // 2. NO FREE TEXT. Only the error's `name` (a class identifier), a numeric
32
+ // `rpcCode`, and a numeric HTTP `status` are read. `message`, argv, URLs,
33
+ // connection ids, company slugs and any other upstream/user text can NEVER
34
+ // reach the fingerprint, so the key cannot be inflated by input.
35
+ //
36
+ // An error that carries no bounded discriminator returns `null`: the caller
37
+ // leaves `event.fingerprint` unset and Sentry keeps its DEFAULT grouping, so the
38
+ // blast radius of this change is limited to gateway- and HTTP-typed failures.
39
+ /**
40
+ * The finite set of JSON-RPC error codes the integration gateway is known to
41
+ * mint (hq-pro's `IntegrationMcpError` → code map plus the JSON-RPC reserved
42
+ * range it uses): PARSE_ERROR (-32700), METHOD_NOT_FOUND (-32601),
43
+ * INVALID_PARAMS (-32602), INTERNAL_ERROR (-32603), UNAUTHORIZED (-32003),
44
+ * CONFLICT (-32009, also raised for a confirm queue being unavailable), and
45
+ * PROVIDER_ERROR (-32050). Anything outside this set collapses to `rpc:other`.
46
+ */
47
+ const KNOWN_JSONRPC_CODES = new Set([
48
+ -32700, -32601, -32602, -32603, -32003, -32009, -32050,
49
+ ]);
50
+ /**
51
+ * The finite set of HTTP statuses worth their own group. Any other numeric
52
+ * status collapses to `http:other`, so the bucket count stays bounded no matter
53
+ * what an upstream returns.
54
+ */
55
+ const KNOWN_HTTP_STATUSES = new Set([
56
+ 400, 401, 403, 404, 405, 408, 409, 410, 422, 429,
57
+ 500, 501, 502, 503, 504,
58
+ ]);
59
+ /** Sentry's placeholder for "keep the normal, stack-based grouping too". */
60
+ const DEFAULT_GROUPING = "{{ default }}";
61
+ /**
62
+ * The CLOSED set of error class names allowed to appear VERBATIM in a
63
+ * fingerprint. This hook runs on every capture route and accepts `unknown`, so
64
+ * a status-bearing error whose `name` is dynamic — an upstream error TYPE, or a
65
+ * request identifier spliced into the name — could otherwise mint an unbounded
66
+ * number of groups DESPITE the bounded discriminator. The finite-cardinality
67
+ * promise must hold for the WHOLE key, so any name outside this set collapses to
68
+ * FALLBACK_ERROR_NAME. The only first-party error carrying `rpcCode`/`status`
69
+ * is IntegrationsCliError; add a name here only when a new bounded carrier
70
+ * genuinely warrants its own family of groups.
71
+ */
72
+ const KNOWN_ERROR_NAMES = new Set(["IntegrationsCliError"]);
73
+ /** Fixed bucket for any error name outside the closed allowlist. */
74
+ const FALLBACK_ERROR_NAME = "other";
75
+ /**
76
+ * Return a bounded `event.fingerprint` array for `err`, or `null` when `err`
77
+ * carries no discriminator from the closed allowlist (in which case the caller
78
+ * must leave the fingerprint unset so Sentry's default grouping applies).
79
+ *
80
+ * The shape is always `[DEFAULT_GROUPING, errorName, discriminator]`:
81
+ * - a numeric `rpcCode` (only the gateway's IntegrationsCliError carries one)
82
+ * yields `rpc:<code>` for a known code, else `rpc:other`;
83
+ * - otherwise a numeric HTTP `status` yields `http:<status>` for a known
84
+ * status, else `http:other`.
85
+ *
86
+ * `rpcCode` is checked first so a gateway JSON-RPC fault groups by its protocol
87
+ * code; the two carriers are disjoint in practice (the JSON-RPC throw site sets
88
+ * only `rpcCode`, the REST throw sites set only `status`).
89
+ */
90
+ export function sentryFingerprintFor(err) {
91
+ if (err === null || typeof err !== "object")
92
+ return null;
93
+ const record = err;
94
+ // The error's class identifier for the fingerprint's middle component. A name
95
+ // is required (so the key has a stable label), but it is bounded to the closed
96
+ // allowlist: a recognized class keeps its name, anything else collapses to the
97
+ // fixed FALLBACK_ERROR_NAME bucket so a dynamic name cannot inflate the key.
98
+ const rawName = typeof record.name === "string" && record.name.length > 0 ? record.name : null;
99
+ if (rawName === null)
100
+ return null;
101
+ const name = KNOWN_ERROR_NAMES.has(rawName) ? rawName : FALLBACK_ERROR_NAME;
102
+ const rpcCode = record.rpcCode;
103
+ if (typeof rpcCode === "number" && Number.isInteger(rpcCode)) {
104
+ const discriminator = KNOWN_JSONRPC_CODES.has(rpcCode)
105
+ ? `rpc:${rpcCode}`
106
+ : "rpc:other";
107
+ return [DEFAULT_GROUPING, name, discriminator];
108
+ }
109
+ const status = record.status;
110
+ if (typeof status === "number" && Number.isInteger(status)) {
111
+ const discriminator = KNOWN_HTTP_STATUSES.has(status)
112
+ ? `http:${status}`
113
+ : "http:other";
114
+ return [DEFAULT_GROUPING, name, discriminator];
115
+ }
116
+ return null;
117
+ }
118
+ //# sourceMappingURL=sentry-fingerprint.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.103.14",
3
+ "version": "5.103.16",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -30,7 +30,7 @@
30
30
  "dependencies": {
31
31
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
32
32
  "@aws-sdk/client-s3": "^3.1049.0",
33
- "@indigoai-us/hq-cloud": "~6.15.24",
33
+ "@indigoai-us/hq-cloud": "~6.15.27",
34
34
  "@indigoai-us/hq-onboarding": "^0.1.0",
35
35
  "@sentry/node": "^10.49.0",
36
36
  "@tobilu/qmd": "2.5.3",