@darksol/terminal 0.3.6 → 0.4.0-beta.2

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.
@@ -0,0 +1,705 @@
1
+ import { AgentMailClient } from 'agentmail';
2
+ import open from 'open';
3
+ import { getConfig, setConfig } from '../config/store.js';
4
+ import { hasKey, getKeyAuto, addKeyDirect, SERVICES } from '../config/keys.js';
5
+ import { theme } from '../ui/theme.js';
6
+ import { spinner, kvDisplay, success, error, warn, info, table } from '../ui/components.js';
7
+ import { showSection, showDivider } from '../ui/banner.js';
8
+ import inquirer from 'inquirer';
9
+
10
+ // ══════════════════════════════════════════════════
11
+ // AGENTMAIL INTEGRATION
12
+ // ══════════════════════════════════════════════════
13
+ //
14
+ // Email for AI agents. Create inboxes, send/receive
15
+ // emails, manage threads — all from the terminal.
16
+ //
17
+ // API: https://docs.agentmail.to
18
+ // Console: https://console.agentmail.to
19
+ // SDK: npm install agentmail
20
+ //
21
+ // ══════════════════════════════════════════════════
22
+
23
+ const CONSOLE_URL = 'https://console.agentmail.to';
24
+ const DOCS_URL = 'https://docs.agentmail.to';
25
+
26
+ /**
27
+ * Get an authenticated AgentMail client
28
+ */
29
+ async function getClient() {
30
+ // Try vault first, then env
31
+ let apiKey = getKeyAuto('agentmail');
32
+ if (!apiKey) apiKey = process.env.AGENTMAIL_API_KEY;
33
+
34
+ if (!apiKey) {
35
+ return null;
36
+ }
37
+
38
+ return new AgentMailClient({ apiKey });
39
+ }
40
+
41
+ /**
42
+ * Ensure AgentMail is set up — prompt if not
43
+ */
44
+ async function ensureSetup() {
45
+ const client = await getClient();
46
+ if (client) return client;
47
+
48
+ console.log('');
49
+ showSection('📧 AGENTMAIL SETUP');
50
+ console.log('');
51
+ console.log(theme.dim(' AgentMail gives your agent a real email address.'));
52
+ console.log(theme.dim(' Send, receive, reply — fully programmatic.'));
53
+ console.log('');
54
+ console.log(theme.gold(' What you need:'));
55
+ console.log(theme.dim(' 1. Create a free account at console.agentmail.to'));
56
+ console.log(theme.dim(' 2. Generate an API key (starts with am_)'));
57
+ console.log(theme.dim(' 3. Paste it here'));
58
+ console.log('');
59
+
60
+ const { action } = await inquirer.prompt([{
61
+ type: 'list',
62
+ name: 'action',
63
+ message: theme.gold('How to proceed?'),
64
+ choices: [
65
+ { name: '🌐 Open AgentMail Console in browser', value: 'open' },
66
+ { name: '🔑 I have an API key — enter it now', value: 'key' },
67
+ { name: '❌ Skip for now', value: 'skip' },
68
+ ],
69
+ }]);
70
+
71
+ if (action === 'skip') {
72
+ info('Run later: darksol mail setup');
73
+ return null;
74
+ }
75
+
76
+ if (action === 'open') {
77
+ try {
78
+ await open(CONSOLE_URL);
79
+ success('Opened AgentMail Console in your browser');
80
+ } catch {
81
+ info(`Go to: ${CONSOLE_URL}`);
82
+ }
83
+ console.log('');
84
+ info('Create an account, then generate an API key.');
85
+ info('Come back and run: darksol mail setup');
86
+ console.log('');
87
+
88
+ const { hasKey: gotKey } = await inquirer.prompt([{
89
+ type: 'confirm',
90
+ name: 'hasKey',
91
+ message: theme.gold('Do you have your API key now?'),
92
+ default: false,
93
+ }]);
94
+
95
+ if (!gotKey) {
96
+ info('No problem. Run: darksol mail setup');
97
+ return null;
98
+ }
99
+ }
100
+
101
+ // Enter API key
102
+ const { apiKey } = await inquirer.prompt([{
103
+ type: 'password',
104
+ name: 'apiKey',
105
+ message: theme.gold('AgentMail API key:'),
106
+ mask: '●',
107
+ validate: (v) => {
108
+ if (!v || v.length < 5) return 'Key too short';
109
+ if (!v.startsWith('am_')) return 'AgentMail keys start with am_';
110
+ return true;
111
+ },
112
+ }]);
113
+
114
+ // Store encrypted
115
+ addKeyDirect('agentmail', apiKey);
116
+ success('AgentMail API key stored (encrypted)');
117
+
118
+ // Verify connection
119
+ const spin = spinner('Verifying connection...').start();
120
+ try {
121
+ const client = new AgentMailClient({ apiKey });
122
+ const inboxes = await client.inboxes.list();
123
+ spin.succeed(`Connected — ${inboxes.inboxes?.length || 0} existing inbox(es)`);
124
+ return client;
125
+ } catch (err) {
126
+ spin.fail('Connection failed');
127
+ error(err.message);
128
+ info('Check your API key and try again: darksol mail setup');
129
+ return null;
130
+ }
131
+ }
132
+
133
+ // ══════════════════════════════════════════════════
134
+ // COMMANDS
135
+ // ══════════════════════════════════════════════════
136
+
137
+ /**
138
+ * Run setup flow
139
+ */
140
+ export async function mailSetup() {
141
+ await ensureSetup();
142
+ }
143
+
144
+ /**
145
+ * Create a new inbox
146
+ */
147
+ export async function mailCreate(opts = {}) {
148
+ const client = await ensureSetup();
149
+ if (!client) return;
150
+
151
+ let username = opts.username;
152
+ let displayName = opts.displayName;
153
+
154
+ if (!username) {
155
+ const answers = await inquirer.prompt([
156
+ {
157
+ type: 'input',
158
+ name: 'username',
159
+ message: theme.gold('Inbox username (optional, leave blank for auto):'),
160
+ },
161
+ {
162
+ type: 'input',
163
+ name: 'displayName',
164
+ message: theme.gold('Display name:'),
165
+ default: 'DARKSOL Agent',
166
+ },
167
+ ]);
168
+ username = answers.username || undefined;
169
+ displayName = answers.displayName;
170
+ }
171
+
172
+ const spin = spinner('Creating inbox...').start();
173
+
174
+ try {
175
+ const inbox = await client.inboxes.create({
176
+ username: username || undefined,
177
+ displayName: displayName || 'DARKSOL Agent',
178
+ clientId: `darksol-${Date.now()}`,
179
+ });
180
+
181
+ spin.succeed('Inbox created');
182
+
183
+ console.log('');
184
+ showSection('📧 NEW INBOX');
185
+ kvDisplay([
186
+ ['Inbox ID', inbox.inboxId],
187
+ ['Email', inbox.email],
188
+ ['Display Name', inbox.displayName || '-'],
189
+ ['Created', new Date().toLocaleString()],
190
+ ]);
191
+
192
+ // Store as active inbox
193
+ setConfig('mailInboxId', inbox.inboxId);
194
+ setConfig('mailEmail', inbox.email);
195
+ console.log('');
196
+ success('Set as active inbox');
197
+ info(`Your agent can now send and receive email at: ${theme.gold(inbox.email)}`);
198
+ console.log('');
199
+
200
+ return inbox;
201
+ } catch (err) {
202
+ spin.fail('Failed to create inbox');
203
+ error(err.message);
204
+ }
205
+ }
206
+
207
+ /**
208
+ * List all inboxes
209
+ */
210
+ export async function mailInboxes() {
211
+ const client = await ensureSetup();
212
+ if (!client) return;
213
+
214
+ const spin = spinner('Fetching inboxes...').start();
215
+
216
+ try {
217
+ const result = await client.inboxes.list();
218
+ const inboxes = result.inboxes || [];
219
+
220
+ if (inboxes.length === 0) {
221
+ spin.succeed('No inboxes found');
222
+ info('Create one: darksol mail create');
223
+ return;
224
+ }
225
+
226
+ spin.succeed(`${inboxes.length} inbox(es)`);
227
+
228
+ const activeId = getConfig('mailInboxId');
229
+
230
+ console.log('');
231
+ showSection('📧 INBOXES');
232
+
233
+ const rows = inboxes.map(i => [
234
+ i.inboxId === activeId ? theme.gold('► ' + (i.displayName || 'Unnamed')) : ' ' + (i.displayName || 'Unnamed'),
235
+ i.email || '-',
236
+ i.inboxId.slice(0, 12) + '...',
237
+ ]);
238
+
239
+ table(['Name', 'Email', 'ID'], rows);
240
+ console.log('');
241
+ } catch (err) {
242
+ spin.fail('Failed to list inboxes');
243
+ error(err.message);
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Send an email
249
+ */
250
+ export async function mailSend(opts = {}) {
251
+ const client = await ensureSetup();
252
+ if (!client) return;
253
+
254
+ const inboxId = opts.inbox || getConfig('mailInboxId');
255
+ if (!inboxId) {
256
+ error('No active inbox. Create one: darksol mail create');
257
+ return;
258
+ }
259
+
260
+ let to = opts.to;
261
+ let subject = opts.subject;
262
+ let text = opts.text;
263
+
264
+ if (!to || !subject) {
265
+ const answers = await inquirer.prompt([
266
+ {
267
+ type: 'input',
268
+ name: 'to',
269
+ message: theme.gold('To:'),
270
+ default: to,
271
+ validate: (v) => v.includes('@') || 'Enter a valid email',
272
+ },
273
+ {
274
+ type: 'input',
275
+ name: 'subject',
276
+ message: theme.gold('Subject:'),
277
+ default: subject,
278
+ validate: (v) => v.length > 0 || 'Subject required',
279
+ },
280
+ {
281
+ type: 'editor',
282
+ name: 'text',
283
+ message: theme.gold('Message body:'),
284
+ default: text || '',
285
+ },
286
+ ]);
287
+ to = answers.to;
288
+ subject = answers.subject;
289
+ text = answers.text;
290
+ }
291
+
292
+ const spin = spinner('Sending...').start();
293
+
294
+ try {
295
+ await client.inboxes.messages.send(inboxId, {
296
+ to,
297
+ subject,
298
+ text,
299
+ });
300
+
301
+ spin.succeed('Email sent');
302
+ console.log('');
303
+ kvDisplay([
304
+ ['From', getConfig('mailEmail') || inboxId],
305
+ ['To', to],
306
+ ['Subject', subject],
307
+ ]);
308
+ console.log('');
309
+ } catch (err) {
310
+ spin.fail('Failed to send');
311
+ error(err.message);
312
+ }
313
+ }
314
+
315
+ /**
316
+ * List received messages
317
+ */
318
+ export async function mailList(opts = {}) {
319
+ const client = await ensureSetup();
320
+ if (!client) return;
321
+
322
+ const inboxId = opts.inbox || getConfig('mailInboxId');
323
+ if (!inboxId) {
324
+ error('No active inbox. Create one: darksol mail create');
325
+ return;
326
+ }
327
+
328
+ const limit = parseInt(opts.limit || '10');
329
+ const spin = spinner('Fetching messages...').start();
330
+
331
+ try {
332
+ const result = await client.inboxes.messages.list(inboxId, { limit });
333
+ const messages = result.messages || [];
334
+
335
+ if (messages.length === 0) {
336
+ spin.succeed('No messages');
337
+ info(`Inbox: ${getConfig('mailEmail') || inboxId}`);
338
+ return;
339
+ }
340
+
341
+ spin.succeed(`${messages.length} message(s)`);
342
+
343
+ console.log('');
344
+ showSection(`📧 INBOX — ${getConfig('mailEmail') || 'messages'}`);
345
+
346
+ const rows = messages.map((m, i) => {
347
+ const from = m.from?.address || m.from || '?';
348
+ const shortFrom = from.length > 25 ? from.slice(0, 22) + '...' : from;
349
+ const subject = (m.subject || '(no subject)').slice(0, 35);
350
+ const date = m.createdAt ? new Date(m.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '';
351
+ const read = m.labels?.includes('READ') ? theme.dim(' ') : theme.success('● ');
352
+
353
+ return [
354
+ `${read}${i + 1}`,
355
+ shortFrom,
356
+ subject,
357
+ date,
358
+ ];
359
+ });
360
+
361
+ table(['#', 'From', 'Subject', 'Date'], rows);
362
+ console.log('');
363
+ info('Read a message: darksol mail read <number>');
364
+ console.log('');
365
+
366
+ // Store message IDs for quick reference
367
+ setConfig('mailMessageIds', messages.map(m => m.messageId));
368
+ } catch (err) {
369
+ spin.fail('Failed to fetch messages');
370
+ error(err.message);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Read a specific message
376
+ */
377
+ export async function mailRead(messageRef, opts = {}) {
378
+ const client = await ensureSetup();
379
+ if (!client) return;
380
+
381
+ const inboxId = opts.inbox || getConfig('mailInboxId');
382
+ if (!inboxId) {
383
+ error('No active inbox. Create one: darksol mail create');
384
+ return;
385
+ }
386
+
387
+ // Resolve message ID — could be a number (index) or actual ID
388
+ let messageId = messageRef;
389
+ const num = parseInt(messageRef);
390
+ if (!isNaN(num)) {
391
+ const storedIds = getConfig('mailMessageIds') || [];
392
+ if (num > 0 && num <= storedIds.length) {
393
+ messageId = storedIds[num - 1];
394
+ }
395
+ }
396
+
397
+ const spin = spinner('Fetching message...').start();
398
+
399
+ try {
400
+ const msg = await client.inboxes.messages.get(inboxId, messageId);
401
+
402
+ spin.succeed('Message loaded');
403
+
404
+ console.log('');
405
+ showSection('📧 MESSAGE');
406
+ kvDisplay([
407
+ ['From', msg.from?.address || msg.from || '?'],
408
+ ['To', msg.to?.map(t => t.address || t).join(', ') || '?'],
409
+ ['Subject', msg.subject || '(no subject)'],
410
+ ['Date', msg.createdAt ? new Date(msg.createdAt).toLocaleString() : '?'],
411
+ ['ID', msg.messageId],
412
+ ]);
413
+
414
+ console.log('');
415
+ showDivider();
416
+
417
+ // Show message body
418
+ const body = msg.extractedText || msg.text || msg.extractedHtml || msg.html || '(empty)';
419
+ const lines = body.split('\n');
420
+ for (const line of lines) {
421
+ console.log(' ' + line);
422
+ }
423
+
424
+ console.log('');
425
+ showDivider();
426
+ console.log('');
427
+ info(`Reply: darksol mail reply ${messageRef}`);
428
+ info(`Forward: darksol mail forward ${messageRef}`);
429
+ console.log('');
430
+
431
+ return msg;
432
+ } catch (err) {
433
+ spin.fail('Failed to read message');
434
+ error(err.message);
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Reply to a message
440
+ */
441
+ export async function mailReply(messageRef, opts = {}) {
442
+ const client = await ensureSetup();
443
+ if (!client) return;
444
+
445
+ const inboxId = opts.inbox || getConfig('mailInboxId');
446
+ if (!inboxId) {
447
+ error('No active inbox');
448
+ return;
449
+ }
450
+
451
+ // Resolve message ID
452
+ let messageId = messageRef;
453
+ const num = parseInt(messageRef);
454
+ if (!isNaN(num)) {
455
+ const storedIds = getConfig('mailMessageIds') || [];
456
+ if (num > 0 && num <= storedIds.length) {
457
+ messageId = storedIds[num - 1];
458
+ }
459
+ }
460
+
461
+ let text = opts.text;
462
+ if (!text) {
463
+ const { body } = await inquirer.prompt([{
464
+ type: 'editor',
465
+ name: 'body',
466
+ message: theme.gold('Reply message:'),
467
+ }]);
468
+ text = body;
469
+ }
470
+
471
+ const spin = spinner('Sending reply...').start();
472
+
473
+ try {
474
+ await client.inboxes.messages.reply(inboxId, messageId, { text });
475
+ spin.succeed('Reply sent');
476
+ } catch (err) {
477
+ spin.fail('Reply failed');
478
+ error(err.message);
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Forward a message
484
+ */
485
+ export async function mailForward(messageRef, opts = {}) {
486
+ const client = await ensureSetup();
487
+ if (!client) return;
488
+
489
+ const inboxId = opts.inbox || getConfig('mailInboxId');
490
+ if (!inboxId) {
491
+ error('No active inbox');
492
+ return;
493
+ }
494
+
495
+ let messageId = messageRef;
496
+ const num = parseInt(messageRef);
497
+ if (!isNaN(num)) {
498
+ const storedIds = getConfig('mailMessageIds') || [];
499
+ if (num > 0 && num <= storedIds.length) {
500
+ messageId = storedIds[num - 1];
501
+ }
502
+ }
503
+
504
+ let to = opts.to;
505
+ if (!to) {
506
+ const { forwardTo } = await inquirer.prompt([{
507
+ type: 'input',
508
+ name: 'forwardTo',
509
+ message: theme.gold('Forward to:'),
510
+ validate: (v) => v.includes('@') || 'Enter a valid email',
511
+ }]);
512
+ to = forwardTo;
513
+ }
514
+
515
+ const spin = spinner('Forwarding...').start();
516
+
517
+ try {
518
+ await client.inboxes.messages.forward(inboxId, messageId, { to });
519
+ spin.succeed(`Forwarded to ${to}`);
520
+ } catch (err) {
521
+ spin.fail('Forward failed');
522
+ error(err.message);
523
+ }
524
+ }
525
+
526
+ /**
527
+ * List threads
528
+ */
529
+ export async function mailThreads(opts = {}) {
530
+ const client = await ensureSetup();
531
+ if (!client) return;
532
+
533
+ const inboxId = opts.inbox || getConfig('mailInboxId');
534
+ if (!inboxId) {
535
+ error('No active inbox');
536
+ return;
537
+ }
538
+
539
+ const spin = spinner('Fetching threads...').start();
540
+
541
+ try {
542
+ const result = await client.inboxes.threads.list(inboxId);
543
+ const threads = result.threads || [];
544
+
545
+ if (threads.length === 0) {
546
+ spin.succeed('No threads');
547
+ return;
548
+ }
549
+
550
+ spin.succeed(`${threads.length} thread(s)`);
551
+
552
+ console.log('');
553
+ showSection('📧 THREADS');
554
+
555
+ const rows = threads.map(t => {
556
+ const subject = (t.subject || '(no subject)').slice(0, 40);
557
+ const count = t.messageCount || '?';
558
+ const date = t.latestMessageAt ? new Date(t.latestMessageAt).toLocaleDateString() : '';
559
+
560
+ return [
561
+ subject,
562
+ `${count} msgs`,
563
+ date,
564
+ (t.threadId || '').slice(0, 12) + '...',
565
+ ];
566
+ });
567
+
568
+ table(['Subject', 'Messages', 'Latest', 'Thread ID'], rows);
569
+ console.log('');
570
+ } catch (err) {
571
+ spin.fail('Failed to list threads');
572
+ error(err.message);
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Delete an inbox
578
+ */
579
+ export async function mailDelete(inboxId) {
580
+ const client = await ensureSetup();
581
+ if (!client) return;
582
+
583
+ inboxId = inboxId || getConfig('mailInboxId');
584
+ if (!inboxId) {
585
+ error('No inbox to delete. Specify an inbox ID.');
586
+ return;
587
+ }
588
+
589
+ const { confirm } = await inquirer.prompt([{
590
+ type: 'confirm',
591
+ name: 'confirm',
592
+ message: theme.accent(`Delete inbox ${inboxId}? This cannot be undone.`),
593
+ default: false,
594
+ }]);
595
+
596
+ if (!confirm) return;
597
+
598
+ const spin = spinner('Deleting inbox...').start();
599
+
600
+ try {
601
+ await client.inboxes.delete(inboxId);
602
+ spin.succeed('Inbox deleted');
603
+
604
+ if (getConfig('mailInboxId') === inboxId) {
605
+ setConfig('mailInboxId', null);
606
+ setConfig('mailEmail', null);
607
+ }
608
+ } catch (err) {
609
+ spin.fail('Delete failed');
610
+ error(err.message);
611
+ }
612
+ }
613
+
614
+ /**
615
+ * Use a specific inbox as active
616
+ */
617
+ export async function mailUse(inboxId) {
618
+ const client = await ensureSetup();
619
+ if (!client) return;
620
+
621
+ const spin = spinner('Fetching inbox...').start();
622
+
623
+ try {
624
+ const inbox = await client.inboxes.get(inboxId);
625
+ spin.succeed('Inbox found');
626
+
627
+ setConfig('mailInboxId', inbox.inboxId);
628
+ setConfig('mailEmail', inbox.email);
629
+
630
+ kvDisplay([
631
+ ['Active Inbox', inbox.email],
632
+ ['ID', inbox.inboxId],
633
+ ['Display Name', inbox.displayName || '-'],
634
+ ]);
635
+ console.log('');
636
+ } catch (err) {
637
+ spin.fail('Inbox not found');
638
+ error(err.message);
639
+ }
640
+ }
641
+
642
+ /**
643
+ * Show inbox metrics/stats
644
+ */
645
+ export async function mailStats(opts = {}) {
646
+ const client = await ensureSetup();
647
+ if (!client) return;
648
+
649
+ const inboxId = opts.inbox || getConfig('mailInboxId');
650
+ if (!inboxId) {
651
+ error('No active inbox');
652
+ return;
653
+ }
654
+
655
+ const spin = spinner('Fetching stats...').start();
656
+
657
+ try {
658
+ const metrics = await client.inboxes.metrics.get(inboxId);
659
+ spin.succeed('Stats loaded');
660
+
661
+ console.log('');
662
+ showSection('📧 INBOX STATS');
663
+ kvDisplay([
664
+ ['Email', getConfig('mailEmail') || inboxId],
665
+ ['Total Sent', metrics.totalSent || 0],
666
+ ['Total Received', metrics.totalReceived || 0],
667
+ ['Total Threads', metrics.totalThreads || 0],
668
+ ]);
669
+ console.log('');
670
+ } catch (err) {
671
+ spin.fail('Failed to fetch stats');
672
+ // Metrics endpoint might not exist on all plans
673
+ info('Stats may not be available for your plan');
674
+ }
675
+ }
676
+
677
+ /**
678
+ * Show mail status and help
679
+ */
680
+ export async function mailStatus() {
681
+ const hasApiKey = hasKey('agentmail') || !!process.env.AGENTMAIL_API_KEY;
682
+ const activeInbox = getConfig('mailInboxId');
683
+ const activeEmail = getConfig('mailEmail');
684
+
685
+ showSection('📧 AGENTMAIL STATUS');
686
+ console.log('');
687
+ kvDisplay([
688
+ ['API Key', hasApiKey ? theme.success('● Connected') : theme.dim('○ Not configured')],
689
+ ['Active Inbox', activeEmail || theme.dim('(none)')],
690
+ ['Inbox ID', activeInbox ? activeInbox.slice(0, 16) + '...' : theme.dim('—')],
691
+ ['Console', CONSOLE_URL],
692
+ ['Docs', DOCS_URL],
693
+ ]);
694
+
695
+ if (!hasApiKey) {
696
+ console.log('');
697
+ info('Get started: darksol mail setup');
698
+ info('Or go to: console.agentmail.to');
699
+ } else if (!activeInbox) {
700
+ console.log('');
701
+ info('Create an inbox: darksol mail create');
702
+ }
703
+
704
+ console.log('');
705
+ }