@ghenya/clinn 0.8.8 → 0.9.1

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/README.md CHANGED
@@ -1,49 +1,35 @@
1
1
  # Clinn — 终端原生 AI 编程助手
2
2
 
3
- > DeepSeek 驱动 · 50+ 工具 · 对话记忆 · 虚拟浏览器 · 零依赖运行
3
+ > Self-Evolving Terminal AI · Ink TUI · 50+ Tools · Session Memory
4
4
 
5
- Clinn 是一个运行在终端里的 AI Agent,直接和你的文件系统、Shell、网络交互。不需要 IDE、不需要插件——打开终端,说人话,它帮你干活。
6
-
7
- 🌐 官方网站:[yxpil.com](https://yxpil.com)
5
+ ```bash
6
+ npm install -g @ghenya/clinn
7
+ clinn
8
+ ```
8
9
 
9
10
  ```
10
- ██████ ██ ██ ███ ██ ███ ██
11
+ ██████ ██ ██ ███ ██ ███ ██
11
12
  ██ ██ ██ ████ ██ ████ ██
12
13
  ██ ██ ██ ██ ██ ██ ██ ██ ██
13
14
  ██ ██ ██ ██ ██ ██ ██ ██ ██
14
- ██████ ███████ ██ ██ ████ ██ █ 0.7
15
+ ██████ ███████ ██ ██ ████ ██ █ 0.9
15
16
  ```
16
17
 
17
18
  ---
18
19
 
19
20
  ## 安装
20
21
 
21
- ### macOS / Linux / WSL
22
-
23
22
  ```bash
24
- git clone https://github.com/PillowBots/clinn.git
25
- cd clinn
26
- bash install.sh
27
- ```
28
-
29
- 安装后全局可用:终端输入 `clinn` 即可启动。
23
+ # npm 全局安装
24
+ npm install -g @ghenya/clinn
30
25
 
31
- ### Windows
32
-
33
- ```bat
26
+ # 或从 GitHub
34
27
  git clone https://github.com/PillowBots/clinn.git
35
- cd clinn
36
- install.bat
28
+ cd clinn && bash install.sh
37
29
  ```
38
30
 
39
- 或直接双击 `clinn.bat`。
40
-
41
- ### 前置条件
42
-
43
31
  - Node.js >= 18
44
- - DeepSeek API Key[获取地址](https://platform.deepseek.com/)
45
-
46
- 首次运行时会提示输入 API Key。
32
+ - DeepSeek API Key ([获取](https://platform.deepseek.com/))
47
33
 
48
34
  ---
49
35
 
@@ -51,190 +37,116 @@ install.bat
51
37
 
52
38
  | 类别 | 工具 |
53
39
  |---|---|
54
- | **文件操作** | read, write, delete_file, move_file, copy_file, search_replace, edit_lines, read_lines, file_info |
55
- | **目录浏览** | ls, list_dir, tree, glob — 一键看完项目结构 |
56
- | **搜索** | grep, search_in_files, search_in_range, find_files |
57
- | **终端** | exec_console, check_command_status 直接跑命令 |
58
- | **网络** | web_search(Bing 搜索), web_fetch(网页抓取), browse_page(虚拟浏览器反反爬) |
59
- | **记忆** | search_memory, save_memory, list_memory, delete_memory |
60
- | **对话** | forget_conversation, restart_session |
61
- | **任务** | todo_write, set_timer, skill |
62
- | **工具管理** | save_tool, delete_tool_file, list_saved_tools — AI 自己写的工具持久化保存 |
63
- | **诊断** | get_diagnostics |
64
- | **预览** | open_preview |
40
+ | **文件操作** | read, write, delete_file, move_file, copy_file, search_replace, edit_lines |
41
+ | **搜索** | grep (ripgrep优先), glob, search_in_files, find_files |
42
+ | **终端** | exec_console, wait_command |
43
+ | **网络** | web_search (Bing), web_fetch, browse_page (Puppeteer) |
44
+ | **记忆** | mem_rom (永久), mem_ram (会话), search_memory, list_memory, delete_memory |
45
+ | **对话** | forget_conversation, restart_session, compress_context |
46
+ | **Session** | list_sessions, view_session, search_sessions — 全局历史搜索 |
47
+ | **工具管理** | save_tool, delete_tool_file, list_saved_tools — AI 自写工具持久化 |
65
48
 
66
49
  ---
67
50
 
68
- ## 使用方式
69
-
70
- 启动后直接输入自然语言:
71
-
72
- ```
73
- > 帮我分析这个项目的代码结构
74
- > 搜索项目里所有 TODO 注释
75
- > 把 README.md 里的英文翻译成中文
76
- > 液氮超频的最高纪录是多少?
77
- ```
78
-
79
- ### 内置命令
51
+ ## 内置命令
80
52
 
81
53
  | 命令 | 说明 |
82
54
  |---|---|
83
- | `/help` | 查看所有命令 |
84
- | `/tools` | 查看工具列表 |
85
- | `/history [n]` | 查看最近 n 条对话 |
86
- | `/history files` | 查看历史文件列表 |
87
- | `/history search <关键词>` | 搜索历史 |
88
- | `/reset` | 重置当前对话 |
89
- | `/ctx` | 查看上下文使用量 |
90
- | `/clear` | 清除上下文记忆 |
91
- | `/status` | 查看当前状态 |
92
- | `/trusted` | 查看受信任工具 |
93
- | `/trust <name>` | 永久信任工具 |
94
- | `/untrust <name>` | 取消信任 |
55
+ | `/help` | 查看帮助 |
56
+ | `/reset` | 重置对话 (新 session) |
57
+ | `/clear` | 清除消息 |
58
+ | `/ctx` | 上下文使用量 |
59
+ | `/status` | 当前状态 |
60
+ | `/tools` | 工具列表 |
61
+ | `/history [n]` | 最近历史 |
62
+ | `/history search <q>` | 搜索历史 |
63
+ | `/sessions` | 列出 session |
64
+ | `/session <id>` | 查看 session |
65
+ | `/session_search <q>` | 全局搜索 |
66
+ | `/memory` | 记忆统计 |
67
+ | `/memory_list [n]` | 记忆列表 |
68
+ | `/memory_search <q>` | 搜索记忆 |
69
+ | `/compress` | 压缩上下文 |
70
+ | `/trust <name>` | 信任工具 |
71
+ | `/api` | API 配置 |
95
72
  | `/exit` | 退出 |
96
73
 
97
74
  ---
98
75
 
99
76
  ## 核心特性
100
77
 
101
- ### 对话记忆与历史
78
+ ### Ink 全屏 TUI (v0.9)
102
79
 
103
- 每次对话自动保存到 `Mem/history-YYYY-MM-DD.json`,包含完整的工具调用链路。支持历史搜索、回顾、分页浏览(less 分页器)。
80
+ 全屏终端界面,左侧 LOGO + 右侧欢迎框,消息折叠、工具调用折叠/展开、Ctrl+C 中断、消息队列排队执行。
104
81
 
105
- ### 虚拟浏览器
82
+ ### 双级记忆系统 (v0.9)
106
83
 
107
- `browse_page` 工具基于 puppeteer-core + Chrome Headless,可绕过大部分反爬机制,用于抓取需要 JS 渲染的页面。
84
+ - **mem_rom**:永久记忆,落盘 `~/.clinn/mem/persistent-memory.json`,重启不丢
85
+ - **mem_ram**:会话记忆,仅本次窗口,关闭即丢
86
+ - 搜索同时覆盖 ROM + RAM,带 `[ROM]`/`[RAM]` 标签
108
87
 
109
- ### 自定义工具持久化
110
-
111
- AI 可以在对话中生成工具代码,通过 `save_tool` 持久化到 `Tools/custom/` 目录,下次启动自动加载。
112
-
113
- ### 上下文实时监控
114
-
115
- - 90%:黄色提示 "建议 /clear 清除对话"
116
- - 95%:二次警告 "请尽快 /clear"
117
- - 不会再自动压缩上下文——由用户自己决定何时 `/clear`
88
+ ### Session 回顾与全局搜索 (v0.9)
118
89
 
119
- ### 思考动画
90
+ - 每次 `/reset` 或重启创建新 session
91
+ - `/sessions` 列出所有历史会话
92
+ - `/session_search` 全文搜索全部历史
120
93
 
121
- braille spinner 旋转指示 LLM 推理状态,120ms 一帧,流畅不刷屏。有内容输出时自动静默。
94
+ ### 虚拟浏览器
122
95
 
123
- ---
96
+ `browse_page` 基于 puppeteer-core + Chrome Headless,可绕过大部分反爬机制。
124
97
 
125
- ## 项目结构
98
+ ### 自定义工具持久化
126
99
 
127
- ```
128
- Clinn/
129
- ├── Src/
130
- │ ├── index.js # CLI 入口、UI 渲染、斜杠命令
131
- │ ├── agent.js # Agent 主逻辑、工具循环、记忆管理
132
- │ └── llm.js # DeepSeek API 客户端、流式解析
133
- ├── Tools/
134
- │ ├── index.js # 工具注册、权限、自定义工具加载
135
- │ ├── file_tools.js # 文件读写操作
136
- │ ├── edit_tools.js # search_replace、edit_lines
137
- │ ├── search_tools.js # grep、find_files、web_fetch
138
- │ ├── extended_tools.js # web_search (Bing)、exec_console、todo_write 等
139
- │ ├── browser.js # 虚拟浏览器 (puppeteer-core)
140
- │ ├── tokenizer.js # Token 估算
141
- │ └── custom/ # 用户自定义工具持久化目录
142
- ├── Mem/
143
- │ ├── index.js # ConversationMemory
144
- │ ├── history.js # 历史文件读写、搜索
145
- │ └── history-*.json # 按日期存储的对话历史
146
- ├── Logos/
147
- │ └── StartLogo.txt # 启动 Logo
148
- ├── config.json # 配置文件
149
- ├── install.sh # macOS/Linux 安装脚本
150
- ├── install.bat # Windows 安装脚本
151
- ├── clinn.bat # Windows 快捷启动
152
- ├── package.json
153
- └── public/ # 发布版本
154
- └── clinn-v0.7.1/
155
- ```
100
+ AI 在对话中生成工具代码,通过 `save_tool` 持久化到 `~/.clinn/Tools/custom/`,下次启动自动加载。
156
101
 
157
102
  ---
158
103
 
159
104
  ## 配置
160
105
 
161
- 编辑 `config.json`:
106
+ 编辑 `~/.clinn/config.json` 或对话内 `/api` 命令:
162
107
 
163
108
  ```json
164
109
  {
165
110
  "llm": {
166
- "provider": "deepseek",
167
- "apiKey": "YOUR_DEEPSEEK_API_KEY_HERE",
111
+ "apiKey": "YOUR_KEY",
168
112
  "baseURL": "https://api.deepseek.com/v1",
169
113
  "model": "deepseek-v4-pro",
170
- "maxTokens": 8000,
114
+ "contextWindow": 131072,
115
+ "maxTokens": 16384,
171
116
  "temperature": 0.7
172
- },
173
- "memory": {
174
- "maxHistory": 100,
175
- "maxEntries": 800
176
- },
177
- "ui": {
178
- "showLogo": true,
179
- "dividerChar": "-"
180
117
  }
181
118
  }
182
119
  ```
183
120
 
184
121
  ---
185
122
 
186
- ## 更新日志 (Changelog)
187
-
188
- ### v0.7.9 — 版本查询
189
-
190
- - **`--version` / `-v`**:终端直接 `clinn --version` 查看版本
191
- - **`/version`**:对话内 `/version` 查看版本
192
-
193
- ### v0.7.8 — Windows 兼容 & 权限修复
123
+ ## 更新日志
194
124
 
195
- - **bin 入口改为 `.js`**:`#!/usr/bin/env node` + `.js` 后缀,npm 自动生成跨平台 shim,解决 Windows 上 `不是内部或外部命令` 问题
196
- - **修复双重权限弹窗**:危险工具只弹一次 Y/N 确认
125
+ ### v0.9.0 Ink TUI · 双级记忆 · Session 搜索
197
126
 
198
- ### v0.7.1 CLI 增强 & npm 发布
127
+ - **Ink 7 全屏 TUI**:React 19 + Ink 7.0.5,左右分栏启动页,消息折叠,工具调用折叠/展开,颜色标记(红绿黄)
128
+ - **双级记忆**:mem_rom(永久落盘)+ mem_ram(会话级),重启不丢
129
+ - **Session 系统**:会话自动分组,全局全文搜索,历史回顾
130
+ - **消息队列**:AI 忙时消息自动排队,可视队列
131
+ - **Ctrl+C 中断**:不退出进程,中断执行或清空输入
132
+ - **输入框字符计数**:长文本自动缩略显示末尾
133
+ - **grep 升级**:ripgrep 优先,支持上下文行/files_only/count
134
+ - **分隔线终端宽度对齐**,不再刷屏
199
135
 
200
- - **`/api` 命令**:直接在终端配置 API Key、API 地址、模型名称,无需编辑文件
201
- - `/api` — 查看当前 API 设置(Key 脱敏显示)
202
- - `/api key <KEY>` — 直接设置 API Key
203
- - `/api url <URL>` — 切换 API 地址(兼容其他兼容接口)
204
- - `/api model <MODEL>` — 切换模型
205
- - **npm 发布**:`npm install -g @ghenya/clinn` 全球安装,配置自动存 `~/.clinn/`
206
- - **历史/Mem/自定义工具路径统一到 `~/.clinn/`**
136
+ ### v0.8.0 上下文修复 · 语法校验 · 颜文字
207
137
 
208
- ### v0.7.0 交互重构 & 稳定性
138
+ - 上下文窗口 6K→105K (×17),模型感知检测
139
+ - Token 估算修正 (CJK ×0.8 + ASCII ×0.3)
140
+ - 写文件后自动 node/py/json 语法校验
141
+ - 动态颜文字引擎:关键词匹配,不再随机
142
+ - Glob 纯 Node.js 跨平台,模板引擎 + 5 技能
209
143
 
210
- - **上下文监控三级预警**:90% 提示 / 95% 警告 / 不再自动压缩,用户自主 `/clear`
211
- - **工具执行静默化**:不再刷屏动画,只显示 `✓ tool_name` 完成标记
212
- - **思考动画**:braille spinner `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏` 120ms 旋转指示推理状态
213
- - **修复死循环误判**:循环检测改为工具名+参数逐字比对,只有 300 次完全相同才拦截
214
- - **修复上下文百分比漂移**:统一用持久记忆计算,排除临时网页正文干扰
215
- - **全量对话历史**:`history-*.json` 保存完整工具调用链路
216
- - **web_search 切换 Bing**:cn.bing.com HTML 解析,国内可用
217
- - **虚拟浏览器**:browse_page 基于 puppeteer-core,反反爬
144
+ ### v0.7.0 交互重构 · npm 发布
218
145
 
219
- ### v0.6.0 记忆系统
220
-
221
- - 对话记忆与历史文件持久化
222
- - `/history` 命令 + less 分页器浏览
223
- - 历史搜索功能
224
- - 表格结构转纯文本输出
225
- - 宁缺毋滥行宽控制
226
-
227
- ### v0.5.0 — 首发
228
-
229
- - DeepSeek API 集成
230
- - 50+ 工具体系
231
- - 工具权限控制
232
- - 自定义工具持久化
233
- - install.sh / install.bat 一键安装
234
- - 流式响应输出
146
+ - npm 发布:`npm install -g @ghenya/clinn`
147
+ - `/api` 命令链式配置,上下文三级预警
148
+ - web_search 切换 Bing,虚拟浏览器
235
149
 
236
150
  ---
237
151
 
238
- ## 许可
239
-
240
152
  MIT License
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");