@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 +300 -283
- package/package.json +2 -2
- package/bin/tool-project-info.exe +0 -0
package/index.js
CHANGED
|
@@ -1,191 +1,102 @@
|
|
|
1
1
|
// ═══════════════════════════════════════════════════════════════
|
|
2
|
-
// tool-project-info —
|
|
2
|
+
// tool-project-info — 项目知识库(单工具 project_info(op=write/read/list/tree/search/delete/explore);2026-09 由 7 工具合并)
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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": "
|
|
11
|
-
"description": "
|
|
12
|
-
"usageGuide": "
|
|
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
|
-
"
|
|
20
|
-
"description": "
|
|
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": "
|
|
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
|
-
"
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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 '
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
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 === '..') {
|
|
165
|
-
|
|
80
|
+
if (s === '..') { out.pop(); continue }
|
|
81
|
+
out.push(s)
|
|
166
82
|
}
|
|
167
|
-
return
|
|
83
|
+
return out.join('/')
|
|
168
84
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
96
|
+
// notesToBranchRel 把决策树路径映射到知识库树分支路径(复刻 Go 实现)。
|
|
186
97
|
function notesToBranchRel(n) {
|
|
187
98
|
n = String(n).replace(/^notes\//, '').replace(/^\//, '')
|
|
188
|
-
if (
|
|
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
|
|
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
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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.
|
|
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
|
-
//
|
|
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: {}
|
|
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.
|
|
181
|
+
const parts = e.rel.split('/')
|
|
248
182
|
let cur = root
|
|
249
|
-
for (let i = 0; i < parts.length - 1; 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
|
-
|
|
252
|
-
|
|
189
|
+
if (!cur.children[leaf]) cur.children[leaf] = { name: leaf, children: {} }
|
|
190
|
+
cur.children[leaf].entry = e
|
|
253
191
|
}
|
|
254
|
-
let
|
|
255
|
-
const walk = (
|
|
256
|
-
const keys = Object.keys(
|
|
257
|
-
keys.
|
|
258
|
-
const ch =
|
|
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
|
-
|
|
261
|
-
|
|
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.
|
|
204
|
+
const leaf = partsOf(ch.entry.rel)
|
|
267
205
|
if (leaf && leaf !== title) title += '(' + leaf + ')'
|
|
268
|
-
|
|
206
|
+
b += prefix + conn + title + mark + '\n'
|
|
269
207
|
} else {
|
|
270
|
-
|
|
208
|
+
b += prefix + conn + keys[i] + '/\n'
|
|
271
209
|
}
|
|
272
210
|
walk(ch, nextPrefix)
|
|
273
|
-
}
|
|
211
|
+
}
|
|
274
212
|
}
|
|
275
213
|
walk(root, '')
|
|
276
|
-
return
|
|
214
|
+
return b
|
|
277
215
|
}
|
|
278
216
|
|
|
279
|
-
|
|
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
|
-
//
|
|
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) {
|
|
285
|
-
|
|
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
|
-
|
|
292
|
-
const dirs =
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
303
|
-
return
|
|
281
|
+
b += '\n建议:用 read 读关键文件分析后,project_info(op=write) 写入「概览」「模块-<名>」「决策-<主题>」等中文条目。'
|
|
282
|
+
return b
|
|
304
283
|
}
|
|
305
284
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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: '项目知识库(
|
|
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) =>
|
|
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
|
|
7
|
-
"description": "项目知识库(
|
|
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
|