@vibe-forge/core 0.7.0 → 0.7.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-forge/core",
3
- "version": "0.7.0",
3
+ "version": "0.7.3",
4
4
  "imports": {
5
5
  "#~/*.js": {
6
6
  "__vibe-forge__": {
@@ -6,10 +6,18 @@ import type { ChatMessage, ChatMessageContent } from '../types'
6
6
 
7
7
  export type AdapterMessageContent = ChatMessageContent
8
8
 
9
+ export interface AdapterErrorData {
10
+ message: string
11
+ code?: string
12
+ details?: unknown
13
+ fatal?: boolean
14
+ }
15
+
9
16
  export type AdapterOutputEvent =
10
17
  | { type: 'init'; data: SessionInitInfo }
11
18
  | { type: 'summary'; data: SessionSummaryInfo }
12
19
  | { type: 'message'; data: ChatMessage }
20
+ | { type: 'error'; data: AdapterErrorData }
13
21
  | { type: 'exit'; data: { exitCode?: number; stderr?: string } }
14
22
  | { type: 'stop'; data?: ChatMessage }
15
23
 
@@ -34,6 +34,20 @@ export interface ModelServiceConfig {
34
34
  * 模型服务支持的模型别名
35
35
  */
36
36
  modelsAlias?: Record<string, string[]>
37
+ /**
38
+ * 模型服务超时(毫秒)。
39
+ * - Codex: 映射为 `stream_idle_timeout_ms`
40
+ * - Claude Code Router: 映射为全局 `API_TIMEOUT_MS`
41
+ * - OpenCode: 映射为 provider `timeout` / `chunkTimeout`
42
+ */
43
+ timeoutMs?: number
44
+ /**
45
+ * 模型服务默认最大输出 token。
46
+ * - Codex: 映射为 `turn/start.maxOutputTokens`
47
+ * - Claude Code Router: 映射为 `maxtoken` transformer
48
+ * - OpenCode: 映射为 model `options.maxOutputTokens` / `limit.output`
49
+ */
50
+ maxOutputTokens?: number
37
51
  /**
38
52
  * 拓展配置,由下游自行消费
39
53
  */
@@ -178,6 +192,12 @@ export interface Config {
178
192
  * 自定义对话风格。通过指定提示词约束对话风格。
179
193
  */
180
194
  customInstructions?: string
195
+ /**
196
+ * 是否注入 Vibe Forge 自动生成的默认系统提示词
197
+ * (例如 rules / skills / entities / specs 生成的提示词)。
198
+ * 默认为 true。
199
+ */
200
+ injectDefaultSystemPrompt?: boolean
181
201
  }
182
202
  /**
183
203
  * 插件配置
@@ -1,24 +1,122 @@
1
1
  import process from 'node:process'
2
+ import { basename, dirname } from 'node:path'
2
3
 
3
4
  import type { AdapterQueryOptions } from '#~/adapter/type.js'
4
5
  import { DefinitionLoader } from '#~/utils/definition-loader.js'
5
- import type { Definition, Filter, Skill } from '#~/utils/definition-loader.js'
6
+ import type { Definition, Entity, Filter, Skill, Spec } from '#~/utils/definition-loader.js'
7
+
8
+ const filterSkills = (
9
+ skills: Definition<Skill>[],
10
+ selection?: AdapterQueryOptions['skills']
11
+ ) => {
12
+ if (selection == null) return skills
13
+
14
+ const include = selection.include != null && selection.include.length > 0
15
+ ? new Set(selection.include)
16
+ : undefined
17
+ const exclude = new Set(selection.exclude ?? [])
18
+
19
+ return skills.filter((skill) => {
20
+ const name = basename(dirname(skill.path))
21
+ return (include == null || include.has(name)) && !exclude.has(name)
22
+ })
23
+ }
24
+
25
+ const dedupeSkills = (skills: Definition<Skill>[]) => {
26
+ const seen = new Set<string>()
27
+ return skills.filter((skill) => {
28
+ if (seen.has(skill.path)) return false
29
+ seen.add(skill.path)
30
+ return true
31
+ })
32
+ }
33
+
34
+ type SkillSelectionInput =
35
+ | AdapterQueryOptions['skills']
36
+ | Entity['skills']
37
+ | Spec['skills']
38
+
39
+ const toNormalizedSkillSelection = (
40
+ selection?: SkillSelectionInput
41
+ ): AdapterQueryOptions['skills'] | undefined => {
42
+ if (selection == null) return undefined
43
+
44
+ if (Array.isArray(selection)) {
45
+ return selection.length > 0
46
+ ? {
47
+ include: selection
48
+ }
49
+ : undefined
50
+ }
51
+
52
+ if ('type' in selection && Array.isArray(selection.list)) {
53
+ const list = selection.list.filter((item): item is string => typeof item === 'string')
54
+ return selection.type === 'include'
55
+ ? {
56
+ include: list
57
+ }
58
+ : {
59
+ exclude: list
60
+ }
61
+ }
62
+
63
+ return selection
64
+ }
65
+
66
+ const mergeSkillSelections = (
67
+ ...selections: Array<SkillSelectionInput | undefined>
68
+ ): AdapterQueryOptions['skills'] | undefined => {
69
+ let include: Set<string> | undefined
70
+ const exclude = new Set<string>()
71
+
72
+ for (const selection of selections) {
73
+ const normalized = toNormalizedSkillSelection(selection)
74
+ if (normalized == null) continue
75
+
76
+ if (normalized.include != null && normalized.include.length > 0) {
77
+ const current = new Set(normalized.include)
78
+ include = include == null
79
+ ? current
80
+ : new Set([...include].filter(item => current.has(item)))
81
+ }
82
+
83
+ for (const item of normalized.exclude ?? []) {
84
+ exclude.add(item)
85
+ }
86
+ }
87
+
88
+ if (include == null && exclude.size === 0) return undefined
89
+
90
+ return {
91
+ include: include == null ? undefined : [...include],
92
+ exclude: exclude.size === 0 ? undefined : [...exclude]
93
+ }
94
+ }
95
+
96
+ const getIncludedSkillNames = (selection?: SkillSelectionInput): string[] => {
97
+ const normalized = toNormalizedSkillSelection(selection)
98
+ return normalized?.include ?? []
99
+ }
6
100
 
7
101
  export async function generateAdapterQueryOptions(
8
102
  type: 'spec' | 'entity' | undefined,
9
103
  name?: string,
10
- cwd: string = process.cwd()
104
+ cwd: string = process.cwd(),
105
+ input?: {
106
+ skills?: AdapterQueryOptions['skills']
107
+ }
11
108
  ) {
12
109
  const loader = new DefinitionLoader(cwd)
13
110
  const options: Partial<AdapterQueryOptions> = {}
14
111
  const systemPromptParts: string[] = []
112
+ let effectiveSkillSelection = toNormalizedSkillSelection(input?.skills)
15
113
 
16
114
  // 1. 获取数据
17
115
  // 1.1 获取默认数据
18
116
  const entities = type !== 'entity'
19
117
  ? await loader.loadDefaultEntities()
20
118
  : []
21
- const skills = await loader.loadDefaultSkills()
119
+ const defaultSkills = await loader.loadDefaultSkills()
22
120
  const rules = await loader.loadDefaultRules()
23
121
  const specs = await loader.loadDefaultSpecs()
24
122
 
@@ -42,7 +140,9 @@ export async function generateAdapterQueryOptions(
42
140
  // always load spec or entity tagged rules
43
141
  rules.push(
44
142
  ...(
45
- await loader.loadRules(attributes.rules)
143
+ await loader.loadRules(attributes.rules, {
144
+ baseDir: dirname(data.path)
145
+ })
46
146
  ).map((rule) => ({
47
147
  ...rule,
48
148
  attributes: {
@@ -56,18 +156,40 @@ export async function generateAdapterQueryOptions(
56
156
  if (
57
157
  attributes.skills
58
158
  ) {
59
- targetSkills.push(...await loader.loadSkills(attributes.skills))
159
+ effectiveSkillSelection = mergeSkillSelections(
160
+ input?.skills,
161
+ attributes.skills
162
+ )
163
+ targetSkills.push(
164
+ ...filterSkills(
165
+ await loader.loadSkills(getIncludedSkillNames(attributes.skills)),
166
+ effectiveSkillSelection
167
+ )
168
+ )
60
169
  }
61
170
 
62
171
  targetBody = body
63
172
  targetToolsFilter = attributes.tools
64
173
  targetMcpServersFilter = attributes.mcpServers
65
174
  }
175
+ const skills = filterSkills(defaultSkills, effectiveSkillSelection)
176
+ let selectedSkillsPrompt: Definition<Skill>[] = []
177
+ if (input?.skills?.include != null && input.skills.include.length > 0) {
178
+ selectedSkillsPrompt = dedupeSkills(
179
+ filterSkills(
180
+ await loader.loadSkills(input.skills.include),
181
+ effectiveSkillSelection
182
+ )
183
+ )
184
+ }
66
185
 
67
186
  // 2. 基于数据生成上下文
68
187
  // 2.1 加载关联上下文
69
188
  systemPromptParts.push(loader.generateRulesPrompt(rules))
70
- systemPromptParts.push(loader.generateSkillsPrompt(targetSkills))
189
+ systemPromptParts.push(loader.generateSkillsPrompt(dedupeSkills(targetSkills)))
190
+ systemPromptParts.push(loader.generateSkillsPrompt(
191
+ selectedSkillsPrompt.filter(skill => !targetSkills.some(target => target.path === skill.path))
192
+ ))
71
193
  // 2.2 加载上下文路由
72
194
  systemPromptParts.push(loader.generateEntitiesRoutePrompt(entities))
73
195
  systemPromptParts.push(loader.generateSkillsRoutePrompt(skills))
@@ -36,6 +36,25 @@ export interface Spec {
36
36
  tools?: Filter
37
37
  }
38
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
+
39
58
  export interface Entity {
40
59
  name?: string
41
60
  always?: boolean
@@ -43,8 +62,8 @@ export interface Entity {
43
62
  tags?: string[]
44
63
  prompt?: string
45
64
  promptPath?: string
46
- rules?: string[]
47
- skills?: string[]
65
+ rules?: RuleReference[]
66
+ skills?: string[] | SkillSelection
48
67
  mcpServers?: Filter
49
68
  tools?: Filter
50
69
  }
@@ -123,6 +142,66 @@ const resolveSpecIdentifier = (path: string, explicitName?: string) => {
123
142
  return resolveDocumentName(path, explicitName, ['index.md'])
124
143
  }
125
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
+
126
205
  export class DefinitionLoader {
127
206
  private readonly cwd: string
128
207
 
@@ -142,10 +221,53 @@ export class DefinitionLoader {
142
221
  })
143
222
  }
144
223
 
145
- async loadRules(rules: string[]) {
146
- return loadLocalDocuments<Rule>(
147
- await this.scan(rules)
148
- )
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
149
271
  }
150
272
  async loadDefaultRules(): Promise<Definition<Rule>[]> {
151
273
  return this.loadRules([
package/src/ws.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { SessionInfo } from './adapter/index.js'
1
+ import type { AdapterErrorData, SessionInfo } from './adapter/index.js'
2
2
  import type { AskUserQuestionParams, ChatMessage } from './types.js'
3
3
 
4
4
  export type WSEvent =
5
- | { type: 'error'; message: string }
5
+ | { type: 'error'; data: AdapterErrorData; message?: string }
6
6
  | { type: 'message'; message: ChatMessage }
7
7
  | { type: 'session_info'; info: SessionInfo }
8
8
  | { type: 'tool_result'; toolCallId: string; output: any; isError: boolean }