@vibe-forge/workspace-assets 0.8.0 → 0.8.4
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/AGENTS.md +7 -2
- package/__tests__/__snapshots__/workspace-assets-rich.snapshot.json +845 -0
- package/__tests__/adapter-asset-plan.spec.ts +170 -0
- package/__tests__/bundle.spec.ts +212 -0
- package/__tests__/prompt-selection.spec.ts +388 -0
- package/__tests__/snapshot.ts +276 -0
- package/__tests__/test-helpers.ts +33 -0
- package/__tests__/workspace-assets.snapshot.spec.ts +204 -0
- package/package.json +5 -5
- package/src/index.ts +1368 -3
- package/src/internal-types.ts +1 -39
- package/__tests__/workspace-assets.spec.ts +0 -279
- package/src/adapter-asset-plan.ts +0 -174
- package/src/bundle.ts +0 -185
- package/src/document-assets.ts +0 -201
- package/src/helpers.ts +0 -35
- package/src/plugin-assets.ts +0 -178
- package/src/prompt-selection.ts +0 -151
package/src/index.ts
CHANGED
|
@@ -1,3 +1,1368 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { basename, dirname, extname, resolve } from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_VIBE_FORGE_MCP_SERVER_NAME,
|
|
7
|
+
buildConfigJsonVariables,
|
|
8
|
+
loadConfig,
|
|
9
|
+
resolveDefaultVibeForgeMcpServerConfig
|
|
10
|
+
} from '@vibe-forge/config'
|
|
11
|
+
import type {
|
|
12
|
+
AdapterAssetPlan,
|
|
13
|
+
AdapterOverlayEntry,
|
|
14
|
+
AssetDiagnostic,
|
|
15
|
+
Config,
|
|
16
|
+
Definition,
|
|
17
|
+
Entity,
|
|
18
|
+
Filter,
|
|
19
|
+
PluginConfig,
|
|
20
|
+
PluginOverlayConfig,
|
|
21
|
+
Rule,
|
|
22
|
+
RuleReference,
|
|
23
|
+
Skill,
|
|
24
|
+
SkillSelection,
|
|
25
|
+
Spec,
|
|
26
|
+
WorkspaceAsset,
|
|
27
|
+
WorkspaceAssetAdapter,
|
|
28
|
+
WorkspaceAssetBundle,
|
|
29
|
+
WorkspaceAssetKind,
|
|
30
|
+
WorkspaceMcpSelection,
|
|
31
|
+
WorkspaceSkillSelection
|
|
32
|
+
} from '@vibe-forge/types'
|
|
33
|
+
import {
|
|
34
|
+
normalizePath,
|
|
35
|
+
resolveDocumentName,
|
|
36
|
+
resolvePromptPath,
|
|
37
|
+
resolveRelativePath,
|
|
38
|
+
resolveSpecIdentifier
|
|
39
|
+
} from '@vibe-forge/utils'
|
|
40
|
+
import {
|
|
41
|
+
flattenPluginInstances,
|
|
42
|
+
mergePluginConfigs,
|
|
43
|
+
normalizePluginConfig,
|
|
44
|
+
resolveConfiguredPluginInstances,
|
|
45
|
+
resolvePluginHooksEntryPath
|
|
46
|
+
} from '@vibe-forge/utils/plugin-resolver'
|
|
47
|
+
import type { ResolvedPluginInstance } from '@vibe-forge/utils/plugin-resolver'
|
|
48
|
+
import { glob } from 'fast-glob'
|
|
49
|
+
import fm from 'front-matter'
|
|
50
|
+
import yaml from 'js-yaml'
|
|
51
|
+
|
|
52
|
+
type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
|
|
53
|
+
type OpenCodeOverlayKind = Extract<WorkspaceAssetKind, 'agent' | 'command' | 'mode' | 'nativePlugin'>
|
|
54
|
+
type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetKind }> & {
|
|
55
|
+
payload: {
|
|
56
|
+
definition: TDefinition & { path: string }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
interface OpenCodeOverlayAssetEntry {
|
|
60
|
+
kind: OpenCodeOverlayKind
|
|
61
|
+
sourcePath: string
|
|
62
|
+
entryName: string
|
|
63
|
+
targetSubpath: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type OpenCodeOverlayAsset<TKind extends OpenCodeOverlayKind> = Extract<WorkspaceAsset, { kind: TKind }>
|
|
67
|
+
|
|
68
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => (
|
|
69
|
+
value != null && typeof value === 'object' && !Array.isArray(value)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const getFirstNonEmptyLine = (text: string) =>
|
|
73
|
+
text
|
|
74
|
+
.split('\n')
|
|
75
|
+
.map(line => line.trim())
|
|
76
|
+
.find(Boolean)
|
|
77
|
+
|
|
78
|
+
const resolveDisplayName = (name: string, scope?: string) => (
|
|
79
|
+
scope != null && scope.trim() !== '' ? `${scope}/${name}` : name
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const resolveDocumentDescription = (
|
|
83
|
+
body: string,
|
|
84
|
+
explicitDescription?: string,
|
|
85
|
+
fallbackName?: string
|
|
86
|
+
) => {
|
|
87
|
+
const trimmedDescription = explicitDescription?.trim()
|
|
88
|
+
return trimmedDescription || getFirstNonEmptyLine(body) || fallbackName || ''
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const isAlwaysRule = (attributes: Pick<Rule, 'always' | 'alwaysApply'>) => (
|
|
92
|
+
attributes.always ?? attributes.alwaysApply ?? false
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const resolveDefinitionName = <T extends { name?: string }>(
|
|
96
|
+
definition: Definition<T>,
|
|
97
|
+
indexFileNames: string[] = []
|
|
98
|
+
) => definition.resolvedName?.trim() || resolveDocumentName(definition.path, definition.attributes.name, indexFileNames)
|
|
99
|
+
|
|
100
|
+
const toMarkdownBlockquote = (content: string) => (
|
|
101
|
+
content
|
|
102
|
+
.trim()
|
|
103
|
+
.split('\n')
|
|
104
|
+
.map(line => line === '' ? '>' : `> ${line}`)
|
|
105
|
+
.join('\n')
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const buildOptionalRuleGuidance = (cwd: string, rule: Definition<Rule>) => {
|
|
109
|
+
const name = resolveDefinitionName(rule)
|
|
110
|
+
const desc = resolveDocumentDescription(rule.body, rule.attributes.description, name)
|
|
111
|
+
return [
|
|
112
|
+
`适用场景:${desc}`,
|
|
113
|
+
`规则文件路径:${resolvePromptPath(cwd, rule.path)}`,
|
|
114
|
+
'仅在任务满足上述场景时,再阅读该规则文件。'
|
|
115
|
+
].join('\n')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const buildSkillSummary = (
|
|
119
|
+
cwd: string,
|
|
120
|
+
skill: Definition<Skill>,
|
|
121
|
+
guidance: string
|
|
122
|
+
) => {
|
|
123
|
+
const name = resolveDefinitionName(skill, ['skill.md'])
|
|
124
|
+
const desc = resolveDocumentDescription(skill.body, skill.attributes.description, name)
|
|
125
|
+
return toMarkdownBlockquote(
|
|
126
|
+
[
|
|
127
|
+
`技能介绍:${desc}`,
|
|
128
|
+
`技能文件路径:${resolvePromptPath(cwd, skill.path)}`,
|
|
129
|
+
guidance
|
|
130
|
+
].join('\n')
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const resolveEntityIdentifier = (path: string, explicitName?: string) => (
|
|
135
|
+
resolveDocumentName(path, explicitName, ['readme.md', 'index.json'])
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const resolveSkillIdentifier = (path: string, explicitName?: string) => (
|
|
139
|
+
resolveDocumentName(path, explicitName, ['skill.md'])
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const parseScopedReference = (value: string) => {
|
|
143
|
+
if (
|
|
144
|
+
value.startsWith('./') ||
|
|
145
|
+
value.startsWith('../') ||
|
|
146
|
+
value.startsWith('/') ||
|
|
147
|
+
value.endsWith('.md') ||
|
|
148
|
+
value.endsWith('.json') ||
|
|
149
|
+
value.endsWith('.yaml') ||
|
|
150
|
+
value.endsWith('.yml')
|
|
151
|
+
) {
|
|
152
|
+
return undefined
|
|
153
|
+
}
|
|
154
|
+
const separatorIndex = value.indexOf('/')
|
|
155
|
+
if (separatorIndex <= 0) return undefined
|
|
156
|
+
return {
|
|
157
|
+
scope: value.slice(0, separatorIndex),
|
|
158
|
+
name: value.slice(separatorIndex + 1)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const isPathLikeReference = (value: string) => (
|
|
163
|
+
value.startsWith('./') ||
|
|
164
|
+
value.startsWith('../') ||
|
|
165
|
+
value.startsWith('/') ||
|
|
166
|
+
value.includes('*') ||
|
|
167
|
+
value.endsWith('.md') ||
|
|
168
|
+
value.endsWith('.json') ||
|
|
169
|
+
value.endsWith('.yaml') ||
|
|
170
|
+
value.endsWith('.yml')
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
const loadWorkspaceConfig = async (cwd: string) => (
|
|
174
|
+
loadConfig({
|
|
175
|
+
cwd,
|
|
176
|
+
jsonVariables: buildConfigJsonVariables(cwd, process.env)
|
|
177
|
+
})
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const parseFrontmatterDocument = async <TDefinition extends object>(
|
|
181
|
+
path: string
|
|
182
|
+
): Promise<Definition<TDefinition>> => {
|
|
183
|
+
const content = await readFile(path, 'utf-8')
|
|
184
|
+
const { body, attributes } = fm<TDefinition>(content)
|
|
185
|
+
return {
|
|
186
|
+
path,
|
|
187
|
+
body,
|
|
188
|
+
attributes
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const parseEntityIndexJson = async (path: string): Promise<Definition<Entity>> => {
|
|
193
|
+
const raw = JSON.parse(await readFile(path, 'utf-8')) as Record<string, unknown>
|
|
194
|
+
const promptPath = typeof raw.promptPath === 'string'
|
|
195
|
+
? (raw.promptPath.startsWith('/') ? raw.promptPath : resolve(dirname(path), raw.promptPath))
|
|
196
|
+
: undefined
|
|
197
|
+
const prompt = typeof raw.prompt === 'string'
|
|
198
|
+
? raw.prompt
|
|
199
|
+
: promptPath != null
|
|
200
|
+
? await readFile(promptPath, 'utf-8')
|
|
201
|
+
: ''
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
path,
|
|
205
|
+
body: prompt,
|
|
206
|
+
attributes: raw as Entity
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const parseStructuredMcpFile = async (path: string) => {
|
|
211
|
+
const raw = await readFile(path, 'utf8')
|
|
212
|
+
const extension = extname(path).toLowerCase()
|
|
213
|
+
return extension === '.yaml' || extension === '.yml'
|
|
214
|
+
? yaml.load(raw)
|
|
215
|
+
: JSON.parse(raw)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const createDocumentAsset = <
|
|
219
|
+
TKind extends DocumentAssetKind,
|
|
220
|
+
TDefinition extends { path: string; attributes: { name?: string } },
|
|
221
|
+
>(params: {
|
|
222
|
+
cwd: string
|
|
223
|
+
kind: TKind
|
|
224
|
+
definition: TDefinition
|
|
225
|
+
origin: 'workspace' | 'plugin'
|
|
226
|
+
scope?: string
|
|
227
|
+
instance?: ResolvedPluginInstance
|
|
228
|
+
}) => {
|
|
229
|
+
const name = ({
|
|
230
|
+
rule: resolveDocumentName,
|
|
231
|
+
spec: resolveSpecIdentifier,
|
|
232
|
+
entity: resolveEntityIdentifier,
|
|
233
|
+
skill: resolveSkillIdentifier
|
|
234
|
+
}[params.kind])(params.definition.path, params.definition.attributes.name)
|
|
235
|
+
const displayName = resolveDisplayName(name, params.scope)
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
id: `${params.kind}:${params.origin}:${params.instance?.instancePath ?? 'workspace'}:${displayName}:${
|
|
239
|
+
resolveRelativePath(params.cwd, params.definition.path)
|
|
240
|
+
}`,
|
|
241
|
+
kind: params.kind,
|
|
242
|
+
name,
|
|
243
|
+
displayName,
|
|
244
|
+
scope: params.scope,
|
|
245
|
+
origin: params.origin,
|
|
246
|
+
sourcePath: params.definition.path,
|
|
247
|
+
instancePath: params.instance?.instancePath,
|
|
248
|
+
packageId: params.instance?.packageId,
|
|
249
|
+
resolvedBy: params.instance?.resolvedBy,
|
|
250
|
+
taskOverlaySource: params.instance?.overlaySource,
|
|
251
|
+
payload: {
|
|
252
|
+
definition: params.definition
|
|
253
|
+
}
|
|
254
|
+
} as Extract<WorkspaceAsset, { kind: TKind }>
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const createMcpAsset = (params: {
|
|
258
|
+
cwd: string
|
|
259
|
+
name: string
|
|
260
|
+
config: NonNullable<Config['mcpServers']>[string]
|
|
261
|
+
origin: 'workspace' | 'plugin'
|
|
262
|
+
scope?: string
|
|
263
|
+
sourcePath: string
|
|
264
|
+
instance?: ResolvedPluginInstance
|
|
265
|
+
}) => {
|
|
266
|
+
const displayName = resolveDisplayName(params.name, params.scope)
|
|
267
|
+
return {
|
|
268
|
+
id: `mcpServer:${params.origin}:${params.instance?.instancePath ?? 'workspace'}:${displayName}:${
|
|
269
|
+
resolveRelativePath(params.cwd, params.sourcePath)
|
|
270
|
+
}`,
|
|
271
|
+
kind: 'mcpServer',
|
|
272
|
+
name: params.name,
|
|
273
|
+
displayName,
|
|
274
|
+
scope: params.scope,
|
|
275
|
+
origin: params.origin,
|
|
276
|
+
sourcePath: params.sourcePath,
|
|
277
|
+
instancePath: params.instance?.instancePath,
|
|
278
|
+
packageId: params.instance?.packageId,
|
|
279
|
+
resolvedBy: params.instance?.resolvedBy,
|
|
280
|
+
taskOverlaySource: params.instance?.overlaySource,
|
|
281
|
+
payload: {
|
|
282
|
+
name: displayName,
|
|
283
|
+
config: params.config
|
|
284
|
+
}
|
|
285
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'mcpServer' }>
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const createHookPluginAsset = (
|
|
289
|
+
instance: ResolvedPluginInstance
|
|
290
|
+
) => ({
|
|
291
|
+
id: `hookPlugin:${instance.instancePath}:${instance.packageId ?? instance.requestId}`,
|
|
292
|
+
kind: 'hookPlugin',
|
|
293
|
+
name: instance.requestId,
|
|
294
|
+
displayName: resolveDisplayName(instance.requestId, instance.scope),
|
|
295
|
+
scope: instance.scope,
|
|
296
|
+
origin: 'plugin' as const,
|
|
297
|
+
sourcePath: instance.rootDir,
|
|
298
|
+
instancePath: instance.instancePath,
|
|
299
|
+
packageId: instance.packageId,
|
|
300
|
+
resolvedBy: instance.resolvedBy,
|
|
301
|
+
taskOverlaySource: instance.overlaySource,
|
|
302
|
+
payload: {
|
|
303
|
+
packageName: instance.packageId,
|
|
304
|
+
config: instance.options
|
|
305
|
+
}
|
|
306
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'hookPlugin' }>)
|
|
307
|
+
|
|
308
|
+
const createOpenCodeOverlayAsset = <TKind extends OpenCodeOverlayKind>(params: {
|
|
309
|
+
cwd: string
|
|
310
|
+
kind: TKind
|
|
311
|
+
sourcePath: string
|
|
312
|
+
entryName: string
|
|
313
|
+
targetSubpath: string
|
|
314
|
+
instance: ResolvedPluginInstance
|
|
315
|
+
}): OpenCodeOverlayAsset<TKind> => ({
|
|
316
|
+
id: `${params.kind}:plugin:${params.instance.instancePath}:${
|
|
317
|
+
resolveDisplayName(params.entryName, params.instance.scope)
|
|
318
|
+
}:${resolveRelativePath(params.cwd, params.sourcePath)}`,
|
|
319
|
+
kind: params.kind,
|
|
320
|
+
name: params.entryName,
|
|
321
|
+
displayName: resolveDisplayName(params.entryName, params.instance.scope),
|
|
322
|
+
scope: params.instance.scope,
|
|
323
|
+
origin: 'plugin' as const,
|
|
324
|
+
sourcePath: params.sourcePath,
|
|
325
|
+
instancePath: params.instance.instancePath,
|
|
326
|
+
packageId: params.instance.packageId,
|
|
327
|
+
resolvedBy: params.instance.resolvedBy,
|
|
328
|
+
taskOverlaySource: params.instance.overlaySource,
|
|
329
|
+
payload: {
|
|
330
|
+
entryName: params.entryName,
|
|
331
|
+
targetSubpath: params.targetSubpath
|
|
332
|
+
}
|
|
333
|
+
} as OpenCodeOverlayAsset<TKind>)
|
|
334
|
+
|
|
335
|
+
const scanWorkspaceDocuments = async (cwd: string) => {
|
|
336
|
+
const [rulePaths, skillPaths, specPaths, entityDocPaths, entityJsonPaths, mcpPaths] = await Promise.all([
|
|
337
|
+
glob(['.ai/rules/*.md'], { cwd, absolute: true }),
|
|
338
|
+
glob(['.ai/skills/*/SKILL.md'], { cwd, absolute: true }),
|
|
339
|
+
glob(['.ai/specs/*.md', '.ai/specs/*/index.md'], { cwd, absolute: true }),
|
|
340
|
+
glob(['.ai/entities/*.md', '.ai/entities/*/README.md'], { cwd, absolute: true }),
|
|
341
|
+
glob(['.ai/entities/*/index.json'], { cwd, absolute: true }),
|
|
342
|
+
glob(['.ai/mcp/*.json', '.ai/mcp/*.yaml', '.ai/mcp/*.yml'], { cwd, absolute: true })
|
|
343
|
+
])
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
rulePaths,
|
|
347
|
+
skillPaths,
|
|
348
|
+
specPaths,
|
|
349
|
+
entityDocPaths,
|
|
350
|
+
entityJsonPaths,
|
|
351
|
+
mcpPaths
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const scanInstanceDocuments = async (instance: ResolvedPluginInstance) => {
|
|
356
|
+
const rootDir = instance.rootDir
|
|
357
|
+
const assets = instance.manifest?.assets
|
|
358
|
+
const resolveAssetRoot = (dir: string | undefined, fallback: string) => resolve(rootDir, dir ?? fallback)
|
|
359
|
+
|
|
360
|
+
const [rulePaths, skillPaths, specPaths, entityDocPaths, entityJsonPaths, mcpPaths] = await Promise.all([
|
|
361
|
+
glob(['*.md'], { cwd: resolveAssetRoot(assets?.rules, 'rules'), absolute: true }).catch(() => [] as string[]),
|
|
362
|
+
glob(['*/SKILL.md'], { cwd: resolveAssetRoot(assets?.skills, 'skills'), absolute: true }).catch(() =>
|
|
363
|
+
[] as string[]
|
|
364
|
+
),
|
|
365
|
+
glob(['*.md', '*/index.md'], { cwd: resolveAssetRoot(assets?.specs, 'specs'), absolute: true }).catch(() =>
|
|
366
|
+
[] as string[]
|
|
367
|
+
),
|
|
368
|
+
glob(['*.md', '*/README.md'], { cwd: resolveAssetRoot(assets?.entities, 'entities'), absolute: true }).catch(() =>
|
|
369
|
+
[] as string[]
|
|
370
|
+
),
|
|
371
|
+
glob(['*/index.json'], { cwd: resolveAssetRoot(assets?.entities, 'entities'), absolute: true }).catch(() =>
|
|
372
|
+
[] as string[]
|
|
373
|
+
),
|
|
374
|
+
glob(['*.json', '*.yaml', '*.yml'], { cwd: resolveAssetRoot(assets?.mcp, 'mcp'), absolute: true }).catch(() =>
|
|
375
|
+
[] as string[]
|
|
376
|
+
)
|
|
377
|
+
])
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
rulePaths,
|
|
381
|
+
skillPaths,
|
|
382
|
+
specPaths,
|
|
383
|
+
entityDocPaths,
|
|
384
|
+
entityJsonPaths,
|
|
385
|
+
mcpPaths
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const toOpenCodeOverlayEntries = (
|
|
390
|
+
kind: OpenCodeOverlayKind,
|
|
391
|
+
targetDir: 'agents' | 'commands' | 'modes' | 'plugins',
|
|
392
|
+
paths: string[]
|
|
393
|
+
): OpenCodeOverlayAssetEntry[] =>
|
|
394
|
+
paths.map((sourcePath) => ({
|
|
395
|
+
kind,
|
|
396
|
+
sourcePath,
|
|
397
|
+
entryName: basename(sourcePath, extname(sourcePath)),
|
|
398
|
+
targetSubpath: `${targetDir}/${basename(sourcePath)}`
|
|
399
|
+
}))
|
|
400
|
+
|
|
401
|
+
const scanInstanceOpenCodeOverlays = async (
|
|
402
|
+
instance: ResolvedPluginInstance
|
|
403
|
+
) => {
|
|
404
|
+
const opencodeRoot = resolve(instance.rootDir, 'opencode')
|
|
405
|
+
const [agentPaths, commandPaths, modePaths, nativePluginPaths] = await Promise.all([
|
|
406
|
+
glob(['*.md'], { cwd: resolve(opencodeRoot, 'agents'), absolute: true, onlyFiles: true }).catch(() =>
|
|
407
|
+
[] as string[]
|
|
408
|
+
),
|
|
409
|
+
glob(['*.md'], { cwd: resolve(opencodeRoot, 'commands'), absolute: true, onlyFiles: true }).catch(() =>
|
|
410
|
+
[] as string[]
|
|
411
|
+
),
|
|
412
|
+
glob(['*.md'], { cwd: resolve(opencodeRoot, 'modes'), absolute: true, onlyFiles: true }).catch(() =>
|
|
413
|
+
[] as string[]
|
|
414
|
+
),
|
|
415
|
+
glob(['**/*'], { cwd: resolve(opencodeRoot, 'plugins'), absolute: true, onlyFiles: true }).catch(() =>
|
|
416
|
+
[] as string[]
|
|
417
|
+
)
|
|
418
|
+
])
|
|
419
|
+
|
|
420
|
+
return [
|
|
421
|
+
...toOpenCodeOverlayEntries('agent', 'agents', agentPaths),
|
|
422
|
+
...toOpenCodeOverlayEntries('command', 'commands', commandPaths),
|
|
423
|
+
...toOpenCodeOverlayEntries('mode', 'modes', modePaths),
|
|
424
|
+
...toOpenCodeOverlayEntries('nativePlugin', 'plugins', nativePluginPaths)
|
|
425
|
+
]
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const definitionWithResolvedName = <TDefinition>(
|
|
429
|
+
definition: Definition<TDefinition>,
|
|
430
|
+
resolvedName: string,
|
|
431
|
+
instancePath?: string
|
|
432
|
+
) => ({
|
|
433
|
+
...definition,
|
|
434
|
+
resolvedName,
|
|
435
|
+
resolvedInstancePath: instancePath
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
const toDocumentDefinitions = <TDefinition>(
|
|
439
|
+
assets: Array<DocumentAsset<TDefinition>>
|
|
440
|
+
) =>
|
|
441
|
+
assets.map(asset =>
|
|
442
|
+
definitionWithResolvedName(
|
|
443
|
+
asset.payload.definition,
|
|
444
|
+
asset.displayName,
|
|
445
|
+
asset.instancePath
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
const assertNoDocumentConflicts = (
|
|
450
|
+
assets: Array<Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>>
|
|
451
|
+
) => {
|
|
452
|
+
const seen = new Map<string, WorkspaceAsset>()
|
|
453
|
+
for (const asset of assets) {
|
|
454
|
+
const key = `${asset.kind}:${asset.displayName}`
|
|
455
|
+
const existing = seen.get(key)
|
|
456
|
+
if (existing != null) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
`Duplicate ${asset.kind} asset ${asset.displayName} from ${existing.sourcePath} and ${asset.sourcePath}`
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
seen.set(key, asset)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const assertNoMcpConflicts = (
|
|
466
|
+
assets: Array<Extract<WorkspaceAsset, { kind: 'mcpServer' }>>
|
|
467
|
+
) => {
|
|
468
|
+
const seen = new Map<string, WorkspaceAsset>()
|
|
469
|
+
for (const asset of assets) {
|
|
470
|
+
const existing = seen.get(asset.displayName)
|
|
471
|
+
if (existing != null) {
|
|
472
|
+
throw new Error(`Duplicate MCP server ${asset.displayName} from ${existing.sourcePath} and ${asset.sourcePath}`)
|
|
473
|
+
}
|
|
474
|
+
seen.set(asset.displayName, asset)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const resolveUniqueAssetByName = <TAsset extends Extract<WorkspaceAsset, { kind: DocumentAssetKind }>>(
|
|
479
|
+
assets: TAsset[],
|
|
480
|
+
name: string
|
|
481
|
+
) => {
|
|
482
|
+
const matches = assets.filter(asset => asset.name === name)
|
|
483
|
+
if (matches.length === 0) return undefined
|
|
484
|
+
const unscopedMatches = matches.filter(asset => asset.scope == null)
|
|
485
|
+
if (unscopedMatches.length === 1) {
|
|
486
|
+
return unscopedMatches[0]
|
|
487
|
+
}
|
|
488
|
+
if (matches.length > 1) {
|
|
489
|
+
throw new Error(
|
|
490
|
+
`Ambiguous asset reference ${name}. Candidates: ${matches.map(match => match.displayName).join(', ')}`
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
return matches[0]
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const resolveScopedAsset = <TAsset extends Extract<WorkspaceAsset, { kind: DocumentAssetKind }>>(
|
|
497
|
+
assets: TAsset[],
|
|
498
|
+
scope: string,
|
|
499
|
+
name: string
|
|
500
|
+
) => assets.find(asset => asset.scope === scope && asset.name === name)
|
|
501
|
+
|
|
502
|
+
const resolveNamedAssets = <TAsset extends Extract<WorkspaceAsset, { kind: DocumentAssetKind }>>(
|
|
503
|
+
assets: TAsset[],
|
|
504
|
+
refs: string[] | undefined,
|
|
505
|
+
currentInstancePath?: string
|
|
506
|
+
) => {
|
|
507
|
+
if (refs == null || refs.length === 0) return [] as TAsset[]
|
|
508
|
+
|
|
509
|
+
const selected: TAsset[] = []
|
|
510
|
+
const seen = new Set<string>()
|
|
511
|
+
|
|
512
|
+
const add = (asset: TAsset) => {
|
|
513
|
+
if (seen.has(asset.id)) return
|
|
514
|
+
seen.add(asset.id)
|
|
515
|
+
selected.push(asset)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
for (const ref of refs) {
|
|
519
|
+
const scoped = parseScopedReference(ref)
|
|
520
|
+
if (scoped != null) {
|
|
521
|
+
const asset = resolveScopedAsset(assets, scoped.scope, scoped.name)
|
|
522
|
+
if (asset == null) throw new Error(`Failed to resolve asset ${ref}`)
|
|
523
|
+
add(asset)
|
|
524
|
+
continue
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (currentInstancePath != null) {
|
|
528
|
+
const local = assets.find(asset => asset.instancePath === currentInstancePath && asset.name === ref)
|
|
529
|
+
if (local != null) {
|
|
530
|
+
add(local)
|
|
531
|
+
continue
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const asset = resolveUniqueAssetByName(assets, ref)
|
|
536
|
+
if (asset == null) throw new Error(`Failed to resolve asset ${ref}`)
|
|
537
|
+
add(asset)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return selected
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const toRuleSelectionRefs = (
|
|
544
|
+
refs: RuleReference[] | string[] | undefined
|
|
545
|
+
) =>
|
|
546
|
+
(refs ?? []).flatMap((ref) => {
|
|
547
|
+
if (typeof ref === 'string') return [ref]
|
|
548
|
+
if (ref.type === 'remote') return []
|
|
549
|
+
return [ref.path]
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
const createRemoteRuleDefinition = (
|
|
553
|
+
rule: Extract<RuleReference, { type: 'remote' }>,
|
|
554
|
+
index: number
|
|
555
|
+
): Definition<Rule> => {
|
|
556
|
+
const tags = Array.isArray(rule.tags)
|
|
557
|
+
? rule.tags.filter((value): value is string => typeof value === 'string' && value.trim() !== '').map(value =>
|
|
558
|
+
value.trim()
|
|
559
|
+
)
|
|
560
|
+
: []
|
|
561
|
+
const description = rule.desc?.trim() || (
|
|
562
|
+
tags.length > 0
|
|
563
|
+
? `远程知识库标签:${tags.join(', ')}`
|
|
564
|
+
: '远程知识库规则引用'
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
path: `remote-rule-${index + 1}.md`,
|
|
569
|
+
body: [
|
|
570
|
+
description,
|
|
571
|
+
tags.length > 0 ? `知识库标签:${tags.join(', ')}` : undefined,
|
|
572
|
+
'该规则来自远程知识库引用,不对应本地文件。'
|
|
573
|
+
].filter((value): value is string => value != null && value !== '').join('\n'),
|
|
574
|
+
attributes: {
|
|
575
|
+
name: tags.length > 0 ? `remote:${tags.join(',')}` : `remote-rule-${index + 1}`,
|
|
576
|
+
description
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const resolvePathMatchedRules = async (
|
|
582
|
+
bundle: WorkspaceAssetBundle,
|
|
583
|
+
ref: string
|
|
584
|
+
) => {
|
|
585
|
+
const matchedPaths = new Set(
|
|
586
|
+
(await glob(ref, {
|
|
587
|
+
cwd: bundle.cwd,
|
|
588
|
+
absolute: true
|
|
589
|
+
})).map(normalizePath)
|
|
590
|
+
)
|
|
591
|
+
return bundle.rules.filter(rule => matchedPaths.has(normalizePath(rule.sourcePath)))
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const resolveRuleSelection = async (
|
|
595
|
+
bundle: WorkspaceAssetBundle,
|
|
596
|
+
refs: RuleReference[] | string[] | undefined,
|
|
597
|
+
currentInstancePath?: string
|
|
598
|
+
) => {
|
|
599
|
+
const assets: Array<Extract<WorkspaceAsset, { kind: 'rule' }>> = []
|
|
600
|
+
const remoteDefinitions: Definition<Rule>[] = []
|
|
601
|
+
const seen = new Set<string>()
|
|
602
|
+
|
|
603
|
+
const addAsset = (asset: Extract<WorkspaceAsset, { kind: 'rule' }>) => {
|
|
604
|
+
if (seen.has(asset.id)) return
|
|
605
|
+
seen.add(asset.id)
|
|
606
|
+
assets.push(asset)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
let remoteIndex = 0
|
|
610
|
+
for (const ref of refs ?? []) {
|
|
611
|
+
if (typeof ref === 'object' && ref != null && ref.type === 'remote') {
|
|
612
|
+
remoteDefinitions.push(createRemoteRuleDefinition(ref, remoteIndex++))
|
|
613
|
+
continue
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const value = typeof ref === 'string' ? ref : ref.path
|
|
617
|
+
if (isPathLikeReference(value)) {
|
|
618
|
+
const matched = await resolvePathMatchedRules(bundle, value)
|
|
619
|
+
matched.forEach(addAsset)
|
|
620
|
+
continue
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const scoped = parseScopedReference(value)
|
|
624
|
+
if (scoped != null) {
|
|
625
|
+
const asset = resolveScopedAsset(bundle.rules, scoped.scope, scoped.name)
|
|
626
|
+
if (asset == null) throw new Error(`Failed to resolve rule ${value}`)
|
|
627
|
+
addAsset(asset)
|
|
628
|
+
continue
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (currentInstancePath != null) {
|
|
632
|
+
const local = bundle.rules.find(rule => rule.instancePath === currentInstancePath && rule.name === value)
|
|
633
|
+
if (local != null) {
|
|
634
|
+
addAsset(local)
|
|
635
|
+
continue
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const asset = resolveUniqueAssetByName(bundle.rules, value)
|
|
640
|
+
if (asset == null) throw new Error(`Failed to resolve rule ${value}`)
|
|
641
|
+
addAsset(asset)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
assets,
|
|
646
|
+
remoteDefinitions
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const resolveIncludedSkillRefs = (selection: string[] | SkillSelection | undefined) => {
|
|
651
|
+
if (selection == null) return undefined
|
|
652
|
+
if (Array.isArray(selection)) return selection
|
|
653
|
+
return selection.type === 'include' ? selection.list : undefined
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const resolveExcludedSkillRefs = (selection: string[] | SkillSelection | undefined) => {
|
|
657
|
+
if (selection == null || Array.isArray(selection)) return undefined
|
|
658
|
+
return selection.type === 'exclude' ? selection.list : undefined
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const resolveSelectedSkillAssets = (
|
|
662
|
+
assets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>,
|
|
663
|
+
selection?: WorkspaceSkillSelection
|
|
664
|
+
) => {
|
|
665
|
+
if (selection == null) return assets
|
|
666
|
+
|
|
667
|
+
const included = selection.include != null && selection.include.length > 0
|
|
668
|
+
? resolveNamedAssets(assets, selection.include)
|
|
669
|
+
: assets
|
|
670
|
+
const excluded = new Set(
|
|
671
|
+
resolveNamedAssets(assets, selection.exclude).map(asset => asset.id)
|
|
672
|
+
)
|
|
673
|
+
return included.filter(asset => !excluded.has(asset.id))
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const resolveSelectedMcpNames = (
|
|
677
|
+
bundle: WorkspaceAssetBundle,
|
|
678
|
+
selection: WorkspaceMcpSelection | undefined
|
|
679
|
+
) => {
|
|
680
|
+
const allAssets = Object.values(bundle.mcpServers)
|
|
681
|
+
const includeRefs = selection?.include ??
|
|
682
|
+
(bundle.defaultIncludeMcpServers.length > 0 ? bundle.defaultIncludeMcpServers : undefined)
|
|
683
|
+
const excludeRefs = selection?.exclude ??
|
|
684
|
+
(bundle.defaultExcludeMcpServers.length > 0 ? bundle.defaultExcludeMcpServers : undefined)
|
|
685
|
+
|
|
686
|
+
const resolveRefs = (refs: string[] | undefined) => {
|
|
687
|
+
if (refs == null || refs.length === 0) return undefined
|
|
688
|
+
return new Set(refs.map((ref) => {
|
|
689
|
+
const scoped = parseScopedReference(ref)
|
|
690
|
+
if (scoped != null) {
|
|
691
|
+
const asset = allAssets.find(item => item.scope === scoped.scope && item.name === scoped.name)
|
|
692
|
+
if (asset == null) throw new Error(`Failed to resolve MCP server ${ref}`)
|
|
693
|
+
return asset.displayName
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const matches = allAssets.filter(item => item.name === ref || item.displayName === ref)
|
|
697
|
+
if (matches.length === 0) throw new Error(`Failed to resolve MCP server ${ref}`)
|
|
698
|
+
if (matches.length > 1) {
|
|
699
|
+
throw new Error(
|
|
700
|
+
`Ambiguous MCP server reference ${ref}. Candidates: ${matches.map(match => match.displayName).join(', ')}`
|
|
701
|
+
)
|
|
702
|
+
}
|
|
703
|
+
return matches[0].displayName
|
|
704
|
+
}))
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const include = resolveRefs(includeRefs)
|
|
708
|
+
const exclude = resolveRefs(excludeRefs) ?? new Set<string>()
|
|
709
|
+
|
|
710
|
+
return allAssets
|
|
711
|
+
.map(asset => asset.displayName)
|
|
712
|
+
.filter(name => (include == null || include.has(name)) && !exclude.has(name))
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const resolvePluginOverlay = (
|
|
716
|
+
basePlugins: PluginConfig | undefined,
|
|
717
|
+
overlay: PluginOverlayConfig | undefined
|
|
718
|
+
) => {
|
|
719
|
+
if (overlay == null) return basePlugins
|
|
720
|
+
if (overlay.mode !== 'override' && overlay.mode !== 'extend') {
|
|
721
|
+
throw new Error('Invalid plugins overlay. "mode" must be "extend" or "override".')
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const overlayList = normalizePluginConfig(overlay.list, 'plugins overlay list') ?? []
|
|
725
|
+
return overlay.mode === 'override'
|
|
726
|
+
? overlayList
|
|
727
|
+
: mergePluginConfigs(basePlugins, overlayList)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const generateRulesPrompt = (cwd: string, rules: Definition<Rule>[]) => {
|
|
731
|
+
const rulesPrompt = rules
|
|
732
|
+
.map((rule) => {
|
|
733
|
+
const name = resolveDefinitionName(rule)
|
|
734
|
+
const content = isAlwaysRule(rule.attributes) && rule.body.trim()
|
|
735
|
+
? rule.body.trim()
|
|
736
|
+
: buildOptionalRuleGuidance(cwd, rule)
|
|
737
|
+
return `# ${name}\n\n${toMarkdownBlockquote(content)}`
|
|
738
|
+
})
|
|
739
|
+
.filter(Boolean)
|
|
740
|
+
.join('\n\n')
|
|
741
|
+
|
|
742
|
+
return `<system-prompt>\n项目系统规则如下:\n${rulesPrompt}\n</system-prompt>\n`
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const generateSkillsPrompt = (cwd: string, skills: Definition<Skill>[]) => {
|
|
746
|
+
const modules = skills
|
|
747
|
+
.map((skill) => {
|
|
748
|
+
const name = resolveDefinitionName(skill, ['skill.md'])
|
|
749
|
+
return [
|
|
750
|
+
`# ${name}`,
|
|
751
|
+
'',
|
|
752
|
+
buildSkillSummary(
|
|
753
|
+
cwd,
|
|
754
|
+
skill,
|
|
755
|
+
'资源内容中的相对路径相对该技能文件所在目录解析。'
|
|
756
|
+
),
|
|
757
|
+
'',
|
|
758
|
+
'<skill-content>',
|
|
759
|
+
skill.body.trim(),
|
|
760
|
+
'</skill-content>'
|
|
761
|
+
].join('\n')
|
|
762
|
+
})
|
|
763
|
+
.filter(Boolean)
|
|
764
|
+
.join('\n\n')
|
|
765
|
+
|
|
766
|
+
if (modules === '') return ''
|
|
767
|
+
|
|
768
|
+
return `<system-prompt>\n项目已加载如下技能模块:\n${modules}\n</system-prompt>\n`
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const generateSkillsRoutePrompt = (cwd: string, skills: Definition<Skill>[]) => {
|
|
772
|
+
const modules = skills
|
|
773
|
+
.filter(({ attributes: { always } }) => always !== false)
|
|
774
|
+
.map((skill) => {
|
|
775
|
+
const name = resolveDefinitionName(skill, ['skill.md'])
|
|
776
|
+
return [
|
|
777
|
+
`# ${name}`,
|
|
778
|
+
'',
|
|
779
|
+
buildSkillSummary(
|
|
780
|
+
cwd,
|
|
781
|
+
skill,
|
|
782
|
+
'默认无需预先加载正文;仅在任务明确需要该技能时,再读取对应技能文件。'
|
|
783
|
+
)
|
|
784
|
+
].join('\n')
|
|
785
|
+
})
|
|
786
|
+
.filter(Boolean)
|
|
787
|
+
.join('\n\n')
|
|
788
|
+
|
|
789
|
+
if (modules === '') return ''
|
|
790
|
+
|
|
791
|
+
return `<skills>\n${modules}\n</skills>\n`
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const generateSpecRoutePrompt = (specs: Definition<Spec>[], options?: { active?: boolean }) => {
|
|
795
|
+
const specsRouteStr = specs
|
|
796
|
+
.filter(({ attributes }) => attributes.always !== false)
|
|
797
|
+
.map((spec) => {
|
|
798
|
+
const name = resolveDefinitionName(spec, ['index.md'])
|
|
799
|
+
const desc = resolveDocumentDescription(spec.body, spec.attributes.description, name)
|
|
800
|
+
const identifier = spec.resolvedName?.trim() || resolveSpecIdentifier(spec.path, spec.attributes.name)
|
|
801
|
+
const params = spec.attributes.params ?? []
|
|
802
|
+
const paramsPrompt = params.length > 0
|
|
803
|
+
? params.map(({ name: paramName, description }) => ` - ${paramName}:${description ?? '无'}\n`).join('')
|
|
804
|
+
: ' - 无\n'
|
|
805
|
+
|
|
806
|
+
return (
|
|
807
|
+
`- 流程名称:${name}\n` +
|
|
808
|
+
` - 介绍:${desc}\n` +
|
|
809
|
+
` - 标识:${identifier}\n` +
|
|
810
|
+
' - 参数:\n' +
|
|
811
|
+
`${paramsPrompt}`
|
|
812
|
+
)
|
|
813
|
+
})
|
|
814
|
+
.join('\n')
|
|
815
|
+
|
|
816
|
+
const activeIdentityPrompt = options?.active
|
|
817
|
+
? (
|
|
818
|
+
'你是一个专业的项目推进管理大师,能够熟练指导其他实体来为你的目标工作。对你的预期是:\n' +
|
|
819
|
+
'\n' +
|
|
820
|
+
'- 永远不要单独完成代码开发工作\n' +
|
|
821
|
+
'- 必须要协调其他的开发人员来完成任务\n' +
|
|
822
|
+
'- 必须让他们按照目标进行完成,不要偏离目标,检查他们任务完成后的汇报内容是否符合要求\n' +
|
|
823
|
+
'\n'
|
|
824
|
+
)
|
|
825
|
+
: ''
|
|
826
|
+
|
|
827
|
+
return `<system-prompt>
|
|
828
|
+
${activeIdentityPrompt}根据用户需要以及实际的开发目标来决定使用不同的工作流程,调用 \`load-spec\` mcp tool 完成工作流程的加载。
|
|
829
|
+
- 根据实际需求传入标识,这不是路径,只能使用工具进行加载
|
|
830
|
+
- 通过参数的描述以及实际应用场景决定怎么传入参数
|
|
831
|
+
项目存在如下工作流程:
|
|
832
|
+
${specsRouteStr}
|
|
833
|
+
</system-prompt>
|
|
834
|
+
`
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const generateEntitiesRoutePrompt = (entities: Definition<Entity>[]) => (
|
|
838
|
+
'<system-prompt>\n' +
|
|
839
|
+
'项目存在如下实体:\n' +
|
|
840
|
+
`${
|
|
841
|
+
entities
|
|
842
|
+
.filter(({ attributes }) => attributes.always !== false)
|
|
843
|
+
.map((entity) => {
|
|
844
|
+
const name = resolveDefinitionName(entity, ['readme.md', 'index.json'])
|
|
845
|
+
const desc = resolveDocumentDescription(entity.body, entity.attributes.description, name)
|
|
846
|
+
return ` - ${name}:${desc}\n`
|
|
847
|
+
})
|
|
848
|
+
.join('')
|
|
849
|
+
}\n` +
|
|
850
|
+
'解决用户问题时,需根据用户需求可以通过 run-tasks 工具指定为实体后,自行调度多个不同类型的实体来完成工作。\n' +
|
|
851
|
+
'</system-prompt>\n'
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
const pickSpecAsset = (bundle: WorkspaceAssetBundle, ref: string) => {
|
|
855
|
+
const scoped = parseScopedReference(ref)
|
|
856
|
+
if (scoped != null) {
|
|
857
|
+
return resolveScopedAsset(bundle.specs, scoped.scope, scoped.name)
|
|
858
|
+
}
|
|
859
|
+
return resolveUniqueAssetByName(bundle.specs, ref)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const pickEntityAsset = (bundle: WorkspaceAssetBundle, ref: string) => {
|
|
863
|
+
const scoped = parseScopedReference(ref)
|
|
864
|
+
if (scoped != null) {
|
|
865
|
+
return resolveScopedAsset(bundle.entities, scoped.scope, scoped.name)
|
|
866
|
+
}
|
|
867
|
+
return resolveUniqueAssetByName(bundle.entities, ref)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
export async function resolveWorkspaceAssetBundle(params: {
|
|
871
|
+
cwd: string
|
|
872
|
+
configs?: [Config?, Config?]
|
|
873
|
+
plugins?: PluginConfig
|
|
874
|
+
overlaySource?: string
|
|
875
|
+
useDefaultVibeForgeMcpServer?: boolean
|
|
876
|
+
}): Promise<WorkspaceAssetBundle> {
|
|
877
|
+
const [config, userConfig] = params.configs ?? await loadWorkspaceConfig(params.cwd)
|
|
878
|
+
const pluginConfigs = params.plugins ?? mergePluginConfigs(config?.plugins, userConfig?.plugins)
|
|
879
|
+
const pluginInstances = await resolveConfiguredPluginInstances({
|
|
880
|
+
cwd: params.cwd,
|
|
881
|
+
plugins: pluginConfigs,
|
|
882
|
+
overlaySource: params.overlaySource
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
const localScan = await scanWorkspaceDocuments(params.cwd)
|
|
886
|
+
const flattenedPluginInstances = flattenPluginInstances(pluginInstances)
|
|
887
|
+
const pluginScans = await Promise.all(flattenedPluginInstances.map(instance => scanInstanceDocuments(instance)))
|
|
888
|
+
const pluginOverlayScans = await Promise.all(
|
|
889
|
+
flattenedPluginInstances.map(instance => scanInstanceOpenCodeOverlays(instance))
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
const assets: WorkspaceAsset[] = []
|
|
893
|
+
|
|
894
|
+
const pushDocumentAssets = async <TKind extends DocumentAssetKind>(
|
|
895
|
+
kind: TKind,
|
|
896
|
+
paths: string[],
|
|
897
|
+
origin: 'workspace' | 'plugin',
|
|
898
|
+
instance?: ResolvedPluginInstance,
|
|
899
|
+
parser?: (path: string) => Promise<any>
|
|
900
|
+
) => {
|
|
901
|
+
const definitions = await Promise.all(paths.map(path => (
|
|
902
|
+
parser != null ? parser(path) : parseFrontmatterDocument(path)
|
|
903
|
+
)))
|
|
904
|
+
assets.push(
|
|
905
|
+
...definitions.map(definition =>
|
|
906
|
+
createDocumentAsset({
|
|
907
|
+
cwd: params.cwd,
|
|
908
|
+
kind,
|
|
909
|
+
definition,
|
|
910
|
+
origin,
|
|
911
|
+
scope: instance?.scope,
|
|
912
|
+
instance
|
|
913
|
+
})
|
|
914
|
+
)
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
await pushDocumentAssets('rule', localScan.rulePaths, 'workspace')
|
|
919
|
+
await pushDocumentAssets('skill', localScan.skillPaths, 'workspace')
|
|
920
|
+
await pushDocumentAssets('spec', localScan.specPaths, 'workspace')
|
|
921
|
+
await pushDocumentAssets('entity', localScan.entityDocPaths, 'workspace')
|
|
922
|
+
await pushDocumentAssets('entity', localScan.entityJsonPaths, 'workspace', undefined, parseEntityIndexJson)
|
|
923
|
+
|
|
924
|
+
for (let index = 0; index < flattenedPluginInstances.length; index++) {
|
|
925
|
+
const instance = flattenedPluginInstances[index]
|
|
926
|
+
const scan = pluginScans[index]
|
|
927
|
+
await pushDocumentAssets('rule', scan.rulePaths, 'plugin', instance)
|
|
928
|
+
await pushDocumentAssets('skill', scan.skillPaths, 'plugin', instance)
|
|
929
|
+
await pushDocumentAssets('spec', scan.specPaths, 'plugin', instance)
|
|
930
|
+
await pushDocumentAssets('entity', scan.entityDocPaths, 'plugin', instance)
|
|
931
|
+
await pushDocumentAssets('entity', scan.entityJsonPaths, 'plugin', instance, parseEntityIndexJson)
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const mcpAssets = new Map<string, Extract<WorkspaceAsset, { kind: 'mcpServer' }>>()
|
|
935
|
+
const addMcpAsset = (
|
|
936
|
+
asset: Extract<WorkspaceAsset, { kind: 'mcpServer' }>,
|
|
937
|
+
options?: { overwrite?: boolean }
|
|
938
|
+
) => {
|
|
939
|
+
const existing = mcpAssets.get(asset.displayName)
|
|
940
|
+
if (existing != null && options?.overwrite !== true) {
|
|
941
|
+
throw new Error(`Duplicate MCP server ${asset.displayName} from ${existing.sourcePath} and ${asset.sourcePath}`)
|
|
942
|
+
}
|
|
943
|
+
mcpAssets.set(asset.displayName, asset)
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (params.useDefaultVibeForgeMcpServer !== false) {
|
|
947
|
+
const defaultVibeForgeMcpServer = resolveDefaultVibeForgeMcpServerConfig()
|
|
948
|
+
if (defaultVibeForgeMcpServer != null) {
|
|
949
|
+
addMcpAsset(createMcpAsset({
|
|
950
|
+
cwd: params.cwd,
|
|
951
|
+
name: DEFAULT_VIBE_FORGE_MCP_SERVER_NAME,
|
|
952
|
+
config: defaultVibeForgeMcpServer,
|
|
953
|
+
origin: 'workspace',
|
|
954
|
+
sourcePath: resolve(params.cwd, '.ai')
|
|
955
|
+
}))
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
for (const [name, configValue] of Object.entries(config?.mcpServers ?? {})) {
|
|
960
|
+
if (configValue.enabled === false) continue
|
|
961
|
+
const { enabled: _enabled, ...nextConfig } = configValue
|
|
962
|
+
addMcpAsset(
|
|
963
|
+
createMcpAsset({
|
|
964
|
+
cwd: params.cwd,
|
|
965
|
+
name,
|
|
966
|
+
config: nextConfig as NonNullable<Config['mcpServers']>[string],
|
|
967
|
+
origin: 'workspace',
|
|
968
|
+
sourcePath: resolve(params.cwd, '.ai.config.json')
|
|
969
|
+
}),
|
|
970
|
+
{ overwrite: true }
|
|
971
|
+
)
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
for (const [name, configValue] of Object.entries(userConfig?.mcpServers ?? {})) {
|
|
975
|
+
if (configValue.enabled === false) continue
|
|
976
|
+
const { enabled: _enabled, ...nextConfig } = configValue
|
|
977
|
+
addMcpAsset(
|
|
978
|
+
createMcpAsset({
|
|
979
|
+
cwd: params.cwd,
|
|
980
|
+
name,
|
|
981
|
+
config: nextConfig as NonNullable<Config['mcpServers']>[string],
|
|
982
|
+
origin: 'workspace',
|
|
983
|
+
sourcePath: resolve(params.cwd, '.ai.dev.config.json')
|
|
984
|
+
}),
|
|
985
|
+
{ overwrite: true }
|
|
986
|
+
)
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
for (let index = 0; index < flattenedPluginInstances.length; index++) {
|
|
990
|
+
const instance = flattenedPluginInstances[index]
|
|
991
|
+
const scan = pluginScans[index]
|
|
992
|
+
for (const path of scan.mcpPaths) {
|
|
993
|
+
const parsed = await parseStructuredMcpFile(path)
|
|
994
|
+
if (!isRecord(parsed)) continue
|
|
995
|
+
const fileName = basename(path, extname(path))
|
|
996
|
+
const name = typeof parsed.name === 'string' && parsed.name.trim() !== ''
|
|
997
|
+
? parsed.name.trim()
|
|
998
|
+
: fileName
|
|
999
|
+
const { name: _name, enabled, ...configValue } = parsed
|
|
1000
|
+
if (enabled === false) continue
|
|
1001
|
+
addMcpAsset(createMcpAsset({
|
|
1002
|
+
cwd: params.cwd,
|
|
1003
|
+
name,
|
|
1004
|
+
config: configValue as NonNullable<Config['mcpServers']>[string],
|
|
1005
|
+
origin: 'plugin',
|
|
1006
|
+
scope: instance.scope,
|
|
1007
|
+
sourcePath: path,
|
|
1008
|
+
instance
|
|
1009
|
+
}))
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const hookPlugins = flattenedPluginInstances
|
|
1014
|
+
.filter(instance =>
|
|
1015
|
+
instance.packageId != null && resolvePluginHooksEntryPath(params.cwd, instance.packageId) != null
|
|
1016
|
+
)
|
|
1017
|
+
.map(instance => createHookPluginAsset(instance))
|
|
1018
|
+
assets.push(...hookPlugins)
|
|
1019
|
+
|
|
1020
|
+
const opencodeOverlayAssets = flattenedPluginInstances.flatMap((instance, index) => (
|
|
1021
|
+
pluginOverlayScans[index].map((entry) =>
|
|
1022
|
+
createOpenCodeOverlayAsset({
|
|
1023
|
+
cwd: params.cwd,
|
|
1024
|
+
kind: entry.kind,
|
|
1025
|
+
sourcePath: entry.sourcePath,
|
|
1026
|
+
entryName: entry.entryName,
|
|
1027
|
+
targetSubpath: entry.targetSubpath,
|
|
1028
|
+
instance
|
|
1029
|
+
})
|
|
1030
|
+
)
|
|
1031
|
+
))
|
|
1032
|
+
assets.push(...opencodeOverlayAssets)
|
|
1033
|
+
|
|
1034
|
+
assets.push(...mcpAssets.values())
|
|
1035
|
+
|
|
1036
|
+
const rules = assets.filter((asset): asset is Extract<WorkspaceAsset, { kind: 'rule' }> => asset.kind === 'rule')
|
|
1037
|
+
const specs = assets.filter((asset): asset is Extract<WorkspaceAsset, { kind: 'spec' }> => asset.kind === 'spec')
|
|
1038
|
+
const entities = assets.filter((asset): asset is Extract<WorkspaceAsset, { kind: 'entity' }> =>
|
|
1039
|
+
asset.kind === 'entity'
|
|
1040
|
+
)
|
|
1041
|
+
const skills = assets.filter((asset): asset is Extract<WorkspaceAsset, { kind: 'skill' }> => asset.kind === 'skill')
|
|
1042
|
+
|
|
1043
|
+
assertNoDocumentConflicts([...rules, ...specs, ...entities, ...skills])
|
|
1044
|
+
assertNoMcpConflicts(Array.from(mcpAssets.values()))
|
|
1045
|
+
|
|
1046
|
+
return {
|
|
1047
|
+
cwd: params.cwd,
|
|
1048
|
+
pluginConfigs,
|
|
1049
|
+
pluginInstances,
|
|
1050
|
+
assets,
|
|
1051
|
+
rules,
|
|
1052
|
+
specs,
|
|
1053
|
+
entities,
|
|
1054
|
+
skills,
|
|
1055
|
+
mcpServers: Object.fromEntries(Array.from(mcpAssets.values()).map(asset => [asset.displayName, asset])),
|
|
1056
|
+
hookPlugins,
|
|
1057
|
+
opencodeOverlayAssets,
|
|
1058
|
+
defaultIncludeMcpServers: [
|
|
1059
|
+
...(config?.defaultIncludeMcpServers ?? []),
|
|
1060
|
+
...(userConfig?.defaultIncludeMcpServers ?? [])
|
|
1061
|
+
],
|
|
1062
|
+
defaultExcludeMcpServers: [
|
|
1063
|
+
...(config?.defaultExcludeMcpServers ?? []),
|
|
1064
|
+
...(userConfig?.defaultExcludeMcpServers ?? [])
|
|
1065
|
+
]
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
export async function resolvePromptAssetSelection(params: {
|
|
1070
|
+
bundle: WorkspaceAssetBundle
|
|
1071
|
+
type: 'spec' | 'entity' | undefined
|
|
1072
|
+
name?: string
|
|
1073
|
+
input?: {
|
|
1074
|
+
skills?: WorkspaceSkillSelection
|
|
1075
|
+
}
|
|
1076
|
+
}) {
|
|
1077
|
+
const options: {
|
|
1078
|
+
systemPrompt?: string
|
|
1079
|
+
tools?: Filter
|
|
1080
|
+
mcpServers?: WorkspaceMcpSelection
|
|
1081
|
+
promptAssetIds?: string[]
|
|
1082
|
+
assetBundle?: WorkspaceAssetBundle
|
|
1083
|
+
} = {
|
|
1084
|
+
assetBundle: params.bundle
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
let effectiveBundle = params.bundle
|
|
1088
|
+
let pinnedTargetAsset: Extract<WorkspaceAsset, { kind: 'spec' | 'entity' }> | undefined
|
|
1089
|
+
let targetBody = ''
|
|
1090
|
+
let targetToolsFilter: Filter | undefined
|
|
1091
|
+
let targetMcpServersFilter: Filter | undefined
|
|
1092
|
+
let targetInstancePath: string | undefined
|
|
1093
|
+
|
|
1094
|
+
if (params.type && params.name) {
|
|
1095
|
+
const baseTarget = params.type === 'spec'
|
|
1096
|
+
? pickSpecAsset(params.bundle, params.name)
|
|
1097
|
+
: pickEntityAsset(params.bundle, params.name)
|
|
1098
|
+
if (baseTarget == null) {
|
|
1099
|
+
throw new Error(`Failed to load ${params.type} ${params.name}`)
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const pluginOverlay = baseTarget.payload.definition.attributes.plugins as PluginOverlayConfig | undefined
|
|
1103
|
+
if (pluginOverlay != null) {
|
|
1104
|
+
effectiveBundle = await resolveWorkspaceAssetBundle({
|
|
1105
|
+
cwd: params.bundle.cwd,
|
|
1106
|
+
plugins: resolvePluginOverlay(params.bundle.pluginConfigs, pluginOverlay),
|
|
1107
|
+
overlaySource: `${params.type}:${baseTarget.displayName}`
|
|
1108
|
+
})
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
pinnedTargetAsset = baseTarget
|
|
1112
|
+
targetBody = baseTarget.payload.definition.body
|
|
1113
|
+
targetToolsFilter = baseTarget.payload.definition.attributes.tools
|
|
1114
|
+
targetMcpServersFilter = baseTarget.payload.definition.attributes.mcpServers
|
|
1115
|
+
targetInstancePath = baseTarget.instancePath
|
|
1116
|
+
options.assetBundle = effectiveBundle
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const selectedSkillAssets = resolveSelectedSkillAssets(effectiveBundle.skills, params.input?.skills)
|
|
1120
|
+
const promptAssetIds = new Set<string>([
|
|
1121
|
+
...effectiveBundle.rules.map(asset => asset.id),
|
|
1122
|
+
...effectiveBundle.specs.map(asset => asset.id),
|
|
1123
|
+
...selectedSkillAssets.map(asset => asset.id),
|
|
1124
|
+
...(params.type !== 'entity' ? effectiveBundle.entities.map(asset => asset.id) : [])
|
|
1125
|
+
])
|
|
1126
|
+
const ruleDefinitions = new Map<string, Definition<Rule>>(
|
|
1127
|
+
effectiveBundle.rules.map(asset => [
|
|
1128
|
+
asset.id,
|
|
1129
|
+
definitionWithResolvedName(asset.payload.definition, asset.displayName, asset.instancePath)
|
|
1130
|
+
])
|
|
1131
|
+
)
|
|
1132
|
+
const targetSkillsAssets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>> = []
|
|
1133
|
+
|
|
1134
|
+
if (pinnedTargetAsset != null) {
|
|
1135
|
+
promptAssetIds.add(pinnedTargetAsset.id)
|
|
1136
|
+
const attributes = pinnedTargetAsset.payload.definition.attributes
|
|
1137
|
+
|
|
1138
|
+
if (attributes.rules != null) {
|
|
1139
|
+
const selection = await resolveRuleSelection(
|
|
1140
|
+
effectiveBundle,
|
|
1141
|
+
attributes.rules as RuleReference[] | string[],
|
|
1142
|
+
targetInstancePath
|
|
1143
|
+
)
|
|
1144
|
+
for (const asset of selection.assets) {
|
|
1145
|
+
promptAssetIds.add(asset.id)
|
|
1146
|
+
ruleDefinitions.set(
|
|
1147
|
+
asset.id,
|
|
1148
|
+
definitionWithResolvedName(
|
|
1149
|
+
{
|
|
1150
|
+
...asset.payload.definition,
|
|
1151
|
+
attributes: {
|
|
1152
|
+
...asset.payload.definition.attributes,
|
|
1153
|
+
always: true
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
asset.displayName,
|
|
1157
|
+
asset.instancePath
|
|
1158
|
+
)
|
|
1159
|
+
)
|
|
1160
|
+
}
|
|
1161
|
+
selection.remoteDefinitions.forEach((definition) => {
|
|
1162
|
+
ruleDefinitions.set(definition.path, definition)
|
|
1163
|
+
})
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const skillSelection = attributes.skills as string[] | SkillSelection | undefined
|
|
1167
|
+
const includedRefs = resolveIncludedSkillRefs(skillSelection)
|
|
1168
|
+
const excludedRefs = resolveExcludedSkillRefs(skillSelection)
|
|
1169
|
+
const includedAssets = skillSelection == null
|
|
1170
|
+
? []
|
|
1171
|
+
: includedRefs != null
|
|
1172
|
+
? (includedRefs.length > 0 ? resolveNamedAssets(effectiveBundle.skills, includedRefs, targetInstancePath) : [])
|
|
1173
|
+
: effectiveBundle.skills
|
|
1174
|
+
const excludedIds = new Set(
|
|
1175
|
+
resolveNamedAssets(effectiveBundle.skills, excludedRefs, targetInstancePath).map(asset => asset.id)
|
|
1176
|
+
)
|
|
1177
|
+
|
|
1178
|
+
includedAssets
|
|
1179
|
+
.filter(asset => !excludedIds.has(asset.id))
|
|
1180
|
+
.forEach((asset) => {
|
|
1181
|
+
targetSkillsAssets.push(asset)
|
|
1182
|
+
promptAssetIds.add(asset.id)
|
|
1183
|
+
})
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const rules = Array.from(ruleDefinitions.values())
|
|
1187
|
+
const targetSkills = toDocumentDefinitions(targetSkillsAssets)
|
|
1188
|
+
const routedSkills = toDocumentDefinitions(
|
|
1189
|
+
selectedSkillAssets.filter(skill => !targetSkillsAssets.some(target => target.id === skill.id))
|
|
1190
|
+
)
|
|
1191
|
+
const entities = params.type !== 'entity'
|
|
1192
|
+
? toDocumentDefinitions(effectiveBundle.entities)
|
|
1193
|
+
: []
|
|
1194
|
+
const skills = toDocumentDefinitions(selectedSkillAssets)
|
|
1195
|
+
const specs = toDocumentDefinitions(effectiveBundle.specs)
|
|
1196
|
+
|
|
1197
|
+
options.systemPrompt = [
|
|
1198
|
+
generateRulesPrompt(effectiveBundle.cwd, rules),
|
|
1199
|
+
generateSkillsPrompt(effectiveBundle.cwd, targetSkills),
|
|
1200
|
+
generateEntitiesRoutePrompt(entities),
|
|
1201
|
+
generateSkillsRoutePrompt(effectiveBundle.cwd, routedSkills),
|
|
1202
|
+
generateSpecRoutePrompt(specs, { active: params.type === 'spec' }),
|
|
1203
|
+
targetBody
|
|
1204
|
+
].join('\n\n')
|
|
1205
|
+
|
|
1206
|
+
if (targetToolsFilter != null) {
|
|
1207
|
+
options.tools = targetToolsFilter
|
|
1208
|
+
}
|
|
1209
|
+
if (targetMcpServersFilter != null) {
|
|
1210
|
+
options.mcpServers = targetMcpServersFilter
|
|
1211
|
+
}
|
|
1212
|
+
options.promptAssetIds = Array.from(promptAssetIds)
|
|
1213
|
+
|
|
1214
|
+
return [
|
|
1215
|
+
{
|
|
1216
|
+
rules,
|
|
1217
|
+
targetSkills,
|
|
1218
|
+
entities,
|
|
1219
|
+
skills,
|
|
1220
|
+
specs,
|
|
1221
|
+
targetBody,
|
|
1222
|
+
promptAssetIds: Array.from(promptAssetIds)
|
|
1223
|
+
},
|
|
1224
|
+
options
|
|
1225
|
+
] as const
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
export function buildAdapterAssetPlan(params: {
|
|
1229
|
+
adapter: WorkspaceAssetAdapter
|
|
1230
|
+
bundle: WorkspaceAssetBundle
|
|
1231
|
+
options: {
|
|
1232
|
+
mcpServers?: WorkspaceMcpSelection
|
|
1233
|
+
skills?: WorkspaceSkillSelection
|
|
1234
|
+
promptAssetIds?: string[]
|
|
1235
|
+
}
|
|
1236
|
+
}): AdapterAssetPlan {
|
|
1237
|
+
const diagnostics: AssetDiagnostic[] = []
|
|
1238
|
+
|
|
1239
|
+
for (const assetId of params.options.promptAssetIds ?? []) {
|
|
1240
|
+
const asset = params.bundle.assets.find(item => item.id === assetId)
|
|
1241
|
+
if (asset == null || asset.kind === 'mcpServer') continue
|
|
1242
|
+
diagnostics.push({
|
|
1243
|
+
assetId,
|
|
1244
|
+
adapter: params.adapter,
|
|
1245
|
+
status: 'prompt',
|
|
1246
|
+
reason: 'Mapped into the generated system prompt.',
|
|
1247
|
+
packageId: asset.packageId,
|
|
1248
|
+
scope: asset.scope,
|
|
1249
|
+
instancePath: asset.instancePath,
|
|
1250
|
+
origin: asset.origin,
|
|
1251
|
+
resolvedBy: asset.resolvedBy,
|
|
1252
|
+
taskOverlaySource: asset.taskOverlaySource
|
|
1253
|
+
})
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const selectedMcpNames = resolveSelectedMcpNames(params.bundle, params.options.mcpServers)
|
|
1257
|
+
const mcpServers = Object.fromEntries(
|
|
1258
|
+
selectedMcpNames.map(name => [name, params.bundle.mcpServers[name].payload.config])
|
|
1259
|
+
)
|
|
1260
|
+
|
|
1261
|
+
selectedMcpNames.forEach((name) => {
|
|
1262
|
+
const asset = params.bundle.mcpServers[name]
|
|
1263
|
+
diagnostics.push({
|
|
1264
|
+
assetId: asset.id,
|
|
1265
|
+
adapter: params.adapter,
|
|
1266
|
+
status: params.adapter === 'claude-code' ? 'native' : 'translated',
|
|
1267
|
+
reason: params.adapter === 'claude-code'
|
|
1268
|
+
? 'Mapped into adapter MCP settings.'
|
|
1269
|
+
: 'Translated into adapter-specific MCP configuration.',
|
|
1270
|
+
packageId: asset.packageId,
|
|
1271
|
+
scope: asset.scope,
|
|
1272
|
+
instancePath: asset.instancePath,
|
|
1273
|
+
origin: asset.origin,
|
|
1274
|
+
resolvedBy: asset.resolvedBy,
|
|
1275
|
+
taskOverlaySource: asset.taskOverlaySource
|
|
1276
|
+
})
|
|
1277
|
+
})
|
|
1278
|
+
|
|
1279
|
+
params.bundle.hookPlugins.forEach((asset) => {
|
|
1280
|
+
diagnostics.push({
|
|
1281
|
+
assetId: asset.id,
|
|
1282
|
+
adapter: params.adapter,
|
|
1283
|
+
status: 'native',
|
|
1284
|
+
reason: params.adapter === 'claude-code'
|
|
1285
|
+
? 'Mapped into the Claude Code native hooks bridge.'
|
|
1286
|
+
: params.adapter === 'codex'
|
|
1287
|
+
? 'Mapped into the Codex native hooks bridge.'
|
|
1288
|
+
: 'Mapped into the OpenCode native hooks bridge.',
|
|
1289
|
+
packageId: asset.packageId,
|
|
1290
|
+
scope: asset.scope,
|
|
1291
|
+
instancePath: asset.instancePath,
|
|
1292
|
+
origin: asset.origin,
|
|
1293
|
+
resolvedBy: asset.resolvedBy,
|
|
1294
|
+
taskOverlaySource: asset.taskOverlaySource
|
|
1295
|
+
})
|
|
1296
|
+
})
|
|
1297
|
+
|
|
1298
|
+
const selectedSkillAssets = resolveSelectedSkillAssets(params.bundle.skills, params.options.skills)
|
|
1299
|
+
if (params.adapter === 'opencode') {
|
|
1300
|
+
selectedSkillAssets.forEach((asset) => {
|
|
1301
|
+
diagnostics.push({
|
|
1302
|
+
assetId: asset.id,
|
|
1303
|
+
adapter: params.adapter,
|
|
1304
|
+
status: 'native',
|
|
1305
|
+
reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native skill.',
|
|
1306
|
+
packageId: asset.packageId,
|
|
1307
|
+
scope: asset.scope,
|
|
1308
|
+
instancePath: asset.instancePath,
|
|
1309
|
+
origin: asset.origin,
|
|
1310
|
+
resolvedBy: asset.resolvedBy,
|
|
1311
|
+
taskOverlaySource: asset.taskOverlaySource
|
|
1312
|
+
})
|
|
1313
|
+
})
|
|
1314
|
+
params.bundle.opencodeOverlayAssets.forEach((asset) => {
|
|
1315
|
+
diagnostics.push({
|
|
1316
|
+
assetId: asset.id,
|
|
1317
|
+
adapter: params.adapter,
|
|
1318
|
+
status: 'native',
|
|
1319
|
+
reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native OpenCode asset.',
|
|
1320
|
+
packageId: asset.packageId,
|
|
1321
|
+
scope: asset.scope,
|
|
1322
|
+
instancePath: asset.instancePath,
|
|
1323
|
+
origin: asset.origin,
|
|
1324
|
+
resolvedBy: asset.resolvedBy,
|
|
1325
|
+
taskOverlaySource: asset.taskOverlaySource
|
|
1326
|
+
})
|
|
1327
|
+
})
|
|
1328
|
+
} else if (params.adapter === 'codex') {
|
|
1329
|
+
params.bundle.opencodeOverlayAssets.forEach((asset) => {
|
|
1330
|
+
diagnostics.push({
|
|
1331
|
+
assetId: asset.id,
|
|
1332
|
+
adapter: params.adapter,
|
|
1333
|
+
status: 'skipped',
|
|
1334
|
+
reason: 'No stable native Codex mapping exists for this asset kind in V1.',
|
|
1335
|
+
packageId: asset.packageId,
|
|
1336
|
+
scope: asset.scope,
|
|
1337
|
+
instancePath: asset.instancePath,
|
|
1338
|
+
origin: asset.origin,
|
|
1339
|
+
resolvedBy: asset.resolvedBy,
|
|
1340
|
+
taskOverlaySource: asset.taskOverlaySource
|
|
1341
|
+
})
|
|
1342
|
+
})
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const overlays: AdapterOverlayEntry[] = params.adapter === 'opencode'
|
|
1346
|
+
? [
|
|
1347
|
+
...selectedSkillAssets.map((asset): AdapterOverlayEntry => ({
|
|
1348
|
+
assetId: asset.id,
|
|
1349
|
+
kind: 'skill',
|
|
1350
|
+
sourcePath: dirname(asset.sourcePath),
|
|
1351
|
+
targetPath: `skills/${asset.displayName.replaceAll('/', '__')}`
|
|
1352
|
+
})),
|
|
1353
|
+
...params.bundle.opencodeOverlayAssets.map((asset): AdapterOverlayEntry => ({
|
|
1354
|
+
assetId: asset.id,
|
|
1355
|
+
kind: asset.kind,
|
|
1356
|
+
sourcePath: asset.sourcePath,
|
|
1357
|
+
targetPath: asset.payload.targetSubpath
|
|
1358
|
+
}))
|
|
1359
|
+
]
|
|
1360
|
+
: []
|
|
1361
|
+
|
|
1362
|
+
return {
|
|
1363
|
+
adapter: params.adapter,
|
|
1364
|
+
diagnostics,
|
|
1365
|
+
mcpServers,
|
|
1366
|
+
overlays
|
|
1367
|
+
}
|
|
1368
|
+
}
|