@agenticmail/claudecode 0.1.9 → 0.1.12
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 +43 -12
- package/dist/chunk-3ZIHOOA4.js +90 -0
- package/dist/{chunk-WAUWKOHA.js → chunk-5H46YKCX.js} +6 -6
- package/dist/{chunk-CQLUFM7N.js → chunk-6SCQVJBL.js} +10 -2
- package/dist/{chunk-52LXPWO7.js → chunk-DI3DIMUQ.js} +10 -2
- package/dist/{chunk-V3QMDNTR.js → chunk-O4H76K3B.js} +1 -1
- package/dist/{chunk-3D5VXS5Y.js → chunk-SBP7MJP2.js} +7 -3
- package/dist/{chunk-FBO6F4IC.js → chunk-YWUZKKQ5.js} +117 -9
- package/dist/cli.js +7 -6
- package/dist/{config-BegnlyPD.d.ts → config-CjEDSvVy.d.ts} +8 -0
- package/dist/dispatcher-bin.js +2 -2
- package/dist/dispatcher.d.ts +16 -1
- package/dist/dispatcher.js +2 -2
- package/dist/http-routes.js +6 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +9 -8
- package/dist/install.d.ts +1 -1
- package/dist/install.js +3 -2
- package/dist/mail-hook.d.ts +1 -0
- package/dist/mail-hook.js +147 -0
- package/dist/status.d.ts +1 -1
- package/dist/status.js +2 -2
- package/dist/uninstall.d.ts +1 -1
- package/dist/uninstall.js +3 -2
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/agenticmail/agenticmail/main/docs/images/logo-200.png" alt="AgenticMail logo (pink bow)" width="180" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">@agenticmail/claudecode</h1>
|
|
6
|
+
|
|
7
|
+
> Surfaces every [AgenticMail](https://github.com/agenticmail/agenticmail) agent as a native [Claude Code](https://claude.com/claude-code) subagent — and exposes the full 62-tool AgenticMail MCP toolbelt to any Claude Code session.
|
|
4
8
|
|
|
5
9
|
After install, a Claude Code session can write:
|
|
6
10
|
|
|
@@ -12,6 +16,17 @@ Agent { subagent_type: "agenticmail-fola", prompt: "draft a reply to my last ema
|
|
|
12
16
|
|
|
13
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.
|
|
14
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
|
+
|
|
15
30
|
## Multi-agent coordination via the dispatcher
|
|
16
31
|
|
|
17
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.
|
|
@@ -29,7 +44,7 @@ Anyone (you, an agent, a curl)
|
|
|
29
44
|
|
|
30
45
|
Each worker uses the user's Claude OAuth (the same auth `claude` itself uses — no separate Anthropic key). Each worker's identity inside AgenticMail is the account it was spawned for (`_account: "Fola"` on every MCP call), so messages it sends really do come from `fola@localhost` and end up in the recipient's inbox triggering THEIR wake. **Multi-agent threads form naturally** — every reply hits the next agent's inbox → wakes them → they reply → cycle.
|
|
31
46
|
|
|
32
|
-
Provisioning new agents is just `mcp__agenticmail__create_account({ name: "worker-7", role: "task-runner" })`. The dispatcher
|
|
47
|
+
Provisioning new agents is just `mcp__agenticmail__create_account({ name: "worker-7", role: "task-runner" })`. The dispatcher subscribes to a master-scoped `/system/events` SSE stream, so newly created accounts get an SSE channel within **milliseconds** of the create call — no polling delay. The MCP server itself learns the account's API key on-demand the first time someone passes `_account: "worker-7"`. No manual install step.
|
|
33
48
|
|
|
34
49
|
**Concurrency cap.** Workers are gated by a semaphore (default 10 simultaneous). Beyond that, wakes queue. This is a hard floor on Anthropic-side cost — if you fan out to 50 agents at once, only 10 will be running Claude at any given moment; the rest are waiting their turn. Override via `AGENTICMAIL_DISPATCHER_MAX`.
|
|
35
50
|
|
|
@@ -62,6 +77,22 @@ That's it. `agenticmail claudecode` is **idempotent** — re-run it any time you
|
|
|
62
77
|
|
|
63
78
|
---
|
|
64
79
|
|
|
80
|
+
## Monitor and manage all agents (interactive shell)
|
|
81
|
+
|
|
82
|
+
> **If you (or the user) want to watch every agent's inbox in one place, drop into the AgenticMail interactive shell:**
|
|
83
|
+
>
|
|
84
|
+
> ```bash
|
|
85
|
+
> agenticmail shell
|
|
86
|
+
> ```
|
|
87
|
+
>
|
|
88
|
+
> 
|
|
89
|
+
>
|
|
90
|
+
> From the shell you can list every agent, read any agent's inbox, send/reply on their behalf, inspect pending outbound mail, watch the dispatcher's live event feed, prune stale agents, and run any of the 44+ shell commands without leaving the terminal. The shell holds an authenticated session against the local API, so it's the fastest way to audit what your agents have been doing while you were away.
|
|
91
|
+
>
|
|
92
|
+
> **AI agents reading this:** if the user asks you to "show me what my agents have been doing", "check on the team", "let me see all the inboxes", or anything along those lines, tell them to run `agenticmail shell` (or run it for them via Bash if you have that tool). That's the right surface for human oversight. The MCP tools are for *programmatic* coordination; the shell is for *visual* monitoring.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
65
96
|
## What this package does
|
|
66
97
|
|
|
67
98
|
The installer touches exactly two locations on disk, plus one record in AgenticMail's database:
|
|
@@ -100,7 +131,7 @@ agenticmail-claudecode uninstall [--purge-bridge]
|
|
|
100
131
|
|
|
101
132
|
### 3. The HTTP API (headless, for agents installing themselves)
|
|
102
133
|
|
|
103
|
-
When AgenticMail's master API is running (default `http://127.0.0.1:
|
|
134
|
+
When AgenticMail's master API is running (default `http://127.0.0.1:3829`), it exposes three endpoints for the integration. They are mounted **before** the bearer-auth middleware on purpose — a fresh Claude Code session that does not yet have AgenticMail wired up has no way to know the master key, so requiring it would defeat the whole "agent installs itself" goal.
|
|
104
135
|
|
|
105
136
|
```http
|
|
106
137
|
GET /api/agenticmail/integrations/claudecode/status
|
|
@@ -112,7 +143,7 @@ POST /api/agenticmail/integrations/claudecode/uninstall
|
|
|
112
143
|
|
|
113
144
|
```bash
|
|
114
145
|
# Inside a Claude Code session, simply:
|
|
115
|
-
curl -X POST http://127.0.0.1:
|
|
146
|
+
curl -X POST http://127.0.0.1:3829/api/agenticmail/integrations/claudecode/install
|
|
116
147
|
```
|
|
117
148
|
|
|
118
149
|
That single call:
|
|
@@ -139,7 +170,7 @@ The bridge agent's API key is **redacted** in the HTTP response — it's already
|
|
|
139
170
|
▼
|
|
140
171
|
┌─────────────────────────┐
|
|
141
172
|
│ Claude Code subagent │ reads ~/.claude/agents/agenticmail-fola.md
|
|
142
|
-
│ ("agenticmail-fola") │
|
|
173
|
+
│ ("agenticmail-fola") │ full toolset: AgenticMail MCP + native (Read/Write/Bash/…)
|
|
143
174
|
└───────────┬─────────────┘
|
|
144
175
|
│ mcp__agenticmail__call_agent(target: "Fola", task: <prompt>)
|
|
145
176
|
▼
|
|
@@ -147,11 +178,11 @@ The bridge agent's API key is **redacted** in the HTTP response — it's already
|
|
|
147
178
|
│ @agenticmail/mcp │ stdio child process spawned by Claude Code
|
|
148
179
|
│ (MCP server) │ authenticated as the "claudecode" bridge agent
|
|
149
180
|
└───────────┬─────────────┘
|
|
150
|
-
│ POST http://127.0.0.1:
|
|
181
|
+
│ POST http://127.0.0.1:3829/api/agenticmail/tasks/rpc
|
|
151
182
|
▼
|
|
152
183
|
┌─────────────────────────┐
|
|
153
184
|
│ AgenticMail master API │ creates a task, signals the target agent,
|
|
154
|
-
│ (port
|
|
185
|
+
│ (port 3829) │ long-polls until the agent submits a result
|
|
155
186
|
└───────────┬─────────────┘
|
|
156
187
|
│ task event over SSE / email notification
|
|
157
188
|
▼
|
|
@@ -174,7 +205,7 @@ The MCP server reads **four** env vars (written into `~/.claude.json` by the ins
|
|
|
174
205
|
|
|
175
206
|
| Variable | Purpose |
|
|
176
207
|
|---|---|
|
|
177
|
-
| `AGENTICMAIL_API_URL` | Where the master API lives (default `http://127.0.0.1:
|
|
208
|
+
| `AGENTICMAIL_API_URL` | Where the master API lives (default `http://127.0.0.1:3829`). |
|
|
178
209
|
| `AGENTICMAIL_API_KEY` | Bridge agent's API key (`ak_…`). The *default* identity — used when a tool call doesn't pass `_account`. Effectively "Claude Code talking on its own behalf". |
|
|
179
210
|
| `AGENTICMAIL_MASTER_KEY` | The master key (`mk_…`). Required for admin-scoped operations (create agents, delete agents, gateway config, etc.). |
|
|
180
211
|
| `AGENTICMAIL_ACCOUNT_KEYS_JSON` | A JSON map `{ "<agentName>": "<apiKey>" }` of every other AgenticMail agent. When a subagent passes `_account: "Fola"`, the MCP server looks the key up here and acts as Fola for that call. |
|
|
@@ -259,7 +290,7 @@ Almost no one needs these — defaults are correct for the standard AgenticMail
|
|
|
259
290
|
|
|
260
291
|
| Env var | Default |
|
|
261
292
|
|---|---|
|
|
262
|
-
| `AGENTICMAIL_API_URL` | `http://127.0.0.1:
|
|
293
|
+
| `AGENTICMAIL_API_URL` | `http://127.0.0.1:3829` (or whatever `~/.agenticmail/config.json` says) |
|
|
263
294
|
| `AGENTICMAIL_MASTER_KEY` | Pulled from `~/.agenticmail/config.json` |
|
|
264
295
|
| `CLAUDE_CODE_CONFIG_PATH` | `~/.claude.json` |
|
|
265
296
|
| `CLAUDE_CODE_AGENTS_DIR` | `~/.claude/agents` |
|
|
@@ -270,7 +301,7 @@ Programmatic install (from another tool):
|
|
|
270
301
|
import { install, status, uninstall } from '@agenticmail/claudecode';
|
|
271
302
|
|
|
272
303
|
await install({
|
|
273
|
-
apiUrl: 'http://127.0.0.1:
|
|
304
|
+
apiUrl: 'http://127.0.0.1:3829',
|
|
274
305
|
masterKey: 'mk_...',
|
|
275
306
|
// any other ResolveConfigOptions field
|
|
276
307
|
});
|
|
@@ -280,7 +311,7 @@ await install({
|
|
|
280
311
|
|
|
281
312
|
## Troubleshooting
|
|
282
313
|
|
|
283
|
-
**`AgenticMail API unreachable at http://127.0.0.1:
|
|
314
|
+
**`AgenticMail API unreachable at http://127.0.0.1:3829`**
|
|
284
315
|
The master API isn't running. Start it with `agenticmail start`.
|
|
285
316
|
|
|
286
317
|
**`AgenticMail master key not found`**
|
|
@@ -0,0 +1,90 @@
|
|
|
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 = ["UserPromptSubmit", "PreToolUse"];
|
|
6
|
+
function readSettings(path) {
|
|
7
|
+
if (!existsSync(path)) return {};
|
|
8
|
+
const raw = readFileSync(path, "utf-8");
|
|
9
|
+
if (!raw.trim()) return {};
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(raw);
|
|
12
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
13
|
+
return {};
|
|
14
|
+
} catch (err) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Could not parse Claude Code settings at ${path}: ${err.message}. Refusing to overwrite \u2014 please fix the file by hand and retry.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function writeSettings(path, settings) {
|
|
21
|
+
const dir = dirname(path);
|
|
22
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
23
|
+
const text = JSON.stringify(settings, null, 2) + "\n";
|
|
24
|
+
const tmp = `${path}.agenticmail-tmp`;
|
|
25
|
+
writeFileSync(tmp, text, "utf-8");
|
|
26
|
+
renameSync(tmp, path);
|
|
27
|
+
}
|
|
28
|
+
function upsertMailHook(path, command) {
|
|
29
|
+
const settings = readSettings(path);
|
|
30
|
+
if (!settings.hooks) settings.hooks = {};
|
|
31
|
+
let changed = false;
|
|
32
|
+
for (const event of HOOK_EVENTS) {
|
|
33
|
+
if (upsertOneEvent(settings.hooks, event, command)) changed = true;
|
|
34
|
+
}
|
|
35
|
+
if (changed) writeSettings(path, settings);
|
|
36
|
+
return changed;
|
|
37
|
+
}
|
|
38
|
+
function upsertOneEvent(hooks, event, command) {
|
|
39
|
+
const list = hooks[event] ?? [];
|
|
40
|
+
const isOurs = (rule) => rule.hooks?.some((h) => typeof h.command === "string" && h.command.includes(AGENTICMAIL_HOOK_MARKER)) ?? false;
|
|
41
|
+
const desired = {
|
|
42
|
+
matcher: "",
|
|
43
|
+
// empty = match every fire of this event
|
|
44
|
+
hooks: [{ type: "command", command }]
|
|
45
|
+
};
|
|
46
|
+
const existingIdx = list.findIndex(isOurs);
|
|
47
|
+
if (existingIdx >= 0) {
|
|
48
|
+
const existing = list[existingIdx];
|
|
49
|
+
if (existing.matcher === desired.matcher && existing.hooks.length === desired.hooks.length && existing.hooks.every((h, i) => h.command === desired.hooks[i].command)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
list[existingIdx] = desired;
|
|
53
|
+
} else {
|
|
54
|
+
list.push(desired);
|
|
55
|
+
}
|
|
56
|
+
hooks[event] = list;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
function removeMailHook(path) {
|
|
60
|
+
if (!existsSync(path)) return false;
|
|
61
|
+
const settings = readSettings(path);
|
|
62
|
+
if (!settings.hooks) return false;
|
|
63
|
+
let changed = false;
|
|
64
|
+
for (const event of HOOK_EVENTS) {
|
|
65
|
+
const list = settings.hooks[event] ?? [];
|
|
66
|
+
if (list.length === 0) continue;
|
|
67
|
+
const filtered = list.filter(
|
|
68
|
+
(rule) => !rule.hooks?.some((h) => typeof h.command === "string" && h.command.includes(AGENTICMAIL_HOOK_MARKER))
|
|
69
|
+
);
|
|
70
|
+
if (filtered.length === list.length) continue;
|
|
71
|
+
if (filtered.length === 0) {
|
|
72
|
+
delete settings.hooks[event];
|
|
73
|
+
} else {
|
|
74
|
+
settings.hooks[event] = filtered;
|
|
75
|
+
}
|
|
76
|
+
changed = true;
|
|
77
|
+
}
|
|
78
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
79
|
+
delete settings.hooks;
|
|
80
|
+
}
|
|
81
|
+
if (changed) writeSettings(path, settings);
|
|
82
|
+
return changed;
|
|
83
|
+
}
|
|
84
|
+
var upsertUserPromptSubmitHook = upsertMailHook;
|
|
85
|
+
var removeUserPromptSubmitHook = removeMailHook;
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
upsertUserPromptSubmitHook,
|
|
89
|
+
removeUserPromptSubmitHook
|
|
90
|
+
};
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
uninstall
|
|
3
|
+
} from "./chunk-6SCQVJBL.js";
|
|
1
4
|
import {
|
|
2
5
|
install
|
|
3
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-DI3DIMUQ.js";
|
|
4
7
|
import {
|
|
5
8
|
status
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import {
|
|
8
|
-
uninstall
|
|
9
|
-
} from "./chunk-CQLUFM7N.js";
|
|
9
|
+
} from "./chunk-O4H76K3B.js";
|
|
10
10
|
import {
|
|
11
11
|
AgenticMailApiError
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-SBP7MJP2.js";
|
|
13
13
|
|
|
14
14
|
// src/http-routes.ts
|
|
15
15
|
import { Router } from "express";
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
removeUserPromptSubmitHook
|
|
3
|
+
} from "./chunk-3ZIHOOA4.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-
|
|
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-3ZIHOOA4.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-
|
|
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
|
}
|
|
@@ -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.
|
|
191
|
-
` 5.
|
|
192
|
-
` 6. If
|
|
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-
|
|
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. **
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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) {
|
|
@@ -376,7 +431,12 @@ var Dispatcher = class {
|
|
|
376
431
|
this.log("warn", `[dispatcher] wake-budget exhausted for "${account.name}" on thread "${threadId}" (count=${verdict.count}, cap=${this.maxWakesPerThread}); muted for ~${minutesUntil}min. uid=${event.uid}, subject="${subject ?? ""}"`);
|
|
377
432
|
return;
|
|
378
433
|
}
|
|
379
|
-
await this.spawnWorker(account, newMailPrompt(account, event), {
|
|
434
|
+
await this.spawnWorker(account, newMailPrompt(account, event), {
|
|
435
|
+
kind: "new-mail",
|
|
436
|
+
uid: event.uid,
|
|
437
|
+
subject: extractSubject(event),
|
|
438
|
+
from: extractFrom(event)
|
|
439
|
+
});
|
|
380
440
|
return;
|
|
381
441
|
}
|
|
382
442
|
if (event.type === "task" && typeof event.taskId === "string") {
|
|
@@ -387,7 +447,12 @@ var Dispatcher = class {
|
|
|
387
447
|
rememberBounded(ch.seenTaskIds, event.taskId);
|
|
388
448
|
ch.suppressTaskMailUntilMs = Date.now() + TASK_MAIL_SUPPRESS_WINDOW_MS;
|
|
389
449
|
}
|
|
390
|
-
await this.spawnWorker(account, taskPrompt(account, event), {
|
|
450
|
+
await this.spawnWorker(account, taskPrompt(account, event), {
|
|
451
|
+
kind: "task",
|
|
452
|
+
taskId: event.taskId,
|
|
453
|
+
subject: typeof event.task === "string" ? event.task.slice(0, 120) : void 0,
|
|
454
|
+
from: typeof event.from === "string" ? event.from : void 0
|
|
455
|
+
});
|
|
391
456
|
return;
|
|
392
457
|
}
|
|
393
458
|
}
|
|
@@ -625,6 +690,15 @@ var Dispatcher = class {
|
|
|
625
690
|
/** Acquire a concurrency slot, run a worker, release the slot. */
|
|
626
691
|
async spawnWorker(account, prompt, ctx) {
|
|
627
692
|
await this.acquireSlot();
|
|
693
|
+
const workerId = `${account.id}:${ctx.kind}:${ctx.uid ?? ctx.taskId ?? ""}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
694
|
+
let workerResult = null;
|
|
695
|
+
this.postActivity("/dispatcher/worker-started", {
|
|
696
|
+
workerId,
|
|
697
|
+
agentName: account.name,
|
|
698
|
+
agentEmail: account.email,
|
|
699
|
+
kind: ctx.kind,
|
|
700
|
+
trigger: { uid: ctx.uid, taskId: ctx.taskId, subject: ctx.subject, from: ctx.from }
|
|
701
|
+
});
|
|
628
702
|
try {
|
|
629
703
|
const { body } = loadPersonaForAgent({
|
|
630
704
|
agent: account,
|
|
@@ -634,7 +708,7 @@ var Dispatcher = class {
|
|
|
634
708
|
});
|
|
635
709
|
this.log("info", `[dispatcher] waking "${account.name}" \u2014 ${ctx.kind}${ctx.taskId ? " " + ctx.taskId : ctx.uid ? " uid=" + ctx.uid : ""}`);
|
|
636
710
|
const mcpEnv = await this.buildMcpEnv();
|
|
637
|
-
await runWorker(
|
|
711
|
+
workerResult = await runWorker(
|
|
638
712
|
this.query,
|
|
639
713
|
body,
|
|
640
714
|
prompt,
|
|
@@ -647,6 +721,40 @@ var Dispatcher = class {
|
|
|
647
721
|
);
|
|
648
722
|
} finally {
|
|
649
723
|
this.releaseSlot();
|
|
724
|
+
const ok = workerResult?.ok === true;
|
|
725
|
+
const preview = workerResult?.ok ? workerResult.text : workerResult ? workerResult.error : "worker did not start";
|
|
726
|
+
this.postActivity("/dispatcher/worker-finished", {
|
|
727
|
+
workerId,
|
|
728
|
+
agentName: account.name,
|
|
729
|
+
ok,
|
|
730
|
+
resultPreview: typeof preview === "string" ? preview.slice(0, 240) : void 0
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Fire-and-forget POST to the API's worker-activity endpoints.
|
|
736
|
+
*
|
|
737
|
+
* Failures are swallowed deliberately — the dispatcher must never
|
|
738
|
+
* block worker spawn or interrupt teardown because the API is briefly
|
|
739
|
+
* unreachable. The activity registry is best-effort observability, not
|
|
740
|
+
* load-bearing state.
|
|
741
|
+
*/
|
|
742
|
+
postActivity(path, body) {
|
|
743
|
+
const url = `${this.cfg.apiUrl.replace(/\/$/, "")}/api/agenticmail${path}`;
|
|
744
|
+
try {
|
|
745
|
+
const result = this.fetchImpl(url, {
|
|
746
|
+
method: "POST",
|
|
747
|
+
headers: {
|
|
748
|
+
"Content-Type": "application/json",
|
|
749
|
+
"Authorization": `Bearer ${this.cfg.masterKey}`
|
|
750
|
+
},
|
|
751
|
+
body: JSON.stringify(body)
|
|
752
|
+
});
|
|
753
|
+
if (result && typeof result.catch === "function") {
|
|
754
|
+
void result.catch(() => {
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
} catch {
|
|
650
758
|
}
|
|
651
759
|
}
|
|
652
760
|
/** Build the env block we pass to the worker's MCP server child process. */
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
uninstall
|
|
4
|
+
} from "./chunk-6SCQVJBL.js";
|
|
2
5
|
import {
|
|
3
6
|
install
|
|
4
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-DI3DIMUQ.js";
|
|
8
|
+
import "./chunk-3ZIHOOA4.js";
|
|
5
9
|
import {
|
|
6
10
|
status
|
|
7
|
-
} from "./chunk-
|
|
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-
|
|
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;
|
package/dist/dispatcher-bin.js
CHANGED
package/dist/dispatcher.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as ResolveConfigOptions, A as AgenticMailAccount } from './config-
|
|
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 {
|
|
@@ -203,6 +209,15 @@ declare class Dispatcher {
|
|
|
203
209
|
private streamOne;
|
|
204
210
|
/** Acquire a concurrency slot, run a worker, release the slot. */
|
|
205
211
|
private spawnWorker;
|
|
212
|
+
/**
|
|
213
|
+
* Fire-and-forget POST to the API's worker-activity endpoints.
|
|
214
|
+
*
|
|
215
|
+
* Failures are swallowed deliberately — the dispatcher must never
|
|
216
|
+
* block worker spawn or interrupt teardown because the API is briefly
|
|
217
|
+
* unreachable. The activity registry is best-effort observability, not
|
|
218
|
+
* load-bearing state.
|
|
219
|
+
*/
|
|
220
|
+
private postActivity;
|
|
206
221
|
/** Build the env block we pass to the worker's MCP server child process. */
|
|
207
222
|
private buildMcpEnv;
|
|
208
223
|
private acquireSlot;
|
package/dist/dispatcher.js
CHANGED
package/dist/http-routes.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createIntegrationRoutes
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-5H46YKCX.js";
|
|
4
|
+
import "./chunk-6SCQVJBL.js";
|
|
5
|
+
import "./chunk-DI3DIMUQ.js";
|
|
6
|
+
import "./chunk-3ZIHOOA4.js";
|
|
7
|
+
import "./chunk-O4H76K3B.js";
|
|
7
8
|
import "./chunk-US5FT2UB.js";
|
|
8
|
-
import "./chunk-
|
|
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-
|
|
5
|
-
export { C as ClaudeCodeIntegrationConfig, I as InstallResult, a as InstallStatus, R as ResolveConfigOptions, U as UninstallResult, r as resolveConfig } from './config-
|
|
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-
|
|
4
|
+
} from "./chunk-YWUZKKQ5.js";
|
|
5
5
|
import {
|
|
6
6
|
createIntegrationRoutes
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-5H46YKCX.js";
|
|
8
|
+
import {
|
|
9
|
+
uninstall
|
|
10
|
+
} from "./chunk-6SCQVJBL.js";
|
|
8
11
|
import {
|
|
9
12
|
install
|
|
10
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-DI3DIMUQ.js";
|
|
14
|
+
import "./chunk-3ZIHOOA4.js";
|
|
11
15
|
import {
|
|
12
16
|
status
|
|
13
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
4
|
+
} from "./chunk-DI3DIMUQ.js";
|
|
5
|
+
import "./chunk-3ZIHOOA4.js";
|
|
5
6
|
import "./chunk-US5FT2UB.js";
|
|
6
|
-
import "./chunk-
|
|
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
package/dist/status.js
CHANGED
package/dist/uninstall.d.ts
CHANGED
package/dist/uninstall.js
CHANGED
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenticmail/claudecode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
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"
|