carlyemail 0.2.0 → 0.4.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 +77 -9
- package/carlyemail.js +209 -13
- package/package.json +12 -3
- package/sdk.d.ts +814 -0
- package/sdk.js +832 -0
- package/webhooks.d.ts +35 -0
- package/webhooks.js +110 -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
|
|
4
7
|
|
|
5
8
|
```bash
|
|
6
|
-
|
|
9
|
+
npm install carlyemail
|
|
7
10
|
```
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx carlyemail signup
|
|
51
|
+
```
|
|
52
|
+
|
|
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,14 @@ npx carlyemail send \
|
|
|
18
62
|
--text "Sent by an agent with its own address."
|
|
19
63
|
```
|
|
20
64
|
|
|
21
|
-
|
|
65
|
+
Load the saved key and its first inbox into the current shell before starting
|
|
66
|
+
LangChain, the Claude Agent SDK, Eve, Mastra, or another framework:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
eval "$(npx carlyemail env)"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Install it if you use it often:
|
|
22
73
|
|
|
23
74
|
```bash
|
|
24
75
|
npm install -g carlyemail
|
|
@@ -31,6 +82,7 @@ npm install -g carlyemail
|
|
|
31
82
|
| `signup --human-email <e> --username <u>` | Create an account and an inbox |
|
|
32
83
|
| `verify <code>` | Confirm the owner email |
|
|
33
84
|
| `whoami` | Identity and scope of the current key |
|
|
85
|
+
| `env` | Print `CARLYEMAIL_API_KEY` and `CARLYEMAIL_INBOX` shell exports |
|
|
34
86
|
| `inboxes` | List inboxes |
|
|
35
87
|
| `create --username <u>` | Create an inbox |
|
|
36
88
|
| `delete <inbox> --yes` | Delete an inbox and its mail |
|
|
@@ -82,10 +134,10 @@ rather than a login.
|
|
|
82
134
|
|
|
83
135
|
## Notes
|
|
84
136
|
|
|
85
|
-
**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
137
|
+
**An owner email is not a login.** Running sign-up again for an existing address
|
|
138
|
+
returns `account_exists` and leaves every key untouched. Recover access with an
|
|
139
|
+
emailed code at https://console.carlyemail.com; use `create` from an
|
|
140
|
+
authenticated CLI session to add another inbox.
|
|
89
141
|
|
|
90
142
|
**`delete` requires `--yes`.** It removes the inbox and every message in it,
|
|
91
143
|
and there is no undo.
|
|
@@ -108,6 +160,22 @@ Node 18 or newer. No dependencies — this is one file that uses Node's built-in
|
|
|
108
160
|
`fetch`, so `npx` never resolves a tree and never fails for a reason unrelated
|
|
109
161
|
to the thing you asked for.
|
|
110
162
|
|
|
163
|
+
## Inbound email in a Worker
|
|
164
|
+
|
|
165
|
+
The dependency-free webhook helper uses Web Crypto, so the same verified
|
|
166
|
+
`onEmail` callback runs in Cloudflare Workers, Node, and other edge runtimes:
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
import { createEmailHandler } from "carlyemail/webhooks";
|
|
170
|
+
|
|
171
|
+
const handleEmail = createEmailHandler({
|
|
172
|
+
secret: env.CARLYEMAIL_WEBHOOK_SECRET,
|
|
173
|
+
async onEmail(event) {
|
|
174
|
+
console.log(event.message);
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
111
179
|
## Licence
|
|
112
180
|
|
|
113
181
|
MIT — see `LICENSE`. That covers **this CLI only**. The CarlyEmail service it
|
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.4.0";
|
|
21
22
|
|
|
22
23
|
const DEFAULT_API = "https://api.carlyemail.com";
|
|
23
24
|
const CONFIG_DIR = join(homedir(), ".carlyemail");
|
|
@@ -39,6 +40,11 @@ export function maskKey(key) {
|
|
|
39
40
|
return `${key.slice(0, 9)}${"·".repeat(8)}${key.slice(-4)}`;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/** Quote one value for POSIX shells. `env` is designed to be used with eval. */
|
|
44
|
+
export function shellQuote(value) {
|
|
45
|
+
return `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
// ---------------------------------------------------------------- config
|
|
43
49
|
|
|
44
50
|
/**
|
|
@@ -108,7 +114,12 @@ class UsageError extends Error {}
|
|
|
108
114
|
|
|
109
115
|
// ---------------------------------------------------------------- transport
|
|
110
116
|
|
|
111
|
-
export async function request(
|
|
117
|
+
export async function request(
|
|
118
|
+
config,
|
|
119
|
+
method,
|
|
120
|
+
path,
|
|
121
|
+
{ body, auth = true, raw = false, fetchImpl = fetch } = {}
|
|
122
|
+
) {
|
|
112
123
|
const headers = { accept: "application/json" };
|
|
113
124
|
if (body !== undefined) headers["content-type"] = "application/json";
|
|
114
125
|
if (auth) {
|
|
@@ -145,7 +156,9 @@ export async function request(config, method, path, { body, auth = true, fetchIm
|
|
|
145
156
|
}
|
|
146
157
|
}
|
|
147
158
|
if (!response.ok) throw new ApiError(response.status, parsed);
|
|
148
|
-
|
|
159
|
+
// The zone file is text/plain, not our envelope. Parsing it returns null,
|
|
160
|
+
// which is how it silently printed nothing.
|
|
161
|
+
return raw ? text : parsed;
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
// ---------------------------------------------------------------- arguments
|
|
@@ -211,13 +224,34 @@ function emit(ctx, payload, render) {
|
|
|
211
224
|
render();
|
|
212
225
|
}
|
|
213
226
|
|
|
227
|
+
/** A name to offer when somebody has no preference. Short enough to type again. */
|
|
228
|
+
function suggestUsername() {
|
|
229
|
+
return `agent-${randomBytes(2).toString("hex")}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
214
232
|
define(
|
|
215
233
|
"signup",
|
|
216
234
|
"Create an account and an inbox",
|
|
217
|
-
"carlyemail signup --human-email you@example.com --username my-agent",
|
|
235
|
+
"carlyemail signup (or: --human-email you@example.com --username my-agent)",
|
|
218
236
|
async (ctx) => {
|
|
219
|
-
|
|
220
|
-
|
|
237
|
+
// `signup` is the first command anybody runs, and refusing it with a usage
|
|
238
|
+
// error is a bad way to say hello. At a terminal, ask.
|
|
239
|
+
let human_email = ctx.flags["human-email"];
|
|
240
|
+
if (typeof human_email !== "string") {
|
|
241
|
+
const answer = await ctx.ask("Your email (for the confirmation code): ");
|
|
242
|
+
if (answer) human_email = answer;
|
|
243
|
+
}
|
|
244
|
+
if (!human_email) throw new UsageError("--human-email is required");
|
|
245
|
+
|
|
246
|
+
let username = ctx.flags.username;
|
|
247
|
+
if (typeof username !== "string") {
|
|
248
|
+
const suggested = suggestUsername();
|
|
249
|
+
const answer = await ctx.ask(`Inbox name [${suggested}]: `);
|
|
250
|
+
// null is "no terminal", which still has to fail. "" is a bare Enter,
|
|
251
|
+
// which means take the suggestion.
|
|
252
|
+
if (answer !== null) username = answer || suggested;
|
|
253
|
+
}
|
|
254
|
+
if (!username) throw new UsageError("--username is required");
|
|
221
255
|
|
|
222
256
|
const out = await request(ctx.config, "POST", "/v0/agent/sign-up", {
|
|
223
257
|
auth: false,
|
|
@@ -336,7 +370,11 @@ define("messages", "List messages in an inbox", "carlyemail messages me@carlyema
|
|
|
336
370
|
for (const m of out.messages) {
|
|
337
371
|
const when = (m.timestamp || "").slice(0, 16).replace("T", " ");
|
|
338
372
|
ctx.print(`${dim(when)} ${bold(m.subject || "(no subject)")}`);
|
|
339
|
-
|
|
373
|
+
// The id goes on screen because every other command takes one. Listing
|
|
374
|
+
// messages and then having to go to the API to find out what to call
|
|
375
|
+
// `read` or `reply` with was the gap people kept falling into.
|
|
376
|
+
const id = m.message_id ? ` ${m.message_id}` : "";
|
|
377
|
+
ctx.print(`${" ".repeat(18)}${dim(`from ${m.from ?? m.from_address ?? "?"}${id}`)}`);
|
|
340
378
|
}
|
|
341
379
|
});
|
|
342
380
|
});
|
|
@@ -406,16 +444,39 @@ define("threads", "List conversations in an inbox", "carlyemail threads me@carly
|
|
|
406
444
|
});
|
|
407
445
|
});
|
|
408
446
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
447
|
+
/** The newest message in an inbox that the inbox did not itself send. */
|
|
448
|
+
async function lastReceived(ctx, inbox) {
|
|
449
|
+
const out = await request(
|
|
450
|
+
ctx.config,
|
|
451
|
+
"GET",
|
|
452
|
+
`/v0/inboxes/${encodeURIComponent(inbox)}/messages?limit=10`
|
|
453
|
+
);
|
|
454
|
+
// Sent mail is stored in the inbox alongside what arrived, so the newest
|
|
455
|
+
// message is often the agent's own last reply. Replying to yourself is never
|
|
456
|
+
// what `--last` means.
|
|
457
|
+
const mine = inbox.toLowerCase();
|
|
458
|
+
const found = (out.messages || []).find(
|
|
459
|
+
(m) => !String(m.from ?? m.from_address ?? "").toLowerCase().includes(mine)
|
|
460
|
+
);
|
|
461
|
+
if (!found) throw new UsageError(`nothing to reply to in ${inbox}`);
|
|
462
|
+
return found.message_id;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
define("reply", "Reply to a message", 'carlyemail reply me@x.com --last --text "On it"', async (ctx) => {
|
|
466
|
+
const [inbox, given] = ctx.positional;
|
|
467
|
+
if (!inbox) throw new UsageError("carlyemail reply <inbox> <message-id> --text ...");
|
|
468
|
+
if (!given && !ctx.flags.last) {
|
|
469
|
+
throw new UsageError("carlyemail reply <inbox> <message-id> --text ... (or --last)");
|
|
470
|
+
}
|
|
412
471
|
const body = {
|
|
413
472
|
text: typeof ctx.flags.text === "string" ? ctx.flags.text : undefined,
|
|
414
473
|
html: typeof ctx.flags.html === "string" ? ctx.flags.html : undefined,
|
|
415
474
|
};
|
|
475
|
+
// Checked before `--last` costs a request, and long before anything is sent.
|
|
416
476
|
if (body.text === undefined && body.html === undefined) {
|
|
417
477
|
throw new UsageError("--text or --html is required");
|
|
418
478
|
}
|
|
479
|
+
const id = given || (await lastReceived(ctx, inbox));
|
|
419
480
|
// reply-all is opt-in. Quietly copying everyone on the original thread is
|
|
420
481
|
// the kind of default that sends an agent's message to people the caller
|
|
421
482
|
// never saw.
|
|
@@ -481,6 +542,33 @@ define("send-draft", "Send a draft that was written earlier", "carlyemail send-d
|
|
|
481
542
|
emit(ctx, sent, () => ctx.print(ok(`sent ${dim(sent.message_id ?? "")}`)));
|
|
482
543
|
});
|
|
483
544
|
|
|
545
|
+
define(
|
|
546
|
+
"webhook",
|
|
547
|
+
"Create a webhook endpoint",
|
|
548
|
+
"carlyemail webhook https://you.example/mail --events message.received",
|
|
549
|
+
async (ctx) => {
|
|
550
|
+
const url = ctx.positional[0];
|
|
551
|
+
if (!url) throw new UsageError("carlyemail webhook <url> --events message.received");
|
|
552
|
+
const events =
|
|
553
|
+
typeof ctx.flags.events === "string"
|
|
554
|
+
? ctx.flags.events.split(",").map((e) => e.trim()).filter(Boolean)
|
|
555
|
+
: ["message.received"];
|
|
556
|
+
|
|
557
|
+
const made = await request(ctx.config, "POST", "/v0/webhooks", {
|
|
558
|
+
body: { url, event_types: events },
|
|
559
|
+
});
|
|
560
|
+
emit(ctx, made, () => {
|
|
561
|
+
ctx.print(ok(`webhook ${dim(made.webhook_id ?? "")}`));
|
|
562
|
+
// Returned once and never again. Printing it without saying so is how
|
|
563
|
+
// people end up recreating the webhook to get a secret back.
|
|
564
|
+
if (made.secret) {
|
|
565
|
+
ctx.print(` secret ${made.secret}`);
|
|
566
|
+
ctx.print(dim(" shown once — store it, you need it to verify deliveries"));
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
|
|
484
572
|
define("webhooks", "List webhook endpoints", "carlyemail webhooks", async (ctx) => {
|
|
485
573
|
const out = await request(ctx.config, "GET", "/v0/webhooks");
|
|
486
574
|
emit(ctx, out, () => {
|
|
@@ -495,6 +583,74 @@ define("webhooks", "List webhook endpoints", "carlyemail webhooks", async (ctx)
|
|
|
495
583
|
});
|
|
496
584
|
});
|
|
497
585
|
|
|
586
|
+
/** Print a domain's records with each one's status. */
|
|
587
|
+
function printRecords(ctx, domain) {
|
|
588
|
+
for (const r of domain.records || []) {
|
|
589
|
+
// The per-record status is the whole point: a stuck domain needs to say
|
|
590
|
+
// which record is wrong, not that something is.
|
|
591
|
+
const mark = r.status === "VALID" ? paint(32, "✓") : dim("·");
|
|
592
|
+
const priority = r.priority ? dim(` (priority ${r.priority})`) : "";
|
|
593
|
+
ctx.print(` ${mark} ${dim(r.type.padEnd(5))} ${r.name}`);
|
|
594
|
+
ctx.print(` ${dim("→")} ${r.value}${priority}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
define(
|
|
599
|
+
"domain",
|
|
600
|
+
"Add a custom sending domain, or re-check one",
|
|
601
|
+
"carlyemail domain mail.yourcompany.com [--subdomains] [--check] [--zone]",
|
|
602
|
+
async (ctx) => {
|
|
603
|
+
const name = ctx.positional[0];
|
|
604
|
+
if (!name) throw new UsageError("carlyemail domain <name> [--check]");
|
|
605
|
+
|
|
606
|
+
/** `verify` and the zone file take an id; a person has a name. */
|
|
607
|
+
const findId = async () => {
|
|
608
|
+
const out = await request(ctx.config, "GET", "/v0/domains");
|
|
609
|
+
const found = (out.domains || []).find((d) => d.domain === name);
|
|
610
|
+
if (!found) throw new UsageError(`${name} is not on this account`);
|
|
611
|
+
return found.domain_id;
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
if (ctx.flags.zone) {
|
|
615
|
+
// Straight to stdout with nothing around it, so it can be redirected into
|
|
616
|
+
// a file and handed to a DNS provider's import button.
|
|
617
|
+
const id = await findId();
|
|
618
|
+
ctx.print(await request(ctx.config, "GET", `/v0/domains/${id}/zone-file`, { raw: true }));
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
let domain = ctx.flags.check
|
|
623
|
+
? await request(ctx.config, "POST", `/v0/domains/${await findId()}/verify`)
|
|
624
|
+
// `domain`, not `name`. The docs said `name` for months, which 422s.
|
|
625
|
+
: await request(ctx.config, "POST", "/v0/domains", {
|
|
626
|
+
body: { domain: name, subdomains_enabled: Boolean(ctx.flags.subdomains) },
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// Create is idempotent and returns an existing domain unchanged. Make the
|
|
630
|
+
// flag useful there too: `domain example.com --subdomains` means enabled
|
|
631
|
+
// whether this is the first run or the fifth.
|
|
632
|
+
if (ctx.flags.subdomains && !domain.subdomains_enabled) {
|
|
633
|
+
domain = await request(ctx.config, "PATCH", `/v0/domains/${domain.domain_id}`, {
|
|
634
|
+
body: { subdomains_enabled: true },
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
emit(ctx, domain, () => {
|
|
639
|
+
const verified = domain.status === "VERIFIED";
|
|
640
|
+
ctx.print(`${verified ? paint(32, "✓") : dim("…")} ${bold(name)} ${dim(domain.status)}`);
|
|
641
|
+
if (verified) {
|
|
642
|
+
ctx.print(dim(` carlyemail create --username hello --domain ${name}`));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
ctx.print("");
|
|
646
|
+
printRecords(ctx, domain);
|
|
647
|
+
ctx.print("");
|
|
648
|
+
ctx.print(dim(` publish these, then: carlyemail domain ${name} --check`));
|
|
649
|
+
ctx.print(dim(` or take the file: carlyemail domain ${name} --zone > ${name}.zone`));
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
|
|
498
654
|
define("domains", "List custom sending domains", "carlyemail domains", async (ctx) => {
|
|
499
655
|
const out = await request(ctx.config, "GET", "/v0/domains");
|
|
500
656
|
emit(ctx, out, () => {
|
|
@@ -566,6 +722,25 @@ define("mcp", "Print the MCP endpoint for Claude and other clients", "carlyemail
|
|
|
566
722
|
ctx.print(dim("An API key works too: Authorization: Bearer <key>"));
|
|
567
723
|
});
|
|
568
724
|
|
|
725
|
+
define(
|
|
726
|
+
"env",
|
|
727
|
+
"Export this account for an agent framework",
|
|
728
|
+
'eval "$(carlyemail env)"',
|
|
729
|
+
async (ctx) => {
|
|
730
|
+
// Resolve the inbox through the active key instead of trusting saved CLI
|
|
731
|
+
// state. CARLYEMAIL_API_KEY may intentionally point at a different account,
|
|
732
|
+
// pod, or inbox than ~/.carlyemail/config.json.
|
|
733
|
+
const out = await request(ctx.config, "GET", "/v0/inboxes?limit=1&ascending=true");
|
|
734
|
+
const inbox = out?.inboxes?.[0]?.email;
|
|
735
|
+
if (!inbox) throw new UsageError("This key cannot see an inbox to export.");
|
|
736
|
+
|
|
737
|
+
// This command is the explicit escape hatch from the 0600 CLI config into
|
|
738
|
+
// a process environment. Normal commands continue to mask the credential.
|
|
739
|
+
ctx.print(`export CARLYEMAIL_API_KEY=${shellQuote(ctx.config.api_key)}`);
|
|
740
|
+
ctx.print(`export CARLYEMAIL_INBOX=${shellQuote(inbox)}`);
|
|
741
|
+
}
|
|
742
|
+
);
|
|
743
|
+
|
|
569
744
|
// ---------------------------------------------------------------- help
|
|
570
745
|
|
|
571
746
|
export function helpText() {
|
|
@@ -583,8 +758,9 @@ export function helpText() {
|
|
|
583
758
|
lines.push(
|
|
584
759
|
"",
|
|
585
760
|
bold("Getting started"),
|
|
586
|
-
" carlyemail signup
|
|
761
|
+
" carlyemail signup",
|
|
587
762
|
" carlyemail verify 123456",
|
|
763
|
+
' eval "$(carlyemail env)"' + dim(" # load the key and inbox into this shell"),
|
|
588
764
|
" carlyemail send --from my-agent@agents.carlyemail.com \\",
|
|
589
765
|
' --to someone@example.com --subject Hi --text "Hello from an agent"',
|
|
590
766
|
"",
|
|
@@ -603,7 +779,27 @@ export function helpText() {
|
|
|
603
779
|
|
|
604
780
|
// ---------------------------------------------------------------- entry
|
|
605
781
|
|
|
606
|
-
|
|
782
|
+
/**
|
|
783
|
+
* Read one line from the terminal.
|
|
784
|
+
*
|
|
785
|
+
* Returns null when there is no terminal — a CI job or a piped script must get
|
|
786
|
+
* the usage error it can act on, not a prompt nobody will ever answer.
|
|
787
|
+
*/
|
|
788
|
+
async function askTerminal(question) {
|
|
789
|
+
if (!process.stdin.isTTY) return null;
|
|
790
|
+
const { createInterface } = await import("node:readline/promises");
|
|
791
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
792
|
+
try {
|
|
793
|
+
return (await rl.question(question)).trim();
|
|
794
|
+
} finally {
|
|
795
|
+
rl.close();
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
export async function main(
|
|
800
|
+
argv,
|
|
801
|
+
{ env = process.env, print = console.log, ask = askTerminal, configFile, configDir } = {}
|
|
802
|
+
) {
|
|
607
803
|
const { flags, positional } = parseArgs(argv);
|
|
608
804
|
const name = positional.shift();
|
|
609
805
|
|
|
@@ -629,7 +825,7 @@ export async function main(argv, { env = process.env, print = console.log, confi
|
|
|
629
825
|
|
|
630
826
|
const config = loadConfig(env, configFile);
|
|
631
827
|
try {
|
|
632
|
-
await command.run({ config, flags, positional, print, configFile, configDir });
|
|
828
|
+
await command.run({ config, flags, positional, print, ask, configFile, configDir });
|
|
633
829
|
return 0;
|
|
634
830
|
} catch (error) {
|
|
635
831
|
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.4.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,18 @@
|
|
|
24
24
|
"bin": {
|
|
25
25
|
"carlyemail": "./carlyemail.js"
|
|
26
26
|
},
|
|
27
|
-
"
|
|
27
|
+
"types": "./sdk.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./sdk.js",
|
|
30
|
+
"./webhooks": "./webhooks.js",
|
|
31
|
+
"./cli": "./carlyemail.js"
|
|
32
|
+
},
|
|
28
33
|
"files": [
|
|
29
34
|
"carlyemail.js",
|
|
35
|
+
"sdk.js",
|
|
36
|
+
"sdk.d.ts",
|
|
37
|
+
"webhooks.js",
|
|
38
|
+
"webhooks.d.ts",
|
|
30
39
|
"README.md",
|
|
31
40
|
"LICENSE"
|
|
32
41
|
],
|