uwonbot 1.0.1 → 1.0.3
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 +11 -1
- package/package.json +7 -3
- package/src/agent.js +305 -0
- package/src/assistants.js +5 -1
- package/src/auth.js +3 -2
- package/src/firebase-client.js +61 -41
package/bin/uwonbot.js
CHANGED
|
@@ -5,13 +5,14 @@ import { loginCommand, logoutCommand, whoamiCommand } from '../src/auth.js';
|
|
|
5
5
|
import { listAssistants, selectAssistant } from '../src/assistants.js';
|
|
6
6
|
import { startChat } from '../src/chat.js';
|
|
7
7
|
import { getConfig } from '../src/config.js';
|
|
8
|
+
import { startAgent } from '../src/agent.js';
|
|
8
9
|
|
|
9
10
|
showBanner();
|
|
10
11
|
|
|
11
12
|
program
|
|
12
13
|
.name('uwonbot')
|
|
13
14
|
.description('Uwonbot AI Assistant — Your AI controls your computer')
|
|
14
|
-
.version('1.0.
|
|
15
|
+
.version('1.0.3');
|
|
15
16
|
|
|
16
17
|
program
|
|
17
18
|
.command('login')
|
|
@@ -52,6 +53,14 @@ program
|
|
|
52
53
|
}
|
|
53
54
|
});
|
|
54
55
|
|
|
56
|
+
program
|
|
57
|
+
.command('agent')
|
|
58
|
+
.description('Start the local agent for OS-level mouse/keyboard control')
|
|
59
|
+
.option('-p, --port <port>', 'WebSocket server port', '9876')
|
|
60
|
+
.action(async (opts) => {
|
|
61
|
+
await startAgent(parseInt(opts.port));
|
|
62
|
+
});
|
|
63
|
+
|
|
55
64
|
program
|
|
56
65
|
.command('run <command>')
|
|
57
66
|
.description('Ask your assistant to run a task')
|
|
@@ -78,6 +87,7 @@ if (process.argv.length <= 2) {
|
|
|
78
87
|
console.log(' uwonbot chat Start chatting with your AI assistant');
|
|
79
88
|
console.log(' uwonbot assistants List your AI assistants');
|
|
80
89
|
console.log(' uwonbot run "..." Ask AI to run a task');
|
|
90
|
+
console.log(' uwonbot agent Start local agent (OS control)');
|
|
81
91
|
console.log(' uwonbot logout Log out');
|
|
82
92
|
console.log('');
|
|
83
93
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uwonbot",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Uwonbot AI Assistant CLI — Your AI controls your computer",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,10 +26,14 @@
|
|
|
26
26
|
"chalk": "^5.3.0",
|
|
27
27
|
"commander": "^12.1.0",
|
|
28
28
|
"conf": "^13.0.1",
|
|
29
|
-
"firebase": "^11.0.0",
|
|
30
29
|
"inquirer": "^12.0.0",
|
|
31
30
|
"node-fetch": "^3.3.2",
|
|
32
31
|
"open": "^10.1.0",
|
|
33
|
-
"ora": "^8.1.0"
|
|
32
|
+
"ora": "^8.1.0",
|
|
33
|
+
"ws": "^8.18.0"
|
|
34
|
+
},
|
|
35
|
+
"optionalDependencies": {
|
|
36
|
+
"@nut-tree-fork/nut-js": "^4.2.0",
|
|
37
|
+
"screenshot-desktop": "^1.15.0"
|
|
34
38
|
}
|
|
35
39
|
}
|
package/src/agent.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { getConfig } from './config.js';
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
const platform = process.platform;
|
|
9
|
+
|
|
10
|
+
let robot = null;
|
|
11
|
+
let screenshotDesktop = null;
|
|
12
|
+
|
|
13
|
+
async function loadNativeModules() {
|
|
14
|
+
try {
|
|
15
|
+
robot = (await import('@nut-tree-fork/nut-js')).default || (await import('@nut-tree-fork/nut-js'));
|
|
16
|
+
if (robot.mouse) {
|
|
17
|
+
robot.mouse.config.autoDelayMs = 0;
|
|
18
|
+
robot.mouse.config.mouseSpeed = 2000;
|
|
19
|
+
}
|
|
20
|
+
console.log(chalk.green(' ✓ nut-js loaded (mouse/keyboard control)'));
|
|
21
|
+
} catch {
|
|
22
|
+
try {
|
|
23
|
+
const rjs = await import('robotjs');
|
|
24
|
+
robot = rjs.default || rjs;
|
|
25
|
+
console.log(chalk.green(' ✓ robotjs loaded (mouse/keyboard control)'));
|
|
26
|
+
} catch {
|
|
27
|
+
console.log(chalk.yellow(' ⚠ No mouse/keyboard module found.'));
|
|
28
|
+
console.log(chalk.yellow(' Install: npm install -g @nut-tree-fork/nut-js'));
|
|
29
|
+
robot = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const mod = await import('screenshot-desktop');
|
|
35
|
+
screenshotDesktop = mod.default || mod;
|
|
36
|
+
console.log(chalk.green(' ✓ screenshot-desktop loaded'));
|
|
37
|
+
} catch {
|
|
38
|
+
console.log(chalk.yellow(' ⚠ screenshot-desktop not found.'));
|
|
39
|
+
console.log(chalk.yellow(' Install: npm install -g screenshot-desktop'));
|
|
40
|
+
screenshotDesktop = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function getScreenSize() {
|
|
45
|
+
if (robot?.screen?.width) {
|
|
46
|
+
const w = await robot.screen.width();
|
|
47
|
+
const h = await robot.screen.height();
|
|
48
|
+
return { width: w, height: h };
|
|
49
|
+
}
|
|
50
|
+
if (robot?.getScreenSize) {
|
|
51
|
+
return robot.getScreenSize();
|
|
52
|
+
}
|
|
53
|
+
return { width: 1920, height: 1080 };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function moveMouse(x, y) {
|
|
57
|
+
const screen = await getScreenSize();
|
|
58
|
+
const px = Math.round(x * screen.width);
|
|
59
|
+
const py = Math.round(y * screen.height);
|
|
60
|
+
|
|
61
|
+
if (robot?.mouse?.setPosition) {
|
|
62
|
+
const { Point } = await import('@nut-tree-fork/nut-js');
|
|
63
|
+
await robot.mouse.setPosition(new Point(px, py));
|
|
64
|
+
} else if (robot?.moveMouse) {
|
|
65
|
+
robot.moveMouse(px, py);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function mouseClick(button = 'left', double = false) {
|
|
70
|
+
if (robot?.mouse?.click) {
|
|
71
|
+
const { Button } = await import('@nut-tree-fork/nut-js');
|
|
72
|
+
const btn = button === 'right' ? Button.RIGHT : Button.LEFT;
|
|
73
|
+
await robot.mouse.click(btn);
|
|
74
|
+
if (double) await robot.mouse.click(btn);
|
|
75
|
+
} else if (robot?.mouseClick) {
|
|
76
|
+
robot.mouseClick(button, double);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function mouseDown(button = 'left') {
|
|
81
|
+
if (robot?.mouse?.pressButton) {
|
|
82
|
+
const { Button } = await import('@nut-tree-fork/nut-js');
|
|
83
|
+
await robot.mouse.pressButton(button === 'right' ? Button.RIGHT : Button.LEFT);
|
|
84
|
+
} else if (robot?.mouseToggle) {
|
|
85
|
+
robot.mouseToggle('down', button);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function mouseUp(button = 'left') {
|
|
90
|
+
if (robot?.mouse?.releaseButton) {
|
|
91
|
+
const { Button } = await import('@nut-tree-fork/nut-js');
|
|
92
|
+
await robot.mouse.releaseButton(button === 'right' ? Button.RIGHT : Button.LEFT);
|
|
93
|
+
} else if (robot?.mouseToggle) {
|
|
94
|
+
robot.mouseToggle('up', button);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function mouseScroll(dx, dy) {
|
|
99
|
+
if (robot?.mouse?.scrollDown && robot?.mouse?.scrollUp) {
|
|
100
|
+
const amount = Math.abs(dy);
|
|
101
|
+
if (dy > 0) await robot.mouse.scrollDown(amount);
|
|
102
|
+
else await robot.mouse.scrollUp(amount);
|
|
103
|
+
} else if (robot?.scrollMouse) {
|
|
104
|
+
robot.scrollMouse(dx, dy);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function mouseDrag(fromX, fromY, toX, toY) {
|
|
109
|
+
await moveMouse(fromX, fromY);
|
|
110
|
+
await mouseDown('left');
|
|
111
|
+
await new Promise(r => setTimeout(r, 50));
|
|
112
|
+
await moveMouse(toX, toY);
|
|
113
|
+
await new Promise(r => setTimeout(r, 50));
|
|
114
|
+
await mouseUp('left');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function typeText(text) {
|
|
118
|
+
if (robot?.keyboard?.type) {
|
|
119
|
+
await robot.keyboard.type(text);
|
|
120
|
+
} else if (robot?.typeString) {
|
|
121
|
+
robot.typeString(text);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function pressKey(keys) {
|
|
126
|
+
const keyArr = keys.split('+').map(k => k.trim().toLowerCase());
|
|
127
|
+
|
|
128
|
+
if (robot?.keyboard?.pressKey && robot?.keyboard?.releaseKey) {
|
|
129
|
+
const { Key } = await import('@nut-tree-fork/nut-js');
|
|
130
|
+
const mapped = keyArr.map(k => {
|
|
131
|
+
const map = {
|
|
132
|
+
cmd: Key.LeftSuper, command: Key.LeftSuper, meta: Key.LeftSuper,
|
|
133
|
+
ctrl: Key.LeftControl, control: Key.LeftControl,
|
|
134
|
+
alt: Key.LeftAlt, option: Key.LeftAlt,
|
|
135
|
+
shift: Key.LeftShift,
|
|
136
|
+
enter: Key.Return, return: Key.Return,
|
|
137
|
+
tab: Key.Tab, escape: Key.Escape, esc: Key.Escape,
|
|
138
|
+
space: Key.Space, backspace: Key.Backspace, delete: Key.Delete,
|
|
139
|
+
up: Key.Up, down: Key.Down, left: Key.Left, right: Key.Right,
|
|
140
|
+
home: Key.Home, end: Key.End, pageup: Key.PageUp, pagedown: Key.PageDown,
|
|
141
|
+
f1: Key.F1, f2: Key.F2, f3: Key.F3, f4: Key.F4, f5: Key.F5,
|
|
142
|
+
};
|
|
143
|
+
return map[k] || Key[k.toUpperCase()] || Key[k];
|
|
144
|
+
}).filter(Boolean);
|
|
145
|
+
|
|
146
|
+
for (const k of mapped) await robot.keyboard.pressKey(k);
|
|
147
|
+
for (const k of mapped.reverse()) await robot.keyboard.releaseKey(k);
|
|
148
|
+
} else if (robot?.keyTap) {
|
|
149
|
+
const modifiers = [];
|
|
150
|
+
let mainKey = keyArr[keyArr.length - 1];
|
|
151
|
+
for (let i = 0; i < keyArr.length - 1; i++) {
|
|
152
|
+
const k = keyArr[i];
|
|
153
|
+
if (k === 'cmd' || k === 'command') modifiers.push('command');
|
|
154
|
+
else if (k === 'ctrl' || k === 'control') modifiers.push('control');
|
|
155
|
+
else if (k === 'alt' || k === 'option') modifiers.push('alt');
|
|
156
|
+
else if (k === 'shift') modifiers.push('shift');
|
|
157
|
+
}
|
|
158
|
+
robot.keyTap(mainKey, modifiers);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function takeScreenshot() {
|
|
163
|
+
if (!screenshotDesktop) return null;
|
|
164
|
+
try {
|
|
165
|
+
const buf = await screenshotDesktop({ format: 'png' });
|
|
166
|
+
return buf.toString('base64');
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.error('Screenshot failed:', e.message);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function openApp(appName) {
|
|
174
|
+
const name = appName.toLowerCase();
|
|
175
|
+
try {
|
|
176
|
+
if (platform === 'darwin') {
|
|
177
|
+
const appMap = {
|
|
178
|
+
chrome: 'Google Chrome', safari: 'Safari', firefox: 'Firefox',
|
|
179
|
+
finder: 'Finder', terminal: 'Terminal', code: 'Visual Studio Code',
|
|
180
|
+
slack: 'Slack', spotify: 'Spotify', discord: 'Discord',
|
|
181
|
+
notes: 'Notes', calculator: 'Calculator', music: 'Music',
|
|
182
|
+
};
|
|
183
|
+
const resolved = appMap[name] || appName;
|
|
184
|
+
await execAsync(`open -a "${resolved}"`);
|
|
185
|
+
} else if (platform === 'win32') {
|
|
186
|
+
await execAsync(`start "" "${appName}"`);
|
|
187
|
+
} else {
|
|
188
|
+
await execAsync(`xdg-open "${appName}" || ${appName}`);
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
} catch (e) {
|
|
192
|
+
console.error('App open failed:', e.message);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function handleCommand(msg) {
|
|
198
|
+
try {
|
|
199
|
+
const cmd = JSON.parse(msg);
|
|
200
|
+
switch (cmd.type) {
|
|
201
|
+
case 'mouse_move':
|
|
202
|
+
await moveMouse(cmd.x, cmd.y);
|
|
203
|
+
return { ok: true };
|
|
204
|
+
case 'mouse_click':
|
|
205
|
+
await mouseClick(cmd.button || 'left', cmd.double || false);
|
|
206
|
+
return { ok: true };
|
|
207
|
+
case 'mouse_down':
|
|
208
|
+
await mouseDown(cmd.button || 'left');
|
|
209
|
+
return { ok: true };
|
|
210
|
+
case 'mouse_up':
|
|
211
|
+
await mouseUp(cmd.button || 'left');
|
|
212
|
+
return { ok: true };
|
|
213
|
+
case 'mouse_scroll':
|
|
214
|
+
await mouseScroll(cmd.dx || 0, cmd.dy || 0);
|
|
215
|
+
return { ok: true };
|
|
216
|
+
case 'mouse_drag':
|
|
217
|
+
await mouseDrag(cmd.fromX, cmd.fromY, cmd.toX, cmd.toY);
|
|
218
|
+
return { ok: true };
|
|
219
|
+
case 'key_type':
|
|
220
|
+
await typeText(cmd.text || '');
|
|
221
|
+
return { ok: true };
|
|
222
|
+
case 'key_press':
|
|
223
|
+
await pressKey(cmd.keys || '');
|
|
224
|
+
return { ok: true };
|
|
225
|
+
case 'screenshot':
|
|
226
|
+
const img = await takeScreenshot();
|
|
227
|
+
return { ok: !!img, image: img };
|
|
228
|
+
case 'app_open':
|
|
229
|
+
const opened = await openApp(cmd.name || '');
|
|
230
|
+
return { ok: opened };
|
|
231
|
+
case 'screen_size':
|
|
232
|
+
const size = await getScreenSize();
|
|
233
|
+
return { ok: true, ...size };
|
|
234
|
+
case 'ping':
|
|
235
|
+
return { ok: true, pong: true };
|
|
236
|
+
default:
|
|
237
|
+
return { ok: false, error: `Unknown command: ${cmd.type}` };
|
|
238
|
+
}
|
|
239
|
+
} catch (e) {
|
|
240
|
+
return { ok: false, error: e.message };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function startAgent(port = 9876) {
|
|
245
|
+
console.log('');
|
|
246
|
+
console.log(chalk.bold.cyan(' 🖥️ Uwonbot Agent'));
|
|
247
|
+
console.log(chalk.gray(' ────────────────────────'));
|
|
248
|
+
console.log('');
|
|
249
|
+
|
|
250
|
+
await loadNativeModules();
|
|
251
|
+
console.log('');
|
|
252
|
+
|
|
253
|
+
const config = getConfig();
|
|
254
|
+
const uid = config.get('uid');
|
|
255
|
+
const email = config.get('email');
|
|
256
|
+
|
|
257
|
+
if (!uid) {
|
|
258
|
+
console.log(chalk.red(' ✗ Not logged in. Run: uwonbot login'));
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
console.log(chalk.gray(` User: ${email}`));
|
|
263
|
+
console.log(chalk.gray(` Port: ${port}`));
|
|
264
|
+
console.log('');
|
|
265
|
+
|
|
266
|
+
const wss = new WebSocketServer({ port });
|
|
267
|
+
|
|
268
|
+
wss.on('connection', (ws, req) => {
|
|
269
|
+
const origin = req.headers.origin || 'unknown';
|
|
270
|
+
console.log(chalk.green(` ✓ Client connected from ${origin}`));
|
|
271
|
+
|
|
272
|
+
ws.on('message', async (data) => {
|
|
273
|
+
const result = await handleCommand(data.toString());
|
|
274
|
+
ws.send(JSON.stringify(result));
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
ws.on('close', () => {
|
|
278
|
+
console.log(chalk.yellow(' ○ Client disconnected'));
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
ws.send(JSON.stringify({ type: 'welcome', agent: 'uwonbot', version: '1.0.3', uid }));
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
wss.on('error', (err) => {
|
|
285
|
+
if (err.code === 'EADDRINUSE') {
|
|
286
|
+
console.log(chalk.red(` ✗ Port ${port} is already in use`));
|
|
287
|
+
console.log(chalk.gray(` Try: uwonbot agent --port ${port + 1}`));
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
console.error(chalk.red(' ✗ Server error:'), err.message);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
console.log(chalk.bold.green(` ✓ Agent running on ws://localhost:${port}`));
|
|
294
|
+
console.log(chalk.gray(' Waiting for connections...'));
|
|
295
|
+
console.log('');
|
|
296
|
+
console.log(chalk.gray(' Press Ctrl+C to stop'));
|
|
297
|
+
console.log('');
|
|
298
|
+
|
|
299
|
+
process.on('SIGINT', () => {
|
|
300
|
+
console.log('');
|
|
301
|
+
console.log(chalk.yellow(' Agent stopped.'));
|
|
302
|
+
wss.close();
|
|
303
|
+
process.exit(0);
|
|
304
|
+
});
|
|
305
|
+
}
|
package/src/assistants.js
CHANGED
|
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { getConfig } from './config.js';
|
|
5
|
-
import { fetchAssistants } from './firebase-client.js';
|
|
5
|
+
import { fetchAssistants, setIdToken } from './firebase-client.js';
|
|
6
6
|
|
|
7
7
|
export async function listAssistants() {
|
|
8
8
|
const config = getConfig();
|
|
@@ -11,6 +11,8 @@ export async function listAssistants() {
|
|
|
11
11
|
console.log(chalk.yellow('\n ⚠️ Please log in first: uwonbot login\n'));
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
|
+
const token = config.get('idToken');
|
|
15
|
+
if (token) setIdToken(token);
|
|
14
16
|
|
|
15
17
|
const spinner = ora('Fetching assistants...').start();
|
|
16
18
|
try {
|
|
@@ -56,6 +58,8 @@ export async function selectAssistant() {
|
|
|
56
58
|
console.log(chalk.yellow('\n ⚠️ Please log in first: uwonbot login\n'));
|
|
57
59
|
return null;
|
|
58
60
|
}
|
|
61
|
+
const token = config.get('idToken');
|
|
62
|
+
if (token) setIdToken(token);
|
|
59
63
|
|
|
60
64
|
const spinner = ora('Loading assistants...').start();
|
|
61
65
|
try {
|
package/src/auth.js
CHANGED
|
@@ -50,9 +50,10 @@ export async function loginCommand() {
|
|
|
50
50
|
console.log('');
|
|
51
51
|
} catch (err) {
|
|
52
52
|
spinner.fail(chalk.red('Login failed'));
|
|
53
|
-
|
|
53
|
+
const msg = (err.message || '').toUpperCase();
|
|
54
|
+
if (msg.includes('EMAIL_NOT_FOUND') || msg.includes('USER_NOT_FOUND')) {
|
|
54
55
|
console.log(chalk.yellow(' No account found. Sign up at https://chartapp-653e1.web.app/auth'));
|
|
55
|
-
} else if (
|
|
56
|
+
} else if (msg.includes('INVALID_PASSWORD') || msg.includes('INVALID_LOGIN_CREDENTIALS')) {
|
|
56
57
|
console.log(chalk.yellow(' Invalid password. Try again.'));
|
|
57
58
|
} else {
|
|
58
59
|
console.log(chalk.red(` ${err.message}`));
|
package/src/firebase-client.js
CHANGED
|
@@ -1,53 +1,73 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const API_KEY = 'AIzaSyATfC0Ycfkjhgoe-QeS-Vww5DSdQINSWNU';
|
|
2
|
+
const PROJECT_ID = 'chartapp-653e1';
|
|
3
|
+
const AUTH_URL = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${API_KEY}`;
|
|
4
|
+
const FIRESTORE_URL = `https://firestore.googleapis.com/v1/projects/${PROJECT_ID}/databases/(default)/documents`;
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
apiKey: "AIzaSyATfC0Ycfkjhgoe-QeS-Vww5DSdQINSWNU",
|
|
7
|
-
authDomain: "chartapp-653e1.firebaseapp.com",
|
|
8
|
-
projectId: "chartapp-653e1",
|
|
9
|
-
storageBucket: "chartapp-653e1.firebasestorage.app",
|
|
10
|
-
messagingSenderId: "725204716095",
|
|
11
|
-
appId: "1:725204716095:web:3c179131466ed50716c68a",
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
let app = null;
|
|
15
|
-
let auth = null;
|
|
16
|
-
let db = null;
|
|
17
|
-
|
|
18
|
-
export function getFirebaseApp() {
|
|
19
|
-
if (!app) {
|
|
20
|
-
app = initializeApp(firebaseConfig);
|
|
21
|
-
auth = getAuth(app);
|
|
22
|
-
db = getFirestore(app);
|
|
23
|
-
}
|
|
24
|
-
return { app, auth, db };
|
|
25
|
-
}
|
|
6
|
+
let cachedIdToken = null;
|
|
26
7
|
|
|
27
8
|
export async function loginWithEmail(email, password) {
|
|
28
|
-
const
|
|
29
|
-
|
|
9
|
+
const res = await fetch(AUTH_URL, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: { 'Content-Type': 'application/json' },
|
|
12
|
+
body: JSON.stringify({ email, password, returnSecureToken: true }),
|
|
13
|
+
});
|
|
14
|
+
const data = await res.json();
|
|
15
|
+
if (data.error) {
|
|
16
|
+
throw new Error(data.error.message || 'Login failed');
|
|
17
|
+
}
|
|
18
|
+
cachedIdToken = data.idToken;
|
|
30
19
|
return {
|
|
31
|
-
uid:
|
|
32
|
-
email:
|
|
33
|
-
displayName:
|
|
34
|
-
idToken:
|
|
35
|
-
refreshToken:
|
|
20
|
+
uid: data.localId,
|
|
21
|
+
email: data.email,
|
|
22
|
+
displayName: data.displayName || '',
|
|
23
|
+
idToken: data.idToken,
|
|
24
|
+
refreshToken: data.refreshToken,
|
|
36
25
|
};
|
|
37
26
|
}
|
|
38
27
|
|
|
28
|
+
function parseFirestoreDoc(doc) {
|
|
29
|
+
const fields = doc.fields || {};
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const [key, val] of Object.entries(fields)) {
|
|
32
|
+
if ('stringValue' in val) result[key] = val.stringValue;
|
|
33
|
+
else if ('integerValue' in val) result[key] = Number(val.integerValue);
|
|
34
|
+
else if ('doubleValue' in val) result[key] = val.doubleValue;
|
|
35
|
+
else if ('booleanValue' in val) result[key] = val.booleanValue;
|
|
36
|
+
else if ('nullValue' in val) result[key] = null;
|
|
37
|
+
else if ('arrayValue' in val) {
|
|
38
|
+
result[key] = (val.arrayValue.values || []).map(v =>
|
|
39
|
+
v.stringValue ?? v.integerValue ?? v.doubleValue ?? v.booleanValue ?? null
|
|
40
|
+
);
|
|
41
|
+
} else if ('mapValue' in val) result[key] = parseFirestoreDoc(val.mapValue);
|
|
42
|
+
else if ('timestampValue' in val) result[key] = val.timestampValue;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
export async function fetchAssistants(uid) {
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
if (!cachedIdToken) throw new Error('Not logged in');
|
|
49
|
+
const url = `${FIRESTORE_URL}/users/${uid}/assistants?orderBy=createdAt%20desc`;
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
headers: { 'Authorization': `Bearer ${cachedIdToken}` },
|
|
52
|
+
});
|
|
53
|
+
const data = await res.json();
|
|
54
|
+
if (data.error) throw new Error(data.error.message);
|
|
55
|
+
return (data.documents || []).map(doc => {
|
|
56
|
+
const parts = doc.name.split('/');
|
|
57
|
+
return { id: parts[parts.length - 1], ...parseFirestoreDoc(doc) };
|
|
58
|
+
});
|
|
45
59
|
}
|
|
46
60
|
|
|
47
61
|
export async function fetchAssistant(uid, assistantId) {
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
if (!cachedIdToken) throw new Error('Not logged in');
|
|
63
|
+
const url = `${FIRESTORE_URL}/users/${uid}/assistants/${assistantId}`;
|
|
64
|
+
const res = await fetch(url, {
|
|
65
|
+
headers: { 'Authorization': `Bearer ${cachedIdToken}` },
|
|
66
|
+
});
|
|
67
|
+
const data = await res.json();
|
|
68
|
+
if (data.error) return null;
|
|
69
|
+
const parts = data.name.split('/');
|
|
70
|
+
return { id: parts[parts.length - 1], ...parseFirestoreDoc(data) };
|
|
53
71
|
}
|
|
72
|
+
|
|
73
|
+
export function setIdToken(token) { cachedIdToken = token; }
|