ollanet 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,248 @@
1
+ # ollanet
2
+
3
+ Chat with **Ollama** servers on **any network you can reach** — LAN, localhost, Tailscale, VPN, or a raw IP.
4
+
5
+ Scan for models, fire prompts, and continue conversations later by a short hash. No browser UI required.
6
+
7
+ ## Origin
8
+
9
+ This started as a selfish little script. I had Ollama running on a laptop on my Tailscale network and wanted an easy way to discover it, poke at the models, and actually talk to them from another machine — without babysitting IPs or opening a browser. One scan/prompt tool later, chats by hash showed up, LAN discovery crept in, and the thing grew into **ollanet**: a tiny CLI for using Ollama over whatever network you already have.
10
+
11
+ ## Related: Finetuna
12
+
13
+ **ollanet** is the client-side / network tool. On the machine that *runs* Ollama, use **[Finetuna](https://github.com/Catalyst-Forge-LLC/finetuna)** to turn a stock model into a GPU-tuned, named variant (context, batch, Modelfile, auto-tune).
14
+
15
+ Typical loop:
16
+
17
+ 1. On the host: `finetuna` → create something like `gemma4-ctx32k`
18
+ 2. From anywhere on the network: `ollanet scan` → `ollanet prompt that-host gemma4-ctx32k "…"`
19
+
20
+ They share the same Ollama API; Finetuna shapes the models, ollanet finds and chats with them.
21
+
22
+ ## Features
23
+
24
+ - **Discover** Ollama hosts from localhost, `config.hosts`, `OLLANET_HOSTS`, optional Tailscale, and optional LAN scan
25
+ - **Prompt** any hostname/IP/model with streaming replies
26
+ - **Persist** chats as `responses/<hash>.json` with topic, machine, model, timestamps
27
+ - **Continue** any thread with `--chat <hash>`
28
+ - **Configure** per-machine defaults (model, temperature, context, …)
29
+
30
+ ## Requirements
31
+
32
+ - Node.js 20+
33
+ - One or more [Ollama](https://ollama.com) servers reachable on port `11434` (or a custom port)
34
+
35
+ Tailscale is optional. If the `tailscale` CLI is present, peers are included automatically.
36
+
37
+ ## Install
38
+
39
+ Run without installing:
40
+
41
+ ```bash
42
+ npx ollanet scan # from npm (once published)
43
+ npx --allow-git=all github:Catalyst-Forge-LLC/ollanet scan # straight from GitHub
44
+ ```
45
+
46
+ > npm 12+ blocks git dependencies by default, hence `--allow-git=all` for the
47
+ > GitHub form (npm ≤11 doesn't need it). Note that npx runs a one-off command —
48
+ > it does not put `ollanet` on your PATH.
49
+
50
+ Or install globally, which puts the `ollanet` command on your PATH:
51
+
52
+ ```bash
53
+ npm install -g ollanet # from npm (once published)
54
+ npm install -g --allow-git=all github:Catalyst-Forge-LLC/ollanet # from GitHub
55
+ ollanet scan
56
+ ```
57
+
58
+ When installed this way, config lives at `~/.ollanet/config.json` and chats are
59
+ saved to `~/.ollanet/responses/`.
60
+
61
+ ### From a checkout (development)
62
+
63
+ ```bash
64
+ cd ollanet
65
+ pnpm install # also builds dist/ via the prepare hook
66
+ pnpm link --global # optional: put `ollanet` on your PATH
67
+
68
+ # Or without a global link:
69
+ pnpm ollanet -- help
70
+ pnpm scan
71
+ pnpm prompt -- localhost "hello"
72
+ pnpm chats
73
+ ```
74
+
75
+ A checkout uses the repo-local `config.json` and `responses/` instead of `~/.ollanet/`.
76
+
77
+ ## Quick start
78
+
79
+ ```bash
80
+ # Find Ollama servers (localhost + configured hosts + Tailscale if available)
81
+ ollanet scan
82
+
83
+ # Also TCP-scan your local /24s for open Ollama ports
84
+ ollanet scan --lan
85
+
86
+ # Talk to a machine by name, MagicDNS name, or IP
87
+ ollanet prompt localhost "What is MagicDNS?"
88
+ ollanet prompt 192.168.1.50 gemma4:12b "Hello from the LAN"
89
+
90
+ # Continue later
91
+ ollanet prompt --chat a1b2c3d4e5f6 "Give a concrete example"
92
+
93
+ # Browse saved chats
94
+ ollanet chats
95
+ ollanet chats --id a1b2c3d4e5f6
96
+ ```
97
+
98
+ ## Commands
99
+
100
+ | Command | Description |
101
+ |---|---|
102
+ | `ollanet scan` | Probe known/discovered hosts for Ollama + list models |
103
+ | `ollanet prompt …` | Send a prompt / continue a chat |
104
+ | `ollanet chats` | List or inspect saved transcripts |
105
+
106
+ ### `scan` options
107
+
108
+ - `--json` — machine-readable output
109
+ - `--all` — also probe offline Tailscale peers
110
+ - `--lan` — scan local LAN CIDRs for open Ollama ports
111
+
112
+ ### `prompt` options
113
+
114
+ ```text
115
+ ollanet prompt <machine|ip> [model] <prompt...>
116
+ ollanet prompt --chat <hash> <prompt...>
117
+ ```
118
+
119
+ | Flag | Meaning |
120
+ |---|---|
121
+ | `--chat <hash>` | Continue a saved chat |
122
+ | `--machine <name>` | Explicit machine (or override when continuing) |
123
+ | `--model <name>` | Model override |
124
+ | `--system <text>` | System prompt |
125
+ | `--temperature <n>` | Sampling temperature |
126
+ | `--num-predict <n>` | Max tokens to generate |
127
+ | `--num-ctx <n>` | Context window |
128
+ | `--keep-alive <value>` | Keep model loaded (`5m`, `0`, `-1`, …) |
129
+ | `--format json\|<schema>` | JSON mode / schema |
130
+ | `--no-stream` | Buffer the full reply |
131
+ | `--no-save` | Do not write a transcript |
132
+ | `--json` | Emit JSON (includes chat id when saved) |
133
+
134
+ Machine can be a discovered name, hostname, FQDN, or IP (`192.168.1.50`, `host:11434`). Direct addresses work even if they were never scanned.
135
+
136
+ ### `chats` options
137
+
138
+ - `--json` — list summary JSON
139
+ - `--id <hash>` — show one chat (full JSON with `--json`)
140
+
141
+ ## Discovery
142
+
143
+ ollanet combines several sources (deduped by `ip:port`):
144
+
145
+ | Source | When |
146
+ |---|---|
147
+ | localhost | Always (unless disabled) |
148
+ | `config.hosts` | Always when configured |
149
+ | `OLLANET_HOSTS` | Env comma/space list of hosts/IPs |
150
+ | Tailscale | If `tailscale status --json` works |
151
+ | LAN scan | `ollanet scan --lan` or `discovery.lan: true` |
152
+
153
+ ## Configuration
154
+
155
+ Edit `~/.ollanet/config.json` (installed) or `config.json` (checkout) — or point
156
+ `OLLANET_CONFIG` at another file:
157
+
158
+ ```json
159
+ {
160
+ "hosts": [
161
+ "192.168.1.50",
162
+ { "name": "studio", "host": "studio.local", "port": 11434 }
163
+ ],
164
+ "discovery": {
165
+ "localhost": true,
166
+ "tailscale": true,
167
+ "lan": false,
168
+ "cidrs": ["192.168.1.0/24"]
169
+ },
170
+ "defaultModels": {
171
+ "localhost": "llama3.2:1b",
172
+ "studio": "gemma4:12b"
173
+ },
174
+ "defaults": {
175
+ "temperature": 0.7,
176
+ "num_predict": 512,
177
+ "keep_alive": "5m"
178
+ },
179
+ "machineDefaults": {
180
+ "studio": { "num_ctx": 16384 }
181
+ }
182
+ }
183
+ ```
184
+
185
+ **Precedence:** CLI flags → env vars → `machineDefaults` → `defaults`.
186
+
187
+ ### Environment
188
+
189
+ | Variable | Purpose |
190
+ |---|---|
191
+ | `OLLANET_CONFIG` | Path to config JSON |
192
+ | `OLLANET_RESPONSES_DIR` | Chat transcript directory |
193
+ | `OLLANET_HOSTS` | Extra hosts/IPs to include |
194
+ | `OLLANET_LAN_TIMEOUT_MS` | Per-IP LAN probe timeout (default `200`) |
195
+ | `OLLANET_LAN_CONCURRENCY` | LAN probe concurrency (default `64`) |
196
+ | `OLLAMA_PORT` | Default Ollama port (`11434`) |
197
+ | `OLLAMA_TEMPERATURE` | Default temperature |
198
+ | `OLLAMA_NUM_PREDICT` | Default max tokens |
199
+ | `OLLAMA_NUM_CTX` | Default context size |
200
+ | `OLLAMA_KEEP_ALIVE` | Default keep-alive |
201
+ | `OLLAMA_FORMAT` | Default format (`json` or schema JSON) |
202
+ | `OLLAMA_SYSTEM` | Default system prompt |
203
+ | `OLLAMA_TIMEOUT_MS` | HTTP scan/probe timeout |
204
+ | `OLLAMA_CONCURRENCY` | Scan concurrency |
205
+
206
+ ## Chat transcripts
207
+
208
+ Each saved chat is a JSON file:
209
+
210
+ ```text
211
+ ~/.ollanet/responses/<hash>.json # installed
212
+ responses/<hash>.json # checkout
213
+ ```
214
+
215
+ Fields include `id`, `topic`, `machine`, `model`, `system`, `created_at`, `updated_at`, and a `messages[]` array (`role`, `content`, `timestamp`, plus assistant `stats`).
216
+
217
+ The first turn asks the model for a short topic title (falls back to the prompt text).
218
+
219
+ ## Project layout
220
+
221
+ ```text
222
+ ollanet/
223
+ src/
224
+ cli.ts # subcommand router (CLI entry)
225
+ hosts.ts # discovery + host resolution
226
+ scan.ts
227
+ prompt.ts
228
+ chats.ts
229
+ chat-store.ts
230
+ config.ts
231
+ paths.ts # checkout vs installed path resolution
232
+ dist/ # compiled output (published to npm)
233
+ config.json
234
+ responses/ # gitignored chat history
235
+ ```
236
+
237
+ ## Development
238
+
239
+ ```bash
240
+ pnpm install # builds dist/ via prepare
241
+ pnpm typecheck
242
+ pnpm ollanet -- scan # runs from source via tsx
243
+ pnpm build # compile dist/ manually
244
+ ```
245
+
246
+ ## License
247
+
248
+ MIT
@@ -0,0 +1,131 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { defaultResponsesDir } from "./paths.js";
5
+ const RESPONSES_DIR = process.env.OLLANET_RESPONSES_DIR ??
6
+ process.env.OLLAMA_RESPONSES_DIR ??
7
+ defaultResponsesDir();
8
+ export function responsesDir() {
9
+ return RESPONSES_DIR;
10
+ }
11
+ export function newChatId() {
12
+ return randomBytes(6).toString("hex");
13
+ }
14
+ export function chatPath(id) {
15
+ const safe = normalizeChatId(id);
16
+ return path.join(RESPONSES_DIR, `${safe}.json`);
17
+ }
18
+ export function normalizeChatId(id) {
19
+ const cleaned = id.trim().toLowerCase().replace(/\.json$/i, "");
20
+ if (!/^[a-f0-9]{8,32}$/.test(cleaned)) {
21
+ throw new Error(`Invalid chat id "${id}". Expected a hex hash from a prior prompt.`);
22
+ }
23
+ return cleaned;
24
+ }
25
+ export async function ensureResponsesDir() {
26
+ await mkdir(RESPONSES_DIR, { recursive: true });
27
+ return RESPONSES_DIR;
28
+ }
29
+ export async function loadChat(id) {
30
+ const safe = normalizeChatId(id);
31
+ try {
32
+ const raw = await readFile(chatPath(safe), "utf8");
33
+ const parsed = JSON.parse(raw);
34
+ if (!parsed.id || !Array.isArray(parsed.messages)) {
35
+ throw new Error("missing id/messages");
36
+ }
37
+ return parsed;
38
+ }
39
+ catch (err) {
40
+ const code = err.code;
41
+ if (code === "ENOENT") {
42
+ throw new Error(`Chat not found: ${safe} (${chatPath(safe)})`);
43
+ }
44
+ throw new Error(`Failed to load chat ${safe}: ${err instanceof Error ? err.message : err}`);
45
+ }
46
+ }
47
+ export async function saveChat(chat) {
48
+ await ensureResponsesDir();
49
+ const file = chatPath(chat.id);
50
+ await writeFile(file, `${JSON.stringify(chat, null, 2)}\n`, "utf8");
51
+ return file;
52
+ }
53
+ export function createChat(opts) {
54
+ const now = new Date().toISOString();
55
+ return {
56
+ id: newChatId(),
57
+ topic: opts.topic?.trim() || "Untitled chat",
58
+ created_at: now,
59
+ updated_at: now,
60
+ machine: opts.machine,
61
+ model: opts.model,
62
+ ...(opts.system ? { system: opts.system } : {}),
63
+ messages: [],
64
+ };
65
+ }
66
+ export function appendMessage(chat, message) {
67
+ const entry = {
68
+ ...message,
69
+ timestamp: message.timestamp ?? new Date().toISOString(),
70
+ };
71
+ chat.messages.push(entry);
72
+ chat.updated_at = entry.timestamp;
73
+ if (message.machine)
74
+ chat.machine = message.machine;
75
+ if (message.model)
76
+ chat.model = message.model;
77
+ return entry;
78
+ }
79
+ export async function listChats() {
80
+ await ensureResponsesDir();
81
+ const files = (await readdir(RESPONSES_DIR))
82
+ .filter((f) => f.endsWith(".json"))
83
+ .sort();
84
+ const chats = [];
85
+ for (const file of files) {
86
+ try {
87
+ const raw = await readFile(path.join(RESPONSES_DIR, file), "utf8");
88
+ const parsed = JSON.parse(raw);
89
+ if (parsed.id && Array.isArray(parsed.messages))
90
+ chats.push(parsed);
91
+ }
92
+ catch {
93
+ // skip corrupt files
94
+ }
95
+ }
96
+ chats.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
97
+ return chats;
98
+ }
99
+ /** Fallback topic from the first user message when model summarization fails. */
100
+ export function topicFromPrompt(prompt) {
101
+ const oneLine = prompt.replace(/\s+/g, " ").trim();
102
+ if (!oneLine)
103
+ return "Untitled chat";
104
+ if (oneLine.length <= 72)
105
+ return oneLine;
106
+ return `${oneLine.slice(0, 69).trimEnd()}...`;
107
+ }
108
+ export function cleanTopic(raw, fallback) {
109
+ const cleaned = raw
110
+ .replace(/^["'`]+|["'`]+$/g, "")
111
+ .replace(/^(topic|title)\s*:\s*/i, "")
112
+ .replace(/\s+/g, " ")
113
+ .trim();
114
+ if (!cleaned)
115
+ return fallback;
116
+ if (cleaned.length <= 80)
117
+ return cleaned;
118
+ return `${cleaned.slice(0, 77).trimEnd()}...`;
119
+ }
120
+ export function toApiMessages(chat) {
121
+ const out = [];
122
+ if (chat.system) {
123
+ out.push({ role: "system", content: chat.system });
124
+ }
125
+ for (const msg of chat.messages) {
126
+ if (msg.role === "system")
127
+ continue;
128
+ out.push({ role: msg.role, content: msg.content });
129
+ }
130
+ return out;
131
+ }
package/dist/chats.js ADDED
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * List saved ollanet chats.
4
+ *
5
+ * Usage:
6
+ * ollanet chats
7
+ * ollanet chats --json
8
+ * ollanet chats --id a1b2c3d4e5f6
9
+ */
10
+ import { loadChat, listChats, responsesDir, } from "./chat-store.js";
11
+ function usage() {
12
+ console.error(`Usage: ollanet chats [--json] [--id <hash>]
13
+
14
+ Lists transcripts in ${responsesDir()}.
15
+
16
+ Options:
17
+ --json Machine-readable output
18
+ --id <hash> Show one chat (messages included with --json, preview otherwise)`);
19
+ process.exit(1);
20
+ }
21
+ function parseArgs(argv) {
22
+ const args = [...argv];
23
+ let json = false;
24
+ let id;
25
+ while (args.length > 0) {
26
+ const arg = args.shift();
27
+ if (arg === "--")
28
+ continue;
29
+ if (arg === "--help" || arg === "-h")
30
+ usage();
31
+ if (arg === "--json") {
32
+ json = true;
33
+ continue;
34
+ }
35
+ if (arg === "--id" || arg.startsWith("--id=")) {
36
+ id = arg.includes("=") ? arg.slice("--id=".length) : args.shift();
37
+ if (!id) {
38
+ console.error("--id requires a value");
39
+ usage();
40
+ }
41
+ continue;
42
+ }
43
+ console.error(`Unknown flag: ${arg}`);
44
+ usage();
45
+ }
46
+ return { json, id };
47
+ }
48
+ function pad(value, width) {
49
+ if (value.length >= width)
50
+ return `${value.slice(0, width - 1)}…`;
51
+ return value.padEnd(width);
52
+ }
53
+ function formatWhen(iso) {
54
+ const d = new Date(iso);
55
+ if (Number.isNaN(d.getTime()))
56
+ return iso;
57
+ const yyyy = d.getFullYear();
58
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
59
+ const dd = String(d.getDate()).padStart(2, "0");
60
+ const hh = String(d.getHours()).padStart(2, "0");
61
+ const mi = String(d.getMinutes()).padStart(2, "0");
62
+ return `${yyyy}-${mm}-${dd} ${hh}:${mi}`;
63
+ }
64
+ function summarize(chat) {
65
+ return {
66
+ id: chat.id,
67
+ topic: chat.topic,
68
+ machine: chat.machine,
69
+ model: chat.model,
70
+ messages: chat.messages.length,
71
+ created_at: chat.created_at,
72
+ updated_at: chat.updated_at,
73
+ system: chat.system ?? null,
74
+ };
75
+ }
76
+ function printTable(chats) {
77
+ if (chats.length === 0) {
78
+ console.log(`No chats in ${responsesDir()}`);
79
+ console.log(`Start one with: ollanet prompt <machine> "hello"`);
80
+ return;
81
+ }
82
+ console.log(`${pad("ID", 14)} ${pad("UPDATED", 17)} ${pad("MACHINE", 18)} ${pad("MODEL", 22)} ${pad("MSGS", 5)} TOPIC`);
83
+ for (const chat of chats) {
84
+ console.log(`${pad(chat.id, 14)} ${pad(formatWhen(chat.updated_at), 17)} ${pad(chat.machine, 18)} ${pad(chat.model, 22)} ${pad(String(chat.messages.length), 5)} ${chat.topic}`);
85
+ }
86
+ console.log(`\n${chats.length} chat(s) in ${responsesDir()}`);
87
+ console.log(`Continue: ollanet prompt --chat <id> "follow-up"`);
88
+ }
89
+ function printPreview(chat) {
90
+ console.log(`id: ${chat.id}`);
91
+ console.log(`topic: ${chat.topic}`);
92
+ console.log(`machine: ${chat.machine}`);
93
+ console.log(`model: ${chat.model}`);
94
+ console.log(`created: ${chat.created_at}`);
95
+ console.log(`updated: ${chat.updated_at}`);
96
+ if (chat.system)
97
+ console.log(`system: ${chat.system}`);
98
+ console.log(`messages: ${chat.messages.length}`);
99
+ console.log("");
100
+ for (const msg of chat.messages) {
101
+ const who = msg.role.padEnd(9);
102
+ const when = formatWhen(msg.timestamp);
103
+ const body = msg.content.replace(/\s+/g, " ").trim();
104
+ const preview = body.length > 160 ? `${body.slice(0, 157)}...` : body;
105
+ console.log(`[${when}] ${who} ${preview}`);
106
+ }
107
+ console.log(`\nContinue: ollanet prompt --chat ${chat.id} "follow-up"`);
108
+ }
109
+ export async function main() {
110
+ const { json, id } = parseArgs(process.argv.slice(2));
111
+ if (id) {
112
+ const chat = await loadChat(id);
113
+ if (json) {
114
+ console.log(JSON.stringify(chat, null, 2));
115
+ }
116
+ else {
117
+ printPreview(chat);
118
+ }
119
+ return;
120
+ }
121
+ const chats = await listChats();
122
+ if (json) {
123
+ console.log(JSON.stringify(chats.map(summarize), null, 2));
124
+ return;
125
+ }
126
+ printTable(chats);
127
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ollanet — chat with Ollama servers on any reachable network.
4
+ *
5
+ * ollanet scan
6
+ * ollanet prompt <machine-or-ip> "hello"
7
+ * ollanet chats
8
+ */
9
+ const HELP = `ollanet — Ollama over your network
10
+
11
+ Discover Ollama hosts on localhost, your LAN, Tailscale, or any IP/hostname
12
+ you can reach — then chat with hash-addressed transcripts.
13
+
14
+ Usage:
15
+ ollanet scan [--json] [--all] [--lan]
16
+ ollanet prompt <machine|ip> [model] <prompt...>
17
+ ollanet prompt --chat <hash> <prompt...>
18
+ ollanet chats [--json] [--id <hash>]
19
+
20
+ Examples:
21
+ ollanet scan
22
+ ollanet scan --lan
23
+ ollanet prompt localhost "What is MagicDNS?"
24
+ ollanet prompt 192.168.1.50 gemma4:12b "Hello"
25
+ ollanet prompt --chat a1b2c3d4e5f6 "Tell me more"
26
+ ollanet chats
27
+
28
+ Config: ~/.ollanet/config.json when installed, ./config.json in a checkout
29
+ (override with OLLANET_CONFIG)
30
+ Hosts: config.hosts, OLLANET_HOSTS, optional Tailscale + --lan
31
+ Chats: ~/.ollanet/responses or ./responses (override with OLLANET_RESPONSES_DIR)
32
+ `;
33
+ async function main() {
34
+ // pnpm/npm often forward a bare `--`; ignore it.
35
+ const argv = process.argv.slice(2).filter((arg) => arg !== "--");
36
+ const cmd = argv[0];
37
+ if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help") {
38
+ process.stdout.write(HELP);
39
+ return;
40
+ }
41
+ if (cmd === "-v" || cmd === "--version" || cmd === "version") {
42
+ const { readFile } = await import("node:fs/promises");
43
+ const { projectPath } = await import("./paths.js");
44
+ const pkg = JSON.parse(await readFile(projectPath("package.json"), "utf8"));
45
+ console.log(`ollanet ${pkg.version ?? "0.0.0"}`);
46
+ return;
47
+ }
48
+ // Strip the subcommand so command modules see their own argv.
49
+ process.argv = [process.argv[0] ?? "node", process.argv[1] ?? "ollanet", ...argv.slice(1)];
50
+ switch (cmd) {
51
+ case "scan":
52
+ case "ls-servers": {
53
+ const { main: run } = await import("./scan.js");
54
+ await run();
55
+ return;
56
+ }
57
+ case "prompt":
58
+ case "ask":
59
+ case "chat": {
60
+ const { main: run } = await import("./prompt.js");
61
+ await run();
62
+ return;
63
+ }
64
+ case "chats":
65
+ case "ls":
66
+ case "history": {
67
+ const { main: run } = await import("./chats.js");
68
+ await run();
69
+ return;
70
+ }
71
+ default:
72
+ console.error(`Unknown command: ${cmd}\n`);
73
+ process.stderr.write(HELP);
74
+ process.exitCode = 1;
75
+ }
76
+ }
77
+ main().catch((err) => {
78
+ console.error(err instanceof Error ? err.message : err);
79
+ process.exitCode = 1;
80
+ });
81
+ export {};