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.
- package/README.md +242 -31
- package/dist/agent.js +1233 -485
- package/dist/assistant-stream.js +11 -7
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +7 -632
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +35 -17
- package/dist/compaction.js +457 -169
- package/dist/config.js +298 -38
- package/dist/confirm.js +105 -9
- package/dist/context-window.js +156 -75
- package/dist/doom-loop.js +268 -26
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/http.js +119 -0
- package/dist/instructions.js +72 -33
- package/dist/logger.js +95 -0
- package/dist/markdown.js +35 -50
- package/dist/mcp.js +847 -102
- package/dist/memory.js +128 -45
- package/dist/output.js +42 -31
- package/dist/paste-handler.js +3 -3
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +76 -11
- package/dist/pricing.js +119 -0
- package/dist/provider.js +34 -15
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +74 -635
- package/dist/sessions.js +197 -15
- package/dist/skills.js +531 -77
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +9 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +178 -0
- package/dist/tool-output.js +53 -46
- package/dist/tools/apply_patch.js +265 -0
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +186 -71
- package/dist/tools/code_search.js +13 -6
- package/dist/tools/edit.js +26 -9
- package/dist/tools/explore.js +144 -16
- package/dist/tools/glob.js +7 -3
- package/dist/tools/grep.js +153 -14
- package/dist/tools/index.js +9 -24
- package/dist/tools/question.js +31 -30
- package/dist/tools/read.js +77 -15
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +100 -33
- package/dist/tools/todo.js +178 -67
- package/dist/tools/web_fetch.js +158 -46
- package/dist/tools/web_search.js +217 -29
- package/dist/tools/write.js +34 -11
- package/dist/tui/App.js +89 -6
- package/dist/tui/ConfirmBar.js +57 -4
- package/dist/tui/InputBar.js +504 -44
- package/dist/tui/MessageList.js +674 -20
- package/dist/tui/ModelPicker.js +113 -0
- package/dist/tui/QuestionBar.js +136 -0
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +14 -12
- package/dist/tui/agent-runner.js +223 -0
- package/dist/tui/caret-pos.js +177 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +49 -0
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +189 -31
- package/dist/tui/input-history.js +125 -0
- package/dist/tui/layout.js +88 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +226 -0
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +106 -0
- package/dist/tui/slash-handler.js +545 -0
- package/dist/tui/text-width.js +113 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +29 -0
- package/dist/tui-chat.js +346 -330
- package/dist/updater.js +116 -0
- package/dist/xml-search.js +194 -0
- package/docs/API.md +410 -32
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
- package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +12 -8
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
package/dist/memory.js
CHANGED
|
@@ -1,74 +1,138 @@
|
|
|
1
|
-
import { readFileSync,
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
65
|
+
atomicWriteFileSync(file, JSON.stringify(memories, null, 2));
|
|
23
66
|
}
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
52
|
-
export function getMemorySystemPrompt() {
|
|
53
|
-
const memories = loadMemories();
|
|
95
|
+
function formatMemoryBlock(memories, heading) {
|
|
54
96
|
if (memories.length === 0)
|
|
55
97
|
return "";
|
|
56
|
-
const
|
|
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 ` ${
|
|
106
|
+
return ` ${num}. ${m.content}${tags}`;
|
|
59
107
|
});
|
|
60
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
|
86
|
-
|
|
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
|
|
100
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
123
|
-
return
|
|
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
|
|
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 =
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
37
|
-
const output = usage
|
|
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 (
|
|
41
|
-
const pct = Math.round((
|
|
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
|
|
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
|
-
}
|
package/dist/paste-handler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export
|
|
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
|
-
|
|
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
|
-
|
|
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;
|