agent-peers 0.1.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.
- package/CHANGELOG.md +14 -0
- package/CLAUDE-snippet.md +3 -0
- package/CONTRIBUTING.md +55 -0
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/SECURITY.md +13 -0
- package/__init__.py +8 -0
- package/after-install.md +11 -0
- package/agent_peers/__init__.py +6 -0
- package/agent_peers/plugin.py +182 -0
- package/agent_peers/runtime.py +293 -0
- package/bin/agent-peers.mjs +69 -0
- package/docs/compatibility.md +38 -0
- package/docs/hermes.md +55 -0
- package/docs/reference.md +66 -0
- package/docs/release-verification.md +31 -0
- package/hermes-plugin/__init__.py +15 -0
- package/hermes-plugin/plugin.yaml +13 -0
- package/package.json +67 -0
- package/plugin.yaml +20 -0
- package/scripts/install-hermes-plugin.py +34 -0
- package/src/app-server.mjs +137 -0
- package/src/codex-peer.mjs +504 -0
- package/src/diagnostics.mjs +42 -0
- package/src/discovery.mjs +57 -0
- package/src/guidance.mjs +5 -0
- package/src/hermes-bridge.mjs +376 -0
- package/src/version.mjs +3 -0
- package/src/wire.mjs +313 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// Private reply inboxes for Hermes. This process never writes a discovery registry entry:
|
|
2
|
+
// a conversation becomes reachable only by sharing its address with a recipient.
|
|
3
|
+
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { chmodSync, lstatSync, rmSync } from "node:fs";
|
|
6
|
+
import { createServer } from "node:net";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { AppServer } from "./app-server.mjs";
|
|
9
|
+
import {
|
|
10
|
+
MAX_LINE, MAX_SOCKET_PATH, address, allPeers, messageLine, parseEnvelope, peerSocket, post, readLines,
|
|
11
|
+
} from "./wire.mjs";
|
|
12
|
+
|
|
13
|
+
const BUCKET = 30, REFILL_PER_S = 0.5, DEDUP_MS = 30_000, IDLE_MS = 600_000;
|
|
14
|
+
const QUEUE_MAX = 50, TOTAL_QUEUE_MAX = 200, SOURCES_MAX = 256, SESSIONS_MAX = 256;
|
|
15
|
+
const IPC_QUEUE_MAX = 128, IPC_WRITE_TIMEOUT_MS = 5000;
|
|
16
|
+
const logger = (message) => console.error(`[hermes-bridge ${process.pid}] ${message}`);
|
|
17
|
+
|
|
18
|
+
function requiredString(value, field, max = MAX_LINE) {
|
|
19
|
+
if (typeof value !== "string" || !value.trim() || value.length > max) {
|
|
20
|
+
throw new Error(`${field} must be a nonempty string of at most ${max} characters`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveTarget(to) {
|
|
26
|
+
const peers = allPeers();
|
|
27
|
+
if (to.startsWith("uds:")) {
|
|
28
|
+
const socket = peerSocket(to);
|
|
29
|
+
if (!socket) throw new Error("invalid peer inbox address; use an address from list_peers or a received message");
|
|
30
|
+
return peers.find((p) => p.address === to) ?? { address: to, socket, name: to };
|
|
31
|
+
}
|
|
32
|
+
const exact = peers.filter((p) => p.name === to || p.name === `claude:${to}` || p.name === `codex:${to}`);
|
|
33
|
+
const matches = exact.length ? exact : peers.filter((p) => p.name.includes(to));
|
|
34
|
+
if (matches.length === 1) return matches[0];
|
|
35
|
+
throw new Error(matches.length
|
|
36
|
+
? `"${to}" is ambiguous: ${matches.map((p) => `${p.name} (${p.address})`).join(", ")}`
|
|
37
|
+
: `no live peer named "${to}"; call list_peers`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class HermesBridge {
|
|
41
|
+
#app;
|
|
42
|
+
#emit;
|
|
43
|
+
#log;
|
|
44
|
+
#routes = new Map();
|
|
45
|
+
#locks = new Map();
|
|
46
|
+
#closed = false;
|
|
47
|
+
#closing;
|
|
48
|
+
#queued = 0;
|
|
49
|
+
|
|
50
|
+
constructor({ app = new AppServer(), emit = async () => {}, log = logger } = {}) {
|
|
51
|
+
this.#app = app;
|
|
52
|
+
this.#emit = emit;
|
|
53
|
+
this.#log = log;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async request(method, params = {}) {
|
|
57
|
+
if (this.#closed) throw new Error("Hermes bridge is closed");
|
|
58
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("params must be an object");
|
|
59
|
+
if (method === "list_peers") return this.#listPeers();
|
|
60
|
+
if (method !== "send_peer" && method !== "close_session") throw new Error(`unknown method: ${method}`);
|
|
61
|
+
const session = requiredString(params.session, "session", 512);
|
|
62
|
+
// A close cannot race a send creating the same route. Other conversations remain independent.
|
|
63
|
+
const previous = this.#locks.get(session) ?? Promise.resolve();
|
|
64
|
+
const operation = previous.catch(() => {}).then(async () => {
|
|
65
|
+
if (this.#closed) throw new Error("Hermes bridge is closed");
|
|
66
|
+
if (method === "send_peer") return this.#send(session, params);
|
|
67
|
+
const routes = [...this.#routes.values()].filter((route) => route.session === session);
|
|
68
|
+
await Promise.all(routes.map((route) => this.#retire(route)));
|
|
69
|
+
return { closed: routes.length > 0 };
|
|
70
|
+
});
|
|
71
|
+
this.#locks.set(session, operation);
|
|
72
|
+
try {
|
|
73
|
+
return await operation;
|
|
74
|
+
} finally {
|
|
75
|
+
if (this.#locks.get(session) === operation) this.#locks.delete(session);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async #listPeers() {
|
|
80
|
+
const peers = allPeers().filter((p) => p.kind === "claude" || p.kind === "codex");
|
|
81
|
+
let note;
|
|
82
|
+
const live = new Map();
|
|
83
|
+
if (peers.some((p) => p.kind === "codex")) {
|
|
84
|
+
let timer, expired = false;
|
|
85
|
+
const read = async () => {
|
|
86
|
+
// Never thread/read a historical thread: it would load it into the daemon.
|
|
87
|
+
const ids = new Set(await this.#app.loadedThreads());
|
|
88
|
+
if (expired) return;
|
|
89
|
+
const results = await Promise.allSettled(peers.filter((p) => p.kind === "codex" && ids.has(p.threadId))
|
|
90
|
+
.map(async (p) => ({ id: p.threadId, thread: await this.#app.readThread(p.threadId) })));
|
|
91
|
+
if (expired) return;
|
|
92
|
+
for (const r of results) if (r.status === "fulfilled") live.set(r.value.id, r.value.thread);
|
|
93
|
+
if (results.some((r) => r.status === "rejected")) note = "Some live Codex statuses are unavailable.";
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
await Promise.race([read(), new Promise((_, reject) => {
|
|
97
|
+
timer = setTimeout(() => reject(new Error("status lookup timed out")), 3000);
|
|
98
|
+
})]);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
note = `Live Codex status unavailable: ${error.message}. Registered inboxes are still listed.`;
|
|
101
|
+
} finally {
|
|
102
|
+
expired = true;
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
peers: peers.map((p) => ({
|
|
108
|
+
name: p.name, kind: p.kind, cwd: p.cwd ?? null,
|
|
109
|
+
status: live.get(p.threadId)?.status?.type ?? p.status ?? null, address: p.address,
|
|
110
|
+
})),
|
|
111
|
+
...(note ? { note } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async #bind(session, name, directory) {
|
|
116
|
+
// Claude only accepts reply addresses in its own runtime directory. A
|
|
117
|
+
// conversation needs one private endpoint per destination directory.
|
|
118
|
+
const key = JSON.stringify([session, directory]);
|
|
119
|
+
for (const route of this.#routes.values()) {
|
|
120
|
+
if (route.session === session && route.name !== name) {
|
|
121
|
+
throw new Error("this conversation already belongs to a different Hermes identity");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const previous = this.#routes.get(key);
|
|
125
|
+
if (previous) {
|
|
126
|
+
if (previous.name !== name) throw new Error("this conversation already belongs to a different Hermes identity");
|
|
127
|
+
return previous;
|
|
128
|
+
}
|
|
129
|
+
if (this.#routes.size >= SESSIONS_MAX) throw new Error("too many open Hermes conversation inboxes");
|
|
130
|
+
const socket = join(directory, `hermes-${randomUUID().replaceAll("-", "")}.sock`);
|
|
131
|
+
if (Buffer.byteLength(socket) > MAX_SOCKET_PATH) {
|
|
132
|
+
throw new Error("peer socket path exceeds the 103-byte Unix limit; use a shorter XDG_RUNTIME_DIR for all participating agents");
|
|
133
|
+
}
|
|
134
|
+
if (!peerSocket(address(socket))) throw new Error("unsafe Hermes reply inbox directory");
|
|
135
|
+
const route = {
|
|
136
|
+
key, session, name, socket, address: address(socket), closed: false, connections: new Set(),
|
|
137
|
+
buckets: new Map(), recent: new Map(), pending: new Map(), queued: 0, chain: Promise.resolve(),
|
|
138
|
+
};
|
|
139
|
+
route.server = createServer((conn) => {
|
|
140
|
+
if (route.closed || route.connections.size >= QUEUE_MAX) return conn.destroy();
|
|
141
|
+
route.connections.add(conn);
|
|
142
|
+
conn.once("close", () => route.connections.delete(conn));
|
|
143
|
+
readLines(conn, (line) => this.#receive(route, line));
|
|
144
|
+
});
|
|
145
|
+
route.server.on("error", (error) => this.#log(`inbox error: ${error.message}`));
|
|
146
|
+
try {
|
|
147
|
+
await new Promise((resolve, reject) => {
|
|
148
|
+
route.server.once("error", reject);
|
|
149
|
+
// Never unlink before binding, even in the unlikely event of an address collision.
|
|
150
|
+
route.server.listen(socket, () => { route.server.off("error", reject); resolve(); });
|
|
151
|
+
});
|
|
152
|
+
route.owner = lstatSync(socket);
|
|
153
|
+
chmodSync(socket, 0o600);
|
|
154
|
+
this.#routes.set(key, route);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
route.closed = true;
|
|
157
|
+
for (const conn of route.connections) conn.destroy();
|
|
158
|
+
// Binding is not yet published in #routes: clean the listener even if stat or
|
|
159
|
+
// chmod fails, otherwise close() cannot find it and the process stays alive.
|
|
160
|
+
await new Promise((resolve) => route.server.close(resolve));
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
return route;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async #send(session, params) {
|
|
167
|
+
const name = requiredString(params.name, "name", 80);
|
|
168
|
+
if (!/^hermes:[A-Za-z0-9:_-]+$/.test(name)) throw new Error("name must identify a Hermes profile as hermes:<profile>");
|
|
169
|
+
const to = requiredString(params.to, "to", 512);
|
|
170
|
+
const message = requiredString(params.message, "message");
|
|
171
|
+
const target = resolveTarget(to);
|
|
172
|
+
const targetSocket = peerSocket(target.address);
|
|
173
|
+
if (!targetSocket) throw new Error("the peer registry contains an invalid inbox address; call list_peers again");
|
|
174
|
+
const route = await this.#bind(session, name, dirname(targetSocket));
|
|
175
|
+
if (target.address === route.address) throw new Error("that address is this conversation's own inbox");
|
|
176
|
+
// Claude labels every external envelope as another Claude session. Correct that frame.
|
|
177
|
+
const body = target.kind === "codex" ? message : `${message}\n\n` +
|
|
178
|
+
`(From ${name}, a Hermes profile with a private reply inbox. Use your peer messaging tool ` +
|
|
179
|
+
`to reply to the sender address. Claude's notify_when_idle is unavailable for this peer.)`;
|
|
180
|
+
const line = messageLine({ from: route.address, fromName: name, body });
|
|
181
|
+
let finish, timer;
|
|
182
|
+
const receipt = new Promise((resolve) => {
|
|
183
|
+
finish = resolve;
|
|
184
|
+
route.pending.set(line.msg_id, resolve);
|
|
185
|
+
timer = setTimeout(() => resolve(undefined), 1500);
|
|
186
|
+
});
|
|
187
|
+
try {
|
|
188
|
+
await post(targetSocket, line);
|
|
189
|
+
const status = await receipt;
|
|
190
|
+
const text = status === "held"
|
|
191
|
+
? `Delivered to ${target.name}'s inbox; its session is holding it for its user's approval.`
|
|
192
|
+
: status && status !== "delivered" ? `Not delivered to ${target.name}: ${status}.`
|
|
193
|
+
: `Delivered to ${target.name}'s inbox. Any reply arrives in this Hermes conversation.`;
|
|
194
|
+
return { text, address: route.address, message_id: line.msg_id, ...(status ? { status } : {}) };
|
|
195
|
+
} finally {
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
route.pending.delete(line.msg_id);
|
|
198
|
+
finish(undefined);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#admit(route, from, body, id) {
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
for (const [key, bucket] of route.buckets) if (now - bucket.at > IDLE_MS) route.buckets.delete(key);
|
|
205
|
+
for (const [key, time] of route.recent) if (now - time > DEDUP_MS) route.recent.delete(key);
|
|
206
|
+
if (!route.buckets.has(from) && route.buckets.size >= SOURCES_MAX) return false;
|
|
207
|
+
const bucket = route.buckets.get(from) ?? { tokens: BUCKET, at: now };
|
|
208
|
+
bucket.tokens = Math.min(BUCKET, bucket.tokens + (now - bucket.at) / 1000 * REFILL_PER_S);
|
|
209
|
+
bucket.at = now;
|
|
210
|
+
route.buckets.set(from, bucket);
|
|
211
|
+
const bodyKey = createHash("sha256").update(from).update("\0").update(body).digest("hex");
|
|
212
|
+
const idKey = `${from}\0${id}`;
|
|
213
|
+
if (bucket.tokens < 1 || route.recent.has(bodyKey) || route.recent.has(idKey) ||
|
|
214
|
+
route.queued >= QUEUE_MAX || this.#queued >= TOTAL_QUEUE_MAX) return false;
|
|
215
|
+
bucket.tokens--;
|
|
216
|
+
route.recent.set(bodyKey, now);
|
|
217
|
+
route.recent.set(idKey, now);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#receive(route, line) {
|
|
222
|
+
if (route.closed || !line || typeof line !== "object") return;
|
|
223
|
+
if (line.type === "control" && line.action === "peer_message_status") {
|
|
224
|
+
if (typeof line.status === "string" && line.status.length <= 128) route.pending.get(line.orig_msg_id)?.(line.status);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (line.type !== "user" || typeof line.message?.content !== "string") return;
|
|
228
|
+
const env = parseEnvelope(line.message.content);
|
|
229
|
+
if (!env || !peerSocket(env.from) || env.from === route.address || (line.from && line.from !== env.from)) return;
|
|
230
|
+
const id = typeof line.msg_id === "string" && line.msg_id.length <= 256 && line.msg_id ? line.msg_id : randomUUID();
|
|
231
|
+
if (!this.#admit(route, env.from, env.body, id)) return;
|
|
232
|
+
route.queued++;
|
|
233
|
+
this.#queued++;
|
|
234
|
+
route.chain = route.chain.then(async () => {
|
|
235
|
+
if (route.closed) return;
|
|
236
|
+
await this.#emit({ method: "peer_message", params: {
|
|
237
|
+
session: route.session, message_id: id, from: env.from,
|
|
238
|
+
from_name: /^[A-Za-z0-9:_ .-]{1,80}$/.test(env.fromName ?? "") ? env.fromName : "an unnamed peer",
|
|
239
|
+
message: env.body,
|
|
240
|
+
} });
|
|
241
|
+
}).catch((error) => this.#log(`incoming message delivery failed: ${error.message}`))
|
|
242
|
+
.finally(() => { route.queued--; this.#queued--; });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async #retire(route) {
|
|
246
|
+
route.closed = true;
|
|
247
|
+
this.#routes.delete(route.key);
|
|
248
|
+
for (const resolve of route.pending.values()) resolve("closed");
|
|
249
|
+
route.pending.clear();
|
|
250
|
+
for (const conn of route.connections) conn.destroy();
|
|
251
|
+
let owned = false;
|
|
252
|
+
try {
|
|
253
|
+
const current = lstatSync(route.socket);
|
|
254
|
+
owned = current.dev === route.owner.dev && current.ino === route.owner.ino;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (error.code !== "ENOENT") this.#log(`cannot inspect inbox ownership: ${error.message}`);
|
|
257
|
+
}
|
|
258
|
+
if (owned) {
|
|
259
|
+
// Node closes and unlinks a Unix listener by pathname; do not close it if somebody
|
|
260
|
+
// replaced that pathname, because Node would also unlink their replacement.
|
|
261
|
+
await new Promise((resolve) => route.server.close(resolve));
|
|
262
|
+
try {
|
|
263
|
+
const current = lstatSync(route.socket);
|
|
264
|
+
if (current.dev === route.owner.dev && current.ino === route.owner.ino) rmSync(route.socket, { force: true });
|
|
265
|
+
} catch {}
|
|
266
|
+
} else {
|
|
267
|
+
// No longer discoverable at our address; retain the inactive handle until process
|
|
268
|
+
// exit rather than delete another owner's socket. It cannot keep the process alive.
|
|
269
|
+
route.server.unref();
|
|
270
|
+
}
|
|
271
|
+
route.buckets.clear();
|
|
272
|
+
route.recent.clear();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
close() {
|
|
276
|
+
if (this.#closing) return this.#closing;
|
|
277
|
+
this.#closed = true;
|
|
278
|
+
this.#closing = (async () => {
|
|
279
|
+
try {
|
|
280
|
+
await Promise.allSettled([...this.#locks.values()]);
|
|
281
|
+
await Promise.all([...this.#routes.values()].map((route) => this.#retire(route)));
|
|
282
|
+
} finally {
|
|
283
|
+
await this.#app.close?.();
|
|
284
|
+
}
|
|
285
|
+
})();
|
|
286
|
+
return this.#closing;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// The subprocess protocol is newline-delimited JSON. Stdout is reserved for protocol
|
|
291
|
+
// messages and respects stream backpressure; diagnostic text goes to stderr.
|
|
292
|
+
export function runHermesBridge() {
|
|
293
|
+
const write = (message) => new Promise((resolve, reject) => {
|
|
294
|
+
const timer = setTimeout(() => {
|
|
295
|
+
reject(new Error("bridge output timed out"));
|
|
296
|
+
void stop(1);
|
|
297
|
+
}, IPC_WRITE_TIMEOUT_MS);
|
|
298
|
+
process.stdout.write(JSON.stringify(message) + "\n", (error) => {
|
|
299
|
+
clearTimeout(timer);
|
|
300
|
+
if (error) reject(error); else resolve();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
const bridge = new HermesBridge({ emit: write });
|
|
304
|
+
let buffer = "", queued = 0, chain = Promise.resolve(), stopping = false;
|
|
305
|
+
const stop = async (code = 0) => {
|
|
306
|
+
if (stopping) return;
|
|
307
|
+
stopping = true;
|
|
308
|
+
process.stdin.pause();
|
|
309
|
+
// A blocked parent or daemon must not keep an unloaded plugin alive indefinitely.
|
|
310
|
+
const deadline = setTimeout(() => process.exit(code || 1), IPC_WRITE_TIMEOUT_MS);
|
|
311
|
+
deadline.unref();
|
|
312
|
+
try {
|
|
313
|
+
await bridge.close();
|
|
314
|
+
} catch (error) {
|
|
315
|
+
logger(`bridge shutdown failed: ${error.message}`);
|
|
316
|
+
code = 1;
|
|
317
|
+
}
|
|
318
|
+
process.exit(code);
|
|
319
|
+
};
|
|
320
|
+
process.stdin.setEncoding("utf8");
|
|
321
|
+
process.stdin.on("data", (data) => {
|
|
322
|
+
if (stopping) return;
|
|
323
|
+
buffer += data;
|
|
324
|
+
let newline;
|
|
325
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
326
|
+
const raw = buffer.slice(0, newline);
|
|
327
|
+
buffer = buffer.slice(newline + 1);
|
|
328
|
+
if (Buffer.byteLength(raw) + 1 > MAX_LINE || queued >= IPC_QUEUE_MAX) {
|
|
329
|
+
logger("bridge input limit exceeded");
|
|
330
|
+
void stop(1);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (!raw.trim()) continue;
|
|
334
|
+
queued++;
|
|
335
|
+
chain = chain.then(async () => {
|
|
336
|
+
if (stopping) return;
|
|
337
|
+
let request, id = null;
|
|
338
|
+
try {
|
|
339
|
+
request = JSON.parse(raw);
|
|
340
|
+
if (!request || typeof request !== "object" || Array.isArray(request) ||
|
|
341
|
+
!(typeof request.id === "string" && request.id.length > 0 && request.id.length <= 256 ||
|
|
342
|
+
Number.isSafeInteger(request.id)) || typeof request.method !== "string" ||
|
|
343
|
+
request.method.length > 80) {
|
|
344
|
+
throw new Error("expected {id, method, params}");
|
|
345
|
+
}
|
|
346
|
+
id = request.id;
|
|
347
|
+
if (request.method === "shutdown") {
|
|
348
|
+
await write({ id, result: { closed: true } });
|
|
349
|
+
await stop();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const result = await bridge.request(request.method, request.params);
|
|
353
|
+
await write({ id, result });
|
|
354
|
+
} catch (error) {
|
|
355
|
+
await write({ id, error: { message: error.message } });
|
|
356
|
+
}
|
|
357
|
+
}).catch((error) => { logger(error.message); void stop(1); }).finally(() => queued--);
|
|
358
|
+
}
|
|
359
|
+
if (Buffer.byteLength(buffer) >= MAX_LINE) {
|
|
360
|
+
logger("bridge input line too large");
|
|
361
|
+
void stop(1);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
process.stdin.on("end", () => {
|
|
365
|
+
if (buffer.trim()) {
|
|
366
|
+
logger("incomplete bridge input at EOF");
|
|
367
|
+
void stop(1);
|
|
368
|
+
} else void chain.finally(() => stop());
|
|
369
|
+
});
|
|
370
|
+
process.stdin.on("error", () => { void stop(1); });
|
|
371
|
+
process.stdout.on("error", () => { void stop(1); });
|
|
372
|
+
process.once("SIGINT", () => { void stop(); });
|
|
373
|
+
process.once("SIGTERM", () => { void stop(); });
|
|
374
|
+
process.on("uncaughtException", (error) => { logger(error.message); void stop(1); });
|
|
375
|
+
return bridge;
|
|
376
|
+
}
|
package/src/version.mjs
ADDED