atris 3.32.0 → 3.33.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/atris.js CHANGED
@@ -127,6 +127,19 @@ function isOptionValue(args, index, optionNames) {
127
127
  }
128
128
 
129
129
  function applyRunnerFlags(args) {
130
+ // --engine <name> is the operator-facing spelling of --runner-profile:
131
+ // one flag rents a specific intelligence for this run.
132
+ const engineFlag = readOptionArg(args, '--engine');
133
+ if (engineFlag) {
134
+ const { canonicalEngineName } = require('../commands/engine');
135
+ const { RUNNER_PROFILE_NAMES } = require('../lib/runner-command');
136
+ const canonical = canonicalEngineName(engineFlag);
137
+ if (!canonical) {
138
+ console.error(`Unknown --engine "${engineFlag}". Known engines: ${RUNNER_PROFILE_NAMES.join(', ')}.`);
139
+ process.exit(1);
140
+ }
141
+ process.env.ATRIS_RUNNER_PROFILE = canonical;
142
+ }
130
143
  const runnerProfile = readOptionArg(args, '--runner-profile');
131
144
  if (runnerProfile) {
132
145
  // Fail fast at the CLI boundary: an unknown profile otherwise stays silent
@@ -153,8 +166,30 @@ function applyRunnerFlags(args) {
153
166
  process.env.ATRIS_RUNNER_MODEL = runnerModel;
154
167
  process.env.ATRIS_CLAUDE_MODEL = runnerModel;
155
168
  }
169
+ // No per-run choice and no env: the workspace's saved engine
170
+ // (.atris/engine.json, written by `atris engine <name>`) becomes the
171
+ // profile for every loop spawn. For the loop commands themselves, fall all
172
+ // the way to the house engine (atris-fast when ax is installed) so the
173
+ // default intelligence is our own. Heartbeats stay engine-agnostic without
174
+ // any loop code knowing about engines.
175
+ if (!process.env.ATRIS_RUNNER_PROFILE) {
176
+ try {
177
+ const engine = require('../commands/engine');
178
+ const saved = engine.readSavedEngine();
179
+ if (saved) {
180
+ process.env.ATRIS_RUNNER_PROFILE = saved;
181
+ } else if (RUNNER_SPAWNING_COMMANDS.includes(command)) {
182
+ const resolved = engine.resolveDefaultEngine();
183
+ if (resolved.source === 'house') process.env.ATRIS_RUNNER_PROFILE = resolved.name;
184
+ }
185
+ } catch {}
186
+ }
156
187
  }
157
188
 
189
+ // Commands whose ticks spawn a worker engine; only these pay the engine
190
+ // detection probe when nothing is configured.
191
+ const RUNNER_SPAWNING_COMMANDS = ['run', 'autopilot', 'mission', 'pulse', 'gm', 'spaceship'];
192
+
158
193
  const isBusinessSyncSafetyCommand = command === 'sync'
159
194
  && (
160
195
  commandArgs.includes('--status')
@@ -429,11 +464,13 @@ function showHelp() {
429
464
  console.log('Quick Start:');
430
465
  console.log('');
431
466
  console.log(' 1. atris Open a persistent AI computer for this workspace');
467
+ console.log(' npx atris Same command after a local project install');
432
468
  console.log(' 2. Describe what you want run, built, researched, or validated');
433
469
  console.log(' 3. Atris acts with context, memory, tools, and a review loop');
434
470
  console.log('');
435
471
  console.log('Common invocations:');
436
- console.log(' atris init');
472
+ console.log(' atris init Global install: initialize this project');
473
+ console.log(' npx atris init Local install: initialize this project');
437
474
  console.log(' atris computer');
438
475
  console.log(' atris business init "My Company"');
439
476
  console.log(' atris run');
@@ -497,6 +534,7 @@ function showHelp() {
497
534
  console.log(' worktree - Isolated Git worktrees plus guarded ship/merge for parallel agents');
498
535
  console.log(' land - The landing: what is actually done vs still in the air; --reap backs up + clears overdue');
499
536
  console.log(' autoland - Approve the policy once; certified work lands itself, you keep irreversible calls');
537
+ console.log(' engine - Bring any intelligence: roster of installed coding CLIs, default engine, --engine per run, `engine test` preflight');
500
538
  console.log(' sign - Co-author trailer on every commit in an atris workspace (on/off/status)');
501
539
  console.log(' visualize - Generate a Slack/deck-ready visual from a prompt');
502
540
  console.log(' youtube - Process YouTube videos with timestamped transcript-first analysis');
@@ -567,7 +605,7 @@ function showHelp() {
567
605
  console.log(' accounts - Manage accounts (list, add, remove)');
568
606
  console.log('');
569
607
  console.log('Integrations:');
570
- console.log(' gmail - Email commands (inbox, read)');
608
+ console.log(' gmail - Email commands (inbox, read, archive)');
571
609
  console.log(' calendar - Calendar commands (today, week)');
572
610
  console.log(' twitter - Twitter commands (post)');
573
611
  console.log(' slack - Slack commands (channels)');
@@ -937,7 +975,7 @@ const knownCommands = ['init', 'log', 'now', 'radar', 'ctop', 'launchpad', 'stat
937
975
  'clean', 'harvest', 'verify', 'search', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube',
938
976
  'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'probe', 'worktree', 'land', 'autoland', 'aeo', 'slop', 'strings', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'xp', 'play', 'gm', 'x', 'recap', 'signup', 'clarity', 'moves',
939
977
  'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
940
- 'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'compile', 'spaceship', 'truth', 'sign'];
978
+ 'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines'];
941
979
 
942
980
  // Check if command is an atris.md spec file - triggers welcome visualization
943
981
  function isSpecFile(cmd) {
@@ -1059,7 +1097,11 @@ async function interactiveEntry(userInput) {
1059
1097
 
1060
1098
  // Fresh install - offer init
1061
1099
  if (state.state === 'fresh') {
1062
- console.log('\nNo atris/ folder found. Run: atris init');
1100
+ console.log('\nNo atris/ folder found.');
1101
+ console.log('');
1102
+ console.log('Start here:');
1103
+ console.log(' atris init if Atris is installed globally');
1104
+ console.log(' npx atris init if Atris was installed in this project');
1063
1105
  return;
1064
1106
  }
1065
1107
 
@@ -2272,6 +2314,13 @@ if (command === 'init') {
2272
2314
  Promise.resolve(require('../commands/compile').compileCommand(subcommand, ...args))
2273
2315
  .then(() => process.exit(process.exitCode || 0))
2274
2316
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
2317
+ } else if (command === 'engine' || command === 'engines') {
2318
+ // Engine: bring any intelligence — roster of installed coding CLIs, default
2319
+ // engine per workspace, --engine <name> rides any loop for one run.
2320
+ try {
2321
+ const engineArgs = command === 'engines' && process.argv.length <= 3 ? [] : process.argv.slice(3);
2322
+ process.exit(require('../commands/engine').engineCommand(engineArgs) || 0);
2323
+ } catch (err) { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); }
2275
2324
  } else if (command === 'sign') {
2276
2325
  // Sign: prepare-commit-msg hook that credits Atris as co-author on commits in atris workspaces.
2277
2326
  try { process.exit(require('../commands/sign').signCommand(process.argv[3]) || 0); }
@@ -357,15 +357,21 @@ function showHelp() {
357
357
  console.log(' on you past 24h');
358
358
  console.log(' atris autoland off back to approving every item');
359
359
  console.log(' atris autoland digest [--send] the daily message, now');
360
- console.log(' atris autoland tick one heartbeat (what the hourly cron runs)');
360
+ console.log(' atris autoland tick [--json] one heartbeat (what the hourly cron runs)');
361
+ console.log('');
362
+ console.log('help is always read-only: atris autoland tick --help never lands work.');
361
363
  console.log('');
362
364
  return 0;
363
365
  }
364
366
 
367
+ function wantsHelp(args = []) {
368
+ return args.some((arg) => arg === 'help' || arg === '--help' || arg === '-h');
369
+ }
370
+
365
371
  function autolandCommand(args = []) {
366
372
  const [sub, ...rest] = args;
373
+ if (wantsHelp(args)) return showHelp();
367
374
  if (!sub || sub.startsWith('--')) return showStatus(repoRoot(), args);
368
- if (sub === 'help' || sub === '--help' || sub === '-h') return showHelp();
369
375
  const root = repoRoot();
370
376
  if (sub === 'on') return turnOn(root, rest);
371
377
  if (sub === 'off') return turnOff(root);
@@ -0,0 +1,299 @@
1
+ 'use strict';
2
+
3
+ // atris engine: bring any intelligence.
4
+ // Every installed headless coding CLI is a swappable worker behind one
5
+ // contract (bounded prompt in, verified proof out, engines never
6
+ // self-certify). This command shows the roster, flips the default, and the
7
+ // same names ride --engine on mission run / autopilot / run.
8
+ //
9
+ // atris engine roster + current default
10
+ // atris engine cursor make cursor the default engine here
11
+ // atris engine reset back to the house default (atris-fast)
12
+ //
13
+ // The default persists to .atris/engine.json. Per-run flags and
14
+ // ATRIS_RUNNER_PROFILE always beat the file.
15
+
16
+ const fs = require('fs');
17
+ const os = require('os');
18
+ const path = require('path');
19
+ const { spawnSync } = require('child_process');
20
+ const {
21
+ RUNNER_PROFILE_DEFS,
22
+ RUNNER_PROFILE_ALIASES,
23
+ RUNNER_PROFILE_NAMES,
24
+ buildRunnerCommand,
25
+ } = require('../lib/runner-command');
26
+
27
+ const HOUSE_ENGINE = 'atris-fast';
28
+
29
+ function engineFile(root = process.cwd()) {
30
+ return path.join(root, '.atris', 'engine.json');
31
+ }
32
+
33
+ function binInstalled(bin) {
34
+ const safe = String(bin || '').replace(/[^A-Za-z0-9_.-]/g, '');
35
+ if (!safe) return false;
36
+ const probe = spawnSync('sh', ['-c', `command -v ${safe}`], { encoding: 'utf8' });
37
+ return probe.status === 0 && Boolean(String(probe.stdout || '').trim());
38
+ }
39
+
40
+ function canonicalEngineName(name) {
41
+ const trimmed = String(name || '').trim();
42
+ if (!trimmed) return '';
43
+ if (RUNNER_PROFILE_DEFS[trimmed]) return trimmed;
44
+ if (RUNNER_PROFILE_ALIASES[trimmed]) return RUNNER_PROFILE_ALIASES[trimmed];
45
+ return '';
46
+ }
47
+
48
+ function readSavedEngine(root = process.cwd()) {
49
+ try {
50
+ const saved = JSON.parse(fs.readFileSync(engineFile(root), 'utf8'));
51
+ return canonicalEngineName(saved.default);
52
+ } catch {
53
+ return '';
54
+ }
55
+ }
56
+
57
+ // The default engine for this workspace, in precedence order:
58
+ // env (per-run flags land here) -> .atris/engine.json -> house default.
59
+ // The house default is our own intelligence when it is installed.
60
+ function resolveDefaultEngine(root = process.cwd()) {
61
+ const env = canonicalEngineName(process.env.ATRIS_RUNNER_PROFILE);
62
+ if (env) return { name: env, source: 'env' };
63
+ const saved = readSavedEngine(root);
64
+ if (saved) return { name: saved, source: 'saved' };
65
+ if (binInstalled(RUNNER_PROFILE_DEFS[HOUSE_ENGINE].bin)) return { name: HOUSE_ENGINE, source: 'house' };
66
+ const fallback = RUNNER_PROFILE_NAMES.find((name) => binInstalled(RUNNER_PROFILE_DEFS[name].bin));
67
+ return fallback ? { name: fallback, source: 'detected' } : { name: HOUSE_ENGINE, source: 'none' };
68
+ }
69
+
70
+ function roster(root = process.cwd()) {
71
+ const current = resolveDefaultEngine(root);
72
+ return RUNNER_PROFILE_NAMES.map((name) => ({
73
+ name,
74
+ bin: RUNNER_PROFILE_DEFS[name].bin,
75
+ installed: binInstalled(RUNNER_PROFILE_DEFS[name].bin),
76
+ default: name === current.name,
77
+ }));
78
+ }
79
+
80
+ function setEngine(name, root = process.cwd()) {
81
+ const canonical = canonicalEngineName(name);
82
+ if (!canonical) {
83
+ throw new Error(`Unknown engine "${name}". Known engines: ${RUNNER_PROFILE_NAMES.join(', ')}`);
84
+ }
85
+ const file = engineFile(root);
86
+ fs.mkdirSync(path.dirname(file), { recursive: true });
87
+ fs.writeFileSync(file, `${JSON.stringify({ default: canonical, set_at: new Date().toISOString() }, null, 2)}\n`);
88
+ return canonical;
89
+ }
90
+
91
+ function resetEngine(root = process.cwd()) {
92
+ try { fs.unlinkSync(engineFile(root)); return true; } catch { return false; }
93
+ }
94
+
95
+ function printRoster(root) {
96
+ const list = roster(root);
97
+ const found = list.filter((e) => e.installed).length;
98
+ const current = resolveDefaultEngine(root);
99
+ console.log('');
100
+ console.log(` engines — ${found} intelligence${found === 1 ? '' : 's'} found`);
101
+ console.log('');
102
+ for (const engine of list) {
103
+ const mark = engine.default ? '→' : ' ';
104
+ const state = engine.installed ? 'ready' : 'not installed';
105
+ console.log(` ${mark} ${engine.name.padEnd(12)} ${state}`);
106
+ }
107
+ console.log('');
108
+ console.log(` default: ${current.name}${current.source === 'saved' ? ' (set here)' : current.source === 'env' ? ' (this session)' : ''}`);
109
+ console.log(` switch: atris engine <name> · one run: --engine <name> on mission run / autopilot / run`);
110
+ console.log('');
111
+ }
112
+
113
+ // Preflight: run one engine CLI headless with a reply-OK prompt and report
114
+ // pass/fail. A dead login, missing binary, or hung spawn is a one-command
115
+ // diagnosis instead of a failed overnight flight. `name` is canonical.
116
+ const PROBE_PROMPT = 'Reply with exactly the two characters: OK';
117
+ // Real engines think before replying: cursor measured at ~68s for a one-word
118
+ // answer on 2026-07-02. 30s produced a false FAIL on a healthy engine.
119
+ const PROBE_DEFAULT_TIMEOUT_MS = 120000;
120
+
121
+ function probeEngine(name, { timeout = PROBE_DEFAULT_TIMEOUT_MS } = {}) {
122
+ const def = RUNNER_PROFILE_DEFS[name];
123
+ if (!binInstalled(def.bin)) {
124
+ return {
125
+ engine: name,
126
+ bin: def.bin,
127
+ pass: false,
128
+ reason: 'not-installed',
129
+ message: `${def.bin} CLI not installed`,
130
+ stdout: '',
131
+ stderr: '',
132
+ durationMs: 0,
133
+ };
134
+ }
135
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-engine-probe-'));
136
+ const promptFile = path.join(tmpDir, 'prompt.txt');
137
+ fs.writeFileSync(promptFile, `${PROBE_PROMPT}\n`);
138
+ let cmd;
139
+ const prevProfile = process.env.ATRIS_RUNNER_PROFILE;
140
+ process.env.ATRIS_RUNNER_PROFILE = name;
141
+ try {
142
+ cmd = buildRunnerCommand({ promptFile });
143
+ } finally {
144
+ if (prevProfile === undefined) delete process.env.ATRIS_RUNNER_PROFILE;
145
+ else process.env.ATRIS_RUNNER_PROFILE = prevProfile;
146
+ }
147
+ const start = Date.now();
148
+ let res;
149
+ try {
150
+ res = spawnSync('sh', ['-c', cmd], { encoding: 'utf8', timeout });
151
+ } catch (err) {
152
+ fs.rmSync(tmpDir, { recursive: true, force: true });
153
+ return {
154
+ engine: name,
155
+ bin: def.bin,
156
+ pass: false,
157
+ reason: 'spawn-error',
158
+ message: String(err && err.message ? err.message : err),
159
+ stdout: '',
160
+ stderr: '',
161
+ durationMs: Date.now() - start,
162
+ };
163
+ }
164
+ const durationMs = Date.now() - start;
165
+ fs.rmSync(tmpDir, { recursive: true, force: true });
166
+ const stdout = res.stdout || '';
167
+ const stderr = res.stderr || '';
168
+ const timedOut = res.status === null && Boolean(res.signal) && String(res.signal).toLowerCase().includes('term');
169
+ const combined = `${stdout}\n${stderr}`.trim();
170
+ const ok = res.status === 0 && /OK/i.test(combined);
171
+ let reason;
172
+ if (ok) reason = 'ok';
173
+ else if (timedOut) reason = 'timeout';
174
+ else if (res.status !== 0 && res.status !== null) reason = 'bad-exit';
175
+ else if (!/OK/i.test(combined)) reason = 'no-ok';
176
+ else reason = 'unknown';
177
+ return {
178
+ engine: name,
179
+ bin: def.bin,
180
+ pass: ok,
181
+ reason,
182
+ message: ok
183
+ ? 'responded OK'
184
+ : (timedOut ? `no reply within ${timeout}ms` : `did not reply OK (exit ${res.status}, signal ${res.signal})`),
185
+ stdout,
186
+ stderr,
187
+ durationMs,
188
+ };
189
+ }
190
+
191
+ function runEngineTest(targets, { json, root } = {}) {
192
+ let enginesToTest;
193
+ if (targets && targets.length) {
194
+ enginesToTest = targets.map((n) => {
195
+ const c = canonicalEngineName(n);
196
+ if (!c) {
197
+ throw new Error(`Unknown engine "${n}". Known engines: ${RUNNER_PROFILE_NAMES.join(', ')}`);
198
+ }
199
+ return c;
200
+ });
201
+ } else {
202
+ enginesToTest = RUNNER_PROFILE_NAMES.filter((n) => binInstalled(RUNNER_PROFILE_DEFS[n].bin));
203
+ if (!enginesToTest.length) {
204
+ if (json) {
205
+ console.log(JSON.stringify({ ok: false, results: [], summary: { pass: 0, fail: 0 } }, null, 2));
206
+ } else {
207
+ console.error('\n no installed engines to test\n');
208
+ }
209
+ return 1;
210
+ }
211
+ }
212
+ const results = enginesToTest.map((name) => probeEngine(name));
213
+ const failures = results.filter((r) => !r.pass);
214
+ const passed = results.length - failures.length;
215
+ if (json) {
216
+ console.log(JSON.stringify({
217
+ ok: failures.length === 0,
218
+ results,
219
+ summary: { pass: passed, fail: failures.length },
220
+ }, null, 2));
221
+ } else {
222
+ console.log('');
223
+ for (const r of results) {
224
+ const mark = r.pass ? '✓' : '✗';
225
+ const line = r.pass
226
+ ? ` ${mark} ${r.engine.padEnd(12)} pass — ${r.message} (${r.durationMs}ms)`
227
+ : ` ${mark} ${r.engine.padEnd(12)} FAIL — ${r.message}`;
228
+ if (r.pass) console.log(line);
229
+ else console.error(line);
230
+ }
231
+ console.log('');
232
+ if (failures.length) {
233
+ console.error(` ${failures.length} engine${failures.length === 1 ? '' : 's'} failed: ${failures.map((f) => f.engine).join(', ')}`);
234
+ console.error(` fix the login/binary, then re-run: atris engine test${targets && targets.length ? ' ' + targets.join(' ') : ''}`);
235
+ } else {
236
+ console.log(` all engines responded — clear for flight`);
237
+ }
238
+ console.log('');
239
+ }
240
+ return failures.length ? 1 : 0;
241
+ }
242
+
243
+ function engineCommand(args = []) {
244
+ const json = args.includes('--json');
245
+ const positional = args.filter((a) => !a.startsWith('--'));
246
+ const sub = (positional[0] || '').trim();
247
+ const root = process.cwd();
248
+
249
+ if (sub === 'test') {
250
+ return runEngineTest(positional.slice(1), { json, root });
251
+ }
252
+
253
+ if (!sub || sub === 'list' || sub === 'status') {
254
+ if (json) {
255
+ const current = resolveDefaultEngine(root);
256
+ console.log(JSON.stringify({ engines: roster(root), default: current.name, source: current.source }, null, 2));
257
+ return 0;
258
+ }
259
+ printRoster(root);
260
+ return 0;
261
+ }
262
+
263
+ if (sub === 'reset' || sub === 'off') {
264
+ const removed = resetEngine(root);
265
+ console.log(removed
266
+ ? `\n default engine reset — back to ${resolveDefaultEngine(root).name}\n`
267
+ : `\n nothing to reset — no engine was set here\n`);
268
+ return 0;
269
+ }
270
+
271
+ if (sub === 'help') {
272
+ console.log('\n atris engine roster + current default\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
273
+ return 0;
274
+ }
275
+
276
+ // atris engine <name> — flip the default.
277
+ const canonical = setEngine(sub, root);
278
+ const def = RUNNER_PROFILE_DEFS[canonical];
279
+ const installed = binInstalled(def.bin);
280
+ console.log('');
281
+ console.log(` default engine: ${canonical}`);
282
+ if (!installed) console.log(` heads up: its CLI (${def.bin}) is not installed here yet — runs will fail until it is.`);
283
+ console.log(` every mission run / autopilot / run tick now rides it. one-off: --engine <name>. undo: atris engine reset`);
284
+ console.log('');
285
+ return 0;
286
+ }
287
+
288
+ module.exports = {
289
+ engineCommand,
290
+ resolveDefaultEngine,
291
+ canonicalEngineName,
292
+ readSavedEngine,
293
+ setEngine,
294
+ resetEngine,
295
+ roster,
296
+ probeEngine,
297
+ runEngineTest,
298
+ HOUSE_ENGINE,
299
+ };
@@ -124,6 +124,32 @@ async function gmailRead(messageId) {
124
124
  console.log(msg.body || msg.snippet || '(no body)');
125
125
  }
126
126
 
127
+ async function gmailArchive(messageIds) {
128
+ const ids = (messageIds || []).map(id => String(id || '').trim()).filter(Boolean);
129
+ if (!ids.length) {
130
+ console.error('Usage: atris gmail archive <message_id> [<message_id> ...]');
131
+ process.exit(1);
132
+ }
133
+
134
+ const token = await getAuthToken();
135
+
136
+ console.log(`Archiving ${ids.length} message${ids.length === 1 ? '' : 's'}...`);
137
+
138
+ const result = await apiRequestJson('/integrations/gmail/messages/batch-archive', {
139
+ method: 'POST',
140
+ token,
141
+ body: { message_ids: ids },
142
+ });
143
+
144
+ if (!result.ok) {
145
+ console.error(`Error: ${result.error || 'Failed to archive'}`);
146
+ process.exit(1);
147
+ }
148
+
149
+ const archived = result.data?.archived ?? result.data?.count ?? ids.length;
150
+ console.log(`Archived ${archived} message${archived === 1 ? '' : 's'}. They stay searchable in All Mail.`);
151
+ }
152
+
127
153
  async function gmailCommand(subcommand, ...args) {
128
154
  switch (subcommand) {
129
155
  case 'inbox':
@@ -133,10 +159,14 @@ async function gmailCommand(subcommand, ...args) {
133
159
  case 'read':
134
160
  await gmailRead(args[0]);
135
161
  break;
162
+ case 'archive':
163
+ await gmailArchive(args);
164
+ break;
136
165
  default:
137
166
  console.log('Gmail commands:');
138
167
  console.log(' atris gmail inbox - List recent emails');
139
168
  console.log(' atris gmail read <id> - Read specific email');
169
+ console.log(' atris gmail archive <id> [...] - Archive messages (reversible, All Mail keeps them)');
140
170
  }
141
171
  }
142
172
 
@@ -474,6 +504,58 @@ async function calendarDate(dateText) {
474
504
  await calendarRange(`Events on ${dateText}`, { timeMin: start.toISOString(), timeMax: end.toISOString() });
475
505
  }
476
506
 
507
+ async function calendarAdd(args) {
508
+ // atris calendar add "Title" --date YYYY-MM-DD --from HH:MM --to HH:MM
509
+ // or: --start <ISO> --end <ISO>; optional --location, --description
510
+ const rest = [...(args || [])];
511
+ const flags = {};
512
+ const positional = [];
513
+ for (let i = 0; i < rest.length; i++) {
514
+ const arg = String(rest[i] || '');
515
+ if (arg.startsWith('--')) {
516
+ flags[arg.slice(2)] = String(rest[i + 1] || '');
517
+ i += 1;
518
+ } else {
519
+ positional.push(arg);
520
+ }
521
+ }
522
+ const summary = positional.join(' ').trim();
523
+ let start = String(flags.start || '').trim();
524
+ let end = String(flags.end || '').trim();
525
+ if (!start && flags.date && flags.from) start = `${flags.date}T${flags.from}:00`;
526
+ if (!end && flags.date && flags.to) end = `${flags.date}T${flags.to}:00`;
527
+ if (!summary || !start || !end) {
528
+ console.error('Usage: atris calendar add "Title" --date YYYY-MM-DD --from HH:MM --to HH:MM');
529
+ console.error(' or: atris calendar add "Title" --start <ISO datetime> --end <ISO datetime>');
530
+ process.exit(1);
531
+ }
532
+
533
+ const token = await getAuthToken();
534
+
535
+ console.log(`Creating event: ${summary} (${start} to ${end})...`);
536
+
537
+ const body = { summary, start, end };
538
+ if (flags.location) body.location = flags.location;
539
+ if (flags.description) body.description = flags.description;
540
+ if (flags.timezone) body.timezone = flags.timezone;
541
+
542
+ const result = await apiRequestJson('/integrations/google-calendar/events', {
543
+ method: 'POST',
544
+ token,
545
+ body,
546
+ });
547
+
548
+ if (!result.ok) {
549
+ console.error(`Error: ${result.error || 'Failed to create event'}`);
550
+ process.exit(1);
551
+ }
552
+
553
+ const created = result.data || {};
554
+ console.log(`Created: ${created.summary || summary}`);
555
+ if (created.htmlLink) console.log(`Link: ${created.htmlLink}`);
556
+ console.log(`Verified on calendar: ${created.verified ? 'yes' : 'no'}`);
557
+ }
558
+
477
559
  async function calendarCommand(subcommand, ...args) {
478
560
  switch (subcommand) {
479
561
  case 'today':
@@ -492,6 +574,10 @@ async function calendarCommand(subcommand, ...args) {
492
574
  case 'search':
493
575
  await calendarSearch(args);
494
576
  break;
577
+ case 'add':
578
+ case 'create':
579
+ await calendarAdd(args);
580
+ break;
495
581
  default:
496
582
  console.log('Calendar commands:');
497
583
  console.log(' atris calendar today - Show today\'s events');
@@ -499,6 +585,7 @@ async function calendarCommand(subcommand, ...args) {
499
585
  console.log(' atris calendar week - Show this week\'s events');
500
586
  console.log(' atris calendar date YYYY-MM-DD - Show events on a date');
501
587
  console.log(' atris calendar search <query> [--days 30] [--limit 20] [--timeout-ms 10000] [--json] - Search upcoming events');
588
+ console.log(' atris calendar add \"Title\" --date YYYY-MM-DD --from HH:MM --to HH:MM - Create an event');
502
589
  }
503
590
  }
504
591
 
@@ -737,6 +824,58 @@ async function slackSearch(query, args = []) {
737
824
  formatSlackMessages(messages);
738
825
  }
739
826
 
827
+ async function slackSend(channel, textParts) {
828
+ const target = String(channel || '').trim();
829
+ const text = (textParts || []).join(' ').trim();
830
+ if (!target || !text) {
831
+ console.error('Usage: atris slack send <channel> "<message>"');
832
+ process.exit(1);
833
+ }
834
+
835
+ const token = await getAuthToken();
836
+
837
+ console.log(`Sending to ${target}...`);
838
+
839
+ const result = await apiRequestJson('/integrations/slack/me/send', {
840
+ method: 'POST',
841
+ token,
842
+ body: { channel: target, text },
843
+ });
844
+
845
+ if (!result.ok) {
846
+ console.error(`Error: ${result.error || 'Failed to send'}`);
847
+ process.exit(1);
848
+ }
849
+
850
+ console.log(`Sent to ${target} as you.`);
851
+ }
852
+
853
+ async function slackDm(userId, textParts) {
854
+ const target = String(userId || '').trim();
855
+ const text = (textParts || []).join(' ').trim();
856
+ if (!target || !text) {
857
+ console.error('Usage: atris slack dm <slack_user_id> "<message>"');
858
+ process.exit(1);
859
+ }
860
+
861
+ const token = await getAuthToken();
862
+
863
+ console.log(`Sending DM to ${target}...`);
864
+
865
+ const result = await apiRequestJson('/integrations/slack/me/dm', {
866
+ method: 'POST',
867
+ token,
868
+ body: { slack_user_id: target, text },
869
+ });
870
+
871
+ if (!result.ok) {
872
+ console.error(`Error: ${result.error || 'Failed to send DM'}`);
873
+ process.exit(1);
874
+ }
875
+
876
+ console.log(`DM sent to ${target} as you.`);
877
+ }
878
+
740
879
  async function slackCommand(subcommand, ...args) {
741
880
  switch (subcommand) {
742
881
  case 'channels':
@@ -753,12 +892,20 @@ async function slackCommand(subcommand, ...args) {
753
892
  case 'search':
754
893
  await slackSearch(argsWithoutLimit(args).join(' '), args);
755
894
  break;
895
+ case 'send':
896
+ await slackSend(args[0], args.slice(1));
897
+ break;
898
+ case 'dm':
899
+ await slackDm(args[0], args.slice(1));
900
+ break;
756
901
  default:
757
902
  console.log('Slack commands:');
758
903
  console.log(' atris slack channels - List Slack channels');
759
904
  console.log(' atris slack dms - List Slack DMs');
760
905
  console.log(' atris slack messages <channel> - Read recent messages');
761
906
  console.log(' atris slack search <query> - Search Slack messages');
907
+ console.log(' atris slack send <channel> "<message>" - Send a message as you');
908
+ console.log(' atris slack dm <user_id> "<message>" - DM someone as you');
762
909
  }
763
910
  }
764
911