0agent 1.0.21 → 1.0.22

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 (3) hide show
  1. package/bin/0agent.js +60 -15
  2. package/bin/chat.js +100 -17
  3. package/package.json +1 -1
package/bin/0agent.js CHANGED
@@ -512,10 +512,12 @@ async function streamSession(sessionId) {
512
512
 
513
513
  return new Promise((resolve) => {
514
514
  const ws = new WS(`ws://localhost:4200/ws`);
515
- let streaming = false; // true when mid-token-stream
515
+ let streaming = false;
516
+ const spinner = new Spinner('Thinking');
516
517
 
517
518
  ws.on('open', () => {
518
519
  ws.send(JSON.stringify({ type: 'subscribe', topics: ['sessions'] }));
520
+ spinner.start(); // show immediately on connect
519
521
  });
520
522
 
521
523
  ws.on('message', async (data) => {
@@ -525,28 +527,31 @@ async function streamSession(sessionId) {
525
527
 
526
528
  switch (event.type) {
527
529
  case 'session.step':
528
- // Newline before step if we were mid-stream
530
+ spinner.stop();
529
531
  if (streaming) { process.stdout.write('\n'); streaming = false; }
530
532
  console.log(` \x1b[2m›\x1b[0m ${event.step}`);
533
+ spinner.start(event.step.slice(0, 50)); // show next spinner with current step
531
534
  break;
532
535
  case 'session.token':
533
- // Token-by-token streaming print without newline
536
+ spinner.stop(); // stop spinner before first token
534
537
  if (!streaming) { process.stdout.write('\n '); streaming = true; }
535
538
  process.stdout.write(event.token);
536
539
  break;
537
540
  case 'session.completed': {
541
+ spinner.stop();
538
542
  if (streaming) { process.stdout.write('\n'); streaming = false; }
539
543
  const r = event.result ?? {};
540
544
  if (r.files_written?.length) console.log(`\n \x1b[32m✓\x1b[0m Files: ${r.files_written.join(', ')}`);
541
545
  if (r.commands_run?.length) console.log(` \x1b[32m✓\x1b[0m Commands run: ${r.commands_run.length}`);
542
546
  if (r.tokens_used) console.log(` \x1b[2m${r.tokens_used} tokens · ${r.model}\x1b[0m`);
543
547
  console.log('\n \x1b[32m✓ Done\x1b[0m\n');
544
- await showResultPreview(r); // confirm server/file actually exists
548
+ await showResultPreview(r);
545
549
  ws.close();
546
550
  resolve();
547
551
  break;
548
552
  }
549
553
  case 'session.failed':
554
+ spinner.stop();
550
555
  if (streaming) { process.stdout.write('\n'); streaming = false; }
551
556
  console.log(`\n \x1b[31m✗ Failed:\x1b[0m ${event.error}\n`);
552
557
  ws.close();
@@ -557,7 +562,7 @@ async function streamSession(sessionId) {
557
562
  });
558
563
 
559
564
  ws.on('error', () => {
560
- // WS not available — fall back to polling
565
+ spinner.stop();
561
566
  ws.close();
562
567
  pollSession(sessionId).then(resolve);
563
568
  });
@@ -569,31 +574,39 @@ async function streamSession(sessionId) {
569
574
 
570
575
  async function pollSession(sessionId) {
571
576
  let lastStepCount = 0;
577
+ const spinner = new Spinner('Thinking');
578
+ spinner.start();
579
+
572
580
  for (let i = 0; i < 300; i++) {
573
581
  await sleep(600);
574
582
  const res = await fetch(`${BASE_URL}/api/sessions/${sessionId}`);
575
583
  const s = await res.json();
576
584
 
577
- // Print any new steps since last poll
585
+ // Print new steps
578
586
  const steps = s.steps ?? [];
579
587
  for (let j = lastStepCount; j < steps.length; j++) {
580
- console.log(` › ${steps[j].description}`);
588
+ spinner.stop();
589
+ console.log(` \x1b[2m›\x1b[0m ${steps[j].description}`);
590
+ spinner.start(steps[j].description.slice(0, 50));
581
591
  }
582
592
  lastStepCount = steps.length;
583
593
 
584
594
  if (s.status === 'completed') {
585
- console.log('\n ✓ Done\n');
595
+ spinner.stop();
596
+ console.log('\n \x1b[32m✓ Done\x1b[0m\n');
586
597
  const out = s.result?.output ?? s.result;
587
598
  if (out && typeof out === 'string') console.log(` ${out}\n`);
588
599
  await showResultPreview(s.result ?? {});
589
600
  return;
590
601
  }
591
602
  if (s.status === 'failed') {
592
- console.log(`\n ✗ Failed: ${s.error}\n`);
603
+ spinner.stop();
604
+ console.log(`\n \x1b[31m✗ Failed:\x1b[0m ${s.error}\n`);
593
605
  return;
594
606
  }
595
607
  }
596
- console.log('\n Timed out waiting for session.\n');
608
+ spinner.stop();
609
+ console.log('\n Timed out.\n');
597
610
  }
598
611
 
599
612
  // ─── Chat REPL ───────────────────────────────────────────────────────────
@@ -1311,19 +1324,20 @@ async function requireDaemon() {
1311
1324
  process.exit(1);
1312
1325
  }
1313
1326
 
1314
- process.stdout.write(' Starting daemon');
1327
+ const startSpinner = new Spinner('Starting daemon');
1328
+ startSpinner.start();
1315
1329
  await _startDaemonBackground();
1316
1330
 
1317
1331
  for (let i = 0; i < 24; i++) {
1318
1332
  await sleep(500);
1319
- process.stdout.write('.');
1320
1333
  if (await isDaemonRunning()) {
1321
- process.stdout.write(' ✓\n\n');
1334
+ startSpinner.stop();
1335
+ process.stdout.write(' \x1b[32m✓\x1b[0m Daemon ready\n\n');
1322
1336
  return;
1323
1337
  }
1324
1338
  }
1325
- process.stdout.write(' ✗\n');
1326
- console.log(' Daemon failed to start. Check: 0agent logs\n');
1339
+ startSpinner.stop();
1340
+ console.log(' \x1b[31m✗\x1b[0m Daemon failed to start. Check: 0agent logs\n');
1327
1341
  process.exit(1);
1328
1342
  }
1329
1343
 
@@ -1359,6 +1373,37 @@ function sleep(ms) {
1359
1373
  return new Promise(r => setTimeout(r, ms));
1360
1374
  }
1361
1375
 
1376
+ // ─── Spinner — shows when agent is thinking/loading ───────────────────────────
1377
+ class Spinner {
1378
+ constructor(msg = 'Thinking') {
1379
+ this._msg = msg;
1380
+ this._frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
1381
+ this._i = 0;
1382
+ this._timer = null;
1383
+ this._active = false;
1384
+ }
1385
+ start(msg) {
1386
+ if (this._active) return;
1387
+ if (msg) this._msg = msg;
1388
+ this._active = true;
1389
+ process.stdout.write('\n');
1390
+ this._timer = setInterval(() => {
1391
+ process.stdout.write(`\r \x1b[36m${this._frames[this._i++ % this._frames.length]}\x1b[0m \x1b[2m${this._msg}\x1b[0m `);
1392
+ }, 80);
1393
+ }
1394
+ update(msg) {
1395
+ this._msg = msg;
1396
+ }
1397
+ stop(clearIt = true) {
1398
+ if (!this._active) return;
1399
+ clearInterval(this._timer);
1400
+ this._timer = null;
1401
+ this._active = false;
1402
+ if (clearIt) process.stdout.write('\r\x1b[2K');
1403
+ }
1404
+ get active() { return this._active; }
1405
+ }
1406
+
1362
1407
  function ask(question) {
1363
1408
  return new Promise(resolve => {
1364
1409
  const rl = createInterface({ input: process.stdin, output: process.stdout });
package/bin/chat.js CHANGED
@@ -17,6 +17,30 @@ const AGENT_DIR = resolve(homedir(), '.0agent');
17
17
  const CONFIG_PATH = resolve(AGENT_DIR, 'config.yaml');
18
18
  const BASE_URL = process.env['ZEROAGENT_URL'] ?? 'http://localhost:4200';
19
19
 
20
+ // ─── Spinner ──────────────────────────────────────────────────────────────────
21
+ class Spinner {
22
+ constructor(msg = 'Thinking') {
23
+ this._msg = msg;
24
+ this._frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
25
+ this._i = 0; this._timer = null; this._active = false;
26
+ }
27
+ start(msg) {
28
+ if (this._active) return;
29
+ if (msg) this._msg = msg;
30
+ this._active = true;
31
+ this._timer = setInterval(() => {
32
+ process.stdout.write(`\r \x1b[36m${this._frames[this._i++ % this._frames.length]}\x1b[0m \x1b[2m${this._msg}\x1b[0m `);
33
+ }, 80);
34
+ }
35
+ update(msg) { this._msg = msg; }
36
+ stop(clearIt = true) {
37
+ if (!this._active) return;
38
+ clearInterval(this._timer); this._timer = null; this._active = false;
39
+ if (clearIt) process.stdout.write('\r\x1b[2K');
40
+ }
41
+ get active() { return this._active; }
42
+ }
43
+
20
44
  // ─── ANSI helpers ─────────────────────────────────────────────────────────────
21
45
  const C = {
22
46
  reset: '\x1b[0m',
@@ -50,12 +74,13 @@ function getCurrentProvider(cfg) {
50
74
 
51
75
  // ─── State ────────────────────────────────────────────────────────────────────
52
76
  let cfg = loadConfig();
53
- let sessionId = null; // current chat session
54
- let streaming = false; // mid-token-stream
55
- let ws = null; // WebSocket
56
- let wsReady = false;
77
+ let sessionId = null;
78
+ let streaming = false;
79
+ let ws = null;
80
+ let wsReady = false;
57
81
  let pendingResolve = null;
58
- let lineBuffer = ''; // accumulated line for stream
82
+ let lineBuffer = '';
83
+ const spinner = new Spinner('Thinking');
59
84
  const history = []; // command history for arrow keys
60
85
 
61
86
  // ─── Header ──────────────────────────────────────────────────────────────────
@@ -102,17 +127,21 @@ function handleWsEvent(event) {
102
127
 
103
128
  switch (event.type) {
104
129
  case 'session.step': {
130
+ spinner.stop();
105
131
  if (streaming) { process.stdout.write('\n'); streaming = false; }
106
132
  console.log(` ${fmt(C.dim, '›')} ${event.step}`);
133
+ spinner.start(event.step.slice(0, 50)); // resume with current step label
107
134
  break;
108
135
  }
109
136
  case 'session.token': {
137
+ spinner.stop();
110
138
  if (!streaming) { process.stdout.write('\n '); streaming = true; }
111
139
  process.stdout.write(event.token);
112
140
  lineBuffer += event.token;
113
141
  break;
114
142
  }
115
143
  case 'session.completed': {
144
+ spinner.stop();
116
145
  if (streaming) { process.stdout.write('\n'); streaming = false; }
117
146
  const r = event.result ?? {};
118
147
  if (r.files_written?.length) console.log(`\n ${fmt(C.green, '✓')} ${r.files_written.join(', ')}`);
@@ -126,6 +155,7 @@ function handleWsEvent(event) {
126
155
  break;
127
156
  }
128
157
  case 'session.failed': {
158
+ spinner.stop();
129
159
  if (streaming) { process.stdout.write('\n'); streaming = false; }
130
160
  console.log(`\n ${fmt(C.red, '✗')} ${event.error}\n`);
131
161
  lineBuffer = '';
@@ -189,6 +219,7 @@ async function runTask(input) {
189
219
  });
190
220
  const s = await res.json();
191
221
  sessionId = s.session_id ?? s.id;
222
+ spinner.start('Thinking'); // show immediately after session created
192
223
  return new Promise(resolve => { pendingResolve = resolve; });
193
224
  } catch (e) {
194
225
  console.log(` ${fmt(C.red, '✗')} ${e.message}`);
@@ -377,12 +408,18 @@ printInsights();
377
408
  // Connect WebSocket for live events
378
409
  connectWS();
379
410
 
380
- // Check if daemon is running
381
- fetch(`${BASE_URL}/api/health`, { signal: AbortSignal.timeout(1500) })
382
- .then(() => { rl.prompt(); })
383
- .catch(async () => {
384
- process.stdout.write(fmt(C.dim, ' Starting daemon'));
385
- // Trigger auto-start via requireDaemon-equivalent
411
+ // ── Startup: check daemon + LLM connection ──────────────────────────────────
412
+ (async () => {
413
+ const startSpin = new Spinner('Starting daemon');
414
+
415
+ // Step 1: ensure daemon is running
416
+ let daemonOk = false;
417
+ try {
418
+ await fetch(`${BASE_URL}/api/health`, { signal: AbortSignal.timeout(1500) });
419
+ daemonOk = true;
420
+ } catch {
421
+ // Auto-start
422
+ startSpin.start();
386
423
  const { resolve: res, existsSync: ef } = await import('node:path').then(m => m);
387
424
  const pkgRoot = res(new URL(import.meta.url).pathname, '..', '..');
388
425
  const bundled = res(pkgRoot, 'dist', 'daemon.mjs');
@@ -392,14 +429,60 @@ fetch(`${BASE_URL}/api/health`, { signal: AbortSignal.timeout(1500) })
392
429
  child.unref();
393
430
  for (let i = 0; i < 20; i++) {
394
431
  await new Promise(r => setTimeout(r, 500));
395
- process.stdout.write(fmt(C.dim, '.'));
396
- try { await fetch(`${BASE_URL}/api/health`, { signal: AbortSignal.timeout(500) }); process.stdout.write(fmt(C.green, ' ✓\n\n')); break; } catch {}
432
+ try { await fetch(`${BASE_URL}/api/health`, { signal: AbortSignal.timeout(500) }); daemonOk = true; break; } catch {}
397
433
  }
398
- } else {
399
- console.log(`\n ${fmt(C.dim, 'Run: 0agent start')}\n`);
400
434
  }
401
- rl.prompt();
402
- });
435
+ startSpin.stop();
436
+ if (daemonOk) process.stdout.write(` ${fmt(C.green, '✓')} Daemon ready\n`);
437
+ else { console.log(` ${fmt(C.red, '✗')} Daemon failed. Run: 0agent start`); rl.prompt(); return; }
438
+ }
439
+
440
+ // Step 2: verify LLM is reachable with a quick "hello" check
441
+ const provider = getCurrentProvider(cfg);
442
+ if (provider?.api_key?.trim() || provider?.provider === 'ollama') {
443
+ const llmSpin = new Spinner(`Connecting to ${provider.provider}/${provider.model}`);
444
+ llmSpin.start();
445
+ try {
446
+ // Ask the daemon to run a minimal LLM ping via the session API
447
+ const res = await fetch(`${BASE_URL}/api/sessions`, {
448
+ method: 'POST',
449
+ headers: { 'Content-Type': 'application/json' },
450
+ body: JSON.stringify({ task: 'Reply with exactly: ready', options: { max_steps: 1 } }),
451
+ });
452
+ const s = await res.json();
453
+ const sid = s.session_id ?? s.id;
454
+ // Poll briefly for completion
455
+ for (let i = 0; i < 20; i++) {
456
+ await new Promise(r => setTimeout(r, 500));
457
+ const check = await fetch(`${BASE_URL}/api/sessions/${sid}`).then(r => r.json()).catch(() => null);
458
+ if (check?.status === 'completed') {
459
+ llmSpin.stop();
460
+ const model = check.result?.model ?? provider.model;
461
+ console.log(` ${fmt(C.green, '✓')} LLM connected — ${fmt(C.cyan, model)}\n`);
462
+ break;
463
+ }
464
+ if (check?.status === 'failed') {
465
+ llmSpin.stop();
466
+ console.log(` ${fmt(C.red, '✗')} LLM error: ${check.error}`);
467
+ console.log(` ${fmt(C.dim, 'Check your API key with: /key ' + provider.provider)}\n`);
468
+ break;
469
+ }
470
+ }
471
+ if (llmSpin.active) {
472
+ llmSpin.stop();
473
+ console.log(` ${fmt(C.yellow, '⚠')} LLM check timed out — it may still work\n`);
474
+ }
475
+ } catch {
476
+ llmSpin.stop();
477
+ console.log(` ${fmt(C.yellow, '⚠')} Could not verify LLM connection\n`);
478
+ }
479
+ } else {
480
+ console.log(` ${fmt(C.yellow, '⚠')} No API key set. Use: ${fmt(C.cyan, '/key ' + (provider?.provider ?? 'anthropic') + ' <your-key>')}\n`);
481
+ }
482
+
483
+ rl.prompt();
484
+ })();
485
+
403
486
 
404
487
  rl.on('line', async (input) => {
405
488
  const line = input.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0agent",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
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",