ollanet 0.5.0 → 0.6.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/dist/prompt.js CHANGED
@@ -14,8 +14,11 @@
14
14
  import { stdin as stdinStream } from "node:process";
15
15
  import { appendMessage, cleanTopic, createChat, loadChat, saveChat, toApiMessages, topicFromPrompt, } from "./chat-store.js";
16
16
  import { configPath, defaultModelForHost, loadConfig, machineSettingsForHost, mergeSettings, parseFormat, settingsFromEnv, settingsSummary, configFromPartial, } from "./config.js";
17
- import { discoverHosts, envInt, findDiscoveredHost, ollamaBaseUrl, resolveHost, shortName, } from "./hosts.js";
17
+ import { envInt, findDiscoveredHost, ollamaBaseUrl, resolveHost, shortName, } from "./hosts.js";
18
18
  import { ollamaChat } from "./ollama-chat.js";
19
+ import { assemblePrompt, readPromptFile } from "./prompt-input.js";
20
+ import { consumeSettingsFlag, takeFlag, takeValue } from "./argv.js";
21
+ import { listTargets, resolveTarget } from "./target.js";
19
22
  /** Prompt/chat HTTP timeout (ms). 0 = no timeout. Separate from scan's OLLAMA_TIMEOUT_MS. */
20
23
  function promptTimeoutMs() {
21
24
  return envInt("OLLAMA_PROMPT_TIMEOUT_MS", 600_000);
@@ -30,6 +33,7 @@ Examples:
30
33
  ollanet prompt localhost "Explain MagicDNS"
31
34
  ollanet prompt --chat a1b2c3d4e5f6 "Give an example"
32
35
  ollanet prompt localhost --temperature 0.2 --num-predict 64 "2+2?"
36
+ ollanet prompt localhost --file ./notes.md
33
37
  ollanet chats
34
38
 
35
39
  Each reply is saved under responses/<hash>.json and the hash is printed so you
@@ -51,6 +55,7 @@ Options:
51
55
  --format <json|schema> Force JSON mode or a JSON schema string
52
56
  --think Enable model thinking (qwen3 etc.); streams to stderr
53
57
  --no-think Disable thinking (default) so tokens go to the reply
58
+ --file <path> Prompt from a .txt or .md file (joins argv / stdin)
54
59
  --no-stream Wait for the full response
55
60
  --json Emit final response JSON (implies --no-stream)`);
56
61
  process.exit(1);
@@ -116,27 +121,12 @@ async function maybePeelModel(host, promptParts, fromStdin) {
116
121
  }
117
122
  return { promptParts };
118
123
  }
119
- function takeValue(args, flag) {
120
- const value = args.shift();
121
- if (!value) {
122
- console.error(`${flag} requires a value`);
123
- usage();
124
- }
125
- return value;
126
- }
127
- function parseNumberFlag(raw, flag) {
128
- const n = Number(raw);
129
- if (!Number.isFinite(n)) {
130
- console.error(`${flag} must be a number (got "${raw}")`);
131
- usage();
132
- }
133
- return n;
134
- }
135
124
  function parseArgs(argv) {
136
125
  const args = [...argv];
137
126
  let machineFlag;
138
127
  let modelFlag;
139
128
  let chatId;
129
+ let file;
140
130
  let stream = true;
141
131
  let json = false;
142
132
  let save = true;
@@ -161,55 +151,34 @@ function parseArgs(argv) {
161
151
  save = false;
162
152
  continue;
163
153
  }
164
- if (arg === "--think") {
165
- settings.think = true;
166
- continue;
167
- }
168
- if (arg === "--no-think") {
169
- settings.think = false;
170
- continue;
171
- }
172
- if (arg === "--chat" || arg.startsWith("--chat=")) {
173
- chatId = arg.includes("=") ? arg.slice("--chat=".length) : takeValue(args, "--chat");
174
- continue;
175
- }
176
- if (arg === "--machine" || arg.startsWith("--machine=")) {
177
- machineFlag = arg.includes("=") ? arg.slice("--machine=".length) : takeValue(args, "--machine");
154
+ if (consumeSettingsFlag(arg, args, settings, usage, {
155
+ temperature: true,
156
+ numPredict: true,
157
+ system: true,
158
+ })) {
178
159
  continue;
179
160
  }
180
- if (arg === "--model" || arg.startsWith("--model=")) {
181
- modelFlag = arg.includes("=") ? arg.slice("--model=".length) : takeValue(args, "--model");
161
+ const chat = takeFlag(arg, "--chat", args, usage);
162
+ if (chat !== undefined) {
163
+ chatId = chat;
182
164
  continue;
183
165
  }
184
- if (arg === "--system" || arg.startsWith("--system=")) {
185
- settings.system = arg.includes("=")
186
- ? arg.slice("--system=".length)
187
- : takeValue(args, "--system");
166
+ const machine = takeFlag(arg, "--machine", args, usage);
167
+ if (machine !== undefined) {
168
+ machineFlag = machine;
188
169
  continue;
189
170
  }
190
- if (arg === "--temperature" || arg === "-t" || arg.startsWith("--temperature=")) {
191
- const raw = arg.includes("=") ? arg.slice("--temperature=".length) : takeValue(args, "--temperature");
192
- settings.temperature = parseNumberFlag(raw, "--temperature");
171
+ const model = takeFlag(arg, "--model", args, usage);
172
+ if (model !== undefined) {
173
+ modelFlag = model;
193
174
  continue;
194
175
  }
195
- if (arg === "--num-predict" || arg.startsWith("--num-predict=")) {
196
- const raw = arg.includes("=") ? arg.slice("--num-predict=".length) : takeValue(args, "--num-predict");
197
- settings.num_predict = Math.trunc(parseNumberFlag(raw, "--num-predict"));
198
- continue;
199
- }
200
- if (arg === "--num-ctx" || arg.startsWith("--num-ctx=")) {
201
- const raw = arg.includes("=") ? arg.slice("--num-ctx=".length) : takeValue(args, "--num-ctx");
202
- settings.num_ctx = Math.trunc(parseNumberFlag(raw, "--num-ctx"));
203
- continue;
204
- }
205
- if (arg === "--keep-alive" || arg.startsWith("--keep-alive=")) {
206
- const raw = arg.includes("=") ? arg.slice("--keep-alive=".length) : takeValue(args, "--keep-alive");
207
- const asNum = Number(raw);
208
- settings.keep_alive = raw.trim() !== "" && Number.isFinite(asNum) && String(asNum) === raw ? asNum : raw;
176
+ if (arg === "-f" || arg === "--file" || arg.startsWith("--file=")) {
177
+ file = arg === "-f" ? takeValue(args, "--file", usage) : takeFlag(arg, "--file", args, usage);
209
178
  continue;
210
179
  }
211
180
  if (arg === "--format" || arg.startsWith("--format=")) {
212
- const raw = arg.includes("=") ? arg.slice("--format=".length) : takeValue(args, "--format");
181
+ const raw = takeFlag(arg, "--format", args, usage) ?? "";
213
182
  const format = parseFormat(raw);
214
183
  if (format !== undefined)
215
184
  settings.format = format;
@@ -235,7 +204,7 @@ function parseArgs(argv) {
235
204
  }
236
205
  // Continue (--chat): all positionals are the prompt. Optional machine override
237
206
  // via --machine, or a leading discovered-host name peeled in main().
238
- return { machine, model, promptParts, stream, json, save, chatId, settings };
207
+ return { machine, model, promptParts, file, stream, json, save, chatId, settings };
239
208
  }
240
209
  async function readStdinIfPiped() {
241
210
  if (stdinStream.isTTY)
@@ -261,7 +230,7 @@ async function readStdinIfPiped() {
261
230
  const onEnd = () => done(true);
262
231
  stdinStream.once("readable", onReadable);
263
232
  stdinStream.once("end", onEnd);
264
- const timer = setTimeout(() => done(stdinStream.readableLength > 0), 25);
233
+ const timer = setTimeout(() => done(stdinStream.readableLength > 0), 100);
265
234
  });
266
235
  if (!hasData)
267
236
  return "";
@@ -317,10 +286,6 @@ export async function runPrompt(opts) {
317
286
  const writeStdout = opts.writeStdout !== false;
318
287
  let promptParts = opts.prompt ? [opts.prompt] : [];
319
288
  const config = opts.config ? configFromPartial(opts.config) : await loadConfig();
320
- const { hosts: targets } = await discoverHosts({
321
- hosts: config.hosts,
322
- discovery: config.discovery,
323
- });
324
289
  let chat;
325
290
  let isNewChat = false;
326
291
  let machineQuery = opts.machine;
@@ -331,10 +296,7 @@ export async function runPrompt(opts) {
331
296
  if (!machineQuery) {
332
297
  throw new Error("Machine is required for a new chat (or pass chatId).");
333
298
  }
334
- const host = resolveHost(targets, machineQuery);
335
- if (!host.online && !host.isSelf && host.source === "tailscale") {
336
- throw new Error(`Machine "${shortName(host)}" appears offline.`);
337
- }
299
+ const host = await resolveTarget(machineQuery, config);
338
300
  let peeledModel;
339
301
  if (!opts.chatId && !opts.model) {
340
302
  const peeled = await maybePeelModel(host, promptParts, "");
@@ -436,11 +398,19 @@ export async function main() {
436
398
  const { stream, json, save } = parsed;
437
399
  let promptParts = parsed.promptParts;
438
400
  const fromStdin = await readStdinIfPiped();
401
+ let fromFile = "";
402
+ if (parsed.file) {
403
+ try {
404
+ fromFile = await readPromptFile(parsed.file);
405
+ }
406
+ catch (err) {
407
+ console.error(err instanceof Error ? err.message : err);
408
+ process.exitCode = 1;
409
+ return;
410
+ }
411
+ }
439
412
  const config = await loadConfig();
440
- const { hosts: targets } = await discoverHosts({
441
- hosts: config.hosts,
442
- discovery: config.discovery,
443
- });
413
+ const targets = await listTargets(config);
444
414
  let machineQuery = parsed.machine;
445
415
  if (parsed.chatId) {
446
416
  // Allow `ollanet prompt <host> --chat HASH "follow-up"` only when the
@@ -460,13 +430,17 @@ export async function main() {
460
430
  usage();
461
431
  }
462
432
  const host = resolveHost(targets, machineQuery);
463
- const peeled = await maybePeelModel(host, promptParts, fromStdin);
433
+ const peeled = await maybePeelModel(host, promptParts, fromStdin || fromFile);
464
434
  peeledModel = peeled.model;
465
435
  promptParts = peeled.promptParts;
466
436
  }
467
- const promptText = [promptParts.join(" ").trim(), fromStdin].filter(Boolean).join("\n\n").trim();
437
+ const promptText = assemblePrompt({
438
+ argv: promptParts.join(" "),
439
+ file: fromFile,
440
+ stdin: fromStdin,
441
+ });
468
442
  if (!promptText) {
469
- console.error("No prompt provided (pass args and/or pipe stdin).");
443
+ console.error("No prompt provided (pass args, --file, and/or pipe stdin).");
470
444
  usage();
471
445
  }
472
446
  try {
package/dist/ps.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import { configFromPartial, loadConfig } from "./config.js";
8
8
  import { discoverHosts, envInt, ollamaBaseUrl, shortName, } from "./hosts.js";
9
9
  import { ollamaPs, ollamaPsRequired } from "./ollama-chat.js";
10
+ import { takeFlag } from "./argv.js";
10
11
  import { resolveTarget } from "./target.js";
11
12
  import { looksTuned } from "./tuned.js";
12
13
  function psTimeoutMs() {
@@ -29,14 +30,6 @@ Options:
29
30
  --json Emit result JSON on stdout`);
30
31
  process.exit(1);
31
32
  }
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
33
  function parseArgs(argv) {
41
34
  const args = [...argv];
42
35
  let machineFlag;
@@ -52,8 +45,9 @@ function parseArgs(argv) {
52
45
  json = true;
53
46
  continue;
54
47
  }
55
- if (arg === "--machine") {
56
- machineFlag = takeValue(args, "--machine");
48
+ const machine = takeFlag(arg, "--machine", args, usage);
49
+ if (machine !== undefined) {
50
+ machineFlag = machine;
57
51
  continue;
58
52
  }
59
53
  if (arg.startsWith("-")) {
package/dist/pull.js CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
10
10
  import { ollamaPull } from "./ollama-chat.js";
11
+ import { takeFlag } from "./argv.js";
11
12
  import { resolveTarget } from "./target.js";
12
13
  /** Pull HTTP timeout (ms). 0 = none. Large models can take hours. */
13
14
  function pullTimeoutMs() {
@@ -35,14 +36,6 @@ Options:
35
36
  --json Emit result JSON on stdout`);
36
37
  process.exit(1);
37
38
  }
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
39
  function parseArgs(argv) {
47
40
  const args = [...argv];
48
41
  let machineFlag;
@@ -69,12 +62,14 @@ function parseArgs(argv) {
69
62
  json = true;
70
63
  continue;
71
64
  }
72
- if (arg === "--machine") {
73
- machineFlag = takeValue(args, "--machine");
65
+ const machine = takeFlag(arg, "--machine", args, usage);
66
+ if (machine !== undefined) {
67
+ machineFlag = machine;
74
68
  continue;
75
69
  }
76
- if (arg === "--model") {
77
- modelFlag = takeValue(args, "--model");
70
+ const model = takeFlag(arg, "--model", args, usage);
71
+ if (model !== undefined) {
72
+ modelFlag = model;
78
73
  continue;
79
74
  }
80
75
  if (arg.startsWith("-")) {
package/dist/rm.js CHANGED
@@ -6,6 +6,7 @@
6
6
  import { createInterface } from "node:readline";
7
7
  import { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
8
8
  import { ollamaDelete } from "./ollama-chat.js";
9
+ import { takeFlag } from "./argv.js";
9
10
  import { resolveTarget } from "./target.js";
10
11
  function rmTimeoutMs() {
11
12
  return envInt("OLLAMA_RM_TIMEOUT_MS", 30_000);
@@ -29,14 +30,6 @@ Options:
29
30
  --json Emit result JSON on stdout`);
30
31
  process.exit(1);
31
32
  }
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
33
  function parseArgs(argv) {
41
34
  const args = [...argv];
42
35
  let machineFlag;
@@ -58,12 +51,14 @@ function parseArgs(argv) {
58
51
  json = true;
59
52
  continue;
60
53
  }
61
- if (arg === "--machine") {
62
- machineFlag = takeValue(args, "--machine");
54
+ const machine = takeFlag(arg, "--machine", args, usage);
55
+ if (machine !== undefined) {
56
+ machineFlag = machine;
63
57
  continue;
64
58
  }
65
- if (arg === "--model") {
66
- modelFlag = takeValue(args, "--model");
59
+ const model = takeFlag(arg, "--model", args, usage);
60
+ if (model !== undefined) {
61
+ modelFlag = model;
67
62
  continue;
68
63
  }
69
64
  if (arg.startsWith("-")) {
@@ -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/dist/show.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
7
7
  import { ollamaShow } from "./ollama-chat.js";
8
+ import { takeFlag } from "./argv.js";
8
9
  import { resolveTarget } from "./target.js";
9
10
  import { looksTuned } from "./tuned.js";
10
11
  function showTimeoutMs() {
@@ -28,14 +29,6 @@ Options:
28
29
  --json Emit result JSON on stdout`);
29
30
  process.exit(1);
30
31
  }
31
- function takeValue(args, flag) {
32
- const value = args.shift();
33
- if (!value) {
34
- console.error(`${flag} requires a value`);
35
- usage();
36
- }
37
- return value;
38
- }
39
32
  function parseArgs(argv) {
40
33
  const args = [...argv];
41
34
  let machineFlag;
@@ -52,12 +45,14 @@ function parseArgs(argv) {
52
45
  json = true;
53
46
  continue;
54
47
  }
55
- if (arg === "--machine") {
56
- machineFlag = takeValue(args, "--machine");
48
+ const machine = takeFlag(arg, "--machine", args, usage);
49
+ if (machine !== undefined) {
50
+ machineFlag = machine;
57
51
  continue;
58
52
  }
59
- if (arg === "--model") {
60
- modelFlag = takeValue(args, "--model");
53
+ const model = takeFlag(arg, "--model", args, usage);
54
+ if (model !== undefined) {
55
+ modelFlag = model;
61
56
  continue;
62
57
  }
63
58
  if (arg.startsWith("-")) {
package/dist/target.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { type AppConfig } from "./config.js";
2
2
  import { type HostTarget } from "./hosts.js";
3
+ /** Discover hosts the same way every command does (config + Tailscale + …). */
4
+ export declare function listTargets(config?: Partial<AppConfig>): Promise<HostTarget[]>;
3
5
  /** Resolve a machine name the same way pull / prompt / bench do. */
4
6
  export declare function resolveTarget(machine: string, config?: Partial<AppConfig>): Promise<HostTarget>;