@ghenya/clinn 0.7.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/Logos/StartLogo.txt +7 -0
- package/Mem/history.js +130 -0
- package/Mem/index.js +113 -0
- package/README.md +230 -0
- package/Src/agent.js +342 -0
- package/Src/index.js +984 -0
- package/Src/llm.js +195 -0
- package/Tools/browser.js +133 -0
- package/Tools/custom/.gitkeep +0 -0
- package/Tools/edit_tools.js +93 -0
- package/Tools/extended_tools.js +408 -0
- package/Tools/file_tools.js +201 -0
- package/Tools/index.js +311 -0
- package/Tools/search_tools.js +280 -0
- package/Tools/tokenizer.js +150 -0
- package/config.json +251 -0
- package/package.json +48 -0
package/Src/index.js
ADDED
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const readline = require("readline");
|
|
6
|
+
const { spawn } = require("child_process");
|
|
7
|
+
const Agent = require("./agent");
|
|
8
|
+
const Tools = require("../Tools");
|
|
9
|
+
const { listRecentTurns, searchHistory, getFileList, loadFileTurns } = require("../Mem/history");
|
|
10
|
+
|
|
11
|
+
const C = {
|
|
12
|
+
reset: "\x1b[0m",
|
|
13
|
+
bold: "\x1b[1m",
|
|
14
|
+
dim: "\x1b[2m",
|
|
15
|
+
red: "\x1b[31m",
|
|
16
|
+
green: "\x1b[32m",
|
|
17
|
+
yellow: "\x1b[33m",
|
|
18
|
+
blue: "\x1b[34m",
|
|
19
|
+
magenta: "\x1b[35m",
|
|
20
|
+
cyan: "\x1b[36m",
|
|
21
|
+
white: "\x1b[37m",
|
|
22
|
+
gray: "\x1b[90m",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let config;
|
|
26
|
+
let agent;
|
|
27
|
+
let rl;
|
|
28
|
+
let pendingPermission = null;
|
|
29
|
+
let permissionResolve = null;
|
|
30
|
+
let isAgentRunning = false;
|
|
31
|
+
let sigintCount = 0;
|
|
32
|
+
let sigintTimer = null;
|
|
33
|
+
|
|
34
|
+
function emoji(key) {
|
|
35
|
+
return config.ui?.emoji?.[key] || "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const CLINN_DIR = path.join(os.homedir(), ".clinn");
|
|
39
|
+
const CLINN_CONFIG = path.join(CLINN_DIR, "config.json");
|
|
40
|
+
const PKG_CONFIG = path.join(__dirname, "..", "config.json");
|
|
41
|
+
|
|
42
|
+
function ensureClinnDir() {
|
|
43
|
+
if (!fs.existsSync(CLINN_DIR)) fs.mkdirSync(CLINN_DIR, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadConfig() {
|
|
47
|
+
ensureClinnDir();
|
|
48
|
+
if (fs.existsSync(CLINN_CONFIG)) {
|
|
49
|
+
const raw = fs.readFileSync(CLINN_CONFIG, "utf-8");
|
|
50
|
+
config = JSON.parse(raw);
|
|
51
|
+
} else {
|
|
52
|
+
const raw = fs.readFileSync(PKG_CONFIG, "utf-8");
|
|
53
|
+
config = JSON.parse(raw);
|
|
54
|
+
fs.writeFileSync(CLINN_CONFIG, JSON.stringify(config, null, 2), "utf-8");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function saveConfig() {
|
|
59
|
+
ensureClinnDir();
|
|
60
|
+
fs.writeFileSync(CLINN_CONFIG, JSON.stringify(config, null, 2), "utf-8");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function maxWidth() {
|
|
64
|
+
const cfgMax = config.ui?.maxWidth || 0;
|
|
65
|
+
const termW = process.stdout.columns || 80;
|
|
66
|
+
return cfgMax > 0 ? Math.min(cfgMax, termW) : termW;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function div(ch) {
|
|
70
|
+
const c = ch || config.ui?.dividerChar || "─";
|
|
71
|
+
return C.dim + c.repeat(Math.max(1, maxWidth() - 1)) + C.reset;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function pipeToPager(text) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
const p = spawn("less", ["-R", "-F", "-X"], { stdio: ["pipe", "inherit", "inherit"] });
|
|
77
|
+
p.stdin.write(text);
|
|
78
|
+
p.stdin.end();
|
|
79
|
+
p.on("close", resolve);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function showLogo() {
|
|
84
|
+
if (config.ui?.showLogo && config.ui?.logoPath) {
|
|
85
|
+
const logoPath = path.resolve(__dirname, "..", config.ui.logoPath);
|
|
86
|
+
if (fs.existsSync(logoPath)) {
|
|
87
|
+
process.stdout.write(C.cyan + fs.readFileSync(logoPath, "utf-8") + C.reset);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function wordWrap(text, width) {
|
|
93
|
+
if (!text) return "";
|
|
94
|
+
const w = width || maxWidth() - 4;
|
|
95
|
+
const plainLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
96
|
+
const lines = [];
|
|
97
|
+
for (const para of text.split("\n")) {
|
|
98
|
+
if (!para.trim()) { lines.push(""); continue; }
|
|
99
|
+
let cur = "";
|
|
100
|
+
for (const ch of para) {
|
|
101
|
+
if (plainLen(cur + ch) >= w) { lines.push(cur); cur = ""; }
|
|
102
|
+
cur += ch;
|
|
103
|
+
}
|
|
104
|
+
if (cur) lines.push(cur);
|
|
105
|
+
}
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function typeset(text) {
|
|
110
|
+
const w = Math.max(40, maxWidth() - 2);
|
|
111
|
+
const lines = text.split("\n");
|
|
112
|
+
const out = [];
|
|
113
|
+
let inCode = false;
|
|
114
|
+
let codeLines = [];
|
|
115
|
+
let codeMinIndent = Infinity;
|
|
116
|
+
let inTable = false;
|
|
117
|
+
let tableRows = [];
|
|
118
|
+
let tableAlign = [];
|
|
119
|
+
|
|
120
|
+
function flushCode() {
|
|
121
|
+
if (codeLines.length === 0) return;
|
|
122
|
+
const maxW = Math.min(w - 4, 50);
|
|
123
|
+
out.push(C.dim + " ┌" + "─".repeat(maxW) + C.reset);
|
|
124
|
+
for (const cl of codeLines) {
|
|
125
|
+
const trimmed = cl.slice(codeMinIndent).replace(/\t/g, " ");
|
|
126
|
+
out.push(` ${C.dim}│${C.reset} ${C.green}${trimmed}${C.reset}`);
|
|
127
|
+
}
|
|
128
|
+
out.push(C.dim + " └" + "─".repeat(maxW) + C.reset);
|
|
129
|
+
codeLines = [];
|
|
130
|
+
codeMinIndent = Infinity;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function flushTable() {
|
|
134
|
+
if (tableRows.length === 0) return;
|
|
135
|
+
const ncols = tableRows[0].length;
|
|
136
|
+
const colW = new Array(ncols).fill(0);
|
|
137
|
+
for (const row of tableRows) {
|
|
138
|
+
for (let i = 0; i < row.length; i++) {
|
|
139
|
+
colW[i] = Math.max(colW[i], row[i].replace(/\x1b\[[0-9;]*m/g, "").length);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const totalW = colW.reduce((a, b) => a + b, 0) + ncols * 3 + 1;
|
|
143
|
+
const maxColW = w - 2;
|
|
144
|
+
let scale = 1;
|
|
145
|
+
if (totalW > maxColW) {
|
|
146
|
+
scale = maxColW / totalW;
|
|
147
|
+
for (let i = 0; i < colW.length; i++) colW[i] = Math.max(3, Math.floor(colW[i] * scale));
|
|
148
|
+
}
|
|
149
|
+
for (let ri = 0; ri < tableRows.length; ri++) {
|
|
150
|
+
const row = tableRows[ri];
|
|
151
|
+
const cells = row.map((c, ci) => {
|
|
152
|
+
const plain = c.replace(/\x1b\[[0-9;]*m/g, "");
|
|
153
|
+
const pad = colW[ci] - plain.length;
|
|
154
|
+
const align = tableAlign[ci] || "left";
|
|
155
|
+
if (align === "right") return " ".repeat(pad) + c;
|
|
156
|
+
if (align === "center") return " ".repeat(Math.floor(pad / 2)) + c + " ".repeat(Math.ceil(pad / 2));
|
|
157
|
+
return c + " ".repeat(pad);
|
|
158
|
+
});
|
|
159
|
+
const cellStyle = ri === 0 ? C.bold : C.reset;
|
|
160
|
+
out.push(` ${cellStyle}${cells.join(` ${C.dim}│${C.reset} `)}${C.reset}`);
|
|
161
|
+
}
|
|
162
|
+
tableRows = [];
|
|
163
|
+
tableAlign = [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const raw of lines) {
|
|
167
|
+
if (raw.trim().startsWith("```")) {
|
|
168
|
+
if (inCode) { flushCode(); inCode = false; }
|
|
169
|
+
else { inCode = true; flushTable(); }
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (inCode) {
|
|
173
|
+
const stripped = raw.replace(/\t/g, " ");
|
|
174
|
+
const indent = stripped.match(/^ */)[0].length;
|
|
175
|
+
if (stripped.trim()) codeMinIndent = Math.min(codeMinIndent, indent);
|
|
176
|
+
codeLines.push(stripped);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const isSep = /^\|[\s\-:|]+\|$/.test(raw.trim());
|
|
180
|
+
const isRow = /^\|[\s\S]+\|$/.test(raw.trim());
|
|
181
|
+
if (isSep && tableRows.length === 1) {
|
|
182
|
+
const cells = raw.split("|").filter((c) => c.trim() !== "").map((c) => c.trim());
|
|
183
|
+
tableAlign = cells.map((c) => {
|
|
184
|
+
if (c.startsWith(":") && c.endsWith(":")) return "center";
|
|
185
|
+
if (c.endsWith(":")) return "right";
|
|
186
|
+
return "left";
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (isRow) {
|
|
191
|
+
if (!inTable) { flushCode(); inTable = true; }
|
|
192
|
+
const parts = raw.split("|");
|
|
193
|
+
const start = parts[0].trim() ? 0 : 1;
|
|
194
|
+
const end = parts[parts.length - 1].trim() ? parts.length : parts.length - 1;
|
|
195
|
+
tableRows.push(parts.slice(start, end).map((c) => c.trim()));
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (inTable && tableRows.length > 0) { flushTable(); inTable = false; }
|
|
199
|
+
|
|
200
|
+
let line = raw;
|
|
201
|
+
if (/^#{1,6}\s/.test(line)) {
|
|
202
|
+
const level = line.match(/^(#+)/)[1].length;
|
|
203
|
+
const title = line.replace(/^#+\s*/, "");
|
|
204
|
+
out.push("");
|
|
205
|
+
if (level === 1) {
|
|
206
|
+
out.push(C.bold + C.cyan + ` ━━ ${title} ━━` + C.reset);
|
|
207
|
+
} else if (level === 2) {
|
|
208
|
+
out.push(C.bold + ` ▎${title}` + C.reset);
|
|
209
|
+
out.push(C.dim + " " + "─".repeat(Math.min(w - 4, title.replace(/\x1b\[[0-9;]*m/g, "").length)) + C.reset);
|
|
210
|
+
} else {
|
|
211
|
+
out.push(C.bold + ` ${title}` + C.reset);
|
|
212
|
+
}
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (/^\s*[-*]\s/.test(line)) {
|
|
216
|
+
out.push(` ${C.yellow}•${C.reset} ${wordWrap(line.replace(/^\s*[-*]\s*/, ""), w - 4)}`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (/^\s*\d+[.)]\s/.test(line)) {
|
|
220
|
+
const num = line.match(/^\s*(\d+)[.)]/)[1];
|
|
221
|
+
out.push(` ${C.yellow}${num}.${C.reset} ${wordWrap(line.replace(/^\s*\d+[.)]\s*/, ""), w - 5)}`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (/^>\s/.test(line)) {
|
|
225
|
+
out.push(`${C.dim} ▎ ${wordWrap(line.replace(/^>\s*/, ""), w - 5)}${C.reset}`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (/^[-\*]{3,}\s*$/.test(line.trim())) {
|
|
229
|
+
out.push(C.dim + " · · · · ·" + C.reset);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
line = line.replace(/`([^`]+)`/g, C.green + "$1" + C.reset);
|
|
233
|
+
line = line.replace(/\*\*\*(.+?)\*\*\*/g, C.bold + C.yellow + "$1" + C.reset);
|
|
234
|
+
line = line.replace(/\*\*(.+?)\*\*/g, C.bold + "$1" + C.reset);
|
|
235
|
+
line = line.replace(/\*(.+?)\*/g, C.dim + "$1" + C.reset);
|
|
236
|
+
line = line.replace(/~~(.+?)~~/g, C.dim + C.red + "$1" + C.reset);
|
|
237
|
+
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, C.cyan + "$1" + C.reset + C.dim + " ($2)" + C.reset);
|
|
238
|
+
if (!line.trim()) {
|
|
239
|
+
flushCode();
|
|
240
|
+
out.push("");
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
out.push(" " + wordWrap(line, w - 2));
|
|
244
|
+
}
|
|
245
|
+
flushCode();
|
|
246
|
+
flushTable();
|
|
247
|
+
return out.join("\n");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function inlineTypeset(line) {
|
|
251
|
+
if (/^#{1,6}\s/.test(line)) {
|
|
252
|
+
const level = line.match(/^(#+)/)[1].length;
|
|
253
|
+
const title = line.replace(/^#+\s*/, "");
|
|
254
|
+
if (level === 1) return `\n${C.bold}${C.cyan} ━━ ${title} ━━${C.reset}`;
|
|
255
|
+
if (level === 2) return `\n${C.bold} ▎${title}${C.reset}`;
|
|
256
|
+
return `\n${C.bold} ${title}${C.reset}`;
|
|
257
|
+
}
|
|
258
|
+
if (/^\s*[-*]\s/.test(line)) {
|
|
259
|
+
return ` ${C.yellow}•${C.reset} ${line.replace(/^\s*[-*]\s*/, "")}`;
|
|
260
|
+
}
|
|
261
|
+
if (/^\s*\d+[.)]\s/.test(line)) {
|
|
262
|
+
const num = line.match(/^\s*(\d+)[.)]/)[1];
|
|
263
|
+
return ` ${C.yellow}${num}.${C.reset} ${line.replace(/^\s*\d+[.)]\s*/, "")}`;
|
|
264
|
+
}
|
|
265
|
+
if (/^>\s/.test(line)) {
|
|
266
|
+
return `${C.dim} ▎ ${line.replace(/^>\s*/, "")}${C.reset}`;
|
|
267
|
+
}
|
|
268
|
+
if (/^[-\*]{3,}\s*$/.test(line.trim())) {
|
|
269
|
+
return C.dim + " · · · · ·" + C.reset;
|
|
270
|
+
}
|
|
271
|
+
let l = line;
|
|
272
|
+
l = l.replace(/`([^`]+)`/g, C.green + "$1" + C.reset);
|
|
273
|
+
l = l.replace(/\*\*\*(.+?)\*\*\*/g, C.bold + C.yellow + "$1" + C.reset);
|
|
274
|
+
l = l.replace(/\*\*(.+?)\*\*/g, C.bold + "$1" + C.reset);
|
|
275
|
+
l = l.replace(/\*(.+?)\*/g, C.dim + "$1" + C.reset);
|
|
276
|
+
l = l.replace(/~~(.+?)~~/g, C.dim + C.red + "$1" + C.reset);
|
|
277
|
+
l = l.replace(/\[([^\]]+)\]\(([^)]+)\)/g, C.cyan + "$1" + C.reset + C.dim + " ($2)" + C.reset);
|
|
278
|
+
if (!l.trim()) return "";
|
|
279
|
+
return " " + l;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function ctxBar(pct) {
|
|
283
|
+
const w = maxWidth();
|
|
284
|
+
const barW = Math.min(w - 4, 30);
|
|
285
|
+
const filled = Math.max(1, Math.round((pct / 100) * barW));
|
|
286
|
+
const empty = barW - filled;
|
|
287
|
+
const barChar = emoji("ctxBar") || "━";
|
|
288
|
+
const color = pct > 80 ? C.red : pct > 60 ? C.yellow : C.green;
|
|
289
|
+
return C.dim + "[" + C.reset + color + barChar.repeat(filled) + C.reset
|
|
290
|
+
+ C.dim + "─".repeat(empty) + "]" + C.reset
|
|
291
|
+
+ ` ${color}${pct}%${C.reset}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
ctxBar._frame = 0;
|
|
295
|
+
ctxBar.live = function (pct, maxBarW) {
|
|
296
|
+
const barW = maxBarW || Math.min(maxWidth() - 4, 30);
|
|
297
|
+
const filled = Math.max(1, Math.round((pct / 100) * barW));
|
|
298
|
+
const empty = barW - filled;
|
|
299
|
+
const color = pct > 80 ? C.red : pct > 60 ? C.yellow : C.green;
|
|
300
|
+
const brightColor = pct > 80 ? "\x1b[91m" : pct > 60 ? "\x1b[93m" : "\x1b[92m";
|
|
301
|
+
ctxBar._frame++;
|
|
302
|
+
const phase = ctxBar._frame % 6;
|
|
303
|
+
const breatheColor = phase < 3 ? brightColor : "\x1b[2m";
|
|
304
|
+
const bar = "\x1b[1m█\x1b[0m" + color + "━".repeat(Math.max(0, filled - 1)) + C.reset;
|
|
305
|
+
return C.dim + "[" + C.reset + bar
|
|
306
|
+
+ C.dim + "─".repeat(Math.max(0, empty)) + "]" + C.reset
|
|
307
|
+
+ ` ${breatheColor}${pct}%${C.reset}`;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
function tokenBar(promptTokens, completionTokens, elapsedMs, ctxPct) {
|
|
311
|
+
const w = maxWidth();
|
|
312
|
+
const elapsed = (elapsedMs / 1000).toFixed(1) + "s";
|
|
313
|
+
const parts = [
|
|
314
|
+
`${emoji("tokenIn")}${C.cyan}${promptTokens}${C.reset}`,
|
|
315
|
+
`${emoji("tokenOut")}${C.magenta}${completionTokens}${C.reset}`,
|
|
316
|
+
`${emoji("clock")}${C.yellow}${elapsed}${C.reset}`,
|
|
317
|
+
`${C.dim}∑${promptTokens + completionTokens}${C.reset}`,
|
|
318
|
+
];
|
|
319
|
+
let line = parts.join(" ");
|
|
320
|
+
if (ctxPct != null) {
|
|
321
|
+
line += " " + ctxBar(ctxPct);
|
|
322
|
+
}
|
|
323
|
+
const plainLen = line.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
324
|
+
const dashTotal = Math.max(0, w - plainLen - 2);
|
|
325
|
+
return C.dim + "─".repeat(Math.max(0, Math.floor(dashTotal / 2))) + C.reset
|
|
326
|
+
+ " " + line + " "
|
|
327
|
+
+ C.dim + "─".repeat(Math.max(0, Math.ceil(dashTotal / 2))) + C.reset;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function toolEmoji(name) {
|
|
331
|
+
const em = config.ui?.emoji || {};
|
|
332
|
+
if (name.startsWith("read_") || name === "list_dir" || name === "read" || name === "file_info" || name === "ls" || name === "tree") return em.fileRead || "";
|
|
333
|
+
if (name.startsWith("write_") || name === "copy_file" || name === "move_file" || name === "write") return em.fileWrite || "";
|
|
334
|
+
if (name.startsWith("search_") || name.startsWith("find_") || name === "grep" || name === "glob") return em.search || "";
|
|
335
|
+
if (name === "exec_console" || name === "wait_command" || name === "check_command_status") return em.exec || "";
|
|
336
|
+
if (name === "web_fetch" || name === "web_search" || name === "open_preview") return em.web || "";
|
|
337
|
+
if (name === "todo_write") return em.todo || "";
|
|
338
|
+
if (name === "forget_conversation") return em.forget || "";
|
|
339
|
+
if (name === "restart_session") return em.restart || "";
|
|
340
|
+
if (name === "search_replace" || name === "edit_lines") return "✏";
|
|
341
|
+
return em.toolCall || "";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function inputValidator(line) {
|
|
345
|
+
const sanitized = line.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
346
|
+
if (sanitized.length > 10000) return sanitized.slice(0, 10000);
|
|
347
|
+
return sanitized;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function showHelp() {
|
|
351
|
+
console.log(div("="));
|
|
352
|
+
console.log(C.bold + C.cyan + " Clinn 控制台命令" + C.reset);
|
|
353
|
+
console.log(div("="));
|
|
354
|
+
const cmds = [
|
|
355
|
+
["/help", "显示此帮助"], ["/exit", "退出程序"], ["/reset", "重置当前对话"],
|
|
356
|
+
["/tools", "列出所有工具"], ["/tool_search <q>", "搜索工具"],
|
|
357
|
+
["/tools_more", "查看全部工具(含扩展)"],
|
|
358
|
+
["/temp <0-2>", "设置温度"], ["/token <n>", "设置最大输出token"],
|
|
359
|
+
["/memory", "查看记忆统计"], ["/memory_list [n]", "列出记忆条目"],
|
|
360
|
+
["/memory_search <q>", "搜索记忆"], ["/memory_del <id>", "删除记忆"],
|
|
361
|
+
["/memory_clear", "清空所有记忆"], ["/compress", "手动压缩上下文"],
|
|
362
|
+
["/history [n]", "查看最近n条历史对话"], ["/history files", "查看历史文件列表"],
|
|
363
|
+
["/history search <q>", "搜索历史对话"], ["/history read <f>", "读取文件含完整工具调用"],
|
|
364
|
+
["/tool_save <name>", "持久化保存工具"], ["/tool_list_saved", "列出持久化工具"],
|
|
365
|
+
["/tool_del_saved <name>", "删除持久化工具"],
|
|
366
|
+
["/trusted", "查看受信任工具"], ["/trust <name>", "永久信任工具"],
|
|
367
|
+
["/untrust <name>", "取消信任"], ["/status", "查看当前状态"],
|
|
368
|
+
["/ctx", "查看上下文使用量"],
|
|
369
|
+
];
|
|
370
|
+
for (const [cmd, desc] of cmds) {
|
|
371
|
+
console.log(` ${C.yellow + cmd.padEnd(22) + C.reset} ${desc}`);
|
|
372
|
+
}
|
|
373
|
+
console.log(div("="));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function showStatus() {
|
|
377
|
+
const usage = agent ? agent.getUsage() : { prompt: 0, completion: 0 };
|
|
378
|
+
const mem = agent ? agent.memory.stats() : {};
|
|
379
|
+
const ctxPct = agent ? agent.estimateContextPct() : 0;
|
|
380
|
+
console.log(div("="));
|
|
381
|
+
console.log(` 模型: ${C.bold}${config.llm.model}${C.reset} 温度: ${config.llm.temperature}`);
|
|
382
|
+
console.log(` Tokens 累计: ${emoji("tokenIn")}${C.cyan}${usage.prompt}${C.reset} ${emoji("tokenOut")}${C.magenta}${usage.completion}${C.reset}`);
|
|
383
|
+
console.log(` 上下文使用: ${ctxBar(ctxPct)}`);
|
|
384
|
+
console.log(` 记忆: ${mem.entries || 0}/${mem.maxEntries || config.memory.maxEntries} 条目, 历史 ${mem.historyMessages || 0} 条`);
|
|
385
|
+
const tools = Tools.listToolNames();
|
|
386
|
+
console.log(` 工具: ${tools.length} 个 — ${tools.slice(0, 8).join(", ")}${tools.length > 8 ? " ..." : ""}`);
|
|
387
|
+
console.log(div("="));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function askPermission(name, args) {
|
|
391
|
+
return new Promise((resolve) => {
|
|
392
|
+
const argStr = JSON.stringify(args).slice(0, 120);
|
|
393
|
+
console.log(`\n ${C.yellow}${emoji("warn")} 权限请求 ${name}${C.reset} ${C.dim}${argStr}${C.reset}`);
|
|
394
|
+
console.log(` ${C.green}[Y]${C.reset}放行 ${C.blue}[A]${C.reset}永久 ${C.red}[N]${C.reset}拒绝`);
|
|
395
|
+
process.stdout.write(" > ");
|
|
396
|
+
permissionResolve = (answer) => resolve(answer);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function handlePermissionResponse(line) {
|
|
401
|
+
if (!permissionResolve) return false;
|
|
402
|
+
const l = line.trim().toLowerCase();
|
|
403
|
+
if (l === "y" || l === "yes") { permissionResolve("once"); permissionResolve = null; return true; }
|
|
404
|
+
if (l === "a" || l === "always") { permissionResolve("always"); permissionResolve = null; return true; }
|
|
405
|
+
if (l === "n" || l === "no") { permissionResolve("deny"); permissionResolve = null; return true; }
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function onPermission(name, args) {
|
|
410
|
+
const result = await askPermission(name, args);
|
|
411
|
+
if (result === "always") {
|
|
412
|
+
Tools.addTrusted(name);
|
|
413
|
+
config.tools.trustedTools = [...new Set([...(config.tools?.trustedTools || []), name])];
|
|
414
|
+
saveConfig();
|
|
415
|
+
console.log(` ${C.green}已永久信任 ${name}${C.reset}\n`);
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
if (result === "once") { console.log(` ${C.green}本次放行${C.reset}\n`); return true; }
|
|
419
|
+
console.log(` ${C.red}已拒绝${C.reset}\n`);
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function buildAgent() {
|
|
424
|
+
agent = new Agent(config, {
|
|
425
|
+
onPermission,
|
|
426
|
+
onTimer: (seconds, message) => {
|
|
427
|
+
setTimeout(() => {
|
|
428
|
+
console.log(`\n ${C.magenta}[定时通知 ${seconds}s]${C.reset} ${message}`);
|
|
429
|
+
if (agent) {
|
|
430
|
+
runUserInput(`[系统定时通知 | ${seconds}秒前] ${message}`).catch((e) => {
|
|
431
|
+
console.log(`${C.red}${emoji("error")} 定时任务错误: ${e.message}${C.reset}`);
|
|
432
|
+
rl.prompt();
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}, seconds * 1000);
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function handleSlashCommand(input) {
|
|
441
|
+
const parts = input.slice(1).trim().split(/\s+/);
|
|
442
|
+
const cmd = parts[0].toLowerCase();
|
|
443
|
+
const rest = parts.slice(1).join(" ");
|
|
444
|
+
|
|
445
|
+
switch (cmd) {
|
|
446
|
+
case "help": showHelp(); break;
|
|
447
|
+
case "exit":
|
|
448
|
+
console.log(C.dim + `再见~ ${emoji("done")}` + C.reset);
|
|
449
|
+
process.exit(0);
|
|
450
|
+
case "reset":
|
|
451
|
+
agent.reset();
|
|
452
|
+
console.log(`${C.green}对话已重置${C.reset}`);
|
|
453
|
+
break;
|
|
454
|
+
case "tools": {
|
|
455
|
+
const names = Tools.listToolNames();
|
|
456
|
+
const baseTools = names.filter((n) => !["forget_conversation", "restart_session", "todo_write", "search_replace", "glob", "grep", "read", "write", "ls", "web_search", "check_command_status", "open_preview", "get_diagnostics", "skill"].includes(n));
|
|
457
|
+
console.log(div());
|
|
458
|
+
for (const n of baseTools) {
|
|
459
|
+
const t = Tools.getTool(n);
|
|
460
|
+
const mark = t?.dangerous ? C.yellow + "⚠" + C.reset : " ";
|
|
461
|
+
console.log(` ${mark} ${C.cyan}${n}${C.reset} ${C.dim}${t?.description || ""}${C.reset}`);
|
|
462
|
+
}
|
|
463
|
+
console.log(C.dim + ` 还有 ${names.length - baseTools.length} 个扩展工具, 输入 /tools_more 查看全部` + C.reset);
|
|
464
|
+
console.log(div());
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
case "tools_more": {
|
|
468
|
+
const names = Tools.listToolNames();
|
|
469
|
+
console.log(div());
|
|
470
|
+
for (const n of names) {
|
|
471
|
+
const t = Tools.getTool(n);
|
|
472
|
+
const mark = t?.dangerous ? C.yellow + "⚠" + C.reset : " ";
|
|
473
|
+
console.log(` ${mark} ${C.cyan}${n}${C.reset} ${C.dim}${t?.description || ""}${C.reset}`);
|
|
474
|
+
}
|
|
475
|
+
console.log(div());
|
|
476
|
+
console.log(` 共 ${names.length} 个工具`);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
case "ctx": {
|
|
480
|
+
const pct = agent.estimateContextPct();
|
|
481
|
+
const maxCtx = agent.getMaxContextTokens();
|
|
482
|
+
const usage = agent.getUsage();
|
|
483
|
+
console.log(div());
|
|
484
|
+
console.log(` 上下文: ${ctxBar(pct)} 上限: ${maxCtx} tokens`);
|
|
485
|
+
console.log(` 累计消耗: ▾${usage.prompt} ▴${usage.completion} ∑${usage.prompt + usage.completion}`);
|
|
486
|
+
console.log(` 提示: 上下文 >80% 时可输入 /compress 压缩或让AI调用 forget_conversation`);
|
|
487
|
+
console.log(div());
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
case "tool_search": {
|
|
491
|
+
if (!rest) { console.log(`用法: /tool_search <关键词>`); break; }
|
|
492
|
+
const results = Tools.searchToolRegistry(rest);
|
|
493
|
+
if (results.length === 0) { console.log(`无匹配: ${rest}`); break; }
|
|
494
|
+
console.log(div());
|
|
495
|
+
for (const r of results) console.log(` ${C.cyan}${r.name}${C.reset} ${C.dim}${r.description}${C.reset}`);
|
|
496
|
+
console.log(div());
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
case "temp": {
|
|
500
|
+
const v = parseFloat(rest);
|
|
501
|
+
if (isNaN(v) || v < 0 || v > 2) { console.log(`温度范围 0-2`); break; }
|
|
502
|
+
config.llm.temperature = v;
|
|
503
|
+
saveConfig();
|
|
504
|
+
buildAgent();
|
|
505
|
+
console.log(`${C.green}温度已设为 ${v}${C.reset}`);
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
case "token": {
|
|
509
|
+
const v = parseInt(rest, 10);
|
|
510
|
+
if (isNaN(v) || v < 1 || v > 128000) { console.log(`范围 1-128000`); break; }
|
|
511
|
+
config.llm.maxTokens = v;
|
|
512
|
+
saveConfig();
|
|
513
|
+
buildAgent();
|
|
514
|
+
console.log(`${C.green}MaxTokens 已设为 ${v}${C.reset}`);
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
case "max_history": {
|
|
518
|
+
const v = parseInt(rest, 10);
|
|
519
|
+
if (isNaN(v) || v < 1 || v > 200) { console.log(`范围 1-200`); break; }
|
|
520
|
+
config.memory.maxHistory = v;
|
|
521
|
+
saveConfig();
|
|
522
|
+
agent.memory.setMaxHistory(v);
|
|
523
|
+
console.log(`${C.green}历史上限已设为 ${v}${C.reset}`);
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
case "ctx_length": {
|
|
527
|
+
const v = parseInt(rest, 10);
|
|
528
|
+
if (isNaN(v) || v < 1 || v > 128000) { console.log(`范围 1-128000`); break; }
|
|
529
|
+
config.llm.maxTokens = v;
|
|
530
|
+
saveConfig();
|
|
531
|
+
buildAgent();
|
|
532
|
+
console.log(`${C.green}上下文长度已设为 ${v}${C.reset}`);
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
case "memory": {
|
|
536
|
+
const s = agent.memory.stats();
|
|
537
|
+
console.log(div());
|
|
538
|
+
console.log(` 条目: ${s.entries}/${s.maxEntries} 历史消息: ${s.historyMessages}`);
|
|
539
|
+
console.log(div());
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
case "memory_list": {
|
|
543
|
+
const n = parseInt(rest, 10) || 20;
|
|
544
|
+
const entries = agent.memory.getAllEntries().slice(-n);
|
|
545
|
+
if (entries.length === 0) { console.log("(无记忆条目)"); break; }
|
|
546
|
+
console.log(div());
|
|
547
|
+
for (const e of entries) console.log(` ${C.yellow}#${e.id}${C.reset} ${C.dim}[${e.tags?.join(",") || "-"}]${C.reset} ${e.text}`);
|
|
548
|
+
console.log(div());
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
case "memory_search": {
|
|
552
|
+
if (!rest) { console.log(`用法: /memory_search <关键词>`); break; }
|
|
553
|
+
const results = agent.memory.searchEntries(rest, 10);
|
|
554
|
+
if (results.length === 0) { console.log(`无匹配: ${rest}`); break; }
|
|
555
|
+
console.log(div());
|
|
556
|
+
for (const e of results) console.log(` ${C.yellow}#${e.id}${C.reset} ${e.text}`);
|
|
557
|
+
console.log(div());
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
case "memory_del": {
|
|
561
|
+
const id = parseInt(rest, 10);
|
|
562
|
+
if (isNaN(id)) { console.log(`用法: /memory_del <id>`); break; }
|
|
563
|
+
const ok = agent.memory.removeEntry(id);
|
|
564
|
+
console.log(ok ? `${C.green}已删除 #${id}${C.reset}` : `${C.yellow}未找到 #${id}${C.reset}`);
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
case "memory_clear":
|
|
568
|
+
agent.memory.clearEntries();
|
|
569
|
+
console.log(`${C.green}所有记忆条目已清空${C.reset}`);
|
|
570
|
+
break;
|
|
571
|
+
case "history": {
|
|
572
|
+
const subCmd = rest ? rest.split(/\s+/)[0] : "";
|
|
573
|
+
let out = "";
|
|
574
|
+
if (subCmd === "files") {
|
|
575
|
+
const files = getFileList();
|
|
576
|
+
if (files.length === 0) { console.log("(暂无历史文件)"); break; }
|
|
577
|
+
out = C.bold + C.cyan + " 历史对话文件" + C.reset + "\n" + div() + "\n";
|
|
578
|
+
for (const f of files) {
|
|
579
|
+
out += ` ${C.cyan}${f.file}${C.reset} ${C.dim}${String(f.turns).padStart(4)}轮 ${String(f.size).padStart(5)}KB ${f.created.slice(0, 10)}${C.reset}\n`;
|
|
580
|
+
}
|
|
581
|
+
out += div();
|
|
582
|
+
} else if (subCmd === "search") {
|
|
583
|
+
const q = rest.replace(/^search\s*/, "");
|
|
584
|
+
if (!q) { console.log(`用法: /history search <关键词>`); break; }
|
|
585
|
+
const results = searchHistory(q, 20);
|
|
586
|
+
if (results.length === 0) { console.log(`无匹配: ${q}`); break; }
|
|
587
|
+
out = `${C.bold + C.yellow}搜索: "${q}"${C.reset} (${results.length} 条)\n` + div() + "\n";
|
|
588
|
+
for (const r of results) {
|
|
589
|
+
out += `${C.yellow}${r.time?.slice(0, 16) || "?"}${C.reset} ${C.dim}[${r.file}]${C.reset}\n`;
|
|
590
|
+
if (r.cwd) out += ` ${C.dim}${r.cwd}${C.reset}\n`;
|
|
591
|
+
out += ` ${C.cyan}▸${C.reset} ${r.user.slice(0, 300)}\n`;
|
|
592
|
+
out += ` ${C.green}◂${C.reset} ${r.assistant.slice(0, 500)}\n\n`;
|
|
593
|
+
}
|
|
594
|
+
out += div();
|
|
595
|
+
} else if (subCmd === "read") {
|
|
596
|
+
const args = rest.replace(/^read\s*/, "").trim();
|
|
597
|
+
if (!args) { console.log(`用法: /history read <文件名> [轮数]`); break; }
|
|
598
|
+
const [fileName, nStr] = args.split(/\s+/);
|
|
599
|
+
const limit = parseInt(nStr, 10) || 50;
|
|
600
|
+
const turns = loadFileTurns(fileName, limit);
|
|
601
|
+
if (turns.length === 0) { console.log(`文件为空或不存在: ${fileName}`); break; }
|
|
602
|
+
out = `${C.bold + C.cyan}${fileName}${C.reset} (${turns.length} 轮)\n` + div() + "\n";
|
|
603
|
+
for (const t of turns) {
|
|
604
|
+
out += `${C.yellow}${t.time?.slice(0, 16) || "?"}${C.reset}\n`;
|
|
605
|
+
if (t.cwd) out += ` ${C.dim}${t.cwd}${C.reset}\n`;
|
|
606
|
+
out += ` ${C.cyan}▸${C.reset} ${t.user}\n`;
|
|
607
|
+
out += ` ${C.green}◂${C.reset} ${t.assistant}\n`;
|
|
608
|
+
if (t.messages && t.messages.length > 2) {
|
|
609
|
+
out += ` ${C.dim}--- 工具调用过程 (${t.messages.length} 条消息) ---${C.reset}\n`;
|
|
610
|
+
for (const m of t.messages) {
|
|
611
|
+
if (m.role === "tool") continue;
|
|
612
|
+
const tag = m.role === "user" ? `${C.cyan}▸${C.reset}` : m.tool_calls ? `${C.yellow}⚙${C.reset}` : `${C.green}◂${C.reset}`;
|
|
613
|
+
const txt = m.content || (m.tool_calls ? m.tool_calls.map(tc => tc.function.name).join(", ") : "");
|
|
614
|
+
if (txt) out += ` ${tag} ${txt.slice(0, 400)}\n`;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
out += "\n";
|
|
618
|
+
}
|
|
619
|
+
out += div();
|
|
620
|
+
} else {
|
|
621
|
+
const turns = listRecentTurns(rest ? parseInt(rest, 10) || 30 : 30);
|
|
622
|
+
if (turns.length === 0) { console.log("(暂无历史记录)"); break; }
|
|
623
|
+
out = `${C.bold + C.cyan}最近 ${turns.length} 轮对话${C.reset}\n` + div() + "\n";
|
|
624
|
+
for (const t of turns) {
|
|
625
|
+
out += `${C.yellow}${t.time?.slice(0, 16) || "?"}${C.reset} ${C.dim}[${t.file}]${C.reset}\n`;
|
|
626
|
+
if (t.cwd) out += ` ${C.dim}${t.cwd}${C.reset}\n`;
|
|
627
|
+
out += ` ${C.cyan}▸${C.reset} ${t.user.slice(0, 300)}\n`;
|
|
628
|
+
out += ` ${C.green}◂${C.reset} ${t.assistant.slice(0, 500)}\n\n`;
|
|
629
|
+
}
|
|
630
|
+
out += div();
|
|
631
|
+
}
|
|
632
|
+
const plainLen = Math.max(60, maxWidth());
|
|
633
|
+
const wrapped = div("=") + "\n" + out;
|
|
634
|
+
await pipeToPager(wrapped);
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
case "compress": {
|
|
638
|
+
console.log(`${C.yellow}正在压缩上下文...${C.reset}`);
|
|
639
|
+
const result = await agent._handleAgentTool("compress_context", {});
|
|
640
|
+
console.log(C.green + result + C.reset);
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
case "trusted": {
|
|
644
|
+
const names = [...new Set(config.tools?.trustedTools || [])];
|
|
645
|
+
console.log(div());
|
|
646
|
+
if (names.length === 0) console.log(" (无受信任工具)");
|
|
647
|
+
else names.forEach((n) => console.log(` ${C.green}✓${C.reset} ${n}`));
|
|
648
|
+
console.log(div());
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
case "trust": {
|
|
652
|
+
if (!rest) { console.log(`用法: /trust <工具名>`); break; }
|
|
653
|
+
Tools.addTrusted(rest);
|
|
654
|
+
config.tools.trustedTools = [...new Set([...(config.tools?.trustedTools || []), rest])];
|
|
655
|
+
saveConfig();
|
|
656
|
+
console.log(`${C.green}已永久信任: ${rest}${C.reset}`);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
case "untrust": {
|
|
660
|
+
if (!rest) { console.log(`用法: /untrust <工具名>`); break; }
|
|
661
|
+
Tools.removeTrusted(rest);
|
|
662
|
+
config.tools.trustedTools = (config.tools?.trustedTools || []).filter((n) => n !== rest);
|
|
663
|
+
saveConfig();
|
|
664
|
+
console.log(`${C.yellow}已取消信任: ${rest}${C.reset}`);
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
case "tool_save": {
|
|
668
|
+
if (!rest) { console.log(`用法: /tool_save <工具名> <JS代码>`); break; }
|
|
669
|
+
const spaceIdx = rest.indexOf(" ");
|
|
670
|
+
if (spaceIdx < 0) { console.log(`用法: /tool_save <工具名> <JS代码>`); break; }
|
|
671
|
+
const result = Tools.saveToolToFile(rest.slice(0, spaceIdx), rest.slice(spaceIdx + 1));
|
|
672
|
+
if (result.startsWith("[OK]")) agent.refreshTools();
|
|
673
|
+
console.log((result.startsWith("[OK]") ? C.green : C.yellow) + result + C.reset);
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
case "tool_list_saved": {
|
|
677
|
+
const saved = Tools.listCustomTools();
|
|
678
|
+
if (saved.length === 0) { console.log("(无持久化工具)"); break; }
|
|
679
|
+
console.log(div());
|
|
680
|
+
for (const s of saved) console.log(` ${C.cyan}${s.file}${C.reset} 导出: ${s.exports.join(", ")}`);
|
|
681
|
+
console.log(div());
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
case "tool_del_saved": {
|
|
685
|
+
if (!rest) { console.log(`用法: /tool_del_saved <工具名>`); break; }
|
|
686
|
+
const result = Tools.deleteToolFile(rest);
|
|
687
|
+
if (result.startsWith("[OK]")) agent.refreshTools();
|
|
688
|
+
console.log((result.startsWith("[OK]") ? C.green : C.yellow) + result + C.reset);
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
case "status": showStatus(); break;
|
|
692
|
+
default:
|
|
693
|
+
console.log(`${C.yellow}未知命令: /${cmd}${C.reset} 输入 ${C.cyan}/help${C.reset} 查看所有命令`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function runUserInput(input) {
|
|
698
|
+
const startTime = Date.now();
|
|
699
|
+
const usageBefore = agent.getUsage();
|
|
700
|
+
isAgentRunning = true;
|
|
701
|
+
let lineBuf = "";
|
|
702
|
+
|
|
703
|
+
let tableBuf = [];
|
|
704
|
+
let inCode = false;
|
|
705
|
+
|
|
706
|
+
function isTableRow(s) { return /^\|[\s\S]+\|$/.test(s.trim()); }
|
|
707
|
+
function isTableSep(s) { return /^\|[\s\-:|]+\|$/.test(s.trim()); }
|
|
708
|
+
function isCodeFence(s) { return s.trim().startsWith("```"); }
|
|
709
|
+
|
|
710
|
+
function flushTableBuf() {
|
|
711
|
+
if (tableBuf.length === 0) return;
|
|
712
|
+
const rows = tableBuf.map(r => r.split("|").filter((c, i, a) => i > 0 && i < a.length - 1).map(c => c.trim()));
|
|
713
|
+
const header = rows[0];
|
|
714
|
+
const isSep = (r) => r.every(c => /^:?-+:?$/.test(c));
|
|
715
|
+
const dataRows = rows.filter((r, i) => i > 0 && !isSep(r));
|
|
716
|
+
const stripBold = (s) => (s || "").replace(/\*\*(.+?)\*\*/g, "$1");
|
|
717
|
+
for (const row of dataRows) {
|
|
718
|
+
const parts = [];
|
|
719
|
+
for (let i = 0; i < header.length && i < row.length; i++) {
|
|
720
|
+
const key = stripBold(header[i]);
|
|
721
|
+
const val = stripBold(row[i]);
|
|
722
|
+
if (val) parts.push(`${C.bold}${key}${C.reset}: ${val}`);
|
|
723
|
+
}
|
|
724
|
+
process.stdout.write(` ${parts.join("\n ")}\n`);
|
|
725
|
+
}
|
|
726
|
+
tableBuf = [];
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function flushOne(line) {
|
|
730
|
+
if (isCodeFence(line)) {
|
|
731
|
+
inCode = !inCode;
|
|
732
|
+
if (inCode) {
|
|
733
|
+
flushTableBuf();
|
|
734
|
+
process.stdout.write(C.dim + " ┌" + "─".repeat(Math.min(maxWidth() - 6, 50)) + C.reset + "\n");
|
|
735
|
+
} else {
|
|
736
|
+
process.stdout.write(C.dim + " └" + "─".repeat(Math.min(maxWidth() - 6, 50)) + C.reset + "\n");
|
|
737
|
+
}
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (inCode) {
|
|
741
|
+
const trimmed = line.replace(/\t/g, " ");
|
|
742
|
+
process.stdout.write(` ${C.dim}│${C.reset} ${C.green}${trimmed}${C.reset}\n`);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (isTableRow(line)) {
|
|
746
|
+
if (tableBuf.length === 0) {
|
|
747
|
+
tableBuf.push(line);
|
|
748
|
+
} else if (tableBuf.length === 1 && isTableSep(line)) {
|
|
749
|
+
tableBuf.push(line);
|
|
750
|
+
} else if (isTableSep(line) && tableBuf.length >= 2) {
|
|
751
|
+
tableBuf.push(line);
|
|
752
|
+
} else if (isTableRow(line)) {
|
|
753
|
+
tableBuf.push(line);
|
|
754
|
+
} else {
|
|
755
|
+
flushTableBuf();
|
|
756
|
+
tableBuf.push(line);
|
|
757
|
+
}
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (tableBuf.length > 0) {
|
|
761
|
+
flushTableBuf();
|
|
762
|
+
}
|
|
763
|
+
if (!line) {
|
|
764
|
+
process.stdout.write("\n");
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const rendered = inlineTypeset(line);
|
|
768
|
+
process.stdout.write(rendered + "\n");
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function flushAllBufs() {
|
|
772
|
+
flushTableBuf();
|
|
773
|
+
if (inCode) {
|
|
774
|
+
inCode = false;
|
|
775
|
+
process.stdout.write(C.dim + " └" + "─".repeat(Math.min(maxWidth() - 6, 50)) + C.reset + "\n");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
let streamingStarted = false;
|
|
780
|
+
let labelPrinted = false;
|
|
781
|
+
let warned90 = false;
|
|
782
|
+
let warned95 = false;
|
|
783
|
+
let thinkTimer = null;
|
|
784
|
+
let thinkFrame = 0;
|
|
785
|
+
|
|
786
|
+
const THINK_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
787
|
+
|
|
788
|
+
function startThinking() {
|
|
789
|
+
if (thinkTimer) return;
|
|
790
|
+
thinkFrame = 0;
|
|
791
|
+
thinkTimer = setInterval(() => {
|
|
792
|
+
process.stdout.write(`\r\x1b[0K${C.cyan}${THINK_FRAMES[thinkFrame % THINK_FRAMES.length]}${C.reset} ${C.dim}思考中…${C.reset}`);
|
|
793
|
+
thinkFrame++;
|
|
794
|
+
}, 120);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function stopThinking() {
|
|
798
|
+
if (thinkTimer) { clearInterval(thinkTimer); thinkTimer = null; }
|
|
799
|
+
process.stdout.write("\r\x1b[0K");
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function warnContext(pct) {
|
|
803
|
+
if (pct >= 95 && !warned95) {
|
|
804
|
+
warned95 = true;
|
|
805
|
+
stopThinking();
|
|
806
|
+
process.stdout.write(`\n ${C.yellow}⚠${C.reset} 上下文已使用 ${C.bold}${C.yellow}${pct}%${C.reset},请尽快 /clear\n`);
|
|
807
|
+
} else if (pct >= 90 && !warned90) {
|
|
808
|
+
warned90 = true;
|
|
809
|
+
stopThinking();
|
|
810
|
+
process.stdout.write(`\n ${C.yellow}⚠${C.reset} 上下文已使用 ${C.bold}${C.yellow}${pct}%${C.reset},建议 /clear 清除对话\n`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
try {
|
|
815
|
+
const response = await agent.run(input, {
|
|
816
|
+
onContextPct: (pct) => { warnContext(pct); },
|
|
817
|
+
onThinking: () => {
|
|
818
|
+
if (!streamingStarted) {
|
|
819
|
+
streamingStarted = true;
|
|
820
|
+
startThinking();
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
onContent: (token) => {
|
|
824
|
+
stopThinking();
|
|
825
|
+
if (!labelPrinted) {
|
|
826
|
+
console.log("");
|
|
827
|
+
console.log(`${C.cyan}Clinn${C.reset}:`);
|
|
828
|
+
labelPrinted = true;
|
|
829
|
+
}
|
|
830
|
+
for (const ch of token) {
|
|
831
|
+
lineBuf += ch;
|
|
832
|
+
if (ch === "\n") {
|
|
833
|
+
flushOne(lineBuf.slice(0, -1));
|
|
834
|
+
lineBuf = "";
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
onToolCall: (name, args, round) => {
|
|
839
|
+
stopThinking();
|
|
840
|
+
if (lineBuf.trim()) {
|
|
841
|
+
flushOne(lineBuf);
|
|
842
|
+
lineBuf = "";
|
|
843
|
+
}
|
|
844
|
+
startThinking();
|
|
845
|
+
},
|
|
846
|
+
onToolResult: (name, preview) => {
|
|
847
|
+
stopThinking();
|
|
848
|
+
process.stdout.write(` ${C.dim}${C.green}✓${C.reset} ${name}\n`);
|
|
849
|
+
startThinking();
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
} finally {
|
|
853
|
+
stopThinking();
|
|
854
|
+
if (lineBuf.trim()) {
|
|
855
|
+
flushOne(lineBuf);
|
|
856
|
+
}
|
|
857
|
+
flushAllBufs();
|
|
858
|
+
isAgentRunning = false;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const elapsed = Date.now() - startTime;
|
|
862
|
+
|
|
863
|
+
const usageAfter = agent.getUsage();
|
|
864
|
+
const diffPrompt = usageAfter.prompt - usageBefore.prompt;
|
|
865
|
+
const diffCompletion = usageAfter.completion - usageBefore.completion;
|
|
866
|
+
const ctxPct = agent.estimateContextPct();
|
|
867
|
+
|
|
868
|
+
console.log("");
|
|
869
|
+
console.log(tokenBar(diffPrompt, diffCompletion, elapsed, ctxPct));
|
|
870
|
+
console.log(div());
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async function handleInput(line) {
|
|
874
|
+
const input = inputValidator(line.trim());
|
|
875
|
+
if (!input) return;
|
|
876
|
+
|
|
877
|
+
if (permissionResolve) {
|
|
878
|
+
handlePermissionResponse(input);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (input.startsWith("/")) {
|
|
883
|
+
await handleSlashCommand(input);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
console.log(div());
|
|
888
|
+
try {
|
|
889
|
+
await runUserInput(input);
|
|
890
|
+
} catch (e) {
|
|
891
|
+
isAgentRunning = false;
|
|
892
|
+
console.log(`${C.red}${emoji("error")} 错误: ${e.message}${C.reset}`);
|
|
893
|
+
console.log(div());
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
async function main() {
|
|
898
|
+
loadConfig();
|
|
899
|
+
showLogo();
|
|
900
|
+
buildAgent();
|
|
901
|
+
|
|
902
|
+
readline.emitKeypressEvents(process.stdin);
|
|
903
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
904
|
+
|
|
905
|
+
let currentLine = "";
|
|
906
|
+
let showedMenu = false;
|
|
907
|
+
|
|
908
|
+
process.stdin.on("keypress", (str, key) => {
|
|
909
|
+
if (key && key.name === "backspace") {
|
|
910
|
+
currentLine = currentLine.slice(0, -1);
|
|
911
|
+
} else if (key && key.name === "return") {
|
|
912
|
+
currentLine = "";
|
|
913
|
+
showedMenu = false;
|
|
914
|
+
} else if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
915
|
+
currentLine += str;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (currentLine === "/" && !showedMenu) {
|
|
919
|
+
showedMenu = true;
|
|
920
|
+
const menu = [
|
|
921
|
+
`${C.cyan}help${C.reset}`, `${C.cyan}exit${C.reset}`, `${C.cyan}reset${C.reset}`,
|
|
922
|
+
`${C.cyan}tools${C.reset}`, `${C.cyan}status${C.reset}`, `${C.cyan}ctx${C.reset}`,
|
|
923
|
+
`${C.cyan}temp${C.reset}`, `${C.cyan}token${C.reset}`,
|
|
924
|
+
`${C.cyan}compress${C.reset}`, `${C.cyan}memory${C.reset}`, `${C.cyan}history${C.reset}`,
|
|
925
|
+
`${C.cyan}tool_save${C.reset}`, `${C.cyan}tool_del${C.reset}`,
|
|
926
|
+
];
|
|
927
|
+
process.stdout.write("\n" + C.dim + menu.join(" ") + C.reset + "\n");
|
|
928
|
+
process.stdout.write(`${C.green}> ${C.reset}/${currentLine.slice(1)}`);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
const mw = maxWidth();
|
|
933
|
+
console.log(`\n ${C.bold + C.cyan}${config.agent.name}${C.reset} v${config.agent.version} ${C.dim}DeepSeek驱动 | 最大宽度: ${mw}列${C.reset}`);
|
|
934
|
+
console.log(` ${C.dim}输入 ${C.yellow}/help${C.dim} 查看命令 | 输入 ${C.yellow}/${C.dim} 弹出命令菜单${C.reset}`);
|
|
935
|
+
console.log(div("="));
|
|
936
|
+
|
|
937
|
+
rl = readline.createInterface({
|
|
938
|
+
input: process.stdin,
|
|
939
|
+
output: process.stdout,
|
|
940
|
+
prompt: `${C.green}> ${C.reset}`,
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
rl.prompt();
|
|
944
|
+
|
|
945
|
+
rl.on("line", async (line) => {
|
|
946
|
+
await handleInput(line);
|
|
947
|
+
if (!permissionResolve) rl.prompt();
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
rl.on("close", () => {
|
|
951
|
+
console.log(C.dim + `\n再见~ ${emoji("done")}` + C.reset);
|
|
952
|
+
process.exit(0);
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
process.on("SIGINT", () => {
|
|
956
|
+
if (permissionResolve) {
|
|
957
|
+
permissionResolve("deny");
|
|
958
|
+
permissionResolve = null;
|
|
959
|
+
rl.prompt();
|
|
960
|
+
sigintCount = 0;
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (isAgentRunning) {
|
|
964
|
+
isAgentRunning = false;
|
|
965
|
+
console.log(`\n${C.yellow}⚠ AI 已中断${C.reset} ${C.dim}(再按一次 Ctrl+C 退出)${C.reset}`);
|
|
966
|
+
rl.prompt();
|
|
967
|
+
sigintCount = 0;
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
sigintCount++;
|
|
971
|
+
if (sigintCount >= 2) {
|
|
972
|
+
rl.close();
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (sigintTimer) clearTimeout(sigintTimer);
|
|
976
|
+
sigintTimer = setTimeout(() => { sigintCount = 0; }, 1500);
|
|
977
|
+
rl.close();
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
main().catch((e) => {
|
|
982
|
+
console.error(C.red + "启动失败: " + e.message + C.reset);
|
|
983
|
+
process.exit(1);
|
|
984
|
+
});
|