@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 +364 -113
- package/package.json +2 -2
- package/bin/tool-project-info.exe +0 -0
package/index.js
CHANGED
|
@@ -1,147 +1,389 @@
|
|
|
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": "
|
|
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
|
-
"
|
|
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
|
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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
|