nebula-notebook 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +222 -0
  2. package/bin/nebula-notebook.js +33 -0
  3. package/dist/assets/index-C1h_sArD.css +32 -0
  4. package/dist/assets/index-CDSTBon8.js +658 -0
  5. package/dist/favicon.svg +11 -0
  6. package/dist/index.html +73 -0
  7. package/node-server/dist/app.d.ts +5 -0
  8. package/node-server/dist/app.js +38 -0
  9. package/node-server/dist/auth/auth-middleware.d.ts +24 -0
  10. package/node-server/dist/auth/auth-middleware.js +276 -0
  11. package/node-server/dist/auth/auth-service.d.ts +84 -0
  12. package/node-server/dist/auth/auth-service.js +265 -0
  13. package/node-server/dist/auth/index.d.ts +3 -0
  14. package/node-server/dist/auth/index.js +8 -0
  15. package/node-server/dist/cluster/client-registration.d.ts +43 -0
  16. package/node-server/dist/cluster/client-registration.js +217 -0
  17. package/node-server/dist/cluster/cluster-secret.d.ts +11 -0
  18. package/node-server/dist/cluster/cluster-secret.js +90 -0
  19. package/node-server/dist/cluster/kernel-proxy.d.ts +100 -0
  20. package/node-server/dist/cluster/kernel-proxy.js +361 -0
  21. package/node-server/dist/cluster/server-registry.d.ts +109 -0
  22. package/node-server/dist/cluster/server-registry.js +217 -0
  23. package/node-server/dist/config/output-limits.d.ts +7 -0
  24. package/node-server/dist/config/output-limits.js +10 -0
  25. package/node-server/dist/discovery/discovery-service.d.ts +198 -0
  26. package/node-server/dist/discovery/discovery-service.js +811 -0
  27. package/node-server/dist/discovery/index.d.ts +5 -0
  28. package/node-server/dist/discovery/index.js +21 -0
  29. package/node-server/dist/discovery/types.d.ts +48 -0
  30. package/node-server/dist/discovery/types.js +24 -0
  31. package/node-server/dist/fs/fs-service.d.ts +218 -0
  32. package/node-server/dist/fs/fs-service.js +1422 -0
  33. package/node-server/dist/fs/index.d.ts +5 -0
  34. package/node-server/dist/fs/index.js +21 -0
  35. package/node-server/dist/fs/types.d.ts +132 -0
  36. package/node-server/dist/fs/types.js +5 -0
  37. package/node-server/dist/index.d.ts +13 -0
  38. package/node-server/dist/index.js +556 -0
  39. package/node-server/dist/kernel/default-kernel.d.ts +5 -0
  40. package/node-server/dist/kernel/default-kernel.js +138 -0
  41. package/node-server/dist/kernel/index.d.ts +7 -0
  42. package/node-server/dist/kernel/index.js +23 -0
  43. package/node-server/dist/kernel/kernel-service.d.ts +290 -0
  44. package/node-server/dist/kernel/kernel-service.js +1714 -0
  45. package/node-server/dist/kernel/kernelspec.d.ts +29 -0
  46. package/node-server/dist/kernel/kernelspec.js +210 -0
  47. package/node-server/dist/kernel/session-store.d.ts +87 -0
  48. package/node-server/dist/kernel/session-store.js +303 -0
  49. package/node-server/dist/kernel/types.d.ts +143 -0
  50. package/node-server/dist/kernel/types.js +17 -0
  51. package/node-server/dist/llm/index.d.ts +5 -0
  52. package/node-server/dist/llm/index.js +21 -0
  53. package/node-server/dist/llm/llm-service.d.ts +77 -0
  54. package/node-server/dist/llm/llm-service.js +454 -0
  55. package/node-server/dist/llm/types.d.ts +40 -0
  56. package/node-server/dist/llm/types.js +15 -0
  57. package/node-server/dist/notebook/cell-metadata.d.ts +27 -0
  58. package/node-server/dist/notebook/cell-metadata.js +76 -0
  59. package/node-server/dist/notebook/headless-handler.d.ts +127 -0
  60. package/node-server/dist/notebook/headless-handler.js +1530 -0
  61. package/node-server/dist/notebook/notebook-websocket.d.ts +12 -0
  62. package/node-server/dist/notebook/notebook-websocket.js +103 -0
  63. package/node-server/dist/notebook/operation-router.d.ts +115 -0
  64. package/node-server/dist/notebook/operation-router.js +641 -0
  65. package/node-server/dist/notebook/undoRedoManager.d.ts +194 -0
  66. package/node-server/dist/notebook/undoRedoManager.js +558 -0
  67. package/node-server/dist/output/display-data.d.ts +14 -0
  68. package/node-server/dist/output/display-data.js +134 -0
  69. package/node-server/dist/resources/resource-service.d.ts +69 -0
  70. package/node-server/dist/resources/resource-service.js +363 -0
  71. package/node-server/dist/routes/auth.d.ts +5 -0
  72. package/node-server/dist/routes/auth.js +61 -0
  73. package/node-server/dist/routes/cluster.d.ts +7 -0
  74. package/node-server/dist/routes/cluster.js +94 -0
  75. package/node-server/dist/routes/fs.d.ts +7 -0
  76. package/node-server/dist/routes/fs.js +392 -0
  77. package/node-server/dist/routes/kernel.d.ts +13 -0
  78. package/node-server/dist/routes/kernel.js +637 -0
  79. package/node-server/dist/routes/llm.d.ts +8 -0
  80. package/node-server/dist/routes/llm.js +105 -0
  81. package/node-server/dist/routes/notebook.d.ts +10 -0
  82. package/node-server/dist/routes/notebook.js +335 -0
  83. package/node-server/dist/routes/python.d.ts +8 -0
  84. package/node-server/dist/routes/python.js +187 -0
  85. package/node-server/dist/routes/resources.d.ts +7 -0
  86. package/node-server/dist/routes/resources.js +77 -0
  87. package/node-server/dist/scripts/show-auth-qr.d.ts +1 -0
  88. package/node-server/dist/scripts/show-auth-qr.js +81 -0
  89. package/node-server/dist/terminal/pty-manager.d.ts +100 -0
  90. package/node-server/dist/terminal/pty-manager.js +246 -0
  91. package/node-server/dist/terminal/server.d.ts +19 -0
  92. package/node-server/dist/terminal/server.js +254 -0
  93. package/node-server/dist/terminal/types.d.ts +50 -0
  94. package/node-server/dist/terminal/types.js +9 -0
  95. package/node-server/package.json +45 -0
  96. package/package.json +99 -0
  97. package/scripts/postinstall.cjs +26 -0
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ /**
3
+ * Server Registry
4
+ *
5
+ * Manages registration of peer servers in a Nebula cluster.
6
+ * Peer servers can register with a main server to enable
7
+ * cross-server kernel access.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.serverRegistry = void 0;
11
+ const cluster_secret_1 = require("./cluster-secret");
12
+ const HEARTBEAT_TIMEOUT_MS = 90_000; // 90 seconds (3 missed heartbeats at 30s interval)
13
+ const CLEANUP_INTERVAL_MS = 30_000; // Check for stale servers every 30s
14
+ class ServerRegistry {
15
+ servers = new Map();
16
+ clusterSecret = null;
17
+ cleanupInterval = null;
18
+ localServerId = 'local';
19
+ constructor() {
20
+ // Load initial cluster secret from environment or disk
21
+ this.clusterSecret = process.env.NEBULA_CLUSTER_SECRET || (0, cluster_secret_1.readClusterSecret)() || null;
22
+ // Start cleanup interval
23
+ this.startCleanupInterval();
24
+ }
25
+ /**
26
+ * Set the local server's ID (called on startup)
27
+ */
28
+ setLocalServerId(id) {
29
+ this.localServerId = id;
30
+ }
31
+ /**
32
+ * Get the local server's ID
33
+ */
34
+ getLocalServerId() {
35
+ return this.localServerId;
36
+ }
37
+ /**
38
+ * Validate registration secret
39
+ */
40
+ getClusterSecret() {
41
+ const envSecret = process.env.NEBULA_CLUSTER_SECRET;
42
+ // If the env var is explicitly set (even to an empty string), treat it as the
43
+ // source of truth. This allows users/tests to intentionally disable a disk secret
44
+ // without relying on filesystem state.
45
+ if (envSecret !== undefined) {
46
+ this.clusterSecret = envSecret || null;
47
+ return this.clusterSecret;
48
+ }
49
+ // Disk secret is cached defensively: if the file can't be read, don't clear an
50
+ // existing secret to avoid accidentally dropping auth due to transient errors.
51
+ const diskSecret = (0, cluster_secret_1.readClusterSecret)() || null;
52
+ if (diskSecret && diskSecret !== this.clusterSecret) {
53
+ this.clusterSecret = diskSecret;
54
+ }
55
+ return this.clusterSecret;
56
+ }
57
+ validateSecret(providedSecret) {
58
+ const clusterSecret = this.getClusterSecret();
59
+ // If no cluster secret configured, allow all registrations (dev mode)
60
+ if (!clusterSecret) {
61
+ return true;
62
+ }
63
+ return providedSecret === clusterSecret;
64
+ }
65
+ /**
66
+ * Generate server ID from host and port
67
+ */
68
+ generateServerId(host, port) {
69
+ return `${host}:${port}`;
70
+ }
71
+ /**
72
+ * Register a peer server
73
+ */
74
+ register(request) {
75
+ // Validate secret
76
+ if (!this.validateSecret(request.secret)) {
77
+ return { success: false, error: 'Invalid cluster secret' };
78
+ }
79
+ const serverId = this.generateServerId(request.host, request.port);
80
+ const now = Date.now();
81
+ // Check if already registered
82
+ const existing = this.servers.get(serverId);
83
+ if (existing) {
84
+ // Update existing registration
85
+ existing.lastHeartbeat = now;
86
+ existing.status = 'online';
87
+ existing.name = request.name || existing.name;
88
+ if (request.resources) {
89
+ existing.resources = request.resources;
90
+ }
91
+ console.log(`[ServerRegistry] Updated registration for ${serverId}`);
92
+ return { success: true, serverId };
93
+ }
94
+ // New registration
95
+ const server = {
96
+ id: serverId,
97
+ host: request.host,
98
+ port: request.port,
99
+ name: request.name,
100
+ url: `http://${request.host}:${request.port}`,
101
+ registeredAt: now,
102
+ lastHeartbeat: now,
103
+ status: 'online',
104
+ resources: request.resources,
105
+ };
106
+ this.servers.set(serverId, server);
107
+ console.log(`[ServerRegistry] Registered new server: ${serverId} (${request.name || 'unnamed'})`);
108
+ return { success: true, serverId };
109
+ }
110
+ /**
111
+ * Unregister a peer server
112
+ */
113
+ unregister(serverId) {
114
+ const removed = this.servers.delete(serverId);
115
+ if (removed) {
116
+ console.log(`[ServerRegistry] Unregistered server: ${serverId}`);
117
+ }
118
+ return removed;
119
+ }
120
+ /**
121
+ * Record heartbeat from a peer server
122
+ */
123
+ heartbeat(serverId, resources) {
124
+ const server = this.servers.get(serverId);
125
+ if (!server) {
126
+ return false;
127
+ }
128
+ server.lastHeartbeat = Date.now();
129
+ server.status = 'online';
130
+ if (resources) {
131
+ server.resources = resources;
132
+ }
133
+ return true;
134
+ }
135
+ /**
136
+ * Get a specific peer server
137
+ */
138
+ getServer(serverId) {
139
+ return this.servers.get(serverId);
140
+ }
141
+ /**
142
+ * Get all registered peer servers
143
+ */
144
+ getAllServers() {
145
+ return Array.from(this.servers.values());
146
+ }
147
+ /**
148
+ * Get all online peer servers
149
+ */
150
+ getOnlineServers() {
151
+ return this.getAllServers().filter(s => s.status === 'online');
152
+ }
153
+ /**
154
+ * Check server health and update status
155
+ */
156
+ checkServerHealth() {
157
+ const now = Date.now();
158
+ for (const server of this.servers.values()) {
159
+ if (now - server.lastHeartbeat > HEARTBEAT_TIMEOUT_MS) {
160
+ if (server.status === 'online') {
161
+ console.log(`[ServerRegistry] Server ${server.id} marked offline (heartbeat timeout)`);
162
+ server.status = 'offline';
163
+ }
164
+ }
165
+ }
166
+ }
167
+ /**
168
+ * Start the cleanup interval
169
+ */
170
+ startCleanupInterval() {
171
+ if (this.cleanupInterval) {
172
+ clearInterval(this.cleanupInterval);
173
+ }
174
+ this.cleanupInterval = setInterval(() => {
175
+ this.checkServerHealth();
176
+ }, CLEANUP_INTERVAL_MS);
177
+ }
178
+ /**
179
+ * Stop the cleanup interval (for shutdown)
180
+ */
181
+ shutdown() {
182
+ if (this.cleanupInterval) {
183
+ clearInterval(this.cleanupInterval);
184
+ this.cleanupInterval = null;
185
+ }
186
+ }
187
+ /**
188
+ * Get cluster info for API response
189
+ */
190
+ getClusterInfo(localResources) {
191
+ const servers = this.getAllServers().map(s => ({
192
+ id: s.id,
193
+ name: s.name,
194
+ url: s.url,
195
+ status: s.status,
196
+ isLocal: false,
197
+ resources: s.resources,
198
+ }));
199
+ // Add local server at the beginning
200
+ servers.unshift({
201
+ id: this.localServerId,
202
+ name: 'Local',
203
+ url: '', // Local doesn't need URL
204
+ status: 'online',
205
+ isLocal: true,
206
+ resources: localResources,
207
+ });
208
+ return {
209
+ localServerId: this.localServerId,
210
+ peerCount: this.servers.size,
211
+ onlineCount: this.getOnlineServers().length,
212
+ servers,
213
+ };
214
+ }
215
+ }
216
+ // Global singleton
217
+ exports.serverRegistry = new ServerRegistry();
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Kernel output buffer limits (keep in sync with frontend display.config.ts)
3
+ */
4
+ export declare const MAX_OUTPUT_LINES = 10000;
5
+ export declare const MAX_OUTPUT_CHARS = 100000000;
6
+ export declare const MAX_OUTPUT_LINES_ERROR = 10000;
7
+ export declare const MAX_OUTPUT_CHARS_ERROR = 100000000;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * Kernel output buffer limits (keep in sync with frontend display.config.ts)
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MAX_OUTPUT_CHARS_ERROR = exports.MAX_OUTPUT_LINES_ERROR = exports.MAX_OUTPUT_CHARS = exports.MAX_OUTPUT_LINES = void 0;
7
+ exports.MAX_OUTPUT_LINES = 10000;
8
+ exports.MAX_OUTPUT_CHARS = 100_000_000;
9
+ exports.MAX_OUTPUT_LINES_ERROR = 10000;
10
+ exports.MAX_OUTPUT_CHARS_ERROR = 100_000_000;
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Python Discovery Service
3
+ *
4
+ * Discovers Python environments on the system (conda, pyenv, venv, system).
5
+ * Node.js port of the Python PythonDiscoveryService.
6
+ */
7
+ import { PythonEnvironment, PythonEnvType, CacheInfo, InstallKernelResult } from './types';
8
+ export interface DiscoveryServiceOptions {
9
+ cacheFile?: string;
10
+ cacheTtlHours?: number;
11
+ versionCheckTimeoutMs?: number;
12
+ ipykernelCheckTimeoutMs?: number;
13
+ condaListTimeoutMs?: number;
14
+ kernelInstallTimeoutMs?: number;
15
+ registrationTimeoutMs?: number;
16
+ }
17
+ export declare class PythonDiscoveryService {
18
+ private cache;
19
+ private cacheTimestamp;
20
+ private cacheFile;
21
+ private cacheTtlHours;
22
+ private versionCheckTimeoutMs;
23
+ private ipykernelCheckTimeoutMs;
24
+ private condaListTimeoutMs;
25
+ private kernelInstallTimeoutMs;
26
+ private registrationTimeoutMs;
27
+ private backgroundRefreshInProgress;
28
+ constructor(options?: DiscoveryServiceOptions);
29
+ /**
30
+ * Load cache from disk
31
+ */
32
+ private loadCache;
33
+ /**
34
+ * Save environments to cache
35
+ */
36
+ saveToCache(environments: Record<string, PythonEnvironment>): void;
37
+ /**
38
+ * Get environments from cache (returns null if empty)
39
+ */
40
+ getFromCache(): Record<string, PythonEnvironment> | null;
41
+ /**
42
+ * Check if cache is still valid
43
+ */
44
+ isCacheValid(): boolean;
45
+ /**
46
+ * Get cache info
47
+ */
48
+ getCacheInfo(): CacheInfo & {
49
+ refreshing: boolean;
50
+ };
51
+ /**
52
+ * Run a command asynchronously with timeout
53
+ */
54
+ private runCommand;
55
+ /**
56
+ * Generate display name for an environment
57
+ */
58
+ generateDisplayName(version: string, envType: PythonEnvType, envName: string | null): string;
59
+ /**
60
+ * Sort environments by type priority and name
61
+ */
62
+ sortEnvironments(envs: PythonEnvironment[]): PythonEnvironment[];
63
+ /**
64
+ * Check if a python path exists
65
+ */
66
+ pythonExists(pythonPath: string): boolean;
67
+ /**
68
+ * Get common conda base paths to search
69
+ */
70
+ getCondaBasePaths(): string[];
71
+ /**
72
+ * Get common system python paths
73
+ */
74
+ getSystemPythonPaths(): string[];
75
+ /**
76
+ * Get pyenv versions path
77
+ */
78
+ getPyenvVersionsPath(): string;
79
+ /**
80
+ * Get common virtualenv paths
81
+ */
82
+ getVirtualenvPaths(): string[];
83
+ /**
84
+ * Generate a unique kernel name from path
85
+ */
86
+ generateKernelName(pythonPath: string, version: string): string;
87
+ /**
88
+ * Simple string hash function
89
+ */
90
+ private hashString;
91
+ /**
92
+ * Parse Python version from version string
93
+ */
94
+ parseVersionString(versionOutput: string): string;
95
+ /**
96
+ * Get Python version from executable
97
+ */
98
+ private getPythonVersion;
99
+ /**
100
+ * Check if ipykernel is installed
101
+ */
102
+ private checkIpykernel;
103
+ /**
104
+ * Probe an interpreter for the two facts that drive kernel provisioning, in a
105
+ * single Python invocation (keeps discovery cheap — one spawn per env):
106
+ * - whether `ipykernel` is importable
107
+ * - whether the interpreter is PEP 668 "externally managed" (an
108
+ * `EXTERNALLY-MANAGED` marker beside the stdlib), which blocks pip installs.
109
+ *
110
+ * Detecting the marker file is capability-based and stable: we ask Python
111
+ * itself rather than fingerprinting tool names by path, so it keeps working as
112
+ * new package managers (uv, pixi, …) appear.
113
+ */
114
+ private probeEnvironment;
115
+ /**
116
+ * Refine an environment's type using stable path markers that the coarse
117
+ * discovery scans miss. Only used to pick a better label / guidance hint —
118
+ * the safe-vs-unsafe install decision relies on `externallyManaged`, not this.
119
+ *
120
+ * A venv is left as-is: its `bin/python` symlinks to (and `sysconfig` resolves
121
+ * to) its base interpreter, so following the symlink to e.g. a uv-managed build
122
+ * must NOT relabel the venv itself as "uv".
123
+ */
124
+ private classifyEnvType;
125
+ /**
126
+ * Build a copy-pasteable command that makes `ipykernel` available for an env,
127
+ * tailored to the detected ecosystem. After the user runs it (and clicks
128
+ * Refresh), the env shows up with ipykernel and Nebula offers one-click
129
+ * Register — Nebula never installs packages itself.
130
+ *
131
+ * Returns null when ipykernel is already present (nothing to do but Register).
132
+ */
133
+ private buildInstallHint;
134
+ /**
135
+ * Find conda environments
136
+ */
137
+ private findCondaEnvs;
138
+ /**
139
+ * Find pyenv Python versions
140
+ */
141
+ private findPyenvVersions;
142
+ /**
143
+ * Find virtualenvs in common locations
144
+ */
145
+ private findVirtualenvs;
146
+ /**
147
+ * Find system Python installations
148
+ */
149
+ private findSystemPythons;
150
+ /**
151
+ * Enrich a candidate with version and ipykernel info
152
+ */
153
+ private enrichEnvironment;
154
+ /**
155
+ * Perform the actual discovery (internal, always runs full scan)
156
+ */
157
+ private performDiscovery;
158
+ /**
159
+ * Trigger background refresh (non-blocking)
160
+ */
161
+ private triggerBackgroundRefresh;
162
+ /**
163
+ * Discover all Python environments
164
+ *
165
+ * Uses stale-while-revalidate pattern:
166
+ * - If cache exists (even stale): return immediately, refresh in background if stale
167
+ * - If no cache: block and perform full discovery
168
+ * - If forceRefresh: block and perform full discovery
169
+ */
170
+ discover(options?: {
171
+ forceRefresh?: boolean;
172
+ }): Promise<PythonEnvironment[]>;
173
+ /**
174
+ * Check if background refresh is in progress
175
+ */
176
+ isRefreshing(): boolean;
177
+ /**
178
+ * Register a Python environment as a Jupyter kernel.
179
+ *
180
+ * Policy (capability-based, see ADR in the kernel onboarding flow):
181
+ * - If `ipykernel` is already importable → just register the kernelspec.
182
+ * This is the safe, universal path and works for every ecosystem.
183
+ * - If `ipykernel` is missing and the interpreter is PEP 668 externally
184
+ * managed → refuse, with a `needs_ipykernel`/`externally_managed` code so
185
+ * the UI shows guidance instead of a raw pip traceback. Nebula does not
186
+ * install into managed interpreters.
187
+ * - If `ipykernel` is missing but the interpreter is writable (a plain venv,
188
+ * conda env, …) → install it, then register. This keeps the API usable for
189
+ * deliberate callers (MCP, scripts); the UI funnels everything through the
190
+ * register-only path.
191
+ */
192
+ installKernel(pythonPath: string, kernelName?: string): Promise<InstallKernelResult>;
193
+ /** Extract combined stdout+stderr text from a child_process error. */
194
+ private errorOutput;
195
+ /** Detect PEP 668 "externally managed" refusals generically (not tool-specific). */
196
+ private isExternallyManagedError;
197
+ }
198
+ export declare const pythonDiscovery: PythonDiscoveryService;