miii-agent 0.1.14 → 0.1.15

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 +31 -2
  2. package/dist/cli.js +98 -30
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  <a href="https://ollama.com"><img src="https://img.shields.io/badge/powered%20by-Ollama-black" alt="powered by Ollama"></a>
17
17
  </p>
18
18
 
19
- miii is a local-first AI coding agent that lives in your terminal. Powered by [Ollama](https://ollama.com), it reads your code, writes features, runs tests, and fixes bugs entirely on your hardware, at native speed.
19
+ miii is a local-first AI coding agent that lives in your terminal. Powered by [Ollama](https://ollama.com) — or any OpenAI-compatible local server like [llama.cpp](https://github.com/ggml-org/llama.cpp) and [LM Studio](https://lmstudio.ai) — it reads your code, writes features, runs tests, and fixes bugs entirely on your hardware, at native speed.
20
20
 
21
21
  ---
22
22
 
@@ -48,7 +48,7 @@ Most AI coding tools are wrappers around cloud APIs. They are slow, expensive, a
48
48
 
49
49
  miii flips the script:
50
50
 
51
- - **Absolute Privacy** — Powered by Ollama. Your code stays on your disk, period.
51
+ - **Absolute Privacy** — Powered by Ollama, llama.cpp, or any local OpenAI-compatible server. Your code stays on your disk, period.
52
52
  - **Zero Friction** — No API keys, no billing, no accounts. Just `miii`.
53
53
  - **True Agency** — miii doesn't just chat; it decomposes problems, invokes tools, and verifies results like a senior engineer.
54
54
  - **Native Performance** — No network round-trips. Latency is limited by your GPU, not a CDN.
@@ -197,6 +197,35 @@ Settings live in `~/.miii/config.json` and are created on first run.
197
197
  | `ollamaHost` | Ollama API endpoint | URL string |
198
198
  | `effort` | Controls temperature & limits | `low` \| `medium` \| `high` |
199
199
 
200
+ ### Other Local Backends
201
+
202
+ Ollama is the default, but miii talks to any **OpenAI-compatible** local server — so you can run [llama.cpp](https://github.com/ggml-org/llama.cpp) or [LM Studio](https://lmstudio.ai) instead. Your code still never leaves your machine.
203
+
204
+ Start `llama-server` (ships with llama.cpp), then point a named provider at it:
205
+
206
+ ```bash
207
+ llama-server -m ./qwen2.5-coder-14b.gguf --port 8080
208
+ ```
209
+
210
+ ```json
211
+ {
212
+ "model": "qwen2.5-coder-14b",
213
+ "provider": "llamacpp",
214
+ "providers": {
215
+ "llamacpp": { "type": "openai", "baseUrl": "http://localhost:8080" }
216
+ }
217
+ }
218
+ ```
219
+
220
+ | Field | Description | Values |
221
+ |-------|-------------|--------|
222
+ | `provider` | Active provider name (keys into `providers`) | e.g. `ollama`, `llamacpp` |
223
+ | `providers.<name>.type` | Wire protocol | `ollama` \| `openai` |
224
+ | `providers.<name>.baseUrl` | Server endpoint | URL string |
225
+ | `providers.<name>.apiKey` | Optional bearer token | string |
226
+
227
+ Switch the active provider at launch with `miii --provider llamacpp`. Any `openai`-type provider on a `localhost` URL counts as local — no key, no cloud.
228
+
200
229
  ---
201
230
 
202
231
  ## System Architecture
package/dist/cli.js CHANGED
@@ -1767,7 +1767,7 @@ import { sep as sep2 } from "path";
1767
1767
  // src/ui/WelcomeBlock.tsx
1768
1768
  import { Box, Text } from "ink";
1769
1769
  import { jsx, jsxs } from "react/jsx-runtime";
1770
- function WelcomeBlock({ model, activeCtx, effort, cwd }) {
1770
+ function WelcomeBlock({ model, activeCtx, effort, cwd, updateAvailable }) {
1771
1771
  const ctxLabel = activeCtx != null ? `${Math.round(activeCtx / 1024)}k ctx` : "\u2014 ctx";
1772
1772
  return /* @__PURE__ */ jsxs(
1773
1773
  Box,
@@ -1790,18 +1790,19 @@ function WelcomeBlock({ model, activeCtx, effort, cwd }) {
1790
1790
  " effort"
1791
1791
  ] })
1792
1792
  ] }),
1793
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: cwd })
1793
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: cwd }),
1794
+ updateAvailable && /* @__PURE__ */ jsx(Text, { color: "yellow", children: `\u2191 update available: v${updateAvailable} \u2014 run: miii --update` })
1794
1795
  ]
1795
1796
  }
1796
1797
  );
1797
1798
  }
1798
1799
 
1799
1800
  // src/ui/InputBar.tsx
1800
- import { useEffect, useState } from "react";
1801
+ import { memo, useEffect, useState } from "react";
1801
1802
  import { Box as Box2, Text as Text2 } from "ink";
1802
1803
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1803
1804
  var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1804
- function InputBar({ input, disabled, processingLabel }) {
1805
+ var InputBar = memo(function InputBar2({ input, disabled, processingLabel }) {
1805
1806
  const [frame, setFrame] = useState(0);
1806
1807
  useEffect(() => {
1807
1808
  if (!disabled) return;
@@ -1829,7 +1830,7 @@ function InputBar({ input, disabled, processingLabel }) {
1829
1830
  ] })
1830
1831
  }
1831
1832
  );
1832
- }
1833
+ });
1833
1834
 
1834
1835
  // src/ui/ModelsView.tsx
1835
1836
  import { Box as Box3, Text as Text3 } from "ink";
@@ -1837,7 +1838,7 @@ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1837
1838
  function ModelsView({ models, cursor, model, host, provider, effort, query, requireSelection }) {
1838
1839
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginLeft: 2, children: [
1839
1840
  /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
1840
- /* @__PURE__ */ jsxs3(Text3, { children: [
1841
+ /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", children: [
1841
1842
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "provider " }),
1842
1843
  /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: provider }),
1843
1844
  /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
@@ -1855,7 +1856,7 @@ function ModelsView({ models, cursor, model, host, provider, effort, query, requ
1855
1856
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "select model" }),
1856
1857
  /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: models.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: query ? `no models match "${query}"` : provider === "lmstudio" ? "no models. load a model in LM Studio and start the server." : "no models found." }) : models.map((m, i) => {
1857
1858
  const sel = i === cursor;
1858
- return /* @__PURE__ */ jsxs3(Text3, { color: sel ? "blue" : void 0, dimColor: !sel, children: [
1859
+ return /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", color: sel ? "blue" : void 0, dimColor: !sel, children: [
1859
1860
  sel ? "\u276F " : " ",
1860
1861
  m,
1861
1862
  m === model ? /* @__PURE__ */ jsx3(Text3, { color: "green", children: " \u25CF" }) : null
@@ -1901,6 +1902,11 @@ function ProviderPicker({ entries, cursor, activeName, query }) {
1901
1902
  // src/ui/SessionsView.tsx
1902
1903
  import { Box as Box5, Text as Text5 } from "ink";
1903
1904
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1905
+ function truncate(s, max) {
1906
+ if (max <= 0) return "";
1907
+ if (s.length <= max) return s;
1908
+ return s.slice(0, Math.max(0, max - 1)) + "\u2026";
1909
+ }
1904
1910
  function relativeTime(iso) {
1905
1911
  const diff = Date.now() - new Date(iso).getTime();
1906
1912
  const min = Math.floor(diff / 6e4);
@@ -1916,13 +1922,16 @@ function SessionsView({ sessions, cursor }) {
1916
1922
  /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "resume session" }),
1917
1923
  /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: sessions.length === 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "no saved sessions yet" }) : sessions.map((s, i) => {
1918
1924
  const active2 = i === cursor;
1919
- const label = s.title;
1925
+ const meta = `\xB7 ${s.messageCount} msgs \xB7 ${relativeTime(s.updatedAt)}`;
1926
+ const cols = process.stdout.columns ?? 80;
1927
+ const titleMax = cols - 9 - meta.length;
1928
+ const label = truncate(s.title, titleMax);
1920
1929
  return /* @__PURE__ */ jsxs5(Box5, { gap: 1, children: [
1921
- /* @__PURE__ */ jsxs5(Text5, { color: active2 ? "blue" : void 0, dimColor: !active2, children: [
1930
+ /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", color: active2 ? "blue" : void 0, dimColor: !active2, children: [
1922
1931
  active2 ? "\u276F " : " ",
1923
1932
  label
1924
1933
  ] }),
1925
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `\xB7 ${s.messageCount} msgs \xB7 ${relativeTime(s.updatedAt)}` })
1934
+ /* @__PURE__ */ jsx5(Text5, { wrap: "truncate", dimColor: true, children: meta })
1926
1935
  ] }, s.id);
1927
1936
  }) }),
1928
1937
  /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 navigate enter resume d delete esc cancel" }) })
@@ -2198,8 +2207,9 @@ function FilePicker({ matches: matches2, cursor }) {
2198
2207
  }
2199
2208
 
2200
2209
  // src/ui/ChatView.tsx
2201
- import { useState as useState3, useEffect as useEffect3 } from "react";
2210
+ import { memo as memo2, useState as useState3, useEffect as useEffect3 } from "react";
2202
2211
  import { Box as Box9, Text as Text9 } from "ink";
2212
+ import { highlight } from "cli-highlight";
2203
2213
 
2204
2214
  // src/ui/ThinkingBlock.tsx
2205
2215
  import { useState as useState2, useEffect as useEffect2 } from "react";
@@ -2292,6 +2302,55 @@ function countLines(s) {
2292
2302
  if (!s) return 0;
2293
2303
  return s.split("\n").length;
2294
2304
  }
2305
+ var EXT_LANG = {
2306
+ ts: "typescript",
2307
+ tsx: "typescript",
2308
+ mts: "typescript",
2309
+ cts: "typescript",
2310
+ js: "javascript",
2311
+ jsx: "javascript",
2312
+ mjs: "javascript",
2313
+ cjs: "javascript",
2314
+ json: "json",
2315
+ py: "python",
2316
+ rb: "ruby",
2317
+ go: "go",
2318
+ rs: "rust",
2319
+ java: "java",
2320
+ c: "c",
2321
+ h: "c",
2322
+ cpp: "cpp",
2323
+ cc: "cpp",
2324
+ hpp: "cpp",
2325
+ cs: "csharp",
2326
+ php: "php",
2327
+ swift: "swift",
2328
+ kt: "kotlin",
2329
+ scala: "scala",
2330
+ sh: "bash",
2331
+ bash: "bash",
2332
+ zsh: "bash",
2333
+ yml: "yaml",
2334
+ yaml: "yaml",
2335
+ html: "xml",
2336
+ xml: "xml",
2337
+ css: "css",
2338
+ scss: "scss",
2339
+ sql: "sql",
2340
+ md: "markdown"
2341
+ };
2342
+ function langFromPath(path) {
2343
+ const ext = path.split(".").pop()?.toLowerCase();
2344
+ return ext ? EXT_LANG[ext] : void 0;
2345
+ }
2346
+ function highlightLine(text, lang) {
2347
+ if (!lang) return text;
2348
+ try {
2349
+ return highlight(text, { language: lang, ignoreIllegals: true });
2350
+ } catch {
2351
+ return text;
2352
+ }
2353
+ }
2295
2354
  function FileEditBlock({
2296
2355
  label,
2297
2356
  path,
@@ -2302,6 +2361,7 @@ function FileEditBlock({
2302
2361
  const expanded = useToolExpanded();
2303
2362
  const shown = expanded ? previewLines : previewLines.slice(0, COLLAPSED_LINES);
2304
2363
  const extra = previewLines.length - shown.length;
2364
+ const lang = langFromPath(path);
2305
2365
  return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
2306
2366
  /* @__PURE__ */ jsxs9(Box9, { children: [
2307
2367
  /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25CF " }),
@@ -2319,15 +2379,19 @@ function FileEditBlock({
2319
2379
  ] }) }),
2320
2380
  shown.map((ln, i) => {
2321
2381
  const width = (process.stdout.columns ?? 80) - 6 - 20;
2322
- const raw = `${ln.sign} ${ln.text}`;
2323
- const content = raw.length > width ? raw.slice(0, width) : raw.padEnd(width);
2324
- return /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(
2382
+ const textWidth = Math.max(0, width - 2);
2383
+ const plain = ln.text.length > textWidth ? ln.text.slice(0, textWidth) : ln.text.padEnd(textWidth);
2384
+ const code = ln.sign === " " ? plain : highlightLine(plain, lang);
2385
+ return /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(
2325
2386
  Text9,
2326
2387
  {
2327
2388
  wrap: "truncate",
2328
2389
  backgroundColor: ln.sign === "+" ? "#13351f" : ln.sign === "-" ? "#3b1414" : void 0,
2329
2390
  dimColor: ln.sign === " ",
2330
- children: content
2391
+ children: [
2392
+ `${ln.sign} `,
2393
+ code
2394
+ ]
2331
2395
  }
2332
2396
  ) }, i);
2333
2397
  }),
@@ -2346,7 +2410,7 @@ var TOOL_LABEL = {
2346
2410
  glob: "Glob",
2347
2411
  grep: "Grep"
2348
2412
  };
2349
- function truncate(s, max) {
2413
+ function truncate2(s, max) {
2350
2414
  if (s.length <= max) return s;
2351
2415
  return s.slice(0, max - 1) + "\u2026";
2352
2416
  }
@@ -2362,15 +2426,15 @@ function toolHeader(use) {
2362
2426
  break;
2363
2427
  case "run_bash": {
2364
2428
  const cmd2 = String(input.command ?? "").replace(/\s+/g, " ");
2365
- arg = truncate(cmd2, 120);
2429
+ arg = truncate2(cmd2, 120);
2366
2430
  break;
2367
2431
  }
2368
2432
  case "glob":
2369
2433
  case "grep":
2370
- arg = truncate(String(input.pattern ?? ""), 120);
2434
+ arg = truncate2(String(input.pattern ?? ""), 120);
2371
2435
  break;
2372
2436
  default: {
2373
- arg = truncate(JSON.stringify(input), 80);
2437
+ arg = truncate2(JSON.stringify(input), 80);
2374
2438
  }
2375
2439
  }
2376
2440
  return { label, arg };
@@ -2412,7 +2476,7 @@ function ToolResultBlock({ result, toolName }) {
2412
2476
  }
2413
2477
  const MAX_LINE_WIDTH = 200;
2414
2478
  const visible = expanded ? lines : lines.slice(0, COLLAPSED_LINES);
2415
- const shown = visible.map((l) => truncate(l, MAX_LINE_WIDTH));
2479
+ const shown = visible.map((l) => truncate2(l, MAX_LINE_WIDTH));
2416
2480
  const extra = lines.length - shown.length;
2417
2481
  return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
2418
2482
  /* @__PURE__ */ jsxs9(Text9, { color: result.is_error ? "red" : void 0, dimColor: !result.is_error, children: [
@@ -2462,7 +2526,13 @@ function ToolUseLine({ use, result }) {
2462
2526
  result && /* @__PURE__ */ jsx9(ToolResultBlock, { result, toolName: use.name })
2463
2527
  ] });
2464
2528
  }
2465
- function AssistantMessage({ msg }) {
2529
+ var UserMessage = memo2(function UserMessage2({ msg }) {
2530
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
2531
+ /* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u25CF " }),
2532
+ /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
2533
+ ] });
2534
+ });
2535
+ var AssistantMessage = memo2(function AssistantMessage2({ msg }) {
2466
2536
  return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
2467
2537
  msg.content && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", children: [
2468
2538
  /* @__PURE__ */ jsx9(Text9, { color: "white", children: "\u25CF " }),
@@ -2477,14 +2547,16 @@ function AssistantMessage({ msg }) {
2477
2547
  msg.duration != null ? ` \xB7 ${formatDuration(msg.duration)}` : ""
2478
2548
  ] }) })
2479
2549
  ] });
2480
- }
2550
+ });
2481
2551
  function summarizeInput(input) {
2482
2552
  if (!input || typeof input !== "object") return "";
2483
2553
  const obj = input;
2484
2554
  const priority = ["path", "file_path", "command", "pattern", "query"];
2485
2555
  for (const k of priority) {
2486
2556
  const v = obj[k];
2487
- if (typeof v === "string" && v.length > 0) return `${k}: ${v}`;
2557
+ if (typeof v === "string" && v.length > 0) {
2558
+ return `${k}: ${v.length > 120 ? v.slice(0, 120) + "\u2026" : v}`;
2559
+ }
2488
2560
  }
2489
2561
  const first = Object.entries(obj).find(([, v]) => typeof v === "string");
2490
2562
  if (first) {
@@ -2509,7 +2581,7 @@ function PermissionPrompt({ req, cursor }) {
2509
2581
  /* @__PURE__ */ jsx9(Text9, { bold: true, children: label }),
2510
2582
  "?"
2511
2583
  ] }) }),
2512
- summary && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: summary }) }),
2584
+ summary && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { wrap: "truncate", dimColor: true, children: summary }) }),
2513
2585
  /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsxs9(Text9, { color: i === cursor ? "blue" : void 0, children: [
2514
2586
  i === cursor ? "\u276F " : " ",
2515
2587
  i + 1,
@@ -2540,10 +2612,7 @@ function ChatView({
2540
2612
  ] }, i))
2541
2613
  ] }),
2542
2614
  messages.map(
2543
- (msg, i) => msg.role === "user" ? /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
2544
- /* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u25CF " }),
2545
- /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
2546
- ] }, i) : /* @__PURE__ */ jsx9(AssistantMessage, { msg }, i)
2615
+ (msg, i) => msg.role === "user" ? /* @__PURE__ */ jsx9(UserMessage, { msg }, i) : /* @__PURE__ */ jsx9(AssistantMessage, { msg }, i)
2547
2616
  ),
2548
2617
  thinking && /* @__PURE__ */ jsx9(ThinkingBlock, { content: thinkingContent }),
2549
2618
  streaming && streamingContent && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
@@ -3262,8 +3331,7 @@ function App() {
3262
3331
  return Math.round(used / activeCtx * 100);
3263
3332
  })();
3264
3333
  return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, children: [
3265
- /* @__PURE__ */ jsx10(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error }),
3266
- updateAvailable && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: `\u2191 update available: v${updateAvailable} \u2014 run: miii --update` }) }),
3334
+ /* @__PURE__ */ jsx10(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable }),
3267
3335
  state === "loading" && !agent.error && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: `connecting to ${provName}\u2026` }) }),
3268
3336
  agent.error && state !== "ready" && /* @__PURE__ */ jsx10(
3269
3337
  ChatView,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-agent",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Terminal AI coding agent powered by Ollama",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,6 +36,7 @@
36
36
  ],
37
37
  "license": "MIT",
38
38
  "dependencies": {
39
+ "cli-highlight": "^2.1.11",
39
40
  "execa": "^9.0.0",
40
41
  "ink": "^5.0.0",
41
42
  "ollama": "^0.5.0",