carlyemail 0.1.0 → 0.2.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/README.md +9 -0
- package/carlyemail.js +176 -1
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -36,6 +36,15 @@ npm install -g carlyemail
|
|
|
36
36
|
| `delete <inbox> --yes` | Delete an inbox and its mail |
|
|
37
37
|
| `send --from <i> --to <a> --subject <s> --text <t>` | Send an email |
|
|
38
38
|
| `messages <inbox>` | List messages |
|
|
39
|
+
| `read <inbox> <id>` | Read one message in full |
|
|
40
|
+
| `search <inbox> <text>` | Search messages |
|
|
41
|
+
| `threads <inbox>` | List conversations |
|
|
42
|
+
| `reply <inbox> <id> --text <t>` | Reply to the sender (`--all` to copy everyone) |
|
|
43
|
+
| `drafts <inbox>` | List drafts |
|
|
44
|
+
| `draft <inbox> --to <a> --text <t>` | Write a draft without sending |
|
|
45
|
+
| `send-draft <inbox> <id>` | Send a draft written earlier |
|
|
46
|
+
| `webhooks` | List webhook endpoints |
|
|
47
|
+
| `domains` | List custom domains, with their DNS records |
|
|
39
48
|
| `plan` | Current plan, with usage against every limit |
|
|
40
49
|
| `upgrade <plan>` | A Stripe checkout link |
|
|
41
50
|
| `billing` | Invoices, card changes, cancellation |
|
package/carlyemail.js
CHANGED
|
@@ -17,7 +17,7 @@ import { homedir } from "node:os";
|
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
|
|
20
|
-
export const VERSION = "0.
|
|
20
|
+
export const VERSION = "0.2.0";
|
|
21
21
|
|
|
22
22
|
const DEFAULT_API = "https://api.carlyemail.com";
|
|
23
23
|
const CONFIG_DIR = join(homedir(), ".carlyemail");
|
|
@@ -341,6 +341,181 @@ define("messages", "List messages in an inbox", "carlyemail messages me@carlyema
|
|
|
341
341
|
});
|
|
342
342
|
});
|
|
343
343
|
|
|
344
|
+
define("read", "Read one message in full", "carlyemail read me@carlyemail.com '<id@host>'", async (ctx) => {
|
|
345
|
+
const [inbox, id] = ctx.positional;
|
|
346
|
+
if (!inbox || !id) throw new UsageError("carlyemail read <inbox> <message-id>");
|
|
347
|
+
const m = await request(
|
|
348
|
+
ctx.config,
|
|
349
|
+
"GET",
|
|
350
|
+
`/v0/inboxes/${encodeURIComponent(inbox)}/messages/${encodeURIComponent(id)}`
|
|
351
|
+
);
|
|
352
|
+
emit(ctx, m, () => {
|
|
353
|
+
ctx.print(`${dim("from")} ${m.from ?? "?"}`);
|
|
354
|
+
ctx.print(`${dim("to")} ${(m.to || []).join(", ")}`);
|
|
355
|
+
ctx.print(`${dim("date")} ${(m.timestamp || "").replace("T", " ").slice(0, 19)}`);
|
|
356
|
+
ctx.print(`${dim("subject")} ${bold(m.subject || "(no subject)")}`);
|
|
357
|
+
if (m.labels?.length) ctx.print(`${dim("labels")} ${m.labels.join(", ")}`);
|
|
358
|
+
if (m.attachments?.length) {
|
|
359
|
+
ctx.print(`${dim("files")} ${m.attachments.map((a) => a.filename ?? a.attachment_id).join(", ")}`);
|
|
360
|
+
}
|
|
361
|
+
ctx.print("");
|
|
362
|
+
// `text` is the body as sent; `extracted_text` has the quoted reply chain
|
|
363
|
+
// stripped. Print the full one — a person reading a single message asked
|
|
364
|
+
// for the message, not our opinion of the interesting part of it.
|
|
365
|
+
ctx.print(m.text || m.preview || dim("(no text body)"));
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
define("search", "Search messages in an inbox", 'carlyemail search me@carlyemail.com "invoice"', async (ctx) => {
|
|
370
|
+
const [inbox, ...terms] = ctx.positional;
|
|
371
|
+
const query = terms.join(" ") || ctx.flags.query;
|
|
372
|
+
if (!inbox || !query) throw new UsageError('carlyemail search <inbox> "what to look for"');
|
|
373
|
+
const out = await request(
|
|
374
|
+
ctx.config,
|
|
375
|
+
"GET",
|
|
376
|
+
// `q`, not `query` — the parameter name comes from the OpenAPI spec, and
|
|
377
|
+
// guessing it produced a 422 that looked like a CLI bug.
|
|
378
|
+
`/v0/inboxes/${encodeURIComponent(inbox)}/messages/search?q=${encodeURIComponent(query)}`
|
|
379
|
+
);
|
|
380
|
+
emit(ctx, out, () => {
|
|
381
|
+
if (!out.messages?.length) {
|
|
382
|
+
ctx.print(dim(`nothing matching ${JSON.stringify(query)}`));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
for (const m of out.messages) {
|
|
386
|
+
ctx.print(`${dim((m.timestamp || "").slice(0, 10))} ${bold(m.subject || "(no subject)")}`);
|
|
387
|
+
ctx.print(`${" ".repeat(12)}${dim(m.message_id ?? "")}`);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
define("threads", "List conversations in an inbox", "carlyemail threads me@carlyemail.com", async (ctx) => {
|
|
393
|
+
const inbox = ctx.positional[0];
|
|
394
|
+
if (!inbox) throw new UsageError("carlyemail threads <inbox>");
|
|
395
|
+
const out = await request(ctx.config, "GET", `/v0/inboxes/${encodeURIComponent(inbox)}/threads`);
|
|
396
|
+
emit(ctx, out, () => {
|
|
397
|
+
if (!out.threads?.length) {
|
|
398
|
+
ctx.print(dim("no conversations"));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
for (const t of out.threads) {
|
|
402
|
+
const count = t.message_count ? dim(` (${t.message_count})`) : "";
|
|
403
|
+
ctx.print(`${bold(t.subject || "(no subject)")}${count}`);
|
|
404
|
+
ctx.print(` ${dim(t.thread_id)} ${dim((t.senders || []).join(", "))}`);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
define("reply", "Reply to a message", 'carlyemail reply me@x.com "<id@host>" --text "On it"', async (ctx) => {
|
|
410
|
+
const [inbox, id] = ctx.positional;
|
|
411
|
+
if (!inbox || !id) throw new UsageError("carlyemail reply <inbox> <message-id> --text ...");
|
|
412
|
+
const body = {
|
|
413
|
+
text: typeof ctx.flags.text === "string" ? ctx.flags.text : undefined,
|
|
414
|
+
html: typeof ctx.flags.html === "string" ? ctx.flags.html : undefined,
|
|
415
|
+
};
|
|
416
|
+
if (body.text === undefined && body.html === undefined) {
|
|
417
|
+
throw new UsageError("--text or --html is required");
|
|
418
|
+
}
|
|
419
|
+
// reply-all is opt-in. Quietly copying everyone on the original thread is
|
|
420
|
+
// the kind of default that sends an agent's message to people the caller
|
|
421
|
+
// never saw.
|
|
422
|
+
const route = ctx.flags["all"] ? "reply-all" : "reply";
|
|
423
|
+
const sent = await request(
|
|
424
|
+
ctx.config,
|
|
425
|
+
"POST",
|
|
426
|
+
`/v0/inboxes/${encodeURIComponent(inbox)}/messages/${encodeURIComponent(id)}/${route}`,
|
|
427
|
+
{ body }
|
|
428
|
+
);
|
|
429
|
+
emit(ctx, sent, () => ctx.print(ok(`replied ${dim(sent.message_id ?? "")}`)));
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
define("drafts", "List drafts in an inbox", "carlyemail drafts me@carlyemail.com", async (ctx) => {
|
|
433
|
+
const inbox = ctx.positional[0];
|
|
434
|
+
if (!inbox) throw new UsageError("carlyemail drafts <inbox>");
|
|
435
|
+
const out = await request(ctx.config, "GET", `/v0/inboxes/${encodeURIComponent(inbox)}/drafts`);
|
|
436
|
+
emit(ctx, out, () => {
|
|
437
|
+
if (!out.drafts?.length) {
|
|
438
|
+
ctx.print(dim("no drafts"));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
for (const d of out.drafts) {
|
|
442
|
+
ctx.print(`${bold(d.subject || "(no subject)")} ${dim(`to ${(d.to || []).join(", ")}`)}`);
|
|
443
|
+
ctx.print(` ${dim(d.draft_id)}`);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
define(
|
|
449
|
+
"draft",
|
|
450
|
+
"Write a draft without sending it",
|
|
451
|
+
'carlyemail draft me@x.com --to you@y.com --subject Hi --text "..."',
|
|
452
|
+
async (ctx) => {
|
|
453
|
+
const inbox = ctx.positional[0];
|
|
454
|
+
if (!inbox) throw new UsageError("carlyemail draft <inbox> --to ... --text ...");
|
|
455
|
+
const body = {
|
|
456
|
+
to: required(ctx.flags, "to").split(",").map((s) => s.trim()),
|
|
457
|
+
subject: typeof ctx.flags.subject === "string" ? ctx.flags.subject : undefined,
|
|
458
|
+
text: typeof ctx.flags.text === "string" ? ctx.flags.text : undefined,
|
|
459
|
+
};
|
|
460
|
+
const d = await request(
|
|
461
|
+
ctx.config,
|
|
462
|
+
"POST",
|
|
463
|
+
`/v0/inboxes/${encodeURIComponent(inbox)}/drafts`,
|
|
464
|
+
{ body }
|
|
465
|
+
);
|
|
466
|
+
emit(ctx, d, () => {
|
|
467
|
+
ctx.print(ok(`draft ${d.draft_id}`));
|
|
468
|
+
ctx.print(arrow(`send it with: carlyemail send-draft ${inbox} ${d.draft_id}`));
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
define("send-draft", "Send a draft that was written earlier", "carlyemail send-draft me@x.com dft_123", async (ctx) => {
|
|
474
|
+
const [inbox, id] = ctx.positional;
|
|
475
|
+
if (!inbox || !id) throw new UsageError("carlyemail send-draft <inbox> <draft-id>");
|
|
476
|
+
const sent = await request(
|
|
477
|
+
ctx.config,
|
|
478
|
+
"POST",
|
|
479
|
+
`/v0/inboxes/${encodeURIComponent(inbox)}/drafts/${encodeURIComponent(id)}/send`
|
|
480
|
+
);
|
|
481
|
+
emit(ctx, sent, () => ctx.print(ok(`sent ${dim(sent.message_id ?? "")}`)));
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
define("webhooks", "List webhook endpoints", "carlyemail webhooks", async (ctx) => {
|
|
485
|
+
const out = await request(ctx.config, "GET", "/v0/webhooks");
|
|
486
|
+
emit(ctx, out, () => {
|
|
487
|
+
if (!out.webhooks?.length) {
|
|
488
|
+
ctx.print(dim("no webhooks"));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
for (const w of out.webhooks) {
|
|
492
|
+
ctx.print(`${w.enabled === false ? dim("(disabled) ") : ""}${bold(w.url)}`);
|
|
493
|
+
ctx.print(` ${dim(w.webhook_id)} ${dim((w.event_types || []).join(", "))}`);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
define("domains", "List custom sending domains", "carlyemail domains", async (ctx) => {
|
|
499
|
+
const out = await request(ctx.config, "GET", "/v0/domains");
|
|
500
|
+
emit(ctx, out, () => {
|
|
501
|
+
if (!out.domains?.length) {
|
|
502
|
+
ctx.print(dim("no custom domains — mail sends from carlyemail.com"));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
for (const d of out.domains) {
|
|
506
|
+
const mark = d.status === "VERIFIED" ? paint(32, "✓") : dim("…");
|
|
507
|
+
ctx.print(`${mark} ${bold(d.domain)} ${dim(d.status)}`);
|
|
508
|
+
// Unverified domains are the common case people get stuck on, and the
|
|
509
|
+
// records are the whole answer, so print them rather than a doc link.
|
|
510
|
+
if (d.status !== "VERIFIED") {
|
|
511
|
+
for (const r of d.records || []) {
|
|
512
|
+
ctx.print(` ${dim(r.type)} ${r.name} ${dim("→")} ${r.value}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
344
519
|
define("plan", "Show the current plan and its limits", "carlyemail plan", async (ctx) => {
|
|
345
520
|
const [billing, org] = await Promise.all([
|
|
346
521
|
request(ctx.config, "GET", "/v0/billing"),
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carlyemail",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Real email inboxes your agent can send, receive and reply from.",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"email",
|
|
7
|
+
"agent",
|
|
8
|
+
"ai",
|
|
9
|
+
"inbox",
|
|
10
|
+
"smtp",
|
|
11
|
+
"mcp",
|
|
12
|
+
"cli"
|
|
13
|
+
],
|
|
6
14
|
"homepage": "https://carlyemail.com",
|
|
7
15
|
"bugs": "https://docs.carlyemail.com/support",
|
|
8
16
|
"repository": {
|
|
@@ -17,7 +25,11 @@
|
|
|
17
25
|
"carlyemail": "./carlyemail.js"
|
|
18
26
|
},
|
|
19
27
|
"exports": "./carlyemail.js",
|
|
20
|
-
"files": [
|
|
28
|
+
"files": [
|
|
29
|
+
"carlyemail.js",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
21
33
|
"engines": {
|
|
22
34
|
"node": ">=18"
|
|
23
35
|
},
|