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
@@ -0,0 +1,158 @@
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
+ }
@@ -1,4 +1,4 @@
1
- import { takeScopeFlags } from "./memory.js";
1
+ import { takeScopeFlags, scopeLabel } from "./scope.js";
2
2
  import { getSandboxSnapshot, setSandboxConfig } from "./config.js";
3
3
  import { parseNetworkPolicy, parseSandboxMode, getEffectiveSandboxPolicy, sandboxEnforcementCaveat, sandboxModeLabel, sandboxStatusLabel, setSandboxOverride, } from "./sandbox.js";
4
4
  export const SANDBOX_CLI_USAGE = [
@@ -17,9 +17,6 @@ function sourceLabel(source) {
17
17
  ? "global"
18
18
  : "default";
19
19
  }
20
- function scopeLabel(scope) {
21
- return scope === "project" ? "project" : "global";
22
- }
23
20
  export function runSandboxCli(input) {
24
21
  const { scope: posScope, rest } = takeScopeFlags(input.positionals);
25
22
  const scope = posScope ?? input.scope ?? "global";
package/dist/scope.js ADDED
@@ -0,0 +1,23 @@
1
+ export function readScope(value) {
2
+ if (value === undefined || value === null || value === "")
3
+ return { ok: true, scope: undefined };
4
+ if (value === "global" || value === "project")
5
+ return { ok: true, scope: value };
6
+ return { ok: false };
7
+ }
8
+ export function takeScopeFlags(tokens) {
9
+ let scope;
10
+ const rest = [];
11
+ for (const t of tokens) {
12
+ if (t === "--project")
13
+ scope = "project";
14
+ else if (t === "--global")
15
+ scope = "global";
16
+ else
17
+ rest.push(t);
18
+ }
19
+ return { scope, rest };
20
+ }
21
+ export function scopeLabel(scope) {
22
+ return scope === "project" ? "project" : "global";
23
+ }
@@ -1,7 +1,9 @@
1
1
  import { randomUUID } from "crypto";
2
- import { getSandboxSnapshot, getPermissionSnapshot, permissionModeLabel } from "../config.js";
2
+ import { getSandboxSnapshot, getPermissionSnapshot, permissionModeLabel, parseThinkingEffort, parseMemoryMode, } from "../config.js";
3
3
  import { parseNetworkPolicy, parseSandboxMode, runWithSandboxPolicy } from "../sandbox.js";
4
4
  import { runWithConfirmContext, getPermissionOverride } from "../confirm.js";
5
+ import { thinkingPayload } from "../thinking.js";
6
+ import { memoryPayload } from "../memory.js";
5
7
  import { runOnceWithSystem } from "../agent.js";
6
8
  import { saveSession, sanitizeForStorage } from "../sessions.js";
7
9
  import { createSaveThrottle } from "../save-throttle.js";
@@ -266,6 +268,23 @@ export function permissionPayload() {
266
268
  label: permissionModeLabel(permission),
267
269
  };
268
270
  }
271
+ export { thinkingPayload, memoryPayload };
272
+ export function readThinking(body) {
273
+ if (body.thinking === undefined)
274
+ return { ok: true };
275
+ const thinking = parseThinkingEffort(body.thinking);
276
+ if (!thinking)
277
+ return { ok: false, error: "invalid_thinking", detail: "thinking must be off, low, medium, high, or max" };
278
+ return { ok: true, thinking };
279
+ }
280
+ export function readMemory(body) {
281
+ if (body.memory === undefined)
282
+ return { ok: true };
283
+ const memory = parseMemoryMode(body.memory);
284
+ if (!memory)
285
+ return { ok: false, error: "invalid_memory", detail: "memory must be on or off" };
286
+ return { ok: true, memory };
287
+ }
269
288
  export function normalizeMessages(body) {
270
289
  if (body.messages && Array.isArray(body.messages)) {
271
290
  if (body.messages.length === 0)
@@ -282,6 +301,8 @@ export function chatRunOptions(body) {
282
301
  temperature: typeof body.temperature === "number" ? body.temperature : undefined,
283
302
  maxTokens: typeof body.maxTokens === "number" ? body.maxTokens : undefined,
284
303
  topP: typeof body.topP === "number" ? body.topP : undefined,
304
+ thinking: parseThinkingEffort(body.thinking),
305
+ memory: parseMemoryMode(body.memory),
285
306
  providerName: typeof body.provider === "string" ? body.provider : undefined,
286
307
  ...(body.plan_mode === true ? { planMode: true } : {}),
287
308
  };
@@ -5,7 +5,7 @@ import { lastUserText } from "../tui/hydrate.js";
5
5
  import { compactMessages } from "../compaction.js";
6
6
  import { resolveModel } from "../provider.js";
7
7
  import { emptyTaskState } from "../tools/todo.js";
8
- import { MAX_CONCURRENT_CHATS, sendJson, sendJsonError, readJsonBody, readSandboxTighten, normalizeMessages, chatRunOptions, executeChat, } from "./common.js";
8
+ import { MAX_CONCURRENT_CHATS, sendJson, sendJsonError, readJsonBody, readSandboxTighten, readThinking, readMemory, normalizeMessages, chatRunOptions, executeChat, } from "./common.js";
9
9
  /** Shared handler for /v1/chat and /v1/code (streaming + JSON). */
10
10
  async function handleChatRequest(ctx, req, res) {
11
11
  if (req.headers["content-type"]?.split(";")[0]?.trim() !== "application/json") {
@@ -33,6 +33,16 @@ async function handleChatRequest(ctx, req, res) {
33
33
  return;
34
34
  }
35
35
  const sandboxTighten = sandboxParsed.policy;
36
+ const thinkingParsed = readThinking(body);
37
+ if (!thinkingParsed.ok) {
38
+ sendJson(res, 400, { error: thinkingParsed.error, detail: thinkingParsed.detail });
39
+ return;
40
+ }
41
+ const memoryParsed = readMemory(body);
42
+ if (!memoryParsed.ok) {
43
+ sendJson(res, 400, { error: memoryParsed.error, detail: memoryParsed.detail });
44
+ return;
45
+ }
36
46
  const modelId = typeof body.model === "string" ? body.model : undefined;
37
47
  const stream = body.stream === true;
38
48
  const sessionId = typeof body.session_id === "string" ? body.session_id : undefined;
@@ -132,6 +142,11 @@ export const handleChatRoutes = async (req, res, ctx, _url, pathname) => {
132
142
  sendJson(res, 400, { error: sandboxParsed.error, detail: sandboxParsed.detail });
133
143
  return true;
134
144
  }
145
+ const thinkingParsed = readThinking(body);
146
+ if (!thinkingParsed.ok) {
147
+ sendJson(res, 400, { error: thinkingParsed.error, detail: thinkingParsed.detail });
148
+ return true;
149
+ }
135
150
  if (ctx.activeChats >= MAX_CONCURRENT_CHATS) {
136
151
  sendJson(res, 429, {
137
152
  error: "too_many_concurrent_requests",
@@ -242,6 +257,11 @@ export const handleChatRoutes = async (req, res, ctx, _url, pathname) => {
242
257
  sendJson(res, 400, { error: sandboxParsed.error, detail: sandboxParsed.detail });
243
258
  return true;
244
259
  }
260
+ const thinkingParsed = readThinking(body);
261
+ if (!thinkingParsed.ok) {
262
+ sendJson(res, 400, { error: thinkingParsed.error, detail: thinkingParsed.detail });
263
+ return true;
264
+ }
245
265
  const imageBuffer = Buffer.from(body.image_base64, "base64");
246
266
  const mimeType = body.mime_type ?? "image/png";
247
267
  const text = body.message ?? "What's in this image?";
@@ -1,8 +1,37 @@
1
- import { loadMemories, addMemory, deleteMemory, searchMemories, readMemoryScope } from "../memory.js";
1
+ import { loadMemories, addMemory, deleteMemory, searchMemories, readMemoryScope, parseMemoryMode, setMemoryMode, memoryPayload, } from "../memory.js";
2
2
  import { sendJson, sendJsonError, readJsonBody } from "./common.js";
3
3
  export const handleMemoryRoutes = async (req, res, _ctx, url, pathname) => {
4
+ if (req.method === "GET" && pathname === "/v1/memory/mode") {
5
+ sendJson(res, 200, memoryPayload());
6
+ return true;
7
+ }
8
+ if (req.method === "POST" && pathname === "/v1/memory/mode") {
9
+ const parsed = await readJsonBody(req);
10
+ if (!parsed.ok) {
11
+ sendJsonError(res, parsed);
12
+ return true;
13
+ }
14
+ const mode = parseMemoryMode(parsed.value.memory);
15
+ if (!mode) {
16
+ sendJson(res, 400, { error: "invalid_memory", detail: "memory must be on or off" });
17
+ return true;
18
+ }
19
+ const scopeParsed = readMemoryScope(parsed.value.scope);
20
+ if (!scopeParsed.ok) {
21
+ sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
22
+ return true;
23
+ }
24
+ const scope = scopeParsed.scope ?? "global";
25
+ setMemoryMode(mode, scope);
26
+ sendJson(res, 200, { ok: true, scope, ...memoryPayload() });
27
+ return true;
28
+ }
4
29
  if (req.method === "GET" && pathname === "/v1/memory") {
5
- sendJson(res, 200, { memories: loadMemories("global"), project_memories: loadMemories("project") });
30
+ sendJson(res, 200, {
31
+ ...memoryPayload(),
32
+ memories: loadMemories("global"),
33
+ project_memories: loadMemories("project"),
34
+ });
6
35
  return true;
7
36
  }
8
37
  if (req.method === "POST" && pathname === "/v1/memory") {
@@ -1,12 +1,14 @@
1
- import { fetchModels, getActiveProvider, getEffectiveConfig, getBudgetSnapshot, setBudgetMaxCost, setSandboxConfig, setPermissionMode, parsePermissionMode, } from "../config.js";
1
+ import { fetchModels, getActiveProvider, getEffectiveConfig, getBudgetSnapshot, setBudgetMaxCost, setSandboxConfig, setPermissionMode, parsePermissionMode, parseThinkingEffort, } from "../config.js";
2
+ import { setThinkingEffort } from "../thinking.js";
2
3
  import { parseNetworkPolicy, parseSandboxMode } from "../sandbox.js";
3
- import { readMemoryScope } from "../memory.js";
4
+ import { readScope } from "../scope.js";
4
5
  import { getWorkingTreeDiff } from "../tui/diff-view.js";
5
6
  import { scanProject } from "../code-mode.js";
6
7
  import { getContextWindowInfo } from "../context-window.js";
8
+ import { parseCtxChoice, setOllamaContextChoice, ctxPayload, CTX_CHOICES } from "../ctx.js";
7
9
  import { getModelPrice, estimateCost } from "../pricing.js";
8
10
  import { checkForUpdate } from "../updater.js";
9
- import { sendJson, sendJsonError, readJsonBody, readStringPathList, sandboxPayload, permissionPayload, } from "./common.js";
11
+ import { sendJson, sendJsonError, readJsonBody, readStringPathList, sandboxPayload, permissionPayload, thinkingPayload, memoryPayload, } from "./common.js";
10
12
  export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
11
13
  if (req.method === "GET" && pathname === "/health") {
12
14
  sendJson(res, 200, { ok: true, service: "min-agent", version: ctx.version });
@@ -19,6 +21,8 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
19
21
  instructions_chars: ctx.instructions.join("\n").length,
20
22
  sandbox: sandboxPayload(),
21
23
  permission: permissionPayload(),
24
+ thinking: thinkingPayload(),
25
+ memory: memoryPayload(),
22
26
  });
23
27
  return true;
24
28
  }
@@ -64,7 +68,7 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
64
68
  sendJson(res, 400, { error: "invalid_max_cost", detail: "maxCostUSD must be a positive number" });
65
69
  return true;
66
70
  }
67
- const scopeParsed = readMemoryScope(parsed.value.scope);
71
+ const scopeParsed = readScope(parsed.value.scope);
68
72
  if (!scopeParsed.ok) {
69
73
  sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
70
74
  return true;
@@ -94,7 +98,7 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
94
98
  sendJson(res, 400, { error: "invalid_permission", detail: "permission must be ask, accept-edits, or allow-all" });
95
99
  return true;
96
100
  }
97
- const scopeParsed = readMemoryScope(parsed.value.scope);
101
+ const scopeParsed = readScope(parsed.value.scope);
98
102
  if (!scopeParsed.ok) {
99
103
  sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
100
104
  return true;
@@ -104,6 +108,31 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
104
108
  sendJson(res, 200, { ok: true, scope, ...permissionPayload() });
105
109
  return true;
106
110
  }
111
+ if (req.method === "GET" && pathname === "/v1/thinking") {
112
+ sendJson(res, 200, thinkingPayload());
113
+ return true;
114
+ }
115
+ if (req.method === "POST" && pathname === "/v1/thinking") {
116
+ const parsed = await readJsonBody(req);
117
+ if (!parsed.ok) {
118
+ sendJsonError(res, parsed);
119
+ return true;
120
+ }
121
+ const effort = parseThinkingEffort(parsed.value.thinking);
122
+ if (!effort) {
123
+ sendJson(res, 400, { error: "invalid_thinking", detail: "thinking must be off, low, medium, high, or max" });
124
+ return true;
125
+ }
126
+ const scopeParsed = readScope(parsed.value.scope);
127
+ if (!scopeParsed.ok) {
128
+ sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
129
+ return true;
130
+ }
131
+ const scope = scopeParsed.scope ?? "global";
132
+ setThinkingEffort(effort, scope);
133
+ sendJson(res, 200, { ok: true, scope, ...thinkingPayload() });
134
+ return true;
135
+ }
107
136
  if (req.method === "POST" && pathname === "/v1/sandbox") {
108
137
  const parsed = await readJsonBody(req);
109
138
  if (!parsed.ok) {
@@ -134,7 +163,7 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
134
163
  sendJson(res, 400, { error: "invalid_body", detail: "Provide mode, network, extraWriteRoots, or extraReadRoots" });
135
164
  return true;
136
165
  }
137
- const scopeParsed = readMemoryScope(body.scope);
166
+ const scopeParsed = readScope(body.scope);
138
167
  if (!scopeParsed.ok) {
139
168
  sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
140
169
  return true;
@@ -181,6 +210,40 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
181
210
  context_window: info.tokens,
182
211
  source: info.source,
183
212
  model: provider?.defaultModel ?? null,
213
+ ...ctxPayload(),
214
+ });
215
+ return true;
216
+ }
217
+ if (req.method === "POST" && pathname === "/v1/context") {
218
+ const parsed = await readJsonBody(req);
219
+ if (!parsed.ok) {
220
+ sendJsonError(res, parsed);
221
+ return true;
222
+ }
223
+ const choice = typeof parsed.value.level === "string" ? parseCtxChoice(parsed.value.level) : null;
224
+ if (!choice) {
225
+ sendJson(res, 400, {
226
+ error: "invalid_context_level",
227
+ detail: `level must be one of: ${CTX_CHOICES.join(", ")}`,
228
+ });
229
+ return true;
230
+ }
231
+ const result = setOllamaContextChoice(choice);
232
+ if (!result.ok) {
233
+ sendJson(res, 400, {
234
+ error: "ctx_not_supported",
235
+ detail: "context levels are only available for an Ollama provider",
236
+ });
237
+ return true;
238
+ }
239
+ const provider = getActiveProvider(getEffectiveConfig());
240
+ const info = await getContextWindowInfo(provider?.defaultModel);
241
+ sendJson(res, 200, {
242
+ ok: true,
243
+ context_window: info.tokens,
244
+ source: info.source,
245
+ model: provider?.defaultModel ?? null,
246
+ ...ctxPayload(),
184
247
  });
185
248
  return true;
186
249
  }
@@ -0,0 +1,36 @@
1
+ import { takeScopeFlags, scopeLabel } from "./scope.js";
2
+ import { parseThinkingEffort, resolveThinkingEffort, setThinkingEffort, setThinkingOverride, thinkingEffortLabel, } from "./thinking.js";
3
+ export const THINK_CLI_USAGE = [
4
+ "Usage: min-agent think [off|low|medium|high|max] [--project|--global]",
5
+ " min-agent --think off|low|medium|high|max [--project|--global]",
6
+ ].join("\n");
7
+ function sourceLabel(source) {
8
+ return source === "cli" ? "this run" : source === "project" ? "project" : source === "global" ? "global" : "default";
9
+ }
10
+ export function runThinkCli(input) {
11
+ const { scope: posScope, rest } = takeScopeFlags(input.positionals);
12
+ const scope = posScope ?? input.scope ?? "global";
13
+ if (rest[0] === "--help" || rest[0] === "-h") {
14
+ return { ok: true, lines: [THINK_CLI_USAGE] };
15
+ }
16
+ if (rest.length === 1) {
17
+ const parsed = parseThinkingEffort(rest[0]);
18
+ if (!parsed)
19
+ return { ok: false, lines: [THINK_CLI_USAGE] };
20
+ setThinkingEffort(parsed, scope);
21
+ setThinkingOverride(parsed);
22
+ return { ok: true, lines: [`✓ Thinking set to ${thinkingEffortLabel(parsed)} (${scopeLabel(scope)})`] };
23
+ }
24
+ if (rest.length > 1)
25
+ return { ok: false, lines: [THINK_CLI_USAGE] };
26
+ if (input.flagEffort) {
27
+ setThinkingEffort(input.flagEffort, scope);
28
+ setThinkingOverride(input.flagEffort);
29
+ return { ok: true, lines: [`✓ Thinking set to ${thinkingEffortLabel(input.flagEffort)} (${scopeLabel(scope)})`] };
30
+ }
31
+ const { thinking, source } = resolveThinkingEffort();
32
+ return {
33
+ ok: true,
34
+ lines: [`Current thinking: ${thinkingEffortLabel(thinking)} (${sourceLabel(source)})`, THINK_CLI_USAGE],
35
+ };
36
+ }