@jacklika/dsh-tool-memory-filesystem 0.1.3-alpha.7 → 0.1.3-alpha.9
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 +5 -5
- package/README.zh.md +5 -5
- package/lib/index.js +56 -18
- package/lib/types/index.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,8 +53,8 @@ A relative `vaultRoot` resolves against the calling session's workspace.
|
|
|
53
53
|
### Tools
|
|
54
54
|
|
|
55
55
|
- `wiki_read(id)` — read one note by vault-relative path and return its frontmatter, body, links, linked notes, a `version` content fingerprint, the file's `mtime` as a `+08:00` timestamp, and a `modifiedExternally` flag that is `true` when the file changed on disk since this plugin last observed it (e.g. an Obsidian edit). The plugin keeps a bounded 256-entry LRU of observed mtimes keyed by absolute path; its own `wiki_write` re-records the mtime so plugin writes never flag.
|
|
56
|
-
- `wiki_search(query)` — keyword search across note titles, ids, and bodies; query terms are OR-matched and ranked by field-weighted BM25-style scoring (title/id hits weigh most, rare terms weigh more) plus a verbatim-phrase bonus and a link-graph boost; when `wiki_semantic_search` is mounted the two rankings fuse via reciprocal rank fusion.
|
|
57
|
-
- `wiki_write(id, content, mode?, baseVersion?)` — create or append to a note. Append mode preserves frontmatter and adds a timestamp header; both write modes normalize `created`/`updated` frontmatter values to `+08:00` second precision (unparseable values pass through). Passing a `version` from `wiki_read` as `baseVersion` makes the write fail loudly when another writer changed the note in between.
|
|
56
|
+
- `wiki_search(query)` — keyword search across note titles, ids, and bodies; query terms are OR-matched and ranked by field-weighted BM25-style scoring (title/id hits weigh most, rare terms weigh more) plus a verbatim-phrase bonus and a link-graph boost; when `wiki_semantic_search` is mounted the two rankings fuse via reciprocal rank fusion. Returns a `{ hits, total, truncated }` page: each hit carries the note id, title, score, and backlinks; `total` counts every match *before* the `maxSearchResults` cap; `truncated` is `true` when that cap cut the list, so a caller can tell a complete result set from a partial one instead of assuming the vault holds nothing else.
|
|
57
|
+
- `wiki_write(id, content, mode?, baseVersion?)` — create or append to a note. Append mode preserves frontmatter and adds a timestamp header; both write modes normalize `created`/`updated` frontmatter values to `+08:00` second precision (unparseable values pass through). Passing a `version` from `wiki_read` as `baseVersion` makes the write fail loudly when another writer changed the note in between. The result is `{ id, mode, bytes, version }`, where `version` fingerprints the bytes just published — feed it straight back as `baseVersion` on the next write to chain optimistic-concurrency checks without a `wiki_read` round-trip.
|
|
58
58
|
|
|
59
59
|
### Security
|
|
60
60
|
|
|
@@ -87,18 +87,18 @@ Read one Markdown note from the wiki vault, optionally following Obsidian-style
|
|
|
87
87
|
##### Verbatim description for `wiki_search`
|
|
88
88
|
|
|
89
89
|
```markdown
|
|
90
|
-
Search the wiki vault by note title or body keyword. Terms are OR-matched; results are ranked by field-weighted relevance (title/id hits outrank body hits, rare terms weigh more, notes linked from strong hits get a boost, and results may be fused with semantic search when available). Returns
|
|
90
|
+
Search the wiki vault by note title or body keyword. Terms are OR-matched; results are ranked by field-weighted relevance (title/id hits outrank body hits, rare terms weigh more, notes linked from strong hits get a boost, and results may be fused with semantic search when available). Returns { hits, total, truncated }: each hit carries the note id, title, score, and backlinks, total counts every match before the result cap, and truncated is true when that cap cut the list. Use this before asking the user which note to read.
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
##### Verbatim description for `wiki_write`
|
|
94
94
|
|
|
95
95
|
```markdown
|
|
96
|
-
Create a new note or append to an existing note in the wiki vault. The path is relative to the vault root. When appending, the new content is inserted at the end of the body after a timestamp header.
|
|
96
|
+
Create a new note or append to an existing note in the wiki vault. The path is relative to the vault root. When appending, the new content is inserted at the end of the body after a timestamp header. Returns { id, mode, bytes, version }, where version fingerprints the written content and can be passed as baseVersion on the next write without reading the note again.
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
#### Token effect
|
|
100
100
|
|
|
101
|
-
`wiki_read` returns the full body of the requested note plus every linked note reachable within `maxLinkDepth`. Long notes or dense link graphs can add many tokens to the next request. `wiki_search` returns a bounded list of result metadata only.
|
|
101
|
+
`wiki_read` returns the full body of the requested note plus every linked note reachable within `maxLinkDepth`. Long notes or dense link graphs can add many tokens to the next request. `wiki_search` returns a bounded list of result metadata only; its `total` reports matches beyond the cap without returning them.
|
|
102
102
|
|
|
103
103
|
#### KV Cache effect
|
|
104
104
|
|
package/README.zh.md
CHANGED
|
@@ -53,8 +53,8 @@ kind: "package-reference"
|
|
|
53
53
|
### 工具
|
|
54
54
|
|
|
55
55
|
- `wiki_read(id)` — 按仓库相对路径读取笔记,返回 frontmatter、正文、链接、已解析的链接笔记、内容指纹 `version`、`+08:00` 格式的文件 `mtime`,以及 `modifiedExternally` 标志(文件自本插件上次观察以来在磁盘上被改动时为 `true`,例如 Obsidian 编辑)。插件按绝对路径维护一个 256 条上限的 LRU mtime 表;自身的 `wiki_write` 会刷新记录,因此插件写入永不误报。
|
|
56
|
-
- `wiki_search(query)` — 按标题、id、正文关键词搜索;查询词采用 OR 匹配,并按字段加权的 BM25 式评分排序(标题/id 命中权重最高,稀有词权重更高),另有整段短语加分与被强命中笔记链接的图增强;挂载 `wiki_semantic_search` 时两路排序经 RRF
|
|
57
|
-
- `wiki_write(id, content, mode?, baseVersion?)` — 创建或追加笔记。追加模式保留 frontmatter 并添加时间戳标题;两种写入模式都会把 `created`/`updated` frontmatter 值归一化为 `+08:00` 秒级精度(不可解析的值原样保留)。把 `wiki_read` 返回的 `version` 作为 `baseVersion`
|
|
56
|
+
- `wiki_search(query)` — 按标题、id、正文关键词搜索;查询词采用 OR 匹配,并按字段加权的 BM25 式评分排序(标题/id 命中权重最高,稀有词权重更高),另有整段短语加分与被强命中笔记链接的图增强;挂载 `wiki_semantic_search` 时两路排序经 RRF 融合。返回 `{ hits, total, truncated }` 分页结构:每条命中含笔记 id、标题、分数与反向链接;`total` 是 `maxSearchResults` 截断**之前**的全部命中数;`truncated` 为 `true` 表示结果被该上限截断,调用方因此能区分“结果完整”与“还有更多”,而不会误以为仓库里只有这些。
|
|
57
|
+
- `wiki_write(id, content, mode?, baseVersion?)` — 创建或追加笔记。追加模式保留 frontmatter 并添加时间戳标题;两种写入模式都会把 `created`/`updated` frontmatter 值归一化为 `+08:00` 秒级精度(不可解析的值原样保留)。把 `wiki_read` 返回的 `version` 作为 `baseVersion` 传入时,若笔记在读取后被其他写入方改动,写入会显式失败。返回值为 `{ id, mode, bytes, version }`,其中 `version` 就是刚发布内容的指纹——可直接作为下一次写入的 `baseVersion`,无需再 `wiki_read` 一次即可串联乐观并发校验。
|
|
58
58
|
|
|
59
59
|
### 安全
|
|
60
60
|
|
|
@@ -87,18 +87,18 @@ Read one Markdown note from the wiki vault, optionally following Obsidian-style
|
|
|
87
87
|
##### `wiki_search` 的完整描述
|
|
88
88
|
|
|
89
89
|
```markdown
|
|
90
|
-
Search the wiki vault by note title or body keyword. Terms are OR-matched; results are ranked by field-weighted relevance (title/id hits outrank body hits, rare terms weigh more, notes linked from strong hits get a boost, and results may be fused with semantic search when available). Returns
|
|
90
|
+
Search the wiki vault by note title or body keyword. Terms are OR-matched; results are ranked by field-weighted relevance (title/id hits outrank body hits, rare terms weigh more, notes linked from strong hits get a boost, and results may be fused with semantic search when available). Returns { hits, total, truncated }: each hit carries the note id, title, score, and backlinks, total counts every match before the result cap, and truncated is true when that cap cut the list. Use this before asking the user which note to read.
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
##### `wiki_write` 的完整描述
|
|
94
94
|
|
|
95
95
|
```markdown
|
|
96
|
-
Create a new note or append to an existing note in the wiki vault. The path is relative to the vault root. When appending, the new content is inserted at the end of the body after a timestamp header.
|
|
96
|
+
Create a new note or append to an existing note in the wiki vault. The path is relative to the vault root. When appending, the new content is inserted at the end of the body after a timestamp header. Returns { id, mode, bytes, version }, where version fingerprints the written content and can be passed as baseVersion on the next write without reading the note again.
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
#### Token 影响
|
|
100
100
|
|
|
101
|
-
`wiki_read` 返回请求笔记的完整正文,以及 `maxLinkDepth` 范围内所有可达链接笔记的正文。长笔记或密集链接图可能显著增加下次请求的 token 数。`wiki_search`
|
|
101
|
+
`wiki_read` 返回请求笔记的完整正文,以及 `maxLinkDepth` 范围内所有可达链接笔记的正文。长笔记或密集链接图可能显著增加下次请求的 token 数。`wiki_search` 只返回有限的结果元数据;其 `total` 会告知被上限截断而未返回的命中数量。
|
|
102
102
|
|
|
103
103
|
#### KV Cache 影响
|
|
104
104
|
|
package/lib/index.js
CHANGED
|
@@ -15,6 +15,12 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
|
15
15
|
import { formatBeijingTime } from '@jacklika/dsh-memory-time';
|
|
16
16
|
import yaml from 'js-yaml';
|
|
17
17
|
import { codeUnitCompare, fuseRankings, rankNotes } from "./search.js";
|
|
18
|
+
/**
|
|
19
|
+
* Locale-independent note-id ordering, re-exported so sibling packages that
|
|
20
|
+
* truncate or tie-break id lists sort by the same rule instead of a copy that
|
|
21
|
+
* can drift.
|
|
22
|
+
*/
|
|
23
|
+
export { codeUnitCompare };
|
|
18
24
|
/**
|
|
19
25
|
* Compute a vault-relative note id from an absolute path. The result always
|
|
20
26
|
* uses POSIX separators so note ids are stable across Windows, macOS, and Linux.
|
|
@@ -480,10 +486,13 @@ export async function buildIndex(root, extensions, indexHiddenDirs = false) {
|
|
|
480
486
|
}
|
|
481
487
|
const index = new Map();
|
|
482
488
|
for (const note of notes) {
|
|
489
|
+
const explicitTitle = typeof note.frontmatter.title === 'string' ? note.frontmatter.title.trim() : '';
|
|
483
490
|
const headingMatch = /^#\s+(.+)$/m.exec(note.body);
|
|
484
|
-
const title =
|
|
485
|
-
?
|
|
486
|
-
:
|
|
491
|
+
const title = explicitTitle !== ''
|
|
492
|
+
? explicitTitle
|
|
493
|
+
: (headingMatch !== null && headingMatch[1] !== undefined
|
|
494
|
+
? headingMatch[1].trim()
|
|
495
|
+
: note.id.replace(/\.[^.]+$/, ''));
|
|
487
496
|
index.set(note.id, { id: note.id, title, backlinks: [], body: note.body });
|
|
488
497
|
}
|
|
489
498
|
const linkResolver = createLinkResolver([...index.keys()]);
|
|
@@ -570,7 +579,7 @@ export function apply(ctx, config) {
|
|
|
570
579
|
})));
|
|
571
580
|
ctx.effect(() => ctx.tools.register(defineTool({
|
|
572
581
|
name: 'wiki_search',
|
|
573
|
-
description: 'Search the wiki vault by note title or body keyword. Terms are OR-matched; results are ranked by field-weighted relevance (title/id hits outrank body hits, rare terms weigh more, notes linked from strong hits get a boost, and results may be fused with semantic search when available). Returns
|
|
582
|
+
description: 'Search the wiki vault by note title or body keyword. Terms are OR-matched; results are ranked by field-weighted relevance (title/id hits outrank body hits, rare terms weigh more, notes linked from strong hits get a boost, and results may be fused with semantic search when available). Returns { hits, total, truncated }: each hit carries the note id, title, score, and backlinks, total counts every match before the result cap, and truncated is true when that cap cut the list. Use this before asking the user which note to read.',
|
|
574
583
|
parameters: {
|
|
575
584
|
query: {
|
|
576
585
|
type: 'string',
|
|
@@ -580,16 +589,24 @@ export function apply(ctx, config) {
|
|
|
580
589
|
},
|
|
581
590
|
output: {
|
|
582
591
|
schema: {
|
|
583
|
-
type: '
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
+
type: 'object',
|
|
593
|
+
additionalProperties: false,
|
|
594
|
+
properties: {
|
|
595
|
+
hits: {
|
|
596
|
+
type: 'array',
|
|
597
|
+
items: {
|
|
598
|
+
type: 'object',
|
|
599
|
+
additionalProperties: false,
|
|
600
|
+
properties: {
|
|
601
|
+
id: { type: 'string' },
|
|
602
|
+
title: { type: 'string' },
|
|
603
|
+
score: { type: 'number' },
|
|
604
|
+
backlinks: { type: 'array', items: { type: 'string' } },
|
|
605
|
+
},
|
|
606
|
+
},
|
|
592
607
|
},
|
|
608
|
+
total: { type: 'integer' },
|
|
609
|
+
truncated: { type: 'boolean' },
|
|
593
610
|
},
|
|
594
611
|
},
|
|
595
612
|
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
@@ -632,12 +649,13 @@ export function apply(ctx, config) {
|
|
|
632
649
|
ctx.logger?.warn('tool-memory-filesystem: semantic fusion skipped: wiki_semantic_search returned an error');
|
|
633
650
|
}
|
|
634
651
|
}
|
|
635
|
-
|
|
652
|
+
const capped = hits.slice(0, resolved.maxSearchResults);
|
|
653
|
+
return { hits: capped, total: hits.length, truncated: capped.length < hits.length };
|
|
636
654
|
},
|
|
637
655
|
})));
|
|
638
656
|
ctx.effect(() => ctx.tools.register(defineTool({
|
|
639
657
|
name: 'wiki_write',
|
|
640
|
-
description: 'Create a new note or append to an existing note in the wiki vault. The path is relative to the vault root. When appending, the new content is inserted at the end of the body after a timestamp header.',
|
|
658
|
+
description: 'Create a new note or append to an existing note in the wiki vault. The path is relative to the vault root. When appending, the new content is inserted at the end of the body after a timestamp header. Returns { id, mode, bytes, version }, where version fingerprints the written content and can be passed as baseVersion on the next write without reading the note again.',
|
|
641
659
|
parameters: {
|
|
642
660
|
id: {
|
|
643
661
|
type: 'string',
|
|
@@ -656,7 +674,7 @@ export function apply(ctx, config) {
|
|
|
656
674
|
},
|
|
657
675
|
baseVersion: {
|
|
658
676
|
type: 'string',
|
|
659
|
-
description: 'Optional version returned by wiki_read. When provided, the write fails if the note changed since that
|
|
677
|
+
description: 'Optional version returned by wiki_read or by an earlier wiki_write. When provided, the write fails if the note changed since that version.',
|
|
660
678
|
},
|
|
661
679
|
},
|
|
662
680
|
output: {
|
|
@@ -667,6 +685,10 @@ export function apply(ctx, config) {
|
|
|
667
685
|
id: { type: 'string' },
|
|
668
686
|
mode: { type: 'string' },
|
|
669
687
|
bytes: { type: 'integer' },
|
|
688
|
+
version: { type: 'string' },
|
|
689
|
+
created: { type: 'string' },
|
|
690
|
+
updated: { type: 'string' },
|
|
691
|
+
committed: { type: 'boolean' },
|
|
670
692
|
},
|
|
671
693
|
},
|
|
672
694
|
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
@@ -694,11 +716,15 @@ export function apply(ctx, config) {
|
|
|
694
716
|
throw new Error(`tool-memory-filesystem: note ${args.id} changed since it was read; re-read it before writing`);
|
|
695
717
|
}
|
|
696
718
|
let finalBody;
|
|
719
|
+
let frontmatter = {};
|
|
697
720
|
if (mode === 'overwrite') {
|
|
698
721
|
finalBody = normalizeTextTimestamps(args.content);
|
|
722
|
+
frontmatter = splitFrontmatter(finalBody).frontmatter;
|
|
699
723
|
}
|
|
700
724
|
else {
|
|
701
|
-
const
|
|
725
|
+
const split = splitFrontmatter(existing ?? '');
|
|
726
|
+
frontmatter = split.frontmatter;
|
|
727
|
+
const body = split.body;
|
|
702
728
|
normalizeNoteTimestamps(frontmatter);
|
|
703
729
|
const timestamp = formatBeijingTime(new Date());
|
|
704
730
|
// Track note lifetime the way memory_capture does: `created` on the
|
|
@@ -722,7 +748,19 @@ export function apply(ctx, config) {
|
|
|
722
748
|
await writeAtomic(absolutePath, finalBody);
|
|
723
749
|
const info = await stat(absolutePath);
|
|
724
750
|
recordMtime(absolutePath, info.mtimeMs);
|
|
725
|
-
|
|
751
|
+
const result = {
|
|
752
|
+
id: vaultRelativeId(vaultRoot, absolutePath),
|
|
753
|
+
mode,
|
|
754
|
+
bytes: Buffer.byteLength(finalBody, 'utf8'),
|
|
755
|
+
// `writeAtomic` publishes `finalBody` verbatim, so this is the same
|
|
756
|
+
// fingerprint `wiki_read` would report — without a second read.
|
|
757
|
+
version: noteVersion(finalBody),
|
|
758
|
+
};
|
|
759
|
+
if (typeof frontmatter.created === 'string' && frontmatter.created !== '')
|
|
760
|
+
result.created = frontmatter.created;
|
|
761
|
+
if (typeof frontmatter.updated === 'string' && frontmatter.updated !== '')
|
|
762
|
+
result.updated = frontmatter.updated;
|
|
763
|
+
return result;
|
|
726
764
|
},
|
|
727
765
|
})));
|
|
728
766
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -10,8 +10,15 @@
|
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
11
|
import z from '@deepseek-ai/schemastery';
|
|
12
12
|
import { type ToolRunContext } from '@deepseek-ai/dsh-tools';
|
|
13
|
+
import { codeUnitCompare } from './search.ts';
|
|
13
14
|
import type { IndexedNote, Note } from './types.ts';
|
|
14
15
|
export type * from './types.ts';
|
|
16
|
+
/**
|
|
17
|
+
* Locale-independent note-id ordering, re-exported so sibling packages that
|
|
18
|
+
* truncate or tie-break id lists sort by the same rule instead of a copy that
|
|
19
|
+
* can drift.
|
|
20
|
+
*/
|
|
21
|
+
export { codeUnitCompare };
|
|
15
22
|
/**
|
|
16
23
|
* Compute a vault-relative note id from an absolute path. The result always
|
|
17
24
|
* uses POSIX separators so note ids are stable across Windows, macOS, and Linux.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacklika/dsh-tool-memory-filesystem",
|
|
3
3
|
"description": "Model-facing wiki/memory tools over a local Markdown vault: read, search, and write notes with Obsidian-style links and YAML frontmatter.",
|
|
4
|
-
"version": "0.1.3-alpha.
|
|
4
|
+
"version": "0.1.3-alpha.9",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|