min-agent 0.4.1 → 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 +32 -2
  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
@@ -0,0 +1,285 @@
1
+ import { readFileSync, mkdirSync, existsSync } from "fs";
2
+ import path from "path";
3
+ import { atomicWriteFileSync } from "./tools/atomic-file.js";
4
+ import { getConfigDir, parseThinkingEffort } from "./config.js";
5
+ const DEFAULT_LONAE_BASE = "https://models.lonae.com";
6
+ const CACHE_TTL = 7 * 24 * 60 * 60 * 1000;
7
+ const CATALOG_ENTRY_VERSION = 3;
8
+ const NEGATIVE_TTL = 60 * 60 * 1000;
9
+ const FETCH_TIMEOUT_MS = 10000;
10
+ const memoryCache = new Map();
11
+ const inFlight = new Map();
12
+ const negativeCache = new Map();
13
+ export function lonaeBaseUrl() {
14
+ const raw = process.env.MIN_AGENT_MODELS_API_URL?.trim();
15
+ if (!raw)
16
+ return DEFAULT_LONAE_BASE;
17
+ return raw.replace(/\/$/, "");
18
+ }
19
+ function cacheFile() {
20
+ return path.join(getConfigDir(), "model-catalog-cache.json");
21
+ }
22
+ function loadDiskCache() {
23
+ const file = cacheFile();
24
+ if (!existsSync(file))
25
+ return {};
26
+ try {
27
+ return JSON.parse(readFileSync(file, "utf-8"));
28
+ }
29
+ catch {
30
+ return {};
31
+ }
32
+ }
33
+ function saveDiskCache(cache) {
34
+ const file = cacheFile();
35
+ mkdirSync(path.dirname(file), { recursive: true });
36
+ atomicWriteFileSync(file, JSON.stringify(cache));
37
+ }
38
+ export function clearModelCatalogCache() {
39
+ memoryCache.clear();
40
+ inFlight.clear();
41
+ negativeCache.clear();
42
+ }
43
+ export function getCachedModelCatalog(modelId) {
44
+ const memory = memoryCache.get(modelId);
45
+ if (memory && memory.version === CATALOG_ENTRY_VERSION && Date.now() - memory.timestamp <= CACHE_TTL)
46
+ return memory;
47
+ const disk = loadDiskCache()[modelId];
48
+ if (!disk || disk.version !== CATALOG_ENTRY_VERSION || Date.now() - disk.timestamp > CACHE_TTL)
49
+ return null;
50
+ memoryCache.set(modelId, disk);
51
+ return disk;
52
+ }
53
+ function setCache(modelId, entry) {
54
+ memoryCache.set(modelId, entry);
55
+ const cache = loadDiskCache();
56
+ cache[modelId] = entry;
57
+ saveDiskCache(cache);
58
+ }
59
+ function isRecord(value) {
60
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
61
+ }
62
+ export function parseReasoningOptions(raw) {
63
+ if (!Array.isArray(raw))
64
+ return { toggle: false, efforts: [] };
65
+ let toggle = false;
66
+ const efforts = new Set();
67
+ for (const item of raw) {
68
+ if (!isRecord(item))
69
+ continue;
70
+ if (item.type === "toggle")
71
+ toggle = true;
72
+ if (item.type === "effort" && Array.isArray(item.values)) {
73
+ for (const value of item.values) {
74
+ if (typeof value === "string" && value && value !== "default")
75
+ efforts.add(value);
76
+ }
77
+ }
78
+ }
79
+ if (efforts.has("none"))
80
+ toggle = true;
81
+ return { toggle, efforts: [...efforts] };
82
+ }
83
+ export function pickListedModel(models, modelId) {
84
+ const lower = modelId.toLowerCase();
85
+ const exact = models.find((m) => m.id.toLowerCase() === lower);
86
+ if (exact)
87
+ return exact;
88
+ const suffix = models.find((m) => {
89
+ const id = m.id.toLowerCase();
90
+ return id.endsWith(`/${lower}`) || lower.endsWith(`/${id}`);
91
+ });
92
+ if (suffix)
93
+ return suffix;
94
+ return models.find((m) => m.name?.toLowerCase() === lower);
95
+ }
96
+ export function matchOffering(offerings, baseURL) {
97
+ if (!baseURL || offerings.length === 0)
98
+ return undefined;
99
+ let host = "";
100
+ try {
101
+ host = new URL(baseURL).hostname.toLowerCase();
102
+ }
103
+ catch {
104
+ return undefined;
105
+ }
106
+ return offerings.find((o) => {
107
+ const id = String(o.providerId ?? "")
108
+ .toLowerCase()
109
+ .replace(/_/g, "-");
110
+ if (!id)
111
+ return false;
112
+ if (host.includes(id))
113
+ return true;
114
+ const slug = id.split("-")[0] ?? "";
115
+ return slug.length >= 4 && host.includes(slug);
116
+ });
117
+ }
118
+ const CANONICAL_EFFORTS = ["low", "medium", "high", "max"];
119
+ function hasEffortLevel(parsed) {
120
+ return parsed.efforts.some((raw) => {
121
+ const canonical = parseThinkingEffort(raw);
122
+ return Boolean(canonical && canonical !== "off");
123
+ });
124
+ }
125
+ /** When the active host is unknown, keep levels a majority of offerings actually list. */
126
+ export function majorityReasoning(offerings) {
127
+ const voters = offerings
128
+ .map((offering) => parseReasoningOptions(offering.reasoningOptions))
129
+ .filter((parsed) => parsed.toggle || parsed.efforts.length > 0);
130
+ if (voters.length === 0)
131
+ return { toggle: false, efforts: [] };
132
+ const effortVoters = voters.filter(hasEffortLevel);
133
+ const toggleThreshold = voters.length / 2;
134
+ const effortThreshold = effortVoters.length === 0 ? Number.POSITIVE_INFINITY : effortVoters.length / 2;
135
+ let toggleVotes = 0;
136
+ let xhighVotes = 0;
137
+ const votes = new Map();
138
+ for (const parsed of voters) {
139
+ if (parsed.toggle || parsed.efforts.includes("none"))
140
+ toggleVotes++;
141
+ }
142
+ for (const parsed of effortVoters) {
143
+ const seen = new Set();
144
+ for (const raw of parsed.efforts) {
145
+ if (raw === "xhigh" || raw === "extra-high")
146
+ xhighVotes++;
147
+ const canonical = parseThinkingEffort(raw);
148
+ if (!canonical || canonical === "off" || seen.has(canonical))
149
+ continue;
150
+ seen.add(canonical);
151
+ votes.set(canonical, (votes.get(canonical) ?? 0) + 1);
152
+ }
153
+ }
154
+ const toggle = toggleVotes >= toggleThreshold;
155
+ const efforts = [];
156
+ if (toggle)
157
+ efforts.push("none");
158
+ for (const key of CANONICAL_EFFORTS) {
159
+ if ((votes.get(key) ?? 0) >= effortThreshold)
160
+ efforts.push(key);
161
+ }
162
+ if (efforts.includes("max") && xhighVotes > 0)
163
+ efforts.push("xhigh");
164
+ return { toggle, efforts };
165
+ }
166
+ function contextFrom(limit) {
167
+ const n = limit?.context;
168
+ if (typeof n === "number" && Number.isFinite(n) && n >= 4096)
169
+ return Math.floor(n);
170
+ return undefined;
171
+ }
172
+ export function catalogFromLonaeModel(model, modelId, baseURL) {
173
+ const offerings = model.offerings ?? [];
174
+ const matched = matchOffering(offerings, baseURL);
175
+ const reasoning = matched ? parseReasoningOptions(matched.reasoningOptions) : majorityReasoning(offerings);
176
+ const contextWindow = contextFrom(matched?.limit) ?? contextFrom(model.limit);
177
+ return {
178
+ version: CATALOG_ENTRY_VERSION,
179
+ modelId,
180
+ lonaeId: model.id,
181
+ ...(contextWindow != null ? { contextWindow } : {}),
182
+ reasoning,
183
+ timestamp: Date.now(),
184
+ };
185
+ }
186
+ async function fetchJson(url) {
187
+ try {
188
+ const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
189
+ if (!response.ok)
190
+ return null;
191
+ return await response.json();
192
+ }
193
+ catch {
194
+ return null;
195
+ }
196
+ }
197
+ function readModel(payload) {
198
+ const root = isRecord(payload) ? payload : null;
199
+ const data = root && isRecord(root.data) ? root.data : root;
200
+ if (!data || typeof data.id !== "string")
201
+ return null;
202
+ const limit = isRecord(data.limit)
203
+ ? { context: typeof data.limit.context === "number" ? data.limit.context : undefined }
204
+ : undefined;
205
+ const offerings = Array.isArray(data.offerings)
206
+ ? data.offerings.filter(isRecord).map((o) => ({
207
+ providerId: typeof o.providerId === "string" ? o.providerId : undefined,
208
+ limit: isRecord(o.limit)
209
+ ? { context: typeof o.limit.context === "number" ? o.limit.context : undefined }
210
+ : undefined,
211
+ reasoningOptions: o.reasoningOptions,
212
+ }))
213
+ : [];
214
+ return {
215
+ id: data.id,
216
+ limit,
217
+ reasoning: data.reasoning === true,
218
+ offerings,
219
+ };
220
+ }
221
+ function readList(payload) {
222
+ const root = isRecord(payload) ? payload : null;
223
+ const data = root && Array.isArray(root.data) ? root.data : Array.isArray(payload) ? payload : [];
224
+ return data.flatMap((item) => {
225
+ if (!isRecord(item) || typeof item.id !== "string")
226
+ return [];
227
+ return [
228
+ {
229
+ id: item.id,
230
+ name: typeof item.name === "string" ? item.name : undefined,
231
+ context: typeof item.context === "number" ? item.context : undefined,
232
+ },
233
+ ];
234
+ });
235
+ }
236
+ function modelPath(id) {
237
+ return id
238
+ .split("/")
239
+ .map((part) => encodeURIComponent(part))
240
+ .join("/");
241
+ }
242
+ async function fetchLonaeModel(modelId) {
243
+ const base = lonaeBaseUrl();
244
+ if (modelId.includes("/")) {
245
+ const direct = readModel(await fetchJson(`${base}/api/v1/models/${modelPath(modelId)}`));
246
+ if (direct)
247
+ return direct;
248
+ }
249
+ const listed = readList(await fetchJson(`${base}/api/v1/models?q=${encodeURIComponent(modelId)}&page_size=20`));
250
+ const match = pickListedModel(listed, modelId);
251
+ if (!match)
252
+ return null;
253
+ return readModel(await fetchJson(`${base}/api/v1/models/${modelPath(match.id)}`));
254
+ }
255
+ /**
256
+ * Resolve context window and thinking options from the Lonae catalog.
257
+ * Successful lookups are cached in memory and on disk for 7 days.
258
+ */
259
+ export async function getModelCatalog(modelId, baseURL) {
260
+ const cached = getCachedModelCatalog(modelId);
261
+ if (cached)
262
+ return cached;
263
+ const negAt = negativeCache.get(modelId);
264
+ if (negAt !== undefined && Date.now() - negAt <= NEGATIVE_TTL)
265
+ return null;
266
+ const pending = inFlight.get(modelId);
267
+ if (pending)
268
+ return pending;
269
+ const probing = fetchLonaeModel(modelId).then((model) => {
270
+ if (!model) {
271
+ negativeCache.set(modelId, Date.now());
272
+ return null;
273
+ }
274
+ const entry = catalogFromLonaeModel(model, modelId, baseURL);
275
+ setCache(modelId, entry);
276
+ return entry;
277
+ });
278
+ inFlight.set(modelId, probing);
279
+ try {
280
+ return await probing;
281
+ }
282
+ finally {
283
+ inFlight.delete(modelId);
284
+ }
285
+ }
@@ -1,4 +1,4 @@
1
- import { takeScopeFlags } from "./memory.js";
1
+ import { takeScopeFlags, scopeLabel } from "./scope.js";
2
2
  import { getPermissionSnapshot, parsePermissionMode, permissionModeLabel, setPermissionMode, } from "./config.js";
3
3
  import { getPermissionOverride, setPermissionOverride } from "./confirm.js";
4
4
  export const PERMISSION_CLI_USAGE = [
@@ -8,9 +8,6 @@ export const PERMISSION_CLI_USAGE = [
8
8
  function sourceLabel(source) {
9
9
  return source === "cli" ? "this run" : source === "project" ? "project" : source === "global" ? "global" : "default";
10
10
  }
11
- function scopeLabel(scope) {
12
- return scope === "project" ? "project" : "global";
13
- }
14
11
  export function runPermissionCli(input) {
15
12
  const { scope: posScope, rest } = takeScopeFlags(input.positionals);
16
13
  const scope = posScope ?? input.scope ?? "global";
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") {