min-agent 0.4.1 → 0.5.1

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 (69) hide show
  1. package/README.md +46 -2
  2. package/dist/agent.js +89 -29
  3. package/dist/cli/commands/chat.js +3 -0
  4. package/dist/cli/commands/ctx.js +7 -0
  5. package/dist/cli/commands/exec.js +3 -0
  6. package/dist/cli/commands/index.js +32 -7
  7. package/dist/cli/commands/memory.js +33 -15
  8. package/dist/cli/commands/setup.js +55 -3
  9. package/dist/cli/commands/shared.js +10 -1
  10. package/dist/cli/commands/think.js +12 -0
  11. package/dist/cli/commands/write-config.js +22 -0
  12. package/dist/cli/option-helpers.js +13 -1
  13. package/dist/cli/program.js +57 -14
  14. package/dist/cli/setup/detect.js +17 -0
  15. package/dist/cli/setup/flags.js +12 -0
  16. package/dist/cli/setup/flow.js +108 -0
  17. package/dist/cli/setup/provider-form.js +102 -0
  18. package/dist/cli/setup/ui.js +534 -0
  19. package/dist/code-mode.js +1 -1
  20. package/dist/config.js +93 -159
  21. package/dist/context-window.js +39 -49
  22. package/dist/ctx-cli.js +30 -0
  23. package/dist/ctx.js +80 -0
  24. package/dist/memory-cli.js +33 -0
  25. package/dist/memory.js +127 -46
  26. package/dist/model-catalog.js +285 -0
  27. package/dist/ollama-model.js +234 -0
  28. package/dist/ollama-openai-bridge.js +383 -0
  29. package/dist/permission-cli.js +1 -4
  30. package/dist/provider.js +4 -1
  31. package/dist/reasoning-stream.js +158 -0
  32. package/dist/sandbox-cli.js +1 -4
  33. package/dist/scope.js +23 -0
  34. package/dist/serve/common.js +22 -1
  35. package/dist/serve/routes-chat.js +21 -1
  36. package/dist/serve/routes-memory.js +31 -2
  37. package/dist/serve/routes-meta.js +69 -6
  38. package/dist/think-cli.js +36 -0
  39. package/dist/thinking-wire.js +239 -0
  40. package/dist/thinking.js +166 -0
  41. package/dist/token-display.js +10 -7
  42. package/dist/tools/todo.js +22 -8
  43. package/dist/tui/App.js +48 -8
  44. package/dist/tui/CtxPicker.js +68 -0
  45. package/dist/tui/InputBar.js +112 -37
  46. package/dist/tui/MessageList.js +53 -22
  47. package/dist/tui/StatusBar.js +7 -3
  48. package/dist/tui/ThinkPicker.js +75 -0
  49. package/dist/tui/bracketed-paste.js +37 -0
  50. package/dist/tui/caret-pos.js +10 -8
  51. package/dist/tui/index.js +13 -1
  52. package/dist/tui/layout.js +17 -0
  53. package/dist/tui/overlay-input.js +12 -0
  54. package/dist/tui/paste-draft.js +173 -0
  55. package/dist/tui/selection.js +8 -2
  56. package/dist/tui/slash-commands.js +24 -1
  57. package/dist/tui/slash-handler.js +88 -18
  58. package/dist/tui/text-width.js +6 -6
  59. package/dist/tui-chat.js +85 -7
  60. package/docs/API.md +69 -6
  61. package/docs/superpowers/plans/2026-08-23-cli-setup.md +501 -0
  62. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +475 -0
  63. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +450 -0
  64. package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +282 -0
  65. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +174 -0
  66. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +140 -0
  67. package/package.json +1 -1
  68. package/skills/self-config/SKILL.md +7 -4
  69. package/skills/self-config/reference.md +12 -6
package/dist/config.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { readFileSync, mkdirSync, existsSync, statSync } from "fs";
2
2
  import path from "path";
3
3
  import os from "os";
4
- import readline from "readline";
5
4
  import { atomicWriteFileSync } from "./tools/atomic-file.js";
6
5
  import { getEffectiveSandboxPolicy, mergeSandboxConfig, parseSandboxConfig, sandboxLaunchSource, sandboxStatusLabel, } from "./sandbox.js";
7
6
  const DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".min-agent");
@@ -107,6 +106,8 @@ export function mergeProjectOverGlobal(global, project) {
107
106
  const sampling = project.sampling ? { ...global.sampling, ...project.sampling } : global.sampling;
108
107
  const budget = project.budget ? { ...global.budget, ...project.budget } : global.budget;
109
108
  const permission = parsePermissionMode(project.permission) ?? parsePermissionMode(global.permission);
109
+ const thinking = parseThinkingEffort(project.thinking) ?? parseThinkingEffort(global.thinking);
110
+ const memory = parseMemoryMode(project.memory) ?? parseMemoryMode(global.memory);
110
111
  const sandbox = mergeSandboxConfig(parseSandboxConfig(global.sandbox), parseSandboxConfig(project.sandbox));
111
112
  const compaction = project.compaction ? { ...global.compaction, ...project.compaction } : global.compaction;
112
113
  const agent = project.agent ? { ...global.agent, ...project.agent } : global.agent;
@@ -122,6 +123,8 @@ export function mergeProjectOverGlobal(global, project) {
122
123
  sampling,
123
124
  budget,
124
125
  permission,
126
+ ...(thinking ? { thinking } : {}),
127
+ ...(memory ? { memory } : {}),
125
128
  ...(sandbox ? { sandbox } : {}),
126
129
  compaction,
127
130
  agent,
@@ -143,6 +146,43 @@ export function parsePermissionMode(value) {
143
146
  return value;
144
147
  return undefined;
145
148
  }
149
+ const THINKING_ALIASES = {
150
+ off: "off",
151
+ none: "off",
152
+ low: "low",
153
+ minimal: "low",
154
+ medium: "medium",
155
+ high: "high",
156
+ max: "max",
157
+ xhigh: "max",
158
+ "extra-high": "max",
159
+ extra_high: "max",
160
+ extrahigh: "max",
161
+ };
162
+ export function parseThinkingEffort(value) {
163
+ if (typeof value !== "string")
164
+ return undefined;
165
+ return THINKING_ALIASES[value.trim().toLowerCase()];
166
+ }
167
+ const MEMORY_ALIASES = {
168
+ on: "on",
169
+ true: "on",
170
+ enable: "on",
171
+ enabled: "on",
172
+ off: "off",
173
+ false: "off",
174
+ disable: "off",
175
+ disabled: "off",
176
+ };
177
+ export function parseMemoryMode(value) {
178
+ if (value === true)
179
+ return "on";
180
+ if (value === false)
181
+ return "off";
182
+ if (typeof value !== "string")
183
+ return undefined;
184
+ return MEMORY_ALIASES[value.trim().toLowerCase()];
185
+ }
146
186
  export function permissionModeLabel(mode) {
147
187
  if (mode === "allow-all")
148
188
  return "allow all";
@@ -246,184 +286,78 @@ async function fetchModelsFromURL(url, apiKey) {
246
286
  signal: AbortSignal.timeout(10000),
247
287
  });
248
288
  if (!response.ok)
249
- return [];
250
- const data = (await response.json());
251
- const models = (data.data ?? data ?? []);
252
- return models.map((m) => m.id).sort();
253
- }
254
- function ask(rl, question, defaultValue) {
255
- const suffix = defaultValue ? ` (${defaultValue})` : "";
256
- return new Promise((resolve) => {
257
- rl.question(`${question}${suffix}: `, (answer) => {
258
- resolve(answer.trim() || defaultValue || "");
259
- });
260
- });
289
+ return { models: [], ok: false, status: response.status };
290
+ const data = await response.json();
291
+ return { models: parseModelIds(data), ok: true, status: response.status };
292
+ }
293
+ function parseModelIds(data) {
294
+ const list = Array.isArray(data)
295
+ ? data
296
+ : data !== null && typeof data === "object" && "data" in data && Array.isArray(data.data)
297
+ ? data.data
298
+ : [];
299
+ return list
300
+ .map((item) => item !== null && typeof item === "object" && "id" in item && typeof item.id === "string" ? item.id : null)
301
+ .filter((id) => id != null)
302
+ .sort();
261
303
  }
262
304
  const MODELS_CACHE_FILE = "models-cache.json";
263
305
  function modelsCachePath() {
264
306
  return path.join(getConfigDir(), MODELS_CACHE_FILE);
265
307
  }
266
- function loadModelsCache() {
308
+ function modelsCacheKey(baseURL) {
309
+ return baseURL.replace(/\/$/, "");
310
+ }
311
+ function loadModelsCacheMap() {
267
312
  const file = modelsCachePath();
268
313
  if (!existsSync(file))
269
- return [];
314
+ return {};
270
315
  try {
271
- return JSON.parse(readFileSync(file, "utf-8"));
316
+ const parsed = JSON.parse(readFileSync(file, "utf-8"));
317
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
318
+ return {};
319
+ return Object.fromEntries(Object.entries(parsed).filter((entry) => Array.isArray(entry[1]) && entry[1].every((item) => typeof item === "string")));
272
320
  }
273
321
  catch {
274
- return [];
322
+ return {};
275
323
  }
276
324
  }
277
- function saveModelsCache(models) {
325
+ function loadModelsCacheBucket(baseURL) {
326
+ return loadModelsCacheMap()[modelsCacheKey(baseURL)] ?? [];
327
+ }
328
+ function saveModelsCacheBucket(baseURL, models) {
329
+ const map = loadModelsCacheMap();
330
+ map[modelsCacheKey(baseURL)] = models;
278
331
  mkdirSync(getConfigDir(), { recursive: true });
279
- atomicWriteFileSync(modelsCachePath(), JSON.stringify(models));
332
+ atomicWriteFileSync(modelsCachePath(), JSON.stringify(map));
280
333
  }
281
- export async function fetchModels(baseURL, apiKey) {
334
+ export async function fetchModelsLive(baseURL, apiKey) {
282
335
  try {
283
- const trimmed = baseURL.replace(/\/$/, "");
284
- let models = await fetchModelsFromURL(`${trimmed}/models`, apiKey);
285
- // Ollama users often provide host without /v1; auto-retry that variant.
286
- if (models.length === 0 && !trimmed.endsWith("/v1")) {
287
- models = await fetchModelsFromURL(`${trimmed}/v1/models`, apiKey);
336
+ const trimmed = modelsCacheKey(baseURL);
337
+ let result = await fetchModelsFromURL(`${trimmed}/models`, apiKey);
338
+ if (result.models.length === 0 && !trimmed.endsWith("/v1")) {
339
+ result = await fetchModelsFromURL(`${trimmed}/v1/models`, apiKey);
288
340
  }
289
- if (models.length > 0) {
290
- saveModelsCache(models);
291
- return models;
341
+ if (result.ok && result.models.length > 0) {
342
+ saveModelsCacheBucket(trimmed, result.models);
343
+ return { models: result.models, ok: true, status: result.status };
292
344
  }
293
- // Fallback to cache if live fetch returned nothing
294
- const cached = loadModelsCache();
295
- if (cached.length > 0) {
296
- console.log("\x1b[90m (using cached model list)\x1b[0m");
297
- }
298
- return cached;
345
+ if (!result.ok)
346
+ return { models: [], ok: false, status: result.status };
347
+ return { models: [], ok: true, status: result.status };
299
348
  }
300
349
  catch {
301
- // Network error fallback to cache
302
- const cached = loadModelsCache();
303
- if (cached.length > 0) {
304
- console.log("\x1b[90m (using cached model list — network unavailable)\x1b[0m");
305
- }
306
- return cached;
350
+ return { models: [], ok: false };
307
351
  }
308
352
  }
309
- export async function runSetup() {
310
- const config = loadConfig();
311
- const providers = config.providers ?? [];
312
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
313
- console.log("\n🔧 min-agent 配置\n");
314
- if (providers.length > 0) {
315
- console.log("已配置的 Provider:");
316
- providers.forEach((p, i) => {
317
- const marker = p.name === config.activeProvider ? " ← 当前" : "";
318
- console.log(` ${i + 1}. ${p.name ?? "(未命名)"} ${p.baseURL} (${p.defaultModel ?? "未设置模型"})${marker}`);
319
- });
320
- console.log();
321
- console.log("选项:");
322
- console.log(" 1. 添加新 Provider");
323
- console.log(" 2. 切换当前 Provider");
324
- console.log(" 3. 修改当前 Provider");
325
- console.log(" 4. 完成");
326
- const choice = await ask(rl, "选择 (1/2/3/4)", "4");
327
- if (choice === "1") {
328
- const provider = await collectProvider(rl, undefined);
329
- provider.name =
330
- (await ask(rl, "Provider 名称", `provider-${providers.length + 1}`)).trim() ||
331
- `provider-${providers.length + 1}`;
332
- providers.push(provider);
333
- config.activeProvider = provider.name;
334
- config.providers = providers;
335
- saveConfig(config);
336
- console.log(`\n✓ Provider "${provider.name}" 已添加并设为当前`);
337
- }
338
- else if (choice === "2") {
339
- const sel = await ask(rl, "输入序号选择当前 Provider");
340
- const idx = parseInt(sel, 10) - 1;
341
- const target = providers[idx];
342
- if (!target?.name) {
343
- console.error("无效序号");
344
- }
345
- else {
346
- config.activeProvider = target.name;
347
- saveConfig(config);
348
- console.log(`\n✓ 当前 Provider 已切换为: ${target.name}`);
349
- }
350
- }
351
- else if (choice === "3") {
352
- const name = getActiveProvider(config)?.name ?? providers[0]?.name;
353
- if (!name) {
354
- console.error("无可用 Provider");
355
- rl.close();
356
- return;
357
- }
358
- const idx = providers.findIndex((p) => p.name === name);
359
- providers[idx] = { ...(await collectProvider(rl, providers[idx])), name };
360
- config.providers = providers;
361
- saveConfig(config);
362
- console.log(`\n✓ Provider "${name}" 已更新`);
363
- }
364
- else {
365
- console.log("\n配置未变更");
366
- }
367
- rl.close();
368
- return;
369
- }
370
- // 首次配置
371
- const provider = await collectProvider(rl, undefined);
372
- provider.name = "default";
373
- config.providers = [provider];
374
- config.activeProvider = "default";
375
- saveConfig(config);
376
- rl.close();
377
- console.log(`\n✓ 配置已保存到 ${path.join(getConfigDir(), CONFIG_FILE_NAME)}`);
378
- }
379
- async function collectProvider(rl, existing) {
380
- console.log("Provider 类型:");
381
- console.log(" 1. openai-compatible (默认,兼容 OpenAI API 的任意服务)");
382
- console.log(" 2. openai (OpenAI 官方)");
383
- console.log(" 3. ollama (本地 Ollama)");
384
- console.log();
385
- const typeChoice = await ask(rl, "选择 Provider (1/2/3)", existing?.type === "ollama" ? "3" : existing?.type === "openai" ? "2" : "1");
386
- const providerType = typeChoice === "3" ? "ollama" : typeChoice === "2" ? "openai" : "openai-compatible";
387
- let baseURL;
388
- let apiKey;
389
- if (providerType === "ollama") {
390
- baseURL = await ask(rl, "Ollama API URL", existing?.baseURL || "http://localhost:11434/v1");
391
- baseURL = normalizeOllamaBaseURL(baseURL);
392
- apiKey = "ollama";
393
- }
394
- else if (providerType === "openai") {
395
- baseURL = "https://api.openai.com/v1";
396
- apiKey = await ask(rl, "OpenAI API Key", existing?.apiKey);
397
- }
398
- else {
399
- baseURL = await ask(rl, "API Base URL", existing?.baseURL || "https://api.openai.com/v1");
400
- apiKey = await ask(rl, "API Key", existing?.apiKey);
401
- }
402
- if (!baseURL || (!apiKey && providerType !== "ollama")) {
403
- console.error("URL 和 Key 不能为空");
404
- process.exit(1);
405
- }
406
- console.log("\n正在获取模型列表...");
407
- const models = await fetchModels(baseURL, apiKey);
408
- let defaultModel = existing?.defaultModel ?? "";
409
- if (models.length > 0) {
410
- console.log(`\n可用模型 (${models.length}):`);
411
- models.forEach((m, i) => {
412
- const marker = m === defaultModel ? " ← 当前默认" : "";
413
- console.log(` ${i + 1}. ${m}${marker}`);
414
- });
415
- console.log();
416
- const choice = await ask(rl, "选择默认模型 (输入序号或模型名)", defaultModel);
417
- const idx = parseInt(choice, 10) - 1;
418
- if (idx >= 0 && idx < models.length)
419
- defaultModel = models[idx];
420
- else if (choice)
421
- defaultModel = choice;
422
- }
423
- else {
424
- console.log(" ⚠ 无法获取模型列表,请手动输入模型名");
425
- const hint = providerType === "ollama" ? "llama3" : providerType === "openai" ? "gpt-4o" : "";
426
- defaultModel = await ask(rl, "默认模型", defaultModel || hint);
353
+ export async function fetchModels(baseURL, apiKey) {
354
+ const live = await fetchModelsLive(baseURL, apiKey);
355
+ if (live.models.length > 0)
356
+ return live.models;
357
+ const cached = loadModelsCacheBucket(baseURL);
358
+ if (cached.length > 0) {
359
+ const suffix = live.status == null && !live.ok ? " — network unavailable" : "";
360
+ console.log(`\x1b[90m (using cached model list${suffix})\x1b[0m`);
427
361
  }
428
- return { type: providerType, baseURL, apiKey, defaultModel };
362
+ return cached;
429
363
  }
@@ -2,14 +2,18 @@ import { readFileSync, mkdirSync, existsSync } from "fs";
2
2
  import path from "path";
3
3
  import { atomicWriteFileSync } from "./tools/atomic-file.js";
4
4
  import { getConfigDir, getEffectiveConfig, getActiveProvider } from "./config.js";
5
+ import { getCachedModelCatalog, getModelCatalog } from "./model-catalog.js";
6
+ import { getCachedOllamaModel, getOllamaModel, OLLAMA_AGENT_CONTEXT_CAP, OLLAMA_DEFAULT_CONTEXT_WINDOW, } from "./ollama-model.js";
5
7
  /**
6
8
  * Auto-detect context window size for the current model.
7
9
  *
8
10
  * Resolution order:
9
11
  * 1. User config: provider.contextWindow (explicit override)
10
- * 2. Provider-specific API (OpenRouter, vLLM, Ollama, OpenAI-compatible /models)
11
- * 3. models.dev API lookup
12
- * 4. Fallback: 512000 (memory only never persisted as a detected value)
12
+ * 2. Ollama providers (`type: "ollama"`): native POST /api/show operational window
13
+ * (Modelfile num_ctx, else architecture length capped at 32k) — never Lonae
14
+ * 3. Provider-specific API (OpenRouter, vLLM, hostname-based Ollama, OpenAI-compatible /models)
15
+ * 4. Lonae model catalog (context + thinking options)
16
+ * 5. Fallback: 512000 (memory only — never persisted as a detected value)
13
17
  */
14
18
  export const DEFAULT_CONTEXT_WINDOW = 512000;
15
19
  const CACHE_TTL = 7 * 24 * 60 * 60 * 1000;
@@ -39,6 +43,9 @@ function cacheTtl(source) {
39
43
  return source === "fallback" ? FALLBACK_MEMORY_TTL : CACHE_TTL;
40
44
  }
41
45
  function getCached(modelId) {
46
+ const catalog = getCachedModelCatalog(modelId);
47
+ if (catalog?.contextWindow)
48
+ return { tokens: catalog.contextWindow, source: "detected" };
42
49
  const memory = memoryCache.get(modelId);
43
50
  if (memory && Date.now() - memory.timestamp <= cacheTtl(memory.source)) {
44
51
  return { tokens: memory.contextWindow, source: memory.source };
@@ -160,51 +167,23 @@ async function tryProviderModels(baseURL, apiKey, modelId) {
160
167
  async function tryOllama(baseURL, modelId) {
161
168
  if (!baseURL.includes("localhost") && !baseURL.includes("127.0.0.1") && !baseURL.includes("ollama"))
162
169
  return null;
163
- try {
164
- // Ollama's /api/show endpoint
165
- const ollamaBase = baseURL.replace(/\/v1\/?$/, "");
166
- const response = await fetch(`${ollamaBase}/api/show`, {
167
- method: "POST",
168
- headers: { "Content-Type": "application/json" },
169
- body: JSON.stringify({ name: modelId }),
170
- signal: AbortSignal.timeout(5000),
171
- });
172
- if (!response.ok)
173
- return null;
174
- const data = (await response.json());
175
- // Ollama returns model_info with context length
176
- const ctxLength = data.model_info?.["general.context_length"] ?? data.model_info?.context_length ?? data.parameters?.num_ctx;
177
- return typeof ctxLength === "number" ? ctxLength : null;
178
- }
179
- catch {
180
- return null;
181
- }
170
+ const info = await getOllamaModel(modelId, baseURL);
171
+ return info?.contextWindow ?? null;
182
172
  }
183
- /** Try models.dev API */
184
- async function tryModelsDev(modelId) {
185
- try {
186
- const response = await fetch("https://models.dev/api.json", {
187
- signal: AbortSignal.timeout(10000),
188
- });
189
- if (!response.ok)
190
- return null;
191
- const providers = (await response.json());
192
- // Single pass: exact match or partial match (some providers prefix model IDs)
193
- for (const provider of Object.values(providers)) {
194
- if (!provider.models)
195
- continue;
196
- for (const [id, model] of Object.entries(provider.models)) {
197
- if (id === modelId || id.endsWith(`/${modelId}`) || modelId.endsWith(`/${id}`)) {
198
- if (model?.limit?.context)
199
- return model.limit.context;
200
- }
201
- }
202
- }
203
- return null;
173
+ async function detectOllamaWindow(baseURL, id) {
174
+ const shown = await getOllamaModel(id, baseURL);
175
+ if (shown?.contextWindow) {
176
+ setCache(id, shown.contextWindow, "detected");
177
+ return { tokens: shown.contextWindow, source: "detected" };
204
178
  }
205
- catch {
206
- return null;
179
+ const listed = await tryProviderModels(baseURL, "ollama", id);
180
+ if (listed) {
181
+ const tokens = Math.min(listed, OLLAMA_AGENT_CONTEXT_CAP);
182
+ setCache(id, tokens, "detected");
183
+ return { tokens, source: "detected" };
207
184
  }
185
+ setCache(id, OLLAMA_DEFAULT_CONTEXT_WINDOW, "fallback");
186
+ return { tokens: OLLAMA_DEFAULT_CONTEXT_WINDOW, source: "fallback" };
208
187
  }
209
188
  /**
210
189
  * Detect context window size for a model by probing all sources in parallel.
@@ -214,12 +193,16 @@ async function detectContextWindow(config, id) {
214
193
  const provider = getActiveProvider(config);
215
194
  const baseURL = provider?.baseURL ?? "";
216
195
  const apiKey = provider?.apiKey ?? "";
196
+ if (provider?.type === "ollama")
197
+ return detectOllamaWindow(baseURL, id);
198
+ const catalog = await getModelCatalog(id, baseURL);
199
+ if (catalog?.contextWindow)
200
+ return { tokens: catalog.contextWindow, source: "detected" };
217
201
  const results = await Promise.all([
218
202
  tryOpenRouter(baseURL, apiKey, id),
219
203
  tryOllama(baseURL, id),
220
204
  tryVllm(baseURL, apiKey, id),
221
205
  tryProviderModels(baseURL, apiKey, id),
222
- tryModelsDev(id),
223
206
  ]);
224
207
  const found = results.find((v) => v !== null);
225
208
  if (found) {
@@ -241,9 +224,16 @@ export async function getContextWindowInfo(modelId) {
241
224
  const id = modelId ?? provider?.defaultModel;
242
225
  if (!id)
243
226
  return { tokens: DEFAULT_CONTEXT_WINDOW, source: "fallback" };
244
- const cached = getCached(id);
245
- if (cached)
246
- return cached;
227
+ if (provider?.type === "ollama") {
228
+ const shown = getCachedOllamaModel(id, provider.baseURL);
229
+ if (shown?.contextWindow)
230
+ return { tokens: shown.contextWindow, source: "detected" };
231
+ }
232
+ else {
233
+ const cached = getCached(id);
234
+ if (cached)
235
+ return cached;
236
+ }
247
237
  const pending = inFlight.get(id);
248
238
  if (pending)
249
239
  return pending;
@@ -0,0 +1,30 @@
1
+ import { activeProviderIsOllama, configuredCtxChoice, ctxChoiceLabel, parseCtxChoice, setOllamaContextChoice, } from "./ctx.js";
2
+ export const CTX_CLI_USAGE = [
3
+ "Usage: min-agent ctx [2k|4k|8k|12k|16k|32k|64k|128k|256k|auto]",
4
+ " Context levels are only available for an Ollama provider.",
5
+ ].join("\n");
6
+ export function runCtxCli(input) {
7
+ if (input.positionals[0] === "--help" || input.positionals[0] === "-h") {
8
+ return { ok: true, lines: [CTX_CLI_USAGE] };
9
+ }
10
+ if (!activeProviderIsOllama()) {
11
+ return { ok: false, lines: ["Context levels are only available for an Ollama provider.", CTX_CLI_USAGE] };
12
+ }
13
+ if (input.positionals.length === 0) {
14
+ const current = configuredCtxChoice();
15
+ const label = current.choice === "auto" && current.tokens != null ? `${current.tokens}` : current.choice;
16
+ return { ok: true, lines: [`Current context window: ${label}`, CTX_CLI_USAGE] };
17
+ }
18
+ if (input.positionals.length > 1)
19
+ return { ok: false, lines: [CTX_CLI_USAGE] };
20
+ const parsed = parseCtxChoice(input.positionals[0]);
21
+ if (!parsed)
22
+ return { ok: false, lines: [CTX_CLI_USAGE] };
23
+ const result = setOllamaContextChoice(parsed);
24
+ if (!result.ok)
25
+ return { ok: false, lines: ["Context levels are only available for an Ollama provider.", CTX_CLI_USAGE] };
26
+ return {
27
+ ok: true,
28
+ lines: [`✓ Context window set to ${result.choice === "auto" ? "auto" : ctxChoiceLabel(result.choice)}`],
29
+ };
30
+ }
package/dist/ctx.js ADDED
@@ -0,0 +1,80 @@
1
+ import { getActiveProvider, loadConfig, saveConfig } from "./config.js";
2
+ export const CTX_LEVELS = ["2k", "4k", "8k", "12k", "16k", "32k", "64k", "128k", "256k"];
3
+ export const CTX_AUTO = "auto";
4
+ export const CTX_CHOICES = [...CTX_LEVELS, CTX_AUTO];
5
+ const CTX_TOKENS = {
6
+ "2k": 2048,
7
+ "4k": 4096,
8
+ "8k": 8192,
9
+ "12k": 12288,
10
+ "16k": 16384,
11
+ "32k": 32768,
12
+ "64k": 65536,
13
+ "128k": 131072,
14
+ "256k": 262144,
15
+ };
16
+ const TOKEN_TO_LEVEL = new Map(CTX_LEVELS.map((level) => [CTX_TOKENS[level], level]));
17
+ export function isCtxLevel(value) {
18
+ return CTX_LEVELS.includes(value);
19
+ }
20
+ export function isCtxChoice(value) {
21
+ return value === CTX_AUTO || isCtxLevel(value);
22
+ }
23
+ export function tokensForCtxLevel(level) {
24
+ return CTX_TOKENS[level];
25
+ }
26
+ export function matchCtxLevel(tokens) {
27
+ return TOKEN_TO_LEVEL.get(Math.floor(tokens)) ?? null;
28
+ }
29
+ export function parseCtxChoice(raw) {
30
+ if (raw == null)
31
+ return null;
32
+ const text = raw.trim().toLowerCase();
33
+ if (!text)
34
+ return null;
35
+ if (text === "auto" || text === "default")
36
+ return CTX_AUTO;
37
+ if (isCtxLevel(text))
38
+ return text;
39
+ if (!/^\d+$/.test(text))
40
+ return null;
41
+ return matchCtxLevel(Number(text));
42
+ }
43
+ export function ctxChoiceLabel(choice) {
44
+ return choice === CTX_AUTO ? "自动" : choice;
45
+ }
46
+ export function activeProviderIsOllama() {
47
+ return getActiveProvider(loadConfig())?.type === "ollama";
48
+ }
49
+ export function configuredCtxChoice() {
50
+ const tokens = getActiveProvider(loadConfig())?.contextWindow;
51
+ if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens <= 0)
52
+ return { choice: CTX_AUTO };
53
+ const level = matchCtxLevel(tokens);
54
+ if (level)
55
+ return { choice: level, tokens: tokensForCtxLevel(level) };
56
+ return { choice: CTX_AUTO, tokens: Math.floor(tokens) };
57
+ }
58
+ export function setOllamaContextChoice(choice) {
59
+ const cfg = loadConfig();
60
+ const provider = getActiveProvider(cfg);
61
+ if (!provider)
62
+ return { ok: false, reason: "no-provider" };
63
+ if (provider.type !== "ollama")
64
+ return { ok: false, reason: "not-ollama" };
65
+ if (choice === CTX_AUTO)
66
+ delete provider.contextWindow;
67
+ else
68
+ provider.contextWindow = tokensForCtxLevel(choice);
69
+ saveConfig(cfg);
70
+ return choice === CTX_AUTO ? { ok: true, choice } : { ok: true, choice, tokens: tokensForCtxLevel(choice) };
71
+ }
72
+ export function ctxPayload() {
73
+ const provider = getActiveProvider(loadConfig());
74
+ if (provider?.type !== "ollama")
75
+ return { configurable: false, level: null };
76
+ const tokens = provider.contextWindow;
77
+ if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens <= 0)
78
+ return { configurable: true, level: CTX_AUTO };
79
+ return { configurable: true, level: matchCtxLevel(tokens) };
80
+ }
@@ -0,0 +1,33 @@
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
+ }