cc-peer 1.1.8 → 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 CHANGED
@@ -61,5 +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
+ - **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.
64
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.
65
66
  - Verified against Claude Code 2.1.269; treat every Claude Code upgrade as a potential protocol change.
@@ -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-IGtoK_0q.cjs");
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");
@@ -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-CWGY6H7F.mjs";
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/ps-proc-info.ts
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
- var PsProcInfo = class PsProcInfo {
355
- async alive(pid) {
356
- return new Promise((resolve) => {
357
- try {
358
- process.kill(pid, 0);
359
- resolve(true);
360
- } catch (error) {
361
- resolve(errnoOf(error) === "EPERM");
362
- }
363
- });
364
- }
365
- /**
366
- * `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.
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 runPs(pid) {
375
- const cached = this.psCache.get(pid);
376
- if (cached !== void 0 && Date.now() - cached.at < PsProcInfo.CACHE_MS) return Promise.resolve(cached.value);
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("ps", [
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
- this.psCache.set(pid, {
424
+ const value = code === 0 && out.trim().length > 0 ? out : void 0;
425
+ this.cache.set(pid, {
406
426
  at: Date.now(),
407
- value: out
427
+ value
408
428
  });
409
- resolve(code === 0 && out.trim().length > 0 ? out : void 0);
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: "darwin"
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: "darwin",
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
- if (AuthLineSchema.is(parsed) && parsed.token !== ownToken) this.log("inbound auth token mismatch (foreign token tolerated)");
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/ps-proc-info.ts
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
- var PsProcInfo = class PsProcInfo {
355
- async alive(pid) {
356
- return new Promise((resolve) => {
357
- try {
358
- process.kill(pid, 0);
359
- resolve(true);
360
- } catch (error) {
361
- resolve(errnoOf(error) === "EPERM");
362
- }
363
- });
364
- }
365
- /**
366
- * `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.
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 runPs(pid) {
375
- const cached = this.psCache.get(pid);
376
- if (cached !== void 0 && Date.now() - cached.at < PsProcInfo.CACHE_MS) return Promise.resolve(cached.value);
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)("ps", [
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
- this.psCache.set(pid, {
424
+ const value = code === 0 && out.trim().length > 0 ? out : void 0;
425
+ this.cache.set(pid, {
406
426
  at: Date.now(),
407
- value: out
427
+ value
408
428
  });
409
- resolve(code === 0 && out.trim().length > 0 ? out : void 0);
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: "darwin"
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: "darwin",
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
- if (AuthLineSchema.is(parsed) && parsed.token !== ownToken) this.log("inbound auth token mismatch (foreign token tolerated)");
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-IGtoK_0q.cjs");
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.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { n as CcPeer, t as CC_PEER_VERSION } from "./cc-peer-CWGY6H7F.mjs";
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.1.8",
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",