Files
moemail/packages/core/src/poll.ts
ty b99b872791 feat(cli,mcp): extract @moemail/core and add MCP server; release 1.0.0
Extract the HTTP client and config into a new @moemail/core package shared
by the CLI and a new @moemail/mcp server, so both frontends talk to the same
MoeMail API through one code path.

- core: api client (now throws typed ConfigError/AuthError/PermissionError/
  QuotaError instead of process.exit), config, msToIso, and a transport-
  agnostic pollForNewMessage helper.
- cli: consume @moemail/core; route command errors through a shared fail()
  that preserves exit codes (config/auth = 2, else = 1). Bump to 1.0.0.
- mcp: new stdio MCP server exposing 8 tools (create/list/read/wait/send/
  delete); wait_for_email is bounded and returns a timeout status to retry.
  Configured via MOEMAIL_API_KEY / MOEMAIL_API_URL env. Release 1.0.0.

Docs:
- Fix packages/cli/README.md (config set, send --content not --body, full
  flag table).
- Add MCP section to both root READMEs; complete the CLI command list
  (send, list, message-level delete).
- SKILL.md: --json works before or after the subcommand.
- Ignore bun.lock in package gitignores.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 23:52:35 +08:00

61 lines
1.6 KiB
TypeScript

import { api } from "./api.js";
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export interface NewMessage {
id: string;
from_address: string;
subject: string;
received_at?: number;
}
export interface PollResult {
status: "received" | "timeout";
message?: NewMessage;
elapsedSec: number;
}
/**
* Poll a mailbox until a message that wasn't present at the start arrives, or
* the timeout elapses. Transport-agnostic: the CLI wraps it with stderr
* progress, the MCP server returns the result as structured JSON.
*
* On timeout this resolves with `status: "timeout"` rather than throwing, so an
* MCP client can simply call the tool again to keep waiting.
*/
export async function pollForNewMessage(
emailId: string,
opts: {
timeoutMs: number;
intervalMs: number;
onTick?: (elapsedSec: number) => void;
},
): Promise<PollResult> {
const initial = (await api.listMessages(emailId)) as any;
const knownIds = new Set<string>(initial.messages.map((m: any) => m.id));
const startTime = Date.now();
while (true) {
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
if (elapsedSec >= opts.timeoutMs / 1000) {
return { status: "timeout", elapsedSec };
}
opts.onTick?.(elapsedSec);
await sleep(opts.intervalMs);
const current = (await api.listMessages(emailId)) as any;
const fresh = current.messages.filter((m: any) => !knownIds.has(m.id));
if (fresh.length > 0) {
return {
status: "received",
message: fresh[0],
elapsedSec: Math.floor((Date.now() - startTime) / 1000),
};
}
}
}