atris 3.31.0 → 3.33.3

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.
@@ -124,6 +124,32 @@ async function gmailRead(messageId) {
124
124
  console.log(msg.body || msg.snippet || '(no body)');
125
125
  }
126
126
 
127
+ async function gmailArchive(messageIds) {
128
+ const ids = (messageIds || []).map(id => String(id || '').trim()).filter(Boolean);
129
+ if (!ids.length) {
130
+ console.error('Usage: atris gmail archive <message_id> [<message_id> ...]');
131
+ process.exit(1);
132
+ }
133
+
134
+ const token = await getAuthToken();
135
+
136
+ console.log(`Archiving ${ids.length} message${ids.length === 1 ? '' : 's'}...`);
137
+
138
+ const result = await apiRequestJson('/integrations/gmail/messages/batch-archive', {
139
+ method: 'POST',
140
+ token,
141
+ body: { message_ids: ids },
142
+ });
143
+
144
+ if (!result.ok) {
145
+ console.error(`Error: ${result.error || 'Failed to archive'}`);
146
+ process.exit(1);
147
+ }
148
+
149
+ const archived = result.data?.archived ?? result.data?.count ?? ids.length;
150
+ console.log(`Archived ${archived} message${archived === 1 ? '' : 's'}. They stay searchable in All Mail.`);
151
+ }
152
+
127
153
  async function gmailCommand(subcommand, ...args) {
128
154
  switch (subcommand) {
129
155
  case 'inbox':
@@ -133,10 +159,14 @@ async function gmailCommand(subcommand, ...args) {
133
159
  case 'read':
134
160
  await gmailRead(args[0]);
135
161
  break;
162
+ case 'archive':
163
+ await gmailArchive(args);
164
+ break;
136
165
  default:
137
166
  console.log('Gmail commands:');
138
167
  console.log(' atris gmail inbox - List recent emails');
139
168
  console.log(' atris gmail read <id> - Read specific email');
169
+ console.log(' atris gmail archive <id> [...] - Archive messages (reversible, All Mail keeps them)');
140
170
  }
141
171
  }
142
172
 
@@ -474,6 +504,58 @@ async function calendarDate(dateText) {
474
504
  await calendarRange(`Events on ${dateText}`, { timeMin: start.toISOString(), timeMax: end.toISOString() });
475
505
  }
476
506
 
507
+ async function calendarAdd(args) {
508
+ // atris calendar add "Title" --date YYYY-MM-DD --from HH:MM --to HH:MM
509
+ // or: --start <ISO> --end <ISO>; optional --location, --description
510
+ const rest = [...(args || [])];
511
+ const flags = {};
512
+ const positional = [];
513
+ for (let i = 0; i < rest.length; i++) {
514
+ const arg = String(rest[i] || '');
515
+ if (arg.startsWith('--')) {
516
+ flags[arg.slice(2)] = String(rest[i + 1] || '');
517
+ i += 1;
518
+ } else {
519
+ positional.push(arg);
520
+ }
521
+ }
522
+ const summary = positional.join(' ').trim();
523
+ let start = String(flags.start || '').trim();
524
+ let end = String(flags.end || '').trim();
525
+ if (!start && flags.date && flags.from) start = `${flags.date}T${flags.from}:00`;
526
+ if (!end && flags.date && flags.to) end = `${flags.date}T${flags.to}:00`;
527
+ if (!summary || !start || !end) {
528
+ console.error('Usage: atris calendar add "Title" --date YYYY-MM-DD --from HH:MM --to HH:MM');
529
+ console.error(' or: atris calendar add "Title" --start <ISO datetime> --end <ISO datetime>');
530
+ process.exit(1);
531
+ }
532
+
533
+ const token = await getAuthToken();
534
+
535
+ console.log(`Creating event: ${summary} (${start} to ${end})...`);
536
+
537
+ const body = { summary, start, end };
538
+ if (flags.location) body.location = flags.location;
539
+ if (flags.description) body.description = flags.description;
540
+ if (flags.timezone) body.timezone = flags.timezone;
541
+
542
+ const result = await apiRequestJson('/integrations/google-calendar/events', {
543
+ method: 'POST',
544
+ token,
545
+ body,
546
+ });
547
+
548
+ if (!result.ok) {
549
+ console.error(`Error: ${result.error || 'Failed to create event'}`);
550
+ process.exit(1);
551
+ }
552
+
553
+ const created = result.data || {};
554
+ console.log(`Created: ${created.summary || summary}`);
555
+ if (created.htmlLink) console.log(`Link: ${created.htmlLink}`);
556
+ console.log(`Verified on calendar: ${created.verified ? 'yes' : 'no'}`);
557
+ }
558
+
477
559
  async function calendarCommand(subcommand, ...args) {
478
560
  switch (subcommand) {
479
561
  case 'today':
@@ -492,6 +574,10 @@ async function calendarCommand(subcommand, ...args) {
492
574
  case 'search':
493
575
  await calendarSearch(args);
494
576
  break;
577
+ case 'add':
578
+ case 'create':
579
+ await calendarAdd(args);
580
+ break;
495
581
  default:
496
582
  console.log('Calendar commands:');
497
583
  console.log(' atris calendar today - Show today\'s events');
@@ -499,6 +585,7 @@ async function calendarCommand(subcommand, ...args) {
499
585
  console.log(' atris calendar week - Show this week\'s events');
500
586
  console.log(' atris calendar date YYYY-MM-DD - Show events on a date');
501
587
  console.log(' atris calendar search <query> [--days 30] [--limit 20] [--timeout-ms 10000] [--json] - Search upcoming events');
588
+ console.log(' atris calendar add \"Title\" --date YYYY-MM-DD --from HH:MM --to HH:MM - Create an event');
502
589
  }
503
590
  }
504
591
 
@@ -737,6 +824,58 @@ async function slackSearch(query, args = []) {
737
824
  formatSlackMessages(messages);
738
825
  }
739
826
 
827
+ async function slackSend(channel, textParts) {
828
+ const target = String(channel || '').trim();
829
+ const text = (textParts || []).join(' ').trim();
830
+ if (!target || !text) {
831
+ console.error('Usage: atris slack send <channel> "<message>"');
832
+ process.exit(1);
833
+ }
834
+
835
+ const token = await getAuthToken();
836
+
837
+ console.log(`Sending to ${target}...`);
838
+
839
+ const result = await apiRequestJson('/integrations/slack/me/send', {
840
+ method: 'POST',
841
+ token,
842
+ body: { channel: target, text },
843
+ });
844
+
845
+ if (!result.ok) {
846
+ console.error(`Error: ${result.error || 'Failed to send'}`);
847
+ process.exit(1);
848
+ }
849
+
850
+ console.log(`Sent to ${target} as you.`);
851
+ }
852
+
853
+ async function slackDm(userId, textParts) {
854
+ const target = String(userId || '').trim();
855
+ const text = (textParts || []).join(' ').trim();
856
+ if (!target || !text) {
857
+ console.error('Usage: atris slack dm <slack_user_id> "<message>"');
858
+ process.exit(1);
859
+ }
860
+
861
+ const token = await getAuthToken();
862
+
863
+ console.log(`Sending DM to ${target}...`);
864
+
865
+ const result = await apiRequestJson('/integrations/slack/me/dm', {
866
+ method: 'POST',
867
+ token,
868
+ body: { slack_user_id: target, text },
869
+ });
870
+
871
+ if (!result.ok) {
872
+ console.error(`Error: ${result.error || 'Failed to send DM'}`);
873
+ process.exit(1);
874
+ }
875
+
876
+ console.log(`DM sent to ${target} as you.`);
877
+ }
878
+
740
879
  async function slackCommand(subcommand, ...args) {
741
880
  switch (subcommand) {
742
881
  case 'channels':
@@ -753,12 +892,20 @@ async function slackCommand(subcommand, ...args) {
753
892
  case 'search':
754
893
  await slackSearch(argsWithoutLimit(args).join(' '), args);
755
894
  break;
895
+ case 'send':
896
+ await slackSend(args[0], args.slice(1));
897
+ break;
898
+ case 'dm':
899
+ await slackDm(args[0], args.slice(1));
900
+ break;
756
901
  default:
757
902
  console.log('Slack commands:');
758
903
  console.log(' atris slack channels - List Slack channels');
759
904
  console.log(' atris slack dms - List Slack DMs');
760
905
  console.log(' atris slack messages <channel> - Read recent messages');
761
906
  console.log(' atris slack search <query> - Search Slack messages');
907
+ console.log(' atris slack send <channel> "<message>" - Send a message as you');
908
+ console.log(' atris slack dm <user_id> "<message>" - DM someone as you');
762
909
  }
763
910
  }
764
911
 
@@ -611,6 +611,89 @@ function startMemberRunMission(name, missionText, args = []) {
611
611
  }
612
612
  }
613
613
 
614
+ // atris member ping <name> "<message>" — talk to a busy member mid-run.
615
+ // Two delivery lanes: the member's most recently touched live mission
616
+ // (including --worktree missions in sibling checkouts), whose next tick
617
+ // consumes the note, and the member's claimed task dialogue, which task-loop
618
+ // agents re-read every step without ever ticking a mission. The ping lands on
619
+ // every lane that exists and errors only when neither does.
620
+ function pingClaimedTaskDialogue(name, text, from) {
621
+ let taskDb;
622
+ try {
623
+ taskDb = require('../lib/task-db');
624
+ } catch {
625
+ return null;
626
+ }
627
+ try {
628
+ const db = taskDb.open();
629
+ const local = taskDb.listTasks(db, { workspaceRoot: taskDb.workspaceRoot(), status: 'claimed', claimedBy: name });
630
+ const pool = local.length ? local : taskDb.listTasks(db, { status: 'claimed', claimedBy: name });
631
+ if (!pool.length) return null;
632
+ const task = [...pool].sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0))[0];
633
+ const result = taskDb.noteTask(db, { id: task.id, actor: from, content: text });
634
+ if (!result.noted) return null;
635
+ return { task_id: task.id, title: task.title };
636
+ } catch {
637
+ return null;
638
+ }
639
+ }
640
+
641
+ function memberPing(name, ...args) {
642
+ if (!name || name === '--help' || name === '-h') {
643
+ console.log('Usage: atris member ping <name> "<message>" [--from <who>] [--json]');
644
+ console.log('Leaves the note on the member\'s active mission and claimed task dialogue; whichever loop the member runs reads it next.');
645
+ return;
646
+ }
647
+ const asJson = args.includes('--json');
648
+ const rest = args.filter((a) => a !== '--json');
649
+ let from = process.env.USER || 'operator';
650
+ const fromIdx = rest.indexOf('--from');
651
+ if (fromIdx !== -1) {
652
+ from = rest[fromIdx + 1] || from;
653
+ rest.splice(fromIdx, 2);
654
+ }
655
+ const text = rest.filter((a) => !a.startsWith('--')).join(' ').trim();
656
+ if (!text) {
657
+ console.error('atris member ping: message required');
658
+ process.exit(2);
659
+ }
660
+ const missionMod = require('./mission');
661
+ const terminal = new Set(['complete', 'stopped', 'failed']);
662
+ const candidates = [
663
+ ...missionMod.listMissions(process.cwd()),
664
+ ...missionMod.listWorktreeRollupMissions(process.cwd()),
665
+ ]
666
+ .filter((m) => m && m.owner === name && !terminal.has(m.status))
667
+ .sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')));
668
+
669
+ const taskNote = pingClaimedTaskDialogue(name, text, from);
670
+
671
+ if (!candidates.length && !taskNote) {
672
+ console.error(`atris member ping: no live mission or claimed task for "${name}". Start one: atris member run ${name} "<objective>"`);
673
+ process.exit(1);
674
+ }
675
+
676
+ const mission = candidates.length
677
+ ? missionMod.pingMission([candidates[0].id, text, '--from', from], { silent: true })
678
+ : null;
679
+ const pendingPings = mission ? (mission.pings || []).filter((p) => p && !p.consumed_at).length : null;
680
+
681
+ if (asJson) {
682
+ console.log(JSON.stringify({
683
+ ok: true,
684
+ action: 'member_ping',
685
+ member: name,
686
+ mission_id: mission ? mission.id : null,
687
+ pending_pings: pendingPings,
688
+ task_id: taskNote ? taskNote.task_id : null,
689
+ }));
690
+ } else {
691
+ if (mission) console.log(`pinged ${mission.id} — the next tick reads it (${pendingPings} unread).`);
692
+ if (taskNote) console.log(`pinged task ${taskNote.task_id} dialogue — ${name} reads it on the next step.`);
693
+ }
694
+ return { mission, task_note: taskNote };
695
+ }
696
+
614
697
  function memberRun(name, ...args) {
615
698
  if (!name || name === '--help' || name === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
616
699
  console.log('Usage: atris member run <name> ["mission text"] [--minutes N|--hours N] [--json]');
@@ -8063,6 +8146,8 @@ async function memberCommand(subcommand, ...args) {
8063
8146
  return memberWake(args[0], ...args.slice(1));
8064
8147
  case 'run':
8065
8148
  return memberRun(args[0], ...args.slice(1));
8149
+ case 'ping':
8150
+ return memberPing(args[0], ...args.slice(1));
8066
8151
  case 'loop':
8067
8152
  return memberLoop(args[0], ...args.slice(1));
8068
8153
  case 'alive':