@ctliz/agent-intercom-claude 0.12.0-connect.3

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.
Files changed (59) hide show
  1. package/.claude-plugin/plugin.json +26 -0
  2. package/.mcp.json +10 -0
  3. package/CHANGELOG.md +62 -0
  4. package/COPYRIGHT +6 -0
  5. package/LICENSE +661 -0
  6. package/LICENSE_TRANSITION.md +25 -0
  7. package/README.md +506 -0
  8. package/THIRD_PARTY_NOTICES.md +25 -0
  9. package/broker/access-credential.ts +76 -0
  10. package/broker/access-registry.ts +460 -0
  11. package/broker/audit.ts +60 -0
  12. package/broker/authorization.ts +125 -0
  13. package/broker/boss-contracts.ts +267 -0
  14. package/broker/broker.ts +2028 -0
  15. package/broker/client.ts +1048 -0
  16. package/broker/framing.ts +87 -0
  17. package/broker/ownership.ts +61 -0
  18. package/broker/paths.ts +161 -0
  19. package/broker/spawn.ts +463 -0
  20. package/broker/validation.ts +132 -0
  21. package/claude/cci.ts +512 -0
  22. package/claude/ccim.ts +31 -0
  23. package/claude/cli-runner.ts +220 -0
  24. package/claude/contact.ts +33 -0
  25. package/claude/inbox-monitor.ts +49 -0
  26. package/claude/inbox.ts +77 -0
  27. package/claude/mcp-protocol.ts +253 -0
  28. package/claude/native-bridge.ts +219 -0
  29. package/claude/native-protocol.ts +346 -0
  30. package/claude/permission-policy.ts +287 -0
  31. package/claude/runtime.ts +510 -0
  32. package/claude/server.ts +69 -0
  33. package/claude/team.ts +348 -0
  34. package/claude/transport.ts +97 -0
  35. package/claude/worker-config.ts +198 -0
  36. package/claude/worker-daemon.ts +418 -0
  37. package/commands/intercom-id.md +9 -0
  38. package/commands/intercom.md +8 -0
  39. package/config.ts +195 -0
  40. package/dist/broker.mjs +2530 -0
  41. package/dist/cci.mjs +3607 -0
  42. package/dist/ccim.mjs +3621 -0
  43. package/dist/claude-server.mjs +2730 -0
  44. package/dist/inbox-monitor.mjs +68 -0
  45. package/dist/worker-daemon.mjs +2797 -0
  46. package/durable-json.ts +25 -0
  47. package/licenses/MIT-pi-claude-link.txt +21 -0
  48. package/licenses/MIT-pi-intercom.txt +21 -0
  49. package/monitors/monitors.json +8 -0
  50. package/outbound-outbox.ts +116 -0
  51. package/package.json +93 -0
  52. package/protocol-v4/contract.ts +45 -0
  53. package/provider/protected-service.ts +162 -0
  54. package/provider/provider.mjs +34 -0
  55. package/scripts/build.mjs +71 -0
  56. package/scripts/verify-core-provenance.mjs +64 -0
  57. package/scripts/verify-packed-runtime.mjs +140 -0
  58. package/skills/claude-intercom/SKILL.md +90 -0
  59. package/types.ts +212 -0
@@ -0,0 +1,219 @@
1
+ import type { Server } from "node:net";
2
+ import { join } from "node:path";
3
+ import { IntercomClient } from "../broker/client.ts";
4
+ import { spawnBrokerIfNeeded } from "../broker/spawn.ts";
5
+ import { INTERCOM_SCOPE_ENV, intercomScopeIdFromEnvForRegistration, parseIntercomScopeIdForRegistration } from "../protocol-v4/contract.ts";
6
+ import { loadConfig } from "../config.ts";
7
+ import type { Message, SessionInfo } from "../types.ts";
8
+ import { formatAttachments, formatSessionDisplay } from "./runtime.ts";
9
+ import {
10
+ bindNativeClaudeSocket,
11
+ buildNativeReceipt,
12
+ claudeNativeSocketDir,
13
+ deregisterNativeClaudePeer,
14
+ nativePeerNameBySocket,
15
+ registerNativeClaudePeer,
16
+ sendNativeClaudeMessage,
17
+ sendNativeFrame,
18
+ stripNativeEnvelope,
19
+ updateNativeClaudePeer,
20
+ type NativeProtocolPaths,
21
+ } from "./native-protocol.ts";
22
+
23
+ interface NativeBridgeClient {
24
+ sessionId: string | null;
25
+ on(event: "message", listener: (from: SessionInfo, message: Message, deliveryId: string) => void): unknown;
26
+ on(event: "disconnected", listener: (error: Error) => void): unknown;
27
+ connect(registration: {
28
+ name: string;
29
+ cwd: string;
30
+ model: string;
31
+ pid: number;
32
+ startedAt: number;
33
+ lastActivity: number;
34
+ status: string;
35
+ }, sessionId?: string): Promise<void>;
36
+ disconnect(): Promise<void>;
37
+ acknowledgeMessage(deliveryId: string): void;
38
+ send(to: string, message: { text: string; replyTo?: string }): Promise<{ delivered: boolean; reason?: string }>;
39
+ updatePresence(update: { status?: string }): void;
40
+ }
41
+
42
+ export interface NativeClaudeBridgeIdentity {
43
+ id: string;
44
+ name: string;
45
+ cwd: string;
46
+ model?: string;
47
+ pid?: number;
48
+ }
49
+
50
+ export interface NativeClaudeBrokerBridgeOptions extends NativeProtocolPaths {
51
+ client?: NativeBridgeClient;
52
+ prepareConnection?: () => Promise<void>;
53
+ socketPath?: string;
54
+ sendNative?: typeof sendNativeClaudeMessage;
55
+ sendFrame?: typeof sendNativeFrame;
56
+ scopeId?: string;
57
+ env?: NodeJS.ProcessEnv;
58
+ }
59
+
60
+ interface PendingRelay {
61
+ from: SessionInfo;
62
+ message: Message;
63
+ }
64
+
65
+ function closeServer(server: Server): Promise<void> {
66
+ return new Promise((resolve) => server.close(() => resolve()));
67
+ }
68
+
69
+ function nativePrompt(from: SessionInfo, message: Message): string {
70
+ const reply = message.expectsReply
71
+ ? "\n\nThe sender is blocking. After acting, use Claude's built-in SendMessage tool to reply to this same native peer. A normal assistant response stays local and will not unblock the sender."
72
+ : "\n\nAct if needed. To acknowledge or report a result, use Claude's built-in SendMessage tool to reply to this same native peer; a normal assistant response stays local.";
73
+ return [
74
+ `[Intercom message from ${formatSessionDisplay(from)} (${from.id})]`,
75
+ message.content.text,
76
+ formatAttachments(message.content.attachments),
77
+ reply,
78
+ ].join("\n");
79
+ }
80
+
81
+ /**
82
+ * Broker identity for one live Claude Code process. Broker messages are injected
83
+ * through Claude's native cross-session socket; native replies are relayed back
84
+ * through the broker while preserving blocking ask correlation.
85
+ */
86
+ export class NativeClaudeBrokerBridge {
87
+ private readonly client: NativeBridgeClient;
88
+ private readonly prepareConnection: () => Promise<void>;
89
+ private readonly sendNative: typeof sendNativeClaudeMessage;
90
+ private readonly sendFrame: typeof sendNativeFrame;
91
+ private readonly pid: number;
92
+ private readonly socketPath: string;
93
+ private server?: Server;
94
+ private targetSocket = "";
95
+ private pending: PendingRelay[] = [];
96
+ private started = false;
97
+
98
+ constructor(
99
+ private readonly identity: NativeClaudeBridgeIdentity,
100
+ private readonly options: NativeClaudeBrokerBridgeOptions = {},
101
+ ) {
102
+ const scopeId = options.scopeId === undefined
103
+ ? intercomScopeIdFromEnvForRegistration(options.env ?? process.env)
104
+ : parseIntercomScopeIdForRegistration(options.scopeId);
105
+ const scopeEnv = scopeId === undefined ? {} : { [INTERCOM_SCOPE_ENV]: scopeId };
106
+ this.client = options.client ?? new IntercomClient({ env: scopeEnv });
107
+ this.prepareConnection = options.prepareConnection ?? (async () => {
108
+ const config = loadConfig();
109
+ await spawnBrokerIfNeeded(config.brokerCommand, config.brokerArgs);
110
+ });
111
+ this.sendNative = options.sendNative ?? sendNativeClaudeMessage;
112
+ this.sendFrame = options.sendFrame ?? sendNativeFrame;
113
+ this.pid = identity.pid ?? process.pid;
114
+ this.socketPath = options.socketPath ?? join(claudeNativeSocketDir(options), `agent-intercom-${this.pid}.sock`);
115
+ }
116
+
117
+ get nativeSocketPath(): string {
118
+ return this.socketPath;
119
+ }
120
+
121
+ async start(targetSocket: string): Promise<void> {
122
+ if (this.started) {
123
+ this.targetSocket = targetSocket;
124
+ return;
125
+ }
126
+ this.targetSocket = targetSocket;
127
+ this.server = await bindNativeClaudeSocket(this.socketPath, (frame) => this.handleNativeFrame(frame));
128
+ await registerNativeClaudePeer({
129
+ pid: this.pid,
130
+ sessionId: this.identity.id,
131
+ name: `${this.identity.name} bridge`,
132
+ cwd: this.identity.cwd,
133
+ socketPath: this.socketPath,
134
+ status: "idle",
135
+ registryDir: this.options.registryDir,
136
+ });
137
+ await this.prepareConnection();
138
+ this.client.on("message", (from, message, deliveryId) => {
139
+ void this.handleBrokerMessage(from, message, deliveryId);
140
+ });
141
+ this.client.on("disconnected", () => {
142
+ void updateNativeClaudePeer(this.pid, { status: "broker disconnected" }, this.options.registryDir);
143
+ });
144
+ await this.client.connect({
145
+ name: this.identity.name,
146
+ cwd: this.identity.cwd,
147
+ model: this.identity.model ?? "claude",
148
+ pid: this.pid,
149
+ startedAt: Date.now(),
150
+ lastActivity: Date.now(),
151
+ status: "idle",
152
+ }, this.identity.id);
153
+ this.started = true;
154
+ }
155
+
156
+ private async handleBrokerMessage(from: SessionInfo, message: Message, deliveryId: string): Promise<void> {
157
+ try {
158
+ this.client.updatePresence({ status: "active" });
159
+ await this.sendNative({
160
+ socketPath: this.targetSocket,
161
+ body: nativePrompt(from, message),
162
+ from: `uds:${this.socketPath}`,
163
+ fromName: this.identity.name,
164
+ });
165
+ this.pending.push({ from, message });
166
+ this.client.acknowledgeMessage(deliveryId);
167
+ } catch (error) {
168
+ const reason = error instanceof Error ? error.message : String(error);
169
+ this.client.updatePresence({ status: `native delivery error: ${reason}` });
170
+ if (message.expectsReply) {
171
+ await this.client.send(from.id, { text: `Native Claude delivery failed: ${reason}`, replyTo: message.id }).catch(() => undefined);
172
+ }
173
+ }
174
+ }
175
+
176
+ private handleNativeFrame(frame: Record<string, unknown>): void {
177
+ if (frame.type !== "user") return;
178
+ const message = frame.message;
179
+ if (typeof message !== "object" || message === null || !("content" in message)) return;
180
+ const envelope = stripNativeEnvelope((message as { content?: unknown }).content);
181
+ if (!envelope.body) return;
182
+ const relay = this.pending.shift();
183
+ const fromAddress = typeof frame.from === "string" ? frame.from : envelope.from;
184
+ if (fromAddress?.startsWith("uds:")) {
185
+ void this.sendFrame(fromAddress.slice(4), buildNativeReceipt({
186
+ status: "delivered",
187
+ from: `uds:${this.socketPath}`,
188
+ originalMessageId: typeof frame.msg_id === "string" ? frame.msg_id : undefined,
189
+ reason: "Relayed to Agent Intercom.",
190
+ })).catch(() => undefined);
191
+ }
192
+ if (!relay) return;
193
+ void this.client.send(relay.from.id, {
194
+ text: envelope.body,
195
+ ...(relay.message.expectsReply ? { replyTo: relay.message.id } : {}),
196
+ }).then(() => {
197
+ this.client.updatePresence({ status: "idle" });
198
+ }).catch(() => undefined);
199
+ }
200
+
201
+ async stop(): Promise<void> {
202
+ this.started = false;
203
+ this.pending = [];
204
+ await this.client.disconnect().catch(() => undefined);
205
+ if (this.server) await closeServer(this.server).catch(() => undefined);
206
+ this.server = undefined;
207
+ await deregisterNativeClaudePeer(this.pid, this.socketPath, this.options.registryDir);
208
+ }
209
+ }
210
+
211
+ export function nativeClaudeSenderName(frame: Record<string, unknown>, registryDir?: string): string | undefined {
212
+ const message = frame.message;
213
+ const content = typeof message === "object" && message !== null && "content" in message
214
+ ? (message as { content?: unknown }).content
215
+ : undefined;
216
+ const envelope = stripNativeEnvelope(content);
217
+ const from = typeof frame.from === "string" ? frame.from : envelope.from;
218
+ return from?.startsWith("uds:") ? nativePeerNameBySocket(from.slice(4), registryDir) ?? envelope.fromName : envelope.fromName;
219
+ }
@@ -0,0 +1,346 @@
1
+ // Native Claude Code cross-session protocol, adapted from pi-claude-link's MIT implementation.
2
+ // The protocol is intentionally isolated from the broker so compatibility can be version-gated.
3
+ import { randomUUID } from "node:crypto";
4
+ import { execFile } from "node:child_process";
5
+ import { chmod, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
6
+ import { readFileSync, readdirSync } from "node:fs";
7
+ import { connect, createServer, type Server, type Socket } from "node:net";
8
+ import { homedir } from "node:os";
9
+ import { basename, dirname, join } from "node:path";
10
+
11
+ export function claudeNativeRegistryDir(
12
+ env: NodeJS.ProcessEnv = process.env,
13
+ home = homedir(),
14
+ ): string {
15
+ return join(env.CLAUDE_CONFIG_DIR || join(home, ".claude"), "sessions");
16
+ }
17
+
18
+ export const DEFAULT_CLAUDE_SESSION_REGISTRY = claudeNativeRegistryDir();
19
+ export const MAX_NATIVE_FRAME_LINE = 1024 * 1024;
20
+
21
+ export interface NativeClaudePeer {
22
+ pid?: number;
23
+ sessionId?: string;
24
+ name: string;
25
+ cwd: string;
26
+ status: string;
27
+ kind?: string;
28
+ startedAt?: number;
29
+ socketPath: string;
30
+ live?: boolean;
31
+ }
32
+
33
+ export interface NativeClaudeUserFrame {
34
+ msgV: 1;
35
+ msg_id: string;
36
+ type: "user";
37
+ priority: string;
38
+ from?: string;
39
+ session_id?: string;
40
+ message: { role: "user"; content: string };
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ export interface NativeProtocolPaths {
45
+ registryDir?: string;
46
+ socketDir?: string;
47
+ }
48
+
49
+ export function claudeNativeSocketDir(paths: NativeProtocolPaths = {}): string {
50
+ if (paths.socketDir) return paths.socketDir;
51
+ const registryDir = paths.registryDir ?? DEFAULT_CLAUDE_SESSION_REGISTRY;
52
+ try {
53
+ for (const file of readdirSync(registryDir)) {
54
+ if (!/^\d+\.json$/.test(file)) continue;
55
+ let entry: Record<string, unknown>;
56
+ try {
57
+ entry = JSON.parse(readFileSync(join(registryDir, file), "utf8")) as Record<string, unknown>;
58
+ } catch {
59
+ continue;
60
+ }
61
+ if (typeof entry.messagingSocketPath === "string" && entry.messagingSocketPath.endsWith(".sock")) {
62
+ return dirname(entry.messagingSocketPath);
63
+ }
64
+ }
65
+ } catch {
66
+ // Registry may not exist before the first Claude process starts.
67
+ }
68
+ return join(process.env.XDG_RUNTIME_DIR || "/tmp", "cc-socks");
69
+ }
70
+
71
+ export function nativeSocketLive(socketPath: string, timeoutMs = 250): Promise<boolean> {
72
+ return new Promise((resolve) => {
73
+ if (!socketPath) return resolve(false);
74
+ const socket = connect({ path: socketPath });
75
+ let settled = false;
76
+ const done = (live: boolean) => {
77
+ if (settled) return;
78
+ settled = true;
79
+ socket.destroy();
80
+ resolve(live);
81
+ };
82
+ socket.setTimeout(timeoutMs, () => done(false));
83
+ socket.on("connect", () => done(true));
84
+ socket.on("error", () => done(false));
85
+ });
86
+ }
87
+
88
+ export async function listNativeClaudeSessions(options: NativeProtocolPaths & { excludeSocket?: string } = {}): Promise<NativeClaudePeer[]> {
89
+ const registryDir = options.registryDir ?? DEFAULT_CLAUDE_SESSION_REGISTRY;
90
+ let files: string[];
91
+ try {
92
+ files = await readdir(registryDir);
93
+ } catch {
94
+ return [];
95
+ }
96
+ const peers: NativeClaudePeer[] = [];
97
+ for (const file of files) {
98
+ if (!/^\d+\.json$/.test(file)) continue;
99
+ let entry: Record<string, unknown>;
100
+ try {
101
+ entry = JSON.parse(await readFile(join(registryDir, file), "utf8")) as Record<string, unknown>;
102
+ } catch {
103
+ continue;
104
+ }
105
+ const socketPath = typeof entry.messagingSocketPath === "string" ? entry.messagingSocketPath : "";
106
+ if (!socketPath || socketPath === options.excludeSocket) continue;
107
+ peers.push({
108
+ pid: typeof entry.pid === "number" ? entry.pid : undefined,
109
+ sessionId: typeof entry.sessionId === "string" ? entry.sessionId : undefined,
110
+ name: typeof entry.name === "string" ? entry.name : `pid ${String(entry.pid ?? "unknown")}`,
111
+ cwd: typeof entry.cwd === "string" ? entry.cwd : "?",
112
+ status: typeof entry.status === "string" ? entry.status : "unknown",
113
+ kind: typeof entry.kind === "string" ? entry.kind : undefined,
114
+ startedAt: typeof entry.startedAt === "number" ? entry.startedAt : undefined,
115
+ socketPath,
116
+ });
117
+ }
118
+ await Promise.all(peers.map(async (peer) => { peer.live = await nativeSocketLive(peer.socketPath); }));
119
+ return peers.filter((peer) => peer.live).sort((left, right) => (right.startedAt ?? 0) - (left.startedAt ?? 0));
120
+ }
121
+
122
+ export async function waitForNativeClaudePeer(
123
+ pid: number,
124
+ options: NativeProtocolPaths & { timeoutMs?: number; intervalMs?: number } = {},
125
+ ): Promise<NativeClaudePeer> {
126
+ const timeoutMs = options.timeoutMs ?? 5000;
127
+ const intervalMs = options.intervalMs ?? 50;
128
+ const deadline = Date.now() + timeoutMs;
129
+ do {
130
+ const peer = (await listNativeClaudeSessions(options)).find((candidate) => candidate.pid === pid);
131
+ if (peer) return peer;
132
+ if (Date.now() >= deadline) break;
133
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
134
+ } while (true);
135
+ throw new Error(`Claude process ${pid} did not publish a live native messaging socket within ${timeoutMs}ms`);
136
+ }
137
+
138
+ export async function resolveNativeClaudeTarget(
139
+ nameOrId: string,
140
+ options: NativeProtocolPaths & { excludeSocket?: string } = {},
141
+ ): Promise<{ target?: NativeClaudePeer; error?: string; candidates?: string[] }> {
142
+ const peers = await listNativeClaudeSessions(options);
143
+ let target = peers.find((peer) => peer.sessionId === nameOrId) ?? peers.find((peer) => peer.name === nameOrId);
144
+ if (!target) {
145
+ const prefixMatches = peers.filter((peer) => peer.name.startsWith(nameOrId));
146
+ if (prefixMatches.length === 1) target = prefixMatches[0];
147
+ else if (prefixMatches.length > 1) return { error: `ambiguous: ${prefixMatches.map((peer) => peer.name).join(", ")}` };
148
+ }
149
+ if (!target) return { error: `no live Claude session matches "${nameOrId}"`, candidates: peers.map((peer) => peer.name) };
150
+ return { target };
151
+ }
152
+
153
+ const ENVELOPE_TAG = "cross-session-message";
154
+ const escapeEnvelopeBody = (body: string) => body.replace(new RegExp(`</(?=${ENVELOPE_TAG}(?:[>\\s/]|$))`, "gi"), "<\\/");
155
+ const unescapeEnvelopeBody = (body: string) => body.replace(new RegExp(`<\\\\/(?=${ENVELOPE_TAG}(?:[>\\s/]|$))`, "gi"), "</");
156
+
157
+ export function buildNativeEnvelope(options: { from?: string; fromName?: string; fromMode?: string; body: string }): string {
158
+ const attributes: string[] = [];
159
+ if (options.from) attributes.push(`from="${options.from}"`);
160
+ if (options.fromName) attributes.push(`from-name="${options.fromName.replace(/["<>]/g, "")}"`);
161
+ if (options.fromMode) attributes.push(`from-mode="${options.fromMode}"`);
162
+ return `<${ENVELOPE_TAG}${attributes.length ? ` ${attributes.join(" ")}` : ""}>\n${escapeEnvelopeBody(options.body)}\n</${ENVELOPE_TAG}>`;
163
+ }
164
+
165
+ export function stripNativeEnvelope(content: unknown): { body: string; from?: string; fromName?: string; fromMode?: string } {
166
+ if (typeof content !== "string") return { body: "" };
167
+ const match = content.match(new RegExp(`^<${ENVELOPE_TAG}((?:\\s+[a-z-]+="[^"]*")*)>\\n([\\s\\S]*)\\n</${ENVELOPE_TAG}>$`));
168
+ if (!match) return { body: content };
169
+ const attributes: Record<string, string> = {};
170
+ for (const attribute of match[1]!.matchAll(/([a-z-]+)="([^"]*)"/g)) attributes[attribute[1]!] = attribute[2]!;
171
+ return {
172
+ body: unescapeEnvelopeBody(match[2]!),
173
+ from: attributes.from,
174
+ fromName: attributes["from-name"],
175
+ fromMode: attributes["from-mode"],
176
+ };
177
+ }
178
+
179
+ export function buildNativeUserFrame(options: { content: string; from?: string; priority?: string; sessionId?: string }): NativeClaudeUserFrame {
180
+ return {
181
+ msgV: 1,
182
+ msg_id: randomUUID(),
183
+ type: "user",
184
+ priority: options.priority || "next",
185
+ ...(options.from ? { from: options.from } : {}),
186
+ ...(options.sessionId ? { session_id: options.sessionId } : {}),
187
+ message: { role: "user", content: options.content },
188
+ };
189
+ }
190
+
191
+ export function sendNativeFrame(socketPath: string, frame: unknown, timeoutMs = 5000): Promise<string> {
192
+ return new Promise((resolve, reject) => {
193
+ const socket = connect({ path: socketPath });
194
+ socket.setTimeout(timeoutMs, () => {
195
+ socket.destroy();
196
+ reject(new Error(`timed out connecting to ${socketPath}`));
197
+ });
198
+ socket.once("error", reject);
199
+ socket.once("connect", () => socket.end(`${JSON.stringify(frame)}\n`, () => {
200
+ const id = typeof frame === "object" && frame !== null && "msg_id" in frame ? String(frame.msg_id) : "";
201
+ resolve(id);
202
+ }));
203
+ });
204
+ }
205
+
206
+ export function sendNativeClaudeMessage(options: {
207
+ socketPath: string;
208
+ body: string;
209
+ from?: string;
210
+ fromName?: string;
211
+ priority?: string;
212
+ }): Promise<string> {
213
+ const content = buildNativeEnvelope({ from: options.from, fromName: options.fromName, body: options.body });
214
+ return sendNativeFrame(options.socketPath, buildNativeUserFrame({ content, from: options.from, priority: options.priority }));
215
+ }
216
+
217
+ export function buildNativeReceipt(options: { status: string; from?: string; originalMessageId?: string; reason?: string }): Record<string, unknown> {
218
+ return {
219
+ msgV: 1,
220
+ msg_id: randomUUID(),
221
+ type: "control",
222
+ action: "peer_message_status",
223
+ status: options.status,
224
+ ...(options.reason ? { reason: options.reason } : {}),
225
+ ...(options.from ? { from: options.from } : {}),
226
+ ...(options.originalMessageId ? { orig_msg_id: options.originalMessageId } : {}),
227
+ };
228
+ }
229
+
230
+ export async function bindNativeClaudeSocket(socketPath: string, onFrame: (frame: Record<string, unknown>, connection: Socket) => void): Promise<Server> {
231
+ const directory = dirname(socketPath);
232
+ await mkdir(directory, { recursive: true, mode: 0o700 });
233
+ await chmod(directory, 0o700).catch(() => undefined);
234
+ await unlink(socketPath).catch(() => undefined);
235
+ const server = createServer((connection) => {
236
+ connection.setEncoding("utf8");
237
+ let buffer = "";
238
+ connection.on("data", (chunk: string) => {
239
+ buffer += chunk;
240
+ if (buffer.length > MAX_NATIVE_FRAME_LINE) {
241
+ buffer = "";
242
+ connection.destroy();
243
+ return;
244
+ }
245
+ let newline: number;
246
+ while ((newline = buffer.indexOf("\n")) !== -1) {
247
+ const line = buffer.slice(0, newline);
248
+ buffer = buffer.slice(newline + 1);
249
+ if (!line.trim()) continue;
250
+ try {
251
+ const frame = JSON.parse(line) as Record<string, unknown>;
252
+ onFrame(frame, connection);
253
+ } catch {
254
+ // Ignore malformed or handler-failing frames, matching Claude's peer behavior.
255
+ }
256
+ }
257
+ });
258
+ connection.on("end", () => connection.end());
259
+ connection.on("error", () => undefined);
260
+ });
261
+ await new Promise<void>((resolve, reject) => {
262
+ server.once("error", reject);
263
+ server.listen(socketPath, () => {
264
+ server.removeListener("error", reject);
265
+ resolve();
266
+ });
267
+ });
268
+ await chmod(socketPath, 0o600).catch(() => undefined);
269
+ return server;
270
+ }
271
+
272
+ function processStart(pid: number): Promise<string | undefined> {
273
+ return new Promise((resolve) => {
274
+ execFile("ps", ["-o", "lstart=", "-p", String(pid)], (error, stdout) => resolve(error ? undefined : stdout.trim() || undefined));
275
+ });
276
+ }
277
+
278
+ export async function registerNativeClaudePeer(options: {
279
+ pid: number;
280
+ sessionId?: string;
281
+ name: string;
282
+ cwd: string;
283
+ socketPath: string;
284
+ status?: string;
285
+ registryDir?: string;
286
+ }): Promise<string> {
287
+ const registryDir = options.registryDir ?? DEFAULT_CLAUDE_SESSION_REGISTRY;
288
+ await mkdir(registryDir, { recursive: true, mode: 0o700 });
289
+ const sessionId = options.sessionId || randomUUID();
290
+ const entry = {
291
+ pid: options.pid,
292
+ sessionId,
293
+ cwd: options.cwd || process.cwd(),
294
+ startedAt: Date.now(),
295
+ procStart: await processStart(options.pid),
296
+ version: "agent-intercom-claude",
297
+ peerProtocol: 1,
298
+ kind: "interactive",
299
+ entrypoint: "agent-intercom",
300
+ messagingSocketPath: options.socketPath,
301
+ name: options.name,
302
+ nameSource: "explicit",
303
+ status: options.status || "idle",
304
+ };
305
+ await writeFile(join(registryDir, `${options.pid}.json`), `${JSON.stringify(entry, null, 2)}\n`, { mode: 0o600 });
306
+ return sessionId;
307
+ }
308
+
309
+ export async function updateNativeClaudePeer(pid: number, patch: Record<string, unknown>, registryDir = DEFAULT_CLAUDE_SESSION_REGISTRY): Promise<void> {
310
+ const path = join(registryDir, `${pid}.json`);
311
+ let current: Record<string, unknown>;
312
+ try {
313
+ current = JSON.parse(await readFile(path, "utf8")) as Record<string, unknown>;
314
+ } catch {
315
+ return;
316
+ }
317
+ await writeFile(path, `${JSON.stringify({ ...current, ...patch }, null, 2)}\n`, { mode: 0o600 });
318
+ }
319
+
320
+ export async function deregisterNativeClaudePeer(pid: number, socketPath?: string, registryDir = DEFAULT_CLAUDE_SESSION_REGISTRY): Promise<void> {
321
+ await unlink(join(registryDir, `${pid}.json`)).catch(() => undefined);
322
+ if (socketPath) await unlink(socketPath).catch(() => undefined);
323
+ }
324
+
325
+ export function nativePeerNameBySocket(socketPath: string, registryDir = DEFAULT_CLAUDE_SESSION_REGISTRY): string | undefined {
326
+ if (!socketPath) return undefined;
327
+ try {
328
+ for (const file of readdirSync(registryDir)) {
329
+ if (!/^\d+\.json$/.test(file)) continue;
330
+ let entry: Record<string, unknown>;
331
+ try {
332
+ entry = JSON.parse(readFileSync(join(registryDir, file), "utf8")) as Record<string, unknown>;
333
+ } catch {
334
+ continue;
335
+ }
336
+ if (entry.messagingSocketPath === socketPath && typeof entry.name === "string") return entry.name;
337
+ }
338
+ } catch {
339
+ // Missing registry has no matching peer.
340
+ }
341
+ return undefined;
342
+ }
343
+
344
+ export function nativeSlugFromCwd(cwd: string): string {
345
+ return (basename(cwd || "claude") || "claude").replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 32);
346
+ }