atris 3.15.57 → 3.16.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.
Files changed (47) hide show
  1. package/AGENTS.md +2 -2
  2. package/GETTING_STARTED.md +1 -1
  3. package/PERSONA.md +4 -4
  4. package/README.md +12 -11
  5. package/atris/skills/copy-editor/SKILL.md +30 -4
  6. package/atris/skills/improve/SKILL.md +18 -20
  7. package/atris/wiki/concepts/agent-activation-contract.md +5 -3
  8. package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
  9. package/atris/wiki/index.md +1 -0
  10. package/ax +522 -73
  11. package/bin/atris.js +78 -44
  12. package/commands/align.js +0 -14
  13. package/commands/apps.js +102 -1
  14. package/commands/autopilot.js +628 -31
  15. package/commands/brain.js +219 -34
  16. package/commands/brainstorm.js +0 -829
  17. package/commands/compile.js +569 -0
  18. package/commands/computer.js +0 -60
  19. package/commands/improve.js +501 -0
  20. package/commands/integrations.js +233 -71
  21. package/commands/lesson.js +44 -0
  22. package/commands/member.js +4498 -226
  23. package/commands/mission.js +302 -27
  24. package/commands/now.js +89 -1
  25. package/commands/probe.js +366 -0
  26. package/commands/radar.js +181 -56
  27. package/commands/recap.js +203 -0
  28. package/commands/skill.js +6 -2
  29. package/commands/soul.js +0 -4
  30. package/commands/task.js +5587 -499
  31. package/commands/terminal.js +14 -10
  32. package/commands/wiki.js +87 -1
  33. package/commands/workflow.js +288 -73
  34. package/commands/worktree.js +52 -15
  35. package/commands/xp.js +6 -65
  36. package/lib/auto-accept-certified.js +294 -0
  37. package/lib/file-ops.js +0 -184
  38. package/lib/member-alive.js +232 -0
  39. package/lib/policy-lessons.js +280 -0
  40. package/lib/receipt-evidence.js +64 -0
  41. package/lib/state-detection.js +75 -1
  42. package/lib/task-db.js +568 -16
  43. package/lib/task-proof.js +43 -0
  44. package/package.json +1 -1
  45. package/utils/auth.js +13 -4
  46. package/commands/research.js +0 -52
  47. package/lib/section-merge.js +0 -196
@@ -8,6 +8,7 @@
8
8
  * atris calendar yesterday - Show yesterday's events
9
9
  * atris calendar week - Show this week's events
10
10
  * atris calendar date YYYY-MM-DD - Show events on a date
11
+ * atris calendar search <query> [--days 30] [--limit 20] [--timeout-ms 10000] [--json] - Search upcoming events
11
12
  * atris twitter post - Post a tweet (interactive)
12
13
  * atris slack channels - List Slack channels
13
14
  * atris slack messages <channel> [--limit 20] - Read recent messages
@@ -21,6 +22,8 @@ const os = require('os');
21
22
  const path = require('path');
22
23
  const { spawnSync } = require('child_process');
23
24
 
25
+ const CALENDAR_CACHE_PATH = path.join(os.homedir(), '.atris', 'calendar-events-cache.json');
26
+
24
27
  async function getAuth() {
25
28
  const ensured = await ensureValidCredentials(apiRequestJson);
26
29
  const creds = ensured.error ? null : ensured.credentials;
@@ -209,6 +212,166 @@ function formatCalendarEvents(label, events) {
209
212
  console.log('─'.repeat(50));
210
213
  }
211
214
 
215
+ function parseCalendarSearchArgs(args = []) {
216
+ const values = [];
217
+ const options = { days: 30, limit: 20, timeoutMs: 10000, json: false };
218
+ for (let i = 0; i < args.length; i += 1) {
219
+ const arg = args[i];
220
+ if (arg === '--json') {
221
+ options.json = true;
222
+ } else if (arg === '--days' || arg === '-d') {
223
+ const parsed = Number.parseInt(args[i + 1], 10);
224
+ if (Number.isFinite(parsed) && parsed > 0) options.days = Math.min(parsed, 365);
225
+ i += 1;
226
+ } else if (arg === '--limit' || arg === '-n') {
227
+ const parsed = Number.parseInt(args[i + 1], 10);
228
+ if (Number.isFinite(parsed) && parsed > 0) options.limit = Math.min(parsed, 100);
229
+ i += 1;
230
+ } else if (arg === '--timeout-ms' || arg === '--timeout') {
231
+ const parsed = Number.parseInt(args[i + 1], 10);
232
+ if (Number.isFinite(parsed) && parsed > 0) options.timeoutMs = Math.min(parsed, 60000);
233
+ i += 1;
234
+ } else {
235
+ values.push(arg);
236
+ }
237
+ }
238
+ return { query: values.join(' ').trim(), ...options };
239
+ }
240
+
241
+ function calendarAttendeesText(attendees) {
242
+ if (!Array.isArray(attendees)) return '';
243
+ return attendees
244
+ .map((attendee) => {
245
+ if (typeof attendee === 'string') return attendee;
246
+ return [
247
+ attendee.email,
248
+ attendee.displayName,
249
+ attendee.name,
250
+ attendee.responseStatus,
251
+ ].filter(Boolean).join(' ');
252
+ })
253
+ .filter(Boolean)
254
+ .join(' ');
255
+ }
256
+
257
+ function calendarSearchText(event = {}) {
258
+ return [
259
+ event.summary,
260
+ event.title,
261
+ event.location,
262
+ event.description,
263
+ calendarAttendeesText(event.attendees),
264
+ ].filter(Boolean).join(' ').toLowerCase();
265
+ }
266
+
267
+ function filterCalendarEventsForSearch(events = [], query = '', limit = 20) {
268
+ const terms = String(query || '').toLowerCase().split(/\s+/).filter(Boolean);
269
+ if (!terms.length) return [];
270
+ return events
271
+ .filter((event) => {
272
+ const haystack = calendarSearchText(event);
273
+ return terms.every((term) => haystack.includes(term));
274
+ })
275
+ .slice(0, limit);
276
+ }
277
+
278
+ function compactCalendarEvent(event = {}) {
279
+ return {
280
+ id: event.id || event.event_id || '',
281
+ start: calendarEventTimeValue(event.start || event.start_time || event.startTime),
282
+ end: calendarEventTimeValue(event.end || event.end_time || event.endTime),
283
+ summary: event.summary || event.title || '(no title)',
284
+ location: event.location || '',
285
+ htmlLink: event.htmlLink || event.link || '',
286
+ };
287
+ }
288
+
289
+ function calendarCachePacket(events = [], meta = {}) {
290
+ return {
291
+ ok: true,
292
+ updatedAt: new Date().toISOString(),
293
+ source: meta.source || 'live',
294
+ days: meta.days || null,
295
+ count: events.length,
296
+ events: events.map(compactCalendarEvent),
297
+ };
298
+ }
299
+
300
+ function writeCalendarEventsCache(events = [], meta = {}) {
301
+ const rows = Array.isArray(events) ? events : [];
302
+ try {
303
+ fs.mkdirSync(path.dirname(CALENDAR_CACHE_PATH), { recursive: true });
304
+ fs.writeFileSync(CALENDAR_CACHE_PATH, `${JSON.stringify(calendarCachePacket(rows, meta), null, 2)}\n`, 'utf8');
305
+ return CALENDAR_CACHE_PATH;
306
+ } catch {
307
+ return '';
308
+ }
309
+ }
310
+
311
+ function readCalendarEventsCache() {
312
+ try {
313
+ const packet = JSON.parse(fs.readFileSync(CALENDAR_CACHE_PATH, 'utf8'));
314
+ const events = Array.isArray(packet.events) ? packet.events : [];
315
+ return {
316
+ ok: true,
317
+ path: CALENDAR_CACHE_PATH,
318
+ updatedAt: packet.updatedAt || '',
319
+ events,
320
+ };
321
+ } catch {
322
+ return { ok: false, path: CALENDAR_CACHE_PATH, updatedAt: '', events: [] };
323
+ }
324
+ }
325
+
326
+ function formatCalendarSearchResults(query, matches, {
327
+ json = false,
328
+ total = matches.length,
329
+ source = 'live',
330
+ liveOk = true,
331
+ cacheUpdatedAt = '',
332
+ error = '',
333
+ } = {}) {
334
+ if (json) {
335
+ console.log(JSON.stringify({
336
+ ok: true,
337
+ query,
338
+ source,
339
+ liveOk,
340
+ cacheUpdatedAt,
341
+ error,
342
+ totalEvents: total,
343
+ matchCount: matches.length,
344
+ matches: matches.map(compactCalendarEvent),
345
+ }, null, 2));
346
+ return;
347
+ }
348
+
349
+ if (!matches.length) {
350
+ console.log(`No upcoming events matched "${query}".`);
351
+ return;
352
+ }
353
+
354
+ console.log(`Found ${matches.length} upcoming event(s) matching "${query}":\n`);
355
+ formatCalendarEvents('matching your search', matches);
356
+ }
357
+
358
+ function formatCalendarSearchError(query, message, { json = false, status = 0 } = {}) {
359
+ if (json) {
360
+ console.log(JSON.stringify({
361
+ ok: false,
362
+ query,
363
+ status,
364
+ error: message || 'Failed to fetch events',
365
+ totalEvents: 0,
366
+ matchCount: 0,
367
+ matches: [],
368
+ }, null, 2));
369
+ return;
370
+ }
371
+ console.error(`Error: ${message || 'Failed to fetch events'}`);
372
+ process.exit(1);
373
+ }
374
+
212
375
  async function calendarRange(label, { timeMin, timeMax, days } = {}) {
213
376
  const { token, email } = await getAuth();
214
377
 
@@ -236,9 +399,63 @@ async function calendarRange(label, { timeMin, timeMax, days } = {}) {
236
399
  }
237
400
 
238
401
  const events = result.data?.events || result.data || [];
402
+ writeCalendarEventsCache(events, { source: 'calendarRange', days: days || null });
239
403
  formatCalendarEvents(label.toLowerCase(), events);
240
404
  }
241
405
 
406
+ async function calendarSearch(args = []) {
407
+ const { query, days, limit, timeoutMs, json } = parseCalendarSearchArgs(args);
408
+ if (!query) {
409
+ console.error('Usage: atris calendar search <query> [--days 30] [--limit 20] [--timeout-ms 10000] [--json]');
410
+ process.exit(1);
411
+ }
412
+
413
+ const credentials = loadCredentials();
414
+ const token = credentials && credentials.token;
415
+ const email = credentials && credentials.email || 'unknown';
416
+ if (!token) {
417
+ formatCalendarSearchError(query, 'Not logged in. Run: atris login', { json, status: 401 });
418
+ return;
419
+ }
420
+ const result = await apiRequestJson(`/integrations/google-calendar/events?days=${encodeURIComponent(String(days))}`, {
421
+ method: 'GET',
422
+ token,
423
+ timeoutMs,
424
+ retries: 0,
425
+ });
426
+
427
+ if (!result.ok) {
428
+ const cache = readCalendarEventsCache();
429
+ if (json && cache.ok) {
430
+ const matches = filterCalendarEventsForSearch(cache.events, query, limit);
431
+ formatCalendarSearchResults(query, matches, {
432
+ json,
433
+ total: cache.events.length,
434
+ source: 'cache',
435
+ liveOk: false,
436
+ cacheUpdatedAt: cache.updatedAt,
437
+ error: result.error || 'Failed to fetch events',
438
+ });
439
+ return;
440
+ }
441
+ if (result.status === 400 || result.status === 401) {
442
+ formatCalendarSearchError(
443
+ query,
444
+ `Calendar not connected for ${email}. Connect at: https://atris.ai/dashboard/settings`,
445
+ { json, status: result.status },
446
+ );
447
+ } else {
448
+ formatCalendarSearchError(query, result.error || 'Failed to fetch events', { json, status: result.status || 0 });
449
+ }
450
+ return;
451
+ }
452
+
453
+ const events = result.data?.events || result.data || [];
454
+ writeCalendarEventsCache(events, { source: 'calendarSearch', days });
455
+ const matches = filterCalendarEventsForSearch(events, query, limit);
456
+ formatCalendarSearchResults(query, matches, { json, total: events.length, source: 'live', liveOk: true });
457
+ }
458
+
242
459
  async function calendarToday() {
243
460
  await calendarRange("Today's events", { days: 1 });
244
461
  }
@@ -272,12 +489,16 @@ async function calendarCommand(subcommand, ...args) {
272
489
  case 'date':
273
490
  await calendarDate(args[0]);
274
491
  break;
492
+ case 'search':
493
+ await calendarSearch(args);
494
+ break;
275
495
  default:
276
496
  console.log('Calendar commands:');
277
497
  console.log(' atris calendar today - Show today\'s events');
278
498
  console.log(' atris calendar yesterday - Show yesterday\'s events');
279
499
  console.log(' atris calendar week - Show this week\'s events');
280
500
  console.log(' atris calendar date YYYY-MM-DD - Show events on a date');
501
+ console.log(' atris calendar search <query> [--days 30] [--limit 20] [--timeout-ms 10000] [--json] - Search upcoming events');
281
502
  }
282
503
  }
283
504
 
@@ -614,31 +835,6 @@ function printImessageDoctor(result, json = false) {
614
835
  }
615
836
  }
616
837
 
617
- function boundedImessageLimit(value) {
618
- const parsed = Number(value || 20);
619
- if (!Number.isFinite(parsed)) return 20;
620
- return Math.max(1, Math.min(100, Math.trunc(parsed)));
621
- }
622
-
623
- function printImessageRows(result, options = {}) {
624
- if (options.json) {
625
- const rows = String(result.stdout || '').trim();
626
- let messages = [];
627
- try {
628
- messages = rows ? JSON.parse(rows) : [];
629
- } catch {
630
- messages = [];
631
- }
632
- console.log(JSON.stringify({
633
- ok: result.status === 0,
634
- count: Array.isArray(messages) ? messages.length : 0,
635
- messages: Array.isArray(messages) ? messages : [],
636
- }, null, 2));
637
- return;
638
- }
639
- console.log(String(result.stdout || '').trim() || 'No recent messages found.');
640
- }
641
-
642
838
  function imessageRecent(handle, options = {}) {
643
839
  if (!handle) {
644
840
  console.error('Usage: atris imessage recent <phone-or-email> [--limit 20]');
@@ -650,7 +846,7 @@ function imessageRecent(handle, options = {}) {
650
846
  process.exit(1);
651
847
  }
652
848
 
653
- const limit = boundedImessageLimit(options.limit);
849
+ const limit = Number(options.limit || 20);
654
850
  const chatDb = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
655
851
  const sql = `
656
852
  SELECT datetime(m.date/1000000000 + 978307200, 'unixepoch', 'localtime') AS ts,
@@ -660,44 +856,14 @@ function imessageRecent(handle, options = {}) {
660
856
  JOIN handle h ON h.rowid = m.handle_id
661
857
  WHERE h.id = '${String(handle).replace(/'/g, "''")}'
662
858
  ORDER BY m.date DESC
663
- LIMIT ${limit};
664
- `;
665
- const sqliteArgs = options.json ? ['-json', '-readonly', chatDb, sql] : ['-readonly', chatDb, sql];
666
- const result = spawnSync('sqlite3', sqliteArgs, { encoding: 'utf8' });
667
- if (result.status !== 0) {
668
- console.error(result.stderr || 'Failed to read Messages database.');
669
- process.exit(1);
670
- }
671
- printImessageRows(result, options);
672
- }
673
-
674
- function imessageLatest(options = {}) {
675
- const doctor = imessageDoctor();
676
- if (!doctor.connected) {
677
- printImessageDoctor(doctor, Boolean(options.json));
678
- process.exit(1);
679
- }
680
-
681
- const limit = boundedImessageLimit(options.limit);
682
- const chatDb = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
683
- const sql = `
684
- SELECT datetime(m.date/1000000000 + 978307200, 'unixepoch', 'localtime') AS ts,
685
- CASE m.is_from_me WHEN 1 THEN 'me' ELSE COALESCE(h.id, 'unknown') END AS sender,
686
- COALESCE(h.id, 'unknown') AS handle,
687
- replace(replace(COALESCE(m.text,''), char(10), ' '), char(13), ' ') AS text
688
- FROM message m
689
- LEFT JOIN handle h ON h.rowid = m.handle_id
690
- WHERE length(COALESCE(m.text,'')) > 0
691
- ORDER BY m.date DESC
692
- LIMIT ${limit};
859
+ LIMIT ${Math.max(1, Math.min(100, limit))};
693
860
  `;
694
- const sqliteArgs = options.json ? ['-json', '-readonly', chatDb, sql] : ['-readonly', chatDb, sql];
695
- const result = spawnSync('sqlite3', sqliteArgs, { encoding: 'utf8' });
861
+ const result = spawnSync('sqlite3', ['-readonly', chatDb, sql], { encoding: 'utf8' });
696
862
  if (result.status !== 0) {
697
863
  console.error(result.stderr || 'Failed to read Messages database.');
698
864
  process.exit(1);
699
865
  }
700
- printImessageRows(result, options);
866
+ console.log(result.stdout.trim() || 'No recent messages found.');
701
867
  }
702
868
 
703
869
  function escapeSqlString(value) {
@@ -1224,22 +1390,12 @@ async function imessageCommand(subcommand, ...args) {
1224
1390
  break;
1225
1391
  }
1226
1392
  case 'recent': {
1393
+ const handle = args[0];
1227
1394
  const limitFlag = args.findIndex((x) => x === '--limit');
1228
1395
  const limit = limitFlag >= 0 ? args[limitFlag + 1] : 20;
1229
- const handle = args.find((arg) => arg && !arg.startsWith('--') && arg !== String(limit));
1230
- if (!handle) {
1231
- imessageLatest({ limit, json: args.includes('--json') });
1232
- break;
1233
- }
1234
1396
  imessageRecent(handle, { limit, json: args.includes('--json') });
1235
1397
  break;
1236
1398
  }
1237
- case 'latest': {
1238
- const limitFlag = args.findIndex((x) => x === '--limit');
1239
- const limit = limitFlag >= 0 ? args[limitFlag + 1] : 20;
1240
- imessageLatest({ limit, json: args.includes('--json') });
1241
- break;
1242
- }
1243
1399
  case 'lookup': {
1244
1400
  imessageLookup(args);
1245
1401
  break;
@@ -1253,7 +1409,6 @@ async function imessageCommand(subcommand, ...args) {
1253
1409
  console.log(' atris imessage doctor [--json] - Check local Messages access');
1254
1410
  console.log(' atris imessage lookup --name <name> [--json] [--refresh]');
1255
1411
  console.log(' atris imessage recent <handle> - Read recent local messages');
1256
- console.log(' atris imessage latest [--limit 20] [--json] - Read latest local messages');
1257
1412
  console.log(' atris imessage send --to <handle> --text <text> --approved [--json] [--receipt]');
1258
1413
  }
1259
1414
  }
@@ -1299,4 +1454,11 @@ module.exports = {
1299
1454
  imessageCommand,
1300
1455
  imessageDoctor,
1301
1456
  integrationsStatus,
1457
+ parseCalendarSearchArgs,
1458
+ filterCalendarEventsForSearch,
1459
+ formatCalendarSearchResults,
1460
+ formatCalendarSearchError,
1461
+ writeCalendarEventsCache,
1462
+ readCalendarEventsCache,
1463
+ CALENDAR_CACHE_PATH,
1302
1464
  };
@@ -2,6 +2,44 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { writeLesson } = require('./autopilot');
4
4
 
5
+ function mineLessons(args) {
6
+ const { loadHistory, mineProofPolicy, writePolicyLessons, syncLessonsMd } = require('../lib/policy-lessons');
7
+ const root = process.cwd();
8
+ const json = args.includes('--json');
9
+ const dryRun = args.includes('--dry-run');
10
+ const history = loadHistory(root);
11
+ const mined = mineProofPolicy(history);
12
+ let statePath = null;
13
+ let lessonsMd = { path: null, written: [] };
14
+ if (!dryRun) {
15
+ statePath = writePolicyLessons(root, mined);
16
+ lessonsMd = syncLessonsMd(root, mined);
17
+ }
18
+ if (json) {
19
+ console.log(JSON.stringify({
20
+ ok: true,
21
+ action: 'lesson_mine',
22
+ dry_run: dryRun,
23
+ state_path: statePath,
24
+ lessons_md_path: lessonsMd.path,
25
+ lessons_md_written: lessonsMd.written,
26
+ ...mined,
27
+ }, null, 2));
28
+ return;
29
+ }
30
+ const { sources } = mined;
31
+ console.log(`mined ${mined.lessons.length} policy lesson(s) from ${sources.career_xp_receipts} receipts / ${sources.task_episodes} episodes / ${sources.scorecards} scorecards (${sources.human_reviewed_episodes} human-reviewed)`);
32
+ for (const lesson of mined.lessons) {
33
+ console.log(`- policy-${lesson.id}: ${lesson.lesson}`);
34
+ }
35
+ if (dryRun) {
36
+ console.log('(dry-run: nothing written)');
37
+ } else {
38
+ console.log(`state: ${statePath}`);
39
+ console.log(`lessons.md: ${lessonsMd.written.length ? lessonsMd.written.map((id) => `policy-${id}`).join(', ') : 'no entries written'}`);
40
+ }
41
+ }
42
+
5
43
  function lessonAtris(subcommand, ...args) {
6
44
  const atrisDir = path.join(process.cwd(), 'atris');
7
45
  if (!fs.existsSync(atrisDir)) {
@@ -9,9 +47,15 @@ function lessonAtris(subcommand, ...args) {
9
47
  process.exit(1);
10
48
  }
11
49
 
50
+ if (subcommand === 'mine') {
51
+ mineLessons(args);
52
+ return;
53
+ }
54
+
12
55
  if (subcommand !== 'add') {
13
56
  console.log('');
14
57
  console.log(' Usage: atris lesson add <slug> <pass|fail> "<text>"');
58
+ console.log(' atris lesson mine [--json] [--dry-run]');
15
59
  console.log('');
16
60
  process.exit(subcommand ? 1 : 0);
17
61
  }