atris 3.15.23 → 3.15.30

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 ADDED
@@ -0,0 +1,1083 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const http = require('http');
5
+ const https = require('https');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const readline = require('readline');
9
+ const { loadCredentials } = require('./utils/auth');
10
+
11
+ const EXIT_WORDS = new Set(['exit', 'quit', ':q']);
12
+ const BACKEND = {
13
+ host: '127.0.0.1',
14
+ port: 8000,
15
+ path: '/api/atris2/turn'
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
+ };
50
+ const ANSI = {
51
+ reset: '\x1b[0m',
52
+ bold: '\x1b[1m',
53
+ dim: '\x1b[2m',
54
+ muted: '\x1b[90m',
55
+ accent: '\x1b[36m',
56
+ ok: '\x1b[32m'
57
+ };
58
+
59
+ function modelForMode(mode) {
60
+ return mode === 'fast' ? 'atris:fast' : 'atris:pro';
61
+ }
62
+
63
+ function formatDuration(ms) {
64
+ const value = Number(ms) || 0;
65
+ if (value < 1000) return `${Math.max(0, Math.round(value))}ms`;
66
+ const totalSeconds = Math.max(1, Math.round(value / 1000));
67
+ return formatSeconds(totalSeconds);
68
+ }
69
+
70
+ function formatSeconds(totalSeconds) {
71
+ if (totalSeconds < 60) return `${totalSeconds}s`;
72
+ const minutes = Math.floor(totalSeconds / 60);
73
+ const seconds = totalSeconds % 60;
74
+ return seconds ? `${minutes}m ${seconds}s` : `${minutes}m`;
75
+ }
76
+
77
+ function formatHeader({ mode = 'pro', cwd = process.cwd(), chat = false } = {}) {
78
+ const label = mode === 'fast' ? 'Atris 2 Fast' : 'Atris 2 Pro';
79
+ return [
80
+ `${label}${chat ? ' chat' : ''} (${modelForMode(mode)})`,
81
+ cwd,
82
+ chat ? 'exit with exit, quit, or :q' : '',
83
+ ].filter(Boolean).join('\n');
84
+ }
85
+
86
+ function formatUsage() {
87
+ return [
88
+ 'ax - Atris 2 local coding agent',
89
+ '',
90
+ 'Usage:',
91
+ ' ax [--pro|--fast] [--local|--cloud] <message>',
92
+ ' ax [--pro|--fast] [--local|--cloud] --chat',
93
+ ' ax [--pro|--fast] --doctor',
94
+ ' ax [--fast] --benchmark',
95
+ '',
96
+ 'Modes:',
97
+ ' --pro local workspace agent, deeper tool loop',
98
+ ' --fast local workspace agent, faster low-latency turns',
99
+ ' --local force local workspace tools',
100
+ ' --cloud force authenticated cloud connectors/chat',
101
+ '',
102
+ 'Examples:',
103
+ ' ax --pro find the config file and explain it',
104
+ ' ax --fast what files are here',
105
+ ' ax --fast what is on my calendar today',
106
+ ' ax --pro --chat',
107
+ ].join('\n');
108
+ }
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
+
117
+ function backendUrl() {
118
+ return new URL(BACKEND.path, backendBaseUrl()).toString();
119
+ }
120
+
121
+ function backendPathUrl(pathname) {
122
+ return new URL(pathname, backendBaseUrl()).toString();
123
+ }
124
+
125
+ function buildRunProfile(options = {}) {
126
+ const mode = options.mode === 'fast' ? 'fast' : 'pro';
127
+ const cwd = options.cwd || process.cwd();
128
+ const route = resolveRoute(options.message || 'doctor', options);
129
+ const payload = buildPayload(options.message || 'doctor', { mode, cwd, route });
130
+ return {
131
+ endpoint: backendUrl(),
132
+ mode,
133
+ route,
134
+ model: payload.model,
135
+ workspace_path: payload.workspace_path || 'cloud',
136
+ max_turns: payload.max_turns,
137
+ streaming: true,
138
+ runtime: route === 'cloud' ? 'authenticated cloud connectors/chat' : 'local workspace',
139
+ reasoning: mode === 'pro'
140
+ ? 'backend reports run row; Pro workspace tool loop uses API default medium'
141
+ : 'backend reports run row; Fast workspace tool loop uses provider default'
142
+ };
143
+ }
144
+
145
+ function formatRunProfile(profile, options = {}) {
146
+ const rows = [
147
+ ['mode', `${profile.mode} (${profile.model})`],
148
+ ['endpoint', profile.endpoint],
149
+ ['route', profile.route || 'auto'],
150
+ ['workspace', formatPathSubject(profile.workspace_path, options)],
151
+ ['turns', String(profile.max_turns)],
152
+ ['streaming', profile.streaming ? 'yes' : 'no'],
153
+ ['runtime', profile.runtime],
154
+ ['thinking', profile.reasoning],
155
+ ];
156
+ return rows.map(([label, value]) => formatAuxRow(label, value, options)).join('\n');
157
+ }
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
+
349
+ function formatPrompt() {
350
+ return '› ';
351
+ }
352
+
353
+ function formatWorkingLine(ms) {
354
+ const totalSeconds = Math.max(1, Math.round((Number(ms) || 0) / 1000));
355
+ return `• Working (${formatSeconds(totalSeconds)} • ctrl-c to interrupt)`;
356
+ }
357
+
358
+ function formatDoneLine(ms) {
359
+ return `— Worked for ${formatDuration(ms)} —`;
360
+ }
361
+
362
+ function useColor(options = {}) {
363
+ if (options.color === false) return false;
364
+ if (process.env.NO_COLOR) return false;
365
+ return Boolean(options.color || options.isTTY);
366
+ }
367
+
368
+ function paint(text, codes, options = {}) {
369
+ if (!useColor(options)) return String(text);
370
+ return `${codes.join('')}${text}${ANSI.reset}`;
371
+ }
372
+
373
+ function truncateMiddle(value, limit = 120) {
374
+ const text = String(value || '');
375
+ if (text.length <= limit) return text;
376
+ const head = Math.max(8, Math.floor((limit - 3) * 0.6));
377
+ const tail = Math.max(8, limit - 3 - head);
378
+ return `${text.slice(0, head)}...${text.slice(-tail)}`;
379
+ }
380
+
381
+ function formatPathSubject(value, options = {}) {
382
+ const raw = truncateMiddle(String(value || '').trim(), options.limit || 120);
383
+ if (!raw) return '';
384
+ if (raw === '.' || raw === '/' || raw.endsWith('/')) {
385
+ return paint(raw, [ANSI.accent], options);
386
+ }
387
+
388
+ const slash = raw.lastIndexOf('/');
389
+ if (slash === -1) return paint(raw, [ANSI.bold, ANSI.accent], options);
390
+
391
+ const parent = raw.slice(0, slash + 1);
392
+ const filename = raw.slice(slash + 1);
393
+ return `${paint(parent, [ANSI.muted], options)} ${paint(filename, [ANSI.bold, ANSI.accent], options)}`;
394
+ }
395
+
396
+ function formatAuxRow(label, value, options = {}) {
397
+ const tag = String(label || '').padEnd(4);
398
+ const color = label === 'ok' ? [ANSI.ok] : [ANSI.muted];
399
+ return ` ${paint(tag, color, options)} ${value}`;
400
+ }
401
+
402
+ function createProgressReporter(output, options = {}) {
403
+ const enabled = options.showProgress !== false;
404
+ const isTty = Boolean(output && output.isTTY);
405
+ const startedAt = Date.now();
406
+ let interval = null;
407
+ let timeout = null;
408
+ let shown = false;
409
+ let closed = false;
410
+
411
+ const render = () => {
412
+ if (!enabled || closed) return;
413
+ const line = formatWorkingLine(Date.now() - startedAt);
414
+ if (isTty) {
415
+ output.write(`\r${line}\x1b[K`);
416
+ } else if (!shown) {
417
+ output.write(`${line}\n`);
418
+ }
419
+ shown = true;
420
+ };
421
+
422
+ return {
423
+ start() {
424
+ if (!enabled) return;
425
+ timeout = setTimeout(() => {
426
+ render();
427
+ if (isTty) interval = setInterval(render, 1000);
428
+ }, 700);
429
+ },
430
+ clear() {
431
+ if (!isTty || !shown || closed) return;
432
+ output.write('\r\x1b[K');
433
+ shown = false;
434
+ },
435
+ stop() {
436
+ closed = true;
437
+ clearTimeout(timeout);
438
+ clearInterval(interval);
439
+ if (isTty && shown) output.write('\r\x1b[K');
440
+ shown = false;
441
+ }
442
+ };
443
+ }
444
+
445
+ function parseSseBlock(block) {
446
+ const data = String(block || '')
447
+ .split(/\r?\n/)
448
+ .filter(line => line.startsWith('data:'))
449
+ .map(line => line.replace(/^data:\s?/, ''))
450
+ .join('\n')
451
+ .trim();
452
+
453
+ if (!data || data === '[DONE]') return null;
454
+ return JSON.parse(data);
455
+ }
456
+
457
+ function summarizeToolInput(block) {
458
+ const options = arguments[1] || {};
459
+ const tool = block.tool || 'tool';
460
+ const input = block.input || {};
461
+ const toolName = paint(tool, [ANSI.bold], options);
462
+ const pathValue = input.file_path || input.path;
463
+ if (input.command) return `${toolName} ${truncateMiddle(input.command, 120)}`;
464
+ if (input.pattern || input.query) {
465
+ const pattern = truncateMiddle(input.pattern || input.query, 80);
466
+ return pathValue
467
+ ? `${toolName} ${pattern} in ${formatPathSubject(pathValue, options)}`
468
+ : `${toolName} ${pattern}`;
469
+ }
470
+ if (pathValue) return `${toolName} ${formatPathSubject(pathValue, options)}`;
471
+ const subject = input.type || '';
472
+ return subject ? `${toolName} ${String(subject).slice(0, 120)}` : toolName;
473
+ }
474
+
475
+ function summarizeToolResult(result) {
476
+ const options = arguments[1] || {};
477
+ const content = String(result.content || '');
478
+ if (!content) return 'done';
479
+
480
+ try {
481
+ const parsed = JSON.parse(content);
482
+ if (parsed.error) return `error: ${parsed.error}`;
483
+ if (Array.isArray(parsed.matches)) {
484
+ const where = parsed.path ? ` in ${formatPathSubject(parsed.path, options)}` : '';
485
+ return `${parsed.matches.length} matches${where}`;
486
+ }
487
+ if (Array.isArray(parsed.files) || Array.isArray(parsed.dirs)) {
488
+ const parts = [];
489
+ if (Array.isArray(parsed.files)) parts.push(`${parsed.files.length} files`);
490
+ if (Array.isArray(parsed.dirs)) parts.push(`${parsed.dirs.length} dirs`);
491
+ const where = parsed.path ? ` in ${formatPathSubject(parsed.path, options)}` : '';
492
+ return `${parts.join(' / ')}${where}`;
493
+ }
494
+ if (parsed.path && parsed.status) return `${parsed.status} ${formatPathSubject(parsed.path, options)}`;
495
+ if (parsed.path) return formatPathSubject(parsed.path, options);
496
+ if (parsed.status) return parsed.status;
497
+ } catch (_) {
498
+ // Bash output and older streams can be plain text.
499
+ }
500
+
501
+ return content.length > 120 ? `${content.slice(0, 117)}...` : content;
502
+ }
503
+
504
+ function formatStatusMessage(message) {
505
+ const text = String(message || '').trim();
506
+ if (!text || text === 'complete') return null;
507
+ if (text === 'retrying_with_required_local_tool') return null;
508
+ return text.replace(/_/g, ' ');
509
+ }
510
+
511
+ function formatSystemInit(event, options = {}) {
512
+ const runtime = event.tool_runtime || {};
513
+ const model = runtime.tool_model || runtime.chat_model || event.model || '';
514
+ const mode = runtime.mode ? String(runtime.mode).replace(/_/g, ' ') : '';
515
+ const thinking = runtime.reasoning_effort ? `thinking ${runtime.reasoning_effort}` : '';
516
+ const parts = [mode || 'runtime', model, thinking].filter(Boolean);
517
+ return parts.length ? parts.join(' ') : null;
518
+ }
519
+
520
+ function clearRetriedText(state) {
521
+ state.pendingText = '';
522
+ state.output = '';
523
+ state.wroteText = false;
524
+ state.lastChar = '\n';
525
+ state.inAuxBlock = false;
526
+ }
527
+
528
+ function stopProgress(state) {
529
+ if (!state.progress) return;
530
+ state.progress.stop();
531
+ state.progress = null;
532
+ }
533
+
534
+ function flushPendingText(state, output) {
535
+ if (!state.pendingText) return;
536
+ stopProgress(state);
537
+ if (state.inAuxBlock && state.lastChar === '\n') output.write('\n');
538
+ output.write(state.pendingText);
539
+ state.wroteText = true;
540
+ state.wroteActivity = true;
541
+ state.lastChar = state.pendingText.slice(-1);
542
+ state.pendingText = '';
543
+ state.inAuxBlock = false;
544
+ }
545
+
546
+ function writeStreamingText(state, output, content) {
547
+ if (!content) return;
548
+ stopProgress(state);
549
+ if (state.inAuxBlock && state.lastChar === '\n') output.write('\n');
550
+ output.write(content);
551
+ state.wroteText = true;
552
+ state.wroteActivity = true;
553
+ state.lastChar = String(content).slice(-1);
554
+ state.inAuxBlock = false;
555
+ }
556
+
557
+ function writeAuxLine(state, output, line) {
558
+ if (!line) return;
559
+ stopProgress(state);
560
+ if (state.wroteText && state.lastChar !== '\n') output.write('\n');
561
+ if (!state.inAuxBlock && state.wroteActivity && state.lastChar === '\n') output.write('\n');
562
+ output.write(`${line}\n`);
563
+ state.wroteActivity = true;
564
+ state.lastChar = '\n';
565
+ state.inAuxBlock = true;
566
+ }
567
+
568
+ function compactHistory(history, limit = 8) {
569
+ return history
570
+ .slice(-limit)
571
+ .map(turn => `${turn.role}: ${String(turn.content || '').slice(0, 1200)}`)
572
+ .join('\n');
573
+ }
574
+
575
+ function buildMessage(message, history = []) {
576
+ const trimmed = String(message || '').trim();
577
+ if (!history.length) return trimmed;
578
+ return [
579
+ 'Continue this terminal coding-agent conversation in the same workspace.',
580
+ 'Use local tools when the user asks about files, code, tests, or edits.',
581
+ '',
582
+ 'Recent conversation:',
583
+ compactHistory(history),
584
+ '',
585
+ `Current user message: ${trimmed}`,
586
+ ].join('\n');
587
+ }
588
+
589
+ function buildPayload(message, options = {}) {
590
+ const mode = options.mode === 'fast' ? 'fast' : 'pro';
591
+ const route = resolveRoute(message, options);
592
+ const local = route !== 'cloud';
593
+ const payload = {
594
+ message: buildMessage(message, options.history || []),
595
+ model: modelForMode(mode),
596
+ max_turns: local ? (mode === 'pro' ? 14 : 8) : 1,
597
+ verify_command: 'true'
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;
614
+ }
615
+
616
+ function handleEvent(event, state, output) {
617
+ if (!event || typeof event !== 'object') return;
618
+ state.events.push(event);
619
+
620
+ if (event.type === 'system_init') {
621
+ state.runtime = event;
622
+ writeAuxLine(state, output, formatAuxRow('run', formatSystemInit(event, output), output));
623
+ return;
624
+ }
625
+
626
+ if ((event.type === 'text_delta' || event.type === 'text') && event.content) {
627
+ state.output += event.content;
628
+ if (output && output.isTTY) writeStreamingText(state, output, event.content);
629
+ else state.pendingText += event.content;
630
+ return;
631
+ }
632
+
633
+ if (event.type === 'assistant_blocks' && Array.isArray(event.blocks)) {
634
+ for (const block of event.blocks) {
635
+ if (block && block.type === 'tool_use') {
636
+ flushPendingText(state, output);
637
+ writeAuxLine(state, output, formatAuxRow('tool', summarizeToolInput(block, output), output));
638
+ }
639
+ }
640
+ return;
641
+ }
642
+
643
+ if (event.type === 'tool_results' && Array.isArray(event.results)) {
644
+ for (const result of event.results) {
645
+ flushPendingText(state, output);
646
+ writeAuxLine(state, output, formatAuxRow('ok', summarizeToolResult(result, output), output));
647
+ }
648
+ return;
649
+ }
650
+
651
+ if (event.type === 'status' && event.message && event.message !== 'complete') {
652
+ if (event.message === 'retrying_with_required_local_tool') {
653
+ clearRetriedText(state);
654
+ return;
655
+ }
656
+ flushPendingText(state, output);
657
+ writeAuxLine(state, output, formatAuxRow('info', formatStatusMessage(event.message), output));
658
+ return;
659
+ }
660
+
661
+ if (event.type === 'error' || event.error) {
662
+ state.errors.push(event.error || event.message || 'Atris2 stream error');
663
+ }
664
+ }
665
+
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 });
679
+ const postData = JSON.stringify(payload);
680
+ const output = options.output || process.stdout;
681
+ const timeoutMs = payload.model === 'atris:pro' ? 180000 : 60000;
682
+ const turnUrl = new URL(backendUrl());
683
+ const transport = turnUrl.protocol === 'https:' ? https : http;
684
+ const state = {
685
+ events: [],
686
+ errors: [],
687
+ output: '',
688
+ pendingText: '',
689
+ wroteText: false,
690
+ wroteActivity: false,
691
+ durationMs: 0,
692
+ lastChar: '\n',
693
+ progress: null,
694
+ inAuxBlock: false
695
+ };
696
+
697
+ return new Promise((resolve, reject) => {
698
+ let settled = false;
699
+ const startedAt = Date.now();
700
+ state.progress = createProgressReporter(output, options);
701
+ state.progress.start();
702
+
703
+ const finish = (error, value) => {
704
+ if (settled) return;
705
+ settled = true;
706
+ if (state.progress) state.progress.stop();
707
+ state.progress = null;
708
+ state.durationMs = Date.now() - startedAt;
709
+ if (error) reject(error);
710
+ else resolve(value);
711
+ };
712
+
713
+ const req = transport.request({
714
+ hostname: turnUrl.hostname,
715
+ port: turnUrl.port || (turnUrl.protocol === 'https:' ? 443 : 80),
716
+ path: `${turnUrl.pathname}${turnUrl.search}`,
717
+ method: 'POST',
718
+ headers: {
719
+ 'Content-Type': 'application/json',
720
+ 'Content-Length': Buffer.byteLength(postData),
721
+ Accept: 'text/event-stream',
722
+ ...(token ? { Authorization: `Bearer ${token}` } : {})
723
+ }
724
+ }, (res) => {
725
+ res.setEncoding('utf8');
726
+ let buffer = '';
727
+ let rawBody = '';
728
+
729
+ res.on('data', (chunk) => {
730
+ if (res.statusCode < 200 || res.statusCode >= 300) {
731
+ rawBody += chunk;
732
+ return;
733
+ }
734
+
735
+ buffer += chunk;
736
+ let boundary = buffer.indexOf('\n\n');
737
+ while (boundary !== -1) {
738
+ const block = buffer.slice(0, boundary);
739
+ buffer = buffer.slice(boundary + 2);
740
+ try {
741
+ handleEvent(parseSseBlock(block), state, output);
742
+ } catch (error) {
743
+ state.errors.push(`bad_sse_event: ${error.message}`);
744
+ }
745
+ boundary = buffer.indexOf('\n\n');
746
+ }
747
+ });
748
+
749
+ res.on('end', () => {
750
+ if (res.statusCode < 200 || res.statusCode >= 300) {
751
+ try {
752
+ const parsed = JSON.parse(rawBody);
753
+ finish(new Error(parsed.detail || parsed.error || `HTTP ${res.statusCode}`));
754
+ } catch (_) {
755
+ finish(new Error(rawBody || `HTTP ${res.statusCode}`));
756
+ }
757
+ return;
758
+ }
759
+
760
+ if (buffer.trim()) {
761
+ try {
762
+ handleEvent(parseSseBlock(buffer), state, output);
763
+ } catch (error) {
764
+ state.errors.push(`bad_sse_event: ${error.message}`);
765
+ }
766
+ }
767
+
768
+ if (state.errors.length) {
769
+ finish(new Error(state.errors.join('; ')));
770
+ return;
771
+ }
772
+
773
+ flushPendingText(state, output);
774
+ finish(null, state);
775
+ });
776
+ });
777
+
778
+ req.on('error', finish);
779
+ req.setTimeout(timeoutMs, () => {
780
+ finish(new Error(`Request timeout after ${timeoutMs / 1000}s`));
781
+ req.destroy();
782
+ });
783
+ req.write(postData);
784
+ req.end();
785
+ });
786
+ }
787
+
788
+ async function chat(options = {}) {
789
+ const mode = options.mode === 'fast' ? 'fast' : 'pro';
790
+ const cwd = options.cwd || process.cwd();
791
+ const input = options.input || process.stdin;
792
+ const output = options.output || process.stdout;
793
+ const history = [];
794
+
795
+ output.write(`${formatHeader({ mode, cwd, chat: true })}\n\n`);
796
+
797
+ const runLine = async (line) => {
798
+ const trimmed = String(line || '').trim();
799
+ if (!trimmed) return false;
800
+ if (EXIT_WORDS.has(trimmed.toLowerCase())) return true;
801
+
802
+ output.write('\n');
803
+ const result = await postTurn(trimmed, { mode, cwd, history, output });
804
+ if (result.output && !result.output.endsWith('\n')) output.write('\n');
805
+ output.write(`${formatDoneLine(result.durationMs)}\n\n`);
806
+ history.push({ role: 'user', content: trimmed });
807
+ history.push({ role: 'assistant', content: result.output || '' });
808
+ return false;
809
+ };
810
+
811
+ if (!input.isTTY) {
812
+ const rl = readline.createInterface({ input, crlfDelay: Infinity });
813
+ for await (const line of rl) {
814
+ if (await runLine(line)) break;
815
+ }
816
+ return;
817
+ }
818
+
819
+ const rl = readline.createInterface({ input, output });
820
+ const ask = () => new Promise(resolve => rl.question(formatPrompt(mode), resolve));
821
+ while (true) {
822
+ const line = await ask();
823
+ if (await runLine(line)) {
824
+ rl.close();
825
+ break;
826
+ }
827
+ }
828
+ }
829
+
830
+ function printBackendHint() {
831
+ console.log('');
832
+ console.log('Start backend:');
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;
1001
+ }
1002
+
1003
+ async function main() {
1004
+ const args = process.argv.slice(2);
1005
+ if (args.includes('--help') || args.includes('-h')) {
1006
+ console.log(formatUsage());
1007
+ return;
1008
+ }
1009
+
1010
+ const mode = args.includes('--fast') ? 'fast' : 'pro';
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';
1016
+ const prompt = args
1017
+ .filter(arg => !['--fast', '--pro', '--chat', '--doctor', '--benchmark', '--local', '--cloud', '--help', '-h'].includes(arg))
1018
+ .join(' ')
1019
+ .trim();
1020
+
1021
+ try {
1022
+ if (benchmark) {
1023
+ await runBenchmark({ mode, cwd: process.cwd(), output: process.stdout });
1024
+ return;
1025
+ }
1026
+
1027
+ if (doctor) {
1028
+ console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }));
1029
+ console.log('');
1030
+ console.log(formatRunProfile(buildRunProfile({ mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route }), process.stdout));
1031
+ return;
1032
+ }
1033
+
1034
+ if (!prompt || args.includes('--chat')) {
1035
+ await chat({ mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route });
1036
+ return;
1037
+ }
1038
+
1039
+ console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }));
1040
+ console.log('');
1041
+ const result = await postTurn(prompt, { mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route });
1042
+ console.log('');
1043
+ console.log(formatDoneLine(result.durationMs));
1044
+ } catch (error) {
1045
+ console.error(`x ${error.message}`);
1046
+ printBackendHint();
1047
+ process.exit(1);
1048
+ }
1049
+ }
1050
+
1051
+ if (require.main === module) {
1052
+ main();
1053
+ }
1054
+
1055
+ module.exports = {
1056
+ authToken,
1057
+ authUserId,
1058
+ backendBaseUrl,
1059
+ backendUrl,
1060
+ buildPayload,
1061
+ buildConnectionContext,
1062
+ buildRunProfile,
1063
+ chat,
1064
+ createProgressReporter,
1065
+ formatDoneLine,
1066
+ formatDuration,
1067
+ formatHeader,
1068
+ formatPathSubject,
1069
+ formatPrompt,
1070
+ formatRunProfile,
1071
+ formatStatusMessage,
1072
+ formatSystemInit,
1073
+ formatUsage,
1074
+ formatWorkingLine,
1075
+ handleEvent,
1076
+ modelForMode,
1077
+ parseSseBlock,
1078
+ postTurn,
1079
+ resolveRoute,
1080
+ runBenchmark,
1081
+ summarizeToolInput,
1082
+ summarizeToolResult
1083
+ };