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,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,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
|
+
}
|