@indigoai-us/hq-cli 5.345.134 → 5.345.136

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 (100) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/assets/scaffold/core/scripts/worktree-gc.sh +98 -8
  3. package/dist/command-catalog.generated.d.ts +62 -6
  4. package/dist/command-catalog.generated.js +79 -7
  5. package/dist/command-registration-plan.d.ts +6 -0
  6. package/dist/command-registration-plan.js +1 -0
  7. package/dist/commands/core-bucket-c-handoff-knowledge-native.d.ts +3 -1
  8. package/dist/commands/core-bucket-c-handoff-knowledge-native.js +8 -4
  9. package/dist/commands/core-checkpoint-native.d.ts +10 -0
  10. package/dist/commands/core-checkpoint-native.js +60 -15
  11. package/dist/commands/core-handoff.d.ts +2 -0
  12. package/dist/commands/core-handoff.js +3 -1
  13. package/dist/commands/core-pipeline-workflow-runner-native.js +7 -5
  14. package/dist/commands/daemon.js +3 -1
  15. package/dist/commands/fingerprint.d.ts +10 -0
  16. package/dist/commands/fingerprint.js +191 -0
  17. package/dist/commands/integrations-core.d.ts +22 -0
  18. package/dist/commands/integrations-core.js +25 -0
  19. package/dist/commands/integrations-manage.d.ts +24 -1
  20. package/dist/commands/integrations-manage.js +92 -17
  21. package/dist/commands/integrations.d.ts +11 -0
  22. package/dist/commands/integrations.js +100 -19
  23. package/dist/commands/lanes-loop.d.ts +9 -0
  24. package/dist/commands/lanes-loop.js +9 -1
  25. package/dist/commands/lanes.js +78 -55
  26. package/dist/commands/meetings.d.ts +2 -0
  27. package/dist/commands/meetings.js +57 -14
  28. package/dist/commands/mesh.d.ts +19 -1
  29. package/dist/commands/mesh.js +158 -70
  30. package/dist/commands/reindex.js +80 -5
  31. package/dist/commands/storage-offload.d.ts +93 -28
  32. package/dist/commands/storage-offload.js +290 -34
  33. package/dist/commands/storage.js +60 -7
  34. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2550 -1748
  35. package/dist/hq-cloud-sync-runner/package.json +1 -1
  36. package/dist/index.js +4 -0
  37. package/dist/lib/capacity/route.js +24 -0
  38. package/dist/lib/core-utils/session-company.d.ts +11 -0
  39. package/dist/lib/core-utils/session-company.js +34 -0
  40. package/dist/lib/core-utils/worktree-resume.js +44 -4
  41. package/dist/lib/core-utils/worktree-session-owner.d.ts +27 -0
  42. package/dist/lib/core-utils/worktree-session-owner.js +157 -0
  43. package/dist/lib/daemon/catalog.d.ts +2 -0
  44. package/dist/lib/daemon/catalog.js +3 -3
  45. package/dist/lib/daemon/hq-child-bootstrap-stage.d.ts +5 -0
  46. package/dist/lib/daemon/hq-child-bootstrap-stage.js +3 -11
  47. package/dist/lib/daemon/server.js +10 -1
  48. package/dist/lib/daemon/supervisor.js +6 -0
  49. package/dist/lib/daemon/sync-runner-bundle.d.ts +5 -0
  50. package/dist/lib/daemon/sync-runner-bundle.js +4 -12
  51. package/dist/lib/doctor/checks/claude-wiring.js +2 -1
  52. package/dist/lib/doctor/fix/apply.d.ts +20 -0
  53. package/dist/lib/doctor/fix/apply.js +63 -3
  54. package/dist/lib/doctor/fix/remediation.d.ts +1 -1
  55. package/dist/lib/doctor/fix/remediation.js +16 -0
  56. package/dist/lib/doctor/shared.d.ts +11 -1
  57. package/dist/lib/doctor/shared.js +16 -2
  58. package/dist/lib/fingerprint/fixtures.d.ts +19 -0
  59. package/dist/lib/fingerprint/fixtures.js +61 -0
  60. package/dist/lib/fingerprint/format.d.ts +50 -0
  61. package/dist/lib/fingerprint/format.js +105 -0
  62. package/dist/lib/fingerprint/offline.d.ts +16 -0
  63. package/dist/lib/fingerprint/offline.js +44 -0
  64. package/dist/lib/fingerprint/registry-client.d.ts +74 -0
  65. package/dist/lib/fingerprint/registry-client.js +141 -0
  66. package/dist/lib/fingerprint/resolve-local.d.ts +14 -0
  67. package/dist/lib/fingerprint/resolve-local.js +67 -0
  68. package/dist/lib/fingerprint/self-core.d.ts +139 -0
  69. package/dist/lib/fingerprint/self-core.js +371 -0
  70. package/dist/lib/fingerprint/self.d.ts +44 -0
  71. package/dist/lib/fingerprint/self.js +191 -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/lanes/monitor.js +14 -2
  75. package/dist/lib/lanes/spawn.d.ts +19 -1
  76. package/dist/lib/lanes/spawn.js +50 -6
  77. package/dist/lib/lanes/story-ledger.d.ts +1 -0
  78. package/dist/lib/lanes/story-ledger.js +3 -1
  79. package/dist/lib/lanes/types.d.ts +12 -0
  80. package/dist/lib/lanes/types.js +12 -1
  81. package/dist/lib/mesh/live/daemon/install.d.ts +12 -0
  82. package/dist/lib/mesh/live/daemon/install.js +22 -0
  83. package/dist/lib/mesh/live/flush.js +106 -3
  84. package/dist/lib/mesh/live/session-events-client.d.ts +9 -0
  85. package/dist/lib/mesh/live/session-events-client.js +15 -0
  86. package/dist/lib/work-context/state.d.ts +10 -0
  87. package/dist/lib/work-context/state.js +5 -0
  88. package/dist/utils/cli-network-lifecycle.d.ts +2 -0
  89. package/dist/utils/cli-network-lifecycle.js +5 -1
  90. package/dist/utils/cognito-session.d.ts +2 -21
  91. package/dist/utils/cognito-session.js +3 -84
  92. package/dist/utils/fsync-directory.d.ts +17 -0
  93. package/dist/utils/fsync-directory.js +27 -0
  94. package/dist/utils/person-token-cache.d.ts +32 -0
  95. package/dist/utils/person-token-cache.js +95 -0
  96. package/dist/utils/vault-api.d.ts +21 -1
  97. package/dist/utils/vault-api.js +54 -5
  98. package/dist/utils/windows-hide-default.d.ts +42 -0
  99. package/dist/utils/windows-hide-default.js +98 -0
  100. package/package.json +2 -2
@@ -46,7 +46,7 @@ import { removeStrayGateEntries } from "../stray-gate-entries.js";
46
46
  import { deriveRemediation } from "./remediation.js";
47
47
  import { botDir } from "../../bot/paths.js";
48
48
  import { readBotConfig } from "../../bot/config.js";
49
- import { buildBotDaemonPaths, rewriteBotDaemonNodePath } from "../../bot/daemon.js";
49
+ import { buildBotDaemonPaths, installedDaemonHqBinary, repairBotDaemonIfStale, rewriteBotDaemonNodePath, } from "../../bot/daemon.js";
50
50
  import { classifyRefreshSyncOutcome, } from "./refresh-sync-outcome.js";
51
51
  import { readRefreshSyncProgress, writeRefreshSyncCursor, writeRefreshSyncLastRun, } from "./refresh-sync-progress.js";
52
52
  import { clientHealthShowManualOnlyEnabled } from "../client-health-reason-codes-flag.js";
@@ -130,6 +130,7 @@ export async function applyFixes(options) {
130
130
  const collect = options.runChecks ?? defaultRunChecks;
131
131
  const refreshSync = options.refreshSync ?? defaultRefreshSync;
132
132
  const rewriteBotNodePath = options.rewriteBotNodePath ?? defaultRewriteBotNodePath;
133
+ const repairBotHqBinary = options.repairBotHqBinary ?? defaultRepairBotHqBinary;
133
134
  // 2. Collect findings, then plan each newly unlocked allowlisted repair.
134
135
  // Identity is deliberately the check id + target pair: an unsuccessful
135
136
  // repair must not be applied repeatedly in one invocation.
@@ -154,7 +155,7 @@ export async function applyFixes(options) {
154
155
  const rem = deriveRemediation(result);
155
156
  if (!rem?.autoFixable)
156
157
  continue;
157
- const plan = planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath);
158
+ const plan = planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath, repairBotHqBinary);
158
159
  if (!plan)
159
160
  continue;
160
161
  const key = JSON.stringify([plan.checkId, plan.target]);
@@ -421,10 +422,12 @@ export function uncommittedHookConfigChanges(hqRoot) {
421
422
  }
422
423
  // --- Fix planning --------------------------------------------------------------
423
424
  /** Build the concrete plan for one auto-fixable finding, or null if unplannable. */
424
- function planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath = defaultRewriteBotNodePath) {
425
+ function planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath = defaultRewriteBotNodePath, repairBotHqBinary = defaultRepairBotHqBinary) {
425
426
  switch (rem.fixClass) {
426
427
  case "bot-node-path":
427
428
  return planBotNodePath(result, rem, rewriteBotNodePath);
429
+ case "bot-hq-binary":
430
+ return planBotHqBinary(result, rem, repairBotHqBinary);
428
431
  case "executable-bit":
429
432
  return planExecutableBit(hqRoot, result, rem);
430
433
  case "gate-profile":
@@ -467,6 +470,63 @@ export function defaultRewriteBotNodePath(name) {
467
470
  return null;
468
471
  return rewriteBotDaemonNodePath(buildBotDaemonPaths({ name, botDir: dir, hqRoot: config.hqRoot, hqBinary: "hq" }), { platform: "darwin" });
469
472
  }
473
+ /** Reinstall one bot LaunchAgent whose hq binary is missing. */
474
+ function planBotHqBinary(result, rem, repair) {
475
+ const name = rem.fixTarget;
476
+ if (!name)
477
+ return null;
478
+ return {
479
+ checkId: result.checkId,
480
+ fixClass: "bot-hq-binary",
481
+ target: name,
482
+ relpath: "",
483
+ preview: ` ~/Library/LaunchAgents/ai.getindigo.hq-bot.${name}.plist\n` +
484
+ ` ~ hq path -> the installed hq (same as hq bot daemon install ${name})\n` +
485
+ ` the agent is reloaded; its old hq path is missing, so it was not running`,
486
+ summary: `reinstall ${name} startup agent against the installed hq`,
487
+ apply: () => {
488
+ repair(name);
489
+ },
490
+ postStatus: (results) => results.find((entry) => entry.checkId === result.checkId)?.status ?? null,
491
+ };
492
+ }
493
+ /** Real launchctl for the `bot-hq-binary` reload; a failure is a non-ok result. */
494
+ const doctorLaunchctl = (args) => {
495
+ const r = spawnSync("launchctl", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
496
+ return { ok: r.status === 0, stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? r.error?.message ?? "") };
497
+ };
498
+ /**
499
+ * An hq path a bot may be pointed at unattended: an installed package entry
500
+ * or its stable shim, never a worktree or one-off build that happens to be
501
+ * running `hq doctor --fix`.
502
+ */
503
+ export function isInstalledHqBinary(hqBinary) {
504
+ const normalized = hqBinary.replace(/\\/g, "/");
505
+ if (/\/node_modules\/@indigoai-us\/hq-cli\//i.test(normalized))
506
+ return true;
507
+ return /\/bin\/hq(\.cmd)?$/i.test(normalized);
508
+ }
509
+ /** Default `bot-hq-binary` repair against the real bot state and LaunchAgents. */
510
+ export function defaultRepairBotHqBinary(name) {
511
+ if (process.platform !== "darwin")
512
+ return null;
513
+ const dir = botDir(name);
514
+ const config = readBotConfig(dir);
515
+ if (!config)
516
+ return null;
517
+ const paths = buildBotDaemonPaths({ name, botDir: dir, hqRoot: config.hqRoot });
518
+ if (!isInstalledHqBinary(paths.hqBinary) || !fs.existsSync(paths.hqBinary))
519
+ return null;
520
+ let from;
521
+ try {
522
+ from = installedDaemonHqBinary(paths);
523
+ }
524
+ catch {
525
+ from = null;
526
+ }
527
+ const repaired = repairBotDaemonIfStale(paths, { launchctl: doctorLaunchctl });
528
+ return repaired ? { from, to: paths.hqBinary } : null;
529
+ }
470
530
  /** Pull one company or the personal vault to refresh a stale journal. */
471
531
  function planRefreshSync(hqRoot, result, rem, refreshSync) {
472
532
  const slug = rem.fixTarget;
@@ -40,7 +40,7 @@
40
40
  */
41
41
  import type { CheckResult } from "../types.js";
42
42
  /** The allowlisted safe repair classes `--fix` is permitted to apply. */
43
- export type FixClass = "executable-bit" | "gate-profile" | "remove-stray-gate-entries" | "refresh-sync" | "bot-node-path";
43
+ export type FixClass = "executable-bit" | "gate-profile" | "remove-stray-gate-entries" | "refresh-sync" | "bot-node-path" | "bot-hq-binary";
44
44
  /**
45
45
  * A finding's structured remediation. `autoFixable`, `action`, and `command`
46
46
  * are the `--json` surface (AC1); `fixTarget`/`fixClass` are the internal handle
@@ -119,6 +119,22 @@ export function deriveRemediation(result) {
119
119
  command: remediation ?? `hq bot start ${target}`,
120
120
  };
121
121
  }
122
+ // bot-hq-binary — a bot LaunchAgent whose hq path no longer exists (an
123
+ // update moved or removed it), so launchd cannot start the bot. The repair
124
+ // is what `hq bot daemon install <name>` does: rewrite the agent against
125
+ // the installed hq and reload it. A job whose hq is missing is not running a
126
+ // turn, so the reload cuts nothing short (feedback_c441dd9a).
127
+ const botBinary = /^bots\.(.+)\.daemon-binary$/.exec(checkId);
128
+ if (botBinary && result.status === "FAIL") {
129
+ const name = botBinary[1];
130
+ return {
131
+ autoFixable: true,
132
+ fixClass: "bot-hq-binary",
133
+ fixTarget: name,
134
+ action: `Reinstall the ${name} startup agent against the installed hq binary and reload it.`,
135
+ command: remediation ?? `hq bot daemon install ${name}`,
136
+ };
137
+ }
122
138
  // Available hq-core upgrade. WARN is intentional in `hq doctor` and is
123
139
  // not a broken sync; --fix has no mechanical repair for it.
124
140
  if (checkId === "sync.update.core") {
@@ -1,6 +1,16 @@
1
1
  /** Collect every `command` string from a `{hooks:{Event:[{hooks:[…]}]}}` doc. */
2
2
  export declare function collectCommands(parsed: unknown): string[];
3
- export declare function fileState(target: string): {
3
+ /**
4
+ * Whether a file mode counts as executable for a hook check.
5
+ *
6
+ * Windows has no POSIX execute bit: Node reports 0o666 for every regular file
7
+ * that is not a .exe/.cmd/.bat/.com, and the hook runtimes start HQ's shell
8
+ * hooks through bash, which never consults it. A mode check there can only
9
+ * produce false FAILs (feedback_a60339e8, 27 of them on one install), so
10
+ * win32 treats every present file as executable.
11
+ */
12
+ export declare function modeIsExecutable(mode: number, platform?: NodeJS.Platform): boolean;
13
+ export declare function fileState(target: string, platform?: NodeJS.Platform): {
4
14
  present: boolean;
5
15
  executable: boolean;
6
16
  };
@@ -23,10 +23,24 @@ export function collectCommands(parsed) {
23
23
  }
24
24
  return commands;
25
25
  }
26
- export function fileState(target) {
26
+ /**
27
+ * Whether a file mode counts as executable for a hook check.
28
+ *
29
+ * Windows has no POSIX execute bit: Node reports 0o666 for every regular file
30
+ * that is not a .exe/.cmd/.bat/.com, and the hook runtimes start HQ's shell
31
+ * hooks through bash, which never consults it. A mode check there can only
32
+ * produce false FAILs (feedback_a60339e8, 27 of them on one install), so
33
+ * win32 treats every present file as executable.
34
+ */
35
+ export function modeIsExecutable(mode, platform = process.platform) {
36
+ if (platform === "win32")
37
+ return true;
38
+ return (mode & 0o111) !== 0;
39
+ }
40
+ export function fileState(target, platform = process.platform) {
27
41
  try {
28
42
  const stat = fs.statSync(target);
29
- return { present: stat.isFile(), executable: (stat.mode & 0o111) !== 0 };
43
+ return { present: stat.isFile(), executable: modeIsExecutable(stat.mode, platform) };
30
44
  }
31
45
  catch {
32
46
  return { present: false, executable: false };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared fingerprint test vectors. hq-pro (US-006) copies this list so the CLI
3
+ * and the server agree on what a valid fingerprint is.
4
+ */
5
+ import type { FingerprintParts } from "./format.js";
6
+ export interface ValidFingerprintVector {
7
+ name: string;
8
+ parts: FingerprintParts;
9
+ value: string;
10
+ }
11
+ export interface InvalidFingerprintVector {
12
+ name: string;
13
+ value: string;
14
+ }
15
+ export declare const FINGERPRINT_TEST_VECTORS: {
16
+ valid: ValidFingerprintVector[];
17
+ invalid: InvalidFingerprintVector[];
18
+ };
19
+ //# sourceMappingURL=fixtures.d.ts.map
@@ -0,0 +1,61 @@
1
+ export const FINGERPRINT_TEST_VECTORS = {
2
+ valid: [
3
+ {
4
+ name: "person on a laptop",
5
+ parts: {
6
+ company: "indigo",
7
+ identityUid: "prs_01J8ZK4Q7M2N3P4R5S6T7V8W9X",
8
+ hostKind: "laptop",
9
+ machineId: "e3ef5a5a-f51d-435d-90b9-dd876caa249b",
10
+ sessionId: "fabb19c9-95a7-48a4-9a95-f77e28d0e3db",
11
+ },
12
+ value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9",
13
+ },
14
+ {
15
+ name: "agent on a fleet box",
16
+ parts: {
17
+ company: "acme",
18
+ identityUid: "agt_01JA0000000000000000000000",
19
+ hostKind: "fleet",
20
+ machineId: "0123456789abcdef",
21
+ sessionId: "sess-1",
22
+ },
23
+ value: "hqfp1:acme:agt_01JA0000:fleet.01234567:sess-1",
24
+ },
25
+ {
26
+ name: "outpost with no company",
27
+ parts: {
28
+ company: null,
29
+ identityUid: "prs_01J8ZK4Q7M2N3P4R5S6T7V8W9X",
30
+ hostKind: "outpost",
31
+ machineId: "ABCDEF12-0000-0000-0000-000000000000",
32
+ sessionId: null,
33
+ },
34
+ value: "hqfp1:-:prs_01J8ZK4Q:outpost.abcdef12:-",
35
+ },
36
+ {
37
+ name: "session only",
38
+ parts: {
39
+ company: null,
40
+ identityUid: null,
41
+ hostKind: null,
42
+ machineId: null,
43
+ sessionId: "abcdefgh12345",
44
+ },
45
+ value: "hqfp1:-:-:-:abcdefgh",
46
+ },
47
+ ],
48
+ invalid: [
49
+ { name: "empty", value: "" },
50
+ { name: "wrong prefix", value: "hqfp2:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9" },
51
+ { name: "too few fields", value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a" },
52
+ { name: "too many fields", value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9:x" },
53
+ { name: "bad identity prefix", value: "hqfp1:indigo:usr_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9" },
54
+ { name: "short identity", value: "hqfp1:indigo:prs_01J8:laptop.e3ef5a5a:fabb19c9" },
55
+ { name: "bad host kind", value: "hqfp1:indigo:prs_01J8ZK4Q:desktop.e3ef5a5a:fabb19c9" },
56
+ { name: "non-hex machine", value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.zzzzzzzz:fabb19c9" },
57
+ { name: "long session", value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9x" },
58
+ { name: "empty field", value: "hqfp1::prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9" },
59
+ ],
60
+ };
61
+ //# sourceMappingURL=fixtures.js.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * HQ session fingerprint value format.
3
+ *
4
+ * A fingerprint names one HQ session without exposing a full identity, machine
5
+ * or session id:
6
+ *
7
+ * hqfp1:<company>:<identity>:<machine>:<session>
8
+ *
9
+ * - company: company slug, or "-"
10
+ * - identity: "prs_" or "agt_" plus the first 8 chars of the ULID, or "-"
11
+ * - machine: "<laptop|outpost|fleet>.<first 8 hex chars of the machine id>", or "-"
12
+ * - session: first 8 chars of the session id, or "-"
13
+ *
14
+ * No component may contain ":". hq-pro (US-006) copies the test vectors in
15
+ * ./fixtures.ts so both sides agree on the format.
16
+ */
17
+ export declare const FINGERPRINT_PREFIX = "hqfp1";
18
+ export declare const UNKNOWN = "-";
19
+ export type HostKind = "laptop" | "outpost" | "fleet";
20
+ export type IdentityKind = "person" | "agent";
21
+ export declare const HOST_KINDS: readonly HostKind[];
22
+ export interface FingerprintParts {
23
+ company: string | null;
24
+ /** Full `prs_*` or `agt_*` uid. Truncated by build(). */
25
+ identityUid: string | null;
26
+ hostKind: HostKind | null;
27
+ /** Full machine id (UUID or hex). Truncated by build(). */
28
+ machineId: string | null;
29
+ /** Full session id. Truncated by build(). */
30
+ sessionId: string | null;
31
+ }
32
+ export interface ParsedFingerprint {
33
+ value: string;
34
+ company: string | null;
35
+ /** Short identity, for example `prs_01J8ABCD`. */
36
+ identity: string | null;
37
+ identityKind: IdentityKind | null;
38
+ hostKind: HostKind | null;
39
+ /** Short machine id, 8 lowercase hex chars. */
40
+ machine: string | null;
41
+ /** Short session id, 8 chars. */
42
+ session: string | null;
43
+ }
44
+ export declare const COMPANY_RE: RegExp;
45
+ /** Build a fingerprint value. Throws when a component is malformed or contains ":". */
46
+ export declare function build(parts: FingerprintParts): string;
47
+ /** Parse a fingerprint value. Returns null for anything that is not a valid hqfp1 value. */
48
+ export declare function parse(value: string): ParsedFingerprint | null;
49
+ export declare function isFingerprint(value: unknown): value is string;
50
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1,105 @@
1
+ /**
2
+ * HQ session fingerprint value format.
3
+ *
4
+ * A fingerprint names one HQ session without exposing a full identity, machine
5
+ * or session id:
6
+ *
7
+ * hqfp1:<company>:<identity>:<machine>:<session>
8
+ *
9
+ * - company: company slug, or "-"
10
+ * - identity: "prs_" or "agt_" plus the first 8 chars of the ULID, or "-"
11
+ * - machine: "<laptop|outpost|fleet>.<first 8 hex chars of the machine id>", or "-"
12
+ * - session: first 8 chars of the session id, or "-"
13
+ *
14
+ * No component may contain ":". hq-pro (US-006) copies the test vectors in
15
+ * ./fixtures.ts so both sides agree on the format.
16
+ */
17
+ export const FINGERPRINT_PREFIX = "hqfp1";
18
+ export const UNKNOWN = "-";
19
+ export const HOST_KINDS = ["laptop", "outpost", "fleet"];
20
+ export const COMPANY_RE = /^[a-z0-9][a-z0-9_-]*$/i;
21
+ const IDENTITY_RE = /^(prs|agt)_[A-Za-z0-9]{8}$/;
22
+ const MACHINE_RE = /^(laptop|outpost|fleet)\.[0-9a-f]{8}$/;
23
+ const SESSION_RE = /^[A-Za-z0-9._-]{1,8}$/;
24
+ function assertNoColon(name, value) {
25
+ if (value.includes(":")) {
26
+ throw new Error(`fingerprint ${name} must not contain ":"`);
27
+ }
28
+ }
29
+ function shortIdentity(uid) {
30
+ const match = /^(prs|agt)_([A-Za-z0-9]+)$/.exec(uid);
31
+ if (!match || match[2].length < 8) {
32
+ throw new Error("fingerprint identity must be prs_ or agt_ followed by at least 8 ULID chars");
33
+ }
34
+ return `${match[1]}_${match[2].slice(0, 8)}`;
35
+ }
36
+ function shortMachine(hostKind, machineId) {
37
+ const hex = machineId.toLowerCase().replace(/-/g, "");
38
+ if (!/^[0-9a-f]{8,}$/.test(hex)) {
39
+ throw new Error("fingerprint machine id must start with at least 8 hex chars");
40
+ }
41
+ return `${hostKind}.${hex.slice(0, 8)}`;
42
+ }
43
+ /** Build a fingerprint value. Throws when a component is malformed or contains ":". */
44
+ export function build(parts) {
45
+ const company = parts.company ?? UNKNOWN;
46
+ assertNoColon("company", company);
47
+ if (company !== UNKNOWN && !COMPANY_RE.test(company)) {
48
+ throw new Error("fingerprint company must be a slug");
49
+ }
50
+ let identity = UNKNOWN;
51
+ if (parts.identityUid) {
52
+ assertNoColon("identity", parts.identityUid);
53
+ identity = shortIdentity(parts.identityUid);
54
+ }
55
+ let machine = UNKNOWN;
56
+ if (parts.machineId) {
57
+ assertNoColon("machine", parts.machineId);
58
+ machine = shortMachine(parts.hostKind ?? "laptop", parts.machineId);
59
+ }
60
+ let session = UNKNOWN;
61
+ if (parts.sessionId) {
62
+ assertNoColon("session", parts.sessionId);
63
+ session = parts.sessionId.slice(0, 8);
64
+ if (!SESSION_RE.test(session)) {
65
+ throw new Error("fingerprint session id has invalid characters");
66
+ }
67
+ }
68
+ return [FINGERPRINT_PREFIX, company, identity, machine, session].join(":");
69
+ }
70
+ /** Parse a fingerprint value. Returns null for anything that is not a valid hqfp1 value. */
71
+ export function parse(value) {
72
+ if (typeof value !== "string")
73
+ return null;
74
+ const fields = value.split(":");
75
+ if (fields.length !== 5)
76
+ return null;
77
+ const [prefix, company, identity, machine, session] = fields;
78
+ if (prefix !== FINGERPRINT_PREFIX)
79
+ return null;
80
+ if (company !== UNKNOWN && !COMPANY_RE.test(company))
81
+ return null;
82
+ if (identity !== UNKNOWN && !IDENTITY_RE.test(identity))
83
+ return null;
84
+ if (machine !== UNKNOWN && !MACHINE_RE.test(machine))
85
+ return null;
86
+ if (session !== UNKNOWN && !SESSION_RE.test(session))
87
+ return null;
88
+ const hostKind = machine === UNKNOWN ? null : machine.split(".")[0];
89
+ let identityKind = null;
90
+ if (identity !== UNKNOWN)
91
+ identityKind = identity.startsWith("agt_") ? "agent" : "person";
92
+ return {
93
+ value,
94
+ company: company === UNKNOWN ? null : company,
95
+ identity: identity === UNKNOWN ? null : identity,
96
+ identityKind,
97
+ hostKind,
98
+ machine: machine === UNKNOWN ? null : machine.split(".")[1],
99
+ session: session === UNKNOWN ? null : session,
100
+ };
101
+ }
102
+ export function isFingerprint(value) {
103
+ return typeof value === "string" && parse(value) !== null;
104
+ }
105
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1,16 @@
1
+ export declare const FINGERPRINT_TRAILER = "HQ-Fingerprint";
2
+ /**
3
+ * The fingerprint for `env`, or null when it cannot be built (no identity and
4
+ * no session id, or an error). `label` prefixes the stderr warning. With
5
+ * `readOnly` it creates no file and is null when no machine id exists yet.
6
+ */
7
+ export declare function offlineFingerprint(hqRoot: string, env: NodeJS.ProcessEnv, label: string, options?: {
8
+ readOnly?: boolean;
9
+ }): string | null;
10
+ /**
11
+ * `git commit` args that add the HQ-Fingerprint trailer, or none without a
12
+ * value. The trailer is a final `-m` paragraph, which `git interpret-trailers`
13
+ * parses on any git; `--trailer` needs git 2.32. Spread after the subject `-m`.
14
+ */
15
+ export declare function fingerprintTrailerArgs(fingerprint: string | null | undefined): string[];
16
+ //# sourceMappingURL=offline.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Offline fingerprint for callers that cannot await: lane start and the
3
+ * handoff commits. Uses the cached ID token and the machine creds only; it
4
+ * never makes a network call.
5
+ */
6
+ import * as fs from "node:fs";
7
+ import { loadMachineCreds, machineCredsFilePath } from "@indigoai-us/hq-cloud";
8
+ import { loadCachedTokens } from "../../utils/person-token-cache.js";
9
+ import { buildSelfFingerprintOfflineSync } from "./self-core.js";
10
+ export const FINGERPRINT_TRAILER = "HQ-Fingerprint";
11
+ /**
12
+ * The fingerprint for `env`, or null when it cannot be built (no identity and
13
+ * no session id, or an error). `label` prefixes the stderr warning. With
14
+ * `readOnly` it creates no file and is null when no machine id exists yet.
15
+ */
16
+ export function offlineFingerprint(hqRoot, env, label, options = {}) {
17
+ try {
18
+ return buildSelfFingerprintOfflineSync({
19
+ hqRoot,
20
+ env,
21
+ readOnly: options.readOnly,
22
+ loadIdToken: () => loadCachedTokens()?.idToken ?? null,
23
+ loadMachineCreds: () => {
24
+ if (!fs.existsSync(machineCredsFilePath()))
25
+ return null;
26
+ const creds = loadMachineCreds();
27
+ return creds ? { entityType: creds.entityType, entityUid: creds.entityUid } : null;
28
+ },
29
+ }).fingerprint;
30
+ }
31
+ catch (err) {
32
+ process.stderr.write(`${label}: could not build the fingerprint: ${err.message}\n`);
33
+ return null;
34
+ }
35
+ }
36
+ /**
37
+ * `git commit` args that add the HQ-Fingerprint trailer, or none without a
38
+ * value. The trailer is a final `-m` paragraph, which `git interpret-trailers`
39
+ * parses on any git; `--trailer` needs git 2.32. Spread after the subject `-m`.
40
+ */
41
+ export function fingerprintTrailerArgs(fingerprint) {
42
+ return fingerprint ? ["-m", `${FINGERPRINT_TRAILER}: ${fingerprint}`] : [];
43
+ }
44
+ //# sourceMappingURL=offline.js.map
@@ -0,0 +1,74 @@
1
+ import type { HostKind } from "./format.js";
2
+ import type { VaultApiOptions } from "../../utils/vault-api.js";
3
+ export declare const REGISTRY_TIMEOUT_MS = 3000;
4
+ export declare const FINGERPRINT_LOG_FILE = "fingerprint.log";
5
+ export type RegistryFetch = (opts: VaultApiOptions) => Promise<Response>;
6
+ export interface RegistryClientOptions {
7
+ /** HTTP client. Defaults to vaultApiFetch, loaded on first use. */
8
+ fetch?: RegistryFetch;
9
+ baseUrl?: string;
10
+ timeoutMs?: number;
11
+ }
12
+ export interface RegisterFingerprintBody {
13
+ fingerprint: string;
14
+ companySlug: string;
15
+ identityUid: string;
16
+ machineId: string;
17
+ hostKind: HostKind;
18
+ sessionId: string;
19
+ label?: string;
20
+ }
21
+ /** hq-pro's FingerprintRow, as returned by POST /v1/fingerprints. */
22
+ export interface FingerprintRow {
23
+ pk: string;
24
+ companyUid: string;
25
+ companySlug: string;
26
+ identityUid: string;
27
+ identityKind: string;
28
+ machineId: string;
29
+ hostKind: HostKind;
30
+ sessionId: string;
31
+ firstSeenAt: string;
32
+ lastSeenAt: string;
33
+ recordedBySub?: string;
34
+ }
35
+ /** GET /v1/fingerprints/{value}: the row without recordedBySub, plus display names. */
36
+ export interface ResolvedFingerprint extends Omit<FingerprintRow, "recordedBySub"> {
37
+ identityName: string | null;
38
+ machineLabel: string | null;
39
+ }
40
+ export type RegistryFailure = {
41
+ ok: false;
42
+ /** HTTP status, or null when no response arrived (timeout, transport error). */
43
+ status: number | null;
44
+ /** The server's `error` text, or the thrown error's class and message. */
45
+ error: string;
46
+ };
47
+ export type RegisterResult = {
48
+ ok: true;
49
+ status: number;
50
+ row: FingerprintRow;
51
+ } | RegistryFailure;
52
+ export type LookupResult = {
53
+ ok: true;
54
+ found: true;
55
+ record: ResolvedFingerprint;
56
+ } | {
57
+ ok: true;
58
+ found: false;
59
+ } | RegistryFailure;
60
+ export declare function registerFingerprint(body: RegisterFingerprintBody, token: string, opts?: RegistryClientOptions): Promise<RegisterResult>;
61
+ export declare function lookupFingerprint(value: string, token: string, opts?: RegistryClientOptions): Promise<LookupResult>;
62
+ export interface FingerprintLogOptions {
63
+ /** Directory that holds fingerprint.log. Defaults to ~/.hq. */
64
+ dir?: string;
65
+ now?: () => Date;
66
+ }
67
+ export declare function fingerprintLogPath(dir?: string): string;
68
+ /**
69
+ * Appends one line to ~/.hq/fingerprint.log. A log that cannot be written is
70
+ * dropped: registration is fire-and-forget and must not print or fail.
71
+ */
72
+ export declare function appendFingerprintLog(op: string, detail: string, opts?: FingerprintLogOptions): void;
73
+ export declare function describeFailure(result: RegistryFailure): string;
74
+ //# sourceMappingURL=registry-client.d.ts.map