@agenticmail/core 0.7.6 → 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 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
  };
@@ -1275,7 +1275,11 @@ function rowToAgent(row) {
1275
1275
  createdAt: row.created_at,
1276
1276
  updatedAt: row.updated_at,
1277
1277
  metadata,
1278
- role: row.role || "secretary"
1278
+ role: row.role || "secretary",
1279
+ // Old rows (pre-migration-016) have undefined `wake_on_cc`;
1280
+ // treat that as the default-true (respect sender's wake list
1281
+ // as-is). Only explicit 0 disables CC wakes for this agent.
1282
+ wakeOnCc: row.wake_on_cc !== void 0 ? row.wake_on_cc !== 0 : true
1279
1283
  };
1280
1284
  }
1281
1285
  var AccountManager = class {
@@ -2858,6 +2862,15 @@ ALTER TABLE agent_tasks ADD COLUMN output_schema TEXT;
2858
2862
  -- stays snappy. NULL means "no attachments", fully back-compat
2859
2863
  -- with rows from before this migration.
2860
2864
  ALTER TABLE drafts ADD COLUMN attachments TEXT;
2865
+ `,
2866
+ "016_agent_wake_on_cc.sql": `
2867
+ -- Per-agent wake preference. When 0, the dispatcher SKIPS this
2868
+ -- agent on every CC-only delivery (the agent is on Cc/Bcc but
2869
+ -- not To), regardless of what the sender passed as the wake
2870
+ -- argument. This is the "coder agent, only wake me when
2871
+ -- explicitly named" preference from the wake-thrash feedback.
2872
+ -- Defaults to 1 (respect the senders wake list as-is).
2873
+ ALTER TABLE agents ADD COLUMN wake_on_cc INTEGER NOT NULL DEFAULT 1;
2861
2874
  `
2862
2875
  };
2863
2876
  function runMigrations(database) {
@@ -5105,12 +5118,12 @@ var GatewayManager = class {
5105
5118
  zone = await this.cfClient.createZone(domain);
5106
5119
  }
5107
5120
  const existingRecords = await this.cfClient.listDnsRecords(zone.id);
5108
- const { homedir: homedir9 } = await import("os");
5109
- const backupDir = join4(homedir9(), ".agenticmail");
5121
+ const { homedir: homedir11 } = await import("os");
5122
+ const backupDir = join4(homedir11(), ".agenticmail");
5110
5123
  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({
5124
+ const { writeFileSync: writeFileSync8, mkdirSync: mkdirSync9 } = await import("fs");
5125
+ mkdirSync9(backupDir, { recursive: true });
5126
+ writeFileSync8(backupPath, JSON.stringify({
5114
5127
  domain,
5115
5128
  zoneId: zone.id,
5116
5129
  backedUpAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7292,10 +7305,301 @@ secret = "${password}"
7292
7305
  return existsSync7(configPath);
7293
7306
  }
7294
7307
  };
7308
+
7309
+ // src/threading/thread-id.ts
7310
+ import { createHash as createHash2 } from "crypto";
7311
+ function stripReplyPrefixes(subject) {
7312
+ let s = subject;
7313
+ for (; ; ) {
7314
+ const next = s.replace(/^\s*(?:re|fwd?|fw)\s*(?:\[\d+\])?\s*:\s*/i, "");
7315
+ if (next === s) break;
7316
+ s = next;
7317
+ }
7318
+ return s;
7319
+ }
7320
+ function stripCoordinationMarkers(subject) {
7321
+ return subject.replace(/\[\s*(?:final|done|closed|wrap)\s*\]/gi, " ");
7322
+ }
7323
+ function normalizeSubject(subject) {
7324
+ if (!subject) return "(no subject)";
7325
+ let s = stripReplyPrefixes(subject);
7326
+ s = stripCoordinationMarkers(s);
7327
+ s = s.replace(/\s+/g, " ").trim().toLowerCase();
7328
+ return s || "(no subject)";
7329
+ }
7330
+ function normalizeAddress(addr) {
7331
+ if (!addr) return "(unknown)";
7332
+ const m = addr.match(/<([^>]+)>/);
7333
+ const raw = m ? m[1] : addr;
7334
+ return raw.trim().toLowerCase();
7335
+ }
7336
+ function threadIdFor(input) {
7337
+ const subject = normalizeSubject(input.subject);
7338
+ return createHash2("sha256").update(subject).digest("base64url").slice(0, 16);
7339
+ }
7340
+
7341
+ // src/threading/thread-cache.ts
7342
+ import {
7343
+ existsSync as existsSync8,
7344
+ mkdirSync as mkdirSync7,
7345
+ readFileSync as readFileSync5,
7346
+ writeFileSync as writeFileSync6,
7347
+ readdirSync,
7348
+ statSync,
7349
+ rmSync,
7350
+ renameSync
7351
+ } from "fs";
7352
+ import { homedir as homedir9 } from "os";
7353
+ import { join as join10 } from "path";
7354
+ var CACHE_DIR_DEFAULT = join10(homedir9(), ".agenticmail", "thread-cache");
7355
+ var DEFAULT_K_MESSAGES = 10;
7356
+ var DEFAULT_LRU_CAP = 5e3;
7357
+ var PREVIEW_MAX_CHARS = 240;
7358
+ var ThreadCache = class {
7359
+ dir;
7360
+ k;
7361
+ lruCap;
7362
+ constructor(opts = {}) {
7363
+ this.dir = opts.cacheDir ?? CACHE_DIR_DEFAULT;
7364
+ this.k = opts.k ?? DEFAULT_K_MESSAGES;
7365
+ this.lruCap = opts.lruCap ?? DEFAULT_LRU_CAP;
7366
+ try {
7367
+ mkdirSync7(this.dir, { recursive: true });
7368
+ } catch {
7369
+ }
7370
+ }
7371
+ pathFor(threadId) {
7372
+ return join10(this.dir, `${threadId}.json`);
7373
+ }
7374
+ read(threadId) {
7375
+ const p = this.pathFor(threadId);
7376
+ if (!existsSync8(p)) return null;
7377
+ try {
7378
+ const raw = readFileSync5(p, "utf-8");
7379
+ return JSON.parse(raw);
7380
+ } catch {
7381
+ try {
7382
+ rmSync(p, { force: true });
7383
+ } catch {
7384
+ }
7385
+ return null;
7386
+ }
7387
+ }
7388
+ /**
7389
+ * Append a message to the thread's cache, pruning to the K
7390
+ * newest entries. Creates the cache entry on first write.
7391
+ *
7392
+ * `rootFromAddr` is the sender of the ROOT message on the
7393
+ * thread; on a brand-new thread this is just `env.fromAddr`,
7394
+ * on a reply it's read off the existing cache entry (callers
7395
+ * should pass the existing entry's rootFromAddr when known).
7396
+ */
7397
+ pushMessage(threadId, env, meta) {
7398
+ const existing = this.read(threadId);
7399
+ const entry = existing ? {
7400
+ ...existing,
7401
+ // We re-affirm subject + rootFromAddr to existing values —
7402
+ // the first-seen values win. Reply messages carry the
7403
+ // replier's `from`, not the original sender's, so we'd
7404
+ // corrupt the thread root if we overwrote here.
7405
+ subject: existing.subject,
7406
+ rootFromAddr: existing.rootFromAddr,
7407
+ lastUpdated: Date.now(),
7408
+ messages: dedupAndCap([env, ...existing.messages], this.k)
7409
+ } : {
7410
+ threadId,
7411
+ subject: meta.subject,
7412
+ rootFromAddr: meta.rootFromAddr,
7413
+ lastUpdated: Date.now(),
7414
+ messages: [env]
7415
+ };
7416
+ this.writeAtomic(threadId, entry);
7417
+ this.maybeEvict();
7418
+ return entry;
7419
+ }
7420
+ /** Permanently remove a thread's cache (called on [FINAL] / [DONE] / [CLOSED] / [WRAP]). */
7421
+ delete(threadId) {
7422
+ try {
7423
+ rmSync(this.pathFor(threadId), { force: true });
7424
+ } catch {
7425
+ }
7426
+ }
7427
+ /**
7428
+ * Render the cache as a compact text block for the wake prompt.
7429
+ * One line per message, newest first. Empty string when the
7430
+ * cache is empty — caller decides whether to suppress the
7431
+ * header in that case.
7432
+ */
7433
+ renderForPrompt(entry) {
7434
+ if (!entry || entry.messages.length === 0) return "";
7435
+ return entry.messages.map((m) => {
7436
+ const preview = m.preview.replace(/\s+/g, " ").slice(0, PREVIEW_MAX_CHARS);
7437
+ return `- UID ${m.uid} \xB7 ${m.from} \xB7 ${m.date} \xB7 "${m.subject}" \xB7 ${preview}`;
7438
+ }).join("\n");
7439
+ }
7440
+ writeAtomic(threadId, entry) {
7441
+ const p = this.pathFor(threadId);
7442
+ const tmp = `${p}.tmp`;
7443
+ writeFileSync6(tmp, JSON.stringify(entry), "utf-8");
7444
+ renameSync(tmp, p);
7445
+ }
7446
+ /**
7447
+ * Best-effort LRU eviction. Runs at most every 256 writes (we
7448
+ * don't track a precise counter — `Math.random()` sampling keeps
7449
+ * the write path cheap). When the directory has more files than
7450
+ * `lruCap`, sort by mtime ascending and delete the oldest 10%.
7451
+ */
7452
+ maybeEvict() {
7453
+ if (Math.random() > 1 / 256) return;
7454
+ let files;
7455
+ try {
7456
+ files = readdirSync(this.dir).filter((f) => f.endsWith(".json"));
7457
+ } catch {
7458
+ return;
7459
+ }
7460
+ if (files.length <= this.lruCap) return;
7461
+ const stats = files.map((f) => {
7462
+ const p = join10(this.dir, f);
7463
+ try {
7464
+ return { p, mtime: statSync(p).mtimeMs };
7465
+ } catch {
7466
+ return { p, mtime: 0 };
7467
+ }
7468
+ });
7469
+ stats.sort((a, b) => a.mtime - b.mtime);
7470
+ const dropCount = Math.max(1, Math.floor(this.lruCap * 0.1));
7471
+ for (let i = 0; i < dropCount; i++) {
7472
+ try {
7473
+ rmSync(stats[i].p, { force: true });
7474
+ } catch {
7475
+ }
7476
+ }
7477
+ }
7478
+ };
7479
+ function dedupAndCap(messages, k) {
7480
+ const seen = /* @__PURE__ */ new Set();
7481
+ const out = [];
7482
+ for (const m of messages) {
7483
+ if (seen.has(m.uid)) continue;
7484
+ seen.add(m.uid);
7485
+ out.push(m);
7486
+ if (out.length >= k) break;
7487
+ }
7488
+ return out;
7489
+ }
7490
+
7491
+ // src/threading/agent-memory.ts
7492
+ import {
7493
+ existsSync as existsSync9,
7494
+ mkdirSync as mkdirSync8,
7495
+ readFileSync as readFileSync6,
7496
+ writeFileSync as writeFileSync7,
7497
+ rmSync as rmSync2,
7498
+ renameSync as renameSync2
7499
+ } from "fs";
7500
+ import { homedir as homedir10 } from "os";
7501
+ import { join as join11 } from "path";
7502
+ var MEMORY_DIR_DEFAULT = join11(homedir10(), ".agenticmail", "agent-memory");
7503
+ var AgentMemoryStore = class {
7504
+ dir;
7505
+ constructor(opts = {}) {
7506
+ this.dir = opts.memoryDir ?? MEMORY_DIR_DEFAULT;
7507
+ try {
7508
+ mkdirSync8(this.dir, { recursive: true });
7509
+ } catch {
7510
+ }
7511
+ }
7512
+ dirFor(agentId) {
7513
+ return join11(this.dir, sanitizeId(agentId));
7514
+ }
7515
+ pathFor(agentId, threadId) {
7516
+ return join11(this.dirFor(agentId), `${sanitizeId(threadId)}.md`);
7517
+ }
7518
+ read(agentId, threadId) {
7519
+ const p = this.pathFor(agentId, threadId);
7520
+ if (!existsSync9(p)) return null;
7521
+ try {
7522
+ const raw = readFileSync6(p, "utf-8");
7523
+ const parsed = parse(raw);
7524
+ return { ...parsed, raw };
7525
+ } catch {
7526
+ return null;
7527
+ }
7528
+ }
7529
+ write(agentId, threadId, fields) {
7530
+ const agentDir = this.dirFor(agentId);
7531
+ try {
7532
+ mkdirSync8(agentDir, { recursive: true });
7533
+ } catch {
7534
+ }
7535
+ const body = render({ ...fields, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
7536
+ const p = this.pathFor(agentId, threadId);
7537
+ const tmp = `${p}.tmp`;
7538
+ writeFileSync7(tmp, body, "utf-8");
7539
+ renameSync2(tmp, p);
7540
+ }
7541
+ delete(agentId, threadId) {
7542
+ try {
7543
+ rmSync2(this.pathFor(agentId, threadId), { force: true });
7544
+ } catch {
7545
+ }
7546
+ }
7547
+ /** Render an agent's memory for injection into a wake prompt.
7548
+ * Returns the raw markdown if present; empty string when there's
7549
+ * no prior memory (the caller decides whether to suppress the
7550
+ * whole "Your own memory" block). */
7551
+ renderForPrompt(memory) {
7552
+ if (!memory) return "";
7553
+ return memory.raw;
7554
+ }
7555
+ };
7556
+ function sanitizeId(id) {
7557
+ return id.replace(/[^a-zA-Z0-9._-]/g, "_");
7558
+ }
7559
+ function render(fields) {
7560
+ const fm = ["---"];
7561
+ fm.push(`updated_at: ${fields.updatedAt}`);
7562
+ if (typeof fields.lastUid === "number") fm.push(`last_uid: ${fields.lastUid}`);
7563
+ fm.push("---", "");
7564
+ const sections = [];
7565
+ if (fields.summary && fields.summary.trim()) {
7566
+ sections.push(fields.summary.trim());
7567
+ }
7568
+ if (fields.commitments && fields.commitments.length > 0) {
7569
+ sections.push(`### Commitments
7570
+ ${fields.commitments.map((c) => `- ${c}`).join("\n")}`);
7571
+ }
7572
+ if (fields.openQuestions && fields.openQuestions.length > 0) {
7573
+ sections.push(`### Open
7574
+ ${fields.openQuestions.map((q) => `- ${q}`).join("\n")}`);
7575
+ }
7576
+ if (fields.lastAction && fields.lastAction.trim()) {
7577
+ sections.push(`### Last action
7578
+ ${fields.lastAction.trim()}`);
7579
+ }
7580
+ return fm.join("\n") + sections.join("\n\n") + "\n";
7581
+ }
7582
+ function parse(raw) {
7583
+ const out = {};
7584
+ const m = raw.match(/^---\n([\s\S]*?)\n---\n/);
7585
+ if (m) {
7586
+ for (const line of m[1].split("\n")) {
7587
+ const kv = line.match(/^(\w+):\s*(.*)$/);
7588
+ if (!kv) continue;
7589
+ if (kv[1] === "updated_at") out.updatedAt = kv[2].trim();
7590
+ else if (kv[1] === "last_uid") {
7591
+ const n = parseInt(kv[2], 10);
7592
+ if (!isNaN(n)) out.lastUid = n;
7593
+ }
7594
+ }
7595
+ }
7596
+ return out;
7597
+ }
7295
7598
  export {
7296
7599
  AGENT_ROLES,
7297
7600
  AccountManager,
7298
7601
  AgentDeletionService,
7602
+ AgentMemoryStore,
7299
7603
  AgenticMailClient,
7300
7604
  CloudflareClient,
7301
7605
  DEFAULT_AGENT_NAME,
@@ -7319,6 +7623,7 @@ export {
7319
7623
  SmsManager,
7320
7624
  SmsPoller,
7321
7625
  StalwartAdmin,
7626
+ ThreadCache,
7322
7627
  TunnelManager,
7323
7628
  WARNING_THRESHOLD,
7324
7629
  buildInboundSecurityAdvisory,
@@ -7333,7 +7638,9 @@ export {
7333
7638
  getDatabase,
7334
7639
  isInternalEmail,
7335
7640
  isValidPhoneNumber,
7641
+ normalizeAddress,
7336
7642
  normalizePhoneNumber,
7643
+ normalizeSubject,
7337
7644
  parseEmail,
7338
7645
  parseGoogleVoiceSms,
7339
7646
  recordToolCall,
@@ -7343,5 +7650,6 @@ export {
7343
7650
  scanOutboundEmail,
7344
7651
  scoreEmail,
7345
7652
  setTelemetryVersion,
7346
- startRelayBridge
7653
+ startRelayBridge,
7654
+ threadIdFor
7347
7655
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/core",
3
- "version": "0.7.6",
3
+ "version": "0.9.1",
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",