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.
Files changed (97) hide show
  1. package/README.md +222 -0
  2. package/bin/nebula-notebook.js +33 -0
  3. package/dist/assets/index-C1h_sArD.css +32 -0
  4. package/dist/assets/index-CDSTBon8.js +658 -0
  5. package/dist/favicon.svg +11 -0
  6. package/dist/index.html +73 -0
  7. package/node-server/dist/app.d.ts +5 -0
  8. package/node-server/dist/app.js +38 -0
  9. package/node-server/dist/auth/auth-middleware.d.ts +24 -0
  10. package/node-server/dist/auth/auth-middleware.js +276 -0
  11. package/node-server/dist/auth/auth-service.d.ts +84 -0
  12. package/node-server/dist/auth/auth-service.js +265 -0
  13. package/node-server/dist/auth/index.d.ts +3 -0
  14. package/node-server/dist/auth/index.js +8 -0
  15. package/node-server/dist/cluster/client-registration.d.ts +43 -0
  16. package/node-server/dist/cluster/client-registration.js +217 -0
  17. package/node-server/dist/cluster/cluster-secret.d.ts +11 -0
  18. package/node-server/dist/cluster/cluster-secret.js +90 -0
  19. package/node-server/dist/cluster/kernel-proxy.d.ts +100 -0
  20. package/node-server/dist/cluster/kernel-proxy.js +361 -0
  21. package/node-server/dist/cluster/server-registry.d.ts +109 -0
  22. package/node-server/dist/cluster/server-registry.js +217 -0
  23. package/node-server/dist/config/output-limits.d.ts +7 -0
  24. package/node-server/dist/config/output-limits.js +10 -0
  25. package/node-server/dist/discovery/discovery-service.d.ts +198 -0
  26. package/node-server/dist/discovery/discovery-service.js +811 -0
  27. package/node-server/dist/discovery/index.d.ts +5 -0
  28. package/node-server/dist/discovery/index.js +21 -0
  29. package/node-server/dist/discovery/types.d.ts +48 -0
  30. package/node-server/dist/discovery/types.js +24 -0
  31. package/node-server/dist/fs/fs-service.d.ts +218 -0
  32. package/node-server/dist/fs/fs-service.js +1422 -0
  33. package/node-server/dist/fs/index.d.ts +5 -0
  34. package/node-server/dist/fs/index.js +21 -0
  35. package/node-server/dist/fs/types.d.ts +132 -0
  36. package/node-server/dist/fs/types.js +5 -0
  37. package/node-server/dist/index.d.ts +13 -0
  38. package/node-server/dist/index.js +556 -0
  39. package/node-server/dist/kernel/default-kernel.d.ts +5 -0
  40. package/node-server/dist/kernel/default-kernel.js +138 -0
  41. package/node-server/dist/kernel/index.d.ts +7 -0
  42. package/node-server/dist/kernel/index.js +23 -0
  43. package/node-server/dist/kernel/kernel-service.d.ts +290 -0
  44. package/node-server/dist/kernel/kernel-service.js +1714 -0
  45. package/node-server/dist/kernel/kernelspec.d.ts +29 -0
  46. package/node-server/dist/kernel/kernelspec.js +210 -0
  47. package/node-server/dist/kernel/session-store.d.ts +87 -0
  48. package/node-server/dist/kernel/session-store.js +303 -0
  49. package/node-server/dist/kernel/types.d.ts +143 -0
  50. package/node-server/dist/kernel/types.js +17 -0
  51. package/node-server/dist/llm/index.d.ts +5 -0
  52. package/node-server/dist/llm/index.js +21 -0
  53. package/node-server/dist/llm/llm-service.d.ts +77 -0
  54. package/node-server/dist/llm/llm-service.js +454 -0
  55. package/node-server/dist/llm/types.d.ts +40 -0
  56. package/node-server/dist/llm/types.js +15 -0
  57. package/node-server/dist/notebook/cell-metadata.d.ts +27 -0
  58. package/node-server/dist/notebook/cell-metadata.js +76 -0
  59. package/node-server/dist/notebook/headless-handler.d.ts +127 -0
  60. package/node-server/dist/notebook/headless-handler.js +1530 -0
  61. package/node-server/dist/notebook/notebook-websocket.d.ts +12 -0
  62. package/node-server/dist/notebook/notebook-websocket.js +103 -0
  63. package/node-server/dist/notebook/operation-router.d.ts +115 -0
  64. package/node-server/dist/notebook/operation-router.js +641 -0
  65. package/node-server/dist/notebook/undoRedoManager.d.ts +194 -0
  66. package/node-server/dist/notebook/undoRedoManager.js +558 -0
  67. package/node-server/dist/output/display-data.d.ts +14 -0
  68. package/node-server/dist/output/display-data.js +134 -0
  69. package/node-server/dist/resources/resource-service.d.ts +69 -0
  70. package/node-server/dist/resources/resource-service.js +363 -0
  71. package/node-server/dist/routes/auth.d.ts +5 -0
  72. package/node-server/dist/routes/auth.js +61 -0
  73. package/node-server/dist/routes/cluster.d.ts +7 -0
  74. package/node-server/dist/routes/cluster.js +94 -0
  75. package/node-server/dist/routes/fs.d.ts +7 -0
  76. package/node-server/dist/routes/fs.js +392 -0
  77. package/node-server/dist/routes/kernel.d.ts +13 -0
  78. package/node-server/dist/routes/kernel.js +637 -0
  79. package/node-server/dist/routes/llm.d.ts +8 -0
  80. package/node-server/dist/routes/llm.js +105 -0
  81. package/node-server/dist/routes/notebook.d.ts +10 -0
  82. package/node-server/dist/routes/notebook.js +335 -0
  83. package/node-server/dist/routes/python.d.ts +8 -0
  84. package/node-server/dist/routes/python.js +187 -0
  85. package/node-server/dist/routes/resources.d.ts +7 -0
  86. package/node-server/dist/routes/resources.js +77 -0
  87. package/node-server/dist/scripts/show-auth-qr.d.ts +1 -0
  88. package/node-server/dist/scripts/show-auth-qr.js +81 -0
  89. package/node-server/dist/terminal/pty-manager.d.ts +100 -0
  90. package/node-server/dist/terminal/pty-manager.js +246 -0
  91. package/node-server/dist/terminal/server.d.ts +19 -0
  92. package/node-server/dist/terminal/server.js +254 -0
  93. package/node-server/dist/terminal/types.d.ts +50 -0
  94. package/node-server/dist/terminal/types.js +9 -0
  95. package/node-server/package.json +45 -0
  96. package/package.json +99 -0
  97. 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 };