@sean.holung/minicode 0.3.5 → 0.3.7

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 (47) hide show
  1. package/README.md +22 -45
  2. package/dist/scripts/run-benchmarks.js +1 -0
  3. package/dist/src/agent/config.js +53 -66
  4. package/dist/src/agent/editable-config.js +56 -58
  5. package/dist/src/agent/home-env.js +74 -0
  6. package/dist/src/cli/config-slash-command.js +15 -13
  7. package/dist/src/serve/agent-bridge.js +87 -28
  8. package/dist/src/serve/mcp-server.js +19 -13
  9. package/dist/src/serve/server.js +190 -4
  10. package/dist/src/session/session-preview.js +14 -0
  11. package/dist/src/shared/graph-search.js +80 -0
  12. package/dist/src/shared/graph-selection.js +40 -0
  13. package/dist/src/shared/symbol-search.js +156 -0
  14. package/dist/src/tools/search-code-map.js +27 -35
  15. package/dist/src/web/app.js +582 -64
  16. package/dist/src/web/index.html +84 -6
  17. package/dist/src/web/style.css +256 -1
  18. package/dist/tests/config-api.test.js +10 -5
  19. package/dist/tests/config-integration.test.js +130 -56
  20. package/dist/tests/config-slash-command.test.js +12 -11
  21. package/dist/tests/config.test.js +21 -4
  22. package/dist/tests/editable-config.test.js +15 -12
  23. package/dist/tests/graph-onboarding.test.js +22 -1
  24. package/dist/tests/graph-search.test.js +66 -0
  25. package/dist/tests/graph-selection.test.js +58 -0
  26. package/dist/tests/home-env.test.js +56 -0
  27. package/dist/tests/mcp-and-plugin.test.js +3 -0
  28. package/dist/tests/search-code-map.test.js +9 -0
  29. package/dist/tests/serve.integration.test.js +255 -6
  30. package/dist/tests/session-preview.test.js +56 -0
  31. package/dist/tests/session-ui.test.js +2 -0
  32. package/dist/tests/settings-ui.test.js +18 -0
  33. package/dist/tests/system-prompt.test.js +1 -0
  34. package/dist/tests/test-utils.js +1 -0
  35. package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +1 -0
  36. package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.map +1 -1
  37. package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts +8 -1
  38. package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
  39. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +143 -27
  40. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
  41. package/node_modules/@minicode/agent-sdk/dist/tests/model-client-openai.test.js +87 -0
  42. package/node_modules/@minicode/agent-sdk/dist/tests/model-client-openai.test.js.map +1 -1
  43. package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.d.ts.map +1 -1
  44. package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.js +1 -0
  45. package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.js.map +1 -1
  46. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  47. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # minicode
2
2
 
3
+ > Now supports connecting to [OpenRouter](https://openrouter.ai/) account via minicode UI. Sign in with OpenRouter account. Use for free with compatible [free tier OpenRouter hosted models](https://openrouter.ai/models?q=free) from MiniMax, Nvidia, Qwen, Google, etc.
4
+
3
5
  A graph-native coding agent and code exploration environment built around structural context optimization that leverages symbol-aware retrieval, dependency graphs, and targeted context. It started as a way to make local models viable under tighter context budgets, and it now also works well with hosted frontier models through the same runtime, web UI, and OpenAI-compatible serve mode.
4
6
 
5
7
  minicode is built on a simple bet: models perform better when you give them less, but better context. Bloated context directly degrades output quality: attention dilutes, positional biases cause mid-context information loss, and inference latency grows as token count increases.
@@ -10,6 +12,16 @@ _Run `minicode serve` to get the web UI on localhost: chat, tool activity, sessi
10
12
 
11
13
  <img width="1723" height="920" alt="Screenshot 2026-03-26 at 6 30 23 PM" src="https://github.com/user-attachments/assets/499c8dc7-cc2b-4125-abd5-32b2fc9795ea" />
12
14
 
15
+ ## Quick Start (OpenRouter)
16
+ ```bash
17
+ npm install -g @sean.holung/minicode
18
+ minicode serve
19
+ ```
20
+
21
+ 1. Navigate to [localhost:4567](http://localhost:4567)
22
+ 2. Click Connect OpenRouter to sign in to [OpenRouter](https://openrouter.ai/) and connect account.
23
+ 3. Choose a model. Choose a (free) model if on free tier. Model must support tool use.
24
+
13
25
 
14
26
  ## Quick Start (LM Studio)
15
27
 
@@ -19,8 +31,8 @@ _Run `minicode serve` to get the web UI on localhost: chat, tool activity, sessi
19
31
  # 2. Install
20
32
  npm install -g @sean.holung/minicode
21
33
 
22
- # 3. Configure (~/.minicode/agent.config.json is auto-created on first run)
23
- # Set your model name — minicode will prompt you if this is missing.
34
+ # 3. Configure
35
+ # Set your model name in ~/.minicode/.env — minicode will prompt you if this is missing.
24
36
  cat > ~/.minicode/.env << 'EOF'
25
37
  MODEL_PROVIDER=openai-compatible
26
38
  MODEL=your-model-name
@@ -164,12 +176,12 @@ See [docs/PLUGIN_SPEC.md](docs/PLUGIN_SPEC.md) for the full specification. Quick
164
176
 
165
177
  ## Configuration
166
178
 
167
- Configuration can come from (later sources override earlier):
179
+ Configuration can come from:
168
180
 
169
- 1. `~/.minicode/.env` — User-level defaults (API keys, model, etc.)
170
- 2. `~/.minicode/agent.config.json`User-level JSON config
171
- 3. Project `.env` and `agent.config.json` in workspace root
172
- 4. Environment variables (highest precedence)
181
+ 1. `~/.minicode/.env` — User-level defaults (API keys, model, runtime settings)
182
+ 2. Environment variables Shell overrides for the current process
183
+
184
+ When the same key is set in both places, the exported environment variable wins.
173
185
 
174
186
  Nothing is written inside your workspace; config and cache live under `~/.minicode/`.
175
187
 
@@ -184,8 +196,10 @@ Nothing is written inside your workspace; config and cache live under `~/.minico
184
196
  | `OPENAI_BASE_URL` | No | `http://localhost:1234/v1` | Base URL for OpenAI-compatible API (LM Studio, etc.) |
185
197
  | `OPENAI_API_KEY` | No | none | Optional for local servers; required if your endpoint enforces auth |
186
198
  | `OPENROUTER_API_KEY` | No | none | Preferred key when `OPENAI_BASE_URL` points at OpenRouter; falls back to `OPENAI_API_KEY` if unset |
199
+ | `COMMAND_DENYLIST` | No | none | Optional JSON array or comma-separated regex patterns appended to the built-in destructive-command denylist |
187
200
  | `MAX_STEPS` | No | `50` | Max agent loop iterations per user turn |
188
201
  | `MAX_TOKENS` | No | `4096` | Max model output tokens per model call |
202
+ | `MODEL_TIMEOUT_SECONDS` | No | `60` | Timeout waiting for a model API call to start responding before aborting and surfacing an error |
189
203
  | `MAX_CONTEXT_TOKENS` | No | `32000` | Approximate session history trimming target. For small models (e.g. 8k context), set lower (e.g. `6000`) to leave room for responses. |
190
204
  | `MAX_TOOL_OUTPUT_CHARS` | No | `8000` | Max chars per tool result before truncation. Set to `0` to disable. |
191
205
  | `WORKSPACE_ROOT` | No | current working directory | Root directory tools are allowed to access (set at runtime, not typically configured) |
@@ -202,44 +216,7 @@ Nothing is written inside your workspace; config and cache live under `~/.minico
202
216
  | `REASONING_EFFORT` | No | unset | Reasoning level for providers that support it. Valid values: `xhigh`, `high`, `medium`, `low`, `minimal`, `none` |
203
217
 
204
218
 
205
- ### `agent.config.json`
206
-
207
- A global `~/.minicode/agent.config.json` is auto-created on first run. Only set what you need — everything has sensible defaults:
208
-
209
- ```json
210
- {
211
- "modelProvider": "openai-compatible",
212
- "model": "your-model-name",
213
- "openAiBaseUrl": "http://localhost:1234/v1",
214
- "maxSteps": 50,
215
- "maxTokens": 4096,
216
- "maxContextTokens": 32000
217
- }
218
- ```
219
-
220
- Field mapping:
221
-
222
- - `modelProvider` ↔ `MODEL_PROVIDER`
223
- - `model` ↔ `MODEL`
224
- - `maxSteps` ↔ `MAX_STEPS`
225
- - `workspaceRoot` ↔ `WORKSPACE_ROOT`
226
- - `maxTokens` ↔ `MAX_TOKENS`
227
- - `maxContextTokens` ↔ `MAX_CONTEXT_TOKENS`
228
- - `commandTimeout` ↔ `COMMAND_TIMEOUT_MS`
229
- - `commandDenylist` ↔ no env equivalent (config-only)
230
- - `confirmDestructive` ↔ `CONFIRM_DESTRUCTIVE`
231
- - `maxFileSizeBytes` ↔ `MAX_FILE_SIZE_BYTES`
232
- - `keepRecentMessages` ↔ `KEEP_RECENT_MESSAGES`
233
- - `loopDetectionWindow` ↔ `LOOP_DETECTION_WINDOW`
234
- - `openAiBaseUrl` ↔ `OPENAI_BASE_URL`
235
- - `openAiApiKey` ↔ `OPENAI_API_KEY` / `OPENROUTER_API_KEY` (when using OpenRouter)
236
- - `maxToolOutputChars` ↔ `MAX_TOOL_OUTPUT_CHARS`
237
- - `enableFileReadDedup` ↔ `ENABLE_FILE_READ_DEDUP`
238
- - `enableAdaptiveKeepRecent` ↔ `ENABLE_ADAPTIVE_KEEP_RECENT`
239
- - `enableToolOutputTruncation` ↔ `ENABLE_TOOL_OUTPUT_TRUNCATION`
240
- - `compactionThreshold` ↔ `COMPACTION_THRESHOLD`
241
- - `compactionModel` ↔ `COMPACTION_MODEL`
242
- - `reasoningEffort` ↔ `REASONING_EFFORT`
219
+ All persisted user-level settings now live in `~/.minicode/.env`. The web UI settings dialog and `/config set` both update that file directly for non-secret runtime defaults.
243
220
 
244
221
  ## Usage
245
222
 
@@ -99,6 +99,7 @@ export function buildConfig(options = {}) {
99
99
  model,
100
100
  maxSteps: getNumberSetting(getShellOverride("MAX_STEPS"), fileConfig.maxSteps, 50),
101
101
  maxTokens: getNumberSetting(getShellOverride("MAX_TOKENS"), fileConfig.maxTokens, 4096),
102
+ modelTimeoutSeconds: getNumberSetting(getShellOverride("MODEL_TIMEOUT_SECONDS"), fileConfig.modelTimeoutSeconds, 60),
102
103
  maxContextTokens: getNumberSetting(getShellOverride("MAX_CONTEXT_TOKENS"), fileConfig.maxContextTokens, 32000),
103
104
  workspaceRoot: repoRoot,
104
105
  commandTimeoutMs: getNumberSetting(getShellOverride("COMMAND_TIMEOUT_MS"), fileConfig.commandTimeoutMs, 30000),
@@ -1,4 +1,4 @@
1
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
@@ -10,12 +10,13 @@ export const MINICODE_HOME = path.join(os.homedir(), ".minicode");
10
10
  */
11
11
  export function formatConfigForDisplay(config) {
12
12
  const lines = [
13
- "configHome: " + MINICODE_HOME + " (.env, agent.config.json)",
13
+ "configHome: " + MINICODE_HOME + " (.env)",
14
14
  "workspaceRoot: " + config.workspaceRoot,
15
15
  "modelProvider: " + config.modelProvider,
16
16
  "model: " + config.model,
17
17
  "maxSteps: " + config.maxSteps,
18
18
  "maxTokens: " + config.maxTokens,
19
+ "modelTimeoutSeconds: " + config.modelTimeoutSeconds,
19
20
  "maxContextTokens: " + config.maxContextTokens,
20
21
  "commandTimeoutMs: " + config.commandTimeoutMs,
21
22
  "maxFileSizeBytes: " + config.maxFileSizeBytes,
@@ -46,9 +47,14 @@ export function formatConfigForDisplay(config) {
46
47
  */
47
48
  export function getConfigMissing(config) {
48
49
  const missing = [];
50
+ const isOpenRouter = config.modelProvider === "openai-compatible" &&
51
+ config.openAiBaseUrl.includes("openrouter");
49
52
  if (!config.model) {
50
53
  missing.push("MODEL is not set");
51
54
  }
55
+ if (isOpenRouter && !config.openAiApiKey?.trim()) {
56
+ missing.push("OPENROUTER_API_KEY is not set");
57
+ }
52
58
  if (config.modelProvider === "anthropic" && !process.env.ANTHROPIC_API_KEY) {
53
59
  missing.push("ANTHROPIC_API_KEY is not set");
54
60
  }
@@ -63,8 +69,8 @@ export function getConfigSetupMessage(config) {
63
69
  "minicode is not configured yet. Missing:",
64
70
  ...missing.map((m) => ` - ${m}`),
65
71
  "",
66
- `Set these in ~/.minicode/.env or as environment variables.`,
67
- `Edit ~/.minicode/agent.config.json for non-secret settings.`,
72
+ "Set these in ~/.minicode/.env or as environment variables.",
73
+ "Editable runtime defaults set through the UI or /config are saved back to ~/.minicode/.env.",
68
74
  "",
69
75
  "Example ~/.minicode/.env for a local model:",
70
76
  " MODEL_PROVIDER=openai-compatible",
@@ -117,20 +123,6 @@ function parseBoolean(value, fallback) {
117
123
  }
118
124
  return fallback;
119
125
  }
120
- export async function loadConfigFile(configPath) {
121
- try {
122
- await access(configPath);
123
- }
124
- catch {
125
- return {};
126
- }
127
- const file = await readFile(configPath, "utf8");
128
- const parsed = JSON.parse(file);
129
- if (!parsed || typeof parsed !== "object") {
130
- return {};
131
- }
132
- return parsed;
133
- }
134
126
  async function loadDotenvFile(envPath) {
135
127
  try {
136
128
  const file = await readFile(envPath, "utf8");
@@ -188,6 +180,24 @@ function parseUserDenylist(patterns) {
188
180
  }
189
181
  return denylist;
190
182
  }
183
+ function parseCommandDenylistEnv(value) {
184
+ if (!value?.trim()) {
185
+ return [];
186
+ }
187
+ const trimmed = value.trim();
188
+ if (trimmed.startsWith("[")) {
189
+ try {
190
+ const parsed = JSON.parse(trimmed);
191
+ if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
192
+ return parseUserDenylist(parsed);
193
+ }
194
+ }
195
+ catch {
196
+ return [];
197
+ }
198
+ }
199
+ return parseUserDenylist(trimmed.split(",").map((pattern) => pattern.trim()).filter(Boolean));
200
+ }
191
201
  function parseModelProvider(value) {
192
202
  const normalized = value?.trim().toLowerCase();
193
203
  if (normalized === "openai-compatible" ||
@@ -198,74 +208,51 @@ function parseModelProvider(value) {
198
208
  }
199
209
  return "anthropic";
200
210
  }
201
- const DEFAULT_CONFIG_CONTENT = `{
202
- "modelProvider": "openai-compatible",
203
- "model": "",
204
- "openAiBaseUrl": "http://localhost:1234/v1",
205
- "maxSteps": 50,
206
- "maxTokens": 4096,
207
- "maxContextTokens": 32000
208
- }
209
- `;
210
211
  async function ensureMinicodeHome(minicodeHome) {
211
212
  await mkdir(minicodeHome, { recursive: true });
212
- const configPath = path.join(minicodeHome, "agent.config.json");
213
- try {
214
- await access(configPath);
215
- }
216
- catch {
217
- await writeFile(configPath, DEFAULT_CONFIG_CONTENT, "utf8");
218
- }
219
213
  }
220
214
  export async function loadAgentConfig(cwd = process.cwd(), options = {}) {
221
215
  const minicodeHome = options.minicodeHome ?? MINICODE_HOME;
222
216
  await ensureMinicodeHome(minicodeHome);
223
- const homeConfigPath = path.join(minicodeHome, "agent.config.json");
224
- const fileConfig = await loadConfigFile(homeConfigPath);
225
217
  const env = (await resolveConfigEnv({ minicodeHome })).values;
226
- const rawWorkspaceRoot = env.WORKSPACE_ROOT ?? fileConfig.workspaceRoot ?? cwd;
218
+ const rawWorkspaceRoot = env.WORKSPACE_ROOT ?? cwd;
227
219
  const workspaceRoot = path.resolve(cwd, rawWorkspaceRoot);
228
220
  const commandDenylist = [
229
221
  ...DEFAULT_COMMAND_DENYLIST,
230
- ...parseUserDenylist(fileConfig.commandDenylist),
222
+ ...parseCommandDenylistEnv(env.COMMAND_DENYLIST),
231
223
  ];
232
- const rawBaseUrl = env.OPENAI_BASE_URL ??
233
- fileConfig.openAiBaseUrl ??
234
- "http://localhost:1234/v1";
224
+ const rawBaseUrl = env.OPENAI_BASE_URL ?? "http://localhost:1234/v1";
235
225
  const isOpenRouter = rawBaseUrl.includes("openrouter");
236
226
  const openAiApiKey = isOpenRouter
237
- ? (env.OPENROUTER_API_KEY ??
238
- env.OPENAI_API_KEY ??
239
- fileConfig.openAiApiKey)
240
- : (env.OPENAI_API_KEY ?? fileConfig.openAiApiKey);
227
+ ? (env.OPENROUTER_API_KEY ?? env.OPENAI_API_KEY)
228
+ : env.OPENAI_API_KEY;
241
229
  return {
242
- modelProvider: parseModelProvider(env.MODEL_PROVIDER ?? fileConfig.modelProvider ?? "openai-compatible"),
243
- model: env.MODEL ??
244
- fileConfig.model ??
245
- "",
246
- maxSteps: parseNumber(env.MAX_STEPS, fileConfig.maxSteps ?? 50),
247
- maxTokens: parseNumber(env.MAX_TOKENS, fileConfig.maxTokens ?? 4096),
248
- maxContextTokens: parseNumber(env.MAX_CONTEXT_TOKENS, fileConfig.maxContextTokens ?? 32_000),
230
+ modelProvider: parseModelProvider(env.MODEL_PROVIDER ?? "openai-compatible"),
231
+ model: env.MODEL ?? "",
232
+ maxSteps: parseNumber(env.MAX_STEPS, 50),
233
+ maxTokens: parseNumber(env.MAX_TOKENS, 4096),
234
+ modelTimeoutSeconds: parseNumber(env.MODEL_TIMEOUT_SECONDS, 60),
235
+ maxContextTokens: parseNumber(env.MAX_CONTEXT_TOKENS, 32_000),
249
236
  workspaceRoot,
250
- commandTimeoutMs: parseNumber(env.COMMAND_TIMEOUT_MS, fileConfig.commandTimeout ?? 30_000),
251
- maxFileSizeBytes: parseNumber(env.MAX_FILE_SIZE_BYTES, fileConfig.maxFileSizeBytes ?? 1_000_000),
237
+ commandTimeoutMs: parseNumber(env.COMMAND_TIMEOUT_MS, 30_000),
238
+ maxFileSizeBytes: parseNumber(env.MAX_FILE_SIZE_BYTES, 1_000_000),
252
239
  commandDenylist,
253
- confirmDestructive: parseBoolean(env.CONFIRM_DESTRUCTIVE, fileConfig.confirmDestructive ?? true),
254
- keepRecentMessages: parseNumber(env.KEEP_RECENT_MESSAGES, fileConfig.keepRecentMessages ?? 12),
255
- loopDetectionWindow: parseNumber(env.LOOP_DETECTION_WINDOW, fileConfig.loopDetectionWindow ?? 6),
256
- maxToolOutputChars: parseNumber(env.MAX_TOOL_OUTPUT_CHARS, fileConfig.maxToolOutputChars ?? 8_000),
240
+ confirmDestructive: parseBoolean(env.CONFIRM_DESTRUCTIVE, true),
241
+ keepRecentMessages: parseNumber(env.KEEP_RECENT_MESSAGES, 12),
242
+ loopDetectionWindow: parseNumber(env.LOOP_DETECTION_WINDOW, 6),
243
+ maxToolOutputChars: parseNumber(env.MAX_TOOL_OUTPUT_CHARS, 8_000),
257
244
  openAiBaseUrl: rawBaseUrl,
258
245
  ...(openAiApiKey !== undefined ? { openAiApiKey } : {}),
259
- enableFileReadDedup: parseBoolean(env.ENABLE_FILE_READ_DEDUP, fileConfig.enableFileReadDedup ?? true),
260
- enableAdaptiveKeepRecent: parseBoolean(env.ENABLE_ADAPTIVE_KEEP_RECENT, fileConfig.enableAdaptiveKeepRecent ?? true),
261
- enableToolOutputTruncation: parseBoolean(env.ENABLE_TOOL_OUTPUT_TRUNCATION, fileConfig.enableToolOutputTruncation ?? true),
262
- compactionThreshold: parseNumber(env.COMPACTION_THRESHOLD, fileConfig.compactionThreshold ?? 0.8),
263
- ...(env.COMPACTION_MODEL ?? fileConfig.compactionModel
264
- ? { compactionModel: env.COMPACTION_MODEL ?? fileConfig.compactionModel }
246
+ enableFileReadDedup: parseBoolean(env.ENABLE_FILE_READ_DEDUP, true),
247
+ enableAdaptiveKeepRecent: parseBoolean(env.ENABLE_ADAPTIVE_KEEP_RECENT, true),
248
+ enableToolOutputTruncation: parseBoolean(env.ENABLE_TOOL_OUTPUT_TRUNCATION, true),
249
+ compactionThreshold: parseNumber(env.COMPACTION_THRESHOLD, 0.8),
250
+ ...(env.COMPACTION_MODEL
251
+ ? { compactionModel: env.COMPACTION_MODEL }
265
252
  : {}),
266
- enableDynamicPrompt: parseBoolean(env.ENABLE_DYNAMIC_PROMPT, fileConfig.enableDynamicPrompt ?? true),
253
+ enableDynamicPrompt: parseBoolean(env.ENABLE_DYNAMIC_PROMPT, true),
267
254
  ...(() => {
268
- const effort = parseReasoningEffort(env.REASONING_EFFORT ?? fileConfig.reasoningEffort);
255
+ const effort = parseReasoningEffort(env.REASONING_EFFORT);
269
256
  return effort ? { reasoningEffort: effort } : {};
270
257
  })(),
271
258
  };
@@ -1,6 +1,5 @@
1
- import { mkdir, rm, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { loadConfigFile, MINICODE_HOME, resolveConfigEnv, } from "./config.js";
1
+ import { MINICODE_HOME, resolveConfigEnv, } from "./config.js";
2
+ import { getHomeEnvPath, loadHomeEnvValues, upsertHomeEnvValues } from "./home-env.js";
4
3
  const REASONING_VALUES = [
5
4
  "xhigh",
6
5
  "high",
@@ -12,7 +11,6 @@ const REASONING_VALUES = [
12
11
  export const EDITABLE_CONFIG_DEFINITIONS = [
13
12
  {
14
13
  key: "modelProvider",
15
- fileKey: "modelProvider",
16
14
  envVar: "MODEL_PROVIDER",
17
15
  type: "enum",
18
16
  values: ["anthropic", "openai-compatible"],
@@ -20,119 +18,108 @@ export const EDITABLE_CONFIG_DEFINITIONS = [
20
18
  },
21
19
  {
22
20
  key: "model",
23
- fileKey: "model",
24
21
  envVar: "MODEL",
25
22
  type: "string",
26
23
  description: "Default model id for new sessions",
27
24
  },
28
25
  {
29
26
  key: "maxSteps",
30
- fileKey: "maxSteps",
31
27
  envVar: "MAX_STEPS",
32
28
  type: "number",
33
29
  description: "Turn call limit before the agent pauses and waits for another prompt",
34
30
  },
35
31
  {
36
32
  key: "maxTokens",
37
- fileKey: "maxTokens",
38
33
  envVar: "MAX_TOKENS",
39
34
  type: "number",
40
35
  description: "Maximum completion tokens per model response",
41
36
  },
37
+ {
38
+ key: "modelTimeoutSeconds",
39
+ envVar: "MODEL_TIMEOUT_SECONDS",
40
+ type: "number",
41
+ description: "Maximum time to wait for a model API call to start responding",
42
+ },
42
43
  {
43
44
  key: "maxContextTokens",
44
- fileKey: "maxContextTokens",
45
45
  envVar: "MAX_CONTEXT_TOKENS",
46
46
  type: "number",
47
47
  description: "Estimated prompt-context budget before compaction",
48
48
  },
49
49
  {
50
50
  key: "commandTimeoutMs",
51
- fileKey: "commandTimeout",
52
51
  envVar: "COMMAND_TIMEOUT_MS",
53
52
  type: "number",
54
53
  description: "Shell command timeout in milliseconds",
55
54
  },
56
55
  {
57
56
  key: "maxFileSizeBytes",
58
- fileKey: "maxFileSizeBytes",
59
57
  envVar: "MAX_FILE_SIZE_BYTES",
60
58
  type: "number",
61
59
  description: "Maximum file size allowed for read/edit tools",
62
60
  },
63
61
  {
64
62
  key: "confirmDestructive",
65
- fileKey: "confirmDestructive",
66
63
  envVar: "CONFIRM_DESTRUCTIVE",
67
64
  type: "boolean",
68
65
  description: "Whether destructive commands require confirmation",
69
66
  },
70
67
  {
71
68
  key: "keepRecentMessages",
72
- fileKey: "keepRecentMessages",
73
69
  envVar: "KEEP_RECENT_MESSAGES",
74
70
  type: "number",
75
71
  description: "Recent messages preserved when trimming session history",
76
72
  },
77
73
  {
78
74
  key: "loopDetectionWindow",
79
- fileKey: "loopDetectionWindow",
80
75
  envVar: "LOOP_DETECTION_WINDOW",
81
76
  type: "number",
82
77
  description: "Window size for repeated-tool-call loop detection",
83
78
  },
84
79
  {
85
80
  key: "maxToolOutputChars",
86
- fileKey: "maxToolOutputChars",
87
81
  envVar: "MAX_TOOL_OUTPUT_CHARS",
88
82
  type: "number",
89
83
  description: "Maximum tool output retained before truncation",
90
84
  },
91
85
  {
92
86
  key: "openAiBaseUrl",
93
- fileKey: "openAiBaseUrl",
94
87
  envVar: "OPENAI_BASE_URL",
95
88
  type: "string",
96
89
  description: "Base URL for OpenAI-compatible providers",
97
90
  },
98
91
  {
99
92
  key: "enableFileReadDedup",
100
- fileKey: "enableFileReadDedup",
101
93
  envVar: "ENABLE_FILE_READ_DEDUP",
102
94
  type: "boolean",
103
95
  description: "Deduplicate repeated file reads in prompt context",
104
96
  },
105
97
  {
106
98
  key: "enableAdaptiveKeepRecent",
107
- fileKey: "enableAdaptiveKeepRecent",
108
99
  envVar: "ENABLE_ADAPTIVE_KEEP_RECENT",
109
100
  type: "boolean",
110
101
  description: "Adjust recent-message retention based on context pressure",
111
102
  },
112
103
  {
113
104
  key: "enableToolOutputTruncation",
114
- fileKey: "enableToolOutputTruncation",
115
105
  envVar: "ENABLE_TOOL_OUTPUT_TRUNCATION",
116
106
  type: "boolean",
117
107
  description: "Truncate oversized tool output before storing it in session history",
118
108
  },
119
109
  {
120
110
  key: "compactionThreshold",
121
- fileKey: "compactionThreshold",
122
111
  envVar: "COMPACTION_THRESHOLD",
123
112
  type: "number",
124
113
  description: "Compaction threshold ratio used before a turn starts",
125
114
  },
126
115
  {
127
116
  key: "compactionModel",
128
- fileKey: "compactionModel",
129
117
  envVar: "COMPACTION_MODEL",
130
118
  type: "string",
131
119
  description: "Optional model id used for LLM-based compaction",
132
120
  },
133
121
  {
134
122
  key: "reasoningEffort",
135
- fileKey: "reasoningEffort",
136
123
  envVar: "REASONING_EFFORT",
137
124
  type: "enum",
138
125
  values: REASONING_VALUES,
@@ -140,7 +127,6 @@ export const EDITABLE_CONFIG_DEFINITIONS = [
140
127
  },
141
128
  {
142
129
  key: "enableDynamicPrompt",
143
- fileKey: "enableDynamicPrompt",
144
130
  envVar: "ENABLE_DYNAMIC_PROMPT",
145
131
  type: "boolean",
146
132
  description: "Toggle project-aware dynamic prompt generation",
@@ -194,9 +180,6 @@ function normalizePersistedValue(value) {
194
180
  }
195
181
  return null;
196
182
  }
197
- function isEmptyConfigFile(config) {
198
- return Object.keys(config).length === 0;
199
- }
200
183
  export function listEditableConfigDefinitions() {
201
184
  return EDITABLE_CONFIG_DEFINITIONS;
202
185
  }
@@ -223,10 +206,25 @@ export function formatPersistedConfigValue(value) {
223
206
  return String(value);
224
207
  }
225
208
  export function getGlobalConfigPath(minicodeHome = MINICODE_HOME) {
226
- return path.join(minicodeHome, "agent.config.json");
209
+ return getHomeEnvPath(minicodeHome);
227
210
  }
228
211
  export async function loadPersistedConfig(minicodeHome = MINICODE_HOME) {
229
- return loadConfigFile(getGlobalConfigPath(minicodeHome));
212
+ return loadHomeEnvValues(minicodeHome);
213
+ }
214
+ function readPersistedEnvValue(definition, persisted) {
215
+ const rawValue = persisted[definition.envVar];
216
+ if (rawValue === undefined) {
217
+ return null;
218
+ }
219
+ try {
220
+ return parseEditableValue(definition, rawValue);
221
+ }
222
+ catch {
223
+ return rawValue;
224
+ }
225
+ }
226
+ function serializeEditableValue(value) {
227
+ return String(value);
230
228
  }
231
229
  export async function buildStructuredConfigPayload(config, minicodeHome = MINICODE_HOME) {
232
230
  const configPath = getGlobalConfigPath(minicodeHome);
@@ -235,10 +233,11 @@ export async function buildStructuredConfigPayload(config, minicodeHome = MINICO
235
233
  return {
236
234
  configPath,
237
235
  entries: EDITABLE_CONFIG_DEFINITIONS.map((definition) => {
238
- const envValue = env.values[definition.envVar];
239
- const envSource = env.sources[definition.envVar] ?? null;
240
- const envSourcePath = envSource === "home-dotenv"
241
- ? env.homeEnvPath
236
+ const envSource = env.sources[definition.envVar] === "process"
237
+ ? "process"
238
+ : null;
239
+ const envValue = envSource === "process"
240
+ ? (env.values[definition.envVar] ?? null)
242
241
  : null;
243
242
  return {
244
243
  key: definition.key,
@@ -247,11 +246,11 @@ export async function buildStructuredConfigPayload(config, minicodeHome = MINICO
247
246
  envVar: definition.envVar,
248
247
  ...(definition.values ? { values: definition.values } : {}),
249
248
  effectiveValue: normalizePersistedValue(config[definition.key]),
250
- persistedValue: normalizePersistedValue(persisted[definition.fileKey]),
251
- envValue: envValue ?? null,
249
+ persistedValue: readPersistedEnvValue(definition, persisted),
250
+ envValue,
252
251
  envSource,
253
- envSourcePath,
254
- overriddenByEnv: envValue !== undefined,
252
+ envSourcePath: envSource === "process" ? null : null,
253
+ overriddenByEnv: envSource === "process",
255
254
  };
256
255
  }),
257
256
  };
@@ -260,26 +259,25 @@ export async function setPersistedConfigValue(options) {
260
259
  const minicodeHome = options.minicodeHome ?? MINICODE_HOME;
261
260
  const definition = getEditableConfigDefinition(options.key);
262
261
  const configPath = getGlobalConfigPath(minicodeHome);
263
- const nextFile = await loadConfigFile(configPath);
264
262
  const storedValue = parseEditableValue(definition, options.rawValue);
265
- nextFile[definition.fileKey] = storedValue;
266
- await mkdir(path.dirname(configPath), { recursive: true });
267
- await writeFile(configPath, JSON.stringify(nextFile, null, 2) + "\n", "utf8");
263
+ await upsertHomeEnvValues({
264
+ minicodeHome,
265
+ values: {
266
+ [definition.envVar]: serializeEditableValue(storedValue),
267
+ },
268
+ });
268
269
  return { path: configPath, storedValue };
269
270
  }
270
271
  export async function unsetPersistedConfigValue(options) {
271
272
  const minicodeHome = options.minicodeHome ?? MINICODE_HOME;
272
273
  const definition = getEditableConfigDefinition(options.key);
273
274
  const configPath = getGlobalConfigPath(minicodeHome);
274
- const nextFile = await loadConfigFile(configPath);
275
- delete nextFile[definition.fileKey];
276
- if (isEmptyConfigFile(nextFile)) {
277
- await rm(configPath, { force: true });
278
- }
279
- else {
280
- await mkdir(path.dirname(configPath), { recursive: true });
281
- await writeFile(configPath, JSON.stringify(nextFile, null, 2) + "\n", "utf8");
282
- }
275
+ await upsertHomeEnvValues({
276
+ minicodeHome,
277
+ values: {
278
+ [definition.envVar]: null,
279
+ },
280
+ });
283
281
  return { path: configPath };
284
282
  }
285
283
  export async function applyPersistedConfigUpdates(options) {
@@ -291,22 +289,22 @@ export async function applyPersistedConfigUpdates(options) {
291
289
  return { key: rawKey, value };
292
290
  });
293
291
  const saved = [];
292
+ const envUpdates = {};
294
293
  for (const item of planned) {
294
+ const definition = getEditableConfigDefinition(item.key);
295
295
  if (item.value === null) {
296
- await unsetPersistedConfigValue({
297
- key: item.key,
298
- minicodeHome,
299
- });
296
+ envUpdates[definition.envVar] = null;
300
297
  saved.push({ key: item.key, value: null });
301
298
  continue;
302
299
  }
303
- await setPersistedConfigValue({
304
- key: item.key,
305
- rawValue: String(item.value),
306
- minicodeHome,
307
- });
308
- saved.push({ key: item.key, value: item.value });
300
+ const storedValue = parseEditableValue(definition, String(item.value));
301
+ envUpdates[definition.envVar] = serializeEditableValue(storedValue);
302
+ saved.push({ key: item.key, value: storedValue });
309
303
  }
304
+ await upsertHomeEnvValues({
305
+ minicodeHome,
306
+ values: envUpdates,
307
+ });
310
308
  return {
311
309
  path: getGlobalConfigPath(minicodeHome),
312
310
  saved,