@indigoai-us/hq-cli 5.98.2 → 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 +11 -0
- package/dist/utils/hook-trust.d.ts +10 -13
- package/dist/utils/hook-trust.js +148 -27
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
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
|
+
|
|
5
16
|
## [5.98.2] — 2026-08-11
|
|
6
17
|
|
|
7
18
|
### Fixed
|
|
@@ -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
|
-
/**
|
|
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
|
package/dist/utils/hook-trust.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn
|
|
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
|
-
|
|
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:
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
/**
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
return { runtime: 'grok', status: 'unchanged', trusted: 0 };
|
|
289
|
+
catch {
|
|
290
|
+
// First install: file does not exist yet.
|
|
275
291
|
}
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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: '
|
|
385
|
+
status: 'skipped',
|
|
286
386
|
trusted: 0,
|
|
287
|
-
reason:
|
|
288
|
-
(detail || `grok-trust.sh exited ${result.status ?? 'unknown'}`),
|
|
387
|
+
reason: 'project .grok bridge sources absent',
|
|
289
388
|
};
|
|
290
389
|
}
|
|
291
|
-
|
|
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.
|
|
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.
|
|
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",
|