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.
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.55.0",
3
+ "version": "3.56.0",
4
4
  "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {