@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.
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ export const uuid = () =>
2
+ 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
3
+ const r = (Math.random() * 16) | 0
4
+ const v = c === 'x' ? r : (r & 0x3) | 0x8
5
+ return v.toString(16)
6
+ })
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[] }