@paircode/tool-project-info 1.0.0 → 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.
- package/bin/tool-project-info.exe +0 -0
- package/index.js +238 -4
- package/package.json +1 -1
|
Binary file
|
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
|
-
//
|
|
5
|
-
//
|
|
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
|
-
|
|
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) =>
|
|
390
|
+
execute: (args) => impls[t.name](ctx, args || {}),
|
|
157
391
|
})
|
|
158
392
|
}
|
|
159
393
|
},
|
package/package.json
CHANGED