@ghenya/clinn 0.8.7 → 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/Src/agent.cjs CHANGED
@@ -2,7 +2,12 @@ const os = require("os");
2
2
  const LLMClient = require("./llm.cjs");
3
3
  const Tools = require("../Tools");
4
4
  const { ConversationMemory } = require("../Mem");
5
- const { saveTurn, searchHistory, getFileList, loadFileTurns } = require("../Mem/history");
5
+ const {
6
+ saveTurn, searchHistory, getFileList, loadFileTurns,
7
+ startSession, endSession, listSessions, getSession,
8
+ loadSessionTurns, searchSessions, globalSearch,
9
+ migrateLegacySessions,
10
+ } = require("../Mem/history");
6
11
 
7
12
  const MAX_ITERATIONS = 500;
8
13
  const MAX_TOOL_RESULT_CHARS = 3000;
@@ -84,6 +89,10 @@ class Agent {
84
89
  this.maxContextTokens = (config.llm?.contextWindow || getContextWindow(config.llm?.model)) * 0.8;
85
90
  this._lastToolCalls = [];
86
91
 
92
+ // session management
93
+ migrateLegacySessions();
94
+ this.sessionId = startSession();
95
+
87
96
  Tools.setTrusted(config.tools?.trustedTools || []);
88
97
  Tools.setPermissionCallback(async (name, args) => {
89
98
  if (callbacks.onPermission) return callbacks.onPermission(name, args);
@@ -98,10 +107,12 @@ class Agent {
98
107
  const self = this;
99
108
  const memTools = [
100
109
  "search_memory", "save_memory", "list_memory", "delete_memory",
110
+ "mem_rom", "mem_ram",
101
111
  "compress_context", "agent_self_invoke", "set_timer",
102
112
  "save_tool", "delete_tool_file", "list_saved_tools",
103
113
  "forget_conversation", "restart_session",
104
114
  "search_history", "list_history_files",
115
+ "list_sessions", "view_session", "search_sessions",
105
116
  ];
106
117
  for (const name of memTools) {
107
118
  const tool = Tools.getTool(name);
@@ -114,10 +125,16 @@ class Agent {
114
125
  switch (name) {
115
126
  case "search_memory":
116
127
  return this._fmtEntries(this.memory.searchEntries(args.query, args.limit || 5));
117
- case "save_memory": {
128
+ case "save_memory":
129
+ case "mem_rom": {
130
+ const tags = args.tags ? args.tags.split(",").map((t) => t.trim()) : [];
131
+ const entry = this.memory.addRomEntry(args.content, tags);
132
+ return entry ? `[ROM] 已保存 #${entry.id}: ${entry.text}` : "[失败] 内容为空";
133
+ }
134
+ case "mem_ram": {
118
135
  const tags = args.tags ? args.tags.split(",").map((t) => t.trim()) : [];
119
- const entry = this.memory.addEntry(args.content, tags);
120
- return entry ? `[OK] 已保存 #${entry.id}: ${entry.text}` : "[失败] 内容为空";
136
+ const entry = this.memory.addRamEntry(args.content, tags);
137
+ return entry ? `[RAM] 已保存 #${entry.id}: ${entry.text}` : "[失败] 内容为空";
121
138
  }
122
139
  case "list_memory": {
123
140
  const all = this.memory.getAllEntries().slice(-(args.limit || 20));
@@ -131,7 +148,7 @@ class Agent {
131
148
  const compressed = this.memory.compressHistory();
132
149
  if (!compressed) return "[跳过] 对话太短";
133
150
  const summary = await this._summarize(compressed);
134
- this.memory.addEntry(summary, ["auto-summary"]);
151
+ this.memory.addRamEntry(summary, ["auto-summary"]);
135
152
  this.memory.clear();
136
153
  return `[OK] 上下文已压缩, 摘要存入记忆: ${summary}`;
137
154
  }
@@ -174,16 +191,18 @@ class Agent {
174
191
  if (compressed) summary = await this._summarize(compressed);
175
192
  }
176
193
  this.memory.clear();
177
- if (summary) { this.memory.addEntry(summary, ["auto-summary"]); }
194
+ if (summary) { this.memory.addRamEntry(summary, ["auto-summary"]); }
178
195
  return summary
179
196
  ? `[OK] 对话已遗忘, 摘要已保存: ${summary}`
180
197
  : "[OK] 对话历史已清空, 像全新对话一样";
181
198
  }
182
199
  case "restart_session": {
200
+ endSession(this.sessionId);
183
201
  this.memory.clear();
184
- this.memory.clearEntries();
202
+ this.memory.clearRamEntries();
185
203
  this.llm.resetUsage();
186
- return "[OK] 会话已完全重启: 历史+记忆+token计数均已重置. 可以开始全新任务.";
204
+ this.sessionId = startSession();
205
+ return "[OK] 会话已完全重启: 历史+记忆+token计数均已重置, 新session已创建. 可以开始全新任务.";
187
206
  }
188
207
  case "search_history": {
189
208
  const q = args.query || "";
@@ -199,6 +218,47 @@ class Agent {
199
218
  if (files.length === 0) return "(暂无历史对话文件)";
200
219
  return files.map((f) => `${f.file} | ${f.turns}轮 | ${f.size}KB | ${f.created.slice(0, 10)}`).join("\n");
201
220
  }
221
+ case "list_sessions": {
222
+ const sessions = listSessions(args.limit || 20);
223
+ if (sessions.length === 0) return "(暂无对话 session)";
224
+ const now = this.sessionId;
225
+ return sessions.map((s, i) => {
226
+ const marker = s.id === now ? " ◀ 当前" : "";
227
+ const status = s.status === "active" ? "●" : "○";
228
+ const date = s.started ? s.started.slice(0, 16) : "?";
229
+ return `${i + 1}. ${status} [${date}] ${s.title || "(无标题)"} | ${s.turnCount}轮${marker}`;
230
+ }).join("\n");
231
+ }
232
+ case "view_session": {
233
+ const sid = args.session_id || "";
234
+ if (!sid) return "[错误] 请提供 session_id (用 list_sessions 获取)";
235
+ const session = getSession(sid);
236
+ if (!session) return `[未找到] session ${sid}`;
237
+ const turns = loadSessionTurns(sid, args.limit || 50);
238
+ if (turns.length === 0) return `[空] session ${sid} 中没有对话轮次`;
239
+ const header = [
240
+ `=== Session: ${session.title || "(无标题)"} ===`,
241
+ `ID: ${session.id} | 开始: ${session.started?.slice(0, 16) || "?"} | 共 ${session.turnCount} 轮`,
242
+ ``,
243
+ ];
244
+ const body = turns.map((t, i) => {
245
+ return `[${i + 1}] ${t.time?.slice(0, 16) || "?"}\n` +
246
+ ` > 用户: ${t.user ? t.user.slice(0, 200) : ""}\n` +
247
+ ` < 回复: ${t.assistant ? t.assistant.slice(0, 300) : ""}`;
248
+ });
249
+ return header.join("\n") + "\n" + body.join("\n\n");
250
+ }
251
+ case "search_sessions": {
252
+ const q = args.query || "";
253
+ if (!q.trim()) return "[错误] 请提供搜索关键词";
254
+ const results = globalSearch(q, args.limit || 10);
255
+ if (results.length === 0) return `[无匹配] 在所有历史 session 中未找到 "${q}"`;
256
+ return results.map((r, i) => {
257
+ const date = r.sessionStarted ? r.sessionStarted.slice(0, 16) : "?";
258
+ const previews = r.topMatches.map((m) => ` · ${m.user.slice(0, 80)}`).join("\n");
259
+ return `${i + 1}. [${date}] ${r.sessionTitle} | 匹配 ${r.matchCount} 处 | 得分 ${r.totalScore}\n${previews}`;
260
+ }).join("\n\n");
261
+ }
202
262
  default:
203
263
  return `[未知内部工具] ${name}`;
204
264
  }
@@ -219,7 +279,10 @@ class Agent {
219
279
 
220
280
  _fmtEntries(entries) {
221
281
  if (!entries || entries.length === 0) return "(无记忆条目)";
222
- return entries.map((e) => `#${e.id} [${e.tags?.join(",") || "-"}] ${e.text}`).join("\n");
282
+ return entries.map((e) => {
283
+ const tag = (e._type === "ram") ? "[RAM]" : "[ROM]";
284
+ return `${tag} #${e.id} [${e.tags?.join(",") || "-"}] ${e.text}`;
285
+ }).join("\n");
223
286
  }
224
287
 
225
288
  refreshTools() {
@@ -326,7 +389,7 @@ class Agent {
326
389
  this.memory.addAssistant(finalResponse);
327
390
 
328
391
  try {
329
- saveTurn(userMessage, finalResponse, process.cwd(), allMsgs);
392
+ saveTurn(userMessage, finalResponse, process.cwd(), allMsgs, this.sessionId);
330
393
  } catch (_) {}
331
394
 
332
395
  if (opts._noAutoSave) return finalResponse;
@@ -341,7 +404,7 @@ class Agent {
341
404
  ];
342
405
  const res = await this.llm.chat(msgs);
343
406
  const habit = res.choices?.[0]?.message?.content?.trim().slice(0, 100);
344
- if (habit) this.memory.addEntry(habit, ["habit"]);
407
+ if (habit) this.memory.addRamEntry(habit, ["habit"]);
345
408
  }
346
409
  } catch (_) {}
347
410
  });
package/Src/index.js CHANGED
@@ -4,7 +4,7 @@ const readline = require("readline");
4
4
  const { spawn } = require("child_process");
5
5
  const Agent = require("./agent");
6
6
  const Tools = require("../Tools");
7
- const { listRecentTurns, searchHistory, getFileList, loadFileTurns } = require("../Mem/history");
7
+ const { listRecentTurns, searchHistory, getFileList, loadFileTurns, listSessions, getSession, loadSessionTurns, globalSearch, endSession, startSession } = require("../Mem/history");
8
8
 
9
9
  const C = {
10
10
  reset: "\x1b[0m",
@@ -345,6 +345,8 @@ function showHelp() {
345
345
  ["/memory_clear", "清空所有记忆"], ["/compress", "手动压缩上下文"],
346
346
  ["/history [n]", "查看最近n条历史对话"], ["/history files", "查看历史文件列表"],
347
347
  ["/history search <q>", "搜索历史对话"], ["/history read <f>", "读取文件含完整工具调用"],
348
+ ["/sessions [n]", "列出对话session"], ["/session <id>", "查看某个session"],
349
+ ["/session_search <q>", "全局搜索session"],
348
350
  ["/tool_save <name>", "持久化保存工具"], ["/tool_list_saved", "列出持久化工具"],
349
351
  ["/tool_del_saved <name>", "删除持久化工具"],
350
352
  ["/trusted", "查看受信任工具"], ["/trust <name>", "永久信任工具"],
@@ -369,7 +371,7 @@ function showStatus() {
369
371
  console.log(` 模型: ${C.bold}${config.llm.model}${C.reset} 温度: ${config.llm.temperature}`);
370
372
  console.log(` Tokens 累计: ${emoji("tokenIn")}${C.cyan}${usage.prompt}${C.reset} ${emoji("tokenOut")}${C.magenta}${usage.completion}${C.reset}`);
371
373
  console.log(` 上下文使用: ${ctxBar(ctxPct)}`);
372
- console.log(` 记忆: ${mem.entries || 0}/${mem.maxEntries || config.memory.maxEntries} 条目, 历史 ${mem.historyMessages || 0} 条`);
374
+ console.log(` 记忆: ROM${mem.romEntries || mem.entries || 0} RAM${mem.ramEntries || 0} · 历史 ${mem.historyMessages || 0} 条`);
373
375
  const tools = Tools.listToolNames();
374
376
  console.log(` 工具: ${tools.length} 个 — ${tools.slice(0, 8).join(", ")}${tools.length > 8 ? " ..." : ""}`);
375
377
  console.log(div("="));
@@ -436,8 +438,10 @@ async function handleSlashCommand(input) {
436
438
  console.log(C.dim + `再见~ ${emoji("done")}` + C.reset);
437
439
  process.exit(0);
438
440
  case "reset":
441
+ endSession(agent.sessionId);
439
442
  agent.reset();
440
- console.log(`${C.green}对话已重置${C.reset}`);
443
+ agent.sessionId = startSession();
444
+ console.log(`${C.green}对话已重置, 新 session 已创建${C.reset}`);
441
445
  break;
442
446
  case "tools": {
443
447
  const names = Tools.listToolNames();
@@ -563,7 +567,7 @@ async function handleSlashCommand(input) {
563
567
  case "memory": {
564
568
  const s = agent.memory.stats();
565
569
  console.log(div());
566
- console.log(` 条目: ${s.entries}/${s.maxEntries} 历史消息: ${s.historyMessages}`);
570
+ console.log(` 条目: ROM${s.romEntries || s.entries} RAM${s.ramEntries || 0} 历史消息: ${s.historyMessages}`);
567
571
  console.log(div());
568
572
  break;
569
573
  }
@@ -572,7 +576,10 @@ async function handleSlashCommand(input) {
572
576
  const entries = agent.memory.getAllEntries().slice(-n);
573
577
  if (entries.length === 0) { console.log("(无记忆条目)"); break; }
574
578
  console.log(div());
575
- for (const e of entries) console.log(` ${C.yellow}#${e.id}${C.reset} ${C.dim}[${e.tags?.join(",") || "-"}]${C.reset} ${e.text}`);
579
+ for (const e of entries) {
580
+ const tag = e._type === "ram" ? `${C.yellow}[RAM]${C.reset}` : `${C.green}[ROM]${C.reset}`;
581
+ console.log(` ${tag} ${C.yellow}#${e.id}${C.reset} ${C.dim}[${e.tags?.join(",") || "-"}]${C.reset} ${e.text}`);
582
+ }
576
583
  console.log(div());
577
584
  break;
578
585
  }
@@ -581,7 +588,10 @@ async function handleSlashCommand(input) {
581
588
  const results = agent.memory.searchEntries(rest, 10);
582
589
  if (results.length === 0) { console.log(`无匹配: ${rest}`); break; }
583
590
  console.log(div());
584
- for (const e of results) console.log(` ${C.yellow}#${e.id}${C.reset} ${e.text}`);
591
+ for (const e of results) {
592
+ const tag = e._type === "ram" ? `${C.yellow}[RAM]${C.reset}` : `${C.green}[ROM]${C.reset}`;
593
+ console.log(` ${tag} ${C.yellow}#${e.id}${C.reset} ${e.text}`);
594
+ }
585
595
  console.log(div());
586
596
  break;
587
597
  }
@@ -593,8 +603,8 @@ async function handleSlashCommand(input) {
593
603
  break;
594
604
  }
595
605
  case "memory_clear":
596
- agent.memory.clearEntries();
597
- console.log(`${C.green}所有记忆条目已清空${C.reset}`);
606
+ agent.memory.clearAllEntries();
607
+ console.log(`${C.green}所有记忆条目已清空 (ROM+RAM)${C.reset}`);
598
608
  break;
599
609
  case "history": {
600
610
  const subCmd = rest ? rest.split(/\s+/)[0] : "";
@@ -662,6 +672,68 @@ async function handleSlashCommand(input) {
662
672
  await pipeToPager(wrapped);
663
673
  break;
664
674
  }
675
+ case "sessions": {
676
+ const limit = rest ? parseInt(rest, 10) || 20 : 20;
677
+ const sessions = listSessions(limit);
678
+ if (sessions.length === 0) { console.log("(暂无对话 session)"); break; }
679
+ let out = `${C.bold + C.cyan}对话 Sessions${C.reset} (${sessions.length})\n` + div() + "\n";
680
+ for (const [i, s] of sessions.entries()) {
681
+ const marker = s.status === "active" ? `${C.green}●${C.reset}` : `${C.dim}○${C.reset}`;
682
+ const active = s.status === "active" ? ` ${C.green}◀ 当前${C.reset}` : "";
683
+ out += ` ${String(i + 1).padStart(2)}. ${marker} [${(s.started || "?").slice(0, 16)}] ${s.title || "(无标题)"} | ${s.turnCount}轮${active}\n`;
684
+ }
685
+ out += "\n" + div() + `\n用 ${C.cyan}/session <编号>${C.reset} 查看详情, ${C.yellow}/session_search <关键词>${C.reset} 搜索`;
686
+ console.log(out);
687
+ break;
688
+ }
689
+ case "session": {
690
+ if (!rest) { console.log(`用法: /session <session_id 或列表序号>\n请先用 /sessions 查看列表`); break; }
691
+ const sessions = listSessions(999);
692
+ let sid = rest.trim();
693
+ if (/^\d+$/.test(sid)) {
694
+ const idx = parseInt(sid) - 1;
695
+ if (idx < 0 || idx >= sessions.length) { console.log(`序号超出范围 (1-${sessions.length})`); break; }
696
+ sid = sessions[idx].id;
697
+ }
698
+ const session = getSession(sid);
699
+ if (!session) { console.log(`未找到 session: ${sid}`); break; }
700
+ const turns = loadSessionTurns(sid, 30);
701
+ let out = `${C.bold + C.cyan}=== ${session.title || "(无标题)"} ===${C.reset}\n`;
702
+ out += `ID: ${C.dim}${session.id}${C.reset} | ${(session.started || "?").slice(0, 16)} | ${session.turnCount}轮\n` + div() + "\n";
703
+ if (turns.length === 0) { out += "(这个 session 暂无对话轮次)\n"; }
704
+ else {
705
+ for (const [i, t] of turns.entries()) {
706
+ out += `${C.yellow}[${i + 1}] ${(t.time || "?").slice(0, 16)}${C.reset}\n`;
707
+ if (t.cwd) out += ` ${C.dim}${t.cwd}${C.reset}\n`;
708
+ out += ` ${C.cyan}▸${C.reset} ${(t.user || "").slice(0, 400)}\n`;
709
+ out += ` ${C.green}◂${C.reset} ${(t.assistant || "").slice(0, 400)}\n\n`;
710
+ }
711
+ out += `(共 ${turns.length} 轮, 显示最近 30 轮)`;
712
+ }
713
+ out += "\n" + div();
714
+ await pipeToPager(out);
715
+ break;
716
+ }
717
+ case "session_search": {
718
+ if (!rest) { console.log("用法: /session_search <关键词> [数量]\n在整个历史中搜索相关 session"); break; }
719
+ const parts = rest.trim().split(/\s+/);
720
+ const maybeNum = parseInt(parts[parts.length - 1], 10);
721
+ const limit = !isNaN(maybeNum) ? maybeNum : 10;
722
+ const query = !isNaN(maybeNum) ? parts.slice(0, -1).join(" ") : rest.trim();
723
+ const results = globalSearch(query, limit);
724
+ if (results.length === 0) { console.log(`全史搜索未找到 "${query}"`); break; }
725
+ let out = `${C.bold + C.yellow}搜索: "${query}"${C.reset} — ${results.length} 个 session\n` + div() + "\n";
726
+ for (const [i, r] of results.entries()) {
727
+ out += `${C.yellow}${i + 1}.${C.reset} [${(r.sessionStarted || "?").slice(0, 16)}] ${r.sessionTitle} | ${r.matchCount}处 | 得分${r.totalScore}\n`;
728
+ for (const m of r.topMatches) {
729
+ out += ` ${C.dim}·${C.reset} ${m.user.slice(0, 100)}\n`;
730
+ }
731
+ out += "\n";
732
+ }
733
+ out += div() + `\n用 ${C.cyan}/session <编号>${C.reset} 查看详情`;
734
+ console.log(out);
735
+ break;
736
+ }
665
737
  case "compress": {
666
738
  console.log(`${C.yellow}正在压缩上下文...${C.reset}`);
667
739
  const result = await agent._handleAgentTool("compress_context", {});
@@ -963,6 +1035,7 @@ async function main() {
963
1035
  `${C.cyan}tools${C.reset}`, `${C.cyan}status${C.reset}`, `${C.cyan}ctx${C.reset}`,
964
1036
  `${C.cyan}temp${C.reset}`, `${C.cyan}token${C.reset}`,
965
1037
  `${C.cyan}compress${C.reset}`, `${C.cyan}memory${C.reset}`, `${C.cyan}history${C.reset}`,
1038
+ `${C.cyan}sessions${C.reset}`, `${C.cyan}session_search${C.reset}`,
966
1039
  `${C.cyan}tool_save${C.reset}`, `${C.cyan}tool_del${C.reset}`,
967
1040
  ];
968
1041
  process.stdout.write("\n" + C.dim + menu.join(" ") + C.reset + "\n");