@ctliz/agent-intercom-claude 0.13.1 → 0.14.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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +7 -0
- package/README.md +34 -0
- package/claude/cci.ts +13 -2
- package/claude/inbox-monitor.ts +34 -10
- package/claude/mcp-protocol.ts +1 -1
- package/claude/native-bridge.ts +8 -1
- package/claude/runtime.ts +72 -22
- package/claude/server.ts +47 -12
- package/claude/session-hook.ts +14 -0
- package/claude/session-lifecycle.ts +165 -0
- package/dist/cci.mjs +36 -1
- package/dist/ccim.mjs +36 -1
- package/dist/claude-server.mjs +254 -42
- package/dist/inbox-monitor.mjs +275 -14
- package/dist/session-hook.mjs +202 -0
- package/hooks/hooks.json +26 -0
- package/package.json +2 -1
- package/scripts/build.mjs +7 -0
- package/scripts/verify-packed-runtime.mjs +12 -1
package/dist/cci.mjs
CHANGED
|
@@ -3062,6 +3062,23 @@ async function deregisterNativeClaudePeer(pid, socketPath, registryDir = DEFAULT
|
|
|
3062
3062
|
await unlink(join8(registryDir, `${pid}.json`)).catch(() => void 0);
|
|
3063
3063
|
if (socketPath) await unlink(socketPath).catch(() => void 0);
|
|
3064
3064
|
}
|
|
3065
|
+
function nativePeerNameBySocket(socketPath, registryDir = DEFAULT_CLAUDE_SESSION_REGISTRY) {
|
|
3066
|
+
if (!socketPath) return void 0;
|
|
3067
|
+
try {
|
|
3068
|
+
for (const file of readdirSync(registryDir)) {
|
|
3069
|
+
if (!/^\d+\.json$/.test(file)) continue;
|
|
3070
|
+
let entry;
|
|
3071
|
+
try {
|
|
3072
|
+
entry = JSON.parse(readFileSync7(join8(registryDir, file), "utf8"));
|
|
3073
|
+
} catch {
|
|
3074
|
+
continue;
|
|
3075
|
+
}
|
|
3076
|
+
if (entry.messagingSocketPath === socketPath && typeof entry.name === "string") return entry.name;
|
|
3077
|
+
}
|
|
3078
|
+
} catch {
|
|
3079
|
+
}
|
|
3080
|
+
return void 0;
|
|
3081
|
+
}
|
|
3065
3082
|
|
|
3066
3083
|
// claude/native-bridge.ts
|
|
3067
3084
|
function closeServer(server) {
|
|
@@ -3141,6 +3158,12 @@ var NativeClaudeBrokerBridge = class {
|
|
|
3141
3158
|
}, this.identity.id);
|
|
3142
3159
|
this.started = true;
|
|
3143
3160
|
}
|
|
3161
|
+
async syncName(name) {
|
|
3162
|
+
if (!this.started || !name.trim() || name === this.identity.name) return;
|
|
3163
|
+
this.identity.name = name;
|
|
3164
|
+
this.client.updatePresence({ name });
|
|
3165
|
+
await updateNativeClaudePeer(this.pid, { name: `${name} bridge` }, this.options.registryDir);
|
|
3166
|
+
}
|
|
3144
3167
|
async handleBrokerMessage(from, message, deliveryId) {
|
|
3145
3168
|
try {
|
|
3146
3169
|
this.client.updatePresence({ status: "active" });
|
|
@@ -3436,7 +3459,7 @@ async function runCciTui(options, id, name, scopeId) {
|
|
|
3436
3459
|
if (resolution.selected === "mcp") {
|
|
3437
3460
|
rmSync(inboxPath, { force: true });
|
|
3438
3461
|
}
|
|
3439
|
-
const args = ["--append-system-prompt", buildTuiAppendSystemPrompt(name, id, resolution.selected)];
|
|
3462
|
+
const args = ["--name", name, "--append-system-prompt", buildTuiAppendSystemPrompt(name, id, resolution.selected)];
|
|
3440
3463
|
if (resolution.selected === "mcp") args.unshift("--plugin-dir", root);
|
|
3441
3464
|
const permission = resolveClaudePermissionPolicy(options);
|
|
3442
3465
|
if (options.model) args.push("--model", options.model);
|
|
@@ -3462,12 +3485,22 @@ async function runCciTui(options, id, name, scopeId) {
|
|
|
3462
3485
|
}
|
|
3463
3486
|
});
|
|
3464
3487
|
let bridge;
|
|
3488
|
+
let nativeNameTimer;
|
|
3489
|
+
let nativeNameSync = Promise.resolve();
|
|
3465
3490
|
if (resolution.selected === "native") {
|
|
3466
3491
|
try {
|
|
3467
3492
|
if (!child.pid) throw new Error("Claude started without a process id; native transport cannot attach");
|
|
3468
3493
|
const peer = await waitForNativeClaudePeer(child.pid);
|
|
3469
3494
|
bridge = new NativeClaudeBrokerBridge({ id, name, cwd: options.cwd, model: options.model }, { scopeId });
|
|
3470
3495
|
await bridge.start(peer.socketPath);
|
|
3496
|
+
nativeNameTimer = setInterval(() => {
|
|
3497
|
+
const name2 = nativePeerNameBySocket(peer.socketPath);
|
|
3498
|
+
if (name2) nativeNameSync = nativeNameSync.then(() => bridge.syncName(name2)).catch((error) => {
|
|
3499
|
+
process.stderr.write(`cci: native name sync failed: ${error instanceof Error ? error.message : String(error)}
|
|
3500
|
+
`);
|
|
3501
|
+
});
|
|
3502
|
+
}, 1e3);
|
|
3503
|
+
nativeNameTimer.unref();
|
|
3471
3504
|
} catch (error) {
|
|
3472
3505
|
child.kill("SIGTERM");
|
|
3473
3506
|
await waitForChildExit(child).catch(() => void 0);
|
|
@@ -3479,6 +3512,8 @@ async function runCciTui(options, id, name, scopeId) {
|
|
|
3479
3512
|
}
|
|
3480
3513
|
}
|
|
3481
3514
|
const [code, signal] = await waitForChildExit(child);
|
|
3515
|
+
clearInterval(nativeNameTimer);
|
|
3516
|
+
await nativeNameSync;
|
|
3482
3517
|
await bridge?.stop();
|
|
3483
3518
|
if (resolution.selected === "mcp") rmSync(inboxPath, { force: true });
|
|
3484
3519
|
if (typeof code === "number") return code;
|
package/dist/ccim.mjs
CHANGED
|
@@ -3066,6 +3066,23 @@ async function deregisterNativeClaudePeer(pid, socketPath, registryDir = DEFAULT
|
|
|
3066
3066
|
await unlink(join8(registryDir, `${pid}.json`)).catch(() => void 0);
|
|
3067
3067
|
if (socketPath) await unlink(socketPath).catch(() => void 0);
|
|
3068
3068
|
}
|
|
3069
|
+
function nativePeerNameBySocket(socketPath, registryDir = DEFAULT_CLAUDE_SESSION_REGISTRY) {
|
|
3070
|
+
if (!socketPath) return void 0;
|
|
3071
|
+
try {
|
|
3072
|
+
for (const file of readdirSync(registryDir)) {
|
|
3073
|
+
if (!/^\d+\.json$/.test(file)) continue;
|
|
3074
|
+
let entry;
|
|
3075
|
+
try {
|
|
3076
|
+
entry = JSON.parse(readFileSync7(join8(registryDir, file), "utf8"));
|
|
3077
|
+
} catch {
|
|
3078
|
+
continue;
|
|
3079
|
+
}
|
|
3080
|
+
if (entry.messagingSocketPath === socketPath && typeof entry.name === "string") return entry.name;
|
|
3081
|
+
}
|
|
3082
|
+
} catch {
|
|
3083
|
+
}
|
|
3084
|
+
return void 0;
|
|
3085
|
+
}
|
|
3069
3086
|
|
|
3070
3087
|
// claude/native-bridge.ts
|
|
3071
3088
|
function closeServer(server) {
|
|
@@ -3145,6 +3162,12 @@ var NativeClaudeBrokerBridge = class {
|
|
|
3145
3162
|
}, this.identity.id);
|
|
3146
3163
|
this.started = true;
|
|
3147
3164
|
}
|
|
3165
|
+
async syncName(name) {
|
|
3166
|
+
if (!this.started || !name.trim() || name === this.identity.name) return;
|
|
3167
|
+
this.identity.name = name;
|
|
3168
|
+
this.client.updatePresence({ name });
|
|
3169
|
+
await updateNativeClaudePeer(this.pid, { name: `${name} bridge` }, this.options.registryDir);
|
|
3170
|
+
}
|
|
3148
3171
|
async handleBrokerMessage(from, message, deliveryId) {
|
|
3149
3172
|
try {
|
|
3150
3173
|
this.client.updatePresence({ status: "active" });
|
|
@@ -3440,7 +3463,7 @@ async function runCciTui(options, id, name, scopeId) {
|
|
|
3440
3463
|
if (resolution.selected === "mcp") {
|
|
3441
3464
|
rmSync(inboxPath, { force: true });
|
|
3442
3465
|
}
|
|
3443
|
-
const args = ["--append-system-prompt", buildTuiAppendSystemPrompt(name, id, resolution.selected)];
|
|
3466
|
+
const args = ["--name", name, "--append-system-prompt", buildTuiAppendSystemPrompt(name, id, resolution.selected)];
|
|
3444
3467
|
if (resolution.selected === "mcp") args.unshift("--plugin-dir", root);
|
|
3445
3468
|
const permission = resolveClaudePermissionPolicy(options);
|
|
3446
3469
|
if (options.model) args.push("--model", options.model);
|
|
@@ -3466,12 +3489,22 @@ async function runCciTui(options, id, name, scopeId) {
|
|
|
3466
3489
|
}
|
|
3467
3490
|
});
|
|
3468
3491
|
let bridge;
|
|
3492
|
+
let nativeNameTimer;
|
|
3493
|
+
let nativeNameSync = Promise.resolve();
|
|
3469
3494
|
if (resolution.selected === "native") {
|
|
3470
3495
|
try {
|
|
3471
3496
|
if (!child.pid) throw new Error("Claude started without a process id; native transport cannot attach");
|
|
3472
3497
|
const peer = await waitForNativeClaudePeer(child.pid);
|
|
3473
3498
|
bridge = new NativeClaudeBrokerBridge({ id, name, cwd: options.cwd, model: options.model }, { scopeId });
|
|
3474
3499
|
await bridge.start(peer.socketPath);
|
|
3500
|
+
nativeNameTimer = setInterval(() => {
|
|
3501
|
+
const name2 = nativePeerNameBySocket(peer.socketPath);
|
|
3502
|
+
if (name2) nativeNameSync = nativeNameSync.then(() => bridge.syncName(name2)).catch((error) => {
|
|
3503
|
+
process.stderr.write(`cci: native name sync failed: ${error instanceof Error ? error.message : String(error)}
|
|
3504
|
+
`);
|
|
3505
|
+
});
|
|
3506
|
+
}, 1e3);
|
|
3507
|
+
nativeNameTimer.unref();
|
|
3475
3508
|
} catch (error) {
|
|
3476
3509
|
child.kill("SIGTERM");
|
|
3477
3510
|
await waitForChildExit(child).catch(() => void 0);
|
|
@@ -3483,6 +3516,8 @@ async function runCciTui(options, id, name, scopeId) {
|
|
|
3483
3516
|
}
|
|
3484
3517
|
}
|
|
3485
3518
|
const [code, signal] = await waitForChildExit(child);
|
|
3519
|
+
clearInterval(nativeNameTimer);
|
|
3520
|
+
await nativeNameSync;
|
|
3486
3521
|
await bridge?.stop();
|
|
3487
3522
|
if (resolution.selected === "mcp") rmSync(inboxPath, { force: true });
|
|
3488
3523
|
if (typeof code === "number") return code;
|
package/dist/claude-server.mjs
CHANGED
|
@@ -1847,6 +1847,12 @@ function loadConfig() {
|
|
|
1847
1847
|
// claude/inbox.ts
|
|
1848
1848
|
import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1849
1849
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
1850
|
+
function sanitizeInboxSegment(value) {
|
|
1851
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "session";
|
|
1852
|
+
}
|
|
1853
|
+
function defaultInboxPath(sessionId, intercomDir = getIntercomDirPath()) {
|
|
1854
|
+
return join5(intercomDir, `inbox-${sanitizeInboxSegment(sessionId)}.jsonl`);
|
|
1855
|
+
}
|
|
1850
1856
|
function toInboxEntry(from, message, now = Date.now()) {
|
|
1851
1857
|
return {
|
|
1852
1858
|
ts: now,
|
|
@@ -2436,6 +2442,7 @@ var ClaudeIntercomRuntime = class {
|
|
|
2436
2442
|
reconnectTimer = null;
|
|
2437
2443
|
reconnectAttempt = 0;
|
|
2438
2444
|
reconnectEnabled = true;
|
|
2445
|
+
registrationConflict = null;
|
|
2439
2446
|
identity;
|
|
2440
2447
|
unread = [];
|
|
2441
2448
|
unresolvedAsks = /* @__PURE__ */ new Map();
|
|
@@ -2460,7 +2467,34 @@ var ClaudeIntercomRuntime = class {
|
|
|
2460
2467
|
getIdentity() {
|
|
2461
2468
|
return this.identity;
|
|
2462
2469
|
}
|
|
2470
|
+
async syncSession(identity) {
|
|
2471
|
+
const changed = identity.sessionId !== this.identity.sessionId || identity.cwd !== this.identity.cwd;
|
|
2472
|
+
if (changed) {
|
|
2473
|
+
await this.disconnect();
|
|
2474
|
+
this.unread = [];
|
|
2475
|
+
this.unresolvedAsks.clear();
|
|
2476
|
+
this.registrationConflict = null;
|
|
2477
|
+
}
|
|
2478
|
+
const renamed = identity.name !== this.identity.name;
|
|
2479
|
+
const modelChanged = identity.model !== this.identity.model;
|
|
2480
|
+
this.identity = identity;
|
|
2481
|
+
if (changed) await this.connect();
|
|
2482
|
+
else if (renamed || modelChanged) this.client?.updatePresence({ name: identity.name, model: identity.model });
|
|
2483
|
+
}
|
|
2484
|
+
pauseOnRegistrationConflict(error2) {
|
|
2485
|
+
let cause = error2;
|
|
2486
|
+
for (let depth = 0; cause instanceof Error && depth < 8; depth++, cause = cause.cause) {
|
|
2487
|
+
if (cause.code === "SESSION_ID_IN_USE") {
|
|
2488
|
+
this.registrationConflict = cause;
|
|
2489
|
+
this.reconnectEnabled = false;
|
|
2490
|
+
this.clearReconnectTimer();
|
|
2491
|
+
return true;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
return false;
|
|
2495
|
+
}
|
|
2463
2496
|
async connect() {
|
|
2497
|
+
if (this.registrationConflict) throw this.registrationConflict;
|
|
2464
2498
|
this.reconnectEnabled = true;
|
|
2465
2499
|
this.clearReconnectTimer();
|
|
2466
2500
|
if (this.client?.isConnected()) return this.client;
|
|
@@ -2468,6 +2502,9 @@ var ClaudeIntercomRuntime = class {
|
|
|
2468
2502
|
this.connectPromise = this.connectOnce();
|
|
2469
2503
|
try {
|
|
2470
2504
|
return await this.connectPromise;
|
|
2505
|
+
} catch (error2) {
|
|
2506
|
+
this.pauseOnRegistrationConflict(error2);
|
|
2507
|
+
throw error2;
|
|
2471
2508
|
} finally {
|
|
2472
2509
|
this.connectPromise = null;
|
|
2473
2510
|
}
|
|
@@ -2479,32 +2516,36 @@ var ClaudeIntercomRuntime = class {
|
|
|
2479
2516
|
this.handleIncomingMessage(from, message);
|
|
2480
2517
|
client.acknowledgeMessage(deliveryId);
|
|
2481
2518
|
});
|
|
2519
|
+
client.on("error", (error2) => {
|
|
2520
|
+
this.pauseOnRegistrationConflict(error2);
|
|
2521
|
+
process.stderr.write(`claude-intercom: ${error2.message}
|
|
2522
|
+
`);
|
|
2523
|
+
});
|
|
2482
2524
|
client.on("disconnected", (error2) => {
|
|
2483
|
-
|
|
2484
|
-
clearTimeout(waiter.timeout);
|
|
2485
|
-
waiter.cleanup?.();
|
|
2486
|
-
waiter.reject(new Error(`Disconnected while waiting for reply: ${error2.message}`, { cause: error2 }));
|
|
2487
|
-
}
|
|
2488
|
-
this.replyWaiters.clear();
|
|
2525
|
+
this.rejectReplyWaiters(new Error(`Disconnected while waiting for reply: ${error2.message}`, { cause: error2 }));
|
|
2489
2526
|
if (this.client === client) this.client = null;
|
|
2490
2527
|
this.scheduleReconnect();
|
|
2491
2528
|
});
|
|
2529
|
+
const registeredIdentity = this.identity;
|
|
2492
2530
|
await client.connect({
|
|
2493
|
-
name:
|
|
2494
|
-
cwd:
|
|
2495
|
-
model:
|
|
2531
|
+
name: registeredIdentity.name,
|
|
2532
|
+
cwd: registeredIdentity.cwd,
|
|
2533
|
+
model: registeredIdentity.model,
|
|
2496
2534
|
pid: process.pid,
|
|
2497
|
-
startedAt:
|
|
2535
|
+
startedAt: registeredIdentity.startedAt,
|
|
2498
2536
|
lastActivity: Date.now(),
|
|
2499
2537
|
status: "idle"
|
|
2500
|
-
},
|
|
2538
|
+
}, registeredIdentity.sessionId);
|
|
2501
2539
|
this.client = client;
|
|
2540
|
+
if (registeredIdentity.name !== this.identity.name || registeredIdentity.model !== this.identity.model) {
|
|
2541
|
+
client.updatePresence({ name: this.identity.name, model: this.identity.model });
|
|
2542
|
+
}
|
|
2502
2543
|
this.reconnectAttempt = 0;
|
|
2503
2544
|
return client;
|
|
2504
2545
|
}
|
|
2505
2546
|
scheduleReconnect() {
|
|
2506
2547
|
if (!this.reconnectEnabled || this.reconnectTimer) return;
|
|
2507
|
-
const
|
|
2548
|
+
const delay2 = this.reconnectDelays[Math.min(this.reconnectAttempt, this.reconnectDelays.length - 1)];
|
|
2508
2549
|
this.reconnectTimer = setTimeout(() => {
|
|
2509
2550
|
this.reconnectTimer = null;
|
|
2510
2551
|
void this.connect().then((client) => {
|
|
@@ -2516,7 +2557,7 @@ var ClaudeIntercomRuntime = class {
|
|
|
2516
2557
|
this.reconnectAttempt += 1;
|
|
2517
2558
|
this.scheduleReconnect();
|
|
2518
2559
|
});
|
|
2519
|
-
},
|
|
2560
|
+
}, delay2);
|
|
2520
2561
|
this.reconnectTimer.unref?.();
|
|
2521
2562
|
}
|
|
2522
2563
|
clearReconnectTimer() {
|
|
@@ -2524,8 +2565,17 @@ var ClaudeIntercomRuntime = class {
|
|
|
2524
2565
|
clearTimeout(this.reconnectTimer);
|
|
2525
2566
|
this.reconnectTimer = null;
|
|
2526
2567
|
}
|
|
2568
|
+
rejectReplyWaiters(error2) {
|
|
2569
|
+
for (const waiter of this.replyWaiters.values()) {
|
|
2570
|
+
clearTimeout(waiter.timeout);
|
|
2571
|
+
waiter.cleanup?.();
|
|
2572
|
+
waiter.reject(error2);
|
|
2573
|
+
}
|
|
2574
|
+
this.replyWaiters.clear();
|
|
2575
|
+
}
|
|
2527
2576
|
async disconnect() {
|
|
2528
2577
|
this.reconnectEnabled = false;
|
|
2578
|
+
this.rejectReplyWaiters(new Error("Claude session disconnected while waiting for reply"));
|
|
2529
2579
|
this.clearReconnectTimer();
|
|
2530
2580
|
if (this.connectPromise) {
|
|
2531
2581
|
try {
|
|
@@ -2600,12 +2650,9 @@ var ClaudeIntercomRuntime = class {
|
|
|
2600
2650
|
if (message.expectsReply) {
|
|
2601
2651
|
this.unresolvedAsks.set(message.id, entry);
|
|
2602
2652
|
}
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
appendInboxMessage(inboxPath, from, message);
|
|
2607
|
-
} catch {
|
|
2608
|
-
}
|
|
2653
|
+
try {
|
|
2654
|
+
appendInboxMessage(process.env.CLAUDE_INTERCOM_INBOX || defaultInboxPath(this.identity.sessionId), from, message);
|
|
2655
|
+
} catch {
|
|
2609
2656
|
}
|
|
2610
2657
|
}
|
|
2611
2658
|
waitForReply(from, replyTo, timeoutMs = getAskTimeoutMs(), signal) {
|
|
@@ -2657,6 +2704,17 @@ cwd: ${this.identity.cwd}`,
|
|
|
2657
2704
|
return textResult(formatIntercomTeam(team), team);
|
|
2658
2705
|
}
|
|
2659
2706
|
async status() {
|
|
2707
|
+
if (this.registrationConflict) {
|
|
2708
|
+
return textResult(`Connected: No
|
|
2709
|
+
Session ID: ${this.identity.sessionId}
|
|
2710
|
+
Registration conflict: ${this.registrationConflict.message}
|
|
2711
|
+
Automatic reconnect paused; the incumbent session was not replaced.`, {
|
|
2712
|
+
connected: false,
|
|
2713
|
+
session_id: this.identity.sessionId,
|
|
2714
|
+
registration_conflict: { code: "SESSION_ID_IN_USE", message: this.registrationConflict.message },
|
|
2715
|
+
reconnect_paused: true
|
|
2716
|
+
}, true);
|
|
2717
|
+
}
|
|
2660
2718
|
const client = await this.connect();
|
|
2661
2719
|
const sessions = await client.listSessions();
|
|
2662
2720
|
return textResult(
|
|
@@ -2813,21 +2871,21 @@ var attachmentsSchema = {
|
|
|
2813
2871
|
additionalProperties: false
|
|
2814
2872
|
}
|
|
2815
2873
|
};
|
|
2816
|
-
function buildToolDefinitions(
|
|
2874
|
+
function buildToolDefinitions(runtime) {
|
|
2817
2875
|
const tools = [
|
|
2818
2876
|
{
|
|
2819
2877
|
name: "intercom_whoami",
|
|
2820
2878
|
description: "Return this Claude session's intercom identity for reliable targeting.",
|
|
2821
2879
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
2822
2880
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
2823
|
-
handler: async () =>
|
|
2881
|
+
handler: async () => runtime.whoami()
|
|
2824
2882
|
},
|
|
2825
2883
|
{
|
|
2826
2884
|
name: "intercom_team",
|
|
2827
2885
|
description: "Show your current manager and the live coworkers owned by that manager. No arguments are required.",
|
|
2828
2886
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
2829
2887
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2830
|
-
handler: async () =>
|
|
2888
|
+
handler: async () => runtime.team()
|
|
2831
2889
|
},
|
|
2832
2890
|
{
|
|
2833
2891
|
name: "intercom_join",
|
|
@@ -2841,7 +2899,7 @@ function buildToolDefinitions(runtime2) {
|
|
|
2841
2899
|
additionalProperties: false
|
|
2842
2900
|
},
|
|
2843
2901
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2844
|
-
handler: async (args) =>
|
|
2902
|
+
handler: async (args) => runtime.join(
|
|
2845
2903
|
typeof args.name === "string" ? args.name : void 0,
|
|
2846
2904
|
args.create === true
|
|
2847
2905
|
)
|
|
@@ -2851,7 +2909,7 @@ function buildToolDefinitions(runtime2) {
|
|
|
2851
2909
|
description: "Show intercom connection status, active sessions, unread messages, and pending asks.",
|
|
2852
2910
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
2853
2911
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2854
|
-
handler: async () =>
|
|
2912
|
+
handler: async () => runtime.status()
|
|
2855
2913
|
},
|
|
2856
2914
|
{
|
|
2857
2915
|
name: "intercom_list",
|
|
@@ -2865,7 +2923,7 @@ function buildToolDefinitions(runtime2) {
|
|
|
2865
2923
|
additionalProperties: false
|
|
2866
2924
|
},
|
|
2867
2925
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2868
|
-
handler: async (args) =>
|
|
2926
|
+
handler: async (args) => runtime.list(
|
|
2869
2927
|
args.scope === "directory" || args.scope === "repo" ? args.scope : "machine",
|
|
2870
2928
|
asBoolean(args.include_self, false)
|
|
2871
2929
|
)
|
|
@@ -2880,7 +2938,7 @@ function buildToolDefinitions(runtime2) {
|
|
|
2880
2938
|
additionalProperties: false
|
|
2881
2939
|
},
|
|
2882
2940
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
2883
|
-
handler: async (args) =>
|
|
2941
|
+
handler: async (args) => runtime.setSummary(asString(args.summary, "summary"))
|
|
2884
2942
|
},
|
|
2885
2943
|
{
|
|
2886
2944
|
name: "intercom_send",
|
|
@@ -2896,7 +2954,7 @@ function buildToolDefinitions(runtime2) {
|
|
|
2896
2954
|
additionalProperties: false
|
|
2897
2955
|
},
|
|
2898
2956
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2899
|
-
handler: async (args) =>
|
|
2957
|
+
handler: async (args) => runtime.send(asString(args.to, "to"), asString(args.message, "message"), asAttachmentArray(args.attachments))
|
|
2900
2958
|
},
|
|
2901
2959
|
{
|
|
2902
2960
|
name: "intercom_ask",
|
|
@@ -2913,7 +2971,7 @@ function buildToolDefinitions(runtime2) {
|
|
|
2913
2971
|
additionalProperties: false
|
|
2914
2972
|
},
|
|
2915
2973
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2916
|
-
handler: async (args, signal) =>
|
|
2974
|
+
handler: async (args, signal) => runtime.ask(
|
|
2917
2975
|
asString(args.to, "to"),
|
|
2918
2976
|
asString(args.message, "message"),
|
|
2919
2977
|
asAttachmentArray(args.attachments),
|
|
@@ -2930,7 +2988,7 @@ function buildToolDefinitions(runtime2) {
|
|
|
2930
2988
|
additionalProperties: false
|
|
2931
2989
|
},
|
|
2932
2990
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2933
|
-
handler: async (args) =>
|
|
2991
|
+
handler: async (args) => runtime.pending(asBoolean(args.mark_read, false))
|
|
2934
2992
|
},
|
|
2935
2993
|
{
|
|
2936
2994
|
name: "intercom_reply",
|
|
@@ -2946,7 +3004,7 @@ function buildToolDefinitions(runtime2) {
|
|
|
2946
3004
|
additionalProperties: false
|
|
2947
3005
|
},
|
|
2948
3006
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2949
|
-
handler: async (args) =>
|
|
3007
|
+
handler: async (args) => runtime.reply(asString(args.message, "message"), typeof args.to === "string" ? args.to : void 0, args.which === "oldest" || args.which === "latest" ? args.which : void 0)
|
|
2950
3008
|
}
|
|
2951
3009
|
];
|
|
2952
3010
|
return tools;
|
|
@@ -2957,7 +3015,7 @@ function ok(id, result) {
|
|
|
2957
3015
|
function error(id, code, message) {
|
|
2958
3016
|
return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
|
|
2959
3017
|
}
|
|
2960
|
-
async function handleMcpRequest(request,
|
|
3018
|
+
async function handleMcpRequest(request, runtime) {
|
|
2961
3019
|
if (!request.method) {
|
|
2962
3020
|
return error(request.id, -32600, "Invalid request");
|
|
2963
3021
|
}
|
|
@@ -2971,13 +3029,13 @@ async function handleMcpRequest(request, runtime2) {
|
|
|
2971
3029
|
if (request.id === void 0 && request.method.startsWith("notifications/")) {
|
|
2972
3030
|
return void 0;
|
|
2973
3031
|
}
|
|
2974
|
-
const tools = buildToolDefinitions(
|
|
3032
|
+
const tools = buildToolDefinitions(runtime);
|
|
2975
3033
|
switch (request.method) {
|
|
2976
3034
|
case "initialize":
|
|
2977
3035
|
return ok(request.id, {
|
|
2978
3036
|
protocolVersion: "2025-06-18",
|
|
2979
3037
|
capabilities: { tools: {} },
|
|
2980
|
-
serverInfo: { name: "claude-intercom", version: "0.
|
|
3038
|
+
serverInfo: { name: "claude-intercom", version: "0.14.0" }
|
|
2981
3039
|
});
|
|
2982
3040
|
case "ping":
|
|
2983
3041
|
return ok(request.id, {});
|
|
@@ -3011,19 +3069,169 @@ async function handleMcpRequest(request, runtime2) {
|
|
|
3011
3069
|
}
|
|
3012
3070
|
}
|
|
3013
3071
|
|
|
3072
|
+
// claude/session-lifecycle.ts
|
|
3073
|
+
import { execFileSync } from "node:child_process";
|
|
3074
|
+
import { closeSync as closeSync2, openSync as openSync2, readFileSync as readFileSync7, readSync, renameSync as renameSync3, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3075
|
+
import { basename as basename2, dirname as dirname4, join as join8 } from "node:path";
|
|
3076
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3077
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3078
|
+
function findClaudeHost(pid = process.ppid) {
|
|
3079
|
+
for (let depth = 0; pid > 1 && depth < 16; depth++) {
|
|
3080
|
+
let parts;
|
|
3081
|
+
try {
|
|
3082
|
+
parts = execFileSync(
|
|
3083
|
+
"ps",
|
|
3084
|
+
["-p", String(pid), "-o", "ppid=", "-o", "lstart=", "-o", "comm="],
|
|
3085
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
3086
|
+
).trim().split(/\s+/);
|
|
3087
|
+
} catch {
|
|
3088
|
+
return void 0;
|
|
3089
|
+
}
|
|
3090
|
+
const command = basename2(parts.slice(6).join(" "));
|
|
3091
|
+
if (/^claude(?:\.exe)?$/i.test(command)) {
|
|
3092
|
+
return { pid, startedAt: parts.slice(1, 6).join(" ") };
|
|
3093
|
+
}
|
|
3094
|
+
if (!/^(sh|bash|zsh|dash|fish)$/.test(command)) return void 0;
|
|
3095
|
+
pid = Number(parts[0]);
|
|
3096
|
+
}
|
|
3097
|
+
return void 0;
|
|
3098
|
+
}
|
|
3099
|
+
function claudeSessionMetadataPath(host) {
|
|
3100
|
+
return join8(getIntercomDirPath(), `claude-host-${host.pid}.json`);
|
|
3101
|
+
}
|
|
3102
|
+
function readClaudeSessionMetadata(path, host) {
|
|
3103
|
+
try {
|
|
3104
|
+
const data = JSON.parse(readFileSync7(path, "utf8"));
|
|
3105
|
+
if (data.hostStartedAt !== host.startedAt || typeof data.sessionId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(data.sessionId) || typeof data.cwd !== "string" || typeof data.transcriptPath !== "string" || typeof data.startedAt !== "number" || typeof data.ended !== "boolean" || data.name !== void 0 && typeof data.name !== "string" || data.model !== void 0 && typeof data.model !== "string") return void 0;
|
|
3106
|
+
return data;
|
|
3107
|
+
} catch {
|
|
3108
|
+
return void 0;
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
function claudeIdentityForSession(metadata, env = process.env, pid = process.pid) {
|
|
3112
|
+
const identity = buildClaudeRuntimeIdentity(env, metadata?.cwd ?? env.PWD ?? process.cwd(), pid);
|
|
3113
|
+
if (!metadata) return identity;
|
|
3114
|
+
if (!env.CLAUDE_INTERCOM_SESSION_ID?.trim() && !env.CLAUDE_PEER_ID?.trim() && !env.AGENT_INTERCOM_SESSION_ID?.trim()) {
|
|
3115
|
+
identity.sessionId = `claude-${metadata.sessionId}`;
|
|
3116
|
+
}
|
|
3117
|
+
identity.name = metadata.name?.trim() || identity.name;
|
|
3118
|
+
if (!env.CLAUDE_INTERCOM_MODEL?.trim()) identity.model = metadata.model?.trim() || identity.model;
|
|
3119
|
+
identity.startedAt = metadata.startedAt;
|
|
3120
|
+
return identity;
|
|
3121
|
+
}
|
|
3122
|
+
var ClaudeSessionReader = class {
|
|
3123
|
+
constructor(path, host) {
|
|
3124
|
+
this.path = path;
|
|
3125
|
+
this.host = host;
|
|
3126
|
+
}
|
|
3127
|
+
path;
|
|
3128
|
+
host;
|
|
3129
|
+
transcriptPath = "";
|
|
3130
|
+
sessionId = "";
|
|
3131
|
+
offset = 0;
|
|
3132
|
+
partial = "";
|
|
3133
|
+
decoder = new StringDecoder("utf8");
|
|
3134
|
+
name;
|
|
3135
|
+
startedAt = 0;
|
|
3136
|
+
read() {
|
|
3137
|
+
const metadata = readClaudeSessionMetadata(this.path, this.host);
|
|
3138
|
+
if (!metadata || metadata.ended) return metadata;
|
|
3139
|
+
if (metadata.sessionId !== this.sessionId || metadata.transcriptPath !== this.transcriptPath || metadata.startedAt !== this.startedAt) {
|
|
3140
|
+
this.sessionId = metadata.sessionId;
|
|
3141
|
+
this.transcriptPath = metadata.transcriptPath;
|
|
3142
|
+
this.startedAt = metadata.startedAt;
|
|
3143
|
+
this.name = void 0;
|
|
3144
|
+
this.partial = "";
|
|
3145
|
+
this.decoder = new StringDecoder("utf8");
|
|
3146
|
+
try {
|
|
3147
|
+
this.offset = Math.max(0, statSync(this.transcriptPath).size - 64 * 1024);
|
|
3148
|
+
} catch {
|
|
3149
|
+
this.offset = 0;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
let fd;
|
|
3153
|
+
try {
|
|
3154
|
+
fd = openSync2(this.transcriptPath, "r");
|
|
3155
|
+
const size = statSync(this.transcriptPath).size;
|
|
3156
|
+
if (size < this.offset) {
|
|
3157
|
+
this.offset = 0;
|
|
3158
|
+
this.partial = "";
|
|
3159
|
+
this.decoder = new StringDecoder("utf8");
|
|
3160
|
+
}
|
|
3161
|
+
const buffer = Buffer.alloc(Math.min(size - this.offset, 64 * 1024));
|
|
3162
|
+
const count = readSync(fd, buffer, 0, buffer.length, this.offset);
|
|
3163
|
+
this.offset += count;
|
|
3164
|
+
const lines = (this.partial + this.decoder.write(buffer.subarray(0, count))).split("\n");
|
|
3165
|
+
this.partial = lines.pop() ?? "";
|
|
3166
|
+
if (this.partial.length > 64 * 1024) this.partial = "";
|
|
3167
|
+
for (const line of lines) {
|
|
3168
|
+
if (!line.includes('"custom-title"')) continue;
|
|
3169
|
+
try {
|
|
3170
|
+
const record = JSON.parse(line);
|
|
3171
|
+
if (record.type === "custom-title" && record.sessionId === metadata.sessionId && typeof record.customTitle === "string" && record.customTitle.trim()) this.name = record.customTitle;
|
|
3172
|
+
} catch {
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
} catch {
|
|
3176
|
+
} finally {
|
|
3177
|
+
if (fd !== void 0) closeSync2(fd);
|
|
3178
|
+
}
|
|
3179
|
+
return { ...metadata, name: this.name ?? metadata.name };
|
|
3180
|
+
}
|
|
3181
|
+
};
|
|
3182
|
+
async function waitForClaudeSession(reader) {
|
|
3183
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
3184
|
+
const metadata = reader.read();
|
|
3185
|
+
if (metadata) return metadata;
|
|
3186
|
+
await delay(50);
|
|
3187
|
+
}
|
|
3188
|
+
return reader.read();
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3014
3191
|
// claude/server.ts
|
|
3015
|
-
var
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3192
|
+
var shuttingDown = false;
|
|
3193
|
+
var stopPolling = () => {
|
|
3194
|
+
};
|
|
3195
|
+
var pendingSync = Promise.resolve();
|
|
3196
|
+
var report = (error2) => {
|
|
3197
|
+
process.stderr.write(`claude-intercom: ${error2 instanceof Error ? error2.message : String(error2)}
|
|
3019
3198
|
`);
|
|
3020
|
-
|
|
3021
|
-
|
|
3199
|
+
};
|
|
3200
|
+
var runtimeReady = (async () => {
|
|
3201
|
+
const host = findClaudeHost();
|
|
3202
|
+
const reader = host ? new ClaudeSessionReader(claudeSessionMetadataPath(host), host) : void 0;
|
|
3203
|
+
const metadata = reader ? await waitForClaudeSession(reader) : void 0;
|
|
3204
|
+
const runtime = new ClaudeIntercomRuntime(claudeIdentityForSession(metadata, process.env, host?.pid));
|
|
3205
|
+
if (!shuttingDown && !metadata?.ended) void runtime.connect().catch(report);
|
|
3206
|
+
if (reader && !shuttingDown) {
|
|
3207
|
+
let previous = JSON.stringify(metadata);
|
|
3208
|
+
let syncing = false;
|
|
3209
|
+
const timer = setInterval(() => {
|
|
3210
|
+
if (syncing || shuttingDown) return;
|
|
3211
|
+
const next = reader.read();
|
|
3212
|
+
const signature = JSON.stringify(next);
|
|
3213
|
+
if (!next || signature === previous) return;
|
|
3214
|
+
previous = signature;
|
|
3215
|
+
syncing = true;
|
|
3216
|
+
pendingSync = (async () => {
|
|
3217
|
+
if (next.ended) await runtime.disconnect();
|
|
3218
|
+
else {
|
|
3219
|
+
await runtime.syncSession(claudeIdentityForSession(next, process.env, host.pid));
|
|
3220
|
+
await runtime.connect();
|
|
3221
|
+
}
|
|
3222
|
+
})().catch(report).finally(() => {
|
|
3223
|
+
syncing = false;
|
|
3224
|
+
});
|
|
3225
|
+
}, 250);
|
|
3226
|
+
timer.unref();
|
|
3227
|
+
stopPolling = () => clearInterval(timer);
|
|
3228
|
+
}
|
|
3229
|
+
return runtime;
|
|
3230
|
+
})();
|
|
3022
3231
|
var rl = readline.createInterface({
|
|
3023
3232
|
input: stdin,
|
|
3024
3233
|
crlfDelay: Infinity
|
|
3025
3234
|
});
|
|
3026
|
-
var shuttingDown = false;
|
|
3027
3235
|
var pendingRequests = 0;
|
|
3028
3236
|
function writeResponse(response) {
|
|
3029
3237
|
if (!response) return;
|
|
@@ -3032,7 +3240,11 @@ function writeResponse(response) {
|
|
|
3032
3240
|
}
|
|
3033
3241
|
function maybeShutdown() {
|
|
3034
3242
|
if (!shuttingDown || pendingRequests > 0) return;
|
|
3035
|
-
|
|
3243
|
+
stopPolling();
|
|
3244
|
+
void runtimeReady.then(async (runtime) => {
|
|
3245
|
+
await pendingSync;
|
|
3246
|
+
await runtime.disconnect();
|
|
3247
|
+
}).finally(() => process.exit(0));
|
|
3036
3248
|
}
|
|
3037
3249
|
rl.on("line", (line) => {
|
|
3038
3250
|
const trimmed = line.trim();
|
|
@@ -3041,7 +3253,7 @@ rl.on("line", (line) => {
|
|
|
3041
3253
|
void (async () => {
|
|
3042
3254
|
try {
|
|
3043
3255
|
const request = JSON.parse(trimmed);
|
|
3044
|
-
writeResponse(await handleMcpRequest(request,
|
|
3256
|
+
writeResponse(await handleMcpRequest(request, await runtimeReady));
|
|
3045
3257
|
} catch (error2) {
|
|
3046
3258
|
writeResponse({
|
|
3047
3259
|
jsonrpc: "2.0",
|