cc-peer 1.3.5 → 1.5.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 +26 -0
- package/dist/adapters/node/alias-worker.cjs +42 -0
- package/dist/adapters/node/alias-worker.d.cts +1 -0
- package/dist/adapters/node/alias-worker.d.mts +1 -0
- package/dist/adapters/node/alias-worker.mjs +41 -0
- package/dist/alias-ipc-Cjs5yivH.cjs +48 -0
- package/dist/alias-ipc-DkaozCo7.mjs +31 -0
- package/dist/alias-pool.cjs +162 -0
- package/dist/alias-pool.d.cts +51 -0
- package/dist/alias-pool.d.mts +51 -0
- package/dist/alias-pool.mjs +160 -0
- package/dist/bin/cc-peer.cjs +10 -30
- package/dist/bin/cc-peer.mjs +2 -1
- package/dist/cc-peer-B3i4ZlKV.d.cts +312 -0
- package/dist/cc-peer-B3i4ZlKV.d.mts +312 -0
- package/dist/{cc-peer-CJqStHIL.mjs → cc-peer-BduoSupO.mjs} +10 -48
- package/dist/{cc-peer-CXzxAUE4.cjs → cc-peer-ClYIGdRf.cjs} +36 -80
- package/dist/cc-peer.cjs +1 -1
- package/dist/cc-peer.d.cts +2 -309
- package/dist/cc-peer.d.mts +2 -309
- package/dist/cc-peer.mjs +1 -1
- package/dist/errors-Dcqq2MIW.mjs +53 -0
- package/dist/errors-Dl_lSq1N.cjs +88 -0
- package/dist/rolldown-runtime-VH7oDXx4.cjs +28 -0
- package/docs/PROTOCOL.md +9 -0
- package/package.json +12 -2
- package/schemas/registry-entry.schema.json +4 -0
package/README.md
CHANGED
|
@@ -57,6 +57,32 @@ Every release is also mirrored to the GitHub Packages registry as `@exadev/cc-pe
|
|
|
57
57
|
|
|
58
58
|
The REST facade (`npx cc-peer`) serves `GET /sessions`, `POST /messages`, `POST /idle-subscriptions`, `GET /events` (SSE), and a self-describing `GET /openapi.json` on loopback with a bearer token.
|
|
59
59
|
|
|
60
|
+
### Session discovery and reply aliases
|
|
61
|
+
|
|
62
|
+
`CcPeer.roster()` already lists every live local Claude Code session, not just ones `cc-peer` itself registered — the registry it reads (`~/.claude/sessions/*.json`) is written by every interactive session on startup. A relay/front application that wants to discover every session to attach to needs nothing beyond `roster()`.
|
|
63
|
+
|
|
64
|
+
Giving a relayed session a name it can reply to natively for each of several correspondents is a different problem: the registry is one file per real OS pid with a single name each, so one process can only ever publish one discoverable name at a time (see [docs/PROTOCOL.md](docs/PROTOCOL.md#session-enumeration-and-reply-aliases-for-a-relayfront-building-on-this-sdk) for the empirical detail). `AliasPool`, exported from `cc-peer/alias-pool`, is the mechanism for this: it lazily forks one lightweight `CcPeer`-backed child process per correspondent name, and relays whatever that alias receives back to the parent.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { AliasPool } from "cc-peer/alias-pool";
|
|
68
|
+
|
|
69
|
+
const aliases = AliasPool.create();
|
|
70
|
+
|
|
71
|
+
aliases.on("message", (m) => {
|
|
72
|
+
// m.alias is the correspondent name the relayed session replied to;
|
|
73
|
+
// forward m.body to that correspondent's own channel.
|
|
74
|
+
console.log(`reply for ${m.alias}: ${m.body}`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Whenever a new correspondent messages the relayed session for the first
|
|
78
|
+
// time, give it a reply-able name (idempotent; a no-op if already active).
|
|
79
|
+
await aliases.ensure("alice");
|
|
80
|
+
|
|
81
|
+
// …later, once a correspondent is no longer relevant:
|
|
82
|
+
await aliases.retire("alice");
|
|
83
|
+
await aliases.stopAll();
|
|
84
|
+
```
|
|
85
|
+
|
|
60
86
|
## Limitations
|
|
61
87
|
|
|
62
88
|
- **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.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("../../rolldown-runtime-VH7oDXx4.cjs");
|
|
2
|
+
const require_cc_peer = require("../../cc-peer-ClYIGdRf.cjs");
|
|
3
|
+
const require_alias_ipc = require("../../alias-ipc-Cjs5yivH.cjs");
|
|
4
|
+
let node_process = require("node:process");
|
|
5
|
+
node_process = require_rolldown_runtime.__toESM(node_process, 1);
|
|
6
|
+
//#region src/adapters/node/alias-worker.ts
|
|
7
|
+
/**
|
|
8
|
+
* Entry point for one reply alias's real OS process, forked by ForkedAliasProcess. Excluded from the coverage gate the same way src/bin/** is (see vitest.config.ts): pure process-lifecycle glue, exercised end to end by a real fork in forked-alias-process.integration.test.ts rather than in-process unit coverage.
|
|
9
|
+
*/
|
|
10
|
+
let peer;
|
|
11
|
+
node_process.default.on("message", (raw) => {
|
|
12
|
+
handleCommand(raw);
|
|
13
|
+
});
|
|
14
|
+
async function handleCommand(raw) {
|
|
15
|
+
if (!require_alias_ipc.AliasCommandSchema.is(raw)) return;
|
|
16
|
+
if (raw.type === "stop") {
|
|
17
|
+
await peer?.stop();
|
|
18
|
+
node_process.default.exit(0);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let created;
|
|
22
|
+
try {
|
|
23
|
+
created = await require_cc_peer.CcPeer.create({
|
|
24
|
+
name: raw.name,
|
|
25
|
+
...raw.homeDir !== void 0 ? { homeDir: raw.homeDir } : {},
|
|
26
|
+
...raw.socketDir !== void 0 ? { socketDir: raw.socketDir } : {},
|
|
27
|
+
...raw.sessionId !== void 0 ? { sessionId: raw.sessionId } : {}
|
|
28
|
+
});
|
|
29
|
+
} catch {
|
|
30
|
+
node_process.default.exit(1);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
peer = created;
|
|
34
|
+
peer.on("message", (message) => {
|
|
35
|
+
node_process.default.send?.({
|
|
36
|
+
type: "message",
|
|
37
|
+
...message
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
node_process.default.send?.({ type: "started" });
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { n as CcPeer } from "../../cc-peer-BduoSupO.mjs";
|
|
2
|
+
import { t as AliasCommandSchema } from "../../alias-ipc-DkaozCo7.mjs";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
//#region src/adapters/node/alias-worker.ts
|
|
5
|
+
/**
|
|
6
|
+
* Entry point for one reply alias's real OS process, forked by ForkedAliasProcess. Excluded from the coverage gate the same way src/bin/** is (see vitest.config.ts): pure process-lifecycle glue, exercised end to end by a real fork in forked-alias-process.integration.test.ts rather than in-process unit coverage.
|
|
7
|
+
*/
|
|
8
|
+
let peer;
|
|
9
|
+
process.on("message", (raw) => {
|
|
10
|
+
handleCommand(raw);
|
|
11
|
+
});
|
|
12
|
+
async function handleCommand(raw) {
|
|
13
|
+
if (!AliasCommandSchema.is(raw)) return;
|
|
14
|
+
if (raw.type === "stop") {
|
|
15
|
+
await peer?.stop();
|
|
16
|
+
process.exit(0);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let created;
|
|
20
|
+
try {
|
|
21
|
+
created = await CcPeer.create({
|
|
22
|
+
name: raw.name,
|
|
23
|
+
...raw.homeDir !== void 0 ? { homeDir: raw.homeDir } : {},
|
|
24
|
+
...raw.socketDir !== void 0 ? { socketDir: raw.socketDir } : {},
|
|
25
|
+
...raw.sessionId !== void 0 ? { sessionId: raw.sessionId } : {}
|
|
26
|
+
});
|
|
27
|
+
} catch {
|
|
28
|
+
process.exit(1);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
peer = created;
|
|
32
|
+
peer.on("message", (message) => {
|
|
33
|
+
process.send?.({
|
|
34
|
+
type: "message",
|
|
35
|
+
...message
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
process.send?.({ type: "started" });
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const require_errors = require("./errors-Dl_lSq1N.cjs");
|
|
2
|
+
let zod = require("zod");
|
|
3
|
+
//#region src/schemas/alias-ipc.ts
|
|
4
|
+
/**
|
|
5
|
+
* The parent-to-child and child-to-parent IPC contract `AliasPool`'s Node adapter (`adapters/node/forked-alias-process.ts`) and worker (`adapters/node/alias-worker.ts`) exchange over `child_process.fork()`'s built-in channel. This is deliberately independent of the wire protocol schemas in `wire.ts`: it never touches a socket, it only carries commands and events between a pool and the real OS process backing one reply alias.
|
|
6
|
+
*/
|
|
7
|
+
const AliasStartCommandSchema = require_errors.defineSchema(zod.z.object({
|
|
8
|
+
type: zod.z.literal("start"),
|
|
9
|
+
name: zod.z.string().min(1),
|
|
10
|
+
homeDir: zod.z.string().optional(),
|
|
11
|
+
socketDir: zod.z.string().optional(),
|
|
12
|
+
sessionId: zod.z.string().optional()
|
|
13
|
+
}));
|
|
14
|
+
const AliasStopCommandSchema = require_errors.defineSchema(zod.z.object({ type: zod.z.literal("stop") }));
|
|
15
|
+
const AliasCommandSchema = require_errors.defineSchema(zod.z.union([AliasStartCommandSchema, AliasStopCommandSchema]));
|
|
16
|
+
/** Sent once the worker's own CcPeer is listening and registered. */
|
|
17
|
+
const AliasStartedEventSchema = require_errors.defineSchema(zod.z.object({ type: zod.z.literal("started") }));
|
|
18
|
+
/** Mirrors CcPeer's own InboundMessage shape, carried over IPC instead of an EventEmitter within one process. */
|
|
19
|
+
const AliasMessageEventSchema = require_errors.defineSchema(zod.z.object({
|
|
20
|
+
type: zod.z.literal("message"),
|
|
21
|
+
from: zod.z.string().optional(),
|
|
22
|
+
fromSession: zod.z.string().optional(),
|
|
23
|
+
fromName: zod.z.string().optional(),
|
|
24
|
+
fromMode: zod.z.enum(["bypass", "prompting"]).optional(),
|
|
25
|
+
hopChain: zod.z.array(zod.z.string()).optional(),
|
|
26
|
+
body: zod.z.string(),
|
|
27
|
+
msgId: zod.z.string()
|
|
28
|
+
}));
|
|
29
|
+
require_errors.defineSchema(zod.z.union([AliasStartedEventSchema, AliasMessageEventSchema]));
|
|
30
|
+
//#endregion
|
|
31
|
+
Object.defineProperty(exports, "AliasCommandSchema", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
get: function() {
|
|
34
|
+
return AliasCommandSchema;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(exports, "AliasMessageEventSchema", {
|
|
38
|
+
enumerable: true,
|
|
39
|
+
get: function() {
|
|
40
|
+
return AliasMessageEventSchema;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
Object.defineProperty(exports, "AliasStartedEventSchema", {
|
|
44
|
+
enumerable: true,
|
|
45
|
+
get: function() {
|
|
46
|
+
return AliasStartedEventSchema;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { o as defineSchema } from "./errors-Dcqq2MIW.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/schemas/alias-ipc.ts
|
|
4
|
+
/**
|
|
5
|
+
* The parent-to-child and child-to-parent IPC contract `AliasPool`'s Node adapter (`adapters/node/forked-alias-process.ts`) and worker (`adapters/node/alias-worker.ts`) exchange over `child_process.fork()`'s built-in channel. This is deliberately independent of the wire protocol schemas in `wire.ts`: it never touches a socket, it only carries commands and events between a pool and the real OS process backing one reply alias.
|
|
6
|
+
*/
|
|
7
|
+
const AliasStartCommandSchema = defineSchema(z.object({
|
|
8
|
+
type: z.literal("start"),
|
|
9
|
+
name: z.string().min(1),
|
|
10
|
+
homeDir: z.string().optional(),
|
|
11
|
+
socketDir: z.string().optional(),
|
|
12
|
+
sessionId: z.string().optional()
|
|
13
|
+
}));
|
|
14
|
+
const AliasStopCommandSchema = defineSchema(z.object({ type: z.literal("stop") }));
|
|
15
|
+
const AliasCommandSchema = defineSchema(z.union([AliasStartCommandSchema, AliasStopCommandSchema]));
|
|
16
|
+
/** Sent once the worker's own CcPeer is listening and registered. */
|
|
17
|
+
const AliasStartedEventSchema = defineSchema(z.object({ type: z.literal("started") }));
|
|
18
|
+
/** Mirrors CcPeer's own InboundMessage shape, carried over IPC instead of an EventEmitter within one process. */
|
|
19
|
+
const AliasMessageEventSchema = defineSchema(z.object({
|
|
20
|
+
type: z.literal("message"),
|
|
21
|
+
from: z.string().optional(),
|
|
22
|
+
fromSession: z.string().optional(),
|
|
23
|
+
fromName: z.string().optional(),
|
|
24
|
+
fromMode: z.enum(["bypass", "prompting"]).optional(),
|
|
25
|
+
hopChain: z.array(z.string()).optional(),
|
|
26
|
+
body: z.string(),
|
|
27
|
+
msgId: z.string()
|
|
28
|
+
}));
|
|
29
|
+
defineSchema(z.union([AliasStartedEventSchema, AliasMessageEventSchema]));
|
|
30
|
+
//#endregion
|
|
31
|
+
export { AliasMessageEventSchema as n, AliasStartedEventSchema as r, AliasCommandSchema as t };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_errors = require("./errors-Dl_lSq1N.cjs");
|
|
3
|
+
const require_alias_ipc = require("./alias-ipc-Cjs5yivH.cjs");
|
|
4
|
+
let node_events = require("node:events");
|
|
5
|
+
let node_child_process = require("node:child_process");
|
|
6
|
+
let node_url = require("node:url");
|
|
7
|
+
//#region src/adapters/node/forked-alias-process.ts
|
|
8
|
+
/**
|
|
9
|
+
* The real, Node-backed {@link AliasProcess}: forks a fresh OS process running `alias-worker.ts`, which registers a standalone CcPeer under the given name and relays every inbound reply back over the fork's own IPC channel.
|
|
10
|
+
*/
|
|
11
|
+
var ForkedAliasProcess = class {
|
|
12
|
+
events = new node_events.EventEmitter();
|
|
13
|
+
forkFn;
|
|
14
|
+
workerPath;
|
|
15
|
+
child;
|
|
16
|
+
constructor(deps) {
|
|
17
|
+
this.forkFn = deps.fork ?? node_child_process.fork;
|
|
18
|
+
this.workerPath = deps.workerPath;
|
|
19
|
+
}
|
|
20
|
+
async start(options) {
|
|
21
|
+
const child = this.forkFn(this.workerPath, [], {});
|
|
22
|
+
this.child = child;
|
|
23
|
+
child.on("message", (raw) => {
|
|
24
|
+
if (require_alias_ipc.AliasMessageEventSchema.is(raw)) this.events.emit("message", toInboundMessage(raw));
|
|
25
|
+
});
|
|
26
|
+
child.on("exit", () => {
|
|
27
|
+
this.events.emit("exit");
|
|
28
|
+
});
|
|
29
|
+
await new Promise((resolve, reject) => {
|
|
30
|
+
function onExitBeforeStart(code) {
|
|
31
|
+
child.off("message", onStarted);
|
|
32
|
+
reject(new require_errors.AliasStartError(`alias worker exited before starting (code ${code === null ? "null" : code.toString()})`));
|
|
33
|
+
}
|
|
34
|
+
function onStarted(raw) {
|
|
35
|
+
if (require_alias_ipc.AliasStartedEventSchema.is(raw)) {
|
|
36
|
+
child.off("message", onStarted);
|
|
37
|
+
child.off("exit", onExitBeforeStart);
|
|
38
|
+
resolve();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
child.on("message", onStarted);
|
|
42
|
+
child.once("exit", onExitBeforeStart);
|
|
43
|
+
child.send({
|
|
44
|
+
type: "start",
|
|
45
|
+
name: options.name,
|
|
46
|
+
...options.homeDir !== void 0 ? { homeDir: options.homeDir } : {},
|
|
47
|
+
...options.socketDir !== void 0 ? { socketDir: options.socketDir } : {},
|
|
48
|
+
...options.sessionId !== void 0 ? { sessionId: options.sessionId } : {}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async stop() {
|
|
53
|
+
const child = this.child;
|
|
54
|
+
if (child === void 0) return;
|
|
55
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
56
|
+
await new Promise((resolve) => {
|
|
57
|
+
child.once("exit", () => {
|
|
58
|
+
resolve();
|
|
59
|
+
});
|
|
60
|
+
child.send({ type: "stop" });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
function toInboundMessage(event) {
|
|
65
|
+
return {
|
|
66
|
+
...event.from !== void 0 ? { from: event.from } : {},
|
|
67
|
+
...event.fromSession !== void 0 ? { fromSession: event.fromSession } : {},
|
|
68
|
+
...event.fromName !== void 0 ? { fromName: event.fromName } : {},
|
|
69
|
+
...event.fromMode !== void 0 ? { fromMode: event.fromMode } : {},
|
|
70
|
+
...event.hopChain !== void 0 ? { hopChain: event.hopChain } : {},
|
|
71
|
+
body: event.body,
|
|
72
|
+
msgId: event.msgId
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/alias-pool.ts
|
|
77
|
+
/** Default log sink: logs go nowhere unless a logger is provided, matching CcPeer's own default. */
|
|
78
|
+
const sinkLog = () => void 0;
|
|
79
|
+
/**
|
|
80
|
+
* Mirrors this module's own real build extension (tsdown's `fixedExtension: true` always emits `.mjs` or `.cjs`, never a plain `.js`) so a computed sibling path names a file tsdown actually produces. Exported as a pure function, independent of `import.meta.url`, so both branches are directly unit-testable: in this repo's own dev/test tree `import.meta.url` always ends in `.ts`, so a test exercising this module's own `import.meta.url` can only ever observe the `.mjs` branch.
|
|
81
|
+
*/
|
|
82
|
+
function workerExtensionFor(moduleUrl) {
|
|
83
|
+
return moduleUrl.endsWith(".cjs") ? ".cjs" : ".mjs";
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* alias-worker.ts's own built location, computed from this file's `import.meta.url` rather than ForkedAliasProcess's own: this file is itself a dedicated tsdown entry (see tsdown.config.ts), so its emitted location is stable and predictable, and alias-worker.ts (also a dedicated entry) is emitted at the same relative position to it as the two source files hold to each other in src/.
|
|
87
|
+
*/
|
|
88
|
+
const DEFAULT_WORKER_PATH = (0, node_url.fileURLToPath)(new URL(`./adapters/node/alias-worker${workerExtensionFor(require("url").pathToFileURL(__filename).href)}`, require("url").pathToFileURL(__filename).href));
|
|
89
|
+
/**
|
|
90
|
+
* Lazily materialises one natively-discoverable Claude Code peer identity per mesh correspondent name, each backed by its own real OS process (see ports/alias-process.ts for why a real process is required). Emits "message" (an {@link AliasMessage}) for every reply an alias receives, and "exit" (an object with an `alias` name field) when an alias's backing process ends, whether from a deliberate retire() or an unexpected crash.
|
|
91
|
+
*/
|
|
92
|
+
var AliasPool = class AliasPool extends node_events.EventEmitter {
|
|
93
|
+
deps;
|
|
94
|
+
options;
|
|
95
|
+
active = /* @__PURE__ */ new Map();
|
|
96
|
+
pending = /* @__PURE__ */ new Map();
|
|
97
|
+
log;
|
|
98
|
+
constructor(deps, options = {}) {
|
|
99
|
+
super();
|
|
100
|
+
this.deps = deps;
|
|
101
|
+
this.options = options;
|
|
102
|
+
this.log = options.logger ?? sinkLog;
|
|
103
|
+
}
|
|
104
|
+
static create(options = {}) {
|
|
105
|
+
return new AliasPool({ spawn: () => new ForkedAliasProcess({ workerPath: DEFAULT_WORKER_PATH }) }, options);
|
|
106
|
+
}
|
|
107
|
+
/** Idempotent: a no-op if the alias is already active, and dedup'd if another ensure() for the same name is already in flight. */
|
|
108
|
+
async ensure(name) {
|
|
109
|
+
if (this.active.has(name)) return;
|
|
110
|
+
const inFlight = this.pending.get(name);
|
|
111
|
+
if (inFlight !== void 0) {
|
|
112
|
+
await inFlight;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const started = this.startAlias(name);
|
|
116
|
+
this.pending.set(name, started);
|
|
117
|
+
try {
|
|
118
|
+
await started;
|
|
119
|
+
} finally {
|
|
120
|
+
this.pending.delete(name);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async startAlias(name) {
|
|
124
|
+
const proc = this.deps.spawn();
|
|
125
|
+
proc.events.on("message", (message) => {
|
|
126
|
+
this.emit("message", {
|
|
127
|
+
alias: name,
|
|
128
|
+
...message
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
proc.events.on("exit", () => {
|
|
132
|
+
this.active.delete(name);
|
|
133
|
+
this.emit("exit", { alias: name });
|
|
134
|
+
});
|
|
135
|
+
await proc.start({
|
|
136
|
+
name,
|
|
137
|
+
...this.options.homeDir !== void 0 ? { homeDir: this.options.homeDir } : {},
|
|
138
|
+
...this.options.socketDir !== void 0 ? { socketDir: this.options.socketDir } : {}
|
|
139
|
+
});
|
|
140
|
+
this.active.set(name, proc);
|
|
141
|
+
this.log(`alias ${name} active`);
|
|
142
|
+
}
|
|
143
|
+
/** Waits for any in-flight ensure() of the same name to settle first, so a retire() issued while an alias is still starting stops it once (and if) it becomes active. A no-op for a name that is neither active nor pending. */
|
|
144
|
+
async retire(name) {
|
|
145
|
+
const inFlight = this.pending.get(name);
|
|
146
|
+
if (inFlight !== void 0) await inFlight.catch(() => void 0);
|
|
147
|
+
const proc = this.active.get(name);
|
|
148
|
+
if (proc === void 0) return;
|
|
149
|
+
this.active.delete(name);
|
|
150
|
+
await proc.stop();
|
|
151
|
+
this.log(`alias ${name} retired`);
|
|
152
|
+
}
|
|
153
|
+
async stopAll() {
|
|
154
|
+
await Promise.all([...this.active.keys()].map(async (name) => this.retire(name)));
|
|
155
|
+
}
|
|
156
|
+
activeAliases() {
|
|
157
|
+
return [...this.active.keys()];
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
//#endregion
|
|
161
|
+
exports.AliasPool = AliasPool;
|
|
162
|
+
exports.workerExtensionFor = workerExtensionFor;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { a as InboundMessage, l as PathConfig } from "./cc-peer-B3i4ZlKV.cjs";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
//#region src/ports/alias-process.d.ts
|
|
4
|
+
interface AliasStartOptions extends PathConfig {
|
|
5
|
+
name: string;
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A synthetic, natively-discoverable Claude Code peer identity backed by a real OS process. A real process is required, not a design choice: the registry is one file per real pid with a single optional name field each (see docs/PROTOCOL.md's "Reply aliases" section), so a single process can only ever publish one discoverable name at a time. `events` emits "message" (an InboundMessage-shaped object) for each reply the alias receives, and "exit" once the backing process has ended, whether from a deliberate stop() or an unexpected crash.
|
|
10
|
+
*/
|
|
11
|
+
interface AliasProcess {
|
|
12
|
+
readonly events: EventEmitter;
|
|
13
|
+
start: (options: Readonly<AliasStartOptions>) => Promise<void>;
|
|
14
|
+
stop: () => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/alias-pool.d.ts
|
|
18
|
+
export interface AliasPoolOptions extends PathConfig {
|
|
19
|
+
logger?: (message: string) => void;
|
|
20
|
+
}
|
|
21
|
+
/** An inbound reply relayed from one alias, with the correspondent name it arrived for attached. */
|
|
22
|
+
export interface AliasMessage extends InboundMessage {
|
|
23
|
+
alias: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Mirrors this module's own real build extension (tsdown's `fixedExtension: true` always emits `.mjs` or `.cjs`, never a plain `.js`) so a computed sibling path names a file tsdown actually produces. Exported as a pure function, independent of `import.meta.url`, so both branches are directly unit-testable: in this repo's own dev/test tree `import.meta.url` always ends in `.ts`, so a test exercising this module's own `import.meta.url` can only ever observe the `.mjs` branch.
|
|
27
|
+
*/
|
|
28
|
+
export declare function workerExtensionFor(moduleUrl: string): ".mjs" | ".cjs";
|
|
29
|
+
interface Deps {
|
|
30
|
+
spawn: () => AliasProcess;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Lazily materialises one natively-discoverable Claude Code peer identity per mesh correspondent name, each backed by its own real OS process (see ports/alias-process.ts for why a real process is required). Emits "message" (an {@link AliasMessage}) for every reply an alias receives, and "exit" (an object with an `alias` name field) when an alias's backing process ends, whether from a deliberate retire() or an unexpected crash.
|
|
34
|
+
*/
|
|
35
|
+
export declare class AliasPool extends EventEmitter {
|
|
36
|
+
private readonly deps;
|
|
37
|
+
private readonly options;
|
|
38
|
+
private readonly active;
|
|
39
|
+
private readonly pending;
|
|
40
|
+
private readonly log;
|
|
41
|
+
constructor(deps: Readonly<Deps>, options?: Readonly<AliasPoolOptions>);
|
|
42
|
+
static create(options?: Readonly<AliasPoolOptions>): AliasPool;
|
|
43
|
+
/** Idempotent: a no-op if the alias is already active, and dedup'd if another ensure() for the same name is already in flight. */
|
|
44
|
+
ensure(name: string): Promise<void>;
|
|
45
|
+
private startAlias;
|
|
46
|
+
/** Waits for any in-flight ensure() of the same name to settle first, so a retire() issued while an alias is still starting stops it once (and if) it becomes active. A no-op for a name that is neither active nor pending. */
|
|
47
|
+
retire(name: string): Promise<void>;
|
|
48
|
+
stopAll(): Promise<void>;
|
|
49
|
+
activeAliases(): string[];
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { a as InboundMessage, l as PathConfig } from "./cc-peer-B3i4ZlKV.mjs";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
//#region src/ports/alias-process.d.ts
|
|
4
|
+
interface AliasStartOptions extends PathConfig {
|
|
5
|
+
name: string;
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A synthetic, natively-discoverable Claude Code peer identity backed by a real OS process. A real process is required, not a design choice: the registry is one file per real pid with a single optional name field each (see docs/PROTOCOL.md's "Reply aliases" section), so a single process can only ever publish one discoverable name at a time. `events` emits "message" (an InboundMessage-shaped object) for each reply the alias receives, and "exit" once the backing process has ended, whether from a deliberate stop() or an unexpected crash.
|
|
10
|
+
*/
|
|
11
|
+
interface AliasProcess {
|
|
12
|
+
readonly events: EventEmitter;
|
|
13
|
+
start: (options: Readonly<AliasStartOptions>) => Promise<void>;
|
|
14
|
+
stop: () => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/alias-pool.d.ts
|
|
18
|
+
export interface AliasPoolOptions extends PathConfig {
|
|
19
|
+
logger?: (message: string) => void;
|
|
20
|
+
}
|
|
21
|
+
/** An inbound reply relayed from one alias, with the correspondent name it arrived for attached. */
|
|
22
|
+
export interface AliasMessage extends InboundMessage {
|
|
23
|
+
alias: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Mirrors this module's own real build extension (tsdown's `fixedExtension: true` always emits `.mjs` or `.cjs`, never a plain `.js`) so a computed sibling path names a file tsdown actually produces. Exported as a pure function, independent of `import.meta.url`, so both branches are directly unit-testable: in this repo's own dev/test tree `import.meta.url` always ends in `.ts`, so a test exercising this module's own `import.meta.url` can only ever observe the `.mjs` branch.
|
|
27
|
+
*/
|
|
28
|
+
export declare function workerExtensionFor(moduleUrl: string): ".mjs" | ".cjs";
|
|
29
|
+
interface Deps {
|
|
30
|
+
spawn: () => AliasProcess;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Lazily materialises one natively-discoverable Claude Code peer identity per mesh correspondent name, each backed by its own real OS process (see ports/alias-process.ts for why a real process is required). Emits "message" (an {@link AliasMessage}) for every reply an alias receives, and "exit" (an object with an `alias` name field) when an alias's backing process ends, whether from a deliberate retire() or an unexpected crash.
|
|
34
|
+
*/
|
|
35
|
+
export declare class AliasPool extends EventEmitter {
|
|
36
|
+
private readonly deps;
|
|
37
|
+
private readonly options;
|
|
38
|
+
private readonly active;
|
|
39
|
+
private readonly pending;
|
|
40
|
+
private readonly log;
|
|
41
|
+
constructor(deps: Readonly<Deps>, options?: Readonly<AliasPoolOptions>);
|
|
42
|
+
static create(options?: Readonly<AliasPoolOptions>): AliasPool;
|
|
43
|
+
/** Idempotent: a no-op if the alias is already active, and dedup'd if another ensure() for the same name is already in flight. */
|
|
44
|
+
ensure(name: string): Promise<void>;
|
|
45
|
+
private startAlias;
|
|
46
|
+
/** Waits for any in-flight ensure() of the same name to settle first, so a retire() issued while an alias is still starting stops it once (and if) it becomes active. A no-op for a name that is neither active nor pending. */
|
|
47
|
+
retire(name: string): Promise<void>;
|
|
48
|
+
stopAll(): Promise<void>;
|
|
49
|
+
activeAliases(): string[];
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { t as AliasStartError } from "./errors-Dcqq2MIW.mjs";
|
|
2
|
+
import { n as AliasMessageEventSchema, r as AliasStartedEventSchema } from "./alias-ipc-DkaozCo7.mjs";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import { fork } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
//#region src/adapters/node/forked-alias-process.ts
|
|
7
|
+
/**
|
|
8
|
+
* The real, Node-backed {@link AliasProcess}: forks a fresh OS process running `alias-worker.ts`, which registers a standalone CcPeer under the given name and relays every inbound reply back over the fork's own IPC channel.
|
|
9
|
+
*/
|
|
10
|
+
var ForkedAliasProcess = class {
|
|
11
|
+
events = new EventEmitter();
|
|
12
|
+
forkFn;
|
|
13
|
+
workerPath;
|
|
14
|
+
child;
|
|
15
|
+
constructor(deps) {
|
|
16
|
+
this.forkFn = deps.fork ?? fork;
|
|
17
|
+
this.workerPath = deps.workerPath;
|
|
18
|
+
}
|
|
19
|
+
async start(options) {
|
|
20
|
+
const child = this.forkFn(this.workerPath, [], {});
|
|
21
|
+
this.child = child;
|
|
22
|
+
child.on("message", (raw) => {
|
|
23
|
+
if (AliasMessageEventSchema.is(raw)) this.events.emit("message", toInboundMessage(raw));
|
|
24
|
+
});
|
|
25
|
+
child.on("exit", () => {
|
|
26
|
+
this.events.emit("exit");
|
|
27
|
+
});
|
|
28
|
+
await new Promise((resolve, reject) => {
|
|
29
|
+
function onExitBeforeStart(code) {
|
|
30
|
+
child.off("message", onStarted);
|
|
31
|
+
reject(new AliasStartError(`alias worker exited before starting (code ${code === null ? "null" : code.toString()})`));
|
|
32
|
+
}
|
|
33
|
+
function onStarted(raw) {
|
|
34
|
+
if (AliasStartedEventSchema.is(raw)) {
|
|
35
|
+
child.off("message", onStarted);
|
|
36
|
+
child.off("exit", onExitBeforeStart);
|
|
37
|
+
resolve();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
child.on("message", onStarted);
|
|
41
|
+
child.once("exit", onExitBeforeStart);
|
|
42
|
+
child.send({
|
|
43
|
+
type: "start",
|
|
44
|
+
name: options.name,
|
|
45
|
+
...options.homeDir !== void 0 ? { homeDir: options.homeDir } : {},
|
|
46
|
+
...options.socketDir !== void 0 ? { socketDir: options.socketDir } : {},
|
|
47
|
+
...options.sessionId !== void 0 ? { sessionId: options.sessionId } : {}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async stop() {
|
|
52
|
+
const child = this.child;
|
|
53
|
+
if (child === void 0) return;
|
|
54
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
55
|
+
await new Promise((resolve) => {
|
|
56
|
+
child.once("exit", () => {
|
|
57
|
+
resolve();
|
|
58
|
+
});
|
|
59
|
+
child.send({ type: "stop" });
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
function toInboundMessage(event) {
|
|
64
|
+
return {
|
|
65
|
+
...event.from !== void 0 ? { from: event.from } : {},
|
|
66
|
+
...event.fromSession !== void 0 ? { fromSession: event.fromSession } : {},
|
|
67
|
+
...event.fromName !== void 0 ? { fromName: event.fromName } : {},
|
|
68
|
+
...event.fromMode !== void 0 ? { fromMode: event.fromMode } : {},
|
|
69
|
+
...event.hopChain !== void 0 ? { hopChain: event.hopChain } : {},
|
|
70
|
+
body: event.body,
|
|
71
|
+
msgId: event.msgId
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/alias-pool.ts
|
|
76
|
+
/** Default log sink: logs go nowhere unless a logger is provided, matching CcPeer's own default. */
|
|
77
|
+
const sinkLog = () => void 0;
|
|
78
|
+
/**
|
|
79
|
+
* Mirrors this module's own real build extension (tsdown's `fixedExtension: true` always emits `.mjs` or `.cjs`, never a plain `.js`) so a computed sibling path names a file tsdown actually produces. Exported as a pure function, independent of `import.meta.url`, so both branches are directly unit-testable: in this repo's own dev/test tree `import.meta.url` always ends in `.ts`, so a test exercising this module's own `import.meta.url` can only ever observe the `.mjs` branch.
|
|
80
|
+
*/
|
|
81
|
+
function workerExtensionFor(moduleUrl) {
|
|
82
|
+
return moduleUrl.endsWith(".cjs") ? ".cjs" : ".mjs";
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* alias-worker.ts's own built location, computed from this file's `import.meta.url` rather than ForkedAliasProcess's own: this file is itself a dedicated tsdown entry (see tsdown.config.ts), so its emitted location is stable and predictable, and alias-worker.ts (also a dedicated entry) is emitted at the same relative position to it as the two source files hold to each other in src/.
|
|
86
|
+
*/
|
|
87
|
+
const DEFAULT_WORKER_PATH = fileURLToPath(new URL(`./adapters/node/alias-worker${workerExtensionFor(import.meta.url)}`, import.meta.url));
|
|
88
|
+
/**
|
|
89
|
+
* Lazily materialises one natively-discoverable Claude Code peer identity per mesh correspondent name, each backed by its own real OS process (see ports/alias-process.ts for why a real process is required). Emits "message" (an {@link AliasMessage}) for every reply an alias receives, and "exit" (an object with an `alias` name field) when an alias's backing process ends, whether from a deliberate retire() or an unexpected crash.
|
|
90
|
+
*/
|
|
91
|
+
var AliasPool = class AliasPool extends EventEmitter {
|
|
92
|
+
deps;
|
|
93
|
+
options;
|
|
94
|
+
active = /* @__PURE__ */ new Map();
|
|
95
|
+
pending = /* @__PURE__ */ new Map();
|
|
96
|
+
log;
|
|
97
|
+
constructor(deps, options = {}) {
|
|
98
|
+
super();
|
|
99
|
+
this.deps = deps;
|
|
100
|
+
this.options = options;
|
|
101
|
+
this.log = options.logger ?? sinkLog;
|
|
102
|
+
}
|
|
103
|
+
static create(options = {}) {
|
|
104
|
+
return new AliasPool({ spawn: () => new ForkedAliasProcess({ workerPath: DEFAULT_WORKER_PATH }) }, options);
|
|
105
|
+
}
|
|
106
|
+
/** Idempotent: a no-op if the alias is already active, and dedup'd if another ensure() for the same name is already in flight. */
|
|
107
|
+
async ensure(name) {
|
|
108
|
+
if (this.active.has(name)) return;
|
|
109
|
+
const inFlight = this.pending.get(name);
|
|
110
|
+
if (inFlight !== void 0) {
|
|
111
|
+
await inFlight;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const started = this.startAlias(name);
|
|
115
|
+
this.pending.set(name, started);
|
|
116
|
+
try {
|
|
117
|
+
await started;
|
|
118
|
+
} finally {
|
|
119
|
+
this.pending.delete(name);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async startAlias(name) {
|
|
123
|
+
const proc = this.deps.spawn();
|
|
124
|
+
proc.events.on("message", (message) => {
|
|
125
|
+
this.emit("message", {
|
|
126
|
+
alias: name,
|
|
127
|
+
...message
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
proc.events.on("exit", () => {
|
|
131
|
+
this.active.delete(name);
|
|
132
|
+
this.emit("exit", { alias: name });
|
|
133
|
+
});
|
|
134
|
+
await proc.start({
|
|
135
|
+
name,
|
|
136
|
+
...this.options.homeDir !== void 0 ? { homeDir: this.options.homeDir } : {},
|
|
137
|
+
...this.options.socketDir !== void 0 ? { socketDir: this.options.socketDir } : {}
|
|
138
|
+
});
|
|
139
|
+
this.active.set(name, proc);
|
|
140
|
+
this.log(`alias ${name} active`);
|
|
141
|
+
}
|
|
142
|
+
/** Waits for any in-flight ensure() of the same name to settle first, so a retire() issued while an alias is still starting stops it once (and if) it becomes active. A no-op for a name that is neither active nor pending. */
|
|
143
|
+
async retire(name) {
|
|
144
|
+
const inFlight = this.pending.get(name);
|
|
145
|
+
if (inFlight !== void 0) await inFlight.catch(() => void 0);
|
|
146
|
+
const proc = this.active.get(name);
|
|
147
|
+
if (proc === void 0) return;
|
|
148
|
+
this.active.delete(name);
|
|
149
|
+
await proc.stop();
|
|
150
|
+
this.log(`alias ${name} retired`);
|
|
151
|
+
}
|
|
152
|
+
async stopAll() {
|
|
153
|
+
await Promise.all([...this.active.keys()].map(async (name) => this.retire(name)));
|
|
154
|
+
}
|
|
155
|
+
activeAliases() {
|
|
156
|
+
return [...this.active.keys()];
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
//#endregion
|
|
160
|
+
export { AliasPool, workerExtensionFor };
|