@agenticmail/api 0.9.0 → 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/dist/index.js +112 -18
- package/package.json +3 -3
- package/public/js/message-view.js +16 -3
- package/public/styles.css +22 -1
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();
|
|
@@ -1815,10 +1828,10 @@ function deriveDefaultWakeList(toField) {
|
|
|
1815
1828
|
const arr = Array.isArray(toField) ? toField : String(toField).split(",");
|
|
1816
1829
|
const localNames = [];
|
|
1817
1830
|
for (const raw of arr) {
|
|
1818
|
-
const
|
|
1819
|
-
|
|
1820
|
-
const
|
|
1821
|
-
|
|
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;
|
|
1822
1835
|
const name = bare.replace(/@localhost$/i, "");
|
|
1823
1836
|
if (name) localNames.push(name);
|
|
1824
1837
|
}
|
|
@@ -1839,15 +1852,25 @@ async function notifyLocalRecipientsOfNewMail(accountManager2, toField, ccField,
|
|
|
1839
1852
|
push(ccField);
|
|
1840
1853
|
push(bccField);
|
|
1841
1854
|
const addrRe = /<([^>]+)>|([^\s,;<>]+@[^\s,;<>]+)/g;
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
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);
|
|
1850
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, "")));
|
|
1851
1874
|
const notified = /* @__PURE__ */ new Set();
|
|
1852
1875
|
for (const addr of addresses) {
|
|
1853
1876
|
const at = addr.indexOf("@");
|
|
@@ -1879,6 +1902,7 @@ async function notifyLocalRecipientsOfNewMail(accountManager2, toField, ccField,
|
|
|
1879
1902
|
lookup = "failed";
|
|
1880
1903
|
}
|
|
1881
1904
|
}
|
|
1905
|
+
const wasOnTo = toLocalNames.has(recipient.name.toLowerCase());
|
|
1882
1906
|
pushEventToAgent(recipient.id, {
|
|
1883
1907
|
type: "new",
|
|
1884
1908
|
uid,
|
|
@@ -1894,7 +1918,11 @@ async function notifyLocalRecipientsOfNewMail(accountManager2, toField, ccField,
|
|
|
1894
1918
|
// dispatcher reads this and spawns a Claude worker only for
|
|
1895
1919
|
// recipients whose name is on the list (or for everyone if the
|
|
1896
1920
|
// field is absent, preserving the v0.8.x default).
|
|
1897
|
-
...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
|
|
1898
1926
|
});
|
|
1899
1927
|
}
|
|
1900
1928
|
}
|
|
@@ -4902,7 +4930,7 @@ function createStorageRoutes(rawDb, accountManager2, config, dialect = "sqlite")
|
|
|
4902
4930
|
const meta = await verifyAccess(agent, tableName, res);
|
|
4903
4931
|
if (!meta) return;
|
|
4904
4932
|
let imported = 0;
|
|
4905
|
-
let
|
|
4933
|
+
let skipped2 = 0;
|
|
4906
4934
|
for (const row of rows) {
|
|
4907
4935
|
const keys = Object.keys(row);
|
|
4908
4936
|
const vals = Object.values(row).map((v) => typeof v === "object" && v !== null ? JSON.stringify(v) : v);
|
|
@@ -4928,7 +4956,7 @@ function createStorageRoutes(rawDb, accountManager2, config, dialect = "sqlite")
|
|
|
4928
4956
|
imported++;
|
|
4929
4957
|
} catch (e) {
|
|
4930
4958
|
if (onConflict === "skip") {
|
|
4931
|
-
|
|
4959
|
+
skipped2++;
|
|
4932
4960
|
continue;
|
|
4933
4961
|
}
|
|
4934
4962
|
throw e;
|
|
@@ -4936,7 +4964,7 @@ function createStorageRoutes(rawDb, accountManager2, config, dialect = "sqlite")
|
|
|
4936
4964
|
}
|
|
4937
4965
|
const countResult = await db.get(`SELECT COUNT(*) as cnt FROM ${tableName}`);
|
|
4938
4966
|
await db.run("UPDATE agenticmail_storage_meta SET row_count = ?, updated_at = " + nowExpr(dialect) + " WHERE table_name = ?", [countResult?.cnt || 0, tableName]);
|
|
4939
|
-
res.json({ ok: true, imported, skipped, totalRows: countResult?.cnt || 0 });
|
|
4967
|
+
res.json({ ok: true, imported, skipped: skipped2, totalRows: countResult?.cnt || 0 });
|
|
4940
4968
|
} catch (err) {
|
|
4941
4969
|
res.status(500).json({ error: err.message });
|
|
4942
4970
|
}
|
|
@@ -5090,6 +5118,10 @@ function prune(nowMs) {
|
|
|
5090
5118
|
recent.delete(first);
|
|
5091
5119
|
}
|
|
5092
5120
|
}
|
|
5121
|
+
var skipped = [];
|
|
5122
|
+
var SKIPPED_CAP = 100;
|
|
5123
|
+
var SKIPPED_TTL_MS = 5 * 60 * 1e3;
|
|
5124
|
+
var processState = null;
|
|
5093
5125
|
function createDispatcherActivityRoutes() {
|
|
5094
5126
|
const router = Router13();
|
|
5095
5127
|
router.post("/dispatcher/worker-started", requireMaster, (req, res) => {
|
|
@@ -5137,7 +5169,8 @@ function createDispatcherActivityRoutes() {
|
|
|
5137
5169
|
endedAtMs: nowMs,
|
|
5138
5170
|
ok: body.ok === false ? false : true,
|
|
5139
5171
|
resultPreview: typeof body.resultPreview === "string" ? body.resultPreview.slice(0, 240) : void 0,
|
|
5140
|
-
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
|
|
5141
5174
|
};
|
|
5142
5175
|
active.delete(body.workerId);
|
|
5143
5176
|
recent.set(body.workerId, info);
|
|
@@ -5170,8 +5203,26 @@ function createDispatcherActivityRoutes() {
|
|
|
5170
5203
|
router.get("/dispatcher/activity", requireMaster, (_req, res) => {
|
|
5171
5204
|
const nowMs = Date.now();
|
|
5172
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
|
+
})();
|
|
5173
5223
|
res.json({
|
|
5174
5224
|
now: nowMs,
|
|
5225
|
+
dispatcher: processHealth,
|
|
5175
5226
|
active: Array.from(active.values()).map((w) => ({
|
|
5176
5227
|
...w,
|
|
5177
5228
|
durationMs: nowMs - w.startedAtMs,
|
|
@@ -5181,9 +5232,52 @@ function createDispatcherActivityRoutes() {
|
|
|
5181
5232
|
recent: Array.from(recent.values()).map((w) => ({
|
|
5182
5233
|
...w,
|
|
5183
5234
|
durationMs: (w.endedAtMs ?? nowMs) - w.startedAtMs
|
|
5184
|
-
}))
|
|
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 }))
|
|
5185
5241
|
});
|
|
5186
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()
|
|
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 });
|
|
5280
|
+
});
|
|
5187
5281
|
router.get("/dispatcher/worker-log/:workerId", requireMaster, (req, res) => {
|
|
5188
5282
|
const rawId = String(req.params.workerId ?? "");
|
|
5189
5283
|
if (!rawId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenticmail/api",
|
|
3
|
-
"version": "0.9.
|
|
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.9.
|
|
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.2.
|
|
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;
|