@agenticmail/claudecode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,408 @@
1
+ import {
2
+ listAccounts,
3
+ renderPersonaBody,
4
+ resolveConfig
5
+ } from "./chunk-XAW5NUNU.js";
6
+
7
+ // src/persona-loader.ts
8
+ import { existsSync, readFileSync } from "fs";
9
+ import { join } from "path";
10
+ function sanitizeSubagentName(name) {
11
+ return name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
12
+ }
13
+ function stripFrontmatter(raw) {
14
+ const text = raw.replace(/\r\n/g, "\n");
15
+ if (!text.startsWith("---\n")) return text;
16
+ const close = text.indexOf("\n---", 4);
17
+ if (close < 0) return text;
18
+ let cursor = close + 4;
19
+ while (cursor < text.length && (text[cursor] === "\n" || text[cursor] === "\r")) cursor++;
20
+ return text.slice(cursor);
21
+ }
22
+ function loadPersonaForAgent(opts) {
23
+ const { agent, agentsDir, subagentPrefix, mcpServerName } = opts;
24
+ const basename = sanitizeSubagentName(`${subagentPrefix}${agent.name}`);
25
+ const filePath = join(agentsDir, `${basename}.md`);
26
+ if (existsSync(filePath)) {
27
+ try {
28
+ const raw = readFileSync(filePath, "utf-8");
29
+ const body2 = stripFrontmatter(raw).trim();
30
+ if (body2) return { body: body2, source: "file", filePath };
31
+ } catch {
32
+ }
33
+ }
34
+ const body = renderPersonaBody({ name: basename, agent, mcpServerName });
35
+ return { body, source: "generated" };
36
+ }
37
+
38
+ // src/dispatcher.ts
39
+ var SEEN_CAP = 1024;
40
+ function rememberBounded(set, item) {
41
+ set.add(item);
42
+ if (set.size > SEEN_CAP) {
43
+ const drop = Array.from(set).slice(0, Math.floor(SEEN_CAP / 2));
44
+ for (const x of drop) set.delete(x);
45
+ }
46
+ }
47
+ var DEFAULT_MAX_CONCURRENT = 10;
48
+ var DEFAULT_SYNC_INTERVAL_MS = 6e4;
49
+ var DEFAULT_RECONNECT_BASE_MS = 2e3;
50
+ var DEFAULT_RECONNECT_MAX_MS = 6e4;
51
+ async function runWorker(query, persona, userPrompt, agent, mcpServerName, mcpCommand, mcpArgs, mcpEnv, log, abortSignal) {
52
+ const opts = {
53
+ systemPrompt: persona,
54
+ mcpServers: {
55
+ [mcpServerName]: {
56
+ command: mcpCommand,
57
+ args: mcpArgs,
58
+ env: mcpEnv
59
+ }
60
+ },
61
+ // Restrict to MCP tools only — workers should never reach for
62
+ // Bash / Read / Edit / etc. Listing them avoids accidental leakage.
63
+ allowedTools: [
64
+ `mcp__${mcpServerName}__whoami`,
65
+ `mcp__${mcpServerName}__list_inbox`,
66
+ `mcp__${mcpServerName}__read_email`,
67
+ `mcp__${mcpServerName}__send_email`,
68
+ `mcp__${mcpServerName}__reply_email`,
69
+ `mcp__${mcpServerName}__search_emails`,
70
+ `mcp__${mcpServerName}__list_agents`,
71
+ `mcp__${mcpServerName}__message_agent`,
72
+ `mcp__${mcpServerName}__call_agent`,
73
+ `mcp__${mcpServerName}__check_tasks`,
74
+ `mcp__${mcpServerName}__claim_task`,
75
+ `mcp__${mcpServerName}__submit_result`,
76
+ `mcp__${mcpServerName}__request_tools`,
77
+ `mcp__${mcpServerName}__invoke`
78
+ ],
79
+ permissionMode: "bypassPermissions",
80
+ abortController: abortSignal ? wrapSignal(abortSignal) : void 0
81
+ };
82
+ const collectedText = [];
83
+ try {
84
+ for await (const msg of query({ prompt: userPrompt, options: opts })) {
85
+ const m = msg;
86
+ if (m.type === "assistant" && Array.isArray(m.message && m.message.content)) {
87
+ for (const block of m.message.content) {
88
+ const b = block;
89
+ if (b.type === "text" && typeof b.text === "string") collectedText.push(b.text);
90
+ }
91
+ }
92
+ if (m.type === "result" && typeof m.result === "string") {
93
+ collectedText.push(m.result);
94
+ }
95
+ }
96
+ const text = collectedText.join("\n").trim();
97
+ log("info", `[dispatcher] worker for "${agent.name}" finished (${text.length} chars output)`);
98
+ return { ok: true, text };
99
+ } catch (err) {
100
+ const msg = err instanceof Error ? err.message : String(err);
101
+ log("error", `[dispatcher] worker for "${agent.name}" failed: ${msg}`);
102
+ return { ok: false, error: msg };
103
+ }
104
+ }
105
+ function wrapSignal(signal) {
106
+ const c = new AbortController();
107
+ if (signal.aborted) c.abort();
108
+ else signal.addEventListener("abort", () => c.abort(), { once: true });
109
+ return c;
110
+ }
111
+ function newMailPrompt(agent, event) {
112
+ const from = event.from ?? "unknown sender";
113
+ const subject = event.subject ?? "(no subject)";
114
+ const uid = event.uid;
115
+ return [
116
+ `You have new mail.`,
117
+ ``,
118
+ `- From: ${from}`,
119
+ `- Subject: ${subject}`,
120
+ uid ? `- UID: ${uid}` : "",
121
+ ``,
122
+ `Open it (with read_email if a UID was given, or list_inbox otherwise), decide what to do as ${agent.name} would, and act:`,
123
+ ` - if it's a question or request \u2192 reply with reply_email`,
124
+ ` - if it requires research \u2192 use call_agent to ask the right teammate, then reply`,
125
+ ` - if it's an FYI \u2192 archive (mark_read) and move on`,
126
+ ` - if it's spam-looking \u2192 trust the auto-spam-filter; only intervene if it slipped through`,
127
+ ``,
128
+ `When you've handled it, return a one-line summary of what you did.`
129
+ ].filter(Boolean).join("\n");
130
+ }
131
+ function taskPrompt(agent, event) {
132
+ const taskId = event.taskId ?? "(missing taskId)";
133
+ const taskText = event.task ?? "(no task description)";
134
+ const taskType = event.taskType ?? "generic";
135
+ const from = event.from ?? "unknown";
136
+ return [
137
+ `You have a pending task \u2014 handle it now.`,
138
+ ``,
139
+ `- Task ID: ${taskId}`,
140
+ `- Type: ${taskType}`,
141
+ `- From: ${from}`,
142
+ `- Task: ${taskText}`,
143
+ ``,
144
+ `Workflow:`,
145
+ ` 1. Call claim_task({ id: "${taskId}", _account: "${agent.name}" }) to mark yourself as the owner.`,
146
+ ` 2. Do the work using whatever pre-loaded or invoke-able MCP tools fit.`,
147
+ ` 3. Call submit_result({ id: "${taskId}", result: { ... }, _account: "${agent.name}" }) with structured JSON.`,
148
+ ` The caller is waiting on a synchronous long-poll \u2014 submit_result is what wakes them.`,
149
+ ``,
150
+ `If you cannot complete the task, submit_result with { status: "failed", reason: "..." }. Never leave it unclaimed \u2014 that strands the caller until timeout.`
151
+ ].join("\n");
152
+ }
153
+ var Dispatcher = class {
154
+ cfg;
155
+ maxConcurrent;
156
+ syncIntervalMs;
157
+ reconnectBaseMs;
158
+ reconnectMaxMs;
159
+ query;
160
+ fetchImpl;
161
+ log;
162
+ channels = /* @__PURE__ */ new Map();
163
+ // keyed by account.id
164
+ accountSyncTimer = null;
165
+ running = 0;
166
+ waiters = [];
167
+ stopped = false;
168
+ constructor(opts = {}) {
169
+ this.cfg = resolveConfig(opts);
170
+ this.maxConcurrent = opts.maxConcurrentWorkers ?? DEFAULT_MAX_CONCURRENT;
171
+ this.syncIntervalMs = opts.accountSyncIntervalMs ?? DEFAULT_SYNC_INTERVAL_MS;
172
+ this.reconnectBaseMs = opts.sseReconnectBaseMs ?? DEFAULT_RECONNECT_BASE_MS;
173
+ this.reconnectMaxMs = opts.sseReconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
174
+ this.query = opts.querySdk ?? defaultQuery();
175
+ this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
176
+ this.log = opts.log ?? defaultLog;
177
+ if (!this.cfg.masterKey) {
178
+ throw new Error("Dispatcher requires AgenticMail master key. Run `agenticmail setup` first.");
179
+ }
180
+ }
181
+ async start() {
182
+ this.log("info", `[dispatcher] starting (maxConcurrent=${this.maxConcurrent}, syncEvery=${this.syncIntervalMs}ms)`);
183
+ await this.syncAccounts();
184
+ this.accountSyncTimer = setInterval(() => {
185
+ this.syncAccounts().catch((err) => this.log("warn", `[dispatcher] account sync failed: ${err}`));
186
+ }, this.syncIntervalMs);
187
+ }
188
+ async stop() {
189
+ this.stopped = true;
190
+ if (this.accountSyncTimer) clearInterval(this.accountSyncTimer);
191
+ this.accountSyncTimer = null;
192
+ for (const ch of this.channels.values()) {
193
+ ch.stopping = true;
194
+ ch.controller?.abort();
195
+ }
196
+ this.channels.clear();
197
+ this.log("info", "[dispatcher] stopped");
198
+ }
199
+ /** Public for tests — directly hand an event to the routing path. */
200
+ async handleEvent(account, event) {
201
+ if (this.stopped) return;
202
+ if (event.type === "new" && typeof event.uid === "number") {
203
+ const ch = this.channels.get(account.id);
204
+ if (ch?.seenUids.has(event.uid)) return;
205
+ if (ch) rememberBounded(ch.seenUids, event.uid);
206
+ await this.spawnWorker(account, newMailPrompt(account, event), { kind: "new-mail", uid: event.uid });
207
+ return;
208
+ }
209
+ if (event.type === "task" && typeof event.taskId === "string") {
210
+ if (typeof event.assignee === "string" && event.assignee.toLowerCase() !== account.name.toLowerCase()) return;
211
+ const ch = this.channels.get(account.id);
212
+ if (ch?.seenTaskIds.has(event.taskId)) return;
213
+ if (ch) rememberBounded(ch.seenTaskIds, event.taskId);
214
+ await this.spawnWorker(account, taskPrompt(account, event), { kind: "task", taskId: event.taskId });
215
+ return;
216
+ }
217
+ }
218
+ /** Re-fetch /accounts; open SSE for new ones, close for vanished ones. */
219
+ async syncAccounts() {
220
+ let accounts;
221
+ try {
222
+ accounts = await listAccounts(this.cfg.apiUrl, this.cfg.masterKey);
223
+ } catch (err) {
224
+ this.log("warn", `[dispatcher] could not list accounts: ${err.message}`);
225
+ return;
226
+ }
227
+ const liveIds = new Set(accounts.map((a) => a.id));
228
+ for (const [id, ch] of this.channels) {
229
+ if (!liveIds.has(id)) {
230
+ ch.stopping = true;
231
+ ch.controller?.abort();
232
+ this.channels.delete(id);
233
+ this.log("info", `[dispatcher] account "${ch.account.name}" removed \u2014 closed SSE channel`);
234
+ }
235
+ }
236
+ for (const account of accounts) {
237
+ if (this.channels.has(account.id)) {
238
+ this.channels.get(account.id).account = account;
239
+ continue;
240
+ }
241
+ const ch = {
242
+ account,
243
+ controller: null,
244
+ stopping: false,
245
+ backoffMs: this.reconnectBaseMs,
246
+ seenUids: /* @__PURE__ */ new Set(),
247
+ seenTaskIds: /* @__PURE__ */ new Set()
248
+ };
249
+ this.channels.set(account.id, ch);
250
+ this.log("info", `[dispatcher] opening SSE for "${account.name}" (${account.email})`);
251
+ void this.runChannel(ch);
252
+ }
253
+ }
254
+ /** Watch one account's SSE stream forever; reconnect with backoff on drop. */
255
+ async runChannel(ch) {
256
+ while (!ch.stopping && !this.stopped) {
257
+ try {
258
+ ch.controller = new AbortController();
259
+ await this.streamOne(ch);
260
+ if (!ch.stopping) {
261
+ this.log("warn", `[dispatcher] SSE for "${ch.account.name}" ended unexpectedly; reconnecting in ${ch.backoffMs}ms`);
262
+ }
263
+ } catch (err) {
264
+ if (ch.stopping) break;
265
+ this.log("warn", `[dispatcher] SSE error for "${ch.account.name}": ${err.message}; reconnecting in ${ch.backoffMs}ms`);
266
+ }
267
+ if (ch.stopping) break;
268
+ await sleep(ch.backoffMs);
269
+ ch.backoffMs = Math.min(ch.backoffMs * 2, this.reconnectMaxMs);
270
+ }
271
+ }
272
+ /** Single SSE attach. Returns when the stream closes for any reason. */
273
+ async streamOne(ch) {
274
+ const url = `${this.cfg.apiUrl.replace(/\/$/, "")}/api/agenticmail/events`;
275
+ const res = await this.fetchImpl(url, {
276
+ headers: {
277
+ "Authorization": `Bearer ${ch.account.apiKey}`,
278
+ "Accept": "text/event-stream"
279
+ },
280
+ signal: ch.controller.signal
281
+ });
282
+ if (!res.ok || !res.body) {
283
+ throw new Error(`SSE handshake HTTP ${res.status}`);
284
+ }
285
+ ch.backoffMs = this.reconnectBaseMs;
286
+ const reader = res.body.getReader();
287
+ const decoder = new TextDecoder();
288
+ let buffer = "";
289
+ while (!ch.stopping) {
290
+ const { value, done } = await reader.read();
291
+ if (done) return;
292
+ buffer += decoder.decode(value, { stream: true });
293
+ let boundary;
294
+ while ((boundary = buffer.indexOf("\n\n")) !== -1) {
295
+ const frame = buffer.slice(0, boundary);
296
+ buffer = buffer.slice(boundary + 2);
297
+ for (const line of frame.split("\n")) {
298
+ if (!line.startsWith("data: ")) continue;
299
+ let event;
300
+ try {
301
+ event = JSON.parse(line.slice(6));
302
+ } catch {
303
+ continue;
304
+ }
305
+ this.handleEvent(ch.account, event).catch(
306
+ (err) => this.log("error", `[dispatcher] handleEvent threw for "${ch.account.name}": ${err}`)
307
+ );
308
+ }
309
+ }
310
+ }
311
+ }
312
+ /** Acquire a concurrency slot, run a worker, release the slot. */
313
+ async spawnWorker(account, prompt, ctx) {
314
+ await this.acquireSlot();
315
+ try {
316
+ const { body } = loadPersonaForAgent({
317
+ agent: account,
318
+ agentsDir: this.cfg.agentsDir,
319
+ subagentPrefix: this.cfg.subagentPrefix,
320
+ mcpServerName: this.cfg.mcpServerName
321
+ });
322
+ this.log("info", `[dispatcher] waking "${account.name}" \u2014 ${ctx.kind}${ctx.taskId ? " " + ctx.taskId : ctx.uid ? " uid=" + ctx.uid : ""}`);
323
+ const mcpEnv = await this.buildMcpEnv();
324
+ await runWorker(
325
+ this.query,
326
+ body,
327
+ prompt,
328
+ account,
329
+ this.cfg.mcpServerName,
330
+ this.cfg.mcpCommand,
331
+ this.cfg.mcpArgs,
332
+ mcpEnv,
333
+ this.log
334
+ );
335
+ } finally {
336
+ this.releaseSlot();
337
+ }
338
+ }
339
+ /** Build the env block we pass to the worker's MCP server child process. */
340
+ async buildMcpEnv() {
341
+ return {
342
+ AGENTICMAIL_API_URL: this.cfg.apiUrl,
343
+ AGENTICMAIL_MASTER_KEY: this.cfg.masterKey
344
+ // No AGENTICMAIL_API_KEY: workers should ALWAYS pass `_account`
345
+ // explicitly. Omitting the default key forces that discipline at
346
+ // the MCP-server level (any forgotten `_account` becomes a clear
347
+ // error rather than a silent identity drift).
348
+ };
349
+ }
350
+ acquireSlot() {
351
+ if (this.running < this.maxConcurrent) {
352
+ this.running++;
353
+ return Promise.resolve();
354
+ }
355
+ return new Promise((resolve) => {
356
+ this.waiters.push(() => {
357
+ this.running++;
358
+ resolve();
359
+ });
360
+ });
361
+ }
362
+ releaseSlot() {
363
+ this.running--;
364
+ const next = this.waiters.shift();
365
+ if (next) next();
366
+ }
367
+ };
368
+ function sleep(ms) {
369
+ return new Promise((resolve) => setTimeout(resolve, ms));
370
+ }
371
+ function defaultLog(level, msg) {
372
+ const stream = level === "error" ? process.stderr : process.stdout;
373
+ stream.write(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] ${msg}
374
+ `);
375
+ }
376
+ function defaultQuery() {
377
+ return (params) => {
378
+ let inner = null;
379
+ const init = async () => {
380
+ try {
381
+ const mod = await import("@anthropic-ai/claude-agent-sdk");
382
+ return mod.query(params);
383
+ } catch (err) {
384
+ throw new Error(
385
+ `Dispatcher needs @anthropic-ai/claude-agent-sdk installed in the package, but: ${err.message}`
386
+ );
387
+ }
388
+ };
389
+ return {
390
+ [Symbol.asyncIterator]() {
391
+ return {
392
+ async next() {
393
+ if (!inner) inner = await init();
394
+ const it = inner[Symbol.asyncIterator]();
395
+ const self = this;
396
+ self.next = it.next.bind(it);
397
+ return it.next();
398
+ }
399
+ };
400
+ }
401
+ };
402
+ };
403
+ }
404
+
405
+ export {
406
+ loadPersonaForAgent,
407
+ Dispatcher
408
+ };
@@ -0,0 +1,139 @@
1
+ import {
2
+ startDispatcher,
3
+ upsertMcpServer
4
+ } from "./chunk-US5FT2UB.js";
5
+ import {
6
+ MANAGED_BY_MARKER,
7
+ checkApiHealth,
8
+ ensureAccount,
9
+ listAccounts,
10
+ renderSubagentMarkdown,
11
+ resolveConfig
12
+ } from "./chunk-XAW5NUNU.js";
13
+
14
+ // src/install.ts
15
+ import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, unlinkSync } from "fs";
16
+ import { join } from "path";
17
+ import { fileURLToPath } from "url";
18
+ function sanitizeSubagentName(name) {
19
+ return name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
20
+ }
21
+ function buildMcpEntry(cfg, bridgeKey, accountKeys) {
22
+ const env = {
23
+ AGENTICMAIL_API_URL: cfg.apiUrl,
24
+ AGENTICMAIL_API_KEY: bridgeKey,
25
+ AGENTICMAIL_MASTER_KEY: cfg.masterKey
26
+ };
27
+ if (Object.keys(accountKeys).length > 0) {
28
+ env.AGENTICMAIL_ACCOUNT_KEYS_JSON = JSON.stringify(accountKeys);
29
+ }
30
+ return {
31
+ type: "stdio",
32
+ command: cfg.mcpCommand,
33
+ args: cfg.mcpArgs,
34
+ env
35
+ };
36
+ }
37
+ function selectExposableAgents(accounts, cfg) {
38
+ return accounts.filter(
39
+ (a) => a.name.toLowerCase() !== cfg.bridgeAgentName.toLowerCase() && a.role !== "bridge"
40
+ );
41
+ }
42
+ function isOwnedSubagent(filepath) {
43
+ try {
44
+ const head = readFileSync(filepath, "utf-8").slice(0, 1024);
45
+ return head.includes(MANAGED_BY_MARKER);
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+ function writeSubagentFiles(agentsDir, cfg, agents) {
51
+ if (!existsSync(agentsDir)) mkdirSync(agentsDir, { recursive: true });
52
+ const updated = [];
53
+ for (const agent of agents) {
54
+ const baseName = sanitizeSubagentName(`${cfg.subagentPrefix}${agent.name}`);
55
+ const filePath = join(agentsDir, `${baseName}.md`);
56
+ const content = renderSubagentMarkdown({
57
+ name: baseName,
58
+ agent,
59
+ mcpServerName: cfg.mcpServerName
60
+ });
61
+ if (existsSync(filePath)) {
62
+ if (!isOwnedSubagent(filePath)) continue;
63
+ const existing = readFileSync(filePath, "utf-8");
64
+ if (existing === content) continue;
65
+ }
66
+ writeFileSync(filePath, content, "utf-8");
67
+ updated.push(baseName);
68
+ }
69
+ return updated;
70
+ }
71
+ function pruneStaleSubagentFiles(agentsDir, cfg, liveAgentNames) {
72
+ if (!existsSync(agentsDir)) return [];
73
+ const prefix = cfg.subagentPrefix.toLowerCase();
74
+ const removed = [];
75
+ for (const file of readdirSync(agentsDir)) {
76
+ if (!file.endsWith(".md")) continue;
77
+ if (!file.toLowerCase().startsWith(prefix)) continue;
78
+ const full = join(agentsDir, file);
79
+ if (!isOwnedSubagent(full)) continue;
80
+ const stem = file.slice(prefix.length, -3);
81
+ if (liveAgentNames.has(stem.toLowerCase())) continue;
82
+ try {
83
+ unlinkSync(full);
84
+ removed.push(file);
85
+ } catch {
86
+ }
87
+ }
88
+ return removed;
89
+ }
90
+ async function install(opts = {}) {
91
+ const cfg = resolveConfig(opts);
92
+ await checkApiHealth(cfg.apiUrl);
93
+ if (!cfg.masterKey) {
94
+ throw new Error(
95
+ "AgenticMail master key not found. Run `agenticmail setup` first to generate one, or pass { masterKey } to install() explicitly."
96
+ );
97
+ }
98
+ const bridge = await ensureAccount(cfg.apiUrl, cfg.masterKey, cfg.bridgeAgentName, "assistant");
99
+ const everyAccount = await listAccounts(cfg.apiUrl, cfg.masterKey);
100
+ const exposable = selectExposableAgents(everyAccount, cfg);
101
+ const accountKeys = {};
102
+ for (const a of exposable) accountKeys[a.name] = a.apiKey;
103
+ accountKeys[bridge.name] = bridge.apiKey;
104
+ const mcpEntry = buildMcpEntry(cfg, bridge.apiKey, accountKeys);
105
+ const mcpChanged = upsertMcpServer(cfg.claudeConfigPath, cfg.mcpServerName, mcpEntry);
106
+ const updated = writeSubagentFiles(cfg.agentsDir, cfg, exposable);
107
+ const liveNames = new Set(exposable.map((a) => sanitizeSubagentName(a.name)));
108
+ const pruned = pruneStaleSubagentFiles(cfg.agentsDir, cfg, liveNames);
109
+ const dispatcherStatus = await startDispatcherForInstall(cfg);
110
+ return {
111
+ registeredAgents: exposable,
112
+ claudeConfigPath: cfg.claudeConfigPath,
113
+ agentsDir: cfg.agentsDir,
114
+ bridgeAgent: bridge,
115
+ changed: mcpChanged || updated.length > 0 || pruned.length > 0 || dispatcherStatus.started,
116
+ dispatcher: dispatcherStatus
117
+ };
118
+ }
119
+ function resolveDispatcherBinPath() {
120
+ const thisFile = fileURLToPath(import.meta.url);
121
+ const dir = thisFile.slice(0, thisFile.lastIndexOf("/"));
122
+ return `${dir}/dispatcher-bin.js`;
123
+ }
124
+ async function startDispatcherForInstall(cfg) {
125
+ const binPath = resolveDispatcherBinPath();
126
+ return startDispatcher({
127
+ binPath,
128
+ env: {
129
+ AGENTICMAIL_API_URL: cfg.apiUrl,
130
+ AGENTICMAIL_MASTER_KEY: cfg.masterKey,
131
+ CLAUDE_CODE_AGENTS_DIR: cfg.agentsDir
132
+ }
133
+ });
134
+ }
135
+
136
+ export {
137
+ selectExposableAgents,
138
+ install
139
+ };
@@ -0,0 +1,89 @@
1
+ import {
2
+ getDispatcherStatus,
3
+ readClaudeConfig
4
+ } from "./chunk-US5FT2UB.js";
5
+ import {
6
+ AgenticMailApiError,
7
+ MANAGED_BY_MARKER,
8
+ getAccountByName,
9
+ resolveConfig
10
+ } from "./chunk-XAW5NUNU.js";
11
+
12
+ // src/status.ts
13
+ import { existsSync, readFileSync, readdirSync } from "fs";
14
+ import { join } from "path";
15
+ async function status(opts = {}) {
16
+ const cfg = resolveConfig(opts);
17
+ const notes = [];
18
+ let mcpInstalled = false;
19
+ if (existsSync(cfg.claudeConfigPath)) {
20
+ try {
21
+ const claudeCfg = readClaudeConfig(cfg.claudeConfigPath);
22
+ mcpInstalled = Boolean(claudeCfg.mcpServers?.[cfg.mcpServerName]);
23
+ } catch (err) {
24
+ notes.push(`Could not parse ${cfg.claudeConfigPath}: ${err.message}`);
25
+ }
26
+ } else {
27
+ notes.push(`Claude Code config not found at ${cfg.claudeConfigPath} \u2014 Claude Code may not be installed yet.`);
28
+ }
29
+ const subagents = [];
30
+ if (existsSync(cfg.agentsDir)) {
31
+ const prefix = cfg.subagentPrefix.toLowerCase();
32
+ for (const file of readdirSync(cfg.agentsDir)) {
33
+ if (!file.endsWith(".md")) continue;
34
+ if (!file.toLowerCase().startsWith(prefix)) continue;
35
+ const full = join(cfg.agentsDir, file);
36
+ try {
37
+ const head = readFileSync(full, "utf-8").slice(0, 1024);
38
+ if (head.includes(MANAGED_BY_MARKER)) subagents.push(file.slice(0, -3));
39
+ } catch {
40
+ }
41
+ }
42
+ }
43
+ let bridgeAgentExists = false;
44
+ if (cfg.masterKey) {
45
+ try {
46
+ const bridge = await getAccountByName(cfg.apiUrl, cfg.masterKey, cfg.bridgeAgentName);
47
+ bridgeAgentExists = Boolean(bridge);
48
+ if (!bridge && mcpInstalled) {
49
+ notes.push(`MCP server is registered but bridge agent "${cfg.bridgeAgentName}" is missing in AgenticMail \u2014 re-run install to recreate it.`);
50
+ }
51
+ } catch (err) {
52
+ if (err instanceof AgenticMailApiError) {
53
+ notes.push(`Could not reach AgenticMail at ${cfg.apiUrl}: ${err.message}`);
54
+ } else {
55
+ notes.push(err.message);
56
+ }
57
+ }
58
+ } else {
59
+ notes.push("AgenticMail master key not found \u2014 run `agenticmail setup` to initialize.");
60
+ }
61
+ const dispatcherInfo = getDispatcherStatus();
62
+ const dispatcher = dispatcherInfo ? {
63
+ running: dispatcherInfo.status === "online",
64
+ pid: dispatcherInfo.pid,
65
+ restartCount: dispatcherInfo.restartCount,
66
+ uptimeMs: dispatcherInfo.uptime ? Date.now() - dispatcherInfo.uptime : void 0
67
+ } : null;
68
+ if (mcpInstalled && (!dispatcher || !dispatcher.running)) {
69
+ notes.push("Dispatcher daemon is not running \u2014 AgenticMail agents will NOT auto-wake on mail/task events. Re-run `agenticmail claudecode` to (re)start it.");
70
+ }
71
+ let state;
72
+ if (mcpInstalled && bridgeAgentExists && subagents.length > 0) state = "installed";
73
+ else if (!mcpInstalled && !bridgeAgentExists && subagents.length === 0) state = "not_installed";
74
+ else state = "partial";
75
+ return {
76
+ state,
77
+ mcpInstalled,
78
+ bridgeAgentExists,
79
+ subagents,
80
+ claudeConfigPath: cfg.claudeConfigPath,
81
+ agentsDir: cfg.agentsDir,
82
+ notes,
83
+ dispatcher
84
+ };
85
+ }
86
+
87
+ export {
88
+ status
89
+ };
@@ -0,0 +1,65 @@
1
+ import {
2
+ removeMcpServer,
3
+ stopDispatcher
4
+ } from "./chunk-US5FT2UB.js";
5
+ import {
6
+ MANAGED_BY_MARKER,
7
+ deleteAccount,
8
+ getAccountByName,
9
+ resolveConfig
10
+ } from "./chunk-XAW5NUNU.js";
11
+
12
+ // src/uninstall.ts
13
+ import { existsSync, readdirSync, readFileSync, unlinkSync } from "fs";
14
+ import { join } from "path";
15
+ function removeOwnedSubagents(agentsDir, prefix) {
16
+ if (!existsSync(agentsDir)) return [];
17
+ const safePrefix = prefix.toLowerCase();
18
+ const removed = [];
19
+ for (const file of readdirSync(agentsDir)) {
20
+ if (!file.endsWith(".md")) continue;
21
+ if (!file.toLowerCase().startsWith(safePrefix)) continue;
22
+ const full = join(agentsDir, file);
23
+ let head;
24
+ try {
25
+ head = readFileSync(full, "utf-8").slice(0, 1024);
26
+ } catch {
27
+ continue;
28
+ }
29
+ if (!head.includes(MANAGED_BY_MARKER)) continue;
30
+ try {
31
+ unlinkSync(full);
32
+ removed.push(file);
33
+ } catch {
34
+ }
35
+ }
36
+ return removed;
37
+ }
38
+ async function uninstall(opts = {}) {
39
+ const cfg = resolveConfig(opts);
40
+ const mcpBlockRemoved = removeMcpServer(cfg.claudeConfigPath, cfg.mcpServerName);
41
+ const removedSubagents = removeOwnedSubagents(cfg.agentsDir, cfg.subagentPrefix);
42
+ const dispatcherStopped = stopDispatcher().stopped;
43
+ let bridgeAgentDeleted = false;
44
+ if (opts.purgeBridgeAgent && cfg.masterKey) {
45
+ try {
46
+ const bridge = await getAccountByName(cfg.apiUrl, cfg.masterKey, cfg.bridgeAgentName);
47
+ if (bridge) {
48
+ await deleteAccount(cfg.apiUrl, cfg.masterKey, bridge.id);
49
+ bridgeAgentDeleted = true;
50
+ }
51
+ } catch {
52
+ }
53
+ }
54
+ return {
55
+ changed: mcpBlockRemoved || removedSubagents.length > 0 || bridgeAgentDeleted || dispatcherStopped,
56
+ removedSubagents,
57
+ mcpBlockRemoved,
58
+ bridgeAgentDeleted,
59
+ dispatcherStopped
60
+ };
61
+ }
62
+
63
+ export {
64
+ uninstall
65
+ };