carlyemail 0.5.0 → 0.6.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 (2) hide show
  1. package/carlyemail.js +81 -14
  2. package/package.json +2 -7
package/carlyemail.js CHANGED
@@ -18,7 +18,7 @@ import { join } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { randomBytes } from "node:crypto";
20
20
 
21
- export const VERSION = "0.5.0";
21
+ export const VERSION = "0.6.0";
22
22
 
23
23
  const DEFAULT_API = "https://api.carlyemail.com";
24
24
  const CONFIG_DIR = join(homedir(), ".carlyemail");
@@ -229,6 +229,28 @@ function suggestUsername() {
229
229
  return `agent-${randomBytes(2).toString("hex")}`;
230
230
  }
231
231
 
232
+ /**
233
+ * Ask the server to email a sign-in code and remember which address it went to,
234
+ * so a later `verify` knows to exchange the code for a key rather than confirm
235
+ * a sign-up. `known` distinguishes "you told me this account exists" (signup
236
+ * got `account_exists`) from the server's deliberately non-committal answer.
237
+ */
238
+ async function startSignin(ctx, human_email, { known = false } = {}) {
239
+ await request(ctx.config, "POST", "/v0/agent/sign-in", {
240
+ auth: false,
241
+ body: { human_email },
242
+ });
243
+ saveConfig({ ...ctx.config, pending_signin: human_email }, ctx.configFile, ctx.configDir);
244
+ ctx.print(
245
+ arrow(
246
+ known
247
+ ? `a sign-in code is on its way to ${human_email} (good for 10 minutes), then:`
248
+ : `if ${human_email} has an account, a code is on its way (good for 10 minutes), then:`
249
+ )
250
+ );
251
+ ctx.print(` carlyemail verify <code>`);
252
+ }
253
+
232
254
  define(
233
255
  "signup",
234
256
  "Create an account and an inbox",
@@ -258,19 +280,33 @@ define(
258
280
  const display_name =
259
281
  typeof ctx.flags["display-name"] === "string" ? ctx.flags["display-name"] : undefined;
260
282
 
261
- const out = await request(ctx.config, "POST", "/v0/agent/sign-up", {
262
- auth: false,
263
- body: { human_email, username, source: "cli", display_name },
264
- });
283
+ let out;
284
+ try {
285
+ out = await request(ctx.config, "POST", "/v0/agent/sign-up", {
286
+ auth: false,
287
+ body: { human_email, username, source: "cli", display_name },
288
+ });
289
+ } catch (error) {
290
+ // An existing account is not a dead end. Fall through to the sign-in
291
+ // flow the same way the console does, without being asked — the person
292
+ // typed their address wanting a working key, not an error about which
293
+ // command would have produced one.
294
+ if (error instanceof ApiError && error.code === "account_exists") {
295
+ ctx.print(`${human_email} already has an account — signing in instead.`);
296
+ await startSignin(ctx, human_email, { known: true });
297
+ return;
298
+ }
299
+ throw error;
300
+ }
265
301
 
266
302
  // Saved before anything else is printed: the key is returned exactly once
267
303
  // and is unrecoverable afterwards, so losing it to a later crash would
268
304
  // cost the account.
269
- const saved = saveConfig(
270
- { ...ctx.config, api_key: out.api_key, organization_id: out.organization_id },
271
- ctx.configFile,
272
- ctx.configDir
273
- );
305
+ const config = { ...ctx.config, api_key: out.api_key, organization_id: out.organization_id };
306
+ // A sign-in abandoned before its code arrived must not re-route the
307
+ // `verify` that belongs to this fresh sign-up.
308
+ delete config.pending_signin;
309
+ const saved = saveConfig(config, ctx.configFile, ctx.configDir);
274
310
 
275
311
  ctx.print(ok(bold(out.inbox_id)));
276
312
  ctx.print(ok(`key saved to ${saved}`));
@@ -278,7 +314,22 @@ define(
278
314
  ctx.print(arrow(`check ${human_email} for a 6-digit code, then:`));
279
315
  ctx.print(` carlyemail verify <code>`);
280
316
  ctx.print("");
281
- ctx.print(dim("Until it is confirmed the account can read its own mail but not send."));
317
+ ctx.print(dim(`Until the code is confirmed the account can only email ${human_email}.`));
318
+ }
319
+ );
320
+
321
+ define(
322
+ "signin",
323
+ "Sign in to an existing account with an emailed code",
324
+ "carlyemail signin you@example.com",
325
+ async (ctx) => {
326
+ let human_email = ctx.positional[0] || ctx.flags["human-email"];
327
+ if (typeof human_email !== "string" || !human_email) {
328
+ const answer = await ctx.ask("The account's owner email: ");
329
+ if (answer) human_email = answer;
330
+ }
331
+ if (!human_email) throw new UsageError("carlyemail signin you@example.com");
332
+ await startSignin(ctx, human_email);
282
333
  }
283
334
  );
284
335
 
@@ -286,8 +337,24 @@ define("verify", "Confirm the owner email with the code", "carlyemail verify 123
286
337
  const code = ctx.positional[0] || ctx.flags.code;
287
338
  if (!code) throw new UsageError("the 6-digit code is required: carlyemail verify 123456");
288
339
 
340
+ // A pending sign-in and a sign-up confirmation are the same step to the
341
+ // person typing the code, but different calls: one trades the code for a new
342
+ // key, the other upgrades the key sign-up already saved. The config
343
+ // remembers which one is open.
344
+ if (ctx.config.pending_signin) {
345
+ const out = await request(ctx.config, "POST", "/v0/agent/sign-in/verify", {
346
+ auth: false,
347
+ body: { human_email: ctx.config.pending_signin, otp_code: String(code) },
348
+ });
349
+ const config = { ...ctx.config, api_key: out.api_key, organization_id: out.organization_id };
350
+ delete config.pending_signin;
351
+ const saved = saveConfig(config, ctx.configFile, ctx.configDir);
352
+ ctx.print(ok(`signed in — key saved to ${saved}`));
353
+ return;
354
+ }
355
+
289
356
  await request(ctx.config, "POST", "/v0/agent/verify", { body: { otp_code: String(code) } });
290
- ctx.print(ok("verified — this account can send now"));
357
+ ctx.print(ok("verified — this account can send anywhere now"));
291
358
  });
292
359
 
293
360
  define("whoami", "Show the key's identity and scope", "carlyemail whoami", async (ctx) => {
@@ -726,9 +793,9 @@ define("plan", "Show the current plan and its limits", "carlyemail plan", async
726
793
  ctx.print(` ${dim("email")} ${cap(billing.monthly_emails)} a month`);
727
794
  });
728
795
 
729
- define("upgrade", "Get a checkout link for a paid plan", "carlyemail upgrade developer", async (ctx) => {
796
+ define("upgrade", "Get a checkout link for a paid plan", "carlyemail upgrade startup", async (ctx) => {
730
797
  const plan = ctx.positional[0] || ctx.flags.plan;
731
- if (!plan) throw new UsageError("which plan? carlyemail upgrade developer|startup");
798
+ if (!plan) throw new UsageError("which plan? carlyemail upgrade startup|business");
732
799
  const out = await request(ctx.config, "POST", "/v0/billing/checkout", { body: { plan } });
733
800
  emit(ctx, out, () => {
734
801
  ctx.print(arrow("open this to upgrade:"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carlyemail",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Real email inboxes your agent can send, receive and reply from. SDK and CLI.",
5
5
  "keywords": [
6
6
  "email",
@@ -11,13 +11,8 @@
11
11
  "mcp",
12
12
  "cli"
13
13
  ],
14
- "homepage": "https://carlyemail.com",
14
+ "homepage": "https://docs.carlyemail.com",
15
15
  "bugs": "https://docs.carlyemail.com/support",
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/shirschfield/carlyemail.git",
19
- "directory": "cli"
20
- },
21
16
  "license": "MIT",
22
17
  "author": "SWH Labs LLC",
23
18
  "type": "module",