opencode-haimati 1.0.2 → 1.0.3

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
 
@@ -70,13 +76,35 @@ haimati_write({
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: "标立方/客户端"
98
+ })
99
+
100
+ // 删除记忆
101
+ haimati_delete({ query: "086" })
102
+
103
+ // 列出所有记忆
104
+ haimati_list({})
105
+
106
+ // 列出指定分类下的记忆
107
+ haimati_list({ category: "标立方", recursive: true })
80
108
  ```
81
109
 
82
110
  ## 索引格式示例
@@ -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,30 @@ 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
+ async function getConfigHaimatiPath(directory) {
18
24
  const configPath = path.join(directory, HAIMATI_CONFIG_FILE);
19
- if (!fs.existsSync(configPath)) {
25
+ if (!await fsExists(configPath)) {
20
26
  return null;
21
27
  }
22
28
  try {
23
- const content = fs.readFileSync(configPath, "utf-8");
29
+ const content = await fsReadFile(configPath, "utf-8");
24
30
  const lines = content.split("\n");
25
31
  for (const line of lines) {
26
32
  const trimmed = line.trim();
@@ -41,35 +47,36 @@ function getConfigHaimatiPath(directory) {
41
47
  }
42
48
  return null;
43
49
  }
44
- function getHaimatiDir(directory) {
45
- const configPath = getConfigHaimatiPath(directory);
50
+ async function getHaimatiDir(directory) {
51
+ const configPath = await getConfigHaimatiPath(directory);
46
52
  if (configPath) {
47
53
  return configPath;
48
54
  }
49
55
  return path.join(directory, HAIMATI_DIR);
50
56
  }
51
- function getIndexPath(directory) {
52
- return path.join(getHaimatiDir(directory), INDEX_FILE);
57
+ async function getIndexPath(directory) {
58
+ return path.join(await getHaimatiDir(directory), INDEX_FILE);
53
59
  }
54
- function getNextIdPath(directory) {
55
- return path.join(getHaimatiDir(directory), NEXT_ID_FILE);
60
+ async function getNextIdPath(directory) {
61
+ return path.join(await getHaimatiDir(directory), NEXT_ID_FILE);
56
62
  }
57
- function getLockPath(directory) {
58
- return path.join(getHaimatiDir(directory), ".lock");
63
+ async function getLockPath(directory) {
64
+ return path.join(await getHaimatiDir(directory), ".lock");
59
65
  }
60
- function getPagesDir(directory) {
61
- return path.join(getHaimatiDir(directory), PAGES_DIR);
66
+ async function getPagesDir(directory) {
67
+ return path.join(await getHaimatiDir(directory), PAGES_DIR);
62
68
  }
63
- function getPageGroupDir(directory, id) {
69
+ async function getPageGroupDir(directory, id) {
64
70
  const n = parseInt(id, 10);
65
71
  const groupStart = Math.floor((n - 1) / 50) * 50 + 1;
66
72
  const groupEnd = groupStart + 49;
67
73
  const groupName = `${String(groupStart).padStart(3, "0")}-${String(groupEnd).padStart(3, "0")}`;
68
- return path.join(getPagesDir(directory), groupName);
74
+ return path.join(await getPagesDir(directory), groupName);
69
75
  }
70
- function getPagePath(directory, id) {
71
- return path.join(getPageGroupDir(directory, id), `${id}.md`);
76
+ async function getPagePath(directory, id) {
77
+ return path.join(await getPageGroupDir(directory, id), `${id}.md`);
72
78
  }
79
+ var fsAppendFile = promisify(fs.appendFile);
73
80
  function getLogFilePath() {
74
81
  const osTmpDir = os.tmpdir();
75
82
  return path.join(osTmpDir, "haimati_logs", LOG_FILE);
@@ -81,12 +88,12 @@ async function writeLogToFile(message) {
81
88
  const timeStr = now.toISOString().replace("T", " ").split(".")[0];
82
89
  const logPath = getLogFilePath();
83
90
  const logDir = path.dirname(logPath);
84
- if (!fs.existsSync(logDir)) {
85
- fs.mkdirSync(logDir, { recursive: true });
91
+ if (!await fsExists(logDir)) {
92
+ await fsMkdir(logDir, { recursive: true });
86
93
  }
87
94
  let shouldOverwrite = false;
88
- if (fs.existsSync(logPath)) {
89
- const mtime = fs.statSync(logPath).mtime;
95
+ if (await fsExists(logPath)) {
96
+ const mtime = (await fsStat(logPath)).mtime;
90
97
  const fileDate = mtime.toISOString().split("T")[0];
91
98
  if (fileDate !== dateStr) {
92
99
  shouldOverwrite = true;
@@ -94,9 +101,9 @@ async function writeLogToFile(message) {
94
101
  }
95
102
  const logLine = `[${timeStr}] ${message}`;
96
103
  if (shouldOverwrite) {
97
- fs.writeFileSync(logPath, logLine + "\n", "utf-8");
104
+ await fsWriteFile(logPath, logLine + "\n", "utf-8");
98
105
  } else {
99
- fs.appendFileSync(logPath, logLine + "\n", "utf-8");
106
+ await fsAppendFile(logPath, logLine + "\n", "utf-8");
100
107
  }
101
108
  } catch (error) {
102
109
  console.error(`[haimati] \u5199\u5165\u65E5\u5FD7\u6587\u4EF6\u5931\u8D25:`, error);
@@ -181,7 +188,7 @@ function findEntryById(entries, id) {
181
188
  function findEntryByPath(entries, categoryPath, title) {
182
189
  return entries.find((e) => e.category === categoryPath && e.title === title) || null;
183
190
  }
184
- async function searchEntriesAsync(entries, keyword, matchMode, directory, searchContent = false) {
191
+ async function searchEntries(entries, keyword, matchMode, directory, searchContent = false) {
185
192
  const keywords = keyword.toLowerCase().split(/\s+/).filter((k) => k.length > 0);
186
193
  if (keywords.length === 0) return [];
187
194
  const matchedWithScore = [];
@@ -197,7 +204,7 @@ async function searchEntriesAsync(entries, keyword, matchMode, directory, search
197
204
  }
198
205
  }
199
206
  if (searchContent && directory) {
200
- const page = await readPageAsync(directory, e.id);
207
+ const page = await readPage(directory, e.id);
201
208
  if (page && page.content) {
202
209
  const contentLower = page.content.toLowerCase();
203
210
  for (const kw of keywords) {
@@ -221,12 +228,12 @@ async function searchEntriesAsync(entries, keyword, matchMode, directory, search
221
228
  matchedWithScore.sort((a, b) => b.score - a.score);
222
229
  return matchedWithScore.map((item) => item.entry);
223
230
  }
224
- async function readNextIdFromFileAsync(directory) {
225
- const nextIdPath = getNextIdPath(directory);
226
- if (!fs.existsSync(nextIdPath)) {
231
+ async function readNextIdFromFile(directory) {
232
+ const nextIdPath = await getNextIdPath(directory);
233
+ if (!await fsExists(nextIdPath)) {
227
234
  return 1;
228
235
  }
229
- const content = fs.readFileSync(nextIdPath, "utf-8");
236
+ const content = await fsReadFile(nextIdPath, "utf-8");
230
237
  const trimmed = content.trim();
231
238
  const parsed = parseInt(trimmed, 10);
232
239
  if (!isNaN(parsed) && parsed > 0) {
@@ -234,58 +241,55 @@ async function readNextIdFromFileAsync(directory) {
234
241
  }
235
242
  return 1;
236
243
  }
237
- async function initializeNextIdIfNeededAsync(directory) {
238
- const nextIdPath = getNextIdPath(directory);
239
- if (fs.existsSync(nextIdPath)) {
244
+ async function initializeNextIdIfNeeded(directory) {
245
+ const nextIdPath = await getNextIdPath(directory);
246
+ if (await fsExists(nextIdPath)) {
240
247
  return;
241
248
  }
242
- const entries = await readIndexAsync(directory);
249
+ const entries = await readIndex(directory);
243
250
  let maxId = 0;
244
251
  for (const e of entries) {
245
252
  const n = parseInt(e.id, 10);
246
253
  if (n > maxId) maxId = n;
247
254
  }
248
- await writeNextIdToFileAsync(directory, maxId + 1);
255
+ await writeNextIdToFile(directory, maxId + 1);
249
256
  }
250
- async function writeNextIdToFileAsync(directory, nextId) {
251
- const haimatiDir = getHaimatiDir(directory);
252
- if (!fs.existsSync(haimatiDir)) {
253
- fs.mkdirSync(haimatiDir, { recursive: true });
257
+ async function writeNextIdToFile(directory, nextId) {
258
+ const haimatiDir = await getHaimatiDir(directory);
259
+ if (!await fsExists(haimatiDir)) {
260
+ await fsMkdir(haimatiDir, { recursive: true });
254
261
  }
255
- const nextIdPath = getNextIdPath(directory);
256
- fs.writeFileSync(nextIdPath, String(nextId), "utf-8");
262
+ const nextIdPath = await getNextIdPath(directory);
263
+ await fsWriteFile(nextIdPath, String(nextId), "utf-8");
257
264
  }
258
- function acquireLock(directory) {
259
- const haimatiDir = getHaimatiDir(directory);
260
- if (!fs.existsSync(haimatiDir)) {
261
- fs.mkdirSync(haimatiDir, { recursive: true });
265
+ async function acquireLock(directory) {
266
+ const haimatiDir = await getHaimatiDir(directory);
267
+ if (!await fsExists(haimatiDir)) {
268
+ await fsMkdir(haimatiDir, { recursive: true });
262
269
  }
263
- const lockPath = getLockPath(directory);
270
+ const lockPath = await getLockPath(directory);
264
271
  const startTime = Date.now();
265
- while (fs.existsSync(lockPath)) {
272
+ while (await fsExists(lockPath)) {
266
273
  if (Date.now() - startTime > LOCK_MAX_WAIT) {
267
274
  return false;
268
275
  }
269
- const sleep = LOCK_RETRY_INTERVAL + Math.random() * 20;
270
- const end = Date.now() + sleep;
271
- while (Date.now() < end) {
272
- }
276
+ await new Promise((resolve) => setTimeout(resolve, 50 + Math.random() * 20));
273
277
  }
274
- fs.writeFileSync(lockPath, String(process.pid), "utf-8");
278
+ await fsWriteFile(lockPath, String(process.pid), "utf-8");
275
279
  return true;
276
280
  }
277
- function releaseLock(directory) {
278
- const lockPath = getLockPath(directory);
279
- if (fs.existsSync(lockPath)) {
280
- fs.unlinkSync(lockPath);
281
+ async function releaseLock(directory) {
282
+ const lockPath = await getLockPath(directory);
283
+ if (await fsExists(lockPath)) {
284
+ await fsUnlink(lockPath);
281
285
  }
282
286
  }
283
- async function readPageAsync(directory, id) {
284
- const pagePath = getPagePath(directory, id);
285
- if (!fs.existsSync(pagePath)) {
287
+ async function readPage(directory, id) {
288
+ const pagePath = await getPagePath(directory, id);
289
+ if (!await fsExists(pagePath)) {
286
290
  return null;
287
291
  }
288
- const content = fs.readFileSync(pagePath, "utf-8");
292
+ const content = await fsReadFile(pagePath, "utf-8");
289
293
  const lines = content.split("\n");
290
294
  if (lines.length < 2) {
291
295
  return null;
@@ -305,25 +309,25 @@ async function readPageAsync(directory, id) {
305
309
  version
306
310
  };
307
311
  }
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
+ async function writePage(directory, id, title, createdAt, content, version = 1) {
313
+ const pagesDir = await getPagesDir(directory);
314
+ if (!await fsExists(pagesDir)) {
315
+ await fsMkdir(pagesDir, { recursive: true });
312
316
  }
313
- const pageGroupDir = getPageGroupDir(directory, id);
314
- if (!fs.existsSync(pageGroupDir)) {
315
- fs.mkdirSync(pageGroupDir, { recursive: true });
317
+ const pageGroupDir = await getPageGroupDir(directory, id);
318
+ if (!await fsExists(pageGroupDir)) {
319
+ await fsMkdir(pageGroupDir, { recursive: true });
316
320
  }
317
- const pagePath = getPagePath(directory, id);
321
+ const pagePath = await getPagePath(directory, id);
318
322
  const fileContent = `${title}
319
323
  ${createdAt}
320
324
  ${content}${VERSION_SEPARATOR}${version}`;
321
- fs.writeFileSync(pagePath, fileContent, "utf-8");
325
+ await fsWriteFile(pagePath, fileContent, "utf-8");
322
326
  }
323
- async function deletePageAsync(directory, id) {
324
- const pagePath = getPagePath(directory, id);
325
- if (fs.existsSync(pagePath)) {
326
- fs.unlinkSync(pagePath);
327
+ async function deletePage(directory, id) {
328
+ const pagePath = await getPagePath(directory, id);
329
+ if (await fsExists(pagePath)) {
330
+ await fsUnlink(pagePath);
327
331
  return true;
328
332
  }
329
333
  return false;
@@ -386,57 +390,57 @@ function buildIndexTree(entries) {
386
390
  function removeIndexEntry(entries, id) {
387
391
  return entries.filter((e) => e.id !== id);
388
392
  }
389
- async function readIndexAsync(directory) {
390
- const indexPath = getIndexPath(directory);
391
- if (!fs.existsSync(indexPath)) {
393
+ async function readIndex(directory) {
394
+ const indexPath = await getIndexPath(directory);
395
+ if (!await fsExists(indexPath)) {
392
396
  return [];
393
397
  }
394
- const content = fs.readFileSync(indexPath, "utf-8");
398
+ const content = await fsReadFile(indexPath, "utf-8");
395
399
  return parseIndex(content);
396
400
  }
397
- async function writeIndexAsync(directory, entries) {
398
- const haimatiDir = getHaimatiDir(directory);
399
- if (!fs.existsSync(haimatiDir)) {
400
- fs.mkdirSync(haimatiDir, { recursive: true });
401
+ async function writeIndex(directory, entries) {
402
+ const haimatiDir = await getHaimatiDir(directory);
403
+ if (!await fsExists(haimatiDir)) {
404
+ await fsMkdir(haimatiDir, { recursive: true });
401
405
  }
402
406
  const tree = buildIndexTree(entries);
403
- const indexPath = getIndexPath(directory);
404
- fs.writeFileSync(indexPath, tree, "utf-8");
407
+ const indexPath = await getIndexPath(directory);
408
+ await fsWriteFile(indexPath, tree, "utf-8");
405
409
  }
406
410
  async function allocateIdWithLock(directory, category, title, now, content) {
407
- if (!acquireLock(directory)) {
411
+ if (!await acquireLock(directory)) {
408
412
  return null;
409
413
  }
410
414
  try {
411
- await initializeNextIdIfNeededAsync(directory);
412
- const entries = await readIndexAsync(directory);
415
+ await initializeNextIdIfNeeded(directory);
416
+ const entries = await readIndex(directory);
413
417
  const existingEntry = entries.find((e) => e.category === category && e.title === title);
414
418
  if (existingEntry) {
415
- const page = await readPageAsync(directory, existingEntry.id);
419
+ const page = await readPage(directory, existingEntry.id);
416
420
  const createdAt = page ? page.createdAt : now;
417
421
  const newVersion = page ? page.version + 1 : 1;
418
- await writePageAsync(directory, existingEntry.id, title, createdAt, content, newVersion);
422
+ await writePage(directory, existingEntry.id, title, createdAt, content, newVersion);
419
423
  await log("info", `[allocateIdWithLock] \u66F4\u65B0\u5DF2\u6709\u6761\u76EE: #${existingEntry.id} ${category}/${title}, \u65B0\u7248\u672C: ${newVersion}`);
420
424
  return null;
421
425
  }
422
- const currentNext = await readNextIdFromFileAsync(directory);
426
+ const currentNext = await readNextIdFromFile(directory);
423
427
  const newId = String(currentNext).padStart(3, "0");
424
- await writePageAsync(directory, newId, title, now, content, 1);
428
+ await writePage(directory, newId, title, now, content, 1);
425
429
  const newEntry = {
426
430
  id: newId,
427
431
  category,
428
432
  title
429
433
  };
430
434
  entries.push(newEntry);
431
- await writeIndexAsync(directory, entries);
432
- await writeNextIdToFileAsync(directory, currentNext + 1);
435
+ await writeIndex(directory, entries);
436
+ await writeNextIdToFile(directory, currentNext + 1);
433
437
  return newId;
434
438
  } catch (error) {
435
439
  const errMsg = error instanceof Error ? error.message : String(error);
436
440
  await log("error", `[allocateIdWithLock] \u5206\u914D\u5E8F\u53F7\u5931\u8D25: ${errMsg}`);
437
441
  return null;
438
442
  } finally {
439
- releaseLock(directory);
443
+ await releaseLock(directory);
440
444
  }
441
445
  }
442
446
  var HaimatiPlugin = async (ctx) => {
@@ -466,16 +470,16 @@ var HaimatiPlugin = async (ctx) => {
466
470
  */
467
471
  async execute(args, context) {
468
472
  const { directory } = context;
469
- const indexPath = getIndexPath(directory);
473
+ const indexPath = await getIndexPath(directory);
470
474
  await log("info", `[haimati_read] \u5165\u53C2: query="${args.query}", offset=${args.offset || 1}, limit=${args.limit || 2e3}`);
471
- if (!fs.existsSync(indexPath)) {
475
+ if (!await fsExists(indexPath)) {
472
476
  const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728\u4E8E ${indexPath}`;
473
477
  await log("warn", `[haimati_read] \u51FA\u53C2:
474
478
  ${truncateForLog(result)}`);
475
479
  return result;
476
480
  }
477
481
  try {
478
- const entries = await readIndexAsync(directory);
482
+ const entries = await readIndex(directory);
479
483
  await log("debug", `[haimati_read] \u8BFB\u53D6\u7D22\u5F15: \u5171 ${entries.length} \u6761\u8BB0\u5F55`);
480
484
  const query = args.query.trim();
481
485
  if (!/^\d+$/.test(query)) {
@@ -492,14 +496,15 @@ ${truncateForLog(result2)}`);
492
496
  ${truncateForLog(result2)}`);
493
497
  return result2;
494
498
  }
495
- const page = await readPageAsync(directory, entry.id);
499
+ const page = await readPage(directory, entry.id);
496
500
  if (!page) {
497
501
  const result2 = `\u672A\u627E\u5230\u4E66\u9875: #${entry.id}`;
498
502
  await log("error", `[haimati_read] \u51FA\u53C2:
499
503
  ${truncateForLog(result2)}`);
500
504
  return result2;
501
505
  }
502
- await log("debug", `[haimati_read] \u8BFB\u53D6\u4E66\u9875: \u4E66\u9875/${getPageGroupDir(directory, entry.id).split(/[\\/]/).pop()}/${entry.id}.md, version=${page.version}`);
506
+ const pageGroupDir = await getPageGroupDir(directory, entry.id);
507
+ await log("debug", `[haimati_read] \u8BFB\u53D6\u4E66\u9875: \u4E66\u9875/${pageGroupDir.split(/[\\/]/).pop()}/${entry.id}.md, version=${page.version}`);
503
508
  const contentLines = page.content.split("\n");
504
509
  const offset = args.offset || 1;
505
510
  const limit = args.limit || 2e3;
@@ -550,8 +555,9 @@ ${truncateForLog(args.content)}
550
555
  await log("warn", `[haimati_write] \u9501\u83B7\u53D6\u5931\u8D25\uFF0C\u91CD\u8BD5\u7B2C ${retry + 1} \u6B21`);
551
556
  }
552
557
  if (newId) {
558
+ const pageGroupDir = await getPageGroupDir(directory, newId);
553
559
  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`);
560
+ await log("debug", `[haimati_write] \u4E66\u9875\u548C\u7D22\u5F15\u5DF2\u66F4\u65B0: \u4E66\u9875/${pageGroupDir.split(/[\\/]/).pop()}/${newId}.md`);
555
561
  const result2 = `\u5DF2\u5199\u5165\u6D77\u9A6C\u4F53 #${newId}
556
562
  \u8DEF\u5F84: ${args.category}/${args.title}
557
563
  \u7248\u672C: 1`;
@@ -559,10 +565,10 @@ ${truncateForLog(args.content)}
559
565
  ${truncateForLog(result2)}`);
560
566
  return result2;
561
567
  }
562
- const entries = await readIndexAsync(directory);
568
+ const entries = await readIndex(directory);
563
569
  const existingEntry = findEntryByPath(entries, args.category, args.title);
564
570
  if (existingEntry) {
565
- const page = await readPageAsync(directory, existingEntry.id);
571
+ const page = await readPage(directory, existingEntry.id);
566
572
  const version = page ? page.version : 1;
567
573
  const result2 = `\u5DF2\u66F4\u65B0\u6D77\u9A6C\u4F53 #${existingEntry.id}
568
574
  \u8DEF\u5F84: ${args.category}/${args.title}
@@ -599,21 +605,21 @@ ${truncateForLog(result)}`);
599
605
  },
600
606
  async execute(args, context) {
601
607
  const { directory } = context;
602
- const indexPath = getIndexPath(directory);
608
+ const indexPath = await getIndexPath(directory);
603
609
  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)) {
610
+ if (!await fsExists(indexPath)) {
605
611
  const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
606
612
  await log("warn", `[haimati_search] \u51FA\u53C2:
607
613
  ${truncateForLog(result)}`);
608
614
  return result;
609
615
  }
610
616
  try {
611
- const entries = await readIndexAsync(directory);
617
+ const entries = await readIndex(directory);
612
618
  const keyword = args.keyword.trim();
613
619
  const match = args.match;
614
620
  const limit = args.limit || 10;
615
621
  const offset = args.offset || 0;
616
- const allMatched = await searchEntriesAsync(entries, keyword, match, directory, true);
622
+ const allMatched = await searchEntries(entries, keyword, match, directory, true);
617
623
  const totalCount = allMatched.length;
618
624
  const matched = allMatched.slice(offset, offset + limit);
619
625
  if (matched.length === 0) {
@@ -632,7 +638,7 @@ ${truncateForLog(result2)}`);
632
638
  `);
633
639
  }
634
640
  for (const entry of matched) {
635
- const page = await readPageAsync(directory, entry.id);
641
+ const page = await readPage(directory, entry.id);
636
642
  const fullPath = `${entry.category}/${entry.title}`;
637
643
  let excerpt = "";
638
644
  if (page) {
@@ -683,18 +689,18 @@ ${truncateForLog(result)}`);
683
689
  },
684
690
  async execute(args, context) {
685
691
  const { directory } = context;
686
- const indexPath = getIndexPath(directory);
692
+ const indexPath = await getIndexPath(directory);
687
693
  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
694
  ${truncateForLog(args.content)}
689
695
  `);
690
- if (!fs.existsSync(indexPath)) {
696
+ if (!await fsExists(indexPath)) {
691
697
  const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
692
698
  await log("warn", `[haimati_edit] \u51FA\u53C2:
693
699
  ${truncateForLog(result)}`);
694
700
  return result;
695
701
  }
696
702
  try {
697
- const entries = await readIndexAsync(directory);
703
+ const entries = await readIndex(directory);
698
704
  const query = args.query.trim();
699
705
  if (!/^\d+$/.test(query)) {
700
706
  const result2 = `\u9519\u8BEF\uFF1Aquery \u5FC5\u987B\u662F\u5E8F\u53F7\uFF0C\u53EA\u652F\u6301\u5E8F\u53F7\u67E5\u8BE2`;
@@ -709,7 +715,7 @@ ${truncateForLog(result2)}`);
709
715
  ${truncateForLog(result2)}`);
710
716
  return result2;
711
717
  }
712
- const page = await readPageAsync(directory, entry.id);
718
+ const page = await readPage(directory, entry.id);
713
719
  if (!page) {
714
720
  const result2 = `\u672A\u627E\u5230\u4E66\u9875: #${entry.id}`;
715
721
  await log("error", `[haimati_edit] \u51FA\u53C2:
@@ -733,15 +739,12 @@ ${truncateForLog(result2)}`);
733
739
  const contentLines = page.content.split("\n");
734
740
  const newContentLines = [
735
741
  ...contentLines.slice(0, offsetBegin - 1),
736
- // [0, offsetBegin-1) 保留
737
742
  ...args.content.split("\n"),
738
- // 插入新内容
739
743
  ...contentLines.slice(offsetEnd - 1)
740
- // [offsetEnd-1, end) 保留
741
744
  ];
742
745
  const newContent = newContentLines.join("\n");
743
746
  const newVersion = page.version + 1;
744
- await writePageAsync(directory, entry.id, entry.title, page.createdAt, newContent, newVersion);
747
+ await writePage(directory, entry.id, entry.title, page.createdAt, newContent, newVersion);
745
748
  const fullPath = `${entry.category}/${entry.title}`;
746
749
  const result = `\u5DF2\u66F4\u65B0\u6D77\u9A6C\u4F53 #${entry.id}
747
750
  \u8DEF\u5F84: ${fullPath}
@@ -768,16 +771,16 @@ ${truncateForLog(result)}`);
768
771
  },
769
772
  async execute(args, context) {
770
773
  const { directory } = context;
771
- const indexPath = getIndexPath(directory);
774
+ const indexPath = await getIndexPath(directory);
772
775
  await log("info", `[haimati_delete] \u5165\u53C2: query="${args.query}"`);
773
- if (!fs.existsSync(indexPath)) {
776
+ if (!await fsExists(indexPath)) {
774
777
  const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
775
778
  await log("warn", `[haimati_delete] \u51FA\u53C2:
776
779
  ${truncateForLog(result)}`);
777
780
  return result;
778
781
  }
779
782
  try {
780
- const entries = await readIndexAsync(directory);
783
+ const entries = await readIndex(directory);
781
784
  const query = args.query.trim();
782
785
  let entry = null;
783
786
  if (/^\d+$/.test(query)) {
@@ -789,7 +792,7 @@ ${truncateForLog(result)}`);
789
792
  const category = parts.join("/");
790
793
  entry = findEntryByPath(entries, category, title);
791
794
  } else {
792
- const results = await searchEntriesAsync(entries, query, "or", directory);
795
+ const results = await searchEntries(entries, query, "or", directory);
793
796
  if (results.length === 1) {
794
797
  entry = results[0];
795
798
  } else if (results.length > 1) {
@@ -805,9 +808,9 @@ ${list}`;
805
808
  ${truncateForLog(result2)}`);
806
809
  return result2;
807
810
  }
808
- await deletePageAsync(directory, entry.id);
811
+ await deletePage(directory, entry.id);
809
812
  const newEntries = removeIndexEntry(entries, entry.id);
810
- await writeIndexAsync(directory, newEntries);
813
+ await writeIndex(directory, newEntries);
811
814
  const fullPath = `${entry.category}/${entry.title}`;
812
815
  const result = `\u5DF2\u5220\u9664\u6D77\u9A6C\u4F53 #${entry.id}
813
816
  \u8DEF\u5F84: ${fullPath}`;
@@ -834,16 +837,16 @@ ${truncateForLog(result)}`);
834
837
  },
835
838
  async execute(args, context) {
836
839
  const { directory } = context;
837
- const indexPath = getIndexPath(directory);
840
+ const indexPath = await getIndexPath(directory);
838
841
  await log("info", `[haimati_move] \u5165\u53C2: query="${args.query}", newCategory="${args.newCategory}"`);
839
- if (!fs.existsSync(indexPath)) {
842
+ if (!await fsExists(indexPath)) {
840
843
  const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
841
844
  await log("warn", `[haimati_move] \u51FA\u53C2:
842
845
  ${truncateForLog(result)}`);
843
846
  return result;
844
847
  }
845
848
  try {
846
- const entries = await readIndexAsync(directory);
849
+ const entries = await readIndex(directory);
847
850
  const query = args.query.trim();
848
851
  let entry = null;
849
852
  if (/^\d+$/.test(query)) {
@@ -855,7 +858,7 @@ ${truncateForLog(result)}`);
855
858
  const category = parts.join("/");
856
859
  entry = findEntryByPath(entries, category, title);
857
860
  } else {
858
- const results = await searchEntriesAsync(entries, query, "or", directory);
861
+ const results = await searchEntries(entries, query, "or", directory);
859
862
  if (results.length === 1) {
860
863
  entry = results[0];
861
864
  } else if (results.length > 1) {
@@ -881,7 +884,7 @@ ${truncateForLog(result2)}`);
881
884
  }
882
885
  return e;
883
886
  });
884
- await writeIndexAsync(directory, newEntries);
887
+ await writeIndex(directory, newEntries);
885
888
  const result = `\u5DF2\u79FB\u52A8\u6D77\u9A6C\u4F53 #${entry.id}
886
889
  \u65E7\u8DEF\u5F84: ${oldPath}
887
890
  \u65B0\u8DEF\u5F84: ${newPath}`;
@@ -908,18 +911,18 @@ ${truncateForLog(result)}`);
908
911
  },
909
912
  async execute(args, context) {
910
913
  const { directory } = context;
911
- const indexPath = getIndexPath(directory);
914
+ const indexPath = await getIndexPath(directory);
912
915
  await log("info", `[haimati_list] \u5165\u53C2: category="${args.category || "(\u65E0)"}, recursive=${args.recursive}"`);
913
- if (!fs.existsSync(indexPath)) {
916
+ if (!await fsExists(indexPath)) {
914
917
  const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
915
918
  await log("warn", `[haimati_list] \u51FA\u53C2:
916
919
  ${truncateForLog(result)}`);
917
920
  return result;
918
921
  }
919
922
  try {
920
- const entries = await readIndexAsync(directory);
923
+ const entries = await readIndex(directory);
921
924
  if (args.category) {
922
- const indexContent = fs.readFileSync(getIndexPath(directory), "utf-8");
925
+ const indexContent = await fsReadFile(await getIndexPath(directory), "utf-8");
923
926
  const lines = indexContent.split("\n");
924
927
  let targetDepth = 0;
925
928
  let currentPath = [];
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,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",