@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.
Files changed (39) hide show
  1. package/README.md +20 -12
  2. package/dist/src/agent/config.js +14 -2
  3. package/dist/src/cli/args.js +31 -0
  4. package/dist/src/index.js +21 -2
  5. package/dist/src/indexer/code-map.js +52 -5
  6. package/dist/src/indexer/focus-tracker.js +63 -0
  7. package/dist/src/indexer/project-index.js +2 -2
  8. package/dist/src/serve/agent-bridge.js +233 -0
  9. package/dist/src/serve/openai-compat.js +144 -0
  10. package/dist/src/serve/server.js +251 -0
  11. package/dist/src/serve/types.js +2 -0
  12. package/dist/src/serve/websocket.js +28 -0
  13. package/dist/src/ui/cli-ink.js +22 -2
  14. package/dist/src/web/app.js +350 -0
  15. package/dist/src/web/index.html +49 -0
  16. package/dist/src/web/style.css +422 -0
  17. package/dist/tests/agent.test.js +62 -0
  18. package/dist/tests/cli-args.test.js +4 -2
  19. package/dist/tests/serve.integration.test.js +534 -0
  20. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts +30 -1
  21. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  22. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +212 -8
  23. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  24. package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +10 -0
  25. package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.map +1 -1
  26. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +1 -1
  27. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
  28. package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
  29. package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
  30. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +2 -0
  31. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
  32. package/node_modules/@minicode/agent-sdk/dist/src/session/session.d.ts +51 -1
  33. package/node_modules/@minicode/agent-sdk/dist/src/session/session.d.ts.map +1 -1
  34. package/node_modules/@minicode/agent-sdk/dist/src/session/session.js +210 -2
  35. package/node_modules/@minicode/agent-sdk/dist/src/session/session.js.map +1 -1
  36. package/node_modules/@minicode/agent-sdk/dist/tests/session.test.js +75 -0
  37. package/node_modules/@minicode/agent-sdk/dist/tests/session.test.js.map +1 -1
  38. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +5 -3
@@ -0,0 +1,144 @@
1
+ import { randomUUID } from "node:crypto";
2
+ function sendJson(res, status, body) {
3
+ res.writeHead(status, { "Content-Type": "application/json" });
4
+ res.end(JSON.stringify(body));
5
+ }
6
+ function readBody(req) {
7
+ return new Promise((resolve, reject) => {
8
+ const chunks = [];
9
+ req.on("data", (chunk) => chunks.push(chunk));
10
+ req.on("end", () => resolve(Buffer.concat(chunks).toString()));
11
+ req.on("error", reject);
12
+ });
13
+ }
14
+ export function handleModels(_req, res) {
15
+ sendJson(res, 200, {
16
+ object: "list",
17
+ data: [
18
+ {
19
+ id: "minicode-agent",
20
+ object: "model",
21
+ created: Math.floor(Date.now() / 1000),
22
+ owned_by: "minicode",
23
+ },
24
+ ],
25
+ });
26
+ }
27
+ export async function handleChatCompletions(req, res, bridge) {
28
+ let body;
29
+ try {
30
+ const raw = await readBody(req);
31
+ body = JSON.parse(raw);
32
+ }
33
+ catch {
34
+ sendJson(res, 400, { error: { message: "Invalid JSON body", type: "invalid_request_error" } });
35
+ return;
36
+ }
37
+ const messages = body.messages;
38
+ if (!messages || messages.length === 0) {
39
+ sendJson(res, 400, { error: { message: "messages array is required", type: "invalid_request_error" } });
40
+ return;
41
+ }
42
+ // Extract last user message
43
+ let userMessage;
44
+ for (let i = messages.length - 1; i >= 0; i--) {
45
+ if (messages[i].role === "user") {
46
+ userMessage = messages[i].content;
47
+ break;
48
+ }
49
+ }
50
+ if (!userMessage) {
51
+ sendJson(res, 400, { error: { message: "No user message found", type: "invalid_request_error" } });
52
+ return;
53
+ }
54
+ if (bridge.isBusy()) {
55
+ sendJson(res, 429, { error: { message: "Agent is busy with another request. Try again later.", type: "rate_limit_error" } });
56
+ return;
57
+ }
58
+ const completionId = `chatcmpl-${randomUUID()}`;
59
+ const created = Math.floor(Date.now() / 1000);
60
+ if (body.stream) {
61
+ await handleStreaming(res, bridge, userMessage, completionId, created);
62
+ }
63
+ else {
64
+ await handleNonStreaming(res, bridge, userMessage, completionId, created);
65
+ }
66
+ }
67
+ async function handleNonStreaming(res, bridge, message, completionId, created) {
68
+ try {
69
+ const result = await bridge.runTurn(message);
70
+ sendJson(res, 200, {
71
+ id: completionId,
72
+ object: "chat.completion",
73
+ created,
74
+ model: "minicode-agent",
75
+ choices: [
76
+ {
77
+ index: 0,
78
+ message: { role: "assistant", content: result.text },
79
+ finish_reason: "stop",
80
+ },
81
+ ],
82
+ usage: result.usage
83
+ ? {
84
+ prompt_tokens: result.usage.inputTokens,
85
+ completion_tokens: result.usage.outputTokens,
86
+ total_tokens: result.usage.inputTokens + result.usage.outputTokens,
87
+ }
88
+ : undefined,
89
+ });
90
+ }
91
+ catch (error) {
92
+ const msg = error instanceof Error ? error.message : "Unknown error";
93
+ sendJson(res, 500, { error: { message: msg, type: "server_error" } });
94
+ }
95
+ }
96
+ async function handleStreaming(res, bridge, message, completionId, created) {
97
+ res.writeHead(200, {
98
+ "Content-Type": "text/event-stream",
99
+ "Cache-Control": "no-cache",
100
+ Connection: "keep-alive",
101
+ });
102
+ function sendSSE(data) {
103
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
104
+ }
105
+ // Send initial role chunk
106
+ sendSSE({
107
+ id: completionId,
108
+ object: "chat.completion.chunk",
109
+ created,
110
+ model: "minicode-agent",
111
+ choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
112
+ });
113
+ const listener = (msg) => {
114
+ if (msg.type === "streaming_chunk" && msg.content) {
115
+ sendSSE({
116
+ id: completionId,
117
+ object: "chat.completion.chunk",
118
+ created,
119
+ model: "minicode-agent",
120
+ choices: [{ index: 0, delta: { content: msg.content }, finish_reason: null }],
121
+ });
122
+ }
123
+ };
124
+ bridge.addListener(listener);
125
+ try {
126
+ await bridge.runTurn(message);
127
+ sendSSE({
128
+ id: completionId,
129
+ object: "chat.completion.chunk",
130
+ created,
131
+ model: "minicode-agent",
132
+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
133
+ });
134
+ res.write("data: [DONE]\n\n");
135
+ }
136
+ catch (error) {
137
+ const msg = error instanceof Error ? error.message : "Unknown error";
138
+ sendSSE({ error: { message: msg, type: "server_error" } });
139
+ }
140
+ finally {
141
+ bridge.removeListener(listener);
142
+ res.end();
143
+ }
144
+ }
@@ -0,0 +1,251 @@
1
+ import { createServer } from "node:http";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { AgentBridge } from "./agent-bridge.js";
6
+ import { createWebSocketServer } from "./websocket.js";
7
+ import { handleChatCompletions, handleModels } from "./openai-compat.js";
8
+ import { formatConfigForDisplay } from "../agent/config.js";
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ // Resolve web dir: works in both dev (src/serve/) and dist (dist/src/serve/)
11
+ const webDir = __dirname.includes(`${path.sep}dist${path.sep}`)
12
+ ? path.resolve(__dirname, "../../src/web")
13
+ : path.resolve(__dirname, "../web");
14
+ const MIME_TYPES = {
15
+ ".html": "text/html",
16
+ ".css": "text/css",
17
+ ".js": "application/javascript",
18
+ ".json": "application/json",
19
+ };
20
+ function sendJson(res, status, body) {
21
+ res.writeHead(status, { "Content-Type": "application/json" });
22
+ res.end(JSON.stringify(body));
23
+ }
24
+ function readBody(req) {
25
+ return new Promise((resolve, reject) => {
26
+ const chunks = [];
27
+ req.on("data", (chunk) => chunks.push(chunk));
28
+ req.on("end", () => resolve(Buffer.concat(chunks).toString()));
29
+ req.on("error", reject);
30
+ });
31
+ }
32
+ async function serveStatic(res, urlPath) {
33
+ const fileName = urlPath === "/" ? "index.html" : urlPath.slice(1);
34
+ const filePath = path.join(webDir, fileName);
35
+ // Prevent path traversal
36
+ if (!filePath.startsWith(webDir)) {
37
+ res.writeHead(403);
38
+ res.end("Forbidden");
39
+ return;
40
+ }
41
+ try {
42
+ const content = await readFile(filePath);
43
+ const ext = path.extname(filePath);
44
+ const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
45
+ res.writeHead(200, { "Content-Type": contentType });
46
+ res.end(content);
47
+ }
48
+ catch {
49
+ res.writeHead(404);
50
+ res.end("Not Found");
51
+ }
52
+ }
53
+ /** Create the HTTP request handler. Exported for testing. */
54
+ export function createRequestHandler(bridge) {
55
+ const config = bridge.getConfig();
56
+ return (req, res) => {
57
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
58
+ const method = req.method ?? "GET";
59
+ const pathname = url.pathname;
60
+ const handle = async () => {
61
+ // OpenAI-compatible routes
62
+ if (pathname === "/v1/models" && method === "GET") {
63
+ handleModels(req, res);
64
+ return;
65
+ }
66
+ if (pathname === "/v1/chat/completions" && method === "POST") {
67
+ await handleChatCompletions(req, res, bridge);
68
+ return;
69
+ }
70
+ // Minicode REST API
71
+ if (pathname === "/api/status" && method === "GET") {
72
+ sendJson(res, 200, {
73
+ status: bridge.isBusy() ? "busy" : "ready",
74
+ workspace: config.workspaceRoot,
75
+ model: config.model,
76
+ provider: config.modelProvider,
77
+ });
78
+ return;
79
+ }
80
+ if (pathname === "/api/config" && method === "GET") {
81
+ sendJson(res, 200, { config: formatConfigForDisplay(config) });
82
+ return;
83
+ }
84
+ if (pathname === "/api/sessions" && method === "GET") {
85
+ const sessions = await bridge.listSess();
86
+ sendJson(res, 200, { sessions });
87
+ return;
88
+ }
89
+ if (pathname === "/api/sessions/save" && method === "POST") {
90
+ const body = JSON.parse(await readBody(req));
91
+ const meta = await bridge.saveSess(body.label);
92
+ sendJson(res, 200, meta);
93
+ return;
94
+ }
95
+ if (pathname === "/api/sessions/load" && method === "POST") {
96
+ const body = JSON.parse(await readBody(req));
97
+ const result = await bridge.loadSess(body.label);
98
+ if (!result) {
99
+ sendJson(res, 404, { error: "Session not found" });
100
+ return;
101
+ }
102
+ sendJson(res, 200, { label: result.label });
103
+ return;
104
+ }
105
+ // ── Graph / Index API ──
106
+ if (pathname === "/api/symbols" && method === "GET") {
107
+ if (!bridge.hasIndex()) {
108
+ sendJson(res, 404, { error: "No project index available" });
109
+ return;
110
+ }
111
+ sendJson(res, 200, { symbols: bridge.getSymbols() });
112
+ return;
113
+ }
114
+ if (pathname.startsWith("/api/symbols/") && pathname.endsWith("/dependencies") && method === "GET") {
115
+ const name = decodeURIComponent(pathname.slice("/api/symbols/".length, -"/dependencies".length));
116
+ const depthParam = url.searchParams.get("depth");
117
+ const depth = depthParam ? Number(depthParam) : undefined;
118
+ const result = bridge.getDependencies(name, depth);
119
+ if (!result) {
120
+ sendJson(res, 404, { error: `Symbol "${name}" not found` });
121
+ return;
122
+ }
123
+ sendJson(res, 200, { symbol: name, dependencies: result });
124
+ return;
125
+ }
126
+ if (pathname.startsWith("/api/symbols/") && pathname.endsWith("/references") && method === "GET") {
127
+ const name = decodeURIComponent(pathname.slice("/api/symbols/".length, -"/references".length));
128
+ const result = bridge.getReferences(name);
129
+ if (!result) {
130
+ sendJson(res, 404, { error: `Symbol "${name}" not found` });
131
+ return;
132
+ }
133
+ sendJson(res, 200, { symbol: name, references: result });
134
+ return;
135
+ }
136
+ if (pathname === "/api/code-map" && method === "GET") {
137
+ const budgetParam = url.searchParams.get("budget");
138
+ const budget = budgetParam ? Number(budgetParam) : undefined;
139
+ const result = bridge.getCodeMap(budget);
140
+ if (!result) {
141
+ sendJson(res, 404, { error: "No project index available" });
142
+ return;
143
+ }
144
+ sendJson(res, 200, result);
145
+ return;
146
+ }
147
+ if (pathname === "/api/graph" && method === "GET") {
148
+ const result = bridge.getGraph();
149
+ if (!result) {
150
+ sendJson(res, 404, { error: "No project index available" });
151
+ return;
152
+ }
153
+ sendJson(res, 200, result);
154
+ return;
155
+ }
156
+ if (pathname === "/api/focus" && method === "GET") {
157
+ sendJson(res, 200, { pinned: bridge.getPinnedSymbols() });
158
+ return;
159
+ }
160
+ if (pathname === "/api/focus" && method === "POST") {
161
+ const body = JSON.parse(await readBody(req));
162
+ if (!body.symbol || !body.action) {
163
+ sendJson(res, 400, { error: "action and symbol are required" });
164
+ return;
165
+ }
166
+ if (body.action === "pin") {
167
+ const ok = bridge.pinSymbol(body.symbol);
168
+ if (!ok) {
169
+ sendJson(res, 404, { error: `Symbol "${body.symbol}" not found` });
170
+ return;
171
+ }
172
+ sendJson(res, 200, { pinned: bridge.getPinnedSymbols() });
173
+ return;
174
+ }
175
+ if (body.action === "unpin") {
176
+ bridge.unpinSymbol(body.symbol);
177
+ sendJson(res, 200, { pinned: bridge.getPinnedSymbols() });
178
+ return;
179
+ }
180
+ sendJson(res, 400, { error: `Unknown action "${body.action}". Use "pin" or "unpin".` });
181
+ return;
182
+ }
183
+ if (pathname === "/api/chat" && method === "POST") {
184
+ const body = JSON.parse(await readBody(req));
185
+ if (!body.message) {
186
+ sendJson(res, 400, { error: "message is required" });
187
+ return;
188
+ }
189
+ if (bridge.isBusy()) {
190
+ sendJson(res, 429, { error: "Agent is busy" });
191
+ return;
192
+ }
193
+ try {
194
+ const result = await bridge.runTurn(body.message);
195
+ sendJson(res, 200, { text: result.text, usage: result.usage });
196
+ }
197
+ catch (error) {
198
+ const msg = error instanceof Error ? error.message : "Unknown error";
199
+ sendJson(res, 500, { error: msg });
200
+ }
201
+ return;
202
+ }
203
+ // Static files
204
+ await serveStatic(res, pathname);
205
+ };
206
+ handle().catch((error) => {
207
+ const msg = error instanceof Error ? error.message : "Unknown error";
208
+ sendJson(res, 500, { error: msg });
209
+ });
210
+ };
211
+ }
212
+ export async function runServe(verbose, port) {
213
+ console.log("Initializing agent...");
214
+ // Set up broadcast plumbing
215
+ let broadcastFn = () => { };
216
+ const bridge = new AgentBridge((msg) => broadcastFn(msg), verbose);
217
+ await bridge.init();
218
+ const config = bridge.getConfig();
219
+ const handler = createRequestHandler(bridge);
220
+ const server = createServer(handler);
221
+ // WebSocket server — captures the real broadcast function
222
+ const wss = createWebSocketServer(server, bridge);
223
+ // Wire up the broadcast: WS clients receive all agent events
224
+ const { WebSocket } = await import("ws");
225
+ broadcastFn = (msg) => {
226
+ const data = JSON.stringify(msg);
227
+ for (const client of wss.clients) {
228
+ if (client.readyState === WebSocket.OPEN) {
229
+ client.send(data);
230
+ }
231
+ }
232
+ };
233
+ // Graceful shutdown
234
+ process.on("SIGINT", () => {
235
+ console.log("\nShutting down...");
236
+ wss.close();
237
+ server.close(() => {
238
+ process.exit(0);
239
+ });
240
+ });
241
+ server.listen(port, "127.0.0.1", () => {
242
+ console.log(`\nminicode serve`);
243
+ console.log(` Workspace: ${config.workspaceRoot}`);
244
+ console.log(` Model: ${config.model} (${config.modelProvider})`);
245
+ console.log(` Web UI: http://localhost:${port}`);
246
+ console.log(` OpenAI: http://localhost:${port}/v1`);
247
+ console.log(`\nPress Ctrl+C to stop.\n`);
248
+ });
249
+ // Keep alive
250
+ await new Promise(() => { });
251
+ }
@@ -0,0 +1,2 @@
1
+ /** WebSocket message protocol types for minicode serve mode. */
2
+ export {};
@@ -0,0 +1,28 @@
1
+ import { WebSocketServer } from "ws";
2
+ export function createWebSocketServer(httpServer, bridge) {
3
+ const wss = new WebSocketServer({ server: httpServer });
4
+ wss.on("connection", (ws) => {
5
+ ws.on("message", (raw) => {
6
+ let msg;
7
+ try {
8
+ msg = JSON.parse(String(raw));
9
+ }
10
+ catch {
11
+ return;
12
+ }
13
+ if (msg.type === "chat") {
14
+ if (bridge.isBusy()) {
15
+ ws.send(JSON.stringify({ type: "busy" }));
16
+ return;
17
+ }
18
+ bridge.runTurn(msg.message).catch(() => {
19
+ // errors already broadcast via agent-bridge
20
+ });
21
+ }
22
+ else if (msg.type === "cancel") {
23
+ bridge.cancel();
24
+ }
25
+ });
26
+ });
27
+ return wss;
28
+ }
@@ -85,7 +85,7 @@ export async function runInkCli(verbose, initialTask) {
85
85
  verbose,
86
86
  ...(session ? { session } : {}),
87
87
  ...(projectIndex !== undefined
88
- ? { getCodeMap: () => projectIndex.getCodeMap() }
88
+ ? { getCodeMap: (focusSymbols) => projectIndex.getCodeMap(undefined, focusSymbols) }
89
89
  : {}),
90
90
  ...(verbose
91
91
  ? {
@@ -121,7 +121,7 @@ export async function runInkCli(verbose, initialTask) {
121
121
  if (trimmed === "/help") {
122
122
  store.addItem({
123
123
  type: "system",
124
- content: 'Commands: "/help", "/config", "/save [label]", "/load [label]", "/sessions", "/exit".',
124
+ content: 'Commands: "/help", "/config", "/compact", "/save [label]", "/load [label]", "/sessions", "/exit".',
125
125
  });
126
126
  return;
127
127
  }
@@ -132,6 +132,26 @@ export async function runInkCli(verbose, initialTask) {
132
132
  });
133
133
  return;
134
134
  }
135
+ if (trimmed === "/compact") {
136
+ const session = agent.getSession();
137
+ const result = await agent.compactContext();
138
+ if (result) {
139
+ const method = config.compactionModel ? "LLM" : "mechanical";
140
+ store.addItem({
141
+ type: "system",
142
+ content: `Compacted (${method}): ${result.removedMessages} messages summarized, ` +
143
+ `${result.previousTokens} → ${result.newTokens} tokens ` +
144
+ `(saved ${result.previousTokens - result.newTokens} tokens)`,
145
+ });
146
+ }
147
+ else {
148
+ store.addItem({
149
+ type: "system",
150
+ content: `Nothing to compact (${session.getTokenEstimate()} tokens, ${session.getMessages().length} messages).`,
151
+ });
152
+ }
153
+ return;
154
+ }
135
155
  if (trimmed === "/save" || trimmed.startsWith("/save ")) {
136
156
  const label = trimmed.slice("/save".length).trim() || undefined;
137
157
  try {