@agenticmail/api 0.7.21 → 0.9.1
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 +8 -1
- package/dist/index.js +218 -18
- package/package.json +3 -3
- package/public/js/message-view.js +16 -3
- package/public/styles.css +22 -1
package/README.md
CHANGED
|
@@ -8,7 +8,14 @@ The API server for [AgenticMail](https://github.com/agenticmail/agenticmail) —
|
|
|
8
8
|
|
|
9
9
|
This package runs a web server that handles everything: sending email and SMS, reading inboxes, managing agents, phone number access, real-time notifications, inter-agent messaging, spam filtering, outbound security scanning, and gateway configuration. Every feature in AgenticMail is accessible through this API.
|
|
10
10
|
|
|
11
|
-
## ✨ What's new in 0.
|
|
11
|
+
## ✨ What's new in 0.9.0
|
|
12
|
+
|
|
13
|
+
- **🧠 Agent-thread memory + thread-id resolver endpoints.** Agents persist their own per-thread judgment so the dispatcher can pre-load it into the next wake's prompt:
|
|
14
|
+
- `GET / POST / DELETE /agents/me/memory/threads/:t` — agent-key scoped; each agent only ever touches its own memory file.
|
|
15
|
+
- `GET /agents/me/thread-id?uid=42&folder=INBOX` — resolves the stable subject-only thread id for a message UID, looking up the canonical root via the dispatcher's ThreadCache when available.
|
|
16
|
+
- **🎯 `wake` default flipped to "To: only".** `POST /mail/send`, `POST /drafts/:id/send`, `POST /templates/:id/send`, and the pending-outbound persistence path all derive the implicit allowlist from local recipients on the `To:` field when `wake` is omitted. CC'd local agents receive the mail without waking. New helper `deriveDefaultWakeList(to)` exported from `routes/mail.ts`. Opt back into the old behaviour with `wake: 'all'`.
|
|
17
|
+
|
|
18
|
+
## ✨ Earlier — 0.7.16
|
|
12
19
|
|
|
13
20
|
- **📐 Typed task contracts** — `POST /tasks/assign` and the long-poll `POST /tasks/rpc` accept an optional `outputSchema` field (JSON Schema, draft-7 subset). The schema is persisted on the task row via migration `014_task_output_schema.sql` and is rendered into the worker's wake prompt. `POST /tasks/:id/result` validates against the schema before accepting; mismatches return **400** with a flat `schemaErrors: [{ path, message }]` list. Validator lives at `src/lib/schema-validator.ts` (hand-rolled, no `ajv` dep) and supports `type`, `required`, `properties`, `items`, `enum`, `additionalProperties: false`, `minLength`/`maxLength`, `minimum`/`maximum`. Tasks without a schema keep the v0.8.x behaviour — fully back-compat.
|
|
14
21
|
- **⭐ Star endpoint** — `POST /mail/messages/:uid/star` with `{ starred: boolean, folder?: string }`. Maps to IMAP's `\Flagged` flag — same on-disk bit Gmail's star uses.
|
package/dist/index.js
CHANGED
|
@@ -527,6 +527,19 @@ function createAccountRoutes(accountManager2, db, config) {
|
|
|
527
527
|
next(err);
|
|
528
528
|
}
|
|
529
529
|
});
|
|
530
|
+
router.patch("/accounts/:id/wake-on-cc", requireMaster, async (req, res, next) => {
|
|
531
|
+
try {
|
|
532
|
+
const wakeOnCc = req.body?.wakeOnCc === false ? 0 : 1;
|
|
533
|
+
const result = db.prepare("UPDATE agents SET wake_on_cc = ? WHERE id = ?").run(wakeOnCc, req.params.id);
|
|
534
|
+
if (result.changes === 0) {
|
|
535
|
+
res.status(404).json({ error: "Agent not found" });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
res.json({ ok: true, wakeOnCc: wakeOnCc === 1 });
|
|
539
|
+
} catch (err) {
|
|
540
|
+
next(err);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
530
543
|
router.delete("/accounts/:id", requireMaster, async (req, res, next) => {
|
|
531
544
|
try {
|
|
532
545
|
const allAgents = await accountManager2.list();
|
|
@@ -903,7 +916,8 @@ function createFeatureRoutes(db, _accountManager, config, gatewayManager) {
|
|
|
903
916
|
return;
|
|
904
917
|
}
|
|
905
918
|
const agent = req.agent;
|
|
906
|
-
const
|
|
919
|
+
const explicitWake = normalizeWakeList(req.body?.wake);
|
|
920
|
+
const wakeList = req.body?.wake === void 0 ? deriveDefaultWakeList(draft.to_addr) : explicitWake;
|
|
907
921
|
const customHeaders = wakeHeaders(wakeList);
|
|
908
922
|
let persistedAttachments;
|
|
909
923
|
if (draft.attachments) {
|
|
@@ -1189,7 +1203,8 @@ function createFeatureRoutes(db, _accountManager, config, gatewayManager) {
|
|
|
1189
1203
|
return;
|
|
1190
1204
|
}
|
|
1191
1205
|
const applyVars = (text, vars2) => text.replace(/\{\{(\w+)\}\}/g, (m, key) => vars2[key] ?? m);
|
|
1192
|
-
const
|
|
1206
|
+
const explicitWake = normalizeWakeList(wake);
|
|
1207
|
+
const wakeList = wake === void 0 ? deriveDefaultWakeList(to) : explicitWake;
|
|
1193
1208
|
const customHeaders = wakeHeaders(wakeList);
|
|
1194
1209
|
const vars = variables && typeof variables === "object" ? variables : {};
|
|
1195
1210
|
const renderedSubject = applyVars(template.subject || "(no subject)", vars);
|
|
@@ -1799,13 +1814,29 @@ function normalizeMessageId(id) {
|
|
|
1799
1814
|
if (!id) return "";
|
|
1800
1815
|
return id.trim().replace(/^<+|>+$/g, "").toLowerCase();
|
|
1801
1816
|
}
|
|
1817
|
+
var WAKE_ALL_SENTINEL = "__wake_all__";
|
|
1802
1818
|
function normalizeWakeList(value) {
|
|
1803
1819
|
if (value === void 0 || value === null) return void 0;
|
|
1820
|
+
if (value === "all" || value === WAKE_ALL_SENTINEL) return void 0;
|
|
1804
1821
|
const strip = (s) => s.trim().replace(/@localhost$/i, "").toLowerCase();
|
|
1805
1822
|
if (Array.isArray(value)) return value.map((v) => strip(String(v))).filter(Boolean);
|
|
1806
1823
|
if (typeof value === "string") return value.split(",").map(strip).filter(Boolean);
|
|
1807
1824
|
return void 0;
|
|
1808
1825
|
}
|
|
1826
|
+
function deriveDefaultWakeList(toField) {
|
|
1827
|
+
if (!toField) return void 0;
|
|
1828
|
+
const arr = Array.isArray(toField) ? toField : String(toField).split(",");
|
|
1829
|
+
const localNames = [];
|
|
1830
|
+
for (const raw of arr) {
|
|
1831
|
+
const trimmed = String(raw).trim().toLowerCase();
|
|
1832
|
+
const m = trimmed.match(/<([^>]+)>/);
|
|
1833
|
+
const bare = (m ? m[1] : trimmed).trim();
|
|
1834
|
+
if (!bare.endsWith("@localhost")) continue;
|
|
1835
|
+
const name = bare.replace(/@localhost$/i, "");
|
|
1836
|
+
if (name) localNames.push(name);
|
|
1837
|
+
}
|
|
1838
|
+
return localNames.length > 0 ? localNames : void 0;
|
|
1839
|
+
}
|
|
1809
1840
|
function wakeHeaders(wakeList) {
|
|
1810
1841
|
if (wakeList === void 0) return {};
|
|
1811
1842
|
return { "X-AgenticMail-Wake": wakeList.join(", ") };
|
|
@@ -1821,15 +1852,25 @@ async function notifyLocalRecipientsOfNewMail(accountManager2, toField, ccField,
|
|
|
1821
1852
|
push(ccField);
|
|
1822
1853
|
push(bccField);
|
|
1823
1854
|
const addrRe = /<([^>]+)>|([^\s,;<>]+@[^\s,;<>]+)/g;
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1855
|
+
function extractAddrs(v) {
|
|
1856
|
+
if (!v) return [];
|
|
1857
|
+
const items = Array.isArray(v) ? v : [v];
|
|
1858
|
+
const out = /* @__PURE__ */ new Set();
|
|
1859
|
+
for (const entry of items) {
|
|
1860
|
+
let match;
|
|
1861
|
+
addrRe.lastIndex = 0;
|
|
1862
|
+
while ((match = addrRe.exec(entry)) !== null) {
|
|
1863
|
+
const a = (match[1] || match[2] || "").trim().toLowerCase();
|
|
1864
|
+
if (a) out.add(a);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
return Array.from(out);
|
|
1832
1868
|
}
|
|
1869
|
+
const toAddrs = extractAddrs(toField);
|
|
1870
|
+
const ccAddrs = extractAddrs(ccField);
|
|
1871
|
+
const bccAddrs = extractAddrs(bccField);
|
|
1872
|
+
const addresses = /* @__PURE__ */ new Set([...toAddrs, ...ccAddrs, ...bccAddrs]);
|
|
1873
|
+
const toLocalNames = new Set(toAddrs.filter((a) => a.endsWith("@localhost")).map((a) => a.replace(/@localhost$/i, "")));
|
|
1833
1874
|
const notified = /* @__PURE__ */ new Set();
|
|
1834
1875
|
for (const addr of addresses) {
|
|
1835
1876
|
const at = addr.indexOf("@");
|
|
@@ -1861,6 +1902,7 @@ async function notifyLocalRecipientsOfNewMail(accountManager2, toField, ccField,
|
|
|
1861
1902
|
lookup = "failed";
|
|
1862
1903
|
}
|
|
1863
1904
|
}
|
|
1905
|
+
const wasOnTo = toLocalNames.has(recipient.name.toLowerCase());
|
|
1864
1906
|
pushEventToAgent(recipient.id, {
|
|
1865
1907
|
type: "new",
|
|
1866
1908
|
uid,
|
|
@@ -1876,7 +1918,11 @@ async function notifyLocalRecipientsOfNewMail(accountManager2, toField, ccField,
|
|
|
1876
1918
|
// dispatcher reads this and spawns a Claude worker only for
|
|
1877
1919
|
// recipients whose name is on the list (or for everyone if the
|
|
1878
1920
|
// field is absent, preserving the v0.8.x default).
|
|
1879
|
-
...wakeList !== void 0 ? { wakeAllowlist: wakeList } : {}
|
|
1921
|
+
...wakeList !== void 0 ? { wakeAllowlist: wakeList } : {},
|
|
1922
|
+
// Per-recipient "was I on To?" flag for wake_on_cc honoring.
|
|
1923
|
+
// The dispatcher uses this combined with account.wakeOnCc to
|
|
1924
|
+
// decide whether to skip a CC-only delivery.
|
|
1925
|
+
wasOnTo
|
|
1880
1926
|
});
|
|
1881
1927
|
}
|
|
1882
1928
|
}
|
|
@@ -1933,7 +1979,8 @@ function createMailRoutes(accountManager2, config, db, gatewayManager) {
|
|
|
1933
1979
|
const pendingId = crypto.randomUUID();
|
|
1934
1980
|
const ownerName2 = agent.metadata?.ownerName;
|
|
1935
1981
|
const fromName2 = ownerName2 ? `${agent.name} from ${ownerName2}` : agent.name;
|
|
1936
|
-
const
|
|
1982
|
+
const explicitWakeForPersist = normalizeWakeList(wake);
|
|
1983
|
+
const wakeListForPersist = wake === void 0 ? deriveDefaultWakeList(to) : explicitWakeForPersist;
|
|
1937
1984
|
const mailOptions = {
|
|
1938
1985
|
to,
|
|
1939
1986
|
subject,
|
|
@@ -2016,7 +2063,8 @@ function createMailRoutes(accountManager2, config, db, gatewayManager) {
|
|
|
2016
2063
|
}
|
|
2017
2064
|
const ownerName = agent.metadata?.ownerName;
|
|
2018
2065
|
const fromName = ownerName ? `${agent.name} from ${ownerName}` : agent.name;
|
|
2019
|
-
const
|
|
2066
|
+
const explicitWake = normalizeWakeList(wake);
|
|
2067
|
+
const wakeList = wake === void 0 ? deriveDefaultWakeList(to) : explicitWake;
|
|
2020
2068
|
const customHeaders = wakeHeaders(wakeList);
|
|
2021
2069
|
const mailOpts = {
|
|
2022
2070
|
to,
|
|
@@ -4882,7 +4930,7 @@ function createStorageRoutes(rawDb, accountManager2, config, dialect = "sqlite")
|
|
|
4882
4930
|
const meta = await verifyAccess(agent, tableName, res);
|
|
4883
4931
|
if (!meta) return;
|
|
4884
4932
|
let imported = 0;
|
|
4885
|
-
let
|
|
4933
|
+
let skipped2 = 0;
|
|
4886
4934
|
for (const row of rows) {
|
|
4887
4935
|
const keys = Object.keys(row);
|
|
4888
4936
|
const vals = Object.values(row).map((v) => typeof v === "object" && v !== null ? JSON.stringify(v) : v);
|
|
@@ -4908,7 +4956,7 @@ function createStorageRoutes(rawDb, accountManager2, config, dialect = "sqlite")
|
|
|
4908
4956
|
imported++;
|
|
4909
4957
|
} catch (e) {
|
|
4910
4958
|
if (onConflict === "skip") {
|
|
4911
|
-
|
|
4959
|
+
skipped2++;
|
|
4912
4960
|
continue;
|
|
4913
4961
|
}
|
|
4914
4962
|
throw e;
|
|
@@ -4916,7 +4964,7 @@ function createStorageRoutes(rawDb, accountManager2, config, dialect = "sqlite")
|
|
|
4916
4964
|
}
|
|
4917
4965
|
const countResult = await db.get(`SELECT COUNT(*) as cnt FROM ${tableName}`);
|
|
4918
4966
|
await db.run("UPDATE agenticmail_storage_meta SET row_count = ?, updated_at = " + nowExpr(dialect) + " WHERE table_name = ?", [countResult?.cnt || 0, tableName]);
|
|
4919
|
-
res.json({ ok: true, imported, skipped, totalRows: countResult?.cnt || 0 });
|
|
4967
|
+
res.json({ ok: true, imported, skipped: skipped2, totalRows: countResult?.cnt || 0 });
|
|
4920
4968
|
} catch (err) {
|
|
4921
4969
|
res.status(500).json({ error: err.message });
|
|
4922
4970
|
}
|
|
@@ -5070,6 +5118,10 @@ function prune(nowMs) {
|
|
|
5070
5118
|
recent.delete(first);
|
|
5071
5119
|
}
|
|
5072
5120
|
}
|
|
5121
|
+
var skipped = [];
|
|
5122
|
+
var SKIPPED_CAP = 100;
|
|
5123
|
+
var SKIPPED_TTL_MS = 5 * 60 * 1e3;
|
|
5124
|
+
var processState = null;
|
|
5073
5125
|
function createDispatcherActivityRoutes() {
|
|
5074
5126
|
const router = Router13();
|
|
5075
5127
|
router.post("/dispatcher/worker-started", requireMaster, (req, res) => {
|
|
@@ -5117,7 +5169,8 @@ function createDispatcherActivityRoutes() {
|
|
|
5117
5169
|
endedAtMs: nowMs,
|
|
5118
5170
|
ok: body.ok === false ? false : true,
|
|
5119
5171
|
resultPreview: typeof body.resultPreview === "string" ? body.resultPreview.slice(0, 240) : void 0,
|
|
5120
|
-
turnCount: typeof body.turnCount === "number" ? body.turnCount : existing?.turnCount
|
|
5172
|
+
turnCount: typeof body.turnCount === "number" ? body.turnCount : existing?.turnCount,
|
|
5173
|
+
usage: typeof body.usage === "string" ? body.usage : existing?.usage
|
|
5121
5174
|
};
|
|
5122
5175
|
active.delete(body.workerId);
|
|
5123
5176
|
recent.set(body.workerId, info);
|
|
@@ -5150,8 +5203,26 @@ function createDispatcherActivityRoutes() {
|
|
|
5150
5203
|
router.get("/dispatcher/activity", requireMaster, (_req, res) => {
|
|
5151
5204
|
const nowMs = Date.now();
|
|
5152
5205
|
prune(nowMs);
|
|
5206
|
+
while (skipped.length > 0 && nowMs - skipped[0].atMs > SKIPPED_TTL_MS) skipped.shift();
|
|
5207
|
+
while (skipped.length > SKIPPED_CAP) skipped.shift();
|
|
5208
|
+
const processHealth = (() => {
|
|
5209
|
+
if (!processState) return { state: "missing" };
|
|
5210
|
+
const age = nowMs - processState.atMs;
|
|
5211
|
+
const isAlive = age <= 9e4;
|
|
5212
|
+
return {
|
|
5213
|
+
state: isAlive ? "alive" : "unhealthy",
|
|
5214
|
+
startedAtMs: processState.startedAtMs,
|
|
5215
|
+
uptimeMs: nowMs - processState.startedAtMs,
|
|
5216
|
+
lastHeartbeatAgeMs: age,
|
|
5217
|
+
channels: processState.channels,
|
|
5218
|
+
coalesceQueueSize: processState.coalesceQueueSize,
|
|
5219
|
+
running: processState.running,
|
|
5220
|
+
maxConcurrent: processState.maxConcurrent
|
|
5221
|
+
};
|
|
5222
|
+
})();
|
|
5153
5223
|
res.json({
|
|
5154
5224
|
now: nowMs,
|
|
5225
|
+
dispatcher: processHealth,
|
|
5155
5226
|
active: Array.from(active.values()).map((w) => ({
|
|
5156
5227
|
...w,
|
|
5157
5228
|
durationMs: nowMs - w.startedAtMs,
|
|
@@ -5161,8 +5232,51 @@ function createDispatcherActivityRoutes() {
|
|
|
5161
5232
|
recent: Array.from(recent.values()).map((w) => ({
|
|
5162
5233
|
...w,
|
|
5163
5234
|
durationMs: (w.endedAtMs ?? nowMs) - w.startedAtMs
|
|
5164
|
-
}))
|
|
5235
|
+
})),
|
|
5236
|
+
// Recent skipped wakes — every filter decision the dispatcher
|
|
5237
|
+
// made that DROPPED a wake. Surfaced so the host can see "the
|
|
5238
|
+
// mail landed, the dispatcher saw it, here's why it skipped"
|
|
5239
|
+
// instead of staring at silence.
|
|
5240
|
+
skipped: skipped.map((s) => ({ ...s, ageMs: nowMs - s.atMs }))
|
|
5241
|
+
});
|
|
5242
|
+
});
|
|
5243
|
+
router.post("/dispatcher/process-heartbeat", requireMaster, (req, res) => {
|
|
5244
|
+
const body = req.body ?? {};
|
|
5245
|
+
if (typeof body.startedAtMs !== "number") {
|
|
5246
|
+
res.status(400).json({ error: "startedAtMs is required" });
|
|
5247
|
+
return;
|
|
5248
|
+
}
|
|
5249
|
+
processState = {
|
|
5250
|
+
startedAtMs: body.startedAtMs,
|
|
5251
|
+
channels: typeof body.channels === "number" ? body.channels : 0,
|
|
5252
|
+
coalesceQueueSize: typeof body.coalesceQueueSize === "number" ? body.coalesceQueueSize : 0,
|
|
5253
|
+
running: typeof body.running === "number" ? body.running : 0,
|
|
5254
|
+
maxConcurrent: typeof body.maxConcurrent === "number" ? body.maxConcurrent : 0,
|
|
5255
|
+
atMs: Date.now()
|
|
5256
|
+
};
|
|
5257
|
+
res.json({ ok: true });
|
|
5258
|
+
});
|
|
5259
|
+
router.post("/dispatcher/worker-skipped", requireMaster, (req, res) => {
|
|
5260
|
+
const body = req.body ?? {};
|
|
5261
|
+
if (typeof body.agentName !== "string" || typeof body.reason !== "string") {
|
|
5262
|
+
res.status(400).json({ error: "agentName and reason are required" });
|
|
5263
|
+
return;
|
|
5264
|
+
}
|
|
5265
|
+
skipped.push({
|
|
5266
|
+
agentId: typeof body.agentId === "string" ? body.agentId : void 0,
|
|
5267
|
+
agentName: body.agentName,
|
|
5268
|
+
uid: typeof body.uid === "number" ? body.uid : void 0,
|
|
5269
|
+
subject: typeof body.subject === "string" ? body.subject : void 0,
|
|
5270
|
+
from: typeof body.from === "string" ? body.from : void 0,
|
|
5271
|
+
reason: body.reason,
|
|
5272
|
+
detail: typeof body.detail === "string" ? body.detail : void 0,
|
|
5273
|
+
atMs: Date.now()
|
|
5165
5274
|
});
|
|
5275
|
+
while (skipped.length > SKIPPED_CAP) skipped.shift();
|
|
5276
|
+
res.json({ ok: true });
|
|
5277
|
+
});
|
|
5278
|
+
router.post("/dispatcher/worker-queued", requireMaster, (req, res) => {
|
|
5279
|
+
res.json({ ok: true, recorded: req.body ?? null });
|
|
5166
5280
|
});
|
|
5167
5281
|
router.get("/dispatcher/worker-log/:workerId", requireMaster, (req, res) => {
|
|
5168
5282
|
const rawId = String(req.params.workerId ?? "");
|
|
@@ -5196,6 +5310,91 @@ function createDispatcherActivityRoutes() {
|
|
|
5196
5310
|
return router;
|
|
5197
5311
|
}
|
|
5198
5312
|
|
|
5313
|
+
// src/routes/agent-memory.ts
|
|
5314
|
+
import { Router as Router14 } from "express";
|
|
5315
|
+
import {
|
|
5316
|
+
AgentMemoryStore,
|
|
5317
|
+
threadIdFor,
|
|
5318
|
+
ThreadCache
|
|
5319
|
+
} from "@agenticmail/core";
|
|
5320
|
+
function createAgentMemoryRoutes(config) {
|
|
5321
|
+
const router = Router14();
|
|
5322
|
+
const memoryStore = new AgentMemoryStore();
|
|
5323
|
+
const threadCache = new ThreadCache();
|
|
5324
|
+
router.get("/agents/me/memory/threads/:t", requireAgent, async (req, res, next) => {
|
|
5325
|
+
try {
|
|
5326
|
+
const t = String(req.params.t);
|
|
5327
|
+
const memory = memoryStore.read(req.agent.id, t);
|
|
5328
|
+
if (!memory) {
|
|
5329
|
+
res.status(404).json({ error: "No memory for this thread" });
|
|
5330
|
+
return;
|
|
5331
|
+
}
|
|
5332
|
+
res.json(memory);
|
|
5333
|
+
} catch (err) {
|
|
5334
|
+
next(err);
|
|
5335
|
+
}
|
|
5336
|
+
});
|
|
5337
|
+
router.post("/agents/me/memory/threads/:t", requireAgent, async (req, res, next) => {
|
|
5338
|
+
try {
|
|
5339
|
+
const t = String(req.params.t);
|
|
5340
|
+
const { summary, commitments, openQuestions, lastAction, lastUid } = req.body ?? {};
|
|
5341
|
+
if (!summary && !commitments && !openQuestions && !lastAction && lastUid === void 0) {
|
|
5342
|
+
res.status(400).json({ error: "At least one field is required (summary, commitments, openQuestions, lastAction, or lastUid)" });
|
|
5343
|
+
return;
|
|
5344
|
+
}
|
|
5345
|
+
memoryStore.write(req.agent.id, t, {
|
|
5346
|
+
summary: typeof summary === "string" ? summary : void 0,
|
|
5347
|
+
commitments: Array.isArray(commitments) ? commitments.map(String) : void 0,
|
|
5348
|
+
openQuestions: Array.isArray(openQuestions) ? openQuestions.map(String) : void 0,
|
|
5349
|
+
lastAction: typeof lastAction === "string" ? lastAction : void 0,
|
|
5350
|
+
lastUid: typeof lastUid === "number" ? lastUid : void 0
|
|
5351
|
+
});
|
|
5352
|
+
res.json({ ok: true, threadId: t });
|
|
5353
|
+
} catch (err) {
|
|
5354
|
+
next(err);
|
|
5355
|
+
}
|
|
5356
|
+
});
|
|
5357
|
+
router.delete("/agents/me/memory/threads/:t", requireAgent, async (req, res, next) => {
|
|
5358
|
+
try {
|
|
5359
|
+
memoryStore.delete(req.agent.id, String(req.params.t));
|
|
5360
|
+
res.json({ ok: true });
|
|
5361
|
+
} catch (err) {
|
|
5362
|
+
next(err);
|
|
5363
|
+
}
|
|
5364
|
+
});
|
|
5365
|
+
router.get("/agents/me/thread-id", requireAgent, async (req, res, next) => {
|
|
5366
|
+
try {
|
|
5367
|
+
const uid = parseInt(String(req.query.uid ?? ""), 10);
|
|
5368
|
+
if (isNaN(uid) || uid < 1) {
|
|
5369
|
+
res.status(400).json({ error: "uid query param is required" });
|
|
5370
|
+
return;
|
|
5371
|
+
}
|
|
5372
|
+
const folder = req.query.folder || "INBOX";
|
|
5373
|
+
const password = getAgentPassword(req.agent);
|
|
5374
|
+
const receiver = await getReceiver(req.agent.stalwartPrincipal, password, config);
|
|
5375
|
+
const envs = await receiver.listEnvelopes(folder, { limit: 1, offset: 0 });
|
|
5376
|
+
const envelope = envs.find((e) => e.uid === uid) ?? (await receiver.listEnvelopes(folder, { limit: 200, offset: 0 })).find((e) => e.uid === uid);
|
|
5377
|
+
if (!envelope) {
|
|
5378
|
+
res.status(404).json({ error: `No message with UID ${uid} in folder ${folder}` });
|
|
5379
|
+
return;
|
|
5380
|
+
}
|
|
5381
|
+
const subject = envelope.subject ?? "";
|
|
5382
|
+
const senderAddr = envelope.from?.[0]?.address ?? "";
|
|
5383
|
+
const provisional = threadIdFor({ subject, rootFromAddr: senderAddr });
|
|
5384
|
+
const existing = threadCache.read(provisional);
|
|
5385
|
+
if (existing) {
|
|
5386
|
+
const canonical = threadIdFor({ subject, rootFromAddr: existing.rootFromAddr });
|
|
5387
|
+
res.json({ threadId: canonical, rootFromAddr: existing.rootFromAddr, subject: existing.subject });
|
|
5388
|
+
return;
|
|
5389
|
+
}
|
|
5390
|
+
res.json({ threadId: provisional, rootFromAddr: senderAddr, subject });
|
|
5391
|
+
} catch (err) {
|
|
5392
|
+
next(err);
|
|
5393
|
+
}
|
|
5394
|
+
});
|
|
5395
|
+
return router;
|
|
5396
|
+
}
|
|
5397
|
+
|
|
5199
5398
|
// src/app.ts
|
|
5200
5399
|
var integrationRouteFactoryPromise = (async () => {
|
|
5201
5400
|
try {
|
|
@@ -5296,6 +5495,7 @@ function createApp(configOverrides) {
|
|
|
5296
5495
|
app2.use("/api/agenticmail", createStorageRoutes(db, accountManager2, config));
|
|
5297
5496
|
app2.use("/api/agenticmail", createSystemEventRoutes());
|
|
5298
5497
|
app2.use("/api/agenticmail", createDispatcherActivityRoutes());
|
|
5498
|
+
app2.use("/api/agenticmail", createAgentMemoryRoutes(config));
|
|
5299
5499
|
app2.use("/api/agenticmail", (_req, res) => {
|
|
5300
5500
|
res.status(404).json({ error: "Not found" });
|
|
5301
5501
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenticmail/api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "REST API server for AgenticMail — email and SMS endpoints for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"prepublishOnly": "npm run build"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@agenticmail/core": "^0.
|
|
31
|
+
"@agenticmail/core": "^0.9.1",
|
|
32
32
|
"cors": "^2.8.5",
|
|
33
33
|
"dotenv": "^16.4.7",
|
|
34
34
|
"express": "^4.21.0",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"uuid": "^11.1.0"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@agenticmail/claudecode": "^0.1
|
|
39
|
+
"@agenticmail/claudecode": "^0.2.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/cors": "^2.8.17",
|
|
@@ -50,11 +50,22 @@ function renderMessage(msg) {
|
|
|
50
50
|
if (!view) return;
|
|
51
51
|
const fromAddr = msg.from?.[0]?.address ?? '?';
|
|
52
52
|
const fromName = msg.from?.[0]?.name || fromAddr;
|
|
53
|
-
const toStr = (msg.to ?? []).map(a => a.name ? `${a.name} <${a.address}>` : a.address).join(', ') || '?';
|
|
54
|
-
const ccStr = (msg.cc ?? []).map(a => a.address).join(', ');
|
|
55
53
|
const senderPseudo = { name: fromName }; // for avatar generation
|
|
56
54
|
const bodyText = msg.text ?? stripHtml(msg.html ?? '');
|
|
57
55
|
|
|
56
|
+
// Build separate To / Cc / Bcc lines so the user can actually
|
|
57
|
+
// tell who was on the action list vs CC'd for awareness. The
|
|
58
|
+
// previous renderer concatenated everything onto one "to" line.
|
|
59
|
+
const formatAddr = (a) => a?.name && a.name !== a.address
|
|
60
|
+
? `${a.name} <${a.address}>`
|
|
61
|
+
: (a?.address ?? '');
|
|
62
|
+
const renderAddrRow = (label, list, cls) => {
|
|
63
|
+
if (!Array.isArray(list) || list.length === 0) return '';
|
|
64
|
+
const rendered = list.map(formatAddr).filter(Boolean).map(escapeHtml).join(', ');
|
|
65
|
+
if (!rendered) return '';
|
|
66
|
+
return `<div class="message-recipient-row ${cls}"><span class="message-recipient-label">${label}</span><span class="message-recipient-list">${rendered}</span></div>`;
|
|
67
|
+
};
|
|
68
|
+
|
|
58
69
|
const attachmentsHtml = (msg.attachments ?? []).length > 0
|
|
59
70
|
? `<div class="message-attachments">${msg.attachments.map((a, i) =>
|
|
60
71
|
`<button class="message-attachment" data-att-index="${i}" data-att-filename="${escapeHtml(a.filename ?? 'attachment')}" title="Click to download">
|
|
@@ -75,7 +86,9 @@ function renderMessage(msg) {
|
|
|
75
86
|
<span class="name">${escapeHtml(fromName)}</span>
|
|
76
87
|
<span class="addr"><${escapeHtml(fromAddr)}></span>
|
|
77
88
|
</div>
|
|
78
|
-
|
|
89
|
+
${renderAddrRow('To', msg.to, 'message-to')}
|
|
90
|
+
${renderAddrRow('Cc', msg.cc, 'message-cc')}
|
|
91
|
+
${renderAddrRow('Bcc', msg.bcc, 'message-bcc')}
|
|
79
92
|
</div>
|
|
80
93
|
<div class="message-date">${escapeHtml(formatDateFull(msg.date))}</div>
|
|
81
94
|
</div>
|
package/public/styles.css
CHANGED
|
@@ -583,7 +583,28 @@ mark.search-hl {
|
|
|
583
583
|
}
|
|
584
584
|
.message-from .name { font-weight: 500; }
|
|
585
585
|
.message-from .addr { color: var(--muted); margin-left: 4px; }
|
|
586
|
-
|
|
586
|
+
/* Recipient rows — one per field (To / Cc / Bcc). Renders only
|
|
587
|
+
when the field is non-empty. The label is a fixed-width tag
|
|
588
|
+
so the addresses align cleanly across the three rows. */
|
|
589
|
+
.message-recipient-row {
|
|
590
|
+
display: flex; gap: 6px;
|
|
591
|
+
font-size: 12px; color: var(--muted);
|
|
592
|
+
margin-top: 4px;
|
|
593
|
+
line-height: 1.5;
|
|
594
|
+
}
|
|
595
|
+
.message-recipient-label {
|
|
596
|
+
flex: 0 0 28px;
|
|
597
|
+
font-weight: 600;
|
|
598
|
+
color: var(--ink-soft);
|
|
599
|
+
text-transform: none;
|
|
600
|
+
}
|
|
601
|
+
.message-recipient-list {
|
|
602
|
+
flex: 1 1 auto;
|
|
603
|
+
min-width: 0;
|
|
604
|
+
word-break: break-word;
|
|
605
|
+
}
|
|
606
|
+
.message-cc .message-recipient-label,
|
|
607
|
+
.message-bcc .message-recipient-label { color: var(--muted); }
|
|
587
608
|
.message-date {
|
|
588
609
|
font-size: 12px; color: var(--muted);
|
|
589
610
|
white-space: nowrap;
|