min-agent 0.5.1 → 0.6.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 (169) hide show
  1. package/README.md +18 -18
  2. package/dist/cli.js +538 -12
  3. package/docs/API.md +6 -6
  4. package/package.json +6 -6
  5. package/skills/self-config/SKILL.md +4 -2
  6. package/skills/self-config/reference.md +4 -1
  7. package/dist/agent.js +0 -1388
  8. package/dist/assistant-stream.js +0 -131
  9. package/dist/cli/commands/chat.js +0 -13
  10. package/dist/cli/commands/ctx.js +0 -7
  11. package/dist/cli/commands/exec.js +0 -35
  12. package/dist/cli/commands/history.js +0 -58
  13. package/dist/cli/commands/index.js +0 -249
  14. package/dist/cli/commands/init.js +0 -18
  15. package/dist/cli/commands/mcp.js +0 -173
  16. package/dist/cli/commands/memory.js +0 -87
  17. package/dist/cli/commands/models.js +0 -21
  18. package/dist/cli/commands/permission.js +0 -12
  19. package/dist/cli/commands/rules.js +0 -33
  20. package/dist/cli/commands/sandbox.js +0 -13
  21. package/dist/cli/commands/serve.js +0 -9
  22. package/dist/cli/commands/setup.js +0 -56
  23. package/dist/cli/commands/shared.js +0 -25
  24. package/dist/cli/commands/skills.js +0 -119
  25. package/dist/cli/commands/think.js +0 -12
  26. package/dist/cli/commands/update.js +0 -7
  27. package/dist/cli/commands/write-config.js +0 -52
  28. package/dist/cli/errors.js +0 -36
  29. package/dist/cli/exec-prompt.js +0 -26
  30. package/dist/cli/option-helpers.js +0 -65
  31. package/dist/cli/program.js +0 -223
  32. package/dist/cli/setup/detect.js +0 -17
  33. package/dist/cli/setup/flags.js +0 -12
  34. package/dist/cli/setup/flow.js +0 -108
  35. package/dist/cli/setup/provider-form.js +0 -102
  36. package/dist/cli/setup/ui.js +0 -534
  37. package/dist/clipboard.js +0 -142
  38. package/dist/code-mode.js +0 -184
  39. package/dist/compaction.js +0 -644
  40. package/dist/config.js +0 -363
  41. package/dist/confirm.js +0 -172
  42. package/dist/context-window.js +0 -256
  43. package/dist/ctx-cli.js +0 -30
  44. package/dist/ctx.js +0 -80
  45. package/dist/doom-loop.js +0 -278
  46. package/dist/fetch-timeout.js +0 -152
  47. package/dist/http-approvals.js +0 -60
  48. package/dist/http.js +0 -119
  49. package/dist/instructions.js +0 -196
  50. package/dist/logger.js +0 -95
  51. package/dist/markdown.js +0 -259
  52. package/dist/mcp.js +0 -1036
  53. package/dist/memory-cli.js +0 -33
  54. package/dist/memory.js +0 -295
  55. package/dist/model-catalog.js +0 -285
  56. package/dist/ollama-model.js +0 -234
  57. package/dist/ollama-openai-bridge.js +0 -383
  58. package/dist/output.js +0 -76
  59. package/dist/paste-handler.js +0 -41
  60. package/dist/permission-cli.js +0 -40
  61. package/dist/plugins.js +0 -131
  62. package/dist/pricing.js +0 -119
  63. package/dist/provider.js +0 -63
  64. package/dist/question-format.js +0 -60
  65. package/dist/reasoning-stream.js +0 -158
  66. package/dist/sandbox-cli.js +0 -79
  67. package/dist/sandbox.js +0 -403
  68. package/dist/save-throttle.js +0 -45
  69. package/dist/scope.js +0 -23
  70. package/dist/serve/common.js +0 -425
  71. package/dist/serve/routes-chat.js +0 -367
  72. package/dist/serve/routes-mcp.js +0 -212
  73. package/dist/serve/routes-memory.js +0 -95
  74. package/dist/serve/routes-meta.js +0 -268
  75. package/dist/serve/routes-sessions.js +0 -61
  76. package/dist/serve/routes-skills.js +0 -70
  77. package/dist/serve.js +0 -138
  78. package/dist/sessions.js +0 -265
  79. package/dist/skills.js +0 -599
  80. package/dist/structured-output.js +0 -29
  81. package/dist/synthetic.js +0 -7
  82. package/dist/think-cli.js +0 -36
  83. package/dist/thinking-wire.js +0 -239
  84. package/dist/thinking.js +0 -166
  85. package/dist/title-gen.js +0 -55
  86. package/dist/token-display.js +0 -39
  87. package/dist/tool-display.js +0 -178
  88. package/dist/tool-output.js +0 -126
  89. package/dist/tools/apply_patch.js +0 -265
  90. package/dist/tools/atomic-file.js +0 -35
  91. package/dist/tools/backend.js +0 -61
  92. package/dist/tools/bash.js +0 -231
  93. package/dist/tools/code_search.js +0 -98
  94. package/dist/tools/edit.js +0 -68
  95. package/dist/tools/explore.js +0 -232
  96. package/dist/tools/glob.js +0 -40
  97. package/dist/tools/grep.js +0 -174
  98. package/dist/tools/index.js +0 -34
  99. package/dist/tools/question.js +0 -54
  100. package/dist/tools/read.js +0 -109
  101. package/dist/tools/search-searxng.js +0 -223
  102. package/dist/tools/search-serper.js +0 -189
  103. package/dist/tools/task.js +0 -165
  104. package/dist/tools/todo.js +0 -213
  105. package/dist/tools/web_fetch.js +0 -195
  106. package/dist/tools/web_search.js +0 -228
  107. package/dist/tools/write.js +0 -55
  108. package/dist/tui/App.js +0 -138
  109. package/dist/tui/ConfirmBar.js +0 -66
  110. package/dist/tui/CtxPicker.js +0 -68
  111. package/dist/tui/InputBar.js +0 -618
  112. package/dist/tui/MessageList.js +0 -718
  113. package/dist/tui/ModelPicker.js +0 -113
  114. package/dist/tui/QuestionBar.js +0 -136
  115. package/dist/tui/SessionPicker.js +0 -79
  116. package/dist/tui/Spinner.js +0 -6
  117. package/dist/tui/StatusBar.js +0 -22
  118. package/dist/tui/ThinkPicker.js +0 -75
  119. package/dist/tui/agent-runner.js +0 -223
  120. package/dist/tui/bracketed-paste.js +0 -37
  121. package/dist/tui/caret-pos.js +0 -179
  122. package/dist/tui/caret.js +0 -69
  123. package/dist/tui/click-count.js +0 -13
  124. package/dist/tui/diff-view.js +0 -61
  125. package/dist/tui/drag-state.js +0 -49
  126. package/dist/tui/hydrate.js +0 -129
  127. package/dist/tui/index.js +0 -264
  128. package/dist/tui/input-history.js +0 -125
  129. package/dist/tui/layout.js +0 -105
  130. package/dist/tui/mouse.js +0 -46
  131. package/dist/tui/overlay-input.js +0 -12
  132. package/dist/tui/paste-draft.js +0 -173
  133. package/dist/tui/prompt-queue.js +0 -24
  134. package/dist/tui/selection.js +0 -232
  135. package/dist/tui/session-switch.js +0 -28
  136. package/dist/tui/slash-commands.js +0 -129
  137. package/dist/tui/slash-handler.js +0 -615
  138. package/dist/tui/text-width.js +0 -113
  139. package/dist/tui/theme.js +0 -12
  140. package/dist/tui/token-info.js +0 -7
  141. package/dist/tui/tool-children.js +0 -19
  142. package/dist/tui/types.js +0 -1
  143. package/dist/tui/undo-stack.js +0 -14
  144. package/dist/tui/use-sgr-mouse.js +0 -29
  145. package/dist/tui-chat.js +0 -478
  146. package/dist/updater.js +0 -116
  147. package/dist/xml-search.js +0 -194
  148. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
  149. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
  150. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
  151. package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
  152. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
  153. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
  154. package/docs/superpowers/plans/2026-08-23-cli-setup.md +0 -501
  155. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
  156. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
  157. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
  158. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
  159. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
  160. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
  161. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
  162. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
  163. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
  164. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
  165. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
  166. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
  167. package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +0 -282
  168. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
  169. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +0 -140
@@ -1,41 +0,0 @@
1
- /**
2
- * Paste handler for large text input.
3
- *
4
- * When the user pastes large content (multi-line or >150 chars),
5
- * shows a collapsed summary in the terminal but preserves the full
6
- * text for sending to the model.
7
- *
8
- * Based on opencode's paste handling in the TUI prompt component.
9
- */
10
- const PASTE_LINE_THRESHOLD = 3;
11
- const PASTE_CHAR_THRESHOLD = 150;
12
- const PREVIEW_LINES = 3;
13
- /**
14
- * Process input text and detect if it's a large paste.
15
- * Returns the full text plus display metadata.
16
- */
17
- export function processPastedInput(text) {
18
- const lineCount = (text.match(/\n/g)?.length ?? 0) + 1;
19
- if (lineCount < PASTE_LINE_THRESHOLD && text.length <= PASTE_CHAR_THRESHOLD) {
20
- return { fullText: text, isLargePaste: false, lineCount };
21
- }
22
- const lines = text.split("\n");
23
- const preview = lines.slice(0, PREVIEW_LINES).join("\n");
24
- const remaining = lineCount - PREVIEW_LINES;
25
- return {
26
- fullText: text,
27
- isLargePaste: true,
28
- lineCount,
29
- summary: remaining > 0
30
- ? `${preview}\n\x1b[90m ... (${remaining} more lines, ~${text.length} chars total)\x1b[0m`
31
- : `\x1b[90m[Pasted ${text.length} chars]\x1b[0m`,
32
- };
33
- }
34
- /**
35
- * Print paste feedback to the user.
36
- */
37
- export function printPasteFeedback(result) {
38
- if (!result.isLargePaste)
39
- return;
40
- console.log(`\x1b[90m 📋 Pasted ~${result.lineCount} lines (${result.fullText.length} chars)\x1b[0m`);
41
- }
@@ -1,40 +0,0 @@
1
- import { takeScopeFlags, scopeLabel } from "./scope.js";
2
- import { getPermissionSnapshot, parsePermissionMode, permissionModeLabel, setPermissionMode, } from "./config.js";
3
- import { getPermissionOverride, setPermissionOverride } from "./confirm.js";
4
- export const PERMISSION_CLI_USAGE = [
5
- "Usage: min-agent permission [ask|accept-edits|allow-all] [--project|--global]",
6
- " min-agent --permission ask|accept-edits|allow-all [--project|--global]",
7
- ].join("\n");
8
- function sourceLabel(source) {
9
- return source === "cli" ? "this run" : source === "project" ? "project" : source === "global" ? "global" : "default";
10
- }
11
- export function runPermissionCli(input) {
12
- const { scope: posScope, rest } = takeScopeFlags(input.positionals);
13
- const scope = posScope ?? input.scope ?? "global";
14
- if (rest[0] === "--help" || rest[0] === "-h") {
15
- return { ok: true, lines: [PERMISSION_CLI_USAGE] };
16
- }
17
- if (rest.length === 1) {
18
- const parsed = parsePermissionMode(rest[0]);
19
- if (!parsed)
20
- return { ok: false, lines: [PERMISSION_CLI_USAGE] };
21
- setPermissionMode(parsed, scope);
22
- setPermissionOverride(parsed);
23
- return { ok: true, lines: [`✓ Permission set to ${permissionModeLabel(parsed)} (${scopeLabel(scope)})`] };
24
- }
25
- if (rest.length > 1)
26
- return { ok: false, lines: [PERMISSION_CLI_USAGE] };
27
- if (input.flagMode) {
28
- setPermissionMode(input.flagMode, scope);
29
- setPermissionOverride(input.flagMode);
30
- return { ok: true, lines: [`✓ Permission set to ${permissionModeLabel(input.flagMode)} (${scopeLabel(scope)})`] };
31
- }
32
- const snap = getPermissionSnapshot();
33
- const override = getPermissionOverride();
34
- const mode = override ?? snap.permission;
35
- const source = override ? "cli" : snap.source;
36
- return {
37
- ok: true,
38
- lines: [`Current permission: ${permissionModeLabel(mode)} (${sourceLabel(source)})`, PERMISSION_CLI_USAGE],
39
- };
40
- }
package/dist/plugins.js DELETED
@@ -1,131 +0,0 @@
1
- import { tool, jsonSchema } from "ai";
2
- import { existsSync, readdirSync, statSync } from "fs";
3
- import { pathToFileURL } from "url";
4
- import path from "path";
5
- import { getConfigDir } from "./config.js";
6
- import { confirm, isAutoApprove } from "./confirm.js";
7
- const PLUGIN_DIRS = [path.join(process.cwd(), ".min-agent", "tools"), path.join(getConfigDir(), "tools")];
8
- let cachedPlugins = null;
9
- let pluginReadOnlyIds = new Set();
10
- export function pluginNeedsConfirm(def) {
11
- if (def.readOnly === true)
12
- return false;
13
- if (def.dangerous === false)
14
- return false;
15
- return true;
16
- }
17
- export function getPluginReadOnlyIds() {
18
- return pluginReadOnlyIds;
19
- }
20
- function pluginSignature(dirs) {
21
- return dirs
22
- .map((dir) => {
23
- if (!existsSync(dir))
24
- return `${dir}:missing`;
25
- const files = readdirSync(dir)
26
- .filter((f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".mjs"))
27
- .sort();
28
- const mtimes = files.map((f) => statSync(path.join(dir, f)).mtimeMs).join(",");
29
- return `${dir}:${files.join(",")}:${mtimes}`;
30
- })
31
- .join("|");
32
- }
33
- export async function loadPluginTools(dirs) {
34
- const pluginDirs = dirs ?? PLUGIN_DIRS;
35
- const signature = pluginSignature(pluginDirs);
36
- if (cachedPlugins && cachedPlugins.signature === signature) {
37
- pluginReadOnlyIds = cachedPlugins.readOnlyIds;
38
- return cachedPlugins.tools;
39
- }
40
- const tools = await loadPluginToolsUncached(pluginDirs);
41
- cachedPlugins = { signature, tools, readOnlyIds: pluginReadOnlyIds };
42
- return tools;
43
- }
44
- async function loadPluginToolsUncached(pluginDirs) {
45
- const tools = {};
46
- const readOnlyIds = new Set();
47
- for (const dir of pluginDirs) {
48
- if (!existsSync(dir))
49
- continue;
50
- const files = readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".mjs"));
51
- for (const file of files) {
52
- const filePath = path.join(dir, file);
53
- const namespace = path.basename(file, path.extname(file));
54
- try {
55
- const mod = await import(pathToFileURL(filePath).href);
56
- for (const [exportName, def] of Object.entries(mod)) {
57
- if (!isPluginTool(def))
58
- continue;
59
- const toolId = exportName === "default" ? namespace : `${namespace}_${exportName}`;
60
- if (tools[toolId]) {
61
- console.warn(`\x1b[33m ⚠ Plugin tool "${toolId}" from ${filePath} conflicts with an existing tool; skipping\x1b[0m`);
62
- continue;
63
- }
64
- const properties = {};
65
- const required = [];
66
- for (const [key, param] of Object.entries(def.parameters)) {
67
- const type = narrowJsonType(param.type);
68
- properties[key] = { type, ...(param.description ? { description: param.description } : {}) };
69
- required.push(key);
70
- }
71
- if (def.readOnly === true)
72
- readOnlyIds.add(toolId);
73
- tools[toolId] = tool({
74
- description: def.description,
75
- inputSchema: jsonSchema({
76
- type: "object",
77
- properties,
78
- required,
79
- }),
80
- execute: async (args) => {
81
- if (pluginNeedsConfirm(def) && !isAutoApprove()) {
82
- const preview = summarizePluginArgs(args);
83
- const approved = await confirm(`运行自定义工具 ${toolId}${preview ? `\n${preview}` : ""}`);
84
- if (!approved)
85
- return "自定义工具已被拒绝。";
86
- }
87
- try {
88
- const result = await def.execute(args);
89
- return typeof result === "string" ? result : (JSON.stringify(result) ?? "undefined");
90
- }
91
- catch (err) {
92
- return `Plugin error: ${String(err instanceof Error ? err.message : err)}`;
93
- }
94
- },
95
- });
96
- }
97
- }
98
- catch (err) {
99
- console.error(`\x1b[90m Plugin "${file}" failed to load: ${String(err instanceof Error ? err.message : err)}\x1b[0m`);
100
- }
101
- }
102
- }
103
- pluginReadOnlyIds = readOnlyIds;
104
- const count = Object.keys(tools).length;
105
- if (count > 0) {
106
- console.log(`\x1b[90m Plugins loaded: ${count} tool(s)\x1b[0m`);
107
- }
108
- return tools;
109
- }
110
- function summarizePluginArgs(args) {
111
- try {
112
- const text = JSON.stringify(args);
113
- if (!text || text === "{}")
114
- return "";
115
- return text.length > 400 ? `${text.slice(0, 400)}…` : text;
116
- }
117
- catch {
118
- return "";
119
- }
120
- }
121
- function narrowJsonType(type) {
122
- if (type === "number" || type === "boolean" || type === "object" || type === "array")
123
- return type;
124
- return "string";
125
- }
126
- function isPluginTool(value) {
127
- if (!value || typeof value !== "object")
128
- return false;
129
- const obj = value;
130
- return typeof obj.description === "string" && typeof obj.parameters === "object" && typeof obj.execute === "function";
131
- }
package/dist/pricing.js DELETED
@@ -1,119 +0,0 @@
1
- import { readFileSync, mkdirSync, existsSync } from "fs";
2
- import path from "path";
3
- import { atomicWriteFileSync } from "./tools/atomic-file.js";
4
- import { getConfigDir, loadConfig } from "./config.js";
5
- const CACHE_TTL = 7 * 24 * 60 * 60 * 1000;
6
- const memoryCache = new Map();
7
- const inFlight = new Map();
8
- const NEGATIVE_TTL = 60 * 60 * 1000;
9
- const negativeCache = new Map();
10
- function cacheFile() {
11
- return path.join(getConfigDir(), "pricing-cache.json");
12
- }
13
- function loadDiskCache() {
14
- const file = cacheFile();
15
- if (!existsSync(file))
16
- return {};
17
- try {
18
- return JSON.parse(readFileSync(file, "utf-8"));
19
- }
20
- catch {
21
- return {};
22
- }
23
- }
24
- function saveCache(cache) {
25
- const file = cacheFile();
26
- mkdirSync(path.dirname(file), { recursive: true });
27
- atomicWriteFileSync(file, JSON.stringify(cache));
28
- }
29
- function getCached(modelId) {
30
- const memory = memoryCache.get(modelId);
31
- if (memory && Date.now() - memory.timestamp <= CACHE_TTL)
32
- return memory;
33
- const disk = loadDiskCache()[modelId];
34
- if (disk && Date.now() - disk.timestamp <= CACHE_TTL) {
35
- memoryCache.set(modelId, disk);
36
- return disk;
37
- }
38
- return null;
39
- }
40
- function setCache(modelId, price) {
41
- const entry = { price, timestamp: Date.now() };
42
- memoryCache.set(modelId, entry);
43
- const cache = loadDiskCache();
44
- cache[modelId] = entry;
45
- saveCache(cache);
46
- }
47
- function priceFromConfig(modelId) {
48
- const override = loadConfig().pricing?.[modelId];
49
- if (!override)
50
- return null;
51
- if (typeof override.inputPerMillion !== "number" || typeof override.outputPerMillion !== "number")
52
- return null;
53
- return { inputPerMillion: override.inputPerMillion, outputPerMillion: override.outputPerMillion };
54
- }
55
- async function fetchFromModelsDev(modelId) {
56
- try {
57
- const response = await fetch("https://models.dev/api.json", { signal: AbortSignal.timeout(10000) });
58
- if (!response.ok)
59
- return null;
60
- const providers = (await response.json());
61
- for (const provider of Object.values(providers)) {
62
- if (!provider.models)
63
- continue;
64
- for (const [id, model] of Object.entries(provider.models)) {
65
- if (id === modelId || id.endsWith(`/${modelId}`) || modelId.endsWith(`/${id}`)) {
66
- const p = model?.pricing;
67
- if (p && typeof p.input === "number" && typeof p.output === "number") {
68
- return { inputPerMillion: p.input, outputPerMillion: p.output };
69
- }
70
- }
71
- }
72
- }
73
- return null;
74
- }
75
- catch {
76
- return null;
77
- }
78
- }
79
- export async function getModelPrice(modelId) {
80
- const override = priceFromConfig(modelId);
81
- if (override)
82
- return override;
83
- const cached = getCached(modelId);
84
- if (cached)
85
- return cached.price;
86
- const negAt = negativeCache.get(modelId);
87
- if (negAt !== undefined && Date.now() - negAt <= NEGATIVE_TTL)
88
- return null;
89
- const pending = inFlight.get(modelId);
90
- if (pending)
91
- return pending;
92
- const probing = fetchFromModelsDev(modelId).then((price) => {
93
- if (price) {
94
- setCache(modelId, price);
95
- }
96
- else {
97
- negativeCache.set(modelId, Date.now());
98
- }
99
- return price;
100
- });
101
- inFlight.set(modelId, probing);
102
- try {
103
- return await probing;
104
- }
105
- finally {
106
- inFlight.delete(modelId);
107
- }
108
- }
109
- export function estimateCost(usage, price) {
110
- if (!price)
111
- return null;
112
- const cost = (usage.inputTokens / 1_000_000) * price.inputPerMillion + (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 DELETED
@@ -1,63 +0,0 @@
1
- import { createOpenAI } from "@ai-sdk/openai";
2
- import { getEffectiveConfig, getActiveProvider, normalizeOllamaBaseURL } from "./config.js";
3
- import { createTimeoutFetch, DEFAULT_FIRST_BYTE_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, } from "./fetch-timeout.js";
4
- import { foldReasoningInChatResponse } from "./reasoning-stream.js";
5
- import { fetchWithThinkingWire } from "./thinking-wire.js";
6
- function positiveMs(raw, fallback) {
7
- const value = typeof raw === "string" ? Number(raw) : raw;
8
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
9
- return fallback;
10
- return value;
11
- }
12
- /** Timeout/trace-aware fetch shared by every provider client. */
13
- export function modelFetch() {
14
- const cfg = getEffectiveConfig();
15
- const inner = createTimeoutFetch({
16
- firstByteTimeoutMs: positiveMs(process.env.MIN_AGENT_REQUEST_TIMEOUT_MS ?? cfg.agent?.requestTimeoutMs, DEFAULT_FIRST_BYTE_TIMEOUT_MS),
17
- idleTimeoutMs: positiveMs(process.env.MIN_AGENT_STREAM_IDLE_TIMEOUT_MS ?? cfg.agent?.streamIdleTimeoutMs, DEFAULT_STREAM_IDLE_TIMEOUT_MS),
18
- trace: process.env.MIN_AGENT_TRACE === "1" || process.env.MIN_AGENT_TRACE === "true",
19
- });
20
- return (input, init) => fetchWithThinkingWire(inner, input, init).then(foldReasoningInChatResponse);
21
- }
22
- export function resolveModelForProvider(provider, modelId) {
23
- if (!provider.baseURL || !provider.apiKey) {
24
- throw new Error("Not configured. Run: min-agent setup");
25
- }
26
- const id = modelId ?? provider.defaultModel;
27
- if (!id) {
28
- throw new Error("No model specified. Run: min-agent setup");
29
- }
30
- const fetchImpl = modelFetch();
31
- switch (provider.type ?? "openai-compatible") {
32
- case "openai": {
33
- const client = createOpenAI({ apiKey: provider.apiKey, fetch: fetchImpl });
34
- return client.chat(id);
35
- }
36
- case "ollama": {
37
- const client = createOpenAI({
38
- baseURL: normalizeOllamaBaseURL(provider.baseURL),
39
- apiKey: provider.apiKey || "ollama",
40
- fetch: fetchImpl,
41
- });
42
- return client.chat(id);
43
- }
44
- case "openai-compatible": {
45
- const client = createOpenAI({
46
- baseURL: provider.baseURL,
47
- apiKey: provider.apiKey,
48
- fetch: fetchImpl,
49
- });
50
- return client.chat(id);
51
- }
52
- default:
53
- throw new Error(`Unknown provider type: ${provider.type}`);
54
- }
55
- }
56
- export function resolveModel(modelId, providerName) {
57
- const config = getEffectiveConfig();
58
- const provider = providerName ? config.providers?.find((p) => p.name === providerName) : getActiveProvider(config);
59
- if (!provider) {
60
- throw new Error(providerName ? `Provider "${providerName}" not found` : "Not configured. Run: min-agent setup");
61
- }
62
- return resolveModelForProvider(provider, modelId);
63
- }
@@ -1,60 +0,0 @@
1
- const OPTION_TEXT_KEYS = ["label", "description", "title", "text", "value", "content", "prompt"];
2
- function firstText(rec, keys) {
3
- const parts = [];
4
- const seen = new Set();
5
- for (const key of keys) {
6
- const v = rec[key];
7
- if (typeof v !== "string")
8
- continue;
9
- const text = v.trim();
10
- if (!text || seen.has(text))
11
- continue;
12
- seen.add(text);
13
- parts.push(text);
14
- }
15
- return parts;
16
- }
17
- /** Pull a user-visible label out of a model-supplied question option. */
18
- export function optionText(item) {
19
- if (item == null)
20
- return null;
21
- if (typeof item === "string") {
22
- const text = item.trim();
23
- return text.length > 0 ? text : null;
24
- }
25
- if (typeof item === "number" || typeof item === "boolean")
26
- return String(item);
27
- if (typeof item !== "object" || Array.isArray(item))
28
- return null;
29
- const rec = item;
30
- const parts = firstText(rec, OPTION_TEXT_KEYS);
31
- if (parts.length > 0)
32
- return parts.join(" — ");
33
- const key = rec.key;
34
- return typeof key === "string" && key.trim() ? key.trim() : null;
35
- }
36
- export function normalizeQuestionPrompt(question) {
37
- if (typeof question === "string")
38
- return question;
39
- return optionText(question) ?? "";
40
- }
41
- /**
42
- * Models often send `{label, description, key}` (Claude-style) instead of
43
- * `string[]`. Normalize to display strings so the TUI never calls string
44
- * methods on objects.
45
- */
46
- export function normalizeQuestionOptions(options) {
47
- if (options == null)
48
- return [];
49
- const list = Array.isArray(options) ? options : [options];
50
- const out = [];
51
- const seen = new Set();
52
- for (const item of list) {
53
- const text = optionText(item);
54
- if (!text || seen.has(text))
55
- continue;
56
- seen.add(text);
57
- out.push(text);
58
- }
59
- return out;
60
- }
@@ -1,158 +0,0 @@
1
- /**
2
- * OpenAI-compatible chat completions put the thinking chain in
3
- * `reasoning_content` / `reasoning` / `reasoning_details`. The @ai-sdk/openai
4
- * chat parser only reads `delta.content`, so those fields never become
5
- * reasoning events. Fold them into `<think>` tags that ThinkingBodySplitter
6
- * already turns into on-screen thinking.
7
- */
8
- function isRecord(v) {
9
- return typeof v === "object" && v !== null && !Array.isArray(v);
10
- }
11
- function sanitizeReasoning(text) {
12
- return text.replace(/<\/?(?:think|thinking)>/gi, "");
13
- }
14
- function stringField(v, key) {
15
- if (!isRecord(v))
16
- return "";
17
- return typeof v[key] === "string" ? v[key] : "";
18
- }
19
- function reasoningFromDetails(details) {
20
- if (!Array.isArray(details))
21
- return "";
22
- return details
23
- .map((item) => {
24
- const text = stringField(item, "text");
25
- if (text)
26
- return text;
27
- return stringField(item, "content");
28
- })
29
- .join("");
30
- }
31
- function reasoningFromValue(v) {
32
- if (typeof v === "string")
33
- return v;
34
- if (!isRecord(v))
35
- return "";
36
- return stringField(v, "content") || stringField(v, "text");
37
- }
38
- export function reasoningTextFromDelta(delta) {
39
- const details = reasoningFromDetails(delta.reasoning_details);
40
- const primary = reasoningFromValue(delta.reasoning_content) ||
41
- reasoningFromValue(delta.reasoning) ||
42
- reasoningFromValue(delta.thinking);
43
- return sanitizeReasoning(`${details}${primary}`);
44
- }
45
- export function createReasoningDeltaTracker() {
46
- let acc = "";
47
- return (text) => {
48
- if (!text)
49
- return "";
50
- if (acc && text.startsWith(acc)) {
51
- const extra = text.slice(acc.length);
52
- acc = text;
53
- return extra;
54
- }
55
- acc += text;
56
- return text;
57
- };
58
- }
59
- export function foldReasoningIntoDelta(delta, takeDelta) {
60
- const reasoning = takeDelta(reasoningTextFromDelta(delta));
61
- if (!reasoning)
62
- return false;
63
- const content = typeof delta.content === "string" ? delta.content : "";
64
- delta.content = `<think>${reasoning}</think>${content}`;
65
- return true;
66
- }
67
- export function foldReasoningInChunk(chunk, takeDelta = (t) => t) {
68
- if (!isRecord(chunk) || !Array.isArray(chunk.choices))
69
- return false;
70
- let changed = false;
71
- for (const choice of chunk.choices) {
72
- if (!isRecord(choice))
73
- continue;
74
- if (isRecord(choice.delta) && foldReasoningIntoDelta(choice.delta, takeDelta))
75
- changed = true;
76
- if (isRecord(choice.message) && foldReasoningIntoDelta(choice.message, takeDelta))
77
- changed = true;
78
- }
79
- return changed;
80
- }
81
- export function foldReasoningIntoSseLine(line, takeDelta) {
82
- const trimmed = line.replace(/\r$/, "");
83
- if (!trimmed.startsWith("data:"))
84
- return line;
85
- const payload = trimmed.slice("data:".length).trim();
86
- if (!payload || payload === "[DONE]")
87
- return line;
88
- try {
89
- const parsed = JSON.parse(payload);
90
- if (!foldReasoningInChunk(parsed, takeDelta))
91
- return line;
92
- const prefix = line.startsWith("data: ") ? "data: " : "data:";
93
- const nl = line.endsWith("\r") ? "\r" : "";
94
- return `${prefix}${JSON.stringify(parsed)}${nl}`;
95
- }
96
- catch {
97
- return line;
98
- }
99
- }
100
- function rewriteSseFragment(fragment, takeDelta) {
101
- const keepLastNl = fragment.endsWith("\n");
102
- const raw = keepLastNl ? fragment.slice(0, -1) : fragment;
103
- return (raw
104
- .split("\n")
105
- .map((line) => foldReasoningIntoSseLine(line, takeDelta))
106
- .join("\n") + (keepLastNl ? "\n" : ""));
107
- }
108
- export function transformReasoningSse(source) {
109
- const decoder = new TextDecoder();
110
- const encoder = new TextEncoder();
111
- const reader = source.getReader();
112
- const takeDelta = createReasoningDeltaTracker();
113
- let pending = "";
114
- return new ReadableStream({
115
- async pull(controller) {
116
- for (;;) {
117
- const { done, value } = await reader.read();
118
- if (done) {
119
- pending += decoder.decode();
120
- if (pending)
121
- controller.enqueue(encoder.encode(rewriteSseFragment(pending, takeDelta)));
122
- controller.close();
123
- return;
124
- }
125
- pending += decoder.decode(value, { stream: true });
126
- const nl = pending.lastIndexOf("\n");
127
- if (nl < 0)
128
- continue;
129
- const complete = pending.slice(0, nl + 1);
130
- pending = pending.slice(nl + 1);
131
- controller.enqueue(encoder.encode(rewriteSseFragment(complete, takeDelta)));
132
- return;
133
- }
134
- },
135
- cancel(reason) {
136
- return reader.cancel(reason);
137
- },
138
- });
139
- }
140
- function isChatSseResponse(response) {
141
- const ctype = (response.headers.get("content-type") ?? "").toLowerCase();
142
- if (ctype.includes("event-stream"))
143
- return true;
144
- if (!ctype || ctype.startsWith("text/plain"))
145
- return true;
146
- return false;
147
- }
148
- export function foldReasoningInChatResponse(response) {
149
- if (!response.body || response.status >= 400)
150
- return response;
151
- if (!isChatSseResponse(response))
152
- return response;
153
- return new Response(transformReasoningSse(response.body), {
154
- status: response.status,
155
- statusText: response.statusText,
156
- headers: response.headers,
157
- });
158
- }