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,1530 @@
1
+ "use strict";
2
+ /**
3
+ * Headless Operation Handler
4
+ *
5
+ * Handles notebook operations when no UI is connected to Nebula.
6
+ * Mirrors useOperationHandler (React) but operates on files instead of UI state.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.HeadlessOperationHandler = void 0;
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const os = __importStar(require("os"));
46
+ const uuid_1 = require("uuid");
47
+ const cell_metadata_1 = require("./cell-metadata");
48
+ const undoRedoManager_1 = require("./undoRedoManager");
49
+ const kernelspec_1 = require("../kernel/kernelspec");
50
+ function copyCellOutput(output) {
51
+ return {
52
+ type: (output.type || 'stdout'),
53
+ content: output.content || '',
54
+ ...(output.mimeBundle ? { mimeBundle: output.mimeBundle } : {}),
55
+ ...(output.metadata ? { metadata: output.metadata } : {}),
56
+ ...(output.preferredMimeType ? { preferredMimeType: output.preferredMimeType } : {}),
57
+ };
58
+ }
59
+ // Output truncation defaults
60
+ const OUTPUT_DEFAULT_MAX_LINES = 100;
61
+ const OUTPUT_DEFAULT_MAX_CHARS = 10000;
62
+ const OUTPUT_DEFAULT_MAX_LINES_ERROR = 200;
63
+ const OUTPUT_DEFAULT_MAX_CHARS_ERROR = 20000;
64
+ class HeadlessOperationHandler {
65
+ fsService;
66
+ kernelService;
67
+ operationRouter;
68
+ undoRedoManager;
69
+ cache = new Map();
70
+ writeLocks = new Map();
71
+ constructor(fsService, operationRouter, kernelService) {
72
+ this.fsService = fsService;
73
+ this.operationRouter = operationRouter || null;
74
+ this.kernelService = kernelService || null;
75
+ this.undoRedoManager = (0, undoRedoManager_1.getUndoRedoManager)(this.fsService);
76
+ }
77
+ /**
78
+ * Get notebook from cache, loading from disk if needed.
79
+ */
80
+ getCachedNotebook(notebookPath) {
81
+ if (!this.cache.has(notebookPath)) {
82
+ const result = this.fsService.getNotebookCells(notebookPath);
83
+ this.cache.set(notebookPath, {
84
+ cells: result.cells || [],
85
+ metadata: result.metadata || {},
86
+ dirty: false,
87
+ });
88
+ }
89
+ return this.cache.get(notebookPath);
90
+ }
91
+ getCells(notebookPath) {
92
+ return this.getCachedNotebook(notebookPath).cells;
93
+ }
94
+ getVisibleOutputs(cell) {
95
+ if (cell.pendingOutputReset) {
96
+ return [];
97
+ }
98
+ return cell.outputs || [];
99
+ }
100
+ saveCells(notebookPath, cells) {
101
+ const notebook = this.getCachedNotebook(notebookPath);
102
+ notebook.cells = cells;
103
+ notebook.dirty = true;
104
+ }
105
+ /**
106
+ * Persist dirty notebooks to disk.
107
+ */
108
+ async flush(notebookPath) {
109
+ const paths = notebookPath ? [notebookPath] : Array.from(this.cache.keys());
110
+ for (const p of paths) {
111
+ const notebook = this.cache.get(p);
112
+ if (notebook?.dirty) {
113
+ await this.asyncPersist(p);
114
+ }
115
+ }
116
+ }
117
+ async asyncPersist(notebookPath) {
118
+ const notebook = this.cache.get(notebookPath);
119
+ if (!notebook)
120
+ return;
121
+ // Keep writing while dirty
122
+ while (notebook.dirty) {
123
+ const cells = JSON.parse(JSON.stringify(notebook.cells));
124
+ notebook.dirty = false;
125
+ const history = this.undoRedoManager.getHistory(notebookPath, cells);
126
+ await this.fsService.saveNotebookBundle(notebookPath, cells, undefined, history);
127
+ }
128
+ }
129
+ schedulePersist(notebookPath) {
130
+ // Schedule async persist
131
+ this.asyncPersist(notebookPath).catch(err => {
132
+ console.error(`[HeadlessHandler] Failed to persist ${notebookPath}:`, err);
133
+ });
134
+ }
135
+ /**
136
+ * Invalidate cache for a notebook.
137
+ */
138
+ invalidate(notebookPath) {
139
+ if (notebookPath) {
140
+ this.cache.delete(notebookPath);
141
+ }
142
+ else {
143
+ this.cache.clear();
144
+ }
145
+ }
146
+ /**
147
+ * Check if notebook has unsaved changes.
148
+ */
149
+ isDirty(notebookPath) {
150
+ return this.cache.get(notebookPath)?.dirty || false;
151
+ }
152
+ /**
153
+ * Check if agent has permission to modify this notebook.
154
+ */
155
+ checkAgentPermission(notebookPath, _operationType) {
156
+ const status = this.fsService.getAgentPermissionStatus(notebookPath);
157
+ if (status.agent_created) {
158
+ return null;
159
+ }
160
+ if (status.agent_permitted) {
161
+ if (!status.has_history) {
162
+ return {
163
+ success: false,
164
+ error: `Agent cannot modify "${notebookPath}": notebook is user-permitted but history is not enabled. Open the notebook in the UI first to enable history tracking, or the agent can create a new notebook.`,
165
+ };
166
+ }
167
+ return null;
168
+ }
169
+ return {
170
+ success: false,
171
+ error: `Agent cannot modify "${notebookPath}": notebook is not agent-permitted. Either open the notebook in Nebula UI and grant agent permission, or the agent can create a new notebook which will be automatically permitted.`,
172
+ };
173
+ }
174
+ /**
175
+ * Apply a notebook operation.
176
+ */
177
+ async applyOperation(operation) {
178
+ const opType = operation.type;
179
+ const notebookPath = operation.notebookPath || '';
180
+ // Read-only operations
181
+ const readOnlyOps = new Set(['readCell', 'readCellOutput', 'searchCells', 'getUpdatesSince', 'startAgentSession', 'endAgentSession']);
182
+ const permissionExemptOps = new Set(['createNotebook', 'readCell', 'readCellOutput', 'searchCells', 'getUpdatesSince', 'startAgentSession', 'endAgentSession']);
183
+ // Check permission for write operations
184
+ if (!permissionExemptOps.has(opType) && notebookPath) {
185
+ const permissionError = this.checkAgentPermission(notebookPath, opType);
186
+ if (permissionError) {
187
+ return permissionError;
188
+ }
189
+ }
190
+ try {
191
+ let result;
192
+ switch (opType) {
193
+ case 'insertCell':
194
+ result = await this.insertCell(operation);
195
+ break;
196
+ case 'deleteCell':
197
+ result = await this.deleteCell(operation);
198
+ break;
199
+ case 'updateContent':
200
+ result = await this.updateContent(operation);
201
+ break;
202
+ case 'updateMetadata':
203
+ result = await this.updateMetadata(operation);
204
+ break;
205
+ case 'moveCell':
206
+ result = await this.moveCell(operation);
207
+ break;
208
+ case 'duplicateCell':
209
+ result = await this.duplicateCell(operation);
210
+ break;
211
+ case 'updateOutputs':
212
+ result = await this.updateOutputs(operation);
213
+ break;
214
+ case 'createNotebook':
215
+ result = await this.createNotebook(operation);
216
+ break;
217
+ case 'readCell':
218
+ result = await this.readCell(operation);
219
+ break;
220
+ case 'readCellOutput':
221
+ result = await this.readCellOutput(operation);
222
+ break;
223
+ case 'clearNotebook':
224
+ result = await this.clearNotebook(operation);
225
+ break;
226
+ case 'deleteCells':
227
+ result = await this.deleteCells(operation);
228
+ break;
229
+ case 'insertCells':
230
+ result = await this.insertCells(operation);
231
+ break;
232
+ case 'searchCells':
233
+ result = await this.searchCells(operation);
234
+ break;
235
+ case 'getUpdatesSince': {
236
+ const sinceTimestamp = operation.sinceTimestamp || 0;
237
+ const updatesSince = this.getUpdatesSince(notebookPath, sinceTimestamp);
238
+ result = {
239
+ success: true,
240
+ updatesSince,
241
+ serverTimestamp: Date.now(),
242
+ };
243
+ break;
244
+ }
245
+ case 'clearOutputs':
246
+ result = await this.clearOutputs(operation);
247
+ break;
248
+ case 'startKernel':
249
+ result = await this.startKernelOp(operation, notebookPath);
250
+ break;
251
+ case 'shutdownKernel':
252
+ result = await this.shutdownKernelOp(notebookPath);
253
+ break;
254
+ case 'restartKernel':
255
+ result = await this.restartKernelOp(notebookPath);
256
+ break;
257
+ case 'interruptKernel':
258
+ result = await this.interruptKernelOp(notebookPath);
259
+ break;
260
+ case 'executeCell':
261
+ result = await this.executeCell(operation, notebookPath);
262
+ break;
263
+ case 'startAgentSession': {
264
+ const agentId = operation.agentId || 'unknown';
265
+ const clientName = operation.clientName;
266
+ const clientVersion = operation.clientVersion;
267
+ if (this.operationRouter) {
268
+ result = this.operationRouter.startAgentSession(notebookPath, agentId, { clientName, clientVersion });
269
+ }
270
+ else {
271
+ result = { success: true, lock: { agentId, clientName, clientVersion, expiresAt: Date.now() + 5 * 60 * 1000, lockedAt: Date.now() } };
272
+ }
273
+ break;
274
+ }
275
+ case 'endAgentSession': {
276
+ const agentId = operation.agentId || 'unknown';
277
+ if (this.operationRouter) {
278
+ result = this.operationRouter.endAgentSession(notebookPath, agentId);
279
+ }
280
+ else {
281
+ result = { success: true };
282
+ }
283
+ break;
284
+ }
285
+ case 'undo':
286
+ result = await this.handleUndo(notebookPath);
287
+ break;
288
+ case 'redo':
289
+ result = await this.handleRedo(notebookPath);
290
+ break;
291
+ default:
292
+ return { success: false, error: `Unknown operation type: ${opType}` };
293
+ }
294
+ // Schedule persist for write operations
295
+ if (result.success && !readOnlyOps.has(opType) && notebookPath) {
296
+ this.schedulePersist(notebookPath);
297
+ }
298
+ return result;
299
+ }
300
+ catch (err) {
301
+ return { success: false, error: String(err) };
302
+ }
303
+ }
304
+ /**
305
+ * Read notebook from cache with optional output truncation.
306
+ */
307
+ async readNotebook(notebookPath, includeOutputs = true, maxLines, maxChars, maxLinesError, maxCharsError) {
308
+ try {
309
+ const notebook = this.getCachedNotebook(notebookPath);
310
+ let cells = notebook.cells;
311
+ if (includeOutputs) {
312
+ const effectiveMaxLines = maxLines ?? OUTPUT_DEFAULT_MAX_LINES;
313
+ const effectiveMaxChars = maxChars ?? OUTPUT_DEFAULT_MAX_CHARS;
314
+ const effectiveMaxLinesError = maxLinesError ?? OUTPUT_DEFAULT_MAX_LINES_ERROR;
315
+ const effectiveMaxCharsError = maxCharsError ?? OUTPUT_DEFAULT_MAX_CHARS_ERROR;
316
+ cells = this.truncateCellOutputs(cells, effectiveMaxLines, effectiveMaxChars, effectiveMaxLinesError, effectiveMaxCharsError);
317
+ }
318
+ else {
319
+ cells = cells.map(cell => ({ ...cell, outputs: [] }));
320
+ }
321
+ return {
322
+ success: true,
323
+ data: {
324
+ path: notebookPath,
325
+ cells,
326
+ metadata: notebook.metadata,
327
+ },
328
+ };
329
+ }
330
+ catch (err) {
331
+ return { success: false, error: String(err) };
332
+ }
333
+ }
334
+ truncateCellOutputs(cells, maxLines, maxChars, maxLinesError, maxCharsError) {
335
+ return cells.map(cell => ({
336
+ ...cell,
337
+ outputs: (cell.outputs || []).map(output => {
338
+ const outputType = output.type || 'stdout';
339
+ const content = output.content || '';
340
+ let result;
341
+ // Skip truncation for binary/image outputs
342
+ if (outputType === 'image' || outputType === 'html' || outputType === 'display_data') {
343
+ result = {
344
+ ...output,
345
+ type: outputType,
346
+ content,
347
+ is_binary: outputType === 'image',
348
+ };
349
+ }
350
+ else {
351
+ // Use separate limits for error outputs
352
+ const linesLimit = outputType === 'error' ? maxLinesError : maxLines;
353
+ const charsLimit = outputType === 'error' ? maxCharsError : maxChars;
354
+ const { truncatedContent, metadata } = this.truncateOutput(content, linesLimit, charsLimit, 0);
355
+ result = {
356
+ ...output,
357
+ type: outputType,
358
+ content: truncatedContent,
359
+ ...metadata,
360
+ };
361
+ }
362
+ // Strip internal-only fields: `id`/`timestamp` are stamped by convertOutputs
363
+ // for the live UI, but the headless/MCP read contract returns clean outputs
364
+ // (see the "should apply output truncation" test).
365
+ const mutable = result;
366
+ delete mutable.id;
367
+ delete mutable.timestamp;
368
+ return result;
369
+ }),
370
+ }));
371
+ }
372
+ truncateOutput(content, maxLines, maxChars, lineOffset = 0) {
373
+ const lines = content.split('\n');
374
+ const totalLines = lines.length;
375
+ const totalChars = content.length;
376
+ const offsetLines = lines.slice(lineOffset);
377
+ const startLine = lineOffset;
378
+ let endLine = startLine;
379
+ let charCount = 0;
380
+ let truncated = false;
381
+ let truncationReason = null;
382
+ const resultLines = [];
383
+ for (let i = 0; i < offsetLines.length; i++) {
384
+ const line = offsetLines[i];
385
+ const newCharCount = charCount + line.length + (i > 0 ? 1 : 0);
386
+ if (i >= maxLines) {
387
+ truncated = true;
388
+ truncationReason = 'lines';
389
+ break;
390
+ }
391
+ if (newCharCount > maxChars && i > 0) {
392
+ truncated = true;
393
+ truncationReason = 'chars';
394
+ break;
395
+ }
396
+ resultLines.push(line);
397
+ charCount = newCharCount;
398
+ endLine = startLine + i + 1;
399
+ }
400
+ const truncatedContent = resultLines.join('\n');
401
+ return {
402
+ truncatedContent,
403
+ metadata: {
404
+ truncated,
405
+ truncation_reason: truncationReason,
406
+ total_lines: totalLines,
407
+ total_chars: totalChars,
408
+ returned_range: {
409
+ start_line: startLine,
410
+ end_line: endLine,
411
+ char_count: truncatedContent.length,
412
+ },
413
+ },
414
+ };
415
+ }
416
+ // Operation implementations
417
+ async insertCell(operation) {
418
+ const notebookPath = operation.notebookPath;
419
+ const index = operation.index;
420
+ const cellData = operation.cell;
421
+ const metadata = cellData.metadata || {};
422
+ const cells = this.getCells(notebookPath);
423
+ let cellId = cellData.id;
424
+ const existingIds = new Set(cells.map(c => c.id));
425
+ const originalId = cellId;
426
+ let idModified = false;
427
+ if (existingIds.has(cellId)) {
428
+ let counter = 2;
429
+ while (existingIds.has(`${originalId}-${counter}`)) {
430
+ counter++;
431
+ }
432
+ cellId = `${originalId}-${counter}`;
433
+ idModified = true;
434
+ }
435
+ const newCell = {
436
+ id: cellId,
437
+ type: cellData.type || 'code',
438
+ content: cellData.content || '',
439
+ outputs: [],
440
+ isExecuting: false,
441
+ executionCount: null,
442
+ scrolled: metadata.scrolled,
443
+ scrolledHeight: metadata.scrolledHeight,
444
+ };
445
+ let actualIndex;
446
+ if (index === -1 || index >= cells.length) {
447
+ cells.push(newCell);
448
+ actualIndex = cells.length - 1;
449
+ }
450
+ else {
451
+ cells.splice(index, 0, newCell);
452
+ actualIndex = index;
453
+ }
454
+ this.saveCells(notebookPath, cells);
455
+ // Record operation for undo/redo
456
+ this.recordUndoableOperation(notebookPath, {
457
+ type: 'insertCell',
458
+ index: actualIndex,
459
+ cell: newCell,
460
+ source: 'mcp'
461
+ });
462
+ return {
463
+ success: true,
464
+ cellId,
465
+ cellIndex: actualIndex,
466
+ idModified,
467
+ requestedId: idModified ? originalId : null,
468
+ totalCells: cells.length,
469
+ };
470
+ }
471
+ async deleteCell(operation) {
472
+ const notebookPath = operation.notebookPath;
473
+ const cellId = operation.cellId;
474
+ const cellIndex = operation.cellIndex;
475
+ const cells = this.getCells(notebookPath);
476
+ let targetIndex = null;
477
+ if (cellId) {
478
+ targetIndex = cells.findIndex(c => c.id === cellId);
479
+ if (targetIndex === -1)
480
+ targetIndex = null;
481
+ }
482
+ else if (cellIndex !== undefined) {
483
+ targetIndex = cellIndex;
484
+ }
485
+ if (targetIndex === null || targetIndex >= cells.length) {
486
+ return { success: false, error: 'Cell not found' };
487
+ }
488
+ // Save the cell for undo before deleting
489
+ const deletedCell = { ...cells[targetIndex] };
490
+ cells.splice(targetIndex, 1);
491
+ this.saveCells(notebookPath, cells);
492
+ // Record operation for undo/redo
493
+ this.recordUndoableOperation(notebookPath, {
494
+ type: 'deleteCell',
495
+ index: targetIndex,
496
+ cell: deletedCell,
497
+ source: 'mcp'
498
+ });
499
+ return {
500
+ success: true,
501
+ cellIndex: targetIndex,
502
+ totalCells: cells.length,
503
+ };
504
+ }
505
+ async updateContent(operation) {
506
+ const notebookPath = operation.notebookPath;
507
+ const cellId = operation.cellId;
508
+ const content = operation.content;
509
+ const cells = this.getCells(notebookPath);
510
+ const targetIndex = cells.findIndex(c => c.id === cellId);
511
+ if (targetIndex === -1) {
512
+ return { success: false, error: `Cell with ID "${cellId}" not found` };
513
+ }
514
+ // Save old content for undo
515
+ const oldContent = cells[targetIndex].content;
516
+ cells[targetIndex].content = content;
517
+ this.saveCells(notebookPath, cells);
518
+ // Record operation for undo/redo (only if content changed)
519
+ if (oldContent !== content) {
520
+ this.recordUndoableOperation(notebookPath, {
521
+ type: 'updateContent',
522
+ cellId,
523
+ oldContent,
524
+ newContent: content,
525
+ source: 'mcp'
526
+ });
527
+ }
528
+ return {
529
+ success: true,
530
+ cellId,
531
+ cellIndex: targetIndex,
532
+ };
533
+ }
534
+ async updateMetadata(operation) {
535
+ const notebookPath = operation.notebookPath;
536
+ const cellId = operation.cellId;
537
+ const changes = operation.changes;
538
+ // Validate all changes
539
+ const errors = [];
540
+ for (const [key, value] of Object.entries(changes)) {
541
+ const validation = (0, cell_metadata_1.validateMetadataValue)(key, value);
542
+ if (!validation.valid) {
543
+ errors.push(validation.error || `Invalid field: ${key}`);
544
+ }
545
+ }
546
+ if (errors.length > 0) {
547
+ return { success: false, error: errors.join('; ') };
548
+ }
549
+ const cells = this.getCells(notebookPath);
550
+ const targetIndex = cells.findIndex(c => c.id === cellId);
551
+ if (targetIndex === -1) {
552
+ return { success: false, error: `Cell with ID "${cellId}" not found` };
553
+ }
554
+ const cell = cells[targetIndex];
555
+ const oldValues = {};
556
+ // Handle ID change specially
557
+ if ('id' in changes) {
558
+ const newId = changes.id;
559
+ const existingIds = new Set(cells.map(c => c.id));
560
+ existingIds.delete(cellId);
561
+ let actualNewId = newId;
562
+ let idModified = false;
563
+ if (existingIds.has(newId)) {
564
+ let counter = 2;
565
+ while (existingIds.has(`${newId}-${counter}`)) {
566
+ counter++;
567
+ }
568
+ actualNewId = `${newId}-${counter}`;
569
+ idModified = true;
570
+ }
571
+ oldValues.id = cellId;
572
+ cell.id = actualNewId;
573
+ }
574
+ // Apply other changes
575
+ for (const [key, value] of Object.entries(changes)) {
576
+ if (key === 'id')
577
+ continue;
578
+ oldValues[key] = cell[key];
579
+ cell[key] = value;
580
+ }
581
+ this.saveCells(notebookPath, cells);
582
+ // Record operation for undo/redo
583
+ const metadataChanges = {};
584
+ for (const [k, v] of Object.entries(changes)) {
585
+ metadataChanges[k] = { old: oldValues[k], new: v };
586
+ }
587
+ this.recordUndoableOperation(notebookPath, {
588
+ type: 'updateMetadata',
589
+ cellId: cell.id,
590
+ changes: metadataChanges,
591
+ source: 'mcp'
592
+ });
593
+ return {
594
+ success: true,
595
+ cellId: cell.id,
596
+ cellIndex: targetIndex,
597
+ changes: Object.fromEntries(Object.entries(changes).map(([k, v]) => [k, { old: oldValues[k], new: v }])),
598
+ };
599
+ }
600
+ async moveCell(operation) {
601
+ const notebookPath = operation.notebookPath;
602
+ const cellId = operation.cellId;
603
+ let fromIndex = operation.fromIndex;
604
+ let toIndex = operation.toIndex;
605
+ const afterCellId = operation.afterCellId;
606
+ const cells = this.getCells(notebookPath);
607
+ // Determine source
608
+ if (cellId) {
609
+ fromIndex = cells.findIndex(c => c.id === cellId);
610
+ if (fromIndex === -1) {
611
+ return { success: false, error: `Cell with ID "${cellId}" not found` };
612
+ }
613
+ }
614
+ else if (fromIndex === undefined) {
615
+ return { success: false, error: 'Must provide cellId or fromIndex' };
616
+ }
617
+ if (fromIndex < 0 || fromIndex >= cells.length) {
618
+ return { success: false, error: 'Invalid fromIndex' };
619
+ }
620
+ // Determine target
621
+ if (afterCellId) {
622
+ const afterIndex = cells.findIndex(c => c.id === afterCellId);
623
+ if (afterIndex === -1) {
624
+ return { success: false, error: `Cell with ID "${afterCellId}" not found` };
625
+ }
626
+ toIndex = afterIndex + 1;
627
+ if (fromIndex < toIndex) {
628
+ toIndex--;
629
+ }
630
+ }
631
+ else if (toIndex === -1) {
632
+ toIndex = 0;
633
+ }
634
+ else if (toIndex === undefined) {
635
+ return { success: false, error: 'Must provide afterCellId or toIndex' };
636
+ }
637
+ if (toIndex < 0 || toIndex >= cells.length) {
638
+ return { success: false, error: 'Invalid toIndex' };
639
+ }
640
+ // Perform move
641
+ const [cell] = cells.splice(fromIndex, 1);
642
+ cells.splice(toIndex, 0, cell);
643
+ this.saveCells(notebookPath, cells);
644
+ // Record operation for undo/redo
645
+ this.recordUndoableOperation(notebookPath, {
646
+ type: 'moveCell',
647
+ fromIndex,
648
+ toIndex,
649
+ source: 'mcp'
650
+ });
651
+ return {
652
+ success: true,
653
+ cellId: cell.id,
654
+ fromIndex,
655
+ toIndex,
656
+ };
657
+ }
658
+ async duplicateCell(operation) {
659
+ const notebookPath = operation.notebookPath;
660
+ const cellIndex = operation.cellIndex;
661
+ const newCellId = operation.newCellId;
662
+ const cells = this.getCells(notebookPath);
663
+ if (cellIndex < 0 || cellIndex >= cells.length) {
664
+ return { success: false, error: 'Invalid cellIndex' };
665
+ }
666
+ const originalCell = cells[cellIndex];
667
+ const existingIds = new Set(cells.map(c => c.id));
668
+ let actualId = newCellId;
669
+ let idModified = false;
670
+ if (existingIds.has(newCellId)) {
671
+ let counter = 2;
672
+ while (existingIds.has(`${newCellId}-${counter}`)) {
673
+ counter++;
674
+ }
675
+ actualId = `${newCellId}-${counter}`;
676
+ idModified = true;
677
+ }
678
+ const newCell = {
679
+ id: actualId,
680
+ type: originalCell.type,
681
+ content: originalCell.content,
682
+ outputs: [],
683
+ isExecuting: false,
684
+ executionCount: null,
685
+ };
686
+ cells.splice(cellIndex + 1, 0, newCell);
687
+ this.saveCells(notebookPath, cells);
688
+ return {
689
+ success: true,
690
+ cellId: actualId,
691
+ cellIndex: cellIndex + 1,
692
+ idModified,
693
+ totalCells: cells.length,
694
+ };
695
+ }
696
+ async updateOutputs(operation) {
697
+ const notebookPath = operation.notebookPath;
698
+ const cellId = operation.cellId;
699
+ const outputs = operation.outputs;
700
+ const executionCount = operation.executionCount;
701
+ const cells = this.getCells(notebookPath);
702
+ const targetIndex = cells.findIndex(c => c.id === cellId);
703
+ if (targetIndex === -1) {
704
+ return { success: false, error: `Cell with ID "${cellId}" not found` };
705
+ }
706
+ cells[targetIndex].outputs = outputs.map(o => copyCellOutput(o));
707
+ if (executionCount !== undefined) {
708
+ cells[targetIndex].executionCount = executionCount;
709
+ }
710
+ this.saveCells(notebookPath, cells);
711
+ return {
712
+ success: true,
713
+ cellId,
714
+ cellIndex: targetIndex,
715
+ };
716
+ }
717
+ async createNotebook(operation) {
718
+ const notebookPath = operation.notebookPath;
719
+ const overwrite = operation.overwrite || false;
720
+ const kernelName = operation.kernelName || 'python3';
721
+ const kernelDisplayName = operation.kernelDisplayName || 'Python 3';
722
+ const normalizedPath = this.fsService.normalizePath(notebookPath);
723
+ if (fs.existsSync(normalizedPath) && !overwrite) {
724
+ return {
725
+ success: false,
726
+ error: `Notebook already exists: ${notebookPath}. Use overwrite=true to replace.`,
727
+ };
728
+ }
729
+ const notebook = {
730
+ nbformat: 4,
731
+ nbformat_minor: 5,
732
+ metadata: {
733
+ kernelspec: {
734
+ name: kernelName,
735
+ display_name: kernelDisplayName,
736
+ },
737
+ language_info: {
738
+ name: 'python',
739
+ },
740
+ nebula: {
741
+ agent_created: true,
742
+ agent_permitted: true,
743
+ },
744
+ },
745
+ cells: [],
746
+ };
747
+ const dir = path.dirname(normalizedPath);
748
+ if (!fs.existsSync(dir)) {
749
+ fs.mkdirSync(dir, { recursive: true });
750
+ }
751
+ fs.writeFileSync(normalizedPath, JSON.stringify(notebook, null, 2), 'utf-8');
752
+ this.invalidate(notebookPath);
753
+ const mtime = fs.statSync(normalizedPath).mtimeMs / 1000; // Convert to seconds like Python
754
+ return {
755
+ success: true,
756
+ path: notebookPath,
757
+ mtime,
758
+ };
759
+ }
760
+ async readCell(operation) {
761
+ const notebookPath = operation.notebookPath;
762
+ const cellId = operation.cellId;
763
+ const cellIndex = operation.cellIndex;
764
+ const cells = this.getCells(notebookPath);
765
+ let targetIndex = null;
766
+ let cell = null;
767
+ if (cellId) {
768
+ targetIndex = cells.findIndex(c => c.id === cellId);
769
+ if (targetIndex === -1) {
770
+ return { success: false, error: `Cell with ID "${cellId}" not found` };
771
+ }
772
+ cell = cells[targetIndex];
773
+ }
774
+ else if (cellIndex !== undefined) {
775
+ if (cellIndex < 0 || cellIndex >= cells.length) {
776
+ return { success: false, error: `Cell index ${cellIndex} out of range` };
777
+ }
778
+ targetIndex = cellIndex;
779
+ cell = cells[cellIndex];
780
+ }
781
+ else {
782
+ return { success: false, error: 'Must provide cellId or cellIndex' };
783
+ }
784
+ return {
785
+ success: true,
786
+ cellId: cell.id,
787
+ cellIndex: targetIndex,
788
+ cell: {
789
+ id: cell.id,
790
+ type: cell.type,
791
+ content: cell.content || '',
792
+ outputs: (cell.outputs || []).map(o => copyCellOutput(o)),
793
+ executionCount: cell.executionCount,
794
+ metadata: {
795
+ scrolled: cell.scrolled,
796
+ scrolledHeight: cell.scrolledHeight,
797
+ },
798
+ },
799
+ };
800
+ }
801
+ async readCellOutput(operation) {
802
+ const notebookPath = operation.notebookPath;
803
+ const cellId = operation.cellId ?? operation.cell_id;
804
+ const cellIndex = operation.cellIndex ?? operation.cell_index;
805
+ const maxLines = operation.max_lines ?? OUTPUT_DEFAULT_MAX_LINES;
806
+ const maxChars = operation.max_chars ?? OUTPUT_DEFAULT_MAX_CHARS;
807
+ const maxLinesError = operation.max_lines_error ?? OUTPUT_DEFAULT_MAX_LINES_ERROR;
808
+ const maxCharsError = operation.max_chars_error ?? OUTPUT_DEFAULT_MAX_CHARS_ERROR;
809
+ const lineOffset = operation.line_offset ?? 0;
810
+ const saveToFile = operation.save_to_file ?? false;
811
+ const maxWait = (operation.maxWait ?? operation.max_wait ?? 0); // seconds
812
+ let cells = this.getCells(notebookPath);
813
+ let targetIndex = null;
814
+ let cell = null;
815
+ let targetCellId = null;
816
+ if (cellId) {
817
+ targetIndex = cells.findIndex(c => c.id === cellId);
818
+ if (targetIndex === -1) {
819
+ return { success: false, error: `Cell with ID "${cellId}" not found` };
820
+ }
821
+ cell = cells[targetIndex];
822
+ targetCellId = cell.id;
823
+ }
824
+ else if (cellIndex !== undefined) {
825
+ if (cellIndex < 0 || cellIndex >= cells.length) {
826
+ return { success: false, error: `Cell index ${cellIndex} out of range` };
827
+ }
828
+ targetIndex = cellIndex;
829
+ cell = cells[cellIndex];
830
+ targetCellId = cell.id;
831
+ }
832
+ else {
833
+ return { success: false, error: 'Must provide cellId or cellIndex' };
834
+ }
835
+ // Poll for new outputs if maxWait > 0
836
+ if (maxWait > 0) {
837
+ let visibleOutputs = this.getVisibleOutputs(cell);
838
+ let baselineOutputCount = visibleOutputs.length;
839
+ let baselineOutputChars = visibleOutputs.reduce((sum, o) => sum + (o.content?.length || 0), 0);
840
+ let baselineExecutionCount = cell.executionCount;
841
+ let wasExecuting = !!cell.isExecuting;
842
+ const startTime = Date.now();
843
+ const pollInterval = 500; // Poll every 500ms like Python
844
+ while ((Date.now() - startTime) < maxWait * 1000) {
845
+ await this.sleep(pollInterval);
846
+ // Re-read cells from cache to detect new outputs
847
+ cells = this.getCells(notebookPath);
848
+ if (targetCellId) {
849
+ targetIndex = cells.findIndex(c => c.id === targetCellId);
850
+ if (targetIndex === -1) {
851
+ return { success: false, error: `Cell with ID "${targetCellId}" not found` };
852
+ }
853
+ }
854
+ cell = cells[targetIndex];
855
+ if (!cell) {
856
+ return { success: false, error: 'Cell not found' };
857
+ }
858
+ // If execution starts after we began polling (e.g. queued), reset the baseline so we
859
+ // wait for outputs from this run rather than comparing against previous outputs.
860
+ if (!wasExecuting && cell.isExecuting) {
861
+ wasExecuting = true;
862
+ visibleOutputs = this.getVisibleOutputs(cell);
863
+ baselineOutputCount = visibleOutputs.length;
864
+ baselineOutputChars = visibleOutputs.reduce((sum, o) => sum + (o.content?.length || 0), 0);
865
+ baselineExecutionCount = cell.executionCount;
866
+ }
867
+ visibleOutputs = this.getVisibleOutputs(cell);
868
+ const currentOutputCount = visibleOutputs.length;
869
+ const currentOutputChars = visibleOutputs.reduce((sum, o) => sum + (o.content?.length || 0), 0);
870
+ const executionCountChanged = cell.executionCount !== baselineExecutionCount;
871
+ // Check if outputs changed (more outputs or more content)
872
+ if (currentOutputCount > baselineOutputCount || currentOutputChars > baselineOutputChars || executionCountChanged) {
873
+ break; // New output arrived
874
+ }
875
+ // If the cell finished executing but produced no additional output, stop waiting.
876
+ if (wasExecuting && !cell.isExecuting) {
877
+ break;
878
+ }
879
+ // If the cell is already idle, there is no in-flight execution to wait on.
880
+ if (!wasExecuting && !cell.isExecuting) {
881
+ break;
882
+ }
883
+ }
884
+ }
885
+ const processedOutputs = [];
886
+ const tempFiles = [];
887
+ for (const output of this.getVisibleOutputs(cell)) {
888
+ const outputType = output.type || 'stdout';
889
+ const content = output.content || '';
890
+ // Images are returned as-is
891
+ if (outputType === 'image') {
892
+ processedOutputs.push({
893
+ type: outputType,
894
+ content,
895
+ truncated: false,
896
+ is_binary: true,
897
+ });
898
+ continue;
899
+ }
900
+ // Save to temp file if requested
901
+ let tempFilePath;
902
+ if (saveToFile && content) {
903
+ tempFilePath = this.saveOutputToTempFile(content, cell.id);
904
+ tempFiles.push(tempFilePath);
905
+ }
906
+ // Use separate limits for errors
907
+ const linesLimit = outputType === 'error' ? maxLinesError : maxLines;
908
+ const charsLimit = outputType === 'error' ? maxCharsError : maxChars;
909
+ const { truncatedContent, metadata } = this.truncateOutput(content, linesLimit, charsLimit, lineOffset);
910
+ const processedOutput = {
911
+ type: outputType,
912
+ content: truncatedContent,
913
+ ...metadata,
914
+ };
915
+ if (tempFilePath) {
916
+ processedOutput.temp_file = tempFilePath;
917
+ processedOutput.temp_file_size = content.length;
918
+ }
919
+ processedOutputs.push(processedOutput);
920
+ }
921
+ return {
922
+ success: true,
923
+ cellId: cell.id,
924
+ cellIndex: targetIndex,
925
+ outputs: processedOutputs,
926
+ executionCount: cell.executionCount,
927
+ executionStatus: cell.isExecuting ? 'busy' : 'idle',
928
+ output_count: processedOutputs.length,
929
+ temp_files: tempFiles.length > 0 ? tempFiles : null,
930
+ };
931
+ }
932
+ sleep(ms) {
933
+ return new Promise(resolve => setTimeout(resolve, ms));
934
+ }
935
+ saveOutputToTempFile(content, cellId) {
936
+ const tempDir = path.join(os.tmpdir(), 'nebula', 'outputs');
937
+ if (!fs.existsSync(tempDir)) {
938
+ fs.mkdirSync(tempDir, { recursive: true });
939
+ }
940
+ const filename = `cell_output_${cellId}_${(0, uuid_1.v4)().slice(0, 8)}.txt`;
941
+ const filepath = path.join(tempDir, filename);
942
+ fs.writeFileSync(filepath, content, 'utf-8');
943
+ return filepath;
944
+ }
945
+ async clearNotebook(operation) {
946
+ const notebookPath = operation.notebookPath;
947
+ const cells = this.getCells(notebookPath);
948
+ const deletedCount = cells.length;
949
+ if (deletedCount === 0) {
950
+ return { success: true, deletedCount: 0 };
951
+ }
952
+ // Delete from end to start to avoid index shifting issues
953
+ for (let i = cells.length - 1; i >= 0; i--) {
954
+ const deletedCell = { ...cells[i] };
955
+ cells.splice(i, 1);
956
+ this.saveCells(notebookPath, cells);
957
+ this.recordUndoableOperation(notebookPath, {
958
+ type: 'deleteCell',
959
+ index: i,
960
+ cell: deletedCell,
961
+ source: 'mcp'
962
+ });
963
+ }
964
+ return {
965
+ success: true,
966
+ deletedCount,
967
+ };
968
+ }
969
+ async deleteCells(operation) {
970
+ const notebookPath = operation.notebookPath;
971
+ const cellIds = operation.cellIds || [];
972
+ if (cellIds.length === 0) {
973
+ return { success: false, error: 'No cell IDs provided' };
974
+ }
975
+ const cells = this.getCells(notebookPath);
976
+ const deletedIds = [];
977
+ const notFound = [];
978
+ const indicesToDelete = [];
979
+ for (const cellId of cellIds) {
980
+ const idx = cells.findIndex(c => c.id === cellId);
981
+ if (idx !== -1) {
982
+ indicesToDelete.push(idx);
983
+ deletedIds.push(cellId);
984
+ }
985
+ else {
986
+ notFound.push(cellId);
987
+ }
988
+ }
989
+ // Delete in reverse order
990
+ for (const idx of indicesToDelete.sort((a, b) => b - a)) {
991
+ cells.splice(idx, 1);
992
+ }
993
+ this.saveCells(notebookPath, cells);
994
+ return {
995
+ success: true,
996
+ deletedCount: deletedIds.length,
997
+ deletedIds,
998
+ notFound: notFound.length > 0 ? notFound : null,
999
+ totalCells: cells.length,
1000
+ };
1001
+ }
1002
+ async insertCells(operation) {
1003
+ const notebookPath = operation.notebookPath;
1004
+ const position = operation.position ?? -1;
1005
+ const newCells = operation.cells || [];
1006
+ if (newCells.length === 0) {
1007
+ return { success: false, error: 'No cells provided' };
1008
+ }
1009
+ const cells = this.getCells(notebookPath);
1010
+ const insertedCells = [];
1011
+ for (let i = 0; i < newCells.length; i++) {
1012
+ const cellData = newCells[i];
1013
+ const cellId = cellData.id || `cell-${cells.length + i}-${Date.now()}`;
1014
+ insertedCells.push({
1015
+ id: cellId,
1016
+ type: cellData.type || 'code',
1017
+ content: cellData.content || '',
1018
+ outputs: cellData.outputs || [],
1019
+ isExecuting: false,
1020
+ executionCount: cellData.executionCount ?? null,
1021
+ });
1022
+ }
1023
+ let insertIndex;
1024
+ if (position < 0 || position >= cells.length) {
1025
+ insertIndex = cells.length;
1026
+ cells.push(...insertedCells);
1027
+ }
1028
+ else {
1029
+ insertIndex = position;
1030
+ cells.splice(position, 0, ...insertedCells);
1031
+ }
1032
+ this.saveCells(notebookPath, cells);
1033
+ return {
1034
+ success: true,
1035
+ insertedCount: insertedCells.length,
1036
+ insertedIds: insertedCells.map(c => c.id),
1037
+ startIndex: insertIndex,
1038
+ totalCells: cells.length,
1039
+ };
1040
+ }
1041
+ async searchCells(operation) {
1042
+ const notebookPath = operation.notebookPath;
1043
+ const query = operation.query || '';
1044
+ const includeOutputs = operation.includeOutputs ?? false;
1045
+ const limit = operation.limit ?? 10;
1046
+ if (!query) {
1047
+ return { success: false, error: 'No search query provided' };
1048
+ }
1049
+ const cells = this.getCells(notebookPath);
1050
+ const queryLower = query.toLowerCase();
1051
+ const matches = [];
1052
+ for (let i = 0; i < cells.length && matches.length < limit; i++) {
1053
+ const cell = cells[i];
1054
+ const content = cell.content || '';
1055
+ // Search in source
1056
+ if (content.toLowerCase().includes(queryLower)) {
1057
+ const lines = content.split('\n');
1058
+ let matchLine = null;
1059
+ for (let j = 0; j < lines.length; j++) {
1060
+ if (lines[j].toLowerCase().includes(queryLower)) {
1061
+ matchLine = j;
1062
+ break;
1063
+ }
1064
+ }
1065
+ matches.push({
1066
+ cellId: cell.id,
1067
+ cellIndex: i,
1068
+ matchLocation: 'source',
1069
+ matchLine,
1070
+ preview: content.slice(0, 200) + (content.length > 200 ? '...' : ''),
1071
+ });
1072
+ }
1073
+ // Search in outputs
1074
+ if (includeOutputs) {
1075
+ for (let j = 0; j < (cell.outputs || []).length; j++) {
1076
+ const output = cell.outputs[j];
1077
+ const outContent = output.content || '';
1078
+ if (outContent.toLowerCase().includes(queryLower)) {
1079
+ matches.push({
1080
+ cellId: cell.id,
1081
+ cellIndex: i,
1082
+ matchLocation: 'output',
1083
+ outputIndex: j,
1084
+ outputType: output.type || 'unknown',
1085
+ preview: outContent.slice(0, 200) + (outContent.length > 200 ? '...' : ''),
1086
+ });
1087
+ }
1088
+ }
1089
+ }
1090
+ }
1091
+ return {
1092
+ success: true,
1093
+ query,
1094
+ matchCount: matches.length,
1095
+ matches: matches.slice(0, limit),
1096
+ hasMore: matches.length > limit,
1097
+ };
1098
+ }
1099
+ async clearOutputs(operation) {
1100
+ const notebookPath = operation.notebookPath;
1101
+ const cellId = operation.cellId;
1102
+ const cellIds = operation.cellIds || [];
1103
+ // Support both single ID and list
1104
+ const targetIds = cellId && cellIds.length === 0 ? [cellId] : cellIds;
1105
+ const cells = this.getCells(notebookPath);
1106
+ const clearedIds = [];
1107
+ const notFound = [];
1108
+ if (targetIds.length === 0) {
1109
+ // Clear all cells
1110
+ for (const cell of cells) {
1111
+ if (cell.outputs && cell.outputs.length > 0) {
1112
+ cell.outputs = [];
1113
+ clearedIds.push(cell.id);
1114
+ }
1115
+ }
1116
+ }
1117
+ else {
1118
+ // Clear specific cells
1119
+ for (const id of targetIds) {
1120
+ const cell = cells.find(c => c.id === id);
1121
+ if (cell) {
1122
+ cell.outputs = [];
1123
+ clearedIds.push(id);
1124
+ }
1125
+ else {
1126
+ notFound.push(id);
1127
+ }
1128
+ }
1129
+ }
1130
+ this.saveCells(notebookPath, cells);
1131
+ return {
1132
+ success: true,
1133
+ clearedCount: clearedIds.length,
1134
+ clearedIds,
1135
+ notFound: notFound.length > 0 ? notFound : null,
1136
+ };
1137
+ }
1138
+ /**
1139
+ * Execute a cell using the kernel service.
1140
+ * Matches Python headless_handler._execute_cell() behavior.
1141
+ */
1142
+ async executeCell(operation, notebookPath) {
1143
+ const cellId = operation.cellId ?? operation.cell_id;
1144
+ const cellIndex = operation.cellIndex ?? operation.cell_index;
1145
+ const maxWait = (operation.maxWait ?? operation.max_wait ?? 10);
1146
+ const saveOutputs = (operation.saveOutputs ?? operation.save_outputs ?? true);
1147
+ // Get the cell
1148
+ const cells = this.getCells(notebookPath);
1149
+ let targetIndex;
1150
+ let cell;
1151
+ if (cellId) {
1152
+ targetIndex = cells.findIndex(c => c.id === cellId);
1153
+ if (targetIndex === -1) {
1154
+ return { success: false, error: `Cell with ID "${cellId}" not found` };
1155
+ }
1156
+ cell = cells[targetIndex];
1157
+ }
1158
+ else if (cellIndex !== undefined) {
1159
+ if (cellIndex < 0 || cellIndex >= cells.length) {
1160
+ return { success: false, error: `Cell index ${cellIndex} out of range` };
1161
+ }
1162
+ targetIndex = cellIndex;
1163
+ cell = cells[cellIndex];
1164
+ }
1165
+ else {
1166
+ return { success: false, error: 'Must provide cellId or cellIndex' };
1167
+ }
1168
+ const actualCellId = cell.id;
1169
+ // Only execute code cells
1170
+ if (cell.type !== 'code') {
1171
+ return { success: false, error: `Cell ${targetIndex} is not a code cell` };
1172
+ }
1173
+ const code = cell.content || '';
1174
+ // Handle empty cells - just clear outputs
1175
+ if (!code.trim()) {
1176
+ cell.outputs = [];
1177
+ cell.executionCount = null;
1178
+ if (saveOutputs) {
1179
+ this.saveCells(notebookPath, cells);
1180
+ }
1181
+ return {
1182
+ success: true,
1183
+ cellId: actualCellId,
1184
+ cellIndex: targetIndex,
1185
+ executionStatus: 'idle',
1186
+ outputs: [],
1187
+ executionCount: null,
1188
+ };
1189
+ }
1190
+ // Check if kernel service is available
1191
+ if (!this.kernelService) {
1192
+ return {
1193
+ success: false,
1194
+ error: 'Kernel service not available. Make sure the Node.js server is properly initialized.',
1195
+ };
1196
+ }
1197
+ // Get or create a kernel session for this notebook
1198
+ const requestedSessionId = operation.sessionId ?? operation.session_id;
1199
+ const preferredKernelName = this.kernelService.getNotebookKernelPreference(notebookPath)?.kernelName || 'python3';
1200
+ let sessionId = null;
1201
+ if (requestedSessionId && this.kernelService.hasSession(requestedSessionId)) {
1202
+ sessionId = requestedSessionId;
1203
+ }
1204
+ else {
1205
+ sessionId = this.kernelService.getSessionIdForFile(notebookPath);
1206
+ }
1207
+ if (!sessionId) {
1208
+ try {
1209
+ const result = await this.kernelService.getOrCreateKernel(notebookPath, preferredKernelName);
1210
+ sessionId = result.sessionId;
1211
+ }
1212
+ catch (err) {
1213
+ const errMsg = err instanceof Error ? err.message : String(err);
1214
+ return { success: false, error: `Failed to start kernel: ${errMsg}` };
1215
+ }
1216
+ }
1217
+ // Preserve previous outputs until fresh output arrives so UI consumers can still
1218
+ // render them, but mark them stale so read_output hides them for this new run.
1219
+ cell.isExecuting = true;
1220
+ cell.pendingOutputReset = true;
1221
+ // Execute the cell with periodic output saving
1222
+ const runId = (0, uuid_1.v4)();
1223
+ const startTime = Date.now();
1224
+ this.recordLogOperation(notebookPath, {
1225
+ type: 'event',
1226
+ category: 'execution',
1227
+ name: 'runCell',
1228
+ target: { cellId: actualCellId, cellIndex: targetIndex },
1229
+ runId,
1230
+ data: { sessionId },
1231
+ source: 'mcp',
1232
+ });
1233
+ const outputs = [];
1234
+ let executionCount = null;
1235
+ let executionError = null;
1236
+ let executionComplete = false;
1237
+ let queueInfo = null;
1238
+ const publishOutputs = () => {
1239
+ if (outputs.length > 0) {
1240
+ cell.outputs = [...outputs];
1241
+ cell.pendingOutputReset = false;
1242
+ return;
1243
+ }
1244
+ if (executionComplete) {
1245
+ cell.outputs = [];
1246
+ cell.pendingOutputReset = false;
1247
+ }
1248
+ };
1249
+ const outputCallback = async (output) => {
1250
+ outputs.push(copyCellOutput(output));
1251
+ // Save outputs periodically (every 5 outputs) like Python
1252
+ if (saveOutputs && outputs.length % 5 === 0) {
1253
+ publishOutputs();
1254
+ this.saveCells(notebookPath, cells);
1255
+ }
1256
+ };
1257
+ try {
1258
+ // Create execution promise
1259
+ const executeTask = async () => {
1260
+ try {
1261
+ const result = await this.kernelService.executeCode(sessionId, code, outputCallback, (info) => {
1262
+ queueInfo = info;
1263
+ }, actualCellId);
1264
+ executionCount = result.executionCount;
1265
+ if (!queueInfo && result.queuePosition !== undefined && result.queueLength !== undefined) {
1266
+ queueInfo = { queuePosition: result.queuePosition, queueLength: result.queueLength };
1267
+ }
1268
+ if (result.status === 'error') {
1269
+ executionError = result.error || 'Unknown error';
1270
+ }
1271
+ }
1272
+ catch (err) {
1273
+ executionError = err instanceof Error ? err.message : String(err);
1274
+ }
1275
+ finally {
1276
+ executionComplete = true;
1277
+ }
1278
+ };
1279
+ // Start execution
1280
+ const executionPromise = executeTask();
1281
+ // Wait for completion or timeout
1282
+ const timeoutPromise = new Promise((resolve) => {
1283
+ setTimeout(resolve, maxWait * 1000);
1284
+ });
1285
+ await Promise.race([executionPromise, timeoutPromise]);
1286
+ const elapsed = Date.now() - startTime;
1287
+ const status = executionComplete
1288
+ ? (executionError ? 'error' : 'idle')
1289
+ : 'busy';
1290
+ // Update cell with any fresh outputs from this run.
1291
+ publishOutputs();
1292
+ if (executionCount !== null) {
1293
+ cell.executionCount = executionCount;
1294
+ }
1295
+ cell.isExecuting = !executionComplete;
1296
+ if (saveOutputs) {
1297
+ this.saveCells(notebookPath, cells);
1298
+ }
1299
+ // Cast queueInfo to avoid TS narrowing issues with async closures
1300
+ const qi = queueInfo;
1301
+ if (!executionComplete) {
1302
+ // Execution continues in background - finalize when promise resolves
1303
+ executionPromise.then(() => {
1304
+ const finalElapsed = Date.now() - startTime;
1305
+ publishOutputs();
1306
+ if (executionCount !== null) {
1307
+ cell.executionCount = executionCount;
1308
+ }
1309
+ cell.isExecuting = false;
1310
+ if (saveOutputs) {
1311
+ this.saveCells(notebookPath, cells);
1312
+ }
1313
+ const success = !executionError;
1314
+ this.recordLogOperation(notebookPath, {
1315
+ type: 'event',
1316
+ category: 'execution',
1317
+ name: 'runCellComplete',
1318
+ target: { cellId: actualCellId, cellIndex: targetIndex },
1319
+ runId,
1320
+ data: { durationMs: finalElapsed, success },
1321
+ source: 'mcp',
1322
+ });
1323
+ }).catch(() => { });
1324
+ // Execution is still running
1325
+ return {
1326
+ success: true,
1327
+ executionStatus: 'busy',
1328
+ cellId: actualCellId,
1329
+ cellIndex: targetIndex,
1330
+ outputs: outputs.map(o => copyCellOutput(o)),
1331
+ executionTime: elapsed,
1332
+ sessionId,
1333
+ queuePosition: qi?.queuePosition,
1334
+ queueLength: qi?.queueLength,
1335
+ message: `Cell still executing after ${maxWait}s. Use read_output with max_wait to poll for results.`,
1336
+ };
1337
+ }
1338
+ const success = !executionError;
1339
+ this.recordLogOperation(notebookPath, {
1340
+ type: 'event',
1341
+ category: 'execution',
1342
+ name: 'runCellComplete',
1343
+ target: { cellId: actualCellId, cellIndex: targetIndex },
1344
+ runId,
1345
+ data: { durationMs: elapsed, success },
1346
+ source: 'mcp',
1347
+ });
1348
+ return {
1349
+ success: true,
1350
+ cellId: actualCellId,
1351
+ cellIndex: targetIndex,
1352
+ executionStatus: status,
1353
+ executionCount,
1354
+ outputs: outputs.map(o => copyCellOutput(o)),
1355
+ executionTime: elapsed,
1356
+ sessionId,
1357
+ queuePosition: qi?.queuePosition,
1358
+ queueLength: qi?.queueLength,
1359
+ error: executionError || undefined,
1360
+ };
1361
+ }
1362
+ catch (err) {
1363
+ const errMsg = err instanceof Error ? err.message : String(err);
1364
+ return { success: false, error: `Execution failed: ${errMsg}` };
1365
+ }
1366
+ }
1367
+ // -------------------------------------------------------------------------
1368
+ // Undo/Redo Operations
1369
+ // -------------------------------------------------------------------------
1370
+ /**
1371
+ * Handle undo operation.
1372
+ */
1373
+ async handleUndo(notebookPath) {
1374
+ const cells = this.getCells(notebookPath);
1375
+ const { cells: newCells, result } = this.undoRedoManager.undo(notebookPath, cells);
1376
+ if (result.success) {
1377
+ this.saveCells(notebookPath, newCells);
1378
+ }
1379
+ return {
1380
+ success: result.success,
1381
+ affectedCellIds: result.affectedCellIds,
1382
+ operationType: result.operationType,
1383
+ error: result.error,
1384
+ canUndo: this.undoRedoManager.canUndo(notebookPath, newCells),
1385
+ canRedo: this.undoRedoManager.canRedo(notebookPath, newCells),
1386
+ };
1387
+ }
1388
+ /**
1389
+ * Handle redo operation.
1390
+ */
1391
+ async handleRedo(notebookPath) {
1392
+ const cells = this.getCells(notebookPath);
1393
+ const { cells: newCells, result } = this.undoRedoManager.redo(notebookPath, cells);
1394
+ if (result.success) {
1395
+ this.saveCells(notebookPath, newCells);
1396
+ }
1397
+ return {
1398
+ success: result.success,
1399
+ affectedCellIds: result.affectedCellIds,
1400
+ operationType: result.operationType,
1401
+ error: result.error,
1402
+ canUndo: this.undoRedoManager.canUndo(notebookPath, newCells),
1403
+ canRedo: this.undoRedoManager.canRedo(notebookPath, newCells),
1404
+ };
1405
+ }
1406
+ /**
1407
+ * Record an undoable operation (helper method for other operations).
1408
+ */
1409
+ recordUndoableOperation(notebookPath, op) {
1410
+ const cells = this.getCells(notebookPath);
1411
+ this.undoRedoManager.recordOperation(notebookPath, cells, op);
1412
+ }
1413
+ /**
1414
+ * Record a non-undoable log operation (helper method for other operations).
1415
+ */
1416
+ recordLogOperation(notebookPath, op) {
1417
+ const cells = this.getCells(notebookPath);
1418
+ this.undoRedoManager.recordLogOperation(notebookPath, cells, op);
1419
+ }
1420
+ /**
1421
+ * Get updates since a timestamp (public method for operation router).
1422
+ * Used by startAgentSession to inform agent what changed between sessions.
1423
+ */
1424
+ getUpdatesSince(notebookPath, sinceTimestamp) {
1425
+ const cells = this.getCells(notebookPath);
1426
+ return this.undoRedoManager.getUpdatesSince(notebookPath, cells, sinceTimestamp);
1427
+ }
1428
+ // -------------------------------------------------------------------------
1429
+ // Kernel Operations
1430
+ // -------------------------------------------------------------------------
1431
+ async startKernelOp(operation, notebookPath) {
1432
+ if (!this.kernelService) {
1433
+ return { success: false, error: 'Kernel service not available' };
1434
+ }
1435
+ const kernelName = operation.kernelName || 'python3';
1436
+ try {
1437
+ const { sessionId, created } = await this.kernelService.getOrCreateKernel(notebookPath, kernelName);
1438
+ this.kernelService.saveNotebookKernelPreference(notebookPath, kernelName);
1439
+ const spec = (0, kernelspec_1.getKernelSpec)(kernelName);
1440
+ const metadataResult = await this.fsService.updateNotebookMetadata(notebookPath, {
1441
+ kernelspec: {
1442
+ name: kernelName,
1443
+ display_name: spec?.displayName || (kernelName === 'python3' ? 'Python 3' : kernelName),
1444
+ language: spec?.language || 'python',
1445
+ },
1446
+ });
1447
+ if (!metadataResult.success) {
1448
+ return { success: false, error: metadataResult.error || `Failed to update kernel metadata for ${notebookPath}` };
1449
+ }
1450
+ if (created) {
1451
+ this.recordLogOperation(notebookPath, {
1452
+ type: 'event',
1453
+ category: 'kernel',
1454
+ name: 'startKernel',
1455
+ data: { sessionId, kernelName },
1456
+ source: 'mcp',
1457
+ });
1458
+ }
1459
+ return { success: true, sessionId, kernelName };
1460
+ }
1461
+ catch (err) {
1462
+ const errMsg = err instanceof Error ? err.message : String(err);
1463
+ return { success: false, error: `Failed to start kernel: ${errMsg}` };
1464
+ }
1465
+ }
1466
+ async shutdownKernelOp(notebookPath) {
1467
+ if (!this.kernelService) {
1468
+ return { success: false, error: 'Kernel service not available' };
1469
+ }
1470
+ const sessionId = this.kernelService.getSessionIdForFile(notebookPath);
1471
+ if (!sessionId) {
1472
+ return { success: false, error: 'No kernel session found for notebook' };
1473
+ }
1474
+ const success = await this.kernelService.stopKernel(sessionId);
1475
+ if (!success) {
1476
+ return { success: false, error: 'Failed to shutdown kernel (session not found)' };
1477
+ }
1478
+ this.recordLogOperation(notebookPath, {
1479
+ type: 'event',
1480
+ category: 'kernel',
1481
+ name: 'shutdownKernel',
1482
+ data: { sessionId },
1483
+ source: 'mcp',
1484
+ });
1485
+ return { success: true, sessionId };
1486
+ }
1487
+ async restartKernelOp(notebookPath) {
1488
+ if (!this.kernelService) {
1489
+ return { success: false, error: 'Kernel service not available' };
1490
+ }
1491
+ const sessionId = this.kernelService.getSessionIdForFile(notebookPath);
1492
+ if (!sessionId) {
1493
+ return { success: false, error: 'No kernel session found for notebook' };
1494
+ }
1495
+ const success = await this.kernelService.restartKernel(sessionId);
1496
+ if (!success) {
1497
+ return { success: false, error: 'Failed to restart kernel (session not found)' };
1498
+ }
1499
+ this.recordLogOperation(notebookPath, {
1500
+ type: 'event',
1501
+ category: 'kernel',
1502
+ name: 'restartKernel',
1503
+ data: { sessionId },
1504
+ source: 'mcp',
1505
+ });
1506
+ return { success: true, sessionId };
1507
+ }
1508
+ async interruptKernelOp(notebookPath) {
1509
+ if (!this.kernelService) {
1510
+ return { success: false, error: 'Kernel service not available' };
1511
+ }
1512
+ const sessionId = this.kernelService.getSessionIdForFile(notebookPath);
1513
+ if (!sessionId) {
1514
+ return { success: false, error: 'No kernel session found for notebook' };
1515
+ }
1516
+ const success = await this.kernelService.interruptKernel(sessionId);
1517
+ if (!success) {
1518
+ return { success: false, error: 'Failed to interrupt kernel (session not found)' };
1519
+ }
1520
+ this.recordLogOperation(notebookPath, {
1521
+ type: 'event',
1522
+ category: 'kernel',
1523
+ name: 'interruptKernel',
1524
+ data: { sessionId },
1525
+ source: 'mcp',
1526
+ });
1527
+ return { success: true, sessionId };
1528
+ }
1529
+ }
1530
+ exports.HeadlessOperationHandler = HeadlessOperationHandler;