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,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stripAutoplayFromHtml = stripAutoplayFromHtml;
4
+ exports.normalizeMimeValue = normalizeMimeValue;
5
+ exports.normalizeMimeBundle = normalizeMimeBundle;
6
+ exports.pickPreferredMimeType = pickPreferredMimeType;
7
+ exports.buildDisplayOutput = buildDisplayOutput;
8
+ exports.convertMimeBundleToJupyter = convertMimeBundleToJupyter;
9
+ const PREFERRED_MIME_TYPES = [
10
+ 'application/vnd.nebula.web+json',
11
+ 'application/vnd.plotly.v1+json',
12
+ 'text/html',
13
+ 'image/png',
14
+ 'text/plain',
15
+ ];
16
+ function stripAutoplayFromHtml(html) {
17
+ if (!html.includes('autoplay'))
18
+ return html;
19
+ return html.replace(/(<(?:audio|video)\b[^>]*?)\s+autoplay(?:=["'][^"']*["'])?/gi, '$1');
20
+ }
21
+ function isJsonValue(value, depth = 0) {
22
+ if (value === null ||
23
+ typeof value === 'string' ||
24
+ typeof value === 'number' ||
25
+ typeof value === 'boolean') {
26
+ return true;
27
+ }
28
+ // Assume deeply-nested structures are valid rather than risk a stack overflow.
29
+ // Plotly figures can nest 10+ levels deep in their default templates.
30
+ if (depth > 64) {
31
+ return typeof value === 'object';
32
+ }
33
+ if (Array.isArray(value)) {
34
+ return value.every((v) => isJsonValue(v, depth + 1));
35
+ }
36
+ if (typeof value === 'object') {
37
+ return Object.values(value).every((v) => isJsonValue(v, depth + 1));
38
+ }
39
+ return false;
40
+ }
41
+ function normalizeMimeValue(mimeType, value) {
42
+ let normalized = value;
43
+ if (Array.isArray(normalized) && normalized.every((item) => typeof item === 'string')) {
44
+ normalized = normalized.join('');
45
+ }
46
+ if (mimeType === 'text/html' && typeof normalized === 'string') {
47
+ normalized = stripAutoplayFromHtml(normalized);
48
+ }
49
+ // JSON-based MIME types (plotly, nebula-web, etc.) arrive from JSON.parse
50
+ // and are inherently valid JSON values. Skip the expensive recursive check
51
+ // which can stack-overflow on deeply-nested Plotly templates.
52
+ if (mimeType.endsWith('+json') && typeof normalized === 'object' && normalized !== null) {
53
+ return normalized;
54
+ }
55
+ return isJsonValue(normalized) ? normalized : null;
56
+ }
57
+ function normalizeMimeBundle(data) {
58
+ const bundle = {};
59
+ for (const [mimeType, value] of Object.entries(data)) {
60
+ const normalized = normalizeMimeValue(mimeType, value);
61
+ if (normalized !== null) {
62
+ bundle[mimeType] = normalized;
63
+ }
64
+ else if (value !== undefined) {
65
+ console.warn(`[display-data] Dropped MIME type "${mimeType}": normalizeMimeValue returned null (value type: ${typeof value}, isArray: ${Array.isArray(value)})`);
66
+ }
67
+ }
68
+ return bundle;
69
+ }
70
+ function pickPreferredMimeType(bundle) {
71
+ for (const mimeType of PREFERRED_MIME_TYPES) {
72
+ if (mimeType in bundle)
73
+ return mimeType;
74
+ }
75
+ const [firstMimeType] = Object.keys(bundle);
76
+ return firstMimeType ?? null;
77
+ }
78
+ function stringifyMimeValue(value) {
79
+ if (typeof value === 'string')
80
+ return value;
81
+ return JSON.stringify(value, null, 2);
82
+ }
83
+ function classifyOutputType(bundle, preferredMimeType) {
84
+ const mimeTypes = Object.keys(bundle);
85
+ if (preferredMimeType === 'image/png' &&
86
+ mimeTypes.every((mimeType) => mimeType === 'image/png' || mimeType === 'text/plain')) {
87
+ return 'image';
88
+ }
89
+ if (preferredMimeType === 'text/html' &&
90
+ mimeTypes.every((mimeType) => mimeType === 'text/html' || mimeType === 'text/plain')) {
91
+ return 'html';
92
+ }
93
+ if (preferredMimeType === 'text/plain' && mimeTypes.length === 1) {
94
+ return 'stdout';
95
+ }
96
+ return 'display_data';
97
+ }
98
+ function getDisplayContent(bundle, preferredMimeType, outputType) {
99
+ // For image and html types the content field IS the rendered data (base64 / markup),
100
+ // so we must return the preferred MIME value, not the text/plain fallback.
101
+ // For display_data / stdout the text/plain fallback is more useful (shown as error
102
+ // fallback text in plotly/nebula-web renderers).
103
+ if (outputType === 'image' || outputType === 'html') {
104
+ const preferredValue = bundle[preferredMimeType];
105
+ return preferredValue === undefined ? '' : stringifyMimeValue(preferredValue);
106
+ }
107
+ const fallbackText = bundle['text/plain'];
108
+ if (preferredMimeType !== 'text/plain' && fallbackText !== undefined) {
109
+ return stringifyMimeValue(fallbackText);
110
+ }
111
+ const preferredValue = bundle[preferredMimeType];
112
+ return preferredValue === undefined ? '' : stringifyMimeValue(preferredValue);
113
+ }
114
+ function buildDisplayOutput(data, metadata) {
115
+ const mimeBundle = normalizeMimeBundle(data);
116
+ const preferredMimeType = pickPreferredMimeType(mimeBundle);
117
+ if (!preferredMimeType) {
118
+ return null;
119
+ }
120
+ const normalizedMetadata = metadata && isJsonValue(metadata)
121
+ ? metadata
122
+ : undefined;
123
+ const type = classifyOutputType(mimeBundle, preferredMimeType);
124
+ return {
125
+ type,
126
+ content: getDisplayContent(mimeBundle, preferredMimeType, type),
127
+ mimeBundle,
128
+ metadata: normalizedMetadata,
129
+ preferredMimeType,
130
+ };
131
+ }
132
+ function convertMimeBundleToJupyter(bundle) {
133
+ return { ...bundle };
134
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Resource Service
3
+ *
4
+ * Collects system resources (RAM, GPU) with defensive timeouts.
5
+ * Resource collection is OPTIONAL and must NEVER block core functionality.
6
+ */
7
+ export interface GPUDevice {
8
+ index: number;
9
+ name: string;
10
+ memoryUsed: number;
11
+ memoryTotal: number;
12
+ utilization?: number;
13
+ temperature?: number;
14
+ }
15
+ export interface GPUInfo {
16
+ vendor: 'nvidia' | 'amd';
17
+ devices: GPUDevice[];
18
+ totalUsed: number;
19
+ totalMemory: number;
20
+ }
21
+ export interface RAMInfo {
22
+ used: number;
23
+ total: number;
24
+ percent: number;
25
+ }
26
+ export interface ServerResources {
27
+ hostname: string;
28
+ ram: RAMInfo;
29
+ gpus: GPUInfo | null;
30
+ gpuError?: 'timeout' | 'not_found' | 'parse_error' | 'command_failed';
31
+ collectedAt: number;
32
+ }
33
+ /**
34
+ * Resource Service - Singleton
35
+ *
36
+ * Provides cached, non-blocking access to system resources.
37
+ */
38
+ declare class ResourceService {
39
+ private cache;
40
+ private cacheTime;
41
+ private collecting;
42
+ private hostname;
43
+ private hasLoggedOnce;
44
+ constructor();
45
+ /**
46
+ * Get resources - NEVER blocks, returns cached or empty
47
+ * Triggers async collection if cache is stale
48
+ */
49
+ getResources(): ServerResources;
50
+ /**
51
+ * Force refresh - waits for collection but still has timeout protection
52
+ * Use sparingly (e.g., on explicit user request)
53
+ */
54
+ refreshResources(): Promise<ServerResources>;
55
+ /**
56
+ * Check if cached data is stale
57
+ */
58
+ isStale(): boolean;
59
+ /**
60
+ * Get empty resources (fallback)
61
+ */
62
+ private getEmptyResources;
63
+ /**
64
+ * Async collection - runs in background, updates cache
65
+ */
66
+ private collectAsync;
67
+ }
68
+ export declare function getResourceService(): ResourceService;
69
+ export { ResourceService };
@@ -0,0 +1,363 @@
1
+ "use strict";
2
+ /**
3
+ * Resource Service
4
+ *
5
+ * Collects system resources (RAM, GPU) with defensive timeouts.
6
+ * Resource collection is OPTIONAL and must NEVER block core functionality.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ResourceService = void 0;
13
+ exports.getResourceService = getResourceService;
14
+ const child_process_1 = require("child_process");
15
+ const util_1 = require("util");
16
+ const os_1 = __importDefault(require("os"));
17
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
18
+ // Constants
19
+ const GPU_COMMAND_TIMEOUT_MS = 3000; // 3 seconds - kill if hung
20
+ const CACHE_TTL_MS = 30000; // 30 seconds cache
21
+ const STALE_THRESHOLD_MS = 60000; // 60 seconds before marking stale
22
+ /**
23
+ * Execute command with strict timeout
24
+ * Returns null on any failure - never throws
25
+ * Silent on command-not-found errors (expected on systems without GPU tools)
26
+ */
27
+ async function execWithTimeout(cmd, timeoutMs = GPU_COMMAND_TIMEOUT_MS) {
28
+ try {
29
+ const { stdout, stderr } = await execAsync(cmd, {
30
+ timeout: timeoutMs,
31
+ killSignal: 'SIGKILL',
32
+ maxBuffer: 1024 * 1024, // 1MB buffer
33
+ });
34
+ return { stdout: stdout.trim(), error: stderr?.trim() };
35
+ }
36
+ catch (err) {
37
+ if (err.killed) {
38
+ // Only log actual timeouts - these are important
39
+ console.warn(`[ResourceService] Command timed out after ${timeoutMs}ms: ${cmd.split(' ')[0]}`);
40
+ }
41
+ // Don't log command-not-found or other errors - expected on systems without GPU tools
42
+ return null;
43
+ }
44
+ }
45
+ /**
46
+ * Collect RAM info using Node.js os module (always available, instant)
47
+ */
48
+ function collectRAM() {
49
+ const total = os_1.default.totalmem();
50
+ const free = os_1.default.freemem();
51
+ const used = total - free;
52
+ return {
53
+ used: Math.round((used / (1024 ** 3)) * 100) / 100, // GB, 2 decimals
54
+ total: Math.round((total / (1024 ** 3)) * 100) / 100, // GB, 2 decimals
55
+ percent: Math.round((used / total) * 100),
56
+ };
57
+ }
58
+ /**
59
+ * Parse nvidia-smi CSV output
60
+ */
61
+ function parseNvidiaSmi(stdout) {
62
+ try {
63
+ const lines = stdout.trim().split('\n').filter(line => line.trim());
64
+ if (lines.length === 0)
65
+ return null;
66
+ const devices = [];
67
+ let totalUsed = 0;
68
+ let totalMemory = 0;
69
+ for (const line of lines) {
70
+ // Format: index, name, memory.used [MiB], memory.total [MiB], utilization.gpu [%], temperature.gpu
71
+ const parts = line.split(',').map(s => s.trim());
72
+ if (parts.length < 4)
73
+ continue;
74
+ const index = parseInt(parts[0], 10);
75
+ const name = parts[1];
76
+ const memUsedMB = parseInt(parts[2], 10);
77
+ const memTotalMB = parseInt(parts[3], 10);
78
+ const utilization = parts[4] ? parseInt(parts[4], 10) : undefined;
79
+ const temperature = parts[5] ? parseInt(parts[5], 10) : undefined;
80
+ if (isNaN(index) || isNaN(memUsedMB) || isNaN(memTotalMB))
81
+ continue;
82
+ const memUsedGB = Math.round((memUsedMB / 1024) * 100) / 100;
83
+ const memTotalGB = Math.round((memTotalMB / 1024) * 100) / 100;
84
+ devices.push({
85
+ index,
86
+ name,
87
+ memoryUsed: memUsedGB,
88
+ memoryTotal: memTotalGB,
89
+ utilization: isNaN(utilization) ? undefined : utilization,
90
+ temperature: isNaN(temperature) ? undefined : temperature,
91
+ });
92
+ totalUsed += memUsedGB;
93
+ totalMemory += memTotalGB;
94
+ }
95
+ if (devices.length === 0)
96
+ return null;
97
+ return {
98
+ vendor: 'nvidia',
99
+ devices,
100
+ totalUsed: Math.round(totalUsed * 100) / 100,
101
+ totalMemory: Math.round(totalMemory * 100) / 100,
102
+ };
103
+ }
104
+ catch (err) {
105
+ console.warn('[ResourceService] Failed to parse nvidia-smi output:', err);
106
+ return null;
107
+ }
108
+ }
109
+ /**
110
+ * Parse rocm-smi output for AMD GPUs
111
+ * Handles multiple output formats across rocm-smi versions
112
+ */
113
+ function parseRocmSmi(stdout) {
114
+ try {
115
+ const devices = [];
116
+ const deviceMap = new Map();
117
+ const lines = stdout.trim().split('\n');
118
+ for (const line of lines) {
119
+ // Format 1: GPU[0] : vram Total Memory (B): 17163091968
120
+ const gpuBracketMatch = line.match(/GPU\[(\d+)\]/);
121
+ if (gpuBracketMatch) {
122
+ const index = parseInt(gpuBracketMatch[1], 10);
123
+ if (!deviceMap.has(index)) {
124
+ deviceMap.set(index, { index, name: `AMD GPU ${index}` });
125
+ }
126
+ const device = deviceMap.get(index);
127
+ // Match memory values (in bytes) - handles "Total Memory" and "Used Memory"
128
+ if (line.includes('Total Memory') && !line.includes('Used')) {
129
+ const match = line.match(/:\s*(\d+)\s*$/);
130
+ if (match) {
131
+ device.memoryTotal = Math.round((parseInt(match[1], 10) / (1024 ** 3)) * 100) / 100;
132
+ }
133
+ }
134
+ else if (line.includes('Used Memory') || line.includes('Total Used')) {
135
+ const match = line.match(/:\s*(\d+)\s*$/);
136
+ if (match) {
137
+ device.memoryUsed = Math.round((parseInt(match[1], 10) / (1024 ** 3)) * 100) / 100;
138
+ }
139
+ }
140
+ continue;
141
+ }
142
+ // Format 2: Table format with GPU index in first column
143
+ // GPU Temp AvgPwr SCLK MCLK Fan Perf PwrCap VRAM% GPU%
144
+ // 0 45c 35.0W 300Mhz 1200Mhz 0% auto 250.0W 5% 0%
145
+ const tableMatch = line.match(/^(\d+)\s+\d+c/);
146
+ if (tableMatch) {
147
+ const index = parseInt(tableMatch[1], 10);
148
+ if (!deviceMap.has(index)) {
149
+ deviceMap.set(index, { index, name: `AMD GPU ${index}` });
150
+ }
151
+ // Extract VRAM% if present
152
+ const vramMatch = line.match(/(\d+)%\s+\d+%\s*$/);
153
+ if (vramMatch) {
154
+ const device = deviceMap.get(index);
155
+ // We only get percentage, not absolute values in this format
156
+ device.utilization = parseInt(vramMatch[1], 10);
157
+ }
158
+ }
159
+ }
160
+ let totalUsed = 0;
161
+ let totalMemory = 0;
162
+ for (const [, device] of deviceMap) {
163
+ // Accept device if we have both memory values, or at least an index
164
+ if (device.memoryUsed !== undefined && device.memoryTotal !== undefined) {
165
+ devices.push(device);
166
+ totalUsed += device.memoryUsed;
167
+ totalMemory += device.memoryTotal;
168
+ }
169
+ }
170
+ if (devices.length === 0)
171
+ return null;
172
+ return {
173
+ vendor: 'amd',
174
+ devices: devices.sort((a, b) => a.index - b.index),
175
+ totalUsed: Math.round(totalUsed * 100) / 100,
176
+ totalMemory: Math.round(totalMemory * 100) / 100,
177
+ };
178
+ }
179
+ catch {
180
+ // Silent failure - parsing errors are not critical
181
+ return null;
182
+ }
183
+ }
184
+ /**
185
+ * Parse rocm-smi --showproductname output to get GPU names
186
+ * Updates devices array in place
187
+ * Format: GPU[0] : Card Series: AMD Instinct MI300X
188
+ */
189
+ function parseRocmSmiNames(stdout, devices) {
190
+ try {
191
+ const lines = stdout.trim().split('\n');
192
+ for (const line of lines) {
193
+ // Match "GPU[X] : Card Series: <name>" with flexible whitespace
194
+ const match = line.match(/GPU\[(\d+)\]\s*:\s*Card\s+Series\s*:\s*(.+)/i);
195
+ if (match) {
196
+ const index = parseInt(match[1], 10);
197
+ const name = match[2].trim();
198
+ // Find device and update name
199
+ const device = devices.find(d => d.index === index);
200
+ if (device && name) {
201
+ device.name = name;
202
+ }
203
+ }
204
+ }
205
+ }
206
+ catch {
207
+ // Silent - names are optional
208
+ }
209
+ }
210
+ // Cache for AMD GPU names (they don't change)
211
+ let amdGpuNamesCache = null;
212
+ /**
213
+ * Collect GPU info - tries nvidia-smi first, then rocm-smi
214
+ * Returns null if no GPU or collection fails
215
+ * Only reports errors for actual problems (timeouts), not for missing tools
216
+ */
217
+ async function collectGPUs() {
218
+ // Try NVIDIA first (more common)
219
+ const nvidiaCmd = 'nvidia-smi --query-gpu=index,name,memory.used,memory.total,utilization.gpu,temperature.gpu --format=csv,nounits,noheader';
220
+ const nvidiaResult = await execWithTimeout(nvidiaCmd);
221
+ if (nvidiaResult?.stdout) {
222
+ const gpus = parseNvidiaSmi(nvidiaResult.stdout);
223
+ if (gpus)
224
+ return { gpus };
225
+ // Output exists but couldn't parse - continue to try rocm-smi
226
+ }
227
+ // Try AMD ROCm - get memory info
228
+ const rocmMemResult = await execWithTimeout('rocm-smi --showmeminfo vram');
229
+ if (rocmMemResult?.stdout) {
230
+ const gpus = parseRocmSmi(rocmMemResult.stdout);
231
+ if (gpus) {
232
+ // Get GPU names from cache or fetch once
233
+ if (!amdGpuNamesCache) {
234
+ const rocmNameResult = await execWithTimeout('rocm-smi --showproductname');
235
+ if (rocmNameResult?.stdout) {
236
+ amdGpuNamesCache = new Map();
237
+ parseRocmSmiNames(rocmNameResult.stdout, gpus.devices);
238
+ // Cache the names
239
+ for (const device of gpus.devices) {
240
+ amdGpuNamesCache.set(device.index, device.name);
241
+ }
242
+ }
243
+ }
244
+ else {
245
+ // Use cached names
246
+ for (const device of gpus.devices) {
247
+ const cachedName = amdGpuNamesCache.get(device.index);
248
+ if (cachedName) {
249
+ device.name = cachedName;
250
+ }
251
+ }
252
+ }
253
+ return { gpus };
254
+ }
255
+ // Output exists but couldn't parse - no error, just no GPUs found
256
+ }
257
+ // No GPU tools found or no parseable output - this is fine, not an error
258
+ return { gpus: null };
259
+ }
260
+ /**
261
+ * Resource Service - Singleton
262
+ *
263
+ * Provides cached, non-blocking access to system resources.
264
+ */
265
+ class ResourceService {
266
+ cache = null;
267
+ cacheTime = 0;
268
+ collecting = false;
269
+ hostname;
270
+ hasLoggedOnce = false;
271
+ constructor() {
272
+ this.hostname = os_1.default.hostname();
273
+ }
274
+ /**
275
+ * Get resources - NEVER blocks, returns cached or empty
276
+ * Triggers async collection if cache is stale
277
+ */
278
+ getResources() {
279
+ const now = Date.now();
280
+ // Trigger async collection if cache is stale and not already collecting
281
+ if (!this.collecting && (now - this.cacheTime) > CACHE_TTL_MS) {
282
+ this.collectAsync();
283
+ }
284
+ // Return cached data or empty resources (never block)
285
+ return this.cache ?? this.getEmptyResources();
286
+ }
287
+ /**
288
+ * Force refresh - waits for collection but still has timeout protection
289
+ * Use sparingly (e.g., on explicit user request)
290
+ */
291
+ async refreshResources() {
292
+ await this.collectAsync();
293
+ return this.cache ?? this.getEmptyResources();
294
+ }
295
+ /**
296
+ * Check if cached data is stale
297
+ */
298
+ isStale() {
299
+ return (Date.now() - this.cacheTime) > STALE_THRESHOLD_MS;
300
+ }
301
+ /**
302
+ * Get empty resources (fallback)
303
+ */
304
+ getEmptyResources() {
305
+ return {
306
+ hostname: this.hostname,
307
+ ram: collectRAM(), // RAM is always instant and available
308
+ gpus: null,
309
+ collectedAt: Date.now(),
310
+ };
311
+ }
312
+ /**
313
+ * Async collection - runs in background, updates cache
314
+ */
315
+ async collectAsync() {
316
+ if (this.collecting)
317
+ return;
318
+ this.collecting = true;
319
+ try {
320
+ // RAM is instant, GPU may timeout (that's OK)
321
+ const [ram, gpuResult] = await Promise.all([
322
+ Promise.resolve(collectRAM()),
323
+ collectGPUs(),
324
+ ]);
325
+ this.cache = {
326
+ hostname: this.hostname,
327
+ ram,
328
+ gpus: gpuResult.gpus,
329
+ gpuError: gpuResult.error,
330
+ collectedAt: Date.now(),
331
+ };
332
+ this.cacheTime = Date.now();
333
+ // Only log once on startup
334
+ if (!this.hasLoggedOnce) {
335
+ this.hasLoggedOnce = true;
336
+ if (gpuResult.gpus) {
337
+ console.log(`[ResourceService] RAM ${ram.total}GB, GPU ${gpuResult.gpus.totalMemory}GB (${gpuResult.gpus.devices.length} ${gpuResult.gpus.vendor} device${gpuResult.gpus.devices.length > 1 ? 's' : ''})`);
338
+ }
339
+ else {
340
+ console.log(`[ResourceService] RAM ${ram.total}GB, no GPU`);
341
+ }
342
+ }
343
+ }
344
+ catch (err) {
345
+ console.error('[ResourceService] Collection failed:', err);
346
+ // Still update RAM at least
347
+ this.cache = this.getEmptyResources();
348
+ this.cacheTime = Date.now();
349
+ }
350
+ finally {
351
+ this.collecting = false;
352
+ }
353
+ }
354
+ }
355
+ exports.ResourceService = ResourceService;
356
+ // Singleton instance
357
+ let instance = null;
358
+ function getResourceService() {
359
+ if (!instance) {
360
+ instance = new ResourceService();
361
+ }
362
+ return instance;
363
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Auth Routes - API endpoints for 2FA authentication
3
+ */
4
+ import { FastifyInstance } from 'fastify';
5
+ export default function authRoutes(fastify: FastifyInstance): Promise<void>;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ /**
3
+ * Auth Routes - API endpoints for 2FA authentication
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = authRoutes;
7
+ const auth_service_1 = require("../auth/auth-service");
8
+ const auth_middleware_1 = require("../auth/auth-middleware");
9
+ async function authRoutes(fastify) {
10
+ /**
11
+ * GET /auth/status
12
+ * Check if 2FA is configured and if the current request is authenticated
13
+ */
14
+ fastify.get('/auth/status', async (request, reply) => {
15
+ // Extract token from Authorization header or query parameter
16
+ let token;
17
+ const authHeader = request.headers.authorization;
18
+ if (authHeader?.startsWith('Bearer ')) {
19
+ token = authHeader.slice(7);
20
+ }
21
+ const status = auth_service_1.authService.getAuthStatus(token);
22
+ return reply.send(status);
23
+ });
24
+ /**
25
+ * POST /auth/verify
26
+ * Verify a TOTP code and issue a session token
27
+ */
28
+ fastify.post('/auth/verify', async (request, reply) => {
29
+ const { code, trustBrowser } = request.body;
30
+ if (!code || typeof code !== 'string') {
31
+ return reply.code(400).send({
32
+ error: 'invalid_request',
33
+ message: 'Verification code is required',
34
+ });
35
+ }
36
+ // Clean the code (remove spaces)
37
+ const cleanCode = code.replace(/\s/g, '');
38
+ if (!/^\d{6}$/.test(cleanCode)) {
39
+ return reply.code(400).send({
40
+ error: 'invalid_format',
41
+ message: 'Code must be 6 digits',
42
+ });
43
+ }
44
+ const result = auth_service_1.authService.verifyCode(cleanCode, !!trustBrowser);
45
+ if (result.success) {
46
+ // Persist token so MCP servers and CLI tools can auto-authenticate
47
+ if (result.token)
48
+ (0, auth_middleware_1.persistSessionToken)(result.token);
49
+ return reply.send({
50
+ success: true,
51
+ token: result.token,
52
+ });
53
+ }
54
+ else {
55
+ return reply.code(401).send({
56
+ success: false,
57
+ error: result.error,
58
+ });
59
+ }
60
+ });
61
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Cluster Management API Routes
3
+ *
4
+ * Endpoints for server registration and cluster management.
5
+ */
6
+ import { FastifyInstance } from 'fastify';
7
+ export default function clusterRoutes(fastify: FastifyInstance): Promise<void>;