min-agent 0.1.4 → 0.1.6

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/dist/cli.js CHANGED
@@ -2,7 +2,7 @@ import { runAgent, runChat } from "./agent.js";
2
2
  import { loadMcpConfig, saveMcpConfig, checkMcpServer, checkAllMcpServers, formatMcpServerBinding, } from "./mcp.js";
3
3
  import { discoverSkills, getSkills } from "./skills.js";
4
4
  import { loadMemories, addMemory, deleteMemory, searchMemories } from "./memory.js";
5
- import { runSetup, isConfigured, loadConfig, fetchModels, getConfigDir, getRulesFile } from "./config.js";
5
+ import { runSetup, isConfigured, loadConfig, saveConfig, fetchModels, getConfigDir, getRulesFile } from "./config.js";
6
6
  import { setAutoApprove } from "./confirm.js";
7
7
  import { existsSync, writeFileSync, mkdirSync } from "fs";
8
8
  import path from "path";
@@ -24,6 +24,7 @@ Usage:
24
24
  min-agent chat <message> Send a message to the agent
25
25
  min-agent chat Start interactive multi-turn chat
26
26
  min-agent chat --resume <id> Resume a previous session
27
+ min-agent code AI coding mode (project-aware)
27
28
  min-agent setup Configure API provider (interactive)
28
29
  min-agent models List available models
29
30
  min-agent history List saved sessions
@@ -159,7 +160,6 @@ async function main() {
159
160
  }
160
161
  const message = chatArgs.join(" ");
161
162
  if (!message) {
162
- // No message provided — enter interactive multi-turn mode
163
163
  await runChat(modelOverride, resumeId);
164
164
  }
165
165
  else {
@@ -167,6 +167,25 @@ async function main() {
167
167
  }
168
168
  break;
169
169
  }
170
+ case "code": {
171
+ if (!isConfigured()) {
172
+ console.error("Not configured. Run: min-agent setup");
173
+ process.exit(1);
174
+ }
175
+ let modelOverride;
176
+ let resumeId;
177
+ for (let i = 1; i < args.length; i++) {
178
+ if ((args[i] === "--model" || args[i] === "-m") && args[i + 1]) {
179
+ modelOverride = args[++i];
180
+ }
181
+ else if (args[i] === "--resume" && args[i + 1]) {
182
+ resumeId = args[++i];
183
+ }
184
+ }
185
+ const { runCode } = await import("./agent.js");
186
+ await runCode(modelOverride, resumeId);
187
+ break;
188
+ }
170
189
  case "serve": {
171
190
  if (!isConfigured()) {
172
191
  console.error("Not configured. Run: min-agent setup");
@@ -188,6 +207,19 @@ async function main() {
188
207
  }
189
208
  case "mcp": {
190
209
  const subcommand = args[1];
210
+ if (subcommand === "-h" || subcommand === "--help" || !subcommand) {
211
+ console.log(`Usage: min-agent mcp <command>
212
+
213
+ Commands:
214
+ list List configured MCP servers (name + status)
215
+ info <name> Show details of a server
216
+ add <name> ... Add a new server (stdio or remote)
217
+ remove <name> Remove a server
218
+ enable <name> Enable a disabled server
219
+ disable <name> Disable a server
220
+ check Test connectivity of all servers`);
221
+ process.exit(0);
222
+ }
191
223
  switch (subcommand) {
192
224
  case "add": {
193
225
  const name = args[2];
@@ -256,17 +288,45 @@ async function main() {
256
288
  const servers = Object.entries(config.mcpServers);
257
289
  if (servers.length === 0) {
258
290
  console.log("No MCP servers configured.");
259
- console.log("Add one with: min-agent mcp add <name> <command...> or --url <https://...>");
291
+ console.log("Add one with: min-agent mcp add <name> <command...>");
260
292
  }
261
293
  else {
262
294
  console.log("MCP Servers:");
263
295
  for (const [name, cfg] of servers) {
264
- const status = cfg.enabled === false ? " (disabled)" : "";
265
- console.log(` ${name}: ${formatMcpServerBinding(cfg)}${status}`);
296
+ const status = cfg.enabled === false ? "\x1b[90mdisabled\x1b[0m" : "\x1b[32menabled\x1b[0m";
297
+ console.log(` ${name} ${status}`);
266
298
  }
299
+ console.log("\nUse: min-agent mcp info <name> for details");
267
300
  }
268
301
  break;
269
302
  }
303
+ case "info": {
304
+ const name = args[2];
305
+ if (!name) {
306
+ console.error("Usage: min-agent mcp info <name>");
307
+ process.exit(1);
308
+ }
309
+ const config = loadMcpConfig();
310
+ const cfg = config.mcpServers[name];
311
+ if (!cfg) {
312
+ console.error(`MCP server "${name}" not found`);
313
+ process.exit(1);
314
+ }
315
+ console.log(`Name: ${name}`);
316
+ console.log(`Status: ${cfg.enabled === false ? "disabled" : "enabled"}`);
317
+ console.log(`Binding: ${formatMcpServerBinding(cfg)}`);
318
+ if (cfg.command)
319
+ console.log(`Command: ${Array.isArray(cfg.command) ? cfg.command.join(" ") : `${cfg.command} ${(cfg.args ?? []).join(" ")}`.trim()}`);
320
+ if (cfg.url)
321
+ console.log(`URL: ${cfg.url}`);
322
+ if (cfg.token)
323
+ console.log(`Token: ***`);
324
+ if (cfg.environment)
325
+ console.log(`Env: ${Object.keys(cfg.environment).join(", ")}`);
326
+ if (cfg.timeout)
327
+ console.log(`Timeout: ${cfg.timeout}ms`);
328
+ break;
329
+ }
270
330
  case "check": {
271
331
  const results = await checkAllMcpServers();
272
332
  if (results.length === 0) {
@@ -298,8 +358,44 @@ async function main() {
298
358
  }
299
359
  break;
300
360
  }
361
+ case "enable": {
362
+ const names = args.slice(2);
363
+ if (names.length === 0) {
364
+ console.error("Usage: min-agent mcp enable <name...>");
365
+ process.exit(1);
366
+ }
367
+ const config = loadMcpConfig();
368
+ for (const name of names) {
369
+ if (!config.mcpServers[name]) {
370
+ console.error(`MCP server "${name}" not found`);
371
+ continue;
372
+ }
373
+ config.mcpServers[name].enabled = true;
374
+ console.log(`✓ MCP server "${name}" enabled`);
375
+ }
376
+ saveMcpConfig(config);
377
+ break;
378
+ }
379
+ case "disable": {
380
+ const names = args.slice(2);
381
+ if (names.length === 0) {
382
+ console.error("Usage: min-agent mcp disable <name...>");
383
+ process.exit(1);
384
+ }
385
+ const config = loadMcpConfig();
386
+ for (const name of names) {
387
+ if (!config.mcpServers[name]) {
388
+ console.error(`MCP server "${name}" not found`);
389
+ continue;
390
+ }
391
+ config.mcpServers[name].enabled = false;
392
+ console.log(`✓ MCP server "${name}" disabled`);
393
+ }
394
+ saveMcpConfig(config);
395
+ break;
396
+ }
301
397
  default:
302
- console.error("Usage: min-agent mcp [add|remove|list|check]");
398
+ console.error("Usage: min-agent mcp -h");
303
399
  process.exit(1);
304
400
  }
305
401
  break;
@@ -391,35 +487,89 @@ async function main() {
391
487
  }
392
488
  case "skills": {
393
489
  const subcommand = args[1];
490
+ if (subcommand === "-h" || subcommand === "--help" || !subcommand) {
491
+ console.log(`Usage: min-agent skills <command>
492
+
493
+ Commands:
494
+ list List skills (name + status)
495
+ info <name> Show details of a skill
496
+ enable <name> Enable a disabled skill
497
+ disable <name> Disable a skill`);
498
+ process.exit(0);
499
+ }
394
500
  switch (subcommand) {
501
+ case "info": {
502
+ const name = args[2];
503
+ if (!name) {
504
+ console.error("Usage: min-agent skills info <name>");
505
+ process.exit(1);
506
+ }
507
+ discoverSkills({ silent: true });
508
+ const { getSkill } = await import("./skills.js");
509
+ const skill = getSkill(name);
510
+ if (!skill) {
511
+ console.error(`Skill "${name}" not found`);
512
+ process.exit(1);
513
+ }
514
+ const config = loadConfig();
515
+ const isDisabled = (config.disabledSkills ?? []).includes(name);
516
+ console.log(`Name: ${skill.name}`);
517
+ console.log(`Status: ${isDisabled ? "disabled" : "enabled"}`);
518
+ console.log(`Description: ${skill.description}`);
519
+ console.log(`Location: ${skill.location}`);
520
+ break;
521
+ }
522
+ case "enable": {
523
+ const names = args.slice(2);
524
+ if (names.length === 0) {
525
+ console.error("Usage: min-agent skills enable <name...>");
526
+ process.exit(1);
527
+ }
528
+ const config = loadConfig();
529
+ config.disabledSkills = (config.disabledSkills ?? []).filter((s) => !names.includes(s));
530
+ saveConfig(config);
531
+ for (const name of names)
532
+ console.log(`✓ Skill "${name}" enabled`);
533
+ break;
534
+ }
535
+ case "disable": {
536
+ const names = args.slice(2);
537
+ if (names.length === 0) {
538
+ console.error("Usage: min-agent skills disable <name...>");
539
+ process.exit(1);
540
+ }
541
+ const config = loadConfig();
542
+ const disabled = config.disabledSkills ?? [];
543
+ for (const name of names) {
544
+ if (!disabled.includes(name))
545
+ disabled.push(name);
546
+ console.log(`✓ Skill "${name}" disabled`);
547
+ }
548
+ config.disabledSkills = disabled;
549
+ saveConfig(config);
550
+ break;
551
+ }
395
552
  case "list": {
396
- discoverSkills();
553
+ discoverSkills({ silent: true });
397
554
  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...");
555
+ const config = loadConfig();
556
+ const disabledNames = config.disabledSkills ?? [];
557
+ if (skills.length === 0 && disabledNames.length === 0) {
558
+ console.log("No skills found. Add SKILL.md files in .min-agent/skills/<name>/");
559
+ break;
411
560
  }
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
- }
561
+ console.log("Skills:");
562
+ for (const skill of skills) {
563
+ console.log(` ${skill.name} \x1b[32menabled\x1b[0m`);
564
+ }
565
+ for (const n of disabledNames) {
566
+ console.log(` ${n} \x1b[90mdisabled\x1b[0m`);
418
567
  }
568
+ console.log("\nUse: min-agent skills info <name> for details");
419
569
  break;
420
570
  }
421
571
  default:
422
- console.error("Usage: min-agent skills [list]");
572
+ console.error("Usage: min-agent skills -h");
423
573
  process.exit(1);
424
574
  }
425
575
  break;
@@ -0,0 +1,106 @@
1
+ import { execSync } from "child_process";
2
+ import path from "path";
3
+ import os from "os";
4
+ export function getClipboardImage() {
5
+ switch (process.platform) {
6
+ case "darwin":
7
+ return getClipboardImageMac();
8
+ case "linux":
9
+ return getClipboardImageLinux();
10
+ case "win32":
11
+ return getClipboardImageWindows();
12
+ default:
13
+ return null;
14
+ }
15
+ }
16
+ function getClipboardImageMac() {
17
+ const tmpFile = path.join(os.tmpdir(), `min-agent-paste-${Date.now()}.png`);
18
+ try {
19
+ // Try pngpaste first (brew install pngpaste)
20
+ execSync(`pngpaste "${tmpFile}" 2>/dev/null`, { stdio: "pipe" });
21
+ const { readFileSync } = require("fs");
22
+ const data = readFileSync(tmpFile);
23
+ try {
24
+ require("fs").unlinkSync(tmpFile);
25
+ }
26
+ catch { }
27
+ if (data.length > 0)
28
+ return { data, mimeType: "image/png" };
29
+ }
30
+ catch { }
31
+ try {
32
+ // Fallback: osascript to save clipboard image
33
+ const script = `
34
+ set tmpFile to POSIX file "${tmpFile}"
35
+ try
36
+ set imgData to the clipboard as «class PNGf»
37
+ set fp to open for access tmpFile with write permission
38
+ write imgData to fp
39
+ close access fp
40
+ return "ok"
41
+ on error
42
+ return "no_image"
43
+ end try
44
+ `;
45
+ const result = execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { encoding: "utf-8" }).trim();
46
+ if (result === "ok") {
47
+ const { readFileSync, unlinkSync } = require("fs");
48
+ const data = readFileSync(tmpFile);
49
+ try {
50
+ unlinkSync(tmpFile);
51
+ }
52
+ catch { }
53
+ if (data.length > 0)
54
+ return { data, mimeType: "image/png" };
55
+ }
56
+ }
57
+ catch { }
58
+ return null;
59
+ }
60
+ function getClipboardImageLinux() {
61
+ try {
62
+ // xclip
63
+ const data = execSync("xclip -selection clipboard -t image/png -o 2>/dev/null", {
64
+ maxBuffer: 10 * 1024 * 1024,
65
+ });
66
+ if (data.length > 0)
67
+ return { data, mimeType: "image/png" };
68
+ }
69
+ catch { }
70
+ try {
71
+ // xsel fallback
72
+ const data = execSync("xsel --clipboard --output 2>/dev/null", {
73
+ maxBuffer: 10 * 1024 * 1024,
74
+ });
75
+ // Check if it's actually image data (PNG magic bytes)
76
+ if (data.length > 8 && data[0] === 0x89 && data[1] === 0x50) {
77
+ return { data, mimeType: "image/png" };
78
+ }
79
+ }
80
+ catch { }
81
+ return null;
82
+ }
83
+ function getClipboardImageWindows() {
84
+ const tmpFile = path.join(os.tmpdir(), `min-agent-paste-${Date.now()}.png`);
85
+ try {
86
+ const ps = `
87
+ Add-Type -AssemblyName System.Windows.Forms
88
+ $img = [System.Windows.Forms.Clipboard]::GetImage()
89
+ if ($img) { $img.Save('${tmpFile.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' }
90
+ else { Write-Output 'no_image' }
91
+ `;
92
+ const result = execSync(`powershell -NoProfile -Command "${ps}"`, { encoding: "utf-8" }).trim();
93
+ if (result === "ok") {
94
+ const { readFileSync, unlinkSync } = require("fs");
95
+ const data = readFileSync(tmpFile);
96
+ try {
97
+ unlinkSync(tmpFile);
98
+ }
99
+ catch { }
100
+ if (data.length > 0)
101
+ return { data, mimeType: "image/png" };
102
+ }
103
+ }
104
+ catch { }
105
+ return null;
106
+ }
@@ -0,0 +1,166 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
3
+ import path from "path";
4
+ export function scanProject() {
5
+ const cwd = process.cwd();
6
+ const isGitRepo = existsSync(path.join(cwd, ".git"));
7
+ let branch;
8
+ if (isGitRepo) {
9
+ try {
10
+ branch = execSync("git branch --show-current", { encoding: "utf-8", cwd }).trim();
11
+ }
12
+ catch { }
13
+ }
14
+ const languages = [];
15
+ const configFiles = [];
16
+ const entryFiles = [];
17
+ // Detect by config files
18
+ const checks = [
19
+ { file: "package.json", lang: "TypeScript/JavaScript", pm: "npm" },
20
+ { file: "bun.lock", lang: "TypeScript/JavaScript", pm: "bun" },
21
+ { file: "yarn.lock", lang: "TypeScript/JavaScript", pm: "yarn" },
22
+ { file: "pnpm-lock.yaml", lang: "TypeScript/JavaScript", pm: "pnpm" },
23
+ { file: "tsconfig.json", lang: "TypeScript" },
24
+ { file: "Cargo.toml", lang: "Rust", pm: "cargo" },
25
+ { file: "go.mod", lang: "Go" },
26
+ { file: "pyproject.toml", lang: "Python", pm: "pip/uv" },
27
+ { file: "requirements.txt", lang: "Python", pm: "pip" },
28
+ { file: "Gemfile", lang: "Ruby", pm: "bundler" },
29
+ { file: "pom.xml", lang: "Java", pm: "maven" },
30
+ { file: "build.gradle", lang: "Java/Kotlin", pm: "gradle" },
31
+ { file: "composer.json", lang: "PHP", pm: "composer" },
32
+ { file: "Makefile", lang: "" },
33
+ { file: "Dockerfile", lang: "" },
34
+ ];
35
+ let packageManager;
36
+ let framework;
37
+ for (const check of checks) {
38
+ if (existsSync(path.join(cwd, check.file))) {
39
+ configFiles.push(check.file);
40
+ if (check.lang && !languages.includes(check.lang))
41
+ languages.push(check.lang);
42
+ if (check.pm && !packageManager)
43
+ packageManager = check.pm;
44
+ }
45
+ }
46
+ // Detect framework from package.json
47
+ if (existsSync(path.join(cwd, "package.json"))) {
48
+ try {
49
+ const pkg = JSON.parse(readFileSync(path.join(cwd, "package.json"), "utf-8"));
50
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
51
+ if (allDeps["next"])
52
+ framework = "Next.js";
53
+ else if (allDeps["nuxt"])
54
+ framework = "Nuxt";
55
+ else if (allDeps["@angular/core"])
56
+ framework = "Angular";
57
+ else if (allDeps["vue"])
58
+ framework = "Vue";
59
+ else if (allDeps["react"])
60
+ framework = "React";
61
+ else if (allDeps["svelte"])
62
+ framework = "Svelte";
63
+ else if (allDeps["express"])
64
+ framework = "Express";
65
+ else if (allDeps["fastify"])
66
+ framework = "Fastify";
67
+ else if (allDeps["hono"])
68
+ framework = "Hono";
69
+ else if (allDeps["effect"])
70
+ framework = "Effect";
71
+ }
72
+ catch { }
73
+ }
74
+ // Find entry files
75
+ const entryPatterns = [
76
+ "src/index.ts", "src/index.js", "src/main.ts", "src/main.js",
77
+ "src/app.ts", "src/app.js", "index.ts", "index.js",
78
+ "main.ts", "main.js", "app.ts", "app.js",
79
+ "src/lib.rs", "main.go", "main.py", "app.py",
80
+ ];
81
+ for (const p of entryPatterns) {
82
+ if (existsSync(path.join(cwd, p)))
83
+ entryFiles.push(p);
84
+ }
85
+ // Build summary
86
+ const parts = [];
87
+ parts.push(`Directory: ${cwd}`);
88
+ if (isGitRepo)
89
+ parts.push(`Git: yes (branch: ${branch ?? "unknown"})`);
90
+ if (languages.length)
91
+ parts.push(`Languages: ${languages.join(", ")}`);
92
+ if (framework)
93
+ parts.push(`Framework: ${framework}`);
94
+ if (packageManager)
95
+ parts.push(`Package manager: ${packageManager}`);
96
+ if (configFiles.length)
97
+ parts.push(`Config files: ${configFiles.join(", ")}`);
98
+ if (entryFiles.length)
99
+ parts.push(`Entry points: ${entryFiles.join(", ")}`);
100
+ // List top-level directory structure
101
+ try {
102
+ const items = readdirSync(cwd)
103
+ .filter((f) => !f.startsWith(".") || f === ".env.example")
104
+ .filter((f) => f !== "node_modules" && f !== ".git")
105
+ .slice(0, 30)
106
+ .map((f) => {
107
+ const stat = statSync(path.join(cwd, f));
108
+ return stat.isDirectory() ? `${f}/` : f;
109
+ });
110
+ parts.push(`Structure: ${items.join(", ")}`);
111
+ }
112
+ catch { }
113
+ return {
114
+ directory: cwd,
115
+ isGitRepo,
116
+ branch,
117
+ languages,
118
+ framework,
119
+ packageManager,
120
+ entryFiles,
121
+ configFiles,
122
+ summary: parts.join("\n"),
123
+ };
124
+ }
125
+ export function buildCodeSystemPrompt(project, instructions) {
126
+ const parts = [
127
+ "You are an expert AI coding assistant. You help users with software engineering tasks including writing code, debugging, refactoring, and architecture decisions.",
128
+ "",
129
+ "# Principles",
130
+ "- Be concise and direct. Minimize output tokens while maintaining quality.",
131
+ "- NEVER add comments to code unless asked. Write self-documenting code.",
132
+ "- Follow existing code conventions in the project. Mimic style, libraries, and patterns.",
133
+ "- NEVER assume a library is available — check package.json/Cargo.toml/etc first.",
134
+ "- Prefer editing existing files over creating new ones.",
135
+ "- After making changes, run lint/typecheck/build if available.",
136
+ "- NEVER commit unless explicitly asked.",
137
+ "",
138
+ "# Workflow",
139
+ "- Use search tools (grep, glob) to understand the codebase before making changes.",
140
+ "- Use the edit tool for precise changes instead of rewriting entire files.",
141
+ "- When fixing bugs: read the relevant code, understand the issue, fix it, verify.",
142
+ "- When adding features: explore existing patterns first, then implement consistently.",
143
+ "- Run tests after changes when a test command is available.",
144
+ "",
145
+ "# Environment",
146
+ `<env>`,
147
+ ` Working directory: ${project.directory}`,
148
+ ` Git repo: ${project.isGitRepo ? `yes (branch: ${project.branch ?? "unknown"})` : "no"}`,
149
+ ` Platform: ${process.platform}`,
150
+ ` Date: ${new Date().toDateString()}`,
151
+ project.languages.length ? ` Languages: ${project.languages.join(", ")}` : "",
152
+ project.framework ? ` Framework: ${project.framework}` : "",
153
+ project.packageManager ? ` Package manager: ${project.packageManager}` : "",
154
+ `</env>`,
155
+ "",
156
+ "# Project Structure",
157
+ "```",
158
+ project.summary,
159
+ "```",
160
+ ].filter(Boolean);
161
+ if (instructions.length > 0) {
162
+ parts.push("", "# User Instructions", "");
163
+ parts.push(...instructions);
164
+ }
165
+ return parts.join("\n");
166
+ }