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 CHANGED
@@ -14,7 +14,7 @@ showBanner();
14
14
  program
15
15
  .name('uwonbot')
16
16
  .description('Uwonbot AI Assistant — Your AI controls your computer')
17
- .version('1.1.2');
17
+ .version('1.1.4');
18
18
 
19
19
  program
20
20
  .command('login')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uwonbot",
3
- "version": "1.1.2",
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 { exec } from 'child_process';
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
- const clapListener = new ClapListener(() => {
299
- console.log(chalk.bold.cyan(' 🎯 Opening terminal...'));
300
- openTerminalWithChat();
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.0.3', uid }));
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.red(` Port ${port} is already in use`));
327
- console.log(chalk.gray(` Try: uwonbot agent --port ${port + 1}`));
328
- process.exit(1);
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
- <true/>
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>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${process.env.PATH || ''}</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
- } catch {}
73
-
74
- console.log(chalk.green(' ✓ uwonbot agent 자동 시작이 등록되었습니다'));
75
- console.log(chalk.gray(` ${plistPath}`));
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
 
@@ -25,9 +25,13 @@ export default class ClapListener {
25
25
  let micModule;
26
26
  try {
27
27
  micModule = (await import('mic')).default;
28
- } catch {
29
- console.log(chalk.yellow(' ⚠ mic module not found. Clap detection disabled.'));
30
- console.log(chalk.gray(' Install: npm install -g mic'));
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