atris 3.15.23 → 3.15.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/ax +625 -0
- package/commands/computer.js +176 -1
- package/commands/gm.js +27 -9
- package/commands/play.js +43 -9
- package/commands/sync.js +9 -4
- package/commands/xp.js +237 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -86,6 +86,56 @@ Core loop: `plan` -> `do` -> `review`
|
|
|
86
86
|
|
|
87
87
|
Integrates with any agent.
|
|
88
88
|
|
|
89
|
+
## Chat With Atris 2
|
|
90
|
+
|
|
91
|
+
`ax` is the local Atris 2 coding-agent CLI. It talks to the AtrisOS backend at `http://127.0.0.1:8000/api/atris2/turn`, streams text, shows tool activity, and sends the current folder as the workspace.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
cd your-project
|
|
95
|
+
ax --pro "find the task system and explain it"
|
|
96
|
+
ax --fast "what files are here?"
|
|
97
|
+
ax --pro --chat
|
|
98
|
+
ax --doctor
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Use Pro for agentic file work and edit/test loops. Use Fast for quick workspace search, short chats, and small edits.
|
|
102
|
+
|
|
103
|
+
## Play AgentXP
|
|
104
|
+
|
|
105
|
+
AgentXP is the proof-backed game loop for getting better with agents. Start it
|
|
106
|
+
inside any project folder:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm exec --yes --package github:atrislabs/atris#v3.15.28 -- atris play --as <player>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The first run creates a local starter mission if one does not exist. The loop is:
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
start -> proof -> accept -> login -> sync
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The player path:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
atris play --as justin
|
|
122
|
+
atris task claim <mission-ref> --as game-manager
|
|
123
|
+
atris task ready <mission-ref> --as game-manager --proof "<artifact path + verifier result>"
|
|
124
|
+
atris task accept <mission-ref> --as justin --proof "<human review>"
|
|
125
|
+
atris xp card --local
|
|
126
|
+
atris login
|
|
127
|
+
atris xp sync --local --as justin
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The manager path:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
atris gm --player justin
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
No proof, no AgentXP. Human accept/revise is the gate before XP can count on
|
|
137
|
+
the local card or hosted leaderboard.
|
|
138
|
+
|
|
89
139
|
## Business Owners
|
|
90
140
|
|
|
91
141
|
If you want a shared owner for a company, lab, collective, community, artist, team, or project, use the business command instead of raw `atris init`.
|
|
@@ -138,6 +188,9 @@ atris business record atris/reports/2026-04-12-operator-recap.md --outcome mixed
|
|
|
138
188
|
| `atris log` | Add inbox items to today's journal |
|
|
139
189
|
| `atris status` | Show active work and completions |
|
|
140
190
|
| `atris task` | Durable local task state with claims, dialogue, review episodes, JSON export, TODO import, TODO render, board, and sync dry-run |
|
|
191
|
+
| `atris play` | Enter the AgentXP player loop for one proof-backed mission |
|
|
192
|
+
| `atris gm` | Enter AgentXP General Manager mode for player missions and review queues |
|
|
193
|
+
| `atris xp` | Show the local AgentXP card and sync eligible proof to the hosted leaderboard |
|
|
141
194
|
| `atris codex-goal` | Inspect or clear a completed native Codex thread goal after writing a backup, row dump, and receipt |
|
|
142
195
|
| `atris learn` | Manage structured learnings |
|
|
143
196
|
| `atris ingest` | Stage raw evidence into `atris/context/` and compile into `atris/wiki/` |
|
package/ax
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
|
|
6
|
+
const EXIT_WORDS = new Set(['exit', 'quit', ':q']);
|
|
7
|
+
const BACKEND = {
|
|
8
|
+
host: '127.0.0.1',
|
|
9
|
+
port: 8000,
|
|
10
|
+
path: '/api/atris2/turn'
|
|
11
|
+
};
|
|
12
|
+
const ANSI = {
|
|
13
|
+
reset: '\x1b[0m',
|
|
14
|
+
bold: '\x1b[1m',
|
|
15
|
+
dim: '\x1b[2m',
|
|
16
|
+
muted: '\x1b[90m',
|
|
17
|
+
accent: '\x1b[36m',
|
|
18
|
+
ok: '\x1b[32m'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function modelForMode(mode) {
|
|
22
|
+
return mode === 'fast' ? 'atris:fast' : 'atris:pro';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatDuration(ms) {
|
|
26
|
+
const value = Number(ms) || 0;
|
|
27
|
+
if (value < 1000) return `${Math.max(0, Math.round(value))}ms`;
|
|
28
|
+
const totalSeconds = Math.max(1, Math.round(value / 1000));
|
|
29
|
+
return formatSeconds(totalSeconds);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatSeconds(totalSeconds) {
|
|
33
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
34
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
35
|
+
const seconds = totalSeconds % 60;
|
|
36
|
+
return seconds ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatHeader({ mode = 'pro', cwd = process.cwd(), chat = false } = {}) {
|
|
40
|
+
const label = mode === 'fast' ? 'Atris 2 Fast' : 'Atris 2 Pro';
|
|
41
|
+
return [
|
|
42
|
+
`${label}${chat ? ' chat' : ''} (${modelForMode(mode)})`,
|
|
43
|
+
cwd,
|
|
44
|
+
chat ? 'exit with exit, quit, or :q' : '',
|
|
45
|
+
].filter(Boolean).join('\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatUsage() {
|
|
49
|
+
return [
|
|
50
|
+
'ax - Atris 2 local coding agent',
|
|
51
|
+
'',
|
|
52
|
+
'Usage:',
|
|
53
|
+
' ax [--pro|--fast] <message>',
|
|
54
|
+
' ax [--pro|--fast] --chat',
|
|
55
|
+
' ax [--pro|--fast] --doctor',
|
|
56
|
+
'',
|
|
57
|
+
'Modes:',
|
|
58
|
+
' --pro local workspace agent, deeper tool loop',
|
|
59
|
+
' --fast local workspace agent, faster low-latency turns',
|
|
60
|
+
'',
|
|
61
|
+
'Examples:',
|
|
62
|
+
' ax --pro find the config file and explain it',
|
|
63
|
+
' ax --fast what files are here',
|
|
64
|
+
' ax --pro --chat',
|
|
65
|
+
].join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function backendUrl() {
|
|
69
|
+
return `http://${BACKEND.host}:${BACKEND.port}${BACKEND.path}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildRunProfile(options = {}) {
|
|
73
|
+
const mode = options.mode === 'fast' ? 'fast' : 'pro';
|
|
74
|
+
const cwd = options.cwd || process.cwd();
|
|
75
|
+
const payload = buildPayload(options.message || 'doctor', { mode, cwd });
|
|
76
|
+
return {
|
|
77
|
+
endpoint: backendUrl(),
|
|
78
|
+
mode,
|
|
79
|
+
model: payload.model,
|
|
80
|
+
workspace_path: payload.workspace_path,
|
|
81
|
+
max_turns: payload.max_turns,
|
|
82
|
+
streaming: true,
|
|
83
|
+
runtime: 'local workspace',
|
|
84
|
+
reasoning: mode === 'pro'
|
|
85
|
+
? 'backend reports run row; Pro workspace tool loop uses API default medium'
|
|
86
|
+
: 'backend reports run row; Fast workspace tool loop uses provider default'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatRunProfile(profile, options = {}) {
|
|
91
|
+
const rows = [
|
|
92
|
+
['mode', `${profile.mode} (${profile.model})`],
|
|
93
|
+
['endpoint', profile.endpoint],
|
|
94
|
+
['workspace', formatPathSubject(profile.workspace_path, options)],
|
|
95
|
+
['turns', String(profile.max_turns)],
|
|
96
|
+
['streaming', profile.streaming ? 'yes' : 'no'],
|
|
97
|
+
['runtime', profile.runtime],
|
|
98
|
+
['thinking', profile.reasoning],
|
|
99
|
+
];
|
|
100
|
+
return rows.map(([label, value]) => formatAuxRow(label, value, options)).join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatPrompt() {
|
|
104
|
+
return '› ';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function formatWorkingLine(ms) {
|
|
108
|
+
const totalSeconds = Math.max(1, Math.round((Number(ms) || 0) / 1000));
|
|
109
|
+
return `• Working (${formatSeconds(totalSeconds)} • ctrl-c to interrupt)`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatDoneLine(ms) {
|
|
113
|
+
return `— Worked for ${formatDuration(ms)} —`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function useColor(options = {}) {
|
|
117
|
+
if (options.color === false) return false;
|
|
118
|
+
if (process.env.NO_COLOR) return false;
|
|
119
|
+
return Boolean(options.color || options.isTTY);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function paint(text, codes, options = {}) {
|
|
123
|
+
if (!useColor(options)) return String(text);
|
|
124
|
+
return `${codes.join('')}${text}${ANSI.reset}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function truncateMiddle(value, limit = 120) {
|
|
128
|
+
const text = String(value || '');
|
|
129
|
+
if (text.length <= limit) return text;
|
|
130
|
+
const head = Math.max(8, Math.floor((limit - 3) * 0.6));
|
|
131
|
+
const tail = Math.max(8, limit - 3 - head);
|
|
132
|
+
return `${text.slice(0, head)}...${text.slice(-tail)}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatPathSubject(value, options = {}) {
|
|
136
|
+
const raw = truncateMiddle(String(value || '').trim(), options.limit || 120);
|
|
137
|
+
if (!raw) return '';
|
|
138
|
+
if (raw === '.' || raw === '/' || raw.endsWith('/')) {
|
|
139
|
+
return paint(raw, [ANSI.accent], options);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const slash = raw.lastIndexOf('/');
|
|
143
|
+
if (slash === -1) return paint(raw, [ANSI.bold, ANSI.accent], options);
|
|
144
|
+
|
|
145
|
+
const parent = raw.slice(0, slash + 1);
|
|
146
|
+
const filename = raw.slice(slash + 1);
|
|
147
|
+
return `${paint(parent, [ANSI.muted], options)} ${paint(filename, [ANSI.bold, ANSI.accent], options)}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatAuxRow(label, value, options = {}) {
|
|
151
|
+
const tag = String(label || '').padEnd(4);
|
|
152
|
+
const color = label === 'ok' ? [ANSI.ok] : [ANSI.muted];
|
|
153
|
+
return ` ${paint(tag, color, options)} ${value}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function createProgressReporter(output, options = {}) {
|
|
157
|
+
const enabled = options.showProgress !== false;
|
|
158
|
+
const isTty = Boolean(output && output.isTTY);
|
|
159
|
+
const startedAt = Date.now();
|
|
160
|
+
let interval = null;
|
|
161
|
+
let timeout = null;
|
|
162
|
+
let shown = false;
|
|
163
|
+
let closed = false;
|
|
164
|
+
|
|
165
|
+
const render = () => {
|
|
166
|
+
if (!enabled || closed) return;
|
|
167
|
+
const line = formatWorkingLine(Date.now() - startedAt);
|
|
168
|
+
if (isTty) {
|
|
169
|
+
output.write(`\r${line}\x1b[K`);
|
|
170
|
+
} else if (!shown) {
|
|
171
|
+
output.write(`${line}\n`);
|
|
172
|
+
}
|
|
173
|
+
shown = true;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
start() {
|
|
178
|
+
if (!enabled) return;
|
|
179
|
+
timeout = setTimeout(() => {
|
|
180
|
+
render();
|
|
181
|
+
if (isTty) interval = setInterval(render, 1000);
|
|
182
|
+
}, 700);
|
|
183
|
+
},
|
|
184
|
+
clear() {
|
|
185
|
+
if (!isTty || !shown || closed) return;
|
|
186
|
+
output.write('\r\x1b[K');
|
|
187
|
+
shown = false;
|
|
188
|
+
},
|
|
189
|
+
stop() {
|
|
190
|
+
closed = true;
|
|
191
|
+
clearTimeout(timeout);
|
|
192
|
+
clearInterval(interval);
|
|
193
|
+
if (isTty && shown) output.write('\r\x1b[K');
|
|
194
|
+
shown = false;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseSseBlock(block) {
|
|
200
|
+
const data = String(block || '')
|
|
201
|
+
.split(/\r?\n/)
|
|
202
|
+
.filter(line => line.startsWith('data:'))
|
|
203
|
+
.map(line => line.replace(/^data:\s?/, ''))
|
|
204
|
+
.join('\n')
|
|
205
|
+
.trim();
|
|
206
|
+
|
|
207
|
+
if (!data || data === '[DONE]') return null;
|
|
208
|
+
return JSON.parse(data);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function summarizeToolInput(block) {
|
|
212
|
+
const options = arguments[1] || {};
|
|
213
|
+
const tool = block.tool || 'tool';
|
|
214
|
+
const input = block.input || {};
|
|
215
|
+
const toolName = paint(tool, [ANSI.bold], options);
|
|
216
|
+
const pathValue = input.file_path || input.path;
|
|
217
|
+
if (input.command) return `${toolName} ${truncateMiddle(input.command, 120)}`;
|
|
218
|
+
if (input.pattern || input.query) {
|
|
219
|
+
const pattern = truncateMiddle(input.pattern || input.query, 80);
|
|
220
|
+
return pathValue
|
|
221
|
+
? `${toolName} ${pattern} in ${formatPathSubject(pathValue, options)}`
|
|
222
|
+
: `${toolName} ${pattern}`;
|
|
223
|
+
}
|
|
224
|
+
if (pathValue) return `${toolName} ${formatPathSubject(pathValue, options)}`;
|
|
225
|
+
const subject = input.type || '';
|
|
226
|
+
return subject ? `${toolName} ${String(subject).slice(0, 120)}` : toolName;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function summarizeToolResult(result) {
|
|
230
|
+
const options = arguments[1] || {};
|
|
231
|
+
const content = String(result.content || '');
|
|
232
|
+
if (!content) return 'done';
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(content);
|
|
236
|
+
if (parsed.error) return `error: ${parsed.error}`;
|
|
237
|
+
if (Array.isArray(parsed.matches)) {
|
|
238
|
+
const where = parsed.path ? ` in ${formatPathSubject(parsed.path, options)}` : '';
|
|
239
|
+
return `${parsed.matches.length} matches${where}`;
|
|
240
|
+
}
|
|
241
|
+
if (Array.isArray(parsed.files) || Array.isArray(parsed.dirs)) {
|
|
242
|
+
const parts = [];
|
|
243
|
+
if (Array.isArray(parsed.files)) parts.push(`${parsed.files.length} files`);
|
|
244
|
+
if (Array.isArray(parsed.dirs)) parts.push(`${parsed.dirs.length} dirs`);
|
|
245
|
+
const where = parsed.path ? ` in ${formatPathSubject(parsed.path, options)}` : '';
|
|
246
|
+
return `${parts.join(' / ')}${where}`;
|
|
247
|
+
}
|
|
248
|
+
if (parsed.path && parsed.status) return `${parsed.status} ${formatPathSubject(parsed.path, options)}`;
|
|
249
|
+
if (parsed.path) return formatPathSubject(parsed.path, options);
|
|
250
|
+
if (parsed.status) return parsed.status;
|
|
251
|
+
} catch (_) {
|
|
252
|
+
// Bash output and older streams can be plain text.
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return content.length > 120 ? `${content.slice(0, 117)}...` : content;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function formatStatusMessage(message) {
|
|
259
|
+
const text = String(message || '').trim();
|
|
260
|
+
if (!text || text === 'complete') return null;
|
|
261
|
+
if (text === 'retrying_with_required_local_tool') return null;
|
|
262
|
+
return text.replace(/_/g, ' ');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function formatSystemInit(event, options = {}) {
|
|
266
|
+
const runtime = event.tool_runtime || {};
|
|
267
|
+
const model = runtime.tool_model || runtime.chat_model || event.model || '';
|
|
268
|
+
const mode = runtime.mode ? String(runtime.mode).replace(/_/g, ' ') : '';
|
|
269
|
+
const thinking = runtime.reasoning_effort ? `thinking ${runtime.reasoning_effort}` : '';
|
|
270
|
+
const parts = [mode || 'runtime', model, thinking].filter(Boolean);
|
|
271
|
+
return parts.length ? parts.join(' ') : null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function clearRetriedText(state) {
|
|
275
|
+
state.pendingText = '';
|
|
276
|
+
state.output = '';
|
|
277
|
+
state.wroteText = false;
|
|
278
|
+
state.lastChar = '\n';
|
|
279
|
+
state.inAuxBlock = false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function stopProgress(state) {
|
|
283
|
+
if (!state.progress) return;
|
|
284
|
+
state.progress.stop();
|
|
285
|
+
state.progress = null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function flushPendingText(state, output) {
|
|
289
|
+
if (!state.pendingText) return;
|
|
290
|
+
stopProgress(state);
|
|
291
|
+
if (state.inAuxBlock && state.lastChar === '\n') output.write('\n');
|
|
292
|
+
output.write(state.pendingText);
|
|
293
|
+
state.wroteText = true;
|
|
294
|
+
state.wroteActivity = true;
|
|
295
|
+
state.lastChar = state.pendingText.slice(-1);
|
|
296
|
+
state.pendingText = '';
|
|
297
|
+
state.inAuxBlock = false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function writeStreamingText(state, output, content) {
|
|
301
|
+
if (!content) return;
|
|
302
|
+
stopProgress(state);
|
|
303
|
+
if (state.inAuxBlock && state.lastChar === '\n') output.write('\n');
|
|
304
|
+
output.write(content);
|
|
305
|
+
state.wroteText = true;
|
|
306
|
+
state.wroteActivity = true;
|
|
307
|
+
state.lastChar = String(content).slice(-1);
|
|
308
|
+
state.inAuxBlock = false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function writeAuxLine(state, output, line) {
|
|
312
|
+
if (!line) return;
|
|
313
|
+
stopProgress(state);
|
|
314
|
+
if (state.wroteText && state.lastChar !== '\n') output.write('\n');
|
|
315
|
+
if (!state.inAuxBlock && state.wroteActivity && state.lastChar === '\n') output.write('\n');
|
|
316
|
+
output.write(`${line}\n`);
|
|
317
|
+
state.wroteActivity = true;
|
|
318
|
+
state.lastChar = '\n';
|
|
319
|
+
state.inAuxBlock = true;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function compactHistory(history, limit = 8) {
|
|
323
|
+
return history
|
|
324
|
+
.slice(-limit)
|
|
325
|
+
.map(turn => `${turn.role}: ${String(turn.content || '').slice(0, 1200)}`)
|
|
326
|
+
.join('\n');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function buildMessage(message, history = []) {
|
|
330
|
+
const trimmed = String(message || '').trim();
|
|
331
|
+
if (!history.length) return trimmed;
|
|
332
|
+
return [
|
|
333
|
+
'Continue this terminal coding-agent conversation in the same workspace.',
|
|
334
|
+
'Use local tools when the user asks about files, code, tests, or edits.',
|
|
335
|
+
'',
|
|
336
|
+
'Recent conversation:',
|
|
337
|
+
compactHistory(history),
|
|
338
|
+
'',
|
|
339
|
+
`Current user message: ${trimmed}`,
|
|
340
|
+
].join('\n');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function buildPayload(message, options = {}) {
|
|
344
|
+
const mode = options.mode === 'fast' ? 'fast' : 'pro';
|
|
345
|
+
return {
|
|
346
|
+
message: buildMessage(message, options.history || []),
|
|
347
|
+
workspace_path: options.cwd || process.cwd(),
|
|
348
|
+
model: modelForMode(mode),
|
|
349
|
+
max_turns: mode === 'pro' ? 14 : 6,
|
|
350
|
+
verify_command: 'true'
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function handleEvent(event, state, output) {
|
|
355
|
+
if (!event || typeof event !== 'object') return;
|
|
356
|
+
state.events.push(event);
|
|
357
|
+
|
|
358
|
+
if (event.type === 'system_init') {
|
|
359
|
+
state.runtime = event;
|
|
360
|
+
writeAuxLine(state, output, formatAuxRow('run', formatSystemInit(event, output), output));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if ((event.type === 'text_delta' || event.type === 'text') && event.content) {
|
|
365
|
+
state.output += event.content;
|
|
366
|
+
if (output && output.isTTY) writeStreamingText(state, output, event.content);
|
|
367
|
+
else state.pendingText += event.content;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (event.type === 'assistant_blocks' && Array.isArray(event.blocks)) {
|
|
372
|
+
for (const block of event.blocks) {
|
|
373
|
+
if (block && block.type === 'tool_use') {
|
|
374
|
+
flushPendingText(state, output);
|
|
375
|
+
writeAuxLine(state, output, formatAuxRow('tool', summarizeToolInput(block, output), output));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (event.type === 'tool_results' && Array.isArray(event.results)) {
|
|
382
|
+
for (const result of event.results) {
|
|
383
|
+
flushPendingText(state, output);
|
|
384
|
+
writeAuxLine(state, output, formatAuxRow('ok', summarizeToolResult(result, output), output));
|
|
385
|
+
}
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (event.type === 'status' && event.message && event.message !== 'complete') {
|
|
390
|
+
if (event.message === 'retrying_with_required_local_tool') {
|
|
391
|
+
clearRetriedText(state);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
flushPendingText(state, output);
|
|
395
|
+
writeAuxLine(state, output, formatAuxRow('info', formatStatusMessage(event.message), output));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (event.type === 'error' || event.error) {
|
|
400
|
+
state.errors.push(event.error || event.message || 'Atris2 stream error');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function postTurn(message, options = {}) {
|
|
405
|
+
const payload = buildPayload(message, options);
|
|
406
|
+
const postData = JSON.stringify(payload);
|
|
407
|
+
const output = options.output || process.stdout;
|
|
408
|
+
const timeoutMs = payload.model === 'atris:pro' ? 180000 : 60000;
|
|
409
|
+
const state = {
|
|
410
|
+
events: [],
|
|
411
|
+
errors: [],
|
|
412
|
+
output: '',
|
|
413
|
+
pendingText: '',
|
|
414
|
+
wroteText: false,
|
|
415
|
+
wroteActivity: false,
|
|
416
|
+
durationMs: 0,
|
|
417
|
+
lastChar: '\n',
|
|
418
|
+
progress: null,
|
|
419
|
+
inAuxBlock: false
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
return new Promise((resolve, reject) => {
|
|
423
|
+
let settled = false;
|
|
424
|
+
const startedAt = Date.now();
|
|
425
|
+
state.progress = createProgressReporter(output, options);
|
|
426
|
+
state.progress.start();
|
|
427
|
+
|
|
428
|
+
const finish = (error, value) => {
|
|
429
|
+
if (settled) return;
|
|
430
|
+
settled = true;
|
|
431
|
+
if (state.progress) state.progress.stop();
|
|
432
|
+
state.progress = null;
|
|
433
|
+
state.durationMs = Date.now() - startedAt;
|
|
434
|
+
if (error) reject(error);
|
|
435
|
+
else resolve(value);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const req = http.request({
|
|
439
|
+
hostname: BACKEND.host,
|
|
440
|
+
port: BACKEND.port,
|
|
441
|
+
path: BACKEND.path,
|
|
442
|
+
method: 'POST',
|
|
443
|
+
headers: {
|
|
444
|
+
'Content-Type': 'application/json',
|
|
445
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
446
|
+
Accept: 'text/event-stream'
|
|
447
|
+
}
|
|
448
|
+
}, (res) => {
|
|
449
|
+
res.setEncoding('utf8');
|
|
450
|
+
let buffer = '';
|
|
451
|
+
let rawBody = '';
|
|
452
|
+
|
|
453
|
+
res.on('data', (chunk) => {
|
|
454
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
455
|
+
rawBody += chunk;
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
buffer += chunk;
|
|
460
|
+
let boundary = buffer.indexOf('\n\n');
|
|
461
|
+
while (boundary !== -1) {
|
|
462
|
+
const block = buffer.slice(0, boundary);
|
|
463
|
+
buffer = buffer.slice(boundary + 2);
|
|
464
|
+
try {
|
|
465
|
+
handleEvent(parseSseBlock(block), state, output);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
state.errors.push(`bad_sse_event: ${error.message}`);
|
|
468
|
+
}
|
|
469
|
+
boundary = buffer.indexOf('\n\n');
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
res.on('end', () => {
|
|
474
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
475
|
+
try {
|
|
476
|
+
const parsed = JSON.parse(rawBody);
|
|
477
|
+
finish(new Error(parsed.detail || parsed.error || `HTTP ${res.statusCode}`));
|
|
478
|
+
} catch (_) {
|
|
479
|
+
finish(new Error(rawBody || `HTTP ${res.statusCode}`));
|
|
480
|
+
}
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (buffer.trim()) {
|
|
485
|
+
try {
|
|
486
|
+
handleEvent(parseSseBlock(buffer), state, output);
|
|
487
|
+
} catch (error) {
|
|
488
|
+
state.errors.push(`bad_sse_event: ${error.message}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (state.errors.length) {
|
|
493
|
+
finish(new Error(state.errors.join('; ')));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
flushPendingText(state, output);
|
|
498
|
+
finish(null, state);
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
req.on('error', finish);
|
|
503
|
+
req.setTimeout(timeoutMs, () => {
|
|
504
|
+
finish(new Error(`Request timeout after ${timeoutMs / 1000}s`));
|
|
505
|
+
req.destroy();
|
|
506
|
+
});
|
|
507
|
+
req.write(postData);
|
|
508
|
+
req.end();
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function chat(options = {}) {
|
|
513
|
+
const mode = options.mode === 'fast' ? 'fast' : 'pro';
|
|
514
|
+
const cwd = options.cwd || process.cwd();
|
|
515
|
+
const input = options.input || process.stdin;
|
|
516
|
+
const output = options.output || process.stdout;
|
|
517
|
+
const history = [];
|
|
518
|
+
|
|
519
|
+
output.write(`${formatHeader({ mode, cwd, chat: true })}\n\n`);
|
|
520
|
+
|
|
521
|
+
const runLine = async (line) => {
|
|
522
|
+
const trimmed = String(line || '').trim();
|
|
523
|
+
if (!trimmed) return false;
|
|
524
|
+
if (EXIT_WORDS.has(trimmed.toLowerCase())) return true;
|
|
525
|
+
|
|
526
|
+
output.write('\n');
|
|
527
|
+
const result = await postTurn(trimmed, { mode, cwd, history, output });
|
|
528
|
+
if (result.output && !result.output.endsWith('\n')) output.write('\n');
|
|
529
|
+
output.write(`${formatDoneLine(result.durationMs)}\n\n`);
|
|
530
|
+
history.push({ role: 'user', content: trimmed });
|
|
531
|
+
history.push({ role: 'assistant', content: result.output || '' });
|
|
532
|
+
return false;
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
if (!input.isTTY) {
|
|
536
|
+
const rl = readline.createInterface({ input, crlfDelay: Infinity });
|
|
537
|
+
for await (const line of rl) {
|
|
538
|
+
if (await runLine(line)) break;
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const rl = readline.createInterface({ input, output });
|
|
544
|
+
const ask = () => new Promise(resolve => rl.question(formatPrompt(mode), resolve));
|
|
545
|
+
while (true) {
|
|
546
|
+
const line = await ask();
|
|
547
|
+
if (await runLine(line)) {
|
|
548
|
+
rl.close();
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function printBackendHint() {
|
|
555
|
+
console.log('');
|
|
556
|
+
console.log('Start backend:');
|
|
557
|
+
console.log(`cd /Users/keshavrao/arena/atrisos-backend/backend && uvicorn main:app --host ${BACKEND.host} --port ${BACKEND.port}`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function main() {
|
|
561
|
+
const args = process.argv.slice(2);
|
|
562
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
563
|
+
console.log(formatUsage());
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const mode = args.includes('--fast') ? 'fast' : 'pro';
|
|
568
|
+
const doctor = args.includes('--doctor');
|
|
569
|
+
const prompt = args
|
|
570
|
+
.filter(arg => !['--fast', '--pro', '--chat', '--doctor', '--help', '-h'].includes(arg))
|
|
571
|
+
.join(' ')
|
|
572
|
+
.trim();
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
if (doctor) {
|
|
576
|
+
console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }));
|
|
577
|
+
console.log('');
|
|
578
|
+
console.log(formatRunProfile(buildRunProfile({ mode, cwd: process.cwd() }), process.stdout));
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (!prompt || args.includes('--chat')) {
|
|
583
|
+
await chat({ mode, cwd: process.cwd() });
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }));
|
|
588
|
+
console.log('');
|
|
589
|
+
const result = await postTurn(prompt, { mode, cwd: process.cwd() });
|
|
590
|
+
console.log('');
|
|
591
|
+
console.log(formatDoneLine(result.durationMs));
|
|
592
|
+
} catch (error) {
|
|
593
|
+
console.error(`x ${error.message}`);
|
|
594
|
+
printBackendHint();
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (require.main === module) {
|
|
600
|
+
main();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
module.exports = {
|
|
604
|
+
backendUrl,
|
|
605
|
+
buildPayload,
|
|
606
|
+
buildRunProfile,
|
|
607
|
+
chat,
|
|
608
|
+
createProgressReporter,
|
|
609
|
+
formatDoneLine,
|
|
610
|
+
formatDuration,
|
|
611
|
+
formatHeader,
|
|
612
|
+
formatPathSubject,
|
|
613
|
+
formatPrompt,
|
|
614
|
+
formatRunProfile,
|
|
615
|
+
formatStatusMessage,
|
|
616
|
+
formatSystemInit,
|
|
617
|
+
formatUsage,
|
|
618
|
+
formatWorkingLine,
|
|
619
|
+
handleEvent,
|
|
620
|
+
modelForMode,
|
|
621
|
+
parseSseBlock,
|
|
622
|
+
postTurn,
|
|
623
|
+
summarizeToolInput,
|
|
624
|
+
summarizeToolResult
|
|
625
|
+
};
|