atris 2.6.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -34
- package/atris/CLAUDE.md +5 -1
- package/atris/atris.md +4 -0
- package/atris/features/README.md +24 -0
- package/atris/skills/autopilot/SKILL.md +74 -75
- package/atris/skills/endgame/SKILL.md +179 -0
- package/atris/skills/flow/SKILL.md +121 -0
- package/atris/skills/improve/SKILL.md +84 -0
- package/atris/skills/loop/SKILL.md +72 -0
- package/atris/skills/wiki/SKILL.md +61 -0
- package/atris/team/executor/MEMBER.md +10 -4
- package/atris/team/navigator/MEMBER.md +2 -0
- package/atris/team/validator/MEMBER.md +8 -5
- package/atris.md +33 -0
- package/bin/atris.js +210 -41
- package/commands/activate.js +28 -2
- package/commands/align.js +720 -0
- package/commands/auth.js +75 -2
- package/commands/autopilot.js +1213 -270
- package/commands/browse.js +100 -0
- package/commands/business.js +785 -12
- package/commands/clean.js +107 -2
- package/commands/computer.js +429 -0
- package/commands/context-sync.js +78 -8
- package/commands/experiments.js +351 -0
- package/commands/feedback.js +150 -0
- package/commands/fleet.js +395 -0
- package/commands/fork.js +127 -0
- package/commands/init.js +50 -1
- package/commands/learn.js +407 -0
- package/commands/lifecycle.js +94 -0
- package/commands/loop.js +114 -0
- package/commands/publish.js +129 -0
- package/commands/pull.js +434 -48
- package/commands/push.js +312 -164
- package/commands/review.js +149 -0
- package/commands/run.js +76 -43
- package/commands/serve.js +360 -0
- package/commands/setup.js +1 -1
- package/commands/soul.js +381 -0
- package/commands/status.js +119 -1
- package/commands/sync.js +147 -1
- package/commands/terminal.js +201 -0
- package/commands/wiki.js +376 -0
- package/commands/workflow.js +191 -74
- package/commands/workspace-clean.js +3 -3
- package/lib/endstate.js +259 -0
- package/lib/learnings.js +235 -0
- package/lib/manifest.js +1 -0
- package/lib/todo.js +9 -5
- package/lib/wiki.js +578 -0
- package/package.json +2 -2
- package/utils/api.js +48 -36
- package/utils/auth.js +1 -0
package/commands/clean.js
CHANGED
|
@@ -50,6 +50,10 @@ function cleanAtris(options = {}) {
|
|
|
50
50
|
const cleaned = cleanEmptySections(atrisDir, options.dryRun);
|
|
51
51
|
results.cleanedSections = cleaned;
|
|
52
52
|
|
|
53
|
+
// 5. Find stale wiki pages (source newer than last_compiled)
|
|
54
|
+
const stalePages = findStalePages(cwd, atrisDir);
|
|
55
|
+
results.stalePages = stalePages;
|
|
56
|
+
|
|
53
57
|
// Report results
|
|
54
58
|
console.log('Results:');
|
|
55
59
|
console.log('');
|
|
@@ -99,7 +103,18 @@ function cleanAtris(options = {}) {
|
|
|
99
103
|
console.log('');
|
|
100
104
|
console.log('─────────────────────────────────────────────────────────────');
|
|
101
105
|
|
|
102
|
-
|
|
106
|
+
// Stale pages
|
|
107
|
+
if (stalePages.length > 0) {
|
|
108
|
+
console.log(`⚠ ${stalePages.length} stale page(s) (source changed since last compiled):`);
|
|
109
|
+
stalePages.forEach(sp => {
|
|
110
|
+
console.log(` • ${path.relative(cwd, sp.page)} — stale source: ${sp.staleSource}`);
|
|
111
|
+
});
|
|
112
|
+
console.log('');
|
|
113
|
+
} else {
|
|
114
|
+
console.log('✓ No stale wiki pages');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const hasIssues = staleTasks.length > 0 || unhealable.length > 0 || stalePages.length > 0;
|
|
103
118
|
if (hasIssues) {
|
|
104
119
|
console.log('Manual action needed:');
|
|
105
120
|
if (staleTasks.length > 0) {
|
|
@@ -108,6 +123,9 @@ function cleanAtris(options = {}) {
|
|
|
108
123
|
if (unhealable.length > 0) {
|
|
109
124
|
console.log(' • Manually fix unhealable MAP.md refs');
|
|
110
125
|
}
|
|
126
|
+
if (stalePages.length > 0) {
|
|
127
|
+
console.log(' • Re-compile stale pages (re-read sources, update content + last_compiled)');
|
|
128
|
+
}
|
|
111
129
|
} else {
|
|
112
130
|
console.log('Workspace is clean. Target state: 0 ✓');
|
|
113
131
|
}
|
|
@@ -369,6 +387,91 @@ function escapeRegExp(string) {
|
|
|
369
387
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
370
388
|
}
|
|
371
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Find wiki pages whose sources have been modified after last_compiled.
|
|
392
|
+
* Scans all .md files under atris/ for frontmatter with last_compiled + sources.
|
|
393
|
+
*/
|
|
394
|
+
function findStalePages(cwd, atrisDir) {
|
|
395
|
+
const stalePages = [];
|
|
396
|
+
|
|
397
|
+
function scanDir(dir) {
|
|
398
|
+
if (!fs.existsSync(dir)) return;
|
|
399
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
400
|
+
for (const entry of entries) {
|
|
401
|
+
const fullPath = path.join(dir, entry.name);
|
|
402
|
+
if (entry.isDirectory() && entry.name !== 'archive' && entry.name !== 'node_modules') {
|
|
403
|
+
scanDir(fullPath);
|
|
404
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
405
|
+
const result = checkPageStaleness(cwd, fullPath);
|
|
406
|
+
if (result) stalePages.push(result);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
scanDir(atrisDir);
|
|
412
|
+
return stalePages;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Parse frontmatter from a markdown file and check if any source is newer than last_compiled.
|
|
417
|
+
* Returns { page, staleSource, compiledDate, sourceDate } or null if not stale.
|
|
418
|
+
*/
|
|
419
|
+
function checkPageStaleness(cwd, filePath) {
|
|
420
|
+
let content;
|
|
421
|
+
try {
|
|
422
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
423
|
+
} catch { return null; }
|
|
424
|
+
|
|
425
|
+
// Check for YAML frontmatter
|
|
426
|
+
if (!content.startsWith('---')) return null;
|
|
427
|
+
|
|
428
|
+
const endIdx = content.indexOf('---', 3);
|
|
429
|
+
if (endIdx === -1) return null;
|
|
430
|
+
|
|
431
|
+
const frontmatter = content.substring(3, endIdx);
|
|
432
|
+
|
|
433
|
+
// Parse last_compiled
|
|
434
|
+
const compiledMatch = frontmatter.match(/last_compiled:\s*(\d{4}-\d{2}-\d{2})/);
|
|
435
|
+
if (!compiledMatch) return null;
|
|
436
|
+
|
|
437
|
+
const compiledDate = new Date(compiledMatch[1] + 'T23:59:59');
|
|
438
|
+
|
|
439
|
+
// Parse sources list
|
|
440
|
+
const sourcesMatch = frontmatter.match(/sources:\s*\n((?:\s+-\s+.+\n?)*)/);
|
|
441
|
+
if (!sourcesMatch) return null;
|
|
442
|
+
|
|
443
|
+
const sources = sourcesMatch[1]
|
|
444
|
+
.split('\n')
|
|
445
|
+
.map(line => line.replace(/^\s+-\s+/, '').trim())
|
|
446
|
+
.filter(Boolean);
|
|
447
|
+
|
|
448
|
+
// Check each source's mtime against last_compiled
|
|
449
|
+
for (const source of sources) {
|
|
450
|
+
const sourcePath = path.isAbsolute(source) ? source : path.join(cwd, source);
|
|
451
|
+
try {
|
|
452
|
+
const stat = fs.statSync(sourcePath);
|
|
453
|
+
if (stat.mtime > compiledDate) {
|
|
454
|
+
return {
|
|
455
|
+
page: filePath,
|
|
456
|
+
staleSource: source,
|
|
457
|
+
compiledDate: compiledMatch[1],
|
|
458
|
+
sourceDate: stat.mtime.toISOString().split('T')[0]
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
} catch {
|
|
462
|
+
// Source file doesn't exist — that's also a staleness signal
|
|
463
|
+
return {
|
|
464
|
+
page: filePath,
|
|
465
|
+
staleSource: source + ' (missing)',
|
|
466
|
+
compiledDate: compiledMatch[1],
|
|
467
|
+
sourceDate: 'N/A'
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
372
475
|
/**
|
|
373
476
|
* Archive journals older than 30 days
|
|
374
477
|
*/
|
|
@@ -452,5 +555,7 @@ module.exports = {
|
|
|
452
555
|
findStaleTasks,
|
|
453
556
|
healBrokenMapRefs,
|
|
454
557
|
archiveOldJournals,
|
|
455
|
-
cleanEmptySections
|
|
558
|
+
cleanEmptySections,
|
|
559
|
+
findStalePages,
|
|
560
|
+
checkPageStaleness
|
|
456
561
|
};
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atris Computer — interact with your EC2 AI Computer
|
|
3
|
+
*
|
|
4
|
+
* atris computer — Show status
|
|
5
|
+
* atris computer wake — Start the computer
|
|
6
|
+
* atris computer sleep — Stop (files persist)
|
|
7
|
+
* atris computer run <command> — Run bash on EC2 (no LLM)
|
|
8
|
+
* atris computer grep <pattern> — Search files on EC2
|
|
9
|
+
* atris computer ls [path] — List files
|
|
10
|
+
* atris computer cat <path> — Read a file
|
|
11
|
+
* atris computer exec <prompt> — Run with LLM (Claude Code)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { loadCredentials } = require('../utils/auth');
|
|
15
|
+
const { apiRequestJson } = require('../utils/api');
|
|
16
|
+
|
|
17
|
+
function getToken() {
|
|
18
|
+
const creds = loadCredentials();
|
|
19
|
+
if (!creds || !creds.token) {
|
|
20
|
+
console.error('Not logged in. Run: atris login');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
return creds.token;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function computerStatus(token) {
|
|
27
|
+
const result = await apiRequestJson('/ai-computer/user/status', {
|
|
28
|
+
method: 'GET',
|
|
29
|
+
token,
|
|
30
|
+
});
|
|
31
|
+
if (!result.ok) {
|
|
32
|
+
console.error(`Failed: ${result.errorMessage || result.status}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const d = result.data;
|
|
36
|
+
const status = d.status || 'unknown';
|
|
37
|
+
const icon = status === 'running' ? '●' : '○';
|
|
38
|
+
console.log(` ${icon} Computer: ${status}`);
|
|
39
|
+
console.log(` Agent: ${(d.agent_id || '?').slice(0, 12)}...`);
|
|
40
|
+
if (d.endpoint) console.log(` Endpoint: ${d.endpoint}`);
|
|
41
|
+
|
|
42
|
+
// If running, show soul stats
|
|
43
|
+
if (status === 'running') {
|
|
44
|
+
try {
|
|
45
|
+
const filesResult = await apiRequestJson('/ai-computer/files?path=soul', { method: 'GET', token });
|
|
46
|
+
if (filesResult.ok) {
|
|
47
|
+
const files = filesResult.data.files || [];
|
|
48
|
+
const totalSize = files.reduce((sum, f) => sum + (f.size || 0), 0);
|
|
49
|
+
const learnings = files.filter(f => f.name && f.name.startsWith('learning-')).length;
|
|
50
|
+
console.log(` Soul: ${files.length} files, ${(totalSize / 1024).toFixed(1)}KB`);
|
|
51
|
+
console.log(` Learnings: ${learnings} self-generated`);
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function computerWake(token) {
|
|
58
|
+
console.log('Waking computer...');
|
|
59
|
+
const result = await apiRequestJson('/ai-computer/user/wake', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
token,
|
|
62
|
+
body: {},
|
|
63
|
+
});
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
console.error(`Failed: ${result.errorMessage || result.status}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
console.log(` Status: ${result.data.status}`);
|
|
69
|
+
console.log(` Endpoint: ${result.data.endpoint}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function computerSleep(token) {
|
|
73
|
+
console.log('Sleeping computer...');
|
|
74
|
+
const result = await apiRequestJson('/ai-computer/user/sleep', {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
token,
|
|
77
|
+
body: {},
|
|
78
|
+
});
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
console.error(`Failed: ${result.errorMessage || result.status}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log(' Computer is sleeping. Files persist.');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function computerRun(token, command) {
|
|
87
|
+
if (!command) {
|
|
88
|
+
console.error('Usage: atris computer run <command>');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
const result = await apiRequestJson('/ai-computer/terminal', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
token,
|
|
94
|
+
body: { command },
|
|
95
|
+
});
|
|
96
|
+
if (!result.ok) {
|
|
97
|
+
if (result.status === 409 || (result.errorMessage || '').includes('running')) {
|
|
98
|
+
console.error('Computer is off. Run: atris computer wake');
|
|
99
|
+
} else {
|
|
100
|
+
console.error(`Failed: ${result.errorMessage || result.status}`);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const d = result.data;
|
|
105
|
+
if (d.stdout) process.stdout.write(d.stdout);
|
|
106
|
+
if (d.stderr) process.stderr.write(d.stderr);
|
|
107
|
+
if (d.exit_code && d.exit_code !== 0) {
|
|
108
|
+
console.error(`Exit: ${d.exit_code}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function computerGrep(token, pattern) {
|
|
113
|
+
if (!pattern) {
|
|
114
|
+
console.error('Usage: atris computer grep <pattern>');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
return computerRun(token, `grep -rni "${pattern}" . --include="*.md" --include="*.py" --include="*.js" --include="*.json" 2>/dev/null | head -30`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function computerLs(token, remotePath) {
|
|
121
|
+
const path = remotePath || '/';
|
|
122
|
+
const result = await apiRequestJson(`/ai-computer/files?path=${encodeURIComponent(path)}`, {
|
|
123
|
+
method: 'GET',
|
|
124
|
+
token,
|
|
125
|
+
});
|
|
126
|
+
if (!result.ok) {
|
|
127
|
+
console.error(`Failed: ${result.errorMessage || result.status}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
for (const f of (result.data.files || [])) {
|
|
131
|
+
const type = f.type === 'dir' ? 'DIR ' : ' ';
|
|
132
|
+
console.log(` ${type}${f.name} (${f.size || 0}b)`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function computerCat(token, remotePath) {
|
|
137
|
+
if (!remotePath) {
|
|
138
|
+
console.error('Usage: atris computer cat <path>');
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
const result = await apiRequestJson(`/ai-computer/file?path=${encodeURIComponent(remotePath)}`, {
|
|
142
|
+
method: 'GET',
|
|
143
|
+
token,
|
|
144
|
+
});
|
|
145
|
+
if (!result.ok) {
|
|
146
|
+
console.error(`Failed: ${result.errorMessage || result.status}`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
console.log(result.data.content || '');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function computerDiff(token, remotePath) {
|
|
153
|
+
const rPath = remotePath || 'soul';
|
|
154
|
+
const fs = require('fs');
|
|
155
|
+
const path = require('path');
|
|
156
|
+
const crypto = require('crypto');
|
|
157
|
+
|
|
158
|
+
// List remote files
|
|
159
|
+
const listResult = await apiRequestJson(`/ai-computer/files?path=${encodeURIComponent(rPath)}`, {
|
|
160
|
+
method: 'GET', token,
|
|
161
|
+
});
|
|
162
|
+
if (!listResult.ok) {
|
|
163
|
+
console.error(`Failed: ${listResult.errorMessage || listResult.status}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const remoteFiles = (listResult.data.files || []).filter(f => f.type === 'file');
|
|
167
|
+
|
|
168
|
+
// Compare with local ec2_pull/
|
|
169
|
+
const localDir = 'experiments/computer/ec2_pull';
|
|
170
|
+
let added = 0, modified = 0, same = 0, deleted = 0;
|
|
171
|
+
|
|
172
|
+
for (const f of remoteFiles) {
|
|
173
|
+
const localPath = path.join(localDir, f.name);
|
|
174
|
+
if (!fs.existsSync(localPath)) {
|
|
175
|
+
console.log(` + ${f.name} (${f.size}b) — new on EC2`);
|
|
176
|
+
added++;
|
|
177
|
+
} else {
|
|
178
|
+
const localSize = fs.statSync(localPath).size;
|
|
179
|
+
if (Math.abs(localSize - (f.size || 0)) > 10) {
|
|
180
|
+
console.log(` ~ ${f.name} (local: ${localSize}b, EC2: ${f.size}b) — changed`);
|
|
181
|
+
modified++;
|
|
182
|
+
} else {
|
|
183
|
+
same++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check for files deleted on EC2
|
|
189
|
+
if (fs.existsSync(localDir)) {
|
|
190
|
+
const remoteNames = new Set(remoteFiles.map(f => f.name));
|
|
191
|
+
for (const localFile of fs.readdirSync(localDir)) {
|
|
192
|
+
if (!remoteNames.has(localFile) && localFile.endsWith('.md')) {
|
|
193
|
+
console.log(` - ${localFile} — deleted on EC2`);
|
|
194
|
+
deleted++;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(`\n ${added} new, ${modified} changed, ${deleted} deleted, ${same} unchanged`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function computerPull(token, remotePath, localDir) {
|
|
203
|
+
const rPath = remotePath || 'soul';
|
|
204
|
+
const lDir = localDir || 'ec2_pull';
|
|
205
|
+
const fs = require('fs');
|
|
206
|
+
const path = require('path');
|
|
207
|
+
|
|
208
|
+
// List files
|
|
209
|
+
const listResult = await apiRequestJson(`/ai-computer/files?path=${encodeURIComponent(rPath)}`, {
|
|
210
|
+
method: 'GET', token,
|
|
211
|
+
});
|
|
212
|
+
if (!listResult.ok) {
|
|
213
|
+
console.error(`Failed to list: ${listResult.errorMessage || listResult.status}`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const files = (listResult.data.files || []).filter(f => f.type === 'file');
|
|
217
|
+
if (files.length === 0) {
|
|
218
|
+
console.log(' No files to pull.');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Create local dir
|
|
223
|
+
fs.mkdirSync(lDir, { recursive: true });
|
|
224
|
+
console.log(`Pulling ${files.length} files from ${rPath}/ → ${lDir}/`);
|
|
225
|
+
|
|
226
|
+
let pulled = 0;
|
|
227
|
+
for (const f of files) {
|
|
228
|
+
const fileResult = await apiRequestJson(
|
|
229
|
+
`/ai-computer/file?path=${encodeURIComponent(rPath + '/' + f.name)}`,
|
|
230
|
+
{ method: 'GET', token, timeoutMs: 15000 }
|
|
231
|
+
);
|
|
232
|
+
if (fileResult.ok && fileResult.data.content) {
|
|
233
|
+
const localPath = path.join(lDir, f.name);
|
|
234
|
+
fs.writeFileSync(localPath, fileResult.data.content);
|
|
235
|
+
console.log(` ${f.name} (${fileResult.data.content.length}b)`);
|
|
236
|
+
pulled++;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
console.log(`\n Pulled ${pulled} files.`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function computerOnboard(token, businessSlug) {
|
|
243
|
+
if (!businessSlug) {
|
|
244
|
+
console.error('Usage: atris computer onboard <business-slug>');
|
|
245
|
+
console.error('');
|
|
246
|
+
console.error('Sets up a new business computer with soul, tools, and first learning cycle.');
|
|
247
|
+
console.error('The business must already exist (atris business create).');
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const fs = require('fs');
|
|
252
|
+
const path = require('path');
|
|
253
|
+
|
|
254
|
+
console.log(`\nOnboarding "${businessSlug}"...`);
|
|
255
|
+
|
|
256
|
+
// Step 1: Push soul template
|
|
257
|
+
console.log('\n 1. Pushing soul template...');
|
|
258
|
+
const soulTemplate = `# Soul — ${businessSlug}\n\n## Identity\nBusiness computer for ${businessSlug}. Self-improving context system.\n\n## Goals\n- Learn the business overnight\n- Accumulate context that makes the agent smarter\n- Track what works and what doesn't\n\n## Rules\n- No emails without approval\n- No destructive actions\n- Save everything to soul/\n`;
|
|
259
|
+
|
|
260
|
+
const templateResult = await apiRequestJson('/ai-computer/terminal', {
|
|
261
|
+
method: 'POST', token,
|
|
262
|
+
body: { command: `mkdir -p soul tools && echo '${soulTemplate.replace(/'/g, "'\\''")}' > soul/soul.md && echo "Soul created"` },
|
|
263
|
+
});
|
|
264
|
+
if (templateResult.ok) console.log(' Soul template created');
|
|
265
|
+
|
|
266
|
+
// Step 2: Push tools
|
|
267
|
+
console.log(' 2. Pushing tools...');
|
|
268
|
+
const toolsResult = await apiRequestJson('/ai-computer/terminal', {
|
|
269
|
+
method: 'POST', token,
|
|
270
|
+
body: { command: [
|
|
271
|
+
'cat > tools/rebuild_index.sh << \'TOOLEOF\'',
|
|
272
|
+
'#!/bin/bash',
|
|
273
|
+
'echo "# Context Index" > soul/INDEX.md',
|
|
274
|
+
'echo "" >> soul/INDEX.md',
|
|
275
|
+
'echo "Last updated: $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> soul/INDEX.md',
|
|
276
|
+
'echo "" >> soul/INDEX.md',
|
|
277
|
+
'COUNT=0; TOTAL_SIZE=0',
|
|
278
|
+
'for f in soul/*.md; do',
|
|
279
|
+
' [ "$f" = "soul/INDEX.md" ] && continue',
|
|
280
|
+
' NAME=$(basename $f .md)',
|
|
281
|
+
' FIRST_LINE=$(head -1 $f | sed "s/^# //")',
|
|
282
|
+
' SIZE=$(wc -c < $f | tr -d " ")',
|
|
283
|
+
' echo "- [$NAME]($NAME.md) — $FIRST_LINE ($SIZE bytes)" >> soul/INDEX.md',
|
|
284
|
+
' COUNT=$((COUNT + 1)); TOTAL_SIZE=$((TOTAL_SIZE + SIZE))',
|
|
285
|
+
'done',
|
|
286
|
+
'echo "" >> soul/INDEX.md',
|
|
287
|
+
'echo "**Total: $COUNT files, $TOTAL_SIZE bytes**" >> soul/INDEX.md',
|
|
288
|
+
'echo "Indexed $COUNT files ($TOTAL_SIZE bytes)"',
|
|
289
|
+
'TOOLEOF',
|
|
290
|
+
'chmod +x tools/rebuild_index.sh',
|
|
291
|
+
'bash tools/rebuild_index.sh',
|
|
292
|
+
].join('\n') },
|
|
293
|
+
});
|
|
294
|
+
if (toolsResult.ok) console.log(` Tools installed. ${(toolsResult.data.stdout || '').trim()}`);
|
|
295
|
+
|
|
296
|
+
// Step 3: Trigger first learning cycle
|
|
297
|
+
console.log(' 3. Starting first learning cycle...');
|
|
298
|
+
const learnResult = await apiRequestJson('/ai-computer/execute', {
|
|
299
|
+
method: 'POST', token,
|
|
300
|
+
body: {
|
|
301
|
+
prompt: `You are a brand new AI computer for a business called "${businessSlug}". ` +
|
|
302
|
+
`Read your soul/soul.md to understand your identity. ` +
|
|
303
|
+
`Then write soul/learning-001.md with your first observation: ` +
|
|
304
|
+
`what information do you need to be useful? What should the business owner push to you first? ` +
|
|
305
|
+
`Be specific about what files/data would make you most helpful. ` +
|
|
306
|
+
`Then run: bash tools/rebuild_index.sh`,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
if (learnResult.ok) {
|
|
310
|
+
console.log(` Learning cycle started: ${learnResult.data.execution_id}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
console.log(`\n ✓ Computer onboarded for "${businessSlug}"`);
|
|
314
|
+
console.log('');
|
|
315
|
+
console.log(' Next steps:');
|
|
316
|
+
console.log(` atris push ${businessSlug} --from <your-context-dir> Push your business files`);
|
|
317
|
+
console.log(` atris computer diff soul See what the computer learned`);
|
|
318
|
+
console.log(` atris computer learn Trigger another learning cycle`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function computerLearn(token) {
|
|
322
|
+
console.log('Starting learning cycle on EC2...');
|
|
323
|
+
|
|
324
|
+
// First check how many learnings exist
|
|
325
|
+
const countResult = await apiRequestJson('/ai-computer/terminal', {
|
|
326
|
+
method: 'POST', token,
|
|
327
|
+
body: { command: 'ls soul/learning-*.md 2>/dev/null | wc -l | tr -d " "' },
|
|
328
|
+
});
|
|
329
|
+
const count = parseInt((countResult.ok && countResult.data.stdout || '0').trim()) || 0;
|
|
330
|
+
const next = String(count + 1).padStart(3, '0');
|
|
331
|
+
console.log(` Existing learnings: ${count}`);
|
|
332
|
+
console.log(` Next: soul/learning-${next}.md`);
|
|
333
|
+
|
|
334
|
+
// Trigger LLM learning cycle
|
|
335
|
+
const prompt = `Self-improvement cycle. Read soul/INDEX.md to see what you know. ` +
|
|
336
|
+
`Check existing soul/learning-*.md files to avoid repeating topics. ` +
|
|
337
|
+
`Pick ONE new topic that would make the overnight agent better at earning money for the business owner. ` +
|
|
338
|
+
`Research it using the files and tools available. ` +
|
|
339
|
+
`Write your finding to soul/learning-${next}.md. Be specific and actionable. ` +
|
|
340
|
+
`Then run: bash tools/rebuild_index.sh`;
|
|
341
|
+
|
|
342
|
+
const result = await apiRequestJson('/ai-computer/execute', {
|
|
343
|
+
method: 'POST', token,
|
|
344
|
+
body: { prompt },
|
|
345
|
+
});
|
|
346
|
+
if (!result.ok) {
|
|
347
|
+
console.error(`Failed: ${result.errorMessage || result.status}`);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
console.log(` Learning cycle started: ${result.data.execution_id}`);
|
|
351
|
+
console.log(` The computer is thinking... check back with: atris computer diff soul`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function computerExec(token, prompt) {
|
|
355
|
+
if (!prompt) {
|
|
356
|
+
console.error('Usage: atris computer exec "<prompt>"');
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
console.log('Executing on computer (with LLM)...');
|
|
360
|
+
const result = await apiRequestJson('/ai-computer/execute', {
|
|
361
|
+
method: 'POST',
|
|
362
|
+
token,
|
|
363
|
+
body: { prompt },
|
|
364
|
+
});
|
|
365
|
+
if (!result.ok) {
|
|
366
|
+
console.error(`Failed: ${result.errorMessage || result.status}`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
console.log(` Execution: ${result.data.execution_id}`);
|
|
370
|
+
console.log(` Stream: ${result.data.endpoint}/events/stream?execution_id=${result.data.execution_id}`);
|
|
371
|
+
console.log(' Use the stream URL to watch progress.');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function runComputer() {
|
|
375
|
+
const sub = process.argv[3];
|
|
376
|
+
|
|
377
|
+
if (!sub || sub === '--help') {
|
|
378
|
+
console.log('Usage: atris computer <command>');
|
|
379
|
+
console.log('');
|
|
380
|
+
console.log('Commands:');
|
|
381
|
+
console.log(' status Show computer status');
|
|
382
|
+
console.log(' wake Start the computer');
|
|
383
|
+
console.log(' sleep Stop the computer (files persist)');
|
|
384
|
+
console.log(' run <cmd> Run bash on EC2 (no LLM cost)');
|
|
385
|
+
console.log(' grep <pattern> Search files on EC2');
|
|
386
|
+
console.log(' ls [path] List files');
|
|
387
|
+
console.log(' cat <path> Read a file');
|
|
388
|
+
console.log(' exec "<prompt>" Run with LLM (Claude Code)');
|
|
389
|
+
console.log(' pull [path] [dir] Pull files from EC2 to local');
|
|
390
|
+
console.log(' diff [path] Show what changed on EC2 since last pull');
|
|
391
|
+
console.log(' learn Trigger autonomous learning cycle');
|
|
392
|
+
console.log(' onboard <slug> Set up a new business computer');
|
|
393
|
+
console.log('');
|
|
394
|
+
console.log('Examples:');
|
|
395
|
+
console.log(' atris computer status');
|
|
396
|
+
console.log(' atris computer wake');
|
|
397
|
+
console.log(' atris computer run "ls -la /workspace"');
|
|
398
|
+
console.log(' atris computer grep "overnight"');
|
|
399
|
+
console.log(' atris computer cat soul/soul.md');
|
|
400
|
+
console.log(' atris computer exec "Read soul/ and suggest what to work on"');
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const token = getToken();
|
|
405
|
+
const rest = process.argv.slice(4).join(' ');
|
|
406
|
+
|
|
407
|
+
switch (sub) {
|
|
408
|
+
case 'status': return computerStatus(token);
|
|
409
|
+
case 'wake': return computerWake(token);
|
|
410
|
+
case 'sleep': return computerSleep(token);
|
|
411
|
+
case 'run': return computerRun(token, rest);
|
|
412
|
+
case 'grep': return computerGrep(token, rest);
|
|
413
|
+
case 'ls': return computerLs(token, rest || undefined);
|
|
414
|
+
case 'cat': return computerCat(token, rest);
|
|
415
|
+
case 'exec': return computerExec(token, rest);
|
|
416
|
+
case 'pull': {
|
|
417
|
+
const parts = rest.split(' ').filter(Boolean);
|
|
418
|
+
return computerPull(token, parts[0], parts[1]);
|
|
419
|
+
}
|
|
420
|
+
case 'diff': return computerDiff(token, rest || undefined);
|
|
421
|
+
case 'learn': return computerLearn(token);
|
|
422
|
+
case 'onboard': return computerOnboard(token, rest);
|
|
423
|
+
default:
|
|
424
|
+
console.error(`Unknown subcommand: ${sub}`);
|
|
425
|
+
console.log('Run: atris computer --help');
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
module.exports = { runComputer };
|
package/commands/context-sync.js
CHANGED
|
@@ -26,7 +26,7 @@ async function resolveBusiness(slug) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const listResult = await apiRequestJson('/
|
|
29
|
+
const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
|
|
30
30
|
if (!listResult.ok) {
|
|
31
31
|
console.error(`Failed to fetch businesses: ${listResult.errorMessage || listResult.status}`);
|
|
32
32
|
process.exit(1);
|
|
@@ -67,15 +67,17 @@ async function businessStatus(slug) {
|
|
|
67
67
|
console.log(`${biz.name}` + (timeSince ? ` \u2014 last synced ${timeSince}` : ' \u2014 never synced'));
|
|
68
68
|
|
|
69
69
|
// Determine local directory
|
|
70
|
-
const atrisDir = path.join(process.cwd(), 'atris', slug);
|
|
71
|
-
const cwdDir = path.join(process.cwd(), slug);
|
|
72
70
|
let localDir = null;
|
|
73
|
-
if (fs.existsSync(
|
|
74
|
-
|
|
71
|
+
if (fs.existsSync(path.join(process.cwd(), '.atris', 'business.json'))) {
|
|
72
|
+
localDir = process.cwd();
|
|
73
|
+
} else {
|
|
74
|
+
const cwdDir = path.join(process.cwd(), slug);
|
|
75
|
+
if (fs.existsSync(cwdDir)) localDir = cwdDir;
|
|
76
|
+
}
|
|
75
77
|
|
|
76
78
|
// Get remote snapshot with content (needed for reliable hash computation)
|
|
77
79
|
const result = await apiRequestJson(
|
|
78
|
-
`/
|
|
80
|
+
`/business/${biz.businessId}/workspaces/${biz.workspaceId}/snapshot?include_content=true`,
|
|
79
81
|
{ method: 'GET', token: biz.token, timeoutMs: 120000 }
|
|
80
82
|
);
|
|
81
83
|
|
|
@@ -152,6 +154,74 @@ async function businessStatus(slug) {
|
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
|
|
157
|
+
/**
|
|
158
|
+
* atris diff <business-slug> [path]
|
|
159
|
+
* Shows actual content diff between local and remote files.
|
|
160
|
+
*/
|
|
161
|
+
async function businessDiff(slug) {
|
|
162
|
+
const biz = await resolveBusiness(slug);
|
|
163
|
+
if (!biz.workspaceId) { console.error(`Business "${slug}" has no workspace.`); process.exit(1); }
|
|
164
|
+
|
|
165
|
+
const manifest = loadManifest(biz.slug);
|
|
166
|
+
const pathFilter = process.argv[4] && !process.argv[4].startsWith('-') ? process.argv[4] : null;
|
|
167
|
+
|
|
168
|
+
// Find local dir
|
|
169
|
+
let localDir = null;
|
|
170
|
+
if (fs.existsSync(path.join(process.cwd(), '.atris', 'business.json'))) {
|
|
171
|
+
localDir = process.cwd();
|
|
172
|
+
} else {
|
|
173
|
+
const cwdDir = path.join(process.cwd(), slug);
|
|
174
|
+
if (fs.existsSync(cwdDir)) localDir = cwdDir;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!localDir) {
|
|
178
|
+
console.error(`No local copy found. Run: atris pull ${slug}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const localFiles = computeLocalHashes(localDir);
|
|
183
|
+
const baseFiles = (manifest && manifest.files) ? manifest.files : {};
|
|
184
|
+
|
|
185
|
+
// Find changed files
|
|
186
|
+
const changed = [];
|
|
187
|
+
for (const [filePath, info] of Object.entries(localFiles)) {
|
|
188
|
+
if (pathFilter && !filePath.replace(/^\//, '').startsWith(pathFilter)) continue;
|
|
189
|
+
const baseHash = baseFiles[filePath] ? baseFiles[filePath].hash : null;
|
|
190
|
+
if (!baseHash || info.hash !== baseHash) {
|
|
191
|
+
changed.push({ path: filePath, isNew: !baseHash });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (changed.length === 0) {
|
|
196
|
+
console.log('\n No local changes.\n');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(`\n ${changed.length} file${changed.length > 1 ? 's' : ''} changed locally:\n`);
|
|
201
|
+
|
|
202
|
+
for (const f of changed) {
|
|
203
|
+
const localPath = path.join(localDir, f.path.replace(/^\//, ''));
|
|
204
|
+
const localContent = fs.readFileSync(localPath, 'utf8');
|
|
205
|
+
|
|
206
|
+
if (f.isNew) {
|
|
207
|
+
console.log(` + ${f.path.replace(/^\//, '')} (new file, ${localContent.split('\n').length} lines)`);
|
|
208
|
+
} else {
|
|
209
|
+
// Show a simple line-count diff
|
|
210
|
+
console.log(` ~ ${f.path.replace(/^\//, '')} (modified)`);
|
|
211
|
+
// Show first few changed lines as preview
|
|
212
|
+
const lines = localContent.split('\n');
|
|
213
|
+
const preview = lines.slice(-5).filter(l => l.trim());
|
|
214
|
+
if (preview.length > 0) {
|
|
215
|
+
for (const line of preview.slice(0, 3)) {
|
|
216
|
+
console.log(` | ${line.substring(0, 80)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
console.log('');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
155
225
|
/**
|
|
156
226
|
* atris log <business-slug>
|
|
157
227
|
* Shows human-readable commit history.
|
|
@@ -169,7 +239,7 @@ async function businessLog(slug) {
|
|
|
169
239
|
|
|
170
240
|
const params = `limit=${limit}` + (pathFilter ? `&path=${encodeURIComponent(pathFilter)}` : '');
|
|
171
241
|
const result = await apiRequestJson(
|
|
172
|
-
`/
|
|
242
|
+
`/business/${biz.businessId}/workspaces/${biz.workspaceId}/git/log?${params}`,
|
|
173
243
|
{ method: 'GET', token: biz.token }
|
|
174
244
|
);
|
|
175
245
|
|
|
@@ -225,4 +295,4 @@ function _timeSince(isoString) {
|
|
|
225
295
|
}
|
|
226
296
|
|
|
227
297
|
|
|
228
|
-
module.exports = { businessStatus, businessLog };
|
|
298
|
+
module.exports = { businessStatus, businessDiff, businessLog };
|