run402 1.38.0 → 1.40.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/lib/email.mjs CHANGED
@@ -1,4 +1,6 @@
1
- import { findProject, resolveProjectId, API, updateProject, loadKeyStore, saveKeyStore } from "./config.mjs";
1
+ import { resolveProjectId } from "./config.mjs";
2
+ import { getSdk } from "./sdk.mjs";
3
+ import { reportSdkError } from "./sdk-errors.mjs";
2
4
 
3
5
  const HELP = `run402 email — Send emails from your project
4
6
 
@@ -44,17 +46,11 @@ Examples:
44
46
  run402 email create my-app
45
47
  run402 email send --template project_invite --to user@example.com \\
46
48
  --var project_name="My App" --var invite_url="https://example.com/invite/abc"
47
- run402 email send --template project_invite --to user@example.com \\
48
- --vars '{"project_name":"My App","invite_url":"https://example.com/invite/abc"}'
49
49
  run402 email send --to user@example.com --subject "Welcome!" \\
50
- --html "<h1>Hello</h1><p>Welcome aboard.</p>" --from-name "My App"
51
- run402 email send --template notification --to admin@example.com \\
52
- --var project_name="My App" --var message="Deploy complete"
50
+ --html "<h1>Hello</h1>" --from-name "My App"
53
51
  run402 email list --limit 50
54
- run402 email list --limit 50 --after msg_abc123
55
52
  run402 email info
56
53
  run402 email get msg_abc123
57
- run402 email get-raw msg_abc123 --output reply.eml
58
54
  run402 email reply msg_abc123 --html "<p>Thanks!</p>"
59
55
  run402 email delete --confirm
60
56
  run402 email webhooks list
@@ -64,7 +60,6 @@ Notes:
64
60
  - One mailbox per project
65
61
  - Single recipient per send (no CC/BCC)
66
62
  - Slug: 3-63 chars, lowercase alphanumeric + hyphens, no consecutive hyphens
67
- - Rate limits vary by tier (prototype: 10/day, hobby: 50/day, team: 500/day)
68
63
  - --project defaults to the active project
69
64
  `;
70
65
 
@@ -80,99 +75,33 @@ Options:
80
75
  --to <email> Recipient email address (required; single recipient)
81
76
  --template <name> Template name (template mode): project_invite, magic_link,
82
77
  notification
83
- --var key=value Template variable (repeatable; required keys vary by
84
- template)
78
+ --var key=value Template variable (repeatable)
85
79
  --vars '<json>' All template variables as a single JSON object
86
- (alternative to multiple --var). Later --var overrides.
87
80
  --subject "..." Subject line (raw HTML mode; required with --html)
88
81
  --html "..." HTML body (raw HTML mode; required with --subject)
89
82
  --text "..." Plain-text body (raw HTML mode; optional)
90
83
  --from-name "..." Display name for the From header
91
84
  --project <id> Project ID (defaults to the active project)
92
-
93
- Templates:
94
- project_invite project_name, invite_url
95
- magic_link project_name, link_url, expires_in
96
- notification project_name, message (max 500 chars)
97
-
98
- Examples:
99
- run402 email send --template project_invite --to user@example.com \\
100
- --var project_name="My App" --var invite_url="https://example.com/invite/abc"
101
- run402 email send --template project_invite --to user@example.com \\
102
- --vars '{"project_name":"My App","invite_url":"https://example.com/invite/abc"}'
103
- run402 email send --to user@example.com --subject "Welcome!" \\
104
- --html "<h1>Hello</h1><p>Welcome aboard.</p>" --from-name "My App"
105
85
  `,
106
86
  list: `run402 email list — List messages in the mailbox
107
87
 
108
88
  Usage:
109
89
  run402 email list [--limit <n>] [--after <cursor>] [--project <id>]
110
-
111
- Options:
112
- --limit <n> Max messages to return (server caps at 200)
113
- --after <cursor> Pagination cursor (message id from prior page)
114
- --project <id> Project ID (defaults to the active project)
115
-
116
- Examples:
117
- run402 email list
118
- run402 email list --limit 50
119
- run402 email list --limit 50 --after msg_abc123
120
90
  `,
121
91
  reply: `run402 email reply — Reply to an inbound message (threaded via In-Reply-To)
122
92
 
123
93
  Usage:
124
94
  run402 email reply <message_id> --html "..." [--text "..."] [options]
125
-
126
- Arguments:
127
- <message_id> Inbound message ID to reply to
128
-
129
- Options:
130
- --html "..." HTML reply body (required unless --text is given)
131
- --text "..." Plain-text reply body (required unless --html is given)
132
- --subject "..." Override the reply subject (default: "Re: <original>")
133
- --from-name "..." Display name for the From header
134
- --project <id> Project ID (defaults to the active project)
135
-
136
- Notes:
137
- The CLI fetches the original message to derive the reply-to address and
138
- subject, then POSTs a new message with in_reply_to = <message_id> so the
139
- server can wire the RFC-822 In-Reply-To / References headers.
140
-
141
- Examples:
142
- run402 email reply msg_abc123 --html "<p>Thanks, here's the info you asked for.</p>"
143
- run402 email reply msg_abc123 --subject "Re: invoice #42" --text "Paid, thanks."
144
95
  `,
145
96
  delete: `run402 email delete — Delete the project's mailbox (irreversible)
146
97
 
147
98
  Usage:
148
99
  run402 email delete [<mailbox_id>] --confirm [--project <id>]
149
-
150
- Arguments:
151
- <mailbox_id> Mailbox ID to delete (defaults to the project's mailbox)
152
-
153
- Options:
154
- --confirm Required: explicit confirmation flag
155
- --project <id> Project ID (defaults to the active project)
156
-
157
- Notes:
158
- Destructive. Drops all messages and webhook subscriptions. Cached
159
- mailbox_id in the local keystore is cleared on success.
160
-
161
- Examples:
162
- run402 email delete --confirm
163
- run402 email delete mbx_abc123 --confirm
164
100
  `,
165
101
  info: `run402 email info — Show mailbox info (ID, address, slug)
166
102
 
167
103
  Usage:
168
104
  run402 email info [--project <id>]
169
-
170
- Options:
171
- --project <id> Project ID (defaults to the active project)
172
-
173
- Notes:
174
- Same output as 'run402 email status' (kept as an alias for backward
175
- compatibility). 'info' is the preferred name.
176
105
  `,
177
106
  status: `run402 email status — Alias for 'run402 email info' (prefer 'info')
178
107
 
@@ -186,22 +115,9 @@ compatibility; new code should use 'info'.
186
115
 
187
116
  Usage:
188
117
  run402 email get-raw <message_id> [--output <file>] [--project <id>]
189
-
190
- Arguments:
191
- <message_id> Message ID to fetch (inbound messages only)
192
-
193
- Options:
194
- --output <file> Write raw bytes to this file; omit to stream to stdout
195
- --project <id> Project ID (defaults to the active project)
196
-
197
- Examples:
198
- run402 email get-raw msg_abc123 --output reply.eml
199
- run402 email get-raw msg_abc123 > reply.eml
200
118
  `,
201
119
  };
202
120
 
203
- const SLUG_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
204
-
205
121
  function parseFlag(args, flag) {
206
122
  for (let i = 0; i < args.length; i++) {
207
123
  if (args[i] === flag && args[i + 1]) return args[i + 1];
@@ -211,7 +127,6 @@ function parseFlag(args, flag) {
211
127
 
212
128
  function parseVars(args) {
213
129
  const vars = {};
214
- // Apply --vars '<json>' first so later --var can override on key collision.
215
130
  for (let i = 0; i < args.length; i++) {
216
131
  if (args[i] === "--vars" && args[i + 1]) {
217
132
  const raw = args[++i];
@@ -227,53 +142,16 @@ function parseVars(args) {
227
142
  for (const [k, v] of Object.entries(parsed)) vars[k] = typeof v === "string" ? v : String(v);
228
143
  }
229
144
  }
230
- // Then --var key=value (later wins).
231
145
  for (let i = 0; i < args.length; i++) {
232
146
  if (args[i] === "--var" && args[i + 1]) {
233
147
  const raw = args[++i];
234
148
  const eq = raw.indexOf("=");
235
- if (eq > 0) {
236
- vars[raw.slice(0, eq)] = raw.slice(eq + 1);
237
- }
149
+ if (eq > 0) vars[raw.slice(0, eq)] = raw.slice(eq + 1);
238
150
  }
239
151
  }
240
152
  return vars;
241
153
  }
242
154
 
243
- async function resolveMailboxId(projectId, serviceKey) {
244
- const store = loadKeyStore();
245
- const proj = store.projects[projectId];
246
- if (proj && proj.mailbox_id) return proj.mailbox_id;
247
-
248
- // Fallback: discover via API
249
- const res = await fetch(`${API}/mailboxes/v1`, {
250
- headers: { "Authorization": `Bearer ${serviceKey}` },
251
- });
252
- if (!res.ok) {
253
- const data = await res.json().catch(() => ({}));
254
- throw Object.assign(new Error("Failed to resolve mailbox"), { http: res.status, ...data });
255
- }
256
- const body = await res.json();
257
- const mailboxes = body.mailboxes || body;
258
- if (!Array.isArray(mailboxes) || mailboxes.length === 0) {
259
- throw new Error("No mailbox found. Run: run402 email create <slug>");
260
- }
261
- const mb = mailboxes[0];
262
- updateProject(projectId, { mailbox_id: mb.mailbox_id, mailbox_address: mb.address });
263
- return mb.mailbox_id;
264
- }
265
-
266
- async function requireMailboxId(projectId, serviceKey) {
267
- try {
268
- return await resolveMailboxId(projectId, serviceKey);
269
- } catch (err) {
270
- const out = { status: "error", message: err.message };
271
- if (err.http) out.http = err.http;
272
- console.error(JSON.stringify(out));
273
- process.exit(1);
274
- }
275
- }
276
-
277
155
  async function create(args) {
278
156
  let slug = null;
279
157
  let projectOpt = null;
@@ -282,48 +160,22 @@ async function create(args) {
282
160
  else if (!args[i].startsWith("--") && !slug) { slug = args[i]; }
283
161
  }
284
162
  const projectId = resolveProjectId(projectOpt);
285
- const p = findProject(projectId);
286
-
287
163
  if (!slug) {
288
164
  console.error(JSON.stringify({ status: "error", message: "Missing slug. Usage: run402 email create <slug>" }));
289
165
  process.exit(1);
290
166
  }
291
- if (slug.length < 3 || slug.length > 63) {
292
- console.error(JSON.stringify({ status: "error", message: "Slug must be 3-63 characters." }));
293
- process.exit(1);
294
- }
295
- if (!SLUG_RE.test(slug)) {
296
- console.error(JSON.stringify({ status: "error", message: "Slug must be lowercase alphanumeric + hyphens, start/end with alphanumeric." }));
297
- process.exit(1);
298
- }
299
- if (slug.includes("--")) {
300
- console.error(JSON.stringify({ status: "error", message: "Slug must not contain consecutive hyphens." }));
301
- process.exit(1);
302
- }
303
167
 
304
- const res = await fetch(`${API}/mailboxes/v1`, {
305
- method: "POST",
306
- headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
307
- body: JSON.stringify({ slug, project_id: projectId }),
308
- });
309
- const data = await res.json();
310
- if (!res.ok) {
311
- // On 409 (mailbox already exists), discover existing mailbox and return it
312
- if (res.status === 409) {
313
- const mbId = await resolveMailboxId(projectId, p.service_key).catch(() => null);
314
- if (mbId) {
315
- const store = loadKeyStore();
316
- const proj = store.projects[projectId];
317
- console.log(JSON.stringify({ status: "ok", mailbox_id: mbId, address: proj?.mailbox_address || mbId, already_existed: true }));
318
- return;
319
- }
168
+ try {
169
+ const data = await getSdk().email.createMailbox(projectId, slug);
170
+ // SDK may return CreateMailboxResult (with slug) on success, or MailboxInfo on 409 idempotency.
171
+ if (data.slug) {
172
+ console.log(JSON.stringify({ status: "ok", mailbox_id: data.mailbox_id, address: data.address, slug: data.slug }));
173
+ } else {
174
+ console.log(JSON.stringify({ status: "ok", mailbox_id: data.mailbox_id, address: data.address, already_existed: true }));
320
175
  }
321
- console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
322
- process.exit(1);
176
+ } catch (err) {
177
+ reportSdkError(err);
323
178
  }
324
-
325
- updateProject(projectId, { mailbox_id: data.mailbox_id, mailbox_address: data.address });
326
- console.log(JSON.stringify({ status: "ok", mailbox_id: data.mailbox_id, address: data.address, slug: data.slug }));
327
179
  }
328
180
 
329
181
  async function send(args) {
@@ -334,7 +186,6 @@ async function send(args) {
334
186
  const text = parseFlag(args, "--text");
335
187
  const fromName = parseFlag(args, "--from-name");
336
188
  const projectId = resolveProjectId(parseFlag(args, "--project"));
337
- const p = findProject(projectId);
338
189
  const variables = parseVars(args);
339
190
 
340
191
  if (!to) {
@@ -342,73 +193,35 @@ async function send(args) {
342
193
  process.exit(1);
343
194
  }
344
195
 
345
- const hasSubject = !!subject;
346
- const hasHtml = !!html;
347
- const isRaw = hasSubject || hasHtml;
348
- const isTemplate = !!template;
349
- if (!isRaw && !isTemplate) {
350
- console.error(JSON.stringify({ status: "error", message: "Provide --template (template mode) or both --subject and --html (raw HTML mode)" }));
351
- process.exit(1);
352
- }
353
- if (isRaw && isTemplate) {
354
- console.error(JSON.stringify({ status: "error", message: "Provide --template OR raw mode (--subject + --html), not both" }));
355
- process.exit(1);
356
- }
357
- if (isRaw && !(hasSubject && hasHtml)) {
358
- const missing = hasSubject ? "--html" : "--subject";
359
- console.error(JSON.stringify({ status: "error", message: `Raw mode requires both --subject and --html (missing ${missing})` }));
360
- process.exit(1);
361
- }
362
-
363
- const mailboxId = await requireMailboxId(projectId, p.service_key);
364
-
365
- const body = { to };
366
- if (isTemplate) {
367
- body.template = template;
368
- body.variables = variables;
369
- } else {
370
- body.subject = subject;
371
- body.html = html;
372
- if (text) body.text = text;
373
- }
374
- if (fromName) body.from_name = fromName;
375
-
376
- const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/messages`, {
377
- method: "POST",
378
- headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
379
- body: JSON.stringify(body),
380
- });
381
- const data = await res.json();
382
- if (!res.ok) {
383
- console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
384
- process.exit(1);
196
+ try {
197
+ const data = await getSdk().email.send(projectId, {
198
+ to,
199
+ template: template ?? undefined,
200
+ variables: template ? variables : undefined,
201
+ subject: subject ?? undefined,
202
+ html: html ?? undefined,
203
+ text: text ?? undefined,
204
+ from_name: fromName ?? undefined,
205
+ });
206
+ console.log(JSON.stringify({ status: "ok", message_id: data.id, to: data.to, template: data.template || null, subject: data.subject || null }));
207
+ } catch (err) {
208
+ reportSdkError(err);
385
209
  }
386
-
387
- console.log(JSON.stringify({ status: "ok", message_id: data.id, to: data.to, template: data.template || null, subject: data.subject || null }));
388
210
  }
389
211
 
390
212
  async function list(args) {
391
213
  const projectId = resolveProjectId(parseFlag(args, "--project"));
392
- const p = findProject(projectId);
393
214
  const limit = parseFlag(args, "--limit");
394
215
  const after = parseFlag(args, "--after");
395
- const mailboxId = await requireMailboxId(projectId, p.service_key);
396
-
397
- const qs = new URLSearchParams();
398
- if (limit) qs.set("limit", limit);
399
- if (after) qs.set("after", after);
400
- const url = `${API}/mailboxes/v1/${mailboxId}/messages${qs.toString() ? "?" + qs.toString() : ""}`;
401
-
402
- const res = await fetch(url, {
403
- headers: { "Authorization": `Bearer ${p.service_key}` },
404
- });
405
- const data = await res.json();
406
- if (!res.ok) {
407
- console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
408
- process.exit(1);
216
+ try {
217
+ const data = await getSdk().email.list(projectId, {
218
+ limit: limit ? Number(limit) : undefined,
219
+ after: after ?? undefined,
220
+ });
221
+ console.log(JSON.stringify(data, null, 2));
222
+ } catch (err) {
223
+ reportSdkError(err);
409
224
  }
410
-
411
- console.log(JSON.stringify(data, null, 2));
412
225
  }
413
226
 
414
227
  async function get(args) {
@@ -419,25 +232,16 @@ async function get(args) {
419
232
  else if (!args[i].startsWith("--") && !messageId) { messageId = args[i]; }
420
233
  }
421
234
  const projectId = resolveProjectId(projectOpt);
422
- const p = findProject(projectId);
423
-
424
235
  if (!messageId) {
425
236
  console.error(JSON.stringify({ status: "error", message: "Missing message_id. Usage: run402 email get <message_id>" }));
426
237
  process.exit(1);
427
238
  }
428
-
429
- const mailboxId = await requireMailboxId(projectId, p.service_key);
430
-
431
- const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/messages/${messageId}`, {
432
- headers: { "Authorization": `Bearer ${p.service_key}` },
433
- });
434
- const data = await res.json();
435
- if (!res.ok) {
436
- console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
437
- process.exit(1);
239
+ try {
240
+ const data = await getSdk().email.get(projectId, messageId);
241
+ console.log(JSON.stringify(data, null, 2));
242
+ } catch (err) {
243
+ reportSdkError(err);
438
244
  }
439
-
440
- console.log(JSON.stringify(data, null, 2));
441
245
  }
442
246
 
443
247
  async function getRaw(args) {
@@ -450,42 +254,30 @@ async function getRaw(args) {
450
254
  else if (!args[i].startsWith("--") && !messageId) { messageId = args[i]; }
451
255
  }
452
256
  const projectId = resolveProjectId(projectOpt);
453
- const p = findProject(projectId);
454
-
455
257
  if (!messageId) {
456
258
  console.error(JSON.stringify({ status: "error", message: "Missing message_id. Usage: run402 email get-raw <message_id> [--output <file>]" }));
457
259
  process.exit(1);
458
260
  }
459
261
 
460
- const mailboxId = await requireMailboxId(projectId, p.service_key);
461
-
462
- const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/messages/${messageId}/raw`, {
463
- headers: { "Authorization": `Bearer ${p.service_key}` },
464
- });
465
- if (!res.ok) {
466
- let errBody;
467
- try { errBody = await res.json(); } catch { errBody = { error: await res.text().catch(() => "Unknown error") }; }
468
- console.error(JSON.stringify({ status: "error", http: res.status, ...errBody }));
469
- process.exit(1);
470
- }
471
-
472
- const buf = Buffer.from(await res.arrayBuffer());
473
-
474
- if (outputFile) {
475
- const { writeFileSync } = await import("node:fs");
476
- writeFileSync(outputFile, buf);
477
- console.log(JSON.stringify({ status: "ok", message_id: messageId, bytes: buf.length, output: outputFile }));
478
- } else {
479
- // Write raw bytes to stdout (no JSON wrapping — binary pipe-friendly)
480
- process.stdout.write(buf);
262
+ try {
263
+ const result = await getSdk().email.getRaw(projectId, messageId);
264
+ const buf = Buffer.from(result.bytes);
265
+
266
+ if (outputFile) {
267
+ const { writeFileSync } = await import("node:fs");
268
+ writeFileSync(outputFile, buf);
269
+ console.log(JSON.stringify({ status: "ok", message_id: messageId, bytes: buf.length, output: outputFile }));
270
+ } else {
271
+ process.stdout.write(buf);
272
+ }
273
+ } catch (err) {
274
+ reportSdkError(err);
481
275
  }
482
276
  }
483
277
 
484
278
  async function reply(args) {
485
279
  let messageId = null;
486
280
  let projectOpt = null;
487
- let outputFile = null;
488
- void outputFile;
489
281
  for (let i = 0; i < args.length; i++) {
490
282
  const a = args[i];
491
283
  if (a === "--project" && args[i + 1]) { projectOpt = args[++i]; }
@@ -497,7 +289,6 @@ async function reply(args) {
497
289
  const subjectOverride = parseFlag(args, "--subject");
498
290
  const fromName = parseFlag(args, "--from-name");
499
291
  const projectId = resolveProjectId(projectOpt);
500
- const p = findProject(projectId);
501
292
 
502
293
  if (!messageId) {
503
294
  console.error(JSON.stringify({ status: "error", message: "Missing message_id. Usage: run402 email reply <message_id> --html \"...\"" }));
@@ -508,45 +299,36 @@ async function reply(args) {
508
299
  process.exit(1);
509
300
  }
510
301
 
511
- const mailboxId = await requireMailboxId(projectId, p.service_key);
512
-
513
- const getRes = await fetch(`${API}/mailboxes/v1/${mailboxId}/messages/${messageId}`, {
514
- headers: { "Authorization": `Bearer ${p.service_key}` },
515
- });
516
- const original = await getRes.json().catch(() => ({}));
517
- if (!getRes.ok) {
518
- console.error(JSON.stringify({ status: "error", http: getRes.status, message: "Failed to fetch original message", ...original }));
519
- process.exit(1);
520
- }
521
-
522
- const replyTo = original.from || original.from_address || original.sender || null;
523
- if (!replyTo) {
524
- console.error(JSON.stringify({ status: "error", message: "Original message has no from address to reply to", original_keys: Object.keys(original) }));
525
- process.exit(1);
526
- }
527
- const origSubject = typeof original.subject === "string" ? original.subject : "";
528
- const defaultSubject = origSubject && origSubject.toLowerCase().startsWith("re:")
529
- ? origSubject
530
- : `Re: ${origSubject || "(no subject)"}`;
531
- const replySubject = subjectOverride || defaultSubject;
532
-
533
- const body = { to: replyTo, subject: replySubject, in_reply_to: messageId };
534
- if (html) body.html = html;
535
- if (text) body.text = text;
536
- if (fromName) body.from_name = fromName;
537
-
538
- const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/messages`, {
539
- method: "POST",
540
- headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
541
- body: JSON.stringify(body),
542
- });
543
- const data = await res.json().catch(() => ({}));
544
- if (!res.ok) {
545
- console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
546
- process.exit(1);
302
+ try {
303
+ // Fetch the original message to derive the reply-to address and subject.
304
+ const original = await getSdk().email.get(projectId, messageId);
305
+ const replyTo = original.from || original.from_address || original.sender || null;
306
+ if (!replyTo) {
307
+ console.error(JSON.stringify({
308
+ status: "error",
309
+ message: "Original message has no from address to reply to",
310
+ original_keys: Object.keys(original),
311
+ }));
312
+ process.exit(1);
313
+ }
314
+ const origSubject = typeof original.subject === "string" ? original.subject : "";
315
+ const defaultSubject = origSubject && origSubject.toLowerCase().startsWith("re:")
316
+ ? origSubject
317
+ : `Re: ${origSubject || "(no subject)"}`;
318
+ const replySubject = subjectOverride || defaultSubject;
319
+
320
+ const data = await getSdk().email.send(projectId, {
321
+ to: replyTo,
322
+ subject: replySubject,
323
+ html: html ?? undefined,
324
+ text: text ?? undefined,
325
+ from_name: fromName ?? undefined,
326
+ in_reply_to: messageId,
327
+ });
328
+ console.log(JSON.stringify({ status: "ok", message_id: data.id, to: data.to, subject: replySubject, in_reply_to: messageId }));
329
+ } catch (err) {
330
+ reportSdkError(err);
547
331
  }
548
-
549
- console.log(JSON.stringify({ status: "ok", message_id: data.id, to: data.to, subject: replySubject, in_reply_to: messageId }));
550
332
  }
551
333
 
552
334
  async function deleteMailbox(args) {
@@ -559,7 +341,6 @@ async function deleteMailbox(args) {
559
341
  else if (!a.startsWith("--") && !positional) { positional = a; }
560
342
  }
561
343
  const projectId = resolveProjectId(projectOpt);
562
- const p = findProject(projectId);
563
344
  const confirmed = args.includes("--confirm");
564
345
 
565
346
  if (!confirmed) {
@@ -570,53 +351,22 @@ async function deleteMailbox(args) {
570
351
  process.exit(1);
571
352
  }
572
353
 
573
- const mailboxId = positional || await requireMailboxId(projectId, p.service_key);
574
-
575
- const res = await fetch(`${API}/mailboxes/v1/${mailboxId}`, {
576
- method: "DELETE",
577
- headers: { "Authorization": `Bearer ${p.service_key}` },
578
- });
579
- if (res.status !== 204 && !res.ok) {
580
- let errBody;
581
- try { errBody = await res.json(); } catch { errBody = {}; }
582
- console.error(JSON.stringify({ status: "error", http: res.status, ...errBody }));
583
- process.exit(1);
584
- }
585
-
586
- // Clear the cached mailbox_id/address from the local keystore so future
587
- // email commands re-discover (or fail-fast with "no mailbox found").
588
- const store = loadKeyStore();
589
- const proj = store.projects[projectId];
590
- if (proj) {
591
- delete proj.mailbox_id;
592
- delete proj.mailbox_address;
593
- saveKeyStore(store);
354
+ try {
355
+ await getSdk().email.deleteMailbox(projectId, positional ?? undefined);
356
+ console.log(JSON.stringify({ status: "ok", mailbox_id: positional ?? "(resolved)", deleted: true }));
357
+ } catch (err) {
358
+ reportSdkError(err);
594
359
  }
595
-
596
- console.log(JSON.stringify({ status: "ok", mailbox_id: mailboxId, deleted: true }));
597
360
  }
598
361
 
599
362
  async function status(args) {
600
363
  const projectId = resolveProjectId(parseFlag(args, "--project"));
601
- const p = findProject(projectId);
602
-
603
- const res = await fetch(`${API}/mailboxes/v1`, {
604
- headers: { "Authorization": `Bearer ${p.service_key}` },
605
- });
606
- if (!res.ok) {
607
- const data = await res.json().catch(() => ({}));
608
- console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
609
- process.exit(1);
610
- }
611
- const body = await res.json();
612
- const mailboxes = body.mailboxes || body;
613
- if (!Array.isArray(mailboxes) || mailboxes.length === 0) {
614
- console.error(JSON.stringify({ status: "error", message: "No mailbox found. Run: run402 email create <slug>" }));
615
- process.exit(1);
364
+ try {
365
+ const mb = await getSdk().email.getMailbox(projectId);
366
+ console.log(JSON.stringify({ status: "ok", mailbox_id: mb.mailbox_id, address: mb.address, slug: mb.slug }));
367
+ } catch (err) {
368
+ reportSdkError(err);
616
369
  }
617
- const mb = mailboxes[0];
618
- updateProject(projectId, { mailbox_id: mb.mailbox_id, mailbox_address: mb.address });
619
- console.log(JSON.stringify({ status: "ok", mailbox_id: mb.mailbox_id, address: mb.address, slug: mb.slug }));
620
370
  }
621
371
 
622
372
  export async function run(sub, args) {
@@ -624,7 +374,7 @@ export async function run(sub, args) {
624
374
  if (Array.isArray(args) && (args.includes("--help") || args.includes("-h")) && sub !== "webhooks") { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
625
375
  switch (sub) {
626
376
  case "create": await create(args); break;
627
- case "info": // fall through — 'info' is the preferred name; 'status' is a backward-compat alias
377
+ case "info":
628
378
  case "status": await status(args); break;
629
379
  case "send": await send(args); break;
630
380
  case "list": await list(args); break;