min-agent 0.3.0 → 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 +111 -28
- package/dist/agent.js +1119 -256
- 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 +5 -888
- package/dist/code-mode.js +32 -14
- package/dist/compaction.js +347 -160
- package/dist/config.js +119 -10
- package/dist/confirm.js +56 -9
- package/dist/context-window.js +107 -39
- package/dist/doom-loop.js +264 -29
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/instructions.js +21 -0
- package/dist/logger.js +33 -4
- package/dist/markdown.js +37 -11
- package/dist/mcp.js +328 -30
- package/dist/memory.js +97 -56
- package/dist/output.js +7 -5
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +46 -8
- package/dist/pricing.js +4 -4
- package/dist/provider.js +23 -6
- 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 +33 -883
- package/dist/sessions.js +53 -9
- package/dist/skills.js +82 -18
- package/dist/title-gen.js +8 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +5 -0
- package/dist/tool-output.js +1 -3
- package/dist/tools/apply_patch.js +85 -11
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +2 -2
- package/dist/tools/bash.js +57 -19
- package/dist/tools/code_search.js +7 -1
- package/dist/tools/edit.js +11 -10
- package/dist/tools/explore.js +74 -14
- package/dist/tools/glob.js +4 -0
- package/dist/tools/grep.js +17 -10
- package/dist/tools/index.js +6 -21
- package/dist/tools/question.js +28 -9
- package/dist/tools/read.js +6 -4
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +84 -30
- package/dist/tools/todo.js +120 -19
- package/dist/tools/web_fetch.js +11 -3
- package/dist/tools/web_search.js +66 -556
- package/dist/tools/write.js +23 -6
- package/dist/tui/App.js +63 -14
- package/dist/tui/ConfirmBar.js +45 -13
- package/dist/tui/InputBar.js +150 -35
- package/dist/tui/MessageList.js +266 -125
- package/dist/tui/ModelPicker.js +8 -3
- package/dist/tui/QuestionBar.js +51 -19
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +8 -14
- package/dist/tui/agent-runner.js +142 -22
- package/dist/tui/caret-pos.js +48 -5
- package/dist/tui/caret.js +1 -1
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/drag-state.js +8 -3
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +42 -13
- package/dist/tui/input-history.js +92 -11
- package/dist/tui/layout.js +75 -4
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +113 -21
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +22 -6
- package/dist/tui/slash-handler.js +233 -58
- package/dist/tui/text-width.js +38 -16
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +1 -1
- package/dist/tui/use-sgr-mouse.js +3 -1
- package/dist/tui-chat.js +276 -40
- package/dist/updater.js +88 -29
- package/dist/xml-search.js +194 -0
- package/docs/API.md +257 -25
- 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-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 +6 -1
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
package/dist/memory.js
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
|
-
import { readFileSync,
|
|
1
|
+
import { readFileSync, mkdirSync, existsSync, renameSync } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { getConfigDir, getProjectConfigDir } from "./config.js";
|
|
4
|
+
import { atomicWriteFileSync } from "./tools/atomic-file.js";
|
|
5
5
|
import { tool, jsonSchema } from "ai";
|
|
6
|
-
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");
|
|
7
32
|
return path.join(getConfigDir(), "memory.json");
|
|
8
33
|
}
|
|
9
34
|
/** Preserve corrupt data instead of letting it be overwritten and lost forever. */
|
|
@@ -15,8 +40,8 @@ function backupCorrupt(file) {
|
|
|
15
40
|
// nothing else we can do with an unreadable file
|
|
16
41
|
}
|
|
17
42
|
}
|
|
18
|
-
export function loadMemories() {
|
|
19
|
-
const file = getMemoryFile();
|
|
43
|
+
export function loadMemories(scope = "global") {
|
|
44
|
+
const file = getMemoryFile(scope);
|
|
20
45
|
if (!existsSync(file))
|
|
21
46
|
return [];
|
|
22
47
|
let data;
|
|
@@ -34,55 +59,40 @@ export function loadMemories() {
|
|
|
34
59
|
return data.filter((m) => typeof m === "object" && m !== null && typeof m.content === "string");
|
|
35
60
|
}
|
|
36
61
|
/** Atomic replace via temp file + rename, so a crash never leaves a truncated memory.json. */
|
|
37
|
-
function saveMemories(memories) {
|
|
38
|
-
const file = getMemoryFile();
|
|
62
|
+
function saveMemories(memories, scope) {
|
|
63
|
+
const file = getMemoryFile(scope);
|
|
39
64
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
writeFileSync(tmp, JSON.stringify(memories, null, 2), "utf-8");
|
|
43
|
-
renameSync(tmp, file);
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
try {
|
|
47
|
-
if (existsSync(tmp))
|
|
48
|
-
unlinkSync(tmp);
|
|
49
|
-
}
|
|
50
|
-
catch { }
|
|
51
|
-
throw err;
|
|
52
|
-
}
|
|
65
|
+
atomicWriteFileSync(file, JSON.stringify(memories, null, 2));
|
|
53
66
|
}
|
|
54
67
|
// read-modify-write runs fully synchronously, so concurrent tool calls in
|
|
55
68
|
// this process cannot interleave between load and save.
|
|
56
|
-
export function addMemory(content, tags = []) {
|
|
57
|
-
const memories = loadMemories();
|
|
69
|
+
export function addMemory(content, tags = [], scope = "global") {
|
|
70
|
+
const memories = loadMemories(scope);
|
|
58
71
|
const memory = {
|
|
59
72
|
content,
|
|
60
73
|
tags,
|
|
61
74
|
created: new Date().toISOString(),
|
|
62
75
|
};
|
|
63
76
|
memories.push(memory);
|
|
64
|
-
saveMemories(memories);
|
|
77
|
+
saveMemories(memories, scope);
|
|
65
78
|
return memory;
|
|
66
79
|
}
|
|
67
|
-
export function deleteMemory(index) {
|
|
68
|
-
const memories = loadMemories();
|
|
80
|
+
export function deleteMemory(index, scope = "global") {
|
|
81
|
+
const memories = loadMemories(scope);
|
|
69
82
|
if (index < 0 || index >= memories.length)
|
|
70
83
|
return false;
|
|
71
84
|
memories.splice(index, 1);
|
|
72
|
-
saveMemories(memories);
|
|
85
|
+
saveMemories(memories, scope);
|
|
73
86
|
return true;
|
|
74
87
|
}
|
|
75
|
-
export function searchMemories(query) {
|
|
76
|
-
const memories = loadMemories();
|
|
88
|
+
export function searchMemories(query, scope = "global") {
|
|
89
|
+
const memories = loadMemories(scope);
|
|
77
90
|
const lower = query.toLowerCase();
|
|
78
91
|
return memories
|
|
79
92
|
.map((m, i) => ({ ...m, index: i }))
|
|
80
|
-
.filter((m) => m.content.toLowerCase().includes(lower) ||
|
|
81
|
-
m.tags.some((t) => t.toLowerCase().includes(lower)));
|
|
93
|
+
.filter((m) => m.content.toLowerCase().includes(lower) || m.tags.some((t) => t.toLowerCase().includes(lower)));
|
|
82
94
|
}
|
|
83
|
-
|
|
84
|
-
export function getMemorySystemPrompt() {
|
|
85
|
-
const memories = loadMemories();
|
|
95
|
+
function formatMemoryBlock(memories, heading) {
|
|
86
96
|
if (memories.length === 0)
|
|
87
97
|
return "";
|
|
88
98
|
const MAX_MEMORIES = 30;
|
|
@@ -98,19 +108,31 @@ export function getMemorySystemPrompt() {
|
|
|
98
108
|
const capped = memories.length > MAX_MEMORIES
|
|
99
109
|
? `\n(${memories.length - MAX_MEMORIES} older memories not shown — use memory_search to find them.)`
|
|
100
110
|
: "";
|
|
101
|
-
return [
|
|
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 = [
|
|
102
120
|
"## Memories",
|
|
103
121
|
"The following are things you have remembered from previous conversations. Use them to provide better, personalized responses.",
|
|
104
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.',
|
|
105
124
|
"",
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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");
|
|
109
131
|
}
|
|
110
132
|
/** Create the memory tools for the agent */
|
|
111
133
|
export function getMemoryTools() {
|
|
112
134
|
const memorySave = tool({
|
|
113
|
-
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.",
|
|
114
136
|
inputSchema: jsonSchema({
|
|
115
137
|
type: "object",
|
|
116
138
|
properties: {
|
|
@@ -120,12 +142,18 @@ export function getMemoryTools() {
|
|
|
120
142
|
items: { type: "string" },
|
|
121
143
|
description: "Optional tags for categorization (e.g. 'preference', 'project', 'convention')",
|
|
122
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
|
+
},
|
|
123
150
|
},
|
|
124
151
|
required: ["content"],
|
|
125
152
|
}),
|
|
126
|
-
execute: async ({ content, tags }) => {
|
|
127
|
-
const
|
|
128
|
-
|
|
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"})`;
|
|
129
157
|
},
|
|
130
158
|
});
|
|
131
159
|
const memorySearch = tool({
|
|
@@ -134,35 +162,48 @@ export function getMemoryTools() {
|
|
|
134
162
|
type: "object",
|
|
135
163
|
properties: {
|
|
136
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
|
+
},
|
|
137
170
|
},
|
|
138
171
|
required: ["query"],
|
|
139
172
|
}),
|
|
140
|
-
execute: async ({ query }) => {
|
|
141
|
-
const
|
|
142
|
-
|
|
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)
|
|
143
183
|
return `No memories found matching "${query}"`;
|
|
144
|
-
return
|
|
145
|
-
.map((m) => {
|
|
146
|
-
const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
|
|
147
|
-
return `#${m.index + 1}: ${m.content}${tags} (${m.created.split("T")[0]})`;
|
|
148
|
-
})
|
|
149
|
-
.join("\n");
|
|
184
|
+
return lines.join("\n");
|
|
150
185
|
},
|
|
151
186
|
});
|
|
152
187
|
const memoryDelete = tool({
|
|
153
|
-
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.",
|
|
154
189
|
inputSchema: jsonSchema({
|
|
155
190
|
type: "object",
|
|
156
191
|
properties: {
|
|
157
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
|
+
},
|
|
158
198
|
},
|
|
159
199
|
required: ["index"],
|
|
160
200
|
}),
|
|
161
|
-
execute: async ({ index }) => {
|
|
162
|
-
const
|
|
201
|
+
execute: async ({ index, scope }) => {
|
|
202
|
+
const resolved = scope === "global" || scope === "project" ? scope : defaultMemoryScope();
|
|
203
|
+
const success = deleteMemory(index - 1, resolved);
|
|
163
204
|
if (success)
|
|
164
|
-
return
|
|
165
|
-
return
|
|
205
|
+
return `${resolved} memory #${index} deleted.`;
|
|
206
|
+
return `${resolved} memory #${index} not found.`;
|
|
166
207
|
},
|
|
167
208
|
});
|
|
168
209
|
return {
|
package/dist/output.js
CHANGED
|
@@ -30,7 +30,7 @@ const RESULT_PREVIEW_MAX_CHARS = 1200;
|
|
|
30
30
|
* applied in one pass, so the "N more lines" count always matches what was
|
|
31
31
|
* actually withheld.
|
|
32
32
|
*/
|
|
33
|
-
export function printToolResult(
|
|
33
|
+
export function printToolResult(_name, result, isError = false) {
|
|
34
34
|
const text = toolResultText(result).replace(/\s+$/, "");
|
|
35
35
|
const lines = text === "" ? [] : text.split("\n");
|
|
36
36
|
const kept = [];
|
|
@@ -53,13 +53,14 @@ export function printToolResult(name, result, isError = false) {
|
|
|
53
53
|
const body = kept.join(`\n${COLORS.dim} `);
|
|
54
54
|
console.log(`${marker} ${COLORS.dim}${body}${suffix}${COLORS.reset}\n`);
|
|
55
55
|
}
|
|
56
|
-
export function printDone(steps, usage, contextWindow) {
|
|
56
|
+
export function printDone(steps, usage, contextWindow, contextTokens) {
|
|
57
57
|
const input = usage?.inputTokens ?? 0;
|
|
58
58
|
const output = usage?.outputTokens ?? 0;
|
|
59
59
|
const total = input + output;
|
|
60
|
+
const occupied = contextTokens !== undefined ? contextTokens : input;
|
|
60
61
|
let contextInfo = "";
|
|
61
|
-
if (
|
|
62
|
-
const pct = Math.round((
|
|
62
|
+
if (occupied > 0 && contextWindow && contextWindow > 0) {
|
|
63
|
+
const pct = Math.round((occupied / contextWindow) * 100);
|
|
63
64
|
const bar = renderBar(pct);
|
|
64
65
|
contextInfo = ` | Context: ${bar} ${pct}%`;
|
|
65
66
|
}
|
|
@@ -67,7 +68,8 @@ export function printDone(steps, usage, contextWindow) {
|
|
|
67
68
|
}
|
|
68
69
|
function renderBar(pct) {
|
|
69
70
|
const width = 10;
|
|
70
|
-
const
|
|
71
|
+
const safePct = Math.max(0, Math.min(100, pct));
|
|
72
|
+
const filled = Math.round((safePct / 100) * width);
|
|
71
73
|
const empty = width - filled;
|
|
72
74
|
const color = pct >= 80 ? "\x1b[31m" : pct >= 50 ? "\x1b[33m" : "\x1b[32m";
|
|
73
75
|
return `${color}${"█".repeat(filled)}${"░".repeat(empty)}\x1b[0m\x1b[90m`;
|
|
@@ -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
|
@@ -3,11 +3,20 @@ 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
|
-
path.join(getConfigDir(), "tools"),
|
|
9
|
-
];
|
|
6
|
+
import { confirm, isAutoApprove } from "./confirm.js";
|
|
7
|
+
const PLUGIN_DIRS = [path.join(process.cwd(), ".min-agent", "tools"), path.join(getConfigDir(), "tools")];
|
|
10
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
|
+
}
|
|
11
20
|
function pluginSignature(dirs) {
|
|
12
21
|
return dirs
|
|
13
22
|
.map((dir) => {
|
|
@@ -24,14 +33,17 @@ function pluginSignature(dirs) {
|
|
|
24
33
|
export async function loadPluginTools(dirs) {
|
|
25
34
|
const pluginDirs = dirs ?? PLUGIN_DIRS;
|
|
26
35
|
const signature = pluginSignature(pluginDirs);
|
|
27
|
-
if (cachedPlugins && cachedPlugins.signature === signature)
|
|
36
|
+
if (cachedPlugins && cachedPlugins.signature === signature) {
|
|
37
|
+
pluginReadOnlyIds = cachedPlugins.readOnlyIds;
|
|
28
38
|
return cachedPlugins.tools;
|
|
39
|
+
}
|
|
29
40
|
const tools = await loadPluginToolsUncached(pluginDirs);
|
|
30
|
-
cachedPlugins = { signature, tools };
|
|
41
|
+
cachedPlugins = { signature, tools, readOnlyIds: pluginReadOnlyIds };
|
|
31
42
|
return tools;
|
|
32
43
|
}
|
|
33
44
|
async function loadPluginToolsUncached(pluginDirs) {
|
|
34
45
|
const tools = {};
|
|
46
|
+
const readOnlyIds = new Set();
|
|
35
47
|
for (const dir of pluginDirs) {
|
|
36
48
|
if (!existsSync(dir))
|
|
37
49
|
continue;
|
|
@@ -52,9 +64,12 @@ async function loadPluginToolsUncached(pluginDirs) {
|
|
|
52
64
|
const properties = {};
|
|
53
65
|
const required = [];
|
|
54
66
|
for (const [key, param] of Object.entries(def.parameters)) {
|
|
55
|
-
|
|
67
|
+
const type = narrowJsonType(param.type);
|
|
68
|
+
properties[key] = { type, ...(param.description ? { description: param.description } : {}) };
|
|
56
69
|
required.push(key);
|
|
57
70
|
}
|
|
71
|
+
if (def.readOnly === true)
|
|
72
|
+
readOnlyIds.add(toolId);
|
|
58
73
|
tools[toolId] = tool({
|
|
59
74
|
description: def.description,
|
|
60
75
|
inputSchema: jsonSchema({
|
|
@@ -63,9 +78,15 @@ async function loadPluginToolsUncached(pluginDirs) {
|
|
|
63
78
|
required,
|
|
64
79
|
}),
|
|
65
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
|
+
}
|
|
66
87
|
try {
|
|
67
88
|
const result = await def.execute(args);
|
|
68
|
-
return typeof result === "string" ? result : JSON.stringify(result) ?? "undefined";
|
|
89
|
+
return typeof result === "string" ? result : (JSON.stringify(result) ?? "undefined");
|
|
69
90
|
}
|
|
70
91
|
catch (err) {
|
|
71
92
|
return `Plugin error: ${String(err instanceof Error ? err.message : err)}`;
|
|
@@ -79,12 +100,29 @@ async function loadPluginToolsUncached(pluginDirs) {
|
|
|
79
100
|
}
|
|
80
101
|
}
|
|
81
102
|
}
|
|
103
|
+
pluginReadOnlyIds = readOnlyIds;
|
|
82
104
|
const count = Object.keys(tools).length;
|
|
83
105
|
if (count > 0) {
|
|
84
106
|
console.log(`\x1b[90m Plugins loaded: ${count} tool(s)\x1b[0m`);
|
|
85
107
|
}
|
|
86
108
|
return tools;
|
|
87
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
|
+
}
|
|
88
126
|
function isPluginTool(value) {
|
|
89
127
|
if (!value || typeof value !== "object")
|
|
90
128
|
return false;
|
package/dist/pricing.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { readFileSync,
|
|
1
|
+
import { readFileSync, mkdirSync, existsSync } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { atomicWriteFileSync } from "./tools/atomic-file.js";
|
|
3
4
|
import { getConfigDir, loadConfig } from "./config.js";
|
|
4
5
|
const CACHE_TTL = 7 * 24 * 60 * 60 * 1000;
|
|
5
6
|
const memoryCache = new Map();
|
|
@@ -23,7 +24,7 @@ function loadDiskCache() {
|
|
|
23
24
|
function saveCache(cache) {
|
|
24
25
|
const file = cacheFile();
|
|
25
26
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
26
|
-
|
|
27
|
+
atomicWriteFileSync(file, JSON.stringify(cache));
|
|
27
28
|
}
|
|
28
29
|
function getCached(modelId) {
|
|
29
30
|
const memory = memoryCache.get(modelId);
|
|
@@ -108,8 +109,7 @@ export async function getModelPrice(modelId) {
|
|
|
108
109
|
export function estimateCost(usage, price) {
|
|
109
110
|
if (!price)
|
|
110
111
|
return null;
|
|
111
|
-
const cost = (usage.inputTokens / 1_000_000) * price.inputPerMillion +
|
|
112
|
-
(usage.outputTokens / 1_000_000) * price.outputPerMillion;
|
|
112
|
+
const cost = (usage.inputTokens / 1_000_000) * price.inputPerMillion + (usage.outputTokens / 1_000_000) * price.outputPerMillion;
|
|
113
113
|
return cost > 0 ? cost : null;
|
|
114
114
|
}
|
|
115
115
|
export function formatCost(cost) {
|
package/dist/provider.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
2
|
-
import {
|
|
2
|
+
import { getEffectiveConfig, getActiveProvider, normalizeOllamaBaseURL } from "./config.js";
|
|
3
|
+
import { createTimeoutFetch, DEFAULT_FIRST_BYTE_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, } from "./fetch-timeout.js";
|
|
4
|
+
function positiveMs(raw, fallback) {
|
|
5
|
+
const value = typeof raw === "string" ? Number(raw) : raw;
|
|
6
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
7
|
+
return fallback;
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
/** Timeout/trace-aware fetch shared by every provider client. */
|
|
11
|
+
export function modelFetch() {
|
|
12
|
+
const cfg = getEffectiveConfig();
|
|
13
|
+
return createTimeoutFetch({
|
|
14
|
+
firstByteTimeoutMs: positiveMs(process.env.MIN_AGENT_REQUEST_TIMEOUT_MS ?? cfg.agent?.requestTimeoutMs, DEFAULT_FIRST_BYTE_TIMEOUT_MS),
|
|
15
|
+
idleTimeoutMs: positiveMs(process.env.MIN_AGENT_STREAM_IDLE_TIMEOUT_MS ?? cfg.agent?.streamIdleTimeoutMs, DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
16
|
+
trace: process.env.MIN_AGENT_TRACE === "1" || process.env.MIN_AGENT_TRACE === "true",
|
|
17
|
+
});
|
|
18
|
+
}
|
|
3
19
|
export function resolveModelForProvider(provider, modelId) {
|
|
4
20
|
if (!provider.baseURL || !provider.apiKey) {
|
|
5
21
|
throw new Error("Not configured. Run: min-agent setup");
|
|
@@ -8,15 +24,17 @@ export function resolveModelForProvider(provider, modelId) {
|
|
|
8
24
|
if (!id) {
|
|
9
25
|
throw new Error("No model specified. Run: min-agent setup");
|
|
10
26
|
}
|
|
27
|
+
const fetchImpl = modelFetch();
|
|
11
28
|
switch (provider.type ?? "openai-compatible") {
|
|
12
29
|
case "openai": {
|
|
13
|
-
const client = createOpenAI({ apiKey: provider.apiKey });
|
|
30
|
+
const client = createOpenAI({ apiKey: provider.apiKey, fetch: fetchImpl });
|
|
14
31
|
return client.chat(id);
|
|
15
32
|
}
|
|
16
33
|
case "ollama": {
|
|
17
34
|
const client = createOpenAI({
|
|
18
35
|
baseURL: normalizeOllamaBaseURL(provider.baseURL),
|
|
19
36
|
apiKey: provider.apiKey || "ollama",
|
|
37
|
+
fetch: fetchImpl,
|
|
20
38
|
});
|
|
21
39
|
return client.chat(id);
|
|
22
40
|
}
|
|
@@ -24,6 +42,7 @@ export function resolveModelForProvider(provider, modelId) {
|
|
|
24
42
|
const client = createOpenAI({
|
|
25
43
|
baseURL: provider.baseURL,
|
|
26
44
|
apiKey: provider.apiKey,
|
|
45
|
+
fetch: fetchImpl,
|
|
27
46
|
});
|
|
28
47
|
return client.chat(id);
|
|
29
48
|
}
|
|
@@ -32,10 +51,8 @@ export function resolveModelForProvider(provider, modelId) {
|
|
|
32
51
|
}
|
|
33
52
|
}
|
|
34
53
|
export function resolveModel(modelId, providerName) {
|
|
35
|
-
const config =
|
|
36
|
-
const provider = providerName
|
|
37
|
-
? config.providers?.find((p) => p.name === providerName)
|
|
38
|
-
: getActiveProvider(config);
|
|
54
|
+
const config = getEffectiveConfig();
|
|
55
|
+
const provider = providerName ? config.providers?.find((p) => p.name === providerName) : getActiveProvider(config);
|
|
39
56
|
if (!provider) {
|
|
40
57
|
throw new Error(providerName ? `Provider "${providerName}" not found` : "Not configured. Run: min-agent setup");
|
|
41
58
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const OPTION_TEXT_KEYS = ["label", "description", "title", "text", "value", "content", "prompt"];
|
|
2
|
+
function firstText(rec, keys) {
|
|
3
|
+
const parts = [];
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
for (const key of keys) {
|
|
6
|
+
const v = rec[key];
|
|
7
|
+
if (typeof v !== "string")
|
|
8
|
+
continue;
|
|
9
|
+
const text = v.trim();
|
|
10
|
+
if (!text || seen.has(text))
|
|
11
|
+
continue;
|
|
12
|
+
seen.add(text);
|
|
13
|
+
parts.push(text);
|
|
14
|
+
}
|
|
15
|
+
return parts;
|
|
16
|
+
}
|
|
17
|
+
/** Pull a user-visible label out of a model-supplied question option. */
|
|
18
|
+
export function optionText(item) {
|
|
19
|
+
if (item == null)
|
|
20
|
+
return null;
|
|
21
|
+
if (typeof item === "string") {
|
|
22
|
+
const text = item.trim();
|
|
23
|
+
return text.length > 0 ? text : null;
|
|
24
|
+
}
|
|
25
|
+
if (typeof item === "number" || typeof item === "boolean")
|
|
26
|
+
return String(item);
|
|
27
|
+
if (typeof item !== "object" || Array.isArray(item))
|
|
28
|
+
return null;
|
|
29
|
+
const rec = item;
|
|
30
|
+
const parts = firstText(rec, OPTION_TEXT_KEYS);
|
|
31
|
+
if (parts.length > 0)
|
|
32
|
+
return parts.join(" — ");
|
|
33
|
+
const key = rec.key;
|
|
34
|
+
return typeof key === "string" && key.trim() ? key.trim() : null;
|
|
35
|
+
}
|
|
36
|
+
export function normalizeQuestionPrompt(question) {
|
|
37
|
+
if (typeof question === "string")
|
|
38
|
+
return question;
|
|
39
|
+
return optionText(question) ?? "";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Models often send `{label, description, key}` (Claude-style) instead of
|
|
43
|
+
* `string[]`. Normalize to display strings so the TUI never calls string
|
|
44
|
+
* methods on objects.
|
|
45
|
+
*/
|
|
46
|
+
export function normalizeQuestionOptions(options) {
|
|
47
|
+
if (options == null)
|
|
48
|
+
return [];
|
|
49
|
+
const list = Array.isArray(options) ? options : [options];
|
|
50
|
+
const out = [];
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
for (const item of list) {
|
|
53
|
+
const text = optionText(item);
|
|
54
|
+
if (!text || seen.has(text))
|
|
55
|
+
continue;
|
|
56
|
+
seen.add(text);
|
|
57
|
+
out.push(text);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { takeScopeFlags } from "./memory.js";
|
|
2
|
+
import { getSandboxSnapshot, setSandboxConfig } from "./config.js";
|
|
3
|
+
import { parseNetworkPolicy, parseSandboxMode, getEffectiveSandboxPolicy, sandboxEnforcementCaveat, sandboxModeLabel, sandboxStatusLabel, setSandboxOverride, } from "./sandbox.js";
|
|
4
|
+
export const SANDBOX_CLI_USAGE = [
|
|
5
|
+
"Usage: min-agent sandbox [off|workspace|strict] [--project|--global]",
|
|
6
|
+
" min-agent sandbox network allow|deny [--project|--global]",
|
|
7
|
+
" min-agent --sandbox off|workspace|strict [--project|--global]",
|
|
8
|
+
].join("\n");
|
|
9
|
+
function sourceLabel(source) {
|
|
10
|
+
return source === "cli"
|
|
11
|
+
? "this run"
|
|
12
|
+
: source === "env"
|
|
13
|
+
? "environment variable"
|
|
14
|
+
: source === "project"
|
|
15
|
+
? "project"
|
|
16
|
+
: source === "global"
|
|
17
|
+
? "global"
|
|
18
|
+
: "default";
|
|
19
|
+
}
|
|
20
|
+
function scopeLabel(scope) {
|
|
21
|
+
return scope === "project" ? "project" : "global";
|
|
22
|
+
}
|
|
23
|
+
export function runSandboxCli(input) {
|
|
24
|
+
const { scope: posScope, rest } = takeScopeFlags(input.positionals);
|
|
25
|
+
const scope = posScope ?? input.scope ?? "global";
|
|
26
|
+
if (rest[0] === "--help" || rest[0] === "-h") {
|
|
27
|
+
return { ok: true, lines: [SANDBOX_CLI_USAGE] };
|
|
28
|
+
}
|
|
29
|
+
if (rest[0] === "network") {
|
|
30
|
+
const parsed = parseNetworkPolicy(rest[1]);
|
|
31
|
+
if (!parsed || rest.length > 2)
|
|
32
|
+
return { ok: false, lines: [SANDBOX_CLI_USAGE] };
|
|
33
|
+
setSandboxConfig({ network: parsed }, scope);
|
|
34
|
+
setSandboxOverride({ network: parsed });
|
|
35
|
+
return { ok: true, lines: [`✓ Network set to ${parsed === "deny" ? "denied" : "allowed"} (${scopeLabel(scope)})`] };
|
|
36
|
+
}
|
|
37
|
+
if (rest.length === 1) {
|
|
38
|
+
const parsed = parseSandboxMode(rest[0]);
|
|
39
|
+
if (!parsed)
|
|
40
|
+
return { ok: false, lines: [SANDBOX_CLI_USAGE] };
|
|
41
|
+
setSandboxConfig({ mode: parsed }, scope);
|
|
42
|
+
setSandboxOverride({ mode: parsed });
|
|
43
|
+
const caveat = sandboxEnforcementCaveat(parsed);
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
lines: [`✓ Sandbox set to ${sandboxModeLabel(parsed)} (${scopeLabel(scope)})`, ...(caveat ? [caveat] : [])],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (rest.length > 1)
|
|
50
|
+
return { ok: false, lines: [SANDBOX_CLI_USAGE] };
|
|
51
|
+
if (input.flagMode || input.flagNetwork) {
|
|
52
|
+
setSandboxConfig({
|
|
53
|
+
...(input.flagMode ? { mode: input.flagMode } : {}),
|
|
54
|
+
...(input.flagNetwork ? { network: input.flagNetwork } : {}),
|
|
55
|
+
}, scope);
|
|
56
|
+
setSandboxOverride({
|
|
57
|
+
...(input.flagMode ? { mode: input.flagMode } : {}),
|
|
58
|
+
...(input.flagNetwork ? { network: input.flagNetwork } : {}),
|
|
59
|
+
});
|
|
60
|
+
const parts = [
|
|
61
|
+
...(input.flagMode ? [`sandbox set to ${sandboxModeLabel(input.flagMode)}`] : []),
|
|
62
|
+
...(input.flagNetwork ? [`network set to ${input.flagNetwork === "deny" ? "denied" : "allowed"}`] : []),
|
|
63
|
+
];
|
|
64
|
+
const caveat = input.flagMode ? sandboxEnforcementCaveat(input.flagMode) : null;
|
|
65
|
+
return { ok: true, lines: [`✓ ${parts.join(", ")} (${scopeLabel(scope)})`, ...(caveat ? [caveat] : [])] };
|
|
66
|
+
}
|
|
67
|
+
const snap = getSandboxSnapshot();
|
|
68
|
+
const extras = [
|
|
69
|
+
...(snap.extraWriteRoots.length > 0 ? [`Extra writable dirs: ${snap.extraWriteRoots.join(", ")}`] : []),
|
|
70
|
+
...(snap.extraReadRoots.length > 0 ? [`Extra readable dirs: ${snap.extraReadRoots.join(", ")}`] : []),
|
|
71
|
+
];
|
|
72
|
+
const caveat = sandboxEnforcementCaveat(getEffectiveSandboxPolicy().mode);
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
lines: [
|
|
76
|
+
`Current sandbox: ${sandboxStatusLabel()} (${sourceLabel(snap.source)})`,
|
|
77
|
+
...extras,
|
|
78
|
+
...(caveat ? [caveat] : []),
|
|
79
|
+
SANDBOX_CLI_USAGE,
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|