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,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* LLM API Routes
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.llmService = void 0;
|
|
7
|
+
exports.default = llmRoutes;
|
|
8
|
+
const llm_service_1 = require("../llm/llm-service");
|
|
9
|
+
const llmService = new llm_service_1.LLMService();
|
|
10
|
+
exports.llmService = llmService;
|
|
11
|
+
async function llmRoutes(fastify) {
|
|
12
|
+
/**
|
|
13
|
+
* List available LLM providers and models
|
|
14
|
+
*/
|
|
15
|
+
fastify.get('/llm/providers', async (_request, reply) => {
|
|
16
|
+
const providers = llmService.getAvailableProviders();
|
|
17
|
+
return reply.send({ providers });
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Generate text/code from LLM
|
|
21
|
+
*/
|
|
22
|
+
fastify.post('/llm/generate', async (request, reply) => {
|
|
23
|
+
try {
|
|
24
|
+
const { prompt, system_prompt, provider = 'google', model = 'gemini-2.5-flash', temperature = 0.2, images, } = request.body;
|
|
25
|
+
// Check for API key from header
|
|
26
|
+
const apiKey = request.headers['x-api-key'];
|
|
27
|
+
const apiProvider = request.headers['x-api-provider'];
|
|
28
|
+
const response = await llmService.generate({
|
|
29
|
+
prompt,
|
|
30
|
+
systemPrompt: system_prompt,
|
|
31
|
+
config: {
|
|
32
|
+
provider: provider,
|
|
33
|
+
model,
|
|
34
|
+
temperature,
|
|
35
|
+
apiKey: apiProvider === provider ? apiKey : undefined,
|
|
36
|
+
},
|
|
37
|
+
images: images,
|
|
38
|
+
});
|
|
39
|
+
return reply.send({ response });
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
43
|
+
return reply.code(500).send({ detail: message });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* Generate structured JSON response from LLM
|
|
48
|
+
*/
|
|
49
|
+
fastify.post('/llm/generate-structured', async (request, reply) => {
|
|
50
|
+
try {
|
|
51
|
+
const { prompt, system_prompt, provider = 'google', model = 'gemini-2.5-flash', temperature = 0.2, } = request.body;
|
|
52
|
+
// Check for API key from header
|
|
53
|
+
const apiKey = request.headers['x-api-key'];
|
|
54
|
+
const apiProvider = request.headers['x-api-provider'];
|
|
55
|
+
const response = await llmService.generateStructured({
|
|
56
|
+
prompt,
|
|
57
|
+
systemPrompt: system_prompt,
|
|
58
|
+
config: {
|
|
59
|
+
provider: provider,
|
|
60
|
+
model,
|
|
61
|
+
temperature,
|
|
62
|
+
apiKey: apiProvider === provider ? apiKey : undefined,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
return reply.send({ response });
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
69
|
+
return reply.code(500).send({ detail: message });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* Chat with LLM including history
|
|
74
|
+
*/
|
|
75
|
+
fastify.post('/llm/chat', async (request, reply) => {
|
|
76
|
+
try {
|
|
77
|
+
const { message, history = [], system_prompt, provider = 'google', model = 'gemini-2.5-flash', temperature = 0.2, images, } = request.body;
|
|
78
|
+
// Check for API key from header
|
|
79
|
+
const apiKey = request.headers['x-api-key'];
|
|
80
|
+
const apiProvider = request.headers['x-api-provider'];
|
|
81
|
+
// Convert history format
|
|
82
|
+
const chatHistory = history.map((h) => ({
|
|
83
|
+
role: h.role,
|
|
84
|
+
content: h.content,
|
|
85
|
+
}));
|
|
86
|
+
const response = await llmService.chat({
|
|
87
|
+
message,
|
|
88
|
+
history: chatHistory,
|
|
89
|
+
systemPrompt: system_prompt,
|
|
90
|
+
config: {
|
|
91
|
+
provider: provider,
|
|
92
|
+
model,
|
|
93
|
+
temperature,
|
|
94
|
+
apiKey: apiProvider === provider ? apiKey : undefined,
|
|
95
|
+
},
|
|
96
|
+
images: images,
|
|
97
|
+
});
|
|
98
|
+
return reply.send({ response });
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
102
|
+
return reply.code(500).send({ detail: message });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notebook API Routes
|
|
3
|
+
*/
|
|
4
|
+
import { FastifyInstance } from 'fastify';
|
|
5
|
+
import { fsService } from '../fs/fs-service';
|
|
6
|
+
import { operationRouter } from '../notebook/operation-router';
|
|
7
|
+
import { HeadlessOperationHandler } from '../notebook/headless-handler';
|
|
8
|
+
declare const headlessHandler: HeadlessOperationHandler;
|
|
9
|
+
export default function notebookRoutes(fastify: FastifyInstance): Promise<void>;
|
|
10
|
+
export { fsService, operationRouter, headlessHandler };
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Notebook API Routes
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.headlessHandler = exports.operationRouter = exports.fsService = void 0;
|
|
7
|
+
exports.default = notebookRoutes;
|
|
8
|
+
const fs_service_1 = require("../fs/fs-service");
|
|
9
|
+
Object.defineProperty(exports, "fsService", { enumerable: true, get: function () { return fs_service_1.fsService; } });
|
|
10
|
+
const operation_router_1 = require("../notebook/operation-router");
|
|
11
|
+
Object.defineProperty(exports, "operationRouter", { enumerable: true, get: function () { return operation_router_1.operationRouter; } });
|
|
12
|
+
const headless_handler_1 = require("../notebook/headless-handler");
|
|
13
|
+
const kernel_1 = require("./kernel");
|
|
14
|
+
// Initialize headless handler with kernel service for cell execution
|
|
15
|
+
const headlessHandler = new headless_handler_1.HeadlessOperationHandler(fs_service_1.fsService, operation_router_1.operationRouter, kernel_1.kernelService);
|
|
16
|
+
exports.headlessHandler = headlessHandler;
|
|
17
|
+
operation_router_1.operationRouter.setHeadlessHandler(headlessHandler);
|
|
18
|
+
async function notebookRoutes(fastify) {
|
|
19
|
+
/**
|
|
20
|
+
* Get cell metadata schema
|
|
21
|
+
*/
|
|
22
|
+
fastify.get('/cell/metadata-schema', async (_request, reply) => {
|
|
23
|
+
return reply.send({
|
|
24
|
+
id: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Unique cell identifier. Agent-created cells should use human-readable IDs.',
|
|
27
|
+
agentMutable: true,
|
|
28
|
+
},
|
|
29
|
+
type: {
|
|
30
|
+
type: 'enum',
|
|
31
|
+
values: ['code', 'markdown'],
|
|
32
|
+
description: 'Cell type: code for executable cells, markdown for documentation.',
|
|
33
|
+
agentMutable: true,
|
|
34
|
+
},
|
|
35
|
+
scrolled: {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
description: 'Whether cell output is collapsed (Jupyter standard).',
|
|
38
|
+
agentMutable: true,
|
|
39
|
+
default: false,
|
|
40
|
+
},
|
|
41
|
+
scrolledHeight: {
|
|
42
|
+
type: 'number',
|
|
43
|
+
description: 'Height in pixels when output is collapsed.',
|
|
44
|
+
agentMutable: true,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* Get notebook cells in internal format
|
|
50
|
+
*/
|
|
51
|
+
fastify.get('/notebook/cells', async (request, reply) => {
|
|
52
|
+
try {
|
|
53
|
+
const filePath = request.query.path;
|
|
54
|
+
if (!filePath) {
|
|
55
|
+
return reply.code(400).send({ detail: 'path query parameter is required' });
|
|
56
|
+
}
|
|
57
|
+
const normalizedPath = fs_service_1.fsService.normalizePath(filePath);
|
|
58
|
+
const result = await fs_service_1.fsService.getNotebookCellsWithKernel(filePath);
|
|
59
|
+
return reply.send({
|
|
60
|
+
path: normalizedPath,
|
|
61
|
+
cells: result.cells,
|
|
62
|
+
metadata: result.metadata,
|
|
63
|
+
kernelspec: result.kernelspec,
|
|
64
|
+
mtime: result.mtime,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
if (err instanceof Error && err.message.includes('not found')) {
|
|
69
|
+
return reply.code(404).send({ detail: err.message });
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
73
|
+
return reply.code(500).send({ detail: message });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
/**
|
|
78
|
+
* Save notebook cells
|
|
79
|
+
*/
|
|
80
|
+
fastify.post('/notebook/save', async (request, reply) => {
|
|
81
|
+
try {
|
|
82
|
+
const { path: filePath, cells, kernel_name, history } = request.body;
|
|
83
|
+
if (!filePath) {
|
|
84
|
+
return reply.code(400).send({ detail: 'path is required' });
|
|
85
|
+
}
|
|
86
|
+
if (!cells) {
|
|
87
|
+
return reply.code(400).send({ detail: 'cells is required' });
|
|
88
|
+
}
|
|
89
|
+
const result = await fs_service_1.fsService.saveNotebookBundle(filePath, cells, kernel_name, history);
|
|
90
|
+
return reply.send({ status: 'ok', path: filePath, mtime: result.mtime });
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
94
|
+
return reply.code(500).send({ detail: message });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
/**
|
|
98
|
+
* Load operation history for a notebook
|
|
99
|
+
*/
|
|
100
|
+
fastify.get('/notebook/history', async (request, reply) => {
|
|
101
|
+
try {
|
|
102
|
+
const notebookPath = request.query.notebook_path;
|
|
103
|
+
if (!notebookPath) {
|
|
104
|
+
return reply.code(400).send({ detail: 'notebook_path query parameter is required' });
|
|
105
|
+
}
|
|
106
|
+
const history = fs_service_1.fsService.loadHistory(notebookPath);
|
|
107
|
+
return reply.send({ notebook_path: notebookPath, history });
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
111
|
+
return reply.code(500).send({ detail: message });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
/**
|
|
115
|
+
* Save operation history for a notebook
|
|
116
|
+
*/
|
|
117
|
+
fastify.post('/notebook/history', async (request, reply) => {
|
|
118
|
+
try {
|
|
119
|
+
const { notebook_path, history } = request.body;
|
|
120
|
+
if (!notebook_path) {
|
|
121
|
+
return reply.code(400).send({ detail: 'notebook_path is required' });
|
|
122
|
+
}
|
|
123
|
+
await fs_service_1.fsService.saveHistory(notebook_path, history || []);
|
|
124
|
+
return reply.send({ status: 'ok', notebook_path });
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
128
|
+
return reply.code(500).send({ detail: message });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
/**
|
|
132
|
+
* Load session state for a notebook
|
|
133
|
+
*/
|
|
134
|
+
fastify.get('/notebook/session', async (request, reply) => {
|
|
135
|
+
try {
|
|
136
|
+
const notebookPath = request.query.notebook_path;
|
|
137
|
+
if (!notebookPath) {
|
|
138
|
+
return reply.code(400).send({ detail: 'notebook_path query parameter is required' });
|
|
139
|
+
}
|
|
140
|
+
const session = fs_service_1.fsService.loadSession(notebookPath);
|
|
141
|
+
return reply.send({ notebook_path: notebookPath, session });
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
145
|
+
return reply.code(500).send({ detail: message });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
/**
|
|
149
|
+
* Save session state for a notebook
|
|
150
|
+
*/
|
|
151
|
+
fastify.post('/notebook/session', async (request, reply) => {
|
|
152
|
+
try {
|
|
153
|
+
const { notebook_path, session } = request.body;
|
|
154
|
+
if (!notebook_path) {
|
|
155
|
+
return reply.code(400).send({ detail: 'notebook_path is required' });
|
|
156
|
+
}
|
|
157
|
+
await fs_service_1.fsService.saveSession(notebook_path, session || {});
|
|
158
|
+
return reply.send({ status: 'ok', notebook_path });
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
162
|
+
return reply.code(500).send({ detail: message });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
/**
|
|
166
|
+
* Grant or revoke agent permission to modify a notebook
|
|
167
|
+
*/
|
|
168
|
+
fastify.post('/notebook/permit-agent', async (request, reply) => {
|
|
169
|
+
try {
|
|
170
|
+
const { notebook_path, permitted = true } = request.body;
|
|
171
|
+
if (!notebook_path) {
|
|
172
|
+
return reply.code(400).send({ detail: 'notebook_path is required' });
|
|
173
|
+
}
|
|
174
|
+
const result = await fs_service_1.fsService.setAgentPermission(notebook_path, permitted);
|
|
175
|
+
if (!result.success) {
|
|
176
|
+
return reply.code(400).send({ detail: result.error || 'Failed to update notebook' });
|
|
177
|
+
}
|
|
178
|
+
return reply.send({
|
|
179
|
+
status: 'ok',
|
|
180
|
+
notebook_path,
|
|
181
|
+
...(result.status || fs_service_1.fsService.getAgentPermissionStatus(notebook_path)),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
186
|
+
return reply.code(500).send({ detail: message });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
/**
|
|
190
|
+
* Get agent permission status for a notebook
|
|
191
|
+
*/
|
|
192
|
+
fastify.get('/notebook/agent-status', async (request, reply) => {
|
|
193
|
+
try {
|
|
194
|
+
const filePath = request.query.path;
|
|
195
|
+
if (!filePath) {
|
|
196
|
+
return reply.code(400).send({ detail: 'path query parameter is required' });
|
|
197
|
+
}
|
|
198
|
+
const status = fs_service_1.fsService.getAgentPermissionStatus(filePath);
|
|
199
|
+
return reply.send({
|
|
200
|
+
notebook_path: filePath,
|
|
201
|
+
...status,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
206
|
+
return reply.code(500).send({ detail: message });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
/**
|
|
210
|
+
* Read notebook via operation router
|
|
211
|
+
*
|
|
212
|
+
* Supports output truncation for large outputs.
|
|
213
|
+
* When include_outputs=true (default), outputs are truncated with defaults.
|
|
214
|
+
*
|
|
215
|
+
* Uses operation router to get live state from UI if connected,
|
|
216
|
+
* otherwise reads from file.
|
|
217
|
+
*/
|
|
218
|
+
fastify.get('/notebook/read', async (request, reply) => {
|
|
219
|
+
try {
|
|
220
|
+
const filePath = request.query.path;
|
|
221
|
+
if (!filePath) {
|
|
222
|
+
return reply.code(400).send({ detail: 'path query parameter is required' });
|
|
223
|
+
}
|
|
224
|
+
const includeOutputs = request.query.include_outputs !== 'false';
|
|
225
|
+
const maxLines = request.query.max_lines ? parseInt(request.query.max_lines, 10) : undefined;
|
|
226
|
+
const maxChars = request.query.max_chars ? parseInt(request.query.max_chars, 10) : undefined;
|
|
227
|
+
const maxLinesError = request.query.max_lines_error ? parseInt(request.query.max_lines_error, 10) : undefined;
|
|
228
|
+
const maxCharsError = request.query.max_chars_error ? parseInt(request.query.max_chars_error, 10) : undefined;
|
|
229
|
+
// Use operation router to get state from UI if connected, otherwise from file
|
|
230
|
+
const result = await operation_router_1.operationRouter.readNotebook(filePath, includeOutputs, maxLines, maxChars, maxLinesError, maxCharsError);
|
|
231
|
+
return reply.send(result);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
235
|
+
return reply.send({ success: false, error: message });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
/**
|
|
239
|
+
* Check if UI is connected for a notebook
|
|
240
|
+
*/
|
|
241
|
+
fastify.get('/notebook/has-ui', async (request, reply) => {
|
|
242
|
+
const filePath = request.query.path;
|
|
243
|
+
if (!filePath) {
|
|
244
|
+
return reply.code(400).send({ detail: 'path query parameter is required' });
|
|
245
|
+
}
|
|
246
|
+
const hasUI = operation_router_1.operationRouter.hasUI(filePath);
|
|
247
|
+
return reply.send({ hasUI, path: filePath });
|
|
248
|
+
});
|
|
249
|
+
/**
|
|
250
|
+
* Apply a notebook operation
|
|
251
|
+
*
|
|
252
|
+
* Routes to UI if connected, otherwise uses headless handler.
|
|
253
|
+
*/
|
|
254
|
+
fastify.post('/notebook/operation', async (request, reply) => {
|
|
255
|
+
try {
|
|
256
|
+
// Python uses {operation: {...}} wrapper via NotebookOperationRequest model
|
|
257
|
+
const operation = request.body.operation || request.body;
|
|
258
|
+
if (!operation || !operation.type) {
|
|
259
|
+
return reply.code(400).send({ detail: 'Operation with type is required' });
|
|
260
|
+
}
|
|
261
|
+
const result = await operation_router_1.operationRouter.applyOperation(operation);
|
|
262
|
+
return reply.send(result);
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
266
|
+
return reply.send({ success: false, error: message });
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
/**
|
|
270
|
+
* Get notebook settings (nebula metadata)
|
|
271
|
+
*/
|
|
272
|
+
fastify.get('/notebook/settings', async (request, reply) => {
|
|
273
|
+
try {
|
|
274
|
+
const filePath = request.query.path;
|
|
275
|
+
if (!filePath) {
|
|
276
|
+
return reply.code(400).send({ detail: 'path query parameter is required' });
|
|
277
|
+
}
|
|
278
|
+
const metadata = fs_service_1.fsService.getNotebookMetadata(filePath);
|
|
279
|
+
const nebula = (metadata.nebula || {});
|
|
280
|
+
return reply.send({
|
|
281
|
+
notebook_path: filePath,
|
|
282
|
+
output_logging: nebula.output_logging || 'minimal',
|
|
283
|
+
full_width: nebula.full_width === true,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
288
|
+
return reply.code(500).send({ detail: message });
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
/**
|
|
292
|
+
* Update notebook settings (nebula metadata)
|
|
293
|
+
*/
|
|
294
|
+
fastify.post('/notebook/settings', async (request, reply) => {
|
|
295
|
+
try {
|
|
296
|
+
const { path: filePath, output_logging, full_width } = request.body;
|
|
297
|
+
if (!filePath) {
|
|
298
|
+
return reply.code(400).send({ detail: 'path is required' });
|
|
299
|
+
}
|
|
300
|
+
// Validate output_logging value
|
|
301
|
+
if (output_logging !== undefined && output_logging !== 'minimal' && output_logging !== 'full') {
|
|
302
|
+
return reply.code(400).send({ detail: 'output_logging must be "minimal" or "full"' });
|
|
303
|
+
}
|
|
304
|
+
if (full_width !== undefined && typeof full_width !== 'boolean') {
|
|
305
|
+
return reply.code(400).send({ detail: 'full_width must be a boolean' });
|
|
306
|
+
}
|
|
307
|
+
// Get existing metadata
|
|
308
|
+
const metadata = fs_service_1.fsService.getNotebookMetadata(filePath);
|
|
309
|
+
const nebula = (metadata.nebula || {});
|
|
310
|
+
// Update settings
|
|
311
|
+
if (output_logging !== undefined) {
|
|
312
|
+
nebula.output_logging = output_logging;
|
|
313
|
+
}
|
|
314
|
+
if (full_width !== undefined) {
|
|
315
|
+
nebula.full_width = full_width;
|
|
316
|
+
}
|
|
317
|
+
// Save back
|
|
318
|
+
const updateResult = await fs_service_1.fsService.updateNotebookMetadata(filePath, { nebula });
|
|
319
|
+
if (!updateResult.success) {
|
|
320
|
+
return reply.code(500).send({ detail: updateResult.error || 'Failed to update notebook metadata' });
|
|
321
|
+
}
|
|
322
|
+
return reply.send({
|
|
323
|
+
status: 'ok',
|
|
324
|
+
notebook_path: filePath,
|
|
325
|
+
output_logging: nebula.output_logging || 'minimal',
|
|
326
|
+
full_width: nebula.full_width === true,
|
|
327
|
+
mtime: updateResult.mtime,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
332
|
+
return reply.code(500).send({ detail: message });
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python Discovery API Routes
|
|
3
|
+
*/
|
|
4
|
+
import { FastifyInstance } from 'fastify';
|
|
5
|
+
import { PythonDiscoveryService } from '../discovery/discovery-service';
|
|
6
|
+
declare const discoveryService: PythonDiscoveryService;
|
|
7
|
+
export default function pythonRoutes(fastify: FastifyInstance): Promise<void>;
|
|
8
|
+
export { discoveryService };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Python Discovery API Routes
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.discoveryService = void 0;
|
|
7
|
+
exports.default = pythonRoutes;
|
|
8
|
+
const discovery_service_1 = require("../discovery/discovery-service");
|
|
9
|
+
const types_1 = require("../discovery/types");
|
|
10
|
+
const kernelspec_1 = require("../kernel/kernelspec");
|
|
11
|
+
const default_kernel_1 = require("../kernel/default-kernel");
|
|
12
|
+
const server_registry_1 = require("../cluster/server-registry");
|
|
13
|
+
const kernel_1 = require("./kernel");
|
|
14
|
+
const discoveryService = new discovery_service_1.PythonDiscoveryService();
|
|
15
|
+
exports.discoveryService = discoveryService;
|
|
16
|
+
async function pythonRoutes(fastify) {
|
|
17
|
+
/**
|
|
18
|
+
* List all discovered Python environments
|
|
19
|
+
* Transforms to snake_case to match Python API format expected by frontend
|
|
20
|
+
*/
|
|
21
|
+
fastify.get('/python/environments', async (request, reply) => {
|
|
22
|
+
try {
|
|
23
|
+
const refresh = request.query.refresh === 'true';
|
|
24
|
+
const serverId = request.query.server_id;
|
|
25
|
+
const localServerId = server_registry_1.serverRegistry.getLocalServerId();
|
|
26
|
+
if (serverId && serverId !== localServerId && serverId !== 'local') {
|
|
27
|
+
const server = server_registry_1.serverRegistry.getServer(serverId);
|
|
28
|
+
if (!server) {
|
|
29
|
+
return reply.code(404).send({ detail: `Server not found: ${serverId}` });
|
|
30
|
+
}
|
|
31
|
+
if (server.status !== 'online') {
|
|
32
|
+
return reply.code(503).send({ detail: `Server is offline: ${serverId}` });
|
|
33
|
+
}
|
|
34
|
+
const response = await fetch(`${server.url}/api/python/environments?refresh=${refresh ? 'true' : 'false'}`, {
|
|
35
|
+
headers: process.env.NEBULA_CLUSTER_SECRET ? { 'X-Nebula-Cluster-Secret': process.env.NEBULA_CLUSTER_SECRET } : undefined,
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
|
39
|
+
return reply.code(response.status).send({ detail: error.detail || 'Failed to fetch Python environments' });
|
|
40
|
+
}
|
|
41
|
+
const data = await response.json();
|
|
42
|
+
return reply.send(data);
|
|
43
|
+
}
|
|
44
|
+
// Get Jupyter kernelspecs and transform to snake_case
|
|
45
|
+
const kernelspecs = (0, kernelspec_1.discoverKernelSpecs)(refresh).map(k => ({
|
|
46
|
+
name: k.name,
|
|
47
|
+
display_name: k.displayName,
|
|
48
|
+
language: k.language,
|
|
49
|
+
path: k.path,
|
|
50
|
+
python_path: k.argv?.[0] || null, // First element of argv is typically the Python executable
|
|
51
|
+
}));
|
|
52
|
+
const kernelspecNames = new Set(kernelspecs.map(k => k.name));
|
|
53
|
+
// Get discovered Python environments
|
|
54
|
+
const environments = await discoveryService.discover({ forceRefresh: refresh });
|
|
55
|
+
// Convert to snake_case and match with kernelspecs
|
|
56
|
+
const envObjects = environments.map(env => ({
|
|
57
|
+
path: env.path,
|
|
58
|
+
version: env.version,
|
|
59
|
+
display_name: env.displayName,
|
|
60
|
+
env_type: env.envType,
|
|
61
|
+
env_name: env.envName,
|
|
62
|
+
has_ipykernel: env.hasIpykernel,
|
|
63
|
+
externally_managed: env.externallyManaged,
|
|
64
|
+
install_hint: env.installHint,
|
|
65
|
+
kernel_name: env.kernelName && kernelspecNames.has(env.kernelName) ? env.kernelName : null,
|
|
66
|
+
}));
|
|
67
|
+
return reply.send({
|
|
68
|
+
kernelspecs,
|
|
69
|
+
environments: envObjects,
|
|
70
|
+
cache_info: discoveryService.getCacheInfo(),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
75
|
+
return reply.code(500).send({ detail: message });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* Install ipykernel and register a Python environment as a Jupyter kernel
|
|
80
|
+
*/
|
|
81
|
+
fastify.post('/python/install-kernel', async (request, reply) => {
|
|
82
|
+
try {
|
|
83
|
+
const { python_path, kernel_name, server_id } = request.body;
|
|
84
|
+
if (!python_path) {
|
|
85
|
+
return reply.code(400).send({ detail: 'python_path is required' });
|
|
86
|
+
}
|
|
87
|
+
const localServerId = server_registry_1.serverRegistry.getLocalServerId();
|
|
88
|
+
if (server_id && server_id !== localServerId && server_id !== 'local') {
|
|
89
|
+
const server = server_registry_1.serverRegistry.getServer(server_id);
|
|
90
|
+
if (!server) {
|
|
91
|
+
return reply.code(404).send({ detail: `Server not found: ${server_id}` });
|
|
92
|
+
}
|
|
93
|
+
if (server.status !== 'online') {
|
|
94
|
+
return reply.code(503).send({ detail: `Server is offline: ${server_id}` });
|
|
95
|
+
}
|
|
96
|
+
const response = await fetch(`${server.url}/api/python/install-kernel`, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: {
|
|
99
|
+
'Content-Type': 'application/json',
|
|
100
|
+
...(process.env.NEBULA_CLUSTER_SECRET ? { 'X-Nebula-Cluster-Secret': process.env.NEBULA_CLUSTER_SECRET } : {}),
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify({ python_path, kernel_name }),
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
// Forward the structured error (code + install_hint) so cluster clients
|
|
106
|
+
// get the same guidance as a local externally-managed env.
|
|
107
|
+
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
|
108
|
+
return reply.code(response.status).send({
|
|
109
|
+
detail: error.detail || 'Failed to install kernel',
|
|
110
|
+
code: error.code,
|
|
111
|
+
install_hint: error.install_hint,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const data = await response.json();
|
|
115
|
+
return reply.send(data);
|
|
116
|
+
}
|
|
117
|
+
const result = await discoveryService.installKernel(python_path, kernel_name);
|
|
118
|
+
// Invalidate caches so the new kernel is discovered without a restart.
|
|
119
|
+
(0, kernelspec_1.invalidateKernelspecCache)();
|
|
120
|
+
(0, default_kernel_1.invalidateDefaultKernelName)();
|
|
121
|
+
kernel_1.kernelService.refreshKernelSpecs();
|
|
122
|
+
// snake_case to match the frontend KernelService client contract.
|
|
123
|
+
return reply.send({
|
|
124
|
+
kernel_name: result.kernelName,
|
|
125
|
+
python_path: result.pythonPath,
|
|
126
|
+
message: result.message,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
// Structured provisioning errors carry a stable code + guidance hint so the
|
|
131
|
+
// UI can show actionable help instead of a raw traceback.
|
|
132
|
+
if (err instanceof types_1.KernelProvisionError) {
|
|
133
|
+
const status = err.code === 'python_not_found' ? 404
|
|
134
|
+
: err.code === 'externally_managed' || err.code === 'needs_ipykernel' ? 422
|
|
135
|
+
: 500;
|
|
136
|
+
return reply.code(status).send({ detail: err.message, code: err.code, install_hint: err.installHint });
|
|
137
|
+
}
|
|
138
|
+
if (err instanceof Error) {
|
|
139
|
+
if (err.message.includes('not found') || err.message.includes('ENOENT')) {
|
|
140
|
+
return reply.code(404).send({ detail: err.message });
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
return reply.code(500).send({ detail: err.message });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
return reply.code(500).send({ detail: 'Unknown error' });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
/**
|
|
152
|
+
* Force refresh the Python environment cache
|
|
153
|
+
*/
|
|
154
|
+
fastify.post('/python/refresh', async (request, reply) => {
|
|
155
|
+
try {
|
|
156
|
+
const serverId = request.query.server_id;
|
|
157
|
+
const localServerId = server_registry_1.serverRegistry.getLocalServerId();
|
|
158
|
+
if (serverId && serverId !== localServerId && serverId !== 'local') {
|
|
159
|
+
const server = server_registry_1.serverRegistry.getServer(serverId);
|
|
160
|
+
if (!server) {
|
|
161
|
+
return reply.code(404).send({ detail: `Server not found: ${serverId}` });
|
|
162
|
+
}
|
|
163
|
+
if (server.status !== 'online') {
|
|
164
|
+
return reply.code(503).send({ detail: `Server is offline: ${serverId}` });
|
|
165
|
+
}
|
|
166
|
+
const response = await fetch(`${server.url}/api/python/refresh`, {
|
|
167
|
+
headers: process.env.NEBULA_CLUSTER_SECRET ? { 'X-Nebula-Cluster-Secret': process.env.NEBULA_CLUSTER_SECRET } : undefined,
|
|
168
|
+
});
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
|
171
|
+
return reply.code(response.status).send({ detail: error.detail || 'Failed to refresh Python environments' });
|
|
172
|
+
}
|
|
173
|
+
const data = await response.json();
|
|
174
|
+
return reply.send(data);
|
|
175
|
+
}
|
|
176
|
+
const environments = await discoveryService.discover({ forceRefresh: true });
|
|
177
|
+
return reply.send({
|
|
178
|
+
count: environments.length,
|
|
179
|
+
cache_info: discoveryService.getCacheInfo(),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
184
|
+
return reply.code(500).send({ detail: message });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|