project-node-memory 1.0.3 → 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 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:
@@ -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) out[name] = { path: info.path, registry: file }
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
- export function discoverLibraries(root, { depth = 2, budget = 300, limit = 60 } = {}) {
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.startsWith('.')) continue
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() || e.name.startsWith('.') || SKIP_DIR.has(e.name)) continue
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 = discoverLibraries(ws).find((r) => r.name === name)
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(), ...discoverLibraries(ws).map((r) => r.name)]
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 = discoverLibraries(ws)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-node-memory",
3
- "version": "1.0.3",
3
+ "version": "1.0.6",
4
4
  "description": "Project node knowledge base for DSH agents: markdown-as-database, stable TIDs, transclusion projection, structure self-check.",
5
5
  "type": "module",
6
6
  "license": "MIT",