@indigoai-us/hq-cli 5.323.0 → 5.324.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 (72) hide show
  1. package/CHANGELOG.md +8 -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/daemon.js +39 -2
  11. package/dist/commands/db-provision.js +6 -1
  12. package/dist/commands/doctor.js +8 -4
  13. package/dist/commands/feedback.d.ts +2 -0
  14. package/dist/commands/feedback.js +10 -0
  15. package/dist/commands/install-global.d.ts +46 -0
  16. package/dist/commands/install-global.js +674 -0
  17. package/dist/commands/link.d.ts +30 -0
  18. package/dist/commands/link.js +184 -0
  19. package/dist/commands/mcp-status.d.ts +6 -0
  20. package/dist/commands/mcp-status.js +21 -4
  21. package/dist/commands/pkg-install.js +20 -1
  22. package/dist/commands/update.d.ts +19 -0
  23. package/dist/commands/update.js +52 -6
  24. package/dist/fast-core.d.ts +2 -1
  25. package/dist/fast-core.js +12 -8
  26. package/dist/index.js +9 -7
  27. package/dist/lib/core-utils/timeout-guard-native-flags.d.ts +6 -0
  28. package/dist/lib/core-utils/timeout-guard-native-flags.js +30 -0
  29. package/dist/lib/core-utils/timeout-guard-native.d.ts +0 -3
  30. package/dist/lib/core-utils/timeout-guard-native.js +4 -30
  31. package/dist/lib/core-utils/timeout-guard.d.ts +2 -0
  32. package/dist/lib/core-utils/timeout-guard.js +2 -1
  33. package/dist/lib/daemon/convergence.d.ts +42 -0
  34. package/dist/lib/daemon/convergence.js +66 -0
  35. package/dist/lib/daemon/heartbeat.d.ts +43 -0
  36. package/dist/lib/daemon/heartbeat.js +70 -0
  37. package/dist/lib/daemon/ops/journal.d.ts +29 -0
  38. package/dist/lib/daemon/ops/journal.js +141 -0
  39. package/dist/lib/daemon/ops/memory.d.ts +20 -0
  40. package/dist/lib/daemon/ops/memory.js +65 -0
  41. package/dist/lib/daemon/ops/mesh.d.ts +38 -0
  42. package/dist/lib/daemon/ops/mesh.js +70 -0
  43. package/dist/lib/daemon/ops/policy.d.ts +78 -0
  44. package/dist/lib/daemon/ops/policy.js +283 -0
  45. package/dist/lib/daemon/protocol.d.ts +68 -0
  46. package/dist/lib/daemon/protocol.js +154 -0
  47. package/dist/lib/daemon/run.d.ts +10 -0
  48. package/dist/lib/daemon/run.js +85 -3
  49. package/dist/lib/daemon/server.d.ts +36 -0
  50. package/dist/lib/daemon/server.js +306 -0
  51. package/dist/lib/daemon/session-registry.d.ts +30 -0
  52. package/dist/lib/daemon/session-registry.js +42 -0
  53. package/dist/lib/doctor/checks/anywhere.d.ts +38 -0
  54. package/dist/lib/doctor/checks/anywhere.js +182 -0
  55. package/dist/lib/doctor/registry.js +6 -0
  56. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  57. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  58. package/dist/lib/install/claude-targets.d.ts +47 -0
  59. package/dist/lib/install/claude-targets.js +203 -0
  60. package/dist/lib/install/codex-targets.d.ts +57 -0
  61. package/dist/lib/install/codex-targets.js +187 -0
  62. package/dist/lib/mesh/live/session-seq.js +62 -9
  63. package/dist/lib/registry/normalize-remote.d.ts +21 -0
  64. package/dist/lib/registry/normalize-remote.js +68 -0
  65. package/dist/lib/registry/registry.d.ts +75 -0
  66. package/dist/lib/registry/registry.js +261 -0
  67. package/dist/utils/cognito-session.d.ts +5 -1
  68. package/dist/utils/cognito-session.js +8 -12
  69. package/dist/utils/feedback-diagnostics.d.ts +2 -0
  70. package/dist/utils/version-check.d.ts +2 -0
  71. package/dist/utils/version-check.js +7 -0
  72. package/package.json +2 -2
@@ -0,0 +1,283 @@
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
+ import { spawn } from "node:child_process";
17
+ import * as fs from "node:fs";
18
+ import * as path from "node:path";
19
+ export const SCOPE_AUTHORIZER = ".claude/hooks/mandatory-scope-authorizer.sh";
20
+ export const POLICY_TRIGGER = ".claude/hooks/inject-policy-on-trigger.sh";
21
+ const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
22
+ const SESSION_RE = /^[A-Za-z0-9._-]+$/;
23
+ export class PolicyOpError extends Error {
24
+ code;
25
+ constructor(code, message) {
26
+ super(message);
27
+ this.code = code;
28
+ this.name = "PolicyOpError";
29
+ }
30
+ }
31
+ /** Cached list of policy files under HQ_ROOT, invalidated by fs.watch. */
32
+ export class PolicyIndex {
33
+ hqRoot;
34
+ log;
35
+ entries = null;
36
+ watchers = [];
37
+ closed = false;
38
+ /** Bumped on every invalidation; lets callers see that the cache was rebuilt. */
39
+ version = 0;
40
+ constructor(hqRoot, log = () => { }) {
41
+ this.hqRoot = hqRoot;
42
+ this.log = log;
43
+ }
44
+ /** Policies visible to `company`: core, personal, and that company only. */
45
+ list(company) {
46
+ if (!this.entries)
47
+ this.rebuild();
48
+ return this.entries.filter((e) => e.scope !== "company" || e.company === company);
49
+ }
50
+ invalidate() {
51
+ if (this.entries === null)
52
+ return;
53
+ this.entries = null;
54
+ this.version += 1;
55
+ }
56
+ close() {
57
+ this.closed = true;
58
+ for (const w of this.watchers)
59
+ w.close();
60
+ this.watchers = [];
61
+ }
62
+ rebuild() {
63
+ const dirs = [
64
+ { dir: path.join(this.hqRoot, "core", "policies"), scope: "core" },
65
+ { dir: path.join(this.hqRoot, "personal", "policies"), scope: "personal" },
66
+ ];
67
+ const companiesDir = path.join(this.hqRoot, "companies");
68
+ const companyDirs = [];
69
+ for (const co of readDirNames(companiesDir)) {
70
+ if (!SLUG_RE.test(co))
71
+ continue;
72
+ const companyDir = path.join(companiesDir, co);
73
+ if (isDir(companyDir))
74
+ companyDirs.push(companyDir);
75
+ const dir = path.join(companiesDir, co, "policies");
76
+ if (isDir(dir))
77
+ dirs.push({ dir, scope: "company", company: co });
78
+ }
79
+ const entries = [];
80
+ for (const d of dirs) {
81
+ for (const name of readDirNames(d.dir)) {
82
+ if (!name.endsWith(".md") || name.startsWith("_"))
83
+ continue;
84
+ entries.push({ slug: name.slice(0, -3), path: path.join(d.dir, name), scope: d.scope, ...(d.company ? { company: d.company } : {}) });
85
+ }
86
+ }
87
+ this.entries = entries;
88
+ this.rewatch([...dirs.map((d) => d.dir), ...companyDirs, companiesDir]);
89
+ }
90
+ rewatch(dirs) {
91
+ for (const w of this.watchers)
92
+ w.close();
93
+ this.watchers = [];
94
+ if (this.closed)
95
+ return;
96
+ for (const dir of dirs) {
97
+ if (!isDir(dir))
98
+ continue;
99
+ try {
100
+ const w = fs.watch(dir, () => this.invalidate());
101
+ w.on("error", (err) => {
102
+ this.log("warn", `policy index watch on ${dir} failed: ${err.message}`);
103
+ this.invalidate();
104
+ });
105
+ this.watchers.push(w);
106
+ }
107
+ catch (err) {
108
+ this.log("warn", `could not watch ${dir}: ${err.message}`);
109
+ }
110
+ }
111
+ }
112
+ }
113
+ function readDirNames(dir) {
114
+ try {
115
+ return fs.readdirSync(dir);
116
+ }
117
+ catch (err) {
118
+ if (err.code === "ENOENT" || err.code === "ENOTDIR")
119
+ return [];
120
+ throw err;
121
+ }
122
+ }
123
+ function isDir(dir) {
124
+ try {
125
+ return fs.statSync(dir).isDirectory();
126
+ }
127
+ catch {
128
+ return false;
129
+ }
130
+ }
131
+ function runScript(script, payload, env, cwd, timeoutMs) {
132
+ return new Promise((resolve, reject) => {
133
+ const child = spawn("bash", [script], { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
134
+ let stdout = "";
135
+ let stderr = "";
136
+ let timedOut = false;
137
+ const timer = setTimeout(() => {
138
+ timedOut = true;
139
+ child.kill("SIGKILL");
140
+ }, timeoutMs);
141
+ child.stdout.on("data", (c) => (stdout += c.toString("utf8")));
142
+ child.stderr.on("data", (c) => (stderr += c.toString("utf8")));
143
+ child.on("error", (err) => {
144
+ clearTimeout(timer);
145
+ reject(err);
146
+ });
147
+ child.on("close", (code) => {
148
+ clearTimeout(timer);
149
+ resolve({ code, stdout, stderr, timedOut });
150
+ });
151
+ child.stdin.on("error", () => {
152
+ // The script may exit before reading stdin; its exit code still decides.
153
+ });
154
+ child.stdin.end(payload);
155
+ });
156
+ }
157
+ /**
158
+ * The scope authorizer reads the session's company from
159
+ * workspace/sessions/<sid>/meta.yaml (or scope-capability.json). hqd binds a
160
+ * session id of its own per company + caller, so a check never borrows the
161
+ * binding of an unrelated session.
162
+ */
163
+ export function bindCheckSession(hqRoot, company, callerSession, now) {
164
+ const caller = (callerSession ?? "default").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80) || "default";
165
+ const sid = `hqd-${company}-${caller}`;
166
+ if (!SESSION_RE.test(sid))
167
+ throw new PolicyOpError("bad_args", "sessionId has no usable characters");
168
+ const dir = path.join(hqRoot, "workspace", "sessions", sid);
169
+ const meta = path.join(dir, "meta.yaml");
170
+ const body = `session_id: ${sid}\nstarted_at: ${now.toISOString()}\nsenior: user\ncompany_slug: ${company}\nsource: hqd\n`;
171
+ let current = "";
172
+ try {
173
+ current = fs.readFileSync(meta, "utf8");
174
+ }
175
+ catch (err) {
176
+ if (err.code !== "ENOENT")
177
+ throw err;
178
+ }
179
+ if (!current.includes(`\ncompany_slug: ${company}\n`)) {
180
+ fs.mkdirSync(dir, { recursive: true });
181
+ const tmp = `${meta}.${process.pid}.tmp`;
182
+ fs.writeFileSync(tmp, body);
183
+ fs.renameSync(tmp, meta);
184
+ }
185
+ return sid;
186
+ }
187
+ function extractContext(stdout) {
188
+ const text = stdout.trim();
189
+ if (!text)
190
+ return "";
191
+ try {
192
+ const parsed = JSON.parse(text);
193
+ const ctx = parsed.hookSpecificOutput?.additionalContext ?? parsed.additionalContext;
194
+ return typeof ctx === "string" ? ctx : "";
195
+ }
196
+ catch {
197
+ return text;
198
+ }
199
+ }
200
+ function blockFromJson(stdout) {
201
+ try {
202
+ const parsed = JSON.parse(stdout.trim());
203
+ if (parsed.decision === "block")
204
+ return typeof parsed.reason === "string" ? parsed.reason : "blocked";
205
+ if (parsed.hookSpecificOutput?.permissionDecision === "deny") {
206
+ const r = parsed.hookSpecificOutput.permissionDecisionReason;
207
+ return typeof r === "string" ? r : "denied";
208
+ }
209
+ }
210
+ catch {
211
+ // Plain-text stdout is not a verdict.
212
+ }
213
+ return null;
214
+ }
215
+ export async function policyCheck(args, deps) {
216
+ const { hqRoot } = deps;
217
+ if (!SLUG_RE.test(args.company))
218
+ throw new PolicyOpError("bad_args", "company must be a company slug");
219
+ if (!isDir(path.join(hqRoot, "companies", args.company))) {
220
+ throw new PolicyOpError("unknown_company", `no companies/${args.company} under ${hqRoot}`);
221
+ }
222
+ const authorizer = path.join(hqRoot, SCOPE_AUTHORIZER);
223
+ const trigger = path.join(hqRoot, POLICY_TRIGGER);
224
+ if (!fs.existsSync(authorizer)) {
225
+ throw new PolicyOpError("policy_engine_unavailable", `scope authorizer not found at ${authorizer}`);
226
+ }
227
+ const event = args.event ?? "PreToolUse";
228
+ const session = bindCheckSession(hqRoot, args.company, args.sessionId, (deps.now ?? (() => new Date()))());
229
+ const payload = JSON.stringify({
230
+ session_id: session,
231
+ hook_event_name: event,
232
+ tool_name: args.tool,
233
+ tool_input: args.input ?? {},
234
+ cwd: args.cwd ?? hqRoot,
235
+ });
236
+ const env = {
237
+ ...(deps.env ?? process.env),
238
+ HQ_ROOT: hqRoot,
239
+ CLAUDE_PROJECT_DIR: hqRoot,
240
+ HQ_POLICY_COMPANY: args.company,
241
+ BASH_ENV: "/dev/null",
242
+ };
243
+ delete env.HQ_HOOK_SESSION_ID;
244
+ delete env.HQ_HOOK_TOOL_NAME;
245
+ const timeoutMs = deps.timeoutMs ?? 15_000;
246
+ const reasons = [];
247
+ let decision = "allow";
248
+ if (event === "PreToolUse") {
249
+ const r = await runScript(authorizer, payload, env, hqRoot, timeoutMs);
250
+ const jsonBlock = blockFromJson(r.stdout);
251
+ if (r.timedOut) {
252
+ decision = "block";
253
+ reasons.push(`scope authorizer timed out after ${timeoutMs}ms; failing closed`);
254
+ }
255
+ else if (r.code === 2 || jsonBlock) {
256
+ decision = "block";
257
+ reasons.push((jsonBlock ?? r.stderr).trim() || "blocked by scope authorizer");
258
+ }
259
+ else if (r.code !== 0) {
260
+ decision = "block";
261
+ reasons.push(`scope authorizer exited ${r.code}; failing closed: ${r.stderr.trim()}`);
262
+ }
263
+ }
264
+ let additionalContext = "";
265
+ if (fs.existsSync(trigger)) {
266
+ const r = await runScript(trigger, payload, env, hqRoot, timeoutMs);
267
+ if (r.code === 0 && !r.timedOut)
268
+ additionalContext = extractContext(r.stdout);
269
+ else
270
+ reasons.push(`policy trigger hook did not complete (exit ${r.code}${r.timedOut ? ", timed out" : ""}); no policy context`);
271
+ }
272
+ else {
273
+ reasons.push(`policy trigger hook not found at ${trigger}; no policy context`);
274
+ }
275
+ return {
276
+ decision,
277
+ reasons,
278
+ additionalContext,
279
+ session,
280
+ policies: { count: deps.index.list(args.company).length, version: deps.index.version },
281
+ };
282
+ }
283
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * hqd wire protocol (hq-anywhere US-008).
3
+ *
4
+ * One Unix domain socket (a named pipe on Windows) carrying newline-delimited
5
+ * JSON. Requests are `{id, op, args}`; each gets exactly one response
6
+ * `{id, ok: true, result}` or `{id, ok: false, error}`. Responses may arrive
7
+ * out of order, so clients match them by `id`.
8
+ */
9
+ export declare const HQD_SOCKET_NAME = "hqd.sock";
10
+ export declare const HQD_OPS: readonly ["ping", "resolve", "session.open", "session.get", "session.close", "policy.check", "policy.index", "journal.append", "memory.get", "memory.put", "mesh.presence", "shutdown"];
11
+ export type HqdOp = (typeof HQD_OPS)[number];
12
+ /** A request line larger than this is rejected and the connection closed. */
13
+ export declare const HQD_MAX_LINE_BYTES: number;
14
+ export interface HqdRequest {
15
+ id: string | number;
16
+ op: string;
17
+ args?: Record<string, unknown>;
18
+ }
19
+ export interface HqdError {
20
+ code: string;
21
+ message: string;
22
+ }
23
+ export type HqdResponse = {
24
+ id: string | number | null;
25
+ ok: true;
26
+ result: unknown;
27
+ } | {
28
+ id: string | number | null;
29
+ ok: false;
30
+ error: HqdError;
31
+ };
32
+ /**
33
+ * Socket path: HQ_HQD_SOCKET, else <HQ_REGISTRY_DIR or ~/.hq>/hqd.sock.
34
+ * On Windows the socket is a per-user named pipe.
35
+ */
36
+ export declare function hqdSocketPath(home?: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string;
37
+ export declare function encodeMessage(message: HqdRequest | HqdResponse): string;
38
+ export type ParsedLine = {
39
+ ok: true;
40
+ request: HqdRequest;
41
+ } | {
42
+ ok: false;
43
+ id: string | number | null;
44
+ error: HqdError;
45
+ };
46
+ /** Validate one request line. Never throws. */
47
+ export declare function parseRequestLine(line: string): ParsedLine;
48
+ /**
49
+ * Split a byte stream into lines. Returns a push function that hands each
50
+ * complete line to `onLine`, and reports overflow when a partial line grows
51
+ * past `maxBytes`.
52
+ */
53
+ export declare function createLineSplitter(onLine: (line: string) => void, onOverflow: () => void, maxBytes?: number): (chunk: Buffer | string) => void;
54
+ export declare class HqdClientError extends Error {
55
+ readonly code: string;
56
+ constructor(code: string, message: string);
57
+ }
58
+ export interface HqdRequestOptions {
59
+ socketPath?: string;
60
+ /** Whole-request budget, including connect. Default 2000ms. */
61
+ timeoutMs?: number;
62
+ }
63
+ /**
64
+ * Send one request and wait for its response. A daemon that is not running
65
+ * fails fast with the socket error code (ENOENT or ECONNREFUSED).
66
+ */
67
+ export declare function hqdRequest(op: HqdOp | string, args?: Record<string, unknown>, opts?: HqdRequestOptions): Promise<unknown>;
68
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1,154 @@
1
+ /**
2
+ * hqd wire protocol (hq-anywhere US-008).
3
+ *
4
+ * One Unix domain socket (a named pipe on Windows) carrying newline-delimited
5
+ * JSON. Requests are `{id, op, args}`; each gets exactly one response
6
+ * `{id, ok: true, result}` or `{id, ok: false, error}`. Responses may arrive
7
+ * out of order, so clients match them by `id`.
8
+ */
9
+ import * as net from "node:net";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+ export const HQD_SOCKET_NAME = "hqd.sock";
13
+ export const HQD_OPS = [
14
+ "ping",
15
+ "resolve",
16
+ "session.open",
17
+ "session.get",
18
+ "session.close",
19
+ "policy.check",
20
+ "policy.index",
21
+ "journal.append",
22
+ "memory.get",
23
+ "memory.put",
24
+ "mesh.presence",
25
+ "shutdown",
26
+ ];
27
+ /** A request line larger than this is rejected and the connection closed. */
28
+ export const HQD_MAX_LINE_BYTES = 1024 * 1024;
29
+ /**
30
+ * Socket path: HQ_HQD_SOCKET, else <HQ_REGISTRY_DIR or ~/.hq>/hqd.sock.
31
+ * On Windows the socket is a per-user named pipe.
32
+ */
33
+ export function hqdSocketPath(home = os.homedir(), env = process.env, platform = process.platform) {
34
+ const override = env.HQ_HQD_SOCKET?.trim();
35
+ if (override)
36
+ return override;
37
+ if (platform === "win32") {
38
+ const user = (env.USERNAME ?? os.userInfo().username).replace(/[^A-Za-z0-9_.-]/g, "-");
39
+ return `\\\\.\\pipe\\hqd-${user}`;
40
+ }
41
+ const dir = env.HQ_REGISTRY_DIR?.trim() ? path.resolve(env.HQ_REGISTRY_DIR.trim()) : path.join(home, ".hq");
42
+ return path.join(dir, HQD_SOCKET_NAME);
43
+ }
44
+ export function encodeMessage(message) {
45
+ return `${JSON.stringify(message)}\n`;
46
+ }
47
+ /** Validate one request line. Never throws. */
48
+ export function parseRequestLine(line) {
49
+ let raw;
50
+ try {
51
+ raw = JSON.parse(line);
52
+ }
53
+ catch {
54
+ return { ok: false, id: null, error: { code: "bad_json", message: "request is not valid JSON" } };
55
+ }
56
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
57
+ return { ok: false, id: null, error: { code: "bad_request", message: "request must be a JSON object" } };
58
+ }
59
+ const obj = raw;
60
+ const id = typeof obj.id === "string" || typeof obj.id === "number" ? obj.id : null;
61
+ if (id === null)
62
+ return { ok: false, id: null, error: { code: "bad_request", message: "request needs a string or number id" } };
63
+ if (typeof obj.op !== "string" || !obj.op)
64
+ return { ok: false, id, error: { code: "bad_request", message: "request needs an op" } };
65
+ if (obj.args !== undefined && (obj.args === null || typeof obj.args !== "object" || Array.isArray(obj.args))) {
66
+ return { ok: false, id, error: { code: "bad_request", message: "args must be an object" } };
67
+ }
68
+ return { ok: true, request: { id, op: obj.op, args: obj.args ?? {} } };
69
+ }
70
+ /**
71
+ * Split a byte stream into lines. Returns a push function that hands each
72
+ * complete line to `onLine`, and reports overflow when a partial line grows
73
+ * past `maxBytes`.
74
+ */
75
+ export function createLineSplitter(onLine, onOverflow, maxBytes = HQD_MAX_LINE_BYTES) {
76
+ let buffer = "";
77
+ let overflowed = false;
78
+ return (chunk) => {
79
+ if (overflowed)
80
+ return;
81
+ buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
82
+ let nl = buffer.indexOf("\n");
83
+ while (nl !== -1) {
84
+ const line = buffer.slice(0, nl).replace(/\r$/, "");
85
+ buffer = buffer.slice(nl + 1);
86
+ if (Buffer.byteLength(line, "utf8") > maxBytes) {
87
+ overflowed = true;
88
+ buffer = "";
89
+ onOverflow();
90
+ return;
91
+ }
92
+ if (line.trim())
93
+ onLine(line);
94
+ nl = buffer.indexOf("\n");
95
+ }
96
+ if (Buffer.byteLength(buffer, "utf8") > maxBytes) {
97
+ overflowed = true;
98
+ buffer = "";
99
+ onOverflow();
100
+ }
101
+ };
102
+ }
103
+ export class HqdClientError extends Error {
104
+ code;
105
+ constructor(code, message) {
106
+ super(message);
107
+ this.code = code;
108
+ this.name = "HqdClientError";
109
+ }
110
+ }
111
+ let nextClientId = 0;
112
+ /**
113
+ * Send one request and wait for its response. A daemon that is not running
114
+ * fails fast with the socket error code (ENOENT or ECONNREFUSED).
115
+ */
116
+ export function hqdRequest(op, args = {}, opts = {}) {
117
+ const socketPath = opts.socketPath ?? hqdSocketPath();
118
+ const timeoutMs = opts.timeoutMs ?? 2_000;
119
+ const id = `c${process.pid}-${(nextClientId += 1)}`;
120
+ return new Promise((resolve, reject) => {
121
+ const socket = net.createConnection(socketPath);
122
+ let settled = false;
123
+ const finish = (fn) => {
124
+ if (settled)
125
+ return;
126
+ settled = true;
127
+ clearTimeout(timer);
128
+ socket.destroy();
129
+ fn();
130
+ };
131
+ const timer = setTimeout(() => finish(() => reject(new HqdClientError("ETIMEDOUT", `hqd did not answer ${op} within ${timeoutMs}ms`))), timeoutMs);
132
+ const push = createLineSplitter((line) => {
133
+ let msg;
134
+ try {
135
+ msg = JSON.parse(line);
136
+ }
137
+ catch {
138
+ finish(() => reject(new HqdClientError("bad_response", "hqd sent a line that is not JSON")));
139
+ return;
140
+ }
141
+ if (msg.id !== id)
142
+ return;
143
+ if (msg.ok)
144
+ finish(() => resolve(msg.result));
145
+ else
146
+ finish(() => reject(new HqdClientError(msg.error.code, msg.error.message)));
147
+ }, () => finish(() => reject(new HqdClientError("bad_response", "hqd response too large"))));
148
+ socket.on("connect", () => socket.write(encodeMessage({ id, op, args })));
149
+ socket.on("data", push);
150
+ socket.on("error", (err) => finish(() => reject(new HqdClientError(err.code ?? "ESOCKET", `hqd is not reachable at ${socketPath}: ${err.message}`))));
151
+ socket.on("close", () => finish(() => reject(new HqdClientError("ECONNRESET", "hqd closed the connection before answering"))));
152
+ });
153
+ }
154
+ //# sourceMappingURL=protocol.js.map
@@ -7,11 +7,21 @@
7
7
  */
8
8
  import { type UnitPlan } from "./catalog.js";
9
9
  import { type LegacyUnit } from "./legacy.js";
10
+ import { type DaemonLockResult } from "./lock.js";
10
11
  import { type MachineType } from "./machine-type.js";
11
12
  import { type HostApp } from "./supervisor.js";
12
13
  export declare const LOG_ROTATE_BYTES: number;
13
14
  /** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
14
15
  export declare const RESTART_EXIT_CODE = 75;
16
+ export interface DaemonLockConflictOutcome {
17
+ exitCode: 0 | 1;
18
+ stdout?: string;
19
+ stderr?: string;
20
+ }
21
+ /** Preserve the legacy lock failure unless Anywhere explicitly enables the no-op behavior. */
22
+ export declare function daemonLockConflictOutcome(lock: DaemonLockResult, anywhereEnabled: boolean): DaemonLockConflictOutcome | null;
23
+ /** Node's Windows named-pipe API uses a default DACL that is not user-scoped. */
24
+ export declare function hqdStartupRefusal(platform: NodeJS.Platform, anywhereEnabled: boolean): string | null;
15
25
  /** Route an updater request through the daemon's existing graceful handoff. */
16
26
  export declare function handoffForPackageUpdateRequest(requested: boolean, restartDaemon: () => void): void;
17
27
  export interface RunDaemonOptions {
@@ -12,6 +12,7 @@ import * as os from "node:os";
12
12
  import * as path from "node:path";
13
13
  import { loadCachedTokens } from "@indigoai-us/hq-cloud";
14
14
  import { CLI_VERSION } from "../../cli-version.js";
15
+ import { anywhereRuntimeEnabled } from "../../commands/anywhere-runtime-flag.js";
15
16
  import { currentPackageUseUpdateRequested } from "../../utils/package-use-lease.js";
16
17
  import { isLocalDependencyInstall, resolveRunningInstall } from "../../utils/version-gate.js";
17
18
  import { agentKitPaths } from "../agent-kit/paths.js";
@@ -26,9 +27,13 @@ import { acquireDaemonLock, isProcessAlive, processCommandLine, releaseDaemonLoc
26
27
  import { retireLegacyMeshUnits } from "./migrate.js";
27
28
  import { daemonDir as meshDaemonDir, daemonPidPath as meshDaemonPidPath } from "../mesh/live/daemon/paths.js";
28
29
  import { readPidLock } from "../mesh/live/daemon/pid-lock.js";
30
+ import { registryDir, registryFile } from "../registry/registry.js";
29
31
  import { daemonRootRefusal } from "./install.js";
30
32
  import { detectMachineType } from "./machine-type.js";
31
33
  import { appendPrivate, daemonPaths, ensurePrivateDir, openPrivateAppend } from "./paths.js";
34
+ import { hqdSocketPath } from "./protocol.js";
35
+ import { startHqdServer } from "./server.js";
36
+ import { heartbeatFile, startHeartbeat } from "./heartbeat.js";
32
37
  import { createProtocolTail, ensureProtocolFifo, openProtocolWriter } from "./protocol-tail.js";
33
38
  import { takeControlRequests } from "./requests.js";
34
39
  import { currentHqCloud, hqCloudRunners } from "./hq-cloud.js";
@@ -40,6 +45,26 @@ import { userToolDirs } from "./tools.js";
40
45
  export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
41
46
  /** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
42
47
  export const RESTART_EXIT_CODE = 75;
48
+ /** Preserve the legacy lock failure unless Anywhere explicitly enables the no-op behavior. */
49
+ export function daemonLockConflictOutcome(lock, anywhereEnabled) {
50
+ if (lock.ok)
51
+ return null;
52
+ if (lock.owner && anywhereEnabled) {
53
+ return { exitCode: 0, stdout: `hq daemon is already running (pid ${lock.owner.pid}).\n` };
54
+ }
55
+ return {
56
+ exitCode: 1,
57
+ stderr: lock.owner
58
+ ? `hq daemon is already running (pid ${lock.owner.pid}).\n`
59
+ : "another hq daemon is starting up.\n",
60
+ };
61
+ }
62
+ /** Node's Windows named-pipe API uses a default DACL that is not user-scoped. */
63
+ export function hqdStartupRefusal(platform, anywhereEnabled) {
64
+ if (platform !== "win32" || !anywhereEnabled)
65
+ return null;
66
+ return "hqd is disabled on Windows because the default named-pipe ACL grants read access to Everyone and Anonymous accounts.";
67
+ }
43
68
  /** Route an updater request through the daemon's existing graceful handoff. */
44
69
  export function handoffForPackageUpdateRequest(requested, restartDaemon) {
45
70
  if (requested)
@@ -280,9 +305,13 @@ export async function runDaemon(opts) {
280
305
  const paths = daemonPaths(home, env);
281
306
  const log = makeLogger(paths, opts.foreground === true);
282
307
  const lock = acquireDaemonLock(paths.lockPath);
283
- if (!lock.ok) {
284
- process.stderr.write(lock.owner ? `hq daemon is already running (pid ${lock.owner.pid}).\n` : "another hq daemon is starting up.\n");
285
- return 1;
308
+ const lockConflict = daemonLockConflictOutcome(lock, !lock.ok && lock.owner ? anywhereRuntimeEnabled(env) : false);
309
+ if (lockConflict) {
310
+ if (lockConflict.stdout)
311
+ process.stdout.write(lockConflict.stdout);
312
+ if (lockConflict.stderr)
313
+ process.stderr.write(lockConflict.stderr);
314
+ return lockConflict.exitCode;
286
315
  }
287
316
  const initial = readDaemonConfig(paths.configPath);
288
317
  const detected = detectMachineType(undefined, {
@@ -659,6 +688,45 @@ export async function runDaemon(opts) {
659
688
  clearTimeout(timer);
660
689
  return on;
661
690
  };
691
+ // hqd: the session/company socket every runtime talks to (hq-anywhere US-008).
692
+ let hqd = null;
693
+ let hqdShutdown = null;
694
+ if (anywhereRuntimeEnabled()) {
695
+ const refusal = hqdStartupRefusal(platform, true);
696
+ if (refusal) {
697
+ log("error", refusal);
698
+ }
699
+ else {
700
+ try {
701
+ const startConfig = readDaemonConfig(paths.configPath);
702
+ hqd = await startHqdServer({
703
+ socketPath: hqdSocketPath(home, env),
704
+ registryPath: registryFile(registryDir(home, env)),
705
+ hqRoot: startConfig.hqRoot ?? (env.HQ_ROOT?.trim() || undefined),
706
+ log,
707
+ onShutdown: () => hqdShutdown?.(),
708
+ reportError: reportHqdCrash,
709
+ });
710
+ }
711
+ catch (err) {
712
+ log("error", `hqd socket did not start; services run without it: ${err.message}`);
713
+ }
714
+ }
715
+ }
716
+ // US-012: 60s presence heartbeat with version and session count.
717
+ const liveHqd = hqd;
718
+ const stopHeartbeat = liveHqd
719
+ ? startHeartbeat({
720
+ version: CLI_VERSION,
721
+ sessionCount: () => liveHqd.sessions.size,
722
+ file: heartbeatFile(home, env),
723
+ log,
724
+ })
725
+ : () => { };
726
+ const closeHqd = () => {
727
+ stopHeartbeat();
728
+ void hqd?.close().catch((err) => log("warn", `hqd socket did not close cleanly: ${err.message}`));
729
+ };
662
730
  return await new Promise((resolve) => {
663
731
  let stopping = false;
664
732
  let restartPending = false;
@@ -667,6 +735,7 @@ export async function runDaemon(opts) {
667
735
  return;
668
736
  stopping = true;
669
737
  log("info", `${why}; stopping services`);
738
+ closeHqd();
670
739
  // The supervisor escalates to SIGKILL after STOP_ESCALATION_MS; the extra
671
740
  // margin only covers a child that survives even that.
672
741
  let capTimer;
@@ -691,6 +760,7 @@ export async function runDaemon(opts) {
691
760
  if (stopping)
692
761
  return;
693
762
  stopping = true;
763
+ closeHqd();
694
764
  const released = supervisor.release();
695
765
  // What these readers have not read stays in the FIFO for the next daemon.
696
766
  for (const tail of protocolTails)
@@ -748,6 +818,7 @@ export async function runDaemon(opts) {
748
818
  return true;
749
819
  });
750
820
  };
821
+ hqdShutdown = () => shutdown("hqd shutdown request");
751
822
  process.on("SIGTERM", () => shutdown("received SIGTERM"));
752
823
  process.on("SIGINT", () => shutdown("received SIGINT"));
753
824
  if (platform !== "win32")
@@ -757,9 +828,20 @@ export async function runDaemon(opts) {
757
828
  }
758
829
  catch (err) {
759
830
  log("error", `supervisor failed to start: ${err.message}`);
831
+ closeHqd();
760
832
  releaseDaemonLock(paths.lockPath);
761
833
  resolve(1);
762
834
  }
763
835
  });
764
836
  }
837
+ /** Report an hqd op crash to the CLI's existing Sentry integration, tagged with the failing op. */
838
+ function reportHqdCrash(op, err) {
839
+ void import("../../sentry.js").then(({ Sentry }) => {
840
+ Sentry.withScope((scope) => {
841
+ scope.setTag("component", "hqd");
842
+ scope.setTag("hqd.op", op);
843
+ Sentry.captureException(err);
844
+ });
845
+ }, (importErr) => process.stderr.write(`hqd could not load Sentry to report a ${op} failure: ${String(importErr)}\n`));
846
+ }
765
847
  //# sourceMappingURL=run.js.map