@agenticmail/claudecode 0.1.10 → 0.1.13

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 CHANGED
@@ -16,6 +16,17 @@ Agent { subagent_type: "agenticmail-fola", prompt: "draft a reply to my last ema
16
16
 
17
17
  This package is to Claude Code what `@agenticmail/openclaw` is to OpenClaw: an integration package that wires AgenticMail into the host AI runtime. It mirrors that package's layout 1:1, so if you know one, you know the other.
18
18
 
19
+ ## ✨ What's new in 0.1.11
20
+
21
+ - **Selective wake** — when the sender sets `wake: ["alice", "bob"]` on `send_email` / `reply_email`, the dispatcher gives a Claude turn only to listed agents. CC'd-but-not-listed agents still receive the mail in their inbox but stay asleep. Single biggest token saver on multi-agent threads.
22
+ - **Thread-close markers** — `[FINAL]`, `[DONE]`, `[CLOSED]`, `[WRAP]` in a subject. The dispatcher stops waking workers on any further reply to that thread. Closes the "no native done signal" gap from the 5-agent stress test.
23
+ - **Wake-budget circuit breaker** — caps per-(agent, thread) wakes at 10 / 24h. Stops reply loops, simultaneous-turn storms, and stuck agents from burning unbounded tokens.
24
+ - **Push-based account lifecycle** — dispatcher subscribes to `/system/events` on start; new agents from `create_account` get an SSE channel within milliseconds, not polling intervals.
25
+ - **Worker activity registry** — every spawn posts `worker-started` / `worker-finished` to the API so the host can call `check_activity` and answer "did Vesper actually start working?" in one MCP call.
26
+ - **Full native toolset** — workers spawn with no `allowedTools` restriction. Read, Write, Edit, Bash, Glob, Grep, WebFetch, WebSearch, NotebookEdit — the same toolset the host has. Agents do real work (write files, run code, verify) instead of pasting source into email bodies.
27
+ - **Dedup guidance in wake prompts** — agents are told to check their own prior contributions to a thread before re-doing work. Cuts the "researcher sends competitive landscape twice" failure mode.
28
+ - **Recent-reply check** — wake prompt and persona both instruct: "if a teammate replied within the last 60 seconds, assume they're handling this turn and stay silent." Cuts simultaneous-reply noise.
29
+
19
30
  ## Multi-agent coordination via the dispatcher
20
31
 
21
32
  After install, a background daemon (`agenticmail-claudecode-dispatcher`, managed by PM2) subscribes to every AgenticMail account's SSE stream. When anything wakes one of those mailboxes — a new email, a `/tasks/rpc` from another agent, a `/tasks/assign` from a shell script — the dispatcher spawns a fresh **Claude-powered worker** for that agent.
@@ -1,3 +1,6 @@
1
+ import {
2
+ removeUserPromptSubmitHook
3
+ } from "./chunk-Q3PCM4MO.js";
1
4
  import {
2
5
  removeMcpServer,
3
6
  stopDispatcher
@@ -7,7 +10,7 @@ import {
7
10
  deleteAccount,
8
11
  getAccountByName,
9
12
  resolveConfig
10
- } from "./chunk-3D5VXS5Y.js";
13
+ } from "./chunk-SBP7MJP2.js";
11
14
 
12
15
  // src/uninstall.ts
13
16
  import { existsSync, readdirSync, readFileSync, unlinkSync } from "fs";
@@ -39,6 +42,11 @@ async function uninstall(opts = {}) {
39
42
  const cfg = resolveConfig(opts);
40
43
  const mcpBlockRemoved = removeMcpServer(cfg.claudeConfigPath, cfg.mcpServerName);
41
44
  const removedSubagents = removeOwnedSubagents(cfg.agentsDir, cfg.subagentPrefix);
45
+ let hookRemoved = false;
46
+ try {
47
+ hookRemoved = removeUserPromptSubmitHook(cfg.claudeSettingsPath);
48
+ } catch {
49
+ }
42
50
  const dispatcherStopped = stopDispatcher().stopped;
43
51
  let bridgeAgentDeleted = false;
44
52
  if (opts.purgeBridgeAgent && cfg.masterKey) {
@@ -52,7 +60,7 @@ async function uninstall(opts = {}) {
52
60
  }
53
61
  }
54
62
  return {
55
- changed: mcpBlockRemoved || removedSubagents.length > 0 || bridgeAgentDeleted || dispatcherStopped,
63
+ changed: mcpBlockRemoved || removedSubagents.length > 0 || bridgeAgentDeleted || dispatcherStopped || hookRemoved,
56
64
  removedSubagents,
57
65
  mcpBlockRemoved,
58
66
  bridgeAgentDeleted,
@@ -1,3 +1,6 @@
1
+ import {
2
+ upsertUserPromptSubmitHook
3
+ } from "./chunk-Q3PCM4MO.js";
1
4
  import {
2
5
  startDispatcher,
3
6
  upsertMcpServer
@@ -9,7 +12,7 @@ import {
9
12
  listAccounts,
10
13
  renderSubagentMarkdown,
11
14
  resolveConfig
12
- } from "./chunk-3D5VXS5Y.js";
15
+ } from "./chunk-SBP7MJP2.js";
13
16
 
14
17
  // src/install.ts
15
18
  import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, unlinkSync } from "fs";
@@ -106,13 +109,18 @@ async function install(opts = {}) {
106
109
  const updated = writeSubagentFiles(cfg.agentsDir, cfg, exposable);
107
110
  const liveNames = new Set(exposable.map((a) => sanitizeSubagentName(a.name)));
108
111
  const pruned = pruneStaleSubagentFiles(cfg.agentsDir, cfg, liveNames);
112
+ let hookChanged = false;
113
+ try {
114
+ hookChanged = upsertUserPromptSubmitHook(cfg.claudeSettingsPath, "agenticmail-mail-hook");
115
+ } catch {
116
+ }
109
117
  const dispatcherStatus = await startDispatcherForInstall(cfg);
110
118
  return {
111
119
  registeredAgents: exposable,
112
120
  claudeConfigPath: cfg.claudeConfigPath,
113
121
  agentsDir: cfg.agentsDir,
114
122
  bridgeAgent: bridge,
115
- changed: mcpChanged || updated.length > 0 || pruned.length > 0 || dispatcherStatus.started,
123
+ changed: mcpChanged || updated.length > 0 || pruned.length > 0 || dispatcherStatus.started || hookChanged,
116
124
  dispatcher: dispatcherStatus
117
125
  };
118
126
  }
@@ -1,15 +1,15 @@
1
+ import {
2
+ uninstall
3
+ } from "./chunk-D2GHBPXZ.js";
1
4
  import {
2
5
  install
3
- } from "./chunk-52LXPWO7.js";
6
+ } from "./chunk-G2CF37UQ.js";
4
7
  import {
5
8
  status
6
- } from "./chunk-V3QMDNTR.js";
7
- import {
8
- uninstall
9
- } from "./chunk-CQLUFM7N.js";
9
+ } from "./chunk-O4H76K3B.js";
10
10
  import {
11
11
  AgenticMailApiError
12
- } from "./chunk-3D5VXS5Y.js";
12
+ } from "./chunk-SBP7MJP2.js";
13
13
 
14
14
  // src/http-routes.ts
15
15
  import { Router } from "express";
@@ -7,7 +7,7 @@ import {
7
7
  MANAGED_BY_MARKER,
8
8
  getAccountByName,
9
9
  resolveConfig
10
- } from "./chunk-3D5VXS5Y.js";
10
+ } from "./chunk-SBP7MJP2.js";
11
11
 
12
12
  // src/status.ts
13
13
  import { existsSync, readFileSync, readdirSync } from "fs";
@@ -0,0 +1,95 @@
1
+ // src/claude-hooks-config.ts
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "fs";
3
+ import { dirname } from "path";
4
+ var AGENTICMAIL_HOOK_MARKER = "agenticmail-mail-hook";
5
+ var HOOK_EVENTS_TO_REGISTER = ["UserPromptSubmit"];
6
+ var HOOK_EVENTS_TO_REMOVE = ["UserPromptSubmit", "PreToolUse"];
7
+ function readSettings(path) {
8
+ if (!existsSync(path)) return {};
9
+ const raw = readFileSync(path, "utf-8");
10
+ if (!raw.trim()) return {};
11
+ try {
12
+ const parsed = JSON.parse(raw);
13
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
14
+ return {};
15
+ } catch (err) {
16
+ throw new Error(
17
+ `Could not parse Claude Code settings at ${path}: ${err.message}. Refusing to overwrite \u2014 please fix the file by hand and retry.`
18
+ );
19
+ }
20
+ }
21
+ function writeSettings(path, settings) {
22
+ const dir = dirname(path);
23
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
24
+ const text = JSON.stringify(settings, null, 2) + "\n";
25
+ const tmp = `${path}.agenticmail-tmp`;
26
+ writeFileSync(tmp, text, "utf-8");
27
+ renameSync(tmp, path);
28
+ }
29
+ function upsertMailHook(path, command) {
30
+ const settings = readSettings(path);
31
+ if (!settings.hooks) settings.hooks = {};
32
+ let changed = false;
33
+ for (const event of HOOK_EVENTS_TO_REGISTER) {
34
+ if (upsertOneEvent(settings.hooks, event, command)) changed = true;
35
+ }
36
+ for (const event of HOOK_EVENTS_TO_REMOVE) {
37
+ if (HOOK_EVENTS_TO_REGISTER.includes(event)) continue;
38
+ if (removeOneEvent(settings.hooks, event)) changed = true;
39
+ }
40
+ if (changed) writeSettings(path, settings);
41
+ return changed;
42
+ }
43
+ function removeOneEvent(hooks, event) {
44
+ const list = hooks[event] ?? [];
45
+ if (list.length === 0) return false;
46
+ const filtered = list.filter(
47
+ (rule) => !rule.hooks?.some((h) => typeof h.command === "string" && h.command.includes(AGENTICMAIL_HOOK_MARKER))
48
+ );
49
+ if (filtered.length === list.length) return false;
50
+ if (filtered.length === 0) delete hooks[event];
51
+ else hooks[event] = filtered;
52
+ return true;
53
+ }
54
+ function upsertOneEvent(hooks, event, command) {
55
+ const list = hooks[event] ?? [];
56
+ const isOurs = (rule) => rule.hooks?.some((h) => typeof h.command === "string" && h.command.includes(AGENTICMAIL_HOOK_MARKER)) ?? false;
57
+ const desired = {
58
+ matcher: "",
59
+ // empty = match every fire of this event
60
+ hooks: [{ type: "command", command }]
61
+ };
62
+ const existingIdx = list.findIndex(isOurs);
63
+ if (existingIdx >= 0) {
64
+ const existing = list[existingIdx];
65
+ if (existing.matcher === desired.matcher && existing.hooks.length === desired.hooks.length && existing.hooks.every((h, i) => h.command === desired.hooks[i].command)) {
66
+ return false;
67
+ }
68
+ list[existingIdx] = desired;
69
+ } else {
70
+ list.push(desired);
71
+ }
72
+ hooks[event] = list;
73
+ return true;
74
+ }
75
+ function removeMailHook(path) {
76
+ if (!existsSync(path)) return false;
77
+ const settings = readSettings(path);
78
+ if (!settings.hooks) return false;
79
+ let changed = false;
80
+ for (const event of HOOK_EVENTS_TO_REMOVE) {
81
+ if (removeOneEvent(settings.hooks, event)) changed = true;
82
+ }
83
+ if (settings.hooks && Object.keys(settings.hooks).length === 0) {
84
+ delete settings.hooks;
85
+ }
86
+ if (changed) writeSettings(path, settings);
87
+ return changed;
88
+ }
89
+ var upsertUserPromptSubmitHook = upsertMailHook;
90
+ var removeUserPromptSubmitHook = removeMailHook;
91
+
92
+ export {
93
+ upsertUserPromptSubmitHook,
94
+ removeUserPromptSubmitHook
95
+ };
@@ -111,6 +111,7 @@ function resolveConfig(opts = {}) {
111
111
  apiUrl: opts.apiUrl ?? defaultApiUrl,
112
112
  masterKey,
113
113
  claudeConfigPath: opts.claudeConfigPath ?? join(homedir(), ".claude.json"),
114
+ claudeSettingsPath: opts.claudeSettingsPath ?? join(homedir(), ".claude", "settings.json"),
114
115
  agentsDir: opts.agentsDir ?? join(homedir(), ".claude", "agents"),
115
116
  mcpServerName: opts.mcpServerName ?? "agenticmail",
116
117
  bridgeAgentName: opts.bridgeAgentName ?? "claudecode",
@@ -187,9 +188,12 @@ function renderPersonaBody(input) {
187
188
  ` 1. Read the new message with \`${tool("read_email")}\`.`,
188
189
  ` 2. Load the rest of the thread with \`${tool("search_emails")}({ subject: "<core subject>", _account: "${agent.name}" })\` and read each prior message. You MUST have full thread context before acting.`,
189
190
  ` 3. Look at To + CC across the thread \u2014 those are your teammates. They will each be woken on every reply-all just like you were.`,
190
- ` 4. Decide if it's YOUR turn: are you addressed by name? Is the previous-stage handoff to your role? Is a question pending for you? **If a teammate replied within the last 60 seconds, assume they are handling this turn and stay silent** \u2014 simultaneous replies are noise. When in doubt, stay silent \u2014 over-replying creates noise.`,
191
- ` 5. If yes: \`${tool("reply_email")}({ uid, replyAll: true, text: "...", _account: "${agent.name}" })\`. Sign with your name. If you're handing off, name the next teammate explicitly ("Orion \u2014 over to you, please \u2026"). To bring a new teammate in, just add them to CC.`,
192
- ` 6. If no: \`mark_read\` and return. Silence IS a valid contribution.`,
191
+ ` 4. **Check your prior contributions first.** In the search results from step 2, count how many messages are from \`${agent.email}\`. If you have already contributed your work to this thread, do NOT redo it on a new wake. Only re-contribute if (a) the latest reply has a NEW specific ask for you by name and you have not yet answered THAT ask, or (b) a teammate's reply genuinely changes the picture and your prior work needs an explicit revision. Redelivering the same content when a teammate posts an update is the most common multi-agent failure mode.`,
192
+ ` 5. Decide if it's YOUR turn: are you addressed by name? Is the previous-stage handoff to your role? Is a question pending for you? **If a teammate replied within the last 60 seconds, assume they are handling this turn and stay silent** \u2014 simultaneous replies are noise. When in doubt, stay silent \u2014 over-replying creates noise.`,
193
+ ` 6. If yes: \`${tool("reply_email")}({ uid, replyAll: true, text: "...", _account: "${agent.name}" })\`. Sign with your name. If you're handing off, name the next teammate explicitly ("Orion \u2014 over to you, please \u2026"). To bring a new teammate in, just add them to CC.`,
194
+ ` 7. If no: \`mark_read\` and return. Silence IS a valid contribution.`,
195
+ "",
196
+ `**Closing a thread.** When the work is genuinely done and no more contributions are needed, send a wrap-up reply with one of these markers in the subject: \`[FINAL]\`, \`[DONE]\`, \`[CLOSED]\`, or \`[WRAP]\`. The dispatcher honours those markers and stops waking workers on any further replies to that thread. Use this when YOU are the one signing off the work, not as a routine ack.`,
193
197
  "",
194
198
  `**When to use \`${tool("call_agent")}\` instead:** only when you need ONE structured answer from ONE teammate, inline in your current turn \u2014 e.g. "give me a JSON list of X". For multi-step / multi-agent work, the thread pattern above is the right primitive.`,
195
199
  "",
@@ -2,7 +2,7 @@ import {
2
2
  listAccounts,
3
3
  renderPersonaBody,
4
4
  resolveConfig
5
- } from "./chunk-3D5VXS5Y.js";
5
+ } from "./chunk-SBP7MJP2.js";
6
6
 
7
7
  // src/persona-loader.ts
8
8
  import { existsSync, readFileSync } from "fs";
@@ -50,6 +50,17 @@ function extractFrom(event) {
50
50
  }
51
51
  return void 0;
52
52
  }
53
+ function extractWakeAllowlist(event) {
54
+ const raw = event.wakeAllowlist;
55
+ if (raw === void 0) return void 0;
56
+ if (!Array.isArray(raw)) return void 0;
57
+ return raw.map((x) => String(x).trim().toLowerCase()).filter(Boolean);
58
+ }
59
+ function isAgentOnWakeAllowlist(accountName, list) {
60
+ if (list === void 0) return true;
61
+ if (list.length === 0) return false;
62
+ return list.includes(accountName.trim().toLowerCase());
63
+ }
53
64
  var SEEN_CAP = 1024;
54
65
  function rememberBounded(set, item) {
55
66
  set.add(item);
@@ -72,6 +83,12 @@ function isTaskNotificationSubject(subject) {
72
83
  }
73
84
  return false;
74
85
  }
86
+ var THREAD_CLOSED_MARKERS = ["[FINAL]", "[DONE]", "[CLOSED]", "[WRAP]"];
87
+ function isThreadClosedSubject(subject) {
88
+ if (!subject) return false;
89
+ const s = subject.toLowerCase();
90
+ return THREAD_CLOSED_MARKERS.some((m) => s.includes(m.toLowerCase()));
91
+ }
75
92
  function threadIdFromSubject(subject) {
76
93
  if (!subject) return "";
77
94
  let s = subject.trim();
@@ -170,11 +187,24 @@ function newMailPrompt(agent, event) {
170
187
  ` to surface earlier messages in the thread, then read_email each prior UID.`,
171
188
  ` You MUST read the full thread before deciding what to do.`,
172
189
  ``,
173
- `3. **Identify the participants.** Look at To + CC across the thread. Those`,
190
+ `3. **CHECK YOUR PRIOR CONTRIBUTIONS to this thread.** When you searched`,
191
+ ` in step 2, look at how many of the messages were sent BY YOU`,
192
+ ` (from: ${agent.email}). If you have already contributed your work`,
193
+ ` to this thread, **do NOT redo it on a new wake**. Redelivering`,
194
+ ` identical content when a teammate posts an update is the most`,
195
+ ` common multi-agent failure mode \u2014 it triples noise and wastes`,
196
+ ` tokens. Only re-contribute if EITHER:`,
197
+ ` (a) the latest reply contains a NEW specific ask addressed to`,
198
+ ` you by name and you have not yet answered THAT ask, OR`,
199
+ ` (b) a teammate's reply genuinely changes the picture and your`,
200
+ ` prior work needs an explicit revision (not a re-post).`,
201
+ ` Otherwise stay silent.`,
202
+ ``,
203
+ `4. **Identify the participants.** Look at To + CC across the thread. Those`,
174
204
  ` are your collaborators. Their names map to AgenticMail agents at`,
175
205
  ` <name>@localhost. They will each be woken on every reply-all the same way you were.`,
176
206
  ``,
177
- `4. **Decide: is it MY turn?** Yes if any of:`,
207
+ `5. **Decide: is it MY turn?** Yes if any of:`,
178
208
  ` - The latest message addresses you by name ("Vesper, please \u2026", "@${agent.name} \u2026").`,
179
209
  ` - The previous-stage handoff is to your role (e.g. designer \u2192 developer, and you are the developer).`,
180
210
  ` - You were directly asked a question and nobody has answered yet.`,
@@ -188,7 +218,7 @@ function newMailPrompt(agent, event) {
188
218
  ` When in doubt, stay silent \u2014 over-replying creates noise. Better to let`,
189
219
  ` the right teammate take the turn than to step on theirs.`,
190
220
  ``,
191
- `5. **If it's your turn \u2014 do the actual work, THEN reply-all about it.**`,
221
+ `6. **If it's your turn \u2014 do the actual work, THEN reply-all about it.**`,
192
222
  ` You have full native tools: Read, Write, Edit, Bash, Glob, Grep, WebFetch,`,
193
223
  ` WebSearch, NotebookEdit, etc. If the task is "implement X", write the file`,
194
224
  ` with Write or Edit and verify with Bash \u2014 do NOT paste source code into an`,
@@ -198,15 +228,31 @@ function newMailPrompt(agent, event) {
198
228
  ` reply_email({ uid: ${uid ?? "<uid>"}, replyAll: true, text: "...", _account: "${agent.name}" })`,
199
229
  ` Sign with your name. Be substantive but concise. If you are handing off`,
200
230
  ` to the next teammate, name them explicitly in your reply ("Orion \u2014 over to you, please \u2026").`,
231
+ ` **NAME the next actor in the \`wake\` parameter** so the dispatcher only`,
232
+ ` gives them a Claude turn \u2014 every other CC'd teammate still receives the`,
233
+ ` mail in their inbox but stays asleep, saving the project a lot of tokens.`,
234
+ ` Example: \`reply_email({ uid, replyAll: true, text: "Orion \u2014 your turn \u2026",`,
235
+ ` wake: ["orion"], _account: "${agent.name}" })\`. If nobody specific is`,
236
+ ` next (the work is complete and you're just signing off), pass \`wake: []\``,
237
+ ` to deliver silently with zero Claude turns spawned.`,
201
238
  ``,
202
- `6. **If you need additional help from a teammate not yet on the thread,**`,
239
+ `7. **If you need additional help from a teammate not yet on the thread,**`,
203
240
  ` include them by CC'ing in your reply-all \u2014 DO NOT spin up a separate`,
204
241
  ` call_agent / message_agent side-channel. The thread is the workspace;`,
205
242
  ` everyone stays in context.`,
206
243
  ``,
207
- `7. **If it's NOT your turn,** mark the message read with mark_read and return.`,
244
+ `8. **If it's NOT your turn,** mark the message read with mark_read and return.`,
208
245
  ` Do not reply just to acknowledge. Silence IS a valid contribution.`,
209
246
  ``,
247
+ `## How threads end`,
248
+ ``,
249
+ `A thread is done when the host (or any participant) sends a wrap-up`,
250
+ `message with one of these markers in the subject: \`[FINAL]\`, \`[DONE]\`,`,
251
+ `\`[CLOSED]\`, \`[WRAP]\`. The dispatcher will stop waking workers on any`,
252
+ `further replies to that thread. If you are sending a wrap-up yourself`,
253
+ `(because the work is complete and no more contributions are needed),`,
254
+ `include one of those markers in your reply subject.`,
255
+ ``,
210
256
  `When you finish, return a one-line summary of what you did:`,
211
257
  ` "Contributed: <one-line description>" OR "Stayed silent \u2014 not my turn."`,
212
258
  ``,
@@ -369,6 +415,15 @@ var Dispatcher = class {
369
415
  return;
370
416
  }
371
417
  if (ch) rememberBounded(ch.seenUids, event.uid);
418
+ if (isThreadClosedSubject(subject)) {
419
+ this.log("info", `[dispatcher] thread closed (subject="${subject ?? ""}") \u2014 skipping wake for "${account.name}" uid=${event.uid}`);
420
+ return;
421
+ }
422
+ const allowlist = extractWakeAllowlist(event);
423
+ if (!isAgentOnWakeAllowlist(account.name, allowlist)) {
424
+ this.log("info", `[dispatcher] wake allowlist excludes "${account.name}" (list=${JSON.stringify(allowlist)}) \u2014 mail delivered, no Claude turn`);
425
+ return;
426
+ }
372
427
  const threadId = threadIdFromSubject(subject);
373
428
  const verdict = this.chargeWake(account.id, threadId);
374
429
  if (!verdict.ok) {
package/dist/cli.js CHANGED
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ uninstall
4
+ } from "./chunk-D2GHBPXZ.js";
2
5
  import {
3
6
  install
4
- } from "./chunk-52LXPWO7.js";
7
+ } from "./chunk-G2CF37UQ.js";
8
+ import "./chunk-Q3PCM4MO.js";
5
9
  import {
6
10
  status
7
- } from "./chunk-V3QMDNTR.js";
8
- import {
9
- uninstall
10
- } from "./chunk-CQLUFM7N.js";
11
+ } from "./chunk-O4H76K3B.js";
11
12
  import "./chunk-US5FT2UB.js";
12
13
  import {
13
14
  AgenticMailApiError
14
- } from "./chunk-3D5VXS5Y.js";
15
+ } from "./chunk-SBP7MJP2.js";
15
16
 
16
17
  // src/cli.ts
17
18
  var GREEN = (s) => `\x1B[32m${s}\x1B[0m`;
@@ -21,6 +21,13 @@ interface ClaudeCodeIntegrationConfig {
21
21
  masterKey: string;
22
22
  /** Path to Claude Code's user-level config (typically ~/.claude.json). */
23
23
  claudeConfigPath: string;
24
+ /**
25
+ * Path to Claude Code's user-level settings (typically
26
+ * ~/.claude/settings.json) where hooks are registered. Different
27
+ * file from `claudeConfigPath` — Claude Code splits OAuth/MCP state
28
+ * from preference/hook state, and so do we.
29
+ */
30
+ claudeSettingsPath: string;
24
31
  /** Directory where per-agent Claude Code subagent .md files live (typically ~/.claude/agents). */
25
32
  agentsDir: string;
26
33
  /** Key under mcpServers in Claude Code's config. */
@@ -105,6 +112,7 @@ interface ResolveConfigOptions {
105
112
  apiUrl?: string;
106
113
  masterKey?: string;
107
114
  claudeConfigPath?: string;
115
+ claudeSettingsPath?: string;
108
116
  agentsDir?: string;
109
117
  mcpServerName?: string;
110
118
  bridgeAgentName?: string;
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Dispatcher
4
- } from "./chunk-JURPYPKP.js";
5
- import "./chunk-3D5VXS5Y.js";
4
+ } from "./chunk-YWUZKKQ5.js";
5
+ import "./chunk-SBP7MJP2.js";
6
6
 
7
7
  // src/dispatcher-bin.ts
8
8
  async function main() {
@@ -1,4 +1,4 @@
1
- import { R as ResolveConfigOptions, A as AgenticMailAccount } from './config-BegnlyPD.js';
1
+ import { R as ResolveConfigOptions, A as AgenticMailAccount } from './config-CjEDSvVy.js';
2
2
 
3
3
  /**
4
4
  * AgenticMail → Claude Code event dispatcher.
@@ -62,6 +62,12 @@ interface SSEEvent {
62
62
  taskType?: string;
63
63
  task?: string;
64
64
  assignee?: string;
65
+ /**
66
+ * Optional wake allowlist set by the sender via `send_email({ wake })`.
67
+ * When present, only listed agents (case-insensitive bare name) get a
68
+ * Claude turn. When absent, every CC'd recipient wakes (v0.8.x default).
69
+ */
70
+ wakeAllowlist?: string[];
65
71
  [key: string]: unknown;
66
72
  }
67
73
  interface DispatcherOptions extends ResolveConfigOptions {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Dispatcher
3
- } from "./chunk-JURPYPKP.js";
4
- import "./chunk-3D5VXS5Y.js";
3
+ } from "./chunk-YWUZKKQ5.js";
4
+ import "./chunk-SBP7MJP2.js";
5
5
  export {
6
6
  Dispatcher
7
7
  };
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  createIntegrationRoutes
3
- } from "./chunk-WAUWKOHA.js";
4
- import "./chunk-52LXPWO7.js";
5
- import "./chunk-V3QMDNTR.js";
6
- import "./chunk-CQLUFM7N.js";
3
+ } from "./chunk-GZR3Z53N.js";
4
+ import "./chunk-D2GHBPXZ.js";
5
+ import "./chunk-G2CF37UQ.js";
6
+ import "./chunk-Q3PCM4MO.js";
7
+ import "./chunk-O4H76K3B.js";
7
8
  import "./chunk-US5FT2UB.js";
8
- import "./chunk-3D5VXS5Y.js";
9
+ import "./chunk-SBP7MJP2.js";
9
10
  export {
10
11
  createIntegrationRoutes
11
12
  };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { install } from './install.js';
2
2
  export { UninstallOptions, uninstall } from './uninstall.js';
3
3
  export { status } from './status.js';
4
- import { A as AgenticMailAccount } from './config-BegnlyPD.js';
5
- export { C as ClaudeCodeIntegrationConfig, I as InstallResult, a as InstallStatus, R as ResolveConfigOptions, U as UninstallResult, r as resolveConfig } from './config-BegnlyPD.js';
4
+ import { A as AgenticMailAccount } from './config-CjEDSvVy.js';
5
+ export { C as ClaudeCodeIntegrationConfig, I as InstallResult, a as InstallStatus, R as ResolveConfigOptions, U as UninstallResult, r as resolveConfig } from './config-CjEDSvVy.js';
6
6
  export { createIntegrationRoutes } from './http-routes.js';
7
7
  export { Dispatcher, DispatcherOptions, QueryFn } from './dispatcher.js';
8
8
  import 'express';
package/dist/index.js CHANGED
@@ -1,19 +1,20 @@
1
1
  import {
2
2
  Dispatcher,
3
3
  loadPersonaForAgent
4
- } from "./chunk-JURPYPKP.js";
4
+ } from "./chunk-YWUZKKQ5.js";
5
5
  import {
6
6
  createIntegrationRoutes
7
- } from "./chunk-WAUWKOHA.js";
7
+ } from "./chunk-GZR3Z53N.js";
8
+ import {
9
+ uninstall
10
+ } from "./chunk-D2GHBPXZ.js";
8
11
  import {
9
12
  install
10
- } from "./chunk-52LXPWO7.js";
13
+ } from "./chunk-G2CF37UQ.js";
14
+ import "./chunk-Q3PCM4MO.js";
11
15
  import {
12
16
  status
13
- } from "./chunk-V3QMDNTR.js";
14
- import {
15
- uninstall
16
- } from "./chunk-CQLUFM7N.js";
17
+ } from "./chunk-O4H76K3B.js";
17
18
  import "./chunk-US5FT2UB.js";
18
19
  import {
19
20
  AgenticMailApiError,
@@ -26,7 +27,7 @@ import {
26
27
  renderPersonaBody,
27
28
  renderSubagentMarkdown,
28
29
  resolveConfig
29
- } from "./chunk-3D5VXS5Y.js";
30
+ } from "./chunk-SBP7MJP2.js";
30
31
  export {
31
32
  AgenticMailApiError,
32
33
  Dispatcher,
package/dist/install.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { R as ResolveConfigOptions, I as InstallResult, A as AgenticMailAccount, C as ClaudeCodeIntegrationConfig } from './config-BegnlyPD.js';
1
+ import { R as ResolveConfigOptions, I as InstallResult, A as AgenticMailAccount, C as ClaudeCodeIntegrationConfig } from './config-CjEDSvVy.js';
2
2
 
3
3
  /**
4
4
  * Install AgenticMail into Claude Code.
package/dist/install.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  install,
3
3
  selectExposableAgents
4
- } from "./chunk-52LXPWO7.js";
4
+ } from "./chunk-G2CF37UQ.js";
5
+ import "./chunk-Q3PCM4MO.js";
5
6
  import "./chunk-US5FT2UB.js";
6
- import "./chunk-3D5VXS5Y.js";
7
+ import "./chunk-SBP7MJP2.js";
7
8
  export {
8
9
  install,
9
10
  selectExposableAgents
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mail-hook.ts
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { join, dirname } from "path";
7
+ var AGENTICMAIL_DIR = join(homedir(), ".agenticmail");
8
+ var CONFIG_PATH = join(AGENTICMAIL_DIR, "config.json");
9
+ var CURSOR_PATH = join(AGENTICMAIL_DIR, "claudecode-hook-cursor.json");
10
+ var HOOK_VERSION = "1";
11
+ var HTTP_TIMEOUT_MS = 2e3;
12
+ var PRE_TOOL_USE_THROTTLE_MS = 3e4;
13
+ async function readStdinJson() {
14
+ if (process.stdin.isTTY) return null;
15
+ return new Promise((resolve) => {
16
+ let buf = "";
17
+ process.stdin.setEncoding("utf-8");
18
+ process.stdin.on("data", (chunk) => {
19
+ buf += chunk;
20
+ });
21
+ process.stdin.on("end", () => {
22
+ if (!buf.trim()) {
23
+ resolve(null);
24
+ return;
25
+ }
26
+ try {
27
+ resolve(JSON.parse(buf));
28
+ } catch {
29
+ resolve(null);
30
+ }
31
+ });
32
+ process.stdin.on("error", () => resolve(null));
33
+ setTimeout(() => resolve(null), 200).unref();
34
+ });
35
+ }
36
+ async function main() {
37
+ const input = await readStdinJson();
38
+ const eventName = input?.hook_event_name ?? "UserPromptSubmit";
39
+ if (!existsSync(CONFIG_PATH)) return;
40
+ let cfg;
41
+ try {
42
+ cfg = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
43
+ } catch {
44
+ return;
45
+ }
46
+ if (!cfg.masterKey) return;
47
+ const apiHost = cfg.api?.host ?? "127.0.0.1";
48
+ const apiPort = cfg.api?.port ?? 3829;
49
+ const apiUrl = `http://${apiHost}:${apiPort}`;
50
+ let bridge;
51
+ try {
52
+ const r = await fetch(`${apiUrl}/api/agenticmail/accounts`, {
53
+ headers: { Authorization: `Bearer ${cfg.masterKey}` },
54
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
55
+ });
56
+ if (!r.ok) return;
57
+ const data = await r.json();
58
+ bridge = (data.agents ?? []).find(
59
+ (a) => a.name === "claudecode" || a.name === "claude" || a.role === "bridge"
60
+ );
61
+ } catch {
62
+ return;
63
+ }
64
+ if (!bridge?.apiKey) return;
65
+ let cursorMs = 0;
66
+ let lastCheckedMs = 0;
67
+ if (existsSync(CURSOR_PATH)) {
68
+ try {
69
+ const c = JSON.parse(readFileSync(CURSOR_PATH, "utf-8"));
70
+ if (typeof c?.lastSeenMs === "number") cursorMs = c.lastSeenMs;
71
+ if (typeof c?.lastCheckedMs === "number") lastCheckedMs = c.lastCheckedMs;
72
+ } catch {
73
+ }
74
+ }
75
+ const now = Date.now();
76
+ if (eventName === "PreToolUse" && now - lastCheckedMs < PRE_TOOL_USE_THROTTLE_MS) {
77
+ return;
78
+ }
79
+ let messages = [];
80
+ try {
81
+ const r = await fetch(`${apiUrl}/api/agenticmail/mail/inbox?limit=20`, {
82
+ headers: { Authorization: `Bearer ${bridge.apiKey}` },
83
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
84
+ });
85
+ if (!r.ok) return;
86
+ const data = await r.json();
87
+ messages = data.messages ?? [];
88
+ } catch {
89
+ return;
90
+ }
91
+ const newOnes = messages.filter((m) => {
92
+ if (!m.date) return false;
93
+ const t = new Date(m.date).getTime();
94
+ return Number.isFinite(t) && t > cursorMs;
95
+ });
96
+ if (newOnes.length === 0) {
97
+ if (eventName === "PreToolUse") {
98
+ try {
99
+ if (!existsSync(dirname(CURSOR_PATH))) mkdirSync(dirname(CURSOR_PATH), { recursive: true });
100
+ writeFileSync(
101
+ CURSOR_PATH,
102
+ JSON.stringify({ lastSeenMs: cursorMs, lastCheckedMs: now, hookVersion: HOOK_VERSION }, null, 2)
103
+ );
104
+ } catch {
105
+ }
106
+ }
107
+ return;
108
+ }
109
+ newOnes.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
110
+ const lines = [];
111
+ lines.push(`[AgenticMail bridge inbox] You have ${newOnes.length} new email${newOnes.length === 1 ? "" : "s"} since your last turn:`);
112
+ for (const m of newOnes) {
113
+ const fromAddr = m.from?.[0]?.address ?? "unknown";
114
+ const fromName = m.from?.[0]?.name ?? "";
115
+ const fromDisp = fromName && fromName !== fromAddr ? `${fromName} <${fromAddr}>` : fromAddr;
116
+ const subj = m.subject ?? "(no subject)";
117
+ const preview = (m.preview ?? "").replace(/\s+/g, " ").trim().slice(0, 120);
118
+ const tail = preview ? ` \u2014 ${preview}${preview.length === 120 ? "\u2026" : ""}` : "";
119
+ lines.push(`- UID ${m.uid} | from ${fromDisp} | "${subj}"${tail}`);
120
+ }
121
+ lines.push("");
122
+ lines.push(
123
+ "These are real replies from sub-agents (or mid-task questions). If any of them are addressed to you (Claude Code, the host), surface them to the user and act on whichever they direct you to. Use mcp__agenticmail__read_email for the full body, mcp__agenticmail__reply_email (with replyAll: true) to respond on the thread. You do NOT need to ping the user \u2014 just be aware these landed."
124
+ );
125
+ const newestMs = Math.max(...newOnes.map((m) => new Date(m.date).getTime()));
126
+ try {
127
+ if (!existsSync(dirname(CURSOR_PATH))) mkdirSync(dirname(CURSOR_PATH), { recursive: true });
128
+ writeFileSync(
129
+ CURSOR_PATH,
130
+ JSON.stringify(
131
+ { lastSeenMs: newestMs, lastCheckedMs: now, hookVersion: HOOK_VERSION },
132
+ null,
133
+ 2
134
+ )
135
+ );
136
+ } catch {
137
+ }
138
+ process.stdout.write(JSON.stringify({
139
+ hookSpecificOutput: {
140
+ hookEventName: eventName,
141
+ additionalContext: lines.join("\n")
142
+ }
143
+ }));
144
+ }
145
+ main().catch(() => {
146
+ process.exit(0);
147
+ });
package/dist/status.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { R as ResolveConfigOptions, a as InstallStatus } from './config-BegnlyPD.js';
1
+ import { R as ResolveConfigOptions, a as InstallStatus } from './config-CjEDSvVy.js';
2
2
 
3
3
  /**
4
4
  * Inspect the current install state of @agenticmail/claudecode.
package/dist/status.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  status
3
- } from "./chunk-V3QMDNTR.js";
3
+ } from "./chunk-O4H76K3B.js";
4
4
  import "./chunk-US5FT2UB.js";
5
- import "./chunk-3D5VXS5Y.js";
5
+ import "./chunk-SBP7MJP2.js";
6
6
  export {
7
7
  status
8
8
  };
@@ -1,4 +1,4 @@
1
- import { R as ResolveConfigOptions, U as UninstallResult } from './config-BegnlyPD.js';
1
+ import { R as ResolveConfigOptions, U as UninstallResult } from './config-CjEDSvVy.js';
2
2
 
3
3
  /**
4
4
  * Uninstall AgenticMail from Claude Code.
package/dist/uninstall.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  uninstall
3
- } from "./chunk-CQLUFM7N.js";
3
+ } from "./chunk-D2GHBPXZ.js";
4
+ import "./chunk-Q3PCM4MO.js";
4
5
  import "./chunk-US5FT2UB.js";
5
- import "./chunk-3D5VXS5Y.js";
6
+ import "./chunk-SBP7MJP2.js";
6
7
  export {
7
8
  uninstall
8
9
  };
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@agenticmail/claudecode",
3
- "version": "0.1.10",
3
+ "version": "0.1.13",
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",
7
7
  "types": "dist/index.d.ts",
8
8
  "bin": {
9
9
  "agenticmail-claudecode": "dist/cli.js",
10
- "agenticmail-claudecode-dispatcher": "dist/dispatcher-bin.js"
10
+ "agenticmail-claudecode-dispatcher": "dist/dispatcher-bin.js",
11
+ "agenticmail-mail-hook": "dist/mail-hook.js"
11
12
  },
12
13
  "exports": {
13
14
  ".": {
@@ -40,7 +41,7 @@
40
41
  "LICENSE"
41
42
  ],
42
43
  "scripts": {
43
- "build": "tsup src/index.ts src/cli.ts src/install.ts src/uninstall.ts src/status.ts src/http-routes.ts src/dispatcher.ts src/dispatcher-bin.ts --format esm --dts --clean",
44
+ "build": "tsup src/index.ts src/cli.ts src/install.ts src/uninstall.ts src/status.ts src/http-routes.ts src/dispatcher.ts src/dispatcher-bin.ts src/mail-hook.ts --format esm --dts --clean",
44
45
  "test": "vitest run --passWithNoTests",
45
46
  "preuninstall": "node scripts/uninstall.mjs",
46
47
  "prepublishOnly": "npm run build"