atris 3.53.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.
Files changed (119) hide show
  1. package/FOR_AGENTS.md +7 -0
  2. package/README.md +1 -1
  3. package/atris/policies/ANTISLOP.md +2 -1
  4. package/atris/skills/copy-editor/SKILL.md +2 -1
  5. package/atris/skills/design/SKILL.md +1 -1
  6. package/atris/skills/x-search/SKILL.md +24 -22
  7. package/atris/skills/youtube/SKILL.md +82 -33
  8. package/atris/team/_template/SOUL.md +40 -0
  9. package/atris/team/validator/MEMBER.md +1 -2
  10. package/bin/atris +21 -0
  11. package/bin/atris.js +563 -373
  12. package/commands/api-key.js +170 -0
  13. package/commands/auth.js +453 -43
  14. package/commands/autoland.js +3 -1
  15. package/commands/autopilot-front.js +58 -11
  16. package/commands/avail.js +5 -4
  17. package/commands/balance.js +55 -0
  18. package/commands/bench.js +29 -1
  19. package/commands/brain.js +63 -12
  20. package/commands/brainstorm.js +18 -0
  21. package/commands/business.js +149 -1
  22. package/commands/chat-scan.js +17 -4
  23. package/commands/close.js +5 -1
  24. package/commands/doctor.js +174 -0
  25. package/commands/dream.js +7 -0
  26. package/commands/drill.js +34 -2
  27. package/commands/engine.js +47 -35
  28. package/commands/errors.js +58 -8
  29. package/commands/experiments.js +14 -0
  30. package/commands/feedback.js +61 -2
  31. package/commands/fleet-report.js +27 -7
  32. package/commands/fleet.js +98 -11
  33. package/commands/founder.js +60 -21
  34. package/commands/human-missions.js +79 -4
  35. package/commands/improve.js +54 -4
  36. package/commands/init.js +68 -92
  37. package/commands/install.js +40 -0
  38. package/commands/integrations.js +565 -30
  39. package/commands/interview.js +8 -0
  40. package/commands/land.js +48 -17
  41. package/commands/learn.js +29 -4
  42. package/commands/log.js +51 -0
  43. package/commands/member.js +68 -10
  44. package/commands/mission.js +287 -50
  45. package/commands/now.js +15 -3
  46. package/commands/orb.js +97 -2
  47. package/commands/pack.js +83 -9
  48. package/commands/playbook.js +381 -0
  49. package/commands/plugin.js +16 -0
  50. package/commands/radar.js +31 -7
  51. package/commands/recap.js +128 -21
  52. package/commands/release.js +102 -2
  53. package/commands/review.js +5 -10
  54. package/commands/revisions.js +55 -0
  55. package/commands/run-front.js +12 -4
  56. package/commands/scout.js +12 -1
  57. package/commands/search.js +123 -28
  58. package/commands/sign.js +2 -2
  59. package/commands/signup.js +12 -3
  60. package/commands/site-deploy.js +17 -4
  61. package/commands/site.js +2 -2
  62. package/commands/skill-eval.js +289 -0
  63. package/commands/skill.js +56 -6
  64. package/commands/social.js +603 -0
  65. package/commands/spaceship.js +69 -6
  66. package/commands/stream.js +8 -1
  67. package/commands/study.js +32 -19
  68. package/commands/sync.js +28 -22
  69. package/commands/task.js +435 -60
  70. package/commands/teach.js +72 -63
  71. package/commands/terminal.js +25 -9
  72. package/commands/topup.js +88 -0
  73. package/commands/truth.js +52 -6
  74. package/commands/usage.js +91 -0
  75. package/commands/verify.js +3 -2
  76. package/commands/version.js +3 -1
  77. package/commands/voice.js +5 -1
  78. package/commands/who.js +69 -11
  79. package/commands/wiki.js +7 -4
  80. package/commands/wish.js +76 -5
  81. package/commands/workflow.js +327 -145
  82. package/commands/worktree.js +1 -0
  83. package/commands/write.js +5 -0
  84. package/commands/x-search.js +340 -0
  85. package/commands/xp.js +5 -1
  86. package/commands/youtube.js +551 -25
  87. package/lib/account-bound.js +47 -0
  88. package/lib/auto-accept-certified.js +8 -1
  89. package/lib/auto-lessons.js +186 -0
  90. package/lib/autoland.js +4 -2
  91. package/lib/chat-log-scan.js +7 -4
  92. package/lib/cli-json.js +77 -0
  93. package/lib/cli-scope.js +26 -0
  94. package/lib/codex-flight.js +1 -0
  95. package/lib/conductor-artifacts.js +1 -1
  96. package/lib/context-gatherer.js +11 -0
  97. package/lib/developer-api.js +116 -0
  98. package/lib/feedback-triage.js +274 -0
  99. package/lib/first-minute.js +398 -0
  100. package/lib/fleet.js +115 -34
  101. package/lib/functional-owner.js +22 -0
  102. package/lib/known-commands.js +31 -5
  103. package/lib/member-scaffold.js +197 -0
  104. package/lib/mission-root.js +4 -2
  105. package/lib/noninteractive.js +88 -0
  106. package/lib/policy-lessons.js +4 -1
  107. package/lib/revision-metric.js +279 -0
  108. package/lib/scratch-root.js +48 -0
  109. package/lib/skill-eval-gate.js +249 -0
  110. package/lib/task-db.js +4 -0
  111. package/lib/task-proof.js +69 -8
  112. package/lib/verifier-quality.js +69 -0
  113. package/lib/wish-audit.js +2 -2
  114. package/lib/wish-delegate.js +10 -1
  115. package/lib/workspace-scaffold.js +270 -0
  116. package/package.json +3 -2
  117. package/scripts/det/checklist-score.js +191 -0
  118. package/scripts/det/ytsearch +31 -0
  119. package/utils/update-check.js +15 -0
@@ -2,8 +2,14 @@
2
2
  * Integration commands for Atris CLI
3
3
  *
4
4
  * Usage:
5
- * atris gmail inbox - List recent emails
6
- * atris gmail read <id> - Read specific email
5
+ * atris gmail inbox [--account <id>] - List recent emails for a mailbox
6
+ * atris gmail read <id> [--account <id>] - Read specific email
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
10
+ * atris gmail connect [name] - Connect or reconnect a Gmail account
11
+ * atris gmail accounts - List connected Gmail accounts and the active account
12
+ * atris gmail use [<account_id|name>] - Choose or set the active Gmail account
7
13
  * atris calendar today - Show today's events
8
14
  * atris calendar yesterday - Show yesterday's events
9
15
  * atris calendar week - Show this week's events
@@ -25,9 +31,116 @@ const {
25
31
  const fs = require('fs');
26
32
  const os = require('os');
27
33
  const path = require('path');
34
+ const readline = require('readline');
28
35
  const { spawnSync } = require('child_process');
29
36
 
30
37
  const CALENDAR_CACHE_PATH = path.join(os.homedir(), '.atris', 'calendar-events-cache.json');
38
+ const GMAIL_CONNECT_POLL_MS = 3000;
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]';
42
+
43
+ function gmailAccountStatePath() {
44
+ return process.env.ATRIS_GMAIL_ACCOUNT_FILE
45
+ || path.join(os.homedir(), '.atris', 'gmail-account.json');
46
+ }
47
+
48
+ function readGmailStickyAccount() {
49
+ try {
50
+ const data = JSON.parse(fs.readFileSync(gmailAccountStatePath(), 'utf8'));
51
+ const id = String(data.account_id || '').trim();
52
+ return id || null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function writeGmailStickyAccount(accountId) {
59
+ const filePath = gmailAccountStatePath();
60
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
61
+ fs.writeFileSync(filePath, `${JSON.stringify({ account_id: accountId }, null, 2)}\n`, 'utf8');
62
+ }
63
+
64
+ function resolveGmailAccountId(explicitId) {
65
+ const trimmed = String(explicitId || '').trim();
66
+ if (trimmed) return trimmed;
67
+ const sticky = readGmailStickyAccount();
68
+ if (sticky) return sticky;
69
+ return 'default';
70
+ }
71
+
72
+ function gmailAccountIdentity(account = {}) {
73
+ const id = String(account.account_id || account.id || 'default').trim() || 'default';
74
+ const email = String(account.email_address || account.email || '').trim().toLowerCase();
75
+ const name = String(account.display_name || account.name || id).trim().toLowerCase() || id.toLowerCase();
76
+ return { id, email, name };
77
+ }
78
+
79
+ function sortGmailAccounts(accounts) {
80
+ return [...accounts].sort((left, right) => {
81
+ const leftIdentity = gmailAccountIdentity(left);
82
+ const rightIdentity = gmailAccountIdentity(right);
83
+ const leftDefault = leftIdentity.id.toLowerCase() === 'default';
84
+ const rightDefault = rightIdentity.id.toLowerCase() === 'default';
85
+ if (leftDefault !== rightDefault) return leftDefault ? -1 : 1;
86
+ return leftIdentity.name.localeCompare(rightIdentity.name)
87
+ || leftIdentity.id.localeCompare(rightIdentity.id);
88
+ });
89
+ }
90
+
91
+ function formatGmailAccountRows(accounts, activeId = null) {
92
+ const rows = sortGmailAccounts(accounts).map((account) => {
93
+ const identity = gmailAccountIdentity(account);
94
+ return { ...identity, emailLabel: identity.email || 'email pending' };
95
+ });
96
+ const nameWidth = Math.max(...rows.map((row) => row.name.length));
97
+ const emailWidth = Math.max(...rows.map((row) => row.emailLabel.length));
98
+ const active = String(activeId || '').toLowerCase();
99
+ return rows.map((row) => {
100
+ const marker = row.id.toLowerCase() === active ? ' (active)' : '';
101
+ return `${row.name.padEnd(nameWidth)} ${row.emailLabel.padEnd(emailWidth)}${marker}`.trimEnd();
102
+ });
103
+ }
104
+
105
+ async function fetchGmailAccounts(token, options = {}) {
106
+ const result = await apiRequestJson('/integrations/gmail/accounts', {
107
+ method: 'GET',
108
+ token,
109
+ });
110
+ if (!result.ok) {
111
+ if (options.quiet) return [];
112
+ console.error(`Error: ${result.error || 'Failed to fetch accounts'}`);
113
+ process.exit(1);
114
+ }
115
+ const accounts = result.data?.accounts || result.data || [];
116
+ return Array.isArray(accounts) ? accounts : [];
117
+ }
118
+
119
+ function findGmailAccount(accounts, accountId) {
120
+ const wanted = String(accountId || '').trim().toLowerCase();
121
+ return accounts.find((account) => gmailAccountIdentity(account).id.toLowerCase() === wanted) || null;
122
+ }
123
+
124
+ function findGmailAccountForUse(accounts, selector) {
125
+ const wanted = String(selector || '').trim().toLowerCase();
126
+ return findGmailAccount(accounts, wanted)
127
+ || accounts.find((account) => gmailAccountIdentity(account).name === wanted)
128
+ || null;
129
+ }
130
+
131
+ async function promptForGmailAccount(deps = {}) {
132
+ if (deps.prompt) return deps.prompt('choose account: ');
133
+ const rl = readline.createInterface({
134
+ input: deps.input || process.stdin,
135
+ output: deps.output || process.stdout,
136
+ });
137
+ return new Promise((resolve) => {
138
+ rl.question('choose account: ', (answer) => {
139
+ rl.close();
140
+ resolve(answer);
141
+ });
142
+ });
143
+ }
31
144
 
32
145
  async function getAuth() {
33
146
  const ensured = await ensureValidCredentials(apiRequestJson);
@@ -51,13 +164,54 @@ async function getAuthToken() {
51
164
  // GMAIL
52
165
  // ============================================================================
53
166
 
167
+ function extractGmailMailboxAccount(args = []) {
168
+ const out = [];
169
+ let mailboxAccountId = null;
170
+ for (let i = 0; i < args.length; i += 1) {
171
+ const arg = args[i];
172
+ if (arg === '--account' && args[i + 1] && !String(args[i + 1]).startsWith('-')) {
173
+ mailboxAccountId = args[i + 1];
174
+ i += 1;
175
+ continue;
176
+ }
177
+ out.push(arg);
178
+ }
179
+ return { args: out, mailboxAccountId };
180
+ }
181
+
182
+ function parseGmailArgs(args = []) {
183
+ const options = { accountId: null };
184
+ const positional = [];
185
+ for (let i = 0; i < args.length; i += 1) {
186
+ const arg = args[i];
187
+ if (arg === '--account') {
188
+ options.accountId = String(args[i + 1] || '').trim();
189
+ if (!options.accountId) {
190
+ console.error('Usage: --account requires an account id');
191
+ process.exit(1);
192
+ }
193
+ i += 1;
194
+ } else {
195
+ positional.push(arg);
196
+ }
197
+ }
198
+ return { ...options, positional };
199
+ }
200
+
54
201
  async function gmailInbox(options = {}) {
55
202
  const { token, email } = await getAuth();
56
203
  const limit = options.limit || 10;
204
+ const accountId = resolveGmailAccountId(options.accountId);
205
+ const accounts = await fetchGmailAccounts(token, { quiet: true });
206
+ const account = findGmailAccount(accounts, accountId);
207
+ const mailboxEmail = account ? gmailAccountIdentity(account).email : '';
57
208
 
209
+ if (mailboxEmail) console.log(`inbox for ${mailboxEmail} (${accountId.toLowerCase()})`);
58
210
  console.log('📬 Fetching inbox...\n');
59
211
 
60
- const result = await apiRequestJson(`/integrations/gmail/messages?max_results=${limit}`, {
212
+ const path = `/integrations/gmail/messages?max_results=${limit}&account_id=${encodeURIComponent(accountId)}`;
213
+
214
+ const result = await apiRequestJson(path, {
61
215
  method: 'GET',
62
216
  token,
63
217
  });
@@ -97,17 +251,20 @@ async function gmailInbox(options = {}) {
97
251
  }
98
252
  }
99
253
 
100
- async function gmailRead(messageId) {
254
+ async function gmailRead(messageId, options = {}) {
101
255
  if (!messageId) {
102
- console.error('Usage: atris gmail read <message_id>');
256
+ console.error('Usage: atris gmail read <message_id> [--account <id>]');
103
257
  process.exit(1);
104
258
  }
105
259
 
106
260
  const token = await getAuthToken();
261
+ const accountId = resolveGmailAccountId(options.accountId);
107
262
 
108
263
  console.log('📧 Fetching message...\n');
109
264
 
110
- const result = await apiRequestJson(`/integrations/gmail/messages/${messageId}`, {
265
+ const path = `/integrations/gmail/messages/${messageId}?account_id=${encodeURIComponent(accountId)}`;
266
+
267
+ const result = await apiRequestJson(path, {
111
268
  method: 'GET',
112
269
  token,
113
270
  });
@@ -129,21 +286,24 @@ async function gmailRead(messageId) {
129
286
  console.log(msg.body || msg.snippet || '(no body)');
130
287
  }
131
288
 
132
- async function gmailArchive(messageIds) {
289
+ async function gmailArchive(messageIds, options = {}) {
133
290
  const ids = (messageIds || []).map(id => String(id || '').trim()).filter(Boolean);
134
291
  if (!ids.length) {
135
- console.error('Usage: atris gmail archive <message_id> [<message_id> ...]');
292
+ console.error('Usage: atris gmail archive <message_id> [<message_id> ...] [--account <id>]');
136
293
  process.exit(1);
137
294
  }
138
295
 
139
296
  const token = await getAuthToken();
297
+ const accountId = resolveGmailAccountId(options.accountId);
140
298
 
141
299
  console.log(`Archiving ${ids.length} message${ids.length === 1 ? '' : 's'}...`);
142
300
 
301
+ const body = { message_ids: ids, account_id: accountId };
302
+
143
303
  const result = await apiRequestJson('/integrations/gmail/messages/batch-archive', {
144
304
  method: 'POST',
145
305
  token,
146
- body: { message_ids: ids },
306
+ body,
147
307
  });
148
308
 
149
309
  if (!result.ok) {
@@ -155,23 +315,330 @@ async function gmailArchive(messageIds) {
155
315
  console.log(`Archived ${archived} message${archived === 1 ? '' : 's'}. They stay searchable in All Mail.`);
156
316
  }
157
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
+
467
+ function openGmailAuthUrl(authUrl, deps = {}) {
468
+ const run = deps.spawnSync || spawnSync;
469
+ const platform = deps.platform || process.platform;
470
+ const command = platform === 'darwin' ? 'open' : 'xdg-open';
471
+ const opened = run(command, [authUrl], { stdio: 'ignore' });
472
+ return !opened.error && opened.status === 0;
473
+ }
474
+
475
+ async function gmailConnect(accountName, deps = {}) {
476
+ const accountId = String(accountName || 'default').trim();
477
+ if (!/^[a-z0-9-]{1,32}$/.test(accountId)) {
478
+ console.error('invalid gmail account name. use 1 to 32 lowercase letters, numbers, or hyphens.');
479
+ process.exit(1);
480
+ }
481
+
482
+ const token = await getAuthToken();
483
+ const result = await apiRequestJson('/integrations/gmail/start', {
484
+ method: 'POST',
485
+ token,
486
+ body: { account_id: accountId },
487
+ });
488
+
489
+ if (!result.ok) {
490
+ const detail = String(result.error || 'request failed').toLowerCase();
491
+ console.error(`could not start gmail connection: ${detail}`);
492
+ process.exit(1);
493
+ }
494
+
495
+ const authUrl = String(result.data?.auth_url || result.data?.url || '').trim();
496
+ if (!authUrl) {
497
+ console.error('could not start gmail connection: the server did not return an auth url.');
498
+ process.exit(1);
499
+ }
500
+
501
+ const openAuthUrl = deps.openAuthUrl || openGmailAuthUrl;
502
+ if (!openAuthUrl(authUrl, deps)) {
503
+ console.log(`open this url in your browser: ${authUrl}`);
504
+ }
505
+
506
+ console.log(`waiting for gmail account ${accountId}...`);
507
+
508
+ const pollIntervalMs = deps.pollIntervalMs ?? GMAIL_CONNECT_POLL_MS;
509
+ const timeoutMs = deps.timeoutMs ?? GMAIL_CONNECT_TIMEOUT_MS;
510
+ const sleep = deps.sleep || ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
511
+ const maxPolls = Math.max(1, Math.ceil(timeoutMs / pollIntervalMs));
512
+
513
+ for (let poll = 0; poll < maxPolls; poll += 1) {
514
+ await sleep(pollIntervalMs);
515
+ const accountsResult = await apiRequestJson('/integrations/gmail/accounts', {
516
+ method: 'GET',
517
+ token,
518
+ });
519
+ const accounts = accountsResult.ok
520
+ ? (accountsResult.data?.accounts || accountsResult.data || [])
521
+ : [];
522
+ const match = Array.isArray(accounts) ? findGmailAccount(accounts, accountId) : null;
523
+ const email = match ? gmailAccountIdentity(match).email.trim() : '';
524
+ if (email && email !== 'unknown') {
525
+ console.log(`connected ${email} as ${accountId}`);
526
+ return;
527
+ }
528
+ }
529
+
530
+ console.error(`connection timed out. finish in your browser: ${authUrl}`);
531
+ console.error(`or connect from settings: https://atris.ai/dashboard/settings?connect=gmail&account=${accountId}`);
532
+ process.exit(1);
533
+ }
534
+
535
+ async function gmailAccounts() {
536
+ const token = await getAuthToken();
537
+ const accounts = await fetchGmailAccounts(token);
538
+ const activeId = resolveGmailAccountId(null);
539
+
540
+ if (!accounts.length) {
541
+ console.log('no connected accounts.');
542
+ return;
543
+ }
544
+
545
+ for (const line of formatGmailAccountRows(accounts, activeId)) console.log(line);
546
+ }
547
+
548
+ async function gmailUse(accountId, deps = {}) {
549
+ const token = await getAuthToken();
550
+ const accounts = await fetchGmailAccounts(token);
551
+ let selector = String(accountId || '').trim();
552
+
553
+ if (!selector) {
554
+ if (!accounts.length) {
555
+ console.log('no connected accounts.');
556
+ return;
557
+ }
558
+ if (accounts.length === 1) {
559
+ const { name, email } = gmailAccountIdentity(accounts[0]);
560
+ console.log(`${name} (${email || 'email pending'}) is already the only gmail account.`);
561
+ return;
562
+ }
563
+
564
+ const sortedAccounts = sortGmailAccounts(accounts);
565
+ const activeId = resolveGmailAccountId(null);
566
+ const rows = formatGmailAccountRows(sortedAccounts, activeId);
567
+ rows.forEach((line, index) => console.log(`${index + 1}. ${line}`));
568
+ const answer = String(await promptForGmailAccount(deps) || '').trim();
569
+ const number = /^\d+$/.test(answer) ? Number(answer) : 0;
570
+ selector = number >= 1 && number <= sortedAccounts.length
571
+ ? gmailAccountIdentity(sortedAccounts[number - 1]).id
572
+ : answer;
573
+ }
574
+
575
+ const match = findGmailAccountForUse(accounts, selector);
576
+ if (!match) {
577
+ console.error(`unknown gmail account "${selector}".`);
578
+ if (accounts.length) {
579
+ console.error('connected accounts:');
580
+ for (const account of sortGmailAccounts(accounts)) {
581
+ const { id, email, name } = gmailAccountIdentity(account);
582
+ console.error(` ${id.toLowerCase()}: ${email || 'email pending'}, ${name}`);
583
+ }
584
+ } else {
585
+ console.error('no connected accounts.');
586
+ }
587
+ process.exit(1);
588
+ }
589
+
590
+ const { id, name, email } = gmailAccountIdentity(match);
591
+ writeGmailStickyAccount(id);
592
+ console.log(`now using ${name} (${email || 'email pending'})`);
593
+ }
594
+
158
595
  async function gmailCommand(subcommand, ...args) {
159
596
  switch (subcommand) {
160
597
  case 'inbox':
161
- case 'list':
162
- await gmailInbox();
598
+ case 'list': {
599
+ const parsed = parseGmailArgs(args);
600
+ await gmailInbox({ accountId: parsed.accountId || undefined });
163
601
  break;
164
- case 'read':
165
- await gmailRead(args[0]);
602
+ }
603
+ case 'read': {
604
+ const parsed = parseGmailArgs(args);
605
+ await gmailRead(parsed.positional[0], { accountId: parsed.accountId || undefined });
166
606
  break;
167
- case 'archive':
168
- await gmailArchive(args);
607
+ }
608
+ case 'archive': {
609
+ const parsed = parseGmailArgs(args);
610
+ await gmailArchive(parsed.positional, { accountId: parsed.accountId || undefined });
611
+ break;
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
+ }
623
+ case 'connect':
624
+ await gmailConnect(args[0]);
625
+ break;
626
+ case 'accounts':
627
+ await gmailAccounts();
628
+ break;
629
+ case 'use':
630
+ await gmailUse(args.join(' '));
169
631
  break;
170
632
  default:
171
- console.log('Gmail commands:');
172
- console.log(' atris gmail inbox - List recent emails');
173
- console.log(' atris gmail read <id> - Read specific email');
174
- console.log(' atris gmail archive <id> [...] - Archive messages (reversible, All Mail keeps them)');
633
+ console.log('gmail commands:');
634
+ console.log(' atris gmail inbox [--account <id>] - list recent emails for a mailbox');
635
+ console.log(' atris gmail read <id> [--account <id>] - read specific email');
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');
639
+ console.log(' atris gmail connect [name] - connect or reconnect a gmail account');
640
+ console.log(' atris gmail accounts - list connected gmail accounts and the active account');
641
+ console.log(' atris gmail use [<account_id|name>] - choose or set the active gmail account');
175
642
  }
176
643
  }
177
644
 
@@ -228,7 +695,8 @@ function formatCalendarTimeRange(event) {
228
695
 
229
696
  function formatCalendarEvents(label, events) {
230
697
  if (events.length === 0) {
231
- console.log(`No events ${label}. 🎉`);
698
+ // label is already a phrase like "today's events" or "events on 2026-08-24"
699
+ console.log(`No ${label}.`);
232
700
  return;
233
701
  }
234
702
 
@@ -599,7 +1067,15 @@ async function calendarCommand(subcommand, ...args) {
599
1067
  // ============================================================================
600
1068
 
601
1069
  async function twitterPost(args = []) {
602
- const outbound = prepareOutboundSendTextOrExit('twitter', args);
1070
+ const approved = args.includes('--approved') || args.includes('--confirm-approved');
1071
+ const cleaned = args.filter((a) => a !== '--approved' && a !== '--confirm-approved');
1072
+ if (!approved) {
1073
+ console.error('Refusing to post without --approved after the exact text is confirmed.');
1074
+ console.error('Usage: atris twitter post "<text>" --approved');
1075
+ process.exit(2);
1076
+ }
1077
+
1078
+ const outbound = prepareOutboundSendTextOrExit('twitter', cleaned);
603
1079
  let text = outbound.body;
604
1080
  if (!text) {
605
1081
  const readline = require('readline');
@@ -658,7 +1134,7 @@ async function twitterCommand(subcommand, ...args) {
658
1134
  break;
659
1135
  default:
660
1136
  console.log('Twitter commands:');
661
- console.log(' atris twitter post [text] - Post a tweet');
1137
+ console.log(' atris twitter post "<text>" --approved - Post a tweet');
662
1138
  }
663
1139
  }
664
1140
 
@@ -834,9 +1310,16 @@ async function slackSearch(query, args = []) {
834
1310
 
835
1311
  async function slackSend(channel, textParts) {
836
1312
  const target = String(channel || '').trim();
837
- const text = prepareOutboundSendTextOrExit('slack', textParts).body;
1313
+ const approved = (textParts || []).includes('--approved') || (textParts || []).includes('--confirm-approved');
1314
+ const cleaned = (textParts || []).filter((a) => a !== '--approved' && a !== '--confirm-approved');
1315
+ if (!approved) {
1316
+ console.error('Refusing to send without --approved after the exact channel and exact text are confirmed.');
1317
+ console.error('Usage: atris slack send <channel> "<message>" --approved');
1318
+ process.exit(2);
1319
+ }
1320
+ const text = prepareOutboundSendTextOrExit('slack', cleaned).body;
838
1321
  if (!target || !text) {
839
- console.error('Usage: atris slack send <channel> "<message>"');
1322
+ console.error('Usage: atris slack send <channel> "<message>" --approved');
840
1323
  process.exit(1);
841
1324
  }
842
1325
 
@@ -860,9 +1343,16 @@ async function slackSend(channel, textParts) {
860
1343
 
861
1344
  async function slackDm(userId, textParts) {
862
1345
  const target = String(userId || '').trim();
863
- const text = prepareOutboundSendTextOrExit('slack', textParts).body;
1346
+ const approved = (textParts || []).includes('--approved') || (textParts || []).includes('--confirm-approved');
1347
+ const cleaned = (textParts || []).filter((a) => a !== '--approved' && a !== '--confirm-approved');
1348
+ if (!approved) {
1349
+ console.error('Refusing to send without --approved after the exact recipient and exact text are confirmed.');
1350
+ console.error('Usage: atris slack dm <slack_user_id> "<message>" --approved');
1351
+ process.exit(2);
1352
+ }
1353
+ const text = prepareOutboundSendTextOrExit('slack', cleaned).body;
864
1354
  if (!target || !text) {
865
- console.error('Usage: atris slack dm <slack_user_id> "<message>"');
1355
+ console.error('Usage: atris slack dm <slack_user_id> "<message>" --approved');
866
1356
  process.exit(1);
867
1357
  }
868
1358
 
@@ -912,8 +1402,8 @@ async function slackCommand(subcommand, ...args) {
912
1402
  console.log(' atris slack dms - List Slack DMs');
913
1403
  console.log(' atris slack messages <channel> - Read recent messages');
914
1404
  console.log(' atris slack search <query> - Search Slack messages');
915
- console.log(' atris slack send <channel> "<message>" - Send a message as you');
916
- console.log(' atris slack dm <user_id> "<message>" - DM someone as you');
1405
+ console.log(' atris slack send <channel> "<message>" --approved - Send a message as you');
1406
+ console.log(' atris slack dm <user_id> "<message>" --approved - DM someone as you');
917
1407
  }
918
1408
  }
919
1409
 
@@ -1588,10 +2078,41 @@ async function imessageCommand(subcommand, ...args) {
1588
2078
  // STATUS
1589
2079
  // ============================================================================
1590
2080
 
1591
- async function integrationsStatus() {
2081
+ async function integrationsStatus(args = []) {
2082
+ const { parseScopeFlag } = require('../lib/cli-scope');
2083
+ const scope = parseScopeFlag(args);
2084
+ const json = scope.args.includes('--json') || args.includes('--json');
2085
+
2086
+ if (!scope.global) {
2087
+ const localPath = path.join(process.cwd(), '.atris', 'integrations.json');
2088
+ let local = null;
2089
+ try {
2090
+ if (fs.existsSync(localPath)) local = JSON.parse(fs.readFileSync(localPath, 'utf8'));
2091
+ } catch {}
2092
+ const payload = {
2093
+ scope: 'workspace',
2094
+ workspace_root: process.cwd(),
2095
+ integrations: Array.isArray(local?.integrations) ? local.integrations : [],
2096
+ note: 'workspace scope only. pass --global for account-level mail/chat/code hosts.',
2097
+ };
2098
+ if (json) {
2099
+ console.log(JSON.stringify(payload, null, 2));
2100
+ return 0;
2101
+ }
2102
+ console.log('Integration Status (workspace)\n');
2103
+ if (!payload.integrations.length) {
2104
+ console.log(' (none configured in this workspace)');
2105
+ } else {
2106
+ for (const name of payload.integrations) console.log(` • ${name}`);
2107
+ }
2108
+ console.log('\nPass --global to check account-level gmail/slack/github status.');
2109
+ return 0;
2110
+ }
2111
+
1592
2112
  const token = await getAuthToken();
2113
+ const rows = [];
1593
2114
 
1594
- console.log('🔌 Integration Status:\n');
2115
+ console.log('🔌 Integration Status (global):\n');
1595
2116
 
1596
2117
  const integrations = ['gmail', 'google-calendar', 'slack', 'twitter', 'github'];
1597
2118
 
@@ -1606,19 +2127,32 @@ async function integrationsStatus() {
1606
2127
  const icon = connected ? '✅' : '❌';
1607
2128
  const displayName = name.replace('google-', '').replace('-', ' ');
1608
2129
  console.log(` ${icon} ${displayName}`);
2130
+ rows.push({ name, connected: Boolean(connected) });
1609
2131
  } catch {
1610
2132
  console.log(` ❓ ${name}`);
2133
+ rows.push({ name, connected: null });
1611
2134
  }
1612
2135
  }
1613
2136
 
1614
2137
  const imessage = imessageDoctor();
1615
2138
  console.log(` ${imessage.connected ? '✅' : '❌'} iMessage (local Mac)`);
2139
+ rows.push({ name: 'imessage', connected: Boolean(imessage.connected) });
1616
2140
 
1617
2141
  console.log('\nConnect integrations at: https://atris.ai/dashboard/settings');
2142
+ if (json) {
2143
+ console.log(JSON.stringify({ scope: 'global', integrations: rows }, null, 2));
2144
+ }
2145
+ return 0;
1618
2146
  }
1619
2147
 
1620
2148
  module.exports = {
1621
2149
  gmailCommand,
2150
+ gmailConnect,
2151
+ gmailVoice,
2152
+ gmailUse,
2153
+ extractGmailMailboxAccount,
2154
+ parseGmailArgs,
2155
+ gmailAccountStatePath,
1622
2156
  calendarCommand,
1623
2157
  twitterCommand,
1624
2158
  slackCommand,
@@ -1627,6 +2161,7 @@ module.exports = {
1627
2161
  integrationsStatus,
1628
2162
  parseCalendarSearchArgs,
1629
2163
  filterCalendarEventsForSearch,
2164
+ formatCalendarEvents,
1630
2165
  formatCalendarSearchResults,
1631
2166
  formatCalendarSearchError,
1632
2167
  };