@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.
- package/README.md +22 -45
- package/dist/scripts/run-benchmarks.js +1 -0
- package/dist/src/agent/config.js +53 -66
- package/dist/src/agent/editable-config.js +56 -58
- package/dist/src/agent/home-env.js +74 -0
- package/dist/src/cli/config-slash-command.js +15 -13
- package/dist/src/serve/agent-bridge.js +87 -28
- package/dist/src/serve/mcp-server.js +19 -13
- package/dist/src/serve/server.js +190 -4
- package/dist/src/session/session-preview.js +14 -0
- package/dist/src/shared/graph-search.js +80 -0
- package/dist/src/shared/graph-selection.js +40 -0
- package/dist/src/shared/symbol-search.js +156 -0
- package/dist/src/tools/search-code-map.js +27 -35
- package/dist/src/web/app.js +582 -64
- package/dist/src/web/index.html +84 -6
- package/dist/src/web/style.css +256 -1
- package/dist/tests/config-api.test.js +10 -5
- package/dist/tests/config-integration.test.js +130 -56
- package/dist/tests/config-slash-command.test.js +12 -11
- package/dist/tests/config.test.js +21 -4
- package/dist/tests/editable-config.test.js +15 -12
- package/dist/tests/graph-onboarding.test.js +22 -1
- package/dist/tests/graph-search.test.js +66 -0
- package/dist/tests/graph-selection.test.js +58 -0
- package/dist/tests/home-env.test.js +56 -0
- package/dist/tests/mcp-and-plugin.test.js +3 -0
- package/dist/tests/search-code-map.test.js +9 -0
- package/dist/tests/serve.integration.test.js +255 -6
- package/dist/tests/session-preview.test.js +56 -0
- package/dist/tests/session-ui.test.js +2 -0
- package/dist/tests/settings-ui.test.js +18 -0
- package/dist/tests/system-prompt.test.js +1 -0
- package/dist/tests/test-utils.js +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts +8 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +143 -27
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/model-client-openai.test.js +87 -0
- package/node_modules/@minicode/agent-sdk/dist/tests/model-client-openai.test.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.js +1 -0
- package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- 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
|
|
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
|
|
179
|
+
Configuration can come from:
|
|
168
180
|
|
|
169
|
-
1. `~/.minicode/.env` — User-level defaults (API keys, model,
|
|
170
|
-
2.
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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),
|
package/dist/src/agent/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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 ??
|
|
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
|
-
...
|
|
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
|
-
|
|
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 ??
|
|
243
|
-
model: env.MODEL ??
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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,
|
|
251
|
-
maxFileSizeBytes: parseNumber(env.MAX_FILE_SIZE_BYTES,
|
|
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,
|
|
254
|
-
keepRecentMessages: parseNumber(env.KEEP_RECENT_MESSAGES,
|
|
255
|
-
loopDetectionWindow: parseNumber(env.LOOP_DETECTION_WINDOW,
|
|
256
|
-
maxToolOutputChars: parseNumber(env.MAX_TOOL_OUTPUT_CHARS,
|
|
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,
|
|
260
|
-
enableAdaptiveKeepRecent: parseBoolean(env.ENABLE_ADAPTIVE_KEEP_RECENT,
|
|
261
|
-
enableToolOutputTruncation: parseBoolean(env.ENABLE_TOOL_OUTPUT_TRUNCATION,
|
|
262
|
-
compactionThreshold: parseNumber(env.COMPACTION_THRESHOLD,
|
|
263
|
-
...(env.COMPACTION_MODEL
|
|
264
|
-
? { compactionModel: env.COMPACTION_MODEL
|
|
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,
|
|
253
|
+
enableDynamicPrompt: parseBoolean(env.ENABLE_DYNAMIC_PROMPT, true),
|
|
267
254
|
...(() => {
|
|
268
|
-
const effort = parseReasoningEffort(env.REASONING_EFFORT
|
|
255
|
+
const effort = parseReasoningEffort(env.REASONING_EFFORT);
|
|
269
256
|
return effort ? { reasoningEffort: effort } : {};
|
|
270
257
|
})(),
|
|
271
258
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
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
|
|
209
|
+
return getHomeEnvPath(minicodeHome);
|
|
227
210
|
}
|
|
228
211
|
export async function loadPersistedConfig(minicodeHome = MINICODE_HOME) {
|
|
229
|
-
return
|
|
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
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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:
|
|
251
|
-
envValue
|
|
249
|
+
persistedValue: readPersistedEnvValue(definition, persisted),
|
|
250
|
+
envValue,
|
|
252
251
|
envSource,
|
|
253
|
-
envSourcePath,
|
|
254
|
-
overriddenByEnv:
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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,
|