mailtea-cli 0.1.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mailtea
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,99 @@
1
+ # `mailtea-cli`
2
+
3
+ Send, schedule, and manage email with [Mailtea](https://mailtea.app) from your
4
+ shell or an AI agent. Wraps the [`mailtea-sdk`](https://www.npmjs.com/package/mailtea-sdk)
5
+ SDK for everyday commands, and bridges the full
6
+ [`mailtea-mcp`](https://www.npmjs.com/package/mailtea-mcp) tool catalog so an agent
7
+ can reach **every** Mailtea capability — the same surface as the MCP server.
8
+ MIT-licensed.
9
+
10
+ ## Quick start
11
+
12
+ ```bash
13
+ # one-off, no install
14
+ npx -y mailtea-cli emails send \
15
+ --from "you@yourdomain.com" \
16
+ --to "user@example.com" \
17
+ --subject "Hello" \
18
+ --html "<p>Sent from the Mailtea CLI</p>"
19
+ # -> {"id":"txemail_..."}
20
+ ```
21
+
22
+ Authenticate with a personal access token (prefix `mt_pat_`) from **Settings →
23
+ API keys**, supplied any of three ways (in order):
24
+
25
+ 1. `--api-key mt_pat_xxx`
26
+ 2. `MAILTEA_API_KEY` (or `MAILTEA_API_TOKEN`) environment variable
27
+ 3. `mailtea login --api-key mt_pat_xxx` (saved to `~/.mailtea/config.json`)
28
+
29
+ Self-hosting or local dev? add `--base-url http://localhost:8787` (or
30
+ `MAILTEA_API_BASE_URL`).
31
+
32
+ ## Full capability bridge (every MCP tool)
33
+
34
+ Beyond the named commands below, the CLI exposes the **entire MCP tool catalog** —
35
+ issue drafting/scheduling/sending, analytics, sections, referrals, monetization,
36
+ AI drafting, and more — through three generic commands:
37
+
38
+ ```bash
39
+ mailtea tools [--names] # list every capability (no auth)
40
+ mailtea schema <tool> # print a tool's JSON input schema (no auth)
41
+ mailtea call <tool> [--json '{...}'] [--arg key=value ...] # invoke any tool
42
+ ```
43
+
44
+ ```bash
45
+ # discover, inspect, then call — exactly how an agent operates
46
+ mailtea tools --names | jq -r '.[]' | grep '^issue\.'
47
+ mailtea schema issue.send_now
48
+ mailtea call issue.send_now --json '{"issueId":"iss_123"}'
49
+
50
+ # --arg overlays string values onto the --json base
51
+ mailtea call email.send --json '{"from":"you@d.com","to":"u@e.com","subject":"Hi"}' --arg html='<p>hi</p>'
52
+ ```
53
+
54
+ `mailtea call` prints the tool's structured result on stdout; a tool/API error is
55
+ JSON on stderr with a non-zero exit code. New MCP tools appear automatically.
56
+
57
+ ## Named commands
58
+
59
+ ```bash
60
+ mailtea emails send --from <a> --to <a> --subject <s> (--html <h> | --text <t> | --template-id <id>)
61
+ mailtea emails batch --json '[{...}, ...]'
62
+ mailtea emails list [--status <s>] [--limit <n>] [--offset <n>] [--tag-name <n>] [--tag-value <v>]
63
+ mailtea emails get|cancel <id>
64
+ mailtea emails reschedule <id> --scheduled-at <iso>
65
+ mailtea emails analytics [--from-date <iso>] [--to-date <iso>]
66
+ mailtea posts send-test <post-id> --to <addr> --from <addr> [--reply-to <addr>]
67
+
68
+ mailtea contacts create|list|get|update|delete --publication-id <pub> ...
69
+ mailtea segments create|list|get|update|delete --publication-id <pub> ...
70
+ mailtea tags create|list|get|update|delete --publication-id <pub> ...
71
+ mailtea domains create|list|get|verify|update|delete --publication-id <pub> ...
72
+ mailtea domains tracking create|list|verify|delete --publication-id <pub> --domain-id <id> ...
73
+ mailtea webhooks create|list|get|update|delete --publication-id <pub> [--event <e> ...]
74
+ mailtea contact-properties create|list|update|delete ... # team-scoped, no --publication-id
75
+ mailtea api-keys create|list|revoke ...
76
+ mailtea templates create|list|get|update|publish|duplicate|delete --publication-id <pub> ...
77
+ mailtea inbound list --publication-id <pub> [--limit <n>] [--cursor <c>]
78
+ mailtea inbound get|attachments <id>
79
+ mailtea inbound attachment <id> <attachment-id>
80
+ mailtea inbound reply <id> (--html <h> | --text <t>) [--from <addr>] [--from-name <n>] [--subject <s>] [--cc <a>] [--bcc <a>] [--idempotency-key <k>]
81
+
82
+ mailtea login --api-key mt_pat_xxx [--base-url <url>]
83
+ mailtea version | help
84
+ ```
85
+
86
+ `emails send` extras: `--cc` / `--bcc` / `--reply-to` (repeatable or comma-separated),
87
+ `--scheduled-at <iso>`, `--tag name=value`, `--header name=value`,
88
+ `--template-var name=value` (all repeatable), and `--dry-run` to print the
89
+ resolved payload without sending.
90
+
91
+ ## Agent-friendly
92
+
93
+ Output is **JSON on stdout**; errors are **JSON on stderr** with a non-zero exit
94
+ code — so an agent in a non-TTY shell can parse results and detect failures.
95
+
96
+ ```bash
97
+ ID=$(mailtea emails send --from you@d.com --to u@e.com --subject Hi --text hi | jq -r .id)
98
+ mailtea emails get "$ID" | jq -r .last_event
99
+ ```
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ // Executable entry for the `mailtea` CLI. Delegates to the built run() and maps
3
+ // its return value to a process exit code.
4
+ import { run } from "../dist/cli.js";
5
+
6
+ run(process.argv.slice(2))
7
+ .then((code) => process.exit(code))
8
+ .catch((error) => {
9
+ console.error(error instanceof Error ? error.message : String(error));
10
+ process.exit(1);
11
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ declare const VERSION = "0.1.0";
2
+ interface RunDeps {
3
+ env?: Record<string, string | undefined>;
4
+ stdout?: (line: string) => void;
5
+ stderr?: (line: string) => void;
6
+ /** Injected for tests; defaults to the global fetch via the SDK. */
7
+ fetchImpl?: typeof fetch;
8
+ /** Home directory for the `login` config file; defaults to os.homedir(). */
9
+ homeDir?: string;
10
+ }
11
+ /**
12
+ * Run the Mailtea CLI. Returns a process exit code (0 ok, non-zero error).
13
+ * All side effects go through injectable deps so the command surface is testable.
14
+ */
15
+ declare function run(argv: string[], deps?: RunDeps): Promise<number>;
16
+
17
+ export { type RunDeps, VERSION, run };
package/dist/cli.js ADDED
@@ -0,0 +1,975 @@
1
+ // src/cli.ts
2
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ import { Mailtea, MailteaError } from "mailtea-sdk";
6
+ import { handleMcpRequest, MCP_TOOLS } from "mailtea-mcp";
7
+
8
+ // src/args.ts
9
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["dry-run", "help", "version", "pretty", "names"]);
10
+ function parseArgs(argv) {
11
+ const positionals = [];
12
+ const values = {};
13
+ const push = (key, value) => {
14
+ (values[key] ??= []).push(value);
15
+ };
16
+ for (let i = 0; i < argv.length; i++) {
17
+ const token = argv[i];
18
+ if (token === "--") {
19
+ positionals.push(...argv.slice(i + 1));
20
+ break;
21
+ }
22
+ if (token.startsWith("--")) {
23
+ const body = token.slice(2);
24
+ const eq = body.indexOf("=");
25
+ if (eq !== -1) {
26
+ push(body.slice(0, eq), body.slice(eq + 1));
27
+ } else if (BOOLEAN_FLAGS.has(body)) {
28
+ push(body, "true");
29
+ } else {
30
+ const next = argv[i + 1];
31
+ if (next !== void 0 && next !== "--" && !next.startsWith("--")) {
32
+ push(body, next);
33
+ i++;
34
+ } else {
35
+ push(body, "true");
36
+ }
37
+ }
38
+ } else {
39
+ positionals.push(token);
40
+ }
41
+ }
42
+ return { positionals, values };
43
+ }
44
+ function flagValue(parsed, key) {
45
+ const arr = parsed.values[key];
46
+ return arr && arr.length > 0 ? arr[arr.length - 1] : void 0;
47
+ }
48
+ function flagList(parsed, key) {
49
+ return parsed.values[key] ?? [];
50
+ }
51
+ function flagBool(parsed, key) {
52
+ return (parsed.values[key] ?? []).includes("true");
53
+ }
54
+ function flagPairs(parsed, key) {
55
+ return flagList(parsed, key).map((raw) => {
56
+ const eq = raw.indexOf("=");
57
+ return eq === -1 ? { name: raw, value: "" } : { name: raw.slice(0, eq), value: raw.slice(eq + 1) };
58
+ });
59
+ }
60
+
61
+ // src/cli.ts
62
+ var VERSION = "0.1.0";
63
+ async function run(argv, deps = {}) {
64
+ const ctx = {
65
+ env: deps.env ?? process.env,
66
+ out: deps.stdout ?? ((line) => process.stdout.write(`${line}
67
+ `)),
68
+ err: deps.stderr ?? ((line) => process.stderr.write(`${line}
69
+ `)),
70
+ home: deps.homeDir ?? homedir(),
71
+ fetchImpl: deps.fetchImpl
72
+ };
73
+ const parsed = parseArgs(argv);
74
+ const [group, sub, ...rest] = parsed.positionals;
75
+ if (group === "version" || flagBool(parsed, "version")) {
76
+ ctx.out(VERSION);
77
+ return 0;
78
+ }
79
+ if (!group || group === "help" || flagBool(parsed, "help")) {
80
+ ctx.out(USAGE);
81
+ return 0;
82
+ }
83
+ if (group === "login") {
84
+ return doLogin(parsed, ctx);
85
+ }
86
+ if (group === "tools") {
87
+ return toolsCommand(parsed, ctx);
88
+ }
89
+ if (group === "schema") {
90
+ return schemaCommand(sub, ctx);
91
+ }
92
+ if (group === "call") {
93
+ return callCommand(sub, parsed, ctx);
94
+ }
95
+ if (group === "emails") {
96
+ return emailsCommand(sub, rest, parsed, ctx);
97
+ }
98
+ if (group === "posts") {
99
+ return postsCommand(sub, rest, parsed, ctx);
100
+ }
101
+ if (group === "contacts") {
102
+ return contactsCommand(sub, rest, parsed, ctx);
103
+ }
104
+ if (group === "segments") {
105
+ return segmentsCommand(sub, rest, parsed, ctx);
106
+ }
107
+ if (group === "tags") {
108
+ return tagsCommand(sub, rest, parsed, ctx);
109
+ }
110
+ if (group === "domains") {
111
+ return domainsCommand(sub, rest, parsed, ctx);
112
+ }
113
+ if (group === "webhooks") {
114
+ return webhooksCommand(sub, rest, parsed, ctx);
115
+ }
116
+ if (group === "contact-properties") {
117
+ return contactPropertiesCommand(sub, rest, parsed, ctx);
118
+ }
119
+ if (group === "api-keys") {
120
+ return apiKeysCommand(sub, rest, parsed, ctx);
121
+ }
122
+ if (group === "templates") {
123
+ return templatesCommand(sub, rest, parsed, ctx);
124
+ }
125
+ if (group === "inbound") {
126
+ return inboundCommand(sub, rest, parsed, ctx);
127
+ }
128
+ ctx.err(`Unknown command: ${group}. Run \`mailtea help\`.`);
129
+ return 1;
130
+ }
131
+ function toolsCommand(parsed, ctx) {
132
+ const namesOnly = flagBool(parsed, "names");
133
+ const list = MCP_TOOLS.map(
134
+ (tool) => namesOnly ? tool.name : { name: tool.name, description: tool.description }
135
+ );
136
+ ctx.out(JSON.stringify(list));
137
+ return 0;
138
+ }
139
+ function schemaCommand(name, ctx) {
140
+ if (!name) return fail(ctx, "schema requires a <tool> name. Run `mailtea tools`.");
141
+ const tool = MCP_TOOLS.find((t) => t.name === name);
142
+ if (!tool) {
143
+ ctx.err(JSON.stringify({ error: `Unknown tool: ${name}. Run \`mailtea tools\`.` }));
144
+ return 1;
145
+ }
146
+ ctx.out(JSON.stringify(tool.inputSchema));
147
+ return 0;
148
+ }
149
+ async function callCommand(name, parsed, ctx) {
150
+ if (!name) return fail(ctx, "call requires a <tool> name. Run `mailtea tools`.");
151
+ let args = {};
152
+ const json = flagValue(parsed, "json");
153
+ if (json !== void 0) {
154
+ let parsedJson;
155
+ try {
156
+ parsedJson = JSON.parse(json);
157
+ } catch {
158
+ return fail(ctx, "--json must be valid JSON");
159
+ }
160
+ if (!parsedJson || typeof parsedJson !== "object" || Array.isArray(parsedJson)) {
161
+ return fail(ctx, "--json must be a JSON object");
162
+ }
163
+ args = parsedJson;
164
+ }
165
+ for (const { name: key, value } of flagPairs(parsed, "arg")) {
166
+ args[key] = value;
167
+ }
168
+ return runBridgeTool(parsed, ctx, name, args);
169
+ }
170
+ async function runBridgeTool(parsed, ctx, toolName, args) {
171
+ const auth = resolveAuth(parsed, ctx);
172
+ if (!auth) return 1;
173
+ const res = await handleMcpRequest(
174
+ { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: toolName, arguments: args } },
175
+ { apiBaseUrl: auth.baseUrl, token: auth.apiKey, fetchImpl: ctx.fetchImpl }
176
+ );
177
+ if (res.error) {
178
+ ctx.err(JSON.stringify({ error: res.error.message, code: res.error.code, details: res.error.data }));
179
+ return 1;
180
+ }
181
+ const result = res.result;
182
+ const text = (result?.content ?? []).map((c) => c.text ?? "").join("\n");
183
+ if (result?.isError) {
184
+ ctx.err(JSON.stringify({ error: text || "Tool returned an error" }));
185
+ return 1;
186
+ }
187
+ ctx.out(JSON.stringify(result?.structuredContent ?? { text }));
188
+ return 0;
189
+ }
190
+ async function emailsCommand(sub, rest, parsed, ctx) {
191
+ switch (sub) {
192
+ case "send": {
193
+ const payload = buildSendPayload(parsed);
194
+ const problem = validateSend(payload);
195
+ if (problem) {
196
+ ctx.err(problem);
197
+ return 1;
198
+ }
199
+ if (flagBool(parsed, "dry-run")) {
200
+ ctx.out(JSON.stringify(payload));
201
+ return 0;
202
+ }
203
+ return callApi(
204
+ parsed,
205
+ ctx,
206
+ (client) => client.emails.send(payload)
207
+ );
208
+ }
209
+ case "batch": {
210
+ const json = flagValue(parsed, "json");
211
+ if (!json) return fail(ctx, "emails batch requires --json '[{...}, ...]' (a JSON array of emails)");
212
+ let emails;
213
+ try {
214
+ emails = JSON.parse(json);
215
+ } catch {
216
+ return fail(ctx, "emails batch --json must be valid JSON");
217
+ }
218
+ if (!Array.isArray(emails)) return fail(ctx, "emails batch --json must be a JSON array");
219
+ return callApi(parsed, ctx, (client) => client.emails.batch(emails));
220
+ }
221
+ case "list": {
222
+ let params;
223
+ try {
224
+ params = buildListParams(parsed);
225
+ } catch (error) {
226
+ return fail(ctx, error instanceof Error ? error.message : String(error));
227
+ }
228
+ return callApi(parsed, ctx, (client) => client.emails.list(params));
229
+ }
230
+ case "get": {
231
+ const id = rest[0];
232
+ if (!id) return fail(ctx, "emails get requires an <id>");
233
+ return callApi(parsed, ctx, (client) => client.emails.get(id));
234
+ }
235
+ case "cancel": {
236
+ const id = rest[0];
237
+ if (!id) return fail(ctx, "emails cancel requires an <id>");
238
+ return callApi(parsed, ctx, (client) => client.emails.cancel(id));
239
+ }
240
+ case "reschedule": {
241
+ const id = rest[0];
242
+ const at = flagValue(parsed, "scheduled-at");
243
+ if (!id || !at) return fail(ctx, "emails reschedule requires <id> and --scheduled-at <iso>");
244
+ return callApi(parsed, ctx, (client) => client.emails.reschedule(id, at));
245
+ }
246
+ case "analytics":
247
+ return callApi(
248
+ parsed,
249
+ ctx,
250
+ (client) => client.emails.analytics({
251
+ from_date: flagValue(parsed, "from-date"),
252
+ to_date: flagValue(parsed, "to-date")
253
+ })
254
+ );
255
+ default:
256
+ return fail(
257
+ ctx,
258
+ `Unknown emails subcommand: ${sub ?? "(none)"}. Try: send, batch, list, get, cancel, reschedule, analytics.`
259
+ );
260
+ }
261
+ }
262
+ async function postsCommand(sub, rest, parsed, ctx) {
263
+ switch (sub) {
264
+ case "create": {
265
+ const pub = flagValue(parsed, "publication-id");
266
+ const subject = flagValue(parsed, "subject");
267
+ const templateId = flagValue(parsed, "template-id");
268
+ const html = flagValue(parsed, "html");
269
+ const kind = flagValue(parsed, "kind");
270
+ if (!pub) return fail(ctx, "posts create requires --publication-id");
271
+ if (!subject) return fail(ctx, "posts create requires --subject");
272
+ if (!templateId && !html) return fail(ctx, "posts create requires --template-id or --html");
273
+ if (templateId && html) return fail(ctx, "posts create: use --template-id OR --html, not both");
274
+ if (kind && kind !== "newsletter" && kind !== "broadcast") {
275
+ return fail(ctx, "posts create --kind must be 'newsletter' or 'broadcast'");
276
+ }
277
+ const vars = flagPairs(parsed, "template-var");
278
+ return callApi(
279
+ parsed,
280
+ ctx,
281
+ (client) => client.posts.create({
282
+ publication_id: pub,
283
+ subject,
284
+ ...templateId ? { template_id: templateId } : {},
285
+ ...vars.length ? { variables: Object.fromEntries(vars.map((v) => [v.name, v.value])) } : {},
286
+ ...html ? { html } : {},
287
+ ...kind ? { kind } : {}
288
+ })
289
+ );
290
+ }
291
+ case "send-test": {
292
+ const id = rest[0];
293
+ const from = flagValue(parsed, "from");
294
+ const recipients = splitList(flagList(parsed, "to"));
295
+ if (!id) return fail(ctx, "posts send-test requires a <post-id>");
296
+ if (!from) return fail(ctx, "posts send-test requires --from");
297
+ if (recipients.length === 0) return fail(ctx, "posts send-test requires --to");
298
+ const replyTo = flagValue(parsed, "reply-to");
299
+ return callApi(
300
+ parsed,
301
+ ctx,
302
+ (client) => client.posts.sendTest(id, {
303
+ recipients,
304
+ from,
305
+ ...replyTo ? { reply_to: replyTo } : {}
306
+ })
307
+ );
308
+ }
309
+ default:
310
+ return fail(ctx, `Unknown posts subcommand: ${sub ?? "(none)"}. Try: create, send-test.`);
311
+ }
312
+ }
313
+ async function contactsCommand(sub, rest, parsed, ctx) {
314
+ const pub = flagValue(parsed, "publication-id");
315
+ switch (sub) {
316
+ case "create": {
317
+ if (!pub) return fail(ctx, "contacts create requires --publication-id");
318
+ const email = flagValue(parsed, "email");
319
+ if (!email) return fail(ctx, "contacts create requires --email");
320
+ const status = flagValue(parsed, "status");
321
+ return callApi(
322
+ parsed,
323
+ ctx,
324
+ (client) => client.contacts.create({ publication_id: pub, email, ...status ? { status } : {} })
325
+ );
326
+ }
327
+ case "list": {
328
+ if (!pub) return fail(ctx, "contacts list requires --publication-id");
329
+ let params;
330
+ try {
331
+ params = listParams(parsed, pub);
332
+ } catch (error) {
333
+ return fail(ctx, error instanceof Error ? error.message : String(error));
334
+ }
335
+ const status = flagValue(parsed, "status");
336
+ const search = flagValue(parsed, "search");
337
+ if (status) params.status = status;
338
+ if (search) params.search = search;
339
+ return callApi(parsed, ctx, (client) => client.contacts.list(params));
340
+ }
341
+ case "get": {
342
+ const idOrEmail = rest[0];
343
+ if (!idOrEmail) return fail(ctx, "contacts get requires an <id-or-email>");
344
+ if (!pub) return fail(ctx, "contacts get requires --publication-id");
345
+ return callApi(parsed, ctx, (client) => client.contacts.get(idOrEmail, { publication_id: pub }));
346
+ }
347
+ case "update": {
348
+ const idOrEmail = rest[0];
349
+ if (!idOrEmail) return fail(ctx, "contacts update requires an <id-or-email>");
350
+ if (!pub) return fail(ctx, "contacts update requires --publication-id");
351
+ const status = flagValue(parsed, "status");
352
+ return callApi(
353
+ parsed,
354
+ ctx,
355
+ (client) => client.contacts.update(idOrEmail, { publication_id: pub, ...status ? { status } : {} })
356
+ );
357
+ }
358
+ case "delete": {
359
+ const idOrEmail = rest[0];
360
+ if (!idOrEmail) return fail(ctx, "contacts delete requires an <id-or-email>");
361
+ if (!pub) return fail(ctx, "contacts delete requires --publication-id");
362
+ return callApi(parsed, ctx, (client) => client.contacts.delete(idOrEmail, { publication_id: pub }));
363
+ }
364
+ default:
365
+ return fail(ctx, `Unknown contacts subcommand: ${sub ?? "(none)"}. Try: create, list, get, update, delete.`);
366
+ }
367
+ }
368
+ async function segmentsCommand(sub, rest, parsed, ctx) {
369
+ const pub = flagValue(parsed, "publication-id");
370
+ switch (sub) {
371
+ case "create": {
372
+ if (!pub) return fail(ctx, "segments create requires --publication-id");
373
+ const name = flagValue(parsed, "name");
374
+ if (!name) return fail(ctx, "segments create requires --name");
375
+ const body = { publication_id: pub, name, ...collect(parsed, ["description", "status-filter", "query-filter"]) };
376
+ return callApi(parsed, ctx, (client) => client.segments.create(body));
377
+ }
378
+ case "list": {
379
+ if (!pub) return fail(ctx, "segments list requires --publication-id");
380
+ return listCall(parsed, ctx, pub, (client, params) => client.segments.list(params));
381
+ }
382
+ case "get": {
383
+ const id = rest[0];
384
+ if (!id || !pub) return fail(ctx, "segments get requires <id> and --publication-id");
385
+ return callApi(parsed, ctx, (client) => client.segments.get(id, { publication_id: pub }));
386
+ }
387
+ case "update": {
388
+ const id = rest[0];
389
+ if (!id || !pub) return fail(ctx, "segments update requires <id> and --publication-id");
390
+ const body = { publication_id: pub, ...collect(parsed, ["name", "description", "status-filter", "query-filter"]) };
391
+ return callApi(parsed, ctx, (client) => client.segments.update(id, body));
392
+ }
393
+ case "delete": {
394
+ const id = rest[0];
395
+ if (!id || !pub) return fail(ctx, "segments delete requires <id> and --publication-id");
396
+ return callApi(parsed, ctx, (client) => client.segments.delete(id, { publication_id: pub }));
397
+ }
398
+ default:
399
+ return fail(ctx, `Unknown segments subcommand: ${sub ?? "(none)"}. Try: create, list, get, update, delete.`);
400
+ }
401
+ }
402
+ async function tagsCommand(sub, rest, parsed, ctx) {
403
+ const pub = flagValue(parsed, "publication-id");
404
+ switch (sub) {
405
+ case "create": {
406
+ if (!pub) return fail(ctx, "tags create requires --publication-id");
407
+ const name = flagValue(parsed, "name");
408
+ const defaultSubscription = flagValue(parsed, "default-subscription");
409
+ if (!name) return fail(ctx, "tags create requires --name");
410
+ if (!defaultSubscription) return fail(ctx, "tags create requires --default-subscription (opt_in|opt_out)");
411
+ const body = {
412
+ publication_id: pub,
413
+ name,
414
+ default_subscription: defaultSubscription,
415
+ ...collect(parsed, ["description", "visibility"])
416
+ };
417
+ return callApi(parsed, ctx, (client) => client.tags.create(body));
418
+ }
419
+ case "list": {
420
+ if (!pub) return fail(ctx, "tags list requires --publication-id");
421
+ return listCall(parsed, ctx, pub, (client, params) => client.tags.list(params));
422
+ }
423
+ case "get": {
424
+ const id = rest[0];
425
+ if (!id || !pub) return fail(ctx, "tags get requires <id> and --publication-id");
426
+ return callApi(parsed, ctx, (client) => client.tags.get(id, { publication_id: pub }));
427
+ }
428
+ case "update": {
429
+ const id = rest[0];
430
+ if (!id || !pub) return fail(ctx, "tags update requires <id> and --publication-id");
431
+ const body = {
432
+ publication_id: pub,
433
+ ...collect(parsed, ["name", "description", "default-subscription", "visibility"])
434
+ };
435
+ return callApi(parsed, ctx, (client) => client.tags.update(id, body));
436
+ }
437
+ case "delete": {
438
+ const id = rest[0];
439
+ if (!id || !pub) return fail(ctx, "tags delete requires <id> and --publication-id");
440
+ return callApi(parsed, ctx, (client) => client.tags.delete(id, { publication_id: pub }));
441
+ }
442
+ default:
443
+ return fail(ctx, `Unknown tags subcommand: ${sub ?? "(none)"}. Try: create, list, get, update, delete.`);
444
+ }
445
+ }
446
+ async function domainsCommand(sub, rest, parsed, ctx) {
447
+ if (sub === "tracking") {
448
+ return domainsTrackingCommand(rest[0], rest.slice(1), parsed, ctx);
449
+ }
450
+ const pub = flagValue(parsed, "publication-id");
451
+ switch (sub) {
452
+ case "create": {
453
+ if (!pub) return fail(ctx, "domains create requires --publication-id");
454
+ const name = flagValue(parsed, "name");
455
+ if (!name) return fail(ctx, "domains create requires --name");
456
+ const body = { publication_id: pub, name, ...collect(parsed, ["purpose", "proxy-target"]) };
457
+ const isPrimary = boolFlag(parsed, "is-primary");
458
+ if (isPrimary !== void 0) body.is_primary = isPrimary;
459
+ return callApi(parsed, ctx, (client) => client.domains.create(body));
460
+ }
461
+ case "list": {
462
+ if (!pub) return fail(ctx, "domains list requires --publication-id");
463
+ return listCall(parsed, ctx, pub, (client, params) => client.domains.list(params));
464
+ }
465
+ case "get": {
466
+ const id = rest[0];
467
+ if (!id || !pub) return fail(ctx, "domains get requires <id> and --publication-id");
468
+ return callApi(parsed, ctx, (client) => client.domains.get(id, { publication_id: pub }));
469
+ }
470
+ case "verify": {
471
+ const id = rest[0];
472
+ if (!id || !pub) return fail(ctx, "domains verify requires <id> and --publication-id");
473
+ return callApi(parsed, ctx, (client) => client.domains.verify(id, { publication_id: pub }));
474
+ }
475
+ case "update": {
476
+ const id = rest[0];
477
+ if (!id || !pub) return fail(ctx, "domains update requires <id> and --publication-id");
478
+ const body = { publication_id: pub, ...collect(parsed, ["purpose", "proxy-target"]) };
479
+ const isPrimary = boolFlag(parsed, "is-primary");
480
+ if (isPrimary !== void 0) body.is_primary = isPrimary;
481
+ return callApi(parsed, ctx, (client) => client.domains.update(id, body));
482
+ }
483
+ case "delete": {
484
+ const id = rest[0];
485
+ if (!id || !pub) return fail(ctx, "domains delete requires <id> and --publication-id");
486
+ return callApi(parsed, ctx, (client) => client.domains.delete(id, { publication_id: pub }));
487
+ }
488
+ default:
489
+ return fail(
490
+ ctx,
491
+ `Unknown domains subcommand: ${sub ?? "(none)"}. Try: create, list, get, verify, update, delete, tracking.`
492
+ );
493
+ }
494
+ }
495
+ async function domainsTrackingCommand(verb, rest, parsed, ctx) {
496
+ const pub = flagValue(parsed, "publication-id");
497
+ const domainId = flagValue(parsed, "domain-id");
498
+ if (!pub) return fail(ctx, "domains tracking requires --publication-id");
499
+ if (!domainId) return fail(ctx, "domains tracking requires --domain-id");
500
+ switch (verb) {
501
+ case "create": {
502
+ const subdomain = flagValue(parsed, "subdomain");
503
+ if (!subdomain) return fail(ctx, "domains tracking create requires --subdomain");
504
+ return callApi(
505
+ parsed,
506
+ ctx,
507
+ (client) => client.domains.tracking.create(domainId, { publication_id: pub, subdomain })
508
+ );
509
+ }
510
+ case "list":
511
+ return callApi(parsed, ctx, (client) => client.domains.tracking.list(domainId, { publication_id: pub }));
512
+ case "verify": {
513
+ const id = rest[0];
514
+ if (!id) return fail(ctx, "domains tracking verify requires a <tracking-domain-id>");
515
+ return callApi(parsed, ctx, (client) => client.domains.tracking.verify(domainId, id, { publication_id: pub }));
516
+ }
517
+ case "delete": {
518
+ const id = rest[0];
519
+ if (!id) return fail(ctx, "domains tracking delete requires a <tracking-domain-id>");
520
+ return callApi(parsed, ctx, (client) => client.domains.tracking.delete(domainId, id, { publication_id: pub }));
521
+ }
522
+ default:
523
+ return fail(ctx, `Unknown domains tracking subcommand: ${verb ?? "(none)"}. Try: create, list, verify, delete.`);
524
+ }
525
+ }
526
+ async function webhooksCommand(sub, rest, parsed, ctx) {
527
+ const pub = flagValue(parsed, "publication-id");
528
+ switch (sub) {
529
+ case "create": {
530
+ if (!pub) return fail(ctx, "webhooks create requires --publication-id");
531
+ const endpoint = flagValue(parsed, "endpoint");
532
+ const events = splitList(flagList(parsed, "event"));
533
+ if (!endpoint) return fail(ctx, "webhooks create requires --endpoint");
534
+ if (events.length === 0) return fail(ctx, "webhooks create requires at least one --event");
535
+ return callApi(
536
+ parsed,
537
+ ctx,
538
+ (client) => client.webhooks.create({ publication_id: pub, endpoint, events })
539
+ );
540
+ }
541
+ case "list": {
542
+ if (!pub) return fail(ctx, "webhooks list requires --publication-id");
543
+ return listCall(parsed, ctx, pub, (client, params) => client.webhooks.list(params));
544
+ }
545
+ case "get": {
546
+ const id = rest[0];
547
+ if (!id || !pub) return fail(ctx, "webhooks get requires <id> and --publication-id");
548
+ return callApi(parsed, ctx, (client) => client.webhooks.get(id, { publication_id: pub }));
549
+ }
550
+ case "update": {
551
+ const id = rest[0];
552
+ if (!id || !pub) return fail(ctx, "webhooks update requires <id> and --publication-id");
553
+ const body = { publication_id: pub, ...collect(parsed, ["endpoint", "status"]) };
554
+ const events = splitList(flagList(parsed, "event"));
555
+ if (events.length > 0) body.events = events;
556
+ return callApi(parsed, ctx, (client) => client.webhooks.update(id, body));
557
+ }
558
+ case "delete": {
559
+ const id = rest[0];
560
+ if (!id || !pub) return fail(ctx, "webhooks delete requires <id> and --publication-id");
561
+ return callApi(parsed, ctx, (client) => client.webhooks.delete(id, { publication_id: pub }));
562
+ }
563
+ default:
564
+ return fail(ctx, `Unknown webhooks subcommand: ${sub ?? "(none)"}. Try: create, list, get, update, delete.`);
565
+ }
566
+ }
567
+ async function contactPropertiesCommand(sub, rest, parsed, ctx) {
568
+ switch (sub) {
569
+ case "create": {
570
+ const key = flagValue(parsed, "key");
571
+ const type = flagValue(parsed, "type");
572
+ if (!key) return fail(ctx, "contact-properties create requires --key");
573
+ if (!type) return fail(ctx, "contact-properties create requires --type (string|number)");
574
+ const body = { key, type, ...collect(parsed, ["fallback-value", "description"]) };
575
+ return callApi(parsed, ctx, (client) => client.contactProperties.create(body));
576
+ }
577
+ case "list":
578
+ return callApi(parsed, ctx, (client) => {
579
+ const params = {};
580
+ const limit = intFlag(parsed, "limit");
581
+ if (limit !== void 0) params.limit = limit;
582
+ const after = flagValue(parsed, "after");
583
+ if (after) params.after = after;
584
+ return client.contactProperties.list(params);
585
+ });
586
+ case "update": {
587
+ const id = rest[0];
588
+ if (!id) return fail(ctx, "contact-properties update requires an <id>");
589
+ const body = collect(parsed, ["fallback-value", "description"]);
590
+ return callApi(parsed, ctx, (client) => client.contactProperties.update(id, body));
591
+ }
592
+ case "delete": {
593
+ const id = rest[0];
594
+ if (!id) return fail(ctx, "contact-properties delete requires an <id>");
595
+ return callApi(parsed, ctx, (client) => client.contactProperties.delete(id));
596
+ }
597
+ default:
598
+ return fail(ctx, `Unknown contact-properties subcommand: ${sub ?? "(none)"}. Try: create, list, update, delete.`);
599
+ }
600
+ }
601
+ async function apiKeysCommand(sub, rest, parsed, ctx) {
602
+ switch (sub) {
603
+ case "create": {
604
+ const name = flagValue(parsed, "name");
605
+ if (!name) return fail(ctx, "api-keys create requires --name");
606
+ const body = { name, ...collect(parsed, ["permission", "domain-id"]) };
607
+ return callApi(parsed, ctx, (client) => client.apiKeys.create(body));
608
+ }
609
+ case "list":
610
+ return callApi(parsed, ctx, (client) => client.apiKeys.list());
611
+ case "revoke": {
612
+ const id = rest[0];
613
+ if (!id) return fail(ctx, "api-keys revoke requires an <id>");
614
+ return callApi(parsed, ctx, (client) => client.apiKeys.revoke(id));
615
+ }
616
+ default:
617
+ return fail(ctx, `Unknown api-keys subcommand: ${sub ?? "(none)"}. Try: create, list, revoke.`);
618
+ }
619
+ }
620
+ async function templatesCommand(sub, rest, parsed, ctx) {
621
+ const pub = flagValue(parsed, "publication-id");
622
+ if (!pub) return fail(ctx, "templates commands require --publication-id");
623
+ switch (sub) {
624
+ case "create": {
625
+ const name = flagValue(parsed, "name");
626
+ if (!name) return fail(ctx, "templates create requires --name");
627
+ const html = flagValue(parsed, "html");
628
+ const spec = parseJsonFlag(parsed, "spec");
629
+ if (spec === INVALID_JSON) return fail(ctx, "templates create --spec must be valid JSON");
630
+ if (!html && spec === void 0) return fail(ctx, "templates create requires --html or --spec");
631
+ const args = { publicationId: pub, name };
632
+ if (html) args.html = html;
633
+ if (spec !== void 0) args.spec = spec;
634
+ const description = flagValue(parsed, "description");
635
+ const subject = flagValue(parsed, "subject");
636
+ if (description) args.description = description;
637
+ if (subject) args.subject = subject;
638
+ return runBridgeTool(parsed, ctx, "template.create", args);
639
+ }
640
+ case "list": {
641
+ const args = { publicationId: pub };
642
+ const limit = intFlag(parsed, "limit");
643
+ if (limit !== void 0) args.limit = limit;
644
+ return runBridgeTool(parsed, ctx, "template.list", args);
645
+ }
646
+ case "get": {
647
+ const id = rest[0];
648
+ if (!id) return fail(ctx, "templates get requires a <template-id>");
649
+ return runBridgeTool(parsed, ctx, "template.get", { publicationId: pub, templateId: id });
650
+ }
651
+ case "update": {
652
+ const id = rest[0];
653
+ if (!id) return fail(ctx, "templates update requires a <template-id>");
654
+ const args = { publicationId: pub, templateId: id };
655
+ for (const key of ["name", "html", "description", "text", "subject", "from", "reply-to"]) {
656
+ const v = flagValue(parsed, key);
657
+ if (v !== void 0) args[key === "reply-to" ? "reply_to" : key] = v;
658
+ }
659
+ const spec = parseJsonFlag(parsed, "spec");
660
+ if (spec === INVALID_JSON) return fail(ctx, "templates update --spec must be valid JSON");
661
+ if (spec !== void 0) args.spec = spec;
662
+ return runBridgeTool(parsed, ctx, "template.update", args);
663
+ }
664
+ case "publish": {
665
+ const id = rest[0];
666
+ if (!id) return fail(ctx, "templates publish requires a <template-id>");
667
+ return runBridgeTool(parsed, ctx, "template.publish", { publicationId: pub, templateId: id });
668
+ }
669
+ case "duplicate": {
670
+ const id = rest[0];
671
+ if (!id) return fail(ctx, "templates duplicate requires a <template-id>");
672
+ return runBridgeTool(parsed, ctx, "template.duplicate", { publicationId: pub, templateId: id });
673
+ }
674
+ case "delete": {
675
+ const id = rest[0];
676
+ if (!id) return fail(ctx, "templates delete requires a <template-id>");
677
+ return runBridgeTool(parsed, ctx, "template.delete", { publicationId: pub, templateId: id });
678
+ }
679
+ default:
680
+ return fail(
681
+ ctx,
682
+ `Unknown templates subcommand: ${sub ?? "(none)"}. Try: create, list, get, update, publish, duplicate, delete.`
683
+ );
684
+ }
685
+ }
686
+ async function inboundCommand(sub, rest, parsed, ctx) {
687
+ switch (sub) {
688
+ case "list": {
689
+ const pub = flagValue(parsed, "publication-id");
690
+ if (!pub) return fail(ctx, "inbound list requires --publication-id");
691
+ const params = { publication_id: pub };
692
+ let limit;
693
+ try {
694
+ limit = intFlag(parsed, "limit");
695
+ } catch (error) {
696
+ return fail(ctx, error instanceof Error ? error.message : String(error));
697
+ }
698
+ if (limit !== void 0) params.limit = limit;
699
+ const cursor = flagValue(parsed, "cursor");
700
+ if (cursor) params.cursor = cursor;
701
+ return callApi(parsed, ctx, (client) => client.emails.inbound.list(params));
702
+ }
703
+ case "get": {
704
+ const id = rest[0];
705
+ if (!id) return fail(ctx, "inbound get requires an <id>");
706
+ return callApi(parsed, ctx, (client) => client.emails.inbound.get(id));
707
+ }
708
+ case "attachments": {
709
+ const id = rest[0];
710
+ if (!id) return fail(ctx, "inbound attachments requires an <id>");
711
+ return callApi(parsed, ctx, (client) => client.emails.inbound.attachments.list(id));
712
+ }
713
+ case "attachment": {
714
+ const id = rest[0];
715
+ const attachmentId = rest[1];
716
+ if (!id || !attachmentId) return fail(ctx, "inbound attachment requires <id> and <attachment-id>");
717
+ return callApi(parsed, ctx, (client) => client.emails.inbound.attachments.get(id, attachmentId));
718
+ }
719
+ case "reply": {
720
+ const id = rest[0];
721
+ if (!id) return fail(ctx, "inbound reply requires an <id>");
722
+ const html = flagValue(parsed, "html");
723
+ const text = flagValue(parsed, "text");
724
+ if (!html && !text) return fail(ctx, "inbound reply requires --html or --text");
725
+ const body = {};
726
+ const fromEmail = flagValue(parsed, "from");
727
+ if (fromEmail) {
728
+ const fromName = flagValue(parsed, "from-name");
729
+ body.from = { email: fromEmail, ...fromName ? { name: fromName } : {} };
730
+ }
731
+ const subject = flagValue(parsed, "subject");
732
+ if (subject) body.subject = subject;
733
+ if (html) body.html = html;
734
+ if (text) body.text = text;
735
+ const cc = splitList(flagList(parsed, "cc"));
736
+ if (cc.length) body.cc = cc;
737
+ const bcc = splitList(flagList(parsed, "bcc"));
738
+ if (bcc.length) body.bcc = bcc;
739
+ const idempotencyKey = flagValue(parsed, "idempotency-key");
740
+ if (idempotencyKey) body.idempotency_key = idempotencyKey;
741
+ return callApi(parsed, ctx, (client) => client.emails.inbound.reply(id, body));
742
+ }
743
+ default:
744
+ return fail(
745
+ ctx,
746
+ `Unknown inbound subcommand: ${sub ?? "(none)"}. Try: list, get, attachments, attachment, reply.`
747
+ );
748
+ }
749
+ }
750
+ function doLogin(parsed, ctx) {
751
+ const apiKey = flagValue(parsed, "api-key");
752
+ if (!apiKey) {
753
+ ctx.err("login requires --api-key mt_pat_xxx");
754
+ return 1;
755
+ }
756
+ const baseUrl = flagValue(parsed, "base-url");
757
+ const dir = join(ctx.home, ".mailtea");
758
+ mkdirSync(dir, { recursive: true });
759
+ const path = join(dir, "config.json");
760
+ writeFileSync(
761
+ path,
762
+ `${JSON.stringify({ apiKey, ...baseUrl ? { baseUrl } : {} }, null, 2)}
763
+ `,
764
+ { mode: 384 }
765
+ );
766
+ ctx.out(JSON.stringify({ ok: true, path }));
767
+ return 0;
768
+ }
769
+ function resolveAuth(parsed, ctx) {
770
+ const config = loadConfig(ctx.home);
771
+ const apiKey = flagValue(parsed, "api-key") ?? ctx.env.MAILTEA_API_KEY ?? ctx.env.MAILTEA_API_TOKEN ?? config.apiKey;
772
+ if (!apiKey) {
773
+ ctx.err(
774
+ JSON.stringify({
775
+ error: "Missing API key. Use --api-key, set MAILTEA_API_KEY, or run `mailtea login`."
776
+ })
777
+ );
778
+ return null;
779
+ }
780
+ const baseUrl = flagValue(parsed, "base-url") ?? ctx.env.MAILTEA_API_BASE_URL ?? config.baseUrl;
781
+ return { apiKey, baseUrl };
782
+ }
783
+ async function callApi(parsed, ctx, op) {
784
+ const auth = resolveAuth(parsed, ctx);
785
+ if (!auth) return 1;
786
+ const client = new Mailtea(auth.apiKey, { baseUrl: auth.baseUrl, fetch: ctx.fetchImpl });
787
+ try {
788
+ const result = await op(client);
789
+ ctx.out(JSON.stringify(result ?? { ok: true }));
790
+ return 0;
791
+ } catch (error) {
792
+ if (error instanceof MailteaError) {
793
+ ctx.err(
794
+ JSON.stringify({
795
+ error: error.message,
796
+ status: error.status,
797
+ details: error.details,
798
+ requestId: error.requestId
799
+ })
800
+ );
801
+ } else {
802
+ ctx.err(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
803
+ }
804
+ return 1;
805
+ }
806
+ }
807
+ function listCall(parsed, ctx, pub, op) {
808
+ let params;
809
+ try {
810
+ params = listParams(parsed, pub);
811
+ } catch (error) {
812
+ return Promise.resolve(fail(ctx, error instanceof Error ? error.message : String(error)));
813
+ }
814
+ return callApi(parsed, ctx, (client) => op(client, params));
815
+ }
816
+ function listParams(parsed, pub) {
817
+ const params = { publication_id: pub };
818
+ const limit = intFlag(parsed, "limit");
819
+ if (limit !== void 0) params.limit = limit;
820
+ const after = flagValue(parsed, "after");
821
+ if (after) params.after = after;
822
+ return params;
823
+ }
824
+ function collect(parsed, keys) {
825
+ const body = {};
826
+ for (const key of keys) {
827
+ const value = flagValue(parsed, key);
828
+ if (value !== void 0) body[key.replace(/-/g, "_")] = value;
829
+ }
830
+ return body;
831
+ }
832
+ function buildSendPayload(parsed) {
833
+ const recipients = (key) => {
834
+ const all = splitList(flagList(parsed, key));
835
+ if (all.length === 0) return void 0;
836
+ return all.length === 1 ? all[0] : all;
837
+ };
838
+ const payload = {
839
+ from: flagValue(parsed, "from"),
840
+ to: recipients("to"),
841
+ subject: flagValue(parsed, "subject")
842
+ };
843
+ const html = flagValue(parsed, "html");
844
+ const text = flagValue(parsed, "text");
845
+ const templateId = flagValue(parsed, "template-id");
846
+ if (html) payload.html = html;
847
+ if (text) payload.text = text;
848
+ if (templateId) {
849
+ const vars = flagPairs(parsed, "template-var");
850
+ payload.template = {
851
+ id: templateId,
852
+ ...vars.length ? { variables: Object.fromEntries(vars.map((v) => [v.name, v.value])) } : {}
853
+ };
854
+ }
855
+ const cc = recipients("cc");
856
+ if (cc) payload.cc = cc;
857
+ const bcc = recipients("bcc");
858
+ if (bcc) payload.bcc = bcc;
859
+ const replyTo = recipients("reply-to");
860
+ if (replyTo) payload.reply_to = replyTo;
861
+ const scheduledAt = flagValue(parsed, "scheduled-at");
862
+ if (scheduledAt) payload.scheduled_at = scheduledAt;
863
+ const tags = flagPairs(parsed, "tag");
864
+ if (tags.length) payload.tags = tags;
865
+ const headers = flagPairs(parsed, "header");
866
+ if (headers.length) payload.headers = Object.fromEntries(headers.map((h) => [h.name, h.value]));
867
+ return payload;
868
+ }
869
+ function validateSend(payload) {
870
+ if (!payload.from) return "emails send requires --from";
871
+ if (!payload.to) return "emails send requires --to";
872
+ if (!payload.subject) return "emails send requires --subject";
873
+ if (!payload.html && !payload.text && !payload.template) {
874
+ return "emails send requires --html, --text, or --template-id";
875
+ }
876
+ return null;
877
+ }
878
+ function buildListParams(parsed) {
879
+ const params = {};
880
+ const status = flagValue(parsed, "status");
881
+ if (status) params.status = status;
882
+ const limit = intFlag(parsed, "limit");
883
+ if (limit !== void 0) params.limit = limit;
884
+ const offset = intFlag(parsed, "offset");
885
+ if (offset !== void 0) params.offset = offset;
886
+ const tagName = flagValue(parsed, "tag-name");
887
+ if (tagName) params.tag_name = tagName;
888
+ const tagValue = flagValue(parsed, "tag-value");
889
+ if (tagValue) params.tag_value = tagValue;
890
+ const fromDate = flagValue(parsed, "from-date");
891
+ if (fromDate) params.from_date = fromDate;
892
+ const toDate = flagValue(parsed, "to-date");
893
+ if (toDate) params.to_date = toDate;
894
+ return params;
895
+ }
896
+ function loadConfig(home) {
897
+ try {
898
+ return JSON.parse(readFileSync(join(home, ".mailtea", "config.json"), "utf8"));
899
+ } catch {
900
+ return {};
901
+ }
902
+ }
903
+ function splitList(values) {
904
+ return values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
905
+ }
906
+ function intFlag(parsed, key) {
907
+ const raw = flagValue(parsed, key);
908
+ if (raw === void 0) return void 0;
909
+ const value = Number(raw);
910
+ if (!Number.isInteger(value) || value < 0) {
911
+ throw new Error(`--${key} must be a non-negative integer (got "${raw}")`);
912
+ }
913
+ return value;
914
+ }
915
+ function boolFlag(parsed, key) {
916
+ const raw = flagValue(parsed, key);
917
+ if (raw === void 0) return void 0;
918
+ return raw !== "false";
919
+ }
920
+ var INVALID_JSON = /* @__PURE__ */ Symbol("invalid-json");
921
+ function parseJsonFlag(parsed, key) {
922
+ const raw = flagValue(parsed, key);
923
+ if (raw === void 0) return void 0;
924
+ try {
925
+ return JSON.parse(raw);
926
+ } catch {
927
+ return INVALID_JSON;
928
+ }
929
+ }
930
+ function fail(ctx, message) {
931
+ ctx.err(message);
932
+ return 1;
933
+ }
934
+ var USAGE = `mailtea \u2014 send and manage email from your shell or AI agent
935
+
936
+ Capabilities (full MCP parity):
937
+ mailtea tools [--names] List every available capability (no auth)
938
+ mailtea schema <tool> Print a tool's JSON input schema (no auth)
939
+ mailtea call <tool> [--json '{...}'] [--arg k=v ...]
940
+ Invoke any of the ~121 MCP tools
941
+
942
+ Common email & audience commands:
943
+ mailtea emails send --from <a> --to <a> --subject <s> (--html <h> | --text <t> | --template-id <id>)
944
+ mailtea emails batch --json '[{...}, ...]'
945
+ mailtea emails list|get|cancel|reschedule|analytics ...
946
+ mailtea posts create --publication-id <id> --subject <s> (--template-id <id> [--template-var k=v ...] | --html <h>) [--kind newsletter|broadcast]
947
+ mailtea posts send-test <post-id> --to <addr> --from <addr> [--reply-to <addr>]
948
+ mailtea contacts create|list|get|update|delete --publication-id <pub> ...
949
+ mailtea segments create|list|get|update|delete --publication-id <pub> ...
950
+ mailtea tags create|list|get|update|delete --publication-id <pub> ...
951
+ mailtea domains create|list|get|verify|update|delete --publication-id <pub> ...
952
+ mailtea domains tracking create|list|verify|delete --publication-id <pub> --domain-id <id> ...
953
+ mailtea webhooks create|list|get|update|delete --publication-id <pub> [--event <e> ...]
954
+ mailtea contact-properties create|list|update|delete ...
955
+ mailtea api-keys create|list|revoke ...
956
+ mailtea templates create|list|get|update|publish|duplicate|delete --publication-id <pub> ...
957
+ mailtea inbound list --publication-id <pub> [--limit <n>] [--cursor <c>]
958
+ mailtea inbound get|attachments <id>
959
+ mailtea inbound attachment <id> <attachment-id>
960
+ mailtea inbound reply <id> (--html <h> | --text <t>) [--from <addr> --from-name <n>] [--subject <s>] [--cc <a> ...] [--bcc <a> ...] [--idempotency-key <k>]
961
+ mailtea login --api-key mt_pat_xxx [--base-url <url>]
962
+ mailtea version | help
963
+
964
+ Auth (in order): --api-key flag, MAILTEA_API_KEY / MAILTEA_API_TOKEN env, or ~/.mailtea/config.json (mailtea login).
965
+ Self-host/local: --base-url or MAILTEA_API_BASE_URL.
966
+
967
+ emails send flags: --cc --bcc --reply-to (repeatable / comma-separated), --scheduled-at <iso>,
968
+ --tag name=value (repeatable), --header name=value (repeatable),
969
+ --template-var name=value (repeatable), --dry-run (print payload, don't send).
970
+
971
+ Output is JSON on stdout; errors are JSON on stderr with a non-zero exit code.`;
972
+ export {
973
+ VERSION,
974
+ run
975
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "mailtea-cli",
3
+ "version": "0.1.1",
4
+ "description": "Mailtea CLI — send, schedule, and manage email from your shell or AI agent.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "mailtea": "bin/mailtea.mjs"
9
+ },
10
+ "main": "dist/cli.js",
11
+ "types": "dist/cli.d.ts",
12
+ "files": [
13
+ "dist",
14
+ "bin",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "mailtea",
19
+ "email",
20
+ "cli",
21
+ "transactional-email",
22
+ "ai-agent"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/mailtea-app/mailtea-cli.git"
27
+ },
28
+ "homepage": "https://mailtea.app",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/cli.d.ts",
35
+ "default": "./dist/cli.js"
36
+ }
37
+ },
38
+ "dependencies": {
39
+ "mailtea-mcp": "0.1.1",
40
+ "mailtea-sdk": "0.1.1"
41
+ },
42
+ "devDependencies": {
43
+ "tsup": "^8.4.0",
44
+ "tsx": "^4.20.5",
45
+ "typescript": "^5.8.2"
46
+ },
47
+ "scripts": {
48
+ "dev": "tsup src/cli.ts --watch --format esm --dts",
49
+ "build": "tsup src/cli.ts --format esm --dts --clean",
50
+ "typecheck": "tsc --noEmit",
51
+ "lint": "echo \"No lint for mailtea-cli yet\"",
52
+ "test": "tsx --test src/*.test.ts"
53
+ }
54
+ }