@sean.holung/minicode 0.3.4 → 0.3.6

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 (72) hide show
  1. package/README.md +25 -47
  2. package/dist/scripts/run-benchmarks.js +73 -28
  3. package/dist/src/agent/config.js +51 -66
  4. package/dist/src/agent/editable-config.js +50 -58
  5. package/dist/src/agent/home-env.js +74 -0
  6. package/dist/src/benchmark/runner.js +142 -59
  7. package/dist/src/cli/config-slash-command.js +15 -13
  8. package/dist/src/indexer/project-index.js +49 -13
  9. package/dist/src/serve/agent-bridge.js +99 -31
  10. package/dist/src/serve/mcp-server.js +70 -21
  11. package/dist/src/serve/server.js +198 -8
  12. package/dist/src/session/session-preview.js +14 -0
  13. package/dist/src/shared/graph-search.js +80 -0
  14. package/dist/src/shared/graph-selection.js +40 -0
  15. package/dist/src/shared/graph-symbols.js +82 -0
  16. package/dist/src/shared/symbol-resolution.js +33 -0
  17. package/dist/src/tools/find-path.js +15 -6
  18. package/dist/src/tools/find-references.js +7 -2
  19. package/dist/src/tools/get-dependencies.js +8 -3
  20. package/dist/src/tools/read-symbol.js +9 -3
  21. package/dist/src/tools/registry.js +4 -1
  22. package/dist/src/tools/search-code-map.js +18 -3
  23. package/dist/src/web/app.js +646 -87
  24. package/dist/src/web/index.html +68 -6
  25. package/dist/src/web/style.css +208 -1
  26. package/dist/tests/benchmark-harness.test.js +100 -0
  27. package/dist/tests/config-api.test.js +5 -5
  28. package/dist/tests/config-integration.test.js +130 -56
  29. package/dist/tests/config-slash-command.test.js +12 -11
  30. package/dist/tests/config.test.js +12 -4
  31. package/dist/tests/editable-config.test.js +15 -12
  32. package/dist/tests/file-tools.test.js +34 -1
  33. package/dist/tests/find-path.test.js +43 -2
  34. package/dist/tests/find-references.test.js +49 -0
  35. package/dist/tests/get-dependencies.test.js +23 -0
  36. package/dist/tests/graph-onboarding.test.js +10 -1
  37. package/dist/tests/graph-search.test.js +66 -0
  38. package/dist/tests/graph-selection.test.js +58 -0
  39. package/dist/tests/graph-symbols.test.js +45 -0
  40. package/dist/tests/home-env.test.js +56 -0
  41. package/dist/tests/indexer.test.js +6 -0
  42. package/dist/tests/read-symbol.test.js +35 -0
  43. package/dist/tests/request-tracker.test.js +15 -0
  44. package/dist/tests/run-benchmarks.test.js +117 -33
  45. package/dist/tests/search-code-map.test.js +2 -0
  46. package/dist/tests/serve.integration.test.js +338 -9
  47. package/dist/tests/session-preview.test.js +56 -0
  48. package/dist/tests/session-ui.test.js +4 -0
  49. package/dist/tests/settings-ui.test.js +18 -0
  50. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  51. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +2 -1
  52. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  53. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +1 -1
  54. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
  55. package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
  56. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +3 -0
  57. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -1
  58. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts +3 -0
  59. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts.map +1 -1
  60. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js +4 -1
  61. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js.map +1 -1
  62. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts +11 -1
  63. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
  64. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +4 -1
  65. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
  66. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.d.ts.map +1 -1
  67. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js +16 -8
  68. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js.map +1 -1
  69. package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js +19 -2
  70. package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js.map +1 -1
  71. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +1 -1
package/README.md CHANGED
@@ -1,13 +1,27 @@
1
1
  # minicode
2
2
 
3
- A graph-native coding agent and code exploration environment built around structural context optimization. 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.
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 from MiniMax, Nvidia, Qwen, Google, etc.
4
+
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.
6
+
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.
8
+
9
+ Read operations dominate token usage in typical agent sessions; minicode addresses this by optimizing for **specific languages**. It indexes your project at startup with language plugins, injects a compact **code map** (signatures only) into the system prompt, and exposes symbol-level tools (`read_symbol`, `find_references`, `get_dependencies`) so the model reads only what it needs instead of entire files. This also enables the agent to walk the code structurally to gain a better understanding of the codebase at a structural level. TypeScript and JavaScript support come built-in, with custom language plugins leaving room for broader language support over time.
4
10
 
5
11
  _Run `minicode serve` to get the web UI on localhost: chat, tool activity, session controls, model switching, symbol focus, annotations, and a live dependency graph._
6
12
 
7
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" />
8
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.
9
24
 
10
- Read operations dominate token usage in typical agent sessions; minicode addresses this by optimizing for **specific languages**. It indexes your project at startup with language plugins, injects a compact **code map** (signatures only) into the system prompt, and exposes symbol-level tools (`read_symbol`, `find_references`, `get_dependencies`) so the model reads only what it needs instead of entire files. TypeScript and JavaScript support come built-in, with custom language plugins leaving room for broader language support over time.
11
25
 
12
26
  ## Quick Start (LM Studio)
13
27
 
@@ -17,8 +31,8 @@ Read operations dominate token usage in typical agent sessions; minicode address
17
31
  # 2. Install
18
32
  npm install -g @sean.holung/minicode
19
33
 
20
- # 3. Configure (~/.minicode/agent.config.json is auto-created on first run)
21
- # 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.
22
36
  cat > ~/.minicode/.env << 'EOF'
23
37
  MODEL_PROVIDER=openai-compatible
24
38
  MODEL=your-model-name
@@ -162,12 +176,12 @@ See [docs/PLUGIN_SPEC.md](docs/PLUGIN_SPEC.md) for the full specification. Quick
162
176
 
163
177
  ## Configuration
164
178
 
165
- Configuration can come from (later sources override earlier):
179
+ Configuration can come from:
180
+
181
+ 1. `~/.minicode/.env` — User-level defaults (API keys, model, runtime settings)
182
+ 2. Environment variables — Shell overrides for the current process
166
183
 
167
- 1. `~/.minicode/.env` User-level defaults (API keys, model, etc.)
168
- 2. `~/.minicode/agent.config.json` — User-level JSON config
169
- 3. Project `.env` and `agent.config.json` in workspace root
170
- 4. Environment variables (highest precedence)
184
+ When the same key is set in both places, the exported environment variable wins.
171
185
 
172
186
  Nothing is written inside your workspace; config and cache live under `~/.minicode/`.
173
187
 
@@ -182,6 +196,7 @@ Nothing is written inside your workspace; config and cache live under `~/.minico
182
196
  | `OPENAI_BASE_URL` | No | `http://localhost:1234/v1` | Base URL for OpenAI-compatible API (LM Studio, etc.) |
183
197
  | `OPENAI_API_KEY` | No | none | Optional for local servers; required if your endpoint enforces auth |
184
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 |
185
200
  | `MAX_STEPS` | No | `50` | Max agent loop iterations per user turn |
186
201
  | `MAX_TOKENS` | No | `4096` | Max model output tokens per model call |
187
202
  | `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. |
@@ -200,44 +215,7 @@ Nothing is written inside your workspace; config and cache live under `~/.minico
200
215
  | `REASONING_EFFORT` | No | unset | Reasoning level for providers that support it. Valid values: `xhigh`, `high`, `medium`, `low`, `minimal`, `none` |
201
216
 
202
217
 
203
- ### `agent.config.json`
204
-
205
- A global `~/.minicode/agent.config.json` is auto-created on first run. Only set what you need — everything has sensible defaults:
206
-
207
- ```json
208
- {
209
- "modelProvider": "openai-compatible",
210
- "model": "your-model-name",
211
- "openAiBaseUrl": "http://localhost:1234/v1",
212
- "maxSteps": 50,
213
- "maxTokens": 4096,
214
- "maxContextTokens": 32000
215
- }
216
- ```
217
-
218
- Field mapping:
219
-
220
- - `modelProvider` ↔ `MODEL_PROVIDER`
221
- - `model` ↔ `MODEL`
222
- - `maxSteps` ↔ `MAX_STEPS`
223
- - `workspaceRoot` ↔ `WORKSPACE_ROOT`
224
- - `maxTokens` ↔ `MAX_TOKENS`
225
- - `maxContextTokens` ↔ `MAX_CONTEXT_TOKENS`
226
- - `commandTimeout` ↔ `COMMAND_TIMEOUT_MS`
227
- - `commandDenylist` ↔ no env equivalent (config-only)
228
- - `confirmDestructive` ↔ `CONFIRM_DESTRUCTIVE`
229
- - `maxFileSizeBytes` ↔ `MAX_FILE_SIZE_BYTES`
230
- - `keepRecentMessages` ↔ `KEEP_RECENT_MESSAGES`
231
- - `loopDetectionWindow` ↔ `LOOP_DETECTION_WINDOW`
232
- - `openAiBaseUrl` ↔ `OPENAI_BASE_URL`
233
- - `openAiApiKey` ↔ `OPENAI_API_KEY` / `OPENROUTER_API_KEY` (when using OpenRouter)
234
- - `maxToolOutputChars` ↔ `MAX_TOOL_OUTPUT_CHARS`
235
- - `enableFileReadDedup` ↔ `ENABLE_FILE_READ_DEDUP`
236
- - `enableAdaptiveKeepRecent` ↔ `ENABLE_ADAPTIVE_KEEP_RECENT`
237
- - `enableToolOutputTruncation` ↔ `ENABLE_TOOL_OUTPUT_TRUNCATION`
238
- - `compactionThreshold` ↔ `COMPACTION_THRESHOLD`
239
- - `compactionModel` ↔ `COMPACTION_MODEL`
240
- - `reasoningEffort` ↔ `REASONING_EFFORT`
218
+ 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.
241
219
 
242
220
  ## Usage
243
221
 
@@ -12,15 +12,20 @@
12
12
  * --out <path> Write the JSON report to a file
13
13
  *
14
14
  * Environment:
15
- * MODEL_PROVIDER, MODEL, OPENAI_BASE_URL, OPENAI_API_KEY, ANTHROPIC_API_KEY
16
- * — same as minicode runtime config.
15
+ * MODEL_PROVIDER, MODEL, OPENAI_BASE_URL, OPENAI_API_KEY, OPENROUTER_API_KEY, ANTHROPIC_API_KEY
16
+ * — benchmark-layer overrides for benchmarks/benchmark.config.json.
17
17
  */
18
+ import { readFileSync, existsSync } from "node:fs";
18
19
  import path from "node:path";
20
+ import { homedir } from "node:os";
19
21
  import { writeFile } from "node:fs/promises";
20
- import { createModelClient, createReadFileTool, createWriteFileTool, createEditFileTool, createSearchTool, createListFilesTool, createRunCommandTool, } from "@minicode/agent-sdk";
22
+ import { createModelClient, } from "@minicode/agent-sdk";
23
+ import { parse as parseDotenv } from "dotenv";
21
24
  import { loadBenchmarkTasks, loadBenchmarkTask } from "../src/benchmark/task-loader.js";
22
25
  import { runBenchmarkSuite } from "../src/benchmark/runner.js";
23
26
  import { buildReport, formatReport } from "../src/benchmark/reporter.js";
27
+ import { buildProjectIndex } from "../src/indexer/project-index.js";
28
+ import { createToolRegistry } from "../src/tools/registry.js";
24
29
  export function parseArgs(argv) {
25
30
  const args = { variant: "ci" };
26
31
  for (let i = 0; i < argv.length; i++) {
@@ -48,25 +53,63 @@ export function parseArgs(argv) {
48
53
  /* ------------------------------------------------------------------ */
49
54
  /* Config builder */
50
55
  /* ------------------------------------------------------------------ */
51
- export function buildConfig() {
52
- const provider = (process.env.MODEL_PROVIDER ?? "openai-compatible");
53
- const model = process.env.MODEL ?? "test-model";
56
+ export function getBenchmarkConfigPath(repoRoot = process.cwd()) {
57
+ return path.resolve(repoRoot, "benchmarks", "benchmark.config.json");
58
+ }
59
+ function loadJsonConfigFile(configPath) {
60
+ if (!existsSync(configPath)) {
61
+ return {};
62
+ }
63
+ return JSON.parse(readFileSync(configPath, "utf8"));
64
+ }
65
+ function loadHomeEnvVars(homeEnvPath) {
66
+ if (!existsSync(homeEnvPath)) {
67
+ return {};
68
+ }
69
+ return parseDotenv(readFileSync(homeEnvPath, "utf8"));
70
+ }
71
+ function firstDefined(...values) {
72
+ return values.find((value) => value != null && value.length > 0);
73
+ }
74
+ function getNumberSetting(envValue, fileValue, fallback) {
75
+ if (envValue != null && envValue.length > 0) {
76
+ return Number(envValue);
77
+ }
78
+ return fileValue ?? fallback;
79
+ }
80
+ export function buildConfig(options = {}) {
81
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
82
+ const env = options.env ?? process.env;
83
+ const homeEnvPath = options.homeEnvPath ?? path.join(homedir(), ".minicode", ".env");
84
+ const configPath = options.configPath ?? getBenchmarkConfigPath(repoRoot);
85
+ const fileConfig = loadJsonConfigFile(configPath);
86
+ const homeEnv = loadHomeEnvVars(homeEnvPath);
87
+ const getShellOverride = (key) => env[key];
88
+ const getSecret = (key) => firstDefined(env[key], homeEnv[key]);
89
+ const provider = (firstDefined(getShellOverride("MODEL_PROVIDER"), fileConfig.modelProvider, "openai-compatible") ?? "openai-compatible");
90
+ const model = firstDefined(getShellOverride("MODEL"), fileConfig.model, "test-model") ?? "test-model";
91
+ const openAiBaseUrl = firstDefined(getShellOverride("OPENAI_BASE_URL"), fileConfig.openAiBaseUrl, "http://localhost:1234/v1") ?? "http://localhost:1234/v1";
92
+ const openAiApiKey = provider === "openai-compatible"
93
+ ? (openAiBaseUrl.includes("openrouter.ai")
94
+ ? firstDefined(getSecret("OPENROUTER_API_KEY"), getSecret("OPENAI_API_KEY"))
95
+ : getSecret("OPENAI_API_KEY"))
96
+ : undefined;
54
97
  return {
55
98
  modelProvider: provider,
56
99
  model,
57
- maxSteps: Number(process.env.MAX_STEPS ?? "50"),
58
- maxTokens: Number(process.env.MAX_TOKENS ?? "4096"),
59
- maxContextTokens: Number(process.env.MAX_CONTEXT_TOKENS ?? "32000"),
60
- workspaceRoot: process.cwd(),
61
- commandTimeoutMs: Number(process.env.COMMAND_TIMEOUT_MS ?? "30000"),
62
- maxFileSizeBytes: Number(process.env.MAX_FILE_SIZE_BYTES ?? "1000000"),
100
+ maxSteps: getNumberSetting(getShellOverride("MAX_STEPS"), fileConfig.maxSteps, 50),
101
+ maxTokens: getNumberSetting(getShellOverride("MAX_TOKENS"), fileConfig.maxTokens, 4096),
102
+ maxContextTokens: getNumberSetting(getShellOverride("MAX_CONTEXT_TOKENS"), fileConfig.maxContextTokens, 32000),
103
+ workspaceRoot: repoRoot,
104
+ commandTimeoutMs: getNumberSetting(getShellOverride("COMMAND_TIMEOUT_MS"), fileConfig.commandTimeoutMs, 30000),
105
+ maxFileSizeBytes: getNumberSetting(getShellOverride("MAX_FILE_SIZE_BYTES"), fileConfig.maxFileSizeBytes, 1000000),
63
106
  commandDenylist: [],
64
107
  confirmDestructive: false,
65
- keepRecentMessages: Number(process.env.KEEP_RECENT_MESSAGES ?? "12"),
66
- loopDetectionWindow: Number(process.env.LOOP_DETECTION_WINDOW ?? "6"),
67
- maxToolOutputChars: Number(process.env.MAX_TOOL_OUTPUT_CHARS ?? "8000"),
68
- openAiBaseUrl: process.env.OPENAI_BASE_URL ?? "http://localhost:1234/v1",
69
- ...(process.env.OPENAI_API_KEY ? { openAiApiKey: process.env.OPENAI_API_KEY } : {}),
108
+ keepRecentMessages: getNumberSetting(getShellOverride("KEEP_RECENT_MESSAGES"), fileConfig.keepRecentMessages, 12),
109
+ loopDetectionWindow: getNumberSetting(getShellOverride("LOOP_DETECTION_WINDOW"), fileConfig.loopDetectionWindow, 6),
110
+ maxToolOutputChars: getNumberSetting(getShellOverride("MAX_TOOL_OUTPUT_CHARS"), fileConfig.maxToolOutputChars, 8000),
111
+ openAiBaseUrl,
112
+ ...(openAiApiKey ? { openAiApiKey } : {}),
70
113
  };
71
114
  }
72
115
  /* ------------------------------------------------------------------ */
@@ -94,8 +137,9 @@ export async function loadTasks(tasksDir, args) {
94
137
  /* ------------------------------------------------------------------ */
95
138
  async function main() {
96
139
  const args = parseArgs(process.argv.slice(2));
97
- const config = buildConfig();
98
- const tasksDir = path.resolve(process.cwd(), "benchmarks", "tasks");
140
+ const repoRoot = process.cwd();
141
+ const config = buildConfig({ repoRoot });
142
+ const tasksDir = path.resolve(repoRoot, "benchmarks", "tasks");
99
143
  console.log(`Benchmark runner starting...`);
100
144
  console.log(` Provider: ${config.modelProvider}`);
101
145
  console.log(` Model: ${config.model}`);
@@ -104,19 +148,20 @@ async function main() {
104
148
  console.log(` Tasks: ${tasks.length}`);
105
149
  console.log("");
106
150
  const modelClient = createModelClient(config);
107
- const tools = [
108
- createReadFileTool(config),
109
- createWriteFileTool(config),
110
- createEditFileTool(config),
111
- createSearchTool(config),
112
- createListFilesTool(config),
113
- createRunCommandTool(config),
114
- ];
115
151
  const traces = await runBenchmarkSuite(tasks, {
116
152
  modelClient,
117
153
  config,
118
- tools,
119
154
  variant: args.variant,
155
+ repoRoot,
156
+ isolateWorkspace: true,
157
+ createToolset: async (taskConfig) => {
158
+ const projectIndex = await buildProjectIndex(taskConfig.workspaceRoot);
159
+ const toolRegistry = createToolRegistry(taskConfig, projectIndex);
160
+ return {
161
+ tools: toolRegistry.getDefinitions(),
162
+ projectIndex,
163
+ };
164
+ },
120
165
  onTaskComplete: (taskId, trace) => {
121
166
  const dur = (trace.durationMs / 1000).toFixed(1);
122
167
  console.log(` [done] ${taskId} (${dur}s, ${trace.toolCalls.length} tool calls)`);
@@ -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,7 +10,7 @@ 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,
@@ -46,9 +46,14 @@ export function formatConfigForDisplay(config) {
46
46
  */
47
47
  export function getConfigMissing(config) {
48
48
  const missing = [];
49
+ const isOpenRouter = config.modelProvider === "openai-compatible" &&
50
+ config.openAiBaseUrl.includes("openrouter");
49
51
  if (!config.model) {
50
52
  missing.push("MODEL is not set");
51
53
  }
54
+ if (isOpenRouter && !config.openAiApiKey?.trim()) {
55
+ missing.push("OPENROUTER_API_KEY is not set");
56
+ }
52
57
  if (config.modelProvider === "anthropic" && !process.env.ANTHROPIC_API_KEY) {
53
58
  missing.push("ANTHROPIC_API_KEY is not set");
54
59
  }
@@ -63,8 +68,8 @@ export function getConfigSetupMessage(config) {
63
68
  "minicode is not configured yet. Missing:",
64
69
  ...missing.map((m) => ` - ${m}`),
65
70
  "",
66
- `Set these in ~/.minicode/.env or as environment variables.`,
67
- `Edit ~/.minicode/agent.config.json for non-secret settings.`,
71
+ "Set these in ~/.minicode/.env or as environment variables.",
72
+ "Editable runtime defaults set through the UI or /config are saved back to ~/.minicode/.env.",
68
73
  "",
69
74
  "Example ~/.minicode/.env for a local model:",
70
75
  " MODEL_PROVIDER=openai-compatible",
@@ -117,20 +122,6 @@ function parseBoolean(value, fallback) {
117
122
  }
118
123
  return fallback;
119
124
  }
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
125
  async function loadDotenvFile(envPath) {
135
126
  try {
136
127
  const file = await readFile(envPath, "utf8");
@@ -188,6 +179,24 @@ function parseUserDenylist(patterns) {
188
179
  }
189
180
  return denylist;
190
181
  }
182
+ function parseCommandDenylistEnv(value) {
183
+ if (!value?.trim()) {
184
+ return [];
185
+ }
186
+ const trimmed = value.trim();
187
+ if (trimmed.startsWith("[")) {
188
+ try {
189
+ const parsed = JSON.parse(trimmed);
190
+ if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
191
+ return parseUserDenylist(parsed);
192
+ }
193
+ }
194
+ catch {
195
+ return [];
196
+ }
197
+ }
198
+ return parseUserDenylist(trimmed.split(",").map((pattern) => pattern.trim()).filter(Boolean));
199
+ }
191
200
  function parseModelProvider(value) {
192
201
  const normalized = value?.trim().toLowerCase();
193
202
  if (normalized === "openai-compatible" ||
@@ -198,74 +207,50 @@ function parseModelProvider(value) {
198
207
  }
199
208
  return "anthropic";
200
209
  }
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
210
  async function ensureMinicodeHome(minicodeHome) {
211
211
  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
212
  }
220
213
  export async function loadAgentConfig(cwd = process.cwd(), options = {}) {
221
214
  const minicodeHome = options.minicodeHome ?? MINICODE_HOME;
222
215
  await ensureMinicodeHome(minicodeHome);
223
- const homeConfigPath = path.join(minicodeHome, "agent.config.json");
224
- const fileConfig = await loadConfigFile(homeConfigPath);
225
216
  const env = (await resolveConfigEnv({ minicodeHome })).values;
226
- const rawWorkspaceRoot = env.WORKSPACE_ROOT ?? fileConfig.workspaceRoot ?? cwd;
217
+ const rawWorkspaceRoot = env.WORKSPACE_ROOT ?? cwd;
227
218
  const workspaceRoot = path.resolve(cwd, rawWorkspaceRoot);
228
219
  const commandDenylist = [
229
220
  ...DEFAULT_COMMAND_DENYLIST,
230
- ...parseUserDenylist(fileConfig.commandDenylist),
221
+ ...parseCommandDenylistEnv(env.COMMAND_DENYLIST),
231
222
  ];
232
- const rawBaseUrl = env.OPENAI_BASE_URL ??
233
- fileConfig.openAiBaseUrl ??
234
- "http://localhost:1234/v1";
223
+ const rawBaseUrl = env.OPENAI_BASE_URL ?? "http://localhost:1234/v1";
235
224
  const isOpenRouter = rawBaseUrl.includes("openrouter");
236
225
  const openAiApiKey = isOpenRouter
237
- ? (env.OPENROUTER_API_KEY ??
238
- env.OPENAI_API_KEY ??
239
- fileConfig.openAiApiKey)
240
- : (env.OPENAI_API_KEY ?? fileConfig.openAiApiKey);
226
+ ? (env.OPENROUTER_API_KEY ?? env.OPENAI_API_KEY)
227
+ : env.OPENAI_API_KEY;
241
228
  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),
229
+ modelProvider: parseModelProvider(env.MODEL_PROVIDER ?? "openai-compatible"),
230
+ model: env.MODEL ?? "",
231
+ maxSteps: parseNumber(env.MAX_STEPS, 50),
232
+ maxTokens: parseNumber(env.MAX_TOKENS, 4096),
233
+ maxContextTokens: parseNumber(env.MAX_CONTEXT_TOKENS, 32_000),
249
234
  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),
235
+ commandTimeoutMs: parseNumber(env.COMMAND_TIMEOUT_MS, 30_000),
236
+ maxFileSizeBytes: parseNumber(env.MAX_FILE_SIZE_BYTES, 1_000_000),
252
237
  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),
238
+ confirmDestructive: parseBoolean(env.CONFIRM_DESTRUCTIVE, true),
239
+ keepRecentMessages: parseNumber(env.KEEP_RECENT_MESSAGES, 12),
240
+ loopDetectionWindow: parseNumber(env.LOOP_DETECTION_WINDOW, 6),
241
+ maxToolOutputChars: parseNumber(env.MAX_TOOL_OUTPUT_CHARS, 8_000),
257
242
  openAiBaseUrl: rawBaseUrl,
258
243
  ...(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 }
244
+ enableFileReadDedup: parseBoolean(env.ENABLE_FILE_READ_DEDUP, true),
245
+ enableAdaptiveKeepRecent: parseBoolean(env.ENABLE_ADAPTIVE_KEEP_RECENT, true),
246
+ enableToolOutputTruncation: parseBoolean(env.ENABLE_TOOL_OUTPUT_TRUNCATION, true),
247
+ compactionThreshold: parseNumber(env.COMPACTION_THRESHOLD, 0.8),
248
+ ...(env.COMPACTION_MODEL
249
+ ? { compactionModel: env.COMPACTION_MODEL }
265
250
  : {}),
266
- enableDynamicPrompt: parseBoolean(env.ENABLE_DYNAMIC_PROMPT, fileConfig.enableDynamicPrompt ?? true),
251
+ enableDynamicPrompt: parseBoolean(env.ENABLE_DYNAMIC_PROMPT, true),
267
252
  ...(() => {
268
- const effort = parseReasoningEffort(env.REASONING_EFFORT ?? fileConfig.reasoningEffort);
253
+ const effort = parseReasoningEffort(env.REASONING_EFFORT);
269
254
  return effort ? { reasoningEffort: effort } : {};
270
255
  })(),
271
256
  };