@indigoai-us/hq-cli 5.85.0 → 5.85.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.85.2]
6
+
7
+ ### Fixed
8
+
9
+ - Bumped the `@indigoai-us/hq-cloud` floor to ^6.14.45, which scopes the sync
10
+ watcher to paths that can actually upload. On a large HQ root the watcher had
11
+ been asking the OS to report every directory in the tree — including
12
+ `repos/`, `workspace/worktrees/`, `node_modules/` and build output that sync
13
+ never uploads — and paying a blocking stat per event before discarding it.
14
+ One observed root reported 154,489 directories where 20,308 were in scope,
15
+ with the sync runner pinned above 100% CPU in garbage collection and growing
16
+ to 1.2 GB RSS over a 12-hour run. (hq-cloud#283)
17
+
18
+ ## [5.85.1]
19
+
20
+ ### Fixed
21
+
22
+ - Integration/provider failures now print a bounded, credential-scrubbed
23
+ diagnostic to stderr while remaining captured in Sentry, so failed remote
24
+ MCP calls no longer exit silently. (#298)
25
+ - Package-root resolution now recovers safely from transient manifest loss and
26
+ reports bounded diagnostics when a packaged install is genuinely broken.
27
+ (#297)
28
+
5
29
  ## [5.85.0]
6
30
 
7
31
  ### Changed
@@ -307,7 +307,20 @@ function writeStamps(liveRoot, sessionId) {
307
307
  fs.writeFileSync(stampPath, timestamp);
308
308
  return stampPaths;
309
309
  }
310
- const CODEX_CHECKPOINT_ACCOUNT = "hassaan@getindigo.ai";
310
+ /**
311
+ * Codex Stop-gate rollout scope: every HQ user on the operator domain, rather
312
+ * than the single account that dogfooded it first. Eligibility is decided on
313
+ * the email domain alone — a candidate must have exactly one `@` and the part
314
+ * after it must equal this domain, so lookalikes never pass (`xgetindigo.ai`,
315
+ * `getindigo.ai.evil.test`, `a@b@getindigo.ai`).
316
+ */
317
+ const CODEX_CHECKPOINT_DOMAIN = "getindigo.ai";
318
+ function hasCodexCheckpointDomain(candidate) {
319
+ if (typeof candidate !== "string")
320
+ return false;
321
+ const parts = candidate.trim().toLowerCase().split("@");
322
+ return parts.length === 2 && parts[0].length > 0 && parts[1] === CODEX_CHECKPOINT_DOMAIN;
323
+ }
311
324
  function checkpointGateRuntime() {
312
325
  const runtime = (process.env.HQ_CHECKPOINT_RUNTIME ?? "claude").trim().toLowerCase();
313
326
  if (runtime === "claude" || runtime === "codex")
@@ -331,7 +344,7 @@ function gateEligibility(runtime) {
331
344
  return false;
332
345
  const claims = peekIdToken(tokens.idToken);
333
346
  const candidateEmails = [claims.email, claims["custom:delegatedEmail"]];
334
- return candidateEmails.some((candidate) => typeof candidate === "string" && candidate.toLowerCase() === CODEX_CHECKPOINT_ACCOUNT);
347
+ return candidateEmails.some(hasCodexCheckpointDomain);
335
348
  }
336
349
  catch {
337
350
  return false;
package/dist/main.d.ts CHANGED
@@ -3,5 +3,13 @@
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
5
  import "./node-preflight.js";
6
+ import { Sentry } from "./sentry.js";
6
7
  export declare function runCli(): Promise<void>;
8
+ export type TopLevelErrorDependencies = {
9
+ sentry: Pick<typeof Sentry, "captureException">;
10
+ stderr: Pick<typeof process.stderr, "write">;
11
+ setExitCode: (code: number) => void;
12
+ };
13
+ /** Classify a top-level failure without making the CLI process boundary opaque to tests. */
14
+ export declare function handleTopLevelError(err: unknown, deps?: TopLevelErrorDependencies): void;
7
15
  //# sourceMappingURL=main.d.ts.map
package/dist/main.js CHANGED
@@ -70,6 +70,8 @@ import { CLI_VERSION } from "./cli-version.js";
70
70
  import { emitCliSessionStarted } from "./utils/cli-telemetry.js";
71
71
  import { settleWithin } from "./utils/settle-with-timeout.js";
72
72
  import { emitPlanLimitNag } from "./lib/plan-limit-nag.js";
73
+ import { isPackageRootResolutionError, packageRootCaptureContext, } from "./utils/package-root-diagnostics.js";
74
+ import { unexpectedCliErrorMessage } from "./utils/unexpected-cli-error.js";
73
75
  /** Hard upper bound for non-user-visible release-health finalization. */
74
76
  const RELEASE_HEALTH_SETTLE_TIMEOUT_MS = 3_000;
75
77
  // Swallow EPIPE when a downstream reader (e.g. `source <(…)`, `| head`) closes
@@ -247,71 +249,7 @@ export async function runCli() {
247
249
  await program.parseAsync();
248
250
  }
249
251
  catch (err) {
250
- // A broken pipe (EPIPE) means the reader of `hq`'s output closed it early
251
- // (`hq … | head`, `source <(hq …)`, a parent that exited). That is normal
252
- // Unix behavior with no user-facing degradation — exit cleanly (0) and
253
- // skip Sentry capture instead of shipping a fatal (HQ-6B). A synchronous
254
- // `write EPIPE` thrown out of console.log lands here rather than on the
255
- // stream 'error' listener above.
256
- if (isEpipe(err)) {
257
- process.exitCode = 0;
258
- }
259
- else if (isInterceptedProcessExit(err)) {
260
- // A security/audit FUZZ harness replaced `process.exit` with a throw so it
261
- // can keep exercising the binary. Commander calling `process.exit` for
262
- // normal CLI control flow (e.g. an unknown command → exit 1) then surfaces
263
- // here as that synthetic marker. It is a test-harness artifact, NOT an
264
- // hq-cli defect — a real user's `process.exit` just exits, so nothing is
265
- // thrown or captured. Skip Sentry capture (no signal, no user-facing
266
- // degradation) and preserve the intended non-zero exit (HQ-CLI-3).
267
- process.exitCode = 1;
268
- }
269
- else if (isCompanySelectionError(err)) {
270
- // The user has multiple (or zero) active company memberships and ran a
271
- // command that needs exactly one without `--company`, or a `--company`
272
- // slug collided across companies. That's an expected, user-actionable
273
- // disambiguation prompt — the message already tells them exactly how to
274
- // proceed (re-run with `--company <slug-or-uid>`) — not an hq-cli defect.
275
- // The CLI can't pick a company for them. Print the actionable message and
276
- // exit non-zero, but skip Sentry capture so a normal "pick a company"
277
- // prompt doesn't flood the tracker with unfixable "crashes" (HQ-CLI-7).
278
- process.stderr.write(`hq: ${err.message}\n`);
279
- process.exitCode = 1;
280
- }
281
- else if (isAuthError(err)) {
282
- // HQ-CLI-8: the vault API returned 401 Unauthorized — the caller's HQ
283
- // session is expired or missing. That's an expected auth state the user
284
- // fixes with `hq login`, not an hq-cli defect. Print the actionable
285
- // message and skip Sentry so an expired login doesn't flood the tracker
286
- // with identical, unfixable "crashes".
287
- process.stderr.write(`hq: ${err.message}\n`);
288
- process.exitCode = 1;
289
- }
290
- else if (isExpectedUserError(err)) {
291
- // HQ-CLI-6: a user-facing, client-caused error (a non-owner running
292
- // `hq integrations approve`, a stale queueId, a bad --args, an unknown
293
- // connection) is the caller's request/state/permission, not an hq-cli
294
- // defect. Print the actionable message and skip Sentry so a correctly-
295
- // denied 4xx doesn't flood the tracker with identical, unfixable crash
296
- // reports. Genuine server (5xx) / unknown failures still capture below.
297
- process.stderr.write(`hq: ${err.message}\n`);
298
- process.exitCode = 1;
299
- }
300
- else {
301
- // A full disk / exhausted quota / read-only filesystem is the user's
302
- // machine, not an HQ code defect. Surface a clear, actionable message and
303
- // skip Sentry capture so one full disk doesn't flood the tracker with
304
- // identical, unfixable crash reports (HQ-CLI-2). Genuine errors still go
305
- // to Sentry and still exit 1.
306
- const envMsg = environmentalFsErrorMessage(err);
307
- if (envMsg) {
308
- process.stderr.write(`hq: ${envMsg}\n`);
309
- }
310
- else {
311
- Sentry.captureException(err);
312
- }
313
- process.exitCode = 1;
314
- }
252
+ handleTopLevelError(err);
315
253
  }
316
254
  finally {
317
255
  // Plan-limit nag (US-016): stderr-only, never throws, never touches
@@ -326,4 +264,89 @@ export async function runCli() {
326
264
  await settleWithin([refreshVersionCache(), Sentry.flush(2000)], RELEASE_HEALTH_SETTLE_TIMEOUT_MS);
327
265
  }
328
266
  }
267
+ const defaultTopLevelErrorDependencies = {
268
+ sentry: Sentry,
269
+ stderr: process.stderr,
270
+ setExitCode: (code) => {
271
+ process.exitCode = code;
272
+ },
273
+ };
274
+ /** Classify a top-level failure without making the CLI process boundary opaque to tests. */
275
+ export function handleTopLevelError(err, deps = defaultTopLevelErrorDependencies) {
276
+ // A broken pipe (EPIPE) means the reader of `hq`'s output closed it early
277
+ // (`hq … | head`, `source <(hq …)`, a parent that exited). That is normal
278
+ // Unix behavior with no user-facing degradation — exit cleanly (0) and
279
+ // skip Sentry capture instead of shipping a fatal (HQ-6B). A synchronous
280
+ // `write EPIPE` thrown out of console.log lands here rather than on the
281
+ // stream 'error' listener above.
282
+ if (isEpipe(err)) {
283
+ deps.setExitCode(0);
284
+ }
285
+ else if (isInterceptedProcessExit(err)) {
286
+ // A security/audit FUZZ harness replaced `process.exit` with a throw so it
287
+ // can keep exercising the binary. Commander calling `process.exit` for
288
+ // normal CLI control flow (e.g. an unknown command → exit 1) then surfaces
289
+ // here as that synthetic marker. It is a test-harness artifact, NOT an
290
+ // hq-cli defect — a real user's `process.exit` just exits, so nothing is
291
+ // thrown or captured. Skip Sentry capture (no signal, no user-facing
292
+ // degradation) and preserve the intended non-zero exit (HQ-CLI-3).
293
+ deps.setExitCode(1);
294
+ }
295
+ else if (isCompanySelectionError(err)) {
296
+ // The user has multiple (or zero) active company memberships and ran a
297
+ // command that needs exactly one without `--company`, or a `--company`
298
+ // slug collided across companies. That's an expected, user-actionable
299
+ // disambiguation prompt — the message already tells them exactly how to
300
+ // proceed (re-run with `--company <slug-or-uid>`) — not an hq-cli defect.
301
+ // The CLI can't pick a company for them. Print the actionable message and
302
+ // exit non-zero, but skip Sentry capture so a normal "pick a company"
303
+ // prompt doesn't flood the tracker with unfixable "crashes" (HQ-CLI-7).
304
+ deps.stderr.write(`hq: ${err.message}\n`);
305
+ deps.setExitCode(1);
306
+ }
307
+ else if (isAuthError(err)) {
308
+ // HQ-CLI-8: the vault API returned 401 Unauthorized — the caller's HQ
309
+ // session is expired or missing. That's an expected auth state the user
310
+ // fixes with `hq login`, not an hq-cli defect. Print the actionable
311
+ // message and skip Sentry so an expired login doesn't flood the tracker
312
+ // with identical, unfixable "crashes".
313
+ deps.stderr.write(`hq: ${err.message}\n`);
314
+ deps.setExitCode(1);
315
+ }
316
+ else if (isExpectedUserError(err)) {
317
+ // HQ-CLI-6: a user-facing, client-caused error (a non-owner running
318
+ // `hq integrations approve`, a stale queueId, a bad --args, an unknown
319
+ // connection) is the caller's request/state/permission, not an hq-cli
320
+ // defect. Print the actionable message and skip Sentry so a correctly-
321
+ // denied 4xx doesn't flood the tracker with identical, unfixable crash
322
+ // reports. Genuine server (5xx) / unknown failures still capture below.
323
+ deps.stderr.write(`hq: ${err.message}\n`);
324
+ deps.setExitCode(1);
325
+ }
326
+ else if (isPackageRootResolutionError(err)) {
327
+ deps.stderr.write(`hq: ${err.message}\n`);
328
+ deps.sentry.captureException(err, {
329
+ contexts: packageRootCaptureContext(err),
330
+ });
331
+ deps.setExitCode(1);
332
+ }
333
+ else {
334
+ // A full disk / exhausted quota / read-only filesystem is the user's
335
+ // machine, not an HQ code defect. Surface a clear, actionable message and
336
+ // skip Sentry capture so one full disk doesn't flood the tracker with
337
+ // identical, unfixable crash reports (HQ-CLI-2). Genuine errors still go
338
+ // to Sentry and still exit 1.
339
+ const envMsg = environmentalFsErrorMessage(err);
340
+ if (envMsg) {
341
+ deps.stderr.write(`hq: ${envMsg}\n`);
342
+ }
343
+ else {
344
+ deps.sentry.captureException(err);
345
+ const userMessage = unexpectedCliErrorMessage(err);
346
+ if (userMessage)
347
+ deps.stderr.write(`hq: ${userMessage}\n`);
348
+ }
349
+ deps.setExitCode(1);
350
+ }
351
+ }
329
352
  //# sourceMappingURL=main.js.map
@@ -24,6 +24,7 @@
24
24
  * `import.meta.url`. A relocated script asking "where is HQ" must get the live
25
25
  * tree, not the package it happens to be bundled in.
26
26
  */
27
+ import * as fs from "fs";
27
28
  /** Where bundled scaffold assets live, relative to the package root. */
28
29
  export declare const BUNDLED_ASSET_DIR: string;
29
30
  /**
@@ -35,6 +36,21 @@ export declare const BUNDLED_ASSET_DIR: string;
35
36
  * checkout without a personal overlay, and a personal-only overlay.
36
37
  */
37
38
  export declare function isHqRoot(dir: string): boolean;
39
+ export type PackageRootFs = Pick<typeof fs, "existsSync" | "readFileSync" | "realpathSync" | "statSync">;
40
+ export type PackageRootResolverOptions = {
41
+ /** Directory containing the currently-running compiled module. */
42
+ moduleDir: string;
43
+ /** Full module path used only in safe diagnostic output. */
44
+ modulePath?: string;
45
+ /** Injectable filesystem seam for resolver tests. */
46
+ fs?: PackageRootFs;
47
+ };
48
+ /**
49
+ * Make a resolver for this package's root. It caches only a successful lookup:
50
+ * a concurrently replaced manifest must be retried by the next command, not
51
+ * converted into a permanent process failure.
52
+ */
53
+ export declare function createPackageRootResolver(options: PackageRootResolverOptions): () => string;
38
54
  /**
39
55
  * This package's installed root — the directory holding its `package.json`.
40
56
  *
@@ -27,6 +27,7 @@
27
27
  import * as fs from "fs";
28
28
  import * as path from "path";
29
29
  import { fileURLToPath } from "url";
30
+ import { PackageRootResolutionError, } from "./package-root-diagnostics.js";
30
31
  /** This package's name, used to identify its root when walking upward. */
31
32
  const CLI_PACKAGE_NAME = "@indigoai-us/hq-cli";
32
33
  /**
@@ -60,54 +61,135 @@ export function isHqRoot(dir) {
60
61
  fs.existsSync(path.join(dir, "core")) ||
61
62
  fs.existsSync(path.join(dir, "personal"))));
62
63
  }
63
- function realpathOrResolve(p) {
64
+ function realpathOrResolve(p, fileSystem = fs) {
64
65
  try {
65
- return fs.realpathSync(p);
66
+ return fileSystem.realpathSync(p);
66
67
  }
67
68
  catch {
68
69
  return path.resolve(p);
69
70
  }
70
71
  }
71
- let cachedPackageRoot;
72
+ function errorCode(err) {
73
+ if (err && typeof err === "object" && "code" in err) {
74
+ const code = err.code;
75
+ return typeof code === "string" ? code : undefined;
76
+ }
77
+ return undefined;
78
+ }
79
+ function failureOutcome(code) {
80
+ if (code === "ENOENT")
81
+ return "no-manifest";
82
+ if (code === "EACCES")
83
+ return "eacces";
84
+ if (code === "EISDIR")
85
+ return "eisdir";
86
+ return "read-error";
87
+ }
72
88
  /**
73
- * This package's installed root the directory holding its `package.json`.
74
- *
75
- * Walks up from the compiled module (`dist/utils/hq-roots.js`) until it finds a
76
- * `package.json` whose name matches, rather than hard-coding `..` hops, so the
77
- * answer survives a change in output layout.
89
+ * Make a resolver for this package's root. It caches only a successful lookup:
90
+ * a concurrently replaced manifest must be retried by the next command, not
91
+ * converted into a permanent process failure.
78
92
  */
79
- export function packageRoot() {
80
- if (cachedPackageRoot === null) {
81
- throw new Error(`Could not locate the ${CLI_PACKAGE_NAME} package root from ${import.meta.url}.`);
82
- }
83
- if (cachedPackageRoot !== undefined)
84
- return cachedPackageRoot;
85
- let dir = path.dirname(fileURLToPath(import.meta.url));
86
- for (;;) {
87
- const manifest = path.join(dir, "package.json");
88
- if (fs.existsSync(manifest)) {
93
+ export function createPackageRootResolver(options) {
94
+ const fileSystem = options.fs ?? fs;
95
+ const modulePath = options.modulePath ?? options.moduleDir;
96
+ let cachedPackageRoot;
97
+ return () => {
98
+ if (cachedPackageRoot !== undefined)
99
+ return cachedPackageRoot;
100
+ const visited = [];
101
+ let dir = options.moduleDir;
102
+ for (;;) {
103
+ const manifest = path.join(dir, "package.json");
89
104
  try {
90
- const parsed = JSON.parse(fs.readFileSync(manifest, "utf8"));
105
+ const parsed = JSON.parse(fileSystem.readFileSync(manifest, "utf8"));
91
106
  if (parsed.name === CLI_PACKAGE_NAME) {
92
- cachedPackageRoot = realpathOrResolve(dir);
107
+ cachedPackageRoot = realpathOrResolve(dir, fileSystem);
93
108
  return cachedPackageRoot;
94
109
  }
110
+ visited.push({
111
+ directory: dir,
112
+ outcome: "name-mismatch",
113
+ manifestName: String(parsed.name),
114
+ });
95
115
  }
96
- catch {
97
- // A malformed package.json on the way up is not our package; keep going.
116
+ catch (err) {
117
+ const code = errorCode(err);
118
+ if (code) {
119
+ visited.push({ directory: dir, outcome: failureOutcome(code), code });
120
+ }
121
+ else {
122
+ visited.push({ directory: dir, outcome: "parse-error" });
123
+ }
98
124
  }
125
+ const parent = path.dirname(dir);
126
+ if (parent === dir)
127
+ break;
128
+ dir = parent;
99
129
  }
100
- const parent = path.dirname(dir);
101
- if (parent === dir)
102
- break;
103
- dir = parent;
104
- }
105
- cachedPackageRoot = null;
106
- throw new Error(`Could not locate the ${CLI_PACKAGE_NAME} package root from ${import.meta.url}.`);
130
+ let fallbackRejectedBecause = "no dist-owner ancestor";
131
+ try {
132
+ const resolvedModuleDir = fileSystem.realpathSync(options.moduleDir);
133
+ let candidate = resolvedModuleDir;
134
+ for (;;) {
135
+ const relativeModulePath = path.relative(candidate, resolvedModuleDir);
136
+ if (relativeModulePath.split(path.sep)[0] === "dist") {
137
+ const assetDir = path.join(candidate, BUNDLED_ASSET_DIR);
138
+ try {
139
+ if (fileSystem.existsSync(assetDir) && fileSystem.statSync(assetDir).isDirectory()) {
140
+ cachedPackageRoot = candidate;
141
+ return cachedPackageRoot;
142
+ }
143
+ fallbackRejectedBecause =
144
+ "deepest dist-owner lacks assets/scaffold";
145
+ }
146
+ catch (err) {
147
+ fallbackRejectedBecause =
148
+ `could not inspect deepest dist-owner assets/scaffold (${errorCode(err) ?? "unknown"})`;
149
+ }
150
+ // Never climb to a shallower dist owner after the nearest owner fails.
151
+ break;
152
+ }
153
+ const parent = path.dirname(candidate);
154
+ if (parent === candidate)
155
+ break;
156
+ candidate = parent;
157
+ }
158
+ }
159
+ catch (err) {
160
+ fallbackRejectedBecause =
161
+ `could not realpath module directory (${errorCode(err) ?? "unknown"})`;
162
+ }
163
+ throw new PackageRootResolutionError({
164
+ modulePath,
165
+ startDir: options.moduleDir,
166
+ strategiesTried: ["manifest-walk", "dist-owner-fallback"],
167
+ visited,
168
+ fallbackRejectedBecause,
169
+ });
170
+ };
171
+ }
172
+ const currentModulePath = fileURLToPath(import.meta.url);
173
+ let defaultPackageRootResolver = createPackageRootResolver({
174
+ moduleDir: path.dirname(currentModulePath),
175
+ modulePath: currentModulePath,
176
+ });
177
+ /**
178
+ * This package's installed root — the directory holding its `package.json`.
179
+ *
180
+ * Walks up from the compiled module (`dist/utils/hq-roots.js`) until it finds a
181
+ * `package.json` whose name matches, rather than hard-coding `..` hops, so the
182
+ * answer survives a change in output layout.
183
+ */
184
+ export function packageRoot() {
185
+ return defaultPackageRootResolver();
107
186
  }
108
187
  /** Reset the memoized package root. Tests only. */
109
188
  export function __resetPackageRootCache() {
110
- cachedPackageRoot = undefined;
189
+ defaultPackageRootResolver = createPackageRootResolver({
190
+ moduleDir: path.dirname(currentModulePath),
191
+ modulePath: currentModulePath,
192
+ });
111
193
  }
112
194
  /**
113
195
  * Resolve the user's live HQ installation.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Bounded diagnostics for failures to locate the CLI package root.
3
+ *
4
+ * These values are sent to Sentry, whose general scrubber deliberately keeps
5
+ * filesystem paths intact. Keep this module the single construction boundary
6
+ * so a malformed install cannot turn an error report into an unbounded or
7
+ * multi-line payload.
8
+ */
9
+ export type PackageRootVisitOutcome = "no-manifest" | "eacces" | "eisdir" | "parse-error" | "name-mismatch" | "read-error" | "omitted";
10
+ export type PackageRootVisit = {
11
+ directory: string;
12
+ outcome: PackageRootVisitOutcome;
13
+ code?: string;
14
+ manifestName?: string;
15
+ };
16
+ export type PackageRootResolutionDiagnostics = {
17
+ modulePath: string;
18
+ startDir: string;
19
+ strategiesTried: string[];
20
+ visited: PackageRootVisit[];
21
+ fallbackRejectedBecause: string;
22
+ };
23
+ type RawDiagnostics = {
24
+ modulePath: string;
25
+ startDir: string;
26
+ strategiesTried: string[];
27
+ visited: PackageRootVisit[];
28
+ fallbackRejectedBecause: string;
29
+ };
30
+ /** Replace every Unicode control character before bounding a diagnostic value. */
31
+ export declare function boundedDiagnosticValue(value: unknown, maxBytes: number): string;
32
+ /** An unmarked packaging fault that should remain visible in Sentry. */
33
+ export declare class PackageRootResolutionError extends Error {
34
+ readonly diagnostics: PackageRootResolutionDiagnostics;
35
+ constructor(raw: RawDiagnostics);
36
+ }
37
+ export declare function isPackageRootResolutionError(err: unknown): err is PackageRootResolutionError;
38
+ /**
39
+ * Produce the exact Sentry contexts shape while enforcing a whole-payload cap.
40
+ * The tail is dropped first because nearest directories contain the useful
41
+ * evidence for a package-root walk.
42
+ */
43
+ export declare function packageRootCaptureContext(err: PackageRootResolutionError): {
44
+ package_root_resolution: PackageRootResolutionDiagnostics;
45
+ };
46
+ export {};
47
+ //# sourceMappingURL=package-root-diagnostics.d.ts.map
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Bounded diagnostics for failures to locate the CLI package root.
3
+ *
4
+ * These values are sent to Sentry, whose general scrubber deliberately keeps
5
+ * filesystem paths intact. Keep this module the single construction boundary
6
+ * so a malformed install cannot turn an error report into an unbounded or
7
+ * multi-line payload.
8
+ */
9
+ import { beforeSend } from "../sentry-before-send.js";
10
+ const CONTROL_CHARACTER = /[\p{C}\p{Zl}\p{Zp}]/gu;
11
+ const REPLACEMENT = "?";
12
+ const ELLIPSIS = "…";
13
+ const MODULE_PATH_BYTES = 256;
14
+ const START_DIR_BYTES = 256;
15
+ const DIRECTORY_BYTES = 256;
16
+ const MANIFEST_NAME_BYTES = 64;
17
+ const CODE_BYTES = 32;
18
+ const MESSAGE_BYTES = 2048;
19
+ const CONTEXT_BYTES = 4096;
20
+ const MAX_VISITED = 12;
21
+ /** Replace every Unicode control character before bounding a diagnostic value. */
22
+ export function boundedDiagnosticValue(value, maxBytes) {
23
+ const normalized = String(value).replace(CONTROL_CHARACTER, REPLACEMENT);
24
+ if (Buffer.byteLength(normalized, "utf8") <= maxBytes)
25
+ return normalized;
26
+ const markerBytes = Buffer.byteLength(ELLIPSIS, "utf8");
27
+ let result = "";
28
+ let used = 0;
29
+ for (const character of normalized) {
30
+ const bytes = Buffer.byteLength(character, "utf8");
31
+ if (used + bytes + markerBytes > maxBytes)
32
+ break;
33
+ result += character;
34
+ used += bytes;
35
+ }
36
+ return result + ELLIPSIS;
37
+ }
38
+ function boundedVisit(visit) {
39
+ return {
40
+ directory: boundedDiagnosticValue(visit.directory, DIRECTORY_BYTES),
41
+ outcome: visit.outcome,
42
+ ...(visit.code === undefined
43
+ ? {}
44
+ : { code: boundedDiagnosticValue(visit.code, CODE_BYTES) }),
45
+ ...(visit.manifestName === undefined
46
+ ? {}
47
+ : { manifestName: boundedDiagnosticValue(visit.manifestName, MANIFEST_NAME_BYTES) }),
48
+ };
49
+ }
50
+ function boundedVisits(visits) {
51
+ if (visits.length <= MAX_VISITED)
52
+ return visits.map(boundedVisit);
53
+ const shown = visits.slice(0, MAX_VISITED - 1).map(boundedVisit);
54
+ shown.push({
55
+ directory: `${visits.length - shown.length} more omitted`,
56
+ outcome: "omitted",
57
+ });
58
+ return shown;
59
+ }
60
+ function normalizeDiagnostics(raw) {
61
+ return {
62
+ modulePath: boundedDiagnosticValue(raw.modulePath, MODULE_PATH_BYTES),
63
+ startDir: boundedDiagnosticValue(raw.startDir, START_DIR_BYTES),
64
+ strategiesTried: raw.strategiesTried.map((strategy) => boundedDiagnosticValue(strategy, CODE_BYTES)),
65
+ visited: boundedVisits(raw.visited),
66
+ fallbackRejectedBecause: boundedDiagnosticValue(raw.fallbackRejectedBecause, DIRECTORY_BYTES),
67
+ };
68
+ }
69
+ function messageFor(diagnostics) {
70
+ return boundedDiagnosticValue("Could not locate the @indigoai-us/hq-cli package root from " +
71
+ `${diagnostics.modulePath}; manifest walk exhausted from ${diagnostics.startDir}; ` +
72
+ `dist-owner fallback ${diagnostics.fallbackRejectedBecause}.`, MESSAGE_BYTES);
73
+ }
74
+ /** An unmarked packaging fault that should remain visible in Sentry. */
75
+ export class PackageRootResolutionError extends Error {
76
+ diagnostics;
77
+ constructor(raw) {
78
+ const diagnostics = normalizeDiagnostics(raw);
79
+ super(messageFor(diagnostics));
80
+ this.name = "PackageRootResolutionError";
81
+ this.diagnostics = diagnostics;
82
+ Object.setPrototypeOf(this, new.target.prototype);
83
+ }
84
+ }
85
+ export function isPackageRootResolutionError(err) {
86
+ return err instanceof PackageRootResolutionError;
87
+ }
88
+ /**
89
+ * Produce the exact Sentry contexts shape while enforcing a whole-payload cap.
90
+ * The tail is dropped first because nearest directories contain the useful
91
+ * evidence for a package-root walk.
92
+ */
93
+ export function packageRootCaptureContext(err) {
94
+ const context = {
95
+ ...err.diagnostics,
96
+ strategiesTried: [...err.diagnostics.strategiesTried],
97
+ visited: err.diagnostics.visited.map((visit) => ({ ...visit })),
98
+ };
99
+ let sentryContext = scrubbedContext(context);
100
+ while (Buffer.byteLength(JSON.stringify({ package_root_resolution: sentryContext }), "utf8") >
101
+ CONTEXT_BYTES &&
102
+ context.visited.length > 0) {
103
+ context.visited.pop();
104
+ sentryContext = scrubbedContext(context);
105
+ }
106
+ return { package_root_resolution: sentryContext };
107
+ }
108
+ /** Apply the repository's canonical credential scrubber before calculating caps. */
109
+ function scrubbedContext(context) {
110
+ const event = {
111
+ type: undefined,
112
+ contexts: { package_root_resolution: context },
113
+ };
114
+ const scrubbed = beforeSend(event, {});
115
+ if (!scrubbed?.contexts?.package_root_resolution) {
116
+ throw new Error("Package root diagnostics were unexpectedly dropped by Sentry scrubbing.");
117
+ }
118
+ return scrubbed.contexts.package_root_resolution;
119
+ }
120
+ //# sourceMappingURL=package-root-diagnostics.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Return a bounded operator-facing diagnostic for unexpected errors whose
3
+ * messages are already part of a user-facing protocol contract. Unknown
4
+ * exceptions remain Sentry-only so local implementation details and secrets
5
+ * are not printed indiscriminately.
6
+ */
7
+ export declare function unexpectedCliErrorMessage(err: unknown): string | null;
8
+ //# sourceMappingURL=unexpected-cli-error.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { IntegrationsCliError } from "../commands/integrations.js";
2
+ /**
3
+ * Return a bounded operator-facing diagnostic for unexpected errors whose
4
+ * messages are already part of a user-facing protocol contract. Unknown
5
+ * exceptions remain Sentry-only so local implementation details and secrets
6
+ * are not printed indiscriminately.
7
+ */
8
+ export function unexpectedCliErrorMessage(err) {
9
+ if (!(err instanceof IntegrationsCliError) || err.expected)
10
+ return null;
11
+ const message = err.message
12
+ .replace(/-----BEGIN[^-]+PRIVATE KEY-----[\s\S]*?-----END[^-]+PRIVATE KEY-----/g, "[REDACTED]")
13
+ .replace(/\p{Cc}/gu, " ")
14
+ .replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "[REDACTED]")
15
+ .replace(/\b(password|secret|client[_-]?secret|api[_-]?key|(?:id|access|refresh)[_-]?token|token|authorization)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s&"'<>;,}]+)/gi, (_match, label) => `${label}=[REDACTED]`)
16
+ .replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]")
17
+ .replace(/\b(?:hqk_|xox[abprs]-|github_pat_|gh[pousr]_)[A-Za-z0-9_-]+\b/gi, "[REDACTED]")
18
+ .replace(/\bAKIA[0-9A-Z]{16}\b/g, "[REDACTED]")
19
+ .replace(/\s+/g, " ")
20
+ .trim()
21
+ .slice(0, 1_000);
22
+ return message || "Integration request failed";
23
+ }
24
+ //# 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.85.0",
3
+ "version": "5.85.2",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -29,7 +29,7 @@
29
29
  "dependencies": {
30
30
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
31
31
  "@aws-sdk/client-s3": "^3.1049.0",
32
- "@indigoai-us/hq-cloud": "^6.14.37",
32
+ "@indigoai-us/hq-cloud": "^6.14.45",
33
33
  "@indigoai-us/hq-onboarding": "^0.1.0",
34
34
  "@sentry/node": "^10.49.0",
35
35
  "better-sqlite3": "^12.11.1",