ollanet 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -9
- package/dist/bench-suite.d.ts +2 -0
- package/dist/bench-suite.js +1 -0
- package/dist/bench.js +54 -18
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +52 -2
- package/dist/compare-store.d.ts +8 -0
- package/dist/compare-store.js +55 -0
- package/dist/compare.d.ts +52 -0
- package/dist/compare.js +338 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.js +9 -1
- package/dist/mcp.js +235 -3
- package/dist/ollama-chat.d.ts +46 -2
- package/dist/ollama-chat.js +121 -2
- package/dist/paths.d.ts +4 -0
- package/dist/paths.js +8 -0
- package/dist/prompt-input.d.ts +7 -0
- package/dist/prompt-input.js +36 -0
- package/dist/prompt.js +28 -5
- package/dist/ps.d.ts +31 -0
- package/dist/ps.js +165 -0
- package/dist/pull.d.ts +36 -0
- package/dist/pull.js +198 -0
- package/dist/rm.d.ts +22 -0
- package/dist/rm.js +134 -0
- package/dist/scan-store.d.ts +15 -0
- package/dist/scan-store.js +41 -0
- package/dist/scan.d.ts +14 -1
- package/dist/scan.js +76 -42
- package/dist/show.d.ts +28 -0
- package/dist/show.js +131 -0
- package/dist/target.d.ts +4 -0
- package/dist/target.js +18 -0
- package/dist/tuned.d.ts +8 -0
- package/dist/tuned.js +17 -0
- package/package.json +8 -2
package/dist/compare.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Head-to-head: one prompt, 2–5 models on the same host.
|
|
3
|
+
*
|
|
4
|
+
* ollanet compare studio gemma3:12b llama3.2:3b --prompt "Explain MagicDNS"
|
|
5
|
+
* ollanet compare studio m1 m2 --file task.md
|
|
6
|
+
*/
|
|
7
|
+
import { configFromPartial, loadConfig, machineSettingsForHost, mergeSettings, settingsFromEnv, } from "./config.js";
|
|
8
|
+
import { discoverHosts, envInt, ollamaBaseUrl, resolveHost, shortName } from "./hosts.js";
|
|
9
|
+
import { ollamaChat, ollamaTags, ollamaUnload, waitUntilUnloaded, } from "./ollama-chat.js";
|
|
10
|
+
import { assemblePrompt, readPromptFile } from "./prompt-input.js";
|
|
11
|
+
import { comparesDir, newCompareId, saveCompare } from "./compare-store.js";
|
|
12
|
+
const MIN_MODELS = 2;
|
|
13
|
+
const MAX_MODELS = 5;
|
|
14
|
+
/** Used when --prompt, --file, and stdin are all omitted. */
|
|
15
|
+
export const DEFAULT_COMPARE_PROMPT = "In 6–10 sentences, explain how you’d pick an Ollama host on a mixed LAN/Tailscale mesh " +
|
|
16
|
+
"for a 12B chat model. Then give one failure mode (wrong host, cold load, or VRAM spill) " +
|
|
17
|
+
"and how you’d confirm it. Be specific.";
|
|
18
|
+
function compareTimeoutMs() {
|
|
19
|
+
return envInt("OLLAMA_COMPARE_TIMEOUT_MS", envInt("OLLAMA_PROMPT_TIMEOUT_MS", 600_000));
|
|
20
|
+
}
|
|
21
|
+
function unloadWaitMs() {
|
|
22
|
+
return envInt("OLLAMA_COMPARE_UNLOAD_WAIT_MS", 60_000);
|
|
23
|
+
}
|
|
24
|
+
function usage() {
|
|
25
|
+
console.error(`Usage:
|
|
26
|
+
ollanet compare <machine> <model> <model> [model...]
|
|
27
|
+
ollanet compare <machine> <model> <model> [model...] --prompt <text>
|
|
28
|
+
ollanet compare <machine> <model> <model> [model...] --file <path.txt|.md>
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
ollanet compare studio gemma3:12b llama3.2:3b
|
|
32
|
+
ollanet compare studio gemma3:12b llama3.2:3b --prompt "Explain MagicDNS"
|
|
33
|
+
ollanet compare studio gemma3:12b qwen2.5:7b llama3.2:3b --file ./task.md
|
|
34
|
+
|
|
35
|
+
Runs the same prompt on 2–5 models on one host. Prints a summary table and
|
|
36
|
+
writes compares/<id>.md plus .json (prompt, replies, stats). Not a bench suite.
|
|
37
|
+
Omit --prompt/--file to use the built-in mesh-host tasting prompt.
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
--prompt <text> Prompt text (default: built-in mesh-host prompt)
|
|
41
|
+
--file <path> Prompt from a .txt or .md file
|
|
42
|
+
--system <text> System prompt
|
|
43
|
+
--temperature <n>
|
|
44
|
+
--num-predict <n>
|
|
45
|
+
--num-ctx <n>
|
|
46
|
+
--keep-alive <value>
|
|
47
|
+
--think / --no-think
|
|
48
|
+
--unload Unload each model before the next (fairer tok/s)
|
|
49
|
+
--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;
|
|
60
|
+
}
|
|
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;
|
|
68
|
+
}
|
|
69
|
+
function parseArgs(argv) {
|
|
70
|
+
const args = [...argv];
|
|
71
|
+
let prompt;
|
|
72
|
+
let file;
|
|
73
|
+
let json = false;
|
|
74
|
+
let save = true;
|
|
75
|
+
let unload = false;
|
|
76
|
+
const settings = {};
|
|
77
|
+
const positional = [];
|
|
78
|
+
while (args.length > 0) {
|
|
79
|
+
const arg = args.shift();
|
|
80
|
+
if (arg === "--")
|
|
81
|
+
continue;
|
|
82
|
+
if (arg === "--help" || arg === "-h")
|
|
83
|
+
usage();
|
|
84
|
+
if (arg === "--json") {
|
|
85
|
+
json = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (arg === "--no-save") {
|
|
89
|
+
save = false;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (arg === "--unload") {
|
|
93
|
+
unload = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
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");
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (arg === "--system" || arg.startsWith("--system=")) {
|
|
113
|
+
settings.system = arg.includes("=")
|
|
114
|
+
? arg.slice("--system=".length)
|
|
115
|
+
: takeValue(args, "--system");
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
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;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (arg.startsWith("-")) {
|
|
141
|
+
console.error(`Unknown flag: ${arg}`);
|
|
142
|
+
usage();
|
|
143
|
+
}
|
|
144
|
+
positional.push(arg);
|
|
145
|
+
}
|
|
146
|
+
const machine = positional[0];
|
|
147
|
+
const models = positional.slice(1);
|
|
148
|
+
return { machine, models, prompt, file, json, save, unload, settings };
|
|
149
|
+
}
|
|
150
|
+
function tokPerSec(chunk) {
|
|
151
|
+
if (chunk.eval_count == null || chunk.eval_duration == null || chunk.eval_duration <= 0) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
return chunk.eval_count / (chunk.eval_duration / 1e9);
|
|
155
|
+
}
|
|
156
|
+
function resolveModelName(available, query) {
|
|
157
|
+
const q = query.toLowerCase();
|
|
158
|
+
const exact = available.find((n) => n.toLowerCase() === q);
|
|
159
|
+
if (exact)
|
|
160
|
+
return exact;
|
|
161
|
+
const prefix = available.filter((n) => n.toLowerCase().startsWith(`${q}:`) || q.startsWith(`${n.toLowerCase()}:`));
|
|
162
|
+
return prefix.length === 1 ? prefix[0] : undefined;
|
|
163
|
+
}
|
|
164
|
+
function printSummary(record) {
|
|
165
|
+
const nameWidth = Math.max(8, ...record.results.map((r) => r.model.length));
|
|
166
|
+
process.stderr.write(`Compare on ${record.machine} ${record.results.length} models ${record.prompt.length} char prompt\n`);
|
|
167
|
+
process.stderr.write(`${"model".padEnd(nameWidth)} ${"tok/s".padStart(7)} ${"tokens".padStart(6)} ${"wall".padStart(7)} done\n`);
|
|
168
|
+
for (const r of record.results) {
|
|
169
|
+
if (r.error) {
|
|
170
|
+
process.stderr.write(`${r.model.padEnd(nameWidth)} ERROR ${r.error}\n`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const toks = r.tok_s != null ? r.tok_s.toFixed(1) : "—";
|
|
174
|
+
const tokens = r.eval_count != null ? String(r.eval_count) : "—";
|
|
175
|
+
const wall = r.wall_ms != null ? `${(r.wall_ms / 1000).toFixed(2)}s` : "—";
|
|
176
|
+
process.stderr.write(`${r.model.padEnd(nameWidth)} ${toks.padStart(7)} ${tokens.padStart(6)} ${wall.padStart(7)} ${r.done_reason ?? "—"}\n`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
export async function runCompare(opts) {
|
|
180
|
+
const models = opts.models.map((m) => m.trim()).filter(Boolean);
|
|
181
|
+
if (models.length < MIN_MODELS || models.length > MAX_MODELS) {
|
|
182
|
+
throw new Error(`Compare needs ${MIN_MODELS}–${MAX_MODELS} models (got ${models.length}).`);
|
|
183
|
+
}
|
|
184
|
+
let fileText;
|
|
185
|
+
if (opts.file) {
|
|
186
|
+
fileText = await readPromptFile(opts.file);
|
|
187
|
+
}
|
|
188
|
+
const assembled = assemblePrompt({ argv: opts.prompt, file: fileText });
|
|
189
|
+
const defaulted = !assembled;
|
|
190
|
+
const prompt = assembled || DEFAULT_COMPARE_PROMPT;
|
|
191
|
+
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
|
+
}
|
|
200
|
+
const baseUrl = ollamaBaseUrl(host);
|
|
201
|
+
const timeoutMs = opts.timeoutMs ?? compareTimeoutMs();
|
|
202
|
+
let available = null;
|
|
203
|
+
try {
|
|
204
|
+
available = (await ollamaTags(baseUrl, Math.min(timeoutMs || 5000, 10_000))).map((t) => t.name);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
available = null;
|
|
208
|
+
}
|
|
209
|
+
const resolved = [];
|
|
210
|
+
for (const name of models) {
|
|
211
|
+
if (available) {
|
|
212
|
+
const hit = resolveModelName(available, name);
|
|
213
|
+
if (!hit) {
|
|
214
|
+
throw new Error(`Unknown model "${name}" on ${shortName(host)}. Available: ${available.join(", ")}`);
|
|
215
|
+
}
|
|
216
|
+
resolved.push(hit);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
resolved.push(name);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const settings = mergeSettings({ think: false }, config.defaults, machineSettingsForHost(config, host), settingsFromEnv(), opts.settings);
|
|
223
|
+
const quiet = opts.quiet === true || opts.writeStdout === false;
|
|
224
|
+
const results = [];
|
|
225
|
+
for (let i = 0; i < resolved.length; i += 1) {
|
|
226
|
+
const model = resolved[i];
|
|
227
|
+
if (!quiet) {
|
|
228
|
+
process.stderr.write(`→ ${model} (${i + 1}/${resolved.length})\n`);
|
|
229
|
+
}
|
|
230
|
+
const started = Date.now();
|
|
231
|
+
try {
|
|
232
|
+
const { content, thinking, chunk } = await ollamaChat({
|
|
233
|
+
baseUrl,
|
|
234
|
+
model,
|
|
235
|
+
messages: [
|
|
236
|
+
...(settings.system ? [{ role: "system", content: settings.system }] : []),
|
|
237
|
+
{ role: "user", content: prompt },
|
|
238
|
+
],
|
|
239
|
+
stream: false,
|
|
240
|
+
writeStdout: false,
|
|
241
|
+
timeoutMs,
|
|
242
|
+
settings,
|
|
243
|
+
});
|
|
244
|
+
results.push({
|
|
245
|
+
model,
|
|
246
|
+
content,
|
|
247
|
+
thinking: thinking || undefined,
|
|
248
|
+
tok_s: tokPerSec(chunk),
|
|
249
|
+
eval_count: chunk.eval_count,
|
|
250
|
+
eval_duration: chunk.eval_duration,
|
|
251
|
+
total_duration: chunk.total_duration,
|
|
252
|
+
load_duration: chunk.load_duration,
|
|
253
|
+
wall_ms: Date.now() - started,
|
|
254
|
+
done_reason: chunk.done_reason,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
results.push({
|
|
259
|
+
model,
|
|
260
|
+
wall_ms: Date.now() - started,
|
|
261
|
+
error: err instanceof Error ? err.message : String(err),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (opts.unload && i < resolved.length - 1) {
|
|
265
|
+
try {
|
|
266
|
+
await ollamaUnload(baseUrl, model, Math.min(timeoutMs || 30_000, 30_000));
|
|
267
|
+
await waitUntilUnloaded(baseUrl, model, unloadWaitMs());
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Fairness hint only — do not fail the compare.
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const record = {
|
|
275
|
+
id: newCompareId(),
|
|
276
|
+
created_at: new Date().toISOString(),
|
|
277
|
+
machine: shortName(host),
|
|
278
|
+
endpoint: baseUrl,
|
|
279
|
+
prompt,
|
|
280
|
+
default_prompt: defaulted,
|
|
281
|
+
system: settings.system,
|
|
282
|
+
results,
|
|
283
|
+
};
|
|
284
|
+
if (opts.save !== false) {
|
|
285
|
+
record.files = await saveCompare(record);
|
|
286
|
+
}
|
|
287
|
+
return record;
|
|
288
|
+
}
|
|
289
|
+
export async function main() {
|
|
290
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
291
|
+
if (!parsed.machine) {
|
|
292
|
+
console.error("Machine is required.");
|
|
293
|
+
usage();
|
|
294
|
+
}
|
|
295
|
+
const { stdin: stdinStream } = await import("node:process");
|
|
296
|
+
let fromStdin = "";
|
|
297
|
+
if (!stdinStream.isTTY) {
|
|
298
|
+
const chunks = [];
|
|
299
|
+
for await (const chunk of stdinStream)
|
|
300
|
+
chunks.push(chunk);
|
|
301
|
+
fromStdin = Buffer.concat(chunks).toString("utf8").trim();
|
|
302
|
+
}
|
|
303
|
+
let fileText;
|
|
304
|
+
if (parsed.file) {
|
|
305
|
+
try {
|
|
306
|
+
fileText = await readPromptFile(parsed.file);
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
console.error(err instanceof Error ? err.message : err);
|
|
310
|
+
process.exitCode = 1;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const prompt = assemblePrompt({ argv: parsed.prompt, file: fileText, stdin: fromStdin });
|
|
315
|
+
const record = await runCompare({
|
|
316
|
+
machine: parsed.machine,
|
|
317
|
+
models: parsed.models,
|
|
318
|
+
prompt: prompt || undefined,
|
|
319
|
+
settings: parsed.settings,
|
|
320
|
+
save: parsed.save,
|
|
321
|
+
unload: parsed.unload,
|
|
322
|
+
writeStdout: !parsed.json,
|
|
323
|
+
quiet: parsed.json,
|
|
324
|
+
});
|
|
325
|
+
if (record.default_prompt) {
|
|
326
|
+
process.stderr.write("Using default compare prompt (pass --prompt or --file to override).\n");
|
|
327
|
+
}
|
|
328
|
+
if (parsed.json) {
|
|
329
|
+
process.stdout.write(`${JSON.stringify(record, null, 2)}\n`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
printSummary(record);
|
|
333
|
+
if (record.files) {
|
|
334
|
+
process.stderr.write(`saved ${record.files.md}\n`);
|
|
335
|
+
process.stderr.write(` ${record.files.json}\n`);
|
|
336
|
+
process.stderr.write(`compares dir: ${comparesDir()}\n`);
|
|
337
|
+
}
|
|
338
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,11 +8,26 @@
|
|
|
8
8
|
* const { servers } = await scanNetwork({ lanScan: false });
|
|
9
9
|
*/
|
|
10
10
|
export { scanNetwork } from "./scan.js";
|
|
11
|
-
export type { ScanOptions, ScanPayload, ScannedServer } from "./scan.js";
|
|
11
|
+
export type { ScanOptions, ScanPayload, ScannedServer, ScanFailure } from "./scan.js";
|
|
12
|
+
export { lastScan, lastScanPath, loadLastScan } from "./scan-store.js";
|
|
13
|
+
export type { StoredScan } from "./scan-store.js";
|
|
12
14
|
export { discoverHosts, resolveHost, ollamaBaseUrl, shortName, ollamaPort, } from "./hosts.js";
|
|
13
15
|
export type { DiscoverOptions, DiscoveryResult, HostTarget, HostSource, HostConfigEntry, } from "./hosts.js";
|
|
14
16
|
export { loadConfig, defaultModelForHost, mergeSettings, configFromPartial, } from "./config.js";
|
|
15
17
|
export type { AppConfig, DiscoveryConfig, GenerateSettings } from "./config.js";
|
|
16
18
|
export { runPrompt } from "./prompt.js";
|
|
17
19
|
export type { PromptRunOptions, PromptRunResult } from "./prompt.js";
|
|
18
|
-
export {
|
|
20
|
+
export { runCompare, DEFAULT_COMPARE_PROMPT } from "./compare.js";
|
|
21
|
+
export type { CompareOptions, CompareRecord, CompareModelResult } from "./compare.js";
|
|
22
|
+
export { assemblePrompt, readPromptFile } from "./prompt-input.js";
|
|
23
|
+
export { pullModel } from "./pull.js";
|
|
24
|
+
export type { PullOptions, PullResult } from "./pull.js";
|
|
25
|
+
export { showModel } from "./show.js";
|
|
26
|
+
export type { ShowOptions, ShowResult } from "./show.js";
|
|
27
|
+
export { removeModel } from "./rm.js";
|
|
28
|
+
export type { RemoveOptions, RemoveResult } from "./rm.js";
|
|
29
|
+
export { listLoaded } from "./ps.js";
|
|
30
|
+
export type { PsOptions, PsResult, LoadedHost, LoadedModel } from "./ps.js";
|
|
31
|
+
export { looksTuned } from "./tuned.js";
|
|
32
|
+
export { ollamaChat, ollamaTags, ollamaShow, ollamaPs, ollamaPsRequired, ollamaPull, ollamaDelete, } from "./ollama-chat.js";
|
|
33
|
+
export type { PullChunk, OllamaPullOptions, OllamaPullResult, OllamaShowInfo, } from "./ollama-chat.js";
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,15 @@
|
|
|
8
8
|
* const { servers } = await scanNetwork({ lanScan: false });
|
|
9
9
|
*/
|
|
10
10
|
export { scanNetwork } from "./scan.js";
|
|
11
|
+
export { lastScan, lastScanPath, loadLastScan } from "./scan-store.js";
|
|
11
12
|
export { discoverHosts, resolveHost, ollamaBaseUrl, shortName, ollamaPort, } from "./hosts.js";
|
|
12
13
|
export { loadConfig, defaultModelForHost, mergeSettings, configFromPartial, } from "./config.js";
|
|
13
14
|
export { runPrompt } from "./prompt.js";
|
|
14
|
-
export {
|
|
15
|
+
export { runCompare, DEFAULT_COMPARE_PROMPT } from "./compare.js";
|
|
16
|
+
export { assemblePrompt, readPromptFile } from "./prompt-input.js";
|
|
17
|
+
export { pullModel } from "./pull.js";
|
|
18
|
+
export { showModel } from "./show.js";
|
|
19
|
+
export { removeModel } from "./rm.js";
|
|
20
|
+
export { listLoaded } from "./ps.js";
|
|
21
|
+
export { looksTuned } from "./tuned.js";
|
|
22
|
+
export { ollamaChat, ollamaTags, ollamaShow, ollamaPs, ollamaPsRequired, ollamaPull, ollamaDelete, } from "./ollama-chat.js";
|
package/dist/mcp.js
CHANGED
|
@@ -12,8 +12,15 @@ import { createInterface } from "node:readline";
|
|
|
12
12
|
import { readFile } from "node:fs/promises";
|
|
13
13
|
import { listChats, loadChat } from "./chat-store.js";
|
|
14
14
|
import { projectPath } from "./paths.js";
|
|
15
|
+
import { runCompare } from "./compare.js";
|
|
16
|
+
import { listLoaded } from "./ps.js";
|
|
17
|
+
import { pullModel } from "./pull.js";
|
|
18
|
+
import { assemblePrompt, readPromptFile } from "./prompt-input.js";
|
|
15
19
|
import { runPrompt } from "./prompt.js";
|
|
20
|
+
import { removeModel } from "./rm.js";
|
|
21
|
+
import { lastScan } from "./scan-store.js";
|
|
16
22
|
import { scanNetwork } from "./scan.js";
|
|
23
|
+
import { showModel } from "./show.js";
|
|
17
24
|
// Pin to the oldest widely-supported MCP revision. Clients may request a newer
|
|
18
25
|
// date; we still answer with this — legal negotiation, max compatibility.
|
|
19
26
|
const PROTOCOL_VERSION = "2024-11-05";
|
|
@@ -34,6 +41,10 @@ const TOOLS = [
|
|
|
34
41
|
type: "boolean",
|
|
35
42
|
description: "Also probe offline Tailscale peers (default false)",
|
|
36
43
|
},
|
|
44
|
+
last: {
|
|
45
|
+
type: "boolean",
|
|
46
|
+
description: "Return the last saved scan without probing the network (default false)",
|
|
47
|
+
},
|
|
37
48
|
},
|
|
38
49
|
additionalProperties: false,
|
|
39
50
|
},
|
|
@@ -80,8 +91,123 @@ const TOOLS = [
|
|
|
80
91
|
type: "boolean",
|
|
81
92
|
description: "Persist the transcript (default true)",
|
|
82
93
|
},
|
|
94
|
+
file: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "Read prompt body from a local .txt or .md file (joined with prompt if both set)",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
additionalProperties: false,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "ollanet_compare",
|
|
104
|
+
description: "Head-to-head: run the same prompt on 2–5 models on one Ollama host. " +
|
|
105
|
+
"Returns a summary plus saved markdown/JSON paths. " +
|
|
106
|
+
"Omit prompt/file to use the built-in mesh-host tasting prompt. " +
|
|
107
|
+
"Machine is a discovered name, MagicDNS name, hostname, or IP[:port].",
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
machine: { type: "string", description: "Host name/IP" },
|
|
112
|
+
models: {
|
|
113
|
+
type: "array",
|
|
114
|
+
items: { type: "string" },
|
|
115
|
+
minItems: 2,
|
|
116
|
+
maxItems: 5,
|
|
117
|
+
description: "2–5 model names on that host",
|
|
118
|
+
},
|
|
119
|
+
prompt: {
|
|
120
|
+
type: "string",
|
|
121
|
+
description: "Prompt text (optional; default is the built-in mesh-host prompt)",
|
|
122
|
+
},
|
|
123
|
+
file: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "Read prompt from a local .txt or .md file",
|
|
126
|
+
},
|
|
127
|
+
system: { type: "string" },
|
|
128
|
+
temperature: { type: "number" },
|
|
129
|
+
num_predict: { type: "integer" },
|
|
130
|
+
num_ctx: { type: "integer" },
|
|
131
|
+
think: { type: "boolean" },
|
|
132
|
+
unload: {
|
|
133
|
+
type: "boolean",
|
|
134
|
+
description: "Unload each model before the next (fairer tok/s)",
|
|
135
|
+
},
|
|
136
|
+
save: { type: "boolean", description: "Write compares/*.md and .json (default true)" },
|
|
137
|
+
},
|
|
138
|
+
required: ["machine", "models"],
|
|
139
|
+
additionalProperties: false,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "ollanet_pull",
|
|
144
|
+
description: "Pull (or update) a model onto a reachable Ollama host. " +
|
|
145
|
+
"The named machine downloads from the registry onto its own disk — " +
|
|
146
|
+
"ollanet does not upload weights. Machine is a discovered name, MagicDNS name, hostname, or IP[:port].",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
machine: {
|
|
151
|
+
type: "string",
|
|
152
|
+
description: "Host name/IP that should download the model",
|
|
153
|
+
},
|
|
154
|
+
model: {
|
|
155
|
+
type: "string",
|
|
156
|
+
description: "Model to pull (e.g. gemma3:12b)",
|
|
157
|
+
},
|
|
158
|
+
insecure: {
|
|
159
|
+
type: "boolean",
|
|
160
|
+
description: "Allow HTTP / self-signed model registries (default false)",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
required: ["machine", "model"],
|
|
164
|
+
additionalProperties: false,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "ollanet_show",
|
|
169
|
+
description: "Inspect a model on a reachable Ollama host (Modelfile, parameters, capabilities). " +
|
|
170
|
+
"Marks Finetuna-style tuned names. Machine is a discovered name, MagicDNS name, hostname, or IP[:port].",
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: "object",
|
|
173
|
+
properties: {
|
|
174
|
+
machine: { type: "string", description: "Host name/IP" },
|
|
175
|
+
model: { type: "string", description: "Model to inspect" },
|
|
176
|
+
},
|
|
177
|
+
required: ["machine", "model"],
|
|
178
|
+
additionalProperties: false,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "ollanet_rm",
|
|
183
|
+
description: "Delete a model from a reachable Ollama host's disk. Destructive. " +
|
|
184
|
+
"Requires confirm=true. Machine is a discovered name, MagicDNS name, hostname, or IP[:port].",
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {
|
|
188
|
+
machine: { type: "string", description: "Host name/IP" },
|
|
189
|
+
model: { type: "string", description: "Model to delete" },
|
|
190
|
+
confirm: {
|
|
191
|
+
type: "boolean",
|
|
192
|
+
description: "Must be true to actually delete (default false)",
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
required: ["machine", "model", "confirm"],
|
|
196
|
+
additionalProperties: false,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "ollanet_ps",
|
|
201
|
+
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
|
+
inputSchema: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
machine: {
|
|
207
|
+
type: "string",
|
|
208
|
+
description: "Host name/IP (optional; omit for all discovered hosts)",
|
|
209
|
+
},
|
|
83
210
|
},
|
|
84
|
-
required: ["prompt"],
|
|
85
211
|
additionalProperties: false,
|
|
86
212
|
},
|
|
87
213
|
},
|
|
@@ -142,6 +268,13 @@ async function packageVersion() {
|
|
|
142
268
|
async function callTool(name, args) {
|
|
143
269
|
switch (name) {
|
|
144
270
|
case "ollanet_scan": {
|
|
271
|
+
if (args.last === true) {
|
|
272
|
+
const stored = await lastScan();
|
|
273
|
+
if (!stored) {
|
|
274
|
+
return textResult({ error: "No saved scan. Run ollanet_scan without last=true first." }, true);
|
|
275
|
+
}
|
|
276
|
+
return textResult(stored);
|
|
277
|
+
}
|
|
145
278
|
const payload = await scanNetwork({
|
|
146
279
|
lanScan: Boolean(args.lan),
|
|
147
280
|
includeOffline: Boolean(args.all),
|
|
@@ -149,9 +282,21 @@ async function callTool(name, args) {
|
|
|
149
282
|
return textResult(payload);
|
|
150
283
|
}
|
|
151
284
|
case "ollanet_prompt": {
|
|
152
|
-
|
|
285
|
+
let fileText = "";
|
|
286
|
+
if (typeof args.file === "string" && args.file.trim()) {
|
|
287
|
+
try {
|
|
288
|
+
fileText = await readPromptFile(args.file);
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
return textResult({ error: err instanceof Error ? err.message : String(err) }, true);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const prompt = assemblePrompt({
|
|
295
|
+
argv: typeof args.prompt === "string" ? args.prompt : "",
|
|
296
|
+
file: fileText,
|
|
297
|
+
});
|
|
153
298
|
if (!prompt.trim()) {
|
|
154
|
-
return textResult({ error: "prompt is required" }, true);
|
|
299
|
+
return textResult({ error: "prompt or file is required" }, true);
|
|
155
300
|
}
|
|
156
301
|
const settings = {};
|
|
157
302
|
if (typeof args.system === "string")
|
|
@@ -187,6 +332,93 @@ async function callTool(name, args) {
|
|
|
187
332
|
});
|
|
188
333
|
return textResult(result);
|
|
189
334
|
}
|
|
335
|
+
case "ollanet_compare": {
|
|
336
|
+
const machine = typeof args.machine === "string" ? args.machine : "";
|
|
337
|
+
const models = Array.isArray(args.models)
|
|
338
|
+
? args.models.filter((m) => typeof m === "string")
|
|
339
|
+
: [];
|
|
340
|
+
if (!machine.trim())
|
|
341
|
+
return textResult({ error: "machine is required" }, true);
|
|
342
|
+
if (models.length < 2 || models.length > 5) {
|
|
343
|
+
return textResult({ error: "models must be an array of 2–5 names" }, true);
|
|
344
|
+
}
|
|
345
|
+
let fileText = "";
|
|
346
|
+
if (typeof args.file === "string" && args.file.trim()) {
|
|
347
|
+
try {
|
|
348
|
+
fileText = await readPromptFile(args.file);
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
return textResult({ error: err instanceof Error ? err.message : String(err) }, true);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const prompt = assemblePrompt({
|
|
355
|
+
argv: typeof args.prompt === "string" ? args.prompt : "",
|
|
356
|
+
file: fileText,
|
|
357
|
+
});
|
|
358
|
+
const settings = {};
|
|
359
|
+
if (typeof args.system === "string")
|
|
360
|
+
settings.system = args.system;
|
|
361
|
+
if (typeof args.temperature === "number")
|
|
362
|
+
settings.temperature = args.temperature;
|
|
363
|
+
if (typeof args.num_predict === "number")
|
|
364
|
+
settings.num_predict = Math.trunc(args.num_predict);
|
|
365
|
+
if (typeof args.num_ctx === "number")
|
|
366
|
+
settings.num_ctx = Math.trunc(args.num_ctx);
|
|
367
|
+
if (typeof args.think === "boolean")
|
|
368
|
+
settings.think = args.think;
|
|
369
|
+
const result = await runCompare({
|
|
370
|
+
machine,
|
|
371
|
+
models,
|
|
372
|
+
prompt: prompt || undefined,
|
|
373
|
+
settings,
|
|
374
|
+
save: args.save === undefined ? true : Boolean(args.save),
|
|
375
|
+
unload: args.unload === true,
|
|
376
|
+
writeStdout: false,
|
|
377
|
+
quiet: true,
|
|
378
|
+
});
|
|
379
|
+
return textResult(result);
|
|
380
|
+
}
|
|
381
|
+
case "ollanet_pull": {
|
|
382
|
+
const machine = typeof args.machine === "string" ? args.machine : "";
|
|
383
|
+
const model = typeof args.model === "string" ? args.model : "";
|
|
384
|
+
if (!machine.trim())
|
|
385
|
+
return textResult({ error: "machine is required" }, true);
|
|
386
|
+
if (!model.trim())
|
|
387
|
+
return textResult({ error: "model is required" }, true);
|
|
388
|
+
const result = await pullModel({
|
|
389
|
+
machine,
|
|
390
|
+
model,
|
|
391
|
+
insecure: args.insecure === true,
|
|
392
|
+
writeStdout: false,
|
|
393
|
+
quiet: true,
|
|
394
|
+
});
|
|
395
|
+
return textResult(result);
|
|
396
|
+
}
|
|
397
|
+
case "ollanet_show": {
|
|
398
|
+
const machine = typeof args.machine === "string" ? args.machine : "";
|
|
399
|
+
const model = typeof args.model === "string" ? args.model : "";
|
|
400
|
+
if (!machine.trim())
|
|
401
|
+
return textResult({ error: "machine is required" }, true);
|
|
402
|
+
if (!model.trim())
|
|
403
|
+
return textResult({ error: "model is required" }, true);
|
|
404
|
+
return textResult(await showModel({ machine, model }));
|
|
405
|
+
}
|
|
406
|
+
case "ollanet_rm": {
|
|
407
|
+
const machine = typeof args.machine === "string" ? args.machine : "";
|
|
408
|
+
const model = typeof args.model === "string" ? args.model : "";
|
|
409
|
+
if (!machine.trim())
|
|
410
|
+
return textResult({ error: "machine is required" }, true);
|
|
411
|
+
if (!model.trim())
|
|
412
|
+
return textResult({ error: "model is required" }, true);
|
|
413
|
+
if (args.confirm !== true) {
|
|
414
|
+
return textResult({ error: "confirm must be true to delete a model" }, true);
|
|
415
|
+
}
|
|
416
|
+
return textResult(await removeModel({ machine, model, yes: true }));
|
|
417
|
+
}
|
|
418
|
+
case "ollanet_ps": {
|
|
419
|
+
const machine = typeof args.machine === "string" ? args.machine : undefined;
|
|
420
|
+
return textResult(await listLoaded({ machine }));
|
|
421
|
+
}
|
|
190
422
|
case "ollanet_list_chats": {
|
|
191
423
|
const chats = await listChats();
|
|
192
424
|
return textResult(chats.map((c) => ({
|