@indigoai-us/hq-cli 5.108.1 → 5.108.3

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 (109) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/assets/mesh-daemon/README.md +23 -0
  3. package/assets/mesh-daemon/launchd.plist.template +33 -0
  4. package/assets/mesh-daemon/systemd.system.service.template +27 -0
  5. package/assets/mesh-daemon/systemd.user.service.template +18 -0
  6. package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +267 -0
  7. package/dist/commands/core.js +5 -4
  8. package/dist/commands/index-cmd.js +13 -0
  9. package/dist/commands/mesh.d.ts +15 -0
  10. package/dist/commands/mesh.js +732 -4
  11. package/dist/commands/search.d.ts +23 -0
  12. package/dist/commands/search.js +42 -1
  13. package/dist/lib/doctor/checks/work-context.d.ts +23 -0
  14. package/dist/lib/doctor/checks/work-context.js +195 -0
  15. package/dist/lib/doctor/registry.js +4 -0
  16. package/dist/lib/mesh/api.d.ts +18 -0
  17. package/dist/lib/mesh/api.js +55 -0
  18. package/dist/lib/mesh/cache.js +35 -4
  19. package/dist/lib/mesh/client.d.ts +118 -0
  20. package/dist/lib/mesh/client.js +268 -0
  21. package/dist/lib/mesh/live/backoff.d.ts +16 -0
  22. package/dist/lib/mesh/live/backoff.js +20 -0
  23. package/dist/lib/mesh/live/daemon/board-refresh.d.ts +49 -0
  24. package/dist/lib/mesh/live/daemon/board-refresh.js +185 -0
  25. package/dist/lib/mesh/live/daemon/credentials.d.ts +71 -0
  26. package/dist/lib/mesh/live/daemon/credentials.js +227 -0
  27. package/dist/lib/mesh/live/daemon/doctor.d.ts +37 -0
  28. package/dist/lib/mesh/live/daemon/doctor.js +139 -0
  29. package/dist/lib/mesh/live/daemon/index.d.ts +24 -0
  30. package/dist/lib/mesh/live/daemon/index.js +14 -0
  31. package/dist/lib/mesh/live/daemon/install.d.ts +83 -0
  32. package/dist/lib/mesh/live/daemon/install.js +330 -0
  33. package/dist/lib/mesh/live/daemon/log.d.ts +11 -0
  34. package/dist/lib/mesh/live/daemon/log.js +79 -0
  35. package/dist/lib/mesh/live/daemon/paths.d.ts +18 -0
  36. package/dist/lib/mesh/live/daemon/paths.js +31 -0
  37. package/dist/lib/mesh/live/daemon/pid-lock.d.ts +48 -0
  38. package/dist/lib/mesh/live/daemon/pid-lock.js +134 -0
  39. package/dist/lib/mesh/live/daemon/presence.d.ts +101 -0
  40. package/dist/lib/mesh/live/daemon/presence.js +250 -0
  41. package/dist/lib/mesh/live/daemon/presign.d.ts +18 -0
  42. package/dist/lib/mesh/live/daemon/presign.js +65 -0
  43. package/dist/lib/mesh/live/daemon/run.d.ts +74 -0
  44. package/dist/lib/mesh/live/daemon/run.js +400 -0
  45. package/dist/lib/mesh/live/daemon/state.d.ts +24 -0
  46. package/dist/lib/mesh/live/daemon/state.js +57 -0
  47. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +170 -0
  48. package/dist/lib/mesh/live/daemon/transcript-watch.js +520 -0
  49. package/dist/lib/mesh/live/enqueue.d.ts +48 -0
  50. package/dist/lib/mesh/live/enqueue.js +100 -0
  51. package/dist/lib/mesh/live/flush.d.ts +56 -0
  52. package/dist/lib/mesh/live/flush.js +360 -0
  53. package/dist/lib/mesh/live/format-spool-line.d.ts +42 -0
  54. package/dist/lib/mesh/live/format-spool-line.js +106 -0
  55. package/dist/lib/mesh/live/index.d.ts +22 -0
  56. package/dist/lib/mesh/live/index.js +16 -0
  57. package/dist/lib/mesh/live/paths.d.ts +13 -0
  58. package/dist/lib/mesh/live/paths.js +30 -0
  59. package/dist/lib/mesh/live/session-events-client.d.ts +58 -0
  60. package/dist/lib/mesh/live/session-events-client.js +120 -0
  61. package/dist/lib/mesh/live/session-identity.d.ts +39 -0
  62. package/dist/lib/mesh/live/session-identity.js +91 -0
  63. package/dist/lib/mesh/live/spool.d.ts +52 -0
  64. package/dist/lib/mesh/live/spool.js +193 -0
  65. package/dist/lib/mesh/live/ulid.d.ts +12 -0
  66. package/dist/lib/mesh/live/ulid.js +39 -0
  67. package/dist/lib/mesh/live/validate-session-event.d.ts +39 -0
  68. package/dist/lib/mesh/live/validate-session-event.js +128 -0
  69. package/dist/lib/search-index/index.d.ts +98 -0
  70. package/dist/lib/search-index/index.js +174 -2
  71. package/dist/lib/work-context/atomic.d.ts +18 -0
  72. package/dist/lib/work-context/atomic.js +132 -0
  73. package/dist/lib/work-context/company.d.ts +103 -0
  74. package/dist/lib/work-context/company.js +274 -0
  75. package/dist/lib/work-context/config.d.ts +59 -0
  76. package/dist/lib/work-context/config.js +172 -0
  77. package/dist/lib/work-context/contract.d.ts +81 -0
  78. package/dist/lib/work-context/contract.js +65 -0
  79. package/dist/lib/work-context/errors.d.ts +27 -0
  80. package/dist/lib/work-context/errors.js +46 -0
  81. package/dist/lib/work-context/index.d.ts +23 -0
  82. package/dist/lib/work-context/index.js +20 -0
  83. package/dist/lib/work-context/migrate.d.ts +84 -0
  84. package/dist/lib/work-context/migrate.js +287 -0
  85. package/dist/lib/work-context/organize.d.ts +130 -0
  86. package/dist/lib/work-context/organize.js +471 -0
  87. package/dist/lib/work-context/outbox.d.ts +97 -0
  88. package/dist/lib/work-context/outbox.js +357 -0
  89. package/dist/lib/work-context/paths.d.ts +14 -0
  90. package/dist/lib/work-context/paths.js +39 -0
  91. package/dist/lib/work-context/project.d.ts +78 -0
  92. package/dist/lib/work-context/project.js +247 -0
  93. package/dist/lib/work-context/reconcile.d.ts +85 -0
  94. package/dist/lib/work-context/reconcile.js +651 -0
  95. package/dist/lib/work-context/repo-remote.d.ts +43 -0
  96. package/dist/lib/work-context/repo-remote.js +200 -0
  97. package/dist/lib/work-context/state.d.ts +112 -0
  98. package/dist/lib/work-context/state.js +162 -0
  99. package/dist/main.js +93 -4
  100. package/dist/utils/cognito-session.d.ts +85 -9
  101. package/dist/utils/cognito-session.js +216 -9
  102. package/dist/utils/qmd-model-download-error.d.ts +27 -0
  103. package/dist/utils/qmd-model-download-error.js +114 -0
  104. package/dist/utils/qmd-query-document-error.d.ts +23 -0
  105. package/dist/utils/qmd-query-document-error.js +117 -0
  106. package/dist/utils/qmd-workdir-missing-error.d.ts +18 -0
  107. package/dist/utils/qmd-workdir-missing-error.js +78 -0
  108. package/dist/utils/sentry-fingerprint.js +7 -0
  109. package/package.json +5 -1
@@ -0,0 +1,330 @@
1
+ /**
2
+ * User-level service install for hq mesh daemon.
3
+ *
4
+ * - macOS: LaunchAgent ai.getindigo.hq-mesh-daemon
5
+ * - Linux: systemd user unit hq-mesh-daemon.service
6
+ * (documented fallback: root-owned unit running as ec2-user)
7
+ *
8
+ * The unit embeds the hq binary resolved at install time. Logs go to
9
+ * ~/.hq/work-mesh/daemon/log (rotation at 5MB handled by the daemon).
10
+ * Tests must not actually install services — use a temp HOME/prefix.
11
+ */
12
+ import * as fs from "node:fs";
13
+ import * as os from "node:os";
14
+ import * as path from "node:path";
15
+ import { resolveDaemonAssetDir } from "./log.js";
16
+ import { LAUNCHD_LABEL, SYSTEMD_UNIT_NAME, daemonDir, daemonLogPath, } from "./paths.js";
17
+ export function detectPlatform(platform = process.platform) {
18
+ if (platform === "darwin")
19
+ return "darwin";
20
+ if (platform === "linux")
21
+ return "linux";
22
+ return "other";
23
+ }
24
+ /** Resolve the hq binary the current shell uses (argv[1] or PATH). */
25
+ export function resolveHqBinary(argv = process.argv, env = process.env) {
26
+ const entry = argv[1];
27
+ if (entry && fs.existsSync(entry)) {
28
+ return path.resolve(entry);
29
+ }
30
+ const pathEnv = env.PATH ?? "";
31
+ for (const dir of pathEnv.split(path.delimiter)) {
32
+ if (!dir)
33
+ continue;
34
+ for (const name of ["hq", "hq.js", "hq.mjs"]) {
35
+ const candidate = path.join(dir, name);
36
+ if (fs.existsSync(candidate))
37
+ return candidate;
38
+ }
39
+ }
40
+ throw new Error("Cannot resolve hq binary for daemon install");
41
+ }
42
+ export function resolveNodeBinary(execPath = process.execPath, env = process.env) {
43
+ if (execPath && fs.existsSync(execPath))
44
+ return execPath;
45
+ const pathEnv = env.PATH ?? "";
46
+ for (const dir of pathEnv.split(path.delimiter)) {
47
+ const candidate = path.join(dir, "node");
48
+ if (fs.existsSync(candidate))
49
+ return candidate;
50
+ }
51
+ return "/usr/bin/node";
52
+ }
53
+ export function buildInstallPaths(opts) {
54
+ const home = opts.home ?? os.homedir();
55
+ const env = opts.env ?? process.env;
56
+ const ddir = daemonDir(opts.meshRoot, home, env);
57
+ const hqBinary = opts.hqBinary ?? resolveHqBinary(opts.argv, env);
58
+ const nodeBinary = opts.nodeBinary ?? resolveNodeBinary(opts.execPath, env);
59
+ return {
60
+ home,
61
+ hqBinary,
62
+ nodeBinary,
63
+ daemonDir: ddir,
64
+ logPath: daemonLogPath(ddir),
65
+ launchAgentsDir: path.join(home, "Library", "LaunchAgents"),
66
+ plistPath: path.join(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`),
67
+ systemdUserDir: path.join(home, ".config", "systemd", "user"),
68
+ systemdUnitPath: path.join(home, ".config", "systemd", "user", SYSTEMD_UNIT_NAME),
69
+ systemUser: opts.systemUser,
70
+ };
71
+ }
72
+ export function renderLaunchdPlist(paths) {
73
+ return `<?xml version="1.0" encoding="UTF-8"?>
74
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
75
+ <plist version="1.0">
76
+ <dict>
77
+ <key>Label</key>
78
+ <string>${LAUNCHD_LABEL}</string>
79
+ <key>ProgramArguments</key>
80
+ <array>
81
+ <string>${escapeXml(paths.nodeBinary)}</string>
82
+ <string>${escapeXml(paths.hqBinary)}</string>
83
+ <string>mesh</string>
84
+ <string>daemon</string>
85
+ <string>run</string>
86
+ </array>
87
+ <key>WorkingDirectory</key>
88
+ <string>${escapeXml(paths.daemonDir)}</string>
89
+ <key>RunAtLoad</key>
90
+ <true/>
91
+ <key>KeepAlive</key>
92
+ <true/>
93
+ <key>StandardOutPath</key>
94
+ <string>${escapeXml(paths.logPath)}</string>
95
+ <key>StandardErrorPath</key>
96
+ <string>${escapeXml(paths.logPath)}</string>
97
+ <key>EnvironmentVariables</key>
98
+ <dict>
99
+ <key>HOME</key>
100
+ <string>${escapeXml(paths.home)}</string>
101
+ <key>PATH</key>
102
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
103
+ </dict>
104
+ </dict>
105
+ </plist>
106
+ `;
107
+ }
108
+ export function renderSystemdUserUnit(paths) {
109
+ return `[Unit]
110
+ Description=HQ Work Mesh Live daemon
111
+ After=network-online.target
112
+ Wants=network-online.target
113
+
114
+ [Service]
115
+ Type=simple
116
+ ExecStart=${shellQuote(paths.nodeBinary)} ${shellQuote(paths.hqBinary)} mesh daemon run
117
+ WorkingDirectory=${paths.daemonDir}
118
+ Restart=on-failure
119
+ RestartSec=5
120
+ Environment=HOME=${paths.home}
121
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin
122
+ StandardOutput=append:${paths.logPath}
123
+ StandardError=append:${paths.logPath}
124
+
125
+ [Install]
126
+ WantedBy=default.target
127
+ `;
128
+ }
129
+ /**
130
+ * Root-owned unit running as ec2-user (agent boxes without a user session bus).
131
+ * Documented fallback — written only when --system-user is passed.
132
+ */
133
+ export function renderSystemdSystemUnit(paths) {
134
+ const user = paths.systemUser || "ec2-user";
135
+ return `[Unit]
136
+ Description=HQ Work Mesh Live daemon (system, user=${user})
137
+ After=network-online.target
138
+ Wants=network-online.target
139
+
140
+ # Fallback for agent boxes where no user session bus exists:
141
+ # install with: hq mesh daemon install --system-user ${user}
142
+ # then: sudo systemctl enable --now ${SYSTEMD_UNIT_NAME}
143
+
144
+ [Service]
145
+ Type=simple
146
+ User=${user}
147
+ Group=${user}
148
+ ExecStart=${shellQuote(paths.nodeBinary)} ${shellQuote(paths.hqBinary)} mesh daemon run
149
+ WorkingDirectory=/home/${user}/.hq/work-mesh/daemon
150
+ Restart=on-failure
151
+ RestartSec=5
152
+ Environment=HOME=/home/${user}
153
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin
154
+ Environment=HQ_MACHINE_CREDS_FILE=/home/${user}/.hq-agent/machine-creds.json
155
+ Environment=HQ_AGENT_IDENTITY_FILE=/var/lib/hq-agent/identity.json
156
+ StandardOutput=append:/home/${user}/.hq/work-mesh/daemon/log
157
+ StandardError=append:/home/${user}/.hq/work-mesh/daemon/log
158
+
159
+ [Install]
160
+ WantedBy=multi-user.target
161
+ `;
162
+ }
163
+ function escapeXml(value) {
164
+ return value
165
+ .replace(/&/g, "&amp;")
166
+ .replace(/</g, "&lt;")
167
+ .replace(/>/g, "&gt;")
168
+ .replace(/"/g, "&quot;");
169
+ }
170
+ function shellQuote(value) {
171
+ if (/^[A-Za-z0-9_./:-]+$/.test(value))
172
+ return value;
173
+ return `'${value.replace(/'/g, `'\\''`)}'`;
174
+ }
175
+ export function installDaemonService(opts) {
176
+ const platform = opts.platform ?? detectPlatform();
177
+ const paths = opts.paths ??
178
+ buildInstallPaths({ systemUser: opts.systemUser });
179
+ const writeFileSync = opts.writeFileSync ?? fs.writeFileSync;
180
+ const mkdirSync = opts.mkdirSync ?? fs.mkdirSync;
181
+ mkdirSync(paths.daemonDir, { recursive: true, mode: 0o700 });
182
+ if (platform === "darwin") {
183
+ const rendered = renderLaunchdPlist(paths);
184
+ mkdirSync(paths.launchAgentsDir, { recursive: true });
185
+ writeFileSync(paths.plistPath, rendered, { mode: 0o600 });
186
+ opts.activate?.(platform, paths.plistPath);
187
+ return {
188
+ platform,
189
+ action: "install",
190
+ paths,
191
+ rendered,
192
+ dest: paths.plistPath,
193
+ message: `Installed LaunchAgent ${LAUNCHD_LABEL} → ${paths.plistPath}`,
194
+ };
195
+ }
196
+ if (platform === "linux") {
197
+ if (opts.systemUser || paths.systemUser) {
198
+ const rendered = renderSystemdSystemUnit({
199
+ ...paths,
200
+ systemUser: opts.systemUser ?? paths.systemUser,
201
+ });
202
+ // Write under daemon dir as a template the operator can sudo-install.
203
+ const dest = path.join(paths.daemonDir, SYSTEMD_UNIT_NAME);
204
+ writeFileSync(dest, rendered, { mode: 0o644 });
205
+ return {
206
+ platform,
207
+ action: "install",
208
+ paths,
209
+ rendered,
210
+ dest,
211
+ message: `Wrote system unit template for user ${opts.systemUser ?? paths.systemUser} at ${dest}. ` +
212
+ `Install with: sudo cp ${dest} /etc/systemd/system/${SYSTEMD_UNIT_NAME} && ` +
213
+ `sudo systemctl daemon-reload && sudo systemctl enable --now ${SYSTEMD_UNIT_NAME}`,
214
+ };
215
+ }
216
+ const rendered = renderSystemdUserUnit(paths);
217
+ mkdirSync(paths.systemdUserDir, { recursive: true });
218
+ writeFileSync(paths.systemdUnitPath, rendered, { mode: 0o600 });
219
+ opts.activate?.(platform, paths.systemdUnitPath);
220
+ return {
221
+ platform,
222
+ action: "install",
223
+ paths,
224
+ rendered,
225
+ dest: paths.systemdUnitPath,
226
+ message: `Installed systemd user unit ${SYSTEMD_UNIT_NAME} → ${paths.systemdUnitPath}`,
227
+ };
228
+ }
229
+ return {
230
+ platform,
231
+ action: "install",
232
+ paths,
233
+ message: `Unsupported platform for service install; run: ${paths.nodeBinary} ${paths.hqBinary} mesh daemon run`,
234
+ };
235
+ }
236
+ export function uninstallDaemonService(opts) {
237
+ const platform = opts.platform ?? detectPlatform();
238
+ const paths = opts.paths ?? buildInstallPaths({});
239
+ const unlinkSync = opts.unlinkSync ?? fs.unlinkSync;
240
+ const existsSync = opts.existsSync ?? fs.existsSync;
241
+ if (platform === "darwin") {
242
+ if (existsSync(paths.plistPath)) {
243
+ opts.deactivate?.(platform, paths.plistPath);
244
+ unlinkSync(paths.plistPath);
245
+ }
246
+ return {
247
+ platform,
248
+ action: "uninstall",
249
+ paths,
250
+ dest: paths.plistPath,
251
+ message: `Removed LaunchAgent ${LAUNCHD_LABEL}`,
252
+ };
253
+ }
254
+ if (platform === "linux") {
255
+ if (existsSync(paths.systemdUnitPath)) {
256
+ opts.deactivate?.(platform, paths.systemdUnitPath);
257
+ unlinkSync(paths.systemdUnitPath);
258
+ }
259
+ const systemTemplate = path.join(paths.daemonDir, SYSTEMD_UNIT_NAME);
260
+ if (existsSync(systemTemplate)) {
261
+ unlinkSync(systemTemplate);
262
+ }
263
+ return {
264
+ platform,
265
+ action: "uninstall",
266
+ paths,
267
+ dest: paths.systemdUnitPath,
268
+ message: `Removed systemd unit ${SYSTEMD_UNIT_NAME}`,
269
+ };
270
+ }
271
+ return {
272
+ platform,
273
+ action: "uninstall",
274
+ paths,
275
+ message: "Nothing to uninstall on this platform",
276
+ };
277
+ }
278
+ export function daemonServiceStatus(opts) {
279
+ const platform = opts.platform ?? detectPlatform();
280
+ const paths = opts.paths ?? buildInstallPaths({});
281
+ const existsSync = opts.existsSync ?? fs.existsSync;
282
+ if (platform === "darwin") {
283
+ const installed = existsSync(paths.plistPath);
284
+ const running = installed
285
+ ? (opts.isLoaded?.(platform, paths.plistPath) ?? false)
286
+ : false;
287
+ return {
288
+ platform,
289
+ action: "status",
290
+ paths,
291
+ dest: paths.plistPath,
292
+ running,
293
+ message: installed
294
+ ? `LaunchAgent ${LAUNCHD_LABEL}: ${running ? "loaded" : "installed (not loaded)"}`
295
+ : `LaunchAgent ${LAUNCHD_LABEL}: not installed`,
296
+ };
297
+ }
298
+ if (platform === "linux") {
299
+ const installed = existsSync(paths.systemdUnitPath);
300
+ const running = installed
301
+ ? (opts.isLoaded?.(platform, paths.systemdUnitPath) ?? false)
302
+ : false;
303
+ return {
304
+ platform,
305
+ action: "status",
306
+ paths,
307
+ dest: paths.systemdUnitPath,
308
+ running,
309
+ message: installed
310
+ ? `systemd unit ${SYSTEMD_UNIT_NAME}: ${running ? "active" : "installed (inactive)"}`
311
+ : `systemd unit ${SYSTEMD_UNIT_NAME}: not installed`,
312
+ };
313
+ }
314
+ return {
315
+ platform,
316
+ action: "status",
317
+ paths,
318
+ running: false,
319
+ message: "No service manager integration on this platform",
320
+ };
321
+ }
322
+ /** Read baked template files from assets/ (optional; renderers are primary). */
323
+ export function readAssetTemplate(name) {
324
+ const dir = resolveDaemonAssetDir();
325
+ const p = path.join(dir, name);
326
+ if (!fs.existsSync(p))
327
+ return null;
328
+ return fs.readFileSync(p, "utf8");
329
+ }
330
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Daemon log under ~/.hq/work-mesh/daemon/log with rotation at 5MB.
3
+ * Never writes prompts, tokens, credentials, or message bodies.
4
+ */
5
+ export declare function ensureDaemonLog(dir: string): string;
6
+ export declare function rotateDaemonLogIfNeeded(dir: string, maxBytes?: number): void;
7
+ export declare function appendDaemonLog(dir: string, line: string): void;
8
+ export declare function daemonLogLine(level: "info" | "warn" | "error", message: string, now?: () => Date): string;
9
+ /** Resolve path to the LaunchAgent / systemd unit template assets. */
10
+ export declare function resolveDaemonAssetDir(fromModuleUrl?: string): string;
11
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Daemon log under ~/.hq/work-mesh/daemon/log with rotation at 5MB.
3
+ * Never writes prompts, tokens, credentials, or message bodies.
4
+ */
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ import { DAEMON_LOG_MAX_BYTES, daemonLogPath } from "./paths.js";
8
+ export function ensureDaemonLog(dir) {
9
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
10
+ const logPath = daemonLogPath(dir);
11
+ if (!fs.existsSync(logPath)) {
12
+ fs.writeFileSync(logPath, "", { mode: 0o600 });
13
+ }
14
+ try {
15
+ fs.chmodSync(logPath, 0o600);
16
+ }
17
+ catch {
18
+ /* best-effort */
19
+ }
20
+ return logPath;
21
+ }
22
+ export function rotateDaemonLogIfNeeded(dir, maxBytes = DAEMON_LOG_MAX_BYTES) {
23
+ const logPath = daemonLogPath(dir);
24
+ try {
25
+ if (!fs.existsSync(logPath))
26
+ return;
27
+ const size = fs.statSync(logPath).size;
28
+ if (size <= maxBytes)
29
+ return;
30
+ const rotated = `${logPath}.1`;
31
+ try {
32
+ fs.unlinkSync(rotated);
33
+ }
34
+ catch {
35
+ /* ignore */
36
+ }
37
+ fs.renameSync(logPath, rotated);
38
+ fs.writeFileSync(logPath, "", { mode: 0o600 });
39
+ }
40
+ catch {
41
+ /* best-effort */
42
+ }
43
+ }
44
+ export function appendDaemonLog(dir, line) {
45
+ rotateDaemonLogIfNeeded(dir);
46
+ const logPath = ensureDaemonLog(dir);
47
+ const text = line.endsWith("\n") ? line : `${line}\n`;
48
+ try {
49
+ fs.appendFileSync(logPath, text);
50
+ }
51
+ catch {
52
+ /* best-effort */
53
+ }
54
+ }
55
+ export function daemonLogLine(level, message, now = () => new Date()) {
56
+ // Sanitize: strip anything that looks like a token/bearer.
57
+ const safe = message
58
+ .replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer [REDACTED]")
59
+ .replace(/op:\/\/[^\s"]+/g, "op://[REDACTED]")
60
+ .slice(0, 500);
61
+ return `${now().toISOString()} ${level} ${safe}`;
62
+ }
63
+ /** Resolve path to the LaunchAgent / systemd unit template assets. */
64
+ export function resolveDaemonAssetDir(fromModuleUrl = import.meta.url) {
65
+ // dist/lib/mesh/live/daemon/*.js → repo assets/mesh-daemon
66
+ // or src/... during vitest (tsx/vite).
67
+ const here = path.dirname(new URL(fromModuleUrl).pathname);
68
+ const candidates = [
69
+ path.resolve(here, "../../../../../../assets/mesh-daemon"),
70
+ path.resolve(here, "../../../../../assets/mesh-daemon"),
71
+ path.resolve(process.cwd(), "assets/mesh-daemon"),
72
+ ];
73
+ for (const c of candidates) {
74
+ if (fs.existsSync(c))
75
+ return c;
76
+ }
77
+ return candidates[0];
78
+ }
79
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Daemon local paths under ~/.hq/work-mesh/daemon/.
3
+ * Injectable root for tests. Files are owner 0600.
4
+ */
5
+ export declare const DAEMON_DIRNAME = "daemon";
6
+ export declare const DAEMON_PID_NAME = "pid";
7
+ export declare const DAEMON_STATE_NAME = "state.json";
8
+ export declare const DAEMON_LOG_NAME = "log";
9
+ export declare const DAEMON_LOG_MAX_BYTES: number;
10
+ export declare const LAUNCHD_LABEL = "ai.getindigo.hq-mesh-daemon";
11
+ export declare const SYSTEMD_UNIT_NAME = "hq-mesh-daemon.service";
12
+ export declare function daemonDir(meshRoot?: string, home?: string, env?: NodeJS.ProcessEnv): string;
13
+ export declare function daemonPidPath(dir?: string): string;
14
+ export declare function daemonStatePath(dir?: string): string;
15
+ export declare function daemonLogPath(dir?: string): string;
16
+ /** Session board snapshot: ~/.hq/work-context/sessions/<sid>/board.md */
17
+ export declare function sessionBoardPath(workContextRoot: string, sessionId: string): string;
18
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Daemon local paths under ~/.hq/work-mesh/daemon/.
3
+ * Injectable root for tests. Files are owner 0600.
4
+ */
5
+ import * as os from "node:os";
6
+ import * as path from "node:path";
7
+ import { workMeshRoot } from "../paths.js";
8
+ export const DAEMON_DIRNAME = "daemon";
9
+ export const DAEMON_PID_NAME = "pid";
10
+ export const DAEMON_STATE_NAME = "state.json";
11
+ export const DAEMON_LOG_NAME = "log";
12
+ export const DAEMON_LOG_MAX_BYTES = 5 * 1024 * 1024;
13
+ export const LAUNCHD_LABEL = "ai.getindigo.hq-mesh-daemon";
14
+ export const SYSTEMD_UNIT_NAME = "hq-mesh-daemon.service";
15
+ export function daemonDir(meshRoot, home = os.homedir(), env = process.env) {
16
+ return path.join(meshRoot ?? workMeshRoot(home, env), DAEMON_DIRNAME);
17
+ }
18
+ export function daemonPidPath(dir) {
19
+ return path.join(dir ?? daemonDir(), DAEMON_PID_NAME);
20
+ }
21
+ export function daemonStatePath(dir) {
22
+ return path.join(dir ?? daemonDir(), DAEMON_STATE_NAME);
23
+ }
24
+ export function daemonLogPath(dir) {
25
+ return path.join(dir ?? daemonDir(), DAEMON_LOG_NAME);
26
+ }
27
+ /** Session board snapshot: ~/.hq/work-context/sessions/<sid>/board.md */
28
+ export function sessionBoardPath(workContextRoot, sessionId) {
29
+ return path.join(workContextRoot, "sessions", sessionId, "board.md");
30
+ }
31
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Single-file daemon pid lock (avoids the stale watch-lock directory bug).
3
+ *
4
+ * Format (0600):
5
+ * pid=<n>
6
+ * ts=<unix seconds>
7
+ *
8
+ * Stale takeover when the recorded pid is not alive. Never uses a lock
9
+ * directory with a temp file inside (rmdir can never clear that class).
10
+ */
11
+ import * as fs from "node:fs";
12
+ export interface PidLockRecord {
13
+ pid: number;
14
+ ts: number;
15
+ }
16
+ export type PidLockResult = {
17
+ ok: true;
18
+ path: string;
19
+ } | {
20
+ ok: false;
21
+ reason: "held";
22
+ owner: PidLockRecord;
23
+ path: string;
24
+ };
25
+ export interface PidLockDeps {
26
+ pid: number;
27
+ now: () => number;
28
+ isProcessAlive: (pid: number) => boolean;
29
+ mkdirSync?: typeof fs.mkdirSync;
30
+ writeFileSync?: typeof fs.writeFileSync;
31
+ readFileSync?: typeof fs.readFileSync;
32
+ unlinkSync?: typeof fs.unlinkSync;
33
+ existsSync?: typeof fs.existsSync;
34
+ chmodSync?: typeof fs.chmodSync;
35
+ }
36
+ export declare function parsePidLock(content: string): PidLockRecord | null;
37
+ export declare function readPidLock(lockPath: string, deps?: Pick<PidLockDeps, "readFileSync" | "existsSync">): PidLockRecord | null;
38
+ export declare function pidLockStatus(lockPath: string, deps: Pick<PidLockDeps, "isProcessAlive" | "readFileSync" | "existsSync">): "free" | "held" | "stale";
39
+ /**
40
+ * Acquire the single-file pid lock via exclusive create (`wx`).
41
+ * Stale takeover: verify recorded pid is dead, unlink, retry exclusive create.
42
+ * Never rename over a live contender.
43
+ */
44
+ export declare function acquirePidLock(dir: string, deps: PidLockDeps): PidLockResult;
45
+ export declare function releasePidLock(dir: string, deps: Pick<PidLockDeps, "pid" | "readFileSync" | "unlinkSync" | "existsSync">): void;
46
+ export declare function defaultPidLockDeps(overrides?: Partial<PidLockDeps>): PidLockDeps;
47
+ export declare function resolveDaemonDir(meshRoot?: string, home?: string, env?: NodeJS.ProcessEnv): string;
48
+ //# sourceMappingURL=pid-lock.d.ts.map
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Single-file daemon pid lock (avoids the stale watch-lock directory bug).
3
+ *
4
+ * Format (0600):
5
+ * pid=<n>
6
+ * ts=<unix seconds>
7
+ *
8
+ * Stale takeover when the recorded pid is not alive. Never uses a lock
9
+ * directory with a temp file inside (rmdir can never clear that class).
10
+ */
11
+ import * as fs from "node:fs";
12
+ import { daemonDir, daemonPidPath } from "./paths.js";
13
+ function defaultAlive(pid) {
14
+ try {
15
+ process.kill(pid, 0);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ export function parsePidLock(content) {
23
+ const fields = {};
24
+ for (const line of content.split("\n")) {
25
+ const i = line.indexOf("=");
26
+ if (i === -1)
27
+ continue;
28
+ fields[line.slice(0, i)] = line.slice(i + 1);
29
+ }
30
+ if (!/^\d+$/.test(fields.pid ?? "") || !/^\d+$/.test(fields.ts ?? "")) {
31
+ return null;
32
+ }
33
+ return { pid: Number(fields.pid), ts: Number(fields.ts) };
34
+ }
35
+ export function readPidLock(lockPath, deps = {}) {
36
+ const existsSync = deps.existsSync ?? fs.existsSync;
37
+ const readFileSync = deps.readFileSync ?? fs.readFileSync;
38
+ if (!existsSync(lockPath))
39
+ return null;
40
+ try {
41
+ return parsePidLock(readFileSync(lockPath, "utf8"));
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ export function pidLockStatus(lockPath, deps) {
48
+ const record = readPidLock(lockPath, deps);
49
+ if (!record)
50
+ return "free";
51
+ return deps.isProcessAlive(record.pid) ? "held" : "stale";
52
+ }
53
+ /** Bounded stale-takeover retries (exclusive create after unlink). */
54
+ const STALE_TAKEOVER_ATTEMPTS = 3;
55
+ /**
56
+ * Acquire the single-file pid lock via exclusive create (`wx`).
57
+ * Stale takeover: verify recorded pid is dead, unlink, retry exclusive create.
58
+ * Never rename over a live contender.
59
+ */
60
+ export function acquirePidLock(dir, deps) {
61
+ const mkdirSync = deps.mkdirSync ?? fs.mkdirSync;
62
+ const writeFileSync = deps.writeFileSync ?? fs.writeFileSync;
63
+ const unlinkSync = deps.unlinkSync ?? fs.unlinkSync;
64
+ const existsSync = deps.existsSync ?? fs.existsSync;
65
+ const chmodSync = deps.chmodSync ?? fs.chmodSync;
66
+ const readFileSync = deps.readFileSync ?? fs.readFileSync;
67
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
68
+ const lockPath = daemonPidPath(dir);
69
+ const body = `pid=${deps.pid}\nts=${deps.now()}\n`;
70
+ for (let attempt = 0; attempt < STALE_TAKEOVER_ATTEMPTS; attempt += 1) {
71
+ try {
72
+ writeFileSync(lockPath, body, { flag: "wx", mode: 0o600 });
73
+ try {
74
+ chmodSync(lockPath, 0o600);
75
+ }
76
+ catch {
77
+ /* best-effort */
78
+ }
79
+ return { ok: true, path: lockPath };
80
+ }
81
+ catch (err) {
82
+ const code = err?.code;
83
+ if (code !== "EEXIST")
84
+ throw err;
85
+ const owner = readPidLock(lockPath, { readFileSync, existsSync });
86
+ if (owner && deps.isProcessAlive(owner.pid)) {
87
+ return { ok: false, reason: "held", owner, path: lockPath };
88
+ }
89
+ // Stale or unreadable: unlink and retry exclusive create.
90
+ try {
91
+ unlinkSync(lockPath);
92
+ }
93
+ catch (unlinkErr) {
94
+ const ucode = unlinkErr?.code;
95
+ if (ucode !== "ENOENT") {
96
+ const raced = readPidLock(lockPath, { readFileSync, existsSync });
97
+ if (raced && deps.isProcessAlive(raced.pid)) {
98
+ return { ok: false, reason: "held", owner: raced, path: lockPath };
99
+ }
100
+ throw unlinkErr;
101
+ }
102
+ }
103
+ }
104
+ }
105
+ const finalOwner = readPidLock(lockPath, { readFileSync, existsSync });
106
+ if (finalOwner && deps.isProcessAlive(finalOwner.pid)) {
107
+ return { ok: false, reason: "held", owner: finalOwner, path: lockPath };
108
+ }
109
+ throw new Error(`pid lock acquire failed after ${STALE_TAKEOVER_ATTEMPTS} attempts`);
110
+ }
111
+ export function releasePidLock(dir, deps) {
112
+ const lockPath = daemonPidPath(dir);
113
+ const record = readPidLock(lockPath, deps);
114
+ if (!record || record.pid !== deps.pid)
115
+ return;
116
+ try {
117
+ (deps.unlinkSync ?? fs.unlinkSync)(lockPath);
118
+ }
119
+ catch {
120
+ /* ignore */
121
+ }
122
+ }
123
+ export function defaultPidLockDeps(overrides = {}) {
124
+ return {
125
+ pid: process.pid,
126
+ now: () => Math.floor(Date.now() / 1000),
127
+ isProcessAlive: defaultAlive,
128
+ ...overrides,
129
+ };
130
+ }
131
+ export function resolveDaemonDir(meshRoot, home, env) {
132
+ return daemonDir(meshRoot, home, env);
133
+ }
134
+ //# sourceMappingURL=pid-lock.js.map