uwonbot 1.0.6 → 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.6');
15
+ .version('1.0.8');
16
16
 
17
17
  program
18
18
  .command('login')
@@ -42,18 +42,30 @@ program
42
42
  program
43
43
  .command('chat [assistantName]')
44
44
  .description('Start chatting with an AI assistant')
45
- .action(async (assistantName) => {
45
+ .option('-n, --name <name>', 'Assistant name to launch directly')
46
+ .action(async (assistantName, opts) => {
46
47
  const config = getConfig();
47
48
  if (!config.get('uid')) {
48
49
  console.log('\n ⚠️ Please log in first: uwonbot login\n');
49
50
  process.exit(1);
50
51
  }
51
- if (assistantName) {
52
- await startChat(assistantName);
52
+ const targetName = opts.name || assistantName;
53
+ if (targetName) {
54
+ await startChat(targetName);
53
55
  } else {
54
56
  const assistant = await selectAssistant();
55
57
  if (assistant) {
56
58
  await startChat(null, assistant);
59
+ } else {
60
+ const defaultBot = {
61
+ name: 'Uwonbot',
62
+ avatar: '🤖',
63
+ brain: 'default',
64
+ voiceLang: 'ko-KR',
65
+ voiceStyle: 'male',
66
+ isDefaultBot: true,
67
+ };
68
+ await startChat(null, defaultBot);
57
69
  }
58
70
  }
59
71
  });
@@ -62,8 +74,9 @@ program
62
74
  .command('agent')
63
75
  .description('Start the local agent for OS-level mouse/keyboard control')
64
76
  .option('-p, --port <port>', 'WebSocket server port', '9876')
77
+ .option('--no-mic', 'Disable microphone clap detection')
65
78
  .action(async (opts) => {
66
- await startAgent(parseInt(opts.port));
79
+ await startAgent(parseInt(opts.port), { noMic: !opts.mic });
67
80
  });
68
81
 
69
82
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uwonbot",
3
- "version": "1.0.6",
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;
@@ -194,6 +195,33 @@ async function openApp(appName) {
194
195
  }
195
196
  }
196
197
 
198
+ async function openTerminalWithChat(assistantName) {
199
+ const name = assistantName || 'uwonbot';
200
+ try {
201
+ if (platform === 'darwin') {
202
+ await execAsync(`osascript -e 'tell application "Terminal"
203
+ activate
204
+ do script "uwonbot chat --name \\"${name}\\""
205
+ end tell'`);
206
+ } else if (platform === 'win32') {
207
+ await execAsync(`start cmd /k "uwonbot chat --name \\"${name}\\""`);
208
+ } else {
209
+ const terminals = ['gnome-terminal', 'xterm', 'konsole', 'xfce4-terminal'];
210
+ for (const t of terminals) {
211
+ try {
212
+ await execAsync(`which ${t}`);
213
+ await execAsync(`${t} -- uwonbot chat --name "${name}"`);
214
+ return true;
215
+ } catch {}
216
+ }
217
+ }
218
+ return true;
219
+ } catch (e) {
220
+ console.error(chalk.red(' Terminal open failed:'), e.message);
221
+ return false;
222
+ }
223
+ }
224
+
197
225
  async function handleCommand(msg) {
198
226
  try {
199
227
  const cmd = JSON.parse(msg);
@@ -231,6 +259,9 @@ async function handleCommand(msg) {
231
259
  case 'screen_size':
232
260
  const size = await getScreenSize();
233
261
  return { ok: true, ...size };
262
+ case 'open_terminal':
263
+ const termOk = await openTerminalWithChat(cmd.assistantName || cmd.name);
264
+ return { ok: termOk };
234
265
  case 'ping':
235
266
  return { ok: true, pong: true };
236
267
  default:
@@ -241,7 +272,7 @@ async function handleCommand(msg) {
241
272
  }
242
273
  }
243
274
 
244
- export async function startAgent(port = 9876) {
275
+ export async function startAgent(port = 9876, options = {}) {
245
276
  console.log('');
246
277
  console.log(chalk.bold.cyan(' 🖥️ Uwonbot Agent'));
247
278
  console.log(chalk.gray(' ────────────────────────'));
@@ -263,6 +294,15 @@ export async function startAgent(port = 9876) {
263
294
  console.log(chalk.gray(` Port: ${port}`));
264
295
  console.log('');
265
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
+
266
306
  const wss = new WebSocketServer({ port });
267
307
 
268
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
+ }