ollanet 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -7
- package/dist/bench-suite.d.ts +2 -0
- package/dist/bench-suite.js +1 -0
- package/dist/bench.js +54 -18
- package/dist/cli.js +16 -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 +6 -1
- package/dist/index.js +3 -0
- package/dist/mcp.js +118 -3
- 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/scan-store.d.ts +15 -0
- package/dist/scan-store.js +41 -0
- package/dist/scan.d.ts +12 -1
- package/dist/scan.js +73 -42
- package/package.json +2 -1
package/dist/prompt.js
CHANGED
|
@@ -16,6 +16,7 @@ import { appendMessage, cleanTopic, createChat, loadChat, saveChat, toApiMessage
|
|
|
16
16
|
import { configPath, defaultModelForHost, loadConfig, machineSettingsForHost, mergeSettings, parseFormat, settingsFromEnv, settingsSummary, configFromPartial, } from "./config.js";
|
|
17
17
|
import { discoverHosts, envInt, findDiscoveredHost, ollamaBaseUrl, resolveHost, shortName, } from "./hosts.js";
|
|
18
18
|
import { ollamaChat } from "./ollama-chat.js";
|
|
19
|
+
import { assemblePrompt, readPromptFile } from "./prompt-input.js";
|
|
19
20
|
/** Prompt/chat HTTP timeout (ms). 0 = no timeout. Separate from scan's OLLAMA_TIMEOUT_MS. */
|
|
20
21
|
function promptTimeoutMs() {
|
|
21
22
|
return envInt("OLLAMA_PROMPT_TIMEOUT_MS", 600_000);
|
|
@@ -30,6 +31,7 @@ Examples:
|
|
|
30
31
|
ollanet prompt localhost "Explain MagicDNS"
|
|
31
32
|
ollanet prompt --chat a1b2c3d4e5f6 "Give an example"
|
|
32
33
|
ollanet prompt localhost --temperature 0.2 --num-predict 64 "2+2?"
|
|
34
|
+
ollanet prompt localhost --file ./notes.md
|
|
33
35
|
ollanet chats
|
|
34
36
|
|
|
35
37
|
Each reply is saved under responses/<hash>.json and the hash is printed so you
|
|
@@ -51,6 +53,7 @@ Options:
|
|
|
51
53
|
--format <json|schema> Force JSON mode or a JSON schema string
|
|
52
54
|
--think Enable model thinking (qwen3 etc.); streams to stderr
|
|
53
55
|
--no-think Disable thinking (default) so tokens go to the reply
|
|
56
|
+
--file <path> Prompt from a .txt or .md file (joins argv / stdin)
|
|
54
57
|
--no-stream Wait for the full response
|
|
55
58
|
--json Emit final response JSON (implies --no-stream)`);
|
|
56
59
|
process.exit(1);
|
|
@@ -137,6 +140,7 @@ function parseArgs(argv) {
|
|
|
137
140
|
let machineFlag;
|
|
138
141
|
let modelFlag;
|
|
139
142
|
let chatId;
|
|
143
|
+
let file;
|
|
140
144
|
let stream = true;
|
|
141
145
|
let json = false;
|
|
142
146
|
let save = true;
|
|
@@ -181,6 +185,10 @@ function parseArgs(argv) {
|
|
|
181
185
|
modelFlag = arg.includes("=") ? arg.slice("--model=".length) : takeValue(args, "--model");
|
|
182
186
|
continue;
|
|
183
187
|
}
|
|
188
|
+
if (arg === "--file" || arg === "-f" || arg.startsWith("--file=")) {
|
|
189
|
+
file = arg.includes("=") ? arg.slice("--file=".length) : takeValue(args, "--file");
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
184
192
|
if (arg === "--system" || arg.startsWith("--system=")) {
|
|
185
193
|
settings.system = arg.includes("=")
|
|
186
194
|
? arg.slice("--system=".length)
|
|
@@ -235,7 +243,7 @@ function parseArgs(argv) {
|
|
|
235
243
|
}
|
|
236
244
|
// Continue (--chat): all positionals are the prompt. Optional machine override
|
|
237
245
|
// via --machine, or a leading discovered-host name peeled in main().
|
|
238
|
-
return { machine, model, promptParts, stream, json, save, chatId, settings };
|
|
246
|
+
return { machine, model, promptParts, file, stream, json, save, chatId, settings };
|
|
239
247
|
}
|
|
240
248
|
async function readStdinIfPiped() {
|
|
241
249
|
if (stdinStream.isTTY)
|
|
@@ -261,7 +269,7 @@ async function readStdinIfPiped() {
|
|
|
261
269
|
const onEnd = () => done(true);
|
|
262
270
|
stdinStream.once("readable", onReadable);
|
|
263
271
|
stdinStream.once("end", onEnd);
|
|
264
|
-
const timer = setTimeout(() => done(stdinStream.readableLength > 0),
|
|
272
|
+
const timer = setTimeout(() => done(stdinStream.readableLength > 0), 100);
|
|
265
273
|
});
|
|
266
274
|
if (!hasData)
|
|
267
275
|
return "";
|
|
@@ -436,6 +444,17 @@ export async function main() {
|
|
|
436
444
|
const { stream, json, save } = parsed;
|
|
437
445
|
let promptParts = parsed.promptParts;
|
|
438
446
|
const fromStdin = await readStdinIfPiped();
|
|
447
|
+
let fromFile = "";
|
|
448
|
+
if (parsed.file) {
|
|
449
|
+
try {
|
|
450
|
+
fromFile = await readPromptFile(parsed.file);
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
console.error(err instanceof Error ? err.message : err);
|
|
454
|
+
process.exitCode = 1;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
439
458
|
const config = await loadConfig();
|
|
440
459
|
const { hosts: targets } = await discoverHosts({
|
|
441
460
|
hosts: config.hosts,
|
|
@@ -460,13 +479,17 @@ export async function main() {
|
|
|
460
479
|
usage();
|
|
461
480
|
}
|
|
462
481
|
const host = resolveHost(targets, machineQuery);
|
|
463
|
-
const peeled = await maybePeelModel(host, promptParts, fromStdin);
|
|
482
|
+
const peeled = await maybePeelModel(host, promptParts, fromStdin || fromFile);
|
|
464
483
|
peeledModel = peeled.model;
|
|
465
484
|
promptParts = peeled.promptParts;
|
|
466
485
|
}
|
|
467
|
-
const promptText =
|
|
486
|
+
const promptText = assemblePrompt({
|
|
487
|
+
argv: promptParts.join(" "),
|
|
488
|
+
file: fromFile,
|
|
489
|
+
stdin: fromStdin,
|
|
490
|
+
});
|
|
468
491
|
if (!promptText) {
|
|
469
|
-
console.error("No prompt provided (pass args and/or pipe stdin).");
|
|
492
|
+
console.error("No prompt provided (pass args, --file, and/or pipe stdin).");
|
|
470
493
|
usage();
|
|
471
494
|
}
|
|
472
495
|
try {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ScanPayload } from "./scan.js";
|
|
2
|
+
export interface StoredScan extends ScanPayload {
|
|
3
|
+
scanned_at: string;
|
|
4
|
+
lan: boolean;
|
|
5
|
+
include_offline: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Path for the last-scan snapshot. Read at call time so import stays safe. */
|
|
8
|
+
export declare function lastScanPath(): string;
|
|
9
|
+
export declare function saveLastScan(payload: ScanPayload, meta: {
|
|
10
|
+
lan: boolean;
|
|
11
|
+
includeOffline: boolean;
|
|
12
|
+
}): Promise<string>;
|
|
13
|
+
export declare function loadLastScan(): Promise<StoredScan | null>;
|
|
14
|
+
/** Last saved scan, or null if none. Does not probe the network. */
|
|
15
|
+
export declare function lastScan(): Promise<StoredScan | null>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { defaultLastScanPath } from "./paths.js";
|
|
4
|
+
/** Path for the last-scan snapshot. Read at call time so import stays safe. */
|
|
5
|
+
export function lastScanPath() {
|
|
6
|
+
return process.env.OLLANET_LAST_SCAN ?? defaultLastScanPath();
|
|
7
|
+
}
|
|
8
|
+
export async function saveLastScan(payload, meta) {
|
|
9
|
+
const stored = {
|
|
10
|
+
...payload,
|
|
11
|
+
scanned_at: new Date().toISOString(),
|
|
12
|
+
lan: meta.lan,
|
|
13
|
+
include_offline: meta.includeOffline,
|
|
14
|
+
};
|
|
15
|
+
const file = lastScanPath();
|
|
16
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
17
|
+
await writeFile(file, `${JSON.stringify(stored, null, 2)}\n`, "utf8");
|
|
18
|
+
return file;
|
|
19
|
+
}
|
|
20
|
+
export async function loadLastScan() {
|
|
21
|
+
const file = lastScanPath();
|
|
22
|
+
let text;
|
|
23
|
+
try {
|
|
24
|
+
text = await readFile(file, "utf8");
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : "";
|
|
28
|
+
if (code === "ENOENT")
|
|
29
|
+
return null;
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
const parsed = JSON.parse(text);
|
|
33
|
+
if (!parsed || !Array.isArray(parsed.servers)) {
|
|
34
|
+
throw new Error(`Invalid last-scan file: ${file}`);
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
/** Last saved scan, or null if none. Does not probe the network. */
|
|
39
|
+
export async function lastScan() {
|
|
40
|
+
return loadLastScan();
|
|
41
|
+
}
|
package/dist/scan.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scan reachable networks for Ollama servers and list available models.
|
|
3
3
|
*
|
|
4
|
-
* Usage: ollanet scan [--lan] [--json] [--all]
|
|
4
|
+
* Usage: ollanet scan [--lan] [--json] [--all] [--last]
|
|
5
5
|
*/
|
|
6
6
|
import { type AppConfig } from "./config.js";
|
|
7
7
|
export interface ScanOptions {
|
|
@@ -13,6 +13,11 @@ export interface ScanOptions {
|
|
|
13
13
|
* is not read. Omit to keep CLI/MCP file behavior.
|
|
14
14
|
*/
|
|
15
15
|
config?: Partial<AppConfig>;
|
|
16
|
+
/**
|
|
17
|
+
* Persist this result as the last scan. Default true when `config` is omitted
|
|
18
|
+
* (CLI / MCP). Default false when `config` is passed (hermetic apps/tests).
|
|
19
|
+
*/
|
|
20
|
+
save?: boolean;
|
|
16
21
|
}
|
|
17
22
|
export interface ScannedServer {
|
|
18
23
|
hostname: string;
|
|
@@ -34,12 +39,18 @@ export interface ScannedServer {
|
|
|
34
39
|
tuned: boolean;
|
|
35
40
|
}>;
|
|
36
41
|
}
|
|
42
|
+
export interface ScanFailure {
|
|
43
|
+
name: string;
|
|
44
|
+
ip: string;
|
|
45
|
+
error: string;
|
|
46
|
+
}
|
|
37
47
|
export interface ScanPayload {
|
|
38
48
|
network: string;
|
|
39
49
|
sources: string[];
|
|
40
50
|
port: number;
|
|
41
51
|
scanned: number;
|
|
42
52
|
servers: ScannedServer[];
|
|
53
|
+
failures?: ScanFailure[];
|
|
43
54
|
}
|
|
44
55
|
/** Programmatic scan used by CLI `--json`, MCP, and `import { scanNetwork } from "ollanet"`. */
|
|
45
56
|
export declare function scanNetwork(options?: ScanOptions): Promise<ScanPayload>;
|
package/dist/scan.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scan reachable networks for Ollama servers and list available models.
|
|
3
3
|
*
|
|
4
|
-
* Usage: ollanet scan [--lan] [--json] [--all]
|
|
4
|
+
* Usage: ollanet scan [--lan] [--json] [--all] [--last]
|
|
5
5
|
*/
|
|
6
6
|
import { configFromPartial, loadConfig } from "./config.js";
|
|
7
7
|
import { discoverHosts, envInt, ollamaPort, shortName, } from "./hosts.js";
|
|
8
|
+
import { lastScanPath, loadLastScan, saveLastScan } from "./scan-store.js";
|
|
8
9
|
import { looksTuned } from "./tuned.js";
|
|
9
10
|
function scanTimeoutMs() {
|
|
10
11
|
return envInt("OLLAMA_TIMEOUT_MS", 2500);
|
|
@@ -76,48 +77,53 @@ async function scanHost(host) {
|
|
|
76
77
|
return { host, ok: false, models: [], error, url };
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
function printPayload(payload, cached) {
|
|
81
|
+
if (cached?.scanned_at) {
|
|
82
|
+
const flags = [
|
|
83
|
+
cached.lan ? "lan" : null,
|
|
84
|
+
cached.include_offline ? "all" : null,
|
|
85
|
+
].filter(Boolean);
|
|
86
|
+
console.log(`Cached scan from ${cached.scanned_at}${flags.length ? ` (${flags.join(", ")})` : ""}`);
|
|
87
|
+
console.log(`File: ${lastScanPath()}\n`);
|
|
88
|
+
}
|
|
89
|
+
console.log(`Network: ${payload.network}`);
|
|
90
|
+
console.log(`Discovery: ${payload.sources.join(", ") || "none"}`);
|
|
91
|
+
console.log(`Probing Ollama on ${payload.scanned} host(s)\n`);
|
|
92
|
+
if (payload.servers.length === 0) {
|
|
87
93
|
console.log("No Ollama servers found.");
|
|
88
94
|
console.log("Tip: add hosts to config.json, set OLLANET_HOSTS, pass --lan, or prompt an IP directly.");
|
|
89
|
-
const interesting =
|
|
95
|
+
const interesting = (payload.failures ?? []).filter((f) => f.error !== "offline");
|
|
90
96
|
if (interesting.length > 0) {
|
|
91
97
|
console.log("\nNon-timeout failures (first few):");
|
|
92
|
-
for (const
|
|
93
|
-
console.log(` - ${
|
|
98
|
+
for (const f of interesting.slice(0, 5)) {
|
|
99
|
+
console.log(` - ${f.name} (${f.ip}): ${f.error}`);
|
|
94
100
|
}
|
|
95
101
|
}
|
|
96
102
|
return;
|
|
97
103
|
}
|
|
98
|
-
for (const
|
|
99
|
-
const label =
|
|
100
|
-
const selfTag =
|
|
104
|
+
for (const server of payload.servers) {
|
|
105
|
+
const label = server.dnsName || server.hostname;
|
|
106
|
+
const selfTag = server.self ? " [this device]" : "";
|
|
101
107
|
console.log(`${label}${selfTag}`);
|
|
102
|
-
console.log(` IP: ${
|
|
103
|
-
console.log(` Endpoint:
|
|
104
|
-
if (
|
|
108
|
+
console.log(` IP: ${server.ip} source: ${server.source} OS: ${server.os}`);
|
|
109
|
+
console.log(` Endpoint: ${server.endpoint}`);
|
|
110
|
+
if (server.models.length === 0) {
|
|
105
111
|
console.log(" Models: (none)");
|
|
106
112
|
}
|
|
107
113
|
else {
|
|
108
|
-
console.log(` Models (${
|
|
109
|
-
for (const model of
|
|
114
|
+
console.log(` Models (${server.models.length}):`);
|
|
115
|
+
for (const model of server.models) {
|
|
110
116
|
const size = formatBytes(model.size);
|
|
111
|
-
const params = model.
|
|
112
|
-
const quant = model.
|
|
117
|
+
const params = model.parameter_size;
|
|
118
|
+
const quant = model.quantization_level;
|
|
113
119
|
const meta = [params, quant, size].filter(Boolean).join(" · ");
|
|
114
|
-
const tag =
|
|
120
|
+
const tag = model.tuned ? " [tuned]" : "";
|
|
115
121
|
console.log(meta ? ` - ${model.name} (${meta})${tag}` : ` - ${model.name}${tag}`);
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
124
|
console.log("");
|
|
119
125
|
}
|
|
120
|
-
console.log(`Found ${
|
|
126
|
+
console.log(`Found ${payload.servers.length} Ollama server(s) with ${payload.servers.reduce((n, s) => n + s.models.length, 0)} model entrie(s).`);
|
|
121
127
|
}
|
|
122
128
|
function toPayload(results, networkLabel, sources) {
|
|
123
129
|
return {
|
|
@@ -146,6 +152,13 @@ function toPayload(results, networkLabel, sources) {
|
|
|
146
152
|
tuned: looksTuned(m.name),
|
|
147
153
|
})),
|
|
148
154
|
})),
|
|
155
|
+
failures: results
|
|
156
|
+
.filter((r) => !r.ok)
|
|
157
|
+
.map((r) => ({
|
|
158
|
+
name: shortName(r.host),
|
|
159
|
+
ip: r.host.ip,
|
|
160
|
+
error: r.error ?? "unknown",
|
|
161
|
+
})),
|
|
149
162
|
};
|
|
150
163
|
}
|
|
151
164
|
/** Programmatic scan used by CLI `--json`, MCP, and `import { scanNetwork } from "ollanet"`. */
|
|
@@ -161,31 +174,49 @@ export async function scanNetwork(options = {}) {
|
|
|
161
174
|
throw new Error("No hosts to scan. Add config.hosts, set OLLANET_HOSTS, or enable Tailscale/LAN discovery.");
|
|
162
175
|
}
|
|
163
176
|
const results = await mapPool(hosts, scanConcurrency(), scanHost);
|
|
164
|
-
|
|
177
|
+
const payload = toPayload(results, networkLabel, sources);
|
|
178
|
+
const shouldSave = options.save ?? options.config == null;
|
|
179
|
+
if (shouldSave) {
|
|
180
|
+
await saveLastScan(payload, {
|
|
181
|
+
lan: options.lanScan ?? false,
|
|
182
|
+
includeOffline: options.includeOffline ?? false,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return payload;
|
|
165
186
|
}
|
|
166
187
|
export async function main() {
|
|
167
188
|
const includeOffline = process.argv.includes("--all");
|
|
168
189
|
const jsonOut = process.argv.includes("--json");
|
|
169
190
|
const lanScan = process.argv.includes("--lan");
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
191
|
+
const cached = process.argv.includes("--last") || process.argv.includes("--cached");
|
|
192
|
+
if (cached) {
|
|
193
|
+
const stored = await loadLastScan();
|
|
194
|
+
if (!stored) {
|
|
195
|
+
console.error(`No saved scan at ${lastScanPath()}. Run ollanet scan first.`);
|
|
196
|
+
process.exitCode = 1;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (jsonOut) {
|
|
200
|
+
console.log(JSON.stringify(stored, null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
printPayload(stored, stored);
|
|
180
204
|
return;
|
|
181
205
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
206
|
+
try {
|
|
207
|
+
const payload = await scanNetwork({ includeOffline, lanScan });
|
|
208
|
+
if (jsonOut) {
|
|
209
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
printPayload(payload);
|
|
213
|
+
}
|
|
214
|
+
if (payload.servers.length === 0) {
|
|
215
|
+
process.exitCode = 2;
|
|
216
|
+
}
|
|
186
217
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
process.exitCode =
|
|
218
|
+
catch (err) {
|
|
219
|
+
console.error(err instanceof Error ? err.message : err);
|
|
220
|
+
process.exitCode = 1;
|
|
190
221
|
}
|
|
191
222
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ollanet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Discover and talk to Ollama on any reachable network — CLI, MCP, and a Node library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"ollanet": "tsx src/cli.ts",
|
|
30
30
|
"scan": "tsx src/cli.ts scan",
|
|
31
31
|
"prompt": "tsx src/cli.ts prompt",
|
|
32
|
+
"compare": "tsx src/cli.ts compare",
|
|
32
33
|
"pull": "tsx src/cli.ts pull",
|
|
33
34
|
"show": "tsx src/cli.ts show",
|
|
34
35
|
"rm": "tsx src/cli.ts rm",
|