min-agent 0.2.1 → 0.3.0

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 (81) hide show
  1. package/README.md +146 -18
  2. package/dist/agent.js +293 -408
  3. package/dist/assistant-stream.js +11 -7
  4. package/dist/cli.js +397 -139
  5. package/dist/clipboard.js +59 -23
  6. package/dist/code-mode.js +3 -3
  7. package/dist/compaction.js +182 -81
  8. package/dist/config.js +186 -35
  9. package/dist/confirm.js +55 -6
  10. package/dist/context-window.js +67 -54
  11. package/dist/doom-loop.js +19 -12
  12. package/dist/http.js +119 -0
  13. package/dist/instructions.js +51 -33
  14. package/dist/logger.js +66 -0
  15. package/dist/markdown.js +3 -44
  16. package/dist/mcp.js +547 -100
  17. package/dist/memory.js +48 -6
  18. package/dist/output.js +36 -27
  19. package/dist/paste-handler.js +3 -3
  20. package/dist/plugins.js +33 -6
  21. package/dist/pricing.js +119 -0
  22. package/dist/provider.js +17 -15
  23. package/dist/serve.js +658 -369
  24. package/dist/sessions.js +151 -13
  25. package/dist/skills.js +466 -76
  26. package/dist/synthetic.js +7 -0
  27. package/dist/title-gen.js +2 -1
  28. package/dist/tool-display.js +173 -0
  29. package/dist/tool-output.js +54 -45
  30. package/dist/tools/apply_patch.js +191 -0
  31. package/dist/tools/backend.js +61 -0
  32. package/dist/tools/bash.js +147 -70
  33. package/dist/tools/code_search.js +6 -5
  34. package/dist/tools/edit.js +23 -7
  35. package/dist/tools/explore.js +80 -12
  36. package/dist/tools/glob.js +3 -3
  37. package/dist/tools/grep.js +146 -14
  38. package/dist/tools/index.js +7 -7
  39. package/dist/tools/question.js +4 -22
  40. package/dist/tools/read.js +71 -11
  41. package/dist/tools/task.js +33 -20
  42. package/dist/tools/todo.js +83 -73
  43. package/dist/tools/web_fetch.js +150 -46
  44. package/dist/tools/web_search.js +706 -28
  45. package/dist/tools/write.js +13 -7
  46. package/dist/tui/App.js +40 -6
  47. package/dist/tui/ConfirmBar.js +24 -3
  48. package/dist/tui/InputBar.js +390 -45
  49. package/dist/tui/MessageList.js +533 -20
  50. package/dist/tui/ModelPicker.js +108 -0
  51. package/dist/tui/QuestionBar.js +104 -0
  52. package/dist/tui/StatusBar.js +19 -11
  53. package/dist/tui/agent-runner.js +103 -0
  54. package/dist/tui/caret-pos.js +134 -0
  55. package/dist/tui/caret.js +69 -0
  56. package/dist/tui/diff-view.js +61 -0
  57. package/dist/tui/drag-state.js +44 -0
  58. package/dist/tui/index.js +153 -24
  59. package/dist/tui/input-history.js +44 -0
  60. package/dist/tui/layout.js +17 -0
  61. package/dist/tui/mouse.js +46 -0
  62. package/dist/tui/selection.js +134 -0
  63. package/dist/tui/slash-commands.js +90 -0
  64. package/dist/tui/slash-handler.js +370 -0
  65. package/dist/tui/text-width.js +91 -0
  66. package/dist/tui/theme.js +12 -0
  67. package/dist/tui/undo-stack.js +14 -0
  68. package/dist/tui/use-sgr-mouse.js +27 -0
  69. package/dist/tui-chat.js +111 -331
  70. package/dist/updater.js +57 -0
  71. package/docs/API.md +160 -14
  72. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
  73. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
  74. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
  75. package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
  76. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
  77. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
  78. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
  79. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
  80. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
  81. package/package.json +7 -8
package/dist/cli.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import { runAgent } from "./agent.js";
2
- import { loadMcpConfig, saveMcpConfig, checkMcpServer, checkAllMcpServers, formatMcpServerBinding, } from "./mcp.js";
3
- import { discoverSkills, getSkills } from "./skills.js";
2
+ import { loadMcpConfigFile, loadMcpConfigEntries, saveMcpConfig, checkMcpServer, checkAllMcpServers, formatMcpServerBinding, } from "./mcp.js";
3
+ import { discoverSkills } from "./skills.js";
4
4
  import { loadMemories, addMemory, deleteMemory, searchMemories } from "./memory.js";
5
- import { runSetup, isConfigured, loadConfig, saveConfig, fetchModels, getConfigDir, getRulesFile } from "./config.js";
5
+ import { runSetup, isConfigured, loadConfig, fetchModels, getConfigDir, getRulesFile, getActiveProvider } from "./config.js";
6
6
  import { setAutoApprove } from "./confirm.js";
7
7
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
8
8
  import path from "path";
9
+ import os from "os";
10
+ import { fileURLToPath } from "url";
11
+ import { initLogger, log } from "./logger.js";
9
12
  const rawArgs = process.argv.slice(2);
10
13
  // Extract leading global flags only, so subcommands can still use "-y"
11
14
  const args = [...rawArgs];
@@ -16,6 +19,45 @@ while (args[0] === "--yes" || args[0] === "-y") {
16
19
  }
17
20
  if (hasYes)
18
21
  setAutoApprove(true);
22
+ /** Parse common flags in any position; unknown args become positionals. */
23
+ function parseFlags(argv) {
24
+ let model;
25
+ let provider;
26
+ let resume;
27
+ const images = [];
28
+ let port;
29
+ let host;
30
+ const positionals = [];
31
+ for (let i = 0; i < argv.length; i++) {
32
+ const a = argv[i];
33
+ const take = () => (argv[i + 1] ? argv[++i] : "");
34
+ if ((a === "--model" || a === "-m") && argv[i + 1]) {
35
+ model = take();
36
+ }
37
+ else if (a === "--provider" && argv[i + 1]) {
38
+ provider = take();
39
+ }
40
+ else if (a === "--resume" && argv[i + 1]) {
41
+ resume = take();
42
+ }
43
+ else if ((a === "--image" || a === "-i") && argv[i + 1]) {
44
+ images.push(take());
45
+ }
46
+ else if ((a === "--port" || a === "-p") && argv[i + 1]) {
47
+ port = parseInt(take(), 10);
48
+ }
49
+ else if (a === "--host" && argv[i + 1]) {
50
+ host = take();
51
+ }
52
+ else if (a === "--yes" || a === "-y") {
53
+ setAutoApprove(true);
54
+ }
55
+ else {
56
+ positionals.push(a);
57
+ }
58
+ }
59
+ return { model, provider, resume, images, port, host, positionals };
60
+ }
19
61
  function printUsage() {
20
62
  console.log(`
21
63
  min-agent - Minimal AI coding agent
@@ -26,9 +68,13 @@ Usage:
26
68
  min-agent chat Start interactive multi-turn chat
27
69
  min-agent chat --resume <id> Resume a previous session
28
70
  min-agent code AI coding mode (project-aware)
29
- min-agent setup Configure API provider (interactive)
71
+ min-agent setup Configure providers (interactive)
30
72
  min-agent models List available models
31
73
  min-agent history List saved sessions
74
+ min-agent history delete <id> Delete a saved session
75
+ min-agent history rename <id> <t> Rename a saved session
76
+ min-agent history export <id> [-o <file>] Export a session as JSON
77
+ min-agent update Check for updates and upgrade
32
78
  min-agent rules Show loaded instruction rules
33
79
  min-agent rules edit Edit global rules file
34
80
  min-agent memory List all memories
@@ -36,15 +82,18 @@ Usage:
36
82
  min-agent memory search <query> Search memories
37
83
  min-agent memory delete <index> Delete a memory by number
38
84
  min-agent mcp add <name> <cmd> Add a local MCP server (stdio)
39
- min-agent mcp add <name> --url <url> [--sse] [--token <t>] Add remote MCP (HTTP)
85
+ min-agent mcp add <name> --url <url> [--sse] [--streamable-http] [--token <t>] Add remote MCP (HTTP)
40
86
  min-agent mcp remove <name> Remove an MCP server
41
87
  min-agent mcp list List configured MCP servers
42
88
  min-agent mcp check Check MCP server availability
43
89
  min-agent skills list List available skills
90
+ min-agent skills info <name> Show skill details
91
+ min-agent skills enable|disable <name...> [--project] Toggle skills
44
92
  min-agent serve [--host H] [--port P] HTTP API (see docs/API.md)
45
93
 
46
94
  Options:
47
95
  --model, -m <model> Override model for this request
96
+ --provider <name> Use a specific configured provider
48
97
  --image, -i <path> Attach an image (can be used multiple times)
49
98
  --yes, -y Auto-approve all confirmations (dangerous commands, file overwrites)
50
99
 
@@ -72,6 +121,10 @@ function parseMcpAddArgs(argv) {
72
121
  let url;
73
122
  let token;
74
123
  let sse = false;
124
+ let streamableHttp = false;
125
+ let project = false;
126
+ let timeout;
127
+ const environment = {};
75
128
  const cmd = [];
76
129
  for (let i = 0; i < argv.length; i++) {
77
130
  const a = argv[i];
@@ -83,6 +136,14 @@ function parseMcpAddArgs(argv) {
83
136
  sse = true;
84
137
  continue;
85
138
  }
139
+ if (a === "--streamable-http") {
140
+ streamableHttp = true;
141
+ continue;
142
+ }
143
+ if (a === "--project") {
144
+ project = true;
145
+ continue;
146
+ }
86
147
  if (a === "--url" && argv[i + 1]) {
87
148
  url = argv[++i];
88
149
  continue;
@@ -91,9 +152,38 @@ function parseMcpAddArgs(argv) {
91
152
  token = argv[++i];
92
153
  continue;
93
154
  }
155
+ if (a === "--timeout" && argv[i + 1]) {
156
+ const value = parseInt(argv[++i], 10);
157
+ if (!Number.isFinite(value) || value <= 0) {
158
+ console.error(`Invalid --timeout value: ${argv[i]} (must be a positive number of ms)`);
159
+ process.exit(1);
160
+ }
161
+ timeout = value;
162
+ continue;
163
+ }
164
+ if (a === "--env") {
165
+ const kv = argv[i + 1];
166
+ if (!kv) {
167
+ console.error("Invalid --env: missing KEY=VALUE");
168
+ process.exit(1);
169
+ }
170
+ const eq = kv.indexOf("=");
171
+ if (eq <= 0) {
172
+ console.error(`Invalid --env format: ${kv} (use KEY=VALUE)`);
173
+ process.exit(1);
174
+ }
175
+ i++;
176
+ environment[kv.slice(0, eq)] = kv.slice(eq + 1);
177
+ continue;
178
+ }
94
179
  cmd.push(a);
95
180
  }
96
- return { skipCheck, url, token, sse, cmd };
181
+ return { skipCheck, url, token, sse, streamableHttp, project, timeout, environment, cmd };
182
+ }
183
+ /** One-line, length-capped text for list output. */
184
+ function truncate(text, max) {
185
+ const flat = text.replace(/\s+/g, " ").trim();
186
+ return flat.length <= max ? flat : `${flat.slice(0, max - 1)}…`;
97
187
  }
98
188
  function ensureProjectDefaults() {
99
189
  const projectConfigDir = path.join(process.cwd(), ".min-agent");
@@ -113,32 +203,38 @@ async function main() {
113
203
  process.exit(0);
114
204
  }
115
205
  if (args[0] === "--version" || args[0] === "-v") {
116
- const pkgPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../package.json");
206
+ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
117
207
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
118
208
  console.log(pkg.version);
119
209
  process.exit(0);
120
210
  }
121
211
  const command = args[0];
212
+ initLogger();
213
+ log("info", `min-agent started: ${command}`);
122
214
  switch (command) {
123
215
  case "setup": {
124
216
  await runSetup();
125
217
  break;
126
218
  }
127
219
  case "models": {
220
+ const flags = parseFlags(args.slice(1));
128
221
  const config = loadConfig();
129
- if (!config.provider?.baseURL || !config.provider?.apiKey) {
222
+ const provider = flags.provider
223
+ ? config.providers?.find((p) => p.name === flags.provider)
224
+ : getActiveProvider(config);
225
+ if (!provider?.baseURL || !provider?.apiKey) {
130
226
  console.error("Not configured. Run: min-agent setup");
131
227
  process.exit(1);
132
228
  }
133
- console.log("Fetching models...");
134
- const models = await fetchModels(config.provider.baseURL, config.provider.apiKey);
229
+ console.log(`Fetching models (${provider.name ?? "default"})...`);
230
+ const models = await fetchModels(provider.baseURL, provider.apiKey);
135
231
  if (models.length === 0) {
136
232
  console.log("No models found or unable to fetch model list.");
137
233
  }
138
234
  else {
139
235
  console.log(`\nAvailable models (${models.length}):`);
140
236
  for (const m of models) {
141
- const marker = m === config.provider.defaultModel ? " ← default" : "";
237
+ const marker = m === provider.defaultModel ? " ← default" : "";
142
238
  console.log(` ${m}${marker}`);
143
239
  }
144
240
  }
@@ -149,32 +245,18 @@ async function main() {
149
245
  console.error("Not configured. Run: min-agent setup");
150
246
  process.exit(1);
151
247
  }
152
- // Parse --model / -m, --resume, and --image / -i flags
153
- let modelOverride;
154
- let resumeId;
155
- const images = [];
156
- const chatArgs = [];
157
- for (let i = 1; i < args.length; i++) {
158
- if ((args[i] === "--model" || args[i] === "-m") && args[i + 1]) {
159
- modelOverride = args[++i];
160
- }
161
- else if (args[i] === "--resume" && args[i + 1]) {
162
- resumeId = args[++i];
163
- }
164
- else if ((args[i] === "--image" || args[i] === "-i") && args[i + 1]) {
165
- images.push(args[++i]);
166
- }
167
- else {
168
- chatArgs.push(args[i]);
169
- }
248
+ const { model: modelOverride, provider: providerOverride, resume: resumeId, images, positionals } = parseFlags(args.slice(1));
249
+ const message = positionals.join(" ");
250
+ if (resumeId && message) {
251
+ console.error("--resume <id> cannot be combined with a message; run `min-agent chat --resume <id>` to continue that session");
252
+ process.exit(1);
170
253
  }
171
- const message = chatArgs.join(" ");
172
254
  if (!message) {
173
255
  const { runTui } = await import("./tui-chat.js");
174
- await runTui({ modelId: modelOverride, resumeSessionId: resumeId, mode: "chat" });
256
+ await runTui({ modelId: modelOverride, providerName: providerOverride, resumeSessionId: resumeId, mode: "chat", images });
175
257
  }
176
258
  else {
177
- await runAgent(message, modelOverride, images.length > 0 ? images : undefined);
259
+ await runAgent(message, modelOverride, images.length > 0 ? images : undefined, providerOverride);
178
260
  }
179
261
  break;
180
262
  }
@@ -183,18 +265,9 @@ async function main() {
183
265
  console.error("Not configured. Run: min-agent setup");
184
266
  process.exit(1);
185
267
  }
186
- let modelOverride;
187
- let resumeId;
188
- for (let i = 1; i < args.length; i++) {
189
- if ((args[i] === "--model" || args[i] === "-m") && args[i + 1]) {
190
- modelOverride = args[++i];
191
- }
192
- else if (args[i] === "--resume" && args[i + 1]) {
193
- resumeId = args[++i];
194
- }
195
- }
268
+ const { model: modelOverride, provider: providerOverride, resume: resumeId } = parseFlags(args.slice(1));
196
269
  const { runTui } = await import("./tui-chat.js");
197
- await runTui({ modelId: modelOverride, resumeSessionId: resumeId, mode: "code" });
270
+ await runTui({ modelId: modelOverride, providerName: providerOverride, resumeSessionId: resumeId, mode: "code" });
198
271
  break;
199
272
  }
200
273
  case "serve": {
@@ -202,16 +275,7 @@ async function main() {
202
275
  console.error("Not configured. Run: min-agent setup");
203
276
  process.exit(1);
204
277
  }
205
- let servePort;
206
- let serveHost;
207
- for (let i = 1; i < args.length; i++) {
208
- if ((args[i] === "--port" || args[i] === "-p") && args[i + 1]) {
209
- servePort = parseInt(args[++i], 10);
210
- }
211
- else if (args[i] === "--host" && args[i + 1]) {
212
- serveHost = args[++i];
213
- }
214
- }
278
+ const { port: servePort, host: serveHost } = parseFlags(args.slice(1));
215
279
  const { runServe } = await import("./serve.js");
216
280
  await runServe({ port: servePort, host: serveHost });
217
281
  break;
@@ -222,42 +286,59 @@ async function main() {
222
286
  console.log(`Usage: min-agent mcp <command>
223
287
 
224
288
  Commands:
225
- list List configured MCP servers (name + status)
226
- info <name> Show details of a server
227
- add <name> ... Add a new server (stdio or remote)
228
- remove <name> Remove a server
229
- enable <name> Enable a disabled server
230
- disable <name> Disable a server
231
- check Test connectivity of all servers`);
289
+ list List configured MCP servers (name + status + scope)
290
+ info <name> Show details of a server
291
+ add <name> ... Add a new server (stdio or remote)
292
+ remove <name> [--project] Remove a server
293
+ enable <name...> [--project] Enable a disabled server
294
+ disable <name...> [--project] Disable a server
295
+ check Test connectivity of all servers
296
+
297
+ Add flags:
298
+ --project Write to .min-agent/mcp.json (project scope)
299
+ --url <url> Remote server URL
300
+ --sse | --streamable-http Remote transport mode (default: auto)
301
+ --token <t> Bearer token for remote servers
302
+ --env KEY=VALUE Extra env for stdio servers (repeatable)
303
+ --timeout <ms> Connection/call timeout
304
+ --skip-check Skip validation before saving`);
232
305
  process.exit(0);
233
306
  }
234
307
  switch (subcommand) {
235
308
  case "add": {
236
309
  const name = args[2];
237
310
  if (!name) {
238
- console.error("Usage: min-agent mcp add <name> [--skip-check] <command...>");
239
- console.error(" or: min-agent mcp add <name> --url <https://host/mcp> [--sse] [--token <bearer>] [--skip-check]");
311
+ console.error("Usage: min-agent mcp add <name> [--project] [--skip-check] <command...>");
312
+ console.error(" or: min-agent mcp add <name> --url <https://host/mcp> [--sse|--streamable-http] [--token <bearer>] [--project] [--skip-check]");
313
+ process.exit(1);
314
+ }
315
+ const { skipCheck, url, token, sse, streamableHttp, project, timeout, environment, cmd } = parseMcpAddArgs(args.slice(3));
316
+ const scope = project ? "project" : "global";
317
+ if (sse && streamableHttp) {
318
+ console.error("--sse and --streamable-http are mutually exclusive");
240
319
  process.exit(1);
241
320
  }
242
- const { skipCheck, url, token, sse, cmd } = parseMcpAddArgs(args.slice(3));
243
321
  let entry;
244
322
  if (url?.trim()) {
245
323
  entry = {
246
324
  url: url.trim(),
247
325
  enabled: true,
248
- remoteTransport: sse ? "sse" : "auto",
326
+ remoteTransport: streamableHttp ? "streamable-http" : sse ? "sse" : "auto",
327
+ ...(timeout ? { timeout } : {}),
249
328
  };
250
329
  if (token?.trim())
251
330
  entry.token = token.trim();
252
331
  }
253
332
  else if (cmd.length > 0) {
254
- entry = { command: cmd, enabled: true };
333
+ entry = { command: cmd, enabled: true, ...(timeout ? { timeout } : {}) };
255
334
  }
256
335
  else {
257
- console.error("Usage: min-agent mcp add <name> [--skip-check] <command...>");
258
- console.error(" or: min-agent mcp add <name> --url <https://host/mcp> [--sse] [--token <bearer>] [--skip-check]");
336
+ console.error("Usage: min-agent mcp add <name> [--project] [--skip-check] <command...>");
337
+ console.error(" or: min-agent mcp add <name> --url <https://host/mcp> [--sse|--streamable-http] [--token <bearer>] [--project] [--skip-check]");
259
338
  process.exit(1);
260
339
  }
340
+ if (Object.keys(environment).length > 0)
341
+ entry.environment = environment;
261
342
  let detectedTools = 0;
262
343
  if (!skipCheck) {
263
344
  console.log(`Validating MCP server "${name}"...`);
@@ -271,41 +352,47 @@ Commands:
271
352
  else {
272
353
  console.log(`Skipping MCP validation for "${name}" (--skip-check).`);
273
354
  }
274
- const config = loadMcpConfig();
355
+ const config = loadMcpConfigFile(scope);
275
356
  config.mcpServers[name] = entry;
276
- saveMcpConfig(config);
357
+ saveMcpConfig(config, scope);
277
358
  const toolsSuffix = skipCheck ? "" : ` (${detectedTools} tools detected)`;
278
- console.log(`✓ MCP server "${name}" added: ${formatMcpServerBinding(entry)}${toolsSuffix}`);
359
+ const scopeSuffix = project ? " [project]" : "";
360
+ console.log(`✓ MCP server "${name}" added${scopeSuffix}: ${formatMcpServerBinding(entry)}${toolsSuffix}`);
279
361
  break;
280
362
  }
281
363
  case "remove": {
282
- const name = args[2];
364
+ const project = args.includes("--project");
365
+ const scope = project ? "project" : "global";
366
+ const name = args.slice(2).find((a) => a !== "--project");
283
367
  if (!name) {
284
- console.error("Usage: min-agent mcp remove <name>");
368
+ console.error("Usage: min-agent mcp remove <name> [--project]");
285
369
  process.exit(1);
286
370
  }
287
- const config = loadMcpConfig();
371
+ const config = loadMcpConfigFile(scope);
288
372
  if (!config.mcpServers[name]) {
289
- console.error(`MCP server "${name}" not found`);
373
+ const elsewhere = loadMcpConfigEntries().find((e) => e.name === name);
374
+ console.error(elsewhere
375
+ ? `MCP server "${name}" is defined in ${elsewhere.scope} scope, not ${scope}.`
376
+ : `MCP server "${name}" not found`);
290
377
  process.exit(1);
291
378
  }
292
379
  delete config.mcpServers[name];
293
- saveMcpConfig(config);
294
- console.log(`✓ MCP server "${name}" removed`);
380
+ saveMcpConfig(config, scope);
381
+ console.log(`✓ MCP server "${name}" removed${project ? " [project]" : ""}`);
295
382
  break;
296
383
  }
297
384
  case "list": {
298
- const config = loadMcpConfig();
299
- const servers = Object.entries(config.mcpServers);
300
- if (servers.length === 0) {
385
+ const entries = loadMcpConfigEntries();
386
+ if (entries.length === 0) {
301
387
  console.log("No MCP servers configured.");
302
388
  console.log("Add one with: min-agent mcp add <name> <command...>");
303
389
  }
304
390
  else {
305
391
  console.log("MCP Servers:");
306
- for (const [name, cfg] of servers) {
307
- const status = cfg.enabled === false ? "\x1b[90mdisabled\x1b[0m" : "\x1b[32menabled\x1b[0m";
308
- console.log(` ${name} ${status}`);
392
+ for (const { name, config, scope } of entries) {
393
+ const status = config.enabled === false ? "\x1b[90mdisabled\x1b[0m" : "\x1b[32menabled\x1b[0m";
394
+ const scopeMark = scope === "project" ? " \x1b[90m[project]\x1b[0m" : "";
395
+ console.log(` ${name} ${status}${scopeMark}`);
309
396
  }
310
397
  console.log("\nUse: min-agent mcp info <name> for details");
311
398
  }
@@ -317,25 +404,39 @@ Commands:
317
404
  console.error("Usage: min-agent mcp info <name>");
318
405
  process.exit(1);
319
406
  }
320
- const config = loadMcpConfig();
321
- const cfg = config.mcpServers[name];
322
- if (!cfg) {
407
+ const found = loadMcpConfigEntries().find((e) => e.name === name);
408
+ if (!found) {
323
409
  console.error(`MCP server "${name}" not found`);
324
410
  process.exit(1);
325
411
  }
412
+ const cfg = found.config;
326
413
  console.log(`Name: ${name}`);
414
+ console.log(`Scope: ${found.scope}`);
327
415
  console.log(`Status: ${cfg.enabled === false ? "disabled" : "enabled"}`);
328
416
  console.log(`Binding: ${formatMcpServerBinding(cfg)}`);
329
417
  if (cfg.command)
330
418
  console.log(`Command: ${Array.isArray(cfg.command) ? cfg.command.join(" ") : `${cfg.command} ${(cfg.args ?? []).join(" ")}`.trim()}`);
331
- if (cfg.url)
419
+ if (cfg.url) {
332
420
  console.log(`URL: ${cfg.url}`);
421
+ if (cfg.remoteTransport)
422
+ console.log(`Transport: ${cfg.remoteTransport}`);
423
+ if (cfg.oauth === false)
424
+ console.log(`OAuth: disabled`);
425
+ else if (cfg.oauth)
426
+ console.log(`OAuth: enabled${cfg.oauth.clientId ? ` (clientId set)` : ""}`);
427
+ }
428
+ if (cfg.headers)
429
+ console.log(`Headers: ${Object.keys(cfg.headers).join(", ")} (values hidden)`);
333
430
  if (cfg.token)
334
431
  console.log(`Token: ***`);
335
432
  if (cfg.environment)
336
433
  console.log(`Env: ${Object.keys(cfg.environment).join(", ")}`);
337
434
  if (cfg.timeout)
338
435
  console.log(`Timeout: ${cfg.timeout}ms`);
436
+ if (cfg.connectTimeout)
437
+ console.log(`ConnectTimeout: ${cfg.connectTimeout}ms`);
438
+ if (cfg.callTimeout)
439
+ console.log(`CallTimeout: ${cfg.callTimeout}ms`);
339
440
  break;
340
441
  }
341
442
  case "check": {
@@ -370,39 +471,43 @@ Commands:
370
471
  break;
371
472
  }
372
473
  case "enable": {
373
- const names = args.slice(2);
474
+ const project = args.includes("--project");
475
+ const scope = project ? "project" : "global";
476
+ const names = args.slice(2).filter((a) => a !== "--project");
374
477
  if (names.length === 0) {
375
- console.error("Usage: min-agent mcp enable <name...>");
478
+ console.error("Usage: min-agent mcp enable <name...> [--project]");
376
479
  process.exit(1);
377
480
  }
378
- const config = loadMcpConfig();
481
+ const config = loadMcpConfigFile(scope);
379
482
  for (const name of names) {
380
483
  if (!config.mcpServers[name]) {
381
- console.error(`MCP server "${name}" not found`);
484
+ console.error(`MCP server "${name}" not found${project ? " in project scope" : ""}`);
382
485
  continue;
383
486
  }
384
487
  config.mcpServers[name].enabled = true;
385
- console.log(`✓ MCP server "${name}" enabled`);
488
+ console.log(`✓ MCP server "${name}" enabled${project ? " [project]" : ""}`);
386
489
  }
387
- saveMcpConfig(config);
490
+ saveMcpConfig(config, scope);
388
491
  break;
389
492
  }
390
493
  case "disable": {
391
- const names = args.slice(2);
494
+ const project = args.includes("--project");
495
+ const scope = project ? "project" : "global";
496
+ const names = args.slice(2).filter((a) => a !== "--project");
392
497
  if (names.length === 0) {
393
- console.error("Usage: min-agent mcp disable <name...>");
498
+ console.error("Usage: min-agent mcp disable <name...> [--project]");
394
499
  process.exit(1);
395
500
  }
396
- const config = loadMcpConfig();
501
+ const config = loadMcpConfigFile(scope);
397
502
  for (const name of names) {
398
503
  if (!config.mcpServers[name]) {
399
- console.error(`MCP server "${name}" not found`);
504
+ console.error(`MCP server "${name}" not found${project ? " in project scope" : ""}`);
400
505
  continue;
401
506
  }
402
507
  config.mcpServers[name].enabled = false;
403
- console.log(`✓ MCP server "${name}" disabled`);
508
+ console.log(`✓ MCP server "${name}" disabled${project ? " [project]" : ""}`);
404
509
  }
405
- saveMcpConfig(config);
510
+ saveMcpConfig(config, scope);
406
511
  break;
407
512
  }
408
513
  default:
@@ -412,6 +517,74 @@ Commands:
412
517
  break;
413
518
  }
414
519
  case "history": {
520
+ const subcommand = args[1];
521
+ if (subcommand === "delete") {
522
+ const id = args[2];
523
+ if (!id) {
524
+ console.error("Usage: min-agent history delete <id>");
525
+ process.exit(1);
526
+ }
527
+ const { deleteSession } = await import("./sessions.js");
528
+ if (deleteSession(id)) {
529
+ console.log(`✓ 会话 ${id} 已删除`);
530
+ }
531
+ else {
532
+ console.error(`会话 ${id} 不存在`);
533
+ process.exit(1);
534
+ }
535
+ break;
536
+ }
537
+ if (subcommand === "rename") {
538
+ const id = args[2];
539
+ const title = args.slice(3).join(" ");
540
+ if (!id || !title) {
541
+ console.error("Usage: min-agent history rename <id> <title>");
542
+ process.exit(1);
543
+ }
544
+ const { renameSession } = await import("./sessions.js");
545
+ if (renameSession(id, title)) {
546
+ console.log(`✓ 会话 ${id} 已重命名为: ${title}`);
547
+ }
548
+ else {
549
+ console.error(`会话 ${id} 不存在`);
550
+ process.exit(1);
551
+ }
552
+ break;
553
+ }
554
+ if (subcommand === "export") {
555
+ const id = args[2];
556
+ if (!id) {
557
+ console.error("Usage: min-agent history export <id> [-o <file>]");
558
+ process.exit(1);
559
+ }
560
+ const { loadSession } = await import("./sessions.js");
561
+ const session = loadSession(id);
562
+ if (!session) {
563
+ console.error(`会话 ${id} 不存在`);
564
+ process.exit(1);
565
+ }
566
+ const json = JSON.stringify(session, null, 2);
567
+ const outIdx = args.indexOf("-o");
568
+ const outFile = outIdx !== -1 ? args[outIdx + 1] : undefined;
569
+ if (outFile) {
570
+ try {
571
+ writeFileSync(outFile, json, "utf-8");
572
+ console.log(`✓ 会话 ${id} 已导出到 ${outFile}`);
573
+ }
574
+ catch (err) {
575
+ console.error(`导出失败: ${err.message}`);
576
+ process.exit(1);
577
+ }
578
+ }
579
+ else {
580
+ if (outIdx !== -1) {
581
+ console.error("Usage: min-agent history export <id> [-o <file>](-o 后缺少文件路径)");
582
+ process.exit(1);
583
+ }
584
+ console.log(json);
585
+ }
586
+ break;
587
+ }
415
588
  const { listSessions } = await import("./sessions.js");
416
589
  const sessions = listSessions();
417
590
  if (sessions.length === 0) {
@@ -502,89 +675,174 @@ Commands:
502
675
  console.log(`Usage: min-agent skills <command>
503
676
 
504
677
  Commands:
505
- list List skills (name + status)
506
- info <name> Show details of a skill
507
- enable <name> Enable a disabled skill
508
- disable <name> Disable a skill`);
678
+ list List skills (name + status + description)
679
+ info <name> Show details of a skill
680
+ new <name> Create a new skill scaffold
681
+ enable <name...> Enable disabled skills
682
+ disable <name...> Disable skills
683
+
684
+ Options (enable/disable):
685
+ --project Apply to this project only (.min-agent/config.json)
686
+ --global Apply to every project (default, ~/.min-agent/config.json)
687
+
688
+ Options (list/info):
689
+ --json Machine-readable JSON output
690
+
691
+ Options (new):
692
+ --global Create in ~/.agents/skills/ instead of .min-agent/skills/
693
+
694
+ Project scope wins over global, so a skill disabled globally can be
695
+ re-enabled for one repo with: min-agent skills enable <name> --project
696
+
697
+ Skills are discovered in (later paths override earlier ones):
698
+ ~/.agents/skills/ .min-agent/skills/ .agents/skills/
699
+ .opencode/skills/ .claude/skills/`);
509
700
  process.exit(0);
510
701
  }
511
702
  switch (subcommand) {
512
703
  case "info": {
513
- const name = args[2];
704
+ const flags = args.slice(2);
705
+ const name = flags.find((a) => !a.startsWith("-"));
514
706
  if (!name) {
515
- console.error("Usage: min-agent skills info <name>");
707
+ console.error("Usage: min-agent skills info <name> [--json]");
516
708
  process.exit(1);
517
709
  }
518
710
  discoverSkills({ silent: true });
519
- const { getSkill } = await import("./skills.js");
711
+ const { getSkill, getAllSkills } = await import("./skills.js");
520
712
  const skill = getSkill(name);
521
713
  if (!skill) {
522
- console.error(`Skill "${name}" not found`);
714
+ const names = getAllSkills().map((s) => s.name);
715
+ console.error(`Skill "${name}" not found.${names.length ? ` Available: ${names.join(", ")}` : " No skills discovered."}`);
523
716
  process.exit(1);
524
717
  }
525
- const config = loadConfig();
526
- const isDisabled = (config.disabledSkills ?? []).includes(name);
718
+ if (flags.includes("--json")) {
719
+ console.log(JSON.stringify(skill, null, 2));
720
+ break;
721
+ }
527
722
  console.log(`Name: ${skill.name}`);
528
- console.log(`Status: ${isDisabled ? "disabled" : "enabled"}`);
723
+ console.log(`Status: ${skill.enabled ? "enabled" : `disabled (${skill.disabledScope} scope)`}`);
724
+ if (skill.version)
725
+ console.log(`Version: ${skill.version}`);
529
726
  console.log(`Description: ${skill.description}`);
530
727
  console.log(`Location: ${skill.location}`);
728
+ if (skill.allowedTools.length > 0)
729
+ console.log(`Tools: ${skill.allowedTools.join(", ")}`);
730
+ if (skill.requiredBins.length > 0)
731
+ console.log(`Requires: ${skill.requiredBins.join(", ")}`);
732
+ if (skill.content.trim()) {
733
+ console.log("\n--- Content ---\n");
734
+ console.log(skill.content.trim());
735
+ }
531
736
  break;
532
737
  }
533
- case "enable": {
534
- const names = args.slice(2);
738
+ case "enable":
739
+ case "disable": {
740
+ const flags = args.slice(2);
741
+ const scope = flags.includes("--project") ? "project" : "global";
742
+ const names = flags.filter((a) => a !== "--project" && a !== "--global");
743
+ const unknownFlag = flags.find((a) => a.startsWith("-") && a !== "--project" && a !== "--global");
744
+ if (unknownFlag) {
745
+ console.error(`Unknown option: ${unknownFlag} (use --project or --global)`);
746
+ process.exit(1);
747
+ }
535
748
  if (names.length === 0) {
536
- console.error("Usage: min-agent skills enable <name...>");
749
+ console.error(`Usage: min-agent skills ${subcommand} <name...> [--project]`);
750
+ process.exit(1);
751
+ }
752
+ discoverSkills({ silent: true });
753
+ const { getAllSkills, setSkillEnabled, getStaleDisabledSkills } = await import("./skills.js");
754
+ const known = new Set(getAllSkills().map((s) => s.name));
755
+ const stale = new Set(getStaleDisabledSkills());
756
+ // `enable` must accept stale names so they can be cleaned up from config;
757
+ // `disable` on an unknown name is always an error.
758
+ const unknown = names.filter((n) => !known.has(n) && !(subcommand === "enable" && stale.has(n)));
759
+ if (unknown.length > 0) {
760
+ console.error(`Skill${unknown.length > 1 ? "s" : ""} not found: ${unknown.join(", ")}`);
761
+ console.error(known.size ? `Available: ${[...known].join(", ")}` : "No skills discovered.");
537
762
  process.exit(1);
538
763
  }
539
- const config = loadConfig();
540
- config.disabledSkills = (config.disabledSkills ?? []).filter((s) => !names.includes(s));
541
- saveConfig(config);
542
- for (const name of names)
543
- console.log(`✓ Skill "${name}" enabled`);
764
+ for (const result of setSkillEnabled(names, subcommand === "enable", scope)) {
765
+ console.log(`✓ Skill "${result.name}" ${subcommand}d (${scope} scope)`);
766
+ if (result.blockedBy) {
767
+ console.log(`\x1b[33m ⚠ still ${result.enabled ? "enabled" : "disabled"} — overridden by ${result.blockedBy} scope\x1b[0m`);
768
+ }
769
+ }
544
770
  break;
545
771
  }
546
- case "disable": {
547
- const names = args.slice(2);
548
- if (names.length === 0) {
549
- console.error("Usage: min-agent skills disable <name...>");
772
+ case "new": {
773
+ const flags = args.slice(2);
774
+ const name = flags.find((a) => !a.startsWith("-"));
775
+ if (!name || !/^[\w-]+$/.test(name)) {
776
+ console.error("Usage: min-agent skills new <name> [--global] (name: letters, digits, _ or -)");
550
777
  process.exit(1);
551
778
  }
552
- const config = loadConfig();
553
- const disabled = config.disabledSkills ?? [];
554
- for (const name of names) {
555
- if (!disabled.includes(name))
556
- disabled.push(name);
557
- console.log(`✓ Skill "${name}" disabled`);
779
+ const global = flags.includes("--global");
780
+ const root = global ? path.join(os.homedir(), ".agents", "skills") : path.join(process.cwd(), ".min-agent", "skills");
781
+ const dir = path.join(root, name);
782
+ if (existsSync(path.join(dir, "SKILL.md"))) {
783
+ console.error(`Skill "${name}" already exists at ${dir}`);
784
+ process.exit(1);
558
785
  }
559
- config.disabledSkills = disabled;
560
- saveConfig(config);
786
+ mkdirSync(dir, { recursive: true });
787
+ writeFileSync(path.join(dir, "SKILL.md"), `---\nname: ${name}\ndescription: What this skill does\n# version: 1.0.0\n# allowed-tools: [read, grep]\n# metadata:\n# requires:\n# bins: ["some-cli"]\n---\n\n# ${name}\n\nWrite the skill instructions here.\n`, "utf-8");
788
+ console.log(`✓ Created skill "${name}" at ${path.join(dir, "SKILL.md")}`);
561
789
  break;
562
790
  }
563
791
  case "list": {
792
+ const json = args.includes("--json");
564
793
  discoverSkills({ silent: true });
565
- const skills = getSkills();
566
- const config = loadConfig();
567
- const disabledNames = config.disabledSkills ?? [];
568
- if (skills.length === 0 && disabledNames.length === 0) {
794
+ const { getAllSkills, getStaleDisabledSkills } = await import("./skills.js");
795
+ const all = getAllSkills().sort((a, b) => a.name.localeCompare(b.name));
796
+ const stale = getStaleDisabledSkills();
797
+ if (json) {
798
+ console.log(JSON.stringify({
799
+ count: all.length,
800
+ enabled_count: all.filter((s) => s.enabled).length,
801
+ stale_disabled: stale,
802
+ skills: all.map((s) => ({
803
+ name: s.name,
804
+ description: s.description,
805
+ location: s.location,
806
+ enabled: s.enabled,
807
+ ...(s.disabledScope ? { disabled_scope: s.disabledScope } : {}),
808
+ ...(s.version ? { version: s.version } : {}),
809
+ ...(s.allowedTools.length ? { allowed_tools: s.allowedTools } : {}),
810
+ ...(s.requiredBins.length ? { requires_bins: s.requiredBins } : {}),
811
+ })),
812
+ }, null, 2));
813
+ break;
814
+ }
815
+ if (all.length === 0 && stale.length === 0) {
569
816
  console.log("No skills found. Add SKILL.md files in .min-agent/skills/<name>/");
570
817
  break;
571
818
  }
572
- console.log("Skills:");
573
- for (const skill of skills) {
574
- console.log(` ${skill.name} \x1b[32menabled\x1b[0m`);
819
+ console.log(`Skills (${all.filter((s) => s.enabled).length} enabled, ${all.filter((s) => !s.enabled).length} disabled):`);
820
+ for (const skill of all) {
821
+ const status = skill.enabled
822
+ ? "\x1b[32menabled \x1b[0m"
823
+ : `\x1b[90mdisabled(${skill.disabledScope === "project" ? "proj" : "glob"})\x1b[0m`;
824
+ console.log(` ${skill.name} ${status} \x1b[90m${truncate(skill.description, 60)}\x1b[0m`);
825
+ console.log(` \x1b[90m${skill.location}\x1b[0m`);
575
826
  }
576
- for (const n of disabledNames) {
577
- console.log(` ${n} \x1b[90mdisabled\x1b[0m`);
827
+ if (stale.length > 0) {
828
+ console.log(`\n\x1b[33m⚠ disabledSkills/enabledSkills entries matching no skill: ${stale.join(", ")}\x1b[0m`);
829
+ console.log(`\x1b[33m Remove them with: min-agent skills enable ${stale.join(" ")} (add --project if the entry is project-scoped)\x1b[0m`);
578
830
  }
579
831
  console.log("\nUse: min-agent skills info <name> for details");
580
832
  break;
581
833
  }
582
834
  default:
583
- console.error("Usage: min-agent skills -h");
835
+ console.error(`Unknown skills command: ${subcommand}`);
836
+ console.error("Available: list, info, new, enable, disable (see: min-agent skills -h)");
584
837
  process.exit(1);
585
838
  }
586
839
  break;
587
840
  }
841
+ case "update": {
842
+ const { runUpdate } = await import("./updater.js");
843
+ await runUpdate();
844
+ break;
845
+ }
588
846
  case "init": {
589
847
  ensureProjectDefaults();
590
848
  console.log(`✓ Initialized .min-agent/ in ${process.cwd()}`);