@freesyntax/notch-cli 0.5.20 → 0.5.22

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.
Files changed (40) hide show
  1. package/dist/{apply-patch-D5PDUXUC.js → apply-patch-U6K67CMT.js} +1 -0
  2. package/dist/auth-UAMMP5IJ.js +29 -0
  3. package/dist/chunk-4HPRBCSY.js +167 -0
  4. package/dist/chunk-6NKRMZTX.js +198 -0
  5. package/dist/{chunk-YBYF7L4A.js → chunk-EPSOOCNB.js} +1832 -1331
  6. package/dist/chunk-FZVPGJJW.js +511 -0
  7. package/dist/chunk-GFVLHUSS.js +155 -0
  8. package/dist/chunk-J66N6AFH.js +137 -0
  9. package/dist/chunk-JXQ4HZ47.js +544 -0
  10. package/dist/chunk-KCAR5DOB.js +52 -0
  11. package/dist/chunk-KFQGP6VL.js +33 -0
  12. package/dist/chunk-O6AKZ4OH.js +0 -0
  13. package/dist/{chunk-6M6CXXWR.js → chunk-PKZKVOAN.js} +209 -1
  14. package/dist/{chunk-FIFC4V2R.js → chunk-PPEBWOMJ.js} +91 -7
  15. package/dist/compression-YJLWEHCC.js +33 -0
  16. package/dist/config-set-3IWEVZQ4.js +110 -0
  17. package/dist/{edit-JEFEK43H.js → edit-6QYAXVNU.js} +1 -0
  18. package/dist/{git-5T5TSQTX.js → git-DNQ5EELH.js} +1 -0
  19. package/dist/{github-DWRGWX6U.js → github-34T4QQIH.js} +1 -0
  20. package/dist/{glob-BI3P4C7Q.js → glob-XT43LEJ4.js} +1 -0
  21. package/dist/{grep-VZ3I5GNW.js → grep-T2CXYNRI.js} +1 -0
  22. package/dist/index.js +2606 -960
  23. package/dist/{lsp-UPY6I3L7.js → lsp-JXQVU7NP.js} +1 -0
  24. package/dist/model-download-3NDKS3VM.js +176 -0
  25. package/dist/{notebook-FXJBTSPA.js → notebook-MFODW345.js} +1 -0
  26. package/dist/ollama-bench-5V5CCOCQ.js +194 -0
  27. package/dist/ollama-launch-P5KBK7AJ.js +22 -0
  28. package/dist/ollama-usage-3PROM2WC.js +70 -0
  29. package/dist/{plugins-OG2P75K5.js → plugins-PNGRZLFW.js} +1 -0
  30. package/dist/{read-OVJG2XKW.js → read-B64XE7N3.js} +1 -0
  31. package/dist/{server-W7FRCVRZ.js → server-IGOZHW52.js} +17 -15
  32. package/dist/session-index-7FWEVP6E.js +22 -0
  33. package/dist/{shell-4X545EVN.js → shell-BOZTHQUT.js} +1 -0
  34. package/dist/{task-OS3E5F3X.js → task-67G4KLYC.js} +1 -0
  35. package/dist/{tools-Q7CDHB4K.js → tools-XWKCW4RN.js} +4 -1
  36. package/dist/{web-fetch-KNIV3Z3W.js → web-fetch-OTNDICGJ.js} +1 -0
  37. package/dist/{write-NNHLOTYK.js → write-ZOSB7I4J.js} +1 -0
  38. package/package.json +2 -1
  39. package/dist/auth-JQX6MHJG.js +0 -16
  40. package/dist/compression-UTB2Y4BB.js +0 -16
@@ -0,0 +1,511 @@
1
+ import {
2
+ OLLAMA_CLOUD_BASE_URL,
3
+ OLLAMA_LOCAL_BASE_URL,
4
+ detectDaemon,
5
+ formatBytes,
6
+ isCloudBaseUrl,
7
+ isCloudModel,
8
+ listModels,
9
+ pullModel,
10
+ showModel
11
+ } from "./chunk-GFVLHUSS.js";
12
+ import {
13
+ clearOllamaCreds,
14
+ ollamaCredsPath,
15
+ readOllamaCreds,
16
+ writeOllamaCreds
17
+ } from "./chunk-KCAR5DOB.js";
18
+ import {
19
+ findByokProvider
20
+ } from "./chunk-JXQ4HZ47.js";
21
+
22
+ // src/commands/ollama-launch.ts
23
+ import fs from "fs/promises";
24
+ import path from "path";
25
+ import readline from "readline";
26
+ import chalk from "chalk";
27
+ var readCreds = readOllamaCreds;
28
+ var writeCreds = writeOllamaCreds;
29
+ var clearCreds = clearOllamaCreds;
30
+ var credsPath = ollamaCredsPath;
31
+ function parseFlags(argv) {
32
+ const out = { positional: [], cloud: false, yes: false, help: false };
33
+ for (let i = 0; i < argv.length; i++) {
34
+ const a = argv[i];
35
+ if (a === "--cloud") out.cloud = true;
36
+ else if (a === "-y" || a === "--yes") out.yes = true;
37
+ else if (a === "--help" || a === "-h") out.help = true;
38
+ else if (a === "--model" || a === "-m") out.model = argv[++i];
39
+ else if (a.startsWith("--model=")) out.model = a.slice("--model=".length);
40
+ else if (a === "--host") out.host = argv[++i];
41
+ else if (a.startsWith("--host=")) out.host = a.slice("--host=".length);
42
+ else if (a === "--api-key") out.apiKey = argv[++i];
43
+ else if (a.startsWith("--api-key=")) out.apiKey = a.slice("--api-key=".length);
44
+ else if (a === "--compat") {
45
+ const v = argv[++i];
46
+ if (v === "openai" || v === "anthropic") out.compat = v;
47
+ } else if (a.startsWith("--compat=")) {
48
+ const v = a.slice("--compat=".length);
49
+ if (v === "openai" || v === "anthropic") out.compat = v;
50
+ } else if (!a.startsWith("-")) {
51
+ out.positional.push(a);
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+ async function resolveEndpoint(flags) {
57
+ let baseUrl;
58
+ if (flags.host) {
59
+ baseUrl = flags.host;
60
+ } else if (flags.cloud) {
61
+ baseUrl = OLLAMA_CLOUD_BASE_URL;
62
+ } else if (process.env.OLLAMA_HOST) {
63
+ baseUrl = process.env.OLLAMA_HOST.startsWith("http") ? process.env.OLLAMA_HOST : `http://${process.env.OLLAMA_HOST}`;
64
+ } else {
65
+ baseUrl = OLLAMA_LOCAL_BASE_URL;
66
+ }
67
+ const mode = isCloudBaseUrl(baseUrl) ? "cloud" : "local";
68
+ let apiKey = flags.apiKey ?? process.env.OLLAMA_API_KEY ?? void 0;
69
+ if (!apiKey && mode === "cloud") {
70
+ const stored = await readCreds();
71
+ if (stored && stored.endpoint === baseUrl) {
72
+ apiKey = stored.apiKey;
73
+ }
74
+ }
75
+ return { baseUrl, apiKey, mode };
76
+ }
77
+ async function promptLine(prompt, opts = {}) {
78
+ if (!process.stdin.isTTY) {
79
+ throw new Error("Interactive prompt requested but stdin is not a TTY.");
80
+ }
81
+ if (!opts.mask) {
82
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
83
+ const answer = await new Promise((resolve) => {
84
+ rl.question(prompt, (ans) => resolve(ans));
85
+ });
86
+ rl.close();
87
+ return answer.trim();
88
+ }
89
+ return new Promise((resolve, reject) => {
90
+ process.stdout.write(prompt);
91
+ const stdin = process.stdin;
92
+ const wasRaw = stdin.isRaw;
93
+ stdin.setRawMode(true);
94
+ stdin.resume();
95
+ let buf = "";
96
+ const onData = (chunk) => {
97
+ for (const byte of chunk) {
98
+ if (byte === 3) {
99
+ cleanup();
100
+ process.stdout.write("\n");
101
+ reject(new Error("Cancelled"));
102
+ return;
103
+ }
104
+ if (byte === 13 || byte === 10) {
105
+ cleanup();
106
+ process.stdout.write("\n");
107
+ resolve(buf.trim());
108
+ return;
109
+ }
110
+ if (byte === 127 || byte === 8) {
111
+ if (buf.length > 0) {
112
+ buf = buf.slice(0, -1);
113
+ process.stdout.write("\b \b");
114
+ }
115
+ continue;
116
+ }
117
+ if (byte < 32) continue;
118
+ buf += String.fromCharCode(byte);
119
+ process.stdout.write("*");
120
+ }
121
+ };
122
+ const cleanup = () => {
123
+ stdin.removeListener("data", onData);
124
+ stdin.setRawMode(wasRaw ?? false);
125
+ stdin.pause();
126
+ };
127
+ stdin.on("data", onData);
128
+ });
129
+ }
130
+ function pickFromList(items, title) {
131
+ if (items.length === 0) return Promise.resolve(null);
132
+ if (!process.stdin.isTTY) return Promise.resolve(items[0] ?? null);
133
+ return new Promise((resolve) => {
134
+ let cursor = 0;
135
+ const render = (first) => {
136
+ if (!first) process.stdout.write(`\x1B[${items.length + 2}A\x1B[J`);
137
+ process.stdout.write(chalk.gray(` ${title} (\u2191\u2193 to move, Enter to select, Esc to cancel)
138
+
139
+ `));
140
+ for (let i = 0; i < items.length; i++) {
141
+ const pointer = i === cursor ? chalk.cyan("\u276F") : " ";
142
+ const label = i === cursor ? chalk.bold(items[i]) : chalk.gray(items[i]);
143
+ process.stdout.write(` ${pointer} ${label}
144
+ `);
145
+ }
146
+ };
147
+ render(true);
148
+ const stdin = process.stdin;
149
+ const wasRaw = stdin.isRaw;
150
+ stdin.setRawMode(true);
151
+ stdin.resume();
152
+ const onKey = (key) => {
153
+ const s = key.toString();
154
+ if (s === "\x1B[A") {
155
+ cursor = (cursor - 1 + items.length) % items.length;
156
+ render(false);
157
+ } else if (s === "\x1B[B") {
158
+ cursor = (cursor + 1) % items.length;
159
+ render(false);
160
+ } else if (s === "\r" || s === "\n") {
161
+ cleanup();
162
+ resolve(items[cursor] ?? null);
163
+ } else if (s === "\x1B" || s === "") {
164
+ cleanup();
165
+ resolve(null);
166
+ }
167
+ };
168
+ const cleanup = () => {
169
+ stdin.removeListener("data", onKey);
170
+ stdin.setRawMode(wasRaw ?? false);
171
+ stdin.pause();
172
+ };
173
+ stdin.on("data", onKey);
174
+ });
175
+ }
176
+ function renderProgressLine(ev, lastStatus) {
177
+ if (ev.kind === "progress") {
178
+ const width = 30;
179
+ const filled = Math.max(0, Math.min(width, Math.round(ev.percent / 100 * width)));
180
+ const bar = `${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}`;
181
+ const suffix = `${ev.percent.toFixed(1).padStart(5)}% ${formatBytes(ev.completed)}/${formatBytes(ev.total)}`;
182
+ const label = ev.message || lastStatus.label;
183
+ lastStatus.label = label;
184
+ process.stdout.write(`\r ${chalk.cyan(bar)} ${chalk.gray(suffix)} ${chalk.dim(label)}\x1B[K`);
185
+ } else if (ev.kind === "status") {
186
+ lastStatus.label = ev.message;
187
+ process.stdout.write(`\r ${chalk.gray(ev.message)}\x1B[K`);
188
+ } else {
189
+ process.stdout.write("\n");
190
+ }
191
+ }
192
+ async function persistOllamaChoice(projectRoot, opts) {
193
+ const p = path.resolve(projectRoot, ".notch.json");
194
+ let current = {};
195
+ try {
196
+ const raw = await fs.readFile(p, "utf-8");
197
+ current = JSON.parse(raw);
198
+ } catch (err) {
199
+ const code = err.code;
200
+ if (code && code !== "ENOENT") throw err;
201
+ }
202
+ const byok = {
203
+ provider: opts.providerId,
204
+ model: opts.modelId
205
+ };
206
+ if (opts.baseUrl) byok.baseUrl = opts.baseUrl;
207
+ if (opts.apiShape) byok.apiShape = opts.apiShape;
208
+ current.byok = byok;
209
+ await fs.writeFile(p, JSON.stringify(current, null, 2) + "\n", "utf-8");
210
+ }
211
+ async function cmdList(flags) {
212
+ const { baseUrl, apiKey, mode } = await resolveEndpoint(flags);
213
+ const ok = await detectDaemon(baseUrl, { apiKey });
214
+ if (!ok) {
215
+ printNotReachable(baseUrl, mode);
216
+ return 1;
217
+ }
218
+ let rows;
219
+ try {
220
+ rows = await listModels(baseUrl, { apiKey });
221
+ } catch (err) {
222
+ console.error(chalk.red(` ${err.message}`));
223
+ return 1;
224
+ }
225
+ if (rows.length === 0) {
226
+ console.log(chalk.gray(`
227
+ No models installed at ${baseUrl}.`));
228
+ console.log(chalk.gray(` Pull one: notch ollama pull llama3.2:latest
229
+ `));
230
+ return 0;
231
+ }
232
+ const header = ` ${"name".padEnd(36)} ${"params".padEnd(8)} ${"quant".padEnd(10)} ${"size".padStart(8)}`;
233
+ console.log(chalk.gray(`
234
+ ${mode === "cloud" ? "Ollama Cloud" : "Ollama local"} @ ${baseUrl}
235
+ `));
236
+ console.log(chalk.gray(header));
237
+ console.log(chalk.gray(` ${"-".repeat(header.length - 2)}`));
238
+ for (const r of rows) {
239
+ console.log(
240
+ ` ${chalk.white(r.name.padEnd(36))} ${chalk.gray(r.parameterSize.padEnd(8))} ${chalk.gray(r.quantization.padEnd(10))} ${chalk.gray(formatBytes(r.sizeBytes))}`
241
+ );
242
+ }
243
+ console.log("");
244
+ return 0;
245
+ }
246
+ async function cmdPull(flags) {
247
+ const modelName = flags.positional[0] ?? flags.model;
248
+ if (!modelName) {
249
+ console.error(chalk.red(" Usage: notch ollama pull <model>"));
250
+ return 2;
251
+ }
252
+ const { baseUrl, apiKey, mode } = await resolveEndpoint(flags);
253
+ const ok = await detectDaemon(baseUrl, { apiKey });
254
+ if (!ok) {
255
+ printNotReachable(baseUrl, mode);
256
+ return 1;
257
+ }
258
+ console.log(chalk.cyan(`
259
+ Pulling ${chalk.bold(modelName)} from ${baseUrl}...
260
+ `));
261
+ const lastStatus = { label: "" };
262
+ try {
263
+ for await (const ev of pullModel(modelName, baseUrl, { apiKey })) {
264
+ renderProgressLine(ev, lastStatus);
265
+ }
266
+ } catch (err) {
267
+ process.stdout.write("\n");
268
+ console.error(chalk.red(` Pull failed: ${err.message}`));
269
+ return 1;
270
+ }
271
+ console.log(chalk.green(` \u2713 ${modelName} ready.
272
+ `));
273
+ return 0;
274
+ }
275
+ async function cmdStatus(flags) {
276
+ const { baseUrl, apiKey, mode } = await resolveEndpoint(flags);
277
+ const ok = await detectDaemon(baseUrl, { apiKey });
278
+ console.log("");
279
+ console.log(chalk.gray(" Endpoint"), chalk.white(baseUrl));
280
+ console.log(chalk.gray(" Mode "), chalk.white(mode));
281
+ console.log(chalk.gray(" Auth "), apiKey ? chalk.green("yes (api key)") : chalk.yellow("none"));
282
+ console.log(chalk.gray(" Reach "), ok ? chalk.green("ok") : chalk.red("unreachable"));
283
+ console.log("");
284
+ return ok ? 0 : 1;
285
+ }
286
+ async function cmdLogin(flags) {
287
+ const endpoint = flags.host ?? OLLAMA_CLOUD_BASE_URL;
288
+ console.log(chalk.cyan(`
289
+ Ollama login \u2014 paste your API key for ${endpoint}`));
290
+ console.log(chalk.gray(` Get one at https://ollama.com/settings/keys
291
+ `));
292
+ let apiKey = flags.apiKey;
293
+ if (!apiKey) {
294
+ apiKey = await promptLine(" API key: ", { mask: true });
295
+ }
296
+ if (!apiKey) {
297
+ console.error(chalk.red(" No key provided."));
298
+ return 2;
299
+ }
300
+ const reachable = await detectDaemon(endpoint, { apiKey, timeoutMs: 8e3 });
301
+ if (!reachable) {
302
+ console.error(chalk.red(` Could not authenticate against ${endpoint} with the provided key.`));
303
+ return 1;
304
+ }
305
+ await writeCreds({ apiKey, endpoint, createdAt: Date.now() });
306
+ console.log(chalk.green(` \u2713 Key stored at ${credsPath()}`));
307
+ console.log(chalk.gray(` The CLI will auto-load it when you run \`notch ollama --cloud\`.
308
+ `));
309
+ return 0;
310
+ }
311
+ async function cmdLogout(_flags) {
312
+ const existed = await readCreds();
313
+ await clearCreds();
314
+ if (existed) {
315
+ console.log(chalk.green(`
316
+ \u2713 Cleared Ollama credentials for ${existed.endpoint}
317
+ `));
318
+ } else {
319
+ console.log(chalk.gray("\n No stored Ollama credentials.\n"));
320
+ }
321
+ return 0;
322
+ }
323
+ async function cmdLaunch(flags, opts) {
324
+ const { baseUrl, apiKey, mode } = await resolveEndpoint(flags);
325
+ const ok = await detectDaemon(baseUrl, { apiKey });
326
+ if (!ok) {
327
+ printNotReachable(baseUrl, mode);
328
+ return 1;
329
+ }
330
+ let rows = [];
331
+ try {
332
+ rows = await listModels(baseUrl, { apiKey });
333
+ } catch (err) {
334
+ console.error(chalk.red(` Could not list models: ${err.message}`));
335
+ return 1;
336
+ }
337
+ let chosen = flags.model;
338
+ if (!chosen) {
339
+ if (rows.length === 0) {
340
+ console.log(chalk.yellow(`
341
+ No models installed at ${baseUrl}.`));
342
+ const suggest = ["qwen3-coder:30b", "gpt-oss:20b", "llama3.2:latest"];
343
+ const pick = await pickFromList(
344
+ suggest,
345
+ "Pick one to pull"
346
+ );
347
+ if (!pick) return 0;
348
+ chosen = pick;
349
+ const pullCode = await cmdPull({ ...flags, positional: [chosen] });
350
+ if (pullCode !== 0) return pullCode;
351
+ } else {
352
+ const pick = await pickFromList(rows.map((r) => r.name), "Select a model");
353
+ if (!pick) return 0;
354
+ chosen = pick;
355
+ }
356
+ } else if (mode === "local" && !rows.some((r) => r.name === chosen) && !isCloudModel(chosen)) {
357
+ const consent = flags.yes ? true : (await promptLine(` Model ${chosen} isn't installed. Pull now? [Y/n] `)).toLowerCase() !== "n";
358
+ if (consent) {
359
+ const pullCode = await cmdPull({ ...flags, positional: [chosen] });
360
+ if (pullCode !== 0) return pullCode;
361
+ } else {
362
+ console.error(chalk.red(" Aborted: model not installed."));
363
+ return 1;
364
+ }
365
+ }
366
+ if (mode === "local") {
367
+ try {
368
+ const info = await showModel(chosen, baseUrl, { apiKey });
369
+ if (typeof info.contextLength === "number" && info.contextLength < 64e3) {
370
+ console.warn(
371
+ chalk.yellow(
372
+ ` \u26A0 ${chosen} exposes only ${info.contextLength.toLocaleString()} tokens of context. Notch recommends \u226564,000 \u2014 agent loops will truncate aggressively.`
373
+ )
374
+ );
375
+ }
376
+ } catch {
377
+ }
378
+ }
379
+ const compat = flags.compat ?? "openai";
380
+ let providerId;
381
+ if (mode === "cloud") {
382
+ providerId = "ollama-cloud";
383
+ if (flags.compat === "anthropic") {
384
+ console.warn(chalk.yellow(
385
+ " ! --compat anthropic is ignored for Ollama Cloud (only the local daemon exposes /v1/messages)."
386
+ ));
387
+ }
388
+ } else if (compat === "anthropic") {
389
+ providerId = "ollama-anthropic";
390
+ } else {
391
+ providerId = "ollama";
392
+ }
393
+ const providerInfo = findByokProvider(providerId);
394
+ if (!providerInfo) {
395
+ console.error(chalk.red(` Internal: provider ${providerId} missing from catalog.`));
396
+ return 1;
397
+ }
398
+ try {
399
+ await persistOllamaChoice(opts.projectRoot, {
400
+ providerId,
401
+ modelId: chosen,
402
+ // Only persist baseUrl when it differs from the provider default
403
+ // — otherwise the catalog default carries forward and survives
404
+ // future CLI upgrades.
405
+ baseUrl: baseUrl === providerInfo.baseUrl || mode === "cloud" && baseUrl === OLLAMA_CLOUD_BASE_URL ? void 0 : baseUrl,
406
+ apiShape: providerInfo.apiShape
407
+ });
408
+ } catch (err) {
409
+ console.warn(chalk.yellow(` ! Could not write .notch.json: ${err.message}`));
410
+ }
411
+ console.log("");
412
+ console.log(chalk.green(` \u2713 Ollama configured.`));
413
+ console.log(chalk.gray(" provider "), chalk.white(providerId));
414
+ console.log(chalk.gray(" model "), chalk.white(chosen));
415
+ console.log(chalk.gray(" endpoint "), chalk.white(baseUrl));
416
+ console.log(chalk.gray(" compat "), chalk.white(providerInfo.apiShape ?? "openai"));
417
+ console.log("");
418
+ if (mode === "cloud" && apiKey && !process.env.OLLAMA_API_KEY) {
419
+ console.log(chalk.gray(` Ollama Cloud key loaded from ${credsPath()}`));
420
+ }
421
+ console.log(chalk.gray(` Run: notch (the selection is saved in .notch.json)`));
422
+ console.log("");
423
+ return 0;
424
+ }
425
+ function printNotReachable(baseUrl, mode) {
426
+ console.error(chalk.red(`
427
+ Cannot reach Ollama at ${baseUrl}.`));
428
+ if (mode === "local") {
429
+ console.error(chalk.gray(` Start the daemon: ollama serve`));
430
+ console.error(chalk.gray(` Or install: https://ollama.com/download
431
+ `));
432
+ } else {
433
+ console.error(chalk.gray(` Check that OLLAMA_API_KEY is set (notch ollama login).
434
+ `));
435
+ }
436
+ }
437
+ function printHelp() {
438
+ console.log(`
439
+ Notch CLI \u2014 Ollama integration
440
+
441
+ Usage:
442
+ notch ollama Interactive setup + launch
443
+ notch ollama launch Same as above (explicit)
444
+ notch ollama list List installed / cloud models
445
+ notch ollama pull <model> Pull a model with a progress bar
446
+ notch ollama status Show daemon reachability
447
+ notch ollama login Store OLLAMA_API_KEY for Cloud
448
+ notch ollama logout Clear stored Ollama credentials
449
+ notch ollama bench Benchmark multiple models on a prompt
450
+
451
+ Flags (for every subcommand):
452
+ --cloud Use https://ollama.com (Ollama Cloud)
453
+ --host <url> Override the daemon base URL
454
+ --model, -m <name> Model to use / pull / bench
455
+ --api-key <key> API key override (env: OLLAMA_API_KEY)
456
+ --compat <openai|anthropic> Wire protocol for local daemon
457
+ -y, --yes Auto-confirm destructive prompts
458
+
459
+ Examples:
460
+ notch ollama Detect daemon, pick a model, save to .notch.json
461
+ notch ollama --cloud Target Ollama Cloud
462
+ notch ollama --compat anthropic --model qwen3-coder:30b
463
+ notch ollama pull llama3.2:latest
464
+ notch ollama bench "summarize this repo" --model llama3.2,qwen3-coder:30b
465
+ `);
466
+ }
467
+ async function runOllamaCli(argv, cwd) {
468
+ const flags = parseFlags(argv);
469
+ if (flags.help && flags.positional.length === 0) {
470
+ printHelp();
471
+ return 0;
472
+ }
473
+ const sub = flags.positional[0] ?? "launch";
474
+ const subFlags = { ...flags, positional: flags.positional.slice(1) };
475
+ switch (sub) {
476
+ case "launch":
477
+ case void 0:
478
+ return cmdLaunch(subFlags, { projectRoot: cwd });
479
+ case "list":
480
+ case "ls":
481
+ return cmdList(subFlags);
482
+ case "pull":
483
+ return cmdPull(subFlags);
484
+ case "status":
485
+ return cmdStatus(subFlags);
486
+ case "login":
487
+ return cmdLogin(subFlags);
488
+ case "logout":
489
+ return cmdLogout(subFlags);
490
+ case "bench": {
491
+ const { runOllamaBench } = await import("./ollama-bench-5V5CCOCQ.js");
492
+ return runOllamaBench(subFlags, { projectRoot: cwd });
493
+ }
494
+ case "help":
495
+ printHelp();
496
+ return 0;
497
+ default:
498
+ console.error(chalk.red(` Unknown subcommand: ollama ${sub}`));
499
+ printHelp();
500
+ return 2;
501
+ }
502
+ }
503
+
504
+ export {
505
+ parseFlags,
506
+ resolveEndpoint,
507
+ renderProgressLine,
508
+ persistOllamaChoice,
509
+ printNotReachable,
510
+ runOllamaCli
511
+ };
@@ -0,0 +1,155 @@
1
+ // src/providers/ollama.ts
2
+ var OLLAMA_LOCAL_BASE_URL = "http://localhost:11434";
3
+ var OLLAMA_CLOUD_BASE_URL = "https://ollama.com";
4
+ var DEFAULT_TIMEOUT_MS = 5e3;
5
+ function authHeaders(apiKey) {
6
+ return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
7
+ }
8
+ function joinUrl(baseUrl, path) {
9
+ const root = baseUrl.replace(/\/+$/, "");
10
+ const suffix = path.startsWith("/") ? path : `/${path}`;
11
+ return `${root}${suffix}`;
12
+ }
13
+ async function detectDaemon(baseUrl = OLLAMA_LOCAL_BASE_URL, opts = {}) {
14
+ try {
15
+ const res = await fetch(joinUrl(baseUrl, "/api/tags"), {
16
+ signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
17
+ headers: authHeaders(opts.apiKey)
18
+ });
19
+ return res.ok;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+ async function listModels(baseUrl = OLLAMA_LOCAL_BASE_URL, opts = {}) {
25
+ const res = await fetch(joinUrl(baseUrl, "/api/tags"), {
26
+ signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
27
+ headers: authHeaders(opts.apiKey)
28
+ });
29
+ if (!res.ok) {
30
+ throw new Error(`Ollama ${baseUrl} returned ${res.status} for /api/tags`);
31
+ }
32
+ const body = await res.json();
33
+ const models = body.models ?? [];
34
+ return models.map((m) => ({
35
+ name: m.name,
36
+ parameterSize: m.details?.parameter_size ?? "\u2014",
37
+ quantization: m.details?.quantization_level ?? "\u2014",
38
+ sizeBytes: m.size ?? 0,
39
+ modifiedAt: m.modified_at
40
+ }));
41
+ }
42
+ async function showModel(modelName, baseUrl = OLLAMA_LOCAL_BASE_URL, opts = {}) {
43
+ const res = await fetch(joinUrl(baseUrl, "/api/show"), {
44
+ method: "POST",
45
+ signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
46
+ headers: { "Content-Type": "application/json", ...authHeaders(opts.apiKey) },
47
+ body: JSON.stringify({ name: modelName })
48
+ });
49
+ if (!res.ok) {
50
+ throw new Error(`Ollama /api/show(${modelName}) returned ${res.status}`);
51
+ }
52
+ const body = await res.json();
53
+ const info = body.model_info ?? {};
54
+ let contextLength;
55
+ for (const [key, value] of Object.entries(info)) {
56
+ if (key.endsWith(".context_length") && typeof value === "number") {
57
+ contextLength = value;
58
+ break;
59
+ }
60
+ }
61
+ return {
62
+ contextLength,
63
+ parameterSize: body.details?.parameter_size,
64
+ family: body.details?.family,
65
+ modelInfo: info
66
+ };
67
+ }
68
+ async function* pullModel(modelName, baseUrl = OLLAMA_LOCAL_BASE_URL, opts = {}) {
69
+ const res = await fetch(joinUrl(baseUrl, "/api/pull"), {
70
+ method: "POST",
71
+ signal: opts.signal,
72
+ headers: { "Content-Type": "application/json", ...authHeaders(opts.apiKey) },
73
+ body: JSON.stringify({ name: modelName, stream: true })
74
+ });
75
+ if (!res.ok || !res.body) {
76
+ const text = await res.text().catch(() => "");
77
+ throw new Error(`Ollama /api/pull returned ${res.status}${text ? `: ${text.slice(0, 200)}` : ""}`);
78
+ }
79
+ const reader = res.body.getReader();
80
+ const decoder = new TextDecoder();
81
+ let buf = "";
82
+ let done = false;
83
+ while (!done) {
84
+ const { value, done: streamDone } = await reader.read();
85
+ if (streamDone) break;
86
+ buf += decoder.decode(value, { stream: true });
87
+ let nl = buf.indexOf("\n");
88
+ while (nl >= 0) {
89
+ const line = buf.slice(0, nl).trim();
90
+ buf = buf.slice(nl + 1);
91
+ nl = buf.indexOf("\n");
92
+ if (!line) continue;
93
+ let frame;
94
+ try {
95
+ frame = JSON.parse(line);
96
+ } catch {
97
+ continue;
98
+ }
99
+ if (frame.error) {
100
+ throw new Error(`Ollama pull error: ${frame.error}`);
101
+ }
102
+ const status = frame.status ?? "";
103
+ if (status === "success") {
104
+ done = true;
105
+ break;
106
+ }
107
+ if (typeof frame.total === "number" && typeof frame.completed === "number" && frame.total > 0) {
108
+ yield {
109
+ kind: "progress",
110
+ message: status,
111
+ completed: frame.completed,
112
+ total: frame.total,
113
+ percent: Math.min(100, frame.completed / frame.total * 100)
114
+ };
115
+ } else if (status) {
116
+ yield { kind: "status", message: status };
117
+ }
118
+ }
119
+ }
120
+ yield { kind: "done" };
121
+ }
122
+ function isCloudBaseUrl(baseUrl) {
123
+ try {
124
+ const host = new URL(baseUrl).hostname;
125
+ return host === "ollama.com" || host.endsWith(".ollama.com");
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+ function isCloudModel(modelName) {
131
+ return /(^|[:\-])cloud$/i.test(modelName) || modelName.endsWith(":cloud");
132
+ }
133
+ function formatBytes(n) {
134
+ if (!n || n <= 0) return " \u2014";
135
+ const units = ["B", "KB", "MB", "GB", "TB"];
136
+ let v = n;
137
+ let i = 0;
138
+ while (v >= 1024 && i < units.length - 1) {
139
+ v /= 1024;
140
+ i++;
141
+ }
142
+ return `${v.toFixed(v >= 10 ? 0 : 1)} ${units[i]}`.padStart(8);
143
+ }
144
+
145
+ export {
146
+ OLLAMA_LOCAL_BASE_URL,
147
+ OLLAMA_CLOUD_BASE_URL,
148
+ detectDaemon,
149
+ listModels,
150
+ showModel,
151
+ pullModel,
152
+ isCloudBaseUrl,
153
+ isCloudModel,
154
+ formatBytes
155
+ };