opencode-haimati 1.0.2

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 ADDED
@@ -0,0 +1,100 @@
1
+ # opencode-haimati
2
+
3
+ 海马体记忆系统插件 - 为 OpenCode 提供基于文件系统的长期记忆存储和检索功能。
4
+
5
+ ## 功能特性
6
+
7
+ - 🧠 类海马体记忆机制:模拟人脑记忆系统的分类存储
8
+ - 📁 树形索引结构:通过 `.haimati/索引.md` 维护记忆索引
9
+ - 📚 分组书页存储:记忆内容按序号分组存储于 `.haimati/书页/` 目录
10
+ - 🔍 多模式查询:支持序号、路径、标题精确匹配和模糊搜索
11
+ - ✏️ 完整 CRUD:读取、写入、更新、删除、移动记忆
12
+ - 🔄 自动规则注入:新会话自动注入海马体使用原则
13
+ - 📋 日志追踪:完整的操作日志记录
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ npm install -g opencode-haimati
19
+ # 或
20
+ bun add -g opencode-haimati
21
+ ```
22
+
23
+ ## 使用方法
24
+
25
+ 将插件添加到你的 `opencode.json`:
26
+
27
+ ```json
28
+ {
29
+ "$schema": "https://opencode.ai/config.json",
30
+ "plugin": ["opencode-haimati"],
31
+ }
32
+ ```
33
+
34
+ ## 海马体结构
35
+
36
+ ```
37
+ .haimati/
38
+ ├── 索引.md # 树形结构索引
39
+ └── 书页/ # 存储各记忆内容(按序号分组)
40
+ ├── 001-050/
41
+ │ ├── 001.md
42
+ │ └── 002.md
43
+ └── 051-100/
44
+ └── ...
45
+ ```
46
+
47
+ ## 可用工具
48
+
49
+ - `haimati_read` - 从海马体读取记忆(支持序号、路径、标题查询)
50
+ - `haimati_write` - 写入新记忆
51
+ - `haimati_search` - 搜索记忆内容
52
+ - `haimati_update` - 更新已有记忆的内容
53
+ - `haimati_move` - 修改记忆的分类路径
54
+ - `haimati_delete` - 删除记忆
55
+ - `haimati_list` - 列出记忆索引
56
+
57
+ ## 海马体三大原则
58
+
59
+ 1. **信息不丢失**: 完整保留所有有价值的信息
60
+ 2. **错误信息修正**: 不再保留已被纠正的错误信息
61
+ 3. **拒绝冗余**: 不要重复记录相同内容
62
+
63
+ ## 使用示例
64
+
65
+ ```typescript
66
+ // 写入新记忆
67
+ haimati_write({
68
+ category: "标立方/签章",
69
+ title: "签章服务WS通信机制",
70
+ content: "WebSocket通信采用base64编码..."
71
+ })
72
+
73
+ // 读取记忆
74
+ haimati_read({ query: "086" })
75
+ // 或
76
+ haimati_read({ query: "标立方/签章/签章服务WS通信机制" })
77
+
78
+ // 搜索记忆
79
+ haimati_search({ keyword: "WebSocket", limit: 10 })
80
+ ```
81
+
82
+ ## 索引格式示例
83
+
84
+ ```
85
+ 标立方/
86
+ ├── 签章/
87
+ │ └── 签章服务WS通信机制 - 086
88
+ └── 通用/
89
+ └── 用户注册密码 - 033
90
+ ```
91
+
92
+ ## 依赖要求
93
+
94
+ - OpenCode v0.15.0 或更高版本
95
+ - Node.js 或 Bun 运行时
96
+ - 项目目录的文件系统写入权限
97
+
98
+ ## 许可证
99
+
100
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,1098 @@
1
+ // index.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import os from "os";
5
+ import { tool } from "@opencode-ai/plugin";
6
+ var HAIMATI_DIR = ".haimati";
7
+ var INDEX_FILE = "\u7D22\u5F15.md";
8
+ var PAGES_DIR = "\u4E66\u9875";
9
+ var NEXT_ID_FILE = "next-id.txt";
10
+ var LOG_FILE = "haimati.log";
11
+ var LOCK_MAX_WAIT = 5e3;
12
+ var LOCK_RETRY_INTERVAL = 50;
13
+ var HAIMATI_CONFIG_FILE = "opencode-haimati.conf";
14
+ var HAIMATI_PATH_KEY = "haimati-path";
15
+ var VERSION_SEPARATOR = "\n---\nversion:";
16
+ var DEFAULT_VERSION = 1;
17
+ function getConfigHaimatiPath(directory) {
18
+ const configPath = path.join(directory, HAIMATI_CONFIG_FILE);
19
+ if (!fs.existsSync(configPath)) {
20
+ return null;
21
+ }
22
+ try {
23
+ const content = fs.readFileSync(configPath, "utf-8");
24
+ const lines = content.split("\n");
25
+ for (const line of lines) {
26
+ const trimmed = line.trim();
27
+ if (!trimmed || trimmed.startsWith("#")) {
28
+ continue;
29
+ }
30
+ const keyValueMatch = trimmed.match(/^([^:]+):\s*(.+)$/);
31
+ if (keyValueMatch) {
32
+ const key = keyValueMatch[1].trim();
33
+ const value = keyValueMatch[2].trim();
34
+ if (key === HAIMATI_PATH_KEY && value) {
35
+ return path.resolve(directory, value);
36
+ }
37
+ }
38
+ }
39
+ } catch (error) {
40
+ console.error(`[haimati] \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25:`, error);
41
+ }
42
+ return null;
43
+ }
44
+ function getHaimatiDir(directory) {
45
+ const configPath = getConfigHaimatiPath(directory);
46
+ if (configPath) {
47
+ return configPath;
48
+ }
49
+ return path.join(directory, HAIMATI_DIR);
50
+ }
51
+ function getIndexPath(directory) {
52
+ return path.join(getHaimatiDir(directory), INDEX_FILE);
53
+ }
54
+ function getNextIdPath(directory) {
55
+ return path.join(getHaimatiDir(directory), NEXT_ID_FILE);
56
+ }
57
+ function getLockPath(directory) {
58
+ return path.join(getHaimatiDir(directory), ".lock");
59
+ }
60
+ function getPagesDir(directory) {
61
+ return path.join(getHaimatiDir(directory), PAGES_DIR);
62
+ }
63
+ function getPageGroupDir(directory, id) {
64
+ const n = parseInt(id, 10);
65
+ const groupStart = Math.floor((n - 1) / 50) * 50 + 1;
66
+ const groupEnd = groupStart + 49;
67
+ const groupName = `${String(groupStart).padStart(3, "0")}-${String(groupEnd).padStart(3, "0")}`;
68
+ return path.join(getPagesDir(directory), groupName);
69
+ }
70
+ function getPagePath(directory, id) {
71
+ return path.join(getPageGroupDir(directory, id), `${id}.md`);
72
+ }
73
+ function getLogFilePath() {
74
+ const osTmpDir = os.tmpdir();
75
+ return path.join(osTmpDir, "haimati_logs", LOG_FILE);
76
+ }
77
+ async function writeLogToFile(message) {
78
+ try {
79
+ const now = /* @__PURE__ */ new Date();
80
+ const dateStr = now.toISOString().split("T")[0];
81
+ const timeStr = now.toISOString().replace("T", " ").split(".")[0];
82
+ const logPath = getLogFilePath();
83
+ const logDir = path.dirname(logPath);
84
+ if (!fs.existsSync(logDir)) {
85
+ fs.mkdirSync(logDir, { recursive: true });
86
+ }
87
+ let shouldOverwrite = false;
88
+ if (fs.existsSync(logPath)) {
89
+ const mtime = fs.statSync(logPath).mtime;
90
+ const fileDate = mtime.toISOString().split("T")[0];
91
+ if (fileDate !== dateStr) {
92
+ shouldOverwrite = true;
93
+ }
94
+ }
95
+ const logLine = `[${timeStr}] ${message}`;
96
+ if (shouldOverwrite) {
97
+ fs.writeFileSync(logPath, logLine + "\n", "utf-8");
98
+ } else {
99
+ fs.appendFileSync(logPath, logLine + "\n", "utf-8");
100
+ }
101
+ } catch (error) {
102
+ console.error(`[haimati] \u5199\u5165\u65E5\u5FD7\u6587\u4EF6\u5931\u8D25:`, error);
103
+ }
104
+ }
105
+ function truncateForLog(content, maxLen = 500) {
106
+ if (content.length <= maxLen) return content;
107
+ const half = Math.floor((maxLen - 20) / 2);
108
+ return content.substring(0, half) + "...(\u7701\u7565" + (content.length - maxLen) + "\u5B57\u7B26)..." + content.substring(content.length - half);
109
+ }
110
+ async function log(level, message) {
111
+ await writeLogToFile(`[${level.toUpperCase()}] ${message}`);
112
+ }
113
+ function getTreeDepth(line) {
114
+ let depth = 0;
115
+ let i = 0;
116
+ const len = line.length;
117
+ while (i < len) {
118
+ if (line[i] === "\u2502" && i + 3 < len && line.slice(i + 1, i + 4) === " ") {
119
+ depth++;
120
+ i += 4;
121
+ } else if (line.slice(i, i + 4) === " ") {
122
+ depth++;
123
+ i += 4;
124
+ } else if ((line[i] === "\u251C" || line[i] === "\u2514") && i + 3 < len && line.slice(i + 1, i + 4) === "\u2500\u2500 ") {
125
+ break;
126
+ } else {
127
+ break;
128
+ }
129
+ }
130
+ return depth;
131
+ }
132
+ function extractCategoryName(line) {
133
+ const trimmed = line.trim();
134
+ let name = trimmed.replace(/^[│├└─\s]+/, "").trim();
135
+ if (name === "/") {
136
+ return "/";
137
+ }
138
+ if (!name.endsWith("/")) {
139
+ return null;
140
+ }
141
+ name = name.slice(0, -1);
142
+ if (!name) return null;
143
+ return name;
144
+ }
145
+ function parseIndex(content) {
146
+ const entries = [];
147
+ const lines = content.split("\n");
148
+ const categoryPath = [];
149
+ for (const line of lines) {
150
+ if (!line.trim()) continue;
151
+ const depth = getTreeDepth(line);
152
+ const entryMatch = line.match(/^\s*[│├└─\s]*(.+?)\s*-\s*(\d+)\s*$/);
153
+ if (entryMatch) {
154
+ const title = entryMatch[1].trim();
155
+ const id = entryMatch[2];
156
+ const entryCategoryParts = categoryPath.slice(0, depth).filter((p) => p !== "");
157
+ const category = entryCategoryParts.join("/");
158
+ entries.push({ id, category, title });
159
+ } else {
160
+ const name = extractCategoryName(line);
161
+ if (name) {
162
+ if (name === "/") {
163
+ categoryPath[depth] = "";
164
+ for (let j = depth + 1; j < categoryPath.length; j++) {
165
+ delete categoryPath[j];
166
+ }
167
+ } else {
168
+ categoryPath[depth] = name;
169
+ for (let j = depth + 1; j < categoryPath.length; j++) {
170
+ delete categoryPath[j];
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+ return entries;
177
+ }
178
+ function findEntryById(entries, id) {
179
+ return entries.find((e) => e.id === id) || null;
180
+ }
181
+ function findEntryByPath(entries, categoryPath, title) {
182
+ return entries.find((e) => e.category === categoryPath && e.title === title) || null;
183
+ }
184
+ async function searchEntriesAsync(entries, keyword, matchMode, directory, searchContent = false) {
185
+ const keywords = keyword.toLowerCase().split(/\s+/).filter((k) => k.length > 0);
186
+ if (keywords.length === 0) return [];
187
+ const matchedWithScore = [];
188
+ for (const e of entries) {
189
+ const titleLower = e.title.toLowerCase();
190
+ const categoryLower = e.category.toLowerCase();
191
+ const idLower = e.id.toLowerCase();
192
+ let metaKeywordsCount = 0;
193
+ let contentKeywordsCount = 0;
194
+ for (const kw of keywords) {
195
+ if (titleLower.includes(kw) || categoryLower.includes(kw) || idLower.includes(kw)) {
196
+ metaKeywordsCount++;
197
+ }
198
+ }
199
+ if (searchContent && directory) {
200
+ const page = await readPageAsync(directory, e.id);
201
+ if (page && page.content) {
202
+ const contentLower = page.content.toLowerCase();
203
+ for (const kw of keywords) {
204
+ if (contentLower.includes(kw)) {
205
+ contentKeywordsCount++;
206
+ }
207
+ }
208
+ }
209
+ }
210
+ const totalMatchCount = metaKeywordsCount + contentKeywordsCount;
211
+ if (matchMode === "and") {
212
+ if (totalMatchCount >= keywords.length) {
213
+ matchedWithScore.push({ entry: e, score: totalMatchCount });
214
+ }
215
+ } else {
216
+ if (metaKeywordsCount > 0 || searchContent && contentKeywordsCount > 0) {
217
+ matchedWithScore.push({ entry: e, score: totalMatchCount });
218
+ }
219
+ }
220
+ }
221
+ matchedWithScore.sort((a, b) => b.score - a.score);
222
+ return matchedWithScore.map((item) => item.entry);
223
+ }
224
+ async function readNextIdFromFileAsync(directory) {
225
+ const nextIdPath = getNextIdPath(directory);
226
+ if (!fs.existsSync(nextIdPath)) {
227
+ return 1;
228
+ }
229
+ const content = fs.readFileSync(nextIdPath, "utf-8");
230
+ const trimmed = content.trim();
231
+ const parsed = parseInt(trimmed, 10);
232
+ if (!isNaN(parsed) && parsed > 0) {
233
+ return parsed;
234
+ }
235
+ return 1;
236
+ }
237
+ async function initializeNextIdIfNeededAsync(directory) {
238
+ const nextIdPath = getNextIdPath(directory);
239
+ if (fs.existsSync(nextIdPath)) {
240
+ return;
241
+ }
242
+ const entries = await readIndexAsync(directory);
243
+ let maxId = 0;
244
+ for (const e of entries) {
245
+ const n = parseInt(e.id, 10);
246
+ if (n > maxId) maxId = n;
247
+ }
248
+ await writeNextIdToFileAsync(directory, maxId + 1);
249
+ }
250
+ async function writeNextIdToFileAsync(directory, nextId) {
251
+ const haimatiDir = getHaimatiDir(directory);
252
+ if (!fs.existsSync(haimatiDir)) {
253
+ fs.mkdirSync(haimatiDir, { recursive: true });
254
+ }
255
+ const nextIdPath = getNextIdPath(directory);
256
+ fs.writeFileSync(nextIdPath, String(nextId), "utf-8");
257
+ }
258
+ function acquireLock(directory) {
259
+ const haimatiDir = getHaimatiDir(directory);
260
+ if (!fs.existsSync(haimatiDir)) {
261
+ fs.mkdirSync(haimatiDir, { recursive: true });
262
+ }
263
+ const lockPath = getLockPath(directory);
264
+ const startTime = Date.now();
265
+ while (fs.existsSync(lockPath)) {
266
+ if (Date.now() - startTime > LOCK_MAX_WAIT) {
267
+ return false;
268
+ }
269
+ const sleep = LOCK_RETRY_INTERVAL + Math.random() * 20;
270
+ const end = Date.now() + sleep;
271
+ while (Date.now() < end) {
272
+ }
273
+ }
274
+ fs.writeFileSync(lockPath, String(process.pid), "utf-8");
275
+ return true;
276
+ }
277
+ function releaseLock(directory) {
278
+ const lockPath = getLockPath(directory);
279
+ if (fs.existsSync(lockPath)) {
280
+ fs.unlinkSync(lockPath);
281
+ }
282
+ }
283
+ async function readPageAsync(directory, id) {
284
+ const pagePath = getPagePath(directory, id);
285
+ if (!fs.existsSync(pagePath)) {
286
+ return null;
287
+ }
288
+ const content = fs.readFileSync(pagePath, "utf-8");
289
+ const lines = content.split("\n");
290
+ if (lines.length < 2) {
291
+ return null;
292
+ }
293
+ let version = 1;
294
+ let actualContentLines = lines.slice(2);
295
+ const versionMatchIndex = content.lastIndexOf(VERSION_SEPARATOR);
296
+ if (versionMatchIndex !== -1) {
297
+ const versionStr = content.substring(versionMatchIndex + VERSION_SEPARATOR.length);
298
+ version = parseInt(versionStr.trim(), 10) || DEFAULT_VERSION;
299
+ actualContentLines = lines.slice(2, lines.length - 2);
300
+ }
301
+ return {
302
+ title: lines[0],
303
+ createdAt: lines[1],
304
+ content: actualContentLines.join("\n"),
305
+ version
306
+ };
307
+ }
308
+ async function writePageAsync(directory, id, title, createdAt, content, version = 1) {
309
+ const pagesDir = getPagesDir(directory);
310
+ if (!fs.existsSync(pagesDir)) {
311
+ fs.mkdirSync(pagesDir, { recursive: true });
312
+ }
313
+ const pageGroupDir = getPageGroupDir(directory, id);
314
+ if (!fs.existsSync(pageGroupDir)) {
315
+ fs.mkdirSync(pageGroupDir, { recursive: true });
316
+ }
317
+ const pagePath = getPagePath(directory, id);
318
+ const fileContent = `${title}
319
+ ${createdAt}
320
+ ${content}${VERSION_SEPARATOR}${version}`;
321
+ fs.writeFileSync(pagePath, fileContent, "utf-8");
322
+ }
323
+ async function deletePageAsync(directory, id) {
324
+ const pagePath = getPagePath(directory, id);
325
+ if (fs.existsSync(pagePath)) {
326
+ fs.unlinkSync(pagePath);
327
+ return true;
328
+ }
329
+ return false;
330
+ }
331
+ function buildIndexTree(entries) {
332
+ if (entries.length === 0) {
333
+ return "";
334
+ }
335
+ const root = { name: "/", children: [], entry: null };
336
+ for (const entry of entries) {
337
+ const parts = entry.category.split("/");
338
+ let current = root;
339
+ for (let i = 0; i < parts.length; i++) {
340
+ const part = parts[i];
341
+ if (!part) continue;
342
+ let child = current.children.find((c) => c.name === part);
343
+ if (!child) {
344
+ child = { name: part, children: [], entry: null };
345
+ current.children.push(child);
346
+ }
347
+ current = child;
348
+ }
349
+ const entryNode = {
350
+ name: entry.id,
351
+ children: [],
352
+ entry
353
+ };
354
+ current.children.push(entryNode);
355
+ }
356
+ function sortNode(node) {
357
+ node.children.sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" }));
358
+ for (const child of node.children) {
359
+ sortNode(child);
360
+ }
361
+ }
362
+ sortNode(root);
363
+ function nodeToLines(node, prefix, isLast) {
364
+ const lines = [];
365
+ const displayPrefix = prefix + (isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ");
366
+ const nodeName = node.name === "/" ? "/" : node.name + "/";
367
+ lines.push(`${displayPrefix}${nodeName}`);
368
+ prefix = prefix + (isLast ? " " : "\u2502 ");
369
+ const categories = node.children.filter((c) => c.entry === null);
370
+ const items = node.children.filter((c) => c.entry !== null);
371
+ const allChildren = [...categories, ...items];
372
+ for (let i = 0; i < allChildren.length; i++) {
373
+ const child = allChildren[i];
374
+ const childIsLast = i === allChildren.length - 1;
375
+ if (child.entry) {
376
+ const itemPrefix = prefix + (childIsLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ");
377
+ lines.push(`${itemPrefix}${child.entry.title} - ${child.entry.id}`);
378
+ } else {
379
+ lines.push(...nodeToLines(child, prefix, childIsLast));
380
+ }
381
+ }
382
+ return lines;
383
+ }
384
+ return nodeToLines(root, "", true).join("\n");
385
+ }
386
+ function removeIndexEntry(entries, id) {
387
+ return entries.filter((e) => e.id !== id);
388
+ }
389
+ async function readIndexAsync(directory) {
390
+ const indexPath = getIndexPath(directory);
391
+ if (!fs.existsSync(indexPath)) {
392
+ return [];
393
+ }
394
+ const content = fs.readFileSync(indexPath, "utf-8");
395
+ return parseIndex(content);
396
+ }
397
+ async function writeIndexAsync(directory, entries) {
398
+ const haimatiDir = getHaimatiDir(directory);
399
+ if (!fs.existsSync(haimatiDir)) {
400
+ fs.mkdirSync(haimatiDir, { recursive: true });
401
+ }
402
+ const tree = buildIndexTree(entries);
403
+ const indexPath = getIndexPath(directory);
404
+ fs.writeFileSync(indexPath, tree, "utf-8");
405
+ }
406
+ async function allocateIdWithLock(directory, category, title, now, content) {
407
+ if (!acquireLock(directory)) {
408
+ return null;
409
+ }
410
+ try {
411
+ await initializeNextIdIfNeededAsync(directory);
412
+ const entries = await readIndexAsync(directory);
413
+ const existingEntry = entries.find((e) => e.category === category && e.title === title);
414
+ if (existingEntry) {
415
+ const page = await readPageAsync(directory, existingEntry.id);
416
+ const createdAt = page ? page.createdAt : now;
417
+ const newVersion = page ? page.version + 1 : 1;
418
+ await writePageAsync(directory, existingEntry.id, title, createdAt, content, newVersion);
419
+ await log("info", `[allocateIdWithLock] \u66F4\u65B0\u5DF2\u6709\u6761\u76EE: #${existingEntry.id} ${category}/${title}, \u65B0\u7248\u672C: ${newVersion}`);
420
+ return null;
421
+ }
422
+ const currentNext = await readNextIdFromFileAsync(directory);
423
+ const newId = String(currentNext).padStart(3, "0");
424
+ await writePageAsync(directory, newId, title, now, content, 1);
425
+ const newEntry = {
426
+ id: newId,
427
+ category,
428
+ title
429
+ };
430
+ entries.push(newEntry);
431
+ await writeIndexAsync(directory, entries);
432
+ await writeNextIdToFileAsync(directory, currentNext + 1);
433
+ return newId;
434
+ } catch (error) {
435
+ const errMsg = error instanceof Error ? error.message : String(error);
436
+ await log("error", `[allocateIdWithLock] \u5206\u914D\u5E8F\u53F7\u5931\u8D25: ${errMsg}`);
437
+ return null;
438
+ } finally {
439
+ releaseLock(directory);
440
+ }
441
+ }
442
+ var HaimatiPlugin = async (ctx) => {
443
+ await log("info", `\u6D77\u9A6C\u4F53\u8BB0\u5FC6\u7CFB\u7EDF\u63D2\u4EF6(\u6587\u4EF6\u7248)\u5DF2\u52A0\u8F7D`);
444
+ return {
445
+ tool: {
446
+ /**
447
+ * haimati_read - 从海马体读取记忆
448
+ *
449
+ * 只支持序号查询:如 "086"
450
+ */
451
+ haimati_read: tool({
452
+ description: "\u4ECE\u6D77\u9A6C\u4F53\u8BB0\u5FC6\u7CFB\u7EDF\u8BFB\u53D6\u5185\u5BB9\u3002\u901A\u8FC7\u5E8F\u53F7\uFF08\u5982 '086'\uFF09\u6765\u5B9A\u4F4D\u5E76\u8BFB\u53D6\u8BB0\u5FC6\u5185\u5BB9\u3002\u652F\u6301\u5206\u9875\u8BFB\u53D6\uFF08offset/limit\uFF09\uFF0C\u8FD4\u56DE\u5185\u5BB9\u5E26\u884C\u53F7\u548C\u7248\u672C\u53F7\u3002",
453
+ args: {
454
+ query: tool.schema.string().describe("\u5E8F\u53F7\u3002\u4F8B\u5982\uFF1A'086'"),
455
+ offset: tool.schema.number().optional().default(1).describe("\u8D77\u59CB\u884C\u53F7\uFF08\u9ED8\u8BA4 1\uFF09"),
456
+ limit: tool.schema.number().optional().default(2e3).describe("\u6700\u5927\u8BFB\u53D6\u884C\u6570\uFF08\u9ED8\u8BA4 2000\uFF09")
457
+ },
458
+ /**
459
+ * 返回格式:
460
+ * version:1
461
+ * 1|第一行内容
462
+ * 2|第二行内容
463
+ * ...
464
+ *
465
+ * version 用于后续 haimati_edit 的并发控制,请妥善保存
466
+ */
467
+ async execute(args, context) {
468
+ const { directory } = context;
469
+ const indexPath = getIndexPath(directory);
470
+ await log("info", `[haimati_read] \u5165\u53C2: query="${args.query}", offset=${args.offset || 1}, limit=${args.limit || 2e3}`);
471
+ if (!fs.existsSync(indexPath)) {
472
+ const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728\u4E8E ${indexPath}`;
473
+ await log("warn", `[haimati_read] \u51FA\u53C2:
474
+ ${truncateForLog(result)}`);
475
+ return result;
476
+ }
477
+ try {
478
+ const entries = await readIndexAsync(directory);
479
+ await log("debug", `[haimati_read] \u8BFB\u53D6\u7D22\u5F15: \u5171 ${entries.length} \u6761\u8BB0\u5F55`);
480
+ const query = args.query.trim();
481
+ if (!/^\d+$/.test(query)) {
482
+ const result2 = `\u9519\u8BEF\uFF1Aquery \u5FC5\u987B\u662F\u5E8F\u53F7\uFF0C\u53EA\u652F\u6301\u5E8F\u53F7\u67E5\u8BE2`;
483
+ await log("warn", `[haimati_read] \u51FA\u53C2:
484
+ ${truncateForLog(result2)}`);
485
+ return result2;
486
+ }
487
+ const entry = findEntryById(entries, query);
488
+ await log("debug", `[haimati_read] \u5E8F\u53F7\u67E5\u8BE2 #${query}: ${entry ? "\u627E\u5230" : "\u672A\u627E\u5230"}`);
489
+ if (!entry) {
490
+ const result2 = `\u672A\u627E\u5230\u8BB0\u5FC6: ${args.query}`;
491
+ await log("info", `[haimati_read] \u51FA\u53C2:
492
+ ${truncateForLog(result2)}`);
493
+ return result2;
494
+ }
495
+ const page = await readPageAsync(directory, entry.id);
496
+ if (!page) {
497
+ const result2 = `\u672A\u627E\u5230\u4E66\u9875: #${entry.id}`;
498
+ await log("error", `[haimati_read] \u51FA\u53C2:
499
+ ${truncateForLog(result2)}`);
500
+ return result2;
501
+ }
502
+ await log("debug", `[haimati_read] \u8BFB\u53D6\u4E66\u9875: \u4E66\u9875/${getPageGroupDir(directory, entry.id).split(/[\\/]/).pop()}/${entry.id}.md, version=${page.version}`);
503
+ const contentLines = page.content.split("\n");
504
+ const offset = args.offset || 1;
505
+ const limit = args.limit || 2e3;
506
+ const totalLines = contentLines.length;
507
+ const startIndex = Math.max(0, offset - 1);
508
+ const endIndex = Math.min(totalLines, startIndex + limit);
509
+ const linesOutput = [];
510
+ linesOutput.push(`version:${page.version}`);
511
+ for (let i = startIndex; i < endIndex; i++) {
512
+ linesOutput.push(`${i + 1}|${contentLines[i]}`);
513
+ }
514
+ const result = linesOutput.join("\n");
515
+ await log("info", `[haimati_read] \u51FA\u53C2:
516
+ ${truncateForLog(result)}`);
517
+ return result;
518
+ } catch (error) {
519
+ const errorMsg = error instanceof Error ? error.message : String(error);
520
+ const stack = error instanceof Error ? error.stack : "";
521
+ await log("error", `[haimati_read] \u51FA\u53C2: \u9519\u8BEF: ${errorMsg}
522
+ \u5806\u6808: ${stack}`);
523
+ return `\u9519\u8BEF: ${errorMsg}`;
524
+ }
525
+ }
526
+ }),
527
+ /**
528
+ * haimati_write - 将新信息写入海马体记忆系统
529
+ *
530
+ * 会自动分配新的序号并存储到文件
531
+ */
532
+ haimati_write: tool({
533
+ description: "\u5C06\u65B0\u4FE1\u606F\u5199\u5165\u6D77\u9A6C\u4F53\u8BB0\u5FC6\u7CFB\u7EDF\u3002\u9700\u8981\u63D0\u4F9B\u5206\u7C7B\u8DEF\u5F84\uFF08\u5982 '\u6807\u7ACB\u65B9/\u7B7E\u7AE0'\uFF09\u3001\u6807\u9898\u548C\u5185\u5BB9\u3002",
534
+ args: {
535
+ category: tool.schema.string().describe("\u5206\u7C7B\u8DEF\u5F84\uFF0C\u5982 '\u6807\u7ACB\u65B9/\u7B7E\u7AE0' \u6216 '\u901A\u7528'"),
536
+ title: tool.schema.string().describe("\u8BB0\u5FC6\u6807\u9898\uFF0C\u5982 '\u7B7E\u7AE0\u670D\u52A1WS\u901A\u4FE1\u673A\u5236'"),
537
+ content: tool.schema.string().describe("\u8981\u5199\u5165\u7684\u8BE6\u7EC6\u5185\u5BB9")
538
+ },
539
+ async execute(args, context) {
540
+ const { directory } = context;
541
+ await log("info", `[haimati_write] \u5165\u53C2: category="${args.category}", title="${args.title}", content\u957F\u5EA6=${args.content.length}\u5B57\u7B26, content=
542
+ ${truncateForLog(args.content)}
543
+ `);
544
+ try {
545
+ const now = (/* @__PURE__ */ new Date()).toISOString();
546
+ let newId = null;
547
+ for (let retry = 0; retry < 3; retry++) {
548
+ newId = await allocateIdWithLock(directory, args.category, args.title, now, args.content);
549
+ if (newId) break;
550
+ await log("warn", `[haimati_write] \u9501\u83B7\u53D6\u5931\u8D25\uFF0C\u91CD\u8BD5\u7B2C ${retry + 1} \u6B21`);
551
+ }
552
+ if (newId) {
553
+ await log("debug", `[haimati_write] \u5206\u914D\u65B0\u5E8F\u53F7: #${newId}`);
554
+ await log("debug", `[haimati_write] \u4E66\u9875\u548C\u7D22\u5F15\u5DF2\u66F4\u65B0: \u4E66\u9875/${getPageGroupDir(directory, newId).split(/[\\/]/).pop()}/${newId}.md`);
555
+ const result2 = `\u5DF2\u5199\u5165\u6D77\u9A6C\u4F53 #${newId}
556
+ \u8DEF\u5F84: ${args.category}/${args.title}
557
+ \u7248\u672C: 1`;
558
+ await log("info", `[haimati_write] \u51FA\u53C2:
559
+ ${truncateForLog(result2)}`);
560
+ return result2;
561
+ }
562
+ const entries = await readIndexAsync(directory);
563
+ const existingEntry = findEntryByPath(entries, args.category, args.title);
564
+ if (existingEntry) {
565
+ const page = await readPageAsync(directory, existingEntry.id);
566
+ const version = page ? page.version : 1;
567
+ const result2 = `\u5DF2\u66F4\u65B0\u6D77\u9A6C\u4F53 #${existingEntry.id}
568
+ \u8DEF\u5F84: ${args.category}/${args.title}
569
+ \u7248\u672C: ${version}`;
570
+ await log("info", `[haimati_write] \u51FA\u53C2:
571
+ ${truncateForLog(result2)}`);
572
+ return result2;
573
+ }
574
+ const result = `\u9519\u8BEF\uFF1A\u65E0\u6CD5\u5206\u914D\u5E8F\u53F7\uFF08\u9501\u7B49\u5F85\u8D85\u65F6\uFF09`;
575
+ await log("error", `[haimati_write] \u51FA\u53C2:
576
+ ${truncateForLog(result)}`);
577
+ return result;
578
+ } catch (error) {
579
+ const errorMsg = error instanceof Error ? error.message : String(error);
580
+ const stack = error instanceof Error ? error.stack : "";
581
+ await log("error", `[haimati_write] \u51FA\u53C2: \u9519\u8BEF: ${errorMsg}
582
+ \u5806\u6808: ${stack}`);
583
+ return `\u9519\u8BEF: ${errorMsg}`;
584
+ }
585
+ }
586
+ }),
587
+ /**
588
+ * haimati_search - 搜索海马体记忆系统
589
+ *
590
+ * 在标题、分类、序号和书页内容中进行搜索
591
+ */
592
+ haimati_search: tool({
593
+ description: "\u641C\u7D22\u6D77\u9A6C\u4F53\u8BB0\u5FC6\u7CFB\u7EDF\uFF08\u652F\u6301\u591A\u5173\u952E\u5B57\uFF0C\u7528\u7A7A\u683C\u5206\u9694\uFF09\u3002\u641C\u7D22\u8303\u56F4\u5305\u62EC\uFF1A\u6807\u9898\u3001\u5206\u7C7B\u3001\u5E8F\u53F7\u3001\u4E66\u9875\u5185\u5BB9\u3002\u8F93\u5165\u5173\u952E\u8BCD\uFF0C\u8FD4\u56DE\u5339\u914D\u7684\u6761\u76EE\u5217\u8868\u548C\u5185\u5BB9\u7247\u6BB5\uFF0C\u652F\u6301\u5206\u9875\uFF08limit/offset\uFF09\u3002",
594
+ args: {
595
+ keyword: tool.schema.string().describe("\u641C\u7D22\u5173\u952E\u8BCD\uFF08\u652F\u6301\u591A\u5173\u952E\u5B57\uFF0C\u7528\u7A7A\u683C\u5206\u9694\uFF09"),
596
+ match: tool.schema.string().describe("\u5339\u914D\u6A21\u5F0F\uFF1Aand=\u6240\u6709\u5173\u952E\u5B57\u90FD\u5339\u914D\uFF0Cor=\u4EFB\u610F\u5173\u952E\u5B57\u5339\u914D"),
597
+ limit: tool.schema.number().default(10).describe("\u8FD4\u56DE\u7ED3\u679C\u6570\u91CF\u9650\u5236"),
598
+ offset: tool.schema.number().default(0).describe("\u5206\u9875\u504F\u79FB\u91CF\uFF0C\u7528\u4E8E\u83B7\u53D6\u540E\u7EED\u9875\u9762")
599
+ },
600
+ async execute(args, context) {
601
+ const { directory } = context;
602
+ const indexPath = getIndexPath(directory);
603
+ await log("info", `[haimati_search] \u5165\u53C2: keyword="${args.keyword}", match="${args.match}", limit=${args.limit || 10}, offset=${args.offset || 0}`);
604
+ if (!fs.existsSync(indexPath)) {
605
+ const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
606
+ await log("warn", `[haimati_search] \u51FA\u53C2:
607
+ ${truncateForLog(result)}`);
608
+ return result;
609
+ }
610
+ try {
611
+ const entries = await readIndexAsync(directory);
612
+ const keyword = args.keyword.trim();
613
+ const match = args.match;
614
+ const limit = args.limit || 10;
615
+ const offset = args.offset || 0;
616
+ const allMatched = await searchEntriesAsync(entries, keyword, match, directory, true);
617
+ const totalCount = allMatched.length;
618
+ const matched = allMatched.slice(offset, offset + limit);
619
+ if (matched.length === 0) {
620
+ const result2 = `\u672A\u627E\u5230\u5305\u542B '${keyword}' \u7684\u8BB0\u5FC6`;
621
+ await log("info", `[haimati_search] \u51FA\u53C2:
622
+ ${truncateForLog(result2)}`);
623
+ return result2;
624
+ }
625
+ const results = [];
626
+ const hasMore = offset + matched.length < totalCount;
627
+ const displayCount = hasMore ? `${offset + 1}-${offset + matched.length}/${totalCount}` : `${offset + 1}-${offset + matched.length}/${totalCount}`;
628
+ results.push(`\u627E\u5230 ${displayCount} \u6761\u5339\u914D\uFF1A
629
+ `);
630
+ if (hasMore) {
631
+ results.push(`\uFF08\u5982\u9700\u83B7\u53D6\u66F4\u591A\uFF0C\u8BF7\u4F7F\u7528 offset=${offset + limit} \u7EE7\u7EED\u7FFB\u9875\uFF09
632
+ `);
633
+ }
634
+ for (const entry of matched) {
635
+ const page = await readPageAsync(directory, entry.id);
636
+ const fullPath = `${entry.category}/${entry.title}`;
637
+ let excerpt = "";
638
+ if (page) {
639
+ const lines = page.content.split("\n").filter((l) => l.trim());
640
+ excerpt = lines.slice(0, 3).join(" | ").substring(0, 150);
641
+ if (excerpt.length === 150) excerpt += "...";
642
+ }
643
+ results.push(`## ${fullPath} [${entry.id}]`);
644
+ if (excerpt) {
645
+ results.push(`> ${excerpt}`);
646
+ }
647
+ results.push("");
648
+ }
649
+ const result = results.join("\n");
650
+ await log("info", `[haimati_search] \u51FA\u53C2:
651
+ ${truncateForLog(result)}`);
652
+ return result;
653
+ } catch (error) {
654
+ const errorMsg = error instanceof Error ? error.message : String(error);
655
+ const stack = error instanceof Error ? error.stack : "";
656
+ await log("error", `[haimati_search] \u51FA\u53C2: \u9519\u8BEF: ${errorMsg}
657
+ \u5806\u6808: ${stack}`);
658
+ return `\u9519\u8BEF: ${errorMsg}`;
659
+ }
660
+ }
661
+ }),
662
+ /**
663
+ * haimati_edit - 修改海马体中的记忆内容(部分替换)
664
+ *
665
+ * 使用流程:
666
+ * 1. 先调用 haimati_read 获取当前内容的行号和 version
667
+ * 2. 传入 offsetBegin/offsetEnd 指定替换范围,传入 content 替换内容
668
+ * 3. 传入 read 返回的 version 进行并发控制
669
+ *
670
+ * 并发控制原理:
671
+ * - 如果在你读取后、其他会话修改了该记忆,version会变化
672
+ * - 写入时检查 version 是否匹配,不匹配则拒绝写入
673
+ * - 写入成功后 version 自动+1
674
+ */
675
+ haimati_edit: tool({
676
+ description: "\u4FEE\u6539\u6D77\u9A6C\u4F53\u8BB0\u5FC6\u7684\u90E8\u5206\u5185\u5BB9\u3002\u5C06\u6307\u5B9A\u884C\u53F7\u8303\u56F4 [offsetBegin, offsetEnd) \u7684\u5185\u5BB9\u66FF\u6362\u4E3A\u65B0 content\u3002\u9700\u8981\u63D0\u4F9B read \u65F6\u8FD4\u56DE\u7684 version \u8FDB\u884C\u5E76\u53D1\u63A7\u5236\u3002",
677
+ args: {
678
+ query: tool.schema.string().describe("\u5E8F\u53F7\uFF0C\u7528\u4E8E\u5B9A\u4F4D\u8981\u66F4\u65B0\u7684\u8BB0\u5FC6"),
679
+ offsetBegin: tool.schema.number().describe("\u8D77\u59CB\u884C\u53F7\uFF08\u5305\u62EC\uFF09"),
680
+ offsetEnd: tool.schema.number().describe("\u7ED3\u675F\u884C\u53F7\uFF08\u4E0D\u5305\u62EC\uFF09"),
681
+ content: tool.schema.string().describe("\u66FF\u6362\u5185\u5BB9"),
682
+ version: tool.schema.number().describe("read \u65F6\u8FD4\u56DE\u7684 version\uFF0C\u7528\u4E8E\u5E76\u53D1\u63A7\u5236")
683
+ },
684
+ async execute(args, context) {
685
+ const { directory } = context;
686
+ const indexPath = getIndexPath(directory);
687
+ await log("info", `[haimati_edit] \u5165\u53C2: query="${args.query}", offsetBegin=${args.offsetBegin}, offsetEnd=${args.offsetEnd}, content\u957F\u5EA6=${args.content.length}\u5B57\u7B26, version=${args.version}, content=
688
+ ${truncateForLog(args.content)}
689
+ `);
690
+ if (!fs.existsSync(indexPath)) {
691
+ const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
692
+ await log("warn", `[haimati_edit] \u51FA\u53C2:
693
+ ${truncateForLog(result)}`);
694
+ return result;
695
+ }
696
+ try {
697
+ const entries = await readIndexAsync(directory);
698
+ const query = args.query.trim();
699
+ if (!/^\d+$/.test(query)) {
700
+ const result2 = `\u9519\u8BEF\uFF1Aquery \u5FC5\u987B\u662F\u5E8F\u53F7\uFF0C\u53EA\u652F\u6301\u5E8F\u53F7\u67E5\u8BE2`;
701
+ await log("warn", `[haimati_edit] \u51FA\u53C2:
702
+ ${truncateForLog(result2)}`);
703
+ return result2;
704
+ }
705
+ const entry = findEntryById(entries, query);
706
+ if (!entry) {
707
+ const result2 = `\u672A\u627E\u5230\u4E0E '${query}' \u76F8\u5173\u7684\u8BB0\u5FC6`;
708
+ await log("info", `[haimati_edit] \u51FA\u53C2:
709
+ ${truncateForLog(result2)}`);
710
+ return result2;
711
+ }
712
+ const page = await readPageAsync(directory, entry.id);
713
+ if (!page) {
714
+ const result2 = `\u672A\u627E\u5230\u4E66\u9875: #${entry.id}`;
715
+ await log("error", `[haimati_edit] \u51FA\u53C2:
716
+ ${truncateForLog(result2)}`);
717
+ return result2;
718
+ }
719
+ const offsetBegin = args.offsetBegin;
720
+ const offsetEnd = args.offsetEnd;
721
+ if (offsetBegin > page.content.split("\n").length || offsetBegin >= offsetEnd) {
722
+ const result2 = `\u9519\u8BEF\uFF1Aoffset \u975E\u6CD5\uFF0CoffsetBegin=${offsetBegin}, offsetEnd=${offsetEnd}, \u6587\u4EF6\u603B\u884C\u6570=${page.content.split("\n").length}`;
723
+ await log("warn", `[haimati_edit] \u51FA\u53C2:
724
+ ${truncateForLog(result2)}`);
725
+ return result2;
726
+ }
727
+ if (page.version !== args.version) {
728
+ const result2 = `\u9519\u8BEF\uFF1A\u7248\u672C\u51B2\u7A81\uFF0C\u5F53\u524D version=${page.version}\uFF0C\u4F20\u5165 version=${args.version}\uFF0C\u8BF7\u91CD\u65B0\u8BFB\u53D6\u540E\u518D\u64CD\u4F5C`;
729
+ await log("warn", `[haimati_edit] \u51FA\u53C2:
730
+ ${truncateForLog(result2)}`);
731
+ return result2;
732
+ }
733
+ const contentLines = page.content.split("\n");
734
+ const newContentLines = [
735
+ ...contentLines.slice(0, offsetBegin - 1),
736
+ // [0, offsetBegin-1) 保留
737
+ ...args.content.split("\n"),
738
+ // 插入新内容
739
+ ...contentLines.slice(offsetEnd - 1)
740
+ // [offsetEnd-1, end) 保留
741
+ ];
742
+ const newContent = newContentLines.join("\n");
743
+ const newVersion = page.version + 1;
744
+ await writePageAsync(directory, entry.id, entry.title, page.createdAt, newContent, newVersion);
745
+ const fullPath = `${entry.category}/${entry.title}`;
746
+ const result = `\u5DF2\u66F4\u65B0\u6D77\u9A6C\u4F53 #${entry.id}
747
+ \u8DEF\u5F84: ${fullPath}
748
+ \u65B0\u7248\u672C: ${newVersion}`;
749
+ await log("info", `[haimati_edit] \u51FA\u53C2:
750
+ ${truncateForLog(result)}`);
751
+ return result;
752
+ } catch (error) {
753
+ const errorMsg = error instanceof Error ? error.message : String(error);
754
+ const stack = error instanceof Error ? error.stack : "";
755
+ await log("error", `[haimati_edit] \u51FA\u53C2: \u9519\u8BEF: ${errorMsg}
756
+ \u5806\u6808: ${stack}`);
757
+ return `\u9519\u8BEF: ${errorMsg}`;
758
+ }
759
+ }
760
+ }),
761
+ /**
762
+ * haimati_delete - 删除海马体中的记忆
763
+ */
764
+ haimati_delete: tool({
765
+ description: "\u5220\u9664\u6D77\u9A6C\u4F53\u4E2D\u7684\u8BB0\u5FC6\u3002\u901A\u8FC7\u5E8F\u53F7\uFF08\u5982 '086'\uFF09\u6216\u5B8C\u6574\u8DEF\u5F84\uFF08\u5982 '\u6807\u7ACB\u65B9/\u7B7E\u7AE0/\u7B7E\u7AE0\u670D\u52A1WS\u901A\u4FE1\u673A\u5236'\uFF09\u6765\u5B9A\u4F4D\u8981\u5220\u9664\u7684\u8BB0\u5FC6\u3002",
766
+ args: {
767
+ query: tool.schema.string().describe("\u5E8F\u53F7\u6216\u5B8C\u6574\u8DEF\u5F84\uFF0C\u7528\u4E8E\u5B9A\u4F4D\u8981\u5220\u9664\u7684\u8BB0\u5FC6")
768
+ },
769
+ async execute(args, context) {
770
+ const { directory } = context;
771
+ const indexPath = getIndexPath(directory);
772
+ await log("info", `[haimati_delete] \u5165\u53C2: query="${args.query}"`);
773
+ if (!fs.existsSync(indexPath)) {
774
+ const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
775
+ await log("warn", `[haimati_delete] \u51FA\u53C2:
776
+ ${truncateForLog(result)}`);
777
+ return result;
778
+ }
779
+ try {
780
+ const entries = await readIndexAsync(directory);
781
+ const query = args.query.trim();
782
+ let entry = null;
783
+ if (/^\d+$/.test(query)) {
784
+ entry = findEntryById(entries, query);
785
+ } else {
786
+ const parts = query.split("/").filter(Boolean);
787
+ if (parts.length >= 2) {
788
+ const title = parts.pop();
789
+ const category = parts.join("/");
790
+ entry = findEntryByPath(entries, category, title);
791
+ } else {
792
+ const results = await searchEntriesAsync(entries, query, "or", directory);
793
+ if (results.length === 1) {
794
+ entry = results[0];
795
+ } else if (results.length > 1) {
796
+ const list = results.slice(0, 10).map((e) => ` - ${e.category}/${e.title} [${e.id}]`).join("\n");
797
+ return `\u627E\u5230\u591A\u6761\u5339\u914D\uFF0C\u8BF7\u4F7F\u7528\u5E8F\u53F7\u7CBE\u786E\u6307\u5B9A\uFF1A
798
+ ${list}`;
799
+ }
800
+ }
801
+ }
802
+ if (!entry) {
803
+ const result2 = `\u672A\u627E\u5230\u4E0E '${query}' \u76F8\u5173\u7684\u8BB0\u5FC6`;
804
+ await log("info", `[haimati_delete] \u51FA\u53C2:
805
+ ${truncateForLog(result2)}`);
806
+ return result2;
807
+ }
808
+ await deletePageAsync(directory, entry.id);
809
+ const newEntries = removeIndexEntry(entries, entry.id);
810
+ await writeIndexAsync(directory, newEntries);
811
+ const fullPath = `${entry.category}/${entry.title}`;
812
+ const result = `\u5DF2\u5220\u9664\u6D77\u9A6C\u4F53 #${entry.id}
813
+ \u8DEF\u5F84: ${fullPath}`;
814
+ await log("info", `[haimati_delete] \u51FA\u53C2:
815
+ ${truncateForLog(result)}`);
816
+ return result;
817
+ } catch (error) {
818
+ const errorMsg = error instanceof Error ? error.message : String(error);
819
+ const stack = error instanceof Error ? error.stack : "";
820
+ await log("error", `[haimati_delete] \u51FA\u53C2: \u9519\u8BEF: ${errorMsg}
821
+ \u5806\u6808: ${stack}`);
822
+ return `\u9519\u8BEF: ${errorMsg}`;
823
+ }
824
+ }
825
+ }),
826
+ /**
827
+ * haimati_move - 修改记忆的分类路径
828
+ */
829
+ haimati_move: tool({
830
+ description: "\u4FEE\u6539\u6D77\u9A6C\u4F53\u8BB0\u5FC6\u7684\u5206\u7C7B\u8DEF\u5F84\u3002\u901A\u8FC7\u5E8F\u53F7\u6216\u5B8C\u6574\u8DEF\u5F84\u5B9A\u4F4D\u8BB0\u5FC6\uFF0C\u7136\u540E\u5C06\u5176\u79FB\u52A8\u5230\u65B0\u7684\u5206\u7C7B\u3002",
831
+ args: {
832
+ query: tool.schema.string().describe("\u5E8F\u53F7\u6216\u5B8C\u6574\u8DEF\u5F84\uFF0C\u7528\u4E8E\u5B9A\u4F4D\u8981\u79FB\u52A8\u7684\u8BB0\u5FC6"),
833
+ newCategory: tool.schema.string().describe("\u65B0\u7684\u5206\u7C7B\u8DEF\u5F84\uFF0C\u5982 '\u6807\u7ACB\u65B9/\u5BA2\u6237\u7AEF' \u6216 '\u901A\u7528'")
834
+ },
835
+ async execute(args, context) {
836
+ const { directory } = context;
837
+ const indexPath = getIndexPath(directory);
838
+ await log("info", `[haimati_move] \u5165\u53C2: query="${args.query}", newCategory="${args.newCategory}"`);
839
+ if (!fs.existsSync(indexPath)) {
840
+ const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
841
+ await log("warn", `[haimati_move] \u51FA\u53C2:
842
+ ${truncateForLog(result)}`);
843
+ return result;
844
+ }
845
+ try {
846
+ const entries = await readIndexAsync(directory);
847
+ const query = args.query.trim();
848
+ let entry = null;
849
+ if (/^\d+$/.test(query)) {
850
+ entry = findEntryById(entries, query);
851
+ } else {
852
+ const parts = query.split("/").filter(Boolean);
853
+ if (parts.length >= 2) {
854
+ const title = parts.pop();
855
+ const category = parts.join("/");
856
+ entry = findEntryByPath(entries, category, title);
857
+ } else {
858
+ const results = await searchEntriesAsync(entries, query, "or", directory);
859
+ if (results.length === 1) {
860
+ entry = results[0];
861
+ } else if (results.length > 1) {
862
+ const list = results.slice(0, 10).map((e) => ` - ${e.category}/${e.title} [${e.id}]`).join("\n");
863
+ return `\u627E\u5230\u591A\u6761\u5339\u914D\uFF0C\u8BF7\u4F7F\u7528\u5E8F\u53F7\u7CBE\u786E\u6307\u5B9A\uFF1A
864
+ ${list}`;
865
+ }
866
+ }
867
+ }
868
+ if (!entry) {
869
+ const result2 = `\u672A\u627E\u5230\u4E0E '${query}' \u76F8\u5173\u7684\u8BB0\u5FC6`;
870
+ await log("info", `[haimati_move] \u51FA\u53C2:
871
+ ${truncateForLog(result2)}`);
872
+ return result2;
873
+ }
874
+ const oldCategory = entry.category;
875
+ const oldPath = `${oldCategory}/${entry.title}`;
876
+ const newCategory = args.newCategory;
877
+ const newPath = `${newCategory}/${entry.title}`;
878
+ const newEntries = entries.map((e) => {
879
+ if (e.id === entry.id) {
880
+ return { ...e, category: newCategory };
881
+ }
882
+ return e;
883
+ });
884
+ await writeIndexAsync(directory, newEntries);
885
+ const result = `\u5DF2\u79FB\u52A8\u6D77\u9A6C\u4F53 #${entry.id}
886
+ \u65E7\u8DEF\u5F84: ${oldPath}
887
+ \u65B0\u8DEF\u5F84: ${newPath}`;
888
+ await log("info", `[haimati_move] \u51FA\u53C2:
889
+ ${truncateForLog(result)}`);
890
+ return result;
891
+ } catch (error) {
892
+ const errorMsg = error instanceof Error ? error.message : String(error);
893
+ const stack = error instanceof Error ? error.stack : "";
894
+ await log("error", `[haimati_move] \u51FA\u53C2: \u9519\u8BEF: ${errorMsg}
895
+ \u5806\u6808: ${stack}`);
896
+ return `\u9519\u8BEF: ${errorMsg}`;
897
+ }
898
+ }
899
+ }),
900
+ /**
901
+ * haimati_list - 列出海马体索引结构
902
+ */
903
+ haimati_list: tool({
904
+ description: "\u5217\u51FA\u6D77\u9A6C\u4F53\u8BB0\u5FC6\u7CFB\u7EDF\u7684\u5B8C\u6574\u7D22\u5F15\u7ED3\u6784\u3002",
905
+ args: {
906
+ category: tool.schema.string().optional().describe("\u53EF\u9009\u7684\u5206\u7C7B\u8DEF\u5F84\uFF0C\u5217\u51FA\u8BE5\u5206\u7C7B\u4E0B\u7684\u6761\u76EE"),
907
+ recursive: tool.schema.boolean().optional().default(false).describe("\u662F\u5426\u9012\u5F52\u904D\u5386\u5B50\u5206\u7C7B\uFF0C\u9ED8\u8BA4false")
908
+ },
909
+ async execute(args, context) {
910
+ const { directory } = context;
911
+ const indexPath = getIndexPath(directory);
912
+ await log("info", `[haimati_list] \u5165\u53C2: category="${args.category || "(\u65E0)"}, recursive=${args.recursive}"`);
913
+ if (!fs.existsSync(indexPath)) {
914
+ const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
915
+ await log("warn", `[haimati_list] \u51FA\u53C2:
916
+ ${truncateForLog(result)}`);
917
+ return result;
918
+ }
919
+ try {
920
+ const entries = await readIndexAsync(directory);
921
+ if (args.category) {
922
+ const indexContent = fs.readFileSync(getIndexPath(directory), "utf-8");
923
+ const lines = indexContent.split("\n");
924
+ let targetDepth = 0;
925
+ let currentPath = [];
926
+ for (const line of lines) {
927
+ if (!line.trim()) continue;
928
+ const depth = getTreeDepth(line);
929
+ const entryMatch = line.match(/^\s*[│├└─\s]*(.+?)\s*-\s*(\d+)\s*$/);
930
+ if (entryMatch) continue;
931
+ const name = extractCategoryName(line);
932
+ if (name) {
933
+ if (name === "/") {
934
+ currentPath[depth] = "";
935
+ } else {
936
+ currentPath[depth] = name;
937
+ }
938
+ currentPath = currentPath.slice(0, depth + 1);
939
+ const fullPath = currentPath.filter(Boolean).join("/");
940
+ if (fullPath === args.category) {
941
+ targetDepth = depth;
942
+ break;
943
+ }
944
+ }
945
+ }
946
+ const subCategories = [];
947
+ const directEntries = [];
948
+ currentPath = [];
949
+ for (const line of lines) {
950
+ if (!line.trim()) continue;
951
+ const depth = getTreeDepth(line);
952
+ const entryMatch = line.match(/^\s*[│├└─\s]*(.+?)\s*-\s*(\d+)\s*$/);
953
+ if (entryMatch) {
954
+ const title = entryMatch[1].trim();
955
+ const id = entryMatch[2];
956
+ if (depth === targetDepth + 1) {
957
+ const entryCategory = currentPath.slice(1, depth).filter(Boolean).join("/");
958
+ if (entryCategory === args.category) {
959
+ directEntries.push({ category: entryCategory, title, id });
960
+ }
961
+ }
962
+ continue;
963
+ }
964
+ const name = extractCategoryName(line);
965
+ if (name) {
966
+ if (name === "/") {
967
+ currentPath[depth] = "";
968
+ } else {
969
+ currentPath[depth] = name;
970
+ }
971
+ currentPath = currentPath.slice(0, depth + 1);
972
+ if (depth === targetDepth + 1) {
973
+ const fullPath = currentPath.filter(Boolean).join("/");
974
+ if (fullPath === args.category) {
975
+ subCategories.push(name);
976
+ }
977
+ }
978
+ }
979
+ }
980
+ let result2 = `## ${args.category}
981
+
982
+ `;
983
+ if (args.recursive) {
984
+ const allDescendants = entries.filter(
985
+ (e) => e.category === args.category || e.category.startsWith(args.category + "/")
986
+ );
987
+ const grouped = /* @__PURE__ */ new Map();
988
+ for (const entry of allDescendants) {
989
+ const rel = entry.category.slice(args.category.length + 1);
990
+ const firstPart = rel.split("/")[0] || "(\u76F4\u63A5)";
991
+ if (!grouped.has(firstPart)) {
992
+ grouped.set(firstPart, []);
993
+ }
994
+ grouped.get(firstPart).push(entry);
995
+ }
996
+ for (const [group, groupEntries] of grouped) {
997
+ result2 += `### ${group}/
998
+ `;
999
+ result2 += groupEntries.map((e) => `- ${e.title} - ${e.id}`).join("\n") + "\n\n";
1000
+ }
1001
+ } else {
1002
+ if (subCategories.length > 0) {
1003
+ result2 += "**\u5B50\u5206\u7C7B\uFF1A**\n" + subCategories.map((c) => `- ${c}/`).join("\n") + "\n\n";
1004
+ }
1005
+ if (directEntries.length > 0) {
1006
+ result2 += "**\u76F4\u63A5\u6761\u76EE\uFF1A**\n" + directEntries.map((e) => `- ${e.title} - ${e.id}`).join("\n");
1007
+ }
1008
+ }
1009
+ await log("info", `[haimati_list] \u51FA\u53C2:
1010
+ ${truncateForLog(result2)}`);
1011
+ return result2;
1012
+ }
1013
+ const tree = buildIndexTree(entries);
1014
+ const result = `## \u6D77\u9A6C\u4F53\u7D22\u5F15
1015
+
1016
+ ${tree || "(\u7A7A)"}`;
1017
+ await log("info", `[haimati_list] \u51FA\u53C2:
1018
+ ${truncateForLog(result)}`);
1019
+ return result;
1020
+ } catch (error) {
1021
+ const errorMsg = error instanceof Error ? error.message : String(error);
1022
+ const stack = error instanceof Error ? error.stack : "";
1023
+ await log("error", `[haimati_list] \u51FA\u53C2: \u9519\u8BEF: ${errorMsg}
1024
+ \u5806\u6808: ${stack}`);
1025
+ return `\u9519\u8BEF: ${errorMsg}`;
1026
+ }
1027
+ }
1028
+ })
1029
+ },
1030
+ /**
1031
+ * session.created - 会话创建时注入海马体规则
1032
+ * 在每次新会话开始时,自动将海马体三大原则注入到上下文中
1033
+ */
1034
+ event: async ({ event: evt }) => {
1035
+ if (evt.type !== "session.created") {
1036
+ return;
1037
+ }
1038
+ try {
1039
+ const sessionId = evt.properties?.info?.id;
1040
+ if (!sessionId) {
1041
+ await log("warn", "[session.created] \u65E0\u6CD5\u83B7\u53D6 sessionID\uFF0C\u8DF3\u8FC7\u6CE8\u5165");
1042
+ return;
1043
+ }
1044
+ await ctx.client.session.prompt({
1045
+ path: { id: sessionId },
1046
+ body: {
1047
+ noReply: true,
1048
+ parts: [{
1049
+ type: "text",
1050
+ text: `## \u6D77\u9A6C\u4F53\u8BB0\u5FC6\u7CFB\u7EDF\u4F7F\u7528\u89C4\u5219
1051
+
1052
+ \u4F60\u6709\u4E00\u4E2A\u6D77\u9A6C\u4F53\u8BB0\u5FC6\u7CFB\u7EDF\u53EF\u7528\uFF0C\u7528\u4E8E\u5B58\u50A8\u548C\u68C0\u7D22\u8DE8\u4F1A\u8BDD\u7684\u77E5\u8BC6\u3002
1053
+
1054
+ ### \u6D77\u9A6C\u4F53\u4E09\u5927\u539F\u5219
1055
+
1056
+ 1. **\u4FE1\u606F\u4E0D\u4E22\u5931**: \u5B8C\u6574\u4FDD\u7559\u6240\u6709\u6709\u4EF7\u503C\u7684\u4FE1\u606F\uFF0C\u8BB0\u4F4F
1057
+ - \u5B8C\u6574\u7684\u4E1A\u52A1\u548C\u6280\u672F\u77E5\u8BC6
1058
+ - \u4E0E\u7528\u6237\u7684\u9700\u6C42\u9897\u7C92\u5EA6\u5BF9\u9F50
1059
+ - \u9009\u62E9\u7684\u51B3\u7B56\uFF0C\u4EE5\u53CA\u597D\u5904\u4E0E\u574F\u5904
1060
+ - xxx\u662F\u4EC0\u4E48\u3001\u4E3A\u4EC0\u4E48xxx\uFF08\u4E3A\u4EC0\u4E48\u8FD9\u4E48\u53BB\u505A\uFF09\u3001xxx\u600E\u4E48\u529E\uFF08\u9047\u5230\u67D0\u67D0\u60C5\u51B5\uFF0C\u662F\u600E\u4E48\u529E\u7684\uFF09
1061
+ - \u7B2C\u4E09\u65B9\u63A5\u53E3\u5B8C\u6574\u7684\u5165\u53C2\u3001\u51FA\u53C2\u3001\u8BF4\u660E\u3001\u8C03\u7528\u65F6\u673A
1062
+ - \u5176\u4ED6\u5404\u79CD\u6709\u4EF7\u503C\u7684\u77E5\u8BC6
1063
+
1064
+ 2. **\u9519\u8BEF\u4FE1\u606F\u4FEE\u6B63**: \u4E0D\u518D\u4FDD\u7559\u5DF2\u88AB\u7EA0\u6B63\u7684\u9519\u8BEF\u4FE1\u606F\u3001\u4E0D\u4FDD\u7559\u5DF2\u7ECF\u8FC7\u671F\u7684\u5E9F\u5F03\u7684\u4FE1\u606F
1065
+
1066
+ 3. **\u62D2\u7EDD\u5197\u4F59**: \u4E0D\u8981\u91CD\u590D\u8BB0\u5F55\u76F8\u540C\u5185\u5BB9\uFF0C\u8282\u7EA6TOKEN
1067
+
1068
+ ### \u53EF\u7528\u5DE5\u5177
1069
+
1070
+ **\u6CE8\u610F**\uFF1A\u4EE5\u4E0B\u5DE5\u5177\u5217\u8868\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u4F7F\u7528\u5E8F\u53F7\u67E5\u8BE2\u4EE3\u66FF\u8DEF\u5F84\u67E5\u8BE2\u3002haimati_read/haimati_edit \u8FD4\u56DE\u7684 version \u7528\u4E8E\u5E76\u53D1\u63A7\u5236\u3002
1071
+
1072
+ - \`haimati_read\` - \u8BFB\u53D6\u8BB0\u5FC6\u5185\u5BB9\uFF08\u53EA\u652F\u6301\u5E8F\u53F7\u67E5\u8BE2\uFF0C\u652F\u6301\u5206\u9875\uFF0C\u8FD4\u56DE\u5E26\u884C\u53F7\u548C\u7248\u672C\u53F7\uFF09
1073
+ - \`haimati_write\` - \u5199\u5165\u65B0\u8BB0\u5FC6\uFF08\u8986\u76D6\u5DF2\u6709\u5185\u5BB9\u65F6\u7248\u672C+1\uFF09
1074
+ - \`haimati_search\` - \u641C\u7D22\u8BB0\u5FC6\u5185\u5BB9\uFF08\u641C\u7D22\u8303\u56F4\uFF1A\u6807\u9898\u3001\u5206\u7C7B\u3001\u5E8F\u53F7\u3001\u4E66\u9875\u5185\u5BB9\u3002**\u6CE8\u610F\uFF1Amatch\u53C2\u6570\u5FC5\u586B\uFF0Cand=\u6240\u6709\u5173\u952E\u5B57\u90FD\u5339\u914D\uFF0Cor=\u4EFB\u610F\u5173\u952E\u5B57\u5339\u914D**\uFF09
1075
+ - \`haimati_edit\` - \u4FEE\u6539\u8BB0\u5FC6\u5185\u5BB9\uFF08\u90E8\u5206\u66FF\u6362\uFF0C\u901A\u8FC7\u884C\u53F7\u8303\u56F4\u66FF\u6362\u5185\u5BB9\uFF0C\u652F\u6301\u7248\u672C\u5E76\u53D1\u63A7\u5236\uFF09
1076
+ - \`haimati_move\` - \u4FEE\u6539\u8BB0\u5FC6\u7684\u5206\u7C7B\u8DEF\u5F84
1077
+ - \`haimati_delete\` - \u5220\u9664\u8BB0\u5FC6
1078
+ - \`haimati_list\` - \u5217\u51FA\u8BB0\u5FC6\u7D22\u5F15
1079
+
1080
+ ### \u4F7F\u7528\u65F6\u673A
1081
+
1082
+ **\u4F1A\u8BDD\u5F00\u59CB\u524D**: \u4F7F\u7528 haimati_list \u6216 haimati_search \u8BFB\u53D6\u4E0E\u5F53\u524D\u4EFB\u52A1\u76F8\u5173\u7684\u8BB0\u5FC6\u5185\u5BB9\uFF0C\u518D\u5F00\u59CB\u5DE5\u4F5C\u3002
1083
+
1084
+ **\u4F1A\u8BDD\u7ED3\u675F\u540E**: \u4F7F\u7528 haimati_write \u5C06\u65B0\u83B7\u5F97\u7684\u77E5\u8BC6\u3001\u7ECF\u9A8C\u3001\u51B3\u7B56\u5199\u5165\u6D77\u9A6C\u4F53\uFF0C\u4FDD\u6301\u4FE1\u606F\u7684\u6301\u7EED\u79EF\u7D2F\u3002`
1085
+ }]
1086
+ }
1087
+ });
1088
+ await log("info", `[session.created] \u6D77\u9A6C\u4F53\u89C4\u5219\u5DF2\u6CE8\u5165\u65B0\u4F1A\u8BDD (sessionID: ${sessionId})`);
1089
+ } catch (error) {
1090
+ const errorMsg = error instanceof Error ? error.message : String(error);
1091
+ await log("error", `[session.created] \u6CE8\u5165\u6D77\u9A6C\u4F53\u89C4\u5219\u5931\u8D25: ${errorMsg}`);
1092
+ }
1093
+ }
1094
+ };
1095
+ };
1096
+ export {
1097
+ HaimatiPlugin
1098
+ };
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "opencode-haimati",
3
+ "version": "1.0.2",
4
+ "description": "OpenCode plugin for a file-based memory system inspired by the hippocampus, providing long-term memory storage and retrieval across sessions.",
5
+ "main": "./dist/index.js",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "files": ["dist"],
9
+ "scripts": {
10
+ "build": "npx esbuild index.ts --bundle --platform=node --outdir=dist --format=esm --external:@opencode-ai/plugin && cp package.json dist/",
11
+ "prepublishOnly": "npm run build",
12
+ "test": "npx tsx test/index.ts"
13
+ },
14
+ "peerDependencies": {
15
+ "@opencode-ai/plugin": ">=0.15.0",
16
+ "typescript": ">=5.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@opencode-ai/plugin": "^0.15.0",
20
+ "@types/bun": "^1.3.1",
21
+ "typescript": "^5.9.3"
22
+ }
23
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "opencode-haimati",
3
+ "version": "1.0.2",
4
+ "description": "OpenCode plugin for a file-based memory system inspired by the hippocampus, providing long-term memory storage and retrieval across sessions.",
5
+ "main": "./dist/index.js",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "files": ["dist"],
9
+ "scripts": {
10
+ "build": "npx esbuild index.ts --bundle --platform=node --outdir=dist --format=esm --external:@opencode-ai/plugin && cp package.json dist/",
11
+ "prepublishOnly": "npm run build",
12
+ "test": "npx tsx test/index.ts"
13
+ },
14
+ "peerDependencies": {
15
+ "@opencode-ai/plugin": ">=0.15.0",
16
+ "typescript": ">=5.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@opencode-ai/plugin": "^0.15.0",
20
+ "@types/bun": "^1.3.1",
21
+ "typescript": "^5.9.3"
22
+ }
23
+ }