ollanet 0.4.1 → 0.5.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 CHANGED
@@ -18,7 +18,7 @@ This started as a personal little script. I had Ollama running on a laptop on my
18
18
 
19
19
  ## Related: Finetuna
20
20
 
21
- **ollanet** is the client-side / network tool. On the machine that *runs* Ollama, use **[Finetuna](https://github.com/Catalyst-Forge-LLC/finetuna)** to turn a stock model into a GPU-tuned, named variant (context, batch, Modelfile, auto-tune).
21
+ **ollanet** is the client-side / network tool. On the machine that *runs* Ollama, use **[Finetuna](https://finetuna.net)** to turn a stock model into a GPU-tuned, named variant (context, batch, Modelfile, auto-tune).
22
22
 
23
23
  Typical loop:
24
24
 
@@ -30,11 +30,13 @@ They share the same Ollama API; Finetuna shapes the models, ollanet finds and ch
30
30
  ## Features
31
31
 
32
32
  - **Discover** Ollama hosts from localhost, `config.hosts`, `OLLANET_HOSTS`, optional Tailscale, and optional LAN scan (`--lan`, off by default)
33
+ - **Pull** a library model onto a named host (`ollanet pull studio gemma3:12b`) — the server downloads; re-pull updates
34
+ - **Show / rm / ps** — inspect a Modelfile, delete a model (`--yes`), see what’s in VRAM. Scan marks Finetuna-style `[tuned]` names
33
35
  - **Prompt** any hostname/IP/model with streaming replies
34
36
  - **Persist** chats as `responses/<hash>.json` with topic, machine, model, timestamps
35
37
  - **Continue** any thread with `--chat <hash>`
36
38
  - **Configure** per-machine defaults (model, temperature, context, …)
37
- - **MCP** — `ollanet mcp` exposes scan/prompt/chats as Model Context Protocol tools for agents
39
+ - **MCP** — `ollanet mcp` exposes scan/prompt/pull/show/rm/ps/chats as Model Context Protocol tools for agents
38
40
  - **Library** — `import { scanNetwork } from "ollanet"` for apps (Node 20+, not the browser)
39
41
 
40
42
  ## Use cases
@@ -44,11 +46,12 @@ The core idea: turn “I have to remember which IP has which models loaded” in
44
46
  **For agents and automation** — prefer `ollanet mcp` (stdio MCP). The same flows also work via CLI flags:
45
47
 
46
48
  - **Live model inventory / routing** — `ollanet_scan` (or `scan --json`) lists reachable hosts and models. Route each sub-task to the best host: the big-VRAM box for reasoning, the laptop for quick lookups.
49
+ - **Put a new model on a box** — `ollanet_pull` (or `pull studio gemma3:12b`) asks that host to download from the registry. Re-pull updates. Then `show` / `ps` / `rm` manage what’s on that disk and in VRAM.
47
50
  - **Conversations that survive machines** — chats are stored by short hash with full history. Start a long thread on the GPU box, continue it later with `ollanet_prompt` + `chat_id` (or `--chat <hash>`).
48
51
  - **A “talk to any Ollama on my network” tool** — `ollanet_prompt` / `prompt … --json --no-stream`; no hard-coded endpoints.
49
52
  - **Embed in an app** — same scan + pick + call as a library. The app does not own the GPUs; it discovers them (FilePress, CLIs, Electron, a local companion for a hosted product).
50
53
  - **Fleet health and speed checks** — periodic `ollanet bench <host> --json` builds a record of which model on which machine is currently fastest (or has silently gone offline). Saved results live in `benchmarks/`.
51
- - **The Finetuna loop** — [Finetuna](https://github.com/Catalyst-Forge-LLC/finetuna) shapes a GPU-tuned named variant on the host; ollanet discovers and uses it from anywhere on the network.
54
+ - **The Finetuna loop** — [Finetuna](https://finetuna.net) shapes a GPU-tuned named variant on the host; ollanet `scan` / `show` / `prompt` it from anywhere on the network. After `pull`, ollanet prints the next step (`finetuna --model …`).
52
55
 
53
56
  **For humans:**
54
57
 
@@ -115,6 +118,11 @@ ollanet scan
115
118
  # Also TCP-scan your local /24s for open Ollama ports
116
119
  ollanet scan --lan
117
120
 
121
+ # Put a library model on a named machine (that host downloads it)
122
+ ollanet pull studio gemma3:12b
123
+ ollanet show studio gemma3:12b
124
+ ollanet ps studio
125
+
118
126
  # Talk to a machine by name, MagicDNS name, or IP
119
127
  ollanet prompt localhost "What is MagicDNS?"
120
128
  ollanet prompt 192.168.1.50 gemma4:12b "Hello from the LAN"
@@ -162,7 +170,9 @@ await scanNetwork({
162
170
 
163
171
  `runPrompt({ save: false, writeStdout: false })` is a one-shot call that does not write `~/.ollanet/responses/`.
164
172
 
165
- Requires **ollanet ≥ 0.4.0**. Bundlers (Vite / SvelteKit SSR):
173
+ `pullModel({ machine, model })` asks that host to download (or update) a library model. The bits never transit ollanet. `showModel`, `removeModel({ yes: true })`, and `listLoaded` are the inspect / delete / VRAM trio.
174
+
175
+ Requires **ollanet ≥ 0.4.0** (library). **Model management (`pull` / `show` / `rm` / `ps`) needs ≥ 0.5.0.** Bundlers (Vite / SvelteKit SSR):
166
176
 
167
177
  ```ts
168
178
  // vite.config.ts
@@ -175,6 +185,10 @@ optimizeDeps: { exclude: ["ollanet"] },
175
185
  | Command | Description |
176
186
  |---|---|
177
187
  | `ollanet scan` | Probe known/discovered hosts for Ollama + list models |
188
+ | `ollanet pull <machine> <model>` | Ask that host to download / update a library model |
189
+ | `ollanet show <machine> <model>` | Inspect Modelfile / params / capabilities (`[tuned]` when it looks like Finetuna) |
190
+ | `ollanet rm <machine> <model> --yes` | Delete a model from that host’s disk |
191
+ | `ollanet ps [machine]` | Models loaded in VRAM (omit machine = every discovered host) |
178
192
  | `ollanet prompt …` | Send a prompt / continue a chat |
179
193
  | `ollanet chats` | List or inspect saved transcripts |
180
194
  | `ollanet bench …` | Benchmark models for tok/s + lightweight quality checks |
@@ -211,6 +225,40 @@ ollanet prompt --chat <hash> <prompt...>
211
225
 
212
226
  Machine can be a discovered name, hostname, FQDN, or IP (`192.168.1.50`, `host:11434`). Direct addresses work even if they were never scanned.
213
227
 
228
+ ### `pull` options
229
+
230
+ ```text
231
+ ollanet pull <machine|ip> <model>
232
+ ```
233
+
234
+ The named machine downloads from the Ollama registry onto **its** disk. Re-pulling the same name updates it. This is not a file upload and not a Finetuna runtime tune.
235
+
236
+ | Flag | Meaning |
237
+ |---|---|
238
+ | `--machine <name>` | Host (same resolution as `prompt`) |
239
+ | `--model <name>` | Model to pull |
240
+ | `--insecure` | Allow HTTP / self-signed registries |
241
+ | `--no-stream` | Single final response (no progress chunks) |
242
+ | `--json` | Result JSON on stdout |
243
+
244
+ Timeout default is none (large pulls can take hours). Override with `OLLAMA_PULL_TIMEOUT_MS`. After a successful pull, stderr points at [Finetuna](https://finetuna.net) for a host-side named variant.
245
+
246
+ ### `show` / `rm` / `ps`
247
+
248
+ ```text
249
+ ollanet show <machine|ip> <model>
250
+ ollanet rm <machine|ip> <model> --yes
251
+ ollanet ps [machine]
252
+ ```
253
+
254
+ | Flag | Meaning |
255
+ |---|---|
256
+ | `--yes` / `-y` | Required for `rm` unless you confirm on a TTY |
257
+ | `--json` | Result JSON on stdout |
258
+ | `--machine` / `--model` | Same host resolution as `prompt` |
259
+
260
+ `scan` is on-disk inventory and marks `*-ctx32k` / `*-flash` / `*finetuna*` as `[tuned]`. `ps` is what’s resident. `show` reads the Modelfile so you can see a Finetuna bake from another machine.
261
+
214
262
  ### `chats` options
215
263
 
216
264
  - `--json` — list summary JSON
@@ -228,6 +276,10 @@ Runs a **stdio MCP server** (no extra npm deps). Cursor / Claude Desktop / any M
228
276
  |---|---|
229
277
  | `ollanet_scan` | Discover hosts + models (`lan`, `all` optional) |
230
278
  | `ollanet_prompt` | Prompt a host or continue with `chat_id` |
279
+ | `ollanet_pull` | Pull / update a model on a host (`machine`, `model`) |
280
+ | `ollanet_show` | Inspect a model (`machine`, `model`) |
281
+ | `ollanet_rm` | Delete a model (`machine`, `model`, `confirm: true`) |
282
+ | `ollanet_ps` | Loaded models (`machine` optional) |
231
283
  | `ollanet_list_chats` | Summarize saved transcripts |
232
284
  | `ollanet_get_chat` | Load one chat (full history) |
233
285
 
package/dist/cli.d.ts CHANGED
@@ -4,6 +4,10 @@
4
4
  *
5
5
  * ollanet scan
6
6
  * ollanet prompt <machine-or-ip> "hello"
7
+ * ollanet pull <machine> <model>
8
+ * ollanet show <machine> <model>
9
+ * ollanet rm <machine> <model> --yes
10
+ * ollanet ps [machine]
7
11
  * ollanet chats
8
12
  */
9
13
  export {};
package/dist/cli.js CHANGED
@@ -4,6 +4,10 @@
4
4
  *
5
5
  * ollanet scan
6
6
  * ollanet prompt <machine-or-ip> "hello"
7
+ * ollanet pull <machine> <model>
8
+ * ollanet show <machine> <model>
9
+ * ollanet rm <machine> <model> --yes
10
+ * ollanet ps [machine]
7
11
  * ollanet chats
8
12
  */
9
13
  const HELP = `ollanet — Ollama over your network
@@ -15,6 +19,10 @@ Usage:
15
19
  ollanet scan [--json] [--all] [--lan]
16
20
  ollanet prompt <machine|ip> [model] <prompt...>
17
21
  ollanet prompt --chat <hash> <prompt...>
22
+ ollanet pull <machine|ip> <model>
23
+ ollanet show <machine|ip> <model>
24
+ ollanet rm <machine|ip> <model> --yes
25
+ ollanet ps [machine]
18
26
  ollanet chats [--json] [--id <hash>]
19
27
  ollanet bench <machine|ip> [model...] [--all] [options]
20
28
  ollanet mcp
@@ -25,6 +33,10 @@ Examples:
25
33
  ollanet prompt localhost "What is MagicDNS?"
26
34
  ollanet prompt 192.168.1.50 gemma4:12b "Hello"
27
35
  ollanet prompt --chat a1b2c3d4e5f6 "Tell me more"
36
+ ollanet pull studio gemma3:12b
37
+ ollanet show studio gemma4-ctx32k
38
+ ollanet rm studio llama3.2:1b --yes
39
+ ollanet ps studio
28
40
  ollanet chats
29
41
  ollanet bench localhost --all
30
42
  ollanet bench localhost llama3.2:1b --runs 3
@@ -73,6 +85,30 @@ async function main() {
73
85
  await run();
74
86
  return;
75
87
  }
88
+ case "pull":
89
+ case "download": {
90
+ const { main: run } = await import("./pull.js");
91
+ await run();
92
+ return;
93
+ }
94
+ case "show":
95
+ case "inspect": {
96
+ const { main: run } = await import("./show.js");
97
+ await run();
98
+ return;
99
+ }
100
+ case "rm":
101
+ case "delete": {
102
+ const { main: run } = await import("./rm.js");
103
+ await run();
104
+ return;
105
+ }
106
+ case "ps":
107
+ case "loaded": {
108
+ const { main: run } = await import("./ps.js");
109
+ await run();
110
+ return;
111
+ }
76
112
  case "bench":
77
113
  case "benchmark": {
78
114
  const { main: run } = await import("./bench.js");
package/dist/index.d.ts CHANGED
@@ -15,4 +15,14 @@ export { loadConfig, defaultModelForHost, mergeSettings, configFromPartial, } fr
15
15
  export type { AppConfig, DiscoveryConfig, GenerateSettings } from "./config.js";
16
16
  export { runPrompt } from "./prompt.js";
17
17
  export type { PromptRunOptions, PromptRunResult } from "./prompt.js";
18
- export { ollamaChat, ollamaTags, ollamaShow, ollamaPs } from "./ollama-chat.js";
18
+ export { pullModel } from "./pull.js";
19
+ export type { PullOptions, PullResult } from "./pull.js";
20
+ export { showModel } from "./show.js";
21
+ export type { ShowOptions, ShowResult } from "./show.js";
22
+ export { removeModel } from "./rm.js";
23
+ export type { RemoveOptions, RemoveResult } from "./rm.js";
24
+ export { listLoaded } from "./ps.js";
25
+ export type { PsOptions, PsResult, LoadedHost, LoadedModel } from "./ps.js";
26
+ export { looksTuned } from "./tuned.js";
27
+ export { ollamaChat, ollamaTags, ollamaShow, ollamaPs, ollamaPsRequired, ollamaPull, ollamaDelete, } from "./ollama-chat.js";
28
+ export type { PullChunk, OllamaPullOptions, OllamaPullResult, OllamaShowInfo, } from "./ollama-chat.js";
package/dist/index.js CHANGED
@@ -11,4 +11,9 @@ export { scanNetwork } from "./scan.js";
11
11
  export { discoverHosts, resolveHost, ollamaBaseUrl, shortName, ollamaPort, } from "./hosts.js";
12
12
  export { loadConfig, defaultModelForHost, mergeSettings, configFromPartial, } from "./config.js";
13
13
  export { runPrompt } from "./prompt.js";
14
- export { ollamaChat, ollamaTags, ollamaShow, ollamaPs } from "./ollama-chat.js";
14
+ export { pullModel } from "./pull.js";
15
+ export { showModel } from "./show.js";
16
+ export { removeModel } from "./rm.js";
17
+ export { listLoaded } from "./ps.js";
18
+ export { looksTuned } from "./tuned.js";
19
+ export { ollamaChat, ollamaTags, ollamaShow, ollamaPs, ollamaPsRequired, ollamaPull, ollamaDelete, } from "./ollama-chat.js";
package/dist/mcp.js CHANGED
@@ -12,8 +12,12 @@ import { createInterface } from "node:readline";
12
12
  import { readFile } from "node:fs/promises";
13
13
  import { listChats, loadChat } from "./chat-store.js";
14
14
  import { projectPath } from "./paths.js";
15
+ import { listLoaded } from "./ps.js";
16
+ import { pullModel } from "./pull.js";
15
17
  import { runPrompt } from "./prompt.js";
18
+ import { removeModel } from "./rm.js";
16
19
  import { scanNetwork } from "./scan.js";
20
+ import { showModel } from "./show.js";
17
21
  // Pin to the oldest widely-supported MCP revision. Clients may request a newer
18
22
  // date; we still answer with this — legal negotiation, max compatibility.
19
23
  const PROTOCOL_VERSION = "2024-11-05";
@@ -85,6 +89,78 @@ const TOOLS = [
85
89
  additionalProperties: false,
86
90
  },
87
91
  },
92
+ {
93
+ name: "ollanet_pull",
94
+ description: "Pull (or update) a model onto a reachable Ollama host. " +
95
+ "The named machine downloads from the registry onto its own disk — " +
96
+ "ollanet does not upload weights. Machine is a discovered name, MagicDNS name, hostname, or IP[:port].",
97
+ inputSchema: {
98
+ type: "object",
99
+ properties: {
100
+ machine: {
101
+ type: "string",
102
+ description: "Host name/IP that should download the model",
103
+ },
104
+ model: {
105
+ type: "string",
106
+ description: "Model to pull (e.g. gemma3:12b)",
107
+ },
108
+ insecure: {
109
+ type: "boolean",
110
+ description: "Allow HTTP / self-signed model registries (default false)",
111
+ },
112
+ },
113
+ required: ["machine", "model"],
114
+ additionalProperties: false,
115
+ },
116
+ },
117
+ {
118
+ name: "ollanet_show",
119
+ description: "Inspect a model on a reachable Ollama host (Modelfile, parameters, capabilities). " +
120
+ "Marks Finetuna-style tuned names. Machine is a discovered name, MagicDNS name, hostname, or IP[:port].",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ machine: { type: "string", description: "Host name/IP" },
125
+ model: { type: "string", description: "Model to inspect" },
126
+ },
127
+ required: ["machine", "model"],
128
+ additionalProperties: false,
129
+ },
130
+ },
131
+ {
132
+ name: "ollanet_rm",
133
+ description: "Delete a model from a reachable Ollama host's disk. Destructive. " +
134
+ "Requires confirm=true. Machine is a discovered name, MagicDNS name, hostname, or IP[:port].",
135
+ inputSchema: {
136
+ type: "object",
137
+ properties: {
138
+ machine: { type: "string", description: "Host name/IP" },
139
+ model: { type: "string", description: "Model to delete" },
140
+ confirm: {
141
+ type: "boolean",
142
+ description: "Must be true to actually delete (default false)",
143
+ },
144
+ },
145
+ required: ["machine", "model", "confirm"],
146
+ additionalProperties: false,
147
+ },
148
+ },
149
+ {
150
+ name: "ollanet_ps",
151
+ description: "List models currently loaded in VRAM. Optional machine; omit to probe every discovered host. " +
152
+ "scan is on-disk inventory; ps is what is resident right now.",
153
+ inputSchema: {
154
+ type: "object",
155
+ properties: {
156
+ machine: {
157
+ type: "string",
158
+ description: "Host name/IP (optional; omit for all discovered hosts)",
159
+ },
160
+ },
161
+ additionalProperties: false,
162
+ },
163
+ },
88
164
  {
89
165
  name: "ollanet_list_chats",
90
166
  description: "List saved ollanet chat transcripts (id, topic, machine, model, message count).",
@@ -187,6 +263,47 @@ async function callTool(name, args) {
187
263
  });
188
264
  return textResult(result);
189
265
  }
266
+ case "ollanet_pull": {
267
+ const machine = typeof args.machine === "string" ? args.machine : "";
268
+ const model = typeof args.model === "string" ? args.model : "";
269
+ if (!machine.trim())
270
+ return textResult({ error: "machine is required" }, true);
271
+ if (!model.trim())
272
+ return textResult({ error: "model is required" }, true);
273
+ const result = await pullModel({
274
+ machine,
275
+ model,
276
+ insecure: args.insecure === true,
277
+ writeStdout: false,
278
+ quiet: true,
279
+ });
280
+ return textResult(result);
281
+ }
282
+ case "ollanet_show": {
283
+ const machine = typeof args.machine === "string" ? args.machine : "";
284
+ const model = typeof args.model === "string" ? args.model : "";
285
+ if (!machine.trim())
286
+ return textResult({ error: "machine is required" }, true);
287
+ if (!model.trim())
288
+ return textResult({ error: "model is required" }, true);
289
+ return textResult(await showModel({ machine, model }));
290
+ }
291
+ case "ollanet_rm": {
292
+ const machine = typeof args.machine === "string" ? args.machine : "";
293
+ const model = typeof args.model === "string" ? args.model : "";
294
+ if (!machine.trim())
295
+ return textResult({ error: "machine is required" }, true);
296
+ if (!model.trim())
297
+ return textResult({ error: "model is required" }, true);
298
+ if (args.confirm !== true) {
299
+ return textResult({ error: "confirm must be true to delete a model" }, true);
300
+ }
301
+ return textResult(await removeModel({ machine, model, yes: true }));
302
+ }
303
+ case "ollanet_ps": {
304
+ const machine = typeof args.machine === "string" ? args.machine : undefined;
305
+ return textResult(await listLoaded({ machine }));
306
+ }
190
307
  case "ollanet_list_chats": {
191
308
  const chats = await listChats();
192
309
  return textResult(chats.map((c) => ({
@@ -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 declare function ollamaShow(baseUrl: string, model: string, timeoutMs: number): Promise<{
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;
@@ -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
- const body = await ollamaGetJson(`${baseUrl}/api/ps`, timeoutMs);
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/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>;