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.
- package/bin/chat.js +196 -3
- package/dist/daemon.mjs +2410 -932
- 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
|
-
|
|
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 + ' '));
|