cc-peer 1.1.5 → 1.1.6

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.
@@ -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-CH38-mD2.cjs");
24
+ const require_cc_peer = require("../cc-peer-IGtoK_0q.cjs");
25
25
  let node_crypto = require("node:crypto");
26
26
  let zod = require("zod");
27
27
  let node_process = require("node:process");
@@ -92,17 +92,25 @@ API_REGISTRY.add(ErrorResponseSchema, {
92
92
  function isJsonObject(value) {
93
93
  return typeof value === "object" && value !== null && !Array.isArray(value);
94
94
  }
95
- function apiComponentSchemas() {
96
- const converted = zod.z.toJSONSchema(API_REGISTRY, { uri: (id) => `#/components/schemas/${id}` });
97
- const schemas = isJsonObject(converted) && isJsonObject(converted.schemas) ? converted.schemas : {};
95
+ /**
96
+ * Pure extraction of the conversion shape so malformed registry output fails loudly instead of silently producing empty components. Exported for direct unit coverage of every malformed-input path.
97
+ */
98
+ function componentSchemasFrom(converted) {
99
+ if (!isJsonObject(converted)) throw new Error("zod registry conversion did not produce an object");
100
+ const schemas = converted.schemas;
101
+ if (!isJsonObject(schemas)) throw new Error("zod registry conversion produced no schemas object");
98
102
  const stripped = {};
99
- for (const [name, schema] of Object.entries(schemas)) if (isJsonObject(schema) && "$schema" in schema) {
103
+ for (const [name, schema] of Object.entries(schemas)) {
104
+ if (!isJsonObject(schema)) throw new Error(`component ${name} is not a JSON object`);
100
105
  const rest = { ...schema };
101
106
  delete rest.$schema;
102
107
  stripped[name] = rest;
103
- } else stripped[name] = schema;
108
+ }
104
109
  return stripped;
105
110
  }
111
+ function apiComponentSchemas() {
112
+ return componentSchemasFrom(zod.z.toJSONSchema(API_REGISTRY, { uri: (id) => `#/components/schemas/${id}` }));
113
+ }
106
114
  //#endregion
107
115
  //#region src/api/server.ts
108
116
  /** Loopback only; this bridges onto a same-user IPC trust boundary. */
@@ -127,9 +135,8 @@ async function createApiServer(peer, options = {}) {
127
135
  });
128
136
  return new Promise((resolve) => {
129
137
  server.listen(options.port ?? 0, BIND_HOST, () => {
130
- const address = server.address();
131
138
  resolve({
132
- port: typeof address === "object" && address !== null ? address.port : 0,
139
+ port: listeningPort(server.address()),
133
140
  token,
134
141
  close: async () => {
135
142
  await new Promise((resolveClose) => {
@@ -150,7 +157,7 @@ async function handle(peer, req, res, token) {
150
157
  });
151
158
  res.end(body);
152
159
  };
153
- const host = (req.headers.host ?? "").split(":")[0] ?? "";
160
+ const host = hostnameOf(req.headers.host);
154
161
  if (!ALLOWED_HOSTS.has(host)) {
155
162
  finish(HTTP_FORBIDDEN, errorBody("host not allowed"));
156
163
  return;
@@ -161,7 +168,7 @@ async function handle(peer, req, res, token) {
161
168
  return;
162
169
  }
163
170
  }
164
- const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
171
+ const url = requestUrl(req.url, req.headers.host);
165
172
  try {
166
173
  if (req.method === "GET" && url.pathname === "/healthz") {
167
174
  finish(HTTP_OK, JSON.stringify({ ok: true }));
@@ -197,9 +204,39 @@ async function handle(peer, req, res, token) {
197
204
  }
198
205
  finish(HTTP_NOT_FOUND, errorBody("not found"));
199
206
  } catch (error) {
200
- finish(HTTP_INTERNAL, errorBody(error instanceof Error ? error.message : "internal"));
207
+ finish(HTTP_INTERNAL, errorBody(httpErrorMessage(error)));
201
208
  }
202
209
  }
210
+ /**
211
+ * Extract the numeric port `createApiServer` reports after `listen()`. The server always listens on a TCP host:port pair (never a named pipe), so `address()` returning anything but an AddressInfo object is a Node behaviour our own call site cannot trigger; a thrown error surfaces that violated assumption loudly rather than silently reporting port 0. Exported so the impossible-input side is directly unit-coverable without mocking node:net.
212
+ */
213
+ function listeningPort(address) {
214
+ if (address === null || typeof address !== "object") throw new Error("expected the server to report an AddressInfo after listen()");
215
+ return address.port;
216
+ }
217
+ /**
218
+ * Extract the hostname portion of a Host header, ignoring any port suffix. Uses indexOf/slice rather than split()[0] so every branch is genuinely reachable: an absent header is a legitimate "reject as disallowed" case, and a header with no colon is the common case, both real inputs a test can construct directly, unlike split()[0]'s type-only undefined case.
219
+ */
220
+ function hostnameOf(hostHeader) {
221
+ if (hostHeader === void 0) return "";
222
+ const colonIndex = hostHeader.indexOf(":");
223
+ return colonIndex === -1 ? hostHeader : hostHeader.slice(0, colonIndex);
224
+ }
225
+ /**
226
+ * Build the request URL from possibly-absent raw parts. Exported as a pure function so both undefined sides of the fallbacks are directly coverable (Node always populates these for well-formed requests, but the types allow absence and malformed raw requests exercise it).
227
+ */
228
+ function requestUrl(rawUrl, host) {
229
+ return new URL(rawUrl ?? "/", `http://${host ?? "localhost"}`);
230
+ }
231
+ /**
232
+ * Map a caught throwable to an HTTP error body message. Exported for direct unit coverage of the non-Error side, which live handlers cannot produce (every throw site raises Error subclasses).
233
+ */
234
+ function httpErrorMessage(error) {
235
+ return error instanceof Error ? error.message : JSON.stringify(error);
236
+ }
237
+ /**
238
+ * Narrow an already schema-refined target to exactly one PeerRef shape. Exported for direct unit coverage of the no-field throw, which the PeerTargetSchema refine makes unreachable through the HTTP surface.
239
+ */
203
240
  function toPeerRef(to) {
204
241
  if (to.pid !== void 0) return { pid: to.pid };
205
242
  if (to.name !== void 0) return { name: to.name };
@@ -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-BpNw-MoF.mjs";
2
+ import { a as defineSchema, i as RegistryEntrySchema, n as CcPeer, r as PrioritySchema } from "../cc-peer-CWGY6H7F.mjs";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { z } from "zod";
5
5
  import process from "node:process";
@@ -69,17 +69,25 @@ API_REGISTRY.add(ErrorResponseSchema, {
69
69
  function isJsonObject(value) {
70
70
  return typeof value === "object" && value !== null && !Array.isArray(value);
71
71
  }
72
- function apiComponentSchemas() {
73
- const converted = z.toJSONSchema(API_REGISTRY, { uri: (id) => `#/components/schemas/${id}` });
74
- const schemas = isJsonObject(converted) && isJsonObject(converted.schemas) ? converted.schemas : {};
72
+ /**
73
+ * Pure extraction of the conversion shape so malformed registry output fails loudly instead of silently producing empty components. Exported for direct unit coverage of every malformed-input path.
74
+ */
75
+ function componentSchemasFrom(converted) {
76
+ if (!isJsonObject(converted)) throw new Error("zod registry conversion did not produce an object");
77
+ const schemas = converted.schemas;
78
+ if (!isJsonObject(schemas)) throw new Error("zod registry conversion produced no schemas object");
75
79
  const stripped = {};
76
- for (const [name, schema] of Object.entries(schemas)) if (isJsonObject(schema) && "$schema" in schema) {
80
+ for (const [name, schema] of Object.entries(schemas)) {
81
+ if (!isJsonObject(schema)) throw new Error(`component ${name} is not a JSON object`);
77
82
  const rest = { ...schema };
78
83
  delete rest.$schema;
79
84
  stripped[name] = rest;
80
- } else stripped[name] = schema;
85
+ }
81
86
  return stripped;
82
87
  }
88
+ function apiComponentSchemas() {
89
+ return componentSchemasFrom(z.toJSONSchema(API_REGISTRY, { uri: (id) => `#/components/schemas/${id}` }));
90
+ }
83
91
  //#endregion
84
92
  //#region src/api/server.ts
85
93
  /** Loopback only; this bridges onto a same-user IPC trust boundary. */
@@ -104,9 +112,8 @@ async function createApiServer(peer, options = {}) {
104
112
  });
105
113
  return new Promise((resolve) => {
106
114
  server.listen(options.port ?? 0, BIND_HOST, () => {
107
- const address = server.address();
108
115
  resolve({
109
- port: typeof address === "object" && address !== null ? address.port : 0,
116
+ port: listeningPort(server.address()),
110
117
  token,
111
118
  close: async () => {
112
119
  await new Promise((resolveClose) => {
@@ -127,7 +134,7 @@ async function handle(peer, req, res, token) {
127
134
  });
128
135
  res.end(body);
129
136
  };
130
- const host = (req.headers.host ?? "").split(":")[0] ?? "";
137
+ const host = hostnameOf(req.headers.host);
131
138
  if (!ALLOWED_HOSTS.has(host)) {
132
139
  finish(HTTP_FORBIDDEN, errorBody("host not allowed"));
133
140
  return;
@@ -138,7 +145,7 @@ async function handle(peer, req, res, token) {
138
145
  return;
139
146
  }
140
147
  }
141
- const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
148
+ const url = requestUrl(req.url, req.headers.host);
142
149
  try {
143
150
  if (req.method === "GET" && url.pathname === "/healthz") {
144
151
  finish(HTTP_OK, JSON.stringify({ ok: true }));
@@ -174,9 +181,39 @@ async function handle(peer, req, res, token) {
174
181
  }
175
182
  finish(HTTP_NOT_FOUND, errorBody("not found"));
176
183
  } catch (error) {
177
- finish(HTTP_INTERNAL, errorBody(error instanceof Error ? error.message : "internal"));
184
+ finish(HTTP_INTERNAL, errorBody(httpErrorMessage(error)));
178
185
  }
179
186
  }
187
+ /**
188
+ * Extract the numeric port `createApiServer` reports after `listen()`. The server always listens on a TCP host:port pair (never a named pipe), so `address()` returning anything but an AddressInfo object is a Node behaviour our own call site cannot trigger; a thrown error surfaces that violated assumption loudly rather than silently reporting port 0. Exported so the impossible-input side is directly unit-coverable without mocking node:net.
189
+ */
190
+ function listeningPort(address) {
191
+ if (address === null || typeof address !== "object") throw new Error("expected the server to report an AddressInfo after listen()");
192
+ return address.port;
193
+ }
194
+ /**
195
+ * Extract the hostname portion of a Host header, ignoring any port suffix. Uses indexOf/slice rather than split()[0] so every branch is genuinely reachable: an absent header is a legitimate "reject as disallowed" case, and a header with no colon is the common case, both real inputs a test can construct directly, unlike split()[0]'s type-only undefined case.
196
+ */
197
+ function hostnameOf(hostHeader) {
198
+ if (hostHeader === void 0) return "";
199
+ const colonIndex = hostHeader.indexOf(":");
200
+ return colonIndex === -1 ? hostHeader : hostHeader.slice(0, colonIndex);
201
+ }
202
+ /**
203
+ * Build the request URL from possibly-absent raw parts. Exported as a pure function so both undefined sides of the fallbacks are directly coverable (Node always populates these for well-formed requests, but the types allow absence and malformed raw requests exercise it).
204
+ */
205
+ function requestUrl(rawUrl, host) {
206
+ return new URL(rawUrl ?? "/", `http://${host ?? "localhost"}`);
207
+ }
208
+ /**
209
+ * Map a caught throwable to an HTTP error body message. Exported for direct unit coverage of the non-Error side, which live handlers cannot produce (every throw site raises Error subclasses).
210
+ */
211
+ function httpErrorMessage(error) {
212
+ return error instanceof Error ? error.message : JSON.stringify(error);
213
+ }
214
+ /**
215
+ * Narrow an already schema-refined target to exactly one PeerRef shape. Exported for direct unit coverage of the no-field throw, which the PeerTargetSchema refine makes unreachable through the HTTP surface.
216
+ */
180
217
  function toPeerRef(to) {
181
218
  if (to.pid !== void 0) return { pid: to.pid };
182
219
  if (to.name !== void 0) return { name: to.name };
@@ -9,8 +9,6 @@ import { spawn } from "node:child_process";
9
9
  //#region src/adapters/node/uds-transport.ts
10
10
  /** macOS linger before close, matching the reference client's ~150ms. */
11
11
  const DEFAULT_LINGER_MS = 150;
12
- const CONNECT_TIMEOUT_MS = 5e3;
13
- const PROBE_TIMEOUT_MS = 2e3;
14
12
  var NodeInboundConnection = class {
15
13
  socket;
16
14
  buffer = "";
@@ -34,14 +32,12 @@ var NodeInboundConnection = class {
34
32
  index = this.buffer.indexOf("\n");
35
33
  }
36
34
  });
37
- socket.on("close", () => {
35
+ const finish = () => {
38
36
  this.ended = true;
39
37
  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
- });
38
+ };
39
+ socket.on("close", finish);
40
+ socket.on("error", finish);
45
41
  }
46
42
  peerPid() {
47
43
  return this.cachedPid;
@@ -69,15 +65,9 @@ var UdsTransport = class {
69
65
  socket.destroy();
70
66
  reject(error);
71
67
  };
72
- socket.setTimeout(CONNECT_TIMEOUT_MS, () => {
73
- fail(/* @__PURE__ */ new Error(`timeout connecting ${socketPath}`));
74
- });
75
68
  socket.once("error", fail);
76
69
  socket.once("connect", () => {
77
- socket.setTimeout(0);
78
- socket.write(payload, (error) => {
79
- if (error !== null && error !== void 0) fail(error);
80
- });
70
+ socket.write(payload);
81
71
  setTimeout(() => {
82
72
  socket.end();
83
73
  }, lingerMs).unref();
@@ -97,9 +87,6 @@ var UdsTransport = class {
97
87
  socket.destroy();
98
88
  resolve(value);
99
89
  };
100
- socket.setTimeout(PROBE_TIMEOUT_MS, () => {
101
- done(false);
102
- });
103
90
  socket.once("error", (error) => {
104
91
  done(error.code === "EBUSY");
105
92
  });
@@ -127,12 +114,12 @@ var UdsTransport = class {
127
114
  return {
128
115
  socketPath,
129
116
  close: async () => {
117
+ for (const socket of accepted.splice(0)) socket.destroy();
130
118
  await new Promise((resolve) => {
131
119
  server.close(() => {
132
120
  resolve();
133
121
  });
134
122
  });
135
- for (const socket of accepted) socket.destroy();
136
123
  }
137
124
  };
138
125
  }
@@ -170,7 +157,11 @@ const PeerKeyFileSchema = defineSchema(z.object({
170
157
  }));
171
158
  //#endregion
172
159
  //#region src/adapters/node/paths.ts
173
- /** Candidate socket directories, in the order the reference client accepts them. */
160
+ /**
161
+ * Candidate socket directories, in the order the reference client accepts
162
+ * them. The tuple return type guarantees at least one candidate exists, so
163
+ * callers can index [0] without a fallback branch.
164
+ */
174
165
  function socketDirCandidates(config = {}) {
175
166
  if (config.socketDir !== void 0) return [config.socketDir];
176
167
  const runtimeDir = process.env.XDG_RUNTIME_DIR;
@@ -184,7 +175,7 @@ function sessionsDir(config = {}) {
184
175
  return join(config.homeDir ?? homedir(), ".claude", "sessions");
185
176
  }
186
177
  function socketPathForPid(pid, config = {}) {
187
- return `${socketDirCandidates(config)[0] ?? "/tmp/cc-socks"}/${pid.toString()}.sock`;
178
+ return `${socketDirCandidates(config)[0]}/${pid.toString()}.sock`;
188
179
  }
189
180
  function registryFilePath(pid, config = {}) {
190
181
  return join(sessionsDir(config), `${pid.toString()}.json`);
@@ -195,7 +186,7 @@ function keyFilePath(socketPath, config = {}) {
195
186
  return join(sessionsDir(config), `${pidFromSocketPath(socketPath).toString()}.${hash}.key`);
196
187
  }
197
188
  function pidFromSocketPath(socketPath) {
198
- const base = socketPath.split("/").at(-1) ?? "";
189
+ const base = socketPath.substring(socketPath.lastIndexOf("/") + 1);
199
190
  const pid = Number.parseInt(base.replace(/\.sock$/, ""), 10);
200
191
  return Number.isNaN(pid) ? 0 : pid;
201
192
  }
@@ -353,18 +344,21 @@ var FsRegistryStore = class {
353
344
  };
354
345
  //#endregion
355
346
  //#region src/adapters/node/ps-proc-info.ts
347
+ /**
348
+ * 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
+ */
350
+ function errnoOf(error) {
351
+ if (error instanceof Error && "code" in error && typeof error.code === "string") return error.code;
352
+ return "";
353
+ }
356
354
  var PsProcInfo = class PsProcInfo {
357
355
  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
356
  return new Promise((resolve) => {
363
357
  try {
364
358
  process.kill(pid, 0);
365
359
  resolve(true);
366
360
  } catch (error) {
367
- resolve(errno(error) === "EPERM");
361
+ resolve(errnoOf(error) === "EPERM");
368
362
  }
369
363
  });
370
364
  }
@@ -383,31 +377,25 @@ var PsProcInfo = class PsProcInfo {
383
377
  const existing = this.inFlight.get(pid);
384
378
  if (existing !== void 0) return existing;
385
379
  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
- }
380
+ const child = spawn("ps", [
381
+ "-o",
382
+ "lstart=",
383
+ "-p",
384
+ String(pid)
385
+ ], {
386
+ env: {
387
+ ...process.env,
388
+ LC_ALL: "C",
389
+ TZ: "UTC"
390
+ },
391
+ stdio: [
392
+ "ignore",
393
+ "pipe",
394
+ "ignore"
395
+ ]
396
+ });
409
397
  let out = "";
410
- child.stdout?.on("data", (chunk) => {
398
+ child.stdout.on("data", (chunk) => {
411
399
  out += chunk.toString("utf8");
412
400
  });
413
401
  child.on("error", () => {
@@ -471,7 +459,9 @@ function buildEnvelope(attrs, body) {
471
459
  function parseEnvelope(content) {
472
460
  const match = ENVELOPE_RE.exec(content);
473
461
  if (match === null) return void 0;
474
- const parsed = { body: match[6] ?? "" };
462
+ const openingEnd = content.indexOf(">\n") + 2;
463
+ const closingStart = content.length - `\n</${TAG}>`.length;
464
+ const parsed = { body: content.substring(openingEnd, closingStart) };
475
465
  if (match[1] !== void 0) parsed.from = match[1];
476
466
  if (match[2] !== void 0) parsed.fromSession = match[2];
477
467
  if (match[3] !== void 0) parsed.hopChain = match[3].split(",");
@@ -500,8 +490,7 @@ var Pacer = class {
500
490
  /** Milliseconds to wait before one token is available (0 = now). */
501
491
  msUntilNextToken() {
502
492
  this.refill();
503
- if (this.tokens >= 1) return 0;
504
- const deficit = 1 - this.tokens;
493
+ const deficit = Math.max(0, 1 - this.tokens);
505
494
  return Math.ceil(deficit / this.refillPerSecond * MS_PER_SECOND);
506
495
  }
507
496
  /** Consume one token if available. */
@@ -511,10 +500,10 @@ var Pacer = class {
511
500
  this.tokens -= 1;
512
501
  return true;
513
502
  }
503
+ /** Clamps negative elapsed time (a clock moving backward) to zero rather than draining tokens, folding the guard into the arithmetic instead of a separate branch. */
514
504
  refill() {
515
505
  const now = this.clock.nowMs();
516
- const elapsedSeconds = (now - this.lastRefillMs) / MS_PER_SECOND;
517
- if (elapsedSeconds <= 0) return;
506
+ const elapsedSeconds = Math.max(0, (now - this.lastRefillMs) / MS_PER_SECOND);
518
507
  this.tokens = Math.min(this.capacity, this.tokens + elapsedSeconds * this.refillPerSecond);
519
508
  this.lastRefillMs = now;
520
509
  }
@@ -531,20 +520,16 @@ function newMsgId() {
531
520
  * 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
521
  */
533
522
  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);
523
+ return (await Promise.all(entries.map(async (entry) => checkEntry(entry, probes)))).filter((v) => v.admitted).map((v) => v.entry);
540
524
  }
525
+ /** Exported for direct unit coverage of each rejection reason, which filterRoster's own filtered-entries return value cannot distinguish. */
541
526
  async function checkEntry(entry, probes) {
542
527
  if (entry.messagingSocketPath.length === 0) return {
543
528
  entry,
544
529
  admitted: false,
545
530
  reason: "no-socket"
546
531
  };
547
- if (probes.ownSocketPath !== void 0 && entry.messagingSocketPath === probes.ownSocketPath) return {
532
+ if (entry.messagingSocketPath === probes.ownSocketPath) return {
548
533
  entry,
549
534
  admitted: false,
550
535
  reason: "own-socket"
@@ -554,8 +539,7 @@ async function checkEntry(entry, probes) {
554
539
  admitted: false,
555
540
  reason: "pid-dead"
556
541
  };
557
- const lstart = await probes.procInfo.lstart(entry.pid);
558
- if (lstart === void 0 || lstart !== entry.procStart) return {
542
+ if (await probes.procInfo.lstart(entry.pid) !== entry.procStart) return {
559
543
  entry,
560
544
  admitted: false,
561
545
  reason: "proc-start-mismatch"
@@ -646,6 +630,8 @@ const PeerIdleNoticeSchema = defineSchema(z.object({
646
630
  msgV: z.number().int(),
647
631
  msg_id: z.string().min(1)
648
632
  }));
633
+ /** Bare enum, exported separately so a test can assert its own membership directly rather than through the object field's .catch("claim") fallback, which would otherwise mask a corrupted "claim" member by coincidentally recovering the same value. */
634
+ const YieldReasonSchema = z.enum(["resume", "claim"]);
649
635
  const YieldArtifactRepliesSchema = defineSchema(z.object({
650
636
  type: z.literal("control"),
651
637
  action: z.literal("yield_artifact_replies"),
@@ -653,7 +639,7 @@ const YieldArtifactRepliesSchema = defineSchema(z.object({
653
639
  msg_id: z.string().min(1).max(128),
654
640
  session_id: z.string().max(512),
655
641
  slugs: z.array(z.string().max(128)).max(16),
656
- reason: z.enum(["resume", "claim"]).catch("claim"),
642
+ reason: YieldReasonSchema.catch("claim"),
657
643
  sent_at: z.number(),
658
644
  claimed_at: z.number().optional(),
659
645
  requester: z.object({
@@ -768,6 +754,9 @@ var CcPeer = class CcPeer extends EventEmitter {
768
754
  await peer.start();
769
755
  return peer;
770
756
  }
757
+ /**
758
+ * Bind, publish key and registry, and begin listening. Public rather than private because dependency-injected construction (this constructor takes the full Deps) needs to trigger it explicitly; everyday callers use the static create(), which wires the real node adapters.
759
+ */
771
760
  async start() {
772
761
  const socketPath = socketPathForPid(process.pid, this.options);
773
762
  this.ownKey = {
@@ -781,10 +770,11 @@ var CcPeer = class CcPeer extends EventEmitter {
781
770
  recursive: true,
782
771
  mode: 448
783
772
  });
784
- const entry = this.buildRegistryEntry();
773
+ const entry = this.buildRegistryEntry(this.ownKey.procStart);
785
774
  await this.deps.registry.write(entry);
775
+ const ownToken = this.ownKey.peerToken;
786
776
  this.listening = await this.deps.transport.listen(socketPath, (conn) => {
787
- this.handleConnection(conn);
777
+ this.handleConnection(conn, ownToken);
788
778
  });
789
779
  this.heartbeatTimer = setInterval(() => {
790
780
  this.deps.registry.touch(process.pid).catch(() => void 0);
@@ -792,14 +782,15 @@ var CcPeer = class CcPeer extends EventEmitter {
792
782
  this.heartbeatTimer.unref();
793
783
  this.log(`listening as ${this.options.name ?? "unnamed"} at ${socketPath}`);
794
784
  }
795
- buildRegistryEntry() {
785
+ /** procStart is a required parameter, not read from this.ownKey: start() already guarantees a non-empty value before this is called, so the type checker enforces it rather than a runtime fallback that can never actually fire. */
786
+ buildRegistryEntry(procStart) {
796
787
  const now = this.deps.clock.nowMs();
797
788
  return {
798
789
  pid: process.pid,
799
790
  sessionId: this.options.sessionId ?? newMsgId(),
800
791
  cwd: process.cwd(),
801
792
  startedAt: now,
802
- procStart: this.ownKey?.procStart ?? "",
793
+ procStart,
803
794
  version: "cc-peer",
804
795
  peerProtocol: 1,
805
796
  peerFeatures: ["notify_idle", "reply_across_default_dirs"],
@@ -864,10 +855,7 @@ var CcPeer = class CcPeer extends EventEmitter {
864
855
  resolve();
865
856
  }, waitMs).unref();
866
857
  });
867
- if (!this.pacer.tryReserve()) {
868
- await this.pacedSend(socketPath, lines);
869
- return;
870
- }
858
+ this.pacer.tryReserve();
871
859
  await this.deps.transport.connectWrite(socketPath, lines);
872
860
  }
873
861
  async subscribeIdle(target) {
@@ -898,14 +886,13 @@ var CcPeer = class CcPeer extends EventEmitter {
898
886
  if (match === void 0) throw new UnknownPeerError(`no roster entry named ${target.name}`);
899
887
  return match.messagingSocketPath;
900
888
  }
901
- async handleConnection(conn) {
889
+ /** ownToken is passed explicitly rather than read from this.ownKey: the listener callback that invokes this is only ever registered after start() has set ownKey, so the parameter records that guarantee at the type level instead of a runtime guard that can never actually be false. */
890
+ async handleConnection(conn, ownToken) {
902
891
  const lines = conn.readLines();
903
892
  const first = await lines[Symbol.asyncIterator]().next();
904
893
  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
- }
894
+ const parsed = JSON.parse(first.value);
895
+ if (AuthLineSchema.is(parsed) && parsed.token !== ownToken) this.log("inbound auth token mismatch (foreign token tolerated)");
909
896
  for await (const line of lines) {
910
897
  let frame;
911
898
  try {
@@ -9,8 +9,6 @@ let node_child_process = require("node:child_process");
9
9
  //#region src/adapters/node/uds-transport.ts
10
10
  /** macOS linger before close, matching the reference client's ~150ms. */
11
11
  const DEFAULT_LINGER_MS = 150;
12
- const CONNECT_TIMEOUT_MS = 5e3;
13
- const PROBE_TIMEOUT_MS = 2e3;
14
12
  var NodeInboundConnection = class {
15
13
  socket;
16
14
  buffer = "";
@@ -34,14 +32,12 @@ var NodeInboundConnection = class {
34
32
  index = this.buffer.indexOf("\n");
35
33
  }
36
34
  });
37
- socket.on("close", () => {
35
+ const finish = () => {
38
36
  this.ended = true;
39
37
  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
- });
38
+ };
39
+ socket.on("close", finish);
40
+ socket.on("error", finish);
45
41
  }
46
42
  peerPid() {
47
43
  return this.cachedPid;
@@ -69,15 +65,9 @@ var UdsTransport = class {
69
65
  socket.destroy();
70
66
  reject(error);
71
67
  };
72
- socket.setTimeout(CONNECT_TIMEOUT_MS, () => {
73
- fail(/* @__PURE__ */ new Error(`timeout connecting ${socketPath}`));
74
- });
75
68
  socket.once("error", fail);
76
69
  socket.once("connect", () => {
77
- socket.setTimeout(0);
78
- socket.write(payload, (error) => {
79
- if (error !== null && error !== void 0) fail(error);
80
- });
70
+ socket.write(payload);
81
71
  setTimeout(() => {
82
72
  socket.end();
83
73
  }, lingerMs).unref();
@@ -97,9 +87,6 @@ var UdsTransport = class {
97
87
  socket.destroy();
98
88
  resolve(value);
99
89
  };
100
- socket.setTimeout(PROBE_TIMEOUT_MS, () => {
101
- done(false);
102
- });
103
90
  socket.once("error", (error) => {
104
91
  done(error.code === "EBUSY");
105
92
  });
@@ -127,12 +114,12 @@ var UdsTransport = class {
127
114
  return {
128
115
  socketPath,
129
116
  close: async () => {
117
+ for (const socket of accepted.splice(0)) socket.destroy();
130
118
  await new Promise((resolve) => {
131
119
  server.close(() => {
132
120
  resolve();
133
121
  });
134
122
  });
135
- for (const socket of accepted) socket.destroy();
136
123
  }
137
124
  };
138
125
  }
@@ -170,7 +157,11 @@ const PeerKeyFileSchema = defineSchema(zod.z.object({
170
157
  }));
171
158
  //#endregion
172
159
  //#region src/adapters/node/paths.ts
173
- /** Candidate socket directories, in the order the reference client accepts them. */
160
+ /**
161
+ * Candidate socket directories, in the order the reference client accepts
162
+ * them. The tuple return type guarantees at least one candidate exists, so
163
+ * callers can index [0] without a fallback branch.
164
+ */
174
165
  function socketDirCandidates(config = {}) {
175
166
  if (config.socketDir !== void 0) return [config.socketDir];
176
167
  const runtimeDir = process.env.XDG_RUNTIME_DIR;
@@ -184,7 +175,7 @@ function sessionsDir(config = {}) {
184
175
  return (0, node_path.join)(config.homeDir ?? (0, node_os.homedir)(), ".claude", "sessions");
185
176
  }
186
177
  function socketPathForPid(pid, config = {}) {
187
- return `${socketDirCandidates(config)[0] ?? "/tmp/cc-socks"}/${pid.toString()}.sock`;
178
+ return `${socketDirCandidates(config)[0]}/${pid.toString()}.sock`;
188
179
  }
189
180
  function registryFilePath(pid, config = {}) {
190
181
  return (0, node_path.join)(sessionsDir(config), `${pid.toString()}.json`);
@@ -195,7 +186,7 @@ function keyFilePath(socketPath, config = {}) {
195
186
  return (0, node_path.join)(sessionsDir(config), `${pidFromSocketPath(socketPath).toString()}.${hash}.key`);
196
187
  }
197
188
  function pidFromSocketPath(socketPath) {
198
- const base = socketPath.split("/").at(-1) ?? "";
189
+ const base = socketPath.substring(socketPath.lastIndexOf("/") + 1);
199
190
  const pid = Number.parseInt(base.replace(/\.sock$/, ""), 10);
200
191
  return Number.isNaN(pid) ? 0 : pid;
201
192
  }
@@ -353,18 +344,21 @@ var FsRegistryStore = class {
353
344
  };
354
345
  //#endregion
355
346
  //#region src/adapters/node/ps-proc-info.ts
347
+ /**
348
+ * 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
+ */
350
+ function errnoOf(error) {
351
+ if (error instanceof Error && "code" in error && typeof error.code === "string") return error.code;
352
+ return "";
353
+ }
356
354
  var PsProcInfo = class PsProcInfo {
357
355
  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
356
  return new Promise((resolve) => {
363
357
  try {
364
358
  process.kill(pid, 0);
365
359
  resolve(true);
366
360
  } catch (error) {
367
- resolve(errno(error) === "EPERM");
361
+ resolve(errnoOf(error) === "EPERM");
368
362
  }
369
363
  });
370
364
  }
@@ -383,31 +377,25 @@ var PsProcInfo = class PsProcInfo {
383
377
  const existing = this.inFlight.get(pid);
384
378
  if (existing !== void 0) return existing;
385
379
  const promise = new Promise((resolve) => {
386
- let child;
387
- try {
388
- child = (0, node_child_process.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
- }
380
+ const child = (0, node_child_process.spawn)("ps", [
381
+ "-o",
382
+ "lstart=",
383
+ "-p",
384
+ String(pid)
385
+ ], {
386
+ env: {
387
+ ...process.env,
388
+ LC_ALL: "C",
389
+ TZ: "UTC"
390
+ },
391
+ stdio: [
392
+ "ignore",
393
+ "pipe",
394
+ "ignore"
395
+ ]
396
+ });
409
397
  let out = "";
410
- child.stdout?.on("data", (chunk) => {
398
+ child.stdout.on("data", (chunk) => {
411
399
  out += chunk.toString("utf8");
412
400
  });
413
401
  child.on("error", () => {
@@ -471,7 +459,9 @@ function buildEnvelope(attrs, body) {
471
459
  function parseEnvelope(content) {
472
460
  const match = ENVELOPE_RE.exec(content);
473
461
  if (match === null) return void 0;
474
- const parsed = { body: match[6] ?? "" };
462
+ const openingEnd = content.indexOf(">\n") + 2;
463
+ const closingStart = content.length - `\n</${TAG}>`.length;
464
+ const parsed = { body: content.substring(openingEnd, closingStart) };
475
465
  if (match[1] !== void 0) parsed.from = match[1];
476
466
  if (match[2] !== void 0) parsed.fromSession = match[2];
477
467
  if (match[3] !== void 0) parsed.hopChain = match[3].split(",");
@@ -500,8 +490,7 @@ var Pacer = class {
500
490
  /** Milliseconds to wait before one token is available (0 = now). */
501
491
  msUntilNextToken() {
502
492
  this.refill();
503
- if (this.tokens >= 1) return 0;
504
- const deficit = 1 - this.tokens;
493
+ const deficit = Math.max(0, 1 - this.tokens);
505
494
  return Math.ceil(deficit / this.refillPerSecond * MS_PER_SECOND);
506
495
  }
507
496
  /** Consume one token if available. */
@@ -511,10 +500,10 @@ var Pacer = class {
511
500
  this.tokens -= 1;
512
501
  return true;
513
502
  }
503
+ /** Clamps negative elapsed time (a clock moving backward) to zero rather than draining tokens, folding the guard into the arithmetic instead of a separate branch. */
514
504
  refill() {
515
505
  const now = this.clock.nowMs();
516
- const elapsedSeconds = (now - this.lastRefillMs) / MS_PER_SECOND;
517
- if (elapsedSeconds <= 0) return;
506
+ const elapsedSeconds = Math.max(0, (now - this.lastRefillMs) / MS_PER_SECOND);
518
507
  this.tokens = Math.min(this.capacity, this.tokens + elapsedSeconds * this.refillPerSecond);
519
508
  this.lastRefillMs = now;
520
509
  }
@@ -531,20 +520,16 @@ function newMsgId() {
531
520
  * 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
521
  */
533
522
  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);
523
+ return (await Promise.all(entries.map(async (entry) => checkEntry(entry, probes)))).filter((v) => v.admitted).map((v) => v.entry);
540
524
  }
525
+ /** Exported for direct unit coverage of each rejection reason, which filterRoster's own filtered-entries return value cannot distinguish. */
541
526
  async function checkEntry(entry, probes) {
542
527
  if (entry.messagingSocketPath.length === 0) return {
543
528
  entry,
544
529
  admitted: false,
545
530
  reason: "no-socket"
546
531
  };
547
- if (probes.ownSocketPath !== void 0 && entry.messagingSocketPath === probes.ownSocketPath) return {
532
+ if (entry.messagingSocketPath === probes.ownSocketPath) return {
548
533
  entry,
549
534
  admitted: false,
550
535
  reason: "own-socket"
@@ -554,8 +539,7 @@ async function checkEntry(entry, probes) {
554
539
  admitted: false,
555
540
  reason: "pid-dead"
556
541
  };
557
- const lstart = await probes.procInfo.lstart(entry.pid);
558
- if (lstart === void 0 || lstart !== entry.procStart) return {
542
+ if (await probes.procInfo.lstart(entry.pid) !== entry.procStart) return {
559
543
  entry,
560
544
  admitted: false,
561
545
  reason: "proc-start-mismatch"
@@ -646,6 +630,8 @@ const PeerIdleNoticeSchema = defineSchema(zod.z.object({
646
630
  msgV: zod.z.number().int(),
647
631
  msg_id: zod.z.string().min(1)
648
632
  }));
633
+ /** Bare enum, exported separately so a test can assert its own membership directly rather than through the object field's .catch("claim") fallback, which would otherwise mask a corrupted "claim" member by coincidentally recovering the same value. */
634
+ const YieldReasonSchema = zod.z.enum(["resume", "claim"]);
649
635
  const YieldArtifactRepliesSchema = defineSchema(zod.z.object({
650
636
  type: zod.z.literal("control"),
651
637
  action: zod.z.literal("yield_artifact_replies"),
@@ -653,7 +639,7 @@ const YieldArtifactRepliesSchema = defineSchema(zod.z.object({
653
639
  msg_id: zod.z.string().min(1).max(128),
654
640
  session_id: zod.z.string().max(512),
655
641
  slugs: zod.z.array(zod.z.string().max(128)).max(16),
656
- reason: zod.z.enum(["resume", "claim"]).catch("claim"),
642
+ reason: YieldReasonSchema.catch("claim"),
657
643
  sent_at: zod.z.number(),
658
644
  claimed_at: zod.z.number().optional(),
659
645
  requester: zod.z.object({
@@ -768,6 +754,9 @@ var CcPeer = class CcPeer extends node_events.EventEmitter {
768
754
  await peer.start();
769
755
  return peer;
770
756
  }
757
+ /**
758
+ * Bind, publish key and registry, and begin listening. Public rather than private because dependency-injected construction (this constructor takes the full Deps) needs to trigger it explicitly; everyday callers use the static create(), which wires the real node adapters.
759
+ */
771
760
  async start() {
772
761
  const socketPath = socketPathForPid(process.pid, this.options);
773
762
  this.ownKey = {
@@ -781,10 +770,11 @@ var CcPeer = class CcPeer extends node_events.EventEmitter {
781
770
  recursive: true,
782
771
  mode: 448
783
772
  });
784
- const entry = this.buildRegistryEntry();
773
+ const entry = this.buildRegistryEntry(this.ownKey.procStart);
785
774
  await this.deps.registry.write(entry);
775
+ const ownToken = this.ownKey.peerToken;
786
776
  this.listening = await this.deps.transport.listen(socketPath, (conn) => {
787
- this.handleConnection(conn);
777
+ this.handleConnection(conn, ownToken);
788
778
  });
789
779
  this.heartbeatTimer = setInterval(() => {
790
780
  this.deps.registry.touch(process.pid).catch(() => void 0);
@@ -792,14 +782,15 @@ var CcPeer = class CcPeer extends node_events.EventEmitter {
792
782
  this.heartbeatTimer.unref();
793
783
  this.log(`listening as ${this.options.name ?? "unnamed"} at ${socketPath}`);
794
784
  }
795
- buildRegistryEntry() {
785
+ /** procStart is a required parameter, not read from this.ownKey: start() already guarantees a non-empty value before this is called, so the type checker enforces it rather than a runtime fallback that can never actually fire. */
786
+ buildRegistryEntry(procStart) {
796
787
  const now = this.deps.clock.nowMs();
797
788
  return {
798
789
  pid: process.pid,
799
790
  sessionId: this.options.sessionId ?? newMsgId(),
800
791
  cwd: process.cwd(),
801
792
  startedAt: now,
802
- procStart: this.ownKey?.procStart ?? "",
793
+ procStart,
803
794
  version: "cc-peer",
804
795
  peerProtocol: 1,
805
796
  peerFeatures: ["notify_idle", "reply_across_default_dirs"],
@@ -864,10 +855,7 @@ var CcPeer = class CcPeer extends node_events.EventEmitter {
864
855
  resolve();
865
856
  }, waitMs).unref();
866
857
  });
867
- if (!this.pacer.tryReserve()) {
868
- await this.pacedSend(socketPath, lines);
869
- return;
870
- }
858
+ this.pacer.tryReserve();
871
859
  await this.deps.transport.connectWrite(socketPath, lines);
872
860
  }
873
861
  async subscribeIdle(target) {
@@ -898,14 +886,13 @@ var CcPeer = class CcPeer extends node_events.EventEmitter {
898
886
  if (match === void 0) throw new UnknownPeerError(`no roster entry named ${target.name}`);
899
887
  return match.messagingSocketPath;
900
888
  }
901
- async handleConnection(conn) {
889
+ /** ownToken is passed explicitly rather than read from this.ownKey: the listener callback that invokes this is only ever registered after start() has set ownKey, so the parameter records that guarantee at the type level instead of a runtime guard that can never actually be false. */
890
+ async handleConnection(conn, ownToken) {
902
891
  const lines = conn.readLines();
903
892
  const first = await lines[Symbol.asyncIterator]().next();
904
893
  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
- }
894
+ const parsed = JSON.parse(first.value);
895
+ if (AuthLineSchema.is(parsed) && parsed.token !== ownToken) this.log("inbound auth token mismatch (foreign token tolerated)");
909
896
  for await (const line of lines) {
910
897
  let frame;
911
898
  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-CH38-mD2.cjs");
2
+ const require_cc_peer = require("./cc-peer-IGtoK_0q.cjs");
3
3
  exports.CC_PEER_VERSION = require_cc_peer.CC_PEER_VERSION;
4
4
  exports.CcPeer = require_cc_peer.CcPeer;
@@ -287,7 +287,11 @@ export declare class CcPeer extends EventEmitter {
287
287
  private stopped;
288
288
  constructor(options: Readonly<CcPeerOptions>, deps: Deps);
289
289
  static create(options?: Readonly<CcPeerOptions>): Promise<CcPeer>;
290
- private start;
290
+ /**
291
+ * Bind, publish key and registry, and begin listening. Public rather than private because dependency-injected construction (this constructor takes the full Deps) needs to trigger it explicitly; everyday callers use the static create(), which wires the real node adapters.
292
+ */
293
+ start(): Promise<void>;
294
+ /** procStart is a required parameter, not read from this.ownKey: start() already guarantees a non-empty value before this is called, so the type checker enforces it rather than a runtime fallback that can never actually fire. */
291
295
  private buildRegistryEntry;
292
296
  roster(): Promise<RegistryEntry[]>;
293
297
  send(target: PeerRef, body: string, options?: Readonly<SendOptions>): Promise<{
@@ -298,6 +302,7 @@ export declare class CcPeer extends EventEmitter {
298
302
  msgId: string;
299
303
  }>;
300
304
  private resolveTarget;
305
+ /** ownToken is passed explicitly rather than read from this.ownKey: the listener callback that invokes this is only ever registered after start() has set ownKey, so the parameter records that guarantee at the type level instead of a runtime guard that can never actually be false. */
301
306
  private handleConnection;
302
307
  stop(): Promise<void>;
303
308
  }
@@ -287,7 +287,11 @@ export declare class CcPeer extends EventEmitter {
287
287
  private stopped;
288
288
  constructor(options: Readonly<CcPeerOptions>, deps: Deps);
289
289
  static create(options?: Readonly<CcPeerOptions>): Promise<CcPeer>;
290
- private start;
290
+ /**
291
+ * Bind, publish key and registry, and begin listening. Public rather than private because dependency-injected construction (this constructor takes the full Deps) needs to trigger it explicitly; everyday callers use the static create(), which wires the real node adapters.
292
+ */
293
+ start(): Promise<void>;
294
+ /** procStart is a required parameter, not read from this.ownKey: start() already guarantees a non-empty value before this is called, so the type checker enforces it rather than a runtime fallback that can never actually fire. */
291
295
  private buildRegistryEntry;
292
296
  roster(): Promise<RegistryEntry[]>;
293
297
  send(target: PeerRef, body: string, options?: Readonly<SendOptions>): Promise<{
@@ -298,6 +302,7 @@ export declare class CcPeer extends EventEmitter {
298
302
  msgId: string;
299
303
  }>;
300
304
  private resolveTarget;
305
+ /** ownToken is passed explicitly rather than read from this.ownKey: the listener callback that invokes this is only ever registered after start() has set ownKey, so the parameter records that guarantee at the type level instead of a runtime guard that can never actually be false. */
301
306
  private handleConnection;
302
307
  stop(): Promise<void>;
303
308
  }
package/dist/cc-peer.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { n as CcPeer, t as CC_PEER_VERSION } from "./cc-peer-BpNw-MoF.mjs";
1
+ import { n as CcPeer, t as CC_PEER_VERSION } from "./cc-peer-CWGY6H7F.mjs";
2
2
  export { CC_PEER_VERSION, CcPeer };
package/docs/PROTOCOL.md CHANGED
@@ -180,7 +180,7 @@ A receiving peer is the mirror: bind the socket, write key file and registry (pr
180
180
  | Holds, attestation, self-sent verdict, `crossSessionInbound` parity | verified live |
181
181
  | Peer registration, roster admission, name discovery | verified live (standalone peer in `ListAgents`, named `SendMessage`) |
182
182
  | Receipts: held / delivered / denied / expired / dropped{duplicate, rate-limited, hop-loop, hop-runaway} | verified live |
183
- | `queue-full` | code-verified; trigger attempted (55 queued), effective cap dynamically raised |
183
+ | `queue-full` | code-verified; trigger attempted (55 queued behind a hold dialog, no drop). The `tengu_harbor_kite_limits` dynamic override has never been served to this account (absent from the Statsig evaluations cache), so the effective cap is the code default of 50 — the non-firing therefore reflects queue accounting (messages parked behind the approval dialog do not count toward the undelivered-peer-message queue), not a raised cap |
184
184
  | Idle subscriptions (`idle`, `exited`) | verified live |
185
185
  | artifact_yield admission + answer | verified live (refused and admitted paths); populated handover not exercised |
186
186
  | File transfer | staging + wire replicated; receive path behind a never-served server flag (evidenced) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-peer",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
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",
@@ -55,7 +55,7 @@
55
55
  "_typecheck": "tsc --noEmit",
56
56
  "test": "vitest run",
57
57
  "test:watch": "vitest",
58
- "test:mutation": "stryker run",
58
+ "test:mutation": "stryker run stryker.config.ts",
59
59
  "prepublishOnly": "pnpm lint && pnpm typecheck && pnpm test:coverage && pnpm build && publint && attw --pack",
60
60
  "prepare": "husky",
61
61
  "test:coverage": "vitest run --coverage"
@@ -91,7 +91,7 @@
91
91
  "@stryker-mutator/core": "10.0.0",
92
92
  "@stryker-mutator/vitest-runner": "10.0.0",
93
93
  "@types/node": "26.4.1",
94
- "@vitest/coverage-v8": "5.0.0",
94
+ "@vitest/coverage-v8": "4.1.11",
95
95
  "commitlint": "21.2.2",
96
96
  "eslint": "10.10.0",
97
97
  "eslint-config-prettier": "10.1.8",
@@ -107,6 +107,6 @@
107
107
  "tsx": "4.23.13",
108
108
  "turbo": "2.10.12",
109
109
  "typescript": "6.0.3",
110
- "vitest": "5.0.0"
110
+ "vitest": "4.1.11"
111
111
  }
112
112
  }