@vibe-forge/core 0.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/LICENSE +21 -0
- package/package.json +73 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/loader.ts +9 -0
- package/src/adapter/type.ts +99 -0
- package/src/config.ts +375 -0
- package/src/controllers/config/index.ts +207 -0
- package/src/controllers/system/assets/completed.mp3 +0 -0
- package/src/controllers/system/assets/mcp.png +0 -0
- package/src/controllers/system/index.ts +102 -0
- package/src/controllers/task/generate-adapter-query-options.ts +86 -0
- package/src/controllers/task/index.ts +2 -0
- package/src/controllers/task/prepare.ts +60 -0
- package/src/controllers/task/run.ts +98 -0
- package/src/controllers/task/schema.ts +131 -0
- package/src/controllers/task/type.ts +6 -0
- package/src/env.ts +43 -0
- package/src/hooks/index.ts +37 -0
- package/src/hooks/loader.ts +75 -0
- package/src/hooks/type.ts +173 -0
- package/src/index.ts +9 -0
- package/src/schema.ts +13 -0
- package/src/types.ts +66 -0
- package/src/utils/cache.ts +51 -0
- package/src/utils/create-logger.ts +74 -0
- package/src/utils/definition-loader.ts +354 -0
- package/src/utils/filter.ts +26 -0
- package/src/utils/string-transform.ts +37 -0
- package/src/utils/uuid.ts +6 -0
- package/src/ws.ts +13 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import { basename, dirname, join, 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
|
+
/**
|
|
18
|
+
* 是否默认加载至系统上下文
|
|
19
|
+
*/
|
|
20
|
+
always?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Spec {
|
|
24
|
+
name?: string
|
|
25
|
+
always?: boolean
|
|
26
|
+
description?: string
|
|
27
|
+
tags?: string[]
|
|
28
|
+
params?: {
|
|
29
|
+
name: string
|
|
30
|
+
description?: string
|
|
31
|
+
}[]
|
|
32
|
+
rules?: string[]
|
|
33
|
+
skills?: string[]
|
|
34
|
+
mcpServers?: Filter
|
|
35
|
+
tools?: Filter
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Entity {
|
|
39
|
+
name?: string
|
|
40
|
+
always?: boolean
|
|
41
|
+
description?: string
|
|
42
|
+
tags?: string[]
|
|
43
|
+
prompt?: string
|
|
44
|
+
promptPath?: string
|
|
45
|
+
rules?: string[]
|
|
46
|
+
skills?: string[]
|
|
47
|
+
mcpServers?: Filter
|
|
48
|
+
tools?: Filter
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface Skill {
|
|
52
|
+
name?: string
|
|
53
|
+
description?: string
|
|
54
|
+
always?: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Definition<T> {
|
|
58
|
+
path: string
|
|
59
|
+
body: string
|
|
60
|
+
attributes: T
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 以结构化的方式加载本地文档数据
|
|
65
|
+
*/
|
|
66
|
+
export const loadLocalDocuments = async <Attrs extends object>(
|
|
67
|
+
paths: string[]
|
|
68
|
+
): Promise<Definition<Attrs>[]> => {
|
|
69
|
+
const promises = paths.map(async (path) => {
|
|
70
|
+
const content = await readFile(path, 'utf-8')
|
|
71
|
+
const { body, attributes } = fm<Attrs>(content)
|
|
72
|
+
return {
|
|
73
|
+
path,
|
|
74
|
+
body,
|
|
75
|
+
attributes
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
return Promise.all(promises)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class DefinitionLoader {
|
|
82
|
+
private readonly cwd: string
|
|
83
|
+
|
|
84
|
+
constructor(cwd: string = process.cwd()) {
|
|
85
|
+
this.cwd = cwd
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async scan(
|
|
89
|
+
patterns: string[],
|
|
90
|
+
cwd: string = this.cwd
|
|
91
|
+
): Promise<string[]> {
|
|
92
|
+
return glob(patterns, { cwd, absolute: true })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async loadRules(rules: string[]) {
|
|
96
|
+
return loadLocalDocuments<Rule>(
|
|
97
|
+
await this.scan(rules)
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
async loadDefaultRules(): Promise<Definition<Rule>[]> {
|
|
101
|
+
return this.loadRules([
|
|
102
|
+
'.ai/rules/*.md',
|
|
103
|
+
'.ai/plugins/*/rules/*.md'
|
|
104
|
+
])
|
|
105
|
+
}
|
|
106
|
+
generateRulesPrompt(rules: Definition<Rule>[]): string {
|
|
107
|
+
const rulesPrompt = rules
|
|
108
|
+
.map((rule) => {
|
|
109
|
+
const { path, body, attributes } = rule
|
|
110
|
+
const name = attributes.name ?? basename(path)
|
|
111
|
+
const desc = attributes.description ?? name
|
|
112
|
+
return (
|
|
113
|
+
` - ${name}:${desc}\n` +
|
|
114
|
+
`${attributes.always ? body : ''}\n` +
|
|
115
|
+
'--------------------\n'
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.join('\n')
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
'<system-prompt>\n' +
|
|
123
|
+
'项目系统规则如下:\n' +
|
|
124
|
+
`${rulesPrompt}\n` +
|
|
125
|
+
'</system-prompt>\n'
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async loadSkills(skills?: string[]): Promise<Definition<Skill>[]> {
|
|
130
|
+
// 1. Scan for skills in standard locations
|
|
131
|
+
// Project root skills: .ai/skills/{name}/SKILL.md
|
|
132
|
+
// Plugin skills: .ai/plugins/{plugin}/skills/{name}/SKILL.md
|
|
133
|
+
|
|
134
|
+
// Note: The user code uses readdir to iterate plugins.
|
|
135
|
+
// We can use glob for easier path finding.
|
|
136
|
+
|
|
137
|
+
const patterns = [
|
|
138
|
+
'.ai/skills/*/SKILL.md',
|
|
139
|
+
'.ai/plugins/*/skills/*/SKILL.md'
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
let paths = await this.scan(patterns)
|
|
143
|
+
|
|
144
|
+
// Filter by directory name (skill name)
|
|
145
|
+
if (skills) {
|
|
146
|
+
paths = paths.filter(path => {
|
|
147
|
+
const parts = path.split('/')
|
|
148
|
+
// .../skills/{name}/SKILL.md
|
|
149
|
+
return skills.includes(parts[parts.length - 2])
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return loadLocalDocuments<Skill>(paths)
|
|
154
|
+
}
|
|
155
|
+
async loadDefaultSkills(): Promise<Definition<Skill>[]> {
|
|
156
|
+
return this.loadSkills()
|
|
157
|
+
}
|
|
158
|
+
generateSkillsPrompt(skills: Definition<Skill>[]): string {
|
|
159
|
+
return skills
|
|
160
|
+
.map((skill) => {
|
|
161
|
+
const { path, body } = skill
|
|
162
|
+
return (
|
|
163
|
+
'技能相关信息如下,通过阅读以下内容了解技能的详细信息:\n' +
|
|
164
|
+
`- 技能文件资源路径:${dirname(path)}\n` +
|
|
165
|
+
'- 资源内容:\n' +
|
|
166
|
+
'<skill-content>\n' +
|
|
167
|
+
`${body}\n` +
|
|
168
|
+
'</skill-content>\n' +
|
|
169
|
+
'资源内容中的文件路径相对「技能文件资源路径」路径,通过读取相关工具按照实际需要进行阅读。\n'
|
|
170
|
+
)
|
|
171
|
+
})
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
.join('\n')
|
|
174
|
+
}
|
|
175
|
+
generateSkillsRoutePrompt(skills: Definition<Skill>[]): string {
|
|
176
|
+
return (
|
|
177
|
+
'<skills>\n' +
|
|
178
|
+
`${
|
|
179
|
+
skills
|
|
180
|
+
.filter(({ attributes: { always } }) => always !== false)
|
|
181
|
+
.map(
|
|
182
|
+
({ attributes: { name, description } }) => ` - ${name}:${description}\n`
|
|
183
|
+
)
|
|
184
|
+
.join('')
|
|
185
|
+
}\n` +
|
|
186
|
+
'</skills>\n'
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async loadSpec(name: string): Promise<Definition<Spec> | undefined> {
|
|
191
|
+
const patterns = [
|
|
192
|
+
`.ai/specs/${name}.md`,
|
|
193
|
+
`.ai/specs/${name}/index.md`,
|
|
194
|
+
`.ai/plugins/*/specs/${name}.md`
|
|
195
|
+
]
|
|
196
|
+
const paths = await this.scan(patterns)
|
|
197
|
+
if (paths.length === 0) return undefined
|
|
198
|
+
|
|
199
|
+
const projectPath = paths.find(p => p.includes('/.ai/specs/'))
|
|
200
|
+
const targetPath = projectPath || paths[0]
|
|
201
|
+
|
|
202
|
+
const [doc] = await loadLocalDocuments<Spec>([targetPath])
|
|
203
|
+
return doc
|
|
204
|
+
}
|
|
205
|
+
async loadDefaultSpecs(): Promise<Definition<Spec>[]> {
|
|
206
|
+
const patterns = [
|
|
207
|
+
'.ai/specs/*.md',
|
|
208
|
+
'.ai/specs/*/index.md',
|
|
209
|
+
'.ai/plugins/*/specs/*.md',
|
|
210
|
+
'.ai/plugins/*/specs/*/index.md'
|
|
211
|
+
]
|
|
212
|
+
const paths = await this.scan(patterns)
|
|
213
|
+
return loadLocalDocuments<Spec>(paths)
|
|
214
|
+
}
|
|
215
|
+
generateSpecRoutePrompt(specsDocuments: Definition<Spec>[]): string {
|
|
216
|
+
const specsRouteStr = specsDocuments
|
|
217
|
+
.filter(({ attributes }) => attributes.always !== false)
|
|
218
|
+
.map(({ path, attributes }) => {
|
|
219
|
+
const name = attributes.name ?? basename(dirname(path))
|
|
220
|
+
const desc = attributes.description ?? name
|
|
221
|
+
const params = attributes.params ?? []
|
|
222
|
+
// Calculate relative path for display/ID
|
|
223
|
+
// User code used relative('.ai/specs', path), but here path is absolute.
|
|
224
|
+
// We can try to make it relative to cwd/.ai/specs if possible, or just relative to cwd.
|
|
225
|
+
// The user code seems to assume specs are in .ai/specs.
|
|
226
|
+
// Let's use relative(join(this.cwd, '.ai/specs'), path) if it's in there, otherwise...
|
|
227
|
+
// Actually, just providing a relative path from project root is probably fine or the name.
|
|
228
|
+
// User code: relative('.ai/specs', path)
|
|
229
|
+
|
|
230
|
+
let relPath = relative(join(this.cwd, '.ai/specs'), path)
|
|
231
|
+
if (relPath.startsWith('..')) {
|
|
232
|
+
// Maybe in a plugin?
|
|
233
|
+
relPath = relative(this.cwd, path)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
`- 流程名称:${name}\n` +
|
|
238
|
+
` - 介绍:${desc}\n` +
|
|
239
|
+
` - 标识:${relPath}\n` +
|
|
240
|
+
' - 参数:\n' +
|
|
241
|
+
`${
|
|
242
|
+
params
|
|
243
|
+
.map(({ name, description }) => ` - ${name}:${description}\n`)
|
|
244
|
+
.join('')
|
|
245
|
+
}\n`
|
|
246
|
+
)
|
|
247
|
+
})
|
|
248
|
+
.join('\n')
|
|
249
|
+
return (
|
|
250
|
+
'<system-prompt>\n' +
|
|
251
|
+
'你是一个专业的项目推进管理大师,能够熟练指导其他实体来为你的目标工作。对你的预期是:\n' +
|
|
252
|
+
'\n' +
|
|
253
|
+
'- 永远不要单独完成代码开发工作\n' +
|
|
254
|
+
'- 必须要协调其他的开发人员来完成任务\n' +
|
|
255
|
+
'- 必须让他们按照目标进行完成,不要偏离目标,检查他们任务完成后的汇报内容是否符合要求\n' +
|
|
256
|
+
'\n' +
|
|
257
|
+
'根据用户需要以及实际的开发目标来决定使用不同的工作流程,调用 `mcp__TmarAITools__load-spec` mcp tool 完成工作流程的加载。\n' +
|
|
258
|
+
'- 根据实际需求传入标识,这不是路径,只能使用工具进行加载\n' +
|
|
259
|
+
'- 通过参数的描述以及实际应用场景决定怎么传入参数\n' +
|
|
260
|
+
'项目存在如下工作流程:\n' +
|
|
261
|
+
`${specsRouteStr}\n` +
|
|
262
|
+
'</system-prompt>\n'
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async loadEntity(name: string): Promise<Definition<Entity> | undefined> {
|
|
267
|
+
// 1. Try to load from index.json (Directory based entity)
|
|
268
|
+
const jsonPatterns = [
|
|
269
|
+
`.ai/entities/${name}/index.json`,
|
|
270
|
+
`.ai/plugins/*/entities/${name}/index.json`
|
|
271
|
+
]
|
|
272
|
+
const jsonPaths = await this.scan(jsonPatterns)
|
|
273
|
+
|
|
274
|
+
if (jsonPaths.length > 0) {
|
|
275
|
+
const projectPath = jsonPaths.find(p => p.includes('/.ai/entities/'))
|
|
276
|
+
const targetPath = projectPath || jsonPaths[0]
|
|
277
|
+
return this.loadEntityFromJson(targetPath)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 2. Fallback to Markdown file
|
|
281
|
+
const patterns = [
|
|
282
|
+
`.ai/entities/${name}/README.md`,
|
|
283
|
+
`.ai/plugins/*/entities/${name}/README.md`
|
|
284
|
+
]
|
|
285
|
+
const paths = await this.scan(patterns)
|
|
286
|
+
if (paths.length === 0) return undefined
|
|
287
|
+
|
|
288
|
+
const projectPath = paths.find(p => p.includes('/.ai/entities/'))
|
|
289
|
+
const targetPath = projectPath || paths[0]
|
|
290
|
+
|
|
291
|
+
const [doc] = await loadLocalDocuments<Entity>([targetPath])
|
|
292
|
+
return doc
|
|
293
|
+
}
|
|
294
|
+
private async loadEntityFromJson(jsonPath: string): Promise<Definition<Entity>> {
|
|
295
|
+
const entityDir = dirname(jsonPath)
|
|
296
|
+
const jsonVariables: Record<string, string> = {
|
|
297
|
+
workspaceFolder: process.env.WORKSPACE_FOLDER || this.cwd
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const content = await readFile(jsonPath, 'utf-8')
|
|
301
|
+
const entityJSONContent = content.replace(/\$\{([^}]+)\}/g, (_, key) => jsonVariables[key] ?? `$\{${key}}`)
|
|
302
|
+
|
|
303
|
+
const entityData = JSON.parse(entityJSONContent) as Entity
|
|
304
|
+
|
|
305
|
+
let prompt = entityData.prompt
|
|
306
|
+
if (!prompt) {
|
|
307
|
+
const promptPath = entityData.promptPath || 'AGENTS.md'
|
|
308
|
+
const resolvedPromptPath = resolve(entityDir, promptPath)
|
|
309
|
+
if (existsSync(resolvedPromptPath)) {
|
|
310
|
+
prompt = await readFile(resolvedPromptPath, 'utf-8')
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
path: jsonPath,
|
|
316
|
+
body: prompt || '',
|
|
317
|
+
attributes: entityData
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async loadDefaultEntities(): Promise<Definition<Entity>[]> {
|
|
321
|
+
// List both .md and index.json entities
|
|
322
|
+
const mdPatterns = [
|
|
323
|
+
'.ai/entities/*.md',
|
|
324
|
+
'.ai/plugins/*/entities/*.md'
|
|
325
|
+
]
|
|
326
|
+
const jsonPatterns = [
|
|
327
|
+
'.ai/entities/*/index.json',
|
|
328
|
+
'.ai/plugins/*/entities/*/index.json'
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
const [mdPaths, jsonPaths] = await Promise.all([
|
|
332
|
+
this.scan(mdPatterns),
|
|
333
|
+
this.scan(jsonPatterns)
|
|
334
|
+
])
|
|
335
|
+
|
|
336
|
+
const mdDocs = await loadLocalDocuments<Entity>(mdPaths)
|
|
337
|
+
const jsonDocs = await Promise.all(jsonPaths.map(p => this.loadEntityFromJson(p)))
|
|
338
|
+
|
|
339
|
+
return [...mdDocs, ...jsonDocs]
|
|
340
|
+
}
|
|
341
|
+
generateEntitiesRoutePrompt(entities: Definition<Entity>[]): string {
|
|
342
|
+
return (
|
|
343
|
+
'<system-prompt>\n' +
|
|
344
|
+
'项目存在如下实体:\n' +
|
|
345
|
+
`${
|
|
346
|
+
entities
|
|
347
|
+
.map(({ attributes: { name, prompt: _p }, body }) => ` - ${name}:${body}\n`)
|
|
348
|
+
.join('')
|
|
349
|
+
}\n` +
|
|
350
|
+
'解决用户问题时,需根据用户需求可以通过 run-tasks 工具指定为实体后,自行调度多个不同类型的实体来完成工作。\n' +
|
|
351
|
+
'</system-prompt>\n'
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
}
|
package/src/ws.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SessionInfo } from './adapter/index.js'
|
|
2
|
+
import type { AskUserQuestionParams, ChatMessage } from './types.js'
|
|
3
|
+
|
|
4
|
+
export type WSEvent =
|
|
5
|
+
| { type: 'error'; message: string }
|
|
6
|
+
| { type: 'message'; message: ChatMessage }
|
|
7
|
+
| { type: 'session_info'; info: SessionInfo }
|
|
8
|
+
| { type: 'tool_result'; toolCallId: string; output: any; isError: boolean }
|
|
9
|
+
| { type: 'adapter_result'; result: any; usage?: any }
|
|
10
|
+
| { type: 'adapter_event'; data: any }
|
|
11
|
+
| { type: 'session_updated'; session: any }
|
|
12
|
+
| { type: 'interaction_request'; id: string; payload: AskUserQuestionParams }
|
|
13
|
+
| { type: 'interaction_response'; id: string; data: string | string[] }
|