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.
- package/README.md +46 -2
- package/dist/agent.js +89 -29
- package/dist/cli/commands/chat.js +3 -0
- package/dist/cli/commands/ctx.js +7 -0
- package/dist/cli/commands/exec.js +3 -0
- package/dist/cli/commands/index.js +32 -7
- package/dist/cli/commands/memory.js +33 -15
- package/dist/cli/commands/setup.js +55 -3
- package/dist/cli/commands/shared.js +10 -1
- package/dist/cli/commands/think.js +12 -0
- package/dist/cli/commands/write-config.js +22 -0
- package/dist/cli/option-helpers.js +13 -1
- package/dist/cli/program.js +57 -14
- package/dist/cli/setup/detect.js +17 -0
- package/dist/cli/setup/flags.js +12 -0
- package/dist/cli/setup/flow.js +108 -0
- package/dist/cli/setup/provider-form.js +102 -0
- package/dist/cli/setup/ui.js +534 -0
- package/dist/code-mode.js +1 -1
- package/dist/config.js +93 -159
- package/dist/context-window.js +39 -49
- package/dist/ctx-cli.js +30 -0
- package/dist/ctx.js +80 -0
- package/dist/memory-cli.js +33 -0
- package/dist/memory.js +127 -46
- package/dist/model-catalog.js +285 -0
- package/dist/ollama-model.js +234 -0
- package/dist/ollama-openai-bridge.js +383 -0
- package/dist/permission-cli.js +1 -4
- package/dist/provider.js +4 -1
- package/dist/reasoning-stream.js +158 -0
- package/dist/sandbox-cli.js +1 -4
- package/dist/scope.js +23 -0
- package/dist/serve/common.js +22 -1
- package/dist/serve/routes-chat.js +21 -1
- package/dist/serve/routes-memory.js +31 -2
- package/dist/serve/routes-meta.js +69 -6
- package/dist/think-cli.js +36 -0
- package/dist/thinking-wire.js +239 -0
- package/dist/thinking.js +166 -0
- package/dist/token-display.js +10 -7
- package/dist/tools/todo.js +22 -8
- package/dist/tui/App.js +48 -8
- package/dist/tui/CtxPicker.js +68 -0
- package/dist/tui/InputBar.js +112 -37
- package/dist/tui/MessageList.js +53 -22
- package/dist/tui/StatusBar.js +7 -3
- package/dist/tui/ThinkPicker.js +75 -0
- package/dist/tui/bracketed-paste.js +37 -0
- package/dist/tui/caret-pos.js +10 -8
- package/dist/tui/index.js +13 -1
- package/dist/tui/layout.js +17 -0
- package/dist/tui/overlay-input.js +12 -0
- package/dist/tui/paste-draft.js +173 -0
- package/dist/tui/selection.js +8 -2
- package/dist/tui/slash-commands.js +24 -1
- package/dist/tui/slash-handler.js +88 -18
- package/dist/tui/text-width.js +6 -6
- package/dist/tui-chat.js +85 -7
- package/docs/API.md +69 -6
- package/docs/superpowers/plans/2026-08-23-cli-setup.md +501 -0
- package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +475 -0
- package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +450 -0
- package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +282 -0
- package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +174 -0
- package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +140 -0
- package/package.json +1 -1
- package/skills/self-config/SKILL.md +7 -4
- 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 =
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
332
|
+
atomicWriteFileSync(modelsCachePath(), JSON.stringify(map));
|
|
280
333
|
}
|
|
281
|
-
export async function
|
|
334
|
+
export async function fetchModelsLive(baseURL, apiKey) {
|
|
282
335
|
try {
|
|
283
|
-
const trimmed = baseURL
|
|
284
|
-
let
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
362
|
+
return cached;
|
|
429
363
|
}
|
package/dist/context-window.js
CHANGED
|
@@ -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.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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;
|
package/dist/ctx-cli.js
ADDED
|
@@ -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
|
+
}
|