@paircode/tool-project-info 1.0.1 → 1.0.2

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.
Files changed (2) hide show
  1. package/index.js +238 -4
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // ═══════════════════════════════════════════════════════════════
2
2
  // tool-project-info — 项目知识库(project_info_write/read/list/search/delete/explore)
3
3
  //
4
- // 生成来源(2026-08-16):内置 Go 工具组 磁盘外置插件(tool_plugin_gen.go
5
- // 自动生成,schema 完整外置拷贝)。api 声明在插件,execute 调 ctx.binary 复用本插件目录 bin/ 下的独立二进制(源码 plugins-src/plugins/<name>/,改实现重编译即更换)。
4
+ // 2026-08-22 JS 原生化:调用实现(JS 编排 ctx.fs 读写 .pair/project-info/、
5
+ // 目录扫描建树、explore 结构概览)完全在插件内——不再依赖独立二进制。
6
6
  // 工具清单:project_info_write、project_info_read、project_info_list、project_info_tree、project_info_search、project_info_delete、project_info_explore
7
7
  // ═══════════════════════════════════════════════════════════════
8
8
  const tools = [
@@ -139,9 +139,243 @@ const tools = [
139
139
  }
140
140
  ];
141
141
 
142
+ // ─── JS 原生化实现(ctx.fs 读写 .pair/project-info/) ─────────
143
+
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'
150
+ }
151
+ return '../' + String(project).replace(/[\\/]+$/, '') + '/.pair/project-info'
152
+ }
153
+
154
+ const infoBranches = { 目标: 1, 架构: 1, 实现: 1, 关键点: 1, 设计思想: 1 }
155
+ function isInfoBranch(head) { return !!infoBranches[head] }
156
+
157
+ // 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('/')) {
163
+ if (s === '' || s === '.') continue
164
+ if (s === '..') { segs.pop(); continue }
165
+ segs.push(s)
166
+ }
167
+ return segs.join('/')
168
+ }
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'
175
+ }
176
+
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
183
+ }
184
+
185
+ // notesToBranchRel 参考项目决策树路径 → 知识库树分支路径(与 Go 版同映射)。
186
+ function notesToBranchRel(n) {
187
+ n = String(n).replace(/^notes\//, '').replace(/^\//, '')
188
+ if (!n) return null
189
+ const segs = n.split('/')
190
+ const leaf = segs[segs.length - 1]
191
+ let branch
192
+ if (segs.length >= 2 && segs[0] === 'implemented' && segs[1] === 'architecture') branch = '架构'
193
+ else if (segs.length >= 2 && segs[0] === 'implemented' && (segs[1] === 'decision' || segs[1] === 'decisions')) branch = '设计思想'
194
+ else if (segs.length >= 2 && segs[0] === 'implemented' && segs[1] === 'feature') branch = '实现'
195
+ else if (segs.length >= 2 && segs[0] === 'implemented') branch = '关键点'
196
+ else if (segs[0] === 'decision' || segs[0] === 'decisions') branch = '设计思想'
197
+ else if (segs.length >= 2 && segs[0] === 'inbox') branch = '实现'
198
+ else branch = '实现'
199
+ return branch + '/' + leaf
200
+ }
201
+
202
+ // scanInfoEntries 递归扫描知识库目录(.md),返回 [{path,title,level,content}]。
203
+ // 附加源:工作区 .agents/notes/ 存在时并入(路径前缀 notes/;树中已有镜像则跳过)。
204
+ function scanInfoEntries(ctx, args) {
205
+ const rootDir = infoRoot(ctx, args)
206
+ 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
+ }
222
+ }
223
+ }
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
+ })
234
+ }
235
+ out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
236
+ return out
237
+ }
238
+
239
+ // infoTree 构建知识库缩进树文本(分支=目录,叶子=条目)。showLevel 时叶子带分级标记。
240
+ 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
+ }
246
+ for (const e of entries) {
247
+ const parts = e.path.split('/')
248
+ let cur = root
249
+ for (let i = 0; i < parts.length - 1; i++) cur = ensure(cur, parts[i])
250
+ const leaf = parts[parts.length - 1]
251
+ const n = ensure(cur, leaf)
252
+ n.entry = e
253
+ }
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]
259
+ const last = i === keys.length - 1
260
+ const conn = last ? '└── ' : '├── '
261
+ const nextPrefix = prefix + (last ? ' ' : '│ ')
262
+ if (ch.entry) {
263
+ let mark = ''
264
+ if (showLevel) mark = ' [' + ch.entry.level + ']'
265
+ let title = ch.entry.title
266
+ const leaf = ch.entry.path.split('/').pop()
267
+ if (leaf && leaf !== title) title += '(' + leaf + ')'
268
+ text += prefix + conn + title + mark + '\n'
269
+ } else {
270
+ text += prefix + conn + k + '/\n'
271
+ }
272
+ walk(ch, nextPrefix)
273
+ })
274
+ }
275
+ walk(root, '')
276
+ return text
277
+ }
278
+
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 }
280
+
281
+ // exploreProjectStructure 轻量项目结构概览(根目录关键文件 + 顶层目录及文件数)。
282
+ function exploreProjectStructure(ctx) {
283
+ 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'
290
+ }
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
+ }
301
+ }
302
+ if (dirs.length) out += '- ' + dirs.join('\n- ') + '\n'
303
+ return out
304
+ }
305
+
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('/'))
326
+ if (npd && !ctx.fs.exists(npd)) ctx.fs.mkdir(npd, true)
327
+ ctx.fs.writeFile(nfp, String(args.content || ''))
328
+ }
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 + ')')
360
+ }
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
+ },
373
+ }
374
+
142
375
  return {
143
376
  name: 'tool-project-info',
144
- purpose: '项目知识库(project_info_write/read/list/search/delete/explore)(自动生成,迁移自内置 Go 工具组)',
377
+ inject: ['fs'],
378
+ purpose: '项目知识库(project_info_write/read/list/search/delete/explore)——2026-08-22 JS 原生化:调用实现(JS 编排 ctx.fs)完全在插件内,不再依赖独立二进制',
145
379
  apply(ctx) {
146
380
  for (const t of tools) {
147
381
  ctx.tools.register({
@@ -153,7 +387,7 @@ return {
153
387
  requiresApproval: t.requiresApproval,
154
388
  systemTool: t.systemTool,
155
389
  parameters: t.parameters,
156
- execute: (args) => ctx.binary.exec(t.name, args || {}),
390
+ execute: (args) => impls[t.name](ctx, args || {}),
157
391
  })
158
392
  }
159
393
  },
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "@paircode/tool-project-info",
4
4
  "scope": "global",
5
5
  "type": "plugin",
6
- "version": "1.0.1",
6
+ "version": "1.0.2",
7
7
  "description": "项目知识库(project_info_write/read/list/search/delete/explore)(自动生成,迁移自内置 Go 工具组)",
8
8
  "keywords": [
9
9
  "paircode"