atris 3.55.0 → 3.56.1
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/atris/skills/engines/SKILL.md +11 -3
- package/atris/skills/x-search/SKILL.md +20 -1
- package/atris/skills/youtube/SKILL.md +51 -43
- package/bin/atris.js +119 -87
- package/commands/autopilot-front.js +29 -13
- package/commands/brainstorm.js +77 -476
- package/commands/business.js +13 -1
- package/commands/fleet-report.js +2 -2
- package/commands/human-missions.js +26 -2
- package/commands/init.js +2 -35
- package/commands/integrations.js +266 -0
- package/commands/land.js +53 -23
- package/commands/later.js +52 -0
- package/commands/log.js +79 -30
- package/commands/mission.js +117 -22
- package/commands/next.js +153 -63
- package/commands/now.js +80 -38
- package/commands/recap.js +66 -4
- package/commands/run-front.js +20 -5
- package/commands/spaceship.js +52 -12
- package/commands/status.js +30 -7
- package/commands/task.js +70 -16
- package/commands/terminal.js +5 -5
- package/commands/workflow.js +18 -5
- package/commands/x-search.js +123 -13
- package/commands/youtube.js +639 -36
- package/lib/account-bound.js +7 -0
- package/lib/apply-gate.js +94 -0
- package/lib/context-gatherer.js +55 -8
- package/lib/engine-ask.js +16 -6
- package/lib/first-minute.js +552 -26
- package/lib/known-commands.js +1 -1
- package/lib/runner-command.js +5 -3
- package/lib/scratch-root.js +44 -0
- package/package.json +1 -1
- package/utils/auth.js +9 -0
package/commands/business.js
CHANGED
|
@@ -3277,11 +3277,14 @@ async function deployBusiness(slug) {
|
|
|
3277
3277
|
// Upload workspace files
|
|
3278
3278
|
const workspaceDir = path.join(bizDir, 'workspace');
|
|
3279
3279
|
let uploadCount = 0;
|
|
3280
|
+
let uploadFailureCount = 0;
|
|
3281
|
+
let uploadTotal = 0;
|
|
3280
3282
|
if (fs.existsSync(workspaceDir)) {
|
|
3281
3283
|
const files = walkDir(workspaceDir);
|
|
3282
3284
|
for (const filePath of files) {
|
|
3283
3285
|
const relativePath = path.relative(workspaceDir, filePath);
|
|
3284
3286
|
if (relativePath.startsWith('.')) continue;
|
|
3287
|
+
uploadTotal++;
|
|
3285
3288
|
try {
|
|
3286
3289
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
3287
3290
|
const uploadResult = await apiRequestJson(
|
|
@@ -3291,9 +3294,14 @@ async function deployBusiness(slug) {
|
|
|
3291
3294
|
if (uploadResult.ok) {
|
|
3292
3295
|
uploadCount++;
|
|
3293
3296
|
process.stdout.write(` Uploaded: ${relativePath}\n`);
|
|
3297
|
+
} else {
|
|
3298
|
+
uploadFailureCount++;
|
|
3299
|
+
const detail = uploadResult.errorMessage || uploadResult.error || uploadResult.status || 'unknown error';
|
|
3300
|
+
console.log(` Failed: ${relativePath} (${detail})`);
|
|
3294
3301
|
}
|
|
3295
3302
|
} catch (e) {
|
|
3296
|
-
|
|
3303
|
+
uploadFailureCount++;
|
|
3304
|
+
console.log(` Failed: ${relativePath} (${e.message || String(e)})`);
|
|
3297
3305
|
}
|
|
3298
3306
|
}
|
|
3299
3307
|
}
|
|
@@ -3314,6 +3322,10 @@ async function deployBusiness(slug) {
|
|
|
3314
3322
|
|
|
3315
3323
|
console.log(`\n Deployed ${uploadCount} files to ${bizConfig.name}`);
|
|
3316
3324
|
console.log(` Dashboard: https://atris.ai/dashboard/gm/${bizConfig.business_id}`);
|
|
3325
|
+
if (uploadFailureCount > 0) {
|
|
3326
|
+
console.log(` ${uploadFailureCount} of ${uploadTotal} files failed to upload`);
|
|
3327
|
+
process.exitCode = 1;
|
|
3328
|
+
}
|
|
3317
3329
|
console.log('');
|
|
3318
3330
|
}
|
|
3319
3331
|
|
package/commands/fleet-report.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
const { loadCredentials } = require('../utils/auth');
|
|
17
17
|
const { apiRequestJson } = require('../utils/api');
|
|
18
|
-
const { requireAccountBound, refuseAccountGlobal } = require('../lib/account-bound');
|
|
18
|
+
const { looksLikeBusinessSlug, requireAccountBound, refuseAccountGlobal } = require('../lib/account-bound');
|
|
19
19
|
const { argsWantHelp, wantsJson } = require('../lib/noninteractive');
|
|
20
20
|
|
|
21
21
|
function sleep(ms) {
|
|
@@ -181,7 +181,7 @@ async function fleetReport() {
|
|
|
181
181
|
const wake = gate.args.includes('--wake');
|
|
182
182
|
const dryRun = gate.args.includes('--dry-run');
|
|
183
183
|
const board = gate.args.includes('--leaderboard');
|
|
184
|
-
const slug = gate.args.find((a) =>
|
|
184
|
+
const slug = gate.args.find((a) => looksLikeBusinessSlug(a));
|
|
185
185
|
|
|
186
186
|
if (!allAlive && !slug && !board) {
|
|
187
187
|
console.log(usage);
|
|
@@ -14,7 +14,7 @@ const {
|
|
|
14
14
|
const { apiRequestJson } = require('../utils/api');
|
|
15
15
|
const { decodeJwtClaims, loadCredentials } = require('../utils/auth');
|
|
16
16
|
const { isHelpToken } = require('../lib/noninteractive');
|
|
17
|
-
const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
|
|
17
|
+
const { isFreshWorkspace, speakFirstMinute, speakNothingRunning } = require('../lib/first-minute');
|
|
18
18
|
|
|
19
19
|
const PACKAGE_PATH = path.join(__dirname, '..', 'package.json');
|
|
20
20
|
const HUMAN_STATES = Object.freeze({
|
|
@@ -168,7 +168,8 @@ function parseMissionId(args = []) {
|
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
170
|
* Scratch / unbound folders must not drive account-current cloud work.
|
|
171
|
-
*
|
|
171
|
+
* Empty-folder stop talks first-talk instead. After a room exists,
|
|
172
|
+
* require an explicit mission/task id: atris stop --mission <id>
|
|
172
173
|
*/
|
|
173
174
|
function refuseUnboundCloudComputer(args = [], options = {}, commandName = 'stop') {
|
|
174
175
|
const root = options.root || process.cwd();
|
|
@@ -634,6 +635,15 @@ async function currentMissionCommand(args = [], options = {}) {
|
|
|
634
635
|
outputCard(card, asJson, options.log || console.log);
|
|
635
636
|
return 0;
|
|
636
637
|
} catch (caught) {
|
|
638
|
+
if (options.fallbackOnMissing) {
|
|
639
|
+
const code = speakFirstMinute({
|
|
640
|
+
root: options.root || process.cwd(),
|
|
641
|
+
asJson,
|
|
642
|
+
log: options.log || console.log,
|
|
643
|
+
});
|
|
644
|
+
if (options.setProcessExitCode !== false) process.exitCode = code;
|
|
645
|
+
return code;
|
|
646
|
+
}
|
|
637
647
|
return reportError(caught, 'show the current mission', asJson, options);
|
|
638
648
|
}
|
|
639
649
|
}
|
|
@@ -734,6 +744,20 @@ async function stopCommand(args, options = {}) {
|
|
|
734
744
|
(options.log || console.log)('Usage: atris stop [--mission <id>] [--json]');
|
|
735
745
|
return 0;
|
|
736
746
|
}
|
|
747
|
+
const root = options.root || process.cwd();
|
|
748
|
+
const asJson = args.includes('--json');
|
|
749
|
+
// Fresh folder: empty talks first-talk. A file already here
|
|
750
|
+
// names that file, same as bare atris. Do not mint a room.
|
|
751
|
+
if (isFreshWorkspace(root) && !parseMissionId(args)) {
|
|
752
|
+
const unbound = refuseUnboundCloudComputer(args, options, 'stop');
|
|
753
|
+
if (unbound) {
|
|
754
|
+
return speakNothingRunning({
|
|
755
|
+
root,
|
|
756
|
+
asJson,
|
|
757
|
+
log: options.log || console.log,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
737
761
|
return changeCurrentMission('stop', {}, args, options);
|
|
738
762
|
}
|
|
739
763
|
|
package/commands/init.js
CHANGED
|
@@ -468,41 +468,8 @@ function initAtris() {
|
|
|
468
468
|
}
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
if (!fs.existsSync(intuitionFile)) {
|
|
474
|
-
fs.writeFileSync(intuitionFile, `# INTUITION.md
|
|
475
|
-
|
|
476
|
-
> Accumulated learnings. Read before major decisions. Update after discoveries.
|
|
477
|
-
|
|
478
|
-
---
|
|
479
|
-
|
|
480
|
-
## Tripwires
|
|
481
|
-
|
|
482
|
-
*Things that seem obvious but break unexpectedly. Check these first when debugging.*
|
|
483
|
-
|
|
484
|
-
- (none yet — add when you hit surprising failures)
|
|
485
|
-
|
|
486
|
-
---
|
|
487
|
-
|
|
488
|
-
## Preferences
|
|
489
|
-
|
|
490
|
-
*Patterns this codebase prefers. Follow these over generic best practices.*
|
|
491
|
-
|
|
492
|
-
- (none yet — add as you discover the codebase style)
|
|
493
|
-
|
|
494
|
-
---
|
|
495
|
-
|
|
496
|
-
## Dead Ends
|
|
497
|
-
|
|
498
|
-
*Approaches tried and abandoned. Don't retry without new information.*
|
|
499
|
-
|
|
500
|
-
- (none yet — log failed approaches so future agents skip them)
|
|
501
|
-
|
|
502
|
-
---
|
|
503
|
-
`);
|
|
504
|
-
markReady('workspace', 'INTUITION.md', '✓ Created INTUITION.md');
|
|
505
|
-
}
|
|
471
|
+
// INTUITION.md was retired into lessons.md. Do not write a parallel lesson file.
|
|
472
|
+
// Leave a user's existing INTUITION.md alone if one is already on disk.
|
|
506
473
|
|
|
507
474
|
// Create lessons.md (feedback loop for learning across features)
|
|
508
475
|
const lessonsFile = path.join(targetDir, 'lessons.md');
|
package/commands/integrations.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* atris gmail inbox [--account <id>] - List recent emails for a mailbox
|
|
6
6
|
* atris gmail read <id> [--account <id>] - Read specific email
|
|
7
7
|
* atris gmail archive <id> [...] [--account <id>] - Archive messages
|
|
8
|
+
* atris gmail verdicts [--account <id>] [--limit N] - List recent archive verdicts
|
|
9
|
+
* atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>] - Send an email
|
|
10
|
+
* atris gmail voice [account] [--clear] - Edit an account's writing voice
|
|
8
11
|
* atris gmail connect [name] - Connect or reconnect a Gmail account
|
|
9
12
|
* atris gmail accounts - List connected Gmail accounts and the active account
|
|
10
13
|
* atris gmail use [<account_id|name>] - Choose or set the active Gmail account
|
|
@@ -35,6 +38,79 @@ const { spawnSync } = require('child_process');
|
|
|
35
38
|
const CALENDAR_CACHE_PATH = path.join(os.homedir(), '.atris', 'calendar-events-cache.json');
|
|
36
39
|
const GMAIL_CONNECT_POLL_MS = 3000;
|
|
37
40
|
const GMAIL_CONNECT_TIMEOUT_MS = 3 * 60 * 1000;
|
|
41
|
+
const GMAIL_SEND_USAGE = 'usage: atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>]';
|
|
42
|
+
const GMAIL_VOICE_USAGE = 'usage: atris gmail voice [account] [--clear]';
|
|
43
|
+
const GMAIL_VERDICTS_USAGE = 'usage: atris gmail verdicts [--account <id>] [--limit N]';
|
|
44
|
+
|
|
45
|
+
function gmailVerdictsPath(root = process.cwd()) {
|
|
46
|
+
return path.join(root, '.atris', 'state', 'gmail-verdicts.jsonl');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function appendGmailVerdicts(verdicts, options = {}) {
|
|
50
|
+
const rows = Array.isArray(verdicts) ? verdicts : [];
|
|
51
|
+
if (!rows.length) return;
|
|
52
|
+
const filePath = options.filePath || gmailVerdictsPath(options.root);
|
|
53
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
54
|
+
const lines = rows.map((row) => JSON.stringify({
|
|
55
|
+
ts: row.ts || new Date().toISOString(),
|
|
56
|
+
account: String(row.account || ''),
|
|
57
|
+
verdict: 'archive',
|
|
58
|
+
message_id: String(row.message_id || ''),
|
|
59
|
+
...(row.from ? { from: row.from } : {}),
|
|
60
|
+
...(row.subject ? { subject: row.subject } : {}),
|
|
61
|
+
}));
|
|
62
|
+
fs.appendFileSync(filePath, `${lines.join('\n')}\n`, 'utf8');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readGmailVerdicts(options = {}) {
|
|
66
|
+
const filePath = options.filePath || gmailVerdictsPath(options.root);
|
|
67
|
+
let lines;
|
|
68
|
+
try {
|
|
69
|
+
lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error.code === 'ENOENT') return [];
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
const account = String(options.account || '').trim();
|
|
75
|
+
const limit = Number.isInteger(options.limit) ? options.limit : 20;
|
|
76
|
+
return lines.reduce((rows, line) => {
|
|
77
|
+
try {
|
|
78
|
+
const row = JSON.parse(line);
|
|
79
|
+
if (!account || row.account === account) rows.push(row);
|
|
80
|
+
} catch {}
|
|
81
|
+
return rows;
|
|
82
|
+
}, []).reverse().slice(0, limit);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function printGmailVerdicts(options = {}) {
|
|
86
|
+
const rows = readGmailVerdicts(options);
|
|
87
|
+
if (!rows.length) {
|
|
88
|
+
console.log('no gmail verdicts found.');
|
|
89
|
+
return rows;
|
|
90
|
+
}
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
const details = [row.from && `from ${row.from}`, row.subject && `subject ${row.subject}`].filter(Boolean);
|
|
93
|
+
console.log(`${row.ts} archive ${row.message_id} account ${row.account}${details.length ? `, ${details.join(', ')}` : ''}`);
|
|
94
|
+
}
|
|
95
|
+
return rows;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseGmailVerdictsArgs(args = []) {
|
|
99
|
+
let account = null;
|
|
100
|
+
let limit = 20;
|
|
101
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
102
|
+
const arg = args[i];
|
|
103
|
+
const value = String(args[i + 1] || '').trim();
|
|
104
|
+
if (arg === '--account' && value && !value.startsWith('--')) account = value;
|
|
105
|
+
else if (arg === '--limit' && /^\d+$/.test(value) && Number(value) > 0) limit = Number(value);
|
|
106
|
+
else {
|
|
107
|
+
console.error(GMAIL_VERDICTS_USAGE);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
i += 1;
|
|
111
|
+
}
|
|
112
|
+
return { account, limit };
|
|
113
|
+
}
|
|
38
114
|
|
|
39
115
|
function gmailAccountStatePath() {
|
|
40
116
|
return process.env.ATRIS_GMAIL_ACCOUNT_FILE
|
|
@@ -308,9 +384,176 @@ async function gmailArchive(messageIds, options = {}) {
|
|
|
308
384
|
}
|
|
309
385
|
|
|
310
386
|
const archived = result.data?.archived ?? result.data?.count ?? ids.length;
|
|
387
|
+
const responseMessages = [
|
|
388
|
+
result.data?.messages,
|
|
389
|
+
result.data?.archived_messages,
|
|
390
|
+
result.data?.results,
|
|
391
|
+
].find(Array.isArray) || [];
|
|
392
|
+
const metadata = new Map(responseMessages.map((message) => [
|
|
393
|
+
String(message.id || message.message_id || ''),
|
|
394
|
+
message,
|
|
395
|
+
]));
|
|
396
|
+
appendGmailVerdicts(ids.map((messageId) => {
|
|
397
|
+
const message = metadata.get(messageId) || {};
|
|
398
|
+
return {
|
|
399
|
+
account: accountId,
|
|
400
|
+
message_id: messageId,
|
|
401
|
+
from: message.from || message.sender || undefined,
|
|
402
|
+
subject: message.subject || undefined,
|
|
403
|
+
};
|
|
404
|
+
}));
|
|
311
405
|
console.log(`Archived ${archived} message${archived === 1 ? '' : 's'}. They stay searchable in All Mail.`);
|
|
312
406
|
}
|
|
313
407
|
|
|
408
|
+
function parseGmailSendArgs(args = []) {
|
|
409
|
+
let accountId = null;
|
|
410
|
+
let bodyFile = null;
|
|
411
|
+
const positional = [];
|
|
412
|
+
|
|
413
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
414
|
+
const arg = args[i];
|
|
415
|
+
if (arg === '--account' || arg === '--body-file') {
|
|
416
|
+
const value = String(args[i + 1] || '').trim();
|
|
417
|
+
if (!value || value.startsWith('--')) {
|
|
418
|
+
console.error(GMAIL_SEND_USAGE);
|
|
419
|
+
process.exit(2);
|
|
420
|
+
}
|
|
421
|
+
if (arg === '--account') accountId = value;
|
|
422
|
+
else bodyFile = value;
|
|
423
|
+
i += 1;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
positional.push(arg);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const [to, subject, ...bodyParts] = positional;
|
|
430
|
+
if (!String(to || '').trim()
|
|
431
|
+
|| !String(subject || '').trim()
|
|
432
|
+
|| (bodyFile ? bodyParts.length > 0 : bodyParts.length === 0)) {
|
|
433
|
+
console.error(GMAIL_SEND_USAGE);
|
|
434
|
+
process.exit(2);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let body = bodyParts.join(' ');
|
|
438
|
+
if (bodyFile) {
|
|
439
|
+
try {
|
|
440
|
+
body = fs.readFileSync(bodyFile, 'utf8');
|
|
441
|
+
} catch (error) {
|
|
442
|
+
console.error(`could not read body file "${bodyFile}": ${error.message}`);
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return { to, subject, body, accountId };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function gmailSend(to, subject, body, options = {}) {
|
|
451
|
+
const token = await getAuthToken();
|
|
452
|
+
const accountId = resolveGmailAccountId(options.accountId);
|
|
453
|
+
const accounts = await fetchGmailAccounts(token);
|
|
454
|
+
const account = findGmailAccount(accounts, accountId);
|
|
455
|
+
|
|
456
|
+
if (!account) {
|
|
457
|
+
console.error(`gmail account "${accountId}" is not connected.`);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const result = await apiRequestJson('/integrations/gmail/send', {
|
|
462
|
+
method: 'POST',
|
|
463
|
+
token,
|
|
464
|
+
body: { to, subject, body, account_id: accountId },
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
if (!result.ok) {
|
|
468
|
+
console.error(`could not send gmail message: ${result.error || 'request failed'}`);
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const { email, id } = gmailAccountIdentity(account);
|
|
473
|
+
console.log(`sent to ${to} from ${email || id}`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function parseGmailVoiceArgs(args = []) {
|
|
477
|
+
let clear = false;
|
|
478
|
+
const positional = [];
|
|
479
|
+
for (const arg of args) {
|
|
480
|
+
if (arg === '--clear') clear = true;
|
|
481
|
+
else positional.push(arg);
|
|
482
|
+
}
|
|
483
|
+
if (positional.length > 1 || positional.some((arg) => String(arg).startsWith('--'))) {
|
|
484
|
+
console.error(GMAIL_VOICE_USAGE);
|
|
485
|
+
process.exit(2);
|
|
486
|
+
}
|
|
487
|
+
return { accountId: positional[0] || null, clear };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function editGmailVoice(currentVoice, deps = {}) {
|
|
491
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-gmail-voice-'));
|
|
492
|
+
const voicePath = path.join(tempDir, 'voice.md');
|
|
493
|
+
let editedVoice;
|
|
494
|
+
let editorError = null;
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
fs.writeFileSync(voicePath, currentVoice, 'utf8');
|
|
498
|
+
const editor = deps.editor || process.env.EDITOR || 'vi';
|
|
499
|
+
const shell = deps.shell || process.env.SHELL || '/bin/sh';
|
|
500
|
+
const run = deps.spawnSync || spawnSync;
|
|
501
|
+
const result = run(shell, ['-c', 'exec $EDITOR "$1"', 'atris-gmail-voice', voicePath], {
|
|
502
|
+
stdio: 'inherit',
|
|
503
|
+
env: { ...process.env, EDITOR: editor },
|
|
504
|
+
});
|
|
505
|
+
if (result.error || result.status !== 0) {
|
|
506
|
+
editorError = result.error?.message || `editor exited with status ${result.status}`;
|
|
507
|
+
} else {
|
|
508
|
+
editedVoice = fs.readFileSync(voicePath, 'utf8');
|
|
509
|
+
}
|
|
510
|
+
} finally {
|
|
511
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (editorError) {
|
|
515
|
+
console.error(`could not edit gmail voice: ${editorError}`);
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
return editedVoice;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function gmailVoice(accountId, options = {}) {
|
|
522
|
+
const token = await getAuthToken();
|
|
523
|
+
const resolvedAccountId = resolveGmailAccountId(accountId);
|
|
524
|
+
const accounts = await fetchGmailAccounts(token);
|
|
525
|
+
const account = findGmailAccount(accounts, resolvedAccountId);
|
|
526
|
+
|
|
527
|
+
if (!account) {
|
|
528
|
+
console.error(`gmail account "${resolvedAccountId}" is not connected.`);
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
let voiceMd = '';
|
|
533
|
+
if (!options.clear) {
|
|
534
|
+
const stdinIsTTY = options.stdinIsTTY === undefined
|
|
535
|
+
? Boolean(process.stdin.isTTY)
|
|
536
|
+
: options.stdinIsTTY;
|
|
537
|
+
voiceMd = stdinIsTTY
|
|
538
|
+
? editGmailVoice(String(account.voice_md || ''), options)
|
|
539
|
+
: String((options.readStdin || (() => fs.readFileSync(0, 'utf8')))());
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const { id, name } = gmailAccountIdentity(account);
|
|
543
|
+
const result = await apiRequestJson(`/integrations/gmail/accounts/${encodeURIComponent(id)}`, {
|
|
544
|
+
method: 'PATCH',
|
|
545
|
+
token,
|
|
546
|
+
body: { voice_md: voiceMd },
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
if (!result.ok) {
|
|
550
|
+
console.error(`could not ${options.clear ? 'clear' : 'save'} gmail voice: ${result.error || 'request failed'}`);
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
console.log(`voice ${options.clear ? 'cleared' : 'saved'} for ${name}`);
|
|
555
|
+
}
|
|
556
|
+
|
|
314
557
|
function openGmailAuthUrl(authUrl, deps = {}) {
|
|
315
558
|
const run = deps.spawnSync || spawnSync;
|
|
316
559
|
const platform = deps.platform || process.platform;
|
|
@@ -457,6 +700,21 @@ async function gmailCommand(subcommand, ...args) {
|
|
|
457
700
|
await gmailArchive(parsed.positional, { accountId: parsed.accountId || undefined });
|
|
458
701
|
break;
|
|
459
702
|
}
|
|
703
|
+
case 'verdicts': {
|
|
704
|
+
const parsed = parseGmailVerdictsArgs(args);
|
|
705
|
+
printGmailVerdicts(parsed);
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
case 'send': {
|
|
709
|
+
const parsed = parseGmailSendArgs(args);
|
|
710
|
+
await gmailSend(parsed.to, parsed.subject, parsed.body, { accountId: parsed.accountId || undefined });
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
case 'voice': {
|
|
714
|
+
const parsed = parseGmailVoiceArgs(args);
|
|
715
|
+
await gmailVoice(parsed.accountId, { clear: parsed.clear });
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
460
718
|
case 'connect':
|
|
461
719
|
await gmailConnect(args[0]);
|
|
462
720
|
break;
|
|
@@ -471,6 +729,9 @@ async function gmailCommand(subcommand, ...args) {
|
|
|
471
729
|
console.log(' atris gmail inbox [--account <id>] - list recent emails for a mailbox');
|
|
472
730
|
console.log(' atris gmail read <id> [--account <id>] - read specific email');
|
|
473
731
|
console.log(' atris gmail archive <id> [...] [--account <id>] - archive messages (reversible, all mail keeps them)');
|
|
732
|
+
console.log(' atris gmail verdicts [--account <id>] [--limit N] - list recent archive verdicts');
|
|
733
|
+
console.log(' atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>] - send an email');
|
|
734
|
+
console.log(' atris gmail voice [account] [--clear] - edit or clear an account writing voice');
|
|
474
735
|
console.log(' atris gmail connect [name] - connect or reconnect a gmail account');
|
|
475
736
|
console.log(' atris gmail accounts - list connected gmail accounts and the active account');
|
|
476
737
|
console.log(' atris gmail use [<account_id|name>] - choose or set the active gmail account');
|
|
@@ -1983,10 +2244,15 @@ async function integrationsStatus(args = []) {
|
|
|
1983
2244
|
module.exports = {
|
|
1984
2245
|
gmailCommand,
|
|
1985
2246
|
gmailConnect,
|
|
2247
|
+
gmailVoice,
|
|
1986
2248
|
gmailUse,
|
|
1987
2249
|
extractGmailMailboxAccount,
|
|
1988
2250
|
parseGmailArgs,
|
|
1989
2251
|
gmailAccountStatePath,
|
|
2252
|
+
gmailVerdictsPath,
|
|
2253
|
+
appendGmailVerdicts,
|
|
2254
|
+
readGmailVerdicts,
|
|
2255
|
+
printGmailVerdicts,
|
|
1990
2256
|
calendarCommand,
|
|
1991
2257
|
twitterCommand,
|
|
1992
2258
|
slackCommand,
|
package/commands/land.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { runGit } = require('../lib/git-spawn');
|
|
4
|
+
const { compactErrorPayload, printCliJson } = require('../lib/cli-json');
|
|
4
5
|
const { listWorktrees, statusCounts } = require('./worktree');
|
|
5
6
|
|
|
6
7
|
const DEFAULT_TTL_DAYS = 7;
|
|
@@ -236,14 +237,18 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_S
|
|
|
236
237
|
// merged branch residue.
|
|
237
238
|
function landSummary(cwd = process.cwd(), ttlDays = DEFAULT_TTL_DAYS) {
|
|
238
239
|
const root = repoRoot(cwd);
|
|
239
|
-
if (!root) return null;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
240
|
+
if (!root || !hasCommits(root)) return null;
|
|
241
|
+
try {
|
|
242
|
+
const board = collectBoard(root, { ttlDays, light: true });
|
|
243
|
+
return {
|
|
244
|
+
branches: board.summary.active + board.summary.due,
|
|
245
|
+
due: board.summary.due,
|
|
246
|
+
stale: board.summary.stale,
|
|
247
|
+
ttlDays,
|
|
248
|
+
};
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
function salvageDir(root) {
|
|
@@ -636,6 +641,40 @@ function readFollowingFlag(args, name, fallback = '') {
|
|
|
636
641
|
return args[idx + 1] || fallback;
|
|
637
642
|
}
|
|
638
643
|
|
|
644
|
+
const EMPTY_LAND = {
|
|
645
|
+
not_a_git_repo: 'this folder is not a git repo yet, so there is nothing to land.',
|
|
646
|
+
no_commits: 'nothing is in the air yet because this folder has no commits.',
|
|
647
|
+
no_base: 'no master or main branch yet, so there is nothing to land.',
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
function emptyLandKindFromError(err) {
|
|
651
|
+
if (err && err.code === 'LAND_NO_BASE') return 'no_base';
|
|
652
|
+
const msg = String(err && err.message || err || '');
|
|
653
|
+
if (/not a git/i.test(msg)) return 'not_a_git_repo';
|
|
654
|
+
if (/no commits/i.test(msg)) return 'no_commits';
|
|
655
|
+
if (/no master\/main/i.test(msg)) return 'no_base';
|
|
656
|
+
return '';
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function printEmptyLand(kind, json) {
|
|
660
|
+
const reason = EMPTY_LAND[kind] ? kind : 'no_base';
|
|
661
|
+
const detail = EMPTY_LAND[reason];
|
|
662
|
+
if (json) {
|
|
663
|
+
const payload = compactErrorPayload({ reason, detail });
|
|
664
|
+
printCliJson(payload, payload, ['--json']);
|
|
665
|
+
} else {
|
|
666
|
+
console.log(detail);
|
|
667
|
+
}
|
|
668
|
+
return 2;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function finishLandError(err, json) {
|
|
672
|
+
const kind = emptyLandKindFromError(err);
|
|
673
|
+
if (kind) return printEmptyLand(kind, json);
|
|
674
|
+
console.error(String(err && err.message || err).split('\n')[0]);
|
|
675
|
+
return 2;
|
|
676
|
+
}
|
|
677
|
+
|
|
639
678
|
function showHelp() {
|
|
640
679
|
console.log('');
|
|
641
680
|
console.log('atris land: the landing, what is actually done vs still in the air');
|
|
@@ -666,20 +705,14 @@ function landCommand(args = []) {
|
|
|
666
705
|
showHelp();
|
|
667
706
|
return 0;
|
|
668
707
|
}
|
|
708
|
+
const json = args.includes('--json');
|
|
669
709
|
const root = repoRoot();
|
|
670
|
-
if (!root)
|
|
671
|
-
|
|
672
|
-
return 1;
|
|
673
|
-
}
|
|
674
|
-
if (!hasCommits(root)) {
|
|
675
|
-
console.error('no commits yet. fix: git commit --allow-empty -m init');
|
|
676
|
-
return 1;
|
|
677
|
-
}
|
|
710
|
+
if (!root) return printEmptyLand('not_a_git_repo', json);
|
|
711
|
+
if (!hasCommits(root)) return printEmptyLand('no_commits', json);
|
|
678
712
|
const ttlRaw = readFollowingFlag(args, '--ttl', '');
|
|
679
713
|
const ttlParsed = Number(ttlRaw);
|
|
680
714
|
const ttlDays = ttlRaw !== '' && Number.isFinite(ttlParsed) && ttlParsed >= 0 ? ttlParsed : DEFAULT_TTL_DAYS;
|
|
681
715
|
const base = readFollowingFlag(args, '--base', '');
|
|
682
|
-
const json = args.includes('--json');
|
|
683
716
|
|
|
684
717
|
if (args.includes('--reap')) {
|
|
685
718
|
try {
|
|
@@ -688,8 +721,7 @@ function landCommand(args = []) {
|
|
|
688
721
|
else printReceipt(receipt);
|
|
689
722
|
return 0;
|
|
690
723
|
} catch (err) {
|
|
691
|
-
|
|
692
|
-
return 1;
|
|
724
|
+
return finishLandError(err, json);
|
|
693
725
|
}
|
|
694
726
|
}
|
|
695
727
|
|
|
@@ -706,8 +738,7 @@ function landCommand(args = []) {
|
|
|
706
738
|
else printStory(story);
|
|
707
739
|
return 0;
|
|
708
740
|
} catch (err) {
|
|
709
|
-
|
|
710
|
-
return 1;
|
|
741
|
+
return finishLandError(err, json);
|
|
711
742
|
}
|
|
712
743
|
}
|
|
713
744
|
|
|
@@ -717,8 +748,7 @@ function landCommand(args = []) {
|
|
|
717
748
|
else printBoard(board);
|
|
718
749
|
return 0;
|
|
719
750
|
} catch (err) {
|
|
720
|
-
|
|
721
|
-
return 1;
|
|
751
|
+
return finishLandError(err, json);
|
|
722
752
|
}
|
|
723
753
|
}
|
|
724
754
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { argsWantHelp } = require('../lib/noninteractive');
|
|
4
|
+
const {
|
|
5
|
+
laterNotePath,
|
|
6
|
+
listUserVisibleWork,
|
|
7
|
+
personName,
|
|
8
|
+
renderLaterRemember,
|
|
9
|
+
spokenLaterNote,
|
|
10
|
+
writeLaterNote,
|
|
11
|
+
} = require('../lib/first-minute');
|
|
12
|
+
|
|
13
|
+
function laterUsage() {
|
|
14
|
+
return 'Usage: atris later "<sentence>"';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function laterSentence(args = []) {
|
|
18
|
+
return spokenLaterNote((Array.isArray(args) ? args : [])
|
|
19
|
+
.filter((arg) => !String(arg).startsWith('-'))
|
|
20
|
+
.join(' '));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function laterAtris(args = process.argv.slice(3), {
|
|
24
|
+
root = process.cwd(),
|
|
25
|
+
log = console.log,
|
|
26
|
+
} = {}) {
|
|
27
|
+
const list = Array.isArray(args) ? args : [];
|
|
28
|
+
if (argsWantHelp(list)) {
|
|
29
|
+
log(laterUsage());
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
const sentence = laterSentence(list);
|
|
33
|
+
if (!sentence) {
|
|
34
|
+
log(laterUsage());
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
37
|
+
writeLaterNote(root, sentence);
|
|
38
|
+
const files = listUserVisibleWork(root);
|
|
39
|
+
log('');
|
|
40
|
+
log(renderLaterRemember({
|
|
41
|
+
person: personName(),
|
|
42
|
+
sentence,
|
|
43
|
+
files,
|
|
44
|
+
root,
|
|
45
|
+
}));
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
laterAtris,
|
|
51
|
+
laterNotePath,
|
|
52
|
+
};
|