ollanet 0.6.1 → 0.6.3

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
@@ -192,12 +192,13 @@ optimizeDeps: { exclude: ["ollanet"] },
192
192
  | `ollanet pull <machine> <model>` | Ask that host to download / update a library model |
193
193
  | `ollanet show <machine> <model>` | Inspect Modelfile / params / capabilities (`[tuned]` when it looks like Finetuna) |
194
194
  | `ollanet rm <machine> <model> --yes` | Delete a model from that host’s disk |
195
- | `ollanet ps [machine]` | Models loaded in VRAM (omit machine = every discovered host) |
195
+ | `ollanet ps [machine]` | Models loaded in VRAM, with CPU/GPU % (omit machine = every discovered host) |
196
196
  | `ollanet prompt …` | Send a prompt / continue a chat (`--file` for `.txt` / `.md`) |
197
197
  | `ollanet compare …` | Same prompt on 2–5 models; summary + `compares/<id>.md` |
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
 
@@ -339,10 +340,12 @@ ollanet bench localhost gemma4:12b --hot --runs 5
339
340
  ```
340
341
 
341
342
  Runs a built-in suite (`quick` default, or `--suite full`) against one or more **completion**
342
- models: instruction checks once, throughput case repeated (`--runs`, default 3) with
343
+ models: instruction checks once, 256-token throughput repeated (`--runs`, default 3) with
343
344
  pinned `num_predict` / `seed` / `temperature`. Reports median tok/s + spread.
344
- `--hot` discards the first throughput run and leaves models loaded so the counted
345
- repeats are already warm (Finetuna steady-state). Under `--all`,
345
+ `--hot` discards the first 256-token run and leaves models loaded so the counted
346
+ repeats are already warm (Finetuna steady-state). `--suite full` also runs json/reason
347
+ checks and **one** 1024-token prose generation (`tok/s long`) — closer to a long chat
348
+ reply. Under `--all`,
346
349
  skips non-completion models (embeddings); multimodal `completion`+`vision` models stay in
347
350
  (early-stop / pass_rate handle text-suite failures). Use `--exclude-vision` to drop them.
348
351
  Saves `benchmarks/<id>.json` (or `~/.ollanet/benchmarks/` when installed).
@@ -350,7 +353,7 @@ See [`docs/bench-spec.md`](docs/bench-spec.md) for measurement rules.
350
353
 
351
354
  `quick` has only two scored checks (`ping`, `math`), so `pass` is a coarse
352
355
  liveness/sanity gauge (`0/2`, `1/2`, `2/2`) — not “50% broken.” Use `--suite full`
353
- when you care more about the quality signal.
356
+ when you care more about the quality signal or want the long tok/s column.
354
357
 
355
358
  ## Discovery
356
359
 
package/dist/argv.d.ts CHANGED
@@ -3,6 +3,11 @@
3
3
  */
4
4
  import type { GenerateSettings } from "./config.js";
5
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;
6
11
  export declare function takeValue(args: string[], flag: string, usage: UsageFn): string;
7
12
  /** `--name value` or `--name=value`. Returns undefined if `arg` is not this flag. */
8
13
  export declare function takeFlag(arg: string, name: string, args: string[], usage: UsageFn): string | undefined;
package/dist/argv.js CHANGED
@@ -1,3 +1,16 @@
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
+ }
1
14
  export function takeValue(args, flag, usage) {
2
15
  const value = args.shift();
3
16
  if (!value) {
@@ -3,14 +3,21 @@ export interface BenchCase {
3
3
  id: string;
4
4
  role: CaseRole;
5
5
  prompt: string;
6
- /** Cap for this case; throughput uses settings.throughput_num_predict. */
6
+ /** Cap for this case. Peak throughput uses `--num-predict` when omitted. */
7
7
  num_predict?: number;
8
+ /**
9
+ * Throughput attempts. Omit to use `--runs` (and `--hot` discard).
10
+ * Set `1` for a single large shot that does not repeat.
11
+ */
12
+ repeats?: number;
8
13
  format?: "json";
9
14
  check?: (content: string) => {
10
15
  ok: boolean;
11
16
  detail?: string;
12
17
  };
13
18
  }
19
+ /** Pinned length for `full`'s prose throughput case (`throughput_long`). */
20
+ export declare const LONG_THROUGHPUT_NUM_PREDICT = 1024;
14
21
  export type SuiteName = "quick" | "full";
15
22
  export declare function getSuiteCases(suite: SuiteName): BenchCase[];
16
23
  export declare function suiteRevision(suite: SuiteName): string;
@@ -2,6 +2,8 @@
2
2
  * Built-in ollanet bench suites, checkers, and revision hashes.
3
3
  */
4
4
  import { createHash } from "node:crypto";
5
+ /** Pinned length for `full`'s prose throughput case (`throughput_long`). */
6
+ export const LONG_THROUGHPUT_NUM_PREDICT = 1024;
5
7
  const MATH_ANSWER = 323;
6
8
  const REASON_ANSWER = 42;
7
9
  function lastTokenNormalized(content) {
@@ -104,18 +106,23 @@ const FULL_EXTRA = [
104
106
  {
105
107
  id: "reason",
106
108
  role: "check",
107
- prompt: "A box has 10 apples. You eat 3, then buy 5 more, then give away 2 times as many as you ate. " +
108
- "How many apples are left? Reply with only the final number.",
109
+ prompt: "Start with 40. Add 7. Subtract 5. Reply with only the final number.",
109
110
  num_predict: 48,
110
- // 10 - 3 + 5 - 6 = 6... wait: gave away 2*3=6, so 10-3+5-6=6. Spec said 42 as placeholder.
111
- // Use a problem that clearly ends at 42:
112
- // Actually rewrite for 42: "Start with 40. Add 7. Subtract 5. Reply with only the number." => 42
113
111
  check: checkExpectedInt(REASON_ANSWER),
114
112
  },
113
+ {
114
+ id: "throughput_long",
115
+ role: "throughput",
116
+ // Chat-shaped tokens (markdown, lists, a table) at a long pin. Anti-EOS
117
+ // so done_reason=length; one shot (not --runs) so full stays usable.
118
+ prompt: "Write a detailed trade-off briefing for a product team choosing a web UI stack. " +
119
+ "Compare at least two options. Use markdown headings, bullet lists, and at least one table. " +
120
+ "Keep adding sections (testing, hiring, migration, failure modes, cost) without wrapping up. " +
121
+ "Do not write a closing recommendation that ends the piece. Do not stop early.",
122
+ num_predict: LONG_THROUGHPUT_NUM_PREDICT,
123
+ repeats: 1,
124
+ },
115
125
  ];
116
- // Fix reason prompt to match REASON_ANSWER
117
- FULL_EXTRA[FULL_EXTRA.length - 1].prompt =
118
- "Start with 40. Add 7. Subtract 5. Reply with only the final number.";
119
126
  export function getSuiteCases(suite) {
120
127
  if (suite === "full")
121
128
  return [...QUICK_CASES, ...FULL_EXTRA];
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
@@ -2,46 +2,55 @@
2
2
  * ollanet bench — speed + lightweight quality suite against Ollama models.
3
3
  */
4
4
  import { defaultModelForHost, loadConfig, machineSettingsForHost, mergeSettings, settingsFromEnv, } from "./config.js";
5
- import { comparabilityKey, getSuiteCases, median, suiteRevision, } from "./bench-suite.js";
5
+ import { comparabilityKey, getSuiteCases, LONG_THROUGHPUT_NUM_PREDICT, 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
8
  import { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
9
- import { consumeSettingsFlag, parseNumberFlag, takeFlag } from "./argv.js";
9
+ import { consumeSettingsFlag, failUsage, isHelpFlag, parseNumberFlag, printHelp, takeFlag } from "./argv.js";
10
10
  import { resolveTarget } from "./target.js";
11
11
  import { projectPath } from "./paths.js";
12
12
  import { contextLengthForModel, isCompletionCapable, isVisionCapable, ollamaChat, ollamaShow, ollamaTags, ollamaUnload, ollamaVersion, ollamaPs, psHasModel, shouldWarnNoThinking, vramForModel, waitUntilUnloaded, } from "./ollama-chat.js";
13
13
  const MODEL_COL = 34;
14
14
  const BENCH_TIMEOUT_MS = envInt("OLLAMA_BENCH_TIMEOUT_MS", 60_000);
15
15
  const UNLOAD_WAIT_MS = envInt("OLLAMA_BENCH_UNLOAD_WAIT_MS", 60_000);
16
- function usage() {
17
- console.error(`Usage:
16
+ export function helpText() {
17
+ return `Usage:
18
18
  ollanet bench <machine|ip> [model...] [options]
19
19
  ollanet bench <machine|ip> --all [options]
20
20
 
21
21
  Examples:
22
22
  ollanet bench studio gemma3:12b --runs 5
23
23
  ollanet bench studio gemma3:12b --hot --runs 5
24
+ ollanet help bench
25
+
26
+ Fixed suite (not compare). Checks run once; 256-token throughput repeats --runs times.
27
+ Median tok/s excludes early-stops. Unload is between models, not between repeats.
28
+ full adds json/reason checks plus one 1024-token prose generation (tok/s long).
24
29
 
25
30
  Options:
26
31
  --all Every completion-capable model from /api/tags
27
32
  --exclude-vision With --all, skip models that advertise vision
28
33
  --suite quick|full Prompt suite (default quick)
29
- --runs <n> Throughput repeats (default 3)
30
- --hot Discard first throughput run; keep models loaded
31
- --warmup / --no-warmup
34
+ --runs <n> Counted 256-token throughput repeats (default 3)
35
+ --hot Discard first 256-token throughput run; keep models loaded
36
+ --warmup Short discarded call before timed cases (default on)
37
+ --no-warmup Skip the short warmup (--hot implies this)
32
38
  --cold-load Measure cold load via unload + /api/ps
33
- --num-predict <n> Pin throughput length (default 256)
34
- --num-ctx <n>
35
- --keep-alive <value>
36
- --think / --no-think
37
- --judge --judge-model <name>
39
+ --num-predict <n> Pin 256-token throughput length (default 256; not the long case)
40
+ --num-ctx <n> Context window override
41
+ --keep-alive <value> Keep model loaded (e.g. 5m, 0, -1)
42
+ --think / --no-think Thinking tokens (default off)
43
+ --judge Second pass: score check answers 1–5
44
+ --judge-model <name> Required with --judge (no default)
38
45
  --json Machine-readable stdout
39
- --save / --no-save
40
- --fail-fast
46
+ --save / --no-save Persist under benchmarks/ (default on)
47
+ --fail-fast Stop remaining models after the first hard error
41
48
  --fail-on-error Exit 2 if any model/case errored
42
49
 
43
- See docs/bench-spec.md for measurement rules.`);
44
- process.exit(1);
50
+ See docs/bench-spec.md for measurement rules.`;
51
+ }
52
+ function usage() {
53
+ failUsage(helpText());
45
54
  }
46
55
  function isLoopback(host) {
47
56
  return (host.source === "localhost" ||
@@ -57,6 +66,26 @@ function resolveModelName(tags, query) {
57
66
  const prefix = tags.find((t) => t.name.toLowerCase().startsWith(`${q}:`) || q.startsWith(`${t.name.toLowerCase()}:`));
58
67
  return prefix?.name;
59
68
  }
69
+ function pinnedNumPredict(caseDef, throughputNumPredict) {
70
+ if (caseDef.role === "throughput") {
71
+ return caseDef.num_predict ?? throughputNumPredict;
72
+ }
73
+ return caseDef.num_predict ?? 32;
74
+ }
75
+ /** At least the global timeout; longer pins need room to finish at ~8 tok/s. */
76
+ function attemptTimeoutMs(numPredict) {
77
+ return Math.max(BENCH_TIMEOUT_MS, Math.ceil(numPredict / 8) * 1000);
78
+ }
79
+ function caseReps(caseDef, runs, hot) {
80
+ if (caseDef.role !== "throughput")
81
+ return 1;
82
+ if (caseDef.repeats != null)
83
+ return caseDef.repeats;
84
+ return runs + (hot ? 1 : 0);
85
+ }
86
+ function discardFirstThroughput(caseDef, hot) {
87
+ return hot && caseDef.role === "throughput" && caseDef.repeats == null;
88
+ }
60
89
  function tokPerSec(chunk) {
61
90
  if (chunk.eval_count == null || chunk.eval_duration == null || chunk.eval_duration <= 0) {
62
91
  return undefined;
@@ -90,7 +119,9 @@ function parseArgs(argv) {
90
119
  const positional = [];
91
120
  while (args.length > 0) {
92
121
  const arg = args.shift();
93
- if (arg === "--" || arg === "--help" || arg === "-h")
122
+ if (isHelpFlag(arg))
123
+ printHelp(helpText());
124
+ if (arg === "--")
94
125
  usage();
95
126
  if (arg === "--all") {
96
127
  all = true;
@@ -275,9 +306,7 @@ async function runColdLoad(baseUrl, model, keepAlive) {
275
306
  }
276
307
  }
277
308
  async function runCaseAttempt(opts) {
278
- const numPredict = opts.caseDef.role === "throughput"
279
- ? opts.throughputNumPredict
280
- : (opts.caseDef.num_predict ?? 32);
309
+ const numPredict = pinnedNumPredict(opts.caseDef, opts.throughputNumPredict);
281
310
  const settings = mergeSettings(opts.settings, {
282
311
  temperature: 0,
283
312
  seed: 0,
@@ -293,7 +322,7 @@ async function runCaseAttempt(opts) {
293
322
  messages: [{ role: "user", content: opts.caseDef.prompt }],
294
323
  stream: false,
295
324
  writeStdout: false,
296
- timeoutMs: BENCH_TIMEOUT_MS,
325
+ timeoutMs: attemptTimeoutMs(numPredict),
297
326
  settings,
298
327
  });
299
328
  const wallMs = Date.now() - wall0;
@@ -363,12 +392,23 @@ async function runCaseAttempt(opts) {
363
392
  function countedAttempts(attempts) {
364
393
  return attempts.filter((a) => !a.discarded);
365
394
  }
366
- function summarizeModel(cases, cold, selfJudge) {
367
- const throughput = cases.find((c) => c.id === "throughput");
368
- const counted = countedAttempts(throughput?.attempts ?? []);
395
+ function speedFrom(cases, id) {
396
+ const counted = countedAttempts(cases.find((c) => c.id === id)?.attempts ?? []);
369
397
  const fullLen = counted.filter((a) => !a.error && !a.early_stop && a.tok_s != null);
370
398
  const toks = fullLen.map((a) => a.tok_s);
371
- const earlyStopCount = counted.filter((a) => a.early_stop || (a.done_reason && a.done_reason !== "length")).length;
399
+ return {
400
+ median: median(toks) ?? null,
401
+ min: toks.length ? Math.min(...toks) : null,
402
+ max: toks.length ? Math.max(...toks) : null,
403
+ };
404
+ }
405
+ function summarizeModel(cases, cold, selfJudge) {
406
+ const peak = speedFrom(cases, "throughput");
407
+ const long = speedFrom(cases, "throughput_long");
408
+ const throughputAttempts = cases
409
+ .filter((c) => c.role === "throughput")
410
+ .flatMap((c) => countedAttempts(c.attempts));
411
+ const earlyStopCount = throughputAttempts.filter((a) => a.early_stop || (a.done_reason && a.done_reason !== "length")).length;
372
412
  const checkAttempts = cases
373
413
  .filter((c) => c.role === "check")
374
414
  .flatMap((c) => c.attempts)
@@ -376,9 +416,12 @@ function summarizeModel(cases, cold, selfJudge) {
376
416
  const passed = checkAttempts.filter((a) => a.quality?.pass).length;
377
417
  const passRate = checkAttempts.length > 0 ? passed / checkAttempts.length : null;
378
418
  return {
379
- tok_s_median: median(toks) ?? null,
380
- tok_s_min: toks.length ? Math.min(...toks) : null,
381
- tok_s_max: toks.length ? Math.max(...toks) : null,
419
+ tok_s_median: peak.median,
420
+ tok_s_min: peak.min,
421
+ tok_s_max: peak.max,
422
+ tok_s_long_median: long.median,
423
+ tok_s_long_min: long.min,
424
+ tok_s_long_max: long.max,
382
425
  early_stop_count: earlyStopCount,
383
426
  pass_rate: passRate,
384
427
  load_ms: cold && cold.error == null && cold.load_ms > 0 ? cold.load_ms : cold?.error ? null : cold?.load_ms ?? null,
@@ -394,17 +437,22 @@ function printTable(machine, suite, runs, revision, compKey, models, skipped, co
394
437
  lines.push("Cold-load: on");
395
438
  if (hot)
396
439
  lines.push("Hot: first throughput run discarded; models stay loaded");
440
+ if (suite === "full")
441
+ lines.push("full: tok/s long is one 1024-token prose generation");
397
442
  lines.push("");
443
+ const showLong = suite === "full";
398
444
  const header = coldLoad
399
445
  ? pad("Model", MODEL_COL) +
400
446
  pad("tok/s (med)", 12) +
401
447
  pad("spread", 12) +
448
+ (showLong ? pad("tok/s long", 12) : "") +
402
449
  pad("load", 8) +
403
450
  pad("pass", 8) +
404
451
  "notes"
405
452
  : pad("Model", MODEL_COL) +
406
453
  pad("tok/s (med)", 12) +
407
454
  pad("spread", 12) +
455
+ (showLong ? pad("tok/s long", 12) : "") +
408
456
  pad("pass", 8) +
409
457
  "notes";
410
458
  lines.push(header);
@@ -438,6 +486,7 @@ function printTable(machine, suite, runs, revision, compKey, models, skipped, co
438
486
  const spread = m.summary.tok_s_min != null && m.summary.tok_s_max != null
439
487
  ? `${m.summary.tok_s_min.toFixed(1)}–${m.summary.tok_s_max.toFixed(1)}`
440
488
  : "—";
489
+ const tokLong = m.summary.tok_s_long_median != null ? m.summary.tok_s_long_median.toFixed(1) : "—";
441
490
  const pass = m.summary.pass_rate != null
442
491
  ? `${Math.round(m.summary.pass_rate * checkDenom(m))}/${checkDenom(m)}`
443
492
  : "—";
@@ -457,12 +506,14 @@ function printTable(machine, suite, runs, revision, compKey, models, skipped, co
457
506
  ? pad(m.name, MODEL_COL) +
458
507
  pad(tok, 12) +
459
508
  pad(spread, 12) +
509
+ (showLong ? pad(tokLong, 12) : "") +
460
510
  pad(load, 8) +
461
511
  pad(pass, 8) +
462
512
  notes.join("; ")
463
513
  : pad(m.name, MODEL_COL) +
464
514
  pad(tok, 12) +
465
515
  pad(spread, 12) +
516
+ (showLong ? pad(tokLong, 12) : "") +
466
517
  pad(pass, 8) +
467
518
  notes.join("; ");
468
519
  lines.push(row);
@@ -575,10 +626,13 @@ export async function main() {
575
626
  hot: parsed.hot,
576
627
  });
577
628
  const suiteCases = getSuiteCases(parsed.suite);
578
- const throughputReps = parsed.runs + (parsed.hot ? 1 : 0);
579
629
  const caseCount = selected.length *
580
- (suiteCases.filter((c) => c.role !== "throughput").length +
581
- suiteCases.filter((c) => c.role === "throughput").length * throughputReps);
630
+ suiteCases.reduce((n, c) => n + caseReps(c, parsed.runs, parsed.hot), 0);
631
+ const worstCaseMs = selected.length *
632
+ suiteCases.reduce((n, c) => {
633
+ const pin = pinnedNumPredict(c, parsed.throughputNumPredict);
634
+ return n + caseReps(c, parsed.runs, parsed.hot) * attemptTimeoutMs(pin);
635
+ }, 0);
582
636
  if (!parsed.json) {
583
637
  const skipBits = [
584
638
  skipped.filter((s) => s.reason === "non-completion").length
@@ -593,7 +647,10 @@ export async function main() {
593
647
  ` × suite=${parsed.suite} × runs=${parsed.runs}` +
594
648
  (parsed.hot ? " (hot: +1 discarded)" : ""));
595
649
  console.error(`Cases: ~${caseCount} chat calls timeout: ${BENCH_TIMEOUT_MS}ms/case` +
596
- ` worst-case ceiling: ~${Math.ceil((caseCount * BENCH_TIMEOUT_MS) / 60000)}m if every call times out`);
650
+ (parsed.suite === "full"
651
+ ? ` (long case ≥${attemptTimeoutMs(LONG_THROUGHPUT_NUM_PREDICT)}ms)`
652
+ : "") +
653
+ ` worst-case ceiling: ~${Math.ceil(worstCaseMs / 60000)}m if every call times out`);
597
654
  if (!parsed.hot && !isLoopback(host) && (selected.length >= 2 || parsed.coldLoad)) {
598
655
  const bits = [
599
656
  selected.length >= 2 ? "unloads each model before the next" : "",
@@ -655,8 +712,8 @@ export async function main() {
655
712
  let modelError;
656
713
  for (const caseDef of suiteCases) {
657
714
  const attempts = [];
658
- const discardFirst = parsed.hot && caseDef.role === "throughput";
659
- const reps = caseDef.role === "throughput" ? parsed.runs + (discardFirst ? 1 : 0) : 1;
715
+ const discardFirst = discardFirstThroughput(caseDef, parsed.hot);
716
+ const reps = caseReps(caseDef, parsed.runs, parsed.hot);
660
717
  for (let i = 0; i < reps; i += 1) {
661
718
  const discarded = discardFirst && i === 0;
662
719
  const attempt = await runCaseAttempt({
@@ -748,6 +805,7 @@ export async function main() {
748
805
  temperature: 0,
749
806
  seed: 0,
750
807
  throughput_num_predict: parsed.throughputNumPredict,
808
+ throughput_long_num_predict: parsed.suite === "full" ? LONG_THROUGHPUT_NUM_PREDICT : null,
751
809
  num_ctx: baseSettings.num_ctx ?? null,
752
810
  warmup: parsed.warmup,
753
811
  hot: parsed.hot,
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,16 +7,18 @@
7
7
  * ollanet chats --id a1b2c3d4e5f6
8
8
  */
9
9
  import { loadChat, listChats, responsesDir, } from "./chat-store.js";
10
- import { takeFlag } from "./argv.js";
11
- function usage() {
12
- 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>]
13
13
 
14
14
  Lists transcripts in ${responsesDir()}.
15
15
 
16
16
  Options:
17
17
  --json Machine-readable output
18
- --id <hash> Show one chat (messages included with --json, preview otherwise)`);
19
- process.exit(1);
18
+ --id <hash> Show one chat (messages included with --json, preview otherwise)`;
19
+ }
20
+ function usage() {
21
+ failUsage(helpText());
20
22
  }
21
23
  function parseArgs(argv) {
22
24
  const args = [...argv];
@@ -26,8 +28,8 @@ function parseArgs(argv) {
26
28
  const arg = args.shift();
27
29
  if (arg === "--")
28
30
  continue;
29
- if (arg === "--help" || arg === "-h")
30
- usage();
31
+ if (isHelpFlag(arg))
32
+ printHelp(helpText());
31
33
  if (arg === "--json") {
32
34
  json = true;
33
35
  continue;
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
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { configFromPartial, loadConfig, machineSettingsForHost, mergeSettings, settingsFromEnv, } from "./config.js";
8
8
  import { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
9
- import { consumeSettingsFlag, takeFlag, takeValue } from "./argv.js";
9
+ import { consumeSettingsFlag, failUsage, isHelpFlag, printHelp, takeFlag, takeValue } from "./argv.js";
10
10
  import { resolveTarget } from "./target.js";
11
11
  import { ollamaChat, ollamaTags, ollamaUnload, waitUntilUnloaded, } from "./ollama-chat.js";
12
12
  import { assemblePrompt, readPromptFile } from "./prompt-input.js";
@@ -23,8 +23,8 @@ function compareTimeoutMs() {
23
23
  function unloadWaitMs() {
24
24
  return envInt("OLLAMA_COMPARE_UNLOAD_WAIT_MS", 60_000);
25
25
  }
26
- function usage() {
27
- console.error(`Usage:
26
+ export function helpText() {
27
+ return `Usage:
28
28
  ollanet compare <machine> <model> <model> [model...]
29
29
  ollanet compare <machine> <model> <model> [model...] --prompt <text>
30
30
  ollanet compare <machine> <model> <model> [model...] --file <path.txt|.md>
@@ -49,8 +49,10 @@ Options:
49
49
  --think / --no-think
50
50
  --unload Unload each model before the next (fairer tok/s)
51
51
  --no-save Do not write compares/*
52
- --json Emit the full record on stdout`);
53
- process.exit(1);
52
+ --json Emit the full record on stdout`;
53
+ }
54
+ function usage() {
55
+ failUsage(helpText());
54
56
  }
55
57
  function parseArgs(argv) {
56
58
  const args = [...argv];
@@ -65,8 +67,8 @@ function parseArgs(argv) {
65
67
  const arg = args.shift();
66
68
  if (arg === "--")
67
69
  continue;
68
- if (arg === "--help" || arg === "-h")
69
- usage();
70
+ if (isHelpFlag(arg))
71
+ printHelp(helpText());
70
72
  if (arg === "--json") {
71
73
  json = true;
72
74
  continue;
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/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";
@@ -199,7 +200,7 @@ const TOOLS = [
199
200
  {
200
201
  name: "ollanet_ps",
201
202
  description: "List models currently loaded in VRAM. Optional machine; omit to probe every discovered host. " +
202
- "scan is on-disk inventory; ps is what is resident right now.",
203
+ "scan is on-disk inventory; ps is what is resident right now, with CPU/GPU %.",
203
204
  inputSchema: {
204
205
  type: "object",
205
206
  properties: {
@@ -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 });
@@ -74,6 +74,8 @@ export declare function ollamaVersion(baseUrl: string, timeoutMs: number): Promi
74
74
  export interface PsModel {
75
75
  name?: string;
76
76
  model?: string;
77
+ /** Total resident bytes (VRAM + CPU/RAM). */
78
+ size?: number;
77
79
  size_vram?: number;
78
80
  context_length?: number;
79
81
  digest?: string;
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
@@ -17,14 +17,14 @@ import { configPath, defaultModelForHost, loadConfig, machineSettingsForHost, me
17
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, takeFlag, takeValue } from "./argv.js";
20
+ import { consumeSettingsFlag, failUsage, isHelpFlag, printHelp, takeFlag, takeValue } from "./argv.js";
21
21
  import { listTargets, resolveTarget } from "./target.js";
22
22
  /** Prompt/chat HTTP timeout (ms). 0 = no timeout. Separate from scan's OLLAMA_TIMEOUT_MS. */
23
23
  function promptTimeoutMs() {
24
24
  return envInt("OLLAMA_PROMPT_TIMEOUT_MS", 600_000);
25
25
  }
26
- function usage() {
27
- console.error(`Usage:
26
+ export function helpText() {
27
+ return `Usage:
28
28
  ollanet prompt <machine> [model] <prompt...>
29
29
  ollanet prompt --chat <hash> <prompt...>
30
30
  ollanet prompt <machine> --chat <hash> <prompt...>
@@ -57,8 +57,10 @@ Options:
57
57
  --no-think Disable thinking (default) so tokens go to the reply
58
58
  --file <path> Prompt from a .txt or .md file (joins argv / stdin)
59
59
  --no-stream Wait for the full response
60
- --json Emit final response JSON (implies --no-stream)`);
61
- process.exit(1);
60
+ --json Emit final response JSON (implies --no-stream)`;
61
+ }
62
+ function usage() {
63
+ failUsage(helpText());
62
64
  }
63
65
  /**
64
66
  * Heuristic for an optional positional model token.
@@ -136,8 +138,8 @@ function parseArgs(argv) {
136
138
  const arg = args.shift();
137
139
  if (arg === "--")
138
140
  continue;
139
- if (arg === "--help" || arg === "-h")
140
- usage();
141
+ if (isHelpFlag(arg))
142
+ printHelp(helpText());
141
143
  if (arg === "--no-stream") {
142
144
  stream = false;
143
145
  continue;
package/dist/ps.d.ts CHANGED
@@ -5,9 +5,15 @@
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;
11
+ size?: number;
10
12
  size_vram?: number;
13
+ cpu_percent?: number;
14
+ gpu_percent?: number;
15
+ /** Same wording as `ollama ps` PROCESSOR (e.g. "100% GPU", "83%/17% CPU/GPU"). */
16
+ processor?: string;
11
17
  context_length?: number;
12
18
  digest?: string;
13
19
  tuned: boolean;
@@ -27,5 +33,11 @@ export interface PsOptions {
27
33
  export interface PsResult {
28
34
  hosts: LoadedHost[];
29
35
  }
36
+ /** Match `ollama ps` PROCESSOR: CPU% from (size - size_vram) / size. */
37
+ export declare function processorSplit(size?: number, sizeVram?: number): {
38
+ cpu_percent?: number;
39
+ gpu_percent?: number;
40
+ processor?: string;
41
+ };
30
42
  export declare function listLoaded(opts?: PsOptions): Promise<PsResult>;
31
43
  export declare function main(): Promise<void>;
package/dist/ps.js CHANGED
@@ -7,14 +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 { takeFlag } from "./argv.js";
10
+ import { failUsage, isHelpFlag, printHelp, takeFlag } from "./argv.js";
11
11
  import { resolveTarget } from "./target.js";
12
12
  import { looksTuned } from "./tuned.js";
13
13
  function psTimeoutMs() {
14
14
  return envInt("OLLAMA_PS_TIMEOUT_MS", 10_000);
15
15
  }
16
- function usage() {
17
- console.error(`Usage:
16
+ export function helpText() {
17
+ return `Usage:
18
18
  ollanet ps [machine]
19
19
  ollanet ps --machine <name>
20
20
 
@@ -23,12 +23,15 @@ Examples:
23
23
  ollanet ps studio
24
24
  ollanet ps studio --json
25
25
 
26
- scan lists models on disk. ps lists what is loaded in VRAM right now.
26
+ scan lists models on disk. ps lists what is loaded and the CPU/GPU split
27
+ (same rule as ollama ps: size_vram / size).
27
28
 
28
29
  Options:
29
30
  --machine <name> One host (default: every discovered host)
30
- --json Emit result JSON on stdout`);
31
- process.exit(1);
31
+ --json Emit result JSON on stdout`;
32
+ }
33
+ function usage() {
34
+ failUsage(helpText());
32
35
  }
33
36
  function parseArgs(argv) {
34
37
  const args = [...argv];
@@ -39,8 +42,8 @@ function parseArgs(argv) {
39
42
  const arg = args.shift();
40
43
  if (arg === "--")
41
44
  continue;
42
- if (arg === "--help" || arg === "-h")
43
- usage();
45
+ if (isHelpFlag(arg))
46
+ printHelp(helpText());
44
47
  if (arg === "--json") {
45
48
  json = true;
46
49
  continue;
@@ -74,11 +77,31 @@ function formatBytes(bytes) {
74
77
  }
75
78
  return `${value.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
76
79
  }
80
+ /** Match `ollama ps` PROCESSOR: CPU% from (size - size_vram) / size. */
81
+ export function processorSplit(size, sizeVram) {
82
+ const total = size ?? 0;
83
+ const vram = sizeVram ?? 0;
84
+ if (vram === 0 && total === 0)
85
+ return {};
86
+ if (vram === 0)
87
+ return { cpu_percent: 100, gpu_percent: 0, processor: "100% CPU" };
88
+ if (total > 0 && vram === total) {
89
+ return { cpu_percent: 0, gpu_percent: 100, processor: "100% GPU" };
90
+ }
91
+ if (vram > total || total === 0)
92
+ return { processor: "Unknown" };
93
+ const cpu = Math.round(((total - vram) / total) * 100);
94
+ const gpu = 100 - cpu;
95
+ return { cpu_percent: cpu, gpu_percent: gpu, processor: `${cpu}%/${gpu}% CPU/GPU` };
96
+ }
77
97
  function toLoaded(m) {
78
98
  const name = (m.name ?? m.model ?? "").trim();
99
+ const split = processorSplit(m.size, m.size_vram);
79
100
  return {
80
101
  name,
102
+ size: m.size,
81
103
  size_vram: m.size_vram,
104
+ ...split,
82
105
  context_length: m.context_length,
83
106
  digest: m.digest,
84
107
  tuned: looksTuned(name),
@@ -141,7 +164,7 @@ function printPs(result) {
141
164
  const vram = formatBytes(m.size_vram);
142
165
  const ctx = m.context_length != null ? `ctx ${m.context_length}` : "";
143
166
  const tag = m.tuned ? " [tuned]" : "";
144
- const meta = [vram ? `VRAM ${vram}` : "", ctx].filter(Boolean).join(" ");
167
+ const meta = [vram ? `VRAM ${vram}` : "", m.processor, ctx].filter(Boolean).join(" ");
145
168
  process.stdout.write(meta ? ` ${m.name} ${meta}${tag}\n` : ` ${m.name}${tag}\n`);
146
169
  }
147
170
  }
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,14 +8,14 @@
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
+ import { failUsage, isHelpFlag, printHelp, takeFlag } from "./argv.js";
12
12
  import { resolveTarget } from "./target.js";
13
13
  /** Pull HTTP timeout (ms). 0 = none. Large models can take hours. */
14
14
  function pullTimeoutMs() {
15
15
  return envInt("OLLAMA_PULL_TIMEOUT_MS", 0);
16
16
  }
17
- function usage() {
18
- console.error(`Usage:
17
+ export function helpText() {
18
+ return `Usage:
19
19
  ollanet pull <machine> <model>
20
20
  ollanet pull --machine <name> --model <name>
21
21
 
@@ -33,8 +33,10 @@ Options:
33
33
  --model <name> Model to pull (e.g. gemma3:12b)
34
34
  --insecure Allow HTTP / self-signed model registries
35
35
  --no-stream Wait for a single final response (no progress chunks)
36
- --json Emit result JSON on stdout`);
37
- process.exit(1);
36
+ --json Emit result JSON on stdout`;
37
+ }
38
+ function usage() {
39
+ failUsage(helpText());
38
40
  }
39
41
  function parseArgs(argv) {
40
42
  const args = [...argv];
@@ -48,8 +50,8 @@ function parseArgs(argv) {
48
50
  const arg = args.shift();
49
51
  if (arg === "--")
50
52
  continue;
51
- if (arg === "--help" || arg === "-h")
52
- usage();
53
+ if (isHelpFlag(arg))
54
+ printHelp(helpText());
53
55
  if (arg === "--insecure") {
54
56
  insecure = true;
55
57
  continue;
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,13 +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 { takeFlag } from "./argv.js";
9
+ import { failUsage, isHelpFlag, printHelp, takeFlag } from "./argv.js";
10
10
  import { resolveTarget } from "./target.js";
11
11
  function rmTimeoutMs() {
12
12
  return envInt("OLLAMA_RM_TIMEOUT_MS", 30_000);
13
13
  }
14
- function usage() {
15
- console.error(`Usage:
14
+ export function helpText() {
15
+ return `Usage:
16
16
  ollanet rm <machine> <model> --yes
17
17
  ollanet rm --machine <name> --model <name> --yes
18
18
 
@@ -27,8 +27,10 @@ Options:
27
27
  --machine <name> Host (discovered name, MagicDNS, hostname, or IP[:port])
28
28
  --model <name> Model to delete
29
29
  --yes Required unless stdin is a TTY and you confirm
30
- --json Emit result JSON on stdout`);
31
- process.exit(1);
30
+ --json Emit result JSON on stdout`;
31
+ }
32
+ function usage() {
33
+ failUsage(helpText());
32
34
  }
33
35
  function parseArgs(argv) {
34
36
  const args = [...argv];
@@ -41,8 +43,8 @@ function parseArgs(argv) {
41
43
  const arg = args.shift();
42
44
  if (arg === "--")
43
45
  continue;
44
- if (arg === "--help" || arg === "-h")
45
- usage();
46
+ if (isHelpFlag(arg))
47
+ printHelp(helpText());
46
48
  if (arg === "--yes" || arg === "-y") {
47
49
  yes = true;
48
50
  continue;
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,14 +5,14 @@
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
+ import { failUsage, isHelpFlag, printHelp, takeFlag } from "./argv.js";
9
9
  import { resolveTarget } from "./target.js";
10
10
  import { looksTuned } from "./tuned.js";
11
11
  function showTimeoutMs() {
12
12
  return envInt("OLLAMA_SHOW_TIMEOUT_MS", 30_000);
13
13
  }
14
- function usage() {
15
- console.error(`Usage:
14
+ export function helpText() {
15
+ return `Usage:
16
16
  ollanet show <machine> <model>
17
17
  ollanet show --machine <name> --model <name>
18
18
 
@@ -26,8 +26,10 @@ Tuned Finetuna-style names are marked [tuned].
26
26
  Options:
27
27
  --machine <name> Host (discovered name, MagicDNS, hostname, or IP[:port])
28
28
  --model <name> Model to inspect
29
- --json Emit result JSON on stdout`);
30
- process.exit(1);
29
+ --json Emit result JSON on stdout`;
30
+ }
31
+ function usage() {
32
+ failUsage(helpText());
31
33
  }
32
34
  function parseArgs(argv) {
33
35
  const args = [...argv];
@@ -39,8 +41,8 @@ function parseArgs(argv) {
39
41
  const arg = args.shift();
40
42
  if (arg === "--")
41
43
  continue;
42
- if (arg === "--help" || arg === "-h")
43
- usage();
44
+ if (isHelpFlag(arg))
45
+ printHelp(helpText());
44
46
  if (arg === "--json") {
45
47
  json = true;
46
48
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ollanet",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
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": {