@agenticmail/api 0.7.21 → 0.9.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.
Files changed (3) hide show
  1. package/README.md +8 -1
  2. package/dist/index.js +110 -4
  3. package/package.json +3 -3
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.7.16
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
@@ -903,7 +903,8 @@ function createFeatureRoutes(db, _accountManager, config, gatewayManager) {
903
903
  return;
904
904
  }
905
905
  const agent = req.agent;
906
- const wakeList = normalizeWakeList(req.body?.wake);
906
+ const explicitWake = normalizeWakeList(req.body?.wake);
907
+ const wakeList = req.body?.wake === void 0 ? deriveDefaultWakeList(draft.to_addr) : explicitWake;
907
908
  const customHeaders = wakeHeaders(wakeList);
908
909
  let persistedAttachments;
909
910
  if (draft.attachments) {
@@ -1189,7 +1190,8 @@ function createFeatureRoutes(db, _accountManager, config, gatewayManager) {
1189
1190
  return;
1190
1191
  }
1191
1192
  const applyVars = (text, vars2) => text.replace(/\{\{(\w+)\}\}/g, (m, key) => vars2[key] ?? m);
1192
- const wakeList = normalizeWakeList(wake);
1193
+ const explicitWake = normalizeWakeList(wake);
1194
+ const wakeList = wake === void 0 ? deriveDefaultWakeList(to) : explicitWake;
1193
1195
  const customHeaders = wakeHeaders(wakeList);
1194
1196
  const vars = variables && typeof variables === "object" ? variables : {};
1195
1197
  const renderedSubject = applyVars(template.subject || "(no subject)", vars);
@@ -1799,13 +1801,29 @@ function normalizeMessageId(id) {
1799
1801
  if (!id) return "";
1800
1802
  return id.trim().replace(/^<+|>+$/g, "").toLowerCase();
1801
1803
  }
1804
+ var WAKE_ALL_SENTINEL = "__wake_all__";
1802
1805
  function normalizeWakeList(value) {
1803
1806
  if (value === void 0 || value === null) return void 0;
1807
+ if (value === "all" || value === WAKE_ALL_SENTINEL) return void 0;
1804
1808
  const strip = (s) => s.trim().replace(/@localhost$/i, "").toLowerCase();
1805
1809
  if (Array.isArray(value)) return value.map((v) => strip(String(v))).filter(Boolean);
1806
1810
  if (typeof value === "string") return value.split(",").map(strip).filter(Boolean);
1807
1811
  return void 0;
1808
1812
  }
1813
+ function deriveDefaultWakeList(toField) {
1814
+ if (!toField) return void 0;
1815
+ const arr = Array.isArray(toField) ? toField : String(toField).split(",");
1816
+ const localNames = [];
1817
+ for (const raw of arr) {
1818
+ const addr = String(raw).trim().toLowerCase();
1819
+ if (!addr.endsWith("@localhost")) continue;
1820
+ const m = addr.match(/<([^>]+)>/);
1821
+ const bare = m ? m[1].trim() : addr;
1822
+ const name = bare.replace(/@localhost$/i, "");
1823
+ if (name) localNames.push(name);
1824
+ }
1825
+ return localNames.length > 0 ? localNames : void 0;
1826
+ }
1809
1827
  function wakeHeaders(wakeList) {
1810
1828
  if (wakeList === void 0) return {};
1811
1829
  return { "X-AgenticMail-Wake": wakeList.join(", ") };
@@ -1933,7 +1951,8 @@ function createMailRoutes(accountManager2, config, db, gatewayManager) {
1933
1951
  const pendingId = crypto.randomUUID();
1934
1952
  const ownerName2 = agent.metadata?.ownerName;
1935
1953
  const fromName2 = ownerName2 ? `${agent.name} from ${ownerName2}` : agent.name;
1936
- const wakeListForPersist = normalizeWakeList(wake);
1954
+ const explicitWakeForPersist = normalizeWakeList(wake);
1955
+ const wakeListForPersist = wake === void 0 ? deriveDefaultWakeList(to) : explicitWakeForPersist;
1937
1956
  const mailOptions = {
1938
1957
  to,
1939
1958
  subject,
@@ -2016,7 +2035,8 @@ function createMailRoutes(accountManager2, config, db, gatewayManager) {
2016
2035
  }
2017
2036
  const ownerName = agent.metadata?.ownerName;
2018
2037
  const fromName = ownerName ? `${agent.name} from ${ownerName}` : agent.name;
2019
- const wakeList = normalizeWakeList(wake);
2038
+ const explicitWake = normalizeWakeList(wake);
2039
+ const wakeList = wake === void 0 ? deriveDefaultWakeList(to) : explicitWake;
2020
2040
  const customHeaders = wakeHeaders(wakeList);
2021
2041
  const mailOpts = {
2022
2042
  to,
@@ -5196,6 +5216,91 @@ function createDispatcherActivityRoutes() {
5196
5216
  return router;
5197
5217
  }
5198
5218
 
5219
+ // src/routes/agent-memory.ts
5220
+ import { Router as Router14 } from "express";
5221
+ import {
5222
+ AgentMemoryStore,
5223
+ threadIdFor,
5224
+ ThreadCache
5225
+ } from "@agenticmail/core";
5226
+ function createAgentMemoryRoutes(config) {
5227
+ const router = Router14();
5228
+ const memoryStore = new AgentMemoryStore();
5229
+ const threadCache = new ThreadCache();
5230
+ router.get("/agents/me/memory/threads/:t", requireAgent, async (req, res, next) => {
5231
+ try {
5232
+ const t = String(req.params.t);
5233
+ const memory = memoryStore.read(req.agent.id, t);
5234
+ if (!memory) {
5235
+ res.status(404).json({ error: "No memory for this thread" });
5236
+ return;
5237
+ }
5238
+ res.json(memory);
5239
+ } catch (err) {
5240
+ next(err);
5241
+ }
5242
+ });
5243
+ router.post("/agents/me/memory/threads/:t", requireAgent, async (req, res, next) => {
5244
+ try {
5245
+ const t = String(req.params.t);
5246
+ const { summary, commitments, openQuestions, lastAction, lastUid } = req.body ?? {};
5247
+ if (!summary && !commitments && !openQuestions && !lastAction && lastUid === void 0) {
5248
+ res.status(400).json({ error: "At least one field is required (summary, commitments, openQuestions, lastAction, or lastUid)" });
5249
+ return;
5250
+ }
5251
+ memoryStore.write(req.agent.id, t, {
5252
+ summary: typeof summary === "string" ? summary : void 0,
5253
+ commitments: Array.isArray(commitments) ? commitments.map(String) : void 0,
5254
+ openQuestions: Array.isArray(openQuestions) ? openQuestions.map(String) : void 0,
5255
+ lastAction: typeof lastAction === "string" ? lastAction : void 0,
5256
+ lastUid: typeof lastUid === "number" ? lastUid : void 0
5257
+ });
5258
+ res.json({ ok: true, threadId: t });
5259
+ } catch (err) {
5260
+ next(err);
5261
+ }
5262
+ });
5263
+ router.delete("/agents/me/memory/threads/:t", requireAgent, async (req, res, next) => {
5264
+ try {
5265
+ memoryStore.delete(req.agent.id, String(req.params.t));
5266
+ res.json({ ok: true });
5267
+ } catch (err) {
5268
+ next(err);
5269
+ }
5270
+ });
5271
+ router.get("/agents/me/thread-id", requireAgent, async (req, res, next) => {
5272
+ try {
5273
+ const uid = parseInt(String(req.query.uid ?? ""), 10);
5274
+ if (isNaN(uid) || uid < 1) {
5275
+ res.status(400).json({ error: "uid query param is required" });
5276
+ return;
5277
+ }
5278
+ const folder = req.query.folder || "INBOX";
5279
+ const password = getAgentPassword(req.agent);
5280
+ const receiver = await getReceiver(req.agent.stalwartPrincipal, password, config);
5281
+ const envs = await receiver.listEnvelopes(folder, { limit: 1, offset: 0 });
5282
+ const envelope = envs.find((e) => e.uid === uid) ?? (await receiver.listEnvelopes(folder, { limit: 200, offset: 0 })).find((e) => e.uid === uid);
5283
+ if (!envelope) {
5284
+ res.status(404).json({ error: `No message with UID ${uid} in folder ${folder}` });
5285
+ return;
5286
+ }
5287
+ const subject = envelope.subject ?? "";
5288
+ const senderAddr = envelope.from?.[0]?.address ?? "";
5289
+ const provisional = threadIdFor({ subject, rootFromAddr: senderAddr });
5290
+ const existing = threadCache.read(provisional);
5291
+ if (existing) {
5292
+ const canonical = threadIdFor({ subject, rootFromAddr: existing.rootFromAddr });
5293
+ res.json({ threadId: canonical, rootFromAddr: existing.rootFromAddr, subject: existing.subject });
5294
+ return;
5295
+ }
5296
+ res.json({ threadId: provisional, rootFromAddr: senderAddr, subject });
5297
+ } catch (err) {
5298
+ next(err);
5299
+ }
5300
+ });
5301
+ return router;
5302
+ }
5303
+
5199
5304
  // src/app.ts
5200
5305
  var integrationRouteFactoryPromise = (async () => {
5201
5306
  try {
@@ -5296,6 +5401,7 @@ function createApp(configOverrides) {
5296
5401
  app2.use("/api/agenticmail", createStorageRoutes(db, accountManager2, config));
5297
5402
  app2.use("/api/agenticmail", createSystemEventRoutes());
5298
5403
  app2.use("/api/agenticmail", createDispatcherActivityRoutes());
5404
+ app2.use("/api/agenticmail", createAgentMemoryRoutes(config));
5299
5405
  app2.use("/api/agenticmail", (_req, res) => {
5300
5406
  res.status(404).json({ error: "Not found" });
5301
5407
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/api",
3
- "version": "0.7.21",
3
+ "version": "0.9.0",
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.7.0",
31
+ "@agenticmail/core": "^0.9.0",
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.0"
39
+ "@agenticmail/claudecode": "^0.2.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/cors": "^2.8.17",