@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
|
-
|
|
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.", {
|
|
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.
|
|
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.
|
|
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",
|