atris 2.6.3 → 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.
Files changed (54) hide show
  1. package/README.md +124 -34
  2. package/atris/CLAUDE.md +5 -1
  3. package/atris/atris.md +4 -0
  4. package/atris/features/README.md +24 -0
  5. package/atris/skills/autopilot/SKILL.md +74 -75
  6. package/atris/skills/endgame/SKILL.md +179 -0
  7. package/atris/skills/flow/SKILL.md +121 -0
  8. package/atris/skills/improve/SKILL.md +84 -0
  9. package/atris/skills/loop/SKILL.md +72 -0
  10. package/atris/skills/wiki/SKILL.md +61 -0
  11. package/atris/team/executor/MEMBER.md +10 -4
  12. package/atris/team/navigator/MEMBER.md +2 -0
  13. package/atris/team/validator/MEMBER.md +8 -5
  14. package/atris.md +33 -0
  15. package/bin/atris.js +210 -41
  16. package/commands/activate.js +28 -2
  17. package/commands/align.js +720 -0
  18. package/commands/auth.js +75 -2
  19. package/commands/autopilot.js +1213 -270
  20. package/commands/browse.js +100 -0
  21. package/commands/business.js +785 -12
  22. package/commands/clean.js +107 -2
  23. package/commands/computer.js +429 -0
  24. package/commands/context-sync.js +78 -8
  25. package/commands/experiments.js +351 -0
  26. package/commands/feedback.js +150 -0
  27. package/commands/fleet.js +395 -0
  28. package/commands/fork.js +127 -0
  29. package/commands/init.js +50 -1
  30. package/commands/learn.js +407 -0
  31. package/commands/lifecycle.js +94 -0
  32. package/commands/loop.js +114 -0
  33. package/commands/publish.js +129 -0
  34. package/commands/pull.js +369 -38
  35. package/commands/push.js +283 -246
  36. package/commands/review.js +149 -0
  37. package/commands/run.js +76 -43
  38. package/commands/serve.js +360 -0
  39. package/commands/setup.js +1 -1
  40. package/commands/soul.js +381 -0
  41. package/commands/status.js +119 -1
  42. package/commands/sync.js +147 -1
  43. package/commands/terminal.js +201 -0
  44. package/commands/wiki.js +376 -0
  45. package/commands/workflow.js +191 -74
  46. package/commands/workspace-clean.js +3 -3
  47. package/lib/endstate.js +259 -0
  48. package/lib/learnings.js +235 -0
  49. package/lib/manifest.js +1 -0
  50. package/lib/todo.js +9 -5
  51. package/lib/wiki.js +578 -0
  52. package/package.json +2 -2
  53. package/utils/api.js +40 -35
  54. package/utils/auth.js +1 -0
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Atris Serve — make this directory a live AI Computer.
3
+ *
4
+ * atris serve Start the bridge in current directory
5
+ * atris serve --agent <agent_id> Bind to a specific agent
6
+ * atris serve --once <op_id> Apply one queued op and exit (debug)
7
+ *
8
+ * Opens a session with the Atris backend, subscribes via SSE to incoming
9
+ * file operations, and applies them to the local working directory.
10
+ *
11
+ * Cloud agents (or any authenticated caller) can dispatch operations:
12
+ * POST /api/cli/sessions/{id}/file-op
13
+ *
14
+ * Operations supported:
15
+ * - write: create or replace a file
16
+ * - edit: find/replace in a file
17
+ * - read: read a file (returns content via ack)
18
+ * - delete: remove a file
19
+ * - bash: run a shell command
20
+ *
21
+ * Path safety: all paths are resolved against the working directory.
22
+ * Anything that escapes is rejected.
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const https = require('https');
28
+ const http = require('http');
29
+ const { execSync } = require('child_process');
30
+ const { loadCredentials } = require('../utils/auth');
31
+ const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
32
+
33
+ const HEARTBEAT_INTERVAL_MS = 30000;
34
+ const RECONNECT_DELAY_MS = 2000;
35
+ const MAX_RECONNECT_DELAY_MS = 30000;
36
+ // Bash commands are bounded to 10s — long batches won't lock the CLI for hours
37
+ const BASH_TIMEOUT_MS = 10000;
38
+ // Hard size limits to prevent OOM on large payloads
39
+ const MAX_WRITE_BYTES = 10 * 1024 * 1024; // 10 MB
40
+ const MAX_EDIT_BYTES = 1 * 1024 * 1024; // 1 MB find/replace
41
+
42
+ function getToken() {
43
+ const creds = loadCredentials();
44
+ if (!creds || !creds.token) {
45
+ console.error('Not logged in. Run: atris login');
46
+ process.exit(1);
47
+ }
48
+ return creds.token;
49
+ }
50
+
51
+ /**
52
+ * Validate a path stays inside the working directory.
53
+ * Returns the absolute resolved path or throws.
54
+ */
55
+ function safePath(workingDir, requestedPath) {
56
+ if (!requestedPath || typeof requestedPath !== 'string') {
57
+ throw new Error('path required');
58
+ }
59
+ if (requestedPath.startsWith('/')) {
60
+ throw new Error('path must be relative');
61
+ }
62
+ if (requestedPath.split('/').includes('..')) {
63
+ throw new Error('path may not contain ..');
64
+ }
65
+ const realWd = fs.realpathSync(workingDir);
66
+ const resolved = path.resolve(realWd, requestedPath);
67
+ // Walk up to find a parent that exists, then realpath that
68
+ let parent = path.dirname(resolved);
69
+ while (parent && !fs.existsSync(parent) && parent !== path.dirname(parent)) {
70
+ parent = path.dirname(parent);
71
+ }
72
+ const realParent = fs.existsSync(parent) ? fs.realpathSync(parent) : parent;
73
+ // The realParent must be inside realWd
74
+ if (!realParent.startsWith(realWd + path.sep) && realParent !== realWd) {
75
+ throw new Error('path escapes working directory');
76
+ }
77
+ return resolved;
78
+ }
79
+
80
+ /**
81
+ * Apply a single operation locally. Returns { status, result }.
82
+ */
83
+ async function applyOp(workingDir, op) {
84
+ try {
85
+ const type = op.type;
86
+
87
+ if (type === 'write') {
88
+ const content = op.content || '';
89
+ const bytes = Buffer.byteLength(content, 'utf8');
90
+ if (bytes > MAX_WRITE_BYTES) {
91
+ return { status: 'error', result: { error: `content exceeds ${MAX_WRITE_BYTES} bytes` } };
92
+ }
93
+ const target = safePath(workingDir, op.path);
94
+ fs.mkdirSync(path.dirname(target), { recursive: true });
95
+ fs.writeFileSync(target, content, 'utf8');
96
+ return { status: 'ok', result: { bytes_written: bytes } };
97
+ }
98
+
99
+ if (type === 'read') {
100
+ const target = safePath(workingDir, op.path);
101
+ if (!fs.existsSync(target)) {
102
+ return { status: 'error', result: { error: 'file not found' } };
103
+ }
104
+ const content = fs.readFileSync(target, 'utf8');
105
+ return { status: 'ok', result: { content, bytes: Buffer.byteLength(content, 'utf8') } };
106
+ }
107
+
108
+ if (type === 'edit') {
109
+ if (Buffer.byteLength(op.find || '', 'utf8') > MAX_EDIT_BYTES ||
110
+ Buffer.byteLength(op.replace || '', 'utf8') > MAX_EDIT_BYTES) {
111
+ return { status: 'error', result: { error: `find/replace exceeds ${MAX_EDIT_BYTES} bytes` } };
112
+ }
113
+ const target = safePath(workingDir, op.path);
114
+ if (!fs.existsSync(target)) {
115
+ return { status: 'error', result: { error: 'file not found' } };
116
+ }
117
+ const stat = fs.statSync(target);
118
+ if (stat.size > MAX_WRITE_BYTES) {
119
+ return { status: 'error', result: { error: `file too large (>${MAX_WRITE_BYTES} bytes)` } };
120
+ }
121
+ const original = fs.readFileSync(target, 'utf8');
122
+ if (!original.includes(op.find)) {
123
+ return { status: 'error', result: { error: 'find string not present' } };
124
+ }
125
+ const updated = original.split(op.find).join(op.replace);
126
+ fs.writeFileSync(target, updated, 'utf8');
127
+ return { status: 'ok', result: { replacements: original.split(op.find).length - 1 } };
128
+ }
129
+
130
+ if (type === 'delete') {
131
+ const target = safePath(workingDir, op.path);
132
+ if (!fs.existsSync(target)) {
133
+ return { status: 'error', result: { error: 'file not found' } };
134
+ }
135
+ fs.unlinkSync(target);
136
+ return { status: 'ok', result: { deleted: true } };
137
+ }
138
+
139
+ if (type === 'bash') {
140
+ // Execute bash in the working directory with a timeout.
141
+ // Note: This is a powerful op — only the session owner can dispatch it,
142
+ // AND the session must have been created with allow_bash=true.
143
+ try {
144
+ const stdout = execSync(op.command, {
145
+ cwd: workingDir,
146
+ timeout: BASH_TIMEOUT_MS,
147
+ encoding: 'utf8',
148
+ maxBuffer: 10 * 1024 * 1024,
149
+ });
150
+ const truncated = stdout.length > 100000;
151
+ return {
152
+ status: 'ok',
153
+ result: { stdout: stdout.slice(0, 100000), truncated, exit_code: 0 },
154
+ };
155
+ } catch (execErr) {
156
+ const stderr = (execErr.stderr || '').toString();
157
+ const stdout = (execErr.stdout || '').toString();
158
+ return {
159
+ status: 'error',
160
+ result: {
161
+ error: execErr.message,
162
+ stdout: stdout.slice(0, 100000),
163
+ stderr: stderr.slice(0, 100000),
164
+ stdout_truncated: stdout.length > 100000,
165
+ stderr_truncated: stderr.length > 100000,
166
+ exit_code: execErr.status ?? 1,
167
+ },
168
+ };
169
+ }
170
+ }
171
+
172
+ return { status: 'error', result: { error: `unknown op type: ${type}` } };
173
+ } catch (err) {
174
+ return { status: 'error', result: { error: err.message } };
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Open an SSE stream to /api/cli/sessions/{id}/events and apply each op.
180
+ */
181
+ function streamSession(token, sessionId, workingDir) {
182
+ const baseUrl = getApiBaseUrl();
183
+ const url = new URL(`${baseUrl}/cli/sessions/${sessionId}/events`);
184
+ const isHttps = url.protocol === 'https:';
185
+ const transport = isHttps ? https : http;
186
+
187
+ const options = {
188
+ method: 'GET',
189
+ hostname: url.hostname,
190
+ port: url.port || (isHttps ? 443 : 80),
191
+ path: `${url.pathname}${url.search}`,
192
+ headers: {
193
+ 'Authorization': `Bearer ${token}`,
194
+ 'Accept': 'text/event-stream',
195
+ 'Cache-Control': 'no-cache',
196
+ },
197
+ timeout: 0,
198
+ };
199
+
200
+ return new Promise((resolve, reject) => {
201
+ const req = transport.request(options, (res) => {
202
+ if (res.statusCode !== 200) {
203
+ let body = '';
204
+ res.on('data', (chunk) => { body += chunk; });
205
+ res.on('end', () => reject(new Error(`SSE failed: ${res.statusCode} ${body}`)));
206
+ return;
207
+ }
208
+
209
+ console.log(`● Bridge active — listening for ops on session ${sessionId.slice(0, 8)}...`);
210
+
211
+ let buffer = '';
212
+ res.on('data', async (chunk) => {
213
+ buffer += chunk.toString();
214
+ const messages = buffer.split('\n\n');
215
+ buffer = messages.pop() || '';
216
+
217
+ for (const msg of messages) {
218
+ if (!msg.startsWith('data: ')) continue;
219
+ const dataStr = msg.slice(6).trim();
220
+ if (!dataStr) continue;
221
+
222
+ let event;
223
+ try {
224
+ event = JSON.parse(dataStr);
225
+ } catch {
226
+ continue;
227
+ }
228
+
229
+ if (event.type === 'ping' || event.type === 'hello') {
230
+ continue;
231
+ }
232
+ if (event.type === 'close') {
233
+ console.log(' ✓ Session closed by backend');
234
+ res.destroy();
235
+ return;
236
+ }
237
+
238
+ // Apply the operation
239
+ const startMs = Date.now();
240
+ const ackPayload = await applyOp(workingDir, event);
241
+ const durationMs = Date.now() - startMs;
242
+
243
+ const icon = ackPayload.status === 'ok' ? '✓' : '✗';
244
+ console.log(` ${icon} ${event.type} ${event.path || event.command || ''} (${durationMs}ms)`);
245
+
246
+ // Send the ack
247
+ try {
248
+ await apiRequestJson(`/cli/sessions/${sessionId}/ack`, {
249
+ method: 'POST',
250
+ token,
251
+ body: {
252
+ op_id: event.op_id,
253
+ status: ackPayload.status,
254
+ result: ackPayload.result,
255
+ },
256
+ });
257
+ } catch (ackErr) {
258
+ console.error(` failed to ack: ${ackErr.message}`);
259
+ }
260
+ }
261
+ });
262
+
263
+ res.on('end', () => {
264
+ console.log(' · Stream ended');
265
+ resolve();
266
+ });
267
+ res.on('error', reject);
268
+ });
269
+
270
+ req.on('error', reject);
271
+ req.end();
272
+ });
273
+ }
274
+
275
+ async function serveAtris(options = {}) {
276
+ const token = getToken();
277
+ const workingDir = process.cwd();
278
+ const agentId = options.agent || null;
279
+ const allowBash = options.allowBash === true;
280
+
281
+ console.log('');
282
+ console.log('╭──────────────────────────────────────────╮');
283
+ console.log('│ ATRIS SERVE — Local AI Computer Bridge │');
284
+ console.log('╰──────────────────────────────────────────╯');
285
+ console.log('');
286
+ console.log(` 📁 Directory: ${workingDir}`);
287
+ console.log(` 🤖 Agent: ${agentId || '(none)'}`);
288
+ console.log(` ⚡ Bash: ${allowBash ? 'enabled (REMOTE BASH ALLOWED)' : 'disabled (read/write/edit/delete only)'}`);
289
+ console.log('');
290
+
291
+ // Register the session
292
+ let session;
293
+ try {
294
+ const result = await apiRequestJson('/cli/sessions', {
295
+ method: 'POST',
296
+ token,
297
+ body: {
298
+ working_directory: workingDir,
299
+ agent_id: agentId,
300
+ allow_bash: allowBash,
301
+ },
302
+ });
303
+ if (!result.ok) {
304
+ console.error(`✗ Failed to create session: ${result.errorMessage || result.status}`);
305
+ process.exit(1);
306
+ }
307
+ session = result.data;
308
+ } catch (err) {
309
+ console.error(`✗ Could not register session: ${err.message}`);
310
+ process.exit(1);
311
+ }
312
+
313
+ console.log(` ✓ Session: ${session.session_id}`);
314
+ console.log('');
315
+ console.log(' Cloud agents can now modify files in this directory via:');
316
+ console.log(` POST /api/cli/sessions/${session.session_id}/file-op`);
317
+ console.log('');
318
+ console.log(' Press Ctrl+C to stop.');
319
+ console.log('');
320
+
321
+ // Cleanup on exit
322
+ let shuttingDown = false;
323
+ const shutdown = async () => {
324
+ if (shuttingDown) return;
325
+ shuttingDown = true;
326
+ console.log('\n · Closing session...');
327
+ try {
328
+ await apiRequestJson(`/cli/sessions/${session.session_id}`, {
329
+ method: 'DELETE',
330
+ token,
331
+ });
332
+ } catch {
333
+ // best effort
334
+ }
335
+ process.exit(0);
336
+ };
337
+ process.on('SIGINT', shutdown);
338
+ process.on('SIGTERM', shutdown);
339
+
340
+ // Reconnect loop with exponential backoff
341
+ let reconnectDelay = RECONNECT_DELAY_MS;
342
+ while (!shuttingDown) {
343
+ try {
344
+ await streamSession(token, session.session_id, workingDir);
345
+ reconnectDelay = RECONNECT_DELAY_MS; // reset on clean disconnect
346
+ } catch (err) {
347
+ if (shuttingDown) break;
348
+ console.error(` ⚠ Stream error: ${err.message}, reconnecting in ${reconnectDelay / 1000}s...`);
349
+ await new Promise((r) => setTimeout(r, reconnectDelay));
350
+ reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
351
+ }
352
+ }
353
+ }
354
+
355
+ module.exports = {
356
+ serveAtris,
357
+ // exported for testing
358
+ safePath,
359
+ applyOp,
360
+ };
package/commands/setup.js CHANGED
@@ -68,7 +68,7 @@ async function setupAtris() {
68
68
  console.log(' [3/4] Fetching your businesses...');
69
69
  let businesses = [];
70
70
  try {
71
- const result = await apiRequestJson('/businesses/', {
71
+ const result = await apiRequestJson('/business/', {
72
72
  method: 'GET',
73
73
  token: creds.token,
74
74
  });