@hivemind-os/collective-shim 0.2.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.
@@ -0,0 +1,14 @@
1
+ $ tsup
2
+ CLI Building entry: src/index.ts
3
+ CLI Using tsconfig: tsconfig.json
4
+ CLI tsup v8.5.1
5
+ CLI Using tsup config: /home/runner/work/collective/collective/packages/shim/tsup.config.ts
6
+ CLI Target: es2022
7
+ CLI Cleaning output folder
8
+ ESM Build start
9
+ ESM dist/index.js 10.91 KB
10
+ ESM dist/index.js.map 21.04 KB
11
+ ESM ⚡️ Build success in 22ms
12
+ DTS Build start
13
+ DTS ⚡️ Build success in 1711ms
14
+ DTS dist/index.d.ts 20.00 B
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bridge.ts
4
+ import { execFile } from "child_process";
5
+ import { basename } from "path";
6
+ import { setTimeout as delay2 } from "timers/promises";
7
+ import { promisify } from "util";
8
+
9
+ // src/daemon-launcher.ts
10
+ import { spawn, spawnSync } from "child_process";
11
+ import { createRequire } from "module";
12
+ import net from "net";
13
+ import { homedir, userInfo } from "os";
14
+ import { resolve } from "path";
15
+ import { setTimeout as delay } from "timers/promises";
16
+ import { fileURLToPath } from "url";
17
+ var require2 = createRequire(import.meta.url);
18
+ var LEGACY_WINDOWS_PIPE_PATH = "\\\\.\\pipe\\hivemind-collective";
19
+ function getDefaultIpcPath() {
20
+ return process.env.COLLECTIVE_IPC_PATH ?? (process.platform === "win32" ? `${LEGACY_WINDOWS_PIPE_PATH}-${sanitizePipeSegment(getCurrentUsername())}` : resolve(homedir(), ".hivemind-os/collective", "mesh.sock"));
21
+ }
22
+ function getDefaultPidFile() {
23
+ return process.env.COLLECTIVE_PID_FILE ?? resolve(homedir(), ".hivemind-os/collective", "daemon.pid");
24
+ }
25
+ function resolveDaemonBin() {
26
+ if (process.env.COLLECTIVE_DAEMON_BIN) {
27
+ return process.env.COLLECTIVE_DAEMON_BIN;
28
+ }
29
+ try {
30
+ return require2.resolve("@hivemind-os/collective-daemon");
31
+ } catch {
32
+ if (commandExists("mesh-daemon")) {
33
+ return "mesh-daemon";
34
+ }
35
+ return fileURLToPath(new URL("../../daemon/dist/index.js", import.meta.url));
36
+ }
37
+ }
38
+ async function isDaemonRunning(ipcPath) {
39
+ return new Promise((resolvePromise) => {
40
+ const socket = net.connect(ipcPath);
41
+ const finish = (running) => {
42
+ socket.removeAllListeners();
43
+ socket.destroy();
44
+ resolvePromise(running);
45
+ };
46
+ socket.once("connect", () => {
47
+ finish(true);
48
+ });
49
+ socket.once("error", () => {
50
+ finish(false);
51
+ });
52
+ });
53
+ }
54
+ async function ensureDaemonRunning(options) {
55
+ if (await isDaemonRunning(options.ipcPath)) {
56
+ return;
57
+ }
58
+ const command = options.daemonBin.endsWith(".js") ? process.execPath : options.daemonBin;
59
+ const args = options.daemonBin.endsWith(".js") ? [options.daemonBin] : [];
60
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
61
+ child.unref();
62
+ const deadline = Date.now() + options.startupTimeoutMs;
63
+ while (Date.now() < deadline) {
64
+ await delay(200);
65
+ if (await isDaemonRunning(options.ipcPath)) {
66
+ return;
67
+ }
68
+ }
69
+ throw new Error(`Timed out waiting for daemon IPC at ${options.ipcPath} (pid file: ${options.pidFile})`);
70
+ }
71
+ function commandExists(command) {
72
+ const probe = process.platform === "win32" ? "where" : "which";
73
+ return spawnSync(probe, [command], { stdio: "ignore" }).status === 0;
74
+ }
75
+ function getCurrentUsername() {
76
+ try {
77
+ return userInfo().username;
78
+ } catch {
79
+ return process.env.USERNAME ?? process.env.USER ?? "unknown-user";
80
+ }
81
+ }
82
+ function sanitizePipeSegment(value) {
83
+ const leaf = value.split(/[\\/]+/).filter(Boolean).at(-1) ?? value;
84
+ const sanitized = leaf.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
85
+ return sanitized || "unknown-user";
86
+ }
87
+
88
+ // src/ipc-client.ts
89
+ import { randomUUID } from "crypto";
90
+ import net2 from "net";
91
+ var IpcClient = class {
92
+ constructor(ipcPath) {
93
+ this.ipcPath = ipcPath;
94
+ }
95
+ ipcPath;
96
+ socket;
97
+ buffer = "";
98
+ messageHandlers = /* @__PURE__ */ new Set();
99
+ closeHandlers = /* @__PURE__ */ new Set();
100
+ helloId;
101
+ helloWaiter;
102
+ async connect(appName = "unknown") {
103
+ this.close();
104
+ const socket = await new Promise((resolve2, reject) => {
105
+ const client = net2.connect(this.ipcPath, () => {
106
+ client.off("error", reject);
107
+ resolve2(client);
108
+ });
109
+ client.once("error", reject);
110
+ });
111
+ this.socket = socket;
112
+ socket.setEncoding("utf8");
113
+ socket.setNoDelay(true);
114
+ socket.on("data", (chunk) => {
115
+ this.buffer += chunk.toString();
116
+ this.drainBuffer();
117
+ });
118
+ socket.on("close", () => {
119
+ this.handleClose();
120
+ });
121
+ socket.on("end", () => {
122
+ this.handleClose();
123
+ });
124
+ socket.on("error", () => void 0);
125
+ this.helloId = `shim-hello-${randomUUID()}`;
126
+ await new Promise((resolve2, reject) => {
127
+ this.helloWaiter = { resolve: resolve2, reject };
128
+ this.send({
129
+ jsonrpc: "2.0",
130
+ id: this.helloId,
131
+ method: "shim_hello",
132
+ params: {
133
+ appName,
134
+ pid: process.pid,
135
+ ...process.env.COLLECTIVE_PROFILE ? { profile: process.env.COLLECTIVE_PROFILE } : {}
136
+ }
137
+ });
138
+ });
139
+ }
140
+ send(message) {
141
+ this.sendRaw(JSON.stringify(message));
142
+ }
143
+ sendRaw(line) {
144
+ const socket = this.socket;
145
+ if (!socket || socket.destroyed) {
146
+ throw new Error("IPC client is not connected");
147
+ }
148
+ socket.write(`${line.trimEnd()}
149
+ `);
150
+ }
151
+ onMessage(handler) {
152
+ this.messageHandlers.add(handler);
153
+ }
154
+ onClose(handler) {
155
+ this.closeHandlers.add(handler);
156
+ }
157
+ close() {
158
+ const socket = this.socket;
159
+ this.socket = void 0;
160
+ this.buffer = "";
161
+ this.rejectHello(new Error("IPC connection closed"));
162
+ if (socket && !socket.destroyed) {
163
+ socket.destroy();
164
+ }
165
+ }
166
+ drainBuffer() {
167
+ let newlineIndex = this.buffer.indexOf("\n");
168
+ while (newlineIndex >= 0) {
169
+ const line = this.buffer.slice(0, newlineIndex).trim();
170
+ this.buffer = this.buffer.slice(newlineIndex + 1);
171
+ if (line) {
172
+ const message = JSON.parse(line);
173
+ if (this.helloWaiter && message.id === this.helloId) {
174
+ const waiter = this.helloWaiter;
175
+ this.helloWaiter = void 0;
176
+ this.helloId = void 0;
177
+ if (message.error && typeof message.error === "object") {
178
+ waiter.reject(new Error(String(message.error.message ?? "shim_hello failed")));
179
+ } else {
180
+ waiter.resolve();
181
+ }
182
+ } else {
183
+ for (const handler of this.messageHandlers) {
184
+ handler(message);
185
+ }
186
+ }
187
+ }
188
+ newlineIndex = this.buffer.indexOf("\n");
189
+ }
190
+ }
191
+ handleClose() {
192
+ if (!this.socket && !this.helloWaiter) {
193
+ return;
194
+ }
195
+ this.socket = void 0;
196
+ this.buffer = "";
197
+ this.rejectHello(new Error("IPC connection closed"));
198
+ for (const handler of this.closeHandlers) {
199
+ handler();
200
+ }
201
+ }
202
+ rejectHello(error) {
203
+ if (!this.helloWaiter) {
204
+ return;
205
+ }
206
+ const waiter = this.helloWaiter;
207
+ this.helloWaiter = void 0;
208
+ this.helloId = void 0;
209
+ waiter.reject(error);
210
+ }
211
+ };
212
+
213
+ // src/bridge.ts
214
+ var execFileAsync = promisify(execFile);
215
+ async function startShim() {
216
+ await createBridge();
217
+ }
218
+ async function createBridge(options = {}) {
219
+ const stdin = options.stdin ?? process.stdin;
220
+ const stdout = options.stdout ?? process.stdout;
221
+ const stderr = options.stderr ?? process.stderr;
222
+ const exit = options.exit ?? ((code) => process.exit(code));
223
+ const ensureDaemon = options.ensureDaemon ?? ensureDaemonRunning;
224
+ const launcherOptions = {
225
+ ipcPath: options.ipcPath ?? getDefaultIpcPath(),
226
+ pidFile: options.pidFile ?? getDefaultPidFile(),
227
+ daemonBin: options.daemonBin ?? resolveDaemonBin(),
228
+ startupTimeoutMs: options.startupTimeoutMs ?? 5e3
229
+ };
230
+ const appName = options.appName ?? await guessAppName();
231
+ let client;
232
+ let stdinBuffer = "";
233
+ let closing = false;
234
+ let reconnecting;
235
+ const pending = [];
236
+ const writeError = (message) => {
237
+ stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32e3, message } })}
238
+ `);
239
+ stderr.write(`mesh-shim: ${message}
240
+ `);
241
+ };
242
+ const flushPending = () => {
243
+ if (!client) {
244
+ return;
245
+ }
246
+ while (pending.length > 0) {
247
+ try {
248
+ client.sendRaw(pending[0]);
249
+ pending.shift();
250
+ } catch {
251
+ return;
252
+ }
253
+ }
254
+ };
255
+ const connect = async () => {
256
+ await ensureDaemon(launcherOptions);
257
+ const deadline = Date.now() + launcherOptions.startupTimeoutMs;
258
+ while (true) {
259
+ const next = new IpcClient(launcherOptions.ipcPath);
260
+ next.onMessage((message) => {
261
+ stdout.write(`${JSON.stringify(message)}
262
+ `);
263
+ });
264
+ next.onClose(() => {
265
+ if (!closing && client === next) {
266
+ client = void 0;
267
+ void reconnect();
268
+ }
269
+ });
270
+ try {
271
+ await next.connect(appName);
272
+ client = next;
273
+ flushPending();
274
+ return;
275
+ } catch (error) {
276
+ next.close();
277
+ if (Date.now() >= deadline) {
278
+ throw error;
279
+ }
280
+ await delay2(200);
281
+ }
282
+ }
283
+ };
284
+ const reconnect = async () => {
285
+ if (reconnecting) {
286
+ return reconnecting;
287
+ }
288
+ reconnecting = (async () => {
289
+ try {
290
+ await connect();
291
+ } catch (error) {
292
+ if (!closing) {
293
+ closing = true;
294
+ client?.close();
295
+ writeError(`Unable to reach mesh daemon: ${error.message}`);
296
+ exit(1);
297
+ }
298
+ } finally {
299
+ reconnecting = void 0;
300
+ }
301
+ })();
302
+ return reconnecting;
303
+ };
304
+ try {
305
+ await connect();
306
+ } catch (error) {
307
+ closing = true;
308
+ writeError(`Unable to start mesh daemon: ${error.message}`);
309
+ exit(1);
310
+ return { close: () => void 0 };
311
+ }
312
+ if ("setEncoding" in stdin && typeof stdin.setEncoding === "function") {
313
+ stdin.setEncoding("utf8");
314
+ }
315
+ const handleChunk = (chunk) => {
316
+ stdinBuffer += chunk.toString();
317
+ let newlineIndex = stdinBuffer.indexOf("\n");
318
+ while (newlineIndex >= 0) {
319
+ const line = stdinBuffer.slice(0, newlineIndex).trim();
320
+ stdinBuffer = stdinBuffer.slice(newlineIndex + 1);
321
+ if (line) {
322
+ if (client) {
323
+ try {
324
+ client.sendRaw(line);
325
+ } catch {
326
+ pending.push(line);
327
+ void reconnect();
328
+ }
329
+ } else {
330
+ pending.push(line);
331
+ void reconnect();
332
+ }
333
+ }
334
+ newlineIndex = stdinBuffer.indexOf("\n");
335
+ }
336
+ };
337
+ const shutdown = () => {
338
+ if (closing) {
339
+ return;
340
+ }
341
+ closing = true;
342
+ stdin.off("data", handleChunk);
343
+ stdin.off("end", shutdown);
344
+ stdin.off("close", shutdown);
345
+ client?.close();
346
+ exit(0);
347
+ };
348
+ stdin.on("data", handleChunk);
349
+ stdin.on("end", shutdown);
350
+ stdin.on("close", shutdown);
351
+ if ("resume" in stdin && typeof stdin.resume === "function") {
352
+ stdin.resume();
353
+ }
354
+ return { close: shutdown };
355
+ }
356
+ async function guessAppName() {
357
+ if (process.env.COLLECTIVE_APP_NAME) {
358
+ return process.env.COLLECTIVE_APP_NAME;
359
+ }
360
+ const arg = process.argv.map((value) => basename(value).toLowerCase()).find((value) => value && !["node", "node.exe", "mesh-shim", "index.js"].includes(value));
361
+ if (arg) {
362
+ return arg.replace(/\.(cmd|exe)$/i, "");
363
+ }
364
+ try {
365
+ const result = process.platform === "win32" ? await execFileAsync(
366
+ "powershell",
367
+ ["-NoProfile", "-Command", `(Get-CimInstance Win32_Process -Filter "ProcessId = ${process.ppid}").Name`],
368
+ { windowsHide: true }
369
+ ) : await execFileAsync("ps", ["-o", "comm=", "-p", String(process.ppid)]);
370
+ const name = basename(result.stdout.trim()).replace(/\.(cmd|exe)$/i, "").toLowerCase();
371
+ return name || "unknown";
372
+ } catch {
373
+ return "unknown";
374
+ }
375
+ }
376
+
377
+ // src/index.ts
378
+ startShim().catch((err) => {
379
+ process.stderr.write(`mesh-shim fatal: ${err.message}
380
+ `);
381
+ process.exit(1);
382
+ });
383
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/bridge.ts","../src/daemon-launcher.ts","../src/ipc-client.ts","../src/index.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport { basename } from 'node:path';\nimport { setTimeout as delay } from 'node:timers/promises';\nimport { promisify } from 'node:util';\n\nimport {\n ensureDaemonRunning,\n getDefaultIpcPath,\n getDefaultPidFile,\n resolveDaemonBin,\n type LauncherOptions,\n} from './daemon-launcher.js';\nimport { IpcClient } from './ipc-client.js';\n\nconst execFileAsync = promisify(execFile);\n\nexport interface BridgeOptions {\n ipcPath?: string;\n pidFile?: string;\n daemonBin?: string;\n startupTimeoutMs?: number;\n appName?: string;\n stdin?: NodeJS.ReadableStream;\n stdout?: NodeJS.WritableStream;\n stderr?: NodeJS.WritableStream;\n exit?: (code: number) => void;\n ensureDaemon?: (options: LauncherOptions) => Promise<void>;\n}\n\nexport interface BridgeHandle {\n close(): void;\n}\n\nexport async function startShim(): Promise<void> {\n await createBridge();\n}\n\nexport async function createBridge(options: BridgeOptions = {}): Promise<BridgeHandle> {\n const stdin = options.stdin ?? process.stdin;\n const stdout = options.stdout ?? process.stdout;\n const stderr = options.stderr ?? process.stderr;\n const exit = options.exit ?? ((code: number) => process.exit(code));\n const ensureDaemon = options.ensureDaemon ?? ensureDaemonRunning;\n const launcherOptions: LauncherOptions = {\n ipcPath: options.ipcPath ?? getDefaultIpcPath(),\n pidFile: options.pidFile ?? getDefaultPidFile(),\n daemonBin: options.daemonBin ?? resolveDaemonBin(),\n startupTimeoutMs: options.startupTimeoutMs ?? 5_000,\n };\n const appName = options.appName ?? (await guessAppName());\n\n let client: IpcClient | undefined;\n let stdinBuffer = '';\n let closing = false;\n let reconnecting: Promise<void> | undefined;\n const pending: string[] = [];\n\n const writeError = (message: string) => {\n stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32000, message } })}\\n`);\n stderr.write(`mesh-shim: ${message}\\n`);\n };\n\n const flushPending = () => {\n if (!client) {\n return;\n }\n\n while (pending.length > 0) {\n try {\n client.sendRaw(pending[0] as string);\n pending.shift();\n } catch {\n return;\n }\n }\n };\n\n const connect = async () => {\n await ensureDaemon(launcherOptions);\n const deadline = Date.now() + launcherOptions.startupTimeoutMs;\n while (true) {\n const next = new IpcClient(launcherOptions.ipcPath);\n next.onMessage((message) => {\n stdout.write(`${JSON.stringify(message)}\\n`);\n });\n next.onClose(() => {\n if (!closing && client === next) {\n client = undefined;\n void reconnect();\n }\n });\n\n try {\n await next.connect(appName);\n client = next;\n flushPending();\n return;\n } catch (error) {\n next.close();\n if (Date.now() >= deadline) {\n throw error;\n }\n await delay(200);\n }\n }\n };\n\n const reconnect = async () => {\n if (reconnecting) {\n return reconnecting;\n }\n\n reconnecting = (async () => {\n try {\n await connect();\n } catch (error) {\n if (!closing) {\n closing = true;\n client?.close();\n writeError(`Unable to reach mesh daemon: ${(error as Error).message}`);\n exit(1);\n }\n } finally {\n reconnecting = undefined;\n }\n })();\n\n return reconnecting;\n };\n\n try {\n await connect();\n } catch (error) {\n closing = true;\n writeError(`Unable to start mesh daemon: ${(error as Error).message}`);\n exit(1);\n return { close: () => undefined };\n }\n\n if ('setEncoding' in stdin && typeof stdin.setEncoding === 'function') {\n stdin.setEncoding('utf8');\n }\n\n const handleChunk = (chunk: string | Buffer) => {\n stdinBuffer += chunk.toString();\n let newlineIndex = stdinBuffer.indexOf('\\n');\n while (newlineIndex >= 0) {\n const line = stdinBuffer.slice(0, newlineIndex).trim();\n stdinBuffer = stdinBuffer.slice(newlineIndex + 1);\n if (line) {\n if (client) {\n try {\n client.sendRaw(line);\n } catch {\n pending.push(line);\n void reconnect();\n }\n } else {\n pending.push(line);\n void reconnect();\n }\n }\n newlineIndex = stdinBuffer.indexOf('\\n');\n }\n };\n\n const shutdown = () => {\n if (closing) {\n return;\n }\n\n closing = true;\n stdin.off('data', handleChunk);\n stdin.off('end', shutdown);\n stdin.off('close', shutdown);\n client?.close();\n exit(0);\n };\n\n stdin.on('data', handleChunk);\n stdin.on('end', shutdown);\n stdin.on('close', shutdown);\n if ('resume' in stdin && typeof stdin.resume === 'function') {\n stdin.resume();\n }\n\n return { close: shutdown };\n}\n\nasync function guessAppName(): Promise<string> {\n if (process.env.COLLECTIVE_APP_NAME) {\n return process.env.COLLECTIVE_APP_NAME;\n }\n\n const arg = process.argv\n .map((value) => basename(value).toLowerCase())\n .find((value) => value && !['node', 'node.exe', 'mesh-shim', 'index.js'].includes(value));\n if (arg) {\n return arg.replace(/\\.(cmd|exe)$/i, '');\n }\n\n try {\n const result =\n process.platform === 'win32'\n ? await execFileAsync(\n 'powershell',\n ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter \"ProcessId = ${process.ppid}\").Name`],\n { windowsHide: true },\n )\n : await execFileAsync('ps', ['-o', 'comm=', '-p', String(process.ppid)]);\n const name = basename(result.stdout.trim()).replace(/\\.(cmd|exe)$/i, '').toLowerCase();\n return name || 'unknown';\n } catch {\n return 'unknown';\n }\n}\n","import { spawn, spawnSync } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport net from 'node:net';\nimport { homedir, userInfo } from 'node:os';\nimport { resolve } from 'node:path';\nimport { setTimeout as delay } from 'node:timers/promises';\nimport { fileURLToPath } from 'node:url';\n\nconst require = createRequire(import.meta.url);\n\nexport interface LauncherOptions {\n ipcPath: string;\n pidFile: string;\n daemonBin: string;\n startupTimeoutMs: number;\n}\n\nconst LEGACY_WINDOWS_PIPE_PATH = '\\\\\\\\.\\\\pipe\\\\hivemind-collective';\n\nexport function getDefaultIpcPath(): string {\n return process.env.COLLECTIVE_IPC_PATH ??\n (process.platform === 'win32'\n ? `${LEGACY_WINDOWS_PIPE_PATH}-${sanitizePipeSegment(getCurrentUsername())}`\n : resolve(homedir(), '.hivemind-os/collective', 'mesh.sock'));\n}\n\nexport function getDefaultPidFile(): string {\n return process.env.COLLECTIVE_PID_FILE ?? resolve(homedir(), '.hivemind-os/collective', 'daemon.pid');\n}\n\nexport function resolveDaemonBin(): string {\n if (process.env.COLLECTIVE_DAEMON_BIN) {\n return process.env.COLLECTIVE_DAEMON_BIN;\n }\n\n try {\n return require.resolve('@hivemind-os/collective-daemon');\n } catch {\n if (commandExists('mesh-daemon')) {\n return 'mesh-daemon';\n }\n\n return fileURLToPath(new URL('../../daemon/dist/index.js', import.meta.url));\n }\n}\n\nexport async function isDaemonRunning(ipcPath: string): Promise<boolean> {\n return new Promise((resolvePromise) => {\n const socket = net.connect(ipcPath);\n const finish = (running: boolean) => {\n socket.removeAllListeners();\n socket.destroy();\n resolvePromise(running);\n };\n\n socket.once('connect', () => {\n finish(true);\n });\n socket.once('error', () => {\n finish(false);\n });\n });\n}\n\nexport async function ensureDaemonRunning(options: LauncherOptions): Promise<void> {\n if (await isDaemonRunning(options.ipcPath)) {\n return;\n }\n\n const command = options.daemonBin.endsWith('.js') ? process.execPath : options.daemonBin;\n const args = options.daemonBin.endsWith('.js') ? [options.daemonBin] : [];\n const child = spawn(command, args, { detached: true, stdio: 'ignore' });\n child.unref();\n\n const deadline = Date.now() + options.startupTimeoutMs;\n while (Date.now() < deadline) {\n await delay(200);\n if (await isDaemonRunning(options.ipcPath)) {\n return;\n }\n }\n\n throw new Error(`Timed out waiting for daemon IPC at ${options.ipcPath} (pid file: ${options.pidFile})`);\n}\n\nfunction commandExists(command: string): boolean {\n const probe = process.platform === 'win32' ? 'where' : 'which';\n return spawnSync(probe, [command], { stdio: 'ignore' }).status === 0;\n}\n\nfunction getCurrentUsername(): string {\n try {\n return userInfo().username;\n } catch {\n return process.env.USERNAME ?? process.env.USER ?? 'unknown-user';\n }\n}\n\nfunction sanitizePipeSegment(value: string): string {\n const leaf = value.split(/[\\\\/]+/).filter(Boolean).at(-1) ?? value;\n const sanitized = leaf.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');\n return sanitized || 'unknown-user';\n}\n","import { randomUUID } from 'node:crypto';\nimport net from 'node:net';\n\ntype MessageHandler = (message: object) => void;\ntype CloseHandler = () => void;\n\nexport class IpcClient {\n private socket?: net.Socket;\n private buffer = '';\n private readonly messageHandlers = new Set<MessageHandler>();\n private readonly closeHandlers = new Set<CloseHandler>();\n private helloId?: string;\n private helloWaiter?: { resolve: () => void; reject: (error: Error) => void };\n\n constructor(private readonly ipcPath: string) {}\n\n async connect(appName = 'unknown'): Promise<void> {\n this.close();\n const socket = await new Promise<net.Socket>((resolve, reject) => {\n const client = net.connect(this.ipcPath, () => {\n client.off('error', reject);\n resolve(client);\n });\n client.once('error', reject);\n });\n\n this.socket = socket;\n socket.setEncoding('utf8');\n socket.setNoDelay(true);\n socket.on('data', (chunk: string | Buffer) => {\n this.buffer += chunk.toString();\n this.drainBuffer();\n });\n socket.on('close', () => {\n this.handleClose();\n });\n socket.on('end', () => {\n this.handleClose();\n });\n socket.on('error', () => undefined);\n\n this.helloId = `shim-hello-${randomUUID()}`;\n await new Promise<void>((resolve, reject) => {\n this.helloWaiter = { resolve, reject };\n this.send({\n jsonrpc: '2.0',\n id: this.helloId as string,\n method: 'shim_hello',\n params: {\n appName,\n pid: process.pid,\n ...(process.env.COLLECTIVE_PROFILE ? { profile: process.env.COLLECTIVE_PROFILE } : {}),\n },\n });\n });\n }\n\n send(message: object): void {\n this.sendRaw(JSON.stringify(message));\n }\n\n sendRaw(line: string): void {\n const socket = this.socket;\n if (!socket || socket.destroyed) {\n throw new Error('IPC client is not connected');\n }\n\n socket.write(`${line.trimEnd()}\\n`);\n }\n\n onMessage(handler: (message: object) => void): void {\n this.messageHandlers.add(handler);\n }\n\n onClose(handler: () => void): void {\n this.closeHandlers.add(handler);\n }\n\n close(): void {\n const socket = this.socket;\n this.socket = undefined;\n this.buffer = '';\n this.rejectHello(new Error('IPC connection closed'));\n if (socket && !socket.destroyed) {\n socket.destroy();\n }\n }\n\n private drainBuffer(): void {\n let newlineIndex = this.buffer.indexOf('\\n');\n while (newlineIndex >= 0) {\n const line = this.buffer.slice(0, newlineIndex).trim();\n this.buffer = this.buffer.slice(newlineIndex + 1);\n if (line) {\n const message = JSON.parse(line) as Record<string, unknown>;\n if (this.helloWaiter && message.id === this.helloId) {\n const waiter = this.helloWaiter;\n this.helloWaiter = undefined;\n this.helloId = undefined;\n if (message.error && typeof message.error === 'object') {\n waiter.reject(new Error(String((message.error as { message?: unknown }).message ?? 'shim_hello failed')));\n } else {\n waiter.resolve();\n }\n } else {\n for (const handler of this.messageHandlers) {\n handler(message);\n }\n }\n }\n newlineIndex = this.buffer.indexOf('\\n');\n }\n }\n\n private handleClose(): void {\n if (!this.socket && !this.helloWaiter) {\n return;\n }\n\n this.socket = undefined;\n this.buffer = '';\n this.rejectHello(new Error('IPC connection closed'));\n for (const handler of this.closeHandlers) {\n handler();\n }\n }\n\n private rejectHello(error: Error): void {\n if (!this.helloWaiter) {\n return;\n }\n\n const waiter = this.helloWaiter;\n this.helloWaiter = undefined;\n this.helloId = undefined;\n waiter.reject(error);\n }\n}\n","#!/usr/bin/env node\n\nimport { startShim } from './bridge.js';\n\nstartShim().catch((err) => {\n process.stderr.write(`mesh-shim fatal: ${err.message}\\n`);\n process.exit(1);\n});\n"],"mappings":"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","names":["delay","require","net","resolve","delay"]}
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@hivemind-os/collective-shim",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "bin": {
8
+ "collective-shim": "./dist/index.js"
9
+ },
10
+ "dependencies": {
11
+ "pino": "^9.0.0"
12
+ },
13
+ "devDependencies": {
14
+ "vitest": "^3.0.0",
15
+ "tsup": "^8.0.0",
16
+ "typescript": "^5.7.0"
17
+ },
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "test": "vitest",
21
+ "lint": "eslint src/"
22
+ }
23
+ }
package/src/bridge.ts ADDED
@@ -0,0 +1,216 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { basename } from 'node:path';
3
+ import { setTimeout as delay } from 'node:timers/promises';
4
+ import { promisify } from 'node:util';
5
+
6
+ import {
7
+ ensureDaemonRunning,
8
+ getDefaultIpcPath,
9
+ getDefaultPidFile,
10
+ resolveDaemonBin,
11
+ type LauncherOptions,
12
+ } from './daemon-launcher.js';
13
+ import { IpcClient } from './ipc-client.js';
14
+
15
+ const execFileAsync = promisify(execFile);
16
+
17
+ export interface BridgeOptions {
18
+ ipcPath?: string;
19
+ pidFile?: string;
20
+ daemonBin?: string;
21
+ startupTimeoutMs?: number;
22
+ appName?: string;
23
+ stdin?: NodeJS.ReadableStream;
24
+ stdout?: NodeJS.WritableStream;
25
+ stderr?: NodeJS.WritableStream;
26
+ exit?: (code: number) => void;
27
+ ensureDaemon?: (options: LauncherOptions) => Promise<void>;
28
+ }
29
+
30
+ export interface BridgeHandle {
31
+ close(): void;
32
+ }
33
+
34
+ export async function startShim(): Promise<void> {
35
+ await createBridge();
36
+ }
37
+
38
+ export async function createBridge(options: BridgeOptions = {}): Promise<BridgeHandle> {
39
+ const stdin = options.stdin ?? process.stdin;
40
+ const stdout = options.stdout ?? process.stdout;
41
+ const stderr = options.stderr ?? process.stderr;
42
+ const exit = options.exit ?? ((code: number) => process.exit(code));
43
+ const ensureDaemon = options.ensureDaemon ?? ensureDaemonRunning;
44
+ const launcherOptions: LauncherOptions = {
45
+ ipcPath: options.ipcPath ?? getDefaultIpcPath(),
46
+ pidFile: options.pidFile ?? getDefaultPidFile(),
47
+ daemonBin: options.daemonBin ?? resolveDaemonBin(),
48
+ startupTimeoutMs: options.startupTimeoutMs ?? 5_000,
49
+ };
50
+ const appName = options.appName ?? (await guessAppName());
51
+
52
+ let client: IpcClient | undefined;
53
+ let stdinBuffer = '';
54
+ let closing = false;
55
+ let reconnecting: Promise<void> | undefined;
56
+ const pending: string[] = [];
57
+
58
+ const writeError = (message: string) => {
59
+ stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32000, message } })}\n`);
60
+ stderr.write(`mesh-shim: ${message}\n`);
61
+ };
62
+
63
+ const flushPending = () => {
64
+ if (!client) {
65
+ return;
66
+ }
67
+
68
+ while (pending.length > 0) {
69
+ try {
70
+ client.sendRaw(pending[0] as string);
71
+ pending.shift();
72
+ } catch {
73
+ return;
74
+ }
75
+ }
76
+ };
77
+
78
+ const connect = async () => {
79
+ await ensureDaemon(launcherOptions);
80
+ const deadline = Date.now() + launcherOptions.startupTimeoutMs;
81
+ while (true) {
82
+ const next = new IpcClient(launcherOptions.ipcPath);
83
+ next.onMessage((message) => {
84
+ stdout.write(`${JSON.stringify(message)}\n`);
85
+ });
86
+ next.onClose(() => {
87
+ if (!closing && client === next) {
88
+ client = undefined;
89
+ void reconnect();
90
+ }
91
+ });
92
+
93
+ try {
94
+ await next.connect(appName);
95
+ client = next;
96
+ flushPending();
97
+ return;
98
+ } catch (error) {
99
+ next.close();
100
+ if (Date.now() >= deadline) {
101
+ throw error;
102
+ }
103
+ await delay(200);
104
+ }
105
+ }
106
+ };
107
+
108
+ const reconnect = async () => {
109
+ if (reconnecting) {
110
+ return reconnecting;
111
+ }
112
+
113
+ reconnecting = (async () => {
114
+ try {
115
+ await connect();
116
+ } catch (error) {
117
+ if (!closing) {
118
+ closing = true;
119
+ client?.close();
120
+ writeError(`Unable to reach mesh daemon: ${(error as Error).message}`);
121
+ exit(1);
122
+ }
123
+ } finally {
124
+ reconnecting = undefined;
125
+ }
126
+ })();
127
+
128
+ return reconnecting;
129
+ };
130
+
131
+ try {
132
+ await connect();
133
+ } catch (error) {
134
+ closing = true;
135
+ writeError(`Unable to start mesh daemon: ${(error as Error).message}`);
136
+ exit(1);
137
+ return { close: () => undefined };
138
+ }
139
+
140
+ if ('setEncoding' in stdin && typeof stdin.setEncoding === 'function') {
141
+ stdin.setEncoding('utf8');
142
+ }
143
+
144
+ const handleChunk = (chunk: string | Buffer) => {
145
+ stdinBuffer += chunk.toString();
146
+ let newlineIndex = stdinBuffer.indexOf('\n');
147
+ while (newlineIndex >= 0) {
148
+ const line = stdinBuffer.slice(0, newlineIndex).trim();
149
+ stdinBuffer = stdinBuffer.slice(newlineIndex + 1);
150
+ if (line) {
151
+ if (client) {
152
+ try {
153
+ client.sendRaw(line);
154
+ } catch {
155
+ pending.push(line);
156
+ void reconnect();
157
+ }
158
+ } else {
159
+ pending.push(line);
160
+ void reconnect();
161
+ }
162
+ }
163
+ newlineIndex = stdinBuffer.indexOf('\n');
164
+ }
165
+ };
166
+
167
+ const shutdown = () => {
168
+ if (closing) {
169
+ return;
170
+ }
171
+
172
+ closing = true;
173
+ stdin.off('data', handleChunk);
174
+ stdin.off('end', shutdown);
175
+ stdin.off('close', shutdown);
176
+ client?.close();
177
+ exit(0);
178
+ };
179
+
180
+ stdin.on('data', handleChunk);
181
+ stdin.on('end', shutdown);
182
+ stdin.on('close', shutdown);
183
+ if ('resume' in stdin && typeof stdin.resume === 'function') {
184
+ stdin.resume();
185
+ }
186
+
187
+ return { close: shutdown };
188
+ }
189
+
190
+ async function guessAppName(): Promise<string> {
191
+ if (process.env.COLLECTIVE_APP_NAME) {
192
+ return process.env.COLLECTIVE_APP_NAME;
193
+ }
194
+
195
+ const arg = process.argv
196
+ .map((value) => basename(value).toLowerCase())
197
+ .find((value) => value && !['node', 'node.exe', 'mesh-shim', 'index.js'].includes(value));
198
+ if (arg) {
199
+ return arg.replace(/\.(cmd|exe)$/i, '');
200
+ }
201
+
202
+ try {
203
+ const result =
204
+ process.platform === 'win32'
205
+ ? await execFileAsync(
206
+ 'powershell',
207
+ ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId = ${process.ppid}").Name`],
208
+ { windowsHide: true },
209
+ )
210
+ : await execFileAsync('ps', ['-o', 'comm=', '-p', String(process.ppid)]);
211
+ const name = basename(result.stdout.trim()).replace(/\.(cmd|exe)$/i, '').toLowerCase();
212
+ return name || 'unknown';
213
+ } catch {
214
+ return 'unknown';
215
+ }
216
+ }
@@ -0,0 +1,103 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+ import net from 'node:net';
4
+ import { homedir, userInfo } from 'node:os';
5
+ import { resolve } from 'node:path';
6
+ import { setTimeout as delay } from 'node:timers/promises';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const require = createRequire(import.meta.url);
10
+
11
+ export interface LauncherOptions {
12
+ ipcPath: string;
13
+ pidFile: string;
14
+ daemonBin: string;
15
+ startupTimeoutMs: number;
16
+ }
17
+
18
+ const LEGACY_WINDOWS_PIPE_PATH = '\\\\.\\pipe\\hivemind-collective';
19
+
20
+ export function getDefaultIpcPath(): string {
21
+ return process.env.COLLECTIVE_IPC_PATH ??
22
+ (process.platform === 'win32'
23
+ ? `${LEGACY_WINDOWS_PIPE_PATH}-${sanitizePipeSegment(getCurrentUsername())}`
24
+ : resolve(homedir(), '.hivemind-os/collective', 'mesh.sock'));
25
+ }
26
+
27
+ export function getDefaultPidFile(): string {
28
+ return process.env.COLLECTIVE_PID_FILE ?? resolve(homedir(), '.hivemind-os/collective', 'daemon.pid');
29
+ }
30
+
31
+ export function resolveDaemonBin(): string {
32
+ if (process.env.COLLECTIVE_DAEMON_BIN) {
33
+ return process.env.COLLECTIVE_DAEMON_BIN;
34
+ }
35
+
36
+ try {
37
+ return require.resolve('@hivemind-os/collective-daemon');
38
+ } catch {
39
+ if (commandExists('mesh-daemon')) {
40
+ return 'mesh-daemon';
41
+ }
42
+
43
+ return fileURLToPath(new URL('../../daemon/dist/index.js', import.meta.url));
44
+ }
45
+ }
46
+
47
+ export async function isDaemonRunning(ipcPath: string): Promise<boolean> {
48
+ return new Promise((resolvePromise) => {
49
+ const socket = net.connect(ipcPath);
50
+ const finish = (running: boolean) => {
51
+ socket.removeAllListeners();
52
+ socket.destroy();
53
+ resolvePromise(running);
54
+ };
55
+
56
+ socket.once('connect', () => {
57
+ finish(true);
58
+ });
59
+ socket.once('error', () => {
60
+ finish(false);
61
+ });
62
+ });
63
+ }
64
+
65
+ export async function ensureDaemonRunning(options: LauncherOptions): Promise<void> {
66
+ if (await isDaemonRunning(options.ipcPath)) {
67
+ return;
68
+ }
69
+
70
+ const command = options.daemonBin.endsWith('.js') ? process.execPath : options.daemonBin;
71
+ const args = options.daemonBin.endsWith('.js') ? [options.daemonBin] : [];
72
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' });
73
+ child.unref();
74
+
75
+ const deadline = Date.now() + options.startupTimeoutMs;
76
+ while (Date.now() < deadline) {
77
+ await delay(200);
78
+ if (await isDaemonRunning(options.ipcPath)) {
79
+ return;
80
+ }
81
+ }
82
+
83
+ throw new Error(`Timed out waiting for daemon IPC at ${options.ipcPath} (pid file: ${options.pidFile})`);
84
+ }
85
+
86
+ function commandExists(command: string): boolean {
87
+ const probe = process.platform === 'win32' ? 'where' : 'which';
88
+ return spawnSync(probe, [command], { stdio: 'ignore' }).status === 0;
89
+ }
90
+
91
+ function getCurrentUsername(): string {
92
+ try {
93
+ return userInfo().username;
94
+ } catch {
95
+ return process.env.USERNAME ?? process.env.USER ?? 'unknown-user';
96
+ }
97
+ }
98
+
99
+ function sanitizePipeSegment(value: string): string {
100
+ const leaf = value.split(/[\\/]+/).filter(Boolean).at(-1) ?? value;
101
+ const sanitized = leaf.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
102
+ return sanitized || 'unknown-user';
103
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { startShim } from './bridge.js';
4
+
5
+ startShim().catch((err) => {
6
+ process.stderr.write(`mesh-shim fatal: ${err.message}\n`);
7
+ process.exit(1);
8
+ });
@@ -0,0 +1,138 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import net from 'node:net';
3
+
4
+ type MessageHandler = (message: object) => void;
5
+ type CloseHandler = () => void;
6
+
7
+ export class IpcClient {
8
+ private socket?: net.Socket;
9
+ private buffer = '';
10
+ private readonly messageHandlers = new Set<MessageHandler>();
11
+ private readonly closeHandlers = new Set<CloseHandler>();
12
+ private helloId?: string;
13
+ private helloWaiter?: { resolve: () => void; reject: (error: Error) => void };
14
+
15
+ constructor(private readonly ipcPath: string) {}
16
+
17
+ async connect(appName = 'unknown'): Promise<void> {
18
+ this.close();
19
+ const socket = await new Promise<net.Socket>((resolve, reject) => {
20
+ const client = net.connect(this.ipcPath, () => {
21
+ client.off('error', reject);
22
+ resolve(client);
23
+ });
24
+ client.once('error', reject);
25
+ });
26
+
27
+ this.socket = socket;
28
+ socket.setEncoding('utf8');
29
+ socket.setNoDelay(true);
30
+ socket.on('data', (chunk: string | Buffer) => {
31
+ this.buffer += chunk.toString();
32
+ this.drainBuffer();
33
+ });
34
+ socket.on('close', () => {
35
+ this.handleClose();
36
+ });
37
+ socket.on('end', () => {
38
+ this.handleClose();
39
+ });
40
+ socket.on('error', () => undefined);
41
+
42
+ this.helloId = `shim-hello-${randomUUID()}`;
43
+ await new Promise<void>((resolve, reject) => {
44
+ this.helloWaiter = { resolve, reject };
45
+ this.send({
46
+ jsonrpc: '2.0',
47
+ id: this.helloId as string,
48
+ method: 'shim_hello',
49
+ params: {
50
+ appName,
51
+ pid: process.pid,
52
+ ...(process.env.COLLECTIVE_PROFILE ? { profile: process.env.COLLECTIVE_PROFILE } : {}),
53
+ },
54
+ });
55
+ });
56
+ }
57
+
58
+ send(message: object): void {
59
+ this.sendRaw(JSON.stringify(message));
60
+ }
61
+
62
+ sendRaw(line: string): void {
63
+ const socket = this.socket;
64
+ if (!socket || socket.destroyed) {
65
+ throw new Error('IPC client is not connected');
66
+ }
67
+
68
+ socket.write(`${line.trimEnd()}\n`);
69
+ }
70
+
71
+ onMessage(handler: (message: object) => void): void {
72
+ this.messageHandlers.add(handler);
73
+ }
74
+
75
+ onClose(handler: () => void): void {
76
+ this.closeHandlers.add(handler);
77
+ }
78
+
79
+ close(): void {
80
+ const socket = this.socket;
81
+ this.socket = undefined;
82
+ this.buffer = '';
83
+ this.rejectHello(new Error('IPC connection closed'));
84
+ if (socket && !socket.destroyed) {
85
+ socket.destroy();
86
+ }
87
+ }
88
+
89
+ private drainBuffer(): void {
90
+ let newlineIndex = this.buffer.indexOf('\n');
91
+ while (newlineIndex >= 0) {
92
+ const line = this.buffer.slice(0, newlineIndex).trim();
93
+ this.buffer = this.buffer.slice(newlineIndex + 1);
94
+ if (line) {
95
+ const message = JSON.parse(line) as Record<string, unknown>;
96
+ if (this.helloWaiter && message.id === this.helloId) {
97
+ const waiter = this.helloWaiter;
98
+ this.helloWaiter = undefined;
99
+ this.helloId = undefined;
100
+ if (message.error && typeof message.error === 'object') {
101
+ waiter.reject(new Error(String((message.error as { message?: unknown }).message ?? 'shim_hello failed')));
102
+ } else {
103
+ waiter.resolve();
104
+ }
105
+ } else {
106
+ for (const handler of this.messageHandlers) {
107
+ handler(message);
108
+ }
109
+ }
110
+ }
111
+ newlineIndex = this.buffer.indexOf('\n');
112
+ }
113
+ }
114
+
115
+ private handleClose(): void {
116
+ if (!this.socket && !this.helloWaiter) {
117
+ return;
118
+ }
119
+
120
+ this.socket = undefined;
121
+ this.buffer = '';
122
+ this.rejectHello(new Error('IPC connection closed'));
123
+ for (const handler of this.closeHandlers) {
124
+ handler();
125
+ }
126
+ }
127
+
128
+ private rejectHello(error: Error): void {
129
+ if (!this.helloWaiter) {
130
+ return;
131
+ }
132
+
133
+ const waiter = this.helloWaiter;
134
+ this.helloWaiter = undefined;
135
+ this.helloId = undefined;
136
+ waiter.reject(error);
137
+ }
138
+ }
@@ -0,0 +1,287 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, rm } from 'node:fs/promises';
3
+ import net from 'node:net';
4
+ import { resolve } from 'node:path';
5
+ import { PassThrough } from 'node:stream';
6
+
7
+ import { afterEach, describe, expect, it, vi } from 'vitest';
8
+
9
+ import { createBridge } from '../src/bridge.js';
10
+
11
+ const createdPaths: string[] = [];
12
+ const servers = new Set<net.Server>();
13
+ const sockets = new Set<net.Socket>();
14
+
15
+ afterEach(async () => {
16
+ for (const socket of [...sockets]) {
17
+ socket.destroy();
18
+ }
19
+
20
+ await Promise.all(
21
+ [...servers].map(
22
+ (server) =>
23
+ new Promise<void>((resolvePromise) => {
24
+ server.close(() => {
25
+ resolvePromise();
26
+ });
27
+ }),
28
+ ),
29
+ );
30
+ servers.clear();
31
+ await Promise.all(createdPaths.splice(0).map((path) => rm(path, { recursive: true, force: true })));
32
+ });
33
+
34
+ class NdjsonReader {
35
+ private buffer = '';
36
+ private readonly messages: unknown[] = [];
37
+ private readonly waiters: Array<(message: unknown) => void> = [];
38
+
39
+ constructor(stream: NodeJS.ReadableStream) {
40
+ if ('setEncoding' in stream && typeof stream.setEncoding === 'function') {
41
+ stream.setEncoding('utf8');
42
+ }
43
+
44
+ stream.on('data', (chunk: string | Buffer) => {
45
+ this.buffer += chunk.toString();
46
+ this.drainBuffer();
47
+ });
48
+ }
49
+
50
+ async nextMessage(): Promise<any> {
51
+ if (this.messages.length > 0) {
52
+ return this.messages.shift();
53
+ }
54
+
55
+ return new Promise((resolvePromise) => {
56
+ this.waiters.push(resolvePromise);
57
+ });
58
+ }
59
+
60
+ collected(): unknown[] {
61
+ return [...this.messages];
62
+ }
63
+
64
+ private drainBuffer(): void {
65
+ let newlineIndex = this.buffer.indexOf('\n');
66
+ while (newlineIndex >= 0) {
67
+ const line = this.buffer.slice(0, newlineIndex).trim();
68
+ this.buffer = this.buffer.slice(newlineIndex + 1);
69
+ if (line) {
70
+ const message = JSON.parse(line) as unknown;
71
+ const waiter = this.waiters.shift();
72
+ if (waiter) {
73
+ waiter(message);
74
+ } else {
75
+ this.messages.push(message);
76
+ }
77
+ }
78
+ newlineIndex = this.buffer.indexOf('\n');
79
+ }
80
+ }
81
+ }
82
+
83
+ class MockIpcServer {
84
+ private server?: net.Server;
85
+ private socket?: net.Socket;
86
+ private reader?: NdjsonReader;
87
+
88
+ constructor(private readonly ipcPath: string) {}
89
+
90
+ async start(): Promise<void> {
91
+ this.server = net.createServer((socket) => {
92
+ this.socket = socket;
93
+ this.reader = new NdjsonReader(socket);
94
+ sockets.add(socket);
95
+ socket.once('close', () => {
96
+ sockets.delete(socket);
97
+ if (this.socket === socket) {
98
+ this.socket = undefined;
99
+ this.reader = undefined;
100
+ }
101
+ });
102
+ });
103
+
104
+ servers.add(this.server);
105
+ await new Promise<void>((resolvePromise, reject) => {
106
+ this.server?.once('error', reject);
107
+ this.server?.listen(this.ipcPath, () => {
108
+ this.server?.off('error', reject);
109
+ resolvePromise();
110
+ });
111
+ });
112
+ }
113
+
114
+ async stop(): Promise<void> {
115
+ this.socket?.destroy();
116
+ if (!this.server) {
117
+ return;
118
+ }
119
+
120
+ const server = this.server;
121
+ this.server = undefined;
122
+ servers.delete(server);
123
+ await new Promise<void>((resolvePromise) => {
124
+ server.close(() => {
125
+ resolvePromise();
126
+ });
127
+ });
128
+ }
129
+
130
+ async nextMessage(): Promise<any> {
131
+ while (!this.reader) {
132
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 10));
133
+ }
134
+
135
+ return this.reader.nextMessage();
136
+ }
137
+
138
+ send(message: object): void {
139
+ this.socket?.write(`${JSON.stringify(message)}\n`);
140
+ }
141
+ }
142
+
143
+ async function createTestDir(): Promise<string> {
144
+ const dir = resolve(process.cwd(), '.test-data', randomUUID());
145
+ createdPaths.push(dir);
146
+ await mkdir(dir, { recursive: true });
147
+ return dir;
148
+ }
149
+
150
+ function createIpcPath(dir: string): string {
151
+ if (process.platform === 'win32') {
152
+ return `\\\\.\\pipe\\hivemind-collective-shim-${randomUUID()}`;
153
+ }
154
+ // Unix socket paths have a 108-char limit; use /tmp with a short name
155
+ const short = randomUUID().slice(0, 8);
156
+ return `/tmp/hm-shim-${short}.sock`;
157
+ }
158
+
159
+ describe('bridge', () => {
160
+ it('sends shim_hello and bridges stdin/stdout messages', async () => {
161
+ const dir = await createTestDir();
162
+ const ipcPath = createIpcPath(dir);
163
+ const server = new MockIpcServer(ipcPath);
164
+ await server.start();
165
+
166
+ const stdin = new PassThrough();
167
+ const stdout = new PassThrough();
168
+ const stderr = new PassThrough();
169
+ const output = new NdjsonReader(stdout);
170
+ const exit = vi.fn();
171
+
172
+ const bridgePromise = createBridge({
173
+ ipcPath,
174
+ pidFile: resolve(dir, 'daemon.pid'),
175
+ daemonBin: 'mesh-daemon',
176
+ appName: 'claude-desktop',
177
+ stdin,
178
+ stdout,
179
+ stderr,
180
+ exit,
181
+ ensureDaemon: async () => undefined,
182
+ });
183
+
184
+ const hello = await server.nextMessage();
185
+ expect(hello).toMatchObject({
186
+ jsonrpc: '2.0',
187
+ method: 'shim_hello',
188
+ params: {
189
+ appName: 'claude-desktop',
190
+ pid: process.pid,
191
+ },
192
+ });
193
+
194
+ server.send({
195
+ jsonrpc: '2.0',
196
+ id: hello.id,
197
+ result: { acknowledged: true, connectionId: 'test-connection' },
198
+ });
199
+ const bridge = await bridgePromise;
200
+
201
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 25));
202
+ expect(output.collected()).toEqual([]);
203
+
204
+ stdin.write('{"jsonrpc":"2.0","id":"ping","method":"ping"}\n');
205
+ const forwarded = await server.nextMessage();
206
+ expect(forwarded).toEqual({ jsonrpc: '2.0', id: 'ping', method: 'ping' });
207
+
208
+ server.send({ jsonrpc: '2.0', id: 'pong', result: { ok: true } });
209
+ await expect(output.nextMessage()).resolves.toEqual({ jsonrpc: '2.0', id: 'pong', result: { ok: true } });
210
+
211
+ bridge.close();
212
+ expect(exit).toHaveBeenCalledWith(0);
213
+ });
214
+
215
+ it('reconnects and sends shim_hello again after the IPC connection drops', async () => {
216
+ const dir = await createTestDir();
217
+ const ipcPath = createIpcPath(dir);
218
+ const firstServer = new MockIpcServer(ipcPath);
219
+ await firstServer.start();
220
+
221
+ let releaseReconnect = () => undefined;
222
+ const reconnectGate = new Promise<void>((resolvePromise) => {
223
+ releaseReconnect = resolvePromise;
224
+ });
225
+ const ensureDaemon = vi.fn(async () => {
226
+ if (ensureDaemon.mock.calls.length > 1) {
227
+ await reconnectGate;
228
+ }
229
+ });
230
+
231
+ const stdin = new PassThrough();
232
+ const stdout = new PassThrough();
233
+ const stderr = new PassThrough();
234
+ const exit = vi.fn();
235
+
236
+ const bridgePromise = createBridge({
237
+ ipcPath,
238
+ pidFile: resolve(dir, 'daemon.pid'),
239
+ daemonBin: 'mesh-daemon',
240
+ appName: 'vscode',
241
+ stdin,
242
+ stdout,
243
+ stderr,
244
+ exit,
245
+ ensureDaemon,
246
+ startupTimeoutMs: 1_000,
247
+ });
248
+
249
+ const firstHello = await firstServer.nextMessage();
250
+ firstServer.send({
251
+ jsonrpc: '2.0',
252
+ id: firstHello.id,
253
+ result: { acknowledged: true, connectionId: 'first' },
254
+ });
255
+ const bridge = await bridgePromise;
256
+
257
+ await firstServer.stop();
258
+
259
+ const secondServer = new MockIpcServer(ipcPath);
260
+ await secondServer.start();
261
+ releaseReconnect();
262
+
263
+ const secondHello = await secondServer.nextMessage();
264
+ expect(secondHello).toMatchObject({
265
+ jsonrpc: '2.0',
266
+ method: 'shim_hello',
267
+ params: { appName: 'vscode', pid: process.pid },
268
+ });
269
+
270
+ secondServer.send({
271
+ jsonrpc: '2.0',
272
+ id: secondHello.id,
273
+ result: { acknowledged: true, connectionId: 'second' },
274
+ });
275
+
276
+ stdin.write('{"jsonrpc":"2.0","id":"after-reconnect","method":"ping"}\n');
277
+ await expect(secondServer.nextMessage()).resolves.toEqual({
278
+ jsonrpc: '2.0',
279
+ id: 'after-reconnect',
280
+ method: 'ping',
281
+ });
282
+
283
+ bridge.close();
284
+ expect(exit).toHaveBeenCalledWith(0);
285
+ expect(ensureDaemon).toHaveBeenCalledTimes(2);
286
+ });
287
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "outDir": "dist",
6
+ "types": ["node", "vitest/globals"]
7
+ },
8
+ "include": ["src/**/*.ts", "tests/**/*.ts", "vitest.config.ts", "tsup.config.ts"]
9
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm'],
6
+ dts: true,
7
+ clean: true,
8
+ sourcemap: true,
9
+ target: 'es2022',
10
+ });
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ passWithNoTests: false,
7
+ },
8
+ });