uwonbot 1.0.7 → 1.0.8

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
@@ -12,7 +12,7 @@ showBanner();
12
12
  program
13
13
  .name('uwonbot')
14
14
  .description('Uwonbot AI Assistant — Your AI controls your computer')
15
- .version('1.0.7');
15
+ .version('1.0.8');
16
16
 
17
17
  program
18
18
  .command('login')
@@ -74,8 +74,9 @@ program
74
74
  .command('agent')
75
75
  .description('Start the local agent for OS-level mouse/keyboard control')
76
76
  .option('-p, --port <port>', 'WebSocket server port', '9876')
77
+ .option('--no-mic', 'Disable microphone clap detection')
77
78
  .action(async (opts) => {
78
- await startAgent(parseInt(opts.port));
79
+ await startAgent(parseInt(opts.port), { noMic: !opts.mic });
79
80
  });
80
81
 
81
82
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uwonbot",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Uwonbot AI Assistant CLI — Your AI controls your computer",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -34,6 +34,7 @@
34
34
  },
35
35
  "optionalDependencies": {
36
36
  "@nut-tree-fork/nut-js": "^4.2.0",
37
+ "mic": "^2.5.1",
37
38
  "screenshot-desktop": "^1.15.0"
38
39
  }
39
40
  }
package/src/agent.js CHANGED
@@ -3,6 +3,7 @@ import { exec } from 'child_process';
3
3
  import { promisify } from 'util';
4
4
  import chalk from 'chalk';
5
5
  import { getConfig } from './config.js';
6
+ import ClapListener from './clapListener.js';
6
7
 
7
8
  const execAsync = promisify(exec);
8
9
  const platform = process.platform;
@@ -271,7 +272,7 @@ async function handleCommand(msg) {
271
272
  }
272
273
  }
273
274
 
274
- export async function startAgent(port = 9876) {
275
+ export async function startAgent(port = 9876, options = {}) {
275
276
  console.log('');
276
277
  console.log(chalk.bold.cyan(' 🖥️ Uwonbot Agent'));
277
278
  console.log(chalk.gray(' ────────────────────────'));
@@ -293,6 +294,15 @@ export async function startAgent(port = 9876) {
293
294
  console.log(chalk.gray(` Port: ${port}`));
294
295
  console.log('');
295
296
 
297
+ if (!options.noMic) {
298
+ const clapListener = new ClapListener(() => {
299
+ console.log(chalk.bold.cyan(' 🎯 Opening terminal...'));
300
+ openTerminalWithChat();
301
+ });
302
+ await clapListener.start();
303
+ console.log('');
304
+ }
305
+
296
306
  const wss = new WebSocketServer({ port });
297
307
 
298
308
  wss.on('connection', (ws, req) => {
package/src/chat.js CHANGED
@@ -2,9 +2,78 @@ import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
3
  import ora from 'ora';
4
4
  import readline from 'readline';
5
+ import crypto from 'crypto';
6
+ import open from 'open';
5
7
  import { getConfig } from './config.js';
6
8
  import { sendToBrain } from './brain.js';
7
9
  import { showMiniBar } from './banner.js';
10
+ import {
11
+ hasRegisteredDevices,
12
+ createCLISession,
13
+ checkCLISession,
14
+ deleteCLISession,
15
+ } from './firebase-client.js';
16
+
17
+ const WEB_APP_URL = 'https://chartapp-653e1.web.app';
18
+
19
+ async function requireBiometricAuth(uid) {
20
+ try {
21
+ const hasDevices = await hasRegisteredDevices(uid);
22
+ if (!hasDevices) return true;
23
+ } catch {
24
+ return true;
25
+ }
26
+
27
+ const token = crypto.randomUUID();
28
+
29
+ try {
30
+ await createCLISession(uid, token);
31
+ } catch (err) {
32
+ console.log(chalk.yellow(`\n ⚠ 인증 세션 생성 실패: ${err.message}\n`));
33
+ return false;
34
+ }
35
+
36
+ const authUrl = `${WEB_APP_URL}/cli-auth?token=${token}`;
37
+ console.log('');
38
+ console.log(chalk.bold.cyan(' 🔐 생체인증이 필요합니다'));
39
+ console.log(chalk.gray(' ─────────────────────────'));
40
+ console.log(chalk.white(' 브라우저에서 지문/Face ID 인증을 완료해주세요.'));
41
+ console.log('');
42
+ console.log(chalk.gray(` ${authUrl}`));
43
+ console.log('');
44
+
45
+ try {
46
+ await open(authUrl);
47
+ } catch {
48
+ console.log(chalk.yellow(' 브라우저를 자동으로 열 수 없습니다. 위 URL을 직접 열어주세요.'));
49
+ }
50
+
51
+ const spinner = ora({
52
+ text: chalk.gray('인증 대기 중... (60초 이내에 완료해주세요)'),
53
+ spinner: 'dots',
54
+ color: 'cyan',
55
+ }).start();
56
+
57
+ const MAX_WAIT = 60000;
58
+ const POLL_INTERVAL = 2000;
59
+ const start = Date.now();
60
+
61
+ while (Date.now() - start < MAX_WAIT) {
62
+ await new Promise(r => setTimeout(r, POLL_INTERVAL));
63
+ try {
64
+ const session = await checkCLISession(uid, token);
65
+ if (session?.verified) {
66
+ spinner.succeed(chalk.green('인증 완료!'));
67
+ try { await deleteCLISession(uid, token); } catch {}
68
+ return true;
69
+ }
70
+ } catch {}
71
+ }
72
+
73
+ spinner.fail(chalk.red('인증 시간 초과'));
74
+ try { await deleteCLISession(uid, token); } catch {}
75
+ return false;
76
+ }
8
77
 
9
78
  export async function startChat(assistantName, assistant, initialCommand) {
10
79
  const config = getConfig();
@@ -19,6 +88,14 @@ export async function startChat(assistantName, assistant, initialCommand) {
19
88
  return;
20
89
  }
21
90
 
91
+ if (!assistant.isDefaultBot) {
92
+ const authOk = await requireBiometricAuth(uid);
93
+ if (!authOk) {
94
+ console.log(chalk.red('\n ✗ 인증에 실패했습니다. 비서를 사용하려면 인증이 필요합니다.\n'));
95
+ return;
96
+ }
97
+ }
98
+
22
99
  const brainLabel = assistant.brain === 'default' ? 'Uwonbot (Gemini)'
23
100
  : assistant.brain === 'openai' ? 'OpenAI GPT'
24
101
  : assistant.brain === 'claude' ? 'Anthropic Claude'
@@ -0,0 +1,107 @@
1
+ import chalk from 'chalk';
2
+
3
+ const DEFAULT_THRESHOLD = 0.4;
4
+ const CLAP_MIN_INTERVAL = 100;
5
+ const CLAP_MAX_INTERVAL = 800;
6
+ const REQUIRED_CLAPS = 2;
7
+ const RESET_TIMEOUT = 1500;
8
+ const SAMPLE_RATE = 16000;
9
+ const BUFFER_SIZE = 1024;
10
+
11
+ export default class ClapListener {
12
+ constructor(onDoubleClap) {
13
+ this.onDoubleClap = onDoubleClap;
14
+ this.threshold = DEFAULT_THRESHOLD;
15
+ this.clapTimes = [];
16
+ this.lastClapTime = 0;
17
+ this.resetTimer = null;
18
+ this.mic = null;
19
+ this.running = false;
20
+ }
21
+
22
+ async start() {
23
+ if (this.running) return;
24
+
25
+ let micModule;
26
+ try {
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'));
31
+ return false;
32
+ }
33
+
34
+ try {
35
+ this.mic = micModule({
36
+ rate: String(SAMPLE_RATE),
37
+ channels: '1',
38
+ bitwidth: '16',
39
+ encoding: 'signed-integer',
40
+ endian: 'little',
41
+ device: 'default',
42
+ debug: false,
43
+ });
44
+
45
+ const stream = this.mic.getAudioStream();
46
+ this.running = true;
47
+
48
+ stream.on('data', (buf) => {
49
+ if (!this.running) return;
50
+ this._processBuffer(buf);
51
+ });
52
+
53
+ stream.on('error', (err) => {
54
+ if (err.message?.includes('spawn') || err.message?.includes('sox') || err.message?.includes('rec')) {
55
+ console.log(chalk.yellow(' ⚠ SoX not found. Install for clap detection:'));
56
+ console.log(chalk.gray(' macOS: brew install sox'));
57
+ console.log(chalk.gray(' Ubuntu: sudo apt install sox'));
58
+ this.running = false;
59
+ }
60
+ });
61
+
62
+ this.mic.start();
63
+ console.log(chalk.green(' ✓ Microphone listening for claps'));
64
+ return true;
65
+ } catch (e) {
66
+ console.log(chalk.yellow(` ⚠ Microphone init failed: ${e.message}`));
67
+ return false;
68
+ }
69
+ }
70
+
71
+ stop() {
72
+ this.running = false;
73
+ clearTimeout(this.resetTimer);
74
+ try { this.mic?.stop(); } catch {}
75
+ this.mic = null;
76
+ }
77
+
78
+ _processBuffer(buf) {
79
+ let peak = 0;
80
+ for (let i = 0; i < buf.length - 1; i += 2) {
81
+ const sample = buf.readInt16LE(i) / 32768;
82
+ const abs = Math.abs(sample);
83
+ if (abs > peak) peak = abs;
84
+ }
85
+
86
+ const now = Date.now();
87
+ const isClap = peak > this.threshold && (now - this.lastClapTime) > CLAP_MIN_INTERVAL;
88
+
89
+ if (!isClap) return;
90
+
91
+ this.lastClapTime = now;
92
+ this.clapTimes.push(now);
93
+
94
+ clearTimeout(this.resetTimer);
95
+ this.resetTimer = setTimeout(() => { this.clapTimes = []; }, RESET_TIMEOUT);
96
+
97
+ if (this.clapTimes.length >= REQUIRED_CLAPS) {
98
+ const interval = this.clapTimes[this.clapTimes.length - 1] - this.clapTimes[this.clapTimes.length - 2];
99
+ if (interval <= CLAP_MAX_INTERVAL) {
100
+ this.clapTimes = [];
101
+ clearTimeout(this.resetTimer);
102
+ console.log(chalk.cyan(' 👏 Double clap detected!'));
103
+ this.onDoubleClap?.();
104
+ }
105
+ }
106
+ }
107
+ }
@@ -83,3 +83,57 @@ export async function sendPasswordReset(email) {
83
83
  }
84
84
 
85
85
  export function setIdToken(token) { cachedIdToken = token; }
86
+
87
+ export async function createCLISession(uid, token) {
88
+ if (!cachedIdToken) throw new Error('Not logged in');
89
+ const url = `${FIRESTORE_URL}/users/${uid}/cliSessions/${token}`;
90
+ const res = await fetch(url, {
91
+ method: 'PATCH',
92
+ headers: {
93
+ 'Authorization': `Bearer ${cachedIdToken}`,
94
+ 'Content-Type': 'application/json',
95
+ },
96
+ body: JSON.stringify({
97
+ fields: {
98
+ verified: { booleanValue: false },
99
+ createdAt: { timestampValue: new Date().toISOString() },
100
+ },
101
+ }),
102
+ });
103
+ const data = await res.json();
104
+ if (data.error) throw new Error(data.error.message);
105
+ return data;
106
+ }
107
+
108
+ export async function checkCLISession(uid, token) {
109
+ if (!cachedIdToken) throw new Error('Not logged in');
110
+ const url = `${FIRESTORE_URL}/users/${uid}/cliSessions/${token}`;
111
+ const res = await fetch(url, {
112
+ headers: { 'Authorization': `Bearer ${cachedIdToken}` },
113
+ });
114
+ const data = await res.json();
115
+ if (data.error) return null;
116
+ const fields = data.fields || {};
117
+ return {
118
+ verified: fields.verified?.booleanValue === true,
119
+ };
120
+ }
121
+
122
+ export async function deleteCLISession(uid, token) {
123
+ if (!cachedIdToken) throw new Error('Not logged in');
124
+ const url = `${FIRESTORE_URL}/users/${uid}/cliSessions/${token}`;
125
+ await fetch(url, {
126
+ method: 'DELETE',
127
+ headers: { 'Authorization': `Bearer ${cachedIdToken}` },
128
+ });
129
+ }
130
+
131
+ export async function hasRegisteredDevices(uid) {
132
+ if (!cachedIdToken) throw new Error('Not logged in');
133
+ const url = `${FIRESTORE_URL}/users/${uid}/devices?pageSize=1`;
134
+ const res = await fetch(url, {
135
+ headers: { 'Authorization': `Bearer ${cachedIdToken}` },
136
+ });
137
+ const data = await res.json();
138
+ return (data.documents || []).length > 0;
139
+ }