@jacklika/dsh-tool-memory-filesystem 0.1.3-alpha.5 → 0.1.3-alpha.6
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 +3 -3
- package/README.zh.md +3 -3
- package/lib/index.js +73 -3
- package/lib/types/index.d.ts +7 -0
- package/lib/types/types.d.ts +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,9 +52,9 @@ A relative `vaultRoot` resolves against the calling session's workspace.
|
|
|
52
52
|
|
|
53
53
|
### Tools
|
|
54
54
|
|
|
55
|
-
- `wiki_read(id)` — read one note by vault-relative path and return its frontmatter, body, links, linked notes,
|
|
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
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. Results include the score and backlink counts.
|
|
57
|
-
- `wiki_write(id, content, mode?, baseVersion?)` — create or append to a note. Append mode preserves frontmatter and adds a timestamp header. Passing a `version` from `wiki_read` as `baseVersion` makes the write fail loudly when another writer changed the note in between.
|
|
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.
|
|
58
58
|
|
|
59
59
|
### Security
|
|
60
60
|
|
|
@@ -81,7 +81,7 @@ The model sees the generated `wiki_read`, `wiki_search`, and `wiki_write` schema
|
|
|
81
81
|
##### Verbatim description for `wiki_read`
|
|
82
82
|
|
|
83
83
|
```markdown
|
|
84
|
-
Read one Markdown note from the wiki vault, optionally following Obsidian-style [[link]] references up to the configured depth. Returns the note id, frontmatter, body, and
|
|
84
|
+
Read one Markdown note from the wiki vault, optionally following Obsidian-style [[link]] references up to the configured depth. Returns the note id, frontmatter, body, linked notes, mtime, and a modifiedExternally flag that is true when the file changed on disk since this tool last observed it.
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
##### Verbatim description for `wiki_search`
|
package/README.zh.md
CHANGED
|
@@ -52,9 +52,9 @@ kind: "package-reference"
|
|
|
52
52
|
|
|
53
53
|
### 工具
|
|
54
54
|
|
|
55
|
-
- `wiki_read(id)` — 按仓库相对路径读取笔记,返回 frontmatter
|
|
55
|
+
- `wiki_read(id)` — 按仓库相对路径读取笔记,返回 frontmatter、正文、链接、已解析的链接笔记、内容指纹 `version`、`+08:00` 格式的文件 `mtime`,以及 `modifiedExternally` 标志(文件自本插件上次观察以来在磁盘上被改动时为 `true`,例如 Obsidian 编辑)。插件按绝对路径维护一个 256 条上限的 LRU mtime 表;自身的 `wiki_write` 会刷新记录,因此插件写入永不误报。
|
|
56
56
|
- `wiki_search(query)` — 按标题、id、正文关键词搜索;查询词采用 OR 匹配,并按字段加权的 BM25 式评分排序(标题/id 命中权重最高,稀有词权重更高),另有整段短语加分与被强命中笔记链接的图增强;挂载 `wiki_semantic_search` 时两路排序经 RRF 融合。结果包含分数与反向链接数量。
|
|
57
|
-
- `wiki_write(id, content, mode?, baseVersion?)` — 创建或追加笔记。追加模式保留 frontmatter
|
|
57
|
+
- `wiki_write(id, content, mode?, baseVersion?)` — 创建或追加笔记。追加模式保留 frontmatter 并添加时间戳标题;两种写入模式都会把 `created`/`updated` frontmatter 值归一化为 `+08:00` 秒级精度(不可解析的值原样保留)。把 `wiki_read` 返回的 `version` 作为 `baseVersion` 传入时,若笔记在读取后被其他写入方改动,写入会显式失败。
|
|
58
58
|
|
|
59
59
|
### 安全
|
|
60
60
|
|
|
@@ -81,7 +81,7 @@ kind: "package-reference"
|
|
|
81
81
|
##### `wiki_read` 的完整描述
|
|
82
82
|
|
|
83
83
|
```markdown
|
|
84
|
-
Read one Markdown note from the wiki vault, optionally following Obsidian-style [[link]] references up to the configured depth. Returns the note id, frontmatter, body, and
|
|
84
|
+
Read one Markdown note from the wiki vault, optionally following Obsidian-style [[link]] references up to the configured depth. Returns the note id, frontmatter, body, linked notes, mtime, and a modifiedExternally flag that is true when the file changed on disk since this tool last observed it.
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
##### `wiki_search` 的完整描述
|
package/lib/index.js
CHANGED
|
@@ -136,6 +136,68 @@ export function splitFrontmatter(text) {
|
|
|
136
136
|
export function noteVersion(text) {
|
|
137
137
|
return createHash('sha1').update(text, 'utf8').digest('hex');
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Last `mtimeMs` observed per note path, keyed by absolute path so identical
|
|
141
|
+
* note ids in different vaults never collide. Bounded at 256 entries with
|
|
142
|
+
* LRU eviction: an unbounded map would grow with every note ever read in a
|
|
143
|
+
* long-running process, while a fixed cap keeps the newest observations —
|
|
144
|
+
* the only ones a `modifiedExternally` comparison can still act on.
|
|
145
|
+
*/
|
|
146
|
+
const observedMtimes = new Map();
|
|
147
|
+
const OBSERVED_MTIME_LIMIT = 256;
|
|
148
|
+
/**
|
|
149
|
+
* Record the freshest observed `mtimeMs` for a path, evicting the oldest
|
|
150
|
+
* entry when the cap is reached.
|
|
151
|
+
*/
|
|
152
|
+
function recordMtime(path, mtimeMs) {
|
|
153
|
+
observedMtimes.delete(path);
|
|
154
|
+
observedMtimes.set(path, mtimeMs);
|
|
155
|
+
if (observedMtimes.size > OBSERVED_MTIME_LIMIT) {
|
|
156
|
+
const oldest = observedMtimes.keys().next().value;
|
|
157
|
+
if (oldest !== undefined)
|
|
158
|
+
observedMtimes.delete(oldest);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Whether the note changed on disk since this plugin last observed it.
|
|
163
|
+
* The first observation records and reports `false`; a successful
|
|
164
|
+
* `wiki_write` re-records so the plugin's own writes never flag.
|
|
165
|
+
*/
|
|
166
|
+
function detectExternalChange(path, mtimeMs) {
|
|
167
|
+
const seen = observedMtimes.get(path);
|
|
168
|
+
recordMtime(path, mtimeMs);
|
|
169
|
+
return seen !== undefined && seen !== mtimeMs;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Normalize `created`/`updated` frontmatter timestamps to the vault's
|
|
173
|
+
* `+08:00` second-precision convention. Parseable values in any other form
|
|
174
|
+
* (`Z`, `+00:00`, millisecond precision) are rewritten; unparseable values
|
|
175
|
+
* pass through untouched so a non-date string is never corrupted.
|
|
176
|
+
*/
|
|
177
|
+
export function normalizeNoteTimestamps(frontmatter) {
|
|
178
|
+
for (const key of ['created', 'updated']) {
|
|
179
|
+
const value = frontmatter[key];
|
|
180
|
+
if (typeof value !== 'string')
|
|
181
|
+
continue;
|
|
182
|
+
const parsed = new Date(value);
|
|
183
|
+
if (Number.isNaN(parsed.getTime()))
|
|
184
|
+
continue;
|
|
185
|
+
const normalized = formatBeijingTime(parsed);
|
|
186
|
+
if (normalized !== value)
|
|
187
|
+
frontmatter[key] = normalized;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Normalize timestamps inside a complete note text. Returns the text
|
|
192
|
+
* unchanged when there is no frontmatter to rewrite.
|
|
193
|
+
*/
|
|
194
|
+
function normalizeTextTimestamps(text) {
|
|
195
|
+
const { frontmatter, body } = splitFrontmatter(text);
|
|
196
|
+
if (Object.keys(frontmatter).length === 0)
|
|
197
|
+
return text;
|
|
198
|
+
normalizeNoteTimestamps(frontmatter);
|
|
199
|
+
return `---\n${yaml.dump(frontmatter).trim()}\n---\n${body}`;
|
|
200
|
+
}
|
|
139
201
|
/**
|
|
140
202
|
* Find all Obsidian-style `[[link]]` references in note text. Aliases of the
|
|
141
203
|
* form `[[link|alias]]` return the link target only.
|
|
@@ -480,7 +542,7 @@ export function apply(ctx, config) {
|
|
|
480
542
|
const vaultRootFor = (exec) => resolveMemoryVaultRoot(resolved.vaultRoot, exec);
|
|
481
543
|
ctx.effect(() => ctx.tools.register(defineTool({
|
|
482
544
|
name: 'wiki_read',
|
|
483
|
-
description: 'Read one Markdown note from the wiki vault, optionally following Obsidian-style [[link]] references up to the configured depth. Returns the note id, frontmatter, body, and
|
|
545
|
+
description: 'Read one Markdown note from the wiki vault, optionally following Obsidian-style [[link]] references up to the configured depth. Returns the note id, frontmatter, body, linked notes, mtime, and a modifiedExternally flag that is true when the file changed on disk since this tool last observed it.',
|
|
484
546
|
parameters: {
|
|
485
547
|
id: {
|
|
486
548
|
type: 'string',
|
|
@@ -498,7 +560,12 @@ export function apply(ctx, config) {
|
|
|
498
560
|
const linkResolver = createLinkResolver((await listNotePaths(vaultRoot, resolved.extensions, resolved.indexHiddenDirs))
|
|
499
561
|
.map(path => vaultRelativeId(vaultRoot, path)));
|
|
500
562
|
const note = await readNote(vaultRoot, resolved.extensions, resolved.maxLinkDepth, absolutePath, new Set(), linkResolver);
|
|
501
|
-
|
|
563
|
+
const info = await stat(absolutePath);
|
|
564
|
+
return {
|
|
565
|
+
...note,
|
|
566
|
+
mtime: formatBeijingTime(info.mtime),
|
|
567
|
+
modifiedExternally: detectExternalChange(absolutePath, info.mtimeMs),
|
|
568
|
+
};
|
|
502
569
|
},
|
|
503
570
|
})));
|
|
504
571
|
ctx.effect(() => ctx.tools.register(defineTool({
|
|
@@ -628,10 +695,11 @@ export function apply(ctx, config) {
|
|
|
628
695
|
}
|
|
629
696
|
let finalBody;
|
|
630
697
|
if (mode === 'overwrite') {
|
|
631
|
-
finalBody = args.content;
|
|
698
|
+
finalBody = normalizeTextTimestamps(args.content);
|
|
632
699
|
}
|
|
633
700
|
else {
|
|
634
701
|
const { frontmatter, body } = splitFrontmatter(existing ?? '');
|
|
702
|
+
normalizeNoteTimestamps(frontmatter);
|
|
635
703
|
const frontmatterText = Object.keys(frontmatter).length > 0
|
|
636
704
|
? `---\n${yaml.dump(frontmatter).trim()}\n---\n\n`
|
|
637
705
|
: '';
|
|
@@ -640,6 +708,8 @@ export function apply(ctx, config) {
|
|
|
640
708
|
}
|
|
641
709
|
exec.signal.throwIfAborted();
|
|
642
710
|
await writeAtomic(absolutePath, finalBody);
|
|
711
|
+
const info = await stat(absolutePath);
|
|
712
|
+
recordMtime(absolutePath, info.mtimeMs);
|
|
643
713
|
return { id: vaultRelativeId(vaultRoot, absolutePath), mode, bytes: Buffer.byteLength(finalBody, 'utf8') };
|
|
644
714
|
},
|
|
645
715
|
})));
|
package/lib/types/index.d.ts
CHANGED
|
@@ -88,6 +88,13 @@ export declare function splitFrontmatter(text: string): {
|
|
|
88
88
|
* @returns content hash stable across processes.
|
|
89
89
|
*/
|
|
90
90
|
export declare function noteVersion(text: string): string;
|
|
91
|
+
/**
|
|
92
|
+
* Normalize `created`/`updated` frontmatter timestamps to the vault's
|
|
93
|
+
* `+08:00` second-precision convention. Parseable values in any other form
|
|
94
|
+
* (`Z`, `+00:00`, millisecond precision) are rewritten; unparseable values
|
|
95
|
+
* pass through untouched so a non-date string is never corrupted.
|
|
96
|
+
*/
|
|
97
|
+
export declare function normalizeNoteTimestamps(frontmatter: Record<string, unknown>): void;
|
|
91
98
|
/**
|
|
92
99
|
* Find all Obsidian-style `[[link]]` references in note text. Aliases of the
|
|
93
100
|
* form `[[link|alias]]` return the link target only.
|
package/lib/types/types.d.ts
CHANGED
|
@@ -20,6 +20,17 @@ export interface Note {
|
|
|
20
20
|
* note in between.
|
|
21
21
|
*/
|
|
22
22
|
version: string;
|
|
23
|
+
/**
|
|
24
|
+
* File modification time rendered as a `+08:00` timestamp. Only set on the
|
|
25
|
+
* top-level note returned by `wiki_read`.
|
|
26
|
+
*/
|
|
27
|
+
mtime?: string;
|
|
28
|
+
/**
|
|
29
|
+
* True when the file's `mtime` changed since this plugin last observed it —
|
|
30
|
+
* evidence that an external writer (e.g. Obsidian) touched the note. Only
|
|
31
|
+
* set on the top-level note returned by `wiki_read`.
|
|
32
|
+
*/
|
|
33
|
+
modifiedExternally?: boolean;
|
|
23
34
|
}
|
|
24
35
|
/** Result of a search across the vault. */
|
|
25
36
|
export interface SearchResult {
|
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.6",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|