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/dist/pull.js ADDED
@@ -0,0 +1,198 @@
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 { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
10
+ import { ollamaPull } from "./ollama-chat.js";
11
+ import { resolveTarget } from "./target.js";
12
+ /** Pull HTTP timeout (ms). 0 = none. Large models can take hours. */
13
+ function pullTimeoutMs() {
14
+ return envInt("OLLAMA_PULL_TIMEOUT_MS", 0);
15
+ }
16
+ function usage() {
17
+ console.error(`Usage:
18
+ ollanet pull <machine> <model>
19
+ ollanet pull --machine <name> --model <name>
20
+
21
+ Examples:
22
+ ollanet pull studio gemma3:12b
23
+ ollanet pull localhost llama3.2:1b
24
+ ollanet pull 192.168.1.50 gemma3:12b --json
25
+
26
+ The named machine downloads the model onto its own disk. ollanet does not
27
+ upload weights. Re-pulling the same name updates it when the registry has a
28
+ newer digest.
29
+
30
+ Options:
31
+ --machine <name> Host (discovered name, MagicDNS, hostname, or IP[:port])
32
+ --model <name> Model to pull (e.g. gemma3:12b)
33
+ --insecure Allow HTTP / self-signed model registries
34
+ --no-stream Wait for a single final response (no progress chunks)
35
+ --json Emit result JSON on stdout`);
36
+ process.exit(1);
37
+ }
38
+ function takeValue(args, flag) {
39
+ const value = args.shift();
40
+ if (!value) {
41
+ console.error(`${flag} requires a value`);
42
+ usage();
43
+ }
44
+ return value;
45
+ }
46
+ function parseArgs(argv) {
47
+ const args = [...argv];
48
+ let machineFlag;
49
+ let modelFlag;
50
+ let insecure = false;
51
+ let stream = true;
52
+ let json = false;
53
+ const positional = [];
54
+ while (args.length > 0) {
55
+ const arg = args.shift();
56
+ if (arg === "--")
57
+ continue;
58
+ if (arg === "--help" || arg === "-h")
59
+ usage();
60
+ if (arg === "--insecure") {
61
+ insecure = true;
62
+ continue;
63
+ }
64
+ if (arg === "--no-stream") {
65
+ stream = false;
66
+ continue;
67
+ }
68
+ if (arg === "--json") {
69
+ json = true;
70
+ continue;
71
+ }
72
+ if (arg === "--machine") {
73
+ machineFlag = takeValue(args, "--machine");
74
+ continue;
75
+ }
76
+ if (arg === "--model") {
77
+ modelFlag = takeValue(args, "--model");
78
+ continue;
79
+ }
80
+ if (arg.startsWith("-")) {
81
+ console.error(`Unknown flag: ${arg}`);
82
+ usage();
83
+ }
84
+ positional.push(arg);
85
+ }
86
+ const machine = machineFlag ?? positional[0];
87
+ const model = modelFlag ?? (machineFlag ? positional[0] : positional[1]);
88
+ if (positional.length > 2 || (machineFlag && modelFlag && positional.length > 0)) {
89
+ console.error("Too many arguments.");
90
+ usage();
91
+ }
92
+ return { machine, model, insecure, stream, json };
93
+ }
94
+ function formatBytes(n) {
95
+ if (n < 1024)
96
+ return `${n} B`;
97
+ if (n < 1024 ** 2)
98
+ return `${(n / 1024).toFixed(1)} KB`;
99
+ if (n < 1024 ** 3)
100
+ return `${(n / 1024 ** 2).toFixed(1)} MB`;
101
+ return `${(n / 1024 ** 3).toFixed(2)} GB`;
102
+ }
103
+ function shortDigest(digest) {
104
+ const hex = digest.replace(/^sha256:/, "");
105
+ return hex.slice(0, 12);
106
+ }
107
+ function formatPullLine(chunk) {
108
+ const status = chunk.status ?? "pulling";
109
+ if (chunk.total && chunk.total > 0) {
110
+ const pct = Math.min(100, Math.floor((100 * (chunk.completed ?? 0)) / chunk.total));
111
+ const digest = chunk.digest ? ` ${shortDigest(chunk.digest)}` : "";
112
+ return `${status}${digest} ${pct}% ${formatBytes(chunk.completed ?? 0)} / ${formatBytes(chunk.total)}`;
113
+ }
114
+ return status;
115
+ }
116
+ function emitProgress(chunk, state, tty) {
117
+ if (chunk.total && chunk.total > 0 && tty) {
118
+ process.stderr.write(`\r${formatPullLine(chunk).padEnd(80)}`);
119
+ state.usedCarriage = true;
120
+ state.lastStatus = chunk.status ?? state.lastStatus;
121
+ return;
122
+ }
123
+ if (chunk.status && chunk.status !== state.lastStatus) {
124
+ if (state.usedCarriage) {
125
+ process.stderr.write("\n");
126
+ state.usedCarriage = false;
127
+ }
128
+ process.stderr.write(`${chunk.status}\n`);
129
+ state.lastStatus = chunk.status;
130
+ }
131
+ }
132
+ /** Programmatic pull used by the CLI, MCP, and apps. */
133
+ export async function pullModel(opts) {
134
+ const model = (opts.model ?? "").trim();
135
+ const machineQuery = (opts.machine ?? "").trim();
136
+ if (!machineQuery)
137
+ throw new Error("Machine is required.");
138
+ if (!model)
139
+ throw new Error("Model is required.");
140
+ const host = await resolveTarget(machineQuery, opts.config);
141
+ const writeProgress = opts.writeStdout !== false;
142
+ const quiet = opts.quiet === true || !writeProgress;
143
+ const state = { lastStatus: "", usedCarriage: false };
144
+ const tty = Boolean(process.stderr.isTTY);
145
+ if (!quiet) {
146
+ console.error(`→ ${shortName(host)} (${host.ip}) pulling ${model}`);
147
+ }
148
+ const pulled = await ollamaPull({
149
+ baseUrl: ollamaBaseUrl(host),
150
+ model,
151
+ insecure: opts.insecure === true,
152
+ stream: opts.stream !== false,
153
+ timeoutMs: opts.timeoutMs ?? pullTimeoutMs(),
154
+ onProgress: (chunk) => {
155
+ opts.onProgress?.(chunk);
156
+ if (!quiet)
157
+ emitProgress(chunk, state, tty);
158
+ },
159
+ });
160
+ if (!quiet && state.usedCarriage) {
161
+ process.stderr.write("\n");
162
+ }
163
+ const machine = shortName(host);
164
+ return {
165
+ machine,
166
+ model,
167
+ status: pulled.status,
168
+ endpoint: ollamaBaseUrl(host),
169
+ next: {
170
+ hint: `On ${machine}: finetuna --model ${model} → then ollanet show ${machine} <tuned-name>`,
171
+ site: "https://finetuna.net",
172
+ },
173
+ };
174
+ }
175
+ export async function main() {
176
+ const parsed = parseArgs(process.argv.slice(2));
177
+ if (!parsed.machine || !parsed.model) {
178
+ if (!parsed.machine)
179
+ console.error("Machine is required.");
180
+ else
181
+ console.error("Model is required.");
182
+ usage();
183
+ }
184
+ const result = await pullModel({
185
+ machine: parsed.machine,
186
+ model: parsed.model,
187
+ insecure: parsed.insecure,
188
+ stream: parsed.stream,
189
+ writeStdout: true,
190
+ quiet: false,
191
+ });
192
+ if (parsed.json) {
193
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
194
+ return;
195
+ }
196
+ process.stdout.write(`pulled ${result.model} on ${result.machine}\n`);
197
+ process.stderr.write(`${result.next.hint}\n${result.next.site}\n`);
198
+ }
package/dist/rm.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Delete a model from a reachable Ollama host.
3
+ *
4
+ * ollanet rm studio llama3.2:1b --yes
5
+ */
6
+ import type { AppConfig } from "./config.js";
7
+ export interface RemoveOptions {
8
+ machine: string;
9
+ model: string;
10
+ /** Must be true unless the caller is an interactive TTY confirm. */
11
+ yes?: boolean;
12
+ config?: Partial<AppConfig>;
13
+ timeoutMs?: number;
14
+ }
15
+ export interface RemoveResult {
16
+ machine: string;
17
+ model: string;
18
+ endpoint: string;
19
+ deleted: true;
20
+ }
21
+ export declare function removeModel(opts: RemoveOptions): Promise<RemoveResult>;
22
+ export declare function main(): Promise<void>;
package/dist/rm.js ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Delete a model from a reachable Ollama host.
3
+ *
4
+ * ollanet rm studio llama3.2:1b --yes
5
+ */
6
+ import { createInterface } from "node:readline";
7
+ import { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
8
+ import { ollamaDelete } from "./ollama-chat.js";
9
+ import { resolveTarget } from "./target.js";
10
+ function rmTimeoutMs() {
11
+ return envInt("OLLAMA_RM_TIMEOUT_MS", 30_000);
12
+ }
13
+ function usage() {
14
+ console.error(`Usage:
15
+ ollanet rm <machine> <model> --yes
16
+ ollanet rm --machine <name> --model <name> --yes
17
+
18
+ Examples:
19
+ ollanet rm studio llama3.2:1b --yes
20
+ ollanet rm localhost old-model:7b --yes --json
21
+
22
+ Deletes the named model from that host's disk (POST /api/delete).
23
+ Non-interactive use (pipes, MCP, CI) requires --yes / confirm: true.
24
+
25
+ Options:
26
+ --machine <name> Host (discovered name, MagicDNS, hostname, or IP[:port])
27
+ --model <name> Model to delete
28
+ --yes Required unless stdin is a TTY and you confirm
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 modelFlag;
44
+ let yes = false;
45
+ let json = false;
46
+ const positional = [];
47
+ while (args.length > 0) {
48
+ const arg = args.shift();
49
+ if (arg === "--")
50
+ continue;
51
+ if (arg === "--help" || arg === "-h")
52
+ usage();
53
+ if (arg === "--yes" || arg === "-y") {
54
+ yes = true;
55
+ continue;
56
+ }
57
+ if (arg === "--json") {
58
+ json = true;
59
+ continue;
60
+ }
61
+ if (arg === "--machine") {
62
+ machineFlag = takeValue(args, "--machine");
63
+ continue;
64
+ }
65
+ if (arg === "--model") {
66
+ modelFlag = takeValue(args, "--model");
67
+ continue;
68
+ }
69
+ if (arg.startsWith("-")) {
70
+ console.error(`Unknown flag: ${arg}`);
71
+ usage();
72
+ }
73
+ positional.push(arg);
74
+ }
75
+ const machine = machineFlag ?? positional[0];
76
+ const model = modelFlag ?? (machineFlag ? positional[0] : positional[1]);
77
+ if (positional.length > 2 || (machineFlag && modelFlag && positional.length > 0)) {
78
+ console.error("Too many arguments.");
79
+ usage();
80
+ }
81
+ return { machine, model, yes, json };
82
+ }
83
+ async function confirmDelete(machine, model, yes) {
84
+ if (yes)
85
+ return;
86
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
87
+ throw new Error(`Refusing to delete ${model} on ${machine} without --yes.`);
88
+ }
89
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
90
+ const answer = await new Promise((resolve) => {
91
+ rl.question(`Delete ${model} on ${machine}? [y/N] `, resolve);
92
+ });
93
+ rl.close();
94
+ if (!/^\s*y(es)?\s*$/i.test(answer)) {
95
+ throw new Error("Aborted.");
96
+ }
97
+ }
98
+ export async function removeModel(opts) {
99
+ const model = (opts.model ?? "").trim();
100
+ const machineQuery = (opts.machine ?? "").trim();
101
+ if (!machineQuery)
102
+ throw new Error("Machine is required.");
103
+ if (!model)
104
+ throw new Error("Model is required.");
105
+ await confirmDelete(machineQuery, model, opts.yes === true);
106
+ const host = await resolveTarget(machineQuery, opts.config);
107
+ await ollamaDelete(ollamaBaseUrl(host), model, opts.timeoutMs ?? rmTimeoutMs());
108
+ return {
109
+ machine: shortName(host),
110
+ model,
111
+ endpoint: ollamaBaseUrl(host),
112
+ deleted: true,
113
+ };
114
+ }
115
+ export async function main() {
116
+ const parsed = parseArgs(process.argv.slice(2));
117
+ if (!parsed.machine || !parsed.model) {
118
+ if (!parsed.machine)
119
+ console.error("Machine is required.");
120
+ else
121
+ console.error("Model is required.");
122
+ usage();
123
+ }
124
+ const result = await removeModel({
125
+ machine: parsed.machine,
126
+ model: parsed.model,
127
+ yes: parsed.yes,
128
+ });
129
+ if (parsed.json) {
130
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
131
+ return;
132
+ }
133
+ process.stdout.write(`deleted ${result.model} on ${result.machine}\n`);
134
+ }
@@ -0,0 +1,15 @@
1
+ import type { ScanPayload } from "./scan.js";
2
+ export interface StoredScan extends ScanPayload {
3
+ scanned_at: string;
4
+ lan: boolean;
5
+ include_offline: boolean;
6
+ }
7
+ /** Path for the last-scan snapshot. Read at call time so import stays safe. */
8
+ export declare function lastScanPath(): string;
9
+ export declare function saveLastScan(payload: ScanPayload, meta: {
10
+ lan: boolean;
11
+ includeOffline: boolean;
12
+ }): Promise<string>;
13
+ export declare function loadLastScan(): Promise<StoredScan | null>;
14
+ /** Last saved scan, or null if none. Does not probe the network. */
15
+ export declare function lastScan(): Promise<StoredScan | null>;
@@ -0,0 +1,41 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { defaultLastScanPath } from "./paths.js";
4
+ /** Path for the last-scan snapshot. Read at call time so import stays safe. */
5
+ export function lastScanPath() {
6
+ return process.env.OLLANET_LAST_SCAN ?? defaultLastScanPath();
7
+ }
8
+ export async function saveLastScan(payload, meta) {
9
+ const stored = {
10
+ ...payload,
11
+ scanned_at: new Date().toISOString(),
12
+ lan: meta.lan,
13
+ include_offline: meta.includeOffline,
14
+ };
15
+ const file = lastScanPath();
16
+ await mkdir(path.dirname(file), { recursive: true });
17
+ await writeFile(file, `${JSON.stringify(stored, null, 2)}\n`, "utf8");
18
+ return file;
19
+ }
20
+ export async function loadLastScan() {
21
+ const file = lastScanPath();
22
+ let text;
23
+ try {
24
+ text = await readFile(file, "utf8");
25
+ }
26
+ catch (err) {
27
+ const code = err && typeof err === "object" && "code" in err ? err.code : "";
28
+ if (code === "ENOENT")
29
+ return null;
30
+ throw err;
31
+ }
32
+ const parsed = JSON.parse(text);
33
+ if (!parsed || !Array.isArray(parsed.servers)) {
34
+ throw new Error(`Invalid last-scan file: ${file}`);
35
+ }
36
+ return parsed;
37
+ }
38
+ /** Last saved scan, or null if none. Does not probe the network. */
39
+ export async function lastScan() {
40
+ return loadLastScan();
41
+ }
package/dist/scan.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Scan reachable networks for Ollama servers and list available models.
3
3
  *
4
- * Usage: ollanet scan [--lan] [--json] [--all]
4
+ * Usage: ollanet scan [--lan] [--json] [--all] [--last]
5
5
  */
6
6
  import { type AppConfig } from "./config.js";
7
7
  export interface ScanOptions {
@@ -13,6 +13,11 @@ export interface ScanOptions {
13
13
  * is not read. Omit to keep CLI/MCP file behavior.
14
14
  */
15
15
  config?: Partial<AppConfig>;
16
+ /**
17
+ * Persist this result as the last scan. Default true when `config` is omitted
18
+ * (CLI / MCP). Default false when `config` is passed (hermetic apps/tests).
19
+ */
20
+ save?: boolean;
16
21
  }
17
22
  export interface ScannedServer {
18
23
  hostname: string;
@@ -30,14 +35,22 @@ export interface ScannedServer {
30
35
  parameter_size?: string;
31
36
  quantization_level?: string;
32
37
  family?: string;
38
+ /** Name looks like a Finetuna-style host-side tune. */
39
+ tuned: boolean;
33
40
  }>;
34
41
  }
42
+ export interface ScanFailure {
43
+ name: string;
44
+ ip: string;
45
+ error: string;
46
+ }
35
47
  export interface ScanPayload {
36
48
  network: string;
37
49
  sources: string[];
38
50
  port: number;
39
51
  scanned: number;
40
52
  servers: ScannedServer[];
53
+ failures?: ScanFailure[];
41
54
  }
42
55
  /** Programmatic scan used by CLI `--json`, MCP, and `import { scanNetwork } from "ollanet"`. */
43
56
  export declare function scanNetwork(options?: ScanOptions): Promise<ScanPayload>;
package/dist/scan.js CHANGED
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Scan reachable networks for Ollama servers and list available models.
3
3
  *
4
- * Usage: ollanet scan [--lan] [--json] [--all]
4
+ * Usage: ollanet scan [--lan] [--json] [--all] [--last]
5
5
  */
6
6
  import { configFromPartial, loadConfig } from "./config.js";
7
7
  import { discoverHosts, envInt, ollamaPort, shortName, } from "./hosts.js";
8
+ import { lastScanPath, loadLastScan, saveLastScan } from "./scan-store.js";
9
+ import { looksTuned } from "./tuned.js";
8
10
  function scanTimeoutMs() {
9
11
  return envInt("OLLAMA_TIMEOUT_MS", 2500);
10
12
  }
@@ -75,47 +77,53 @@ async function scanHost(host) {
75
77
  return { host, ok: false, models: [], error, url };
76
78
  }
77
79
  }
78
- function printResults(results, networkLabel, sources) {
79
- const found = results.filter((r) => r.ok);
80
- const offline = results.filter((r) => r.error === "offline").length;
81
- const probed = results.length - offline;
82
- console.log(`Network: ${networkLabel}`);
83
- console.log(`Discovery: ${sources.join(", ") || "none"}`);
84
- console.log(`Probing Ollama on ${results.length} host(s) (${probed} online/reachable candidates)\n`);
85
- if (found.length === 0) {
80
+ function printPayload(payload, cached) {
81
+ if (cached?.scanned_at) {
82
+ const flags = [
83
+ cached.lan ? "lan" : null,
84
+ cached.include_offline ? "all" : null,
85
+ ].filter(Boolean);
86
+ console.log(`Cached scan from ${cached.scanned_at}${flags.length ? ` (${flags.join(", ")})` : ""}`);
87
+ console.log(`File: ${lastScanPath()}\n`);
88
+ }
89
+ console.log(`Network: ${payload.network}`);
90
+ console.log(`Discovery: ${payload.sources.join(", ") || "none"}`);
91
+ console.log(`Probing Ollama on ${payload.scanned} host(s)\n`);
92
+ if (payload.servers.length === 0) {
86
93
  console.log("No Ollama servers found.");
87
94
  console.log("Tip: add hosts to config.json, set OLLANET_HOSTS, pass --lan, or prompt an IP directly.");
88
- const interesting = results.filter((r) => r.error && r.error !== "offline");
95
+ const interesting = (payload.failures ?? []).filter((f) => f.error !== "offline");
89
96
  if (interesting.length > 0) {
90
97
  console.log("\nNon-timeout failures (first few):");
91
- for (const r of interesting.slice(0, 5)) {
92
- console.log(` - ${shortName(r.host)} (${r.host.ip}): ${r.error}`);
98
+ for (const f of interesting.slice(0, 5)) {
99
+ console.log(` - ${f.name} (${f.ip}): ${f.error}`);
93
100
  }
94
101
  }
95
102
  return;
96
103
  }
97
- for (const result of found) {
98
- const label = result.host.dnsName || result.host.hostname;
99
- const selfTag = result.host.isSelf ? " [this device]" : "";
104
+ for (const server of payload.servers) {
105
+ const label = server.dnsName || server.hostname;
106
+ const selfTag = server.self ? " [this device]" : "";
100
107
  console.log(`${label}${selfTag}`);
101
- console.log(` IP: ${result.host.ip} source: ${result.host.source} OS: ${result.host.os}`);
102
- console.log(` Endpoint: http://${result.host.ip}:${result.host.port}`);
103
- if (result.models.length === 0) {
108
+ console.log(` IP: ${server.ip} source: ${server.source} OS: ${server.os}`);
109
+ console.log(` Endpoint: ${server.endpoint}`);
110
+ if (server.models.length === 0) {
104
111
  console.log(" Models: (none)");
105
112
  }
106
113
  else {
107
- console.log(` Models (${result.models.length}):`);
108
- for (const model of result.models) {
114
+ console.log(` Models (${server.models.length}):`);
115
+ for (const model of server.models) {
109
116
  const size = formatBytes(model.size);
110
- const params = model.details?.parameter_size;
111
- const quant = model.details?.quantization_level;
117
+ const params = model.parameter_size;
118
+ const quant = model.quantization_level;
112
119
  const meta = [params, quant, size].filter(Boolean).join(" · ");
113
- console.log(meta ? ` - ${model.name} (${meta})` : ` - ${model.name}`);
120
+ const tag = model.tuned ? " [tuned]" : "";
121
+ console.log(meta ? ` - ${model.name} (${meta})${tag}` : ` - ${model.name}${tag}`);
114
122
  }
115
123
  }
116
124
  console.log("");
117
125
  }
118
- console.log(`Found ${found.length} Ollama server(s) with ${found.reduce((n, r) => n + r.models.length, 0)} model entrie(s).`);
126
+ console.log(`Found ${payload.servers.length} Ollama server(s) with ${payload.servers.reduce((n, s) => n + s.models.length, 0)} model entrie(s).`);
119
127
  }
120
128
  function toPayload(results, networkLabel, sources) {
121
129
  return {
@@ -141,8 +149,16 @@ function toPayload(results, networkLabel, sources) {
141
149
  parameter_size: m.details?.parameter_size,
142
150
  quantization_level: m.details?.quantization_level,
143
151
  family: m.details?.family,
152
+ tuned: looksTuned(m.name),
144
153
  })),
145
154
  })),
155
+ failures: results
156
+ .filter((r) => !r.ok)
157
+ .map((r) => ({
158
+ name: shortName(r.host),
159
+ ip: r.host.ip,
160
+ error: r.error ?? "unknown",
161
+ })),
146
162
  };
147
163
  }
148
164
  /** Programmatic scan used by CLI `--json`, MCP, and `import { scanNetwork } from "ollanet"`. */
@@ -158,31 +174,49 @@ export async function scanNetwork(options = {}) {
158
174
  throw new Error("No hosts to scan. Add config.hosts, set OLLANET_HOSTS, or enable Tailscale/LAN discovery.");
159
175
  }
160
176
  const results = await mapPool(hosts, scanConcurrency(), scanHost);
161
- return toPayload(results, networkLabel, sources);
177
+ const payload = toPayload(results, networkLabel, sources);
178
+ const shouldSave = options.save ?? options.config == null;
179
+ if (shouldSave) {
180
+ await saveLastScan(payload, {
181
+ lan: options.lanScan ?? false,
182
+ includeOffline: options.includeOffline ?? false,
183
+ });
184
+ }
185
+ return payload;
162
186
  }
163
187
  export async function main() {
164
188
  const includeOffline = process.argv.includes("--all");
165
189
  const jsonOut = process.argv.includes("--json");
166
190
  const lanScan = process.argv.includes("--lan");
167
- const config = await loadConfig();
168
- const { hosts, sources, networkLabel } = await discoverHosts({
169
- hosts: config.hosts,
170
- discovery: config.discovery,
171
- includeOffline,
172
- lanScan,
173
- });
174
- if (hosts.length === 0) {
175
- console.error("No hosts to scan. Add config.hosts, set OLLANET_HOSTS, or enable Tailscale/LAN discovery.");
176
- process.exitCode = 1;
191
+ const cached = process.argv.includes("--last") || process.argv.includes("--cached");
192
+ if (cached) {
193
+ const stored = await loadLastScan();
194
+ if (!stored) {
195
+ console.error(`No saved scan at ${lastScanPath()}. Run ollanet scan first.`);
196
+ process.exitCode = 1;
197
+ return;
198
+ }
199
+ if (jsonOut) {
200
+ console.log(JSON.stringify(stored, null, 2));
201
+ return;
202
+ }
203
+ printPayload(stored, stored);
177
204
  return;
178
205
  }
179
- const results = await mapPool(hosts, scanConcurrency(), scanHost);
180
- if (jsonOut) {
181
- console.log(JSON.stringify(toPayload(results, networkLabel, sources), null, 2));
182
- return;
206
+ try {
207
+ const payload = await scanNetwork({ includeOffline, lanScan });
208
+ if (jsonOut) {
209
+ console.log(JSON.stringify(payload, null, 2));
210
+ }
211
+ else {
212
+ printPayload(payload);
213
+ }
214
+ if (payload.servers.length === 0) {
215
+ process.exitCode = 2;
216
+ }
183
217
  }
184
- printResults(results, networkLabel, sources);
185
- if (!results.some((r) => r.ok)) {
186
- process.exitCode = 2;
218
+ catch (err) {
219
+ console.error(err instanceof Error ? err.message : err);
220
+ process.exitCode = 1;
187
221
  }
188
222
  }
package/dist/show.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Inspect a model on a reachable Ollama host (Modelfile, params, capabilities).
3
+ *
4
+ * ollanet show studio gemma4-ctx32k
5
+ */
6
+ import { type OllamaShowInfo } from "./ollama-chat.js";
7
+ import type { AppConfig } from "./config.js";
8
+ export interface ShowOptions {
9
+ machine: string;
10
+ model: string;
11
+ config?: Partial<AppConfig>;
12
+ timeoutMs?: number;
13
+ }
14
+ export interface ShowResult {
15
+ machine: string;
16
+ model: string;
17
+ endpoint: string;
18
+ tuned: boolean;
19
+ capabilities?: string[];
20
+ modelfile?: string;
21
+ parameters?: string;
22
+ template?: string;
23
+ system?: string;
24
+ details?: OllamaShowInfo["details"];
25
+ modified_at?: string;
26
+ }
27
+ export declare function showModel(opts: ShowOptions): Promise<ShowResult>;
28
+ export declare function main(): Promise<void>;