atris 3.23.0 → 3.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/atris/atrisDev.md +717 -0
- package/atris/policies/outbound-artifact-gate.md +48 -0
- package/atris/skills/atris-feedback/SKILL.md +2 -3
- package/atris/wiki/sources/atris-labs-2026-05-10.txt +6 -9
- package/atris/wiki/sources/atris-labs-goals-2026-05-10.txt +4 -5
- package/atris.md +19 -43
- package/ax +1687 -95
- package/bin/atris.js +2 -32
- package/commands/aeo.js +5 -5
- package/commands/computer.js +0 -1
- package/commands/mission.js +2 -1
- package/commands/recap.js +0 -16
- package/commands/sync.js +2 -0
- package/commands/workflow.js +1 -2
- package/commands/youtube.js +183 -0
- package/lib/ax-chat-input.js +164 -0
- package/lib/ax-goal.js +307 -0
- package/lib/ax-prefs.js +70 -0
- package/lib/ax-shimmer.js +63 -0
- package/lib/context-gatherer.js +8 -26
- package/package.json +2 -1
- package/commands/card.js +0 -121
- package/commands/deck.js +0 -184
- package/commands/site.js +0 -48
- package/commands/slop.js +0 -307
- package/commands/theme.js +0 -217
- package/lib/card.js +0 -120
- package/lib/deck-from-md.js +0 -110
- package/lib/html-render.js +0 -257
- package/lib/memory-view.js +0 -95
- package/lib/site.js +0 -114
- package/lib/slides-deck.js +0 -237
- package/lib/theme.js +0 -264
package/ax
CHANGED
|
@@ -6,13 +6,46 @@ const https = require('https');
|
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const readline = require('readline');
|
|
9
|
+
const { spawnSync } = require('child_process');
|
|
10
|
+
const { Readable } = require('stream');
|
|
9
11
|
const { loadCredentials } = require('./utils/auth');
|
|
12
|
+
const {
|
|
13
|
+
MEMBER_SLUG,
|
|
14
|
+
loadAxPrefs,
|
|
15
|
+
setBypassPermissions,
|
|
16
|
+
resolveBypassPermissions,
|
|
17
|
+
permissionsLabel,
|
|
18
|
+
} = require('./lib/ax-prefs');
|
|
19
|
+
const {
|
|
20
|
+
renderShimmerText,
|
|
21
|
+
} = require('./lib/ax-shimmer');
|
|
22
|
+
const {
|
|
23
|
+
attachMultilineChatInput,
|
|
24
|
+
stripMultilineCsiText,
|
|
25
|
+
} = require('./lib/ax-chat-input');
|
|
26
|
+
const {
|
|
27
|
+
accumulateGoalUsage,
|
|
28
|
+
buildGoalDirective,
|
|
29
|
+
clearGoalState,
|
|
30
|
+
createGoalState,
|
|
31
|
+
evaluateGoalTurn,
|
|
32
|
+
finishGoalAchieved,
|
|
33
|
+
formatGoalAchieved,
|
|
34
|
+
formatGoalActiveBanner,
|
|
35
|
+
formatGoalContinue,
|
|
36
|
+
formatGoalStatus,
|
|
37
|
+
formatGoalStopped,
|
|
38
|
+
goalBudgetExceeded,
|
|
39
|
+
goalTurnLimitReached,
|
|
40
|
+
parseGoalCommand,
|
|
41
|
+
} = require('./lib/ax-goal');
|
|
10
42
|
|
|
11
43
|
const EXIT_WORDS = new Set(['exit', 'quit', ':q']);
|
|
12
44
|
const BACKEND = {
|
|
13
45
|
host: '127.0.0.1',
|
|
14
46
|
port: 8000,
|
|
15
|
-
path: '/api/atris2/turn'
|
|
47
|
+
path: '/api/atris2/turn',
|
|
48
|
+
publicBase: 'https://api.atris.ai'
|
|
16
49
|
};
|
|
17
50
|
const DEFAULT_BACKEND_BASE = `http://${BACKEND.host}:${BACKEND.port}`;
|
|
18
51
|
const CODE_FAST = {
|
|
@@ -55,14 +88,22 @@ const CONNECTOR_SCOPES = {
|
|
|
55
88
|
const ANSI = {
|
|
56
89
|
reset: '\x1b[0m',
|
|
57
90
|
bold: '\x1b[1m',
|
|
91
|
+
italic: '\x1b[3m',
|
|
58
92
|
dim: '\x1b[2m',
|
|
59
93
|
muted: '\x1b[90m',
|
|
60
94
|
accent: '\x1b[36m',
|
|
61
95
|
ok: '\x1b[32m',
|
|
62
|
-
|
|
96
|
+
safe: '\x1b[34m',
|
|
97
|
+
warn: '\x1b[33m',
|
|
98
|
+
magenta: '\x1b[35m',
|
|
99
|
+
underline: '\x1b[4m',
|
|
100
|
+
strike: '\x1b[9m',
|
|
63
101
|
};
|
|
64
102
|
|
|
65
103
|
const TIER_COLORS = { fast: '\x1b[32m', pro: '\x1b[36m', max: '\x1b[35m' };
|
|
104
|
+
const PROGRESS_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
105
|
+
const PROGRESS_START_DELAY_MS = 350;
|
|
106
|
+
const PROGRESS_FRAME_MS = 100;
|
|
66
107
|
|
|
67
108
|
function tierColor(mode) {
|
|
68
109
|
return TIER_COLORS[mode] || ANSI.accent;
|
|
@@ -95,11 +136,23 @@ function tierLabel(mode) {
|
|
|
95
136
|
return 'Atris 2 Pro';
|
|
96
137
|
}
|
|
97
138
|
|
|
98
|
-
function formatHeader({ mode = '
|
|
139
|
+
function formatHeader({ mode = 'fast', cwd = process.cwd(), chat = false, bypassPermissions, goal } = {}, options = {}) {
|
|
140
|
+
const goalLine = chat && goal ? formatGoalActiveBanner(goal, {
|
|
141
|
+
paint: (text, codes) => paint(text, codes, options),
|
|
142
|
+
bold: ANSI.bold,
|
|
143
|
+
magenta: ANSI.magenta,
|
|
144
|
+
muted: ANSI.muted,
|
|
145
|
+
accent: ANSI.accent,
|
|
146
|
+
...options,
|
|
147
|
+
}) : '';
|
|
148
|
+
const home = os.homedir();
|
|
149
|
+
const shortCwd = cwd && home && (cwd === home || cwd.startsWith(`${home}/`)) ? `~${cwd.slice(home.length)}` : cwd;
|
|
150
|
+
const title = paint(`${tierLabel(mode)}${chat ? ' chat' : ''}`, [ANSI.bold, tierColor(normalizeMode(mode))], options);
|
|
151
|
+
const titleLine = shortCwd ? `${title}${paint(` · ${shortCwd}`, [ANSI.muted], options)}` : title;
|
|
99
152
|
return [
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
chat ? paint('
|
|
153
|
+
titleLine,
|
|
154
|
+
goalLine,
|
|
155
|
+
chat ? paint('shift+tab permissions · shift+enter newline · ctrl-c · / · exit', [ANSI.muted], options) : '',
|
|
103
156
|
].filter(Boolean).join('\n');
|
|
104
157
|
}
|
|
105
158
|
|
|
@@ -108,23 +161,51 @@ function formatUsage() {
|
|
|
108
161
|
'ax - Atris local/code agent',
|
|
109
162
|
'',
|
|
110
163
|
'Usage:',
|
|
111
|
-
|
|
112
|
-
|
|
164
|
+
' ax [--fast|--pro|--max|--code-fast] [--local|--cloud] [--bypass|--safe] <message>',
|
|
165
|
+
' ax [--fast|--pro|--max|--code-fast] [--local|--cloud] [--bypass|--safe] --chat',
|
|
166
|
+
' ax chat [ax] [--fast|--pro|--max]',
|
|
167
|
+
' ax spawn <role> --task "..." [--engine manual|codex|claude|cursor|devin]',
|
|
168
|
+
' ax spawns [--json]',
|
|
169
|
+
' ax youtube <url> [--query "..."] [--json]',
|
|
170
|
+
' ax --dogfood-chat [--loops 25] [--json]',
|
|
171
|
+
' ax --dogfood-status [--json]',
|
|
113
172
|
' ax [--max|--pro|--fast] --business <slug> [<message>|--chat]',
|
|
114
173
|
' ax [--max|--pro|--fast|--code-fast] --doctor',
|
|
115
174
|
' ax [--max|--fast] --benchmark',
|
|
116
175
|
'',
|
|
117
176
|
'Modes:',
|
|
118
|
-
' --max
|
|
119
|
-
' --pro
|
|
120
|
-
' --fast
|
|
177
|
+
' --max hosted Atris 2, highest reasoning, slowest turns',
|
|
178
|
+
' --pro hosted Atris 2, deeper tool loop',
|
|
179
|
+
' --fast hosted Atris 2, faster low-latency turns',
|
|
121
180
|
' --code-fast Atris Code Fast public lane',
|
|
122
181
|
' --local force local workspace tools',
|
|
123
182
|
' --cloud force authenticated cloud connectors/chat',
|
|
124
183
|
' --business <slug> run tools on that business cloud workspace (EC2)',
|
|
125
184
|
' --verify <cmd> gate the turn on this command passing (default: no verifier)',
|
|
185
|
+
' --bypass allow external side effects for this run (default: safe / approval-only)',
|
|
186
|
+
' --safe force approval-only mode for this run',
|
|
187
|
+
' --dogfood-chat run the safe ax fast chat checklist harness',
|
|
188
|
+
' --dogfood-status summarize the overnight dogfood mission progress',
|
|
189
|
+
'',
|
|
190
|
+
'Chat:',
|
|
191
|
+
' /goal <condition> work autonomously until the condition is met',
|
|
192
|
+
' /goal show active goal, turns, and evaluator reason',
|
|
193
|
+
' /goal clear stop the active goal',
|
|
194
|
+
' /bypass persist bypass mode (external actions allowed when backend supports it)',
|
|
195
|
+
' /safe persist safe mode (default; approval-only external actions)',
|
|
196
|
+
'',
|
|
197
|
+
'Delegation:',
|
|
198
|
+
' ax spawn <role> --task "..." create a worker request from ax',
|
|
199
|
+
' atris agent spawn <role> --task "..." same request ledger from the Atris CLI',
|
|
200
|
+
' ax youtube <url> process a YouTube video through Atris',
|
|
201
|
+
' ax --dogfood-chat --loops 25 exercise chat UI and routing without spending credits',
|
|
202
|
+
' ax --dogfood-status show overnight dogfood proof and remaining loops',
|
|
126
203
|
'',
|
|
127
204
|
'Examples:',
|
|
205
|
+
' ax spawn worker --task "Fix the failing smoke test" --engine codex',
|
|
206
|
+
' ax youtube https://www.youtube.com/watch?v=VIDEO_ID',
|
|
207
|
+
' ax --dogfood-chat --loops 25',
|
|
208
|
+
' ax --dogfood-status',
|
|
128
209
|
' ax --pro find the config file and explain it',
|
|
129
210
|
' ax --fast what files are here',
|
|
130
211
|
' ax --max refactor this module and verify the tests',
|
|
@@ -145,7 +226,7 @@ function stripAnsi(value) {
|
|
|
145
226
|
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '');
|
|
146
227
|
}
|
|
147
228
|
|
|
148
|
-
function createRunLogger({ cwd = process.cwd(), mode = '
|
|
229
|
+
function createRunLogger({ cwd = process.cwd(), mode = 'fast', kind = 'play', output = process.stdout } = {}) {
|
|
149
230
|
if (process.env.AX_AUTO_LOG === '0') return null;
|
|
150
231
|
if (output !== process.stdout && output !== process.stderr && !output.isTTY) return null;
|
|
151
232
|
|
|
@@ -185,30 +266,51 @@ function createRunLogger({ cwd = process.cwd(), mode = 'pro', kind = 'play', out
|
|
|
185
266
|
};
|
|
186
267
|
}
|
|
187
268
|
|
|
188
|
-
function backendBaseUrl() {
|
|
189
|
-
|
|
190
|
-
|
|
269
|
+
function backendBaseUrl(options = {}) {
|
|
270
|
+
const route = options.route === 'local' || options.forceLocal
|
|
271
|
+
? 'local'
|
|
272
|
+
: options.route === 'cloud' || options.forceCloud
|
|
273
|
+
? 'cloud'
|
|
274
|
+
: 'auto';
|
|
275
|
+
const localOverride = process.env.AX_BACKEND_URL || process.env.OBELISK_LOCAL_ATRIS2_BACKEND_URL;
|
|
276
|
+
const cloudOverride = process.env.OBELISK_ATRIS2_BACKEND_URL || process.env.ATRIS_API_BASE || BACKEND.publicBase;
|
|
277
|
+
if (route === 'cloud') {
|
|
278
|
+
if (localOverride && !isLoopbackUrl(localOverride)) return localOverride.replace(/\/$/, '');
|
|
279
|
+
return cloudOverride.replace(/\/$/, '');
|
|
280
|
+
}
|
|
281
|
+
if (route === 'auto' && !localOverride) {
|
|
282
|
+
return cloudOverride.replace(/\/$/, '');
|
|
283
|
+
}
|
|
284
|
+
return (localOverride
|
|
191
285
|
|| process.env.OBELISK_ATRIS2_BACKEND_URL
|
|
192
286
|
|| DEFAULT_BACKEND_BASE).replace(/\/$/, '');
|
|
193
287
|
}
|
|
194
288
|
|
|
195
|
-
function backendUrl() {
|
|
196
|
-
return new URL(BACKEND.path, backendBaseUrl()).toString();
|
|
289
|
+
function backendUrl(options = {}) {
|
|
290
|
+
return new URL(BACKEND.path, backendBaseUrl(options)).toString();
|
|
197
291
|
}
|
|
198
292
|
|
|
199
|
-
function backendPathUrl(pathname) {
|
|
200
|
-
return new URL(pathname, backendBaseUrl()).toString();
|
|
293
|
+
function backendPathUrl(pathname, options = {}) {
|
|
294
|
+
return new URL(pathname, backendBaseUrl(options)).toString();
|
|
201
295
|
}
|
|
202
296
|
|
|
203
|
-
function codeFastBaseUrl() {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
||
|
|
207
|
-
|
|
297
|
+
function codeFastBaseUrl(options = {}) {
|
|
298
|
+
const route = options.route === 'local' || options.forceLocal
|
|
299
|
+
? 'local'
|
|
300
|
+
: options.route === 'cloud' || options.forceCloud
|
|
301
|
+
? 'cloud'
|
|
302
|
+
: 'auto';
|
|
303
|
+
const backendOverride = process.env.AX_CODE_FAST_BACKEND_URL;
|
|
304
|
+
const useBackendOverride = backendOverride && !(route === 'cloud' && isLoopbackUrl(backendOverride));
|
|
305
|
+
return (useBackendOverride
|
|
306
|
+
? backendOverride
|
|
307
|
+
: process.env.AX_CODE_FAST_API_BASE
|
|
308
|
+
|| process.env.ATRIS_API_BASE
|
|
309
|
+
|| CODE_FAST.publicBase).replace(/\/$/, '');
|
|
208
310
|
}
|
|
209
311
|
|
|
210
|
-
function codeFastUrl() {
|
|
211
|
-
return new URL(CODE_FAST.path, codeFastBaseUrl()).toString();
|
|
312
|
+
function codeFastUrl(options = {}) {
|
|
313
|
+
return new URL(CODE_FAST.path, codeFastBaseUrl(options)).toString();
|
|
212
314
|
}
|
|
213
315
|
|
|
214
316
|
function isLoopbackUrl(value) {
|
|
@@ -220,6 +322,15 @@ function isLoopbackUrl(value) {
|
|
|
220
322
|
}
|
|
221
323
|
}
|
|
222
324
|
|
|
325
|
+
function configuredLocalBackendUrl() {
|
|
326
|
+
return process.env.AX_BACKEND_URL || process.env.OBELISK_LOCAL_ATRIS2_BACKEND_URL || '';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function hasLocalWorkspaceBackend(options = {}) {
|
|
330
|
+
if (options.localWorkspaceBackend === true) return true;
|
|
331
|
+
return Boolean(configuredLocalBackendUrl());
|
|
332
|
+
}
|
|
333
|
+
|
|
223
334
|
function isCodeFastLocal(options = {}) {
|
|
224
335
|
const route = options.route === 'local' || options.forceLocal ? 'local' : 'cloud';
|
|
225
336
|
return route === 'local' && isLoopbackUrl(options.codeFastBaseUrl || codeFastBaseUrl());
|
|
@@ -231,7 +342,7 @@ function buildRunProfile(options = {}) {
|
|
|
231
342
|
if (mode === 'code-fast') {
|
|
232
343
|
const route = options.route === 'local' || options.forceLocal ? 'local' : 'cloud';
|
|
233
344
|
return {
|
|
234
|
-
endpoint: codeFastUrl(),
|
|
345
|
+
endpoint: codeFastUrl({ route }),
|
|
235
346
|
mode,
|
|
236
347
|
route,
|
|
237
348
|
model: CODE_FAST.model,
|
|
@@ -242,15 +353,18 @@ function buildRunProfile(options = {}) {
|
|
|
242
353
|
reasoning: 'Composer 2.5 fast lane; charges 10 credits per public turn'
|
|
243
354
|
};
|
|
244
355
|
}
|
|
245
|
-
const
|
|
246
|
-
const
|
|
356
|
+
const profileMessage = options.message || 'what files are here?';
|
|
357
|
+
const route = resolveRoute(profileMessage, options);
|
|
358
|
+
const payload = buildPayload(profileMessage, { mode, cwd, route, bypassPermissions: options.bypassPermissions });
|
|
247
359
|
return {
|
|
248
|
-
endpoint: backendUrl(),
|
|
360
|
+
endpoint: backendUrl({ route }),
|
|
249
361
|
mode,
|
|
250
362
|
route,
|
|
251
363
|
model: payload.model,
|
|
252
364
|
workspace_path: payload.workspace_path || 'cloud',
|
|
253
365
|
max_turns: payload.max_turns,
|
|
366
|
+
member_slug: payload.member_slug,
|
|
367
|
+
bypass_permissions: payload.bypass_permissions,
|
|
254
368
|
streaming: true,
|
|
255
369
|
runtime: route === 'cloud' ? 'authenticated cloud connectors/chat' : 'local workspace',
|
|
256
370
|
reasoning: mode === 'max'
|
|
@@ -264,6 +378,8 @@ function buildRunProfile(options = {}) {
|
|
|
264
378
|
function formatRunProfile(profile, options = {}) {
|
|
265
379
|
const rows = [
|
|
266
380
|
['mode', profile.mode],
|
|
381
|
+
['agent', profile.member_slug || MEMBER_SLUG],
|
|
382
|
+
['permissions', profile.bypass_permissions ? 'bypass' : 'safe'],
|
|
267
383
|
['endpoint', profile.endpoint],
|
|
268
384
|
['route', profile.route || 'auto'],
|
|
269
385
|
['workspace', formatPathSubject(profile.workspace_path, options)],
|
|
@@ -307,9 +423,9 @@ function authUserId() {
|
|
|
307
423
|
}
|
|
308
424
|
}
|
|
309
425
|
|
|
310
|
-
function isLoopbackBackend() {
|
|
426
|
+
function isLoopbackBackend(options = {}) {
|
|
311
427
|
try {
|
|
312
|
-
const parsed = new URL(backendBaseUrl());
|
|
428
|
+
const parsed = new URL(backendBaseUrl(options));
|
|
313
429
|
return ['127.0.0.1', 'localhost', '::1'].includes(parsed.hostname);
|
|
314
430
|
} catch (_) {
|
|
315
431
|
return false;
|
|
@@ -491,24 +607,220 @@ function githubWorkspaceIntent(message) {
|
|
|
491
607
|
return /\b(push|commit|commits?|branch|branches|checkout|merge|rebase|tag|release|pr|pull request|pull-request|repo change|code change|small change)\b/i.test(text);
|
|
492
608
|
}
|
|
493
609
|
|
|
610
|
+
function casualChatIntent(message) {
|
|
611
|
+
const text = String(message || '').trim();
|
|
612
|
+
if (!text) return false;
|
|
613
|
+
if (workspaceIntent(text) || mentionsConnector(text) || githubWorkspaceIntent(text)) return false;
|
|
614
|
+
if (/^(hi+|hello+|hey+|yo+|sup|gm|good morning|good afternoon|good evening|thanks?|thank you|ok+|okay|k|cool|nice|lol|lmao|haha|why\??|what\??|huh\??|yes|no|nah|yep|nope)[.!?]*$/i.test(text)) return true;
|
|
615
|
+
if (/^(hi|hello|hey)\b/i.test(text)) return true;
|
|
616
|
+
return /^[a-z]{1,8}[.!?]*$/i.test(text);
|
|
617
|
+
}
|
|
618
|
+
|
|
494
619
|
function resolveRoute(message, options = {}) {
|
|
495
620
|
if (options.route === 'local' || options.forceLocal) return 'local';
|
|
496
621
|
if (options.route === 'cloud' || options.forceCloud) return 'cloud';
|
|
497
|
-
if (
|
|
622
|
+
if (casualChatIntent(message)) return 'cloud';
|
|
623
|
+
const localWorkspace = hasLocalWorkspaceBackend(options);
|
|
624
|
+
if (githubWorkspaceIntent(message) && localWorkspace) return 'local';
|
|
498
625
|
if (mentionsConnector(message) && !workspaceIntent(message)) return 'cloud';
|
|
626
|
+
if (!localWorkspace) return 'cloud';
|
|
499
627
|
return 'local';
|
|
500
628
|
}
|
|
501
629
|
|
|
502
630
|
function normalizeMode(mode) {
|
|
503
631
|
if (mode === 'code-fast' || mode === 'code') return 'code-fast';
|
|
504
632
|
if (mode === 'max') return 'max';
|
|
505
|
-
|
|
633
|
+
if (mode === 'pro') return 'pro';
|
|
634
|
+
return 'fast';
|
|
506
635
|
}
|
|
507
636
|
|
|
508
637
|
function formatPrompt(mode, options = {}) {
|
|
509
|
-
if (!mode) return '› ';
|
|
510
638
|
const tier = normalizeMode(mode);
|
|
511
|
-
|
|
639
|
+
const goalHint = options.goal && options.goal.active
|
|
640
|
+
? `${paint('◎ ', [ANSI.bold, ANSI.magenta], options)}`
|
|
641
|
+
: '';
|
|
642
|
+
if (!mode) return `${goalHint}› `;
|
|
643
|
+
return `${goalHint}${paint(tier, [ANSI.bold, tierColor(tier)], options)} › `;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function terminalWidth(options = {}) {
|
|
647
|
+
const cols = Number(options.columns || process.stdout.columns || 0);
|
|
648
|
+
if (Number.isFinite(cols) && cols >= 48) return cols;
|
|
649
|
+
return 72;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function inputBoxPlainWidth(options = {}) {
|
|
653
|
+
const cols = Number(options.columns || process.stdout.columns || 0);
|
|
654
|
+
if (Number.isFinite(cols) && cols >= 48) return cols - 1;
|
|
655
|
+
return 71;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function inputBoxLayoutOptions(stream, overrides = {}) {
|
|
659
|
+
const cols = Number(stream?.columns || process.stdout.columns || 0);
|
|
660
|
+
return {
|
|
661
|
+
...overrides,
|
|
662
|
+
columns: Number.isFinite(cols) && cols >= 48 ? cols - 1 : 71,
|
|
663
|
+
isTTY: stream?.isTTY ?? process.stdout.isTTY,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function buildInputBoxTopPlain(options = {}) {
|
|
668
|
+
const width = inputBoxPlainWidth(options);
|
|
669
|
+
return `╭${'─'.repeat(Math.max(0, width - 2))}╮`;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// The mode + newline affordance lives on its own line BELOW the box (claude
|
|
673
|
+
// style), so the rules above/below stay clean and we never say "enter send".
|
|
674
|
+
function buildInputBoxStatusPlain(options = {}) {
|
|
675
|
+
return ` ${formatPermissionModeBrief(options)} · shift+enter`;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function buildInputBoxBottomPlain(options = {}) {
|
|
679
|
+
const width = inputBoxPlainWidth(options);
|
|
680
|
+
return `╰${'─'.repeat(Math.max(0, width - 2))}╯`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function formatInputBoxInputRow(prompt, line, options = {}) {
|
|
684
|
+
const width = inputBoxPlainWidth(options);
|
|
685
|
+
const promptPlain = stripAnsi(prompt);
|
|
686
|
+
const linePlain = stripAnsi(String(line || ''));
|
|
687
|
+
const rightBar = paint('│', [ANSI.dim, ANSI.muted], options);
|
|
688
|
+
const rightPlain = '│';
|
|
689
|
+
const pad = Math.max(0, width - promptPlain.length - linePlain.length - rightPlain.length);
|
|
690
|
+
return `${prompt}${line}${' '.repeat(pad)}${rightBar}`;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function formatPermissionModeBrief(options = {}) {
|
|
694
|
+
return resolveBypassPermissions(options) ? 'full access' : 'approve';
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function permissionAccentCodes(options = {}) {
|
|
698
|
+
return resolveBypassPermissions(options) ? [ANSI.dim, ANSI.warn] : [ANSI.dim, ANSI.safe];
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function formatPermissionToggleMessage(bypassPermissions, options = {}) {
|
|
702
|
+
const persistPrefs = options.persistPrefs !== false;
|
|
703
|
+
const suffix = persistPrefs ? ' (saved)' : ' (session)';
|
|
704
|
+
if (bypassPermissions) {
|
|
705
|
+
return paint(`· Full access${suffix} — external actions can run without approval`, [ANSI.warn], options);
|
|
706
|
+
}
|
|
707
|
+
return paint(`· Approve mode${suffix} — external actions ask before they run`, [ANSI.safe], options);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function isPermissionToggleKey(key) {
|
|
711
|
+
if (!key) return false;
|
|
712
|
+
if (key.shift && key.name === 'tab') return true;
|
|
713
|
+
const sequence = String(key.sequence || '');
|
|
714
|
+
return sequence === '\x1b[Z' || sequence === '\x1b[1;2Z';
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function isMultilineInsertKey(str, key) {
|
|
718
|
+
if (!key) return str === '\n';
|
|
719
|
+
if (key.shift && (key.name === 'return' || key.name === 'enter')) return true;
|
|
720
|
+
if (key.meta && (key.name === 'return' || key.name === 'enter')) return true;
|
|
721
|
+
if (str === '\n' && key.name !== 'return') return true;
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function insertMultilineBreak(rl) {
|
|
726
|
+
if (!rl) return;
|
|
727
|
+
// Insert a literal newline at the cursor. rl.write('\n') would SUBMIT the
|
|
728
|
+
// line (readline treats it as Enter) and also re-enter our _ttyWrite hook.
|
|
729
|
+
if (typeof rl._insertString === 'function') {
|
|
730
|
+
rl._insertString('\n');
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (typeof rl.line === 'string') {
|
|
734
|
+
const cursor = Number.isFinite(rl.cursor) ? rl.cursor : rl.line.length;
|
|
735
|
+
rl.line = rl.line.slice(0, cursor) + '\n' + rl.line.slice(cursor);
|
|
736
|
+
rl.cursor = cursor + 1;
|
|
737
|
+
if (typeof rl._refreshLine === 'function') rl._refreshLine();
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function formatInputBoxTop(options = {}) {
|
|
742
|
+
return paint(buildInputBoxTopPlain(options), [ANSI.dim, ANSI.muted], options);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function formatInputBoxBottom(options = {}) {
|
|
746
|
+
return paint(buildInputBoxBottomPlain(options), [ANSI.dim, ANSI.muted], options);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Status line shown below the box: permission mode + the shift+enter newline
|
|
750
|
+
// affordance, mode-colored. Drawn by the repaint, cleared on submit.
|
|
751
|
+
function formatInputBoxStatus(options = {}) {
|
|
752
|
+
const plain = buildInputBoxStatusPlain(options);
|
|
753
|
+
const modeLabel = formatPermissionModeBrief(options);
|
|
754
|
+
const idx = plain.indexOf(modeLabel);
|
|
755
|
+
if (idx === -1) return paint(plain, [ANSI.dim, ANSI.muted], options);
|
|
756
|
+
const modePainted = paint(modeLabel, permissionAccentCodes(options), options);
|
|
757
|
+
const dim = [ANSI.dim, ANSI.muted];
|
|
758
|
+
return `${paint(plain.slice(0, idx), dim, options)}${modePainted}${paint(plain.slice(idx + modeLabel.length), dim, options)}`;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function closeInputBox(output, options = {}) {
|
|
762
|
+
// On submit the cursor sits on the bottom-rule row: redraw the rule, then
|
|
763
|
+
// clear the status line the repaint left below it and leave the cursor on a
|
|
764
|
+
// fresh row for the turn output.
|
|
765
|
+
output.write(`\r\x1b[2K${formatInputBoxBottom(options)}\n\x1b[2K\r`);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Keep the input box closed while the user is typing: after each readline
|
|
769
|
+
// render, repaint the bottom rule one row below the (possibly wrapped/multiline)
|
|
770
|
+
// input plus a status line below that, then return the cursor. readline clears
|
|
771
|
+
// to end-of-display on every refresh, so both must be redrawn each time. Writes
|
|
772
|
+
// to the raw stream readline renders to (not the logger) since these are
|
|
773
|
+
// transient cursor moves.
|
|
774
|
+
function repaintInputBoxBottom(rl, output, options = {}) {
|
|
775
|
+
if (!rl || !output || typeof output.write !== 'function') return;
|
|
776
|
+
if (typeof rl._getDisplayPos !== 'function' || typeof rl.getCursorPos !== 'function') return;
|
|
777
|
+
let total;
|
|
778
|
+
let cur;
|
|
779
|
+
try {
|
|
780
|
+
total = rl._getDisplayPos(`${rl._prompt || ''}${rl.line || ''}`);
|
|
781
|
+
cur = rl.getCursorPos();
|
|
782
|
+
} catch {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (!total || !cur) return;
|
|
786
|
+
// When the input is taller than the viewport the terminal scrolls and the
|
|
787
|
+
// relative cursor math below can't land the rule reliably (it would corrupt
|
|
788
|
+
// the scrollback). Degrade gracefully: skip the closing rule for a giant
|
|
789
|
+
// input rather than scramble it. readline still renders the text correctly.
|
|
790
|
+
const rows = Number(output.rows || (typeof process !== 'undefined' && process.stdout && process.stdout.rows) || 0);
|
|
791
|
+
if (rows && total.rows + 3 >= rows) return;
|
|
792
|
+
const down = Math.max(1, (total.rows - cur.rows) + 1);
|
|
793
|
+
let seq = `\r\x1b[${down}B\x1b[2K${formatInputBoxBottom(options)}`;
|
|
794
|
+
seq += `\r\x1b[1B\x1b[2K${formatInputBoxStatus(options)}`;
|
|
795
|
+
seq += `\x1b[${down + 1}A\r`;
|
|
796
|
+
if (cur.cols > 0) seq += `\x1b[${cur.cols}C`;
|
|
797
|
+
output.write(seq);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function formatChatInputInnerPrefix(mode, options = {}) {
|
|
801
|
+
const tier = normalizeMode(mode);
|
|
802
|
+
const goalHint = options.goal && options.goal.active
|
|
803
|
+
? `${paint('◎ ', [ANSI.bold, ANSI.magenta], options)}`
|
|
804
|
+
: '';
|
|
805
|
+
const tierPart = mode
|
|
806
|
+
? `${goalHint}${paint(tier, [ANSI.bold, tierColor(tier)], options)} › `
|
|
807
|
+
: `${goalHint}› `;
|
|
808
|
+
return `${paint('→ ', [ANSI.muted], options)}${tierPart}`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function formatChatInputPrompt(mode, options = {}) {
|
|
812
|
+
// Two-space indent (no left bar) to match the plain rules and the status line.
|
|
813
|
+
return ` ${formatChatInputInnerPrefix(mode, options)}`;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const PERMISSION_COMMANDS = new Map([
|
|
817
|
+
['/bypass', true],
|
|
818
|
+
['/safe', false],
|
|
819
|
+
]);
|
|
820
|
+
|
|
821
|
+
function chatPermissionCommand(line) {
|
|
822
|
+
const key = String(line || '').trim().toLowerCase();
|
|
823
|
+
return PERMISSION_COMMANDS.has(key) ? PERMISSION_COMMANDS.get(key) : null;
|
|
512
824
|
}
|
|
513
825
|
|
|
514
826
|
const TIER_COMMANDS = new Map([
|
|
@@ -522,21 +834,70 @@ function chatTierCommand(line) {
|
|
|
522
834
|
}
|
|
523
835
|
|
|
524
836
|
const CHAT_COMMANDS = [
|
|
837
|
+
['/goal', 'set/show autonomous completion condition'],
|
|
525
838
|
['/fast', 'quick answers, lowest latency'],
|
|
526
839
|
['/pro', 'deeper tool loop for real work'],
|
|
527
840
|
['/max', 'highest reasoning for the hardest jobs'],
|
|
841
|
+
['/bypass', 'allow external side effects (persisted)'],
|
|
842
|
+
['/safe', 'approval-only external actions (default, persisted)'],
|
|
528
843
|
['/help', 'show this menu'],
|
|
529
844
|
['exit', 'leave chat'],
|
|
530
845
|
];
|
|
531
846
|
|
|
847
|
+
const CHAT_SHORTCUTS = [
|
|
848
|
+
['shift+tab', 'toggle approve ↔ full access'],
|
|
849
|
+
['shift+enter', 'new line (enter sends)'],
|
|
850
|
+
];
|
|
851
|
+
|
|
852
|
+
const CHAT_DOGFOOD_BASE_CASES = [
|
|
853
|
+
{ input: '/help', kind: 'command', coverage: 'menu' },
|
|
854
|
+
{ input: '/fast', kind: 'command', coverage: 'tier_fast' },
|
|
855
|
+
{ input: 'hi', kind: 'turn', expectRoute: 'cloud', coverage: 'small_talk' },
|
|
856
|
+
{ input: 'pop', kind: 'turn', expectRoute: 'cloud', coverage: 'short_noise' },
|
|
857
|
+
{ input: 'hello why', kind: 'turn', expectRoute: 'cloud', coverage: 'greeting_phrase' },
|
|
858
|
+
{ input: 'what files are here?', kind: 'turn', expectRoute: 'cloud', coverage: 'workspace_read' },
|
|
859
|
+
{ input: 'search src for the input component', kind: 'turn', expectRoute: 'cloud', coverage: 'workspace_search' },
|
|
860
|
+
{ input: 'fix the xp game tests', kind: 'turn', expectRoute: 'cloud', coverage: 'workspace_fix' },
|
|
861
|
+
{ input: 'what is on my calendar today?', kind: 'turn', expectRoute: 'cloud', coverage: 'connector_read' },
|
|
862
|
+
{ input: 'which integrations are connected?', kind: 'turn', expectRoute: 'cloud', coverage: 'connector_status' },
|
|
863
|
+
{ input: 'what github repos do I have?', kind: 'turn', expectRoute: 'cloud', coverage: 'github_read' },
|
|
864
|
+
{ input: 'push something to github', kind: 'turn', expectRoute: 'cloud', coverage: 'github_workspace_mutation' },
|
|
865
|
+
{ input: 'https://www.youtube.com/watch?v=gBukk9LIklc', kind: 'youtube', coverage: 'youtube_url' },
|
|
866
|
+
{ input: '/max', kind: 'command', coverage: 'tier_max' },
|
|
867
|
+
{ input: 'refactor this module and verify the tests', kind: 'turn', expectRoute: 'cloud', expectMode: 'max', coverage: 'max_cloud' },
|
|
868
|
+
{ input: '/pro', kind: 'command', coverage: 'tier_pro' },
|
|
869
|
+
{ input: 'send a slack message to the team', kind: 'turn', expectRoute: 'cloud', expectMode: 'pro', coverage: 'connector_write_safe' },
|
|
870
|
+
{ input: '/bypass', kind: 'command', coverage: 'permission_bypass' },
|
|
871
|
+
{ input: 'send a slack message to the team', kind: 'turn', expectRoute: 'cloud', expectBypass: true, coverage: 'connector_write_bypass' },
|
|
872
|
+
{ input: '/safe', kind: 'command', coverage: 'permission_safe' },
|
|
873
|
+
{ input: 'send a slack message to the team', kind: 'turn', expectRoute: 'cloud', expectBypass: false, coverage: 'connector_write_resafe' },
|
|
874
|
+
{ input: '/wat', kind: 'command', coverage: 'unknown_slash' },
|
|
875
|
+
{ input: '/goal', kind: 'command', coverage: 'goal_status' },
|
|
876
|
+
{ input: 'oj', kind: 'turn', expectRoute: 'cloud', coverage: 'short_noise_repeat' },
|
|
877
|
+
{ input: 'read MAP.md and do not edit', kind: 'turn', expectRoute: 'cloud', coverage: 'workspace_read_specific' },
|
|
878
|
+
];
|
|
879
|
+
|
|
880
|
+
function buildChatDogfoodCases(loopCount = 25) {
|
|
881
|
+
const count = Math.max(1, Number(loopCount) || 25);
|
|
882
|
+
const cases = [];
|
|
883
|
+
for (let i = 0; i < count; i += 1) {
|
|
884
|
+
cases.push({ ...CHAT_DOGFOOD_BASE_CASES[i % CHAT_DOGFOOD_BASE_CASES.length], index: i + 1 });
|
|
885
|
+
}
|
|
886
|
+
return cases;
|
|
887
|
+
}
|
|
888
|
+
|
|
532
889
|
function chatMenu(options = {}) {
|
|
533
|
-
|
|
890
|
+
const commands = CHAT_COMMANDS
|
|
534
891
|
.map(([name, desc]) => {
|
|
535
892
|
const tier = TIER_COMMANDS.get(name);
|
|
536
893
|
const color = tier ? tierColor(tier) : ANSI.accent;
|
|
537
894
|
return ` ${paint(name.padEnd(6), [color], options)} ${paint(desc, [ANSI.muted], options)}`;
|
|
538
895
|
})
|
|
539
896
|
.join('\n');
|
|
897
|
+
const shortcuts = CHAT_SHORTCUTS
|
|
898
|
+
.map(([name, desc]) => ` ${paint(name.padEnd(6), [ANSI.ok], options)} ${paint(desc, [ANSI.muted], options)}`)
|
|
899
|
+
.join('\n');
|
|
900
|
+
return `${commands}\n${shortcuts}`;
|
|
540
901
|
}
|
|
541
902
|
|
|
542
903
|
function chatCompleter(line) {
|
|
@@ -547,10 +908,132 @@ function chatCompleter(line) {
|
|
|
547
908
|
return [hits.length ? hits : names, trimmed];
|
|
548
909
|
}
|
|
549
910
|
|
|
550
|
-
function
|
|
911
|
+
function parsePermissionFlags(args) {
|
|
912
|
+
let bypassPermissions;
|
|
913
|
+
if (args.includes('--bypass')) bypassPermissions = true;
|
|
914
|
+
if (args.includes('--safe')) bypassPermissions = false;
|
|
915
|
+
return bypassPermissions;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function stripPermissionFlags(args) {
|
|
919
|
+
return args.filter(arg => arg !== '--bypass' && arg !== '--safe');
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function normalizeChatCommandArgs(args = []) {
|
|
923
|
+
const normalized = [...args];
|
|
924
|
+
let firstPositional = -1;
|
|
925
|
+
const flagsWithValue = new Set(['--business', '--verify', '--loops']);
|
|
926
|
+
|
|
927
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
928
|
+
const arg = String(normalized[i] || '');
|
|
929
|
+
if (flagsWithValue.has(arg)) {
|
|
930
|
+
i += 1;
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (arg.startsWith('-')) continue;
|
|
934
|
+
firstPositional = i;
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
let chatRequested = normalized.includes('--chat');
|
|
939
|
+
if (firstPositional !== -1 && normalized[firstPositional] === 'chat') {
|
|
940
|
+
chatRequested = true;
|
|
941
|
+
normalized.splice(firstPositional, 1, '--chat');
|
|
942
|
+
const target = String(normalized[firstPositional + 1] || '').toLowerCase();
|
|
943
|
+
if (target === MEMBER_SLUG || target === 'ax') {
|
|
944
|
+
normalized.splice(firstPositional + 1, 1);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return { args: normalized, chatRequested };
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function isAxSpawnCommand(command) {
|
|
952
|
+
return ['spawn', 'spawns', 'spawn-list', 'list-spawns', 'spawn-status', 'spawn-show'].includes(String(command || ''));
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function runAxSpawnCommand(args = [], deps = {}) {
|
|
956
|
+
const [command, ...rest] = args;
|
|
957
|
+
const root = deps.root || process.cwd();
|
|
958
|
+
const output = deps.output || ((line = '') => console.log(line));
|
|
959
|
+
const {
|
|
960
|
+
agentSpawnCommand,
|
|
961
|
+
agentSpawnListCommand,
|
|
962
|
+
agentSpawnStatusCommand,
|
|
963
|
+
} = require('./commands/agent-spawn');
|
|
964
|
+
|
|
965
|
+
if (command === 'spawn') return agentSpawnCommand(rest, { root, output, commandName: 'ax spawn' });
|
|
966
|
+
if (command === 'spawns' || command === 'spawn-list' || command === 'list-spawns') {
|
|
967
|
+
return agentSpawnListCommand(rest, { root, output });
|
|
968
|
+
}
|
|
969
|
+
if (command === 'spawn-status' || command === 'spawn-show') {
|
|
970
|
+
return agentSpawnStatusCommand(rest, { root, output });
|
|
971
|
+
}
|
|
972
|
+
throw new Error('Unknown ax spawn command');
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const YOUTUBE_URL_PATTERN = /\bhttps?:\/\/(?:www\.)?(?:youtube\.com\/watch\?[^\s]+|youtu\.be\/[^\s]+|youtube\.com\/shorts\/[^\s]+)/i;
|
|
976
|
+
|
|
977
|
+
function cleanYoutubeUrl(url) {
|
|
978
|
+
return String(url || '').replace(/[),.;\]]+$/g, '');
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function extractYoutubeUrl(text) {
|
|
982
|
+
const match = String(text || '').match(YOUTUBE_URL_PATTERN);
|
|
983
|
+
return match ? cleanYoutubeUrl(match[0]) : null;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function youtubeQueryFromPrompt(prompt, youtubeUrl) {
|
|
987
|
+
const withoutUrl = String(prompt || '').replace(youtubeUrl, '').trim();
|
|
988
|
+
return withoutUrl.replace(/^[-:,\s]+|[-:,\s]+$/g, '').trim();
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function buildAxYoutubeArgs(prompt, extraArgs = []) {
|
|
992
|
+
const text = Array.isArray(prompt) ? prompt.join(' ') : String(prompt || '');
|
|
993
|
+
const youtubeUrl = extractYoutubeUrl(text);
|
|
994
|
+
if (!youtubeUrl) return null;
|
|
995
|
+
const query = youtubeQueryFromPrompt(text, youtubeUrl);
|
|
996
|
+
const args = [youtubeUrl];
|
|
997
|
+
if (query) args.push('--query', query);
|
|
998
|
+
return args.concat(extraArgs || []);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
async function runAxYoutubeCommand(args = [], deps = {}) {
|
|
1002
|
+
const output = deps.output || ((line = '') => console.log(line));
|
|
1003
|
+
const commandArgs = args[0] === 'youtube' ? args.slice(1) : args;
|
|
1004
|
+
const inferred = args[0] === 'youtube' ? null : buildAxYoutubeArgs(commandArgs);
|
|
1005
|
+
const finalArgs = inferred || commandArgs;
|
|
1006
|
+
const youtubeCommand = deps.youtubeCommand || require('./commands/youtube').youtubeCommand;
|
|
1007
|
+
const commandName = args[0] === 'youtube' ? 'ax youtube' : deps.commandName;
|
|
1008
|
+
return youtubeCommand(finalArgs, { ...deps, output, commandName });
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function normalizeWorkingOptions(frameOrOptions, maybeOptions = {}) {
|
|
1012
|
+
if (typeof frameOrOptions === 'string') {
|
|
1013
|
+
return { ...maybeOptions, frameChar: frameOrOptions };
|
|
1014
|
+
}
|
|
1015
|
+
if (frameOrOptions && typeof frameOrOptions === 'object') {
|
|
1016
|
+
return frameOrOptions;
|
|
1017
|
+
}
|
|
1018
|
+
return maybeOptions;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function formatWorkingLine(ms, verb, frameOrOptions, maybeOptions = {}) {
|
|
1022
|
+
const options = normalizeWorkingOptions(frameOrOptions, maybeOptions);
|
|
551
1023
|
const totalSeconds = Math.max(1, Math.round((Number(ms) || 0) / 1000));
|
|
552
|
-
|
|
553
|
-
|
|
1024
|
+
const tick = Number.isFinite(options.tick) ? options.tick : null;
|
|
1025
|
+
const labelVerb = String(verb || 'Thinking').trim() || 'Thinking';
|
|
1026
|
+
const meta = paint(
|
|
1027
|
+
`(${formatSeconds(totalSeconds)} · ctrl-c to interrupt)`,
|
|
1028
|
+
[ANSI.dim, ANSI.muted],
|
|
1029
|
+
options
|
|
1030
|
+
);
|
|
1031
|
+
const label = tick === null
|
|
1032
|
+
? paint(labelVerb, [ANSI.muted], options)
|
|
1033
|
+
: renderShimmerText(labelVerb, tick ?? 0, options);
|
|
1034
|
+
const suffix = paint('…', [ANSI.dim, ANSI.muted], options);
|
|
1035
|
+
const prefix = paint('•', [ANSI.muted], options);
|
|
1036
|
+
return `${prefix} ${label}${suffix} ${meta}`;
|
|
554
1037
|
}
|
|
555
1038
|
|
|
556
1039
|
function verbForTool(tool) {
|
|
@@ -559,7 +1042,8 @@ function verbForTool(tool) {
|
|
|
559
1042
|
if (name.includes('grep') || name.includes('glob') || name.includes('search')) return 'Searching';
|
|
560
1043
|
if (name.includes('bash') || name.includes('command') || name.includes('run')) return 'Running';
|
|
561
1044
|
if (name.includes('write') || name.includes('edit')) return 'Editing';
|
|
562
|
-
return '
|
|
1045
|
+
if (name.includes('think') || name.includes('reason') || name === 'task') return 'Thinking';
|
|
1046
|
+
return 'Thinking';
|
|
563
1047
|
}
|
|
564
1048
|
|
|
565
1049
|
function formatDoneLine(ms, credits) {
|
|
@@ -581,7 +1065,18 @@ function paint(text, codes, options = {}) {
|
|
|
581
1065
|
return `${codes.join('')}${text}${ANSI.reset}`;
|
|
582
1066
|
}
|
|
583
1067
|
|
|
584
|
-
function
|
|
1068
|
+
function formatMarkdownLink(label, url, options = {}) {
|
|
1069
|
+
const display = String(label || '').trim();
|
|
1070
|
+
const href = String(url || '').trim();
|
|
1071
|
+
if (!href) return display;
|
|
1072
|
+
const colored = paint(display, [ANSI.accent, ANSI.underline], options);
|
|
1073
|
+
if (useColor(options) && (/^(https?:\/\/|file:\/\/)/i.test(href) || href.startsWith('/'))) {
|
|
1074
|
+
return hyperlinkPath(colored, href, options);
|
|
1075
|
+
}
|
|
1076
|
+
return `${display} (${href})`;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function renderTerminalInline(text, options = {}) {
|
|
585
1080
|
let rendered = String(text || '');
|
|
586
1081
|
const codeSpans = [];
|
|
587
1082
|
rendered = rendered.replace(/`([^`\n]+)`/g, (_, code) => {
|
|
@@ -589,23 +1084,131 @@ function renderTerminalMarkdown(text, options = {}) {
|
|
|
589
1084
|
codeSpans.push(code);
|
|
590
1085
|
return token;
|
|
591
1086
|
});
|
|
592
|
-
rendered = rendered.replace(
|
|
1087
|
+
rendered = rendered.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => formatMarkdownLink(label, url, options));
|
|
1088
|
+
rendered = rendered.replace(/\*\*\*([^*\n]+)\*\*\*/g, (_, value) => paint(value, [ANSI.bold, ANSI.italic], options));
|
|
593
1089
|
rendered = rendered.replace(/\*\*([^*\n]+)\*\*/g, (_, value) => paint(value, [ANSI.bold], options));
|
|
594
1090
|
rendered = rendered.replace(/__([^_\n]+)__/g, (_, value) => paint(value, [ANSI.bold], options));
|
|
1091
|
+
rendered = rendered.replace(/~~([^~\n]+)~~/g, (_, value) => paint(value, [ANSI.strike], options));
|
|
1092
|
+
rendered = rendered.replace(/\*([^*\n]+)\*/g, (_, value) => paint(value, [ANSI.italic], options));
|
|
1093
|
+
rendered = rendered.replace(/_([^_\n]+)_/g, (_, value) => paint(value, [ANSI.italic], options));
|
|
595
1094
|
rendered = rendered.replace(/\u0000CODE(\d+)\u0000/g, (_, index) => paint(codeSpans[Number(index)] || '', [ANSI.accent], options));
|
|
596
1095
|
return rendered;
|
|
597
1096
|
}
|
|
598
1097
|
|
|
1098
|
+
function renderTerminalBlockLine(line, options = {}) {
|
|
1099
|
+
const header = line.match(/^(#{1,6})\s+(.+)$/);
|
|
1100
|
+
if (header) return paint(header[2], [ANSI.bold], options);
|
|
1101
|
+
|
|
1102
|
+
const list = line.match(/^(\s*)(?:[*+-]|\d+\.)\s+(.*)$/);
|
|
1103
|
+
if (list) {
|
|
1104
|
+
const bullet = paint('•', [ANSI.accent], options);
|
|
1105
|
+
return `${list[1]}${bullet} ${renderTerminalInline(list[2], options)}`;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const quote = line.match(/^>\s?(.*)$/);
|
|
1109
|
+
if (quote) {
|
|
1110
|
+
return `${paint('▏ ', [ANSI.muted], options)}${renderTerminalInline(quote[1], options)}`;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (/^(\*{3,}|-{3,}|_{3,})$/.test(line.trim())) {
|
|
1114
|
+
const width = Math.min(Math.max(16, terminalWidth(options) - 4), 48);
|
|
1115
|
+
return paint('─'.repeat(width), [ANSI.dim, ANSI.muted], options);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
return renderTerminalInline(line, options);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function renderTerminalMarkdown(text, options = {}) {
|
|
1122
|
+
const source = String(text || '');
|
|
1123
|
+
const parts = [];
|
|
1124
|
+
const fence = /```[^\n]*\n([\s\S]*?)```/g;
|
|
1125
|
+
let last = 0;
|
|
1126
|
+
let match = fence.exec(source);
|
|
1127
|
+
while (match) {
|
|
1128
|
+
if (match.index > last) {
|
|
1129
|
+
parts.push({ kind: 'text', value: source.slice(last, match.index) });
|
|
1130
|
+
}
|
|
1131
|
+
parts.push({ kind: 'code', value: match[1] || '' });
|
|
1132
|
+
last = match.index + match[0].length;
|
|
1133
|
+
match = fence.exec(source);
|
|
1134
|
+
}
|
|
1135
|
+
if (last < source.length) parts.push({ kind: 'text', value: source.slice(last) });
|
|
1136
|
+
|
|
1137
|
+
return parts.map((part) => {
|
|
1138
|
+
if (part.kind === 'code') {
|
|
1139
|
+
return part.value
|
|
1140
|
+
.replace(/\n$/, '')
|
|
1141
|
+
.split('\n')
|
|
1142
|
+
.map((line) => paint(` ${line}`, [ANSI.muted], options))
|
|
1143
|
+
.join('\n');
|
|
1144
|
+
}
|
|
1145
|
+
return part.value
|
|
1146
|
+
.split('\n')
|
|
1147
|
+
.map((line) => renderTerminalBlockLine(line, options))
|
|
1148
|
+
.join('\n');
|
|
1149
|
+
}).join('\n');
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function renderFenceBuffer(buffer, options = {}) {
|
|
1153
|
+
return String(buffer || '')
|
|
1154
|
+
.replace(/\n$/, '')
|
|
1155
|
+
.split('\n')
|
|
1156
|
+
.map((line) => paint(` ${line}`, [ANSI.muted], options))
|
|
1157
|
+
.join('\n');
|
|
1158
|
+
}
|
|
1159
|
+
|
|
599
1160
|
function resetMarkdownState(state) {
|
|
600
1161
|
state.markdownMode = 'normal';
|
|
601
1162
|
state.markdownBuffer = '';
|
|
602
1163
|
state.markdownCarry = '';
|
|
1164
|
+
state.markdownDelim = '';
|
|
1165
|
+
state.atLineStart = true;
|
|
603
1166
|
}
|
|
604
1167
|
|
|
605
1168
|
function ensureMarkdownState(state) {
|
|
606
1169
|
if (!state.markdownMode) state.markdownMode = 'normal';
|
|
607
1170
|
if (typeof state.markdownBuffer !== 'string') state.markdownBuffer = '';
|
|
608
1171
|
if (typeof state.markdownCarry !== 'string') state.markdownCarry = '';
|
|
1172
|
+
if (typeof state.markdownDelim !== 'string') state.markdownDelim = '';
|
|
1173
|
+
if (typeof state.atLineStart !== 'boolean') state.atLineStart = true;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function streamingListMarkerAt(input, index) {
|
|
1177
|
+
const rest = input.slice(index);
|
|
1178
|
+
const match = rest.match(/^(\s*)(?:[*+-]|\d+\.)\s+/);
|
|
1179
|
+
if (!match) return null;
|
|
1180
|
+
return { length: match[0].length, indent: match[1] || '' };
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function streamingHeadingAt(input, index) {
|
|
1184
|
+
const rest = input.slice(index);
|
|
1185
|
+
const match = rest.match(/^#{1,6}\s+([^\n]*)/);
|
|
1186
|
+
if (!match) return null;
|
|
1187
|
+
return { length: match[0].length, title: match[1] || '' };
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function streamingBlockquoteAt(input, index) {
|
|
1191
|
+
const rest = input.slice(index);
|
|
1192
|
+
const match = rest.match(/^>\s?/);
|
|
1193
|
+
if (!match) return null;
|
|
1194
|
+
return { length: match[0].length };
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function streamingHorizontalRuleAt(input, index) {
|
|
1198
|
+
const rest = input.slice(index);
|
|
1199
|
+
const match = rest.match(/^(-{3,}|\*{3,}|_{3,})(?=\n|$)/);
|
|
1200
|
+
if (!match) return null;
|
|
1201
|
+
return { length: match[0].length };
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function streamingLinkAt(input, index, options) {
|
|
1205
|
+
const rest = input.slice(index);
|
|
1206
|
+
const match = rest.match(/^\[([^\]]+)\]\(([^)]+)\)/);
|
|
1207
|
+
if (!match) return null;
|
|
1208
|
+
return {
|
|
1209
|
+
length: match[0].length,
|
|
1210
|
+
rendered: formatMarkdownLink(match[1], match[2], options),
|
|
1211
|
+
};
|
|
609
1212
|
}
|
|
610
1213
|
|
|
611
1214
|
function renderStreamingMarkdown(state, text, options = {}) {
|
|
@@ -615,43 +1218,239 @@ function renderStreamingMarkdown(state, text, options = {}) {
|
|
|
615
1218
|
let out = '';
|
|
616
1219
|
|
|
617
1220
|
for (let i = 0; i < input.length;) {
|
|
1221
|
+
if (state.markdownMode === 'fence') {
|
|
1222
|
+
if (state.atLineStart) {
|
|
1223
|
+
const rest = input.slice(i);
|
|
1224
|
+
if (/^```/.test(rest)) {
|
|
1225
|
+
out += renderFenceBuffer(state.markdownBuffer, options);
|
|
1226
|
+
state.markdownBuffer = '';
|
|
1227
|
+
state.markdownMode = 'normal';
|
|
1228
|
+
const nl = rest.indexOf('\n');
|
|
1229
|
+
if (nl === -1) { i = input.length; state.atLineStart = true; continue; }
|
|
1230
|
+
out += '\n';
|
|
1231
|
+
i += nl + 1;
|
|
1232
|
+
state.atLineStart = true;
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
if (/^`{1,3}$/.test(rest)) { state.markdownCarry = rest; break; }
|
|
1236
|
+
}
|
|
1237
|
+
const c = input[i];
|
|
1238
|
+
state.markdownBuffer += c;
|
|
1239
|
+
state.atLineStart = c === '\n';
|
|
1240
|
+
i += 1;
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
if (state.markdownMode === 'normal' && state.atLineStart) {
|
|
1245
|
+
const rest = input.slice(i);
|
|
1246
|
+
// Code fence ```lang … ``` — wait for the full opening line, then stream
|
|
1247
|
+
// the body as an indented block (handled by the 'fence' branch above).
|
|
1248
|
+
if (/^```/.test(rest)) {
|
|
1249
|
+
const nl = rest.indexOf('\n');
|
|
1250
|
+
if (nl === -1) { state.markdownCarry = rest; break; }
|
|
1251
|
+
state.markdownMode = 'fence';
|
|
1252
|
+
state.markdownBuffer = '';
|
|
1253
|
+
state.atLineStart = true;
|
|
1254
|
+
i += nl + 1;
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
// Incomplete block marker at the end of this delta (heading ##, fence ```,
|
|
1258
|
+
// bullet - / +, numbered 1.) — carry it so the next delta completes the
|
|
1259
|
+
// marker instead of leaking it as plain text.
|
|
1260
|
+
if (/^(?:#{1,6}|`{1,3}|[-+]|\d{1,9}\.)$/.test(rest)) { state.markdownCarry = rest; break; }
|
|
1261
|
+
|
|
1262
|
+
const heading = streamingHeadingAt(input, i);
|
|
1263
|
+
if (heading) {
|
|
1264
|
+
out += paint(heading.title, [ANSI.bold], options);
|
|
1265
|
+
i += heading.length;
|
|
1266
|
+
if (input[i] === '\n') {
|
|
1267
|
+
out += '\n';
|
|
1268
|
+
i += 1;
|
|
1269
|
+
state.atLineStart = true;
|
|
1270
|
+
} else {
|
|
1271
|
+
state.atLineStart = false;
|
|
1272
|
+
}
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
const list = streamingListMarkerAt(input, i);
|
|
1276
|
+
if (list) {
|
|
1277
|
+
out += `${list.indent}${paint('•', [ANSI.accent], options)} `;
|
|
1278
|
+
i += list.length;
|
|
1279
|
+
state.atLineStart = false;
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
const quote = streamingBlockquoteAt(input, i);
|
|
1283
|
+
if (quote) {
|
|
1284
|
+
out += paint('▏ ', [ANSI.muted], options);
|
|
1285
|
+
i += quote.length;
|
|
1286
|
+
state.atLineStart = false;
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
const hr = streamingHorizontalRuleAt(input, i);
|
|
1290
|
+
if (hr) {
|
|
1291
|
+
const width = Math.min(Math.max(16, terminalWidth(options) - 4), 48);
|
|
1292
|
+
out += paint('─'.repeat(width), [ANSI.dim, ANSI.muted], options);
|
|
1293
|
+
i += hr.length;
|
|
1294
|
+
if (input[i] === '\n') {
|
|
1295
|
+
out += '\n';
|
|
1296
|
+
i += 1;
|
|
1297
|
+
state.atLineStart = true;
|
|
1298
|
+
} else {
|
|
1299
|
+
state.atLineStart = false;
|
|
1300
|
+
}
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
618
1305
|
const char = input[i];
|
|
619
1306
|
const next = input[i + 1];
|
|
1307
|
+
const third = input[i + 2];
|
|
620
1308
|
|
|
621
1309
|
if (state.markdownMode === 'normal') {
|
|
622
|
-
|
|
623
|
-
|
|
1310
|
+
const link = streamingLinkAt(input, i, options);
|
|
1311
|
+
if (link) {
|
|
1312
|
+
out += link.rendered;
|
|
1313
|
+
i += link.length;
|
|
1314
|
+
state.atLineStart = false;
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
if (char === '[') {
|
|
1318
|
+
state.markdownCarry = '[';
|
|
624
1319
|
break;
|
|
625
1320
|
}
|
|
1321
|
+
if (char === '*' && next === '*' && third === '*') {
|
|
1322
|
+
state.markdownMode = 'bold_italic';
|
|
1323
|
+
state.markdownBuffer = '';
|
|
1324
|
+
state.markdownDelim = '***';
|
|
1325
|
+
i += 3;
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
626
1328
|
if (char === '*' && next === '*') {
|
|
627
1329
|
state.markdownMode = 'bold';
|
|
628
1330
|
state.markdownBuffer = '';
|
|
1331
|
+
state.markdownDelim = '**';
|
|
1332
|
+
i += 2;
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
if (char === '*') {
|
|
1336
|
+
if (next === undefined) {
|
|
1337
|
+
state.markdownCarry = '*';
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1340
|
+
state.markdownMode = 'italic';
|
|
1341
|
+
state.markdownBuffer = '';
|
|
1342
|
+
state.markdownDelim = '*';
|
|
1343
|
+
i += 1;
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
if (char === '_' && next === '_') {
|
|
1347
|
+
state.markdownMode = 'bold';
|
|
1348
|
+
state.markdownBuffer = '';
|
|
1349
|
+
state.markdownDelim = '__';
|
|
1350
|
+
i += 2;
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
if (char === '_') {
|
|
1354
|
+
if (next === undefined) {
|
|
1355
|
+
state.markdownCarry = '_';
|
|
1356
|
+
break;
|
|
1357
|
+
}
|
|
1358
|
+
state.markdownMode = 'italic';
|
|
1359
|
+
state.markdownBuffer = '';
|
|
1360
|
+
state.markdownDelim = '_';
|
|
1361
|
+
i += 1;
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
if (char === '~' && next === '~') {
|
|
1365
|
+
state.markdownMode = 'strike';
|
|
1366
|
+
state.markdownBuffer = '';
|
|
1367
|
+
state.markdownDelim = '~~';
|
|
629
1368
|
i += 2;
|
|
630
1369
|
continue;
|
|
631
1370
|
}
|
|
632
1371
|
if (char === '`') {
|
|
633
1372
|
state.markdownMode = 'code';
|
|
634
1373
|
state.markdownBuffer = '';
|
|
1374
|
+
state.markdownDelim = '`';
|
|
635
1375
|
i += 1;
|
|
636
1376
|
continue;
|
|
637
1377
|
}
|
|
638
1378
|
out += char;
|
|
1379
|
+
if (char === '\n') state.atLineStart = true;
|
|
1380
|
+
else state.atLineStart = false;
|
|
639
1381
|
i += 1;
|
|
640
1382
|
continue;
|
|
641
1383
|
}
|
|
642
1384
|
|
|
643
|
-
if (state.markdownMode === '
|
|
1385
|
+
if (state.markdownMode === 'bold_italic') {
|
|
1386
|
+
if (char === '*' && next === '*' && third === '*') {
|
|
1387
|
+
out += paint(state.markdownBuffer, [ANSI.bold, ANSI.italic], options);
|
|
1388
|
+
state.markdownMode = 'normal';
|
|
1389
|
+
state.markdownBuffer = '';
|
|
1390
|
+
state.markdownDelim = '';
|
|
1391
|
+
i += 3;
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
644
1394
|
if (char === '*' && next === undefined) {
|
|
645
1395
|
state.markdownCarry = '*';
|
|
646
1396
|
break;
|
|
647
1397
|
}
|
|
648
|
-
|
|
1398
|
+
state.markdownBuffer += char;
|
|
1399
|
+
i += 1;
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (state.markdownMode === 'bold') {
|
|
1404
|
+
const close = state.markdownDelim === '__' ? (char === '_' && next === '_') : (char === '*' && next === '*');
|
|
1405
|
+
if (close) {
|
|
649
1406
|
out += paint(state.markdownBuffer, [ANSI.bold], options);
|
|
650
1407
|
state.markdownMode = 'normal';
|
|
651
1408
|
state.markdownBuffer = '';
|
|
1409
|
+
state.markdownDelim = '';
|
|
1410
|
+
i += 2;
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
if ((char === '*' || char === '_') && next === undefined) {
|
|
1414
|
+
state.markdownCarry = char;
|
|
1415
|
+
break;
|
|
1416
|
+
}
|
|
1417
|
+
state.markdownBuffer += char;
|
|
1418
|
+
i += 1;
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
if (state.markdownMode === 'italic') {
|
|
1423
|
+
const close = char === state.markdownDelim && next !== state.markdownDelim;
|
|
1424
|
+
if (close) {
|
|
1425
|
+
out += paint(state.markdownBuffer, [ANSI.italic], options);
|
|
1426
|
+
state.markdownMode = 'normal';
|
|
1427
|
+
state.markdownBuffer = '';
|
|
1428
|
+
state.markdownDelim = '';
|
|
1429
|
+
i += 1;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
if (char === state.markdownDelim && next === undefined) {
|
|
1433
|
+
state.markdownCarry = char;
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
state.markdownBuffer += char;
|
|
1437
|
+
i += 1;
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
if (state.markdownMode === 'strike') {
|
|
1442
|
+
if (char === '~' && next === '~') {
|
|
1443
|
+
out += paint(state.markdownBuffer, [ANSI.strike], options);
|
|
1444
|
+
state.markdownMode = 'normal';
|
|
1445
|
+
state.markdownBuffer = '';
|
|
1446
|
+
state.markdownDelim = '';
|
|
652
1447
|
i += 2;
|
|
653
1448
|
continue;
|
|
654
1449
|
}
|
|
1450
|
+
if (char === '~' && next === undefined) {
|
|
1451
|
+
state.markdownCarry = '~';
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
655
1454
|
state.markdownBuffer += char;
|
|
656
1455
|
i += 1;
|
|
657
1456
|
continue;
|
|
@@ -677,10 +1476,19 @@ function renderStreamingMarkdown(state, text, options = {}) {
|
|
|
677
1476
|
function flushStreamingMarkdown(state, output) {
|
|
678
1477
|
ensureMarkdownState(state);
|
|
679
1478
|
let out = state.markdownCarry || '';
|
|
680
|
-
|
|
1479
|
+
state.markdownCarry = '';
|
|
1480
|
+
if (state.markdownMode === 'bold_italic' && state.markdownBuffer) {
|
|
1481
|
+
out += paint(state.markdownBuffer, [ANSI.bold, ANSI.italic], output);
|
|
1482
|
+
} else if (state.markdownMode === 'bold' && state.markdownBuffer) {
|
|
681
1483
|
out += paint(state.markdownBuffer, [ANSI.bold], output);
|
|
1484
|
+
} else if (state.markdownMode === 'italic' && state.markdownBuffer) {
|
|
1485
|
+
out += paint(state.markdownBuffer, [ANSI.italic], output);
|
|
1486
|
+
} else if (state.markdownMode === 'strike' && state.markdownBuffer) {
|
|
1487
|
+
out += paint(state.markdownBuffer, [ANSI.strike], output);
|
|
682
1488
|
} else if (state.markdownMode === 'code' && state.markdownBuffer) {
|
|
683
1489
|
out += paint(state.markdownBuffer, [ANSI.accent], output);
|
|
1490
|
+
} else if (state.markdownMode === 'fence' && state.markdownBuffer) {
|
|
1491
|
+
out += renderFenceBuffer(state.markdownBuffer, output);
|
|
684
1492
|
}
|
|
685
1493
|
resetMarkdownState(state);
|
|
686
1494
|
if (out) output.write(out);
|
|
@@ -730,21 +1538,26 @@ function createProgressReporter(output, options = {}) {
|
|
|
730
1538
|
const enabled = options.showProgress !== false;
|
|
731
1539
|
const isTty = Boolean(output && output.isTTY);
|
|
732
1540
|
const startedAt = Date.now();
|
|
733
|
-
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
734
1541
|
let frameIndex = 0;
|
|
735
|
-
let verb = '
|
|
1542
|
+
let verb = 'Thinking';
|
|
736
1543
|
let interval = null;
|
|
737
1544
|
let timeout = null;
|
|
738
1545
|
let shown = false;
|
|
739
1546
|
let closed = false;
|
|
1547
|
+
const renderOptions = () => ({
|
|
1548
|
+
...output,
|
|
1549
|
+
color: options.color,
|
|
1550
|
+
tick: frameIndex,
|
|
1551
|
+
frameIndex,
|
|
1552
|
+
});
|
|
740
1553
|
|
|
741
1554
|
const render = () => {
|
|
742
1555
|
if (!enabled || closed) return;
|
|
743
1556
|
if (isTty) {
|
|
744
|
-
|
|
745
|
-
|
|
1557
|
+
output.write(`\r${formatWorkingLine(Date.now() - startedAt, verb, renderOptions())}\x1b[K`);
|
|
1558
|
+
frameIndex += 1;
|
|
746
1559
|
} else if (!shown) {
|
|
747
|
-
output.write(`${formatWorkingLine(Date.now() - startedAt)}\n`);
|
|
1560
|
+
output.write(`${formatWorkingLine(Date.now() - startedAt, verb, renderOptions())}\n`);
|
|
748
1561
|
}
|
|
749
1562
|
shown = true;
|
|
750
1563
|
};
|
|
@@ -754,11 +1567,11 @@ function createProgressReporter(output, options = {}) {
|
|
|
754
1567
|
if (!enabled) return;
|
|
755
1568
|
timeout = setTimeout(() => {
|
|
756
1569
|
render();
|
|
757
|
-
if (isTty) interval = setInterval(render,
|
|
758
|
-
},
|
|
1570
|
+
if (isTty) interval = setInterval(render, PROGRESS_FRAME_MS);
|
|
1571
|
+
}, PROGRESS_START_DELAY_MS);
|
|
759
1572
|
},
|
|
760
1573
|
setVerb(next) {
|
|
761
|
-
verb = next || '
|
|
1574
|
+
verb = next || 'Thinking';
|
|
762
1575
|
},
|
|
763
1576
|
clear() {
|
|
764
1577
|
if (!isTty || !shown || closed) return;
|
|
@@ -775,6 +1588,17 @@ function createProgressReporter(output, options = {}) {
|
|
|
775
1588
|
};
|
|
776
1589
|
}
|
|
777
1590
|
|
|
1591
|
+
function manageTurnInterrupt(onInterrupt, options = {}) {
|
|
1592
|
+
const trigger = () => onInterrupt();
|
|
1593
|
+
const register = options.registerAbort;
|
|
1594
|
+
if (typeof register === 'function') {
|
|
1595
|
+
register(trigger);
|
|
1596
|
+
return () => register(null);
|
|
1597
|
+
}
|
|
1598
|
+
process.once('SIGINT', trigger);
|
|
1599
|
+
return () => process.removeListener('SIGINT', trigger);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
778
1602
|
function parseSseBlock(block) {
|
|
779
1603
|
const data = String(block || '')
|
|
780
1604
|
.split(/\r?\n/)
|
|
@@ -888,7 +1712,7 @@ function stopProgress(state) {
|
|
|
888
1712
|
}
|
|
889
1713
|
|
|
890
1714
|
function flushPendingText(state, output) {
|
|
891
|
-
const hasMarkdownRemainder = output &&
|
|
1715
|
+
const hasMarkdownRemainder = output && (
|
|
892
1716
|
state.markdownCarry
|
|
893
1717
|
|| state.markdownBuffer
|
|
894
1718
|
|| (state.markdownMode && state.markdownMode !== 'normal')
|
|
@@ -900,7 +1724,7 @@ function flushPendingText(state, output) {
|
|
|
900
1724
|
output.write('● ');
|
|
901
1725
|
state.needsBullet = false;
|
|
902
1726
|
}
|
|
903
|
-
if (state.pendingText) output.write(
|
|
1727
|
+
if (state.pendingText) output.write(renderTerminalMarkdown(state.pendingText, output));
|
|
904
1728
|
const flushedMarkdown = hasMarkdownRemainder ? flushStreamingMarkdown(state, output) : '';
|
|
905
1729
|
state.wroteText = true;
|
|
906
1730
|
state.wroteActivity = true;
|
|
@@ -956,7 +1780,8 @@ function buildMessage(message, history = []) {
|
|
|
956
1780
|
'Recent conversation:',
|
|
957
1781
|
compactHistory(history),
|
|
958
1782
|
'',
|
|
959
|
-
|
|
1783
|
+
'# Current user message',
|
|
1784
|
+
trimmed,
|
|
960
1785
|
].join('\n');
|
|
961
1786
|
}
|
|
962
1787
|
|
|
@@ -964,12 +1789,15 @@ function buildPayload(message, options = {}) {
|
|
|
964
1789
|
const mode = normalizeMode(options.mode);
|
|
965
1790
|
const route = options.business ? 'local' : resolveRoute(message, options);
|
|
966
1791
|
const local = route !== 'cloud';
|
|
1792
|
+
const bypass = resolveBypassPermissions(options);
|
|
967
1793
|
const verifyCommand = String(options.verify || '').trim();
|
|
968
1794
|
const payload = {
|
|
969
1795
|
message: buildMessage(message, options.history || []),
|
|
970
1796
|
model: modelForMode(mode),
|
|
971
|
-
max_turns: local ? (mode === 'fast' ? 8 : 14) : 1,
|
|
972
|
-
verify_command: verifyCommand || 'true'
|
|
1797
|
+
max_turns: options.goalEval ? 1 : (local ? (mode === 'fast' ? 8 : 14) : 1),
|
|
1798
|
+
verify_command: verifyCommand || 'true',
|
|
1799
|
+
member_slug: MEMBER_SLUG,
|
|
1800
|
+
bypass_permissions: bypass,
|
|
973
1801
|
};
|
|
974
1802
|
if (options.business) {
|
|
975
1803
|
// Business cloud workspace: the model loop stays on the backend and every
|
|
@@ -986,7 +1814,7 @@ function buildPayload(message, options = {}) {
|
|
|
986
1814
|
if (!local && options.connectionUserId) {
|
|
987
1815
|
payload.connection_user_id = options.connectionUserId;
|
|
988
1816
|
}
|
|
989
|
-
if (!local && connectorWriteIntent(message)) {
|
|
1817
|
+
if (!local && bypass && connectorWriteIntent(message)) {
|
|
990
1818
|
payload.allow_external_actions = true;
|
|
991
1819
|
payload.cleanup_external_actions = true;
|
|
992
1820
|
payload.max_turns = mode === 'fast' ? 2 : 4;
|
|
@@ -1052,6 +1880,7 @@ function handleEvent(event, state, output) {
|
|
|
1052
1880
|
writeAuxLine(state, output, formatToolResultLine(summarizeToolResult(result, output), output));
|
|
1053
1881
|
state.lastAux = 'result';
|
|
1054
1882
|
}
|
|
1883
|
+
if (state.progress && state.progress.setVerb) state.progress.setVerb('Thinking');
|
|
1055
1884
|
return;
|
|
1056
1885
|
}
|
|
1057
1886
|
|
|
@@ -1082,8 +1911,14 @@ function handleEvent(event, state, output) {
|
|
|
1082
1911
|
clearRetriedText(state);
|
|
1083
1912
|
return;
|
|
1084
1913
|
}
|
|
1914
|
+
if (state.progress && state.progress.setVerb && /think|reason|plan/i.test(event.message)) {
|
|
1915
|
+
state.progress.setVerb('Thinking');
|
|
1916
|
+
}
|
|
1085
1917
|
flushPendingText(state, output);
|
|
1086
|
-
|
|
1918
|
+
const statusText = formatStatusMessage(event.message);
|
|
1919
|
+
if (statusText) {
|
|
1920
|
+
writeAuxLine(state, output, paint(`✦ ${statusText}`, [ANSI.bold, ANSI.magenta], output));
|
|
1921
|
+
}
|
|
1087
1922
|
return;
|
|
1088
1923
|
}
|
|
1089
1924
|
|
|
@@ -1111,7 +1946,7 @@ async function postTurn(message, options = {}) {
|
|
|
1111
1946
|
const connectionContext = options.connectionContext || (shouldSendConnectionContext
|
|
1112
1947
|
? await buildConnectionContext({ token, localWorkspace: local })
|
|
1113
1948
|
: null);
|
|
1114
|
-
const connectionUserId = !local && isLoopbackBackend() ? authUserId() : '';
|
|
1949
|
+
const connectionUserId = !local && isLoopbackBackend({ route }) ? authUserId() : '';
|
|
1115
1950
|
const payload = buildPayload(message, { ...options, route, connectionContext, connectionUserId });
|
|
1116
1951
|
const postData = JSON.stringify(payload);
|
|
1117
1952
|
const output = options.output || process.stdout;
|
|
@@ -1119,7 +1954,7 @@ async function postTurn(message, options = {}) {
|
|
|
1119
1954
|
// SSE traffic in between, so the socket-idle timeout needs more headroom.
|
|
1120
1955
|
const baseTimeoutMs = payload.model === 'atris:max' ? 300000 : payload.model === 'atris:pro' ? 180000 : 60000;
|
|
1121
1956
|
const timeoutMs = options.business ? Math.max(baseTimeoutMs, 180000) : baseTimeoutMs;
|
|
1122
|
-
const turnUrl = new URL(backendUrl());
|
|
1957
|
+
const turnUrl = new URL(backendUrl({ route }));
|
|
1123
1958
|
const transport = turnUrl.protocol === 'https:' ? https : http;
|
|
1124
1959
|
const state = {
|
|
1125
1960
|
events: [],
|
|
@@ -1137,15 +1972,19 @@ async function postTurn(message, options = {}) {
|
|
|
1137
1972
|
markdownMode: 'normal',
|
|
1138
1973
|
markdownBuffer: '',
|
|
1139
1974
|
markdownCarry: '',
|
|
1975
|
+
atLineStart: true,
|
|
1140
1976
|
relay: options.business ? {
|
|
1141
1977
|
chain: Promise.resolve(),
|
|
1142
1978
|
execute: options.business.executor,
|
|
1143
|
-
post: (callId, result) => options.business.postToolResult(callId, result, backendBaseUrl())
|
|
1979
|
+
post: (callId, result) => options.business.postToolResult(callId, result, backendBaseUrl({ route }))
|
|
1144
1980
|
} : null
|
|
1145
1981
|
};
|
|
1146
1982
|
|
|
1147
1983
|
return new Promise((resolve, reject) => {
|
|
1148
1984
|
let settled = false;
|
|
1985
|
+
let interrupted = false;
|
|
1986
|
+
let req = null;
|
|
1987
|
+
let removeInterrupt = null;
|
|
1149
1988
|
const startedAt = Date.now();
|
|
1150
1989
|
state.progress = createProgressReporter(output, options);
|
|
1151
1990
|
state.progress.start();
|
|
@@ -1153,6 +1992,8 @@ async function postTurn(message, options = {}) {
|
|
|
1153
1992
|
const finish = (error, value) => {
|
|
1154
1993
|
if (settled) return;
|
|
1155
1994
|
settled = true;
|
|
1995
|
+
if (removeInterrupt) removeInterrupt();
|
|
1996
|
+
removeInterrupt = null;
|
|
1156
1997
|
if (state.progress) state.progress.stop();
|
|
1157
1998
|
state.progress = null;
|
|
1158
1999
|
state.durationMs = Date.now() - startedAt;
|
|
@@ -1160,7 +2001,15 @@ async function postTurn(message, options = {}) {
|
|
|
1160
2001
|
else resolve(value);
|
|
1161
2002
|
};
|
|
1162
2003
|
|
|
1163
|
-
|
|
2004
|
+
removeInterrupt = manageTurnInterrupt(() => {
|
|
2005
|
+
if (settled) return;
|
|
2006
|
+
interrupted = true;
|
|
2007
|
+
flushPendingText(state, output);
|
|
2008
|
+
if (req) req.destroy();
|
|
2009
|
+
finish(null, { ...state, interrupted: true });
|
|
2010
|
+
}, options);
|
|
2011
|
+
|
|
2012
|
+
req = transport.request({
|
|
1164
2013
|
hostname: turnUrl.hostname,
|
|
1165
2014
|
port: turnUrl.port || (turnUrl.protocol === 'https:' ? 443 : 80),
|
|
1166
2015
|
path: `${turnUrl.pathname}${turnUrl.search}`,
|
|
@@ -1225,8 +2074,12 @@ async function postTurn(message, options = {}) {
|
|
|
1225
2074
|
});
|
|
1226
2075
|
});
|
|
1227
2076
|
|
|
1228
|
-
req.on('error',
|
|
2077
|
+
req.on('error', (error) => {
|
|
2078
|
+
if (interrupted) return;
|
|
2079
|
+
finish(error);
|
|
2080
|
+
});
|
|
1229
2081
|
req.setTimeout(timeoutMs, () => {
|
|
2082
|
+
if (settled) return;
|
|
1230
2083
|
finish(new Error(`Request timeout after ${timeoutMs / 1000}s`));
|
|
1231
2084
|
req.destroy();
|
|
1232
2085
|
});
|
|
@@ -1246,7 +2099,7 @@ async function postCodeFastTurn(message, options = {}) {
|
|
|
1246
2099
|
const output = options.output || process.stdout;
|
|
1247
2100
|
const token = authToken();
|
|
1248
2101
|
const timeoutMs = Math.max(10000, Math.min(600000, Number(payload.timeout_seconds || 180) * 1000));
|
|
1249
|
-
const turnUrl = new URL(codeFastUrl());
|
|
2102
|
+
const turnUrl = new URL(codeFastUrl({ ...options, route }));
|
|
1250
2103
|
const transport = turnUrl.protocol === 'https:' ? https : http;
|
|
1251
2104
|
const state = {
|
|
1252
2105
|
events: [],
|
|
@@ -1263,11 +2116,15 @@ async function postCodeFastTurn(message, options = {}) {
|
|
|
1263
2116
|
lastAux: '',
|
|
1264
2117
|
markdownMode: 'normal',
|
|
1265
2118
|
markdownBuffer: '',
|
|
1266
|
-
markdownCarry: ''
|
|
2119
|
+
markdownCarry: '',
|
|
2120
|
+
atLineStart: true,
|
|
1267
2121
|
};
|
|
1268
2122
|
|
|
1269
2123
|
return new Promise((resolve, reject) => {
|
|
1270
2124
|
let settled = false;
|
|
2125
|
+
let interrupted = false;
|
|
2126
|
+
let req = null;
|
|
2127
|
+
let removeInterrupt = null;
|
|
1271
2128
|
const startedAt = Date.now();
|
|
1272
2129
|
state.progress = createProgressReporter(output, options);
|
|
1273
2130
|
state.progress.start();
|
|
@@ -1275,6 +2132,8 @@ async function postCodeFastTurn(message, options = {}) {
|
|
|
1275
2132
|
const finish = (error, value) => {
|
|
1276
2133
|
if (settled) return;
|
|
1277
2134
|
settled = true;
|
|
2135
|
+
if (removeInterrupt) removeInterrupt();
|
|
2136
|
+
removeInterrupt = null;
|
|
1278
2137
|
if (state.progress) state.progress.stop();
|
|
1279
2138
|
state.progress = null;
|
|
1280
2139
|
state.durationMs = Date.now() - startedAt;
|
|
@@ -1282,7 +2141,15 @@ async function postCodeFastTurn(message, options = {}) {
|
|
|
1282
2141
|
else resolve(value);
|
|
1283
2142
|
};
|
|
1284
2143
|
|
|
1285
|
-
|
|
2144
|
+
removeInterrupt = manageTurnInterrupt(() => {
|
|
2145
|
+
if (settled) return;
|
|
2146
|
+
interrupted = true;
|
|
2147
|
+
flushPendingText(state, output);
|
|
2148
|
+
if (req) req.destroy();
|
|
2149
|
+
finish(null, { ...state, interrupted: true });
|
|
2150
|
+
}, options);
|
|
2151
|
+
|
|
2152
|
+
req = transport.request({
|
|
1286
2153
|
hostname: turnUrl.hostname,
|
|
1287
2154
|
port: turnUrl.port || (turnUrl.protocol === 'https:' ? 443 : 80),
|
|
1288
2155
|
path: `${turnUrl.pathname}${turnUrl.search}`,
|
|
@@ -1334,8 +2201,12 @@ async function postCodeFastTurn(message, options = {}) {
|
|
|
1334
2201
|
});
|
|
1335
2202
|
});
|
|
1336
2203
|
|
|
1337
|
-
req.on('error',
|
|
2204
|
+
req.on('error', (error) => {
|
|
2205
|
+
if (interrupted) return;
|
|
2206
|
+
finish(error);
|
|
2207
|
+
});
|
|
1338
2208
|
req.setTimeout(timeoutMs, () => {
|
|
2209
|
+
if (settled) return;
|
|
1339
2210
|
finish(new Error(`Request timeout after ${timeoutMs / 1000}s`));
|
|
1340
2211
|
req.destroy();
|
|
1341
2212
|
});
|
|
@@ -1350,15 +2221,137 @@ function turnFunctionForMode(mode) {
|
|
|
1350
2221
|
|
|
1351
2222
|
async function chat(options = {}) {
|
|
1352
2223
|
let mode = normalizeMode(options.mode);
|
|
2224
|
+
let bypassPermissions = resolveBypassPermissions(options);
|
|
1353
2225
|
const cwd = options.cwd || process.cwd();
|
|
1354
2226
|
const input = options.input || process.stdin;
|
|
1355
2227
|
const baseOutput = options.output || process.stdout;
|
|
1356
2228
|
const logger = createRunLogger({ cwd, mode, kind: 'play', output: baseOutput });
|
|
1357
2229
|
const output = logger ? logger.output : baseOutput;
|
|
1358
2230
|
const history = [];
|
|
2231
|
+
let goalState = null;
|
|
2232
|
+
let lastAchievedGoal = null;
|
|
2233
|
+
|
|
2234
|
+
const goalPaintOptions = {
|
|
2235
|
+
paint: (text, codes) => paint(text, codes, output),
|
|
2236
|
+
bold: ANSI.bold,
|
|
2237
|
+
magenta: ANSI.magenta,
|
|
2238
|
+
muted: ANSI.muted,
|
|
2239
|
+
accent: ANSI.accent,
|
|
2240
|
+
ok: ANSI.ok,
|
|
2241
|
+
};
|
|
2242
|
+
|
|
2243
|
+
const writeHeader = () => {
|
|
2244
|
+
output.write(`${formatHeader({ mode, cwd, chat: true, bypassPermissions, goal: goalState }, output)}\n\n`);
|
|
2245
|
+
};
|
|
2246
|
+
|
|
2247
|
+
writeHeader();
|
|
2248
|
+
|
|
2249
|
+
const executeChatTurn = async (message, turnOptions = {}) => {
|
|
2250
|
+
let result;
|
|
2251
|
+
try {
|
|
2252
|
+
const turnFn = typeof options.turnFunction === 'function'
|
|
2253
|
+
? options.turnFunction
|
|
2254
|
+
: turnFunctionForMode(mode);
|
|
2255
|
+
result = await turnFn(message, {
|
|
2256
|
+
mode,
|
|
2257
|
+
cwd,
|
|
2258
|
+
history,
|
|
2259
|
+
output,
|
|
2260
|
+
route: options.route,
|
|
2261
|
+
business: options.business,
|
|
2262
|
+
verify: options.verify,
|
|
2263
|
+
bypassPermissions,
|
|
2264
|
+
registerAbort: options.registerAbort,
|
|
2265
|
+
goalEval: turnOptions.goalEval === true,
|
|
2266
|
+
});
|
|
2267
|
+
} finally {
|
|
2268
|
+
if (typeof options.registerAbort === 'function') options.registerAbort(null);
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
if (result.interrupted) {
|
|
2272
|
+
if (result.output && !result.output.endsWith('\n')) output.write('\n');
|
|
2273
|
+
if (!turnOptions.goalEval) {
|
|
2274
|
+
output.write(`${paint('· Interrupted', [ANSI.muted], output)}\n\n`);
|
|
2275
|
+
}
|
|
2276
|
+
if (result.output && result.output.trim()) {
|
|
2277
|
+
history.push({ role: 'user', content: message });
|
|
2278
|
+
history.push({ role: 'assistant', content: result.output });
|
|
2279
|
+
}
|
|
2280
|
+
return result;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
if (result.output && !result.output.endsWith('\n')) output.write('\n');
|
|
2284
|
+
if (!turnOptions.goalEval) {
|
|
2285
|
+
output.write(`${formatDoneLine(result.durationMs, creditsFromState(result))}\n\n`);
|
|
2286
|
+
}
|
|
2287
|
+
history.push({ role: 'user', content: message });
|
|
2288
|
+
history.push({ role: 'assistant', content: result.output || '' });
|
|
2289
|
+
return result;
|
|
2290
|
+
};
|
|
2291
|
+
|
|
2292
|
+
const runGoalLoop = async (activeGoal) => {
|
|
2293
|
+
output.write(`${formatGoalStatus(activeGoal, goalPaintOptions)}\n\n`);
|
|
2294
|
+
writeHeader();
|
|
2295
|
+
|
|
2296
|
+
while (activeGoal.active) {
|
|
2297
|
+
const directive = buildGoalDirective(activeGoal, { continue: activeGoal.turns > 0 });
|
|
2298
|
+
if (activeGoal.turns > 0) {
|
|
2299
|
+
output.write(`${formatGoalContinue(activeGoal, goalPaintOptions)}\n\n`);
|
|
2300
|
+
}
|
|
2301
|
+
output.write('\n');
|
|
2302
|
+
|
|
2303
|
+
const result = await executeChatTurn(directive, { goalLoop: true });
|
|
2304
|
+
activeGoal.turns += 1;
|
|
2305
|
+
accumulateGoalUsage(activeGoal, result, creditsFromState);
|
|
1359
2306
|
|
|
1360
|
-
|
|
1361
|
-
|
|
2307
|
+
if (result.interrupted) {
|
|
2308
|
+
output.write(`${paint('· Goal turn interrupted — /goal for status · /goal clear to stop', [ANSI.muted], output)}\n\n`);
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
const evalResult = await evaluateGoalTurn(activeGoal, {
|
|
2313
|
+
history,
|
|
2314
|
+
lastOutput: result.output,
|
|
2315
|
+
turnOptions: {
|
|
2316
|
+
mode,
|
|
2317
|
+
cwd,
|
|
2318
|
+
bypassPermissions,
|
|
2319
|
+
route: options.route,
|
|
2320
|
+
business: options.business,
|
|
2321
|
+
verify: options.verify,
|
|
2322
|
+
registerAbort: options.registerAbort,
|
|
2323
|
+
},
|
|
2324
|
+
}, { postTurn, evaluateGoal: options.evaluateGoal });
|
|
2325
|
+
|
|
2326
|
+
activeGoal.evalTurns += 1;
|
|
2327
|
+
activeGoal.lastReason = evalResult.reason || activeGoal.lastReason;
|
|
2328
|
+
|
|
2329
|
+
if (evalResult.achieved) {
|
|
2330
|
+
finishGoalAchieved(activeGoal, evalResult.reason);
|
|
2331
|
+
output.write(`${formatGoalAchieved(activeGoal, goalPaintOptions)}\n\n`);
|
|
2332
|
+
lastAchievedGoal = { ...activeGoal };
|
|
2333
|
+
goalState = null;
|
|
2334
|
+
writeHeader();
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
if (goalTurnLimitReached(activeGoal)) {
|
|
2339
|
+
clearGoalState(activeGoal);
|
|
2340
|
+
output.write(`${formatGoalStopped(activeGoal, 'Turn limit reached.', goalPaintOptions)}\n\n`);
|
|
2341
|
+
goalState = null;
|
|
2342
|
+
writeHeader();
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
if (goalBudgetExceeded(activeGoal)) {
|
|
2347
|
+
clearGoalState(activeGoal);
|
|
2348
|
+
output.write(`${formatGoalStopped(activeGoal, 'Token budget reached.', goalPaintOptions)}\n\n`);
|
|
2349
|
+
goalState = null;
|
|
2350
|
+
writeHeader();
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
};
|
|
1362
2355
|
|
|
1363
2356
|
const runLine = async (line) => {
|
|
1364
2357
|
const trimmed = String(line || '').trim();
|
|
@@ -1378,18 +2371,68 @@ async function chat(options = {}) {
|
|
|
1378
2371
|
return false;
|
|
1379
2372
|
}
|
|
1380
2373
|
|
|
2374
|
+
const permission = chatPermissionCommand(trimmed);
|
|
2375
|
+
if (permission !== null) {
|
|
2376
|
+
bypassPermissions = permission;
|
|
2377
|
+
const persistPrefs = options.persistPrefs !== false;
|
|
2378
|
+
setBypassPermissions(permission, { persist: persistPrefs });
|
|
2379
|
+
if (logger) logger.write(`${trimmed}\n`);
|
|
2380
|
+
output.write(`${formatPermissionToggleMessage(permission, { ...output, persistPrefs })}\n\n`);
|
|
2381
|
+
return false;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
const goalCmd = parseGoalCommand(trimmed);
|
|
2385
|
+
if (goalCmd) {
|
|
2386
|
+
if (logger) logger.write(`${trimmed}\n`);
|
|
2387
|
+
if (goalCmd.action === 'clear') {
|
|
2388
|
+
if (goalState) clearGoalState(goalState);
|
|
2389
|
+
goalState = null;
|
|
2390
|
+
output.write(`${paint('· Goal cleared', [ANSI.ok], output)}\n\n`);
|
|
2391
|
+
writeHeader();
|
|
2392
|
+
return false;
|
|
2393
|
+
}
|
|
2394
|
+
if (goalCmd.action === 'status') {
|
|
2395
|
+
output.write(`${formatGoalStatus(goalState || lastAchievedGoal, goalPaintOptions)}\n\n`);
|
|
2396
|
+
return false;
|
|
2397
|
+
}
|
|
2398
|
+
if (goalCmd.action === 'set') {
|
|
2399
|
+
if (goalState && goalState.active) clearGoalState(goalState);
|
|
2400
|
+
goalState = createGoalState(goalCmd.condition, goalCmd);
|
|
2401
|
+
lastAchievedGoal = null;
|
|
2402
|
+
await runGoalLoop(goalState);
|
|
2403
|
+
return false;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
|
|
1381
2407
|
if (trimmed.startsWith('/')) {
|
|
1382
2408
|
output.write(`${chatMenu(output)}\n\n`);
|
|
1383
2409
|
return false;
|
|
1384
2410
|
}
|
|
1385
2411
|
|
|
1386
|
-
if (
|
|
2412
|
+
if (extractYoutubeUrl(trimmed)) {
|
|
2413
|
+
if (logger) logger.write(`${formatPrompt(mode, { ...output, goal: goalState })}${trimmed}\n`);
|
|
2414
|
+
output.write('\n');
|
|
2415
|
+
const startedAt = Date.now();
|
|
2416
|
+
const youtubeOutput = [];
|
|
2417
|
+
await runAxYoutubeCommand([trimmed], {
|
|
2418
|
+
youtubeCommand: options.youtubeCommand,
|
|
2419
|
+
ensureValidCredentials: options.ensureValidCredentials,
|
|
2420
|
+
apiRequestJson: options.apiRequestJson,
|
|
2421
|
+
output: (line = '') => {
|
|
2422
|
+
const text = String(line);
|
|
2423
|
+
youtubeOutput.push(text);
|
|
2424
|
+
output.write(`${text}\n`);
|
|
2425
|
+
},
|
|
2426
|
+
});
|
|
2427
|
+
output.write(`${formatDoneLine(Date.now() - startedAt)}\n\n`);
|
|
2428
|
+
history.push({ role: 'user', content: trimmed });
|
|
2429
|
+
history.push({ role: 'assistant', content: youtubeOutput.join('\n') });
|
|
2430
|
+
return false;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
if (logger) logger.write(`${formatPrompt(mode, { ...output, goal: goalState })}${trimmed}\n`);
|
|
1387
2434
|
output.write('\n');
|
|
1388
|
-
|
|
1389
|
-
if (result.output && !result.output.endsWith('\n')) output.write('\n');
|
|
1390
|
-
output.write(`${formatDoneLine(result.durationMs, creditsFromState(result))}\n\n`);
|
|
1391
|
-
history.push({ role: 'user', content: trimmed });
|
|
1392
|
-
history.push({ role: 'assistant', content: result.output || '' });
|
|
2435
|
+
await executeChatTurn(trimmed);
|
|
1393
2436
|
return false;
|
|
1394
2437
|
};
|
|
1395
2438
|
|
|
@@ -1402,22 +2445,443 @@ async function chat(options = {}) {
|
|
|
1402
2445
|
return;
|
|
1403
2446
|
}
|
|
1404
2447
|
|
|
1405
|
-
const rl = readline.createInterface({
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
2448
|
+
const rl = readline.createInterface({
|
|
2449
|
+
input,
|
|
2450
|
+
output: baseOutput,
|
|
2451
|
+
completer: chatCompleter,
|
|
2452
|
+
crlfDelay: Infinity,
|
|
2453
|
+
});
|
|
2454
|
+
readline.emitKeypressEvents(input, rl);
|
|
2455
|
+
// Bracketed paste: terminals wrap pasted text in \x1b[200~ … \x1b[201~ so a
|
|
2456
|
+
// multi-line paste lands as ONE message instead of submitting on every
|
|
2457
|
+
// newline. readline + the multiline hook already understand the markers; we
|
|
2458
|
+
// just have to turn the mode on (and off again on every exit path).
|
|
2459
|
+
const setBracketedPaste = (on) => {
|
|
2460
|
+
if (baseOutput && baseOutput.isTTY && typeof baseOutput.write === 'function') {
|
|
2461
|
+
baseOutput.write(on ? '\x1b[?2004h' : '\x1b[?2004l');
|
|
2462
|
+
}
|
|
2463
|
+
};
|
|
2464
|
+
const disableBracketedPasteOnExit = () => setBracketedPaste(false);
|
|
2465
|
+
setBracketedPaste(true);
|
|
2466
|
+
process.once('exit', disableBracketedPasteOnExit);
|
|
2467
|
+
// readline can't emit a 'line' string that holds a literal newline, so the
|
|
2468
|
+
// multiline hook hands us the true buffer here and ask() prefers it.
|
|
2469
|
+
let pendingMultilineLine = null;
|
|
2470
|
+
const detachMultilineInput = attachMultilineChatInput(rl, {
|
|
2471
|
+
insertBreak: insertMultilineBreak,
|
|
2472
|
+
onSubmit: (line) => { pendingMultilineLine = line; },
|
|
2473
|
+
});
|
|
2474
|
+
const boxLayoutOptions = () => inputBoxLayoutOptions(baseOutput, { bypassPermissions });
|
|
2475
|
+
let exiting = false;
|
|
2476
|
+
let awaitingInput = false;
|
|
2477
|
+
// Repaint the box bottom rule after every readline render so the frame stays
|
|
2478
|
+
// closed while typing (empty, wrapped, and multiline input alike).
|
|
2479
|
+
const baseRefreshLine = typeof rl._refreshLine === 'function' ? rl._refreshLine.bind(rl) : null;
|
|
2480
|
+
if (baseRefreshLine) {
|
|
2481
|
+
rl._refreshLine = () => {
|
|
2482
|
+
baseRefreshLine();
|
|
2483
|
+
if (awaitingInput) repaintInputBoxBottom(rl, baseOutput, boxLayoutOptions());
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
let turnAbort = null;
|
|
2487
|
+
options.registerAbort = (fn) => {
|
|
2488
|
+
turnAbort = typeof fn === 'function' ? fn : null;
|
|
2489
|
+
};
|
|
2490
|
+
const onChatKeypress = (_str, key) => {
|
|
2491
|
+
if (!awaitingInput) return;
|
|
2492
|
+
if (!isPermissionToggleKey(key)) return;
|
|
2493
|
+
bypassPermissions = !bypassPermissions;
|
|
2494
|
+
const persistPrefs = options.persistPrefs !== false;
|
|
2495
|
+
setBypassPermissions(bypassPermissions, { persist: persistPrefs });
|
|
2496
|
+
if (logger) logger.write(`[shift+tab permissions ${bypassPermissions ? 'bypass' : 'safe'}]\n`);
|
|
2497
|
+
// The mode label lives on the status line below the box now, so repaint
|
|
2498
|
+
// that (it also restores the cursor) instead of redrawing the plain top.
|
|
2499
|
+
repaintInputBoxBottom(rl, baseOutput, boxLayoutOptions());
|
|
2500
|
+
};
|
|
2501
|
+
input.prependListener('keypress', onChatKeypress);
|
|
2502
|
+
rl.on('SIGINT', () => {
|
|
2503
|
+
if (turnAbort) {
|
|
2504
|
+
turnAbort();
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
exiting = true;
|
|
2508
|
+
output.write('\n');
|
|
2509
|
+
rl.close();
|
|
2510
|
+
});
|
|
2511
|
+
const ask = () => new Promise((resolve, reject) => {
|
|
2512
|
+
function onClose() {
|
|
2513
|
+
reject(new Error('CHAT_CLOSED'));
|
|
2514
|
+
}
|
|
2515
|
+
rl.once('close', onClose);
|
|
2516
|
+
awaitingInput = true;
|
|
2517
|
+
const layout = boxLayoutOptions();
|
|
2518
|
+
output.write(`\n${formatInputBoxTop(layout)}\n`);
|
|
2519
|
+
pendingMultilineLine = null;
|
|
2520
|
+
rl.question(formatChatInputPrompt(mode, { ...layout, goal: goalState }), (answer) => {
|
|
2521
|
+
awaitingInput = false;
|
|
2522
|
+
rl.removeListener('close', onClose);
|
|
2523
|
+
closeInputBox(output, layout);
|
|
2524
|
+
const captured = pendingMultilineLine;
|
|
2525
|
+
pendingMultilineLine = null;
|
|
2526
|
+
resolve(stripMultilineCsiText(captured != null ? captured : answer));
|
|
2527
|
+
});
|
|
2528
|
+
});
|
|
2529
|
+
while (!exiting) {
|
|
2530
|
+
let line = '';
|
|
2531
|
+
try {
|
|
2532
|
+
line = await ask();
|
|
2533
|
+
} catch (error) {
|
|
2534
|
+
if (error && error.message === 'CHAT_CLOSED') break;
|
|
2535
|
+
throw error;
|
|
2536
|
+
}
|
|
1409
2537
|
if (await runLine(line)) {
|
|
1410
2538
|
rl.close();
|
|
1411
2539
|
break;
|
|
1412
2540
|
}
|
|
1413
2541
|
}
|
|
2542
|
+
input.removeListener('keypress', onChatKeypress);
|
|
2543
|
+
detachMultilineInput();
|
|
2544
|
+
if (baseRefreshLine) rl._refreshLine = baseRefreshLine;
|
|
2545
|
+
setBracketedPaste(false);
|
|
2546
|
+
process.removeListener('exit', disableBracketedPasteOnExit);
|
|
1414
2547
|
if (logger) logger.close(0);
|
|
1415
2548
|
}
|
|
1416
2549
|
|
|
2550
|
+
function newestAxLog(cwd, sinceMs = 0) {
|
|
2551
|
+
const runsDir = path.join(cwd, 'atris', 'runs');
|
|
2552
|
+
if (!fs.existsSync(runsDir)) return null;
|
|
2553
|
+
let newest = null;
|
|
2554
|
+
for (const name of fs.readdirSync(runsDir)) {
|
|
2555
|
+
if (!/^ax-.*\.log$/.test(name)) continue;
|
|
2556
|
+
const full = path.join(runsDir, name);
|
|
2557
|
+
let stat = null;
|
|
2558
|
+
try {
|
|
2559
|
+
stat = fs.statSync(full);
|
|
2560
|
+
} catch {
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
if (!stat.isFile() || stat.mtimeMs < sinceMs) continue;
|
|
2564
|
+
if (!newest || stat.mtimeMs > newest.mtimeMs) newest = { path: full, mtimeMs: stat.mtimeMs };
|
|
2565
|
+
}
|
|
2566
|
+
return newest ? newest.path : null;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
function makeChecklistItem(id, label, passed, evidence = '') {
|
|
2570
|
+
return { id, label, passed: passed === true, evidence: compactText(evidence, 180) };
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
function compactText(text, max = 180) {
|
|
2574
|
+
const one = String(text || '').replace(/\s+/g, ' ').trim();
|
|
2575
|
+
if (!one) return '';
|
|
2576
|
+
return one.length <= max ? one : `${one.slice(0, max - 3)}...`;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
function buildDogfoodChecklist({ cases, outputText, turnCalls, youtubeCalls, logPath }) {
|
|
2580
|
+
const turnCases = cases.filter(item => item.kind === 'turn');
|
|
2581
|
+
const paired = turnCases.map((item, index) => ({ case: item, call: turnCalls[index] || null }));
|
|
2582
|
+
const smallTalk = paired.filter(item => /small_talk|short_noise|greeting/.test(item.case.coverage));
|
|
2583
|
+
const workspace = paired.filter(item => /workspace_|github_workspace_mutation|max_cloud/.test(item.case.coverage));
|
|
2584
|
+
const cloud = paired.filter(item => item.case.expectRoute === 'cloud');
|
|
2585
|
+
const bypassCall = paired.find(item => item.case.coverage === 'connector_write_bypass')?.call;
|
|
2586
|
+
const safeCalls = paired.filter(item => /connector_write_(safe|resafe)/.test(item.case.coverage)).map(item => item.call);
|
|
2587
|
+
const maxCall = paired.find(item => item.case.coverage === 'max_cloud')?.call;
|
|
2588
|
+
const firstHeader = outputText.split(/\r?\n/).slice(0, 4).map(stripAnsi);
|
|
2589
|
+
const longestHeader = firstHeader.reduce((max, line) => Math.max(max, line.length), 0);
|
|
2590
|
+
const modelPattern = /atris:|composer-2-5|gpt-|kimi|fable|fireworks|openrouter/i;
|
|
2591
|
+
const errorPattern = /Cannot access 'business'|Blocked: direct network access|ReferenceError|TypeError|UnhandledPromiseRejection|ECONNREFUSED|HTTP 5\d\d/i;
|
|
2592
|
+
|
|
2593
|
+
return [
|
|
2594
|
+
makeChecklistItem('loops_25', 'Ran the requested loop count', cases.length === 25, `${cases.length}/25 loops`),
|
|
2595
|
+
makeChecklistItem('header_compact', 'Chat header fits a small terminal', longestHeader <= 96 && /shift\+enter/.test(outputText), `longest header line ${longestHeader}`),
|
|
2596
|
+
makeChecklistItem('menu_discoverable', 'Slash menu exposes key controls', /\/goal/.test(outputText) && /\/fast/.test(outputText) && /\/safe/.test(outputText), 'menu printed by /help and unknown slash'),
|
|
2597
|
+
makeChecklistItem('small_talk_cloud', 'Greetings/noise avoid workspace listings', smallTalk.length >= 4 && smallTalk.every(item => item.call?.route === 'cloud' && !item.call?.workspace_path), smallTalk.map(item => `${item.case.input}:${item.call?.route}`).join(', ')),
|
|
2598
|
+
makeChecklistItem('workspace_cloud_default', 'Workspace/code asks stay hosted by default', workspace.length >= 5 && workspace.every(item => item.call?.route === 'cloud' && !item.call?.workspace_path), workspace.map(item => `${item.case.coverage}:${item.call?.route}`).join(', ')),
|
|
2599
|
+
makeChecklistItem('connectors_cloud', 'Connector reads/writes stay cloud by default', cloud.length >= 8 && cloud.every(item => item.call?.route === 'cloud'), cloud.map(item => `${item.case.coverage}:${item.call?.route}`).join(', ')),
|
|
2600
|
+
makeChecklistItem('tier_switches', 'Tier commands affect following turns', maxCall?.mode === 'max', `max turn mode ${maxCall?.mode || 'missing'}`),
|
|
2601
|
+
makeChecklistItem('permission_gates', 'Safe/bypass gates are visible in payloads', bypassCall?.allow_external_actions === true && safeCalls.every(call => call && call.allow_external_actions !== true), `bypass=${bypassCall?.allow_external_actions === true}; safe=${safeCalls.length}`),
|
|
2602
|
+
makeChecklistItem('youtube_shortcut', 'YouTube URLs route to local processor path', youtubeCalls.length === 1 && !turnCalls.some(call => extractYoutubeUrl(call.message)), `${youtubeCalls.length} youtube call(s)`),
|
|
2603
|
+
makeChecklistItem('no_model_ids', 'UI hides backend model ids', !modelPattern.test(outputText), 'no raw model ids in transcript'),
|
|
2604
|
+
makeChecklistItem('no_error_patterns', 'Transcript has no known crash/block patterns', !errorPattern.test(outputText), 'business TDZ/network/crash patterns absent'),
|
|
2605
|
+
makeChecklistItem('log_written', 'Dogfood leaves an inspectable ax run log', Boolean(logPath && fs.existsSync(logPath)), logPath || 'missing'),
|
|
2606
|
+
];
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
async function runChatDogfood(options = {}) {
|
|
2610
|
+
const cwd = options.cwd || process.cwd();
|
|
2611
|
+
const loops = Math.max(1, Number(options.loops) || 25);
|
|
2612
|
+
const cases = buildChatDogfoodCases(loops);
|
|
2613
|
+
const startedAt = Date.now();
|
|
2614
|
+
const chunks = [];
|
|
2615
|
+
const output = options.output || {
|
|
2616
|
+
isTTY: true,
|
|
2617
|
+
color: false,
|
|
2618
|
+
write(chunk) {
|
|
2619
|
+
chunks.push(String(chunk || ''));
|
|
2620
|
+
return true;
|
|
2621
|
+
},
|
|
2622
|
+
};
|
|
2623
|
+
const turnCalls = [];
|
|
2624
|
+
const youtubeCalls = [];
|
|
2625
|
+
const turnFunction = options.live === true ? undefined : async (message, turnOptions = {}) => {
|
|
2626
|
+
const route = resolveRoute(message, turnOptions);
|
|
2627
|
+
const payload = buildPayload(message, { ...turnOptions, route });
|
|
2628
|
+
const record = {
|
|
2629
|
+
message,
|
|
2630
|
+
mode: normalizeMode(turnOptions.mode),
|
|
2631
|
+
route,
|
|
2632
|
+
workspace_path: payload.workspace_path || null,
|
|
2633
|
+
max_turns: payload.max_turns,
|
|
2634
|
+
bypass_permissions: payload.bypass_permissions === true,
|
|
2635
|
+
allow_external_actions: payload.allow_external_actions === true,
|
|
2636
|
+
};
|
|
2637
|
+
turnCalls.push(record);
|
|
2638
|
+
const text = `dogfood ok: ${record.mode}/${record.route}`;
|
|
2639
|
+
if (turnOptions.output) turnOptions.output.write(text);
|
|
2640
|
+
return {
|
|
2641
|
+
output: text,
|
|
2642
|
+
durationMs: 1,
|
|
2643
|
+
events: [{ type: 'receipt', receipt: { billing: { billed: false }, dogfood: record } }],
|
|
2644
|
+
};
|
|
2645
|
+
};
|
|
2646
|
+
const youtubeCommand = async (argv, deps = {}) => {
|
|
2647
|
+
youtubeCalls.push(argv);
|
|
2648
|
+
if (deps.output) deps.output(`dogfood youtube: ${argv[0] || 'missing-url'}`);
|
|
2649
|
+
return 0;
|
|
2650
|
+
};
|
|
2651
|
+
const input = Readable.from(cases.map(item => `${item.input}\n`).concat('exit\n'));
|
|
2652
|
+
|
|
2653
|
+
await chat({
|
|
2654
|
+
mode: 'fast',
|
|
2655
|
+
cwd,
|
|
2656
|
+
input,
|
|
2657
|
+
output,
|
|
2658
|
+
turnFunction,
|
|
2659
|
+
youtubeCommand: options.youtubeCommand || youtubeCommand,
|
|
2660
|
+
persistPrefs: false,
|
|
2661
|
+
bypassPermissions: false,
|
|
2662
|
+
});
|
|
2663
|
+
|
|
2664
|
+
const outputText = typeof output.text === 'function' ? output.text() : chunks.join('');
|
|
2665
|
+
const logPath = newestAxLog(cwd, startedAt - 1000);
|
|
2666
|
+
const checklist = buildDogfoodChecklist({ cases, outputText, turnCalls, youtubeCalls, logPath });
|
|
2667
|
+
const failures = checklist.filter(item => !item.passed);
|
|
2668
|
+
return {
|
|
2669
|
+
schema: 'atris.ax_chat_dogfood.v1',
|
|
2670
|
+
mode: 'fast',
|
|
2671
|
+
loops_requested: loops,
|
|
2672
|
+
loops_run: cases.length,
|
|
2673
|
+
dry_run: options.live !== true,
|
|
2674
|
+
cwd,
|
|
2675
|
+
log_path: logPath,
|
|
2676
|
+
turn_calls: turnCalls,
|
|
2677
|
+
youtube_calls: youtubeCalls,
|
|
2678
|
+
checklist,
|
|
2679
|
+
failures,
|
|
2680
|
+
output: outputText,
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
function formatChatDogfoodReport(report) {
|
|
2685
|
+
const passed = report.checklist.filter(item => item.passed).length;
|
|
2686
|
+
const lines = [
|
|
2687
|
+
'Ax fast chat dogfood',
|
|
2688
|
+
`loops: ${report.loops_run}/${report.loops_requested}`,
|
|
2689
|
+
`checklist: ${passed}/${report.checklist.length} passed`,
|
|
2690
|
+
`log: ${report.log_path || 'missing'}`,
|
|
2691
|
+
'',
|
|
2692
|
+
];
|
|
2693
|
+
for (const item of report.checklist) {
|
|
2694
|
+
lines.push(`${item.passed ? 'ok' : 'fail'} ${item.id} — ${item.evidence || item.label}`);
|
|
2695
|
+
}
|
|
2696
|
+
if (report.failures.length) {
|
|
2697
|
+
lines.push('');
|
|
2698
|
+
lines.push(`failures: ${report.failures.map(item => item.id).join(', ')}`);
|
|
2699
|
+
}
|
|
2700
|
+
return lines.join('\n');
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
function safeJsonFile(filePath) {
|
|
2704
|
+
try {
|
|
2705
|
+
if (!fs.existsSync(filePath)) return null;
|
|
2706
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
2707
|
+
} catch {
|
|
2708
|
+
return null;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
function readJsonlRows(filePath) {
|
|
2713
|
+
try {
|
|
2714
|
+
if (!fs.existsSync(filePath)) return [];
|
|
2715
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
2716
|
+
.split(/\r?\n/)
|
|
2717
|
+
.filter(Boolean)
|
|
2718
|
+
.map(line => {
|
|
2719
|
+
try {
|
|
2720
|
+
return JSON.parse(line);
|
|
2721
|
+
} catch {
|
|
2722
|
+
return null;
|
|
2723
|
+
}
|
|
2724
|
+
})
|
|
2725
|
+
.filter(Boolean);
|
|
2726
|
+
} catch {
|
|
2727
|
+
return [];
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
function parseDogfoodVerifierOutput(stdout = '') {
|
|
2732
|
+
const text = String(stdout || '');
|
|
2733
|
+
const loops = text.match(/loops:\s*(\d+)\/(\d+)/i);
|
|
2734
|
+
const checklist = text.match(/checklist:\s*(\d+)\/(\d+)\s+passed/i);
|
|
2735
|
+
const log = text.match(/^log:\s*(.+)$/im);
|
|
2736
|
+
return {
|
|
2737
|
+
loops_passed: loops ? Number(loops[1]) : 0,
|
|
2738
|
+
loops_total: loops ? Number(loops[2]) : 0,
|
|
2739
|
+
checklist_passed: checklist ? Number(checklist[1]) : 0,
|
|
2740
|
+
checklist_total: checklist ? Number(checklist[2]) : 0,
|
|
2741
|
+
log_path: log ? log[1].trim() : null,
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
function latestDogfoodMission(cwd = process.cwd()) {
|
|
2746
|
+
const rows = readJsonlRows(path.join(cwd, '.atris', 'state', 'missions.jsonl'));
|
|
2747
|
+
const byId = new Map();
|
|
2748
|
+
for (const row of rows) {
|
|
2749
|
+
if (!row || row.schema !== 'atris.mission.v1') continue;
|
|
2750
|
+
if (!/--dogfood-chat/.test(String(row.verifier || ''))) continue;
|
|
2751
|
+
byId.set(row.id, row);
|
|
2752
|
+
}
|
|
2753
|
+
const missions = Array.from(byId.values());
|
|
2754
|
+
missions.sort((a, b) => new Date(b.updated_at || b.created_at || 0) - new Date(a.updated_at || a.created_at || 0));
|
|
2755
|
+
return missions[0] || null;
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
function collectDogfoodTicks(cwd, mission) {
|
|
2759
|
+
const runsDir = path.join(cwd, 'atris', 'runs');
|
|
2760
|
+
const out = new Map();
|
|
2761
|
+
if (!mission || !mission.id || !fs.existsSync(runsDir)) return [];
|
|
2762
|
+
const prefix = `mission-${mission.id}-`;
|
|
2763
|
+
for (const name of fs.readdirSync(runsDir)) {
|
|
2764
|
+
if (!name.startsWith(prefix) || !name.endsWith('.json')) continue;
|
|
2765
|
+
const full = path.join(runsDir, name);
|
|
2766
|
+
const payload = safeJsonFile(full);
|
|
2767
|
+
if (!payload) continue;
|
|
2768
|
+
let mtimeMs = 0;
|
|
2769
|
+
try {
|
|
2770
|
+
mtimeMs = fs.statSync(full).mtimeMs;
|
|
2771
|
+
} catch {}
|
|
2772
|
+
const body = payload.result || payload;
|
|
2773
|
+
const stdout = body.verifier_result?.stdout || payload.verifier_result?.stdout || payload.mission?.verifier_result?.stdout || '';
|
|
2774
|
+
const ticks = [];
|
|
2775
|
+
if (payload.tick) ticks.push(payload.tick);
|
|
2776
|
+
if (body.tick) ticks.push(body.tick);
|
|
2777
|
+
if (Array.isArray(payload.ticks)) ticks.push(...payload.ticks);
|
|
2778
|
+
if (Array.isArray(body.ticks)) ticks.push(...body.ticks);
|
|
2779
|
+
for (const tick of ticks) {
|
|
2780
|
+
const index = Number(tick && tick.tick_index);
|
|
2781
|
+
if (!Number.isFinite(index) || index < 1) continue;
|
|
2782
|
+
const previous = out.get(index);
|
|
2783
|
+
if (previous && previous.mtimeMs > mtimeMs) continue;
|
|
2784
|
+
const summary = parseDogfoodVerifierOutput(stdout);
|
|
2785
|
+
out.set(index, {
|
|
2786
|
+
index,
|
|
2787
|
+
verifier_passed: tick.verifier_passed === true || body.verifier_result?.passed === true || payload.verifier_result?.passed === true,
|
|
2788
|
+
receipt_path: path.relative(cwd, full),
|
|
2789
|
+
mtimeMs,
|
|
2790
|
+
...summary,
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
return Array.from(out.values()).sort((a, b) => a.index - b.index);
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
function dogfoodCrontabInstalled(crontabText = null) {
|
|
2798
|
+
const text = crontabText !== null ? String(crontabText || '') : (() => {
|
|
2799
|
+
try {
|
|
2800
|
+
const res = spawnSync('crontab', ['-l'], { encoding: 'utf8', timeout: 8000 });
|
|
2801
|
+
return res.status === 0 ? String(res.stdout || '') : '';
|
|
2802
|
+
} catch {
|
|
2803
|
+
return '';
|
|
2804
|
+
}
|
|
2805
|
+
})();
|
|
2806
|
+
return /ATRIS_AX_FAST_CHAT_DOGFOOD/.test(text);
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
function countDogfoodCronRuns(cwd = process.cwd(), cronLogText = null) {
|
|
2810
|
+
const text = cronLogText !== null
|
|
2811
|
+
? String(cronLogText || '')
|
|
2812
|
+
: String(safeReadText(path.join(cwd, 'atris', 'runs', 'ax-fast-chat-overnight-cron.log')) || '');
|
|
2813
|
+
return (text.match(/^=== ax fast chat dogfood tick /gm) || []).length;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
function safeReadText(filePath) {
|
|
2817
|
+
try {
|
|
2818
|
+
if (!fs.existsSync(filePath)) return '';
|
|
2819
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
2820
|
+
} catch {
|
|
2821
|
+
return '';
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
function buildChatDogfoodStatus(options = {}) {
|
|
2826
|
+
const cwd = options.cwd || process.cwd();
|
|
2827
|
+
const targetLoops = Math.max(1, Number(options.targetLoops) || 25);
|
|
2828
|
+
const mission = options.mission || latestDogfoodMission(cwd);
|
|
2829
|
+
const ticks = collectDogfoodTicks(cwd, mission);
|
|
2830
|
+
const cleanTicks = ticks.filter(tick => tick.verifier_passed && tick.loops_passed > 0);
|
|
2831
|
+
const cleanLoops = cleanTicks.reduce((sum, tick) => sum + tick.loops_passed, 0);
|
|
2832
|
+
const latestClean = cleanTicks[cleanTicks.length - 1] || null;
|
|
2833
|
+
const latestTick = ticks[ticks.length - 1] || null;
|
|
2834
|
+
const checklistPassed = latestClean && latestClean.checklist_total > 0 && latestClean.checklist_passed === latestClean.checklist_total;
|
|
2835
|
+
const cronInstalled = dogfoodCrontabInstalled(options.crontabText ?? null);
|
|
2836
|
+
const cronRuns = countDogfoodCronRuns(cwd, options.cronLogText ?? null);
|
|
2837
|
+
return {
|
|
2838
|
+
schema: 'atris.ax_chat_dogfood_status.v1',
|
|
2839
|
+
cwd,
|
|
2840
|
+
mission_id: mission ? mission.id : null,
|
|
2841
|
+
mission_status: mission ? mission.status : null,
|
|
2842
|
+
mission_cadence: mission ? mission.cadence : null,
|
|
2843
|
+
last_tick_index: mission ? Number(mission.last_tick_index || 0) : 0,
|
|
2844
|
+
last_tick_at: mission ? mission.last_tick_at || null : null,
|
|
2845
|
+
target_loops: targetLoops,
|
|
2846
|
+
clean_chat_loops: cleanLoops,
|
|
2847
|
+
remaining_loops: Math.max(0, targetLoops - cleanLoops),
|
|
2848
|
+
target_met: cleanLoops >= targetLoops,
|
|
2849
|
+
clean_tick_count: cleanTicks.length,
|
|
2850
|
+
latest_receipt_path: latestClean ? latestClean.receipt_path : latestTick ? latestTick.receipt_path : null,
|
|
2851
|
+
latest_log_path: latestClean ? latestClean.log_path : null,
|
|
2852
|
+
latest_checklist_passed: checklistPassed === true,
|
|
2853
|
+
cron_installed: cronInstalled,
|
|
2854
|
+
cron_runs: cronRuns,
|
|
2855
|
+
overnight_proven: cronRuns > 0 && cleanLoops >= targetLoops,
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
function formatChatDogfoodStatusReport(status) {
|
|
2860
|
+
const lines = [
|
|
2861
|
+
'Ax fast chat overnight',
|
|
2862
|
+
`mission: ${status.mission_id || 'missing'}`,
|
|
2863
|
+
`status: ${status.mission_status || 'missing'} · cadence ${status.mission_cadence || 'n/a'}`,
|
|
2864
|
+
`loops: ${status.clean_chat_loops}/${status.target_loops} clean (${status.remaining_loops} remaining)`,
|
|
2865
|
+
`ticks: ${status.clean_tick_count} clean · last ${status.last_tick_index || 0} at ${status.last_tick_at || 'never'}`,
|
|
2866
|
+
`checklist: ${status.latest_checklist_passed ? 'passed' : 'missing/failing'}`,
|
|
2867
|
+
`cron: ${status.cron_installed ? 'installed' : 'missing'} · runs ${status.cron_runs}`,
|
|
2868
|
+
`receipt: ${status.latest_receipt_path || 'missing'}`,
|
|
2869
|
+
`log: ${status.latest_log_path || 'missing'}`,
|
|
2870
|
+
];
|
|
2871
|
+
lines.push(status.overnight_proven ? 'result: overnight 25-loop proof is complete' : 'result: still collecting overnight proof');
|
|
2872
|
+
return lines.join('\n');
|
|
2873
|
+
}
|
|
2874
|
+
|
|
1417
2875
|
function printBackendHint() {
|
|
1418
2876
|
console.log('');
|
|
1419
|
-
console.log('
|
|
1420
|
-
console.log(
|
|
2877
|
+
console.log('Hosted lane:');
|
|
2878
|
+
console.log('ax --cloud "hello"');
|
|
2879
|
+
console.log('');
|
|
2880
|
+
console.log('Cloud API:');
|
|
2881
|
+
console.log(`ATRIS_API_BASE=${BACKEND.publicBase}`);
|
|
2882
|
+
console.log('');
|
|
2883
|
+
console.log('Local workspace lane:');
|
|
2884
|
+
console.log(`Set AX_BACKEND_URL=http://${BACKEND.host}:${BACKEND.port} after starting a local Atris2 backend.`);
|
|
1421
2885
|
}
|
|
1422
2886
|
|
|
1423
2887
|
function bufferedOutput() {
|
|
@@ -1597,12 +3061,35 @@ async function runBenchmark(options = {}) {
|
|
|
1597
3061
|
}
|
|
1598
3062
|
|
|
1599
3063
|
async function main() {
|
|
1600
|
-
|
|
3064
|
+
let args = process.argv.slice(2);
|
|
3065
|
+
if (isAxSpawnCommand(args[0])) {
|
|
3066
|
+
try {
|
|
3067
|
+
runAxSpawnCommand(args, { root: process.cwd() });
|
|
3068
|
+
} catch (error) {
|
|
3069
|
+
console.error(`x ${error.message}`);
|
|
3070
|
+
process.exit(1);
|
|
3071
|
+
}
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
if (args[0] === 'youtube') {
|
|
3075
|
+
try {
|
|
3076
|
+
await runAxYoutubeCommand(args, { output: (line = '') => console.log(line) });
|
|
3077
|
+
} catch (error) {
|
|
3078
|
+
console.error(`x ${error.message}`);
|
|
3079
|
+
process.exit(1);
|
|
3080
|
+
}
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
1601
3083
|
if (args.includes('--help') || args.includes('-h')) {
|
|
1602
3084
|
console.log(formatUsage());
|
|
1603
3085
|
return;
|
|
1604
3086
|
}
|
|
1605
3087
|
|
|
3088
|
+
const bypassPermissions = parsePermissionFlags(args);
|
|
3089
|
+
args = stripPermissionFlags(args);
|
|
3090
|
+
const chatArgs = normalizeChatCommandArgs(args);
|
|
3091
|
+
args = chatArgs.args;
|
|
3092
|
+
|
|
1606
3093
|
// --business <slug>: chat against that business's cloud workspace. The flag
|
|
1607
3094
|
// takes a value, so splice the pair out before building the prompt.
|
|
1608
3095
|
let businessSlug = null;
|
|
@@ -1629,17 +3116,63 @@ async function main() {
|
|
|
1629
3116
|
args.splice(verifyIdx, 2);
|
|
1630
3117
|
}
|
|
1631
3118
|
|
|
1632
|
-
|
|
3119
|
+
let dogfoodLoops = 25;
|
|
3120
|
+
const loopsIdx = args.indexOf('--loops');
|
|
3121
|
+
if (loopsIdx !== -1) {
|
|
3122
|
+
dogfoodLoops = Number(args[loopsIdx + 1]);
|
|
3123
|
+
if (!Number.isInteger(dogfoodLoops) || dogfoodLoops < 1 || dogfoodLoops > 100) {
|
|
3124
|
+
console.error('Usage: ax --dogfood-chat --loops <1-100>');
|
|
3125
|
+
process.exit(1);
|
|
3126
|
+
}
|
|
3127
|
+
args.splice(loopsIdx, 2);
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
const mode = args.includes('--code-fast') || args.includes('--code') ? 'code-fast'
|
|
3131
|
+
: args.includes('--max') ? 'max'
|
|
3132
|
+
: args.includes('--pro') ? 'pro'
|
|
3133
|
+
: 'fast';
|
|
1633
3134
|
const doctor = args.includes('--doctor');
|
|
1634
3135
|
const benchmark = args.includes('--benchmark');
|
|
3136
|
+
const dogfoodChat = args.includes('--dogfood-chat');
|
|
3137
|
+
const dogfoodStatus = args.includes('--dogfood-status');
|
|
3138
|
+
const dogfoodJson = dogfoodChat && args.includes('--json');
|
|
3139
|
+
const dogfoodLive = dogfoodChat && args.includes('--live');
|
|
1635
3140
|
const forceCloud = args.includes('--cloud');
|
|
1636
3141
|
const forceLocal = args.includes('--local');
|
|
1637
3142
|
const route = forceCloud ? 'cloud' : forceLocal ? 'local' : 'auto';
|
|
1638
3143
|
const prompt = args
|
|
1639
|
-
.filter(arg => !['--max', '--fast', '--pro', '--code-fast', '--code', '--chat', '--doctor', '--benchmark', '--local', '--cloud', '--help', '-h'].includes(arg))
|
|
3144
|
+
.filter(arg => !['--max', '--fast', '--pro', '--code-fast', '--code', '--chat', '--doctor', '--benchmark', '--dogfood-chat', '--dogfood-status', '--json', '--live', '--local', '--cloud', '--help', '-h'].includes(arg))
|
|
1640
3145
|
.join(' ')
|
|
1641
3146
|
.trim();
|
|
1642
3147
|
|
|
3148
|
+
if (dogfoodStatus) {
|
|
3149
|
+
const status = buildChatDogfoodStatus({ cwd: process.cwd(), targetLoops: dogfoodLoops });
|
|
3150
|
+
console.log(args.includes('--json') ? JSON.stringify(status, null, 2) : formatChatDogfoodStatusReport(status));
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
if (dogfoodChat) {
|
|
3155
|
+
try {
|
|
3156
|
+
const report = await runChatDogfood({ mode, cwd: process.cwd(), loops: dogfoodLoops, live: dogfoodLive });
|
|
3157
|
+
console.log(dogfoodJson ? JSON.stringify(report, null, 2) : formatChatDogfoodReport(report));
|
|
3158
|
+
if (report.failures.length) process.exit(1);
|
|
3159
|
+
} catch (error) {
|
|
3160
|
+
console.error(`x ${error.message}`);
|
|
3161
|
+
process.exit(1);
|
|
3162
|
+
}
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
if (prompt && extractYoutubeUrl(prompt)) {
|
|
3167
|
+
try {
|
|
3168
|
+
await runAxYoutubeCommand([prompt], { output: (line = '') => console.log(line) });
|
|
3169
|
+
} catch (error) {
|
|
3170
|
+
console.error(`x ${error.message}`);
|
|
3171
|
+
process.exit(1);
|
|
3172
|
+
}
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
|
|
1643
3176
|
let business = null;
|
|
1644
3177
|
if (businessSlug) {
|
|
1645
3178
|
if (normalizeMode(mode) === 'code-fast') {
|
|
@@ -1671,6 +3204,15 @@ async function main() {
|
|
|
1671
3204
|
console.log(`cloud workspace: ${biz.businessName || businessSlug}`);
|
|
1672
3205
|
}
|
|
1673
3206
|
|
|
3207
|
+
const runOptions = {
|
|
3208
|
+
mode,
|
|
3209
|
+
cwd: process.cwd(),
|
|
3210
|
+
route: route === 'auto' ? undefined : route,
|
|
3211
|
+
business,
|
|
3212
|
+
verify,
|
|
3213
|
+
bypassPermissions,
|
|
3214
|
+
};
|
|
3215
|
+
|
|
1674
3216
|
try {
|
|
1675
3217
|
if (benchmark) {
|
|
1676
3218
|
await runBenchmark({ mode, cwd: process.cwd(), output: process.stdout });
|
|
@@ -1678,20 +3220,25 @@ async function main() {
|
|
|
1678
3220
|
}
|
|
1679
3221
|
|
|
1680
3222
|
if (doctor) {
|
|
1681
|
-
console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }, process.stdout));
|
|
3223
|
+
console.log(formatHeader({ mode, cwd: process.cwd(), chat: false, bypassPermissions: resolveBypassPermissions(runOptions) }, process.stdout));
|
|
1682
3224
|
console.log('');
|
|
1683
|
-
console.log(formatRunProfile(buildRunProfile(
|
|
3225
|
+
console.log(formatRunProfile(buildRunProfile(runOptions), process.stdout));
|
|
1684
3226
|
return;
|
|
1685
3227
|
}
|
|
1686
3228
|
|
|
1687
|
-
if (!prompt ||
|
|
1688
|
-
await chat(
|
|
3229
|
+
if (!prompt || chatArgs.chatRequested) {
|
|
3230
|
+
await chat(runOptions);
|
|
1689
3231
|
return;
|
|
1690
3232
|
}
|
|
1691
3233
|
|
|
1692
|
-
console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }, process.stdout));
|
|
3234
|
+
console.log(formatHeader({ mode, cwd: process.cwd(), chat: false, bypassPermissions: resolveBypassPermissions(runOptions) }, process.stdout));
|
|
1693
3235
|
console.log('');
|
|
1694
|
-
const result = await turnFunctionForMode(mode)(prompt,
|
|
3236
|
+
const result = await turnFunctionForMode(mode)(prompt, runOptions);
|
|
3237
|
+
if (result.interrupted) {
|
|
3238
|
+
console.log('');
|
|
3239
|
+
console.log('· Interrupted');
|
|
3240
|
+
process.exit(130);
|
|
3241
|
+
}
|
|
1695
3242
|
console.log('');
|
|
1696
3243
|
console.log(formatDoneLine(result.durationMs, creditsFromState(result)));
|
|
1697
3244
|
} catch (error) {
|
|
@@ -1710,15 +3257,29 @@ module.exports = {
|
|
|
1710
3257
|
authUserId,
|
|
1711
3258
|
backendBaseUrl,
|
|
1712
3259
|
backendUrl,
|
|
3260
|
+
buildAxYoutubeArgs,
|
|
1713
3261
|
buildCodeFastPayload,
|
|
3262
|
+
buildChatDogfoodCases,
|
|
3263
|
+
buildChatDogfoodStatus,
|
|
3264
|
+
buildMessage,
|
|
1714
3265
|
buildPayload,
|
|
1715
3266
|
cachedIntegrationStatus,
|
|
1716
3267
|
buildConnectionContext,
|
|
1717
3268
|
buildRunProfile,
|
|
1718
3269
|
chat,
|
|
1719
3270
|
chatCompleter,
|
|
3271
|
+
chatGoalCommand: parseGoalCommand,
|
|
1720
3272
|
chatMenu,
|
|
3273
|
+
chatPermissionCommand,
|
|
1721
3274
|
chatTierCommand,
|
|
3275
|
+
loadAxPrefs,
|
|
3276
|
+
MEMBER_SLUG,
|
|
3277
|
+
parsePermissionFlags,
|
|
3278
|
+
permissionsLabel,
|
|
3279
|
+
printBackendHint,
|
|
3280
|
+
resolveBypassPermissions,
|
|
3281
|
+
runAxSpawnCommand,
|
|
3282
|
+
setBypassPermissions,
|
|
1722
3283
|
codeFastWorkspaceNotice,
|
|
1723
3284
|
creditsFromState,
|
|
1724
3285
|
codeFastBaseUrl,
|
|
@@ -1726,24 +3287,55 @@ module.exports = {
|
|
|
1726
3287
|
createRunLogger,
|
|
1727
3288
|
createProgressReporter,
|
|
1728
3289
|
formatDoneLine,
|
|
3290
|
+
formatChatDogfoodReport,
|
|
3291
|
+
formatChatDogfoodStatusReport,
|
|
1729
3292
|
formatDuration,
|
|
1730
3293
|
formatHeader,
|
|
1731
3294
|
formatPathSubject,
|
|
3295
|
+
buildInputBoxBottomPlain,
|
|
3296
|
+
buildInputBoxStatusPlain,
|
|
3297
|
+
buildInputBoxTopPlain,
|
|
3298
|
+
closeInputBox,
|
|
3299
|
+
repaintInputBoxBottom,
|
|
3300
|
+
formatChatInputInnerPrefix,
|
|
3301
|
+
formatChatInputPrompt,
|
|
3302
|
+
formatInputBoxBottom,
|
|
3303
|
+
formatInputBoxInputRow,
|
|
3304
|
+
formatInputBoxStatus,
|
|
3305
|
+
formatInputBoxTop,
|
|
3306
|
+
formatPermissionModeBrief,
|
|
3307
|
+
formatPermissionToggleMessage,
|
|
1732
3308
|
formatPrompt,
|
|
3309
|
+
insertMultilineBreak,
|
|
3310
|
+
inputBoxLayoutOptions,
|
|
3311
|
+
inputBoxPlainWidth,
|
|
3312
|
+
isMultilineInsertKey,
|
|
3313
|
+
isPermissionToggleKey,
|
|
3314
|
+
permissionAccentCodes,
|
|
1733
3315
|
formatRunProfile,
|
|
1734
3316
|
formatStatusMessage,
|
|
1735
3317
|
formatSystemInit,
|
|
1736
3318
|
formatUsage,
|
|
1737
3319
|
formatWorkingLine,
|
|
1738
3320
|
handleEvent,
|
|
3321
|
+
extractYoutubeUrl,
|
|
3322
|
+
isAxSpawnCommand,
|
|
3323
|
+
manageTurnInterrupt,
|
|
1739
3324
|
modelForMode,
|
|
3325
|
+
normalizeChatCommandArgs,
|
|
1740
3326
|
parseSseBlock,
|
|
1741
3327
|
postCodeFastTurn,
|
|
1742
3328
|
postTurn,
|
|
3329
|
+
renderTerminalInline,
|
|
3330
|
+
renderTerminalBlockLine,
|
|
3331
|
+
formatMarkdownLink,
|
|
1743
3332
|
renderStreamingMarkdown,
|
|
3333
|
+
renderShimmerText,
|
|
1744
3334
|
renderTerminalMarkdown,
|
|
1745
3335
|
resolveRoute,
|
|
1746
3336
|
runBenchmark,
|
|
3337
|
+
runChatDogfood,
|
|
3338
|
+
runAxYoutubeCommand,
|
|
1747
3339
|
summarizeToolInput,
|
|
1748
3340
|
summarizeToolResult,
|
|
1749
3341
|
tierLabel
|