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,33 +0,0 @@
1
- import { takeScopeFlags, scopeLabel } from "./scope.js";
2
- import { parseMemoryMode, resolveMemoryMode, setMemoryMode, setMemoryOverride, memoryModeLabel, memorySourceLabel, } from "./memory.js";
3
- export const MEMORY_CLI_USAGE = [
4
- "Usage: min-agent memory [on|off] [--project|--global]",
5
- " min-agent --memory on|off [--project|--global]",
6
- ].join("\n");
7
- export function runMemoryCli(input) {
8
- const { scope: posScope, rest } = takeScopeFlags(input.positionals);
9
- const scope = posScope ?? input.scope ?? "global";
10
- if (rest[0] === "--help" || rest[0] === "-h") {
11
- return { ok: true, lines: [MEMORY_CLI_USAGE] };
12
- }
13
- if (rest.length === 1) {
14
- const parsed = parseMemoryMode(rest[0]);
15
- if (!parsed)
16
- return { ok: false, lines: [MEMORY_CLI_USAGE] };
17
- setMemoryMode(parsed, scope);
18
- setMemoryOverride(parsed);
19
- return { ok: true, lines: [`✓ Memory set to ${memoryModeLabel(parsed)} (${scopeLabel(scope)})`] };
20
- }
21
- if (rest.length > 1)
22
- return { ok: false, lines: [MEMORY_CLI_USAGE] };
23
- if (input.flagMode) {
24
- setMemoryMode(input.flagMode, scope);
25
- setMemoryOverride(input.flagMode);
26
- return { ok: true, lines: [`✓ Memory set to ${memoryModeLabel(input.flagMode)} (${scopeLabel(scope)})`] };
27
- }
28
- const { memory, source } = resolveMemoryMode();
29
- return {
30
- ok: true,
31
- lines: [`Current memory: ${memoryModeLabel(memory)} (${memorySourceLabel(source)})`, MEMORY_CLI_USAGE],
32
- };
33
- }
package/dist/memory.js DELETED
@@ -1,295 +0,0 @@
1
- import { readFileSync, mkdirSync, existsSync, renameSync, statSync } from "fs";
2
- import path from "path";
3
- import { tool, jsonSchema } from "ai";
4
- import { getConfigDir, getProjectConfigDir, loadConfig, loadProjectConfig, saveConfig, saveProjectConfig, parseMemoryMode, } from "./config.js";
5
- import { atomicWriteFileSync } from "./tools/atomic-file.js";
6
- import { readScope, takeScopeFlags } from "./scope.js";
7
- export { parseMemoryMode, readScope as readMemoryScope, takeScopeFlags };
8
- export const DEFAULT_MEMORY_MODE = "off";
9
- const MAX_INJECTED_MEMORIES = 30;
10
- let memoryOverride;
11
- export function setMemoryOverride(mode) {
12
- memoryOverride = mode;
13
- }
14
- export function getMemoryOverride() {
15
- return memoryOverride;
16
- }
17
- export function memoryModeLabel(mode) {
18
- return mode;
19
- }
20
- export function memorySourceLabel(source) {
21
- if (source === "cli")
22
- return "this run";
23
- if (source === "project")
24
- return "project";
25
- if (source === "global")
26
- return "global";
27
- return "default";
28
- }
29
- export function getMemorySnapshot() {
30
- const project = parseMemoryMode(loadProjectConfig().memory);
31
- if (project !== undefined)
32
- return { memory: project, source: "project" };
33
- const global = parseMemoryMode(loadConfig().memory);
34
- if (global !== undefined)
35
- return { memory: global, source: "global" };
36
- return { memory: undefined, source: null };
37
- }
38
- export function resolveMemoryMode(runOverride) {
39
- if (runOverride)
40
- return { memory: runOverride, source: "cli" };
41
- const override = getMemoryOverride();
42
- if (override)
43
- return { memory: override, source: "cli" };
44
- const snap = getMemorySnapshot();
45
- return { memory: snap.memory ?? DEFAULT_MEMORY_MODE, source: snap.source };
46
- }
47
- export function isMemoryEnabled(runOverride) {
48
- return resolveMemoryMode(runOverride).memory === "on";
49
- }
50
- function clearProjectMemoryMode() {
51
- const project = loadProjectConfig();
52
- if (project.memory === undefined)
53
- return;
54
- const { memory: _removed, ...rest } = project;
55
- saveProjectConfig(rest);
56
- }
57
- export function setMemoryMode(mode, scope) {
58
- if (scope === "project") {
59
- saveProjectConfig({ ...loadProjectConfig(), memory: mode });
60
- return;
61
- }
62
- saveConfig({ ...loadConfig(), memory: mode });
63
- clearProjectMemoryMode();
64
- }
65
- export function memoryPayload() {
66
- const { memory, source } = resolveMemoryMode();
67
- return { memory, source, label: memoryModeLabel(memory) };
68
- }
69
- export function defaultMemoryScope() {
70
- return existsSync(getProjectConfigDir()) ? "project" : "global";
71
- }
72
- function resolveStore(scope) {
73
- return scope === "global" || scope === "project" ? scope : defaultMemoryScope();
74
- }
75
- function getMemoryFile(scope) {
76
- if (scope === "project")
77
- return path.join(getProjectConfigDir(), "memory.json");
78
- return path.join(getConfigDir(), "memory.json");
79
- }
80
- const memoryFileCache = new Map();
81
- function parseMemoryItem(value) {
82
- if (typeof value !== "object" || value === null)
83
- return undefined;
84
- const rec = value;
85
- if (typeof rec.content !== "string")
86
- return undefined;
87
- const tags = Array.isArray(rec.tags) ? rec.tags.filter((t) => typeof t === "string") : [];
88
- const created = typeof rec.created === "string" ? rec.created : "";
89
- return { content: rec.content, tags, created };
90
- }
91
- function backupCorrupt(file) {
92
- try {
93
- renameSync(file, `${file}.corrupt-${Date.now()}`);
94
- }
95
- catch { }
96
- }
97
- function rememberCache(file, memories) {
98
- try {
99
- const st = statSync(file);
100
- memoryFileCache.set(file, { mtimeMs: st.mtimeMs, size: st.size, memories });
101
- }
102
- catch {
103
- memoryFileCache.delete(file);
104
- }
105
- }
106
- export function loadMemories(scope = "global") {
107
- const file = getMemoryFile(scope);
108
- let st;
109
- try {
110
- st = statSync(file);
111
- }
112
- catch {
113
- memoryFileCache.delete(file);
114
- return [];
115
- }
116
- const cached = memoryFileCache.get(file);
117
- if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size)
118
- return cached.memories;
119
- let data;
120
- try {
121
- data = JSON.parse(readFileSync(file, "utf-8"));
122
- }
123
- catch {
124
- backupCorrupt(file);
125
- memoryFileCache.delete(file);
126
- return [];
127
- }
128
- if (!Array.isArray(data)) {
129
- backupCorrupt(file);
130
- memoryFileCache.delete(file);
131
- return [];
132
- }
133
- const memories = data.flatMap((item) => {
134
- const parsed = parseMemoryItem(item);
135
- return parsed ? [parsed] : [];
136
- });
137
- memoryFileCache.set(file, { mtimeMs: st.mtimeMs, size: st.size, memories });
138
- return memories;
139
- }
140
- function saveMemories(memories, scope) {
141
- const file = getMemoryFile(scope);
142
- mkdirSync(path.dirname(file), { recursive: true });
143
- atomicWriteFileSync(file, JSON.stringify(memories, null, 2));
144
- rememberCache(file, memories);
145
- }
146
- export function addMemory(content, tags = [], scope = "global") {
147
- const memories = loadMemories(scope);
148
- const memory = {
149
- content,
150
- tags,
151
- created: new Date().toISOString(),
152
- };
153
- memories.push(memory);
154
- saveMemories(memories, scope);
155
- return memory;
156
- }
157
- export function deleteMemory(index, scope = "global") {
158
- const memories = loadMemories(scope);
159
- if (index < 0 || index >= memories.length)
160
- return false;
161
- memories.splice(index, 1);
162
- saveMemories(memories, scope);
163
- return true;
164
- }
165
- export function searchMemories(query, scope = "global") {
166
- const memories = loadMemories(scope);
167
- const lower = query.toLowerCase();
168
- return memories
169
- .map((m, i) => ({ ...m, index: i }))
170
- .filter((m) => m.content.toLowerCase().includes(lower) || m.tags.some((t) => t.toLowerCase().includes(lower)));
171
- }
172
- export function formatMemoryLine(memory, index, opts) {
173
- const tags = memory.tags.length > 0 ? ` [${memory.tags.join(", ")}]` : "";
174
- const date = opts?.date ? memory.created.split("T")[0] : "";
175
- const suffix = date ? ` (${date})` : "";
176
- return ` #${index + 1}: ${memory.content}${tags}${suffix}`;
177
- }
178
- function formatMemoryBlock(memories, heading) {
179
- if (memories.length === 0)
180
- return "";
181
- const start = Math.max(0, memories.length - MAX_INJECTED_MEMORIES);
182
- const items = memories
183
- .slice(start)
184
- .map((m, i) => ({ m, num: start + i + 1 }))
185
- .reverse()
186
- .map(({ m, num }) => {
187
- const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
188
- return ` ${num}. ${m.content}${tags}`;
189
- });
190
- const capped = memories.length > MAX_INJECTED_MEMORIES
191
- ? `\n(${memories.length - MAX_INJECTED_MEMORIES} older memories not shown — use memory_search to find them.)`
192
- : "";
193
- return [heading, ...items, capped].filter(Boolean).join("\n");
194
- }
195
- export function getMemorySystemPrompt(runOverride) {
196
- if (!isMemoryEnabled(runOverride))
197
- return "";
198
- const project = loadMemories("project");
199
- const global = loadMemories("global");
200
- if (project.length === 0 && global.length === 0)
201
- return "";
202
- const intro = [
203
- "## Memories",
204
- "Background facts from previous conversations. Use them to personalize replies. They are not pending tasks and not a request to act.",
205
- "Do not call APIs, run commands, or continue old work based on memories unless the current user message explicitly asks.",
206
- "Do not use remembered credentials or API keys unless this message asks you to perform that action.",
207
- "You can save new memories with the memory_save tool when the user tells you something worth remembering (preferences, project details, conventions, etc).",
208
- 'Use scope "project" for repo-specific facts and "global" for preferences that apply everywhere.',
209
- "",
210
- ];
211
- const sections = [
212
- formatMemoryBlock(project, "### This project"),
213
- formatMemoryBlock(global, project.length > 0 ? "### Global" : ""),
214
- ].filter((s) => s.length > 0);
215
- return [...intro, ...sections].join("\n");
216
- }
217
- export function getMemoryTools() {
218
- const memorySave = tool({
219
- description: "Save a memory for future conversations. Use this when the user shares preferences, project conventions, important context, or asks you to remember something. Memories persist across sessions and are injected as background reference, not as work to resume. Do not store secrets, API keys, or passwords. Use scope project for this repository, global for all projects.",
220
- inputSchema: jsonSchema({
221
- type: "object",
222
- properties: {
223
- content: { type: "string", description: "The information to remember" },
224
- tags: {
225
- type: "array",
226
- items: { type: "string" },
227
- description: "Optional tags for categorization (e.g. 'preference', 'project', 'convention')",
228
- },
229
- scope: {
230
- type: "string",
231
- enum: ["global", "project"],
232
- description: "Where to store the memory. Defaults to project when .min-agent exists, otherwise global.",
233
- },
234
- },
235
- required: ["content"],
236
- }),
237
- execute: async ({ content, tags, scope }) => {
238
- const resolved = resolveStore(scope);
239
- const memory = addMemory(content, tags ?? [], resolved);
240
- return `Saved ${resolved} memory: "${content}" (tags: ${memory.tags.length > 0 ? memory.tags.join(", ") : "none"})`;
241
- },
242
- });
243
- const memorySearch = tool({
244
- description: "Search through saved memories by keyword. Use this to recall previously saved information.",
245
- inputSchema: jsonSchema({
246
- type: "object",
247
- properties: {
248
- query: { type: "string", description: "Search keyword or phrase" },
249
- scope: {
250
- type: "string",
251
- enum: ["global", "project"],
252
- description: "Limit search to one store. Omit to search both.",
253
- },
254
- },
255
- required: ["query"],
256
- }),
257
- execute: async ({ query, scope }) => {
258
- const scopes = scope === "global" || scope === "project" ? [scope] : ["project", "global"];
259
- const lines = scopes.flatMap((s) => searchMemories(query, s).map((m) => {
260
- const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
261
- return `${s} #${m.index + 1}: ${m.content}${tags} (${m.created.split("T")[0]})`;
262
- }));
263
- if (lines.length === 0)
264
- return `No memories found matching "${query}"`;
265
- return lines.join("\n");
266
- },
267
- });
268
- const memoryDelete = tool({
269
- description: "Delete a memory by its number. Use memory_search first to find the index and scope.",
270
- inputSchema: jsonSchema({
271
- type: "object",
272
- properties: {
273
- index: { type: "number", description: "The memory number to delete (1-based)" },
274
- scope: {
275
- type: "string",
276
- enum: ["global", "project"],
277
- description: "Which store the number belongs to. Defaults to project when .min-agent exists, otherwise global.",
278
- },
279
- },
280
- required: ["index"],
281
- }),
282
- execute: async ({ index, scope }) => {
283
- const resolved = resolveStore(scope);
284
- const success = deleteMemory(index - 1, resolved);
285
- if (success)
286
- return `${resolved} memory #${index} deleted.`;
287
- return `${resolved} memory #${index} not found.`;
288
- },
289
- });
290
- return {
291
- memory_save: memorySave,
292
- memory_search: memorySearch,
293
- memory_delete: memoryDelete,
294
- };
295
- }
@@ -1,285 +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, parseThinkingEffort } from "./config.js";
5
- const DEFAULT_LONAE_BASE = "https://models.lonae.com";
6
- const CACHE_TTL = 7 * 24 * 60 * 60 * 1000;
7
- const CATALOG_ENTRY_VERSION = 3;
8
- const NEGATIVE_TTL = 60 * 60 * 1000;
9
- const FETCH_TIMEOUT_MS = 10000;
10
- const memoryCache = new Map();
11
- const inFlight = new Map();
12
- const negativeCache = new Map();
13
- export function lonaeBaseUrl() {
14
- const raw = process.env.MIN_AGENT_MODELS_API_URL?.trim();
15
- if (!raw)
16
- return DEFAULT_LONAE_BASE;
17
- return raw.replace(/\/$/, "");
18
- }
19
- function cacheFile() {
20
- return path.join(getConfigDir(), "model-catalog-cache.json");
21
- }
22
- function loadDiskCache() {
23
- const file = cacheFile();
24
- if (!existsSync(file))
25
- return {};
26
- try {
27
- return JSON.parse(readFileSync(file, "utf-8"));
28
- }
29
- catch {
30
- return {};
31
- }
32
- }
33
- function saveDiskCache(cache) {
34
- const file = cacheFile();
35
- mkdirSync(path.dirname(file), { recursive: true });
36
- atomicWriteFileSync(file, JSON.stringify(cache));
37
- }
38
- export function clearModelCatalogCache() {
39
- memoryCache.clear();
40
- inFlight.clear();
41
- negativeCache.clear();
42
- }
43
- export function getCachedModelCatalog(modelId) {
44
- const memory = memoryCache.get(modelId);
45
- if (memory && memory.version === CATALOG_ENTRY_VERSION && Date.now() - memory.timestamp <= CACHE_TTL)
46
- return memory;
47
- const disk = loadDiskCache()[modelId];
48
- if (!disk || disk.version !== CATALOG_ENTRY_VERSION || Date.now() - disk.timestamp > CACHE_TTL)
49
- return null;
50
- memoryCache.set(modelId, disk);
51
- return disk;
52
- }
53
- function setCache(modelId, entry) {
54
- memoryCache.set(modelId, entry);
55
- const cache = loadDiskCache();
56
- cache[modelId] = entry;
57
- saveDiskCache(cache);
58
- }
59
- function isRecord(value) {
60
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
61
- }
62
- export function parseReasoningOptions(raw) {
63
- if (!Array.isArray(raw))
64
- return { toggle: false, efforts: [] };
65
- let toggle = false;
66
- const efforts = new Set();
67
- for (const item of raw) {
68
- if (!isRecord(item))
69
- continue;
70
- if (item.type === "toggle")
71
- toggle = true;
72
- if (item.type === "effort" && Array.isArray(item.values)) {
73
- for (const value of item.values) {
74
- if (typeof value === "string" && value && value !== "default")
75
- efforts.add(value);
76
- }
77
- }
78
- }
79
- if (efforts.has("none"))
80
- toggle = true;
81
- return { toggle, efforts: [...efforts] };
82
- }
83
- export function pickListedModel(models, modelId) {
84
- const lower = modelId.toLowerCase();
85
- const exact = models.find((m) => m.id.toLowerCase() === lower);
86
- if (exact)
87
- return exact;
88
- const suffix = models.find((m) => {
89
- const id = m.id.toLowerCase();
90
- return id.endsWith(`/${lower}`) || lower.endsWith(`/${id}`);
91
- });
92
- if (suffix)
93
- return suffix;
94
- return models.find((m) => m.name?.toLowerCase() === lower);
95
- }
96
- export function matchOffering(offerings, baseURL) {
97
- if (!baseURL || offerings.length === 0)
98
- return undefined;
99
- let host = "";
100
- try {
101
- host = new URL(baseURL).hostname.toLowerCase();
102
- }
103
- catch {
104
- return undefined;
105
- }
106
- return offerings.find((o) => {
107
- const id = String(o.providerId ?? "")
108
- .toLowerCase()
109
- .replace(/_/g, "-");
110
- if (!id)
111
- return false;
112
- if (host.includes(id))
113
- return true;
114
- const slug = id.split("-")[0] ?? "";
115
- return slug.length >= 4 && host.includes(slug);
116
- });
117
- }
118
- const CANONICAL_EFFORTS = ["low", "medium", "high", "max"];
119
- function hasEffortLevel(parsed) {
120
- return parsed.efforts.some((raw) => {
121
- const canonical = parseThinkingEffort(raw);
122
- return Boolean(canonical && canonical !== "off");
123
- });
124
- }
125
- /** When the active host is unknown, keep levels a majority of offerings actually list. */
126
- export function majorityReasoning(offerings) {
127
- const voters = offerings
128
- .map((offering) => parseReasoningOptions(offering.reasoningOptions))
129
- .filter((parsed) => parsed.toggle || parsed.efforts.length > 0);
130
- if (voters.length === 0)
131
- return { toggle: false, efforts: [] };
132
- const effortVoters = voters.filter(hasEffortLevel);
133
- const toggleThreshold = voters.length / 2;
134
- const effortThreshold = effortVoters.length === 0 ? Number.POSITIVE_INFINITY : effortVoters.length / 2;
135
- let toggleVotes = 0;
136
- let xhighVotes = 0;
137
- const votes = new Map();
138
- for (const parsed of voters) {
139
- if (parsed.toggle || parsed.efforts.includes("none"))
140
- toggleVotes++;
141
- }
142
- for (const parsed of effortVoters) {
143
- const seen = new Set();
144
- for (const raw of parsed.efforts) {
145
- if (raw === "xhigh" || raw === "extra-high")
146
- xhighVotes++;
147
- const canonical = parseThinkingEffort(raw);
148
- if (!canonical || canonical === "off" || seen.has(canonical))
149
- continue;
150
- seen.add(canonical);
151
- votes.set(canonical, (votes.get(canonical) ?? 0) + 1);
152
- }
153
- }
154
- const toggle = toggleVotes >= toggleThreshold;
155
- const efforts = [];
156
- if (toggle)
157
- efforts.push("none");
158
- for (const key of CANONICAL_EFFORTS) {
159
- if ((votes.get(key) ?? 0) >= effortThreshold)
160
- efforts.push(key);
161
- }
162
- if (efforts.includes("max") && xhighVotes > 0)
163
- efforts.push("xhigh");
164
- return { toggle, efforts };
165
- }
166
- function contextFrom(limit) {
167
- const n = limit?.context;
168
- if (typeof n === "number" && Number.isFinite(n) && n >= 4096)
169
- return Math.floor(n);
170
- return undefined;
171
- }
172
- export function catalogFromLonaeModel(model, modelId, baseURL) {
173
- const offerings = model.offerings ?? [];
174
- const matched = matchOffering(offerings, baseURL);
175
- const reasoning = matched ? parseReasoningOptions(matched.reasoningOptions) : majorityReasoning(offerings);
176
- const contextWindow = contextFrom(matched?.limit) ?? contextFrom(model.limit);
177
- return {
178
- version: CATALOG_ENTRY_VERSION,
179
- modelId,
180
- lonaeId: model.id,
181
- ...(contextWindow != null ? { contextWindow } : {}),
182
- reasoning,
183
- timestamp: Date.now(),
184
- };
185
- }
186
- async function fetchJson(url) {
187
- try {
188
- const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
189
- if (!response.ok)
190
- return null;
191
- return await response.json();
192
- }
193
- catch {
194
- return null;
195
- }
196
- }
197
- function readModel(payload) {
198
- const root = isRecord(payload) ? payload : null;
199
- const data = root && isRecord(root.data) ? root.data : root;
200
- if (!data || typeof data.id !== "string")
201
- return null;
202
- const limit = isRecord(data.limit)
203
- ? { context: typeof data.limit.context === "number" ? data.limit.context : undefined }
204
- : undefined;
205
- const offerings = Array.isArray(data.offerings)
206
- ? data.offerings.filter(isRecord).map((o) => ({
207
- providerId: typeof o.providerId === "string" ? o.providerId : undefined,
208
- limit: isRecord(o.limit)
209
- ? { context: typeof o.limit.context === "number" ? o.limit.context : undefined }
210
- : undefined,
211
- reasoningOptions: o.reasoningOptions,
212
- }))
213
- : [];
214
- return {
215
- id: data.id,
216
- limit,
217
- reasoning: data.reasoning === true,
218
- offerings,
219
- };
220
- }
221
- function readList(payload) {
222
- const root = isRecord(payload) ? payload : null;
223
- const data = root && Array.isArray(root.data) ? root.data : Array.isArray(payload) ? payload : [];
224
- return data.flatMap((item) => {
225
- if (!isRecord(item) || typeof item.id !== "string")
226
- return [];
227
- return [
228
- {
229
- id: item.id,
230
- name: typeof item.name === "string" ? item.name : undefined,
231
- context: typeof item.context === "number" ? item.context : undefined,
232
- },
233
- ];
234
- });
235
- }
236
- function modelPath(id) {
237
- return id
238
- .split("/")
239
- .map((part) => encodeURIComponent(part))
240
- .join("/");
241
- }
242
- async function fetchLonaeModel(modelId) {
243
- const base = lonaeBaseUrl();
244
- if (modelId.includes("/")) {
245
- const direct = readModel(await fetchJson(`${base}/api/v1/models/${modelPath(modelId)}`));
246
- if (direct)
247
- return direct;
248
- }
249
- const listed = readList(await fetchJson(`${base}/api/v1/models?q=${encodeURIComponent(modelId)}&page_size=20`));
250
- const match = pickListedModel(listed, modelId);
251
- if (!match)
252
- return null;
253
- return readModel(await fetchJson(`${base}/api/v1/models/${modelPath(match.id)}`));
254
- }
255
- /**
256
- * Resolve context window and thinking options from the Lonae catalog.
257
- * Successful lookups are cached in memory and on disk for 7 days.
258
- */
259
- export async function getModelCatalog(modelId, baseURL) {
260
- const cached = getCachedModelCatalog(modelId);
261
- if (cached)
262
- return cached;
263
- const negAt = negativeCache.get(modelId);
264
- if (negAt !== undefined && Date.now() - negAt <= NEGATIVE_TTL)
265
- return null;
266
- const pending = inFlight.get(modelId);
267
- if (pending)
268
- return pending;
269
- const probing = fetchLonaeModel(modelId).then((model) => {
270
- if (!model) {
271
- negativeCache.set(modelId, Date.now());
272
- return null;
273
- }
274
- const entry = catalogFromLonaeModel(model, modelId, baseURL);
275
- setCache(modelId, entry);
276
- return entry;
277
- });
278
- inFlight.set(modelId, probing);
279
- try {
280
- return await probing;
281
- }
282
- finally {
283
- inFlight.delete(modelId);
284
- }
285
- }