polymath-agent 0.4.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +21 -1
  2. package/dist/cli.js +283 -3
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -42,7 +42,25 @@ beats a pricey generalist at edits. Polymath assigns the cheapest model that gen
42
42
 
43
43
  ```bash
44
44
  npm install -g polymath-agent
45
- poly login # guided OpenRouter key setup
45
+ poly setup # guided: optionally install a local LLM (Ollama) + connect models
46
+ ```
47
+
48
+ `poly setup` asks whether to install a local LLM, or skip the prompt with a flag:
49
+
50
+ ```bash
51
+ poly setup --local # install Ollama + pull a model (RAM-aware default)
52
+ poly setup --local -m qwen2.5-coder:7b # choose the model
53
+ poly setup --no-local # cloud only — just connect an OpenRouter key
54
+ poly setup --local -y # non-interactive (accept defaults / auto-install)
55
+ ```
56
+
57
+ Keep everything current with `poly update` (the CLI via npm, the Ollama runtime, and
58
+ your local models) — add `--check` to only report what's available:
59
+
60
+ ```bash
61
+ poly update # update CLI + Ollama + re-pull local models
62
+ poly update --check # report-only
63
+ poly update --self # just the CLI (also --ollama, --models)
46
64
  ```
47
65
 
48
66
  **From source** (no npm publish needed):
@@ -88,6 +106,8 @@ poly usage # cost by date + model
88
106
 
89
107
  | Command | What it does |
90
108
  |---|---|
109
+ | `poly setup` | First-run: optionally install a local LLM (Ollama) + connect models. `--local` / `--no-local` / `-m <model>` / `-y`. |
110
+ | `poly update` | Update the CLI (npm), the Ollama runtime, and local models. `--check`, `--self`, `--ollama`, `--models`. |
91
111
  | `poly login` | Connect/replace your OpenRouter API key (Claude-Code-style onboarding). |
92
112
  | `poly run [goal]` | Launch the interactive agent. Shows the recommended routing, then executes. |
93
113
  | `poly recommend <goal>` | Pre-run recommendation: cheapest / best-value / best-quality model combos + savings. |
package/dist/cli.js CHANGED
@@ -2180,6 +2180,274 @@ function logCompletion(result, taskType, sessionId, command = "run") {
2180
2180
  return entry;
2181
2181
  }
2182
2182
 
2183
+ // src/setup/commands.ts
2184
+ import { execSync as execSync2 } from "node:child_process";
2185
+
2186
+ // src/util/prompt.ts
2187
+ import readline2 from "node:readline";
2188
+ function interactive() {
2189
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
2190
+ }
2191
+ function ask2(question) {
2192
+ return new Promise((resolve2) => {
2193
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
2194
+ rl.question(question, (a) => {
2195
+ rl.close();
2196
+ resolve2(a.trim());
2197
+ });
2198
+ });
2199
+ }
2200
+ async function confirm(question, def = true) {
2201
+ if (!interactive()) return def;
2202
+ const hint = def ? "[Y/n]" : "[y/N]";
2203
+ const a = (await ask2(`${question} ${hint} `)).toLowerCase();
2204
+ if (!a) return def;
2205
+ return /^y/.test(a);
2206
+ }
2207
+ async function select(question, items, render2) {
2208
+ if (!interactive() || items.length <= 1) return items[0];
2209
+ console.log(question);
2210
+ items.forEach((it, i) => console.log(` ${i + 1}) ${render2(it)}`));
2211
+ const a = await ask2(`Choose [1-${items.length}] (default 1): `);
2212
+ const n = parseInt(a, 10);
2213
+ return Number.isInteger(n) && n >= 1 && n <= items.length ? items[n - 1] : items[0];
2214
+ }
2215
+
2216
+ // src/setup/localllm.ts
2217
+ import { execSync, spawn } from "node:child_process";
2218
+ import os2 from "node:os";
2219
+ function suggestModels() {
2220
+ const ramGb = Math.round(os2.totalmem() / 1024 ** 3);
2221
+ const list = [];
2222
+ if (ramGb >= 13) list.push({ id: "qwen2.5-coder:7b", label: "Qwen2.5 Coder 7B", sizeGb: 4.7, note: "best coding pick for ~16GB" });
2223
+ list.push({ id: "llama3.2:3b", label: "Llama 3.2 3B", sizeGb: 2, note: "fast, light; great for cheap tasks" });
2224
+ if (ramGb >= 30) list.push({ id: "qwen2.5-coder:14b", label: "Qwen2.5 Coder 14B", sizeGb: 9, note: "stronger coding for 32GB+" });
2225
+ return list;
2226
+ }
2227
+ function totalRamGb() {
2228
+ return Math.round(os2.totalmem() / 1024 ** 3);
2229
+ }
2230
+ function which(cmd) {
2231
+ try {
2232
+ execSync(process.platform === "win32" ? `where ${cmd}` : `command -v ${cmd}`, { stdio: "ignore" });
2233
+ return true;
2234
+ } catch {
2235
+ return false;
2236
+ }
2237
+ }
2238
+ function ollamaInstalled() {
2239
+ return which("ollama");
2240
+ }
2241
+ function ollamaVersion() {
2242
+ try {
2243
+ return execSync("ollama --version", { encoding: "utf8" }).trim();
2244
+ } catch {
2245
+ return null;
2246
+ }
2247
+ }
2248
+ async function ollamaServerUp(baseUrl = "http://localhost:11434") {
2249
+ try {
2250
+ const res = await fetch(`${baseUrl.replace(/\/v1\/?$/, "")}/api/version`);
2251
+ return res.ok;
2252
+ } catch {
2253
+ return false;
2254
+ }
2255
+ }
2256
+ async function installedModels(baseUrl = "http://localhost:11434") {
2257
+ try {
2258
+ const res = await fetch(`${baseUrl.replace(/\/v1\/?$/, "")}/api/tags`);
2259
+ if (!res.ok) return [];
2260
+ const json = await res.json();
2261
+ return (json.models ?? []).map((m) => m.name);
2262
+ } catch {
2263
+ return [];
2264
+ }
2265
+ }
2266
+ function run(cmd, args) {
2267
+ return new Promise((resolve2) => {
2268
+ const child = spawn(cmd, args, { stdio: "inherit" });
2269
+ child.on("error", () => resolve2(false));
2270
+ child.on("exit", (code) => resolve2(code === 0));
2271
+ });
2272
+ }
2273
+ function ollamaInstallPlan() {
2274
+ const platform = process.platform;
2275
+ if (platform === "darwin") {
2276
+ if (which("brew")) return { canAuto: true, command: { cmd: "brew", args: ["install", "ollama"] }, manual: "brew install ollama" };
2277
+ return { canAuto: false, manual: "Install Homebrew (https://brew.sh) then `brew install ollama`, or download https://ollama.com/download" };
2278
+ }
2279
+ if (platform === "linux") {
2280
+ return { canAuto: true, command: { cmd: "sh", args: ["-c", "curl -fsSL https://ollama.com/install.sh | sh"] }, manual: "curl -fsSL https://ollama.com/install.sh | sh" };
2281
+ }
2282
+ if (platform === "win32") {
2283
+ if (which("winget")) return { canAuto: true, command: { cmd: "winget", args: ["install", "-e", "--id", "Ollama.Ollama"] }, manual: "winget install Ollama.Ollama" };
2284
+ return { canAuto: false, manual: "Download the installer from https://ollama.com/download" };
2285
+ }
2286
+ return { canAuto: false, manual: "See https://ollama.com/download" };
2287
+ }
2288
+ async function ensureServer(baseUrl = "http://localhost:11434") {
2289
+ if (await ollamaServerUp(baseUrl)) return true;
2290
+ if (process.platform === "darwin" && which("brew")) {
2291
+ await run("brew", ["services", "start", "ollama"]);
2292
+ } else {
2293
+ try {
2294
+ const child = spawn("ollama", ["serve"], { stdio: "ignore", detached: true });
2295
+ child.unref();
2296
+ } catch {
2297
+ }
2298
+ }
2299
+ for (let i = 0; i < 10; i++) {
2300
+ if (await ollamaServerUp(baseUrl)) return true;
2301
+ await delay(500);
2302
+ }
2303
+ return false;
2304
+ }
2305
+ function delay(ms) {
2306
+ return new Promise((r) => setTimeout(r, ms));
2307
+ }
2308
+
2309
+ // src/setup/commands.ts
2310
+ async function runSetup(opts) {
2311
+ console.log(c.bold("\n\u{1F527} Polymath setup\n"));
2312
+ const config = loadConfig();
2313
+ let wantLocal = opts.local;
2314
+ if (wantLocal === void 0) {
2315
+ wantLocal = await confirm(
2316
+ `Install a local LLM (Ollama) for $0, offline, no-API-key runs? (RAM detected: ${totalRamGb()}GB)`,
2317
+ true
2318
+ );
2319
+ }
2320
+ if (wantLocal) {
2321
+ await setupLocal(opts, config);
2322
+ } else {
2323
+ config.local.enabled = false;
2324
+ saveConfig(config);
2325
+ console.log(c.dim("Skipping local LLM. (You can run `poly setup --local` later.)"));
2326
+ }
2327
+ const freshConfig = loadConfig();
2328
+ if (!resolveApiKey(freshConfig)) {
2329
+ const wantKey = opts.yes ? false : await confirm("Connect an OpenRouter API key for cloud models (300+ models)?", !wantLocal);
2330
+ if (wantKey) await runLogin();
2331
+ else if (!wantLocal) console.log(c.yellow("No models configured yet \u2014 run `poly login` or `poly setup --local`."));
2332
+ }
2333
+ console.log(c.green("\n\u2713 Setup complete.") + c.dim(' Try: poly recommend "add a dark-mode toggle" \xB7 poly run -w "..."'));
2334
+ }
2335
+ async function setupLocal(opts, config) {
2336
+ if (!ollamaInstalled()) {
2337
+ const plan = ollamaInstallPlan();
2338
+ console.log(c.cyan("Local LLM runtime: Ollama is not installed."));
2339
+ if (plan.canAuto && plan.command) {
2340
+ const go = opts.yes || await confirm(`Install Ollama via \`${plan.command.cmd} ${plan.command.args.join(" ")}\`?`, true);
2341
+ if (go) {
2342
+ const ok = await run(plan.command.cmd, plan.command.args);
2343
+ if (!ok) console.log(c.yellow("Auto-install failed. Manual: " + plan.manual));
2344
+ } else {
2345
+ console.log(c.dim("Manual install: " + plan.manual));
2346
+ }
2347
+ } else {
2348
+ console.log(c.yellow("Install manually: " + plan.manual));
2349
+ }
2350
+ } else {
2351
+ console.log(c.green("\u2713 Ollama present ") + c.dim(ollamaVersion() ?? ""));
2352
+ }
2353
+ if (!ollamaInstalled()) {
2354
+ console.log(c.yellow("Ollama still not on PATH \u2014 re-run `poly setup --local` after installing."));
2355
+ return;
2356
+ }
2357
+ process.stdout.write("Starting Ollama server\u2026 ");
2358
+ const up = await ensureServer(config.local.baseUrl);
2359
+ console.log(up ? c.green("ok") : c.yellow("could not confirm (start it with `ollama serve`)"));
2360
+ const have = await installedModels(config.local.baseUrl);
2361
+ let modelId = opts.model;
2362
+ if (!modelId) {
2363
+ const suggestions = suggestModels().filter((s) => !have.includes(s.id));
2364
+ if (have.length && !suggestions.length) {
2365
+ modelId = have[0];
2366
+ console.log(c.dim(`Using already-installed model ${modelId}.`));
2367
+ } else {
2368
+ const pick = opts.yes ? suggestModels()[0] : await select(
2369
+ "Pick a model to download:",
2370
+ suggestModels(),
2371
+ (s) => `${s.label} (~${s.sizeGb}GB) \u2014 ${s.note}${have.includes(s.id) ? " [installed]" : ""}`
2372
+ );
2373
+ modelId = pick.id;
2374
+ }
2375
+ }
2376
+ if (!have.includes(modelId)) {
2377
+ console.log(c.cyan(`Downloading ${modelId}\u2026`));
2378
+ const ok = await run("ollama", ["pull", modelId]);
2379
+ if (!ok) {
2380
+ console.log(c.yellow(`Could not pull ${modelId}. Run \`ollama pull ${modelId}\` manually.`));
2381
+ return;
2382
+ }
2383
+ }
2384
+ config.local.enabled = true;
2385
+ saveConfig(config);
2386
+ console.log(c.green(`\u2713 Local LLM ready: ${modelId} \u2192 local/${modelId} ($0). `) + c.dim("Enabled in config."));
2387
+ }
2388
+ function cmp(a, b) {
2389
+ const pa = a.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
2390
+ const pb = b.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
2391
+ for (let i = 0; i < 3; i++) {
2392
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
2393
+ }
2394
+ return 0;
2395
+ }
2396
+ async function runUpdate(currentVersion, opts) {
2397
+ const all = !opts.self && !opts.ollama && !opts.models;
2398
+ console.log(c.bold("\n\u2B06\uFE0F Polymath update") + (opts.check ? c.dim(" (check only)") : "") + "\n");
2399
+ if (all || opts.self) {
2400
+ let latest = "";
2401
+ try {
2402
+ latest = execSync2("npm view polymath-agent version", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2403
+ } catch {
2404
+ latest = "";
2405
+ }
2406
+ if (!latest) {
2407
+ console.log(c.dim("CLI: could not reach npm registry."));
2408
+ } else if (cmp(latest, currentVersion) > 0) {
2409
+ console.log(c.yellow(`CLI: ${currentVersion} \u2192 ${latest} available.`));
2410
+ if (!opts.check) {
2411
+ const ok = await run("npm", ["install", "-g", `polymath-agent@${latest}`]);
2412
+ console.log(ok ? c.green(`\u2713 Updated to ${latest}.`) : c.red("npm update failed (try: sudo npm i -g polymath-agent@latest)."));
2413
+ } else {
2414
+ console.log(c.dim(" Run `poly update` to install."));
2415
+ }
2416
+ } else {
2417
+ console.log(c.green(`\u2713 CLI is up to date (${currentVersion}).`));
2418
+ }
2419
+ }
2420
+ if (all || opts.ollama) {
2421
+ if (!ollamaInstalled()) {
2422
+ console.log(c.dim("Ollama: not installed (run `poly setup --local`)."));
2423
+ } else if (opts.check) {
2424
+ console.log(c.dim(`Ollama: ${ollamaVersion() ?? "present"} (update with \`poly update --ollama\`).`));
2425
+ } else if (process.platform === "darwin") {
2426
+ console.log(c.cyan("Updating Ollama\u2026"));
2427
+ await run("brew", ["upgrade", "ollama"]).then((ok) => !ok && console.log(c.dim(" (brew upgrade skipped/failed)")));
2428
+ } else if (process.platform === "linux") {
2429
+ await run("sh", ["-c", "curl -fsSL https://ollama.com/install.sh | sh"]);
2430
+ } else {
2431
+ console.log(c.dim("Ollama: update via your installer (winget upgrade Ollama.Ollama)."));
2432
+ }
2433
+ }
2434
+ if (all || opts.models) {
2435
+ const config = loadConfig();
2436
+ const models = await installedModels(config.local.baseUrl);
2437
+ if (!models.length) {
2438
+ console.log(c.dim("Models: none installed."));
2439
+ } else if (opts.check) {
2440
+ console.log(c.dim(`Models: ${models.join(", ")} (re-pull to update).`));
2441
+ } else {
2442
+ for (const m of models) {
2443
+ console.log(c.cyan(`Updating ${m}\u2026`));
2444
+ await run("ollama", ["pull", m]);
2445
+ }
2446
+ }
2447
+ }
2448
+ console.log("");
2449
+ }
2450
+
2183
2451
  // src/tui/App.tsx
2184
2452
  import { useState, useEffect, useCallback } from "react";
2185
2453
  import { Box, Text, useApp, useInput } from "ink";
@@ -2189,7 +2457,7 @@ import Spinner from "ink-spinner";
2189
2457
  // src/agent/tools.ts
2190
2458
  import fs4 from "node:fs";
2191
2459
  import path2 from "node:path";
2192
- import { execSync } from "node:child_process";
2460
+ import { execSync as execSync3 } from "node:child_process";
2193
2461
  var TOOL_SCHEMAS = [
2194
2462
  {
2195
2463
  type: "function",
@@ -2332,7 +2600,7 @@ function executeTool(name, argsJson, ctx) {
2332
2600
  }
2333
2601
  case "run_command": {
2334
2602
  if (!ctx.allowCommands) return { result: "Denied: run_command is disabled." };
2335
- const out = execSync(String(args.command), {
2603
+ const out = execSync3(String(args.command), {
2336
2604
  cwd: ctx.cwd,
2337
2605
  encoding: "utf8",
2338
2606
  env: scrubbedEnv(),
@@ -3013,8 +3281,9 @@ function truncate2(s, n) {
3013
3281
  }
3014
3282
 
3015
3283
  // src/index.ts
3284
+ var VERSION = "0.5.0";
3016
3285
  var program = new Command();
3017
- program.name("poly").description("Polymath \u2014 cost-optimized, multi-model TUI coding agent").version("0.4.0");
3286
+ program.name("poly").description("Polymath \u2014 cost-optimized, multi-model TUI coding agent").version(VERSION);
3018
3287
  function client(config) {
3019
3288
  return new OpenRouterClient({
3020
3289
  apiKey: resolveApiKey(config),
@@ -3085,6 +3354,14 @@ async function loadCatalog(config, refresh = false) {
3085
3354
  }
3086
3355
  return models;
3087
3356
  }
3357
+ program.command("setup").description("First-run setup: optionally install a local LLM (Ollama) and connect models").option("--local", "install a local LLM (Ollama) \u2014 skips the prompt").option("--no-local", "skip the local LLM \u2014 skips the prompt").option("-m, --model <id>", "local model to pull (e.g. qwen2.5-coder:7b)").option("-y, --yes", "accept defaults / auto-install without prompts", false).action(async (opts) => {
3358
+ const argv = process.argv;
3359
+ const local = argv.includes("--local") ? true : argv.includes("--no-local") ? false : void 0;
3360
+ await runSetup({ local, model: opts.model, yes: !!opts.yes });
3361
+ });
3362
+ program.command("update").description("Update Polymath, the Ollama runtime, and local models").option("--check", "report available updates without installing", false).option("--self", "only the Polymath CLI", false).option("--ollama", "only the Ollama runtime", false).option("--models", "only the local models", false).action(async (opts) => {
3363
+ await runUpdate(VERSION, { check: !!opts.check, self: !!opts.self, ollama: !!opts.ollama, models: !!opts.models });
3364
+ });
3088
3365
  program.command("login").description("Connect Polymath to OpenRouter (set/replace your API key)").action(async () => {
3089
3366
  await runLogin();
3090
3367
  });
@@ -3294,3 +3571,6 @@ program.parseAsync().catch((err) => {
3294
3571
  console.error(c.red(err?.message ?? String(err)));
3295
3572
  process.exit(1);
3296
3573
  });
3574
+ export {
3575
+ VERSION
3576
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polymath-agent",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Polymath — a cost-optimized, multi-model TUI coding agent. Decomposes work into typed tasks, routes each task to the cheapest capable model via OpenRouter, and logs real usage/cost by date + model.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,8 @@
18
18
  "dev": "tsx src/index.ts",
19
19
  "start": "node dist/cli.js",
20
20
  "prepare": "node esbuild.config.mjs",
21
- "prepublishOnly": "npm run typecheck && npm run build"
21
+ "prepublishOnly": "npm run typecheck && npm run build",
22
+ "postinstall": "node -e \"process.stdout.isTTY && console.log('\\n\\u2728 Polymath ready. Run: poly setup (connect models / optionally install a local LLM)\\n')\" || true"
22
23
  },
23
24
  "keywords": [
24
25
  "ai",