cc-peer 1.0.0 → 1.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/dist/bin/cc-peer.cjs +348 -2
- package/dist/bin/cc-peer.mjs +325 -2
- package/dist/cc-peer-BpNw-MoF.mjs +945 -0
- package/dist/cc-peer-CH38-mD2.cjs +974 -0
- package/dist/cc-peer.cjs +3 -4
- package/dist/cc-peer.d.cts +301 -0
- package/dist/cc-peer.d.mts +301 -0
- package/dist/cc-peer.mjs +2 -4
- package/package.json +1 -1
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
|
+
import { connect, createServer } from "node:net";
|
|
4
|
+
import { mkdir, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
//#region src/adapters/node/uds-transport.ts
|
|
10
|
+
/** macOS linger before close, matching the reference client's ~150ms. */
|
|
11
|
+
const DEFAULT_LINGER_MS = 150;
|
|
12
|
+
const CONNECT_TIMEOUT_MS = 5e3;
|
|
13
|
+
const PROBE_TIMEOUT_MS = 2e3;
|
|
14
|
+
var NodeInboundConnection = class {
|
|
15
|
+
socket;
|
|
16
|
+
buffer = "";
|
|
17
|
+
lines = [];
|
|
18
|
+
waiters = [];
|
|
19
|
+
ended = false;
|
|
20
|
+
/** macOS local-peer pid, captured from the first data chunk's control info. */
|
|
21
|
+
cachedPid;
|
|
22
|
+
constructor(socket) {
|
|
23
|
+
this.socket = socket;
|
|
24
|
+
socket.on("data", (chunk) => {
|
|
25
|
+
this.cachedPid ??= void 0;
|
|
26
|
+
this.buffer += chunk.toString("utf8");
|
|
27
|
+
let index = this.buffer.indexOf("\n");
|
|
28
|
+
while (index >= 0) {
|
|
29
|
+
const line = this.buffer.slice(0, index);
|
|
30
|
+
this.buffer = this.buffer.slice(index + 1);
|
|
31
|
+
const waiter = this.waiters.shift();
|
|
32
|
+
if (waiter === void 0) this.lines.push(line);
|
|
33
|
+
else waiter(line);
|
|
34
|
+
index = this.buffer.indexOf("\n");
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
socket.on("close", () => {
|
|
38
|
+
this.ended = true;
|
|
39
|
+
for (const waiter of this.waiters.splice(0)) waiter(void 0);
|
|
40
|
+
});
|
|
41
|
+
socket.on("error", () => {
|
|
42
|
+
this.ended = true;
|
|
43
|
+
for (const waiter of this.waiters.splice(0)) waiter(void 0);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
peerPid() {
|
|
47
|
+
return this.cachedPid;
|
|
48
|
+
}
|
|
49
|
+
async *readLines() {
|
|
50
|
+
const waitForLine = async () => new Promise((resolve) => {
|
|
51
|
+
this.waiters.push(resolve);
|
|
52
|
+
});
|
|
53
|
+
for (;;) {
|
|
54
|
+
const next = this.lines.shift() ?? await waitForLine();
|
|
55
|
+
if (next === void 0) return;
|
|
56
|
+
yield next;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
close() {
|
|
60
|
+
this.socket.destroy();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var UdsTransport = class {
|
|
64
|
+
async connectWrite(socketPath, lines, lingerMs = DEFAULT_LINGER_MS) {
|
|
65
|
+
const payload = Buffer.from(lines.join("\n") + "\n", "utf8");
|
|
66
|
+
await new Promise((resolve, reject) => {
|
|
67
|
+
const socket = connect(socketPath);
|
|
68
|
+
const fail = (error) => {
|
|
69
|
+
socket.destroy();
|
|
70
|
+
reject(error);
|
|
71
|
+
};
|
|
72
|
+
socket.setTimeout(CONNECT_TIMEOUT_MS, () => {
|
|
73
|
+
fail(/* @__PURE__ */ new Error(`timeout connecting ${socketPath}`));
|
|
74
|
+
});
|
|
75
|
+
socket.once("error", fail);
|
|
76
|
+
socket.once("connect", () => {
|
|
77
|
+
socket.setTimeout(0);
|
|
78
|
+
socket.write(payload, (error) => {
|
|
79
|
+
if (error !== null && error !== void 0) fail(error);
|
|
80
|
+
});
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
socket.end();
|
|
83
|
+
}, lingerMs).unref();
|
|
84
|
+
});
|
|
85
|
+
socket.once("close", () => {
|
|
86
|
+
resolve();
|
|
87
|
+
});
|
|
88
|
+
socket.once("finish", () => {
|
|
89
|
+
resolve();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async probe(socketPath) {
|
|
94
|
+
return new Promise((resolve) => {
|
|
95
|
+
const socket = connect(socketPath);
|
|
96
|
+
const done = (value) => {
|
|
97
|
+
socket.destroy();
|
|
98
|
+
resolve(value);
|
|
99
|
+
};
|
|
100
|
+
socket.setTimeout(PROBE_TIMEOUT_MS, () => {
|
|
101
|
+
done(false);
|
|
102
|
+
});
|
|
103
|
+
socket.once("error", (error) => {
|
|
104
|
+
done(error.code === "EBUSY");
|
|
105
|
+
});
|
|
106
|
+
socket.once("connect", () => {
|
|
107
|
+
done(true);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async listen(socketPath, onConnection) {
|
|
112
|
+
const accepted = [];
|
|
113
|
+
const server = createServer((socket) => {
|
|
114
|
+
accepted.push(socket);
|
|
115
|
+
socket.once("close", () => {
|
|
116
|
+
const index = accepted.indexOf(socket);
|
|
117
|
+
if (index >= 0) accepted.splice(index, 1);
|
|
118
|
+
});
|
|
119
|
+
onConnection(new NodeInboundConnection(socket));
|
|
120
|
+
});
|
|
121
|
+
await new Promise((resolve, reject) => {
|
|
122
|
+
server.once("error", reject);
|
|
123
|
+
server.listen(socketPath, () => {
|
|
124
|
+
resolve();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
socketPath,
|
|
129
|
+
close: async () => {
|
|
130
|
+
await new Promise((resolve) => {
|
|
131
|
+
server.close(() => {
|
|
132
|
+
resolve();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
for (const socket of accepted) socket.destroy();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
var SystemClock = class {
|
|
141
|
+
nowMs() {
|
|
142
|
+
return Date.now();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/schemas/define-schema.ts
|
|
147
|
+
/**
|
|
148
|
+
* Attach an `.is()` type guard to a Zod schema so schema, inferred type, and runtime guard derive from one definition (single source of truth). `Schema.parse()` at JSON boundaries; `Schema.is()` for narrowing.
|
|
149
|
+
*/
|
|
150
|
+
function defineSchema(schema) {
|
|
151
|
+
return Object.assign(schema, { is(value) {
|
|
152
|
+
return schema.safeParse(value).success;
|
|
153
|
+
} });
|
|
154
|
+
}
|
|
155
|
+
const hex = (bytes) => bytes * 2;
|
|
156
|
+
const PEER_TOKEN_HEX_LENGTH = hex(16);
|
|
157
|
+
const HOP_ID_HEX_LENGTH = hex(12);
|
|
158
|
+
/**
|
|
159
|
+
* Lint-clean string form of a numeric limit for regex interpolation;
|
|
160
|
+
* `restrict-template-expressions` rejects numbers inside template literals.
|
|
161
|
+
*/
|
|
162
|
+
const count = (n) => String(n);
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/schemas/keyfile.ts
|
|
165
|
+
/** The auth key a session publishes next to its socket. */
|
|
166
|
+
const PeerKeyFileSchema = defineSchema(z.object({
|
|
167
|
+
peerToken: z.string().regex(new RegExp(`^[0-9a-f]{${count(PEER_TOKEN_HEX_LENGTH)}}$`)),
|
|
168
|
+
procStart: z.string().min(1),
|
|
169
|
+
pidDomain: z.string().min(1)
|
|
170
|
+
}));
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/adapters/node/paths.ts
|
|
173
|
+
/** Candidate socket directories, in the order the reference client accepts them. */
|
|
174
|
+
function socketDirCandidates(config = {}) {
|
|
175
|
+
if (config.socketDir !== void 0) return [config.socketDir];
|
|
176
|
+
const runtimeDir = process.env.XDG_RUNTIME_DIR;
|
|
177
|
+
return [
|
|
178
|
+
"/tmp/cc-socks",
|
|
179
|
+
"/private/tmp/cc-socks",
|
|
180
|
+
...runtimeDir !== void 0 ? [`${runtimeDir}/cc-socks`] : []
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
function sessionsDir(config = {}) {
|
|
184
|
+
return join(config.homeDir ?? homedir(), ".claude", "sessions");
|
|
185
|
+
}
|
|
186
|
+
function socketPathForPid(pid, config = {}) {
|
|
187
|
+
return `${socketDirCandidates(config)[0] ?? "/tmp/cc-socks"}/${pid.toString()}.sock`;
|
|
188
|
+
}
|
|
189
|
+
function registryFilePath(pid, config = {}) {
|
|
190
|
+
return join(sessionsDir(config), `${pid.toString()}.json`);
|
|
191
|
+
}
|
|
192
|
+
/** Key files are named pid.sha256-of-canonical-socket-path with a .key suffix. */
|
|
193
|
+
function keyFilePath(socketPath, config = {}) {
|
|
194
|
+
const hash = createHash("sha256").update(socketPath).digest("hex");
|
|
195
|
+
return join(sessionsDir(config), `${pidFromSocketPath(socketPath).toString()}.${hash}.key`);
|
|
196
|
+
}
|
|
197
|
+
function pidFromSocketPath(socketPath) {
|
|
198
|
+
const base = socketPath.split("/").at(-1) ?? "";
|
|
199
|
+
const pid = Number.parseInt(base.replace(/\.sock$/, ""), 10);
|
|
200
|
+
return Number.isNaN(pid) ? 0 : pid;
|
|
201
|
+
}
|
|
202
|
+
/** Temporary-suffix helper keeping pid interpolations string-typed for lint. */
|
|
203
|
+
const tmpSuffix = () => `tmp-${process.pid.toString()}`;
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/adapters/node/fs-key-store.ts
|
|
206
|
+
var FsKeyStore = class {
|
|
207
|
+
config;
|
|
208
|
+
constructor(config = {}) {
|
|
209
|
+
this.config = config;
|
|
210
|
+
}
|
|
211
|
+
async readForSocket(socketPath) {
|
|
212
|
+
let raw;
|
|
213
|
+
try {
|
|
214
|
+
raw = await readFile(keyFilePath(socketPath, this.config), "utf8");
|
|
215
|
+
} catch {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
let parsed;
|
|
219
|
+
try {
|
|
220
|
+
parsed = JSON.parse(raw);
|
|
221
|
+
} catch {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const result = PeerKeyFileSchema.safeParse(parsed);
|
|
225
|
+
return result.success ? result.data : void 0;
|
|
226
|
+
}
|
|
227
|
+
async writeForSocket(socketPath, keyFile) {
|
|
228
|
+
const path = keyFilePath(socketPath, this.config);
|
|
229
|
+
await mkdir(dirname(path), {
|
|
230
|
+
recursive: true,
|
|
231
|
+
mode: 448
|
|
232
|
+
});
|
|
233
|
+
const tmp = `${path}.${tmpSuffix()}`;
|
|
234
|
+
await writeFile(tmp, `${JSON.stringify(keyFile)}\n`, { mode: 384 });
|
|
235
|
+
await rename(tmp, path);
|
|
236
|
+
}
|
|
237
|
+
async removeForSocket(socketPath) {
|
|
238
|
+
await unlink(keyFilePath(socketPath, this.config)).catch(() => void 0);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/schemas/registry.ts
|
|
243
|
+
/** Whitelist the receiver applies on read; other values parse to undefined. */
|
|
244
|
+
const NameSourceSchema = defineSchema(z.enum([
|
|
245
|
+
"user",
|
|
246
|
+
"peer",
|
|
247
|
+
"derived",
|
|
248
|
+
"collision",
|
|
249
|
+
"auto",
|
|
250
|
+
"hook"
|
|
251
|
+
]));
|
|
252
|
+
const PeerStatusSchema = defineSchema(z.enum([
|
|
253
|
+
"busy",
|
|
254
|
+
"shell",
|
|
255
|
+
"idle",
|
|
256
|
+
"waiting"
|
|
257
|
+
]));
|
|
258
|
+
const SessionKindSchema = defineSchema(z.enum([
|
|
259
|
+
"interactive",
|
|
260
|
+
"bg",
|
|
261
|
+
"daemon",
|
|
262
|
+
"daemon-worker"
|
|
263
|
+
]));
|
|
264
|
+
const PeerFeatureSchema = defineSchema(z.enum([
|
|
265
|
+
"notify_idle",
|
|
266
|
+
"reply_across_default_dirs",
|
|
267
|
+
"artifact_yield"
|
|
268
|
+
]));
|
|
269
|
+
/** One entry of the ~/.claude/sessions/<pid>.json registry. */
|
|
270
|
+
const RegistryEntrySchema = defineSchema(z.object({
|
|
271
|
+
pid: z.number().int().positive(),
|
|
272
|
+
sessionId: z.string().min(1),
|
|
273
|
+
cwd: z.string(),
|
|
274
|
+
startedAt: z.number().int().nonnegative(),
|
|
275
|
+
procStart: z.string().min(1),
|
|
276
|
+
version: z.string().min(1),
|
|
277
|
+
peerProtocol: z.number().int(),
|
|
278
|
+
peerFeatures: z.array(PeerFeatureSchema),
|
|
279
|
+
kind: SessionKindSchema,
|
|
280
|
+
entrypoint: z.string(),
|
|
281
|
+
pidDomain: z.string().min(1),
|
|
282
|
+
messagingSocketPath: z.string().min(1),
|
|
283
|
+
name: z.string().optional(),
|
|
284
|
+
nameSource: NameSourceSchema.optional(),
|
|
285
|
+
nameSince: z.number().int().nonnegative().optional(),
|
|
286
|
+
updatedAt: z.number().int().nonnegative(),
|
|
287
|
+
status: PeerStatusSchema.optional(),
|
|
288
|
+
statusUpdatedAt: z.number().int().nonnegative().optional(),
|
|
289
|
+
bridgeSessionId: z.string().optional()
|
|
290
|
+
}));
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region src/adapters/node/fs-registry-store.ts
|
|
293
|
+
var FsRegistryStore = class {
|
|
294
|
+
config;
|
|
295
|
+
constructor(config = {}) {
|
|
296
|
+
this.config = config;
|
|
297
|
+
}
|
|
298
|
+
async list() {
|
|
299
|
+
const dir = sessionsDir(this.config);
|
|
300
|
+
let names;
|
|
301
|
+
try {
|
|
302
|
+
names = await readdir(dir);
|
|
303
|
+
} catch {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
const entries = [];
|
|
307
|
+
for (const name of names) {
|
|
308
|
+
if (!/^\d+\.json$/.test(name)) continue;
|
|
309
|
+
const entry = await this.read(Number.parseInt(name, 10));
|
|
310
|
+
if (entry !== void 0) entries.push(entry);
|
|
311
|
+
}
|
|
312
|
+
return entries;
|
|
313
|
+
}
|
|
314
|
+
async read(pid) {
|
|
315
|
+
let raw;
|
|
316
|
+
try {
|
|
317
|
+
raw = await readFile(registryFilePath(pid, this.config), "utf8");
|
|
318
|
+
} catch {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
let parsed;
|
|
322
|
+
try {
|
|
323
|
+
parsed = JSON.parse(raw);
|
|
324
|
+
} catch {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const result = RegistryEntrySchema.safeParse(parsed);
|
|
328
|
+
return result.success ? result.data : void 0;
|
|
329
|
+
}
|
|
330
|
+
async write(entry) {
|
|
331
|
+
const path = registryFilePath(entry.pid, this.config);
|
|
332
|
+
await mkdir(dirname(path), {
|
|
333
|
+
recursive: true,
|
|
334
|
+
mode: 448
|
|
335
|
+
});
|
|
336
|
+
const tmp = `${path}.${tmpSuffix()}`;
|
|
337
|
+
await writeFile(tmp, `${JSON.stringify(entry)}\n`, { mode: 420 });
|
|
338
|
+
await rename(tmp, path);
|
|
339
|
+
}
|
|
340
|
+
async touch(pid) {
|
|
341
|
+
const entry = await this.read(pid);
|
|
342
|
+
if (entry === void 0) return;
|
|
343
|
+
const now = Date.now();
|
|
344
|
+
await this.write({
|
|
345
|
+
...entry,
|
|
346
|
+
updatedAt: now,
|
|
347
|
+
...entry.status !== void 0 ? { statusUpdatedAt: now } : {}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
async remove(pid) {
|
|
351
|
+
await unlink(registryFilePath(pid, this.config)).catch(() => void 0);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/adapters/node/ps-proc-info.ts
|
|
356
|
+
var PsProcInfo = class PsProcInfo {
|
|
357
|
+
async alive(pid) {
|
|
358
|
+
const errno = (error) => {
|
|
359
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string") return error.code;
|
|
360
|
+
return "";
|
|
361
|
+
};
|
|
362
|
+
return new Promise((resolve) => {
|
|
363
|
+
try {
|
|
364
|
+
process.kill(pid, 0);
|
|
365
|
+
resolve(true);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
resolve(errno(error) === "EPERM");
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* `ps -o lstart=` under forced C locale and UTC, returning the trimmed output byte-exact. The registry's liveness check string-compares this value, so the forced environment is load-bearing: bare `ps` follows the user's locale (day-before-month order under en_GB) and local time.
|
|
373
|
+
*/
|
|
374
|
+
async lstart(pid) {
|
|
375
|
+
return (await this.runPs(pid))?.trim();
|
|
376
|
+
}
|
|
377
|
+
psCache = /* @__PURE__ */ new Map();
|
|
378
|
+
static CACHE_MS = 6e4;
|
|
379
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
380
|
+
async runPs(pid) {
|
|
381
|
+
const cached = this.psCache.get(pid);
|
|
382
|
+
if (cached !== void 0 && Date.now() - cached.at < PsProcInfo.CACHE_MS) return Promise.resolve(cached.value);
|
|
383
|
+
const existing = this.inFlight.get(pid);
|
|
384
|
+
if (existing !== void 0) return existing;
|
|
385
|
+
const promise = new Promise((resolve) => {
|
|
386
|
+
let child;
|
|
387
|
+
try {
|
|
388
|
+
child = spawn("ps", [
|
|
389
|
+
"-o",
|
|
390
|
+
"lstart=",
|
|
391
|
+
"-p",
|
|
392
|
+
String(pid)
|
|
393
|
+
], {
|
|
394
|
+
env: {
|
|
395
|
+
...process.env,
|
|
396
|
+
LC_ALL: "C",
|
|
397
|
+
TZ: "UTC"
|
|
398
|
+
},
|
|
399
|
+
stdio: [
|
|
400
|
+
"ignore",
|
|
401
|
+
"pipe",
|
|
402
|
+
"ignore"
|
|
403
|
+
]
|
|
404
|
+
});
|
|
405
|
+
} catch {
|
|
406
|
+
resolve(void 0);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
let out = "";
|
|
410
|
+
child.stdout?.on("data", (chunk) => {
|
|
411
|
+
out += chunk.toString("utf8");
|
|
412
|
+
});
|
|
413
|
+
child.on("error", () => {
|
|
414
|
+
resolve(void 0);
|
|
415
|
+
});
|
|
416
|
+
child.on("close", (code) => {
|
|
417
|
+
this.psCache.set(pid, {
|
|
418
|
+
at: Date.now(),
|
|
419
|
+
value: out
|
|
420
|
+
});
|
|
421
|
+
resolve(code === 0 && out.trim().length > 0 ? out : void 0);
|
|
422
|
+
});
|
|
423
|
+
}).finally(() => {
|
|
424
|
+
this.inFlight.delete(pid);
|
|
425
|
+
});
|
|
426
|
+
this.inFlight.set(pid, promise);
|
|
427
|
+
return promise;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/schemas/envelope.ts
|
|
432
|
+
/**
|
|
433
|
+
* Grammar of the <cross-session-message> envelope attributes, mirroring the receiver's own parser. The serialized attribute ORDER is canonical (from, from-session, hop-chain, from-name, from-mode): the receiver's regex matches that sequence only, so any other order parses as nothing.
|
|
434
|
+
*/
|
|
435
|
+
const EnvelopeAddressSchema = defineSchema(z.string().regex(new RegExp(`^[A-Za-z0-9%:_/.\\\\-]{1,${count(300)}}$`)));
|
|
436
|
+
const FromModeSchema = defineSchema(z.enum(["bypass", "prompting"]));
|
|
437
|
+
const HopIdSchema = z.string().regex(new RegExp(`^[0-9a-f]{${count(HOP_ID_HEX_LENGTH)}}$`));
|
|
438
|
+
const EnvelopeAttributesSchema = defineSchema(z.object({
|
|
439
|
+
from: EnvelopeAddressSchema,
|
|
440
|
+
fromSession: z.string().regex(new RegExp(`^[A-Za-z0-9_-]{1,${count(80)}}$`)).optional(),
|
|
441
|
+
/** Parsed hop chain: at most 32 ids at the grammar level. */
|
|
442
|
+
hopChain: z.array(HopIdSchema).max(32).optional(),
|
|
443
|
+
fromName: z.string().min(1).max(80).optional(),
|
|
444
|
+
fromMode: FromModeSchema.optional()
|
|
445
|
+
}));
|
|
446
|
+
//#endregion
|
|
447
|
+
//#region src/domain/envelope.ts
|
|
448
|
+
const TAG = "cross-session-message";
|
|
449
|
+
/**
|
|
450
|
+
* The receiver's own parse shape: attributes in canonical order, each value constrained to its grammar, body between newlines. Mirrors `HB` in the reference client, including the round-trip check (rebuild-and-compare). The body is captured verbatim: escaping is applied on build only and is idempotent, so parsed bodies stay in escaped form exactly as sent.
|
|
451
|
+
*/
|
|
452
|
+
const ENVELOPE_RE = new RegExp(`^<${TAG}(?: from="([A-Za-z0-9%:_/.\\\\-]{1,${count(300)}})")?(?: from-session="([A-Za-z0-9_-]{1,${count(80)}})")?(?: hop-chain="([0-9a-f]{${count(HOP_ID_HEX_LENGTH)}}(?:,[0-9a-f]{${count(HOP_ID_HEX_LENGTH)}}){0,${count(31)}})")?(?: from-name="([^"<>\\n\\r]{1,${count(80)}})")?(?: from-mode="(bypass|prompting)")?>\\n([\\s\\S]*)\\n</${TAG}>$`);
|
|
453
|
+
/** Occurrences of the closing tag inside a body are escaped to a literal `<\`. Idempotent. */
|
|
454
|
+
function escapeBody(body) {
|
|
455
|
+
return body.replaceAll(`</${TAG}`, "<\\");
|
|
456
|
+
}
|
|
457
|
+
/** Serialize attributes in the canonical order the receiver's regex requires. */
|
|
458
|
+
function serializeAttributes(attrs) {
|
|
459
|
+
const parts = [];
|
|
460
|
+
parts.push(` from="${attrs.from}"`);
|
|
461
|
+
if (attrs.fromSession !== void 0) parts.push(` from-session="${attrs.fromSession}"`);
|
|
462
|
+
if (attrs.hopChain !== void 0 && attrs.hopChain.length > 0) parts.push(` hop-chain="${attrs.hopChain.join(",")}"`);
|
|
463
|
+
if (attrs.fromName !== void 0) parts.push(` from-name="${attrs.fromName.replaceAll("\"", "")}"`);
|
|
464
|
+
if (attrs.fromMode !== void 0) parts.push(` from-mode="${attrs.fromMode}"`);
|
|
465
|
+
return parts.join("");
|
|
466
|
+
}
|
|
467
|
+
function buildEnvelope(attrs, body) {
|
|
468
|
+
const validated = EnvelopeAttributesSchema.parse(attrs);
|
|
469
|
+
return `<${TAG}${serializeAttributes(validated)}>\n${escapeBody(body)}\n</${TAG}>`;
|
|
470
|
+
}
|
|
471
|
+
function parseEnvelope(content) {
|
|
472
|
+
const match = ENVELOPE_RE.exec(content);
|
|
473
|
+
if (match === null) return void 0;
|
|
474
|
+
const parsed = { body: match[6] ?? "" };
|
|
475
|
+
if (match[1] !== void 0) parsed.from = match[1];
|
|
476
|
+
if (match[2] !== void 0) parsed.fromSession = match[2];
|
|
477
|
+
if (match[3] !== void 0) parsed.hopChain = match[3].split(",");
|
|
478
|
+
if (match[4] !== void 0) parsed.fromName = match[4];
|
|
479
|
+
if (match[5] === "bypass" || match[5] === "prompting") parsed.fromMode = match[5];
|
|
480
|
+
return parsed;
|
|
481
|
+
}
|
|
482
|
+
const DEFAULT_REFILL_PER_SECOND = .5;
|
|
483
|
+
const MS_PER_SECOND = 1e3;
|
|
484
|
+
/**
|
|
485
|
+
* Outbound token bucket mirroring the receiver's admission rate limit (30 tokens, 0.5/s refill): sending faster than the target accepts gets messages dropped with reason "rate-limited". reserve() blocks until a token is available; a negative wait means tokens are already full.
|
|
486
|
+
*/
|
|
487
|
+
var Pacer = class {
|
|
488
|
+
clock;
|
|
489
|
+
capacity;
|
|
490
|
+
refillPerSecond;
|
|
491
|
+
tokens;
|
|
492
|
+
lastRefillMs;
|
|
493
|
+
constructor(clock, capacity = 30, refillPerSecond = DEFAULT_REFILL_PER_SECOND) {
|
|
494
|
+
this.clock = clock;
|
|
495
|
+
this.capacity = capacity;
|
|
496
|
+
this.refillPerSecond = refillPerSecond;
|
|
497
|
+
this.tokens = capacity;
|
|
498
|
+
this.lastRefillMs = clock.nowMs();
|
|
499
|
+
}
|
|
500
|
+
/** Milliseconds to wait before one token is available (0 = now). */
|
|
501
|
+
msUntilNextToken() {
|
|
502
|
+
this.refill();
|
|
503
|
+
if (this.tokens >= 1) return 0;
|
|
504
|
+
const deficit = 1 - this.tokens;
|
|
505
|
+
return Math.ceil(deficit / this.refillPerSecond * MS_PER_SECOND);
|
|
506
|
+
}
|
|
507
|
+
/** Consume one token if available. */
|
|
508
|
+
tryReserve() {
|
|
509
|
+
this.refill();
|
|
510
|
+
if (this.tokens < 1) return false;
|
|
511
|
+
this.tokens -= 1;
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
refill() {
|
|
515
|
+
const now = this.clock.nowMs();
|
|
516
|
+
const elapsedSeconds = (now - this.lastRefillMs) / MS_PER_SECOND;
|
|
517
|
+
if (elapsedSeconds <= 0) return;
|
|
518
|
+
this.tokens = Math.min(this.capacity, this.tokens + elapsedSeconds * this.refillPerSecond);
|
|
519
|
+
this.lastRefillMs = now;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/domain/ids.ts
|
|
524
|
+
/** Message ids are UUID v4, matching the reference client's `qM()`. */
|
|
525
|
+
function newMsgId() {
|
|
526
|
+
return randomUUID();
|
|
527
|
+
}
|
|
528
|
+
//#endregion
|
|
529
|
+
//#region src/domain/roster.ts
|
|
530
|
+
/**
|
|
531
|
+
* The receiver's own admission rules, verified live: an entry is listed when it has a socket, is not the caller's own, is not spare/parked, its socket accepts a live connect probe, and its pid is alive with a procStart that byte-matches the ps output. Mismatches classify as recycled and are silently skipped by the reference roster builder.
|
|
532
|
+
*/
|
|
533
|
+
async function filterRoster(entries, probes) {
|
|
534
|
+
const verdicts = [];
|
|
535
|
+
for (const entry of entries) {
|
|
536
|
+
const verdict = await checkEntry(entry, probes);
|
|
537
|
+
verdicts.push(verdict);
|
|
538
|
+
}
|
|
539
|
+
return verdicts.filter((v) => v.admitted).map((v) => v.entry);
|
|
540
|
+
}
|
|
541
|
+
async function checkEntry(entry, probes) {
|
|
542
|
+
if (entry.messagingSocketPath.length === 0) return {
|
|
543
|
+
entry,
|
|
544
|
+
admitted: false,
|
|
545
|
+
reason: "no-socket"
|
|
546
|
+
};
|
|
547
|
+
if (probes.ownSocketPath !== void 0 && entry.messagingSocketPath === probes.ownSocketPath) return {
|
|
548
|
+
entry,
|
|
549
|
+
admitted: false,
|
|
550
|
+
reason: "own-socket"
|
|
551
|
+
};
|
|
552
|
+
if (!await probes.procInfo.alive(entry.pid)) return {
|
|
553
|
+
entry,
|
|
554
|
+
admitted: false,
|
|
555
|
+
reason: "pid-dead"
|
|
556
|
+
};
|
|
557
|
+
const lstart = await probes.procInfo.lstart(entry.pid);
|
|
558
|
+
if (lstart === void 0 || lstart !== entry.procStart) return {
|
|
559
|
+
entry,
|
|
560
|
+
admitted: false,
|
|
561
|
+
reason: "proc-start-mismatch"
|
|
562
|
+
};
|
|
563
|
+
if (!await probes.transport.probe(entry.messagingSocketPath)) return {
|
|
564
|
+
entry,
|
|
565
|
+
admitted: false,
|
|
566
|
+
reason: "socket-dead"
|
|
567
|
+
};
|
|
568
|
+
return {
|
|
569
|
+
entry,
|
|
570
|
+
admitted: true
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
//#endregion
|
|
574
|
+
//#region src/schemas/wire.ts
|
|
575
|
+
/** Line 1 of every connection: the receiver's own peerToken (peer class) or childToken (self-sent class). */
|
|
576
|
+
const AuthLineSchema = defineSchema(z.object({
|
|
577
|
+
type: z.literal("auth"),
|
|
578
|
+
token: z.string().regex(new RegExp(`^[0-9a-f]{${count(PEER_TOKEN_HEX_LENGTH)}}$`))
|
|
579
|
+
}));
|
|
580
|
+
const PrioritySchema = defineSchema(z.enum(["next", "later"]));
|
|
581
|
+
const FileAttachmentSchema = defineSchema(z.object({
|
|
582
|
+
path: z.string().min(1),
|
|
583
|
+
file_name: z.string().min(1),
|
|
584
|
+
file_size: z.number().int().nonnegative(),
|
|
585
|
+
sha256: z.string().regex(new RegExp(`^[0-9a-f]{${count(64)}}$`)),
|
|
586
|
+
media_type: z.string().optional()
|
|
587
|
+
}));
|
|
588
|
+
/** A user-turn message. `"type": "user"` is load-bearing: any other value is silently dropped. */
|
|
589
|
+
const UserFrameSchema = defineSchema(z.object({
|
|
590
|
+
msgV: z.number().int(),
|
|
591
|
+
msg_id: z.string().min(1),
|
|
592
|
+
type: z.literal("user"),
|
|
593
|
+
message: z.object({
|
|
594
|
+
role: z.literal("user"),
|
|
595
|
+
content: z.string()
|
|
596
|
+
}),
|
|
597
|
+
priority: PrioritySchema,
|
|
598
|
+
from: z.string().regex(new RegExp(`^[A-Za-z0-9%:_/.\\\\-]{1,${count(300)}}$`)),
|
|
599
|
+
/** When present, must match the receiver's sessionId or the frame is silently dropped. */
|
|
600
|
+
session_id: z.string().optional(),
|
|
601
|
+
file_attachments: z.array(FileAttachmentSchema).max(16).optional()
|
|
602
|
+
}));
|
|
603
|
+
const DropReasonSchema = defineSchema(z.enum([
|
|
604
|
+
"rate-limited",
|
|
605
|
+
"duplicate",
|
|
606
|
+
"hop-loop",
|
|
607
|
+
"hop-runaway",
|
|
608
|
+
"queue-full"
|
|
609
|
+
]));
|
|
610
|
+
const PeerMessageStatusSchema = defineSchema(z.object({
|
|
611
|
+
type: z.literal("control"),
|
|
612
|
+
action: z.literal("peer_message_status"),
|
|
613
|
+
status: z.enum([
|
|
614
|
+
"held",
|
|
615
|
+
"delivered",
|
|
616
|
+
"denied",
|
|
617
|
+
"expired",
|
|
618
|
+
"dropped"
|
|
619
|
+
]),
|
|
620
|
+
reason: z.string(),
|
|
621
|
+
from: z.string().regex(new RegExp(`^[A-Za-z0-9%:_/.\\\\-]{1,${count(300)}}$`)),
|
|
622
|
+
orig_msg_id: z.string().min(1),
|
|
623
|
+
status_detail: z.string().optional(),
|
|
624
|
+
drop_reason: DropReasonSchema.optional(),
|
|
625
|
+
dropped_msg_ids: z.array(z.string()).optional(),
|
|
626
|
+
msgV: z.number().int(),
|
|
627
|
+
msg_id: z.string().min(1)
|
|
628
|
+
}));
|
|
629
|
+
const NotifyWhenIdleSchema = defineSchema(z.object({
|
|
630
|
+
type: z.literal("control"),
|
|
631
|
+
action: z.literal("notify_when_idle"),
|
|
632
|
+
from: z.string().regex(new RegExp(`^[A-Za-z0-9%:_/.\\\\-]{1,${count(300)}}$`)),
|
|
633
|
+
from_mode: z.enum(["bypass", "prompting"]).optional(),
|
|
634
|
+
msgV: z.number().int(),
|
|
635
|
+
msg_id: z.string().min(1)
|
|
636
|
+
}));
|
|
637
|
+
const PeerIdleNoticeSchema = defineSchema(z.object({
|
|
638
|
+
type: z.literal("control"),
|
|
639
|
+
action: z.literal("peer_idle_notice"),
|
|
640
|
+
orig_msg_id: z.string().min(1),
|
|
641
|
+
state: z.enum(["idle", "exited"]),
|
|
642
|
+
finished_at: z.number(),
|
|
643
|
+
detail: z.string().optional(),
|
|
644
|
+
from: z.string().regex(new RegExp(`^[A-Za-z0-9%:_/.\\\\-]{1,${count(300)}}$`)),
|
|
645
|
+
from_mode: z.enum(["bypass", "prompting"]).optional(),
|
|
646
|
+
msgV: z.number().int(),
|
|
647
|
+
msg_id: z.string().min(1)
|
|
648
|
+
}));
|
|
649
|
+
const YieldArtifactRepliesSchema = defineSchema(z.object({
|
|
650
|
+
type: z.literal("control"),
|
|
651
|
+
action: z.literal("yield_artifact_replies"),
|
|
652
|
+
from: z.string().max(512),
|
|
653
|
+
msg_id: z.string().min(1).max(128),
|
|
654
|
+
session_id: z.string().max(512),
|
|
655
|
+
slugs: z.array(z.string().max(128)).max(16),
|
|
656
|
+
reason: z.enum(["resume", "claim"]).catch("claim"),
|
|
657
|
+
sent_at: z.number(),
|
|
658
|
+
claimed_at: z.number().optional(),
|
|
659
|
+
requester: z.object({
|
|
660
|
+
cwd: z.string().optional(),
|
|
661
|
+
tmux: z.string().optional()
|
|
662
|
+
}).optional(),
|
|
663
|
+
msgV: z.number().int()
|
|
664
|
+
}));
|
|
665
|
+
const ArtifactRepliesYieldedSchema = defineSchema(z.object({
|
|
666
|
+
type: z.literal("control"),
|
|
667
|
+
action: z.literal("artifact_replies_yielded"),
|
|
668
|
+
orig_msg_id: z.string().max(128),
|
|
669
|
+
yielded: z.string().optional(),
|
|
670
|
+
not_held: z.string().optional(),
|
|
671
|
+
refused: z.string().optional(),
|
|
672
|
+
msgV: z.number().int(),
|
|
673
|
+
msg_id: z.string().min(1),
|
|
674
|
+
from: z.string().optional()
|
|
675
|
+
}));
|
|
676
|
+
const UnyieldArtifactRepliesSchema = defineSchema(z.object({
|
|
677
|
+
type: z.literal("control"),
|
|
678
|
+
action: z.literal("unyield_artifact_replies"),
|
|
679
|
+
orig_msg_id: z.string().max(128),
|
|
680
|
+
slugs: z.array(z.string().max(128)).max(16),
|
|
681
|
+
stopped: z.boolean().optional(),
|
|
682
|
+
msgV: z.number().int(),
|
|
683
|
+
msg_id: z.string().min(1),
|
|
684
|
+
from: z.string().optional()
|
|
685
|
+
}));
|
|
686
|
+
const ControlFrameSchema = z.union([
|
|
687
|
+
PeerMessageStatusSchema,
|
|
688
|
+
NotifyWhenIdleSchema,
|
|
689
|
+
PeerIdleNoticeSchema,
|
|
690
|
+
YieldArtifactRepliesSchema,
|
|
691
|
+
ArtifactRepliesYieldedSchema,
|
|
692
|
+
UnyieldArtifactRepliesSchema
|
|
693
|
+
]);
|
|
694
|
+
const WireFrameSchema = z.union([UserFrameSchema, ControlFrameSchema]);
|
|
695
|
+
//#endregion
|
|
696
|
+
//#region src/errors.ts
|
|
697
|
+
/** Base error for every cc-peer failure; `code` is machine-readable. */
|
|
698
|
+
var CcPeerError = class extends Error {
|
|
699
|
+
code;
|
|
700
|
+
constructor(code, message) {
|
|
701
|
+
super(message);
|
|
702
|
+
this.code = code;
|
|
703
|
+
this.name = "CcPeerError";
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
var NoLiveInboxError = class extends CcPeerError {
|
|
707
|
+
constructor(message) {
|
|
708
|
+
super("NO_LIVE_INBOX", message);
|
|
709
|
+
this.name = "NoLiveInboxError";
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
var UnknownPeerError = class extends CcPeerError {
|
|
713
|
+
constructor(message) {
|
|
714
|
+
super("UNKNOWN_PEER", message);
|
|
715
|
+
this.name = "UnknownPeerError";
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
var MessageTooLargeError = class extends CcPeerError {
|
|
719
|
+
constructor(message) {
|
|
720
|
+
super("MESSAGE_TOO_LARGE", message);
|
|
721
|
+
this.name = "MessageTooLargeError";
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
var NotStartedError = class extends CcPeerError {
|
|
725
|
+
constructor(message) {
|
|
726
|
+
super("NOT_STARTED", message);
|
|
727
|
+
this.name = "NotStartedError";
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
//#endregion
|
|
731
|
+
//#region src/cc-peer.ts
|
|
732
|
+
/** Package version placeholder until semantic-release owns it. */
|
|
733
|
+
const CC_PEER_VERSION = "0.0.0";
|
|
734
|
+
/** Default log sink: logs go nowhere unless a logger is provided. */
|
|
735
|
+
const sinkLog = () => void 0;
|
|
736
|
+
/** Peer tokens are 16 random bytes in hex, matching the reference key files. */
|
|
737
|
+
const PEER_TOKEN_BYTES = 16;
|
|
738
|
+
/** Byte cap on the serialized frame line, mirroring the receiver's line guard. */
|
|
739
|
+
const MAX_FRAME_CHARS = 12e4;
|
|
740
|
+
const DEFAULT_HEARTBEAT_MS = 15e3;
|
|
741
|
+
/**
|
|
742
|
+
* A registered peer on Claude Code's cross-session mesh. Receipts and idle notices only reach the process that owns the listening socket (the protocol verifies return addresses via kernel peer pids), so do not split sending and listening across differently-owned processes.
|
|
743
|
+
*/
|
|
744
|
+
var CcPeer = class CcPeer extends EventEmitter {
|
|
745
|
+
options;
|
|
746
|
+
deps;
|
|
747
|
+
listening;
|
|
748
|
+
heartbeatTimer;
|
|
749
|
+
ownKey;
|
|
750
|
+
pacer;
|
|
751
|
+
log;
|
|
752
|
+
stopped = false;
|
|
753
|
+
constructor(options, deps) {
|
|
754
|
+
super();
|
|
755
|
+
this.options = options;
|
|
756
|
+
this.deps = deps;
|
|
757
|
+
this.pacer = new Pacer(deps.clock);
|
|
758
|
+
this.log = options.logger ?? sinkLog;
|
|
759
|
+
}
|
|
760
|
+
static async create(options = {}) {
|
|
761
|
+
const peer = new CcPeer(options, {
|
|
762
|
+
transport: new UdsTransport(),
|
|
763
|
+
registry: new FsRegistryStore(options),
|
|
764
|
+
keys: new FsKeyStore(options),
|
|
765
|
+
procInfo: new PsProcInfo(),
|
|
766
|
+
clock: new SystemClock()
|
|
767
|
+
});
|
|
768
|
+
await peer.start();
|
|
769
|
+
return peer;
|
|
770
|
+
}
|
|
771
|
+
async start() {
|
|
772
|
+
const socketPath = socketPathForPid(process.pid, this.options);
|
|
773
|
+
this.ownKey = {
|
|
774
|
+
peerToken: randomBytes(PEER_TOKEN_BYTES).toString("hex"),
|
|
775
|
+
procStart: await this.deps.procInfo.lstart(process.pid) ?? "",
|
|
776
|
+
pidDomain: "darwin"
|
|
777
|
+
};
|
|
778
|
+
if (this.ownKey.procStart === "") throw new NotStartedError("could not read own procStart via ps");
|
|
779
|
+
await this.deps.keys.writeForSocket(socketPath, this.ownKey);
|
|
780
|
+
await mkdir(dirname(socketPath), {
|
|
781
|
+
recursive: true,
|
|
782
|
+
mode: 448
|
|
783
|
+
});
|
|
784
|
+
const entry = this.buildRegistryEntry();
|
|
785
|
+
await this.deps.registry.write(entry);
|
|
786
|
+
this.listening = await this.deps.transport.listen(socketPath, (conn) => {
|
|
787
|
+
this.handleConnection(conn);
|
|
788
|
+
});
|
|
789
|
+
this.heartbeatTimer = setInterval(() => {
|
|
790
|
+
this.deps.registry.touch(process.pid).catch(() => void 0);
|
|
791
|
+
}, this.options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS);
|
|
792
|
+
this.heartbeatTimer.unref();
|
|
793
|
+
this.log(`listening as ${this.options.name ?? "unnamed"} at ${socketPath}`);
|
|
794
|
+
}
|
|
795
|
+
buildRegistryEntry() {
|
|
796
|
+
const now = this.deps.clock.nowMs();
|
|
797
|
+
return {
|
|
798
|
+
pid: process.pid,
|
|
799
|
+
sessionId: this.options.sessionId ?? newMsgId(),
|
|
800
|
+
cwd: process.cwd(),
|
|
801
|
+
startedAt: now,
|
|
802
|
+
procStart: this.ownKey?.procStart ?? "",
|
|
803
|
+
version: "cc-peer",
|
|
804
|
+
peerProtocol: 1,
|
|
805
|
+
peerFeatures: ["notify_idle", "reply_across_default_dirs"],
|
|
806
|
+
kind: "interactive",
|
|
807
|
+
entrypoint: "cli",
|
|
808
|
+
pidDomain: "darwin",
|
|
809
|
+
messagingSocketPath: socketPathForPid(process.pid, this.options),
|
|
810
|
+
...this.options.name !== void 0 ? {
|
|
811
|
+
name: this.options.name,
|
|
812
|
+
nameSource: "user",
|
|
813
|
+
nameSince: now
|
|
814
|
+
} : {},
|
|
815
|
+
updatedAt: now,
|
|
816
|
+
status: "idle",
|
|
817
|
+
statusUpdatedAt: now
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
async roster() {
|
|
821
|
+
return filterRoster(await this.deps.registry.list(), {
|
|
822
|
+
transport: this.deps.transport,
|
|
823
|
+
procInfo: this.deps.procInfo,
|
|
824
|
+
...this.listening !== void 0 ? { ownSocketPath: this.listening.socketPath } : {}
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
async send(target, body, options = {}) {
|
|
828
|
+
if (this.stopped || this.listening === void 0 || this.ownKey === void 0) throw new NotStartedError("peer is not running");
|
|
829
|
+
const socketPath = await this.resolveTarget(target);
|
|
830
|
+
const key = await this.deps.keys.readForSocket(socketPath);
|
|
831
|
+
const msgId = newMsgId();
|
|
832
|
+
const from = `uds:${this.listening.socketPath}`;
|
|
833
|
+
const frame = {
|
|
834
|
+
msgV: 1,
|
|
835
|
+
msg_id: msgId,
|
|
836
|
+
type: "user",
|
|
837
|
+
message: {
|
|
838
|
+
role: "user",
|
|
839
|
+
content: buildEnvelope({
|
|
840
|
+
from,
|
|
841
|
+
...(options.sessionId ?? this.options.sessionId) !== void 0 ? { fromSession: options.sessionId ?? this.options.sessionId } : {},
|
|
842
|
+
...this.options.name !== void 0 ? { fromName: this.options.name } : {},
|
|
843
|
+
...options.fromMode !== false && options.fromMode !== void 0 ? { fromMode: options.fromMode } : {},
|
|
844
|
+
...options.fromMode === void 0 ? { fromMode: "bypass" } : {}
|
|
845
|
+
}, body)
|
|
846
|
+
},
|
|
847
|
+
priority: options.priority ?? "next",
|
|
848
|
+
from
|
|
849
|
+
};
|
|
850
|
+
if (key === void 0) throw new NoLiveInboxError(`no auth key published for ${socketPath}`);
|
|
851
|
+
const auth = AuthLineSchema.parse({
|
|
852
|
+
type: "auth",
|
|
853
|
+
token: key.peerToken
|
|
854
|
+
});
|
|
855
|
+
const line = JSON.stringify(frame);
|
|
856
|
+
if (line.length > MAX_FRAME_CHARS) throw new MessageTooLargeError(`serialized frame is ${line.length.toString()} chars, cap ${MAX_FRAME_CHARS.toString()}`);
|
|
857
|
+
await this.pacedSend(socketPath, [JSON.stringify(auth), line]);
|
|
858
|
+
return { msgId };
|
|
859
|
+
}
|
|
860
|
+
async pacedSend(socketPath, lines) {
|
|
861
|
+
const waitMs = this.pacer.msUntilNextToken();
|
|
862
|
+
if (waitMs > 0) await new Promise((resolve) => {
|
|
863
|
+
setTimeout(() => {
|
|
864
|
+
resolve();
|
|
865
|
+
}, waitMs).unref();
|
|
866
|
+
});
|
|
867
|
+
if (!this.pacer.tryReserve()) {
|
|
868
|
+
await this.pacedSend(socketPath, lines);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
await this.deps.transport.connectWrite(socketPath, lines);
|
|
872
|
+
}
|
|
873
|
+
async subscribeIdle(target) {
|
|
874
|
+
if (this.stopped || this.listening === void 0 || this.ownKey === void 0) throw new NotStartedError("peer is not running");
|
|
875
|
+
const socketPath = await this.resolveTarget(target);
|
|
876
|
+
const key = await this.deps.keys.readForSocket(socketPath);
|
|
877
|
+
if (key === void 0) throw new NoLiveInboxError(`no auth key published for ${socketPath}`);
|
|
878
|
+
const msgId = newMsgId();
|
|
879
|
+
const frame = {
|
|
880
|
+
msgV: 1,
|
|
881
|
+
msg_id: msgId,
|
|
882
|
+
type: "control",
|
|
883
|
+
action: "notify_when_idle",
|
|
884
|
+
from: `uds:${this.listening.socketPath}`,
|
|
885
|
+
from_mode: "bypass"
|
|
886
|
+
};
|
|
887
|
+
const auth = AuthLineSchema.parse({
|
|
888
|
+
type: "auth",
|
|
889
|
+
token: key.peerToken
|
|
890
|
+
});
|
|
891
|
+
await this.deps.transport.connectWrite(socketPath, [JSON.stringify(auth), JSON.stringify(frame)]);
|
|
892
|
+
return { msgId };
|
|
893
|
+
}
|
|
894
|
+
async resolveTarget(target) {
|
|
895
|
+
if ("pid" in target) return socketPathForPid(target.pid, this.options);
|
|
896
|
+
if ("address" in target) return target.address.replace(/^uds:/, "");
|
|
897
|
+
const match = (await this.roster()).find((entry) => entry.name === target.name);
|
|
898
|
+
if (match === void 0) throw new UnknownPeerError(`no roster entry named ${target.name}`);
|
|
899
|
+
return match.messagingSocketPath;
|
|
900
|
+
}
|
|
901
|
+
async handleConnection(conn) {
|
|
902
|
+
const lines = conn.readLines();
|
|
903
|
+
const first = await lines[Symbol.asyncIterator]().next();
|
|
904
|
+
if (first.done === true) return;
|
|
905
|
+
if (this.ownKey !== void 0) {
|
|
906
|
+
const parsed = JSON.parse(first.value);
|
|
907
|
+
if (AuthLineSchema.is(parsed) && parsed.token !== this.ownKey.peerToken) this.log("inbound auth token mismatch (foreign token tolerated)");
|
|
908
|
+
}
|
|
909
|
+
for await (const line of lines) {
|
|
910
|
+
let frame;
|
|
911
|
+
try {
|
|
912
|
+
frame = JSON.parse(line);
|
|
913
|
+
} catch {
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
if (!WireFrameSchema.safeParse(frame).success) continue;
|
|
917
|
+
if (UserFrameSchema.is(frame)) {
|
|
918
|
+
const envelope = parseEnvelope(frame.message.content);
|
|
919
|
+
this.emit("message", {
|
|
920
|
+
...envelope?.from !== void 0 ? { from: envelope.from } : {},
|
|
921
|
+
...envelope?.fromSession !== void 0 ? { fromSession: envelope.fromSession } : {},
|
|
922
|
+
...envelope?.fromName !== void 0 ? { fromName: envelope.fromName } : {},
|
|
923
|
+
...envelope?.fromMode !== void 0 ? { fromMode: envelope.fromMode } : {},
|
|
924
|
+
...envelope?.hopChain !== void 0 ? { hopChain: envelope.hopChain } : {},
|
|
925
|
+
body: envelope?.body ?? frame.message.content,
|
|
926
|
+
msgId: frame.msg_id
|
|
927
|
+
});
|
|
928
|
+
} else if (PeerMessageStatusSchema.is(frame)) this.emit("receipt", frame);
|
|
929
|
+
else if (PeerIdleNoticeSchema.is(frame)) this.emit("idle", frame);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
async stop() {
|
|
933
|
+
if (this.stopped) return;
|
|
934
|
+
this.stopped = true;
|
|
935
|
+
if (this.heartbeatTimer !== void 0) clearInterval(this.heartbeatTimer);
|
|
936
|
+
if (this.listening !== void 0) {
|
|
937
|
+
const socketPath = this.listening.socketPath;
|
|
938
|
+
await this.listening.close();
|
|
939
|
+
await this.deps.keys.removeForSocket(socketPath);
|
|
940
|
+
await this.deps.registry.remove(process.pid);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
//#endregion
|
|
945
|
+
export { defineSchema as a, RegistryEntrySchema as i, CcPeer as n, PrioritySchema as r, CC_PEER_VERSION as t };
|