@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.
- package/README.md +25 -47
- package/dist/scripts/run-benchmarks.js +73 -28
- 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/benchmark/runner.js +142 -59
- package/dist/src/cli/config-slash-command.js +15 -13
- package/dist/src/indexer/project-index.js +49 -13
- package/dist/src/serve/agent-bridge.js +99 -31
- package/dist/src/serve/mcp-server.js +70 -21
- package/dist/src/serve/server.js +198 -8
- 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/graph-symbols.js +82 -0
- package/dist/src/shared/symbol-resolution.js +33 -0
- package/dist/src/tools/find-path.js +15 -6
- package/dist/src/tools/find-references.js +7 -2
- package/dist/src/tools/get-dependencies.js +8 -3
- package/dist/src/tools/read-symbol.js +9 -3
- package/dist/src/tools/registry.js +4 -1
- package/dist/src/tools/search-code-map.js +18 -3
- package/dist/src/web/app.js +646 -87
- package/dist/src/web/index.html +68 -6
- package/dist/src/web/style.css +208 -1
- package/dist/tests/benchmark-harness.test.js +100 -0
- 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/file-tools.test.js +34 -1
- package/dist/tests/find-path.test.js +43 -2
- package/dist/tests/find-references.test.js +49 -0
- package/dist/tests/get-dependencies.test.js +23 -0
- 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/graph-symbols.test.js +45 -0
- package/dist/tests/home-env.test.js +56 -0
- package/dist/tests/indexer.test.js +6 -0
- package/dist/tests/read-symbol.test.js +35 -0
- package/dist/tests/request-tracker.test.js +15 -0
- package/dist/tests/run-benchmarks.test.js +117 -33
- package/dist/tests/search-code-map.test.js +2 -0
- package/dist/tests/serve.integration.test.js +338 -9
- package/dist/tests/session-preview.test.js +56 -0
- package/dist/tests/session-ui.test.js +4 -0
- package/dist/tests/settings-ui.test.js +18 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +2 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +3 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts +3 -0
- package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js +4 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts +11 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +4 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/search.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js +16 -8
- package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js +19 -2
- package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.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,13 +1,27 @@
|
|
|
1
1
|
# minicode
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
* —
|
|
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,
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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:
|
|
58
|
-
maxTokens:
|
|
59
|
-
maxContextTokens:
|
|
60
|
-
workspaceRoot:
|
|
61
|
-
commandTimeoutMs:
|
|
62
|
-
maxFileSizeBytes:
|
|
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:
|
|
66
|
-
loopDetectionWindow:
|
|
67
|
-
maxToolOutputChars:
|
|
68
|
-
openAiBaseUrl
|
|
69
|
-
...(
|
|
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
|
|
98
|
-
const
|
|
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)`);
|
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
|
};
|