@zaddy6/agentemail 0.1.0
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/LICENSE +21 -0
- package/README.md +106 -0
- package/bin/agent-email.js +10 -0
- package/dist/auth.js +64 -0
- package/dist/cli.js +144 -0
- package/dist/commands/accounts.js +74 -0
- package/dist/commands/create.js +61 -0
- package/dist/commands/delete.js +11 -0
- package/dist/commands/meta.js +12 -0
- package/dist/commands/read.js +33 -0
- package/dist/commands/show.js +29 -0
- package/dist/config-store.js +142 -0
- package/dist/context.js +1 -0
- package/dist/mailtm-client.js +203 -0
- package/dist/output.js +57 -0
- package/dist/runner.js +12 -0
- package/dist/types.js +16 -0
- package/dist/utils.js +30 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Isaac David
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# agent-email
|
|
2
|
+
|
|
3
|
+
`agent-email` is a Bun CLI for AI agents that need access to a disposable email inbox powered by [Mail.tm](https://mail.tm).
|
|
4
|
+
|
|
5
|
+
## Open Source
|
|
6
|
+
|
|
7
|
+
This project is open source under the [MIT License](./LICENSE).
|
|
8
|
+
|
|
9
|
+
Community docs:
|
|
10
|
+
|
|
11
|
+
- [Contributing](./CONTRIBUTING.md)
|
|
12
|
+
- [Code of Conduct](./CODE_OF_CONDUCT.md)
|
|
13
|
+
- [Security Policy](./SECURITY.md)
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @zaddy6/agentemail
|
|
19
|
+
# or
|
|
20
|
+
bun install -g @zaddy6/agentemail
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
Create an inbox with one command (no flags):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
agent-email create
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Read latest emails:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
agent-email read <email>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
All commands return JSON by default.
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
- `agent-email create`
|
|
42
|
+
- `agent-email read <email|alias> [--limit <n>] [--full] [--wait <seconds>] [--interval <seconds>]`
|
|
43
|
+
- `agent-email show <email|alias> <messageId>`
|
|
44
|
+
- `agent-email delete <email|alias> <messageId>`
|
|
45
|
+
- `agent-email accounts list`
|
|
46
|
+
- `agent-email accounts add <email> --password <pwd> [--set-default]`
|
|
47
|
+
- `agent-email accounts remove <email|alias>`
|
|
48
|
+
- `agent-email use <email|alias>`
|
|
49
|
+
- `agent-email config path`
|
|
50
|
+
- `agent-email about`
|
|
51
|
+
|
|
52
|
+
Aliases for account selection:
|
|
53
|
+
|
|
54
|
+
- `default`
|
|
55
|
+
- `active`
|
|
56
|
+
- `me`
|
|
57
|
+
|
|
58
|
+
Use human-readable output if needed:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
agent-email --human accounts list
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Config
|
|
65
|
+
|
|
66
|
+
Credentials are stored in:
|
|
67
|
+
|
|
68
|
+
- `~/.config/agent-email/config.json`
|
|
69
|
+
|
|
70
|
+
Override path with:
|
|
71
|
+
|
|
72
|
+
- `AGENT_EMAIL_CONFIG=/custom/path/config.json`
|
|
73
|
+
|
|
74
|
+
The tool enforces `0600` permissions for the config file on Unix.
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
bun install
|
|
80
|
+
bun run check
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Run live Mail.tm smoke test (opt-in):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
AGENT_EMAIL_LIVE_TESTS=1 bun run test:live
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## API and Attribution
|
|
90
|
+
|
|
91
|
+
This project uses [Mail.tm API](https://api.mail.tm). If you use the API, follow Mail.tm terms and attribution requirements.
|
|
92
|
+
|
|
93
|
+
## Release / Publish
|
|
94
|
+
|
|
95
|
+
Local publish flow:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
bun run check
|
|
99
|
+
npm login
|
|
100
|
+
npm publish --access public
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
GitHub Actions workflows are included for CI and release publishing:
|
|
104
|
+
|
|
105
|
+
- `.github/workflows/ci.yml`
|
|
106
|
+
- `.github/workflows/publish.yml` (requires `NPM_TOKEN` repository secret)
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { toCliError } from "./output.js";
|
|
2
|
+
import { CliError } from "./types.js";
|
|
3
|
+
export function resolveAccountIdentifier(config, identifier) {
|
|
4
|
+
const normalized = identifier.trim().toLowerCase();
|
|
5
|
+
if (["default", "active", "me"].includes(normalized)) {
|
|
6
|
+
if (!config.activeEmail) {
|
|
7
|
+
throw new CliError({
|
|
8
|
+
code: "NO_ACTIVE_ACCOUNT",
|
|
9
|
+
message: "No active account is configured.",
|
|
10
|
+
hint: "Create an account or run `agent-email use <email>`.",
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return config.activeEmail;
|
|
14
|
+
}
|
|
15
|
+
if (config.accounts[identifier]) {
|
|
16
|
+
return identifier;
|
|
17
|
+
}
|
|
18
|
+
throw new CliError({
|
|
19
|
+
code: "ACCOUNT_NOT_FOUND",
|
|
20
|
+
message: `Account '${identifier}' is not configured.`,
|
|
21
|
+
hint: "Run `agent-email accounts list` to inspect known inboxes.",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export async function withAccountSession(store, client, identifier, fn) {
|
|
25
|
+
const config = await store.load();
|
|
26
|
+
const accountEmail = resolveAccountIdentifier(config, identifier);
|
|
27
|
+
const configuredAccount = config.accounts[accountEmail];
|
|
28
|
+
if (!configuredAccount) {
|
|
29
|
+
throw new CliError({
|
|
30
|
+
code: "ACCOUNT_NOT_FOUND",
|
|
31
|
+
message: `Account '${accountEmail}' is not configured.`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
let account = configuredAccount;
|
|
35
|
+
const refreshToken = async () => {
|
|
36
|
+
const tokenResp = await client.createToken(account.email, account.password);
|
|
37
|
+
account = {
|
|
38
|
+
...account,
|
|
39
|
+
token: tokenResp.token,
|
|
40
|
+
tokenUpdatedAt: new Date().toISOString(),
|
|
41
|
+
accountId: tokenResp.id || account.accountId,
|
|
42
|
+
};
|
|
43
|
+
config.accounts[accountEmail] = account;
|
|
44
|
+
await store.save(config);
|
|
45
|
+
};
|
|
46
|
+
const session = {
|
|
47
|
+
accountEmail,
|
|
48
|
+
getAccount: () => account,
|
|
49
|
+
run: async (request) => {
|
|
50
|
+
try {
|
|
51
|
+
return await request(account.token);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const cliError = toCliError(error);
|
|
55
|
+
if (cliError.status !== 401) {
|
|
56
|
+
throw cliError;
|
|
57
|
+
}
|
|
58
|
+
await refreshToken();
|
|
59
|
+
return request(account.token);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
return fn(session);
|
|
64
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ConfigStore } from "./config-store.js";
|
|
3
|
+
import { handleAccountsAdd, handleAccountsList, handleAccountsRemove, handleUse, } from "./commands/accounts.js";
|
|
4
|
+
import { handleCreate } from "./commands/create.js";
|
|
5
|
+
import { handleDelete } from "./commands/delete.js";
|
|
6
|
+
import { handleAbout, handleConfigPath } from "./commands/meta.js";
|
|
7
|
+
import { handleRead } from "./commands/read.js";
|
|
8
|
+
import { handleShow } from "./commands/show.js";
|
|
9
|
+
import { MailTmClient } from "./mailtm-client.js";
|
|
10
|
+
import { runCommand } from "./runner.js";
|
|
11
|
+
function parsePositiveInt(value, optionName) {
|
|
12
|
+
const parsed = Number(value);
|
|
13
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
14
|
+
throw new Error(`${optionName} must be a positive number.`);
|
|
15
|
+
}
|
|
16
|
+
return Math.floor(parsed);
|
|
17
|
+
}
|
|
18
|
+
async function buildContext(cmd) {
|
|
19
|
+
const opts = cmd.optsWithGlobals();
|
|
20
|
+
const store = new ConfigStore();
|
|
21
|
+
const cfg = await store.load();
|
|
22
|
+
return {
|
|
23
|
+
format: opts.human ? "human" : "json",
|
|
24
|
+
store,
|
|
25
|
+
client: new MailTmClient(cfg.apiBaseUrl),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function getActionCommand(args) {
|
|
29
|
+
const cmd = args.at(-1);
|
|
30
|
+
if (!cmd || typeof cmd.optsWithGlobals !== "function") {
|
|
31
|
+
throw new Error("Failed to resolve command context.");
|
|
32
|
+
}
|
|
33
|
+
return cmd;
|
|
34
|
+
}
|
|
35
|
+
export async function main(argv = process.argv) {
|
|
36
|
+
const program = new Command();
|
|
37
|
+
program
|
|
38
|
+
.name("agent-email")
|
|
39
|
+
.description("Disposable inbox CLI for AI agents (Mail.tm)")
|
|
40
|
+
.version("0.1.0")
|
|
41
|
+
.option("--human", "Print human-readable output instead of JSON")
|
|
42
|
+
.showHelpAfterError();
|
|
43
|
+
program
|
|
44
|
+
.command("create")
|
|
45
|
+
.description("Create a new random inbox and persist credentials")
|
|
46
|
+
.action(async (...args) => {
|
|
47
|
+
const ctx = await buildContext(getActionCommand(args));
|
|
48
|
+
await runCommand("create", ctx.format, async () => handleCreate(ctx));
|
|
49
|
+
});
|
|
50
|
+
program
|
|
51
|
+
.command("read")
|
|
52
|
+
.description("Read latest emails from an inbox")
|
|
53
|
+
.argument("<emailOrAlias>", "Email address or alias (default|active|me)")
|
|
54
|
+
.option("--limit <n>", "Max messages to return (default 10)", (v) => parsePositiveInt(v, "--limit"))
|
|
55
|
+
.option("--full", "Fetch full message payloads")
|
|
56
|
+
.option("--wait <seconds>", "Poll until a message arrives or timeout", (v) => parsePositiveInt(v, "--wait"))
|
|
57
|
+
.option("--interval <seconds>", "Polling interval in seconds (default 2)", (v) => parsePositiveInt(v, "--interval"))
|
|
58
|
+
.action(async (...args) => {
|
|
59
|
+
const [emailOrAlias, options] = args;
|
|
60
|
+
const ctx = await buildContext(getActionCommand(args));
|
|
61
|
+
await runCommand("read", ctx.format, async () => handleRead(ctx, emailOrAlias, {
|
|
62
|
+
limit: options.limit,
|
|
63
|
+
full: options.full,
|
|
64
|
+
waitSeconds: options.wait,
|
|
65
|
+
intervalSeconds: options.interval,
|
|
66
|
+
}));
|
|
67
|
+
});
|
|
68
|
+
program
|
|
69
|
+
.command("show")
|
|
70
|
+
.description("Show a single message")
|
|
71
|
+
.argument("<emailOrAlias>", "Email address or alias")
|
|
72
|
+
.argument("<messageId>", "Message id")
|
|
73
|
+
.action(async (...args) => {
|
|
74
|
+
const [emailOrAlias, messageId] = args;
|
|
75
|
+
const ctx = await buildContext(getActionCommand(args));
|
|
76
|
+
await runCommand("show", ctx.format, async () => handleShow(ctx, emailOrAlias, messageId));
|
|
77
|
+
});
|
|
78
|
+
program
|
|
79
|
+
.command("delete")
|
|
80
|
+
.description("Delete a message")
|
|
81
|
+
.argument("<emailOrAlias>", "Email address or alias")
|
|
82
|
+
.argument("<messageId>", "Message id")
|
|
83
|
+
.action(async (...args) => {
|
|
84
|
+
const [emailOrAlias, messageId] = args;
|
|
85
|
+
const ctx = await buildContext(getActionCommand(args));
|
|
86
|
+
await runCommand("delete", ctx.format, async () => handleDelete(ctx, emailOrAlias, messageId));
|
|
87
|
+
});
|
|
88
|
+
const accounts = program.command("accounts").description("Manage locally stored accounts");
|
|
89
|
+
accounts
|
|
90
|
+
.command("list")
|
|
91
|
+
.description("List configured accounts")
|
|
92
|
+
.action(async (...args) => {
|
|
93
|
+
const ctx = await buildContext(getActionCommand(args));
|
|
94
|
+
await runCommand("accounts list", ctx.format, async () => handleAccountsList(ctx));
|
|
95
|
+
});
|
|
96
|
+
accounts
|
|
97
|
+
.command("add")
|
|
98
|
+
.description("Add an existing mailbox credentials")
|
|
99
|
+
.argument("<email>", "Email address")
|
|
100
|
+
.requiredOption("--password <password>", "Mailbox password")
|
|
101
|
+
.option("--set-default", "Set account as active default")
|
|
102
|
+
.action(async (...args) => {
|
|
103
|
+
const [email, options] = args;
|
|
104
|
+
const ctx = await buildContext(getActionCommand(args));
|
|
105
|
+
await runCommand("accounts add", ctx.format, async () => handleAccountsAdd(ctx, email, options.password, Boolean(options.setDefault)));
|
|
106
|
+
});
|
|
107
|
+
accounts
|
|
108
|
+
.command("remove")
|
|
109
|
+
.description("Remove a local account profile")
|
|
110
|
+
.argument("<emailOrAlias>", "Email address or alias")
|
|
111
|
+
.action(async (...args) => {
|
|
112
|
+
const [emailOrAlias] = args;
|
|
113
|
+
const ctx = await buildContext(getActionCommand(args));
|
|
114
|
+
await runCommand("accounts remove", ctx.format, async () => handleAccountsRemove(ctx, emailOrAlias));
|
|
115
|
+
});
|
|
116
|
+
program
|
|
117
|
+
.command("use")
|
|
118
|
+
.description("Set the active default account")
|
|
119
|
+
.argument("<emailOrAlias>", "Email address or alias")
|
|
120
|
+
.action(async (...args) => {
|
|
121
|
+
const [emailOrAlias] = args;
|
|
122
|
+
const ctx = await buildContext(getActionCommand(args));
|
|
123
|
+
await runCommand("use", ctx.format, async () => handleUse(ctx, emailOrAlias));
|
|
124
|
+
});
|
|
125
|
+
const config = program.command("config").description("Configuration helpers");
|
|
126
|
+
config
|
|
127
|
+
.command("path")
|
|
128
|
+
.description("Show config file path")
|
|
129
|
+
.action(async (...args) => {
|
|
130
|
+
const ctx = await buildContext(getActionCommand(args));
|
|
131
|
+
await runCommand("config path", ctx.format, async () => handleConfigPath(ctx));
|
|
132
|
+
});
|
|
133
|
+
program
|
|
134
|
+
.command("about")
|
|
135
|
+
.description("Show project info and attribution")
|
|
136
|
+
.action(async (...args) => {
|
|
137
|
+
const ctx = await buildContext(getActionCommand(args));
|
|
138
|
+
await runCommand("about", ctx.format, async () => handleAbout(ctx));
|
|
139
|
+
});
|
|
140
|
+
await program.parseAsync(argv);
|
|
141
|
+
}
|
|
142
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
143
|
+
void main();
|
|
144
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { resolveAccountIdentifier } from "../auth.js";
|
|
2
|
+
export async function handleAccountsList(ctx) {
|
|
3
|
+
const cfg = await ctx.store.load();
|
|
4
|
+
const emails = Object.keys(cfg.accounts).sort();
|
|
5
|
+
return {
|
|
6
|
+
activeEmail: cfg.activeEmail,
|
|
7
|
+
total: emails.length,
|
|
8
|
+
accounts: emails.map((email) => {
|
|
9
|
+
const acct = cfg.accounts[email];
|
|
10
|
+
return {
|
|
11
|
+
email,
|
|
12
|
+
accountId: acct.accountId,
|
|
13
|
+
createdAt: acct.createdAt,
|
|
14
|
+
tokenUpdatedAt: acct.tokenUpdatedAt,
|
|
15
|
+
isActive: cfg.activeEmail === email,
|
|
16
|
+
};
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function handleAccountsAdd(ctx, email, password, setDefault) {
|
|
21
|
+
const tokenResp = await ctx.client.createToken(email, password);
|
|
22
|
+
const now = new Date().toISOString();
|
|
23
|
+
let accountId = tokenResp.id;
|
|
24
|
+
try {
|
|
25
|
+
const me = await ctx.client.getMe(tokenResp.token);
|
|
26
|
+
accountId = me.id || accountId;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Keep the ID from /token if /me is unavailable.
|
|
30
|
+
}
|
|
31
|
+
const cfg = await ctx.store.update((config) => {
|
|
32
|
+
const existing = config.accounts[email];
|
|
33
|
+
config.accounts[email] = {
|
|
34
|
+
email,
|
|
35
|
+
password,
|
|
36
|
+
token: tokenResp.token,
|
|
37
|
+
tokenUpdatedAt: now,
|
|
38
|
+
accountId,
|
|
39
|
+
createdAt: existing?.createdAt ?? now,
|
|
40
|
+
};
|
|
41
|
+
if (setDefault || !config.activeEmail) {
|
|
42
|
+
config.activeEmail = email;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
email,
|
|
47
|
+
accountId,
|
|
48
|
+
activeEmail: cfg.activeEmail,
|
|
49
|
+
added: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export async function handleAccountsRemove(ctx, identifier) {
|
|
53
|
+
const cfg = await ctx.store.load();
|
|
54
|
+
const email = resolveAccountIdentifier(cfg, identifier);
|
|
55
|
+
delete cfg.accounts[email];
|
|
56
|
+
if (cfg.activeEmail === email) {
|
|
57
|
+
const next = Object.keys(cfg.accounts)[0] ?? null;
|
|
58
|
+
cfg.activeEmail = next;
|
|
59
|
+
}
|
|
60
|
+
await ctx.store.save(cfg);
|
|
61
|
+
return {
|
|
62
|
+
removed: email,
|
|
63
|
+
activeEmail: cfg.activeEmail,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export async function handleUse(ctx, identifier) {
|
|
67
|
+
const cfg = await ctx.store.load();
|
|
68
|
+
const email = resolveAccountIdentifier(cfg, identifier);
|
|
69
|
+
cfg.activeEmail = email;
|
|
70
|
+
await ctx.store.save(cfg);
|
|
71
|
+
return {
|
|
72
|
+
activeEmail: email,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { CliError } from "../types.js";
|
|
2
|
+
import { generatePassword24, generateRandomLocalPart } from "../utils.js";
|
|
3
|
+
const MAX_CREATE_RETRIES = 5;
|
|
4
|
+
export async function handleCreate(ctx) {
|
|
5
|
+
const domains = await ctx.client.getDomains();
|
|
6
|
+
const selected = domains["hydra:member"].find((domain) => domain.isActive && !domain.isPrivate);
|
|
7
|
+
if (!selected) {
|
|
8
|
+
throw new CliError({
|
|
9
|
+
code: "NO_ACTIVE_DOMAIN",
|
|
10
|
+
message: "No active public Mail.tm domain is available.",
|
|
11
|
+
hint: "Retry later when a domain is available.",
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const password = generatePassword24();
|
|
15
|
+
let createdEmail = "";
|
|
16
|
+
let accountId = "";
|
|
17
|
+
let token = "";
|
|
18
|
+
for (let attempt = 0; attempt < MAX_CREATE_RETRIES; attempt += 1) {
|
|
19
|
+
const address = `${generateRandomLocalPart()}@${selected.domain}`;
|
|
20
|
+
try {
|
|
21
|
+
const accountResp = await ctx.client.createAccount(address, password);
|
|
22
|
+
const tokenResp = await ctx.client.createToken(address, password);
|
|
23
|
+
createdEmail = address;
|
|
24
|
+
accountId = tokenResp.id || accountResp.id;
|
|
25
|
+
token = tokenResp.token;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error instanceof CliError && error.status === 422 && attempt < MAX_CREATE_RETRIES - 1) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!createdEmail || !accountId || !token) {
|
|
36
|
+
throw new CliError({
|
|
37
|
+
code: "ACCOUNT_CREATION_FAILED",
|
|
38
|
+
message: "Failed to create an account after multiple attempts.",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const now = new Date().toISOString();
|
|
42
|
+
const cfg = await ctx.store.update((config) => {
|
|
43
|
+
config.accounts[createdEmail] = {
|
|
44
|
+
email: createdEmail,
|
|
45
|
+
password,
|
|
46
|
+
token,
|
|
47
|
+
tokenUpdatedAt: now,
|
|
48
|
+
accountId,
|
|
49
|
+
createdAt: now,
|
|
50
|
+
};
|
|
51
|
+
if (!config.activeEmail) {
|
|
52
|
+
config.activeEmail = createdEmail;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
email: createdEmail,
|
|
57
|
+
password,
|
|
58
|
+
accountId,
|
|
59
|
+
activeEmail: cfg.activeEmail,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { withAccountSession } from "../auth.js";
|
|
2
|
+
export async function handleDelete(ctx, identifier, messageId) {
|
|
3
|
+
return withAccountSession(ctx.store, ctx.client, identifier, async (session) => {
|
|
4
|
+
await session.run((token) => ctx.client.deleteMessage(token, messageId));
|
|
5
|
+
return {
|
|
6
|
+
email: session.getAccount().email,
|
|
7
|
+
deleted: true,
|
|
8
|
+
messageId,
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export async function handleConfigPath(ctx) {
|
|
2
|
+
return { path: ctx.store.path };
|
|
3
|
+
}
|
|
4
|
+
export async function handleAbout(_) {
|
|
5
|
+
return {
|
|
6
|
+
name: "agent-email",
|
|
7
|
+
description: "CLI for disposable Mail.tm inboxes designed for AI agents.",
|
|
8
|
+
api: "https://api.mail.tm",
|
|
9
|
+
attribution: "Powered by Mail.tm. Include attribution when integrating this API.",
|
|
10
|
+
docs: "https://docs.mail.tm",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { withAccountSession } from "../auth.js";
|
|
2
|
+
import { clamp, sleep } from "../utils.js";
|
|
3
|
+
export async function handleRead(ctx, identifier, options) {
|
|
4
|
+
const limit = clamp(options.limit ?? 10, 1, 30);
|
|
5
|
+
const full = options.full ?? false;
|
|
6
|
+
const waitSeconds = Math.max(0, options.waitSeconds ?? 0);
|
|
7
|
+
const intervalSeconds = Math.max(1, options.intervalSeconds ?? 2);
|
|
8
|
+
return withAccountSession(ctx.store, ctx.client, identifier, async (session) => {
|
|
9
|
+
const started = Date.now();
|
|
10
|
+
const deadline = started + waitSeconds * 1000;
|
|
11
|
+
let summaries = [];
|
|
12
|
+
do {
|
|
13
|
+
const list = await session.run((token) => ctx.client.listMessages(token, 1));
|
|
14
|
+
summaries = list["hydra:member"].slice(0, limit);
|
|
15
|
+
if (summaries.length > 0 || waitSeconds === 0 || Date.now() >= deadline) {
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
await sleep(intervalSeconds * 1000);
|
|
19
|
+
} while (true);
|
|
20
|
+
let messages = summaries;
|
|
21
|
+
if (full) {
|
|
22
|
+
messages = await Promise.all(summaries.map((summary) => session.run((token) => ctx.client.getMessage(token, summary.id))));
|
|
23
|
+
}
|
|
24
|
+
const waitedSecondsActual = Math.round((Date.now() - started) / 100) / 10;
|
|
25
|
+
return {
|
|
26
|
+
email: session.getAccount().email,
|
|
27
|
+
count: messages.length,
|
|
28
|
+
timedOut: messages.length === 0 && waitSeconds > 0,
|
|
29
|
+
waitedSeconds: waitedSecondsActual,
|
|
30
|
+
messages,
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { withAccountSession } from "../auth.js";
|
|
2
|
+
import { extractResourceId } from "../utils.js";
|
|
3
|
+
export async function handleShow(ctx, identifier, messageId) {
|
|
4
|
+
return withAccountSession(ctx.store, ctx.client, identifier, async (session) => {
|
|
5
|
+
const message = await session.run((token) => ctx.client.getMessage(token, messageId));
|
|
6
|
+
let markedSeen = false;
|
|
7
|
+
if (!message.seen) {
|
|
8
|
+
await session.run((token) => ctx.client.patchMessageSeen(token, messageId, true));
|
|
9
|
+
message.seen = true;
|
|
10
|
+
markedSeen = true;
|
|
11
|
+
}
|
|
12
|
+
const sourceId = extractResourceId(message.sourceUrl, "sources");
|
|
13
|
+
let source;
|
|
14
|
+
if (sourceId) {
|
|
15
|
+
try {
|
|
16
|
+
source = await session.run((token) => ctx.client.getSource(token, sourceId));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
source = undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
email: session.getAccount().email,
|
|
24
|
+
message,
|
|
25
|
+
markedSeen,
|
|
26
|
+
source,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { CliError } from "./types.js";
|
|
5
|
+
export const DEFAULT_API_BASE_URL = "https://api.mail.tm";
|
|
6
|
+
function defaultConfig() {
|
|
7
|
+
return {
|
|
8
|
+
version: 1,
|
|
9
|
+
apiBaseUrl: DEFAULT_API_BASE_URL,
|
|
10
|
+
activeEmail: null,
|
|
11
|
+
accounts: {},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function resolveConfigPath(explicitPath) {
|
|
15
|
+
if (explicitPath) {
|
|
16
|
+
return explicitPath;
|
|
17
|
+
}
|
|
18
|
+
if (process.env.AGENT_EMAIL_CONFIG) {
|
|
19
|
+
return process.env.AGENT_EMAIL_CONFIG;
|
|
20
|
+
}
|
|
21
|
+
return path.join(os.homedir(), ".config", "agent-email", "config.json");
|
|
22
|
+
}
|
|
23
|
+
function isStoredAccount(value) {
|
|
24
|
+
if (typeof value !== "object" || value === null) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const v = value;
|
|
28
|
+
return (typeof v.email === "string" &&
|
|
29
|
+
typeof v.password === "string" &&
|
|
30
|
+
typeof v.token === "string" &&
|
|
31
|
+
typeof v.tokenUpdatedAt === "string" &&
|
|
32
|
+
typeof v.accountId === "string" &&
|
|
33
|
+
typeof v.createdAt === "string");
|
|
34
|
+
}
|
|
35
|
+
function validateConfig(parsed) {
|
|
36
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
37
|
+
throw new CliError({
|
|
38
|
+
code: "CONFIG_INVALID",
|
|
39
|
+
message: "Config file is not a JSON object.",
|
|
40
|
+
hint: "Delete the file or fix its structure.",
|
|
41
|
+
exitCode: 2,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const cfg = parsed;
|
|
45
|
+
if (cfg.version !== 1) {
|
|
46
|
+
throw new CliError({
|
|
47
|
+
code: "CONFIG_VERSION_UNSUPPORTED",
|
|
48
|
+
message: "Config version is unsupported.",
|
|
49
|
+
hint: "Delete the config file and run `agent-email create` again.",
|
|
50
|
+
exitCode: 2,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (typeof cfg.apiBaseUrl !== "string") {
|
|
54
|
+
throw new CliError({
|
|
55
|
+
code: "CONFIG_INVALID",
|
|
56
|
+
message: "Config is missing `apiBaseUrl`.",
|
|
57
|
+
exitCode: 2,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (!(cfg.activeEmail === null || typeof cfg.activeEmail === "string")) {
|
|
61
|
+
throw new CliError({
|
|
62
|
+
code: "CONFIG_INVALID",
|
|
63
|
+
message: "Config has invalid `activeEmail`.",
|
|
64
|
+
exitCode: 2,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (typeof cfg.accounts !== "object" || cfg.accounts === null) {
|
|
68
|
+
throw new CliError({
|
|
69
|
+
code: "CONFIG_INVALID",
|
|
70
|
+
message: "Config has invalid `accounts` map.",
|
|
71
|
+
exitCode: 2,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
for (const [email, account] of Object.entries(cfg.accounts)) {
|
|
75
|
+
if (!isStoredAccount(account) || account.email !== email) {
|
|
76
|
+
throw new CliError({
|
|
77
|
+
code: "CONFIG_INVALID_ACCOUNT",
|
|
78
|
+
message: `Config account entry for ${email} is invalid.`,
|
|
79
|
+
exitCode: 2,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return cfg;
|
|
84
|
+
}
|
|
85
|
+
async function ensurePermission0600(filePath) {
|
|
86
|
+
if (process.platform === "win32") {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const stat = await fs.stat(filePath);
|
|
90
|
+
if ((stat.mode & 0o077) !== 0) {
|
|
91
|
+
throw new CliError({
|
|
92
|
+
code: "CONFIG_PERMISSIONS",
|
|
93
|
+
message: "Config file permissions are too open.",
|
|
94
|
+
hint: `Run: chmod 600 ${filePath}`,
|
|
95
|
+
exitCode: 2,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export class ConfigStore {
|
|
100
|
+
path;
|
|
101
|
+
constructor(explicitPath) {
|
|
102
|
+
this.path = resolveConfigPath(explicitPath);
|
|
103
|
+
}
|
|
104
|
+
async load() {
|
|
105
|
+
try {
|
|
106
|
+
const raw = await fs.readFile(this.path, "utf8");
|
|
107
|
+
await ensurePermission0600(this.path);
|
|
108
|
+
const parsed = JSON.parse(raw);
|
|
109
|
+
return validateConfig(parsed);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error.code === "ENOENT") {
|
|
113
|
+
return defaultConfig();
|
|
114
|
+
}
|
|
115
|
+
if (error instanceof SyntaxError) {
|
|
116
|
+
throw new CliError({
|
|
117
|
+
code: "CONFIG_PARSE_ERROR",
|
|
118
|
+
message: "Failed to parse config JSON.",
|
|
119
|
+
hint: "Fix the JSON format or delete the file and recreate it.",
|
|
120
|
+
exitCode: 2,
|
|
121
|
+
cause: error,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async save(config) {
|
|
128
|
+
const dir = path.dirname(this.path);
|
|
129
|
+
await fs.mkdir(dir, { recursive: true });
|
|
130
|
+
const payload = `${JSON.stringify(config, null, 2)}\n`;
|
|
131
|
+
await fs.writeFile(this.path, payload, { mode: 0o600 });
|
|
132
|
+
if (process.platform !== "win32") {
|
|
133
|
+
await fs.chmod(this.path, 0o600);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async update(mutator) {
|
|
137
|
+
const cfg = await this.load();
|
|
138
|
+
mutator(cfg);
|
|
139
|
+
await this.save(cfg);
|
|
140
|
+
return cfg;
|
|
141
|
+
}
|
|
142
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE_URL } from "./config-store.js";
|
|
2
|
+
import { CliError, } from "./types.js";
|
|
3
|
+
import { sleep } from "./utils.js";
|
|
4
|
+
function statusHint(status) {
|
|
5
|
+
if (status === 401) {
|
|
6
|
+
return "Credentials may be stale. Re-authenticate the mailbox account.";
|
|
7
|
+
}
|
|
8
|
+
if (status === 404) {
|
|
9
|
+
return "The requested resource does not exist.";
|
|
10
|
+
}
|
|
11
|
+
if (status === 422) {
|
|
12
|
+
return "Check payload fields (address, domain, or message id).";
|
|
13
|
+
}
|
|
14
|
+
if (status === 429) {
|
|
15
|
+
return "Rate limit exceeded. Retry shortly.";
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
function statusCode(status) {
|
|
20
|
+
if (status === 401) {
|
|
21
|
+
return "AUTH_REQUIRED";
|
|
22
|
+
}
|
|
23
|
+
if (status === 404) {
|
|
24
|
+
return "RESOURCE_NOT_FOUND";
|
|
25
|
+
}
|
|
26
|
+
if (status === 422) {
|
|
27
|
+
return "VALIDATION_ERROR";
|
|
28
|
+
}
|
|
29
|
+
if (status === 429) {
|
|
30
|
+
return "RATE_LIMITED";
|
|
31
|
+
}
|
|
32
|
+
return "API_ERROR";
|
|
33
|
+
}
|
|
34
|
+
export class MailTmClient {
|
|
35
|
+
fetchLike;
|
|
36
|
+
baseUrl;
|
|
37
|
+
constructor(baseUrl = DEFAULT_API_BASE_URL, fetchLike = fetch) {
|
|
38
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
39
|
+
this.fetchLike = fetchLike;
|
|
40
|
+
}
|
|
41
|
+
async getDomains() {
|
|
42
|
+
const payload = await this.requestJson("/domains");
|
|
43
|
+
return this.normalizeCollection(payload, "domains");
|
|
44
|
+
}
|
|
45
|
+
async createAccount(address, password) {
|
|
46
|
+
return this.requestJson("/accounts", {
|
|
47
|
+
method: "POST",
|
|
48
|
+
body: { address, password },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async createToken(address, password) {
|
|
52
|
+
return this.requestJson("/token", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
body: { address, password },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async getMe(token) {
|
|
58
|
+
return this.requestJson("/me", { token });
|
|
59
|
+
}
|
|
60
|
+
async listMessages(token, page = 1) {
|
|
61
|
+
const payload = await this.requestJson(`/messages?page=${page}`, { token });
|
|
62
|
+
return this.normalizeCollection(payload, "messages");
|
|
63
|
+
}
|
|
64
|
+
async getMessage(token, id) {
|
|
65
|
+
return this.requestJson(`/messages/${id}`, { token });
|
|
66
|
+
}
|
|
67
|
+
async patchMessageSeen(token, id, seen = true) {
|
|
68
|
+
return this.requestJson(`/messages/${id}`, {
|
|
69
|
+
token,
|
|
70
|
+
method: "PATCH",
|
|
71
|
+
body: { seen },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async deleteMessage(token, id) {
|
|
75
|
+
await this.requestJson(`/messages/${id}`, {
|
|
76
|
+
token,
|
|
77
|
+
method: "DELETE",
|
|
78
|
+
expectNoContent: true,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async getSource(token, id) {
|
|
82
|
+
return this.requestJson(`/sources/${id}`, { token });
|
|
83
|
+
}
|
|
84
|
+
async deleteAccount(token, id) {
|
|
85
|
+
await this.requestJson(`/accounts/${id}`, {
|
|
86
|
+
token,
|
|
87
|
+
method: "DELETE",
|
|
88
|
+
expectNoContent: true,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async requestJson(path, options = {}) {
|
|
92
|
+
const max429Retries = options.max429Retries ?? 4;
|
|
93
|
+
for (let attempt = 0;; attempt += 1) {
|
|
94
|
+
const headers = new Headers({
|
|
95
|
+
Accept: "application/ld+json, application/json;q=0.9",
|
|
96
|
+
});
|
|
97
|
+
if (options.body !== undefined) {
|
|
98
|
+
headers.set("Content-Type", "application/json");
|
|
99
|
+
}
|
|
100
|
+
if (options.token) {
|
|
101
|
+
headers.set("Authorization", `Bearer ${options.token}`);
|
|
102
|
+
}
|
|
103
|
+
let response;
|
|
104
|
+
try {
|
|
105
|
+
response = await this.fetchLike(`${this.baseUrl}${path}`, {
|
|
106
|
+
method: options.method ?? "GET",
|
|
107
|
+
headers,
|
|
108
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
throw new CliError({
|
|
113
|
+
code: "NETWORK_ERROR",
|
|
114
|
+
message: "Request to Mail.tm failed.",
|
|
115
|
+
hint: "Check internet connectivity and retry.",
|
|
116
|
+
cause: error,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const textBody = await response.text();
|
|
120
|
+
const parsedBody = this.tryParseBody(textBody);
|
|
121
|
+
if (response.ok) {
|
|
122
|
+
if (options.expectNoContent || response.status === 204 || textBody.length === 0) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return parsedBody;
|
|
126
|
+
}
|
|
127
|
+
if (response.status === 429 && attempt < max429Retries) {
|
|
128
|
+
const delayMs = this.computeRetryDelayMs(response, attempt);
|
|
129
|
+
await sleep(delayMs);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const message = this.extractErrorMessage(parsedBody, response.statusText);
|
|
133
|
+
throw new CliError({
|
|
134
|
+
code: statusCode(response.status),
|
|
135
|
+
message,
|
|
136
|
+
status: response.status,
|
|
137
|
+
hint: statusHint(response.status),
|
|
138
|
+
details: parsedBody,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
tryParseBody(textBody) {
|
|
143
|
+
if (!textBody) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(textBody);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return textBody;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
extractErrorMessage(parsedBody, fallback) {
|
|
154
|
+
if (typeof parsedBody === "object" && parsedBody !== null) {
|
|
155
|
+
const obj = parsedBody;
|
|
156
|
+
if (typeof obj.message === "string") {
|
|
157
|
+
return obj.message;
|
|
158
|
+
}
|
|
159
|
+
if (typeof obj["hydra:description"] === "string") {
|
|
160
|
+
return obj["hydra:description"];
|
|
161
|
+
}
|
|
162
|
+
if (typeof obj.detail === "string") {
|
|
163
|
+
return obj.detail;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (typeof parsedBody === "string" && parsedBody.length > 0) {
|
|
167
|
+
return parsedBody;
|
|
168
|
+
}
|
|
169
|
+
return fallback || "Mail.tm request failed.";
|
|
170
|
+
}
|
|
171
|
+
normalizeCollection(payload, resourceName) {
|
|
172
|
+
if (Array.isArray(payload)) {
|
|
173
|
+
return {
|
|
174
|
+
"hydra:member": payload,
|
|
175
|
+
"hydra:totalItems": payload.length,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (typeof payload === "object" && payload !== null) {
|
|
179
|
+
const obj = payload;
|
|
180
|
+
if (Array.isArray(obj["hydra:member"])) {
|
|
181
|
+
return payload;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
throw new CliError({
|
|
185
|
+
code: "API_RESPONSE_INVALID",
|
|
186
|
+
message: `Unexpected ${resourceName} response shape from Mail.tm.`,
|
|
187
|
+
hint: "Retry the command. If this persists, the upstream API format may have changed.",
|
|
188
|
+
details: payload,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
computeRetryDelayMs(response, attempt) {
|
|
192
|
+
const retryAfter = response.headers.get("retry-after");
|
|
193
|
+
if (retryAfter) {
|
|
194
|
+
const seconds = Number(retryAfter);
|
|
195
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
196
|
+
return Math.round(seconds * 1000);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const base = 250 * (2 ** attempt);
|
|
200
|
+
const jitter = Math.floor(Math.random() * 250);
|
|
201
|
+
return Math.min(base + jitter, 5000);
|
|
202
|
+
}
|
|
203
|
+
}
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { inspect } from "node:util";
|
|
2
|
+
import { CliError } from "./types.js";
|
|
3
|
+
export function toCliError(error) {
|
|
4
|
+
if (error instanceof CliError) {
|
|
5
|
+
return error;
|
|
6
|
+
}
|
|
7
|
+
if (error instanceof Error) {
|
|
8
|
+
return new CliError({
|
|
9
|
+
code: "UNEXPECTED_ERROR",
|
|
10
|
+
message: error.message,
|
|
11
|
+
cause: error,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return new CliError({
|
|
15
|
+
code: "UNEXPECTED_ERROR",
|
|
16
|
+
message: "Unexpected failure.",
|
|
17
|
+
details: error,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function buildSuccessEnvelope(command, data) {
|
|
21
|
+
return {
|
|
22
|
+
ok: true,
|
|
23
|
+
command,
|
|
24
|
+
data,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function buildErrorEnvelope(command, error) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
command,
|
|
31
|
+
error: {
|
|
32
|
+
code: error.code,
|
|
33
|
+
message: error.message,
|
|
34
|
+
status: error.status,
|
|
35
|
+
hint: error.hint,
|
|
36
|
+
details: error.details,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function printEnvelope(envelope, format) {
|
|
41
|
+
if (format === "json") {
|
|
42
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (envelope.ok) {
|
|
46
|
+
console.log(`${envelope.command}: ok`);
|
|
47
|
+
console.log(inspect(envelope.data, { depth: null, colors: false, compact: false }));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.error(`${envelope.command}: error [${envelope.error.code}] ${envelope.error.message}`);
|
|
51
|
+
if (envelope.error.status !== undefined) {
|
|
52
|
+
console.error(`status: ${envelope.error.status}`);
|
|
53
|
+
}
|
|
54
|
+
if (envelope.error.hint) {
|
|
55
|
+
console.error(`hint: ${envelope.error.hint}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { buildErrorEnvelope, buildSuccessEnvelope, printEnvelope, toCliError } from "./output.js";
|
|
2
|
+
export async function runCommand(command, format, action) {
|
|
3
|
+
try {
|
|
4
|
+
const data = await action();
|
|
5
|
+
printEnvelope(buildSuccessEnvelope(command, data), format);
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
const cliError = toCliError(error);
|
|
9
|
+
printEnvelope(buildErrorEnvelope(command, cliError), format);
|
|
10
|
+
process.exitCode = cliError.exitCode;
|
|
11
|
+
}
|
|
12
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class CliError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
status;
|
|
4
|
+
hint;
|
|
5
|
+
exitCode;
|
|
6
|
+
details;
|
|
7
|
+
constructor(params) {
|
|
8
|
+
super(params.message, { cause: params.cause });
|
|
9
|
+
this.name = "CliError";
|
|
10
|
+
this.code = params.code;
|
|
11
|
+
this.status = params.status;
|
|
12
|
+
this.hint = params.hint;
|
|
13
|
+
this.exitCode = params.exitCode ?? 1;
|
|
14
|
+
this.details = params.details;
|
|
15
|
+
}
|
|
16
|
+
}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
export function generateRandomLocalPart() {
|
|
3
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
4
|
+
const bytes = randomBytes(12);
|
|
5
|
+
let body = "";
|
|
6
|
+
for (const byte of bytes) {
|
|
7
|
+
body += alphabet[byte % alphabet.length];
|
|
8
|
+
}
|
|
9
|
+
return `ae${body}`;
|
|
10
|
+
}
|
|
11
|
+
export function generatePassword24() {
|
|
12
|
+
return randomBytes(18).toString("base64url");
|
|
13
|
+
}
|
|
14
|
+
export function clamp(value, min, max) {
|
|
15
|
+
return Math.min(Math.max(value, min), max);
|
|
16
|
+
}
|
|
17
|
+
export function sleep(ms) {
|
|
18
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
}
|
|
20
|
+
export function extractResourceId(resourceUrl, resourceName) {
|
|
21
|
+
if (!resourceUrl) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const marker = `/${resourceName}/`;
|
|
25
|
+
const idx = resourceUrl.lastIndexOf(marker);
|
|
26
|
+
if (idx === -1) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return resourceUrl.slice(idx + marker.length) || null;
|
|
30
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zaddy6/agentemail",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mail.tm CLI for AI agents",
|
|
5
|
+
"private": false,
|
|
6
|
+
"packageManager": "bun@1.0.29",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"agent-email": "bin/agent-email.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"bin",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"dev": "bun --watch src/cli.ts",
|
|
20
|
+
"check": "bun run build && bun run test",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:live": "AGENT_EMAIL_LIVE_TESTS=1 vitest run tests/live.test.ts",
|
|
23
|
+
"prepublishOnly": "bun run check"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"email",
|
|
27
|
+
"cli",
|
|
28
|
+
"mail.tm",
|
|
29
|
+
"ai-agent"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"author": "Isaac David",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20",
|
|
35
|
+
"bun": ">=1.0.29"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/zaddy6/agent-email.git"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/zaddy6/agent-email/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/zaddy6/agent-email#readme",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"commander": "^12.1.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^22.10.2",
|
|
53
|
+
"typescript": "^5.7.2",
|
|
54
|
+
"vitest": "^2.1.8"
|
|
55
|
+
}
|
|
56
|
+
}
|