carlyemail 0.2.0 → 0.3.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 +49 -5
- package/carlyemail.js +107 -11
- package/package.json +9 -3
- package/sdk.d.ts +777 -0
- package/sdk.js +791 -0
package/README.md
CHANGED
|
@@ -1,13 +1,57 @@
|
|
|
1
1
|
# carlyemail
|
|
2
2
|
|
|
3
|
-
Real email inboxes your agent can send, receive and reply from.
|
|
3
|
+
Real email inboxes your agent can send, receive and reply from. One package,
|
|
4
|
+
two things: a typed client and a command line.
|
|
5
|
+
|
|
6
|
+
## The client
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install carlyemail
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```javascript
|
|
13
|
+
import { CarlyEmail } from "carlyemail";
|
|
14
|
+
|
|
15
|
+
const carly = new CarlyEmail(); // reads CARLYEMAIL_API_KEY
|
|
16
|
+
|
|
17
|
+
const inbox = await carly.inboxes.create({ username: "hello" });
|
|
18
|
+
|
|
19
|
+
await carly.messages.send(inbox.email, {
|
|
20
|
+
to: ["you@example.com"],
|
|
21
|
+
subject: "Hello",
|
|
22
|
+
text: "From an agent.",
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Generated from the [OpenAPI spec](https://docs.carlyemail.com/openapi.json), so
|
|
27
|
+
it cannot describe an endpoint the API does not serve. One `fetch` call per
|
|
28
|
+
method and no dependencies, so it runs in Node, the browser, a worker and on the
|
|
29
|
+
edge with no build step. Types ship with it.
|
|
30
|
+
|
|
31
|
+
Errors carry the message, the fix and a documentation link:
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
import { CarlyEmailError } from "carlyemail";
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await carly.inboxes.create({ username: "hello" });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error instanceof CarlyEmailError) {
|
|
40
|
+
console.log(error.status, error.code, error.fix, error.docs);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
There is a Python client too: `pip install carlyemail`.
|
|
46
|
+
|
|
47
|
+
## The command line
|
|
4
48
|
|
|
5
49
|
```bash
|
|
6
|
-
npx carlyemail signup
|
|
50
|
+
npx carlyemail signup
|
|
7
51
|
```
|
|
8
52
|
|
|
9
|
-
|
|
10
|
-
can send:
|
|
53
|
+
It asks for your email, offers an inbox name, and writes the key to
|
|
54
|
+
`~/.carlyemail/config.json`. Confirm the emailed code and it can send:
|
|
11
55
|
|
|
12
56
|
```bash
|
|
13
57
|
npx carlyemail verify 123456
|
|
@@ -18,7 +62,7 @@ npx carlyemail send \
|
|
|
18
62
|
--text "Sent by an agent with its own address."
|
|
19
63
|
```
|
|
20
64
|
|
|
21
|
-
Install it
|
|
65
|
+
Install it if you use it often:
|
|
22
66
|
|
|
23
67
|
```bash
|
|
24
68
|
npm install -g carlyemail
|
package/carlyemail.js
CHANGED
|
@@ -16,8 +16,9 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, realpath
|
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { randomBytes } from "node:crypto";
|
|
19
20
|
|
|
20
|
-
export const VERSION = "0.
|
|
21
|
+
export const VERSION = "0.3.0";
|
|
21
22
|
|
|
22
23
|
const DEFAULT_API = "https://api.carlyemail.com";
|
|
23
24
|
const CONFIG_DIR = join(homedir(), ".carlyemail");
|
|
@@ -211,13 +212,34 @@ function emit(ctx, payload, render) {
|
|
|
211
212
|
render();
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
/** A name to offer when somebody has no preference. Short enough to type again. */
|
|
216
|
+
function suggestUsername() {
|
|
217
|
+
return `agent-${randomBytes(2).toString("hex")}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
214
220
|
define(
|
|
215
221
|
"signup",
|
|
216
222
|
"Create an account and an inbox",
|
|
217
|
-
"carlyemail signup --human-email you@example.com --username my-agent",
|
|
223
|
+
"carlyemail signup (or: --human-email you@example.com --username my-agent)",
|
|
218
224
|
async (ctx) => {
|
|
219
|
-
|
|
220
|
-
|
|
225
|
+
// `signup` is the first command anybody runs, and refusing it with a usage
|
|
226
|
+
// error is a bad way to say hello. At a terminal, ask.
|
|
227
|
+
let human_email = ctx.flags["human-email"];
|
|
228
|
+
if (typeof human_email !== "string") {
|
|
229
|
+
const answer = await ctx.ask("Your email (for the confirmation code): ");
|
|
230
|
+
if (answer) human_email = answer;
|
|
231
|
+
}
|
|
232
|
+
if (!human_email) throw new UsageError("--human-email is required");
|
|
233
|
+
|
|
234
|
+
let username = ctx.flags.username;
|
|
235
|
+
if (typeof username !== "string") {
|
|
236
|
+
const suggested = suggestUsername();
|
|
237
|
+
const answer = await ctx.ask(`Inbox name [${suggested}]: `);
|
|
238
|
+
// null is "no terminal", which still has to fail. "" is a bare Enter,
|
|
239
|
+
// which means take the suggestion.
|
|
240
|
+
if (answer !== null) username = answer || suggested;
|
|
241
|
+
}
|
|
242
|
+
if (!username) throw new UsageError("--username is required");
|
|
221
243
|
|
|
222
244
|
const out = await request(ctx.config, "POST", "/v0/agent/sign-up", {
|
|
223
245
|
auth: false,
|
|
@@ -336,7 +358,11 @@ define("messages", "List messages in an inbox", "carlyemail messages me@carlyema
|
|
|
336
358
|
for (const m of out.messages) {
|
|
337
359
|
const when = (m.timestamp || "").slice(0, 16).replace("T", " ");
|
|
338
360
|
ctx.print(`${dim(when)} ${bold(m.subject || "(no subject)")}`);
|
|
339
|
-
|
|
361
|
+
// The id goes on screen because every other command takes one. Listing
|
|
362
|
+
// messages and then having to go to the API to find out what to call
|
|
363
|
+
// `read` or `reply` with was the gap people kept falling into.
|
|
364
|
+
const id = m.message_id ? ` ${m.message_id}` : "";
|
|
365
|
+
ctx.print(`${" ".repeat(18)}${dim(`from ${m.from ?? m.from_address ?? "?"}${id}`)}`);
|
|
340
366
|
}
|
|
341
367
|
});
|
|
342
368
|
});
|
|
@@ -406,16 +432,39 @@ define("threads", "List conversations in an inbox", "carlyemail threads me@carly
|
|
|
406
432
|
});
|
|
407
433
|
});
|
|
408
434
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
435
|
+
/** The newest message in an inbox that the inbox did not itself send. */
|
|
436
|
+
async function lastReceived(ctx, inbox) {
|
|
437
|
+
const out = await request(
|
|
438
|
+
ctx.config,
|
|
439
|
+
"GET",
|
|
440
|
+
`/v0/inboxes/${encodeURIComponent(inbox)}/messages?limit=10`
|
|
441
|
+
);
|
|
442
|
+
// Sent mail is stored in the inbox alongside what arrived, so the newest
|
|
443
|
+
// message is often the agent's own last reply. Replying to yourself is never
|
|
444
|
+
// what `--last` means.
|
|
445
|
+
const mine = inbox.toLowerCase();
|
|
446
|
+
const found = (out.messages || []).find(
|
|
447
|
+
(m) => !String(m.from ?? m.from_address ?? "").toLowerCase().includes(mine)
|
|
448
|
+
);
|
|
449
|
+
if (!found) throw new UsageError(`nothing to reply to in ${inbox}`);
|
|
450
|
+
return found.message_id;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
define("reply", "Reply to a message", 'carlyemail reply me@x.com --last --text "On it"', async (ctx) => {
|
|
454
|
+
const [inbox, given] = ctx.positional;
|
|
455
|
+
if (!inbox) throw new UsageError("carlyemail reply <inbox> <message-id> --text ...");
|
|
456
|
+
if (!given && !ctx.flags.last) {
|
|
457
|
+
throw new UsageError("carlyemail reply <inbox> <message-id> --text ... (or --last)");
|
|
458
|
+
}
|
|
412
459
|
const body = {
|
|
413
460
|
text: typeof ctx.flags.text === "string" ? ctx.flags.text : undefined,
|
|
414
461
|
html: typeof ctx.flags.html === "string" ? ctx.flags.html : undefined,
|
|
415
462
|
};
|
|
463
|
+
// Checked before `--last` costs a request, and long before anything is sent.
|
|
416
464
|
if (body.text === undefined && body.html === undefined) {
|
|
417
465
|
throw new UsageError("--text or --html is required");
|
|
418
466
|
}
|
|
467
|
+
const id = given || (await lastReceived(ctx, inbox));
|
|
419
468
|
// reply-all is opt-in. Quietly copying everyone on the original thread is
|
|
420
469
|
// the kind of default that sends an agent's message to people the caller
|
|
421
470
|
// never saw.
|
|
@@ -481,6 +530,33 @@ define("send-draft", "Send a draft that was written earlier", "carlyemail send-d
|
|
|
481
530
|
emit(ctx, sent, () => ctx.print(ok(`sent ${dim(sent.message_id ?? "")}`)));
|
|
482
531
|
});
|
|
483
532
|
|
|
533
|
+
define(
|
|
534
|
+
"webhook",
|
|
535
|
+
"Create a webhook endpoint",
|
|
536
|
+
"carlyemail webhook https://you.example/mail --events message.received",
|
|
537
|
+
async (ctx) => {
|
|
538
|
+
const url = ctx.positional[0];
|
|
539
|
+
if (!url) throw new UsageError("carlyemail webhook <url> --events message.received");
|
|
540
|
+
const events =
|
|
541
|
+
typeof ctx.flags.events === "string"
|
|
542
|
+
? ctx.flags.events.split(",").map((e) => e.trim()).filter(Boolean)
|
|
543
|
+
: ["message.received"];
|
|
544
|
+
|
|
545
|
+
const made = await request(ctx.config, "POST", "/v0/webhooks", {
|
|
546
|
+
body: { url, event_types: events },
|
|
547
|
+
});
|
|
548
|
+
emit(ctx, made, () => {
|
|
549
|
+
ctx.print(ok(`webhook ${dim(made.webhook_id ?? "")}`));
|
|
550
|
+
// Returned once and never again. Printing it without saying so is how
|
|
551
|
+
// people end up recreating the webhook to get a secret back.
|
|
552
|
+
if (made.secret) {
|
|
553
|
+
ctx.print(` secret ${made.secret}`);
|
|
554
|
+
ctx.print(dim(" shown once — store it, you need it to verify deliveries"));
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
|
|
484
560
|
define("webhooks", "List webhook endpoints", "carlyemail webhooks", async (ctx) => {
|
|
485
561
|
const out = await request(ctx.config, "GET", "/v0/webhooks");
|
|
486
562
|
emit(ctx, out, () => {
|
|
@@ -583,7 +659,7 @@ export function helpText() {
|
|
|
583
659
|
lines.push(
|
|
584
660
|
"",
|
|
585
661
|
bold("Getting started"),
|
|
586
|
-
" carlyemail signup
|
|
662
|
+
" carlyemail signup",
|
|
587
663
|
" carlyemail verify 123456",
|
|
588
664
|
" carlyemail send --from my-agent@agents.carlyemail.com \\",
|
|
589
665
|
' --to someone@example.com --subject Hi --text "Hello from an agent"',
|
|
@@ -603,7 +679,27 @@ export function helpText() {
|
|
|
603
679
|
|
|
604
680
|
// ---------------------------------------------------------------- entry
|
|
605
681
|
|
|
606
|
-
|
|
682
|
+
/**
|
|
683
|
+
* Read one line from the terminal.
|
|
684
|
+
*
|
|
685
|
+
* Returns null when there is no terminal — a CI job or a piped script must get
|
|
686
|
+
* the usage error it can act on, not a prompt nobody will ever answer.
|
|
687
|
+
*/
|
|
688
|
+
async function askTerminal(question) {
|
|
689
|
+
if (!process.stdin.isTTY) return null;
|
|
690
|
+
const { createInterface } = await import("node:readline/promises");
|
|
691
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
692
|
+
try {
|
|
693
|
+
return (await rl.question(question)).trim();
|
|
694
|
+
} finally {
|
|
695
|
+
rl.close();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export async function main(
|
|
700
|
+
argv,
|
|
701
|
+
{ env = process.env, print = console.log, ask = askTerminal, configFile, configDir } = {}
|
|
702
|
+
) {
|
|
607
703
|
const { flags, positional } = parseArgs(argv);
|
|
608
704
|
const name = positional.shift();
|
|
609
705
|
|
|
@@ -629,7 +725,7 @@ export async function main(argv, { env = process.env, print = console.log, confi
|
|
|
629
725
|
|
|
630
726
|
const config = loadConfig(env, configFile);
|
|
631
727
|
try {
|
|
632
|
-
await command.run({ config, flags, positional, print, configFile, configDir });
|
|
728
|
+
await command.run({ config, flags, positional, print, ask, configFile, configDir });
|
|
633
729
|
return 0;
|
|
634
730
|
} catch (error) {
|
|
635
731
|
if (error instanceof ApiError) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carlyemail",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Real email inboxes your agent can send, receive and reply from.",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Real email inboxes your agent can send, receive and reply from. SDK and CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"email",
|
|
7
7
|
"agent",
|
|
@@ -24,9 +24,15 @@
|
|
|
24
24
|
"bin": {
|
|
25
25
|
"carlyemail": "./carlyemail.js"
|
|
26
26
|
},
|
|
27
|
-
"
|
|
27
|
+
"types": "./sdk.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./sdk.js",
|
|
30
|
+
"./cli": "./carlyemail.js"
|
|
31
|
+
},
|
|
28
32
|
"files": [
|
|
29
33
|
"carlyemail.js",
|
|
34
|
+
"sdk.js",
|
|
35
|
+
"sdk.d.ts",
|
|
30
36
|
"README.md",
|
|
31
37
|
"LICENSE"
|
|
32
38
|
],
|