@agenticmail/claudecode 0.2.19 → 0.2.20
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/dist/{chunk-N6OG2X3V.js → chunk-ALWSCQFP.js} +199 -2
- package/dist/dispatcher-bin.js +1 -1
- package/dist/dispatcher.d.ts +25 -0
- package/dist/dispatcher.js +1 -1
- package/dist/index.js +1 -1
- package/dist/mail-hook.js +7 -0
- package/package.json +2 -2
|
@@ -160,7 +160,95 @@ var DispatcherState = class {
|
|
|
160
160
|
import { mkdirSync as mkdirSync2, createWriteStream, rmSync } from "fs";
|
|
161
161
|
import { join as join3 } from "path";
|
|
162
162
|
import { homedir as homedir2 } from "os";
|
|
163
|
-
import { ThreadCache, AgentMemoryStore, threadIdFor, normalizeSubject } from "@agenticmail/core";
|
|
163
|
+
import { ThreadCache, AgentMemoryStore, threadIdFor, normalizeSubject, loadHostSession, forgetHostSession } from "@agenticmail/core";
|
|
164
|
+
|
|
165
|
+
// src/bridge-wake.ts
|
|
166
|
+
async function resumeBridgeSession(query, input, log) {
|
|
167
|
+
const startMs = Date.now();
|
|
168
|
+
const timeoutMs = input.timeoutMs ?? 5 * 60 * 1e3;
|
|
169
|
+
let result = { ok: false };
|
|
170
|
+
try {
|
|
171
|
+
log("info", `[bridge-wake] resuming Claude Code session ${input.sessionId.slice(0, 8)}\u2026 for ${input.bridge.name}`);
|
|
172
|
+
const opts = {
|
|
173
|
+
resume: input.sessionId,
|
|
174
|
+
cwd: input.cwd,
|
|
175
|
+
mcpServers: {
|
|
176
|
+
agenticmail: {
|
|
177
|
+
command: "agenticmail-mcp",
|
|
178
|
+
args: [],
|
|
179
|
+
env: input.mcpEnv
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
permissionMode: "bypassPermissions",
|
|
183
|
+
// Headless mode — no stdin tty.
|
|
184
|
+
includePartialMessages: false
|
|
185
|
+
};
|
|
186
|
+
let assistantText = "";
|
|
187
|
+
let timedOut = false;
|
|
188
|
+
const timeoutHandle = setTimeout(() => {
|
|
189
|
+
timedOut = true;
|
|
190
|
+
}, timeoutMs);
|
|
191
|
+
timeoutHandle.unref?.();
|
|
192
|
+
try {
|
|
193
|
+
const stream = query({ prompt: input.prompt, options: opts });
|
|
194
|
+
for await (const event of stream) {
|
|
195
|
+
if (timedOut) break;
|
|
196
|
+
const e = event;
|
|
197
|
+
if (e.type === "assistant" && Array.isArray(e.message?.content)) {
|
|
198
|
+
for (const block of e.message.content) {
|
|
199
|
+
const b = block;
|
|
200
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
201
|
+
assistantText = b.text;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} finally {
|
|
207
|
+
clearTimeout(timeoutHandle);
|
|
208
|
+
}
|
|
209
|
+
if (timedOut) {
|
|
210
|
+
log("warn", `[bridge-wake] timeout after ${timeoutMs}ms \u2014 bridge wake gave up`);
|
|
211
|
+
return { ok: false, error: "timeout", durationMs: Date.now() - startMs };
|
|
212
|
+
}
|
|
213
|
+
result = { ok: true, text: assistantText, durationMs: Date.now() - startMs };
|
|
214
|
+
log("info", `[bridge-wake] resumed session ok (${result.durationMs}ms, ${assistantText.length} chars)`);
|
|
215
|
+
return result;
|
|
216
|
+
} catch (err) {
|
|
217
|
+
const msg = err?.message ?? String(err);
|
|
218
|
+
const m = msg.toLowerCase();
|
|
219
|
+
const expired = m.includes("session not found") || m.includes("invalid session") || m.includes("session expired") || m.includes("no such session") || m.includes("unknown session");
|
|
220
|
+
const sdkMissing = m.includes("cannot find module") || m.includes("could not be found") || m.includes("command not found");
|
|
221
|
+
const error = expired ? "session-expired" : sdkMissing ? "sdk-missing" : "other";
|
|
222
|
+
log("warn", `[bridge-wake] resume failed (${error}): ${msg.slice(0, 200)}`);
|
|
223
|
+
return { ok: false, error, errorMessage: msg, durationMs: Date.now() - startMs };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function composeBridgeWakePrompt(args) {
|
|
227
|
+
const subject = args.subject ?? "(no subject)";
|
|
228
|
+
const from = args.from ?? "unknown";
|
|
229
|
+
const preview = (args.preview ?? "").slice(0, 600);
|
|
230
|
+
return [
|
|
231
|
+
`\u{1F380} Bridge mail arrived \u2014 headless wake.`,
|
|
232
|
+
"",
|
|
233
|
+
`You are being resumed against your last session because new mail landed in your bridge inbox (${args.bridgeName}@localhost) and you weren't actively at the keyboard.`,
|
|
234
|
+
"",
|
|
235
|
+
`Trigger:`,
|
|
236
|
+
` UID: ${args.uid}`,
|
|
237
|
+
` From: ${from}`,
|
|
238
|
+
` Subject: ${subject}`,
|
|
239
|
+
` Preview: ${preview}`,
|
|
240
|
+
"",
|
|
241
|
+
`Read it with mcp__agenticmail__read_email({ uid: ${args.uid} }) and decide:`,
|
|
242
|
+
` \xB7 Does it need a reply from YOU (the operator's session)? Reply via mcp__agenticmail__reply_email.`,
|
|
243
|
+
` \xB7 Does it need a teammate to act? Forward / re-route by replying with wake: ["<teammate>"].`,
|
|
244
|
+
` \xB7 Is it [NEEDS OPERATOR] / [BLOCKED]? Then it's actually for the human \u2014 mark it unread, and the operator will see it on their next keystroke.`,
|
|
245
|
+
` \xB7 Is it FYI noise? mark_read and exit.`,
|
|
246
|
+
"",
|
|
247
|
+
`Keep this turn SHORT. You're being resumed to handle ONE piece of mail, not to continue the prior conversation.`
|
|
248
|
+
].join("\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/dispatcher.ts
|
|
164
252
|
function extractSubject(event) {
|
|
165
253
|
if (typeof event.subject === "string") return event.subject;
|
|
166
254
|
if (event.message && typeof event.message.subject === "string") return event.message.subject;
|
|
@@ -761,6 +849,14 @@ var Dispatcher = class _Dispatcher {
|
|
|
761
849
|
/** Public for tests — directly hand an event to the routing path. */
|
|
762
850
|
async handleEvent(account, event) {
|
|
763
851
|
if (this.stopped) return;
|
|
852
|
+
if (event.type === "new" && typeof event.uid === "number" && account.name.toLowerCase() === this.cfg.bridgeAgentName.toLowerCase()) {
|
|
853
|
+
const ch = this.channels.get(account.id);
|
|
854
|
+
if (ch?.seenUids.has(event.uid)) return;
|
|
855
|
+
if (ch) rememberBounded(ch.seenUids, event.uid);
|
|
856
|
+
this.state.markSeen(account.id, event.uid);
|
|
857
|
+
void this.handleBridgeMail(account, event);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
764
860
|
if (event.type === "new" && typeof event.uid === "number") {
|
|
765
861
|
const ch = this.channels.get(account.id);
|
|
766
862
|
if (ch?.seenUids.has(event.uid)) return;
|
|
@@ -860,7 +956,7 @@ var Dispatcher = class _Dispatcher {
|
|
|
860
956
|
shouldWatch(account) {
|
|
861
957
|
const bridgeName = this.cfg.bridgeAgentName.toLowerCase();
|
|
862
958
|
const meta = account.metadata;
|
|
863
|
-
if (account.name.toLowerCase() === bridgeName) return
|
|
959
|
+
if (account.name.toLowerCase() === bridgeName) return true;
|
|
864
960
|
if (account.role === "bridge") return false;
|
|
865
961
|
if (meta && meta.bridge === true) return false;
|
|
866
962
|
if (meta && typeof meta.host === "string" && meta.host.length > 0) {
|
|
@@ -1333,6 +1429,107 @@ var Dispatcher = class _Dispatcher {
|
|
|
1333
1429
|
sections.push("this thread will load that memory into context for you.");
|
|
1334
1430
|
return sections.join("\n");
|
|
1335
1431
|
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Handle mail that lands in the host's OWN bridge inbox.
|
|
1434
|
+
*
|
|
1435
|
+
* Unlike normal sub-agent wakes (which spawn a fresh worker turn),
|
|
1436
|
+
* bridge wakes resume the operator's last interactive session via
|
|
1437
|
+
* the Claude Agent SDK's `resume` option — keeping the operator's
|
|
1438
|
+
* context intact and avoiding a duplicate "second Claude trying to
|
|
1439
|
+
* be Claude Code". When no fresh session is available (operator
|
|
1440
|
+
* hasn't run `claude` in >24h, or `~/.agenticmail/host-sessions.json`
|
|
1441
|
+
* was wiped), the dispatcher emits an `urgent` system event so the
|
|
1442
|
+
* web UI's notification surface + any future SMS escalation hook
|
|
1443
|
+
* can pick it up. See packages/claudecode/src/bridge-wake.ts.
|
|
1444
|
+
*
|
|
1445
|
+
* Two short-circuits:
|
|
1446
|
+
* 1. If lastSeenMs is within 30s, the operator is actively at the
|
|
1447
|
+
* keyboard right now — their own UserPromptSubmit / Stop hook
|
|
1448
|
+
* will surface the mail on their next keystroke. We skip the
|
|
1449
|
+
* resume entirely to avoid token waste.
|
|
1450
|
+
* 2. If a previous bridge-wake for this same UID is already
|
|
1451
|
+
* in-flight (same key in `inFlightBridgeWakes`), skip — IMAP
|
|
1452
|
+
* IDLE replays + SSE reconnects can fire the same event
|
|
1453
|
+
* twice and we don't want to double-resume.
|
|
1454
|
+
*/
|
|
1455
|
+
inFlightBridgeWakes = /* @__PURE__ */ new Set();
|
|
1456
|
+
async handleBridgeMail(account, event) {
|
|
1457
|
+
if (typeof event.uid !== "number") return;
|
|
1458
|
+
if (this.inFlightBridgeWakes.has(event.uid)) return;
|
|
1459
|
+
this.inFlightBridgeWakes.add(event.uid);
|
|
1460
|
+
const startMs = Date.now();
|
|
1461
|
+
const subject = extractSubject(event);
|
|
1462
|
+
const from = extractFrom(event);
|
|
1463
|
+
const preview = event.preview ?? event.message?.preview ?? "";
|
|
1464
|
+
try {
|
|
1465
|
+
const saved = loadHostSession("claudecode");
|
|
1466
|
+
if (saved && Date.now() - saved.lastSeenMs < 3e4) {
|
|
1467
|
+
this.log("info", `[bridge-wake] skip \u2014 operator is live (lastSeen=${Date.now() - saved.lastSeenMs}ms ago); their hook will surface uid=${event.uid}`);
|
|
1468
|
+
this.postActivity("/dispatcher/bridge-skipped", {
|
|
1469
|
+
uid: event.uid,
|
|
1470
|
+
agentName: account.name,
|
|
1471
|
+
reason: "operator-live"
|
|
1472
|
+
});
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (!saved) {
|
|
1476
|
+
this.log("warn", `[bridge-wake] no fresh Claude Code session recorded \u2014 bridge mail uid=${event.uid} cannot resume. Escalating via system-event.`);
|
|
1477
|
+
this.postActivity("/dispatcher/bridge-escalation", {
|
|
1478
|
+
uid: event.uid,
|
|
1479
|
+
agentName: account.name,
|
|
1480
|
+
subject,
|
|
1481
|
+
from,
|
|
1482
|
+
preview,
|
|
1483
|
+
reason: "no-fresh-session"
|
|
1484
|
+
});
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
const mcpEnv = await this.buildMcpEnv();
|
|
1488
|
+
const prompt = composeBridgeWakePrompt({
|
|
1489
|
+
bridgeName: account.name,
|
|
1490
|
+
uid: event.uid,
|
|
1491
|
+
subject,
|
|
1492
|
+
from,
|
|
1493
|
+
preview
|
|
1494
|
+
});
|
|
1495
|
+
const result = await resumeBridgeSession(this.query, {
|
|
1496
|
+
bridge: account,
|
|
1497
|
+
sessionId: saved.sessionId,
|
|
1498
|
+
cwd: saved.workspace,
|
|
1499
|
+
prompt,
|
|
1500
|
+
mcpEnv
|
|
1501
|
+
}, this.log);
|
|
1502
|
+
if (result.ok) {
|
|
1503
|
+
this.postActivity("/dispatcher/bridge-resumed", {
|
|
1504
|
+
uid: event.uid,
|
|
1505
|
+
agentName: account.name,
|
|
1506
|
+
subject,
|
|
1507
|
+
from,
|
|
1508
|
+
durationMs: result.durationMs,
|
|
1509
|
+
resultPreview: result.text?.slice(0, 240)
|
|
1510
|
+
});
|
|
1511
|
+
} else {
|
|
1512
|
+
if (result.error === "session-expired") {
|
|
1513
|
+
this.log("warn", `[bridge-wake] session expired; forgetting and escalating uid=${event.uid}`);
|
|
1514
|
+
forgetHostSession("claudecode");
|
|
1515
|
+
}
|
|
1516
|
+
this.postActivity("/dispatcher/bridge-escalation", {
|
|
1517
|
+
uid: event.uid,
|
|
1518
|
+
agentName: account.name,
|
|
1519
|
+
subject,
|
|
1520
|
+
from,
|
|
1521
|
+
preview,
|
|
1522
|
+
reason: result.error ?? "resume-failed",
|
|
1523
|
+
errorMessage: result.errorMessage
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
} catch (err) {
|
|
1527
|
+
this.log("error", `[bridge-wake] uid=${event.uid} threw: ${err.message}`);
|
|
1528
|
+
} finally {
|
|
1529
|
+
this.inFlightBridgeWakes.delete(event.uid);
|
|
1530
|
+
this.log("info", `[bridge-wake] finished uid=${event.uid} in ${Date.now() - startMs}ms`);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1336
1533
|
/** Acquire a concurrency slot, run a worker, release the slot. */
|
|
1337
1534
|
async spawnWorker(account, prompt, ctx) {
|
|
1338
1535
|
const releaseAgentLock = await this.acquireAgentSerial(account.id);
|
package/dist/dispatcher-bin.js
CHANGED
package/dist/dispatcher.d.ts
CHANGED
|
@@ -370,6 +370,31 @@ declare class Dispatcher {
|
|
|
370
370
|
subject?: string;
|
|
371
371
|
uid?: number;
|
|
372
372
|
}, prompt: string): string;
|
|
373
|
+
/**
|
|
374
|
+
* Handle mail that lands in the host's OWN bridge inbox.
|
|
375
|
+
*
|
|
376
|
+
* Unlike normal sub-agent wakes (which spawn a fresh worker turn),
|
|
377
|
+
* bridge wakes resume the operator's last interactive session via
|
|
378
|
+
* the Claude Agent SDK's `resume` option — keeping the operator's
|
|
379
|
+
* context intact and avoiding a duplicate "second Claude trying to
|
|
380
|
+
* be Claude Code". When no fresh session is available (operator
|
|
381
|
+
* hasn't run `claude` in >24h, or `~/.agenticmail/host-sessions.json`
|
|
382
|
+
* was wiped), the dispatcher emits an `urgent` system event so the
|
|
383
|
+
* web UI's notification surface + any future SMS escalation hook
|
|
384
|
+
* can pick it up. See packages/claudecode/src/bridge-wake.ts.
|
|
385
|
+
*
|
|
386
|
+
* Two short-circuits:
|
|
387
|
+
* 1. If lastSeenMs is within 30s, the operator is actively at the
|
|
388
|
+
* keyboard right now — their own UserPromptSubmit / Stop hook
|
|
389
|
+
* will surface the mail on their next keystroke. We skip the
|
|
390
|
+
* resume entirely to avoid token waste.
|
|
391
|
+
* 2. If a previous bridge-wake for this same UID is already
|
|
392
|
+
* in-flight (same key in `inFlightBridgeWakes`), skip — IMAP
|
|
393
|
+
* IDLE replays + SSE reconnects can fire the same event
|
|
394
|
+
* twice and we don't want to double-resume.
|
|
395
|
+
*/
|
|
396
|
+
private inFlightBridgeWakes;
|
|
397
|
+
private handleBridgeMail;
|
|
373
398
|
/** Acquire a concurrency slot, run a worker, release the slot. */
|
|
374
399
|
private spawnWorker;
|
|
375
400
|
/**
|
package/dist/dispatcher.js
CHANGED
package/dist/index.js
CHANGED
package/dist/mail-hook.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
import { join, dirname } from "path";
|
|
7
|
+
import { saveHostSession } from "@agenticmail/core";
|
|
7
8
|
var AGENTICMAIL_DIR = join(homedir(), ".agenticmail");
|
|
8
9
|
var CONFIG_PATH = join(AGENTICMAIL_DIR, "config.json");
|
|
9
10
|
var CURSOR_PATH = join(AGENTICMAIL_DIR, "claudecode-hook-cursor.json");
|
|
@@ -92,6 +93,12 @@ async function main() {
|
|
|
92
93
|
const input = await readStdinJson();
|
|
93
94
|
const eventName = input?.hook_event_name ?? "UserPromptSubmit";
|
|
94
95
|
const sessionId = typeof input?.session_id === "string" ? input.session_id : "";
|
|
96
|
+
if (sessionId) {
|
|
97
|
+
try {
|
|
98
|
+
saveHostSession("claudecode", { sessionId, workspace: process.cwd() });
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
95
102
|
if (eventName === "SessionStart") {
|
|
96
103
|
process.stdout.write(JSON.stringify({
|
|
97
104
|
hookSpecificOutput: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenticmail/claudecode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.20",
|
|
4
4
|
"description": "Claude Code integration for AgenticMail — surfaces every AgenticMail agent as a native Claude Code subagent so any Claude Code session can delegate to them with the Agent tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"prepublishOnly": "npm run build"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@agenticmail/core": "^0.9.
|
|
50
|
+
"@agenticmail/core": "^0.9.6",
|
|
51
51
|
"@agenticmail/mcp": "^0.9.7",
|
|
52
52
|
"@anthropic-ai/claude-agent-sdk": "^0.2.140"
|
|
53
53
|
},
|