atris 3.15.0 → 3.15.12

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/commands/init.js CHANGED
@@ -423,6 +423,13 @@ function initAtris() {
423
423
  console.log('✓ Created TODO.md placeholder');
424
424
  }
425
425
 
426
+ const nowFile = path.join(targetDir, 'now.md');
427
+ if (!fs.existsSync(nowFile)) {
428
+ const { renderDefaultNow } = require('./now');
429
+ fs.writeFileSync(nowFile, renderDefaultNow(cwd), 'utf8');
430
+ console.log('✓ Created now.md');
431
+ }
432
+
426
433
  // Create lessons.md (feedback loop for learning across features)
427
434
  const lessonsFile = path.join(targetDir, 'lessons.md');
428
435
  if (!fs.existsSync(lessonsFile)) {
@@ -605,7 +612,9 @@ This is the Atris boot sequence. Show the output to the user, then respond natur
605
612
  | File | Purpose |
606
613
  |------|---------|
607
614
  | \`atris/PERSONA.md\` | Communication style (read first) |
608
- | \`atris/TODO.md\` | Current tasks |
615
+ | \`atris task\` | Current tasks, claims, dialogue, proof |
616
+ | \`.atris/state/tasks.projection.json\` | Readable task projection for UIs/agents |
617
+ | \`atris/TODO.md\` | Rendered/legacy task view only |
609
618
  | \`atris/MAP.md\` | Navigation (where is X?) |
610
619
 
611
620
  ## Workflow
@@ -621,14 +630,17 @@ CHECK → atris review (verify + cleanup)
621
630
  - [ ] 3-4 sentences max per response
622
631
  - [ ] Use ASCII visuals for planning
623
632
  - [ ] Check MAP.md before touching code
624
- - [ ] Claim tasks in TODO.md before working
625
- - [ ] Delete tasks when done
633
+ - [ ] Run \`atris task list\` or \`atris task next\` before picking work
634
+ - [ ] Claim tasks with \`atris task claim <id> --as <agent>\`
635
+ - [ ] Finish tasks with proof via \`atris task finish <id> --proof "..."\`
636
+ - [ ] Treat \`atris/TODO.md\` as a rendered view; do not manually use it as the source of truth
626
637
 
627
638
  ## Anti-patterns
628
639
 
629
640
  - Don't explore codebase manually (use MAP.md)
630
641
  - Don't skip visualization step
631
642
  - Don't leave stale tasks
643
+ - Don't hand-edit TODO.md for active task ownership
632
644
  - Don't write verbose docs
633
645
 
634
646
  ---
@@ -12,6 +12,10 @@
12
12
 
13
13
  const { loadCredentials, ensureValidCredentials } = require('../utils/auth');
14
14
  const { apiRequestJson } = require('../utils/api');
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const path = require('path');
18
+ const { spawnSync } = require('child_process');
15
19
 
16
20
  async function getAuth() {
17
21
  const ensured = await ensureValidCredentials(apiRequestJson);
@@ -310,6 +314,133 @@ async function slackCommand(subcommand, ...args) {
310
314
  }
311
315
  }
312
316
 
317
+ // ============================================================================
318
+ // IMESSAGE
319
+ // ============================================================================
320
+
321
+ function imessageDoctor() {
322
+ const chatDb = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
323
+ const checks = {
324
+ macos: process.platform === 'darwin',
325
+ chat_db_exists: false,
326
+ chat_db_readable: false,
327
+ sqlite3_available: false,
328
+ osascript_available: false,
329
+ messages_automation: false,
330
+ };
331
+ const issues = [];
332
+
333
+ checks.chat_db_exists = fs.existsSync(chatDb);
334
+ if (checks.chat_db_exists) {
335
+ try {
336
+ fs.accessSync(chatDb, fs.constants.R_OK);
337
+ checks.chat_db_readable = true;
338
+ } catch {
339
+ issues.push('Messages database exists but is not readable. Grant Full Disk Access to this terminal or Atris.');
340
+ }
341
+ } else {
342
+ issues.push('Messages database not found on this Mac.');
343
+ }
344
+
345
+ checks.sqlite3_available = spawnSync('sqlite3', ['--version'], { encoding: 'utf8' }).status === 0;
346
+ if (!checks.sqlite3_available) issues.push('sqlite3 is not available.');
347
+
348
+ checks.osascript_available = spawnSync('osascript', ['-e', 'return "ok"'], { encoding: 'utf8' }).status === 0;
349
+ if (!checks.osascript_available) issues.push('osascript is not available.');
350
+ if (checks.osascript_available) {
351
+ checks.messages_automation = spawnSync('osascript', ['-e', 'tell application "Messages" to count services'], {
352
+ encoding: 'utf8',
353
+ timeout: 4000,
354
+ }).status === 0;
355
+ }
356
+ if (!checks.messages_automation) issues.push('Messages automation permission is not available yet.');
357
+
358
+ if (!checks.macos) issues.push('Local iMessage requires macOS.');
359
+
360
+ const connected = checks.macos && checks.chat_db_exists && checks.chat_db_readable && checks.sqlite3_available && checks.osascript_available && checks.messages_automation;
361
+ return {
362
+ connected,
363
+ provider: 'local_imessage',
364
+ mode: 'local',
365
+ checks,
366
+ issues,
367
+ next_step: connected
368
+ ? 'iMessage is available on this Mac.'
369
+ : 'Open System Settings -> Privacy & Security -> Full Disk Access and allow your terminal or Atris, then run this check again.',
370
+ };
371
+ }
372
+
373
+ function printImessageDoctor(result, json = false) {
374
+ if (json) {
375
+ console.log(JSON.stringify(result, null, 2));
376
+ return;
377
+ }
378
+ console.log('iMessage local check\n');
379
+ console.log(`Status: ${result.connected ? 'Connected on this Mac' : 'Needs permission or setup'}`);
380
+ for (const [name, ok] of Object.entries(result.checks)) {
381
+ console.log(` ${ok ? '✓' : '✗'} ${name.replace(/_/g, ' ')}`);
382
+ }
383
+ if (result.issues.length) {
384
+ console.log('\nNext:');
385
+ for (const issue of result.issues) console.log(` - ${issue}`);
386
+ console.log(` - ${result.next_step}`);
387
+ }
388
+ }
389
+
390
+ function imessageRecent(handle, options = {}) {
391
+ if (!handle) {
392
+ console.error('Usage: atris imessage recent <phone-or-email> [--limit 20]');
393
+ process.exit(1);
394
+ }
395
+ const doctor = imessageDoctor();
396
+ if (!doctor.connected) {
397
+ printImessageDoctor(doctor, Boolean(options.json));
398
+ process.exit(1);
399
+ }
400
+
401
+ const limit = Number(options.limit || 20);
402
+ const chatDb = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
403
+ const sql = `
404
+ SELECT datetime(m.date/1000000000 + 978307200, 'unixepoch', 'localtime') AS ts,
405
+ CASE m.is_from_me WHEN 1 THEN 'me' ELSE h.id END AS sender,
406
+ replace(replace(COALESCE(m.text,''), char(10), ' '), char(13), ' ') AS text
407
+ FROM message m
408
+ JOIN handle h ON h.rowid = m.handle_id
409
+ WHERE h.id = '${String(handle).replace(/'/g, "''")}'
410
+ ORDER BY m.date DESC
411
+ LIMIT ${Math.max(1, Math.min(100, limit))};
412
+ `;
413
+ const result = spawnSync('sqlite3', ['-readonly', chatDb, sql], { encoding: 'utf8' });
414
+ if (result.status !== 0) {
415
+ console.error(result.stderr || 'Failed to read Messages database.');
416
+ process.exit(1);
417
+ }
418
+ console.log(result.stdout.trim() || 'No recent messages found.');
419
+ }
420
+
421
+ async function imessageCommand(subcommand, ...args) {
422
+ switch (subcommand) {
423
+ case 'doctor': {
424
+ const json = args.includes('--json');
425
+ const result = imessageDoctor();
426
+ printImessageDoctor(result, json);
427
+ if (!result.connected && args.includes('--strict')) process.exit(1);
428
+ break;
429
+ }
430
+ case 'recent': {
431
+ const handle = args[0];
432
+ const limitFlag = args.findIndex((x) => x === '--limit');
433
+ const limit = limitFlag >= 0 ? args[limitFlag + 1] : 20;
434
+ imessageRecent(handle, { limit, json: args.includes('--json') });
435
+ break;
436
+ }
437
+ default:
438
+ console.log('iMessage commands:');
439
+ console.log(' atris imessage doctor [--json] - Check local Messages access');
440
+ console.log(' atris imessage recent <handle> - Read recent local messages');
441
+ }
442
+ }
443
+
313
444
  // ============================================================================
314
445
  // STATUS
315
446
  // ============================================================================
@@ -337,6 +468,9 @@ async function integrationsStatus() {
337
468
  }
338
469
  }
339
470
 
471
+ const imessage = imessageDoctor();
472
+ console.log(` ${imessage.connected ? '✅' : '❌'} iMessage (local Mac)`);
473
+
340
474
  console.log('\nConnect integrations at: https://atris.ai/dashboard/settings');
341
475
  }
342
476
 
@@ -345,5 +479,7 @@ module.exports = {
345
479
  calendarCommand,
346
480
  twitterCommand,
347
481
  slackCommand,
482
+ imessageCommand,
483
+ imessageDoctor,
348
484
  integrationsStatus,
349
485
  };
package/commands/live.js CHANGED
@@ -45,21 +45,43 @@ function firstPositionalArg(args) {
45
45
  return null;
46
46
  }
47
47
 
48
- function readBusinessSlugFromCwd(cwd) {
48
+ function readBusinessMetaFromCwd(cwd) {
49
49
  const file = path.join(cwd, '.atris', 'business.json');
50
50
  if (!fs.existsSync(file)) return null;
51
51
  try {
52
- const data = JSON.parse(fs.readFileSync(file, 'utf8'));
53
- return data.slug || data.canonical_slug || data.name || null;
52
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
54
53
  } catch {
55
54
  return null;
56
55
  }
57
56
  }
58
57
 
58
+ function readBusinessSlugFromCwd(cwd) {
59
+ const data = readBusinessMetaFromCwd(cwd);
60
+ return data ? data.slug || data.canonical_slug || data.name || null : null;
61
+ }
62
+
63
+ function resolveWorkspaceCwd(slug, cwd) {
64
+ if (!slug) return cwd;
65
+
66
+ const currentSlug = readBusinessSlugFromCwd(cwd);
67
+ if (currentSlug === slug) return cwd;
68
+
69
+ const child = path.join(cwd, slug);
70
+ if (fs.existsSync(child) && fs.statSync(child).isDirectory()) {
71
+ const childSlug = readBusinessSlugFromCwd(child);
72
+ const hasAtris = fs.existsSync(path.join(child, 'atris'));
73
+ if (childSlug || hasAtris) return child;
74
+ }
75
+
76
+ return cwd;
77
+ }
78
+
59
79
  function parseLiveOptions(args, cwd = process.cwd()) {
60
80
  const first = firstPositionalArg(args);
81
+ const slug = first || readBusinessSlugFromCwd(cwd);
82
+ const workspaceCwd = resolveWorkspaceCwd(slug, cwd);
61
83
  return {
62
- slug: first || readBusinessSlugFromCwd(cwd),
84
+ slug,
63
85
  once: args.includes('--once'),
64
86
  dryRun: args.includes('--dry-run'),
65
87
  noDoctor: args.includes('--no-doctor'),
@@ -68,8 +90,8 @@ function parseLiveOptions(args, cwd = process.cwd()) {
68
90
  debounceSec: parseNumberFlag(args, 'debounce', DEFAULT_DEBOUNCE_SEC),
69
91
  timeoutSec: parseNumberFlag(args, 'timeout', DEFAULT_TIMEOUT_SEC),
70
92
  only: parseStringFlag(args, 'only'),
71
- root: parseStringFlag(args, 'root') || path.dirname(cwd),
72
- cwd,
93
+ root: parseStringFlag(args, 'root') || path.dirname(workspaceCwd),
94
+ cwd: workspaceCwd,
73
95
  };
74
96
  }
75
97
 
@@ -0,0 +1,263 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const NOW_PATH = path.join('atris', 'now.md');
5
+
6
+ function todayIso() {
7
+ return new Date().toISOString().split('T')[0];
8
+ }
9
+
10
+ function ensureAtrisDir(root = process.cwd()) {
11
+ const atrisDir = path.join(root, 'atris');
12
+ if (!fs.existsSync(atrisDir)) {
13
+ throw new Error('atris/ folder not found. Run "atris init" first.');
14
+ }
15
+ return atrisDir;
16
+ }
17
+
18
+ function hasWorkspaceMarkers(atrisDir) {
19
+ return fs.existsSync(path.join(atrisDir, 'MAP.md')) || fs.existsSync(path.join(atrisDir, 'TODO.md'));
20
+ }
21
+
22
+ function findChildWorkspaces(root = process.cwd()) {
23
+ if (!fs.existsSync(root)) return [];
24
+
25
+ return fs
26
+ .readdirSync(root, { withFileTypes: true })
27
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
28
+ .map((entry) => {
29
+ const workspaceRoot = path.join(root, entry.name);
30
+ const atrisDir = path.join(workspaceRoot, 'atris');
31
+ if (!fs.existsSync(atrisDir) || !hasWorkspaceMarkers(atrisDir)) return null;
32
+ const mapPath = path.join(atrisDir, 'MAP.md');
33
+ const todoPath = path.join(atrisDir, 'TODO.md');
34
+ return {
35
+ slug: entry.name,
36
+ root: workspaceRoot,
37
+ atrisDir,
38
+ mapPath,
39
+ todoPath,
40
+ nowPath: path.join(atrisDir, 'now.md'),
41
+ };
42
+ })
43
+ .filter(Boolean)
44
+ .sort((a, b) => a.slug.localeCompare(b.slug));
45
+ }
46
+
47
+ function readFirstHeading(filePath) {
48
+ if (!fs.existsSync(filePath)) return null;
49
+ const content = fs.readFileSync(filePath, 'utf8');
50
+ const line = content.split(/\r?\n/).find((l) => l.trim().startsWith('#'));
51
+ return line ? line.replace(/^#+\s*/, '').trim() : null;
52
+ }
53
+
54
+ function countMatches(filePath, pattern) {
55
+ if (!fs.existsSync(filePath)) return 0;
56
+ const content = fs.readFileSync(filePath, 'utf8');
57
+ return (content.match(pattern) || []).length;
58
+ }
59
+
60
+ function currentJournalPath(root = process.cwd()) {
61
+ const now = new Date();
62
+ const year = String(now.getFullYear());
63
+ const date = todayIso();
64
+ return path.join(root, 'atris', 'logs', year, `${date}.md`);
65
+ }
66
+
67
+ function renderDefaultNow(root = process.cwd()) {
68
+ const atrisDir = ensureAtrisDir(root);
69
+ const mapHeading = readFirstHeading(path.join(atrisDir, 'MAP.md')) || 'MAP not filled yet';
70
+ const todoPath = path.join(atrisDir, 'TODO.md');
71
+ const journalPath = currentJournalPath(root);
72
+ const backlogCount = countMatches(todoPath, /^-\s+\*\*.+?\*\*/gm);
73
+ const inboxCount = countMatches(journalPath, /^-\s+\*\*I\d+:/gm);
74
+ const completedCount = countMatches(journalPath, /^-\s+\*\*C\d+:/gm);
75
+ const generated = todayIso();
76
+
77
+ return `# now
78
+
79
+ > Current operating truth for this workspace.
80
+ > Read this first. Follow links only when needed.
81
+
82
+ Last updated: ${generated}
83
+
84
+ ## What Matters Now
85
+
86
+ - Decide the next useful move before opening more context.
87
+
88
+ ## Current Priority
89
+
90
+ - Keep the workspace coherent and useful for the next human or agent.
91
+
92
+ ## Signals
93
+
94
+ - Map: ${mapHeading}
95
+ - TODO items visible: ${backlogCount}
96
+ - Inbox items today: ${inboxCount}
97
+ - Completed receipts today: ${completedCount}
98
+
99
+ ## Watchouts
100
+
101
+ - Do not treat old logs as current truth unless this file links to them.
102
+ - Do not create motion for its own sake.
103
+ - If facts conflict, surface the conflict and cite the receipts.
104
+
105
+ ## Next Move
106
+
107
+ - Read \`atris/MAP.md\`, \`atris/TODO.md\`, and today's journal only as needed for the task in front of you.
108
+
109
+ ## Receipts
110
+
111
+ - \`atris/MAP.md\`
112
+ - \`atris/TODO.md\`
113
+ - \`${path.relative(root, journalPath)}\`
114
+ `;
115
+ }
116
+
117
+ function renderPortfolioNow(root = process.cwd()) {
118
+ const workspaces = findChildWorkspaces(root);
119
+ if (workspaces.length === 0) {
120
+ throw new Error('atris/ folder not found. Run "atris init" first.');
121
+ }
122
+
123
+ const generated = todayIso();
124
+ const lines = workspaces.map((workspace) => {
125
+ const heading = readFirstHeading(workspace.mapPath) || workspace.slug;
126
+ const todoCount = countMatches(workspace.todoPath, /^-\s+\*\*.+?\*\*/gm);
127
+ const nowState = fs.existsSync(workspace.nowPath) ? 'has now.md' : 'needs now.md';
128
+ return `- ${workspace.slug}: ${heading}; ${todoCount} visible TODO item${todoCount === 1 ? '' : 's'}; ${nowState}.`;
129
+ });
130
+
131
+ return `# now
132
+
133
+ > Current operating truth for this portfolio of Atris workspaces.
134
+ > Read this first. Then enter the specific workspace that matters.
135
+
136
+ Last updated: ${generated}
137
+
138
+ ## What Matters Now
139
+
140
+ - Keep the active business workspaces easy to scan, update, and hand off.
141
+
142
+ ## Current Priority
143
+
144
+ - Use the child workspace with the right slug; avoid creating duplicate business brains.
145
+
146
+ ## Workspace Signals
147
+
148
+ ${lines.join('\n')}
149
+
150
+ ## Watchouts
151
+
152
+ - Parent status is a map, not the source of truth for each business.
153
+ - Each active workspace should own its own \`atris/now.md\`.
154
+ - If slugs conflict, resolve the workspace identity before pushing or pulling.
155
+
156
+ ## Next Move
157
+
158
+ - Run \`atris now\` inside the workspace you are about to operate.
159
+
160
+ ## Receipts
161
+
162
+ ${workspaces.map((workspace) => `- \`${workspace.slug}/atris/MAP.md\``).join('\n')}
163
+ `;
164
+ }
165
+
166
+ function ensureNowFile(root = process.cwd()) {
167
+ let atrisDir = path.join(root, 'atris');
168
+ const isWorkspace = fs.existsSync(atrisDir) && hasWorkspaceMarkers(atrisDir);
169
+ const childWorkspaces = isWorkspace ? [] : findChildWorkspaces(root);
170
+ if (!isWorkspace && childWorkspaces.length === 0) {
171
+ ensureAtrisDir(root);
172
+ }
173
+ if (!isWorkspace && childWorkspaces.length > 0) {
174
+ fs.mkdirSync(atrisDir, { recursive: true });
175
+ }
176
+ const nowPath = path.join(atrisDir, 'now.md');
177
+ if (!fs.existsSync(nowPath)) {
178
+ const content = isWorkspace ? renderDefaultNow(root) : renderPortfolioNow(root);
179
+ fs.writeFileSync(nowPath, content, 'utf8');
180
+ return { created: true, path: nowPath };
181
+ }
182
+ return { created: false, path: nowPath };
183
+ }
184
+
185
+ function refreshNowFile(root = process.cwd()) {
186
+ const atrisDir = path.join(root, 'atris');
187
+ const isWorkspace = fs.existsSync(atrisDir) && hasWorkspaceMarkers(atrisDir);
188
+ const childWorkspaces = isWorkspace ? [] : findChildWorkspaces(root);
189
+ if (!isWorkspace && childWorkspaces.length === 0) {
190
+ ensureAtrisDir(root);
191
+ }
192
+ if (!isWorkspace && childWorkspaces.length > 0) {
193
+ fs.mkdirSync(atrisDir, { recursive: true });
194
+ }
195
+ const nowPath = path.join(atrisDir, 'now.md');
196
+ const content = isWorkspace ? renderDefaultNow(root) : renderPortfolioNow(root);
197
+ fs.writeFileSync(nowPath, content, 'utf8');
198
+ return { path: nowPath };
199
+ }
200
+
201
+ function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
202
+ const help = args.includes('--help') || args.includes('-h');
203
+ if (help) {
204
+ console.log('Usage: atris now [--init|--refresh|--all|--path]');
205
+ console.log('');
206
+ console.log('Show the current operating truth for this workspace.');
207
+ console.log('');
208
+ console.log(' atris now Show atris/now.md');
209
+ console.log(' atris now --init Create atris/now.md if missing');
210
+ console.log(' atris now --refresh Regenerate a small local now.md');
211
+ console.log(' atris now --all Refresh this parent and every child Atris workspace');
212
+ console.log(' atris now --path Print the file path only');
213
+ return;
214
+ }
215
+
216
+ const init = args.includes('--init');
217
+ const refresh = args.includes('--refresh');
218
+ const all = args.includes('--all');
219
+ const pathOnly = args.includes('--path');
220
+
221
+ let result;
222
+ if (all) {
223
+ const workspaces = findChildWorkspaces(root);
224
+ for (const workspace of workspaces) {
225
+ refreshNowFile(workspace.root);
226
+ }
227
+ result = refreshNowFile(root);
228
+ if (!pathOnly) {
229
+ console.log(`Refreshed ${workspaces.length} child workspace${workspaces.length === 1 ? '' : 's'}.`);
230
+ console.log('');
231
+ }
232
+ } else if (refresh) {
233
+ result = refreshNowFile(root);
234
+ } else if (init) {
235
+ result = ensureNowFile(root);
236
+ } else {
237
+ result = ensureNowFile(root);
238
+ }
239
+
240
+ const rel = path.relative(root, result.path);
241
+ if (pathOnly) {
242
+ console.log(rel);
243
+ return;
244
+ }
245
+
246
+ if (result.created) {
247
+ console.log(`Created ${rel}`);
248
+ console.log('');
249
+ }
250
+
251
+ const content = fs.readFileSync(result.path, 'utf8').trimEnd();
252
+ console.log(content);
253
+ }
254
+
255
+ module.exports = {
256
+ NOW_PATH,
257
+ ensureNowFile,
258
+ findChildWorkspaces,
259
+ nowAtris,
260
+ refreshNowFile,
261
+ renderDefaultNow,
262
+ renderPortfolioNow,
263
+ };