min-agent 0.1.5 → 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";
@@ -207,6 +207,19 @@ async function main() {
207
207
  }
208
208
  case "mcp": {
209
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
+ }
210
223
  switch (subcommand) {
211
224
  case "add": {
212
225
  const name = args[2];
@@ -275,15 +288,43 @@ async function main() {
275
288
  const servers = Object.entries(config.mcpServers);
276
289
  if (servers.length === 0) {
277
290
  console.log("No MCP servers configured.");
278
- 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...>");
279
292
  }
280
293
  else {
281
294
  console.log("MCP Servers:");
282
295
  for (const [name, cfg] of servers) {
283
- const status = cfg.enabled === false ? " (disabled)" : "";
284
- 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}`);
285
298
  }
299
+ console.log("\nUse: min-agent mcp info <name> for details");
300
+ }
301
+ break;
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);
286
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`);
287
328
  break;
288
329
  }
289
330
  case "check": {
@@ -317,8 +358,44 @@ async function main() {
317
358
  }
318
359
  break;
319
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
+ }
320
397
  default:
321
- console.error("Usage: min-agent mcp [add|remove|list|check]");
398
+ console.error("Usage: min-agent mcp -h");
322
399
  process.exit(1);
323
400
  }
324
401
  break;
@@ -410,35 +487,89 @@ async function main() {
410
487
  }
411
488
  case "skills": {
412
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
+ }
413
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
+ }
414
552
  case "list": {
415
- discoverSkills();
553
+ discoverSkills({ silent: true });
416
554
  const skills = getSkills();
417
- if (skills.length === 0) {
418
- console.log("No skills found.");
419
- console.log("Add skills by creating SKILL.md files in:");
420
- console.log(" ~/.agents/skills/<name>/SKILL.md (global, all projects)");
421
- console.log(" .min-agent/skills/<name>/SKILL.md");
422
- console.log(" .opencode/skills/<name>/SKILL.md");
423
- console.log("");
424
- console.log("SKILL.md format:");
425
- console.log(" ---");
426
- console.log(" name: my-skill");
427
- console.log(" description: What this skill does");
428
- console.log(" ---");
429
- 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;
430
560
  }
431
- else {
432
- console.log("Available Skills:");
433
- for (const skill of skills) {
434
- console.log(` ${skill.name}: ${skill.description}`);
435
- console.log(` ${skill.location}`);
436
- }
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`);
437
567
  }
568
+ console.log("\nUse: min-agent skills info <name> for details");
438
569
  break;
439
570
  }
440
571
  default:
441
- console.error("Usage: min-agent skills [list]");
572
+ console.error("Usage: min-agent skills -h");
442
573
  process.exit(1);
443
574
  }
444
575
  break;
package/dist/mcp.js CHANGED
@@ -7,24 +7,47 @@ import { tool, jsonSchema } from "ai";
7
7
  import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
8
8
  import path from "path";
9
9
  import { truncateToolOutput } from "./tool-output.js";
10
+ import { getConfigDir } from "./config.js";
10
11
  const DEFAULT_TIMEOUT = 30000;
11
12
  function getMcpConfigPath() {
12
- return path.join(process.cwd(), ".min-agent", "mcp.json");
13
+ // Check project-local first, then global
14
+ const local = path.join(process.cwd(), ".min-agent", "mcp.json");
15
+ if (existsSync(local))
16
+ return local;
17
+ return path.join(getConfigDir(), "mcp.json");
18
+ }
19
+ function getMcpConfigWritePath() {
20
+ // Write to project-local if it exists, otherwise global
21
+ const local = path.join(process.cwd(), ".min-agent", "mcp.json");
22
+ if (existsSync(path.dirname(local)))
23
+ return local;
24
+ return path.join(getConfigDir(), "mcp.json");
13
25
  }
14
26
  let connectedServers = {};
15
27
  export function loadMcpConfig() {
16
- const configPath = getMcpConfigPath();
17
- if (!existsSync(configPath))
18
- return { mcpServers: {} };
19
- try {
20
- return JSON.parse(readFileSync(configPath, "utf-8"));
28
+ const globalPath = path.join(getConfigDir(), "mcp.json");
29
+ const localPath = path.join(process.cwd(), ".min-agent", "mcp.json");
30
+ let config = { mcpServers: {} };
31
+ // Load global first
32
+ if (existsSync(globalPath)) {
33
+ try {
34
+ const global = JSON.parse(readFileSync(globalPath, "utf-8"));
35
+ config.mcpServers = { ...config.mcpServers, ...global.mcpServers };
36
+ }
37
+ catch { }
21
38
  }
22
- catch {
23
- return { mcpServers: {} };
39
+ // Local overrides global
40
+ if (existsSync(localPath) && localPath !== globalPath) {
41
+ try {
42
+ const local = JSON.parse(readFileSync(localPath, "utf-8"));
43
+ config.mcpServers = { ...config.mcpServers, ...local.mcpServers };
44
+ }
45
+ catch { }
24
46
  }
47
+ return config;
25
48
  }
26
49
  export function saveMcpConfig(config) {
27
- const configPath = getMcpConfigPath();
50
+ const configPath = path.join(getConfigDir(), "mcp.json");
28
51
  const dir = path.dirname(configPath);
29
52
  mkdirSync(dir, { recursive: true });
30
53
  writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
@@ -43,7 +66,21 @@ function buildRemoteRequestInit(config) {
43
66
  return { headers };
44
67
  }
45
68
  async function openStdioMcpServer(name, config) {
46
- const [cmd, ...args] = config.command ?? [];
69
+ // Support both formats:
70
+ // { command: ["uvx", "mcp-server-time"] } — min-agent native
71
+ // { command: "uvx", args: ["mcp-server-time"] } — opencode/claude style
72
+ let cmd;
73
+ let args;
74
+ if (Array.isArray(config.command)) {
75
+ [cmd, ...args] = config.command;
76
+ }
77
+ else if (typeof config.command === "string") {
78
+ cmd = config.command;
79
+ args = config.args ?? [];
80
+ }
81
+ else {
82
+ throw new Error(`MCP "${name}" has empty command`);
83
+ }
47
84
  if (!cmd) {
48
85
  throw new Error(`MCP "${name}" has empty command`);
49
86
  }
@@ -131,13 +168,18 @@ export function formatMcpServerBinding(config) {
131
168
  const mode = config.remoteTransport ?? "auto";
132
169
  return `${config.url} [remote:${mode}]`;
133
170
  }
134
- return (config.command ?? []).join(" ");
171
+ const cmd = Array.isArray(config.command) ? config.command.join(" ") : `${config.command ?? ""} ${(config.args ?? []).join(" ")}`.trim();
172
+ return cmd || "(no command)";
135
173
  }
136
174
  export async function connectMcpServer(name, config) {
137
175
  if (config.enabled === false)
138
176
  return null;
177
+ const timeout = config.timeout ?? DEFAULT_TIMEOUT;
139
178
  try {
140
- const server = await openMcpServer(name, config);
179
+ const server = await Promise.race([
180
+ openMcpServer(name, config),
181
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`connection timed out after ${timeout}ms`)), timeout)),
182
+ ]);
141
183
  console.log(`\x1b[90m MCP "${name}" connected (${server.tools.length} tools)\x1b[0m`);
142
184
  return server;
143
185
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Paste handler for large text input.
3
+ *
4
+ * When the user pastes large content (multi-line or >150 chars),
5
+ * shows a collapsed summary in the terminal but preserves the full
6
+ * text for sending to the model.
7
+ *
8
+ * Based on opencode's paste handling in the TUI prompt component.
9
+ */
10
+ const PASTE_LINE_THRESHOLD = 3;
11
+ const PASTE_CHAR_THRESHOLD = 150;
12
+ const PREVIEW_LINES = 3;
13
+ /**
14
+ * Process input text and detect if it's a large paste.
15
+ * Returns the full text plus display metadata.
16
+ */
17
+ export function processPastedInput(text) {
18
+ const lineCount = (text.match(/\n/g)?.length ?? 0) + 1;
19
+ if (lineCount < PASTE_LINE_THRESHOLD && text.length <= PASTE_CHAR_THRESHOLD) {
20
+ return { fullText: text, isLargePaste: false };
21
+ }
22
+ const lines = text.split("\n");
23
+ const preview = lines.slice(0, PREVIEW_LINES).join("\n");
24
+ const remaining = lineCount - PREVIEW_LINES;
25
+ return {
26
+ fullText: text,
27
+ isLargePaste: true,
28
+ summary: remaining > 0
29
+ ? `${preview}\n\x1b[90m ... (${remaining} more lines, ~${text.length} chars total)\x1b[0m`
30
+ : `\x1b[90m[Pasted ${text.length} chars]\x1b[0m`,
31
+ };
32
+ }
33
+ /**
34
+ * Print paste feedback to the user.
35
+ */
36
+ export function printPasteFeedback(result) {
37
+ if (!result.isLargePaste)
38
+ return;
39
+ const lineCount = (result.fullText.match(/\n/g)?.length ?? 0) + 1;
40
+ console.log(`\x1b[90m 📋 Pasted ~${lineCount} lines (${result.fullText.length} chars)\x1b[0m`);
41
+ }
package/dist/skills.js CHANGED
@@ -3,6 +3,7 @@ import { readFileSync, existsSync, readdirSync, statSync } from "fs";
3
3
  import os from "os";
4
4
  import path from "path";
5
5
  import { globSync } from "glob";
6
+ import { loadConfig } from "./config.js";
6
7
  /**
7
8
  * Skill scan order: later entries win on duplicate `name` in frontmatter.
8
9
  * Global user skills first, then project-local dirs so repo skills override ~/.agents.
@@ -15,7 +16,7 @@ const SKILL_DIRS = [
15
16
  path.join(process.cwd(), ".claude", "skills"),
16
17
  ];
17
18
  let loadedSkills = {};
18
- export function discoverSkills() {
19
+ export function discoverSkills(opts) {
19
20
  loadedSkills = {};
20
21
  for (const dir of SKILL_DIRS) {
21
22
  if (!existsSync(dir))
@@ -29,8 +30,10 @@ export function discoverSkills() {
29
30
  }
30
31
  }
31
32
  const count = Object.keys(loadedSkills).length;
32
- if (count > 0) {
33
- console.log(`\x1b[90m Skills loaded: ${count} (${Object.keys(loadedSkills).join(", ")})\x1b[0m`);
33
+ if (count > 0 && !opts?.silent) {
34
+ const disabled = new Set(loadConfig().disabledSkills ?? []);
35
+ const enabledCount = Object.keys(loadedSkills).filter((n) => !disabled.has(n)).length;
36
+ console.log(`\x1b[90m Skills: ${enabledCount} enabled${count > enabledCount ? `, ${count - enabledCount} disabled` : ""}\x1b[0m`);
34
37
  }
35
38
  }
36
39
  function parseSkillFile(filePath) {
@@ -58,7 +61,8 @@ function parseSkillFile(filePath) {
58
61
  }
59
62
  }
60
63
  export function getSkills() {
61
- return Object.values(loadedSkills);
64
+ const disabled = new Set(loadConfig().disabledSkills ?? []);
65
+ return Object.values(loadedSkills).filter((s) => !disabled.has(s.name));
62
66
  }
63
67
  export function getSkill(name) {
64
68
  return loadedSkills[name];
@@ -74,9 +78,13 @@ export function getSkillsTool() {
74
78
  required: ["name"],
75
79
  }),
76
80
  execute: async ({ name }) => {
81
+ const disabled = new Set(loadConfig().disabledSkills ?? []);
82
+ if (disabled.has(name)) {
83
+ return `Skill "${name}" is disabled. Available skills: ${getSkills().map((s) => s.name).join(", ") || "none"}`;
84
+ }
77
85
  const skill = loadedSkills[name];
78
86
  if (!skill) {
79
- const available = Object.keys(loadedSkills);
87
+ const available = getSkills().map((s) => s.name);
80
88
  return `Skill "${name}" not found. Available skills: ${available.length ? available.join(", ") : "none"}`;
81
89
  }
82
90
  const dir = path.dirname(skill.location);
@@ -104,7 +112,7 @@ export function getSkillsTool() {
104
112
  });
105
113
  }
106
114
  export function getSkillsSystemPrompt() {
107
- const skills = Object.values(loadedSkills);
115
+ const skills = getSkills();
108
116
  if (skills.length === 0)
109
117
  return "";
110
118
  return [
@@ -115,7 +123,7 @@ export function getSkillsSystemPrompt() {
115
123
  ].join("\n");
116
124
  }
117
125
  function buildSkillDescription() {
118
- const skills = Object.values(loadedSkills);
126
+ const skills = getSkills();
119
127
  if (skills.length === 0)
120
128
  return "Load a specialized skill. No skills are currently available.";
121
129
  return [
@@ -21,11 +21,15 @@ export const bashTool = tool({
21
21
  const chunks = [];
22
22
  let killed = false;
23
23
  let timer;
24
- const proc = spawn(command, [], {
24
+ // On Windows, force UTF-8 codepage to avoid Chinese garbled text
25
+ const isWin = process.platform === "win32";
26
+ const actualCommand = isWin ? `chcp 65001 >nul && ${command}` : command;
27
+ const proc = spawn(actualCommand, [], {
25
28
  shell: true,
26
29
  cwd: process.cwd(),
27
30
  stdio: ["ignore", "pipe", "pipe"],
28
31
  detached: process.platform !== "win32",
32
+ env: { ...process.env, ...(isWin ? { PYTHONIOENCODING: "utf-8" } : {}) },
29
33
  });
30
34
  proc.stdout?.on("data", (chunk) => chunks.push(chunk));
31
35
  proc.stderr?.on("data", (chunk) => chunks.push(chunk));