project-node-memory 1.0.4 → 1.0.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 +14 -0
- package/README_EN.md +9 -1
- package/lib/engine/registry.js +60 -9
- package/lib/index.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,9 +68,17 @@ dsh plugin add ./project-node-memory-<版本>.tgz
|
|
|
68
68
|
| `workspace` | 会话工作区 → 进程 cwd | 覆盖「工作区」,库发现与路径白名单都以它为界 |
|
|
69
69
|
| `libraries` | 无 | `名字: 目录`,显式登记工作区之外的库(目录下需有 `.nodekb/`) |
|
|
70
70
|
| `registries` | 工作区 `.nodekb/registry.json` + 用户全局注册表 | 额外的 `kb_registry.json`(与 Python 版同格式) |
|
|
71
|
+
| `exclude` | 见下 | 库发现时忽略的目录名,支持 `*` 通配;`!pattern` 强制放行 |
|
|
71
72
|
| `prompt.enabled` | `true` | 是否注册系统提示词段落 |
|
|
72
73
|
| `skill.enabled` | `true` | 是否把随包 Skill 注册进宿主 `skills` 服务 |
|
|
73
74
|
|
|
75
|
+
`exclude` 默认忽略这些名字:`tmp`、`tmp_*`、`temp*`、`.tmp*`、`*_probe`、`nodekb_lab`、`__pycache__`、`_release_assets`、`venv`。临时目录、探针目录里的 `.nodekb` 通常是实验残渣,列进选择器只会碍事。要放行某个目录写 `"!tmp_keep"`;要额外忽略写 `"scratch"`(与默认名单叠加)。
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
config:
|
|
79
|
+
exclude: ['tmp_*', '!tmp_keep', 'scratch']
|
|
80
|
+
```
|
|
81
|
+
|
|
74
82
|
## 面板
|
|
75
83
|
|
|
76
84
|
侧边栏多一个图标(名字「项目节点知识库」),点开是中栏三列:
|
|
@@ -146,6 +154,12 @@ dsh plugin add ./project-node-memory-<版本>.tgz
|
|
|
146
154
|
|
|
147
155
|
读取顺序:`config.libraries` → `config.registries` 指定的文件 → `<ws>/.nodekb/registry.json` → `<ws>/kb_registry.json` → 用户目录的全局注册表(`~/.dsh/nodekb/registry.json`)。注册表里登记的名字优先级高于自动发现的名字,路径重复的以先出现的为准。
|
|
148
156
|
|
|
157
|
+
给条目加 `"hidden": true` 后,它**只作为别名**存在:仍然可以用 `kb="my-notes"` 调用(老文档里写死的库名不会失效),但不会出现在面板的知识库下拉里。
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{ "libraries": { "my-notes": { "path": "D:\\notes", "hidden": true } } }
|
|
161
|
+
```
|
|
162
|
+
|
|
149
163
|
## 语法
|
|
150
164
|
|
|
151
165
|
```md
|
package/README_EN.md
CHANGED
|
@@ -65,9 +65,17 @@ All optional, under `config:` on the plugin's row in `cordis.patch.yml`:
|
|
|
65
65
|
| `workspace` | session workspace → process cwd | Overrides "the workspace"; library discovery and the path whitelist are scoped to it |
|
|
66
66
|
| `libraries` | none | `name: directory` pairs registering libraries outside the workspace (each needs a `.nodekb/`) |
|
|
67
67
|
| `registries` | workspace `.nodekb/registry.json` + user-global registry | Extra `kb_registry.json` files (same format as the Python build) |
|
|
68
|
+
| `exclude` | see below | Directory names skipped during discovery (`*` wildcard; `!pattern` forces inclusion) |
|
|
68
69
|
| `prompt.enabled` | `true` | Register the system-prompt section |
|
|
69
70
|
| `skill.enabled` | `true` | Register the bundled skill with the host `skills` service |
|
|
70
71
|
|
|
72
|
+
`exclude` defaults to `tmp`, `tmp_*`, `temp*`, `.tmp*`, `*_probe`, `nodekb_lab`, `__pycache__`, `_release_assets`, `venv`. A `.nodekb` inside a scratch or probe directory is usually leftover from an experiment; listing it only clutters the selector. Use `"!tmp_keep"` to force one back in, and plain names to add more:
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
config:
|
|
76
|
+
exclude: ['tmp_*', '!tmp_keep', 'scratch']
|
|
77
|
+
```
|
|
78
|
+
|
|
71
79
|
## Panel
|
|
72
80
|
|
|
73
81
|
The sidebar gains an icon; opening it gives a three-column center panel (modeled on nodekb's MCP WebUI):
|
|
@@ -112,7 +120,7 @@ The right column renders at most 10,000 characters per view (with a truncation n
|
|
|
112
120
|
|
|
113
121
|
It skips `node_modules` / `dist` / `build` / `out` / `target` / `coverage` / dot-directories and caps `readdir` calls, so the panel can afford to run it on every load. That is what "the knowledge base travels with the project" means: point at a project root and the `.nodekb/` directories inside skills surface on their own.
|
|
114
122
|
|
|
115
|
-
Registries use the same format as the Python build's `kb_registry.json`; by default the plugin reads `.nodekb/registry.json` in the workspace and the global registry in the user directory. Libraries can also be declared inline:
|
|
123
|
+
Registries use the same format as the Python build's `kb_registry.json`; by default the plugin reads `.nodekb/registry.json` in the workspace and the global registry in the user directory. Add `"hidden": true` to an entry to keep it as an **alias only** — `kb="my-notes"` still resolves (old docs keep working) but the entry never shows up in the panel's library picker. Libraries can also be declared inline:
|
|
116
124
|
|
|
117
125
|
```yaml
|
|
118
126
|
- insert:
|
package/lib/engine/registry.js
CHANGED
|
@@ -51,7 +51,9 @@ export function readRegistryFile(file) {
|
|
|
51
51
|
if (!libs || typeof libs !== 'object') return {}
|
|
52
52
|
const out = {}
|
|
53
53
|
for (const [name, info] of Object.entries(libs)) {
|
|
54
|
-
if (info && typeof info.path === 'string' && info.path)
|
|
54
|
+
if (info && typeof info.path === 'string' && info.path) {
|
|
55
|
+
out[name] = { path: info.path, registry: file, hidden: info.hidden === true }
|
|
56
|
+
}
|
|
55
57
|
}
|
|
56
58
|
return out
|
|
57
59
|
} catch {
|
|
@@ -94,7 +96,51 @@ function safeReaddir(dir) {
|
|
|
94
96
|
*/
|
|
95
97
|
const SKIP_DIR = new Set(['node_modules', 'dist', 'build', 'out', 'target', 'coverage', '.git'])
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
/**
|
|
100
|
+
* 默认忽略的目录名(支持 * 通配、大小写不敏感)。这些目录里的 .nodekb 一律不算库:
|
|
101
|
+
* 临时/探针/构建产物目录里的库通常是实验残渣,列出来只会污染选择器。
|
|
102
|
+
* 可用 `--lib` 显式指定、或在插件配置里写 `"exclude": ["我的目录"]` 覆盖;
|
|
103
|
+
* 写 `"!tmp_*"` 之类的反向模式可重新放行(见 isExcluded)。
|
|
104
|
+
*/
|
|
105
|
+
export const DEFAULT_EXCLUDE = [
|
|
106
|
+
'tmp',
|
|
107
|
+
'tmp_*',
|
|
108
|
+
'temp',
|
|
109
|
+
'temp_*',
|
|
110
|
+
'.tmp*',
|
|
111
|
+
'*_probe',
|
|
112
|
+
'*_probe_*',
|
|
113
|
+
'nodekb_lab',
|
|
114
|
+
'__pycache__',
|
|
115
|
+
'_release_assets',
|
|
116
|
+
'venv',
|
|
117
|
+
'.venv',
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
function globMatch(str, pattern) {
|
|
121
|
+
const re = new RegExp(
|
|
122
|
+
'^' +
|
|
123
|
+
pattern
|
|
124
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
125
|
+
.replace(/\*/g, '.*')
|
|
126
|
+
.replace(/\?/g, '.') +
|
|
127
|
+
'$',
|
|
128
|
+
)
|
|
129
|
+
return re.test(str)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 目录名是否被忽略。exclude 里的 `!xxx` 表示强制放行(优先于默认名单)。 */
|
|
133
|
+
export function isExcluded(name, { exclude } = {}) {
|
|
134
|
+
const n = String(name).toLowerCase()
|
|
135
|
+
const extra = Array.isArray(exclude) ? exclude.map((p) => String(p)) : []
|
|
136
|
+
const allow = extra.filter((p) => p.startsWith('!')).map((p) => p.slice(1).toLowerCase())
|
|
137
|
+
if (allow.some((p) => globMatch(n, p))) return false
|
|
138
|
+
const patterns = [...DEFAULT_EXCLUDE, ...extra.filter((p) => !p.startsWith('!'))]
|
|
139
|
+
return patterns.some((p) => globMatch(n, p.toLowerCase()))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function discoverLibraries(root, { depth = 2, budget = 300, limit = 60, exclude } = {}) {
|
|
143
|
+
const skipName = (name) => name.startsWith('.') || SKIP_DIR.has(name) || isExcluded(name, { exclude })
|
|
98
144
|
const ws = path.resolve(root || '.')
|
|
99
145
|
const out = []
|
|
100
146
|
const seen = new Set()
|
|
@@ -111,7 +157,7 @@ export function discoverLibraries(root, { depth = 2, budget = 300, limit = 60 }
|
|
|
111
157
|
// 技能自带的知识库是约定位置,单独直查(不进 BFS,因为 .dsh 是点目录)
|
|
112
158
|
const scanSkills = (base, prefix) => {
|
|
113
159
|
for (const e of safeReaddir(path.join(base, '.dsh', 'skills'))) {
|
|
114
|
-
if (!e.isDirectory() || e.name
|
|
160
|
+
if (!e.isDirectory() || skipName(e.name)) continue
|
|
115
161
|
push(path.join(base, '.dsh', 'skills', e.name), (prefix ? prefix + '/' : '') + '.dsh/skills/' + e.name, 'discovered')
|
|
116
162
|
if (out.length >= limit) return
|
|
117
163
|
}
|
|
@@ -131,7 +177,7 @@ export function discoverLibraries(root, { depth = 2, budget = 300, limit = 60 }
|
|
|
131
177
|
}
|
|
132
178
|
for (const e of entries) {
|
|
133
179
|
if (out.length >= limit || spent >= budget) break
|
|
134
|
-
if (!e.isDirectory() ||
|
|
180
|
+
if (!e.isDirectory() || skipName(e.name)) continue
|
|
135
181
|
const rel = cur.rel ? cur.rel + '/' + e.name : e.name
|
|
136
182
|
const dir = path.join(cur.dir, e.name)
|
|
137
183
|
push(dir, rel, 'discovered')
|
|
@@ -148,6 +194,8 @@ export function discoverLibraries(root, { depth = 2, budget = 300, limit = 60 }
|
|
|
148
194
|
*/
|
|
149
195
|
export function resolveLibrary({ kb, lib, workspace, config } = {}) {
|
|
150
196
|
const ws = path.resolve(workspace || process.cwd())
|
|
197
|
+
const exclude = config && config.exclude
|
|
198
|
+
const discover = () => discoverLibraries(ws, { exclude })
|
|
151
199
|
const registries = loadRegistries(
|
|
152
200
|
config && config.registries ? config.registries : defaultRegistryPaths(ws),
|
|
153
201
|
config && config.libraries,
|
|
@@ -177,7 +225,7 @@ export function resolveLibrary({ kb, lib, workspace, config } = {}) {
|
|
|
177
225
|
if (!isDir(nodekb)) throw new Error(`库目录不存在: ${nodekb}(注册表 ${name} 指向 ${entry.path})`)
|
|
178
226
|
return { name, base: entry.path, nodekb, origin: entry.registry }
|
|
179
227
|
}
|
|
180
|
-
const discovered =
|
|
228
|
+
const discovered = discover().find((r) => r.name === name)
|
|
181
229
|
if (discovered) {
|
|
182
230
|
return { name, base: path.dirname(discovered.nodekb), nodekb: discovered.nodekb, origin: discovered.origin }
|
|
183
231
|
}
|
|
@@ -185,14 +233,14 @@ export function resolveLibrary({ kb, lib, workspace, config } = {}) {
|
|
|
185
233
|
const wsLib = workspaceBase(ws)
|
|
186
234
|
if (wsLib) return wsLib
|
|
187
235
|
}
|
|
188
|
-
const names = [...registries.keys(), ...
|
|
236
|
+
const names = [...registries.keys(), ...discover().map((r) => r.name)]
|
|
189
237
|
throw new Error(`未注册的库: ${name}(可用: ${names.join(', ') || '无'};或用 lib 参数给绝对路径)`)
|
|
190
238
|
}
|
|
191
239
|
|
|
192
240
|
// 3) 无参:工作区自带 → 工作区里唯一发现 → 唯一的注册表库
|
|
193
241
|
const wsLib = workspaceBase(ws)
|
|
194
242
|
if (wsLib) return wsLib
|
|
195
|
-
const found =
|
|
243
|
+
const found = discover()
|
|
196
244
|
if (found.length === 1) {
|
|
197
245
|
return { name: found[0].name, base: path.dirname(found[0].nodekb), nodekb: found[0].nodekb, origin: found[0].origin }
|
|
198
246
|
}
|
|
@@ -222,8 +270,10 @@ export function resolveLibrary({ kb, lib, workspace, config } = {}) {
|
|
|
222
270
|
/** 列出可用库(不解析内容,避免一次拖动整库)
|
|
223
271
|
* scope='workspace' 时只回「真的在当前工作区里」的库(发现的 + 注册表里路径落在工作区内的),
|
|
224
272
|
* 面板用它做「知识库选择只显示当前工作区的库」;全局注册表项不再混进来。
|
|
273
|
+
* 注册项可以写 `hidden: true`:仍然能按名字解析(兼容老文档里的 kb="filmmeasure"),
|
|
274
|
+
* 但不进任何列表——面板里不显示,相当于「只保留别名」。
|
|
225
275
|
*/
|
|
226
|
-
export function listLibraries({ workspace, config, scope } = {}) {
|
|
276
|
+
export function listLibraries({ workspace, config, scope, includeHidden = false } = {}) {
|
|
227
277
|
const ws = path.resolve(workspace || process.cwd())
|
|
228
278
|
const registries = loadRegistries(
|
|
229
279
|
config && config.registries ? config.registries : defaultRegistryPaths(ws),
|
|
@@ -231,11 +281,12 @@ export function listLibraries({ workspace, config, scope } = {}) {
|
|
|
231
281
|
)
|
|
232
282
|
const rows = []
|
|
233
283
|
const seen = new Set()
|
|
234
|
-
for (const r of discoverLibraries(ws)) {
|
|
284
|
+
for (const r of discoverLibraries(ws, { exclude: config && config.exclude })) {
|
|
235
285
|
rows.push({ name: r.name, path: path.dirname(r.nodekb), exists: true, origin: r.origin })
|
|
236
286
|
seen.add(r.nodekb)
|
|
237
287
|
}
|
|
238
288
|
for (const [name, entry] of registries) {
|
|
289
|
+
if (entry.hidden && !includeHidden) continue
|
|
239
290
|
const nodekb = path.join(entry.path, '.nodekb')
|
|
240
291
|
if (seen.has(nodekb)) continue
|
|
241
292
|
// 工作区视图只保留「路径位于工作区内」的注册项,否则一个全局注册表会污染所有工作区
|
package/lib/index.js
CHANGED
|
@@ -22,6 +22,8 @@ export const inject = ['tools', 'webServer']
|
|
|
22
22
|
* - libraries: { 名字: 技能目录绝对路径 } —— 内联登记库
|
|
23
23
|
* - registries: [注册表 JSON 路径] —— 与 Python 版 kb_registry.json 同格式
|
|
24
24
|
* - workspace: 工作区路径(默认取会话工作区,取不到用进程 cwd)
|
|
25
|
+
* - exclude: ['tmp_*', '!tmp_keep'] —— 库发现时额外忽略的目录名(支持 * 通配;
|
|
26
|
+
* `!` 前缀表示强制放行,可覆盖默认名单)。默认已忽略 tmp/temp/*_probe/nodekb_lab 等。
|
|
25
27
|
* - prompt: { enabled?: false, text?: string } —— 系统提示词里的那段约定(默认开)
|
|
26
28
|
*/
|
|
27
29
|
export function apply(ctx, config) {
|
package/package.json
CHANGED