@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.
Files changed (168) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -0
  3. package/assets/bot-workers/setup/worker.yaml +11 -0
  4. package/dist/command-catalog.generated.d.ts +341 -11
  5. package/dist/command-catalog.generated.js +421 -11
  6. package/dist/command-help.js +2 -1
  7. package/dist/commands/access.d.ts +45 -2
  8. package/dist/commands/access.js +537 -223
  9. package/dist/commands/agent-inbox.js +2 -1
  10. package/dist/commands/billing-usage.d.ts +33 -2
  11. package/dist/commands/billing-usage.js +75 -4
  12. package/dist/commands/bot.js +2 -2
  13. package/dist/commands/channels.d.ts +27 -0
  14. package/dist/commands/channels.js +108 -4
  15. package/dist/commands/cloud.js +31 -1
  16. package/dist/commands/core-refresh-vault-access-native.d.ts +33 -0
  17. package/dist/commands/core-refresh-vault-access-native.js +356 -28
  18. package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
  19. package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
  20. package/dist/commands/core.js +9 -0
  21. package/dist/commands/daemon.d.ts +7 -0
  22. package/dist/commands/daemon.js +37 -3
  23. package/dist/commands/dm.js +18 -3
  24. package/dist/commands/doctor.d.ts +11 -1
  25. package/dist/commands/doctor.js +56 -1
  26. package/dist/commands/install-global.d.ts +8 -8
  27. package/dist/commands/install-global.js +139 -61
  28. package/dist/commands/lanes-shared.d.ts +3 -1
  29. package/dist/commands/lanes.d.ts +4 -4
  30. package/dist/commands/lanes.js +380 -25
  31. package/dist/commands/library-add.d.ts +31 -1
  32. package/dist/commands/library-add.js +69 -2
  33. package/dist/commands/library.js +37 -3
  34. package/dist/commands/pack-install.d.ts +2 -2
  35. package/dist/commands/pack-install.js +133 -35
  36. package/dist/commands/reindex.js +19 -0
  37. package/dist/commands/sync-manifest.d.ts +2 -9
  38. package/dist/commands/sync-manifest.js +4 -15
  39. package/dist/hook-engine-path.d.ts +4 -0
  40. package/dist/hook-engine-path.js +43 -0
  41. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2783 -2354
  42. package/dist/hq-cloud-sync-runner/package.json +1 -1
  43. package/dist/index.d.ts +10 -0
  44. package/dist/index.js +76 -5
  45. package/dist/lazy-commands.js +2 -0
  46. package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
  47. package/dist/lib/agent-kit/inbox-redact.js +361 -0
  48. package/dist/lib/agent-kit/inbox-state.js +11 -2
  49. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  50. package/dist/lib/agent-kit/run/inbox.js +36 -7
  51. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
  52. package/dist/lib/anywhere-runtime-flag-key.js +9 -0
  53. package/dist/lib/billing/usage-cost.d.ts +31 -1
  54. package/dist/lib/billing/usage-cost.js +46 -0
  55. package/dist/lib/company-skill-wrapper.js +6 -4
  56. package/dist/lib/daemon/install.d.ts +47 -1
  57. package/dist/lib/daemon/install.js +181 -15
  58. package/dist/lib/daemon/lock.d.ts +32 -0
  59. package/dist/lib/daemon/lock.js +55 -2
  60. package/dist/lib/db/local.d.ts +2 -2
  61. package/dist/lib/db/local.js +2 -2
  62. package/dist/lib/db/sql.js +2 -2
  63. package/dist/lib/db/sqlite-binding.d.ts +22 -0
  64. package/dist/lib/db/sqlite-binding.js +168 -0
  65. package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
  66. package/dist/lib/doctor/checks/anywhere.d.ts +15 -0
  67. package/dist/lib/doctor/checks/anywhere.js +77 -11
  68. package/dist/lib/doctor/checks/cli-path.d.ts +12 -0
  69. package/dist/lib/doctor/checks/cli-path.js +46 -0
  70. package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
  71. package/dist/lib/doctor/checks/hook-engine.js +17 -0
  72. package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
  73. package/dist/lib/doctor/checks/sync-health.js +38 -2
  74. package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
  75. package/dist/lib/doctor/checks/temp-homes.js +54 -0
  76. package/dist/lib/doctor/fix/apply.d.ts +90 -1
  77. package/dist/lib/doctor/fix/apply.js +176 -10
  78. package/dist/lib/doctor/fix/json-output.d.ts +4 -1
  79. package/dist/lib/doctor/fix/json-output.js +6 -0
  80. package/dist/lib/doctor/fix/remediation.d.ts +7 -1
  81. package/dist/lib/doctor/fix/remediation.js +23 -1
  82. package/dist/lib/doctor/registry.js +10 -0
  83. package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
  84. package/dist/lib/doctor/temp-home-sweeper.js +418 -0
  85. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  86. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  87. package/dist/lib/flag-registry.js +9 -1
  88. package/dist/lib/flag-snapshot-file.js +8 -3
  89. package/dist/lib/hook-engine/reconcile.d.ts +9 -0
  90. package/dist/lib/hook-engine/reconcile.js +134 -0
  91. package/dist/lib/hook-engine/registration.d.ts +17 -0
  92. package/dist/lib/hook-engine/registration.js +50 -0
  93. package/dist/lib/hook-engine/resolve.d.ts +31 -0
  94. package/dist/lib/hook-engine/resolve.js +134 -0
  95. package/dist/lib/install/claude-targets.d.ts +2 -1
  96. package/dist/lib/install/claude-targets.js +8 -3
  97. package/dist/lib/install/codex-targets.d.ts +3 -0
  98. package/dist/lib/install/codex-targets.js +15 -4
  99. package/dist/lib/install/hook-target.d.ts +9 -0
  100. package/dist/lib/install/hook-target.js +39 -0
  101. package/dist/lib/lanes/admission-host.d.ts +1 -0
  102. package/dist/lib/lanes/admission-host.js +8 -1
  103. package/dist/lib/lanes/admission.d.ts +4 -0
  104. package/dist/lib/lanes/admission.js +6 -2
  105. package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
  106. package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
  107. package/dist/lib/lanes/index.d.ts +2 -2
  108. package/dist/lib/lanes/index.js +1 -1
  109. package/dist/lib/lanes/keep-alive.js +4 -1
  110. package/dist/lib/lanes/local-host-identity.d.ts +40 -0
  111. package/dist/lib/lanes/local-host-identity.js +314 -0
  112. package/dist/lib/lanes/reaper.d.ts +1 -1
  113. package/dist/lib/lanes/reaper.js +7 -0
  114. package/dist/lib/lanes/reconcile.d.ts +4 -0
  115. package/dist/lib/lanes/reconcile.js +16 -8
  116. package/dist/lib/lanes/spawn.d.ts +20 -0
  117. package/dist/lib/lanes/spawn.js +48 -2
  118. package/dist/lib/lanes/status-fields.js +6 -1
  119. package/dist/lib/lanes/store.js +8 -1
  120. package/dist/lib/lanes/throttle.d.ts +49 -6
  121. package/dist/lib/lanes/throttle.js +154 -17
  122. package/dist/lib/lanes/types.d.ts +22 -1
  123. package/dist/lib/lanes/types.js +27 -3
  124. package/dist/lib/lanes/watcher.js +27 -2
  125. package/dist/lib/lanes/worker-continuity.d.ts +2 -0
  126. package/dist/lib/lanes/worker-continuity.js +18 -11
  127. package/dist/lib/manifest-disabled.d.ts +13 -0
  128. package/dist/lib/manifest-disabled.js +19 -0
  129. package/dist/lib/service-manager/launchd.d.ts +1 -0
  130. package/dist/lib/service-manager/launchd.js +1 -1
  131. package/dist/lib/windows/console-encoding.d.ts +24 -0
  132. package/dist/lib/windows/console-encoding.js +83 -0
  133. package/dist/lib/windows/install-followup.d.ts +35 -0
  134. package/dist/lib/windows/install-followup.js +142 -0
  135. package/dist/lib/windows/user-path.d.ts +125 -0
  136. package/dist/lib/windows/user-path.js +275 -0
  137. package/dist/lib/work-context/auto-bind.d.ts +1 -1
  138. package/dist/lib/work-context/auto-bind.js +22 -3
  139. package/dist/lib/work-context/lane-bind.d.ts +31 -0
  140. package/dist/lib/work-context/lane-bind.js +101 -0
  141. package/dist/lib/work-context/organize.js +1 -0
  142. package/dist/lib/work-context/prd-sync.js +7 -0
  143. package/dist/lib/work-context/project.d.ts +36 -10
  144. package/dist/lib/work-context/project.js +130 -41
  145. package/dist/lib/work-context/reconcile.d.ts +6 -0
  146. package/dist/lib/work-context/reconcile.js +25 -5
  147. package/dist/lib/work-context/state.d.ts +6 -0
  148. package/dist/lib/work-context/state.js +29 -1
  149. package/dist/main.js +2 -1
  150. package/dist/unknown-command.d.ts +18 -0
  151. package/dist/unknown-command.js +88 -0
  152. package/dist/utils/cognito-session.d.ts +8 -0
  153. package/dist/utils/cognito-session.js +14 -2
  154. package/dist/utils/copy-tree.d.ts +61 -0
  155. package/dist/utils/copy-tree.js +249 -0
  156. package/dist/utils/environmental-error.js +7 -3
  157. package/dist/utils/hook-trust.d.ts +9 -0
  158. package/dist/utils/hook-trust.js +30 -2
  159. package/dist/utils/npm-invocation.d.ts +27 -0
  160. package/dist/utils/npm-invocation.js +54 -0
  161. package/dist/utils/qmd-native-binding-error.js +3 -1
  162. package/dist/utils/resolve-executable.d.ts +2 -0
  163. package/dist/utils/resolve-executable.js +6 -2
  164. package/dist/utils/safe-link.d.ts +50 -0
  165. package/dist/utils/safe-link.js +300 -0
  166. package/package.json +17 -5
  167. package/scripts/hook-engine-manifest.mjs +22 -0
  168. 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, env = process.env) {
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
- const options = { env, maxAgeMs: Number.POSITIVE_INFINITY };
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 = hookCommand(hqRoot, event, target);
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: [{ type: 'command', command: want, timeout: 300 }] });
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: [{ type: 'command', command: want, timeout: event === 'SessionEnd' ? 3 : 300 }] });
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;