@paircode/tool-project-info 1.0.2 → 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,191 +1,102 @@
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-22 JS 原生化:调用实现(JS 编排 ctx.fs 读写 .pair/project-info/、
5
- // 目录扫描建树、explore 结构概览)完全在插件内——不再依赖独立二进制。
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": "关键词",
47
+ "description": "search 用:关键词",
99
48
  "type": "string"
100
49
  }
101
50
  },
102
51
  "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 等。",
121
- "type": "string"
122
- }
123
- },
124
- "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
 
142
- // ─── JS 原生化实现(ctx.fs 读写 .pair/project-info/) ─────────
143
62
 
144
- // 知识库根:project 非空 ../<project>/.pair/project-info(多根归属由 resolve 检查);绝对路径直接传。
145
- function infoRoot(ctx, args) {
146
- const project = args.project
147
- if (!project) return '.pair/project-info'
148
- if (/^[a-zA-Z]:[\\/]/.test(project) || project.startsWith('/')) {
149
- return String(project).replace(/[\\/]+$/, '') + '/.pair/project-info'
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'
150
71
  }
151
- return '../' + String(project).replace(/[\\/]+$/, '') + '/.pair/project-info'
72
+ return '.pair/project-info'
152
73
  }
153
74
 
154
- const infoBranches = { 目标: 1, 架构: 1, 实现: 1, 关键点: 1, 设计思想: 1 }
155
- function isInfoBranch(head) { return !!infoBranches[head] }
156
-
157
75
  // safeInfoPath 规范化条目路径:去 .md、清理、禁路径穿越(..、绝对路径),允许 / 嵌套。
158
- function safeInfoPath(p) {
159
- p = String(p || '').replace(/\.md$/, '').trim().replace(/\\/g, '/')
160
- // path.Clean 近似:拆段抵消 ...
161
- const segs = []
162
- for (const s of p.split('/')) {
76
+ function cleanSegs(p) {
77
+ const out = []
78
+ for (const s of String(p).split('/')) {
163
79
  if (s === '' || s === '.') continue
164
- if (s === '..') { segs.pop(); continue }
165
- segs.push(s)
80
+ if (s === '..') { out.pop(); continue }
81
+ out.push(s)
166
82
  }
167
- return segs.join('/')
83
+ return out.join('/')
168
84
  }
169
-
170
- function infoLevel(rel) {
171
- const low = String(rel).toLowerCase()
172
- if (low === 'overview' || rel === '概览' || rel === '项目概览') return 'overview'
173
- if ((rel.match(/\//g) || []).length >= 2) return 'detail'
174
- return 'module'
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, '')
175
89
  }
176
90
 
177
- function firstHeading(md, fallback) {
178
- for (const ln of String(md).split('\n')) {
179
- const s = ln.trim()
180
- if (s.startsWith('# ')) return s.slice(2).trim()
181
- }
182
- return fallback
91
+ const infoBranches = ['目标', '架构', '实现', '关键点', '设计思想']
92
+ function isInfoBranch(head) {
93
+ return infoBranches.indexOf(head) >= 0
183
94
  }
184
95
 
185
- // notesToBranchRel 参考项目决策树路径 → 知识库树分支路径(与 Go 版同映射)。
96
+ // notesToBranchRel 把决策树路径映射到知识库树分支路径(复刻 Go 实现)。
186
97
  function notesToBranchRel(n) {
187
98
  n = String(n).replace(/^notes\//, '').replace(/^\//, '')
188
- if (!n) return null
99
+ if (n === '') return null
189
100
  const segs = n.split('/')
190
101
  const leaf = segs[segs.length - 1]
191
102
  let branch
@@ -196,186 +107,283 @@ function notesToBranchRel(n) {
196
107
  else if (segs[0] === 'decision' || segs[0] === 'decisions') branch = '设计思想'
197
108
  else if (segs.length >= 2 && segs[0] === 'inbox') branch = '实现'
198
109
  else branch = '实现'
199
- return branch + '/' + leaf
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'
200
128
  }
201
129
 
202
- // scanInfoEntries 递归扫描知识库目录(.md),返回 [{path,title,level,content}]。
203
- // 附加源:工作区 .agents/notes/ 存在时并入(路径前缀 notes/;树中已有镜像则跳过)。
204
- function scanInfoEntries(ctx, args) {
205
- const rootDir = infoRoot(ctx, args)
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 [] }
206
134
  const out = []
207
- const walkDir = (base, prefix, skip) => {
208
- let names = []
209
- try { names = ctx.fs.readdir(base) } catch { return }
210
- for (const n of names.sort()) {
211
- const p = base + '/' + n
212
- let st
213
- try { st = ctx.fs.stat(p) } catch { continue }
214
- const rel = (prefix ? prefix + '/' : '') + n.replace(/\.md$/, '')
215
- if (st.isDir) {
216
- walkDir(p, rel, skip)
217
- } else if (n.endsWith('.md')) {
218
- if (skip && skip(rel)) continue
219
- const content = ctx.fs.readFile(p)
220
- out.push({ path: rel, title: firstHeading(content, rel), level: infoLevel(rel), content })
221
- }
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
222
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 })
223
148
  }
224
- walkDir(rootDir, '', null)
225
- // .agents/notes 附加源:rootDir 为 <projRoot>/.pair/project-info → 项目根 = 去掉该尾缀
226
- const projRoot = rootDir.replace(/\/\.pair\/project-info$/, '')
227
- const notes = (projRoot ? projRoot + '/' : '') + '.agents/notes'
228
- if (notes !== rootDir) {
229
- walkDir(notes, 'notes', (nrel) => {
230
- const br = notesToBranchRel(nrel)
231
- if (!br) return false
232
- try { ctx.fs.exists(rootDir + '/' + br + '.md'); return ctx.fs.exists(rootDir + '/' + br + '.md') } catch { return false }
233
- })
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
+ }
234
166
  }
235
- out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
167
+ out.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0))
236
168
  return out
237
169
  }
238
170
 
239
- // infoTree 构建知识库缩进树文本(分支=目录,叶子=条目)。showLevel 时叶子带分级标记。
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 构建知识库条目树(分支=目录,叶子=条目),返回缩进树文本。
240
178
  function infoTree(entries, showLevel) {
241
- const root = { children: {}, entry: null }
242
- const ensure = (node, name) => {
243
- if (!node.children[name]) node.children[name] = { children: {}, entry: null, name }
244
- return node.children[name]
245
- }
179
+ const root = { children: {} }
246
180
  for (const e of entries) {
247
- const parts = e.path.split('/')
181
+ const parts = e.rel.split('/')
248
182
  let cur = root
249
- for (let i = 0; i < parts.length - 1; i++) cur = ensure(cur, parts[i])
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
+ }
250
188
  const leaf = parts[parts.length - 1]
251
- const n = ensure(cur, leaf)
252
- n.entry = e
189
+ if (!cur.children[leaf]) cur.children[leaf] = { name: leaf, children: {} }
190
+ cur.children[leaf].entry = e
253
191
  }
254
- let text = ''
255
- const walk = (node, prefix) => {
256
- const keys = Object.keys(node.children).sort()
257
- keys.forEach((k, i) => {
258
- const ch = node.children[k]
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]]
259
197
  const last = i === keys.length - 1
260
- const conn = last ? '└── ' : '├── '
261
- const nextPrefix = prefix + (last ? ' ' : '│ ')
198
+ let conn = '├── ', nextPrefix = prefix + '│ '
199
+ if (last) { conn = '└── '; nextPrefix = prefix + ' ' }
262
200
  if (ch.entry) {
263
201
  let mark = ''
264
202
  if (showLevel) mark = ' [' + ch.entry.level + ']'
265
203
  let title = ch.entry.title
266
- const leaf = ch.entry.path.split('/').pop()
204
+ const leaf = partsOf(ch.entry.rel)
267
205
  if (leaf && leaf !== title) title += '(' + leaf + ')'
268
- text += prefix + conn + title + mark + '\n'
206
+ b += prefix + conn + title + mark + '\n'
269
207
  } else {
270
- text += prefix + conn + k + '/\n'
208
+ b += prefix + conn + keys[i] + '/\n'
271
209
  }
272
210
  walk(ch, nextPrefix)
273
- })
211
+ }
274
212
  }
275
213
  walk(root, '')
276
- return text
214
+ return b
277
215
  }
278
216
 
279
- const infoKeyFiles = { 'go.mod': 1, 'package.json': 1, 'cargo.toml': 1, 'pyproject.toml': 1, 'pom.xml': 1, 'main.go': 1, 'agents.md': 1, 'claude.md': 1, 'go.sum': 1, 'tsconfig.json': 1 }
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
+ }
280
224
 
281
- // exploreProjectStructure 轻量项目结构概览(根目录关键文件 + 顶层目录及文件数)。
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)。
282
265
  function exploreProjectStructure(ctx) {
283
266
  let names = []
284
- try { names = ctx.fs.readdir('.') } catch (e) { return '无法读取项目根目录:' + e.message }
285
- let out = '# 项目结构概览(供分析后写入知识库)\n\n## 根目录关键文件\n'
286
- for (const n of names.sort()) {
287
- let st
288
- try { st = ctx.fs.stat(n) } catch { continue }
289
- if (!st.isDir && (/(readme|makefile)/i.test(n) || infoKeyFiles[n.toLowerCase()])) out += '- ' + n + '\n'
267
+ try { names = ctx.fs.readdir('.') } catch (e) {
268
+ return '无法读取项目根目录:' + (e && e.message ? e.message : String(e))
290
269
  }
291
- out += '\n## 顶层目录(文件数)\n'
292
- const dirs = []
293
- for (const n of names.sort()) {
294
- let st
295
- try { st = ctx.fs.stat(n) } catch { continue }
296
- if (st.isDir) {
297
- let cnt = 0
298
- try { cnt = (ctx.fs.readdir(n) || []).length } catch { cnt = 0 }
299
- dirs.push(n + ' (' + cnt + ')')
300
- }
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'
301
280
  }
302
- if (dirs.length) out += '- ' + dirs.join('\n- ') + '\n'
303
- return out
281
+ b += '\n建议:用 read 读关键文件分析后,project_info(op=write) 写入「概览」「模块-<名>」「决策-<主题>」等中文条目。'
282
+ return b
304
283
  }
305
284
 
306
- const impls = {
307
- project_info_write(ctx, args) {
308
- const dir = infoRoot(ctx, args)
309
- let rel = safeInfoPath(args.path)
310
- if (!rel) throw new Error('path 不能为空')
311
- let branchRel = rel, mirrorRel = ''
312
- if (rel.startsWith('notes/')) {
313
- if (notesToBranchRel(rel)) branchRel = notesToBranchRel(rel)
314
- mirrorRel = rel.replace(/^notes\//, '')
315
- }
316
- const fp = dir + '/' + branchRel + '.md'
317
- const parentDir = fp.slice(0, fp.lastIndexOf('/'))
318
- if (parentDir && !ctx.fs.exists(parentDir)) ctx.fs.mkdir(parentDir, true)
319
- const updating = ctx.fs.exists(fp)
320
- ctx.fs.writeFile(fp, String(args.content || ''))
321
- if (mirrorRel) {
322
- // 镜像 .agents/notes/<原相对路径>.md(项目根 = dir 去掉尾缀)
323
- const projRoot = dir.replace(/\/\.pair\/project-info$/, '')
324
- const nfp = (projRoot ? projRoot + '/' : '') + '.agents/notes/' + mirrorRel + '.md'
325
- const npd = nfp.slice(0, nfp.lastIndexOf('/'))
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)
326
310
  if (npd && !ctx.fs.exists(npd)) ctx.fs.mkdir(npd, true)
327
- ctx.fs.writeFile(nfp, String(args.content || ''))
328
311
  }
329
- const head = branchRel.includes('/') ? branchRel.slice(0, branchRel.indexOf('/')) : branchRel
330
- let hint = ''
331
- if (!rel.startsWith('notes/') && head !== '概览' && !isInfoBranch(head)) {
332
- hint = '(提示:知识库是树,建议用顶层分支 目标/架构/实现/关键点/设计思想 开头,如 架构/' + branchRel + ')'
333
- }
334
- const verb = updating ? '已更新知识库' : '已写入知识库'
335
- if (mirrorRel) return verb + '' + branchRel + '(notes/ 参考路径已镜像 .agents/notes/' + mirrorRel + ')'
336
- return verb + ':' + branchRel + hint
337
- },
338
- project_info_read(ctx, args) {
339
- const rel = safeInfoPath(args.path)
340
- const fp = infoRoot(ctx, args) + '/' + rel + '.md'
341
- if (!ctx.fs.exists(fp)) throw new Error('无此知识库条目:' + rel + '(用 project_info_list 看全部)')
342
- return ctx.fs.readFile(fp)
343
- },
344
- project_info_list(ctx, args) {
345
- const entries = scanInfoEntries(ctx, args)
346
- if (entries.length === 0) return '(知识库为空。用 project_info_explore 起步、project_info_write 写入,或菜单「探索项目知识库」。)'
347
- return infoTree(entries, true)
348
- },
349
- project_info_tree(ctx, args) {
350
- const entries = scanInfoEntries(ctx, args)
351
- if (entries.length === 0) return '(知识库为空)'
352
- return '# 项目知识库(树)\n' + infoTree(entries, false)
353
- },
354
- project_info_search(ctx, args) {
355
- const q = String(args.query || '').trim().toLowerCase()
356
- if (!q) throw new Error('query 不能为空')
357
- const lines = []
358
- for (const e of scanInfoEntries(ctx, args)) {
359
- if ((e.path + e.title + e.content).toLowerCase().includes(q)) lines.push('- ' + e.title + '(' + e.path + ')')
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 + ')')
360
353
  }
361
- return lines.length === 0 ? '(无匹配条目)' : lines.join('\n')
362
- },
363
- project_info_delete(ctx, args) {
364
- const rel = safeInfoPath(args.path)
365
- const fp = infoRoot(ctx, args) + '/' + rel + '.md'
366
- if (!ctx.fs.exists(fp)) throw new Error('无此知识库条目:' + rel)
367
- ctx.fs.rm(fp, false)
368
- return '已删除知识库条目:' + rel
369
- },
370
- project_info_explore(ctx, args) {
371
- return exploreProjectStructure(ctx)
372
- },
354
+ }
355
+ if (lines.length === 0) return '(无匹配条目)'
356
+ return lines.join('\n')
373
357
  }
374
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
+
375
383
  return {
376
384
  name: 'tool-project-info',
377
385
  inject: ['fs'],
378
- purpose: '项目知识库(project_info_write/read/list/search/delete/explore)——2026-08-22 JS 原生化:调用实现(JS 编排 ctx.fs)完全在插件内,不再依赖独立二进制',
386
+ purpose: '项目知识库(project_info(op=write/read/list/tree/search/delete/explore))——迁移自内置 Go 工具组;调用实现(JS 编排 ctx.fs)完全在插件内(Round2 JS 原生化;2026-09 由 7 工具合并为单工具)',
379
387
  apply(ctx) {
380
388
  for (const t of tools) {
381
389
  ctx.tools.register({
@@ -387,7 +395,16 @@ return {
387
395
  requiresApproval: t.requiresApproval,
388
396
  systemTool: t.systemTool,
389
397
  parameters: t.parameters,
390
- execute: (args) => impls[t.name](ctx, 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
+ },
391
408
  })
392
409
  }
393
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.2",
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