@indigoai-us/hq-cli 5.345.147 → 5.345.149
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 +65 -0
- package/README.md +2 -0
- package/assets/bot-workers/setup/worker.yaml +11 -0
- package/dist/command-catalog.generated.d.ts +341 -11
- package/dist/command-catalog.generated.js +421 -11
- package/dist/command-help.js +2 -1
- package/dist/commands/access.d.ts +45 -2
- package/dist/commands/access.js +537 -223
- package/dist/commands/agent-inbox.js +2 -1
- package/dist/commands/billing-usage.d.ts +33 -2
- package/dist/commands/billing-usage.js +75 -4
- package/dist/commands/bot.js +2 -2
- package/dist/commands/channels.d.ts +27 -0
- package/dist/commands/channels.js +108 -4
- package/dist/commands/cloud.js +31 -1
- package/dist/commands/core-refresh-vault-access-native.d.ts +33 -0
- package/dist/commands/core-refresh-vault-access-native.js +356 -28
- package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
- package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
- package/dist/commands/core.js +9 -0
- package/dist/commands/daemon.d.ts +7 -0
- package/dist/commands/daemon.js +37 -3
- package/dist/commands/dm.js +18 -3
- package/dist/commands/doctor.d.ts +11 -1
- package/dist/commands/doctor.js +56 -1
- package/dist/commands/install-global.d.ts +8 -8
- package/dist/commands/install-global.js +139 -61
- package/dist/commands/lanes-shared.d.ts +3 -1
- package/dist/commands/lanes.d.ts +4 -4
- package/dist/commands/lanes.js +380 -25
- package/dist/commands/library-add.d.ts +31 -1
- package/dist/commands/library-add.js +69 -2
- package/dist/commands/library.js +37 -3
- package/dist/commands/pack-install.d.ts +2 -2
- package/dist/commands/pack-install.js +133 -35
- package/dist/commands/reindex.js +19 -0
- package/dist/commands/sync-manifest.d.ts +2 -9
- package/dist/commands/sync-manifest.js +4 -15
- package/dist/hook-engine-path.d.ts +4 -0
- package/dist/hook-engine-path.js +43 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2783 -2354
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +76 -5
- package/dist/lazy-commands.js +2 -0
- package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
- package/dist/lib/agent-kit/inbox-redact.js +361 -0
- package/dist/lib/agent-kit/inbox-state.js +11 -2
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/agent-kit/run/inbox.js +36 -7
- package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
- package/dist/lib/anywhere-runtime-flag-key.js +9 -0
- package/dist/lib/billing/usage-cost.d.ts +31 -1
- package/dist/lib/billing/usage-cost.js +46 -0
- package/dist/lib/company-skill-wrapper.js +6 -4
- package/dist/lib/daemon/install.d.ts +47 -1
- package/dist/lib/daemon/install.js +181 -15
- package/dist/lib/daemon/lock.d.ts +32 -0
- package/dist/lib/daemon/lock.js +55 -2
- package/dist/lib/db/local.d.ts +2 -2
- package/dist/lib/db/local.js +2 -2
- package/dist/lib/db/sql.js +2 -2
- package/dist/lib/db/sqlite-binding.d.ts +22 -0
- package/dist/lib/db/sqlite-binding.js +168 -0
- package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +15 -0
- package/dist/lib/doctor/checks/anywhere.js +77 -11
- package/dist/lib/doctor/checks/cli-path.d.ts +12 -0
- package/dist/lib/doctor/checks/cli-path.js +46 -0
- package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
- package/dist/lib/doctor/checks/hook-engine.js +17 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
- package/dist/lib/doctor/checks/sync-health.js +38 -2
- package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
- package/dist/lib/doctor/checks/temp-homes.js +54 -0
- package/dist/lib/doctor/fix/apply.d.ts +90 -1
- package/dist/lib/doctor/fix/apply.js +176 -10
- package/dist/lib/doctor/fix/json-output.d.ts +4 -1
- package/dist/lib/doctor/fix/json-output.js +6 -0
- package/dist/lib/doctor/fix/remediation.d.ts +7 -1
- package/dist/lib/doctor/fix/remediation.js +23 -1
- package/dist/lib/doctor/registry.js +10 -0
- package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
- package/dist/lib/doctor/temp-home-sweeper.js +418 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/flag-registry.js +9 -1
- package/dist/lib/flag-snapshot-file.js +8 -3
- package/dist/lib/hook-engine/reconcile.d.ts +9 -0
- package/dist/lib/hook-engine/reconcile.js +134 -0
- package/dist/lib/hook-engine/registration.d.ts +17 -0
- package/dist/lib/hook-engine/registration.js +50 -0
- package/dist/lib/hook-engine/resolve.d.ts +31 -0
- package/dist/lib/hook-engine/resolve.js +134 -0
- package/dist/lib/install/claude-targets.d.ts +2 -1
- package/dist/lib/install/claude-targets.js +8 -3
- package/dist/lib/install/codex-targets.d.ts +3 -0
- package/dist/lib/install/codex-targets.js +15 -4
- package/dist/lib/install/hook-target.d.ts +9 -0
- package/dist/lib/install/hook-target.js +39 -0
- package/dist/lib/lanes/admission-host.d.ts +1 -0
- package/dist/lib/lanes/admission-host.js +8 -1
- package/dist/lib/lanes/admission.d.ts +4 -0
- package/dist/lib/lanes/admission.js +6 -2
- package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
- package/dist/lib/lanes/index.d.ts +2 -2
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/keep-alive.js +4 -1
- package/dist/lib/lanes/local-host-identity.d.ts +40 -0
- package/dist/lib/lanes/local-host-identity.js +314 -0
- package/dist/lib/lanes/reaper.d.ts +1 -1
- package/dist/lib/lanes/reaper.js +7 -0
- package/dist/lib/lanes/reconcile.d.ts +4 -0
- package/dist/lib/lanes/reconcile.js +16 -8
- package/dist/lib/lanes/spawn.d.ts +20 -0
- package/dist/lib/lanes/spawn.js +48 -2
- package/dist/lib/lanes/status-fields.js +6 -1
- package/dist/lib/lanes/store.js +8 -1
- package/dist/lib/lanes/throttle.d.ts +49 -6
- package/dist/lib/lanes/throttle.js +154 -17
- package/dist/lib/lanes/types.d.ts +22 -1
- package/dist/lib/lanes/types.js +27 -3
- package/dist/lib/lanes/watcher.js +27 -2
- package/dist/lib/lanes/worker-continuity.d.ts +2 -0
- package/dist/lib/lanes/worker-continuity.js +18 -11
- package/dist/lib/manifest-disabled.d.ts +13 -0
- package/dist/lib/manifest-disabled.js +19 -0
- package/dist/lib/service-manager/launchd.d.ts +1 -0
- package/dist/lib/service-manager/launchd.js +1 -1
- package/dist/lib/windows/console-encoding.d.ts +24 -0
- package/dist/lib/windows/console-encoding.js +83 -0
- package/dist/lib/windows/install-followup.d.ts +35 -0
- package/dist/lib/windows/install-followup.js +142 -0
- package/dist/lib/windows/user-path.d.ts +125 -0
- package/dist/lib/windows/user-path.js +275 -0
- package/dist/lib/work-context/auto-bind.d.ts +1 -1
- package/dist/lib/work-context/auto-bind.js +22 -3
- package/dist/lib/work-context/lane-bind.d.ts +31 -0
- package/dist/lib/work-context/lane-bind.js +101 -0
- package/dist/lib/work-context/organize.js +1 -0
- package/dist/lib/work-context/prd-sync.js +7 -0
- package/dist/lib/work-context/project.d.ts +36 -10
- package/dist/lib/work-context/project.js +130 -41
- package/dist/lib/work-context/reconcile.d.ts +6 -0
- package/dist/lib/work-context/reconcile.js +25 -5
- package/dist/lib/work-context/state.d.ts +6 -0
- package/dist/lib/work-context/state.js +29 -1
- package/dist/main.js +2 -1
- package/dist/unknown-command.d.ts +18 -0
- package/dist/unknown-command.js +88 -0
- package/dist/utils/cognito-session.d.ts +8 -0
- package/dist/utils/cognito-session.js +14 -2
- package/dist/utils/copy-tree.d.ts +61 -0
- package/dist/utils/copy-tree.js +249 -0
- package/dist/utils/environmental-error.js +7 -3
- package/dist/utils/hook-trust.d.ts +9 -0
- package/dist/utils/hook-trust.js +30 -2
- package/dist/utils/npm-invocation.d.ts +27 -0
- package/dist/utils/npm-invocation.js +54 -0
- package/dist/utils/qmd-native-binding-error.js +3 -1
- package/dist/utils/resolve-executable.d.ts +2 -0
- package/dist/utils/resolve-executable.js +6 -2
- package/dist/utils/safe-link.d.ts +50 -0
- package/dist/utils/safe-link.js +300 -0
- package/package.json +17 -5
- package/scripts/hook-engine-manifest.mjs +22 -0
- package/scripts/install-hook-engine.mjs +10 -0
|
@@ -12,6 +12,7 @@ import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.gener
|
|
|
12
12
|
import { resolveFlagSnapshotCompanyUid, resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
|
|
13
13
|
import { registerFlagRegistryRefreshWaiter } from "./flag-registry-refresh-wait.js";
|
|
14
14
|
import { flagSnapshotWriteFailureTags, readDefaultOnFlagSnapshot, readFlagSnapshotFile, writePersonFlagSnapshotFile, writeFlagSnapshotFile, } from "./flag-snapshot-file.js";
|
|
15
|
+
import { freshAnywhereRuntimeFlags } from "./anywhere-runtime-flag-key.js";
|
|
15
16
|
function captureFlagRegistryException(error, tags, sentry) {
|
|
16
17
|
if (!sentry)
|
|
17
18
|
return;
|
|
@@ -42,6 +43,7 @@ function normalizedCompanyUid(value) {
|
|
|
42
43
|
return COMPANY_UID_RE.test(candidate) ? candidate : undefined;
|
|
43
44
|
}
|
|
44
45
|
let heldSnapshot = null;
|
|
46
|
+
let heldSnapshotFetchedAt = 0;
|
|
45
47
|
let processReader = null;
|
|
46
48
|
let refreshCompletion = Promise.resolve();
|
|
47
49
|
let completeRefresh = null;
|
|
@@ -113,7 +115,10 @@ function localOverrideOrFallback(flagKey, legacyFallback, env = process.env) {
|
|
|
113
115
|
return processLocalOverride(flagKey, env) ?? legacyFallback();
|
|
114
116
|
}
|
|
115
117
|
/** Create the same snapshot-backed reader used by process gates. */
|
|
116
|
-
export function createProcessSnapshotReader(companyIdentifiers, getSnapshot = () => heldSnapshot
|
|
118
|
+
export function createProcessSnapshotReader(companyIdentifiers, getSnapshot = () => heldSnapshot && ({
|
|
119
|
+
...heldSnapshot,
|
|
120
|
+
flags: freshAnywhereRuntimeFlags(heldSnapshot.flags, heldSnapshotFetchedAt),
|
|
121
|
+
}), env = process.env) {
|
|
117
122
|
return {
|
|
118
123
|
isEnabled(flagKey, lookup = {}) {
|
|
119
124
|
return resolveFlag({
|
|
@@ -169,6 +174,7 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
169
174
|
: null;
|
|
170
175
|
if (persistedSnapshot) {
|
|
171
176
|
heldSnapshot = { version: 0, flags: persistedSnapshot.flags };
|
|
177
|
+
heldSnapshotFetchedAt = Date.parse(persistedSnapshot.fetched_at);
|
|
172
178
|
}
|
|
173
179
|
refreshCompletion = Promise.resolve();
|
|
174
180
|
completeRefresh = null;
|
|
@@ -234,6 +240,7 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
234
240
|
worker.on("message", (value) => {
|
|
235
241
|
if (isFlagSnapshot(value)) {
|
|
236
242
|
heldSnapshot = value;
|
|
243
|
+
heldSnapshotFetchedAt = Date.now();
|
|
237
244
|
if (completeRefresh)
|
|
238
245
|
refreshPendingSnapshot = value;
|
|
239
246
|
if (refreshCompanyUid && refreshPendingSnapshot) {
|
|
@@ -392,6 +399,7 @@ export function currentFlagSnapshot() {
|
|
|
392
399
|
/** Test-only reset for process-global readiness state. */
|
|
393
400
|
export function _resetFlagRegistryForTests() {
|
|
394
401
|
heldSnapshot = null;
|
|
402
|
+
heldSnapshotFetchedAt = 0;
|
|
395
403
|
processReader = null;
|
|
396
404
|
refreshCompletion = Promise.resolve();
|
|
397
405
|
completeRefresh = null;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { freshAnywhereRuntimeFlags, HQ_ANYWHERE_RUNTIME_FLAG, HQ_ANYWHERE_RUNTIME_FALSE_MAX_AGE_MS } from "./anywhere-runtime-flag-key.js";
|
|
5
6
|
export const FLAG_SNAPSHOT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
6
7
|
const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
|
|
7
8
|
function markWriteFailure(error, context) {
|
|
@@ -263,7 +264,7 @@ export function readPersonFlagSnapshotFile(options = {}) {
|
|
|
263
264
|
continue;
|
|
264
265
|
if (file === fallback && fallback)
|
|
265
266
|
fallbackCacheUsed = true;
|
|
266
|
-
return { scope: "person", fetched_at: value.fetched_at, flags: flags };
|
|
267
|
+
return { scope: "person", fetched_at: value.fetched_at, flags: freshAnywhereRuntimeFlags(flags, fetchedAt, options.now) };
|
|
267
268
|
}
|
|
268
269
|
return null;
|
|
269
270
|
}
|
|
@@ -348,7 +349,7 @@ function parseSnapshotFile(companyUid, file, options) {
|
|
|
348
349
|
return {
|
|
349
350
|
company_uid: companyUid,
|
|
350
351
|
fetched_at: value.fetched_at,
|
|
351
|
-
flags: flags,
|
|
352
|
+
flags: freshAnywhereRuntimeFlags(flags, fetchedAt, options.now),
|
|
352
353
|
};
|
|
353
354
|
}
|
|
354
355
|
/** Return null for every miss, malformed file, company mismatch, or stale file. */
|
|
@@ -374,7 +375,11 @@ export function readFlagSnapshotFile(companyUid, options = {}) {
|
|
|
374
375
|
}
|
|
375
376
|
/** Resolve a default-on kill switch from offline snapshots, allowing stale decisions. */
|
|
376
377
|
export function readDefaultOnFlagSnapshot(flagKey, companyUid, env = process.env) {
|
|
377
|
-
|
|
378
|
+
// Native-route kill switches intentionally retain stale decisions. Only
|
|
379
|
+
// Anywhere has the GA one-hour disable limit; do not change other gates.
|
|
380
|
+
const options = { env, maxAgeMs: flagKey === HQ_ANYWHERE_RUNTIME_FLAG
|
|
381
|
+
? HQ_ANYWHERE_RUNTIME_FALSE_MAX_AGE_MS
|
|
382
|
+
: Number.POSITIVE_INFINITY };
|
|
378
383
|
if (companyUid)
|
|
379
384
|
return readFlagSnapshotFile(companyUid, options)?.flags[flagKey] !== false;
|
|
380
385
|
const directories = new Set([path.dirname(personFlagSnapshotPath(env))]);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ReconcileResult {
|
|
2
|
+
/** Codex user-scope trust keys for the HQ entries in the global hooks.json. */
|
|
3
|
+
codexUserKeys: string[];
|
|
4
|
+
/** One `<file>: <reason>` line per registration file left unchanged by an error. */
|
|
5
|
+
failures: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Reconcile existing registrations only; reindex must not install a runtime. */
|
|
8
|
+
export declare function reconcileHookRegistrations(root: string, home?: string, env?: NodeJS.ProcessEnv, daemonInstalled?: boolean): ReconcileResult;
|
|
9
|
+
//# sourceMappingURL=reconcile.d.ts.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { writeConfigAtomic, jsonFormat } from '../../commands/mcp-registration.js';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { addHqHooks, removeHqHooks } from '../install/claude-targets.js';
|
|
6
|
+
import { hookTargetFor } from '../install/hook-target.js';
|
|
7
|
+
import { addCodexHooks, removeCodexHooks, codexHookEntry, codexHookTrustKeys } from '../install/codex-targets.js';
|
|
8
|
+
import { engineArgs, isNativeEntry, registrationEngine } from './registration.js';
|
|
9
|
+
/** The project master-hook command shipped in HQ's .claude/settings.json. */
|
|
10
|
+
function projectClaudeCommand(event) {
|
|
11
|
+
return 'env BASH_ENV=/dev/null bash "$CLAUDE_PROJECT_DIR/.claude/hooks/master-hook.sh" ' + event;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Rewrite matching entries where they sit. Groups, order, foreign entries and
|
|
15
|
+
* extra fields such as timeout are kept, so a flag-on then flag-off pass
|
|
16
|
+
* restores the original file.
|
|
17
|
+
*/
|
|
18
|
+
function rewriteEntries(doc, convert) {
|
|
19
|
+
let changed = false;
|
|
20
|
+
const hooks = {};
|
|
21
|
+
for (const [event, groups] of Object.entries(doc.hooks)) {
|
|
22
|
+
hooks[event] = !Array.isArray(groups) ? groups : groups.map((group) => {
|
|
23
|
+
if (!Array.isArray(group?.hooks))
|
|
24
|
+
return group;
|
|
25
|
+
return { ...group, hooks: group.hooks.map((entry) => {
|
|
26
|
+
const next = entry && typeof entry === 'object' ? convert(entry, event) : undefined;
|
|
27
|
+
if (!next)
|
|
28
|
+
return entry;
|
|
29
|
+
changed = true;
|
|
30
|
+
return next;
|
|
31
|
+
}) };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return changed ? { ...doc, hooks } : doc;
|
|
35
|
+
}
|
|
36
|
+
/** Reconcile existing registrations only; reindex must not install a runtime. */
|
|
37
|
+
export function reconcileHookRegistrations(root, home = homedir(), env = process.env, daemonInstalled) {
|
|
38
|
+
const result = { codexUserKeys: [], failures: [] };
|
|
39
|
+
const engine = registrationEngine(home, env);
|
|
40
|
+
// Flag-off rollback restores the same bash target install would choose (the
|
|
41
|
+
// hqd shim when the daemon serves this root). Resolved only when a
|
|
42
|
+
// registration is rewritten, so the default-off path reads no daemon state.
|
|
43
|
+
let target;
|
|
44
|
+
const globalTarget = () => (target ??= engine ? { engine } : hookTargetFor(root, home, daemonInstalled, env));
|
|
45
|
+
const codexBash = (event) => [codexHookEntry(root, event, 'master').command, codexHookEntry(root, event, 'shim').command];
|
|
46
|
+
for (const [file, runtime, global] of [
|
|
47
|
+
[join(root, '.claude/settings.json'), 'claude', false],
|
|
48
|
+
[join(env.CLAUDE_CONFIG_DIR || join(home, '.claude'), 'settings.json'), 'claude', true],
|
|
49
|
+
[join(env.CODEX_HOME || join(home, '.codex'), 'hooks.json'), 'codex', true],
|
|
50
|
+
[join(root, '.codex/hooks.json'), 'codex', false],
|
|
51
|
+
]) {
|
|
52
|
+
// Each file stands alone: an unreadable or locked project file must not
|
|
53
|
+
// undo or hide the trust keys of a global file already rewritten.
|
|
54
|
+
try {
|
|
55
|
+
if (!existsSync(file))
|
|
56
|
+
continue;
|
|
57
|
+
// Default-off must not acquire a write lock or create backups for a legacy
|
|
58
|
+
// registration. The merge below repeats this check under the lock when a
|
|
59
|
+
// native entry was found, so a concurrent writer still wins safely.
|
|
60
|
+
if (!engine) {
|
|
61
|
+
let current;
|
|
62
|
+
// A file we cannot parse holds no native entry of ours; leave it untouched.
|
|
63
|
+
try {
|
|
64
|
+
current = jsonFormat.parse(readFileSync(file, 'utf8'));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const entries = current?.hooks && typeof current.hooks === 'object'
|
|
70
|
+
? Object.values(current.hooks).flatMap(groups => Array.isArray(groups) ? groups.flatMap(g => Array.isArray(g?.hooks) ? g.hooks : []) : [])
|
|
71
|
+
: [];
|
|
72
|
+
if (!entries.some(e => e && isNativeEntry(e, root)))
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
let expected;
|
|
76
|
+
let codexKeys;
|
|
77
|
+
writeConfigAtomic({ target: file, pack: 'hq-hook-engine', format: {
|
|
78
|
+
...jsonFormat,
|
|
79
|
+
assertEntry: (doc) => {
|
|
80
|
+
if (expected !== undefined && JSON.stringify(doc) !== expected)
|
|
81
|
+
throw new Error('Hook registration verification failed');
|
|
82
|
+
},
|
|
83
|
+
}, env: { home }, assertName: 'hooks', merge: (doc) => {
|
|
84
|
+
if (!doc || typeof doc !== 'object' || !doc.hooks || typeof doc.hooks !== 'object')
|
|
85
|
+
return doc;
|
|
86
|
+
const entries = Object.values(doc.hooks).flatMap(groups => Array.isArray(groups) ? groups.flatMap(g => Array.isArray(g?.hooks) ? g.hooks : []) : []);
|
|
87
|
+
if (!engine && !entries.some(e => e && isNativeEntry(e, root)))
|
|
88
|
+
return doc;
|
|
89
|
+
if (global && (runtime === 'codex' ? removeCodexHooks(doc, root) : removeHqHooks(doc, root)) === doc)
|
|
90
|
+
return doc;
|
|
91
|
+
let next;
|
|
92
|
+
if (global)
|
|
93
|
+
next = runtime === 'codex' ? addCodexHooks(doc, root, globalTarget()) : addHqHooks(doc, root, globalTarget());
|
|
94
|
+
else if (runtime === 'codex') {
|
|
95
|
+
// Only an entry install itself writes is converted. HQ's own project
|
|
96
|
+
// file routes through hook-gate.sh and has none, so it stays as is.
|
|
97
|
+
next = rewriteEntries(doc, (entry, event) => {
|
|
98
|
+
if (!isNativeEntry(entry, root) && !codexBash(event).includes(entry.command))
|
|
99
|
+
return undefined;
|
|
100
|
+
const { command: _command, commandWindows: _windows, ...rest } = entry;
|
|
101
|
+
const { command, commandWindows } = codexHookEntry(root, event, globalTarget());
|
|
102
|
+
return { ...rest, command, ...(commandWindows ? { commandWindows } : {}) };
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// Only the exact shipped master-hook command is converted; a wrapped
|
|
107
|
+
// or extended invocation is someone's edit and is left alone.
|
|
108
|
+
next = rewriteEntries(doc, (entry, event) => {
|
|
109
|
+
if (!isNativeEntry(entry, root) && entry.command !== projectClaudeCommand(event))
|
|
110
|
+
return undefined;
|
|
111
|
+
const { args, commandWindows: _windows, ...rest } = entry;
|
|
112
|
+
if (engine)
|
|
113
|
+
return { ...rest, command: engine, args: engineArgs(root, event, 'claude') };
|
|
114
|
+
return { ...rest, command: projectClaudeCommand(Array.isArray(args) && typeof args[0] === 'string' ? args[0] : event) };
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
expected = JSON.stringify(next);
|
|
118
|
+
if (runtime === 'codex' && global)
|
|
119
|
+
codexKeys = codexHookTrustKeys(next, file, root);
|
|
120
|
+
return next;
|
|
121
|
+
} });
|
|
122
|
+
// Keys are positions in the file. Only once the write and its verify have
|
|
123
|
+
// succeeded do they point at HQ's entries; a failed write leaves the old
|
|
124
|
+
// file, where the same position can hold a foreign hook.
|
|
125
|
+
if (codexKeys)
|
|
126
|
+
result.codexUserKeys = codexKeys;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
result.failures.push(`${file}: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=reconcile.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function launcherEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
2
|
+
export declare function registrationEngine(home: string, env?: NodeJS.ProcessEnv): string | null;
|
|
3
|
+
export interface NativeTarget {
|
|
4
|
+
engine: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Root spelling passed to the launcher. A trailing separator is dropped because
|
|
8
|
+
* Windows PowerShell 5.1 re-quotes a native argument containing a space as
|
|
9
|
+
* "...", and a final backslash would then escape the closing quote.
|
|
10
|
+
*/
|
|
11
|
+
export declare function launcherRoot(root: string): string;
|
|
12
|
+
export declare function engineArgs(root: string, event: string, runtime: 'claude' | 'codex', global?: boolean): string[];
|
|
13
|
+
/** Recognize only our pinned executable with the exact root argument. */
|
|
14
|
+
export declare function isNativeEntry(entry: Record<string, unknown>, root: string): boolean;
|
|
15
|
+
export declare function quotePosix(value: string): string;
|
|
16
|
+
export declare function quoteWindows(value: string): string;
|
|
17
|
+
//# sourceMappingURL=registration.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Offline, default-off registration gate. No network work on the hook path. */
|
|
2
|
+
import { readFlagSnapshotFile, readPersonFlagSnapshotFile } from '../flag-snapshot-file.js';
|
|
3
|
+
import { resolveProcessFlagCompanyContext } from '../process-flag-company-context.js';
|
|
4
|
+
import { installHookEngine } from './resolve.js';
|
|
5
|
+
export function launcherEnabled(env = process.env) {
|
|
6
|
+
const { companyUid: uid, companySlug } = resolveProcessFlagCompanyContext(env);
|
|
7
|
+
if (!uid && companySlug)
|
|
8
|
+
return false;
|
|
9
|
+
const snapshot = uid ? readFlagSnapshotFile(uid, { env }) : readPersonFlagSnapshotFile({ env });
|
|
10
|
+
return snapshot?.flags['hook-engine-launcher'] === true;
|
|
11
|
+
}
|
|
12
|
+
export function registrationEngine(home, env = process.env) {
|
|
13
|
+
if (!launcherEnabled({ ...env, HOME: home, USERPROFILE: home }))
|
|
14
|
+
return null;
|
|
15
|
+
return installHookEngine({ home });
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Root spelling passed to the launcher. A trailing separator is dropped because
|
|
19
|
+
* Windows PowerShell 5.1 re-quotes a native argument containing a space as
|
|
20
|
+
* "...", and a final backslash would then escape the closing quote.
|
|
21
|
+
*/
|
|
22
|
+
export function launcherRoot(root) {
|
|
23
|
+
const trimmed = root.replace(/[\\/]+$/, '');
|
|
24
|
+
return trimmed === '' || /^[A-Za-z]:$/.test(trimmed) ? root : trimmed;
|
|
25
|
+
}
|
|
26
|
+
export function engineArgs(root, event, runtime, global = false) {
|
|
27
|
+
return [event, '--hq-root', launcherRoot(root), '--runtime', runtime, ...(global && runtime === 'claude' ? ['--global'] : [])];
|
|
28
|
+
}
|
|
29
|
+
/** Recognize only our pinned executable with the exact root argument. */
|
|
30
|
+
export function isNativeEntry(entry, root) {
|
|
31
|
+
if (typeof entry.command !== 'string')
|
|
32
|
+
return false;
|
|
33
|
+
const pinned = /[\\/]\.hq[\\/]bin[\\/]hq-hook[\\/]\d+\.\d+\.\d+[^\\/]*[\\/]/;
|
|
34
|
+
if (!pinned.test(entry.command))
|
|
35
|
+
return false;
|
|
36
|
+
const spelling = launcherRoot(root);
|
|
37
|
+
if (Array.isArray(entry.args)) {
|
|
38
|
+
const i = entry.args.indexOf('--hq-root');
|
|
39
|
+
return i >= 0 && entry.args[i + 1] === spelling;
|
|
40
|
+
}
|
|
41
|
+
return entry.command.includes(quotePosix('--hq-root') + ' ' + quotePosix(spelling));
|
|
42
|
+
}
|
|
43
|
+
export function quotePosix(value) { return "'" + value.replace(/'/g, "'\\''") + "'"; }
|
|
44
|
+
export function quoteWindows(value) {
|
|
45
|
+
// Codex 0.162.1 uses its Windows PowerShell runtime shell for hooks.
|
|
46
|
+
// Single-quoted literals prevent variable, subexpression and wildcard expansion.
|
|
47
|
+
// PowerShell also closes them on the typographic quotes U+2018-U+201B.
|
|
48
|
+
return "'" + value.replace(/['‘’‚‛]/g, '$&$&') + "'";
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=registration.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const HOOK_ENGINE_REPAIR = "npm install -g @indigoai-us/hq-cli --include=optional";
|
|
2
|
+
export declare const HOOK_ENGINE_REPAIR_PIN = "hq core hook-engine-path --repair";
|
|
3
|
+
export interface EngineOptions {
|
|
4
|
+
home?: string;
|
|
5
|
+
platform?: string;
|
|
6
|
+
arch?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
resolvePackage?: (name: string) => string;
|
|
9
|
+
repair?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface EnginePackage {
|
|
12
|
+
source: string;
|
|
13
|
+
pinned: string;
|
|
14
|
+
version: string;
|
|
15
|
+
sourceDirectory: string;
|
|
16
|
+
pinnedDirectory: string;
|
|
17
|
+
}
|
|
18
|
+
/** US-020 must set this package capability when the replaced bash hooks retire. */
|
|
19
|
+
export declare function hookEngineRequired(): boolean;
|
|
20
|
+
/** Check the whole signed bundle, including its resource seal and stapled ticket. */
|
|
21
|
+
export declare function pinnedHookEngineMatches(engine: EnginePackage): boolean;
|
|
22
|
+
/** Offline lookup. Never silently use an engine from a different CLI version. */
|
|
23
|
+
export declare function findHookEngine(options?: EngineOptions): EnginePackage | null;
|
|
24
|
+
/** Never truncate or replace a running Windows executable. */
|
|
25
|
+
export declare function installHookEngine(options?: EngineOptions): string | null;
|
|
26
|
+
/** Call only after settings no longer reference these old versions. Windows
|
|
27
|
+
* refuses unlink of a mapped executable; a busy version stays for a later pass.
|
|
28
|
+
* POSIX permits unlink while running, so retain those versions conservatively.
|
|
29
|
+
*/
|
|
30
|
+
export declare function pruneHookEngines(current: string, referencedVersions: readonly string[], options?: EngineOptions): string[];
|
|
31
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, rmdirSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
// Capture metadata while the package-use lease protects this install tree.
|
|
7
|
+
// Resident commands may call doctor after an update removes the old manifest.
|
|
8
|
+
const cliManifest = JSON.parse(readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'));
|
|
9
|
+
export const HOOK_ENGINE_REPAIR = 'npm install -g @indigoai-us/hq-cli --include=optional';
|
|
10
|
+
export const HOOK_ENGINE_REPAIR_PIN = 'hq core hook-engine-path --repair';
|
|
11
|
+
const platforms = new Set(['win32-x64', 'win32-arm64', 'darwin-x64', 'darwin-arm64', 'linux-x64', 'linux-arm64']);
|
|
12
|
+
/** US-020 must set this package capability when the replaced bash hooks retire. */
|
|
13
|
+
export function hookEngineRequired() {
|
|
14
|
+
return cliManifest.hqHookEngineRequired === true;
|
|
15
|
+
}
|
|
16
|
+
/** Check the whole signed bundle, including its resource seal and stapled ticket. */
|
|
17
|
+
export function pinnedHookEngineMatches(engine) {
|
|
18
|
+
function equal(source, pinned) {
|
|
19
|
+
if (!existsSync(pinned))
|
|
20
|
+
return false;
|
|
21
|
+
const a = lstatSync(source);
|
|
22
|
+
const b = lstatSync(pinned);
|
|
23
|
+
if (a.isSymbolicLink())
|
|
24
|
+
return b.isSymbolicLink() && readlinkSync(source) === readlinkSync(pinned);
|
|
25
|
+
if (a.isDirectory()) {
|
|
26
|
+
if (!b.isDirectory())
|
|
27
|
+
return false;
|
|
28
|
+
const names = readdirSync(source).sort();
|
|
29
|
+
if (JSON.stringify(names) !== JSON.stringify(readdirSync(pinned).sort()))
|
|
30
|
+
return false;
|
|
31
|
+
return names.every(name => equal(join(source, name), join(pinned, name)));
|
|
32
|
+
}
|
|
33
|
+
return a.isFile() && b.isFile() && readFileSync(source).equals(readFileSync(pinned));
|
|
34
|
+
}
|
|
35
|
+
return equal(engine.sourceDirectory, engine.pinnedDirectory);
|
|
36
|
+
}
|
|
37
|
+
/** Offline lookup. Never silently use an engine from a different CLI version. */
|
|
38
|
+
export function findHookEngine(options = {}) {
|
|
39
|
+
const platform = options.platform ?? process.platform;
|
|
40
|
+
const key = `${platform}-${options.arch ?? process.arch}`;
|
|
41
|
+
if (!platforms.has(key))
|
|
42
|
+
return null;
|
|
43
|
+
const version = options.version ?? cliManifest.version;
|
|
44
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
|
|
45
|
+
throw new Error('Invalid hook engine version');
|
|
46
|
+
let manifest;
|
|
47
|
+
try {
|
|
48
|
+
manifest = (options.resolvePackage ?? require.resolve)(`@indigoai-us/hq-hook-${key}/package.json`);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === 'MODULE_NOT_FOUND')
|
|
52
|
+
return null;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
const pkg = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
56
|
+
if (pkg.version !== version)
|
|
57
|
+
return null;
|
|
58
|
+
const binary = platform === 'win32' ? 'hq-hook.exe' : platform === 'darwin' ? join('hq-hook.app', 'Contents', 'MacOS', 'hq-hook') : 'hq-hook';
|
|
59
|
+
const sourceDirectory = join(dirname(manifest), 'bin');
|
|
60
|
+
const pinnedDirectory = join(options.home ?? homedir(), '.hq', 'bin', 'hq-hook', version);
|
|
61
|
+
const source = join(sourceDirectory, binary);
|
|
62
|
+
if (!existsSync(source))
|
|
63
|
+
return null;
|
|
64
|
+
return { source, version, sourceDirectory, pinnedDirectory, pinned: join(pinnedDirectory, binary) };
|
|
65
|
+
}
|
|
66
|
+
/** Never truncate or replace a running Windows executable. */
|
|
67
|
+
export function installHookEngine(options = {}) {
|
|
68
|
+
const engine = findHookEngine(options);
|
|
69
|
+
if (!engine)
|
|
70
|
+
return null;
|
|
71
|
+
if (options.repair && existsSync(engine.pinnedDirectory)) {
|
|
72
|
+
// Unlink the executable first: Windows refuses this if any process maps it.
|
|
73
|
+
// Never recursively remove the bundle before that lock check has succeeded.
|
|
74
|
+
try {
|
|
75
|
+
unlinkSync(engine.pinned);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code !== 'ENOENT')
|
|
79
|
+
throw new Error('Close running HQ hook processes before repairing the pinned engine', { cause: error });
|
|
80
|
+
}
|
|
81
|
+
rmSync(engine.pinnedDirectory, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
mkdirSync(dirname(engine.pinnedDirectory), { recursive: true });
|
|
84
|
+
if (!existsSync(engine.pinnedDirectory)) {
|
|
85
|
+
const stage = mkdtempSync(join(dirname(engine.pinnedDirectory), '.install-'));
|
|
86
|
+
try {
|
|
87
|
+
cpSync(engine.sourceDirectory, stage, { recursive: true, errorOnExist: true, force: false });
|
|
88
|
+
try {
|
|
89
|
+
renameSync(stage, engine.pinnedDirectory);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (!existsSync(engine.pinned))
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
rmSync(stage, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!pinnedHookEngineMatches(engine)) {
|
|
101
|
+
throw new Error(`Hook engine ${engine.version} differs from its pinned copy; close HQ sessions and repair with: ${HOOK_ENGINE_REPAIR_PIN}`);
|
|
102
|
+
}
|
|
103
|
+
return engine.pinned;
|
|
104
|
+
}
|
|
105
|
+
/** Call only after settings no longer reference these old versions. Windows
|
|
106
|
+
* refuses unlink of a mapped executable; a busy version stays for a later pass.
|
|
107
|
+
* POSIX permits unlink while running, so retain those versions conservatively.
|
|
108
|
+
*/
|
|
109
|
+
export function pruneHookEngines(current, referencedVersions, options = {}) {
|
|
110
|
+
if ((options.platform ?? process.platform) !== 'win32')
|
|
111
|
+
return [];
|
|
112
|
+
const root = join(options.home ?? homedir(), '.hq', 'bin', 'hq-hook');
|
|
113
|
+
if (!existsSync(root))
|
|
114
|
+
return [];
|
|
115
|
+
const removed = [];
|
|
116
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
117
|
+
if (!entry.isDirectory() || entry.name === current || referencedVersions.includes(entry.name) || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(entry.name))
|
|
118
|
+
continue;
|
|
119
|
+
const dir = join(root, entry.name);
|
|
120
|
+
if (readdirSync(dir).some(name => name !== 'hq-hook.exe'))
|
|
121
|
+
continue;
|
|
122
|
+
try {
|
|
123
|
+
unlinkSync(join(dir, 'hq-hook.exe'));
|
|
124
|
+
rmdirSync(dir);
|
|
125
|
+
removed.push(entry.name);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (!['EPERM', 'EACCES', 'EBUSY', 'ENOENT', 'ENOTEMPTY'].includes(error.code ?? ''))
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return removed;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type NativeTarget } from '../hook-engine/registration.js';
|
|
1
2
|
/** Name of the HQ MCP server entry in ~/.claude.json. */
|
|
2
3
|
export declare const HQ_MCP_SERVER_NAME = "hq";
|
|
3
4
|
/** `_hqPack` provenance stamped on the HQ MCP server entry. */
|
|
@@ -21,7 +22,7 @@ export declare function masterHookPath(hqRoot: string): string;
|
|
|
21
22
|
/** hqd hook shim (US-010): registered instead of master-hook.sh once hqd is installed. */
|
|
22
23
|
export declare function shimPath(hqRoot: string): string;
|
|
23
24
|
/** Which HQ hook script the user-scope install registers. */
|
|
24
|
-
export type HookTarget = 'master' | 'shim';
|
|
25
|
+
export type HookTarget = 'master' | 'shim' | NativeTarget;
|
|
25
26
|
export declare function charterPath(hqRoot: string): string;
|
|
26
27
|
export declare function hqSkillsSource(hqRoot: string): string;
|
|
27
28
|
/** The exact hook command registered for one event (absolute path, quoted). */
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* safe-write substrate.
|
|
6
6
|
*/
|
|
7
7
|
import * as path from 'path';
|
|
8
|
+
import { engineArgs, isNativeEntry } from '../hook-engine/registration.js';
|
|
8
9
|
/** Events master-hook.sh is registered for at user scope. */
|
|
9
10
|
const GLOBAL_HOOK_EVENTS = [
|
|
10
11
|
'SessionStart',
|
|
@@ -98,6 +99,8 @@ export function removeClaudeMdBlock(current) {
|
|
|
98
99
|
function isHqHookEntry(entry, hqRoot) {
|
|
99
100
|
if (!entry || typeof entry !== 'object')
|
|
100
101
|
return false;
|
|
102
|
+
if (isNativeEntry(entry, hqRoot))
|
|
103
|
+
return true;
|
|
101
104
|
const cmd = entry.command;
|
|
102
105
|
if (typeof cmd !== 'string')
|
|
103
106
|
return false;
|
|
@@ -111,13 +114,15 @@ export function addHqHooks(doc, hqRoot, target = 'master') {
|
|
|
111
114
|
const hooks = { ...(doc.hooks ?? {}) };
|
|
112
115
|
let changed = false;
|
|
113
116
|
for (const event of GLOBAL_HOOK_EVENTS) {
|
|
114
|
-
const want =
|
|
117
|
+
const want = typeof target === 'object'
|
|
118
|
+
? { type: 'command', command: target.engine, args: engineArgs(hqRoot, event, 'claude', true), timeout: 300 }
|
|
119
|
+
: { type: 'command', command: hookCommand(hqRoot, event, target), timeout: 300 };
|
|
115
120
|
let groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
|
|
116
|
-
const present = groups.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => h.command === want));
|
|
121
|
+
const present = groups.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => h.command === want.command && JSON.stringify(h.args) === JSON.stringify('args' in want ? want.args : undefined)));
|
|
117
122
|
if (present)
|
|
118
123
|
continue;
|
|
119
124
|
groups = removeHqHooks({ hooks: { [event]: groups } }, hqRoot).hooks?.[event] ?? [];
|
|
120
|
-
groups.push({ hooks: [
|
|
125
|
+
groups.push({ hooks: [want] });
|
|
121
126
|
hooks[event] = groups;
|
|
122
127
|
changed = true;
|
|
123
128
|
}
|
|
@@ -23,6 +23,9 @@ export declare function addAgentsMdBlock(current: string | null, charter: string
|
|
|
23
23
|
/** Strip the HQ block, plus the separator `addAgentsMdBlock` put before it. */
|
|
24
24
|
export declare function removeAgentsMdBlock(current: string): string;
|
|
25
25
|
type Doc = Record<string, unknown>;
|
|
26
|
+
export declare function isAdapterEntry(entry: unknown, hqRoot: string): boolean;
|
|
27
|
+
/** The HQ hooks.json entry for one event and target. */
|
|
28
|
+
export declare function codexHookEntry(hqRoot: string, event: string, target?: HookTarget): Record<string, unknown>;
|
|
26
29
|
/**
|
|
27
30
|
* Add one HQ hook group per event; idempotent. An HQ entry for the other
|
|
28
31
|
* target (adapter vs the hqd shim) is swapped out. Foreign hooks are untouched.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* module never writes a second adapter.
|
|
11
11
|
*/
|
|
12
12
|
import * as fs from 'fs';
|
|
13
|
+
import { engineArgs, isNativeEntry, quotePosix, quoteWindows } from '../hook-engine/registration.js';
|
|
13
14
|
import * as path from 'path';
|
|
14
15
|
import { shimPath } from './claude-targets.js';
|
|
15
16
|
/** Events the Codex hook adapter is registered for at user scope. */
|
|
@@ -88,15 +89,25 @@ export function removeAgentsMdBlock(current) {
|
|
|
88
89
|
start -= 1;
|
|
89
90
|
return current.slice(0, start) + current.slice(end);
|
|
90
91
|
}
|
|
91
|
-
function isAdapterEntry(entry, hqRoot) {
|
|
92
|
+
export function isAdapterEntry(entry, hqRoot) {
|
|
92
93
|
if (!entry || typeof entry !== 'object')
|
|
93
94
|
return false;
|
|
95
|
+
if (isNativeEntry(entry, hqRoot))
|
|
96
|
+
return true;
|
|
94
97
|
const cmd = entry.command;
|
|
95
98
|
return (typeof cmd === 'string' &&
|
|
96
99
|
(cmd.includes(JSON.stringify(codexAdapterPath(hqRoot))) ||
|
|
97
100
|
cmd.includes(JSON.stringify(shimPath(hqRoot))) ||
|
|
98
101
|
cmd.includes(JSON.stringify(path.join(hqRoot, 'core', 'packages', 'hq-anywhere', 'hooks', 'codex.sh')))));
|
|
99
102
|
}
|
|
103
|
+
/** The HQ hooks.json entry for one event and target. */
|
|
104
|
+
export function codexHookEntry(hqRoot, event, target = 'master') {
|
|
105
|
+
const timeout = event === 'SessionEnd' ? 3 : 300;
|
|
106
|
+
if (typeof target !== 'object')
|
|
107
|
+
return { type: 'command', command: codexHookCommand(hqRoot, target), timeout };
|
|
108
|
+
const argv = [target.engine, ...engineArgs(hqRoot, event, 'codex')];
|
|
109
|
+
return { type: 'command', command: argv.map(quotePosix).join(' '), ...(target.engine.endsWith('.exe') ? { commandWindows: '& ' + argv.map(quoteWindows).join(' ') } : {}), timeout };
|
|
110
|
+
}
|
|
100
111
|
/**
|
|
101
112
|
* Add one HQ hook group per event; idempotent. An HQ entry for the other
|
|
102
113
|
* target (adapter vs the hqd shim) is swapped out. Foreign hooks are untouched.
|
|
@@ -104,14 +115,14 @@ function isAdapterEntry(entry, hqRoot) {
|
|
|
104
115
|
export function addCodexHooks(doc, hqRoot, target = 'master') {
|
|
105
116
|
const hooks = { ...(doc.hooks ?? {}) };
|
|
106
117
|
let changed = false;
|
|
107
|
-
const want = codexHookCommand(hqRoot, target);
|
|
108
118
|
for (const event of CODEX_HOOK_EVENTS) {
|
|
119
|
+
const want = codexHookEntry(hqRoot, event, target);
|
|
109
120
|
let groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
|
|
110
|
-
const present = groups.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => h.command === want));
|
|
121
|
+
const present = groups.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => h.command === want.command && h.commandWindows === ('commandWindows' in want ? want.commandWindows : undefined)));
|
|
111
122
|
if (present)
|
|
112
123
|
continue;
|
|
113
124
|
groups = removeCodexHooks({ hooks: { [event]: groups } }, hqRoot).hooks?.[event] ?? [];
|
|
114
|
-
groups.push({ hooks: [
|
|
125
|
+
groups.push({ hooks: [want] });
|
|
115
126
|
hooks[event] = groups;
|
|
116
127
|
changed = true;
|
|
117
128
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type HookTarget } from './claude-targets.js';
|
|
2
|
+
/** True when an hq daemon unit (launchd plist or systemd unit) exists. */
|
|
3
|
+
export declare function isDaemonInstalled(home: string, platform?: NodeJS.Platform, queryTask?: (task: string) => boolean): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* US-010: with hqd installed (and the checkout shipping the shim), hooks go to
|
|
6
|
+
* the hqd shim; otherwise to master-hook.sh / the Codex adapter.
|
|
7
|
+
*/
|
|
8
|
+
export declare function hookTargetFor(hqRoot: string, home: string, daemonInstalled?: boolean, env?: NodeJS.ProcessEnv): HookTarget;
|
|
9
|
+
//# sourceMappingURL=hook-target.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which hook target a user-scope registration uses. Kept apart from
|
|
3
|
+
* install-global so hq reindex can reconcile registrations without loading the
|
|
4
|
+
* install command graph.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { realpathOrSelf } from '../../commands/mcp-registration.js';
|
|
10
|
+
import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from '../daemon/install.js';
|
|
11
|
+
import { readDaemonConfig } from '../daemon/config.js';
|
|
12
|
+
import { daemonPaths } from '../daemon/paths.js';
|
|
13
|
+
import { registrationEngine } from '../hook-engine/registration.js';
|
|
14
|
+
import { shimPath } from './claude-targets.js';
|
|
15
|
+
/** True when an hq daemon unit (launchd plist or systemd unit) exists. */
|
|
16
|
+
export function isDaemonInstalled(home, platform = process.platform, queryTask = (task) => spawnSync('schtasks.exe', ['/Query', '/TN', task], { encoding: 'utf8', windowsHide: true }).status === 0) {
|
|
17
|
+
if (platform === 'win32')
|
|
18
|
+
return queryTask(DAEMON_WINDOWS_TASK);
|
|
19
|
+
const candidates = platform === 'darwin'
|
|
20
|
+
? [path.join(home, 'Library', 'LaunchAgents', `${DAEMON_LAUNCHD_LABEL}.plist`)]
|
|
21
|
+
: platform === 'linux'
|
|
22
|
+
? [path.join(home, '.config', 'systemd', 'user', DAEMON_SYSTEMD_UNIT), path.join(SYSTEM_UNIT_DIR, DAEMON_SYSTEMD_UNIT)]
|
|
23
|
+
: [];
|
|
24
|
+
return candidates.some((p) => fs.existsSync(p));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* US-010: with hqd installed (and the checkout shipping the shim), hooks go to
|
|
28
|
+
* the hqd shim; otherwise to master-hook.sh / the Codex adapter.
|
|
29
|
+
*/
|
|
30
|
+
export function hookTargetFor(hqRoot, home, daemonInstalled, env = process.env) {
|
|
31
|
+
const engine = registrationEngine(home, env);
|
|
32
|
+
if (engine)
|
|
33
|
+
return { engine };
|
|
34
|
+
const installed = daemonInstalled ?? isDaemonInstalled(home);
|
|
35
|
+
const configuredRoot = readDaemonConfig(daemonPaths(home, env).configPath).hqRoot;
|
|
36
|
+
const daemonMatchesRoot = configuredRoot !== undefined && realpathOrSelf(configuredRoot) === realpathOrSelf(hqRoot);
|
|
37
|
+
return installed && daemonMatchesRoot && fs.existsSync(shimPath(hqRoot)) ? 'shim' : 'master';
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=hook-target.js.map
|
|
@@ -13,6 +13,7 @@ export type OutpostIdentity = {
|
|
|
13
13
|
export declare function readOutpostIdentity(existsSync?: (p: string) => boolean, readFileSync?: (p: string) => string): OutpostIdentity;
|
|
14
14
|
export declare function resolveAdmissionHost(deps?: {
|
|
15
15
|
hostname?: () => string;
|
|
16
|
+
homedir?: () => string;
|
|
16
17
|
readOutpostIdentity?: () => OutpostIdentity;
|
|
17
18
|
existsSync?: (p: string) => boolean;
|
|
18
19
|
readFileSync?: (p: string) => string;
|