atris 3.15.31 → 3.15.36
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/ax +479 -21
- package/bin/atris.js +1 -1
- package/commands/aeo.js +377 -13
- package/commands/business.js +21 -2
- package/commands/computer.js +346 -16
- package/commands/gm.js +3 -3
- package/commands/lifecycle.js +115 -0
- package/commands/mission.js +9 -3
- package/commands/play.js +3 -3
- package/commands/xp.js +295 -2
- package/lib/runtime-bootstrap.js +107 -0
- package/package.json +1 -1
package/ax
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
3
4
|
const http = require('http');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
4
8
|
const readline = require('readline');
|
|
9
|
+
const { loadCredentials } = require('./utils/auth');
|
|
5
10
|
|
|
6
11
|
const EXIT_WORDS = new Set(['exit', 'quit', ':q']);
|
|
7
12
|
const BACKEND = {
|
|
@@ -9,6 +14,39 @@ const BACKEND = {
|
|
|
9
14
|
port: 8000,
|
|
10
15
|
path: '/api/atris2/turn'
|
|
11
16
|
};
|
|
17
|
+
const DEFAULT_BACKEND_BASE = `http://${BACKEND.host}:${BACKEND.port}`;
|
|
18
|
+
const CONNECTION_STATUS_PATH = '/api/integrations/status';
|
|
19
|
+
const CONNECTION_CAPABILITIES_PATH = '/api/atris2/connection-capabilities';
|
|
20
|
+
const ATRIS2_CONNECTION_STATUS_PATH = '/api/atris2/connection-status';
|
|
21
|
+
const CONNECTOR_NAMES = {
|
|
22
|
+
gmail: 'Gmail',
|
|
23
|
+
google_calendar: 'Google Calendar',
|
|
24
|
+
google_drive: 'Google Drive',
|
|
25
|
+
google_docs: 'Google Docs',
|
|
26
|
+
github: 'GitHub',
|
|
27
|
+
slack: 'Slack',
|
|
28
|
+
linear: 'Linear',
|
|
29
|
+
notion: 'Notion',
|
|
30
|
+
hubspot: 'HubSpot',
|
|
31
|
+
twitter: 'Twitter',
|
|
32
|
+
microsoft: 'Microsoft',
|
|
33
|
+
discord: 'Discord',
|
|
34
|
+
jira: 'Jira',
|
|
35
|
+
linkedin: 'LinkedIn',
|
|
36
|
+
whatsapp: 'WhatsApp',
|
|
37
|
+
salesforce: 'Salesforce'
|
|
38
|
+
};
|
|
39
|
+
const CONNECTOR_SCOPES = {
|
|
40
|
+
gmail: ['mail'],
|
|
41
|
+
google_calendar: ['calendar'],
|
|
42
|
+
google_drive: ['drive'],
|
|
43
|
+
google_docs: ['docs'],
|
|
44
|
+
github: ['repos', 'issues', 'pull_requests'],
|
|
45
|
+
slack: ['messages'],
|
|
46
|
+
linear: ['issues'],
|
|
47
|
+
notion: ['pages', 'databases'],
|
|
48
|
+
hubspot: ['crm']
|
|
49
|
+
};
|
|
12
50
|
const ANSI = {
|
|
13
51
|
reset: '\x1b[0m',
|
|
14
52
|
bold: '\x1b[1m',
|
|
@@ -50,37 +88,54 @@ function formatUsage() {
|
|
|
50
88
|
'ax - Atris 2 local coding agent',
|
|
51
89
|
'',
|
|
52
90
|
'Usage:',
|
|
53
|
-
' ax [--pro|--fast] <message>',
|
|
54
|
-
' ax [--pro|--fast] --chat',
|
|
91
|
+
' ax [--pro|--fast] [--local|--cloud] <message>',
|
|
92
|
+
' ax [--pro|--fast] [--local|--cloud] --chat',
|
|
55
93
|
' ax [--pro|--fast] --doctor',
|
|
94
|
+
' ax [--fast] --benchmark',
|
|
56
95
|
'',
|
|
57
96
|
'Modes:',
|
|
58
97
|
' --pro local workspace agent, deeper tool loop',
|
|
59
98
|
' --fast local workspace agent, faster low-latency turns',
|
|
99
|
+
' --local force local workspace tools',
|
|
100
|
+
' --cloud force authenticated cloud connectors/chat',
|
|
60
101
|
'',
|
|
61
102
|
'Examples:',
|
|
62
103
|
' ax --pro find the config file and explain it',
|
|
63
104
|
' ax --fast what files are here',
|
|
105
|
+
' ax --fast what is on my calendar today',
|
|
64
106
|
' ax --pro --chat',
|
|
65
107
|
].join('\n');
|
|
66
108
|
}
|
|
67
109
|
|
|
110
|
+
function backendBaseUrl() {
|
|
111
|
+
return (process.env.AX_BACKEND_URL
|
|
112
|
+
|| process.env.OBELISK_LOCAL_ATRIS2_BACKEND_URL
|
|
113
|
+
|| process.env.OBELISK_ATRIS2_BACKEND_URL
|
|
114
|
+
|| DEFAULT_BACKEND_BASE).replace(/\/$/, '');
|
|
115
|
+
}
|
|
116
|
+
|
|
68
117
|
function backendUrl() {
|
|
69
|
-
return
|
|
118
|
+
return new URL(BACKEND.path, backendBaseUrl()).toString();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function backendPathUrl(pathname) {
|
|
122
|
+
return new URL(pathname, backendBaseUrl()).toString();
|
|
70
123
|
}
|
|
71
124
|
|
|
72
125
|
function buildRunProfile(options = {}) {
|
|
73
126
|
const mode = options.mode === 'fast' ? 'fast' : 'pro';
|
|
74
127
|
const cwd = options.cwd || process.cwd();
|
|
75
|
-
const
|
|
128
|
+
const route = resolveRoute(options.message || 'doctor', options);
|
|
129
|
+
const payload = buildPayload(options.message || 'doctor', { mode, cwd, route });
|
|
76
130
|
return {
|
|
77
131
|
endpoint: backendUrl(),
|
|
78
132
|
mode,
|
|
133
|
+
route,
|
|
79
134
|
model: payload.model,
|
|
80
|
-
workspace_path: payload.workspace_path,
|
|
135
|
+
workspace_path: payload.workspace_path || 'cloud',
|
|
81
136
|
max_turns: payload.max_turns,
|
|
82
137
|
streaming: true,
|
|
83
|
-
runtime: 'local workspace',
|
|
138
|
+
runtime: route === 'cloud' ? 'authenticated cloud connectors/chat' : 'local workspace',
|
|
84
139
|
reasoning: mode === 'pro'
|
|
85
140
|
? 'backend reports run row; Pro workspace tool loop uses API default medium'
|
|
86
141
|
: 'backend reports run row; Fast workspace tool loop uses provider default'
|
|
@@ -91,6 +146,7 @@ function formatRunProfile(profile, options = {}) {
|
|
|
91
146
|
const rows = [
|
|
92
147
|
['mode', `${profile.mode} (${profile.model})`],
|
|
93
148
|
['endpoint', profile.endpoint],
|
|
149
|
+
['route', profile.route || 'auto'],
|
|
94
150
|
['workspace', formatPathSubject(profile.workspace_path, options)],
|
|
95
151
|
['turns', String(profile.max_turns)],
|
|
96
152
|
['streaming', profile.streaming ? 'yes' : 'no'],
|
|
@@ -100,6 +156,196 @@ function formatRunProfile(profile, options = {}) {
|
|
|
100
156
|
return rows.map(([label, value]) => formatAuxRow(label, value, options)).join('\n');
|
|
101
157
|
}
|
|
102
158
|
|
|
159
|
+
function canonicalConnectorId(value) {
|
|
160
|
+
return String(value || '').trim().toLowerCase().replace(/-/g, '_');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function connectorDisplayName(id) {
|
|
164
|
+
const key = canonicalConnectorId(id);
|
|
165
|
+
if (CONNECTOR_NAMES[key]) return CONNECTOR_NAMES[key];
|
|
166
|
+
return key.split('_').map(part => part ? part[0].toUpperCase() + part.slice(1) : '').join(' ');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function authToken() {
|
|
170
|
+
if (process.env.OBELISK_AUTH_TOKEN) return process.env.OBELISK_AUTH_TOKEN;
|
|
171
|
+
if (process.env.ATRIS_TOKEN) return process.env.ATRIS_TOKEN;
|
|
172
|
+
try {
|
|
173
|
+
const creds = loadCredentials();
|
|
174
|
+
return creds && creds.token ? creds.token : '';
|
|
175
|
+
} catch (_) {
|
|
176
|
+
return '';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function authUserId() {
|
|
181
|
+
if (process.env.OBELISK_USER_ID) return process.env.OBELISK_USER_ID;
|
|
182
|
+
if (process.env.ATRIS_USER_ID) return process.env.ATRIS_USER_ID;
|
|
183
|
+
try {
|
|
184
|
+
const creds = loadCredentials();
|
|
185
|
+
return creds && creds.user_id ? creds.user_id : '';
|
|
186
|
+
} catch (_) {
|
|
187
|
+
return '';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isLoopbackBackend() {
|
|
192
|
+
try {
|
|
193
|
+
const parsed = new URL(backendBaseUrl());
|
|
194
|
+
return ['127.0.0.1', 'localhost', '::1'].includes(parsed.hostname);
|
|
195
|
+
} catch (_) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function requestJson(pathname, { token = authToken(), timeoutMs = 6000 } = {}) {
|
|
201
|
+
const url = backendPathUrl(pathname);
|
|
202
|
+
const parsed = new URL(url);
|
|
203
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
204
|
+
return new Promise((resolve) => {
|
|
205
|
+
const req = transport.request({
|
|
206
|
+
method: 'GET',
|
|
207
|
+
hostname: parsed.hostname,
|
|
208
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
209
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
210
|
+
headers: {
|
|
211
|
+
Accept: 'application/json',
|
|
212
|
+
...(token ? { Authorization: `Bearer ${token}` } : {})
|
|
213
|
+
}
|
|
214
|
+
}, (res) => {
|
|
215
|
+
const chunks = [];
|
|
216
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
217
|
+
res.on('end', () => {
|
|
218
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
219
|
+
try {
|
|
220
|
+
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, data: text ? JSON.parse(text) : null, text });
|
|
221
|
+
} catch (_) {
|
|
222
|
+
resolve({ ok: false, status: res.statusCode, data: null, text });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
req.on('error', error => resolve({ ok: false, status: 0, data: null, text: '', error: error.message }));
|
|
227
|
+
req.setTimeout(timeoutMs, () => {
|
|
228
|
+
req.destroy();
|
|
229
|
+
resolve({ ok: false, status: 0, data: null, text: '', error: `timeout after ${timeoutMs}ms` });
|
|
230
|
+
});
|
|
231
|
+
req.end();
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function defaultAuthority(id) {
|
|
236
|
+
const key = canonicalConnectorId(id);
|
|
237
|
+
if (key === 'gmail') return { list_messages: 'read_only', get_message: 'read_only', send_message: 'approval_required' };
|
|
238
|
+
if (key === 'google_calendar') return { list_events: 'read_only', get_event: 'read_only', create_event: 'approval_required' };
|
|
239
|
+
if (key === 'google_drive') return { list_files: 'read_only', search_files: 'read_only', download_file: 'read_only', upload_file: 'approval_required' };
|
|
240
|
+
if (key === 'google_docs') return { list_documents: 'read_only', get_document: 'read_only', create_document: 'approval_required' };
|
|
241
|
+
if (key === 'github') return { list_repos: 'read_only', get_repo: 'read_only', list_issues: 'read_only', create_issue: 'approval_required' };
|
|
242
|
+
if (key === 'slack') return { list_channels: 'read_only', search_messages: 'read_only', post_message: 'approval_required' };
|
|
243
|
+
if (key === 'linear') return { list_issues: 'read_only', get_issue: 'read_only', create_issue: 'approval_required' };
|
|
244
|
+
if (key === 'notion') return { search: 'read_only', get_page: 'read_only', create_page: 'approval_required' };
|
|
245
|
+
if (key === 'hubspot') return { list_contacts: 'read_only', search_contacts: 'read_only', create_contact: 'approval_required' };
|
|
246
|
+
return { inspect_status: 'read_only' };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function connectionFromStatus(id, connected, contractConnector = null) {
|
|
250
|
+
const key = canonicalConnectorId(id);
|
|
251
|
+
const authority = contractConnector && contractConnector.authority && typeof contractConnector.authority === 'object'
|
|
252
|
+
? contractConnector.authority
|
|
253
|
+
: defaultAuthority(key);
|
|
254
|
+
return {
|
|
255
|
+
id: key,
|
|
256
|
+
name: connectorDisplayName(key),
|
|
257
|
+
connected: connected === true,
|
|
258
|
+
local: false,
|
|
259
|
+
actions: Object.keys(authority),
|
|
260
|
+
scopes: CONNECTOR_SCOPES[key] || [],
|
|
261
|
+
authority,
|
|
262
|
+
source: 'ax_integrations_status'
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function statusConnected(statuses, id) {
|
|
267
|
+
const keys = [id, id.replace(/_/g, '-'), canonicalConnectorId(id)];
|
|
268
|
+
for (const key of keys) {
|
|
269
|
+
const value = statuses[key];
|
|
270
|
+
if (value === true) return true;
|
|
271
|
+
if (value && typeof value === 'object') {
|
|
272
|
+
if (value.connected === true) return true;
|
|
273
|
+
if (String(value.status || '').toLowerCase() === 'connected') return true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function buildConnectionContext(options = {}) {
|
|
280
|
+
const token = options.token || authToken();
|
|
281
|
+
const localStatusUserId = isLoopbackBackend() ? authUserId() : '';
|
|
282
|
+
const statusPath = localStatusUserId
|
|
283
|
+
? `${ATRIS2_CONNECTION_STATUS_PATH}?connection_user_id=${encodeURIComponent(localStatusUserId)}`
|
|
284
|
+
: CONNECTION_STATUS_PATH;
|
|
285
|
+
const [statusRes, contractRes] = await Promise.all([
|
|
286
|
+
localStatusUserId || token ? requestJson(statusPath, { token: localStatusUserId ? '' : token }) : Promise.resolve({ ok: false, data: null }),
|
|
287
|
+
requestJson(CONNECTION_CAPABILITIES_PATH, { token: '' })
|
|
288
|
+
]);
|
|
289
|
+
const statusData = statusRes.ok && statusRes.data && typeof statusRes.data === 'object' ? statusRes.data : {};
|
|
290
|
+
const statuses = statusData.statuses && typeof statusData.statuses === 'object' ? statusData.statuses : statusData;
|
|
291
|
+
const contractConnectors = new Map();
|
|
292
|
+
if (contractRes.ok && contractRes.data && Array.isArray(contractRes.data.connectors)) {
|
|
293
|
+
for (const connector of contractRes.data.connectors) {
|
|
294
|
+
const key = canonicalConnectorId(connector.id);
|
|
295
|
+
if (key) contractConnectors.set(key, connector);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const ids = new Set([
|
|
299
|
+
...Object.keys(CONNECTOR_NAMES),
|
|
300
|
+
...Object.keys(statuses).map(canonicalConnectorId),
|
|
301
|
+
...contractConnectors.keys(),
|
|
302
|
+
]);
|
|
303
|
+
const connections = [...ids].sort().map(id => connectionFromStatus(
|
|
304
|
+
id,
|
|
305
|
+
statusConnected(statuses, id),
|
|
306
|
+
contractConnectors.get(id)
|
|
307
|
+
));
|
|
308
|
+
connections.push({
|
|
309
|
+
id: 'computer.local',
|
|
310
|
+
name: 'This Mac workspace',
|
|
311
|
+
connected: options.localWorkspace === true,
|
|
312
|
+
local: true,
|
|
313
|
+
status: options.localWorkspace === true ? 'available' : 'cloud_only',
|
|
314
|
+
actions: ['inspect_status', 'use_local_workspace'],
|
|
315
|
+
scopes: ['computer', 'workspace'],
|
|
316
|
+
authority: { inspect_status: 'read_only', use_local_workspace: 'read_only' },
|
|
317
|
+
source: 'ax'
|
|
318
|
+
});
|
|
319
|
+
return {
|
|
320
|
+
schema: 'atris.connection_capabilities.v1',
|
|
321
|
+
source: 'ax',
|
|
322
|
+
stale: false,
|
|
323
|
+
capability_contract_source: contractRes.ok ? 'backend' : 'fallback',
|
|
324
|
+
capability_contract_schema: contractRes.data && contractRes.data.schema,
|
|
325
|
+
capability_contract_connectors: contractConnectors.size || undefined,
|
|
326
|
+
connections
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function mentionsConnector(message) {
|
|
331
|
+
return /\b(gmail|email|mail|inbox|calendar|events?|meetings?|schedule|google drive|drive|docs?|sheets?|github|slack|linear|notion|hubspot|integrations?|connectors?|connections?|connected apps?|connected tools?)\b/i.test(message || '');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function connectorWriteIntent(message) {
|
|
335
|
+
return mentionsConnector(message) && /\b(send|post|dm|message|reply|draft|compose|schedule|book|create|update|delete|archive|move|share|comment|invite)\b/i.test(message || '');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function workspaceIntent(message) {
|
|
339
|
+
return /\b(files?|folders?|repo|workspace|project|directory|tree|read|open|inspect|search|grep|find|locate|where|edit|write|change|modify|patch|fix|test|tests?|build|src|source|code|diff|git|backend|frontend|atris task|atris xp|xp game|career xp|agentxp|todo|map)\b/i.test(message || '');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function resolveRoute(message, options = {}) {
|
|
343
|
+
if (options.route === 'local' || options.forceLocal) return 'local';
|
|
344
|
+
if (options.route === 'cloud' || options.forceCloud) return 'cloud';
|
|
345
|
+
if (mentionsConnector(message) && !workspaceIntent(message)) return 'cloud';
|
|
346
|
+
return 'local';
|
|
347
|
+
}
|
|
348
|
+
|
|
103
349
|
function formatPrompt() {
|
|
104
350
|
return '› ';
|
|
105
351
|
}
|
|
@@ -342,13 +588,29 @@ function buildMessage(message, history = []) {
|
|
|
342
588
|
|
|
343
589
|
function buildPayload(message, options = {}) {
|
|
344
590
|
const mode = options.mode === 'fast' ? 'fast' : 'pro';
|
|
345
|
-
|
|
591
|
+
const route = resolveRoute(message, options);
|
|
592
|
+
const local = route !== 'cloud';
|
|
593
|
+
const payload = {
|
|
346
594
|
message: buildMessage(message, options.history || []),
|
|
347
|
-
workspace_path: options.cwd || process.cwd(),
|
|
348
595
|
model: modelForMode(mode),
|
|
349
|
-
max_turns: mode === 'pro' ? 14 :
|
|
596
|
+
max_turns: local ? (mode === 'pro' ? 14 : 8) : 1,
|
|
350
597
|
verify_command: 'true'
|
|
351
598
|
};
|
|
599
|
+
if (local) {
|
|
600
|
+
payload.workspace_path = options.cwd || process.cwd();
|
|
601
|
+
}
|
|
602
|
+
if (options.connectionContext) {
|
|
603
|
+
payload.connection_context = options.connectionContext;
|
|
604
|
+
}
|
|
605
|
+
if (!local && options.connectionUserId) {
|
|
606
|
+
payload.connection_user_id = options.connectionUserId;
|
|
607
|
+
}
|
|
608
|
+
if (!local && connectorWriteIntent(message)) {
|
|
609
|
+
payload.allow_external_actions = true;
|
|
610
|
+
payload.cleanup_external_actions = true;
|
|
611
|
+
payload.max_turns = mode === 'pro' ? 4 : 2;
|
|
612
|
+
}
|
|
613
|
+
return payload;
|
|
352
614
|
}
|
|
353
615
|
|
|
354
616
|
function handleEvent(event, state, output) {
|
|
@@ -401,11 +663,24 @@ function handleEvent(event, state, output) {
|
|
|
401
663
|
}
|
|
402
664
|
}
|
|
403
665
|
|
|
404
|
-
function postTurn(message, options = {}) {
|
|
405
|
-
const
|
|
666
|
+
async function postTurn(message, options = {}) {
|
|
667
|
+
const route = resolveRoute(message, options);
|
|
668
|
+
const local = route !== 'cloud';
|
|
669
|
+
const token = authToken();
|
|
670
|
+
const shouldSendConnectionContext = options.connectionContext
|
|
671
|
+
|| route === 'cloud'
|
|
672
|
+
|| mentionsConnector(message)
|
|
673
|
+
|| /\b(can you use|can you access|connected|connections?|integrations?|tools?|capabilities)\b/i.test(message || '');
|
|
674
|
+
const connectionContext = options.connectionContext || (shouldSendConnectionContext
|
|
675
|
+
? await buildConnectionContext({ token, localWorkspace: local })
|
|
676
|
+
: null);
|
|
677
|
+
const connectionUserId = !local && isLoopbackBackend() ? authUserId() : '';
|
|
678
|
+
const payload = buildPayload(message, { ...options, route, connectionContext, connectionUserId });
|
|
406
679
|
const postData = JSON.stringify(payload);
|
|
407
680
|
const output = options.output || process.stdout;
|
|
408
681
|
const timeoutMs = payload.model === 'atris:pro' ? 180000 : 60000;
|
|
682
|
+
const turnUrl = new URL(backendUrl());
|
|
683
|
+
const transport = turnUrl.protocol === 'https:' ? https : http;
|
|
409
684
|
const state = {
|
|
410
685
|
events: [],
|
|
411
686
|
errors: [],
|
|
@@ -435,15 +710,16 @@ function postTurn(message, options = {}) {
|
|
|
435
710
|
else resolve(value);
|
|
436
711
|
};
|
|
437
712
|
|
|
438
|
-
const req =
|
|
439
|
-
hostname:
|
|
440
|
-
port:
|
|
441
|
-
path:
|
|
713
|
+
const req = transport.request({
|
|
714
|
+
hostname: turnUrl.hostname,
|
|
715
|
+
port: turnUrl.port || (turnUrl.protocol === 'https:' ? 443 : 80),
|
|
716
|
+
path: `${turnUrl.pathname}${turnUrl.search}`,
|
|
442
717
|
method: 'POST',
|
|
443
718
|
headers: {
|
|
444
719
|
'Content-Type': 'application/json',
|
|
445
720
|
'Content-Length': Buffer.byteLength(postData),
|
|
446
|
-
Accept: 'text/event-stream'
|
|
721
|
+
Accept: 'text/event-stream',
|
|
722
|
+
...(token ? { Authorization: `Bearer ${token}` } : {})
|
|
447
723
|
}
|
|
448
724
|
}, (res) => {
|
|
449
725
|
res.setEncoding('utf8');
|
|
@@ -554,7 +830,174 @@ async function chat(options = {}) {
|
|
|
554
830
|
function printBackendHint() {
|
|
555
831
|
console.log('');
|
|
556
832
|
console.log('Start backend:');
|
|
557
|
-
console.log(`cd /Users/keshavrao/arena/atrisos-backend/backend && uvicorn main:app --host ${BACKEND.host} --port ${BACKEND.port}`);
|
|
833
|
+
console.log(`cd /Users/keshavrao/arena/atrisos-backend/backend && ATRIS2_ALLOW_LOCAL_WORKSPACE=1 ENVIRONMENT=development ENV=development ../venv/bin/uvicorn main:app --host ${BACKEND.host} --port ${BACKEND.port}`);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function bufferedOutput() {
|
|
837
|
+
let text = '';
|
|
838
|
+
return {
|
|
839
|
+
isTTY: false,
|
|
840
|
+
write(chunk) {
|
|
841
|
+
text += String(chunk || '');
|
|
842
|
+
return true;
|
|
843
|
+
},
|
|
844
|
+
text() {
|
|
845
|
+
return text;
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function receiptFromState(state) {
|
|
851
|
+
const event = [...(state.events || [])].reverse().find(item => item && item.type === 'receipt' && item.receipt);
|
|
852
|
+
return event ? event.receipt : null;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function toolEventsFromState(state) {
|
|
856
|
+
const receipt = receiptFromState(state);
|
|
857
|
+
if (receipt && Array.isArray(receipt.tool_events)) return receipt.tool_events;
|
|
858
|
+
return [];
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function assertBenchmark(condition, message) {
|
|
862
|
+
if (!condition) throw new Error(message);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function toolEventText(events) {
|
|
866
|
+
return JSON.stringify(events || []);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async function runBenchmarkCase(label, fn, results, output) {
|
|
870
|
+
const startedAt = Date.now();
|
|
871
|
+
output.write(`- ${label} ... `);
|
|
872
|
+
try {
|
|
873
|
+
const detail = await fn();
|
|
874
|
+
const duration = formatDuration(Date.now() - startedAt);
|
|
875
|
+
output.write(`ok (${duration})${detail ? ` - ${detail}` : ''}\n`);
|
|
876
|
+
results.push({ label, ok: true, duration_ms: Date.now() - startedAt, detail });
|
|
877
|
+
} catch (error) {
|
|
878
|
+
const duration = formatDuration(Date.now() - startedAt);
|
|
879
|
+
output.write(`fail (${duration}) - ${error.message}\n`);
|
|
880
|
+
results.push({ label, ok: false, duration_ms: Date.now() - startedAt, error: error.message });
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
async function runBenchmark(options = {}) {
|
|
885
|
+
const mode = options.mode === 'pro' ? 'pro' : 'fast';
|
|
886
|
+
const output = options.output || process.stdout;
|
|
887
|
+
const results = [];
|
|
888
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ax-atris2-benchmark-'));
|
|
889
|
+
const localOut = () => bufferedOutput();
|
|
890
|
+
|
|
891
|
+
output.write(`Atris 2 ${mode === 'fast' ? 'Fast' : 'Pro'} benchmark\n`);
|
|
892
|
+
output.write(`${backendUrl()}\n\n`);
|
|
893
|
+
|
|
894
|
+
try {
|
|
895
|
+
await runBenchmarkCase('search through files', async () => {
|
|
896
|
+
const workspace = path.join(tempRoot, 'search');
|
|
897
|
+
fs.mkdirSync(path.join(workspace, 'src', 'deep'), { recursive: true });
|
|
898
|
+
fs.writeFileSync(path.join(workspace, 'package.json'), '{"name":"ax-search-bench"}\n');
|
|
899
|
+
fs.writeFileSync(path.join(workspace, 'src', 'deep', 'needle.txt'), 'AX_SEARCH_SENTINEL=found-here\n');
|
|
900
|
+
const sink = localOut();
|
|
901
|
+
const state = await postTurn(
|
|
902
|
+
'Use local tools to search for AX_SEARCH_SENTINEL and answer with the exact file path. Do not edit.',
|
|
903
|
+
{ mode, cwd: workspace, route: 'local', output: sink, showProgress: false }
|
|
904
|
+
);
|
|
905
|
+
const combined = `${state.output}\n${toolEventText(toolEventsFromState(state))}`;
|
|
906
|
+
assertBenchmark(/src\/deep\/needle\.txt/.test(combined), 'missing proof path src/deep/needle.txt');
|
|
907
|
+
assertBenchmark(/AX_SEARCH_SENTINEL/.test(combined), 'missing sentinel proof');
|
|
908
|
+
return 'found src/deep/needle.txt';
|
|
909
|
+
}, results, output);
|
|
910
|
+
|
|
911
|
+
await runBenchmarkCase('modify files', async () => {
|
|
912
|
+
const workspace = path.join(tempRoot, 'edit');
|
|
913
|
+
fs.mkdirSync(path.join(workspace, 'src'), { recursive: true });
|
|
914
|
+
fs.writeFileSync(path.join(workspace, 'package.json'), '{"name":"ax-edit-bench"}\n');
|
|
915
|
+
const target = path.join(workspace, 'src', 'bench-target.txt');
|
|
916
|
+
fs.writeFileSync(target, 'status=AX_BEFORE\n');
|
|
917
|
+
const sink = localOut();
|
|
918
|
+
const state = await postTurn(
|
|
919
|
+
'Use local tools to edit src/bench-target.txt. Replace AX_BEFORE with AX_AFTER, then report the changed file. Do not create any other files.',
|
|
920
|
+
{ mode, cwd: workspace, route: 'local', output: sink, showProgress: false }
|
|
921
|
+
);
|
|
922
|
+
const text = fs.readFileSync(target, 'utf8');
|
|
923
|
+
assertBenchmark(text.includes('AX_AFTER'), 'src/bench-target.txt was not modified');
|
|
924
|
+
const combined = `${state.output}\n${toolEventText(toolEventsFromState(state))}`;
|
|
925
|
+
assertBenchmark(/bench-target\.txt/.test(combined), 'missing changed-file proof in response or receipt');
|
|
926
|
+
return 'edited src/bench-target.txt';
|
|
927
|
+
}, results, output);
|
|
928
|
+
|
|
929
|
+
await runBenchmarkCase('use Atris workspace state', async () => {
|
|
930
|
+
const cwd = options.cwd || process.cwd();
|
|
931
|
+
assertBenchmark(fs.existsSync(path.join(cwd, 'atris')), 'current workspace has no atris/ directory');
|
|
932
|
+
const sink = localOut();
|
|
933
|
+
const state = await postTurn(
|
|
934
|
+
'Use Atris local tools to inspect current task or map state. Report one concrete Atris file or task source you used. Do not edit files.',
|
|
935
|
+
{ mode, cwd, route: 'local', output: sink, showProgress: false }
|
|
936
|
+
);
|
|
937
|
+
const combined = `${state.output}\n${toolEventText(toolEventsFromState(state))}`;
|
|
938
|
+
assertBenchmark(/atris\/(TODO|MAP|atris)\.md|local_task_op|local_map_op|Task|Map/.test(combined), 'missing Atris task/map proof');
|
|
939
|
+
return 'read Atris task/map surface';
|
|
940
|
+
}, results, output);
|
|
941
|
+
|
|
942
|
+
await runBenchmarkCase('know XP system', async () => {
|
|
943
|
+
const cwd = options.cwd || process.cwd();
|
|
944
|
+
assertBenchmark(fs.existsSync(path.join(cwd, 'atris')), 'current workspace has no atris/ directory');
|
|
945
|
+
const sink = localOut();
|
|
946
|
+
const state = await postTurn(
|
|
947
|
+
'Use the local RewardRubric/Atris tools or workspace files to explain the Career XP reward system in this workspace. Mention proof, review/acceptance, and reward/XP. Cite the exact tool or file you used. Do not edit files.',
|
|
948
|
+
{ mode, cwd, route: 'local', output: sink, showProgress: false }
|
|
949
|
+
);
|
|
950
|
+
const combined = `${state.output}\n${toolEventText(toolEventsFromState(state))}`;
|
|
951
|
+
assertBenchmark(/xp|reward|rubric/i.test(combined), 'missing XP/reward/rubric proof');
|
|
952
|
+
assertBenchmark(/proof|review|accept|approval/i.test(combined), 'missing proof/review/acceptance explanation');
|
|
953
|
+
assertBenchmark(/RewardRubric|local_reward_rubric_op|atris\/|main\.js|taskBoard/i.test(combined), 'missing cited XP source or tool');
|
|
954
|
+
return 'explained XP reward loop';
|
|
955
|
+
}, results, output);
|
|
956
|
+
|
|
957
|
+
await runBenchmarkCase('list connections', async () => {
|
|
958
|
+
const context = await buildConnectionContext({ token: authToken(), localWorkspace: false });
|
|
959
|
+
const connected = (context.connections || []).filter(row => row.connected);
|
|
960
|
+
assertBenchmark((context.connections || []).length >= 5, 'connection capability context is missing');
|
|
961
|
+
const sink = localOut();
|
|
962
|
+
const state = await postTurn(
|
|
963
|
+
'Which Atris integrations are connected for me? Include Google Calendar status. Answer from live connection context.',
|
|
964
|
+
{ mode, route: 'cloud', output: sink, showProgress: false, connectionContext: context }
|
|
965
|
+
);
|
|
966
|
+
const receipt = receiptFromState(state);
|
|
967
|
+
assertBenchmark(receipt && receipt.connection_context, 'receipt missing connection_context');
|
|
968
|
+
const answer = `${state.output}\n${receipt && receipt.final ? receipt.final : ''}`;
|
|
969
|
+
assertBenchmark(/calendar|gmail|github|slack|drive|integration|connected/i.test(answer), 'answer did not discuss integrations');
|
|
970
|
+
return `${connected.length} connected reported`;
|
|
971
|
+
}, results, output);
|
|
972
|
+
|
|
973
|
+
await runBenchmarkCase('read calendar', async () => {
|
|
974
|
+
const sink = localOut();
|
|
975
|
+
const state = await postTurn(
|
|
976
|
+
'What is on my calendar today? Use the connected Google Calendar read path and be concise.',
|
|
977
|
+
{ mode, route: 'cloud', output: sink, showProgress: false }
|
|
978
|
+
);
|
|
979
|
+
const events = toolEventsFromState(state);
|
|
980
|
+
const text = toolEventText(events);
|
|
981
|
+
assertBenchmark(/google_calendar/.test(text), 'calendar tool event missing');
|
|
982
|
+
assertBenchmark(!/"status":"not_connected"/.test(text), 'Google Calendar is not connected for this account');
|
|
983
|
+
assertBenchmark(/Calendar|event|No calendar events|meeting/i.test(state.output), 'calendar answer missing');
|
|
984
|
+
return 'calendar read path responded';
|
|
985
|
+
}, results, output);
|
|
986
|
+
} finally {
|
|
987
|
+
try {
|
|
988
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
989
|
+
} catch (_) {}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const failed = results.filter(result => !result.ok);
|
|
993
|
+
output.write('\n');
|
|
994
|
+
output.write(failed.length ? `Benchmark failed: ${failed.length}/${results.length} failed\n` : `Benchmark passed: ${results.length}/${results.length}\n`);
|
|
995
|
+
if (failed.length) {
|
|
996
|
+
const error = new Error(failed.map(result => `${result.label}: ${result.error}`).join('; '));
|
|
997
|
+
error.results = results;
|
|
998
|
+
throw error;
|
|
999
|
+
}
|
|
1000
|
+
return results;
|
|
558
1001
|
}
|
|
559
1002
|
|
|
560
1003
|
async function main() {
|
|
@@ -566,27 +1009,36 @@ async function main() {
|
|
|
566
1009
|
|
|
567
1010
|
const mode = args.includes('--fast') ? 'fast' : 'pro';
|
|
568
1011
|
const doctor = args.includes('--doctor');
|
|
1012
|
+
const benchmark = args.includes('--benchmark');
|
|
1013
|
+
const forceCloud = args.includes('--cloud');
|
|
1014
|
+
const forceLocal = args.includes('--local');
|
|
1015
|
+
const route = forceCloud ? 'cloud' : forceLocal ? 'local' : 'auto';
|
|
569
1016
|
const prompt = args
|
|
570
|
-
.filter(arg => !['--fast', '--pro', '--chat', '--doctor', '--help', '-h'].includes(arg))
|
|
1017
|
+
.filter(arg => !['--fast', '--pro', '--chat', '--doctor', '--benchmark', '--local', '--cloud', '--help', '-h'].includes(arg))
|
|
571
1018
|
.join(' ')
|
|
572
1019
|
.trim();
|
|
573
1020
|
|
|
574
1021
|
try {
|
|
1022
|
+
if (benchmark) {
|
|
1023
|
+
await runBenchmark({ mode, cwd: process.cwd(), output: process.stdout });
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
575
1027
|
if (doctor) {
|
|
576
1028
|
console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }));
|
|
577
1029
|
console.log('');
|
|
578
|
-
console.log(formatRunProfile(buildRunProfile({ mode, cwd: process.cwd() }), process.stdout));
|
|
1030
|
+
console.log(formatRunProfile(buildRunProfile({ mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route }), process.stdout));
|
|
579
1031
|
return;
|
|
580
1032
|
}
|
|
581
1033
|
|
|
582
1034
|
if (!prompt || args.includes('--chat')) {
|
|
583
|
-
await chat({ mode, cwd: process.cwd() });
|
|
1035
|
+
await chat({ mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route });
|
|
584
1036
|
return;
|
|
585
1037
|
}
|
|
586
1038
|
|
|
587
1039
|
console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }));
|
|
588
1040
|
console.log('');
|
|
589
|
-
const result = await postTurn(prompt, { mode, cwd: process.cwd() });
|
|
1041
|
+
const result = await postTurn(prompt, { mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route });
|
|
590
1042
|
console.log('');
|
|
591
1043
|
console.log(formatDoneLine(result.durationMs));
|
|
592
1044
|
} catch (error) {
|
|
@@ -601,8 +1053,12 @@ if (require.main === module) {
|
|
|
601
1053
|
}
|
|
602
1054
|
|
|
603
1055
|
module.exports = {
|
|
1056
|
+
authToken,
|
|
1057
|
+
authUserId,
|
|
1058
|
+
backendBaseUrl,
|
|
604
1059
|
backendUrl,
|
|
605
1060
|
buildPayload,
|
|
1061
|
+
buildConnectionContext,
|
|
606
1062
|
buildRunProfile,
|
|
607
1063
|
chat,
|
|
608
1064
|
createProgressReporter,
|
|
@@ -620,6 +1076,8 @@ module.exports = {
|
|
|
620
1076
|
modelForMode,
|
|
621
1077
|
parseSseBlock,
|
|
622
1078
|
postTurn,
|
|
1079
|
+
resolveRoute,
|
|
1080
|
+
runBenchmark,
|
|
623
1081
|
summarizeToolInput,
|
|
624
1082
|
summarizeToolResult
|
|
625
1083
|
};
|
package/bin/atris.js
CHANGED
|
@@ -1666,7 +1666,7 @@ if (command === 'init') {
|
|
|
1666
1666
|
require('../commands/workflow').executeAgentSDKFast(userInput);
|
|
1667
1667
|
} else if (command === 'computer') {
|
|
1668
1668
|
require('../commands/computer').runComputer()
|
|
1669
|
-
.then(() => process.exit(0))
|
|
1669
|
+
.then(() => process.exit(process.exitCode || 0))
|
|
1670
1670
|
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
1671
1671
|
} else if (command === 'diff') {
|
|
1672
1672
|
let diffSlug = process.argv[3];
|