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.
- package/FOR_AGENTS.md +7 -0
- package/README.md +1 -1
- package/atris/policies/ANTISLOP.md +2 -1
- package/atris/skills/copy-editor/SKILL.md +2 -1
- package/atris/skills/design/SKILL.md +1 -1
- package/atris/skills/x-search/SKILL.md +24 -22
- package/atris/skills/youtube/SKILL.md +82 -33
- package/atris/team/_template/SOUL.md +40 -0
- package/atris/team/validator/MEMBER.md +1 -2
- package/bin/atris +21 -0
- package/bin/atris.js +563 -373
- package/commands/api-key.js +170 -0
- package/commands/auth.js +453 -43
- package/commands/autoland.js +3 -1
- package/commands/autopilot-front.js +58 -11
- package/commands/avail.js +5 -4
- package/commands/balance.js +55 -0
- package/commands/bench.js +29 -1
- package/commands/brain.js +63 -12
- package/commands/brainstorm.js +18 -0
- package/commands/business.js +149 -1
- package/commands/chat-scan.js +17 -4
- package/commands/close.js +5 -1
- package/commands/doctor.js +174 -0
- package/commands/dream.js +7 -0
- package/commands/drill.js +34 -2
- package/commands/engine.js +47 -35
- package/commands/errors.js +58 -8
- package/commands/experiments.js +14 -0
- package/commands/feedback.js +61 -2
- package/commands/fleet-report.js +27 -7
- package/commands/fleet.js +98 -11
- package/commands/founder.js +60 -21
- package/commands/human-missions.js +79 -4
- package/commands/improve.js +54 -4
- package/commands/init.js +68 -92
- package/commands/install.js +40 -0
- package/commands/integrations.js +565 -30
- package/commands/interview.js +8 -0
- package/commands/land.js +48 -17
- package/commands/learn.js +29 -4
- package/commands/log.js +51 -0
- package/commands/member.js +68 -10
- package/commands/mission.js +287 -50
- package/commands/now.js +15 -3
- package/commands/orb.js +97 -2
- package/commands/pack.js +83 -9
- package/commands/playbook.js +381 -0
- package/commands/plugin.js +16 -0
- package/commands/radar.js +31 -7
- package/commands/recap.js +128 -21
- package/commands/release.js +102 -2
- package/commands/review.js +5 -10
- package/commands/revisions.js +55 -0
- package/commands/run-front.js +12 -4
- package/commands/scout.js +12 -1
- package/commands/search.js +123 -28
- package/commands/sign.js +2 -2
- package/commands/signup.js +12 -3
- package/commands/site-deploy.js +17 -4
- package/commands/site.js +2 -2
- package/commands/skill-eval.js +289 -0
- package/commands/skill.js +56 -6
- package/commands/social.js +603 -0
- package/commands/spaceship.js +69 -6
- package/commands/stream.js +8 -1
- package/commands/study.js +32 -19
- package/commands/sync.js +28 -22
- package/commands/task.js +435 -60
- package/commands/teach.js +72 -63
- package/commands/terminal.js +25 -9
- package/commands/topup.js +88 -0
- package/commands/truth.js +52 -6
- package/commands/usage.js +91 -0
- package/commands/verify.js +3 -2
- package/commands/version.js +3 -1
- package/commands/voice.js +5 -1
- package/commands/who.js +69 -11
- package/commands/wiki.js +7 -4
- package/commands/wish.js +76 -5
- package/commands/workflow.js +327 -145
- package/commands/worktree.js +1 -0
- package/commands/write.js +5 -0
- package/commands/x-search.js +340 -0
- package/commands/xp.js +5 -1
- package/commands/youtube.js +551 -25
- package/lib/account-bound.js +47 -0
- package/lib/auto-accept-certified.js +8 -1
- package/lib/auto-lessons.js +186 -0
- package/lib/autoland.js +4 -2
- package/lib/chat-log-scan.js +7 -4
- package/lib/cli-json.js +77 -0
- package/lib/cli-scope.js +26 -0
- package/lib/codex-flight.js +1 -0
- package/lib/conductor-artifacts.js +1 -1
- package/lib/context-gatherer.js +11 -0
- package/lib/developer-api.js +116 -0
- package/lib/feedback-triage.js +274 -0
- package/lib/first-minute.js +398 -0
- package/lib/fleet.js +115 -34
- package/lib/functional-owner.js +22 -0
- package/lib/known-commands.js +31 -5
- package/lib/member-scaffold.js +197 -0
- package/lib/mission-root.js +4 -2
- package/lib/noninteractive.js +88 -0
- package/lib/policy-lessons.js +4 -1
- package/lib/revision-metric.js +279 -0
- package/lib/scratch-root.js +48 -0
- package/lib/skill-eval-gate.js +249 -0
- package/lib/task-db.js +4 -0
- package/lib/task-proof.js +69 -8
- package/lib/verifier-quality.js +69 -0
- package/lib/wish-audit.js +2 -2
- package/lib/wish-delegate.js +10 -1
- package/lib/workspace-scaffold.js +270 -0
- package/package.json +3 -2
- package/scripts/det/checklist-score.js +191 -0
- package/scripts/det/ytsearch +31 -0
- package/utils/update-check.js +15 -0
package/commands/integrations.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
598
|
+
case 'list': {
|
|
599
|
+
const parsed = parseGmailArgs(args);
|
|
600
|
+
await gmailInbox({ accountId: parsed.accountId || undefined });
|
|
163
601
|
break;
|
|
164
|
-
|
|
165
|
-
|
|
602
|
+
}
|
|
603
|
+
case 'read': {
|
|
604
|
+
const parsed = parseGmailArgs(args);
|
|
605
|
+
await gmailRead(parsed.positional[0], { accountId: parsed.accountId || undefined });
|
|
166
606
|
break;
|
|
167
|
-
|
|
168
|
-
|
|
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('
|
|
172
|
-
console.log(' atris gmail inbox -
|
|
173
|
-
console.log(' atris gmail read <id> -
|
|
174
|
-
console.log(' atris gmail archive <id> [...] -
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
};
|