@sean.holung/minicode 0.3.5 → 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.
- package/README.md +21 -45
- package/dist/src/agent/config.js +51 -66
- package/dist/src/agent/editable-config.js +50 -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/server.js +161 -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/web/app.js +494 -56
- package/dist/src/web/index.html +68 -6
- package/dist/src/web/style.css +208 -1
- package/dist/tests/config-api.test.js +5 -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 +12 -4
- package/dist/tests/editable-config.test.js +15 -12
- package/dist/tests/graph-onboarding.test.js +10 -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/serve.integration.test.js +229 -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/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 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,6 +196,7 @@ 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 |
|
|
189
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. |
|
|
@@ -202,44 +215,7 @@ Nothing is written inside your workspace; config and cache live under `~/.minico
|
|
|
202
215
|
| `REASONING_EFFORT` | No | unset | Reasoning level for providers that support it. Valid values: `xhigh`, `high`, `medium`, `low`, `minimal`, `none` |
|
|
203
216
|
|
|
204
217
|
|
|
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`
|
|
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.
|
|
243
219
|
|
|
244
220
|
## Usage
|
|
245
221
|
|
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,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
|
|
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
|
-
|
|
67
|
-
|
|
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 ??
|
|
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
|
-
...
|
|
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
|
-
|
|
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 ??
|
|
243
|
-
model: env.MODEL ??
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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,
|
|
251
|
-
maxFileSizeBytes: parseNumber(env.MAX_FILE_SIZE_BYTES,
|
|
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,
|
|
254
|
-
keepRecentMessages: parseNumber(env.KEEP_RECENT_MESSAGES,
|
|
255
|
-
loopDetectionWindow: parseNumber(env.LOOP_DETECTION_WINDOW,
|
|
256
|
-
maxToolOutputChars: parseNumber(env.MAX_TOOL_OUTPUT_CHARS,
|
|
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,
|
|
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
|
|
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,
|
|
251
|
+
enableDynamicPrompt: parseBoolean(env.ENABLE_DYNAMIC_PROMPT, true),
|
|
267
252
|
...(() => {
|
|
268
|
-
const effort = parseReasoningEffort(env.REASONING_EFFORT
|
|
253
|
+
const effort = parseReasoningEffort(env.REASONING_EFFORT);
|
|
269
254
|
return effort ? { reasoningEffort: effort } : {};
|
|
270
255
|
})(),
|
|
271
256
|
};
|
|
@@ -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,102 @@ 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
|
},
|
|
42
37
|
{
|
|
43
38
|
key: "maxContextTokens",
|
|
44
|
-
fileKey: "maxContextTokens",
|
|
45
39
|
envVar: "MAX_CONTEXT_TOKENS",
|
|
46
40
|
type: "number",
|
|
47
41
|
description: "Estimated prompt-context budget before compaction",
|
|
48
42
|
},
|
|
49
43
|
{
|
|
50
44
|
key: "commandTimeoutMs",
|
|
51
|
-
fileKey: "commandTimeout",
|
|
52
45
|
envVar: "COMMAND_TIMEOUT_MS",
|
|
53
46
|
type: "number",
|
|
54
47
|
description: "Shell command timeout in milliseconds",
|
|
55
48
|
},
|
|
56
49
|
{
|
|
57
50
|
key: "maxFileSizeBytes",
|
|
58
|
-
fileKey: "maxFileSizeBytes",
|
|
59
51
|
envVar: "MAX_FILE_SIZE_BYTES",
|
|
60
52
|
type: "number",
|
|
61
53
|
description: "Maximum file size allowed for read/edit tools",
|
|
62
54
|
},
|
|
63
55
|
{
|
|
64
56
|
key: "confirmDestructive",
|
|
65
|
-
fileKey: "confirmDestructive",
|
|
66
57
|
envVar: "CONFIRM_DESTRUCTIVE",
|
|
67
58
|
type: "boolean",
|
|
68
59
|
description: "Whether destructive commands require confirmation",
|
|
69
60
|
},
|
|
70
61
|
{
|
|
71
62
|
key: "keepRecentMessages",
|
|
72
|
-
fileKey: "keepRecentMessages",
|
|
73
63
|
envVar: "KEEP_RECENT_MESSAGES",
|
|
74
64
|
type: "number",
|
|
75
65
|
description: "Recent messages preserved when trimming session history",
|
|
76
66
|
},
|
|
77
67
|
{
|
|
78
68
|
key: "loopDetectionWindow",
|
|
79
|
-
fileKey: "loopDetectionWindow",
|
|
80
69
|
envVar: "LOOP_DETECTION_WINDOW",
|
|
81
70
|
type: "number",
|
|
82
71
|
description: "Window size for repeated-tool-call loop detection",
|
|
83
72
|
},
|
|
84
73
|
{
|
|
85
74
|
key: "maxToolOutputChars",
|
|
86
|
-
fileKey: "maxToolOutputChars",
|
|
87
75
|
envVar: "MAX_TOOL_OUTPUT_CHARS",
|
|
88
76
|
type: "number",
|
|
89
77
|
description: "Maximum tool output retained before truncation",
|
|
90
78
|
},
|
|
91
79
|
{
|
|
92
80
|
key: "openAiBaseUrl",
|
|
93
|
-
fileKey: "openAiBaseUrl",
|
|
94
81
|
envVar: "OPENAI_BASE_URL",
|
|
95
82
|
type: "string",
|
|
96
83
|
description: "Base URL for OpenAI-compatible providers",
|
|
97
84
|
},
|
|
98
85
|
{
|
|
99
86
|
key: "enableFileReadDedup",
|
|
100
|
-
fileKey: "enableFileReadDedup",
|
|
101
87
|
envVar: "ENABLE_FILE_READ_DEDUP",
|
|
102
88
|
type: "boolean",
|
|
103
89
|
description: "Deduplicate repeated file reads in prompt context",
|
|
104
90
|
},
|
|
105
91
|
{
|
|
106
92
|
key: "enableAdaptiveKeepRecent",
|
|
107
|
-
fileKey: "enableAdaptiveKeepRecent",
|
|
108
93
|
envVar: "ENABLE_ADAPTIVE_KEEP_RECENT",
|
|
109
94
|
type: "boolean",
|
|
110
95
|
description: "Adjust recent-message retention based on context pressure",
|
|
111
96
|
},
|
|
112
97
|
{
|
|
113
98
|
key: "enableToolOutputTruncation",
|
|
114
|
-
fileKey: "enableToolOutputTruncation",
|
|
115
99
|
envVar: "ENABLE_TOOL_OUTPUT_TRUNCATION",
|
|
116
100
|
type: "boolean",
|
|
117
101
|
description: "Truncate oversized tool output before storing it in session history",
|
|
118
102
|
},
|
|
119
103
|
{
|
|
120
104
|
key: "compactionThreshold",
|
|
121
|
-
fileKey: "compactionThreshold",
|
|
122
105
|
envVar: "COMPACTION_THRESHOLD",
|
|
123
106
|
type: "number",
|
|
124
107
|
description: "Compaction threshold ratio used before a turn starts",
|
|
125
108
|
},
|
|
126
109
|
{
|
|
127
110
|
key: "compactionModel",
|
|
128
|
-
fileKey: "compactionModel",
|
|
129
111
|
envVar: "COMPACTION_MODEL",
|
|
130
112
|
type: "string",
|
|
131
113
|
description: "Optional model id used for LLM-based compaction",
|
|
132
114
|
},
|
|
133
115
|
{
|
|
134
116
|
key: "reasoningEffort",
|
|
135
|
-
fileKey: "reasoningEffort",
|
|
136
117
|
envVar: "REASONING_EFFORT",
|
|
137
118
|
type: "enum",
|
|
138
119
|
values: REASONING_VALUES,
|
|
@@ -140,7 +121,6 @@ export const EDITABLE_CONFIG_DEFINITIONS = [
|
|
|
140
121
|
},
|
|
141
122
|
{
|
|
142
123
|
key: "enableDynamicPrompt",
|
|
143
|
-
fileKey: "enableDynamicPrompt",
|
|
144
124
|
envVar: "ENABLE_DYNAMIC_PROMPT",
|
|
145
125
|
type: "boolean",
|
|
146
126
|
description: "Toggle project-aware dynamic prompt generation",
|
|
@@ -194,9 +174,6 @@ function normalizePersistedValue(value) {
|
|
|
194
174
|
}
|
|
195
175
|
return null;
|
|
196
176
|
}
|
|
197
|
-
function isEmptyConfigFile(config) {
|
|
198
|
-
return Object.keys(config).length === 0;
|
|
199
|
-
}
|
|
200
177
|
export function listEditableConfigDefinitions() {
|
|
201
178
|
return EDITABLE_CONFIG_DEFINITIONS;
|
|
202
179
|
}
|
|
@@ -223,10 +200,25 @@ export function formatPersistedConfigValue(value) {
|
|
|
223
200
|
return String(value);
|
|
224
201
|
}
|
|
225
202
|
export function getGlobalConfigPath(minicodeHome = MINICODE_HOME) {
|
|
226
|
-
return
|
|
203
|
+
return getHomeEnvPath(minicodeHome);
|
|
227
204
|
}
|
|
228
205
|
export async function loadPersistedConfig(minicodeHome = MINICODE_HOME) {
|
|
229
|
-
return
|
|
206
|
+
return loadHomeEnvValues(minicodeHome);
|
|
207
|
+
}
|
|
208
|
+
function readPersistedEnvValue(definition, persisted) {
|
|
209
|
+
const rawValue = persisted[definition.envVar];
|
|
210
|
+
if (rawValue === undefined) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
return parseEditableValue(definition, rawValue);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return rawValue;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function serializeEditableValue(value) {
|
|
221
|
+
return String(value);
|
|
230
222
|
}
|
|
231
223
|
export async function buildStructuredConfigPayload(config, minicodeHome = MINICODE_HOME) {
|
|
232
224
|
const configPath = getGlobalConfigPath(minicodeHome);
|
|
@@ -235,10 +227,11 @@ export async function buildStructuredConfigPayload(config, minicodeHome = MINICO
|
|
|
235
227
|
return {
|
|
236
228
|
configPath,
|
|
237
229
|
entries: EDITABLE_CONFIG_DEFINITIONS.map((definition) => {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
230
|
+
const envSource = env.sources[definition.envVar] === "process"
|
|
231
|
+
? "process"
|
|
232
|
+
: null;
|
|
233
|
+
const envValue = envSource === "process"
|
|
234
|
+
? (env.values[definition.envVar] ?? null)
|
|
242
235
|
: null;
|
|
243
236
|
return {
|
|
244
237
|
key: definition.key,
|
|
@@ -247,11 +240,11 @@ export async function buildStructuredConfigPayload(config, minicodeHome = MINICO
|
|
|
247
240
|
envVar: definition.envVar,
|
|
248
241
|
...(definition.values ? { values: definition.values } : {}),
|
|
249
242
|
effectiveValue: normalizePersistedValue(config[definition.key]),
|
|
250
|
-
persistedValue:
|
|
251
|
-
envValue
|
|
243
|
+
persistedValue: readPersistedEnvValue(definition, persisted),
|
|
244
|
+
envValue,
|
|
252
245
|
envSource,
|
|
253
|
-
envSourcePath,
|
|
254
|
-
overriddenByEnv:
|
|
246
|
+
envSourcePath: envSource === "process" ? null : null,
|
|
247
|
+
overriddenByEnv: envSource === "process",
|
|
255
248
|
};
|
|
256
249
|
}),
|
|
257
250
|
};
|
|
@@ -260,26 +253,25 @@ export async function setPersistedConfigValue(options) {
|
|
|
260
253
|
const minicodeHome = options.minicodeHome ?? MINICODE_HOME;
|
|
261
254
|
const definition = getEditableConfigDefinition(options.key);
|
|
262
255
|
const configPath = getGlobalConfigPath(minicodeHome);
|
|
263
|
-
const nextFile = await loadConfigFile(configPath);
|
|
264
256
|
const storedValue = parseEditableValue(definition, options.rawValue);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
257
|
+
await upsertHomeEnvValues({
|
|
258
|
+
minicodeHome,
|
|
259
|
+
values: {
|
|
260
|
+
[definition.envVar]: serializeEditableValue(storedValue),
|
|
261
|
+
},
|
|
262
|
+
});
|
|
268
263
|
return { path: configPath, storedValue };
|
|
269
264
|
}
|
|
270
265
|
export async function unsetPersistedConfigValue(options) {
|
|
271
266
|
const minicodeHome = options.minicodeHome ?? MINICODE_HOME;
|
|
272
267
|
const definition = getEditableConfigDefinition(options.key);
|
|
273
268
|
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
|
-
}
|
|
269
|
+
await upsertHomeEnvValues({
|
|
270
|
+
minicodeHome,
|
|
271
|
+
values: {
|
|
272
|
+
[definition.envVar]: null,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
283
275
|
return { path: configPath };
|
|
284
276
|
}
|
|
285
277
|
export async function applyPersistedConfigUpdates(options) {
|
|
@@ -291,22 +283,22 @@ export async function applyPersistedConfigUpdates(options) {
|
|
|
291
283
|
return { key: rawKey, value };
|
|
292
284
|
});
|
|
293
285
|
const saved = [];
|
|
286
|
+
const envUpdates = {};
|
|
294
287
|
for (const item of planned) {
|
|
288
|
+
const definition = getEditableConfigDefinition(item.key);
|
|
295
289
|
if (item.value === null) {
|
|
296
|
-
|
|
297
|
-
key: item.key,
|
|
298
|
-
minicodeHome,
|
|
299
|
-
});
|
|
290
|
+
envUpdates[definition.envVar] = null;
|
|
300
291
|
saved.push({ key: item.key, value: null });
|
|
301
292
|
continue;
|
|
302
293
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
minicodeHome,
|
|
307
|
-
});
|
|
308
|
-
saved.push({ key: item.key, value: item.value });
|
|
294
|
+
const storedValue = parseEditableValue(definition, String(item.value));
|
|
295
|
+
envUpdates[definition.envVar] = serializeEditableValue(storedValue);
|
|
296
|
+
saved.push({ key: item.key, value: storedValue });
|
|
309
297
|
}
|
|
298
|
+
await upsertHomeEnvValues({
|
|
299
|
+
minicodeHome,
|
|
300
|
+
values: envUpdates,
|
|
301
|
+
});
|
|
310
302
|
return {
|
|
311
303
|
path: getGlobalConfigPath(minicodeHome),
|
|
312
304
|
saved,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import dotenv from "dotenv";
|
|
4
|
+
import { MINICODE_HOME } from "./config.js";
|
|
5
|
+
export function getHomeEnvPath(minicodeHome = MINICODE_HOME) {
|
|
6
|
+
return path.join(minicodeHome, ".env");
|
|
7
|
+
}
|
|
8
|
+
export async function loadHomeEnvValues(minicodeHome = MINICODE_HOME) {
|
|
9
|
+
const envPath = getHomeEnvPath(minicodeHome);
|
|
10
|
+
try {
|
|
11
|
+
const existing = await readFile(envPath, "utf8");
|
|
12
|
+
return dotenv.parse(existing);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function formatEnvValue(value) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
export async function upsertHomeEnvValues(options) {
|
|
22
|
+
const minicodeHome = options.minicodeHome ?? MINICODE_HOME;
|
|
23
|
+
const envPath = getHomeEnvPath(minicodeHome);
|
|
24
|
+
await mkdir(path.dirname(envPath), { recursive: true });
|
|
25
|
+
let existing = "";
|
|
26
|
+
try {
|
|
27
|
+
existing = await readFile(envPath, "utf8");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
existing = "";
|
|
31
|
+
}
|
|
32
|
+
const pending = new Map(Object.entries(options.values));
|
|
33
|
+
const managedKeys = new Set(pending.keys());
|
|
34
|
+
const assignmentPattern = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
35
|
+
const normalizedLines = existing === ""
|
|
36
|
+
? []
|
|
37
|
+
: existing.split(/\r?\n/);
|
|
38
|
+
const nextLines = [];
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
for (const line of normalizedLines) {
|
|
41
|
+
const match = line.match(assignmentPattern);
|
|
42
|
+
if (!match) {
|
|
43
|
+
nextLines.push(line);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const key = match[1];
|
|
47
|
+
if (!managedKeys.has(key)) {
|
|
48
|
+
nextLines.push(line);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (seen.has(key)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const nextValue = pending.get(key);
|
|
55
|
+
if (nextValue !== null) {
|
|
56
|
+
nextLines.push(`${key}=${formatEnvValue(nextValue)}`);
|
|
57
|
+
}
|
|
58
|
+
seen.add(key);
|
|
59
|
+
pending.delete(key);
|
|
60
|
+
}
|
|
61
|
+
const pendingEntries = [...pending.entries()].filter((entry) => entry[1] !== null);
|
|
62
|
+
if (pendingEntries.length > 0 && nextLines.length > 0 && nextLines[nextLines.length - 1] !== "") {
|
|
63
|
+
nextLines.push("");
|
|
64
|
+
}
|
|
65
|
+
for (const [key, value] of pendingEntries) {
|
|
66
|
+
nextLines.push(`${key}=${formatEnvValue(value)}`);
|
|
67
|
+
}
|
|
68
|
+
const fileContent = `${nextLines.join("\n").replace(/\n+$/, "")}\n`;
|
|
69
|
+
await writeFile(envPath, fileContent, "utf8");
|
|
70
|
+
return {
|
|
71
|
+
path: envPath,
|
|
72
|
+
updatedKeys: Object.keys(options.values),
|
|
73
|
+
};
|
|
74
|
+
}
|