min-agent 0.1.8 → 0.2.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/dist/agent.js +6 -421
- package/dist/cli.js +16 -5
- package/dist/compaction.js +91 -29
- package/dist/confirm.js +26 -5
- package/dist/skills.js +29 -19
- package/dist/tui/App.js +15 -0
- package/dist/tui/ConfirmBar.js +13 -0
- package/dist/tui/InputBar.js +83 -0
- package/dist/tui/MessageList.js +33 -0
- package/dist/tui/Spinner.js +6 -0
- package/dist/tui/StatusBar.js +16 -0
- package/dist/tui/index.js +94 -0
- package/dist/tui/types.js +1 -0
- package/dist/tui-chat.js +384 -0
- package/package.json +5 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
import { App } from "./App.js";
|
|
4
|
+
/**
|
|
5
|
+
* TUI renderer — manages the Ink app lifecycle and provides
|
|
6
|
+
* imperative methods to update state from the agent loop.
|
|
7
|
+
*/
|
|
8
|
+
export class TuiRenderer {
|
|
9
|
+
state;
|
|
10
|
+
rerender = null;
|
|
11
|
+
inkInstance = null;
|
|
12
|
+
callbacks;
|
|
13
|
+
constructor(callbacks, model) {
|
|
14
|
+
this.state = {
|
|
15
|
+
messages: [],
|
|
16
|
+
isRunning: false,
|
|
17
|
+
model,
|
|
18
|
+
};
|
|
19
|
+
this.callbacks = callbacks;
|
|
20
|
+
}
|
|
21
|
+
start() {
|
|
22
|
+
this.inkInstance = render(_jsx(App, { initialState: this.state, onSubmit: this.callbacks.onSubmit, onConfirm: this.callbacks.onConfirm, onExit: this.callbacks.onExit }), { exitOnCtrlC: false });
|
|
23
|
+
// Position cursor at bottom-left for IME
|
|
24
|
+
const rows = process.stdout.rows ?? 24;
|
|
25
|
+
process.stdout.write(`\x1b[${rows};3H`);
|
|
26
|
+
}
|
|
27
|
+
/** Update state and trigger re-render */
|
|
28
|
+
update(partial) {
|
|
29
|
+
Object.assign(this.state, partial);
|
|
30
|
+
// Ink re-renders automatically when we call render again
|
|
31
|
+
if (this.inkInstance) {
|
|
32
|
+
this.inkInstance.rerender(_jsx(App, { initialState: this.state, onSubmit: this.callbacks.onSubmit, onConfirm: this.callbacks.onConfirm, onExit: this.callbacks.onExit }));
|
|
33
|
+
// Move terminal cursor to bottom-left (input bar area) for IME positioning
|
|
34
|
+
const rows = process.stdout.rows ?? 24;
|
|
35
|
+
process.stdout.write(`\x1b[${rows};3H`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
addMessage(msg) {
|
|
39
|
+
this.state.messages = [...this.state.messages, msg];
|
|
40
|
+
this.update({});
|
|
41
|
+
}
|
|
42
|
+
/** Append text to the last assistant message (for streaming) */
|
|
43
|
+
appendToLast(delta) {
|
|
44
|
+
const msgs = [...this.state.messages];
|
|
45
|
+
const last = msgs[msgs.length - 1];
|
|
46
|
+
if (last && last.role === "assistant") {
|
|
47
|
+
last.content += delta;
|
|
48
|
+
this.state.messages = msgs;
|
|
49
|
+
this.update({});
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
this.addMessage({
|
|
53
|
+
id: `msg-${Date.now()}`,
|
|
54
|
+
role: "assistant",
|
|
55
|
+
content: delta,
|
|
56
|
+
timestamp: Date.now(),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Append text to the current thinking message (for streaming thinking) */
|
|
61
|
+
appendToThinking(delta) {
|
|
62
|
+
const msgs = [...this.state.messages];
|
|
63
|
+
const last = msgs[msgs.length - 1];
|
|
64
|
+
if (last && last.role === "thinking") {
|
|
65
|
+
last.content += delta;
|
|
66
|
+
this.state.messages = msgs;
|
|
67
|
+
this.update({});
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.addMessage({
|
|
71
|
+
id: `thinking-${Date.now()}`,
|
|
72
|
+
role: "thinking",
|
|
73
|
+
content: delta,
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
setRunning(running, spinnerText) {
|
|
79
|
+
this.update({ isRunning: running, spinnerText });
|
|
80
|
+
}
|
|
81
|
+
showConfirm(message) {
|
|
82
|
+
this.update({ confirmMessage: message });
|
|
83
|
+
}
|
|
84
|
+
hideConfirm() {
|
|
85
|
+
this.update({ confirmMessage: undefined });
|
|
86
|
+
}
|
|
87
|
+
setTokenInfo(input, output, contextWindow) {
|
|
88
|
+
this.update({ tokenInfo: { input, output, contextWindow } });
|
|
89
|
+
}
|
|
90
|
+
destroy() {
|
|
91
|
+
this.inkInstance?.unmount();
|
|
92
|
+
this.inkInstance = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/tui-chat.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { TuiRenderer } from "./tui/index.js";
|
|
2
|
+
import { initMcp, shutdownMcp, getMcpStatus, loadMcpConfig } from "./mcp.js";
|
|
3
|
+
import { discoverSkills, getSkills } from "./skills.js";
|
|
4
|
+
import { loadInstructions } from "./instructions.js";
|
|
5
|
+
import { TokenTracker, estimateTokens, compactMessages } from "./compaction.js";
|
|
6
|
+
import { runOnce, runOnceWithSystem } from "./agent.js";
|
|
7
|
+
import { killActiveProcesses } from "./tools/bash.js";
|
|
8
|
+
import { setTuiConfirm } from "./confirm.js";
|
|
9
|
+
import { getContextWindow } from "./context-window.js";
|
|
10
|
+
import { loadConfig } from "./config.js";
|
|
11
|
+
import { resolveModel } from "./provider.js";
|
|
12
|
+
import { scanProject, buildCodeSystemPrompt } from "./code-mode.js";
|
|
13
|
+
let confirmResolver = null;
|
|
14
|
+
export async function runTui(opts) {
|
|
15
|
+
const { modelId, resumeSessionId, mode } = opts;
|
|
16
|
+
await initMcp();
|
|
17
|
+
discoverSkills();
|
|
18
|
+
const instructions = await loadInstructions();
|
|
19
|
+
const config = loadConfig();
|
|
20
|
+
// Code mode: scan project and build code-specific system prompt
|
|
21
|
+
let codeSystemPrompt;
|
|
22
|
+
if (mode === "code") {
|
|
23
|
+
const project = scanProject();
|
|
24
|
+
codeSystemPrompt = buildCodeSystemPrompt(project, instructions);
|
|
25
|
+
}
|
|
26
|
+
let messages = [];
|
|
27
|
+
let sessionId = resumeSessionId;
|
|
28
|
+
const tracker = new TokenTracker();
|
|
29
|
+
let abortController = null;
|
|
30
|
+
// Resume session if requested
|
|
31
|
+
if (resumeSessionId) {
|
|
32
|
+
const { loadSession } = await import("./sessions.js");
|
|
33
|
+
const session = loadSession(resumeSessionId);
|
|
34
|
+
if (session) {
|
|
35
|
+
messages = session.messages;
|
|
36
|
+
sessionId = resumeSessionId;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Forward reference — assigned after tui is created
|
|
40
|
+
let executeAgent;
|
|
41
|
+
const tui = new TuiRenderer({
|
|
42
|
+
onSubmit: async (text) => {
|
|
43
|
+
if (text.startsWith("/")) {
|
|
44
|
+
const result = await handleSlashCommand(text, tui, messages, modelId, tracker);
|
|
45
|
+
if (result === "run") {
|
|
46
|
+
await executeAgent();
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Add user message
|
|
51
|
+
tui.addMessage({
|
|
52
|
+
id: `user-${Date.now()}`,
|
|
53
|
+
role: "user",
|
|
54
|
+
content: text,
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
});
|
|
57
|
+
messages.push({ role: "user", content: text });
|
|
58
|
+
await executeAgent();
|
|
59
|
+
},
|
|
60
|
+
onConfirm: (accepted) => {
|
|
61
|
+
tui.hideConfirm();
|
|
62
|
+
if (confirmResolver) {
|
|
63
|
+
confirmResolver(accepted);
|
|
64
|
+
confirmResolver = null;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
onExit: () => {
|
|
68
|
+
if (abortController) {
|
|
69
|
+
killActiveProcesses();
|
|
70
|
+
abortController.abort();
|
|
71
|
+
abortController = null;
|
|
72
|
+
tui.setRunning(false);
|
|
73
|
+
tui.addMessage({
|
|
74
|
+
id: `sys-${Date.now()}`,
|
|
75
|
+
role: "system",
|
|
76
|
+
content: "(已取消)",
|
|
77
|
+
timestamp: Date.now(),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
}, modelId ?? config.provider?.defaultModel);
|
|
82
|
+
// Define the agent execution function (uses tui, messages, etc. from closure)
|
|
83
|
+
executeAgent = async () => {
|
|
84
|
+
abortController = new AbortController();
|
|
85
|
+
tui.setRunning(true, "思考中...");
|
|
86
|
+
const callbacks = {
|
|
87
|
+
onAssistantDisplayDelta: (delta) => {
|
|
88
|
+
// Pass raw delta — markdown formatting is applied at display time
|
|
89
|
+
tui.appendToLast(delta);
|
|
90
|
+
},
|
|
91
|
+
onThinkingDelta: (delta) => {
|
|
92
|
+
tui.setRunning(true, "推理中...");
|
|
93
|
+
tui.appendToThinking(delta);
|
|
94
|
+
},
|
|
95
|
+
onToolCall: (toolName, input) => {
|
|
96
|
+
tui.setRunning(true, `执行 ${toolName}...`);
|
|
97
|
+
const args = typeof input === "object" && input
|
|
98
|
+
? Object.entries(input)
|
|
99
|
+
.map(([k, v]) => `${k}=${typeof v === "string" ? (v.length > 40 ? v.slice(0, 40) + "..." : v) : JSON.stringify(v)}`)
|
|
100
|
+
.join(" ")
|
|
101
|
+
: "";
|
|
102
|
+
tui.addMessage({
|
|
103
|
+
id: `tool-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
104
|
+
role: "tool",
|
|
105
|
+
content: args,
|
|
106
|
+
toolName,
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
onToolResult: (_toolName, _output) => {
|
|
111
|
+
tui.setRunning(true, "思考中...");
|
|
112
|
+
},
|
|
113
|
+
onStreamError: (msg) => {
|
|
114
|
+
tui.addMessage({
|
|
115
|
+
id: `err-${Date.now()}`,
|
|
116
|
+
role: "system",
|
|
117
|
+
content: `错误: ${msg}`,
|
|
118
|
+
timestamp: Date.now(),
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
onRunFinish: async (info) => {
|
|
122
|
+
abortController = null;
|
|
123
|
+
tui.setRunning(false);
|
|
124
|
+
if (info.usage) {
|
|
125
|
+
const input = info.usage.inputTokens ?? 0;
|
|
126
|
+
const output = info.usage.outputTokens ?? 0;
|
|
127
|
+
const total = input + output;
|
|
128
|
+
const ctxWindow = await getContextWindow(modelId);
|
|
129
|
+
const pct = ctxWindow > 0 ? Math.round((input / ctxWindow) * 100) : 0;
|
|
130
|
+
tui.setTokenInfo(input, output, ctxWindow);
|
|
131
|
+
tui.addMessage({
|
|
132
|
+
id: `done-${Date.now()}`,
|
|
133
|
+
role: "system",
|
|
134
|
+
content: `Done in ${info.stepCount} step(s) | Tokens: ${input} in / ${output} out / ${total} total | Context: ${pct}%`,
|
|
135
|
+
timestamp: Date.now(),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
if (mode === "code" && codeSystemPrompt) {
|
|
141
|
+
await runOnceWithSystem(messages, codeSystemPrompt, modelId, abortController?.signal, callbacks, tracker);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
await runOnce(messages, instructions, modelId, abortController?.signal, callbacks, tracker);
|
|
145
|
+
}
|
|
146
|
+
abortController = null;
|
|
147
|
+
tui.setRunning(false);
|
|
148
|
+
};
|
|
149
|
+
// Override the confirm module to use TUI overlay
|
|
150
|
+
setTuiConfirm((message) => {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
confirmResolver = resolve;
|
|
153
|
+
tui.showConfirm(message);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
tui.start();
|
|
157
|
+
// Keep process alive until exit
|
|
158
|
+
await new Promise((resolve) => {
|
|
159
|
+
process.on("SIGINT", () => {
|
|
160
|
+
if (abortController) {
|
|
161
|
+
killActiveProcesses();
|
|
162
|
+
abortController.abort();
|
|
163
|
+
abortController = null;
|
|
164
|
+
tui.setRunning(false);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
resolve();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
// Save session
|
|
172
|
+
if (messages.length > 0) {
|
|
173
|
+
const { saveSession } = await import("./sessions.js");
|
|
174
|
+
sessionId = saveSession(messages, sessionId);
|
|
175
|
+
}
|
|
176
|
+
setTuiConfirm(null);
|
|
177
|
+
tui.destroy();
|
|
178
|
+
await shutdownMcp();
|
|
179
|
+
}
|
|
180
|
+
async function handleSlashCommand(input, tui, messages, modelId, tracker) {
|
|
181
|
+
const [cmd, ...rest] = input.slice(1).split(/\s+/);
|
|
182
|
+
const arg = rest.join(" ");
|
|
183
|
+
switch (cmd) {
|
|
184
|
+
case "exit":
|
|
185
|
+
case "quit":
|
|
186
|
+
case "q":
|
|
187
|
+
process.emit("SIGINT", "SIGINT");
|
|
188
|
+
break;
|
|
189
|
+
case "clear":
|
|
190
|
+
messages.length = 0;
|
|
191
|
+
tui.update({ messages: [] });
|
|
192
|
+
sysMsg(tui, "✓ 对话已清除");
|
|
193
|
+
break;
|
|
194
|
+
case "compact": {
|
|
195
|
+
if (messages.length < 4) {
|
|
196
|
+
sysMsg(tui, "消息太少,无需压缩");
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
sysMsg(tui, "⟳ 正在压缩上下文...");
|
|
200
|
+
const model = resolveModel(modelId);
|
|
201
|
+
const result = await compactMessages(messages, model, { keepRecentTurns: 2, autoContinue: false });
|
|
202
|
+
messages.length = 0;
|
|
203
|
+
messages.push(...result.messages);
|
|
204
|
+
if (tracker)
|
|
205
|
+
tracker.resetContext();
|
|
206
|
+
sysMsg(tui, `✓ 已压缩 (约 ${estimateTokens(messages)} tokens)`);
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case "model": {
|
|
211
|
+
if (arg) {
|
|
212
|
+
const config = await import("./config.js");
|
|
213
|
+
const cfg = config.loadConfig();
|
|
214
|
+
if (cfg.provider) {
|
|
215
|
+
cfg.provider.defaultModel = arg;
|
|
216
|
+
config.saveConfig(cfg);
|
|
217
|
+
sysMsg(tui, `✓ 默认模型已设为: ${arg}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const config = loadConfig();
|
|
222
|
+
sysMsg(tui, `当前模型: ${config.provider?.defaultModel ?? "未设置"}`);
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case "models": {
|
|
227
|
+
const { fetchModels } = await import("./config.js");
|
|
228
|
+
const cfg = loadConfig();
|
|
229
|
+
if (!cfg.provider?.baseURL || !cfg.provider?.apiKey) {
|
|
230
|
+
sysMsg(tui, "未配置 Provider,运行: min-agent setup");
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
sysMsg(tui, "正在获取模型列表...");
|
|
234
|
+
const models = await fetchModels(cfg.provider.baseURL, cfg.provider.apiKey);
|
|
235
|
+
if (models.length === 0) {
|
|
236
|
+
sysMsg(tui, "无法获取模型列表");
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
const lines = models.map((m) => {
|
|
240
|
+
const marker = m === cfg.provider?.defaultModel ? " ← 默认" : "";
|
|
241
|
+
return ` ${m}${marker}`;
|
|
242
|
+
});
|
|
243
|
+
sysMsg(tui, `可用模型 (${models.length}):\n${lines.join("\n")}`);
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case "memory": {
|
|
248
|
+
if (arg) {
|
|
249
|
+
const { addMemory } = await import("./memory.js");
|
|
250
|
+
addMemory(arg);
|
|
251
|
+
sysMsg(tui, `✓ 已保存记忆: "${arg}"`);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const { loadMemories } = await import("./memory.js");
|
|
255
|
+
const memories = loadMemories();
|
|
256
|
+
if (memories.length === 0) {
|
|
257
|
+
sysMsg(tui, "无已保存的记忆");
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const lines = memories.map((m, i) => {
|
|
261
|
+
const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
|
|
262
|
+
return ` #${i + 1}: ${m.content}${tags}`;
|
|
263
|
+
});
|
|
264
|
+
sysMsg(tui, `记忆 (${memories.length}):\n${lines.join("\n")}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case "path":
|
|
270
|
+
case "pwd":
|
|
271
|
+
sysMsg(tui, process.cwd());
|
|
272
|
+
break;
|
|
273
|
+
case "paste": {
|
|
274
|
+
const { getClipboardImage } = await import("./clipboard.js");
|
|
275
|
+
const img = getClipboardImage();
|
|
276
|
+
if (!img) {
|
|
277
|
+
sysMsg(tui, "剪贴板中未找到图片");
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
sysMsg(tui, `📎 剪贴板图片 (${(img.data.length / 1024).toFixed(1)} KB)`);
|
|
281
|
+
const text = arg || "What's in this image?";
|
|
282
|
+
const content = [
|
|
283
|
+
{ type: "text", text },
|
|
284
|
+
{ type: "image", image: img.data, mimeType: img.mimeType },
|
|
285
|
+
];
|
|
286
|
+
messages.push({ role: "user", content });
|
|
287
|
+
tui.addMessage({
|
|
288
|
+
id: `user-${Date.now()}`,
|
|
289
|
+
role: "user",
|
|
290
|
+
content: `${text} [📎 图片]`,
|
|
291
|
+
timestamp: Date.now(),
|
|
292
|
+
});
|
|
293
|
+
// Signal that we need to run the agent
|
|
294
|
+
return "run";
|
|
295
|
+
}
|
|
296
|
+
case "tokens": {
|
|
297
|
+
if (tracker && tracker.lastInputTokens > 0) {
|
|
298
|
+
const ctxWindow = await getContextWindow(modelId);
|
|
299
|
+
const pct = Math.round((tracker.lastInputTokens / ctxWindow) * 100);
|
|
300
|
+
sysMsg(tui, [
|
|
301
|
+
`Context: ${tracker.lastInputTokens} / ${ctxWindow} tokens (${pct}%)`,
|
|
302
|
+
`Total: ${tracker.totalInputTokens} in / ${tracker.totalOutputTokens} out`,
|
|
303
|
+
`Messages: ${messages.length}`,
|
|
304
|
+
].join("\n"));
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
sysMsg(tui, `估算 tokens: ${estimateTokens(messages)} | Messages: ${messages.length}`);
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
case "mcp": {
|
|
312
|
+
const config = loadMcpConfig();
|
|
313
|
+
const servers = Object.entries(config.mcpServers);
|
|
314
|
+
if (servers.length === 0) {
|
|
315
|
+
sysMsg(tui, "无 MCP 服务器配置");
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
const status = getMcpStatus();
|
|
319
|
+
const lines = servers.map(([name, cfg]) => {
|
|
320
|
+
const disabled = cfg.enabled === false;
|
|
321
|
+
const connected = status[name]?.connected ?? false;
|
|
322
|
+
const toolCount = status[name]?.tools.length ?? 0;
|
|
323
|
+
const state = disabled ? "disabled" : connected ? "connected" : "disconnected";
|
|
324
|
+
return ` ${name}: ${state}${toolCount > 0 ? ` (${toolCount} tools)` : ""}`;
|
|
325
|
+
});
|
|
326
|
+
sysMsg(tui, `MCP 服务器 (${servers.length}):\n${lines.join("\n")}`);
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case "skills": {
|
|
331
|
+
discoverSkills({ silent: true });
|
|
332
|
+
const skills = getSkills();
|
|
333
|
+
const cfg = loadConfig();
|
|
334
|
+
const disabledNames = cfg.disabledSkills ?? [];
|
|
335
|
+
if (skills.length === 0 && disabledNames.length === 0) {
|
|
336
|
+
sysMsg(tui, "无可用 skills。添加 SKILL.md 到 .min-agent/skills/<name>/");
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const lines = [];
|
|
340
|
+
for (const s of skills) {
|
|
341
|
+
lines.push(` ${s.name} enabled`);
|
|
342
|
+
}
|
|
343
|
+
for (const n of disabledNames) {
|
|
344
|
+
lines.push(` ${n} disabled`);
|
|
345
|
+
}
|
|
346
|
+
sysMsg(tui, `Skills:\n${lines.join("\n")}`);
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case "help":
|
|
351
|
+
sysMsg(tui, [
|
|
352
|
+
"命令:",
|
|
353
|
+
" /clear 清除对话历史",
|
|
354
|
+
" /compact 压缩上下文",
|
|
355
|
+
" /model [n] 显示/切换模型",
|
|
356
|
+
" /models 列出可用模型",
|
|
357
|
+
" /memory [t] 查看/保存记忆",
|
|
358
|
+
" /tokens 显示 token 用量",
|
|
359
|
+
" /mcp 显示 MCP 状态",
|
|
360
|
+
" /skills 显示可用 skills",
|
|
361
|
+
" /path 显示工作目录",
|
|
362
|
+
" /paste [t] 粘贴剪贴板图片 + 可选提示",
|
|
363
|
+
" /help 显示帮助",
|
|
364
|
+
" /exit 退出",
|
|
365
|
+
"",
|
|
366
|
+
"快捷键:",
|
|
367
|
+
" Esc 取消当前任务",
|
|
368
|
+
" Ctrl+J 输入框换行",
|
|
369
|
+
" Ctrl+C 退出程序",
|
|
370
|
+
" PageUp/Down 滚动消息",
|
|
371
|
+
].join("\n"));
|
|
372
|
+
break;
|
|
373
|
+
default:
|
|
374
|
+
sysMsg(tui, `未知命令: /${cmd},输入 /help 查看可用命令`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function sysMsg(tui, content) {
|
|
378
|
+
tui.addMessage({
|
|
379
|
+
id: `sys-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
380
|
+
role: "system",
|
|
381
|
+
content,
|
|
382
|
+
timestamp: Date.now(),
|
|
383
|
+
});
|
|
384
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "min-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Minimal AI coding agent with tool use, MCP, and skills support",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,10 +51,14 @@
|
|
|
51
51
|
"@modelcontextprotocol/sdk": "1.27.1",
|
|
52
52
|
"ai": "6.0.168",
|
|
53
53
|
"glob": "13.0.5",
|
|
54
|
+
"ink": "5",
|
|
55
|
+
"ink-spinner": "5",
|
|
56
|
+
"react": "18",
|
|
54
57
|
"zod": "^3.25.76"
|
|
55
58
|
},
|
|
56
59
|
"devDependencies": {
|
|
57
60
|
"@types/node": "22.13.9",
|
|
61
|
+
"@types/react": "18",
|
|
58
62
|
"tsx": "4.19.4",
|
|
59
63
|
"typescript": "5.8.2"
|
|
60
64
|
}
|