ollanet 0.5.0 → 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/prompt.js CHANGED
@@ -16,6 +16,7 @@ import { appendMessage, cleanTopic, createChat, loadChat, saveChat, toApiMessage
16
16
  import { configPath, defaultModelForHost, loadConfig, machineSettingsForHost, mergeSettings, parseFormat, settingsFromEnv, settingsSummary, configFromPartial, } from "./config.js";
17
17
  import { discoverHosts, envInt, findDiscoveredHost, ollamaBaseUrl, resolveHost, shortName, } from "./hosts.js";
18
18
  import { ollamaChat } from "./ollama-chat.js";
19
+ import { assemblePrompt, readPromptFile } from "./prompt-input.js";
19
20
  /** Prompt/chat HTTP timeout (ms). 0 = no timeout. Separate from scan's OLLAMA_TIMEOUT_MS. */
20
21
  function promptTimeoutMs() {
21
22
  return envInt("OLLAMA_PROMPT_TIMEOUT_MS", 600_000);
@@ -30,6 +31,7 @@ Examples:
30
31
  ollanet prompt localhost "Explain MagicDNS"
31
32
  ollanet prompt --chat a1b2c3d4e5f6 "Give an example"
32
33
  ollanet prompt localhost --temperature 0.2 --num-predict 64 "2+2?"
34
+ ollanet prompt localhost --file ./notes.md
33
35
  ollanet chats
34
36
 
35
37
  Each reply is saved under responses/<hash>.json and the hash is printed so you
@@ -51,6 +53,7 @@ Options:
51
53
  --format <json|schema> Force JSON mode or a JSON schema string
52
54
  --think Enable model thinking (qwen3 etc.); streams to stderr
53
55
  --no-think Disable thinking (default) so tokens go to the reply
56
+ --file <path> Prompt from a .txt or .md file (joins argv / stdin)
54
57
  --no-stream Wait for the full response
55
58
  --json Emit final response JSON (implies --no-stream)`);
56
59
  process.exit(1);
@@ -137,6 +140,7 @@ function parseArgs(argv) {
137
140
  let machineFlag;
138
141
  let modelFlag;
139
142
  let chatId;
143
+ let file;
140
144
  let stream = true;
141
145
  let json = false;
142
146
  let save = true;
@@ -181,6 +185,10 @@ function parseArgs(argv) {
181
185
  modelFlag = arg.includes("=") ? arg.slice("--model=".length) : takeValue(args, "--model");
182
186
  continue;
183
187
  }
188
+ if (arg === "--file" || arg === "-f" || arg.startsWith("--file=")) {
189
+ file = arg.includes("=") ? arg.slice("--file=".length) : takeValue(args, "--file");
190
+ continue;
191
+ }
184
192
  if (arg === "--system" || arg.startsWith("--system=")) {
185
193
  settings.system = arg.includes("=")
186
194
  ? arg.slice("--system=".length)
@@ -235,7 +243,7 @@ function parseArgs(argv) {
235
243
  }
236
244
  // Continue (--chat): all positionals are the prompt. Optional machine override
237
245
  // via --machine, or a leading discovered-host name peeled in main().
238
- return { machine, model, promptParts, stream, json, save, chatId, settings };
246
+ return { machine, model, promptParts, file, stream, json, save, chatId, settings };
239
247
  }
240
248
  async function readStdinIfPiped() {
241
249
  if (stdinStream.isTTY)
@@ -261,7 +269,7 @@ async function readStdinIfPiped() {
261
269
  const onEnd = () => done(true);
262
270
  stdinStream.once("readable", onReadable);
263
271
  stdinStream.once("end", onEnd);
264
- const timer = setTimeout(() => done(stdinStream.readableLength > 0), 25);
272
+ const timer = setTimeout(() => done(stdinStream.readableLength > 0), 100);
265
273
  });
266
274
  if (!hasData)
267
275
  return "";
@@ -436,6 +444,17 @@ export async function main() {
436
444
  const { stream, json, save } = parsed;
437
445
  let promptParts = parsed.promptParts;
438
446
  const fromStdin = await readStdinIfPiped();
447
+ let fromFile = "";
448
+ if (parsed.file) {
449
+ try {
450
+ fromFile = await readPromptFile(parsed.file);
451
+ }
452
+ catch (err) {
453
+ console.error(err instanceof Error ? err.message : err);
454
+ process.exitCode = 1;
455
+ return;
456
+ }
457
+ }
439
458
  const config = await loadConfig();
440
459
  const { hosts: targets } = await discoverHosts({
441
460
  hosts: config.hosts,
@@ -460,13 +479,17 @@ export async function main() {
460
479
  usage();
461
480
  }
462
481
  const host = resolveHost(targets, machineQuery);
463
- const peeled = await maybePeelModel(host, promptParts, fromStdin);
482
+ const peeled = await maybePeelModel(host, promptParts, fromStdin || fromFile);
464
483
  peeledModel = peeled.model;
465
484
  promptParts = peeled.promptParts;
466
485
  }
467
- const promptText = [promptParts.join(" ").trim(), fromStdin].filter(Boolean).join("\n\n").trim();
486
+ const promptText = assemblePrompt({
487
+ argv: promptParts.join(" "),
488
+ file: fromFile,
489
+ stdin: fromStdin,
490
+ });
468
491
  if (!promptText) {
469
- console.error("No prompt provided (pass args and/or pipe stdin).");
492
+ console.error("No prompt provided (pass args, --file, and/or pipe stdin).");
470
493
  usage();
471
494
  }
472
495
  try {
@@ -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;
@@ -34,12 +39,18 @@ export interface ScannedServer {
34
39
  tuned: boolean;
35
40
  }>;
36
41
  }
42
+ export interface ScanFailure {
43
+ name: string;
44
+ ip: string;
45
+ error: string;
46
+ }
37
47
  export interface ScanPayload {
38
48
  network: string;
39
49
  sources: string[];
40
50
  port: number;
41
51
  scanned: number;
42
52
  servers: ScannedServer[];
53
+ failures?: ScanFailure[];
43
54
  }
44
55
  /** Programmatic scan used by CLI `--json`, MCP, and `import { scanNetwork } from "ollanet"`. */
45
56
  export declare function scanNetwork(options?: ScanOptions): Promise<ScanPayload>;
package/dist/scan.js CHANGED
@@ -1,10 +1,11 @@
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";
8
9
  import { looksTuned } from "./tuned.js";
9
10
  function scanTimeoutMs() {
10
11
  return envInt("OLLAMA_TIMEOUT_MS", 2500);
@@ -76,48 +77,53 @@ async function scanHost(host) {
76
77
  return { host, ok: false, models: [], error, url };
77
78
  }
78
79
  }
79
- function printResults(results, networkLabel, sources) {
80
- const found = results.filter((r) => r.ok);
81
- const offline = results.filter((r) => r.error === "offline").length;
82
- const probed = results.length - offline;
83
- console.log(`Network: ${networkLabel}`);
84
- console.log(`Discovery: ${sources.join(", ") || "none"}`);
85
- console.log(`Probing Ollama on ${results.length} host(s) (${probed} online/reachable candidates)\n`);
86
- 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) {
87
93
  console.log("No Ollama servers found.");
88
94
  console.log("Tip: add hosts to config.json, set OLLANET_HOSTS, pass --lan, or prompt an IP directly.");
89
- const interesting = results.filter((r) => r.error && r.error !== "offline");
95
+ const interesting = (payload.failures ?? []).filter((f) => f.error !== "offline");
90
96
  if (interesting.length > 0) {
91
97
  console.log("\nNon-timeout failures (first few):");
92
- for (const r of interesting.slice(0, 5)) {
93
- 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}`);
94
100
  }
95
101
  }
96
102
  return;
97
103
  }
98
- for (const result of found) {
99
- const label = result.host.dnsName || result.host.hostname;
100
- 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]" : "";
101
107
  console.log(`${label}${selfTag}`);
102
- console.log(` IP: ${result.host.ip} source: ${result.host.source} OS: ${result.host.os}`);
103
- console.log(` Endpoint: http://${result.host.ip}:${result.host.port}`);
104
- 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) {
105
111
  console.log(" Models: (none)");
106
112
  }
107
113
  else {
108
- console.log(` Models (${result.models.length}):`);
109
- for (const model of result.models) {
114
+ console.log(` Models (${server.models.length}):`);
115
+ for (const model of server.models) {
110
116
  const size = formatBytes(model.size);
111
- const params = model.details?.parameter_size;
112
- const quant = model.details?.quantization_level;
117
+ const params = model.parameter_size;
118
+ const quant = model.quantization_level;
113
119
  const meta = [params, quant, size].filter(Boolean).join(" · ");
114
- const tag = looksTuned(model.name) ? " [tuned]" : "";
120
+ const tag = model.tuned ? " [tuned]" : "";
115
121
  console.log(meta ? ` - ${model.name} (${meta})${tag}` : ` - ${model.name}${tag}`);
116
122
  }
117
123
  }
118
124
  console.log("");
119
125
  }
120
- 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).`);
121
127
  }
122
128
  function toPayload(results, networkLabel, sources) {
123
129
  return {
@@ -146,6 +152,13 @@ function toPayload(results, networkLabel, sources) {
146
152
  tuned: looksTuned(m.name),
147
153
  })),
148
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
+ })),
149
162
  };
150
163
  }
151
164
  /** Programmatic scan used by CLI `--json`, MCP, and `import { scanNetwork } from "ollanet"`. */
@@ -161,31 +174,49 @@ export async function scanNetwork(options = {}) {
161
174
  throw new Error("No hosts to scan. Add config.hosts, set OLLANET_HOSTS, or enable Tailscale/LAN discovery.");
162
175
  }
163
176
  const results = await mapPool(hosts, scanConcurrency(), scanHost);
164
- 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;
165
186
  }
166
187
  export async function main() {
167
188
  const includeOffline = process.argv.includes("--all");
168
189
  const jsonOut = process.argv.includes("--json");
169
190
  const lanScan = process.argv.includes("--lan");
170
- const config = await loadConfig();
171
- const { hosts, sources, networkLabel } = await discoverHosts({
172
- hosts: config.hosts,
173
- discovery: config.discovery,
174
- includeOffline,
175
- lanScan,
176
- });
177
- if (hosts.length === 0) {
178
- console.error("No hosts to scan. Add config.hosts, set OLLANET_HOSTS, or enable Tailscale/LAN discovery.");
179
- 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);
180
204
  return;
181
205
  }
182
- const results = await mapPool(hosts, scanConcurrency(), scanHost);
183
- if (jsonOut) {
184
- console.log(JSON.stringify(toPayload(results, networkLabel, sources), null, 2));
185
- 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
+ }
186
217
  }
187
- printResults(results, networkLabel, sources);
188
- if (!results.some((r) => r.ok)) {
189
- process.exitCode = 2;
218
+ catch (err) {
219
+ console.error(err instanceof Error ? err.message : err);
220
+ process.exitCode = 1;
190
221
  }
191
222
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ollanet",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Discover and talk to Ollama on any reachable network — CLI, MCP, and a Node library",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,6 +29,7 @@
29
29
  "ollanet": "tsx src/cli.ts",
30
30
  "scan": "tsx src/cli.ts scan",
31
31
  "prompt": "tsx src/cli.ts prompt",
32
+ "compare": "tsx src/cli.ts compare",
32
33
  "pull": "tsx src/cli.ts pull",
33
34
  "show": "tsx src/cli.ts show",
34
35
  "rm": "tsx src/cli.ts rm",