@benzsiangco/jarvis 1.0.0 → 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.
- package/README.md +5 -0
- package/bin/{jarvis.js → jarvis} +1 -1
- package/dist/cli.js +476 -350
- package/dist/electron/main.js +160 -0
- package/dist/electron/preload.js +19 -0
- package/package.json +21 -8
- package/skills.md +147 -0
- package/src/agents/index.ts +248 -0
- package/src/brain/loader.ts +136 -0
- package/src/cli.ts +411 -0
- package/src/config/index.ts +363 -0
- package/src/core/executor.ts +222 -0
- package/src/core/plugins.ts +148 -0
- package/src/core/types.ts +217 -0
- package/src/electron/main.ts +192 -0
- package/src/electron/preload.ts +25 -0
- package/src/electron/types.d.ts +20 -0
- package/src/index.ts +12 -0
- package/src/providers/antigravity-loader.ts +233 -0
- package/src/providers/antigravity.ts +585 -0
- package/src/providers/index.ts +523 -0
- package/src/sessions/index.ts +194 -0
- package/src/tools/index.ts +436 -0
- package/src/tui/index.tsx +784 -0
- package/src/utils/auth-prompt.ts +394 -0
- package/src/utils/index.ts +180 -0
- package/src/utils/native-picker.ts +71 -0
- package/src/utils/skills.ts +99 -0
- package/src/utils/table-integration-examples.ts +617 -0
- package/src/utils/table-utils.ts +401 -0
- package/src/web/build-ui.ts +27 -0
- package/src/web/server.ts +674 -0
- package/src/web/ui/dist/.gitkeep +0 -0
- package/src/web/ui/dist/main.css +1 -0
- package/src/web/ui/dist/main.js +320 -0
- package/src/web/ui/dist/main.js.map +20 -0
- package/src/web/ui/index.html +46 -0
- package/src/web/ui/src/App.tsx +143 -0
- package/src/web/ui/src/Modules/Safety/GuardianModal.tsx +83 -0
- package/src/web/ui/src/components/Layout/ContextPanel.tsx +243 -0
- package/src/web/ui/src/components/Layout/Header.tsx +91 -0
- package/src/web/ui/src/components/Layout/ModelSelector.tsx +235 -0
- package/src/web/ui/src/components/Layout/SessionStats.tsx +369 -0
- package/src/web/ui/src/components/Layout/Sidebar.tsx +895 -0
- package/src/web/ui/src/components/Modules/Chat/ChatStage.tsx +620 -0
- package/src/web/ui/src/components/Modules/Chat/MessageItem.tsx +446 -0
- package/src/web/ui/src/components/Modules/Editor/CommandInspector.tsx +71 -0
- package/src/web/ui/src/components/Modules/Editor/DiffViewer.tsx +83 -0
- package/src/web/ui/src/components/Modules/Terminal/TabbedTerminal.tsx +202 -0
- package/src/web/ui/src/components/Settings/SettingsModal.tsx +935 -0
- package/src/web/ui/src/config/models.ts +70 -0
- package/src/web/ui/src/main.tsx +13 -0
- package/src/web/ui/src/store/agentStore.ts +41 -0
- package/src/web/ui/src/store/uiStore.ts +64 -0
- 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}
|