0agent 1.0.46 → 1.0.47
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/bin/chat.js +168 -21
- package/package.json +1 -1
package/bin/chat.js
CHANGED
|
@@ -130,6 +130,117 @@ const C = {
|
|
|
130
130
|
const fmt = (color, text) => `${color}${text}${C.reset}`;
|
|
131
131
|
const clearLine = () => process.stdout.write('\r\x1b[2K');
|
|
132
132
|
|
|
133
|
+
// ─── Markdown renderer ────────────────────────────────────────────────────────
|
|
134
|
+
// Applied to the full streamed response at session.completed — rewrites raw
|
|
135
|
+
// LLM output with ANSI formatting (bold, code, headers, bullets).
|
|
136
|
+
function renderMarkdown(text) {
|
|
137
|
+
const lines = text.split('\n');
|
|
138
|
+
const out = [];
|
|
139
|
+
let inCode = false;
|
|
140
|
+
let codeLang = '';
|
|
141
|
+
|
|
142
|
+
for (const raw of lines) {
|
|
143
|
+
if (raw.startsWith('```')) {
|
|
144
|
+
inCode = !inCode;
|
|
145
|
+
codeLang = inCode ? raw.slice(3).trim() : '';
|
|
146
|
+
if (!inCode) out.push(''); // blank after code block
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (inCode) {
|
|
150
|
+
out.push(` \x1b[36m${raw}\x1b[0m`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let line = raw;
|
|
155
|
+
|
|
156
|
+
// Headers
|
|
157
|
+
if (line.startsWith('### ')) line = `\x1b[1m${line.slice(4)}\x1b[0m`;
|
|
158
|
+
else if (line.startsWith('## ')) line = `\x1b[1;4m${line.slice(3)}\x1b[0m`;
|
|
159
|
+
else if (line.startsWith('# ')) line = `\x1b[1;4m${line.slice(2)}\x1b[0m`;
|
|
160
|
+
// Bullets
|
|
161
|
+
else if (/^[-*] /.test(line)) line = ` \x1b[36m·\x1b[0m ${line.slice(2)}`;
|
|
162
|
+
else if (/^\d+\. /.test(line)) line = ` ${line}`;
|
|
163
|
+
// Horizontal rule
|
|
164
|
+
else if (/^-{3,}$/.test(line)) line = `\x1b[2m${'─'.repeat(54)}\x1b[0m`;
|
|
165
|
+
|
|
166
|
+
// Inline: bold, code, italic
|
|
167
|
+
line = line
|
|
168
|
+
.replace(/\*\*([^*\n]+)\*\*/g, '\x1b[1m$1\x1b[0m')
|
|
169
|
+
.replace(/`([^`\n]+)`/g, '\x1b[36m$1\x1b[0m')
|
|
170
|
+
.replace(/\*([^*\s][^*\n]*)\*/g,'\x1b[3m$1\x1b[0m');
|
|
171
|
+
|
|
172
|
+
out.push(' ' + line);
|
|
173
|
+
}
|
|
174
|
+
return out.join('\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Step formatter ───────────────────────────────────────────────────────────
|
|
178
|
+
// Converts raw step labels from AgentExecutor into icon + clean readable form.
|
|
179
|
+
function formatStep(step) {
|
|
180
|
+
const ICONS = {
|
|
181
|
+
shell_exec: `\x1b[33m⚡\x1b[0m`,
|
|
182
|
+
file_op: `\x1b[34m◈\x1b[0m`,
|
|
183
|
+
web_search: `\x1b[35m⌕\x1b[0m`,
|
|
184
|
+
scrape_url: `\x1b[35m◎\x1b[0m`,
|
|
185
|
+
memory_write: `\x1b[32m◆\x1b[0m`,
|
|
186
|
+
browser_open: `\x1b[34m◉\x1b[0m`,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Tool call: "▶ shell_exec("cmd")"
|
|
190
|
+
const toolMatch = step.match(/^▶\s+(\w+)\((.{0,100})\)/);
|
|
191
|
+
if (toolMatch) {
|
|
192
|
+
const [, tool, args] = toolMatch;
|
|
193
|
+
const icon = ICONS[tool] ?? fmt(C.dim, '›');
|
|
194
|
+
const clean = args.replace(/^["'](.*)["']$/, '$1').replace(/\\n/g, ' ').slice(0, 72);
|
|
195
|
+
return ` ${icon} \x1b[2m${clean}\x1b[0m`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Result: " ↳ text"
|
|
199
|
+
if (/^\s*↳/.test(step)) {
|
|
200
|
+
const text = step.replace(/^\s*↳\s*/, '');
|
|
201
|
+
return ` \x1b[2m↳ ${text.slice(0, 100)}\x1b[0m`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Thinking / Continuing (suppress — replaced by startSession static status)
|
|
205
|
+
if (/^(Thinking|Continuing)/.test(step)) return null;
|
|
206
|
+
|
|
207
|
+
// Summary lines (Done, Files written, Commands run)
|
|
208
|
+
if (/^(Done|Files|Commands)/.test(step))
|
|
209
|
+
return ` \x1b[2m${step}\x1b[0m`;
|
|
210
|
+
|
|
211
|
+
return ` \x1b[2m› ${step}\x1b[0m`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── Cost estimator ───────────────────────────────────────────────────────────
|
|
215
|
+
function estimateCost(model, tokens) {
|
|
216
|
+
const RATES = { // $ per 1M tokens (blended in+out)
|
|
217
|
+
'claude-sonnet-4-6': 4.0, 'claude-opus-4-6': 22.0, 'claude-haiku-4-5': 0.5,
|
|
218
|
+
'gpt-4o': 5.0, 'gpt-4o-mini': 0.2, 'grok-3': 3.0,
|
|
219
|
+
'gemini-2.0-flash': 0.12, 'gemini-2.0-pro': 3.5,
|
|
220
|
+
};
|
|
221
|
+
if (!model || !tokens) return '';
|
|
222
|
+
const key = Object.keys(RATES).find(k => String(model).includes(k));
|
|
223
|
+
if (!key) return '';
|
|
224
|
+
const usd = (tokens / 1_000_000) * RATES[key];
|
|
225
|
+
return usd < 0.01 ? ' · <$0.01' : ` · $${usd.toFixed(3)}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── OS notification ──────────────────────────────────────────────────────────
|
|
229
|
+
async function notifyDone(task, success) {
|
|
230
|
+
try {
|
|
231
|
+
const { execSync } = await import('node:child_process');
|
|
232
|
+
const title = success ? '0agent ✓' : '0agent ✗';
|
|
233
|
+
const body = task.replace(/'/g, '').slice(0, 80);
|
|
234
|
+
if (process.platform === 'darwin') {
|
|
235
|
+
execSync(`osascript -e 'display notification "${body}" with title "${title}"'`,
|
|
236
|
+
{ stdio: 'ignore', timeout: 3000 });
|
|
237
|
+
} else if (process.platform === 'linux') {
|
|
238
|
+
execSync(`notify-send "${title}" "${body}" 2>/dev/null`,
|
|
239
|
+
{ stdio: 'ignore', timeout: 3000 });
|
|
240
|
+
}
|
|
241
|
+
} catch {}
|
|
242
|
+
}
|
|
243
|
+
|
|
133
244
|
// ─── LLM ping — direct 1-token call, bypasses daemon, instant ────────────────
|
|
134
245
|
async function pingLLM(provider) {
|
|
135
246
|
const key = provider.api_key ?? '';
|
|
@@ -195,25 +306,30 @@ function getCurrentProvider(cfg) {
|
|
|
195
306
|
}
|
|
196
307
|
|
|
197
308
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
198
|
-
let cfg
|
|
309
|
+
let cfg = loadConfig();
|
|
199
310
|
let sessionId = null;
|
|
200
311
|
const messageQueue = []; // queued tasks while session is running
|
|
201
312
|
let lastFailedTask = null; // for retry-on-abort
|
|
202
313
|
let streaming = false;
|
|
314
|
+
let streamLineCount = 0; // newlines printed during streaming (for re-render)
|
|
203
315
|
let ws = null;
|
|
204
316
|
let wsReady = false;
|
|
205
317
|
let pendingResolve = null;
|
|
206
318
|
let lineBuffer = '';
|
|
319
|
+
let currentTask = ''; // task being executed (for notifications)
|
|
320
|
+
let sessionStartMs = 0; // when current session started (for elapsed time)
|
|
207
321
|
const spinner = new Spinner('Thinking');
|
|
208
|
-
const history
|
|
322
|
+
const history = []; // command history for arrow keys
|
|
209
323
|
|
|
210
324
|
// ─── Header ──────────────────────────────────────────────────────────────────
|
|
211
325
|
function printHeader() {
|
|
212
326
|
const provider = getCurrentProvider(cfg);
|
|
213
327
|
const modelStr = provider ? `${provider.provider}/${provider.model}` : 'no model';
|
|
328
|
+
const ws = cfg?.workspace?.path ?? null;
|
|
214
329
|
console.log();
|
|
215
|
-
console.log(fmt(C.bold, '
|
|
216
|
-
console.log(fmt(C.dim,
|
|
330
|
+
console.log(` ${fmt(C.bold, '0agent')} ${fmt(C.dim, '·')} ${fmt(C.cyan, modelStr)}`);
|
|
331
|
+
if (ws) console.log(fmt(C.dim, ` ${ws}`));
|
|
332
|
+
console.log(fmt(C.dim, '\n Type a task, or / for commands.\n'));
|
|
217
333
|
}
|
|
218
334
|
|
|
219
335
|
function printInsights() {
|
|
@@ -252,23 +368,26 @@ function handleWsEvent(event) {
|
|
|
252
368
|
switch (event.type) {
|
|
253
369
|
case 'session.step': {
|
|
254
370
|
spinner.stop();
|
|
255
|
-
if (streaming) { process.stdout.write('\n'); streaming = false; }
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
371
|
+
if (streaming) { process.stdout.write('\n'); streaming = false; streamLineCount = 0; }
|
|
372
|
+
const formatted = formatStep(event.step);
|
|
373
|
+
if (formatted !== null) {
|
|
374
|
+
process.stdout.write('\r\x1b[2K');
|
|
375
|
+
console.log(formatted);
|
|
376
|
+
}
|
|
259
377
|
spinner.startSession(event.step.slice(0, 50));
|
|
260
|
-
rl.prompt(true);
|
|
378
|
+
rl.prompt(true);
|
|
261
379
|
break;
|
|
262
380
|
}
|
|
263
381
|
case 'session.token': {
|
|
264
382
|
spinner.stop();
|
|
265
383
|
if (!streaming) {
|
|
266
|
-
|
|
267
|
-
process.stdout.write('\r\x1b[2K\n ');
|
|
384
|
+
process.stdout.write('\r\x1b[2K\n');
|
|
268
385
|
streaming = true;
|
|
386
|
+
streamLineCount = 1;
|
|
269
387
|
}
|
|
270
388
|
process.stdout.write(event.token);
|
|
271
389
|
lineBuffer += event.token;
|
|
390
|
+
streamLineCount += (event.token.match(/\n/g) || []).length;
|
|
272
391
|
break;
|
|
273
392
|
}
|
|
274
393
|
case 'runtime.heal_proposal': {
|
|
@@ -353,10 +472,29 @@ function handleWsEvent(event) {
|
|
|
353
472
|
}
|
|
354
473
|
case 'session.completed': {
|
|
355
474
|
spinner.stop();
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (
|
|
359
|
-
|
|
475
|
+
|
|
476
|
+
// Re-render streamed response with markdown (rewind cursor, clear, reprint)
|
|
477
|
+
if (streaming) {
|
|
478
|
+
const rendered = renderMarkdown(lineBuffer.trim());
|
|
479
|
+
const rewound = streamLineCount + 1;
|
|
480
|
+
process.stdout.write(`\x1b[${rewound}A\x1b[0J`); // move up + clear to end
|
|
481
|
+
process.stdout.write(rendered + '\n');
|
|
482
|
+
streaming = false;
|
|
483
|
+
streamLineCount = 0;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const r = event.result ?? {};
|
|
487
|
+
const elapsed = sessionStartMs ? `${((Date.now() - sessionStartMs) / 1000).toFixed(1)}s` : '';
|
|
488
|
+
const cost = estimateCost(r.model, r.tokens_used);
|
|
489
|
+
|
|
490
|
+
if (r.files_written?.length)
|
|
491
|
+
console.log(`\n ${fmt(C.green, '✓')} ${r.files_written.join(', ')}`);
|
|
492
|
+
|
|
493
|
+
// Stats line: tokens · model · elapsed · cost
|
|
494
|
+
if (r.tokens_used) {
|
|
495
|
+
process.stdout.write(fmt(C.dim,
|
|
496
|
+
`\n ${r.tokens_used.toLocaleString()} tokens · ${r.model ?? ''}${elapsed ? ` · ${elapsed}` : ''}${cost}\n`));
|
|
497
|
+
}
|
|
360
498
|
|
|
361
499
|
// Contextual next-step suggestions
|
|
362
500
|
const suggestions = _suggestNext(lineBuffer, r);
|
|
@@ -366,18 +504,22 @@ function handleWsEvent(event) {
|
|
|
366
504
|
);
|
|
367
505
|
}
|
|
368
506
|
|
|
369
|
-
//
|
|
507
|
+
// OS notification for tasks that took > 8s (user may have switched windows)
|
|
508
|
+
if (sessionStartMs && Date.now() - sessionStartMs > 8000) {
|
|
509
|
+
notifyDone(currentTask, true);
|
|
510
|
+
}
|
|
511
|
+
|
|
370
512
|
confirmServer(r, lineBuffer);
|
|
371
513
|
lineBuffer = '';
|
|
372
514
|
if (pendingResolve) { pendingResolve(); pendingResolve = null; }
|
|
373
515
|
sessionId = null;
|
|
374
|
-
// auto-drain queued messages
|
|
375
516
|
drainQueue();
|
|
376
517
|
break;
|
|
377
518
|
}
|
|
378
519
|
case 'session.failed': {
|
|
379
520
|
spinner.stop();
|
|
380
|
-
if (streaming) { process.stdout.write('\n'); streaming = false; }
|
|
521
|
+
if (streaming) { process.stdout.write('\n'); streaming = false; streamLineCount = 0; }
|
|
522
|
+
if (sessionStartMs && Date.now() - sessionStartMs > 8000) notifyDone(currentTask, false);
|
|
381
523
|
const isAbort = /aborted|timeout|AbortError/i.test(event.error ?? '');
|
|
382
524
|
console.log(`\n ${fmt(C.red, '✗')} ${event.error}\n`);
|
|
383
525
|
// Offer retry if it was a timeout/abort
|
|
@@ -478,7 +620,9 @@ async function runTask(input) {
|
|
|
478
620
|
body: JSON.stringify(body),
|
|
479
621
|
});
|
|
480
622
|
const s = await res.json();
|
|
481
|
-
sessionId
|
|
623
|
+
sessionId = s.session_id ?? s.id;
|
|
624
|
+
sessionStartMs = Date.now();
|
|
625
|
+
currentTask = task;
|
|
482
626
|
// Start session-mode status (no \r animation) then restore › so user can type
|
|
483
627
|
process.stdout.write('\n');
|
|
484
628
|
spinner.startSession('Thinking');
|
|
@@ -513,8 +657,11 @@ async function runTask(input) {
|
|
|
513
657
|
const steps = session.steps ?? [];
|
|
514
658
|
for (let j = lastPolledStep; j < steps.length; j++) {
|
|
515
659
|
spinner.stop();
|
|
516
|
-
|
|
517
|
-
|
|
660
|
+
const formatted = formatStep(steps[j].description);
|
|
661
|
+
if (formatted !== null) {
|
|
662
|
+
process.stdout.write('\r\x1b[2K');
|
|
663
|
+
console.log(formatted);
|
|
664
|
+
}
|
|
518
665
|
spinner.startSession(steps[j].description.slice(0, 50));
|
|
519
666
|
rl.prompt(true);
|
|
520
667
|
}
|