@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
JavaScript
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