min-agent 0.1.0 → 0.1.1
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 +6 -0
- package/bin/min-agent.js +2 -2
- package/dist/agent.d.ts +23 -0
- package/dist/agent.js +566 -0
- package/dist/assistant-stream.d.ts +23 -0
- package/dist/assistant-stream.js +114 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +471 -0
- package/dist/compaction.d.ts +14 -0
- package/dist/compaction.js +99 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +142 -0
- package/dist/confirm.d.ts +6 -0
- package/dist/confirm.js +37 -0
- package/dist/instructions.d.ts +1 -0
- package/dist/instructions.js +115 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +130 -0
- package/dist/mcp.d.ts +61 -0
- package/dist/mcp.js +237 -0
- package/dist/memory.d.ts +31 -0
- package/dist/memory.js +131 -0
- package/dist/output.d.ts +6 -0
- package/dist/output.js +52 -0
- package/dist/plugins.d.ts +2 -0
- package/dist/plugins.js +66 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +41 -0
- package/dist/serve.d.ts +9 -0
- package/dist/serve.js +351 -0
- package/dist/sessions.d.ts +17 -0
- package/dist/sessions.js +74 -0
- package/dist/skills.d.ts +12 -0
- package/dist/skills.js +127 -0
- package/dist/tool-output.d.ts +31 -0
- package/dist/tool-output.js +119 -0
- package/dist/tools/bash.d.ts +6 -0
- package/dist/tools/bash.js +93 -0
- package/dist/tools/edit.d.ts +7 -0
- package/dist/tools/edit.js +51 -0
- package/dist/tools/glob.d.ts +6 -0
- package/dist/tools/glob.js +36 -0
- package/dist/tools/grep.d.ts +7 -0
- package/dist/tools/grep.js +35 -0
- package/dist/tools/index.d.ts +37 -0
- package/dist/tools/index.js +20 -0
- package/dist/tools/read.d.ts +7 -0
- package/dist/tools/read.js +36 -0
- package/dist/tools/web_fetch.d.ts +6 -0
- package/dist/tools/web_fetch.js +83 -0
- package/dist/tools/web_search.d.ts +6 -0
- package/dist/tools/web_search.js +40 -0
- package/dist/tools/write.d.ts +6 -0
- package/dist/tools/write.js +32 -0
- package/package.json +4 -5
- package/src/agent.ts +0 -609
- package/src/assistant-stream.ts +0 -128
- package/src/cli.ts +0 -494
- package/src/compaction.ts +0 -119
- package/src/config.ts +0 -172
- package/src/confirm.ts +0 -42
- package/src/instructions.ts +0 -123
- package/src/markdown.ts +0 -140
- package/src/mcp.ts +0 -300
- package/src/memory.ts +0 -164
- package/src/output.ts +0 -58
- package/src/plugins.ts +0 -94
- package/src/provider.ts +0 -50
- package/src/serve.ts +0 -400
- package/src/sessions.ts +0 -94
- package/src/skills.ts +0 -146
- package/src/tool-output.ts +0 -146
- package/src/tools/bash.ts +0 -108
- package/src/tools/edit.ts +0 -65
- package/src/tools/glob.ts +0 -37
- package/src/tools/grep.ts +0 -37
- package/src/tools/index.ts +0 -21
- package/src/tools/read.ts +0 -38
- package/src/tools/web_fetch.ts +0 -87
- package/src/tools/web_search.ts +0 -42
- package/src/tools/write.ts +0 -36
- package/tsconfig.json +0 -15
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ModelMessage } from "ai";
|
|
2
|
+
export interface SessionMeta {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
created: string;
|
|
6
|
+
updated: string;
|
|
7
|
+
messageCount: number;
|
|
8
|
+
}
|
|
9
|
+
interface SessionData {
|
|
10
|
+
meta: SessionMeta;
|
|
11
|
+
messages: ModelMessage[];
|
|
12
|
+
}
|
|
13
|
+
export declare function saveSession(messages: ModelMessage[], existingId?: string): string;
|
|
14
|
+
export declare function loadSession(id: string): SessionData | null;
|
|
15
|
+
export declare function listSessions(): SessionMeta[];
|
|
16
|
+
export declare function deleteSession(id: string): boolean;
|
|
17
|
+
export {};
|
package/dist/sessions.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getConfigDir } from "./config.js";
|
|
4
|
+
function getSessionsDir() {
|
|
5
|
+
return path.join(getConfigDir(), "sessions");
|
|
6
|
+
}
|
|
7
|
+
function sessionPath(id) {
|
|
8
|
+
return path.join(getSessionsDir(), `${id}.json`);
|
|
9
|
+
}
|
|
10
|
+
function generateId() {
|
|
11
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
12
|
+
}
|
|
13
|
+
function deriveTitle(messages) {
|
|
14
|
+
const first = messages.find((m) => m.role === "user");
|
|
15
|
+
if (!first)
|
|
16
|
+
return "Untitled";
|
|
17
|
+
const content = typeof first.content === "string" ? first.content : "";
|
|
18
|
+
return content.slice(0, 60) || "Untitled";
|
|
19
|
+
}
|
|
20
|
+
export function saveSession(messages, existingId) {
|
|
21
|
+
const dir = getSessionsDir();
|
|
22
|
+
mkdirSync(dir, { recursive: true });
|
|
23
|
+
const id = existingId ?? generateId();
|
|
24
|
+
const now = new Date().toISOString();
|
|
25
|
+
const data = {
|
|
26
|
+
meta: {
|
|
27
|
+
id,
|
|
28
|
+
title: deriveTitle(messages),
|
|
29
|
+
created: existingId ? loadSession(id)?.meta.created ?? now : now,
|
|
30
|
+
updated: now,
|
|
31
|
+
messageCount: messages.length,
|
|
32
|
+
},
|
|
33
|
+
messages,
|
|
34
|
+
};
|
|
35
|
+
writeFileSync(sessionPath(id), JSON.stringify(data, null, 2), "utf-8");
|
|
36
|
+
return id;
|
|
37
|
+
}
|
|
38
|
+
export function loadSession(id) {
|
|
39
|
+
const file = sessionPath(id);
|
|
40
|
+
if (!existsSync(file))
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(readFileSync(file, "utf-8"));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function listSessions() {
|
|
50
|
+
const dir = getSessionsDir();
|
|
51
|
+
if (!existsSync(dir))
|
|
52
|
+
return [];
|
|
53
|
+
return readdirSync(dir)
|
|
54
|
+
.filter((f) => f.endsWith(".json"))
|
|
55
|
+
.map((f) => {
|
|
56
|
+
try {
|
|
57
|
+
const data = JSON.parse(readFileSync(path.join(dir, f), "utf-8"));
|
|
58
|
+
return data.meta;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
.filter((m) => m !== null)
|
|
65
|
+
.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
66
|
+
}
|
|
67
|
+
export function deleteSession(id) {
|
|
68
|
+
const file = sessionPath(id);
|
|
69
|
+
if (!existsSync(file))
|
|
70
|
+
return false;
|
|
71
|
+
const { unlinkSync } = require("fs");
|
|
72
|
+
unlinkSync(file);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Tool } from "ai";
|
|
2
|
+
export interface SkillInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
location: string;
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function discoverSkills(): void;
|
|
9
|
+
export declare function getSkills(): SkillInfo[];
|
|
10
|
+
export declare function getSkill(name: string): SkillInfo | undefined;
|
|
11
|
+
export declare function getSkillsTool(): Tool;
|
|
12
|
+
export declare function getSkillsSystemPrompt(): string;
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { tool, jsonSchema } from "ai";
|
|
2
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { globSync } from "glob";
|
|
6
|
+
/**
|
|
7
|
+
* Skill scan order: later entries win on duplicate `name` in frontmatter.
|
|
8
|
+
* Global user skills first, then project-local dirs so repo skills override ~/.agents.
|
|
9
|
+
*/
|
|
10
|
+
const SKILL_DIRS = [
|
|
11
|
+
path.join(os.homedir(), ".agents", "skills"),
|
|
12
|
+
path.join(process.cwd(), ".min-agent", "skills"),
|
|
13
|
+
path.join(process.cwd(), ".agent-demo", "skills"),
|
|
14
|
+
path.join(process.cwd(), ".opencode", "skills"),
|
|
15
|
+
path.join(process.cwd(), ".claude", "skills"),
|
|
16
|
+
];
|
|
17
|
+
let loadedSkills = {};
|
|
18
|
+
export function discoverSkills() {
|
|
19
|
+
loadedSkills = {};
|
|
20
|
+
for (const dir of SKILL_DIRS) {
|
|
21
|
+
if (!existsSync(dir))
|
|
22
|
+
continue;
|
|
23
|
+
const matches = globSync("**/SKILL.md", { cwd: dir, absolute: true });
|
|
24
|
+
for (const match of matches) {
|
|
25
|
+
const skill = parseSkillFile(match);
|
|
26
|
+
if (skill) {
|
|
27
|
+
loadedSkills[skill.name] = skill;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const count = Object.keys(loadedSkills).length;
|
|
32
|
+
if (count > 0) {
|
|
33
|
+
console.log(`\x1b[90m Skills loaded: ${count} (${Object.keys(loadedSkills).join(", ")})\x1b[0m`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function parseSkillFile(filePath) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
39
|
+
// Parse frontmatter (---\n...\n---)
|
|
40
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
41
|
+
if (!fmMatch)
|
|
42
|
+
return null;
|
|
43
|
+
const frontmatter = fmMatch[1];
|
|
44
|
+
const content = fmMatch[2];
|
|
45
|
+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
46
|
+
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
47
|
+
if (!nameMatch || !descMatch)
|
|
48
|
+
return null;
|
|
49
|
+
return {
|
|
50
|
+
name: nameMatch[1].trim().replace(/^["']|["']$/g, ""),
|
|
51
|
+
description: descMatch[1].trim().replace(/^["']|["']$/g, ""),
|
|
52
|
+
location: filePath,
|
|
53
|
+
content: content.trim(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function getSkills() {
|
|
61
|
+
return Object.values(loadedSkills);
|
|
62
|
+
}
|
|
63
|
+
export function getSkill(name) {
|
|
64
|
+
return loadedSkills[name];
|
|
65
|
+
}
|
|
66
|
+
export function getSkillsTool() {
|
|
67
|
+
return tool({
|
|
68
|
+
description: buildSkillDescription(),
|
|
69
|
+
inputSchema: jsonSchema({
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
name: { type: "string", description: "The name of the skill to load" },
|
|
73
|
+
},
|
|
74
|
+
required: ["name"],
|
|
75
|
+
}),
|
|
76
|
+
execute: async ({ name }) => {
|
|
77
|
+
const skill = loadedSkills[name];
|
|
78
|
+
if (!skill) {
|
|
79
|
+
const available = Object.keys(loadedSkills);
|
|
80
|
+
return `Skill "${name}" not found. Available skills: ${available.length ? available.join(", ") : "none"}`;
|
|
81
|
+
}
|
|
82
|
+
const dir = path.dirname(skill.location);
|
|
83
|
+
let files = [];
|
|
84
|
+
try {
|
|
85
|
+
files = readdirSync(dir)
|
|
86
|
+
.filter((f) => f !== "SKILL.md" && !statSync(path.join(dir, f)).isDirectory())
|
|
87
|
+
.slice(0, 10);
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
return [
|
|
91
|
+
`<skill_content name="${skill.name}">`,
|
|
92
|
+
`# Skill: ${skill.name}`,
|
|
93
|
+
"",
|
|
94
|
+
skill.content,
|
|
95
|
+
"",
|
|
96
|
+
`Base directory: ${dir}`,
|
|
97
|
+
"",
|
|
98
|
+
files.length ? `<skill_files>\n${files.map((f) => ` ${f}`).join("\n")}\n</skill_files>` : "",
|
|
99
|
+
`</skill_content>`,
|
|
100
|
+
]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join("\n");
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
export function getSkillsSystemPrompt() {
|
|
107
|
+
const skills = Object.values(loadedSkills);
|
|
108
|
+
if (skills.length === 0)
|
|
109
|
+
return "";
|
|
110
|
+
return [
|
|
111
|
+
"## Available Skills",
|
|
112
|
+
"Use the `skill` tool to load specialized instructions when a task matches a skill's description.",
|
|
113
|
+
"",
|
|
114
|
+
...skills.map((s) => `- **${s.name}**: ${s.description}`),
|
|
115
|
+
].join("\n");
|
|
116
|
+
}
|
|
117
|
+
function buildSkillDescription() {
|
|
118
|
+
const skills = Object.values(loadedSkills);
|
|
119
|
+
if (skills.length === 0)
|
|
120
|
+
return "Load a specialized skill. No skills are currently available.";
|
|
121
|
+
return [
|
|
122
|
+
"Load a specialized skill that provides domain-specific instructions and workflows.",
|
|
123
|
+
"",
|
|
124
|
+
"Available skills:",
|
|
125
|
+
...skills.map((s) => `- ${s.name}: ${s.description}`),
|
|
126
|
+
].join("\n");
|
|
127
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Default limits (aligned with common agent tooling practice). */
|
|
2
|
+
export declare const TOOL_OUTPUT_MAX_LINES = 2000;
|
|
3
|
+
export declare const TOOL_OUTPUT_MAX_BYTES: number;
|
|
4
|
+
export type TruncateDirection = "head" | "tail";
|
|
5
|
+
export interface TruncateToolOutputOptions {
|
|
6
|
+
maxLines?: number;
|
|
7
|
+
maxBytes?: number;
|
|
8
|
+
direction?: TruncateDirection;
|
|
9
|
+
}
|
|
10
|
+
export interface TruncateToolOutputResult {
|
|
11
|
+
content: string;
|
|
12
|
+
truncated: boolean;
|
|
13
|
+
outputPath?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Write full text to ~/.min-agent/tool-output/ and return absolute path. */
|
|
16
|
+
export declare function writeFullToolOutput(fullText: string): string;
|
|
17
|
+
/** Keep end of text within line/byte limits (good for shell logs). */
|
|
18
|
+
export declare function tailPreview(text: string, maxLines: number, maxBytes: number): {
|
|
19
|
+
text: string;
|
|
20
|
+
cut: boolean;
|
|
21
|
+
};
|
|
22
|
+
/** Keep start of text within line/byte limits (good for files / HTTP bodies). */
|
|
23
|
+
export declare function headPreview(text: string, maxLines: number, maxBytes: number): {
|
|
24
|
+
text: string;
|
|
25
|
+
cut: boolean;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* If text exceeds limits, write full text to disk and return a preview + path hint.
|
|
29
|
+
* Otherwise returns the original string.
|
|
30
|
+
*/
|
|
31
|
+
export declare function truncateToolOutput(text: string, options?: TruncateToolOutputOptions): TruncateToolOutputResult;
|