@agenticmail/core 0.7.5 → 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.
package/dist/index.js CHANGED
@@ -1087,14 +1087,14 @@ var StalwartAdmin = class {
1087
1087
  if (!isValidDomain(domain)) {
1088
1088
  throw new Error(`Invalid domain format: "${domain}"`);
1089
1089
  }
1090
- const { readFileSync: readFileSync5, writeFileSync: writeFileSync6 } = await import("fs");
1091
- const { homedir: homedir9 } = await import("os");
1092
- const { join: join10 } = await import("path");
1093
- const configPath = join10(homedir9(), ".agenticmail", "stalwart.toml");
1090
+ const { readFileSync: readFileSync7, writeFileSync: writeFileSync8 } = await import("fs");
1091
+ const { homedir: homedir11 } = await import("os");
1092
+ const { join: join12 } = await import("path");
1093
+ const configPath = join12(homedir11(), ".agenticmail", "stalwart.toml");
1094
1094
  try {
1095
- let config = readFileSync5(configPath, "utf-8");
1095
+ let config = readFileSync7(configPath, "utf-8");
1096
1096
  config = config.replace(/^hostname\s*=\s*"[^"]*"/m, `hostname = "${escapeTomlString(domain)}"`);
1097
- writeFileSync6(configPath, config);
1097
+ writeFileSync8(configPath, config);
1098
1098
  console.log(`[Stalwart] Updated hostname to "${domain}" in stalwart.toml`);
1099
1099
  } catch (err) {
1100
1100
  throw new Error(`Failed to set config server.hostname=${domain}`);
@@ -1103,15 +1103,15 @@ var StalwartAdmin = class {
1103
1103
  // --- DKIM ---
1104
1104
  /** Path to the host-side stalwart.toml (mounted read-only into container) */
1105
1105
  get configPath() {
1106
- const { homedir: homedir9 } = __require("os");
1107
- const { join: join10 } = __require("path");
1108
- return join10(homedir9(), ".agenticmail", "stalwart.toml");
1106
+ const { homedir: homedir11 } = __require("os");
1107
+ const { join: join12 } = __require("path");
1108
+ return join12(homedir11(), ".agenticmail", "stalwart.toml");
1109
1109
  }
1110
1110
  /** Path to host-side DKIM key directory */
1111
1111
  get dkimDir() {
1112
- const { homedir: homedir9 } = __require("os");
1113
- const { join: join10 } = __require("path");
1114
- return join10(homedir9(), ".agenticmail");
1112
+ const { homedir: homedir11 } = __require("os");
1113
+ const { join: join12 } = __require("path");
1114
+ return join12(homedir11(), ".agenticmail");
1115
1115
  }
1116
1116
  /**
1117
1117
  * Create/reuse a DKIM signing key for a domain.
@@ -1212,12 +1212,12 @@ var StalwartAdmin = class {
1212
1212
  * This bypasses the need for a PTR record on the sending IP.
1213
1213
  */
1214
1214
  async configureOutboundRelay(config) {
1215
- const { readFileSync: readFileSync5, writeFileSync: writeFileSync6 } = await import("fs");
1216
- const { homedir: homedir9 } = await import("os");
1217
- const { join: join10 } = await import("path");
1215
+ const { readFileSync: readFileSync7, writeFileSync: writeFileSync8 } = await import("fs");
1216
+ const { homedir: homedir11 } = await import("os");
1217
+ const { join: join12 } = await import("path");
1218
1218
  const routeName = config.routeName ?? "gmail";
1219
- const tomlPath = join10(homedir9(), ".agenticmail", "stalwart.toml");
1220
- let toml = readFileSync5(tomlPath, "utf-8");
1219
+ const tomlPath = join12(homedir11(), ".agenticmail", "stalwart.toml");
1220
+ let toml = readFileSync7(tomlPath, "utf-8");
1221
1221
  toml = toml.replace(/\n\[queue\.route\.gmail\][\s\S]*?(?=\n\[|$)/, "");
1222
1222
  toml = toml.replace(/\n\[queue\.strategy\][\s\S]*?(?=\n\[|$)/, "");
1223
1223
  const safeRouteName = routeName.replace(/[^a-zA-Z0-9_-]/g, "");
@@ -1237,7 +1237,7 @@ auth.secret = "${escapeTomlString(config.password)}"
1237
1237
  route = [ { if = "is_local_domain('', rcpt_domain)", then = "'local'" },
1238
1238
  { else = "'${safeRouteName}'" } ]
1239
1239
  `;
1240
- writeFileSync6(tomlPath, toml, "utf-8");
1240
+ writeFileSync8(tomlPath, toml, "utf-8");
1241
1241
  await this.restartContainer();
1242
1242
  }
1243
1243
  };
@@ -2846,6 +2846,18 @@ CREATE INDEX IF NOT EXISTS idx_pending_notification ON pending_outbound(notifica
2846
2846
  -- Column is optional; NULL means "no schema, accept anything" (the
2847
2847
  -- v0.8.x behaviour, fully back-compat).
2848
2848
  ALTER TABLE agent_tasks ADD COLUMN output_schema TEXT;
2849
+ `,
2850
+ "015_draft_attachments.sql": `
2851
+ -- Persist attachments alongside their draft.
2852
+ --
2853
+ -- Stored as a JSON array on the row: each entry is
2854
+ -- { filename, contentType, content (base64), size }. The web UI
2855
+ -- cap is 20 MB total per draft, which SQLite handles fine without
2856
+ -- bloating other queries \u2014 the column is only fetched on the
2857
+ -- per-draft GET (not on the list endpoint) so the Drafts sidebar
2858
+ -- stays snappy. NULL means "no attachments", fully back-compat
2859
+ -- with rows from before this migration.
2860
+ ALTER TABLE drafts ADD COLUMN attachments TEXT;
2849
2861
  `
2850
2862
  };
2851
2863
  function runMigrations(database) {
@@ -5093,12 +5105,12 @@ var GatewayManager = class {
5093
5105
  zone = await this.cfClient.createZone(domain);
5094
5106
  }
5095
5107
  const existingRecords = await this.cfClient.listDnsRecords(zone.id);
5096
- const { homedir: homedir9 } = await import("os");
5097
- const backupDir = join4(homedir9(), ".agenticmail");
5108
+ const { homedir: homedir11 } = await import("os");
5109
+ const backupDir = join4(homedir11(), ".agenticmail");
5098
5110
  const backupPath = join4(backupDir, `dns-backup-${domain}-${Date.now()}.json`);
5099
- const { writeFileSync: writeFileSync6, mkdirSync: mkdirSync7 } = await import("fs");
5100
- mkdirSync7(backupDir, { recursive: true });
5101
- writeFileSync6(backupPath, JSON.stringify({
5111
+ const { writeFileSync: writeFileSync8, mkdirSync: mkdirSync9 } = await import("fs");
5112
+ mkdirSync9(backupDir, { recursive: true });
5113
+ writeFileSync8(backupPath, JSON.stringify({
5102
5114
  domain,
5103
5115
  zoneId: zone.id,
5104
5116
  backedUpAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7280,10 +7292,301 @@ secret = "${password}"
7280
7292
  return existsSync7(configPath);
7281
7293
  }
7282
7294
  };
7295
+
7296
+ // src/threading/thread-id.ts
7297
+ import { createHash as createHash2 } from "crypto";
7298
+ function stripReplyPrefixes(subject) {
7299
+ let s = subject;
7300
+ for (; ; ) {
7301
+ const next = s.replace(/^\s*(?:re|fwd?|fw)\s*(?:\[\d+\])?\s*:\s*/i, "");
7302
+ if (next === s) break;
7303
+ s = next;
7304
+ }
7305
+ return s;
7306
+ }
7307
+ function stripCoordinationMarkers(subject) {
7308
+ return subject.replace(/\[\s*(?:final|done|closed|wrap)\s*\]/gi, " ");
7309
+ }
7310
+ function normalizeSubject(subject) {
7311
+ if (!subject) return "(no subject)";
7312
+ let s = stripReplyPrefixes(subject);
7313
+ s = stripCoordinationMarkers(s);
7314
+ s = s.replace(/\s+/g, " ").trim().toLowerCase();
7315
+ return s || "(no subject)";
7316
+ }
7317
+ function normalizeAddress(addr) {
7318
+ if (!addr) return "(unknown)";
7319
+ const m = addr.match(/<([^>]+)>/);
7320
+ const raw = m ? m[1] : addr;
7321
+ return raw.trim().toLowerCase();
7322
+ }
7323
+ function threadIdFor(input) {
7324
+ const subject = normalizeSubject(input.subject);
7325
+ return createHash2("sha256").update(subject).digest("base64url").slice(0, 16);
7326
+ }
7327
+
7328
+ // src/threading/thread-cache.ts
7329
+ import {
7330
+ existsSync as existsSync8,
7331
+ mkdirSync as mkdirSync7,
7332
+ readFileSync as readFileSync5,
7333
+ writeFileSync as writeFileSync6,
7334
+ readdirSync,
7335
+ statSync,
7336
+ rmSync,
7337
+ renameSync
7338
+ } from "fs";
7339
+ import { homedir as homedir9 } from "os";
7340
+ import { join as join10 } from "path";
7341
+ var CACHE_DIR_DEFAULT = join10(homedir9(), ".agenticmail", "thread-cache");
7342
+ var DEFAULT_K_MESSAGES = 10;
7343
+ var DEFAULT_LRU_CAP = 5e3;
7344
+ var PREVIEW_MAX_CHARS = 240;
7345
+ var ThreadCache = class {
7346
+ dir;
7347
+ k;
7348
+ lruCap;
7349
+ constructor(opts = {}) {
7350
+ this.dir = opts.cacheDir ?? CACHE_DIR_DEFAULT;
7351
+ this.k = opts.k ?? DEFAULT_K_MESSAGES;
7352
+ this.lruCap = opts.lruCap ?? DEFAULT_LRU_CAP;
7353
+ try {
7354
+ mkdirSync7(this.dir, { recursive: true });
7355
+ } catch {
7356
+ }
7357
+ }
7358
+ pathFor(threadId) {
7359
+ return join10(this.dir, `${threadId}.json`);
7360
+ }
7361
+ read(threadId) {
7362
+ const p = this.pathFor(threadId);
7363
+ if (!existsSync8(p)) return null;
7364
+ try {
7365
+ const raw = readFileSync5(p, "utf-8");
7366
+ return JSON.parse(raw);
7367
+ } catch {
7368
+ try {
7369
+ rmSync(p, { force: true });
7370
+ } catch {
7371
+ }
7372
+ return null;
7373
+ }
7374
+ }
7375
+ /**
7376
+ * Append a message to the thread's cache, pruning to the K
7377
+ * newest entries. Creates the cache entry on first write.
7378
+ *
7379
+ * `rootFromAddr` is the sender of the ROOT message on the
7380
+ * thread; on a brand-new thread this is just `env.fromAddr`,
7381
+ * on a reply it's read off the existing cache entry (callers
7382
+ * should pass the existing entry's rootFromAddr when known).
7383
+ */
7384
+ pushMessage(threadId, env, meta) {
7385
+ const existing = this.read(threadId);
7386
+ const entry = existing ? {
7387
+ ...existing,
7388
+ // We re-affirm subject + rootFromAddr to existing values —
7389
+ // the first-seen values win. Reply messages carry the
7390
+ // replier's `from`, not the original sender's, so we'd
7391
+ // corrupt the thread root if we overwrote here.
7392
+ subject: existing.subject,
7393
+ rootFromAddr: existing.rootFromAddr,
7394
+ lastUpdated: Date.now(),
7395
+ messages: dedupAndCap([env, ...existing.messages], this.k)
7396
+ } : {
7397
+ threadId,
7398
+ subject: meta.subject,
7399
+ rootFromAddr: meta.rootFromAddr,
7400
+ lastUpdated: Date.now(),
7401
+ messages: [env]
7402
+ };
7403
+ this.writeAtomic(threadId, entry);
7404
+ this.maybeEvict();
7405
+ return entry;
7406
+ }
7407
+ /** Permanently remove a thread's cache (called on [FINAL] / [DONE] / [CLOSED] / [WRAP]). */
7408
+ delete(threadId) {
7409
+ try {
7410
+ rmSync(this.pathFor(threadId), { force: true });
7411
+ } catch {
7412
+ }
7413
+ }
7414
+ /**
7415
+ * Render the cache as a compact text block for the wake prompt.
7416
+ * One line per message, newest first. Empty string when the
7417
+ * cache is empty — caller decides whether to suppress the
7418
+ * header in that case.
7419
+ */
7420
+ renderForPrompt(entry) {
7421
+ if (!entry || entry.messages.length === 0) return "";
7422
+ return entry.messages.map((m) => {
7423
+ const preview = m.preview.replace(/\s+/g, " ").slice(0, PREVIEW_MAX_CHARS);
7424
+ return `- UID ${m.uid} \xB7 ${m.from} \xB7 ${m.date} \xB7 "${m.subject}" \xB7 ${preview}`;
7425
+ }).join("\n");
7426
+ }
7427
+ writeAtomic(threadId, entry) {
7428
+ const p = this.pathFor(threadId);
7429
+ const tmp = `${p}.tmp`;
7430
+ writeFileSync6(tmp, JSON.stringify(entry), "utf-8");
7431
+ renameSync(tmp, p);
7432
+ }
7433
+ /**
7434
+ * Best-effort LRU eviction. Runs at most every 256 writes (we
7435
+ * don't track a precise counter — `Math.random()` sampling keeps
7436
+ * the write path cheap). When the directory has more files than
7437
+ * `lruCap`, sort by mtime ascending and delete the oldest 10%.
7438
+ */
7439
+ maybeEvict() {
7440
+ if (Math.random() > 1 / 256) return;
7441
+ let files;
7442
+ try {
7443
+ files = readdirSync(this.dir).filter((f) => f.endsWith(".json"));
7444
+ } catch {
7445
+ return;
7446
+ }
7447
+ if (files.length <= this.lruCap) return;
7448
+ const stats = files.map((f) => {
7449
+ const p = join10(this.dir, f);
7450
+ try {
7451
+ return { p, mtime: statSync(p).mtimeMs };
7452
+ } catch {
7453
+ return { p, mtime: 0 };
7454
+ }
7455
+ });
7456
+ stats.sort((a, b) => a.mtime - b.mtime);
7457
+ const dropCount = Math.max(1, Math.floor(this.lruCap * 0.1));
7458
+ for (let i = 0; i < dropCount; i++) {
7459
+ try {
7460
+ rmSync(stats[i].p, { force: true });
7461
+ } catch {
7462
+ }
7463
+ }
7464
+ }
7465
+ };
7466
+ function dedupAndCap(messages, k) {
7467
+ const seen = /* @__PURE__ */ new Set();
7468
+ const out = [];
7469
+ for (const m of messages) {
7470
+ if (seen.has(m.uid)) continue;
7471
+ seen.add(m.uid);
7472
+ out.push(m);
7473
+ if (out.length >= k) break;
7474
+ }
7475
+ return out;
7476
+ }
7477
+
7478
+ // src/threading/agent-memory.ts
7479
+ import {
7480
+ existsSync as existsSync9,
7481
+ mkdirSync as mkdirSync8,
7482
+ readFileSync as readFileSync6,
7483
+ writeFileSync as writeFileSync7,
7484
+ rmSync as rmSync2,
7485
+ renameSync as renameSync2
7486
+ } from "fs";
7487
+ import { homedir as homedir10 } from "os";
7488
+ import { join as join11 } from "path";
7489
+ var MEMORY_DIR_DEFAULT = join11(homedir10(), ".agenticmail", "agent-memory");
7490
+ var AgentMemoryStore = class {
7491
+ dir;
7492
+ constructor(opts = {}) {
7493
+ this.dir = opts.memoryDir ?? MEMORY_DIR_DEFAULT;
7494
+ try {
7495
+ mkdirSync8(this.dir, { recursive: true });
7496
+ } catch {
7497
+ }
7498
+ }
7499
+ dirFor(agentId) {
7500
+ return join11(this.dir, sanitizeId(agentId));
7501
+ }
7502
+ pathFor(agentId, threadId) {
7503
+ return join11(this.dirFor(agentId), `${sanitizeId(threadId)}.md`);
7504
+ }
7505
+ read(agentId, threadId) {
7506
+ const p = this.pathFor(agentId, threadId);
7507
+ if (!existsSync9(p)) return null;
7508
+ try {
7509
+ const raw = readFileSync6(p, "utf-8");
7510
+ const parsed = parse(raw);
7511
+ return { ...parsed, raw };
7512
+ } catch {
7513
+ return null;
7514
+ }
7515
+ }
7516
+ write(agentId, threadId, fields) {
7517
+ const agentDir = this.dirFor(agentId);
7518
+ try {
7519
+ mkdirSync8(agentDir, { recursive: true });
7520
+ } catch {
7521
+ }
7522
+ const body = render({ ...fields, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
7523
+ const p = this.pathFor(agentId, threadId);
7524
+ const tmp = `${p}.tmp`;
7525
+ writeFileSync7(tmp, body, "utf-8");
7526
+ renameSync2(tmp, p);
7527
+ }
7528
+ delete(agentId, threadId) {
7529
+ try {
7530
+ rmSync2(this.pathFor(agentId, threadId), { force: true });
7531
+ } catch {
7532
+ }
7533
+ }
7534
+ /** Render an agent's memory for injection into a wake prompt.
7535
+ * Returns the raw markdown if present; empty string when there's
7536
+ * no prior memory (the caller decides whether to suppress the
7537
+ * whole "Your own memory" block). */
7538
+ renderForPrompt(memory) {
7539
+ if (!memory) return "";
7540
+ return memory.raw;
7541
+ }
7542
+ };
7543
+ function sanitizeId(id) {
7544
+ return id.replace(/[^a-zA-Z0-9._-]/g, "_");
7545
+ }
7546
+ function render(fields) {
7547
+ const fm = ["---"];
7548
+ fm.push(`updated_at: ${fields.updatedAt}`);
7549
+ if (typeof fields.lastUid === "number") fm.push(`last_uid: ${fields.lastUid}`);
7550
+ fm.push("---", "");
7551
+ const sections = [];
7552
+ if (fields.summary && fields.summary.trim()) {
7553
+ sections.push(fields.summary.trim());
7554
+ }
7555
+ if (fields.commitments && fields.commitments.length > 0) {
7556
+ sections.push(`### Commitments
7557
+ ${fields.commitments.map((c) => `- ${c}`).join("\n")}`);
7558
+ }
7559
+ if (fields.openQuestions && fields.openQuestions.length > 0) {
7560
+ sections.push(`### Open
7561
+ ${fields.openQuestions.map((q) => `- ${q}`).join("\n")}`);
7562
+ }
7563
+ if (fields.lastAction && fields.lastAction.trim()) {
7564
+ sections.push(`### Last action
7565
+ ${fields.lastAction.trim()}`);
7566
+ }
7567
+ return fm.join("\n") + sections.join("\n\n") + "\n";
7568
+ }
7569
+ function parse(raw) {
7570
+ const out = {};
7571
+ const m = raw.match(/^---\n([\s\S]*?)\n---\n/);
7572
+ if (m) {
7573
+ for (const line of m[1].split("\n")) {
7574
+ const kv = line.match(/^(\w+):\s*(.*)$/);
7575
+ if (!kv) continue;
7576
+ if (kv[1] === "updated_at") out.updatedAt = kv[2].trim();
7577
+ else if (kv[1] === "last_uid") {
7578
+ const n = parseInt(kv[2], 10);
7579
+ if (!isNaN(n)) out.lastUid = n;
7580
+ }
7581
+ }
7582
+ }
7583
+ return out;
7584
+ }
7283
7585
  export {
7284
7586
  AGENT_ROLES,
7285
7587
  AccountManager,
7286
7588
  AgentDeletionService,
7589
+ AgentMemoryStore,
7287
7590
  AgenticMailClient,
7288
7591
  CloudflareClient,
7289
7592
  DEFAULT_AGENT_NAME,
@@ -7307,6 +7610,7 @@ export {
7307
7610
  SmsManager,
7308
7611
  SmsPoller,
7309
7612
  StalwartAdmin,
7613
+ ThreadCache,
7310
7614
  TunnelManager,
7311
7615
  WARNING_THRESHOLD,
7312
7616
  buildInboundSecurityAdvisory,
@@ -7321,7 +7625,9 @@ export {
7321
7625
  getDatabase,
7322
7626
  isInternalEmail,
7323
7627
  isValidPhoneNumber,
7628
+ normalizeAddress,
7324
7629
  normalizePhoneNumber,
7630
+ normalizeSubject,
7325
7631
  parseEmail,
7326
7632
  parseGoogleVoiceSms,
7327
7633
  recordToolCall,
@@ -7331,5 +7637,6 @@ export {
7331
7637
  scanOutboundEmail,
7332
7638
  scoreEmail,
7333
7639
  setTelemetryVersion,
7334
- startRelayBridge
7640
+ startRelayBridge,
7641
+ threadIdFor
7335
7642
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/core",
3
- "version": "0.7.5",
3
+ "version": "0.9.0",
4
4
  "description": "Core SDK for AgenticMail — email, SMS, and phone number access for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",