min-agent 0.2.1 → 0.4.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 (137) hide show
  1. package/README.md +242 -31
  2. package/dist/agent.js +1233 -485
  3. package/dist/assistant-stream.js +11 -7
  4. package/dist/cli/commands/chat.js +10 -0
  5. package/dist/cli/commands/exec.js +32 -0
  6. package/dist/cli/commands/history.js +58 -0
  7. package/dist/cli/commands/index.js +224 -0
  8. package/dist/cli/commands/init.js +18 -0
  9. package/dist/cli/commands/mcp.js +173 -0
  10. package/dist/cli/commands/memory.js +69 -0
  11. package/dist/cli/commands/models.js +21 -0
  12. package/dist/cli/commands/permission.js +12 -0
  13. package/dist/cli/commands/rules.js +33 -0
  14. package/dist/cli/commands/sandbox.js +13 -0
  15. package/dist/cli/commands/serve.js +9 -0
  16. package/dist/cli/commands/setup.js +4 -0
  17. package/dist/cli/commands/shared.js +16 -0
  18. package/dist/cli/commands/skills.js +119 -0
  19. package/dist/cli/commands/update.js +7 -0
  20. package/dist/cli/commands/write-config.js +30 -0
  21. package/dist/cli/errors.js +36 -0
  22. package/dist/cli/exec-prompt.js +26 -0
  23. package/dist/cli/option-helpers.js +53 -0
  24. package/dist/cli/program.js +180 -0
  25. package/dist/cli.js +7 -632
  26. package/dist/clipboard.js +59 -23
  27. package/dist/code-mode.js +35 -17
  28. package/dist/compaction.js +457 -169
  29. package/dist/config.js +298 -38
  30. package/dist/confirm.js +105 -9
  31. package/dist/context-window.js +156 -75
  32. package/dist/doom-loop.js +268 -26
  33. package/dist/fetch-timeout.js +152 -0
  34. package/dist/http-approvals.js +60 -0
  35. package/dist/http.js +119 -0
  36. package/dist/instructions.js +72 -33
  37. package/dist/logger.js +95 -0
  38. package/dist/markdown.js +35 -50
  39. package/dist/mcp.js +847 -102
  40. package/dist/memory.js +128 -45
  41. package/dist/output.js +42 -31
  42. package/dist/paste-handler.js +3 -3
  43. package/dist/permission-cli.js +43 -0
  44. package/dist/plugins.js +76 -11
  45. package/dist/pricing.js +119 -0
  46. package/dist/provider.js +34 -15
  47. package/dist/question-format.js +60 -0
  48. package/dist/sandbox-cli.js +82 -0
  49. package/dist/sandbox.js +403 -0
  50. package/dist/save-throttle.js +45 -0
  51. package/dist/serve/common.js +404 -0
  52. package/dist/serve/routes-chat.js +347 -0
  53. package/dist/serve/routes-mcp.js +212 -0
  54. package/dist/serve/routes-memory.js +66 -0
  55. package/dist/serve/routes-meta.js +205 -0
  56. package/dist/serve/routes-sessions.js +61 -0
  57. package/dist/serve/routes-skills.js +70 -0
  58. package/dist/serve.js +74 -635
  59. package/dist/sessions.js +197 -15
  60. package/dist/skills.js +531 -77
  61. package/dist/synthetic.js +7 -0
  62. package/dist/title-gen.js +9 -2
  63. package/dist/token-display.js +36 -0
  64. package/dist/tool-display.js +178 -0
  65. package/dist/tool-output.js +53 -46
  66. package/dist/tools/apply_patch.js +265 -0
  67. package/dist/tools/atomic-file.js +35 -0
  68. package/dist/tools/backend.js +61 -0
  69. package/dist/tools/bash.js +186 -71
  70. package/dist/tools/code_search.js +13 -6
  71. package/dist/tools/edit.js +26 -9
  72. package/dist/tools/explore.js +144 -16
  73. package/dist/tools/glob.js +7 -3
  74. package/dist/tools/grep.js +153 -14
  75. package/dist/tools/index.js +9 -24
  76. package/dist/tools/question.js +31 -30
  77. package/dist/tools/read.js +77 -15
  78. package/dist/tools/search-searxng.js +223 -0
  79. package/dist/tools/search-serper.js +189 -0
  80. package/dist/tools/task.js +100 -33
  81. package/dist/tools/todo.js +178 -67
  82. package/dist/tools/web_fetch.js +158 -46
  83. package/dist/tools/web_search.js +217 -29
  84. package/dist/tools/write.js +34 -11
  85. package/dist/tui/App.js +89 -6
  86. package/dist/tui/ConfirmBar.js +57 -4
  87. package/dist/tui/InputBar.js +504 -44
  88. package/dist/tui/MessageList.js +674 -20
  89. package/dist/tui/ModelPicker.js +113 -0
  90. package/dist/tui/QuestionBar.js +136 -0
  91. package/dist/tui/SessionPicker.js +79 -0
  92. package/dist/tui/StatusBar.js +14 -12
  93. package/dist/tui/agent-runner.js +223 -0
  94. package/dist/tui/caret-pos.js +177 -0
  95. package/dist/tui/caret.js +69 -0
  96. package/dist/tui/click-count.js +13 -0
  97. package/dist/tui/diff-view.js +61 -0
  98. package/dist/tui/drag-state.js +49 -0
  99. package/dist/tui/hydrate.js +129 -0
  100. package/dist/tui/index.js +189 -31
  101. package/dist/tui/input-history.js +125 -0
  102. package/dist/tui/layout.js +88 -0
  103. package/dist/tui/mouse.js +46 -0
  104. package/dist/tui/prompt-queue.js +24 -0
  105. package/dist/tui/selection.js +226 -0
  106. package/dist/tui/session-switch.js +28 -0
  107. package/dist/tui/slash-commands.js +106 -0
  108. package/dist/tui/slash-handler.js +545 -0
  109. package/dist/tui/text-width.js +113 -0
  110. package/dist/tui/theme.js +12 -0
  111. package/dist/tui/token-info.js +7 -0
  112. package/dist/tui/tool-children.js +19 -0
  113. package/dist/tui/undo-stack.js +14 -0
  114. package/dist/tui/use-sgr-mouse.js +29 -0
  115. package/dist/tui-chat.js +346 -330
  116. package/dist/updater.js +116 -0
  117. package/dist/xml-search.js +194 -0
  118. package/docs/API.md +410 -32
  119. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
  120. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
  121. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
  122. package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
  123. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
  124. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
  125. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
  126. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
  127. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
  128. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
  129. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
  130. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
  131. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
  132. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
  133. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
  134. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
  135. package/package.json +12 -8
  136. package/skills/self-config/SKILL.md +90 -0
  137. package/skills/self-config/reference.md +149 -0
package/dist/memory.js CHANGED
@@ -1,74 +1,138 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1
+ import { readFileSync, mkdirSync, existsSync, renameSync } from "fs";
2
2
  import path from "path";
3
- import { getConfigDir } from "./config.js";
3
+ import { getConfigDir, getProjectConfigDir } from "./config.js";
4
+ import { atomicWriteFileSync } from "./tools/atomic-file.js";
4
5
  import { tool, jsonSchema } from "ai";
5
- function getMemoryFile() {
6
+ export function readMemoryScope(value) {
7
+ if (value === undefined || value === null || value === "")
8
+ return { ok: true, scope: undefined };
9
+ if (value === "global" || value === "project")
10
+ return { ok: true, scope: value };
11
+ return { ok: false };
12
+ }
13
+ export function takeScopeFlags(tokens) {
14
+ let scope;
15
+ const rest = [];
16
+ for (const t of tokens) {
17
+ if (t === "--project")
18
+ scope = "project";
19
+ else if (t === "--global")
20
+ scope = "global";
21
+ else
22
+ rest.push(t);
23
+ }
24
+ return { scope, rest };
25
+ }
26
+ export function defaultMemoryScope() {
27
+ return existsSync(getProjectConfigDir()) ? "project" : "global";
28
+ }
29
+ function getMemoryFile(scope) {
30
+ if (scope === "project")
31
+ return path.join(getProjectConfigDir(), "memory.json");
6
32
  return path.join(getConfigDir(), "memory.json");
7
33
  }
8
- export function loadMemories() {
9
- const file = getMemoryFile();
34
+ /** Preserve corrupt data instead of letting it be overwritten and lost forever. */
35
+ function backupCorrupt(file) {
36
+ try {
37
+ renameSync(file, `${file}.corrupt-${Date.now()}`);
38
+ }
39
+ catch {
40
+ // nothing else we can do with an unreadable file
41
+ }
42
+ }
43
+ export function loadMemories(scope = "global") {
44
+ const file = getMemoryFile(scope);
10
45
  if (!existsSync(file))
11
46
  return [];
47
+ let data;
12
48
  try {
13
- return JSON.parse(readFileSync(file, "utf-8"));
49
+ data = JSON.parse(readFileSync(file, "utf-8"));
14
50
  }
15
51
  catch {
52
+ backupCorrupt(file);
16
53
  return [];
17
54
  }
55
+ if (!Array.isArray(data)) {
56
+ backupCorrupt(file);
57
+ return [];
58
+ }
59
+ return data.filter((m) => typeof m === "object" && m !== null && typeof m.content === "string");
18
60
  }
19
- function saveMemories(memories) {
20
- const file = getMemoryFile();
61
+ /** Atomic replace via temp file + rename, so a crash never leaves a truncated memory.json. */
62
+ function saveMemories(memories, scope) {
63
+ const file = getMemoryFile(scope);
21
64
  mkdirSync(path.dirname(file), { recursive: true });
22
- writeFileSync(file, JSON.stringify(memories, null, 2), "utf-8");
65
+ atomicWriteFileSync(file, JSON.stringify(memories, null, 2));
23
66
  }
24
- export function addMemory(content, tags = []) {
25
- const memories = loadMemories();
67
+ // read-modify-write runs fully synchronously, so concurrent tool calls in
68
+ // this process cannot interleave between load and save.
69
+ export function addMemory(content, tags = [], scope = "global") {
70
+ const memories = loadMemories(scope);
26
71
  const memory = {
27
72
  content,
28
73
  tags,
29
74
  created: new Date().toISOString(),
30
75
  };
31
76
  memories.push(memory);
32
- saveMemories(memories);
77
+ saveMemories(memories, scope);
33
78
  return memory;
34
79
  }
35
- export function deleteMemory(index) {
36
- const memories = loadMemories();
80
+ export function deleteMemory(index, scope = "global") {
81
+ const memories = loadMemories(scope);
37
82
  if (index < 0 || index >= memories.length)
38
83
  return false;
39
84
  memories.splice(index, 1);
40
- saveMemories(memories);
85
+ saveMemories(memories, scope);
41
86
  return true;
42
87
  }
43
- export function searchMemories(query) {
44
- const memories = loadMemories();
88
+ export function searchMemories(query, scope = "global") {
89
+ const memories = loadMemories(scope);
45
90
  const lower = query.toLowerCase();
46
91
  return memories
47
92
  .map((m, i) => ({ ...m, index: i }))
48
- .filter((m) => m.content.toLowerCase().includes(lower) ||
49
- m.tags.some((t) => t.toLowerCase().includes(lower)));
93
+ .filter((m) => m.content.toLowerCase().includes(lower) || m.tags.some((t) => t.toLowerCase().includes(lower)));
50
94
  }
51
- /** Build a system prompt section from stored memories */
52
- export function getMemorySystemPrompt() {
53
- const memories = loadMemories();
95
+ function formatMemoryBlock(memories, heading) {
54
96
  if (memories.length === 0)
55
97
  return "";
56
- const items = memories.map((m, i) => {
98
+ const MAX_MEMORIES = 30;
99
+ const start = Math.max(0, memories.length - MAX_MEMORIES);
100
+ const items = memories
101
+ .slice(start)
102
+ .map((m, i) => ({ m, num: start + i + 1 }))
103
+ .reverse()
104
+ .map(({ m, num }) => {
57
105
  const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
58
- return ` ${i + 1}. ${m.content}${tags}`;
106
+ return ` ${num}. ${m.content}${tags}`;
59
107
  });
60
- return [
108
+ const capped = memories.length > MAX_MEMORIES
109
+ ? `\n(${memories.length - MAX_MEMORIES} older memories not shown — use memory_search to find them.)`
110
+ : "";
111
+ return [heading, ...items, capped].filter(Boolean).join("\n");
112
+ }
113
+ /** Build a system prompt section from stored memories (newest first, capped). */
114
+ export function getMemorySystemPrompt() {
115
+ const project = loadMemories("project");
116
+ const global = loadMemories("global");
117
+ if (project.length === 0 && global.length === 0)
118
+ return "";
119
+ const intro = [
61
120
  "## Memories",
62
121
  "The following are things you have remembered from previous conversations. Use them to provide better, personalized responses.",
63
122
  "You can save new memories with the memory_save tool when the user tells you something worth remembering (preferences, project details, conventions, etc).",
123
+ 'Use scope "project" for repo-specific facts and "global" for preferences that apply everywhere.',
64
124
  "",
65
- ...items,
66
- ].join("\n");
125
+ ];
126
+ const sections = [
127
+ formatMemoryBlock(project, "### This project"),
128
+ formatMemoryBlock(global, project.length > 0 ? "### Global" : ""),
129
+ ].filter((s) => s.length > 0);
130
+ return [...intro, ...sections].join("\n");
67
131
  }
68
132
  /** Create the memory tools for the agent */
69
133
  export function getMemoryTools() {
70
134
  const memorySave = tool({
71
- description: "Save a memory for future conversations. Use this when the user shares preferences, project conventions, important context, or asks you to remember something. Memories persist across sessions.",
135
+ description: "Save a memory for future conversations. Use this when the user shares preferences, project conventions, important context, or asks you to remember something. Memories persist across sessions. Use scope project for this repository, global for all projects.",
72
136
  inputSchema: jsonSchema({
73
137
  type: "object",
74
138
  properties: {
@@ -78,12 +142,18 @@ export function getMemoryTools() {
78
142
  items: { type: "string" },
79
143
  description: "Optional tags for categorization (e.g. 'preference', 'project', 'convention')",
80
144
  },
145
+ scope: {
146
+ type: "string",
147
+ enum: ["global", "project"],
148
+ description: "Where to store the memory. Defaults to project when .min-agent exists, otherwise global.",
149
+ },
81
150
  },
82
151
  required: ["content"],
83
152
  }),
84
- execute: async ({ content, tags }) => {
85
- const memory = addMemory(content, tags ?? []);
86
- return `Saved memory: "${content}" (tags: ${memory.tags.length > 0 ? memory.tags.join(", ") : "none"})`;
153
+ execute: async ({ content, tags, scope }) => {
154
+ const resolved = scope === "global" || scope === "project" ? scope : defaultMemoryScope();
155
+ const memory = addMemory(content, tags ?? [], resolved);
156
+ return `Saved ${resolved} memory: "${content}" (tags: ${memory.tags.length > 0 ? memory.tags.join(", ") : "none"})`;
87
157
  },
88
158
  });
89
159
  const memorySearch = tool({
@@ -92,35 +162,48 @@ export function getMemoryTools() {
92
162
  type: "object",
93
163
  properties: {
94
164
  query: { type: "string", description: "Search keyword or phrase" },
165
+ scope: {
166
+ type: "string",
167
+ enum: ["global", "project"],
168
+ description: "Limit search to one store. Omit to search both.",
169
+ },
95
170
  },
96
171
  required: ["query"],
97
172
  }),
98
- execute: async ({ query }) => {
99
- const results = searchMemories(query);
100
- if (results.length === 0)
173
+ execute: async ({ query, scope }) => {
174
+ const scopes = scope === "global" || scope === "project" ? [scope] : ["project", "global"];
175
+ const lines = [];
176
+ for (const s of scopes) {
177
+ for (const m of searchMemories(query, s)) {
178
+ const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
179
+ lines.push(`${s} #${m.index + 1}: ${m.content}${tags} (${m.created.split("T")[0]})`);
180
+ }
181
+ }
182
+ if (lines.length === 0)
101
183
  return `No memories found matching "${query}"`;
102
- return results
103
- .map((m) => {
104
- const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
105
- return `#${m.index + 1}: ${m.content}${tags} (${m.created.split("T")[0]})`;
106
- })
107
- .join("\n");
184
+ return lines.join("\n");
108
185
  },
109
186
  });
110
187
  const memoryDelete = tool({
111
- description: "Delete a memory by its number. Use memory_search or memory_list first to find the index.",
188
+ description: "Delete a memory by its number. Use memory_search first to find the index and scope.",
112
189
  inputSchema: jsonSchema({
113
190
  type: "object",
114
191
  properties: {
115
192
  index: { type: "number", description: "The memory number to delete (1-based)" },
193
+ scope: {
194
+ type: "string",
195
+ enum: ["global", "project"],
196
+ description: "Which store the number belongs to. Defaults to project when .min-agent exists, otherwise global.",
197
+ },
116
198
  },
117
199
  required: ["index"],
118
200
  }),
119
- execute: async ({ index }) => {
120
- const success = deleteMemory(index - 1);
201
+ execute: async ({ index, scope }) => {
202
+ const resolved = scope === "global" || scope === "project" ? scope : defaultMemoryScope();
203
+ const success = deleteMemory(index - 1, resolved);
121
204
  if (success)
122
- return `Memory #${index} deleted.`;
123
- return `Memory #${index} not found.`;
205
+ return `${resolved} memory #${index} deleted.`;
206
+ return `${resolved} memory #${index} not found.`;
124
207
  },
125
208
  });
126
209
  return {
package/dist/output.js CHANGED
@@ -1,4 +1,5 @@
1
- import { loadConfig } from "./config.js";
1
+ import { loadConfig, getActiveProvider } from "./config.js";
2
+ import { summarizeToolCall, toolResultText, truncateDisplay } from "./tool-display.js";
2
3
  const COLORS = {
3
4
  reset: "\x1b[0m",
4
5
  dim: "\x1b[2m",
@@ -11,34 +12,55 @@ const COLORS = {
11
12
  gray: "\x1b[90m",
12
13
  };
13
14
  export function printHeader(modelId) {
14
- const config = loadConfig();
15
- const model = modelId ?? config.provider?.defaultModel ?? "unknown";
15
+ const model = modelId ?? getActiveProvider(loadConfig())?.defaultModel ?? "unknown";
16
16
  console.log(`${COLORS.bold}🤖 min-agent${COLORS.reset} ${COLORS.dim}(${model})${COLORS.reset}`);
17
17
  }
18
18
  export function printDivider() {
19
19
  console.log(`${COLORS.dim}${"─".repeat(60)}${COLORS.reset}`);
20
20
  }
21
21
  export function printToolCall(name, input) {
22
- const argsStr = formatArgs(input);
22
+ const argsStr = summarizeToolCall(name, input, 160);
23
23
  console.log(`\n${COLORS.yellow}⚡ ${name}${COLORS.reset} ${COLORS.dim}${argsStr}${COLORS.reset}`);
24
24
  }
25
- export function printToolResult(name, result) {
26
- const output = typeof result === "string" ? result : JSON.stringify(result, null, 2);
27
- const lines = output.split("\n");
28
- const maxLines = 20;
29
- const truncated = lines.length > maxLines;
30
- const preview = truncated ? lines.slice(0, maxLines).join("\n") : output;
31
- const display = preview.length > 500 ? preview.slice(0, 500) + "..." : preview;
32
- const suffix = truncated ? ` (${lines.length - maxLines} more lines)` : "";
33
- console.log(`${COLORS.green} ✓${COLORS.reset} ${COLORS.dim}${display}${suffix}${COLORS.reset}\n`);
25
+ /** Console preview limits for a tool result (the full text goes to the model). */
26
+ const RESULT_PREVIEW_MAX_LINES = 20;
27
+ const RESULT_PREVIEW_MAX_CHARS = 1200;
28
+ /**
29
+ * Print a tool result preview. Lines are kept whole and both limits are
30
+ * applied in one pass, so the "N more lines" count always matches what was
31
+ * actually withheld.
32
+ */
33
+ export function printToolResult(_name, result, isError = false) {
34
+ const text = toolResultText(result).replace(/\s+$/, "");
35
+ const lines = text === "" ? [] : text.split("\n");
36
+ const kept = [];
37
+ let chars = 0;
38
+ for (const line of lines) {
39
+ if (kept.length >= RESULT_PREVIEW_MAX_LINES)
40
+ break;
41
+ if (chars + line.length > RESULT_PREVIEW_MAX_CHARS)
42
+ break;
43
+ kept.push(line);
44
+ chars += line.length + 1;
45
+ }
46
+ // A single very long first line still deserves a preview.
47
+ if (kept.length === 0 && lines.length > 0) {
48
+ kept.push(truncateDisplay(lines[0], RESULT_PREVIEW_MAX_CHARS));
49
+ }
50
+ const hidden = lines.length - kept.length;
51
+ const suffix = hidden > 0 ? ` (${hidden} more line${hidden === 1 ? "" : "s"})` : "";
52
+ const marker = isError ? `${COLORS.red} ✗${COLORS.reset}` : `${COLORS.green} ✓${COLORS.reset}`;
53
+ const body = kept.join(`\n${COLORS.dim} `);
54
+ console.log(`${marker} ${COLORS.dim}${body}${suffix}${COLORS.reset}\n`);
34
55
  }
35
- export function printDone(steps, usage, contextWindow) {
36
- const input = usage.inputTokens ?? 0;
37
- const output = usage.outputTokens ?? 0;
56
+ export function printDone(steps, usage, contextWindow, contextTokens) {
57
+ const input = usage?.inputTokens ?? 0;
58
+ const output = usage?.outputTokens ?? 0;
38
59
  const total = input + output;
60
+ const occupied = contextTokens !== undefined ? contextTokens : input;
39
61
  let contextInfo = "";
40
- if (input > 0 && contextWindow && contextWindow > 0) {
41
- const pct = Math.round((input / contextWindow) * 100);
62
+ if (occupied > 0 && contextWindow && contextWindow > 0) {
63
+ const pct = Math.round((occupied / contextWindow) * 100);
42
64
  const bar = renderBar(pct);
43
65
  contextInfo = ` | Context: ${bar} ${pct}%`;
44
66
  }
@@ -46,20 +68,9 @@ export function printDone(steps, usage, contextWindow) {
46
68
  }
47
69
  function renderBar(pct) {
48
70
  const width = 10;
49
- const filled = Math.round((pct / 100) * width);
71
+ const safePct = Math.max(0, Math.min(100, pct));
72
+ const filled = Math.round((safePct / 100) * width);
50
73
  const empty = width - filled;
51
74
  const color = pct >= 80 ? "\x1b[31m" : pct >= 50 ? "\x1b[33m" : "\x1b[32m";
52
75
  return `${color}${"█".repeat(filled)}${"░".repeat(empty)}\x1b[0m\x1b[90m`;
53
76
  }
54
- function formatArgs(args) {
55
- if (!args || typeof args !== "object")
56
- return "";
57
- const entries = Object.entries(args);
58
- if (entries.length === 0)
59
- return "";
60
- const parts = entries.map(([k, v]) => {
61
- const val = typeof v === "string" ? (v.length > 60 ? v.slice(0, 60) + "..." : v) : JSON.stringify(v);
62
- return `${k}=${val}`;
63
- });
64
- return parts.join(" ");
65
- }
@@ -17,7 +17,7 @@ const PREVIEW_LINES = 3;
17
17
  export function processPastedInput(text) {
18
18
  const lineCount = (text.match(/\n/g)?.length ?? 0) + 1;
19
19
  if (lineCount < PASTE_LINE_THRESHOLD && text.length <= PASTE_CHAR_THRESHOLD) {
20
- return { fullText: text, isLargePaste: false };
20
+ return { fullText: text, isLargePaste: false, lineCount };
21
21
  }
22
22
  const lines = text.split("\n");
23
23
  const preview = lines.slice(0, PREVIEW_LINES).join("\n");
@@ -25,6 +25,7 @@ export function processPastedInput(text) {
25
25
  return {
26
26
  fullText: text,
27
27
  isLargePaste: true,
28
+ lineCount,
28
29
  summary: remaining > 0
29
30
  ? `${preview}\n\x1b[90m ... (${remaining} more lines, ~${text.length} chars total)\x1b[0m`
30
31
  : `\x1b[90m[Pasted ${text.length} chars]\x1b[0m`,
@@ -36,6 +37,5 @@ export function processPastedInput(text) {
36
37
  export function printPasteFeedback(result) {
37
38
  if (!result.isLargePaste)
38
39
  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`);
40
+ console.log(`\x1b[90m 📋 Pasted ~${result.lineCount} lines (${result.fullText.length} chars)\x1b[0m`);
41
41
  }
@@ -0,0 +1,43 @@
1
+ import { takeScopeFlags } from "./memory.js";
2
+ import { getPermissionSnapshot, parsePermissionMode, permissionModeLabel, setPermissionMode, } from "./config.js";
3
+ import { getPermissionOverride, setPermissionOverride } from "./confirm.js";
4
+ export const PERMISSION_CLI_USAGE = [
5
+ "Usage: min-agent permission [ask|accept-edits|allow-all] [--project|--global]",
6
+ " min-agent --permission ask|accept-edits|allow-all [--project|--global]",
7
+ ].join("\n");
8
+ function sourceLabel(source) {
9
+ return source === "cli" ? "this run" : source === "project" ? "project" : source === "global" ? "global" : "default";
10
+ }
11
+ function scopeLabel(scope) {
12
+ return scope === "project" ? "project" : "global";
13
+ }
14
+ export function runPermissionCli(input) {
15
+ const { scope: posScope, rest } = takeScopeFlags(input.positionals);
16
+ const scope = posScope ?? input.scope ?? "global";
17
+ if (rest[0] === "--help" || rest[0] === "-h") {
18
+ return { ok: true, lines: [PERMISSION_CLI_USAGE] };
19
+ }
20
+ if (rest.length === 1) {
21
+ const parsed = parsePermissionMode(rest[0]);
22
+ if (!parsed)
23
+ return { ok: false, lines: [PERMISSION_CLI_USAGE] };
24
+ setPermissionMode(parsed, scope);
25
+ setPermissionOverride(parsed);
26
+ return { ok: true, lines: [`✓ Permission set to ${permissionModeLabel(parsed)} (${scopeLabel(scope)})`] };
27
+ }
28
+ if (rest.length > 1)
29
+ return { ok: false, lines: [PERMISSION_CLI_USAGE] };
30
+ if (input.flagMode) {
31
+ setPermissionMode(input.flagMode, scope);
32
+ setPermissionOverride(input.flagMode);
33
+ return { ok: true, lines: [`✓ Permission set to ${permissionModeLabel(input.flagMode)} (${scopeLabel(scope)})`] };
34
+ }
35
+ const snap = getPermissionSnapshot();
36
+ const override = getPermissionOverride();
37
+ const mode = override ?? snap.permission;
38
+ const source = override ? "cli" : snap.source;
39
+ return {
40
+ ok: true,
41
+ lines: [`Current permission: ${permissionModeLabel(mode)} (${sourceLabel(source)})`, PERMISSION_CLI_USAGE],
42
+ };
43
+ }
package/dist/plugins.js CHANGED
@@ -1,15 +1,50 @@
1
1
  import { tool, jsonSchema } from "ai";
2
- import { existsSync, readdirSync } from "fs";
2
+ import { existsSync, readdirSync, statSync } from "fs";
3
3
  import { pathToFileURL } from "url";
4
4
  import path from "path";
5
5
  import { getConfigDir } from "./config.js";
6
- const PLUGIN_DIRS = [
7
- path.join(process.cwd(), ".min-agent", "tools"),
8
- path.join(getConfigDir(), "tools"),
9
- ];
10
- export async function loadPluginTools() {
6
+ import { confirm, isAutoApprove } from "./confirm.js";
7
+ const PLUGIN_DIRS = [path.join(process.cwd(), ".min-agent", "tools"), path.join(getConfigDir(), "tools")];
8
+ let cachedPlugins = null;
9
+ let pluginReadOnlyIds = new Set();
10
+ export function pluginNeedsConfirm(def) {
11
+ if (def.readOnly === true)
12
+ return false;
13
+ if (def.dangerous === false)
14
+ return false;
15
+ return true;
16
+ }
17
+ export function getPluginReadOnlyIds() {
18
+ return pluginReadOnlyIds;
19
+ }
20
+ function pluginSignature(dirs) {
21
+ return dirs
22
+ .map((dir) => {
23
+ if (!existsSync(dir))
24
+ return `${dir}:missing`;
25
+ const files = readdirSync(dir)
26
+ .filter((f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".mjs"))
27
+ .sort();
28
+ const mtimes = files.map((f) => statSync(path.join(dir, f)).mtimeMs).join(",");
29
+ return `${dir}:${files.join(",")}:${mtimes}`;
30
+ })
31
+ .join("|");
32
+ }
33
+ export async function loadPluginTools(dirs) {
34
+ const pluginDirs = dirs ?? PLUGIN_DIRS;
35
+ const signature = pluginSignature(pluginDirs);
36
+ if (cachedPlugins && cachedPlugins.signature === signature) {
37
+ pluginReadOnlyIds = cachedPlugins.readOnlyIds;
38
+ return cachedPlugins.tools;
39
+ }
40
+ const tools = await loadPluginToolsUncached(pluginDirs);
41
+ cachedPlugins = { signature, tools, readOnlyIds: pluginReadOnlyIds };
42
+ return tools;
43
+ }
44
+ async function loadPluginToolsUncached(pluginDirs) {
11
45
  const tools = {};
12
- for (const dir of PLUGIN_DIRS) {
46
+ const readOnlyIds = new Set();
47
+ for (const dir of pluginDirs) {
13
48
  if (!existsSync(dir))
14
49
  continue;
15
50
  const files = readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".mjs"));
@@ -22,12 +57,19 @@ export async function loadPluginTools() {
22
57
  if (!isPluginTool(def))
23
58
  continue;
24
59
  const toolId = exportName === "default" ? namespace : `${namespace}_${exportName}`;
60
+ if (tools[toolId]) {
61
+ console.warn(`\x1b[33m ⚠ Plugin tool "${toolId}" from ${filePath} conflicts with an existing tool; skipping\x1b[0m`);
62
+ continue;
63
+ }
25
64
  const properties = {};
26
65
  const required = [];
27
66
  for (const [key, param] of Object.entries(def.parameters)) {
28
- properties[key] = { type: param.type, description: param.description };
67
+ const type = narrowJsonType(param.type);
68
+ properties[key] = { type, ...(param.description ? { description: param.description } : {}) };
29
69
  required.push(key);
30
70
  }
71
+ if (def.readOnly === true)
72
+ readOnlyIds.add(toolId);
31
73
  tools[toolId] = tool({
32
74
  description: def.description,
33
75
  inputSchema: jsonSchema({
@@ -36,28 +78,51 @@ export async function loadPluginTools() {
36
78
  required,
37
79
  }),
38
80
  execute: async (args) => {
81
+ if (pluginNeedsConfirm(def) && !isAutoApprove()) {
82
+ const preview = summarizePluginArgs(args);
83
+ const approved = await confirm(`运行自定义工具 ${toolId}${preview ? `\n${preview}` : ""}`);
84
+ if (!approved)
85
+ return "自定义工具已被拒绝。";
86
+ }
39
87
  try {
40
88
  const result = await def.execute(args);
41
- return typeof result === "string" ? result : JSON.stringify(result);
89
+ return typeof result === "string" ? result : (JSON.stringify(result) ?? "undefined");
42
90
  }
43
91
  catch (err) {
44
- return `Plugin error: ${err.message}`;
92
+ return `Plugin error: ${String(err instanceof Error ? err.message : err)}`;
45
93
  }
46
94
  },
47
95
  });
48
96
  }
49
97
  }
50
98
  catch (err) {
51
- console.error(`\x1b[90m Plugin "${file}" failed to load: ${err.message}\x1b[0m`);
99
+ console.error(`\x1b[90m Plugin "${file}" failed to load: ${String(err instanceof Error ? err.message : err)}\x1b[0m`);
52
100
  }
53
101
  }
54
102
  }
103
+ pluginReadOnlyIds = readOnlyIds;
55
104
  const count = Object.keys(tools).length;
56
105
  if (count > 0) {
57
106
  console.log(`\x1b[90m Plugins loaded: ${count} tool(s)\x1b[0m`);
58
107
  }
59
108
  return tools;
60
109
  }
110
+ function summarizePluginArgs(args) {
111
+ try {
112
+ const text = JSON.stringify(args);
113
+ if (!text || text === "{}")
114
+ return "";
115
+ return text.length > 400 ? `${text.slice(0, 400)}…` : text;
116
+ }
117
+ catch {
118
+ return "";
119
+ }
120
+ }
121
+ function narrowJsonType(type) {
122
+ if (type === "number" || type === "boolean" || type === "object" || type === "array")
123
+ return type;
124
+ return "string";
125
+ }
61
126
  function isPluginTool(value) {
62
127
  if (!value || typeof value !== "object")
63
128
  return false;