ollanet 0.6.8 → 0.6.10

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
@@ -4,11 +4,11 @@
4
4
 
5
5
  # ollanet
6
6
 
7
- Talk to **Ollama** on any network you can reach: LAN, localhost, Tailscale, VPN, or a raw IP.
7
+ Find, manage, and use Ollama models on the hosts you choose.
8
8
 
9
9
  **CLI** for humans · **MCP** for agents · **Node** for apps.
10
10
 
11
- Scan for hosts, send a prompt, continue a chat by a short hash. No browser UI. **Node 20+ · zero runtime deps.**
11
+ Discover hosts, give models convenient aliases, and prompt or compare them. A reachable host is not a trusted host. LAN scan is opt-in. Node 20+, zero runtime deps.
12
12
 
13
13
  **Docs:** [ollanet.dev/docs](https://ollanet.dev/docs) · **Site:** [ollanet.dev](https://ollanet.dev)
14
14
 
@@ -27,19 +27,57 @@ Or one-off: `npx ollanet scan`
27
27
  ollanet scan
28
28
  ollanet alias add desk studio gemma3:12b
29
29
  ollanet prompt desk "What is MagicDNS?"
30
- ollanet bench desk --hot
31
- ollanet mcp
32
30
  ```
33
31
 
32
+ `alias` writes local config. `prompt` sends that text to the selected host. `pull` and `rm` change models on the remote host. `scan`, `show`, `ps`, and `alias` do not install or delete models.
33
+
34
34
  Host-first: `ollanet <cmd> <machine> …`. The machine is a MagicDNS name, a config name, an IP, or an alias for a machine + model pair.
35
35
 
36
- ## What you get
36
+ ## Three jobs
37
+
38
+ **Discover.** Probe localhost, config, env, and Tailscale. `--lan` is an opt-in TCP sweep of your subnet on port `11434`.
39
+
40
+ **Manage.** `pull`, `show`, `rm`, and `ps` on a named machine. Comparisons and benches are deeper workflows on [ollanet.dev/docs](https://ollanet.dev/docs).
41
+
42
+ **Use.** Prompt, continue a chat by hash, or call the same inventory from MCP and the Node library.
43
+
44
+ ## Reachability and trust
45
+
46
+ ollanet talks to Ollama endpoints you configure or select. A scan that gets an answer means the port responded. It does not mean you should send prompts or pull models there. Authentication and network protection depend on how that Ollama host is deployed. Discovery is not a security boundary, and a LAN is not inherently safe.
47
+
48
+ ## Network effects
37
49
 
38
- Discover hosts. `pull` / `show` / `rm` / `ps`. Aliases. Prompt and hash-addressed chats. Compare. Bench. MCP. Node library. Flags and config live in the [docs](https://ollanet.dev/docs).
50
+ This package does not include vendor telemetry. Commands still cause network traffic to the hosts you name.
51
+
52
+ | Operation | Client sends | Remote host may do | Stored where |
53
+ | --- | --- | --- | --- |
54
+ | `scan` | Health and model-list probes to discovered or configured hosts | Answer with tags | Last scan on this machine (`last-scan.json`) |
55
+ | `scan --lan` | Extra TCP probes on local `/24`s, port `11434` | Same, if something answers | Same |
56
+ | `prompt`, `compare`, `bench` | Prompt text, and prior turns when you continue a chat | Run inference | Chats, compares, or benches on this machine unless you pass `--no-save` |
57
+ | `pull` | `POST /api/pull` | Fetch the model from the Ollama registry onto that host | Model files on the remote host |
58
+ | `show`, `ps` | Read requests | Return metadata or loaded-model rows | Nothing new on disk |
59
+ | `rm` | Delete request (`--yes`) | Remove that model from the host | Model removed on the remote host |
60
+ | `alias` | None to Ollama | None | Local `config.json` |
61
+ | `chats` | None to Ollama | None | Reads local transcripts |
62
+
63
+ Other machines see a saved chat only if they share the responses directory (or the same `~/.ollanet/` when installed). This table covers the operations inspected in this repo. It does not prove every possible data path is listed.
39
64
 
40
65
  ## Finetuna
41
66
 
42
- On the machine that *runs* Ollama, **[Finetuna](https://finetuna.net)** shapes a GPU-tuned named variant. ollanet finds that name from anywhere on the network.
67
+ [Finetuna](https://finetuna.net) can shape a GPU-tuned named variant on the machine that runs Ollama. That pairing is optional. Each tool works alone.
68
+
69
+ | Role | Tool | Where |
70
+ | --- | --- | --- |
71
+ | Host-side runtime settings | Finetuna | The GPU box |
72
+ | Client-side discover, manage, use | ollanet | Any machine that can reach that host |
73
+
74
+ ```bash
75
+ ollanet show studio gemma4-ctx32k
76
+ ollanet prompt studio gemma4-ctx32k "What is MagicDNS?"
77
+ ollanet bench studio gemma4-ctx32k --hot --runs 5
78
+ ```
79
+
80
+ `bench` `quick` reports median tok/s on a 256-token counted decode, plus ping, math, and haiku checks, for that host, model, suite, and run count. Those checks are not a general quality ranking and do not compare across hardware without the same conditions.
43
81
 
44
82
  <!-- xfacts-nutrition-label -->
45
83
 
package/dist/cli.js CHANGED
@@ -13,10 +13,11 @@
13
13
  * ollanet chats
14
14
  * ollanet mcp
15
15
  */
16
- const HELP = `ollanet — Ollama over your network
16
+ const HELP = `ollanet: find, manage, and use Ollama on hosts you choose
17
17
 
18
18
  Discover hosts, manage models, prompt and compare, bench tok/s, and continue
19
19
  chats by hash. CLI for humans, MCP for agents, Node for apps.
20
+ A reachable host is not a trusted host. pull and rm change the remote host.
20
21
 
21
22
  Usage:
22
23
  ollanet scan [--json] [--all] [--lan] [--last]
package/dist/config.js CHANGED
@@ -247,6 +247,8 @@ export function hostLookupKeys(host) {
247
247
  host.dnsName,
248
248
  host.dnsName.split(".")[0] ?? "",
249
249
  host.ip,
250
+ ...(host.also ?? []),
251
+ ...(host.isSelf ? ["localhost"] : []),
250
252
  ]
251
253
  .map((k) => k.toLowerCase())
252
254
  .filter(Boolean);
package/dist/hosts.d.ts CHANGED
@@ -23,6 +23,12 @@ export interface HostTarget {
23
23
  hostname: string;
24
24
  dnsName: string;
25
25
  ip: string;
26
+ /**
27
+ * Other IPs that reach this same instance. Used when loopback and Tailscale
28
+ * Self (or a LAN NIC) are the same machine — we probe once and keep both
29
+ * addresses for resolve / display.
30
+ */
31
+ also?: string[];
26
32
  online: boolean;
27
33
  os: string;
28
34
  isSelf: boolean;
@@ -44,6 +50,11 @@ export interface DiscoverOptions {
44
50
  }
45
51
  export declare function shortName(host: HostTarget): string;
46
52
  export declare function ollamaBaseUrl(host: HostTarget, port?: number): string;
53
+ /**
54
+ * Collapse loopback, Tailscale Self, and this machine's NIC IPs into one host
55
+ * per port so scan does not list the same Ollama twice.
56
+ */
57
+ export declare function foldLocalHosts(hosts: HostTarget[]): HostTarget[];
47
58
  /** Discover candidate Ollama hosts from localhost, config, env, Tailscale, and optional LAN scan. */
48
59
  export declare function discoverHosts(opts?: DiscoverOptions): Promise<DiscoveryResult>;
49
60
  /**
package/dist/hosts.js CHANGED
@@ -37,10 +37,12 @@ export function ollamaBaseUrl(host, port = host.port || ollamaPort()) {
37
37
  return `http://${hostPart}:${port}`;
38
38
  }
39
39
  function makeHost(partial) {
40
+ const also = [...new Set((partial.also ?? []).filter((ip) => ip && ip !== partial.ip))];
40
41
  return {
41
42
  hostname: partial.hostname,
42
43
  dnsName: partial.dnsName ?? "",
43
44
  ip: partial.ip,
45
+ ...(also.length > 0 ? { also } : {}),
44
46
  online: partial.online ?? true,
45
47
  os: partial.os ?? "unknown",
46
48
  isSelf: partial.isSelf ?? false,
@@ -71,6 +73,7 @@ function mergeHosts(hosts) {
71
73
  os: existing.os !== "unknown" ? existing.os : host.os,
72
74
  isSelf: existing.isSelf || host.isSelf,
73
75
  source: existing.source === "direct" ? host.source : existing.source,
76
+ also: [...new Set([...(existing.also ?? []), ...(host.also ?? [])])].filter((ip) => ip !== existing.ip && ip !== host.ip),
74
77
  });
75
78
  }
76
79
  return [...byKey.values()].sort((a, b) => {
@@ -79,6 +82,96 @@ function mergeHosts(hosts) {
79
82
  return shortName(a).localeCompare(shortName(b));
80
83
  });
81
84
  }
85
+ function isIpv4Literal(value) {
86
+ return /^\d{1,3}(\.\d{1,3}){3}$/.test(value);
87
+ }
88
+ function localIdentityIps() {
89
+ const ips = new Set(["127.0.0.1", "::1"]);
90
+ for (const entries of Object.values(os.networkInterfaces())) {
91
+ for (const entry of entries ?? []) {
92
+ if (entry.family === "IPv4")
93
+ ips.add(entry.address);
94
+ }
95
+ }
96
+ return ips;
97
+ }
98
+ function isThisDevice(host, localIps) {
99
+ return host.isSelf || localIps.has(host.ip);
100
+ }
101
+ function nameQuality(host) {
102
+ const n = host.hostname;
103
+ if (!n || isIpv4Literal(n) || n.toLowerCase() === "localhost")
104
+ return 0;
105
+ if (host.source === "config" || host.source === "env")
106
+ return 3;
107
+ if (host.source === "tailscale")
108
+ return 2;
109
+ return 1;
110
+ }
111
+ function dnsQuality(dns) {
112
+ if (!dns || dns.toLowerCase() === "localhost" || isIpv4Literal(dns))
113
+ return 0;
114
+ if (dns.includes("."))
115
+ return 2;
116
+ return 1;
117
+ }
118
+ function pickFoldSource(group, probeIsLoopback) {
119
+ const sources = new Set(group.map((h) => h.source));
120
+ if (sources.has("config"))
121
+ return "config";
122
+ if (sources.has("env"))
123
+ return "env";
124
+ if (sources.has("localhost") && probeIsLoopback)
125
+ return "localhost";
126
+ if (sources.has("tailscale"))
127
+ return "tailscale";
128
+ return group[0].source;
129
+ }
130
+ function mergeThisDevice(group) {
131
+ const ips = [...new Set(group.flatMap((h) => [h.ip, ...(h.also ?? [])]))];
132
+ const loopback = ips.find((ip) => ip === "127.0.0.1" || ip === "::1");
133
+ const ip = loopback ?? ips[0];
134
+ const named = [...group].sort((a, b) => nameQuality(b) - nameQuality(a))[0];
135
+ const dnsHost = [...group].sort((a, b) => dnsQuality(b.dnsName) - dnsQuality(a.dnsName))[0];
136
+ return makeHost({
137
+ hostname: named.hostname,
138
+ dnsName: dnsQuality(dnsHost.dnsName) > 0 ? dnsHost.dnsName : named.dnsName,
139
+ ip,
140
+ also: ips.filter((addr) => addr !== ip),
141
+ online: group.some((h) => h.online),
142
+ os: group.find((h) => h.os !== "unknown")?.os ?? named.os,
143
+ isSelf: true,
144
+ source: pickFoldSource(group, Boolean(loopback)),
145
+ port: group[0].port,
146
+ });
147
+ }
148
+ /**
149
+ * Collapse loopback, Tailscale Self, and this machine's NIC IPs into one host
150
+ * per port so scan does not list the same Ollama twice.
151
+ */
152
+ export function foldLocalHosts(hosts) {
153
+ const localIps = localIdentityIps();
154
+ const groups = new Map();
155
+ const rest = [];
156
+ for (const host of hosts) {
157
+ if (!isThisDevice(host, localIps)) {
158
+ rest.push(host);
159
+ continue;
160
+ }
161
+ const list = groups.get(host.port) ?? [];
162
+ list.push(host);
163
+ groups.set(host.port, list);
164
+ }
165
+ const folded = [];
166
+ for (const group of groups.values()) {
167
+ folded.push(group.length === 1 ? { ...group[0], isSelf: true } : mergeThisDevice(group));
168
+ }
169
+ return [...folded, ...rest].sort((a, b) => {
170
+ if (a.isSelf !== b.isSelf)
171
+ return a.isSelf ? -1 : 1;
172
+ return shortName(a).localeCompare(shortName(b));
173
+ });
174
+ }
82
175
  function localhostTargets() {
83
176
  const hostname = os.hostname() || "localhost";
84
177
  return [
@@ -378,7 +471,7 @@ export async function discoverHosts(opts = {}) {
378
471
  sources.push("lan");
379
472
  }
380
473
  }
381
- const hosts = mergeHosts(collected);
474
+ const hosts = foldLocalHosts(mergeHosts(collected));
382
475
  const networkLabel = labels[0] ??
383
476
  (sources.includes("tailscale")
384
477
  ? "tailscale"
@@ -388,7 +481,10 @@ export async function discoverHosts(opts = {}) {
388
481
  return { hosts, sources, networkLabel };
389
482
  }
390
483
  function hostNameMatches(host, q) {
391
- const names = [host.hostname, host.dnsName, shortName(host), host.ip].map((n) => n.toLowerCase());
484
+ const names = [host.hostname, host.dnsName, shortName(host), host.ip, ...(host.also ?? [])].map((n) => n.toLowerCase());
485
+ if (host.isSelf || host.ip === "127.0.0.1" || (host.also ?? []).includes("127.0.0.1")) {
486
+ names.push("localhost");
487
+ }
392
488
  return names.includes(q) || names.some((n) => n.startsWith(`${q}.`));
393
489
  }
394
490
  /**
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * ollanet — Node library for discovering and talking to Ollama on any
3
- * reachable network (loopback, LAN, Tailscale, VPN).
2
+ * ollanet: Node library for discovering and talking to Ollama on hosts
3
+ * you configure or select (loopback, LAN, Tailscale, VPN).
4
4
  *
5
5
  * Node 20+ only. Not for the browser. LAN scan is opt-in.
6
6
  *
@@ -11,7 +11,7 @@ export { scanNetwork } from "./scan.js";
11
11
  export type { ScanOptions, ScanPayload, ScannedServer, ScanFailure } from "./scan.js";
12
12
  export { lastScan, lastScanPath, loadLastScan } from "./scan-store.js";
13
13
  export type { StoredScan } from "./scan-store.js";
14
- export { discoverHosts, resolveHost, ollamaBaseUrl, shortName, ollamaPort, } from "./hosts.js";
14
+ export { discoverHosts, foldLocalHosts, resolveHost, ollamaBaseUrl, shortName, ollamaPort, } from "./hosts.js";
15
15
  export type { DiscoverOptions, DiscoveryResult, HostTarget, HostSource, HostConfigEntry, } from "./hosts.js";
16
16
  export { loadConfig, defaultModelForHost, mergeSettings, configFromPartial, lookupAlias, expandMachineModel, } from "./config.js";
17
17
  export type { AppConfig, DiscoveryConfig, GenerateSettings, ModelAlias } from "./config.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * ollanet — Node library for discovering and talking to Ollama on any
3
- * reachable network (loopback, LAN, Tailscale, VPN).
2
+ * ollanet: Node library for discovering and talking to Ollama on hosts
3
+ * you configure or select (loopback, LAN, Tailscale, VPN).
4
4
  *
5
5
  * Node 20+ only. Not for the browser. LAN scan is opt-in.
6
6
  *
@@ -9,7 +9,7 @@
9
9
  */
10
10
  export { scanNetwork } from "./scan.js";
11
11
  export { lastScan, lastScanPath, loadLastScan } from "./scan-store.js";
12
- export { discoverHosts, resolveHost, ollamaBaseUrl, shortName, ollamaPort, } from "./hosts.js";
12
+ export { discoverHosts, foldLocalHosts, resolveHost, ollamaBaseUrl, shortName, ollamaPort, } from "./hosts.js";
13
13
  export { loadConfig, defaultModelForHost, mergeSettings, configFromPartial, lookupAlias, expandMachineModel, } from "./config.js";
14
14
  export { runPrompt } from "./prompt.js";
15
15
  export { runCompare, DEFAULT_COMPARE_PROMPT } from "./compare.js";
package/dist/mcp.js CHANGED
@@ -29,9 +29,10 @@ const PROTOCOL_VERSION = "2024-11-05";
29
29
  const TOOLS = [
30
30
  {
31
31
  name: "ollanet_scan",
32
- description: "Discover reachable Ollama hosts and list their models. " +
33
- "Use this before routing work to pick a host/model. " +
34
- "Optional LAN TCP scan (off unless lan=true; equivalent to CLI --lan) and include offline Tailscale peers.",
32
+ description: "Discover configured or selected Ollama hosts and list their models. " +
33
+ "A responding host is reachable, not automatically trusted. " +
34
+ "Optional LAN TCP scan is off unless lan=true (CLI --lan). " +
35
+ "all=true also probes offline Tailscale peers.",
35
36
  inputSchema: {
36
37
  type: "object",
37
38
  properties: {
package/dist/scan.d.ts CHANGED
@@ -23,6 +23,8 @@ export interface ScannedServer {
23
23
  hostname: string;
24
24
  dnsName: string;
25
25
  ip: string;
26
+ /** Extra IPs for the same instance (loopback + Tailscale Self, etc.). */
27
+ also?: string[];
26
28
  port: number;
27
29
  os: string;
28
30
  source: string;
package/dist/scan.js CHANGED
@@ -105,8 +105,9 @@ function printPayload(payload, cached) {
105
105
  for (const server of payload.servers) {
106
106
  const label = server.dnsName || server.hostname;
107
107
  const selfTag = server.self ? " [this device]" : "";
108
+ const also = server.also?.length ? ` also: ${server.also.join(", ")}` : "";
108
109
  console.log(`${label}${selfTag}`);
109
- console.log(` IP: ${server.ip} source: ${server.source} OS: ${server.os}`);
110
+ console.log(` IP: ${server.ip}${also} source: ${server.source} OS: ${server.os}`);
110
111
  console.log(` Endpoint: ${server.endpoint}`);
111
112
  if (server.models.length === 0) {
112
113
  console.log(" Models: (none)");
@@ -138,6 +139,7 @@ function toPayload(results, networkLabel, sources) {
138
139
  hostname: r.host.hostname,
139
140
  dnsName: r.host.dnsName,
140
141
  ip: r.host.ip,
142
+ ...(r.host.also?.length ? { also: r.host.also } : {}),
141
143
  port: r.host.port,
142
144
  os: r.host.os,
143
145
  source: r.host.source,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ollanet",
3
- "version": "0.6.8",
4
- "description": "Talk to Ollama on any reachable network. CLI for humans, MCP for agents, Node for apps",
3
+ "version": "0.6.10",
4
+ "description": "Find, manage, and use Ollama models on the hosts you choose. CLI for humans, MCP for agents, Node for apps",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ollanet": "./dist/cli.js"