atris 3.55.0 → 3.56.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/commands/integrations.js +166 -0
- package/commands/task.js +18 -1
- package/package.json +1 -1
package/commands/integrations.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* atris gmail inbox [--account <id>] - List recent emails for a mailbox
|
|
6
6
|
* atris gmail read <id> [--account <id>] - Read specific email
|
|
7
7
|
* atris gmail archive <id> [...] [--account <id>] - Archive messages
|
|
8
|
+
* atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>] - Send an email
|
|
9
|
+
* atris gmail voice [account] [--clear] - Edit an account's writing voice
|
|
8
10
|
* atris gmail connect [name] - Connect or reconnect a Gmail account
|
|
9
11
|
* atris gmail accounts - List connected Gmail accounts and the active account
|
|
10
12
|
* atris gmail use [<account_id|name>] - Choose or set the active Gmail account
|
|
@@ -35,6 +37,8 @@ const { spawnSync } = require('child_process');
|
|
|
35
37
|
const CALENDAR_CACHE_PATH = path.join(os.homedir(), '.atris', 'calendar-events-cache.json');
|
|
36
38
|
const GMAIL_CONNECT_POLL_MS = 3000;
|
|
37
39
|
const GMAIL_CONNECT_TIMEOUT_MS = 3 * 60 * 1000;
|
|
40
|
+
const GMAIL_SEND_USAGE = 'usage: atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>]';
|
|
41
|
+
const GMAIL_VOICE_USAGE = 'usage: atris gmail voice [account] [--clear]';
|
|
38
42
|
|
|
39
43
|
function gmailAccountStatePath() {
|
|
40
44
|
return process.env.ATRIS_GMAIL_ACCOUNT_FILE
|
|
@@ -311,6 +315,155 @@ async function gmailArchive(messageIds, options = {}) {
|
|
|
311
315
|
console.log(`Archived ${archived} message${archived === 1 ? '' : 's'}. They stay searchable in All Mail.`);
|
|
312
316
|
}
|
|
313
317
|
|
|
318
|
+
function parseGmailSendArgs(args = []) {
|
|
319
|
+
let accountId = null;
|
|
320
|
+
let bodyFile = null;
|
|
321
|
+
const positional = [];
|
|
322
|
+
|
|
323
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
324
|
+
const arg = args[i];
|
|
325
|
+
if (arg === '--account' || arg === '--body-file') {
|
|
326
|
+
const value = String(args[i + 1] || '').trim();
|
|
327
|
+
if (!value || value.startsWith('--')) {
|
|
328
|
+
console.error(GMAIL_SEND_USAGE);
|
|
329
|
+
process.exit(2);
|
|
330
|
+
}
|
|
331
|
+
if (arg === '--account') accountId = value;
|
|
332
|
+
else bodyFile = value;
|
|
333
|
+
i += 1;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
positional.push(arg);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const [to, subject, ...bodyParts] = positional;
|
|
340
|
+
if (!String(to || '').trim()
|
|
341
|
+
|| !String(subject || '').trim()
|
|
342
|
+
|| (bodyFile ? bodyParts.length > 0 : bodyParts.length === 0)) {
|
|
343
|
+
console.error(GMAIL_SEND_USAGE);
|
|
344
|
+
process.exit(2);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let body = bodyParts.join(' ');
|
|
348
|
+
if (bodyFile) {
|
|
349
|
+
try {
|
|
350
|
+
body = fs.readFileSync(bodyFile, 'utf8');
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.error(`could not read body file "${bodyFile}": ${error.message}`);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return { to, subject, body, accountId };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function gmailSend(to, subject, body, options = {}) {
|
|
361
|
+
const token = await getAuthToken();
|
|
362
|
+
const accountId = resolveGmailAccountId(options.accountId);
|
|
363
|
+
const accounts = await fetchGmailAccounts(token);
|
|
364
|
+
const account = findGmailAccount(accounts, accountId);
|
|
365
|
+
|
|
366
|
+
if (!account) {
|
|
367
|
+
console.error(`gmail account "${accountId}" is not connected.`);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const result = await apiRequestJson('/integrations/gmail/send', {
|
|
372
|
+
method: 'POST',
|
|
373
|
+
token,
|
|
374
|
+
body: { to, subject, body, account_id: accountId },
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (!result.ok) {
|
|
378
|
+
console.error(`could not send gmail message: ${result.error || 'request failed'}`);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const { email, id } = gmailAccountIdentity(account);
|
|
383
|
+
console.log(`sent to ${to} from ${email || id}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function parseGmailVoiceArgs(args = []) {
|
|
387
|
+
let clear = false;
|
|
388
|
+
const positional = [];
|
|
389
|
+
for (const arg of args) {
|
|
390
|
+
if (arg === '--clear') clear = true;
|
|
391
|
+
else positional.push(arg);
|
|
392
|
+
}
|
|
393
|
+
if (positional.length > 1 || positional.some((arg) => String(arg).startsWith('--'))) {
|
|
394
|
+
console.error(GMAIL_VOICE_USAGE);
|
|
395
|
+
process.exit(2);
|
|
396
|
+
}
|
|
397
|
+
return { accountId: positional[0] || null, clear };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function editGmailVoice(currentVoice, deps = {}) {
|
|
401
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-gmail-voice-'));
|
|
402
|
+
const voicePath = path.join(tempDir, 'voice.md');
|
|
403
|
+
let editedVoice;
|
|
404
|
+
let editorError = null;
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
fs.writeFileSync(voicePath, currentVoice, 'utf8');
|
|
408
|
+
const editor = deps.editor || process.env.EDITOR || 'vi';
|
|
409
|
+
const shell = deps.shell || process.env.SHELL || '/bin/sh';
|
|
410
|
+
const run = deps.spawnSync || spawnSync;
|
|
411
|
+
const result = run(shell, ['-c', 'exec $EDITOR "$1"', 'atris-gmail-voice', voicePath], {
|
|
412
|
+
stdio: 'inherit',
|
|
413
|
+
env: { ...process.env, EDITOR: editor },
|
|
414
|
+
});
|
|
415
|
+
if (result.error || result.status !== 0) {
|
|
416
|
+
editorError = result.error?.message || `editor exited with status ${result.status}`;
|
|
417
|
+
} else {
|
|
418
|
+
editedVoice = fs.readFileSync(voicePath, 'utf8');
|
|
419
|
+
}
|
|
420
|
+
} finally {
|
|
421
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (editorError) {
|
|
425
|
+
console.error(`could not edit gmail voice: ${editorError}`);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
return editedVoice;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function gmailVoice(accountId, options = {}) {
|
|
432
|
+
const token = await getAuthToken();
|
|
433
|
+
const resolvedAccountId = resolveGmailAccountId(accountId);
|
|
434
|
+
const accounts = await fetchGmailAccounts(token);
|
|
435
|
+
const account = findGmailAccount(accounts, resolvedAccountId);
|
|
436
|
+
|
|
437
|
+
if (!account) {
|
|
438
|
+
console.error(`gmail account "${resolvedAccountId}" is not connected.`);
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let voiceMd = '';
|
|
443
|
+
if (!options.clear) {
|
|
444
|
+
const stdinIsTTY = options.stdinIsTTY === undefined
|
|
445
|
+
? Boolean(process.stdin.isTTY)
|
|
446
|
+
: options.stdinIsTTY;
|
|
447
|
+
voiceMd = stdinIsTTY
|
|
448
|
+
? editGmailVoice(String(account.voice_md || ''), options)
|
|
449
|
+
: String((options.readStdin || (() => fs.readFileSync(0, 'utf8')))());
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const { id, name } = gmailAccountIdentity(account);
|
|
453
|
+
const result = await apiRequestJson(`/integrations/gmail/accounts/${encodeURIComponent(id)}`, {
|
|
454
|
+
method: 'PATCH',
|
|
455
|
+
token,
|
|
456
|
+
body: { voice_md: voiceMd },
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (!result.ok) {
|
|
460
|
+
console.error(`could not ${options.clear ? 'clear' : 'save'} gmail voice: ${result.error || 'request failed'}`);
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
console.log(`voice ${options.clear ? 'cleared' : 'saved'} for ${name}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
314
467
|
function openGmailAuthUrl(authUrl, deps = {}) {
|
|
315
468
|
const run = deps.spawnSync || spawnSync;
|
|
316
469
|
const platform = deps.platform || process.platform;
|
|
@@ -457,6 +610,16 @@ async function gmailCommand(subcommand, ...args) {
|
|
|
457
610
|
await gmailArchive(parsed.positional, { accountId: parsed.accountId || undefined });
|
|
458
611
|
break;
|
|
459
612
|
}
|
|
613
|
+
case 'send': {
|
|
614
|
+
const parsed = parseGmailSendArgs(args);
|
|
615
|
+
await gmailSend(parsed.to, parsed.subject, parsed.body, { accountId: parsed.accountId || undefined });
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
case 'voice': {
|
|
619
|
+
const parsed = parseGmailVoiceArgs(args);
|
|
620
|
+
await gmailVoice(parsed.accountId, { clear: parsed.clear });
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
460
623
|
case 'connect':
|
|
461
624
|
await gmailConnect(args[0]);
|
|
462
625
|
break;
|
|
@@ -471,6 +634,8 @@ async function gmailCommand(subcommand, ...args) {
|
|
|
471
634
|
console.log(' atris gmail inbox [--account <id>] - list recent emails for a mailbox');
|
|
472
635
|
console.log(' atris gmail read <id> [--account <id>] - read specific email');
|
|
473
636
|
console.log(' atris gmail archive <id> [...] [--account <id>] - archive messages (reversible, all mail keeps them)');
|
|
637
|
+
console.log(' atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>] - send an email');
|
|
638
|
+
console.log(' atris gmail voice [account] [--clear] - edit or clear an account writing voice');
|
|
474
639
|
console.log(' atris gmail connect [name] - connect or reconnect a gmail account');
|
|
475
640
|
console.log(' atris gmail accounts - list connected gmail accounts and the active account');
|
|
476
641
|
console.log(' atris gmail use [<account_id|name>] - choose or set the active gmail account');
|
|
@@ -1983,6 +2148,7 @@ async function integrationsStatus(args = []) {
|
|
|
1983
2148
|
module.exports = {
|
|
1984
2149
|
gmailCommand,
|
|
1985
2150
|
gmailConnect,
|
|
2151
|
+
gmailVoice,
|
|
1986
2152
|
gmailUse,
|
|
1987
2153
|
extractGmailMailboxAccount,
|
|
1988
2154
|
parseGmailArgs,
|
package/commands/task.js
CHANGED
|
@@ -60,7 +60,7 @@ const {
|
|
|
60
60
|
decisionMarkerFor,
|
|
61
61
|
DECISION_REFUSE_REASON,
|
|
62
62
|
} = require('../lib/task-decision');
|
|
63
|
-
const { buildFirstMinute, deskNextCommand, personName, pickNext, taskCommand, taskNextCommand } = require('../lib/first-minute');
|
|
63
|
+
const { buildFirstMinute, deskNextCommand, personName, pickNext, speakFirstMinute, taskCommand, taskNextCommand } = require('../lib/first-minute');
|
|
64
64
|
|
|
65
65
|
const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
|
|
66
66
|
|| process.env.USER
|
|
@@ -6166,6 +6166,23 @@ function renderTaskDesk(rows, refRows = rows) {
|
|
|
6166
6166
|
}
|
|
6167
6167
|
|
|
6168
6168
|
function cmdAdd(args) {
|
|
6169
|
+
const root = process.cwd();
|
|
6170
|
+
// Empty folder talks like bare atris. A leftover title is not a
|
|
6171
|
+
// task desk. After init, a title still files.
|
|
6172
|
+
if (isUninitializedTaskFolder(root)) {
|
|
6173
|
+
if (wantsJson(args)) {
|
|
6174
|
+
printJson({
|
|
6175
|
+
ok: true,
|
|
6176
|
+
action: 'init',
|
|
6177
|
+
command: 'atris init --minimal',
|
|
6178
|
+
task_id: null,
|
|
6179
|
+
projection_path: null,
|
|
6180
|
+
task: null,
|
|
6181
|
+
});
|
|
6182
|
+
return;
|
|
6183
|
+
}
|
|
6184
|
+
return speakFirstMinute({ root, fresh: true });
|
|
6185
|
+
}
|
|
6169
6186
|
const pos = positional(args);
|
|
6170
6187
|
const title = pos.join(' ').trim();
|
|
6171
6188
|
if (!title) {
|