atris 3.2.0 → 3.11.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 (55) hide show
  1. package/GETTING_STARTED.md +65 -131
  2. package/README.md +18 -2
  3. package/atris/GETTING_STARTED.md +65 -131
  4. package/atris/PERSONA.md +5 -1
  5. package/atris/atris.md +122 -153
  6. package/atris/skills/aeo/SKILL.md +117 -0
  7. package/atris/skills/atris/SKILL.md +49 -25
  8. package/atris/skills/create-member/SKILL.md +29 -9
  9. package/atris/skills/endgame/SKILL.md +9 -0
  10. package/atris/skills/research-search/SKILL.md +167 -0
  11. package/atris/skills/research-search/arxiv_search.py +157 -0
  12. package/atris/skills/research-search/program.md +48 -0
  13. package/atris/skills/research-search/results.tsv +6 -0
  14. package/atris/skills/research-search/scholar_search.py +154 -0
  15. package/atris/skills/tidy/SKILL.md +36 -21
  16. package/atris/team/_template/MEMBER.md +2 -0
  17. package/atris/team/validator/MEMBER.md +35 -1
  18. package/atris.md +118 -178
  19. package/bin/atris.js +46 -12
  20. package/cli/__pycache__/atris_code.cpython-314.pyc +0 -0
  21. package/cli/__pycache__/runtime_guard.cpython-312.pyc +0 -0
  22. package/cli/__pycache__/runtime_guard.cpython-314.pyc +0 -0
  23. package/cli/atris_code.py +889 -0
  24. package/cli/runtime_guard.py +693 -0
  25. package/commands/align.js +16 -0
  26. package/commands/app.js +316 -0
  27. package/commands/autopilot.js +863 -23
  28. package/commands/brainstorm.js +7 -5
  29. package/commands/business.js +677 -2
  30. package/commands/clean.js +19 -3
  31. package/commands/computer.js +2022 -43
  32. package/commands/context-sync.js +5 -0
  33. package/commands/integrations.js +14 -9
  34. package/commands/lifecycle.js +12 -0
  35. package/commands/plugin.js +24 -0
  36. package/commands/pull.js +86 -11
  37. package/commands/push.js +153 -9
  38. package/commands/serve.js +1 -0
  39. package/commands/sync.js +272 -76
  40. package/commands/verify.js +50 -1
  41. package/commands/wiki.js +27 -2
  42. package/commands/workflow.js +24 -9
  43. package/lib/file-ops.js +13 -1
  44. package/lib/journal.js +23 -0
  45. package/lib/manifest.js +3 -0
  46. package/lib/scorecard.js +42 -4
  47. package/lib/sync-telemetry.js +59 -0
  48. package/lib/todo.js +6 -0
  49. package/lib/wiki.js +150 -6
  50. package/lib/workspace-safety.js +87 -0
  51. package/package.json +2 -1
  52. package/utils/api.js +19 -0
  53. package/utils/auth.js +25 -1
  54. package/utils/config.js +24 -0
  55. package/utils/update-check.js +16 -0
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Atris Computer — interact with your EC2 AI Computer
3
3
  *
4
- * atris computer — Show status
4
+ * atris computer — Open SMART mode (cloud in business workspace, local elsewhere)
5
+ * atris computer --cloud — Open CLOUD workspace mode
5
6
  * atris computer wake — Start the computer
6
7
  * atris computer sleep — Stop (files persist)
7
8
  * atris computer run <command> — Run bash on EC2 (no LLM)
@@ -11,8 +12,16 @@
11
12
  * atris computer exec <prompt> — Run with LLM (Claude Code)
12
13
  */
13
14
 
14
- const { loadCredentials } = require('../utils/auth');
15
- const { apiRequestJson } = require('../utils/api');
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const path = require('path');
18
+ const readline = require('readline');
19
+ const { spawnSync } = require('child_process');
20
+ const { loadCredentials, decodeJwtClaims } = require('../utils/auth');
21
+ const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
22
+ const { loadBusinesses, saveBusinesses } = require('./business');
23
+ const { consoleCommand, gatherAtrisContext, buildSystemPrompt } = require('./console');
24
+ const { streamSession } = require('./serve');
16
25
 
17
26
  function getToken() {
18
27
  const creds = loadCredentials();
@@ -23,7 +32,907 @@ function getToken() {
23
32
  return creds.token;
24
33
  }
25
34
 
26
- async function computerStatus(token) {
35
+ function sleep(ms) {
36
+ return new Promise((resolve) => setTimeout(resolve, ms));
37
+ }
38
+
39
+ const VALID_CLOUD_WORKERS = new Set(['claude', 'openai']);
40
+ const LOCAL_BRIDGE_RECONNECT_MS = 2000;
41
+ const KNOWN_CHAT_COMMANDS = new Set([
42
+ '/audit',
43
+ '/exit',
44
+ '/files',
45
+ '/help',
46
+ '/login',
47
+ '/model',
48
+ '/pwd',
49
+ '/quit',
50
+ '/reset',
51
+ '/run',
52
+ '/start',
53
+ '/status',
54
+ '/worker',
55
+ ]);
56
+
57
+ function color(code, value) {
58
+ if (process.env.NO_COLOR || !process.stdout.isTTY) return String(value);
59
+ return `\x1b[${code}m${value}\x1b[0m`;
60
+ }
61
+
62
+ const ui = {
63
+ bold: (value) => color(1, value),
64
+ dim: (value) => color(2, value),
65
+ green: (value) => color(32, value),
66
+ yellow: (value) => color(33, value),
67
+ cyan: (value) => color(36, value),
68
+ red: (value) => color(31, value),
69
+ };
70
+
71
+ function useInteractiveCloudUi() {
72
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.ATRIS_NO_INTERACTIVE);
73
+ }
74
+
75
+ function useInteractiveTerminalUi() {
76
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.ATRIS_NO_INTERACTIVE);
77
+ }
78
+
79
+ async function readPipedStdin() {
80
+ if (process.stdin.isTTY) return null;
81
+ let input = '';
82
+ for await (const chunk of process.stdin) {
83
+ input += chunk.toString();
84
+ }
85
+ return input;
86
+ }
87
+
88
+ function printCloudWordmark() {
89
+ if (!process.stdout.isTTY) return;
90
+ console.log(ui.cyan(' ___ __________ ________ CLOUD'));
91
+ console.log(ui.cyan(' / _ |/_ __/ __ \\/ _/ __/'));
92
+ console.log(ui.cyan(' / __ | / / / /_/ // /_\\ \\ '));
93
+ console.log(ui.cyan(' /_/ |_|/_/ \\____/___/___/ '));
94
+ }
95
+
96
+ function printLocalWordmark() {
97
+ if (!process.stdout.isTTY) return;
98
+ console.log(ui.green(' ___ __________ ________ LOCAL'));
99
+ console.log(ui.green(' / _ |/_ __/ __ \\/ _/ __/'));
100
+ console.log(ui.green(' / __ | / / / /_/ // /_\\ \\ '));
101
+ console.log(ui.green(' /_/ |_|/_/ \\____/___/___/ '));
102
+ }
103
+
104
+ function activeWorker(worker) {
105
+ return (worker || 'claude').toLowerCase() === 'default' ? 'claude' : (worker || 'claude').toLowerCase();
106
+ }
107
+
108
+ function formatWorkerName(worker) {
109
+ const active = activeWorker(worker);
110
+ return active === 'openai' ? 'OpenAI' : 'Claude';
111
+ }
112
+
113
+ function formatBillingMode(worker) {
114
+ return activeWorker(worker) === 'openai'
115
+ ? 'Atris credits'
116
+ : 'Claude subscription lane';
117
+ }
118
+
119
+ async function describeClaudeAuth(token, ctx) {
120
+ try {
121
+ const status = await fetchBusinessClaudeLoginStatus(token, ctx);
122
+ if (!status.ok) {
123
+ return {
124
+ connected: false,
125
+ label: 'Claude login: unknown',
126
+ detail: 'run /login to connect the remote computer',
127
+ };
128
+ }
129
+ const data = status.data || {};
130
+ if (data.loggedIn || data.connected || data.status === 'completed' || data.next_action === 'connected') {
131
+ return {
132
+ connected: true,
133
+ label: 'Claude login: connected',
134
+ detail: 'Claude subscription lane is active',
135
+ };
136
+ }
137
+ return {
138
+ connected: false,
139
+ label: 'Claude login: not connected',
140
+ detail: 'run /login to turn on the 0-credit Claude lane',
141
+ };
142
+ } catch {
143
+ return {
144
+ connected: false,
145
+ label: 'Claude login: unknown',
146
+ detail: 'run /login to connect the remote computer',
147
+ };
148
+ }
149
+ }
150
+
151
+ async function describeBillingMode(token, ctx, worker) {
152
+ if (activeWorker(worker) === 'openai') {
153
+ return 'Atris credits';
154
+ }
155
+ const auth = await describeClaudeAuth(token, ctx);
156
+ if (auth.connected) {
157
+ return 'Claude subscription connected - 0 Atris credits';
158
+ }
159
+ return 'Claude via Atris credits - /login makes it 0 credits';
160
+ }
161
+
162
+ function printCloudHelp() {
163
+ console.log('');
164
+ console.log(ui.bold('Useful commands'));
165
+ console.log(' /start Show the beginner flow again');
166
+ console.log(' /help Show this menu');
167
+ console.log(' /status Show cloud computer status');
168
+ console.log(' /files [path] List files in the workspace');
169
+ console.log(' /run <cmd> Run shell without the model');
170
+ console.log(' /audit [n] Show recent runs, output, and charges');
171
+ console.log(' /worker claude Use Claude subscription lane');
172
+ console.log(' /worker openai Use OpenAI credit lane');
173
+ console.log(' /login Connect Claude subscription on the remote box');
174
+ console.log(' /reset Start a fresh chat session');
175
+ console.log(' /exit Leave cloud mode');
176
+ console.log('');
177
+ console.log(ui.dim('No code needed: type the outcome in normal English. Unknown /commands are blocked locally.'));
178
+ }
179
+
180
+ function printCloudStartPanel(ctx, worker, model, billingLabel, authSummary = null) {
181
+ console.log('');
182
+ console.log(ui.bold('Atris Cloud Computer'));
183
+ console.log(`${ctx.businessName} ${ui.dim('/workspace persists')}`);
184
+ console.log(`Lane: ${ui.bold(formatWorkerName(worker))} ${ui.dim(formatCloudSelection({ worker, model }))}`);
185
+ console.log(`Billing: ${billingLabel}`);
186
+ if (authSummary) console.log(`${authSummary.label} ${ui.dim(authSummary.detail)}`);
187
+ console.log(`${ui.green('Atris loaded')} ${ui.dim('plain English -> workspace actions')}`);
188
+ console.log('');
189
+ console.log(ui.bold('Start here'));
190
+ console.log(' Type what you want built. Atris can inspect, edit, run, and save files.');
191
+ console.log(' "look around this workspace and tell me what is here"');
192
+ console.log(' "build me a one-page website for my coffee shop"');
193
+ console.log(' "make a script that turns a CSV into a chart"');
194
+ console.log('');
195
+ console.log(ui.bold('Controls'));
196
+ console.log(' /start this screen /status lane, auth, billing');
197
+ console.log(' /files workspace files /run pwd shell without the model');
198
+ console.log(' /login connect Claude /worker openai use credits');
199
+ console.log(' /audit 5 recent runs /exit leave cloud mode');
200
+ console.log('');
201
+ console.log(ui.dim('Plain English goes to Atris. Slash commands control the computer.'));
202
+ }
203
+
204
+ function buildLocalBridgeSystemPrompt(sessionId, localRoot, allowBash) {
205
+ const endpoint = `/api/cli/sessions/${sessionId}/file-op`;
206
+ const bashLine = allowBash
207
+ ? '- Run local commands with local_file_op({ "type": "bash", "command": "..." }).'
208
+ : '- Bash is disabled for this local session. Use read/write/edit/delete only.';
209
+
210
+ return `
211
+
212
+ ## Atris Local Folder Mode
213
+
214
+ The user connected their LOCAL folder to Atris through CLI session ${sessionId}.
215
+ Their local root is: ${localRoot}
216
+ Treat this local folder as the primary workspace for this chat.
217
+ The cloud /workspace is only a control plane.
218
+ Do not use Write/Edit/apply_patch for requested local edits.
219
+ Use the native local_file_op tool for every local filesystem change.
220
+
221
+ Preferred tool calls:
222
+ - local_file_op({ "type": "read", "path": "relative/path.txt" })
223
+ - local_file_op({ "type": "write", "path": "file.txt", "content": "..." })
224
+ - local_file_op({ "type": "edit", "path": "file.txt", "find": "...", "replace": "..." })
225
+ - local_file_op({ "type": "delete", "path": "file.txt" })
226
+ ${bashLine}
227
+
228
+ Fallback if the native tool is unavailable: use Bash to call the Atris Python API from the cloud workspace:
229
+
230
+ \`\`\`python
231
+ from atris_api import api
232
+ api("POST", "${endpoint}", {
233
+ "type": "read",
234
+ "path": "relative/path.txt",
235
+ "wait_for_ack": True,
236
+ "timeout_seconds": 30,
237
+ })
238
+ \`\`\`
239
+
240
+ Supported operations:
241
+ - Read: { "type": "read", "path": "file.txt", "wait_for_ack": true }
242
+ - Write: { "type": "write", "path": "file.txt", "content": "...", "wait_for_ack": true }
243
+ - Edit: { "type": "edit", "path": "file.txt", "find": "...", "replace": "...", "wait_for_ack": true }
244
+ - Delete: { "type": "delete", "path": "file.txt", "wait_for_ack": true }
245
+
246
+ Rules:
247
+ - All paths must be relative to the local root.
248
+ - Read before editing unless you are creating a new file.
249
+ - Use local bash for ls/rg/tests when available.
250
+ - Do not ask the user to copy, paste, or save files. Apply the change through the bridge.
251
+ - In final answers, say what changed locally and how you verified it.
252
+ ---`;
253
+ }
254
+
255
+ function printLocalAtrisStartPanel(ctx, bridge, worker, model, billingLabel, authSummary = null) {
256
+ console.log('');
257
+ console.log(ui.bold('Atris Local Computer'));
258
+ console.log(`${ctx.businessName} ${ui.dim('cloud brain -> local folder')}`);
259
+ console.log(`Local: ${bridge.workingDir}`);
260
+ console.log(`Bridge: ${bridge.sessionId.slice(0, 8)} ${ui.dim(bridge.allowBash ? 'local bash enabled' : 'file ops only')}`);
261
+ console.log(`Lane: ${ui.bold(formatWorkerName(worker))} ${ui.dim(formatCloudSelection({ worker, model }))}`);
262
+ console.log(`Billing: ${billingLabel}`);
263
+ if (authSummary) console.log(`${authSummary.label} ${ui.dim(authSummary.detail)}`);
264
+ console.log(`${ui.green('Atris loaded')} ${ui.dim('plain English -> local edits')}`);
265
+ console.log('');
266
+ console.log(ui.bold('Start here'));
267
+ console.log(' "look around this folder and tell me what is here"');
268
+ console.log(' "make the homepage look premium"');
269
+ console.log(' "add a script that converts a CSV into a chart"');
270
+ console.log('');
271
+ console.log(ui.bold('Controls'));
272
+ console.log(' /status local bridge, lane, billing');
273
+ console.log(' /files local files');
274
+ console.log(' /run local shell command');
275
+ console.log(' /audit recent cloud brain runs');
276
+ console.log(' /worker claude|openai');
277
+ console.log(' /exit leave local Atris mode');
278
+ console.log('');
279
+ console.log(ui.dim('Tokens run through Atris/cloud billing. Edits land in this local folder.'));
280
+ }
281
+
282
+ async function printCloudSessionStatus(token, ctx, worker, model) {
283
+ const statusResult = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/status`, {
284
+ method: 'GET',
285
+ token,
286
+ });
287
+ const d = statusResult.ok ? (statusResult.data || {}) : {};
288
+ const computerState = d.status || (statusResult.ok ? 'unknown' : `error ${statusResult.status}`);
289
+ const authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
290
+ const billingLabel = await describeBillingMode(token, ctx, worker);
291
+
292
+ console.log('');
293
+ console.log(ui.bold('Cloud status'));
294
+ console.log(` Computer: ${computerState}`);
295
+ console.log(` Business: ${ctx.businessName}`);
296
+ console.log(' Workspace: /workspace');
297
+ console.log(` Lane: ${formatWorkerName(worker)} ${formatCloudSelection({ worker, model })}`);
298
+ console.log(` Billing: ${billingLabel}`);
299
+ console.log(' Atris: loaded');
300
+ if (authSummary) console.log(` Claude: ${authSummary.connected ? 'connected' : 'not connected'} ${authSummary.detail}`);
301
+ if (d.endpoint) console.log(` Endpoint: ${d.endpoint}`);
302
+ }
303
+
304
+ function formatDropdownLine(choice, selected) {
305
+ const pointer = selected ? '>' : ' ';
306
+ const label = selected ? ui.bold(choice.label) : choice.label;
307
+ return `${pointer} ${label} ${ui.dim(choice.detail || '')}`.trimEnd();
308
+ }
309
+
310
+ function questionAsync(rl, question) {
311
+ return new Promise((resolve) => rl.question(question, resolve));
312
+ }
313
+
314
+ async function selectFromDropdown(title, choices) {
315
+ if (!useInteractiveTerminalUi() || !choices.length) return choices[0] || null;
316
+
317
+ console.log(ui.bold(title));
318
+ choices.forEach((choice, i) => {
319
+ console.log(`${i + 1}. ${choice.label} ${ui.dim(choice.detail || '')}`.trimEnd());
320
+ });
321
+
322
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
323
+ const answer = String(await questionAsync(rl, `Choose [1-${choices.length}] (default 1): `) || '').trim();
324
+ rl.close();
325
+
326
+ if (!answer) return choices[0];
327
+ if (answer.toLowerCase() === 'q' || answer.toLowerCase() === 'quit' || answer.toLowerCase() === 'exit') {
328
+ return null;
329
+ }
330
+ const selected = Number.parseInt(answer, 10);
331
+ if (Number.isFinite(selected) && selected >= 1 && selected <= choices.length) {
332
+ return choices[selected - 1];
333
+ }
334
+ return choices[0];
335
+ }
336
+
337
+ function describeLocalClaudeAuth() {
338
+ if (process.env.ANTHROPIC_API_KEY) {
339
+ return 'Local auth: ANTHROPIC_API_KEY set on this Mac';
340
+ }
341
+ const hasClaude = spawnSync('which', ['claude'], { encoding: 'utf8', timeout: 1000 }).status === 0;
342
+ if (!hasClaude) {
343
+ return 'Local auth: Claude CLI not found; use Cloud workspace or install Claude Code';
344
+ }
345
+ const status = spawnSync('claude', ['auth', 'status', '--json'], {
346
+ encoding: 'utf8',
347
+ timeout: 1500,
348
+ stdio: 'pipe',
349
+ });
350
+ if (status.error && status.error.code === 'ETIMEDOUT') {
351
+ return 'Local auth: Claude CLI installed, auth check timed out; Cloud subscription does not carry over';
352
+ }
353
+ const raw = String(status.stdout || status.stderr || '').trim();
354
+ try {
355
+ const parsed = JSON.parse(raw);
356
+ if (parsed.loggedIn || parsed.status === 'logged_in' || parsed.authMethod) {
357
+ const plan = parsed.subscriptionType || parsed.plan || parsed.authMethod || 'connected';
358
+ return `Local auth: Claude logged in on this Mac (${plan})`;
359
+ }
360
+ } catch {
361
+ // Fall through to text checks.
362
+ }
363
+ if (/logged\s*in|subscription|max|pro/i.test(raw)) {
364
+ return 'Local auth: Claude appears logged in on this Mac';
365
+ }
366
+ return 'Local auth: not confirmed; run `claude login` on this Mac or choose Cloud workspace';
367
+ }
368
+
369
+ async function chooseComputerSurface(hasBusinessBinding, hasLocalHarness) {
370
+ if (!useInteractiveTerminalUi()) {
371
+ return hasBusinessBinding ? 'cloud' : 'local';
372
+ }
373
+ if (hasBusinessBinding) {
374
+ const choices = [
375
+ { label: 'Cloud workspace', value: 'cloud', detail: '/workspace, shared, Atris loaded' },
376
+ { label: 'Local folder', value: 'local-atris', detail: 'edits this folder, tokens run through Atris' },
377
+ ];
378
+ if (hasLocalHarness) {
379
+ choices.push({ label: 'Local BYO Claude', value: 'local-byo', detail: 'advanced, tokens go to Anthropic' });
380
+ }
381
+ const selected = await selectFromDropdown('Choose computer', choices);
382
+ if (selected === null) return null;
383
+ return selected?.value || 'cloud';
384
+ }
385
+ return 'local';
386
+ }
387
+
388
+ async function chooseCloudLane(token, ctx, initialOptions = {}) {
389
+ let worker = initialOptions.worker || null;
390
+ let model = initialOptions.model || null;
391
+
392
+ if (!worker && useInteractiveCloudUi()) {
393
+ const selected = await selectFromDropdown('Choose compute lane', [
394
+ { label: 'Claude', value: 'claude', detail: 'subscription lane when connected, 0 Atris credits' },
395
+ { label: 'OpenAI', value: 'openai', detail: 'works now, uses Atris credits' },
396
+ ]);
397
+ if (selected === null) return { cancelled: true };
398
+ if (selected?.value) worker = selected.value;
399
+ }
400
+
401
+ if (activeWorker(worker) === 'claude' && useInteractiveCloudUi()) {
402
+ let state = null;
403
+ try {
404
+ const status = await fetchBusinessClaudeLoginStatus(token, ctx);
405
+ state = status.ok ? status.data : null;
406
+ } catch {
407
+ state = null;
408
+ }
409
+
410
+ if (!state?.connected && !state?.loggedIn && state?.status !== 'completed' && state?.next_action !== 'connected') {
411
+ const selected = await selectFromDropdown('Claude subscription auth', [
412
+ { label: 'Use Atris Claude', value: 'continue', detail: 'works now, uses Atris credits' },
413
+ { label: 'Login to Claude', value: 'login', detail: 'turns on 0-credit Claude lane' },
414
+ { label: 'Use OpenAI', value: 'openai', detail: 'works now, uses Atris credits' },
415
+ ]);
416
+ if (selected === null) return { cancelled: true };
417
+ if (selected?.value === 'login') {
418
+ await computerCloudLogin(token, ctx);
419
+ } else if (selected?.value === 'openai') {
420
+ worker = 'openai';
421
+ }
422
+ }
423
+ }
424
+
425
+ return { worker, model };
426
+ }
427
+
428
+ function parseComputerOptions(argv) {
429
+ const positional = [];
430
+ let worker = process.env.ATRIS_CLOUD_WORKER || null;
431
+ let model = process.env.ATRIS_CLOUD_MODEL || null;
432
+
433
+ for (let i = 0; i < argv.length; i++) {
434
+ const arg = argv[i];
435
+ if (arg === '--worker' && argv[i + 1]) {
436
+ worker = argv[i + 1];
437
+ i++;
438
+ continue;
439
+ }
440
+ if (arg.startsWith('--worker=')) {
441
+ worker = arg.split('=', 2)[1] || null;
442
+ continue;
443
+ }
444
+ if (arg === '--model' && argv[i + 1]) {
445
+ model = argv[i + 1];
446
+ i++;
447
+ continue;
448
+ }
449
+ if (arg.startsWith('--model=')) {
450
+ model = arg.split('=', 2)[1] || null;
451
+ continue;
452
+ }
453
+ positional.push(arg);
454
+ }
455
+
456
+ if (worker && !VALID_CLOUD_WORKERS.has(worker)) {
457
+ console.error(`Invalid cloud worker: ${worker}`);
458
+ console.error('Expected one of: claude, openai');
459
+ process.exit(1);
460
+ }
461
+
462
+ return {
463
+ positional,
464
+ options: {
465
+ worker: worker || null,
466
+ model: model || null,
467
+ },
468
+ };
469
+ }
470
+
471
+ function formatCloudSelection(options = {}) {
472
+ const worker = activeWorker(options.worker);
473
+ const parts = [`worker=${worker}`];
474
+ if (options.model) parts.push(`model=${options.model}`);
475
+ if (!options.model) parts.push('model=default');
476
+ return parts.join(' ');
477
+ }
478
+
479
+ function printModeBanner(mode, root, lines = []) {
480
+ console.log(`Mode: ${mode}`);
481
+ console.log(`Root: ${root}`);
482
+ for (const line of lines) console.log(line);
483
+ console.log('');
484
+ }
485
+
486
+ function findAtrisCodeTerminal() {
487
+ const envPath = process.env.ATRIS_CODE_PY;
488
+ const candidates = [
489
+ envPath,
490
+ path.join(__dirname, '..', 'cli', 'atris_code.py'),
491
+ path.join(process.cwd(), 'cli', 'atris_code.py'),
492
+ path.join(os.homedir(), 'arena', 'atrisos-backend', 'cli', 'atris_code.py'),
493
+ ].filter(Boolean);
494
+
495
+ let dir = process.cwd();
496
+ for (let i = 0; i < 4; i++) {
497
+ candidates.push(path.join(dir, 'cli', 'atris_code.py'));
498
+ dir = path.dirname(dir);
499
+ }
500
+
501
+ for (const p of candidates) {
502
+ if (fs.existsSync(p)) return p;
503
+ }
504
+ return null;
505
+ }
506
+
507
+ function findAtrisCodePython(terminalPath) {
508
+ const envPython = process.env.ATRIS_CODE_PYTHON;
509
+ if (envPython && fs.existsSync(envPython)) return envPython;
510
+ if (!terminalPath) return 'python3';
511
+
512
+ const projectRoot = path.dirname(path.dirname(terminalPath));
513
+ const candidates = [
514
+ path.join(projectRoot, 'venv', 'bin', 'python3'),
515
+ path.join(projectRoot, '.venv', 'bin', 'python3'),
516
+ ];
517
+ for (const p of candidates) {
518
+ if (fs.existsSync(p)) return p;
519
+ }
520
+ return 'python3';
521
+ }
522
+
523
+ function computerLocalLegacy(extraArgs = []) {
524
+ printModeBanner('LOCAL', process.cwd(), [
525
+ 'Current folder is the workspace.',
526
+ 'Legacy console mode.',
527
+ ]);
528
+
529
+ const originalArgv = process.argv;
530
+ process.argv = [originalArgv[0], originalArgv[1], originalArgv[2], ...extraArgs];
531
+ try {
532
+ consoleCommand();
533
+ } finally {
534
+ process.argv = originalArgv;
535
+ }
536
+ }
537
+
538
+ function computerLocal(extraArgs = []) {
539
+ printLocalWordmark();
540
+ printModeBanner('LOCAL', process.cwd(), [
541
+ 'Claude Code + Atris workspace context.',
542
+ 'BYO local Claude: tokens go through Anthropic, not Atris.',
543
+ 'No Atris credits, no cloud audit, no remote workspace.',
544
+ 'Remote /login only applies to Cloud workspace.',
545
+ ]);
546
+
547
+ const originalArgv = process.argv;
548
+ process.argv = [originalArgv[0], originalArgv[1], originalArgv[2], 'claude', ...extraArgs];
549
+ try {
550
+ consoleCommand();
551
+ } finally {
552
+ process.argv = originalArgv;
553
+ }
554
+ }
555
+
556
+ async function startLocalAtrisBridge(token, options = {}) {
557
+ const workingDir = process.cwd();
558
+ const allowBash = options.allowBash !== false;
559
+ const result = await apiRequestJson('/cli/sessions', {
560
+ method: 'POST',
561
+ token,
562
+ body: {
563
+ working_directory: workingDir,
564
+ agent_id: null,
565
+ allow_bash: allowBash,
566
+ },
567
+ timeoutMs: 15000,
568
+ });
569
+
570
+ if (!result.ok) {
571
+ throw new Error(result.errorMessage || result.error || `failed to create local bridge (${result.status})`);
572
+ }
573
+
574
+ const session = result.data || {};
575
+ const sessionId = session.session_id;
576
+ if (!sessionId) {
577
+ throw new Error('local bridge did not return a session id');
578
+ }
579
+
580
+ let stopped = false;
581
+ const loop = async () => {
582
+ while (!stopped) {
583
+ try {
584
+ await streamSession(token, sessionId, workingDir);
585
+ } catch (err) {
586
+ if (!stopped) console.error(ui.dim(` local bridge reconnecting: ${err.message}`));
587
+ }
588
+ if (!stopped) await sleep(LOCAL_BRIDGE_RECONNECT_MS);
589
+ }
590
+ };
591
+ loop();
592
+
593
+ return {
594
+ sessionId,
595
+ workingDir,
596
+ allowBash,
597
+ stop: async () => {
598
+ stopped = true;
599
+ await apiRequestJson(`/cli/sessions/${sessionId}`, {
600
+ method: 'DELETE',
601
+ token,
602
+ timeoutMs: 10000,
603
+ }).catch(() => {});
604
+ },
605
+ };
606
+ }
607
+
608
+ async function runLocalBridgeOp(token, sessionId, op, timeoutSeconds = 30) {
609
+ const result = await apiRequestJson(`/cli/sessions/${sessionId}/file-op`, {
610
+ method: 'POST',
611
+ token,
612
+ body: {
613
+ ...op,
614
+ wait_for_ack: true,
615
+ timeout_seconds: timeoutSeconds,
616
+ },
617
+ timeoutMs: Math.max(10, timeoutSeconds + 5) * 1000,
618
+ });
619
+
620
+ if (!result.ok) {
621
+ console.error(`Failed: ${result.errorMessage || result.error || result.status}`);
622
+ return null;
623
+ }
624
+
625
+ const data = result.data || {};
626
+ if (data.status === 'error') {
627
+ const err = data.result?.error || 'local operation failed';
628
+ console.error(`Failed: ${err}`);
629
+ }
630
+ return data;
631
+ }
632
+
633
+ function readBusinessBinding() {
634
+ const bindingPath = path.join(process.cwd(), '.atris', 'business.json');
635
+ if (!fs.existsSync(bindingPath)) return null;
636
+ try {
637
+ return JSON.parse(fs.readFileSync(bindingPath, 'utf8'));
638
+ } catch {
639
+ return null;
640
+ }
641
+ }
642
+
643
+ async function resolveBusinessContext(token) {
644
+ const binding = readBusinessBinding();
645
+ if (!binding) return null;
646
+
647
+ if (binding.business_id && binding.workspace_id) {
648
+ return {
649
+ slug: binding.slug,
650
+ businessId: binding.business_id,
651
+ workspaceId: binding.workspace_id,
652
+ businessName: binding.name || binding.slug || 'business',
653
+ };
654
+ }
655
+
656
+ const slug = binding.slug;
657
+ if (!slug) return null;
658
+
659
+ const businesses = loadBusinesses();
660
+ const list = await apiRequestJson('/business/', { method: 'GET', token });
661
+ if (list.ok) {
662
+ const match = (list.data || []).find(
663
+ (b) => b.slug === slug || (b.name || '').toLowerCase() === slug.toLowerCase()
664
+ );
665
+ if (match) {
666
+ businesses[slug] = {
667
+ business_id: match.id,
668
+ workspace_id: match.workspace_id,
669
+ name: match.name,
670
+ slug: match.slug,
671
+ added_at: new Date().toISOString(),
672
+ };
673
+ saveBusinesses(businesses);
674
+ return {
675
+ slug: match.slug,
676
+ businessId: match.id,
677
+ workspaceId: match.workspace_id,
678
+ businessName: match.name || match.slug,
679
+ };
680
+ }
681
+ }
682
+
683
+ const cached = businesses[slug];
684
+ if (cached && cached.business_id && cached.workspace_id) {
685
+ return {
686
+ slug,
687
+ businessId: cached.business_id,
688
+ workspaceId: cached.workspace_id,
689
+ businessName: cached.name || slug,
690
+ };
691
+ }
692
+
693
+ return null;
694
+ }
695
+
696
+ async function resolveBusinessContextBySlug(token, slug) {
697
+ if (!slug) return null;
698
+
699
+ const businesses = loadBusinesses();
700
+ const list = await apiRequestJson('/business/', { method: 'GET', token });
701
+ if (list.ok) {
702
+ const match = (list.data || []).find(
703
+ (b) => b.slug === slug || (b.name || '').toLowerCase() === slug.toLowerCase()
704
+ );
705
+ if (match) {
706
+ businesses[match.slug || slug] = {
707
+ business_id: match.id,
708
+ workspace_id: match.workspace_id,
709
+ name: match.name,
710
+ slug: match.slug,
711
+ added_at: new Date().toISOString(),
712
+ };
713
+ saveBusinesses(businesses);
714
+ return {
715
+ slug: match.slug,
716
+ businessId: match.id,
717
+ workspaceId: match.workspace_id,
718
+ businessName: match.name || match.slug,
719
+ };
720
+ }
721
+ }
722
+
723
+ return null;
724
+ }
725
+
726
+ function shellQuote(value) {
727
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
728
+ }
729
+
730
+ function businessPromptUserId(token) {
731
+ const claims = decodeJwtClaims(token) || {};
732
+ return claims.sub || claims.user_id || claims.uid || null;
733
+ }
734
+
735
+ async function runBusinessTerminalCommand(token, ctx, command, timeout = 30) {
736
+ return apiRequestJson(
737
+ `/business/${ctx.businessId}/workspaces/${ctx.workspaceId}/terminal`,
738
+ {
739
+ method: 'POST',
740
+ token,
741
+ body: { command, timeout },
742
+ timeoutMs: Math.max(timeout + 10, 40) * 1000,
743
+ }
744
+ );
745
+ }
746
+
747
+ async function readBusinessWorkspaceFile(token, ctx, remotePath, timeoutMs = 15000) {
748
+ return apiRequestJson(
749
+ `/business/${ctx.businessId}/workspaces/${ctx.workspaceId}/file?path=${encodeURIComponent(remotePath)}`,
750
+ {
751
+ method: 'GET',
752
+ token,
753
+ timeoutMs,
754
+ }
755
+ );
756
+ }
757
+
758
+ function extractRunnerProxyText(payload = {}) {
759
+ const result = String(payload.result || '').trim();
760
+ if (result) return result;
761
+ if (Array.isArray(payload.assistant_text)) {
762
+ const joined = payload.assistant_text.join('').trim();
763
+ if (joined) return joined;
764
+ }
765
+ if (typeof payload.text === 'string' && payload.text.trim()) {
766
+ return payload.text.trim();
767
+ }
768
+ return '';
769
+ }
770
+
771
+ async function runBusinessPromptViaRunnerProxy(token, ctx, prompt, options = {}) {
772
+ const requestId = `cli-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
773
+ const remoteDir = '/workspace/.atris-runner-proxy';
774
+ const outputPath = `${remoteDir}/${requestId}.json`;
775
+ const scriptPath = `/tmp/atris_runner_proxy_${requestId}.py`;
776
+ const stdoutPath = `/tmp/atris_runner_proxy_${requestId}.stdout`;
777
+ const stderrPath = `/tmp/atris_runner_proxy_${requestId}.stderr`;
778
+ const payload = {
779
+ prompt,
780
+ permission_mode: 'bypassPermissions',
781
+ max_turns: Math.min(Math.max(Number(options.maxTurns || 12), 1), 25),
782
+ reset_context: Boolean(options.resetContext),
783
+ };
784
+ if (options.worker) payload.worker = options.worker;
785
+ if (options.model) payload.model = options.model;
786
+ if (options.systemPrompt) payload.system_prompt = options.systemPrompt;
787
+ if (options.allowedTools) payload.allowed_tools = options.allowedTools;
788
+ if (options.localCliSessionId) payload.local_cli_session_id = options.localCliSessionId;
789
+
790
+ const userId = businessPromptUserId(token);
791
+ if (userId) payload.user_id = userId;
792
+
793
+ const payloadB64 = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
794
+ const remoteScript = [
795
+ 'import base64, json, pathlib, time, urllib.request',
796
+ `PAYLOAD = json.loads(base64.b64decode(${JSON.stringify(payloadB64)}).decode("utf-8"))`,
797
+ `OUTPUT_PATH = pathlib.Path(${JSON.stringify(outputPath)})`,
798
+ 'TOKEN = ""',
799
+ 'with open("/opt/atris/config/env", "r", encoding="utf-8") as fh:',
800
+ ' for line in fh:',
801
+ ' if line.startswith("ATRIS_SERVICE_TOKEN="):',
802
+ ' TOKEN = line.split("=", 1)[1].strip()',
803
+ ' break',
804
+ 'if not TOKEN:',
805
+ ' OUTPUT_PATH.write_text(json.dumps({"status":"error","error":"missing ATRIS_SERVICE_TOKEN"}), encoding="utf-8")',
806
+ ' raise SystemExit(0)',
807
+ 'def _fetch(req, timeout=120):',
808
+ ' with urllib.request.urlopen(req, timeout=timeout) as resp:',
809
+ ' return json.loads(resp.read().decode("utf-8"))',
810
+ 'try:',
811
+ ' start_req = urllib.request.Request(',
812
+ ' "http://127.0.0.1:8081/execute-background",',
813
+ ' data=json.dumps(PAYLOAD).encode("utf-8"),',
814
+ ' headers={"Content-Type":"application/json","X-Atris-Service-Token":TOKEN},',
815
+ ' method="POST",',
816
+ ' )',
817
+ ' start = _fetch(start_req)',
818
+ ' execution_id = start.get("execution_id")',
819
+ ' result = {"execution_id": execution_id, "assistant_text": [], "result": "", "status": "running", "result_event": None}',
820
+ ' from_index = 0',
821
+ ' deadline = time.time() + 300',
822
+ ' while time.time() < deadline:',
823
+ ' poll_req = urllib.request.Request(',
824
+ ' f"http://127.0.0.1:8081/events?execution_id={execution_id}&from_index={from_index}",',
825
+ ' headers={"X-Atris-Service-Token":TOKEN},',
826
+ ' method="GET",',
827
+ ' )',
828
+ ' data = _fetch(poll_req, timeout=60)',
829
+ ' events = data.get("events") or []',
830
+ ' for event in events:',
831
+ ' typ = event.get("type")',
832
+ ' if typ in ("assistant_text", "text"):',
833
+ ' content = event.get("content") or ""',
834
+ ' if content:',
835
+ ' result["assistant_text"].append(content)',
836
+ ' elif typ == "result":',
837
+ ' result["result"] = event.get("result") or result["result"]',
838
+ ' result["result_event"] = event',
839
+ ' from_index = data.get("next_index", from_index + len(events))',
840
+ ' result["status"] = data.get("status") or result["status"]',
841
+ ' if result["status"] in ("completed", "failed", "error", "cancelled"):',
842
+ ' break',
843
+ ' time.sleep(2)',
844
+ ' OUTPUT_PATH.write_text(json.dumps(result), encoding="utf-8")',
845
+ 'except Exception as exc:',
846
+ ' OUTPUT_PATH.write_text(json.dumps({"execution_id": None, "assistant_text": [], "result": "", "status": "error", "error": str(exc)}), encoding="utf-8")',
847
+ ].join('\n');
848
+
849
+ const launcher = [
850
+ `mkdir -p ${shellQuote(remoteDir)}`,
851
+ `cat > ${shellQuote(scriptPath)} <<'PY'`,
852
+ remoteScript,
853
+ 'PY',
854
+ `nohup python3 ${shellQuote(scriptPath)} >${shellQuote(stdoutPath)} 2>${shellQuote(stderrPath)} < /dev/null &`,
855
+ 'echo launched',
856
+ ].join('\n');
857
+
858
+ const launchResult = await runBusinessTerminalCommand(token, ctx, launcher, 30);
859
+ if (!launchResult.ok) {
860
+ return {
861
+ ok: false,
862
+ error: launchResult.error || `launcher failed (${launchResult.status})`,
863
+ status: launchResult.status,
864
+ };
865
+ }
866
+
867
+ const deadline = Date.now() + 330000;
868
+ while (Date.now() < deadline) {
869
+ const fileResult = await readBusinessWorkspaceFile(token, ctx, outputPath, 15000);
870
+ if (!fileResult.ok) {
871
+ if (fileResult.status === 404) {
872
+ await sleep(2000);
873
+ continue;
874
+ }
875
+ return {
876
+ ok: false,
877
+ error: fileResult.error || `runner proxy read failed (${fileResult.status})`,
878
+ status: fileResult.status,
879
+ };
880
+ }
881
+ try {
882
+ const payload = JSON.parse(fileResult.data?.content || '{}');
883
+ const status = payload.status || 'unknown';
884
+ if (['completed', 'failed', 'error', 'cancelled', 'timeout'].includes(status)) {
885
+ return { ok: status === 'completed', payload, status };
886
+ }
887
+ } catch {
888
+ // Ignore partial file writes and keep polling.
889
+ }
890
+ await sleep(2000);
891
+ }
892
+
893
+ return { ok: false, error: 'runner proxy timed out', status: 0 };
894
+ }
895
+
896
+ async function ensureBusinessAwake(token, ctx, maxWaitSec = 90) {
897
+ const status = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/status`, { method: 'GET', token });
898
+ if (status.ok && status.data && status.data.status === 'running' && status.data.endpoint) {
899
+ return true;
900
+ }
901
+ process.stdout.write(' Waking business computer... ');
902
+ await apiRequestJson(`/business/${ctx.businessId}/ai-computer/wake`, { method: 'POST', token, body: {} });
903
+ const start = Date.now();
904
+ while (Date.now() - start < maxWaitSec * 1000) {
905
+ await sleep(3000);
906
+ const next = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/status`, { method: 'GET', token });
907
+ if (next.ok && next.data && next.data.status === 'running' && next.data.endpoint) {
908
+ const elapsed = Math.floor((Date.now() - start) / 1000);
909
+ console.log(`awake (${elapsed}s)`);
910
+ return true;
911
+ }
912
+ }
913
+ console.log('timeout');
914
+ return false;
915
+ }
916
+
917
+ async function computerStatus(token, ctx = null) {
918
+ if (ctx) {
919
+ const result = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/status`, {
920
+ method: 'GET',
921
+ token,
922
+ });
923
+ if (!result.ok) {
924
+ console.error(`Failed: ${result.errorMessage || result.status}`);
925
+ return;
926
+ }
927
+ const d = result.data || {};
928
+ const status = d.status || 'unknown';
929
+ const icon = status === 'running' ? '●' : '○';
930
+ console.log(` ${icon} Computer: ${status}`);
931
+ console.log(` Business: ${ctx.businessName}`);
932
+ if (d.endpoint) console.log(` Endpoint: ${d.endpoint}`);
933
+ return;
934
+ }
935
+
27
936
  const result = await apiRequestJson('/ai-computer/user/status', {
28
937
  method: 'GET',
29
938
  token,
@@ -54,7 +963,23 @@ async function computerStatus(token) {
54
963
  }
55
964
  }
56
965
 
57
- async function computerWake(token) {
966
+ async function computerWake(token, ctx = null) {
967
+ if (ctx) {
968
+ console.log(`Waking computer for ${ctx.businessName}...`);
969
+ const result = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/wake`, {
970
+ method: 'POST',
971
+ token,
972
+ body: {},
973
+ });
974
+ if (!result.ok) {
975
+ console.error(`Failed: ${result.errorMessage || result.status}`);
976
+ return;
977
+ }
978
+ console.log(` Status: ${result.data.status}`);
979
+ if (result.data.endpoint) console.log(` Endpoint: ${result.data.endpoint}`);
980
+ return;
981
+ }
982
+
58
983
  console.log('Waking computer...');
59
984
  const result = await apiRequestJson('/ai-computer/user/wake', {
60
985
  method: 'POST',
@@ -69,7 +994,22 @@ async function computerWake(token) {
69
994
  console.log(` Endpoint: ${result.data.endpoint}`);
70
995
  }
71
996
 
72
- async function computerSleep(token) {
997
+ async function computerSleep(token, ctx = null) {
998
+ if (ctx) {
999
+ console.log(`Sleeping computer for ${ctx.businessName}...`);
1000
+ const result = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/sleep`, {
1001
+ method: 'POST',
1002
+ token,
1003
+ body: {},
1004
+ });
1005
+ if (!result.ok) {
1006
+ console.error(`Failed: ${result.errorMessage || result.status}`);
1007
+ return;
1008
+ }
1009
+ console.log(' Computer is sleeping. Files persist.');
1010
+ return;
1011
+ }
1012
+
73
1013
  console.log('Sleeping computer...');
74
1014
  const result = await apiRequestJson('/ai-computer/user/sleep', {
75
1015
  method: 'POST',
@@ -83,11 +1023,44 @@ async function computerSleep(token) {
83
1023
  console.log(' Computer is sleeping. Files persist.');
84
1024
  }
85
1025
 
86
- async function computerRun(token, command) {
1026
+ async function computerRun(token, command, ctx = null) {
87
1027
  if (!command) {
88
1028
  console.error('Usage: atris computer run <command>');
89
1029
  process.exit(1);
90
1030
  }
1031
+
1032
+ if (ctx) {
1033
+ const awake = await ensureBusinessAwake(token, ctx);
1034
+ if (!awake) {
1035
+ console.error(' Computer did not become ready in time.');
1036
+ return;
1037
+ }
1038
+ const result = await apiRequestJson(
1039
+ `/business/${ctx.businessId}/workspaces/${ctx.workspaceId}/terminal`,
1040
+ {
1041
+ method: 'POST',
1042
+ token,
1043
+ body: { command, timeout: 30 },
1044
+ timeoutMs: 40000,
1045
+ }
1046
+ );
1047
+ if (!result.ok) {
1048
+ if (result.status === 409 || (result.errorMessage || '').includes('running')) {
1049
+ console.error('Computer is off. Run: atris computer wake');
1050
+ } else {
1051
+ console.error(`Failed: ${result.errorMessage || result.status}`);
1052
+ }
1053
+ return;
1054
+ }
1055
+ const d = result.data || {};
1056
+ if (d.stdout) process.stdout.write(d.stdout);
1057
+ if (d.stderr) process.stderr.write(d.stderr);
1058
+ if (d.exit_code && d.exit_code !== 0) {
1059
+ console.error(`Exit: ${d.exit_code}`);
1060
+ }
1061
+ return;
1062
+ }
1063
+
91
1064
  const result = await apiRequestJson('/ai-computer/terminal', {
92
1065
  method: 'POST',
93
1066
  token,
@@ -109,16 +1082,36 @@ async function computerRun(token, command) {
109
1082
  }
110
1083
  }
111
1084
 
112
- async function computerGrep(token, pattern) {
1085
+ async function computerGrep(token, pattern, ctx = null) {
113
1086
  if (!pattern) {
114
1087
  console.error('Usage: atris computer grep <pattern>');
115
1088
  process.exit(1);
116
1089
  }
117
- return computerRun(token, `grep -rni "${pattern}" . --include="*.md" --include="*.py" --include="*.js" --include="*.json" 2>/dev/null | head -30`);
1090
+ return computerRun(token, `grep -rni "${pattern}" . --include="*.md" --include="*.py" --include="*.js" --include="*.json" 2>/dev/null | head -30`, ctx);
118
1091
  }
119
1092
 
120
- async function computerLs(token, remotePath) {
1093
+ async function computerLs(token, remotePath, ctx = null) {
121
1094
  const path = remotePath || '/';
1095
+
1096
+ if (ctx) {
1097
+ const result = await apiRequestJson(
1098
+ `/business/${ctx.businessId}/workspaces/${ctx.workspaceId}/files?path=${encodeURIComponent(path)}`,
1099
+ {
1100
+ method: 'GET',
1101
+ token,
1102
+ }
1103
+ );
1104
+ if (!result.ok) {
1105
+ console.error(`Failed: ${result.errorMessage || result.status}`);
1106
+ return;
1107
+ }
1108
+ for (const f of (result.data.files || [])) {
1109
+ const type = f.type === 'dir' ? 'DIR ' : ' ';
1110
+ console.log(` ${type}${f.name} (${f.size || 0}b)`);
1111
+ }
1112
+ return;
1113
+ }
1114
+
122
1115
  const result = await apiRequestJson(`/ai-computer/files?path=${encodeURIComponent(path)}`, {
123
1116
  method: 'GET',
124
1117
  token,
@@ -133,11 +1126,28 @@ async function computerLs(token, remotePath) {
133
1126
  }
134
1127
  }
135
1128
 
136
- async function computerCat(token, remotePath) {
1129
+ async function computerCat(token, remotePath, ctx = null) {
137
1130
  if (!remotePath) {
138
1131
  console.error('Usage: atris computer cat <path>');
139
1132
  process.exit(1);
140
1133
  }
1134
+
1135
+ if (ctx) {
1136
+ const result = await apiRequestJson(
1137
+ `/business/${ctx.businessId}/workspaces/${ctx.workspaceId}/file?path=${encodeURIComponent(remotePath)}`,
1138
+ {
1139
+ method: 'GET',
1140
+ token,
1141
+ }
1142
+ );
1143
+ if (!result.ok) {
1144
+ console.error(`Failed: ${result.errorMessage || result.status}`);
1145
+ return;
1146
+ }
1147
+ console.log(result.data.content || '');
1148
+ return;
1149
+ }
1150
+
141
1151
  const result = await apiRequestJson(`/ai-computer/file?path=${encodeURIComponent(remotePath)}`, {
142
1152
  method: 'GET',
143
1153
  token,
@@ -149,16 +1159,14 @@ async function computerCat(token, remotePath) {
149
1159
  console.log(result.data.content || '');
150
1160
  }
151
1161
 
152
- async function computerDiff(token, remotePath) {
1162
+ async function computerDiff(token, remotePath, ctx = null) {
153
1163
  const rPath = remotePath || 'soul';
154
- const fs = require('fs');
155
- const path = require('path');
156
- const crypto = require('crypto');
157
1164
 
158
1165
  // List remote files
159
- const listResult = await apiRequestJson(`/ai-computer/files?path=${encodeURIComponent(rPath)}`, {
160
- method: 'GET', token,
161
- });
1166
+ const listPath = ctx
1167
+ ? `/business/${ctx.businessId}/workspaces/${ctx.workspaceId}/files?path=${encodeURIComponent(rPath)}`
1168
+ : `/ai-computer/files?path=${encodeURIComponent(rPath)}`;
1169
+ const listResult = await apiRequestJson(listPath, { method: 'GET', token });
162
1170
  if (!listResult.ok) {
163
1171
  console.error(`Failed: ${listResult.errorMessage || listResult.status}`);
164
1172
  return;
@@ -199,16 +1207,15 @@ async function computerDiff(token, remotePath) {
199
1207
  console.log(`\n ${added} new, ${modified} changed, ${deleted} deleted, ${same} unchanged`);
200
1208
  }
201
1209
 
202
- async function computerPull(token, remotePath, localDir) {
1210
+ async function computerPull(token, remotePath, localDir, ctx = null) {
203
1211
  const rPath = remotePath || 'soul';
204
1212
  const lDir = localDir || 'ec2_pull';
205
- const fs = require('fs');
206
- const path = require('path');
207
1213
 
208
1214
  // List files
209
- const listResult = await apiRequestJson(`/ai-computer/files?path=${encodeURIComponent(rPath)}`, {
210
- method: 'GET', token,
211
- });
1215
+ const listPath = ctx
1216
+ ? `/business/${ctx.businessId}/workspaces/${ctx.workspaceId}/files?path=${encodeURIComponent(rPath)}`
1217
+ : `/ai-computer/files?path=${encodeURIComponent(rPath)}`;
1218
+ const listResult = await apiRequestJson(listPath, { method: 'GET', token });
212
1219
  if (!listResult.ok) {
213
1220
  console.error(`Failed to list: ${listResult.errorMessage || listResult.status}`);
214
1221
  return;
@@ -225,8 +1232,11 @@ async function computerPull(token, remotePath, localDir) {
225
1232
 
226
1233
  let pulled = 0;
227
1234
  for (const f of files) {
1235
+ const filePath = ctx
1236
+ ? `/business/${ctx.businessId}/workspaces/${ctx.workspaceId}/file?path=${encodeURIComponent(rPath + '/' + f.name)}`
1237
+ : `/ai-computer/file?path=${encodeURIComponent(rPath + '/' + f.name)}`;
228
1238
  const fileResult = await apiRequestJson(
229
- `/ai-computer/file?path=${encodeURIComponent(rPath + '/' + f.name)}`,
1239
+ filePath,
230
1240
  { method: 'GET', token, timeoutMs: 15000 }
231
1241
  );
232
1242
  if (fileResult.ok && fileResult.data.content) {
@@ -318,7 +1328,11 @@ async function computerOnboard(token, businessSlug) {
318
1328
  console.log(` atris computer learn Trigger another learning cycle`);
319
1329
  }
320
1330
 
321
- async function computerLearn(token) {
1331
+ async function computerLearn(token, ctx = null) {
1332
+ if (ctx) {
1333
+ console.error('Learning mode is not wired for business workspaces yet. Use `atris computer exec` for now.');
1334
+ return;
1335
+ }
322
1336
  console.log('Starting learning cycle on EC2...');
323
1337
 
324
1338
  // First check how many learnings exist
@@ -351,12 +1365,59 @@ async function computerLearn(token) {
351
1365
  console.log(` The computer is thinking... check back with: atris computer diff soul`);
352
1366
  }
353
1367
 
354
- async function computerExec(token, prompt) {
1368
+ async function computerExec(token, prompt, ctx = null, options = {}) {
355
1369
  if (!prompt) {
356
1370
  console.error('Usage: atris computer exec "<prompt>"');
357
1371
  process.exit(1);
358
1372
  }
359
1373
  console.log('Executing on computer (with LLM)...');
1374
+
1375
+ if (ctx) {
1376
+ const awake = await ensureBusinessAwake(token, ctx);
1377
+ if (!awake) {
1378
+ console.error(' Computer did not become ready in time.');
1379
+ return;
1380
+ }
1381
+ const result = await apiRequestJson(`/business/${ctx.businessId}/chat`, {
1382
+ method: 'POST',
1383
+ token,
1384
+ body: {
1385
+ message: prompt,
1386
+ workspace_id: ctx.workspaceId,
1387
+ ...(options.worker ? { worker: options.worker } : {}),
1388
+ ...(options.model ? { model: options.model } : {}),
1389
+ },
1390
+ timeoutMs: 40000,
1391
+ });
1392
+ if (!result.ok) {
1393
+ const fallback = await runBusinessPromptViaRunnerProxy(token, ctx, prompt, options);
1394
+ if (!fallback.ok) {
1395
+ console.error(`Failed: ${result.error || result.status}`);
1396
+ if (fallback.error) {
1397
+ console.error(`Fallback failed: ${fallback.error}`);
1398
+ }
1399
+ return;
1400
+ }
1401
+ const text = extractRunnerProxyText(fallback.payload);
1402
+ if (fallback.payload?.execution_id) {
1403
+ console.log(` Execution: ${fallback.payload.execution_id} (runner fallback)`);
1404
+ }
1405
+ if (text) {
1406
+ console.log(text);
1407
+ } else {
1408
+ console.log('(no result)');
1409
+ }
1410
+ return;
1411
+ }
1412
+ const data = result.data || {};
1413
+ const base = getApiBaseUrl();
1414
+ console.log(` Execution: ${data.execution_id}`);
1415
+ console.log(` Session: ${data.session_id}`);
1416
+ console.log(` Stream: ${base}/business/${ctx.businessId}/chat/stream?execution_id=${data.execution_id}&workspace_id=${ctx.workspaceId}`);
1417
+ console.log(' Use the stream URL to watch progress.');
1418
+ return;
1419
+ }
1420
+
360
1421
  const result = await apiRequestJson('/ai-computer/execute', {
361
1422
  method: 'POST',
362
1423
  token,
@@ -371,13 +1432,903 @@ async function computerExec(token, prompt) {
371
1432
  console.log(' Use the stream URL to watch progress.');
372
1433
  }
373
1434
 
1435
+ async function cancelBusinessChat(token, ctx, executionId) {
1436
+ return apiRequestJson(
1437
+ `/business/${ctx.businessId}/chat/cancel?execution_id=${encodeURIComponent(executionId)}&workspace_id=${encodeURIComponent(ctx.workspaceId)}`,
1438
+ { method: 'POST', token, timeoutMs: 15000, retries: 0 }
1439
+ );
1440
+ }
1441
+
1442
+ async function fetchBusinessChatAudit(token, ctx, limit = 10) {
1443
+ return apiRequestJson(
1444
+ `/business/${ctx.businessId}/chat/audit?workspace_id=${encodeURIComponent(ctx.workspaceId)}&limit=${Math.max(1, Math.min(limit, 25))}`,
1445
+ { method: 'GET', token, timeoutMs: 15000, retries: 0 }
1446
+ );
1447
+ }
1448
+
1449
+ async function startBusinessClaudeLogin(token, ctx) {
1450
+ return apiRequestJson('/sandbox-secrets/claude-login/start', {
1451
+ method: 'POST',
1452
+ token,
1453
+ body: { business_id: ctx.businessId },
1454
+ timeoutMs: 15000,
1455
+ retries: 0,
1456
+ });
1457
+ }
1458
+
1459
+ async function fetchBusinessClaudeLoginStatus(token, ctx) {
1460
+ return apiRequestJson(
1461
+ `/sandbox-secrets/claude-login/status?business_id=${encodeURIComponent(ctx.businessId)}`,
1462
+ { method: 'GET', token, timeoutMs: 15000, retries: 0 }
1463
+ );
1464
+ }
1465
+
1466
+ async function submitBusinessClaudeLoginCode(token, ctx, code) {
1467
+ return apiRequestJson('/sandbox-secrets/claude-login/input', {
1468
+ method: 'POST',
1469
+ token,
1470
+ body: { business_id: ctx.businessId, code },
1471
+ timeoutMs: 15000,
1472
+ retries: 0,
1473
+ });
1474
+ }
1475
+
1476
+ async function stopBusinessClaudeLogin(token, ctx) {
1477
+ return apiRequestJson('/sandbox-secrets/claude-login/stop', {
1478
+ method: 'POST',
1479
+ token,
1480
+ body: { business_id: ctx.businessId },
1481
+ timeoutMs: 15000,
1482
+ retries: 0,
1483
+ });
1484
+ }
1485
+
1486
+ function maybeOpenUrl(url) {
1487
+ if (!url) return;
1488
+ if (process.platform === 'darwin') {
1489
+ spawnSync('open', [url], { stdio: 'ignore' });
1490
+ }
1491
+ }
1492
+
1493
+ async function computerCloudLogin(token, ctx, rawArg = '') {
1494
+ if (!ctx) {
1495
+ console.error('Cloud login requires a bound business workspace.');
1496
+ return;
1497
+ }
1498
+
1499
+ const arg = String(rawArg || '').trim();
1500
+ if (arg.toLowerCase() === 'stop') {
1501
+ const stopped = await stopBusinessClaudeLogin(token, ctx);
1502
+ if (!stopped.ok) {
1503
+ console.error(`Failed: ${stopped.errorMessage || stopped.status}`);
1504
+ return;
1505
+ }
1506
+ console.log('Claude login stopped.');
1507
+ return;
1508
+ }
1509
+
1510
+ if (arg) {
1511
+ const submitted = await submitBusinessClaudeLoginCode(token, ctx, arg);
1512
+ if (!submitted.ok) {
1513
+ console.error(`Failed: ${submitted.errorMessage || submitted.status}`);
1514
+ return;
1515
+ }
1516
+ console.log(`Claude login status: ${submitted.data?.status || 'running'}`);
1517
+ return;
1518
+ }
1519
+
1520
+ const started = await startBusinessClaudeLogin(token, ctx);
1521
+ if (!started.ok) {
1522
+ console.error(`Failed: ${started.errorMessage || started.status}`);
1523
+ return;
1524
+ }
1525
+
1526
+ let state = started.data || {};
1527
+ const startedAt = Date.now();
1528
+ while (!state.url && !['completed', 'failed', 'idle'].includes(state.status || '') && Date.now() - startedAt < 15000) {
1529
+ await sleep(1000);
1530
+ const status = await fetchBusinessClaudeLoginStatus(token, ctx);
1531
+ if (!status.ok) {
1532
+ console.error(`Failed: ${status.errorMessage || status.status}`);
1533
+ return;
1534
+ }
1535
+ state = status.data || {};
1536
+ }
1537
+
1538
+ if (state.loggedIn || state.status === 'completed') {
1539
+ console.log('Claude App is already logged in on this computer.');
1540
+ return;
1541
+ }
1542
+ if (state.url) {
1543
+ console.log('Open this URL to log the remote computer into your Claude subscription:');
1544
+ console.log(state.url);
1545
+ maybeOpenUrl(state.url);
1546
+ console.log('After approval, paste the code with `/login <code>`.');
1547
+ return;
1548
+ }
1549
+ if (state.output) {
1550
+ console.log(state.output);
1551
+ console.log('If Claude asks for a code, paste it with `/login <code>`.');
1552
+ return;
1553
+ }
1554
+ console.log(`Claude login status: ${state.status || 'unknown'}`);
1555
+ }
1556
+
1557
+ function printBusinessChatAudit(rows) {
1558
+ console.log('');
1559
+ console.log(ui.bold('Recent cloud runs'));
1560
+ if (!rows.length) {
1561
+ console.log(' No recent cloud runs.');
1562
+ return;
1563
+ }
1564
+ for (const row of rows) {
1565
+ const when = row.started_at ? String(row.started_at).replace('T', ' ').replace(/\.\d+/, '').replace('+00:00', 'Z') : '-';
1566
+ const tokens = Number.isFinite(row.tokens_used) ? `${row.tokens_used} tok` : '-';
1567
+ const cost = Number.isFinite(row.cost_usd) ? `$${Number(row.cost_usd).toFixed(4)}` : '-';
1568
+ const credits = Number(row.credits_charged || 0);
1569
+ const charge = credits > 0 ? ui.yellow(`${credits} cr`) : ui.green('0 cr');
1570
+ console.log(` ${when} ${row.status || 'unknown'} ${tokens} cost ${cost} charge ${charge}`);
1571
+ if (row.worker || row.model) console.log(` ${row.worker || '-'} ${row.model || '-'}`);
1572
+ if (row.preview) console.log(` ${String(row.preview).slice(0, 140)}`);
1573
+ if (row.result_preview) console.log(` => ${String(row.result_preview).slice(0, 180)}`);
1574
+ }
1575
+ }
1576
+
1577
+ async function computerAudit(token, ctx, limit = 10) {
1578
+ if (!ctx) {
1579
+ console.error('Cloud audit requires a bound business workspace.');
1580
+ return;
1581
+ }
1582
+ const result = await fetchBusinessChatAudit(token, ctx, limit);
1583
+ if (!result.ok) {
1584
+ console.error(`Failed: ${result.errorMessage || result.status}`);
1585
+ return;
1586
+ }
1587
+ printBusinessChatAudit(result.data?.rows || []);
1588
+ }
1589
+
1590
+ async function streamBusinessChatResult(token, ctx, executionId, rl = null) {
1591
+ let fromIndex = 0;
1592
+ let errors = 0;
1593
+ let cancelling = false;
1594
+ let cancelPromise = null;
1595
+ let sawVisibleOutput = false;
1596
+
1597
+ const requestCancel = async () => {
1598
+ if (cancelling) return;
1599
+ cancelling = true;
1600
+ process.stdout.write('\nInterrupting cloud run...\n');
1601
+ const result = await cancelBusinessChat(token, ctx, executionId);
1602
+ if (!result.ok) {
1603
+ console.error(`Interrupt failed: ${result.error || result.status}`);
1604
+ return;
1605
+ }
1606
+ const status = result.data?.status || 'sent';
1607
+ if (status === 'not_found') {
1608
+ console.log('Run already finished.');
1609
+ return;
1610
+ }
1611
+ if (status === 'idle') {
1612
+ console.log('No active run to interrupt.');
1613
+ return;
1614
+ }
1615
+ console.log('Interrupt sent.');
1616
+ };
1617
+
1618
+ const onSigint = () => {
1619
+ if (!cancelPromise) {
1620
+ cancelPromise = requestCancel();
1621
+ }
1622
+ };
1623
+
1624
+ if (rl) {
1625
+ rl.on('SIGINT', onSigint);
1626
+ }
1627
+
1628
+ console.log(ui.dim('Running on cloud. Ctrl-C interrupts this run.'));
1629
+
1630
+ try {
1631
+ while (true) {
1632
+ await sleep(1200);
1633
+ const events = await apiRequestJson(
1634
+ `/business/${ctx.businessId}/chat/events?execution_id=${executionId}&workspace_id=${ctx.workspaceId}&from_index=${fromIndex}`,
1635
+ { method: 'GET', token, timeoutMs: 60000 }
1636
+ );
1637
+
1638
+ if (!events.ok) {
1639
+ if (++errors >= 5) {
1640
+ console.error('\nLost connection to AI computer.');
1641
+ return;
1642
+ }
1643
+ continue;
1644
+ }
1645
+
1646
+ errors = 0;
1647
+ let done = false;
1648
+ for (const event of (events.data?.events || [])) {
1649
+ fromIndex++;
1650
+ if (event.type === 'assistant_text' && event.content) {
1651
+ sawVisibleOutput = true;
1652
+ process.stdout.write(event.content);
1653
+ } else if (event.type === 'result' && event.result && !sawVisibleOutput) {
1654
+ sawVisibleOutput = true;
1655
+ process.stdout.write(String(event.result));
1656
+ } else if (event.type === 'tool_use' && event.tool) {
1657
+ const arg = event.input?.file_path || event.input?.path || event.input?.pattern || event.input?.command || '';
1658
+ if (arg) {
1659
+ console.log(`\n [${event.tool}] ${String(arg).slice(0, 120)}`);
1660
+ } else {
1661
+ console.log(`\n [${event.tool}]`);
1662
+ }
1663
+ } else if (event.type === 'error') {
1664
+ if (event.error) console.error(`\n${event.error}`);
1665
+ done = true;
1666
+ break;
1667
+ } else if (event.type === 'complete') {
1668
+ done = true;
1669
+ break;
1670
+ }
1671
+ }
1672
+
1673
+ if (done || ['completed', 'error', 'failed'].includes(events.data?.status)) {
1674
+ if (!process.stdout.write('\n')) {
1675
+ // no-op: keep line handling stable
1676
+ }
1677
+ return;
1678
+ }
1679
+ }
1680
+ } finally {
1681
+ if (rl) {
1682
+ rl.removeListener('SIGINT', onSigint);
1683
+ }
1684
+ }
1685
+ }
1686
+
1687
+ async function sendBusinessChat(token, ctx, message, sessionId, resetContext = false, rl = null, options = {}) {
1688
+ const result = await apiRequestJson(`/business/${ctx.businessId}/chat`, {
1689
+ method: 'POST',
1690
+ token,
1691
+ body: {
1692
+ message,
1693
+ workspace_id: ctx.workspaceId,
1694
+ session_id: sessionId,
1695
+ reset_context: resetContext,
1696
+ ...(options.worker ? { worker: options.worker } : {}),
1697
+ ...(options.model ? { model: options.model } : {}),
1698
+ ...(options.systemPrompt ? { system_prompt: options.systemPrompt } : {}),
1699
+ ...(options.allowedTools ? { allowed_tools: options.allowedTools } : {}),
1700
+ ...(options.localCliSessionId ? { local_cli_session_id: options.localCliSessionId } : {}),
1701
+ },
1702
+ timeoutMs: 40000,
1703
+ });
1704
+
1705
+ if (!result.ok) {
1706
+ const fallback = await runBusinessPromptViaRunnerProxy(token, ctx, message, {
1707
+ ...options,
1708
+ resetContext,
1709
+ maxTurns: 25,
1710
+ });
1711
+ if (!fallback.ok) {
1712
+ console.error(`Failed: ${result.error || result.status}`);
1713
+ if (fallback.error) {
1714
+ console.error(`Fallback failed: ${fallback.error}`);
1715
+ }
1716
+ return sessionId;
1717
+ }
1718
+ const text = extractRunnerProxyText(fallback.payload);
1719
+ if (text) {
1720
+ process.stdout.write(`${text}\n`);
1721
+ } else {
1722
+ process.stdout.write('(no result)\n');
1723
+ }
1724
+ return sessionId;
1725
+ }
1726
+
1727
+ const data = result.data || {};
1728
+ const nextSessionId = data.session_id || sessionId;
1729
+ if (rl) rl.pause();
1730
+ try {
1731
+ await streamBusinessChatResult(token, ctx, data.execution_id, rl);
1732
+ } finally {
1733
+ if (rl) rl.resume();
1734
+ }
1735
+ return nextSessionId;
1736
+ }
1737
+
1738
+ async function computerChat(token, ctx, initialOptions = {}) {
1739
+ if (!ctx) {
1740
+ console.error('Cloud computer mode requires a bound business workspace.');
1741
+ console.error('Run this inside ~/arena/atris-business/<slug>/, or use `atris computer --local` for local mode.');
1742
+ return;
1743
+ }
1744
+
1745
+ let sessionId = `biz-${ctx.businessId.slice(0, 8)}-${Date.now().toString(36)}`;
1746
+ printCloudWordmark();
1747
+ const selection = await chooseCloudLane(token, ctx, initialOptions);
1748
+ if (selection.cancelled) return;
1749
+ let worker = selection.worker || null;
1750
+ let model = selection.model || null;
1751
+ let awaitingLoginCode = false;
1752
+ let billingLabel = await describeBillingMode(token, ctx, worker);
1753
+ let authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
1754
+
1755
+ const awake = await ensureBusinessAwake(token, ctx);
1756
+ if (!awake) {
1757
+ console.error(' Computer did not become ready in time.');
1758
+ return;
1759
+ }
1760
+
1761
+ printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
1762
+
1763
+ const rl = readline.createInterface({
1764
+ input: process.stdin,
1765
+ output: process.stdout,
1766
+ prompt: 'cloud> ',
1767
+ });
1768
+
1769
+ rl.prompt();
1770
+
1771
+ try {
1772
+ for await (const rawLine of rl) {
1773
+ const line = String(rawLine || '').trim();
1774
+ if (!line) {
1775
+ rl.prompt();
1776
+ continue;
1777
+ }
1778
+ if (line === '/exit' || line === '/quit') {
1779
+ rl.close();
1780
+ break;
1781
+ }
1782
+ if (line === '/start') {
1783
+ billingLabel = await describeBillingMode(token, ctx, worker);
1784
+ authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
1785
+ printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
1786
+ rl.prompt();
1787
+ continue;
1788
+ }
1789
+ if (line === '/help') {
1790
+ printCloudHelp();
1791
+ rl.prompt();
1792
+ continue;
1793
+ }
1794
+ if (line === '/status') {
1795
+ await printCloudSessionStatus(token, ctx, worker, model);
1796
+ rl.prompt();
1797
+ continue;
1798
+ }
1799
+ if (line === '/pwd') {
1800
+ await computerRun(token, 'pwd', ctx);
1801
+ rl.prompt();
1802
+ continue;
1803
+ }
1804
+ if (line === '/files' || line.startsWith('/files ')) {
1805
+ const filePath = line.slice('/files'.length).trim() || '/workspace';
1806
+ await computerLs(token, filePath, ctx);
1807
+ rl.prompt();
1808
+ continue;
1809
+ }
1810
+ if (line === '/reset') {
1811
+ sessionId = `biz-${ctx.businessId.slice(0, 8)}-${Date.now().toString(36)}`;
1812
+ console.log('Session reset.');
1813
+ rl.prompt();
1814
+ continue;
1815
+ }
1816
+ if (line === '/worker' || line.startsWith('/worker ')) {
1817
+ const nextWorker = line.split(/\s+/, 2)[1];
1818
+ if (!nextWorker) {
1819
+ console.log(`Worker: ${worker || 'default'}`);
1820
+ } else if (!VALID_CLOUD_WORKERS.has(nextWorker)) {
1821
+ console.log('Expected: /worker claude|openai');
1822
+ } else {
1823
+ worker = nextWorker;
1824
+ billingLabel = await describeBillingMode(token, ctx, worker);
1825
+ authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
1826
+ console.log(`Lane: ${formatWorkerName(worker)}`);
1827
+ console.log(`Billing: ${billingLabel}`);
1828
+ if (authSummary) console.log(authSummary.label);
1829
+ }
1830
+ rl.prompt();
1831
+ continue;
1832
+ }
1833
+ if (line === '/model' || line.startsWith('/model ')) {
1834
+ const nextModel = line.split(/\s+/, 2)[1];
1835
+ if (!nextModel) {
1836
+ console.log(`Model: ${model || 'default'}`);
1837
+ } else {
1838
+ model = nextModel;
1839
+ console.log(`Model set: ${model}`);
1840
+ }
1841
+ rl.prompt();
1842
+ continue;
1843
+ }
1844
+ if (line === '/run') {
1845
+ console.log('Usage: /run <shell command>');
1846
+ rl.prompt();
1847
+ continue;
1848
+ }
1849
+ if (line.startsWith('/run ')) {
1850
+ await computerRun(token, line.slice(5), ctx);
1851
+ rl.prompt();
1852
+ continue;
1853
+ }
1854
+ if (line === '/audit' || line.startsWith('/audit ')) {
1855
+ const rawLimit = line.split(/\s+/, 2)[1];
1856
+ const limit = rawLimit ? Number.parseInt(rawLimit, 10) : 10;
1857
+ await computerAudit(token, ctx, Number.isFinite(limit) ? limit : 10);
1858
+ rl.prompt();
1859
+ continue;
1860
+ }
1861
+ if (line === '/login' || line.startsWith('/login ')) {
1862
+ const loginArg = line.split(/\s+/, 2)[1] || '';
1863
+ await computerCloudLogin(token, ctx, loginArg);
1864
+ billingLabel = await describeBillingMode(token, ctx, worker);
1865
+ authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
1866
+ awaitingLoginCode = !loginArg || loginArg.toLowerCase() === 'stop' ? !loginArg : false;
1867
+ rl.prompt();
1868
+ continue;
1869
+ }
1870
+ if (
1871
+ awaitingLoginCode &&
1872
+ !line.startsWith('/') &&
1873
+ /^[A-Za-z0-9._~-]+#[A-Za-z0-9._~-]+$/.test(line)
1874
+ ) {
1875
+ await computerCloudLogin(token, ctx, line);
1876
+ billingLabel = await describeBillingMode(token, ctx, worker);
1877
+ authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
1878
+ awaitingLoginCode = false;
1879
+ rl.prompt();
1880
+ continue;
1881
+ }
1882
+ if (line.startsWith('/')) {
1883
+ const command = line.split(/\s+/, 1)[0];
1884
+ if (!KNOWN_CHAT_COMMANDS.has(command)) {
1885
+ console.log(`Unknown command: ${command}`);
1886
+ console.log('Type /help for commands, or remove the slash to ask the model.');
1887
+ rl.prompt();
1888
+ continue;
1889
+ }
1890
+ }
1891
+
1892
+ sessionId = await sendBusinessChat(token, ctx, line, sessionId, false, rl, { worker, model });
1893
+ rl.prompt();
1894
+ }
1895
+ } catch (error) {
1896
+ if (!String(error?.message || error || '').includes('readline was closed')) {
1897
+ throw error;
1898
+ }
1899
+ }
1900
+ }
1901
+
1902
+ async function computerLocalAtris(token, ctx, initialOptions = {}) {
1903
+ if (!ctx) {
1904
+ console.error('Atris local mode needs a bound business workspace for the cloud brain.');
1905
+ console.error('Run inside ~/arena/atris-business/<slug>/, or use `atris computer local-byo`.');
1906
+ return;
1907
+ }
1908
+
1909
+ const pipedInput = await readPipedStdin();
1910
+
1911
+ printLocalWordmark();
1912
+ const selection = await chooseCloudLane(token, ctx, initialOptions);
1913
+ if (selection.cancelled) return;
1914
+ let worker = selection.worker || null;
1915
+ let model = selection.model || null;
1916
+ let bridge = null;
1917
+ let sessionId = `local-${ctx.businessId.slice(0, 8)}-${Date.now().toString(36)}`;
1918
+
1919
+ try {
1920
+ bridge = await startLocalAtrisBridge(token, { allowBash: true });
1921
+ } catch (err) {
1922
+ console.error(`Failed to start local bridge: ${err.message}`);
1923
+ return;
1924
+ }
1925
+
1926
+ const cleanup = async () => {
1927
+ if (bridge) {
1928
+ const stop = bridge.stop;
1929
+ bridge = null;
1930
+ await stop();
1931
+ }
1932
+ };
1933
+
1934
+ process.once('SIGINT', cleanup);
1935
+ process.once('SIGTERM', cleanup);
1936
+
1937
+ try {
1938
+ const awake = await ensureBusinessAwake(token, ctx);
1939
+ if (!awake) {
1940
+ console.error(' Computer did not become ready in time.');
1941
+ return;
1942
+ }
1943
+
1944
+ let billingLabel = await describeBillingMode(token, ctx, worker);
1945
+ let authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
1946
+ let awaitingLoginCode = false;
1947
+ let localSystemPrompt = buildLocalBridgeSystemPrompt(bridge.sessionId, bridge.workingDir, bridge.allowBash);
1948
+
1949
+ printLocalAtrisStartPanel(ctx, bridge, worker, model, billingLabel, authSummary);
1950
+
1951
+ const rl = pipedInput === null
1952
+ ? readline.createInterface({
1953
+ input: process.stdin,
1954
+ output: process.stdout,
1955
+ prompt: 'local> ',
1956
+ })
1957
+ : null;
1958
+ const inputLines = pipedInput === null ? rl : String(pipedInput).split(/\r?\n/);
1959
+ const promptLocal = () => {
1960
+ if (rl) rl.prompt();
1961
+ };
1962
+
1963
+ promptLocal();
1964
+
1965
+ try {
1966
+ for await (const rawLine of inputLines) {
1967
+ const line = String(rawLine || '').trim();
1968
+ if (!line) {
1969
+ promptLocal();
1970
+ continue;
1971
+ }
1972
+ if (line === '/exit' || line === '/quit') {
1973
+ if (rl) rl.close();
1974
+ break;
1975
+ }
1976
+ if (line === '/start') {
1977
+ billingLabel = await describeBillingMode(token, ctx, worker);
1978
+ authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
1979
+ printLocalAtrisStartPanel(ctx, bridge, worker, model, billingLabel, authSummary);
1980
+ promptLocal();
1981
+ continue;
1982
+ }
1983
+ if (line === '/help') {
1984
+ console.log('');
1985
+ console.log(ui.bold('Local Atris commands'));
1986
+ console.log(' /status Show local bridge, lane, billing');
1987
+ console.log(' /files [path] List local files');
1988
+ console.log(' /run <cmd> Run shell in this local folder');
1989
+ console.log(' /audit [n] Show recent cloud brain runs');
1990
+ console.log(' /worker claude Use Claude lane');
1991
+ console.log(' /worker openai Use OpenAI lane');
1992
+ console.log(' /model [id] Set model override');
1993
+ console.log(' /login Connect Claude subscription on remote brain');
1994
+ console.log(' /reset Start a fresh chat session');
1995
+ console.log(' /exit Leave local Atris mode');
1996
+ console.log('');
1997
+ promptLocal();
1998
+ continue;
1999
+ }
2000
+ if (line === '/status') {
2001
+ console.log('');
2002
+ console.log(ui.bold('Local status'));
2003
+ console.log(` Local folder: ${bridge.workingDir}`);
2004
+ console.log(` Bridge: ${bridge.sessionId}`);
2005
+ console.log(` Bash: ${bridge.allowBash ? 'enabled' : 'disabled'}`);
2006
+ console.log(` Business: ${ctx.businessName}`);
2007
+ console.log(` Lane: ${formatWorkerName(worker)} ${formatCloudSelection({ worker, model })}`);
2008
+ billingLabel = await describeBillingMode(token, ctx, worker);
2009
+ console.log(` Billing: ${billingLabel}`);
2010
+ promptLocal();
2011
+ continue;
2012
+ }
2013
+ if (line === '/files' || line.startsWith('/files ')) {
2014
+ const filePath = line.slice('/files'.length).trim();
2015
+ const safePath = filePath ? filePath.replace(/'/g, "'\\''") : '.';
2016
+ const op = await runLocalBridgeOp(token, bridge.sessionId, {
2017
+ type: 'bash',
2018
+ command: `ls -la '${safePath}'`,
2019
+ });
2020
+ const stdout = op?.result?.stdout || '';
2021
+ const stderr = op?.result?.stderr || '';
2022
+ if (stdout) process.stdout.write(stdout);
2023
+ if (stderr) process.stderr.write(stderr);
2024
+ promptLocal();
2025
+ continue;
2026
+ }
2027
+ if (line === '/run') {
2028
+ console.log('Usage: /run <local shell command>');
2029
+ promptLocal();
2030
+ continue;
2031
+ }
2032
+ if (line.startsWith('/run ')) {
2033
+ const op = await runLocalBridgeOp(token, bridge.sessionId, {
2034
+ type: 'bash',
2035
+ command: line.slice(5),
2036
+ }, 30);
2037
+ const stdout = op?.result?.stdout || '';
2038
+ const stderr = op?.result?.stderr || '';
2039
+ if (stdout) process.stdout.write(stdout);
2040
+ if (stderr) process.stderr.write(stderr);
2041
+ promptLocal();
2042
+ continue;
2043
+ }
2044
+ if (line === '/audit' || line.startsWith('/audit ')) {
2045
+ const rawLimit = line.split(/\s+/, 2)[1];
2046
+ const limit = rawLimit ? Number.parseInt(rawLimit, 10) : 10;
2047
+ await computerAudit(token, ctx, Number.isFinite(limit) ? limit : 10);
2048
+ promptLocal();
2049
+ continue;
2050
+ }
2051
+ if (line === '/reset') {
2052
+ sessionId = `local-${ctx.businessId.slice(0, 8)}-${Date.now().toString(36)}`;
2053
+ console.log('Session reset.');
2054
+ promptLocal();
2055
+ continue;
2056
+ }
2057
+ if (line === '/worker' || line.startsWith('/worker ')) {
2058
+ const nextWorker = line.split(/\s+/, 2)[1];
2059
+ if (!nextWorker) {
2060
+ console.log(`Worker: ${worker || 'default'}`);
2061
+ } else if (!VALID_CLOUD_WORKERS.has(nextWorker)) {
2062
+ console.log('Expected: /worker claude|openai');
2063
+ } else {
2064
+ worker = nextWorker;
2065
+ billingLabel = await describeBillingMode(token, ctx, worker);
2066
+ authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
2067
+ console.log(`Lane: ${formatWorkerName(worker)}`);
2068
+ console.log(`Billing: ${billingLabel}`);
2069
+ if (authSummary) console.log(authSummary.label);
2070
+ }
2071
+ promptLocal();
2072
+ continue;
2073
+ }
2074
+ if (line === '/model' || line.startsWith('/model ')) {
2075
+ const nextModel = line.split(/\s+/, 2)[1];
2076
+ if (!nextModel) {
2077
+ console.log(`Model: ${model || 'default'}`);
2078
+ } else {
2079
+ model = nextModel;
2080
+ console.log(`Model set: ${model}`);
2081
+ }
2082
+ promptLocal();
2083
+ continue;
2084
+ }
2085
+ if (line === '/login' || line.startsWith('/login ')) {
2086
+ const loginArg = line.split(/\s+/, 2)[1] || '';
2087
+ await computerCloudLogin(token, ctx, loginArg);
2088
+ billingLabel = await describeBillingMode(token, ctx, worker);
2089
+ authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
2090
+ awaitingLoginCode = !loginArg || loginArg.toLowerCase() === 'stop' ? !loginArg : false;
2091
+ promptLocal();
2092
+ continue;
2093
+ }
2094
+ if (
2095
+ awaitingLoginCode &&
2096
+ !line.startsWith('/') &&
2097
+ /^[A-Za-z0-9._~-]+#[A-Za-z0-9._~-]+$/.test(line)
2098
+ ) {
2099
+ await computerCloudLogin(token, ctx, line);
2100
+ billingLabel = await describeBillingMode(token, ctx, worker);
2101
+ authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
2102
+ awaitingLoginCode = false;
2103
+ promptLocal();
2104
+ continue;
2105
+ }
2106
+ if (line.startsWith('/')) {
2107
+ const command = line.split(/\s+/, 1)[0];
2108
+ if (!KNOWN_CHAT_COMMANDS.has(command)) {
2109
+ console.log(`Unknown command: ${command}`);
2110
+ console.log('Type /help for commands, or remove the slash to ask the model.');
2111
+ promptLocal();
2112
+ continue;
2113
+ }
2114
+ }
2115
+
2116
+ localSystemPrompt = buildLocalBridgeSystemPrompt(bridge.sessionId, bridge.workingDir, bridge.allowBash);
2117
+ sessionId = await sendBusinessChat(token, ctx, line, sessionId, false, rl, {
2118
+ worker,
2119
+ model,
2120
+ systemPrompt: localSystemPrompt,
2121
+ localCliSessionId: bridge.sessionId,
2122
+ });
2123
+ promptLocal();
2124
+ }
2125
+ } catch (error) {
2126
+ if (!String(error?.message || error || '').includes('readline was closed')) {
2127
+ throw error;
2128
+ }
2129
+ }
2130
+ } finally {
2131
+ process.removeListener('SIGINT', cleanup);
2132
+ process.removeListener('SIGTERM', cleanup);
2133
+ await cleanup();
2134
+ }
2135
+ }
2136
+
2137
+ async function computerProof(token, ctx, initialOptions = {}) {
2138
+ if (!ctx) {
2139
+ console.error('Atris computer proof needs a bound business workspace.');
2140
+ console.error('Run inside ~/arena/atris-business/<slug>/ first.');
2141
+ process.exitCode = 1;
2142
+ return;
2143
+ }
2144
+
2145
+ const worker = initialOptions.worker || 'openai';
2146
+ const model = initialOptions.model || null;
2147
+ const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
2148
+ const fileName = `atris-proof-${stamp}.txt`;
2149
+ const expected = `ATRIS_PROOF_OK_${stamp}`;
2150
+ const sessionId = `proof-${ctx.businessId.slice(0, 8)}-${Date.now().toString(36)}`;
2151
+ let bridge = null;
2152
+
2153
+ console.log('');
2154
+ console.log(ui.bold('Atris Computer Proof'));
2155
+ console.log(`${ctx.businessName} ${ui.dim('cloud brain -> local folder -> audit')}`);
2156
+ console.log(`Lane: ${ui.bold(formatWorkerName(worker))} ${ui.dim(formatCloudSelection({ worker, model }))}`);
2157
+
2158
+ try {
2159
+ bridge = await startLocalAtrisBridge(token, { allowBash: true });
2160
+ } catch (err) {
2161
+ console.error(`Failed to start local bridge: ${err.message}`);
2162
+ process.exitCode = 1;
2163
+ return;
2164
+ }
2165
+
2166
+ const cleanup = async () => {
2167
+ if (bridge) {
2168
+ const stop = bridge.stop;
2169
+ bridge = null;
2170
+ await stop();
2171
+ }
2172
+ };
2173
+
2174
+ process.once('SIGINT', cleanup);
2175
+ process.once('SIGTERM', cleanup);
2176
+
2177
+ try {
2178
+ const awake = await ensureBusinessAwake(token, ctx);
2179
+ if (!awake) {
2180
+ console.error('Computer did not become ready in time.');
2181
+ process.exitCode = 1;
2182
+ return;
2183
+ }
2184
+
2185
+ const billingLabel = await describeBillingMode(token, ctx, worker);
2186
+ console.log(`Local: ${bridge.workingDir}`);
2187
+ console.log(`Bridge: ${bridge.sessionId.slice(0, 8)} ${ui.dim('local bash enabled')}`);
2188
+ console.log(`Billing: ${billingLabel}`);
2189
+ console.log('');
2190
+
2191
+ const prompt = [
2192
+ `Create a file named ${fileName} in the LOCAL folder with exactly ${expected}.`,
2193
+ 'Use local_file_op for the write.',
2194
+ 'Read it back through local_file_op.',
2195
+ 'Reply with exactly ATRIS COMPUTER PROOF OK.',
2196
+ ].join(' ');
2197
+ const systemPrompt = buildLocalBridgeSystemPrompt(bridge.sessionId, bridge.workingDir, bridge.allowBash);
2198
+
2199
+ console.log(ui.bold('Run'));
2200
+ console.log(` prompt: ${prompt}`);
2201
+ const nextSessionId = await sendBusinessChat(token, ctx, prompt, sessionId, true, null, {
2202
+ worker,
2203
+ model,
2204
+ systemPrompt,
2205
+ localCliSessionId: bridge.sessionId,
2206
+ });
2207
+
2208
+ const localPath = path.join(bridge.workingDir, fileName);
2209
+ let localContent = '';
2210
+ try {
2211
+ localContent = fs.readFileSync(localPath, 'utf8').trim();
2212
+ } catch {
2213
+ localContent = '';
2214
+ }
2215
+ const localOk = localContent === expected;
2216
+
2217
+ const cloudFile = await readBusinessWorkspaceFile(token, ctx, fileName, 15000);
2218
+ const cloudClear = !cloudFile.ok && cloudFile.status === 404;
2219
+
2220
+ const audit = await fetchBusinessChatAudit(token, ctx, 5);
2221
+ const rows = audit.ok ? (audit.data?.rows || []) : [];
2222
+ const auditRow = rows.find((row) => row.session_id === nextSessionId || row.preview?.includes(fileName)) || rows[0] || {};
2223
+ const auditOk = audit.ok && auditRow.status === 'completed' && String(auditRow.result_preview || '').includes('ATRIS COMPUTER PROOF OK');
2224
+
2225
+ console.log('');
2226
+ console.log(ui.bold('Proof'));
2227
+ console.log(` local edit: ${localOk ? ui.green('PASS') : ui.red('FAIL')} ${fileName}`);
2228
+ console.log(` local contents: ${localContent || '(missing)'}`);
2229
+ console.log(` cloud isolation: ${cloudClear ? ui.green('PASS') : ui.red('FAIL')} /workspace/${fileName} ${cloudClear ? 'absent' : 'present or unchecked'}`);
2230
+ console.log(` audit: ${auditOk ? ui.green('PASS') : ui.red('CHECK')} ${auditRow.status || 'unknown'} ${auditRow.worker || '-'} charge ${auditRow.credits_charged ?? '-'} cr`);
2231
+ if (auditRow.result_preview) console.log(` result: ${String(auditRow.result_preview).slice(0, 120)}`);
2232
+ console.log('');
2233
+ console.log(ui.bold('Team command'));
2234
+ console.log(' atris computer local --worker openai');
2235
+ console.log('');
2236
+
2237
+ if (!localOk || !cloudClear || !auditOk) {
2238
+ process.exitCode = 1;
2239
+ }
2240
+ } finally {
2241
+ process.removeListener('SIGINT', cleanup);
2242
+ process.removeListener('SIGTERM', cleanup);
2243
+ await cleanup();
2244
+ }
2245
+ }
2246
+
374
2247
  async function runComputer() {
375
- const sub = process.argv[3];
2248
+ const parsed = parseComputerOptions(process.argv.slice(3));
2249
+ const args = parsed.positional;
2250
+ const cloudOptions = parsed.options;
2251
+ const sub = args[0];
2252
+
2253
+ if (!sub) {
2254
+ const hasBusinessBinding = Boolean(readBusinessBinding());
2255
+ const hasLocalHarness = Boolean(findAtrisCodeTerminal());
2256
+ const surface = await chooseComputerSurface(hasBusinessBinding, hasLocalHarness);
2257
+ if (!surface) return;
2258
+ if ((surface === 'cloud' || surface === 'local-atris') && hasBusinessBinding) {
2259
+ const token = getToken();
2260
+ const ctx = await resolveBusinessContext(token);
2261
+ if (ctx) {
2262
+ if (surface === 'local-atris') {
2263
+ await computerLocalAtris(token, ctx, cloudOptions);
2264
+ } else {
2265
+ await computerChat(token, ctx, cloudOptions);
2266
+ }
2267
+ return;
2268
+ }
2269
+ }
2270
+ if (surface === 'local-byo' && hasLocalHarness) {
2271
+ computerLocal();
2272
+ return;
2273
+ }
2274
+ computerLocal();
2275
+ return;
2276
+ }
376
2277
 
377
- if (!sub || sub === '--help') {
378
- console.log('Usage: atris computer <command>');
2278
+ if (sub === '--local' || sub === 'local') {
2279
+ const token = getToken();
2280
+ const ctx = await resolveBusinessContext(token);
2281
+ if (ctx) {
2282
+ await computerLocalAtris(token, ctx, cloudOptions);
2283
+ return;
2284
+ }
2285
+ computerLocal(args.slice(1));
2286
+ return;
2287
+ }
2288
+
2289
+ if (sub === 'local-atris') {
2290
+ const token = getToken();
2291
+ const ctx = await resolveBusinessContext(token);
2292
+ await computerLocalAtris(token, ctx, cloudOptions);
2293
+ return;
2294
+ }
2295
+
2296
+ if (sub === 'local-byo' || sub === '--local-byo') {
2297
+ computerLocal(args.slice(1));
2298
+ return;
2299
+ }
2300
+
2301
+ if (sub === 'claude' || sub === 'codex') {
2302
+ computerLocalLegacy(args);
2303
+ return;
2304
+ }
2305
+
2306
+ if (sub === '--help') {
2307
+ console.log('Usage: atris computer [mode|command]');
2308
+ console.log('');
2309
+ console.log('First use:');
2310
+ console.log(' cd ~/arena/atris-business/<business>');
2311
+ console.log(' atris computer');
2312
+ console.log(' Choose Cloud workspace or Local folder, then type the outcome in plain English.');
379
2313
  console.log('');
380
- console.log('Commands:');
2314
+ console.log('Modes:');
2315
+ console.log(' (default) Choose CLOUD vs LOCAL when both are available');
2316
+ console.log(' local Open LOCAL Atris mode; cloud brain edits this folder');
2317
+ console.log(' proof Run the local-edit + cloud-isolation + audit proof');
2318
+ console.log(' local-byo Open LOCAL BYO Claude mode; Anthropic tokens, no cloud audit');
2319
+ console.log(' --cloud Open CLOUD workspace mode in the bound business workspace');
2320
+ console.log(' cloud Open CLOUD workspace mode in the bound business workspace');
2321
+ console.log(' codeops Open Atris CodeOps cloud workspace if your account has access');
2322
+ console.log(' --worker Cloud worker override: claude | openai');
2323
+ console.log(' --model Cloud model override');
2324
+ console.log(' claude|codex Legacy local console backends');
2325
+ console.log('');
2326
+ console.log('Cloud commands:');
2327
+ console.log(' chat Interactive cloud workspace chat');
2328
+ console.log(' Ctrl-C during a cloud run interrupts it');
2329
+ console.log(' /start shows the beginner flow');
2330
+ console.log(' /status shows lane, Claude auth, and billing');
2331
+ console.log(' /audit [n] shows recent cloud runs inside chat');
381
2332
  console.log(' status Show computer status');
382
2333
  console.log(' wake Start the computer');
383
2334
  console.log(' sleep Stop the computer (files persist)');
@@ -392,6 +2343,14 @@ async function runComputer() {
392
2343
  console.log(' onboard <slug> Set up a new business computer');
393
2344
  console.log('');
394
2345
  console.log('Examples:');
2346
+ console.log(' atris computer');
2347
+ console.log(' atris computer proof');
2348
+ console.log(' atris computer local');
2349
+ console.log(' atris computer codex');
2350
+ console.log(' atris computer --cloud');
2351
+ console.log(' atris computer --cloud --worker openai --model gpt-5.4');
2352
+ console.log(' atris computer cloud');
2353
+ console.log(' atris computer codeops');
395
2354
  console.log(' atris computer status');
396
2355
  console.log(' atris computer wake');
397
2356
  console.log(' atris computer run "ls -la /workspace"');
@@ -402,23 +2361,43 @@ async function runComputer() {
402
2361
  }
403
2362
 
404
2363
  const token = getToken();
405
- const rest = process.argv.slice(4).join(' ');
2364
+ const ctx = await resolveBusinessContext(token);
2365
+
2366
+ if (sub === 'codeops') {
2367
+ const codeopsCtx = await resolveBusinessContextBySlug(token, 'atris-codeops');
2368
+ if (!codeopsCtx) {
2369
+ console.error('Atris CodeOps is not available for this account.');
2370
+ console.error('Ask an Atris CodeOps admin to add you to the atris-codeops business.');
2371
+ return;
2372
+ }
2373
+ await computerChat(token, codeopsCtx, { worker: 'claude', ...cloudOptions });
2374
+ return;
2375
+ }
2376
+
2377
+ if (sub === '--cloud' || sub === 'cloud') {
2378
+ await computerChat(token, ctx, cloudOptions);
2379
+ return;
2380
+ }
2381
+
2382
+ const rest = args.slice(1).join(' ');
406
2383
 
407
2384
  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);
2385
+ case 'chat': return computerChat(token, ctx, cloudOptions);
2386
+ case 'proof': return computerProof(token, ctx, cloudOptions);
2387
+ case 'status': return computerStatus(token, ctx);
2388
+ case 'wake': return computerWake(token, ctx);
2389
+ case 'sleep': return computerSleep(token, ctx);
2390
+ case 'run': return computerRun(token, rest, ctx);
2391
+ case 'grep': return computerGrep(token, rest, ctx);
2392
+ case 'ls': return computerLs(token, rest || undefined, ctx);
2393
+ case 'cat': return computerCat(token, rest, ctx);
2394
+ case 'exec': return computerExec(token, rest, ctx, cloudOptions);
416
2395
  case 'pull': {
417
2396
  const parts = rest.split(' ').filter(Boolean);
418
- return computerPull(token, parts[0], parts[1]);
2397
+ return computerPull(token, parts[0], parts[1], ctx);
419
2398
  }
420
- case 'diff': return computerDiff(token, rest || undefined);
421
- case 'learn': return computerLearn(token);
2399
+ case 'diff': return computerDiff(token, rest || undefined, ctx);
2400
+ case 'learn': return computerLearn(token, ctx);
422
2401
  case 'onboard': return computerOnboard(token, rest);
423
2402
  default:
424
2403
  console.error(`Unknown subcommand: ${sub}`);