min-agent 0.2.0 → 0.3.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 (81) hide show
  1. package/README.md +146 -18
  2. package/dist/agent.js +293 -408
  3. package/dist/assistant-stream.js +11 -7
  4. package/dist/cli.js +403 -140
  5. package/dist/clipboard.js +59 -23
  6. package/dist/code-mode.js +3 -3
  7. package/dist/compaction.js +182 -81
  8. package/dist/config.js +186 -35
  9. package/dist/confirm.js +55 -6
  10. package/dist/context-window.js +67 -54
  11. package/dist/doom-loop.js +19 -12
  12. package/dist/http.js +119 -0
  13. package/dist/instructions.js +51 -33
  14. package/dist/logger.js +66 -0
  15. package/dist/markdown.js +3 -44
  16. package/dist/mcp.js +547 -100
  17. package/dist/memory.js +48 -6
  18. package/dist/output.js +36 -27
  19. package/dist/paste-handler.js +3 -3
  20. package/dist/plugins.js +33 -6
  21. package/dist/pricing.js +119 -0
  22. package/dist/provider.js +17 -15
  23. package/dist/serve.js +658 -369
  24. package/dist/sessions.js +151 -13
  25. package/dist/skills.js +466 -76
  26. package/dist/synthetic.js +7 -0
  27. package/dist/title-gen.js +2 -1
  28. package/dist/tool-display.js +173 -0
  29. package/dist/tool-output.js +54 -45
  30. package/dist/tools/apply_patch.js +191 -0
  31. package/dist/tools/backend.js +61 -0
  32. package/dist/tools/bash.js +147 -70
  33. package/dist/tools/code_search.js +6 -5
  34. package/dist/tools/edit.js +23 -7
  35. package/dist/tools/explore.js +80 -12
  36. package/dist/tools/glob.js +3 -3
  37. package/dist/tools/grep.js +146 -14
  38. package/dist/tools/index.js +7 -7
  39. package/dist/tools/question.js +4 -22
  40. package/dist/tools/read.js +71 -11
  41. package/dist/tools/task.js +33 -20
  42. package/dist/tools/todo.js +83 -73
  43. package/dist/tools/web_fetch.js +150 -46
  44. package/dist/tools/web_search.js +706 -28
  45. package/dist/tools/write.js +13 -7
  46. package/dist/tui/App.js +40 -6
  47. package/dist/tui/ConfirmBar.js +24 -3
  48. package/dist/tui/InputBar.js +390 -45
  49. package/dist/tui/MessageList.js +533 -20
  50. package/dist/tui/ModelPicker.js +108 -0
  51. package/dist/tui/QuestionBar.js +104 -0
  52. package/dist/tui/StatusBar.js +19 -11
  53. package/dist/tui/agent-runner.js +103 -0
  54. package/dist/tui/caret-pos.js +134 -0
  55. package/dist/tui/caret.js +69 -0
  56. package/dist/tui/diff-view.js +61 -0
  57. package/dist/tui/drag-state.js +44 -0
  58. package/dist/tui/index.js +153 -24
  59. package/dist/tui/input-history.js +44 -0
  60. package/dist/tui/layout.js +17 -0
  61. package/dist/tui/mouse.js +46 -0
  62. package/dist/tui/selection.js +134 -0
  63. package/dist/tui/slash-commands.js +90 -0
  64. package/dist/tui/slash-handler.js +370 -0
  65. package/dist/tui/text-width.js +91 -0
  66. package/dist/tui/theme.js +12 -0
  67. package/dist/tui/undo-stack.js +14 -0
  68. package/dist/tui/use-sgr-mouse.js +27 -0
  69. package/dist/tui-chat.js +111 -331
  70. package/dist/updater.js +57 -0
  71. package/docs/API.md +160 -14
  72. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
  73. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
  74. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
  75. package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
  76. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
  77. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
  78. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
  79. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
  80. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
  81. package/package.json +7 -8
package/dist/memory.js CHANGED
@@ -1,26 +1,58 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync } from "fs";
2
2
  import path from "path";
3
+ import { randomUUID } from "crypto";
3
4
  import { getConfigDir } from "./config.js";
4
5
  import { tool, jsonSchema } from "ai";
5
6
  function getMemoryFile() {
6
7
  return path.join(getConfigDir(), "memory.json");
7
8
  }
9
+ /** Preserve corrupt data instead of letting it be overwritten and lost forever. */
10
+ function backupCorrupt(file) {
11
+ try {
12
+ renameSync(file, `${file}.corrupt-${Date.now()}`);
13
+ }
14
+ catch {
15
+ // nothing else we can do with an unreadable file
16
+ }
17
+ }
8
18
  export function loadMemories() {
9
19
  const file = getMemoryFile();
10
20
  if (!existsSync(file))
11
21
  return [];
22
+ let data;
12
23
  try {
13
- return JSON.parse(readFileSync(file, "utf-8"));
24
+ data = JSON.parse(readFileSync(file, "utf-8"));
14
25
  }
15
26
  catch {
27
+ backupCorrupt(file);
16
28
  return [];
17
29
  }
30
+ if (!Array.isArray(data)) {
31
+ backupCorrupt(file);
32
+ return [];
33
+ }
34
+ return data.filter((m) => typeof m === "object" && m !== null && typeof m.content === "string");
18
35
  }
36
+ /** Atomic replace via temp file + rename, so a crash never leaves a truncated memory.json. */
19
37
  function saveMemories(memories) {
20
38
  const file = getMemoryFile();
21
39
  mkdirSync(path.dirname(file), { recursive: true });
22
- writeFileSync(file, JSON.stringify(memories, null, 2), "utf-8");
40
+ const tmp = `${file}.tmp-${randomUUID()}`;
41
+ try {
42
+ writeFileSync(tmp, JSON.stringify(memories, null, 2), "utf-8");
43
+ renameSync(tmp, file);
44
+ }
45
+ catch (err) {
46
+ try {
47
+ if (existsSync(tmp))
48
+ unlinkSync(tmp);
49
+ }
50
+ catch { }
51
+ throw err;
52
+ }
23
53
  }
54
+ // read-modify-write runs fully synchronously, so concurrent tool calls in
55
+ // this process cannot interleave between load and save.
24
56
  export function addMemory(content, tags = []) {
25
57
  const memories = loadMemories();
26
58
  const memory = {
@@ -48,21 +80,31 @@ export function searchMemories(query) {
48
80
  .filter((m) => m.content.toLowerCase().includes(lower) ||
49
81
  m.tags.some((t) => t.toLowerCase().includes(lower)));
50
82
  }
51
- /** Build a system prompt section from stored memories */
83
+ /** Build a system prompt section from stored memories (newest first, capped). */
52
84
  export function getMemorySystemPrompt() {
53
85
  const memories = loadMemories();
54
86
  if (memories.length === 0)
55
87
  return "";
56
- const items = memories.map((m, i) => {
88
+ const MAX_MEMORIES = 30;
89
+ const start = Math.max(0, memories.length - MAX_MEMORIES);
90
+ const items = memories
91
+ .slice(start)
92
+ .map((m, i) => ({ m, num: start + i + 1 }))
93
+ .reverse()
94
+ .map(({ m, num }) => {
57
95
  const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
58
- return ` ${i + 1}. ${m.content}${tags}`;
96
+ return ` ${num}. ${m.content}${tags}`;
59
97
  });
98
+ const capped = memories.length > MAX_MEMORIES
99
+ ? `\n(${memories.length - MAX_MEMORIES} older memories not shown — use memory_search to find them.)`
100
+ : "";
60
101
  return [
61
102
  "## Memories",
62
103
  "The following are things you have remembered from previous conversations. Use them to provide better, personalized responses.",
63
104
  "You can save new memories with the memory_save tool when the user tells you something worth remembering (preferences, project details, conventions, etc).",
64
105
  "",
65
106
  ...items,
107
+ capped,
66
108
  ].join("\n");
67
109
  }
68
110
  /** Create the memory tools for the agent */
package/dist/output.js CHANGED
@@ -1,4 +1,5 @@
1
- import { loadConfig } from "./config.js";
1
+ import { loadConfig, getActiveProvider } from "./config.js";
2
+ import { summarizeToolCall, toolResultText, truncateDisplay } from "./tool-display.js";
2
3
  const COLORS = {
3
4
  reset: "\x1b[0m",
4
5
  dim: "\x1b[2m",
@@ -11,30 +12,50 @@ const COLORS = {
11
12
  gray: "\x1b[90m",
12
13
  };
13
14
  export function printHeader(modelId) {
14
- const config = loadConfig();
15
- const model = modelId ?? config.provider?.defaultModel ?? "unknown";
15
+ const model = modelId ?? getActiveProvider(loadConfig())?.defaultModel ?? "unknown";
16
16
  console.log(`${COLORS.bold}🤖 min-agent${COLORS.reset} ${COLORS.dim}(${model})${COLORS.reset}`);
17
17
  }
18
18
  export function printDivider() {
19
19
  console.log(`${COLORS.dim}${"─".repeat(60)}${COLORS.reset}`);
20
20
  }
21
21
  export function printToolCall(name, input) {
22
- const argsStr = formatArgs(input);
22
+ const argsStr = summarizeToolCall(name, input, 160);
23
23
  console.log(`\n${COLORS.yellow}⚡ ${name}${COLORS.reset} ${COLORS.dim}${argsStr}${COLORS.reset}`);
24
24
  }
25
- export function printToolResult(name, result) {
26
- const output = typeof result === "string" ? result : JSON.stringify(result, null, 2);
27
- const lines = output.split("\n");
28
- const maxLines = 20;
29
- const truncated = lines.length > maxLines;
30
- const preview = truncated ? lines.slice(0, maxLines).join("\n") : output;
31
- const display = preview.length > 500 ? preview.slice(0, 500) + "..." : preview;
32
- const suffix = truncated ? ` (${lines.length - maxLines} more lines)` : "";
33
- console.log(`${COLORS.green} ✓${COLORS.reset} ${COLORS.dim}${display}${suffix}${COLORS.reset}\n`);
25
+ /** Console preview limits for a tool result (the full text goes to the model). */
26
+ const RESULT_PREVIEW_MAX_LINES = 20;
27
+ const RESULT_PREVIEW_MAX_CHARS = 1200;
28
+ /**
29
+ * Print a tool result preview. Lines are kept whole and both limits are
30
+ * applied in one pass, so the "N more lines" count always matches what was
31
+ * actually withheld.
32
+ */
33
+ export function printToolResult(name, result, isError = false) {
34
+ const text = toolResultText(result).replace(/\s+$/, "");
35
+ const lines = text === "" ? [] : text.split("\n");
36
+ const kept = [];
37
+ let chars = 0;
38
+ for (const line of lines) {
39
+ if (kept.length >= RESULT_PREVIEW_MAX_LINES)
40
+ break;
41
+ if (chars + line.length > RESULT_PREVIEW_MAX_CHARS)
42
+ break;
43
+ kept.push(line);
44
+ chars += line.length + 1;
45
+ }
46
+ // A single very long first line still deserves a preview.
47
+ if (kept.length === 0 && lines.length > 0) {
48
+ kept.push(truncateDisplay(lines[0], RESULT_PREVIEW_MAX_CHARS));
49
+ }
50
+ const hidden = lines.length - kept.length;
51
+ const suffix = hidden > 0 ? ` (${hidden} more line${hidden === 1 ? "" : "s"})` : "";
52
+ const marker = isError ? `${COLORS.red} ✗${COLORS.reset}` : `${COLORS.green} ✓${COLORS.reset}`;
53
+ const body = kept.join(`\n${COLORS.dim} `);
54
+ console.log(`${marker} ${COLORS.dim}${body}${suffix}${COLORS.reset}\n`);
34
55
  }
35
56
  export function printDone(steps, usage, contextWindow) {
36
- const input = usage.inputTokens ?? 0;
37
- const output = usage.outputTokens ?? 0;
57
+ const input = usage?.inputTokens ?? 0;
58
+ const output = usage?.outputTokens ?? 0;
38
59
  const total = input + output;
39
60
  let contextInfo = "";
40
61
  if (input > 0 && contextWindow && contextWindow > 0) {
@@ -51,15 +72,3 @@ function renderBar(pct) {
51
72
  const color = pct >= 80 ? "\x1b[31m" : pct >= 50 ? "\x1b[33m" : "\x1b[32m";
52
73
  return `${color}${"█".repeat(filled)}${"░".repeat(empty)}\x1b[0m\x1b[90m`;
53
74
  }
54
- function formatArgs(args) {
55
- if (!args || typeof args !== "object")
56
- return "";
57
- const entries = Object.entries(args);
58
- if (entries.length === 0)
59
- return "";
60
- const parts = entries.map(([k, v]) => {
61
- const val = typeof v === "string" ? (v.length > 60 ? v.slice(0, 60) + "..." : v) : JSON.stringify(v);
62
- return `${k}=${val}`;
63
- });
64
- return parts.join(" ");
65
- }
@@ -17,7 +17,7 @@ const PREVIEW_LINES = 3;
17
17
  export function processPastedInput(text) {
18
18
  const lineCount = (text.match(/\n/g)?.length ?? 0) + 1;
19
19
  if (lineCount < PASTE_LINE_THRESHOLD && text.length <= PASTE_CHAR_THRESHOLD) {
20
- return { fullText: text, isLargePaste: false };
20
+ return { fullText: text, isLargePaste: false, lineCount };
21
21
  }
22
22
  const lines = text.split("\n");
23
23
  const preview = lines.slice(0, PREVIEW_LINES).join("\n");
@@ -25,6 +25,7 @@ export function processPastedInput(text) {
25
25
  return {
26
26
  fullText: text,
27
27
  isLargePaste: true,
28
+ lineCount,
28
29
  summary: remaining > 0
29
30
  ? `${preview}\n\x1b[90m ... (${remaining} more lines, ~${text.length} chars total)\x1b[0m`
30
31
  : `\x1b[90m[Pasted ${text.length} chars]\x1b[0m`,
@@ -36,6 +37,5 @@ export function processPastedInput(text) {
36
37
  export function printPasteFeedback(result) {
37
38
  if (!result.isLargePaste)
38
39
  return;
39
- const lineCount = (result.fullText.match(/\n/g)?.length ?? 0) + 1;
40
- console.log(`\x1b[90m 📋 Pasted ~${lineCount} lines (${result.fullText.length} chars)\x1b[0m`);
40
+ console.log(`\x1b[90m 📋 Pasted ~${result.lineCount} lines (${result.fullText.length} chars)\x1b[0m`);
41
41
  }
package/dist/plugins.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { tool, jsonSchema } from "ai";
2
- import { existsSync, readdirSync } from "fs";
2
+ import { existsSync, readdirSync, statSync } from "fs";
3
3
  import { pathToFileURL } from "url";
4
4
  import path from "path";
5
5
  import { getConfigDir } from "./config.js";
@@ -7,9 +7,32 @@ const PLUGIN_DIRS = [
7
7
  path.join(process.cwd(), ".min-agent", "tools"),
8
8
  path.join(getConfigDir(), "tools"),
9
9
  ];
10
- export async function loadPluginTools() {
10
+ let cachedPlugins = null;
11
+ function pluginSignature(dirs) {
12
+ return dirs
13
+ .map((dir) => {
14
+ if (!existsSync(dir))
15
+ return `${dir}:missing`;
16
+ const files = readdirSync(dir)
17
+ .filter((f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".mjs"))
18
+ .sort();
19
+ const mtimes = files.map((f) => statSync(path.join(dir, f)).mtimeMs).join(",");
20
+ return `${dir}:${files.join(",")}:${mtimes}`;
21
+ })
22
+ .join("|");
23
+ }
24
+ export async function loadPluginTools(dirs) {
25
+ const pluginDirs = dirs ?? PLUGIN_DIRS;
26
+ const signature = pluginSignature(pluginDirs);
27
+ if (cachedPlugins && cachedPlugins.signature === signature)
28
+ return cachedPlugins.tools;
29
+ const tools = await loadPluginToolsUncached(pluginDirs);
30
+ cachedPlugins = { signature, tools };
31
+ return tools;
32
+ }
33
+ async function loadPluginToolsUncached(pluginDirs) {
11
34
  const tools = {};
12
- for (const dir of PLUGIN_DIRS) {
35
+ for (const dir of pluginDirs) {
13
36
  if (!existsSync(dir))
14
37
  continue;
15
38
  const files = readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".mjs"));
@@ -22,6 +45,10 @@ export async function loadPluginTools() {
22
45
  if (!isPluginTool(def))
23
46
  continue;
24
47
  const toolId = exportName === "default" ? namespace : `${namespace}_${exportName}`;
48
+ if (tools[toolId]) {
49
+ console.warn(`\x1b[33m ⚠ Plugin tool "${toolId}" from ${filePath} conflicts with an existing tool; skipping\x1b[0m`);
50
+ continue;
51
+ }
25
52
  const properties = {};
26
53
  const required = [];
27
54
  for (const [key, param] of Object.entries(def.parameters)) {
@@ -38,17 +65,17 @@ export async function loadPluginTools() {
38
65
  execute: async (args) => {
39
66
  try {
40
67
  const result = await def.execute(args);
41
- return typeof result === "string" ? result : JSON.stringify(result);
68
+ return typeof result === "string" ? result : JSON.stringify(result) ?? "undefined";
42
69
  }
43
70
  catch (err) {
44
- return `Plugin error: ${err.message}`;
71
+ return `Plugin error: ${String(err instanceof Error ? err.message : err)}`;
45
72
  }
46
73
  },
47
74
  });
48
75
  }
49
76
  }
50
77
  catch (err) {
51
- console.error(`\x1b[90m Plugin "${file}" failed to load: ${err.message}\x1b[0m`);
78
+ console.error(`\x1b[90m Plugin "${file}" failed to load: ${String(err instanceof Error ? err.message : err)}\x1b[0m`);
52
79
  }
53
80
  }
54
81
  }
@@ -0,0 +1,119 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import path from "path";
3
+ import { getConfigDir, loadConfig } from "./config.js";
4
+ const CACHE_TTL = 7 * 24 * 60 * 60 * 1000;
5
+ const memoryCache = new Map();
6
+ const inFlight = new Map();
7
+ const NEGATIVE_TTL = 60 * 60 * 1000;
8
+ const negativeCache = new Map();
9
+ function cacheFile() {
10
+ return path.join(getConfigDir(), "pricing-cache.json");
11
+ }
12
+ function loadDiskCache() {
13
+ const file = cacheFile();
14
+ if (!existsSync(file))
15
+ return {};
16
+ try {
17
+ return JSON.parse(readFileSync(file, "utf-8"));
18
+ }
19
+ catch {
20
+ return {};
21
+ }
22
+ }
23
+ function saveCache(cache) {
24
+ const file = cacheFile();
25
+ mkdirSync(path.dirname(file), { recursive: true });
26
+ writeFileSync(file, JSON.stringify(cache), "utf-8");
27
+ }
28
+ function getCached(modelId) {
29
+ const memory = memoryCache.get(modelId);
30
+ if (memory && Date.now() - memory.timestamp <= CACHE_TTL)
31
+ return memory;
32
+ const disk = loadDiskCache()[modelId];
33
+ if (disk && Date.now() - disk.timestamp <= CACHE_TTL) {
34
+ memoryCache.set(modelId, disk);
35
+ return disk;
36
+ }
37
+ return null;
38
+ }
39
+ function setCache(modelId, price) {
40
+ const entry = { price, timestamp: Date.now() };
41
+ memoryCache.set(modelId, entry);
42
+ const cache = loadDiskCache();
43
+ cache[modelId] = entry;
44
+ saveCache(cache);
45
+ }
46
+ function priceFromConfig(modelId) {
47
+ const override = loadConfig().pricing?.[modelId];
48
+ if (!override)
49
+ return null;
50
+ if (typeof override.inputPerMillion !== "number" || typeof override.outputPerMillion !== "number")
51
+ return null;
52
+ return { inputPerMillion: override.inputPerMillion, outputPerMillion: override.outputPerMillion };
53
+ }
54
+ async function fetchFromModelsDev(modelId) {
55
+ try {
56
+ const response = await fetch("https://models.dev/api.json", { signal: AbortSignal.timeout(10000) });
57
+ if (!response.ok)
58
+ return null;
59
+ const providers = (await response.json());
60
+ for (const provider of Object.values(providers)) {
61
+ if (!provider.models)
62
+ continue;
63
+ for (const [id, model] of Object.entries(provider.models)) {
64
+ if (id === modelId || id.endsWith(`/${modelId}`) || modelId.endsWith(`/${id}`)) {
65
+ const p = model?.pricing;
66
+ if (p && typeof p.input === "number" && typeof p.output === "number") {
67
+ return { inputPerMillion: p.input, outputPerMillion: p.output };
68
+ }
69
+ }
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ export async function getModelPrice(modelId) {
79
+ const override = priceFromConfig(modelId);
80
+ if (override)
81
+ return override;
82
+ const cached = getCached(modelId);
83
+ if (cached)
84
+ return cached.price;
85
+ const negAt = negativeCache.get(modelId);
86
+ if (negAt !== undefined && Date.now() - negAt <= NEGATIVE_TTL)
87
+ return null;
88
+ const pending = inFlight.get(modelId);
89
+ if (pending)
90
+ return pending;
91
+ const probing = fetchFromModelsDev(modelId).then((price) => {
92
+ if (price) {
93
+ setCache(modelId, price);
94
+ }
95
+ else {
96
+ negativeCache.set(modelId, Date.now());
97
+ }
98
+ return price;
99
+ });
100
+ inFlight.set(modelId, probing);
101
+ try {
102
+ return await probing;
103
+ }
104
+ finally {
105
+ inFlight.delete(modelId);
106
+ }
107
+ }
108
+ export function estimateCost(usage, price) {
109
+ if (!price)
110
+ return null;
111
+ const cost = (usage.inputTokens / 1_000_000) * price.inputPerMillion +
112
+ (usage.outputTokens / 1_000_000) * price.outputPerMillion;
113
+ return cost > 0 ? cost : null;
114
+ }
115
+ export function formatCost(cost) {
116
+ if (cost === null)
117
+ return "--";
118
+ return cost >= 0.1 ? `$${cost.toFixed(3)}` : `$${cost.toFixed(4)}`;
119
+ }
package/dist/provider.js CHANGED
@@ -1,41 +1,43 @@
1
1
  import { createOpenAI } from "@ai-sdk/openai";
2
- import { loadConfig } from "./config.js";
3
- function normalizeOllamaBaseURL(baseURL) {
4
- const trimmed = baseURL.replace(/\/$/, "");
5
- return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
6
- }
7
- export function resolveModel(modelId) {
8
- const config = loadConfig();
9
- const provider = config.provider;
10
- if (!provider?.baseURL || !provider?.apiKey) {
2
+ import { loadConfig, getActiveProvider, normalizeOllamaBaseURL } from "./config.js";
3
+ export function resolveModelForProvider(provider, modelId) {
4
+ if (!provider.baseURL || !provider.apiKey) {
11
5
  throw new Error("Not configured. Run: min-agent setup");
12
6
  }
13
7
  const id = modelId ?? provider.defaultModel;
14
8
  if (!id) {
15
9
  throw new Error("No model specified. Run: min-agent setup");
16
10
  }
17
- const type = provider.type ?? "openai-compatible";
18
- switch (type) {
11
+ switch (provider.type ?? "openai-compatible") {
19
12
  case "openai": {
20
13
  const client = createOpenAI({ apiKey: provider.apiKey });
21
14
  return client.chat(id);
22
15
  }
23
16
  case "ollama": {
24
- // Ollama exposes an OpenAI-compatible API at /v1.
25
- // Accept both "...:11434" and "...:11434/v1" in user config.
26
17
  const client = createOpenAI({
27
18
  baseURL: normalizeOllamaBaseURL(provider.baseURL),
28
19
  apiKey: provider.apiKey || "ollama",
29
20
  });
30
21
  return client.chat(id);
31
22
  }
32
- case "openai-compatible":
33
- default: {
23
+ case "openai-compatible": {
34
24
  const client = createOpenAI({
35
25
  baseURL: provider.baseURL,
36
26
  apiKey: provider.apiKey,
37
27
  });
38
28
  return client.chat(id);
39
29
  }
30
+ default:
31
+ throw new Error(`Unknown provider type: ${provider.type}`);
32
+ }
33
+ }
34
+ export function resolveModel(modelId, providerName) {
35
+ const config = loadConfig();
36
+ const provider = providerName
37
+ ? config.providers?.find((p) => p.name === providerName)
38
+ : getActiveProvider(config);
39
+ if (!provider) {
40
+ throw new Error(providerName ? `Provider "${providerName}" not found` : "Not configured. Run: min-agent setup");
40
41
  }
42
+ return resolveModelForProvider(provider, modelId);
41
43
  }