cc-peer 1.1.9 → 1.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.
- package/README.md +1 -1
- package/dist/bin/cc-peer.cjs +1 -1
- package/dist/bin/cc-peer.mjs +1 -1
- package/dist/{cc-peer-CWGY6H7F.mjs → cc-peer-CJqStHIL.mjs} +103 -37
- package/dist/{cc-peer-IGtoK_0q.cjs → cc-peer-CXzxAUE4.cjs} +103 -37
- package/dist/cc-peer.cjs +1 -1
- package/dist/cc-peer.d.cts +3 -3
- package/dist/cc-peer.d.mts +3 -3
- package/dist/cc-peer.mjs +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -61,6 +61,6 @@ The REST facade (`npx cc-peer`) serves `GET /sessions`, `POST /messages`, `POST
|
|
|
61
61
|
|
|
62
62
|
- **Same-process constraint**: receipts and idle notices only reach the process that owns the peer's listening socket (the protocol verifies return addresses via kernel peer-pids). Do not split `CcPeer` listening and sending across processes or differently-owned workers.
|
|
63
63
|
- **Single machine**: the local protocol is Unix-socket only. Writing to cloud sessions directly is blocked by design (device-attestation-signed events); bridged sessions reachable locally still work via their local mirror.
|
|
64
|
-
- **
|
|
64
|
+
- **Windows uses a named pipe, not a Unix socket**: Node's `net` module has no real AF_UNIX support on Windows (its local domain there is a named pipe, under `\\.\pipe\`, not an arbitrary filesystem path — [nodejs/node#55979](https://github.com/nodejs/node/issues/55979)), and Claude Code's own docs confirm it uses exactly that on native Windows. `cc-peer` branches to a named pipe there automatically; nothing to configure. Windows also requires a valid, matching auth line on every inbound connection (macOS and Linux tolerate an absent or foreign one). The exact `procStart` string format `cc-peer` computes on Windows is its own convention (PowerShell's process start time, ISO-8601) rather than a confirmed match for a real native-Windows Claude Code session's own registry entries, which is not publicly documented.
|
|
65
65
|
- **File transfers to Claude sessions** wait on an upstream feature flag (`tengu_send_file`) before Claude-side materialisation activates; peer-to-peer transfers work today.
|
|
66
66
|
- Verified against Claude Code 2.1.269; treat every Claude Code upgrade as a potential protocol change.
|
package/dist/bin/cc-peer.cjs
CHANGED
|
@@ -21,7 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
|
-
const require_cc_peer = require("../cc-peer-
|
|
24
|
+
const require_cc_peer = require("../cc-peer-CXzxAUE4.cjs");
|
|
25
25
|
let node_crypto = require("node:crypto");
|
|
26
26
|
let zod = require("zod");
|
|
27
27
|
let node_process = require("node:process");
|
package/dist/bin/cc-peer.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as defineSchema, i as RegistryEntrySchema, n as CcPeer, r as PrioritySchema } from "../cc-peer-
|
|
2
|
+
import { a as defineSchema, i as RegistryEntrySchema, n as CcPeer, r as PrioritySchema } from "../cc-peer-CJqStHIL.mjs";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import process from "node:process";
|
|
@@ -157,10 +157,17 @@ const PeerKeyFileSchema = defineSchema(z.object({
|
|
|
157
157
|
}));
|
|
158
158
|
//#endregion
|
|
159
159
|
//#region src/adapters/node/paths.ts
|
|
160
|
+
/** Named-pipe prefix Windows requires; a listen()/connect() path must live under it. */
|
|
161
|
+
const WINDOWS_PIPE_PREFIX = "\\\\.\\pipe\\";
|
|
162
|
+
function isWindows() {
|
|
163
|
+
return process.platform === "win32";
|
|
164
|
+
}
|
|
160
165
|
/**
|
|
161
166
|
* Candidate socket directories, in the order the reference client accepts
|
|
162
167
|
* them. The tuple return type guarantees at least one candidate exists, so
|
|
163
|
-
* callers can index [0] without a fallback branch.
|
|
168
|
+
* callers can index [0] without a fallback branch. Meaningless on Windows,
|
|
169
|
+
* where a named pipe has no filesystem directory of its own — callers there
|
|
170
|
+
* use socketPathForPid directly instead of building a path from a directory.
|
|
164
171
|
*/
|
|
165
172
|
function socketDirCandidates(config = {}) {
|
|
166
173
|
if (config.socketDir !== void 0) return [config.socketDir];
|
|
@@ -174,7 +181,23 @@ function socketDirCandidates(config = {}) {
|
|
|
174
181
|
function sessionsDir(config = {}) {
|
|
175
182
|
return join(config.homeDir ?? homedir(), ".claude", "sessions");
|
|
176
183
|
}
|
|
184
|
+
/** Long enough that two distinct socketDir values collide only by the same astronomically small chance any truncated hash does; short enough to keep the pipe name readable. */
|
|
185
|
+
const PIPE_NAMESPACE_HEX_LENGTH = 8;
|
|
186
|
+
/**
|
|
187
|
+
* Short, path-safe token distinguishing one caller-supplied socketDir from another in a Windows pipe name. A named pipe has no filesystem directory of its own to carry that distinction the way a POSIX socket path does, so the directory is folded into the name instead.
|
|
188
|
+
*/
|
|
189
|
+
function pipeNamespace(socketDir) {
|
|
190
|
+
return createHash("sha256").update(socketDir).digest("hex").slice(0, PIPE_NAMESPACE_HEX_LENGTH);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* On native Windows, Claude Code's own inbox is a named pipe rather than a Unix domain socket — Node's net module has no filesystem-path AF_UNIX support there at all (see docs/PROTOCOL.md), so unlike every other config override in this module, an explicit socketDir can never become a literal filesystem path on Windows: net.Server.listen() would reject it with EACCES regardless of what directory it names. Node still dispatches to the right OS primitive from the path's own shape, so UdsTransport needs no change; only path construction does. A caller-supplied socketDir keeps its usual purpose — separating one caller's sockets from another's, e.g. across concurrent test runs — by namespacing the pipe name instead of pointing at a real directory. The exact pipe name only needs to be unique per pid (and per socketDir) on this machine, not to match any specific value a real Windows Claude Code session uses.
|
|
194
|
+
*/
|
|
177
195
|
function socketPathForPid(pid, config = {}) {
|
|
196
|
+
if (isWindows()) {
|
|
197
|
+
const namespace = config.socketDir === void 0 ? "" : `-${pipeNamespace(config.socketDir)}`;
|
|
198
|
+
return `${WINDOWS_PIPE_PREFIX}cc-peer${namespace}-${pid.toString()}`;
|
|
199
|
+
}
|
|
200
|
+
if (config.socketDir !== void 0) return `${config.socketDir}/${pid.toString()}.sock`;
|
|
178
201
|
return `${socketDirCandidates(config)[0]}/${pid.toString()}.sock`;
|
|
179
202
|
}
|
|
180
203
|
function registryFilePath(pid, config = {}) {
|
|
@@ -186,6 +209,11 @@ function keyFilePath(socketPath, config = {}) {
|
|
|
186
209
|
return join(sessionsDir(config), `${pidFromSocketPath(socketPath).toString()}.${hash}.key`);
|
|
187
210
|
}
|
|
188
211
|
function pidFromSocketPath(socketPath) {
|
|
212
|
+
if (socketPath.startsWith(WINDOWS_PIPE_PREFIX)) {
|
|
213
|
+
const name = socketPath.slice(9);
|
|
214
|
+
const pid = Number.parseInt(name.slice(name.lastIndexOf("-") + 1), 10);
|
|
215
|
+
return Number.isNaN(pid) ? 0 : pid;
|
|
216
|
+
}
|
|
189
217
|
const base = socketPath.substring(socketPath.lastIndexOf("/") + 1);
|
|
190
218
|
const pid = Number.parseInt(base.replace(/\.sock$/, ""), 10);
|
|
191
219
|
return Number.isNaN(pid) ? 0 : pid;
|
|
@@ -343,7 +371,7 @@ var FsRegistryStore = class {
|
|
|
343
371
|
}
|
|
344
372
|
};
|
|
345
373
|
//#endregion
|
|
346
|
-
//#region src/adapters/node/
|
|
374
|
+
//#region src/adapters/node/cached-command.ts
|
|
347
375
|
/**
|
|
348
376
|
* The errno code of an unknown throwable: Node's process.kill throws a SystemError carrying a string code, but a defensive caller may hand us anything, so the narrowing is explicit rather than assumed. Exported for direct unit coverage of every narrowing side.
|
|
349
377
|
*/
|
|
@@ -351,38 +379,29 @@ function errnoOf(error) {
|
|
|
351
379
|
if (error instanceof Error && "code" in error && typeof error.code === "string") return error.code;
|
|
352
380
|
return "";
|
|
353
381
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
*/
|
|
368
|
-
async lstart(pid) {
|
|
369
|
-
return (await this.runPs(pid))?.trim();
|
|
370
|
-
}
|
|
371
|
-
psCache = /* @__PURE__ */ new Map();
|
|
382
|
+
/** Shared existence probe for PsProcInfo and WinProcInfo: no throw means the pid is live; EPERM means it exists but belongs to another user (still live, and Node emulates this check on Windows too); ESRCH (or any other code) means it is gone. */
|
|
383
|
+
async function signalZeroAlive(pid) {
|
|
384
|
+
return new Promise((resolve) => {
|
|
385
|
+
try {
|
|
386
|
+
process.kill(pid, 0);
|
|
387
|
+
resolve(true);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
resolve(errnoOf(error) === "EPERM");
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/** Shared per-pid cache and in-flight dedup for a proc-info command runner, used by both PsProcInfo and WinProcInfo. */
|
|
394
|
+
var CachedPidCommand = class CachedPidCommand {
|
|
395
|
+
cache = /* @__PURE__ */ new Map();
|
|
372
396
|
static CACHE_MS = 6e4;
|
|
373
397
|
inFlight = /* @__PURE__ */ new Map();
|
|
374
|
-
async
|
|
375
|
-
const cached = this.
|
|
376
|
-
if (cached !== void 0 && Date.now() - cached.at <
|
|
398
|
+
async run(pid, command, args) {
|
|
399
|
+
const cached = this.cache.get(pid);
|
|
400
|
+
if (cached !== void 0 && Date.now() - cached.at < CachedPidCommand.CACHE_MS) return Promise.resolve(cached.value);
|
|
377
401
|
const existing = this.inFlight.get(pid);
|
|
378
402
|
if (existing !== void 0) return existing;
|
|
379
403
|
const promise = new Promise((resolve) => {
|
|
380
|
-
const child = spawn(
|
|
381
|
-
"-o",
|
|
382
|
-
"lstart=",
|
|
383
|
-
"-p",
|
|
384
|
-
String(pid)
|
|
385
|
-
], {
|
|
404
|
+
const child = spawn(command, args, {
|
|
386
405
|
env: {
|
|
387
406
|
...process.env,
|
|
388
407
|
LC_ALL: "C",
|
|
@@ -402,11 +421,12 @@ var PsProcInfo = class PsProcInfo {
|
|
|
402
421
|
resolve(void 0);
|
|
403
422
|
});
|
|
404
423
|
child.on("close", (code) => {
|
|
405
|
-
|
|
424
|
+
const value = code === 0 && out.trim().length > 0 ? out : void 0;
|
|
425
|
+
this.cache.set(pid, {
|
|
406
426
|
at: Date.now(),
|
|
407
|
-
value
|
|
427
|
+
value
|
|
408
428
|
});
|
|
409
|
-
resolve(
|
|
429
|
+
resolve(value);
|
|
410
430
|
});
|
|
411
431
|
}).finally(() => {
|
|
412
432
|
this.inFlight.delete(pid);
|
|
@@ -416,6 +436,45 @@ var PsProcInfo = class PsProcInfo {
|
|
|
416
436
|
}
|
|
417
437
|
};
|
|
418
438
|
//#endregion
|
|
439
|
+
//#region src/adapters/node/ps-proc-info.ts
|
|
440
|
+
var PsProcInfo = class {
|
|
441
|
+
command = new CachedPidCommand();
|
|
442
|
+
async alive(pid) {
|
|
443
|
+
return signalZeroAlive(pid);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* `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.
|
|
447
|
+
*/
|
|
448
|
+
async lstart(pid) {
|
|
449
|
+
return (await this.command.run(pid, "ps", [
|
|
450
|
+
"-o",
|
|
451
|
+
"lstart=",
|
|
452
|
+
"-p",
|
|
453
|
+
pid.toString()
|
|
454
|
+
]))?.trim();
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region src/adapters/node/win-proc-info.ts
|
|
459
|
+
/**
|
|
460
|
+
* Native Windows has no `ps`, so process-start-time verification uses PowerShell's own Process object instead. The exact string format is a cc-peer convention (round-trip ISO-8601, UTC), not a reproduction of whatever format a real native-Windows Claude Code session emits for its own registry entries — that value is not publicly documented and this implementation has not been verified against a live Windows Claude Code session. It is self-consistent for cc-peer's own entries (written and re-read with the same formatting), which is what roster admission actually needs for a peer this SDK itself created.
|
|
461
|
+
*/
|
|
462
|
+
var WinProcInfo = class {
|
|
463
|
+
command = new CachedPidCommand();
|
|
464
|
+
async alive(pid) {
|
|
465
|
+
return signalZeroAlive(pid);
|
|
466
|
+
}
|
|
467
|
+
async lstart(pid) {
|
|
468
|
+
const script = `(Get-Process -Id ${pid.toString()} -ErrorAction Stop).StartTime.ToUniversalTime().ToString('o')`;
|
|
469
|
+
return (await this.command.run(pid, "powershell.exe", [
|
|
470
|
+
"-NoProfile",
|
|
471
|
+
"-NonInteractive",
|
|
472
|
+
"-Command",
|
|
473
|
+
script
|
|
474
|
+
]))?.trim();
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
//#endregion
|
|
419
478
|
//#region src/schemas/envelope.ts
|
|
420
479
|
/**
|
|
421
480
|
* 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.
|
|
@@ -748,7 +807,7 @@ var CcPeer = class CcPeer extends EventEmitter {
|
|
|
748
807
|
transport: new UdsTransport(),
|
|
749
808
|
registry: new FsRegistryStore(options),
|
|
750
809
|
keys: new FsKeyStore(options),
|
|
751
|
-
procInfo: new PsProcInfo(),
|
|
810
|
+
procInfo: isWindows() ? new WinProcInfo() : new PsProcInfo(),
|
|
752
811
|
clock: new SystemClock()
|
|
753
812
|
});
|
|
754
813
|
await peer.start();
|
|
@@ -762,11 +821,11 @@ var CcPeer = class CcPeer extends EventEmitter {
|
|
|
762
821
|
this.ownKey = {
|
|
763
822
|
peerToken: randomBytes(PEER_TOKEN_BYTES).toString("hex"),
|
|
764
823
|
procStart: await this.deps.procInfo.lstart(process.pid) ?? "",
|
|
765
|
-
pidDomain:
|
|
824
|
+
pidDomain: process.platform
|
|
766
825
|
};
|
|
767
826
|
if (this.ownKey.procStart === "") throw new NotStartedError("could not read own procStart via ps");
|
|
768
827
|
await this.deps.keys.writeForSocket(socketPath, this.ownKey);
|
|
769
|
-
await mkdir(dirname(socketPath), {
|
|
828
|
+
if (!isWindows()) await mkdir(dirname(socketPath), {
|
|
770
829
|
recursive: true,
|
|
771
830
|
mode: 448
|
|
772
831
|
});
|
|
@@ -796,7 +855,7 @@ var CcPeer = class CcPeer extends EventEmitter {
|
|
|
796
855
|
peerFeatures: ["notify_idle", "reply_across_default_dirs"],
|
|
797
856
|
kind: "interactive",
|
|
798
857
|
entrypoint: "cli",
|
|
799
|
-
pidDomain:
|
|
858
|
+
pidDomain: process.platform,
|
|
800
859
|
messagingSocketPath: socketPathForPid(process.pid, this.options),
|
|
801
860
|
...this.options.name !== void 0 ? {
|
|
802
861
|
name: this.options.name,
|
|
@@ -892,7 +951,14 @@ var CcPeer = class CcPeer extends EventEmitter {
|
|
|
892
951
|
const first = await lines[Symbol.asyncIterator]().next();
|
|
893
952
|
if (first.done === true) return;
|
|
894
953
|
const parsed = JSON.parse(first.value);
|
|
895
|
-
|
|
954
|
+
const authenticated = AuthLineSchema.is(parsed) && parsed.token === ownToken;
|
|
955
|
+
if (isWindows()) {
|
|
956
|
+
if (!authenticated) {
|
|
957
|
+
this.log("inbound auth line missing or mismatched (connection closed)");
|
|
958
|
+
conn.close();
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
} else if (!authenticated) this.log("inbound auth token mismatch (foreign token tolerated)");
|
|
896
962
|
for await (const line of lines) {
|
|
897
963
|
let frame;
|
|
898
964
|
try {
|
|
@@ -157,10 +157,17 @@ const PeerKeyFileSchema = defineSchema(zod.z.object({
|
|
|
157
157
|
}));
|
|
158
158
|
//#endregion
|
|
159
159
|
//#region src/adapters/node/paths.ts
|
|
160
|
+
/** Named-pipe prefix Windows requires; a listen()/connect() path must live under it. */
|
|
161
|
+
const WINDOWS_PIPE_PREFIX = "\\\\.\\pipe\\";
|
|
162
|
+
function isWindows() {
|
|
163
|
+
return process.platform === "win32";
|
|
164
|
+
}
|
|
160
165
|
/**
|
|
161
166
|
* Candidate socket directories, in the order the reference client accepts
|
|
162
167
|
* them. The tuple return type guarantees at least one candidate exists, so
|
|
163
|
-
* callers can index [0] without a fallback branch.
|
|
168
|
+
* callers can index [0] without a fallback branch. Meaningless on Windows,
|
|
169
|
+
* where a named pipe has no filesystem directory of its own — callers there
|
|
170
|
+
* use socketPathForPid directly instead of building a path from a directory.
|
|
164
171
|
*/
|
|
165
172
|
function socketDirCandidates(config = {}) {
|
|
166
173
|
if (config.socketDir !== void 0) return [config.socketDir];
|
|
@@ -174,7 +181,23 @@ function socketDirCandidates(config = {}) {
|
|
|
174
181
|
function sessionsDir(config = {}) {
|
|
175
182
|
return (0, node_path.join)(config.homeDir ?? (0, node_os.homedir)(), ".claude", "sessions");
|
|
176
183
|
}
|
|
184
|
+
/** Long enough that two distinct socketDir values collide only by the same astronomically small chance any truncated hash does; short enough to keep the pipe name readable. */
|
|
185
|
+
const PIPE_NAMESPACE_HEX_LENGTH = 8;
|
|
186
|
+
/**
|
|
187
|
+
* Short, path-safe token distinguishing one caller-supplied socketDir from another in a Windows pipe name. A named pipe has no filesystem directory of its own to carry that distinction the way a POSIX socket path does, so the directory is folded into the name instead.
|
|
188
|
+
*/
|
|
189
|
+
function pipeNamespace(socketDir) {
|
|
190
|
+
return (0, node_crypto.createHash)("sha256").update(socketDir).digest("hex").slice(0, PIPE_NAMESPACE_HEX_LENGTH);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* On native Windows, Claude Code's own inbox is a named pipe rather than a Unix domain socket — Node's net module has no filesystem-path AF_UNIX support there at all (see docs/PROTOCOL.md), so unlike every other config override in this module, an explicit socketDir can never become a literal filesystem path on Windows: net.Server.listen() would reject it with EACCES regardless of what directory it names. Node still dispatches to the right OS primitive from the path's own shape, so UdsTransport needs no change; only path construction does. A caller-supplied socketDir keeps its usual purpose — separating one caller's sockets from another's, e.g. across concurrent test runs — by namespacing the pipe name instead of pointing at a real directory. The exact pipe name only needs to be unique per pid (and per socketDir) on this machine, not to match any specific value a real Windows Claude Code session uses.
|
|
194
|
+
*/
|
|
177
195
|
function socketPathForPid(pid, config = {}) {
|
|
196
|
+
if (isWindows()) {
|
|
197
|
+
const namespace = config.socketDir === void 0 ? "" : `-${pipeNamespace(config.socketDir)}`;
|
|
198
|
+
return `${WINDOWS_PIPE_PREFIX}cc-peer${namespace}-${pid.toString()}`;
|
|
199
|
+
}
|
|
200
|
+
if (config.socketDir !== void 0) return `${config.socketDir}/${pid.toString()}.sock`;
|
|
178
201
|
return `${socketDirCandidates(config)[0]}/${pid.toString()}.sock`;
|
|
179
202
|
}
|
|
180
203
|
function registryFilePath(pid, config = {}) {
|
|
@@ -186,6 +209,11 @@ function keyFilePath(socketPath, config = {}) {
|
|
|
186
209
|
return (0, node_path.join)(sessionsDir(config), `${pidFromSocketPath(socketPath).toString()}.${hash}.key`);
|
|
187
210
|
}
|
|
188
211
|
function pidFromSocketPath(socketPath) {
|
|
212
|
+
if (socketPath.startsWith(WINDOWS_PIPE_PREFIX)) {
|
|
213
|
+
const name = socketPath.slice(9);
|
|
214
|
+
const pid = Number.parseInt(name.slice(name.lastIndexOf("-") + 1), 10);
|
|
215
|
+
return Number.isNaN(pid) ? 0 : pid;
|
|
216
|
+
}
|
|
189
217
|
const base = socketPath.substring(socketPath.lastIndexOf("/") + 1);
|
|
190
218
|
const pid = Number.parseInt(base.replace(/\.sock$/, ""), 10);
|
|
191
219
|
return Number.isNaN(pid) ? 0 : pid;
|
|
@@ -343,7 +371,7 @@ var FsRegistryStore = class {
|
|
|
343
371
|
}
|
|
344
372
|
};
|
|
345
373
|
//#endregion
|
|
346
|
-
//#region src/adapters/node/
|
|
374
|
+
//#region src/adapters/node/cached-command.ts
|
|
347
375
|
/**
|
|
348
376
|
* The errno code of an unknown throwable: Node's process.kill throws a SystemError carrying a string code, but a defensive caller may hand us anything, so the narrowing is explicit rather than assumed. Exported for direct unit coverage of every narrowing side.
|
|
349
377
|
*/
|
|
@@ -351,38 +379,29 @@ function errnoOf(error) {
|
|
|
351
379
|
if (error instanceof Error && "code" in error && typeof error.code === "string") return error.code;
|
|
352
380
|
return "";
|
|
353
381
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
*/
|
|
368
|
-
async lstart(pid) {
|
|
369
|
-
return (await this.runPs(pid))?.trim();
|
|
370
|
-
}
|
|
371
|
-
psCache = /* @__PURE__ */ new Map();
|
|
382
|
+
/** Shared existence probe for PsProcInfo and WinProcInfo: no throw means the pid is live; EPERM means it exists but belongs to another user (still live, and Node emulates this check on Windows too); ESRCH (or any other code) means it is gone. */
|
|
383
|
+
async function signalZeroAlive(pid) {
|
|
384
|
+
return new Promise((resolve) => {
|
|
385
|
+
try {
|
|
386
|
+
process.kill(pid, 0);
|
|
387
|
+
resolve(true);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
resolve(errnoOf(error) === "EPERM");
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/** Shared per-pid cache and in-flight dedup for a proc-info command runner, used by both PsProcInfo and WinProcInfo. */
|
|
394
|
+
var CachedPidCommand = class CachedPidCommand {
|
|
395
|
+
cache = /* @__PURE__ */ new Map();
|
|
372
396
|
static CACHE_MS = 6e4;
|
|
373
397
|
inFlight = /* @__PURE__ */ new Map();
|
|
374
|
-
async
|
|
375
|
-
const cached = this.
|
|
376
|
-
if (cached !== void 0 && Date.now() - cached.at <
|
|
398
|
+
async run(pid, command, args) {
|
|
399
|
+
const cached = this.cache.get(pid);
|
|
400
|
+
if (cached !== void 0 && Date.now() - cached.at < CachedPidCommand.CACHE_MS) return Promise.resolve(cached.value);
|
|
377
401
|
const existing = this.inFlight.get(pid);
|
|
378
402
|
if (existing !== void 0) return existing;
|
|
379
403
|
const promise = new Promise((resolve) => {
|
|
380
|
-
const child = (0, node_child_process.spawn)(
|
|
381
|
-
"-o",
|
|
382
|
-
"lstart=",
|
|
383
|
-
"-p",
|
|
384
|
-
String(pid)
|
|
385
|
-
], {
|
|
404
|
+
const child = (0, node_child_process.spawn)(command, args, {
|
|
386
405
|
env: {
|
|
387
406
|
...process.env,
|
|
388
407
|
LC_ALL: "C",
|
|
@@ -402,11 +421,12 @@ var PsProcInfo = class PsProcInfo {
|
|
|
402
421
|
resolve(void 0);
|
|
403
422
|
});
|
|
404
423
|
child.on("close", (code) => {
|
|
405
|
-
|
|
424
|
+
const value = code === 0 && out.trim().length > 0 ? out : void 0;
|
|
425
|
+
this.cache.set(pid, {
|
|
406
426
|
at: Date.now(),
|
|
407
|
-
value
|
|
427
|
+
value
|
|
408
428
|
});
|
|
409
|
-
resolve(
|
|
429
|
+
resolve(value);
|
|
410
430
|
});
|
|
411
431
|
}).finally(() => {
|
|
412
432
|
this.inFlight.delete(pid);
|
|
@@ -416,6 +436,45 @@ var PsProcInfo = class PsProcInfo {
|
|
|
416
436
|
}
|
|
417
437
|
};
|
|
418
438
|
//#endregion
|
|
439
|
+
//#region src/adapters/node/ps-proc-info.ts
|
|
440
|
+
var PsProcInfo = class {
|
|
441
|
+
command = new CachedPidCommand();
|
|
442
|
+
async alive(pid) {
|
|
443
|
+
return signalZeroAlive(pid);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* `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.
|
|
447
|
+
*/
|
|
448
|
+
async lstart(pid) {
|
|
449
|
+
return (await this.command.run(pid, "ps", [
|
|
450
|
+
"-o",
|
|
451
|
+
"lstart=",
|
|
452
|
+
"-p",
|
|
453
|
+
pid.toString()
|
|
454
|
+
]))?.trim();
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region src/adapters/node/win-proc-info.ts
|
|
459
|
+
/**
|
|
460
|
+
* Native Windows has no `ps`, so process-start-time verification uses PowerShell's own Process object instead. The exact string format is a cc-peer convention (round-trip ISO-8601, UTC), not a reproduction of whatever format a real native-Windows Claude Code session emits for its own registry entries — that value is not publicly documented and this implementation has not been verified against a live Windows Claude Code session. It is self-consistent for cc-peer's own entries (written and re-read with the same formatting), which is what roster admission actually needs for a peer this SDK itself created.
|
|
461
|
+
*/
|
|
462
|
+
var WinProcInfo = class {
|
|
463
|
+
command = new CachedPidCommand();
|
|
464
|
+
async alive(pid) {
|
|
465
|
+
return signalZeroAlive(pid);
|
|
466
|
+
}
|
|
467
|
+
async lstart(pid) {
|
|
468
|
+
const script = `(Get-Process -Id ${pid.toString()} -ErrorAction Stop).StartTime.ToUniversalTime().ToString('o')`;
|
|
469
|
+
return (await this.command.run(pid, "powershell.exe", [
|
|
470
|
+
"-NoProfile",
|
|
471
|
+
"-NonInteractive",
|
|
472
|
+
"-Command",
|
|
473
|
+
script
|
|
474
|
+
]))?.trim();
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
//#endregion
|
|
419
478
|
//#region src/schemas/envelope.ts
|
|
420
479
|
/**
|
|
421
480
|
* 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.
|
|
@@ -748,7 +807,7 @@ var CcPeer = class CcPeer extends node_events.EventEmitter {
|
|
|
748
807
|
transport: new UdsTransport(),
|
|
749
808
|
registry: new FsRegistryStore(options),
|
|
750
809
|
keys: new FsKeyStore(options),
|
|
751
|
-
procInfo: new PsProcInfo(),
|
|
810
|
+
procInfo: isWindows() ? new WinProcInfo() : new PsProcInfo(),
|
|
752
811
|
clock: new SystemClock()
|
|
753
812
|
});
|
|
754
813
|
await peer.start();
|
|
@@ -762,11 +821,11 @@ var CcPeer = class CcPeer extends node_events.EventEmitter {
|
|
|
762
821
|
this.ownKey = {
|
|
763
822
|
peerToken: (0, node_crypto.randomBytes)(PEER_TOKEN_BYTES).toString("hex"),
|
|
764
823
|
procStart: await this.deps.procInfo.lstart(process.pid) ?? "",
|
|
765
|
-
pidDomain:
|
|
824
|
+
pidDomain: process.platform
|
|
766
825
|
};
|
|
767
826
|
if (this.ownKey.procStart === "") throw new NotStartedError("could not read own procStart via ps");
|
|
768
827
|
await this.deps.keys.writeForSocket(socketPath, this.ownKey);
|
|
769
|
-
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(socketPath), {
|
|
828
|
+
if (!isWindows()) await (0, node_fs_promises.mkdir)((0, node_path.dirname)(socketPath), {
|
|
770
829
|
recursive: true,
|
|
771
830
|
mode: 448
|
|
772
831
|
});
|
|
@@ -796,7 +855,7 @@ var CcPeer = class CcPeer extends node_events.EventEmitter {
|
|
|
796
855
|
peerFeatures: ["notify_idle", "reply_across_default_dirs"],
|
|
797
856
|
kind: "interactive",
|
|
798
857
|
entrypoint: "cli",
|
|
799
|
-
pidDomain:
|
|
858
|
+
pidDomain: process.platform,
|
|
800
859
|
messagingSocketPath: socketPathForPid(process.pid, this.options),
|
|
801
860
|
...this.options.name !== void 0 ? {
|
|
802
861
|
name: this.options.name,
|
|
@@ -892,7 +951,14 @@ var CcPeer = class CcPeer extends node_events.EventEmitter {
|
|
|
892
951
|
const first = await lines[Symbol.asyncIterator]().next();
|
|
893
952
|
if (first.done === true) return;
|
|
894
953
|
const parsed = JSON.parse(first.value);
|
|
895
|
-
|
|
954
|
+
const authenticated = AuthLineSchema.is(parsed) && parsed.token === ownToken;
|
|
955
|
+
if (isWindows()) {
|
|
956
|
+
if (!authenticated) {
|
|
957
|
+
this.log("inbound auth line missing or mismatched (connection closed)");
|
|
958
|
+
conn.close();
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
} else if (!authenticated) this.log("inbound auth token mismatch (foreign token tolerated)");
|
|
896
962
|
for await (const line of lines) {
|
|
897
963
|
let frame;
|
|
898
964
|
try {
|
package/dist/cc-peer.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_cc_peer = require("./cc-peer-
|
|
2
|
+
const require_cc_peer = require("./cc-peer-CXzxAUE4.cjs");
|
|
3
3
|
exports.CC_PEER_VERSION = require_cc_peer.CC_PEER_VERSION;
|
|
4
4
|
exports.CcPeer = require_cc_peer.CcPeer;
|
package/dist/cc-peer.d.cts
CHANGED
|
@@ -70,12 +70,12 @@ declare const RegistryEntrySchema: z.ZodObject<{
|
|
|
70
70
|
nameSince: z.ZodOptional<z.ZodNumber>;
|
|
71
71
|
updatedAt: z.ZodNumber;
|
|
72
72
|
status: z.ZodOptional<z.ZodEnum<{
|
|
73
|
+
idle: "idle";
|
|
73
74
|
busy: "busy";
|
|
74
75
|
shell: "shell";
|
|
75
|
-
idle: "idle";
|
|
76
76
|
waiting: "waiting";
|
|
77
77
|
}> & {
|
|
78
|
-
is(value: unknown): value is "
|
|
78
|
+
is(value: unknown): value is "idle" | "busy" | "shell" | "waiting";
|
|
79
79
|
}>;
|
|
80
80
|
statusUpdatedAt: z.ZodOptional<z.ZodNumber>;
|
|
81
81
|
bridgeSessionId: z.ZodOptional<z.ZodString>;
|
|
@@ -97,7 +97,7 @@ declare const RegistryEntrySchema: z.ZodObject<{
|
|
|
97
97
|
name?: string | undefined;
|
|
98
98
|
nameSource?: "user" | "peer" | "derived" | "collision" | "auto" | "hook" | undefined;
|
|
99
99
|
nameSince?: number | undefined;
|
|
100
|
-
status?: "
|
|
100
|
+
status?: "idle" | "busy" | "shell" | "waiting" | undefined;
|
|
101
101
|
statusUpdatedAt?: number | undefined;
|
|
102
102
|
bridgeSessionId?: string | undefined;
|
|
103
103
|
};
|
package/dist/cc-peer.d.mts
CHANGED
|
@@ -70,12 +70,12 @@ declare const RegistryEntrySchema: z.ZodObject<{
|
|
|
70
70
|
nameSince: z.ZodOptional<z.ZodNumber>;
|
|
71
71
|
updatedAt: z.ZodNumber;
|
|
72
72
|
status: z.ZodOptional<z.ZodEnum<{
|
|
73
|
+
idle: "idle";
|
|
73
74
|
busy: "busy";
|
|
74
75
|
shell: "shell";
|
|
75
|
-
idle: "idle";
|
|
76
76
|
waiting: "waiting";
|
|
77
77
|
}> & {
|
|
78
|
-
is(value: unknown): value is "
|
|
78
|
+
is(value: unknown): value is "idle" | "busy" | "shell" | "waiting";
|
|
79
79
|
}>;
|
|
80
80
|
statusUpdatedAt: z.ZodOptional<z.ZodNumber>;
|
|
81
81
|
bridgeSessionId: z.ZodOptional<z.ZodString>;
|
|
@@ -97,7 +97,7 @@ declare const RegistryEntrySchema: z.ZodObject<{
|
|
|
97
97
|
name?: string | undefined;
|
|
98
98
|
nameSource?: "user" | "peer" | "derived" | "collision" | "auto" | "hook" | undefined;
|
|
99
99
|
nameSince?: number | undefined;
|
|
100
|
-
status?: "
|
|
100
|
+
status?: "idle" | "busy" | "shell" | "waiting" | undefined;
|
|
101
101
|
statusUpdatedAt?: number | undefined;
|
|
102
102
|
bridgeSessionId?: string | undefined;
|
|
103
103
|
};
|
package/dist/cc-peer.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as CcPeer, t as CC_PEER_VERSION } from "./cc-peer-
|
|
1
|
+
import { n as CcPeer, t as CC_PEER_VERSION } from "./cc-peer-CJqStHIL.mjs";
|
|
2
2
|
export { CC_PEER_VERSION, CcPeer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-peer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Communicate with local Claude Code instances over their native cross-session peer messaging: send and receive messages, register as a named discoverable peer, receipts, idle subscriptions, and a REST facade via `npx cc-peer`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"build": "turbo run _build",
|
|
51
51
|
"_build": "tsdown && tsx scripts/generate-json-schema.ts",
|
|
52
52
|
"lint": "turbo run _lint",
|
|
53
|
-
"_lint": "eslint . --cache",
|
|
53
|
+
"_lint": "eslint . --cache --fix",
|
|
54
54
|
"typecheck": "turbo run _typecheck",
|
|
55
55
|
"_typecheck": "tsc --noEmit",
|
|
56
56
|
"test": "vitest run",
|