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.
- package/bin/chat.js +175 -2
- package/dist/daemon.mjs +2395 -980
- 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(', ')}`);
|
|
@@ -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 + ' '));
|