@paircode/tool-project-info 1.0.1 → 1.1.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/index.js CHANGED
@@ -1,147 +1,389 @@
1
1
  // ═══════════════════════════════════════════════════════════════
2
- // tool-project-info — 项目知识库(project_info_write/read/list/search/delete/explore
2
+ // tool-project-info — 项目知识库(单工具 project_info(op=write/read/list/tree/search/delete/explore);2026-09 由 7 工具合并)
3
3
  //
4
- // 生成来源(2026-08-16):内置 Go 工具组磁盘外置插件(tool_plugin_gen.go
5
- // 自动生成,schema 完整外置拷贝)。api 声明在插件,execute 调 ctx.binary 复用本插件目录 bin/ 下的独立二进制(源码 plugins-src/plugins/<name>/,改实现重编译即更换)。
6
- // 工具清单:project_info_write、project_info_read、project_info_list、project_info_tree、project_info_search、project_info_delete、project_info_explore
4
+ // 迁移(2026-08-22 Round2):binary 形态JS 原生(对齐 tool-core 模式)。
5
+ // execute 调 ctx.binary.exec 复用插件目录 bin/ 下独立二进制(已归档
6
+ // bin/legacy-plugin-bins/),现实现完全在插件内(ctx.fs 读写 .pair/project-info/),
7
+ // 不再依赖 ctx.binary。行为复刻 internal/agent/projectinfo.go(树形路径分级、
8
+ // notes/ 前缀镜像、渐进式披露、多项目 project 路由)。
9
+ // 工具清单:project_info(op=write/read/list/tree/search/delete/explore)
7
10
  // ═══════════════════════════════════════════════════════════════
11
+ // ★ 2026-08-29(候选 A 创造需求):改用 mini Node API——require('path')
12
+ // 统一路径拼接(手工 d+'/'+name 在 Windows 分隔符下有隐患)。
13
+ const path = require('path')
8
14
  const tools = [
9
15
  {
10
- "name": "project_info_write",
11
- "description": "写入/更新项目知识库的一篇(.pair/project-info/\u003c路径\u003e.md)——记录项目架构/模块职责/数据流/设计决策等结构化理解,跨会话复用、你和用户都能看。★树形路径:顶层分支 目标/架构/实现/关键点/设计思想,根条目用 概览(如 架构/模块-agent / 设计思想/决策-渲染架构);兼容参考项目 notes/ 前缀路径(自动映射分支+镜像 .agents/notes/)。",
12
- "usageGuide": "写入/更新项目知识库条目,跨会话复用。★知识库是树:顶层分支 = 目标/架构/实现/关键点/设计思想(根为 概览)——路径带分支前缀(如 架构/模块-agent / 设计思想/决策-渲染架构)。也可用参考项目风格路径 notes/implemented/architecture/x(自动归入树分支 架构/x 并镜像 .agents/notes/)。读完关键文件后立即写入,积累项目的结构化理解。比记在脑子里可靠(持久化+跨会话可见)。多项目工作区可用 project 参数指定目标项目。",
16
+ "name": "project_info",
17
+ "description": "项目知识库(.pair/project-info/ 树形文档)统一入口。op=write 写入/更新条目(path+content);op=read 读某篇全文(path);op=list 列出全部条目的总览;op=tree 返回完整树形结构;op=search 按关键词搜索(query);op=delete 删除某篇(path);op=explore 返回项目目录结构概览(构建知识库的起点)。",
18
+ "usageGuide": "项目知识库操作(渐进式披露):先 list/tree 看有哪些文档,search 查关键词,read 读全文;explore 扫描项目结构起步;write 记录结构化理解(顶层分支:目标/架构/实现/关键点/设计思想,根为 概览;path 带分支前缀如 架构/模块-agent,兼容 notes/ 前缀镜像 .agents/notes/);delete 清理过时条目。比记在脑子里可靠(持久化+跨会话可见)。多项目工作区用 project 指定项目。",
13
19
  "parameters": {
14
20
  "properties": {
15
21
  "content": {
16
- "description": "Markdown 正文(首行用 # 标题)",
22
+ "description": "write 用:Markdown 正文(首行用 # 标题)",
17
23
  "type": "string"
18
24
  },
19
- "path": {
20
- "description": "条目路径(中文,带顶层分支前缀:目标/架构/实现/关键点/设计思想,如 架构/模块-agent),不含 .md;用 / 嵌套为细节篇",
25
+ "op": {
26
+ "description": "操作:write 写入/read 读取/list 总览/tree 树形/search 搜索/delete 删除/explore 项目结构扫描",
27
+ "enum": [
28
+ "write",
29
+ "read",
30
+ "list",
31
+ "tree",
32
+ "search",
33
+ "delete",
34
+ "explore"
35
+ ],
21
36
  "type": "string"
22
37
  },
23
- "project": {
24
- "description": "可选:目标项目(工作区项目目录名如 wb-ui,或相对主项目的路径/绝对路径)。省略 = 主项目。多项目工作区:gou-ide、wb-ui、ref 等。",
25
- "type": "string"
26
- }
27
- },
28
- "required": [
29
- "path",
30
- "content"
31
- ],
32
- "type": "object"
33
- }
34
- },
35
- {
36
- "name": "project_info_read",
37
- "description": "读取知识库某篇的全文(按路径,如 概览 / 模块-agent)。渐进式披露的细节层。",
38
- "usageGuide": "读取知识库某篇全文。渐进式披露:先 project_info_list 看总览,再用此工具读具体细则。比翻目录更方便(自动解析路径+内容格式化)。",
39
- "parameters": {
40
- "properties": {
41
38
  "path": {
42
- "description": "条目路径,不含 .md",
39
+ "description": "write/read/delete 用:条目路径(中文,带顶层分支前缀:目标/架构/实现/关键点/设计思想,如 架构/模块-agent),不含 .md;用 / 嵌套为细节篇",
43
40
  "type": "string"
44
41
  },
45
- "project": {
46
- "description": "可选:目标项目(工作区项目目录名如 wb-ui,或相对主项目的路径/绝对路径)。省略 = 主项目。多项目工作区:gou-ide、wb-ui、ref 等。",
47
- "type": "string"
48
- }
49
- },
50
- "required": [
51
- "path"
52
- ],
53
- "type": "object"
54
- },
55
- "readOnly": true
56
- },
57
- {
58
- "name": "project_info_list",
59
- "description": "列出知识库所有条目的【总览】(路径 + 标题 + 分级)。渐进式披露的总览层。",
60
- "usageGuide": "列出知识库所有条目的总览(路径+标题+分级)。新项目先调此工具查看已有哪些文档,避免重复写入。",
61
- "parameters": {
62
- "properties": {
63
- "project": {
64
- "description": "可选:目标项目(工作区项目目录名如 wb-ui,或相对主项目的路径/绝对路径)。省略 = 主项目。多项目工作区:gou-ide、wb-ui、ref 等。",
65
- "type": "string"
66
- }
67
- },
68
- "type": "object"
69
- },
70
- "readOnly": true
71
- },
72
- {
73
- "name": "project_info_tree",
74
- "description": "返回知识库完整树形结构(缩进树:目标/架构/实现/关键点/设计思想 分支 + 条目)。人可读的树形导航。",
75
- "usageGuide": "查看知识库完整树形结构(分支/子类/条目缩进树)。比 project_info_list 更直观:先看树定位条目,再 project_info_read 读全文。",
76
- "parameters": {
77
- "properties": {
78
- "project": {
79
- "description": "可选:目标项目(工作区项目目录名如 wb-ui,或相对主项目的路径/绝对路径)。省略 = 主项目。多项目工作区:gou-ide、wb-ui、ref 等。",
80
- "type": "string"
81
- }
82
- },
83
- "type": "object"
84
- },
85
- "readOnly": true
86
- },
87
- {
88
- "name": "project_info_search",
89
- "description": "按关键词搜索知识库(匹配路径/标题/正文),返回命中条目。",
90
- "usageGuide": "按关键词搜索知识库(匹配路径/标题/正文)。想查某个模块/概念是否已有文档时优先用此工具。",
91
- "parameters": {
92
- "properties": {
93
42
  "project": {
94
43
  "description": "可选:目标项目(工作区项目目录名如 wb-ui,或相对主项目的路径/绝对路径)。省略 = 主项目。多项目工作区:gou-ide、wb-ui、ref 等。",
95
44
  "type": "string"
96
45
  },
97
46
  "query": {
98
- "description": "关键词",
99
- "type": "string"
100
- }
101
- },
102
- "required": [
103
- "query"
104
- ],
105
- "type": "object"
106
- },
107
- "readOnly": true
108
- },
109
- {
110
- "name": "project_info_delete",
111
- "description": "删除知识库某篇(按路径)。",
112
- "usageGuide": "删除知识库某篇(按路径)。知识库条目过时/错误时用此工具清理。删除前建议先 project_info_read 确认。",
113
- "parameters": {
114
- "properties": {
115
- "path": {
116
- "description": "条目路径,不含 .md",
117
- "type": "string"
118
- },
119
- "project": {
120
- "description": "可选:目标项目(工作区项目目录名如 wb-ui,或相对主项目的路径/绝对路径)。省略 = 主项目。多项目工作区:gou-ide、wb-ui、ref 等。",
47
+ "description": "search 用:关键词",
121
48
  "type": "string"
122
49
  }
123
50
  },
124
51
  "required": [
125
- "path"
52
+ "op"
126
53
  ],
127
54
  "type": "object"
128
55
  }
129
56
  },
130
- {
131
- "name": "project_info_explore",
132
- "description": "返回项目目录结构概览(根目录关键文件、顶层目录及文件数)——构建知识库的起点;据此用 read_file 读关键文件分析,再 project_info_write 写入 概览/模块-*/决策-*。",
133
- "usageGuide": "扫描项目目录结构概览——构建知识库的起点。新项目首次接触时先调此工具了解项目全貌,再用 read_file 读关键文件,最后 project_info_write 写入结构化理解。",
134
- "parameters": {
135
- "properties": {},
136
- "type": "object"
137
- },
138
- "readOnly": true
139
- }
57
+ // (project_info_read / project_info_list 已并入单工具 project_info——op=read/list)
58
+ // (project_info_tree / project_info_search 已并入单工具 project_info——op=tree/search)
59
+ // (project_info_delete / project_info_explore 已并入单工具 project_info——op=delete/explore)
140
60
  ];
141
61
 
62
+
63
+ // ─── JS 原生化实现(ctx.fs) ────────────────────
64
+
65
+ // infoDirFromArgs 解析 project 参数 → 目标项目知识库目录(缺省 = 主项目)。
66
+ // 多项目工作区中知识库按项目隔离(.pair/project-info/ 在各项目根下)。
67
+ function infoDir(ctx, args) {
68
+ const project = args && args.project
69
+ if (project && !/^[a-zA-Z]:[\/]/.test(project) && !project.startsWith('/')) {
70
+ return '../' + String(project).replace(/[\/]+$/, '') + '/.pair/project-info'
71
+ }
72
+ return '.pair/project-info'
73
+ }
74
+
75
+ // safeInfoPath 规范化条目路径:去 .md、清理、禁路径穿越(..、绝对路径),允许 / 嵌套。
76
+ function cleanSegs(p) {
77
+ const out = []
78
+ for (const s of String(p).split('/')) {
79
+ if (s === '' || s === '.') continue
80
+ if (s === '..') { out.pop(); continue }
81
+ out.push(s)
82
+ }
83
+ return out.join('/')
84
+ }
85
+ function safeInfoPath(p) {
86
+ let s = String(p == null ? '' : p).replace(/\.md\s*$/, '').trim()
87
+ s = s.replace(/\\/g, '/')
88
+ return cleanSegs('/' + s).replace(/^\/+|\/+$/g, '')
89
+ }
90
+
91
+ const infoBranches = ['目标', '架构', '实现', '关键点', '设计思想']
92
+ function isInfoBranch(head) {
93
+ return infoBranches.indexOf(head) >= 0
94
+ }
95
+
96
+ // notesToBranchRel 把决策树路径映射到知识库树分支路径(复刻 Go 实现)。
97
+ function notesToBranchRel(n) {
98
+ n = String(n).replace(/^notes\//, '').replace(/^\//, '')
99
+ if (n === '') return null
100
+ const segs = n.split('/')
101
+ const leaf = segs[segs.length - 1]
102
+ let branch
103
+ if (segs.length >= 2 && segs[0] === 'implemented' && segs[1] === 'architecture') branch = '架构'
104
+ else if (segs.length >= 2 && segs[0] === 'implemented' && (segs[1] === 'decision' || segs[1] === 'decisions')) branch = '设计思想'
105
+ else if (segs.length >= 2 && segs[0] === 'implemented' && segs[1] === 'feature') branch = '实现'
106
+ else if (segs.length >= 2 && segs[0] === 'implemented') branch = '关键点'
107
+ else if (segs[0] === 'decision' || segs[0] === 'decisions') branch = '设计思想'
108
+ else if (segs.length >= 2 && segs[0] === 'inbox') branch = '实现'
109
+ else branch = '实现'
110
+ return path.join(branch, leaf)
111
+ }
112
+
113
+ // firstHeading 取 Markdown 首行 # 标题。
114
+ function firstHeading(md, fallback) {
115
+ for (const ln of String(md).split('\n')) {
116
+ const s = ln.trim()
117
+ if (s.startsWith('# ')) return s.slice(2).trim()
118
+ }
119
+ return fallback
120
+ }
121
+
122
+ // infoLevel 按路径分级:overview = 根条目;module = 1 层;detail = 2+ 层。
123
+ function infoLevel(rel) {
124
+ const low = String(rel).toLowerCase()
125
+ if (low === 'overview' || rel === '概览' || rel === '项目概览') return 'overview'
126
+ if (String(rel).split('/').length >= 3) return 'detail'
127
+ return 'module'
128
+ }
129
+
130
+ // walkMd 递归扫描目录下的 .md 条目(返回 {rel, title, content} 列表)。
131
+ function walkMd(ctx, base, prefix) {
132
+ let names = []
133
+ try { names = ctx.fs.readdir(base) } catch (e) { return [] }
134
+ const out = []
135
+ for (const n of names) {
136
+ const full = path.join(base, n)
137
+ let st = null
138
+ try { st = ctx.fs.stat(full) } catch (e) { continue }
139
+ if (st.isDir) {
140
+ out.push(...walkMd(ctx, full, path.join(prefix, n)))
141
+ continue
142
+ }
143
+ if (!n.endsWith('.md')) continue
144
+ let content = ''
145
+ try { content = ctx.fs.readFile(full) } catch (e) { continue }
146
+ const rel = (prefix ? prefix + '/' : '') + n.slice(0, -3)
147
+ out.push({ rel, title: firstHeading(content, rel), level: infoLevel(rel), content })
148
+ }
149
+ return out
150
+ }
151
+
152
+ // scanInfoEntries 递归扫描知识库目录;附加源 .agents/notes/ 并入(路径前缀 notes/)。
153
+ function scanInfoEntries(ctx, dir) {
154
+ const out = walkMd(ctx, dir, '')
155
+ // 项目根:dir 为 <root>/.pair/project-info → root = 去掉后缀
156
+ const root = String(dir).replace(/\/\.pair\/project-info$/, '')
157
+ const notes = root + '/.agents/notes'
158
+ if (notes !== dir) {
159
+ for (const e of walkMd(ctx, notes, 'notes')) {
160
+ const br = notesToBranchRel(e.rel)
161
+ if (!br) continue
162
+ // 树中已有镜像副本 → 跳过,避免重复
163
+ if (ctx.fs.exists(path.join(dir, br + '.md'))) continue
164
+ out.push({ rel: e.rel, title: e.title, level: infoLevel(e.rel), content: e.content })
165
+ }
166
+ }
167
+ out.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0))
168
+ return out
169
+ }
170
+
171
+ // partsOf 取条目路径末段(文件名段)。
172
+ function partsOf(rel) {
173
+ const i = String(rel).lastIndexOf('/')
174
+ return i >= 0 ? rel.slice(i + 1) : rel
175
+ }
176
+
177
+ // infoTree 构建知识库条目树(分支=目录,叶子=条目),返回缩进树文本。
178
+ function infoTree(entries, showLevel) {
179
+ const root = { children: {} }
180
+ for (const e of entries) {
181
+ const parts = e.rel.split('/')
182
+ let cur = root
183
+ for (let i = 0; i < parts.length - 1; i++) {
184
+ const seg = parts[i]
185
+ if (!cur.children[seg]) cur.children[seg] = { name: seg, children: {} }
186
+ cur = cur.children[seg]
187
+ }
188
+ const leaf = parts[parts.length - 1]
189
+ if (!cur.children[leaf]) cur.children[leaf] = { name: leaf, children: {} }
190
+ cur.children[leaf].entry = e
191
+ }
192
+ let b = ''
193
+ const walk = (n, prefix) => {
194
+ const keys = Object.keys(n.children).sort()
195
+ for (let i = 0; i < keys.length; i++) {
196
+ const ch = n.children[keys[i]]
197
+ const last = i === keys.length - 1
198
+ let conn = '├── ', nextPrefix = prefix + '│ '
199
+ if (last) { conn = '└── '; nextPrefix = prefix + ' ' }
200
+ if (ch.entry) {
201
+ let mark = ''
202
+ if (showLevel) mark = ' [' + ch.entry.level + ']'
203
+ let title = ch.entry.title
204
+ const leaf = partsOf(ch.entry.rel)
205
+ if (leaf && leaf !== title) title += '(' + leaf + ')'
206
+ b += prefix + conn + title + mark + '\n'
207
+ } else {
208
+ b += prefix + conn + keys[i] + '/\n'
209
+ }
210
+ walk(ch, nextPrefix)
211
+ }
212
+ }
213
+ walk(root, '')
214
+ return b
215
+ }
216
+
217
+ // infoKeyFile 是否为「根目录关键文件」。
218
+ function infoKeyFile(n) {
219
+ const low = String(n).toLowerCase()
220
+ if (low.startsWith('readme') || low.startsWith('makefile')) return true
221
+ return ['go.mod', 'package.json', 'cargo.toml', 'pyproject.toml', 'pom.xml',
222
+ 'main.go', 'agents.md', 'claude.md', 'go.sum', 'tsconfig.json'].indexOf(low) >= 0
223
+ }
224
+
225
+ // isSkipDir 常见忽略目录(explore/文件树统计用)。
226
+ // ★ 与内核搜索忽略集(internal/agent/search.go defaultSkipDirs/rootLevelSkipDirs)
227
+ // 保持同源:依赖库/构建产物/VCS + IDE 运行数据目录(_temp/logs/bin/release/…)。
228
+ const skipDirs = new Set([
229
+ '.git', '.svn', '.hg', '.idea', '.vscode', '.vs',
230
+ 'node_modules', 'bower_components', 'jspm_packages', 'vendor', 'pods',
231
+ '.pnpm-store', '.yarn', '.dart_tool', '.bundle',
232
+ 'venv', '.venv', '__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache', '.tox',
233
+ 'dist', 'build', 'out', 'target', '.next', '.nuxt', '.svelte-kit', '.output', '.angular',
234
+ '.gradle', '.cache', '.turbo', '.parcel-cache', '.eslintcache', 'coverage', '.nyc_output', '.terraform',
235
+ '.pair', '源码备份', '.agents',
236
+ '.agent-teams', '.chrome-test', '.verify-tmp', '_temp', '_desktop-archive',
237
+ 'tmp', 'logs', 'gocache', 'screenshots', 'release', 'bin', 'obj', 'gomod', 'gomodcache',
238
+ ])
239
+ function isSkipDir(n) {
240
+ return skipDirs.has(String(n).toLowerCase())
241
+ }
242
+
243
+ // countDirFiles 数目录下文件(递归,跳过依赖/产物目录,上限 2000 防卡)。
244
+ function countDirFiles(ctx, dir) {
245
+ let n = 0
246
+ const walk = (d) => {
247
+ let names = []
248
+ try { names = ctx.fs.readdir(d) } catch (e) { return }
249
+ for (const name of names) {
250
+ if (n >= 2000) return
251
+ let st = null
252
+ try { st = ctx.fs.stat(path.join(d, name)) } catch (e) { continue }
253
+ if (st.isDir) {
254
+ if (!isSkipDir(name)) walk(path.join(d, name))
255
+ } else {
256
+ n++
257
+ }
258
+ }
259
+ }
260
+ walk(dir)
261
+ return n
262
+ }
263
+
264
+ // exploreProjectStructure 轻量项目结构概览(复刻 Go exploreProjectStructure)。
265
+ function exploreProjectStructure(ctx) {
266
+ let names = []
267
+ try { names = ctx.fs.readdir('.') } catch (e) {
268
+ return '无法读取项目根目录:' + (e && e.message ? e.message : String(e))
269
+ }
270
+ const keyFiles = names.filter(n => !isSkipDir(n) && !n.startsWith('.') && infoKeyFile(n))
271
+ const dirs = names.filter(n => !isSkipDir(n) && !n.startsWith('.'))
272
+ let b = '# 项目结构概览(供分析后写入知识库)\n\n## 根目录关键文件\n'
273
+ for (const f of keyFiles) b += '- ' + f + '\n'
274
+ b += '\n## 顶层目录(约略文件数)\n'
275
+ for (const d of dirs) {
276
+ let st = null
277
+ try { st = ctx.fs.stat(d) } catch (e) { continue }
278
+ if (!st.isDir) continue
279
+ b += '- ' + d + '/(约 ' + countDirFiles(ctx, d) + ' 文件)\n'
280
+ }
281
+ b += '\n建议:用 read 读关键文件分析后,project_info(op=write) 写入「概览」「模块-<名>」「决策-<主题>」等中文条目。'
282
+ return b
283
+ }
284
+
285
+ // projectInfoWrite(op=write):写/更新 + notes/ 镜像 + 非分支路径提示。
286
+ function projectInfoWrite(ctx, args) {
287
+ const dir = infoDir(ctx, args)
288
+ const rel = safeInfoPath(args.path)
289
+ if (rel === '') throw new Error('path 不能为空')
290
+ let branchRel = rel, mirrorRel = ''
291
+ if (rel.startsWith('notes/')) {
292
+ const br = notesToBranchRel(rel)
293
+ if (br) branchRel = br
294
+ mirrorRel = rel.replace(/^notes\//, '')
295
+ }
296
+ const fp = path.join(dir, branchRel + '.md')
297
+ const slash = Math.max(fp.lastIndexOf('/'), fp.lastIndexOf('\\'))
298
+ if (slash > 0) {
299
+ const pd = fp.slice(0, slash)
300
+ if (pd && !ctx.fs.exists(pd)) ctx.fs.mkdir(pd, true)
301
+ }
302
+ const updating = ctx.fs.exists(fp)
303
+ ctx.fs.writeFile(fp, String(args.content == null ? '' : args.content))
304
+ if (mirrorRel) { // 镜像:.agents/notes/<原相对路径>.md
305
+ const root = String(dir).replace(/\/\.pair\/project-info$/, '')
306
+ const nfp = root + '/.agents/notes/' + mirrorRel + '.md'
307
+ const nsl = Math.max(nfp.lastIndexOf('/'), nfp.lastIndexOf('\\'))
308
+ if (nsl > 0) {
309
+ const npd = nfp.slice(0, nsl)
310
+ if (npd && !ctx.fs.exists(npd)) ctx.fs.mkdir(npd, true)
311
+ }
312
+ ctx.fs.writeFile(nfp, String(args.content == null ? '' : args.content))
313
+ }
314
+ let head = branchRel
315
+ const hi = head.indexOf('/')
316
+ if (hi > 0) head = head.slice(0, hi)
317
+ let hint = ''
318
+ if (!rel.startsWith('notes/') && head !== '概览' && !isInfoBranch(head)) {
319
+ hint = '(提示:知识库是树,建议用顶层分支 目标/架构/实现/关键点/设计思想 开头,如 架构/' + branchRel + ')'
320
+ }
321
+ const verb = updating ? '已更新知识库' : '已写入知识库'
322
+ if (mirrorRel) return verb + ':' + branchRel + '(notes/ 参考路径已镜像 .agents/notes/' + mirrorRel + ')'
323
+ return verb + ':' + branchRel + hint
324
+ }
325
+
326
+ function projectInfoRead(ctx, args) {
327
+ const dir = infoDir(ctx, args)
328
+ const rel = safeInfoPath(args.path)
329
+ const fp = path.join(dir, rel + '.md')
330
+ if (!ctx.fs.exists(fp)) throw new Error('无此知识库条目:' + rel + '(用 project_info(op=list) 看全部)')
331
+ return ctx.fs.readFile(fp)
332
+ }
333
+
334
+ function projectInfoList(ctx, args) {
335
+ const entries = scanInfoEntries(ctx, infoDir(ctx, args))
336
+ if (entries.length === 0) return '(知识库为空。用 project_info(op=explore) 起步、project_info(op=write) 写入,或菜单「探索项目知识库」。)'
337
+ return infoTree(entries, true)
338
+ }
339
+
340
+ function projectInfoTree(ctx, args) {
341
+ const entries = scanInfoEntries(ctx, infoDir(ctx, args))
342
+ if (entries.length === 0) return '(知识库为空)'
343
+ return '# 项目知识库(树)\n' + infoTree(entries, false)
344
+ }
345
+
346
+ function projectInfoSearch(ctx, args) {
347
+ const q = String(args.query == null ? '' : args.query).toLowerCase().trim()
348
+ if (q === '') throw new Error('query 不能为空')
349
+ const lines = []
350
+ for (const e of scanInfoEntries(ctx, infoDir(ctx, args))) {
351
+ if ((e.rel + e.title + e.content).toLowerCase().includes(q)) {
352
+ lines.push('- ' + e.title + '(' + e.rel + ')')
353
+ }
354
+ }
355
+ if (lines.length === 0) return '(无匹配条目)'
356
+ return lines.join('\n')
357
+ }
358
+
359
+ function projectInfoDelete(ctx, args) {
360
+ const dir = infoDir(ctx, args)
361
+ const rel = safeInfoPath(args.path)
362
+ const fp = path.join(dir, rel + '.md')
363
+ if (!ctx.fs.exists(fp)) throw new Error('无此知识库条目:' + rel)
364
+ ctx.fs.rm(fp, false)
365
+ return '已删除知识库条目:' + rel
366
+ }
367
+
368
+ function projectInfoExplore(ctx, args) {
369
+ return exploreProjectStructure(ctx)
370
+ }
371
+
372
+ const impls = {
373
+ write: projectInfoWrite,
374
+ read: projectInfoRead,
375
+ list: projectInfoList,
376
+ tree: projectInfoTree,
377
+ search: projectInfoSearch,
378
+ delete: projectInfoDelete,
379
+ explore: projectInfoExplore,
380
+ }
381
+
382
+
142
383
  return {
143
384
  name: 'tool-project-info',
144
- purpose: '项目知识库(project_info_write/read/list/search/delete/explore)(自动生成,迁移自内置 Go 工具组)',
385
+ inject: ['fs'],
386
+ purpose: '项目知识库(project_info(op=write/read/list/tree/search/delete/explore))——迁移自内置 Go 工具组;调用实现(JS 编排 ctx.fs)完全在插件内(Round2 JS 原生化;2026-09 由 7 工具合并为单工具)',
145
387
  apply(ctx) {
146
388
  for (const t of tools) {
147
389
  ctx.tools.register({
@@ -153,7 +395,16 @@ return {
153
395
  requiresApproval: t.requiresApproval,
154
396
  systemTool: t.systemTool,
155
397
  parameters: t.parameters,
156
- execute: (args) => ctx.binary.exec(t.name, args || {}),
398
+ execute: (args) => {
399
+ const a = args || {}
400
+ const fn = impls[a.op]
401
+ if (!fn) {
402
+ return Promise.resolve(
403
+ 'project_info:op 无效(可用 write/read/list/tree/search/delete/explore)——未执行任何操作'
404
+ )
405
+ }
406
+ return fn(ctx, a)
407
+ },
157
408
  })
158
409
  }
159
410
  },
package/package.json CHANGED
@@ -3,8 +3,8 @@
3
3
  "name": "@paircode/tool-project-info",
4
4
  "scope": "global",
5
5
  "type": "plugin",
6
- "version": "1.0.1",
7
- "description": "项目知识库(project_info_write/read/list/search/delete/explore)(自动生成,迁移自内置 Go 工具组)",
6
+ "version": "1.1.0",
7
+ "description": "项目知识库(project_info(op=write/read/list/tree/search/delete/explore),2026-09 7 工具合并;JS 原生实现,非生成物)",
8
8
  "keywords": [
9
9
  "paircode"
10
10
  ],
Binary file