@agenticmail/core 0.7.6 → 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
  };
@@ -5105,12 +5105,12 @@ var GatewayManager = class {
5105
5105
  zone = await this.cfClient.createZone(domain);
5106
5106
  }
5107
5107
  const existingRecords = await this.cfClient.listDnsRecords(zone.id);
5108
- const { homedir: homedir9 } = await import("os");
5109
- const backupDir = join4(homedir9(), ".agenticmail");
5108
+ const { homedir: homedir11 } = await import("os");
5109
+ const backupDir = join4(homedir11(), ".agenticmail");
5110
5110
  const backupPath = join4(backupDir, `dns-backup-${domain}-${Date.now()}.json`);
5111
- const { writeFileSync: writeFileSync6, mkdirSync: mkdirSync7 } = await import("fs");
5112
- mkdirSync7(backupDir, { recursive: true });
5113
- writeFileSync6(backupPath, JSON.stringify({
5111
+ const { writeFileSync: writeFileSync8, mkdirSync: mkdirSync9 } = await import("fs");
5112
+ mkdirSync9(backupDir, { recursive: true });
5113
+ writeFileSync8(backupPath, JSON.stringify({
5114
5114
  domain,
5115
5115
  zoneId: zone.id,
5116
5116
  backedUpAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7292,10 +7292,301 @@ secret = "${password}"
7292
7292
  return existsSync7(configPath);
7293
7293
  }
7294
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
+ }
7295
7585
  export {
7296
7586
  AGENT_ROLES,
7297
7587
  AccountManager,
7298
7588
  AgentDeletionService,
7589
+ AgentMemoryStore,
7299
7590
  AgenticMailClient,
7300
7591
  CloudflareClient,
7301
7592
  DEFAULT_AGENT_NAME,
@@ -7319,6 +7610,7 @@ export {
7319
7610
  SmsManager,
7320
7611
  SmsPoller,
7321
7612
  StalwartAdmin,
7613
+ ThreadCache,
7322
7614
  TunnelManager,
7323
7615
  WARNING_THRESHOLD,
7324
7616
  buildInboundSecurityAdvisory,
@@ -7333,7 +7625,9 @@ export {
7333
7625
  getDatabase,
7334
7626
  isInternalEmail,
7335
7627
  isValidPhoneNumber,
7628
+ normalizeAddress,
7336
7629
  normalizePhoneNumber,
7630
+ normalizeSubject,
7337
7631
  parseEmail,
7338
7632
  parseGoogleVoiceSms,
7339
7633
  recordToolCall,
@@ -7343,5 +7637,6 @@ export {
7343
7637
  scanOutboundEmail,
7344
7638
  scoreEmail,
7345
7639
  setTelemetryVersion,
7346
- startRelayBridge
7640
+ startRelayBridge,
7641
+ threadIdFor
7347
7642
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/core",
3
- "version": "0.7.6",
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",