opencode-haimati 1.0.2 → 1.0.4

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
@@ -7,10 +7,12 @@
7
7
  - 🧠 类海马体记忆机制:模拟人脑记忆系统的分类存储
8
8
  - 📁 树形索引结构:通过 `.haimati/索引.md` 维护记忆索引
9
9
  - 📚 分组书页存储:记忆内容按序号分组存储于 `.haimati/书页/` 目录
10
- - 🔍 多模式查询:支持序号、路径、标题精确匹配和模糊搜索
11
- - ✏️ 完整 CRUD:读取、写入、更新、删除、移动记忆
10
+ - 🔍 多模式搜索:支持序号精确查询和关键字模糊搜索
11
+ - ✏️ 完整 CRUD:读取、写入、更新(部分替换)、删除、移动记忆
12
+ - 🔄 版本并发控制:基于 version 的乐观锁,防止多会话并发冲突
12
13
  - 🔄 自动规则注入:新会话自动注入海马体使用原则
13
14
  - 📋 日志追踪:完整的操作日志记录
15
+ - ⚡ 纯异步架构:所有文件操作非阻塞执行
14
16
 
15
17
  ## 安装
16
18
 
@@ -35,8 +37,10 @@ bun add -g opencode-haimati
35
37
 
36
38
  ```
37
39
  .haimati/
38
- ├── 索引.md # 树形结构索引
39
- └── 书页/ # 存储各记忆内容(按序号分组)
40
+ ├── .lock # 序号分配锁
41
+ ├── 索引.md # 树形结构索引
42
+ ├── next-id.txt # 下一个可用序号
43
+ └── 书页/ # 存储各记忆内容(按序号分组)
40
44
  ├── 001-050/
41
45
  │ ├── 001.md
42
46
  │ └── 002.md
@@ -46,13 +50,15 @@ bun add -g opencode-haimati
46
50
 
47
51
  ## 可用工具
48
52
 
49
- - `haimati_read` - 从海马体读取记忆(支持序号、路径、标题查询)
50
- - `haimati_write` - 写入新记忆
51
- - `haimati_search` - 搜索记忆内容
52
- - `haimati_update` - 更新已有记忆的内容
53
- - `haimati_move` - 修改记忆的分类路径
54
- - `haimati_delete` - 删除记忆
55
- - `haimati_list` - 列出记忆索引
53
+ | 工具 | 说明 |
54
+ |------|------|
55
+ | `haimati_read` | 读取记忆内容(只支持序号查询,支持分页,返回带行号和版本号) |
56
+ | `haimati_write` | 写入新记忆(覆盖已有内容时版本+1) |
57
+ | `haimati_search` | 搜索记忆内容(标题、分类、序号、书页内容) |
58
+ | `haimati_edit` | 修改记忆内容(部分替换,支持版本并发控制) |
59
+ | `haimati_move` | 修改记忆的分类路径 |
60
+ | `haimati_delete` | 删除记忆 |
61
+ | `haimati_list` | 列出记忆索引 |
56
62
 
57
63
  ## 海马体三大原则
58
64
 
@@ -65,24 +71,46 @@ bun add -g opencode-haimati
65
71
  ```typescript
66
72
  // 写入新记忆
67
73
  haimati_write({
68
- category: "标立方/签章",
74
+ category: "xxx项目/签章",
69
75
  title: "签章服务WS通信机制",
70
76
  content: "WebSocket通信采用base64编码..."
71
77
  })
72
78
 
73
- // 读取记忆
79
+ // 读取记忆(只支持序号查询)
74
80
  haimati_read({ query: "086" })
75
- // 或
76
- haimati_read({ query: "标立方/签章/签章服务WS通信机制" })
77
81
 
78
82
  // 搜索记忆
79
- haimati_search({ keyword: "WebSocket", limit: 10 })
83
+ haimati_search({ keyword: "WebSocket", match: "or", limit: 10 })
84
+
85
+ // 部分修改记忆
86
+ haimati_edit({
87
+ query: "086",
88
+ offsetBegin: 3,
89
+ offsetEnd: 5,
90
+ content: "新的替换内容",
91
+ version: 1
92
+ })
93
+
94
+ // 移动记忆到新分类
95
+ haimati_move({
96
+ query: "086",
97
+ newCategory: "xxx项目/客户端"
98
+ })
99
+
100
+ // 删除记忆
101
+ haimati_delete({ query: "086" })
102
+
103
+ // 列出所有记忆
104
+ haimati_list({})
105
+
106
+ // 列出指定分类下的记忆
107
+ haimati_list({ category: "xxx项目", recursive: true })
80
108
  ```
81
109
 
82
110
  ## 索引格式示例
83
111
 
84
112
  ```
85
- 标立方/
113
+ xxx项目/
86
114
  ├── 签章/
87
115
  │ └── 签章服务WS通信机制 - 086
88
116
  └── 通用/
@@ -97,4 +125,4 @@ haimati_search({ keyword: "WebSocket", limit: 10 })
97
125
 
98
126
  ## 许可证
99
127
 
100
- MIT
128
+ MIT
package/dist/index.js CHANGED
@@ -3,24 +3,31 @@ import fs from "fs";
3
3
  import path from "path";
4
4
  import os from "os";
5
5
  import { tool } from "@opencode-ai/plugin";
6
+ import { promisify } from "util";
7
+ var fsReadFile = promisify(fs.readFile);
8
+ var fsWriteFile = promisify(fs.writeFile);
9
+ var fsUnlink = promisify(fs.unlink);
10
+ var fsMkdir = promisify(fs.mkdir);
11
+ var fsExists = promisify(fs.exists);
12
+ var fsStat = promisify(fs.stat);
6
13
  var HAIMATI_DIR = ".haimati";
7
14
  var INDEX_FILE = "\u7D22\u5F15.md";
8
15
  var PAGES_DIR = "\u4E66\u9875";
9
16
  var NEXT_ID_FILE = "next-id.txt";
10
17
  var LOG_FILE = "haimati.log";
11
18
  var LOCK_MAX_WAIT = 5e3;
12
- var LOCK_RETRY_INTERVAL = 50;
13
19
  var HAIMATI_CONFIG_FILE = "opencode-haimati.conf";
14
20
  var HAIMATI_PATH_KEY = "haimati-path";
15
21
  var VERSION_SEPARATOR = "\n---\nversion:";
16
22
  var DEFAULT_VERSION = 1;
17
- function getConfigHaimatiPath(directory) {
23
+ var AUTO_INJECT_CONTEXT = false;
24
+ async function getConfigHaimatiPath(directory) {
18
25
  const configPath = path.join(directory, HAIMATI_CONFIG_FILE);
19
- if (!fs.existsSync(configPath)) {
26
+ if (!await fsExists(configPath)) {
20
27
  return null;
21
28
  }
22
29
  try {
23
- const content = fs.readFileSync(configPath, "utf-8");
30
+ const content = await fsReadFile(configPath, "utf-8");
24
31
  const lines = content.split("\n");
25
32
  for (const line of lines) {
26
33
  const trimmed = line.trim();
@@ -41,35 +48,36 @@ function getConfigHaimatiPath(directory) {
41
48
  }
42
49
  return null;
43
50
  }
44
- function getHaimatiDir(directory) {
45
- const configPath = getConfigHaimatiPath(directory);
51
+ async function getHaimatiDir(directory) {
52
+ const configPath = await getConfigHaimatiPath(directory);
46
53
  if (configPath) {
47
54
  return configPath;
48
55
  }
49
56
  return path.join(directory, HAIMATI_DIR);
50
57
  }
51
- function getIndexPath(directory) {
52
- return path.join(getHaimatiDir(directory), INDEX_FILE);
58
+ async function getIndexPath(directory) {
59
+ return path.join(await getHaimatiDir(directory), INDEX_FILE);
53
60
  }
54
- function getNextIdPath(directory) {
55
- return path.join(getHaimatiDir(directory), NEXT_ID_FILE);
61
+ async function getNextIdPath(directory) {
62
+ return path.join(await getHaimatiDir(directory), NEXT_ID_FILE);
56
63
  }
57
- function getLockPath(directory) {
58
- return path.join(getHaimatiDir(directory), ".lock");
64
+ async function getLockPath(directory) {
65
+ return path.join(await getHaimatiDir(directory), ".lock");
59
66
  }
60
- function getPagesDir(directory) {
61
- return path.join(getHaimatiDir(directory), PAGES_DIR);
67
+ async function getPagesDir(directory) {
68
+ return path.join(await getHaimatiDir(directory), PAGES_DIR);
62
69
  }
63
- function getPageGroupDir(directory, id) {
70
+ async function getPageGroupDir(directory, id) {
64
71
  const n = parseInt(id, 10);
65
72
  const groupStart = Math.floor((n - 1) / 50) * 50 + 1;
66
73
  const groupEnd = groupStart + 49;
67
74
  const groupName = `${String(groupStart).padStart(3, "0")}-${String(groupEnd).padStart(3, "0")}`;
68
- return path.join(getPagesDir(directory), groupName);
75
+ return path.join(await getPagesDir(directory), groupName);
69
76
  }
70
- function getPagePath(directory, id) {
71
- return path.join(getPageGroupDir(directory, id), `${id}.md`);
77
+ async function getPagePath(directory, id) {
78
+ return path.join(await getPageGroupDir(directory, id), `${id}.md`);
72
79
  }
80
+ var fsAppendFile = promisify(fs.appendFile);
73
81
  function getLogFilePath() {
74
82
  const osTmpDir = os.tmpdir();
75
83
  return path.join(osTmpDir, "haimati_logs", LOG_FILE);
@@ -81,12 +89,12 @@ async function writeLogToFile(message) {
81
89
  const timeStr = now.toISOString().replace("T", " ").split(".")[0];
82
90
  const logPath = getLogFilePath();
83
91
  const logDir = path.dirname(logPath);
84
- if (!fs.existsSync(logDir)) {
85
- fs.mkdirSync(logDir, { recursive: true });
92
+ if (!await fsExists(logDir)) {
93
+ await fsMkdir(logDir, { recursive: true });
86
94
  }
87
95
  let shouldOverwrite = false;
88
- if (fs.existsSync(logPath)) {
89
- const mtime = fs.statSync(logPath).mtime;
96
+ if (await fsExists(logPath)) {
97
+ const mtime = (await fsStat(logPath)).mtime;
90
98
  const fileDate = mtime.toISOString().split("T")[0];
91
99
  if (fileDate !== dateStr) {
92
100
  shouldOverwrite = true;
@@ -94,9 +102,9 @@ async function writeLogToFile(message) {
94
102
  }
95
103
  const logLine = `[${timeStr}] ${message}`;
96
104
  if (shouldOverwrite) {
97
- fs.writeFileSync(logPath, logLine + "\n", "utf-8");
105
+ await fsWriteFile(logPath, logLine + "\n", "utf-8");
98
106
  } else {
99
- fs.appendFileSync(logPath, logLine + "\n", "utf-8");
107
+ await fsAppendFile(logPath, logLine + "\n", "utf-8");
100
108
  }
101
109
  } catch (error) {
102
110
  console.error(`[haimati] \u5199\u5165\u65E5\u5FD7\u6587\u4EF6\u5931\u8D25:`, error);
@@ -181,7 +189,7 @@ function findEntryById(entries, id) {
181
189
  function findEntryByPath(entries, categoryPath, title) {
182
190
  return entries.find((e) => e.category === categoryPath && e.title === title) || null;
183
191
  }
184
- async function searchEntriesAsync(entries, keyword, matchMode, directory, searchContent = false) {
192
+ async function searchEntries(entries, keyword, matchMode, directory, searchContent = false) {
185
193
  const keywords = keyword.toLowerCase().split(/\s+/).filter((k) => k.length > 0);
186
194
  if (keywords.length === 0) return [];
187
195
  const matchedWithScore = [];
@@ -197,7 +205,7 @@ async function searchEntriesAsync(entries, keyword, matchMode, directory, search
197
205
  }
198
206
  }
199
207
  if (searchContent && directory) {
200
- const page = await readPageAsync(directory, e.id);
208
+ const page = await readPage(directory, e.id);
201
209
  if (page && page.content) {
202
210
  const contentLower = page.content.toLowerCase();
203
211
  for (const kw of keywords) {
@@ -221,12 +229,12 @@ async function searchEntriesAsync(entries, keyword, matchMode, directory, search
221
229
  matchedWithScore.sort((a, b) => b.score - a.score);
222
230
  return matchedWithScore.map((item) => item.entry);
223
231
  }
224
- async function readNextIdFromFileAsync(directory) {
225
- const nextIdPath = getNextIdPath(directory);
226
- if (!fs.existsSync(nextIdPath)) {
232
+ async function readNextIdFromFile(directory) {
233
+ const nextIdPath = await getNextIdPath(directory);
234
+ if (!await fsExists(nextIdPath)) {
227
235
  return 1;
228
236
  }
229
- const content = fs.readFileSync(nextIdPath, "utf-8");
237
+ const content = await fsReadFile(nextIdPath, "utf-8");
230
238
  const trimmed = content.trim();
231
239
  const parsed = parseInt(trimmed, 10);
232
240
  if (!isNaN(parsed) && parsed > 0) {
@@ -234,58 +242,55 @@ async function readNextIdFromFileAsync(directory) {
234
242
  }
235
243
  return 1;
236
244
  }
237
- async function initializeNextIdIfNeededAsync(directory) {
238
- const nextIdPath = getNextIdPath(directory);
239
- if (fs.existsSync(nextIdPath)) {
245
+ async function initializeNextIdIfNeeded(directory) {
246
+ const nextIdPath = await getNextIdPath(directory);
247
+ if (await fsExists(nextIdPath)) {
240
248
  return;
241
249
  }
242
- const entries = await readIndexAsync(directory);
250
+ const entries = await readIndex(directory);
243
251
  let maxId = 0;
244
252
  for (const e of entries) {
245
253
  const n = parseInt(e.id, 10);
246
254
  if (n > maxId) maxId = n;
247
255
  }
248
- await writeNextIdToFileAsync(directory, maxId + 1);
256
+ await writeNextIdToFile(directory, maxId + 1);
249
257
  }
250
- async function writeNextIdToFileAsync(directory, nextId) {
251
- const haimatiDir = getHaimatiDir(directory);
252
- if (!fs.existsSync(haimatiDir)) {
253
- fs.mkdirSync(haimatiDir, { recursive: true });
258
+ async function writeNextIdToFile(directory, nextId) {
259
+ const haimatiDir = await getHaimatiDir(directory);
260
+ if (!await fsExists(haimatiDir)) {
261
+ await fsMkdir(haimatiDir, { recursive: true });
254
262
  }
255
- const nextIdPath = getNextIdPath(directory);
256
- fs.writeFileSync(nextIdPath, String(nextId), "utf-8");
263
+ const nextIdPath = await getNextIdPath(directory);
264
+ await fsWriteFile(nextIdPath, String(nextId), "utf-8");
257
265
  }
258
- function acquireLock(directory) {
259
- const haimatiDir = getHaimatiDir(directory);
260
- if (!fs.existsSync(haimatiDir)) {
261
- fs.mkdirSync(haimatiDir, { recursive: true });
266
+ async function acquireLock(directory) {
267
+ const haimatiDir = await getHaimatiDir(directory);
268
+ if (!await fsExists(haimatiDir)) {
269
+ await fsMkdir(haimatiDir, { recursive: true });
262
270
  }
263
- const lockPath = getLockPath(directory);
271
+ const lockPath = await getLockPath(directory);
264
272
  const startTime = Date.now();
265
- while (fs.existsSync(lockPath)) {
273
+ while (await fsExists(lockPath)) {
266
274
  if (Date.now() - startTime > LOCK_MAX_WAIT) {
267
275
  return false;
268
276
  }
269
- const sleep = LOCK_RETRY_INTERVAL + Math.random() * 20;
270
- const end = Date.now() + sleep;
271
- while (Date.now() < end) {
272
- }
277
+ await new Promise((resolve) => setTimeout(resolve, 50 + Math.random() * 20));
273
278
  }
274
- fs.writeFileSync(lockPath, String(process.pid), "utf-8");
279
+ await fsWriteFile(lockPath, String(process.pid), "utf-8");
275
280
  return true;
276
281
  }
277
- function releaseLock(directory) {
278
- const lockPath = getLockPath(directory);
279
- if (fs.existsSync(lockPath)) {
280
- fs.unlinkSync(lockPath);
282
+ async function releaseLock(directory) {
283
+ const lockPath = await getLockPath(directory);
284
+ if (await fsExists(lockPath)) {
285
+ await fsUnlink(lockPath);
281
286
  }
282
287
  }
283
- async function readPageAsync(directory, id) {
284
- const pagePath = getPagePath(directory, id);
285
- if (!fs.existsSync(pagePath)) {
288
+ async function readPage(directory, id) {
289
+ const pagePath = await getPagePath(directory, id);
290
+ if (!await fsExists(pagePath)) {
286
291
  return null;
287
292
  }
288
- const content = fs.readFileSync(pagePath, "utf-8");
293
+ const content = await fsReadFile(pagePath, "utf-8");
289
294
  const lines = content.split("\n");
290
295
  if (lines.length < 2) {
291
296
  return null;
@@ -305,25 +310,25 @@ async function readPageAsync(directory, id) {
305
310
  version
306
311
  };
307
312
  }
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 });
313
+ async function writePage(directory, id, title, createdAt, content, version = 1) {
314
+ const pagesDir = await getPagesDir(directory);
315
+ if (!await fsExists(pagesDir)) {
316
+ await fsMkdir(pagesDir, { recursive: true });
312
317
  }
313
- const pageGroupDir = getPageGroupDir(directory, id);
314
- if (!fs.existsSync(pageGroupDir)) {
315
- fs.mkdirSync(pageGroupDir, { recursive: true });
318
+ const pageGroupDir = await getPageGroupDir(directory, id);
319
+ if (!await fsExists(pageGroupDir)) {
320
+ await fsMkdir(pageGroupDir, { recursive: true });
316
321
  }
317
- const pagePath = getPagePath(directory, id);
322
+ const pagePath = await getPagePath(directory, id);
318
323
  const fileContent = `${title}
319
324
  ${createdAt}
320
325
  ${content}${VERSION_SEPARATOR}${version}`;
321
- fs.writeFileSync(pagePath, fileContent, "utf-8");
326
+ await fsWriteFile(pagePath, fileContent, "utf-8");
322
327
  }
323
- async function deletePageAsync(directory, id) {
324
- const pagePath = getPagePath(directory, id);
325
- if (fs.existsSync(pagePath)) {
326
- fs.unlinkSync(pagePath);
328
+ async function deletePage(directory, id) {
329
+ const pagePath = await getPagePath(directory, id);
330
+ if (await fsExists(pagePath)) {
331
+ await fsUnlink(pagePath);
327
332
  return true;
328
333
  }
329
334
  return false;
@@ -386,57 +391,57 @@ function buildIndexTree(entries) {
386
391
  function removeIndexEntry(entries, id) {
387
392
  return entries.filter((e) => e.id !== id);
388
393
  }
389
- async function readIndexAsync(directory) {
390
- const indexPath = getIndexPath(directory);
391
- if (!fs.existsSync(indexPath)) {
394
+ async function readIndex(directory) {
395
+ const indexPath = await getIndexPath(directory);
396
+ if (!await fsExists(indexPath)) {
392
397
  return [];
393
398
  }
394
- const content = fs.readFileSync(indexPath, "utf-8");
399
+ const content = await fsReadFile(indexPath, "utf-8");
395
400
  return parseIndex(content);
396
401
  }
397
- async function writeIndexAsync(directory, entries) {
398
- const haimatiDir = getHaimatiDir(directory);
399
- if (!fs.existsSync(haimatiDir)) {
400
- fs.mkdirSync(haimatiDir, { recursive: true });
402
+ async function writeIndex(directory, entries) {
403
+ const haimatiDir = await getHaimatiDir(directory);
404
+ if (!await fsExists(haimatiDir)) {
405
+ await fsMkdir(haimatiDir, { recursive: true });
401
406
  }
402
407
  const tree = buildIndexTree(entries);
403
- const indexPath = getIndexPath(directory);
404
- fs.writeFileSync(indexPath, tree, "utf-8");
408
+ const indexPath = await getIndexPath(directory);
409
+ await fsWriteFile(indexPath, tree, "utf-8");
405
410
  }
406
411
  async function allocateIdWithLock(directory, category, title, now, content) {
407
- if (!acquireLock(directory)) {
412
+ if (!await acquireLock(directory)) {
408
413
  return null;
409
414
  }
410
415
  try {
411
- await initializeNextIdIfNeededAsync(directory);
412
- const entries = await readIndexAsync(directory);
416
+ await initializeNextIdIfNeeded(directory);
417
+ const entries = await readIndex(directory);
413
418
  const existingEntry = entries.find((e) => e.category === category && e.title === title);
414
419
  if (existingEntry) {
415
- const page = await readPageAsync(directory, existingEntry.id);
420
+ const page = await readPage(directory, existingEntry.id);
416
421
  const createdAt = page ? page.createdAt : now;
417
422
  const newVersion = page ? page.version + 1 : 1;
418
- await writePageAsync(directory, existingEntry.id, title, createdAt, content, newVersion);
423
+ await writePage(directory, existingEntry.id, title, createdAt, content, newVersion);
419
424
  await log("info", `[allocateIdWithLock] \u66F4\u65B0\u5DF2\u6709\u6761\u76EE: #${existingEntry.id} ${category}/${title}, \u65B0\u7248\u672C: ${newVersion}`);
420
425
  return null;
421
426
  }
422
- const currentNext = await readNextIdFromFileAsync(directory);
427
+ const currentNext = await readNextIdFromFile(directory);
423
428
  const newId = String(currentNext).padStart(3, "0");
424
- await writePageAsync(directory, newId, title, now, content, 1);
429
+ await writePage(directory, newId, title, now, content, 1);
425
430
  const newEntry = {
426
431
  id: newId,
427
432
  category,
428
433
  title
429
434
  };
430
435
  entries.push(newEntry);
431
- await writeIndexAsync(directory, entries);
432
- await writeNextIdToFileAsync(directory, currentNext + 1);
436
+ await writeIndex(directory, entries);
437
+ await writeNextIdToFile(directory, currentNext + 1);
433
438
  return newId;
434
439
  } catch (error) {
435
440
  const errMsg = error instanceof Error ? error.message : String(error);
436
441
  await log("error", `[allocateIdWithLock] \u5206\u914D\u5E8F\u53F7\u5931\u8D25: ${errMsg}`);
437
442
  return null;
438
443
  } finally {
439
- releaseLock(directory);
444
+ await releaseLock(directory);
440
445
  }
441
446
  }
442
447
  var HaimatiPlugin = async (ctx) => {
@@ -466,16 +471,16 @@ var HaimatiPlugin = async (ctx) => {
466
471
  */
467
472
  async execute(args, context) {
468
473
  const { directory } = context;
469
- const indexPath = getIndexPath(directory);
474
+ const indexPath = await getIndexPath(directory);
470
475
  await log("info", `[haimati_read] \u5165\u53C2: query="${args.query}", offset=${args.offset || 1}, limit=${args.limit || 2e3}`);
471
- if (!fs.existsSync(indexPath)) {
476
+ if (!await fsExists(indexPath)) {
472
477
  const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728\u4E8E ${indexPath}`;
473
478
  await log("warn", `[haimati_read] \u51FA\u53C2:
474
479
  ${truncateForLog(result)}`);
475
480
  return result;
476
481
  }
477
482
  try {
478
- const entries = await readIndexAsync(directory);
483
+ const entries = await readIndex(directory);
479
484
  await log("debug", `[haimati_read] \u8BFB\u53D6\u7D22\u5F15: \u5171 ${entries.length} \u6761\u8BB0\u5F55`);
480
485
  const query = args.query.trim();
481
486
  if (!/^\d+$/.test(query)) {
@@ -492,14 +497,15 @@ ${truncateForLog(result2)}`);
492
497
  ${truncateForLog(result2)}`);
493
498
  return result2;
494
499
  }
495
- const page = await readPageAsync(directory, entry.id);
500
+ const page = await readPage(directory, entry.id);
496
501
  if (!page) {
497
502
  const result2 = `\u672A\u627E\u5230\u4E66\u9875: #${entry.id}`;
498
503
  await log("error", `[haimati_read] \u51FA\u53C2:
499
504
  ${truncateForLog(result2)}`);
500
505
  return result2;
501
506
  }
502
- await log("debug", `[haimati_read] \u8BFB\u53D6\u4E66\u9875: \u4E66\u9875/${getPageGroupDir(directory, entry.id).split(/[\\/]/).pop()}/${entry.id}.md, version=${page.version}`);
507
+ const pageGroupDir = await getPageGroupDir(directory, entry.id);
508
+ await log("debug", `[haimati_read] \u8BFB\u53D6\u4E66\u9875: \u4E66\u9875/${pageGroupDir.split(/[\\/]/).pop()}/${entry.id}.md, version=${page.version}`);
503
509
  const contentLines = page.content.split("\n");
504
510
  const offset = args.offset || 1;
505
511
  const limit = args.limit || 2e3;
@@ -530,9 +536,9 @@ ${truncateForLog(result)}`);
530
536
  * 会自动分配新的序号并存储到文件
531
537
  */
532
538
  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",
539
+ 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 'xxx\u9879\u76EE/\u7B7E\u7AE0'\uFF09\u3001\u6807\u9898\u548C\u5185\u5BB9\u3002",
534
540
  args: {
535
- category: tool.schema.string().describe("\u5206\u7C7B\u8DEF\u5F84\uFF0C\u5982 '\u6807\u7ACB\u65B9/\u7B7E\u7AE0' \u6216 '\u901A\u7528'"),
541
+ category: tool.schema.string().describe("\u5206\u7C7B\u8DEF\u5F84\uFF0C\u5982 'xxx\u9879\u76EE/\u7B7E\u7AE0' \u6216 '\u901A\u7528'"),
536
542
  title: tool.schema.string().describe("\u8BB0\u5FC6\u6807\u9898\uFF0C\u5982 '\u7B7E\u7AE0\u670D\u52A1WS\u901A\u4FE1\u673A\u5236'"),
537
543
  content: tool.schema.string().describe("\u8981\u5199\u5165\u7684\u8BE6\u7EC6\u5185\u5BB9")
538
544
  },
@@ -550,8 +556,9 @@ ${truncateForLog(args.content)}
550
556
  await log("warn", `[haimati_write] \u9501\u83B7\u53D6\u5931\u8D25\uFF0C\u91CD\u8BD5\u7B2C ${retry + 1} \u6B21`);
551
557
  }
552
558
  if (newId) {
559
+ const pageGroupDir = await getPageGroupDir(directory, newId);
553
560
  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`);
561
+ await log("debug", `[haimati_write] \u4E66\u9875\u548C\u7D22\u5F15\u5DF2\u66F4\u65B0: \u4E66\u9875/${pageGroupDir.split(/[\\/]/).pop()}/${newId}.md`);
555
562
  const result2 = `\u5DF2\u5199\u5165\u6D77\u9A6C\u4F53 #${newId}
556
563
  \u8DEF\u5F84: ${args.category}/${args.title}
557
564
  \u7248\u672C: 1`;
@@ -559,10 +566,10 @@ ${truncateForLog(args.content)}
559
566
  ${truncateForLog(result2)}`);
560
567
  return result2;
561
568
  }
562
- const entries = await readIndexAsync(directory);
569
+ const entries = await readIndex(directory);
563
570
  const existingEntry = findEntryByPath(entries, args.category, args.title);
564
571
  if (existingEntry) {
565
- const page = await readPageAsync(directory, existingEntry.id);
572
+ const page = await readPage(directory, existingEntry.id);
566
573
  const version = page ? page.version : 1;
567
574
  const result2 = `\u5DF2\u66F4\u65B0\u6D77\u9A6C\u4F53 #${existingEntry.id}
568
575
  \u8DEF\u5F84: ${args.category}/${args.title}
@@ -599,21 +606,21 @@ ${truncateForLog(result)}`);
599
606
  },
600
607
  async execute(args, context) {
601
608
  const { directory } = context;
602
- const indexPath = getIndexPath(directory);
609
+ const indexPath = await getIndexPath(directory);
603
610
  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)) {
611
+ if (!await fsExists(indexPath)) {
605
612
  const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
606
613
  await log("warn", `[haimati_search] \u51FA\u53C2:
607
614
  ${truncateForLog(result)}`);
608
615
  return result;
609
616
  }
610
617
  try {
611
- const entries = await readIndexAsync(directory);
618
+ const entries = await readIndex(directory);
612
619
  const keyword = args.keyword.trim();
613
620
  const match = args.match;
614
621
  const limit = args.limit || 10;
615
622
  const offset = args.offset || 0;
616
- const allMatched = await searchEntriesAsync(entries, keyword, match, directory, true);
623
+ const allMatched = await searchEntries(entries, keyword, match, directory, true);
617
624
  const totalCount = allMatched.length;
618
625
  const matched = allMatched.slice(offset, offset + limit);
619
626
  if (matched.length === 0) {
@@ -632,7 +639,7 @@ ${truncateForLog(result2)}`);
632
639
  `);
633
640
  }
634
641
  for (const entry of matched) {
635
- const page = await readPageAsync(directory, entry.id);
642
+ const page = await readPage(directory, entry.id);
636
643
  const fullPath = `${entry.category}/${entry.title}`;
637
644
  let excerpt = "";
638
645
  if (page) {
@@ -683,18 +690,18 @@ ${truncateForLog(result)}`);
683
690
  },
684
691
  async execute(args, context) {
685
692
  const { directory } = context;
686
- const indexPath = getIndexPath(directory);
693
+ const indexPath = await getIndexPath(directory);
687
694
  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
695
  ${truncateForLog(args.content)}
689
696
  `);
690
- if (!fs.existsSync(indexPath)) {
697
+ if (!await fsExists(indexPath)) {
691
698
  const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
692
699
  await log("warn", `[haimati_edit] \u51FA\u53C2:
693
700
  ${truncateForLog(result)}`);
694
701
  return result;
695
702
  }
696
703
  try {
697
- const entries = await readIndexAsync(directory);
704
+ const entries = await readIndex(directory);
698
705
  const query = args.query.trim();
699
706
  if (!/^\d+$/.test(query)) {
700
707
  const result2 = `\u9519\u8BEF\uFF1Aquery \u5FC5\u987B\u662F\u5E8F\u53F7\uFF0C\u53EA\u652F\u6301\u5E8F\u53F7\u67E5\u8BE2`;
@@ -709,7 +716,7 @@ ${truncateForLog(result2)}`);
709
716
  ${truncateForLog(result2)}`);
710
717
  return result2;
711
718
  }
712
- const page = await readPageAsync(directory, entry.id);
719
+ const page = await readPage(directory, entry.id);
713
720
  if (!page) {
714
721
  const result2 = `\u672A\u627E\u5230\u4E66\u9875: #${entry.id}`;
715
722
  await log("error", `[haimati_edit] \u51FA\u53C2:
@@ -733,15 +740,12 @@ ${truncateForLog(result2)}`);
733
740
  const contentLines = page.content.split("\n");
734
741
  const newContentLines = [
735
742
  ...contentLines.slice(0, offsetBegin - 1),
736
- // [0, offsetBegin-1) 保留
737
743
  ...args.content.split("\n"),
738
- // 插入新内容
739
744
  ...contentLines.slice(offsetEnd - 1)
740
- // [offsetEnd-1, end) 保留
741
745
  ];
742
746
  const newContent = newContentLines.join("\n");
743
747
  const newVersion = page.version + 1;
744
- await writePageAsync(directory, entry.id, entry.title, page.createdAt, newContent, newVersion);
748
+ await writePage(directory, entry.id, entry.title, page.createdAt, newContent, newVersion);
745
749
  const fullPath = `${entry.category}/${entry.title}`;
746
750
  const result = `\u5DF2\u66F4\u65B0\u6D77\u9A6C\u4F53 #${entry.id}
747
751
  \u8DEF\u5F84: ${fullPath}
@@ -762,22 +766,22 @@ ${truncateForLog(result)}`);
762
766
  * haimati_delete - 删除海马体中的记忆
763
767
  */
764
768
  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",
769
+ 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 'xxx\u9879\u76EE/\u7B7E\u7AE0/\u7B7E\u7AE0\u670D\u52A1WS\u901A\u4FE1\u673A\u5236'\uFF09\u6765\u5B9A\u4F4D\u8981\u5220\u9664\u7684\u8BB0\u5FC6\u3002",
766
770
  args: {
767
771
  query: tool.schema.string().describe("\u5E8F\u53F7\u6216\u5B8C\u6574\u8DEF\u5F84\uFF0C\u7528\u4E8E\u5B9A\u4F4D\u8981\u5220\u9664\u7684\u8BB0\u5FC6")
768
772
  },
769
773
  async execute(args, context) {
770
774
  const { directory } = context;
771
- const indexPath = getIndexPath(directory);
775
+ const indexPath = await getIndexPath(directory);
772
776
  await log("info", `[haimati_delete] \u5165\u53C2: query="${args.query}"`);
773
- if (!fs.existsSync(indexPath)) {
777
+ if (!await fsExists(indexPath)) {
774
778
  const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
775
779
  await log("warn", `[haimati_delete] \u51FA\u53C2:
776
780
  ${truncateForLog(result)}`);
777
781
  return result;
778
782
  }
779
783
  try {
780
- const entries = await readIndexAsync(directory);
784
+ const entries = await readIndex(directory);
781
785
  const query = args.query.trim();
782
786
  let entry = null;
783
787
  if (/^\d+$/.test(query)) {
@@ -789,7 +793,7 @@ ${truncateForLog(result)}`);
789
793
  const category = parts.join("/");
790
794
  entry = findEntryByPath(entries, category, title);
791
795
  } else {
792
- const results = await searchEntriesAsync(entries, query, "or", directory);
796
+ const results = await searchEntries(entries, query, "or", directory);
793
797
  if (results.length === 1) {
794
798
  entry = results[0];
795
799
  } else if (results.length > 1) {
@@ -805,9 +809,9 @@ ${list}`;
805
809
  ${truncateForLog(result2)}`);
806
810
  return result2;
807
811
  }
808
- await deletePageAsync(directory, entry.id);
812
+ await deletePage(directory, entry.id);
809
813
  const newEntries = removeIndexEntry(entries, entry.id);
810
- await writeIndexAsync(directory, newEntries);
814
+ await writeIndex(directory, newEntries);
811
815
  const fullPath = `${entry.category}/${entry.title}`;
812
816
  const result = `\u5DF2\u5220\u9664\u6D77\u9A6C\u4F53 #${entry.id}
813
817
  \u8DEF\u5F84: ${fullPath}`;
@@ -830,20 +834,20 @@ ${truncateForLog(result)}`);
830
834
  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
835
  args: {
832
836
  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'")
837
+ newCategory: tool.schema.string().describe("\u65B0\u7684\u5206\u7C7B\u8DEF\u5F84\uFF0C\u5982 'xxx\u9879\u76EE/\u5BA2\u6237\u7AEF' \u6216 '\u901A\u7528'")
834
838
  },
835
839
  async execute(args, context) {
836
840
  const { directory } = context;
837
- const indexPath = getIndexPath(directory);
841
+ const indexPath = await getIndexPath(directory);
838
842
  await log("info", `[haimati_move] \u5165\u53C2: query="${args.query}", newCategory="${args.newCategory}"`);
839
- if (!fs.existsSync(indexPath)) {
843
+ if (!await fsExists(indexPath)) {
840
844
  const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
841
845
  await log("warn", `[haimati_move] \u51FA\u53C2:
842
846
  ${truncateForLog(result)}`);
843
847
  return result;
844
848
  }
845
849
  try {
846
- const entries = await readIndexAsync(directory);
850
+ const entries = await readIndex(directory);
847
851
  const query = args.query.trim();
848
852
  let entry = null;
849
853
  if (/^\d+$/.test(query)) {
@@ -855,7 +859,7 @@ ${truncateForLog(result)}`);
855
859
  const category = parts.join("/");
856
860
  entry = findEntryByPath(entries, category, title);
857
861
  } else {
858
- const results = await searchEntriesAsync(entries, query, "or", directory);
862
+ const results = await searchEntries(entries, query, "or", directory);
859
863
  if (results.length === 1) {
860
864
  entry = results[0];
861
865
  } else if (results.length > 1) {
@@ -881,7 +885,7 @@ ${truncateForLog(result2)}`);
881
885
  }
882
886
  return e;
883
887
  });
884
- await writeIndexAsync(directory, newEntries);
888
+ await writeIndex(directory, newEntries);
885
889
  const result = `\u5DF2\u79FB\u52A8\u6D77\u9A6C\u4F53 #${entry.id}
886
890
  \u65E7\u8DEF\u5F84: ${oldPath}
887
891
  \u65B0\u8DEF\u5F84: ${newPath}`;
@@ -908,18 +912,18 @@ ${truncateForLog(result)}`);
908
912
  },
909
913
  async execute(args, context) {
910
914
  const { directory } = context;
911
- const indexPath = getIndexPath(directory);
915
+ const indexPath = await getIndexPath(directory);
912
916
  await log("info", `[haimati_list] \u5165\u53C2: category="${args.category || "(\u65E0)"}, recursive=${args.recursive}"`);
913
- if (!fs.existsSync(indexPath)) {
917
+ if (!await fsExists(indexPath)) {
914
918
  const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
915
919
  await log("warn", `[haimati_list] \u51FA\u53C2:
916
920
  ${truncateForLog(result)}`);
917
921
  return result;
918
922
  }
919
923
  try {
920
- const entries = await readIndexAsync(directory);
924
+ const entries = await readIndex(directory);
921
925
  if (args.category) {
922
- const indexContent = fs.readFileSync(getIndexPath(directory), "utf-8");
926
+ const indexContent = await fsReadFile(await getIndexPath(directory), "utf-8");
923
927
  const lines = indexContent.split("\n");
924
928
  let targetDepth = 0;
925
929
  let currentPath = [];
@@ -1041,6 +1045,10 @@ ${truncateForLog(result)}`);
1041
1045
  await log("warn", "[session.created] \u65E0\u6CD5\u83B7\u53D6 sessionID\uFF0C\u8DF3\u8FC7\u6CE8\u5165");
1042
1046
  return;
1043
1047
  }
1048
+ if (!AUTO_INJECT_CONTEXT) {
1049
+ await log("info", "[session.created] \u81EA\u52A8\u6CE8\u5165\u5DF2\u5173\u95ED\uFF0C\u8DF3\u8FC7\u6CE8\u5165 (AUTO_INJECT_CONTEXT=false)");
1050
+ return;
1051
+ }
1044
1052
  await ctx.client.session.prompt({
1045
1053
  path: { id: sessionId },
1046
1054
  body: {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-haimati",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "OpenCode plugin for a file-based memory system inspired by the hippocampus, providing long-term memory storage and retrieval across sessions.",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "opencode-haimati",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "OpenCode plugin for a file-based memory system inspired by the hippocampus, providing long-term memory storage and retrieval across sessions.",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
7
7
  "license": "MIT",
8
- "files": ["dist"],
8
+ "files": [
9
+ "dist"
10
+ ],
9
11
  "scripts": {
10
12
  "build": "npx esbuild index.ts --bundle --platform=node --outdir=dist --format=esm --external:@opencode-ai/plugin && cp package.json dist/",
11
13
  "prepublishOnly": "npm run build",
@@ -20,4 +22,4 @@
20
22
  "@types/bun": "^1.3.1",
21
23
  "typescript": "^5.9.3"
22
24
  }
23
- }
25
+ }