uwonbot 1.1.2 → 1.1.4
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/uwonbot.js +1 -1
- package/package.json +2 -2
- package/src/agent.js +204 -12
- package/src/autostart.js +38 -6
- package/src/clapListener.js +7 -3
package/bin/uwonbot.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uwonbot",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Uwonbot AI Assistant CLI — Your AI controls your computer",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"commander": "^12.1.0",
|
|
28
28
|
"conf": "^13.0.1",
|
|
29
29
|
"inquirer": "^12.0.0",
|
|
30
|
+
"mic": "^2.5.1",
|
|
30
31
|
"node-fetch": "^3.3.2",
|
|
31
32
|
"open": "^10.1.0",
|
|
32
33
|
"ora": "^8.1.0",
|
|
@@ -34,7 +35,6 @@
|
|
|
34
35
|
},
|
|
35
36
|
"optionalDependencies": {
|
|
36
37
|
"@nut-tree-fork/nut-js": "^4.2.0",
|
|
37
|
-
"mic": "^2.5.1",
|
|
38
38
|
"screenshot-desktop": "^1.15.0"
|
|
39
39
|
}
|
|
40
40
|
}
|
package/src/agent.js
CHANGED
|
@@ -1,11 +1,61 @@
|
|
|
1
1
|
import { WebSocketServer } from 'ws';
|
|
2
|
-
import
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
import { exec, execSync } from 'child_process';
|
|
3
4
|
import { promisify } from 'util';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import { getConfig } from './config.js';
|
|
6
7
|
import ClapListener from './clapListener.js';
|
|
8
|
+
import { fetchAssistants, setIdToken } from './firebase-client.js';
|
|
7
9
|
|
|
8
10
|
const execAsync = promisify(exec);
|
|
11
|
+
const WEB_APP_URL = 'https://chartapp-653e1.web.app';
|
|
12
|
+
|
|
13
|
+
function isPortInUse(port) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
16
|
+
const timer = setTimeout(() => { ws.terminate(); resolve(false); }, 1000);
|
|
17
|
+
ws.on('open', () => { clearTimeout(timer); ws.close(); resolve(true); });
|
|
18
|
+
ws.on('error', () => { clearTimeout(timer); resolve(false); });
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function killProcessOnPort(port) {
|
|
23
|
+
try {
|
|
24
|
+
if (process.platform === 'win32') {
|
|
25
|
+
const out = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8' });
|
|
26
|
+
const pid = out.trim().split(/\s+/).pop();
|
|
27
|
+
if (pid && pid !== '0') execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
|
|
28
|
+
} else {
|
|
29
|
+
execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, { stdio: 'ignore' });
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
} catch { return false; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function checkSoxInstalled() {
|
|
36
|
+
try { execSync('which sox', { stdio: 'ignore' }); return true; } catch {
|
|
37
|
+
try { execSync('which rec', { stdio: 'ignore' }); return true; } catch { return false; }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function ensureSox() {
|
|
42
|
+
if (checkSoxInstalled()) return true;
|
|
43
|
+
console.log(chalk.yellow(' ⚠ SoX가 설치되어 있지 않습니다 (박수 감지에 필요)'));
|
|
44
|
+
if (process.platform === 'darwin') {
|
|
45
|
+
console.log(chalk.cyan(' → Homebrew로 자동 설치 시도 중...'));
|
|
46
|
+
try {
|
|
47
|
+
execSync('brew install sox', { stdio: 'inherit' });
|
|
48
|
+
console.log(chalk.green(' ✓ SoX 설치 완료'));
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
console.log(chalk.yellow(' ⚠ SoX 자동 설치 실패. 수동 설치: brew install sox'));
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
console.log(chalk.gray(' macOS: brew install sox'));
|
|
56
|
+
console.log(chalk.gray(' Linux: sudo apt install sox'));
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
9
59
|
const platform = process.platform;
|
|
10
60
|
|
|
11
61
|
let robot = null;
|
|
@@ -272,32 +322,160 @@ async function handleCommand(msg) {
|
|
|
272
322
|
}
|
|
273
323
|
}
|
|
274
324
|
|
|
325
|
+
async function openWebAssistant(assistantId) {
|
|
326
|
+
const url = `${WEB_APP_URL}/assistant/live?id=${assistantId}`;
|
|
327
|
+
try {
|
|
328
|
+
if (platform === 'darwin') {
|
|
329
|
+
await execAsync(`open -na "Google Chrome" --args --new-window "${url}" 2>/dev/null || open -na "Safari" --args "${url}" 2>/dev/null || open "${url}"`);
|
|
330
|
+
} else if (platform === 'win32') {
|
|
331
|
+
await execAsync(`start chrome --new-window "${url}" 2>nul || start "" "${url}"`);
|
|
332
|
+
} else {
|
|
333
|
+
await execAsync(`google-chrome --new-window "${url}" 2>/dev/null || xdg-open "${url}"`);
|
|
334
|
+
}
|
|
335
|
+
return true;
|
|
336
|
+
} catch {
|
|
337
|
+
try { await execAsync(`open "${url}"`); return true; } catch { return false; }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function activateAllAssistants(assistants) {
|
|
342
|
+
const opened = [];
|
|
343
|
+
for (const a of assistants) {
|
|
344
|
+
const mode = a.activationMode || 'web';
|
|
345
|
+
if (mode === 'terminal') {
|
|
346
|
+
await openTerminalWithChat(a.name);
|
|
347
|
+
opened.push({ name: a.name, mode: 'terminal' });
|
|
348
|
+
} else if (mode === 'web') {
|
|
349
|
+
await openWebAssistant(a.id);
|
|
350
|
+
opened.push({ name: a.name, mode: 'web' });
|
|
351
|
+
} else {
|
|
352
|
+
await openTerminalWithChat(a.name);
|
|
353
|
+
await openWebAssistant(a.id);
|
|
354
|
+
opened.push({ name: a.name, mode: 'both' });
|
|
355
|
+
}
|
|
356
|
+
await new Promise(r => setTimeout(r, 500));
|
|
357
|
+
}
|
|
358
|
+
return opened;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function loadUserAssistants(uid) {
|
|
362
|
+
try {
|
|
363
|
+
const assistants = await fetchAssistants(uid);
|
|
364
|
+
return assistants;
|
|
365
|
+
} catch {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function listenForName(assistants, apiKey) {
|
|
371
|
+
if (!apiKey || assistants.length <= 1) return;
|
|
372
|
+
let VoiceInput;
|
|
373
|
+
try {
|
|
374
|
+
VoiceInput = (await import('./voiceInput.js')).default;
|
|
375
|
+
} catch { return; }
|
|
376
|
+
|
|
377
|
+
const vi = new VoiceInput(apiKey);
|
|
378
|
+
const names = assistants.map(a => a.name.toLowerCase());
|
|
379
|
+
|
|
380
|
+
console.log(chalk.cyan(' 🎙 비서 이름을 말하면 해당 비서만 유지됩니다...'));
|
|
381
|
+
console.log(chalk.gray(` 비서 목록: ${assistants.map(a => a.name).join(', ')}`));
|
|
382
|
+
|
|
383
|
+
const ok = await vi.start({
|
|
384
|
+
onListening: () => {},
|
|
385
|
+
onSpeechStart: () => {},
|
|
386
|
+
onSpeechEnd: () => {},
|
|
387
|
+
onTranscript: (text) => {
|
|
388
|
+
const lower = text.toLowerCase().trim();
|
|
389
|
+
const matched = names.find(n => lower.includes(n));
|
|
390
|
+
if (matched) {
|
|
391
|
+
const matchedAssistant = assistants.find(a => a.name.toLowerCase() === matched);
|
|
392
|
+
console.log(chalk.bold.green(` ✓ "${matchedAssistant.name}" 선택됨 — 나머지 비서를 종료합니다.`));
|
|
393
|
+
vi.stop();
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (!ok) {
|
|
399
|
+
console.log(chalk.gray(' (음성 인식을 사용할 수 없어 모든 비서가 활성 상태로 유지됩니다)'));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
275
403
|
export async function startAgent(port = 9876, options = {}) {
|
|
276
404
|
console.log('');
|
|
277
405
|
console.log(chalk.bold.cyan(' 🖥️ Uwonbot Agent'));
|
|
278
406
|
console.log(chalk.gray(' ────────────────────────'));
|
|
279
407
|
console.log('');
|
|
280
408
|
|
|
409
|
+
const alreadyRunning = await isPortInUse(port);
|
|
410
|
+
if (alreadyRunning) {
|
|
411
|
+
console.log(chalk.yellow(` ⚠ Agent가 이미 포트 ${port}에서 실행 중입니다.`));
|
|
412
|
+
console.log(chalk.gray(' 기존 프로세스를 종료하고 재시작합니다...'));
|
|
413
|
+
killProcessOnPort(port);
|
|
414
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
415
|
+
}
|
|
416
|
+
|
|
281
417
|
await loadNativeModules();
|
|
282
418
|
console.log('');
|
|
283
419
|
|
|
284
420
|
const config = getConfig();
|
|
285
421
|
const uid = config.get('uid');
|
|
286
422
|
const email = config.get('email');
|
|
423
|
+
const idToken = config.get('idToken');
|
|
287
424
|
|
|
288
425
|
if (!uid) {
|
|
289
426
|
console.log(chalk.red(' ✗ Not logged in. Run: uwonbot login'));
|
|
290
427
|
process.exit(1);
|
|
291
428
|
}
|
|
292
429
|
|
|
430
|
+
if (idToken) setIdToken(idToken);
|
|
431
|
+
|
|
293
432
|
console.log(chalk.gray(` User: ${email}`));
|
|
294
433
|
console.log(chalk.gray(` Port: ${port}`));
|
|
434
|
+
|
|
435
|
+
let userAssistants = [];
|
|
436
|
+
try {
|
|
437
|
+
userAssistants = await loadUserAssistants(uid);
|
|
438
|
+
if (userAssistants.length > 0) {
|
|
439
|
+
console.log(chalk.gray(` Assistants: ${userAssistants.map(a => a.name).join(', ')}`));
|
|
440
|
+
} else {
|
|
441
|
+
console.log(chalk.gray(' Assistants: (none — default Uwonbot will be used)'));
|
|
442
|
+
}
|
|
443
|
+
} catch {}
|
|
295
444
|
console.log('');
|
|
296
445
|
|
|
297
446
|
if (!options.noMic) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
447
|
+
await ensureSox();
|
|
448
|
+
const clapListener = new ClapListener(async () => {
|
|
449
|
+
console.log(chalk.bold.cyan(' 👏 박수 감지! 비서 활성화 중...'));
|
|
450
|
+
|
|
451
|
+
if (userAssistants.length === 0) {
|
|
452
|
+
console.log(chalk.gray(' → 기본 Uwonbot 실행'));
|
|
453
|
+
openTerminalWithChat('Uwonbot');
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (userAssistants.length === 1) {
|
|
458
|
+
const a = userAssistants[0];
|
|
459
|
+
const mode = a.activationMode || 'web';
|
|
460
|
+
console.log(chalk.green(` → ${a.name} 실행 (${mode})`));
|
|
461
|
+
if (mode === 'terminal' || mode === 'both') await openTerminalWithChat(a.name);
|
|
462
|
+
if (mode === 'web' || mode === 'both') await openWebAssistant(a.id);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
console.log(chalk.cyan(` → ${userAssistants.length}개 비서 모두 활성화`));
|
|
467
|
+
const opened = await activateAllAssistants(userAssistants);
|
|
468
|
+
opened.forEach(o => {
|
|
469
|
+
console.log(chalk.gray(` ✓ ${o.name} (${o.mode})`));
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const geminiKey = userAssistants.find(a => a.apiKey)?.apiKey;
|
|
473
|
+
if (geminiKey) {
|
|
474
|
+
await listenForName(userAssistants, geminiKey);
|
|
475
|
+
} else {
|
|
476
|
+
console.log(chalk.gray(' → 모든 비서가 활성 상태로 유지됩니다.'));
|
|
477
|
+
console.log(chalk.gray(' (비서 이름을 말해 선택하려면 API 키가 필요합니다)'));
|
|
478
|
+
}
|
|
301
479
|
});
|
|
302
480
|
await clapListener.start();
|
|
303
481
|
console.log('');
|
|
@@ -318,22 +496,36 @@ export async function startAgent(port = 9876, options = {}) {
|
|
|
318
496
|
console.log(chalk.yellow(' ○ Client disconnected'));
|
|
319
497
|
});
|
|
320
498
|
|
|
321
|
-
ws.send(JSON.stringify({ type: 'welcome', agent: 'uwonbot', version: '1.
|
|
499
|
+
ws.send(JSON.stringify({ type: 'welcome', agent: 'uwonbot', version: '1.1.2', uid }));
|
|
322
500
|
});
|
|
323
501
|
|
|
324
|
-
wss.on('error', (err) => {
|
|
502
|
+
wss.on('error', async (err) => {
|
|
325
503
|
if (err.code === 'EADDRINUSE') {
|
|
326
|
-
console.log(chalk.
|
|
327
|
-
|
|
328
|
-
|
|
504
|
+
console.log(chalk.yellow(` ⚠ 포트 ${port} 충돌 — 기존 프로세스 종료 후 재시도...`));
|
|
505
|
+
killProcessOnPort(port);
|
|
506
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
507
|
+
try {
|
|
508
|
+
const retry = new WebSocketServer({ port });
|
|
509
|
+
retry.on('connection', (ws, req) => {
|
|
510
|
+
ws.on('message', async (data) => {
|
|
511
|
+
const result = await handleCommand(data.toString());
|
|
512
|
+
ws.send(JSON.stringify(result));
|
|
513
|
+
});
|
|
514
|
+
ws.send(JSON.stringify({ type: 'welcome', agent: 'uwonbot', version: '1.1.4', uid }));
|
|
515
|
+
});
|
|
516
|
+
console.log(chalk.green(` ✓ 재시도 성공 — ws://localhost:${port}`));
|
|
517
|
+
} catch {
|
|
518
|
+
console.log(chalk.red(` ✗ 포트 ${port}를 사용할 수 없습니다.`));
|
|
519
|
+
console.log(chalk.gray(` uwonbot agent --port ${port + 1}`));
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
return;
|
|
329
523
|
}
|
|
330
524
|
console.error(chalk.red(' ✗ Server error:'), err.message);
|
|
331
525
|
});
|
|
332
526
|
|
|
333
527
|
console.log(chalk.bold.green(` ✓ Agent running on ws://localhost:${port}`));
|
|
334
|
-
console.log(chalk.gray(' Waiting for connections...'));
|
|
335
|
-
console.log('');
|
|
336
|
-
console.log(chalk.gray(' Press Ctrl+C to stop'));
|
|
528
|
+
console.log(chalk.gray(' Waiting for connections... (Ctrl+C to stop)'));
|
|
337
529
|
console.log('');
|
|
338
530
|
|
|
339
531
|
process.on('SIGINT', () => {
|
package/src/autostart.js
CHANGED
|
@@ -12,9 +12,25 @@ function getUwonbotPath() {
|
|
|
12
12
|
const p = execSync('which uwonbot', { encoding: 'utf8' }).trim();
|
|
13
13
|
if (p) return p;
|
|
14
14
|
} catch {}
|
|
15
|
+
const globalPaths = [
|
|
16
|
+
'/usr/local/bin/uwonbot',
|
|
17
|
+
'/opt/homebrew/bin/uwonbot',
|
|
18
|
+
join(homedir(), '.npm-global', 'bin', 'uwonbot'),
|
|
19
|
+
join(homedir(), '.nvm', 'versions', 'node', '*', 'bin', 'uwonbot'),
|
|
20
|
+
];
|
|
21
|
+
for (const gp of globalPaths) {
|
|
22
|
+
if (existsSync(gp)) return gp;
|
|
23
|
+
}
|
|
15
24
|
return 'uwonbot';
|
|
16
25
|
}
|
|
17
26
|
|
|
27
|
+
function getNodePath() {
|
|
28
|
+
try {
|
|
29
|
+
return execSync('which node', { encoding: 'utf8' }).trim();
|
|
30
|
+
} catch {}
|
|
31
|
+
return '/usr/local/bin/node';
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
function getMacPlistPath() {
|
|
19
35
|
const dir = join(homedir(), 'Library', 'LaunchAgents');
|
|
20
36
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
@@ -36,6 +52,10 @@ export function enableAutostart() {
|
|
|
36
52
|
const logDir = join(homedir(), '.uwonbot');
|
|
37
53
|
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
38
54
|
|
|
55
|
+
const nodePath = getNodePath();
|
|
56
|
+
const nodeDir = nodePath.replace(/\/node$/, '');
|
|
57
|
+
const fullPath = `/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${nodeDir}:${process.env.PATH || ''}`;
|
|
58
|
+
|
|
39
59
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
40
60
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
41
61
|
<plist version="1.0">
|
|
@@ -50,7 +70,12 @@ export function enableAutostart() {
|
|
|
50
70
|
<key>RunAtLoad</key>
|
|
51
71
|
<true/>
|
|
52
72
|
<key>KeepAlive</key>
|
|
53
|
-
<
|
|
73
|
+
<dict>
|
|
74
|
+
<key>SuccessfulExit</key>
|
|
75
|
+
<false/>
|
|
76
|
+
</dict>
|
|
77
|
+
<key>ThrottleInterval</key>
|
|
78
|
+
<integer>10</integer>
|
|
54
79
|
<key>StandardOutPath</key>
|
|
55
80
|
<string>${logPath}</string>
|
|
56
81
|
<key>StandardErrorPath</key>
|
|
@@ -58,7 +83,9 @@ export function enableAutostart() {
|
|
|
58
83
|
<key>EnvironmentVariables</key>
|
|
59
84
|
<dict>
|
|
60
85
|
<key>PATH</key>
|
|
61
|
-
<string
|
|
86
|
+
<string>${fullPath}</string>
|
|
87
|
+
<key>HOME</key>
|
|
88
|
+
<string>${homedir()}</string>
|
|
62
89
|
</dict>
|
|
63
90
|
</dict>
|
|
64
91
|
</plist>`;
|
|
@@ -69,11 +96,16 @@ export function enableAutostart() {
|
|
|
69
96
|
} catch {}
|
|
70
97
|
try {
|
|
71
98
|
execSync(`launchctl load -w "${plistPath}"`);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
99
|
+
console.log(chalk.green(' ✓ uwonbot agent 자동 시작이 등록되었습니다'));
|
|
100
|
+
console.log(chalk.green(' ✓ agent가 백그라운드에서 실행되었습니다'));
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.log(chalk.yellow(` ⚠ launchctl 등록 중 문제: ${e.message}`));
|
|
103
|
+
}
|
|
104
|
+
console.log(chalk.gray(` 설정: ${plistPath}`));
|
|
76
105
|
console.log(chalk.gray(` 로그: ${logPath}`));
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(chalk.white(' 컴퓨터를 켤 때마다 자동으로 agent가 실행됩니다.'));
|
|
108
|
+
console.log(chalk.white(' 박수 감지, OS 제어 등이 항상 준비됩니다.'));
|
|
77
109
|
return true;
|
|
78
110
|
}
|
|
79
111
|
|
package/src/clapListener.js
CHANGED
|
@@ -25,9 +25,13 @@ export default class ClapListener {
|
|
|
25
25
|
let micModule;
|
|
26
26
|
try {
|
|
27
27
|
micModule = (await import('mic')).default;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
console.log(chalk.
|
|
28
|
+
if (!micModule) throw new Error('mic default export is null');
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.log(chalk.yellow(' ⚠ mic 모듈을 로드할 수 없습니다. 박수 감지를 사용할 수 없습니다.'));
|
|
31
|
+
console.log(chalk.gray(' 해결 방법:'));
|
|
32
|
+
console.log(chalk.white(' 1. npm install -g uwonbot@latest (mic 포함 재설치)'));
|
|
33
|
+
console.log(chalk.white(' 2. brew install sox (macOS)'));
|
|
34
|
+
console.log(chalk.gray(` 원인: ${e.message}`));
|
|
31
35
|
return false;
|
|
32
36
|
}
|
|
33
37
|
|