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.
- package/GETTING_STARTED.md +65 -131
- package/README.md +18 -2
- package/atris/GETTING_STARTED.md +65 -131
- package/atris/PERSONA.md +5 -1
- package/atris/atris.md +122 -153
- package/atris/skills/aeo/SKILL.md +117 -0
- package/atris/skills/atris/SKILL.md +49 -25
- package/atris/skills/create-member/SKILL.md +29 -9
- package/atris/skills/endgame/SKILL.md +9 -0
- package/atris/skills/research-search/SKILL.md +167 -0
- package/atris/skills/research-search/arxiv_search.py +157 -0
- package/atris/skills/research-search/program.md +48 -0
- package/atris/skills/research-search/results.tsv +6 -0
- package/atris/skills/research-search/scholar_search.py +154 -0
- package/atris/skills/tidy/SKILL.md +36 -21
- package/atris/team/_template/MEMBER.md +2 -0
- package/atris/team/validator/MEMBER.md +35 -1
- package/atris.md +118 -178
- package/bin/atris.js +46 -12
- package/cli/__pycache__/atris_code.cpython-314.pyc +0 -0
- package/cli/__pycache__/runtime_guard.cpython-312.pyc +0 -0
- package/cli/__pycache__/runtime_guard.cpython-314.pyc +0 -0
- package/cli/atris_code.py +889 -0
- package/cli/runtime_guard.py +693 -0
- package/commands/align.js +16 -0
- package/commands/app.js +316 -0
- package/commands/autopilot.js +863 -23
- package/commands/brainstorm.js +7 -5
- package/commands/business.js +677 -2
- package/commands/clean.js +19 -3
- package/commands/computer.js +2022 -43
- package/commands/context-sync.js +5 -0
- package/commands/integrations.js +14 -9
- package/commands/lifecycle.js +12 -0
- package/commands/plugin.js +24 -0
- package/commands/pull.js +86 -11
- package/commands/push.js +153 -9
- package/commands/serve.js +1 -0
- package/commands/sync.js +272 -76
- package/commands/verify.js +50 -1
- package/commands/wiki.js +27 -2
- package/commands/workflow.js +24 -9
- package/lib/file-ops.js +13 -1
- package/lib/journal.js +23 -0
- package/lib/manifest.js +3 -0
- package/lib/scorecard.js +42 -4
- package/lib/sync-telemetry.js +59 -0
- package/lib/todo.js +6 -0
- package/lib/wiki.js +150 -6
- package/lib/workspace-safety.js +87 -0
- package/package.json +2 -1
- package/utils/api.js +19 -0
- package/utils/auth.js +25 -1
- package/utils/config.js +24 -0
- package/utils/update-check.js +16 -0
package/commands/computer.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Atris Computer — interact with your EC2 AI Computer
|
|
3
3
|
*
|
|
4
|
-
* atris computer —
|
|
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
|
|
15
|
-
const
|
|
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
|
-
|
|
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
|
|
160
|
-
|
|
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
|
|
210
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
378
|
-
|
|
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('
|
|
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
|
|
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 '
|
|
409
|
-
case '
|
|
410
|
-
case '
|
|
411
|
-
case '
|
|
412
|
-
case '
|
|
413
|
-
case '
|
|
414
|
-
case '
|
|
415
|
-
case '
|
|
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}`);
|