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,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;
|