ollanet 0.4.1 → 0.6.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 +95 -9
- package/dist/bench-suite.d.ts +2 -0
- package/dist/bench-suite.js +1 -0
- package/dist/bench.js +54 -18
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +52 -2
- package/dist/compare-store.d.ts +8 -0
- package/dist/compare-store.js +55 -0
- package/dist/compare.d.ts +52 -0
- package/dist/compare.js +338 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.js +9 -1
- package/dist/mcp.js +235 -3
- package/dist/ollama-chat.d.ts +46 -2
- package/dist/ollama-chat.js +121 -2
- package/dist/paths.d.ts +4 -0
- package/dist/paths.js +8 -0
- package/dist/prompt-input.d.ts +7 -0
- package/dist/prompt-input.js +36 -0
- package/dist/prompt.js +28 -5
- package/dist/ps.d.ts +31 -0
- package/dist/ps.js +165 -0
- package/dist/pull.d.ts +36 -0
- package/dist/pull.js +198 -0
- package/dist/rm.d.ts +22 -0
- package/dist/rm.js +134 -0
- package/dist/scan-store.d.ts +15 -0
- package/dist/scan-store.js +41 -0
- package/dist/scan.d.ts +14 -1
- package/dist/scan.js +76 -42
- package/dist/show.d.ts +28 -0
- package/dist/show.js +131 -0
- package/dist/target.d.ts +4 -0
- package/dist/target.js +18 -0
- package/dist/tuned.d.ts +8 -0
- package/dist/tuned.js +17 -0
- package/package.json +8 -2
package/dist/ollama-chat.d.ts
CHANGED
|
@@ -50,9 +50,26 @@ export interface OllamaModelInfo {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
export declare function ollamaTags(baseUrl: string, timeoutMs: number): Promise<OllamaModelInfo[]>;
|
|
53
|
-
export
|
|
53
|
+
export interface OllamaShowInfo {
|
|
54
54
|
capabilities?: string[];
|
|
55
|
-
|
|
55
|
+
modelfile?: string;
|
|
56
|
+
parameters?: string;
|
|
57
|
+
template?: string;
|
|
58
|
+
system?: string;
|
|
59
|
+
details?: {
|
|
60
|
+
parent_model?: string;
|
|
61
|
+
format?: string;
|
|
62
|
+
family?: string;
|
|
63
|
+
families?: string[];
|
|
64
|
+
parameter_size?: string;
|
|
65
|
+
quantization_level?: string;
|
|
66
|
+
};
|
|
67
|
+
model_info?: Record<string, unknown>;
|
|
68
|
+
modified_at?: string;
|
|
69
|
+
}
|
|
70
|
+
export declare function ollamaShow(baseUrl: string, model: string, timeoutMs: number): Promise<OllamaShowInfo>;
|
|
71
|
+
/** POST /api/delete — removes the named model from that host's disk. */
|
|
72
|
+
export declare function ollamaDelete(baseUrl: string, model: string, timeoutMs: number): Promise<void>;
|
|
56
73
|
export declare function ollamaVersion(baseUrl: string, timeoutMs: number): Promise<string | undefined>;
|
|
57
74
|
export interface PsModel {
|
|
58
75
|
name?: string;
|
|
@@ -61,7 +78,34 @@ export interface PsModel {
|
|
|
61
78
|
context_length?: number;
|
|
62
79
|
digest?: string;
|
|
63
80
|
}
|
|
81
|
+
export interface PullChunk {
|
|
82
|
+
status?: string;
|
|
83
|
+
digest?: string;
|
|
84
|
+
total?: number;
|
|
85
|
+
completed?: number;
|
|
86
|
+
error?: string;
|
|
87
|
+
}
|
|
88
|
+
export interface OllamaPullOptions {
|
|
89
|
+
baseUrl: string;
|
|
90
|
+
model: string;
|
|
91
|
+
/** Allow HTTP / self-signed registries (Ollama `insecure`). */
|
|
92
|
+
insecure?: boolean;
|
|
93
|
+
stream: boolean;
|
|
94
|
+
timeoutMs: number;
|
|
95
|
+
onProgress?: (chunk: PullChunk) => void;
|
|
96
|
+
}
|
|
97
|
+
export interface OllamaPullResult {
|
|
98
|
+
status: string;
|
|
99
|
+
chunks: PullChunk[];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* POST /api/pull — the *server* downloads the model. The client only sends the
|
|
103
|
+
* request. Default timeout 0 (none): large pulls can take hours.
|
|
104
|
+
*/
|
|
105
|
+
export declare function ollamaPull(opts: OllamaPullOptions): Promise<OllamaPullResult>;
|
|
64
106
|
export declare function ollamaPs(baseUrl: string, timeoutMs: number): Promise<PsModel[]>;
|
|
107
|
+
/** Like ollamaPs, but unreachable hosts throw instead of looking empty. */
|
|
108
|
+
export declare function ollamaPsRequired(baseUrl: string, timeoutMs: number): Promise<PsModel[]>;
|
|
65
109
|
export declare function psHasModel(models: PsModel[], name: string): boolean;
|
|
66
110
|
export declare function vramForModel(models: PsModel[], name: string): number | undefined;
|
|
67
111
|
export declare function contextLengthForModel(models: PsModel[], name: string): number | undefined;
|
package/dist/ollama-chat.js
CHANGED
|
@@ -164,6 +164,10 @@ export async function ollamaShow(baseUrl, model, timeoutMs) {
|
|
|
164
164
|
const url = `${baseUrl}/api/show`;
|
|
165
165
|
return ollamaPostJson(url, { model }, timeoutMs);
|
|
166
166
|
}
|
|
167
|
+
/** POST /api/delete — removes the named model from that host's disk. */
|
|
168
|
+
export async function ollamaDelete(baseUrl, model, timeoutMs) {
|
|
169
|
+
await ollamaPostJson(`${baseUrl}/api/delete`, { model }, timeoutMs);
|
|
170
|
+
}
|
|
167
171
|
export async function ollamaVersion(baseUrl, timeoutMs) {
|
|
168
172
|
try {
|
|
169
173
|
const body = await ollamaGetJson(`${baseUrl}/api/version`, timeoutMs);
|
|
@@ -173,15 +177,128 @@ export async function ollamaVersion(baseUrl, timeoutMs) {
|
|
|
173
177
|
return undefined;
|
|
174
178
|
}
|
|
175
179
|
}
|
|
180
|
+
function parsePullChunk(url, raw) {
|
|
181
|
+
try {
|
|
182
|
+
return JSON.parse(raw);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
throw new Error(`Non-JSON response from ${url} (is something else on this port?): ${raw.slice(0, 120)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* POST /api/pull — the *server* downloads the model. The client only sends the
|
|
190
|
+
* request. Default timeout 0 (none): large pulls can take hours.
|
|
191
|
+
*/
|
|
192
|
+
export async function ollamaPull(opts) {
|
|
193
|
+
const url = `${opts.baseUrl}/api/pull`;
|
|
194
|
+
const body = {
|
|
195
|
+
model: opts.model,
|
|
196
|
+
stream: opts.stream,
|
|
197
|
+
};
|
|
198
|
+
if (opts.insecure)
|
|
199
|
+
body.insecure = true;
|
|
200
|
+
const controller = new AbortController();
|
|
201
|
+
const timer = opts.timeoutMs > 0 ? setTimeout(() => controller.abort(), opts.timeoutMs) : undefined;
|
|
202
|
+
let res;
|
|
203
|
+
try {
|
|
204
|
+
res = await fetch(url, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
207
|
+
body: JSON.stringify(body),
|
|
208
|
+
signal: controller.signal,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
if (timer)
|
|
213
|
+
clearTimeout(timer);
|
|
214
|
+
const name = err instanceof Error ? err.name : "";
|
|
215
|
+
if (name === "AbortError") {
|
|
216
|
+
throw new Error(`Pull timed out after ${opts.timeoutMs}ms (${url})`);
|
|
217
|
+
}
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
if (!res.ok) {
|
|
221
|
+
if (timer)
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
const text = await res.text().catch(() => "");
|
|
224
|
+
let detail = text;
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(text);
|
|
227
|
+
if (parsed.error)
|
|
228
|
+
detail = parsed.error;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// keep raw text
|
|
232
|
+
}
|
|
233
|
+
throw new Error(`Ollama HTTP ${res.status} ${res.statusText}${detail ? `: ${detail}` : ""}`);
|
|
234
|
+
}
|
|
235
|
+
const chunks = [];
|
|
236
|
+
const take = (chunk) => {
|
|
237
|
+
if (chunk.error)
|
|
238
|
+
throw new Error(chunk.error);
|
|
239
|
+
chunks.push(chunk);
|
|
240
|
+
opts.onProgress?.(chunk);
|
|
241
|
+
};
|
|
242
|
+
try {
|
|
243
|
+
if (!opts.stream) {
|
|
244
|
+
take(parsePullChunk(url, await res.text()));
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
if (!res.body)
|
|
248
|
+
throw new Error("No response body from Ollama");
|
|
249
|
+
const reader = res.body.getReader();
|
|
250
|
+
const decoder = new TextDecoder();
|
|
251
|
+
let buffer = "";
|
|
252
|
+
while (true) {
|
|
253
|
+
const { done, value } = await reader.read();
|
|
254
|
+
if (done)
|
|
255
|
+
break;
|
|
256
|
+
buffer += decoder.decode(value, { stream: true });
|
|
257
|
+
const lines = buffer.split("\n");
|
|
258
|
+
buffer = lines.pop() ?? "";
|
|
259
|
+
for (const line of lines) {
|
|
260
|
+
const trimmed = line.trim();
|
|
261
|
+
if (!trimmed)
|
|
262
|
+
continue;
|
|
263
|
+
take(parsePullChunk(url, trimmed));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const trailing = buffer.trim();
|
|
267
|
+
if (trailing)
|
|
268
|
+
take(parsePullChunk(url, trailing));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
const name = err instanceof Error ? err.name : "";
|
|
273
|
+
if (name === "AbortError") {
|
|
274
|
+
throw new Error(`Pull timed out after ${opts.timeoutMs}ms (${url})`);
|
|
275
|
+
}
|
|
276
|
+
throw err;
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
if (timer)
|
|
280
|
+
clearTimeout(timer);
|
|
281
|
+
}
|
|
282
|
+
const last = chunks.at(-1);
|
|
283
|
+
const status = last?.status ?? "";
|
|
284
|
+
if (status !== "success") {
|
|
285
|
+
throw new Error(status ? `Pull ended: ${status}` : "Pull failed (no success status)");
|
|
286
|
+
}
|
|
287
|
+
return { status, chunks };
|
|
288
|
+
}
|
|
176
289
|
export async function ollamaPs(baseUrl, timeoutMs) {
|
|
177
290
|
try {
|
|
178
|
-
|
|
179
|
-
return body.models ?? [];
|
|
291
|
+
return await ollamaPsRequired(baseUrl, timeoutMs);
|
|
180
292
|
}
|
|
181
293
|
catch {
|
|
182
294
|
return [];
|
|
183
295
|
}
|
|
184
296
|
}
|
|
297
|
+
/** Like ollamaPs, but unreachable hosts throw instead of looking empty. */
|
|
298
|
+
export async function ollamaPsRequired(baseUrl, timeoutMs) {
|
|
299
|
+
const body = await ollamaGetJson(`${baseUrl}/api/ps`, timeoutMs);
|
|
300
|
+
return body.models ?? [];
|
|
301
|
+
}
|
|
185
302
|
function modelKey(name) {
|
|
186
303
|
return name.trim().toLowerCase();
|
|
187
304
|
}
|
|
@@ -293,6 +410,8 @@ async function ollamaPostJson(url, body, timeoutMs) {
|
|
|
293
410
|
throw new Error(`Ollama HTTP ${res.status} ${res.statusText}${text ? `: ${text}` : ""} (${url})`);
|
|
294
411
|
}
|
|
295
412
|
const text = await res.text();
|
|
413
|
+
if (!text.trim())
|
|
414
|
+
return {};
|
|
296
415
|
try {
|
|
297
416
|
return JSON.parse(text);
|
|
298
417
|
}
|
package/dist/paths.d.ts
CHANGED
|
@@ -15,3 +15,7 @@ export declare function defaultConfigPath(): string;
|
|
|
15
15
|
export declare function defaultResponsesDir(): string;
|
|
16
16
|
/** Default benchmark results dir (mirrors responses layout). */
|
|
17
17
|
export declare function defaultBenchmarksDir(): string;
|
|
18
|
+
/** Last successful scan snapshot (single file). */
|
|
19
|
+
export declare function defaultLastScanPath(): string;
|
|
20
|
+
/** Head-to-head compare records (markdown + JSON). */
|
|
21
|
+
export declare function defaultComparesDir(): string;
|
package/dist/paths.js
CHANGED
|
@@ -26,3 +26,11 @@ export function defaultResponsesDir() {
|
|
|
26
26
|
export function defaultBenchmarksDir() {
|
|
27
27
|
return IS_INSTALLED ? path.join(USER_DIR, "benchmarks") : projectPath("benchmarks");
|
|
28
28
|
}
|
|
29
|
+
/** Last successful scan snapshot (single file). */
|
|
30
|
+
export function defaultLastScanPath() {
|
|
31
|
+
return IS_INSTALLED ? path.join(USER_DIR, "last-scan.json") : projectPath("last-scan.json");
|
|
32
|
+
}
|
|
33
|
+
/** Head-to-head compare records (markdown + JSON). */
|
|
34
|
+
export function defaultComparesDir() {
|
|
35
|
+
return IS_INSTALLED ? path.join(USER_DIR, "compares") : projectPath("compares");
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared prompt assembly: argv, --file (.txt / .md), and piped stdin.
|
|
3
|
+
*/
|
|
4
|
+
import { readFile, stat } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
const ALLOWED = new Set([".txt", ".md"]);
|
|
7
|
+
const MAX_BYTES = 1_000_000;
|
|
8
|
+
export function assertPromptFilename(filePath) {
|
|
9
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
10
|
+
if (!ALLOWED.has(ext)) {
|
|
11
|
+
throw new Error(`Prompt file must be .txt or .md (got "${ext || "no extension"}": ${filePath})`);
|
|
12
|
+
}
|
|
13
|
+
return path.resolve(filePath);
|
|
14
|
+
}
|
|
15
|
+
export async function readPromptFile(filePath) {
|
|
16
|
+
const resolved = assertPromptFilename(filePath);
|
|
17
|
+
const info = await stat(resolved);
|
|
18
|
+
if (!info.isFile()) {
|
|
19
|
+
throw new Error(`Prompt path is not a file: ${resolved}`);
|
|
20
|
+
}
|
|
21
|
+
if (info.size > MAX_BYTES) {
|
|
22
|
+
throw new Error(`Prompt file is too large (${info.size} bytes; max ${MAX_BYTES}): ${resolved}`);
|
|
23
|
+
}
|
|
24
|
+
const text = (await readFile(resolved, "utf8")).replace(/^\uFEFF/, "");
|
|
25
|
+
if (!text.trim()) {
|
|
26
|
+
throw new Error(`Prompt file is empty: ${resolved}`);
|
|
27
|
+
}
|
|
28
|
+
return text;
|
|
29
|
+
}
|
|
30
|
+
export function assemblePrompt(parts) {
|
|
31
|
+
return [parts.argv, parts.file, parts.stdin]
|
|
32
|
+
.map((s) => (s ?? "").trim())
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.join("\n\n")
|
|
35
|
+
.trim();
|
|
36
|
+
}
|
package/dist/prompt.js
CHANGED
|
@@ -16,6 +16,7 @@ import { appendMessage, cleanTopic, createChat, loadChat, saveChat, toApiMessage
|
|
|
16
16
|
import { configPath, defaultModelForHost, loadConfig, machineSettingsForHost, mergeSettings, parseFormat, settingsFromEnv, settingsSummary, configFromPartial, } from "./config.js";
|
|
17
17
|
import { discoverHosts, envInt, findDiscoveredHost, ollamaBaseUrl, resolveHost, shortName, } from "./hosts.js";
|
|
18
18
|
import { ollamaChat } from "./ollama-chat.js";
|
|
19
|
+
import { assemblePrompt, readPromptFile } from "./prompt-input.js";
|
|
19
20
|
/** Prompt/chat HTTP timeout (ms). 0 = no timeout. Separate from scan's OLLAMA_TIMEOUT_MS. */
|
|
20
21
|
function promptTimeoutMs() {
|
|
21
22
|
return envInt("OLLAMA_PROMPT_TIMEOUT_MS", 600_000);
|
|
@@ -30,6 +31,7 @@ Examples:
|
|
|
30
31
|
ollanet prompt localhost "Explain MagicDNS"
|
|
31
32
|
ollanet prompt --chat a1b2c3d4e5f6 "Give an example"
|
|
32
33
|
ollanet prompt localhost --temperature 0.2 --num-predict 64 "2+2?"
|
|
34
|
+
ollanet prompt localhost --file ./notes.md
|
|
33
35
|
ollanet chats
|
|
34
36
|
|
|
35
37
|
Each reply is saved under responses/<hash>.json and the hash is printed so you
|
|
@@ -51,6 +53,7 @@ Options:
|
|
|
51
53
|
--format <json|schema> Force JSON mode or a JSON schema string
|
|
52
54
|
--think Enable model thinking (qwen3 etc.); streams to stderr
|
|
53
55
|
--no-think Disable thinking (default) so tokens go to the reply
|
|
56
|
+
--file <path> Prompt from a .txt or .md file (joins argv / stdin)
|
|
54
57
|
--no-stream Wait for the full response
|
|
55
58
|
--json Emit final response JSON (implies --no-stream)`);
|
|
56
59
|
process.exit(1);
|
|
@@ -137,6 +140,7 @@ function parseArgs(argv) {
|
|
|
137
140
|
let machineFlag;
|
|
138
141
|
let modelFlag;
|
|
139
142
|
let chatId;
|
|
143
|
+
let file;
|
|
140
144
|
let stream = true;
|
|
141
145
|
let json = false;
|
|
142
146
|
let save = true;
|
|
@@ -181,6 +185,10 @@ function parseArgs(argv) {
|
|
|
181
185
|
modelFlag = arg.includes("=") ? arg.slice("--model=".length) : takeValue(args, "--model");
|
|
182
186
|
continue;
|
|
183
187
|
}
|
|
188
|
+
if (arg === "--file" || arg === "-f" || arg.startsWith("--file=")) {
|
|
189
|
+
file = arg.includes("=") ? arg.slice("--file=".length) : takeValue(args, "--file");
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
184
192
|
if (arg === "--system" || arg.startsWith("--system=")) {
|
|
185
193
|
settings.system = arg.includes("=")
|
|
186
194
|
? arg.slice("--system=".length)
|
|
@@ -235,7 +243,7 @@ function parseArgs(argv) {
|
|
|
235
243
|
}
|
|
236
244
|
// Continue (--chat): all positionals are the prompt. Optional machine override
|
|
237
245
|
// via --machine, or a leading discovered-host name peeled in main().
|
|
238
|
-
return { machine, model, promptParts, stream, json, save, chatId, settings };
|
|
246
|
+
return { machine, model, promptParts, file, stream, json, save, chatId, settings };
|
|
239
247
|
}
|
|
240
248
|
async function readStdinIfPiped() {
|
|
241
249
|
if (stdinStream.isTTY)
|
|
@@ -261,7 +269,7 @@ async function readStdinIfPiped() {
|
|
|
261
269
|
const onEnd = () => done(true);
|
|
262
270
|
stdinStream.once("readable", onReadable);
|
|
263
271
|
stdinStream.once("end", onEnd);
|
|
264
|
-
const timer = setTimeout(() => done(stdinStream.readableLength > 0),
|
|
272
|
+
const timer = setTimeout(() => done(stdinStream.readableLength > 0), 100);
|
|
265
273
|
});
|
|
266
274
|
if (!hasData)
|
|
267
275
|
return "";
|
|
@@ -436,6 +444,17 @@ export async function main() {
|
|
|
436
444
|
const { stream, json, save } = parsed;
|
|
437
445
|
let promptParts = parsed.promptParts;
|
|
438
446
|
const fromStdin = await readStdinIfPiped();
|
|
447
|
+
let fromFile = "";
|
|
448
|
+
if (parsed.file) {
|
|
449
|
+
try {
|
|
450
|
+
fromFile = await readPromptFile(parsed.file);
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
console.error(err instanceof Error ? err.message : err);
|
|
454
|
+
process.exitCode = 1;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
439
458
|
const config = await loadConfig();
|
|
440
459
|
const { hosts: targets } = await discoverHosts({
|
|
441
460
|
hosts: config.hosts,
|
|
@@ -460,13 +479,17 @@ export async function main() {
|
|
|
460
479
|
usage();
|
|
461
480
|
}
|
|
462
481
|
const host = resolveHost(targets, machineQuery);
|
|
463
|
-
const peeled = await maybePeelModel(host, promptParts, fromStdin);
|
|
482
|
+
const peeled = await maybePeelModel(host, promptParts, fromStdin || fromFile);
|
|
464
483
|
peeledModel = peeled.model;
|
|
465
484
|
promptParts = peeled.promptParts;
|
|
466
485
|
}
|
|
467
|
-
const promptText =
|
|
486
|
+
const promptText = assemblePrompt({
|
|
487
|
+
argv: promptParts.join(" "),
|
|
488
|
+
file: fromFile,
|
|
489
|
+
stdin: fromStdin,
|
|
490
|
+
});
|
|
468
491
|
if (!promptText) {
|
|
469
|
-
console.error("No prompt provided (pass args and/or pipe stdin).");
|
|
492
|
+
console.error("No prompt provided (pass args, --file, and/or pipe stdin).");
|
|
470
493
|
usage();
|
|
471
494
|
}
|
|
472
495
|
try {
|
package/dist/ps.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List models currently loaded in VRAM on reachable Ollama hosts.
|
|
3
|
+
*
|
|
4
|
+
* ollanet ps
|
|
5
|
+
* ollanet ps studio
|
|
6
|
+
*/
|
|
7
|
+
import { type AppConfig } from "./config.js";
|
|
8
|
+
export interface LoadedModel {
|
|
9
|
+
name: string;
|
|
10
|
+
size_vram?: number;
|
|
11
|
+
context_length?: number;
|
|
12
|
+
digest?: string;
|
|
13
|
+
tuned: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface LoadedHost {
|
|
16
|
+
machine: string;
|
|
17
|
+
endpoint: string;
|
|
18
|
+
ok: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
models: LoadedModel[];
|
|
21
|
+
}
|
|
22
|
+
export interface PsOptions {
|
|
23
|
+
machine?: string;
|
|
24
|
+
config?: Partial<AppConfig>;
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface PsResult {
|
|
28
|
+
hosts: LoadedHost[];
|
|
29
|
+
}
|
|
30
|
+
export declare function listLoaded(opts?: PsOptions): Promise<PsResult>;
|
|
31
|
+
export declare function main(): Promise<void>;
|
package/dist/ps.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List models currently loaded in VRAM on reachable Ollama hosts.
|
|
3
|
+
*
|
|
4
|
+
* ollanet ps
|
|
5
|
+
* ollanet ps studio
|
|
6
|
+
*/
|
|
7
|
+
import { configFromPartial, loadConfig } from "./config.js";
|
|
8
|
+
import { discoverHosts, envInt, ollamaBaseUrl, shortName, } from "./hosts.js";
|
|
9
|
+
import { ollamaPs, ollamaPsRequired } from "./ollama-chat.js";
|
|
10
|
+
import { resolveTarget } from "./target.js";
|
|
11
|
+
import { looksTuned } from "./tuned.js";
|
|
12
|
+
function psTimeoutMs() {
|
|
13
|
+
return envInt("OLLAMA_PS_TIMEOUT_MS", 10_000);
|
|
14
|
+
}
|
|
15
|
+
function usage() {
|
|
16
|
+
console.error(`Usage:
|
|
17
|
+
ollanet ps [machine]
|
|
18
|
+
ollanet ps --machine <name>
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
ollanet ps
|
|
22
|
+
ollanet ps studio
|
|
23
|
+
ollanet ps studio --json
|
|
24
|
+
|
|
25
|
+
scan lists models on disk. ps lists what is loaded in VRAM right now.
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--machine <name> One host (default: every discovered host)
|
|
29
|
+
--json Emit result JSON on stdout`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
function takeValue(args, flag) {
|
|
33
|
+
const value = args.shift();
|
|
34
|
+
if (!value) {
|
|
35
|
+
console.error(`${flag} requires a value`);
|
|
36
|
+
usage();
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const args = [...argv];
|
|
42
|
+
let machineFlag;
|
|
43
|
+
let json = false;
|
|
44
|
+
const positional = [];
|
|
45
|
+
while (args.length > 0) {
|
|
46
|
+
const arg = args.shift();
|
|
47
|
+
if (arg === "--")
|
|
48
|
+
continue;
|
|
49
|
+
if (arg === "--help" || arg === "-h")
|
|
50
|
+
usage();
|
|
51
|
+
if (arg === "--json") {
|
|
52
|
+
json = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (arg === "--machine") {
|
|
56
|
+
machineFlag = takeValue(args, "--machine");
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (arg.startsWith("-")) {
|
|
60
|
+
console.error(`Unknown flag: ${arg}`);
|
|
61
|
+
usage();
|
|
62
|
+
}
|
|
63
|
+
positional.push(arg);
|
|
64
|
+
}
|
|
65
|
+
if (positional.length > 1 || (machineFlag && positional.length > 0)) {
|
|
66
|
+
console.error("Too many arguments.");
|
|
67
|
+
usage();
|
|
68
|
+
}
|
|
69
|
+
return { machine: machineFlag ?? positional[0], json };
|
|
70
|
+
}
|
|
71
|
+
function formatBytes(bytes) {
|
|
72
|
+
if (bytes == null || !Number.isFinite(bytes))
|
|
73
|
+
return "";
|
|
74
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
75
|
+
let value = bytes;
|
|
76
|
+
let unit = 0;
|
|
77
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
78
|
+
value /= 1024;
|
|
79
|
+
unit += 1;
|
|
80
|
+
}
|
|
81
|
+
return `${value.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
|
|
82
|
+
}
|
|
83
|
+
function toLoaded(m) {
|
|
84
|
+
const name = (m.name ?? m.model ?? "").trim();
|
|
85
|
+
return {
|
|
86
|
+
name,
|
|
87
|
+
size_vram: m.size_vram,
|
|
88
|
+
context_length: m.context_length,
|
|
89
|
+
digest: m.digest,
|
|
90
|
+
tuned: looksTuned(name),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function psHost(host, timeoutMs, required) {
|
|
94
|
+
const endpoint = ollamaBaseUrl(host);
|
|
95
|
+
try {
|
|
96
|
+
const models = required
|
|
97
|
+
? await ollamaPsRequired(endpoint, timeoutMs)
|
|
98
|
+
: await ollamaPs(endpoint, timeoutMs);
|
|
99
|
+
return {
|
|
100
|
+
machine: shortName(host),
|
|
101
|
+
endpoint,
|
|
102
|
+
ok: true,
|
|
103
|
+
models: models.map(toLoaded).filter((m) => m.name),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
return {
|
|
108
|
+
machine: shortName(host),
|
|
109
|
+
endpoint,
|
|
110
|
+
ok: false,
|
|
111
|
+
error: err instanceof Error ? err.message : String(err),
|
|
112
|
+
models: [],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function listLoaded(opts = {}) {
|
|
117
|
+
const timeoutMs = opts.timeoutMs ?? psTimeoutMs();
|
|
118
|
+
if (opts.machine?.trim()) {
|
|
119
|
+
const host = await resolveTarget(opts.machine, opts.config);
|
|
120
|
+
const row = await psHost(host, timeoutMs, true);
|
|
121
|
+
if (!row.ok)
|
|
122
|
+
throw new Error(row.error ?? `ps failed on ${row.machine}`);
|
|
123
|
+
return { hosts: [row] };
|
|
124
|
+
}
|
|
125
|
+
const app = opts.config ? configFromPartial(opts.config) : await loadConfig();
|
|
126
|
+
const { hosts } = await discoverHosts({
|
|
127
|
+
hosts: app.hosts,
|
|
128
|
+
discovery: app.discovery,
|
|
129
|
+
});
|
|
130
|
+
const rows = await Promise.all(hosts
|
|
131
|
+
.filter((h) => h.online || h.isSelf || h.source !== "tailscale")
|
|
132
|
+
.map((h) => psHost(h, timeoutMs, false)));
|
|
133
|
+
return { hosts: rows.filter((r) => r.ok) };
|
|
134
|
+
}
|
|
135
|
+
function printPs(result) {
|
|
136
|
+
if (result.hosts.length === 0) {
|
|
137
|
+
process.stdout.write("No hosts reported loaded models.\n");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
for (const host of result.hosts) {
|
|
141
|
+
process.stdout.write(`${host.machine} ${host.endpoint}\n`);
|
|
142
|
+
if (host.models.length === 0) {
|
|
143
|
+
process.stdout.write(" (nothing loaded)\n");
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
for (const m of host.models) {
|
|
147
|
+
const vram = formatBytes(m.size_vram);
|
|
148
|
+
const ctx = m.context_length != null ? `ctx ${m.context_length}` : "";
|
|
149
|
+
const tag = m.tuned ? " [tuned]" : "";
|
|
150
|
+
const meta = [vram ? `VRAM ${vram}` : "", ctx].filter(Boolean).join(" ");
|
|
151
|
+
process.stdout.write(meta ? ` ${m.name} ${meta}${tag}\n` : ` ${m.name}${tag}\n`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
process.stdout.write("\n");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function main() {
|
|
158
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
159
|
+
const result = await listLoaded({ machine: parsed.machine });
|
|
160
|
+
if (parsed.json) {
|
|
161
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
printPs(result);
|
|
165
|
+
}
|
package/dist/pull.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pull a model onto a reachable Ollama host.
|
|
3
|
+
*
|
|
4
|
+
* ollanet pull studio gemma3:12b
|
|
5
|
+
*
|
|
6
|
+
* `studio` is the machine (discovered name, MagicDNS, hostname, or IP).
|
|
7
|
+
* The *server* downloads from the registry; ollanet only sends POST /api/pull.
|
|
8
|
+
*/
|
|
9
|
+
import type { AppConfig } from "./config.js";
|
|
10
|
+
import { type PullChunk } from "./ollama-chat.js";
|
|
11
|
+
export interface PullOptions {
|
|
12
|
+
machine: string;
|
|
13
|
+
model: string;
|
|
14
|
+
insecure?: boolean;
|
|
15
|
+
stream?: boolean;
|
|
16
|
+
/** Progress on stderr. Default true for CLI; pass false for MCP / apps. */
|
|
17
|
+
writeStdout?: boolean;
|
|
18
|
+
quiet?: boolean;
|
|
19
|
+
/** In-memory config; skips the config file when set. */
|
|
20
|
+
config?: Partial<AppConfig>;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
onProgress?: (chunk: PullChunk) => void;
|
|
23
|
+
}
|
|
24
|
+
export interface PullResult {
|
|
25
|
+
machine: string;
|
|
26
|
+
model: string;
|
|
27
|
+
status: string;
|
|
28
|
+
endpoint: string;
|
|
29
|
+
next: {
|
|
30
|
+
hint: string;
|
|
31
|
+
site: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Programmatic pull used by the CLI, MCP, and apps. */
|
|
35
|
+
export declare function pullModel(opts: PullOptions): Promise<PullResult>;
|
|
36
|
+
export declare function main(): Promise<void>;
|