@ghenya/clinn 0.7.14 → 0.8.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 +1 -1
- package/Mem/package.json +1 -0
- package/Src/{agent.js → agent.cjs} +1 -1
- package/Src/index.js +7 -0
- package/Src/index.jsx +654 -0
- package/Src/{llm.js → llm.cjs} +29 -0
- package/Tools/package.json +1 -0
- package/bin/clinn.bat +1 -1
- package/bin/clinn.js +12 -1
- package/bin/package.json +1 -0
- package/config.json +2 -2
- package/{install.js → install.cjs} +1 -1
- package/package.json +20 -12
package/Logos/StartLogo.txt
CHANGED
package/Mem/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const os = require("os");
|
|
2
|
-
const LLMClient = require("./llm");
|
|
2
|
+
const LLMClient = require("./llm.cjs");
|
|
3
3
|
const Tools = require("../Tools");
|
|
4
4
|
const { ConversationMemory } = require("../Mem");
|
|
5
5
|
const { saveTurn, searchHistory, getFileList, loadFileTurns } = require("../Mem/history");
|
package/Src/index.js
CHANGED
|
@@ -933,6 +933,13 @@ async function main() {
|
|
|
933
933
|
showLogo();
|
|
934
934
|
buildAgent();
|
|
935
935
|
|
|
936
|
+
if (!config.llm.apiKey || config.llm.apiKey.startsWith("YOUR_")) {
|
|
937
|
+
console.log(` ${C.yellow}⚠ 未配置 API Key${C.reset}`);
|
|
938
|
+
console.log(` 设置: ${C.cyan}/api key <你的DeepSeek Key>${C.reset}`);
|
|
939
|
+
console.log(` 获取: ${C.dim}https://platform.deepseek.com/${C.reset}`);
|
|
940
|
+
console.log(div("="));
|
|
941
|
+
}
|
|
942
|
+
|
|
936
943
|
readline.emitKeypressEvents(process.stdin);
|
|
937
944
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
938
945
|
|
package/Src/index.jsx
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React, { useState, useCallback, useRef } from "react";
|
|
3
|
+
import { render, Box, Text, useInput, useStdout } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import Spinner from "ink-spinner";
|
|
6
|
+
import { createRequire } from "module";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import os from "os";
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
|
|
13
|
+
const Tools = require("../Tools");
|
|
14
|
+
const { listRecentTurns, searchHistory, getFileList, loadFileTurns } = require("../Mem/history");
|
|
15
|
+
|
|
16
|
+
const __filename = new URL(import.meta.url).pathname;
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
|
|
19
|
+
const CLINN_DIR = path.join(os.homedir(), ".clinn");
|
|
20
|
+
const CLINN_CONFIG = path.join(CLINN_DIR, "config.json");
|
|
21
|
+
const PKG_CONFIG = path.join(__dirname, "..", "config.json");
|
|
22
|
+
const LOGO_PATH = path.join(__dirname, "..", "Logos", "StartLogo.txt");
|
|
23
|
+
|
|
24
|
+
const VER = "0.8.0";
|
|
25
|
+
|
|
26
|
+
function ensureDir() { if (!fs.existsSync(CLINN_DIR)) fs.mkdirSync(CLINN_DIR, { recursive: true }); }
|
|
27
|
+
function loadConfig() {
|
|
28
|
+
ensureDir();
|
|
29
|
+
if (fs.existsSync(CLINN_CONFIG)) return JSON.parse(fs.readFileSync(CLINN_CONFIG, "utf-8"));
|
|
30
|
+
const cfg = JSON.parse(fs.readFileSync(PKG_CONFIG, "utf-8"));
|
|
31
|
+
fs.writeFileSync(CLINN_CONFIG, JSON.stringify(cfg, null, 2), "utf-8");
|
|
32
|
+
return cfg;
|
|
33
|
+
}
|
|
34
|
+
function saveConfig(cfg) { ensureDir(); fs.writeFileSync(CLINN_CONFIG, JSON.stringify(cfg, null, 2), "utf-8"); }
|
|
35
|
+
|
|
36
|
+
const CONFIG = loadConfig();
|
|
37
|
+
|
|
38
|
+
const LOGO_LINES = (() => {
|
|
39
|
+
try {
|
|
40
|
+
const raw = fs.readFileSync(LOGO_PATH, "utf-8");
|
|
41
|
+
return raw.replace(/0\.\d+\.\d+/, VER).split("\n").filter(l => l.trim() || l === "");
|
|
42
|
+
} catch (_) { return ["CLINN"]; }
|
|
43
|
+
})();
|
|
44
|
+
|
|
45
|
+
const MAX_MSG = 200;
|
|
46
|
+
|
|
47
|
+
function isWide(cp) {
|
|
48
|
+
return (cp >= 0x1100 && cp <= 0x115F) ||
|
|
49
|
+
(cp >= 0x2329 && cp <= 0x232A) ||
|
|
50
|
+
(cp >= 0x2E80 && cp <= 0x4DBF) ||
|
|
51
|
+
(cp >= 0x4E00 && cp <= 0xA4CF) ||
|
|
52
|
+
(cp >= 0xA960 && cp <= 0xA97F) ||
|
|
53
|
+
(cp >= 0xAC00 && cp <= 0xD7AF) ||
|
|
54
|
+
(cp >= 0xF900 && cp <= 0xFAFF) ||
|
|
55
|
+
(cp >= 0xFE10 && cp <= 0xFE1F) ||
|
|
56
|
+
(cp >= 0xFE30 && cp <= 0xFE6F) ||
|
|
57
|
+
(cp >= 0xFF01 && cp <= 0xFF60) ||
|
|
58
|
+
(cp >= 0xFFE0 && cp <= 0xFFE6) ||
|
|
59
|
+
(cp >= 0x1B000 && cp <= 0x1B2FF) ||
|
|
60
|
+
(cp >= 0x1F200 && cp <= 0x1F2FF) ||
|
|
61
|
+
(cp >= 0x20000 && cp <= 0x2FFFF);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function termLen(s) {
|
|
65
|
+
let w = 0;
|
|
66
|
+
for (const ch of s) w += isWide(ch.codePointAt(0)) ? 2 : 1;
|
|
67
|
+
return w;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function padTerm(s, n) {
|
|
71
|
+
const d = n - termLen(s);
|
|
72
|
+
return s + (d > 0 ? " ".repeat(d) : "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatMd(text, maxW) {
|
|
76
|
+
const w = maxW || 80;
|
|
77
|
+
const lines = text.split("\n");
|
|
78
|
+
const out = [];
|
|
79
|
+
let tableBuf = [];
|
|
80
|
+
let inCode = false;
|
|
81
|
+
const isTR = (s) => /^\|[\s\S]+\|$/.test(s.trim());
|
|
82
|
+
const isTS = (s) => /^\|[\s\-:|]+\|$/.test(s.trim());
|
|
83
|
+
const isCF = (s) => s.trim().startsWith("```");
|
|
84
|
+
|
|
85
|
+
function flush() {
|
|
86
|
+
if (tableBuf.length < 2) { tableBuf = []; return; }
|
|
87
|
+
const rows = tableBuf.map(r =>
|
|
88
|
+
r.split("|").filter((_, i, a) => i > 0 && i < a.length - 1).map(c => c.trim())
|
|
89
|
+
);
|
|
90
|
+
const hd = rows[0];
|
|
91
|
+
const data = rows.filter((r, i) => i > 0 && !r.every(c => /^:?-+:?$/.test(c)));
|
|
92
|
+
if (!hd.length || !data.length) { tableBuf = []; return; }
|
|
93
|
+
|
|
94
|
+
const maxCol = hd.map((c, ci) =>
|
|
95
|
+
Math.max(termLen(c), ...data.map(r => termLen(r[ci] || "")))
|
|
96
|
+
);
|
|
97
|
+
const widths = maxCol.map(x => x + 2);
|
|
98
|
+
const totalW = widths.reduce((a, b) => a + b, 0) + hd.length - 1;
|
|
99
|
+
|
|
100
|
+
if (totalW <= w) {
|
|
101
|
+
const hdrLine = "\u2500".repeat(totalW);
|
|
102
|
+
out.push(hd.map((c, i) => " " + padTerm(c, widths[i] - 1)).join(" "));
|
|
103
|
+
out.push(hdrLine);
|
|
104
|
+
for (const row of data) {
|
|
105
|
+
out.push(row.map((c, i) => " " + padTerm(c || "", widths[i] - 1)).join(" "));
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
for (const row of data) {
|
|
109
|
+
const parts = [];
|
|
110
|
+
for (let i = 0; i < hd.length && i < row.length; i++) {
|
|
111
|
+
if (row[i]) parts.push(hd[i] + ": " + row[i]);
|
|
112
|
+
}
|
|
113
|
+
out.push(parts.join(" \u00b7 "));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
tableBuf = [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const line of lines) {
|
|
120
|
+
if (isCF(line)) { flush(); inCode = !inCode; continue; }
|
|
121
|
+
if (inCode) { out.push(" " + line); continue; }
|
|
122
|
+
if (isTR(line)) { tableBuf.push(line); continue; }
|
|
123
|
+
if (isTS(line) && tableBuf.length >= 1) { tableBuf.push(line); continue; }
|
|
124
|
+
if (tableBuf.length) flush();
|
|
125
|
+
out.push(line);
|
|
126
|
+
}
|
|
127
|
+
flush();
|
|
128
|
+
return out.join("\n")
|
|
129
|
+
.replace(/\*\*(.+?)\*\*/g, "$1")
|
|
130
|
+
.replace(/^#{1,4}\s+/gm, "")
|
|
131
|
+
.replace(/^>\s?/gm, " ");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function Msg({ role, content }) {
|
|
135
|
+
const lines = content.split("\n");
|
|
136
|
+
const C = {
|
|
137
|
+
user: "greenBright",
|
|
138
|
+
system: "yellow",
|
|
139
|
+
assistant: "cyanBright",
|
|
140
|
+
};
|
|
141
|
+
const L = {
|
|
142
|
+
user: "(^o^)ノ 你",
|
|
143
|
+
system: "(!_!) 系统",
|
|
144
|
+
assistant: "(^_^) Clinn",
|
|
145
|
+
};
|
|
146
|
+
const bodyColor = role === "user" ? "white" : role === "system" ? "yellow" : undefined;
|
|
147
|
+
return (
|
|
148
|
+
<Box flexDirection="column">
|
|
149
|
+
<Text color={C[role]} bold>{L[role]}</Text>
|
|
150
|
+
<Text color={bodyColor}>
|
|
151
|
+
{lines.map((l, i) => (i === 0 ? "" : "\n") + " " + (l || " ")).join("")}
|
|
152
|
+
</Text>
|
|
153
|
+
</Box>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function Streaming({ content }) {
|
|
158
|
+
const lines = content.split("\n");
|
|
159
|
+
return (
|
|
160
|
+
<Box flexDirection="column">
|
|
161
|
+
<Box>
|
|
162
|
+
<Text color="cyanBright" bold>(^_^) Clinn</Text>
|
|
163
|
+
<Box marginLeft={1}>
|
|
164
|
+
<Text color="cyan"><Spinner type="dots" /></Text>
|
|
165
|
+
</Box>
|
|
166
|
+
</Box>
|
|
167
|
+
<Text>{lines.map((l, i) => (i === 0 ? "" : "\n") + " " + (l || " ")).join("")}</Text>
|
|
168
|
+
</Box>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function ToolLog({ tools }) {
|
|
173
|
+
if (!tools.length) return null;
|
|
174
|
+
return (
|
|
175
|
+
<Box flexDirection="column" paddingLeft={2}>
|
|
176
|
+
{tools.map((t, i) => (
|
|
177
|
+
<Box key={i} flexDirection="column">
|
|
178
|
+
<Box>
|
|
179
|
+
{t.status === "done"
|
|
180
|
+
? <Text color="green">(^_^)b </Text>
|
|
181
|
+
: <Text color="yellow"><Spinner /> </Text>}
|
|
182
|
+
<Text dimColor>{t.name}</Text>
|
|
183
|
+
{t.args ? <Text color="gray"> {t.args}</Text> : null}
|
|
184
|
+
</Box>
|
|
185
|
+
{t.result ? (
|
|
186
|
+
<Box paddingLeft={4}>
|
|
187
|
+
<Text color="gray">{t.result.slice(0, 120)}</Text>
|
|
188
|
+
</Box>
|
|
189
|
+
) : null}
|
|
190
|
+
</Box>
|
|
191
|
+
))}
|
|
192
|
+
</Box>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let _agent = null;
|
|
197
|
+
function getAgent() {
|
|
198
|
+
if (_agent) return _agent;
|
|
199
|
+
const Agent = require("./agent.cjs");
|
|
200
|
+
_agent = new Agent(CONFIG, { onPermission: () => true });
|
|
201
|
+
return _agent;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const COMMANDS = [
|
|
205
|
+
{ cmd: "/help", desc: "查看帮助" },
|
|
206
|
+
{ cmd: "/exit", desc: "退出程序" },
|
|
207
|
+
{ cmd: "/reset", desc: "重置对话" },
|
|
208
|
+
{ cmd: "/clear", desc: "清除对话" },
|
|
209
|
+
{ cmd: "/version", desc: "查看版本" },
|
|
210
|
+
{ cmd: "/status", desc: "当前状态" },
|
|
211
|
+
{ cmd: "/ctx", desc: "上下文使用量" },
|
|
212
|
+
{ cmd: "/tools", desc: "列出工具" },
|
|
213
|
+
{ cmd: "/tools_more", desc: "全部工具(含扩展)" },
|
|
214
|
+
{ cmd: "/tool_search", desc: "搜索工具" },
|
|
215
|
+
{ cmd: "/tool_list_saved", desc: "列出持久化工具" },
|
|
216
|
+
{ cmd: "/tool_save", desc: "持久化保存工具" },
|
|
217
|
+
{ cmd: "/tool_del_saved", desc: "删除持久化工具" },
|
|
218
|
+
{ cmd: "/temp", desc: "设置温度 0-2" },
|
|
219
|
+
{ cmd: "/token", desc: "设置最大输出token" },
|
|
220
|
+
{ cmd: "/memory", desc: "记忆统计" },
|
|
221
|
+
{ cmd: "/memory_list", desc: "列出记忆条目" },
|
|
222
|
+
{ cmd: "/memory_search", desc: "搜索记忆" },
|
|
223
|
+
{ cmd: "/memory_del", desc: "删除记忆" },
|
|
224
|
+
{ cmd: "/memory_clear", desc: "清空记忆" },
|
|
225
|
+
{ cmd: "/compress", desc: "手动压缩上下文" },
|
|
226
|
+
{ cmd: "/history", desc: "查看最近历史" },
|
|
227
|
+
{ cmd: "/history files", desc: "历史文件列表" },
|
|
228
|
+
{ cmd: "/history search", desc: "搜索历史" },
|
|
229
|
+
{ cmd: "/history read", desc: "读取历史文件" },
|
|
230
|
+
{ cmd: "/trusted", desc: "查看受信任工具" },
|
|
231
|
+
{ cmd: "/trust", desc: "永久信任工具" },
|
|
232
|
+
{ cmd: "/untrust", desc: "取消信任" },
|
|
233
|
+
{ cmd: "/api", desc: "查看/配置API" },
|
|
234
|
+
{ cmd: "/api key", desc: "设置API Key" },
|
|
235
|
+
{ cmd: "/api url", desc: "设置API地址" },
|
|
236
|
+
{ cmd: "/api model", desc: "设置模型" },
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
function App() {
|
|
240
|
+
const { stdout } = useStdout();
|
|
241
|
+
const cols = stdout ? stdout.columns : 80;
|
|
242
|
+
|
|
243
|
+
const [input, setInput] = useState("");
|
|
244
|
+
const [thinking, setThinking] = useState(false);
|
|
245
|
+
const [streaming, setStreaming] = useState("");
|
|
246
|
+
const [tools, setTools] = useState([]);
|
|
247
|
+
const [ctxPct, setCtxPct] = useState(15);
|
|
248
|
+
const [blocked, setBlocked] = useState(false);
|
|
249
|
+
const [slashIdx, setSlashIdx] = useState(0);
|
|
250
|
+
const [msgs, setMsgs] = useState([
|
|
251
|
+
{ role: "system", content: `欢迎来到 Clinn v${VER}!Ink 全屏终端界面。` },
|
|
252
|
+
{ role: "system", content: "按 / 查看命令菜单 ↑↓选择 回车选取" },
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
const addMsg = useCallback((role, text) => {
|
|
256
|
+
setMsgs(prev => {
|
|
257
|
+
const next = [...prev, { role, content: text }];
|
|
258
|
+
return next.length > MAX_MSG ? next.slice(-MAX_MSG) : next;
|
|
259
|
+
});
|
|
260
|
+
}, []);
|
|
261
|
+
|
|
262
|
+
const run = useCallback(async (query) => {
|
|
263
|
+
if (blocked) return;
|
|
264
|
+
setBlocked(true);
|
|
265
|
+
setThinking(true);
|
|
266
|
+
setStreaming("");
|
|
267
|
+
setTools([]);
|
|
268
|
+
|
|
269
|
+
let buf = "";
|
|
270
|
+
const toolMap = new Map();
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
await getAgent().run(query, {
|
|
274
|
+
onContent: (tok) => {
|
|
275
|
+
buf += tok;
|
|
276
|
+
setStreaming(buf.replace(/\*\*(.+?)\*\*/g, "$1"));
|
|
277
|
+
},
|
|
278
|
+
onToolCall: (name, args = {}) => {
|
|
279
|
+
const short = Object.entries(args).map(([k, v]) => {
|
|
280
|
+
const s = String(v);
|
|
281
|
+
return k + "=" + (s.length > 50 ? s.slice(0, 50) + "…" : s);
|
|
282
|
+
}).join(" ");
|
|
283
|
+
toolMap.set(name, { name, args: short, status: "running", result: "" });
|
|
284
|
+
setTools([...toolMap.values()]);
|
|
285
|
+
},
|
|
286
|
+
onToolResult: (name, result) => {
|
|
287
|
+
const t = toolMap.get(name);
|
|
288
|
+
if (t) {
|
|
289
|
+
t.status = "done";
|
|
290
|
+
t.result = String(result || "").replace(/\n/g, " ").slice(0, 150);
|
|
291
|
+
}
|
|
292
|
+
setTools([...toolMap.values()]);
|
|
293
|
+
},
|
|
294
|
+
onContextPct: (pct) => setCtxPct(pct),
|
|
295
|
+
});
|
|
296
|
+
} catch (e) {
|
|
297
|
+
addMsg("system", "错误: " + e.message);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (buf) addMsg("assistant", formatMd(buf, cols - 4));
|
|
301
|
+
setStreaming("");
|
|
302
|
+
setThinking(false);
|
|
303
|
+
setTools([]);
|
|
304
|
+
setBlocked(false);
|
|
305
|
+
}, [blocked, addMsg, cols]);
|
|
306
|
+
|
|
307
|
+
const slashRef = useRef({ filtered: [], clamped: 0 });
|
|
308
|
+
|
|
309
|
+
const handleSubmit = useCallback((val) => {
|
|
310
|
+
const v = val.trim();
|
|
311
|
+
if (!v || thinking) return;
|
|
312
|
+
const { filtered, clamped } = slashRef.current;
|
|
313
|
+
if (filtered.length > 0 && clamped < filtered.length) {
|
|
314
|
+
const sel = filtered[clamped].cmd;
|
|
315
|
+
if (v !== sel) {
|
|
316
|
+
setInput(sel + " ");
|
|
317
|
+
setSlashIdx(0);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
addMsg("user", v);
|
|
322
|
+
setInput("");
|
|
323
|
+
|
|
324
|
+
const parts = v.slice(1).trim().split(/\s+/);
|
|
325
|
+
const cmd = parts[0].toLowerCase();
|
|
326
|
+
const rest = parts.slice(1).join(" ");
|
|
327
|
+
|
|
328
|
+
const say = (t) => addMsg("system", t);
|
|
329
|
+
|
|
330
|
+
if (v === "/help") {
|
|
331
|
+
say([
|
|
332
|
+
"╭────────────────────────────────────╮",
|
|
333
|
+
"│ /help 查看帮助 │",
|
|
334
|
+
"│ /exit 退出程序 │",
|
|
335
|
+
"│ /reset 重置对话 │",
|
|
336
|
+
"│ /clear 清除对话 │",
|
|
337
|
+
"│ /version 查看版本 │",
|
|
338
|
+
"│ /status 当前状态 │",
|
|
339
|
+
"│ /ctx 上下文使用量 │",
|
|
340
|
+
"│ /tools 列出工具 │",
|
|
341
|
+
"│ /tools_more 全部工具 │",
|
|
342
|
+
"│ /tool_search <q> 搜索工具 │",
|
|
343
|
+
"│ /tool_list_saved 列出持久化工具 │",
|
|
344
|
+
"│ /tool_save <name> 持久化保存工具 │",
|
|
345
|
+
"│ /tool_del <name> 删除持久化工具 │",
|
|
346
|
+
"│ /temp <0-2> 设置温度 │",
|
|
347
|
+
"│ /token <n> 设置maxTokens │",
|
|
348
|
+
"│ /memory 记忆统计 │",
|
|
349
|
+
"│ /memory_list [n] 列出记忆条目 │",
|
|
350
|
+
"│ /memory_search <q> 搜索记忆 │",
|
|
351
|
+
"│ /memory_del <id> 删除记忆 │",
|
|
352
|
+
"│ /memory_clear 清空记忆 │",
|
|
353
|
+
"│ /compress 手动压缩上下文 │",
|
|
354
|
+
"│ /history [n] 最近历史 │",
|
|
355
|
+
"│ /history files 历史文件列表 │",
|
|
356
|
+
"│ /history search <q>搜索历史 │",
|
|
357
|
+
"│ /history read <f> 读取历史文件 │",
|
|
358
|
+
"│ /trusted 受信任工具 │",
|
|
359
|
+
"│ /trust <name> 永久信任工具 │",
|
|
360
|
+
"│ /untrust <name> 取消信任 │",
|
|
361
|
+
"│ /api 查看API配置 │",
|
|
362
|
+
"│ /api key <K> 设置Key │",
|
|
363
|
+
"│ /api url <U> 设置地址 │",
|
|
364
|
+
"│ /api model <M> 设置模型 │",
|
|
365
|
+
"╰────────────────────────────────────╯",
|
|
366
|
+
].join("\n"));
|
|
367
|
+
}
|
|
368
|
+
else if (cmd === "exit") process.exit(0);
|
|
369
|
+
else if (cmd === "reset") {
|
|
370
|
+
getAgent().reset();
|
|
371
|
+
setMsgs([{ role: "system", content: "对话已重置。" }]);
|
|
372
|
+
setCtxPct(0);
|
|
373
|
+
say("对话已重置 (历史+token计数已清零)");
|
|
374
|
+
}
|
|
375
|
+
else if (cmd === "clear") {
|
|
376
|
+
setMsgs([{ role: "system", content: "对话已清除。" }]);
|
|
377
|
+
setCtxPct(0);
|
|
378
|
+
}
|
|
379
|
+
else if (cmd === "version") {
|
|
380
|
+
say("Clinn v" + VER + " — Ink Edition · " + CONFIG.llm.model);
|
|
381
|
+
}
|
|
382
|
+
else if (cmd === "status") {
|
|
383
|
+
const a = getAgent();
|
|
384
|
+
const usage = a.getUsage();
|
|
385
|
+
const mem = a.memory.stats();
|
|
386
|
+
const pct = a.estimateContextPct();
|
|
387
|
+
const names = Tools.listToolNames();
|
|
388
|
+
say([
|
|
389
|
+
"╭────────────────────────────────────╮",
|
|
390
|
+
" 模型: " + CONFIG.llm.model + " 温度: " + CONFIG.llm.temperature,
|
|
391
|
+
" Tokens: 输入" + usage.prompt + " · 输出" + usage.completion,
|
|
392
|
+
" 上下文: " + pct + "% (上限 " + a.getMaxContextTokens() + " tokens)",
|
|
393
|
+
" 记忆: " + mem.entries + "/" + mem.maxEntries + " 条 · 历史 " + mem.historyMessages + " 轮",
|
|
394
|
+
" 工具: " + names.length + " 个 — " + names.slice(0, 8).join(", ") + (names.length > 8 ? " ..." : ""),
|
|
395
|
+
"╰────────────────────────────────────╯",
|
|
396
|
+
].join("\n"));
|
|
397
|
+
}
|
|
398
|
+
else if (cmd === "ctx") {
|
|
399
|
+
const a = getAgent();
|
|
400
|
+
const pct = a.estimateContextPct();
|
|
401
|
+
const usage = a.getUsage();
|
|
402
|
+
const barW = 30;
|
|
403
|
+
const filled = Math.max(1, Math.round(pct / 100 * barW));
|
|
404
|
+
const empty = barW - filled;
|
|
405
|
+
const bar = "█".repeat(filled) + "─".repeat(empty);
|
|
406
|
+
say([
|
|
407
|
+
"上下文: [" + bar + "] " + pct + "%",
|
|
408
|
+
"上限: " + a.getMaxContextTokens() + " tokens",
|
|
409
|
+
"累计: 输入" + usage.prompt + " · 输出" + usage.completion + " · 合计" + (usage.prompt + usage.completion),
|
|
410
|
+
">80% 时可 /compress 压缩或让 AI 调用 forget_conversation",
|
|
411
|
+
].join("\n"));
|
|
412
|
+
}
|
|
413
|
+
else if (cmd === "memory") {
|
|
414
|
+
const s = getAgent().memory.stats();
|
|
415
|
+
say("记忆: " + s.entries + "/" + s.maxEntries + " 条 · 历史消息 " + s.historyMessages + " 轮");
|
|
416
|
+
}
|
|
417
|
+
else if (cmd === "memory_list") {
|
|
418
|
+
const n = parseInt(rest) || 20;
|
|
419
|
+
const all = getAgent().memory.getAllEntries().slice(-n);
|
|
420
|
+
if (!all.length) { say("(无记忆条目)"); return; }
|
|
421
|
+
say(all.map((e) => "#" + e.id + " [" + (e.tags?.join(",") || "-") + "] " + e.text).join("\n"));
|
|
422
|
+
}
|
|
423
|
+
else if (cmd === "memory_search") {
|
|
424
|
+
if (!rest) { say("用法: /memory_search <关键词>"); return; }
|
|
425
|
+
const found = getAgent().memory.searchEntries(rest, 10);
|
|
426
|
+
if (!found.length) { say("未找到匹配 \"" + rest + "\" 的记忆"); return; }
|
|
427
|
+
say(found.map((e) => "#" + e.id + " [" + (e.tags?.join(",") || "-") + "] " + e.text).join("\n"));
|
|
428
|
+
}
|
|
429
|
+
else if (cmd === "memory_del") {
|
|
430
|
+
const id = parseInt(rest);
|
|
431
|
+
if (isNaN(id)) { say("用法: /memory_del <id>"); return; }
|
|
432
|
+
const ok = getAgent().memory.removeEntry(id);
|
|
433
|
+
say(ok ? "已删除 #" + id : "不存在 #" + id);
|
|
434
|
+
}
|
|
435
|
+
else if (cmd === "memory_clear") {
|
|
436
|
+
getAgent().memory.clearEntries();
|
|
437
|
+
say("记忆已全部清空");
|
|
438
|
+
}
|
|
439
|
+
else if (cmd === "compress") {
|
|
440
|
+
const a = getAgent();
|
|
441
|
+
const compressed = a.memory.compressHistory();
|
|
442
|
+
if (!compressed) { say("对话太短, 无需压缩"); return; }
|
|
443
|
+
a.memory.clear();
|
|
444
|
+
a.memory.addEntry("手动压缩: " + compressed.slice(0, 180), ["manual-summary"]);
|
|
445
|
+
say("上下文已压缩, 历史清空, 摘要已存入记忆");
|
|
446
|
+
}
|
|
447
|
+
else if (cmd === "tools") {
|
|
448
|
+
const names = Tools.listToolNames();
|
|
449
|
+
const base = 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", "exec_console", "wait_command", "web_fetch"].includes(n));
|
|
450
|
+
const lines = base.map((n) => {
|
|
451
|
+
const t = Tools.getTool(n);
|
|
452
|
+
return " " + n + (t?.description ? " — " + t.description.slice(0, 40) : "");
|
|
453
|
+
});
|
|
454
|
+
say(lines.join("\n") + "\n\n共 " + base.length + " 个核心工具, " + (names.length - base.length) + " 个扩展工具 (输入 /tools_more 查看全部)");
|
|
455
|
+
}
|
|
456
|
+
else if (cmd === "tools_more") {
|
|
457
|
+
const names = Tools.listToolNames();
|
|
458
|
+
say(names.map((n) => {
|
|
459
|
+
const t = Tools.getTool(n);
|
|
460
|
+
return " " + n + (t?.description ? " — " + t.description.slice(0, 60) : "");
|
|
461
|
+
}).join("\n") + "\n\n共 " + names.length + " 个工具");
|
|
462
|
+
}
|
|
463
|
+
else if (cmd === "tool_search") {
|
|
464
|
+
if (!rest) { say("用法: /tool_search <关键词>"); return; }
|
|
465
|
+
const found = Tools.searchToolRegistry(rest);
|
|
466
|
+
if (!found.length) { say("未找到匹配 \"" + rest + "\" 的工具"); return; }
|
|
467
|
+
say(found.map((t) => t.name + " — " + t.description).join("\n"));
|
|
468
|
+
}
|
|
469
|
+
else if (cmd === "tool_list_saved") {
|
|
470
|
+
const saved = Tools.listCustomTools();
|
|
471
|
+
if (!saved.length) { say("(无持久化工具)"); return; }
|
|
472
|
+
say(saved.map((s) => "[" + s.file + "] 导出: " + s.exports.join(", ")).join("\n"));
|
|
473
|
+
}
|
|
474
|
+
else if (cmd === "tool_del_saved") {
|
|
475
|
+
if (!rest) { say("用法: /tool_del_saved <name>"); return; }
|
|
476
|
+
const result = Tools.deleteToolFile(rest);
|
|
477
|
+
if (result.startsWith("[OK]")) getAgent().refreshTools();
|
|
478
|
+
say(result);
|
|
479
|
+
}
|
|
480
|
+
else if (cmd === "temp") {
|
|
481
|
+
const n = parseFloat(rest);
|
|
482
|
+
if (isNaN(n) || n < 0 || n > 2) { say("温度范围 0-2"); return; }
|
|
483
|
+
CONFIG.llm.temperature = n; saveConfig(CONFIG);
|
|
484
|
+
say("温度已设为 " + n);
|
|
485
|
+
}
|
|
486
|
+
else if (cmd === "token") {
|
|
487
|
+
const n = parseInt(rest, 10);
|
|
488
|
+
if (isNaN(n) || n < 1 || n > 128000) { say("范围 1-128000"); return; }
|
|
489
|
+
CONFIG.llm.maxTokens = n; saveConfig(CONFIG);
|
|
490
|
+
say("MaxTokens 已设为 " + n);
|
|
491
|
+
}
|
|
492
|
+
else if (cmd === "history") {
|
|
493
|
+
const subParts = rest.trim().split(/\s+/);
|
|
494
|
+
const sub = subParts[0]?.toLowerCase();
|
|
495
|
+
const arg = subParts.slice(1).join(" ");
|
|
496
|
+
if (sub === "files") {
|
|
497
|
+
const files = getFileList();
|
|
498
|
+
if (!files.length) { say("(暂无历史文件)"); return; }
|
|
499
|
+
say(files.map((f) => f.file + " | " + f.turns + "轮 | " + f.size + "KB | " + f.created.slice(0, 10)).join("\n"));
|
|
500
|
+
} else if (sub === "search") {
|
|
501
|
+
if (!arg) { say("用法: /history search <关键词>"); return; }
|
|
502
|
+
const results = searchHistory(arg, 10);
|
|
503
|
+
if (!results.length) { say("未找到匹配 \"" + arg + "\""); return; }
|
|
504
|
+
say(results.map((r, i) => (i + 1) + ". [" + r.file + "] " + (r.time?.slice(0, 16) || "?") + "\n " + r.user.slice(0, 120)).join("\n\n"));
|
|
505
|
+
} else if (sub === "read") {
|
|
506
|
+
if (!arg) { say("用法: /history read <文件名>"); return; }
|
|
507
|
+
const turns = loadFileTurns(arg, 10);
|
|
508
|
+
if (!turns.length) { say("找不到文件 " + arg); return; }
|
|
509
|
+
say(turns.map((t, i) => (i + 1) + ". [" + (t.time?.slice(0, 16) || "?") + "]\n 问: " + (t.user || "").slice(0, 200) + "\n 答: " + (t.assistant || "").slice(0, 200)).join("\n\n"));
|
|
510
|
+
} else {
|
|
511
|
+
const n = parseInt(sub) || 10;
|
|
512
|
+
const turns = listRecentTurns(n);
|
|
513
|
+
if (!turns.length) { say("(暂无历史记录)"); return; }
|
|
514
|
+
say(turns.map((t, i) => (i + 1) + ". [" + (t.time?.slice(0, 16) || "?") + "] " + (t.user || "").slice(0, 120)).join("\n"));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
else if (cmd === "trusted") {
|
|
518
|
+
const trusted = CONFIG.tools?.trustedTools || [];
|
|
519
|
+
if (!trusted.length) { say("(暂无受信任工具)"); return; }
|
|
520
|
+
say("受信任工具 (" + trusted.length + " 个):\n" + trusted.join("\n"));
|
|
521
|
+
}
|
|
522
|
+
else if (cmd === "trust") {
|
|
523
|
+
if (!rest) { say("用法: /trust <工具名>"); return; }
|
|
524
|
+
Tools.addTrusted(rest);
|
|
525
|
+
CONFIG.tools.trustedTools = [...new Set([...(CONFIG.tools?.trustedTools || []), rest])];
|
|
526
|
+
saveConfig(CONFIG);
|
|
527
|
+
say("已永久信任 " + rest);
|
|
528
|
+
}
|
|
529
|
+
else if (cmd === "untrust") {
|
|
530
|
+
if (!rest) { say("用法: /untrust <工具名>"); return; }
|
|
531
|
+
Tools.removeTrusted(rest);
|
|
532
|
+
CONFIG.tools.trustedTools = (CONFIG.tools?.trustedTools || []).filter((n) => n !== rest);
|
|
533
|
+
saveConfig(CONFIG);
|
|
534
|
+
say("已取消信任 " + rest);
|
|
535
|
+
}
|
|
536
|
+
else if (cmd === "api") {
|
|
537
|
+
const sub = rest.split(/\s+/)[0]?.toLowerCase();
|
|
538
|
+
const val = rest.slice(sub ? sub.length : 0).trim();
|
|
539
|
+
if (sub === "key") {
|
|
540
|
+
if (!val) { say("用法: /api key <Key>"); return; }
|
|
541
|
+
if (val.length < 10) { say("Key 太短"); return; }
|
|
542
|
+
CONFIG.llm.apiKey = val; saveConfig(CONFIG);
|
|
543
|
+
say("API Key 已更新 (" + val.slice(0, 8) + "...)");
|
|
544
|
+
} else if (sub === "url") {
|
|
545
|
+
if (!val) { say("用法: /api url <地址>"); return; }
|
|
546
|
+
CONFIG.llm.baseURL = val.replace(/\/+$/, ""); saveConfig(CONFIG);
|
|
547
|
+
say("API 地址已更新: " + CONFIG.llm.baseURL);
|
|
548
|
+
} else if (sub === "model") {
|
|
549
|
+
if (!val) { say("用法: /api model <模型名>"); return; }
|
|
550
|
+
CONFIG.llm.model = val; saveConfig(CONFIG);
|
|
551
|
+
say("模型已更新: " + CONFIG.llm.model);
|
|
552
|
+
} else {
|
|
553
|
+
const masked = CONFIG.llm.apiKey ? CONFIG.llm.apiKey.slice(0, 8) + "..." + CONFIG.llm.apiKey.slice(-4) : "(未设置)";
|
|
554
|
+
say([
|
|
555
|
+
"API 设置",
|
|
556
|
+
" Key: " + masked,
|
|
557
|
+
" URL: " + CONFIG.llm.baseURL,
|
|
558
|
+
" Model: " + CONFIG.llm.model,
|
|
559
|
+
" 温度: " + CONFIG.llm.temperature + " | MaxTokens: " + (CONFIG.llm.maxTokens || "-"),
|
|
560
|
+
" 设置: /api key <K> | /api url <U> | /api model <M>",
|
|
561
|
+
].join("\n"));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
else { run(v); }
|
|
565
|
+
}, [thinking, addMsg, run]);
|
|
566
|
+
|
|
567
|
+
const slashInput = input.startsWith("/");
|
|
568
|
+
const slashFiltered = slashInput
|
|
569
|
+
? COMMANDS.filter(c => c.cmd.startsWith(input) || input === "/" || c.cmd.includes(input))
|
|
570
|
+
: [];
|
|
571
|
+
const slashClamped = Math.max(0, Math.min(slashIdx, slashFiltered.length - 1));
|
|
572
|
+
slashRef.current = { filtered: slashFiltered, clamped: slashClamped };
|
|
573
|
+
|
|
574
|
+
useInput((_, key) => {
|
|
575
|
+
if (key.escape) process.exit(0);
|
|
576
|
+
if (!slashInput || !slashFiltered.length) return;
|
|
577
|
+
if (key.upArrow) setSlashIdx((slashClamped - 1 + slashFiltered.length) % slashFiltered.length);
|
|
578
|
+
if (key.downArrow || key.tab) setSlashIdx((slashClamped + 1) % slashFiltered.length);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
return (
|
|
582
|
+
<Box borderStyle="round" borderColor="cyan" flexDirection="column" paddingX={1}>
|
|
583
|
+
<Box flexDirection="column" flexShrink={0}>
|
|
584
|
+
{LOGO_LINES.map((line, i) => (
|
|
585
|
+
<Text key={i} color="cyanBright">{line}</Text>
|
|
586
|
+
))}
|
|
587
|
+
<Box marginTop={1}>
|
|
588
|
+
<Text color="magenta" dimColor>Self-Evolving AI · Terminal Agent · Ink</Text>
|
|
589
|
+
</Box>
|
|
590
|
+
<Text color="gray">{"─".repeat(cols - 4)}</Text>
|
|
591
|
+
</Box>
|
|
592
|
+
|
|
593
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
594
|
+
{msgs.map((m, i) => (
|
|
595
|
+
<Msg key={i} role={m.role} content={m.content} />
|
|
596
|
+
))}
|
|
597
|
+
{streaming ? <Streaming content={streaming} /> : null}
|
|
598
|
+
{thinking && !streaming ? (
|
|
599
|
+
<Box paddingLeft={3}>
|
|
600
|
+
<Text color="cyan"><Spinner type="dots" /> 思考中...</Text>
|
|
601
|
+
</Box>
|
|
602
|
+
) : null}
|
|
603
|
+
<ToolLog tools={tools} />
|
|
604
|
+
</Box>
|
|
605
|
+
|
|
606
|
+
{slashInput && slashFiltered.length > 0 && (
|
|
607
|
+
<Box flexDirection="column" paddingLeft={2} marginBottom={0}>
|
|
608
|
+
{slashFiltered.map((c, i) => {
|
|
609
|
+
const sel = i === slashClamped;
|
|
610
|
+
return (
|
|
611
|
+
<Box key={c.cmd} paddingLeft={1}>
|
|
612
|
+
<Text color={sel ? "cyan" : "gray"}>{sel ? "▸ " : " "}</Text>
|
|
613
|
+
<Text color={sel ? "cyanBright" : undefined} bold={sel}>{c.cmd}</Text>
|
|
614
|
+
<Text color="gray">{" · " + c.desc}</Text>
|
|
615
|
+
</Box>
|
|
616
|
+
);
|
|
617
|
+
})}
|
|
618
|
+
<Box paddingLeft={1}>
|
|
619
|
+
<Text color="gray"> ↑↓ 切换 · 回车 选取</Text>
|
|
620
|
+
</Box>
|
|
621
|
+
</Box>
|
|
622
|
+
)}
|
|
623
|
+
|
|
624
|
+
<Box flexDirection="column" flexShrink={0}>
|
|
625
|
+
<Text color="gray">{"─".repeat(cols - 4)}</Text>
|
|
626
|
+
<Box paddingX={1}>
|
|
627
|
+
<Text dimColor>
|
|
628
|
+
<Text color="cyan">(ΦωΦ)</Text> {CONFIG.llm.model}
|
|
629
|
+
{" │ "}
|
|
630
|
+
<Text color="magenta">msg</Text> {msgs.length}
|
|
631
|
+
{" │ "}
|
|
632
|
+
<Text color={ctxPct > 80 ? "yellow" : ctxPct > 90 ? "red" : "green"}>ctx</Text> {ctxPct}%
|
|
633
|
+
{" │ "}
|
|
634
|
+
F1帮助 · Esc退出
|
|
635
|
+
</Text>
|
|
636
|
+
</Box>
|
|
637
|
+
|
|
638
|
+
<Box paddingY={1}>
|
|
639
|
+
<Box borderStyle="round" borderColor="cyan" paddingX={1} width={cols - 4}>
|
|
640
|
+
<Text color="greenBright" bold>("・ω・)ノ </Text>
|
|
641
|
+
<TextInput
|
|
642
|
+
value={input}
|
|
643
|
+
onChange={v => { setInput(v); setSlashIdx(0); }}
|
|
644
|
+
onSubmit={handleSubmit}
|
|
645
|
+
placeholder={thinking ? "思考中,请稍候..." : "输入你的问题,或按 / 查看命令…"}
|
|
646
|
+
/>
|
|
647
|
+
</Box>
|
|
648
|
+
</Box>
|
|
649
|
+
</Box>
|
|
650
|
+
</Box>
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const { waitUntilExit } = render(<App />);
|
package/Src/{llm.js → llm.cjs}
RENAMED
|
@@ -88,6 +88,14 @@ class LLMClient {
|
|
|
88
88
|
let data = "";
|
|
89
89
|
res.on("data", (chunk) => (data += chunk));
|
|
90
90
|
res.on("end", () => {
|
|
91
|
+
if (res.statusCode === 401) {
|
|
92
|
+
reject(new Error("API Key 无效或未设置 — 请用 /api key <你的Key> 设置"));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (res.statusCode === 403) {
|
|
96
|
+
reject(new Error("API Key 无权限 (403 Forbidden)"));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
91
99
|
try {
|
|
92
100
|
const json = JSON.parse(data);
|
|
93
101
|
if (json.error) {
|
|
@@ -123,6 +131,27 @@ class LLMClient {
|
|
|
123
131
|
|
|
124
132
|
return new Promise((resolve, reject) => {
|
|
125
133
|
const req = https.request(options, (res) => {
|
|
134
|
+
if (res.statusCode === 401) {
|
|
135
|
+
reject(new Error("API Key 无效或未设置 — 请用 /api key <你的Key> 设置"));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (res.statusCode === 403) {
|
|
139
|
+
reject(new Error("API Key 无权限 (403 Forbidden)"));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (res.statusCode >= 400) {
|
|
143
|
+
let body = "";
|
|
144
|
+
res.on("data", (c) => (body += c));
|
|
145
|
+
res.on("end", () => {
|
|
146
|
+
try {
|
|
147
|
+
const j = JSON.parse(body);
|
|
148
|
+
reject(new Error(j.error?.message || `HTTP ${res.statusCode}`));
|
|
149
|
+
} catch (_) {
|
|
150
|
+
reject(new Error(`HTTP ${res.statusCode}: ${body.slice(0, 200)}`));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
126
155
|
let buffer = "";
|
|
127
156
|
const toolCallsMap = {};
|
|
128
157
|
let content = "";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
package/bin/clinn.bat
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
@echo off
|
|
2
|
-
node "%~dp0\..\Src\index.
|
|
2
|
+
node --import tsx "%~dp0\..\Src\index.jsx" %*
|
package/bin/clinn.js
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
require("
|
|
2
|
+
const { spawnSync } = require("child_process");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const srcIndex = path.join(__dirname, "..", "Src", "index.jsx");
|
|
6
|
+
|
|
7
|
+
const result = spawnSync(process.execPath, [
|
|
8
|
+
"--import", "tsx",
|
|
9
|
+
srcIndex,
|
|
10
|
+
...process.argv.slice(2),
|
|
11
|
+
], { stdio: "inherit" });
|
|
12
|
+
|
|
13
|
+
process.exit(result.status ?? 1);
|
package/bin/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
package/config.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"agent": {
|
|
3
3
|
"name": "Clinn",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.15",
|
|
5
5
|
"description": "控制台智能体助手"
|
|
6
6
|
},
|
|
7
7
|
"llm": {
|
|
8
8
|
"provider": "deepseek",
|
|
9
|
-
"apiKey": "
|
|
9
|
+
"apiKey": "sk-30a6d324dfa54b3faa6998404db47264",
|
|
10
10
|
"baseURL": "https://api.deepseek.com/v1",
|
|
11
11
|
"model": "deepseek-v4-pro",
|
|
12
12
|
"maxTokens": 8000,
|
|
@@ -6,7 +6,7 @@ const fs = require("fs");
|
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const { execSync } = require("child_process");
|
|
8
8
|
|
|
9
|
-
const VER = "0.
|
|
9
|
+
const VER = "0.8.0";
|
|
10
10
|
const G = "\x1b[0;32m", C = "\x1b[0;36m", Y = "\x1b[0;33m", R = "\x1b[0;31m", N = "\x1b[0m", D = "\x1b[2m";
|
|
11
11
|
|
|
12
12
|
const IS_WIN = process.platform === "win32";
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghenya/clinn",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "终端原生 AI 编程助手 — DeepSeek 驱动,50+
|
|
5
|
-
"main": "Src/index.
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "终端原生 AI 编程助手 — DeepSeek 驱动,50+ 工具,对话记忆,虚拟浏览器,Ink 全屏界面",
|
|
5
|
+
"main": "Src/index.jsx",
|
|
6
6
|
"bin": {
|
|
7
7
|
"clinn": "bin/clinn.js"
|
|
8
8
|
},
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
"bin/",
|
|
15
15
|
"config.json",
|
|
16
16
|
"README.md",
|
|
17
|
-
"install.
|
|
17
|
+
"install.cjs",
|
|
18
18
|
"install.sh",
|
|
19
19
|
"install.bat"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
-
"postinstall": "node install.
|
|
23
|
-
"start": "
|
|
24
|
-
"lint": "node --check Src/index.js && node --check
|
|
22
|
+
"postinstall": "node install.cjs",
|
|
23
|
+
"start": "npx tsx Src/index.jsx",
|
|
24
|
+
"lint": "node --check Src/agent.cjs && node --check Src/llm.cjs && node --check Tools/index.js && node --check Tools/extended_tools.js && node --check Tools/search_tools.js && node --check Tools/browser.js && node --check Tools/file_tools.js && node --check Tools/edit_tools.js && node --check Tools/tokenizer.js && node --check Mem/index.js && node --check Mem/history.js"
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
27
27
|
"ai",
|
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
"terminal",
|
|
32
32
|
"coding-assistant",
|
|
33
33
|
"llm",
|
|
34
|
-
"chatbot"
|
|
34
|
+
"chatbot",
|
|
35
|
+
"ink",
|
|
36
|
+
"react",
|
|
37
|
+
"tui"
|
|
35
38
|
],
|
|
36
39
|
"author": "PillowBots",
|
|
37
40
|
"license": "MIT",
|
|
@@ -43,11 +46,16 @@
|
|
|
43
46
|
"bugs": {
|
|
44
47
|
"url": "https://github.com/PillowBots/clinn/issues"
|
|
45
48
|
},
|
|
46
|
-
"type": "
|
|
49
|
+
"type": "module",
|
|
47
50
|
"engines": {
|
|
48
|
-
"node": ">=
|
|
51
|
+
"node": ">=18.0.0"
|
|
49
52
|
},
|
|
50
53
|
"dependencies": {
|
|
51
|
-
"puppeteer-core": "^25.1.0"
|
|
54
|
+
"puppeteer-core": "^25.1.0",
|
|
55
|
+
"ink": "^5.2.0",
|
|
56
|
+
"ink-text-input": "^6.0.0",
|
|
57
|
+
"ink-spinner": "^5.0.0",
|
|
58
|
+
"react": "^18.3.1",
|
|
59
|
+
"tsx": "^4.0.0"
|
|
52
60
|
}
|
|
53
|
-
}
|
|
61
|
+
}
|