@jacklika/dsh-memory-curator 0.1.0-alpha.4 → 0.1.0-alpha.5
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 +9 -1
- package/README.zh.md +9 -1
- package/lib/index.js +64 -20
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -4,7 +4,15 @@ Curator tools for the DSH memory vault. This package is a member of the `@jackli
|
|
|
4
4
|
|
|
5
5
|
## Tools
|
|
6
6
|
|
|
7
|
-
- `memory_recall(query)` — Search the vault for prior notes before starting a task.
|
|
7
|
+
- `memory_recall(query)` — Search the vault for prior notes before starting a task. Returns `{ query, hits, total }`, where `total` is the vault-wide match count passed through from `wiki_search` and can exceed `hits.length` when the search layer caps its results — so a caller can tell "these are all the matches" from "there are more".
|
|
8
8
|
- `memory_capture(title, summary, ...)` — Persist durable knowledge after significant work, with conflict detection and optional user approval.
|
|
9
9
|
|
|
10
|
+
## Note ids
|
|
11
|
+
|
|
12
|
+
When `id` is omitted, `memory_capture` derives it from the title: Unicode letters and digits are kept (NFC-normalized, lowercased) and every other run of characters becomes a single `-`, so CJK and other non-Latin titles stay readable and distinct. A title with no letters or digits at all falls back to `note-<12 hex chars of its sha256>`, so symbol-only captures never collapse into one shared file.
|
|
13
|
+
|
|
14
|
+
## Overwriting a capture
|
|
15
|
+
|
|
16
|
+
`mode: 'overwrite'` replaces the note body but merges its frontmatter: fields another writer recorded and the original `created` survive, `title` and `tags` come from this call, and `updated` is stamped. A first capture writes `created` only.
|
|
17
|
+
|
|
10
18
|
See the Bundle documentation in `docs/usage.md` for configuration and workflow.
|
package/README.zh.md
CHANGED
|
@@ -4,7 +4,15 @@ DSH 记忆库的策展工具。本包是 `@jacklika/dsh-memory` Bundle 的成员
|
|
|
4
4
|
|
|
5
5
|
## 工具
|
|
6
6
|
|
|
7
|
-
- `memory_recall(query)` —
|
|
7
|
+
- `memory_recall(query)` — 在任务开始前搜索知识库中的相关笔记。返回 `{ query, hits, total }`,其中 `total` 是从 `wiki_search` 透传的全库命中数,在搜索层截断结果时可能大于 `hits.length`——调用方因此能区分“这就是全部命中”与“还有更多”。
|
|
8
8
|
- `memory_capture(title, summary, ...)` — 在有意义的任务结束后持久化知识,支持冲突检测与可选的用户审批。
|
|
9
9
|
|
|
10
|
+
## 笔记 id
|
|
11
|
+
|
|
12
|
+
省略 `id` 时,`memory_capture` 由标题推导笔记 id:保留 Unicode 字母与数字(先做 NFC 归一化并转小写),其余连续字符统一折叠为单个 `-`,因此中日韩等非拉丁标题依然可读且互不相同。完全不含字母与数字的标题退化为 `note-<标题 sha256 的前 12 位十六进制>`,纯符号标题的写入不会坍缩到同一个文件。
|
|
13
|
+
|
|
14
|
+
## 覆盖式入库
|
|
15
|
+
|
|
16
|
+
`mode: 'overwrite'` 替换笔记正文,但会合并既有 frontmatter:其他写入方记录的字段与原始 `created` 都保留,`title` 与 `tags` 以本次调用为准,并写入 `updated`。首次入库只写 `created`。
|
|
17
|
+
|
|
10
18
|
配置与工作流请参阅 Bundle 文档 `docs/usage.zh.md`。
|
package/lib/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* shared curated zone.
|
|
8
8
|
* @module @jacklika/dsh-memory-curator
|
|
9
9
|
*/
|
|
10
|
-
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
11
11
|
import z from '@deepseek-ai/schemastery';
|
|
12
12
|
import { ToolCallId } from '@deepseek-ai/dsh-llm';
|
|
13
13
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
@@ -23,11 +23,22 @@ export const Config = z.object({
|
|
|
23
23
|
defaultScope: z.union(['shared', 'private']).default('shared'),
|
|
24
24
|
notesDir: z.string().default('shared/notes'),
|
|
25
25
|
});
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Fold a title to its comparison form: NFC, case-insensitive, every run of
|
|
28
|
+
* characters that are neither Unicode letters nor digits collapsed to one
|
|
29
|
+
* space. Keeping `\p{L}` means CJK and other non-Latin titles stay distinct
|
|
30
|
+
* instead of folding to the empty string they would all share.
|
|
31
|
+
*/
|
|
32
|
+
function foldTitle(title) {
|
|
33
|
+
return title.normalize('NFC').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim();
|
|
28
34
|
}
|
|
29
35
|
function slugify(title) {
|
|
30
|
-
|
|
36
|
+
const slug = foldTitle(title).replace(/ /g, '-');
|
|
37
|
+
// Emoji-only and punctuation-only titles fold to nothing; a stable content
|
|
38
|
+
// hash keeps them addressable and separate rather than sharing a bare ".md".
|
|
39
|
+
if (slug === '')
|
|
40
|
+
return `note-${createHash('sha256').update(title.normalize('NFC')).digest('hex').slice(0, 12)}`;
|
|
41
|
+
return slug;
|
|
31
42
|
}
|
|
32
43
|
function ensureMdExtension(id) {
|
|
33
44
|
return id.endsWith('.md') ? id : `${id}.md`;
|
|
@@ -66,16 +77,50 @@ async function callTool(ctx, exec, toolName, args) {
|
|
|
66
77
|
return JSON.parse(textFromContent(result.content) || 'null');
|
|
67
78
|
}
|
|
68
79
|
function findConflicts(targetId, title, hits) {
|
|
69
|
-
const normalized =
|
|
80
|
+
const normalized = foldTitle(title);
|
|
81
|
+
const target = targetId.normalize('NFC').toLowerCase();
|
|
70
82
|
return hits.filter(hit => {
|
|
71
|
-
if (hit.id.toLowerCase() ===
|
|
83
|
+
if (hit.id.normalize('NFC').toLowerCase() === target)
|
|
72
84
|
return true;
|
|
73
|
-
|
|
85
|
+
// A title that folds to nothing carries no identity; matching it would make
|
|
86
|
+
// every untitled note a conflict with every other one.
|
|
87
|
+
return normalized !== '' && foldTitle(hit.title) === normalized;
|
|
74
88
|
});
|
|
75
89
|
}
|
|
76
90
|
function dedupeHits(hits) {
|
|
77
91
|
return [...new Map(hits.map(hit => [hit.id, hit])).values()];
|
|
78
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Frontmatter for a capture that overwrites `id`. `wiki_write` overwrite
|
|
95
|
+
* replaces the whole file, so the note's existing fields have to be carried
|
|
96
|
+
* over here or an update silently drops whatever another writer recorded and
|
|
97
|
+
* resets `created` to the update time.
|
|
98
|
+
* @param ctx - registrant context carrying the tool registry.
|
|
99
|
+
* @param exec - the run context the nested read is dispatched under.
|
|
100
|
+
* @param id - vault-relative note id about to be overwritten.
|
|
101
|
+
* @param title - title from this capture; it wins over any stored one.
|
|
102
|
+
* @param tags - tags from this capture; they win over any stored ones.
|
|
103
|
+
* @param now - Beijing-time timestamp used for `created`/`updated`.
|
|
104
|
+
* @returns the merged frontmatter to serialize.
|
|
105
|
+
*/
|
|
106
|
+
async function overwriteFrontmatter(ctx, exec, id, title, tags, now) {
|
|
107
|
+
let existing;
|
|
108
|
+
try {
|
|
109
|
+
const note = await callTool(ctx, exec, 'wiki_read', { id });
|
|
110
|
+
if (note !== null && typeof note === 'object' && note.frontmatter !== null && typeof note.frontmatter === 'object') {
|
|
111
|
+
existing = note.frontmatter;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// No readable note at this id: the ordinary first capture of a new note.
|
|
116
|
+
}
|
|
117
|
+
const frontmatter = { ...existing, title, tags };
|
|
118
|
+
const created = existing?.created;
|
|
119
|
+
frontmatter.created = typeof created === 'string' && created !== '' ? created : now;
|
|
120
|
+
if (existing !== undefined)
|
|
121
|
+
frontmatter.updated = now;
|
|
122
|
+
return frontmatter;
|
|
123
|
+
}
|
|
79
124
|
/**
|
|
80
125
|
* Register `memory_recall` and `memory_capture` on the tool registry.
|
|
81
126
|
* @param ctx - registrant context carrying the tool registry.
|
|
@@ -91,7 +136,7 @@ export function apply(ctx, config) {
|
|
|
91
136
|
const approval = ctx.get('approval');
|
|
92
137
|
ctx.effect(() => ctx.tools.register(defineTool({
|
|
93
138
|
name: 'memory_recall',
|
|
94
|
-
description: 'Search the memory vault for prior notes related to the current task or topic. Call this at the start of a development task to avoid asking the user to repeat context that is already captured. Query terms are OR-matched and ranked by field-weighted relevance, so partial matches still return. Returns matching note ids, titles, scores, and
|
|
139
|
+
description: 'Search the memory vault for prior notes related to the current task or topic. Call this at the start of a development task to avoid asking the user to repeat context that is already captured. Query terms are OR-matched and ranked by field-weighted relevance, so partial matches still return. Returns the matching note ids, titles, scores, and backlinks, plus total — how many notes matched before the result cap, so a total above the returned hit count means the list was truncated.',
|
|
95
140
|
parameters: {
|
|
96
141
|
query: {
|
|
97
142
|
type: 'string',
|
|
@@ -104,11 +149,13 @@ export function apply(ctx, config) {
|
|
|
104
149
|
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
105
150
|
},
|
|
106
151
|
async execute(args, exec) {
|
|
107
|
-
const
|
|
152
|
+
const search = await callTool(ctx, exec, 'wiki_search', { query: args.query });
|
|
108
153
|
return {
|
|
109
154
|
query: args.query,
|
|
110
|
-
hits: hits.slice(0, 20),
|
|
111
|
-
|
|
155
|
+
hits: search.hits.slice(0, 20),
|
|
156
|
+
// Forward the ranking's own count: `hits` is already capped upstream, so
|
|
157
|
+
// its length would understate how much actually matched.
|
|
158
|
+
total: search.total,
|
|
112
159
|
};
|
|
113
160
|
},
|
|
114
161
|
})));
|
|
@@ -165,11 +212,11 @@ export function apply(ctx, config) {
|
|
|
165
212
|
const targetId = resolveNoteId(args.id, args.title, scope, notesDir);
|
|
166
213
|
const mode = args.mode ?? 'append';
|
|
167
214
|
const tags = Array.isArray(args.tags) ? args.tags.map(String) : [];
|
|
168
|
-
const [
|
|
215
|
+
const [titleSearch, idSearch] = await Promise.all([
|
|
169
216
|
callTool(ctx, exec, 'wiki_search', { query: args.title }),
|
|
170
217
|
callTool(ctx, exec, 'wiki_search', { query: targetId }),
|
|
171
218
|
]);
|
|
172
|
-
const conflicts = dedupeHits(findConflicts(targetId, args.title, [...
|
|
219
|
+
const conflicts = dedupeHits(findConflicts(targetId, args.title, [...titleSearch.hits, ...idSearch.hits]));
|
|
173
220
|
const conflictExists = conflicts.length > 0;
|
|
174
221
|
const finalId = conflicts[0]?.id ?? targetId;
|
|
175
222
|
if (conflictExists) {
|
|
@@ -214,15 +261,12 @@ export function apply(ctx, config) {
|
|
|
214
261
|
const now = formatBeijingTime(new Date());
|
|
215
262
|
const detailsText = args.details ? `\n\n## Details\n\n${args.details}` : '';
|
|
216
263
|
const appendContent = `${args.summary}${detailsText}\n`;
|
|
217
|
-
const frontmatter = {
|
|
218
|
-
title: args.title,
|
|
219
|
-
tags,
|
|
220
|
-
created: now,
|
|
221
|
-
};
|
|
222
|
-
const newNoteContent = `---\n${yaml.dump(frontmatter).trim()}\n---\n\n# ${args.title}\n\n${appendContent}`;
|
|
223
264
|
const appending = conflictExists && mode === 'append';
|
|
224
|
-
const content = appending ? appendContent : newNoteContent;
|
|
225
265
|
const writeMode = appending ? 'append' : 'overwrite';
|
|
266
|
+
const frontmatter = appending ? undefined : await overwriteFrontmatter(ctx, exec, finalId, args.title, tags, now);
|
|
267
|
+
const content = appending
|
|
268
|
+
? appendContent
|
|
269
|
+
: `---\n${yaml.dump(frontmatter).trim()}\n---\n\n# ${args.title}\n\n${appendContent}`;
|
|
226
270
|
await callTool(ctx, exec, 'wiki_write', {
|
|
227
271
|
id: finalId,
|
|
228
272
|
content,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacklika/dsh-memory-curator",
|
|
3
3
|
"description": "Curator tools for the DSH memory vault: memory_recall before acting and memory_capture after significant work, with conflict detection and optional user approval.",
|
|
4
|
-
"version": "0.1.0-alpha.
|
|
4
|
+
"version": "0.1.0-alpha.5",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@deepseek-ai/cordis": "4.0.4",
|
|
40
|
-
"@deepseek-ai/dsh-agent": ">=0.1.7-rc.1 <0.3.0-0",
|
|
41
|
-
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.3.0-0",
|
|
42
|
-
"@deepseek-ai/dsh-tools": ">=0.1.7-rc.1 <0.3.0-0",
|
|
43
|
-
"@deepseek-ai/dsh-user-approval": ">=0.1.7-rc.1 <0.3.0-0",
|
|
44
|
-
"@deepseek-ai/dsh-util-values": ">=0.1.7-rc.1 <0.3.0-0"
|
|
40
|
+
"@deepseek-ai/dsh-agent": ">=0.1.7-rc.1 <0.3.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
41
|
+
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.3.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
42
|
+
"@deepseek-ai/dsh-tools": ">=0.1.7-rc.1 <0.3.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
43
|
+
"@deepseek-ai/dsh-user-approval": ">=0.1.7-rc.1 <0.3.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
44
|
+
"@deepseek-ai/dsh-util-values": ">=0.1.7-rc.1 <0.3.0-0 || >=0.2.0-rc.1 <0.3.0-0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@jacklika/dsh-tool-memory-filesystem": "^0.1.3-alpha.
|
|
47
|
+
"@jacklika/dsh-tool-memory-filesystem": "^0.1.3-alpha.8"
|
|
48
48
|
}
|
|
49
49
|
}
|