@indigoai-us/hq-cli 5.323.0 → 5.325.0

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 (73) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/dist/bin/hq-auth-refresh.d.ts +5 -1
  3. package/dist/bin/hq-auth-refresh.js +33 -1
  4. package/dist/command-catalog.generated.d.ts +125 -6
  5. package/dist/command-catalog.generated.js +162 -6
  6. package/dist/command-registration-plan.d.ts +24 -0
  7. package/dist/command-registration-plan.js +4 -0
  8. package/dist/commands/anywhere-runtime-flag.d.ts +9 -0
  9. package/dist/commands/anywhere-runtime-flag.js +22 -0
  10. package/dist/commands/core-derive-trigger-facts-native.js +37 -3
  11. package/dist/commands/daemon.js +39 -2
  12. package/dist/commands/db-provision.js +6 -1
  13. package/dist/commands/doctor.js +8 -4
  14. package/dist/commands/feedback.d.ts +2 -0
  15. package/dist/commands/feedback.js +10 -0
  16. package/dist/commands/install-global.d.ts +46 -0
  17. package/dist/commands/install-global.js +674 -0
  18. package/dist/commands/link.d.ts +30 -0
  19. package/dist/commands/link.js +184 -0
  20. package/dist/commands/mcp-status.d.ts +6 -0
  21. package/dist/commands/mcp-status.js +21 -4
  22. package/dist/commands/pkg-install.js +20 -1
  23. package/dist/commands/update.d.ts +19 -0
  24. package/dist/commands/update.js +52 -6
  25. package/dist/fast-core.d.ts +2 -1
  26. package/dist/fast-core.js +12 -8
  27. package/dist/index.js +9 -7
  28. package/dist/lib/core-utils/timeout-guard-native-flags.d.ts +6 -0
  29. package/dist/lib/core-utils/timeout-guard-native-flags.js +30 -0
  30. package/dist/lib/core-utils/timeout-guard-native.d.ts +0 -3
  31. package/dist/lib/core-utils/timeout-guard-native.js +4 -30
  32. package/dist/lib/core-utils/timeout-guard.d.ts +2 -0
  33. package/dist/lib/core-utils/timeout-guard.js +2 -1
  34. package/dist/lib/daemon/convergence.d.ts +42 -0
  35. package/dist/lib/daemon/convergence.js +66 -0
  36. package/dist/lib/daemon/heartbeat.d.ts +43 -0
  37. package/dist/lib/daemon/heartbeat.js +70 -0
  38. package/dist/lib/daemon/ops/journal.d.ts +29 -0
  39. package/dist/lib/daemon/ops/journal.js +141 -0
  40. package/dist/lib/daemon/ops/memory.d.ts +20 -0
  41. package/dist/lib/daemon/ops/memory.js +65 -0
  42. package/dist/lib/daemon/ops/mesh.d.ts +38 -0
  43. package/dist/lib/daemon/ops/mesh.js +70 -0
  44. package/dist/lib/daemon/ops/policy.d.ts +78 -0
  45. package/dist/lib/daemon/ops/policy.js +283 -0
  46. package/dist/lib/daemon/protocol.d.ts +68 -0
  47. package/dist/lib/daemon/protocol.js +154 -0
  48. package/dist/lib/daemon/run.d.ts +10 -0
  49. package/dist/lib/daemon/run.js +85 -3
  50. package/dist/lib/daemon/server.d.ts +36 -0
  51. package/dist/lib/daemon/server.js +306 -0
  52. package/dist/lib/daemon/session-registry.d.ts +30 -0
  53. package/dist/lib/daemon/session-registry.js +42 -0
  54. package/dist/lib/doctor/checks/anywhere.d.ts +38 -0
  55. package/dist/lib/doctor/checks/anywhere.js +182 -0
  56. package/dist/lib/doctor/registry.js +6 -0
  57. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  58. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  59. package/dist/lib/install/claude-targets.d.ts +47 -0
  60. package/dist/lib/install/claude-targets.js +203 -0
  61. package/dist/lib/install/codex-targets.d.ts +57 -0
  62. package/dist/lib/install/codex-targets.js +187 -0
  63. package/dist/lib/mesh/live/session-seq.js +62 -9
  64. package/dist/lib/registry/normalize-remote.d.ts +21 -0
  65. package/dist/lib/registry/normalize-remote.js +68 -0
  66. package/dist/lib/registry/registry.d.ts +75 -0
  67. package/dist/lib/registry/registry.js +261 -0
  68. package/dist/utils/cognito-session.d.ts +5 -1
  69. package/dist/utils/cognito-session.js +8 -12
  70. package/dist/utils/feedback-diagnostics.d.ts +2 -0
  71. package/dist/utils/version-check.d.ts +2 -0
  72. package/dist/utils/version-check.js +7 -0
  73. package/package.json +2 -2
@@ -222,8 +222,9 @@ export async function timeoutGuardCommand(stdin, deps = {}) {
222
222
  if (!verdict.block)
223
223
  return 0;
224
224
  // Rollout gate: only act for @getindigo.ai identities.
225
+ const flagReader = deps.flagReader ?? await deps.getFlagReader?.();
225
226
  const getEmail = deps.getEmail ?? defaultEmail;
226
- if (!await isGatedUser(await getEmail(), deps.flagReader))
227
+ if (!await isGatedUser(await getEmail(), flagReader))
227
228
  return 0;
228
229
  err.write(`${BLOCK_MESSAGE(verdict.reason ?? "declared timeout exceeds the harness ceiling")}\n`);
229
230
  return 2;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Daemon version convergence (hq-anywhere US-012).
3
+ *
4
+ * After `hq update` (or /update-hq) installs a new hq-cli, the resident
5
+ * daemon still runs the old code until it restarts. This module asks the
6
+ * running hqd for its version and compares it with the version now installed
7
+ * on disk. Policy hq-self-updater-must-verify-convergence-after-install: the
8
+ * caller reports a mismatch as a failure; it is never swallowed.
9
+ */
10
+ export declare const CONVERGENCE_TIMEOUT_MS = 10000;
11
+ export declare const CONVERGENCE_POLL_MS = 500;
12
+ export interface ConvergenceResult {
13
+ converged: boolean;
14
+ expected: string;
15
+ /** The version hqd reported last, or null when it never answered. */
16
+ running: string | null;
17
+ /** The last ping error when hqd never answered. */
18
+ error?: string;
19
+ }
20
+ export interface ConvergenceDeps {
21
+ /** Ask hqd for its version; rejects when hqd is unreachable. */
22
+ ping?: () => Promise<string>;
23
+ timeoutMs?: number;
24
+ pollMs?: number;
25
+ sleep?: (ms: number) => Promise<void>;
26
+ now?: () => number;
27
+ }
28
+ /** Ping hqd over its socket and return the version it reports. */
29
+ export declare function pingHqdVersion(socketPath?: string, timeoutMs?: number): Promise<string>;
30
+ /**
31
+ * The hq-cli version installed on PATH. The process running `hq update` is
32
+ * the old CLI, so its own CLI_VERSION is not the installed version.
33
+ */
34
+ export declare function installedCliVersion(run?: (cmd: string, args: string[]) => {
35
+ status: number | null;
36
+ stdout: string;
37
+ }): string | null;
38
+ /** Poll hqd until it reports `expected` or the budget runs out. */
39
+ export declare function verifyDaemonConvergence(expected: string, deps?: ConvergenceDeps): Promise<ConvergenceResult>;
40
+ /** One-line description of a non-converged result, with the remediation. */
41
+ export declare function describeNonConvergence(result: ConvergenceResult): string;
42
+ //# sourceMappingURL=convergence.d.ts.map
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Daemon version convergence (hq-anywhere US-012).
3
+ *
4
+ * After `hq update` (or /update-hq) installs a new hq-cli, the resident
5
+ * daemon still runs the old code until it restarts. This module asks the
6
+ * running hqd for its version and compares it with the version now installed
7
+ * on disk. Policy hq-self-updater-must-verify-convergence-after-install: the
8
+ * caller reports a mismatch as a failure; it is never swallowed.
9
+ */
10
+ import { spawnSync } from "node:child_process";
11
+ import { hqdRequest } from "./protocol.js";
12
+ export const CONVERGENCE_TIMEOUT_MS = 10_000;
13
+ export const CONVERGENCE_POLL_MS = 500;
14
+ /** Ping hqd over its socket and return the version it reports. */
15
+ export async function pingHqdVersion(socketPath, timeoutMs = 2_000) {
16
+ const result = (await hqdRequest("ping", {}, { socketPath, timeoutMs }));
17
+ if (typeof result?.version !== "string")
18
+ throw new Error("hqd ping did not return a version");
19
+ return result.version;
20
+ }
21
+ /**
22
+ * The hq-cli version installed on PATH. The process running `hq update` is
23
+ * the old CLI, so its own CLI_VERSION is not the installed version.
24
+ */
25
+ export function installedCliVersion(run = defaultRun) {
26
+ const out = run("hq", ["--version"]);
27
+ if (out.status !== 0)
28
+ return null;
29
+ const match = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/.exec(out.stdout);
30
+ return match ? match[0] : null;
31
+ }
32
+ function defaultRun(cmd, args) {
33
+ const result = spawnSync(cmd, args, { encoding: "utf8", timeout: 10_000 });
34
+ return { status: result.status, stdout: typeof result.stdout === "string" ? result.stdout : "" };
35
+ }
36
+ /** Poll hqd until it reports `expected` or the budget runs out. */
37
+ export async function verifyDaemonConvergence(expected, deps = {}) {
38
+ const ping = deps.ping ?? (() => pingHqdVersion());
39
+ const timeoutMs = deps.timeoutMs ?? CONVERGENCE_TIMEOUT_MS;
40
+ const pollMs = deps.pollMs ?? CONVERGENCE_POLL_MS;
41
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
42
+ const now = deps.now ?? Date.now;
43
+ const deadline = now() + timeoutMs;
44
+ let running = null;
45
+ let error;
46
+ for (;;) {
47
+ try {
48
+ running = await ping();
49
+ error = undefined;
50
+ if (running === expected)
51
+ return { converged: true, expected, running };
52
+ }
53
+ catch (err) {
54
+ error = err instanceof Error ? err.message : String(err);
55
+ }
56
+ if (now() >= deadline)
57
+ return { converged: false, expected, running, ...(error ? { error } : {}) };
58
+ await sleep(pollMs);
59
+ }
60
+ }
61
+ /** One-line description of a non-converged result, with the remediation. */
62
+ export function describeNonConvergence(result) {
63
+ const seen = result.running ? `hqd is still running ${result.running}` : `hqd did not answer (${result.error ?? "no response"})`;
64
+ return `Daemon did not converge on hq-cli ${result.expected}: ${seen}. Run: hq daemon restart && hq doctor --only anywhere`;
65
+ }
66
+ //# sourceMappingURL=convergence.js.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * hqd heartbeat (hq-anywhere US-012).
3
+ *
4
+ * Every 60s the daemon records a host-local beat in ~/.hq/hqd-heartbeat.json so `hq doctor` can tell a live daemon
5
+ * from a stale one offline. A device with no beat for 5 minutes is
6
+ * "daemon stale".
7
+ */
8
+ export declare const HEARTBEAT_INTERVAL_MS = 60000;
9
+ export declare const HEARTBEAT_STALE_MS: number;
10
+ export declare const HEARTBEAT_FILE = "hqd-heartbeat.json";
11
+ export declare const DAEMON_STALE_LABEL = "daemon stale";
12
+ export interface HeartbeatState {
13
+ at: string;
14
+ version: string;
15
+ sessions: number;
16
+ device: string;
17
+ }
18
+ export declare function heartbeatFile(home?: string, env?: NodeJS.ProcessEnv): string;
19
+ /** Mesh session id for this device's daemon; only characters the spool accepts. */
20
+ export declare function heartbeatSessionId(hostname?: string): string;
21
+ export declare function readHeartbeat(file: string): HeartbeatState | null;
22
+ /** Presence label for a device given its last beat: "daemon stale" after 5 minutes of silence. */
23
+ export declare function daemonPresenceLabel(state: HeartbeatState | null, now?: Date): "daemon live" | typeof DAEMON_STALE_LABEL;
24
+ export interface HeartbeatOptions {
25
+ version: string;
26
+ sessionCount: () => number;
27
+ file?: string;
28
+ hostname?: string;
29
+ now?: () => Date;
30
+ intervalMs?: number;
31
+ /** Optional host event publisher. The daemon does not publish to Work Mesh without an explicit company-bound session. */
32
+ publish?: (state: HeartbeatState, sessionId: string) => void;
33
+ log?: (level: "info" | "warn" | "error", message: string) => void;
34
+ }
35
+ /** Publish one heartbeat. Errors are logged and returned, never thrown into the daemon loop. */
36
+ export declare function beatOnce(opts: HeartbeatOptions): {
37
+ ok: boolean;
38
+ state: HeartbeatState;
39
+ error?: string;
40
+ };
41
+ /** Beat now and every interval until stopped. The timer does not hold the process open. */
42
+ export declare function startHeartbeat(opts: HeartbeatOptions): () => void;
43
+ //# sourceMappingURL=heartbeat.d.ts.map
@@ -0,0 +1,70 @@
1
+ /**
2
+ * hqd heartbeat (hq-anywhere US-012).
3
+ *
4
+ * Every 60s the daemon records a host-local beat in ~/.hq/hqd-heartbeat.json so `hq doctor` can tell a live daemon
5
+ * from a stale one offline. A device with no beat for 5 minutes is
6
+ * "daemon stale".
7
+ */
8
+ import * as fs from "node:fs";
9
+ import * as os from "node:os";
10
+ import * as path from "node:path";
11
+ export const HEARTBEAT_INTERVAL_MS = 60_000;
12
+ export const HEARTBEAT_STALE_MS = 5 * 60_000;
13
+ export const HEARTBEAT_FILE = "hqd-heartbeat.json";
14
+ export const DAEMON_STALE_LABEL = "daemon stale";
15
+ export function heartbeatFile(home = os.homedir(), env = process.env) {
16
+ const dir = env.HQ_REGISTRY_DIR?.trim() ? path.resolve(env.HQ_REGISTRY_DIR.trim()) : path.join(home, ".hq");
17
+ return path.join(dir, HEARTBEAT_FILE);
18
+ }
19
+ /** Mesh session id for this device's daemon; only characters the spool accepts. */
20
+ export function heartbeatSessionId(hostname = os.hostname()) {
21
+ return `hqd-${hostname.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").slice(0, 48) || "device"}`;
22
+ }
23
+ export function readHeartbeat(file) {
24
+ let raw;
25
+ try {
26
+ raw = fs.readFileSync(file, "utf8");
27
+ }
28
+ catch (err) {
29
+ if (err.code === "ENOENT")
30
+ return null;
31
+ throw err;
32
+ }
33
+ const parsed = JSON.parse(raw);
34
+ if (typeof parsed.at !== "string" || typeof parsed.version !== "string")
35
+ throw new Error(`${file} is not a heartbeat record`);
36
+ return { at: parsed.at, version: parsed.version, sessions: Number(parsed.sessions ?? 0), device: String(parsed.device ?? "") };
37
+ }
38
+ /** Presence label for a device given its last beat: "daemon stale" after 5 minutes of silence. */
39
+ export function daemonPresenceLabel(state, now = new Date()) {
40
+ if (!state)
41
+ return DAEMON_STALE_LABEL;
42
+ const age = now.getTime() - Date.parse(state.at);
43
+ return Number.isFinite(age) && age <= HEARTBEAT_STALE_MS ? "daemon live" : DAEMON_STALE_LABEL;
44
+ }
45
+ /** Publish one heartbeat. Errors are logged and returned, never thrown into the daemon loop. */
46
+ export function beatOnce(opts) {
47
+ const now = (opts.now ?? (() => new Date()))();
48
+ const sessionId = heartbeatSessionId(opts.hostname);
49
+ const state = { at: now.toISOString(), version: opts.version, sessions: opts.sessionCount(), device: sessionId };
50
+ try {
51
+ const file = opts.file ?? heartbeatFile();
52
+ fs.mkdirSync(path.dirname(file), { recursive: true });
53
+ fs.writeFileSync(file, `${JSON.stringify(state)}\n`, { mode: 0o600 });
54
+ opts.publish?.(state, sessionId);
55
+ return { ok: true, state };
56
+ }
57
+ catch (err) {
58
+ const error = err instanceof Error ? err.message : String(err);
59
+ opts.log?.("warn", `hqd heartbeat failed: ${error}`);
60
+ return { ok: false, state, error };
61
+ }
62
+ }
63
+ /** Beat now and every interval until stopped. The timer does not hold the process open. */
64
+ export function startHeartbeat(opts) {
65
+ beatOnce(opts);
66
+ const timer = setInterval(() => beatOnce(opts), opts.intervalMs ?? HEARTBEAT_INTERVAL_MS);
67
+ timer.unref();
68
+ return () => clearInterval(timer);
69
+ }
70
+ //# sourceMappingURL=heartbeat.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * journal.append op (hq-anywhere US-009).
3
+ *
4
+ * Follows `.claude/skills/_shared/journal.sh append` semantics: the entry goes
5
+ * into the project's active journal (frontmatter `status: active`, `project:`
6
+ * = the project folder) as "- <ISO8601> <text>" on the line after the section
7
+ * header, or under a new header at EOF when the header is missing. A daemon
8
+ * has no per-session pointer, so the newest active journal in
9
+ * <project>/journal/ is used, and one is opened (same template as
10
+ * `journal.sh open`) when none exists.
11
+ */
12
+ export declare const JOURNAL_SECTIONS: Record<string, string>;
13
+ export declare class JournalOpError extends Error {
14
+ readonly code: string;
15
+ constructor(code: string, message: string);
16
+ }
17
+ export interface JournalAppendArgs {
18
+ company: string;
19
+ project: string;
20
+ section: string;
21
+ text: string;
22
+ }
23
+ export interface JournalAppendResult {
24
+ path: string;
25
+ line: string;
26
+ opened: boolean;
27
+ }
28
+ export declare function journalAppend(args: JournalAppendArgs, hqRoot: string, now?: Date): JournalAppendResult;
29
+ //# sourceMappingURL=journal.d.ts.map
@@ -0,0 +1,141 @@
1
+ /**
2
+ * journal.append op (hq-anywhere US-009).
3
+ *
4
+ * Follows `.claude/skills/_shared/journal.sh append` semantics: the entry goes
5
+ * into the project's active journal (frontmatter `status: active`, `project:`
6
+ * = the project folder) as "- <ISO8601> <text>" on the line after the section
7
+ * header, or under a new header at EOF when the header is missing. A daemon
8
+ * has no per-session pointer, so the newest active journal in
9
+ * <project>/journal/ is used, and one is opened (same template as
10
+ * `journal.sh open`) when none exists.
11
+ */
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ const SLUG_RE = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
15
+ export const JOURNAL_SECTIONS = {
16
+ decisions: "## Decisions",
17
+ open: "## Open threads",
18
+ findings: "## Findings",
19
+ rejected: "## Rejected",
20
+ };
21
+ export class JournalOpError extends Error {
22
+ code;
23
+ constructor(code, message) {
24
+ super(message);
25
+ this.code = code;
26
+ this.name = "JournalOpError";
27
+ }
28
+ }
29
+ function isoSeconds(d) {
30
+ return `${d.toISOString().slice(0, 19)}Z`;
31
+ }
32
+ function frontmatterValue(text, key) {
33
+ const m = new RegExp(`^${key}: (.*)$`, "m").exec(text);
34
+ return m ? m[1] : null;
35
+ }
36
+ function findActiveJournal(projectDir) {
37
+ const dir = path.join(projectDir, "journal");
38
+ let names;
39
+ try {
40
+ names = fs.readdirSync(dir);
41
+ }
42
+ catch (err) {
43
+ if (err.code === "ENOENT")
44
+ return null;
45
+ throw err;
46
+ }
47
+ for (const name of names.filter((n) => n.endsWith(".md")).sort().reverse()) {
48
+ const file = path.join(dir, name);
49
+ const text = fs.readFileSync(file, "utf8");
50
+ if (frontmatterValue(text, "status") !== "active")
51
+ continue;
52
+ const project = frontmatterValue(text, "project");
53
+ if (!project)
54
+ continue;
55
+ let real;
56
+ try {
57
+ real = fs.realpathSync(project);
58
+ }
59
+ catch {
60
+ continue;
61
+ }
62
+ if (real === projectDir)
63
+ return file;
64
+ }
65
+ return null;
66
+ }
67
+ function openJournal(projectDir, now) {
68
+ const dir = path.join(projectDir, "journal");
69
+ fs.mkdirSync(dir, { recursive: true });
70
+ const iso = now.toISOString();
71
+ const stamp = `${iso.slice(0, 10)}-${iso.slice(11, 13)}${iso.slice(14, 16)}`;
72
+ const base = `${stamp}-hqd-adhoc`;
73
+ let file = path.join(dir, `${base}.md`);
74
+ let suffix = 2;
75
+ while (fs.existsSync(file))
76
+ file = path.join(dir, `${base}-${suffix++}.md`);
77
+ fs.writeFileSync(file, [
78
+ "---",
79
+ "skill: hqd",
80
+ `started_at: ${isoSeconds(now)}`,
81
+ "thread_id: adhoc",
82
+ `project: ${projectDir}`,
83
+ "status: active",
84
+ "auto_capture: true",
85
+ 'summary: ""',
86
+ "---",
87
+ "",
88
+ "## Decisions",
89
+ "## Open threads",
90
+ "## Findings",
91
+ "## Rejected",
92
+ "## Auto-capture",
93
+ "",
94
+ ].join("\n"));
95
+ return file;
96
+ }
97
+ export function journalAppend(args, hqRoot, now = new Date()) {
98
+ if (!SLUG_RE.test(args.company))
99
+ throw new JournalOpError("bad_args", "company must be a company slug");
100
+ if (!SLUG_RE.test(args.project))
101
+ throw new JournalOpError("bad_args", "project must be a project folder name");
102
+ const header = JOURNAL_SECTIONS[args.section];
103
+ if (!header)
104
+ throw new JournalOpError("bad_args", `section must be one of ${Object.keys(JOURNAL_SECTIONS).join(", ")}`);
105
+ const text = args.text.replace(/\s*[\r\n]+\s*/g, " ").trim();
106
+ if (!text)
107
+ throw new JournalOpError("bad_args", "text must be non-empty");
108
+ const rawDir = path.join(hqRoot, "companies", args.company, "projects", args.project);
109
+ let projectDir;
110
+ try {
111
+ projectDir = fs.realpathSync(rawDir);
112
+ }
113
+ catch {
114
+ throw new JournalOpError("unknown_project", `no project folder at companies/${args.company}/projects/${args.project}`);
115
+ }
116
+ const companyRoot = fs.realpathSync(path.join(hqRoot, "companies", args.company));
117
+ if (!projectDir.startsWith(companyRoot + path.sep)) {
118
+ throw new JournalOpError("bad_args", "project folder resolves outside its company");
119
+ }
120
+ let file = findActiveJournal(projectDir);
121
+ const opened = file === null;
122
+ if (!file)
123
+ file = openJournal(projectDir, now);
124
+ const line = `- ${isoSeconds(now)} ${text}`;
125
+ const body = fs.readFileSync(file, "utf8");
126
+ const lines = body.split("\n");
127
+ const at = lines.indexOf(header);
128
+ let next;
129
+ if (at !== -1) {
130
+ lines.splice(at + 1, 0, line);
131
+ next = lines.join("\n");
132
+ }
133
+ else {
134
+ next = `${body}${body.endsWith("\n") || body === "" ? "" : "\n"}\n${header}\n${line}\n`;
135
+ }
136
+ const tmp = `${file}.tmp`;
137
+ fs.writeFileSync(tmp, next);
138
+ fs.renameSync(tmp, file);
139
+ return { path: file, line, opened };
140
+ }
141
+ //# sourceMappingURL=journal.js.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * memory.get / memory.put ops (hq-anywhere US-009): session-scoped notes
3
+ * persisted at workspace/sessions/<sid>/hqd-memory.json under HQ_ROOT.
4
+ */
5
+ export declare const MEMORY_FILE = "hqd-memory.json";
6
+ export declare class MemoryOpError extends Error {
7
+ readonly code: string;
8
+ constructor(code: string, message: string);
9
+ }
10
+ export declare function memoryPath(hqRoot: string, sessionId: string): string;
11
+ export declare function memoryPut(hqRoot: string, sessionId: string, key: string, value: unknown, now?: Date): {
12
+ key: string;
13
+ updatedAt: string;
14
+ };
15
+ export declare function memoryGet(hqRoot: string, sessionId: string, key?: string): {
16
+ value: unknown;
17
+ } | {
18
+ entries: Record<string, unknown>;
19
+ };
20
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * memory.get / memory.put ops (hq-anywhere US-009): session-scoped notes
3
+ * persisted at workspace/sessions/<sid>/hqd-memory.json under HQ_ROOT.
4
+ */
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ const SESSION_RE = /^[A-Za-z0-9._-]+$/;
8
+ export const MEMORY_FILE = "hqd-memory.json";
9
+ const MAX_VALUE_BYTES = 256 * 1024;
10
+ export class MemoryOpError extends Error {
11
+ code;
12
+ constructor(code, message) {
13
+ super(message);
14
+ this.code = code;
15
+ this.name = "MemoryOpError";
16
+ }
17
+ }
18
+ export function memoryPath(hqRoot, sessionId) {
19
+ if (!SESSION_RE.test(sessionId) || sessionId === "." || sessionId === "..") {
20
+ throw new MemoryOpError("bad_args", "sessionId may only contain letters, digits, '.', '_' and '-'");
21
+ }
22
+ return path.join(hqRoot, "workspace", "sessions", sessionId, MEMORY_FILE);
23
+ }
24
+ function readAll(file) {
25
+ let raw;
26
+ try {
27
+ raw = fs.readFileSync(file, "utf8");
28
+ }
29
+ catch (err) {
30
+ if (err.code === "ENOENT")
31
+ return {};
32
+ throw err;
33
+ }
34
+ const parsed = JSON.parse(raw);
35
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
36
+ throw new MemoryOpError("corrupt_memory", `${file} is not a JSON object`);
37
+ }
38
+ return parsed;
39
+ }
40
+ export function memoryPut(hqRoot, sessionId, key, value, now = new Date()) {
41
+ if (!key.trim())
42
+ throw new MemoryOpError("bad_args", "key must be non-empty");
43
+ const serialized = JSON.stringify(value ?? null);
44
+ if (Buffer.byteLength(serialized, "utf8") > MAX_VALUE_BYTES)
45
+ throw new MemoryOpError("too_large", `value is over ${MAX_VALUE_BYTES} bytes`);
46
+ const file = memoryPath(hqRoot, sessionId);
47
+ const all = readAll(file);
48
+ const updatedAt = now.toISOString();
49
+ if (value === null || value === undefined)
50
+ delete all[key];
51
+ else
52
+ all[key] = { value, updatedAt };
53
+ fs.mkdirSync(path.dirname(file), { recursive: true });
54
+ const tmp = `${file}.${process.pid}.tmp`;
55
+ fs.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}\n`, { mode: 0o600 });
56
+ fs.renameSync(tmp, file);
57
+ return { key, updatedAt };
58
+ }
59
+ export function memoryGet(hqRoot, sessionId, key) {
60
+ const all = readAll(memoryPath(hqRoot, sessionId));
61
+ if (key !== undefined)
62
+ return { value: all[key]?.value ?? null };
63
+ return { entries: Object.fromEntries(Object.entries(all).map(([k, e]) => [k, e.value])) };
64
+ }
65
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * mesh.presence op (hq-anywhere US-009).
3
+ *
4
+ * Writes one schema-valid session event to the Work Mesh Live spool through
5
+ * the same enqueue path as `hq mesh session … --enqueue`. The daemon's mesh
6
+ * drainer service already flushes that spool, so presence from a session in a
7
+ * foreign cwd reaches Work Mesh Live without any new transport.
8
+ */
9
+ export declare class MeshOpError extends Error {
10
+ readonly code: string;
11
+ constructor(code: string, message: string);
12
+ }
13
+ export interface MeshPresenceArgs {
14
+ sessionId: string;
15
+ harness: string;
16
+ kind: string;
17
+ cwd?: string;
18
+ company?: string;
19
+ project?: string;
20
+ task?: string;
21
+ taskId?: string;
22
+ status?: string;
23
+ summary?: string;
24
+ reason?: string;
25
+ }
26
+ export interface MeshPresenceDeps {
27
+ hqRoot?: string;
28
+ /** Work-mesh root override (tests); defaults to ~/.hq/work-mesh or HQ_WORK_MESH_ROOT. */
29
+ workMeshRoot?: string;
30
+ env?: NodeJS.ProcessEnv;
31
+ now?: () => Date;
32
+ }
33
+ export declare function meshPresence(args: MeshPresenceArgs, deps?: MeshPresenceDeps): {
34
+ eventId: string;
35
+ seq: number;
36
+ spoolPath: string;
37
+ };
38
+ //# sourceMappingURL=mesh.d.ts.map
@@ -0,0 +1,70 @@
1
+ /**
2
+ * mesh.presence op (hq-anywhere US-009).
3
+ *
4
+ * Writes one schema-valid session event to the Work Mesh Live spool through
5
+ * the same enqueue path as `hq mesh session … --enqueue`. The daemon's mesh
6
+ * drainer service already flushes that spool, so presence from a session in a
7
+ * foreign cwd reaches Work Mesh Live without any new transport.
8
+ */
9
+ import { CLI_VERSION } from "../../../cli-version.js";
10
+ import { CLI_KIND_TO_SCHEMA, EnqueueValidationError, enqueueSessionEvent } from "../../mesh/live/enqueue.js";
11
+ import { reserveSessionSeq } from "../../mesh/live/session-seq.js";
12
+ const HARNESSES = ["claude-code", "claude-desktop", "codex", "grok", "hq-sessions", "agent-box", "agents-v2"];
13
+ const SCHEMA_KINDS = new Set(Object.values(CLI_KIND_TO_SCHEMA));
14
+ const STATUSES = new Set(["queued", "in_progress", "review", "done"]);
15
+ export class MeshOpError extends Error {
16
+ code;
17
+ constructor(code, message) {
18
+ super(message);
19
+ this.code = code;
20
+ this.name = "MeshOpError";
21
+ }
22
+ }
23
+ export function meshPresence(args, deps = {}) {
24
+ const env = deps.env ?? process.env;
25
+ if (!HARNESSES.includes(args.harness)) {
26
+ throw new MeshOpError("bad_args", `harness must be one of ${HARNESSES.join(", ")}`);
27
+ }
28
+ const kind = (CLI_KIND_TO_SCHEMA[args.kind] ?? (SCHEMA_KINDS.has(args.kind) ? args.kind : undefined));
29
+ if (!kind)
30
+ throw new MeshOpError("bad_args", `kind must be one of ${Object.keys(CLI_KIND_TO_SCHEMA).join(", ")}`);
31
+ if (args.status !== undefined && !STATUSES.has(args.status))
32
+ throw new MeshOpError("bad_args", "status must be queued, in_progress, review or done");
33
+ let seq;
34
+ try {
35
+ seq = reserveSessionSeq(args.sessionId, 1, env);
36
+ }
37
+ catch (err) {
38
+ throw new MeshOpError("bad_args", `sessionId rejected: ${err.message}`);
39
+ }
40
+ let result;
41
+ try {
42
+ result = enqueueSessionEvent({
43
+ kind,
44
+ sessionId: args.sessionId,
45
+ harness: args.harness,
46
+ adapterVersion: CLI_VERSION,
47
+ source: "hooks",
48
+ seq,
49
+ cwd: args.cwd,
50
+ hqRoot: deps.hqRoot,
51
+ companySlug: args.company,
52
+ project: args.project,
53
+ task: args.task,
54
+ taskId: args.taskId,
55
+ status: args.status,
56
+ summary: args.summary,
57
+ reason: args.reason,
58
+ root: deps.workMeshRoot,
59
+ env,
60
+ now: deps.now,
61
+ });
62
+ }
63
+ catch (err) {
64
+ if (err instanceof EnqueueValidationError)
65
+ throw new MeshOpError("bad_args", err.message);
66
+ throw err;
67
+ }
68
+ return { eventId: result.event.eventId, seq, spoolPath: result.spoolPath };
69
+ }
70
+ //# sourceMappingURL=mesh.js.map
@@ -0,0 +1,78 @@
1
+ /**
2
+ * policy.check and policy.index ops (hq-anywhere US-009).
3
+ *
4
+ * Verdicts come from the hq-core hook scripts under HQ_ROOT, run as child
5
+ * processes with a synthesized hook payload, so hookless hosts and sub-agents
6
+ * get the same answer a hook-dispatching session would. No rule is
7
+ * reimplemented here:
8
+ * - .claude/hooks/mandatory-scope-authorizer.sh decides allow/block
9
+ * (exit 2 = block, reason on stderr).
10
+ * - .claude/hooks/inject-policy-on-trigger.sh supplies additionalContext.
11
+ *
12
+ * The policy index (which policy files exist for a company) is cached
13
+ * in-process and invalidated by fs.watch on core/policies, personal/policies
14
+ * and companies/<co>/policies.
15
+ */
16
+ export declare const SCOPE_AUTHORIZER = ".claude/hooks/mandatory-scope-authorizer.sh";
17
+ export declare const POLICY_TRIGGER = ".claude/hooks/inject-policy-on-trigger.sh";
18
+ export declare class PolicyOpError extends Error {
19
+ readonly code: string;
20
+ constructor(code: string, message: string);
21
+ }
22
+ export interface PolicyEntry {
23
+ slug: string;
24
+ path: string;
25
+ scope: "core" | "personal" | "company";
26
+ company?: string;
27
+ }
28
+ /** Cached list of policy files under HQ_ROOT, invalidated by fs.watch. */
29
+ export declare class PolicyIndex {
30
+ readonly hqRoot: string;
31
+ private readonly log;
32
+ private entries;
33
+ private watchers;
34
+ private closed;
35
+ /** Bumped on every invalidation; lets callers see that the cache was rebuilt. */
36
+ version: number;
37
+ constructor(hqRoot: string, log?: (level: "info" | "warn" | "error", message: string) => void);
38
+ /** Policies visible to `company`: core, personal, and that company only. */
39
+ list(company?: string): PolicyEntry[];
40
+ invalidate(): void;
41
+ close(): void;
42
+ private rebuild;
43
+ private rewatch;
44
+ }
45
+ export interface PolicyCheckArgs {
46
+ company: string;
47
+ event?: string;
48
+ tool: string;
49
+ input?: Record<string, unknown>;
50
+ sessionId?: string;
51
+ cwd?: string;
52
+ }
53
+ export interface PolicyCheckResult {
54
+ decision: "allow" | "block";
55
+ reasons: string[];
56
+ additionalContext: string;
57
+ session: string;
58
+ policies: {
59
+ count: number;
60
+ version: number;
61
+ };
62
+ }
63
+ /**
64
+ * The scope authorizer reads the session's company from
65
+ * workspace/sessions/<sid>/meta.yaml (or scope-capability.json). hqd binds a
66
+ * session id of its own per company + caller, so a check never borrows the
67
+ * binding of an unrelated session.
68
+ */
69
+ export declare function bindCheckSession(hqRoot: string, company: string, callerSession: string | undefined, now: Date): string;
70
+ export interface PolicyCheckDeps {
71
+ hqRoot: string;
72
+ index: PolicyIndex;
73
+ env?: NodeJS.ProcessEnv;
74
+ now?: () => Date;
75
+ timeoutMs?: number;
76
+ }
77
+ export declare function policyCheck(args: PolicyCheckArgs, deps: PolicyCheckDeps): Promise<PolicyCheckResult>;
78
+ //# sourceMappingURL=policy.d.ts.map