@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
@@ -53,6 +53,11 @@ export function processCommandLine(pid, deps = defaultCommandLineDeps()) {
53
53
  * The pid is alive and is an `hq daemon run`. After a reboot the pid in an old
54
54
  * lock file often belongs to an unrelated program; that must not keep the
55
55
  * daemon from starting.
56
+ *
57
+ * An unreadable command line counts as the daemon here, so a second daemon is
58
+ * never started next to one HQ cannot inspect. Never use this to decide a
59
+ * kill: use `verifyDaemonForKill`, which treats an unreadable command line as
60
+ * "not the daemon".
56
61
  */
57
62
  export function isDaemonProcess(pid, deps = { alive: (p) => isProcessAlive(p), commandLine: (p) => processCommandLine(p) }) {
58
63
  if (!deps.alive(pid))
@@ -60,6 +65,50 @@ export function isDaemonProcess(pid, deps = { alive: (p) => isProcessAlive(p), c
60
65
  const cmd = deps.commandLine(pid);
61
66
  return cmd === null || /\bdaemon\s+run\b/.test(cmd);
62
67
  }
68
+ /**
69
+ * PowerShell that prints a Windows process's creation time (UTC, ISO 8601) on
70
+ * the first line and its command line on the rest, or nothing when no process
71
+ * has that id. It uses only a cmdlet and DateTime methods, so it also runs in
72
+ * Constrained Language Mode. `-ErrorAction Stop` makes a WMI failure exit
73
+ * non-zero, so it reads as "could not read", never as "no such process".
74
+ */
75
+ export function windowsProcessIdentityScript(pid) {
76
+ return (`$p = Get-CimInstance Win32_Process -Filter "ProcessId=${Math.trunc(pid)}" -ErrorAction Stop; ` +
77
+ "if ($p) { $p.CreationDate.ToUniversalTime().ToString('o'); [string]$p.CommandLine }");
78
+ }
79
+ /** Seconds a daemon's creation time may follow its lock's ts (whole-second rounding and clock skew). */
80
+ const LOCK_TS_SLACK_SECONDS = 2;
81
+ /**
82
+ * Decide whether the Windows process in `owner` may be killed as the HQ
83
+ * daemon. Run this immediately before the kill. It is the daemon only when the
84
+ * process exists, its command line is readable and is `hq daemon run`, and it
85
+ * was created no later than the lock was written. A process created after the
86
+ * lock took over a pid the old daemon left behind. Anything that cannot be
87
+ * read is "unverified", and the caller must not kill.
88
+ */
89
+ export function verifyDaemonForKill(owner, run) {
90
+ const encoded = Buffer.from(windowsProcessIdentityScript(owner.pid), "utf16le").toString("base64");
91
+ const r = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded]);
92
+ if (r.status !== 0)
93
+ return { verdict: "unverified", reason: "HQ could not read the process" };
94
+ const lines = r.stdout.split(/\r?\n/).map((l) => l.trim());
95
+ while (lines.length > 0 && lines[lines.length - 1] === "")
96
+ lines.pop();
97
+ if (lines.length === 0)
98
+ return { verdict: "gone" };
99
+ const commandLine = lines.slice(1).join(" ").trim();
100
+ if (!commandLine)
101
+ return { verdict: "unverified", reason: "HQ could not read its command line" };
102
+ if (!/\bdaemon\s+run\b/.test(commandLine))
103
+ return { verdict: "unverified", reason: "it is not hq daemon run" };
104
+ const created = Date.parse(lines[0]);
105
+ if (!Number.isFinite(created))
106
+ return { verdict: "unverified", reason: "HQ could not read its start time" };
107
+ if (Math.floor(created / 1000) > owner.ts + LOCK_TS_SLACK_SECONDS) {
108
+ return { verdict: "unverified", reason: "it started after the daemon lock was written, so it is a different process" };
109
+ }
110
+ return { verdict: "daemon" };
111
+ }
63
112
  function claimTakeover(claim, pid, now) {
64
113
  for (let attempt = 0; attempt < 2; attempt++) {
65
114
  try {
@@ -130,9 +179,13 @@ export function releaseDaemonLock(lockPath, pid = process.pid) {
130
179
  if (owner?.pid === pid)
131
180
  fs.rmSync(lockPath, { force: true });
132
181
  }
182
+ /** The live supervisor's lock record (pid and start time), or null when none is running. */
183
+ export function liveDaemonLock(lockPath, alive = isDaemonProcess) {
184
+ const owner = readPidLock(lockPath);
185
+ return owner && alive(owner.pid) ? owner : null;
186
+ }
133
187
  /** The live supervisor's pid, or null when none is running. */
134
188
  export function runningDaemonPid(lockPath, alive = isDaemonProcess) {
135
- const owner = readPidLock(lockPath);
136
- return owner && alive(owner.pid) ? owner.pid : null;
189
+ return liveDaemonLock(lockPath, alive)?.pid ?? null;
137
190
  }
138
191
  //# sourceMappingURL=lock.js.map
@@ -4,9 +4,9 @@
4
4
  * File lives at ~/.hq/db/{company}/vault.db with WAL journal mode.
5
5
  * Never returns or logs remote connection strings.
6
6
  */
7
- import Database from "better-sqlite3";
7
+ import type BetterSqlite3 from "better-sqlite3";
8
8
  import { type LocalDbPathEnv } from "./paths.js";
9
- export type LocalDb = Database.Database;
9
+ export type LocalDb = BetterSqlite3.Database;
10
10
  export interface OpenLocalDbOptions extends LocalDbPathEnv {
11
11
  /** When true, do not create the file if missing (throws). Default false. */
12
12
  readonly?: boolean;
@@ -5,7 +5,7 @@
5
5
  * Never returns or logs remote connection strings.
6
6
  */
7
7
  import fs from "node:fs";
8
- import Database from "better-sqlite3";
8
+ import { loadBetterSqlite3 } from "./sqlite-binding.js";
9
9
  import { ensureLocalDbDir, resolveLocalDbPath, } from "./paths.js";
10
10
  const MIGRATIONS_TABLE = "hq_schema_migrations";
11
11
  /**
@@ -33,7 +33,7 @@ export function openLocalDb(companySlug, opts) {
33
33
  }
34
34
  ensureLocalDbDir(companySlug, opts);
35
35
  }
36
- const db = new Database(dbPath);
36
+ const db = new (loadBetterSqlite3())(dbPath);
37
37
  // WAL for concurrent readers; safe for agent/CLI workloads.
38
38
  db.pragma("journal_mode = WAL");
39
39
  db.pragma("foreign_keys = ON");
@@ -5,7 +5,7 @@
5
5
  * resolved company slug from HQ session/flags. Path overrides that open
6
6
  * another company's DB are denied unless an explicit dangerous flag is set.
7
7
  */
8
- import Database from "better-sqlite3";
8
+ import { loadBetterSqlite3 } from "./sqlite-binding.js";
9
9
  import { openLocalDb } from "./local.js";
10
10
  import { normalizeCompanySlugForLocalDb, resolveLocalDbPath, } from "./paths.js";
11
11
  const WRITE_PATTERN = /^\s*(INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|ALTER|TRUNCATE|REINDEX|VACUUM|BEGIN|COMMIT|ROLLBACK)\b/i;
@@ -49,7 +49,7 @@ export function resolveSqlDbPath(opts) {
49
49
  return opts.dbPathOverride;
50
50
  }
51
51
  function openByPath(dbPath) {
52
- const db = new Database(dbPath);
52
+ const db = new (loadBetterSqlite3())(dbPath);
53
53
  db.pragma("journal_mode = WAL");
54
54
  db.pragma("foreign_keys = ON");
55
55
  return db;
@@ -0,0 +1,22 @@
1
+ import type BetterSqlite3 from "better-sqlite3";
2
+ import { type SpawnPlan } from "../../utils/windows-spawn.js";
3
+ export type BetterSqlite3Constructor = typeof BetterSqlite3;
4
+ export declare class SqliteBindingError extends Error {
5
+ constructor(cause?: unknown);
6
+ }
7
+ export interface LoadBetterSqlite3Options {
8
+ importDatabase?: () => BetterSqlite3Constructor;
9
+ /** Return true when the native binary is ready to load again. */
10
+ repair?: () => boolean;
11
+ }
12
+ export declare function resetSqliteBindingForTests(): void;
13
+ export declare function clearBetterSqlite3RequireCache(): void;
14
+ /**
15
+ * A `.js` bin runs under this Node. A `.cmd` shim goes through the escaped
16
+ * cmd.exe plan, so a profile path with spaces or `&` stays one argument.
17
+ */
18
+ export declare function planRepairToolSpawn(cmd: string, args: readonly string[], env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): SpawnPlan;
19
+ /** Download or build better-sqlite3's native binary inside its own package directory. */
20
+ export declare function repairBetterSqlite3(): boolean;
21
+ export declare function loadBetterSqlite3(options?: LoadBetterSqlite3Options): BetterSqlite3Constructor;
22
+ //# sourceMappingURL=sqlite-binding.d.ts.map
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Load better-sqlite3 without requiring the user to approve npm scripts.
3
+ * npm 11 skips dependency install scripts, so the prebuilt binary is often
4
+ * missing after `npm install -g`. Try a one-shot prebuild download from the
5
+ * installed package, then surface the exact allow-scripts command.
6
+ */
7
+ import { spawnSync } from "node:child_process";
8
+ import fs from "node:fs";
9
+ import { createRequire } from "node:module";
10
+ import path from "node:path";
11
+ import { planResolvedCommandSpawn } from "../../utils/windows-spawn.js";
12
+ const require = createRequire(import.meta.url);
13
+ export class SqliteBindingError extends Error {
14
+ constructor(cause) {
15
+ super("better-sqlite3 did not load. npm 11.16 and later skip its install script unless scripts are allowed, so the prebuilt binary was not downloaded. " +
16
+ "HQ tried to download the prebuild and it still failed. " +
17
+ "Fix: npm install -g @indigoai-us/hq-cli --allow-scripts=better-sqlite3");
18
+ this.name = "SqliteBindingError";
19
+ if (cause !== undefined)
20
+ this.cause = cause;
21
+ }
22
+ }
23
+ let cached;
24
+ let failed;
25
+ let defaultRepairAttempted = false;
26
+ export function resetSqliteBindingForTests() {
27
+ cached = undefined;
28
+ failed = undefined;
29
+ defaultRepairAttempted = false;
30
+ }
31
+ function isNativeBindingError(error) {
32
+ const record = error;
33
+ const text = error instanceof Error ? `${record?.code ?? ""} ${error.message}` : String(error);
34
+ return /better_sqlite3\.node|could not locate the bindings file|NODE_MODULE_VERSION|was compiled against a different node|not a valid win32 application|dlopen|ERR_DLOPEN_FAILED|MODULE_NOT_FOUND/i.test(text);
35
+ }
36
+ export function clearBetterSqlite3RequireCache() {
37
+ for (const id of Object.keys(require.cache)) {
38
+ if (id.includes(`${path.sep}better-sqlite3${path.sep}`))
39
+ delete require.cache[id];
40
+ }
41
+ }
42
+ function defaultImport() {
43
+ return require("better-sqlite3");
44
+ }
45
+ function resolveTool(dir, tool) {
46
+ const names = process.platform === "win32" ? [`${tool}.cmd`, `${tool}.exe`, tool] : [tool];
47
+ const bins = [path.join(path.dirname(dir), ".bin"), path.join(dir, "node_modules", ".bin")];
48
+ for (const bin of bins) {
49
+ for (const name of names) {
50
+ const candidate = path.join(bin, name);
51
+ if (fs.existsSync(candidate))
52
+ return candidate;
53
+ }
54
+ }
55
+ try {
56
+ const localRequire = createRequire(path.join(dir, "package.json"));
57
+ const pkgJson = localRequire.resolve(`${tool}/package.json`);
58
+ const pkg = JSON.parse(fs.readFileSync(pkgJson, "utf8"));
59
+ const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.[tool];
60
+ if (typeof rel === "string")
61
+ return path.join(path.dirname(pkgJson), rel);
62
+ }
63
+ catch {
64
+ // The tool is not installed next to better-sqlite3.
65
+ }
66
+ return null;
67
+ }
68
+ /**
69
+ * A `.js` bin runs under this Node. A `.cmd` shim goes through the escaped
70
+ * cmd.exe plan, so a profile path with spaces or `&` stays one argument.
71
+ */
72
+ export function planRepairToolSpawn(cmd, args, env = process.env, platform = process.platform) {
73
+ if (cmd.endsWith(".js"))
74
+ return { cmd: process.execPath, args: [cmd, ...args], shell: false };
75
+ return planResolvedCommandSpawn(cmd, args, env, platform);
76
+ }
77
+ function runTool(cmd, args, cwd) {
78
+ const plan = planRepairToolSpawn(cmd, args);
79
+ spawnSync(plan.cmd, plan.args, {
80
+ cwd,
81
+ timeout: 60_000,
82
+ windowsHide: true,
83
+ stdio: "ignore",
84
+ shell: plan.shell,
85
+ ...(plan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
86
+ });
87
+ }
88
+ /** Download or build better-sqlite3's native binary inside its own package directory. */
89
+ export function repairBetterSqlite3() {
90
+ let dir;
91
+ try {
92
+ dir = path.dirname(require.resolve("better-sqlite3/package.json"));
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ const artifact = path.join(dir, "build", "Release", "better_sqlite3.node");
98
+ if (fs.existsSync(artifact))
99
+ return true;
100
+ const prebuild = resolveTool(dir, "prebuild-install");
101
+ if (prebuild)
102
+ runTool(prebuild, [], dir);
103
+ if (fs.existsSync(artifact))
104
+ return true;
105
+ const nodeGyp = resolveTool(dir, "node-gyp");
106
+ if (nodeGyp)
107
+ runTool(nodeGyp, ["rebuild", "--release"], dir);
108
+ return fs.existsSync(artifact);
109
+ }
110
+ function attemptRepair(options) {
111
+ if (options.repair) {
112
+ try {
113
+ return options.repair();
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ if (defaultRepairAttempted)
120
+ return false;
121
+ defaultRepairAttempted = true;
122
+ try {
123
+ return repairBetterSqlite3();
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ }
129
+ export function loadBetterSqlite3(options = {}) {
130
+ const injected = options.importDatabase !== undefined;
131
+ if (!injected && cached)
132
+ return cached;
133
+ if (!injected && failed)
134
+ throw failed;
135
+ const importDatabase = options.importDatabase ?? defaultImport;
136
+ const load = () => {
137
+ const ctor = importDatabase();
138
+ if (!injected)
139
+ cached = ctor;
140
+ return ctor;
141
+ };
142
+ try {
143
+ return load();
144
+ }
145
+ catch (error) {
146
+ if (!isNativeBindingError(error))
147
+ throw error;
148
+ const repaired = attemptRepair(options);
149
+ if (!repaired) {
150
+ const wrapped = new SqliteBindingError(error);
151
+ if (!injected)
152
+ failed = wrapped;
153
+ throw wrapped;
154
+ }
155
+ if (!injected)
156
+ clearBetterSqlite3RequireCache();
157
+ try {
158
+ return load();
159
+ }
160
+ catch (retryError) {
161
+ const wrapped = new SqliteBindingError(retryError);
162
+ if (!injected)
163
+ failed = wrapped;
164
+ throw wrapped;
165
+ }
166
+ }
167
+ }
168
+ //# sourceMappingURL=sqlite-binding.js.map
@@ -3,6 +3,7 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { runDoctor as runDoctorCommand, } from "../../../commands/doctor.js";
5
5
  import { createAnywhereFamily } from "../checks/anywhere.js";
6
+ import { createTempHomesFamily } from "../checks/temp-homes.js";
6
7
  import { createDefaultRegistry } from "../registry.js";
7
8
  /**
8
9
  * Run doctor tests against synthetic state. The production registry normally
@@ -45,6 +46,9 @@ export async function runDoctorForTest(options = {}) {
45
46
  // Command tests exercise doctor rendering and exit behavior offline. The
46
47
  // real person preference is covered in the anywhere-family tests.
47
48
  registry.register(createAnywhereFamily({ personSettingReader: async () => false }));
49
+ // The temp-homes family walks the machine's real temp folders; scan
50
+ // none here. Its behaviour is covered with fake trees in its own tests.
51
+ registry.register(createTempHomesFamily({ roots: [] }));
48
52
  }
49
53
  return await runDoctorCommand({ ...options, registry, integrations: false });
50
54
  }
@@ -17,6 +17,7 @@ export declare const REMEDIATION: {
17
17
  readonly repairCodexSkills: "hq uninstall --global --runtime codex && hq install --global --runtime codex";
18
18
  readonly daemonInstall: "hq daemon install";
19
19
  readonly daemonRestart: "hq daemon restart --daemon";
20
+ readonly daemonStatus: "hq daemon status";
20
21
  readonly link: "hq link <repo-dir> <company>";
21
22
  };
22
23
  interface DaemonServiceState {
@@ -45,10 +46,24 @@ export interface AnywhereDeps {
45
46
  /** The hq-cli version installed on this machine. Default: this CLI's version. */
46
47
  cliVersion?: string;
47
48
  serviceState?: () => DaemonServiceState;
49
+ /** Is an hqd process alive? Default: the daemon lock names a live hqd pid. */
50
+ daemonRunning?: () => boolean;
51
+ /** Transport to report when health throws. Production uses the platform default. */
52
+ unreachableTransport?: "windows_named_pipe" | "unix_socket";
48
53
  now?: () => Date;
49
54
  }
55
+ export interface PipeConnectFailure {
56
+ code?: string;
57
+ detail?: string;
58
+ }
50
59
  export declare function createAnywhereFamily(deps?: AnywhereDeps): CheckFamily;
51
60
  export declare const anywhereFamily: CheckFamily;
61
+ /**
62
+ * What to say when the Windows named pipe cannot be used. The old wording
63
+ * guessed an ACL refusal whenever any process looked like a daemon. This
64
+ * reports the connect error that was actually observed.
65
+ */
66
+ export declare function windowsNamedPipeUnreachableMessage(socketPath: string, daemonRunning: boolean, failure?: PipeConnectFailure): string;
52
67
  export declare function defaultServiceState(home: string, options?: DaemonServiceProbeOptions): DaemonServiceState;
53
68
  export {};
54
69
  //# sourceMappingURL=anywhere.d.ts.map
@@ -23,8 +23,10 @@ import { daemonPresenceLabel, heartbeatFile, readHeartbeat } from "../../daemon/
23
23
  import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK } from "../../daemon/install.js";
24
24
  import { hostedDaemonRunning } from "../../daemon/handoff.js";
25
25
  import { daemonPaths } from "../../daemon/paths.js";
26
+ import { runningDaemonPid } from "../../daemon/lock.js";
26
27
  import { hqdSocketPath } from "../../daemon/protocol.js";
27
28
  import { readRegistry, registryDir, registryFile } from "../../registry/registry.js";
29
+ import { skillCopySource } from "../../../utils/safe-link.js";
28
30
  import { anywhereRuntimeEnabled } from "../../../commands/anywhere-runtime-flag.js";
29
31
  import { readHqAnywherePersonSetting } from "../../anywhere-person-setting.js";
30
32
  const ANYWHERE_FAMILY_ID = "anywhere";
@@ -35,6 +37,7 @@ export const REMEDIATION = {
35
37
  repairCodexSkills: "hq uninstall --global --runtime codex && hq install --global --runtime codex",
36
38
  daemonInstall: "hq daemon install",
37
39
  daemonRestart: "hq daemon restart --daemon",
40
+ daemonStatus: "hq daemon status",
38
41
  link: "hq link <repo-dir> <company>",
39
42
  };
40
43
  export function createAnywhereFamily(deps = {}) {
@@ -60,6 +63,7 @@ async function runAnywhereChecks(context, deps = {}) {
60
63
  : result(personSetting ? "PASS" : "WARN", "person-setting", `HQ Anywhere person setting is ${personSetting ? "on" : "off"}`);
61
64
  let health;
62
65
  let healthUnsupported = false;
66
+ let connectFailure;
63
67
  try {
64
68
  if (deps.health) {
65
69
  health = await deps.health(cwd);
@@ -104,12 +108,14 @@ async function runAnywhereChecks(context, deps = {}) {
104
108
  };
105
109
  healthUnsupported = true;
106
110
  }
107
- catch {
108
- health = unreachableHealth(socketPath);
111
+ catch (inner) {
112
+ health = unreachableHealth(socketPath, deps.unreachableTransport);
113
+ connectFailure = connectFailureFrom(inner);
109
114
  }
110
115
  }
111
116
  else {
112
- health = unreachableHealth(socketPath);
117
+ health = unreachableHealth(socketPath, deps.unreachableTransport);
118
+ connectFailure = connectFailureFrom(error);
113
119
  }
114
120
  }
115
121
  const claude = claudeTargets(home);
@@ -118,7 +124,12 @@ async function runAnywhereChecks(context, deps = {}) {
118
124
  const serviceState = runtimeEnabled && installed
119
125
  ? (deps.serviceState ?? (() => defaultServiceState(home)))()
120
126
  : undefined;
121
- const healthCheck = runtimeHealthCheck(health, healthUnsupported, serviceState);
127
+ // A missing Windows pipe usually means hqd never started; only blame the
128
+ // pipe ACL when a daemon process is actually alive.
129
+ const daemonRunning = health.reachable
130
+ ? true
131
+ : (deps.daemonRunning ?? (() => runningDaemonPid(daemonPaths(home, env).lockPath) !== null))();
132
+ const healthCheck = runtimeHealthCheck(health, healthUnsupported, serviceState, daemonRunning, connectFailure);
122
133
  if (!runtimeEnabled)
123
134
  return [healthCheck, settingCheck];
124
135
  const remoteModes = [
@@ -169,17 +180,67 @@ async function runAnywhereChecks(context, deps = {}) {
169
180
  out.push(checkSkillLinks("codex", codex.skillsDir, REMEDIATION.installCodex, REMEDIATION.repairCodexSkills));
170
181
  return out;
171
182
  }
172
- function unreachableHealth(socketPath) {
183
+ function unreachableHealth(socketPath, transport) {
173
184
  return {
174
185
  reachable: false,
175
186
  version: null,
176
187
  uptimeMs: null,
177
188
  flag: { enabled: null, lastSuccessfulReadAgeMs: null, heldThroughReadError: false },
178
189
  resolution: { scope: "unknown", company: null, fallbackReason: null },
179
- socket: { path: socketPath, transport: process.platform === "win32" ? "windows_named_pipe" : "unix_socket", mode: null, ownerMatchesUser: null, userOnly: null },
190
+ socket: {
191
+ path: socketPath,
192
+ transport: transport ?? (process.platform === "win32" ? "windows_named_pipe" : "unix_socket"),
193
+ mode: null,
194
+ ownerMatchesUser: null,
195
+ userOnly: null,
196
+ },
180
197
  };
181
198
  }
182
- function runtimeHealthCheck(health, healthUnsupported = false, serviceState) {
199
+ function connectFailureFrom(error) {
200
+ if (error instanceof HqdClientError)
201
+ return { code: error.code, detail: error.message };
202
+ if (error instanceof Error) {
203
+ const code = error.code;
204
+ return { code: typeof code === "string" ? code : undefined, detail: error.message };
205
+ }
206
+ return { detail: String(error) };
207
+ }
208
+ function failureSuffix(failure) {
209
+ if (!failure)
210
+ return "";
211
+ const parts = [failure.code, failure.detail].filter((part) => Boolean(part && part.trim()));
212
+ return parts.length === 0 ? "" : ` (${parts.join(": ")})`;
213
+ }
214
+ function accessDenied(failure) {
215
+ const code = (failure?.code ?? "").toUpperCase();
216
+ if (code === "EACCES" || code === "EPERM")
217
+ return true;
218
+ return /access is denied|unsafe acl/i.test(failure?.detail ?? "");
219
+ }
220
+ /**
221
+ * What to say when the Windows named pipe cannot be used. The old wording
222
+ * guessed an ACL refusal whenever any process looked like a daemon. This
223
+ * reports the connect error that was actually observed.
224
+ */
225
+ export function windowsNamedPipeUnreachableMessage(socketPath, daemonRunning, failure) {
226
+ const suffix = failureSuffix(failure);
227
+ if (!daemonRunning) {
228
+ return `hqd is not running, so its named pipe ${socketPath} does not exist${suffix}. Run ${REMEDIATION.daemonStatus} to see why it has not started.`;
229
+ }
230
+ if (accessDenied(failure)) {
231
+ return `hqd is running but access to named pipe ${socketPath} was denied${suffix}. The pipe ACL does not allow this user. Run ${REMEDIATION.daemonRestart} to recreate the pipe.`;
232
+ }
233
+ const code = (failure?.code ?? "").toUpperCase();
234
+ if (code === "ENOENT") {
235
+ return `hqd is running but named pipe ${socketPath} does not exist${suffix}. Run ${REMEDIATION.daemonRestart} to recreate the pipe.`;
236
+ }
237
+ if (code === "ETIMEDOUT") {
238
+ return `hqd is running but did not answer on named pipe ${socketPath}${suffix}. Run ${REMEDIATION.daemonStatus}, then ${REMEDIATION.daemonRestart}.`;
239
+ }
240
+ const observed = suffix || " (observed)";
241
+ return `hqd is not reachable on ${socketPath}${observed}. HQ did not see an access-denied error, so this is not a named-pipe ACL refusal. Run ${REMEDIATION.daemonStatus} to see why, then ${REMEDIATION.daemonRestart}.`;
242
+ }
243
+ function runtimeHealthCheck(health, healthUnsupported = false, serviceState, daemonRunning = true, failure) {
183
244
  const { flag, resolution, socket } = health;
184
245
  const age = flag.lastSuccessfulReadAgeMs === null
185
246
  ? "unknown"
@@ -195,8 +256,9 @@ function runtimeHealthCheck(health, healthUnsupported = false, serviceState) {
195
256
  const socketSecurity = socket.transport === "windows_named_pipe"
196
257
  ? "Windows named pipe (mode and owner are not reported)"
197
258
  : `mode ${socket.mode ?? "unknown"}; owner matches current user: ${socket.ownerMatchesUser === null ? "unknown" : socket.ownerMatchesUser ? "yes" : "no"}; user-only: ${socket.userOnly === null ? "unknown" : socket.userOnly ? "yes" : "no"}`;
259
+ const pipeWithoutDaemon = socket.transport === "windows_named_pipe" && !daemonRunning;
198
260
  const unreachableMessage = socket.transport === "windows_named_pipe"
199
- ? `hqd is not reachable on ${socket.path}; secure Windows named-pipe setup may have refused an unsafe ACL`
261
+ ? windowsNamedPipeUnreachableMessage(socket.path, daemonRunning, failure)
200
262
  : `hqd is not reachable on ${socket.path}; version, uptime, flag state, and cwd binding are unavailable`;
201
263
  const socketNotUserOnly = health.reachable && socket.transport === "unix_socket" && (socket.userOnly !== true || socket.ownerMatchesUser !== true);
202
264
  const warningNotes = [
@@ -207,7 +269,8 @@ function runtimeHealthCheck(health, healthUnsupported = false, serviceState) {
207
269
  ? `hqd reachable; version ${health.version ?? "unknown"}; uptime ${health.uptimeMs === null ? "unknown" : `${new Intl.NumberFormat("en-US").format(health.uptimeMs)} ms`}; runtime flag ${flagLabel}; last successful flag read ${age} ago; held through read error: ${flag.heldThroughReadError ? "yes" : "no"}; cwd binding ${binding}; socket ${socket.path}; ${socketSecurity}${warningNotes.length ? `; ${warningNotes.join("; ")}` : ""}`
208
270
  : unreachableMessage;
209
271
  const remediation = !health.reachable && serviceState
210
- ? serviceState.installed ? REMEDIATION.daemonRestart : REMEDIATION.daemonInstall
272
+ ? !serviceState.installed ? REMEDIATION.daemonInstall
273
+ : pipeWithoutDaemon ? REMEDIATION.daemonStatus : REMEDIATION.daemonRestart
211
274
  : healthUnsupported || socketNotUserOnly ? REMEDIATION.daemonRestart : undefined;
212
275
  return {
213
276
  status: health.reachable && warningNotes.length === 0 ? "PASS" : "WARN",
@@ -336,12 +399,15 @@ function checkSkillLinks(runtime, dir, fix, repair = fix) {
336
399
  return result("WARN", id, `${dir} does not exist`, fix, dir);
337
400
  }
338
401
  const links = names.filter((n) => fs.lstatSync(path.join(dir, n)).isSymbolicLink());
339
- if (links.length === 0)
402
+ // Windows falls back to marked copies when it refuses both link types.
403
+ const copies = names.filter((n) => skillCopySource(path.join(dir, n)) !== null);
404
+ if (links.length === 0 && copies.length === 0)
340
405
  return result("WARN", id, `no hq-* skill links in ${dir}`, fix, dir);
341
406
  const broken = links.filter((n) => !fs.existsSync(path.join(dir, n)));
342
407
  if (broken.length > 0)
343
408
  return result("FAIL", id, `${broken.length} broken skill links in ${dir}: ${broken.slice(0, 5).join(", ")}`, repair, dir);
344
- return result("PASS", id, `${links.length} hq-* skill links in ${dir} resolve`, undefined, dir);
409
+ const copied = copies.length > 0 ? ` (${copies.length} more are copies that do not follow HQ skill updates)` : "";
410
+ return result("PASS", id, `${links.length} hq-* skill links in ${dir} resolve${copied}`, undefined, dir);
345
411
  }
346
412
  export function defaultServiceState(home, options = {}) {
347
413
  const platform = options.platform ?? process.platform;
@@ -0,0 +1,12 @@
1
+ import { type ShimPathCheck } from "../../windows/user-path.js";
2
+ import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
3
+ export declare const CLI_PATH_FAMILY_ID = "cli-path";
4
+ export declare const CLI_PATH_CHECK_ID = "cli-path.npm-shim-on-path";
5
+ /**
6
+ * Windows: is the folder holding the npm `hq.cmd` shim on the PATH a new
7
+ * terminal gets? This check only reads. The CLI adds the folder to HKCU Path
8
+ * on startup; the warning is the fallback when that add did not stick.
9
+ */
10
+ export declare const cliPathFamily: CheckFamily;
11
+ export declare function checkCliPath(_context: CheckContext, check?: () => ShimPathCheck | null): CheckResult[];
12
+ //# sourceMappingURL=cli-path.d.ts.map
@@ -0,0 +1,46 @@
1
+ import { checkShimOnPath, pathFixCommand, shimPathWarning } from "../../windows/user-path.js";
2
+ export const CLI_PATH_FAMILY_ID = "cli-path";
3
+ export const CLI_PATH_CHECK_ID = "cli-path.npm-shim-on-path";
4
+ /**
5
+ * Windows: is the folder holding the npm `hq.cmd` shim on the PATH a new
6
+ * terminal gets? This check only reads. The CLI adds the folder to HKCU Path
7
+ * on startup; the warning is the fallback when that add did not stick.
8
+ */
9
+ export const cliPathFamily = {
10
+ id: CLI_PATH_FAMILY_ID,
11
+ title: "hq command on PATH",
12
+ run: (context) => Promise.resolve(checkCliPath(context)),
13
+ };
14
+ export function checkCliPath(_context, check = () => checkShimOnPath()) {
15
+ const result = check();
16
+ if (!result) {
17
+ return [
18
+ {
19
+ status: "NA",
20
+ checkId: CLI_PATH_CHECK_ID,
21
+ message: "Not a Windows npm global install, so there is no hq.cmd shim folder to check.",
22
+ },
23
+ ];
24
+ }
25
+ if (result.onPath) {
26
+ return [
27
+ {
28
+ status: "PASS",
29
+ checkId: CLI_PATH_CHECK_ID,
30
+ target: result.shimDir,
31
+ message: `The hq command folder ${result.shimDir} is on your Windows PATH.`,
32
+ },
33
+ ];
34
+ }
35
+ return [
36
+ {
37
+ status: "WARN",
38
+ checkId: CLI_PATH_CHECK_ID,
39
+ target: result.shimDir,
40
+ message: shimPathWarning(result)[0],
41
+ details: { source: result.source },
42
+ remediation: `In PowerShell, then open a new terminal: ${pathFixCommand(result.shimDir)}`,
43
+ },
44
+ ];
45
+ }
46
+ //# sourceMappingURL=cli-path.js.map
@@ -0,0 +1,4 @@
1
+ import type { CheckFamily, CheckResult } from '../types.js';
2
+ export declare function checkHookEngine(): CheckResult;
3
+ export declare const hookEngineFamily: CheckFamily;
4
+ //# sourceMappingURL=hook-engine.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { launcherEnabled } from '../../hook-engine/registration.js';
2
+ import { findHookEngine, hookEngineRequired, pinnedHookEngineMatches, HOOK_ENGINE_REPAIR, HOOK_ENGINE_REPAIR_PIN } from '../../hook-engine/resolve.js';
3
+ export function checkHookEngine() {
4
+ const required = hookEngineRequired();
5
+ const fallback = required ? 'Native engine is required; repair before running hooks.' : 'Existing bash hooks remain active.';
6
+ try {
7
+ const engine = findHookEngine();
8
+ if (engine && pinnedHookEngineMatches(engine))
9
+ return { checkId: 'hooks.native-engine', status: 'PASS', target: engine.pinned, message: `Native hook engine ${engine.version} installed at its version-pinned path. Launcher flag: ${launcherEnabled() ? 'on (run hq reindex to reconcile settings)' : 'off (bash registration)'}.` };
10
+ return { checkId: 'hooks.native-engine', status: required ? 'FAIL' : 'WARN', message: `Native hook engine is missing or damaged. ${fallback}`, remediation: engine ? `Close HQ sessions, then run ${HOOK_ENGINE_REPAIR_PIN}` : HOOK_ENGINE_REPAIR };
11
+ }
12
+ catch (error) {
13
+ return { checkId: 'hooks.native-engine', status: required ? 'FAIL' : 'WARN', message: `Native hook engine unavailable: ${error.message}. ${fallback}`, remediation: HOOK_ENGINE_REPAIR };
14
+ }
15
+ }
16
+ export const hookEngineFamily = { id: 'hook-engine', title: 'Native hook engine', run: async () => [checkHookEngine()] };
17
+ //# sourceMappingURL=hook-engine.js.map
@@ -100,6 +100,13 @@ export interface SyncHealthDeps {
100
100
  * unhealthy-sync signal, and `hq doctor --fix` has nothing local to refresh.
101
101
  */
102
102
  localCompanyExists?: (slug: string) => boolean;
103
+ /**
104
+ * Whether HQ_SYNC_MANIFEST_DISABLED turns manifest uploads off on this
105
+ * machine. Defaults to reading the environment the way `hq sync manifest`
106
+ * does. When true a stale row names the switch and is not auto-fixable,
107
+ * since a re-upload would exit 0 without uploading.
108
+ */
109
+ manifestUploadsDisabled?: () => boolean;
103
110
  }
104
111
  /**
105
112
  * True when `slug` is a filesystem-safe company folder name. Rejects empty