@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
|
@@ -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
|
+
}
|
|
@@ -5,7 +5,17 @@
|
|
|
5
5
|
* over tool-call instrumentation and trace capture.
|
|
6
6
|
*/
|
|
7
7
|
import { execSync } from "node:child_process";
|
|
8
|
+
import { cp, mkdtemp, rm } from "node:fs/promises";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
8
11
|
import { CodingAgent, Session, ToolRegistry, } from "@minicode/agent-sdk";
|
|
12
|
+
const COPY_SKIP_NAMES = new Set([
|
|
13
|
+
".git",
|
|
14
|
+
"node_modules",
|
|
15
|
+
"dist",
|
|
16
|
+
"build",
|
|
17
|
+
"coverage",
|
|
18
|
+
]);
|
|
9
19
|
function getGitCommitSha() {
|
|
10
20
|
try {
|
|
11
21
|
return execSync("git rev-parse HEAD", { encoding: "utf8" }).trim();
|
|
@@ -19,73 +29,146 @@ const STRUCTURAL_TOOLS = new Set([
|
|
|
19
29
|
"find_references",
|
|
20
30
|
"get_dependencies",
|
|
21
31
|
"search_code_map",
|
|
32
|
+
"find_path",
|
|
22
33
|
]);
|
|
34
|
+
function sanitizeTaskId(taskId) {
|
|
35
|
+
return taskId.replace(/[^a-z0-9-_]+/gi, "-");
|
|
36
|
+
}
|
|
37
|
+
function resolveSourceWorkspaceRoot(task, options) {
|
|
38
|
+
if (!task.workspaceRoot) {
|
|
39
|
+
return path.resolve(options.config.workspaceRoot);
|
|
40
|
+
}
|
|
41
|
+
const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
|
|
42
|
+
return path.resolve(repoRoot, task.workspaceRoot);
|
|
43
|
+
}
|
|
44
|
+
function shouldCopyPath(src) {
|
|
45
|
+
const name = path.basename(src);
|
|
46
|
+
return !COPY_SKIP_NAMES.has(name);
|
|
47
|
+
}
|
|
48
|
+
async function prepareTaskWorkspace(task, options) {
|
|
49
|
+
const sourceWorkspaceRoot = resolveSourceWorkspaceRoot(task, options);
|
|
50
|
+
if (options.isolateWorkspace === false) {
|
|
51
|
+
return {
|
|
52
|
+
sourceWorkspaceRoot,
|
|
53
|
+
workspaceRoot: sourceWorkspaceRoot,
|
|
54
|
+
cleanup: async () => { },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const tempRoot = await mkdtemp(path.join(tmpdir(), "minicode-benchmark-"));
|
|
58
|
+
const isolatedWorkspaceRoot = path.join(tempRoot, sanitizeTaskId(task.id));
|
|
59
|
+
await cp(sourceWorkspaceRoot, isolatedWorkspaceRoot, {
|
|
60
|
+
recursive: true,
|
|
61
|
+
filter: shouldCopyPath,
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
sourceWorkspaceRoot,
|
|
65
|
+
workspaceRoot: isolatedWorkspaceRoot,
|
|
66
|
+
cleanup: async () => {
|
|
67
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function getTrackedSymbolNames(toolName, input) {
|
|
72
|
+
if (toolName === "find_path") {
|
|
73
|
+
const names = [input.from, input.to]
|
|
74
|
+
.filter((value) => typeof value === "string" && value.length > 0);
|
|
75
|
+
return [...new Set(names)];
|
|
76
|
+
}
|
|
77
|
+
const name = input.symbol ?? input.symbolName ?? input.name ?? input.query;
|
|
78
|
+
return typeof name === "string" && name.length > 0 ? [name] : [];
|
|
79
|
+
}
|
|
80
|
+
function trackStructuralFileReads(toolName, projectIndex, input, filesRead) {
|
|
81
|
+
if (!projectIndex || !STRUCTURAL_TOOLS.has(toolName)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
for (const symbolName of getTrackedSymbolNames(toolName, input)) {
|
|
85
|
+
const symbol = projectIndex.getSymbol(symbolName);
|
|
86
|
+
if (symbol) {
|
|
87
|
+
filesRead.add(symbol.filePath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
23
91
|
/**
|
|
24
92
|
* Run a single benchmark task and return the captured trace.
|
|
25
93
|
*/
|
|
26
94
|
export async function runBenchmarkTask(task, options) {
|
|
95
|
+
const workspace = await prepareTaskWorkspace(task, options);
|
|
27
96
|
const captured = [];
|
|
28
97
|
const filesRead = new Set();
|
|
29
98
|
const symbolsQueried = new Set();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
99
|
+
try {
|
|
100
|
+
const taskConfig = {
|
|
101
|
+
...options.config,
|
|
102
|
+
workspaceRoot: workspace.workspaceRoot,
|
|
103
|
+
};
|
|
104
|
+
const toolset = options.createToolset
|
|
105
|
+
? await options.createToolset(taskConfig, task)
|
|
106
|
+
: { tools: options.tools ?? [] };
|
|
107
|
+
// Wrap each tool to capture calls
|
|
108
|
+
const instrumentedTools = toolset.tools.map((tool) => ({
|
|
109
|
+
...tool,
|
|
110
|
+
execute: async (input) => {
|
|
111
|
+
const start = performance.now();
|
|
112
|
+
const output = await tool.execute(input);
|
|
113
|
+
const durationMs = performance.now() - start;
|
|
114
|
+
captured.push({
|
|
115
|
+
name: tool.name,
|
|
116
|
+
input,
|
|
117
|
+
output: output.length > 2000 ? output.slice(0, 2000) + "…[truncated]" : output,
|
|
118
|
+
durationMs,
|
|
119
|
+
});
|
|
120
|
+
if (tool.name === "read_file") {
|
|
121
|
+
const filePath = input.path ?? input.file_path ?? input.filePath;
|
|
122
|
+
if (typeof filePath === "string") {
|
|
123
|
+
filesRead.add(filePath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
trackStructuralFileReads(tool.name, toolset.projectIndex, input, filesRead);
|
|
127
|
+
if (STRUCTURAL_TOOLS.has(tool.name)) {
|
|
128
|
+
for (const symbolName of getTrackedSymbolNames(tool.name, input)) {
|
|
129
|
+
symbolsQueried.add(symbolName);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return output;
|
|
133
|
+
},
|
|
134
|
+
}));
|
|
135
|
+
const registry = new ToolRegistry(instrumentedTools);
|
|
136
|
+
const session = new Session();
|
|
137
|
+
const agent = new CodingAgent({
|
|
138
|
+
config: taskConfig,
|
|
139
|
+
modelClient: options.modelClient,
|
|
140
|
+
toolRegistry: registry,
|
|
141
|
+
session,
|
|
142
|
+
});
|
|
143
|
+
const startedAt = new Date().toISOString();
|
|
144
|
+
const start = performance.now();
|
|
145
|
+
const { text, usage } = await agent.runTurn(task.prompt);
|
|
146
|
+
const durationMs = performance.now() - start;
|
|
147
|
+
const trace = {
|
|
148
|
+
taskId: task.id,
|
|
149
|
+
model: taskConfig.model,
|
|
150
|
+
variant: options.variant,
|
|
151
|
+
commitSha: getGitCommitSha(),
|
|
152
|
+
sourceWorkspaceRoot: workspace.sourceWorkspaceRoot,
|
|
153
|
+
workspaceRoot: workspace.workspaceRoot,
|
|
154
|
+
response: text,
|
|
155
|
+
toolCalls: captured,
|
|
156
|
+
filesRead: [...filesRead],
|
|
157
|
+
symbolsQueried: [...symbolsQueried],
|
|
158
|
+
usage: {
|
|
159
|
+
inputTokens: usage?.inputTokens ?? 0,
|
|
160
|
+
outputTokens: usage?.outputTokens ?? 0,
|
|
161
|
+
totalTokens: (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0),
|
|
162
|
+
},
|
|
163
|
+
durationMs,
|
|
164
|
+
startedAt,
|
|
165
|
+
};
|
|
166
|
+
options.onTaskComplete?.(task.id, trace);
|
|
167
|
+
return trace;
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
await workspace.cleanup();
|
|
171
|
+
}
|
|
89
172
|
}
|
|
90
173
|
/**
|
|
91
174
|
* Run all provided benchmark tasks sequentially.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatConfigForDisplay, MINICODE_HOME, resolveConfigEnv } from "../agent/config.js";
|
|
2
|
-
import { formatPersistedConfigValue, getEditableConfigDefinition, getEffectiveEditableConfigValue, isEditableConfigKey, listEditableConfigDefinitions,
|
|
2
|
+
import { buildStructuredConfigPayload, formatPersistedConfigValue, getEditableConfigDefinition, getEffectiveEditableConfigValue, isEditableConfigKey, listEditableConfigDefinitions, setPersistedConfigValue, unsetPersistedConfigValue, } from "../agent/editable-config.js";
|
|
3
3
|
function renderUsage() {
|
|
4
4
|
return [
|
|
5
5
|
'Usage:',
|
|
@@ -12,7 +12,7 @@ function renderUsage() {
|
|
|
12
12
|
}
|
|
13
13
|
function renderEditableKeys() {
|
|
14
14
|
const lines = [
|
|
15
|
-
"Editable config keys (persisted in ~/.minicode
|
|
15
|
+
"Editable config keys (persisted in ~/.minicode/.env; exported shell environment variables take precedence):",
|
|
16
16
|
];
|
|
17
17
|
for (const definition of listEditableConfigDefinitions()) {
|
|
18
18
|
const valueHint = definition.type === "enum"
|
|
@@ -21,7 +21,7 @@ function renderEditableKeys() {
|
|
|
21
21
|
lines.push(` ${definition.key} ${valueHint} — ${definition.description} (env: ${definition.envVar})`);
|
|
22
22
|
}
|
|
23
23
|
lines.push("");
|
|
24
|
-
lines.push('Use "/config set <key> <value>" to update
|
|
24
|
+
lines.push('Use "/config set <key> <value>" to update ~/.minicode/.env.');
|
|
25
25
|
lines.push("Secrets like API keys stay env-only for now.");
|
|
26
26
|
return lines.join("\n");
|
|
27
27
|
}
|
|
@@ -31,14 +31,16 @@ async function renderConfigValue(key, context) {
|
|
|
31
31
|
}
|
|
32
32
|
const minicodeHome = context.minicodeHome ?? MINICODE_HOME;
|
|
33
33
|
const definition = getEditableConfigDefinition(key);
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
34
|
+
const payload = await buildStructuredConfigPayload(context.config, minicodeHome);
|
|
35
|
+
const entry = payload.entries.find((item) => item.key === key);
|
|
36
|
+
if (!entry) {
|
|
37
|
+
return `Unknown editable config key "${key}".\n\n${renderEditableKeys()}`;
|
|
38
|
+
}
|
|
37
39
|
return [
|
|
38
40
|
`${definition.key}`,
|
|
39
41
|
` effective: ${getEffectiveEditableConfigValue(context.config, key)}`,
|
|
40
|
-
`
|
|
41
|
-
` env override (${definition.envVar}): ${formatPersistedConfigValue(envValue)}`,
|
|
42
|
+
` saved in ~/.minicode/.env: ${formatPersistedConfigValue(entry.persistedValue)}`,
|
|
43
|
+
` exported env override (${definition.envVar}): ${formatPersistedConfigValue(entry.envValue)}`,
|
|
42
44
|
].join("\n");
|
|
43
45
|
}
|
|
44
46
|
async function persistConfigValue(key, rawValue, context) {
|
|
@@ -59,8 +61,8 @@ async function persistConfigValue(key, rawValue, context) {
|
|
|
59
61
|
`File: ${result.path}`,
|
|
60
62
|
"Restart minicode to pick up persisted config changes in a new session.",
|
|
61
63
|
];
|
|
62
|
-
if (env.
|
|
63
|
-
lines.push(`Note: ${definition.envVar} is currently
|
|
64
|
+
if (env.sources[definition.envVar] === "process") {
|
|
65
|
+
lines.push(`Note: ${definition.envVar} is currently exported in your shell and will override this saved value until it is unset.`);
|
|
64
66
|
}
|
|
65
67
|
return lines.join("\n");
|
|
66
68
|
}
|
|
@@ -82,11 +84,11 @@ async function removeConfigValue(key, context) {
|
|
|
82
84
|
});
|
|
83
85
|
const lines = [
|
|
84
86
|
`Removed persisted value for "${key}".`,
|
|
85
|
-
`File: ${minicodeHome}
|
|
87
|
+
`File: ${minicodeHome}/.env`,
|
|
86
88
|
"Restart minicode to ensure the updated config is applied in a new session.",
|
|
87
89
|
];
|
|
88
|
-
if (env.
|
|
89
|
-
lines.push(`Note: ${definition.envVar} is still
|
|
90
|
+
if (env.sources[definition.envVar] === "process") {
|
|
91
|
+
lines.push(`Note: ${definition.envVar} is still exported in your shell, so the effective value may remain unchanged.`);
|
|
90
92
|
}
|
|
91
93
|
return lines.join("\n");
|
|
92
94
|
}
|