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
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Split "thinking" fragments (inline XML-style blocks some models leak into text)
3
+ * from displayable assistant text. Thinking is emitted to stderr; display goes to Markdown.
4
+ */
5
+ const BT = String.fromCharCode(96); // backtick — some models wrap thinking in `think` / `redacted_thinking`
6
+ const THINKING_BLOCKS = [
7
+ { open: "<think>", close: "</think>" },
8
+ { open: "<thinking>", close: "</thinking>" },
9
+ { open: `<${BT}think${BT}>`, close: `<${BT}/think${BT}>` },
10
+ { open: `<${BT}redacted_thinking${BT}>`, close: `<${BT}/redacted_thinking${BT}>` },
11
+ ];
12
+ /** Max tail to keep when looking for a partial opening tag. */
13
+ const PARTIAL_TAG_HOLD = 72;
14
+ function lower(s) {
15
+ return s.toLowerCase();
16
+ }
17
+ function findFirstOpen(buf) {
18
+ const l = lower(buf);
19
+ let best = null;
20
+ for (const tag of THINKING_BLOCKS) {
21
+ const i = l.indexOf(tag.open);
22
+ if (i >= 0 && (!best || i < best.index))
23
+ best = { index: i, tag };
24
+ }
25
+ return best;
26
+ }
27
+ /**
28
+ * Incremental filter: feed raw model text deltas; get display text (for stdout / history)
29
+ * and thinking text (for stderr). Handles tags split across chunks.
30
+ */
31
+ export class ThinkingBodySplitter {
32
+ buf = "";
33
+ feed(chunk) {
34
+ this.buf += chunk;
35
+ return this.drain(false);
36
+ }
37
+ /** End of stream: flush remainder; incomplete thinking block → stderr only. */
38
+ flush() {
39
+ return this.drain(true);
40
+ }
41
+ drain(isFinal) {
42
+ let display = "";
43
+ let thinking = "";
44
+ while (this.buf.length > 0) {
45
+ const open = findFirstOpen(this.buf);
46
+ if (!open) {
47
+ if (isFinal) {
48
+ display += this.buf;
49
+ this.buf = "";
50
+ }
51
+ else {
52
+ const holdFrom = lastPotentialPartialOpen(this.buf);
53
+ if (holdFrom >= 0) {
54
+ display += this.buf.slice(0, holdFrom);
55
+ this.buf = this.buf.slice(holdFrom);
56
+ }
57
+ else {
58
+ display += this.buf;
59
+ this.buf = "";
60
+ }
61
+ }
62
+ break;
63
+ }
64
+ if (open.index > 0) {
65
+ display += this.buf.slice(0, open.index);
66
+ this.buf = this.buf.slice(open.index);
67
+ }
68
+ const low = lower(this.buf);
69
+ if (!low.startsWith(open.tag.open)) {
70
+ this.buf = this.buf.slice(1);
71
+ continue;
72
+ }
73
+ const afterOpen = open.tag.open.length;
74
+ const closeRel = low.indexOf(open.tag.close, afterOpen);
75
+ if (closeRel < 0) {
76
+ if (isFinal) {
77
+ thinking += this.buf.slice(afterOpen);
78
+ this.buf = "";
79
+ }
80
+ break;
81
+ }
82
+ const inner = this.buf.slice(afterOpen, closeRel);
83
+ thinking += inner;
84
+ this.buf = this.buf.slice(closeRel + open.tag.close.length);
85
+ }
86
+ return { display, thinking };
87
+ }
88
+ }
89
+ /** If `buf` ends with `<...` that could still become a thinking open tag, return index of `<` to hold from; else -1. */
90
+ function lastPotentialPartialOpen(buf) {
91
+ const start = Math.max(0, buf.length - PARTIAL_TAG_HOLD);
92
+ const tail = buf.slice(start);
93
+ const lt = tail.lastIndexOf("<");
94
+ if (lt < 0)
95
+ return -1;
96
+ const globalLt = start + lt;
97
+ const cand = buf.slice(globalLt).toLowerCase();
98
+ if (cand.length > 64)
99
+ return -1;
100
+ for (const tag of THINKING_BLOCKS) {
101
+ if (tag.open.startsWith(cand))
102
+ return globalLt;
103
+ }
104
+ return -1;
105
+ }
106
+ /** Final strip for assistant message (complete tags only). */
107
+ export function stripThinkingFromAssistantText(text) {
108
+ let s = text;
109
+ for (const { open, close } of THINKING_BLOCKS) {
110
+ const re = new RegExp(open.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "[\\s\\S]*?" + close.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
111
+ s = s.replace(re, "");
112
+ }
113
+ return s;
114
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,471 @@
1
+ import { runAgent, runChat } from "./agent.js";
2
+ import { loadMcpConfig, saveMcpConfig, checkMcpServer, checkAllMcpServers, formatMcpServerBinding, } from "./mcp.js";
3
+ import { discoverSkills, getSkills } from "./skills.js";
4
+ import { loadMemories, addMemory, deleteMemory, searchMemories } from "./memory.js";
5
+ import { runSetup, isConfigured, loadConfig, fetchModels, getConfigDir, getRulesFile } from "./config.js";
6
+ import { setAutoApprove } from "./confirm.js";
7
+ import { existsSync, writeFileSync, mkdirSync } from "fs";
8
+ import path from "path";
9
+ const rawArgs = process.argv.slice(2);
10
+ // Extract leading global flags only, so subcommands can still use "-y"
11
+ const args = [...rawArgs];
12
+ let hasYes = false;
13
+ while (args[0] === "--yes" || args[0] === "-y") {
14
+ hasYes = true;
15
+ args.shift();
16
+ }
17
+ if (hasYes)
18
+ setAutoApprove(true);
19
+ function printUsage() {
20
+ console.log(`
21
+ min-agent - Minimal AI coding agent
22
+
23
+ Usage:
24
+ min-agent chat <message> Send a message to the agent
25
+ min-agent chat Start interactive multi-turn chat
26
+ min-agent chat --resume <id> Resume a previous session
27
+ min-agent setup Configure API provider (interactive)
28
+ min-agent models List available models
29
+ min-agent history List saved sessions
30
+ min-agent rules Show loaded instruction rules
31
+ min-agent rules edit Edit global rules file
32
+ min-agent memory List all memories
33
+ min-agent memory add <text> Add a memory manually
34
+ min-agent memory search <query> Search memories
35
+ min-agent memory delete <index> Delete a memory by number
36
+ min-agent mcp add <name> <cmd> Add a local MCP server (stdio)
37
+ min-agent mcp add <name> --url <url> [--sse] [--token <t>] Add remote MCP (HTTP)
38
+ min-agent mcp remove <name> Remove an MCP server
39
+ min-agent mcp list List configured MCP servers
40
+ min-agent mcp check Check MCP server availability
41
+ min-agent skills list List available skills
42
+ min-agent serve [--host H] [--port P] HTTP API (see docs/API.md)
43
+
44
+ Options:
45
+ --model, -m <model> Override model for this request
46
+ --image, -i <path> Attach an image (can be used multiple times)
47
+ --yes, -y Auto-approve all confirmations (dangerous commands, file overwrites)
48
+
49
+ Rules (loaded as system instructions):
50
+ Global: ~/.min-agent/rules.md
51
+ Project: ./AGENTS.md or ./RULES.md or ./.min-agent/AGENTS.md
52
+ Config: "instructions" array in ~/.min-agent/config.json
53
+
54
+ Examples:
55
+ min-agent setup
56
+ min-agent chat "hello"
57
+ min-agent chat
58
+ min-agent serve --port 8787
59
+ min-agent rules edit
60
+ min-agent mcp add filesystem npx -y @modelcontextprotocol/server-filesystem /tmp
61
+ min-agent mcp add remote --url https://example.com/mcp --token "$TOKEN"
62
+ `);
63
+ }
64
+ function parseMcpAddArgs(argv) {
65
+ let skipCheck = false;
66
+ let url;
67
+ let token;
68
+ let sse = false;
69
+ const cmd = [];
70
+ for (let i = 0; i < argv.length; i++) {
71
+ const a = argv[i];
72
+ if (a === "--skip-check") {
73
+ skipCheck = true;
74
+ continue;
75
+ }
76
+ if (a === "--sse") {
77
+ sse = true;
78
+ continue;
79
+ }
80
+ if (a === "--url" && argv[i + 1]) {
81
+ url = argv[++i];
82
+ continue;
83
+ }
84
+ if (a === "--token" && argv[i + 1]) {
85
+ token = argv[++i];
86
+ continue;
87
+ }
88
+ cmd.push(a);
89
+ }
90
+ return { skipCheck, url, token, sse, cmd };
91
+ }
92
+ function ensureProjectDefaults() {
93
+ const projectConfigDir = path.join(process.cwd(), ".min-agent");
94
+ const projectSkillsDir = path.join(projectConfigDir, "skills");
95
+ const projectMcpFile = path.join(projectConfigDir, "mcp.json");
96
+ if (!existsSync(projectSkillsDir)) {
97
+ mkdirSync(projectSkillsDir, { recursive: true });
98
+ }
99
+ if (!existsSync(projectMcpFile)) {
100
+ mkdirSync(projectConfigDir, { recursive: true });
101
+ writeFileSync(projectMcpFile, JSON.stringify({ mcpServers: {} }, null, 2), "utf-8");
102
+ }
103
+ }
104
+ async function main() {
105
+ ensureProjectDefaults();
106
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
107
+ printUsage();
108
+ process.exit(0);
109
+ }
110
+ const command = args[0];
111
+ switch (command) {
112
+ case "setup": {
113
+ await runSetup();
114
+ break;
115
+ }
116
+ case "models": {
117
+ const config = loadConfig();
118
+ if (!config.provider?.baseURL || !config.provider?.apiKey) {
119
+ console.error("Not configured. Run: min-agent setup");
120
+ process.exit(1);
121
+ }
122
+ console.log("Fetching models...");
123
+ const models = await fetchModels(config.provider.baseURL, config.provider.apiKey);
124
+ if (models.length === 0) {
125
+ console.log("No models found or unable to fetch model list.");
126
+ }
127
+ else {
128
+ console.log(`\nAvailable models (${models.length}):`);
129
+ for (const m of models) {
130
+ const marker = m === config.provider.defaultModel ? " ← default" : "";
131
+ console.log(` ${m}${marker}`);
132
+ }
133
+ }
134
+ break;
135
+ }
136
+ case "chat": {
137
+ if (!isConfigured()) {
138
+ console.error("Not configured. Run: min-agent setup");
139
+ process.exit(1);
140
+ }
141
+ // Parse --model / -m, --resume, and --image / -i flags
142
+ let modelOverride;
143
+ let resumeId;
144
+ const images = [];
145
+ const chatArgs = [];
146
+ for (let i = 1; i < args.length; i++) {
147
+ if ((args[i] === "--model" || args[i] === "-m") && args[i + 1]) {
148
+ modelOverride = args[++i];
149
+ }
150
+ else if (args[i] === "--resume" && args[i + 1]) {
151
+ resumeId = args[++i];
152
+ }
153
+ else if ((args[i] === "--image" || args[i] === "-i") && args[i + 1]) {
154
+ images.push(args[++i]);
155
+ }
156
+ else {
157
+ chatArgs.push(args[i]);
158
+ }
159
+ }
160
+ const message = chatArgs.join(" ");
161
+ if (!message) {
162
+ // No message provided — enter interactive multi-turn mode
163
+ await runChat(modelOverride, resumeId);
164
+ }
165
+ else {
166
+ await runAgent(message, modelOverride, images.length > 0 ? images : undefined);
167
+ }
168
+ break;
169
+ }
170
+ case "serve": {
171
+ if (!isConfigured()) {
172
+ console.error("Not configured. Run: min-agent setup");
173
+ process.exit(1);
174
+ }
175
+ let servePort;
176
+ let serveHost;
177
+ for (let i = 1; i < args.length; i++) {
178
+ if ((args[i] === "--port" || args[i] === "-p") && args[i + 1]) {
179
+ servePort = parseInt(args[++i], 10);
180
+ }
181
+ else if (args[i] === "--host" && args[i + 1]) {
182
+ serveHost = args[++i];
183
+ }
184
+ }
185
+ const { runServe } = await import("./serve.js");
186
+ await runServe({ port: servePort, host: serveHost });
187
+ break;
188
+ }
189
+ case "mcp": {
190
+ const subcommand = args[1];
191
+ switch (subcommand) {
192
+ case "add": {
193
+ const name = args[2];
194
+ if (!name) {
195
+ console.error("Usage: min-agent mcp add <name> [--skip-check] <command...>");
196
+ console.error(" or: min-agent mcp add <name> --url <https://host/mcp> [--sse] [--token <bearer>] [--skip-check]");
197
+ process.exit(1);
198
+ }
199
+ const { skipCheck, url, token, sse, cmd } = parseMcpAddArgs(args.slice(3));
200
+ let entry;
201
+ if (url?.trim()) {
202
+ entry = {
203
+ url: url.trim(),
204
+ enabled: true,
205
+ remoteTransport: sse ? "sse" : "auto",
206
+ };
207
+ if (token?.trim())
208
+ entry.token = token.trim();
209
+ }
210
+ else if (cmd.length > 0) {
211
+ entry = { command: cmd, enabled: true };
212
+ }
213
+ else {
214
+ console.error("Usage: min-agent mcp add <name> [--skip-check] <command...>");
215
+ console.error(" or: min-agent mcp add <name> --url <https://host/mcp> [--sse] [--token <bearer>] [--skip-check]");
216
+ process.exit(1);
217
+ }
218
+ let detectedTools = 0;
219
+ if (!skipCheck) {
220
+ console.log(`Validating MCP server "${name}"...`);
221
+ const checkResult = await checkMcpServer(name, entry);
222
+ if (!checkResult.ok) {
223
+ console.error(`MCP server "${name}" validation failed: ${checkResult.error ?? "unknown error"}`);
224
+ process.exit(1);
225
+ }
226
+ detectedTools = checkResult.toolCount;
227
+ }
228
+ else {
229
+ console.log(`Skipping MCP validation for "${name}" (--skip-check).`);
230
+ }
231
+ const config = loadMcpConfig();
232
+ config.mcpServers[name] = entry;
233
+ saveMcpConfig(config);
234
+ const toolsSuffix = skipCheck ? "" : ` (${detectedTools} tools detected)`;
235
+ console.log(`✓ MCP server "${name}" added: ${formatMcpServerBinding(entry)}${toolsSuffix}`);
236
+ break;
237
+ }
238
+ case "remove": {
239
+ const name = args[2];
240
+ if (!name) {
241
+ console.error("Usage: min-agent mcp remove <name>");
242
+ process.exit(1);
243
+ }
244
+ const config = loadMcpConfig();
245
+ if (!config.mcpServers[name]) {
246
+ console.error(`MCP server "${name}" not found`);
247
+ process.exit(1);
248
+ }
249
+ delete config.mcpServers[name];
250
+ saveMcpConfig(config);
251
+ console.log(`✓ MCP server "${name}" removed`);
252
+ break;
253
+ }
254
+ case "list": {
255
+ const config = loadMcpConfig();
256
+ const servers = Object.entries(config.mcpServers);
257
+ if (servers.length === 0) {
258
+ console.log("No MCP servers configured.");
259
+ console.log("Add one with: min-agent mcp add <name> <command...> or --url <https://...>");
260
+ }
261
+ else {
262
+ console.log("MCP Servers:");
263
+ for (const [name, cfg] of servers) {
264
+ const status = cfg.enabled === false ? " (disabled)" : "";
265
+ console.log(` ${name}: ${formatMcpServerBinding(cfg)}${status}`);
266
+ }
267
+ }
268
+ break;
269
+ }
270
+ case "check": {
271
+ const results = await checkAllMcpServers();
272
+ if (results.length === 0) {
273
+ console.log("No MCP servers configured.");
274
+ console.log("Add one with: min-agent mcp add <name> <command...> or --url <https://...>");
275
+ break;
276
+ }
277
+ let failed = 0;
278
+ console.log("MCP Check Results:");
279
+ for (const result of results) {
280
+ if (!result.enabled) {
281
+ console.log(` ${result.name}: skipped (disabled)`);
282
+ continue;
283
+ }
284
+ if (result.ok) {
285
+ console.log(` ${result.name}: ok (${result.toolCount} tools)`);
286
+ }
287
+ else {
288
+ failed++;
289
+ console.log(` ${result.name}: failed${result.error ? ` - ${result.error}` : ""}`);
290
+ }
291
+ }
292
+ if (failed > 0) {
293
+ console.error(`\n${failed} MCP server(s) failed.`);
294
+ process.exit(1);
295
+ }
296
+ else {
297
+ console.log("\nAll enabled MCP servers are available.");
298
+ }
299
+ break;
300
+ }
301
+ default:
302
+ console.error("Usage: min-agent mcp [add|remove|list|check]");
303
+ process.exit(1);
304
+ }
305
+ break;
306
+ }
307
+ case "history": {
308
+ const { listSessions } = await import("./sessions.js");
309
+ const sessions = listSessions();
310
+ if (sessions.length === 0) {
311
+ console.log("No saved sessions.");
312
+ console.log("Sessions are auto-saved when you exit interactive chat.");
313
+ }
314
+ else {
315
+ console.log(`Sessions (${sessions.length}):`);
316
+ for (const s of sessions.slice(0, 20)) {
317
+ const date = s.updated.split("T")[0];
318
+ console.log(` ${s.id} ${date} ${s.title} (${s.messageCount} msgs)`);
319
+ }
320
+ console.log("\nResume with: min-agent chat --resume <id>");
321
+ }
322
+ break;
323
+ }
324
+ case "memory": {
325
+ const subcommand = args[1];
326
+ switch (subcommand) {
327
+ case "add": {
328
+ const text = args.slice(2).join(" ");
329
+ if (!text) {
330
+ console.error("Usage: min-agent memory add <text>");
331
+ process.exit(1);
332
+ }
333
+ addMemory(text);
334
+ console.log(`✓ Memory saved: "${text}"`);
335
+ break;
336
+ }
337
+ case "search": {
338
+ const query = args.slice(2).join(" ");
339
+ if (!query) {
340
+ console.error("Usage: min-agent memory search <query>");
341
+ process.exit(1);
342
+ }
343
+ const results = searchMemories(query);
344
+ if (results.length === 0) {
345
+ console.log(`No memories matching "${query}"`);
346
+ }
347
+ else {
348
+ console.log(`Found ${results.length} memory(s):`);
349
+ for (const m of results) {
350
+ const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
351
+ console.log(` #${m.index + 1}: ${m.content}${tags}`);
352
+ }
353
+ }
354
+ break;
355
+ }
356
+ case "delete": {
357
+ const idx = parseInt(args[2]);
358
+ if (isNaN(idx)) {
359
+ console.error("Usage: min-agent memory delete <number>");
360
+ process.exit(1);
361
+ }
362
+ if (deleteMemory(idx - 1)) {
363
+ console.log(`✓ Memory #${idx} deleted`);
364
+ }
365
+ else {
366
+ console.error(`Memory #${idx} not found`);
367
+ }
368
+ break;
369
+ }
370
+ default: {
371
+ // List all memories
372
+ const memories = loadMemories();
373
+ if (memories.length === 0) {
374
+ console.log("No memories stored.");
375
+ console.log("The agent will automatically save memories during conversations.");
376
+ console.log("Or add manually: min-agent memory add \"prefer TypeScript over JavaScript\"");
377
+ }
378
+ else {
379
+ console.log(`Memories (${memories.length}):`);
380
+ for (let i = 0; i < memories.length; i++) {
381
+ const m = memories[i];
382
+ const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
383
+ const date = m.created.split("T")[0];
384
+ console.log(` #${i + 1}: ${m.content}${tags} (${date})`);
385
+ }
386
+ }
387
+ break;
388
+ }
389
+ }
390
+ break;
391
+ }
392
+ case "skills": {
393
+ const subcommand = args[1];
394
+ switch (subcommand) {
395
+ case "list": {
396
+ discoverSkills();
397
+ const skills = getSkills();
398
+ if (skills.length === 0) {
399
+ console.log("No skills found.");
400
+ console.log("Add skills by creating SKILL.md files in:");
401
+ console.log(" ~/.agents/skills/<name>/SKILL.md (global, all projects)");
402
+ console.log(" .min-agent/skills/<name>/SKILL.md");
403
+ console.log(" .opencode/skills/<name>/SKILL.md");
404
+ console.log("");
405
+ console.log("SKILL.md format:");
406
+ console.log(" ---");
407
+ console.log(" name: my-skill");
408
+ console.log(" description: What this skill does");
409
+ console.log(" ---");
410
+ console.log(" # Instructions content...");
411
+ }
412
+ else {
413
+ console.log("Available Skills:");
414
+ for (const skill of skills) {
415
+ console.log(` ${skill.name}: ${skill.description}`);
416
+ console.log(` ${skill.location}`);
417
+ }
418
+ }
419
+ break;
420
+ }
421
+ default:
422
+ console.error("Usage: min-agent skills [list]");
423
+ process.exit(1);
424
+ }
425
+ break;
426
+ }
427
+ case "rules": {
428
+ const subcommand = args[1];
429
+ if (subcommand === "edit") {
430
+ const rulesFile = getRulesFile();
431
+ if (!existsSync(rulesFile)) {
432
+ mkdirSync(getConfigDir(), { recursive: true });
433
+ writeFileSync(rulesFile, `# Global Agent Rules\n\n<!-- Add your custom instructions here. They will be included in every conversation. -->\n`, "utf-8");
434
+ }
435
+ const editor = process.env.EDITOR || "vi";
436
+ const { execSync } = await import("child_process");
437
+ execSync(`${editor} "${rulesFile}"`, { stdio: "inherit" });
438
+ }
439
+ else {
440
+ const { loadInstructions } = await import("./instructions.js");
441
+ const instructions = await loadInstructions();
442
+ if (instructions.length === 0) {
443
+ console.log("No instruction rules loaded.");
444
+ console.log("");
445
+ console.log("Add rules by creating:");
446
+ console.log(` Global: ${getRulesFile()}`);
447
+ console.log(" Project: ./AGENTS.md or ./RULES.md");
448
+ console.log("");
449
+ console.log("Or add paths/URLs in ~/.min-agent/config.json:");
450
+ console.log(' { "instructions": ["./docs/rules.md", "https://..."] }');
451
+ }
452
+ else {
453
+ console.log(`Loaded ${instructions.length} instruction source(s):\n`);
454
+ for (const inst of instructions) {
455
+ const firstLine = inst.split("\n")[0];
456
+ console.log(` ${firstLine}`);
457
+ }
458
+ }
459
+ }
460
+ break;
461
+ }
462
+ default:
463
+ console.error(`Unknown command: ${command}`);
464
+ printUsage();
465
+ process.exit(1);
466
+ }
467
+ }
468
+ main().catch((err) => {
469
+ console.error(`\x1b[31mFatal error: ${err.message}\x1b[0m`);
470
+ process.exit(1);
471
+ });
@@ -0,0 +1,99 @@
1
+ import { generateText } from "ai";
2
+ /**
3
+ * Context compaction system.
4
+ *
5
+ * When conversation history exceeds a token threshold, older messages are
6
+ * summarized into a compact form to free up context window space.
7
+ *
8
+ * Strategy:
9
+ * 1. Estimate token count of messages (rough: 1 token ≈ 4 chars for English, 2 chars for CJK)
10
+ * 2. When over threshold, take older messages and summarize them via the LLM
11
+ * 3. Replace old messages with a single system summary message
12
+ * 4. Keep recent N turns verbatim for continuity
13
+ */
14
+ const COMPACTION_PROMPT = `You are a conversation summarizer. Summarize the following conversation history into a concise but complete summary that preserves:
15
+ - Key decisions made
16
+ - Important context and facts discussed
17
+ - Current state of any tasks in progress
18
+ - User preferences mentioned
19
+ - File paths, code snippets, or technical details that are still relevant
20
+
21
+ Be concise but don't lose critical information. Output only the summary, no preamble.`;
22
+ // Default: trigger compaction at ~80% of context window
23
+ const DEFAULT_MAX_TOKENS = 128000;
24
+ const COMPACTION_RATIO = 0.75;
25
+ const KEEP_RECENT_TURNS = 4; // Keep last N user+assistant pairs verbatim
26
+ /** Rough token estimation */
27
+ export function estimateTokens(messages) {
28
+ let chars = 0;
29
+ for (const msg of messages) {
30
+ if (typeof msg.content === "string") {
31
+ chars += msg.content.length;
32
+ }
33
+ else if (Array.isArray(msg.content)) {
34
+ for (const part of msg.content) {
35
+ if ("text" in part && typeof part.text === "string") {
36
+ chars += part.text.length;
37
+ }
38
+ }
39
+ }
40
+ }
41
+ // Rough estimate: mix of English (~4 chars/token) and CJK (~2 chars/token)
42
+ return Math.ceil(chars / 3);
43
+ }
44
+ /** Check if compaction is needed */
45
+ export function needsCompaction(messages, config) {
46
+ const maxTokens = config?.maxTokens ?? DEFAULT_MAX_TOKENS;
47
+ const threshold = maxTokens * COMPACTION_RATIO;
48
+ return estimateTokens(messages) > threshold;
49
+ }
50
+ /** Compact messages by summarizing older history */
51
+ export async function compactMessages(messages, model, config) {
52
+ const keepTurns = config?.keepRecentTurns ?? KEEP_RECENT_TURNS;
53
+ if (messages.length <= keepTurns * 2) {
54
+ // Not enough messages to compact
55
+ return { messages, compacted: false };
56
+ }
57
+ // Split: older messages to summarize, recent messages to keep
58
+ const splitIdx = messages.length - keepTurns * 2;
59
+ const toSummarize = messages.slice(0, splitIdx);
60
+ const toKeep = messages.slice(splitIdx);
61
+ // Build conversation text for summarization
62
+ const conversationText = toSummarize
63
+ .map((msg) => {
64
+ const role = msg.role;
65
+ const content = typeof msg.content === "string"
66
+ ? msg.content
67
+ : Array.isArray(msg.content)
68
+ ? msg.content
69
+ .filter((p) => "text" in p)
70
+ .map((p) => p.text)
71
+ .join("\n")
72
+ : "";
73
+ return `[${role}]: ${content.slice(0, 2000)}`;
74
+ })
75
+ .join("\n\n");
76
+ try {
77
+ const result = await generateText({
78
+ model,
79
+ messages: [
80
+ { role: "system", content: COMPACTION_PROMPT },
81
+ { role: "user", content: `Summarize this conversation:\n\n${conversationText}` },
82
+ ],
83
+ });
84
+ const summary = result.text;
85
+ // Build compacted message list
86
+ const compactedMessages = [
87
+ {
88
+ role: "system",
89
+ content: `[Context Summary - Previous conversation was compacted]\n\n${summary}`,
90
+ },
91
+ ...toKeep,
92
+ ];
93
+ return { messages: compactedMessages, compacted: true };
94
+ }
95
+ catch {
96
+ // If summarization fails, just truncate older messages
97
+ return { messages: toKeep, compacted: true };
98
+ }
99
+ }