ollanet 0.6.7 → 0.6.9

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/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
@@ -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
@@ -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/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,6 +1,6 @@
1
1
  {
2
2
  "name": "ollanet",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "Talk to Ollama on any reachable network. CLI for humans, MCP for agents, Node for apps",
5
5
  "type": "module",
6
6
  "bin": {