@indigoai-us/hq-cli 5.345.148 → 5.345.150

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 (83) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +2 -0
  3. package/dist/command-catalog.generated.d.ts +62 -14
  4. package/dist/command-catalog.generated.js +74 -14
  5. package/dist/command-help.js +2 -1
  6. package/dist/commands/_patterns.d.ts +6 -0
  7. package/dist/commands/_patterns.js +6 -0
  8. package/dist/commands/access.d.ts +45 -2
  9. package/dist/commands/access.js +537 -223
  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/core-refresh-vault-access-native.d.ts +16 -0
  16. package/dist/commands/core-refresh-vault-access-native.js +142 -9
  17. package/dist/commands/core.js +4 -1
  18. package/dist/commands/dm.js +9 -1
  19. package/dist/commands/groups.js +9 -6
  20. package/dist/commands/install-global.d.ts +1 -8
  21. package/dist/commands/install-global.js +11 -27
  22. package/dist/commands/lanes-questions.js +7 -4
  23. package/dist/commands/lanes.js +13 -6
  24. package/dist/commands/library-add.d.ts +31 -1
  25. package/dist/commands/library-add.js +69 -2
  26. package/dist/commands/library.js +37 -3
  27. package/dist/commands/members.d.ts +35 -0
  28. package/dist/commands/members.js +65 -14
  29. package/dist/commands/reindex.js +19 -0
  30. package/dist/commands/secrets.d.ts +5 -0
  31. package/dist/commands/secrets.js +11 -6
  32. package/dist/hook-engine-path.js +10 -3
  33. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +63282 -62480
  34. package/dist/hq-cloud-sync-runner/package.json +1 -1
  35. package/dist/index.d.ts +10 -0
  36. package/dist/index.js +70 -5
  37. package/dist/lazy-commands.js +2 -0
  38. package/dist/lib/billing/usage-cost.d.ts +31 -1
  39. package/dist/lib/billing/usage-cost.js +46 -0
  40. package/dist/lib/db/local.d.ts +2 -2
  41. package/dist/lib/db/local.js +2 -2
  42. package/dist/lib/db/sql.js +2 -2
  43. package/dist/lib/db/sqlite-binding.d.ts +22 -0
  44. package/dist/lib/db/sqlite-binding.js +168 -0
  45. package/dist/lib/doctor/checks/anywhere.d.ts +12 -0
  46. package/dist/lib/doctor/checks/anywhere.js +63 -12
  47. package/dist/lib/doctor/checks/cli-path.d.ts +2 -1
  48. package/dist/lib/doctor/checks/cli-path.js +2 -1
  49. package/dist/lib/doctor/checks/hook-engine.js +2 -1
  50. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  51. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  52. package/dist/lib/hook-engine/reconcile.d.ts +9 -0
  53. package/dist/lib/hook-engine/reconcile.js +134 -0
  54. package/dist/lib/hook-engine/registration.d.ts +17 -0
  55. package/dist/lib/hook-engine/registration.js +50 -0
  56. package/dist/lib/install/claude-targets.d.ts +2 -1
  57. package/dist/lib/install/claude-targets.js +8 -3
  58. package/dist/lib/install/codex-targets.d.ts +3 -0
  59. package/dist/lib/install/codex-targets.js +15 -4
  60. package/dist/lib/install/hook-target.d.ts +9 -0
  61. package/dist/lib/install/hook-target.js +39 -0
  62. package/dist/lib/lanes/index.d.ts +1 -1
  63. package/dist/lib/lanes/index.js +1 -1
  64. package/dist/lib/lanes/store.d.ts +11 -0
  65. package/dist/lib/lanes/store.js +20 -0
  66. package/dist/lib/lanes/watch.js +5 -3
  67. package/dist/lib/windows/console-encoding.d.ts +24 -0
  68. package/dist/lib/windows/console-encoding.js +83 -0
  69. package/dist/lib/windows/install-followup.d.ts +35 -0
  70. package/dist/lib/windows/install-followup.js +142 -0
  71. package/dist/lib/windows/user-path.d.ts +80 -3
  72. package/dist/lib/windows/user-path.js +187 -26
  73. package/dist/main.js +2 -1
  74. package/dist/unknown-command.d.ts +18 -0
  75. package/dist/unknown-command.js +88 -0
  76. package/dist/utils/cognito-session.d.ts +8 -0
  77. package/dist/utils/cognito-session.js +14 -2
  78. package/dist/utils/hook-trust.d.ts +9 -0
  79. package/dist/utils/hook-trust.js +30 -2
  80. package/dist/utils/qmd-native-binding-error.js +3 -1
  81. package/dist/utils/resolve-executable.d.ts +2 -0
  82. package/dist/utils/resolve-executable.js +6 -2
  83. package/package.json +9 -9
@@ -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
@@ -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
@@ -20,7 +20,7 @@ export { LANE_STORY_LEDGER_FLAG, buildLaneStatusSnapshot, formatLaneStatusHuman,
20
20
  export type { LaneStatusSnapshot, ProjectStoryLedger, StoryLedgerWriteDeps } from "./story-ledger.js";
21
21
  export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
22
22
  export type { OutpostDispatchResult, OutpostPickupPayload, } from "./outpost-dispatch.js";
23
- export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readAllLanes, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
23
+ export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, expandLocalLaneIdPrefix, LANE_ID_PREFIX_MIN, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readAllLanes, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
24
24
  export type { StoreDeps } from "./store.js";
25
25
  export { DEFAULT_LANE_WATCH_INTERVAL_MS, MAX_LANE_WATCH_INTERVAL_MS, MAX_WATCHED_LANES, MAX_WATCH_OUTPUT_LINES, runLaneWatch, } from "./watch.js";
26
26
  export type { LaneWatchDeps, LaneWatchResult } from "./watch.js";
@@ -12,7 +12,7 @@ export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFT
12
12
  export { resolveLanesHqRoot } from "./hq-root.js";
13
13
  export { LANE_STORY_LEDGER_FLAG, buildLaneStatusSnapshot, formatLaneStatusHuman, laneStoryLedgerEnabled, resolveProjectStoryLedger, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, } from "./story-ledger.js";
14
14
  export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
15
- export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readAllLanes, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
15
+ export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, expandLocalLaneIdPrefix, LANE_ID_PREFIX_MIN, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readAllLanes, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
16
16
  export { DEFAULT_LANE_WATCH_INTERVAL_MS, MAX_LANE_WATCH_INTERVAL_MS, MAX_WATCHED_LANES, MAX_WATCH_OUTPUT_LINES, runLaneWatch, } from "./watch.js";
17
17
  export { MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_TASK_OUTPUT_MAX_BYTES, MONITOR_TRANSCRIPT_MAX_BYTES, MONITOR_UNCOVERED_EXIT_CODE, coveredLaneIds, isMonitorTaskLive, monitorReminderText, parseCanonicalWatchCommand, parseMonitorTranscript, readLiveMonitorEvidence, } from "./monitor.js";
18
18
  export { IMDS_HOST, IMDS_INSTANCE_ID_URL, IMDS_TIMEOUT_MS, IMDS_TOKEN_URL, defaultReadImdsInstanceId, resolveMachineIdentity, } from "./machine-identity.js";
@@ -87,5 +87,16 @@ export declare function createQuestion(hqRoot: string, init: Omit<QuestionRecord
87
87
  export declare function updateQuestion(hqRoot: string, questionId: string, expectedVersion: number, updater: (current: QuestionRecord) => QuestionRecord, deps?: StoreDeps): QuestionRecord;
88
88
  export declare function appendQuestionRouting(hqRoot: string, questionId: string, expectedVersion: number, entry: RoutingHistoryEntry, deps?: StoreDeps): QuestionRecord;
89
89
  export declare function listLaneIds(hqRoot: string, deps?: StoreDeps): string[];
90
+ /** Shortest lane-id prefix a command expands to a full local lane id. */
91
+ export declare const LANE_ID_PREFIX_MIN = 8;
92
+ /**
93
+ * Expand a short lane-id prefix (at least `LANE_ID_PREFIX_MIN` ULID chars,
94
+ * no host suffix) to the one local lane id it starts. Returns `undefined`
95
+ * when the input is not such a prefix or matches no local lane, and throws
96
+ * `lane_id_ambiguous` listing every candidate when it matches more than one.
97
+ * Lane records, questions, run dirs and envelopes are always keyed by the
98
+ * full lane id; a prefix is only ever a lookup convenience.
99
+ */
100
+ export declare function expandLocalLaneIdPrefix(hqRoot: string, input: string, deps?: StoreDeps): string | undefined;
90
101
  export declare function listQuestionIds(hqRoot: string, deps?: StoreDeps): string[];
91
102
  //# sourceMappingURL=store.d.ts.map
@@ -2072,6 +2072,26 @@ function listRecordIds(dir, label, deps) {
2072
2072
  export function listLaneIds(hqRoot, deps = defaultStoreDeps()) {
2073
2073
  return listRecordIds(path.join(lanesStoreRoot(hqRoot), LANE_RECORDS_DIR), "lane", deps);
2074
2074
  }
2075
+ /** Shortest lane-id prefix a command expands to a full local lane id. */
2076
+ export const LANE_ID_PREFIX_MIN = 8;
2077
+ /**
2078
+ * Expand a short lane-id prefix (at least `LANE_ID_PREFIX_MIN` ULID chars,
2079
+ * no host suffix) to the one local lane id it starts. Returns `undefined`
2080
+ * when the input is not such a prefix or matches no local lane, and throws
2081
+ * `lane_id_ambiguous` listing every candidate when it matches more than one.
2082
+ * Lane records, questions, run dirs and envelopes are always keyed by the
2083
+ * full lane id; a prefix is only ever a lookup convenience.
2084
+ */
2085
+ export function expandLocalLaneIdPrefix(hqRoot, input, deps = defaultStoreDeps()) {
2086
+ if (!new RegExp(`^[0-9A-HJKMNP-TV-Z]{${LANE_ID_PREFIX_MIN},26}$`, "i").test(input))
2087
+ return undefined;
2088
+ const prefix = input.toUpperCase();
2089
+ const matches = listLaneIds(hqRoot, deps).filter((laneId) => laneId.toUpperCase().startsWith(prefix)).sort();
2090
+ if (matches.length > 1) {
2091
+ throw new LanesError("lane_id_ambiguous", `Lane id prefix ${input} matches ${matches.length} lanes: ${matches.join(", ")}. Pass the full lane id.`, { lane_id: input, matches });
2092
+ }
2093
+ return matches[0];
2094
+ }
2075
2095
  export function listQuestionIds(hqRoot, deps = defaultStoreDeps()) {
2076
2096
  return listRecordIds(path.join(lanesStoreRoot(hqRoot), QUESTION_RECORDS_DIR), "question", deps);
2077
2097
  }
@@ -1,4 +1,4 @@
1
- import { listQuestionIds, readLane, readQuestion, } from "./store.js";
1
+ import { expandLocalLaneIdPrefix, listQuestionIds, readLane, readQuestion, } from "./store.js";
2
2
  import { LaneNotFoundError, LanesError } from "./errors.js";
3
3
  import { providerSupportsSessionResume } from "./delivery.js";
4
4
  import { canResumeDoneKeepAliveLane, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, } from "./keep-alive.js";
@@ -62,7 +62,9 @@ function canResumeKeepAliveLane(hqRoot, lane, nowMs = Date.now()) {
62
62
  return false;
63
63
  }
64
64
  export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
65
- const watchedIds = validateLaneIds(laneIds);
65
+ // Watch by full lane id: a short prefix expands only when it names exactly
66
+ // one local lane, and an ambiguous prefix is refused with its candidates.
67
+ const watchedIds = [...new Set(validateLaneIds(laneIds).map((laneId) => expandLocalLaneIdPrefix(hqRoot, laneId) ?? laneId))];
66
68
  const readLaneRecord = deps.readLane ?? readLane;
67
69
  const listQuestions = deps.listQuestionIds ?? listQuestionIds;
68
70
  const readQuestionRecord = deps.readQuestion ?? readQuestion;
@@ -130,7 +132,7 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
130
132
  }
131
133
  records.push(record);
132
134
  if (previousStates.get(laneId) !== record.state) {
133
- emit(`lane ${laneId}: state=${record.state}${isTerminalLaneState(record.state) ? " (terminal)" : ""}`);
135
+ emit(`lane ${record.lane_id}: state=${record.state}${isTerminalLaneState(record.state) ? " (terminal)" : ""}`);
134
136
  previousStates.set(laneId, record.state);
135
137
  }
136
138
  }
@@ -0,0 +1,24 @@
1
+ /** ASCII stand-ins for characters cmd.exe cannot show on code page 437. */
2
+ export declare function toAscii(text: string): string;
3
+ /** Parse `chcp` output (`Active code page: 437`). Null when the number is missing. */
4
+ export declare function parseChcpOutput(text: string): number | null;
5
+ type ChcpRun = () => {
6
+ status: number | null;
7
+ stdout: string;
8
+ };
9
+ export declare function readConsoleCodePage(run?: ChcpRun): number | null;
10
+ export interface ConsoleEncodingDeps {
11
+ platform?: NodeJS.Platform;
12
+ stdout?: NodeJS.WriteStream;
13
+ stderr?: NodeJS.WriteStream;
14
+ /** Known code page. Null means the page could not be read and is treated as not UTF-8. */
15
+ codePage?: number | null;
16
+ readCodePage?: () => number | null;
17
+ }
18
+ /**
19
+ * Wrap console stdout and stderr on Windows when the code page is not UTF-8.
20
+ * Returns false when no wrap was installed.
21
+ */
22
+ export declare function installWindowsConsoleEncoding(deps?: ConsoleEncodingDeps): boolean;
23
+ export {};
24
+ //# sourceMappingURL=console-encoding.d.ts.map
@@ -0,0 +1,83 @@
1
+ /**
2
+ * cmd.exe on a non-UTF-8 code page prints UTF-8 em dashes as mojibake.
3
+ * When stdout or stderr is a console and the code page is not 65001, string
4
+ * writes are transliterated to ASCII. Pipes are left as UTF-8 so captured
5
+ * logs stay intact. Buffer writes are not modified.
6
+ */
7
+ import { spawnSync } from "node:child_process";
8
+ const WRAP = Symbol.for("hq.windowsConsoleEncoding");
9
+ const REPLACEMENTS = [
10
+ [/\u2014/g, "--"],
11
+ [/\u2013/g, "-"],
12
+ [/\u2018|\u2019|\u201A/g, "'"],
13
+ [/\u201C|\u201D|\u201E/g, "\""],
14
+ [/\u2026/g, "..."],
15
+ [/\u00A0/g, " "],
16
+ [/\u2022/g, "*"],
17
+ [/\u2192/g, "->"],
18
+ [/\u2190/g, "<-"],
19
+ [/\u21D2/g, "=>"],
20
+ [/\u00D7/g, "x"],
21
+ ];
22
+ /** ASCII stand-ins for characters cmd.exe cannot show on code page 437. */
23
+ export function toAscii(text) {
24
+ let out = text;
25
+ for (const [pattern, replacement] of REPLACEMENTS)
26
+ out = out.replace(pattern, replacement);
27
+ return out.replace(/[^\t\n\r\x20-\x7E]/g, "?");
28
+ }
29
+ /** Parse `chcp` output (`Active code page: 437`). Null when the number is missing. */
30
+ export function parseChcpOutput(text) {
31
+ const match = /:\s*(\d+)/.exec(text);
32
+ if (!match)
33
+ return null;
34
+ const codePage = Number(match[1]);
35
+ return Number.isInteger(codePage) ? codePage : null;
36
+ }
37
+ function defaultChcp() {
38
+ const result = spawnSync("chcp", [], { encoding: "utf8", windowsHide: true, timeout: 2000 });
39
+ return { status: result.status, stdout: result.stdout ?? "" };
40
+ }
41
+ export function readConsoleCodePage(run = defaultChcp) {
42
+ try {
43
+ const result = run();
44
+ if (result.status !== 0)
45
+ return null;
46
+ return parseChcpOutput(result.stdout);
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ function wrapStream(stream) {
53
+ if (stream[WRAP])
54
+ return;
55
+ const original = stream.write.bind(stream);
56
+ stream.write = ((chunk, encoding, cb) => {
57
+ if (typeof chunk === "string")
58
+ return original(toAscii(chunk), encoding, cb);
59
+ return original(chunk, encoding, cb);
60
+ });
61
+ stream[WRAP] = true;
62
+ }
63
+ /**
64
+ * Wrap console stdout and stderr on Windows when the code page is not UTF-8.
65
+ * Returns false when no wrap was installed.
66
+ */
67
+ export function installWindowsConsoleEncoding(deps = {}) {
68
+ if ((deps.platform ?? process.platform) !== "win32")
69
+ return false;
70
+ const stdout = (deps.stdout ?? process.stdout);
71
+ const stderr = (deps.stderr ?? process.stderr);
72
+ if (!stdout.isTTY && !stderr.isTTY)
73
+ return false;
74
+ const codePage = deps.codePage !== undefined ? deps.codePage : (deps.readCodePage ?? readConsoleCodePage)();
75
+ if (codePage === 65001)
76
+ return false;
77
+ if (stdout.isTTY)
78
+ wrapStream(stdout);
79
+ if (stderr.isTTY)
80
+ wrapStream(stderr);
81
+ return true;
82
+ }
83
+ //# sourceMappingURL=console-encoding.js.map
@@ -0,0 +1,35 @@
1
+ export declare function isInstallFollowupSkipped(argv: readonly string[], env: NodeJS.ProcessEnv): boolean;
2
+ export declare function isInstalledPackageEntry(entry: string | undefined): boolean;
3
+ /**
4
+ * `version`: the CLI version whose mesh daemon install succeeded.
5
+ * `path`: the shim folder and CLI version whose PATH step already ran, so a
6
+ * refused or locked HKCU is not retried on every command.
7
+ * `meshFailedAt`: when a daemon install for `meshFailedVersion` last failed.
8
+ * A failure is retried after MESH_RETRY_MS rather than on every command.
9
+ */
10
+ export interface InstallFollowupMarker {
11
+ version?: string;
12
+ path?: {
13
+ dir: string;
14
+ version: string;
15
+ };
16
+ meshFailedVersion?: string;
17
+ meshFailedAt?: number;
18
+ }
19
+ /** A failed daemon install blocks for up to 30 s, so it is retried at most hourly. */
20
+ export declare const MESH_RETRY_MS: number;
21
+ export interface InstallFollowupDeps {
22
+ argv: readonly string[];
23
+ env?: NodeJS.ProcessEnv;
24
+ /** The npm shim folder for this entry, or null when hq is not an npm global install. */
25
+ shimDir?: () => string | null;
26
+ ensurePath?: () => void;
27
+ /** True when the daemon install succeeded. */
28
+ spawnMesh?: () => boolean;
29
+ version?: string;
30
+ readMarker?: () => InstallFollowupMarker;
31
+ writeMarker?: (marker: InstallFollowupMarker) => void;
32
+ now?: () => number;
33
+ }
34
+ export declare function runWindowsInstallFollowup(deps: InstallFollowupDeps): void;
35
+ //# sourceMappingURL=install-followup.d.ts.map