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,295 @@
1
+ import { formatClock, formatPercent, formatSpeed, formatUsdNanos, modelShortName, padRight, stripAnsi, truncate } from '../util/format.js';
2
+ import { BRAND, VERSION } from '../version.js';
3
+ export function initialState(server) {
4
+ return {
5
+ status: 'starting',
6
+ username: null,
7
+ endpoint: null,
8
+ server,
9
+ reliability: null,
10
+ held: false,
11
+ gpuName: null,
12
+ totalVramMb: 0,
13
+ freeVramMb: 0,
14
+ gpuTempC: null,
15
+ gpuPowerW: null,
16
+ gpuUtilPct: null,
17
+ wakeLock: false,
18
+ llamaStatus: 'not started',
19
+ job: null,
20
+ bookings: [],
21
+ liveTokPerSec: null,
22
+ sessionEarnedNanos: 0,
23
+ sessionGuaranteedNanos: 0,
24
+ tokenEarnedNanos: 0,
25
+ publicMode: false,
26
+ priceOutUsd: null,
27
+ priceHourUsd: null,
28
+ walletNanos: null,
29
+ lastHeartbeatAt: null,
30
+ message: null,
31
+ showLogs: false,
32
+ logTail: [],
33
+ keyHint: null,
34
+ };
35
+ }
36
+ function statusLabel(state) {
37
+ if (state.status === 'auth-failed')
38
+ return { text: 'AUTH FAILED', status: state.status };
39
+ if (state.status === 'outdated')
40
+ return { text: 'UPDATE NEEDED', status: state.status };
41
+ if (state.status === 'reconnecting')
42
+ return { text: 'RECONNECTING', status: state.status };
43
+ if (state.status === 'paused')
44
+ return { text: 'HELD', status: state.status };
45
+ if (state.status === 'offline')
46
+ return { text: 'OFFLINE', status: state.status };
47
+ if (state.status === 'working')
48
+ return { text: 'WORKING', status: state.status };
49
+ if (state.status === 'starting')
50
+ return { text: 'STARTING', status: state.status };
51
+ return { text: 'ONLINE', status: state.status };
52
+ }
53
+ function valueOrDash(value) {
54
+ return value && value.trim() ? value : '-';
55
+ }
56
+ function gpuLine(state) {
57
+ if (!state.gpuName)
58
+ return 'No GPU detected (use --gpu / --vram for a test node)';
59
+ const total = state.totalVramMb > 0 ? `${(state.totalVramMb / 1024).toFixed(1)} GB` : '? GB';
60
+ const free = state.freeVramMb > 0 ? `${(state.freeVramMb / 1024).toFixed(1)} GB` : '? GB';
61
+ const util = state.gpuUtilPct != null ? ` · ${state.gpuUtilPct.toFixed(0)}% util` : '';
62
+ return `${state.gpuName} (${free} free / ${total})${util}`;
63
+ }
64
+ function thermalLine(state) {
65
+ const temp = state.gpuTempC != null ? `${state.gpuTempC.toFixed(0)}°C` : 'temp n/a';
66
+ const power = state.gpuPowerW != null ? `${state.gpuPowerW.toFixed(0)}W` : 'power n/a';
67
+ const lock = state.wakeLock ? 'Wake-Lock: ACTIVE - sleep blocked' : 'Wake-Lock: idle';
68
+ return `${temp} · ${power} · ${lock}`;
69
+ }
70
+ function tokS(value) {
71
+ return Number.isInteger(value) ? String(value) : value.toFixed(1);
72
+ }
73
+ /** A positive finite number, or null: a broken value never prints as NaN (or throws in toFixed). */
74
+ function positive(value) {
75
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null;
76
+ }
77
+ /** "50 tok/s" for a speed seat, "full GPU" for an exclusive rental. */
78
+ export function rentalLabel(job) {
79
+ const speed = positive(job.tokS);
80
+ if (speed)
81
+ return `${tokS(speed)} tok/s`;
82
+ return job.shared ? 'shared seat' : 'full GPU';
83
+ }
84
+ /** "2 rentals: 50 tok/s + full GPU"; with more than three, "4 rentals, 150 tok/s reserved". */
85
+ export function rentalsSummary(rentals) {
86
+ if (rentals.length <= 3)
87
+ return `${rentals.length} rental${rentals.length === 1 ? '' : 's'}: ${rentals.map(rentalLabel).join(' + ')}`;
88
+ const reserved = rentals.reduce((sum, job) => sum + (positive(job.tokS) ?? 0), 0);
89
+ const full = rentals.some((job) => !positive(job.tokS) && !job.shared) ? ' + full GPU' : '';
90
+ return `${rentals.length} rentals, ${tokS(reserved)} tok/s reserved${full}`;
91
+ }
92
+ function jobLine(state) {
93
+ const job = state.job;
94
+ const rentals = state.bookings ?? [];
95
+ const single = rentals[0];
96
+ // One full-GPU rental keeps the classic label; seats, several rentals, or public selling list every rental.
97
+ if (rentals.length > 1 || (single && (state.publicMode || single.tokS || single.shared))) {
98
+ const loading = job?.kind === 'request' && job.id && !rentals.some((rental) => rental.id === job.id) ? ' + 1 loading' : '';
99
+ return `${state.publicMode ? 'Per-token + ' : ''}${rentalsSummary(rentals)}${loading}`;
100
+ }
101
+ if (!job)
102
+ return 'Standby - waiting for a web request';
103
+ if (job.kind === 'public')
104
+ return `Public pay-per-token${job.model ? ` · ${modelShortName(job.model)}` : ''}`;
105
+ if (job.id?.startsWith('bench_'))
106
+ return 'Benchmark: measuring this GPU (no buyer)';
107
+ return `Dedicated Buyer Request${job.id ? ` (#${job.id.replace(/^req_/, '').slice(0, 12)})` : ''}`;
108
+ }
109
+ function pricesLine(state) {
110
+ if (!state.publicMode || state.priceOutUsd == null)
111
+ return null;
112
+ const hourly = state.priceHourUsd && state.priceHourUsd > 0 ? `$${state.priceHourUsd.toFixed(2)}/h full GPU` : 'no hourly rentals';
113
+ return `$${state.priceOutUsd.toFixed(2)} per 1M tokens · ${hourly}`;
114
+ }
115
+ function modelLine(state) {
116
+ const model = state.job?.model || null;
117
+ if (!model)
118
+ return state.llamaStatus === 'ready' ? 'llama-server ready' : state.llamaStatus;
119
+ const mark = state.job?.verified ? ' ✓ verified' : '';
120
+ return `${modelShortName(model)}${mark}`;
121
+ }
122
+ function timerLine(state) {
123
+ // With several rentals, show the one that ends first.
124
+ const timed = (state.bookings ?? []).filter((rental) => rental.timerEndsAt)
125
+ .sort((a, b) => (a.timerEndsAt ?? 0) - (b.timerEndsAt ?? 0));
126
+ const job = timed[0] ?? state.job;
127
+ if (!job?.timerEndsAt)
128
+ return job?.kind === 'request' ? 'Timer starts after the model is verified' : '-';
129
+ const remaining = Math.max(0, Math.floor((job.timerEndsAt - Date.now()) / 1000));
130
+ const total = job.hours ? Math.round(job.hours * 3600) : Math.max(remaining, 1);
131
+ return `${formatClock(remaining)} / ${formatClock(total)}${timed.length > 1 ? ` · first of ${timed.length} to end` : ''}`;
132
+ }
133
+ function speedLine(state) {
134
+ const speed = formatSpeed(state.liveTokPerSec);
135
+ const reserved = (state.bookings ?? []).reduce((sum, job) => sum + (positive(job.tokS) ?? 0), 0);
136
+ if (reserved > 0)
137
+ return `${speed} (${tokS(reserved)} tok/s reserved by seats)`;
138
+ const minTokS = positive(state.job?.minTokS);
139
+ const min = minTokS ? ` (min required: ${minTokS.toFixed(1)} tok/s${state.liveTokPerSec != null && state.liveTokPerSec >= minTokS ? ' ✓' : ' !'})` : '';
140
+ return `${speed}${min}`;
141
+ }
142
+ function earningsLine(state) {
143
+ const guaranteed = state.bookings?.length ? state.sessionGuaranteedNanos : (state.job?.guaranteedNanos ?? state.sessionGuaranteedNanos);
144
+ const tokens = state.tokenEarnedNanos ?? 0;
145
+ const split = tokens > 0 && state.sessionEarnedNanos > tokens ? ` (${formatUsdNanos(tokens)} per-token)` : '';
146
+ return `${formatUsdNanos(state.sessionEarnedNanos)}${split}${guaranteed > 0 ? ` / ${formatUsdNanos(guaranteed)} Guaranteed` : ' this session'}`;
147
+ }
148
+ export function renderFrame(state, options = {}) {
149
+ const width = Math.max(54, options.width ?? 72);
150
+ const unicode = options.unicode ?? true;
151
+ const tl = unicode ? '┌' : '+';
152
+ const tr = unicode ? '┐' : '+';
153
+ const bl = unicode ? '└' : '+';
154
+ const br = unicode ? '┘' : '+';
155
+ const h = unicode ? '─' : '-';
156
+ const v = unicode ? '│' : '|';
157
+ const ml = unicode ? '├' : '+';
158
+ const mr = unicode ? '┤' : '+';
159
+ const status = statusLabel(state);
160
+ const dot = unicode ? '●' : '*';
161
+ const title = `${BRAND} GPU TERMINAL AGENT v${VERSION}`;
162
+ const statusText = `${dot} ${status.text}`;
163
+ const inner = width - 2;
164
+ const top = `${tl}${h.repeat(inner)}${tr}`;
165
+ const divider = `${ml}${h.repeat(inner)}${mr}`;
166
+ const bottom = `${bl}${h.repeat(inner)}${br}`;
167
+ const lines = [];
168
+ const add = (left, right = '') => {
169
+ const leftText = ` ${left}`;
170
+ const available = inner - 2;
171
+ const rightWidth = stripAnsi(right).length;
172
+ const clippedLeft = truncate(leftText, Math.max(0, available - rightWidth - 1));
173
+ const leftWidth = stripAnsi(clippedLeft).length;
174
+ const gap = Math.max(1, available - leftWidth - rightWidth);
175
+ const body = clippedLeft + ' '.repeat(gap) + right;
176
+ lines.push(`${v}${padRight(truncate(body, inner), inner)}${v}`);
177
+ };
178
+ const single = (text) => {
179
+ const body = ` ${text}`;
180
+ lines.push(`${v}${padRight(truncate(body, inner), inner)}${v}`);
181
+ };
182
+ lines.push(top);
183
+ add(`${title}`, `STATUS: ${statusText}`);
184
+ lines.push(divider);
185
+ add(`Host Endpoint : ${valueOrDash(state.endpoint)}`);
186
+ add(`Reliability : ${state.reliability != null ? formatPercent(state.reliability) : '-'}${state.held ? ' (HELD - see your host page)' : state.reliability != null ? ' (OK - held only after repeated failures)' : ''}`);
187
+ add(`GPU Detected : ${truncate(gpuLine(state), inner - 20)}`);
188
+ add(`GPU Temp/Power: ${thermalLine(state)}`);
189
+ lines.push(divider);
190
+ add(`CURRENT JOB : ${jobLine(state)}`);
191
+ const prices = pricesLine(state);
192
+ if (prices)
193
+ add(`Prices : ${prices}`);
194
+ add(`Model Loaded : ${modelLine(state)}`);
195
+ add(`Time Remaining: ${timerLine(state)}`);
196
+ add(`Live Speed : ${speedLine(state)}`);
197
+ lines.push(divider);
198
+ add(`Session Earn. : ${earningsLine(state)}`);
199
+ add(`Total Wallet : ${state.walletNanos != null ? formatUsdNanos(state.walletNanos) : '-'}`);
200
+ add(`Server : ${truncate(state.server, inner - 20)}`, state.lastHeartbeatAt ? `hb ${Math.max(0, Math.round((Date.now() - state.lastHeartbeatAt) / 1000))}s` : 'no heartbeat');
201
+ if (state.keyHint)
202
+ add(`Keys : ${truncate(state.keyHint, inner - 20)}`);
203
+ if (state.showLogs && state.logTail?.length) {
204
+ for (const line of state.logTail.slice(-3))
205
+ single(`log ${truncate(line, inner - 8)}`);
206
+ }
207
+ if (state.message)
208
+ add(`Note : ${truncate(state.message, inner - 20)}`);
209
+ lines.push(bottom);
210
+ if (!options.color)
211
+ return lines.join('\n');
212
+ const paintStatus = (text) => {
213
+ const color = status.status === 'online' || status.status === 'working'
214
+ ? '\x1b[32m'
215
+ : status.status === 'starting' || status.status === 'reconnecting'
216
+ ? '\x1b[33m'
217
+ : '\x1b[31m';
218
+ return `${text.replace(statusText, `${color}${statusText}\x1b[0m`)}`;
219
+ };
220
+ return lines.map((line, index) => index === 1 ? paintStatus(line) : line).join('\n');
221
+ }
222
+ export class TerminalDashboard {
223
+ state;
224
+ stream;
225
+ color;
226
+ unicode;
227
+ intervalMs;
228
+ enabled;
229
+ timer = null;
230
+ started = false;
231
+ lastFrame = '';
232
+ lastStatus = null;
233
+ constructor(state, options = {}) {
234
+ this.state = state;
235
+ this.stream = options.stream ?? process.stdout;
236
+ this.color = options.color ?? Boolean(this.stream.isTTY);
237
+ this.unicode = options.unicode ?? true;
238
+ this.intervalMs = options.intervalMs ?? 1_000;
239
+ this.enabled = options.enabled ?? true;
240
+ }
241
+ get current() { return this.state; }
242
+ start() {
243
+ if (this.started || !this.enabled)
244
+ return;
245
+ this.started = true;
246
+ this.lastStatus = this.state.status;
247
+ if (!this.stream.isTTY) {
248
+ this.printOnce();
249
+ return;
250
+ }
251
+ this.stream.write('\x1b[?25l');
252
+ this.rerender(true);
253
+ this.timer = setInterval(() => this.rerender(false), this.intervalMs);
254
+ }
255
+ update(partial) {
256
+ this.state = { ...this.state, ...partial };
257
+ if (!this.started)
258
+ return;
259
+ if (!this.stream.isTTY) {
260
+ if (this.state.status !== this.lastStatus) {
261
+ this.lastStatus = this.state.status;
262
+ this.printOnce();
263
+ }
264
+ return;
265
+ }
266
+ this.rerender(false);
267
+ }
268
+ setJob(job) {
269
+ this.update({ job });
270
+ }
271
+ printOnce() {
272
+ this.stream.write(`${renderFrame(this.state, { color: this.color, unicode: this.unicode })}\n`);
273
+ }
274
+ rerender(force) {
275
+ const frame = renderFrame(this.state, { color: this.color, unicode: this.unicode });
276
+ if (!force && frame === this.lastFrame)
277
+ return;
278
+ this.lastFrame = frame;
279
+ if (force)
280
+ this.stream.write('\x1b[H\x1b[2J');
281
+ else
282
+ this.stream.write('\x1b[H');
283
+ this.stream.write(frame);
284
+ this.stream.write('\x1b[0J');
285
+ }
286
+ stop() {
287
+ if (this.timer)
288
+ clearInterval(this.timer);
289
+ this.timer = null;
290
+ if (this.stream.isTTY && this.started)
291
+ this.stream.write('\x1b[?25h\n');
292
+ this.started = false;
293
+ }
294
+ }
295
+ //# sourceMappingURL=terminalDashboard.js.map
@@ -0,0 +1,3 @@
1
+ /** Prefer the Sublera spelling while retaining legacy deployment settings.
2
+ * An explicitly supplied new value wins, even when empty or invalid. */
3
+ export declare function brandEnv(env: NodeJS.ProcessEnv, legacyKey: string): string | undefined;
@@ -0,0 +1,6 @@
1
+ /** Prefer the Sublera spelling while retaining legacy deployment settings.
2
+ * An explicitly supplied new value wins, even when empty or invalid. */
3
+ export function brandEnv(env, legacyKey) {
4
+ return env[legacyKey.replace(/^SUBLET_/, 'SUBLERA_')] ?? env[legacyKey];
5
+ }
6
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1,23 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ export type ExecResult = {
3
+ code: number | null;
4
+ stdout: string;
5
+ stderr: string;
6
+ timedOut: boolean;
7
+ error?: string;
8
+ };
9
+ export type ExecOptions = {
10
+ timeoutMs?: number;
11
+ cwd?: string;
12
+ env?: NodeJS.ProcessEnv;
13
+ input?: string;
14
+ maxOutputBytes?: number;
15
+ windowsHide?: boolean;
16
+ };
17
+ export declare function toError(value: unknown): Error;
18
+ export declare function sleep(ms: number): Promise<void>;
19
+ export declare function execFile(command: string, args?: string[], options?: ExecOptions): Promise<ExecResult>;
20
+ export declare function killTree(child: ChildProcess | null | undefined): Promise<void>;
21
+ export declare function which(command: string): Promise<string | null>;
22
+ export declare function commandExists(command: string): Promise<boolean>;
23
+ export declare function commandLine(command: string, args: string[]): string;
@@ -0,0 +1,116 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { access, constants } from 'node:fs/promises';
3
+ import { delimiter, extname, join } from 'node:path';
4
+ export function toError(value) {
5
+ return value instanceof Error ? value : new Error(String(value));
6
+ }
7
+ export function sleep(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+ export async function execFile(command, args = [], options = {}) {
11
+ const timeoutMs = options.timeoutMs ?? 15_000;
12
+ const maxBytes = options.maxOutputBytes ?? 2 * 1024 * 1024;
13
+ const spawnOptions = {
14
+ cwd: options.cwd,
15
+ env: { ...process.env, ...options.env },
16
+ windowsHide: options.windowsHide ?? true,
17
+ stdio: ['pipe', 'pipe', 'pipe'],
18
+ };
19
+ return new Promise((resolve) => {
20
+ let child;
21
+ try {
22
+ child = spawn(command, args, spawnOptions);
23
+ }
24
+ catch (error) {
25
+ resolve({ code: null, stdout: '', stderr: '', timedOut: false, error: toError(error).message });
26
+ return;
27
+ }
28
+ let stdout = '';
29
+ let stderr = '';
30
+ let timedOut = false;
31
+ let settled = false;
32
+ const finish = (code, error) => {
33
+ if (settled)
34
+ return;
35
+ settled = true;
36
+ clearTimeout(timer);
37
+ resolve({ code, stdout, stderr, timedOut, error });
38
+ };
39
+ const append = (current, chunk) => {
40
+ if (current.length >= maxBytes)
41
+ return current;
42
+ const next = current + chunk.toString('utf8');
43
+ return next.length > maxBytes ? next.slice(0, maxBytes) : next;
44
+ };
45
+ child.stdout?.on('data', (chunk) => { stdout = append(stdout, chunk); });
46
+ child.stderr?.on('data', (chunk) => { stderr = append(stderr, chunk); });
47
+ child.on('error', (err) => finish(null, err.message));
48
+ child.on('close', (code) => finish(code));
49
+ const timer = setTimeout(() => {
50
+ timedOut = true;
51
+ void killTree(child).finally(() => finish(null, `Command timed out after ${timeoutMs}ms`));
52
+ }, timeoutMs);
53
+ if (options.input != null) {
54
+ child.stdin?.end(options.input);
55
+ }
56
+ else {
57
+ child.stdin?.end();
58
+ }
59
+ });
60
+ }
61
+ export async function killTree(child) {
62
+ if (!child || child.killed || child.exitCode != null)
63
+ return;
64
+ const pid = child.pid;
65
+ if (!pid)
66
+ return;
67
+ if (process.platform === 'win32') {
68
+ await execFile('taskkill', ['/pid', String(pid), '/T', '/F'], { timeoutMs: 10_000 });
69
+ return;
70
+ }
71
+ try {
72
+ process.kill(pid, 'SIGTERM');
73
+ }
74
+ catch {
75
+ return;
76
+ }
77
+ await sleep(800);
78
+ try {
79
+ process.kill(pid, 'SIGKILL');
80
+ }
81
+ catch {
82
+ /* already gone */
83
+ }
84
+ }
85
+ async function isExecutable(path) {
86
+ try {
87
+ await access(path, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
88
+ return true;
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
94
+ export async function which(command) {
95
+ const hasExt = extname(command) !== '';
96
+ const names = process.platform === 'win32' && !hasExt
97
+ ? [`${command}.exe`, `${command}.cmd`, `${command}.bat`, command]
98
+ : [command];
99
+ const dirs = (process.env.PATH || '').split(delimiter).filter(Boolean);
100
+ for (const dir of dirs) {
101
+ for (const name of names) {
102
+ const full = join(dir, name);
103
+ if (await isExecutable(full))
104
+ return full;
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+ export async function commandExists(command) {
110
+ return (await which(command)) != null;
111
+ }
112
+ export function commandLine(command, args) {
113
+ const quote = (value) => /[\s"]/g.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
114
+ return [command, ...args].map(quote).join(' ');
115
+ }
116
+ //# sourceMappingURL=exec.js.map
@@ -0,0 +1,47 @@
1
+ declare const ANSI: {
2
+ readonly reset: "\u001B[0m";
3
+ readonly bold: "\u001B[1m";
4
+ readonly dim: "\u001B[2m";
5
+ readonly red: "\u001B[31m";
6
+ readonly green: "\u001B[32m";
7
+ readonly yellow: "\u001B[33m";
8
+ readonly blue: "\u001B[34m";
9
+ readonly magenta: "\u001B[35m";
10
+ readonly cyan: "\u001B[36m";
11
+ readonly white: "\u001B[37m";
12
+ readonly gray: "\u001B[90m";
13
+ };
14
+ export type ColorName = keyof typeof ANSI;
15
+ export declare function colorEnabled(stream?: NodeJS.WriteStream, env?: NodeJS.ProcessEnv): boolean;
16
+ export declare function paint(text: string, color: ColorName, enabled: boolean): string;
17
+ export declare function bold(text: string, enabled: boolean): string;
18
+ export declare function dim(text: string, enabled: boolean): string;
19
+ export declare const ansi: {
20
+ readonly reset: "\u001B[0m";
21
+ readonly bold: "\u001B[1m";
22
+ readonly dim: "\u001B[2m";
23
+ readonly red: "\u001B[31m";
24
+ readonly green: "\u001B[32m";
25
+ readonly yellow: "\u001B[33m";
26
+ readonly blue: "\u001B[34m";
27
+ readonly magenta: "\u001B[35m";
28
+ readonly cyan: "\u001B[36m";
29
+ readonly white: "\u001B[37m";
30
+ readonly gray: "\u001B[90m";
31
+ };
32
+ export declare function formatBytes(bytes: number | null | undefined, digits?: number): string;
33
+ export declare function formatBytesPerSec(bytes: number): string;
34
+ export declare function formatDuration(totalSeconds: number | null | undefined): string;
35
+ export declare function formatClock(totalSeconds: number | null | undefined): string;
36
+ export declare function formatUsdNanos(nanos: number | null | undefined): string;
37
+ export declare function formatUsd(value: number | null | undefined): string;
38
+ export declare function formatSpeed(tps: number | null | undefined): string;
39
+ export declare function formatPercent(value: number | null | undefined, digits?: number): string;
40
+ export declare function formatNumber(value: number | null | undefined): string;
41
+ export declare function truncate(text: string, max: number): string;
42
+ export declare function padRight(text: string, width: number): string;
43
+ export declare function padLeft(text: string, width: number): string;
44
+ export declare function timeAgo(timestamp: number | null | undefined): string;
45
+ export declare function modelShortName(fileOrId: string | null | undefined): string;
46
+ export declare function stripAnsi(text: string): string;
47
+ export {};
@@ -0,0 +1,125 @@
1
+ const ANSI = {
2
+ reset: '\x1b[0m',
3
+ bold: '\x1b[1m',
4
+ dim: '\x1b[2m',
5
+ red: '\x1b[31m',
6
+ green: '\x1b[32m',
7
+ yellow: '\x1b[33m',
8
+ blue: '\x1b[34m',
9
+ magenta: '\x1b[35m',
10
+ cyan: '\x1b[36m',
11
+ white: '\x1b[37m',
12
+ gray: '\x1b[90m',
13
+ };
14
+ export function colorEnabled(stream = process.stdout, env = process.env) {
15
+ if ('NO_COLOR' in env && env.NO_COLOR !== '')
16
+ return false;
17
+ if (env.FORCE_COLOR === '0')
18
+ return false;
19
+ if (env.FORCE_COLOR && env.FORCE_COLOR !== '0')
20
+ return true;
21
+ return Boolean(stream.isTTY);
22
+ }
23
+ export function paint(text, color, enabled) {
24
+ return enabled ? `${ANSI[color]}${text}${ANSI.reset}` : text;
25
+ }
26
+ export function bold(text, enabled) {
27
+ return enabled ? `${ANSI.bold}${text}${ANSI.reset}` : text;
28
+ }
29
+ export function dim(text, enabled) {
30
+ return enabled ? `${ANSI.dim}${text}${ANSI.reset}` : text;
31
+ }
32
+ export const ansi = ANSI;
33
+ export function formatBytes(bytes, digits = 1) {
34
+ if (bytes == null || !Number.isFinite(bytes) || bytes < 0)
35
+ return '-';
36
+ if (bytes === 0)
37
+ return '0 B';
38
+ const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
39
+ const i = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
40
+ const value = bytes / 1024 ** i;
41
+ return `${value.toFixed(i === 0 ? 0 : digits)} ${units[i]}`;
42
+ }
43
+ export function formatBytesPerSec(bytes) {
44
+ return `${formatBytes(bytes, 1)}/s`;
45
+ }
46
+ export function formatDuration(totalSeconds) {
47
+ if (totalSeconds == null || !Number.isFinite(totalSeconds) || totalSeconds < 0)
48
+ return '-';
49
+ const s = Math.floor(totalSeconds % 60);
50
+ const m = Math.floor((totalSeconds / 60) % 60);
51
+ const h = Math.floor(totalSeconds / 3600);
52
+ if (h > 0)
53
+ return `${h}h ${String(m).padStart(2, '0')}m ${String(s).padStart(2, '0')}s`;
54
+ if (m > 0)
55
+ return `${m}m ${String(s).padStart(2, '0')}s`;
56
+ return `${s}s`;
57
+ }
58
+ export function formatClock(totalSeconds) {
59
+ if (totalSeconds == null || !Number.isFinite(totalSeconds) || totalSeconds < 0)
60
+ return '--:--:--';
61
+ const s = Math.floor(totalSeconds % 60);
62
+ const m = Math.floor((totalSeconds / 60) % 60);
63
+ const h = Math.floor(totalSeconds / 3600);
64
+ return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
65
+ }
66
+ export function formatUsdNanos(nanos) {
67
+ if (nanos == null || !Number.isFinite(nanos))
68
+ return '$0.00';
69
+ return `$${(nanos / 1e9).toFixed(2)}`;
70
+ }
71
+ export function formatUsd(value) {
72
+ if (value == null || !Number.isFinite(value))
73
+ return '$0.00';
74
+ return `$${value.toFixed(2)}`;
75
+ }
76
+ export function formatSpeed(tps) {
77
+ if (tps == null || !Number.isFinite(tps) || tps <= 0)
78
+ return '-';
79
+ return `${tps.toFixed(1)} tok/s`;
80
+ }
81
+ export function formatPercent(value, digits = 1) {
82
+ if (value == null || !Number.isFinite(value))
83
+ return '-';
84
+ return `${value.toFixed(digits)}%`;
85
+ }
86
+ export function formatNumber(value) {
87
+ if (value == null || !Number.isFinite(value))
88
+ return '-';
89
+ return new Intl.NumberFormat('en-US').format(value);
90
+ }
91
+ export function truncate(text, max) {
92
+ if (text.length <= max)
93
+ return text;
94
+ return `${text.slice(0, Math.max(0, max - 1))}…`;
95
+ }
96
+ export function padRight(text, width) {
97
+ return text.length >= width ? text : text + ' '.repeat(width - text.length);
98
+ }
99
+ export function padLeft(text, width) {
100
+ return text.length >= width ? text : ' '.repeat(width - text.length) + text;
101
+ }
102
+ export function timeAgo(timestamp) {
103
+ if (!timestamp)
104
+ return 'never';
105
+ const seconds = Math.max(0, Math.round((Date.now() - timestamp) / 1000));
106
+ if (seconds < 5)
107
+ return 'just now';
108
+ if (seconds < 60)
109
+ return `${seconds}s ago`;
110
+ if (seconds < 3600)
111
+ return `${Math.floor(seconds / 60)}m ago`;
112
+ if (seconds < 86400)
113
+ return `${Math.floor(seconds / 3600)}h ago`;
114
+ return `${Math.floor(seconds / 86400)}d ago`;
115
+ }
116
+ export function modelShortName(fileOrId) {
117
+ if (!fileOrId)
118
+ return '-';
119
+ const base = fileOrId.split(/[\\/]/).pop() || fileOrId;
120
+ return base.replace(/\.gguf$/i, '');
121
+ }
122
+ export function stripAnsi(text) {
123
+ return text.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
124
+ }
125
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1,18 @@
1
+ import { type AgentConfig } from '../config.js';
2
+ export declare function ensureDir(path: string, mode?: number): Promise<void>;
3
+ export declare function pathExists(path: string): Promise<boolean>;
4
+ export declare function fileSize(path: string): Promise<number>;
5
+ export declare function removeIfExists(path: string): Promise<void>;
6
+ /** Bytes available to this user on the disk that holds `dir`, or null when the OS cannot tell. */
7
+ export declare function freeDiskBytes(dir: string): Promise<number | null>;
8
+ /** Serialize same-process writers; each replacement still uses a distinct, synced temporary file. */
9
+ export declare function atomicWrite(path: string, data: string, mode?: number): Promise<void>;
10
+ export declare function readConfigFile(path?: string): Promise<AgentConfig | null>;
11
+ export declare function writeConfigFile(config: AgentConfig, path?: string): Promise<void>;
12
+ export declare function logLine(file: string, line: string): Promise<void>;
13
+ export declare function sha256Hex(input: string | Buffer): string;
14
+ export declare function sha256File(path: string, onProgress?: (bytes: number, total: number) => void): Promise<string>;
15
+ export declare function readFirstBytes(path: string, length: number): Promise<Buffer>;
16
+ /** Refuse paths that escape a directory, e.g. from a malicious archive. */
17
+ export declare function safeJoin(root: string, candidate: string): string;
18
+ export declare function shortPath(path: string, max?: number): string;