ollanet 0.6.0 → 0.6.2

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
@@ -176,7 +176,7 @@ await scanNetwork({
176
176
 
177
177
  `pullModel({ machine, model })` asks that host to download (or update) a library model. The bits never transit ollanet. `showModel`, `removeModel({ yes: true })`, and `listLoaded` are the inspect / delete / VRAM trio.
178
178
 
179
- Requires **ollanet ≥ 0.4.0** (library). **Model management (`pull` / `show` / `rm` / `ps`) needs ≥ 0.5.0.** **Compare, `lastScan`, and bench `--hot` need ≥ 0.6.0.** Bundlers (Vite / SvelteKit SSR):
179
+ Requires **ollanet ≥ 0.4.0** (library). **Model management (`pull` / `show` / `rm` / `ps`) needs ≥ 0.5.0.** **Compare, `lastScan`, and bench `--hot` need ≥ 0.6.0.** **`resolveTarget` / `listTargets` need ≥ 0.6.1.** Bundlers (Vite / SvelteKit SSR):
180
180
 
181
181
  ```ts
182
182
  // vite.config.ts
@@ -198,6 +198,7 @@ optimizeDeps: { exclude: ["ollanet"] },
198
198
  | `ollanet chats` | List or inspect saved transcripts |
199
199
  | `ollanet bench …` | Benchmark models for tok/s + lightweight quality checks |
200
200
  | `ollanet mcp` | Stdio [MCP](https://modelcontextprotocol.io) server for agents |
201
+ | `ollanet help [command]` | This overview, or one command’s flags (`ollanet help bench`, `ollanet bench help`) |
201
202
 
202
203
  ### `scan` options
203
204
 
package/dist/argv.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Shared CLI flag helpers. Commands keep their own usage() and unique flags.
3
+ */
4
+ import type { GenerateSettings } from "./config.js";
5
+ export type UsageFn = () => never;
6
+ export declare function isHelpFlag(arg: string): boolean;
7
+ /** Intentional help: stdout, exit 0. */
8
+ export declare function printHelp(text: string): never;
9
+ /** Usage error: stderr, exit 1. */
10
+ export declare function failUsage(text: string): never;
11
+ export declare function takeValue(args: string[], flag: string, usage: UsageFn): string;
12
+ /** `--name value` or `--name=value`. Returns undefined if `arg` is not this flag. */
13
+ export declare function takeFlag(arg: string, name: string, args: string[], usage: UsageFn): string | undefined;
14
+ export declare function parseNumberFlag(raw: string, flag: string, usage: UsageFn): number;
15
+ export declare function parseKeepAlive(raw: string): string | number;
16
+ export interface SettingsFlagOpts {
17
+ temperature?: boolean;
18
+ numPredict?: boolean;
19
+ system?: boolean;
20
+ }
21
+ /** Shared generate-settings flags. Returns true when `arg` was consumed. */
22
+ export declare function consumeSettingsFlag(arg: string, args: string[], settings: GenerateSettings, usage: UsageFn, opts?: SettingsFlagOpts): boolean;
package/dist/argv.js ADDED
@@ -0,0 +1,86 @@
1
+ export function isHelpFlag(arg) {
2
+ return arg === "--help" || arg === "-h";
3
+ }
4
+ /** Intentional help: stdout, exit 0. */
5
+ export function printHelp(text) {
6
+ process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
7
+ process.exit(0);
8
+ }
9
+ /** Usage error: stderr, exit 1. */
10
+ export function failUsage(text) {
11
+ console.error(text);
12
+ process.exit(1);
13
+ }
14
+ export function takeValue(args, flag, usage) {
15
+ const value = args.shift();
16
+ if (!value) {
17
+ console.error(`${flag} requires a value`);
18
+ usage();
19
+ }
20
+ return value;
21
+ }
22
+ /** `--name value` or `--name=value`. Returns undefined if `arg` is not this flag. */
23
+ export function takeFlag(arg, name, args, usage) {
24
+ if (arg === name)
25
+ return takeValue(args, name, usage);
26
+ if (arg.startsWith(`${name}=`))
27
+ return arg.slice(name.length + 1);
28
+ return undefined;
29
+ }
30
+ export function parseNumberFlag(raw, flag, usage) {
31
+ const n = Number(raw);
32
+ if (!Number.isFinite(n)) {
33
+ console.error(`${flag} must be a number (got "${raw}")`);
34
+ usage();
35
+ }
36
+ return n;
37
+ }
38
+ export function parseKeepAlive(raw) {
39
+ const asNum = Number(raw);
40
+ return raw.trim() !== "" && Number.isFinite(asNum) && String(asNum) === raw ? asNum : raw;
41
+ }
42
+ /** Shared generate-settings flags. Returns true when `arg` was consumed. */
43
+ export function consumeSettingsFlag(arg, args, settings, usage, opts = {}) {
44
+ if (arg === "--think") {
45
+ settings.think = true;
46
+ return true;
47
+ }
48
+ if (arg === "--no-think") {
49
+ settings.think = false;
50
+ return true;
51
+ }
52
+ if (opts.system) {
53
+ const system = takeFlag(arg, "--system", args, usage);
54
+ if (system !== undefined) {
55
+ settings.system = system;
56
+ return true;
57
+ }
58
+ }
59
+ if (opts.temperature) {
60
+ if (arg === "-t" || arg === "--temperature" || arg.startsWith("--temperature=")) {
61
+ const raw = arg === "-t" || arg === "--temperature"
62
+ ? takeValue(args, "--temperature", usage)
63
+ : arg.slice("--temperature=".length);
64
+ settings.temperature = parseNumberFlag(raw, "--temperature", usage);
65
+ return true;
66
+ }
67
+ }
68
+ if (opts.numPredict) {
69
+ const raw = takeFlag(arg, "--num-predict", args, usage);
70
+ if (raw !== undefined) {
71
+ settings.num_predict = Math.trunc(parseNumberFlag(raw, "--num-predict", usage));
72
+ return true;
73
+ }
74
+ }
75
+ const numCtx = takeFlag(arg, "--num-ctx", args, usage);
76
+ if (numCtx !== undefined) {
77
+ settings.num_ctx = Math.trunc(parseNumberFlag(numCtx, "--num-ctx", usage));
78
+ return true;
79
+ }
80
+ const keepAlive = takeFlag(arg, "--keep-alive", args, usage);
81
+ if (keepAlive !== undefined) {
82
+ settings.keep_alive = parseKeepAlive(keepAlive);
83
+ return true;
84
+ }
85
+ return false;
86
+ }
package/dist/bench.d.ts CHANGED
@@ -1 +1,2 @@
1
+ export declare function helpText(): string;
1
2
  export declare function main(): Promise<void>;
package/dist/bench.js CHANGED
@@ -5,57 +5,51 @@ import { defaultModelForHost, loadConfig, machineSettingsForHost, mergeSettings,
5
5
  import { comparabilityKey, getSuiteCases, median, suiteRevision, } from "./bench-suite.js";
6
6
  import { benchmarksDir, newBenchId, saveBenchmark } from "./bench-store.js";
7
7
  import { readFile } from "node:fs/promises";
8
- import { discoverHosts, envInt, ollamaBaseUrl, resolveHost, shortName, } from "./hosts.js";
8
+ import { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
9
+ import { consumeSettingsFlag, failUsage, isHelpFlag, parseNumberFlag, printHelp, takeFlag } from "./argv.js";
10
+ import { resolveTarget } from "./target.js";
9
11
  import { projectPath } from "./paths.js";
10
12
  import { contextLengthForModel, isCompletionCapable, isVisionCapable, ollamaChat, ollamaShow, ollamaTags, ollamaUnload, ollamaVersion, ollamaPs, psHasModel, shouldWarnNoThinking, vramForModel, waitUntilUnloaded, } from "./ollama-chat.js";
11
13
  const MODEL_COL = 34;
12
14
  const BENCH_TIMEOUT_MS = envInt("OLLAMA_BENCH_TIMEOUT_MS", 60_000);
13
15
  const UNLOAD_WAIT_MS = envInt("OLLAMA_BENCH_UNLOAD_WAIT_MS", 60_000);
14
- function usage() {
15
- console.error(`Usage:
16
+ export function helpText() {
17
+ return `Usage:
16
18
  ollanet bench <machine|ip> [model...] [options]
17
19
  ollanet bench <machine|ip> --all [options]
18
20
 
19
21
  Examples:
20
22
  ollanet bench studio gemma3:12b --runs 5
21
23
  ollanet bench studio gemma3:12b --hot --runs 5
24
+ ollanet help bench
25
+
26
+ Fixed suite (not compare). Checks run once; throughput repeats --runs times.
27
+ Median tok/s excludes early-stops. Unload is between models, not between repeats.
22
28
 
23
29
  Options:
24
30
  --all Every completion-capable model from /api/tags
25
31
  --exclude-vision With --all, skip models that advertise vision
26
32
  --suite quick|full Prompt suite (default quick)
27
- --runs <n> Throughput repeats (default 3)
33
+ --runs <n> Counted throughput repeats (default 3)
28
34
  --hot Discard first throughput run; keep models loaded
29
- --warmup / --no-warmup
35
+ --warmup Short discarded call before timed cases (default on)
36
+ --no-warmup Skip the short warmup (--hot implies this)
30
37
  --cold-load Measure cold load via unload + /api/ps
31
38
  --num-predict <n> Pin throughput length (default 256)
32
- --num-ctx <n>
33
- --keep-alive <value>
34
- --think / --no-think
35
- --judge --judge-model <name>
39
+ --num-ctx <n> Context window override
40
+ --keep-alive <value> Keep model loaded (e.g. 5m, 0, -1)
41
+ --think / --no-think Thinking tokens (default off)
42
+ --judge Second pass: score check answers 1–5
43
+ --judge-model <name> Required with --judge (no default)
36
44
  --json Machine-readable stdout
37
- --save / --no-save
38
- --fail-fast
45
+ --save / --no-save Persist under benchmarks/ (default on)
46
+ --fail-fast Stop remaining models after the first hard error
39
47
  --fail-on-error Exit 2 if any model/case errored
40
48
 
41
- See docs/bench-spec.md for measurement rules.`);
42
- process.exit(1);
43
- }
44
- function takeValue(args, flag) {
45
- const value = args.shift();
46
- if (!value) {
47
- console.error(`${flag} requires a value`);
48
- usage();
49
- }
50
- return value;
49
+ See docs/bench-spec.md for measurement rules.`;
51
50
  }
52
- function parseNumberFlag(raw, flag) {
53
- const n = Number(raw);
54
- if (!Number.isFinite(n)) {
55
- console.error(`${flag} must be a number (got "${raw}")`);
56
- usage();
57
- }
58
- return n;
51
+ function usage() {
52
+ failUsage(helpText());
59
53
  }
60
54
  function isLoopback(host) {
61
55
  return (host.source === "localhost" ||
@@ -104,7 +98,9 @@ function parseArgs(argv) {
104
98
  const positional = [];
105
99
  while (args.length > 0) {
106
100
  const arg = args.shift();
107
- if (arg === "--" || arg === "--help" || arg === "-h")
101
+ if (isHelpFlag(arg))
102
+ printHelp(helpText());
103
+ if (arg === "--")
108
104
  usage();
109
105
  if (arg === "--all") {
110
106
  all = true;
@@ -157,57 +153,35 @@ function parseArgs(argv) {
157
153
  failOnError = true;
158
154
  continue;
159
155
  }
160
- if (arg === "--think") {
161
- settings.think = true;
162
- continue;
163
- }
164
- if (arg === "--no-think") {
165
- settings.think = false;
166
- continue;
167
- }
168
156
  if (arg === "--judge") {
169
157
  judge = true;
170
158
  continue;
171
159
  }
172
- if (arg === "--suite" || arg.startsWith("--suite=")) {
173
- const raw = arg.includes("=") ? arg.slice("--suite=".length) : takeValue(args, "--suite");
174
- if (raw !== "quick" && raw !== "full") {
175
- console.error(`Unknown suite "${raw}" (use quick|full)`);
176
- usage();
177
- }
178
- suite = raw;
160
+ if (consumeSettingsFlag(arg, args, settings, usage)) {
179
161
  continue;
180
162
  }
181
- if (arg === "--runs" || arg.startsWith("--runs=")) {
182
- const raw = arg.includes("=") ? arg.slice("--runs=".length) : takeValue(args, "--runs");
183
- runs = Math.max(1, Math.trunc(parseNumberFlag(raw, "--runs")));
184
- continue;
185
- }
186
- if (arg === "--num-predict" || arg.startsWith("--num-predict=")) {
187
- const raw = arg.includes("=")
188
- ? arg.slice("--num-predict=".length)
189
- : takeValue(args, "--num-predict");
190
- throughputNumPredict = Math.trunc(parseNumberFlag(raw, "--num-predict"));
163
+ const suiteRaw = takeFlag(arg, "--suite", args, usage);
164
+ if (suiteRaw !== undefined) {
165
+ if (suiteRaw !== "quick" && suiteRaw !== "full") {
166
+ console.error(`Unknown suite "${suiteRaw}" (use quick|full)`);
167
+ usage();
168
+ }
169
+ suite = suiteRaw;
191
170
  continue;
192
171
  }
193
- if (arg === "--num-ctx" || arg.startsWith("--num-ctx=")) {
194
- const raw = arg.includes("=") ? arg.slice("--num-ctx=".length) : takeValue(args, "--num-ctx");
195
- settings.num_ctx = Math.trunc(parseNumberFlag(raw, "--num-ctx"));
172
+ const runsRaw = takeFlag(arg, "--runs", args, usage);
173
+ if (runsRaw !== undefined) {
174
+ runs = Math.max(1, Math.trunc(parseNumberFlag(runsRaw, "--runs", usage)));
196
175
  continue;
197
176
  }
198
- if (arg === "--keep-alive" || arg.startsWith("--keep-alive=")) {
199
- const raw = arg.includes("=")
200
- ? arg.slice("--keep-alive=".length)
201
- : takeValue(args, "--keep-alive");
202
- const asNum = Number(raw);
203
- settings.keep_alive =
204
- raw.trim() !== "" && Number.isFinite(asNum) && String(asNum) === raw ? asNum : raw;
177
+ const predictRaw = takeFlag(arg, "--num-predict", args, usage);
178
+ if (predictRaw !== undefined) {
179
+ throughputNumPredict = Math.trunc(parseNumberFlag(predictRaw, "--num-predict", usage));
205
180
  continue;
206
181
  }
207
- if (arg === "--judge-model" || arg.startsWith("--judge-model=")) {
208
- judgeModel = arg.includes("=")
209
- ? arg.slice("--judge-model=".length)
210
- : takeValue(args, "--judge-model");
182
+ const judgeRaw = takeFlag(arg, "--judge-model", args, usage);
183
+ if (judgeRaw !== undefined) {
184
+ judgeModel = judgeRaw;
211
185
  continue;
212
186
  }
213
187
  if (arg.startsWith("-")) {
@@ -527,11 +501,7 @@ function pad(s, n) {
527
501
  export async function main() {
528
502
  const parsed = parseArgs(process.argv.slice(2));
529
503
  const config = await loadConfig();
530
- const { hosts: targets } = await discoverHosts({
531
- hosts: config.hosts,
532
- discovery: config.discovery,
533
- });
534
- const host = resolveHost(targets, parsed.machine);
504
+ const host = await resolveTarget(parsed.machine, config);
535
505
  const baseUrl = ollamaBaseUrl(host);
536
506
  const machineLabel = shortName(host);
537
507
  const tags = await ollamaTags(baseUrl, Math.min(BENCH_TIMEOUT_MS, 15_000));
package/dist/chats.d.ts CHANGED
@@ -6,4 +6,5 @@
6
6
  * ollanet chats --json
7
7
  * ollanet chats --id a1b2c3d4e5f6
8
8
  */
9
+ export declare function helpText(): string;
9
10
  export declare function main(): Promise<void>;
package/dist/chats.js CHANGED
@@ -7,15 +7,18 @@
7
7
  * ollanet chats --id a1b2c3d4e5f6
8
8
  */
9
9
  import { loadChat, listChats, responsesDir, } from "./chat-store.js";
10
- function usage() {
11
- console.error(`Usage: ollanet chats [--json] [--id <hash>]
10
+ import { failUsage, isHelpFlag, printHelp, takeFlag } from "./argv.js";
11
+ export function helpText() {
12
+ return `Usage: ollanet chats [--json] [--id <hash>]
12
13
 
13
14
  Lists transcripts in ${responsesDir()}.
14
15
 
15
16
  Options:
16
17
  --json Machine-readable output
17
- --id <hash> Show one chat (messages included with --json, preview otherwise)`);
18
- process.exit(1);
18
+ --id <hash> Show one chat (messages included with --json, preview otherwise)`;
19
+ }
20
+ function usage() {
21
+ failUsage(helpText());
19
22
  }
20
23
  function parseArgs(argv) {
21
24
  const args = [...argv];
@@ -25,18 +28,15 @@ function parseArgs(argv) {
25
28
  const arg = args.shift();
26
29
  if (arg === "--")
27
30
  continue;
28
- if (arg === "--help" || arg === "-h")
29
- usage();
31
+ if (isHelpFlag(arg))
32
+ printHelp(helpText());
30
33
  if (arg === "--json") {
31
34
  json = true;
32
35
  continue;
33
36
  }
34
- if (arg === "--id" || arg.startsWith("--id=")) {
35
- id = arg.includes("=") ? arg.slice("--id=".length) : args.shift();
36
- if (!id) {
37
- console.error("--id requires a value");
38
- usage();
39
- }
37
+ const idFlag = takeFlag(arg, "--id", args, usage);
38
+ if (idFlag !== undefined) {
39
+ id = idFlag;
40
40
  continue;
41
41
  }
42
42
  console.error(`Unknown flag: ${arg}`);
package/dist/cli.js CHANGED
@@ -28,6 +28,7 @@ Usage:
28
28
  ollanet chats [--json] [--id <hash>]
29
29
  ollanet bench <machine|ip> [model...] [--all] [options]
30
30
  ollanet mcp
31
+ ollanet help [command]
31
32
 
32
33
  Examples:
33
34
  ollanet scan
@@ -46,6 +47,7 @@ Examples:
46
47
  ollanet bench localhost --all
47
48
  ollanet bench localhost llama3.2:1b --runs 3
48
49
  ollanet bench localhost gemma3:12b --hot --runs 5
50
+ ollanet help bench # flags for one command (also: ollanet bench help)
49
51
  ollanet mcp # stdio MCP server for agents
50
52
 
51
53
  Config: ~/.ollanet/config.json when installed, ./config.json in a checkout
@@ -58,10 +60,36 @@ async function main() {
58
60
  // pnpm/npm often forward a bare `--`; ignore it.
59
61
  const argv = process.argv.slice(2).filter((arg) => arg !== "--");
60
62
  const cmd = argv[0];
61
- if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help") {
63
+ if (!cmd || cmd === "-h" || cmd === "--help") {
62
64
  process.stdout.write(HELP);
63
65
  return;
64
66
  }
67
+ if (cmd === "help") {
68
+ const topic = argv[1];
69
+ if (!topic || topic === "-h" || topic === "--help") {
70
+ process.stdout.write(HELP);
71
+ return;
72
+ }
73
+ const { commandHelp, commandNames } = await import("./help.js");
74
+ const text = await commandHelp(topic);
75
+ if (!text) {
76
+ console.error(`Unknown command: ${topic}`);
77
+ console.error(`Try: ollanet help (commands: ${commandNames().join(", ")})`);
78
+ process.exitCode = 1;
79
+ return;
80
+ }
81
+ process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
82
+ return;
83
+ }
84
+ const rest = argv.slice(1);
85
+ const { isHelpRequest, commandHelp } = await import("./help.js");
86
+ if (isHelpRequest(rest)) {
87
+ const text = await commandHelp(cmd);
88
+ if (text) {
89
+ process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
90
+ return;
91
+ }
92
+ }
65
93
  if (cmd === "-v" || cmd === "--version" || cmd === "version") {
66
94
  const { readFile } = await import("node:fs/promises");
67
95
  const { projectPath } = await import("./paths.js");
package/dist/compare.d.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  import { type AppConfig, type GenerateSettings } from "./config.js";
8
8
  /** Used when --prompt, --file, and stdin are all omitted. */
9
9
  export declare const DEFAULT_COMPARE_PROMPT: string;
10
+ export declare function helpText(): string;
10
11
  export interface CompareModelResult {
11
12
  model: string;
12
13
  content?: string;
package/dist/compare.js CHANGED
@@ -5,7 +5,9 @@
5
5
  * ollanet compare studio m1 m2 --file task.md
6
6
  */
7
7
  import { configFromPartial, loadConfig, machineSettingsForHost, mergeSettings, settingsFromEnv, } from "./config.js";
8
- import { discoverHosts, envInt, ollamaBaseUrl, resolveHost, shortName } from "./hosts.js";
8
+ import { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
9
+ import { consumeSettingsFlag, failUsage, isHelpFlag, printHelp, takeFlag, takeValue } from "./argv.js";
10
+ import { resolveTarget } from "./target.js";
9
11
  import { ollamaChat, ollamaTags, ollamaUnload, waitUntilUnloaded, } from "./ollama-chat.js";
10
12
  import { assemblePrompt, readPromptFile } from "./prompt-input.js";
11
13
  import { comparesDir, newCompareId, saveCompare } from "./compare-store.js";
@@ -21,8 +23,8 @@ function compareTimeoutMs() {
21
23
  function unloadWaitMs() {
22
24
  return envInt("OLLAMA_COMPARE_UNLOAD_WAIT_MS", 60_000);
23
25
  }
24
- function usage() {
25
- console.error(`Usage:
26
+ export function helpText() {
27
+ return `Usage:
26
28
  ollanet compare <machine> <model> <model> [model...]
27
29
  ollanet compare <machine> <model> <model> [model...] --prompt <text>
28
30
  ollanet compare <machine> <model> <model> [model...] --file <path.txt|.md>
@@ -47,24 +49,10 @@ Options:
47
49
  --think / --no-think
48
50
  --unload Unload each model before the next (fairer tok/s)
49
51
  --no-save Do not write compares/*
50
- --json Emit the full record on stdout`);
51
- process.exit(1);
52
- }
53
- function takeValue(args, flag) {
54
- const value = args.shift();
55
- if (!value) {
56
- console.error(`${flag} requires a value`);
57
- usage();
58
- }
59
- return value;
52
+ --json Emit the full record on stdout`;
60
53
  }
61
- function parseNumberFlag(raw, flag) {
62
- const n = Number(raw);
63
- if (!Number.isFinite(n)) {
64
- console.error(`${flag} must be a number (got "${raw}")`);
65
- usage();
66
- }
67
- return n;
54
+ function usage() {
55
+ failUsage(helpText());
68
56
  }
69
57
  function parseArgs(argv) {
70
58
  const args = [...argv];
@@ -79,8 +67,8 @@ function parseArgs(argv) {
79
67
  const arg = args.shift();
80
68
  if (arg === "--")
81
69
  continue;
82
- if (arg === "--help" || arg === "-h")
83
- usage();
70
+ if (isHelpFlag(arg))
71
+ printHelp(helpText());
84
72
  if (arg === "--json") {
85
73
  json = true;
86
74
  continue;
@@ -93,48 +81,19 @@ function parseArgs(argv) {
93
81
  unload = true;
94
82
  continue;
95
83
  }
96
- if (arg === "--think") {
97
- settings.think = true;
98
- continue;
99
- }
100
- if (arg === "--no-think") {
101
- settings.think = false;
102
- continue;
103
- }
104
- if (arg === "--prompt" || arg === "-p" || arg.startsWith("--prompt=")) {
105
- prompt = arg.includes("=") ? arg.slice("--prompt=".length) : takeValue(args, "--prompt");
106
- continue;
107
- }
108
- if (arg === "--file" || arg === "-f" || arg.startsWith("--file=")) {
109
- file = arg.includes("=") ? arg.slice("--file=".length) : takeValue(args, "--file");
84
+ if (consumeSettingsFlag(arg, args, settings, usage, {
85
+ temperature: true,
86
+ numPredict: true,
87
+ system: true,
88
+ })) {
110
89
  continue;
111
90
  }
112
- if (arg === "--system" || arg.startsWith("--system=")) {
113
- settings.system = arg.includes("=")
114
- ? arg.slice("--system=".length)
115
- : takeValue(args, "--system");
91
+ if (arg === "-p" || arg === "--prompt" || arg.startsWith("--prompt=")) {
92
+ prompt = arg === "-p" ? takeValue(args, "--prompt", usage) : takeFlag(arg, "--prompt", args, usage);
116
93
  continue;
117
94
  }
118
- if (arg === "--temperature" || arg === "-t" || arg.startsWith("--temperature=")) {
119
- const raw = arg.includes("=") ? arg.slice("--temperature=".length) : takeValue(args, "--temperature");
120
- settings.temperature = parseNumberFlag(raw, "--temperature");
121
- continue;
122
- }
123
- if (arg === "--num-predict" || arg.startsWith("--num-predict=")) {
124
- const raw = arg.includes("=") ? arg.slice("--num-predict=".length) : takeValue(args, "--num-predict");
125
- settings.num_predict = Math.trunc(parseNumberFlag(raw, "--num-predict"));
126
- continue;
127
- }
128
- if (arg === "--num-ctx" || arg.startsWith("--num-ctx=")) {
129
- const raw = arg.includes("=") ? arg.slice("--num-ctx=".length) : takeValue(args, "--num-ctx");
130
- settings.num_ctx = Math.trunc(parseNumberFlag(raw, "--num-ctx"));
131
- continue;
132
- }
133
- if (arg === "--keep-alive" || arg.startsWith("--keep-alive=")) {
134
- const raw = arg.includes("=") ? arg.slice("--keep-alive=".length) : takeValue(args, "--keep-alive");
135
- const asNum = Number(raw);
136
- settings.keep_alive =
137
- raw.trim() !== "" && Number.isFinite(asNum) && String(asNum) === raw ? asNum : raw;
95
+ if (arg === "-f" || arg === "--file" || arg.startsWith("--file=")) {
96
+ file = arg === "-f" ? takeValue(args, "--file", usage) : takeFlag(arg, "--file", args, usage);
138
97
  continue;
139
98
  }
140
99
  if (arg.startsWith("-")) {
@@ -189,14 +148,7 @@ export async function runCompare(opts) {
189
148
  const defaulted = !assembled;
190
149
  const prompt = assembled || DEFAULT_COMPARE_PROMPT;
191
150
  const config = opts.config ? configFromPartial(opts.config) : await loadConfig();
192
- const { hosts } = await discoverHosts({
193
- hosts: config.hosts,
194
- discovery: config.discovery,
195
- });
196
- const host = resolveHost(hosts, opts.machine);
197
- if (!host.online && !host.isSelf && host.source === "tailscale") {
198
- throw new Error(`Machine "${shortName(host)}" appears offline.`);
199
- }
151
+ const host = await resolveTarget(opts.machine, config);
200
152
  const baseUrl = ollamaBaseUrl(host);
201
153
  const timeoutMs = opts.timeoutMs ?? compareTimeoutMs();
202
154
  let available = null;
package/dist/help.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Per-command help: `ollanet help bench` and `ollanet bench help`.
3
+ */
4
+ export declare function canonicalCommand(name: string): string | undefined;
5
+ export declare function commandNames(): string[];
6
+ export declare function commandHelp(name: string): Promise<string | undefined>;
7
+ export declare function isHelpRequest(args: string[]): boolean;
package/dist/help.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Per-command help: `ollanet help bench` and `ollanet bench help`.
3
+ */
4
+ const LOADERS = {
5
+ scan: () => import("./scan.js"),
6
+ prompt: () => import("./prompt.js"),
7
+ chats: () => import("./chats.js"),
8
+ pull: () => import("./pull.js"),
9
+ show: () => import("./show.js"),
10
+ rm: () => import("./rm.js"),
11
+ ps: () => import("./ps.js"),
12
+ compare: () => import("./compare.js"),
13
+ bench: () => import("./bench.js"),
14
+ mcp: () => import("./mcp.js"),
15
+ };
16
+ const ALIASES = {
17
+ "ls-servers": "scan",
18
+ ask: "prompt",
19
+ chat: "prompt",
20
+ ls: "chats",
21
+ history: "chats",
22
+ download: "pull",
23
+ inspect: "show",
24
+ delete: "rm",
25
+ loaded: "ps",
26
+ vs: "compare",
27
+ h2h: "compare",
28
+ benchmark: "bench",
29
+ };
30
+ export function canonicalCommand(name) {
31
+ const key = name.trim().toLowerCase();
32
+ if (LOADERS[key])
33
+ return key;
34
+ return ALIASES[key];
35
+ }
36
+ export function commandNames() {
37
+ return Object.keys(LOADERS);
38
+ }
39
+ export async function commandHelp(name) {
40
+ const canon = canonicalCommand(name);
41
+ if (!canon)
42
+ return undefined;
43
+ const load = LOADERS[canon];
44
+ if (!load)
45
+ return undefined;
46
+ const mod = await load();
47
+ return mod.helpText();
48
+ }
49
+ export function isHelpRequest(args) {
50
+ return args.length === 1 && (args[0] === "help" || args[0] === "--help" || args[0] === "-h");
51
+ }
package/dist/index.d.ts CHANGED
@@ -29,5 +29,6 @@ export type { RemoveOptions, RemoveResult } from "./rm.js";
29
29
  export { listLoaded } from "./ps.js";
30
30
  export type { PsOptions, PsResult, LoadedHost, LoadedModel } from "./ps.js";
31
31
  export { looksTuned } from "./tuned.js";
32
+ export { resolveTarget, listTargets } from "./target.js";
32
33
  export { ollamaChat, ollamaTags, ollamaShow, ollamaPs, ollamaPsRequired, ollamaPull, ollamaDelete, } from "./ollama-chat.js";
33
34
  export type { PullChunk, OllamaPullOptions, OllamaPullResult, OllamaShowInfo, } from "./ollama-chat.js";
package/dist/index.js CHANGED
@@ -19,4 +19,5 @@ export { showModel } from "./show.js";
19
19
  export { removeModel } from "./rm.js";
20
20
  export { listLoaded } from "./ps.js";
21
21
  export { looksTuned } from "./tuned.js";
22
+ export { resolveTarget, listTargets } from "./target.js";
22
23
  export { ollamaChat, ollamaTags, ollamaShow, ollamaPs, ollamaPsRequired, ollamaPull, ollamaDelete, } from "./ollama-chat.js";
package/dist/mcp.d.ts CHANGED
@@ -8,4 +8,5 @@
8
8
  *
9
9
  * stdout is reserved for JSON-RPC; log only to stderr.
10
10
  */
11
+ export declare function helpText(): string;
11
12
  export declare function main(): Promise<void>;
package/dist/mcp.js CHANGED
@@ -21,6 +21,7 @@ import { removeModel } from "./rm.js";
21
21
  import { lastScan } from "./scan-store.js";
22
22
  import { scanNetwork } from "./scan.js";
23
23
  import { showModel } from "./show.js";
24
+ import { isHelpFlag, printHelp } from "./argv.js";
24
25
  // Pin to the oldest widely-supported MCP revision. Clients may request a newer
25
26
  // date; we still answer with this — legal negotiation, max compatibility.
26
27
  const PROTOCOL_VERSION = "2024-11-05";
@@ -487,7 +488,29 @@ async function handleRequest(msg, version) {
487
488
  fail(id, -32601, `Method not found: ${method}`);
488
489
  }
489
490
  }
491
+ export function helpText() {
492
+ return `Usage:
493
+ ollanet mcp
494
+
495
+ Stdio MCP server (no extra npm deps). stdout is JSON-RPC; logs go to stderr.
496
+
497
+ Tools:
498
+ ollanet_scan Discover hosts + models (lan, all, last)
499
+ ollanet_prompt Prompt a host or continue with chat_id
500
+ ollanet_compare Same prompt on 2–5 models
501
+ ollanet_pull Pull / update a model on a host
502
+ ollanet_show Inspect a model
503
+ ollanet_rm Delete a model (confirm: true)
504
+ ollanet_ps Models loaded in VRAM
505
+ ollanet_list_chats Saved transcripts
506
+ ollanet_get_chat One chat by id
507
+
508
+ Example Cursor config: { "command": "npx", "args": ["-y", "ollanet", "mcp"] }`;
509
+ }
490
510
  export async function main() {
511
+ if (process.argv.slice(2).some((arg) => isHelpFlag(arg))) {
512
+ printHelp(helpText());
513
+ }
491
514
  const version = await packageVersion();
492
515
  console.error(`ollanet mcp ${version} (stdio) — tools: ${TOOLS.map((t) => t.name).join(", ")}`);
493
516
  const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
package/dist/prompt.d.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  * ollanet chats
13
13
  */
14
14
  import { type AppConfig, type GenerateSettings } from "./config.js";
15
+ export declare function helpText(): string;
15
16
  export interface PromptRunOptions {
16
17
  machine?: string;
17
18
  model?: string;
package/dist/prompt.js CHANGED
@@ -14,15 +14,17 @@
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
19
  import { assemblePrompt, readPromptFile } from "./prompt-input.js";
20
+ import { consumeSettingsFlag, failUsage, isHelpFlag, printHelp, takeFlag, takeValue } from "./argv.js";
21
+ import { listTargets, resolveTarget } from "./target.js";
20
22
  /** Prompt/chat HTTP timeout (ms). 0 = no timeout. Separate from scan's OLLAMA_TIMEOUT_MS. */
21
23
  function promptTimeoutMs() {
22
24
  return envInt("OLLAMA_PROMPT_TIMEOUT_MS", 600_000);
23
25
  }
24
- function usage() {
25
- console.error(`Usage:
26
+ export function helpText() {
27
+ return `Usage:
26
28
  ollanet prompt <machine> [model] <prompt...>
27
29
  ollanet prompt --chat <hash> <prompt...>
28
30
  ollanet prompt <machine> --chat <hash> <prompt...>
@@ -55,8 +57,10 @@ Options:
55
57
  --no-think Disable thinking (default) so tokens go to the reply
56
58
  --file <path> Prompt from a .txt or .md file (joins argv / stdin)
57
59
  --no-stream Wait for the full response
58
- --json Emit final response JSON (implies --no-stream)`);
59
- process.exit(1);
60
+ --json Emit final response JSON (implies --no-stream)`;
61
+ }
62
+ function usage() {
63
+ failUsage(helpText());
60
64
  }
61
65
  /**
62
66
  * Heuristic for an optional positional model token.
@@ -119,22 +123,6 @@ async function maybePeelModel(host, promptParts, fromStdin) {
119
123
  }
120
124
  return { promptParts };
121
125
  }
122
- function takeValue(args, flag) {
123
- const value = args.shift();
124
- if (!value) {
125
- console.error(`${flag} requires a value`);
126
- usage();
127
- }
128
- return value;
129
- }
130
- function parseNumberFlag(raw, flag) {
131
- const n = Number(raw);
132
- if (!Number.isFinite(n)) {
133
- console.error(`${flag} must be a number (got "${raw}")`);
134
- usage();
135
- }
136
- return n;
137
- }
138
126
  function parseArgs(argv) {
139
127
  const args = [...argv];
140
128
  let machineFlag;
@@ -150,8 +138,8 @@ function parseArgs(argv) {
150
138
  const arg = args.shift();
151
139
  if (arg === "--")
152
140
  continue;
153
- if (arg === "--help" || arg === "-h")
154
- usage();
141
+ if (isHelpFlag(arg))
142
+ printHelp(helpText());
155
143
  if (arg === "--no-stream") {
156
144
  stream = false;
157
145
  continue;
@@ -165,59 +153,34 @@ function parseArgs(argv) {
165
153
  save = false;
166
154
  continue;
167
155
  }
168
- if (arg === "--think") {
169
- settings.think = true;
156
+ if (consumeSettingsFlag(arg, args, settings, usage, {
157
+ temperature: true,
158
+ numPredict: true,
159
+ system: true,
160
+ })) {
170
161
  continue;
171
162
  }
172
- if (arg === "--no-think") {
173
- settings.think = false;
163
+ const chat = takeFlag(arg, "--chat", args, usage);
164
+ if (chat !== undefined) {
165
+ chatId = chat;
174
166
  continue;
175
167
  }
176
- if (arg === "--chat" || arg.startsWith("--chat=")) {
177
- chatId = arg.includes("=") ? arg.slice("--chat=".length) : takeValue(args, "--chat");
168
+ const machine = takeFlag(arg, "--machine", args, usage);
169
+ if (machine !== undefined) {
170
+ machineFlag = machine;
178
171
  continue;
179
172
  }
180
- if (arg === "--machine" || arg.startsWith("--machine=")) {
181
- machineFlag = arg.includes("=") ? arg.slice("--machine=".length) : takeValue(args, "--machine");
173
+ const model = takeFlag(arg, "--model", args, usage);
174
+ if (model !== undefined) {
175
+ modelFlag = model;
182
176
  continue;
183
177
  }
184
- if (arg === "--model" || arg.startsWith("--model=")) {
185
- modelFlag = arg.includes("=") ? arg.slice("--model=".length) : takeValue(args, "--model");
186
- continue;
187
- }
188
- if (arg === "--file" || arg === "-f" || arg.startsWith("--file=")) {
189
- file = arg.includes("=") ? arg.slice("--file=".length) : takeValue(args, "--file");
190
- continue;
191
- }
192
- if (arg === "--system" || arg.startsWith("--system=")) {
193
- settings.system = arg.includes("=")
194
- ? arg.slice("--system=".length)
195
- : takeValue(args, "--system");
196
- continue;
197
- }
198
- if (arg === "--temperature" || arg === "-t" || arg.startsWith("--temperature=")) {
199
- const raw = arg.includes("=") ? arg.slice("--temperature=".length) : takeValue(args, "--temperature");
200
- settings.temperature = parseNumberFlag(raw, "--temperature");
201
- continue;
202
- }
203
- if (arg === "--num-predict" || arg.startsWith("--num-predict=")) {
204
- const raw = arg.includes("=") ? arg.slice("--num-predict=".length) : takeValue(args, "--num-predict");
205
- settings.num_predict = Math.trunc(parseNumberFlag(raw, "--num-predict"));
206
- continue;
207
- }
208
- if (arg === "--num-ctx" || arg.startsWith("--num-ctx=")) {
209
- const raw = arg.includes("=") ? arg.slice("--num-ctx=".length) : takeValue(args, "--num-ctx");
210
- settings.num_ctx = Math.trunc(parseNumberFlag(raw, "--num-ctx"));
211
- continue;
212
- }
213
- if (arg === "--keep-alive" || arg.startsWith("--keep-alive=")) {
214
- const raw = arg.includes("=") ? arg.slice("--keep-alive=".length) : takeValue(args, "--keep-alive");
215
- const asNum = Number(raw);
216
- settings.keep_alive = raw.trim() !== "" && Number.isFinite(asNum) && String(asNum) === raw ? asNum : raw;
178
+ if (arg === "-f" || arg === "--file" || arg.startsWith("--file=")) {
179
+ file = arg === "-f" ? takeValue(args, "--file", usage) : takeFlag(arg, "--file", args, usage);
217
180
  continue;
218
181
  }
219
182
  if (arg === "--format" || arg.startsWith("--format=")) {
220
- const raw = arg.includes("=") ? arg.slice("--format=".length) : takeValue(args, "--format");
183
+ const raw = takeFlag(arg, "--format", args, usage) ?? "";
221
184
  const format = parseFormat(raw);
222
185
  if (format !== undefined)
223
186
  settings.format = format;
@@ -325,10 +288,6 @@ export async function runPrompt(opts) {
325
288
  const writeStdout = opts.writeStdout !== false;
326
289
  let promptParts = opts.prompt ? [opts.prompt] : [];
327
290
  const config = opts.config ? configFromPartial(opts.config) : await loadConfig();
328
- const { hosts: targets } = await discoverHosts({
329
- hosts: config.hosts,
330
- discovery: config.discovery,
331
- });
332
291
  let chat;
333
292
  let isNewChat = false;
334
293
  let machineQuery = opts.machine;
@@ -339,10 +298,7 @@ export async function runPrompt(opts) {
339
298
  if (!machineQuery) {
340
299
  throw new Error("Machine is required for a new chat (or pass chatId).");
341
300
  }
342
- const host = resolveHost(targets, machineQuery);
343
- if (!host.online && !host.isSelf && host.source === "tailscale") {
344
- throw new Error(`Machine "${shortName(host)}" appears offline.`);
345
- }
301
+ const host = await resolveTarget(machineQuery, config);
346
302
  let peeledModel;
347
303
  if (!opts.chatId && !opts.model) {
348
304
  const peeled = await maybePeelModel(host, promptParts, "");
@@ -456,10 +412,7 @@ export async function main() {
456
412
  }
457
413
  }
458
414
  const config = await loadConfig();
459
- const { hosts: targets } = await discoverHosts({
460
- hosts: config.hosts,
461
- discovery: config.discovery,
462
- });
415
+ const targets = await listTargets(config);
463
416
  let machineQuery = parsed.machine;
464
417
  if (parsed.chatId) {
465
418
  // Allow `ollanet prompt <host> --chat HASH "follow-up"` only when the
package/dist/ps.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * ollanet ps studio
6
6
  */
7
7
  import { type AppConfig } from "./config.js";
8
+ export declare function helpText(): string;
8
9
  export interface LoadedModel {
9
10
  name: string;
10
11
  size_vram?: number;
package/dist/ps.js CHANGED
@@ -7,13 +7,14 @@
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 { failUsage, isHelpFlag, printHelp, takeFlag } from "./argv.js";
10
11
  import { resolveTarget } from "./target.js";
11
12
  import { looksTuned } from "./tuned.js";
12
13
  function psTimeoutMs() {
13
14
  return envInt("OLLAMA_PS_TIMEOUT_MS", 10_000);
14
15
  }
15
- function usage() {
16
- console.error(`Usage:
16
+ export function helpText() {
17
+ return `Usage:
17
18
  ollanet ps [machine]
18
19
  ollanet ps --machine <name>
19
20
 
@@ -26,16 +27,10 @@ scan lists models on disk. ps lists what is loaded in VRAM right now.
26
27
 
27
28
  Options:
28
29
  --machine <name> One host (default: every discovered host)
29
- --json Emit result JSON on stdout`);
30
- process.exit(1);
30
+ --json Emit result JSON on stdout`;
31
31
  }
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;
32
+ function usage() {
33
+ failUsage(helpText());
39
34
  }
40
35
  function parseArgs(argv) {
41
36
  const args = [...argv];
@@ -46,14 +41,15 @@ function parseArgs(argv) {
46
41
  const arg = args.shift();
47
42
  if (arg === "--")
48
43
  continue;
49
- if (arg === "--help" || arg === "-h")
50
- usage();
44
+ if (isHelpFlag(arg))
45
+ printHelp(helpText());
51
46
  if (arg === "--json") {
52
47
  json = true;
53
48
  continue;
54
49
  }
55
- if (arg === "--machine") {
56
- machineFlag = takeValue(args, "--machine");
50
+ const machine = takeFlag(arg, "--machine", args, usage);
51
+ if (machine !== undefined) {
52
+ machineFlag = machine;
57
53
  continue;
58
54
  }
59
55
  if (arg.startsWith("-")) {
package/dist/pull.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { AppConfig } from "./config.js";
10
10
  import { type PullChunk } from "./ollama-chat.js";
11
+ export declare function helpText(): string;
11
12
  export interface PullOptions {
12
13
  machine: string;
13
14
  model: string;
package/dist/pull.js CHANGED
@@ -8,13 +8,14 @@
8
8
  */
9
9
  import { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
10
10
  import { ollamaPull } from "./ollama-chat.js";
11
+ import { failUsage, isHelpFlag, printHelp, 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() {
14
15
  return envInt("OLLAMA_PULL_TIMEOUT_MS", 0);
15
16
  }
16
- function usage() {
17
- console.error(`Usage:
17
+ export function helpText() {
18
+ return `Usage:
18
19
  ollanet pull <machine> <model>
19
20
  ollanet pull --machine <name> --model <name>
20
21
 
@@ -32,16 +33,10 @@ Options:
32
33
  --model <name> Model to pull (e.g. gemma3:12b)
33
34
  --insecure Allow HTTP / self-signed model registries
34
35
  --no-stream Wait for a single final response (no progress chunks)
35
- --json Emit result JSON on stdout`);
36
- process.exit(1);
36
+ --json Emit result JSON on stdout`;
37
37
  }
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;
38
+ function usage() {
39
+ failUsage(helpText());
45
40
  }
46
41
  function parseArgs(argv) {
47
42
  const args = [...argv];
@@ -55,8 +50,8 @@ function parseArgs(argv) {
55
50
  const arg = args.shift();
56
51
  if (arg === "--")
57
52
  continue;
58
- if (arg === "--help" || arg === "-h")
59
- usage();
53
+ if (isHelpFlag(arg))
54
+ printHelp(helpText());
60
55
  if (arg === "--insecure") {
61
56
  insecure = true;
62
57
  continue;
@@ -69,12 +64,14 @@ function parseArgs(argv) {
69
64
  json = true;
70
65
  continue;
71
66
  }
72
- if (arg === "--machine") {
73
- machineFlag = takeValue(args, "--machine");
67
+ const machine = takeFlag(arg, "--machine", args, usage);
68
+ if (machine !== undefined) {
69
+ machineFlag = machine;
74
70
  continue;
75
71
  }
76
- if (arg === "--model") {
77
- modelFlag = takeValue(args, "--model");
72
+ const model = takeFlag(arg, "--model", args, usage);
73
+ if (model !== undefined) {
74
+ modelFlag = model;
78
75
  continue;
79
76
  }
80
77
  if (arg.startsWith("-")) {
package/dist/rm.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * ollanet rm studio llama3.2:1b --yes
5
5
  */
6
6
  import type { AppConfig } from "./config.js";
7
+ export declare function helpText(): string;
7
8
  export interface RemoveOptions {
8
9
  machine: string;
9
10
  model: string;
package/dist/rm.js CHANGED
@@ -6,12 +6,13 @@
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 { failUsage, isHelpFlag, printHelp, 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);
12
13
  }
13
- function usage() {
14
- console.error(`Usage:
14
+ export function helpText() {
15
+ return `Usage:
15
16
  ollanet rm <machine> <model> --yes
16
17
  ollanet rm --machine <name> --model <name> --yes
17
18
 
@@ -26,16 +27,10 @@ Options:
26
27
  --machine <name> Host (discovered name, MagicDNS, hostname, or IP[:port])
27
28
  --model <name> Model to delete
28
29
  --yes Required unless stdin is a TTY and you confirm
29
- --json Emit result JSON on stdout`);
30
- process.exit(1);
30
+ --json Emit result JSON on stdout`;
31
31
  }
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;
32
+ function usage() {
33
+ failUsage(helpText());
39
34
  }
40
35
  function parseArgs(argv) {
41
36
  const args = [...argv];
@@ -48,8 +43,8 @@ function parseArgs(argv) {
48
43
  const arg = args.shift();
49
44
  if (arg === "--")
50
45
  continue;
51
- if (arg === "--help" || arg === "-h")
52
- usage();
46
+ if (isHelpFlag(arg))
47
+ printHelp(helpText());
53
48
  if (arg === "--yes" || arg === "-y") {
54
49
  yes = true;
55
50
  continue;
@@ -58,12 +53,14 @@ function parseArgs(argv) {
58
53
  json = true;
59
54
  continue;
60
55
  }
61
- if (arg === "--machine") {
62
- machineFlag = takeValue(args, "--machine");
56
+ const machine = takeFlag(arg, "--machine", args, usage);
57
+ if (machine !== undefined) {
58
+ machineFlag = machine;
63
59
  continue;
64
60
  }
65
- if (arg === "--model") {
66
- modelFlag = takeValue(args, "--model");
61
+ const model = takeFlag(arg, "--model", args, usage);
62
+ if (model !== undefined) {
63
+ modelFlag = model;
67
64
  continue;
68
65
  }
69
66
  if (arg.startsWith("-")) {
package/dist/scan.d.ts CHANGED
@@ -54,4 +54,5 @@ export interface ScanPayload {
54
54
  }
55
55
  /** Programmatic scan used by CLI `--json`, MCP, and `import { scanNetwork } from "ollanet"`. */
56
56
  export declare function scanNetwork(options?: ScanOptions): Promise<ScanPayload>;
57
+ export declare function helpText(): string;
57
58
  export declare function main(): Promise<void>;
package/dist/scan.js CHANGED
@@ -7,6 +7,7 @@ import { configFromPartial, loadConfig } from "./config.js";
7
7
  import { discoverHosts, envInt, ollamaPort, shortName, } from "./hosts.js";
8
8
  import { lastScanPath, loadLastScan, saveLastScan } from "./scan-store.js";
9
9
  import { looksTuned } from "./tuned.js";
10
+ import { isHelpFlag, printHelp } from "./argv.js";
10
11
  function scanTimeoutMs() {
11
12
  return envInt("OLLAMA_TIMEOUT_MS", 2500);
12
13
  }
@@ -184,7 +185,30 @@ export async function scanNetwork(options = {}) {
184
185
  }
185
186
  return payload;
186
187
  }
188
+ export function helpText() {
189
+ return `Usage:
190
+ ollanet scan [--json] [--all] [--lan] [--last]
191
+
192
+ Examples:
193
+ ollanet scan
194
+ ollanet scan --lan
195
+ ollanet scan --last
196
+ ollanet scan --json
197
+
198
+ Discovers Ollama hosts (localhost, config, OLLANET_HOSTS, Tailscale) and lists
199
+ models. --lan is an opt-in TCP sweep of local /24s on port 11434.
200
+
201
+ Options:
202
+ --json Machine-readable output
203
+ --all Also probe offline Tailscale peers
204
+ --lan TCP-scan local LAN CIDRs for open Ollama ports
205
+ --last Reprint the last saved scan (no network)
206
+ --cached Same as --last`;
207
+ }
187
208
  export async function main() {
209
+ if (process.argv.slice(2).some((arg) => isHelpFlag(arg))) {
210
+ printHelp(helpText());
211
+ }
188
212
  const includeOffline = process.argv.includes("--all");
189
213
  const jsonOut = process.argv.includes("--json");
190
214
  const lanScan = process.argv.includes("--lan");
package/dist/show.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { type OllamaShowInfo } from "./ollama-chat.js";
7
7
  import type { AppConfig } from "./config.js";
8
+ export declare function helpText(): string;
8
9
  export interface ShowOptions {
9
10
  machine: string;
10
11
  model: string;
package/dist/show.js CHANGED
@@ -5,13 +5,14 @@
5
5
  */
6
6
  import { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
7
7
  import { ollamaShow } from "./ollama-chat.js";
8
+ import { failUsage, isHelpFlag, printHelp, takeFlag } from "./argv.js";
8
9
  import { resolveTarget } from "./target.js";
9
10
  import { looksTuned } from "./tuned.js";
10
11
  function showTimeoutMs() {
11
12
  return envInt("OLLAMA_SHOW_TIMEOUT_MS", 30_000);
12
13
  }
13
- function usage() {
14
- console.error(`Usage:
14
+ export function helpText() {
15
+ return `Usage:
15
16
  ollanet show <machine> <model>
16
17
  ollanet show --machine <name> --model <name>
17
18
 
@@ -25,16 +26,10 @@ Tuned Finetuna-style names are marked [tuned].
25
26
  Options:
26
27
  --machine <name> Host (discovered name, MagicDNS, hostname, or IP[:port])
27
28
  --model <name> Model to inspect
28
- --json Emit result JSON on stdout`);
29
- process.exit(1);
29
+ --json Emit result JSON on stdout`;
30
30
  }
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;
31
+ function usage() {
32
+ failUsage(helpText());
38
33
  }
39
34
  function parseArgs(argv) {
40
35
  const args = [...argv];
@@ -46,18 +41,20 @@ function parseArgs(argv) {
46
41
  const arg = args.shift();
47
42
  if (arg === "--")
48
43
  continue;
49
- if (arg === "--help" || arg === "-h")
50
- usage();
44
+ if (isHelpFlag(arg))
45
+ printHelp(helpText());
51
46
  if (arg === "--json") {
52
47
  json = true;
53
48
  continue;
54
49
  }
55
- if (arg === "--machine") {
56
- machineFlag = takeValue(args, "--machine");
50
+ const machine = takeFlag(arg, "--machine", args, usage);
51
+ if (machine !== undefined) {
52
+ machineFlag = machine;
57
53
  continue;
58
54
  }
59
- if (arg === "--model") {
60
- modelFlag = takeValue(args, "--model");
55
+ const model = takeFlag(arg, "--model", args, usage);
56
+ if (model !== undefined) {
57
+ modelFlag = model;
61
58
  continue;
62
59
  }
63
60
  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>;
package/dist/target.js CHANGED
@@ -1,15 +1,20 @@
1
1
  import { configFromPartial, loadConfig } from "./config.js";
2
2
  import { discoverHosts, resolveHost, shortName } from "./hosts.js";
3
- /** Resolve a machine name the same way pull / prompt / bench do. */
4
- export async function resolveTarget(machine, config) {
5
- const query = machine.trim();
6
- if (!query)
7
- throw new Error("Machine is required.");
3
+ /** Discover hosts the same way every command does (config + Tailscale + …). */
4
+ export async function listTargets(config) {
8
5
  const app = config ? configFromPartial(config) : await loadConfig();
9
6
  const { hosts } = await discoverHosts({
10
7
  hosts: app.hosts,
11
8
  discovery: app.discovery,
12
9
  });
10
+ return hosts;
11
+ }
12
+ /** Resolve a machine name the same way pull / prompt / bench do. */
13
+ export async function resolveTarget(machine, config) {
14
+ const query = machine.trim();
15
+ if (!query)
16
+ throw new Error("Machine is required.");
17
+ const hosts = await listTargets(config);
13
18
  const host = resolveHost(hosts, query);
14
19
  if (!host.online && !host.isSelf && host.source === "tailscale") {
15
20
  throw new Error(`Machine "${shortName(host)}" appears offline.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ollanet",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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": {