@benzsiangco/jarvis 1.0.2 → 1.1.0

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.
Files changed (53) hide show
  1. package/dist/cli.js +478 -347
  2. package/dist/electron/main.js +160 -0
  3. package/dist/electron/preload.js +19 -0
  4. package/package.json +19 -6
  5. package/skills.md +147 -0
  6. package/src/agents/index.ts +248 -0
  7. package/src/brain/loader.ts +136 -0
  8. package/src/cli.ts +411 -0
  9. package/src/config/index.ts +363 -0
  10. package/src/core/executor.ts +222 -0
  11. package/src/core/plugins.ts +148 -0
  12. package/src/core/types.ts +217 -0
  13. package/src/electron/main.ts +192 -0
  14. package/src/electron/preload.ts +25 -0
  15. package/src/electron/types.d.ts +20 -0
  16. package/src/index.ts +12 -0
  17. package/src/providers/antigravity-loader.ts +233 -0
  18. package/src/providers/antigravity.ts +585 -0
  19. package/src/providers/index.ts +523 -0
  20. package/src/sessions/index.ts +194 -0
  21. package/src/tools/index.ts +436 -0
  22. package/src/tui/index.tsx +784 -0
  23. package/src/utils/auth-prompt.ts +394 -0
  24. package/src/utils/index.ts +180 -0
  25. package/src/utils/native-picker.ts +71 -0
  26. package/src/utils/skills.ts +99 -0
  27. package/src/utils/table-integration-examples.ts +617 -0
  28. package/src/utils/table-utils.ts +401 -0
  29. package/src/web/build-ui.ts +27 -0
  30. package/src/web/server.ts +674 -0
  31. package/src/web/ui/dist/.gitkeep +0 -0
  32. package/src/web/ui/dist/main.css +1 -0
  33. package/src/web/ui/dist/main.js +320 -0
  34. package/src/web/ui/dist/main.js.map +20 -0
  35. package/src/web/ui/index.html +46 -0
  36. package/src/web/ui/src/App.tsx +143 -0
  37. package/src/web/ui/src/Modules/Safety/GuardianModal.tsx +83 -0
  38. package/src/web/ui/src/components/Layout/ContextPanel.tsx +243 -0
  39. package/src/web/ui/src/components/Layout/Header.tsx +91 -0
  40. package/src/web/ui/src/components/Layout/ModelSelector.tsx +235 -0
  41. package/src/web/ui/src/components/Layout/SessionStats.tsx +369 -0
  42. package/src/web/ui/src/components/Layout/Sidebar.tsx +895 -0
  43. package/src/web/ui/src/components/Modules/Chat/ChatStage.tsx +620 -0
  44. package/src/web/ui/src/components/Modules/Chat/MessageItem.tsx +446 -0
  45. package/src/web/ui/src/components/Modules/Editor/CommandInspector.tsx +71 -0
  46. package/src/web/ui/src/components/Modules/Editor/DiffViewer.tsx +83 -0
  47. package/src/web/ui/src/components/Modules/Terminal/TabbedTerminal.tsx +202 -0
  48. package/src/web/ui/src/components/Settings/SettingsModal.tsx +935 -0
  49. package/src/web/ui/src/config/models.ts +70 -0
  50. package/src/web/ui/src/main.tsx +13 -0
  51. package/src/web/ui/src/store/agentStore.ts +41 -0
  52. package/src/web/ui/src/store/uiStore.ts +64 -0
  53. package/src/web/ui/src/types/index.ts +54 -0
@@ -0,0 +1,674 @@
1
+ import { Hono } from 'hono';
2
+ import { streamText } from 'hono/streaming';
3
+ import { cors } from 'hono/cors';
4
+ import { serveStatic } from 'hono/bun';
5
+ import { createBunWebSocket } from 'hono/bun';
6
+ import * as pty from 'node-pty';
7
+ import { readdirSync, statSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { loadConfig, saveGlobalConfig } from '../config/index.js';
10
+ import { initializeProviders, getAvailableModels, getProvidersWithStatus, fetchLocalModels } from '../providers/index.js';
11
+ import { initializeAgents, getAgent, getPrimaryAgents } from '../agents/index.js';
12
+ import { initializeTools } from '../tools/index.js';
13
+ import { createSession, getSession, listSessions, getTodos, addMessage, deleteSession } from '../sessions/index.js';
14
+ import { executeAgent } from '../core/executor.js';
15
+ import { getSkillsPath } from '../utils/skills.js';
16
+ import {
17
+ getActiveAccount,
18
+ getAccountStats,
19
+ startAntigravityLogin,
20
+ prepareAntigravityLogin,
21
+ listAccountsWithStatus,
22
+ removeAccount
23
+ } from '../providers/antigravity.js';
24
+ import { pickDirectory } from '../utils/native-picker.js';
25
+
26
+ const { upgradeWebSocket, websocket } = createBunWebSocket();
27
+ const app = new Hono();
28
+
29
+ app.onError((err, c) => {
30
+ console.error('[SERVER ERROR]', err);
31
+ return c.text('Internal Server Error: ' + err.message, 500);
32
+ });
33
+
34
+ app.use('*', cors());
35
+
36
+ let config = loadConfig();
37
+ let globalWorkdir = process.cwd();
38
+
39
+ // Container mode detection and initialization
40
+ const isContainerMode = () => {
41
+ return process.env.JARVIS_MODE === 'container' ||
42
+ process.env.JARVIS_MODE === 'web' ||
43
+ process.env.DOCKER === 'true' ||
44
+ existsSync('/.dockerenv');
45
+ };
46
+
47
+ const initializeDefaultWorkspace = () => {
48
+ if (!isContainerMode()) {
49
+ console.log('[Server] Running in desktop mode');
50
+ return;
51
+ }
52
+
53
+ console.log('[Server] Container mode detected - initializing default workspace');
54
+
55
+ const workspacePath = process.env.JARVIS_WORKSPACE || '/workspace';
56
+ const cfg = loadConfig();
57
+
58
+ // Check if default workspace already exists
59
+ const hasDefault = cfg.workspaces?.some((w: any) => w.id === 'default');
60
+
61
+ if (!hasDefault) {
62
+ // Create workspace directory if it doesn't exist
63
+ if (!existsSync(workspacePath)) {
64
+ mkdirSync(workspacePath, { recursive: true });
65
+ console.log('[Server] Created workspace directory:', workspacePath);
66
+ }
67
+
68
+ // Create default workspace
69
+ const defaultWorkspace = {
70
+ id: 'default',
71
+ name: 'Container Workspace',
72
+ path: workspacePath,
73
+ icon: 'C'
74
+ };
75
+
76
+ cfg.workspaces = [...(cfg.workspaces || []), defaultWorkspace];
77
+ saveGlobalConfig(cfg);
78
+ globalWorkdir = workspacePath;
79
+
80
+ console.log('[Server] Default workspace initialized:', workspacePath);
81
+ } else {
82
+ console.log('[Server] Default workspace already exists');
83
+ const defaultWs = cfg.workspaces?.find((w: any) => w.id === 'default');
84
+ if (defaultWs) {
85
+ globalWorkdir = defaultWs.path;
86
+ }
87
+ }
88
+ };
89
+
90
+ // Initialize container mode if applicable
91
+ initializeDefaultWorkspace();
92
+
93
+ await initializeProviders(config);
94
+ initializeAgents(config.agent);
95
+ initializeTools();
96
+
97
+ // --- Terminal PTY Service ---
98
+ app.get('/terminal', upgradeWebSocket((c) => {
99
+ return {
100
+ onOpen(_event, ws) {
101
+ const shell = process.platform === 'win32' ? 'powershell.exe' : 'bash';
102
+ const ptyProcess = pty.spawn(shell, [], {
103
+ name: 'xterm-color',
104
+ cols: 80,
105
+ rows: 24,
106
+ cwd: globalWorkdir,
107
+ env: process.env as any,
108
+ });
109
+ ptyProcess.onData((data) => ws.send(data));
110
+ (ws as any).pty = ptyProcess;
111
+ },
112
+ onMessage(event, ws) {
113
+ const ptyProcess = (ws as any).pty;
114
+ if (!ptyProcess) return;
115
+ const data = event.data.toString();
116
+
117
+ if (data.startsWith('{"resize":')) {
118
+ try {
119
+ const { resize } = JSON.parse(data);
120
+ ptyProcess.resize(resize.cols, resize.rows);
121
+ } catch (e) {}
122
+ } else if (data === '{"action":"kill"}') {
123
+ // Send SIGINT to the process group if possible, or just kill the process
124
+ if (process.platform !== 'win32') {
125
+ ptyProcess.kill('SIGINT');
126
+ } else {
127
+ ptyProcess.kill();
128
+ }
129
+ ws.send('\r\n\x1b[31;1m[SYSTEM] Process terminated by user.\x1b[0m\r\n');
130
+ } else {
131
+ // Ensure we send a string to ptyProcess.write
132
+ let input: string;
133
+ if (typeof event.data === 'string') {
134
+ input = event.data;
135
+ } else if (event.data instanceof ArrayBuffer) {
136
+ input = new TextDecoder().decode(event.data);
137
+ } else {
138
+ input = event.data.toString();
139
+ }
140
+ ptyProcess.write(input);
141
+ }
142
+ },
143
+ onClose(_event, ws) {
144
+ const ptyProcess = (ws as any).pty;
145
+ if (ptyProcess) ptyProcess.kill();
146
+ }
147
+ };
148
+ }));
149
+
150
+ // --- Enhanced Management APIs ---
151
+ // --- Workspace Management ---
152
+ app.get('/api/workspaces', (c) => {
153
+ const cfg = loadConfig();
154
+ return c.json({ workspaces: cfg.workspaces || [] });
155
+ });
156
+
157
+ app.post('/api/workspaces', async (c) => {
158
+ try {
159
+ console.log('[API] Starting workspace creation...');
160
+
161
+ // Get folder info from request body (sent by frontend HTML5 picker)
162
+ const body = await c.req.json();
163
+ const { name, path } = body;
164
+
165
+ if (!name || !path) {
166
+ console.log('[API] Missing name or path');
167
+ return c.json({ error: 'Missing name or path' }, 400);
168
+ }
169
+
170
+ console.log('[API] Workspace info received:', { name, path });
171
+
172
+ const cfg = loadConfig();
173
+
174
+ // Check if workspace already exists
175
+ const existing = (cfg.workspaces || []).find((w: any) => w.path === path);
176
+ if (existing) {
177
+ console.log('[API] Workspace already exists');
178
+ return c.json({ error: 'Workspace already exists', workspace: existing }, 409);
179
+ }
180
+
181
+ // Create new workspace
182
+ const newWs = {
183
+ id: `ws_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
184
+ name,
185
+ path,
186
+ icon: (name[0] || 'W').toUpperCase()
187
+ };
188
+
189
+ console.log('[API] Creating workspace:', newWs);
190
+
191
+ // Save to config
192
+ const workspaces = [...(cfg.workspaces || []), newWs];
193
+ saveGlobalConfig({ ...cfg, workspaces });
194
+
195
+ globalWorkdir = path;
196
+
197
+ console.log('[API] Workspace created successfully');
198
+ return c.json(newWs);
199
+
200
+ } catch (e) {
201
+ console.error('[API] Workspace creation failed:', e);
202
+ return c.json({ error: (e as Error).message }, 500);
203
+ }
204
+ });
205
+
206
+ app.delete('/api/workspaces/:id', (c) => {
207
+ const id = c.req.param('id');
208
+ console.log('[API] Deleting workspace:', id);
209
+
210
+ const cfg = loadConfig();
211
+ const before = cfg.workspaces?.length || 0;
212
+ const workspaces = (cfg.workspaces || []).filter((w: any) => w.id !== id);
213
+ const after = workspaces.length;
214
+
215
+ if (before === after) {
216
+ console.log('[API] Workspace not found:', id);
217
+ return c.json({ error: 'Workspace not found' }, 404);
218
+ }
219
+
220
+ saveGlobalConfig({ ...cfg, workspaces });
221
+ console.log('[API] Workspace deleted successfully');
222
+
223
+ return c.json({ success: true });
224
+ });
225
+
226
+ // Clean all workspaces
227
+ app.delete('/api/workspaces', (c) => {
228
+ console.log('[API] Cleaning all workspaces');
229
+
230
+ const cfg = loadConfig();
231
+ saveGlobalConfig({ ...cfg, workspaces: [] });
232
+ console.log('[API] All workspaces cleaned successfully');
233
+
234
+ return c.json({ success: true });
235
+ });
236
+
237
+ // --- Unified Config CRUD ---
238
+ app.post('/api/config/full', async (c) => {
239
+ const payload = await c.req.json();
240
+ const currentConfig = loadConfig();
241
+
242
+ const updatedConfig = {
243
+ ...currentConfig,
244
+ instructions: payload.instructions ?? currentConfig.instructions,
245
+ persona: payload.persona ?? currentConfig.persona,
246
+ permission: payload.permission ?? currentConfig.permission,
247
+ restrictedPaths: payload.restrictedPaths ?? currentConfig.restrictedPaths,
248
+ };
249
+
250
+ saveGlobalConfig(updatedConfig);
251
+ return c.json({ success: true });
252
+ });
253
+
254
+ app.get('/api/fs/list', (c) => {
255
+ const path = c.req.query('path') || globalWorkdir;
256
+ try {
257
+ const entries = readdirSync(path, { withFileTypes: true });
258
+ const items = entries.map(e => ({
259
+ name: e.name,
260
+ path: join(path, e.name),
261
+ isDirectory: e.isDirectory(),
262
+ size: e.isFile() ? statSync(join(path, e.name)).size : 0
263
+ })).sort((a, b) => (a.isDirectory === b.isDirectory ? a.name.localeCompare(b.name) : a.isDirectory ? -1 : 1));
264
+ return c.json({ path, items });
265
+ } catch (e) {
266
+ return c.json({ error: (e as Error).message }, 400);
267
+ }
268
+ });
269
+
270
+ app.get('/api/fs/roots', (c) => {
271
+ if (process.platform === 'win32') {
272
+ const drives = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(d => `${d}:\\`).filter(d => existsSync(d));
273
+ return c.json({ roots: drives });
274
+ }
275
+ return c.json({ roots: ['/'] });
276
+ });
277
+
278
+ app.get('/api/workdir/pick', async (c) => {
279
+ console.log('[SERVER] Triggering native directory picker...');
280
+ const { path: pickedPath, error } = await pickDirectory();
281
+ if (pickedPath) {
282
+ console.log(`[SERVER] Directory picked: ${pickedPath}`);
283
+ globalWorkdir = pickedPath;
284
+ return c.json({ path: pickedPath });
285
+ }
286
+ console.log('[SERVER] Directory selection cancelled or failed:', error);
287
+ return c.json({ error: error || 'Selection cancelled' }, 400);
288
+ });
289
+
290
+ app.post('/api/workdir', async (c) => {
291
+ const { path } = await c.req.json();
292
+ if (path && existsSync(path)) {
293
+ globalWorkdir = path;
294
+ return c.json({ success: true, path: globalWorkdir });
295
+ }
296
+ return c.json({ error: 'Invalid path' }, 400);
297
+ });
298
+
299
+ app.get('/api/skills', (c) => {
300
+ const path = getSkillsPath();
301
+ const content = existsSync(path) ? readFileSync(path, 'utf-8') : '# Skills\n';
302
+ return c.json({ content });
303
+ });
304
+
305
+ app.post('/api/skills', async (c) => {
306
+ const { content } = await c.req.json();
307
+ writeFileSync(getSkillsPath(), content, 'utf-8');
308
+ return c.json({ success: true });
309
+ });
310
+
311
+ // Brain endpoints
312
+ app.get('/api/brain', async (c) => {
313
+ try {
314
+ const { loadBrain, hasBrain } = await import('../brain/loader.js');
315
+
316
+ if (!hasBrain()) {
317
+ return c.json({ content: '', exists: false });
318
+ }
319
+
320
+ const content = loadBrain();
321
+ return c.json({ content, exists: true });
322
+ } catch (e) {
323
+ console.error('[API] Brain load error:', e);
324
+ return c.json({ content: '', exists: false });
325
+ }
326
+ });
327
+
328
+ app.post('/api/brain', async (c) => {
329
+ try {
330
+ const { content } = await c.req.json();
331
+ const { saveBrain } = await import('../brain/loader.js');
332
+
333
+ saveBrain(content);
334
+ console.log('[API] Brain updated');
335
+
336
+ return c.json({ success: true });
337
+ } catch (e) {
338
+ console.error('[API] Brain save error:', e);
339
+ return c.json({ error: (e as Error).message }, 500);
340
+ }
341
+ });
342
+
343
+ app.get('/api/config', (c) => c.json({ ...loadConfig(), workdir: globalWorkdir }));
344
+
345
+ app.get('/api/models', async (c) => {
346
+ try {
347
+ const config = loadConfig();
348
+ const staticModels = getAvailableModels(config);
349
+
350
+ // For now, skip local models fetching to prevent hanging
351
+ // TODO: Fix local models fetching properly
352
+ const allModels = staticModels;
353
+
354
+ return c.json(allModels);
355
+ } catch (e) {
356
+ console.error('[Models API] Error:', (e as Error).message);
357
+ // Return minimal models on error
358
+ return c.json([
359
+ { id: 'gpt-4o', name: 'GPT-4o', provider: 'openai', isAvailable: false },
360
+ { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', provider: 'anthropic', isAvailable: false }
361
+ ]);
362
+ }
363
+ });
364
+
365
+ app.get('/api/sessions', (c) => {
366
+ const allSessions = listSessions();
367
+ const workdir = c.req.query('workdir');
368
+
369
+ let filtered = allSessions;
370
+ if (workdir) {
371
+ filtered = allSessions.filter(s => s.workdir === workdir);
372
+ }
373
+
374
+ // Return flat list, the frontend will handle tree construction for simplicity and flexibility
375
+ return c.json(filtered);
376
+ });
377
+
378
+ app.post('/api/sessions', async (c) => {
379
+ const { agentId, workdir } = await c.req.json();
380
+ const session = createSession(agentId || 'build', undefined, undefined, workdir || globalWorkdir);
381
+
382
+ // Store model metadata for session stats
383
+ const currentModel = config.model || 'gemini-2.0-flash-exp';
384
+ const getProviderFromModel = (modelId: string): string => {
385
+ if (modelId.startsWith('gemini')) return 'google';
386
+ if (modelId.startsWith('claude')) return 'anthropic';
387
+ if (modelId.startsWith('gpt') || modelId.startsWith('o1') || modelId.startsWith('o3')) return 'openai';
388
+ return 'unknown';
389
+ };
390
+
391
+ session.metadata = {
392
+ ...session.metadata,
393
+ modelId: currentModel,
394
+ providerId: getProviderFromModel(currentModel),
395
+ persona: config.persona,
396
+ };
397
+
398
+ return c.json(session);
399
+ });
400
+
401
+ app.get('/api/sessions/:id', (c) => {
402
+ const session = getSession(c.req.param('id'));
403
+ if (!session) return c.json({ error: 'Session not found' }, 404);
404
+ return c.json(session);
405
+ });
406
+
407
+ app.delete('/api/sessions/:id', (c) => {
408
+ const id = c.req.param('id');
409
+ console.log('[API] Deleting session:', id);
410
+
411
+ try {
412
+ const session = getSession(id);
413
+ if (!session) {
414
+ console.log('[API] Session not found:', id);
415
+ return c.json({ error: 'Session not found' }, 404);
416
+ }
417
+
418
+ // Delete the session
419
+ deleteSession(id);
420
+
421
+ console.log('[API] Session deleted successfully');
422
+ return c.json({ success: true });
423
+ } catch (e) {
424
+ console.error('[API] Failed to delete session:', e);
425
+ return c.json({ error: (e as Error).message }, 500);
426
+ }
427
+ });
428
+
429
+
430
+ app.post('/api/config/prompt', async (c) => {
431
+ const { prompt } = await c.req.json();
432
+ const newConfig = { ...loadConfig(), instructions: prompt };
433
+ saveGlobalConfig(newConfig);
434
+ return c.json({ success: true });
435
+ });
436
+
437
+ app.post('/api/config/safety', async (c) => {
438
+ const { permission, restrictedPaths } = await c.req.json();
439
+ const newConfig = { ...loadConfig(), permission, restrictedPaths };
440
+ saveGlobalConfig(newConfig);
441
+ return c.json({ success: true });
442
+ });
443
+
444
+ app.get('/api/auth/status', (c) => c.json({
445
+ providers: getProvidersWithStatus(),
446
+ activeAccount: getActiveAccount(),
447
+ stats: getAccountStats(),
448
+ accounts: listAccountsWithStatus()
449
+ }));
450
+
451
+ app.post('/api/auth/antigravity/login', async (c) => {
452
+ try {
453
+ // Generate URL and start listener in background
454
+ const url = await prepareAntigravityLogin();
455
+ return c.json({ success: true, url, message: 'OAuth flow initiated.' });
456
+ } catch (e) {
457
+ return c.json({ success: false, error: (e as Error).message }, 500);
458
+ }
459
+ });
460
+
461
+ app.post('/api/auth/key', async (c) => {
462
+ const { providerId, apiKey } = await c.req.json();
463
+ const cfg = loadConfig();
464
+
465
+ if (!cfg.provider) cfg.provider = {};
466
+ if (!cfg.provider[providerId]) cfg.provider[providerId] = { id: providerId, models: {} };
467
+
468
+ cfg.provider[providerId]!.apiKey = apiKey;
469
+ saveGlobalConfig(cfg);
470
+
471
+ // Re-initialize providers
472
+ await initializeProviders(cfg);
473
+
474
+ return c.json({ success: true });
475
+ });
476
+
477
+ app.delete('/api/auth/account/:id', async (c) => {
478
+ const id = c.req.param('id');
479
+ const removed = removeAccount(id);
480
+ return c.json({ success: removed });
481
+ });
482
+
483
+ // OAuth configuration endpoints
484
+ app.get('/api/oauth/config', (c) => {
485
+ const cfg = loadConfig();
486
+ const defaultUrl = 'http://localhost:51121/oauth-callback';
487
+
488
+ return c.json({
489
+ callbackUrl: cfg.oauth?.callbackUrl || null,
490
+ callbackHost: cfg.oauth?.callbackHost || '0.0.0.0',
491
+ callbackPort: cfg.oauth?.callbackPort || 51121,
492
+ defaultUrl,
493
+ currentUrl: cfg.oauth?.callbackUrl || defaultUrl,
494
+ });
495
+ });
496
+
497
+ app.post('/api/oauth/config', async (c) => {
498
+ const { callbackUrl, callbackHost, callbackPort } = await c.req.json();
499
+
500
+ // Validate URL format if provided
501
+ if (callbackUrl && callbackUrl.trim()) {
502
+ try {
503
+ new URL(callbackUrl);
504
+ } catch (e) {
505
+ return c.json({ error: 'Invalid callback URL format' }, 400);
506
+ }
507
+ }
508
+
509
+ const cfg = loadConfig();
510
+ const updatedConfig = {
511
+ ...cfg,
512
+ oauth: {
513
+ callbackUrl: callbackUrl?.trim() || undefined,
514
+ callbackHost: callbackHost || '0.0.0.0',
515
+ callbackPort: callbackPort || 51121,
516
+ }
517
+ };
518
+
519
+ saveGlobalConfig(updatedConfig);
520
+ console.log('[API] OAuth config updated:', updatedConfig.oauth);
521
+
522
+ return c.json({ success: true, config: updatedConfig.oauth });
523
+ });
524
+
525
+ app.delete('/api/oauth/config', (c) => {
526
+ const cfg = loadConfig();
527
+ const updatedConfig = { ...cfg };
528
+ delete updatedConfig.oauth;
529
+
530
+ saveGlobalConfig(updatedConfig);
531
+ console.log('[API] OAuth config reset to defaults');
532
+
533
+ return c.json({ success: true });
534
+ });
535
+
536
+ const abortControllers = new Map<string, AbortController>();
537
+
538
+ // --- Core Chat API ---
539
+ app.post('/api/chat/cancel', async (c) => {
540
+ const { sessionId } = await c.req.json();
541
+ const controller = abortControllers.get(sessionId);
542
+ if (controller) {
543
+ controller.abort();
544
+ abortControllers.delete(sessionId);
545
+ return c.json({ success: true });
546
+ }
547
+ return c.json({ error: 'No active process found for this session' }, 404);
548
+ });
549
+
550
+ app.post('/api/chat', async (c) => {
551
+ const { sessionId, message, agentId, modelId, variant, attachments } = await c.req.json();
552
+ let session = getSession(sessionId);
553
+ if (!session) session = createSession(agentId || 'build', undefined, undefined, globalWorkdir);
554
+ const agent = getAgent(agentId || session.agentId || 'build');
555
+
556
+ // Save user message to session
557
+ addMessage(session.id, {
558
+ role: 'user',
559
+ content: message,
560
+ attachments: attachments || []
561
+ });
562
+
563
+ // Format message with attachments for multimodal support
564
+ let messageContent: any = message;
565
+
566
+ if (attachments && attachments.length > 0) {
567
+ // Build content array with text and images
568
+ const contentParts: any[] = [{ type: 'text', text: message }];
569
+
570
+ for (const attachment of attachments) {
571
+ if (attachment.type?.startsWith('image/') && attachment.content) {
572
+ // Extract base64 data from data URL
573
+ const base64Match = attachment.content.match(/^data:image\/[^;]+;base64,(.+)$/);
574
+ if (base64Match) {
575
+ contentParts.push({
576
+ type: 'image',
577
+ image: attachment.content // Use the full data URL
578
+ });
579
+ }
580
+ }
581
+ }
582
+
583
+ // If we have images, use content array format
584
+ if (contentParts.length > 1) {
585
+ messageContent = contentParts;
586
+ }
587
+ }
588
+
589
+ // Cancel any existing request for this session
590
+ if (abortControllers.has(sessionId)) {
591
+ abortControllers.get(sessionId)?.abort();
592
+ }
593
+
594
+ const controller = new AbortController();
595
+ abortControllers.set(sessionId, controller);
596
+
597
+ // Accumulate assistant response
598
+ let accumulatedContent = '';
599
+ let accumulatedThinking = '';
600
+ let accumulatedToolCalls: any[] = [];
601
+
602
+ return streamText(c, async (stream) => {
603
+ try {
604
+ await executeAgent(messageContent, {
605
+ session,
606
+ agent: {
607
+ ...agent!,
608
+ model: modelId || agent!.model || loadConfig().model,
609
+ options: { ...agent!.options, variant }
610
+ },
611
+ signal: controller.signal,
612
+ onText: (text) => {
613
+ accumulatedContent += text;
614
+ stream.write(text);
615
+ },
616
+ onThinking: (thought) => {
617
+ accumulatedThinking += thought;
618
+ stream.write(`<<<THOUGHT>>>${thought}<<<END_THOUGHT>>>`);
619
+ },
620
+ onToolStart: (name, args) => stream.write(`<<<TOOL_START:{"name":"${name}","args":${JSON.stringify(args)}}>>>`),
621
+ onToolEnd: (name, result) => {
622
+ // Track tool calls
623
+ const existingCall = accumulatedToolCalls.find(t => t.name === name && !t.result);
624
+ if (existingCall) {
625
+ existingCall.result = result;
626
+ } else {
627
+ accumulatedToolCalls.push({ name, args: {}, result });
628
+ }
629
+ stream.write(`<<<TOOL_END:{"name":"${name}","result":${JSON.stringify(result)}}>>>`);
630
+ },
631
+ });
632
+
633
+ // Save assistant message to session after streaming completes
634
+ addMessage(session!.id, {
635
+ role: 'assistant',
636
+ content: accumulatedContent,
637
+ thinking: accumulatedThinking || undefined,
638
+ toolCalls: accumulatedToolCalls.length > 0 ? accumulatedToolCalls : undefined
639
+ });
640
+ } catch (error) {
641
+ if ((error as any).name === 'AbortError') {
642
+ stream.write(`\n[SYSTEM] Execution cancelled by user.`);
643
+ } else {
644
+ stream.write(`\nError: ${(error as Error).message}`);
645
+ }
646
+ } finally {
647
+ abortControllers.delete(sessionId);
648
+ }
649
+ });
650
+ });
651
+
652
+ app.get('/', async (c) => {
653
+ try {
654
+ const indexPath = join(import.meta.dir, 'ui', 'index.html');
655
+ if (existsSync(indexPath)) {
656
+ const content = readFileSync(indexPath, 'utf-8');
657
+ return c.html(content);
658
+ }
659
+ return c.text('UI index.html not found at ' + indexPath, 404);
660
+ } catch (e) {
661
+ console.error('[SERVER] Error serving index.html:', (e as Error).message);
662
+ return c.text('Internal Server Error: ' + (e as Error).message, 500);
663
+ }
664
+ });
665
+
666
+ // Static Assets
667
+ const webRoot = join(import.meta.dir, 'ui', 'dist');
668
+ app.use('/*', serveStatic({
669
+ root: webRoot,
670
+ rewriteRequestPath: (path) => (path.includes('?') ? path.split('?')[0] : path) || '/'
671
+ }));
672
+
673
+ export default { port: 1138, fetch: app.fetch, websocket };
674
+ console.log('J.A.R.V.I.S. Multi-Channel Server active on http://0.0.0.0:1138');
File without changes
@@ -0,0 +1 @@
1
+ .xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;z-index:5;top:0}.xterm .xterm-helper-textarea{position:absolute;opacity:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none;border:0;width:0;height:0;margin:0;padding:0;top:0;left:-9999em}.xterm .composition-view{color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1;background:#000}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{overflow-y:scroll;cursor:default;position:absolute;background-color:#000;inset:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;line-height:normal;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;z-index:10;color:#0000;pointer-events:none;inset:0}.xterm .live-region{position:absolute;overflow:hidden;width:1px;height:1px;left:-9999px}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;pointer-events:none;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}