ollanet 0.6.23 → 0.7.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 +5 -4
- package/dist/argv.d.ts +18 -0
- package/dist/argv.js +35 -0
- package/dist/bench.js +8 -6
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +65 -26
- package/dist/compare.js +35 -15
- package/dist/funnel.d.ts +70 -0
- package/dist/funnel.js +195 -0
- package/dist/help.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/mcp.js +6 -4
- package/dist/prompt.d.ts +8 -0
- package/dist/prompt.js +41 -6
- package/dist/pull.js +6 -4
- package/dist/scan.d.ts +20 -0
- package/dist/scan.js +98 -20
- package/dist/serve-store.d.ts +52 -0
- package/dist/serve-store.js +215 -0
- package/dist/serve.d.ts +108 -0
- package/dist/serve.js +840 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# ollanet
|
|
6
6
|
|
|
7
|
-
Find, manage, and use Ollama models on the hosts you choose.
|
|
8
|
-
|
|
9
|
-
**CLI** for humans · **MCP** for agents · **Node** for apps.
|
|
7
|
+
Find, manage, and use Ollama models on the hosts you choose, from a CLI, an MCP server for agents, or a Node library.
|
|
10
8
|
|
|
11
9
|
Discover hosts, give models convenient aliases, and prompt or compare them. A reachable host is not a trusted host. LAN scan is opt-in. Node 20+, zero runtime deps.
|
|
12
10
|
|
|
@@ -39,7 +37,7 @@ Host-first: `ollanet <cmd> <machine> …`. The machine is a MagicDNS name, a con
|
|
|
39
37
|
|
|
40
38
|
**Manage.** `pull`, `show`, `rm`, and `ps` on a named machine. Comparisons and benches are deeper workflows on [ollanet.dev/docs](https://ollanet.dev/docs).
|
|
41
39
|
|
|
42
|
-
**Use.** Prompt, continue a chat by hash, or call the same inventory from MCP and the Node library.
|
|
40
|
+
**Use.** Prompt, continue a chat by hash, or call the same inventory from MCP and the Node library. Agents off your network can read a token-gated, read-only model catalog with `ollanet serve` (no hostnames or IPs, no prompt proxying). `-q` keeps stderr for errors only, and `--stdin` reads a piped prompt.
|
|
43
41
|
|
|
44
42
|
## Reachability and trust
|
|
45
43
|
|
|
@@ -59,6 +57,7 @@ This package does not include vendor telemetry. Commands still cause network tra
|
|
|
59
57
|
| `rm` | Delete request (`--yes`) | Remove that model from the host | Model removed on the remote host |
|
|
60
58
|
| `alias` | None to Ollama | None | Local `config.json` |
|
|
61
59
|
| `chats` | None to Ollama | None | Reads local transcripts |
|
|
60
|
+
| `serve` | Tag and metadata probes when it refreshes | Answer with tags and model metadata | Token hashes in `~/.ollanet/serve.json` |
|
|
62
61
|
|
|
63
62
|
Other machines see a saved chat only if they share the responses directory (or the same `~/.ollanet/` when installed). This table covers the operations inspected in this repo. It does not prove every possible data path is listed.
|
|
64
63
|
|
|
@@ -94,6 +93,8 @@ pnpm test
|
|
|
94
93
|
pnpm ollanet -- help
|
|
95
94
|
```
|
|
96
95
|
|
|
96
|
+
A checkout reads `./config.json` (gitignored, optional). Copy `config.example.json` to start. `config.json` was tracked before 0.7.0: if your checkout has one with your hosts or aliases, copy it somewhere safe before pulling, since the pull removes it (or refuses if you edited it), then put it back.
|
|
97
|
+
|
|
97
98
|
Site (FilePress + docs mount): `pnpm --dir site ship`
|
|
98
99
|
|
|
99
100
|
## License
|
package/dist/argv.d.ts
CHANGED
|
@@ -6,6 +6,24 @@ export type UsageFn = () => never;
|
|
|
6
6
|
export declare function isHelpFlag(arg: string): boolean;
|
|
7
7
|
/** Intentional help: stdout, exit 0. */
|
|
8
8
|
export declare function printHelp(text: string): never;
|
|
9
|
+
/**
|
|
10
|
+
* Quiet mode (`-q` / `--quiet` on any command, or OLLANET_QUIET=1): results on
|
|
11
|
+
* stdout only. stderr keeps real errors and warnings (and --think reasoning,
|
|
12
|
+
* which you asked for); saved paths, hints, notices, and progress are dropped.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isQuiet(): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Remove `-q` / `--quiet` from a command's argv and switch quiet mode on.
|
|
17
|
+
* Done once in the CLI entry so every command accepts it in any position.
|
|
18
|
+
*/
|
|
19
|
+
export declare function takeQuietFlag(argv: string[]): string[];
|
|
20
|
+
/**
|
|
21
|
+
* Decorative progress lines (`→ model (1/2)`) are for people watching a
|
|
22
|
+
* terminal. Piped and agent runs skip them so stderr only carries results and
|
|
23
|
+
* real errors (PowerShell 5 wraps every native stderr line in a red
|
|
24
|
+
* NativeCommandError). OLLANET_PROGRESS=1 forces them on, 0 forces them off.
|
|
25
|
+
*/
|
|
26
|
+
export declare function showProgress(): boolean;
|
|
9
27
|
/** Usage error: stderr, exit 1. */
|
|
10
28
|
export declare function failUsage(text: string): never;
|
|
11
29
|
export declare function takeValue(args: string[], flag: string, usage: UsageFn): string;
|
package/dist/argv.js
CHANGED
|
@@ -6,6 +6,41 @@ export function printHelp(text) {
|
|
|
6
6
|
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
7
7
|
process.exit(0);
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Quiet mode (`-q` / `--quiet` on any command, or OLLANET_QUIET=1): results on
|
|
11
|
+
* stdout only. stderr keeps real errors and warnings (and --think reasoning,
|
|
12
|
+
* which you asked for); saved paths, hints, notices, and progress are dropped.
|
|
13
|
+
*/
|
|
14
|
+
export function isQuiet() {
|
|
15
|
+
const flag = process.env.OLLANET_QUIET?.trim().toLowerCase();
|
|
16
|
+
return flag === "1" || flag === "true";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Remove `-q` / `--quiet` from a command's argv and switch quiet mode on.
|
|
20
|
+
* Done once in the CLI entry so every command accepts it in any position.
|
|
21
|
+
*/
|
|
22
|
+
export function takeQuietFlag(argv) {
|
|
23
|
+
const rest = argv.filter((arg) => arg !== "-q" && arg !== "--quiet");
|
|
24
|
+
if (rest.length !== argv.length)
|
|
25
|
+
process.env.OLLANET_QUIET = "1";
|
|
26
|
+
return rest;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Decorative progress lines (`→ model (1/2)`) are for people watching a
|
|
30
|
+
* terminal. Piped and agent runs skip them so stderr only carries results and
|
|
31
|
+
* real errors (PowerShell 5 wraps every native stderr line in a red
|
|
32
|
+
* NativeCommandError). OLLANET_PROGRESS=1 forces them on, 0 forces them off.
|
|
33
|
+
*/
|
|
34
|
+
export function showProgress() {
|
|
35
|
+
if (isQuiet())
|
|
36
|
+
return false;
|
|
37
|
+
const flag = process.env.OLLANET_PROGRESS?.trim().toLowerCase();
|
|
38
|
+
if (flag === "1" || flag === "true")
|
|
39
|
+
return true;
|
|
40
|
+
if (flag === "0" || flag === "false")
|
|
41
|
+
return false;
|
|
42
|
+
return Boolean(process.stderr.isTTY);
|
|
43
|
+
}
|
|
9
44
|
/** Usage error: stderr, exit 1. */
|
|
10
45
|
export function failUsage(text) {
|
|
11
46
|
console.error(text);
|
package/dist/bench.js
CHANGED
|
@@ -6,7 +6,7 @@ import { comparabilityKey, getSuiteCases, LONG_THROUGHPUT_NUM_PREDICT, median, s
|
|
|
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, failUsage, isHelpFlag, parseNumberFlag, printHelp, takeFlag } from "./argv.js";
|
|
9
|
+
import { consumeSettingsFlag, failUsage, isHelpFlag, isQuiet, parseNumberFlag, printHelp, showProgress, 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";
|
|
@@ -43,6 +43,7 @@ Options:
|
|
|
43
43
|
--judge Second pass: score check answers 1–5
|
|
44
44
|
--judge-model <name> Required with --judge (no default)
|
|
45
45
|
--json Machine-readable stdout
|
|
46
|
+
-q, --quiet Table only; no plan, notes, or saved paths on stderr
|
|
46
47
|
--save / --no-save Persist under benchmarks/ (default on)
|
|
47
48
|
--fail-fast Stop remaining models after the first hard error
|
|
48
49
|
--fail-on-error Exit 2 if any model/case errored
|
|
@@ -642,7 +643,8 @@ export async function main() {
|
|
|
642
643
|
const pin = pinnedNumPredict(c, parsed.throughputNumPredict);
|
|
643
644
|
return n + caseReps(c, parsed.runs, parsed.hot) * attemptTimeoutMs(pin);
|
|
644
645
|
}, 0);
|
|
645
|
-
|
|
646
|
+
const quiet = isQuiet();
|
|
647
|
+
if (!parsed.json && !quiet) {
|
|
646
648
|
const skipBits = [
|
|
647
649
|
skipped.filter((s) => s.reason === "non-completion").length
|
|
648
650
|
? `${skipped.filter((s) => s.reason === "non-completion").length} embedding/other`
|
|
@@ -669,7 +671,7 @@ export async function main() {
|
|
|
669
671
|
}
|
|
670
672
|
console.error("Proceeding…");
|
|
671
673
|
}
|
|
672
|
-
if (parsed.hot) {
|
|
674
|
+
if (parsed.hot && !quiet) {
|
|
673
675
|
console.error("Hot: discarding the first throughput run; models stay loaded (no unload between models).");
|
|
674
676
|
}
|
|
675
677
|
if (parsed.settings.think === true) {
|
|
@@ -688,7 +690,7 @@ export async function main() {
|
|
|
688
690
|
const info = tags.find((t) => t.name === model);
|
|
689
691
|
const caps = await fetchCapabilities(baseUrl, model);
|
|
690
692
|
const selfJudge = Boolean(judgeModel && judgeModel === model);
|
|
691
|
-
if (!parsed.json) {
|
|
693
|
+
if (!parsed.json && showProgress()) {
|
|
692
694
|
console.error(`\n→ ${model} (${mi + 1}/${selected.length})`);
|
|
693
695
|
}
|
|
694
696
|
const cold = parsed.coldLoad
|
|
@@ -833,7 +835,7 @@ export async function main() {
|
|
|
833
835
|
};
|
|
834
836
|
if (parsed.save) {
|
|
835
837
|
const file = await saveBenchmark(result);
|
|
836
|
-
if (!parsed.json)
|
|
838
|
+
if (!parsed.json && !quiet)
|
|
837
839
|
console.error(`saved ${file}`);
|
|
838
840
|
}
|
|
839
841
|
if (parsed.json) {
|
|
@@ -842,7 +844,7 @@ export async function main() {
|
|
|
842
844
|
else {
|
|
843
845
|
process.stdout.write("\n");
|
|
844
846
|
printTable(machineLabel, parsed.suite, parsed.runs, revision, compKey, modelResults, skipped, parsed.coldLoad, parsed.hot);
|
|
845
|
-
if (parsed.save)
|
|
847
|
+
if (parsed.save && !quiet)
|
|
846
848
|
console.error(`benchmarks dir: ${benchmarksDir()}`);
|
|
847
849
|
}
|
|
848
850
|
if (parsed.failOnError && anyError) {
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -12,11 +12,10 @@
|
|
|
12
12
|
* ollanet bench <machine> [model...]
|
|
13
13
|
* ollanet chats
|
|
14
14
|
* ollanet mcp
|
|
15
|
+
* ollanet serve [--funnel]
|
|
15
16
|
*/
|
|
16
17
|
const HELP = `ollanet: find, manage, and use Ollama on hosts you choose
|
|
17
|
-
|
|
18
|
-
Discover hosts, manage models, prompt and compare, bench tok/s, and continue
|
|
19
|
-
chats by hash. CLI for humans, MCP for agents, Node for apps.
|
|
18
|
+
CLI for people, MCP for agents (ollanet mcp), Node library for apps (import from "ollanet").
|
|
20
19
|
A reachable host is not a trusted host. pull and rm change the remote host.
|
|
21
20
|
|
|
22
21
|
Usage:
|
|
@@ -24,7 +23,8 @@ Usage:
|
|
|
24
23
|
ollanet prompt <machine|ip> [model] <prompt...>
|
|
25
24
|
ollanet prompt --chat <hash> <prompt...>
|
|
26
25
|
ollanet prompt <machine|ip> --file <path.txt|.md>
|
|
27
|
-
ollanet
|
|
26
|
+
<command> | ollanet prompt <machine|ip> [model] --stdin
|
|
27
|
+
ollanet compare <machine> <model> <model> [model...] [--prompt <text> | --file <path> | --stdin]
|
|
28
28
|
ollanet pull <machine|ip> <model>
|
|
29
29
|
ollanet show <machine|ip> <model>
|
|
30
30
|
ollanet rm <machine|ip> <model> --yes
|
|
@@ -33,36 +33,64 @@ Usage:
|
|
|
33
33
|
ollanet bench <machine|ip> [model...] [--all] [options]
|
|
34
34
|
ollanet alias list|add|rm …
|
|
35
35
|
ollanet mcp
|
|
36
|
+
ollanet serve [--listen <host:port>] [--funnel] (read-only catalog)
|
|
37
|
+
ollanet serve status
|
|
38
|
+
ollanet serve token add|list|rm|prune …
|
|
39
|
+
ollanet serve label set|rm|list …
|
|
36
40
|
ollanet help [command]
|
|
37
41
|
|
|
38
42
|
Examples:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
43
|
+
Find hosts
|
|
44
|
+
ollanet scan
|
|
45
|
+
ollanet scan --lan # also sweep your subnet on port 11434 (opt-in)
|
|
46
|
+
ollanet scan --last # print the last saved scan (no network)
|
|
47
|
+
|
|
48
|
+
Prompt and chat
|
|
49
|
+
ollanet prompt localhost "What is MagicDNS?"
|
|
50
|
+
ollanet prompt localhost --file ./notes.md
|
|
51
|
+
ollanet alias add desk studio gemma3:12b
|
|
52
|
+
ollanet prompt desk "hello" # expands machine + model
|
|
53
|
+
git diff | ollanet prompt desk "Review this" --stdin -q
|
|
54
|
+
ollanet prompt --chat a1b2c3d4e5f6 "Tell me more"
|
|
55
|
+
ollanet chats
|
|
56
|
+
|
|
57
|
+
Compare and bench
|
|
58
|
+
ollanet compare studio gemma3:12b llama3.2:3b
|
|
59
|
+
ollanet compare studio gemma3:12b llama3.2:3b --prompt "Explain MagicDNS"
|
|
60
|
+
ollanet bench localhost --all
|
|
61
|
+
ollanet bench localhost llama3.2:1b --runs 3
|
|
62
|
+
ollanet bench localhost gemma3:12b --hot --runs 5
|
|
63
|
+
|
|
64
|
+
Manage models (pull and rm change the remote host)
|
|
65
|
+
ollanet pull studio gemma3:12b
|
|
66
|
+
ollanet show studio gemma4-ctx32k
|
|
67
|
+
ollanet rm studio llama3.2:1b --yes
|
|
68
|
+
ollanet ps studio
|
|
69
|
+
|
|
70
|
+
Share a read-only catalog
|
|
71
|
+
ollanet serve token add cloud-agent --expires 30d
|
|
72
|
+
ollanet serve --funnel # catalog on https://<device>.<tailnet>.ts.net/
|
|
73
|
+
ollanet serve status # local and Funnel URLs of the running server
|
|
74
|
+
ollanet serve label set studio gpu-box # agents see gpu-box, not the host name
|
|
75
|
+
ollanet serve token prune # expired, or unused 30+ days (asks first)
|
|
59
76
|
|
|
77
|
+
Agents and scripts
|
|
78
|
+
ollanet mcp # stdio MCP server for agents
|
|
79
|
+
ollanet scan --json -q # machine-readable hosts and models
|
|
80
|
+
|
|
81
|
+
Every flag and more examples: ollanet help <command> (or ollanet <command> --help)
|
|
82
|
+
|
|
83
|
+
Quiet: -q / --quiet on any command (or OLLANET_QUIET=1): results on stdout,
|
|
84
|
+
stderr only for errors and warnings
|
|
85
|
+
Progress: compare/bench progress lines only when stderr is a terminal
|
|
86
|
+
(OLLANET_PROGRESS=1 forces on, 0 off)
|
|
60
87
|
Config: ~/.ollanet/config.json when installed, ./config.json in a checkout
|
|
61
88
|
(override with OLLANET_CONFIG)
|
|
62
89
|
Hosts: config.hosts, OLLANET_HOSTS, optional Tailscale + --lan
|
|
63
90
|
Aliases: config.aliases — ollanet alias add|list|rm
|
|
64
91
|
Chats: ~/.ollanet/responses or ./responses (override with OLLANET_RESPONSES_DIR)
|
|
65
92
|
Scan: ~/.ollanet/last-scan.json or ./last-scan.json (override with OLLANET_LAST_SCAN)
|
|
93
|
+
Serve: ~/.ollanet/serve.json, token hashes and labels (override with OLLANET_SERVE_FILE)
|
|
66
94
|
`;
|
|
67
95
|
async function main() {
|
|
68
96
|
// pnpm/npm often forward a bare `--`; ignore it.
|
|
@@ -105,8 +133,14 @@ async function main() {
|
|
|
105
133
|
console.log(`ollanet ${pkg.version ?? "0.0.0"}`);
|
|
106
134
|
return;
|
|
107
135
|
}
|
|
108
|
-
// Strip the subcommand so command modules see their own argv.
|
|
109
|
-
|
|
136
|
+
// Strip the subcommand so command modules see their own argv. -q / --quiet
|
|
137
|
+
// is global: it is removed here and read through isQuiet().
|
|
138
|
+
const { takeQuietFlag } = await import("./argv.js");
|
|
139
|
+
process.argv = [
|
|
140
|
+
process.argv[0] ?? "node",
|
|
141
|
+
process.argv[1] ?? "ollanet",
|
|
142
|
+
...takeQuietFlag(argv.slice(1)),
|
|
143
|
+
];
|
|
110
144
|
switch (cmd) {
|
|
111
145
|
case "scan":
|
|
112
146
|
case "ls-servers": {
|
|
@@ -170,6 +204,11 @@ async function main() {
|
|
|
170
204
|
await run();
|
|
171
205
|
return;
|
|
172
206
|
}
|
|
207
|
+
case "serve": {
|
|
208
|
+
const { main: run } = await import("./serve.js");
|
|
209
|
+
await run();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
173
212
|
case "alias":
|
|
174
213
|
case "aliases": {
|
|
175
214
|
const { main: run } = await import("./alias.js");
|
package/dist/compare.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readPromptStdin } from "./prompt.js";
|
|
2
2
|
/**
|
|
3
3
|
* Head-to-head: one prompt, 2–5 models on the same host.
|
|
4
4
|
*
|
|
@@ -7,7 +7,7 @@ import { readStdinIfPiped } from "./prompt.js";
|
|
|
7
7
|
*/
|
|
8
8
|
import { configFromPartial, loadConfig, lookupAlias, machineSettingsForHost, mergeSettings, settingsFromEnv, } from "./config.js";
|
|
9
9
|
import { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
|
|
10
|
-
import { consumeSettingsFlag, failUsage, isHelpFlag, printHelp, takeFlag, takeValue } from "./argv.js";
|
|
10
|
+
import { consumeSettingsFlag, failUsage, isHelpFlag, isQuiet, printHelp, showProgress, takeFlag, takeValue, } from "./argv.js";
|
|
11
11
|
import { resolveTarget } from "./target.js";
|
|
12
12
|
import { ollamaChat, ollamaTags, ollamaUnload, waitUntilUnloaded, } from "./ollama-chat.js";
|
|
13
13
|
import { assemblePrompt, readPromptFile } from "./prompt-input.js";
|
|
@@ -29,6 +29,7 @@ export function helpText() {
|
|
|
29
29
|
ollanet compare <machine> <model> <model> [model...]
|
|
30
30
|
ollanet compare <machine> <model> <model> [model...] --prompt <text>
|
|
31
31
|
ollanet compare <machine> <model> <model> [model...] --file <path.txt|.md>
|
|
32
|
+
<command> | ollanet compare <machine> <model> <model> [model...] --stdin
|
|
32
33
|
|
|
33
34
|
Examples:
|
|
34
35
|
ollanet compare studio gemma3:12b llama3.2:3b
|
|
@@ -42,6 +43,8 @@ Omit --prompt/--file to use the built-in mesh-host tasting prompt.
|
|
|
42
43
|
Options:
|
|
43
44
|
--prompt <text> Prompt text (default: built-in mesh-host prompt)
|
|
44
45
|
--file <path> Prompt from a .txt or .md file
|
|
46
|
+
--stdin, - Wait for stdin to close and use it as the prompt
|
|
47
|
+
(no 100ms cutoff for slow producers)
|
|
45
48
|
--system <text> System prompt
|
|
46
49
|
--temperature <n>
|
|
47
50
|
--num-predict <n>
|
|
@@ -50,7 +53,8 @@ Options:
|
|
|
50
53
|
--think / --no-think
|
|
51
54
|
--unload Unload each model before the next (fairer tok/s)
|
|
52
55
|
--no-save Do not write compares/*
|
|
53
|
-
--json Emit the full record on stdout
|
|
56
|
+
--json Emit the full record on stdout
|
|
57
|
+
-q, --quiet Table only; no notices or saved paths on stderr`;
|
|
54
58
|
}
|
|
55
59
|
function usage() {
|
|
56
60
|
failUsage(helpText());
|
|
@@ -62,6 +66,7 @@ function parseArgs(argv) {
|
|
|
62
66
|
let json = false;
|
|
63
67
|
let save = true;
|
|
64
68
|
let unload = false;
|
|
69
|
+
let stdin = false;
|
|
65
70
|
const settings = {};
|
|
66
71
|
const positional = [];
|
|
67
72
|
while (args.length > 0) {
|
|
@@ -82,6 +87,10 @@ function parseArgs(argv) {
|
|
|
82
87
|
unload = true;
|
|
83
88
|
continue;
|
|
84
89
|
}
|
|
90
|
+
if (arg === "--stdin" || arg === "-") {
|
|
91
|
+
stdin = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
85
94
|
if (consumeSettingsFlag(arg, args, settings, usage, {
|
|
86
95
|
temperature: true,
|
|
87
96
|
numPredict: true,
|
|
@@ -105,7 +114,7 @@ function parseArgs(argv) {
|
|
|
105
114
|
}
|
|
106
115
|
const machine = positional[0];
|
|
107
116
|
const models = positional.slice(1);
|
|
108
|
-
return { machine, models, prompt, file, json, save, unload, settings };
|
|
117
|
+
return { machine, models, prompt, file, json, save, unload, stdin, settings };
|
|
109
118
|
}
|
|
110
119
|
function tokPerSec(chunk) {
|
|
111
120
|
if (chunk.eval_count == null || chunk.eval_duration == null || chunk.eval_duration <= 0) {
|
|
@@ -121,19 +130,20 @@ function resolveModelName(available, query) {
|
|
|
121
130
|
const prefix = available.filter((n) => n.toLowerCase().startsWith(`${q}:`) || q.startsWith(`${n.toLowerCase()}:`));
|
|
122
131
|
return prefix.length === 1 ? prefix[0] : undefined;
|
|
123
132
|
}
|
|
133
|
+
/** Result table on stdout (like bench); notices and saved paths stay on stderr. */
|
|
124
134
|
function printSummary(record) {
|
|
125
135
|
const nameWidth = Math.max(8, ...record.results.map((r) => r.model.length));
|
|
126
|
-
process.
|
|
127
|
-
process.
|
|
136
|
+
process.stdout.write(`Compare on ${record.machine} ${record.results.length} models ${record.prompt.length} char prompt\n`);
|
|
137
|
+
process.stdout.write(`${"model".padEnd(nameWidth)} ${"tok/s".padStart(7)} ${"tokens".padStart(6)} ${"wall".padStart(7)} done\n`);
|
|
128
138
|
for (const r of record.results) {
|
|
129
139
|
if (r.error) {
|
|
130
|
-
process.
|
|
140
|
+
process.stdout.write(`${r.model.padEnd(nameWidth)} ERROR ${r.error}\n`);
|
|
131
141
|
continue;
|
|
132
142
|
}
|
|
133
143
|
const toks = r.tok_s != null ? r.tok_s.toFixed(1) : "—";
|
|
134
144
|
const tokens = r.eval_count != null ? String(r.eval_count) : "—";
|
|
135
145
|
const wall = r.wall_ms != null ? `${(r.wall_ms / 1000).toFixed(2)}s` : "—";
|
|
136
|
-
process.
|
|
146
|
+
process.stdout.write(`${r.model.padEnd(nameWidth)} ${toks.padStart(7)} ${tokens.padStart(6)} ${wall.padStart(7)} ${r.done_reason ?? "—"}\n`);
|
|
137
147
|
}
|
|
138
148
|
}
|
|
139
149
|
export async function runCompare(opts) {
|
|
@@ -173,7 +183,7 @@ export async function runCompare(opts) {
|
|
|
173
183
|
}
|
|
174
184
|
}
|
|
175
185
|
const settings = mergeSettings({ think: false }, config.defaults, machineSettingsForHost(config, host), settingsFromEnv(), opts.settings);
|
|
176
|
-
const quiet = opts.quiet === true || opts.writeStdout === false;
|
|
186
|
+
const quiet = opts.quiet === true || opts.writeStdout === false || !showProgress();
|
|
177
187
|
const results = [];
|
|
178
188
|
for (let i = 0; i < resolved.length; i += 1) {
|
|
179
189
|
const model = resolved[i];
|
|
@@ -253,8 +263,17 @@ export async function main() {
|
|
|
253
263
|
// `compare desk other:7b` → alias model vs other on alias host
|
|
254
264
|
models = [alias.model, models[0]];
|
|
255
265
|
}
|
|
256
|
-
// Same
|
|
257
|
-
|
|
266
|
+
// Same stdin rules as prompt: --stdin waits for EOF; otherwise a guarded read
|
|
267
|
+
// so a never-closing pipe (agents, CI) cannot hang.
|
|
268
|
+
let fromStdin;
|
|
269
|
+
try {
|
|
270
|
+
fromStdin = await readPromptStdin(parsed.stdin);
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
console.error(err instanceof Error ? err.message : err);
|
|
274
|
+
process.exitCode = 1;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
258
277
|
let fileText;
|
|
259
278
|
if (parsed.file) {
|
|
260
279
|
try {
|
|
@@ -267,6 +286,10 @@ export async function main() {
|
|
|
267
286
|
}
|
|
268
287
|
}
|
|
269
288
|
const prompt = assemblePrompt({ argv: parsed.prompt, file: fileText, stdin: fromStdin });
|
|
289
|
+
const quiet = isQuiet();
|
|
290
|
+
if (!prompt && !quiet) {
|
|
291
|
+
process.stderr.write("Using default compare prompt (pass --prompt or --file to override).\n");
|
|
292
|
+
}
|
|
270
293
|
const record = await runCompare({
|
|
271
294
|
machine,
|
|
272
295
|
models,
|
|
@@ -278,15 +301,12 @@ export async function main() {
|
|
|
278
301
|
quiet: parsed.json,
|
|
279
302
|
config,
|
|
280
303
|
});
|
|
281
|
-
if (record.default_prompt) {
|
|
282
|
-
process.stderr.write("Using default compare prompt (pass --prompt or --file to override).\n");
|
|
283
|
-
}
|
|
284
304
|
if (parsed.json) {
|
|
285
305
|
process.stdout.write(`${JSON.stringify(record, null, 2)}\n`);
|
|
286
306
|
return;
|
|
287
307
|
}
|
|
288
308
|
printSummary(record);
|
|
289
|
-
if (record.files) {
|
|
309
|
+
if (record.files && !quiet) {
|
|
290
310
|
process.stderr.write(`saved ${record.files.md}\n`);
|
|
291
311
|
process.stderr.write(` ${record.files.json}\n`);
|
|
292
312
|
process.stderr.write(`compares dir: ${comparesDir()}\n`);
|
package/dist/funnel.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface ExecResult {
|
|
2
|
+
/** Exit code, or null when the process could not start or was killed. */
|
|
3
|
+
code: number | null;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
timedOut: boolean;
|
|
7
|
+
/** The binary was not found. */
|
|
8
|
+
missing: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ExecOptions {
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
/** Stop early (kill the process) once combined output matches. */
|
|
13
|
+
until?: RegExp;
|
|
14
|
+
}
|
|
15
|
+
export type TailscaleExec = (args: string[], opts: ExecOptions) => Promise<ExecResult>;
|
|
16
|
+
export type TailscaleExecSync = (args: string[], opts: {
|
|
17
|
+
timeoutMs: number;
|
|
18
|
+
}) => ExecResult;
|
|
19
|
+
export declare const FUNNEL_PORTS: readonly [443, 8443, 10000];
|
|
20
|
+
export declare const HTTPS_ADMIN_URL = "https://login.tailscale.com/admin/dns";
|
|
21
|
+
export declare const FUNNEL_DOCS_URL = "https://tailscale.com/kb/1223/funnel";
|
|
22
|
+
export declare const defaultExec: TailscaleExec;
|
|
23
|
+
export declare const defaultExecSync: TailscaleExecSync;
|
|
24
|
+
/** A Funnel setup problem with a message meant for the person at the terminal. */
|
|
25
|
+
export declare class FunnelError extends Error {
|
|
26
|
+
readonly enableUrl?: string | undefined;
|
|
27
|
+
constructor(message: string, enableUrl?: string | undefined);
|
|
28
|
+
}
|
|
29
|
+
interface TailscaleSelf {
|
|
30
|
+
DNSName?: string;
|
|
31
|
+
CapMap?: Record<string, unknown>;
|
|
32
|
+
Capabilities?: string[];
|
|
33
|
+
}
|
|
34
|
+
export interface Preflight {
|
|
35
|
+
/** MagicDNS name without the trailing dot. */
|
|
36
|
+
dnsName: string;
|
|
37
|
+
/** Funnel node attribute present (Self.CapMap / Capabilities). */
|
|
38
|
+
funnelAllowed: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function hasFunnelCap(self: TailscaleSelf | undefined): boolean;
|
|
41
|
+
/** `tailscale status --json`: installed, running, MagicDNS name, HTTPS certs, Funnel attr. */
|
|
42
|
+
export declare function preflight(exec?: TailscaleExec): Promise<Preflight>;
|
|
43
|
+
export declare function funnelOnArgs(funnelPort: number, target: string): string[];
|
|
44
|
+
export declare function funnelOffArgs(funnelPort: number): string[];
|
|
45
|
+
export declare function publicUrl(dnsName: string, funnelPort: number): string;
|
|
46
|
+
/**
|
|
47
|
+
* What `tailscale funnel status --json` serves at https://<dns>:<port>/.
|
|
48
|
+
* Returns the proxy target, null when nothing is configured there, or
|
|
49
|
+
* undefined when the status could not be read.
|
|
50
|
+
*/
|
|
51
|
+
export declare function currentHandler(exec: TailscaleExec, dnsName: string, funnelPort: number): Promise<string | null | undefined>;
|
|
52
|
+
export interface FunnelHandle {
|
|
53
|
+
publicUrl: string;
|
|
54
|
+
/** False when an identical funnel was already up; stop() then leaves it alone. */
|
|
55
|
+
added: boolean;
|
|
56
|
+
/** Turn off only what we added. Synchronous so it can run in an exit handler. */
|
|
57
|
+
stop(): void;
|
|
58
|
+
}
|
|
59
|
+
export interface StartFunnelOptions {
|
|
60
|
+
localPort: number;
|
|
61
|
+
/** Local URL Funnel proxies to, e.g. http://127.0.0.1:11500 */
|
|
62
|
+
target: string;
|
|
63
|
+
funnelPort: number;
|
|
64
|
+
exec?: TailscaleExec;
|
|
65
|
+
execSync?: TailscaleExecSync;
|
|
66
|
+
/** Time allowed for `tailscale funnel --bg` (default 30s). */
|
|
67
|
+
timeoutMs?: number;
|
|
68
|
+
}
|
|
69
|
+
export declare function startFunnel(opts: StartFunnelOptions): Promise<FunnelHandle>;
|
|
70
|
+
export {};
|