atris 3.53.0 → 3.55.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 +399 -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 +418 -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,12 @@
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 connect [name] - Connect or reconnect a Gmail account
9
+ * atris gmail accounts - List connected Gmail accounts and the active account
10
+ * atris gmail use [<account_id|name>] - Choose or set the active Gmail account
7
11
  * atris calendar today - Show today's events
8
12
  * atris calendar yesterday - Show yesterday's events
9
13
  * atris calendar week - Show this week's events
@@ -25,9 +29,114 @@ const {
25
29
  const fs = require('fs');
26
30
  const os = require('os');
27
31
  const path = require('path');
32
+ const readline = require('readline');
28
33
  const { spawnSync } = require('child_process');
29
34
 
30
35
  const CALENDAR_CACHE_PATH = path.join(os.homedir(), '.atris', 'calendar-events-cache.json');
36
+ const GMAIL_CONNECT_POLL_MS = 3000;
37
+ const GMAIL_CONNECT_TIMEOUT_MS = 3 * 60 * 1000;
38
+
39
+ function gmailAccountStatePath() {
40
+ return process.env.ATRIS_GMAIL_ACCOUNT_FILE
41
+ || path.join(os.homedir(), '.atris', 'gmail-account.json');
42
+ }
43
+
44
+ function readGmailStickyAccount() {
45
+ try {
46
+ const data = JSON.parse(fs.readFileSync(gmailAccountStatePath(), 'utf8'));
47
+ const id = String(data.account_id || '').trim();
48
+ return id || null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function writeGmailStickyAccount(accountId) {
55
+ const filePath = gmailAccountStatePath();
56
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
57
+ fs.writeFileSync(filePath, `${JSON.stringify({ account_id: accountId }, null, 2)}\n`, 'utf8');
58
+ }
59
+
60
+ function resolveGmailAccountId(explicitId) {
61
+ const trimmed = String(explicitId || '').trim();
62
+ if (trimmed) return trimmed;
63
+ const sticky = readGmailStickyAccount();
64
+ if (sticky) return sticky;
65
+ return 'default';
66
+ }
67
+
68
+ function gmailAccountIdentity(account = {}) {
69
+ const id = String(account.account_id || account.id || 'default').trim() || 'default';
70
+ const email = String(account.email_address || account.email || '').trim().toLowerCase();
71
+ const name = String(account.display_name || account.name || id).trim().toLowerCase() || id.toLowerCase();
72
+ return { id, email, name };
73
+ }
74
+
75
+ function sortGmailAccounts(accounts) {
76
+ return [...accounts].sort((left, right) => {
77
+ const leftIdentity = gmailAccountIdentity(left);
78
+ const rightIdentity = gmailAccountIdentity(right);
79
+ const leftDefault = leftIdentity.id.toLowerCase() === 'default';
80
+ const rightDefault = rightIdentity.id.toLowerCase() === 'default';
81
+ if (leftDefault !== rightDefault) return leftDefault ? -1 : 1;
82
+ return leftIdentity.name.localeCompare(rightIdentity.name)
83
+ || leftIdentity.id.localeCompare(rightIdentity.id);
84
+ });
85
+ }
86
+
87
+ function formatGmailAccountRows(accounts, activeId = null) {
88
+ const rows = sortGmailAccounts(accounts).map((account) => {
89
+ const identity = gmailAccountIdentity(account);
90
+ return { ...identity, emailLabel: identity.email || 'email pending' };
91
+ });
92
+ const nameWidth = Math.max(...rows.map((row) => row.name.length));
93
+ const emailWidth = Math.max(...rows.map((row) => row.emailLabel.length));
94
+ const active = String(activeId || '').toLowerCase();
95
+ return rows.map((row) => {
96
+ const marker = row.id.toLowerCase() === active ? ' (active)' : '';
97
+ return `${row.name.padEnd(nameWidth)} ${row.emailLabel.padEnd(emailWidth)}${marker}`.trimEnd();
98
+ });
99
+ }
100
+
101
+ async function fetchGmailAccounts(token, options = {}) {
102
+ const result = await apiRequestJson('/integrations/gmail/accounts', {
103
+ method: 'GET',
104
+ token,
105
+ });
106
+ if (!result.ok) {
107
+ if (options.quiet) return [];
108
+ console.error(`Error: ${result.error || 'Failed to fetch accounts'}`);
109
+ process.exit(1);
110
+ }
111
+ const accounts = result.data?.accounts || result.data || [];
112
+ return Array.isArray(accounts) ? accounts : [];
113
+ }
114
+
115
+ function findGmailAccount(accounts, accountId) {
116
+ const wanted = String(accountId || '').trim().toLowerCase();
117
+ return accounts.find((account) => gmailAccountIdentity(account).id.toLowerCase() === wanted) || null;
118
+ }
119
+
120
+ function findGmailAccountForUse(accounts, selector) {
121
+ const wanted = String(selector || '').trim().toLowerCase();
122
+ return findGmailAccount(accounts, wanted)
123
+ || accounts.find((account) => gmailAccountIdentity(account).name === wanted)
124
+ || null;
125
+ }
126
+
127
+ async function promptForGmailAccount(deps = {}) {
128
+ if (deps.prompt) return deps.prompt('choose account: ');
129
+ const rl = readline.createInterface({
130
+ input: deps.input || process.stdin,
131
+ output: deps.output || process.stdout,
132
+ });
133
+ return new Promise((resolve) => {
134
+ rl.question('choose account: ', (answer) => {
135
+ rl.close();
136
+ resolve(answer);
137
+ });
138
+ });
139
+ }
31
140
 
32
141
  async function getAuth() {
33
142
  const ensured = await ensureValidCredentials(apiRequestJson);
@@ -51,13 +160,54 @@ async function getAuthToken() {
51
160
  // GMAIL
52
161
  // ============================================================================
53
162
 
163
+ function extractGmailMailboxAccount(args = []) {
164
+ const out = [];
165
+ let mailboxAccountId = null;
166
+ for (let i = 0; i < args.length; i += 1) {
167
+ const arg = args[i];
168
+ if (arg === '--account' && args[i + 1] && !String(args[i + 1]).startsWith('-')) {
169
+ mailboxAccountId = args[i + 1];
170
+ i += 1;
171
+ continue;
172
+ }
173
+ out.push(arg);
174
+ }
175
+ return { args: out, mailboxAccountId };
176
+ }
177
+
178
+ function parseGmailArgs(args = []) {
179
+ const options = { accountId: null };
180
+ const positional = [];
181
+ for (let i = 0; i < args.length; i += 1) {
182
+ const arg = args[i];
183
+ if (arg === '--account') {
184
+ options.accountId = String(args[i + 1] || '').trim();
185
+ if (!options.accountId) {
186
+ console.error('Usage: --account requires an account id');
187
+ process.exit(1);
188
+ }
189
+ i += 1;
190
+ } else {
191
+ positional.push(arg);
192
+ }
193
+ }
194
+ return { ...options, positional };
195
+ }
196
+
54
197
  async function gmailInbox(options = {}) {
55
198
  const { token, email } = await getAuth();
56
199
  const limit = options.limit || 10;
200
+ const accountId = resolveGmailAccountId(options.accountId);
201
+ const accounts = await fetchGmailAccounts(token, { quiet: true });
202
+ const account = findGmailAccount(accounts, accountId);
203
+ const mailboxEmail = account ? gmailAccountIdentity(account).email : '';
57
204
 
205
+ if (mailboxEmail) console.log(`inbox for ${mailboxEmail} (${accountId.toLowerCase()})`);
58
206
  console.log('📬 Fetching inbox...\n');
59
207
 
60
- const result = await apiRequestJson(`/integrations/gmail/messages?max_results=${limit}`, {
208
+ const path = `/integrations/gmail/messages?max_results=${limit}&account_id=${encodeURIComponent(accountId)}`;
209
+
210
+ const result = await apiRequestJson(path, {
61
211
  method: 'GET',
62
212
  token,
63
213
  });
@@ -97,17 +247,20 @@ async function gmailInbox(options = {}) {
97
247
  }
98
248
  }
99
249
 
100
- async function gmailRead(messageId) {
250
+ async function gmailRead(messageId, options = {}) {
101
251
  if (!messageId) {
102
- console.error('Usage: atris gmail read <message_id>');
252
+ console.error('Usage: atris gmail read <message_id> [--account <id>]');
103
253
  process.exit(1);
104
254
  }
105
255
 
106
256
  const token = await getAuthToken();
257
+ const accountId = resolveGmailAccountId(options.accountId);
107
258
 
108
259
  console.log('📧 Fetching message...\n');
109
260
 
110
- const result = await apiRequestJson(`/integrations/gmail/messages/${messageId}`, {
261
+ const path = `/integrations/gmail/messages/${messageId}?account_id=${encodeURIComponent(accountId)}`;
262
+
263
+ const result = await apiRequestJson(path, {
111
264
  method: 'GET',
112
265
  token,
113
266
  });
@@ -129,21 +282,24 @@ async function gmailRead(messageId) {
129
282
  console.log(msg.body || msg.snippet || '(no body)');
130
283
  }
131
284
 
132
- async function gmailArchive(messageIds) {
285
+ async function gmailArchive(messageIds, options = {}) {
133
286
  const ids = (messageIds || []).map(id => String(id || '').trim()).filter(Boolean);
134
287
  if (!ids.length) {
135
- console.error('Usage: atris gmail archive <message_id> [<message_id> ...]');
288
+ console.error('Usage: atris gmail archive <message_id> [<message_id> ...] [--account <id>]');
136
289
  process.exit(1);
137
290
  }
138
291
 
139
292
  const token = await getAuthToken();
293
+ const accountId = resolveGmailAccountId(options.accountId);
140
294
 
141
295
  console.log(`Archiving ${ids.length} message${ids.length === 1 ? '' : 's'}...`);
142
296
 
297
+ const body = { message_ids: ids, account_id: accountId };
298
+
143
299
  const result = await apiRequestJson('/integrations/gmail/messages/batch-archive', {
144
300
  method: 'POST',
145
301
  token,
146
- body: { message_ids: ids },
302
+ body,
147
303
  });
148
304
 
149
305
  if (!result.ok) {
@@ -155,23 +311,169 @@ async function gmailArchive(messageIds) {
155
311
  console.log(`Archived ${archived} message${archived === 1 ? '' : 's'}. They stay searchable in All Mail.`);
156
312
  }
157
313
 
314
+ function openGmailAuthUrl(authUrl, deps = {}) {
315
+ const run = deps.spawnSync || spawnSync;
316
+ const platform = deps.platform || process.platform;
317
+ const command = platform === 'darwin' ? 'open' : 'xdg-open';
318
+ const opened = run(command, [authUrl], { stdio: 'ignore' });
319
+ return !opened.error && opened.status === 0;
320
+ }
321
+
322
+ async function gmailConnect(accountName, deps = {}) {
323
+ const accountId = String(accountName || 'default').trim();
324
+ if (!/^[a-z0-9-]{1,32}$/.test(accountId)) {
325
+ console.error('invalid gmail account name. use 1 to 32 lowercase letters, numbers, or hyphens.');
326
+ process.exit(1);
327
+ }
328
+
329
+ const token = await getAuthToken();
330
+ const result = await apiRequestJson('/integrations/gmail/start', {
331
+ method: 'POST',
332
+ token,
333
+ body: { account_id: accountId },
334
+ });
335
+
336
+ if (!result.ok) {
337
+ const detail = String(result.error || 'request failed').toLowerCase();
338
+ console.error(`could not start gmail connection: ${detail}`);
339
+ process.exit(1);
340
+ }
341
+
342
+ const authUrl = String(result.data?.auth_url || result.data?.url || '').trim();
343
+ if (!authUrl) {
344
+ console.error('could not start gmail connection: the server did not return an auth url.');
345
+ process.exit(1);
346
+ }
347
+
348
+ const openAuthUrl = deps.openAuthUrl || openGmailAuthUrl;
349
+ if (!openAuthUrl(authUrl, deps)) {
350
+ console.log(`open this url in your browser: ${authUrl}`);
351
+ }
352
+
353
+ console.log(`waiting for gmail account ${accountId}...`);
354
+
355
+ const pollIntervalMs = deps.pollIntervalMs ?? GMAIL_CONNECT_POLL_MS;
356
+ const timeoutMs = deps.timeoutMs ?? GMAIL_CONNECT_TIMEOUT_MS;
357
+ const sleep = deps.sleep || ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
358
+ const maxPolls = Math.max(1, Math.ceil(timeoutMs / pollIntervalMs));
359
+
360
+ for (let poll = 0; poll < maxPolls; poll += 1) {
361
+ await sleep(pollIntervalMs);
362
+ const accountsResult = await apiRequestJson('/integrations/gmail/accounts', {
363
+ method: 'GET',
364
+ token,
365
+ });
366
+ const accounts = accountsResult.ok
367
+ ? (accountsResult.data?.accounts || accountsResult.data || [])
368
+ : [];
369
+ const match = Array.isArray(accounts) ? findGmailAccount(accounts, accountId) : null;
370
+ const email = match ? gmailAccountIdentity(match).email.trim() : '';
371
+ if (email && email !== 'unknown') {
372
+ console.log(`connected ${email} as ${accountId}`);
373
+ return;
374
+ }
375
+ }
376
+
377
+ console.error(`connection timed out. finish in your browser: ${authUrl}`);
378
+ console.error(`or connect from settings: https://atris.ai/dashboard/settings?connect=gmail&account=${accountId}`);
379
+ process.exit(1);
380
+ }
381
+
382
+ async function gmailAccounts() {
383
+ const token = await getAuthToken();
384
+ const accounts = await fetchGmailAccounts(token);
385
+ const activeId = resolveGmailAccountId(null);
386
+
387
+ if (!accounts.length) {
388
+ console.log('no connected accounts.');
389
+ return;
390
+ }
391
+
392
+ for (const line of formatGmailAccountRows(accounts, activeId)) console.log(line);
393
+ }
394
+
395
+ async function gmailUse(accountId, deps = {}) {
396
+ const token = await getAuthToken();
397
+ const accounts = await fetchGmailAccounts(token);
398
+ let selector = String(accountId || '').trim();
399
+
400
+ if (!selector) {
401
+ if (!accounts.length) {
402
+ console.log('no connected accounts.');
403
+ return;
404
+ }
405
+ if (accounts.length === 1) {
406
+ const { name, email } = gmailAccountIdentity(accounts[0]);
407
+ console.log(`${name} (${email || 'email pending'}) is already the only gmail account.`);
408
+ return;
409
+ }
410
+
411
+ const sortedAccounts = sortGmailAccounts(accounts);
412
+ const activeId = resolveGmailAccountId(null);
413
+ const rows = formatGmailAccountRows(sortedAccounts, activeId);
414
+ rows.forEach((line, index) => console.log(`${index + 1}. ${line}`));
415
+ const answer = String(await promptForGmailAccount(deps) || '').trim();
416
+ const number = /^\d+$/.test(answer) ? Number(answer) : 0;
417
+ selector = number >= 1 && number <= sortedAccounts.length
418
+ ? gmailAccountIdentity(sortedAccounts[number - 1]).id
419
+ : answer;
420
+ }
421
+
422
+ const match = findGmailAccountForUse(accounts, selector);
423
+ if (!match) {
424
+ console.error(`unknown gmail account "${selector}".`);
425
+ if (accounts.length) {
426
+ console.error('connected accounts:');
427
+ for (const account of sortGmailAccounts(accounts)) {
428
+ const { id, email, name } = gmailAccountIdentity(account);
429
+ console.error(` ${id.toLowerCase()}: ${email || 'email pending'}, ${name}`);
430
+ }
431
+ } else {
432
+ console.error('no connected accounts.');
433
+ }
434
+ process.exit(1);
435
+ }
436
+
437
+ const { id, name, email } = gmailAccountIdentity(match);
438
+ writeGmailStickyAccount(id);
439
+ console.log(`now using ${name} (${email || 'email pending'})`);
440
+ }
441
+
158
442
  async function gmailCommand(subcommand, ...args) {
159
443
  switch (subcommand) {
160
444
  case 'inbox':
161
- case 'list':
162
- await gmailInbox();
445
+ case 'list': {
446
+ const parsed = parseGmailArgs(args);
447
+ await gmailInbox({ accountId: parsed.accountId || undefined });
163
448
  break;
164
- case 'read':
165
- await gmailRead(args[0]);
449
+ }
450
+ case 'read': {
451
+ const parsed = parseGmailArgs(args);
452
+ await gmailRead(parsed.positional[0], { accountId: parsed.accountId || undefined });
166
453
  break;
167
- case 'archive':
168
- await gmailArchive(args);
454
+ }
455
+ case 'archive': {
456
+ const parsed = parseGmailArgs(args);
457
+ await gmailArchive(parsed.positional, { accountId: parsed.accountId || undefined });
458
+ break;
459
+ }
460
+ case 'connect':
461
+ await gmailConnect(args[0]);
462
+ break;
463
+ case 'accounts':
464
+ await gmailAccounts();
465
+ break;
466
+ case 'use':
467
+ await gmailUse(args.join(' '));
169
468
  break;
170
469
  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)');
470
+ console.log('gmail commands:');
471
+ console.log(' atris gmail inbox [--account <id>] - list recent emails for a mailbox');
472
+ console.log(' atris gmail read <id> [--account <id>] - read specific email');
473
+ console.log(' atris gmail archive <id> [...] [--account <id>] - archive messages (reversible, all mail keeps them)');
474
+ console.log(' atris gmail connect [name] - connect or reconnect a gmail account');
475
+ console.log(' atris gmail accounts - list connected gmail accounts and the active account');
476
+ console.log(' atris gmail use [<account_id|name>] - choose or set the active gmail account');
175
477
  }
176
478
  }
177
479
 
@@ -228,7 +530,8 @@ function formatCalendarTimeRange(event) {
228
530
 
229
531
  function formatCalendarEvents(label, events) {
230
532
  if (events.length === 0) {
231
- console.log(`No events ${label}. 🎉`);
533
+ // label is already a phrase like "today's events" or "events on 2026-08-24"
534
+ console.log(`No ${label}.`);
232
535
  return;
233
536
  }
234
537
 
@@ -599,7 +902,15 @@ async function calendarCommand(subcommand, ...args) {
599
902
  // ============================================================================
600
903
 
601
904
  async function twitterPost(args = []) {
602
- const outbound = prepareOutboundSendTextOrExit('twitter', args);
905
+ const approved = args.includes('--approved') || args.includes('--confirm-approved');
906
+ const cleaned = args.filter((a) => a !== '--approved' && a !== '--confirm-approved');
907
+ if (!approved) {
908
+ console.error('Refusing to post without --approved after the exact text is confirmed.');
909
+ console.error('Usage: atris twitter post "<text>" --approved');
910
+ process.exit(2);
911
+ }
912
+
913
+ const outbound = prepareOutboundSendTextOrExit('twitter', cleaned);
603
914
  let text = outbound.body;
604
915
  if (!text) {
605
916
  const readline = require('readline');
@@ -658,7 +969,7 @@ async function twitterCommand(subcommand, ...args) {
658
969
  break;
659
970
  default:
660
971
  console.log('Twitter commands:');
661
- console.log(' atris twitter post [text] - Post a tweet');
972
+ console.log(' atris twitter post "<text>" --approved - Post a tweet');
662
973
  }
663
974
  }
664
975
 
@@ -834,9 +1145,16 @@ async function slackSearch(query, args = []) {
834
1145
 
835
1146
  async function slackSend(channel, textParts) {
836
1147
  const target = String(channel || '').trim();
837
- const text = prepareOutboundSendTextOrExit('slack', textParts).body;
1148
+ const approved = (textParts || []).includes('--approved') || (textParts || []).includes('--confirm-approved');
1149
+ const cleaned = (textParts || []).filter((a) => a !== '--approved' && a !== '--confirm-approved');
1150
+ if (!approved) {
1151
+ console.error('Refusing to send without --approved after the exact channel and exact text are confirmed.');
1152
+ console.error('Usage: atris slack send <channel> "<message>" --approved');
1153
+ process.exit(2);
1154
+ }
1155
+ const text = prepareOutboundSendTextOrExit('slack', cleaned).body;
838
1156
  if (!target || !text) {
839
- console.error('Usage: atris slack send <channel> "<message>"');
1157
+ console.error('Usage: atris slack send <channel> "<message>" --approved');
840
1158
  process.exit(1);
841
1159
  }
842
1160
 
@@ -860,9 +1178,16 @@ async function slackSend(channel, textParts) {
860
1178
 
861
1179
  async function slackDm(userId, textParts) {
862
1180
  const target = String(userId || '').trim();
863
- const text = prepareOutboundSendTextOrExit('slack', textParts).body;
1181
+ const approved = (textParts || []).includes('--approved') || (textParts || []).includes('--confirm-approved');
1182
+ const cleaned = (textParts || []).filter((a) => a !== '--approved' && a !== '--confirm-approved');
1183
+ if (!approved) {
1184
+ console.error('Refusing to send without --approved after the exact recipient and exact text are confirmed.');
1185
+ console.error('Usage: atris slack dm <slack_user_id> "<message>" --approved');
1186
+ process.exit(2);
1187
+ }
1188
+ const text = prepareOutboundSendTextOrExit('slack', cleaned).body;
864
1189
  if (!target || !text) {
865
- console.error('Usage: atris slack dm <slack_user_id> "<message>"');
1190
+ console.error('Usage: atris slack dm <slack_user_id> "<message>" --approved');
866
1191
  process.exit(1);
867
1192
  }
868
1193
 
@@ -912,8 +1237,8 @@ async function slackCommand(subcommand, ...args) {
912
1237
  console.log(' atris slack dms - List Slack DMs');
913
1238
  console.log(' atris slack messages <channel> - Read recent messages');
914
1239
  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');
1240
+ console.log(' atris slack send <channel> "<message>" --approved - Send a message as you');
1241
+ console.log(' atris slack dm <user_id> "<message>" --approved - DM someone as you');
917
1242
  }
918
1243
  }
919
1244
 
@@ -1588,10 +1913,41 @@ async function imessageCommand(subcommand, ...args) {
1588
1913
  // STATUS
1589
1914
  // ============================================================================
1590
1915
 
1591
- async function integrationsStatus() {
1916
+ async function integrationsStatus(args = []) {
1917
+ const { parseScopeFlag } = require('../lib/cli-scope');
1918
+ const scope = parseScopeFlag(args);
1919
+ const json = scope.args.includes('--json') || args.includes('--json');
1920
+
1921
+ if (!scope.global) {
1922
+ const localPath = path.join(process.cwd(), '.atris', 'integrations.json');
1923
+ let local = null;
1924
+ try {
1925
+ if (fs.existsSync(localPath)) local = JSON.parse(fs.readFileSync(localPath, 'utf8'));
1926
+ } catch {}
1927
+ const payload = {
1928
+ scope: 'workspace',
1929
+ workspace_root: process.cwd(),
1930
+ integrations: Array.isArray(local?.integrations) ? local.integrations : [],
1931
+ note: 'workspace scope only. pass --global for account-level mail/chat/code hosts.',
1932
+ };
1933
+ if (json) {
1934
+ console.log(JSON.stringify(payload, null, 2));
1935
+ return 0;
1936
+ }
1937
+ console.log('Integration Status (workspace)\n');
1938
+ if (!payload.integrations.length) {
1939
+ console.log(' (none configured in this workspace)');
1940
+ } else {
1941
+ for (const name of payload.integrations) console.log(` • ${name}`);
1942
+ }
1943
+ console.log('\nPass --global to check account-level gmail/slack/github status.');
1944
+ return 0;
1945
+ }
1946
+
1592
1947
  const token = await getAuthToken();
1948
+ const rows = [];
1593
1949
 
1594
- console.log('🔌 Integration Status:\n');
1950
+ console.log('🔌 Integration Status (global):\n');
1595
1951
 
1596
1952
  const integrations = ['gmail', 'google-calendar', 'slack', 'twitter', 'github'];
1597
1953
 
@@ -1606,19 +1962,31 @@ async function integrationsStatus() {
1606
1962
  const icon = connected ? '✅' : '❌';
1607
1963
  const displayName = name.replace('google-', '').replace('-', ' ');
1608
1964
  console.log(` ${icon} ${displayName}`);
1965
+ rows.push({ name, connected: Boolean(connected) });
1609
1966
  } catch {
1610
1967
  console.log(` ❓ ${name}`);
1968
+ rows.push({ name, connected: null });
1611
1969
  }
1612
1970
  }
1613
1971
 
1614
1972
  const imessage = imessageDoctor();
1615
1973
  console.log(` ${imessage.connected ? '✅' : '❌'} iMessage (local Mac)`);
1974
+ rows.push({ name: 'imessage', connected: Boolean(imessage.connected) });
1616
1975
 
1617
1976
  console.log('\nConnect integrations at: https://atris.ai/dashboard/settings');
1977
+ if (json) {
1978
+ console.log(JSON.stringify({ scope: 'global', integrations: rows }, null, 2));
1979
+ }
1980
+ return 0;
1618
1981
  }
1619
1982
 
1620
1983
  module.exports = {
1621
1984
  gmailCommand,
1985
+ gmailConnect,
1986
+ gmailUse,
1987
+ extractGmailMailboxAccount,
1988
+ parseGmailArgs,
1989
+ gmailAccountStatePath,
1622
1990
  calendarCommand,
1623
1991
  twitterCommand,
1624
1992
  slackCommand,
@@ -1627,6 +1995,7 @@ module.exports = {
1627
1995
  integrationsStatus,
1628
1996
  parseCalendarSearchArgs,
1629
1997
  filterCalendarEventsForSearch,
1998
+ formatCalendarEvents,
1630
1999
  formatCalendarSearchResults,
1631
2000
  formatCalendarSearchError,
1632
2001
  };
@@ -153,6 +153,14 @@ async function interviewCommand(args = [], root = process.cwd()) {
153
153
  return 0;
154
154
  }
155
155
 
156
+ const { refuseHeadlessUnless } = require('../lib/noninteractive');
157
+ if (refuseHeadlessUnless(args, {
158
+ allowYes: true,
159
+ usage: 'Usage: atris interview [subject] [--yes]\nHeadless interview needs --yes.',
160
+ })) {
161
+ return 2;
162
+ }
163
+
156
164
  const ws = findWorkspaceRoot(root);
157
165
  if (!ws) {
158
166
  console.error('✗ Not inside an Atris workspace (no atris/ directory found).');