ollanet 0.3.0 → 0.4.1

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,13 @@
4
4
 
5
5
  # ollanet
6
6
 
7
- Chat with **Ollama** servers on **any network you can reach** — LAN, localhost, Tailscale, VPN, or a raw IP.
7
+ Find the models. Talk to them. Keep the thread.
8
8
 
9
- Scan for models, fire prompts, and continue conversations later by a short hash. No browser UI required.
9
+ Chat with **Ollama** servers on **any network you can reach** — LAN, localhost, Tailscale, VPN, or a raw IP. CLI for humans, MCP for agents, **Node library for apps**.
10
10
 
11
- **Site:** [ollanet.dev](https://ollanet.dev) — Downpress + Cloudflare Pages (`site/`).
11
+ Scan for models, fire prompts, and continue conversations later by a short hash. No browser UI required. **Node 20+ · zero runtime deps.**
12
+
13
+ **Site:** [ollanet.dev](https://ollanet.dev) — FilePress + Cloudflare Pages (`site/`).
12
14
 
13
15
  ## Origin
14
16
 
@@ -23,16 +25,17 @@ Typical loop:
23
25
  1. On the host: `finetuna` → create something like `gemma4-ctx32k`
24
26
  2. From anywhere on the network: `ollanet scan` → `ollanet prompt that-host gemma4-ctx32k "…"`
25
27
 
26
- They share the same Ollama API; Finetuna shapes the models, ollanet finds and chats with them.
28
+ They share the same Ollama API; Finetuna shapes the models, ollanet finds and chats with them. Host tunes the model. Network finds and uses it. Same API, closed loop.
27
29
 
28
30
  ## Features
29
31
 
30
- - **Discover** Ollama hosts from localhost, `config.hosts`, `OLLANET_HOSTS`, optional Tailscale, and optional LAN scan
32
+ - **Discover** Ollama hosts from localhost, `config.hosts`, `OLLANET_HOSTS`, optional Tailscale, and optional LAN scan (`--lan`, off by default)
31
33
  - **Prompt** any hostname/IP/model with streaming replies
32
34
  - **Persist** chats as `responses/<hash>.json` with topic, machine, model, timestamps
33
35
  - **Continue** any thread with `--chat <hash>`
34
36
  - **Configure** per-machine defaults (model, temperature, context, …)
35
37
  - **MCP** — `ollanet mcp` exposes scan/prompt/chats as Model Context Protocol tools for agents
38
+ - **Library** — `import { scanNetwork } from "ollanet"` for apps (Node 20+, not the browser)
36
39
 
37
40
  ## Use cases
38
41
 
@@ -43,6 +46,7 @@ The core idea: turn “I have to remember which IP has which models loaded” in
43
46
  - **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.
44
47
  - **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>`).
45
48
  - **A “talk to any Ollama on my network” tool** — `ollanet_prompt` / `prompt … --json --no-stream`; no hard-coded endpoints.
49
+ - **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).
46
50
  - **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/`.
47
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.
48
52
 
@@ -65,7 +69,7 @@ Tailscale is optional. If the `tailscale` CLI is present, peers are included aut
65
69
  Run without installing:
66
70
 
67
71
  ```bash
68
- npx ollanet scan # from npm (once published)
72
+ npx ollanet scan
69
73
  npx --allow-git=all github:Catalyst-Forge-LLC/ollanet scan # straight from GitHub
70
74
  ```
71
75
 
@@ -76,7 +80,7 @@ npx --allow-git=all github:Catalyst-Forge-LLC/ollanet scan # straight fro
76
80
  Or install globally, which puts the `ollanet` command on your PATH:
77
81
 
78
82
  ```bash
79
- npm install -g ollanet # from npm (once published)
83
+ npm install -g ollanet
80
84
  npm install -g --allow-git=all github:Catalyst-Forge-LLC/ollanet # from GitHub
81
85
  ollanet scan
82
86
  ```
@@ -123,6 +127,49 @@ ollanet chats
123
127
  ollanet chats --id a1b2c3d4e5f6
124
128
  ```
125
129
 
130
+ ## Use as a library
131
+
132
+ ollanet is a **Node 20+ library that happens to have a CLI**. Not for the browser — discovery uses `node:net`, `child_process` (`tailscale status`), and `os.networkInterfaces()`. Call it from a dev server, CLI, Electron main, or a Worker with Node, never from Svelte/Vite client code.
133
+
134
+ Hard-coding `http://127.0.0.1:11434` treats Ollama like a local daemon. This treats it like a **private inference mesh**: any machine you can already reach. The app does not own the GPUs; it discovers them. LAN scan is opt-in so you do not look like a port scanner. Tailscale is automatic if the CLI exists. Config lives in `~/.ollanet` unless you pass `config`.
135
+
136
+ ```ts
137
+ import { scanNetwork } from "ollanet";
138
+
139
+ const { servers, sources } = await scanNetwork({ lanScan: false });
140
+ for (const s of servers) {
141
+ console.log(s.endpoint, s.models.map((m) => m.name));
142
+ }
143
+ ```
144
+
145
+ | Call | What it does | When apps should use it |
146
+ |---|---|---|
147
+ | `scanNetwork()` | localhost + config + `OLLANET_HOSTS` + Tailscale | Button / first “find servers” |
148
+ | `scanNetwork({ lanScan: true })` | plus TCP sweep of local `/24`s on port 11434 | Checkbox; never the default scan, never on every health check |
149
+ | `discoverHosts()` then probe yourself | candidates only, no `/api/tags` | If the app already has its own Ollama client |
150
+
151
+ Pass `config` to skip the file (useful when the app already has its own config):
152
+
153
+ ```ts
154
+ await scanNetwork({
155
+ lanScan: false,
156
+ config: {
157
+ hosts: ["studio.tailnet.ts.net", "192.168.1.50"],
158
+ discovery: { localhost: true, tailscale: true, lan: false },
159
+ },
160
+ });
161
+ ```
162
+
163
+ `runPrompt({ save: false, writeStdout: false })` is a one-shot call that does not write `~/.ollanet/responses/`.
164
+
165
+ Requires **ollanet ≥ 0.4.0**. Bundlers (Vite / SvelteKit SSR):
166
+
167
+ ```ts
168
+ // vite.config.ts
169
+ ssr: { external: ["ollanet"] },
170
+ optimizeDeps: { exclude: ["ollanet"] },
171
+ ```
172
+
126
173
  ## Commands
127
174
 
128
175
  | Command | Description |
@@ -307,6 +354,7 @@ The first turn asks the model for a short topic title (falls back to the prompt
307
354
  ```text
308
355
  ollanet/
309
356
  src/
357
+ index.ts # public library entry (scan, discover, prompt, ollama helpers)
310
358
  cli.ts # subcommand router (CLI entry)
311
359
  hosts.ts # discovery + host resolution
312
360
  scan.ts
@@ -316,11 +364,13 @@ ollanet/
316
364
  config.ts
317
365
  paths.ts # checkout vs installed path resolution
318
366
  ollama-chat.ts # shared /api/chat (+ tags/show/ps helpers)
367
+ mcp.ts # stdio MCP server
319
368
  bench.ts # ollanet bench
320
369
  bench-suite.ts
321
370
  bench-store.ts
322
371
  dist/ # compiled output (published to npm)
323
372
  docs/bench-spec.md # bench measurement spec
373
+ docs/library-api-spec.md
324
374
  config.json
325
375
  responses/ # gitignored chat history
326
376
  benchmarks/ # gitignored bench results
@@ -0,0 +1,3 @@
1
+ export declare function benchmarksDir(): string;
2
+ export declare function newBenchId(): string;
3
+ export declare function saveBenchmark(result: unknown): Promise<string>;
@@ -0,0 +1,25 @@
1
+ export type CaseRole = "check" | "live" | "throughput";
2
+ export interface BenchCase {
3
+ id: string;
4
+ role: CaseRole;
5
+ prompt: string;
6
+ /** Cap for this case; throughput uses settings.throughput_num_predict. */
7
+ num_predict?: number;
8
+ format?: "json";
9
+ check?: (content: string) => {
10
+ ok: boolean;
11
+ detail?: string;
12
+ };
13
+ }
14
+ export type SuiteName = "quick" | "full";
15
+ export declare function getSuiteCases(suite: SuiteName): BenchCase[];
16
+ export declare function suiteRevision(suite: SuiteName): string;
17
+ export declare function comparabilityKey(opts: {
18
+ suite: SuiteName;
19
+ throughputNumPredict: number;
20
+ seed: number;
21
+ temperature: number;
22
+ think: boolean;
23
+ numCtx: number | null;
24
+ }): string;
25
+ export declare function median(values: number[]): number | undefined;
@@ -0,0 +1 @@
1
+ export declare function main(): Promise<void>;
package/dist/bench.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  /**
3
2
  * ollanet bench — speed + lightweight quality suite against Ollama models.
4
3
  */
@@ -0,0 +1,50 @@
1
+ export type ChatRole = "system" | "user" | "assistant";
2
+ export interface ChatMessage {
3
+ role: ChatRole;
4
+ content: string;
5
+ /** Present for thinking models when reasoning was returned separately. */
6
+ thinking?: string;
7
+ timestamp: string;
8
+ machine?: string;
9
+ model?: string;
10
+ stats?: {
11
+ eval_count?: number;
12
+ eval_duration?: number;
13
+ total_duration?: number;
14
+ prompt_eval_count?: number;
15
+ };
16
+ }
17
+ export interface ChatTranscript {
18
+ id: string;
19
+ topic: string;
20
+ created_at: string;
21
+ updated_at: string;
22
+ machine: string;
23
+ model: string;
24
+ system?: string;
25
+ messages: ChatMessage[];
26
+ }
27
+ export declare function responsesDir(): string;
28
+ export declare function newChatId(): string;
29
+ export declare function chatPath(id: string): string;
30
+ export declare function normalizeChatId(id: string): string;
31
+ export declare function ensureResponsesDir(): Promise<string>;
32
+ export declare function loadChat(id: string): Promise<ChatTranscript>;
33
+ export declare function saveChat(chat: ChatTranscript): Promise<string>;
34
+ export declare function createChat(opts: {
35
+ machine: string;
36
+ model: string;
37
+ system?: string;
38
+ topic?: string;
39
+ }): ChatTranscript;
40
+ export declare function appendMessage(chat: ChatTranscript, message: Omit<ChatMessage, "timestamp"> & {
41
+ timestamp?: string;
42
+ }): ChatMessage;
43
+ export declare function listChats(): Promise<ChatTranscript[]>;
44
+ /** Fallback topic from the first user message when model summarization fails. */
45
+ export declare function topicFromPrompt(prompt: string): string;
46
+ export declare function cleanTopic(raw: string, fallback: string): string;
47
+ export declare function toApiMessages(chat: ChatTranscript): Array<{
48
+ role: ChatRole;
49
+ content: string;
50
+ }>;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * List saved ollanet chats.
3
+ *
4
+ * Usage:
5
+ * ollanet chats
6
+ * ollanet chats --json
7
+ * ollanet chats --id a1b2c3d4e5f6
8
+ */
9
+ export declare function main(): Promise<void>;
package/dist/chats.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  /**
3
2
  * List saved ollanet chats.
4
3
  *
package/dist/cli.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ollanet — chat with Ollama servers on any reachable network.
4
+ *
5
+ * ollanet scan
6
+ * ollanet prompt <machine-or-ip> "hello"
7
+ * ollanet chats
8
+ */
9
+ export {};
@@ -0,0 +1,45 @@
1
+ import { type DiscoveryConfig, type HostConfigEntry, type HostTarget } from "./hosts.js";
2
+ export type { DiscoveryConfig, HostConfigEntry };
3
+ /** Runtime knobs sent to Ollama `/api/generate`. */
4
+ export interface GenerateSettings {
5
+ temperature?: number;
6
+ num_predict?: number;
7
+ num_ctx?: number;
8
+ /** Ollama `options.seed` (-1 = random). */
9
+ seed?: number;
10
+ keep_alive?: string | number;
11
+ /** `"json"` or a JSON-schema object */
12
+ format?: "json" | Record<string, unknown>;
13
+ system?: string;
14
+ /**
15
+ * Ollama "thinking" models (e.g. qwen3). When false/omitted, requests send
16
+ * `think: false` so tokens go to the visible reply instead of hidden reasoning.
17
+ */
18
+ think?: boolean;
19
+ }
20
+ export interface AppConfig {
21
+ /** Explicit hosts/IPs to probe (LAN, VPN, MagicDNS, etc.). */
22
+ hosts: HostConfigEntry[];
23
+ discovery: DiscoveryConfig;
24
+ defaultModels: Record<string, string>;
25
+ /** Global defaults applied to every prompt. */
26
+ defaults: GenerateSettings;
27
+ /** Per-machine overrides (keyed by short name / hostname / DNS / IP). */
28
+ machineDefaults: Record<string, GenerateSettings & {
29
+ model?: string;
30
+ }>;
31
+ }
32
+ /** Build an AppConfig from an in-memory partial (no file I/O). */
33
+ export declare function configFromPartial(partial?: Partial<AppConfig>): AppConfig;
34
+ export declare function loadConfig(): Promise<AppConfig>;
35
+ export declare function normalizeSettings(input: unknown): GenerateSettings;
36
+ export declare function parseFormat(value: unknown): GenerateSettings["format"] | undefined;
37
+ export declare function hostLookupKeys(host: HostTarget): string[];
38
+ /** Resolve a default model for a host using short name, hostname, DNS, or IP keys. */
39
+ export declare function defaultModelForHost(config: AppConfig, host: HostTarget): string | undefined;
40
+ export declare function machineSettingsForHost(config: AppConfig, host: HostTarget): GenerateSettings;
41
+ /** Later layers win. Undefined values are skipped. */
42
+ export declare function mergeSettings(...layers: Array<GenerateSettings | undefined>): GenerateSettings;
43
+ export declare function settingsFromEnv(): GenerateSettings;
44
+ export declare function settingsSummary(settings: GenerateSettings): string;
45
+ export declare function configPath(): string;
package/dist/config.js CHANGED
@@ -10,6 +10,17 @@ function emptyConfig() {
10
10
  machineDefaults: {},
11
11
  };
12
12
  }
13
+ /** Build an AppConfig from an in-memory partial (no file I/O). */
14
+ export function configFromPartial(partial = {}) {
15
+ const empty = emptyConfig();
16
+ return {
17
+ hosts: Array.isArray(partial.hosts) ? partial.hosts : empty.hosts,
18
+ discovery: partial.discovery ?? empty.discovery,
19
+ defaultModels: partial.defaultModels ?? empty.defaultModels,
20
+ defaults: partial.defaults ?? empty.defaults,
21
+ machineDefaults: partial.machineDefaults ?? empty.machineDefaults,
22
+ };
23
+ }
13
24
  const CONFIG_PATH = process.env.OLLANET_CONFIG ??
14
25
  process.env.OLLAMA_CONFIG ??
15
26
  defaultConfigPath();
@@ -0,0 +1,55 @@
1
+ /** Parse a required numeric env var; invalid values throw instead of becoming NaN. */
2
+ export declare function envInt(name: string, fallback: number): number;
3
+ /** Default Ollama port. Read at call time so a bad env var cannot break `import`. */
4
+ export declare function ollamaPort(): number;
5
+ export type HostSource = "localhost" | "config" | "env" | "tailscale" | "lan" | "direct";
6
+ export type HostConfigEntry = string | {
7
+ name?: string;
8
+ host?: string;
9
+ ip?: string;
10
+ port?: number;
11
+ };
12
+ export interface DiscoveryConfig {
13
+ /** Always include localhost / 127.0.0.1 (default true). */
14
+ localhost?: boolean;
15
+ /** Use Tailscale peers when the CLI is available (default true). */
16
+ tailscale?: boolean;
17
+ /** TCP-scan local LAN CIDRs for open Ollama ports (default false). */
18
+ lan?: boolean;
19
+ /** Explicit CIDRs for LAN scan (default: guessed /24s from local interfaces). */
20
+ cidrs?: string[];
21
+ }
22
+ export interface HostTarget {
23
+ hostname: string;
24
+ dnsName: string;
25
+ ip: string;
26
+ online: boolean;
27
+ os: string;
28
+ isSelf: boolean;
29
+ source: HostSource;
30
+ port: number;
31
+ }
32
+ export interface DiscoveryResult {
33
+ hosts: HostTarget[];
34
+ sources: string[];
35
+ networkLabel: string;
36
+ }
37
+ export interface DiscoverOptions {
38
+ hosts?: HostConfigEntry[];
39
+ discovery?: DiscoveryConfig;
40
+ /** Include Tailscale peers marked offline. */
41
+ includeOffline?: boolean;
42
+ /** Probe local LAN CIDRs for open Ollama ports. Off unless true. */
43
+ lanScan?: boolean;
44
+ }
45
+ export declare function shortName(host: HostTarget): string;
46
+ export declare function ollamaBaseUrl(host: HostTarget, port?: number): string;
47
+ /** Discover candidate Ollama hosts from localhost, config, env, Tailscale, and optional LAN scan. */
48
+ export declare function discoverHosts(opts?: DiscoverOptions): Promise<DiscoveryResult>;
49
+ /**
50
+ * Match a query against already-discovered hosts only (no direct-address fallback).
51
+ * Used when peeling an optional leading hostname before a prompt.
52
+ */
53
+ export declare function findDiscoveredHost(targets: HostTarget[], query: string): HostTarget | undefined;
54
+ /** Resolve a machine name against discovered hosts, or treat it as a direct address. */
55
+ export declare function resolveHost(targets: HostTarget[], query: string): HostTarget;
package/dist/hosts.js CHANGED
@@ -14,7 +14,10 @@ export function envInt(name, fallback) {
14
14
  }
15
15
  return Math.trunc(n);
16
16
  }
17
- export const OLLAMA_PORT = envInt("OLLAMA_PORT", 11434);
17
+ /** Default Ollama port. Read at call time so a bad env var cannot break `import`. */
18
+ export function ollamaPort() {
19
+ return envInt("OLLAMA_PORT", 11434);
20
+ }
18
21
  function cleanDnsName(dnsName) {
19
22
  if (!dnsName)
20
23
  return "";
@@ -29,7 +32,7 @@ export function shortName(host) {
29
32
  }
30
33
  return host.hostname;
31
34
  }
32
- export function ollamaBaseUrl(host, port = host.port || OLLAMA_PORT) {
35
+ export function ollamaBaseUrl(host, port = host.port || ollamaPort()) {
33
36
  const hostPart = host.ip.includes(":") ? `[${host.ip}]` : host.ip;
34
37
  return `http://${hostPart}:${port}`;
35
38
  }
@@ -42,7 +45,7 @@ function makeHost(partial) {
42
45
  os: partial.os ?? "unknown",
43
46
  isSelf: partial.isSelf ?? false,
44
47
  source: partial.source,
45
- port: partial.port ?? OLLAMA_PORT,
48
+ port: partial.port ?? ollamaPort(),
46
49
  };
47
50
  }
48
51
  function dedupeKey(host) {
@@ -114,7 +117,7 @@ function parseHostEntry(entry) {
114
117
  ip: address,
115
118
  source: "config",
116
119
  online: true,
117
- port: typeof entry.port === "number" ? entry.port : OLLAMA_PORT,
120
+ port: typeof entry.port === "number" ? entry.port : ollamaPort(),
118
121
  });
119
122
  }
120
123
  function envHostTargets() {
@@ -298,7 +301,7 @@ function parseDirectAddress(query) {
298
301
  return null;
299
302
  let hostname = q;
300
303
  let ip = q;
301
- let port = OLLAMA_PORT;
304
+ let port = ollamaPort();
302
305
  const ipv4WithPort = q.match(/^(\d{1,3}(?:\.\d{1,3}){3}):(\d+)$/);
303
306
  if (ipv4WithPort) {
304
307
  ip = ipv4WithPort[1];
@@ -365,7 +368,7 @@ export async function discoverHosts(opts = {}) {
365
368
  const lanEnabled = opts.lanScan ?? discovery.lan === true;
366
369
  if (lanEnabled) {
367
370
  const cidrs = discovery.cidrs && discovery.cidrs.length > 0 ? discovery.cidrs : guessLocalCidrs();
368
- const lanHosts = await lanScanTargets(cidrs, OLLAMA_PORT);
371
+ const lanHosts = await lanScanTargets(cidrs, ollamaPort());
369
372
  if (lanHosts.length > 0) {
370
373
  collected.push(...lanHosts);
371
374
  sources.push("lan");
@@ -0,0 +1,18 @@
1
+ /**
2
+ * ollanet — Node library for discovering and talking to Ollama on any
3
+ * reachable network (loopback, LAN, Tailscale, VPN).
4
+ *
5
+ * Node 20+ only. Not for the browser. LAN scan is opt-in.
6
+ *
7
+ * import { scanNetwork } from "ollanet";
8
+ * const { servers } = await scanNetwork({ lanScan: false });
9
+ */
10
+ export { scanNetwork } from "./scan.js";
11
+ export type { ScanOptions, ScanPayload, ScannedServer } from "./scan.js";
12
+ export { discoverHosts, resolveHost, ollamaBaseUrl, shortName, ollamaPort, } from "./hosts.js";
13
+ export type { DiscoverOptions, DiscoveryResult, HostTarget, HostSource, HostConfigEntry, } from "./hosts.js";
14
+ export { loadConfig, defaultModelForHost, mergeSettings, configFromPartial, } from "./config.js";
15
+ export type { AppConfig, DiscoveryConfig, GenerateSettings } from "./config.js";
16
+ export { runPrompt } from "./prompt.js";
17
+ export type { PromptRunOptions, PromptRunResult } from "./prompt.js";
18
+ export { ollamaChat, ollamaTags, ollamaShow, ollamaPs } from "./ollama-chat.js";
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * ollanet — Node library for discovering and talking to Ollama on any
3
+ * reachable network (loopback, LAN, Tailscale, VPN).
4
+ *
5
+ * Node 20+ only. Not for the browser. LAN scan is opt-in.
6
+ *
7
+ * import { scanNetwork } from "ollanet";
8
+ * const { servers } = await scanNetwork({ lanScan: false });
9
+ */
10
+ export { scanNetwork } from "./scan.js";
11
+ export { discoverHosts, resolveHost, ollamaBaseUrl, shortName, ollamaPort, } from "./hosts.js";
12
+ export { loadConfig, defaultModelForHost, mergeSettings, configFromPartial, } from "./config.js";
13
+ export { runPrompt } from "./prompt.js";
14
+ export { ollamaChat, ollamaTags, ollamaShow, ollamaPs } from "./ollama-chat.js";
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ollanet mcp — stdio MCP server (zero deps).
3
+ *
4
+ * Speaks the Model Context Protocol over stdin/stdout so agents (Cursor,
5
+ * Claude Desktop, etc.) can discover hosts, prompt models, and continue chats.
6
+ *
7
+ * ollanet mcp
8
+ *
9
+ * stdout is reserved for JSON-RPC; log only to stderr.
10
+ */
11
+ export declare function main(): Promise<void>;
package/dist/mcp.js CHANGED
@@ -14,13 +14,15 @@ import { listChats, loadChat } from "./chat-store.js";
14
14
  import { projectPath } from "./paths.js";
15
15
  import { runPrompt } from "./prompt.js";
16
16
  import { scanNetwork } from "./scan.js";
17
+ // Pin to the oldest widely-supported MCP revision. Clients may request a newer
18
+ // date; we still answer with this — legal negotiation, max compatibility.
17
19
  const PROTOCOL_VERSION = "2024-11-05";
18
20
  const TOOLS = [
19
21
  {
20
22
  name: "ollanet_scan",
21
23
  description: "Discover reachable Ollama hosts and list their models. " +
22
24
  "Use this before routing work to pick a host/model. " +
23
- "Optional LAN TCP scan (--lan equivalent) and include offline Tailscale peers.",
25
+ "Optional LAN TCP scan (off unless lan=true; equivalent to CLI --lan) and include offline Tailscale peers.",
24
26
  inputSchema: {
25
27
  type: "object",
26
28
  properties: {
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Shared Ollama /api/chat client used by prompt and bench.
3
+ */
4
+ import type { GenerateSettings } from "./config.js";
5
+ export interface ChatChunk {
6
+ model?: string;
7
+ message?: {
8
+ role?: string;
9
+ content?: string;
10
+ thinking?: string;
11
+ };
12
+ done?: boolean;
13
+ done_reason?: string;
14
+ error?: string;
15
+ total_duration?: number;
16
+ load_duration?: number;
17
+ prompt_eval_count?: number;
18
+ eval_count?: number;
19
+ eval_duration?: number;
20
+ }
21
+ export interface OllamaChatOptions {
22
+ baseUrl: string;
23
+ model: string;
24
+ messages: Array<{
25
+ role: string;
26
+ content: string;
27
+ }>;
28
+ stream: boolean;
29
+ settings: GenerateSettings;
30
+ /** Write assistant content to stdout / thinking to stderr when streaming. */
31
+ writeStdout: boolean;
32
+ /** Timeout in ms; 0 = none. */
33
+ timeoutMs: number;
34
+ }
35
+ export interface OllamaChatResult {
36
+ content: string;
37
+ thinking: string;
38
+ chunk: ChatChunk;
39
+ }
40
+ /** POST /api/chat with optional streaming and AbortSignal timeout. */
41
+ export declare function ollamaChat(opts: OllamaChatOptions): Promise<OllamaChatResult>;
42
+ export interface OllamaModelInfo {
43
+ name: string;
44
+ digest?: string;
45
+ size?: number;
46
+ details?: {
47
+ parameter_size?: string;
48
+ quantization_level?: string;
49
+ family?: string;
50
+ };
51
+ }
52
+ export declare function ollamaTags(baseUrl: string, timeoutMs: number): Promise<OllamaModelInfo[]>;
53
+ export declare function ollamaShow(baseUrl: string, model: string, timeoutMs: number): Promise<{
54
+ capabilities?: string[];
55
+ }>;
56
+ export declare function ollamaVersion(baseUrl: string, timeoutMs: number): Promise<string | undefined>;
57
+ export interface PsModel {
58
+ name?: string;
59
+ model?: string;
60
+ size_vram?: number;
61
+ context_length?: number;
62
+ digest?: string;
63
+ }
64
+ export declare function ollamaPs(baseUrl: string, timeoutMs: number): Promise<PsModel[]>;
65
+ export declare function psHasModel(models: PsModel[], name: string): boolean;
66
+ export declare function vramForModel(models: PsModel[], name: string): number | undefined;
67
+ export declare function contextLengthForModel(models: PsModel[], name: string): number | undefined;
68
+ /**
69
+ * Capability filter: omitempty / empty means "unknown → treat as completion".
70
+ * Only skip when a non-empty capabilities array is present and lacks completion.
71
+ */
72
+ export declare function isCompletionCapable(capabilities: string[] | undefined | null): boolean;
73
+ /**
74
+ * Vision-capable models (moondream, llava, …) often fail a text-only suite.
75
+ * Only true when capabilities are present and include vision — absent/empty → false
76
+ * (don't skip unknown older servers).
77
+ */
78
+ export declare function isVisionCapable(capabilities: string[] | undefined | null): boolean;
79
+ /** True when capabilities are known and lack thinking (absent/empty → no warning). */
80
+ export declare function shouldWarnNoThinking(capabilities: string[] | undefined | null): boolean;
81
+ /** Poll /api/ps until model is absent (or timeout). Returns elapsed ms. */
82
+ export declare function waitUntilUnloaded(baseUrl: string, model: string, timeoutMs: number, pollMs?: number): Promise<{
83
+ unloaded: boolean;
84
+ waitedMs: number;
85
+ }>;
86
+ /** Request unload via keep_alive: 0 (no-op-ish if already unloaded after a short chat). */
87
+ export declare function ollamaUnload(baseUrl: string, model: string, timeoutMs: number): Promise<void>;
@@ -0,0 +1,17 @@
1
+ /** Package root: parent of `src/` in a checkout, parent of `dist/` when installed. */
2
+ export declare const PROJECT_ROOT: string;
3
+ export declare function projectPath(...parts: string[]): string;
4
+ /**
5
+ * True when running from an installed copy (npx cache, global install, or as a
6
+ * dependency) rather than a source checkout. Installed copies always live under
7
+ * a `node_modules` directory.
8
+ */
9
+ export declare const IS_INSTALLED: boolean;
10
+ /** Per-user data dir used when installed (config + chat transcripts). */
11
+ export declare const USER_DIR: string;
12
+ /** Default config location: repo-local in a checkout, `~/.ollanet/` when installed. */
13
+ export declare function defaultConfigPath(): string;
14
+ /** Default transcript dir: repo-local in a checkout, `~/.ollanet/` when installed. */
15
+ export declare function defaultResponsesDir(): string;
16
+ /** Default benchmark results dir (mirrors responses layout). */
17
+ export declare function defaultBenchmarksDir(): string;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Run a prompt / continue a chat against an Ollama server on the network.
3
+ *
4
+ * New chat (saved by default):
5
+ * ollanet prompt localhost "What is Tailscale?"
6
+ * ollanet prompt 192.168.1.50 "Hello"
7
+ *
8
+ * Continue:
9
+ * ollanet prompt --chat a1b2c3d4e5f6 "Tell me more"
10
+ *
11
+ * List chats:
12
+ * ollanet chats
13
+ */
14
+ import { type AppConfig, type GenerateSettings } from "./config.js";
15
+ export interface PromptRunOptions {
16
+ machine?: string;
17
+ model?: string;
18
+ prompt: string;
19
+ chatId?: string;
20
+ /** Persist transcript. Default true; pass false for one-shot app calls. */
21
+ save?: boolean;
22
+ settings?: GenerateSettings;
23
+ /** When false, never write the reply to stdout (MCP / programmatic). Default true for CLI. */
24
+ writeStdout?: boolean;
25
+ stream?: boolean;
26
+ quiet?: boolean;
27
+ /** In-memory config; skips the config file when set. */
28
+ config?: Partial<AppConfig>;
29
+ }
30
+ export interface PromptRunResult {
31
+ chat_id: string | null;
32
+ topic: string;
33
+ content: string;
34
+ thinking: string | null;
35
+ machine: string;
36
+ model: string;
37
+ ollama: Record<string, unknown>;
38
+ }
39
+ /** Programmatic prompt used by CLI `--json` and the MCP server. */
40
+ export declare function runPrompt(opts: PromptRunOptions): Promise<PromptRunResult>;
41
+ export declare function main(): Promise<void>;
package/dist/prompt.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  /**
3
2
  * Run a prompt / continue a chat against an Ollama server on the network.
4
3
  *
@@ -14,11 +13,13 @@
14
13
  */
15
14
  import { stdin as stdinStream } from "node:process";
16
15
  import { appendMessage, cleanTopic, createChat, loadChat, saveChat, toApiMessages, topicFromPrompt, } from "./chat-store.js";
17
- import { configPath, defaultModelForHost, loadConfig, machineSettingsForHost, mergeSettings, parseFormat, settingsFromEnv, settingsSummary, } from "./config.js";
16
+ import { configPath, defaultModelForHost, loadConfig, machineSettingsForHost, mergeSettings, parseFormat, settingsFromEnv, settingsSummary, configFromPartial, } from "./config.js";
18
17
  import { discoverHosts, envInt, findDiscoveredHost, ollamaBaseUrl, resolveHost, shortName, } from "./hosts.js";
19
18
  import { ollamaChat } from "./ollama-chat.js";
20
19
  /** Prompt/chat HTTP timeout (ms). 0 = no timeout. Separate from scan's OLLAMA_TIMEOUT_MS. */
21
- const PROMPT_TIMEOUT_MS = envInt("OLLAMA_PROMPT_TIMEOUT_MS", 600_000);
20
+ function promptTimeoutMs() {
21
+ return envInt("OLLAMA_PROMPT_TIMEOUT_MS", 600_000);
22
+ }
22
23
  function usage() {
23
24
  console.error(`Usage:
24
25
  ollanet prompt <machine> [model] <prompt...>
@@ -77,7 +78,7 @@ function modelNameMatches(available, candidate) {
77
78
  async function listModelNames(host) {
78
79
  const url = `${ollamaBaseUrl(host)}/api/tags`;
79
80
  const controller = new AbortController();
80
- const timer = setTimeout(() => controller.abort(), Math.min(PROMPT_TIMEOUT_MS || 2500, 5000));
81
+ const timer = setTimeout(() => controller.abort(), Math.min(promptTimeoutMs() || 2500, 5000));
81
82
  try {
82
83
  const res = await fetch(url, {
83
84
  signal: controller.signal,
@@ -278,7 +279,7 @@ async function generateTopic(opts) {
278
279
  model: opts.model,
279
280
  stream: false,
280
281
  writeStdout: false,
281
- timeoutMs: PROMPT_TIMEOUT_MS,
282
+ timeoutMs: promptTimeoutMs(),
282
283
  settings: mergeSettings(opts.settings, {
283
284
  temperature: 0.2,
284
285
  num_predict: 24,
@@ -315,7 +316,7 @@ export async function runPrompt(opts) {
315
316
  const quiet = opts.quiet === true || opts.writeStdout === false;
316
317
  const writeStdout = opts.writeStdout !== false;
317
318
  let promptParts = opts.prompt ? [opts.prompt] : [];
318
- const config = await loadConfig();
319
+ const config = opts.config ? configFromPartial(opts.config) : await loadConfig();
319
320
  const { hosts: targets } = await discoverHosts({
320
321
  hosts: config.hosts,
321
322
  discovery: config.discovery,
@@ -388,7 +389,7 @@ export async function runPrompt(opts) {
388
389
  messages: toApiMessages(chat),
389
390
  stream: writeStdout ? stream : false,
390
391
  writeStdout,
391
- timeoutMs: PROMPT_TIMEOUT_MS,
392
+ timeoutMs: promptTimeoutMs(),
392
393
  settings,
393
394
  });
394
395
  appendMessage(chat, {
package/dist/scan.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Scan reachable networks for Ollama servers and list available models.
3
+ *
4
+ * Usage: ollanet scan [--lan] [--json] [--all]
5
+ */
6
+ import { type AppConfig } from "./config.js";
7
+ export interface ScanOptions {
8
+ includeOffline?: boolean;
9
+ /** TCP-scan local /24s for Ollama (port 11434 by default). Off unless set. */
10
+ lanScan?: boolean;
11
+ /**
12
+ * In-memory config. When set, the config file (`~/.ollanet` / `OLLANET_CONFIG`)
13
+ * is not read. Omit to keep CLI/MCP file behavior.
14
+ */
15
+ config?: Partial<AppConfig>;
16
+ }
17
+ export interface ScannedServer {
18
+ hostname: string;
19
+ dnsName: string;
20
+ ip: string;
21
+ port: number;
22
+ os: string;
23
+ source: string;
24
+ self: boolean;
25
+ endpoint: string;
26
+ models: Array<{
27
+ name: string;
28
+ size?: number;
29
+ modified_at?: string;
30
+ parameter_size?: string;
31
+ quantization_level?: string;
32
+ family?: string;
33
+ }>;
34
+ }
35
+ export interface ScanPayload {
36
+ network: string;
37
+ sources: string[];
38
+ port: number;
39
+ scanned: number;
40
+ servers: ScannedServer[];
41
+ }
42
+ /** Programmatic scan used by CLI `--json`, MCP, and `import { scanNetwork } from "ollanet"`. */
43
+ export declare function scanNetwork(options?: ScanOptions): Promise<ScanPayload>;
44
+ export declare function main(): Promise<void>;
package/dist/scan.js CHANGED
@@ -1,13 +1,16 @@
1
- #!/usr/bin/env node
2
1
  /**
3
2
  * Scan reachable networks for Ollama servers and list available models.
4
3
  *
5
4
  * Usage: ollanet scan [--lan] [--json] [--all]
6
5
  */
7
- import { loadConfig } from "./config.js";
8
- import { OLLAMA_PORT, discoverHosts, envInt, shortName, } from "./hosts.js";
9
- const TIMEOUT_MS = envInt("OLLAMA_TIMEOUT_MS", 2500);
10
- const CONCURRENCY = Math.max(1, envInt("OLLAMA_CONCURRENCY", 16));
6
+ import { configFromPartial, loadConfig } from "./config.js";
7
+ import { discoverHosts, envInt, ollamaPort, shortName, } from "./hosts.js";
8
+ function scanTimeoutMs() {
9
+ return envInt("OLLAMA_TIMEOUT_MS", 2500);
10
+ }
11
+ function scanConcurrency() {
12
+ return Math.max(1, envInt("OLLAMA_CONCURRENCY", 16));
13
+ }
11
14
  function formatBytes(bytes) {
12
15
  if (bytes == null || !Number.isFinite(bytes))
13
16
  return "";
@@ -22,8 +25,9 @@ function formatBytes(bytes) {
22
25
  }
23
26
  async function fetchModels(host) {
24
27
  const url = `http://${host.ip.includes(":") ? `[${host.ip}]` : host.ip}:${host.port}/api/tags`;
28
+ const timeoutMs = scanTimeoutMs();
25
29
  const controller = new AbortController();
26
- const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
30
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
27
31
  try {
28
32
  const res = await fetch(url, {
29
33
  signal: controller.signal,
@@ -66,7 +70,7 @@ async function scanHost(host) {
66
70
  const name = err instanceof Error ? err.name : "";
67
71
  const message = err instanceof Error ? err.message : String(err);
68
72
  const error = name === "AbortError" || message.toLowerCase().includes("abort")
69
- ? `timeout after ${TIMEOUT_MS}ms`
73
+ ? `timeout after ${scanTimeoutMs()}ms`
70
74
  : message;
71
75
  return { host, ok: false, models: [], error, url };
72
76
  }
@@ -117,7 +121,7 @@ function toPayload(results, networkLabel, sources) {
117
121
  return {
118
122
  network: networkLabel,
119
123
  sources,
120
- port: OLLAMA_PORT,
124
+ port: ollamaPort(),
121
125
  scanned: results.length,
122
126
  servers: results
123
127
  .filter((r) => r.ok)
@@ -141,9 +145,9 @@ function toPayload(results, networkLabel, sources) {
141
145
  })),
142
146
  };
143
147
  }
144
- /** Programmatic scan used by CLI `--json` and the MCP server. */
148
+ /** Programmatic scan used by CLI `--json`, MCP, and `import { scanNetwork } from "ollanet"`. */
145
149
  export async function scanNetwork(options = {}) {
146
- const config = await loadConfig();
150
+ const config = options.config ? configFromPartial(options.config) : await loadConfig();
147
151
  const { hosts, sources, networkLabel } = await discoverHosts({
148
152
  hosts: config.hosts,
149
153
  discovery: config.discovery,
@@ -153,7 +157,7 @@ export async function scanNetwork(options = {}) {
153
157
  if (hosts.length === 0) {
154
158
  throw new Error("No hosts to scan. Add config.hosts, set OLLANET_HOSTS, or enable Tailscale/LAN discovery.");
155
159
  }
156
- const results = await mapPool(hosts, CONCURRENCY, scanHost);
160
+ const results = await mapPool(hosts, scanConcurrency(), scanHost);
157
161
  return toPayload(results, networkLabel, sources);
158
162
  }
159
163
  export async function main() {
@@ -172,7 +176,7 @@ export async function main() {
172
176
  process.exitCode = 1;
173
177
  return;
174
178
  }
175
- const results = await mapPool(hosts, CONCURRENCY, scanHost);
179
+ const results = await mapPool(hosts, scanConcurrency(), scanHost);
176
180
  if (jsonOut) {
177
181
  console.log(JSON.stringify(toPayload(results, networkLabel, sources), null, 2));
178
182
  return;
package/package.json CHANGED
@@ -1,19 +1,31 @@
1
1
  {
2
2
  "name": "ollanet",
3
- "version": "0.3.0",
4
- "description": "Chat with Ollama servers on any reachable network — scan hosts, prompt models, continue chats by hash, MCP for agents",
3
+ "version": "0.4.1",
4
+ "description": "Discover and talk to Ollama on any reachable network — CLI, MCP, and a Node library",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ollanet": "./dist/cli.js"
8
8
  },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./scan": {
15
+ "types": "./dist/scan.d.ts",
16
+ "import": "./dist/scan.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "types": "./dist/index.d.ts",
9
21
  "files": [
10
22
  "dist",
11
23
  "README.md",
12
24
  "LICENSE"
13
25
  ],
14
26
  "scripts": {
15
- "build": "tsc -p tsconfig.build.json",
16
- "prepare": "tsc -p tsconfig.build.json",
27
+ "build": "tsc -p tsconfig.build.json && node scripts/fix-dts-extensions.mjs",
28
+ "prepare": "tsc -p tsconfig.build.json && node scripts/fix-dts-extensions.mjs",
17
29
  "ollanet": "tsx src/cli.ts",
18
30
  "scan": "tsx src/cli.ts scan",
19
31
  "prompt": "tsx src/cli.ts prompt",
@@ -22,7 +34,7 @@
22
34
  "mcp": "tsx src/cli.ts mcp",
23
35
  "start": "tsx src/cli.ts scan",
24
36
  "typecheck": "tsc --noEmit",
25
- "test": "tsc -p tsconfig.build.json && node --test test/*.test.mjs",
37
+ "test": "tsc -p tsconfig.build.json && node scripts/fix-dts-extensions.mjs && node --test test/*.test.mjs",
26
38
  "test:only": "node --test test/*.test.mjs",
27
39
  "site:dev": "pnpm --dir site dev",
28
40
  "site:build": "pnpm --dir site build",
@@ -37,7 +49,8 @@
37
49
  "tailscale",
38
50
  "network",
39
51
  "mcp",
40
- "model-context-protocol"
52
+ "model-context-protocol",
53
+ "library"
41
54
  ],
42
55
  "author": "Catalyst Forge LLC",
43
56
  "license": "MIT",