nebula-notebook 0.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 +222 -0
- package/bin/nebula-notebook.js +33 -0
- package/dist/assets/index-C1h_sArD.css +32 -0
- package/dist/assets/index-CDSTBon8.js +658 -0
- package/dist/favicon.svg +11 -0
- package/dist/index.html +73 -0
- package/node-server/dist/app.d.ts +5 -0
- package/node-server/dist/app.js +38 -0
- package/node-server/dist/auth/auth-middleware.d.ts +24 -0
- package/node-server/dist/auth/auth-middleware.js +276 -0
- package/node-server/dist/auth/auth-service.d.ts +84 -0
- package/node-server/dist/auth/auth-service.js +265 -0
- package/node-server/dist/auth/index.d.ts +3 -0
- package/node-server/dist/auth/index.js +8 -0
- package/node-server/dist/cluster/client-registration.d.ts +43 -0
- package/node-server/dist/cluster/client-registration.js +217 -0
- package/node-server/dist/cluster/cluster-secret.d.ts +11 -0
- package/node-server/dist/cluster/cluster-secret.js +90 -0
- package/node-server/dist/cluster/kernel-proxy.d.ts +100 -0
- package/node-server/dist/cluster/kernel-proxy.js +361 -0
- package/node-server/dist/cluster/server-registry.d.ts +109 -0
- package/node-server/dist/cluster/server-registry.js +217 -0
- package/node-server/dist/config/output-limits.d.ts +7 -0
- package/node-server/dist/config/output-limits.js +10 -0
- package/node-server/dist/discovery/discovery-service.d.ts +198 -0
- package/node-server/dist/discovery/discovery-service.js +811 -0
- package/node-server/dist/discovery/index.d.ts +5 -0
- package/node-server/dist/discovery/index.js +21 -0
- package/node-server/dist/discovery/types.d.ts +48 -0
- package/node-server/dist/discovery/types.js +24 -0
- package/node-server/dist/fs/fs-service.d.ts +218 -0
- package/node-server/dist/fs/fs-service.js +1422 -0
- package/node-server/dist/fs/index.d.ts +5 -0
- package/node-server/dist/fs/index.js +21 -0
- package/node-server/dist/fs/types.d.ts +132 -0
- package/node-server/dist/fs/types.js +5 -0
- package/node-server/dist/index.d.ts +13 -0
- package/node-server/dist/index.js +556 -0
- package/node-server/dist/kernel/default-kernel.d.ts +5 -0
- package/node-server/dist/kernel/default-kernel.js +138 -0
- package/node-server/dist/kernel/index.d.ts +7 -0
- package/node-server/dist/kernel/index.js +23 -0
- package/node-server/dist/kernel/kernel-service.d.ts +290 -0
- package/node-server/dist/kernel/kernel-service.js +1714 -0
- package/node-server/dist/kernel/kernelspec.d.ts +29 -0
- package/node-server/dist/kernel/kernelspec.js +210 -0
- package/node-server/dist/kernel/session-store.d.ts +87 -0
- package/node-server/dist/kernel/session-store.js +303 -0
- package/node-server/dist/kernel/types.d.ts +143 -0
- package/node-server/dist/kernel/types.js +17 -0
- package/node-server/dist/llm/index.d.ts +5 -0
- package/node-server/dist/llm/index.js +21 -0
- package/node-server/dist/llm/llm-service.d.ts +77 -0
- package/node-server/dist/llm/llm-service.js +454 -0
- package/node-server/dist/llm/types.d.ts +40 -0
- package/node-server/dist/llm/types.js +15 -0
- package/node-server/dist/notebook/cell-metadata.d.ts +27 -0
- package/node-server/dist/notebook/cell-metadata.js +76 -0
- package/node-server/dist/notebook/headless-handler.d.ts +127 -0
- package/node-server/dist/notebook/headless-handler.js +1530 -0
- package/node-server/dist/notebook/notebook-websocket.d.ts +12 -0
- package/node-server/dist/notebook/notebook-websocket.js +103 -0
- package/node-server/dist/notebook/operation-router.d.ts +115 -0
- package/node-server/dist/notebook/operation-router.js +641 -0
- package/node-server/dist/notebook/undoRedoManager.d.ts +194 -0
- package/node-server/dist/notebook/undoRedoManager.js +558 -0
- package/node-server/dist/output/display-data.d.ts +14 -0
- package/node-server/dist/output/display-data.js +134 -0
- package/node-server/dist/resources/resource-service.d.ts +69 -0
- package/node-server/dist/resources/resource-service.js +363 -0
- package/node-server/dist/routes/auth.d.ts +5 -0
- package/node-server/dist/routes/auth.js +61 -0
- package/node-server/dist/routes/cluster.d.ts +7 -0
- package/node-server/dist/routes/cluster.js +94 -0
- package/node-server/dist/routes/fs.d.ts +7 -0
- package/node-server/dist/routes/fs.js +392 -0
- package/node-server/dist/routes/kernel.d.ts +13 -0
- package/node-server/dist/routes/kernel.js +637 -0
- package/node-server/dist/routes/llm.d.ts +8 -0
- package/node-server/dist/routes/llm.js +105 -0
- package/node-server/dist/routes/notebook.d.ts +10 -0
- package/node-server/dist/routes/notebook.js +335 -0
- package/node-server/dist/routes/python.d.ts +8 -0
- package/node-server/dist/routes/python.js +187 -0
- package/node-server/dist/routes/resources.d.ts +7 -0
- package/node-server/dist/routes/resources.js +77 -0
- package/node-server/dist/scripts/show-auth-qr.d.ts +1 -0
- package/node-server/dist/scripts/show-auth-qr.js +81 -0
- package/node-server/dist/terminal/pty-manager.d.ts +100 -0
- package/node-server/dist/terminal/pty-manager.js +246 -0
- package/node-server/dist/terminal/server.d.ts +19 -0
- package/node-server/dist/terminal/server.js +254 -0
- package/node-server/dist/terminal/types.d.ts +50 -0
- package/node-server/dist/terminal/types.js +9 -0
- package/node-server/package.json +45 -0
- package/package.json +99 -0
- package/scripts/postinstall.cjs +26 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Kernel API Routes
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.kernelService = void 0;
|
|
40
|
+
exports.setupKernelWebSocket = setupKernelWebSocket;
|
|
41
|
+
exports.default = kernelRoutes;
|
|
42
|
+
const ws_1 = require("ws");
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const os = __importStar(require("os"));
|
|
46
|
+
const kernel_service_1 = require("../kernel/kernel-service");
|
|
47
|
+
const kernelspec_1 = require("../kernel/kernelspec");
|
|
48
|
+
const fs_service_1 = require("../fs/fs-service");
|
|
49
|
+
const operation_router_1 = require("../notebook/operation-router");
|
|
50
|
+
const kernel_proxy_1 = require("../cluster/kernel-proxy");
|
|
51
|
+
const server_registry_1 = require("../cluster/server-registry");
|
|
52
|
+
// Track all WebSocket connections per kernel session for broadcasting
|
|
53
|
+
const sessionWebSockets = new Map();
|
|
54
|
+
// Only send streaming outputs to sockets after they've performed an initial output sync.
|
|
55
|
+
// This prevents a race where live `output` messages arrive before `sync_outputs`, causing
|
|
56
|
+
// the frontend to advance its seq watermark and skip replayed outputs on refresh/reconnect.
|
|
57
|
+
const outputSubscribedSockets = new WeakSet();
|
|
58
|
+
// Shared kernel service instance - exported for use by headless handler
|
|
59
|
+
const kernelService = new kernel_service_1.KernelService();
|
|
60
|
+
exports.kernelService = kernelService;
|
|
61
|
+
// Initialize kernel service on module load
|
|
62
|
+
kernelService.initialize().catch(err => {
|
|
63
|
+
console.error('Failed to initialize kernel service:', err);
|
|
64
|
+
});
|
|
65
|
+
function buildKernelMetadata(kernelName) {
|
|
66
|
+
const spec = (0, kernelspec_1.getKernelSpec)(kernelName);
|
|
67
|
+
return {
|
|
68
|
+
kernelspec: {
|
|
69
|
+
name: kernelName,
|
|
70
|
+
display_name: spec?.displayName || (kernelName === 'python3' ? 'Python 3' : kernelName),
|
|
71
|
+
language: spec?.language || 'python',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function persistNotebookKernelMetadata(filePath, kernelName) {
|
|
76
|
+
// Best-effort: persisting the kernel choice into the notebook's metadata is a
|
|
77
|
+
// secondary side-effect. If it fails (e.g. the notebook file doesn't exist yet,
|
|
78
|
+
// or isn't writable), the kernel session itself is still valid — don't fail the
|
|
79
|
+
// whole request. The choice gets re-persisted on the next save.
|
|
80
|
+
try {
|
|
81
|
+
const result = await fs_service_1.fsService.updateNotebookMetadata(filePath, buildKernelMetadata(kernelName));
|
|
82
|
+
if (!result.success) {
|
|
83
|
+
console.warn(`[Kernel] Could not persist kernel metadata for ${filePath}: ${result.error}`);
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
changed: result.changed,
|
|
88
|
+
mtime: result.mtime,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
console.warn(`[Kernel] Could not persist kernel metadata for ${filePath}:`, err);
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Setup WebSocket handler for kernel execution
|
|
98
|
+
*/
|
|
99
|
+
function setupKernelWebSocket(wss) {
|
|
100
|
+
wss.on('connection', (ws, req) => {
|
|
101
|
+
// Extract session ID from URL path: /api/kernels/{sessionId}/ws
|
|
102
|
+
const url = req.url || '';
|
|
103
|
+
const match = url.match(/\/api\/kernels\/([^/]+)\/ws/);
|
|
104
|
+
if (!match) {
|
|
105
|
+
ws.close(1008, 'Invalid path');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const sessionId = decodeURIComponent(match[1]);
|
|
109
|
+
// Check if this is a proxied session
|
|
110
|
+
if ((0, kernel_proxy_1.isProxiedSession)(sessionId)) {
|
|
111
|
+
console.log(`[Kernel WS] Creating proxy for remote session ${sessionId}`);
|
|
112
|
+
const remoteWs = (0, kernel_proxy_1.createWebSocketProxy)(sessionId, ws);
|
|
113
|
+
if (!remoteWs) {
|
|
114
|
+
ws.send(JSON.stringify({ type: 'error', error: 'Failed to connect to remote server' }));
|
|
115
|
+
ws.close(1008, 'Failed to connect to remote server');
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
ws.on('error', (err) => {
|
|
120
|
+
console.error(`[Kernel WS] WebSocket error for session ${sessionId}:`, err);
|
|
121
|
+
});
|
|
122
|
+
// Validate session exists and register socket before handling any messages.
|
|
123
|
+
// Use a fast session snapshot here to avoid `ps`/memory lookups blocking the
|
|
124
|
+
// initial `sync_outputs` handshake on refresh/reconnect.
|
|
125
|
+
const session = kernelService.getSessionStatusFast(sessionId);
|
|
126
|
+
if (!session) {
|
|
127
|
+
console.log(`[Kernel WS] Session ${sessionId} not found, closing connection`);
|
|
128
|
+
ws.send(JSON.stringify({ type: 'error', error: 'Session not found' }));
|
|
129
|
+
ws.close(1008, 'Session not found');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
console.log(`[Kernel WS] Connected for session ${sessionId} (kernel: ${session.kernelName}, status: ${session.status})`);
|
|
133
|
+
// Track this WebSocket for broadcasting
|
|
134
|
+
if (!sessionWebSockets.has(sessionId)) {
|
|
135
|
+
sessionWebSockets.set(sessionId, new Set());
|
|
136
|
+
}
|
|
137
|
+
sessionWebSockets.get(sessionId).add(ws);
|
|
138
|
+
// Send initial status so frontend knows the kernel state immediately
|
|
139
|
+
const executingCellId = kernelService.getExecutingCellId(sessionId);
|
|
140
|
+
ws.send(JSON.stringify({ type: 'status', status: session.status, ...(executingCellId != null && { cell_id: executingCellId }) }));
|
|
141
|
+
const handleMessage = async (raw) => {
|
|
142
|
+
try {
|
|
143
|
+
const message = JSON.parse(raw);
|
|
144
|
+
if (message.type === 'execute') {
|
|
145
|
+
const code = message.code || '';
|
|
146
|
+
const cellId = message.cell_id || null;
|
|
147
|
+
// Broadcast helper: send to all connected WebSockets for this session
|
|
148
|
+
const broadcast = (msg, options) => {
|
|
149
|
+
const outputsOnly = options?.outputsOnly ?? false;
|
|
150
|
+
const sockets = sessionWebSockets.get(sessionId);
|
|
151
|
+
if (sockets) {
|
|
152
|
+
for (const socket of sockets) {
|
|
153
|
+
if (socket.readyState === ws_1.WebSocket.OPEN) {
|
|
154
|
+
if (outputsOnly && !outputSubscribedSockets.has(socket))
|
|
155
|
+
continue;
|
|
156
|
+
socket.send(msg);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
// Send busy status to all clients
|
|
162
|
+
broadcast(JSON.stringify({ type: 'status', status: 'busy', ...(cellId != null && { cell_id: cellId }) }));
|
|
163
|
+
// Execute code with streaming output broadcast to all clients
|
|
164
|
+
const result = await kernelService.executeCode(sessionId, code, async (output, outputCellId) => {
|
|
165
|
+
broadcast(JSON.stringify({
|
|
166
|
+
type: 'output',
|
|
167
|
+
output,
|
|
168
|
+
cell_id: outputCellId ?? null,
|
|
169
|
+
}), { outputsOnly: true });
|
|
170
|
+
}, undefined, cellId);
|
|
171
|
+
// Broadcast result and idle status to all clients
|
|
172
|
+
broadcast(JSON.stringify({ type: 'result', result }));
|
|
173
|
+
broadcast(JSON.stringify({ type: 'status', status: 'idle' }));
|
|
174
|
+
}
|
|
175
|
+
else if (message.type === 'sync_outputs') {
|
|
176
|
+
// Send complete cell output arrays for all buffered cells (replace semantics)
|
|
177
|
+
const cellOutputs = kernelService.getAllCellOutputs(sessionId);
|
|
178
|
+
const cells = {};
|
|
179
|
+
for (const [cId, outputs] of cellOutputs) {
|
|
180
|
+
cells[cId] = outputs.map(o => ({ ...o }));
|
|
181
|
+
}
|
|
182
|
+
const executingCellId = kernelService.getExecutingCellId(sessionId);
|
|
183
|
+
ws.send(JSON.stringify({ type: 'sync_outputs', cells, executing_cell: executingCellId }));
|
|
184
|
+
// Only start streaming outputs after the client has performed an initial sync.
|
|
185
|
+
outputSubscribedSockets.add(ws);
|
|
186
|
+
}
|
|
187
|
+
else if (message.type === 'complete') {
|
|
188
|
+
const code = message.code || '';
|
|
189
|
+
const cursorPos = message.cursor_pos ?? code.length;
|
|
190
|
+
const result = await kernelService.complete(sessionId, code, cursorPos);
|
|
191
|
+
ws.send(JSON.stringify({ type: 'complete_reply', result }));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
const errMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
196
|
+
console.error('[Kernel WS] Error:', errMessage);
|
|
197
|
+
ws.send(JSON.stringify({ type: 'error', error: errMessage }));
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
ws.on('message', (data) => {
|
|
201
|
+
void handleMessage(data.toString());
|
|
202
|
+
});
|
|
203
|
+
ws.on('close', () => {
|
|
204
|
+
console.log(`[Kernel WS] Disconnected for session ${sessionId}`);
|
|
205
|
+
const sockets = sessionWebSockets.get(sessionId);
|
|
206
|
+
if (sockets) {
|
|
207
|
+
sockets.delete(ws);
|
|
208
|
+
if (sockets.size === 0) {
|
|
209
|
+
sessionWebSockets.delete(sessionId);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async function kernelRoutes(fastify) {
|
|
216
|
+
/**
|
|
217
|
+
* List available kernelspecs
|
|
218
|
+
* Transforms to snake_case to match Python API format expected by frontend
|
|
219
|
+
*/
|
|
220
|
+
fastify.get('/kernels', async (request, reply) => {
|
|
221
|
+
const serverId = request.query.server_id;
|
|
222
|
+
const localServerId = server_registry_1.serverRegistry.getLocalServerId();
|
|
223
|
+
if (serverId && serverId !== localServerId && serverId !== 'local') {
|
|
224
|
+
try {
|
|
225
|
+
const data = await (0, kernel_proxy_1.getRemoteKernels)(serverId);
|
|
226
|
+
return reply.send(data);
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
230
|
+
return reply.code(500).send({ detail: message });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const kernels = kernelService.getAvailableKernels().map(k => ({
|
|
234
|
+
name: k.name,
|
|
235
|
+
display_name: k.displayName,
|
|
236
|
+
language: k.language,
|
|
237
|
+
path: k.path,
|
|
238
|
+
python_path: k.argv?.[0] || null, // First element of argv is typically the Python executable
|
|
239
|
+
}));
|
|
240
|
+
return reply.send({ kernels });
|
|
241
|
+
});
|
|
242
|
+
/**
|
|
243
|
+
* Debug endpoint to show kernel discovery paths and environment
|
|
244
|
+
*/
|
|
245
|
+
fastify.get('/kernels/debug', async (_request, reply) => {
|
|
246
|
+
const searchPaths = (0, kernelspec_1.getKernelSearchPaths)();
|
|
247
|
+
// Check common kernel locations
|
|
248
|
+
const commonPaths = [
|
|
249
|
+
path.join(os.homedir(), '.local', 'share', 'jupyter', 'kernels'),
|
|
250
|
+
'/usr/local/share/jupyter/kernels',
|
|
251
|
+
'/usr/share/jupyter/kernels',
|
|
252
|
+
];
|
|
253
|
+
const condaPrefix = process.env.CONDA_PREFIX;
|
|
254
|
+
if (condaPrefix) {
|
|
255
|
+
commonPaths.push(path.join(condaPrefix, 'share', 'jupyter', 'kernels'));
|
|
256
|
+
}
|
|
257
|
+
const pathStatus = {};
|
|
258
|
+
for (const p of commonPaths) {
|
|
259
|
+
if (fs.existsSync(p)) {
|
|
260
|
+
try {
|
|
261
|
+
const entries = fs.readdirSync(p, { withFileTypes: true });
|
|
262
|
+
const kernels = entries
|
|
263
|
+
.filter(e => e.isDirectory() && fs.existsSync(path.join(p, e.name, 'kernel.json')))
|
|
264
|
+
.map(e => e.name);
|
|
265
|
+
pathStatus[p] = { exists: true, kernels };
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
pathStatus[p] = { exists: true, error: String(err) };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
pathStatus[p] = { exists: false };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return reply.send({
|
|
276
|
+
node_executable: process.execPath,
|
|
277
|
+
jupyter_data_paths: searchPaths,
|
|
278
|
+
common_paths: pathStatus,
|
|
279
|
+
env: {
|
|
280
|
+
JUPYTER_PATH: process.env.JUPYTER_PATH || '(not set)',
|
|
281
|
+
CONDA_PREFIX: process.env.CONDA_PREFIX || '(not set)',
|
|
282
|
+
HOME: process.env.HOME || '(not set)',
|
|
283
|
+
},
|
|
284
|
+
discovered_kernels: kernelService.getAvailableKernels().map(k => ({
|
|
285
|
+
name: k.name,
|
|
286
|
+
display_name: k.displayName,
|
|
287
|
+
language: k.language,
|
|
288
|
+
path: k.path,
|
|
289
|
+
})),
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
/**
|
|
293
|
+
* List all active kernel sessions
|
|
294
|
+
* Transforms to snake_case to match Python API format expected by frontend
|
|
295
|
+
*/
|
|
296
|
+
fastify.get('/kernels/sessions', async (request, reply) => {
|
|
297
|
+
const serverId = request.query.server_id;
|
|
298
|
+
const localServerId = server_registry_1.serverRegistry.getLocalServerId();
|
|
299
|
+
if (serverId && serverId !== localServerId && serverId !== 'local') {
|
|
300
|
+
try {
|
|
301
|
+
const data = await (0, kernel_proxy_1.getRemoteKernelSessions)(serverId);
|
|
302
|
+
const sessions = (data.sessions || []).map((session) => ({
|
|
303
|
+
...session,
|
|
304
|
+
id: (0, kernel_proxy_1.createProxiedSessionId)(serverId, session.id),
|
|
305
|
+
}));
|
|
306
|
+
return reply.send({ sessions });
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
310
|
+
return reply.code(500).send({ detail: message });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const allSessions = await kernelService.getAllSessions();
|
|
314
|
+
const sessions = allSessions.map(s => ({
|
|
315
|
+
id: s.id,
|
|
316
|
+
kernel_name: s.kernelName,
|
|
317
|
+
file_path: s.filePath,
|
|
318
|
+
status: s.status,
|
|
319
|
+
execution_count: s.executionCount,
|
|
320
|
+
memory_mb: s.memoryMb,
|
|
321
|
+
pid: s.pid,
|
|
322
|
+
created_at: s.createdAt,
|
|
323
|
+
}));
|
|
324
|
+
return reply.send({ sessions });
|
|
325
|
+
});
|
|
326
|
+
/**
|
|
327
|
+
* Get dead (orphaned/terminated) kernel sessions that can be cleaned up
|
|
328
|
+
* These are sessions from previous server runs that failed to reattach
|
|
329
|
+
*/
|
|
330
|
+
fastify.get('/kernels/dead', async (request, reply) => {
|
|
331
|
+
const serverId = request.query.server_id;
|
|
332
|
+
const localServerId = server_registry_1.serverRegistry.getLocalServerId();
|
|
333
|
+
if (serverId && serverId !== localServerId && serverId !== 'local') {
|
|
334
|
+
try {
|
|
335
|
+
const data = await (0, kernel_proxy_1.getRemoteDeadKernelSessions)(serverId);
|
|
336
|
+
return reply.send(data);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
340
|
+
return reply.code(500).send({ detail: message });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const deadSessions = kernelService.getDeadSessions();
|
|
344
|
+
const sessions = deadSessions.map(s => ({
|
|
345
|
+
session_id: s.sessionId,
|
|
346
|
+
kernel_name: s.kernelName,
|
|
347
|
+
file_path: s.filePath,
|
|
348
|
+
status: s.status,
|
|
349
|
+
last_heartbeat: s.lastHeartbeat,
|
|
350
|
+
}));
|
|
351
|
+
return reply.send({ sessions });
|
|
352
|
+
});
|
|
353
|
+
/**
|
|
354
|
+
* Cleanup dead kernel sessions
|
|
355
|
+
* If session_ids provided, only those are cleaned up
|
|
356
|
+
* Otherwise all dead sessions are cleaned up
|
|
357
|
+
*/
|
|
358
|
+
fastify.post('/kernels/dead/cleanup', async (request, reply) => {
|
|
359
|
+
try {
|
|
360
|
+
const { session_ids, server_id } = request.body;
|
|
361
|
+
const serverId = server_id;
|
|
362
|
+
const localServerId = server_registry_1.serverRegistry.getLocalServerId();
|
|
363
|
+
if (serverId && serverId !== localServerId && serverId !== 'local') {
|
|
364
|
+
const result = await (0, kernel_proxy_1.cleanupRemoteDeadKernelSessions)(serverId, session_ids);
|
|
365
|
+
return reply.send(result);
|
|
366
|
+
}
|
|
367
|
+
const deleted = await kernelService.cleanupDeadSessions(session_ids);
|
|
368
|
+
return reply.send({ deleted });
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
372
|
+
return reply.code(500).send({ detail: message });
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
/**
|
|
376
|
+
* Start a new kernel session
|
|
377
|
+
* If server_id is provided and not local, starts on remote server
|
|
378
|
+
*/
|
|
379
|
+
fastify.post('/kernels/start', async (request, reply) => {
|
|
380
|
+
try {
|
|
381
|
+
const { kernel_name = 'python3', cwd, file_path, server_id, client_origin } = request.body;
|
|
382
|
+
const localServerId = server_registry_1.serverRegistry.getLocalServerId();
|
|
383
|
+
// Check if we should start on a remote server
|
|
384
|
+
if (server_id && server_id !== localServerId && server_id !== 'local') {
|
|
385
|
+
// Start on remote server
|
|
386
|
+
const result = await (0, kernel_proxy_1.startRemoteKernel)(server_id, kernel_name, file_path);
|
|
387
|
+
let notebookMtime;
|
|
388
|
+
if (file_path) {
|
|
389
|
+
kernelService.saveNotebookKernelPreference(file_path, kernel_name, server_id);
|
|
390
|
+
const metadataResult = await persistNotebookKernelMetadata(file_path, kernel_name);
|
|
391
|
+
notebookMtime = metadataResult.mtime;
|
|
392
|
+
if (client_origin !== 'ui') {
|
|
393
|
+
operation_router_1.operationRouter.notifyKernelChanged(file_path, { kernelName: kernel_name, serverId: server_id });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return reply.send({ session_id: result.sessionId, kernel_name, server_id, mtime: notebookMtime });
|
|
397
|
+
}
|
|
398
|
+
// Start locally
|
|
399
|
+
const sessionId = await kernelService.startKernel({
|
|
400
|
+
kernelName: kernel_name,
|
|
401
|
+
cwd,
|
|
402
|
+
filePath: file_path,
|
|
403
|
+
});
|
|
404
|
+
let notebookMtime;
|
|
405
|
+
if (file_path) {
|
|
406
|
+
kernelService.saveNotebookKernelPreference(file_path, kernel_name, localServerId);
|
|
407
|
+
const metadataResult = await persistNotebookKernelMetadata(file_path, kernel_name);
|
|
408
|
+
notebookMtime = metadataResult.mtime;
|
|
409
|
+
if (client_origin !== 'ui') {
|
|
410
|
+
operation_router_1.operationRouter.notifyKernelChanged(file_path, { kernelName: kernel_name, serverId: localServerId });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return reply.send({ session_id: sessionId, kernel_name, server_id: localServerId, mtime: notebookMtime });
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
417
|
+
return reply.code(500).send({ detail: message });
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
/**
|
|
421
|
+
* Get stored kernel preference for a notebook file.
|
|
422
|
+
*/
|
|
423
|
+
fastify.get('/kernels/preference', async (request, reply) => {
|
|
424
|
+
const filePath = request.query.file_path;
|
|
425
|
+
if (!filePath) {
|
|
426
|
+
return reply.code(400).send({ detail: 'file_path query parameter is required' });
|
|
427
|
+
}
|
|
428
|
+
const preference = kernelService.getNotebookKernelPreference(filePath);
|
|
429
|
+
if (!preference) {
|
|
430
|
+
return reply.send({ kernel_name: null, server_id: null });
|
|
431
|
+
}
|
|
432
|
+
return reply.send({
|
|
433
|
+
kernel_name: preference.kernelName,
|
|
434
|
+
server_id: preference.serverId,
|
|
435
|
+
updated_at: preference.updatedAt,
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
/**
|
|
439
|
+
* Get or create kernel for a notebook file
|
|
440
|
+
* If server_id is provided and not local, starts on remote server
|
|
441
|
+
*/
|
|
442
|
+
fastify.post('/kernels/for-file', async (request, reply) => {
|
|
443
|
+
try {
|
|
444
|
+
const { file_path, kernel_name = 'python3', server_id, client_origin } = request.body;
|
|
445
|
+
if (!file_path) {
|
|
446
|
+
return reply.code(400).send({ detail: 'file_path is required' });
|
|
447
|
+
}
|
|
448
|
+
const normalizedFilePath = kernelService.normalizeNotebookPath(file_path);
|
|
449
|
+
let effectiveKernelName = kernel_name;
|
|
450
|
+
let effectiveServerId = server_id;
|
|
451
|
+
const localServerId = server_registry_1.serverRegistry.getLocalServerId();
|
|
452
|
+
if (!effectiveServerId) {
|
|
453
|
+
const preference = kernelService.getNotebookKernelPreference(normalizedFilePath);
|
|
454
|
+
if (preference?.kernelName) {
|
|
455
|
+
effectiveKernelName = preference.kernelName;
|
|
456
|
+
}
|
|
457
|
+
if (preference?.serverId) {
|
|
458
|
+
const preferredServerId = preference.serverId;
|
|
459
|
+
const isLocalPreference = preferredServerId === localServerId || preferredServerId === 'local';
|
|
460
|
+
const hasServer = !!server_registry_1.serverRegistry.getServer(preferredServerId);
|
|
461
|
+
if (isLocalPreference || hasServer) {
|
|
462
|
+
effectiveServerId = preferredServerId;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// Check if we should start on a remote server
|
|
467
|
+
if (effectiveServerId && effectiveServerId !== localServerId && effectiveServerId !== 'local') {
|
|
468
|
+
// Start on remote server
|
|
469
|
+
const result = await (0, kernel_proxy_1.startRemoteKernel)(effectiveServerId, effectiveKernelName, file_path);
|
|
470
|
+
kernelService.saveNotebookKernelPreference(normalizedFilePath, effectiveKernelName, effectiveServerId);
|
|
471
|
+
const metadataResult = await persistNotebookKernelMetadata(normalizedFilePath, effectiveKernelName);
|
|
472
|
+
if (client_origin !== 'ui') {
|
|
473
|
+
operation_router_1.operationRouter.notifyKernelChanged(normalizedFilePath, {
|
|
474
|
+
kernelName: effectiveKernelName,
|
|
475
|
+
serverId: effectiveServerId,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
return reply.send({
|
|
479
|
+
session_id: result.sessionId,
|
|
480
|
+
kernel_name: effectiveKernelName,
|
|
481
|
+
file_path,
|
|
482
|
+
server_id: effectiveServerId,
|
|
483
|
+
created: result.created ?? false,
|
|
484
|
+
created_at: result.createdAt,
|
|
485
|
+
mtime: metadataResult.mtime,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
// Start locally
|
|
489
|
+
const { sessionId, created } = await kernelService.getOrCreateKernel(normalizedFilePath, effectiveKernelName);
|
|
490
|
+
const sessionInfo = await kernelService.getSessionStatus(sessionId);
|
|
491
|
+
kernelService.saveNotebookKernelPreference(normalizedFilePath, effectiveKernelName, localServerId);
|
|
492
|
+
const metadataResult = await persistNotebookKernelMetadata(normalizedFilePath, effectiveKernelName);
|
|
493
|
+
if (client_origin !== 'ui') {
|
|
494
|
+
operation_router_1.operationRouter.notifyKernelChanged(normalizedFilePath, {
|
|
495
|
+
kernelName: effectiveKernelName,
|
|
496
|
+
serverId: localServerId,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
return reply.send({
|
|
500
|
+
session_id: sessionId,
|
|
501
|
+
kernel_name: effectiveKernelName,
|
|
502
|
+
file_path,
|
|
503
|
+
server_id: localServerId,
|
|
504
|
+
created,
|
|
505
|
+
created_at: sessionInfo?.createdAt,
|
|
506
|
+
mtime: metadataResult.mtime,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
511
|
+
return reply.code(500).send({ detail: message });
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
/**
|
|
515
|
+
* Check if a kernel exists for a file
|
|
516
|
+
*/
|
|
517
|
+
fastify.get('/kernels/for-file', async (request, reply) => {
|
|
518
|
+
const filePath = request.query.file_path;
|
|
519
|
+
if (!filePath) {
|
|
520
|
+
return reply.code(400).send({ detail: 'file_path query parameter is required' });
|
|
521
|
+
}
|
|
522
|
+
// We don't have a direct method, so check sessions
|
|
523
|
+
const sessions = await kernelService.getAllSessions();
|
|
524
|
+
const session = sessions.find(s => s.filePath === filePath);
|
|
525
|
+
if (session) {
|
|
526
|
+
return reply.send({ session_id: session.id, exists: true });
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
return reply.send({ session_id: null, exists: false });
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
/**
|
|
533
|
+
* Stop a kernel session
|
|
534
|
+
*/
|
|
535
|
+
fastify.delete('/kernels/:sessionId', async (request, reply) => {
|
|
536
|
+
try {
|
|
537
|
+
const { sessionId } = request.params;
|
|
538
|
+
// Check if this is a proxied session
|
|
539
|
+
if ((0, kernel_proxy_1.isProxiedSession)(sessionId)) {
|
|
540
|
+
await (0, kernel_proxy_1.shutdownRemoteKernel)(sessionId);
|
|
541
|
+
return reply.send({ status: 'ok' });
|
|
542
|
+
}
|
|
543
|
+
const success = await kernelService.stopKernel(sessionId);
|
|
544
|
+
if (success) {
|
|
545
|
+
return reply.send({ status: 'ok' });
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
return reply.code(404).send({ detail: 'Session not found' });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
553
|
+
return reply.code(500).send({ detail: message });
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
/**
|
|
557
|
+
* Interrupt kernel execution
|
|
558
|
+
*/
|
|
559
|
+
fastify.post('/kernels/:sessionId/interrupt', async (request, reply) => {
|
|
560
|
+
try {
|
|
561
|
+
const { sessionId } = request.params;
|
|
562
|
+
// Check if this is a proxied session
|
|
563
|
+
if ((0, kernel_proxy_1.isProxiedSession)(sessionId)) {
|
|
564
|
+
await (0, kernel_proxy_1.interruptRemoteKernel)(sessionId);
|
|
565
|
+
return reply.send({ status: 'ok' });
|
|
566
|
+
}
|
|
567
|
+
const success = await kernelService.interruptKernel(sessionId);
|
|
568
|
+
if (success) {
|
|
569
|
+
return reply.send({ status: 'ok' });
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
return reply.code(404).send({ detail: 'Session not found' });
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
577
|
+
return reply.code(500).send({ detail: message });
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
/**
|
|
581
|
+
* Restart a kernel
|
|
582
|
+
*/
|
|
583
|
+
fastify.post('/kernels/:sessionId/restart', async (request, reply) => {
|
|
584
|
+
try {
|
|
585
|
+
const { sessionId } = request.params;
|
|
586
|
+
// Check if this is a proxied session
|
|
587
|
+
if ((0, kernel_proxy_1.isProxiedSession)(sessionId)) {
|
|
588
|
+
await (0, kernel_proxy_1.restartRemoteKernel)(sessionId);
|
|
589
|
+
return reply.send({ status: 'ok' });
|
|
590
|
+
}
|
|
591
|
+
const success = await kernelService.restartKernel(sessionId);
|
|
592
|
+
if (success) {
|
|
593
|
+
return reply.send({ status: 'ok' });
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
return reply.code(404).send({ detail: 'Session not found' });
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
catch (err) {
|
|
600
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
601
|
+
return reply.code(500).send({ detail: message });
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
/**
|
|
605
|
+
* Get kernel session status
|
|
606
|
+
*/
|
|
607
|
+
fastify.get('/kernels/:sessionId/status', async (request, reply) => {
|
|
608
|
+
const { sessionId } = request.params;
|
|
609
|
+
// Check if this is a proxied session
|
|
610
|
+
if ((0, kernel_proxy_1.isProxiedSession)(sessionId)) {
|
|
611
|
+
try {
|
|
612
|
+
const remoteStatus = await (0, kernel_proxy_1.getRemoteKernelStatus)(sessionId);
|
|
613
|
+
return reply.send(remoteStatus);
|
|
614
|
+
}
|
|
615
|
+
catch (err) {
|
|
616
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
617
|
+
return reply.code(500).send({ detail: message });
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const status = await kernelService.getSessionStatus(sessionId);
|
|
621
|
+
if (status) {
|
|
622
|
+
// Convert to snake_case for frontend compatibility
|
|
623
|
+
return reply.send({
|
|
624
|
+
id: status.id,
|
|
625
|
+
kernel_name: status.kernelName,
|
|
626
|
+
file_path: status.filePath,
|
|
627
|
+
status: status.status,
|
|
628
|
+
execution_count: status.executionCount,
|
|
629
|
+
memory_mb: status.memoryMb,
|
|
630
|
+
pid: status.pid,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
return reply.code(404).send({ detail: 'Session not found' });
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM API Routes
|
|
3
|
+
*/
|
|
4
|
+
import { FastifyInstance } from 'fastify';
|
|
5
|
+
import { LLMService } from '../llm/llm-service';
|
|
6
|
+
declare const llmService: LLMService;
|
|
7
|
+
export default function llmRoutes(fastify: FastifyInstance): Promise<void>;
|
|
8
|
+
export { llmService };
|