sublera-cli 0.0.0-stage → 1.3.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 (72) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/LICENSE +21 -0
  3. package/README.md +585 -2
  4. package/SECURITY.md +78 -0
  5. package/THIRD_PARTY_NOTICES.md +37 -0
  6. package/dist/agent.d.ts +270 -0
  7. package/dist/agent.js +1867 -0
  8. package/dist/commands.d.ts +76 -0
  9. package/dist/commands.js +841 -0
  10. package/dist/config.d.ts +27 -0
  11. package/dist/config.js +95 -0
  12. package/dist/engine/binaryManager.d.ts +67 -0
  13. package/dist/engine/binaryManager.js +590 -0
  14. package/dist/engine/gguf.d.ts +88 -0
  15. package/dist/engine/gguf.js +458 -0
  16. package/dist/engine/llamaRunner.d.ts +78 -0
  17. package/dist/engine/llamaRunner.js +282 -0
  18. package/dist/engine/modelManager.d.ts +63 -0
  19. package/dist/engine/modelManager.js +510 -0
  20. package/dist/engine/selfCheck.d.ts +34 -0
  21. package/dist/engine/selfCheck.js +108 -0
  22. package/dist/exitCodes.d.ts +20 -0
  23. package/dist/exitCodes.js +20 -0
  24. package/dist/hardware/gpuMonitor.d.ts +32 -0
  25. package/dist/hardware/gpuMonitor.js +378 -0
  26. package/dist/hardware/sleepGuard.d.ts +42 -0
  27. package/dist/hardware/sleepGuard.js +149 -0
  28. package/dist/hardware/types.d.ts +40 -0
  29. package/dist/hardware/types.js +2 -0
  30. package/dist/health.d.ts +17 -0
  31. package/dist/health.js +38 -0
  32. package/dist/index.d.ts +11 -0
  33. package/dist/index.js +268 -0
  34. package/dist/logger.d.ts +53 -0
  35. package/dist/logger.js +201 -0
  36. package/dist/network/proxy.d.ts +58 -0
  37. package/dist/network/proxy.js +328 -0
  38. package/dist/network/streamProxy.d.ts +51 -0
  39. package/dist/network/streamProxy.js +286 -0
  40. package/dist/network/tunnelClient.d.ts +54 -0
  41. package/dist/network/tunnelClient.js +257 -0
  42. package/dist/settings.d.ts +53 -0
  43. package/dist/settings.js +171 -0
  44. package/dist/state.d.ts +52 -0
  45. package/dist/state.js +71 -0
  46. package/dist/storageCommand.d.ts +22 -0
  47. package/dist/storageCommand.js +81 -0
  48. package/dist/types.d.ts +222 -0
  49. package/dist/types.js +43 -0
  50. package/dist/ui/progress.d.ts +29 -0
  51. package/dist/ui/progress.js +82 -0
  52. package/dist/ui/terminalDashboard.d.ts +88 -0
  53. package/dist/ui/terminalDashboard.js +295 -0
  54. package/dist/util/env.d.ts +3 -0
  55. package/dist/util/env.js +6 -0
  56. package/dist/util/exec.d.ts +23 -0
  57. package/dist/util/exec.js +116 -0
  58. package/dist/util/format.d.ts +47 -0
  59. package/dist/util/format.js +125 -0
  60. package/dist/util/fs.d.ts +18 -0
  61. package/dist/util/fs.js +140 -0
  62. package/dist/util/http.d.ts +28 -0
  63. package/dist/util/http.js +128 -0
  64. package/dist/util/instanceLock.d.ts +3 -0
  65. package/dist/util/instanceLock.js +108 -0
  66. package/dist/util/open.d.ts +16 -0
  67. package/dist/util/open.js +65 -0
  68. package/dist/util/storage.d.ts +10 -0
  69. package/dist/util/storage.js +69 -0
  70. package/dist/version.d.ts +15 -0
  71. package/dist/version.js +29 -0
  72. package/package.json +68 -4
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Process exit codes. These are part of the public contract: service managers,
3
+ * scripts, and fleet tooling may branch on them, so a code never changes meaning.
4
+ */
5
+ export const EXIT = {
6
+ /** Success, or a booking that ended normally. */
7
+ OK: 0,
8
+ /** A runtime failure: server unreachable, host key rejected, a failed check, a crash loop. */
9
+ ERROR: 1,
10
+ /** Bad command line or bad settings (unknown option, invalid agent.json, invalid proxy). */
11
+ USAGE: 2,
12
+ /** Stopped with Ctrl+C (SIGINT). Saved booking state is cleared. */
13
+ INTERRUPTED: 130,
14
+ /** Stopped by the system or a service manager (SIGTERM). Saved booking state is kept for the next start. */
15
+ TERMINATED: 143,
16
+ };
17
+ /** Configuration failures should not be retried forever by service managers. */
18
+ export class UsageError extends Error {
19
+ }
20
+ //# sourceMappingURL=exitCodes.js.map
@@ -0,0 +1,32 @@
1
+ import type { FitResult, GpuMonitorOptions, GpuProcess, GpuSnapshot, GpuStats, GpuVendor } from './types.js';
2
+ export declare function parseNvidiaCsv(text: string): GpuStats[];
3
+ export declare function parseNvidiaProcesses(text: string): GpuProcess[];
4
+ export declare function parseRocmJson(raw: unknown): GpuStats[];
5
+ export declare function parseSystemProfiler(raw: unknown, totalSystemMemoryMb?: number): GpuStats[];
6
+ export declare function parseWmicJson(raw: unknown): GpuStats[];
7
+ export declare function choosePrimary(gpus: GpuStats[]): GpuStats | null;
8
+ export declare function checkFit(needsMb: number, gpu: GpuStats | null, safetyRatio?: number): FitResult;
9
+ export declare class GpuMonitor {
10
+ private readonly platform;
11
+ private readonly exec;
12
+ private readonly env;
13
+ private readonly cacheMs;
14
+ private cached;
15
+ constructor(options?: GpuMonitorOptions);
16
+ scan(force?: boolean): Promise<GpuSnapshot>;
17
+ private detect;
18
+ private snapshot;
19
+ private detectNvidia;
20
+ private detectRocm;
21
+ private detectApple;
22
+ private detectWindows;
23
+ private detectLspci;
24
+ }
25
+ export declare function gpuSummary(monitor: GpuMonitor, force?: boolean): Promise<GpuSnapshot>;
26
+ export type GpuOverride = {
27
+ name: string | null;
28
+ vramMb: number | null;
29
+ };
30
+ export declare function vendorFromName(name: string): GpuVendor;
31
+ /** `--gpu` / `--vram` win over detection, for cards that report wrong values (for example 4 GB on Windows). */
32
+ export declare function applyGpuOverride(snapshot: GpuSnapshot, override: GpuOverride | null | undefined): GpuSnapshot;
@@ -0,0 +1,378 @@
1
+ import { execFile as realExecFile } from '../util/exec.js';
2
+ function num(value) {
3
+ if (value == null)
4
+ return null;
5
+ const cleaned = String(value).replace(/[^\d.+-]/g, '').trim();
6
+ if (!cleaned || cleaned === '-' || cleaned === '+')
7
+ return null;
8
+ const n = Number(cleaned);
9
+ return Number.isFinite(n) ? n : null;
10
+ }
11
+ function clampMb(value) {
12
+ if (value == null || !Number.isFinite(value) || value < 0)
13
+ return 0;
14
+ return Math.round(value);
15
+ }
16
+ export function parseNvidiaCsv(text) {
17
+ const gpus = [];
18
+ const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
19
+ for (const [index, line] of lines.entries()) {
20
+ const cols = line.split(',').map((c) => c.trim());
21
+ if (cols.length < 3)
22
+ continue;
23
+ const name = cols[0] || 'NVIDIA GPU';
24
+ const total = clampMb(num(cols[1]));
25
+ const freeRaw = num(cols[2]);
26
+ const free = freeRaw == null ? total : clampMb(freeRaw);
27
+ const used = Math.max(0, total - free);
28
+ const utilization = num(cols[3]);
29
+ const temp = num(cols[4]);
30
+ const power = num(cols[5]);
31
+ const limit = num(cols[6]);
32
+ gpus.push({
33
+ index,
34
+ name,
35
+ vendor: 'nvidia',
36
+ totalVramMb: total,
37
+ freeVramMb: free,
38
+ usedVramMb: used,
39
+ utilizationPct: utilization,
40
+ temperatureC: temp,
41
+ powerW: power,
42
+ powerLimitW: limit,
43
+ processes: [],
44
+ });
45
+ }
46
+ return gpus;
47
+ }
48
+ export function parseNvidiaProcesses(text) {
49
+ const out = [];
50
+ for (const line of text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)) {
51
+ const cols = line.split(',').map((c) => c.trim());
52
+ if (cols.length < 3)
53
+ continue;
54
+ const pid = Number(cols[0]);
55
+ if (!Number.isFinite(pid))
56
+ continue;
57
+ out.push({ pid, name: cols[1] || 'process', usedMb: clampMb(num(cols[2])) });
58
+ }
59
+ return out;
60
+ }
61
+ export function parseRocmJson(raw) {
62
+ if (!raw || typeof raw !== 'object')
63
+ return [];
64
+ const data = raw;
65
+ const gpus = [];
66
+ let index = 0;
67
+ for (const [card, values] of Object.entries(data)) {
68
+ if (!/^card\d+$/i.test(card) || !values || typeof values !== 'object')
69
+ continue;
70
+ const entries = Object.entries(values).reduce((acc, [key, value]) => {
71
+ acc[key.toLowerCase()] = String(value);
72
+ return acc;
73
+ }, {});
74
+ const name = entries['card series'] || entries['card model'] || entries['product name'] || entries['card sku'] || 'AMD GPU';
75
+ const total = clampMb(num(entries['vram total memory (b)']) ? Number(entries['vram total memory (b)']) / 1024 / 1024 : num(entries['vram total size']));
76
+ const usedBytes = entries['vram total used memory (b)'] ?? entries['vram used memory (b)'];
77
+ const used = clampMb(num(usedBytes) ? Number(usedBytes) / 1024 / 1024 : num(entries['vram used size']));
78
+ gpus.push({
79
+ index,
80
+ name,
81
+ vendor: 'amd',
82
+ totalVramMb: total,
83
+ freeVramMb: Math.max(0, total - used),
84
+ usedVramMb: used,
85
+ utilizationPct: num(entries['gpu use (%)'] || entries['gpu utilization']),
86
+ temperatureC: num(entries['temperature (sensor edge) (c)'] || entries['temperature (sensor junction) (c)'] || entries['temperature']),
87
+ powerW: num(entries['average power (w)'] || entries['power (w)']),
88
+ powerLimitW: num(entries['max power (w)']),
89
+ processes: [],
90
+ });
91
+ index++;
92
+ }
93
+ return gpus;
94
+ }
95
+ export function parseSystemProfiler(raw, totalSystemMemoryMb = 0) {
96
+ if (!raw || typeof raw !== 'object')
97
+ return [];
98
+ const displays = raw.SPDisplaysDataType;
99
+ if (!Array.isArray(displays) || !displays.length)
100
+ return [];
101
+ return displays.map((display, index) => {
102
+ const name = display.sppci_model || display._name || 'Apple GPU';
103
+ const vramText = display.spdisplays_vram || display.spdisplays_vram_shared || '';
104
+ const match = vramText.match(/(\d+(?:\.\d+)?)\s*(GB|MB)/i);
105
+ let vram = 0;
106
+ if (match)
107
+ vram = clampMb(Number(match[1]) * (/gb/i.test(match[2] || '') ? 1024 : 1));
108
+ else if (totalSystemMemoryMb > 0)
109
+ vram = totalSystemMemoryMb;
110
+ return {
111
+ index,
112
+ name,
113
+ vendor: 'apple',
114
+ totalVramMb: vram,
115
+ // macOS does not expose a reliable free-VRAM number for unified memory.
116
+ freeVramMb: vram,
117
+ usedVramMb: 0,
118
+ utilizationPct: null,
119
+ temperatureC: null,
120
+ powerW: null,
121
+ powerLimitW: null,
122
+ processes: [],
123
+ };
124
+ });
125
+ }
126
+ export function parseWmicJson(raw) {
127
+ let rows = [];
128
+ if (Array.isArray(raw))
129
+ rows = raw;
130
+ else if (raw && typeof raw === 'object')
131
+ rows = [raw];
132
+ const out = [];
133
+ for (const row of rows) {
134
+ const name = row.Name || 'GPU';
135
+ if (/microsoft basic|remote display|virtual/i.test(name))
136
+ continue;
137
+ let ram = Number(row.AdapterRAM || 0);
138
+ if (!Number.isFinite(ram) || ram < 0)
139
+ ram = Math.abs(ram);
140
+ const total = clampMb(ram / 1024 / 1024);
141
+ const vendor = /nvidia/i.test(name) ? 'nvidia' : /amd|radeon/i.test(name) ? 'amd' : /intel/i.test(name) ? 'intel' : 'unknown';
142
+ out.push({
143
+ index: out.length,
144
+ name,
145
+ vendor,
146
+ totalVramMb: total,
147
+ freeVramMb: total,
148
+ usedVramMb: 0,
149
+ utilizationPct: null,
150
+ temperatureC: null,
151
+ powerW: null,
152
+ powerLimitW: null,
153
+ processes: [],
154
+ });
155
+ }
156
+ return out;
157
+ }
158
+ export function choosePrimary(gpus) {
159
+ if (!gpus.length)
160
+ return null;
161
+ const discrete = gpus.filter((g) => g.vendor === 'nvidia' || g.vendor === 'amd' || g.vendor === 'apple');
162
+ const pool = discrete.length ? discrete : gpus;
163
+ return [...pool].sort((a, b) => b.totalVramMb - a.totalVramMb || (b.utilizationPct || 0) - (a.utilizationPct || 0))[0] || null;
164
+ }
165
+ export function checkFit(needsMb, gpu, safetyRatio = 0.02) {
166
+ if (!gpu) {
167
+ return { ok: false, reason: 'No GPU detected', freeMb: 0, needsMb, headroomMb: -needsMb };
168
+ }
169
+ // Some cards are found without a memory size (for example through lspci only). Unknown is not "0 GB free";
170
+ // the load itself shows whether the model fits, and --vram can set the size.
171
+ if (gpu.totalVramMb <= 0)
172
+ return { ok: true, reason: 'VRAM size unknown', freeMb: 0, needsMb, headroomMb: 0 };
173
+ const budget = gpu.freeVramMb;
174
+ const overhead = Math.max(256, Math.round(needsMb * safetyRatio));
175
+ const required = needsMb + overhead;
176
+ return {
177
+ ok: required <= budget,
178
+ reason: required <= budget
179
+ ? undefined
180
+ : `Needs about ${(required / 1024).toFixed(1)}GB free, this GPU has ${(budget / 1024).toFixed(1)}GB free`,
181
+ freeMb: budget,
182
+ needsMb: required,
183
+ headroomMb: budget - required,
184
+ };
185
+ }
186
+ export class GpuMonitor {
187
+ platform;
188
+ exec;
189
+ env;
190
+ cacheMs;
191
+ cached = null;
192
+ constructor(options = {}) {
193
+ this.platform = options.platform ?? process.platform;
194
+ this.exec = options.exec ?? realExecFile;
195
+ this.env = options.env ?? process.env;
196
+ this.cacheMs = options.cacheMs ?? 1_000;
197
+ }
198
+ async scan(force = false) {
199
+ if (!force && this.cached && Date.now() - this.cached.checkedAt < this.cacheMs)
200
+ return this.cached;
201
+ const snapshot = await this.detect();
202
+ this.cached = snapshot;
203
+ return snapshot;
204
+ }
205
+ async detect() {
206
+ const warnings = [];
207
+ const nvidia = await this.detectNvidia(warnings);
208
+ if (nvidia.length)
209
+ return this.snapshot(nvidia, 'nvidia-smi', warnings);
210
+ const rocm = await this.detectRocm(warnings);
211
+ if (rocm.length)
212
+ return this.snapshot(rocm, 'rocm-smi', warnings);
213
+ if (this.platform === 'darwin') {
214
+ const apple = await this.detectApple(warnings);
215
+ if (apple.length)
216
+ return this.snapshot(apple, 'system_profiler', warnings);
217
+ }
218
+ if (this.platform === 'win32') {
219
+ const wmic = await this.detectWindows(warnings);
220
+ if (wmic.length)
221
+ return this.snapshot(wmic, 'win32_VideoController', warnings);
222
+ }
223
+ const lspci = await this.detectLspci(warnings);
224
+ if (lspci.length)
225
+ return this.snapshot(lspci, 'lspci', warnings);
226
+ warnings.push('No supported GPU detected. Use --gpu and --vram to run in test mode.');
227
+ return this.snapshot([], 'none', warnings);
228
+ }
229
+ snapshot(gpus, source, warnings) {
230
+ return {
231
+ gpus,
232
+ primary: choosePrimary(gpus),
233
+ source,
234
+ platform: this.platform,
235
+ checkedAt: Date.now(),
236
+ warnings,
237
+ };
238
+ }
239
+ async detectNvidia(warnings) {
240
+ const fields = 'name,memory.total,memory.free,utilization.gpu,temperature.gpu,power.draw,power.limit';
241
+ const result = await this.exec('nvidia-smi', [
242
+ `--query-gpu=${fields}`,
243
+ '--format=csv,noheader,nounits',
244
+ ], { timeoutMs: 10_000 });
245
+ if (result.code !== 0 || !result.stdout.trim()) {
246
+ if (result.error && !/ENOENT|not found/i.test(result.error))
247
+ warnings.push(`nvidia-smi: ${result.error}`);
248
+ return [];
249
+ }
250
+ const gpus = parseNvidiaCsv(result.stdout);
251
+ if (!gpus.length)
252
+ return [];
253
+ const processes = await this.exec('nvidia-smi', [
254
+ '--query-compute-apps=pid,process_name,used_memory',
255
+ '--format=csv,noheader,nounits',
256
+ ], { timeoutMs: 10_000 });
257
+ if (processes.code === 0 && processes.stdout.trim()) {
258
+ const all = parseNvidiaProcesses(processes.stdout);
259
+ const ownPid = process.pid;
260
+ for (const gpu of gpus) {
261
+ gpu.processes = all.filter((p) => p.pid !== ownPid && p.pid !== 0 && p.usedMb >= 128);
262
+ }
263
+ }
264
+ return gpus;
265
+ }
266
+ async detectRocm(warnings) {
267
+ const result = await this.exec('rocm-smi', [
268
+ '--showproductname', '--showmeminfo', 'vram', '--showuse', '--showtemp', '--showpower', '--json',
269
+ ], { timeoutMs: 10_000 });
270
+ if (result.code !== 0 || !result.stdout.trim()) {
271
+ if (result.error && !/ENOENT|not found/i.test(result.error))
272
+ warnings.push(`rocm-smi: ${result.error}`);
273
+ return [];
274
+ }
275
+ try {
276
+ return parseRocmJson(JSON.parse(result.stdout));
277
+ }
278
+ catch {
279
+ warnings.push('Could not parse rocm-smi JSON');
280
+ return [];
281
+ }
282
+ }
283
+ async detectApple(warnings) {
284
+ const display = await this.exec('system_profiler', ['SPDisplaysDataType', '-json'], { timeoutMs: 15_000 });
285
+ if (display.code !== 0 || !display.stdout.trim())
286
+ return [];
287
+ let totalMb = 0;
288
+ const mem = await this.exec('sysctl', ['-n', 'hw.memsize'], { timeoutMs: 5_000 });
289
+ if (mem.code === 0)
290
+ totalMb = Math.round(Number(mem.stdout.trim()) / 1024 / 1024);
291
+ try {
292
+ const gpus = parseSystemProfiler(JSON.parse(display.stdout), totalMb);
293
+ if (gpus.length && !gpus.some((g) => g.totalVramMb > 0))
294
+ warnings.push('Apple unified memory size was not reported');
295
+ return gpus;
296
+ }
297
+ catch {
298
+ warnings.push('Could not parse system_profiler output');
299
+ return [];
300
+ }
301
+ }
302
+ async detectWindows(warnings) {
303
+ const script = "Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM | ConvertTo-Json -Compress";
304
+ const result = await this.exec('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], { timeoutMs: 15_000 });
305
+ if (result.code !== 0 || !result.stdout.trim())
306
+ return [];
307
+ try {
308
+ const gpus = parseWmicJson(JSON.parse(result.stdout));
309
+ if (gpus.length && gpus.every((g) => g.totalVramMb < 512)) {
310
+ warnings.push('Windows reported less than 512MB VRAM; use --vram to override.');
311
+ }
312
+ return gpus;
313
+ }
314
+ catch {
315
+ warnings.push('Could not parse Windows video controller output');
316
+ return [];
317
+ }
318
+ }
319
+ async detectLspci(warnings) {
320
+ if (this.platform !== 'linux')
321
+ return [];
322
+ const result = await this.exec('lspci', [], { timeoutMs: 8_000 });
323
+ if (result.code !== 0)
324
+ return [];
325
+ const lines = result.stdout.split(/\r?\n/).filter((l) => /VGA|3D controller|Display controller/i.test(l));
326
+ return lines.map((line, index) => {
327
+ const name = line.replace(/^[0-9a-f:.]+\s+[^:]+:\s*/i, '').trim();
328
+ const vendor = /nvidia/i.test(name) ? 'nvidia' : /amd|ati|radeon/i.test(name) ? 'amd' : /intel/i.test(name) ? 'intel' : 'unknown';
329
+ return {
330
+ index,
331
+ name,
332
+ vendor,
333
+ totalVramMb: 0,
334
+ freeVramMb: 0,
335
+ usedVramMb: 0,
336
+ utilizationPct: null,
337
+ temperatureC: null,
338
+ powerW: null,
339
+ powerLimitW: null,
340
+ processes: [],
341
+ };
342
+ }).filter((g) => g.name.length > 0);
343
+ }
344
+ }
345
+ export async function gpuSummary(monitor, force = false) {
346
+ const snapshot = await monitor.scan(force);
347
+ if (!snapshot.gpus.length)
348
+ void 0;
349
+ return snapshot;
350
+ }
351
+ export function vendorFromName(name) {
352
+ if (/nvidia|geforce|rtx|gtx|quadro|tesla/i.test(name))
353
+ return 'nvidia';
354
+ if (/amd|radeon/i.test(name))
355
+ return 'amd';
356
+ if (/apple|\bm[1-9]\b/i.test(name))
357
+ return 'apple';
358
+ if (/intel|\barc\b/i.test(name))
359
+ return 'intel';
360
+ return 'unknown';
361
+ }
362
+ /** `--gpu` / `--vram` win over detection, for cards that report wrong values (for example 4 GB on Windows). */
363
+ export function applyGpuOverride(snapshot, override) {
364
+ if (!override || (!override.name && !override.vramMb))
365
+ return snapshot;
366
+ const base = snapshot.primary;
367
+ const name = override.name || base?.name || 'Unknown GPU';
368
+ const totalVramMb = override.vramMb ?? base?.totalVramMb ?? 0;
369
+ const primary = base
370
+ ? { ...base, name, totalVramMb, freeVramMb: override.vramMb != null ? Math.max(0, override.vramMb - base.usedVramMb) : base.freeVramMb }
371
+ : {
372
+ index: 0, name, vendor: vendorFromName(name), totalVramMb, freeVramMb: totalVramMb, usedVramMb: 0,
373
+ utilizationPct: 0, temperatureC: null, powerW: null, powerLimitW: null, processes: [],
374
+ };
375
+ const gpus = base ? snapshot.gpus.map((gpu) => (gpu === base ? primary : gpu)) : [primary, ...snapshot.gpus];
376
+ return { ...snapshot, gpus, primary, source: base ? snapshot.source : 'override' };
377
+ }
378
+ //# sourceMappingURL=gpuMonitor.js.map
@@ -0,0 +1,42 @@
1
+ import { spawn as realSpawn } from 'node:child_process';
2
+ import { killTree, which } from '../util/exec.js';
3
+ import type { Logger } from '../logger.js';
4
+ type SpawnFn = typeof realSpawn;
5
+ export type SleepGuardOptions = {
6
+ platform?: NodeJS.Platform;
7
+ spawnFn?: SpawnFn;
8
+ logger?: Logger;
9
+ whichFn?: typeof which;
10
+ killFn?: typeof killTree;
11
+ };
12
+ export type SleepGuardState = {
13
+ active: boolean;
14
+ supported: boolean;
15
+ platform: NodeJS.Platform;
16
+ mechanism: string;
17
+ since: number | null;
18
+ lastError: string | null;
19
+ };
20
+ export declare function windowsSleepScript(): string;
21
+ export declare class SleepGuard {
22
+ private readonly platform;
23
+ private readonly spawnFn;
24
+ private readonly logger?;
25
+ private readonly whichFn;
26
+ private readonly killFn;
27
+ private child;
28
+ private state;
29
+ private stopping;
30
+ private exitHandler;
31
+ constructor(options?: SleepGuardOptions);
32
+ private mechanismName;
33
+ status(): SleepGuardState;
34
+ acquire(reason: string): Promise<boolean>;
35
+ private startWindows;
36
+ private startDarwin;
37
+ private startLinux;
38
+ private attach;
39
+ stop(): Promise<void>;
40
+ }
41
+ export declare function sleepGuardAvailable(platform?: NodeJS.Platform, whichFn?: typeof which): Promise<boolean>;
42
+ export {};
@@ -0,0 +1,149 @@
1
+ import { spawn as realSpawn } from 'node:child_process';
2
+ import { killTree, which } from '../util/exec.js';
3
+ export function windowsSleepScript() {
4
+ return [
5
+ "$src = @'",
6
+ 'using System;',
7
+ 'using System.Runtime.InteropServices;',
8
+ 'public static class SubletSleepGuard {',
9
+ ' [DllImport("kernel32.dll", SetLastError = true)]',
10
+ ' public static extern uint SetThreadExecutionState(uint esFlags);',
11
+ '}',
12
+ "'@",
13
+ 'Add-Type -TypeDefinition $src',
14
+ 'while ($true) {',
15
+ ' [SubletSleepGuard]::SetThreadExecutionState([uint32]2147483651) | Out-Null',
16
+ ' Start-Sleep -Seconds 20',
17
+ '}',
18
+ ].join('\n');
19
+ }
20
+ export class SleepGuard {
21
+ platform;
22
+ spawnFn;
23
+ logger;
24
+ whichFn;
25
+ killFn;
26
+ child = null;
27
+ state;
28
+ stopping = false;
29
+ exitHandler = null;
30
+ constructor(options = {}) {
31
+ this.platform = options.platform ?? process.platform;
32
+ this.spawnFn = options.spawnFn ?? realSpawn;
33
+ this.logger = options.logger;
34
+ this.whichFn = options.whichFn ?? which;
35
+ this.killFn = options.killFn ?? killTree;
36
+ this.state = {
37
+ active: false,
38
+ supported: this.platform === 'win32' || this.platform === 'darwin' || this.platform === 'linux',
39
+ platform: this.platform,
40
+ mechanism: this.mechanismName(),
41
+ since: null,
42
+ lastError: null,
43
+ };
44
+ }
45
+ mechanismName() {
46
+ if (this.platform === 'win32')
47
+ return 'SetThreadExecutionState';
48
+ if (this.platform === 'darwin')
49
+ return 'caffeinate';
50
+ if (this.platform === 'linux')
51
+ return 'systemd-inhibit';
52
+ return 'unsupported';
53
+ }
54
+ status() {
55
+ return { ...this.state };
56
+ }
57
+ async acquire(reason) {
58
+ if (this.state.active)
59
+ return true;
60
+ try {
61
+ if (this.platform === 'win32')
62
+ return this.startWindows(reason);
63
+ if (this.platform === 'darwin')
64
+ return this.startDarwin(reason);
65
+ if (this.platform === 'linux')
66
+ return this.startLinux(reason);
67
+ this.state.lastError = `Sleep blocking is not implemented on ${this.platform}`;
68
+ return false;
69
+ }
70
+ catch (error) {
71
+ this.state.lastError = error instanceof Error ? error.message : String(error);
72
+ this.logger?.warn('Could not block sleep', { error: this.state.lastError });
73
+ return false;
74
+ }
75
+ }
76
+ startWindows(reason) {
77
+ const child = this.spawnFn('powershell', [
78
+ '-NoProfile',
79
+ '-NonInteractive',
80
+ '-WindowStyle', 'Hidden',
81
+ '-Command', windowsSleepScript(),
82
+ ], { windowsHide: true, stdio: 'ignore' });
83
+ return this.attach(child, `Windows wake-lock (${reason})`);
84
+ }
85
+ startDarwin(reason) {
86
+ const child = this.spawnFn('caffeinate', ['-dimsu', '-w', String(process.pid)], { stdio: 'ignore' });
87
+ return this.attach(child, `caffeinate (${reason})`);
88
+ }
89
+ startLinux(reason) {
90
+ const child = this.spawnFn('systemd-inhibit', [
91
+ '--what=sleep:idle',
92
+ '--who=sublera-cli',
93
+ `--why=${reason.slice(0, 80)}`,
94
+ 'tail', `--pid=${process.pid}`, '-f', '/dev/null',
95
+ ], { stdio: 'ignore' });
96
+ return this.attach(child, `systemd-inhibit (${reason})`);
97
+ }
98
+ attach(child, description) {
99
+ this.child = child;
100
+ this.stopping = false;
101
+ this.state = {
102
+ active: true,
103
+ supported: true,
104
+ platform: this.platform,
105
+ mechanism: this.mechanismName(),
106
+ since: Date.now(),
107
+ lastError: null,
108
+ };
109
+ child.once('error', (error) => {
110
+ this.state.active = false;
111
+ this.state.lastError = error.message;
112
+ this.logger?.warn(`Sleep guard failed: ${error.message}`);
113
+ });
114
+ child.once('exit', (code) => {
115
+ this.state.active = false;
116
+ this.state.since = null;
117
+ if (!this.stopping) {
118
+ this.logger?.warn(`Sleep guard exited unexpectedly (${description}, code ${code ?? '?'})`);
119
+ }
120
+ });
121
+ this.exitHandler = () => { void this.stop(); };
122
+ process.once('exit', this.exitHandler);
123
+ this.logger?.info(`Sleep blocked: ${description}`);
124
+ return true;
125
+ }
126
+ async stop() {
127
+ this.stopping = true;
128
+ if (this.exitHandler) {
129
+ process.removeListener('exit', this.exitHandler);
130
+ this.exitHandler = null;
131
+ }
132
+ const child = this.child;
133
+ this.child = null;
134
+ this.state.active = false;
135
+ this.state.since = null;
136
+ if (child)
137
+ await this.killFn(child);
138
+ }
139
+ }
140
+ export async function sleepGuardAvailable(platform = process.platform, whichFn = which) {
141
+ if (platform === 'win32')
142
+ return true;
143
+ if (platform === 'darwin')
144
+ return (await whichFn('caffeinate')) != null;
145
+ if (platform === 'linux')
146
+ return (await whichFn('systemd-inhibit')) != null;
147
+ return false;
148
+ }
149
+ //# sourceMappingURL=sleepGuard.js.map
@@ -0,0 +1,40 @@
1
+ export type GpuVendor = 'nvidia' | 'amd' | 'apple' | 'intel' | 'unknown';
2
+ export type GpuProcess = {
3
+ pid: number;
4
+ name: string;
5
+ usedMb: number;
6
+ };
7
+ export type GpuStats = {
8
+ index: number;
9
+ name: string;
10
+ vendor: GpuVendor;
11
+ totalVramMb: number;
12
+ freeVramMb: number;
13
+ usedVramMb: number;
14
+ utilizationPct: number | null;
15
+ temperatureC: number | null;
16
+ powerW: number | null;
17
+ powerLimitW: number | null;
18
+ processes: GpuProcess[];
19
+ };
20
+ export type GpuSnapshot = {
21
+ gpus: GpuStats[];
22
+ primary: GpuStats | null;
23
+ source: string;
24
+ platform: NodeJS.Platform;
25
+ checkedAt: number;
26
+ warnings: string[];
27
+ };
28
+ export type FitResult = {
29
+ ok: boolean;
30
+ reason?: string;
31
+ freeMb: number;
32
+ needsMb: number;
33
+ headroomMb: number;
34
+ };
35
+ export type GpuMonitorOptions = {
36
+ platform?: NodeJS.Platform;
37
+ exec?: typeof import('../util/exec.js').execFile;
38
+ env?: NodeJS.ProcessEnv;
39
+ cacheMs?: number;
40
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,17 @@
1
+ export type AgentHealth = {
2
+ schemaVersion: 1;
3
+ agentVersion: string;
4
+ pid: number;
5
+ updatedAt: number;
6
+ live: boolean;
7
+ ready: boolean;
8
+ draining: boolean;
9
+ simulation: boolean;
10
+ tunnel: string;
11
+ engine: string;
12
+ activeJobs: number;
13
+ loading: boolean;
14
+ };
15
+ export declare function healthPath(env?: NodeJS.ProcessEnv): string;
16
+ /** Local readiness only: not server attestation, billing approval, or a GPU benchmark. */
17
+ export declare function readHealth(env?: NodeJS.ProcessEnv): Promise<AgentHealth>;