0agent 1.0.60 → 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 +175 -2
  2. package/dist/daemon.mjs +2395 -980
  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(', ')}`);
@@ -1108,6 +1113,43 @@ async function handleCommand(input) {
1108
1113
  break;
1109
1114
  }
1110
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
+
1111
1153
  // /graph
1112
1154
  case '/graph': {
1113
1155
  console.log(` ${fmt(C.cyan, 'Knowledge graph:')} ${fmt(C.dim, 'http://localhost:4200')}\n`);
@@ -1373,7 +1415,138 @@ async function _safeJsonFetch(url, opts) {
1373
1415
  catch { return { status: res.status, data: null, raw: text }; }
1374
1416
  }
1375
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
+
1376
1544
  (async () => {
1545
+ if (VOICE_MODE) {
1546
+ await runVoiceMode();
1547
+ return;
1548
+ }
1549
+
1377
1550
  const startSpin = new Spinner('Starting daemon');
1378
1551
 
1379
1552
  // Step 1: Check if daemon is running AND up-to-date (has /api/llm/ping)
@@ -1632,7 +1805,7 @@ function isNewerVersion(a, b) {
1632
1805
  // Only these exact prefixes are built-in commands handled by handleCommand().
1633
1806
  // Everything else starting with '/' is a skill → routed to runTask().
1634
1807
  const COMMAND_PREFIXES = ['/model','/key','/status','/skills','/graph','/clear',
1635
- '/help','/schedule','/update','/telegram'];
1808
+ '/help','/schedule','/update','/telegram','/voice','/surfaces'];
1636
1809
 
1637
1810
  async function executeInput(line) {
1638
1811
  const isCmd = COMMAND_PREFIXES.some(c => line === c || line.startsWith(c + ' '));