@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
@@ -1 +1 @@
1
- {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.88","bundleSha256":"7e9ee380eae55e059a474fd5f415e8aeb16088805b761d791318d5299645becd","formatVersion":1}
1
+ {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.90","bundleSha256":"cb49486573d2d1141ee6e18d73dde825f2b7c02074560f19504e3b1daf7eeba8","formatVersion":1}
package/dist/index.d.ts CHANGED
@@ -3,6 +3,15 @@ import "./node-preflight.js";
3
3
  import "./node-network-compat-fast.js";
4
4
  import { isVersionRequest } from "./version-request.js";
5
5
  import { isFastCoreRequest } from "./fast-scaffold.js";
6
+ /**
7
+ * Whether the install follow-up would read the registry or spawn the mesh
8
+ * daemon. False for fast routes' callers, and for commands that are not run
9
+ * from an npm install: the PATH repair needs the npm shim folder above
10
+ * node_modules, and the daemon step needs the installed package. Inlined so
11
+ * those routes do not load user-path.ts. Keep the skip rules aligned with
12
+ * install-followup.ts.
13
+ */
14
+ export declare function installFollowupWouldDoWork(argv: readonly string[], env: NodeJS.ProcessEnv, platform: NodeJS.Platform): boolean;
6
15
  type PackageUseLeaseLoader = () => Promise<void>;
7
16
  type PackageUseUpdateProbe = () => Promise<boolean>;
8
17
  /** Resolves true once the command was re-run (exit code already set). */
@@ -18,6 +27,7 @@ declare function runCliEntry(entryUrl: string, dispatch: () => void, loadLease?:
18
27
  export declare const __test__: {
19
28
  isVersionRequest: typeof isVersionRequest;
20
29
  isFastCoreRequest: typeof isFastCoreRequest;
30
+ installFollowupWouldDoWork: typeof installFollowupWouldDoWork;
21
31
  dispatchWithPackageUseLease: typeof dispatchWithPackageUseLease;
22
32
  waitForUpdateThenReexec: typeof waitForUpdateThenReexec;
23
33
  runCliEntry: typeof runCliEntry;
package/dist/index.js CHANGED
@@ -156,6 +156,26 @@ async function runMonitorCheckFastWithRecovery(request) {
156
156
  return;
157
157
  await runMainWithInstallTreeRecovery();
158
158
  }
159
+ /**
160
+ * Whether the install follow-up would read the registry or spawn the mesh
161
+ * daemon. False for fast routes' callers, and for commands that are not run
162
+ * from an npm install: the PATH repair needs the npm shim folder above
163
+ * node_modules, and the daemon step needs the installed package. Inlined so
164
+ * those routes do not load user-path.ts. Keep the skip rules aligned with
165
+ * install-followup.ts.
166
+ */
167
+ export function installFollowupWouldDoWork(argv, env, platform) {
168
+ if (env.HQ_INSTALL_FOLLOWUP === "1" || env.HQ_STAGED_INSTALL === "1")
169
+ return false;
170
+ const args = argv.slice(2).filter((arg) => arg !== "--verbose");
171
+ if (args[0] === "mesh" && args[1] === "daemon")
172
+ return false;
173
+ if (args[0] === "daemon" && (args[1] === "run" || args[1] === "install"))
174
+ return false;
175
+ if (platform === "win32")
176
+ return /[/\\]node_modules[/\\]/i.test(argv[1] ?? "");
177
+ return /node_modules[/\\]@indigoai-us[/\\]hq-cli[/\\]/i.test(argv[1] ?? "");
178
+ }
159
179
  function dispatchCliEntry() {
160
180
  // Before anything spawns: a background hq on Windows (no terminal) hides the
161
181
  // console window of every child it starts (feedback_351a40d5).
@@ -165,6 +185,19 @@ function dispatchCliEntry() {
165
185
  void runHookEnginePath(engineArgs);
166
186
  return;
167
187
  }
188
+ // cmd.exe code page 437 prints em dashes as mojibake. Pipes stay UTF-8.
189
+ // Loaded only on Windows so Linux fast routes do not pay for the module.
190
+ if (process.platform === "win32") {
191
+ void import("./lib/windows/console-encoding.js").then(({ installWindowsConsoleEncoding }) => {
192
+ installWindowsConsoleEncoding();
193
+ }).catch(() => undefined).then(() => {
194
+ dispatchCliRoutes();
195
+ });
196
+ return;
197
+ }
198
+ dispatchCliRoutes();
199
+ }
200
+ function dispatchCliRoutes() {
168
201
  if (process.platform === "win32" && coreCommandHasPathOperands(process.argv)) {
169
202
  // Keep cygpath off non-Windows and non-path command routes.
170
203
  process.argv = normalizeCorePathOperands(process.argv, "win32", cygpathMixedPath);
@@ -172,6 +205,37 @@ function dispatchCliEntry() {
172
205
  const versionRequest = isVersionRequest(process.argv);
173
206
  const helpRequest = !versionRequest && isCommandHelpRequest(process.argv);
174
207
  const parsedFastCore = versionRequest || helpRequest ? null : parseFastCore(process.argv);
208
+ const monitorRequest = versionRequest || helpRequest ? null : parseMonitorCheckFastRequest(process.argv);
209
+ const secretsRequest = versionRequest || helpRequest || monitorRequest || parsedFastCore
210
+ ? null
211
+ : parseSecretsEnvFastRequest(process.argv);
212
+ const mcpRequest = !versionRequest &&
213
+ !helpRequest &&
214
+ !monitorRequest &&
215
+ !parsedFastCore &&
216
+ !secretsRequest &&
217
+ isMcpServeRequest(process.argv);
218
+ // `--version`, hook, MCP, and secrets fast routes must not pay for PATH or
219
+ // daemon work, so they never start PowerShell or reg.exe. The module stays
220
+ // unloaded when there is nothing to do, so a Linux fast route does not
221
+ // import it.
222
+ if (!versionRequest && !monitorRequest && !parsedFastCore && !secretsRequest && !mcpRequest
223
+ && installFollowupWouldDoWork(process.argv, process.env, process.platform)) {
224
+ void import("./lib/windows/install-followup.js").then(({ runWindowsInstallFollowup }) => {
225
+ try {
226
+ runWindowsInstallFollowup({ argv: process.argv });
227
+ }
228
+ catch {
229
+ // Install repair must not turn into a failed command.
230
+ }
231
+ }).catch(() => undefined).then(() => {
232
+ dispatchCliCommand(versionRequest, helpRequest, parsedFastCore, monitorRequest, secretsRequest, mcpRequest);
233
+ });
234
+ return;
235
+ }
236
+ dispatchCliCommand(versionRequest, helpRequest, parsedFastCore, monitorRequest, secretsRequest, mcpRequest);
237
+ }
238
+ function dispatchCliCommand(versionRequest, helpRequest, parsedFastCore, monitorRequest, secretsRequest, mcpRequest) {
175
239
  if (versionRequest) {
176
240
  // Bare `--version` is the CLI semver only (the contract field updaters
177
241
  // compare). `hq version` and `--json` print the labelled HQ/CLI report.
@@ -202,11 +266,11 @@ function dispatchCliEntry() {
202
266
  await runMainWithInstallTreeRecovery();
203
267
  });
204
268
  }
205
- else if (parseMonitorCheckFastRequest(process.argv)) {
269
+ else if (monitorRequest) {
206
270
  // The Stop hook runs this every turn in every session. Answer the common
207
271
  // no-active-lane case from the lane-record directory alone; anything the
208
272
  // fast route cannot prove falls back to the full command graph.
209
- void runMonitorCheckFastWithRecovery(parseMonitorCheckFastRequest(process.argv));
273
+ void runMonitorCheckFastWithRecovery(monitorRequest);
210
274
  }
211
275
  else if (parsedFastCore?.entry.kind === "native" &&
212
276
  (process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "true" ||
@@ -222,8 +286,8 @@ function dispatchCliEntry() {
222
286
  // existing full-graph route.
223
287
  void runNativeFastRouteWithRecovery(parsedFastCore);
224
288
  }
225
- else if (parseSecretsEnvFastRequest(process.argv)) {
226
- const request = parseSecretsEnvFastRequest(process.argv);
289
+ else if (secretsRequest) {
290
+ const request = secretsRequest;
227
291
  void import("./secrets-env-fast-route.js").then(async (route) => {
228
292
  await route.dispatchSecretsEnvFastRequest(request, {
229
293
  isEnabled: () => route.secretsEnvFastRouteEnabled(request),
@@ -237,7 +301,7 @@ function dispatchCliEntry() {
237
301
  });
238
302
  });
239
303
  }
240
- else if (isMcpServeRequest(process.argv)) {
304
+ else if (mcpRequest) {
241
305
  // Keep the minimal startup route; runMcpServe applies the hq-flags gate.
242
306
  void import("./commands/mcp-serve.js").then(({ runMcpServe }) => runMcpServe());
243
307
  }
@@ -373,6 +437,7 @@ void runCliEntry(import.meta.url, dispatchCliEntry);
373
437
  export const __test__ = {
374
438
  isVersionRequest,
375
439
  isFastCoreRequest,
440
+ installFollowupWouldDoWork,
376
441
  dispatchWithPackageUseLease,
377
442
  waitForUpdateThenReexec,
378
443
  runCliEntry,
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { COMMAND_CATALOG } from "./command-catalog.generated.js";
6
+ import { installUnknownCommandSuggestions } from "./unknown-command.js";
6
7
  const commandCatalog = COMMAND_CATALOG;
7
8
  /**
8
9
  * One entry for every root, including groups that are assembled by several
@@ -105,6 +106,7 @@ export function findRuntimeHqRoot(start, parentOf = dirname, isRoot = (directory
105
106
  export function registerCommandCatalog(program) {
106
107
  for (const command of commandCatalog)
107
108
  program.addCommand(commandDefinition(command));
109
+ installUnknownCommandSuggestions(program);
108
110
  }
109
111
  function commandForToken(token) {
110
112
  return commandCatalog.find((candidate) => candidate.name === token || candidate.aliases.includes(token));
@@ -201,14 +201,44 @@ export interface UsageReport {
201
201
  freeItems: string;
202
202
  drivers: Driver[];
203
203
  noSyncGuideUrl: string;
204
+ /** Set by `hq billing usage`: the account is on usage pricing, so meters are priced. */
205
+ onUsagePricing?: true;
204
206
  readOnly: true;
205
207
  }
208
+ /** One meter's reading with no price attached. */
209
+ export interface MeterReadingRow {
210
+ key: UsageMeterKey;
211
+ displayName: string;
212
+ /** In billed units (GB for storage). Null unless measured. */
213
+ value: number | null;
214
+ state: MeterState;
215
+ }
216
+ /**
217
+ * The report for an account that is not on usage pricing (a flat plan, or the
218
+ * server did not say). Readings only: no rates, no cost, no projected bill.
219
+ */
220
+ export interface UsageReadingsReport {
221
+ scope: UsageScope;
222
+ source: MonthUsage["source"];
223
+ /** False: flat plan, meters are not billed. Null: HQ did not say. */
224
+ onUsagePricing: false | null;
225
+ period: MonthUsage["period"];
226
+ meters: MeterReadingRow[];
227
+ storage?: StorageSplit;
228
+ /** The one line that says why no cost is shown. */
229
+ notice: string;
230
+ readOnly: true;
231
+ }
232
+ export type BillingUsageReport = UsageReport | UsageReadingsReport;
233
+ export declare function isReadingsReport(r: BillingUsageReport): r is UsageReadingsReport;
234
+ /** Readings for an account not on usage pricing. Never prices a meter. */
235
+ export declare function buildReadingsReport(usage: MonthUsage, onUsagePricing: false | null): UsageReadingsReport;
206
236
  export declare function buildReport(usage: MonthUsage, rates: {
207
237
  rates: readonly UsageMeterRate[];
208
238
  allowanceUsd: number;
209
239
  source: "published" | "bundled";
210
240
  card?: UsageCard;
211
241
  }): UsageReport;
212
- export declare function formatReport(r: UsageReport): string;
242
+ export declare function formatReport(r: BillingUsageReport): string;
213
243
  export {};
214
244
  //# sourceMappingURL=usage-cost.d.ts.map
@@ -362,6 +362,38 @@ export function findDrivers(usage, bill) {
362
362
  // ── Report ──────────────────────────────────────────────────────────────────
363
363
  /** The self-hosted / no-sync guide the usage card links (feed `signup.selfHosted.url`). */
364
364
  export const NO_SYNC_GUIDE_URL = SELF_HOSTED_GUIDE_URL;
365
+ export function isReadingsReport(r) {
366
+ return !("bill" in r);
367
+ }
368
+ function readingsNotice(scope, onUsagePricing) {
369
+ if (onUsagePricing === false) {
370
+ return scope.kind === "company"
371
+ ? `${scope.companySlug} is on a flat plan, so usage meters are not billed.`
372
+ : "Your personal account is on a flat plan, so usage meters are not billed.";
373
+ }
374
+ const who = scope.kind === "company" ? scope.companySlug : "your personal account";
375
+ return `HQ did not say whether ${who} is on usage pricing, so no costs are shown.`;
376
+ }
377
+ /** Readings for an account not on usage pricing. Never prices a meter. */
378
+ export function buildReadingsReport(usage, onUsagePricing) {
379
+ const byKey = new Map(usage.meters.map((m) => [m.key, m]));
380
+ const meters = BUNDLED_USAGE_METER_RATES.map((rate) => {
381
+ const reading = byKey.get(rate.key);
382
+ return reading && reading.state === "measured" && reading.value !== null
383
+ ? { key: rate.key, displayName: rate.displayName, value: reading.value, state: "measured" }
384
+ : { key: rate.key, displayName: rate.displayName, value: null, state: reading?.state ?? "not-available" };
385
+ });
386
+ return {
387
+ scope: usage.scope,
388
+ source: usage.source,
389
+ onUsagePricing,
390
+ period: usage.period,
391
+ meters,
392
+ ...(usage.storage ? { storage: usage.storage } : {}),
393
+ notice: readingsNotice(usage.scope, onUsagePricing),
394
+ readOnly: true,
395
+ };
396
+ }
365
397
  export function buildReport(usage, rates) {
366
398
  const bill = computeBill(usage, rates.rates, rates.allowanceUsd);
367
399
  return {
@@ -394,7 +426,21 @@ function valueText(m) {
394
426
  return `${Math.round(m.value)} vault${Math.round(m.value) === 1 ? "" : "s"}`;
395
427
  return `${Math.round(m.value)}`;
396
428
  }
429
+ function formatReadingsReport(r) {
430
+ const who = r.scope.kind === "company" ? r.scope.companySlug : "your personal HQ";
431
+ const lines = [];
432
+ lines.push(`Usage for ${who}, ${r.period.start.slice(0, 7)} (as of ${r.period.asOf.slice(0, 10)})`);
433
+ lines.push("");
434
+ const nameW = Math.max(...r.meters.map((m) => m.displayName.length));
435
+ for (const m of r.meters)
436
+ lines.push(` ${m.displayName.padEnd(nameW)} ${valueText(m)}`);
437
+ lines.push("");
438
+ lines.push(r.notice);
439
+ return lines.join("\n");
440
+ }
397
441
  export function formatReport(r) {
442
+ if (isReadingsReport(r))
443
+ return formatReadingsReport(r);
398
444
  const who = r.scope.kind === "company" ? r.scope.companySlug : "your personal HQ";
399
445
  const month = r.period.start.slice(0, 7);
400
446
  const lines = [];
@@ -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
@@ -48,10 +48,22 @@ export interface AnywhereDeps {
48
48
  serviceState?: () => DaemonServiceState;
49
49
  /** Is an hqd process alive? Default: the daemon lock names a live hqd pid. */
50
50
  daemonRunning?: () => boolean;
51
+ /** Transport to report when health throws. Production uses the platform default. */
52
+ unreachableTransport?: "windows_named_pipe" | "unix_socket";
51
53
  now?: () => Date;
52
54
  }
55
+ export interface PipeConnectFailure {
56
+ code?: string;
57
+ detail?: string;
58
+ }
53
59
  export declare function createAnywhereFamily(deps?: AnywhereDeps): CheckFamily;
54
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;
55
67
  export declare function defaultServiceState(home: string, options?: DaemonServiceProbeOptions): DaemonServiceState;
56
68
  export {};
57
69
  //# sourceMappingURL=anywhere.d.ts.map
@@ -63,6 +63,7 @@ async function runAnywhereChecks(context, deps = {}) {
63
63
  : result(personSetting ? "PASS" : "WARN", "person-setting", `HQ Anywhere person setting is ${personSetting ? "on" : "off"}`);
64
64
  let health;
65
65
  let healthUnsupported = false;
66
+ let connectFailure;
66
67
  try {
67
68
  if (deps.health) {
68
69
  health = await deps.health(cwd);
@@ -107,12 +108,14 @@ async function runAnywhereChecks(context, deps = {}) {
107
108
  };
108
109
  healthUnsupported = true;
109
110
  }
110
- catch {
111
- health = unreachableHealth(socketPath);
111
+ catch (inner) {
112
+ health = unreachableHealth(socketPath, deps.unreachableTransport);
113
+ connectFailure = connectFailureFrom(inner);
112
114
  }
113
115
  }
114
116
  else {
115
- health = unreachableHealth(socketPath);
117
+ health = unreachableHealth(socketPath, deps.unreachableTransport);
118
+ connectFailure = connectFailureFrom(error);
116
119
  }
117
120
  }
118
121
  const claude = claudeTargets(home);
@@ -126,7 +129,7 @@ async function runAnywhereChecks(context, deps = {}) {
126
129
  const daemonRunning = health.reachable
127
130
  ? true
128
131
  : (deps.daemonRunning ?? (() => runningDaemonPid(daemonPaths(home, env).lockPath) !== null))();
129
- const healthCheck = runtimeHealthCheck(health, healthUnsupported, serviceState, daemonRunning);
132
+ const healthCheck = runtimeHealthCheck(health, healthUnsupported, serviceState, daemonRunning, connectFailure);
130
133
  if (!runtimeEnabled)
131
134
  return [healthCheck, settingCheck];
132
135
  const remoteModes = [
@@ -177,17 +180,67 @@ async function runAnywhereChecks(context, deps = {}) {
177
180
  out.push(checkSkillLinks("codex", codex.skillsDir, REMEDIATION.installCodex, REMEDIATION.repairCodexSkills));
178
181
  return out;
179
182
  }
180
- function unreachableHealth(socketPath) {
183
+ function unreachableHealth(socketPath, transport) {
181
184
  return {
182
185
  reachable: false,
183
186
  version: null,
184
187
  uptimeMs: null,
185
188
  flag: { enabled: null, lastSuccessfulReadAgeMs: null, heldThroughReadError: false },
186
189
  resolution: { scope: "unknown", company: null, fallbackReason: null },
187
- 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
+ },
188
197
  };
189
198
  }
190
- function runtimeHealthCheck(health, healthUnsupported = false, serviceState, daemonRunning = true) {
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) {
191
244
  const { flag, resolution, socket } = health;
192
245
  const age = flag.lastSuccessfulReadAgeMs === null
193
246
  ? "unknown"
@@ -204,11 +257,9 @@ function runtimeHealthCheck(health, healthUnsupported = false, serviceState, dae
204
257
  ? "Windows named pipe (mode and owner are not reported)"
205
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"}`;
206
259
  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`;
260
+ const unreachableMessage = socket.transport === "windows_named_pipe"
261
+ ? windowsNamedPipeUnreachableMessage(socket.path, daemonRunning, failure)
262
+ : `hqd is not reachable on ${socket.path}; version, uptime, flag state, and cwd binding are unavailable`;
212
263
  const socketNotUserOnly = health.reachable && socket.transport === "unix_socket" && (socket.userOnly !== true || socket.ownerMatchesUser !== true);
213
264
  const warningNotes = [
214
265
  healthUnsupported ? "resident hqd does not support detailed health; restart hqd for full diagnostics" : null,