@sean.holung/minicode 0.3.7 → 0.3.8
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/dist/src/agent/config.js +25 -0
- package/dist/src/model-utils.js +18 -1
- package/dist/src/serve/agent-bridge.js +85 -14
- package/dist/src/serve/server.js +137 -3
- package/dist/src/session/session-store.js +18 -0
- package/dist/src/web/app.js +559 -90
- package/dist/src/web/index.html +112 -8
- package/dist/src/web/style.css +141 -7
- package/dist/tests/agent.test.js +16 -0
- package/dist/tests/config-integration.test.js +91 -1
- package/dist/tests/file-tools.test.js +12 -0
- package/dist/tests/graph-onboarding.test.js +8 -0
- package/dist/tests/model-client-openai.test.js +41 -0
- package/dist/tests/model-dropdown-ui.test.js +23 -0
- package/dist/tests/model-utils.test.js +26 -1
- package/dist/tests/serve.integration.test.js +163 -0
- package/dist/tests/session-store.test.js +15 -1
- package/dist/tests/settings-ui.test.js +11 -0
- package/dist/tests/setup-overlay-state.test.js +49 -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 +10 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -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 +21 -0
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -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 +60 -6
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/dist/src/agent/config.js
CHANGED
|
@@ -60,6 +60,31 @@ export function getConfigMissing(config) {
|
|
|
60
60
|
}
|
|
61
61
|
return missing;
|
|
62
62
|
}
|
|
63
|
+
function hasExplicitConfigValue(value) {
|
|
64
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
65
|
+
}
|
|
66
|
+
function parseExplicitModelProvider(value) {
|
|
67
|
+
if (!hasExplicitConfigValue(value)) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return parseModelProvider(value);
|
|
71
|
+
}
|
|
72
|
+
export function getConfiguredProvider(config, env = process.env) {
|
|
73
|
+
const explicitModelProvider = parseExplicitModelProvider(env.MODEL_PROVIDER);
|
|
74
|
+
if (config.modelProvider === "anthropic") {
|
|
75
|
+
return explicitModelProvider === "anthropic" || hasExplicitConfigValue(env.ANTHROPIC_API_KEY)
|
|
76
|
+
? "anthropic"
|
|
77
|
+
: null;
|
|
78
|
+
}
|
|
79
|
+
const hasExplicitOpenAiCompatibleConfig = explicitModelProvider === "openai-compatible" ||
|
|
80
|
+
hasExplicitConfigValue(env.OPENAI_BASE_URL) ||
|
|
81
|
+
hasExplicitConfigValue(env.OPENROUTER_API_KEY);
|
|
82
|
+
if (!hasExplicitOpenAiCompatibleConfig) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const baseUrl = (env.OPENAI_BASE_URL ?? config.openAiBaseUrl).trim().toLowerCase();
|
|
86
|
+
return baseUrl.includes("openrouter") ? "openrouter" : "openai-compatible";
|
|
87
|
+
}
|
|
63
88
|
export function getConfigSetupMessage(config) {
|
|
64
89
|
const missing = getConfigMissing(config);
|
|
65
90
|
if (missing.length === 0) {
|
package/dist/src/model-utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function getModelDisplayName(model) {
|
|
1
|
+
export function getModelDisplayName(model) {
|
|
2
2
|
return (model.name ?? model.id).trim();
|
|
3
3
|
}
|
|
4
4
|
export function sortModelsAlphabetically(models) {
|
|
@@ -16,3 +16,20 @@ export function sortModelsAlphabetically(models) {
|
|
|
16
16
|
});
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
+
function normalizeModelSearchValue(value) {
|
|
20
|
+
return value
|
|
21
|
+
.trim()
|
|
22
|
+
.toLocaleLowerCase()
|
|
23
|
+
.split(/\s+/)
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
export function filterModelsByQuery(models, query) {
|
|
27
|
+
const tokens = normalizeModelSearchValue(query);
|
|
28
|
+
if (tokens.length === 0) {
|
|
29
|
+
return [...models];
|
|
30
|
+
}
|
|
31
|
+
return models.filter((model) => {
|
|
32
|
+
const haystack = `${getModelDisplayName(model)} ${model.id}`.toLocaleLowerCase();
|
|
33
|
+
return tokens.every((token) => haystack.includes(token));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -17,7 +17,7 @@ export class AgentBridge {
|
|
|
17
17
|
modelClient;
|
|
18
18
|
projectIndex;
|
|
19
19
|
toolRegistry;
|
|
20
|
-
|
|
20
|
+
sessionProviderConnection = null;
|
|
21
21
|
busy = false;
|
|
22
22
|
abortController = null;
|
|
23
23
|
broadcast;
|
|
@@ -68,17 +68,10 @@ export class AgentBridge {
|
|
|
68
68
|
catch {
|
|
69
69
|
projectIndex = undefined;
|
|
70
70
|
}
|
|
71
|
-
const toolRegistry = createToolRegistry(config, projectIndex);
|
|
72
|
-
// Wrap tool registry execute to inject annotations into tool results
|
|
73
|
-
const originalExecute = toolRegistry.execute.bind(toolRegistry);
|
|
74
|
-
toolRegistry.execute = async (name, input) => {
|
|
75
|
-
const result = await originalExecute(name, input);
|
|
76
|
-
return this.appendAnnotationsToResult(name, input, result);
|
|
77
|
-
};
|
|
78
71
|
this.config = config;
|
|
79
72
|
this.baseConfig = AgentBridge.cloneConfig(config);
|
|
80
73
|
this.projectIndex = projectIndex;
|
|
81
|
-
this.
|
|
74
|
+
this.installToolRegistry(projectIndex);
|
|
82
75
|
if (this.modelClient) {
|
|
83
76
|
this.agent = this.createAgent();
|
|
84
77
|
}
|
|
@@ -98,6 +91,16 @@ export class AgentBridge {
|
|
|
98
91
|
}
|
|
99
92
|
Object.assign(targetRecord, AgentBridge.cloneConfig(source));
|
|
100
93
|
}
|
|
94
|
+
installToolRegistry(projectIndex) {
|
|
95
|
+
const toolRegistry = createToolRegistry(this.config, projectIndex);
|
|
96
|
+
// Wrap tool registry execute to inject annotations into tool results.
|
|
97
|
+
const originalExecute = toolRegistry.execute.bind(toolRegistry);
|
|
98
|
+
toolRegistry.execute = async (name, input) => {
|
|
99
|
+
const result = await originalExecute(name, input);
|
|
100
|
+
return this.appendAnnotationsToResult(name, input, result);
|
|
101
|
+
};
|
|
102
|
+
this.toolRegistry = toolRegistry;
|
|
103
|
+
}
|
|
101
104
|
createAgent(session, onUiUpdate) {
|
|
102
105
|
if (!this.modelClient || !this.toolRegistry) {
|
|
103
106
|
throw new Error("Agent runtime is not initialized.");
|
|
@@ -179,7 +182,8 @@ export class AgentBridge {
|
|
|
179
182
|
}
|
|
180
183
|
}
|
|
181
184
|
catch {
|
|
182
|
-
// File may have been deleted
|
|
185
|
+
// File may have been deleted or renamed. A workspace refresh prunes stale symbols.
|
|
186
|
+
await this.refreshIndex();
|
|
183
187
|
}
|
|
184
188
|
}
|
|
185
189
|
isReady() {
|
|
@@ -192,7 +196,10 @@ export class AgentBridge {
|
|
|
192
196
|
return this.config;
|
|
193
197
|
}
|
|
194
198
|
isOpenRouterSessionConnected() {
|
|
195
|
-
return this.
|
|
199
|
+
return this.sessionProviderConnection === "openrouter";
|
|
200
|
+
}
|
|
201
|
+
isOpenAiCompatibleSessionConnected() {
|
|
202
|
+
return this.sessionProviderConnection === "openai-compatible";
|
|
196
203
|
}
|
|
197
204
|
connectOpenRouter(apiKey) {
|
|
198
205
|
const trimmedKey = apiKey.trim();
|
|
@@ -206,7 +213,29 @@ export class AgentBridge {
|
|
|
206
213
|
this.config.modelProvider = "openai-compatible";
|
|
207
214
|
this.config.openAiBaseUrl = "https://openrouter.ai/api/v1";
|
|
208
215
|
this.config.openAiApiKey = trimmedKey;
|
|
209
|
-
this.
|
|
216
|
+
this.sessionProviderConnection = "openrouter";
|
|
217
|
+
this.modelClient = createModelClient(this.config);
|
|
218
|
+
this.agent = this.createAgent(currentSession);
|
|
219
|
+
}
|
|
220
|
+
connectOpenAiCompatible(baseUrl, apiKey) {
|
|
221
|
+
const trimmedBaseUrl = baseUrl.trim().replace(/\/+$/, "");
|
|
222
|
+
if (!trimmedBaseUrl) {
|
|
223
|
+
throw new Error("OpenAI-compatible endpoint is required.");
|
|
224
|
+
}
|
|
225
|
+
if (this.busy) {
|
|
226
|
+
throw new Error("busy");
|
|
227
|
+
}
|
|
228
|
+
const currentSession = this.agent?.getSession();
|
|
229
|
+
this.config.modelProvider = "openai-compatible";
|
|
230
|
+
this.config.openAiBaseUrl = trimmedBaseUrl;
|
|
231
|
+
const trimmedApiKey = apiKey?.trim() ?? "";
|
|
232
|
+
if (trimmedApiKey) {
|
|
233
|
+
this.config.openAiApiKey = trimmedApiKey;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
delete this.config.openAiApiKey;
|
|
237
|
+
}
|
|
238
|
+
this.sessionProviderConnection = "openai-compatible";
|
|
210
239
|
this.modelClient = createModelClient(this.config);
|
|
211
240
|
this.agent = this.createAgent(currentSession);
|
|
212
241
|
}
|
|
@@ -214,12 +243,24 @@ export class AgentBridge {
|
|
|
214
243
|
if (this.busy) {
|
|
215
244
|
throw new Error("busy");
|
|
216
245
|
}
|
|
217
|
-
if (
|
|
246
|
+
if (this.sessionProviderConnection !== "openrouter") {
|
|
218
247
|
return false;
|
|
219
248
|
}
|
|
249
|
+
return this.disconnectSessionProvider();
|
|
250
|
+
}
|
|
251
|
+
disconnectOpenAiCompatible() {
|
|
252
|
+
if (this.busy) {
|
|
253
|
+
throw new Error("busy");
|
|
254
|
+
}
|
|
255
|
+
if (this.sessionProviderConnection !== "openai-compatible") {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return this.disconnectSessionProvider();
|
|
259
|
+
}
|
|
260
|
+
disconnectSessionProvider() {
|
|
220
261
|
const currentSession = this.agent?.getSession();
|
|
221
262
|
AgentBridge.applyConfig(this.config, this.baseConfig);
|
|
222
|
-
this.
|
|
263
|
+
this.sessionProviderConnection = null;
|
|
223
264
|
try {
|
|
224
265
|
this.modelClient = createModelClient(this.config);
|
|
225
266
|
this.agent = this.createAgent(currentSession);
|
|
@@ -312,6 +353,36 @@ export class AgentBridge {
|
|
|
312
353
|
hasIndex() {
|
|
313
354
|
return this.projectIndex !== undefined;
|
|
314
355
|
}
|
|
356
|
+
async refreshIndex() {
|
|
357
|
+
try {
|
|
358
|
+
if (this.projectIndex) {
|
|
359
|
+
await this.projectIndex.refreshFromWorkspace();
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
this.projectIndex = await buildProjectIndex(this.config.workspaceRoot);
|
|
363
|
+
this.installToolRegistry(this.projectIndex);
|
|
364
|
+
if (this.modelClient) {
|
|
365
|
+
this.agent = this.createAgent(this.agent?.getSession());
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const index = this.projectIndex;
|
|
369
|
+
if (!index) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
const cacheDir = getWorkspaceCacheDir(this.config.workspaceRoot);
|
|
373
|
+
const fileHashes = await computeFileHashes(this.config.workspaceRoot);
|
|
374
|
+
await saveIndex(index, cacheDir, fileHashes);
|
|
375
|
+
this.evictStaleAnnotations();
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
if (this.verbose) {
|
|
380
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
381
|
+
console.error(`[index] Refresh failed: ${message}`);
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
315
386
|
getSymbols() {
|
|
316
387
|
if (!this.projectIndex)
|
|
317
388
|
return [];
|
package/dist/src/serve/server.js
CHANGED
|
@@ -6,13 +6,14 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { AgentBridge } from "./agent-bridge.js";
|
|
7
7
|
import { createWebSocketServer } from "./websocket.js";
|
|
8
8
|
import { handleChatCompletions, handleModels } from "./openai-compat.js";
|
|
9
|
-
import { formatConfigForDisplay, getConfigMissing } from "../agent/config.js";
|
|
9
|
+
import { formatConfigForDisplay, getConfiguredProvider, getConfigMissing, resolveConfigEnv } from "../agent/config.js";
|
|
10
10
|
import { applyPersistedConfigUpdates, buildStructuredConfigPayload } from "../agent/editable-config.js";
|
|
11
11
|
import { getHomeEnvPath, upsertHomeEnvValues } from "../agent/home-env.js";
|
|
12
12
|
import { sortModelsAlphabetically } from "../model-utils.js";
|
|
13
13
|
import { serializeSymbolMatch } from "../shared/symbol-resolution.js";
|
|
14
14
|
import { handleMcpRequest } from "./mcp-server.js";
|
|
15
15
|
import { buildSessionPreview } from "../session/session-preview.js";
|
|
16
|
+
import { DuplicateSessionLabelError } from "../session/session-store.js";
|
|
16
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
// Resolve web dir: always serve from dist/src/web (built by scripts/build-web.mjs)
|
|
18
19
|
// In dev (tsx): __dirname = src/serve → go up to project root, then dist/src/web
|
|
@@ -51,6 +52,9 @@ function readBody(req) {
|
|
|
51
52
|
req.on("error", reject);
|
|
52
53
|
});
|
|
53
54
|
}
|
|
55
|
+
function normalizeBaseUrl(value) {
|
|
56
|
+
return value.trim().replace(/\/+$/, "");
|
|
57
|
+
}
|
|
54
58
|
async function serveStatic(res, urlPath) {
|
|
55
59
|
const fileName = urlPath === "/" ? "index.html" : urlPath.slice(1);
|
|
56
60
|
const filePath = path.join(webDir, fileName);
|
|
@@ -107,13 +111,16 @@ export function createRequestHandler(bridge, emit, options = {}) {
|
|
|
107
111
|
// Minicode REST API
|
|
108
112
|
if (pathname === "/api/status" && method === "GET") {
|
|
109
113
|
const missing = getConfigMissing(config);
|
|
114
|
+
const resolvedEnv = await resolveConfigEnv({ ...(minicodeHome ? { minicodeHome } : {}) });
|
|
110
115
|
sendJson(res, 200, {
|
|
111
116
|
status: bridge.isBusy() ? "busy" : "ready",
|
|
112
117
|
workspace: config.workspaceRoot,
|
|
113
118
|
model: config.model,
|
|
114
119
|
provider: config.modelProvider,
|
|
115
120
|
baseUrl: config.openAiBaseUrl,
|
|
121
|
+
configuredProvider: getConfiguredProvider(config, resolvedEnv.values),
|
|
116
122
|
sessionOpenRouterConnected: bridge.isOpenRouterSessionConnected(),
|
|
123
|
+
sessionOpenAiCompatibleConnected: bridge.isOpenAiCompatibleSessionConnected(),
|
|
117
124
|
needsSetup: missing.length > 0,
|
|
118
125
|
missing,
|
|
119
126
|
});
|
|
@@ -223,6 +230,84 @@ export function createRequestHandler(bridge, emit, options = {}) {
|
|
|
223
230
|
});
|
|
224
231
|
return;
|
|
225
232
|
}
|
|
233
|
+
if (pathname === "/api/openai-compatible/connect" && method === "POST") {
|
|
234
|
+
const body = JSON.parse(await readBody(req));
|
|
235
|
+
if (!body.baseUrl || typeof body.baseUrl !== "string") {
|
|
236
|
+
sendJson(res, 400, { error: "baseUrl is required" });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const normalizedBaseUrl = normalizeBaseUrl(body.baseUrl);
|
|
240
|
+
if (!normalizedBaseUrl) {
|
|
241
|
+
sendJson(res, 400, { error: "baseUrl is required" });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const parsedUrl = new URL(normalizedBaseUrl);
|
|
246
|
+
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
247
|
+
throw new Error("invalid protocol");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
sendJson(res, 400, { error: "baseUrl must be a valid absolute http(s) URL" });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
bridge.connectOpenAiCompatible(normalizedBaseUrl, body.apiKey);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
const message = error instanceof Error ? error.message : "Failed to configure the OpenAI-compatible provider";
|
|
259
|
+
sendJson(res, message === "busy" ? 409 : 400, { error: message });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
let persistedToEnv = false;
|
|
263
|
+
let persistedEnvPath = null;
|
|
264
|
+
let persistWarning = null;
|
|
265
|
+
const trimmedApiKey = body.apiKey?.trim() ?? "";
|
|
266
|
+
if (body.persistToEnv === true) {
|
|
267
|
+
try {
|
|
268
|
+
const result = await upsertHomeEnvValues({
|
|
269
|
+
values: {
|
|
270
|
+
MODEL_PROVIDER: "openai-compatible",
|
|
271
|
+
OPENAI_BASE_URL: normalizedBaseUrl,
|
|
272
|
+
OPENAI_API_KEY: trimmedApiKey.length > 0 ? trimmedApiKey : null,
|
|
273
|
+
},
|
|
274
|
+
...(minicodeHome ? { minicodeHome } : {}),
|
|
275
|
+
});
|
|
276
|
+
persistedToEnv = true;
|
|
277
|
+
persistedEnvPath = result.path;
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
const message = error instanceof Error ? error.message : "Failed to update ~/.minicode/.env";
|
|
281
|
+
persistedEnvPath = getHomeEnvPath(minicodeHome);
|
|
282
|
+
persistWarning = `The OpenAI-compatible provider connected for this serve session, but minicode could not update ${persistedEnvPath}: ${message}`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const missing = getConfigMissing(config);
|
|
286
|
+
const onlyModelMissing = missing.length === 1 && missing[0] === "MODEL is not set";
|
|
287
|
+
const message = persistWarning
|
|
288
|
+
? `${persistWarning}${onlyModelMissing ? " Select a model to continue." : ""}`
|
|
289
|
+
: persistedToEnv
|
|
290
|
+
? (onlyModelMissing
|
|
291
|
+
? "The OpenAI-compatible provider connected for this serve session and was saved to ~/.minicode/.env. Select a model to continue, and minicode will remember this endpoint for future runs."
|
|
292
|
+
: "The OpenAI-compatible provider connected for this serve session and was saved to ~/.minicode/.env for future runs.")
|
|
293
|
+
: (onlyModelMissing
|
|
294
|
+
? "The OpenAI-compatible provider connected for this serve session. Select a model to continue."
|
|
295
|
+
: "The OpenAI-compatible provider connected for this serve session.");
|
|
296
|
+
sendJson(res, 200, {
|
|
297
|
+
ok: true,
|
|
298
|
+
sessionOnly: true,
|
|
299
|
+
persistedToEnv,
|
|
300
|
+
persistedEnvPath,
|
|
301
|
+
persistWarning,
|
|
302
|
+
provider: config.modelProvider,
|
|
303
|
+
model: config.model,
|
|
304
|
+
baseUrl: config.openAiBaseUrl,
|
|
305
|
+
needsSetup: missing.length > 0,
|
|
306
|
+
missing,
|
|
307
|
+
message,
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
226
311
|
if (pathname === "/api/openrouter/disconnect" && method === "POST") {
|
|
227
312
|
let disconnected = false;
|
|
228
313
|
try {
|
|
@@ -250,6 +335,33 @@ export function createRequestHandler(bridge, emit, options = {}) {
|
|
|
250
335
|
sendJson(res, 200, body);
|
|
251
336
|
return;
|
|
252
337
|
}
|
|
338
|
+
if (pathname === "/api/openai-compatible/disconnect" && method === "POST") {
|
|
339
|
+
let disconnected = false;
|
|
340
|
+
try {
|
|
341
|
+
disconnected = bridge.disconnectOpenAiCompatible();
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
const message = error instanceof Error ? error.message : "Failed to remove the OpenAI-compatible session";
|
|
345
|
+
sendJson(res, message === "busy" ? 409 : 400, { error: message });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const missing = getConfigMissing(config);
|
|
349
|
+
const body = {
|
|
350
|
+
ok: true,
|
|
351
|
+
disconnected,
|
|
352
|
+
sessionOnly: true,
|
|
353
|
+
provider: config.modelProvider,
|
|
354
|
+
model: config.model,
|
|
355
|
+
baseUrl: config.openAiBaseUrl,
|
|
356
|
+
needsSetup: missing.length > 0,
|
|
357
|
+
missing,
|
|
358
|
+
message: disconnected
|
|
359
|
+
? "Removed the session-only OpenAI-compatible connection and restored your original provider settings."
|
|
360
|
+
: "No session-only OpenAI-compatible connection was active.",
|
|
361
|
+
};
|
|
362
|
+
sendJson(res, 200, body);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
253
365
|
if (pathname === "/api/model" && method === "POST") {
|
|
254
366
|
const body = JSON.parse(await readBody(req));
|
|
255
367
|
if (!body.model || typeof body.model !== "string") {
|
|
@@ -336,8 +448,16 @@ export function createRequestHandler(bridge, emit, options = {}) {
|
|
|
336
448
|
}
|
|
337
449
|
if (pathname === "/api/sessions/save" && method === "POST") {
|
|
338
450
|
const body = JSON.parse(await readBody(req));
|
|
339
|
-
|
|
340
|
-
|
|
451
|
+
try {
|
|
452
|
+
const meta = await bridge.saveSess(body.label);
|
|
453
|
+
sendJson(res, 200, meta);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
const message = error instanceof Error ? error.message : "Failed to save session";
|
|
457
|
+
sendJson(res, error instanceof DuplicateSessionLabelError ? 409 : 500, {
|
|
458
|
+
error: message,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
341
461
|
return;
|
|
342
462
|
}
|
|
343
463
|
if (pathname === "/api/sessions/load" && method === "POST") {
|
|
@@ -472,6 +592,20 @@ export function createRequestHandler(bridge, emit, options = {}) {
|
|
|
472
592
|
sendJson(res, 200, result);
|
|
473
593
|
return;
|
|
474
594
|
}
|
|
595
|
+
if (pathname === "/api/index/refresh" && method === "POST") {
|
|
596
|
+
const refreshed = await bridge.refreshIndex();
|
|
597
|
+
if (!refreshed) {
|
|
598
|
+
sendJson(res, 404, { error: "No project index available" });
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const graph = bridge.getGraph();
|
|
602
|
+
sendJson(res, 200, {
|
|
603
|
+
ok: true,
|
|
604
|
+
symbolCount: graph?.nodes.length ?? 0,
|
|
605
|
+
edgeCount: graph?.edges.length ?? 0,
|
|
606
|
+
});
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
475
609
|
if (pathname === "/api/analysis" && method === "GET") {
|
|
476
610
|
const result = bridge.getStructuralAnalysis();
|
|
477
611
|
if (!result) {
|
|
@@ -3,10 +3,23 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Session } from "@minicode/agent-sdk";
|
|
5
5
|
let sessionsDir = path.join(os.homedir(), ".minicode", "sessions");
|
|
6
|
+
export class DuplicateSessionLabelError extends Error {
|
|
7
|
+
label;
|
|
8
|
+
existingSessionId;
|
|
9
|
+
constructor(label, existingSessionId) {
|
|
10
|
+
super(`A saved session named "${label}" already exists. Choose a different name or load that session to update it.`);
|
|
11
|
+
this.label = label;
|
|
12
|
+
this.existingSessionId = existingSessionId;
|
|
13
|
+
this.name = "DuplicateSessionLabelError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
6
16
|
/** Override sessions directory (for testing). */
|
|
7
17
|
export function setSessionsDir(dir) {
|
|
8
18
|
sessionsDir = dir;
|
|
9
19
|
}
|
|
20
|
+
function normalizeSessionLabel(label) {
|
|
21
|
+
return label.trim().toLowerCase();
|
|
22
|
+
}
|
|
10
23
|
export async function saveSession(session, label, annotations) {
|
|
11
24
|
await mkdir(sessionsDir, { recursive: true });
|
|
12
25
|
const savedAt = new Date().toISOString();
|
|
@@ -14,6 +27,11 @@ export async function saveSession(session, label, annotations) {
|
|
|
14
27
|
const resolvedLabel = label && label.trim().length > 0
|
|
15
28
|
? label.trim()
|
|
16
29
|
: new Date().toLocaleString();
|
|
30
|
+
const duplicate = (await listSessions()).find((savedSession) => savedSession.id !== snapshot.id &&
|
|
31
|
+
normalizeSessionLabel(savedSession.label) === normalizeSessionLabel(resolvedLabel));
|
|
32
|
+
if (duplicate) {
|
|
33
|
+
throw new DuplicateSessionLabelError(resolvedLabel, duplicate.id);
|
|
34
|
+
}
|
|
17
35
|
const data = {
|
|
18
36
|
label: resolvedLabel,
|
|
19
37
|
savedAt,
|