@jacklika/dsh-memory-curator 0.1.0-alpha.0

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 ADDED
@@ -0,0 +1,10 @@
1
+ # @jacklika/dsh-memory-curator
2
+
3
+ Curator tools for the DSH memory vault. This package is a member of the `@jacklika/dsh-memory` Bundle; do not install it directly.
4
+
5
+ ## Tools
6
+
7
+ - `memory_recall(query)` — Search the vault for prior notes before starting a task.
8
+ - `memory_capture(title, summary, ...)` — Persist durable knowledge after significant work, with conflict detection and optional user approval.
9
+
10
+ See the Bundle documentation in `docs/usage.md` for configuration and workflow.
package/README.zh.md ADDED
@@ -0,0 +1,10 @@
1
+ # @jacklika/dsh-memory-curator
2
+
3
+ DSH 记忆库的策展工具。本包是 `@jacklika/dsh-memory` Bundle 的成员,不要单独安装。
4
+
5
+ ## 工具
6
+
7
+ - `memory_recall(query)` — 在任务开始前搜索知识库中的相关笔记。
8
+ - `memory_capture(title, summary, ...)` — 在有意义的任务结束后持久化知识,支持冲突检测与可选的用户审批。
9
+
10
+ 配置与工作流请参阅 Bundle 文档 `docs/usage.zh.md`。
package/lib/index.js ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Curator tools for the DSH memory vault. `memory_recall` is meant to run at
3
+ * the start of a task so the agent does not ask the user to repeat context.
4
+ * `memory_capture` runs at the end of significant work: it derives a stable
5
+ * note id from the title, checks for conflicts with existing notes, asks the
6
+ * user for approval when one is found, and writes the captured knowledge to the
7
+ * shared curated zone.
8
+ * @module @jacklika/dsh-memory-curator
9
+ */
10
+ import { randomUUID } from 'node:crypto';
11
+ import z from '@deepseek-ai/schemastery';
12
+ import { ToolCallId } from '@deepseek-ai/dsh-llm';
13
+ import { defineTool } from '@deepseek-ai/dsh-tools';
14
+ import yaml from 'js-yaml';
15
+ /** Cordis plugin name used by loader diagnostics. */
16
+ export const name = 'memory-curator';
17
+ /** Services required by the curator tools. */
18
+ export const inject = ['tools'];
19
+ /** Schemastery configuration for the memory curator consumer. */
20
+ export const Config = z.object({
21
+ conflictPolicy: z.union(['ask', 'skip']).default('ask'),
22
+ defaultScope: z.union(['shared', 'private']).default('shared'),
23
+ notesDir: z.string().default('shared/notes'),
24
+ });
25
+ function normalizeTitle(title) {
26
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
27
+ }
28
+ function slugify(title) {
29
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
30
+ }
31
+ function ensureMdExtension(id) {
32
+ return id.endsWith('.md') ? id : `${id}.md`;
33
+ }
34
+ function resolveNoteId(id, title, scope, notesDir) {
35
+ if (id !== undefined && id !== '') {
36
+ const withExt = ensureMdExtension(id);
37
+ if (scope === 'private')
38
+ return withExt;
39
+ return withExt.startsWith('shared/') ? withExt : `${notesDir}/${withExt.replace(/^\//, '')}`;
40
+ }
41
+ const slug = slugify(title);
42
+ return scope === 'private' ? `${slug}.md` : `${notesDir}/${slug}.md`;
43
+ }
44
+ function textFromContent(content) {
45
+ return content
46
+ .map(block => {
47
+ const maybe = block;
48
+ return maybe.type === 'text' && typeof maybe.text === 'string' ? maybe.text : '';
49
+ })
50
+ .join('');
51
+ }
52
+ async function callTool(ctx, exec, toolName, args) {
53
+ const result = await ctx.tools.execute({
54
+ callId: ToolCallId(`${exec.callId}:${toolName}:${randomUUID()}`),
55
+ rootCallId: exec.rootCallId,
56
+ name: toolName,
57
+ arguments: args,
58
+ parent: exec.token,
59
+ signal: exec.signal,
60
+ ...(exec.agent !== undefined ? { agent: exec.agent } : {}),
61
+ });
62
+ if (result.isError) {
63
+ throw new Error(`memory-curator: nested ${toolName} failed: ${result.error.message}`);
64
+ }
65
+ return JSON.parse(textFromContent(result.content) || 'null');
66
+ }
67
+ function findConflicts(targetId, title, hits) {
68
+ const normalized = normalizeTitle(title);
69
+ return hits.filter(hit => {
70
+ if (hit.id.toLowerCase() === targetId.toLowerCase())
71
+ return true;
72
+ return normalizeTitle(hit.title) === normalized;
73
+ });
74
+ }
75
+ function dedupeHits(hits) {
76
+ return [...new Map(hits.map(hit => [hit.id, hit])).values()];
77
+ }
78
+ /**
79
+ * Register `memory_recall` and `memory_capture` on the tool registry.
80
+ * @param ctx - registrant context carrying the tool registry.
81
+ * @param config - deployment's explicit curator configuration.
82
+ */
83
+ export function apply(ctx, config) {
84
+ const tools = ctx.get('tools');
85
+ if (typeof tools?.execute !== 'function') {
86
+ ctx.logger?.warn('memory-curator: ctx.tools.execute unavailable; curator tools will not register.');
87
+ return;
88
+ }
89
+ const resolved = config;
90
+ const approval = ctx.get('approval');
91
+ ctx.effect(() => ctx.tools.register(defineTool({
92
+ name: 'memory_recall',
93
+ 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. Returns matching note ids, titles, and backlink counts.',
94
+ parameters: {
95
+ query: {
96
+ type: 'string',
97
+ required: true,
98
+ description: 'Keywords, concepts, or the task description to search for in the vault.',
99
+ },
100
+ },
101
+ output: {
102
+ schema: { type: 'json' },
103
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
104
+ },
105
+ async execute(args, exec) {
106
+ const hits = await callTool(ctx, exec, 'wiki_search', { query: args.query });
107
+ return {
108
+ query: args.query,
109
+ hits: hits.slice(0, 20),
110
+ total: hits.length,
111
+ };
112
+ },
113
+ })));
114
+ ctx.effect(() => ctx.tools.register(defineTool({
115
+ name: 'memory_capture',
116
+ description: 'Persist durable knowledge to the memory vault. Call this at the end of a significant task to capture decisions, discovered APIs, project conventions, fixes, or user preferences. It checks for conflicting notes, asks for approval when a conflict is found, and writes the captured note to the shared curated zone by default.',
117
+ parameters: {
118
+ title: {
119
+ type: 'string',
120
+ required: true,
121
+ description: 'Short human-readable title for the knowledge entry. Used to derive the note id when id is omitted.',
122
+ },
123
+ summary: {
124
+ type: 'string',
125
+ required: true,
126
+ description: 'The durable knowledge to record: the decision, API, convention, or fix in concise Markdown.',
127
+ },
128
+ details: {
129
+ type: 'string',
130
+ description: 'Optional additional Markdown context (error traces, alternatives considered, links).',
131
+ },
132
+ id: {
133
+ type: 'string',
134
+ description: 'Optional explicit vault-relative note id. If omitted, it is derived from the title as <notesDir>/<slug>.md.',
135
+ },
136
+ tags: {
137
+ type: 'array',
138
+ items: { type: 'string' },
139
+ description: 'Optional tags for the note frontmatter (e.g. ["api", "decision", "bugfix"]).',
140
+ },
141
+ scope: {
142
+ type: 'string',
143
+ enum: ['shared', 'private'],
144
+ description: 'Whether the note lives in the shared curated zone or the private agent namespace. Defaults to shared.',
145
+ },
146
+ mode: {
147
+ type: 'string',
148
+ enum: ['append', 'overwrite'],
149
+ description: 'When a conflict is approved or the note exists, append adds a timestamped section; overwrite replaces the whole body. Defaults to append.',
150
+ },
151
+ conflictPolicy: {
152
+ type: 'string',
153
+ enum: ['ask', 'skip'],
154
+ description: 'Override the deployment conflict policy for this capture: ask for approval on conflict, or skip the write.',
155
+ },
156
+ },
157
+ output: {
158
+ schema: { type: 'json' },
159
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
160
+ },
161
+ async execute(args, exec) {
162
+ const scope = args.scope ?? resolved.defaultScope;
163
+ const notesDir = scope === 'shared' ? resolved.notesDir : '';
164
+ const targetId = resolveNoteId(args.id, args.title, scope, notesDir);
165
+ const mode = args.mode ?? 'append';
166
+ const tags = Array.isArray(args.tags) ? args.tags.map(String) : [];
167
+ const [titleHits, idHits] = await Promise.all([
168
+ callTool(ctx, exec, 'wiki_search', { query: args.title }),
169
+ callTool(ctx, exec, 'wiki_search', { query: targetId }),
170
+ ]);
171
+ const conflicts = dedupeHits(findConflicts(targetId, args.title, [...titleHits, ...idHits]));
172
+ const conflictExists = conflicts.length > 0;
173
+ const finalId = conflicts[0]?.id ?? targetId;
174
+ if (conflictExists) {
175
+ const policy = args.conflictPolicy ?? resolved.conflictPolicy;
176
+ if (policy === 'skip') {
177
+ return {
178
+ written: false,
179
+ id: finalId,
180
+ conflict: true,
181
+ reason: 'skipped by conflictPolicy=skip',
182
+ };
183
+ }
184
+ if (approval === undefined || exec.agent === undefined) {
185
+ return {
186
+ written: false,
187
+ id: finalId,
188
+ conflict: true,
189
+ reason: 'approval service or agent unavailable; set conflictPolicy=skip to allow silent skip in headless mode',
190
+ };
191
+ }
192
+ const conflictList = conflicts.map(c => `- ${c.id} (${c.title})`).join('\n');
193
+ const outcome = await approval.request({
194
+ agent: exec.agent,
195
+ toolName: 'memory_capture',
196
+ callId: exec.callId,
197
+ reason: `memory_capture conflicts with existing note(s):\n${conflictList}\nProposed action: ${mode === 'append' ? 'append a new section to' : 'overwrite'} "${finalId}".`,
198
+ displayReason: {
199
+ en: `Memory capture conflicts with existing note(s):\n${conflictList}\nAllow ${mode} to "${finalId}"?`,
200
+ },
201
+ signal: exec.signal,
202
+ });
203
+ if (outcome !== 'allowed-once') {
204
+ return {
205
+ written: false,
206
+ id: finalId,
207
+ conflict: true,
208
+ approval: outcome,
209
+ reason: 'user did not approve the conflict',
210
+ };
211
+ }
212
+ }
213
+ const now = new Date().toISOString();
214
+ const detailsText = args.details ? `\n\n## Details\n\n${args.details}` : '';
215
+ const appendContent = `${args.summary}${detailsText}\n`;
216
+ const frontmatter = {
217
+ title: args.title,
218
+ tags,
219
+ created: now,
220
+ };
221
+ const newNoteContent = `---\n${yaml.dump(frontmatter).trim()}\n---\n\n# ${args.title}\n\n${appendContent}`;
222
+ const noteExists = conflictExists;
223
+ const content = noteExists && mode === 'append' ? appendContent : newNoteContent;
224
+ const writeMode = mode;
225
+ await callTool(ctx, exec, 'wiki_write', {
226
+ id: finalId,
227
+ content,
228
+ mode: writeMode,
229
+ });
230
+ return {
231
+ written: true,
232
+ id: finalId,
233
+ mode: writeMode,
234
+ conflict: conflictExists,
235
+ };
236
+ },
237
+ })));
238
+ }
239
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Curator tools for the DSH memory vault. `memory_recall` is meant to run at
3
+ * the start of a task so the agent does not ask the user to repeat context.
4
+ * `memory_capture` runs at the end of significant work: it derives a stable
5
+ * note id from the title, checks for conflicts with existing notes, asks the
6
+ * user for approval when one is found, and writes the captured knowledge to the
7
+ * shared curated zone.
8
+ * @module @jacklika/dsh-memory-curator
9
+ */
10
+ import type { Context } from '@deepseek-ai/cordis';
11
+ import z from '@deepseek-ai/schemastery';
12
+ /** Cordis plugin name used by loader diagnostics. */
13
+ export declare const name = "memory-curator";
14
+ /** Services required by the curator tools. */
15
+ export declare const inject: string[];
16
+ /** Plugin configuration. */
17
+ export interface Config {
18
+ /**
19
+ * What to do when a capture matches an existing note. `ask` requests user
20
+ * approval through the DSH approval seam before writing. `skip` silently
21
+ * aborts the write so nothing is overwritten in headless or CI runs.
22
+ */
23
+ conflictPolicy?: 'ask' | 'skip';
24
+ /**
25
+ * Default zone for captured notes. `shared` puts them under the configured
26
+ * `notesDir` and makes them visible to every agent and session. `private`
27
+ * leaves the id unchanged so `memory-scope` routes it under the current
28
+ * agent's namespace.
29
+ */
30
+ defaultScope?: 'shared' | 'private';
31
+ /** Vault-relative directory where shared curated notes live. */
32
+ notesDir?: string;
33
+ }
34
+ /** Schemastery configuration for the memory curator consumer. */
35
+ export declare const Config: z<Config>;
36
+ /**
37
+ * Register `memory_recall` and `memory_capture` on the tool registry.
38
+ * @param ctx - registrant context carrying the tool registry.
39
+ * @param config - deployment's explicit curator configuration.
40
+ */
41
+ export declare function apply(ctx: Context, config: Config): void;
42
+ //# sourceMappingURL=index.d.ts.map
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@jacklika/dsh-memory-curator",
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.0",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/JackLiKa/jacklika-agent-plugins-repo.git",
11
+ "directory": "packages/memory-curator"
12
+ },
13
+ "type": "module",
14
+ "engines": {
15
+ "node": "^22.19.0 || >=24.0.0"
16
+ },
17
+ "main": "lib/index.js",
18
+ "types": "lib/types/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./lib/types/index.d.ts",
22
+ "default": "./lib/index.js"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "files": [
27
+ "lib/**/*.js",
28
+ "lib/types/**/*.d.ts",
29
+ "README.md",
30
+ "README.zh.md"
31
+ ],
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "@deepseek-ai/schemastery": "3.18.4",
35
+ "js-yaml": "^4.2.0"
36
+ },
37
+ "peerDependencies": {
38
+ "@deepseek-ai/cordis": "4.0.4",
39
+ "@deepseek-ai/dsh-agent": ">=0.1.7-rc.1 <0.3.0-0",
40
+ "@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.3.0-0",
41
+ "@deepseek-ai/dsh-tools": ">=0.1.7-rc.1 <0.3.0-0",
42
+ "@deepseek-ai/dsh-user-approval": ">=0.1.7-rc.1 <0.3.0-0",
43
+ "@deepseek-ai/dsh-util-values": ">=0.1.7-rc.1 <0.3.0-0"
44
+ },
45
+ "devDependencies": {
46
+ "@jacklika/dsh-tool-memory-filesystem": "0.1.3-alpha.2"
47
+ }
48
+ }