0agent 1.0.21 → 1.0.23

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 +128 -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',
@@ -33,6 +57,55 @@ const C = {
33
57
  const fmt = (color, text) => `${color}${text}${C.reset}`;
34
58
  const clearLine = () => process.stdout.write('\r\x1b[2K');
35
59
 
60
+ // ─── LLM ping — direct 1-token call, bypasses daemon, instant ────────────────
61
+ async function pingLLM(provider) {
62
+ const key = provider.api_key ?? '';
63
+ const model = provider.model;
64
+ const sig = AbortSignal.timeout(8000);
65
+
66
+ try {
67
+ if (provider.provider === 'anthropic') {
68
+ const r = await fetch('https://api.anthropic.com/v1/messages', {
69
+ method: 'POST', signal: sig,
70
+ headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({ model, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
72
+ });
73
+ const d = await r.json();
74
+ if (!r.ok) return { ok: false, error: d.error?.message ?? `HTTP ${r.status}` };
75
+ return { ok: true, model: d.model };
76
+ }
77
+
78
+ if (['openai','xai','gemini'].includes(provider.provider)) {
79
+ const base = provider.provider === 'xai' ? 'https://api.x.ai/v1'
80
+ : provider.provider === 'gemini' ? 'https://generativelanguage.googleapis.com/v1beta/openai'
81
+ : 'https://api.openai.com/v1';
82
+ const r = await fetch(`${base}/chat/completions`, {
83
+ method: 'POST', signal: sig,
84
+ headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
85
+ body: JSON.stringify({ model, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
86
+ });
87
+ const d = await r.json();
88
+ if (!r.ok) return { ok: false, error: d.error?.message ?? `HTTP ${r.status}` };
89
+ return { ok: true, model: d.model };
90
+ }
91
+
92
+ if (provider.provider === 'ollama') {
93
+ const base = provider.base_url ?? 'http://localhost:11434';
94
+ const r = await fetch(`${base}/api/generate`, {
95
+ method: 'POST', signal: sig,
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({ model, prompt: 'hi', stream: false }),
98
+ });
99
+ if (!r.ok) return { ok: false, error: `Ollama HTTP ${r.status}` };
100
+ return { ok: true, model };
101
+ }
102
+
103
+ return { ok: true, model }; // unknown provider — skip check
104
+ } catch (e) {
105
+ return { ok: false, error: e.message };
106
+ }
107
+ }
108
+
36
109
  // ─── Config management ────────────────────────────────────────────────────────
37
110
  function loadConfig() {
38
111
  if (!existsSync(CONFIG_PATH)) return null;
@@ -50,12 +123,13 @@ function getCurrentProvider(cfg) {
50
123
 
51
124
  // ─── State ────────────────────────────────────────────────────────────────────
52
125
  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;
126
+ let sessionId = null;
127
+ let streaming = false;
128
+ let ws = null;
129
+ let wsReady = false;
57
130
  let pendingResolve = null;
58
- let lineBuffer = ''; // accumulated line for stream
131
+ let lineBuffer = '';
132
+ const spinner = new Spinner('Thinking');
59
133
  const history = []; // command history for arrow keys
60
134
 
61
135
  // ─── Header ──────────────────────────────────────────────────────────────────
@@ -102,17 +176,21 @@ function handleWsEvent(event) {
102
176
 
103
177
  switch (event.type) {
104
178
  case 'session.step': {
179
+ spinner.stop();
105
180
  if (streaming) { process.stdout.write('\n'); streaming = false; }
106
181
  console.log(` ${fmt(C.dim, '›')} ${event.step}`);
182
+ spinner.start(event.step.slice(0, 50)); // resume with current step label
107
183
  break;
108
184
  }
109
185
  case 'session.token': {
186
+ spinner.stop();
110
187
  if (!streaming) { process.stdout.write('\n '); streaming = true; }
111
188
  process.stdout.write(event.token);
112
189
  lineBuffer += event.token;
113
190
  break;
114
191
  }
115
192
  case 'session.completed': {
193
+ spinner.stop();
116
194
  if (streaming) { process.stdout.write('\n'); streaming = false; }
117
195
  const r = event.result ?? {};
118
196
  if (r.files_written?.length) console.log(`\n ${fmt(C.green, '✓')} ${r.files_written.join(', ')}`);
@@ -126,6 +204,7 @@ function handleWsEvent(event) {
126
204
  break;
127
205
  }
128
206
  case 'session.failed': {
207
+ spinner.stop();
129
208
  if (streaming) { process.stdout.write('\n'); streaming = false; }
130
209
  console.log(`\n ${fmt(C.red, '✗')} ${event.error}\n`);
131
210
  lineBuffer = '';
@@ -189,6 +268,7 @@ async function runTask(input) {
189
268
  });
190
269
  const s = await res.json();
191
270
  sessionId = s.session_id ?? s.id;
271
+ spinner.start('Thinking'); // show immediately after session created
192
272
  return new Promise(resolve => { pendingResolve = resolve; });
193
273
  } catch (e) {
194
274
  console.log(` ${fmt(C.red, '✗')} ${e.message}`);
@@ -377,12 +457,18 @@ printInsights();
377
457
  // Connect WebSocket for live events
378
458
  connectWS();
379
459
 
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
460
+ // ── Startup: check daemon + LLM connection ──────────────────────────────────
461
+ (async () => {
462
+ const startSpin = new Spinner('Starting daemon');
463
+
464
+ // Step 1: ensure daemon is running
465
+ let daemonOk = false;
466
+ try {
467
+ await fetch(`${BASE_URL}/api/health`, { signal: AbortSignal.timeout(1500) });
468
+ daemonOk = true;
469
+ } catch {
470
+ // Auto-start
471
+ startSpin.start();
386
472
  const { resolve: res, existsSync: ef } = await import('node:path').then(m => m);
387
473
  const pkgRoot = res(new URL(import.meta.url).pathname, '..', '..');
388
474
  const bundled = res(pkgRoot, 'dist', 'daemon.mjs');
@@ -392,14 +478,39 @@ fetch(`${BASE_URL}/api/health`, { signal: AbortSignal.timeout(1500) })
392
478
  child.unref();
393
479
  for (let i = 0; i < 20; i++) {
394
480
  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 {}
481
+ try { await fetch(`${BASE_URL}/api/health`, { signal: AbortSignal.timeout(500) }); daemonOk = true; break; } catch {}
397
482
  }
398
- } else {
399
- console.log(`\n ${fmt(C.dim, 'Run: 0agent start')}\n`);
400
483
  }
401
- rl.prompt();
402
- });
484
+ startSpin.stop();
485
+ if (daemonOk) process.stdout.write(` ${fmt(C.green, '✓')} Daemon ready\n`);
486
+ else { console.log(` ${fmt(C.red, '✗')} Daemon failed. Run: 0agent start`); rl.prompt(); return; }
487
+ }
488
+
489
+ // Step 2: lightweight direct API ping (1 token — fast, no daemon involved)
490
+ const provider = getCurrentProvider(cfg);
491
+ if (!provider?.api_key?.trim() && provider?.provider !== 'ollama') {
492
+ console.log(` ${fmt(C.yellow, '⚠')} No API key. Use: ${fmt(C.cyan, '/key ' + (provider?.provider ?? 'anthropic') + ' <key>')}\n`);
493
+ } else {
494
+ const llmSpin = new Spinner(`Checking ${provider.provider}/${provider.model}`);
495
+ llmSpin.start();
496
+ try {
497
+ const result = await pingLLM(provider);
498
+ llmSpin.stop();
499
+ if (result.ok) {
500
+ console.log(` ${fmt(C.green, '✓')} ${fmt(C.cyan, result.model ?? provider.model)} is ready\n`);
501
+ } else {
502
+ console.log(` ${fmt(C.red, '✗')} LLM error: ${result.error}`);
503
+ console.log(` ${fmt(C.dim, 'Fix with: /key ' + provider.provider + ' <new-key>')}\n`);
504
+ }
505
+ } catch (e) {
506
+ llmSpin.stop();
507
+ console.log(` ${fmt(C.yellow, '⚠')} Could not reach ${provider.provider}: ${e.message}\n`);
508
+ }
509
+ }
510
+
511
+ rl.prompt();
512
+ })();
513
+
403
514
 
404
515
  rl.on('line', async (input) => {
405
516
  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.23",
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",