UNPKG

@essentialai/cogent-bridge

Version:

Cogent Bridge — cross-agent comms for Claude Code, OpenAI Codex and Slack. Codex users: install with `curl -fsSL https://cogent.tools/install.sh | sh`, then start with `cogent-codex` for real-time peer wake.

75 lines • 3.55 kB
import fs from "node:fs/promises"; import path from "node:path"; /** In-memory sender: records what was sent. The test double the tool/service drives. */ export class InMemoryMailSender { sent = []; async send(mail) { this.sent.push(mail); return { messageId: `inmem-${this.sent.length}` }; } } /** * Real SMTP sender. nodemailer is imported LAZILY (dynamic import) so it never loads on * the bridge's cold-start path — only when the agent actually sends mail. `transportFactory` * is an injection seam for tests (default builds a real nodemailer SMTP transport from the creds). */ export class NodemailerMailSender { creds; transportFactory; constructor(creds, transportFactory) { this.creds = creds; this.transportFactory = transportFactory; } async send(mail) { const transport = this.transportFactory ? await this.transportFactory() : await this.defaultTransport(); const info = await transport.sendMail({ from: mail.from, to: mail.to, subject: mail.subject, text: mail.text, attachments: mail.attachments.map((a) => ({ filename: a.filename, content: a.content })), }); return { messageId: info.messageId ?? "" }; } async defaultTransport() { // 🔴 LITERAL specifier, deliberately. This was a VARIABLE ("const specifier = \"nodemailer\"") // to keep nodemailer out of the bundle — but esbuild cannot analyse a non-literal, so instead // of being *deferred* it was left as a RUNTIME import. The plugin packages declare no // dependencies and ship no node_modules, so every plugin user got // `Cannot find package 'nodemailer'` the moment they sent mail. Reported by a real agent and // reproduced 2026-09-05; only the npx package (which declares nodemailer) ever worked. // // A literal keeps the laziness the variable was reaching for — esbuild inlines the module but // still evaluates it on first call, so it stays off the cold-start path — AND makes the bundle // self-contained. Guarded by scripts/bundle-selfcontained-test.mjs. const nodemailer = (await import("nodemailer")); return nodemailer.createTransport({ host: this.creds.smtpHost, port: this.creds.smtpPort, secure: this.creds.smtpPort === 465, // implicit TLS on 465, STARTTLS on 587 auth: { user: this.creds.address, pass: this.creds.password }, }); } } /** * Compose and send a mail from the agent's mailbox: reads each attachment from its local path * (streaming the bytes into a Buffer), builds the {@link OutgoingMail}, and sends via the port. * A missing/unreadable attachment path fails loud (the caller surfaces it as errorResult) rather * than silently dropping the file. Returns the message id + the sending address. */ export async function buildAndSendMail(sender, creds, params) { const attachments = []; for (const p of params.attachmentPaths ?? []) { const content = await fs.readFile(p); // throws (ENOENT/EACCES) → fail loud, not a silent drop attachments.push({ filename: path.basename(p), content }); } const { messageId } = await sender.send({ from: creds.address, to: params.to, subject: params.subject, text: params.text, attachments, }); return { messageId, from: creds.address, to: params.to }; } //# sourceMappingURL=mail-sender.js.map