mirror of
https://github.com/beilunyang/moemail.git
synced 2026-04-22 23:07:25 +08:00
feat(cli): add MoeMail AI agent skills
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: moemail
|
||||
description: Use when working in this repository and an AI agent needs a temporary/disposable email address through the MoeMail CLI.
|
||||
---
|
||||
|
||||
# MoeMail for This Repository
|
||||
|
||||
## When to use
|
||||
|
||||
Use this skill when you are inside the `moemail` repository and need to create inboxes, wait for messages, read message content, or send test emails with the MoeMail CLI.
|
||||
|
||||
## Install
|
||||
|
||||
Install the MoeMail CLI globally when it is not already available:
|
||||
|
||||
```bash
|
||||
npm i -g @moemail/cli
|
||||
```
|
||||
|
||||
Then confirm the binary is available:
|
||||
|
||||
```bash
|
||||
moemail --help
|
||||
```
|
||||
|
||||
## Preferred CLI
|
||||
|
||||
Prefer the installed `moemail` CLI when it is available:
|
||||
|
||||
```bash
|
||||
CLI="moemail"
|
||||
```
|
||||
|
||||
If the global CLI is unavailable and you are working inside this repository, fall back to the repo-local build:
|
||||
|
||||
```bash
|
||||
CLI="node packages/cli/dist/index.js"
|
||||
```
|
||||
|
||||
If `packages/cli/dist/index.js` is missing or stale, rebuild it first:
|
||||
|
||||
```bash
|
||||
(cd packages/cli && bun run build)
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
For local development against this repository:
|
||||
|
||||
```bash
|
||||
$CLI config set api-url http://localhost:3000
|
||||
$CLI config set api-key YOUR_API_KEY
|
||||
```
|
||||
|
||||
For the hosted service, use `https://moemail.app` instead.
|
||||
|
||||
You can also use environment variables: `MOEMAIL_API_URL`, `MOEMAIL_API_KEY`.
|
||||
|
||||
## Core workflow
|
||||
|
||||
```bash
|
||||
RESULT=$($CLI --json create --expiry 1h)
|
||||
ID=$(echo "$RESULT" | jq -r '.id')
|
||||
EMAIL=$(echo "$RESULT" | jq -r '.address')
|
||||
|
||||
MSG=$($CLI --json wait --email-id "$ID" --timeout 120)
|
||||
MSG_ID=$(echo "$MSG" | jq -r '.messageId')
|
||||
|
||||
$CLI --json read --email-id "$ID" --message-id "$MSG_ID"
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Required options | Notes |
|
||||
|---------|------------------|-------|
|
||||
| `config set` | `<key> <value>` | keys: `api-url`, `api-key` |
|
||||
| `create` | - | `--name`, `--domain`, `--expiry` |
|
||||
| `list` | - | `--email-id`, `--cursor` |
|
||||
| `wait` | `--email-id` | `--timeout`, `--interval` |
|
||||
| `read` | `--email-id`, `--message-id` | `--format text|html` |
|
||||
| `send` | `--email-id`, `--to`, `--subject`, `--content` | - |
|
||||
| `delete` | `--email-id` | - |
|
||||
|
||||
## Important details
|
||||
|
||||
- Put `--json` before the subcommand.
|
||||
- Call `create` once and parse both `id` and `address` from the same JSON result.
|
||||
- Check both `content` and `html` when reading HTML-heavy messages.
|
||||
@@ -583,6 +583,19 @@ MSG_ID=$(echo $MSG | jq -r '.messageId')
|
||||
CONTENT=$(moemail read --email-id $EMAIL_ID --message-id $MSG_ID --json)
|
||||
```
|
||||
|
||||
### AI Agent Skill
|
||||
|
||||
Install the built-in skill so AI agents (Claude Code, Codex, etc.) automatically know how to use MoeMail:
|
||||
|
||||
```bash
|
||||
# Auto-detect installed agent platforms and install
|
||||
moemail skill install
|
||||
|
||||
# Or specify a platform
|
||||
moemail skill install --platform claude
|
||||
moemail skill install --platform codex
|
||||
```
|
||||
|
||||
For full documentation, see [packages/cli/README.md](packages/cli/README.md).
|
||||
|
||||
## Environment Variables
|
||||
|
||||
@@ -36,6 +36,7 @@ moemail wait --email-id <email_id> --timeout 120
|
||||
| `read` | Read email message content | `--email-id <id>`, `--message-id <id>`, `--json` |
|
||||
| `send` | Send email from temporary address | `--email-id <id>`, `--to <address>`, `--subject <text>`, `--body <text>`, `--json` |
|
||||
| `delete` | Delete temporary email | `--email-id <id>` |
|
||||
| `skill install` | Install AI agent skill | `--platform <claude\|codex\|all>` |
|
||||
|
||||
## Agent Workflow Example
|
||||
|
||||
@@ -62,6 +63,24 @@ CONTENT=$(moemail read --email-id $EMAIL_ID --message-id $MSG_ID --json)
|
||||
moemail delete --email-id $EMAIL_ID
|
||||
```
|
||||
|
||||
## AI Agent Skill
|
||||
|
||||
The CLI ships with a built-in skill file that teaches AI agents how to use MoeMail. Install it to your agent platform:
|
||||
|
||||
```bash
|
||||
# Auto-detect installed platforms (Claude Code, Codex)
|
||||
moemail skill install
|
||||
|
||||
# Install to a specific platform
|
||||
moemail skill install --platform claude
|
||||
moemail skill install --platform codex
|
||||
|
||||
# Install to all supported platforms (skip detection)
|
||||
moemail skill install --platform all
|
||||
```
|
||||
|
||||
After installation, AI agents will automatically know how to create temporary emails, wait for messages, and read content using the MoeMail CLI.
|
||||
|
||||
## JSON Output
|
||||
|
||||
All commands support `--json` flag for structured output, making them ideal for agent automation:
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"build": "bun build ./src/index.ts --outdir ./dist --target=node",
|
||||
"dev": "bun run ./src/index.ts"
|
||||
},
|
||||
"files": ["dist"],
|
||||
"files": ["dist", "skill"],
|
||||
"keywords": ["email", "temporary", "cli", "agent", "ai"],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: moemail
|
||||
description: Use when an AI agent needs a temporary/disposable email address — for receiving verification emails, testing email integrations, or any task requiring a temporary inbox via the moemail CLI
|
||||
---
|
||||
|
||||
# MoeMail — Temporary Email for AI Agents
|
||||
|
||||
## Overview
|
||||
MoeMail provides disposable email addresses with an agent-first CLI (`moemail`). Create inboxes, wait for messages, read content, and send emails programmatically.
|
||||
|
||||
## Install
|
||||
|
||||
Install the MoeMail CLI globally when it is not already available:
|
||||
|
||||
```bash
|
||||
npm i -g @moemail/cli
|
||||
```
|
||||
|
||||
Then confirm the binary is available:
|
||||
|
||||
```bash
|
||||
moemail --help
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
Configure once per environment:
|
||||
```bash
|
||||
moemail config set api-url https://moemail.app
|
||||
moemail config set api-key YOUR_API_KEY
|
||||
```
|
||||
|
||||
Or via environment variables: `MOEMAIL_API_URL`, `MOEMAIL_API_KEY`.
|
||||
|
||||
## Core Workflow: Receive an Email
|
||||
|
||||
```bash
|
||||
# 1. Create inbox — capture ONCE, parse both fields
|
||||
RESULT=$(moemail --json create --expiry 1h)
|
||||
ID=$(echo "$RESULT" | jq -r '.id')
|
||||
EMAIL=$(echo "$RESULT" | jq -r '.address')
|
||||
|
||||
# 2. Use $EMAIL wherever needed (registration, forms, etc.)
|
||||
|
||||
# 3. Wait for message (exits when message arrives or times out)
|
||||
MSG=$(moemail --json wait --email-id "$ID" --timeout 120)
|
||||
MSG_ID=$(echo "$MSG" | jq -r '.messageId')
|
||||
|
||||
# 4. Read full message content
|
||||
moemail --json read --email-id "$ID" --message-id "$MSG_ID"
|
||||
```
|
||||
|
||||
## Command Reference
|
||||
|
||||
| Command | Required Options | Notable Options |
|
||||
|---------|-----------------|-----------------|
|
||||
| `config set` | `<key> <value>` | keys: `api-url`, `api-key` |
|
||||
| `create` | — | `--name`, `--domain`, `--expiry` (1h\|24h\|3d\|permanent) |
|
||||
| `list` | — | `--email-id` (lists messages in mailbox), `--cursor` |
|
||||
| `wait` | `--email-id` | `--timeout` (default 120s), `--interval` (default 5s) |
|
||||
| `read` | `--email-id`, `--message-id` | `--format` (text\|html) |
|
||||
| `send` | `--email-id`, `--to`, `--subject`, `--content` | — |
|
||||
| `delete` | `--email-id` | — |
|
||||
|
||||
**Always put `--json` before the subcommand:**
|
||||
```bash
|
||||
moemail --json create --expiry 24h # ✅ correct
|
||||
moemail create --expiry 24h --json # ❌ wrong position
|
||||
```
|
||||
|
||||
## JSON Output Shapes
|
||||
|
||||
**create:** `{ "id": "...", "address": "user@domain.com", "expiresAt": "2025-..." }`
|
||||
|
||||
**wait:** `{ "messageId": "...", "from": "...", "subject": "...", "receivedAt": "2025-..." }`
|
||||
|
||||
**read:** `{ "id": "...", "from": "...", "to": "...", "subject": "...", "content": "plain text", "html": "...", "receivedAt": "..." }`
|
||||
|
||||
**send:** `{ "success": true, "remainingEmails": 10 }`
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| Calling `create` twice to get id + address | Call once, save to variable, parse both fields |
|
||||
| `--json` after subcommand | Move `--json` before the subcommand |
|
||||
| Timeout too short for slow services | Use `--timeout 300` for unreliable senders |
|
||||
| Inbox expired mid-test | Use `--expiry permanent` for long-running workflows |
|
||||
| Using `content` field for HTML emails | Check both `content` (plain text) and `html` fields |
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Command } from "commander";
|
||||
import { log, printText } from "../output.js";
|
||||
import { readFileSync, mkdirSync, writeFileSync, existsSync } from "fs";
|
||||
import { join, dirname, resolve } from "path";
|
||||
import { homedir } from "os";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
interface Platform {
|
||||
id: string;
|
||||
name: string;
|
||||
dir: string;
|
||||
skillPath: string;
|
||||
}
|
||||
|
||||
function getRuntimeEnv(name: string): string | undefined {
|
||||
const proc = Reflect.get(globalThis, "process") as
|
||||
| { env?: Record<string, string | undefined> }
|
||||
| undefined;
|
||||
return proc?.env?.[name];
|
||||
}
|
||||
|
||||
function getPlatforms(): Platform[] {
|
||||
const home = homedir();
|
||||
const codexHome = getRuntimeEnv("CODEX_HOME") || join(home, ".codex");
|
||||
return [
|
||||
{
|
||||
id: "claude",
|
||||
name: "Claude Code",
|
||||
dir: join(home, ".claude"),
|
||||
skillPath: join(home, ".claude", "skills", "moemail", "SKILL.md"),
|
||||
},
|
||||
{
|
||||
id: "codex",
|
||||
name: "Codex",
|
||||
dir: codexHome,
|
||||
skillPath: join(codexHome, "skills", "moemail", "SKILL.md"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getSkillSource(): string {
|
||||
// Resolve skill file relative to the executing script
|
||||
// Bundled: dist/index.js → ../skill/SKILL.md
|
||||
// Source: src/commands/skill.ts → ../../skill/SKILL.md
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const candidates = [
|
||||
join(scriptDir, "..", "skill", "SKILL.md"), // from dist/
|
||||
join(scriptDir, "..", "..", "skill", "SKILL.md"), // from src/commands/
|
||||
];
|
||||
for (const p of candidates) {
|
||||
const resolved = resolve(p);
|
||||
if (existsSync(resolved)) {
|
||||
return readFileSync(resolved, "utf-8");
|
||||
}
|
||||
}
|
||||
throw new Error("SKILL.md not found");
|
||||
}
|
||||
|
||||
function installTo(platform: Platform, content: string): boolean {
|
||||
try {
|
||||
mkdirSync(dirname(platform.skillPath), { recursive: true });
|
||||
writeFileSync(platform.skillPath, content, "utf-8");
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
log(`Error installing to ${platform.name}: ${e.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function registerSkillCommand(program: Command) {
|
||||
const skill = program
|
||||
.command("skill")
|
||||
.description("Manage MoeMail AI agent skill");
|
||||
|
||||
skill
|
||||
.command("install")
|
||||
.description("Install MoeMail skill to AI agent platforms")
|
||||
.option(
|
||||
"--platform <platform>",
|
||||
"target platform: claude, codex, all (default: auto-detect)"
|
||||
)
|
||||
.action((opts) => {
|
||||
let content: string;
|
||||
try {
|
||||
content = getSkillSource();
|
||||
} catch {
|
||||
log("Error: Could not read skill file from package.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const platforms = getPlatforms();
|
||||
const platformFlag = opts.platform?.toLowerCase();
|
||||
|
||||
let targets: Platform[];
|
||||
|
||||
if (platformFlag === "all") {
|
||||
targets = platforms;
|
||||
} else if (platformFlag) {
|
||||
const match = platforms.find((p) => p.id === platformFlag);
|
||||
if (!match) {
|
||||
log(
|
||||
`Error: Unknown platform "${opts.platform}". Supported: claude, codex, all`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
targets = [match];
|
||||
} else {
|
||||
// Auto-detect: only platforms whose base dir exists
|
||||
targets = platforms.filter((p) => existsSync(p.dir));
|
||||
if (targets.length === 0) {
|
||||
log(
|
||||
"No supported agent platform detected. Use --platform to specify manually."
|
||||
);
|
||||
log("Supported: claude, codex, all");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let installed = 0;
|
||||
let failed = 0;
|
||||
for (const t of targets) {
|
||||
if (installTo(t, content)) {
|
||||
printText(`Installed skill to ${t.name} (${t.skillPath})`);
|
||||
installed++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (installed === 0 || failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { registerWaitCommand } from "./commands/wait.js";
|
||||
import { registerReadCommand } from "./commands/read.js";
|
||||
import { registerDeleteCommand } from "./commands/delete.js";
|
||||
import { registerSendCommand } from "./commands/send.js";
|
||||
import { registerSkillCommand } from "./commands/skill.js";
|
||||
|
||||
const program = new Command();
|
||||
|
||||
@@ -23,5 +24,6 @@ registerWaitCommand(program);
|
||||
registerReadCommand(program);
|
||||
registerDeleteCommand(program);
|
||||
registerSendCommand(program);
|
||||
registerSkillCommand(program);
|
||||
|
||||
program.parse();
|
||||
|
||||
Reference in New Issue
Block a user