@cxx42/dsh-tool-notes 1.0.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,171 @@
1
+ # dsh-tool-notes
2
+
3
+ 一个给 DeepSeek Harness 用的**最小可运行工具插件**:给模型新增三个「个人笔记库」工具。
4
+
5
+ | 工具 | 作用 |
6
+ |---|---|
7
+ | `note_add` | 把一条笔记追加到本地 JSONL 库(`~/.dsh/notes/notes.jsonl`) |
8
+ | `note_search` | 在标题 / 正文 / 标签里做大小写不敏感的子串搜索,按新到旧返回 |
9
+ | `note_list` | 列出最近的笔记,可按标签过滤 |
10
+
11
+ 这个包本身就是**一个 bundle**(`package.json` 里声明了 `dsh.bundle.patch`),
12
+ `cordis.patch.yml` 插入一行自引用(`name: 'dsh-tool-notes'`)——和官方
13
+ `@deepseek-ai/dsh-web-app` 的写法一致。
14
+
15
+ ## 使用方法
16
+
17
+ 插件不给用户提供按钮/命令,它是模型的工具:**新会话**里的 agent 会在合适时机自动调用。
18
+ 你只需要用自然语言提需求,例如:
19
+
20
+ | 你说 | 模型会调用 | 效果 |
21
+ |---|---|---|
22
+ | 「把 XXX 记到笔记里,标题《YYY》,标签 a、b」 | `note_add` | 追加一条笔记到 `~/.dsh/notes/notes.jsonl` |
23
+ | 「搜一下笔记里关于 XX 的内容」 | `note_search` | 标题/正文/标签全文搜索,新到旧返回 |
24
+ | 「列出最近的笔记,只要某标签的」 | `note_list` | 按标签过滤列出最近笔记 |
25
+
26
+ 效果可见处:① 对话流里的工具调用卡片(Add note / Search notes / List notes);
27
+ ② 磁盘文件 `C:\Users\cxx\.dsh\notes\notes.jsonl`(每调用一次 `note_add` 追加一行 JSON);③ 跨会话持久——任何会话都能搜到之前记的笔记。
28
+
29
+ 注意:工具列表在会话创建时绑定,**改过插件或装新工具后请新开会话**再测试。
30
+
31
+ ## 安装(装进你正在用的 profile)
32
+
33
+ ```powershell
34
+ # 装进 desktop profile(当前 GUI 用的就是这个)
35
+ dsh plugin --profile desktop add "file:C:\Users\cxx\Documents\DSHWorkspace\dsh_project1"
36
+
37
+ # 装进 web profile 则换成
38
+ dsh plugin --profile web add "file:C:\Users\cxx\Documents\DSHWorkspace\dsh_project1"
39
+ ```
40
+
41
+ `dsh plugin` 是 pnpm 的薄封装:它会在 profile 目录里跑 `pnpm add`,
42
+ 然后把「声明了 `dsh.bundle` 的包」自动追加进 `dsh.profile.bundles` 层栈。
43
+ 装完**重启 GUI**(启动时会重新组合 bundle 层),工具就出现在模型工具列表里。
44
+
45
+ ## 卸载
46
+
47
+ ```powershell
48
+ dsh plugin --profile desktop remove dsh-tool-notes
49
+ ```
50
+
51
+ ## 本地开发循环
52
+
53
+ ⚠️ Windows 上 `file:` 安装会在 `node_modules` 里生成**实体拷贝,重装不会自动刷新**
54
+ (详见下方常见问题)。改完源码后,任选一种方式刷新 profile 里的拷贝:
55
+
56
+ ```powershell
57
+ # 方式 A(最省事):直接覆盖拷贝里的文件,然后重启 GUI
58
+ Copy-Item "C:\Users\cxx\Documents\DSHWorkspace\dsh_project1\index.js" "C:\Users\cxx\.dsh\profiles\desktop\node_modules\dsh-tool-notes\index.js" -Force
59
+ ```
60
+
61
+ ```powershell
62
+ # 方式 B(彻底重建):
63
+ Remove-Item "C:\Users\cxx\.dsh\profiles\desktop\node_modules\dsh-tool-notes" -Recurse -Force
64
+ dsh plugin --profile desktop install
65
+ ```
66
+
67
+ ## 结构与原理
68
+
69
+ ```
70
+ dsh_project1/
71
+ ├── package.json # dsh.bundle.patch 指向 cordis.patch.yml;依赖 @deepseek-ai/dsh-tools
72
+ ├── cordis.patch.yml # bundle 补丁:insert 一行 tool-notes,config 里 vaultDir 用 !!js dshHomePath('notes')
73
+ ├── index.js # 模块插件:导出 name / inject / Config / apply
74
+ └── README.md
75
+ ```
76
+
77
+ - **插件契约**:Cordis 模块插件导出 `name`、`inject`(声明的服务就绪后才 apply)、
78
+ `Config`(schemastery schema,对应 patch 行的 `config:`)、`apply(ctx, config)`。
79
+ 参考官方 `@deepseek-ai/dsh-tool-todo`。
80
+ - **工具注册**:`ctx.tools.register(defineTool({...}))`,来自
81
+ `@deepseek-ai/dsh-tools`;`parameters` 用 JSON Schema 风格描述,
82
+ `output.schema` + `output.render` 决定 Web GUI 里的展示。
83
+ - **配置注入**:`config.vaultDir` 来自 patch 行的 `vaultDir: !!js dshHomePath('notes')`,
84
+ 与官方 `session-persistence-jsonl` 行的用法相同。
85
+
86
+ ## 注意
87
+
88
+ - 本插件的工具直接使用 `node:fs` 读写 `vaultDir`(默认 `~/.dsh/notes`),
89
+ 不走 DSH 的沙箱文件服务——这是有意为之的简单实现。它只触碰配置的目录,
90
+ 不执行 shell,也没有路径逃逸;如果你要接通用文件访问,建议改走
91
+ `dsh-fs-sandbox` 服务。
92
+ - 依赖 `@deepseek-ai/dsh-tools@^0.1.1-rc.2`:首次 `dsh plugin add` 时
93
+ pnpm 会从 npm registry 拉取(官方包已公开发布)。
94
+
95
+ ## 常见问题
96
+
97
+ ### 启动崩溃:`parameters.xxx.required must be true when present`
98
+
99
+ `defineTool` 的参数/输出规范 DSL 里,**可选参数直接省略 `required` 字段**,
100
+ 写 `required: false` 会被 `dsh-tools` 的 schema 编译器拒绝(作者错误),
101
+ 整个插件树加载失败、harness 起不来。
102
+
103
+ 规则(见 `dsh-tools` 的 `runSchemaCompiler`:`required` 要么缺省、要么 `true`):
104
+
105
+ ```js
106
+ parameters: {
107
+ title: { type: 'string', required: true }, // ✅ 必填
108
+ tags: { type: 'array', items: { type: 'string' } }, // ✅ 可选:不写 required
109
+ // ❌ 错误:required: false
110
+ }
111
+ ```
112
+
113
+ ### ⚠️ 关键的坑:`file:` 安装的拷贝不会自动刷新(Windows)
114
+
115
+ pnpm 把这个 `file:` 依赖按 `type: directory`(链接目录协议)处理,但在 Windows 上
116
+ `node_modules/dsh-tool-notes` 是安装时**实体拷贝出来的目录**——之后即使源目录改了、
117
+ 甚至 `add --force` 重装,**拷贝内容也不会同步**(lockfile 只记目录路径,不校验内容)。
118
+ 表现:改了插件源码、重装、重启,加载到的还是旧代码。
119
+
120
+ 改完源码后的两种可靠刷新方式(任选其一):
121
+
122
+ ```powershell
123
+ # 方式 A(最省事):直接覆盖拷贝里的文件
124
+ Copy-Item "C:\Users\cxx\Documents\DSHWorkspace\dsh_project1\index.js" "C:\Users\cxx\.dsh\profiles\desktop\node_modules\dsh-tool-notes\index.js" -Force
125
+ ```
126
+
127
+ ```powershell
128
+ # 方式 B(彻底重建):删掉拷贝再让 pnpm 重建
129
+ Remove-Item "C:\Users\cxx\.dsh\profiles\desktop\node_modules\dsh-tool-notes" -Recurse -Force
130
+ dsh plugin --profile desktop install
131
+ ```
132
+
133
+ ## 常见问题
134
+
135
+ ### 启动崩溃:`parameters.xxx.required must be true when present`
136
+
137
+ `defineTool` 的参数/输出规范 DSL 里,**可选参数直接省略 `required` 字段**,
138
+ 写 `required: false` 会被 `dsh-tools` 的 schema 编译器拒绝(作者错误),
139
+ 整个插件树加载失败、harness 起不来。
140
+
141
+ 规则(见 `dsh-tools` 的 `runSchemaCompiler`:`required` 要么缺省、要么 `true`):
142
+
143
+ ```js
144
+ parameters: {
145
+ title: { type: 'string', required: true }, // ✅ 必填
146
+ tags: { type: 'array', items: { type: 'string' } }, // ✅ 可选:不写 required
147
+ // ❌ 错误:required: false
148
+ }
149
+ ```
150
+
151
+ 修复后按上面的「方式 A/B」刷新 profile 里的拷贝,再重启。
152
+
153
+ ### 重装报错:`another plugin install recovery transaction is pending`
154
+
155
+ 桌面版 `dsh` 给插件安装加了崩溃恢复 WAL(备份 profile 的 package.json /
156
+ pnpm-lock.yaml / pnpm-workspace.yaml)。装完启动失败会自动回滚,但**手动安装的回滚
157
+ 事务不会自动清除**,而事务文件存在时任何新安装都会被拒绝。清掉即可:
158
+
159
+ ```powershell
160
+ Remove-Item "C:\Users\cxx\AppData\Roaming\DSH Desktop\plugin-install-recovery\state.json" -ErrorAction SilentlyContinue
161
+ Remove-Item "C:\Users\cxx\AppData\Roaming\DSH Desktop\plugin-install-recovery\backups" -Recurse -Force -ErrorAction SilentlyContinue
162
+ ```
163
+
164
+ (回滚本身是安全的:三个文件会恢复成安装前的字节,验证哈希一致即可放心清理。)
165
+
166
+ ## 扩展
167
+
168
+ 想加第四个工具,照葫芦画瓢即可:在 `index.js` 里再写一个
169
+ `ctx.tools.register(defineTool({...}))`,其余不用动(本 bundle 只有一行 patch)。
170
+ 注意改完按「方式 A/B」刷新拷贝。
171
+
@@ -0,0 +1,11 @@
1
+ # @cxx42/dsh-tool-notes bundle patch.
2
+ #
3
+ # Self-activating host row: registers the notes tools on `ctx.tools`. Applied
4
+ # after dsh-base's rows so the `tools` registry already exists.
5
+
6
+ - insert:
7
+ - id: tool-notes
8
+ name: '@cxx42/dsh-tool-notes'
9
+ config:
10
+ vaultDir: !!js dshHomePath('notes')
11
+ maxSearchResults: 10
package/index.js ADDED
@@ -0,0 +1,279 @@
1
+ // dsh-tool-notes — a minimal but complete DSH tool plugin.
2
+ //
3
+ // Follows the official module-plugin contract (same shape as
4
+ // @deepseek-ai/dsh-tool-todo): export `name`, `inject`, `Config`, and `apply`.
5
+ // The bundle's cordis.patch.yml inserts a row naming this package, so Cordis
6
+ // loads THIS module as the plugin and registers the tools below on `ctx.tools`.
7
+ //
8
+ // The vault is a plain JSONL file (one JSON note per line) under a directory
9
+ // configured at load time (default: $DSH_HOME/notes). The tools read/write
10
+ // ONLY that directory — no shell access, no path escape.
11
+
12
+ import { mkdir, appendFile, readFile } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { randomUUID } from 'node:crypto';
15
+ import z from '@deepseek-ai/schemastery';
16
+ import { defineTool } from '@deepseek-ai/dsh-tools';
17
+
18
+ /** Cordis plugin identity (label only). */
19
+ export const name = '@cxx42/dsh-tool-notes';
20
+
21
+ /** Services that must exist before apply() runs. */
22
+ export const inject = ['tools'];
23
+
24
+ /** Schemastery configuration schema — matches the `config:` block in cordis.patch.yml. */
25
+ export const Config = z.object({
26
+ vaultDir: z.string().required(),
27
+ maxSearchResults: z.number().default(10),
28
+ });
29
+
30
+ // ── vault helpers ────────────────────────────────────────────────────────────
31
+
32
+ /** @param {string} vaultDir */
33
+ function vaultFile(vaultDir) {
34
+ return join(vaultDir, 'notes.jsonl');
35
+ }
36
+
37
+ /** Append one note object to the vault, creating the directory/file on demand. */
38
+ async function appendNote(vaultDir, note) {
39
+ await mkdir(vaultDir, { recursive: true });
40
+ await appendFile(vaultFile(vaultDir), `${JSON.stringify(note)}\n`, 'utf8');
41
+ return note;
42
+ }
43
+
44
+ /** Read all notes, newest first. Corrupt lines are skipped, never fatal. */
45
+ async function readNotes(vaultDir) {
46
+ let raw;
47
+ try {
48
+ raw = await readFile(vaultFile(vaultDir), 'utf8');
49
+ } catch (err) {
50
+ if (err.code === 'ENOENT') return [];
51
+ throw err;
52
+ }
53
+ const notes = [];
54
+ for (const line of raw.split('\n')) {
55
+ const trimmed = line.trim();
56
+ if (!trimmed) continue;
57
+ try {
58
+ notes.push(JSON.parse(trimmed));
59
+ } catch {
60
+ // skip a corrupt line rather than failing the whole read
61
+ }
62
+ }
63
+ return notes.reverse();
64
+ }
65
+
66
+ /** Escape regex metacharacters so a plain query is matched literally. */
67
+ function escapeRegex(input) {
68
+ return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
69
+ }
70
+
71
+ /** `$DSH_HOME/notes/notes.jsonl`, plus a human-friendly display form. */
72
+ function describeVault(vaultDir) {
73
+ return vaultFile(vaultDir);
74
+ }
75
+
76
+ // ── tools ────────────────────────────────────────────────────────────────────
77
+
78
+ function apply(ctx, config) {
79
+ const { vaultDir, maxSearchResults } = config;
80
+ const file = describeVault(vaultDir);
81
+
82
+ ctx.tools.register(defineTool({
83
+ name: 'note_add',
84
+ description:
85
+ 'Append a note to your personal vault (a JSONL file on disk). ' +
86
+ 'Use this to persist facts, preferences, decisions, and reference material ' +
87
+ 'the user may want later — across sessions and conversations. ' +
88
+ `Vault file: ${file}. Keep titles short and contents self-contained; ` +
89
+ 'tags group related notes for later search.',
90
+ parameters: {
91
+ title: {
92
+ type: 'string',
93
+ required: true,
94
+ description: 'Short title, <= 100 characters.',
95
+ },
96
+ content: {
97
+ type: 'string',
98
+ required: true,
99
+ description: 'The note body — markdown allowed, keep it accurate and useful.',
100
+ },
101
+ tags: {
102
+ type: 'array',
103
+
104
+ description: 'Optional lowercase tags for grouping, e.g. ["project-x", "idea"].',
105
+ items: { type: 'string' },
106
+ },
107
+ },
108
+ output: {
109
+ schema: {
110
+ type: 'object',
111
+ additionalProperties: false,
112
+ properties: {
113
+ id: { type: 'string', required: true },
114
+ title: { type: 'string', required: true },
115
+ tags: { type: 'array', required: true, items: { type: 'string' } },
116
+ vault: { type: 'string', required: true },
117
+ },
118
+ },
119
+ render: (_args, value) => [{
120
+ type: 'text',
121
+ text: `Saved note "${value.title}" (id ${value.id}) to ${value.vault}`,
122
+ }],
123
+ },
124
+ async execute(args) {
125
+ const note = {
126
+ id: randomUUID(),
127
+ ts: Date.now(),
128
+ title: String(args.title).slice(0, 100),
129
+ content: String(args.content),
130
+ tags: Array.isArray(args.tags) ? args.tags.map(String) : [],
131
+ };
132
+ await appendNote(vaultDir, note);
133
+ return {
134
+ id: note.id,
135
+ title: note.title,
136
+ tags: note.tags,
137
+ vault: file,
138
+ };
139
+ },
140
+ presentCall: (args) => ({
141
+ card: 'generic',
142
+ title: 'Add note',
143
+ kind: 'other',
144
+ rawInput: args,
145
+ }),
146
+ }));
147
+
148
+ ctx.tools.register(defineTool({
149
+ name: 'note_search',
150
+ description:
151
+ 'Full-text search over your personal note vault. Matches the query ' +
152
+ 'against title, body, and tags (case-insensitive substring). Returns the ' +
153
+ 'newest matches first. Use it to recall previously saved notes.',
154
+ parameters: {
155
+ query: {
156
+ type: 'string',
157
+ required: true,
158
+ description: 'Search text — plain words work best.',
159
+ },
160
+ limit: {
161
+ type: 'integer',
162
+
163
+ description: `Max results (default ${maxSearchResults}).`,
164
+ },
165
+ },
166
+ output: {
167
+ schema: {
168
+ type: 'object',
169
+ additionalProperties: false,
170
+ properties: {
171
+ matches: {
172
+ type: 'array',
173
+ required: true,
174
+ items: {
175
+ type: 'object',
176
+ additionalProperties: false,
177
+ properties: {
178
+ id: { type: 'string', required: true },
179
+ title: { type: 'string', required: true },
180
+ content: { type: 'string', required: true },
181
+ tags: { type: 'array', required: true, items: { type: 'string' } },
182
+ },
183
+ },
184
+ },
185
+ },
186
+ },
187
+ render: (_args, value) => value.matches.length
188
+ ? value.matches.map((m) => ({
189
+ type: 'text',
190
+ text: `• ${m.title}${m.tags.length ? ` [${m.tags.join(', ')}]` : ''}\n ${m.content}`,
191
+ }))
192
+ : [{ type: 'text', text: 'No notes matched.' }],
193
+ },
194
+ async execute(args) {
195
+ const limit = Math.max(1, Math.min(Number(args.limit) || maxSearchResults, 50));
196
+ const needle = new RegExp(escapeRegex(String(args.query)), 'i');
197
+ const notes = await readNotes(vaultDir);
198
+ const matches = notes
199
+ .filter((n) => needle.test(n.title) || needle.test(n.content) || needle.test((n.tags ?? []).join(' ')))
200
+ .slice(0, limit)
201
+ .map((n) => ({
202
+ id: n.id,
203
+ title: n.title,
204
+ content: n.content,
205
+ tags: n.tags ?? [],
206
+ }));
207
+ return { matches };
208
+ },
209
+ presentCall: (args) => ({
210
+ card: 'generic',
211
+ title: 'Search notes',
212
+ kind: 'other',
213
+ rawInput: args,
214
+ }),
215
+ }));
216
+
217
+ ctx.tools.register(defineTool({
218
+ name: 'note_list',
219
+ description:
220
+ 'List recent notes from your personal vault (newest first), optionally ' +
221
+ 'filtered by tag. Lightweight overview before deciding what to read or search.',
222
+ parameters: {
223
+ limit: {
224
+ type: 'integer',
225
+
226
+ description: `Max notes (default ${maxSearchResults}).`,
227
+ },
228
+ tag: {
229
+ type: 'string',
230
+
231
+ description: 'Only notes carrying this exact tag.',
232
+ },
233
+ },
234
+ output: {
235
+ schema: {
236
+ type: 'object',
237
+ additionalProperties: false,
238
+ properties: {
239
+ notes: {
240
+ type: 'array',
241
+ required: true,
242
+ items: {
243
+ type: 'object',
244
+ additionalProperties: false,
245
+ properties: {
246
+ id: { type: 'string', required: true },
247
+ title: { type: 'string', required: true },
248
+ tags: { type: 'array', required: true, items: { type: 'string' } },
249
+ },
250
+ },
251
+ },
252
+ },
253
+ },
254
+ render: (_args, value) => value.notes.length
255
+ ? value.notes.map((n) => ({
256
+ type: 'text',
257
+ text: `• ${n.title}${n.tags.length ? ` [${n.tags.join(', ')}]` : ''}`,
258
+ }))
259
+ : [{ type: 'text', text: 'The vault is empty.' }],
260
+ },
261
+ async execute(args) {
262
+ const limit = Math.max(1, Math.min(Number(args.limit) || maxSearchResults, 50));
263
+ const tag = args.tag === undefined ? null : String(args.tag);
264
+ const notes = await readNotes(vaultDir);
265
+ const filtered = tag === null ? notes : notes.filter((n) => (n.tags ?? []).includes(tag));
266
+ return {
267
+ notes: filtered.slice(0, limit).map((n) => ({ id: n.id, title: n.title, tags: n.tags ?? [] })),
268
+ };
269
+ },
270
+ presentCall: (args) => ({
271
+ card: 'generic',
272
+ title: 'List notes',
273
+ kind: 'other',
274
+ rawInput: args,
275
+ }),
276
+ }));
277
+ }
278
+
279
+ export { apply };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@cxx42/dsh-tool-notes",
3
+ "version": "1.0.0",
4
+ "description": "Personal note vault tools for DeepSeek Harness: add, search, and list notes as JSONL",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./package.json": "./package.json"
10
+ },
11
+ "files": [
12
+ "index.js",
13
+ "cordis.patch.yml",
14
+ "README.md"
15
+ ],
16
+ "license": "MIT",
17
+ "keywords": [
18
+ "dsh",
19
+ "deepseek-harness",
20
+ "plugin",
21
+ "tool",
22
+ "notes"
23
+ ],
24
+ "dependencies": {
25
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
26
+ "@deepseek-ai/schemastery": "^3.18.1"
27
+ },
28
+ "dsh": {
29
+ "bundle": {
30
+ "patch": "./cordis.patch.yml"
31
+ }
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }