@sean.holung/minicode 0.2.2 → 0.2.4
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 +20 -12
- package/dist/src/agent/config.js +14 -2
- package/dist/src/cli/args.js +31 -0
- package/dist/src/index.js +21 -2
- package/dist/src/indexer/code-map.js +52 -5
- package/dist/src/indexer/focus-tracker.js +63 -0
- package/dist/src/indexer/project-index.js +2 -2
- package/dist/src/serve/agent-bridge.js +233 -0
- package/dist/src/serve/openai-compat.js +144 -0
- package/dist/src/serve/server.js +251 -0
- package/dist/src/serve/types.js +2 -0
- package/dist/src/serve/websocket.js +28 -0
- package/dist/src/ui/cli-ink.js +22 -2
- package/dist/src/web/app.js +350 -0
- package/dist/src/web/index.html +49 -0
- package/dist/src/web/style.css +422 -0
- package/dist/tests/agent.test.js +62 -0
- package/dist/tests/cli-args.test.js +4 -2
- package/dist/tests/serve.integration.test.js +534 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts +30 -1
- 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 +212 -8
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +10 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.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/model/client.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +2 -0
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/session/session.d.ts +51 -1
- package/node_modules/@minicode/agent-sdk/dist/src/session/session.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/session/session.js +210 -2
- package/node_modules/@minicode/agent-sdk/dist/src/session/session.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/session.test.js +75 -0
- package/node_modules/@minicode/agent-sdk/dist/tests/session.test.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test, afterEach } from "node:test";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { createRequestHandler } from "../src/serve/server.js";
|
|
5
|
+
import { AgentBridge } from "../src/serve/agent-bridge.js";
|
|
6
|
+
import { createTestAgentConfig } from "./test-utils.js";
|
|
7
|
+
/**
|
|
8
|
+
* Lightweight AgentBridge subclass for testing.
|
|
9
|
+
* Overrides all public methods so no real config/agent/index is needed.
|
|
10
|
+
*/
|
|
11
|
+
class MockBridge extends AgentBridge {
|
|
12
|
+
_busy = false;
|
|
13
|
+
turnHistory = [];
|
|
14
|
+
constructor() {
|
|
15
|
+
super(() => { }, false);
|
|
16
|
+
}
|
|
17
|
+
isBusy() {
|
|
18
|
+
return this._busy;
|
|
19
|
+
}
|
|
20
|
+
getConfig() {
|
|
21
|
+
return createTestAgentConfig("/tmp/test-workspace");
|
|
22
|
+
}
|
|
23
|
+
async runTurn(message) {
|
|
24
|
+
if (this._busy)
|
|
25
|
+
throw new Error("busy");
|
|
26
|
+
this._busy = true;
|
|
27
|
+
this.turnHistory.push(message);
|
|
28
|
+
// Simulate streaming by emitting events via the listener system
|
|
29
|
+
this.emit({ type: "streaming_chunk", content: `Echo: ${message}` });
|
|
30
|
+
this._busy = false;
|
|
31
|
+
return {
|
|
32
|
+
text: `Echo: ${message}`,
|
|
33
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Expose the private emit for test streaming simulation. */
|
|
37
|
+
emit(msg) {
|
|
38
|
+
// Call listeners registered via addListener
|
|
39
|
+
for (const fn of this.listeners) {
|
|
40
|
+
fn(msg);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async listSess() {
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
id: "sess-1",
|
|
47
|
+
label: "test-session",
|
|
48
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
49
|
+
savedAt: "2026-01-01T00:00:00.000Z",
|
|
50
|
+
messageCount: 3,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
async saveSess(label) {
|
|
55
|
+
return {
|
|
56
|
+
id: "sess-new",
|
|
57
|
+
label: label ?? "auto-label",
|
|
58
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
59
|
+
savedAt: "2026-01-01T00:00:00.000Z",
|
|
60
|
+
messageCount: 1,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async loadSess(label) {
|
|
64
|
+
if (label === "nonexistent")
|
|
65
|
+
return null;
|
|
66
|
+
return { session: {}, label };
|
|
67
|
+
}
|
|
68
|
+
setBusy(busy) {
|
|
69
|
+
this._busy = busy;
|
|
70
|
+
}
|
|
71
|
+
hasIndex() {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
getSymbols() {
|
|
75
|
+
return [
|
|
76
|
+
{ name: "foo", qualifiedName: "foo", kind: "function", filePath: "src/foo.ts", startLine: 1, endLine: 5, signature: "function foo(): void", exported: true },
|
|
77
|
+
{ name: "Bar", qualifiedName: "Bar", kind: "class", filePath: "src/bar.ts", startLine: 1, endLine: 20, signature: "class Bar", exported: true },
|
|
78
|
+
{ name: "helper", qualifiedName: "Bar.helper", kind: "method", filePath: "src/bar.ts", startLine: 10, endLine: 15, signature: "helper(): string", exported: false },
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
getSymbol(name) {
|
|
82
|
+
const syms = this.getSymbols();
|
|
83
|
+
const match = syms.find((s) => s.qualifiedName === name || s.name === name);
|
|
84
|
+
if (!match)
|
|
85
|
+
return undefined;
|
|
86
|
+
return { ...match, dependencies: [], kind: match.kind };
|
|
87
|
+
}
|
|
88
|
+
getDependencies(symbolName) {
|
|
89
|
+
if (symbolName === "foo") {
|
|
90
|
+
return [
|
|
91
|
+
{ name: "foo", qualifiedName: "foo", kind: "function", filePath: "src/foo.ts", signature: "function foo(): void" },
|
|
92
|
+
{ name: "Bar", qualifiedName: "Bar", kind: "class", filePath: "src/bar.ts", signature: "class Bar" },
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
getReferences(symbolName) {
|
|
98
|
+
if (symbolName === "Bar") {
|
|
99
|
+
return [{ from: "foo", kind: "calls" }];
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
getCodeMap() {
|
|
104
|
+
return { text: "## src/foo.ts\n- foo(): void", shownCount: 1, totalCount: 3 };
|
|
105
|
+
}
|
|
106
|
+
getGraph() {
|
|
107
|
+
return {
|
|
108
|
+
nodes: [
|
|
109
|
+
{ id: "foo", name: "foo", kind: "function", filePath: "src/foo.ts", exported: true },
|
|
110
|
+
{ id: "Bar", name: "Bar", kind: "class", filePath: "src/bar.ts", exported: true },
|
|
111
|
+
],
|
|
112
|
+
edges: [{ from: "foo", to: "Bar", kind: "calls" }],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
getPinnedSymbols() {
|
|
116
|
+
return [...this._pinned];
|
|
117
|
+
}
|
|
118
|
+
_pinned = new Set();
|
|
119
|
+
pinSymbol(name) {
|
|
120
|
+
if (name === "nonexistent")
|
|
121
|
+
return false;
|
|
122
|
+
this._pinned.add(name);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
unpinSymbol(name) {
|
|
126
|
+
this._pinned.delete(name);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ── Test harness ──
|
|
131
|
+
let activeServer;
|
|
132
|
+
function startTestServer(bridge) {
|
|
133
|
+
const handler = createRequestHandler(bridge);
|
|
134
|
+
const server = createServer(handler);
|
|
135
|
+
activeServer = server;
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
server.listen(0, "127.0.0.1", () => {
|
|
138
|
+
const addr = server.address();
|
|
139
|
+
if (typeof addr === "object" && addr) {
|
|
140
|
+
resolve(`http://127.0.0.1:${addr.port}`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function stopTestServer() {
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
if (activeServer) {
|
|
148
|
+
activeServer.close(() => resolve());
|
|
149
|
+
activeServer = undefined;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
resolve();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
afterEach(async () => {
|
|
157
|
+
await stopTestServer();
|
|
158
|
+
});
|
|
159
|
+
// ── REST API tests ──
|
|
160
|
+
test("GET /api/status returns status and config info", async () => {
|
|
161
|
+
const bridge = new MockBridge();
|
|
162
|
+
const base = await startTestServer(bridge);
|
|
163
|
+
const res = await fetch(`${base}/api/status`);
|
|
164
|
+
assert.equal(res.status, 200);
|
|
165
|
+
const body = (await res.json());
|
|
166
|
+
assert.equal(body.status, "ready");
|
|
167
|
+
assert.equal(body.workspace, "/tmp/test-workspace");
|
|
168
|
+
assert.equal(body.model, "test-model");
|
|
169
|
+
assert.equal(body.provider, "anthropic");
|
|
170
|
+
});
|
|
171
|
+
test("GET /api/status returns busy when agent is busy", async () => {
|
|
172
|
+
const bridge = new MockBridge();
|
|
173
|
+
bridge.setBusy(true);
|
|
174
|
+
const base = await startTestServer(bridge);
|
|
175
|
+
const res = await fetch(`${base}/api/status`);
|
|
176
|
+
const body = (await res.json());
|
|
177
|
+
assert.equal(body.status, "busy");
|
|
178
|
+
});
|
|
179
|
+
test("GET /api/config returns formatted config", async () => {
|
|
180
|
+
const bridge = new MockBridge();
|
|
181
|
+
const base = await startTestServer(bridge);
|
|
182
|
+
const res = await fetch(`${base}/api/config`);
|
|
183
|
+
assert.equal(res.status, 200);
|
|
184
|
+
const body = (await res.json());
|
|
185
|
+
assert.ok(body.config.includes("workspaceRoot"));
|
|
186
|
+
assert.ok(body.config.includes("test-model"));
|
|
187
|
+
});
|
|
188
|
+
test("GET /api/sessions returns session list", async () => {
|
|
189
|
+
const bridge = new MockBridge();
|
|
190
|
+
const base = await startTestServer(bridge);
|
|
191
|
+
const res = await fetch(`${base}/api/sessions`);
|
|
192
|
+
assert.equal(res.status, 200);
|
|
193
|
+
const body = (await res.json());
|
|
194
|
+
assert.equal(body.sessions.length, 1);
|
|
195
|
+
assert.equal(body.sessions[0].label, "test-session");
|
|
196
|
+
});
|
|
197
|
+
test("POST /api/sessions/save saves a session", async () => {
|
|
198
|
+
const bridge = new MockBridge();
|
|
199
|
+
const base = await startTestServer(bridge);
|
|
200
|
+
const res = await fetch(`${base}/api/sessions/save`, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: { "Content-Type": "application/json" },
|
|
203
|
+
body: JSON.stringify({ label: "my-save" }),
|
|
204
|
+
});
|
|
205
|
+
assert.equal(res.status, 200);
|
|
206
|
+
const body = (await res.json());
|
|
207
|
+
assert.equal(body.label, "my-save");
|
|
208
|
+
});
|
|
209
|
+
test("POST /api/sessions/load returns 404 for unknown session", async () => {
|
|
210
|
+
const bridge = new MockBridge();
|
|
211
|
+
const base = await startTestServer(bridge);
|
|
212
|
+
const res = await fetch(`${base}/api/sessions/load`, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: { "Content-Type": "application/json" },
|
|
215
|
+
body: JSON.stringify({ label: "nonexistent" }),
|
|
216
|
+
});
|
|
217
|
+
assert.equal(res.status, 404);
|
|
218
|
+
});
|
|
219
|
+
test("POST /api/sessions/load returns success for known session", async () => {
|
|
220
|
+
const bridge = new MockBridge();
|
|
221
|
+
const base = await startTestServer(bridge);
|
|
222
|
+
const res = await fetch(`${base}/api/sessions/load`, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers: { "Content-Type": "application/json" },
|
|
225
|
+
body: JSON.stringify({ label: "test-session" }),
|
|
226
|
+
});
|
|
227
|
+
assert.equal(res.status, 200);
|
|
228
|
+
const body = (await res.json());
|
|
229
|
+
assert.equal(body.label, "test-session");
|
|
230
|
+
});
|
|
231
|
+
test("POST /api/chat returns agent response", async () => {
|
|
232
|
+
const bridge = new MockBridge();
|
|
233
|
+
const base = await startTestServer(bridge);
|
|
234
|
+
const res = await fetch(`${base}/api/chat`, {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: { "Content-Type": "application/json" },
|
|
237
|
+
body: JSON.stringify({ message: "hello" }),
|
|
238
|
+
});
|
|
239
|
+
assert.equal(res.status, 200);
|
|
240
|
+
const body = (await res.json());
|
|
241
|
+
assert.equal(body.text, "Echo: hello");
|
|
242
|
+
assert.equal(body.usage.inputTokens, 10);
|
|
243
|
+
assert.equal(body.usage.outputTokens, 5);
|
|
244
|
+
});
|
|
245
|
+
test("POST /api/chat returns 400 when message is missing", async () => {
|
|
246
|
+
const bridge = new MockBridge();
|
|
247
|
+
const base = await startTestServer(bridge);
|
|
248
|
+
const res = await fetch(`${base}/api/chat`, {
|
|
249
|
+
method: "POST",
|
|
250
|
+
headers: { "Content-Type": "application/json" },
|
|
251
|
+
body: JSON.stringify({}),
|
|
252
|
+
});
|
|
253
|
+
assert.equal(res.status, 400);
|
|
254
|
+
});
|
|
255
|
+
test("POST /api/chat returns 429 when agent is busy", async () => {
|
|
256
|
+
const bridge = new MockBridge();
|
|
257
|
+
bridge.setBusy(true);
|
|
258
|
+
const base = await startTestServer(bridge);
|
|
259
|
+
const res = await fetch(`${base}/api/chat`, {
|
|
260
|
+
method: "POST",
|
|
261
|
+
headers: { "Content-Type": "application/json" },
|
|
262
|
+
body: JSON.stringify({ message: "hello" }),
|
|
263
|
+
});
|
|
264
|
+
assert.equal(res.status, 429);
|
|
265
|
+
});
|
|
266
|
+
// ── OpenAI-compatible API tests ──
|
|
267
|
+
test("GET /v1/models returns minicode-agent model", async () => {
|
|
268
|
+
const bridge = new MockBridge();
|
|
269
|
+
const base = await startTestServer(bridge);
|
|
270
|
+
const res = await fetch(`${base}/v1/models`);
|
|
271
|
+
assert.equal(res.status, 200);
|
|
272
|
+
const body = (await res.json());
|
|
273
|
+
assert.equal(body.object, "list");
|
|
274
|
+
assert.equal(body.data.length, 1);
|
|
275
|
+
assert.equal(body.data[0].id, "minicode-agent");
|
|
276
|
+
assert.equal(body.data[0].owned_by, "minicode");
|
|
277
|
+
});
|
|
278
|
+
test("POST /v1/chat/completions non-streaming returns OpenAI-format response", async () => {
|
|
279
|
+
const bridge = new MockBridge();
|
|
280
|
+
const base = await startTestServer(bridge);
|
|
281
|
+
const res = await fetch(`${base}/v1/chat/completions`, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: { "Content-Type": "application/json" },
|
|
284
|
+
body: JSON.stringify({
|
|
285
|
+
model: "minicode-agent",
|
|
286
|
+
messages: [{ role: "user", content: "list files" }],
|
|
287
|
+
}),
|
|
288
|
+
});
|
|
289
|
+
assert.equal(res.status, 200);
|
|
290
|
+
const body = (await res.json());
|
|
291
|
+
assert.ok(body.id.startsWith("chatcmpl-"));
|
|
292
|
+
assert.equal(body.object, "chat.completion");
|
|
293
|
+
assert.equal(body.model, "minicode-agent");
|
|
294
|
+
assert.equal(body.choices.length, 1);
|
|
295
|
+
assert.equal(body.choices[0].message.role, "assistant");
|
|
296
|
+
assert.equal(body.choices[0].message.content, "Echo: list files");
|
|
297
|
+
assert.equal(body.choices[0].finish_reason, "stop");
|
|
298
|
+
assert.equal(body.usage.prompt_tokens, 10);
|
|
299
|
+
assert.equal(body.usage.completion_tokens, 5);
|
|
300
|
+
assert.equal(body.usage.total_tokens, 15);
|
|
301
|
+
});
|
|
302
|
+
test("POST /v1/chat/completions returns 400 for empty messages", async () => {
|
|
303
|
+
const bridge = new MockBridge();
|
|
304
|
+
const base = await startTestServer(bridge);
|
|
305
|
+
const res = await fetch(`${base}/v1/chat/completions`, {
|
|
306
|
+
method: "POST",
|
|
307
|
+
headers: { "Content-Type": "application/json" },
|
|
308
|
+
body: JSON.stringify({ model: "minicode-agent", messages: [] }),
|
|
309
|
+
});
|
|
310
|
+
assert.equal(res.status, 400);
|
|
311
|
+
});
|
|
312
|
+
test("POST /v1/chat/completions returns 400 for no user message", async () => {
|
|
313
|
+
const bridge = new MockBridge();
|
|
314
|
+
const base = await startTestServer(bridge);
|
|
315
|
+
const res = await fetch(`${base}/v1/chat/completions`, {
|
|
316
|
+
method: "POST",
|
|
317
|
+
headers: { "Content-Type": "application/json" },
|
|
318
|
+
body: JSON.stringify({
|
|
319
|
+
model: "minicode-agent",
|
|
320
|
+
messages: [{ role: "system", content: "You are helpful" }],
|
|
321
|
+
}),
|
|
322
|
+
});
|
|
323
|
+
assert.equal(res.status, 400);
|
|
324
|
+
});
|
|
325
|
+
test("POST /v1/chat/completions returns 429 when busy", async () => {
|
|
326
|
+
const bridge = new MockBridge();
|
|
327
|
+
bridge.setBusy(true);
|
|
328
|
+
const base = await startTestServer(bridge);
|
|
329
|
+
const res = await fetch(`${base}/v1/chat/completions`, {
|
|
330
|
+
method: "POST",
|
|
331
|
+
headers: { "Content-Type": "application/json" },
|
|
332
|
+
body: JSON.stringify({
|
|
333
|
+
model: "minicode-agent",
|
|
334
|
+
messages: [{ role: "user", content: "hello" }],
|
|
335
|
+
}),
|
|
336
|
+
});
|
|
337
|
+
assert.equal(res.status, 429);
|
|
338
|
+
});
|
|
339
|
+
test("POST /v1/chat/completions streaming returns SSE chunks", async () => {
|
|
340
|
+
const bridge = new MockBridge();
|
|
341
|
+
const base = await startTestServer(bridge);
|
|
342
|
+
const res = await fetch(`${base}/v1/chat/completions`, {
|
|
343
|
+
method: "POST",
|
|
344
|
+
headers: { "Content-Type": "application/json" },
|
|
345
|
+
body: JSON.stringify({
|
|
346
|
+
model: "minicode-agent",
|
|
347
|
+
messages: [{ role: "user", content: "stream test" }],
|
|
348
|
+
stream: true,
|
|
349
|
+
}),
|
|
350
|
+
});
|
|
351
|
+
assert.equal(res.status, 200);
|
|
352
|
+
assert.equal(res.headers.get("content-type"), "text/event-stream");
|
|
353
|
+
const text = await res.text();
|
|
354
|
+
const lines = text.split("\n").filter((l) => l.startsWith("data: "));
|
|
355
|
+
// Should have at least: role chunk, finish chunk, [DONE]
|
|
356
|
+
assert.ok(lines.length >= 2, `Expected at least 2 data lines, got ${lines.length}`);
|
|
357
|
+
// First chunk should have role
|
|
358
|
+
const firstChunk = JSON.parse(lines[0].slice(6));
|
|
359
|
+
assert.equal(firstChunk.choices[0].delta.role, "assistant");
|
|
360
|
+
// Last data line before [DONE] should have finish_reason
|
|
361
|
+
const lastDataLine = lines.filter((l) => l !== "data: [DONE]").pop();
|
|
362
|
+
const lastChunk = JSON.parse(lastDataLine.slice(6));
|
|
363
|
+
assert.equal(lastChunk.choices[0].finish_reason, "stop");
|
|
364
|
+
// Should end with [DONE]
|
|
365
|
+
assert.ok(text.includes("data: [DONE]"));
|
|
366
|
+
});
|
|
367
|
+
// ── Static file serving tests ──
|
|
368
|
+
test("GET / serves index.html", async () => {
|
|
369
|
+
const bridge = new MockBridge();
|
|
370
|
+
const base = await startTestServer(bridge);
|
|
371
|
+
const res = await fetch(`${base}/`);
|
|
372
|
+
assert.equal(res.status, 200);
|
|
373
|
+
assert.equal(res.headers.get("content-type"), "text/html");
|
|
374
|
+
const html = await res.text();
|
|
375
|
+
assert.ok(html.includes("minicode"));
|
|
376
|
+
});
|
|
377
|
+
test("GET /style.css serves CSS file", async () => {
|
|
378
|
+
const bridge = new MockBridge();
|
|
379
|
+
const base = await startTestServer(bridge);
|
|
380
|
+
const res = await fetch(`${base}/style.css`);
|
|
381
|
+
assert.equal(res.status, 200);
|
|
382
|
+
assert.equal(res.headers.get("content-type"), "text/css");
|
|
383
|
+
});
|
|
384
|
+
test("GET /app.js serves JS file", async () => {
|
|
385
|
+
const bridge = new MockBridge();
|
|
386
|
+
const base = await startTestServer(bridge);
|
|
387
|
+
const res = await fetch(`${base}/app.js`);
|
|
388
|
+
assert.equal(res.status, 200);
|
|
389
|
+
assert.equal(res.headers.get("content-type"), "application/javascript");
|
|
390
|
+
});
|
|
391
|
+
test("GET /nonexistent returns 404", async () => {
|
|
392
|
+
const bridge = new MockBridge();
|
|
393
|
+
const base = await startTestServer(bridge);
|
|
394
|
+
const res = await fetch(`${base}/nonexistent.html`);
|
|
395
|
+
assert.equal(res.status, 404);
|
|
396
|
+
});
|
|
397
|
+
// ── CLI args tests ──
|
|
398
|
+
test("parseCliArgs parses serve subcommand", async () => {
|
|
399
|
+
const { parseCliArgs } = await import("../src/cli/args.js");
|
|
400
|
+
const parsed = parseCliArgs(["node", "minicode", "serve"]);
|
|
401
|
+
assert.equal(parsed.serve, true);
|
|
402
|
+
assert.equal(parsed.port, 4567);
|
|
403
|
+
});
|
|
404
|
+
test("parseCliArgs parses serve with --port", async () => {
|
|
405
|
+
const { parseCliArgs } = await import("../src/cli/args.js");
|
|
406
|
+
const parsed = parseCliArgs(["node", "minicode", "serve", "--port", "8080"]);
|
|
407
|
+
assert.equal(parsed.serve, true);
|
|
408
|
+
assert.equal(parsed.port, 8080);
|
|
409
|
+
});
|
|
410
|
+
test("parseCliArgs parses serve with --port=value", async () => {
|
|
411
|
+
const { parseCliArgs } = await import("../src/cli/args.js");
|
|
412
|
+
const parsed = parseCliArgs(["node", "minicode", "serve", "--port=9090"]);
|
|
413
|
+
assert.equal(parsed.serve, true);
|
|
414
|
+
assert.equal(parsed.port, 9090);
|
|
415
|
+
});
|
|
416
|
+
test("validateCliArgs rejects serve with --oneshot", async () => {
|
|
417
|
+
const { validateCliArgs, CliUsageError } = await import("../src/cli/args.js");
|
|
418
|
+
assert.throws(() => validateCliArgs({ verbose: false, oneshot: true, json: false, serve: true, port: 4567, task: "test" }), (err) => err instanceof CliUsageError);
|
|
419
|
+
});
|
|
420
|
+
// ── Graph / Index API tests ──
|
|
421
|
+
test("GET /api/symbols returns all symbols", async () => {
|
|
422
|
+
const bridge = new MockBridge();
|
|
423
|
+
const base = await startTestServer(bridge);
|
|
424
|
+
const res = await fetch(`${base}/api/symbols`);
|
|
425
|
+
assert.equal(res.status, 200);
|
|
426
|
+
const body = (await res.json());
|
|
427
|
+
assert.equal(body.symbols.length, 3);
|
|
428
|
+
assert.equal(body.symbols[0].name, "foo");
|
|
429
|
+
assert.equal(body.symbols[1].name, "Bar");
|
|
430
|
+
assert.equal(body.symbols[2].name, "helper");
|
|
431
|
+
});
|
|
432
|
+
test("GET /api/symbols/:name/dependencies returns dependency cone", async () => {
|
|
433
|
+
const bridge = new MockBridge();
|
|
434
|
+
const base = await startTestServer(bridge);
|
|
435
|
+
const res = await fetch(`${base}/api/symbols/foo/dependencies`);
|
|
436
|
+
assert.equal(res.status, 200);
|
|
437
|
+
const body = (await res.json());
|
|
438
|
+
assert.equal(body.symbol, "foo");
|
|
439
|
+
assert.equal(body.dependencies.length, 2);
|
|
440
|
+
});
|
|
441
|
+
test("GET /api/symbols/:name/dependencies returns 404 for unknown symbol", async () => {
|
|
442
|
+
const bridge = new MockBridge();
|
|
443
|
+
const base = await startTestServer(bridge);
|
|
444
|
+
const res = await fetch(`${base}/api/symbols/nonexistent/dependencies`);
|
|
445
|
+
assert.equal(res.status, 404);
|
|
446
|
+
});
|
|
447
|
+
test("GET /api/symbols/:name/references returns references", async () => {
|
|
448
|
+
const bridge = new MockBridge();
|
|
449
|
+
const base = await startTestServer(bridge);
|
|
450
|
+
const res = await fetch(`${base}/api/symbols/Bar/references`);
|
|
451
|
+
assert.equal(res.status, 200);
|
|
452
|
+
const body = (await res.json());
|
|
453
|
+
assert.equal(body.symbol, "Bar");
|
|
454
|
+
assert.equal(body.references.length, 1);
|
|
455
|
+
assert.equal(body.references[0].from, "foo");
|
|
456
|
+
});
|
|
457
|
+
test("GET /api/symbols/:name/references returns 404 for unknown symbol", async () => {
|
|
458
|
+
const bridge = new MockBridge();
|
|
459
|
+
const base = await startTestServer(bridge);
|
|
460
|
+
const res = await fetch(`${base}/api/symbols/nonexistent/references`);
|
|
461
|
+
assert.equal(res.status, 404);
|
|
462
|
+
});
|
|
463
|
+
test("GET /api/code-map returns code map", async () => {
|
|
464
|
+
const bridge = new MockBridge();
|
|
465
|
+
const base = await startTestServer(bridge);
|
|
466
|
+
const res = await fetch(`${base}/api/code-map`);
|
|
467
|
+
assert.equal(res.status, 200);
|
|
468
|
+
const body = (await res.json());
|
|
469
|
+
assert.ok(body.text.includes("foo"));
|
|
470
|
+
assert.equal(body.shownCount, 1);
|
|
471
|
+
assert.equal(body.totalCount, 3);
|
|
472
|
+
});
|
|
473
|
+
test("GET /api/graph returns nodes and edges", async () => {
|
|
474
|
+
const bridge = new MockBridge();
|
|
475
|
+
const base = await startTestServer(bridge);
|
|
476
|
+
const res = await fetch(`${base}/api/graph`);
|
|
477
|
+
assert.equal(res.status, 200);
|
|
478
|
+
const body = (await res.json());
|
|
479
|
+
assert.equal(body.nodes.length, 2);
|
|
480
|
+
assert.equal(body.edges.length, 1);
|
|
481
|
+
assert.equal(body.edges[0].from, "foo");
|
|
482
|
+
assert.equal(body.edges[0].to, "Bar");
|
|
483
|
+
assert.equal(body.edges[0].kind, "calls");
|
|
484
|
+
});
|
|
485
|
+
test("GET /api/focus returns pinned symbols", async () => {
|
|
486
|
+
const bridge = new MockBridge();
|
|
487
|
+
const base = await startTestServer(bridge);
|
|
488
|
+
const res = await fetch(`${base}/api/focus`);
|
|
489
|
+
assert.equal(res.status, 200);
|
|
490
|
+
const body = (await res.json());
|
|
491
|
+
assert.deepEqual(body.pinned, []);
|
|
492
|
+
});
|
|
493
|
+
test("POST /api/focus pins and unpins symbols", async () => {
|
|
494
|
+
const bridge = new MockBridge();
|
|
495
|
+
const base = await startTestServer(bridge);
|
|
496
|
+
// Pin
|
|
497
|
+
const pinRes = await fetch(`${base}/api/focus`, {
|
|
498
|
+
method: "POST",
|
|
499
|
+
headers: { "Content-Type": "application/json" },
|
|
500
|
+
body: JSON.stringify({ action: "pin", symbol: "foo" }),
|
|
501
|
+
});
|
|
502
|
+
assert.equal(pinRes.status, 200);
|
|
503
|
+
const pinBody = (await pinRes.json());
|
|
504
|
+
assert.ok(pinBody.pinned.includes("foo"));
|
|
505
|
+
// Unpin
|
|
506
|
+
const unpinRes = await fetch(`${base}/api/focus`, {
|
|
507
|
+
method: "POST",
|
|
508
|
+
headers: { "Content-Type": "application/json" },
|
|
509
|
+
body: JSON.stringify({ action: "unpin", symbol: "foo" }),
|
|
510
|
+
});
|
|
511
|
+
assert.equal(unpinRes.status, 200);
|
|
512
|
+
const unpinBody = (await unpinRes.json());
|
|
513
|
+
assert.ok(!unpinBody.pinned.includes("foo"));
|
|
514
|
+
});
|
|
515
|
+
test("POST /api/focus returns 404 for unknown symbol", async () => {
|
|
516
|
+
const bridge = new MockBridge();
|
|
517
|
+
const base = await startTestServer(bridge);
|
|
518
|
+
const res = await fetch(`${base}/api/focus`, {
|
|
519
|
+
method: "POST",
|
|
520
|
+
headers: { "Content-Type": "application/json" },
|
|
521
|
+
body: JSON.stringify({ action: "pin", symbol: "nonexistent" }),
|
|
522
|
+
});
|
|
523
|
+
assert.equal(res.status, 404);
|
|
524
|
+
});
|
|
525
|
+
test("POST /api/focus returns 400 for invalid action", async () => {
|
|
526
|
+
const bridge = new MockBridge();
|
|
527
|
+
const base = await startTestServer(bridge);
|
|
528
|
+
const res = await fetch(`${base}/api/focus`, {
|
|
529
|
+
method: "POST",
|
|
530
|
+
headers: { "Content-Type": "application/json" },
|
|
531
|
+
body: JSON.stringify({ action: "toggle", symbol: "foo" }),
|
|
532
|
+
});
|
|
533
|
+
assert.equal(res.status, 400);
|
|
534
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CodeMapResult } from "../prompt/system-prompt.js";
|
|
2
2
|
import { Session } from "../session/session.js";
|
|
3
|
+
import type { CompactionResult } from "../session/session.js";
|
|
3
4
|
import { ToolRegistry } from "../tools/registry.js";
|
|
4
5
|
import type { AgentConfig, ModelClient } from "./types.js";
|
|
5
6
|
export type UiUpdateThinking = {
|
|
@@ -36,17 +37,45 @@ export declare class CodingAgent {
|
|
|
36
37
|
private readonly verbose;
|
|
37
38
|
private readonly onProgress;
|
|
38
39
|
private readonly onUiUpdate;
|
|
40
|
+
/**
|
|
41
|
+
* Tracks symbol names the user/agent has been working with.
|
|
42
|
+
* Persists across turns so the code map stays focused on the
|
|
43
|
+
* current area of interest.
|
|
44
|
+
*/
|
|
45
|
+
private readonly focusedSymbols;
|
|
46
|
+
private focusGeneration;
|
|
47
|
+
/**
|
|
48
|
+
* Cache of recently read file paths (key: "path:offset:limit") to avoid
|
|
49
|
+
* sending duplicate full file contents through the context window.
|
|
50
|
+
* Maps to the step number when the file was last read.
|
|
51
|
+
*/
|
|
52
|
+
private readonly fileReadCache;
|
|
39
53
|
constructor(params: {
|
|
40
54
|
config: AgentConfig;
|
|
41
55
|
modelClient: ModelClient;
|
|
42
56
|
toolRegistry: ToolRegistry;
|
|
43
57
|
session?: Session;
|
|
44
|
-
getCodeMap?: () => CodeMapResult | undefined;
|
|
58
|
+
getCodeMap?: (focusSymbols?: Set<string>) => CodeMapResult | undefined;
|
|
45
59
|
verbose?: boolean;
|
|
46
60
|
onProgress?: (message: string) => void;
|
|
47
61
|
onUiUpdate?: (event: UiUpdate) => void;
|
|
48
62
|
});
|
|
49
63
|
getSession(): Session;
|
|
64
|
+
/**
|
|
65
|
+
* Manually compact the conversation context.
|
|
66
|
+
* Uses LLM-based summarization when compactionModel is configured,
|
|
67
|
+
* otherwise falls back to mechanical compaction.
|
|
68
|
+
*/
|
|
69
|
+
compactContext(): Promise<CompactionResult | null>;
|
|
70
|
+
private addFocusSymbol;
|
|
71
|
+
private getFocusSet;
|
|
72
|
+
/**
|
|
73
|
+
* Check whether a previously-read file's content is still present in the
|
|
74
|
+
* session context (i.e. hasn't been trimmed/compacted away). We look for
|
|
75
|
+
* a tool message from "read_file" whose content still contains the file
|
|
76
|
+
* path and hasn't been replaced with a summary stub.
|
|
77
|
+
*/
|
|
78
|
+
private isFileReadStillInContext;
|
|
50
79
|
runTurn(userMessage: string, options?: {
|
|
51
80
|
signal?: AbortSignal;
|
|
52
81
|
}): Promise<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/agent/agent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAY,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/agent/agent.ts"],"names":[],"mappings":"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"}
|