@indigoai-us/hq-cli 5.332.0 → 5.333.0

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,11 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.333.0] — 2026-10-03
6
+ - Limit session-start telemetry to 30 events per machine per rolling minute.
7
+ - Start the globally registered HQ hook with BASH_ENV cleared, so an inherited shell profile no longer runs before every hook event.
8
+ - Report non-writable npm prefixes as informational while keeping the repair guidance visible, and attribute npm `EBUSY` update exits with a bounded class.
9
+
5
10
  ## [5.332.0] — 2026-10-02
6
11
  - Match macOS shell behavior in native core commands for text, diagnostics, and file modes.
7
12
  - `hq mcp status` now recognizes the real Claude plugin MCP definition and Codex PATH commands, while global and `hq-anywhere` pack installs share one Codex hook per event and one `hq` MCP server.
@@ -52,7 +52,7 @@ export function hookCommand(hqRoot, event, target = 'master') {
52
52
  const skipInsideRoot = `if [ -d "\${CLAUDE_PROJECT_DIR:-}" ] && [ "\${CLAUDE_PROJECT_DIR}" -ef ${shellQuote(hqRoot)} ]; then exit 0; fi; `;
53
53
  if (target === 'shim')
54
54
  return `${skipInsideRoot}exec /bin/sh ${shellQuote(shimPath(hqRoot))} ${shellQuote(event)} --runtime claude`;
55
- return `${skipInsideRoot}exec bash ${shellQuote(masterHookPath(hqRoot))} ${shellQuote(event)}`;
55
+ return `${skipInsideRoot}BASH_ENV=/dev/null exec bash ${shellQuote(masterHookPath(hqRoot))} ${shellQuote(event)}`;
56
56
  }
57
57
  /** Quote one argument for the POSIX shell used by Claude Code command hooks. */
58
58
  function shellQuote(value) {
@@ -1,6 +1,8 @@
1
1
  import { type FirstSyncStepOutcome } from "../lib/first-sync-next-step.js";
2
2
  export { CLI_FIRST_SYNC_NEXT_STEP_FLAG, FIRST_SYNC_CONFIRMATION_COPY, FIRST_SYNC_NEXT_STEP_COPY, claimFirstSyncStepMarker, firstSyncStepMarkerPaths, firstSyncStepTelemetryProperties, isHumanInteractiveCliSession, isWorkBearingSyncResult, maybeShowFirstSyncNextStep, recordFirstWorkBearingSync, shouldConfirmFirstWorkBearingSync, shouldShowFirstSyncNextStep, takeFirstSyncStepOutcomes, type FirstSyncStepOutcome, } from "../lib/first-sync-next-step.js";
3
3
  export declare const TELEMETRY_TIMEOUT_MS = 1200;
4
+ export declare const CLI_SESSION_STARTED_BUDGET_MAX_EVENTS = 30;
5
+ export declare const CLI_SESSION_STARTED_BUDGET_WINDOW_MS = 60000;
4
6
  /**
5
7
  * Exact stderr line printed once on the first signed-in TTY run that is not
6
8
  * `--json`. Keep this notice and its marker stable for session telemetry;
@@ -9,6 +9,8 @@ import { vaultApiFetch } from "./vault-api.js";
9
9
  import { firstSyncStepTelemetryProperties, isHumanInteractiveCliSession, } from "../lib/first-sync-next-step.js";
10
10
  export { CLI_FIRST_SYNC_NEXT_STEP_FLAG, FIRST_SYNC_CONFIRMATION_COPY, FIRST_SYNC_NEXT_STEP_COPY, claimFirstSyncStepMarker, firstSyncStepMarkerPaths, firstSyncStepTelemetryProperties, isHumanInteractiveCliSession, isWorkBearingSyncResult, maybeShowFirstSyncNextStep, recordFirstWorkBearingSync, shouldConfirmFirstWorkBearingSync, shouldShowFirstSyncNextStep, takeFirstSyncStepOutcomes, } from "../lib/first-sync-next-step.js";
11
11
  export const TELEMETRY_TIMEOUT_MS = 1_200;
12
+ export const CLI_SESSION_STARTED_BUDGET_MAX_EVENTS = 30;
13
+ export const CLI_SESSION_STARTED_BUDGET_WINDOW_MS = 60_000;
12
14
  /**
13
15
  * Exact stderr line printed once on the first signed-in TTY run that is not
14
16
  * `--json`. Keep this notice and its marker stable for session telemetry;
@@ -25,6 +27,8 @@ const DISCLOSURE_MARKER = "cli-telemetry-disclosed";
25
27
  const FIRST_SYNC_DISCLOSURE_MARKER = "cli-telemetry-disclosed-first-sync-v1";
26
28
  const INVITE_NUDGE_DISCLOSURE_MARKER = "cli-telemetry-disclosed-invite-nudge-v1";
27
29
  const FEATURE_USED_DISCLOSURE_MARKER = "cli-telemetry-disclosed-feature-used-v1";
30
+ const SESSION_STARTED_BUDGET_FILE = "cli-session-started-budget.json";
31
+ const SESSION_STARTED_BUDGET_LOCK_STALE_MS = 10_000;
28
32
  let cliSessionStartedPromise;
29
33
  const pendingPlanLimitInviteTeammateExposures = [];
30
34
  const pendingCliInviteNudgeEvents = [];
@@ -236,6 +240,84 @@ function hqConfigDir() {
236
240
  const home = process.env.HOME?.trim() || os.homedir();
237
241
  return path.join(home, ".hq");
238
242
  }
243
+ /**
244
+ * Limit only session-start telemetry per machine. Local state is best effort:
245
+ * damaged or unwritable state must never suppress the event or affect a command.
246
+ */
247
+ function allowCliSessionStartedBudget(now = Date.now()) {
248
+ const stampPath = path.join(hqConfigDir(), SESSION_STARTED_BUDGET_FILE);
249
+ const lockPath = `${stampPath}.lock`;
250
+ let lockFd;
251
+ try {
252
+ lockFd = fs.openSync(lockPath, "wx", 0o600);
253
+ }
254
+ catch (error) {
255
+ if (!isAlreadyExistsError(error)) {
256
+ logSessionStartedBudgetError("acquire", error);
257
+ return true;
258
+ }
259
+ try {
260
+ const lockAgeMs = Date.now() - fs.statSync(lockPath).mtimeMs;
261
+ if (lockAgeMs <= SESSION_STARTED_BUDGET_LOCK_STALE_MS)
262
+ return false;
263
+ fs.unlinkSync(lockPath);
264
+ lockFd = fs.openSync(lockPath, "wx", 0o600);
265
+ }
266
+ catch (retryError) {
267
+ if (isAlreadyExistsError(retryError))
268
+ return false;
269
+ logSessionStartedBudgetError("recover", retryError);
270
+ return true;
271
+ }
272
+ }
273
+ try {
274
+ return recordCliSessionStartedBudget(stampPath, now);
275
+ }
276
+ finally {
277
+ try {
278
+ fs.closeSync(lockFd);
279
+ fs.unlinkSync(lockPath);
280
+ }
281
+ catch (error) {
282
+ logSessionStartedBudgetError("release", error);
283
+ }
284
+ }
285
+ }
286
+ function recordCliSessionStartedBudget(stampPath, now) {
287
+ let recent = [];
288
+ if (fs.existsSync(stampPath)) {
289
+ try {
290
+ const parsed = JSON.parse(fs.readFileSync(stampPath, "utf8"));
291
+ if (Array.isArray(parsed) &&
292
+ parsed.every((value) => typeof value === "number" && Number.isFinite(value))) {
293
+ recent = parsed.filter((timestamp) => timestamp <= now && now - timestamp < CLI_SESSION_STARTED_BUDGET_WINDOW_MS);
294
+ }
295
+ else {
296
+ console.error("hq: Invalid CLI session telemetry budget stamp; allowing telemetry.");
297
+ }
298
+ }
299
+ catch (error) {
300
+ logSessionStartedBudgetError("read", error);
301
+ }
302
+ }
303
+ if (recent.length >= CLI_SESSION_STARTED_BUDGET_MAX_EVENTS)
304
+ return false;
305
+ try {
306
+ fs.writeFileSync(stampPath, JSON.stringify([...recent, now]), { mode: 0o600 });
307
+ }
308
+ catch (error) {
309
+ logSessionStartedBudgetError("write", error);
310
+ return true;
311
+ }
312
+ return true;
313
+ }
314
+ function isAlreadyExistsError(error) {
315
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
316
+ }
317
+ function logSessionStartedBudgetError(operation, error) {
318
+ const errorClass = error instanceof Error ? error.name : "unknown";
319
+ console.error(`hq: Could not ${operation} CLI session telemetry budget stamp (${errorClass}); allowing telemetry.`);
320
+ }
239
321
  export function telemetryDisclosureMarkerPath() {
240
322
  return path.join(hqConfigDir(), DISCLOSURE_MARKER);
241
323
  }
@@ -323,6 +405,8 @@ async function emitCachedCliSessionStarted(timeoutMs) {
323
405
  const token = isMachineIdentity() ? cached.idToken : cached.accessToken;
324
406
  if (!token)
325
407
  return;
408
+ if (!allowCliSessionStartedBudget())
409
+ return;
326
410
  const disclosed = maybeDiscloseCliTelemetry();
327
411
  const consentBasis = disclosed ? "disclosed" : "no-consent";
328
412
  const controller = new AbortController();
@@ -842,6 +842,7 @@ const SELF_UPDATE_ERROR_CLASSES = new Set([
842
842
  "MODULE_NOT_FOUND",
843
843
  "ENOENT",
844
844
  "EACCES",
845
+ "EBUSY",
845
846
  "EEXIST",
846
847
  "E404",
847
848
  "EAI_AGAIN",
@@ -861,6 +862,8 @@ function selfUpdateErrorReason(code, failureKind) {
861
862
  case "EACCES":
862
863
  case "EPERM":
863
864
  return "permission_denied";
865
+ case "EBUSY":
866
+ return "resource_busy";
864
867
  case "EEXIST":
865
868
  return "file_collision";
866
869
  case "E404":
@@ -6,6 +6,7 @@ const fs = require("node:fs");
6
6
  const KNOWN_NPM_ERROR_CODES = new Set([
7
7
  "E404",
8
8
  "EACCES",
9
+ "EBUSY",
9
10
  "EAI_AGAIN",
10
11
  "ECONNREFUSED",
11
12
  "ECONNRESET",
@@ -507,7 +507,7 @@ export function captureNonWritableNpmPrefixNotice(input) {
507
507
  if (!claimed)
508
508
  return Promise.resolve();
509
509
  Sentry.captureMessage("Self-update deferred: npm prefix is not writable", {
510
- level: "warning",
510
+ level: "info",
511
511
  fingerprint: ["self-update", "npm-prefix-not-writable"],
512
512
  tags: {
513
513
  from_version: input.fromVersion,
@@ -754,6 +754,7 @@ const INSTALL_DETAIL_MAX_CHARS = 400;
754
754
  const KNOWN_NPM_UPDATE_ERROR_CODES = new Set([
755
755
  "E404",
756
756
  "EACCES",
757
+ "EBUSY",
757
758
  "EAI_AGAIN",
758
759
  "ECONNREFUSED",
759
760
  "ECONNRESET",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.332.0",
3
+ "version": "5.333.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {