@jacklika/dsh-tool-memory-filesystem 0.1.3-alpha.6 → 0.1.3-alpha.8

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
@@ -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. 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; 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 matching note ids, titles, scores, and backlink counts. Use this before asking the user which note to read.
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 matching note ids, titles, scores, and backlink counts. Use this before asking the user which note to read.
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.
@@ -570,7 +576,7 @@ export function apply(ctx, config) {
570
576
  })));
571
577
  ctx.effect(() => ctx.tools.register(defineTool({
572
578
  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 matching note ids, titles, scores, and backlink counts. Use this before asking the user which note to read.',
579
+ 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
580
  parameters: {
575
581
  query: {
576
582
  type: 'string',
@@ -580,16 +586,24 @@ export function apply(ctx, config) {
580
586
  },
581
587
  output: {
582
588
  schema: {
583
- type: 'array',
584
- items: {
585
- type: 'object',
586
- additionalProperties: false,
587
- properties: {
588
- id: { type: 'string' },
589
- title: { type: 'string' },
590
- score: { type: 'number' },
591
- backlinks: { type: 'array', items: { type: 'string' } },
589
+ type: 'object',
590
+ additionalProperties: false,
591
+ properties: {
592
+ hits: {
593
+ type: 'array',
594
+ items: {
595
+ type: 'object',
596
+ additionalProperties: false,
597
+ properties: {
598
+ id: { type: 'string' },
599
+ title: { type: 'string' },
600
+ score: { type: 'number' },
601
+ backlinks: { type: 'array', items: { type: 'string' } },
602
+ },
603
+ },
592
604
  },
605
+ total: { type: 'integer' },
606
+ truncated: { type: 'boolean' },
593
607
  },
594
608
  },
595
609
  render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
@@ -632,12 +646,13 @@ export function apply(ctx, config) {
632
646
  ctx.logger?.warn('tool-memory-filesystem: semantic fusion skipped: wiki_semantic_search returned an error');
633
647
  }
634
648
  }
635
- return hits.slice(0, resolved.maxSearchResults);
649
+ const capped = hits.slice(0, resolved.maxSearchResults);
650
+ return { hits: capped, total: hits.length, truncated: capped.length < hits.length };
636
651
  },
637
652
  })));
638
653
  ctx.effect(() => ctx.tools.register(defineTool({
639
654
  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.',
655
+ 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
656
  parameters: {
642
657
  id: {
643
658
  type: 'string',
@@ -656,7 +671,7 @@ export function apply(ctx, config) {
656
671
  },
657
672
  baseVersion: {
658
673
  type: 'string',
659
- description: 'Optional version returned by wiki_read. When provided, the write fails if the note changed since that read.',
674
+ 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
675
  },
661
676
  },
662
677
  output: {
@@ -667,6 +682,7 @@ export function apply(ctx, config) {
667
682
  id: { type: 'string' },
668
683
  mode: { type: 'string' },
669
684
  bytes: { type: 'integer' },
685
+ version: { type: 'string' },
670
686
  },
671
687
  },
672
688
  render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
@@ -700,17 +716,36 @@ export function apply(ctx, config) {
700
716
  else {
701
717
  const { frontmatter, body } = splitFrontmatter(existing ?? '');
702
718
  normalizeNoteTimestamps(frontmatter);
719
+ const timestamp = formatBeijingTime(new Date());
720
+ // Track note lifetime the way memory_capture does: `created` on the
721
+ // first write, `updated` on every append thereafter.
722
+ if (existing === undefined) {
723
+ frontmatter.created ??= timestamp;
724
+ }
725
+ else {
726
+ frontmatter.updated = timestamp;
727
+ }
703
728
  const frontmatterText = Object.keys(frontmatter).length > 0
704
729
  ? `---\n${yaml.dump(frontmatter).trim()}\n---\n\n`
705
730
  : '';
706
- const timestamp = formatBeijingTime(new Date());
707
- finalBody = `${frontmatterText}${body}\n\n## ${timestamp}\n\n${args.content}\n`;
731
+ // Content that opens with its own heading supplies the section
732
+ // header, so a bare `## <time>` above it would duplicate it.
733
+ const header = args.content.trimStart().startsWith('#') ? '' : `## ${timestamp}\n\n`;
734
+ const base = `${frontmatterText}${body}`.replace(/\s+$/, '');
735
+ finalBody = base === '' ? `${header}${args.content}\n` : `${base}\n\n${header}${args.content}\n`;
708
736
  }
709
737
  exec.signal.throwIfAborted();
710
738
  await writeAtomic(absolutePath, finalBody);
711
739
  const info = await stat(absolutePath);
712
740
  recordMtime(absolutePath, info.mtimeMs);
713
- return { id: vaultRelativeId(vaultRoot, absolutePath), mode, bytes: Buffer.byteLength(finalBody, 'utf8') };
741
+ return {
742
+ id: vaultRelativeId(vaultRoot, absolutePath),
743
+ mode,
744
+ bytes: Buffer.byteLength(finalBody, 'utf8'),
745
+ // `writeAtomic` publishes `finalBody` verbatim, so this is the same
746
+ // fingerprint `wiki_read` would report — without a second read.
747
+ version: noteVersion(finalBody),
748
+ };
714
749
  },
715
750
  })));
716
751
  }
@@ -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.6",
4
+ "version": "0.1.3-alpha.8",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/cordis": "4.0.4",
40
- "@deepseek-ai/dsh-tools": ">=0.1.7-rc.1 <0.3.0-0",
41
- "@deepseek-ai/dsh-util-values": ">=0.1.7-rc.1 <0.3.0-0"
40
+ "@deepseek-ai/dsh-tools": ">=0.1.7-rc.1 <0.3.0-0 || >=0.2.0-rc.1 <0.3.0-0",
41
+ "@deepseek-ai/dsh-util-values": ">=0.1.7-rc.1 <0.3.0-0 || >=0.2.0-rc.1 <0.3.0-0"
42
42
  }
43
43
  }