@vibe-forge/core 0.3.0 → 0.5.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/src/config.ts CHANGED
@@ -1,375 +1,2 @@
1
- import { existsSync } from 'node:fs'
2
- import { readFile } from 'node:fs/promises'
3
- import { resolve } from 'node:path'
4
- import process from 'node:process'
5
-
6
- import { load } from 'js-yaml'
7
-
8
- import type { PluginConfig } from './hooks'
9
-
10
- export interface AdapterMap {}
11
-
12
- export interface ModelServiceConfig {
13
- /**
14
- * 模型服务展示标题
15
- */
16
- title?: string
17
- /**
18
- * 模型服务展示描述
19
- */
20
- description?: string
21
- /**
22
- * 模型服务 API 基础 URL
23
- */
24
- apiBaseUrl: string
25
- /**
26
- * 模型服务 API 密钥
27
- */
28
- apiKey: string
29
- /**
30
- * 模型服务支持的模型列表
31
- */
32
- models?: string[]
33
- /**
34
- * 模型服务支持的模型别名
35
- */
36
- modelsAlias?: Record<string, string[]>
37
- /**
38
- * 拓展配置,由下游自行消费
39
- */
40
- extra?: Record<string, unknown>
41
- }
42
-
43
- export interface RecommendedModelConfig {
44
- service?: string
45
- model: string
46
- title?: string
47
- description?: string
48
- placement?: 'modelSelector'
49
- }
50
-
51
- export type LanguageCode = 'zh' | 'en'
52
-
53
- export type NotificationTrigger = 'completed' | 'failed' | 'terminated' | 'waiting_input'
54
-
55
- export interface NotificationEventConfig {
56
- title?: string
57
- description?: string
58
- disabled?: boolean
59
- sound?: string
60
- }
61
-
62
- export interface NotificationConfig {
63
- disabled?: boolean
64
- volume?: number
65
- events?: Partial<Record<NotificationTrigger, NotificationEventConfig>>
66
- }
67
-
68
- export interface Config {
69
- /**
70
- * 配置目录
71
- */
72
- baseDir?: string
73
- /**
74
- * 适配器配置
75
- */
76
- adapters?: Partial<AdapterMap>
77
- /**
78
- * 默认适配器名称
79
- */
80
- defaultAdapter?: keyof AdapterMap
81
- /**
82
- * 模型服务配置
83
- */
84
- modelServices?: Record<string, ModelServiceConfig>
85
- /**
86
- * 默认模型服务名称
87
- */
88
- defaultModelService?: string
89
- /**
90
- * 默认模型名称
91
- */
92
- defaultModel?: string
93
- recommendedModels?: RecommendedModelConfig[]
94
- interfaceLanguage?: LanguageCode
95
- modelLanguage?: LanguageCode
96
- /**
97
- * MCP 服务器配置
98
- */
99
- mcpServers?: Record<
100
- string,
101
- & {
102
- /**
103
- * 是否启用
104
- */
105
- enabled?: boolean
106
- /**
107
- * 环境变量配置
108
- */
109
- env?: Record<string, string>
110
- }
111
- & (
112
- | {
113
- type?: undefined
114
- command: string
115
- args: string[]
116
- }
117
- | {
118
- type: 'sse'
119
- url: string
120
- headers: Record<string, string>
121
- }
122
- | {
123
- type: 'http'
124
- url: string
125
- headers?: Record<string, string>
126
- }
127
- )
128
- >
129
- /**
130
- * 默认启用的 MCP 服务器列表
131
- */
132
- defaultIncludeMcpServers?: string[]
133
- /**
134
- * 默认禁用的 MCP 服务器列表
135
- */
136
- defaultExcludeMcpServers?: string[]
137
- noDefaultVibeForgeMcpServer?: boolean
138
- /**
139
- * 权限配置
140
- */
141
- permissions?: {
142
- allow?: string[]
143
- deny?: string[]
144
- ask?: string[]
145
- }
146
- /**
147
- * 环境变量配置
148
- */
149
- env?: Record<string, string>
150
- /**
151
- * 公告配置
152
- */
153
- announcements?: string[]
154
- /**
155
- * 快捷键配置
156
- */
157
- shortcuts?: {
158
- newSession?: string
159
- openConfig?: string
160
- }
161
- notifications?: NotificationConfig
162
- /**
163
- * 会话配置
164
- */
165
- conversation?: {
166
- /**
167
- * 对话风格
168
- * - `friendly`: 友好的对话风格,适合用户与助手交互
169
- * - `programmatic`: 程序化的对话风格,适合助手执行任务
170
- */
171
- style?: 'friendly' | 'programmatic'
172
- /**
173
- * 自定义对话风格。通过指定提示词约束对话风格。
174
- */
175
- customInstructions?: string
176
- }
177
- /**
178
- * 插件配置
179
- */
180
- plugins?: PluginConfig
181
- enabledPlugins?: Record<string, boolean>
182
- extraKnownMarketplaces?: Record<
183
- string,
184
- {
185
- source:
186
- | {
187
- source: 'github'
188
- repo: string
189
- }
190
- | {
191
- source: 'git'
192
- url: string
193
- }
194
- | {
195
- source: 'directory'
196
- path: string
197
- }
198
- }
199
- >
200
- }
201
-
202
- export interface AboutInfo {
203
- version?: string
204
- lastReleaseAt?: string
205
- urls?: {
206
- repo?: string
207
- docs?: string
208
- contact?: string
209
- issues?: string
210
- releases?: string
211
- }
212
- }
213
-
214
- export interface ConfigSection {
215
- general?: {
216
- baseDir?: Config['baseDir']
217
- defaultAdapter?: Config['defaultAdapter']
218
- defaultModelService?: Config['defaultModelService']
219
- defaultModel?: Config['defaultModel']
220
- recommendedModels?: Config['recommendedModels']
221
- interfaceLanguage?: Config['interfaceLanguage']
222
- modelLanguage?: Config['modelLanguage']
223
- announcements?: Config['announcements']
224
- permissions?: Config['permissions']
225
- env?: Config['env']
226
- notifications?: Config['notifications']
227
- }
228
- conversation?: Config['conversation']
229
- modelServices?: Config['modelServices']
230
- adapters?: Config['adapters']
231
- plugins?: {
232
- plugins?: Config['plugins']
233
- enabledPlugins?: Config['enabledPlugins']
234
- extraKnownMarketplaces?: Config['extraKnownMarketplaces']
235
- }
236
- mcp?: {
237
- mcpServers?: Config['mcpServers']
238
- defaultIncludeMcpServers?: Config['defaultIncludeMcpServers']
239
- defaultExcludeMcpServers?: Config['defaultExcludeMcpServers']
240
- noDefaultVibeForgeMcpServer?: Config['noDefaultVibeForgeMcpServer']
241
- }
242
- shortcuts?: Config['shortcuts']
243
- }
244
-
245
- export interface ConfigResponse {
246
- sources?: {
247
- project?: ConfigSection
248
- user?: ConfigSection
249
- merged?: ConfigSection
250
- }
251
- meta?: {
252
- workspaceFolder?: string
253
- configPresent?: {
254
- project?: boolean
255
- user?: boolean
256
- }
257
- experiments?: Record<string, unknown>
258
- about?: AboutInfo
259
- }
260
- }
261
-
262
- export const defineConfig = (config: Config) => config
263
-
264
- const loadJSConfig = async (paths: string[]) => {
265
- for (const path of paths) {
266
- try {
267
- const configPath = resolve(process.cwd(), path)
268
- if (!existsSync(configPath)) {
269
- continue
270
- }
271
- // eslint-disable-next-line ts/no-require-imports
272
- return (require(configPath)?.default ?? {}) as Config
273
- } catch (e) {
274
- console.error(`Failed to load config file ${path}: ${e}`)
275
- }
276
- }
277
- }
278
-
279
- const loadJSONConfig = async (paths: string[], jsonVariables: Record<string, string | null | undefined>) => {
280
- for (const path of paths) {
281
- try {
282
- const configPath = resolve(process.cwd(), path)
283
- if (!existsSync(configPath)) {
284
- continue
285
- }
286
- const configContent = await readFile(configPath, 'utf-8')
287
- const configResolvedContent = configContent
288
- .replace(/\$\{(\w+)\}/g, (_, key) => jsonVariables[key] ?? `$\{${key}}`)
289
- return JSON.parse(configResolvedContent) as Config
290
- } catch (e) {
291
- console.error(`Failed to load config file ${path}: ${e}`)
292
- }
293
- }
294
- }
295
-
296
- const loadYAMLConfig = async (paths: string[], jsonVariables: Record<string, string | null | undefined>) => {
297
- for (const path of paths) {
298
- try {
299
- const configPath = resolve(process.cwd(), path)
300
- if (!existsSync(configPath)) {
301
- continue
302
- }
303
- const configContent = await readFile(configPath, 'utf-8')
304
- const configResolvedContent = configContent
305
- .replace(/\$\{(\w+)\}/g, (_, key) => jsonVariables[key] ?? `$\{${key}}`)
306
- return load(configResolvedContent) as Config
307
- } catch (e) {
308
- console.error(`Failed to load config file ${path}: ${e}`)
309
- }
310
- }
311
- }
312
-
313
- let configCache: Promise<readonly [Config | undefined, Config | undefined]> | null = null
314
-
315
- export const resetConfigCache = () => {
316
- configCache = null
317
- }
318
-
319
- export const loadConfig = (options: {
320
- jsonVariables?: Record<string, string | null | undefined>
321
- }) => {
322
- if (configCache) {
323
- return configCache
324
- }
325
-
326
- configCache = (async () =>
327
- [
328
- await loadJSONConfig(
329
- [
330
- './.ai.config.json',
331
- './infra/.ai.config.json'
332
- ],
333
- options.jsonVariables ?? {}
334
- ) ??
335
- await loadYAMLConfig(
336
- [
337
- './.ai.config.yaml',
338
- './.ai.config.yml',
339
- './infra/.ai.config.yaml',
340
- './infra/.ai.config.yml'
341
- ],
342
- options.jsonVariables ?? {}
343
- ),
344
- await loadJSONConfig(
345
- [
346
- './.ai.dev.config.json',
347
- './infra/.ai.dev.config.json'
348
- ],
349
- options.jsonVariables ?? {}
350
- ) ??
351
- await loadYAMLConfig(
352
- [
353
- './.ai.dev.config.yaml',
354
- './.ai.dev.config.yml',
355
- './infra/.ai.dev.config.yaml',
356
- './infra/.ai.dev.config.yml'
357
- ],
358
- options.jsonVariables ?? {}
359
- )
360
- ] as const)()
361
- return configCache
362
- }
363
-
364
- export const loadAdapterConfig = async <
365
- K extends keyof AdapterMap,
366
- >(
367
- name: K,
368
- options: { jsonVariables?: Record<string, string> }
369
- ) => {
370
- const [projectConfig, userConfig] = await loadConfig(options)
371
- return {
372
- ...(projectConfig?.adapters?.[name] ?? {}),
373
- ...(userConfig?.adapters?.[name] ?? {})
374
- } as unknown as NonNullable<Config['adapters']>[K]
375
- }
1
+ export * from './config/load'
2
+ export * from './config/types'
@@ -0,0 +1,89 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { relative, resolve } from 'node:path'
3
+ import process from 'node:process'
4
+
5
+ import { glob } from 'fast-glob'
6
+ import fm from 'front-matter'
7
+
8
+ import type { BenchmarkCase, BenchmarkCategory, BenchmarkCaseSelector, BenchmarkListOptions } from './types'
9
+ import { BenchmarkFrontmatterSchema } from './schema'
10
+ import { readBenchmarkResult } from './result-store'
11
+
12
+ export const resolveBenchmarkRoot = (workspaceFolder = process.cwd()) => resolve(workspaceFolder, '.ai/benchmark')
13
+
14
+ const resolveSummary = (body: string) => {
15
+ const lines = body
16
+ .split('\n')
17
+ .map(line => line.trim())
18
+ .filter(Boolean)
19
+ return lines[0] ?? ''
20
+ }
21
+
22
+ export const listBenchmarkCases = async (options: BenchmarkListOptions = {}): Promise<BenchmarkCase[]> => {
23
+ const workspaceFolder = options.workspaceFolder ?? process.cwd()
24
+ const benchmarkRoot = resolveBenchmarkRoot(workspaceFolder)
25
+ const pattern = options.category == null ? '*/*/rfc.md' : `${options.category}/*/rfc.md`
26
+ const rfcPaths = await glob(pattern, {
27
+ cwd: benchmarkRoot,
28
+ absolute: true
29
+ })
30
+
31
+ const cases = await Promise.all(rfcPaths.map(async (rfcPath) => {
32
+ const relativePath = relative(benchmarkRoot, rfcPath).split('\\').join('/')
33
+ const [category, title] = relativePath.split('/')
34
+ const caseDir = resolve(benchmarkRoot, category, title)
35
+ const rfcRaw = await readFile(rfcPath, 'utf-8')
36
+ const { body, attributes } = fm<Record<string, unknown>>(rfcRaw)
37
+ const frontmatter = BenchmarkFrontmatterSchema.parse(attributes)
38
+ const latestResult = await readBenchmarkResult(workspaceFolder, category, title)
39
+
40
+ return {
41
+ id: `${category}/${title}`,
42
+ category,
43
+ title,
44
+ caseDir,
45
+ rfcPath,
46
+ patchPath: resolve(caseDir, 'patch.diff'),
47
+ patchTestPath: resolve(caseDir, 'patch.test.diff'),
48
+ rfcBody: body.trim(),
49
+ rfcRaw,
50
+ summary: resolveSummary(body),
51
+ frontmatter,
52
+ latestResult
53
+ } satisfies BenchmarkCase
54
+ }))
55
+
56
+ return cases.sort((a, b) => a.id.localeCompare(b.id))
57
+ }
58
+
59
+ export const listBenchmarkCategories = async (options: BenchmarkListOptions = {}): Promise<BenchmarkCategory[]> => {
60
+ const cases = await listBenchmarkCases(options)
61
+ const categoryMap = new Map<string, BenchmarkCategory>()
62
+
63
+ for (const item of cases) {
64
+ const existing = categoryMap.get(item.category) ?? {
65
+ category: item.category,
66
+ caseCount: 0,
67
+ lastStatuses: {
68
+ pass: 0,
69
+ partial: 0,
70
+ fail: 0
71
+ }
72
+ }
73
+ existing.caseCount += 1
74
+ if (item.latestResult != null) {
75
+ existing.lastStatuses[item.latestResult.status] += 1
76
+ }
77
+ categoryMap.set(item.category, existing)
78
+ }
79
+
80
+ return [...categoryMap.values()].sort((a, b) => a.category.localeCompare(b.category))
81
+ }
82
+
83
+ export const getBenchmarkCase = async (input: BenchmarkCaseSelector) => {
84
+ const items = await listBenchmarkCases({
85
+ workspaceFolder: input.workspaceFolder,
86
+ category: input.category
87
+ })
88
+ return items.find(item => item.title === input.title) ?? null
89
+ }
@@ -0,0 +1,24 @@
1
+ export { getBenchmarkCase, listBenchmarkCases, listBenchmarkCategories } from './discover'
2
+ export { listBenchmarkResults, readBenchmarkResult, resolveBenchmarkResultPath, writeBenchmarkResult } from './result-store'
3
+ export { runBenchmarkCase, runBenchmarkCategory } from './runner'
4
+ export { BenchmarkFrontmatterSchema, BenchmarkResultSchema, BenchmarkRunSummarySchema } from './schema'
5
+ export type {
6
+ BenchmarkFrontmatter,
7
+ BenchmarkResult,
8
+ BenchmarkRunSummary,
9
+ BenchmarkScores,
10
+ BenchmarkStatus
11
+ } from './schema'
12
+ export type {
13
+ BenchmarkAgentOptions,
14
+ BenchmarkCase,
15
+ BenchmarkCaseSelector,
16
+ BenchmarkCategory,
17
+ BenchmarkListOptions,
18
+ BenchmarkPermissionMode,
19
+ BenchmarkRunCaseInput,
20
+ BenchmarkRunCaseOutput,
21
+ BenchmarkRunCategoryInput,
22
+ BenchmarkRunCategoryOutput,
23
+ BenchmarkRunEvent
24
+ } from './types'
@@ -0,0 +1,46 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises'
2
+ import { dirname, resolve } from 'node:path'
3
+ import process from 'node:process'
4
+
5
+ import { glob } from 'fast-glob'
6
+
7
+ import { BenchmarkResultSchema, type BenchmarkResult } from './schema'
8
+ import { readTextIfExists } from './utils'
9
+
10
+ export const resolveBenchmarkResultPath = (workspaceFolder: string, category: string, title: string) =>
11
+ resolve(workspaceFolder, '.ai/results', category, title, 'result.json')
12
+
13
+ export const readBenchmarkResult = async (workspaceFolder: string, category: string, title: string) => {
14
+ const resultPath = resolveBenchmarkResultPath(workspaceFolder, category, title)
15
+ const raw = await readTextIfExists(resultPath)
16
+ if (raw == null) return null
17
+ const parsed = JSON.parse(raw) as unknown
18
+ return BenchmarkResultSchema.parse(parsed)
19
+ }
20
+
21
+ export const writeBenchmarkResult = async (workspaceFolder: string, result: BenchmarkResult) => {
22
+ const resultPath = resolveBenchmarkResultPath(workspaceFolder, result.category, result.title)
23
+ await mkdir(dirname(resultPath), { recursive: true })
24
+ await writeFile(resultPath, `${JSON.stringify(result, null, 2)}\n`, 'utf-8')
25
+ return resultPath
26
+ }
27
+
28
+ export const listBenchmarkResults = async (workspaceFolder = process.cwd(), category?: string) => {
29
+ const pattern = category == null
30
+ ? '.ai/results/*/*/result.json'
31
+ : `.ai/results/${category}/*/result.json`
32
+ const resultPaths = await glob(pattern, {
33
+ cwd: workspaceFolder,
34
+ absolute: true
35
+ })
36
+
37
+ const results = await Promise.all(resultPaths.map(async (resultPath) => {
38
+ const raw = await readTextIfExists(resultPath)
39
+ if (raw == null) return null
40
+ return BenchmarkResultSchema.parse(JSON.parse(raw) as unknown)
41
+ }))
42
+
43
+ return results
44
+ .filter((result): result is BenchmarkResult => result != null)
45
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
46
+ }