@indigoai-us/hq-cli 5.98.1 → 5.98.3

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,37 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.98.3] — 2026-08-11
6
+
7
+ ### Fixed
8
+
9
+ - Session-log capture now records Claude/Codex/Grok transcripts even when the
10
+ HQ root is reached through a symlink or case-aliased path (for example,
11
+ `/Users/x/HQ` versus its realpath): `hq reindex` matches the recorded working
12
+ directory against both the lexical root and its realpath. Delivered by bumping
13
+ the bundled `@indigoai-us/hq-cloud` runtime to 6.14.49
14
+ (indigoai-us/hq-cloud#298).
15
+
16
+ ## [5.98.2] — 2026-08-11
17
+
18
+ ### Fixed
19
+
20
+ - `hq core hq-status-summary` — and through it the `/handoff` status summary —
21
+ no longer crashes, or files a Sentry report, on a mature HQ root (HQ-CLI-N,
22
+ Sentry 7664842324). The shared `command()` helper behind the native `hq core`
23
+ utilities ran `spawnSync` with no `maxBuffer`, so every child inherited Node's
24
+ 1 MiB default. `hq-status-summary` shells out to `git status --porcelain
25
+ --ignored`, whose output exceeds 1 MiB on any HQ root carrying the usual
26
+ individually-ignored files alongside tracked content; Node then killed git with
27
+ SIGTERM, handed back truncated stdout, and surfaced `Error: spawnSync git
28
+ ENOBUFS`, failing the handoff (observed once, from a single user). The helper
29
+ now caps captured output at 512 MiB — the same ceiling the repo already uses
30
+ for git reads — matching the bundled shell oracle, which redirects git straight
31
+ to a temp file and has no ceiling at all. Spawn errors stay fatal, so a
32
+ truncated status is never reported as a real summary, and an ENOBUFS above the
33
+ new ceiling now throws an error naming the command, its args, and the cap
34
+ instead of the opaque original.
35
+
5
36
  ## [5.98.1] — 2026-08-10
6
37
 
7
38
  ### Fixed
@@ -11,4 +11,5 @@
11
11
  * non-interactively.
12
12
  */
13
13
  import "../node-preflight.js";
14
+ import "../node-network-compat.js";
14
15
  //# sourceMappingURL=hq-auth-refresh.d.ts.map
@@ -13,6 +13,7 @@
13
13
  // MUST be first: guard the Node version before any dependency that needs a
14
14
  // Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
15
15
  import "../node-preflight.js";
16
+ import "../node-network-compat.js";
16
17
  import { initSentry, Sentry } from "../sentry.js";
17
18
  import { refreshCachedSession } from "../utils/cognito-session.js";
18
19
  initSentry();
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import "./node-preflight.js";
3
+ import "./node-network-compat.js";
3
4
  declare function isVersionRequest(argv: readonly string[]): boolean;
4
5
  export declare const __test__: {
5
6
  isVersionRequest: typeof isVersionRequest;
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // MUST be first: guard the Node version before any dependency that needs a
3
3
  // Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
4
4
  import "./node-preflight.js";
5
+ import "./node-network-compat.js";
5
6
  import { CLI_VERSION } from "./cli-version.js";
6
7
  function isVersionRequest(argv) {
7
8
  const args = argv.slice(2);
@@ -9,10 +9,34 @@ export function ioFor(io = {}) {
9
9
  export function line(write, value) {
10
10
  write(`${value}\n`);
11
11
  }
12
+ /**
13
+ * Ceiling for a single child's captured stdout, mirroring the git-read cap the
14
+ * repo already uses (src/utils/large-file-guard.ts:48). spawnSync defaults
15
+ * maxBuffer to 1 MiB; once a child's output crosses that, Node kills it with
16
+ * SIGTERM and hands back TRUNCATED stdout plus an ENOBUFS error. `hq core
17
+ * hq-status-summary` runs `git status --porcelain --ignored`, whose output
18
+ * exceeds 1 MiB on any mature HQ root (the individually-ignored files that sit
19
+ * beside tracked content), so the default turned a routine /handoff status
20
+ * summary into a crash. The bundled shell oracle this module ports
21
+ * (assets/scaffold/core/scripts/hq-status-summary.sh:108) redirects git straight
22
+ * to a temp file and has no ceiling at all; an explicit, generous cap matches it.
23
+ */
24
+ const MAX_SPAWN_BUFFER = 512 * 1024 * 1024;
12
25
  export function command(command, args, cwd) {
13
- const result = spawnSync(command, args, { cwd, encoding: "utf8" });
14
- if (result.error)
15
- throw result.error;
26
+ const result = spawnSync(command, args, { cwd, encoding: "utf8", maxBuffer: MAX_SPAWN_BUFFER });
27
+ // Spawn errors stay fatal: a truncated child result must never be reported as
28
+ // a real one — that would silently under-count a handoff's status instead of
29
+ // failing loudly. Above the new ceiling ENOBUFS is still theoretically
30
+ // reachable, so wrap it to name the command, its args and the cap; a future
31
+ // recurrence then arrives in Sentry self-diagnosed instead of as the opaque
32
+ // "spawnSync <cmd> ENOBUFS".
33
+ if (result.error) {
34
+ const error = result.error;
35
+ if (error.code === "ENOBUFS") {
36
+ throw new Error(`spawnSync ${command} ${args.join(" ")} exceeded the ${MAX_SPAWN_BUFFER}-byte output ceiling`, { cause: error });
37
+ }
38
+ throw error;
39
+ }
16
40
  return { status: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
17
41
  }
18
42
  export function exists(pathname) {
package/dist/main.d.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
5
  import "./node-preflight.js";
6
+ import "./node-network-compat.js";
6
7
  import { Sentry } from "./sentry.js";
7
8
  export declare function runCli(): Promise<void>;
8
9
  export type TopLevelErrorDependencies = {
package/dist/main.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // MUST be first: guard the Node version before any dependency that needs a
6
6
  // Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
7
7
  import "./node-preflight.js";
8
+ import "./node-network-compat.js";
8
9
  import { Command } from "commander";
9
10
  import { initSentry, Sentry } from "./sentry.js";
10
11
  import { registerAddCommand } from "./commands/add.js";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Node network defaults for HQ CLI entry points.
3
+ *
4
+ * Node's default 250 ms address-family attempt window can fail on hosts where
5
+ * IPv6 is advertised but black-holed. On affected WSL installations, undici's
6
+ * fetch() reaches ETIMEDOUT before its usable IPv4 path completes. A 1 second
7
+ * window fixes the connection while retaining Node's automatic family choice.
8
+ *
9
+ * Keep this module dependency-free and import it immediately after the Node
10
+ * version preflight. Operators can retain a custom value through NODE_OPTIONS
11
+ * or a direct Node CLI option; HQ only supplies the compatibility default when
12
+ * neither is present.
13
+ *
14
+ * Use a namespace import of `node:net` (not a static named import of
15
+ * `setDefaultAutoSelectFamilyAttemptTimeout`). Named ESM imports are linked
16
+ * before any entry-module body runs — including `node-preflight` — so importing
17
+ * a Node 18.13+/20.4+ API by name would turn unsupported runtimes into a
18
+ * module-link SyntaxError instead of the preflight's upgrade message.
19
+ */
20
+ export declare const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
21
+ export interface NodeNetworkCompatibilityOptions {
22
+ nodeOptions?: string;
23
+ execArgv?: readonly string[];
24
+ /**
25
+ * Inject the net timeout setter (tests). Pass `null` to simulate a runtime
26
+ * that lacks `setDefaultAutoSelectFamilyAttemptTimeout`.
27
+ */
28
+ setAttemptTimeout?: ((milliseconds: number) => void) | null;
29
+ }
30
+ export declare function configureNodeNetworkCompatibility(options?: NodeNetworkCompatibilityOptions): boolean;
31
+ //# sourceMappingURL=node-network-compat.d.ts.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Node network defaults for HQ CLI entry points.
3
+ *
4
+ * Node's default 250 ms address-family attempt window can fail on hosts where
5
+ * IPv6 is advertised but black-holed. On affected WSL installations, undici's
6
+ * fetch() reaches ETIMEDOUT before its usable IPv4 path completes. A 1 second
7
+ * window fixes the connection while retaining Node's automatic family choice.
8
+ *
9
+ * Keep this module dependency-free and import it immediately after the Node
10
+ * version preflight. Operators can retain a custom value through NODE_OPTIONS
11
+ * or a direct Node CLI option; HQ only supplies the compatibility default when
12
+ * neither is present.
13
+ *
14
+ * Use a namespace import of `node:net` (not a static named import of
15
+ * `setDefaultAutoSelectFamilyAttemptTimeout`). Named ESM imports are linked
16
+ * before any entry-module body runs — including `node-preflight` — so importing
17
+ * a Node 18.13+/20.4+ API by name would turn unsupported runtimes into a
18
+ * module-link SyntaxError instead of the preflight's upgrade message.
19
+ */
20
+ import * as nodeNet from "node:net";
21
+ export const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
22
+ const ATTEMPT_TIMEOUT_OPTION = "--network-family-autoselection-attempt-timeout";
23
+ function hasExplicitAttemptTimeout(nodeOptions, execArgv) {
24
+ const optionPattern = new RegExp(`(?:^|\\s)${ATTEMPT_TIMEOUT_OPTION}(?:=|\\s|$)`);
25
+ return (optionPattern.test(nodeOptions) ||
26
+ execArgv.some((argument) => argument === ATTEMPT_TIMEOUT_OPTION ||
27
+ argument.startsWith(`${ATTEMPT_TIMEOUT_OPTION}=`)));
28
+ }
29
+ function resolveSetAttemptTimeout(override) {
30
+ if (override === null)
31
+ return undefined;
32
+ if (override)
33
+ return override;
34
+ const fn = nodeNet.setDefaultAutoSelectFamilyAttemptTimeout;
35
+ return typeof fn === "function" ? fn.bind(nodeNet) : undefined;
36
+ }
37
+ export function configureNodeNetworkCompatibility(options = {}) {
38
+ const nodeOptions = options.nodeOptions ?? process.env.NODE_OPTIONS ?? "";
39
+ const execArgv = options.execArgv ?? process.execArgv;
40
+ if (hasExplicitAttemptTimeout(nodeOptions, execArgv)) {
41
+ return false;
42
+ }
43
+ const setAttemptTimeout = resolveSetAttemptTimeout(options.setAttemptTimeout);
44
+ if (!setAttemptTimeout) {
45
+ // Runtime lacks the API (pre-Node 18.13 / preflight should already have exited).
46
+ return false;
47
+ }
48
+ setAttemptTimeout(HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS);
49
+ return true;
50
+ }
51
+ configureNodeNetworkCompatibility();
52
+ //# sourceMappingURL=node-network-compat.js.map
@@ -2,19 +2,8 @@ export interface CodexRpcClient {
2
2
  request(method: string, params: unknown): Promise<unknown>;
3
3
  close(): Promise<void>;
4
4
  }
5
- type SpawnSyncLike = (command: string, args: string[], options: {
6
- cwd: string;
7
- encoding: 'utf8';
8
- stdio: 'pipe';
9
- }) => {
10
- status: number | null;
11
- stdout?: string | Buffer | null;
12
- stderr?: string | Buffer | null;
13
- error?: Error;
14
- };
15
5
  export interface HookTrustDependencies {
16
6
  createCodexClient: (cwd: string) => Promise<CodexRpcClient>;
17
- spawnSync: SpawnSyncLike;
18
7
  homeDir?: () => string;
19
8
  }
20
9
  export interface RuntimeHookTrustResult {
@@ -27,9 +16,17 @@ export interface RuntimeHookTrustResult {
27
16
  export declare function createCodexAppServerClient(cwd: string, executable?: string, args?: string[]): Promise<CodexRpcClient>;
28
17
  /** Trust only hooks declared by this HQ root's project `.codex/` layer. */
29
18
  export declare function trustCodexProjectHooks(hqRoot: string, deps?: HookTrustDependencies): Promise<RuntimeHookTrustResult>;
30
- /** Grok trusts hooks at folder scope; the shipped installer also refreshes the HQ bridge. */
19
+ /**
20
+ * Grok trusts hooks at folder scope, and on observed builds project
21
+ * .grok/hooks often never load, so the user-global bridge under ~/.grok/hooks
22
+ * is the reliable dispatch path. This installer (ported from the retired
23
+ * core/scripts/grok-trust.sh) converges all four pieces:
24
+ * 1. modern folder trust (~/.grok/trusted_folders.toml)
25
+ * 2. legacy trusted-hook-projects entry
26
+ * 3. the user-global bridge copied from <hqRoot>/.grok/hooks/ (all events)
27
+ * 4. [compat.claude] hooks=false in ~/.grok/config.toml
28
+ */
31
29
  export declare function trustGrokProjectHooks(hqRoot: string, deps?: HookTrustDependencies): RuntimeHookTrustResult;
32
30
  /** Converge hook trust without turning an absent runtime into a reindex failure. */
33
31
  export declare function trustHqRuntimeHooks(hqRoot: string, deps?: HookTrustDependencies): Promise<RuntimeHookTrustResult[]>;
34
- export {};
35
32
  //# sourceMappingURL=hook-trust.d.ts.map
@@ -1,4 +1,4 @@
1
- import { spawn, spawnSync as nodeSpawnSync } from 'node:child_process';
1
+ import { spawn } from 'node:child_process';
2
2
  import * as fs from 'node:fs';
3
3
  import * as os from 'node:os';
4
4
  import * as path from 'node:path';
@@ -154,7 +154,6 @@ export async function createCodexAppServerClient(cwd, executable = 'codex', args
154
154
  }
155
155
  const DEFAULT_DEPS = {
156
156
  createCodexClient: createCodexAppServerClient,
157
- spawnSync: nodeSpawnSync,
158
157
  homeDir: () => process.env.HOME ?? os.homedir(),
159
158
  };
160
159
  /** Trust only hooks declared by this HQ root's project `.codex/` layer. */
@@ -183,14 +182,17 @@ export async function trustCodexProjectHooks(hqRoot, deps = DEFAULT_DEPS) {
183
182
  reason: 'no HQ project hooks discovered',
184
183
  };
185
184
  }
186
- const pending = projectHooks.filter((hook) => hook.trustStatus === 'untrusted' || hook.trustStatus === 'modified');
185
+ // Converge every HQ project hook to trusted AND enabled. A hook that is
186
+ // already trusted but was toggled off would otherwise silently stay
187
+ // disabled forever — reindex is the convergence point, so it re-enables.
188
+ const pending = projectHooks.filter((hook) => hook.trustStatus === 'untrusted' || hook.trustStatus === 'modified' || !hook.enabled);
187
189
  if (pending.length === 0) {
188
190
  return { runtime: 'codex', status: 'unchanged', trusted: 0 };
189
191
  }
190
192
  const state = Object.fromEntries(pending.map((hook) => [
191
193
  hook.key,
192
194
  {
193
- enabled: hook.enabled,
195
+ enabled: true,
194
196
  trusted_hash: hook.currentHash,
195
197
  },
196
198
  ]));
@@ -201,7 +203,10 @@ export async function trustCodexProjectHooks(hqRoot, deps = DEFAULT_DEPS) {
201
203
  const verified = hooksFromListResponse(await client.request('hooks/list', { cwds: [hqRoot] }));
202
204
  const verifiedByKey = new Map(verified.hooks.map((hook) => [hook.key, hook]));
203
205
  const stillPending = pending
204
- .filter((hook) => verifiedByKey.get(hook.key)?.trustStatus !== 'trusted')
206
+ .filter((hook) => {
207
+ const after = verifiedByKey.get(hook.key);
208
+ return after?.trustStatus !== 'trusted' || after.enabled !== true;
209
+ })
205
210
  .map((hook) => hook.key);
206
211
  if (verified.errors.length > 0 || stillPending.length > 0) {
207
212
  return {
@@ -238,9 +243,15 @@ function filesMatch(left, right) {
238
243
  return false;
239
244
  }
240
245
  }
246
+ /** Escape a value for use inside a TOML double-quoted (basic) string key. */
247
+ function tomlEscapeKey(value) {
248
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
249
+ }
241
250
  function grokTrustIsCurrent(hqRoot, home) {
242
251
  const grokHome = path.join(home, '.grok');
243
- const rootPattern = regexEscape(hqRoot);
252
+ // The key is written TOML-escaped, so match the escaped form. On POSIX paths
253
+ // (no backslash/quote) this is identical to the raw path.
254
+ const rootPattern = regexEscape(tomlEscapeKey(hqRoot));
244
255
  let modernTrust;
245
256
  let legacyTrust;
246
257
  let config;
@@ -252,9 +263,11 @@ function grokTrustIsCurrent(hqRoot, home) {
252
263
  catch {
253
264
  return false;
254
265
  }
255
- const folderTrusted = new RegExp(`^\\[folders\\."${rootPattern}"\\]\\s*\\n(?:(?!\\[)[^\\n]*\\n)*trusted\\s*=\\s*true\\b`, 'm').test(modernTrust);
266
+ // `[^[]` spans any line ending (LF or CRLF) and EOF, so these matchers are
267
+ // newline-agnostic: header line, then the block body up to the next table.
268
+ const folderTrusted = new RegExp(`\\[folders\\."${rootPattern}"\\][^[]*?trusted\\s*=\\s*true\\b`).test(modernTrust);
256
269
  const legacyTrusted = legacyTrust.split(/\r?\n/).includes(hqRoot);
257
- const claudeCompatQuiet = /^\[compat\.claude\]\s*\n(?:(?!\[)[^\n]*\n)*hooks\s*=\s*false\b/m.test(config);
270
+ const claudeCompatQuiet = /\[compat\.claude\][^[]*?hooks\s*=\s*false\b/.test(config);
258
271
  const sourceHooks = path.join(hqRoot, '.grok', 'hooks');
259
272
  const userHooks = path.join(grokHome, 'hooks');
260
273
  return (folderTrusted &&
@@ -263,32 +276,140 @@ function grokTrustIsCurrent(hqRoot, home) {
263
276
  filesMatch(path.join(sourceHooks, 'hq-grok-user-bridge.sh'), path.join(userHooks, 'hq-hq-bridge.sh')) &&
264
277
  filesMatch(path.join(sourceHooks, 'hq-grok-user-bridge.json'), path.join(userHooks, 'hq-hq-bridge.json')));
265
278
  }
266
- /** Grok trusts hooks at folder scope; the shipped installer also refreshes the HQ bridge. */
267
- export function trustGrokProjectHooks(hqRoot, deps = DEFAULT_DEPS) {
268
- const script = path.join(hqRoot, 'core', 'scripts', 'grok-trust.sh');
269
- if (!fs.existsSync(script)) {
270
- return { runtime: 'grok', status: 'skipped', trusted: 0, reason: 'grok-trust.sh absent' };
279
+ /**
280
+ * Upsert the `[folders."<hqRoot>"]` trust block in ~/.grok/trusted_folders.toml.
281
+ * Mirrors the retired core/scripts/grok-trust.sh writer: replace an existing
282
+ * block for this root in place, else append one.
283
+ */
284
+ function writeGrokFolderTrust(trustTomlPath, hqRoot) {
285
+ let text = '';
286
+ try {
287
+ text = fs.readFileSync(trustTomlPath, 'utf8');
271
288
  }
272
- const home = deps.homeDir?.() ?? process.env.HOME ?? os.homedir();
273
- if (grokTrustIsCurrent(hqRoot, home)) {
274
- return { runtime: 'grok', status: 'unchanged', trusted: 0 };
289
+ catch {
290
+ // First install: file does not exist yet.
275
291
  }
276
- const result = deps.spawnSync('bash', [script], {
277
- cwd: hqRoot,
278
- encoding: 'utf8',
279
- stdio: 'pipe',
280
- });
281
- if (result.error || result.status !== 0) {
282
- const detail = String(result.stderr ?? '').trim().split('\n')[0];
292
+ const decidedAt = Math.floor(Date.now() / 1000);
293
+ const key = tomlEscapeKey(hqRoot);
294
+ const block = `[folders."${key}"]\ntrusted = true\ndecided_at = ${decidedAt}\n`;
295
+ // Consume the header AND the whole block body (any line endings, and a final
296
+ // property with no trailing newline) so replacement never leaves a stray
297
+ // decided_at behind. `[^[]` matches everything up to the next table / EOF.
298
+ const existing = new RegExp(`\\[folders\\."${regexEscape(key)}"\\][^[]*`);
299
+ if (existing.test(text)) {
300
+ text = text.replace(existing, block);
301
+ if (!text.endsWith('\n'))
302
+ text += '\n';
303
+ }
304
+ else {
305
+ if (text && !text.endsWith('\n'))
306
+ text += '\n';
307
+ if (text && !text.endsWith('\n\n'))
308
+ text += '\n';
309
+ text += block;
310
+ }
311
+ fs.writeFileSync(trustTomlPath, text);
312
+ }
313
+ /** Append the HQ root to the legacy trusted-hook-projects list if missing. */
314
+ function writeGrokLegacyTrust(legacyPath, hqRoot) {
315
+ let text = '';
316
+ try {
317
+ text = fs.readFileSync(legacyPath, 'utf8');
318
+ }
319
+ catch {
320
+ // First install.
321
+ }
322
+ if (text.split(/\r?\n/).includes(hqRoot))
323
+ return;
324
+ if (text && !text.endsWith('\n'))
325
+ text += '\n';
326
+ fs.writeFileSync(legacyPath, `${text}${hqRoot}\n`);
327
+ }
328
+ /**
329
+ * Set `[compat.claude] hooks = false` in ~/.grok/config.toml so Grok does not
330
+ * ALSO load every project .claude/settings.json hook (double-running guards and
331
+ * flooding the TUI). HQ policy still runs via bridge -> adapter -> hook-gate.
332
+ */
333
+ function quietGrokClaudeCompat(configPath) {
334
+ let text = '';
335
+ try {
336
+ text = fs.readFileSync(configPath, 'utf8');
337
+ }
338
+ catch {
339
+ // First install.
340
+ }
341
+ // Newline-agnostic (LF/CRLF/EOF): the section is its header plus everything up
342
+ // to the next table declaration.
343
+ const section = /\[compat\.claude\][^[]*/;
344
+ const match = text.match(section);
345
+ if (match) {
346
+ let body = match[0];
347
+ if (/^hooks[^\S\r\n]*=/m.test(body)) {
348
+ body = body.replace(/^hooks[^\S\r\n]*=[^\r\n]*/m, 'hooks = false');
349
+ }
350
+ else {
351
+ body = `${body.replace(/\s+$/, '')}\nhooks = false\n`;
352
+ }
353
+ text = text.replace(section, body);
354
+ }
355
+ else {
356
+ if (text && !text.endsWith('\n'))
357
+ text += '\n';
358
+ if (text && !text.endsWith('\n\n'))
359
+ text += '\n';
360
+ text +=
361
+ '# HQ reindex: Grok enforces via hq-hq-bridge -> adapter -> .claude/hooks.\n' +
362
+ '# Do not also load every .claude/settings.json hook (double work + noisy UI).\n' +
363
+ '[compat.claude]\nhooks = false\n';
364
+ }
365
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
366
+ fs.writeFileSync(configPath, text);
367
+ }
368
+ /**
369
+ * Grok trusts hooks at folder scope, and on observed builds project
370
+ * .grok/hooks often never load, so the user-global bridge under ~/.grok/hooks
371
+ * is the reliable dispatch path. This installer (ported from the retired
372
+ * core/scripts/grok-trust.sh) converges all four pieces:
373
+ * 1. modern folder trust (~/.grok/trusted_folders.toml)
374
+ * 2. legacy trusted-hook-projects entry
375
+ * 3. the user-global bridge copied from <hqRoot>/.grok/hooks/ (all events)
376
+ * 4. [compat.claude] hooks=false in ~/.grok/config.toml
377
+ */
378
+ export function trustGrokProjectHooks(hqRoot, deps = DEFAULT_DEPS) {
379
+ const sourceHooks = path.join(hqRoot, '.grok', 'hooks');
380
+ const sourceBridgeSh = path.join(sourceHooks, 'hq-grok-user-bridge.sh');
381
+ const sourceBridgeJson = path.join(sourceHooks, 'hq-grok-user-bridge.json');
382
+ if (!fs.existsSync(sourceBridgeSh) || !fs.existsSync(sourceBridgeJson)) {
283
383
  return {
284
384
  runtime: 'grok',
285
- status: 'failed',
385
+ status: 'skipped',
286
386
  trusted: 0,
287
- reason: result.error?.message ??
288
- (detail || `grok-trust.sh exited ${result.status ?? 'unknown'}`),
387
+ reason: 'project .grok bridge sources absent',
289
388
  };
290
389
  }
291
- return { runtime: 'grok', status: 'trusted', trusted: 1 };
390
+ const home = deps.homeDir?.() ?? process.env.HOME ?? os.homedir();
391
+ if (grokTrustIsCurrent(hqRoot, home)) {
392
+ return { runtime: 'grok', status: 'unchanged', trusted: 0 };
393
+ }
394
+ try {
395
+ const grokHome = path.join(home, '.grok');
396
+ const userHooks = path.join(grokHome, 'hooks');
397
+ fs.mkdirSync(userHooks, { recursive: true });
398
+ writeGrokFolderTrust(path.join(grokHome, 'trusted_folders.toml'), hqRoot);
399
+ writeGrokLegacyTrust(path.join(grokHome, 'trusted-hook-projects'), hqRoot);
400
+ // Quiet Claude compat BEFORE publishing the bridge. If the config write
401
+ // fails (e.g. read-only), the bridge is not yet active, so we never leave
402
+ // the bridge running while `.claude/settings.json` hooks also load — the
403
+ // double-execution this convergence is meant to prevent.
404
+ quietGrokClaudeCompat(path.join(grokHome, 'config.toml'));
405
+ fs.copyFileSync(sourceBridgeSh, path.join(userHooks, 'hq-hq-bridge.sh'));
406
+ fs.chmodSync(path.join(userHooks, 'hq-hq-bridge.sh'), 0o755);
407
+ fs.copyFileSync(sourceBridgeJson, path.join(userHooks, 'hq-hq-bridge.json'));
408
+ return { runtime: 'grok', status: 'trusted', trusted: 1 };
409
+ }
410
+ catch (error) {
411
+ return { runtime: 'grok', status: 'failed', trusted: 0, reason: errorMessage(error) };
412
+ }
292
413
  }
293
414
  /** Converge hook trust without turning an absent runtime into a reindex failure. */
294
415
  export async function trustHqRuntimeHooks(hqRoot, deps = DEFAULT_DEPS) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.98.1",
3
+ "version": "5.98.3",
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.47",
32
+ "@indigoai-us/hq-cloud": "^6.14.49",
33
33
  "@indigoai-us/hq-onboarding": "^0.1.0",
34
34
  "@sentry/node": "^10.49.0",
35
35
  "@tobilu/qmd": "2.5.3",