@agenticmail/api 0.7.8 → 0.7.11

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 CHANGED
@@ -8,9 +8,9 @@ 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.7
11
+ ## ✨ What's new in 0.7.9
12
12
 
13
- - **🌐 Lightweight Gmail-style web UI bundled** — `packages/api/public/index.html` is served by `express.static` at the API root. Open `http://127.0.0.1:3829/` in any browser, paste the master key, and you get a Gmail/Outlook-style three-pane email client (agents / inbox / message). Real-time SSE updates, markdown rendering, compose + reply with the new `wake` parameter as a field. Run via `agenticmail web` from the CLI.
13
+ - **🌐 Gmail-style web UI, fully redesigned** — `packages/api/public/` ships a proper two-column Gmail layout (sidebar with Compose + folders / content pane) served by `express.static` at the API root. Every emoji replaced with an inline 24×24 vector icon library (`public/js/icons.js`). HTML shell + dedicated `styles.css` + 14 modular ES module JS files under `public/js/`. Hash router (`#/inbox`, `#/m/<uid>`), search with `from:` / `subject:` operators, real-time SSE updates, browser notifications. Run via `agenticmail web` from the CLI.
14
14
  - **Wake allowlist on `POST /mail/send`** — accept a `wake` parameter (array of agent names or comma-separated string). The API normalises it, sets an `X-AgenticMail-Wake` header on the outgoing SMTP envelope, AND surfaces it as `wakeAllowlist` on the SSE event so the dispatcher can decide which CC'd recipients to actually give a Claude turn.
15
15
  - **Shared helpers exported from `routes/mail.ts`** — `normalizeWakeList`, `wakeHeaders`, and `pushLocalRecipientWakes` so every send path (`/mail/send`, `/templates/:id/send`, `/drafts/:id/send`, `/mail/pending/:id/approve`) uses the same primitives.
16
16
  - **System events SSE** at `GET /system/events` — master-auth stream that emits `account_created` / `account_deleted` / `worker_started` / `worker_finished` events. Powers the dispatcher's zero-wait wake on newly-created agents and the `check_activity` MCP tool.
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ import express from "express";
7
7
  import cors from "cors";
8
8
  import rateLimit from "express-rate-limit";
9
9
  import { fileURLToPath as fileURLToPath2 } from "url";
10
- import { dirname as dirname2, join as join2 } from "path";
11
- import { existsSync } from "fs";
10
+ import { dirname as dirname2, join as join3 } from "path";
11
+ import { existsSync as existsSync2 } from "fs";
12
12
  import {
13
13
  resolveConfig,
14
14
  getDatabase,
@@ -4680,15 +4680,15 @@ function createStorageRoutes(rawDb, accountManager2, config, dialect = "sqlite")
4680
4680
 
4681
4681
  // src/routes/dispatcher-activity.ts
4682
4682
  import { Router as Router13 } from "express";
4683
- var ACTIVE_TTL_MS = 30 * 60 * 1e3;
4683
+ import { existsSync, readFileSync as readFileSync2, statSync } from "fs";
4684
+ import { homedir } from "os";
4685
+ import { join as join2 } from "path";
4686
+ var STALE_HEARTBEAT_MS = 90 * 1e3;
4684
4687
  var RECENT_TTL_MS = 2 * 60 * 1e3;
4685
4688
  var HARD_CAP = 256;
4686
4689
  var active = /* @__PURE__ */ new Map();
4687
4690
  var recent = /* @__PURE__ */ new Map();
4688
4691
  function prune(nowMs) {
4689
- for (const [id, w] of active) {
4690
- if (nowMs - w.startedAtMs > ACTIVE_TTL_MS) active.delete(id);
4691
- }
4692
4692
  for (const [id, w] of recent) {
4693
4693
  const t = w.endedAtMs ?? w.startedAtMs;
4694
4694
  if (nowMs - t > RECENT_TTL_MS) recent.delete(id);
@@ -4718,7 +4718,9 @@ function createDispatcherActivityRoutes() {
4718
4718
  agentEmail: typeof body.agentEmail === "string" ? body.agentEmail : void 0,
4719
4719
  kind: typeof body.kind === "string" ? body.kind : "unknown",
4720
4720
  trigger: body.trigger && typeof body.trigger === "object" ? body.trigger : void 0,
4721
- startedAtMs: Date.now()
4721
+ startedAtMs: Date.now(),
4722
+ lastHeartbeatMs: Date.now(),
4723
+ turnCount: 0
4722
4724
  };
4723
4725
  prune(info.startedAtMs);
4724
4726
  active.set(info.workerId, info);
@@ -4748,7 +4750,8 @@ function createDispatcherActivityRoutes() {
4748
4750
  },
4749
4751
  endedAtMs: nowMs,
4750
4752
  ok: body.ok === false ? false : true,
4751
- resultPreview: typeof body.resultPreview === "string" ? body.resultPreview.slice(0, 240) : void 0
4753
+ resultPreview: typeof body.resultPreview === "string" ? body.resultPreview.slice(0, 240) : void 0,
4754
+ turnCount: typeof body.turnCount === "number" ? body.turnCount : existing?.turnCount
4752
4755
  };
4753
4756
  active.delete(body.workerId);
4754
4757
  recent.set(body.workerId, info);
@@ -4762,6 +4765,22 @@ function createDispatcherActivityRoutes() {
4762
4765
  }
4763
4766
  res.json({ ok: true });
4764
4767
  });
4768
+ router.post("/dispatcher/worker-heartbeat", requireMaster, (req, res) => {
4769
+ const body = req.body ?? {};
4770
+ if (typeof body.workerId !== "string") {
4771
+ res.status(400).json({ error: "workerId is required" });
4772
+ return;
4773
+ }
4774
+ const existing = active.get(body.workerId);
4775
+ if (!existing) {
4776
+ res.json({ ok: true, ignored: "unknown worker" });
4777
+ return;
4778
+ }
4779
+ existing.lastHeartbeatMs = Date.now();
4780
+ if (typeof body.lastTool === "string") existing.lastTool = body.lastTool;
4781
+ if (typeof body.turnCount === "number") existing.turnCount = body.turnCount;
4782
+ res.json({ ok: true });
4783
+ });
4765
4784
  router.get("/dispatcher/activity", requireMaster, (_req, res) => {
4766
4785
  const nowMs = Date.now();
4767
4786
  prune(nowMs);
@@ -4769,7 +4788,9 @@ function createDispatcherActivityRoutes() {
4769
4788
  now: nowMs,
4770
4789
  active: Array.from(active.values()).map((w) => ({
4771
4790
  ...w,
4772
- durationMs: nowMs - w.startedAtMs
4791
+ durationMs: nowMs - w.startedAtMs,
4792
+ stale: w.lastHeartbeatMs !== void 0 && nowMs - w.lastHeartbeatMs > STALE_HEARTBEAT_MS,
4793
+ heartbeatAgeMs: w.lastHeartbeatMs !== void 0 ? nowMs - w.lastHeartbeatMs : void 0
4773
4794
  })),
4774
4795
  recent: Array.from(recent.values()).map((w) => ({
4775
4796
  ...w,
@@ -4777,6 +4798,35 @@ function createDispatcherActivityRoutes() {
4777
4798
  }))
4778
4799
  });
4779
4800
  });
4801
+ router.get("/dispatcher/worker-log/:workerId", requireMaster, (req, res) => {
4802
+ const rawId = String(req.params.workerId ?? "");
4803
+ if (!rawId) {
4804
+ res.status(400).json({ error: "workerId is required" });
4805
+ return;
4806
+ }
4807
+ const lines = Math.min(Math.max(Number(req.query.lines ?? 80), 1), 1e3);
4808
+ const safe = rawId.replace(/[^a-zA-Z0-9._-]/g, "_");
4809
+ const path = join2(homedir(), ".agenticmail", "worker-logs", `${safe}.log`);
4810
+ if (!existsSync(path)) {
4811
+ res.status(404).json({ error: "no log file for that workerId" });
4812
+ return;
4813
+ }
4814
+ try {
4815
+ const raw = readFileSync2(path, "utf-8");
4816
+ const stat = statSync(path);
4817
+ const all = raw.split(/\r?\n/);
4818
+ const tail = all.filter(Boolean).slice(-lines);
4819
+ res.json({
4820
+ workerId: rawId,
4821
+ path,
4822
+ bytes: stat.size,
4823
+ lines: tail.length,
4824
+ tail
4825
+ });
4826
+ } catch (err) {
4827
+ res.status(500).json({ error: err.message });
4828
+ }
4829
+ });
4780
4830
  return router;
4781
4831
  }
4782
4832
 
@@ -4852,15 +4902,15 @@ function createApp(configOverrides) {
4852
4902
  const staticDir = (() => {
4853
4903
  const here = dirname2(fileURLToPath2(import.meta.url));
4854
4904
  const candidates = [
4855
- join2(here, "..", "public"),
4856
- join2(here, "public")
4905
+ join3(here, "..", "public"),
4906
+ join3(here, "public")
4857
4907
  ];
4858
- for (const c of candidates) if (existsSync(c)) return c;
4908
+ for (const c of candidates) if (existsSync2(c)) return c;
4859
4909
  return null;
4860
4910
  })();
4861
4911
  if (staticDir) {
4862
4912
  app2.use("/", express.static(staticDir, { index: "index.html", extensions: ["html"] }));
4863
- app2.get("/ui", (_req, res) => res.sendFile(join2(staticDir, "index.html")));
4913
+ app2.get("/ui", (_req, res) => res.sendFile(join3(staticDir, "index.html")));
4864
4914
  }
4865
4915
  app2.use("/api/agenticmail", createHealthRoutes(stalwart));
4866
4916
  app2.use("/api/agenticmail", createInboundRoutes(accountManager2, config, gatewayManager));
@@ -4889,9 +4939,9 @@ function createApp(configOverrides) {
4889
4939
  }
4890
4940
 
4891
4941
  // src/index.ts
4892
- import { readFileSync as readFileSync2 } from "fs";
4942
+ import { readFileSync as readFileSync3 } from "fs";
4893
4943
  import { fileURLToPath as fileURLToPath3 } from "url";
4894
- import { dirname as dirname3, join as join3 } from "path";
4944
+ import { dirname as dirname3, join as join4 } from "path";
4895
4945
  await prepareIntegrations();
4896
4946
  function getLocalIp() {
4897
4947
  const nets = networkInterfaces();
@@ -4906,7 +4956,7 @@ function getLocalIp() {
4906
4956
  var VERSION = (() => {
4907
4957
  try {
4908
4958
  const __dirname = dirname3(fileURLToPath3(import.meta.url));
4909
- const pkg = JSON.parse(readFileSync2(join3(__dirname, "..", "package.json"), "utf-8"));
4959
+ const pkg = JSON.parse(readFileSync3(join4(__dirname, "..", "package.json"), "utf-8"));
4910
4960
  return pkg.version;
4911
4961
  } catch {
4912
4962
  return "0.5.31";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/api",
3
- "version": "0.7.8",
3
+ "version": "0.7.11",
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",
@@ -0,0 +1,2 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="73 220 158 165" fill="currentColor" aria-hidden="true"><path d="m 105.01,322.07 29.14,-16.35 0.49,-1.42 -0.49,-0.79 h -1.42 l -4.87,-0.3 -16.65,-0.45 -14.44,-0.6 -13.99,-0.75 -3.52,-0.75 -3.3,-4.35 0.34,-2.17 2.96,-1.99 4.24,0.37 9.37,0.64 14.06,0.97 10.2,0.6 15.11,1.57 h 2.4 l 0.34,-0.97 -0.82,-0.6 -0.64,-0.6 -14.55,-9.86 -15.75,-10.42 -8.25,-6 -4.46,-3.04 -2.25,-2.85 -0.97,-6.22 4.05,-4.46 5.44,0.37 1.39,0.37 5.51,4.24 11.77,9.11 15.37,11.32 2.25,1.87 0.9,-0.64 0.11,-0.45 -1.01,-1.69 -8.36,-15.11 -8.92,-15.37 -3.97,-6.37 -1.05,-3.82 c -0.37,-1.57 -0.64,-2.89 -0.64,-4.5 l 4.61,-6.26 2.55,-0.82 6.15,0.82 2.59,2.25 3.82,8.74 6.19,13.76 9.6,18.71 2.81,5.55 1.5,5.14 0.56,1.57 h 0.97 v -0.9 l 0.79,-10.54 1.46,-12.94 1.42,-16.65 0.49,-4.69 2.32,-5.62 4.61,-3.04 3.6,1.72 2.96,4.24 -0.41,2.74 -1.76,11.44 -3.45,17.92 -2.25,12 h 1.31 l 1.5,-1.5 6.07,-8.06 10.2,-12.75 4.5,-5.06 5.25,-5.59 3.37,-2.66 h 6.37 l 4.69,6.97 -2.1,7.2 -6.56,8.32 -5.44,7.05 -7.8,10.5 -4.87,8.4 0.45,0.67 1.16,-0.11 17.62,-3.75 9.52,-1.72 11.36,-1.95 5.14,2.4 0.56,2.44 -2.02,4.99 -12.15,3 -14.25,2.85 -21.22,5.02 -0.26,0.19 0.3,0.37 9.56,0.9 4.09,0.22 h 10.01 l 18.64,1.39 4.87,3.22 2.92,3.94 -0.49,3 -7.5,3.82 -10.12,-2.4 -23.62,-5.62 -8.1,-2.02 h -1.12 v 0.67 l 6.75,6.6 12.37,11.17 15.49,14.4 0.79,3.56 -1.99,2.81 -2.1,-0.3 -13.61,-10.24 -5.25,-4.61 -11.89,-10.01 h -0.79 v 1.05 l 2.74,4.01 14.47,21.75 0.75,6.67 -1.05,2.17 -3.75,1.31 -4.12,-0.75 -8.47,-11.89 -8.74,-13.39 -7.05,-12 -0.86,0.49 -4.16,44.81 -1.95,2.29 -4.5,1.72 -3.75,-2.85 -1.99,-4.61 1.99,-9.11 2.4,-11.89 1.95,-9.45 1.76,-11.74 1.05,-3.9 -0.07,-0.26 -0.86,0.11 -8.85,12.15 -13.46,18.19 -10.65,11.4 -2.55,1.01 -4.42,-2.29 0.41,-4.09 2.47,-3.64 14.74,-18.75 8.89,-11.62 5.74,-6.71 -0.04,-0.97 h -0.34 l -39.15,25.42 -6.97,0.9 -3,-2.81 0.37,-4.61 1.42,-1.5 11.77,-8.1 -0.04,0.04 z"
2
+ shape-rendering="optimizeQuality"/></svg>