@indigoai-us/hq-cli 5.345.146 → 5.345.148

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 (138) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +14 -0
  3. package/assets/bot-workers/setup/worker.yaml +11 -0
  4. package/dist/command-catalog.generated.d.ts +326 -4
  5. package/dist/command-catalog.generated.js +403 -4
  6. package/dist/commands/agent-inbox.js +2 -1
  7. package/dist/commands/auth.js +4 -2
  8. package/dist/commands/bot.js +1 -1
  9. package/dist/commands/cloud.js +31 -1
  10. package/dist/commands/core-refresh-vault-access-native.d.ts +17 -0
  11. package/dist/commands/core-refresh-vault-access-native.js +214 -19
  12. package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
  13. package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
  14. package/dist/commands/core.js +6 -0
  15. package/dist/commands/daemon.d.ts +7 -0
  16. package/dist/commands/daemon.js +37 -3
  17. package/dist/commands/dm.js +9 -2
  18. package/dist/commands/doctor.d.ts +11 -1
  19. package/dist/commands/doctor.js +56 -1
  20. package/dist/commands/files.js +10 -6
  21. package/dist/commands/install-global.d.ts +7 -0
  22. package/dist/commands/install-global.js +129 -35
  23. package/dist/commands/lanes-shared.d.ts +3 -1
  24. package/dist/commands/lanes.d.ts +4 -4
  25. package/dist/commands/lanes.js +380 -25
  26. package/dist/commands/login.js +4 -3
  27. package/dist/commands/pack-install.d.ts +2 -2
  28. package/dist/commands/pack-install.js +133 -35
  29. package/dist/commands/sync-manifest.d.ts +2 -9
  30. package/dist/commands/sync-manifest.js +4 -15
  31. package/dist/hook-engine-path.d.ts +4 -0
  32. package/dist/hook-engine-path.js +36 -0
  33. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2784 -2360
  34. package/dist/hq-cloud-sync-runner/package.json +1 -1
  35. package/dist/index.js +6 -0
  36. package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
  37. package/dist/lib/agent-kit/inbox-redact.js +361 -0
  38. package/dist/lib/agent-kit/inbox-state.js +11 -2
  39. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  40. package/dist/lib/agent-kit/run/inbox.js +36 -7
  41. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
  42. package/dist/lib/anywhere-runtime-flag-key.js +9 -0
  43. package/dist/lib/bot/daemon.js +3 -0
  44. package/dist/lib/bot/runtime/index.js +3 -1
  45. package/dist/lib/company-skill-wrapper.js +6 -4
  46. package/dist/lib/daemon/install.d.ts +47 -1
  47. package/dist/lib/daemon/install.js +181 -15
  48. package/dist/lib/daemon/lock.d.ts +32 -0
  49. package/dist/lib/daemon/lock.js +55 -2
  50. package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
  51. package/dist/lib/doctor/checks/anywhere.d.ts +3 -0
  52. package/dist/lib/doctor/checks/anywhere.js +23 -8
  53. package/dist/lib/doctor/checks/cli-path.d.ts +11 -0
  54. package/dist/lib/doctor/checks/cli-path.js +45 -0
  55. package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
  56. package/dist/lib/doctor/checks/hook-engine.js +16 -0
  57. package/dist/lib/doctor/checks/release-links.d.ts +23 -0
  58. package/dist/lib/doctor/checks/release-links.js +152 -0
  59. package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
  60. package/dist/lib/doctor/checks/sync-health.js +38 -2
  61. package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
  62. package/dist/lib/doctor/checks/temp-homes.js +54 -0
  63. package/dist/lib/doctor/fix/apply.d.ts +90 -1
  64. package/dist/lib/doctor/fix/apply.js +176 -10
  65. package/dist/lib/doctor/fix/json-output.d.ts +4 -1
  66. package/dist/lib/doctor/fix/json-output.js +6 -0
  67. package/dist/lib/doctor/fix/remediation.d.ts +7 -1
  68. package/dist/lib/doctor/fix/remediation.js +23 -1
  69. package/dist/lib/doctor/registry.js +13 -0
  70. package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
  71. package/dist/lib/doctor/temp-home-sweeper.js +418 -0
  72. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  73. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  74. package/dist/lib/flag-registry.js +9 -1
  75. package/dist/lib/flag-snapshot-file.js +8 -3
  76. package/dist/lib/hook-engine/resolve.d.ts +31 -0
  77. package/dist/lib/hook-engine/resolve.js +134 -0
  78. package/dist/lib/lanes/admission-host.d.ts +1 -0
  79. package/dist/lib/lanes/admission-host.js +8 -1
  80. package/dist/lib/lanes/admission.d.ts +4 -0
  81. package/dist/lib/lanes/admission.js +6 -2
  82. package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
  83. package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
  84. package/dist/lib/lanes/index.d.ts +2 -2
  85. package/dist/lib/lanes/index.js +1 -1
  86. package/dist/lib/lanes/keep-alive.js +4 -1
  87. package/dist/lib/lanes/local-host-identity.d.ts +40 -0
  88. package/dist/lib/lanes/local-host-identity.js +314 -0
  89. package/dist/lib/lanes/reaper.d.ts +1 -1
  90. package/dist/lib/lanes/reaper.js +7 -0
  91. package/dist/lib/lanes/reconcile.d.ts +4 -0
  92. package/dist/lib/lanes/reconcile.js +16 -8
  93. package/dist/lib/lanes/spawn.d.ts +20 -0
  94. package/dist/lib/lanes/spawn.js +48 -2
  95. package/dist/lib/lanes/status-fields.js +6 -1
  96. package/dist/lib/lanes/store.js +8 -1
  97. package/dist/lib/lanes/throttle.d.ts +49 -6
  98. package/dist/lib/lanes/throttle.js +154 -17
  99. package/dist/lib/lanes/types.d.ts +22 -1
  100. package/dist/lib/lanes/types.js +27 -3
  101. package/dist/lib/lanes/watcher.js +27 -2
  102. package/dist/lib/lanes/worker-continuity.d.ts +2 -0
  103. package/dist/lib/lanes/worker-continuity.js +18 -11
  104. package/dist/lib/manifest-disabled.d.ts +13 -0
  105. package/dist/lib/manifest-disabled.js +19 -0
  106. package/dist/lib/service-manager/launchd.d.ts +1 -0
  107. package/dist/lib/service-manager/launchd.js +1 -1
  108. package/dist/lib/windows/user-path.d.ts +48 -0
  109. package/dist/lib/windows/user-path.js +114 -0
  110. package/dist/lib/work-context/auto-bind.d.ts +1 -1
  111. package/dist/lib/work-context/auto-bind.js +22 -3
  112. package/dist/lib/work-context/lane-bind.d.ts +31 -0
  113. package/dist/lib/work-context/lane-bind.js +101 -0
  114. package/dist/lib/work-context/organize.js +1 -0
  115. package/dist/lib/work-context/prd-sync.js +7 -0
  116. package/dist/lib/work-context/project.d.ts +36 -10
  117. package/dist/lib/work-context/project.js +130 -41
  118. package/dist/lib/work-context/reconcile.d.ts +6 -0
  119. package/dist/lib/work-context/reconcile.js +25 -5
  120. package/dist/lib/work-context/state.d.ts +6 -0
  121. package/dist/lib/work-context/state.js +29 -1
  122. package/dist/lib/workers-registry/index.d.ts +6 -0
  123. package/dist/lib/workers-registry/index.js +8 -0
  124. package/dist/utils/copy-tree.d.ts +61 -0
  125. package/dist/utils/copy-tree.js +249 -0
  126. package/dist/utils/environmental-error.js +7 -3
  127. package/dist/utils/headless.d.ts +2 -0
  128. package/dist/utils/headless.js +8 -0
  129. package/dist/utils/login-browser.d.ts +4 -0
  130. package/dist/utils/login-browser.js +122 -0
  131. package/dist/utils/npm-invocation.d.ts +27 -0
  132. package/dist/utils/npm-invocation.js +54 -0
  133. package/dist/utils/safe-link.d.ts +50 -0
  134. package/dist/utils/safe-link.js +300 -0
  135. package/dist/utils/team-upgrade.js +1 -7
  136. package/package.json +17 -5
  137. package/scripts/hook-engine-manifest.mjs +22 -0
  138. package/scripts/install-hook-engine.mjs +10 -0
@@ -16,6 +16,7 @@
16
16
  * never touch launchctl, systemctl or schtasks.
17
17
  */
18
18
  import { type RunCommand, type SleepFn } from "../service-manager/launchd.js";
19
+ import { type PidLockRecord } from "../mesh/live/daemon/pid-lock.js";
19
20
  import type { MachineType } from "./machine-type.js";
20
21
  export declare const DAEMON_LAUNCHD_LABEL = "ai.getindigo.hq-daemon";
21
22
  export declare const DAEMON_SYSTEMD_UNIT = "hq-daemon.service";
@@ -60,12 +61,28 @@ export interface DaemonUnitResult {
60
61
  running: boolean;
61
62
  error?: string;
62
63
  manualCommand?: string;
64
+ /**
65
+ * Windows: the task is registered but no new daemon process appeared within
66
+ * WINDOWS_START_WAIT_MS of `schtasks /Run`. With no interactive session the
67
+ * logon trigger starts it at the next sign-in; with one, it did not start.
68
+ * The caller decides which, since only it can check the session.
69
+ */
70
+ startPending?: true;
71
+ /** Windows: an older daemon may still be running; HQ could not verify it, so did not stop it. */
72
+ warning?: string;
63
73
  }
64
74
  export interface InstallDeps {
65
75
  run: RunCommand;
66
- /** Blocking sleep for the launchd reload wait (tests inject). */
76
+ /** Blocking sleep for the launchd reload wait and the Windows start wait (tests inject). */
67
77
  sleep?: SleepFn;
78
+ /**
79
+ * The running daemon's lock record (pid and start time), or null. Default:
80
+ * the daemon lock, when it names a live `hq daemon run` process.
81
+ */
82
+ runningDaemon?: () => PidLockRecord | null;
68
83
  }
84
+ /** How long install waits for the Windows task to start a daemon process. */
85
+ export declare const WINDOWS_START_WAIT_MS = 10000;
69
86
  export declare const ROOT_REFUSAL = "hq daemon runs its services as the machine's login user, never as root.";
70
87
  /**
71
88
  * Why the daemon must not run here as root, or null when it may. Agent kits
@@ -76,10 +93,39 @@ export declare function daemonRootRefusal(isRoot: boolean, machineType: MachineT
76
93
  /** Boxes run the daemon as a system unit; everything else per user. */
77
94
  export declare function installScopeFor(machineType: MachineType): InstallScope;
78
95
  export declare function planDaemonUnit(host: DaemonHost, options: InstallOptions): DaemonUnitPlan;
96
+ /** What to tell the user when HQ did not stop pid `pid` because it could not verify it. */
97
+ export declare function oldDaemonWarning(pid: number, reason: string): string;
79
98
  export declare function installDaemonUnit(host: DaemonHost, options: InstallOptions, deps: InstallDeps): DaemonUnitResult;
99
+ /**
100
+ * PowerShell script that lists Windows sessions as `id|state|DOMAIN\user`
101
+ * lines through WTSEnumerateSessions and WTSQuerySessionInformation. State is
102
+ * the numeric WTS_CONNECTSTATE_CLASS (0 = WTSActive), so the output does not
103
+ * depend on the display language, and it needs no query.exe (absent on
104
+ * Windows Home). Constrained Language Mode refuses Add-Type, so there the
105
+ * script prints `unavailable|<mode>` and lists nothing.
106
+ */
107
+ export declare const WINDOWS_SESSIONS_SCRIPT: string;
108
+ /** One Windows session from WINDOWS_SESSIONS_SCRIPT output. */
109
+ export interface WindowsSession {
110
+ id: number;
111
+ /** WTS_CONNECTSTATE_CLASS: 0 is WTSActive. */
112
+ state: number;
113
+ /** `DOMAIN\user`, a bare user name, or "" when nobody is signed in to it. */
114
+ user: string;
115
+ }
116
+ export declare function parseWindowsSessions(stdout: string): WindowsSession[];
117
+ /**
118
+ * Whether `user` (`DOMAIN\user` or a bare name) has an active Windows session.
119
+ * A disconnected session does not count: an interactive-token task cannot
120
+ * start in it. Returns "unavailable" when PowerShell runs in Constrained
121
+ * Language Mode, which refuses the check, and null when the check failed in
122
+ * any other way.
123
+ */
124
+ export declare function windowsInteractiveSession(run: RunCommand, user: string): boolean | null | "unavailable";
80
125
  export declare function uninstallDaemonUnit(host: DaemonHost, options: InstallOptions, deps: InstallDeps): {
81
126
  kind: DaemonUnitPlan["kind"];
82
127
  unitPath: string;
83
128
  removed: boolean;
129
+ warning?: string;
84
130
  };
85
131
  //# sourceMappingURL=install.d.ts.map
@@ -17,15 +17,20 @@
17
17
  */
18
18
  import * as fs from "node:fs";
19
19
  import * as path from "node:path";
20
- import { activateLaunchd, deactivateLaunchd, defaultRunCommand, installLaunchdPlist, launchdIsLoaded, launchdPlistPath, renderLaunchdPlist, } from "../service-manager/launchd.js";
20
+ import { activateLaunchd, blockingSleep, deactivateLaunchd, defaultRunCommand, installLaunchdPlist, launchdIsLoaded, launchdPlistPath, renderLaunchdPlist, } from "../service-manager/launchd.js";
21
21
  import { activateSystemd, deactivateSystemd, renderSystemdUserUnit, systemdUnitPath, } from "../service-manager/systemd.js";
22
22
  import { escapeXml, shellQuote } from "../service-manager/types.js";
23
23
  import { readPidLock } from "../mesh/live/daemon/pid-lock.js";
24
24
  import { userToolDirs } from "./tools.js";
25
+ import { liveDaemonLock, verifyDaemonForKill } from "./lock.js";
26
+ import { daemonPaths } from "./paths.js";
25
27
  export const DAEMON_LAUNCHD_LABEL = "ai.getindigo.hq-daemon";
26
28
  export const DAEMON_SYSTEMD_UNIT = "hq-daemon.service";
27
29
  export const DAEMON_WINDOWS_TASK = "ai.getindigo.hq-daemon";
28
30
  export const SYSTEM_UNIT_DIR = "/etc/systemd/system";
31
+ /** How long install waits for the Windows task to start a daemon process. */
32
+ export const WINDOWS_START_WAIT_MS = 10_000;
33
+ const WINDOWS_START_POLL_MS = 500;
29
34
  export const ROOT_REFUSAL = "hq daemon runs its services as the machine's login user, never as root.";
30
35
  /**
31
36
  * Why the daemon must not run here as root, or null when it may. Agent kits
@@ -209,7 +214,39 @@ export function planDaemonUnit(host, options) {
209
214
  function commandError(what, r) {
210
215
  return new Error(`${what} failed: ${r.stderr.trim() || r.stdout.trim() || `exit ${r.status}`}`);
211
216
  }
212
- function activate(plan, host, run, sleep) {
217
+ /**
218
+ * Stop the Windows task and the recorded daemon's whole process tree. Ending
219
+ * the task stops only its root process (conhost when --headless is used);
220
+ * node and the services it runs keep going.
221
+ *
222
+ * The pid comes from the lock file, which can be stale: after the daemon
223
+ * exits, Windows may give its pid to another program. The process is checked
224
+ * immediately before the kill and killed only when it is verifiably the
225
+ * daemon. Returns a warning when a process was left running unverified.
226
+ */
227
+ function endWindowsDaemon(run, owner) {
228
+ run("schtasks.exe", ["/End", "/TN", DAEMON_WINDOWS_TASK]);
229
+ if (!owner || owner.pid === process.pid)
230
+ return undefined;
231
+ const check = verifyDaemonForKill(owner, run);
232
+ if (check.verdict === "daemon") {
233
+ run("taskkill.exe", ["/PID", String(owner.pid), "/T", "/F"]);
234
+ return undefined;
235
+ }
236
+ if (check.verdict === "gone")
237
+ return undefined;
238
+ return oldDaemonWarning(owner.pid, check.reason);
239
+ }
240
+ /** What to tell the user when HQ did not stop pid `pid` because it could not verify it. */
241
+ export function oldDaemonWarning(pid, reason) {
242
+ return [
243
+ `An older HQ daemon may still be running. HQ did not stop process ${pid} because ${reason}.`,
244
+ "To find a running HQ daemon:",
245
+ ` powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object CommandLine -match 'daemon run' | Select-Object ProcessId,CommandLine"`,
246
+ "Stop the process it lists with: taskkill /PID <ProcessId> /T /F",
247
+ ].join("\n");
248
+ }
249
+ function activate(plan, host, run, sleep, previous = null) {
213
250
  switch (plan.kind) {
214
251
  case "launchd":
215
252
  activateLaunchd(plan.unitPath, run, host.uid, { sleep });
@@ -231,13 +268,17 @@ function activate(plan, host, run, sleep) {
231
268
  const create = run("schtasks.exe", ["/Create", "/TN", DAEMON_WINDOWS_TASK, "/XML", plan.unitPath, "/F"]);
232
269
  if (create.status !== 0)
233
270
  throw commandError("schtasks /Create", create);
271
+ // `/Create /F` leaves a running instance alone and `/Run` is ignored
272
+ // while one runs, so a daemon from an earlier install would keep the old
273
+ // settings. Stop it first so `/Run` starts a new one.
274
+ const warning = previous ? endWindowsDaemon(run, previous) : undefined;
234
275
  const start = run("schtasks.exe", ["/Run", "/TN", DAEMON_WINDOWS_TASK]);
235
276
  if (start.status !== 0)
236
277
  throw commandError("schtasks /Run", start);
237
- return;
278
+ return warning;
238
279
  }
239
280
  case "manual":
240
- return;
281
+ return undefined;
241
282
  }
242
283
  }
243
284
  export function installDaemonUnit(host, options, deps) {
@@ -286,8 +327,32 @@ export function installDaemonUnit(host, options, deps) {
286
327
  else
287
328
  fs.writeFileSync(plan.unitPath, plan.rendered, { mode: plan.kind === "systemd-system" ? 0o644 : 0o600 });
288
329
  try {
289
- if (plan.kind !== "launchd")
290
- activate(plan, host, deps.run, deps.sleep);
330
+ const lockPath = daemonPaths(host.home, { HQ_DAEMON_DIR: host.daemonDir }).lockPath;
331
+ const runningDaemon = deps.runningDaemon ?? (() => liveDaemonLock(lockPath));
332
+ const previous = plan.kind === "schtasks" ? runningDaemon() : null;
333
+ const warning = plan.kind !== "launchd" ? activate(plan, host, deps.run, deps.sleep, previous) : undefined;
334
+ const warn = warning ? { warning } : {};
335
+ if (plan.kind === "schtasks") {
336
+ // schtasks /Run exits 0 even when the task cannot run: an
337
+ // InteractiveToken task with no signed-in session stays at "has not yet
338
+ // run" (267011). Report running only once a daemon started by this
339
+ // install holds the lock; a daemon from before the install does not count.
340
+ const outcome = waitForNewDaemon(runningDaemon, previous, deps.sleep ?? blockingSleep);
341
+ if (outcome === "started")
342
+ return { kind: plan.kind, unitPath: plan.unitPath, installed: true, running: true, ...warn };
343
+ if (outcome === "previous") {
344
+ return {
345
+ kind: plan.kind,
346
+ unitPath: plan.unitPath,
347
+ installed: true,
348
+ running: false,
349
+ error: `the daemon from before this install (pid ${previous?.pid}) is still running, ` +
350
+ "so the new task did not start. Run hq daemon uninstall, then hq daemon install",
351
+ ...warn,
352
+ };
353
+ }
354
+ return { kind: plan.kind, unitPath: plan.unitPath, installed: true, running: false, startPending: true, ...warn };
355
+ }
291
356
  return { kind: plan.kind, unitPath: plan.unitPath, installed: true, running: true };
292
357
  }
293
358
  catch (err) {
@@ -300,6 +365,112 @@ export function installDaemonUnit(host, options, deps) {
300
365
  };
301
366
  }
302
367
  }
368
+ /** A lock record names a different daemon than `previous` (new pid or new start time). */
369
+ function isNewDaemon(now, previous) {
370
+ return !previous || now.pid !== previous.pid || now.ts !== previous.ts;
371
+ }
372
+ /**
373
+ * Wait up to WINDOWS_START_WAIT_MS for a daemon started after `previous`.
374
+ * "previous" means the daemon from before the install still holds the lock at
375
+ * the end of the wait; "none" means no daemon is running.
376
+ */
377
+ function waitForNewDaemon(runningDaemon, previous, sleep) {
378
+ for (let waited = 0;; waited += WINDOWS_START_POLL_MS) {
379
+ const now = runningDaemon();
380
+ if (now && isNewDaemon(now, previous))
381
+ return "started";
382
+ if (waited >= WINDOWS_START_WAIT_MS)
383
+ return now ? "previous" : "none";
384
+ sleep(WINDOWS_START_POLL_MS);
385
+ }
386
+ }
387
+ /**
388
+ * PowerShell script that lists Windows sessions as `id|state|DOMAIN\user`
389
+ * lines through WTSEnumerateSessions and WTSQuerySessionInformation. State is
390
+ * the numeric WTS_CONNECTSTATE_CLASS (0 = WTSActive), so the output does not
391
+ * depend on the display language, and it needs no query.exe (absent on
392
+ * Windows Home). Constrained Language Mode refuses Add-Type, so there the
393
+ * script prints `unavailable|<mode>` and lists nothing.
394
+ */
395
+ export const WINDOWS_SESSIONS_SCRIPT = [
396
+ "$ErrorActionPreference = 'Stop'",
397
+ "$mode = [string]$ExecutionContext.SessionState.LanguageMode",
398
+ "if ($mode -ne 'FullLanguage') { 'unavailable|' + $mode; exit 0 }",
399
+ "Add-Type -TypeDefinition @'",
400
+ "using System;",
401
+ "using System.Runtime.InteropServices;",
402
+ "public static class HqWts {",
403
+ ' [DllImport("wtsapi32.dll", SetLastError = true)]',
404
+ " public static extern bool WTSEnumerateSessionsW(IntPtr server, int reserved, int version, out IntPtr info, out int count);",
405
+ ' [DllImport("wtsapi32.dll", SetLastError = true)]',
406
+ " public static extern bool WTSQuerySessionInformationW(IntPtr server, int sessionId, int infoClass, out IntPtr buffer, out int bytes);",
407
+ ' [DllImport("wtsapi32.dll")]',
408
+ " public static extern void WTSFreeMemory(IntPtr memory);",
409
+ " static string Query(int id, int infoClass) {",
410
+ " IntPtr buffer; int bytes;",
411
+ ' if (!WTSQuerySessionInformationW(IntPtr.Zero, id, infoClass, out buffer, out bytes)) return "";',
412
+ " try { return Marshal.PtrToStringUni(buffer); } finally { WTSFreeMemory(buffer); }",
413
+ " }",
414
+ " public static string[] List() {",
415
+ " IntPtr info; int count;",
416
+ ' if (!WTSEnumerateSessionsW(IntPtr.Zero, 0, 1, out info, out count)) throw new System.ComponentModel.Win32Exception();',
417
+ " try {",
418
+ " int size = 3 * IntPtr.Size;",
419
+ " string[] lines = new string[count];",
420
+ " for (int i = 0; i < count; i++) {",
421
+ " IntPtr row = new IntPtr(info.ToInt64() + (long)i * size);",
422
+ " int id = Marshal.ReadInt32(row);",
423
+ " int state = Marshal.ReadInt32(row, 2 * IntPtr.Size);",
424
+ " string user = Query(id, 5);",
425
+ " string domain = Query(id, 7);",
426
+ ' lines[i] = id + "|" + state + "|" + (domain.Length > 0 ? domain + "\\\\" + user : user);',
427
+ " }",
428
+ " return lines;",
429
+ " } finally { WTSFreeMemory(info); }",
430
+ " }",
431
+ "}",
432
+ "'@",
433
+ "[HqWts]::List() | ForEach-Object { $_ }",
434
+ ].join("\n");
435
+ const WTS_ACTIVE = 0;
436
+ export function parseWindowsSessions(stdout) {
437
+ const sessions = [];
438
+ for (const line of stdout.split(/\r?\n/)) {
439
+ const match = /^(\d+)\|(\d+)\|(.*)$/.exec(line.trim());
440
+ if (match)
441
+ sessions.push({ id: Number(match[1]), state: Number(match[2]), user: match[3].trim() });
442
+ }
443
+ return sessions;
444
+ }
445
+ /**
446
+ * Whether `user` (`DOMAIN\user` or a bare name) has an active Windows session.
447
+ * A disconnected session does not count: an interactive-token task cannot
448
+ * start in it. Returns "unavailable" when PowerShell runs in Constrained
449
+ * Language Mode, which refuses the check, and null when the check failed in
450
+ * any other way.
451
+ */
452
+ export function windowsInteractiveSession(run, user) {
453
+ const encoded = Buffer.from(WINDOWS_SESSIONS_SCRIPT, "utf16le").toString("base64");
454
+ const r = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded]);
455
+ if (r.status !== 0)
456
+ return null;
457
+ if (/^unavailable\|/m.test(r.stdout.trim()))
458
+ return "unavailable";
459
+ const sessions = parseWindowsSessions(r.stdout);
460
+ if (sessions.length === 0)
461
+ return null;
462
+ const wanted = user.trim().toLowerCase();
463
+ const bare = (wanted.split("\\").pop() ?? "").trim();
464
+ if (!bare)
465
+ return null;
466
+ return sessions.some((s) => {
467
+ if (s.state !== WTS_ACTIVE || !s.user)
468
+ return false;
469
+ const name = s.user.toLowerCase();
470
+ // Compare the full DOMAIN\user when both sides have a domain.
471
+ return name.includes("\\") && wanted.includes("\\") ? name === wanted : name.split("\\").pop() === bare;
472
+ });
473
+ }
303
474
  export function uninstallDaemonUnit(host, options, deps) {
304
475
  // A system uninstall does not need the login user; only the unit path matters.
305
476
  const plan = host.platform === "linux" && options.scope === "system"
@@ -312,6 +483,7 @@ export function uninstallDaemonUnit(host, options, deps) {
312
483
  (plan.kind === "schtasks" && deps.run("schtasks.exe", ["/Query", "/TN", DAEMON_WINDOWS_TASK]).status === 0);
313
484
  if (!present)
314
485
  return { kind: plan.kind, unitPath: plan.unitPath, removed: false };
486
+ let warning;
315
487
  switch (plan.kind) {
316
488
  case "launchd":
317
489
  deactivateLaunchd(plan.unitPath, deps.run, host.uid);
@@ -330,14 +502,8 @@ export function uninstallDaemonUnit(host, options, deps) {
330
502
  break;
331
503
  }
332
504
  case "schtasks": {
333
- deps.run("schtasks.exe", ["/End", "/TN", DAEMON_WINDOWS_TASK]);
334
- // Ending the task stops only its root process (conhost when --headless is
335
- // used); node and the services it runs keep going. End the recorded
336
- // daemon's whole process tree.
337
- const owner = readPidLock(path.join(host.daemonDir, "daemon.pid"));
338
- if (owner && owner.pid !== process.pid) {
339
- deps.run("taskkill.exe", ["/PID", String(owner.pid), "/T", "/F"]);
340
- }
505
+ // The lock may be stale; endWindowsDaemon kills only a verified daemon.
506
+ warning = endWindowsDaemon(deps.run, readPidLock(path.join(host.daemonDir, "daemon.pid")));
341
507
  const del = deps.run("schtasks.exe", ["/Delete", "/TN", DAEMON_WINDOWS_TASK, "/F"]);
342
508
  // Keep the XML so a later uninstall still finds the task.
343
509
  if (del.status !== 0)
@@ -346,6 +512,6 @@ export function uninstallDaemonUnit(host, options, deps) {
346
512
  }
347
513
  }
348
514
  fs.rmSync(plan.unitPath, { force: true });
349
- return { kind: plan.kind, unitPath: plan.unitPath, removed: true };
515
+ return { kind: plan.kind, unitPath: plan.unitPath, removed: true, ...(warning ? { warning } : {}) };
350
516
  }
351
517
  //# sourceMappingURL=install.js.map
@@ -31,8 +31,38 @@ export interface DaemonProcessDeps {
31
31
  * The pid is alive and is an `hq daemon run`. After a reboot the pid in an old
32
32
  * lock file often belongs to an unrelated program; that must not keep the
33
33
  * daemon from starting.
34
+ *
35
+ * An unreadable command line counts as the daemon here, so a second daemon is
36
+ * never started next to one HQ cannot inspect. Never use this to decide a
37
+ * kill: use `verifyDaemonForKill`, which treats an unreadable command line as
38
+ * "not the daemon".
34
39
  */
35
40
  export declare function isDaemonProcess(pid: number, deps?: DaemonProcessDeps): boolean;
41
+ /**
42
+ * PowerShell that prints a Windows process's creation time (UTC, ISO 8601) on
43
+ * the first line and its command line on the rest, or nothing when no process
44
+ * has that id. It uses only a cmdlet and DateTime methods, so it also runs in
45
+ * Constrained Language Mode. `-ErrorAction Stop` makes a WMI failure exit
46
+ * non-zero, so it reads as "could not read", never as "no such process".
47
+ */
48
+ export declare function windowsProcessIdentityScript(pid: number): string;
49
+ export type DaemonKillCheck = {
50
+ verdict: "daemon";
51
+ } | {
52
+ verdict: "gone";
53
+ } | {
54
+ verdict: "unverified";
55
+ reason: string;
56
+ };
57
+ /**
58
+ * Decide whether the Windows process in `owner` may be killed as the HQ
59
+ * daemon. Run this immediately before the kill. It is the daemon only when the
60
+ * process exists, its command line is readable and is `hq daemon run`, and it
61
+ * was created no later than the lock was written. A process created after the
62
+ * lock took over a pid the old daemon left behind. Anything that cannot be
63
+ * read is "unverified", and the caller must not kill.
64
+ */
65
+ export declare function verifyDaemonForKill(owner: PidLockRecord, run: RunCommand): DaemonKillCheck;
36
66
  export type DaemonLockResult = {
37
67
  ok: true;
38
68
  } | {
@@ -41,6 +71,8 @@ export type DaemonLockResult = {
41
71
  };
42
72
  export declare function acquireDaemonLock(lockPath: string, pid?: number, alive?: (pid: number) => boolean, now?: () => number): DaemonLockResult;
43
73
  export declare function releaseDaemonLock(lockPath: string, pid?: number): void;
74
+ /** The live supervisor's lock record (pid and start time), or null when none is running. */
75
+ export declare function liveDaemonLock(lockPath: string, alive?: (pid: number) => boolean): PidLockRecord | null;
44
76
  /** The live supervisor's pid, or null when none is running. */
45
77
  export declare function runningDaemonPid(lockPath: string, alive?: (pid: number) => boolean): number | null;
46
78
  export {};
@@ -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
@@ -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,6 +46,8 @@ 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;
48
51
  now?: () => Date;
49
52
  }
50
53
  export declare function createAnywhereFamily(deps?: AnywhereDeps): CheckFamily;
@@ -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 = {}) {
@@ -118,7 +121,12 @@ async function runAnywhereChecks(context, deps = {}) {
118
121
  const serviceState = runtimeEnabled && installed
119
122
  ? (deps.serviceState ?? (() => defaultServiceState(home)))()
120
123
  : undefined;
121
- const healthCheck = runtimeHealthCheck(health, healthUnsupported, serviceState);
124
+ // A missing Windows pipe usually means hqd never started; only blame the
125
+ // pipe ACL when a daemon process is actually alive.
126
+ const daemonRunning = health.reachable
127
+ ? true
128
+ : (deps.daemonRunning ?? (() => runningDaemonPid(daemonPaths(home, env).lockPath) !== null))();
129
+ const healthCheck = runtimeHealthCheck(health, healthUnsupported, serviceState, daemonRunning);
122
130
  if (!runtimeEnabled)
123
131
  return [healthCheck, settingCheck];
124
132
  const remoteModes = [
@@ -179,7 +187,7 @@ function unreachableHealth(socketPath) {
179
187
  socket: { path: socketPath, transport: process.platform === "win32" ? "windows_named_pipe" : "unix_socket", mode: null, ownerMatchesUser: null, userOnly: null },
180
188
  };
181
189
  }
182
- function runtimeHealthCheck(health, healthUnsupported = false, serviceState) {
190
+ function runtimeHealthCheck(health, healthUnsupported = false, serviceState, daemonRunning = true) {
183
191
  const { flag, resolution, socket } = health;
184
192
  const age = flag.lastSuccessfulReadAgeMs === null
185
193
  ? "unknown"
@@ -195,9 +203,12 @@ function runtimeHealthCheck(health, healthUnsupported = false, serviceState) {
195
203
  const socketSecurity = socket.transport === "windows_named_pipe"
196
204
  ? "Windows named pipe (mode and owner are not reported)"
197
205
  : `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"}`;
198
- 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`
200
- : `hqd is not reachable on ${socket.path}; version, uptime, flag state, and cwd binding are unavailable`;
206
+ const pipeWithoutDaemon = socket.transport === "windows_named_pipe" && !daemonRunning;
207
+ const unreachableMessage = pipeWithoutDaemon
208
+ ? `hqd is not running, so its named pipe ${socket.path} does not exist. Run ${REMEDIATION.daemonStatus} to see why it has not started.`
209
+ : socket.transport === "windows_named_pipe"
210
+ ? `hqd is not reachable on ${socket.path}; secure Windows named-pipe setup may have refused an unsafe ACL`
211
+ : `hqd is not reachable on ${socket.path}; version, uptime, flag state, and cwd binding are unavailable`;
201
212
  const socketNotUserOnly = health.reachable && socket.transport === "unix_socket" && (socket.userOnly !== true || socket.ownerMatchesUser !== true);
202
213
  const warningNotes = [
203
214
  healthUnsupported ? "resident hqd does not support detailed health; restart hqd for full diagnostics" : null,
@@ -207,7 +218,8 @@ function runtimeHealthCheck(health, healthUnsupported = false, serviceState) {
207
218
  ? `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
219
  : unreachableMessage;
209
220
  const remediation = !health.reachable && serviceState
210
- ? serviceState.installed ? REMEDIATION.daemonRestart : REMEDIATION.daemonInstall
221
+ ? !serviceState.installed ? REMEDIATION.daemonInstall
222
+ : pipeWithoutDaemon ? REMEDIATION.daemonStatus : REMEDIATION.daemonRestart
211
223
  : healthUnsupported || socketNotUserOnly ? REMEDIATION.daemonRestart : undefined;
212
224
  return {
213
225
  status: health.reachable && warningNotes.length === 0 ? "PASS" : "WARN",
@@ -336,12 +348,15 @@ function checkSkillLinks(runtime, dir, fix, repair = fix) {
336
348
  return result("WARN", id, `${dir} does not exist`, fix, dir);
337
349
  }
338
350
  const links = names.filter((n) => fs.lstatSync(path.join(dir, n)).isSymbolicLink());
339
- if (links.length === 0)
351
+ // Windows falls back to marked copies when it refuses both link types.
352
+ const copies = names.filter((n) => skillCopySource(path.join(dir, n)) !== null);
353
+ if (links.length === 0 && copies.length === 0)
340
354
  return result("WARN", id, `no hq-* skill links in ${dir}`, fix, dir);
341
355
  const broken = links.filter((n) => !fs.existsSync(path.join(dir, n)));
342
356
  if (broken.length > 0)
343
357
  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);
358
+ const copied = copies.length > 0 ? ` (${copies.length} more are copies that do not follow HQ skill updates)` : "";
359
+ return result("PASS", id, `${links.length} hq-* skill links in ${dir} resolve${copied}`, undefined, dir);
345
360
  }
346
361
  export function defaultServiceState(home, options = {}) {
347
362
  const platform = options.platform ?? process.platform;
@@ -0,0 +1,11 @@
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? Reads the registry only; never edits PATH.
8
+ */
9
+ export declare const cliPathFamily: CheckFamily;
10
+ export declare function checkCliPath(_context: CheckContext, check?: () => ShimPathCheck | null): CheckResult[];
11
+ //# sourceMappingURL=cli-path.d.ts.map
@@ -0,0 +1,45 @@
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? Reads the registry only; never edits PATH.
7
+ */
8
+ export const cliPathFamily = {
9
+ id: CLI_PATH_FAMILY_ID,
10
+ title: "hq command on PATH",
11
+ run: (context) => Promise.resolve(checkCliPath(context)),
12
+ };
13
+ export function checkCliPath(_context, check = () => checkShimOnPath()) {
14
+ const result = check();
15
+ if (!result) {
16
+ return [
17
+ {
18
+ status: "NA",
19
+ checkId: CLI_PATH_CHECK_ID,
20
+ message: "Not a Windows npm global install, so there is no hq.cmd shim folder to check.",
21
+ },
22
+ ];
23
+ }
24
+ if (result.onPath) {
25
+ return [
26
+ {
27
+ status: "PASS",
28
+ checkId: CLI_PATH_CHECK_ID,
29
+ target: result.shimDir,
30
+ message: `The hq command folder ${result.shimDir} is on your Windows PATH.`,
31
+ },
32
+ ];
33
+ }
34
+ return [
35
+ {
36
+ status: "WARN",
37
+ checkId: CLI_PATH_CHECK_ID,
38
+ target: result.shimDir,
39
+ message: shimPathWarning(result)[0],
40
+ details: { source: result.source },
41
+ remediation: `In PowerShell, then open a new terminal: ${pathFixCommand(result.shimDir)}`,
42
+ },
43
+ ];
44
+ }
45
+ //# 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