@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.
- package/package.json +2 -1
- package/src/cli.js +142 -0
- package/src/config/keys.js +10 -2
- package/src/services/gas.js +116 -0
- package/src/services/mail.js +705 -0
- package/src/services/watch.js +159 -0
- package/src/ui/banner.js +4 -2
- package/src/wallet/history.js +96 -0
- package/src/wallet/portfolio.js +169 -0
|
@@ -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
|
+
}
|