0agent 1.0.46 → 1.0.48

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.
Files changed (2) hide show
  1. package/bin/chat.js +198 -45
  2. 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 = loadConfig();
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 = []; // command history for arrow keys
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, ' 0agent') + fmt(C.dim, ` ${modelStr}`));
216
- console.log(fmt(C.dim, ' Type a task or /command — press Tab to browse, / to see all.\n'));
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
- // Clear current readline line, print step, then restore › prompt
257
- process.stdout.write('\r\x1b[2K');
258
- console.log(` ${fmt(C.dim, '')} ${event.step}`);
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); // restore › so user can keep typing
378
+ rl.prompt(true);
261
379
  break;
262
380
  }
263
381
  case 'session.token': {
264
382
  spinner.stop();
265
383
  if (!streaming) {
266
- // Clear › prompt line before streaming response
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
- if (streaming) { process.stdout.write('\n'); streaming = false; }
357
- const r = event.result ?? {};
358
- if (r.files_written?.length) console.log(`\n ${fmt(C.green, '✓')} ${r.files_written.join(', ')}`);
359
- if (r.tokens_used) process.stdout.write(fmt(C.dim, `\n ${r.tokens_used} tokens · ${r.model ?? ''}\n`));
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
- // Confirm server if port mentioned
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 = s.session_id ?? s.id;
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
- process.stdout.write('\r\x1b[2K');
517
- console.log(` \x1b[2m›\x1b[0m ${steps[j].description}`);
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
  }
@@ -855,51 +1002,55 @@ async function openPalette(initialFilter = '') {
855
1002
 
856
1003
  let filter = initialFilter.toLowerCase();
857
1004
  let idx = 0;
858
- let drawn = 0; // number of lines we've printed so far
1005
+ let scroll = 0; // top of the visible window
1006
+ let drawn = 0;
1007
+ const PAGE = 10; // visible rows at once
859
1008
 
860
1009
  const getItems = () => SLASH_COMMANDS.filter(c =>
861
1010
  !filter || c.cmd.slice(1).startsWith(filter)
862
1011
  );
863
1012
 
864
1013
  const paint = () => {
865
- // Erase previous draw: move up `drawn` lines, clear everything below
866
1014
  if (drawn > 0) process.stdout.write(`\x1b[${drawn}A\x1b[0J`);
867
1015
 
868
- const items = getItems();
869
- const show = items.slice(0, 14);
870
- if (idx >= items.length) idx = Math.max(0, items.length - 1);
1016
+ const items = getItems();
1017
+ if (items.length === 0) { idx = 0; scroll = 0; }
1018
+ else {
1019
+ idx = Math.max(0, Math.min(idx, items.length - 1));
1020
+ // Keep selected item inside the visible window
1021
+ if (idx < scroll) scroll = idx;
1022
+ if (idx >= scroll + PAGE) scroll = idx - PAGE + 1;
1023
+ }
871
1024
 
1025
+ const show = items.slice(scroll, scroll + PAGE);
872
1026
  const lines = [];
873
1027
 
874
- // Top border
875
1028
  lines.push(` \x1b[2m${'─'.repeat(58)}\x1b[0m`);
876
1029
 
877
- if (show.length === 0) {
1030
+ if (items.length === 0) {
878
1031
  lines.push(` \x1b[2m no commands match "/${filter}"\x1b[0m`);
879
1032
  } else {
880
1033
  for (let i = 0; i < show.length; i++) {
881
1034
  const m = show[i];
882
- const sel = i === idx;
1035
+ const sel = (scroll + i) === idx;
883
1036
  if (sel) {
884
- lines.push(
885
- ` \x1b[36;1m›\x1b[0m \x1b[36;1m${m.cmd.padEnd(22)}\x1b[0m \x1b[0m${m.desc}\x1b[0m`
886
- );
1037
+ lines.push(` \x1b[36;1m›\x1b[0m \x1b[36;1m${m.cmd.padEnd(22)}\x1b[0m ${m.desc}`);
887
1038
  } else {
888
- lines.push(
889
- ` \x1b[36m${m.cmd.padEnd(22)}\x1b[0m \x1b[2m${m.desc}\x1b[0m`
890
- );
1039
+ lines.push(` \x1b[36m${m.cmd.padEnd(22)}\x1b[0m \x1b[2m${m.desc}\x1b[0m`);
891
1040
  }
892
1041
  }
893
1042
  }
894
1043
 
895
- if (items.length > 14) lines.push(` \x1b[2m …${items.length - 14} more\x1b[0m`);
1044
+ // Scroll indicator
1045
+ if (items.length > PAGE) {
1046
+ const pct = Math.round(((scroll + PAGE / 2) / items.length) * 100);
1047
+ lines.push(` \x1b[2m ${idx + 1} / ${items.length} (${pct}%)\x1b[0m`);
1048
+ }
896
1049
 
897
- // Bottom: search input line
898
1050
  lines.push(` \x1b[2m${'─'.repeat(58)}\x1b[0m`);
899
- lines.push(` ${fmt(C.cyan, '/')}${filter}\x1b[K \x1b[2m↑↓ navigate · Enter select · Esc cancel\x1b[0m`);
1051
+ lines.push(` ${fmt(C.cyan, '/')}${filter}\x1b[K \x1b[2m↑↓ scroll · Enter select · Esc cancel\x1b[0m`);
900
1052
 
901
- const out = lines.join('\n') + '\n';
902
- process.stdout.write(out);
1053
+ process.stdout.write(lines.join('\n') + '\n');
903
1054
  drawn = lines.length;
904
1055
  };
905
1056
 
@@ -922,11 +1073,11 @@ async function openPalette(initialFilter = '') {
922
1073
  paint();
923
1074
  } else if (s === '\x7f' || s === '\x08') { // Backspace
924
1075
  filter = filter.slice(0, -1);
925
- idx = 0;
1076
+ idx = 0; scroll = 0;
926
1077
  paint();
927
1078
  } else if (/^[a-z0-9\-_]$/i.test(s)) { // Printable letter/digit/hyphen
928
1079
  filter += s.toLowerCase();
929
- idx = 0;
1080
+ idx = 0; scroll = 0;
930
1081
  paint();
931
1082
  }
932
1083
  };
@@ -1263,17 +1414,19 @@ function isNewerVersion(a, b) {
1263
1414
 
1264
1415
  // ─── Message queue + serial executor ─────────────────────────────────────────
1265
1416
 
1266
- const COMMAND_PREFIXES = ['/model','/key','/status','/skills','/graph','/clear','/help','/schedule','/update'];
1417
+ // Only these exact prefixes are built-in commands handled by handleCommand().
1418
+ // Everything else starting with '/' is a skill → routed to runTask().
1419
+ const COMMAND_PREFIXES = ['/model','/key','/status','/skills','/graph','/clear',
1420
+ '/help','/schedule','/update','/telegram'];
1267
1421
 
1268
1422
  async function executeInput(line) {
1269
- const isCmd = line.startsWith('/') || COMMAND_PREFIXES.some(c => line.startsWith(c));
1423
+ const isCmd = COMMAND_PREFIXES.some(c => line === c || line.startsWith(c + ' '));
1270
1424
  if (isCmd) {
1271
1425
  await handleCommand(line);
1272
1426
  } else {
1273
1427
  lastFailedTask = null;
1274
1428
  await runTask(line);
1275
1429
  }
1276
- // After this input completes, drain the queue
1277
1430
  await drainQueue();
1278
1431
  }
1279
1432
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0agent",
3
- "version": "1.0.46",
3
+ "version": "1.0.48",
4
4
  "description": "A persistent, learning AI agent that runs on your machine. An agent that learns.",
5
5
  "private": false,
6
6
  "license": "Apache-2.0",