uwonbot 1.0.7 → 1.0.9

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
@@ -6,13 +6,14 @@ import { listAssistants, selectAssistant } from '../src/assistants.js';
6
6
  import { startChat } from '../src/chat.js';
7
7
  import { getConfig } from '../src/config.js';
8
8
  import { startAgent } from '../src/agent.js';
9
+ import { runSetupWizard } from '../src/setup.js';
9
10
 
10
11
  showBanner();
11
12
 
12
13
  program
13
14
  .name('uwonbot')
14
15
  .description('Uwonbot AI Assistant — Your AI controls your computer')
15
- .version('1.0.7');
16
+ .version('1.0.9');
16
17
 
17
18
  program
18
19
  .command('login')
@@ -74,8 +75,22 @@ program
74
75
  .command('agent')
75
76
  .description('Start the local agent for OS-level mouse/keyboard control')
76
77
  .option('-p, --port <port>', 'WebSocket server port', '9876')
78
+ .option('--no-mic', 'Disable microphone clap detection')
77
79
  .action(async (opts) => {
78
- await startAgent(parseInt(opts.port));
80
+ await startAgent(parseInt(opts.port), { noMic: !opts.mic });
81
+ });
82
+
83
+ program
84
+ .command('setup')
85
+ .description('Run initial setup wizard again')
86
+ .action(async () => {
87
+ const config = getConfig();
88
+ if (!config.get('uid')) {
89
+ console.log('\n ⚠️ Please log in first: uwonbot login\n');
90
+ process.exit(1);
91
+ }
92
+ config.set('setupComplete', false);
93
+ await runSetupWizard();
79
94
  });
80
95
 
81
96
  program
@@ -126,18 +141,22 @@ if (process.argv.length <= 2) {
126
141
  const config = getConfig();
127
142
  const uid = config.get('uid');
128
143
  if (uid) {
129
- const email = config.get('email') || 'unknown';
130
- console.log(` Logged in as: ${email}`);
131
- console.log('');
132
- console.log(' Commands:');
133
- console.log(' uwonbot chat Start chatting with your AI assistant');
134
- console.log(' uwonbot assistants List your AI assistants');
135
- console.log(' uwonbot run "..." Ask AI to run a task');
136
- console.log(' uwonbot agent Start local agent (OS control)');
137
- console.log(' uwonbot upgrade Upgrade to latest version');
138
- console.log(' uwonbot reset-password Reset password via email');
139
- console.log(' uwonbot logout Log out');
140
- console.log('');
144
+ if (!config.get('setupComplete')) {
145
+ await runSetupWizard();
146
+ } else {
147
+ const email = config.get('email') || 'unknown';
148
+ console.log(` Logged in as: ${email}`);
149
+ console.log('');
150
+ console.log(' Commands:');
151
+ console.log(' uwonbot chat Start chatting with your AI assistant');
152
+ console.log(' uwonbot assistants List your AI assistants');
153
+ console.log(' uwonbot run "..." Ask AI to run a task');
154
+ console.log(' uwonbot agent Start local agent (OS control)');
155
+ console.log(' uwonbot upgrade Upgrade to latest version');
156
+ console.log(' uwonbot reset-password Reset password via email');
157
+ console.log(' uwonbot logout Log out');
158
+ console.log('');
159
+ }
141
160
  } else {
142
161
  console.log(' Get started:');
143
162
  console.log(' uwonbot login Log in to your account');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uwonbot",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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/auth.js CHANGED
@@ -3,6 +3,7 @@ import inquirer from 'inquirer';
3
3
  import ora from 'ora';
4
4
  import { getConfig } from './config.js';
5
5
  import { loginWithEmail, sendPasswordReset } from './firebase-client.js';
6
+ import { runSetupWizard } from './setup.js';
6
7
 
7
8
  export async function loginCommand() {
8
9
  const config = getConfig();
@@ -44,10 +45,15 @@ export async function loginCommand() {
44
45
  config.set('idToken', user.idToken);
45
46
  config.set('refreshToken', user.refreshToken);
46
47
  spinner.succeed(chalk.green(`Logged in as ${user.email}`));
47
- console.log('');
48
- console.log(chalk.gray(' Next: uwonbot chat — Start chatting with your AI assistant'));
49
- console.log(chalk.gray(' uwonbot assistants — List your assistants'));
50
- console.log('');
48
+
49
+ if (!config.get('setupComplete')) {
50
+ await runSetupWizard();
51
+ } else {
52
+ console.log('');
53
+ console.log(chalk.gray(' Next: uwonbot chat — Start chatting with your AI assistant'));
54
+ console.log(chalk.gray(' uwonbot assistants — List your assistants'));
55
+ console.log('');
56
+ }
51
57
  } catch (err) {
52
58
  spinner.fail(chalk.red('Login failed'));
53
59
  const msg = (err.message || '').toUpperCase();
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
+ }
package/src/config.js CHANGED
@@ -14,6 +14,9 @@ export function getConfig() {
14
14
  refreshToken: { type: 'string', default: '' },
15
15
  activeAssistantId: { type: 'string', default: '' },
16
16
  activeAssistantName: { type: 'string', default: '' },
17
+ setupComplete: { type: 'boolean', default: false },
18
+ agentAutoStart: { type: 'boolean', default: false },
19
+ clapDetection: { type: 'boolean', default: true },
17
20
  },
18
21
  });
19
22
  }
@@ -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
+ }
package/src/setup.js ADDED
@@ -0,0 +1,128 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import { execSync } from 'child_process';
4
+ import { getConfig } from './config.js';
5
+
6
+ function checkSoxInstalled() {
7
+ try {
8
+ execSync('which sox', { stdio: 'ignore' });
9
+ return true;
10
+ } catch {
11
+ try {
12
+ execSync('which rec', { stdio: 'ignore' });
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+ }
19
+
20
+ export async function runSetupWizard() {
21
+ const config = getConfig();
22
+
23
+ console.log('');
24
+ console.log(chalk.bold.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
25
+ console.log(chalk.bold.cyan(' 🚀 Uwonbot 초기 설정'));
26
+ console.log(chalk.bold.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
27
+ console.log('');
28
+ console.log(chalk.white(' 환영합니다! Uwonbot을 최대한 활용하기 위한'));
29
+ console.log(chalk.white(' 초기 설정을 진행합니다.'));
30
+ console.log('');
31
+
32
+ console.log(chalk.bold.white(' 1️⃣ 에이전트 (Agent)'));
33
+ console.log(chalk.gray(' AI 비서가 컴퓨터를 제어하고, 박수로 비서를'));
34
+ console.log(chalk.gray(' 활성화하려면 에이전트가 필요합니다.'));
35
+ console.log('');
36
+
37
+ const { enableAgent } = await inquirer.prompt([{
38
+ type: 'confirm',
39
+ name: 'enableAgent',
40
+ message: '에이전트를 자동 시작하시겠습니까?',
41
+ default: true,
42
+ }]);
43
+
44
+ config.set('agentAutoStart', enableAgent);
45
+
46
+ if (enableAgent) {
47
+ console.log('');
48
+ console.log(chalk.bold.white(' 2️⃣ 박수 감지 (Clap Detection)'));
49
+ console.log(chalk.gray(' 박수 2번으로 비서를 활성화할 수 있습니다.'));
50
+ console.log(chalk.gray(' 마이크와 SoX가 필요합니다.'));
51
+ console.log('');
52
+
53
+ const hasSox = checkSoxInstalled();
54
+ if (!hasSox) {
55
+ console.log(chalk.yellow(' ⚠ SoX가 설치되어 있지 않습니다.'));
56
+ console.log(chalk.gray(' 박수 감지를 위해 SoX를 설치해주세요:'));
57
+ console.log('');
58
+ console.log(chalk.white(' macOS: ') + chalk.cyan('brew install sox'));
59
+ console.log(chalk.white(' Ubuntu: ') + chalk.cyan('sudo apt install sox'));
60
+ console.log('');
61
+
62
+ const { installSox } = await inquirer.prompt([{
63
+ type: 'confirm',
64
+ name: 'installSox',
65
+ message: 'SoX를 지금 설치하시겠습니까? (macOS - Homebrew)',
66
+ default: true,
67
+ }]);
68
+
69
+ if (installSox) {
70
+ try {
71
+ console.log(chalk.cyan(' SoX 설치 중...'));
72
+ execSync('brew install sox', { stdio: 'inherit' });
73
+ console.log(chalk.green(' ✓ SoX 설치 완료'));
74
+ } catch {
75
+ console.log(chalk.yellow(' ⚠ SoX 설치에 실패했습니다. 나중에 수동으로 설치해주세요.'));
76
+ }
77
+ }
78
+ } else {
79
+ console.log(chalk.green(' ✓ SoX가 이미 설치되어 있습니다.'));
80
+ }
81
+
82
+ const { enableClap } = await inquirer.prompt([{
83
+ type: 'confirm',
84
+ name: 'enableClap',
85
+ message: '박수 감지를 활성화하시겠습니까?',
86
+ default: true,
87
+ }]);
88
+
89
+ config.set('clapDetection', enableClap);
90
+ }
91
+
92
+ console.log('');
93
+ console.log(chalk.bold.white(' 3️⃣ 기기 생체인증'));
94
+ console.log(chalk.gray(' 보안을 위해 비서 사용 시 지문/Face ID 인증을'));
95
+ console.log(chalk.gray(' 요구할 수 있습니다.'));
96
+ console.log(chalk.gray(' 웹사이트 마이페이지에서 기기를 등록해주세요:'));
97
+ console.log(chalk.cyan(' https://chartapp-653e1.web.app/profile'));
98
+ console.log('');
99
+
100
+ config.set('setupComplete', true);
101
+
102
+ console.log(chalk.bold.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
103
+ console.log(chalk.bold.green(' ✓ 설정 완료!'));
104
+ console.log(chalk.bold.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
105
+ console.log('');
106
+ console.log(chalk.white(' 다음 단계:'));
107
+
108
+ if (enableAgent) {
109
+ console.log(chalk.cyan(' uwonbot agent') + chalk.gray(' — 에이전트 시작 (박수 감지 + OS 제어)'));
110
+ }
111
+ console.log(chalk.cyan(' uwonbot chat') + chalk.gray(' — AI 비서와 대화'));
112
+ console.log(chalk.cyan(' uwonbot assistants') + chalk.gray(' — 비서 목록 확인'));
113
+ console.log('');
114
+
115
+ if (enableAgent) {
116
+ const { startNow } = await inquirer.prompt([{
117
+ type: 'confirm',
118
+ name: 'startNow',
119
+ message: '에이전트를 지금 바로 시작하시겠습니까?',
120
+ default: true,
121
+ }]);
122
+
123
+ if (startNow) {
124
+ const { startAgent } = await import('./agent.js');
125
+ await startAgent(9876, { noMic: !config.get('clapDetection') });
126
+ }
127
+ }
128
+ }