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.
Files changed (3) hide show
  1. package/README.md +9 -0
  2. package/carlyemail.js +176 -1
  3. 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.1.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.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Real email inboxes your agent can send, receive and reply from.",
5
- "keywords": ["email", "agent", "ai", "inbox", "smtp", "mcp", "cli"],
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": ["carlyemail.js", "README.md", "LICENSE"],
28
+ "files": [
29
+ "carlyemail.js",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
21
33
  "engines": {
22
34
  "node": ">=18"
23
35
  },