@ghenya/clinn 0.8.8 → 0.9.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/history.js +326 -24
- package/Mem/index.js +123 -14
- package/Src/agent.cjs +74 -11
- package/Src/index.js +81 -8
- package/Src/index.jsx +366 -108
- package/Tools/extended_tools.js +83 -8
- package/Tools/index.js +1 -1
- package/Tools/search_tools.js +55 -1
- package/config.json +1 -1
- package/package.json +8 -8
package/Tools/extended_tools.js
CHANGED
|
@@ -182,26 +182,101 @@ const globTool = {
|
|
|
182
182
|
|
|
183
183
|
const grepTool = {
|
|
184
184
|
name: "grep",
|
|
185
|
-
description: "
|
|
185
|
+
description: "超快速内容搜索(优先使用ripgrep). 支持上下文行/files_only/count三种输出模式. 比系统grep快5-10x",
|
|
186
186
|
parameters: {
|
|
187
|
-
pattern: { type: "string", required: true, description: "
|
|
187
|
+
pattern: { type: "string", required: true, description: "正则表达式搜索模式" },
|
|
188
188
|
dirPath: { type: "string", required: false, description: "搜索目录, 默认当前目录" },
|
|
189
|
-
glob: { type: "string", required: false, description: "
|
|
189
|
+
glob: { type: "string", required: false, description: "文件过滤, 如 '*.js' 或 '*.{ts,tsx}'" },
|
|
190
190
|
headLimit: { type: "number", required: false, description: "结果上限, 默认50" },
|
|
191
191
|
ignoreCase: { type: "boolean", required: false, description: "忽略大小写, 默认true" },
|
|
192
|
+
context: { type: "number", required: false, description: "上下文行数(前后各N行), 默认0" },
|
|
193
|
+
outputMode: { type: "string", required: false, description: "输出模式: content(默认)/files_only/count" },
|
|
192
194
|
},
|
|
193
|
-
execute: async ({ pattern, dirPath, glob, headLimit, ignoreCase }) => {
|
|
194
|
-
const root = dirPath || process.cwd();
|
|
195
|
+
execute: async ({ pattern, dirPath, glob, headLimit, ignoreCase, context, outputMode }) => {
|
|
196
|
+
const root = path.resolve(dirPath || process.cwd());
|
|
195
197
|
if (!fs.existsSync(root)) return `[不存在] ${root}`;
|
|
196
198
|
const max = headLimit || 50;
|
|
197
199
|
const ic = ignoreCase !== false;
|
|
200
|
+
const mode = outputMode || "content";
|
|
201
|
+
|
|
202
|
+
// 优先使用 ripgrep (更快, 支持 --json + 上下文)
|
|
203
|
+
const rgPath = (() => {
|
|
204
|
+
try { return execSync("which rg", { encoding: "utf-8", timeout: 2000 }).trim(); }
|
|
205
|
+
catch (_) { return null; }
|
|
206
|
+
})();
|
|
207
|
+
|
|
208
|
+
if (rgPath) {
|
|
209
|
+
const args = ["--json"];
|
|
210
|
+
if (ic) args.push("-i");
|
|
211
|
+
if (glob) args.push("-g", glob);
|
|
212
|
+
args.push("-m", String(max));
|
|
213
|
+
if (context && context > 0) args.push("-C", String(context));
|
|
214
|
+
args.push("--no-ignore-vcs");
|
|
215
|
+
args.push("-g", "!node_modules");
|
|
216
|
+
args.push("-g", "!.git");
|
|
217
|
+
args.push("-g", "!__pycache__");
|
|
218
|
+
args.push("-g", "!.DS_Store");
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const stdout = execSync(
|
|
222
|
+
`rg ${args.map(a => JSON.stringify(a)).join(" ")} ${JSON.stringify(pattern)} ${JSON.stringify(root)}`,
|
|
223
|
+
{ encoding: "utf-8", timeout: 30000, maxBuffer: 4 * 1024 * 1024 }
|
|
224
|
+
);
|
|
225
|
+
if (!stdout.trim()) return `[无匹配] "${pattern}" 在 ${root}`;
|
|
226
|
+
|
|
227
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
228
|
+
|
|
229
|
+
if (mode === "count") {
|
|
230
|
+
const counts = {};
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
try {
|
|
233
|
+
const obj = JSON.parse(line);
|
|
234
|
+
if (obj.type === "match") counts[obj.data.path.text] = (counts[obj.data.path.text] || 0) + 1;
|
|
235
|
+
} catch (_) {}
|
|
236
|
+
}
|
|
237
|
+
return Object.entries(counts).map(([f, c]) => `${f}: ${c} 处匹配`).join("\n");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (mode === "files_only") {
|
|
241
|
+
const files = new Set();
|
|
242
|
+
for (const line of lines) {
|
|
243
|
+
try {
|
|
244
|
+
const obj = JSON.parse(line);
|
|
245
|
+
if (obj.type === "match" || obj.type === "begin") files.add(obj.data.path.text);
|
|
246
|
+
} catch (_) {}
|
|
247
|
+
}
|
|
248
|
+
return [...files].join("\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// content 模式
|
|
252
|
+
const results = [];
|
|
253
|
+
for (const line of lines) {
|
|
254
|
+
try {
|
|
255
|
+
const obj = JSON.parse(line);
|
|
256
|
+
if (obj.type === "match") {
|
|
257
|
+
const d = obj.data;
|
|
258
|
+
results.push(`${d.path.text}:${d.line_number}: ${d.lines.text.trimEnd()}`);
|
|
259
|
+
}
|
|
260
|
+
} catch (_) {}
|
|
261
|
+
}
|
|
262
|
+
if (results.length === 0) return `[无匹配] "${pattern}" 在 ${root}`;
|
|
263
|
+
return results.join("\n");
|
|
264
|
+
} catch (e) {
|
|
265
|
+
if (e.status === 1) return `[无匹配] "${pattern}" 在 ${root}`;
|
|
266
|
+
// ripgrep 失败, 回退到系统 grep
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 回退: 系统 grep
|
|
198
271
|
const globFilter = glob ? ` --include="${glob}"` : "";
|
|
199
272
|
const icFlag = ic ? " -i" : "";
|
|
273
|
+
const ctxFlag = context && context > 0 ? ` -C ${context}` : "";
|
|
200
274
|
try {
|
|
201
|
-
const cmd = `grep -rn${icFlag}${globFilter} -m ${max} "${pattern.replace(/"/g, '\\"')}" "${root}"`;
|
|
275
|
+
const cmd = `grep -rn${icFlag}${ctxFlag}${globFilter} -m ${max} "${pattern.replace(/"/g, '\\"')}" "${root}"`;
|
|
202
276
|
const output = execSync(cmd, { cwd: root, encoding: "utf-8", timeout: 15000, maxBuffer: 2 * 1024 * 1024 });
|
|
203
|
-
const lines = output.trim().split("\n").slice(0, max);
|
|
204
|
-
|
|
277
|
+
const lines = output.trim().split("\n").slice(0, mode === "count" ? 9999 : max);
|
|
278
|
+
if (lines.length === 0 || (lines.length === 1 && !lines[0])) return `[无匹配] "${pattern}" 在 ${root}`;
|
|
279
|
+
return lines.join("\n");
|
|
205
280
|
} catch (e) {
|
|
206
281
|
if (e.stdout) return e.stdout.trim().split("\n").slice(0, max).join("\n");
|
|
207
282
|
return `[无匹配] "${pattern}" 在 ${root}`;
|
package/Tools/index.js
CHANGED
|
@@ -241,7 +241,7 @@ function filterToolDeclarations(prompt) {
|
|
|
241
241
|
return toFunctionDeclarations();
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
const alwaysInclude = ["search_memory", "save_memory", "list_memory", "delete_memory", "search_history", "list_history_files", "browse_page", "browse_page_text", "compress_context", "forget_conversation", "restart_session", "todo_write"];
|
|
244
|
+
const alwaysInclude = ["search_memory", "save_memory", "list_memory", "delete_memory", "mem_rom", "mem_ram", "search_history", "list_history_files", "list_sessions", "view_session", "search_sessions", "browse_page", "browse_page_text", "compress_context", "forget_conversation", "restart_session", "todo_write"];
|
|
245
245
|
for (const name of alwaysInclude) {
|
|
246
246
|
if (toolRegistry[name]) matched.add(name);
|
|
247
247
|
}
|
package/Tools/search_tools.js
CHANGED
|
@@ -172,7 +172,7 @@ const searchMemoryTool = {
|
|
|
172
172
|
|
|
173
173
|
const saveMemoryTool = {
|
|
174
174
|
name: "save_memory",
|
|
175
|
-
description: "
|
|
175
|
+
description: "保存一条关键信息到 ROM(永久记忆, 重启不丢). 等价于 mem_rom",
|
|
176
176
|
parameters: {
|
|
177
177
|
content: { type: "string", required: true, description: "记忆内容" },
|
|
178
178
|
tags: { type: "string", required: false, description: "标签,逗号分隔" },
|
|
@@ -180,6 +180,26 @@ const saveMemoryTool = {
|
|
|
180
180
|
execute: () => { return "save_memory must be injected"; },
|
|
181
181
|
};
|
|
182
182
|
|
|
183
|
+
const memRomTool = {
|
|
184
|
+
name: "mem_rom",
|
|
185
|
+
description: "保存一条 ROM 记忆(永久保存, 写入磁盘, 重启后仍存在). 用于身份、偏好、配置等跨会话信息",
|
|
186
|
+
parameters: {
|
|
187
|
+
content: { type: "string", required: true, description: "记忆内容, 不超过200字" },
|
|
188
|
+
tags: { type: "string", required: false, description: "标签,逗号分隔, 如 habit,config" },
|
|
189
|
+
},
|
|
190
|
+
execute: () => { return "mem_rom must be injected"; },
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const memRamTool = {
|
|
194
|
+
name: "mem_ram",
|
|
195
|
+
description: "保存一条 RAM 记忆(仅本次窗口, 关闭即丢失). 用于临时上下文、当前任务状态等",
|
|
196
|
+
parameters: {
|
|
197
|
+
content: { type: "string", required: true, description: "记忆内容, 不超过200字" },
|
|
198
|
+
tags: { type: "string", required: false, description: "标签,逗号分隔" },
|
|
199
|
+
},
|
|
200
|
+
execute: () => { return "mem_ram must be injected"; },
|
|
201
|
+
};
|
|
202
|
+
|
|
183
203
|
const listMemoryTool = {
|
|
184
204
|
name: "list_memory",
|
|
185
205
|
description: "列出所有记忆条目",
|
|
@@ -215,6 +235,35 @@ const listHistoryTool = {
|
|
|
215
235
|
execute: () => { return "list_history_files must be injected"; },
|
|
216
236
|
};
|
|
217
237
|
|
|
238
|
+
const listSessionsTool = {
|
|
239
|
+
name: "list_sessions",
|
|
240
|
+
description: "列出所有对话 session(会话分组),每次 /new 或重启算一个新 session。可以看到过去的对话主题、时间、轮数。用这个来回答「之前聊过什么」类问题。",
|
|
241
|
+
parameters: {
|
|
242
|
+
limit: { type: "number", required: false, description: "返回数量, 默认20" },
|
|
243
|
+
},
|
|
244
|
+
execute: () => { return "list_sessions must be injected"; },
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const viewSessionTool = {
|
|
248
|
+
name: "view_session",
|
|
249
|
+
description: "查看某个 session 的完整对话内容,包括用户消息和 AI 回复。用 list_sessions 获取 session_id 后再调用此工具。",
|
|
250
|
+
parameters: {
|
|
251
|
+
session_id: { type: "string", required: true, description: "Session ID (从 list_sessions 获取)" },
|
|
252
|
+
limit: { type: "number", required: false, description: "返回轮数上限, 默认50" },
|
|
253
|
+
},
|
|
254
|
+
execute: () => { return "view_session must be injected"; },
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const searchSessionsTool = {
|
|
258
|
+
name: "search_sessions",
|
|
259
|
+
description: "在全部历史 session 中全文搜索。比 search_history 更好——会按 session 分组、显示匹配得分和上下文。用这个来回答「你记得关于 X 的哪次对话?」类问题。",
|
|
260
|
+
parameters: {
|
|
261
|
+
query: { type: "string", required: true, description: "搜索关键词,支持多词空格分隔" },
|
|
262
|
+
limit: { type: "number", required: false, description: "返回数量, 默认10" },
|
|
263
|
+
},
|
|
264
|
+
execute: () => { return "search_sessions must be injected"; },
|
|
265
|
+
};
|
|
266
|
+
|
|
218
267
|
const compressContextTool = {
|
|
219
268
|
name: "compress_context",
|
|
220
269
|
description: "压缩当前上下文: 将历史对话摘要存入记忆并清空对话历史",
|
|
@@ -268,10 +317,15 @@ module.exports = {
|
|
|
268
317
|
set_timer: setTimerTool,
|
|
269
318
|
search_memory: searchMemoryTool,
|
|
270
319
|
save_memory: saveMemoryTool,
|
|
320
|
+
mem_rom: memRomTool,
|
|
321
|
+
mem_ram: memRamTool,
|
|
271
322
|
list_memory: listMemoryTool,
|
|
272
323
|
delete_memory: deleteMemoryTool,
|
|
273
324
|
search_history: searchHistoryTool,
|
|
274
325
|
list_history_files: listHistoryTool,
|
|
326
|
+
list_sessions: listSessionsTool,
|
|
327
|
+
view_session: viewSessionTool,
|
|
328
|
+
search_sessions: searchSessionsTool,
|
|
275
329
|
compress_context: compressContextTool,
|
|
276
330
|
agent_self_invoke: agentSelfInvokeTool,
|
|
277
331
|
save_tool: saveToolTool,
|
package/config.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghenya/clinn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "终端原生 AI 编程助手 — DeepSeek 驱动,50+ 工具,对话记忆,虚拟浏览器,Ink 全屏界面",
|
|
5
5
|
"main": "Src/index.jsx",
|
|
6
6
|
"bin": {
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"Tools/",
|
|
12
12
|
"Mem/",
|
|
13
13
|
"Logos/",
|
|
14
|
-
"Skills/",
|
|
15
14
|
"bin/",
|
|
16
15
|
"config.json",
|
|
17
16
|
"README.md",
|
|
18
17
|
"install.cjs",
|
|
19
18
|
"install.sh",
|
|
20
|
-
"install.bat"
|
|
19
|
+
"install.bat",
|
|
20
|
+
"Skills/"
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"postinstall": "node install.cjs",
|
|
@@ -52,11 +52,11 @@
|
|
|
52
52
|
"node": ">=18.0.0"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"
|
|
56
|
-
"ink": "^5.2.0",
|
|
57
|
-
"ink-text-input": "^6.0.0",
|
|
55
|
+
"ink": "^7.0.5",
|
|
58
56
|
"ink-spinner": "^5.0.0",
|
|
59
|
-
"
|
|
57
|
+
"ink-text-input": "^6.0.0",
|
|
58
|
+
"puppeteer-core": "^25.1.0",
|
|
59
|
+
"react": "^19.2.7",
|
|
60
60
|
"tsx": "^4.0.0"
|
|
61
61
|
}
|
|
62
|
-
}
|
|
62
|
+
}
|