0agent 1.0.59 → 1.0.62

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/chat.js +196 -3
  2. package/dist/daemon.mjs +2410 -932
  3. package/package.json +1 -1
package/bin/chat.js CHANGED
@@ -40,6 +40,8 @@ const SLASH_COMMANDS = [
40
40
  { cmd: '/schedule', desc: 'Manage scheduled / recurring tasks' },
41
41
  { cmd: '/update', desc: 'Update 0agent to the latest version' },
42
42
  { cmd: '/graph', desc: 'Open 3D knowledge graph in browser' },
43
+ { cmd: '/voice', desc: 'Start voice mode (Whisper STT + TTS)' },
44
+ { cmd: '/surfaces', desc: 'Show connected surfaces (Telegram, Slack, etc.)' },
43
45
  { cmd: '/clear', desc: 'Clear the screen' },
44
46
  { cmd: '/help', desc: 'Show this help' },
45
47
  ];
@@ -586,7 +588,10 @@ function handleWsEvent(event) {
586
588
 
587
589
  const r = event.result ?? {};
588
590
  const elapsed = sessionStartMs ? `${((Date.now() - sessionStartMs) / 1000).toFixed(1)}s` : '';
589
- const cost = estimateCost(r.model, r.tokens_used);
591
+ // Use precise cost from agent when available, fallback to blended estimate
592
+ const cost = r.cost_usd > 0
593
+ ? (r.cost_usd < 0.01 ? ' · <$0.01' : ` · $${r.cost_usd.toFixed(3)}`)
594
+ : estimateCost(r.model, r.tokens_used);
590
595
 
591
596
  if (r.files_written?.length)
592
597
  console.log(`\n ${fmt(C.green, '✓')} ${r.files_written.join(', ')}`);
@@ -753,6 +758,7 @@ async function runTask(input) {
753
758
  try {
754
759
  const r = await fetch(`${BASE_URL}/api/sessions/${sid}`, { signal: AbortSignal.timeout(2000) });
755
760
  const session = await r.json();
761
+ globalThis._daemonMisses = 0; // daemon responded — reset miss counter
756
762
 
757
763
  // Show any new steps not yet shown via WS
758
764
  const steps = session.steps ?? [];
@@ -789,7 +795,26 @@ async function runTask(input) {
789
795
  resolve_?.();
790
796
  drainQueue(); // auto-process queued messages
791
797
  }
792
- } catch {}
798
+ } catch {
799
+ // Daemon not responding — track misses
800
+ if (typeof _daemonMisses === 'undefined') globalThis._daemonMisses = 0;
801
+ globalThis._daemonMisses = (globalThis._daemonMisses ?? 0) + 1;
802
+ if (globalThis._daemonMisses >= 4) {
803
+ globalThis._daemonMisses = 0;
804
+ clearInterval(pollTimer);
805
+ spinner.stop();
806
+ process.stdout.write(`\r\x1b[2K\n ${fmt(C.yellow, '⚠')} Daemon stopped — restarting…\n\n`);
807
+ const r = await _spawnDaemon();
808
+ if (r === 'ok') {
809
+ process.stdout.write(` ${fmt(C.green, '✓')} Daemon restarted. Re-send your message.\n\n`);
810
+ } else {
811
+ process.stdout.write(` ${fmt(C.red, '✗')} Could not restart daemon. Run: ${fmt(C.dim, '0agent start')}\n\n`);
812
+ }
813
+ if (pendingResolve) { pendingResolve(); pendingResolve = null; }
814
+ sessionId = null; streaming = false; streamLineCount = 0;
815
+ rl.prompt();
816
+ }
817
+ }
793
818
  }, 800);
794
819
 
795
820
  return new Promise(resolve => { pendingResolve = resolve; });
@@ -1088,6 +1113,43 @@ async function handleCommand(input) {
1088
1113
  break;
1089
1114
  }
1090
1115
 
1116
+ // /voice — launch voice mode
1117
+ case '/voice': {
1118
+ console.log(`\n ${fmt(C.green, '🎙️')} Launching voice mode…`);
1119
+ console.log(` ${fmt(C.dim, 'Requires: pip install openai-whisper + brew install sox\n')}`);
1120
+ try {
1121
+ const { spawn: spawnVoice } = await import('node:child_process');
1122
+ const voiceModel = parts[1] ?? 'base';
1123
+ const proc = spawnVoice(process.execPath, [process.argv[1], '--voice', `--voice-model=${voiceModel}`], {
1124
+ stdio: 'inherit',
1125
+ });
1126
+ await new Promise(r => proc.on('close', r));
1127
+ } catch (e) {
1128
+ console.log(` ${fmt(C.red, '✗')} ${e.message}\n`);
1129
+ }
1130
+ break;
1131
+ }
1132
+
1133
+ // /surfaces — show configured surfaces
1134
+ case '/surfaces': {
1135
+ console.log('\n Connected surfaces:\n');
1136
+ const surCfg = cfg?.surfaces ?? {};
1137
+ const checks = [
1138
+ { name: 'Telegram', key: 'telegram', check: c => c?.token?.length > 10 },
1139
+ { name: 'Slack', key: 'slack', check: c => c?.bot_token?.length > 5 },
1140
+ { name: 'WhatsApp', key: 'whatsapp', check: c => c?.provider },
1141
+ { name: 'Voice', key: 'voice', check: c => c?.enabled },
1142
+ { name: 'Meeting', key: 'meeting', check: c => c?.enabled },
1143
+ ];
1144
+ for (const { name, key, check } of checks) {
1145
+ const configured = check(surCfg[key]);
1146
+ const icon = configured ? fmt(C.green, '✓') : fmt(C.dim, '○');
1147
+ console.log(` ${icon} ${name.padEnd(12)} ${configured ? fmt(C.green, 'configured') : fmt(C.dim, 'not configured')}`);
1148
+ }
1149
+ console.log(`\n ${fmt(C.dim, 'Edit ~/.0agent/config.yaml → surfaces: { telegram, slack, whatsapp, voice, meeting }')}\n`);
1150
+ break;
1151
+ }
1152
+
1091
1153
  // /graph
1092
1154
  case '/graph': {
1093
1155
  console.log(` ${fmt(C.cyan, 'Knowledge graph:')} ${fmt(C.dim, 'http://localhost:4200')}\n`);
@@ -1353,7 +1415,138 @@ async function _safeJsonFetch(url, opts) {
1353
1415
  catch { return { status: res.status, data: null, raw: text }; }
1354
1416
  }
1355
1417
 
1418
+ // ─── Voice mode (--voice flag) ───────────────────────────────────────────────
1419
+
1420
+ const VOICE_MODE = process.argv.includes('--voice');
1421
+
1422
+ async function runVoiceMode() {
1423
+ const { spawnSync, execSync, spawn } = await import('node:child_process');
1424
+ const { existsSync: fsExists, mkdirSync: fsMk, writeFileSync: fsWrite, readFileSync: fsRead } = await import('node:fs');
1425
+ const { tmpdir } = await import('node:os');
1426
+ const { join: pathJoin } = await import('node:path');
1427
+
1428
+ // Check whisper
1429
+ const whisperBin = ['whisper', 'faster-whisper'].find(b => {
1430
+ try { return spawnSync(b, ['--help'], { timeout: 2000, stdio: 'pipe' }).status !== null; } catch { return false; }
1431
+ });
1432
+ if (!whisperBin) {
1433
+ console.log(` ${fmt(C.red, '✗')} Whisper not found. Install: ${fmt(C.bold, 'pip install openai-whisper')}`);
1434
+ process.exit(1);
1435
+ }
1436
+
1437
+ // Check sox or ffmpeg for recording
1438
+ const recorderBin = ['sox', 'ffmpeg'].find(b => {
1439
+ try { return spawnSync(b, ['--version'], { timeout: 2000, stdio: 'pipe' }).status !== null; } catch { return false; }
1440
+ });
1441
+ if (!recorderBin) {
1442
+ console.log(` ${fmt(C.red, '✗')} sox or ffmpeg not found. Install: ${fmt(C.bold, 'brew install sox')}`);
1443
+ process.exit(1);
1444
+ }
1445
+
1446
+ const voiceModel = process.argv.find(a => a.startsWith('--voice-model='))?.split('=')[1] ?? 'base';
1447
+ const tmpDir = pathJoin(tmpdir(), '0agent-voice');
1448
+ fsMk(tmpDir, { recursive: true });
1449
+
1450
+ console.log(`\n ${fmt(C.green, '🎙️')} ${fmt(C.bold, 'Voice mode')} (Whisper ${voiceModel})`);
1451
+ console.log(` ${fmt(C.dim, 'Press Enter to start/stop recording. Ctrl+C to quit.\n')}`);
1452
+
1453
+ function recordAudio(outPath, seconds) {
1454
+ if (recorderBin === 'sox') {
1455
+ return spawnSync('sox', ['-d', '-r', '16000', '-c', '1', '-b', '16', outPath, 'trim', '0', String(seconds)],
1456
+ { timeout: (seconds + 5) * 1000, stdio: 'pipe' });
1457
+ }
1458
+ const platform = process.platform;
1459
+ const deviceArgs = platform === 'darwin'
1460
+ ? ['-f', 'avfoundation', '-i', ':0']
1461
+ : ['-f', 'alsa', '-i', 'default'];
1462
+ return spawnSync('ffmpeg', ['-y', ...deviceArgs, '-ar', '16000', '-ac', '1', '-t', String(seconds), outPath],
1463
+ { timeout: (seconds + 5) * 1000, stdio: 'pipe' });
1464
+ }
1465
+
1466
+ function transcribe(audioPath) {
1467
+ try {
1468
+ const cmd = whisperBin === 'faster-whisper'
1469
+ ? `faster-whisper "${audioPath}" --model ${voiceModel} --output_format txt --output_dir "${tmpDir}"`
1470
+ : `whisper "${audioPath}" --model ${voiceModel} --output_format txt --output_dir "${tmpDir}" --fp16 False`;
1471
+ execSync(cmd, { timeout: 120_000, stdio: 'pipe' });
1472
+ const txtPath = audioPath.replace(/\.[^.]+$/, '.txt');
1473
+ if (fsExists(txtPath)) return fsRead(txtPath, 'utf8').trim();
1474
+ return null;
1475
+ } catch { return null; }
1476
+ }
1477
+
1478
+ function speak(text) {
1479
+ const cleaned = text
1480
+ .replace(/```[\s\S]*?```/g, 'code block')
1481
+ .replace(/`[^`]+`/g, '').replace(/\*\*([^*]+)\*\*/g, '$1')
1482
+ .replace(/#+\s*/g, '').replace(/\n/g, ' ').trim();
1483
+ if (!cleaned) return;
1484
+ if (process.platform === 'darwin') {
1485
+ spawn('say', ['-r', '175', cleaned], { stdio: 'ignore', detached: true }).unref();
1486
+ } else {
1487
+ spawn('espeak', [cleaned], { stdio: 'ignore', detached: true }).unref();
1488
+ }
1489
+ }
1490
+
1491
+ const rl2 = createInterface({ input: process.stdin, output: process.stdout });
1492
+ console.log(` ${fmt(C.dim, '↩ press Enter to record (5 seconds)')}`);
1493
+ rl2.prompt();
1494
+
1495
+ rl2.on('line', async () => {
1496
+ const outPath = pathJoin(tmpDir, `voice-${Date.now()}.wav`);
1497
+ process.stdout.write(` ${fmt(C.red, '●')} Recording (5s)…\n`);
1498
+ recordAudio(outPath, 5);
1499
+ if (!fsExists(outPath)) {
1500
+ process.stdout.write(` ${fmt(C.yellow, '!')} Could not record. Check microphone.\n`);
1501
+ rl2.prompt(); return;
1502
+ }
1503
+ process.stdout.write(` ${fmt(C.dim, '⏳ Transcribing…')}\n`);
1504
+ const transcript = transcribe(outPath);
1505
+ if (!transcript) {
1506
+ process.stdout.write(` ${fmt(C.yellow, '!')} Could not transcribe.\n`);
1507
+ rl2.prompt(); return;
1508
+ }
1509
+ process.stdout.write(` ${fmt(C.cyan, '🎤')} "${fmt(C.bold, transcript)}"\n`);
1510
+
1511
+ // Run as agent task
1512
+ try {
1513
+ const res = await fetch(`${BASE_URL}/api/sessions`, {
1514
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1515
+ body: JSON.stringify({ task: transcript, context: { surface: 'voice', is_chat: true } }),
1516
+ signal: AbortSignal.timeout(5000),
1517
+ });
1518
+ const session = await res.json();
1519
+ const sid = session.session_id ?? session.id;
1520
+ if (!sid) { process.stdout.write(` ${fmt(C.red, '✗')} Session error\n`); rl2.prompt(); return; }
1521
+
1522
+ // Poll for result
1523
+ let output = '';
1524
+ for (let i = 0; i < 120; i++) {
1525
+ await new Promise(r => setTimeout(r, 500));
1526
+ const r = await fetch(`${BASE_URL}/api/sessions/${sid}`, { signal: AbortSignal.timeout(3000) });
1527
+ const s = await r.json();
1528
+ if (s.status === 'completed') { output = s.result?.output ?? ''; break; }
1529
+ if (s.status === 'failed' || s.status === 'cancelled') break;
1530
+ }
1531
+ if (output) {
1532
+ process.stdout.write(`\n ${fmt(C.green, '🤖')} ${output}\n\n`);
1533
+ speak(output);
1534
+ }
1535
+ } catch (e) {
1536
+ process.stdout.write(` ${fmt(C.red, '✗')} ${e.message}\n`);
1537
+ }
1538
+ rl2.prompt();
1539
+ });
1540
+
1541
+ rl2.on('close', () => process.exit(0));
1542
+ }
1543
+
1356
1544
  (async () => {
1545
+ if (VOICE_MODE) {
1546
+ await runVoiceMode();
1547
+ return;
1548
+ }
1549
+
1357
1550
  const startSpin = new Spinner('Starting daemon');
1358
1551
 
1359
1552
  // Step 1: Check if daemon is running AND up-to-date (has /api/llm/ping)
@@ -1612,7 +1805,7 @@ function isNewerVersion(a, b) {
1612
1805
  // Only these exact prefixes are built-in commands handled by handleCommand().
1613
1806
  // Everything else starting with '/' is a skill → routed to runTask().
1614
1807
  const COMMAND_PREFIXES = ['/model','/key','/status','/skills','/graph','/clear',
1615
- '/help','/schedule','/update','/telegram'];
1808
+ '/help','/schedule','/update','/telegram','/voice','/surfaces'];
1616
1809
 
1617
1810
  async function executeInput(line) {
1618
1811
  const isCmd = COMMAND_PREFIXES.some(c => line === c || line.startsWith(c + ' '));