@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.
- package/LICENSE +21 -0
- package/README.md +299 -0
- package/claudecode.plugin.json +73 -0
- package/dist/chunk-563BZ447.js +408 -0
- package/dist/chunk-P2DXF7DO.js +139 -0
- package/dist/chunk-RI4USTMC.js +89 -0
- package/dist/chunk-ULKJ773Y.js +65 -0
- package/dist/chunk-UPA2YLSM.js +91 -0
- package/dist/chunk-US5FT2UB.js +151 -0
- package/dist/chunk-XAW5NUNU.js +269 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +174 -0
- package/dist/config-BegnlyPD.d.ts +122 -0
- package/dist/dispatcher-bin.d.ts +1 -0
- package/dist/dispatcher-bin.js +40 -0
- package/dist/dispatcher.d.ts +116 -0
- package/dist/dispatcher.js +7 -0
- package/dist/http-routes.d.ts +36 -0
- package/dist/http-routes.js +11 -0
- package/dist/index.d.ts +176 -0
- package/dist/index.js +47 -0
- package/dist/install.d.ts +47 -0
- package/dist/install.js +10 -0
- package/dist/status.d.ts +18 -0
- package/dist/status.js +8 -0
- package/dist/uninstall.d.ts +25 -0
- package/dist/uninstall.js +8 -0
- package/package.json +97 -0
- package/scripts/uninstall.mjs +78 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
install
|
|
3
|
+
} from "./chunk-P2DXF7DO.js";
|
|
4
|
+
import {
|
|
5
|
+
status
|
|
6
|
+
} from "./chunk-RI4USTMC.js";
|
|
7
|
+
import {
|
|
8
|
+
uninstall
|
|
9
|
+
} from "./chunk-ULKJ773Y.js";
|
|
10
|
+
import {
|
|
11
|
+
AgenticMailApiError
|
|
12
|
+
} from "./chunk-XAW5NUNU.js";
|
|
13
|
+
|
|
14
|
+
// src/http-routes.ts
|
|
15
|
+
import { Router } from "express";
|
|
16
|
+
function sanitizeInstallBody(body) {
|
|
17
|
+
if (!body || typeof body !== "object") return {};
|
|
18
|
+
const b = body;
|
|
19
|
+
const out = {};
|
|
20
|
+
if (typeof b.apiUrl === "string") out.apiUrl = b.apiUrl;
|
|
21
|
+
if (typeof b.masterKey === "string") out.masterKey = b.masterKey;
|
|
22
|
+
if (typeof b.claudeConfigPath === "string") out.claudeConfigPath = b.claudeConfigPath;
|
|
23
|
+
if (typeof b.agentsDir === "string") out.agentsDir = b.agentsDir;
|
|
24
|
+
if (typeof b.mcpServerName === "string") out.mcpServerName = b.mcpServerName;
|
|
25
|
+
if (typeof b.bridgeAgentName === "string") out.bridgeAgentName = b.bridgeAgentName;
|
|
26
|
+
if (typeof b.subagentPrefix === "string") out.subagentPrefix = b.subagentPrefix;
|
|
27
|
+
if (typeof b.mcpCommand === "string") out.mcpCommand = b.mcpCommand;
|
|
28
|
+
if (Array.isArray(b.mcpArgs) && b.mcpArgs.every((a) => typeof a === "string")) {
|
|
29
|
+
out.mcpArgs = b.mcpArgs;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function sanitizeUninstallBody(body) {
|
|
34
|
+
const base = sanitizeInstallBody(body);
|
|
35
|
+
if (body && typeof body === "object" && body.purgeBridgeAgent === true) {
|
|
36
|
+
base.purgeBridgeAgent = true;
|
|
37
|
+
}
|
|
38
|
+
return base;
|
|
39
|
+
}
|
|
40
|
+
function handleError(err, res) {
|
|
41
|
+
if (err instanceof AgenticMailApiError) {
|
|
42
|
+
const code = err.status === 0 ? 503 : err.status;
|
|
43
|
+
res.status(code).json({ error: err.message });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
47
|
+
res.status(500).json({ error: msg });
|
|
48
|
+
}
|
|
49
|
+
function createIntegrationRoutes() {
|
|
50
|
+
const router = Router();
|
|
51
|
+
router.get("/integrations/claudecode/status", async (_req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
const result = await status();
|
|
54
|
+
res.json(result);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
handleError(err, res);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
router.post("/integrations/claudecode/install", async (req, res) => {
|
|
60
|
+
try {
|
|
61
|
+
const body = sanitizeInstallBody(req.body);
|
|
62
|
+
const result = await install(body);
|
|
63
|
+
const safeRegisteredAgents = result.registeredAgents.map((a) => ({
|
|
64
|
+
...a,
|
|
65
|
+
apiKey: "***redacted***"
|
|
66
|
+
}));
|
|
67
|
+
const safeBridge = { ...result.bridgeAgent, apiKey: "***redacted***" };
|
|
68
|
+
res.status(200).json({
|
|
69
|
+
...result,
|
|
70
|
+
registeredAgents: safeRegisteredAgents,
|
|
71
|
+
bridgeAgent: safeBridge
|
|
72
|
+
});
|
|
73
|
+
} catch (err) {
|
|
74
|
+
handleError(err, res);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
router.post("/integrations/claudecode/uninstall", async (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
const body = sanitizeUninstallBody(req.body);
|
|
80
|
+
const result = await uninstall(body);
|
|
81
|
+
res.status(200).json(result);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
handleError(err, res);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return router;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
createIntegrationRoutes
|
|
91
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// src/claude-config.ts
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "fs";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
function readClaudeConfig(path) {
|
|
5
|
+
if (!existsSync(path)) return {};
|
|
6
|
+
const raw = readFileSync(path, "utf-8");
|
|
7
|
+
if (!raw.trim()) return {};
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(raw);
|
|
10
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
11
|
+
return {};
|
|
12
|
+
} catch (err) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Could not parse Claude Code config at ${path}: ${err.message}. Refusing to overwrite \u2014 please fix the file by hand and retry.`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function writeClaudeConfig(path, config) {
|
|
19
|
+
const dir = dirname(path);
|
|
20
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
21
|
+
const text = JSON.stringify(config, null, 2) + "\n";
|
|
22
|
+
const tmp = `${path}.agenticmail-tmp`;
|
|
23
|
+
writeFileSync(tmp, text, "utf-8");
|
|
24
|
+
renameSync(tmp, path);
|
|
25
|
+
}
|
|
26
|
+
function upsertMcpServer(path, serverName, entry) {
|
|
27
|
+
const config = readClaudeConfig(path);
|
|
28
|
+
const servers = config.mcpServers ?? {};
|
|
29
|
+
const existing = servers[serverName];
|
|
30
|
+
if (existing && deepEqual(existing, entry)) return false;
|
|
31
|
+
servers[serverName] = entry;
|
|
32
|
+
config.mcpServers = servers;
|
|
33
|
+
writeClaudeConfig(path, config);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
function removeMcpServer(path, serverName) {
|
|
37
|
+
if (!existsSync(path)) return false;
|
|
38
|
+
const config = readClaudeConfig(path);
|
|
39
|
+
if (!config.mcpServers || !(serverName in config.mcpServers)) return false;
|
|
40
|
+
delete config.mcpServers[serverName];
|
|
41
|
+
writeClaudeConfig(path, config);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
function deepEqual(a, b) {
|
|
45
|
+
if (a === b) return true;
|
|
46
|
+
if (typeof a !== typeof b) return false;
|
|
47
|
+
if (a && b && typeof a === "object") {
|
|
48
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
49
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
50
|
+
if (a.length !== b.length) return false;
|
|
51
|
+
for (let i = 0; i < a.length; i++) {
|
|
52
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
const ao = a;
|
|
57
|
+
const bo = b;
|
|
58
|
+
const ak = Object.keys(ao).sort();
|
|
59
|
+
const bk = Object.keys(bo).sort();
|
|
60
|
+
if (ak.length !== bk.length) return false;
|
|
61
|
+
for (let i = 0; i < ak.length; i++) {
|
|
62
|
+
if (ak[i] !== bk[i]) return false;
|
|
63
|
+
if (!deepEqual(ao[ak[i]], bo[bk[i]])) return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/pm2.ts
|
|
71
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
72
|
+
import { existsSync as existsSync2 } from "fs";
|
|
73
|
+
var DISPATCHER_PM2_NAME = "agenticmail-claudecode-dispatcher";
|
|
74
|
+
function pm2Available() {
|
|
75
|
+
const r = spawnSync("pm2", ["--version"], { stdio: "ignore" });
|
|
76
|
+
return r.status === 0;
|
|
77
|
+
}
|
|
78
|
+
function getDispatcherStatus() {
|
|
79
|
+
if (!pm2Available()) return null;
|
|
80
|
+
let raw;
|
|
81
|
+
try {
|
|
82
|
+
raw = execFileSync("pm2", ["jlist"], { encoding: "utf-8" });
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
let list;
|
|
87
|
+
try {
|
|
88
|
+
list = JSON.parse(raw);
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
for (const proc of list) {
|
|
93
|
+
if (proc.name !== DISPATCHER_PM2_NAME) continue;
|
|
94
|
+
const pm2_env = proc.pm2_env ?? {};
|
|
95
|
+
const monit = proc.monit ?? {};
|
|
96
|
+
return {
|
|
97
|
+
name: DISPATCHER_PM2_NAME,
|
|
98
|
+
pid: typeof proc.pid === "number" ? proc.pid : 0,
|
|
99
|
+
status: typeof pm2_env.status === "string" ? pm2_env.status : "unknown",
|
|
100
|
+
restartCount: typeof pm2_env.restart_time === "number" ? pm2_env.restart_time : 0,
|
|
101
|
+
uptime: typeof pm2_env.pm_uptime === "number" ? pm2_env.pm_uptime : 0,
|
|
102
|
+
// monit fields (cpu, memory) are available if we ever want them.
|
|
103
|
+
...monit ? {} : {}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
function startDispatcher(opts) {
|
|
109
|
+
if (!pm2Available()) return { started: false, reason: "pm2 is not installed (npm install -g pm2)" };
|
|
110
|
+
if (!existsSync2(opts.binPath)) return { started: false, reason: `dispatcher bin not found at ${opts.binPath}` };
|
|
111
|
+
const existing = getDispatcherStatus();
|
|
112
|
+
if (existing) {
|
|
113
|
+
spawnSync("pm2", ["delete", DISPATCHER_PM2_NAME], { stdio: "ignore" });
|
|
114
|
+
}
|
|
115
|
+
const r = spawnSync("pm2", [
|
|
116
|
+
"start",
|
|
117
|
+
opts.binPath,
|
|
118
|
+
"--name",
|
|
119
|
+
DISPATCHER_PM2_NAME,
|
|
120
|
+
// Restart with exponential backoff up to 10 retries before giving up.
|
|
121
|
+
// The dispatcher's internal SSE reconnect logic handles transient
|
|
122
|
+
// network errors; pm2 catches process-level crashes.
|
|
123
|
+
"--max-restarts",
|
|
124
|
+
"10",
|
|
125
|
+
"--restart-delay",
|
|
126
|
+
"2000",
|
|
127
|
+
"--update-env"
|
|
128
|
+
], {
|
|
129
|
+
env: { ...process.env, ...opts.env },
|
|
130
|
+
stdio: "inherit"
|
|
131
|
+
});
|
|
132
|
+
if (r.status !== 0) return { started: false, reason: `pm2 start exited ${r.status}` };
|
|
133
|
+
spawnSync("pm2", ["save"], { stdio: "ignore" });
|
|
134
|
+
return { started: true };
|
|
135
|
+
}
|
|
136
|
+
function stopDispatcher() {
|
|
137
|
+
if (!pm2Available()) return { stopped: false };
|
|
138
|
+
if (!getDispatcherStatus()) return { stopped: false };
|
|
139
|
+
spawnSync("pm2", ["delete", DISPATCHER_PM2_NAME], { stdio: "ignore" });
|
|
140
|
+
spawnSync("pm2", ["save"], { stdio: "ignore" });
|
|
141
|
+
return { stopped: true };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export {
|
|
145
|
+
readClaudeConfig,
|
|
146
|
+
upsertMcpServer,
|
|
147
|
+
removeMcpServer,
|
|
148
|
+
getDispatcherStatus,
|
|
149
|
+
startDispatcher,
|
|
150
|
+
stopDispatcher
|
|
151
|
+
};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// src/api.ts
|
|
2
|
+
var AgenticMailApiError = class extends Error {
|
|
3
|
+
constructor(status, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.name = "AgenticMailApiError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
async function request(apiUrl, masterKey, path, opts = {}) {
|
|
10
|
+
if (!masterKey) {
|
|
11
|
+
throw new AgenticMailApiError(0, "AgenticMail master key is required \u2014 could not find one in ~/.agenticmail/config.json.");
|
|
12
|
+
}
|
|
13
|
+
const url = `${apiUrl.replace(/\/$/, "")}/api/agenticmail${path}`;
|
|
14
|
+
const headers = { "Authorization": `Bearer ${masterKey}` };
|
|
15
|
+
if (opts.body !== void 0) headers["Content-Type"] = "application/json";
|
|
16
|
+
let res;
|
|
17
|
+
try {
|
|
18
|
+
res = await fetch(url, {
|
|
19
|
+
method: opts.method ?? "GET",
|
|
20
|
+
headers,
|
|
21
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
22
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 1e4)
|
|
23
|
+
});
|
|
24
|
+
} catch (err) {
|
|
25
|
+
throw new AgenticMailApiError(0, `AgenticMail API unreachable at ${apiUrl}: ${err.message}`);
|
|
26
|
+
}
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
let text;
|
|
29
|
+
try {
|
|
30
|
+
text = await res.text();
|
|
31
|
+
} catch {
|
|
32
|
+
text = "(could not read response body)";
|
|
33
|
+
}
|
|
34
|
+
throw new AgenticMailApiError(res.status, `AgenticMail API ${res.status}: ${text.slice(0, 300)}`);
|
|
35
|
+
}
|
|
36
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
37
|
+
if (!contentType.includes("application/json")) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return await res.json();
|
|
41
|
+
}
|
|
42
|
+
async function checkApiHealth(apiUrl) {
|
|
43
|
+
const url = `${apiUrl.replace(/\/$/, "")}/api/agenticmail/health`;
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
|
|
46
|
+
if (!res.ok) throw new AgenticMailApiError(res.status, `Health check returned HTTP ${res.status}`);
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
return { ok: true, version: data?.version };
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err instanceof AgenticMailApiError) throw err;
|
|
51
|
+
throw new AgenticMailApiError(0, `AgenticMail API unreachable at ${apiUrl}: ${err.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function listAccounts(apiUrl, masterKey) {
|
|
55
|
+
const data = await request(apiUrl, masterKey, "/accounts");
|
|
56
|
+
return data?.agents ?? [];
|
|
57
|
+
}
|
|
58
|
+
async function getAccountByName(apiUrl, masterKey, name) {
|
|
59
|
+
const all = await listAccounts(apiUrl, masterKey);
|
|
60
|
+
return all.find((a) => a.name.toLowerCase() === name.toLowerCase()) ?? null;
|
|
61
|
+
}
|
|
62
|
+
async function ensureAccount(apiUrl, masterKey, name, role = "assistant") {
|
|
63
|
+
const existing = await getAccountByName(apiUrl, masterKey, name);
|
|
64
|
+
if (existing) return existing;
|
|
65
|
+
try {
|
|
66
|
+
const created = await request(apiUrl, masterKey, "/accounts", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
body: { name, role }
|
|
69
|
+
});
|
|
70
|
+
if (!created || typeof created.apiKey !== "string") {
|
|
71
|
+
throw new AgenticMailApiError(0, "Account creation returned no apiKey \u2014 refusing to continue.");
|
|
72
|
+
}
|
|
73
|
+
return created;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err instanceof AgenticMailApiError && (err.status === 409 || /UNIQUE|exists/i.test(err.message))) {
|
|
76
|
+
const after = await getAccountByName(apiUrl, masterKey, name);
|
|
77
|
+
if (after) return after;
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function deleteAccount(apiUrl, masterKey, id) {
|
|
83
|
+
await request(apiUrl, masterKey, `/accounts/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/config.ts
|
|
87
|
+
import { readFileSync, existsSync } from "fs";
|
|
88
|
+
import { join } from "path";
|
|
89
|
+
import { homedir } from "os";
|
|
90
|
+
var AGENTICMAIL_CONFIG_PATH = join(homedir(), ".agenticmail", "config.json");
|
|
91
|
+
function readAgenticMailConfig(path) {
|
|
92
|
+
if (!existsSync(path)) return {};
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
95
|
+
} catch {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function defaultMcpInvocation() {
|
|
100
|
+
return { command: "npx", args: ["-y", "@agenticmail/mcp"] };
|
|
101
|
+
}
|
|
102
|
+
function resolveConfig(opts = {}) {
|
|
103
|
+
const amConfigPath = opts.agenticmailConfigPath ?? AGENTICMAIL_CONFIG_PATH;
|
|
104
|
+
const onDisk = readAgenticMailConfig(amConfigPath);
|
|
105
|
+
const apiHost = onDisk.api?.host ?? "127.0.0.1";
|
|
106
|
+
const apiPort = onDisk.api?.port ?? 3200;
|
|
107
|
+
const defaultApiUrl = `http://${apiHost}:${apiPort}`;
|
|
108
|
+
const defaultInvocation = defaultMcpInvocation();
|
|
109
|
+
const masterKey = opts.masterKey ?? onDisk.masterKey ?? "";
|
|
110
|
+
return {
|
|
111
|
+
apiUrl: opts.apiUrl ?? defaultApiUrl,
|
|
112
|
+
masterKey,
|
|
113
|
+
claudeConfigPath: opts.claudeConfigPath ?? join(homedir(), ".claude.json"),
|
|
114
|
+
agentsDir: opts.agentsDir ?? join(homedir(), ".claude", "agents"),
|
|
115
|
+
mcpServerName: opts.mcpServerName ?? "agenticmail",
|
|
116
|
+
bridgeAgentName: opts.bridgeAgentName ?? "claudecode",
|
|
117
|
+
subagentPrefix: opts.subagentPrefix ?? "agenticmail-",
|
|
118
|
+
mcpCommand: opts.mcpCommand ?? defaultInvocation.command,
|
|
119
|
+
mcpArgs: opts.mcpArgs ?? defaultInvocation.args
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/subagent-template.ts
|
|
124
|
+
var ESSENTIAL_TOOL_NAMES = [
|
|
125
|
+
"whoami",
|
|
126
|
+
"list_inbox",
|
|
127
|
+
"read_email",
|
|
128
|
+
"send_email",
|
|
129
|
+
"reply_email",
|
|
130
|
+
"search_emails",
|
|
131
|
+
"list_agents",
|
|
132
|
+
"message_agent",
|
|
133
|
+
// call_agent is the synchronous RPC primitive — fire a task at another
|
|
134
|
+
// AgenticMail agent and get back a structured result. It is the reason
|
|
135
|
+
// multi-agent setups work, so it MUST be pre-loaded; making subagents
|
|
136
|
+
// call request_tools just to discover it would be a usability disaster
|
|
137
|
+
// for the most common coordination pattern.
|
|
138
|
+
"call_agent",
|
|
139
|
+
"check_tasks",
|
|
140
|
+
// Meta-tools — these unlock the other ~50 tools on demand.
|
|
141
|
+
"request_tools",
|
|
142
|
+
"invoke"
|
|
143
|
+
];
|
|
144
|
+
var MANAGED_BY_MARKER = "@agenticmail/claudecode";
|
|
145
|
+
function yamlQuote(s) {
|
|
146
|
+
const cleaned = s.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim();
|
|
147
|
+
return `"${cleaned.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
148
|
+
}
|
|
149
|
+
function describeAgent(agent) {
|
|
150
|
+
const role = (agent.role ?? "").trim();
|
|
151
|
+
const owner = typeof agent.metadata?.ownerName === "string" ? agent.metadata.ownerName : "";
|
|
152
|
+
const parts = [];
|
|
153
|
+
parts.push(`AgenticMail agent "${agent.name}" (${agent.email})`);
|
|
154
|
+
if (role && role !== "assistant") parts.push(`role: ${role}`);
|
|
155
|
+
if (owner) parts.push(`owner: ${owner}`);
|
|
156
|
+
parts.push("use for anything that involves reading/writing this agent's email, sending mail from their address, managing their tasks, contacts, signatures, or SMS");
|
|
157
|
+
return parts.join("; ");
|
|
158
|
+
}
|
|
159
|
+
function renderPersonaBody(input) {
|
|
160
|
+
const { agent, mcpServerName } = input;
|
|
161
|
+
const tool = (n) => `mcp__${mcpServerName}__${n}`;
|
|
162
|
+
const roleLine = agent.role && agent.role !== "assistant" ? `Your role: ${agent.role}.` : "";
|
|
163
|
+
const ownerLine = typeof agent.metadata?.ownerName === "string" ? `Your owner is ${agent.metadata.ownerName}. You serve at their direction; treat their instructions as authoritative within the bounds of your role.` : "";
|
|
164
|
+
return [
|
|
165
|
+
`# You are ${agent.name}`,
|
|
166
|
+
"",
|
|
167
|
+
`You are **${agent.name}**, an AgenticMail agent. Your email address is \`${agent.email}\`. ${roleLine}`,
|
|
168
|
+
ownerLine,
|
|
169
|
+
"",
|
|
170
|
+
`You do not have your own connection to Anthropic. You are running inside a Claude Code session \u2014 Claude Code is your brain. Every reasoning step, every tool call, every reply you compose flows through Claude Code's authentication. That is intentional: it is how the AgenticMail \u2194 Claude Code integration works.`,
|
|
171
|
+
"",
|
|
172
|
+
"## Identity",
|
|
173
|
+
"",
|
|
174
|
+
`- **Name:** ${agent.name}`,
|
|
175
|
+
`- **Email:** \`${agent.email}\``,
|
|
176
|
+
agent.role ? `- **Role:** ${agent.role}` : "",
|
|
177
|
+
`- **Agent ID:** \`${agent.id}\``,
|
|
178
|
+
"",
|
|
179
|
+
"## Operating instructions",
|
|
180
|
+
"",
|
|
181
|
+
`You start every session with a small **pre-loaded** set of MCP tools (the ones listed in your frontmatter). Everything else AgenticMail offers \u2014 signatures, drafts, templates, SMS, bulk mail ops, folders, scheduling, spam tools, setup wizards, account admin \u2014 is reachable through the two **meta-tools** that are always pre-loaded:`,
|
|
182
|
+
"",
|
|
183
|
+
`- \`${tool("request_tools")}\` \u2014 Returns a text catalogue of unloaded tools. Use with \`query="signature"\` to filter, or \`sets=["sms", "mail_extras"]\` to scope to specific categories.`,
|
|
184
|
+
`- \`${tool("invoke")}\` \u2014 Calls any AgenticMail tool by name with structured args. Example: \`${tool("invoke")}({ tool: "manage_signatures", args: { action: "create", name: "default", body: "\u2014\\n${agent.name}" }, _account: "${agent.name}" })\`.`,
|
|
185
|
+
"",
|
|
186
|
+
`**On EVERY tool call you make \u2014 pre-loaded OR via \`invoke\` \u2014 you MUST pass \`_account: "${agent.name}"\`.** This tells the MCP server to authenticate as you, not as the integration's bridge identity. Without it, you'd be reading the bridge's empty inbox instead of your own, sending mail from the wrong address, and bypassing your owner's expectation that the agent named "${agent.name}" did the work.`,
|
|
187
|
+
"",
|
|
188
|
+
`Pre-loaded examples:`,
|
|
189
|
+
"",
|
|
190
|
+
"```",
|
|
191
|
+
`${tool("list_inbox")}({ _account: "${agent.name}", limit: 10 })`,
|
|
192
|
+
`${tool("read_email")}({ _account: "${agent.name}", uid: 42 })`,
|
|
193
|
+
`${tool("send_email")}({ _account: "${agent.name}", to: "...", subject: "...", text: "..." })`,
|
|
194
|
+
`${tool("reply_email")}({ _account: "${agent.name}", uid: 42, text: "..." })`,
|
|
195
|
+
`${tool("search_emails")}({ _account: "${agent.name}", from: "boss@..." })`,
|
|
196
|
+
`${tool("list_agents")}({ _account: "${agent.name}" })`,
|
|
197
|
+
`${tool("message_agent")}({ _account: "${agent.name}", agent: "researcher", message: "...fire-and-forget..." })`,
|
|
198
|
+
`// call_agent = SYNCHRONOUS RPC \u2014 sends a task, waits for the agent to do the work, returns the result.`,
|
|
199
|
+
`${tool("call_agent")}({ _account: "${agent.name}", target: "researcher", task: "Summarise the latest emails from accounting", timeout: 240 })`,
|
|
200
|
+
`${tool("check_tasks")}({ _account: "${agent.name}" })`,
|
|
201
|
+
`${tool("whoami")}({ _account: "${agent.name}" })`,
|
|
202
|
+
"```",
|
|
203
|
+
"",
|
|
204
|
+
`**Coordination tip:** When you need another agent to *do work and report back*, prefer \`${tool("call_agent")}\` over \`${tool("message_agent")}\`. message_agent just delivers an email and returns immediately; call_agent runs the AgenticMail RPC pipeline \u2014 the target agent gets the task, processes it, and the structured result flows back into your call. That is the entire reason this platform has multiple agents.`,
|
|
205
|
+
"",
|
|
206
|
+
`On-demand (via invoke) examples \u2014 anything NOT in the pre-loaded list:`,
|
|
207
|
+
"",
|
|
208
|
+
"```",
|
|
209
|
+
`${tool("request_tools")}({ query: "signature" }) // discover signature-related tools`,
|
|
210
|
+
`${tool("invoke")}({ tool: "manage_signatures", args: { action: "list" }, _account: "${agent.name}" })`,
|
|
211
|
+
`${tool("invoke")}({ tool: "sms_send", args: { to: "+1...", body: "..." }, _account: "${agent.name}" })`,
|
|
212
|
+
`${tool("invoke")}({ tool: "forward_email", args: { uid: 42, to: "boss@..." }, _account: "${agent.name}" })`,
|
|
213
|
+
`${tool("invoke")}({ tool: "manage_drafts", args: { action: "save", to: "...", subject: "...", body: "..." }, _account: "${agent.name}" })`,
|
|
214
|
+
"```",
|
|
215
|
+
"",
|
|
216
|
+
`Forgetting \`_account\` is the single most common mistake. If you ever get back "looks empty" or "no messages" when you know you have email \u2014 check that you passed \`_account: "${agent.name}"\`.`,
|
|
217
|
+
"",
|
|
218
|
+
"## What you can do",
|
|
219
|
+
"",
|
|
220
|
+
`Anything an AgenticMail account can do. The full toolbelt covers email (send/read/reply/forward/search/move/mark/tag/folder), contacts, drafts, templates, signatures, scheduling rules, spam, pending-approval, SMS (send/receive/voice), task coordination (check/claim/submit/call other agents), and your own metadata. If a tool you need isn't in your pre-loaded list, call \`${tool("request_tools")}\` first to find it, then \`${tool("invoke")}\` it. Never ask for permission to use a tool \u2014 just use it.`,
|
|
221
|
+
"",
|
|
222
|
+
"## Hard rules",
|
|
223
|
+
"",
|
|
224
|
+
`- **Always pass \`_account: "${agent.name}"\`** on every \`${tool("*")}\` call.`,
|
|
225
|
+
`- **Do NOT use generic Claude Code tools** (Read, Edit, Write, Bash, Glob, Grep, WebFetch, etc.). You are operating an email account, not a developer environment. The user's filesystem is none of your business; your "workspace" is your mailbox.`,
|
|
226
|
+
`- **Do not invent email content.** If you didn't read a real message, do not summarise one. If you don't know the answer, check your inbox / contacts / tasks first.`,
|
|
227
|
+
`- **Do not impersonate other agents.** You are ${agent.name}, and only ${agent.name}. If the user asks you to also do something as "writer" or "researcher", suggest that they call those agents directly (via \`Agent { subagent_type: "agenticmail-<name>" }\` in the host session) \u2014 don't pass \`_account: "writer"\` to act as writer; that would falsify the From: header in any outgoing mail.`,
|
|
228
|
+
`- **Respect outbound guard.** If a send is blocked by the AgenticMail outbound guard, tell the user in plain English \u2014 recipient, subject, the specific warnings \u2014 and ask them to approve. Do NOT rewrite the email to evade detection.`,
|
|
229
|
+
"",
|
|
230
|
+
"## Output style",
|
|
231
|
+
"",
|
|
232
|
+
`Reply as ${agent.name} would. The user invoked you specifically (not the host Claude Code session) because they want ${agent.name}'s voice and judgement. Be direct, useful, and on-character for your role. The host session will see your final response verbatim \u2014 keep it focused on what the user asked.`,
|
|
233
|
+
""
|
|
234
|
+
].filter((line) => line !== void 0).join("\n");
|
|
235
|
+
}
|
|
236
|
+
function renderSubagentMarkdown(input) {
|
|
237
|
+
const { name, agent, mcpServerName } = input;
|
|
238
|
+
const tool = (n) => `mcp__${mcpServerName}__${n}`;
|
|
239
|
+
const description = describeAgent(agent);
|
|
240
|
+
const allowedTools = ESSENTIAL_TOOL_NAMES.map((n) => tool(n)).join(", ");
|
|
241
|
+
const frontmatter = [
|
|
242
|
+
"---",
|
|
243
|
+
`name: ${name}`,
|
|
244
|
+
`description: ${yamlQuote(description)}`,
|
|
245
|
+
`tools: ${allowedTools}`,
|
|
246
|
+
`model: inherit`,
|
|
247
|
+
`# managed-by: ${MANAGED_BY_MARKER}`,
|
|
248
|
+
`# agenticmail-agent-id: ${agent.id}`,
|
|
249
|
+
`# agenticmail-agent-name: ${agent.name}`,
|
|
250
|
+
`# agenticmail-agent-email: ${agent.email}`,
|
|
251
|
+
"---"
|
|
252
|
+
].join("\n");
|
|
253
|
+
return `${frontmatter}
|
|
254
|
+
|
|
255
|
+
${renderPersonaBody(input)}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export {
|
|
259
|
+
AgenticMailApiError,
|
|
260
|
+
checkApiHealth,
|
|
261
|
+
listAccounts,
|
|
262
|
+
getAccountByName,
|
|
263
|
+
ensureAccount,
|
|
264
|
+
deleteAccount,
|
|
265
|
+
resolveConfig,
|
|
266
|
+
MANAGED_BY_MARKER,
|
|
267
|
+
renderPersonaBody,
|
|
268
|
+
renderSubagentMarkdown
|
|
269
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
install
|
|
4
|
+
} from "./chunk-P2DXF7DO.js";
|
|
5
|
+
import {
|
|
6
|
+
status
|
|
7
|
+
} from "./chunk-RI4USTMC.js";
|
|
8
|
+
import {
|
|
9
|
+
uninstall
|
|
10
|
+
} from "./chunk-ULKJ773Y.js";
|
|
11
|
+
import "./chunk-US5FT2UB.js";
|
|
12
|
+
import {
|
|
13
|
+
AgenticMailApiError
|
|
14
|
+
} from "./chunk-XAW5NUNU.js";
|
|
15
|
+
|
|
16
|
+
// src/cli.ts
|
|
17
|
+
var GREEN = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
18
|
+
var RED = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
19
|
+
var DIM = (s) => `\x1B[90m${s}\x1B[0m`;
|
|
20
|
+
var BOLD = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
21
|
+
var PINK = (s) => `\x1B[38;5;205m${s}\x1B[0m`;
|
|
22
|
+
function print(msg) {
|
|
23
|
+
console.log(msg);
|
|
24
|
+
}
|
|
25
|
+
function ok(msg) {
|
|
26
|
+
console.log(` ${GREEN("\u2713")} ${msg}`);
|
|
27
|
+
}
|
|
28
|
+
function fail(msg) {
|
|
29
|
+
console.error(` ${RED("\u2717")} ${msg}`);
|
|
30
|
+
}
|
|
31
|
+
function dim(msg) {
|
|
32
|
+
console.log(` ${DIM(msg)}`);
|
|
33
|
+
}
|
|
34
|
+
function usage() {
|
|
35
|
+
print("");
|
|
36
|
+
print(` ${PINK("\u{1F380} AgenticMail for Claude Code")}`);
|
|
37
|
+
print("");
|
|
38
|
+
print(` ${BOLD("Usage:")} agenticmail-claudecode [command] [flags]`);
|
|
39
|
+
print("");
|
|
40
|
+
print(` ${BOLD("Commands:")}`);
|
|
41
|
+
print(` install Register AgenticMail with Claude Code (default)`);
|
|
42
|
+
print(` uninstall Remove the registration`);
|
|
43
|
+
print(` status Show what's currently installed`);
|
|
44
|
+
print("");
|
|
45
|
+
print(` ${BOLD("Flags:")}`);
|
|
46
|
+
print(` --json (status) Emit machine-readable JSON instead of prose`);
|
|
47
|
+
print(` --purge-bridge (uninstall) Also delete the AgenticMail bridge agent`);
|
|
48
|
+
print(` -h, --help Show this help and exit`);
|
|
49
|
+
print("");
|
|
50
|
+
print(` ${BOLD("Environment overrides:")}`);
|
|
51
|
+
print(` AGENTICMAIL_API_URL Override AgenticMail master API URL`);
|
|
52
|
+
print(` AGENTICMAIL_MASTER_KEY Override master key (otherwise read from ~/.agenticmail/config.json)`);
|
|
53
|
+
print(` CLAUDE_CODE_CONFIG_PATH Override Claude Code config path (default ~/.claude.json)`);
|
|
54
|
+
print(` CLAUDE_CODE_AGENTS_DIR Override Claude Code agents dir (default ~/.claude/agents)`);
|
|
55
|
+
print("");
|
|
56
|
+
}
|
|
57
|
+
function envOptions() {
|
|
58
|
+
return {
|
|
59
|
+
apiUrl: process.env.AGENTICMAIL_API_URL,
|
|
60
|
+
masterKey: process.env.AGENTICMAIL_MASTER_KEY,
|
|
61
|
+
claudeConfigPath: process.env.CLAUDE_CODE_CONFIG_PATH,
|
|
62
|
+
agentsDir: process.env.CLAUDE_CODE_AGENTS_DIR
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function clean(o) {
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const [k, v] of Object.entries(o)) {
|
|
68
|
+
if (v !== void 0) out[k] = v;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
async function runInstall() {
|
|
73
|
+
print("");
|
|
74
|
+
print(` ${PINK("\u{1F380} Installing AgenticMail for Claude Code")}`);
|
|
75
|
+
print("");
|
|
76
|
+
try {
|
|
77
|
+
const result = await install(clean(envOptions()));
|
|
78
|
+
ok(`Bridge agent: ${result.bridgeAgent.name} (${result.bridgeAgent.email})`);
|
|
79
|
+
ok(`MCP server registered in ${result.claudeConfigPath}`);
|
|
80
|
+
ok(`${result.registeredAgents.length} Claude Code subagent${result.registeredAgents.length === 1 ? "" : "s"} written to ${result.agentsDir}`);
|
|
81
|
+
if (result.registeredAgents.length > 0) {
|
|
82
|
+
for (const a of result.registeredAgents) dim(` \u2022 agenticmail-${a.name.toLowerCase()} \u2192 ${a.email}`);
|
|
83
|
+
}
|
|
84
|
+
print("");
|
|
85
|
+
if (!result.changed) {
|
|
86
|
+
print(` ${DIM("Already up to date \u2014 no files were modified.")}`);
|
|
87
|
+
} else {
|
|
88
|
+
print(` ${BOLD("Next step:")} restart Claude Code so it picks up the new MCP server.`);
|
|
89
|
+
print(` ${DIM('Once restarted, try: Agent { subagent_type: "agenticmail-fola", prompt: "hi" }')}`);
|
|
90
|
+
}
|
|
91
|
+
print("");
|
|
92
|
+
return 0;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
fail(err.message);
|
|
95
|
+
if (err instanceof AgenticMailApiError && err.status === 0) {
|
|
96
|
+
dim("Is the AgenticMail server running? Try: agenticmail start");
|
|
97
|
+
}
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function runUninstall(purgeBridge) {
|
|
102
|
+
print("");
|
|
103
|
+
print(` ${PINK("\u{1F380} Removing AgenticMail from Claude Code")}`);
|
|
104
|
+
print("");
|
|
105
|
+
try {
|
|
106
|
+
const result = await uninstall({ ...clean(envOptions()), purgeBridgeAgent: purgeBridge });
|
|
107
|
+
if (result.mcpBlockRemoved) ok("Removed MCP server entry from Claude Code config");
|
|
108
|
+
else dim("No MCP server entry was registered.");
|
|
109
|
+
if (result.removedSubagents.length > 0) ok(`Removed ${result.removedSubagents.length} subagent file(s)`);
|
|
110
|
+
else dim("No subagent files were registered.");
|
|
111
|
+
if (result.bridgeAgentDeleted) ok("Deleted bridge agent from AgenticMail");
|
|
112
|
+
else if (purgeBridge) dim("Bridge agent could not be deleted (already gone, or AgenticMail unreachable).");
|
|
113
|
+
print("");
|
|
114
|
+
if (!result.changed) print(` ${DIM("Nothing to remove.")}`);
|
|
115
|
+
print("");
|
|
116
|
+
return 0;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
fail(err.message);
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function runStatus(asJson) {
|
|
123
|
+
try {
|
|
124
|
+
const result = await status(clean(envOptions()));
|
|
125
|
+
if (asJson) {
|
|
126
|
+
print(JSON.stringify(result, null, 2));
|
|
127
|
+
return result.state === "installed" ? 0 : 1;
|
|
128
|
+
}
|
|
129
|
+
print("");
|
|
130
|
+
print(` ${PINK("\u{1F380} AgenticMail for Claude Code")}`);
|
|
131
|
+
print("");
|
|
132
|
+
const stateLabel = result.state === "installed" ? GREEN("installed") : result.state === "partial" ? `${BOLD("partial")}` : DIM("not installed");
|
|
133
|
+
print(` Status: ${stateLabel}`);
|
|
134
|
+
print(` MCP server registered: ${result.mcpInstalled ? GREEN("yes") : DIM("no")}`);
|
|
135
|
+
print(` Bridge agent in AgenticMail: ${result.bridgeAgentExists ? GREEN("yes") : DIM("no")}`);
|
|
136
|
+
print(` Subagent files: ${result.subagents.length > 0 ? GREEN(String(result.subagents.length)) : DIM("0")}`);
|
|
137
|
+
if (result.subagents.length > 0) for (const s of result.subagents) dim(` \u2022 ${s}`);
|
|
138
|
+
if (result.notes.length > 0) {
|
|
139
|
+
print("");
|
|
140
|
+
print(` ${BOLD("Notes:")}`);
|
|
141
|
+
for (const n of result.notes) dim(` \u2022 ${n}`);
|
|
142
|
+
}
|
|
143
|
+
print("");
|
|
144
|
+
return result.state === "installed" ? 0 : 1;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
fail(err.message);
|
|
147
|
+
return 2;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function main() {
|
|
151
|
+
const args = process.argv.slice(2);
|
|
152
|
+
if (args.includes("-h") || args.includes("--help") || args[0] === "help") {
|
|
153
|
+
usage();
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
const command = args.find((a) => !a.startsWith("-")) ?? "install";
|
|
157
|
+
switch (command) {
|
|
158
|
+
case "install":
|
|
159
|
+
return runInstall();
|
|
160
|
+
case "uninstall":
|
|
161
|
+
case "remove":
|
|
162
|
+
return runUninstall(args.includes("--purge-bridge"));
|
|
163
|
+
case "status":
|
|
164
|
+
return runStatus(args.includes("--json"));
|
|
165
|
+
default:
|
|
166
|
+
fail(`Unknown command: ${command}`);
|
|
167
|
+
usage();
|
|
168
|
+
return 64;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
main().then((code) => process.exit(code)).catch((err) => {
|
|
172
|
+
fail(err.message);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
});
|