@indigoai-us/hq-cli 5.345.148 → 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 (65) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +2 -0
  3. package/dist/command-catalog.generated.d.ts +44 -8
  4. package/dist/command-catalog.generated.js +53 -8
  5. package/dist/command-help.js +2 -1
  6. package/dist/commands/access.d.ts +45 -2
  7. package/dist/commands/access.js +537 -223
  8. package/dist/commands/billing-usage.d.ts +33 -2
  9. package/dist/commands/billing-usage.js +75 -4
  10. package/dist/commands/bot.js +2 -2
  11. package/dist/commands/channels.d.ts +27 -0
  12. package/dist/commands/channels.js +108 -4
  13. package/dist/commands/core-refresh-vault-access-native.d.ts +16 -0
  14. package/dist/commands/core-refresh-vault-access-native.js +142 -9
  15. package/dist/commands/core.js +4 -1
  16. package/dist/commands/dm.js +9 -1
  17. package/dist/commands/install-global.d.ts +1 -8
  18. package/dist/commands/install-global.js +11 -27
  19. package/dist/commands/library-add.d.ts +31 -1
  20. package/dist/commands/library-add.js +69 -2
  21. package/dist/commands/library.js +37 -3
  22. package/dist/commands/reindex.js +19 -0
  23. package/dist/hook-engine-path.js +10 -3
  24. package/dist/index.d.ts +10 -0
  25. package/dist/index.js +70 -5
  26. package/dist/lazy-commands.js +2 -0
  27. package/dist/lib/billing/usage-cost.d.ts +31 -1
  28. package/dist/lib/billing/usage-cost.js +46 -0
  29. package/dist/lib/db/local.d.ts +2 -2
  30. package/dist/lib/db/local.js +2 -2
  31. package/dist/lib/db/sql.js +2 -2
  32. package/dist/lib/db/sqlite-binding.d.ts +22 -0
  33. package/dist/lib/db/sqlite-binding.js +168 -0
  34. package/dist/lib/doctor/checks/anywhere.d.ts +12 -0
  35. package/dist/lib/doctor/checks/anywhere.js +63 -12
  36. package/dist/lib/doctor/checks/cli-path.d.ts +2 -1
  37. package/dist/lib/doctor/checks/cli-path.js +2 -1
  38. package/dist/lib/doctor/checks/hook-engine.js +2 -1
  39. package/dist/lib/hook-engine/reconcile.d.ts +9 -0
  40. package/dist/lib/hook-engine/reconcile.js +134 -0
  41. package/dist/lib/hook-engine/registration.d.ts +17 -0
  42. package/dist/lib/hook-engine/registration.js +50 -0
  43. package/dist/lib/install/claude-targets.d.ts +2 -1
  44. package/dist/lib/install/claude-targets.js +8 -3
  45. package/dist/lib/install/codex-targets.d.ts +3 -0
  46. package/dist/lib/install/codex-targets.js +15 -4
  47. package/dist/lib/install/hook-target.d.ts +9 -0
  48. package/dist/lib/install/hook-target.js +39 -0
  49. package/dist/lib/windows/console-encoding.d.ts +24 -0
  50. package/dist/lib/windows/console-encoding.js +83 -0
  51. package/dist/lib/windows/install-followup.d.ts +35 -0
  52. package/dist/lib/windows/install-followup.js +142 -0
  53. package/dist/lib/windows/user-path.d.ts +80 -3
  54. package/dist/lib/windows/user-path.js +187 -26
  55. package/dist/main.js +2 -1
  56. package/dist/unknown-command.d.ts +18 -0
  57. package/dist/unknown-command.js +88 -0
  58. package/dist/utils/cognito-session.d.ts +8 -0
  59. package/dist/utils/cognito-session.js +14 -2
  60. package/dist/utils/hook-trust.d.ts +9 -0
  61. package/dist/utils/hook-trust.js +30 -2
  62. package/dist/utils/qmd-native-binding-error.js +3 -1
  63. package/dist/utils/resolve-executable.d.ts +2 -0
  64. package/dist/utils/resolve-executable.js +6 -2
  65. package/package.json +8 -8
@@ -1,3 +1,4 @@
1
+ import { hookTargetFor } from '../lib/install/hook-target.js';
1
2
  /**
2
3
  * `hq install --global --runtime claude` / `hq uninstall --global --runtime claude`
3
4
  * (hq-anywhere-runtime US-005).
@@ -25,34 +26,11 @@ import { readRegistry, registryDir, registryFile, writeRegistry } from '../lib/r
25
26
  import { readCompaniesManifestMap } from '../lib/work-context/repo-remote.js';
26
27
  import { resolveDefaultHqRoot } from '../utils/hq-root-resolver.js';
27
28
  import { trustCodexUserHooks } from '../utils/hook-trust.js';
28
- import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from '../lib/daemon/install.js';
29
- import { readDaemonConfig } from '../lib/daemon/config.js';
30
- import { daemonPaths } from '../lib/daemon/paths.js';
31
29
  import { assertProviderWrappersInstallable, installProviderWrappers, providerWrapperInstallPaths, removeProviderWrappers } from '../lib/install/provider-wrappers.js';
32
- import { checkShimOnPath, shimPathWarning } from '../lib/windows/user-path.js';
30
+ import { checkShimOnPath, ensureShimOnUserPath, shimPathWarning } from '../lib/windows/user-path.js';
33
31
  import { addAgentsMdBlock, addCodexHooks, classifyLegacySkills, codexAdapterPath, codexCharterSource, codexHookTrustKeys, codexTargets, hasAllCodexHooks, HQ_OWNED_MARKER, LEGACY_BACKUP_NAME, LEGACY_SKILLS_NAME, removeAgentsMdBlock, removeCodexHooks, } from '../lib/install/codex-targets.js';
34
32
  import { addClaudeMdBlock, addHqHooks, charterPath, claudeTargets, hasAllHqHooks, HQ_ANYWHERE_PACK, HQ_MCP_SERVER_NAME, hqSkillsSource, lineDiff, masterHookPath, shimPath, MCP_SUBPROCESS_PATH, removeClaudeMdBlock, removeHqHooks, SKILL_LINK_PREFIX, } from '../lib/install/claude-targets.js';
35
- /** True when an hq daemon unit (launchd plist or systemd unit) exists. */
36
- export function isDaemonInstalled(home, platform = process.platform, queryTask = (task) => spawnSync('schtasks.exe', ['/Query', '/TN', task], { encoding: 'utf8', windowsHide: true }).status === 0) {
37
- if (platform === 'win32')
38
- return queryTask(DAEMON_WINDOWS_TASK);
39
- const candidates = platform === 'darwin'
40
- ? [path.join(home, 'Library', 'LaunchAgents', `${DAEMON_LAUNCHD_LABEL}.plist`)]
41
- : platform === 'linux'
42
- ? [path.join(home, '.config', 'systemd', 'user', DAEMON_SYSTEMD_UNIT), path.join(SYSTEM_UNIT_DIR, DAEMON_SYSTEMD_UNIT)]
43
- : [];
44
- return candidates.some((p) => fs.existsSync(p));
45
- }
46
- /**
47
- * US-010: with hqd installed (and the checkout shipping the shim), hooks go to
48
- * the hqd shim; otherwise to master-hook.sh / the Codex adapter.
49
- */
50
- export function hookTargetFor(hqRoot, home, daemonInstalled, env = process.env) {
51
- const installed = daemonInstalled ?? isDaemonInstalled(home);
52
- const configuredRoot = readDaemonConfig(daemonPaths(home, env).configPath).hqRoot;
53
- const daemonMatchesRoot = configuredRoot !== undefined && realpathOrSelf(configuredRoot) === realpathOrSelf(hqRoot);
54
- return installed && daemonMatchesRoot && fs.existsSync(shimPath(hqRoot)) ? 'shim' : 'master';
55
- }
33
+ export { hookTargetFor, isDaemonInstalled } from '../lib/install/hook-target.js';
56
34
  const HQ_PACKAGES_MARKETPLACE_REPOSITORY = 'indigoai-us/hq-packages';
57
35
  /** Resolve local overrides first, then the published HQ marketplace. */
58
36
  export function resolveClaudePluginSource(explicitDir, env = process.env, repository = HQ_PACKAGES_MARKETPLACE_REPOSITORY) {
@@ -615,7 +593,7 @@ export function installGlobalClaude(opts) {
615
593
  if (pluginSource)
616
594
  log(` plugin: ${pluginSource.pluginId} installed from ${pluginSource.sourceDir}; direct hooks and MCP entries removed`);
617
595
  else {
618
- log(` hooks: ${hookTarget === 'shim' ? 'hqd-hook-shim.sh' : 'master-hook.sh'} registered in ${t.settingsJson}`);
596
+ log(` hooks: ${typeof hookTarget === 'object' ? hookTarget.engine : hookTarget === 'shim' ? 'hqd-hook-shim.sh' : 'master-hook.sh'} registered in ${t.settingsJson}`);
619
597
  log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.claudeJson}`);
620
598
  }
621
599
  log(` registry: ${seeded.length} manifest entr${seeded.length === 1 ? 'y' : 'ies'} seeded`);
@@ -994,7 +972,7 @@ export function installGlobalCodex(opts) {
994
972
  log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
995
973
  reportSkillCopies(linkKinds, log);
996
974
  reportSkippedSkills(skills, log, false);
997
- log(` hooks: ${hookTarget === 'shim' ? shimPath(hqRoot) : codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
975
+ log(` hooks: ${typeof hookTarget === 'object' ? hookTarget.engine : hookTarget === 'shim' ? shimPath(hqRoot) : codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
998
976
  log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.configToml}`);
999
977
  return touched;
1000
978
  }
@@ -1213,6 +1191,12 @@ export async function runInstallGlobal(opts) {
1213
1191
  if (runtime === 'codex' && !opts.dryRun)
1214
1192
  await trustInstalledCodexHooks(hqRoot, opts);
1215
1193
  if (!opts.dryRun) {
1194
+ try {
1195
+ ensureShimOnUserPath({ env, entry: process.argv[1] });
1196
+ }
1197
+ catch {
1198
+ // The warning below is the fallback when the user Path cannot be updated.
1199
+ }
1216
1200
  const shimPath = (opts.shimPathCheck ?? (() => checkShimOnPath({ env })))();
1217
1201
  if (shimPath)
1218
1202
  for (const line of shimPathWarning(shimPath))
@@ -1,5 +1,5 @@
1
1
  import { Readable } from "node:stream";
2
- import { type CloudAssetMediaKind } from "@indigoai-us/hq-cloud";
2
+ import { type CloudAssetMediaKind, type PullScope, type PullScopeClient } from "@indigoai-us/hq-cloud";
3
3
  import type { LibraryCaptionNote, LibraryCaptionRequest } from "./library-caption.js";
4
4
  export interface LibraryCompany {
5
5
  uid: string;
@@ -37,6 +37,13 @@ export interface LibraryAddDeps {
37
37
  * a caption failure never fails the upload. Absent = no captioning.
38
38
  */
39
39
  caption?: (request: LibraryCaptionRequest) => Promise<LibraryCaptionNote>;
40
+ /**
41
+ * The membership's sync scope for the company. Throws when it cannot be
42
+ * read. Absent = the scope is not checked and nothing is pinned.
43
+ */
44
+ syncScope?: (company: LibraryCompany) => Promise<Pick<PullScope, "syncMode" | "prefixSet">>;
45
+ /** `hq files get`'s pin writer (`.hq/pins.json`). */
46
+ addPin?: (hqRoot: string, companySlug: string, prefix: string) => void;
40
47
  }
41
48
  export interface LibraryAddOptions {
42
49
  company?: string;
@@ -60,9 +67,19 @@ export interface LibraryAddItem {
60
67
  /** AI caption outcome, set when captioning ran for this item. */
61
68
  caption?: LibraryCaptionNote;
62
69
  }
70
+ export interface LibrarySyncScopeNote {
71
+ status: "in-scope" | "pinned" | "not-pinned";
72
+ /** Company-relative pins written (`pinned`) or still needed (`not-pinned`). */
73
+ pins: string[];
74
+ /** Set when not pinned and `hq files get` can pin by hand (the library folder). */
75
+ command?: string;
76
+ reason?: string;
77
+ }
63
78
  export interface LibraryAddResult {
64
79
  company: string;
65
80
  items: LibraryAddItem[];
81
+ /** Set when at least one placeholder was written and the scope was checked. */
82
+ syncScope?: LibrarySyncScopeNote;
66
83
  }
67
84
  export declare function contentTypeFor(name: string, served?: string): string;
68
85
  export declare function mediaKindFor(contentType: string): CloudAssetMediaKind;
@@ -89,6 +106,19 @@ export declare function offloadTimestamp(now: Date): string;
89
106
  * only that source.
90
107
  */
91
108
  export declare function runLibraryAdd(inputs: readonly string[], options: LibraryAddOptions, deps: LibraryAddDeps): Promise<LibraryAddResult>;
109
+ /**
110
+ * A `shared` or `custom` sync pushes only paths inside the membership's
111
+ * prefix set (explicit grants plus pins), so a placeholder written outside it
112
+ * never reaches the vault. Pin it with `hq files get`'s pin writer: the
113
+ * default library folder as a whole, and for any other `--to` folder only the
114
+ * written placeholders, so the rest of that folder stays out of sync.
115
+ */
116
+ export declare function ensureLibraryInSyncScope(company: LibraryCompany, dirRel: string, placeholderPaths: readonly string[], deps: Pick<LibraryAddDeps, "hqRoot" | "syncScope" | "addPin">): Promise<LibrarySyncScopeNote | undefined>;
117
+ /**
118
+ * {@link resolvePullScope} answers `all` when any call fails. Report the
119
+ * failure instead, so a failed lookup is not mistaken for full sync.
120
+ */
121
+ export declare function resolveLibrarySyncScope(client: Required<PullScopeClient>, company: LibraryCompany, hqRoot: string): Promise<PullScope>;
92
122
  export interface LibraryTagResult {
93
123
  placeholderPath: string;
94
124
  tags: string[];
@@ -28,7 +28,7 @@ import http from "node:http";
28
28
  import https from "node:https";
29
29
  import path from "node:path";
30
30
  import { Readable } from "node:stream";
31
- import { formatCloudAssetBlock, formatCloudPlaceholder, parseCloudPlaceholder, } from "@indigoai-us/hq-cloud";
31
+ import { formatCloudAssetBlock, formatCloudPlaceholder, isCoveredByAny, parseCloudPlaceholder, resolvePullScope, } from "@indigoai-us/hq-cloud";
32
32
  import { PLACEHOLDER_SUFFIX, companyOffloadKey, containedPath, durablePlaceholderFs, renderPointer, resolvePlaceholder, } from "./storage-offload.js";
33
33
  import { formatPlanGateError, formatUpgradeRefusal, planGateErrorFromResponse, } from "../utils/plan-gate-error.js";
34
34
  /** Metadata key hq-pro binds to `checksumSha256` (sync's content hash). */
@@ -321,7 +321,74 @@ export async function runLibraryAdd(inputs, options, deps) {
321
321
  .catch((err) => ({ status: "failed", note: err.message }));
322
322
  }
323
323
  }
324
- return { company: slug, items };
324
+ const written = items.flatMap((item) => (item.ok && item.placeholderPath ? [item.placeholderPath] : []));
325
+ if (written.length === 0)
326
+ return { company: slug, items };
327
+ const syncScope = await ensureLibraryInSyncScope(company, dirRel, written, deps);
328
+ return { company: slug, items, ...(syncScope ? { syncScope } : {}) };
329
+ }
330
+ const LIBRARY_FOLDER = "library/";
331
+ /**
332
+ * A `shared` or `custom` sync pushes only paths inside the membership's
333
+ * prefix set (explicit grants plus pins), so a placeholder written outside it
334
+ * never reaches the vault. Pin it with `hq files get`'s pin writer: the
335
+ * default library folder as a whole, and for any other `--to` folder only the
336
+ * written placeholders, so the rest of that folder stays out of sync.
337
+ */
338
+ export async function ensureLibraryInSyncScope(company, dirRel, placeholderPaths, deps) {
339
+ if (!deps.syncScope || !deps.addPin)
340
+ return undefined;
341
+ const companyRel = (rel) => rel.split("/").slice(2).join("/");
342
+ const isLibrary = `${companyRel(dirRel)}/` === LIBRARY_FOLDER;
343
+ const targets = isLibrary ? [LIBRARY_FOLDER] : placeholderPaths.map(companyRel);
344
+ const command = isLibrary ? `hq files get ${LIBRARY_FOLDER} --company ${company.slug}` : undefined;
345
+ let scope;
346
+ try {
347
+ scope = await deps.syncScope(company);
348
+ }
349
+ catch (err) {
350
+ return {
351
+ status: "not-pinned",
352
+ pins: targets,
353
+ ...(command ? { command } : {}),
354
+ reason: `could not read your sync scope: ${err.message}`,
355
+ };
356
+ }
357
+ const missing = scope.syncMode === "all" ? [] : targets.filter((t) => !isCoveredByAny(t, scope.prefixSet ?? []));
358
+ if (missing.length === 0)
359
+ return { status: "in-scope", pins: [] };
360
+ try {
361
+ for (const target of missing)
362
+ deps.addPin(deps.hqRoot, company.slug, target);
363
+ }
364
+ catch (err) {
365
+ return {
366
+ status: "not-pinned",
367
+ pins: missing,
368
+ ...(command ? { command } : {}),
369
+ reason: `could not write the pin: ${err.message}`,
370
+ };
371
+ }
372
+ return { status: "pinned", pins: missing };
373
+ }
374
+ /**
375
+ * {@link resolvePullScope} answers `all` when any call fails. Report the
376
+ * failure instead, so a failed lookup is not mistaken for full sync.
377
+ */
378
+ export async function resolveLibrarySyncScope(client, company, hqRoot) {
379
+ let failure;
380
+ const watch = (call) => call.catch((err) => {
381
+ failure ??= err;
382
+ throw err;
383
+ });
384
+ const scope = await resolvePullScope({
385
+ listMyMemberships: () => watch(client.listMyMemberships()),
386
+ getMembershipSyncConfig: (id) => watch(client.getMembershipSyncConfig(id)),
387
+ listMyExplicitGrants: (uid) => watch(client.listMyExplicitGrants(uid)),
388
+ }, company.uid, company.slug, hqRoot);
389
+ if (failure !== undefined)
390
+ throw failure instanceof Error ? failure : new Error(String(failure));
391
+ return scope;
325
392
  }
326
393
  async function writeAssetPlaceholder(p, options, deps, dirAbs) {
327
394
  const ts = offloadTimestamp(deps.now());
@@ -35,7 +35,7 @@ import { findHqRoot } from "../utils/manifest.js";
35
35
  import { networkTransportErrorCode } from "../utils/network-transport-error.js";
36
36
  import { resolveVaultCredentialForCapability } from "../utils/resolve-vault-credential.js";
37
37
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
38
- import { defaultLibraryIo, runLibraryAdd, runLibraryTag, } from "./library-add.js";
38
+ import { defaultLibraryIo, resolveLibrarySyncScope, runLibraryAdd, runLibraryTag, } from "./library-add.js";
39
39
  import { captionAddedAsset, extractCaptionFrames, runLibraryDescribe, runLocalClaude } from "./library-caption.js";
40
40
  import { LIBRARY_LINK_DEFAULT_MINUTES, LIBRARY_LINK_MAX_MINUTES, runLibraryLink } from "./library-link.js";
41
41
  /** Mirrors hq-pro's MAX_ASSET_SEARCH_TAGS / tag length bound. */
@@ -295,8 +295,39 @@ export function formatLibraryAddResult(result) {
295
295
  return `${line}\n ${chalk.dim(`AI caption added (${item.caption.captionedBy}).`)}`;
296
296
  return `${line}\n ${chalk.yellow(item.caption.note ?? "Captioning skipped.")}`;
297
297
  })
298
+ .concat(formatLibrarySyncScope(result))
298
299
  .join("\n");
299
300
  }
301
+ function formatLibrarySyncScope({ company, syncScope }) {
302
+ if (!syncScope || syncScope.status === "in-scope")
303
+ return [];
304
+ const pins = syncScope.pins.map((pin) => `${company}:${pin}`).join(", ");
305
+ if (syncScope.status === "pinned") {
306
+ return [chalk.dim(`Pinned ${pins} so sync uploads these placeholders to the company vault.`)];
307
+ }
308
+ const fix = syncScope.command
309
+ ? `Pin the folder with: ${syncScope.command}`
310
+ : `Add ${syncScope.pins.join(", ")} under "${company}" in .hq/pins.json at the HQ root.`;
311
+ return [
312
+ chalk.yellow(`Warning: sync may not upload these placeholders (${syncScope.reason}). ` +
313
+ `Until it does, teammates and cloud search will not see them. ${fix}`),
314
+ ];
315
+ }
316
+ /** Sync scope wiring for `hq library add`; an API key has no sync tree to check. */
317
+ async function librarySyncScopeDeps(credential, hqRoot) {
318
+ if (credential.kind !== "cognito")
319
+ return {};
320
+ const [{ VaultClient }, { buildVaultConfig }, { addPin }] = await Promise.all([
321
+ import("@indigoai-us/hq-cloud"),
322
+ import("../utils/cognito-session.js"),
323
+ import("./files-browse.js"),
324
+ ]);
325
+ const client = new VaultClient(buildVaultConfig(credential.token));
326
+ return {
327
+ syncScope: (company) => resolveLibrarySyncScope(client, company, hqRoot),
328
+ addPin,
329
+ };
330
+ }
300
331
  /** GET a presigned URL. Errors never echo the URL, which carries a signature. */
301
332
  async function downloadPresigned(url) {
302
333
  const res = await fetch(url);
@@ -366,9 +397,11 @@ export function registerLibraryCommand(program) {
366
397
  .option("--json", "Print machine-readable JSON")
367
398
  .action(async (sources, options) => {
368
399
  try {
369
- const { token } = await resolveVaultCredentialForCapability("files:write", "library add", {
400
+ const credential = await resolveVaultCredentialForCapability("files:write", "library add", {
370
401
  interactive: true,
371
402
  });
403
+ const { token } = credential;
404
+ const hqRoot = findHqRoot();
372
405
  const result = await runLibraryAdd(sources, {
373
406
  company: options.company,
374
407
  to: options.to,
@@ -376,11 +409,12 @@ export function registerLibraryCommand(program) {
376
409
  title: options.title,
377
410
  note: options.note,
378
411
  }, {
379
- hqRoot: findHqRoot(),
412
+ hqRoot,
380
413
  now: () => new Date(),
381
414
  resolveCompany: (slug) => libraryCompany(token, slug),
382
415
  presign: (body) => vaultApiFetch({ token, path: "/v1/files/presign", method: "POST", body }),
383
416
  ...defaultLibraryIo,
417
+ ...(await librarySyncScopeDeps(credential, hqRoot)),
384
418
  ...(options.caption === false
385
419
  ? {}
386
420
  : {
@@ -1,3 +1,5 @@
1
+ import { reconcileHookRegistrations } from '../lib/hook-engine/reconcile.js';
2
+ import { trustCodexUserHooks } from '../utils/hook-trust.js';
1
3
  /**
2
4
  * hq reindex — surface namespaced skills and commands, clean up retired
3
5
  * personal-overlay mirror links, capture coding-harness session logs, regenerate
@@ -775,6 +777,23 @@ export function registerReindexCommand(program) {
775
777
  // means a sync/rescue holds the shared operation lock and may be
776
778
  // rewriting .claude/settings.json right now, and this is an unlocked
777
779
  // whole-file read-modify-write.
780
+ // Registration reconcile is best-effort: a failure leaves the current
781
+ // registrations in place and must not stop the rest of reindex.
782
+ try {
783
+ // A file that failed is left as it was; the keys of files that were
784
+ // rewritten are still trusted so Codex keeps running their hooks.
785
+ const { codexUserKeys, failures } = reconcileHookRegistrations(hqRoot);
786
+ for (const failure of failures)
787
+ console.warn(`reindex: hook registration reconcile skipped ${failure}`);
788
+ if (codexUserKeys.length) {
789
+ const trust = await trustCodexUserHooks(hqRoot, codexUserKeys);
790
+ if (trust.status === 'failed')
791
+ console.warn(`reindex: Codex hook trust needs repair: ${trust.reason}`);
792
+ }
793
+ }
794
+ catch (error) {
795
+ console.warn(`reindex: hook registration reconcile skipped: ${error.message}`);
796
+ }
778
797
  dropProjectDefaultModeForBypassUser(hqRoot);
779
798
  await trustHqRuntimeHooks(hqRoot, undefined, {
780
799
  rootAliases: [literalHqRoot],
@@ -11,14 +11,21 @@ export function hookEnginePathArgs(argv) {
11
11
  }
12
12
  export async function runHookEnginePath(args) {
13
13
  try {
14
- if (args.some(arg => !['--repair', '--help', '-h'].includes(arg))) {
15
- throw new Error('Usage: hq core hook-engine-path [--repair]');
14
+ if (args.some(arg => !['--repair', '--help', '-h', '--for-registration'].includes(arg))) {
15
+ throw new Error('Usage: hq core hook-engine-path [--repair] [--for-registration]');
16
16
  }
17
17
  if (args.includes('--help') || args.includes('-h')) {
18
- console.log('Usage: hq core hook-engine-path [--repair]\nResolve and pin the installed native hook engine offline.');
18
+ console.log('Usage: hq core hook-engine-path [--repair] [--for-registration]\nResolve and pin the installed native hook engine offline.\n--for-registration prints a path only when the launcher flag is enabled.');
19
19
  return;
20
20
  }
21
21
  const { installHookEngine, hookEngineRequired, HOOK_ENGINE_REPAIR } = await import('./lib/hook-engine/resolve.js');
22
+ if (args.includes('--for-registration')) {
23
+ const { launcherEnabled } = await import('./lib/hook-engine/registration.js');
24
+ if (!launcherEnabled()) {
25
+ process.exitCode = 1;
26
+ return;
27
+ }
28
+ }
22
29
  const binary = installHookEngine({ repair: args.includes('--repair') });
23
30
  if (binary)
24
31
  console.log(binary);
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");