@vibe-forge/core 0.7.4 → 0.8.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/package.json +4 -46
- package/src/env.ts +5 -25
- package/src/index.ts +0 -5
- package/src/types.ts +12 -72
- package/src/ws.ts +3 -12
- package/src/adapter/index.ts +0 -6
- package/src/adapter/loader.ts +0 -11
- package/src/adapter/type.ts +0 -112
- package/src/config/load.ts +0 -122
- package/src/config/types.ts +0 -289
- package/src/config.ts +0 -2
- package/src/controllers/benchmark/discover.ts +0 -89
- package/src/controllers/benchmark/index.ts +0 -24
- package/src/controllers/benchmark/result-store.ts +0 -46
- package/src/controllers/benchmark/runner.ts +0 -415
- package/src/controllers/benchmark/schema.ts +0 -60
- package/src/controllers/benchmark/types.ts +0 -80
- package/src/controllers/benchmark/utils.ts +0 -144
- package/src/controllers/benchmark/workspace.ts +0 -179
- package/src/controllers/config/index.ts +0 -214
- package/src/controllers/system/assets/completed.mp3 +0 -0
- package/src/controllers/system/assets/mcp.png +0 -0
- package/src/controllers/system/index.ts +0 -102
- package/src/controllers/task/generate-adapter-query-options.ts +0 -218
- package/src/controllers/task/index.ts +0 -2
- package/src/controllers/task/prepare.ts +0 -68
- package/src/controllers/task/run.ts +0 -191
- package/src/controllers/task/schema.ts +0 -131
- package/src/controllers/task/type.ts +0 -6
- package/src/hooks/call.ts +0 -74
- package/src/hooks/index.ts +0 -39
- package/src/hooks/loader.ts +0 -75
- package/src/hooks/runtime.ts +0 -139
- package/src/hooks/type.ts +0 -120
- package/src/utils/cache.ts +0 -58
- package/src/utils/create-logger.ts +0 -89
- package/src/utils/definition-loader.ts +0 -530
- package/src/utils/filter.ts +0 -26
- package/src/utils/string-transform.ts +0 -37
- package/src/utils/uuid.ts +0 -6
|
@@ -1,530 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import { basename, dirname, relative, resolve } from 'node:path'
|
|
4
|
-
import process from 'node:process'
|
|
5
|
-
|
|
6
|
-
import { glob } from 'fast-glob'
|
|
7
|
-
import fm from 'front-matter'
|
|
8
|
-
|
|
9
|
-
export interface Filter {
|
|
10
|
-
include?: string[]
|
|
11
|
-
exclude?: string[]
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface Rule {
|
|
15
|
-
name?: string
|
|
16
|
-
description?: string
|
|
17
|
-
globs?: string | string[]
|
|
18
|
-
/**
|
|
19
|
-
* 是否默认加载至系统上下文
|
|
20
|
-
*/
|
|
21
|
-
always?: boolean
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface Spec {
|
|
25
|
-
name?: string
|
|
26
|
-
always?: boolean
|
|
27
|
-
description?: string
|
|
28
|
-
tags?: string[]
|
|
29
|
-
params?: {
|
|
30
|
-
name: string
|
|
31
|
-
description?: string
|
|
32
|
-
}[]
|
|
33
|
-
rules?: string[]
|
|
34
|
-
skills?: string[]
|
|
35
|
-
mcpServers?: Filter
|
|
36
|
-
tools?: Filter
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface LocalRuleReference {
|
|
40
|
-
type?: 'local'
|
|
41
|
-
path: string
|
|
42
|
-
desc?: string
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface RemoteRuleReference {
|
|
46
|
-
type: 'remote'
|
|
47
|
-
tags?: string[]
|
|
48
|
-
desc?: string
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export type RuleReference = string | LocalRuleReference | RemoteRuleReference
|
|
52
|
-
|
|
53
|
-
export interface SkillSelection {
|
|
54
|
-
type: 'include' | 'exclude'
|
|
55
|
-
list: string[]
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface Entity {
|
|
59
|
-
name?: string
|
|
60
|
-
always?: boolean
|
|
61
|
-
description?: string
|
|
62
|
-
tags?: string[]
|
|
63
|
-
prompt?: string
|
|
64
|
-
promptPath?: string
|
|
65
|
-
rules?: RuleReference[]
|
|
66
|
-
skills?: string[] | SkillSelection
|
|
67
|
-
mcpServers?: Filter
|
|
68
|
-
tools?: Filter
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface Skill {
|
|
72
|
-
name?: string
|
|
73
|
-
description?: string
|
|
74
|
-
always?: boolean
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface Definition<T> {
|
|
78
|
-
path: string
|
|
79
|
-
body: string
|
|
80
|
-
attributes: T
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* 以结构化的方式加载本地文档数据
|
|
85
|
-
*/
|
|
86
|
-
export const loadLocalDocuments = async <Attrs extends object>(
|
|
87
|
-
paths: string[]
|
|
88
|
-
): Promise<Definition<Attrs>[]> => {
|
|
89
|
-
const promises = paths.map(async (path) => {
|
|
90
|
-
const content = await readFile(path, 'utf-8')
|
|
91
|
-
const { body, attributes } = fm<Attrs>(content)
|
|
92
|
-
return {
|
|
93
|
-
path,
|
|
94
|
-
body,
|
|
95
|
-
attributes
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
return Promise.all(promises)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const normalizePath = (path: string) => path.split('\\').join('/')
|
|
102
|
-
|
|
103
|
-
const stripExtension = (fileName: string) => fileName.replace(/\.[^/.]+$/, '')
|
|
104
|
-
|
|
105
|
-
const getFirstNonEmptyLine = (text: string) =>
|
|
106
|
-
text
|
|
107
|
-
.split('\n')
|
|
108
|
-
.map(line => line.trim())
|
|
109
|
-
.find(Boolean)
|
|
110
|
-
|
|
111
|
-
const toPromptPath = (cwd: string, path: string) => {
|
|
112
|
-
const relPath = normalizePath(relative(cwd, path))
|
|
113
|
-
return relPath.startsWith('..') ? normalizePath(path) : relPath
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const resolveDocumentName = (
|
|
117
|
-
path: string,
|
|
118
|
-
explicitName?: string,
|
|
119
|
-
indexFileNames: string[] = []
|
|
120
|
-
) => {
|
|
121
|
-
const trimmedName = explicitName?.trim()
|
|
122
|
-
if (trimmedName) return trimmedName
|
|
123
|
-
|
|
124
|
-
const fileName = basename(path).toLowerCase()
|
|
125
|
-
if (indexFileNames.includes(fileName)) {
|
|
126
|
-
return basename(dirname(path))
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return stripExtension(basename(path))
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const resolveDocumentDescription = (
|
|
133
|
-
body: string,
|
|
134
|
-
explicitDescription?: string,
|
|
135
|
-
fallbackName?: string
|
|
136
|
-
) => {
|
|
137
|
-
const trimmedDescription = explicitDescription?.trim()
|
|
138
|
-
return trimmedDescription || getFirstNonEmptyLine(body) || fallbackName || ''
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const resolveSpecIdentifier = (path: string, explicitName?: string) => {
|
|
142
|
-
return resolveDocumentName(path, explicitName, ['index.md'])
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const toNonEmptyStringArray = (values: unknown): string[] => {
|
|
146
|
-
if (!Array.isArray(values)) return []
|
|
147
|
-
return values
|
|
148
|
-
.filter((value): value is string => typeof value === 'string')
|
|
149
|
-
.map(value => value.trim())
|
|
150
|
-
.filter(Boolean)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const isLocalRuleReference = (value: RuleReference): value is LocalRuleReference => {
|
|
154
|
-
return (
|
|
155
|
-
value != null &&
|
|
156
|
-
typeof value === 'object' &&
|
|
157
|
-
typeof value.path === 'string' &&
|
|
158
|
-
(value.type == null || value.type === 'local')
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const isRemoteRuleReference = (value: RuleReference): value is RemoteRuleReference => {
|
|
163
|
-
return (
|
|
164
|
-
value != null &&
|
|
165
|
-
typeof value === 'object' &&
|
|
166
|
-
value.type === 'remote'
|
|
167
|
-
)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const resolveRulePattern = (pattern: string, baseDir: string) => {
|
|
171
|
-
const trimmed = pattern.trim()
|
|
172
|
-
if (!trimmed) return undefined
|
|
173
|
-
if (trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
|
174
|
-
return normalizePath(resolve(baseDir, trimmed))
|
|
175
|
-
}
|
|
176
|
-
return trimmed
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const createRemoteRuleDefinition = (
|
|
180
|
-
rule: RemoteRuleReference,
|
|
181
|
-
index: number
|
|
182
|
-
): Definition<Rule> => {
|
|
183
|
-
const tags = toNonEmptyStringArray(rule.tags)
|
|
184
|
-
const desc = rule.desc?.trim() || (
|
|
185
|
-
tags.length > 0
|
|
186
|
-
? `远程知识库标签:${tags.join(', ')}`
|
|
187
|
-
: '远程知识库规则引用'
|
|
188
|
-
)
|
|
189
|
-
const bodyParts = [
|
|
190
|
-
desc,
|
|
191
|
-
tags.length > 0 ? `知识库标签:${tags.join(', ')}` : undefined,
|
|
192
|
-
'该规则来自远程知识库引用,不对应本地文件。'
|
|
193
|
-
].filter((value): value is string => Boolean(value))
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
path: `remote-rule-${index + 1}.md`,
|
|
197
|
-
body: bodyParts.join('\n'),
|
|
198
|
-
attributes: {
|
|
199
|
-
name: tags.length > 0 ? `remote:${tags.join(',')}` : `remote-rule-${index + 1}`,
|
|
200
|
-
description: desc
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export class DefinitionLoader {
|
|
206
|
-
private readonly cwd: string
|
|
207
|
-
|
|
208
|
-
constructor(cwd: string = process.cwd()) {
|
|
209
|
-
this.cwd = cwd
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
private async scan(
|
|
213
|
-
patterns: string[],
|
|
214
|
-
cwd: string = this.cwd
|
|
215
|
-
): Promise<string[]> {
|
|
216
|
-
const paths = await glob(patterns, { cwd, absolute: true })
|
|
217
|
-
return paths.sort((a, b) => {
|
|
218
|
-
const aKey = relative(cwd, a).split('\\').join('/')
|
|
219
|
-
const bKey = relative(cwd, b).split('\\').join('/')
|
|
220
|
-
return aKey.localeCompare(bKey)
|
|
221
|
-
})
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async loadRules(
|
|
225
|
-
rules: RuleReference[],
|
|
226
|
-
options?: {
|
|
227
|
-
baseDir?: string
|
|
228
|
-
}
|
|
229
|
-
) {
|
|
230
|
-
const baseDir = options?.baseDir ?? this.cwd
|
|
231
|
-
const definitions: Definition<Rule>[] = []
|
|
232
|
-
|
|
233
|
-
for (const [index, rule] of rules.entries()) {
|
|
234
|
-
if (typeof rule === 'string') {
|
|
235
|
-
const pattern = resolveRulePattern(rule, baseDir)
|
|
236
|
-
if (!pattern) continue
|
|
237
|
-
definitions.push(
|
|
238
|
-
...await loadLocalDocuments<Rule>(
|
|
239
|
-
await this.scan([pattern])
|
|
240
|
-
)
|
|
241
|
-
)
|
|
242
|
-
continue
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (isRemoteRuleReference(rule)) {
|
|
246
|
-
definitions.push(createRemoteRuleDefinition(rule, index))
|
|
247
|
-
continue
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (!isLocalRuleReference(rule)) continue
|
|
251
|
-
|
|
252
|
-
const pattern = resolveRulePattern(rule.path, baseDir)
|
|
253
|
-
if (!pattern) continue
|
|
254
|
-
|
|
255
|
-
const docs = await loadLocalDocuments<Rule>(
|
|
256
|
-
await this.scan([pattern])
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
definitions.push(
|
|
260
|
-
...docs.map((doc) => ({
|
|
261
|
-
...doc,
|
|
262
|
-
attributes: {
|
|
263
|
-
...doc.attributes,
|
|
264
|
-
description: rule.desc?.trim() || doc.attributes.description
|
|
265
|
-
}
|
|
266
|
-
}))
|
|
267
|
-
)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return definitions
|
|
271
|
-
}
|
|
272
|
-
async loadDefaultRules(): Promise<Definition<Rule>[]> {
|
|
273
|
-
return this.loadRules([
|
|
274
|
-
'.ai/rules/*.md',
|
|
275
|
-
'.ai/plugins/*/rules/*.md'
|
|
276
|
-
])
|
|
277
|
-
}
|
|
278
|
-
generateRulesPrompt(rules: Definition<Rule>[]): string {
|
|
279
|
-
const rulesPrompt = rules
|
|
280
|
-
.map((rule) => {
|
|
281
|
-
const { path, body, attributes } = rule
|
|
282
|
-
const name = resolveDocumentName(path, attributes.name)
|
|
283
|
-
const desc = resolveDocumentDescription(body, attributes.description, name)
|
|
284
|
-
const content = attributes.always && body.trim()
|
|
285
|
-
? `<rule-content>\n${body.trim()}\n</rule-content>\n`
|
|
286
|
-
: ''
|
|
287
|
-
return ` - ${name}:${desc}\n${content}--------------------\n`
|
|
288
|
-
})
|
|
289
|
-
.filter(Boolean)
|
|
290
|
-
.join('\n')
|
|
291
|
-
|
|
292
|
-
return (
|
|
293
|
-
'<system-prompt>\n' +
|
|
294
|
-
'项目系统规则如下:\n' +
|
|
295
|
-
`${rulesPrompt}\n` +
|
|
296
|
-
'</system-prompt>\n'
|
|
297
|
-
)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async loadSkills(skills?: string[]): Promise<Definition<Skill>[]> {
|
|
301
|
-
// 1. Scan for skills in standard locations
|
|
302
|
-
// Project root skills: .ai/skills/{name}/SKILL.md
|
|
303
|
-
// Plugin skills: .ai/plugins/{plugin}/skills/{name}/SKILL.md
|
|
304
|
-
|
|
305
|
-
// Note: The user code uses readdir to iterate plugins.
|
|
306
|
-
// We can use glob for easier path finding.
|
|
307
|
-
|
|
308
|
-
const patterns = [
|
|
309
|
-
'.ai/skills/*/SKILL.md',
|
|
310
|
-
'.ai/plugins/*/skills/*/SKILL.md'
|
|
311
|
-
]
|
|
312
|
-
|
|
313
|
-
let paths = await this.scan(patterns)
|
|
314
|
-
|
|
315
|
-
// Filter by directory name (skill name)
|
|
316
|
-
if (skills) {
|
|
317
|
-
paths = paths.filter(path => {
|
|
318
|
-
return skills.includes(basename(dirname(path)))
|
|
319
|
-
})
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return loadLocalDocuments<Skill>(paths)
|
|
323
|
-
}
|
|
324
|
-
async loadDefaultSkills(): Promise<Definition<Skill>[]> {
|
|
325
|
-
return this.loadSkills()
|
|
326
|
-
}
|
|
327
|
-
generateSkillsPrompt(skills: Definition<Skill>[]): string {
|
|
328
|
-
return skills
|
|
329
|
-
.map((skill) => {
|
|
330
|
-
const { path, body, attributes } = skill
|
|
331
|
-
const name = resolveDocumentName(path, attributes.name, ['skill.md'])
|
|
332
|
-
const desc = resolveDocumentDescription(body, attributes.description, name)
|
|
333
|
-
return (
|
|
334
|
-
'技能相关信息如下,通过阅读以下内容了解技能的详细信息:\n' +
|
|
335
|
-
`- 技能名称:${name}\n` +
|
|
336
|
-
`- 技能介绍:${desc}\n` +
|
|
337
|
-
`- 技能文件资源路径:${toPromptPath(this.cwd, dirname(path))}\n` +
|
|
338
|
-
'- 资源内容:\n' +
|
|
339
|
-
'<skill-content>\n' +
|
|
340
|
-
`${body.trim()}\n` +
|
|
341
|
-
'</skill-content>\n' +
|
|
342
|
-
'资源内容中的文件路径相对「技能文件资源路径」路径,通过读取相关工具按照实际需要进行阅读。\n'
|
|
343
|
-
)
|
|
344
|
-
})
|
|
345
|
-
.filter(Boolean)
|
|
346
|
-
.join('\n')
|
|
347
|
-
}
|
|
348
|
-
generateSkillsRoutePrompt(skills: Definition<Skill>[]): string {
|
|
349
|
-
return (
|
|
350
|
-
'<skills>\n' +
|
|
351
|
-
`${
|
|
352
|
-
skills
|
|
353
|
-
.filter(({ attributes: { always } }) => always !== false)
|
|
354
|
-
.map(
|
|
355
|
-
({ path, body, attributes }) => {
|
|
356
|
-
const name = resolveDocumentName(path, attributes.name, ['skill.md'])
|
|
357
|
-
const desc = resolveDocumentDescription(body, attributes.description, name)
|
|
358
|
-
return ` - ${name}:${desc}\n`
|
|
359
|
-
}
|
|
360
|
-
)
|
|
361
|
-
.join('')
|
|
362
|
-
}\n` +
|
|
363
|
-
'</skills>\n'
|
|
364
|
-
)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
async loadSpec(name: string): Promise<Definition<Spec> | undefined> {
|
|
368
|
-
const patterns = [
|
|
369
|
-
`.ai/specs/${name}.md`,
|
|
370
|
-
`.ai/specs/${name}/index.md`,
|
|
371
|
-
`.ai/plugins/*/specs/${name}.md`,
|
|
372
|
-
`.ai/plugins/*/specs/${name}/index.md`
|
|
373
|
-
]
|
|
374
|
-
const paths = await this.scan(patterns)
|
|
375
|
-
if (paths.length === 0) return undefined
|
|
376
|
-
|
|
377
|
-
const projectPath = paths.find(p => p.includes('/.ai/specs/'))
|
|
378
|
-
const targetPath = projectPath || paths[0]
|
|
379
|
-
|
|
380
|
-
const [doc] = await loadLocalDocuments<Spec>([targetPath])
|
|
381
|
-
return doc
|
|
382
|
-
}
|
|
383
|
-
async loadDefaultSpecs(): Promise<Definition<Spec>[]> {
|
|
384
|
-
const patterns = [
|
|
385
|
-
'.ai/specs/*.md',
|
|
386
|
-
'.ai/specs/*/index.md',
|
|
387
|
-
'.ai/plugins/*/specs/*.md',
|
|
388
|
-
'.ai/plugins/*/specs/*/index.md'
|
|
389
|
-
]
|
|
390
|
-
const paths = await this.scan(patterns)
|
|
391
|
-
return loadLocalDocuments<Spec>(paths)
|
|
392
|
-
}
|
|
393
|
-
generateSpecRoutePrompt(specsDocuments: Definition<Spec>[]): string {
|
|
394
|
-
const specsRouteStr = specsDocuments
|
|
395
|
-
.filter(({ attributes }) => attributes.always !== false)
|
|
396
|
-
.map(({ path, body, attributes }) => {
|
|
397
|
-
const name = resolveDocumentName(path, attributes.name, ['index.md'])
|
|
398
|
-
const desc = resolveDocumentDescription(body, attributes.description, name)
|
|
399
|
-
const identifier = resolveSpecIdentifier(path, attributes.name)
|
|
400
|
-
const params = attributes.params ?? []
|
|
401
|
-
const paramsPrompt = params.length > 0
|
|
402
|
-
? params
|
|
403
|
-
.map(({ name, description }) => ` - ${name}:${description ?? '无'}\n`)
|
|
404
|
-
.join('')
|
|
405
|
-
: ' - 无\n'
|
|
406
|
-
|
|
407
|
-
return (
|
|
408
|
-
`- 流程名称:${name}\n` +
|
|
409
|
-
` - 介绍:${desc}\n` +
|
|
410
|
-
` - 标识:${identifier}\n` +
|
|
411
|
-
' - 参数:\n' +
|
|
412
|
-
`${paramsPrompt}`
|
|
413
|
-
)
|
|
414
|
-
})
|
|
415
|
-
.join('\n')
|
|
416
|
-
return (
|
|
417
|
-
'<system-prompt>\n' +
|
|
418
|
-
'你是一个专业的项目推进管理大师,能够熟练指导其他实体来为你的目标工作。对你的预期是:\n' +
|
|
419
|
-
'\n' +
|
|
420
|
-
'- 永远不要单独完成代码开发工作\n' +
|
|
421
|
-
'- 必须要协调其他的开发人员来完成任务\n' +
|
|
422
|
-
'- 必须让他们按照目标进行完成,不要偏离目标,检查他们任务完成后的汇报内容是否符合要求\n' +
|
|
423
|
-
'\n' +
|
|
424
|
-
'根据用户需要以及实际的开发目标来决定使用不同的工作流程,调用 `load-spec` mcp tool 完成工作流程的加载。\n' +
|
|
425
|
-
'- 根据实际需求传入标识,这不是路径,只能使用工具进行加载\n' +
|
|
426
|
-
'- 通过参数的描述以及实际应用场景决定怎么传入参数\n' +
|
|
427
|
-
'项目存在如下工作流程:\n' +
|
|
428
|
-
`${specsRouteStr}\n` +
|
|
429
|
-
'</system-prompt>\n'
|
|
430
|
-
)
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
async loadEntity(name: string): Promise<Definition<Entity> | undefined> {
|
|
434
|
-
// 1. Try to load from index.json (Directory based entity)
|
|
435
|
-
const jsonPatterns = [
|
|
436
|
-
`.ai/entities/${name}/index.json`,
|
|
437
|
-
`.ai/plugins/*/entities/${name}/index.json`
|
|
438
|
-
]
|
|
439
|
-
const jsonPaths = await this.scan(jsonPatterns)
|
|
440
|
-
|
|
441
|
-
if (jsonPaths.length > 0) {
|
|
442
|
-
const projectPath = jsonPaths.find(p => p.includes('/.ai/entities/'))
|
|
443
|
-
const targetPath = projectPath || jsonPaths[0]
|
|
444
|
-
return this.loadEntityFromJson(targetPath)
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// 2. Fallback to Markdown file
|
|
448
|
-
const patterns = [
|
|
449
|
-
`.ai/entities/${name}.md`,
|
|
450
|
-
`.ai/entities/${name}/README.md`,
|
|
451
|
-
`.ai/plugins/*/entities/${name}.md`,
|
|
452
|
-
`.ai/plugins/*/entities/${name}/README.md`
|
|
453
|
-
]
|
|
454
|
-
const paths = await this.scan(patterns)
|
|
455
|
-
if (paths.length === 0) return undefined
|
|
456
|
-
|
|
457
|
-
const projectPath = paths.find(p => p.includes('/.ai/entities/'))
|
|
458
|
-
const targetPath = projectPath || paths[0]
|
|
459
|
-
|
|
460
|
-
const [doc] = await loadLocalDocuments<Entity>([targetPath])
|
|
461
|
-
return doc
|
|
462
|
-
}
|
|
463
|
-
private async loadEntityFromJson(jsonPath: string): Promise<Definition<Entity>> {
|
|
464
|
-
const entityDir = dirname(jsonPath)
|
|
465
|
-
const jsonVariables: Record<string, string> = {
|
|
466
|
-
workspaceFolder: process.env.WORKSPACE_FOLDER || this.cwd
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const content = await readFile(jsonPath, 'utf-8')
|
|
470
|
-
const entityJSONContent = content.replace(/\$\{([^}]+)\}/g, (_, key) => jsonVariables[key] ?? `$\{${key}}`)
|
|
471
|
-
|
|
472
|
-
const entityData = JSON.parse(entityJSONContent) as Entity
|
|
473
|
-
|
|
474
|
-
let prompt = entityData.prompt
|
|
475
|
-
if (!prompt) {
|
|
476
|
-
const promptPath = entityData.promptPath || 'AGENTS.md'
|
|
477
|
-
const resolvedPromptPath = resolve(entityDir, promptPath)
|
|
478
|
-
if (existsSync(resolvedPromptPath)) {
|
|
479
|
-
prompt = await readFile(resolvedPromptPath, 'utf-8')
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
return {
|
|
484
|
-
path: jsonPath,
|
|
485
|
-
body: prompt || '',
|
|
486
|
-
attributes: entityData
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
async loadDefaultEntities(): Promise<Definition<Entity>[]> {
|
|
490
|
-
// List both .md and index.json entities
|
|
491
|
-
const mdPatterns = [
|
|
492
|
-
'.ai/entities/*.md',
|
|
493
|
-
'.ai/entities/*/README.md',
|
|
494
|
-
'.ai/plugins/*/entities/*.md',
|
|
495
|
-
'.ai/plugins/*/entities/*/README.md'
|
|
496
|
-
]
|
|
497
|
-
const jsonPatterns = [
|
|
498
|
-
'.ai/entities/*/index.json',
|
|
499
|
-
'.ai/plugins/*/entities/*/index.json'
|
|
500
|
-
]
|
|
501
|
-
|
|
502
|
-
const [mdPaths, jsonPaths] = await Promise.all([
|
|
503
|
-
this.scan(mdPatterns),
|
|
504
|
-
this.scan(jsonPatterns)
|
|
505
|
-
])
|
|
506
|
-
|
|
507
|
-
const mdDocs = await loadLocalDocuments<Entity>(mdPaths)
|
|
508
|
-
const jsonDocs = await Promise.all(jsonPaths.map(p => this.loadEntityFromJson(p)))
|
|
509
|
-
|
|
510
|
-
return [...mdDocs, ...jsonDocs]
|
|
511
|
-
}
|
|
512
|
-
generateEntitiesRoutePrompt(entities: Definition<Entity>[]): string {
|
|
513
|
-
return (
|
|
514
|
-
'<system-prompt>\n' +
|
|
515
|
-
'项目存在如下实体:\n' +
|
|
516
|
-
`${
|
|
517
|
-
entities
|
|
518
|
-
.filter(({ attributes }) => attributes.always !== false)
|
|
519
|
-
.map(({ path, attributes, body }) => {
|
|
520
|
-
const name = resolveDocumentName(path, attributes.name, ['readme.md', 'index.json'])
|
|
521
|
-
const desc = resolveDocumentDescription(body, attributes.description, name)
|
|
522
|
-
return ` - ${name}:${desc}\n`
|
|
523
|
-
})
|
|
524
|
-
.join('')
|
|
525
|
-
}\n` +
|
|
526
|
-
'解决用户问题时,需根据用户需求可以通过 run-tasks 工具指定为实体后,自行调度多个不同类型的实体来完成工作。\n' +
|
|
527
|
-
'</system-prompt>\n'
|
|
528
|
-
)
|
|
529
|
-
}
|
|
530
|
-
}
|
package/src/utils/filter.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export const filterObject = <T extends Record<string, any>>(
|
|
2
|
-
obj: T,
|
|
3
|
-
rules: { include?: string[]; exclude?: string[] }
|
|
4
|
-
): Partial<T> => {
|
|
5
|
-
const { include = [], exclude = [] } = rules
|
|
6
|
-
|
|
7
|
-
if (include.length === 0 && exclude.length === 0) {
|
|
8
|
-
return { ...obj }
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const result: Partial<T> = {}
|
|
12
|
-
|
|
13
|
-
Object.keys(obj).forEach((key) => {
|
|
14
|
-
// If include list is provided, key MUST be in it
|
|
15
|
-
if (include.length > 0 && !include.includes(key)) {
|
|
16
|
-
return
|
|
17
|
-
}
|
|
18
|
-
// If key is in exclude list, it MUST NOT be in it
|
|
19
|
-
if (exclude.includes(key)) {
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
result[key as keyof T] = obj[key]
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
return result
|
|
26
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
export function kebabCase(str: string) {
|
|
2
|
-
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function camelCase(str: string) {
|
|
6
|
-
return str.replace(/[-_]([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function transformAllObjectKeys(
|
|
10
|
-
transformFn: (str: string) => string,
|
|
11
|
-
obj: unknown
|
|
12
|
-
): unknown {
|
|
13
|
-
const boundTransformFn = transformAllObjectKeys.bind(null, transformFn)
|
|
14
|
-
if (typeof obj !== 'object') return obj
|
|
15
|
-
if (Array.isArray(obj)) {
|
|
16
|
-
return obj.map(boundTransformFn)
|
|
17
|
-
}
|
|
18
|
-
if (obj === null) {
|
|
19
|
-
return null
|
|
20
|
-
}
|
|
21
|
-
const newObj: Record<string, unknown> = {}
|
|
22
|
-
for (const key in obj) {
|
|
23
|
-
newObj[transformFn(key)] = boundTransformFn(
|
|
24
|
-
// @ts-ignore
|
|
25
|
-
obj[key]
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
return newObj
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function transformKebabKey<T>(obj: unknown) {
|
|
32
|
-
return transformAllObjectKeys(kebabCase, obj) as T
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function transformCamelKey<T>(obj: unknown) {
|
|
36
|
-
return transformAllObjectKeys(camelCase, obj) as T
|
|
37
|
-
}
|