min-agent 0.1.0 → 0.1.2

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 (56) hide show
  1. package/README.md +6 -0
  2. package/bin/min-agent.js +2 -2
  3. package/dist/agent.js +566 -0
  4. package/dist/assistant-stream.js +114 -0
  5. package/dist/cli.js +471 -0
  6. package/dist/compaction.js +99 -0
  7. package/dist/config.js +142 -0
  8. package/dist/confirm.js +37 -0
  9. package/dist/instructions.js +115 -0
  10. package/dist/markdown.js +130 -0
  11. package/dist/mcp.js +237 -0
  12. package/dist/memory.js +131 -0
  13. package/dist/output.js +52 -0
  14. package/dist/plugins.js +66 -0
  15. package/dist/provider.js +41 -0
  16. package/dist/serve.js +351 -0
  17. package/dist/sessions.js +74 -0
  18. package/dist/skills.js +127 -0
  19. package/dist/tool-output.js +119 -0
  20. package/dist/tools/bash.js +93 -0
  21. package/dist/tools/edit.js +51 -0
  22. package/dist/tools/glob.js +36 -0
  23. package/dist/tools/grep.js +35 -0
  24. package/dist/tools/index.js +20 -0
  25. package/dist/tools/read.js +36 -0
  26. package/dist/tools/web_fetch.js +83 -0
  27. package/dist/tools/web_search.js +40 -0
  28. package/dist/tools/write.js +32 -0
  29. package/package.json +4 -5
  30. package/src/agent.ts +0 -609
  31. package/src/assistant-stream.ts +0 -128
  32. package/src/cli.ts +0 -494
  33. package/src/compaction.ts +0 -119
  34. package/src/config.ts +0 -172
  35. package/src/confirm.ts +0 -42
  36. package/src/instructions.ts +0 -123
  37. package/src/markdown.ts +0 -140
  38. package/src/mcp.ts +0 -300
  39. package/src/memory.ts +0 -164
  40. package/src/output.ts +0 -58
  41. package/src/plugins.ts +0 -94
  42. package/src/provider.ts +0 -50
  43. package/src/serve.ts +0 -400
  44. package/src/sessions.ts +0 -94
  45. package/src/skills.ts +0 -146
  46. package/src/tool-output.ts +0 -146
  47. package/src/tools/bash.ts +0 -108
  48. package/src/tools/edit.ts +0 -65
  49. package/src/tools/glob.ts +0 -37
  50. package/src/tools/grep.ts +0 -37
  51. package/src/tools/index.ts +0 -21
  52. package/src/tools/read.ts +0 -38
  53. package/src/tools/web_fetch.ts +0 -87
  54. package/src/tools/web_search.ts +0 -42
  55. package/src/tools/write.ts +0 -36
  56. package/tsconfig.json +0 -15
package/dist/memory.js ADDED
@@ -0,0 +1,131 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import path from "path";
3
+ import { getConfigDir } from "./config.js";
4
+ import { tool, jsonSchema } from "ai";
5
+ function getMemoryFile() {
6
+ return path.join(getConfigDir(), "memory.json");
7
+ }
8
+ export function loadMemories() {
9
+ const file = getMemoryFile();
10
+ if (!existsSync(file))
11
+ return [];
12
+ try {
13
+ return JSON.parse(readFileSync(file, "utf-8"));
14
+ }
15
+ catch {
16
+ return [];
17
+ }
18
+ }
19
+ function saveMemories(memories) {
20
+ const file = getMemoryFile();
21
+ mkdirSync(path.dirname(file), { recursive: true });
22
+ writeFileSync(file, JSON.stringify(memories, null, 2), "utf-8");
23
+ }
24
+ export function addMemory(content, tags = []) {
25
+ const memories = loadMemories();
26
+ const memory = {
27
+ content,
28
+ tags,
29
+ created: new Date().toISOString(),
30
+ };
31
+ memories.push(memory);
32
+ saveMemories(memories);
33
+ return memory;
34
+ }
35
+ export function deleteMemory(index) {
36
+ const memories = loadMemories();
37
+ if (index < 0 || index >= memories.length)
38
+ return false;
39
+ memories.splice(index, 1);
40
+ saveMemories(memories);
41
+ return true;
42
+ }
43
+ export function searchMemories(query) {
44
+ const memories = loadMemories();
45
+ const lower = query.toLowerCase();
46
+ return memories
47
+ .map((m, i) => ({ ...m, index: i }))
48
+ .filter((m) => m.content.toLowerCase().includes(lower) ||
49
+ m.tags.some((t) => t.toLowerCase().includes(lower)));
50
+ }
51
+ /** Build a system prompt section from stored memories */
52
+ export function getMemorySystemPrompt() {
53
+ const memories = loadMemories();
54
+ if (memories.length === 0)
55
+ return "";
56
+ const items = memories.map((m, i) => {
57
+ const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
58
+ return ` ${i + 1}. ${m.content}${tags}`;
59
+ });
60
+ return [
61
+ "## Memories",
62
+ "The following are things you have remembered from previous conversations. Use them to provide better, personalized responses.",
63
+ "You can save new memories with the memory_save tool when the user tells you something worth remembering (preferences, project details, conventions, etc).",
64
+ "",
65
+ ...items,
66
+ ].join("\n");
67
+ }
68
+ /** Create the memory tools for the agent */
69
+ export function getMemoryTools() {
70
+ const memorySave = tool({
71
+ description: "Save a memory for future conversations. Use this when the user shares preferences, project conventions, important context, or asks you to remember something. Memories persist across sessions.",
72
+ inputSchema: jsonSchema({
73
+ type: "object",
74
+ properties: {
75
+ content: { type: "string", description: "The information to remember" },
76
+ tags: {
77
+ type: "array",
78
+ items: { type: "string" },
79
+ description: "Optional tags for categorization (e.g. 'preference', 'project', 'convention')",
80
+ },
81
+ },
82
+ required: ["content"],
83
+ }),
84
+ execute: async ({ content, tags }) => {
85
+ const memory = addMemory(content, tags ?? []);
86
+ return `Saved memory: "${content}" (tags: ${memory.tags.length > 0 ? memory.tags.join(", ") : "none"})`;
87
+ },
88
+ });
89
+ const memorySearch = tool({
90
+ description: "Search through saved memories by keyword. Use this to recall previously saved information.",
91
+ inputSchema: jsonSchema({
92
+ type: "object",
93
+ properties: {
94
+ query: { type: "string", description: "Search keyword or phrase" },
95
+ },
96
+ required: ["query"],
97
+ }),
98
+ execute: async ({ query }) => {
99
+ const results = searchMemories(query);
100
+ if (results.length === 0)
101
+ return `No memories found matching "${query}"`;
102
+ return results
103
+ .map((m) => {
104
+ const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
105
+ return `#${m.index + 1}: ${m.content}${tags} (${m.created.split("T")[0]})`;
106
+ })
107
+ .join("\n");
108
+ },
109
+ });
110
+ const memoryDelete = tool({
111
+ description: "Delete a memory by its number. Use memory_search or memory_list first to find the index.",
112
+ inputSchema: jsonSchema({
113
+ type: "object",
114
+ properties: {
115
+ index: { type: "number", description: "The memory number to delete (1-based)" },
116
+ },
117
+ required: ["index"],
118
+ }),
119
+ execute: async ({ index }) => {
120
+ const success = deleteMemory(index - 1);
121
+ if (success)
122
+ return `Memory #${index} deleted.`;
123
+ return `Memory #${index} not found.`;
124
+ },
125
+ });
126
+ return {
127
+ memory_save: memorySave,
128
+ memory_search: memorySearch,
129
+ memory_delete: memoryDelete,
130
+ };
131
+ }
package/dist/output.js ADDED
@@ -0,0 +1,52 @@
1
+ import { loadConfig } from "./config.js";
2
+ const COLORS = {
3
+ reset: "\x1b[0m",
4
+ dim: "\x1b[2m",
5
+ bold: "\x1b[1m",
6
+ cyan: "\x1b[36m",
7
+ green: "\x1b[32m",
8
+ yellow: "\x1b[33m",
9
+ red: "\x1b[31m",
10
+ magenta: "\x1b[35m",
11
+ gray: "\x1b[90m",
12
+ };
13
+ export function printHeader(modelId) {
14
+ const config = loadConfig();
15
+ const model = modelId ?? config.provider?.defaultModel ?? "unknown";
16
+ console.log(`${COLORS.bold}🤖 min-agent${COLORS.reset} ${COLORS.dim}(${model})${COLORS.reset}`);
17
+ }
18
+ export function printDivider() {
19
+ console.log(`${COLORS.dim}${"─".repeat(60)}${COLORS.reset}`);
20
+ }
21
+ export function printToolCall(name, input) {
22
+ const argsStr = formatArgs(input);
23
+ console.log(`\n${COLORS.yellow}⚡ ${name}${COLORS.reset} ${COLORS.dim}${argsStr}${COLORS.reset}`);
24
+ }
25
+ export function printToolResult(name, result) {
26
+ const output = typeof result === "string" ? result : JSON.stringify(result, null, 2);
27
+ const lines = output.split("\n");
28
+ const maxLines = 20;
29
+ const truncated = lines.length > maxLines;
30
+ const preview = truncated ? lines.slice(0, maxLines).join("\n") : output;
31
+ const display = preview.length > 500 ? preview.slice(0, 500) + "..." : preview;
32
+ const suffix = truncated ? ` (${lines.length - maxLines} more lines)` : "";
33
+ console.log(`${COLORS.green} ✓${COLORS.reset} ${COLORS.dim}${display}${suffix}${COLORS.reset}\n`);
34
+ }
35
+ export function printDone(steps, usage) {
36
+ const input = usage.inputTokens ?? 0;
37
+ const output = usage.outputTokens ?? 0;
38
+ const total = input + output;
39
+ console.log(`${COLORS.dim}Done in ${steps} step(s) | Tokens: ${input} in / ${output} out / ${total} total${COLORS.reset}`);
40
+ }
41
+ function formatArgs(args) {
42
+ if (!args || typeof args !== "object")
43
+ return "";
44
+ const entries = Object.entries(args);
45
+ if (entries.length === 0)
46
+ return "";
47
+ const parts = entries.map(([k, v]) => {
48
+ const val = typeof v === "string" ? (v.length > 60 ? v.slice(0, 60) + "..." : v) : JSON.stringify(v);
49
+ return `${k}=${val}`;
50
+ });
51
+ return parts.join(" ");
52
+ }
@@ -0,0 +1,66 @@
1
+ import { tool, jsonSchema } from "ai";
2
+ import { existsSync, readdirSync } from "fs";
3
+ import { pathToFileURL } from "url";
4
+ import path from "path";
5
+ import { getConfigDir } from "./config.js";
6
+ const PLUGIN_DIRS = [
7
+ path.join(process.cwd(), ".min-agent", "tools"),
8
+ path.join(getConfigDir(), "tools"),
9
+ ];
10
+ export async function loadPluginTools() {
11
+ const tools = {};
12
+ for (const dir of PLUGIN_DIRS) {
13
+ if (!existsSync(dir))
14
+ continue;
15
+ const files = readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".mjs"));
16
+ for (const file of files) {
17
+ const filePath = path.join(dir, file);
18
+ const namespace = path.basename(file, path.extname(file));
19
+ try {
20
+ const mod = await import(pathToFileURL(filePath).href);
21
+ for (const [exportName, def] of Object.entries(mod)) {
22
+ if (!isPluginTool(def))
23
+ continue;
24
+ const toolId = exportName === "default" ? namespace : `${namespace}_${exportName}`;
25
+ const properties = {};
26
+ const required = [];
27
+ for (const [key, param] of Object.entries(def.parameters)) {
28
+ properties[key] = { type: param.type, description: param.description };
29
+ required.push(key);
30
+ }
31
+ tools[toolId] = tool({
32
+ description: def.description,
33
+ inputSchema: jsonSchema({
34
+ type: "object",
35
+ properties,
36
+ required,
37
+ }),
38
+ execute: async (args) => {
39
+ try {
40
+ const result = await def.execute(args);
41
+ return typeof result === "string" ? result : JSON.stringify(result);
42
+ }
43
+ catch (err) {
44
+ return `Plugin error: ${err.message}`;
45
+ }
46
+ },
47
+ });
48
+ }
49
+ }
50
+ catch (err) {
51
+ console.error(`\x1b[90m Plugin "${file}" failed to load: ${err.message}\x1b[0m`);
52
+ }
53
+ }
54
+ }
55
+ const count = Object.keys(tools).length;
56
+ if (count > 0) {
57
+ console.log(`\x1b[90m Plugins loaded: ${count} tool(s)\x1b[0m`);
58
+ }
59
+ return tools;
60
+ }
61
+ function isPluginTool(value) {
62
+ if (!value || typeof value !== "object")
63
+ return false;
64
+ const obj = value;
65
+ return typeof obj.description === "string" && typeof obj.parameters === "object" && typeof obj.execute === "function";
66
+ }
@@ -0,0 +1,41 @@
1
+ import { createOpenAI } from "@ai-sdk/openai";
2
+ import { loadConfig } from "./config.js";
3
+ function normalizeOllamaBaseURL(baseURL) {
4
+ const trimmed = baseURL.replace(/\/$/, "");
5
+ return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
6
+ }
7
+ export function resolveModel(modelId) {
8
+ const config = loadConfig();
9
+ const provider = config.provider;
10
+ if (!provider?.baseURL || !provider?.apiKey) {
11
+ throw new Error("Not configured. Run: min-agent setup");
12
+ }
13
+ const id = modelId ?? provider.defaultModel;
14
+ if (!id) {
15
+ throw new Error("No model specified. Run: min-agent setup");
16
+ }
17
+ const type = provider.type ?? "openai-compatible";
18
+ switch (type) {
19
+ case "openai": {
20
+ const client = createOpenAI({ apiKey: provider.apiKey });
21
+ return client.chat(id);
22
+ }
23
+ case "ollama": {
24
+ // Ollama exposes an OpenAI-compatible API at /v1.
25
+ // Accept both "...:11434" and "...:11434/v1" in user config.
26
+ const client = createOpenAI({
27
+ baseURL: normalizeOllamaBaseURL(provider.baseURL),
28
+ apiKey: provider.apiKey || "ollama",
29
+ });
30
+ return client.chat(id);
31
+ }
32
+ case "openai-compatible":
33
+ default: {
34
+ const client = createOpenAI({
35
+ baseURL: provider.baseURL,
36
+ apiKey: provider.apiKey,
37
+ });
38
+ return client.chat(id);
39
+ }
40
+ }
41
+ }
package/dist/serve.js ADDED
@@ -0,0 +1,351 @@
1
+ /**
2
+ * HTTP API server: exposes chat / models / health for programmatic use.
3
+ * Run: min-agent serve [--host 127.0.0.1] [--port 8787]
4
+ */
5
+ import { createServer } from "http";
6
+ import { readFileSync, existsSync } from "fs";
7
+ import path from "path";
8
+ import { initMcp, shutdownMcp } from "./mcp.js";
9
+ import { discoverSkills } from "./skills.js";
10
+ import { loadInstructions } from "./instructions.js";
11
+ import { loadConfig, fetchModels, isConfigured } from "./config.js";
12
+ import { setAutoApprove } from "./confirm.js";
13
+ import { runOnce, buildUserContent } from "./agent.js";
14
+ import { loadSession, saveSession } from "./sessions.js";
15
+ const MAX_BODY_BYTES = 2 * 1024 * 1024;
16
+ const MAX_TOOL_RESULT_SSE_CHARS = 48_000;
17
+ function packageVersion() {
18
+ try {
19
+ const pkgPath = path.join(process.cwd(), "package.json");
20
+ if (existsSync(pkgPath)) {
21
+ const j = JSON.parse(readFileSync(pkgPath, "utf-8"));
22
+ return j.version ?? "0.0.0";
23
+ }
24
+ }
25
+ catch { }
26
+ return "0.0.0";
27
+ }
28
+ function corsHeaders() {
29
+ if (process.env.MIN_AGENT_SERVE_CORS === "1" || process.env.MIN_AGENT_SERVE_CORS === "true") {
30
+ return {
31
+ "Access-Control-Allow-Origin": "*",
32
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
33
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
34
+ };
35
+ }
36
+ return {};
37
+ }
38
+ function authOk(req) {
39
+ const token = process.env.MIN_AGENT_SERVE_TOKEN?.trim();
40
+ if (!token)
41
+ return true;
42
+ const h = req.headers.authorization?.trim();
43
+ if (!h?.startsWith("Bearer "))
44
+ return false;
45
+ return h.slice(7) === token;
46
+ }
47
+ function sendJson(res, status, body) {
48
+ const headers = {
49
+ "Content-Type": "application/json; charset=utf-8",
50
+ ...corsHeaders(),
51
+ };
52
+ res.writeHead(status, headers);
53
+ res.end(JSON.stringify(body));
54
+ }
55
+ function readBody(req) {
56
+ return new Promise((resolve, reject) => {
57
+ const chunks = [];
58
+ let total = 0;
59
+ req.on("data", (c) => {
60
+ total += c.length;
61
+ if (total > MAX_BODY_BYTES) {
62
+ reject(new Error("body_too_large"));
63
+ return;
64
+ }
65
+ chunks.push(c);
66
+ });
67
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
68
+ req.on("error", reject);
69
+ });
70
+ }
71
+ function sseWrite(res, obj) {
72
+ try {
73
+ if (res.writableEnded)
74
+ return;
75
+ res.write(`data: ${JSON.stringify(obj)}\n\n`);
76
+ }
77
+ catch {
78
+ /* client gone */
79
+ }
80
+ }
81
+ function truncateForJson(v, max) {
82
+ if (typeof v === "string" && v.length > max)
83
+ return v.slice(0, max) + `\n… [truncated ${v.length - max} chars]`;
84
+ return v;
85
+ }
86
+ function normalizeMessages(body) {
87
+ if (body.messages && Array.isArray(body.messages)) {
88
+ if (body.messages.length === 0)
89
+ return { ok: false, error: "messages must be non-empty" };
90
+ return { ok: true, messages: [...body.messages] };
91
+ }
92
+ if (typeof body.message === "string" && body.message.length > 0) {
93
+ return { ok: true, messages: [{ role: "user", content: body.message }] };
94
+ }
95
+ return { ok: false, error: "Provide `message` (string) or non-empty `messages` array" };
96
+ }
97
+ export async function runServe(opts = {}) {
98
+ if (!isConfigured()) {
99
+ console.error("Not configured. Run: min-agent setup");
100
+ process.exit(1);
101
+ }
102
+ const host = opts.host ?? process.env.MIN_AGENT_SERVE_HOST ?? "127.0.0.1";
103
+ const port = opts.port ?? parseInt(process.env.MIN_AGENT_SERVE_PORT ?? "8787", 10);
104
+ console.error("\x1b[33m⚠ min-agent serve: confirmations are auto-approved for this process (same as -y). Dangerous shell commands will run without prompts.\x1b[0m");
105
+ setAutoApprove(true);
106
+ console.error("\x1b[90m⟳ Initializing MCP, skills, instructions…\x1b[0m");
107
+ await initMcp();
108
+ discoverSkills();
109
+ let instructions = await loadInstructions();
110
+ const version = packageVersion();
111
+ const server = createServer(async (req, res) => {
112
+ const c = corsHeaders();
113
+ if (req.method === "OPTIONS") {
114
+ res.writeHead(204, c);
115
+ res.end();
116
+ return;
117
+ }
118
+ if (!authOk(req)) {
119
+ sendJson(res, 401, { error: "unauthorized", detail: "Set Authorization: Bearer <MIN_AGENT_SERVE_TOKEN> when MIN_AGENT_SERVE_TOKEN is set" });
120
+ return;
121
+ }
122
+ const url = new URL(req.url ?? "/", `http://${host}`);
123
+ const pathname = url.pathname.replace(/\/$/, "") || "/";
124
+ try {
125
+ if (req.method === "GET" && pathname === "/health") {
126
+ sendJson(res, 200, { ok: true, service: "min-agent", version });
127
+ return;
128
+ }
129
+ if (req.method === "GET" && pathname === "/v1/meta") {
130
+ sendJson(res, 200, {
131
+ version,
132
+ cwd: process.cwd(),
133
+ instructions_chars: instructions.join("\n").length,
134
+ });
135
+ return;
136
+ }
137
+ if (req.method === "GET" && pathname === "/v1/models") {
138
+ const config = loadConfig();
139
+ const base = config.provider?.baseURL;
140
+ const key = config.provider?.apiKey;
141
+ if (!base || !key) {
142
+ sendJson(res, 500, { error: "provider_not_configured" });
143
+ return;
144
+ }
145
+ const models = await fetchModels(base, key);
146
+ sendJson(res, 200, {
147
+ default_model: config.provider?.defaultModel ?? null,
148
+ models,
149
+ });
150
+ return;
151
+ }
152
+ if (req.method === "POST" && pathname === "/v1/chat/reload-instructions") {
153
+ instructions = await loadInstructions();
154
+ sendJson(res, 200, { ok: true, instructions_chars: instructions.join("\n").length });
155
+ return;
156
+ }
157
+ if (req.method === "POST" && pathname === "/v1/chat") {
158
+ if (req.headers["content-type"]?.split(";")[0]?.trim() !== "application/json") {
159
+ sendJson(res, 415, { error: "unsupported_media_type", detail: "Use Content-Type: application/json" });
160
+ return;
161
+ }
162
+ let raw;
163
+ try {
164
+ raw = await readBody(req);
165
+ }
166
+ catch (e) {
167
+ if (e?.message === "body_too_large") {
168
+ sendJson(res, 413, { error: "payload_too_large", max_bytes: MAX_BODY_BYTES });
169
+ return;
170
+ }
171
+ throw e;
172
+ }
173
+ let body;
174
+ try {
175
+ body = JSON.parse(raw);
176
+ }
177
+ catch {
178
+ sendJson(res, 400, { error: "invalid_json" });
179
+ return;
180
+ }
181
+ const modelId = typeof body.model === "string" ? body.model : undefined;
182
+ const stream = body.stream === true;
183
+ const sessionId = typeof body.session_id === "string" ? body.session_id : undefined;
184
+ let messages;
185
+ if (sessionId) {
186
+ const session = loadSession(sessionId);
187
+ if (!session) {
188
+ sendJson(res, 404, { error: "session_not_found", session_id: sessionId });
189
+ return;
190
+ }
191
+ if (typeof body.message !== "string" || !body.message.trim()) {
192
+ sendJson(res, 400, {
193
+ error: "session_requires_message",
194
+ detail: "With `session_id`, send a non-empty `message` for the new user turn",
195
+ });
196
+ return;
197
+ }
198
+ messages = [...session.messages];
199
+ const content = body.images && body.images.length > 0 ? await buildUserContent(body.message, body.images) : body.message;
200
+ messages.push({ role: "user", content });
201
+ }
202
+ else {
203
+ const norm = normalizeMessages(body);
204
+ if (!norm.ok) {
205
+ sendJson(res, 400, { error: "invalid_body", detail: norm.error });
206
+ return;
207
+ }
208
+ messages = norm.messages;
209
+ if (body.images && body.images.length > 0) {
210
+ const last = messages[messages.length - 1];
211
+ if (!last || last.role !== "user" || typeof body.message !== "string") {
212
+ sendJson(res, 400, {
213
+ error: "images_require_message",
214
+ detail: "With `images`, send a top-level `message` string for the user turn",
215
+ });
216
+ return;
217
+ }
218
+ messages[messages.length - 1] = {
219
+ role: "user",
220
+ content: await buildUserContent(body.message, body.images),
221
+ };
222
+ }
223
+ }
224
+ const abort = new AbortController();
225
+ req.on("close", () => abort.abort());
226
+ if (stream) {
227
+ res.writeHead(200, {
228
+ "Content-Type": "text/event-stream; charset=utf-8",
229
+ "Cache-Control": "no-cache, no-transform",
230
+ Connection: "keep-alive",
231
+ "X-Accel-Buffering": "no",
232
+ ...c,
233
+ });
234
+ res.flushHeaders?.();
235
+ const toolCalls = [];
236
+ const toolResults = [];
237
+ const callbacks = {
238
+ onAssistantDisplayDelta(delta) {
239
+ sseWrite(res, { type: "assistant", text: delta });
240
+ },
241
+ onThinkingDelta(delta) {
242
+ sseWrite(res, { type: "thinking", text: delta });
243
+ },
244
+ onToolCall(name, input) {
245
+ toolCalls.push({ name, input });
246
+ sseWrite(res, { type: "tool_call", name, input });
247
+ },
248
+ onToolResult(name, output) {
249
+ const out = truncateForJson(output, MAX_TOOL_RESULT_SSE_CHARS);
250
+ toolResults.push({ name, output: out });
251
+ sseWrite(res, { type: "tool_result", name, output: out });
252
+ },
253
+ onCompaction(line) {
254
+ sseWrite(res, { type: "compaction", line });
255
+ },
256
+ onStreamError(message) {
257
+ sseWrite(res, { type: "error", message });
258
+ },
259
+ onRunFinish(info) {
260
+ let saved;
261
+ if (sessionId && messages.length > 0) {
262
+ try {
263
+ saved = saveSession(messages, sessionId);
264
+ }
265
+ catch {
266
+ saved = undefined;
267
+ }
268
+ }
269
+ sseWrite(res, {
270
+ type: "done",
271
+ step_count: info.stepCount,
272
+ usage: info.usage,
273
+ has_error: info.hasError,
274
+ aborted: info.aborted,
275
+ session_id: saved,
276
+ messages,
277
+ });
278
+ if (!res.writableEnded)
279
+ res.end();
280
+ },
281
+ };
282
+ try {
283
+ await runOnce(messages, instructions, modelId, abort.signal, callbacks);
284
+ }
285
+ catch (err) {
286
+ if (!res.writableEnded) {
287
+ sseWrite(res, { type: "fatal", message: err?.message ?? String(err) });
288
+ res.end();
289
+ }
290
+ }
291
+ return;
292
+ }
293
+ const toolCalls = [];
294
+ const toolResults = [];
295
+ const finishBox = { info: null };
296
+ await runOnce(messages, instructions, modelId, abort.signal, {
297
+ onToolCall(name, input) {
298
+ toolCalls.push({ name, input });
299
+ },
300
+ onToolResult(name, output) {
301
+ toolResults.push({ name, output: truncateForJson(output, MAX_TOOL_RESULT_SSE_CHARS) });
302
+ },
303
+ onRunFinish(info) {
304
+ finishBox.info = info;
305
+ },
306
+ });
307
+ const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
308
+ let savedSession;
309
+ if (sessionId && messages.length > 0) {
310
+ try {
311
+ savedSession = saveSession(messages, sessionId);
312
+ }
313
+ catch {
314
+ savedSession = undefined;
315
+ }
316
+ }
317
+ const fi = finishBox.info;
318
+ sendJson(res, 200, {
319
+ messages,
320
+ assistant: lastAssistant ?? null,
321
+ tool_calls: toolCalls,
322
+ tool_results: toolResults,
323
+ session_id: savedSession,
324
+ step_count: fi?.stepCount ?? 0,
325
+ usage: fi?.usage ?? null,
326
+ has_error: fi?.hasError ?? false,
327
+ aborted: fi?.aborted ?? false,
328
+ });
329
+ return;
330
+ }
331
+ sendJson(res, 404, { error: "not_found", path: pathname });
332
+ }
333
+ catch (err) {
334
+ sendJson(res, 500, { error: "internal_error", message: err?.message ?? String(err) });
335
+ }
336
+ });
337
+ await new Promise((resolve, reject) => {
338
+ server.once("error", reject);
339
+ server.listen(port, host, () => {
340
+ console.error(`\x1b[32m✓ min-agent serve\x1b[0m http://${host}:${port} (API: docs/API.md)`);
341
+ resolve();
342
+ });
343
+ });
344
+ const shutdown = async () => {
345
+ await shutdownMcp();
346
+ server.close();
347
+ process.exit(0);
348
+ };
349
+ process.on("SIGINT", shutdown);
350
+ process.on("SIGTERM", shutdown);
351
+ }