min-agent 0.4.0 → 0.5.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 (54) hide show
  1. package/README.md +169 -284
  2. package/dist/agent.js +36 -22
  3. package/dist/cli/commands/chat.js +3 -0
  4. package/dist/cli/commands/exec.js +3 -0
  5. package/dist/cli/commands/index.js +22 -5
  6. package/dist/cli/commands/memory.js +33 -15
  7. package/dist/cli/commands/think.js +12 -0
  8. package/dist/cli/commands/write-config.js +22 -0
  9. package/dist/cli/option-helpers.js +13 -1
  10. package/dist/cli/program.js +50 -13
  11. package/dist/code-mode.js +1 -1
  12. package/dist/config.js +41 -0
  13. package/dist/context-window.js +8 -28
  14. package/dist/memory-cli.js +33 -0
  15. package/dist/memory.js +127 -46
  16. package/dist/model-catalog.js +285 -0
  17. package/dist/permission-cli.js +1 -4
  18. package/dist/provider.js +4 -1
  19. package/dist/reasoning-stream.js +158 -0
  20. package/dist/sandbox-cli.js +1 -4
  21. package/dist/scope.js +23 -0
  22. package/dist/serve/common.js +22 -1
  23. package/dist/serve/routes-chat.js +21 -1
  24. package/dist/serve/routes-memory.js +31 -2
  25. package/dist/serve/routes-meta.js +34 -6
  26. package/dist/think-cli.js +36 -0
  27. package/dist/thinking-wire.js +228 -0
  28. package/dist/thinking.js +142 -0
  29. package/dist/token-display.js +10 -7
  30. package/dist/tools/todo.js +22 -8
  31. package/dist/tui/App.js +36 -8
  32. package/dist/tui/InputBar.js +109 -36
  33. package/dist/tui/MessageList.js +53 -22
  34. package/dist/tui/StatusBar.js +7 -3
  35. package/dist/tui/ThinkPicker.js +77 -0
  36. package/dist/tui/bracketed-paste.js +37 -0
  37. package/dist/tui/caret-pos.js +10 -8
  38. package/dist/tui/index.js +7 -1
  39. package/dist/tui/layout.js +17 -0
  40. package/dist/tui/overlay-input.js +12 -0
  41. package/dist/tui/paste-draft.js +173 -0
  42. package/dist/tui/selection.js +8 -2
  43. package/dist/tui/slash-commands.js +18 -1
  44. package/dist/tui/slash-handler.js +61 -17
  45. package/dist/tui/text-width.js +6 -6
  46. package/dist/tui-chat.js +63 -7
  47. package/docs/API.md +50 -4
  48. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +475 -0
  49. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +450 -0
  50. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +174 -0
  51. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +140 -0
  52. package/package.json +1 -1
  53. package/skills/self-config/SKILL.md +5 -4
  54. package/skills/self-config/reference.md +10 -5
package/dist/provider.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { createOpenAI } from "@ai-sdk/openai";
2
2
  import { getEffectiveConfig, getActiveProvider, normalizeOllamaBaseURL } from "./config.js";
3
3
  import { createTimeoutFetch, DEFAULT_FIRST_BYTE_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, } from "./fetch-timeout.js";
4
+ import { foldReasoningInChatResponse } from "./reasoning-stream.js";
5
+ import { fetchWithThinkingWire } from "./thinking-wire.js";
4
6
  function positiveMs(raw, fallback) {
5
7
  const value = typeof raw === "string" ? Number(raw) : raw;
6
8
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
@@ -10,11 +12,12 @@ function positiveMs(raw, fallback) {
10
12
  /** Timeout/trace-aware fetch shared by every provider client. */
11
13
  export function modelFetch() {
12
14
  const cfg = getEffectiveConfig();
13
- return createTimeoutFetch({
15
+ const inner = createTimeoutFetch({
14
16
  firstByteTimeoutMs: positiveMs(process.env.MIN_AGENT_REQUEST_TIMEOUT_MS ?? cfg.agent?.requestTimeoutMs, DEFAULT_FIRST_BYTE_TIMEOUT_MS),
15
17
  idleTimeoutMs: positiveMs(process.env.MIN_AGENT_STREAM_IDLE_TIMEOUT_MS ?? cfg.agent?.streamIdleTimeoutMs, DEFAULT_STREAM_IDLE_TIMEOUT_MS),
16
18
  trace: process.env.MIN_AGENT_TRACE === "1" || process.env.MIN_AGENT_TRACE === "true",
17
19
  });
20
+ return (input, init) => fetchWithThinkingWire(inner, input, init).then(foldReasoningInChatResponse);
18
21
  }
19
22
  export function resolveModelForProvider(provider, modelId) {
20
23
  if (!provider.baseURL || !provider.apiKey) {
@@ -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,13 @@
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";
7
8
  import { getModelPrice, estimateCost } from "../pricing.js";
8
9
  import { checkForUpdate } from "../updater.js";
9
- import { sendJson, sendJsonError, readJsonBody, readStringPathList, sandboxPayload, permissionPayload, } from "./common.js";
10
+ import { sendJson, sendJsonError, readJsonBody, readStringPathList, sandboxPayload, permissionPayload, thinkingPayload, memoryPayload, } from "./common.js";
10
11
  export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
11
12
  if (req.method === "GET" && pathname === "/health") {
12
13
  sendJson(res, 200, { ok: true, service: "min-agent", version: ctx.version });
@@ -19,6 +20,8 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
19
20
  instructions_chars: ctx.instructions.join("\n").length,
20
21
  sandbox: sandboxPayload(),
21
22
  permission: permissionPayload(),
23
+ thinking: thinkingPayload(),
24
+ memory: memoryPayload(),
22
25
  });
23
26
  return true;
24
27
  }
@@ -64,7 +67,7 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
64
67
  sendJson(res, 400, { error: "invalid_max_cost", detail: "maxCostUSD must be a positive number" });
65
68
  return true;
66
69
  }
67
- const scopeParsed = readMemoryScope(parsed.value.scope);
70
+ const scopeParsed = readScope(parsed.value.scope);
68
71
  if (!scopeParsed.ok) {
69
72
  sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
70
73
  return true;
@@ -94,7 +97,7 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
94
97
  sendJson(res, 400, { error: "invalid_permission", detail: "permission must be ask, accept-edits, or allow-all" });
95
98
  return true;
96
99
  }
97
- const scopeParsed = readMemoryScope(parsed.value.scope);
100
+ const scopeParsed = readScope(parsed.value.scope);
98
101
  if (!scopeParsed.ok) {
99
102
  sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
100
103
  return true;
@@ -104,6 +107,31 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
104
107
  sendJson(res, 200, { ok: true, scope, ...permissionPayload() });
105
108
  return true;
106
109
  }
110
+ if (req.method === "GET" && pathname === "/v1/thinking") {
111
+ sendJson(res, 200, thinkingPayload());
112
+ return true;
113
+ }
114
+ if (req.method === "POST" && pathname === "/v1/thinking") {
115
+ const parsed = await readJsonBody(req);
116
+ if (!parsed.ok) {
117
+ sendJsonError(res, parsed);
118
+ return true;
119
+ }
120
+ const effort = parseThinkingEffort(parsed.value.thinking);
121
+ if (!effort) {
122
+ sendJson(res, 400, { error: "invalid_thinking", detail: "thinking must be off, low, medium, high, or max" });
123
+ return true;
124
+ }
125
+ const scopeParsed = readScope(parsed.value.scope);
126
+ if (!scopeParsed.ok) {
127
+ sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
128
+ return true;
129
+ }
130
+ const scope = scopeParsed.scope ?? "global";
131
+ setThinkingEffort(effort, scope);
132
+ sendJson(res, 200, { ok: true, scope, ...thinkingPayload() });
133
+ return true;
134
+ }
107
135
  if (req.method === "POST" && pathname === "/v1/sandbox") {
108
136
  const parsed = await readJsonBody(req);
109
137
  if (!parsed.ok) {
@@ -134,7 +162,7 @@ export const handleMetaRoutes = async (req, res, ctx, url, pathname) => {
134
162
  sendJson(res, 400, { error: "invalid_body", detail: "Provide mode, network, extraWriteRoots, or extraReadRoots" });
135
163
  return true;
136
164
  }
137
- const scopeParsed = readMemoryScope(body.scope);
165
+ const scopeParsed = readScope(body.scope);
138
166
  if (!scopeParsed.ok) {
139
167
  sendJson(res, 400, { error: "invalid_scope", allowed: ["global", "project"] });
140
168
  return true;
@@ -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
+ }