@vibe-forge/core 0.7.5 → 0.8.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.
Files changed (43) hide show
  1. package/package.json +4 -46
  2. package/src/env.ts +5 -25
  3. package/src/index.ts +0 -5
  4. package/src/types.ts +12 -72
  5. package/src/ws.ts +3 -12
  6. package/src/adapter/index.ts +0 -6
  7. package/src/adapter/loader.ts +0 -11
  8. package/src/adapter/type.ts +0 -117
  9. package/src/config/load.ts +0 -122
  10. package/src/config/types.ts +0 -289
  11. package/src/config.ts +0 -2
  12. package/src/controllers/benchmark/discover.ts +0 -89
  13. package/src/controllers/benchmark/index.ts +0 -24
  14. package/src/controllers/benchmark/result-store.ts +0 -46
  15. package/src/controllers/benchmark/runner.ts +0 -415
  16. package/src/controllers/benchmark/schema.ts +0 -60
  17. package/src/controllers/benchmark/types.ts +0 -80
  18. package/src/controllers/benchmark/utils.ts +0 -144
  19. package/src/controllers/benchmark/workspace.ts +0 -179
  20. package/src/controllers/config/index.ts +0 -214
  21. package/src/controllers/system/assets/completed.mp3 +0 -0
  22. package/src/controllers/system/assets/mcp.png +0 -0
  23. package/src/controllers/system/index.ts +0 -102
  24. package/src/controllers/task/generate-adapter-query-options.ts +0 -25
  25. package/src/controllers/task/index.ts +0 -2
  26. package/src/controllers/task/prepare.ts +0 -74
  27. package/src/controllers/task/run.ts +0 -231
  28. package/src/controllers/task/schema.ts +0 -131
  29. package/src/controllers/task/type.ts +0 -6
  30. package/src/hooks/bridge.ts +0 -368
  31. package/src/hooks/call.ts +0 -74
  32. package/src/hooks/index.ts +0 -41
  33. package/src/hooks/loader.ts +0 -79
  34. package/src/hooks/native.ts +0 -116
  35. package/src/hooks/runtime.ts +0 -139
  36. package/src/hooks/type.ts +0 -145
  37. package/src/utils/cache.ts +0 -58
  38. package/src/utils/create-logger.ts +0 -89
  39. package/src/utils/definition-loader.ts +0 -530
  40. package/src/utils/filter.ts +0 -26
  41. package/src/utils/string-transform.ts +0 -37
  42. package/src/utils/uuid.ts +0 -6
  43. package/src/utils/workspace-assets.ts +0 -919
@@ -1,919 +0,0 @@
1
- import { readFile } from 'node:fs/promises'
2
- import process from 'node:process'
3
- import { basename, dirname, extname, relative } from 'node:path'
4
-
5
- import { glob } from 'fast-glob'
6
- import yaml from 'js-yaml'
7
-
8
- import type { Config } from '../config'
9
- import { loadConfig } from '../config/load'
10
- import {
11
- DefinitionLoader,
12
- type Definition,
13
- type Entity,
14
- type Filter,
15
- type Rule,
16
- type Skill,
17
- type Spec
18
- } from './definition-loader'
19
-
20
- export type WorkspaceAssetKind =
21
- | 'rule'
22
- | 'spec'
23
- | 'entity'
24
- | 'skill'
25
- | 'mcpServer'
26
- | 'hookPlugin'
27
- | 'nativePlugin'
28
- | 'agent'
29
- | 'command'
30
- | 'mode'
31
-
32
- export type WorkspaceAssetAdapter = 'claude-code' | 'codex' | 'opencode'
33
-
34
- export type AssetDiagnosticStatus = 'native' | 'translated' | 'prompt' | 'skipped'
35
-
36
- export interface AssetDiagnostic {
37
- assetId: string
38
- adapter: WorkspaceAssetAdapter
39
- status: AssetDiagnosticStatus
40
- reason: string
41
- }
42
-
43
- export interface AdapterOverlayEntry {
44
- assetId: string
45
- kind: Extract<WorkspaceAssetKind, 'skill' | 'nativePlugin' | 'agent' | 'command' | 'mode'>
46
- sourcePath: string
47
- targetPath: string
48
- }
49
-
50
- interface WorkspaceAssetBase<TKind extends WorkspaceAssetKind, TPayload> {
51
- id: string
52
- kind: TKind
53
- pluginId?: string
54
- origin: 'project' | 'plugin' | 'config' | 'fallback'
55
- scope: 'workspace' | 'project' | 'user' | 'adapter'
56
- enabled: boolean
57
- targets: WorkspaceAssetAdapter[]
58
- payload: TPayload
59
- }
60
-
61
- interface WorkspaceDocumentPayload<TDefinition> {
62
- definition: TDefinition
63
- sourcePath: string
64
- }
65
-
66
- interface WorkspaceHookPluginPayload {
67
- packageName?: string
68
- config: unknown
69
- }
70
-
71
- interface WorkspaceMcpPayload {
72
- name: string
73
- config: NonNullable<Config['mcpServers']>[string]
74
- }
75
-
76
- interface WorkspaceOverlayPayload {
77
- sourcePath: string
78
- entryName: string
79
- targetSubpath: string
80
- }
81
-
82
- interface WorkspaceNativePluginPayload {
83
- name: string
84
- enabled: boolean
85
- }
86
-
87
- export type WorkspaceAsset =
88
- | WorkspaceAssetBase<'rule', WorkspaceDocumentPayload<Definition<Rule>>>
89
- | WorkspaceAssetBase<'spec', WorkspaceDocumentPayload<Definition<Spec>>>
90
- | WorkspaceAssetBase<'entity', WorkspaceDocumentPayload<Definition<Entity>>>
91
- | WorkspaceAssetBase<'skill', WorkspaceDocumentPayload<Definition<Skill>>>
92
- | WorkspaceAssetBase<'mcpServer', WorkspaceMcpPayload>
93
- | WorkspaceAssetBase<'hookPlugin', WorkspaceHookPluginPayload>
94
- | WorkspaceAssetBase<'nativePlugin', WorkspaceNativePluginPayload>
95
- | WorkspaceAssetBase<'agent', WorkspaceOverlayPayload>
96
- | WorkspaceAssetBase<'command', WorkspaceOverlayPayload>
97
- | WorkspaceAssetBase<'mode', WorkspaceOverlayPayload>
98
-
99
- export interface WorkspaceAssetBundle {
100
- cwd: string
101
- assets: WorkspaceAsset[]
102
- rules: Array<Extract<WorkspaceAsset, { kind: 'rule' }>>
103
- specs: Array<Extract<WorkspaceAsset, { kind: 'spec' }>>
104
- entities: Array<Extract<WorkspaceAsset, { kind: 'entity' }>>
105
- skills: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>
106
- mcpServers: Record<string, Extract<WorkspaceAsset, { kind: 'mcpServer' }>>
107
- hookPlugins: Array<Extract<WorkspaceAsset, { kind: 'hookPlugin' }>>
108
- enabledPlugins: Record<string, boolean>
109
- extraKnownMarketplaces: Config['extraKnownMarketplaces']
110
- defaultIncludeMcpServers: string[]
111
- defaultExcludeMcpServers: string[]
112
- }
113
-
114
- export interface PromptAssetResolution {
115
- rules: Definition<Rule>[]
116
- targetSkills: Definition<Skill>[]
117
- entities: Definition<Entity>[]
118
- skills: Definition<Skill>[]
119
- specs: Definition<Spec>[]
120
- targetBody: string
121
- promptAssetIds: string[]
122
- }
123
-
124
- export interface WorkspaceSkillSelection {
125
- include?: string[]
126
- exclude?: string[]
127
- }
128
-
129
- export interface WorkspaceMcpSelection {
130
- include?: string[]
131
- exclude?: string[]
132
- }
133
-
134
- export interface ResolvedPromptAssetOptions {
135
- systemPrompt?: string
136
- tools?: Filter
137
- mcpServers?: WorkspaceMcpSelection
138
- promptAssetIds?: string[]
139
- }
140
-
141
- export interface AdapterAssetPlan {
142
- adapter: WorkspaceAssetAdapter
143
- diagnostics: AssetDiagnostic[]
144
- mcpServers: Record<string, NonNullable<Config['mcpServers']>[string]>
145
- overlays: AdapterOverlayEntry[]
146
- native: {
147
- enabledPlugins?: Record<string, boolean>
148
- extraKnownMarketplaces?: Config['extraKnownMarketplaces']
149
- codexHooks?: {
150
- supportedEvents: string[]
151
- }
152
- }
153
- }
154
-
155
- const normalizePath = (value: string) => value.split('\\').join('/')
156
-
157
- const resolveRelativePath = (cwd: string, value: string) => normalizePath(relative(cwd, value))
158
-
159
- const resolveDocumentName = (
160
- path: string,
161
- explicitName?: string,
162
- indexFileNames: string[] = []
163
- ) => {
164
- const trimmedName = explicitName?.trim()
165
- if (trimmedName) return trimmedName
166
-
167
- const fileName = basename(path).toLowerCase()
168
- if (indexFileNames.includes(fileName)) {
169
- return basename(dirname(path))
170
- }
171
-
172
- return basename(path).replace(/\.[^/.]+$/, '')
173
- }
174
-
175
- const resolveSpecIdentifier = (path: string, explicitName?: string) => (
176
- resolveDocumentName(path, explicitName, ['index.md'])
177
- )
178
-
179
- const resolvePluginIdFromPath = (cwd: string, path: string) => {
180
- const relativePath = resolveRelativePath(cwd, path)
181
- const match = relativePath.match(/^\.ai\/plugins\/([^/]+)\//)
182
- return match?.[1]
183
- }
184
-
185
- const isPluginEnabled = (
186
- enabledPlugins: Record<string, boolean>,
187
- pluginId?: string
188
- ) => pluginId == null || enabledPlugins[pluginId] !== false
189
-
190
- const mergeRecord = <T>(left?: Record<string, T>, right?: Record<string, T>) => ({
191
- ...(left ?? {}),
192
- ...(right ?? {})
193
- })
194
-
195
- const uniqueValues = (values: string[]) => Array.from(new Set(values.filter(Boolean)))
196
-
197
- const assetOriginPriority: Record<WorkspaceAsset['origin'], number> = {
198
- project: 0,
199
- plugin: 1,
200
- config: 2,
201
- fallback: 3
202
- }
203
-
204
- const readConfigForWorkspace = async (cwd: string) => {
205
- const jsonVariables: Record<string, string | null | undefined> = {
206
- ...process.env,
207
- WORKSPACE_FOLDER: cwd,
208
- __VF_PROJECT_WORKSPACE_FOLDER__: cwd
209
- }
210
- return loadConfig({ jsonVariables })
211
- }
212
-
213
- const toAssetScope = (origin: WorkspaceAsset['origin']): WorkspaceAsset['scope'] => (
214
- origin === 'config'
215
- ? 'project'
216
- : origin === 'fallback'
217
- ? 'adapter'
218
- : 'workspace'
219
- )
220
-
221
- const pushAsset = <TAsset extends WorkspaceAsset>(
222
- collection: TAsset[],
223
- next: TAsset
224
- ) => {
225
- collection.push(next)
226
- return next
227
- }
228
-
229
- const createDocumentAsset = <TKind extends Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>, TDefinition>(
230
- params: {
231
- cwd: string
232
- kind: TKind
233
- definition: TDefinition & { path: string }
234
- targets?: WorkspaceAssetAdapter[]
235
- }
236
- ): Extract<WorkspaceAsset, { kind: TKind }> => {
237
- const pluginId = resolvePluginIdFromPath(params.cwd, params.definition.path)
238
- const origin: WorkspaceAsset['origin'] = pluginId == null ? 'project' : 'plugin'
239
- return {
240
- id: `${params.kind}:${resolveRelativePath(params.cwd, params.definition.path)}`,
241
- kind: params.kind,
242
- pluginId,
243
- origin,
244
- scope: toAssetScope(origin),
245
- enabled: true,
246
- targets: params.targets ?? ['claude-code', 'codex', 'opencode'],
247
- payload: {
248
- definition: params.definition as any,
249
- sourcePath: params.definition.path
250
- }
251
- } as Extract<WorkspaceAsset, { kind: TKind }>
252
- }
253
-
254
- const parseStructuredDocument = async (path: string) => {
255
- const raw = await readFile(path, 'utf8')
256
- const extension = extname(path).toLowerCase()
257
- if (extension === '.yaml' || extension === '.yml') {
258
- return yaml.load(raw)
259
- }
260
- return JSON.parse(raw)
261
- }
262
-
263
- const loadPluginMcpAssets = async (
264
- cwd: string,
265
- enabledPlugins: Record<string, boolean>
266
- ) => {
267
- const paths = await glob([
268
- '.ai/plugins/*/mcp/*.json',
269
- '.ai/plugins/*/mcp/*.yaml',
270
- '.ai/plugins/*/mcp/*.yml'
271
- ], {
272
- cwd,
273
- absolute: true
274
- })
275
-
276
- const entries = await Promise.all(paths.map(async (path) => {
277
- const pluginId = resolvePluginIdFromPath(cwd, path)
278
- if (!isPluginEnabled(enabledPlugins, pluginId)) return undefined
279
-
280
- const parsed = await parseStructuredDocument(path)
281
- if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined
282
-
283
- const record = parsed as Record<string, unknown>
284
- const name = typeof record.name === 'string' && record.name.trim() !== ''
285
- ? record.name.trim()
286
- : basename(path, extname(path))
287
- const { name: _name, ...config } = record
288
-
289
- return {
290
- id: `mcpServer:${resolveRelativePath(cwd, path)}`,
291
- kind: 'mcpServer',
292
- pluginId,
293
- origin: 'plugin',
294
- scope: 'workspace',
295
- enabled: true,
296
- targets: ['claude-code', 'codex', 'opencode'],
297
- payload: {
298
- name,
299
- config: config as NonNullable<Config['mcpServers']>[string]
300
- }
301
- } satisfies Extract<WorkspaceAsset, { kind: 'mcpServer' }>
302
- }))
303
-
304
- return entries.filter((entry): entry is NonNullable<typeof entry> => entry != null)
305
- }
306
-
307
- const loadOpenCodeOverlayAssets = async (
308
- cwd: string,
309
- enabledPlugins: Record<string, boolean>
310
- ) => {
311
- const paths = await glob([
312
- '.ai/plugins/*/opencode/plugins/*',
313
- '.ai/plugins/*/opencode/agents/*',
314
- '.ai/plugins/*/opencode/commands/*',
315
- '.ai/plugins/*/opencode/modes/*'
316
- ], {
317
- cwd,
318
- absolute: true,
319
- onlyFiles: false
320
- })
321
-
322
- return paths
323
- .map((path) => {
324
- const relativePath = resolveRelativePath(cwd, path)
325
- const match = relativePath.match(/^\.ai\/plugins\/([^/]+)\/opencode\/(plugins|agents|commands|modes)\/([^/]+)$/)
326
- if (!match) return undefined
327
-
328
- const [, pluginId, rawFolder, entryName] = match
329
- if (!isPluginEnabled(enabledPlugins, pluginId)) return undefined
330
-
331
- const kind = {
332
- plugins: 'nativePlugin',
333
- agents: 'agent',
334
- commands: 'command',
335
- modes: 'mode'
336
- }[rawFolder] as Extract<WorkspaceAssetKind, 'nativePlugin' | 'agent' | 'command' | 'mode'>
337
-
338
- return {
339
- id: `${kind}:${relativePath}`,
340
- kind,
341
- pluginId,
342
- origin: 'plugin',
343
- scope: 'workspace',
344
- enabled: true,
345
- targets: ['opencode'],
346
- payload: {
347
- sourcePath: path,
348
- entryName,
349
- targetSubpath: `${rawFolder}/${entryName}`
350
- }
351
- } satisfies Extract<WorkspaceAsset, { kind: typeof kind }>
352
- })
353
- .filter((entry): entry is NonNullable<typeof entry> => entry != null)
354
- }
355
-
356
- const createHookPluginAssets = (
357
- config: Config['plugins'],
358
- enabledPlugins: Record<string, boolean>,
359
- scope: Extract<WorkspaceAsset['scope'], 'project' | 'user'>
360
- ) => {
361
- if (config == null || Array.isArray(config)) return [] as Array<Extract<WorkspaceAsset, { kind: 'hookPlugin' }>>
362
-
363
- return Object.entries(config)
364
- .filter((entry) => enabledPlugins[entry[0]] !== false)
365
- .map(([pluginId, pluginConfig]) => ({
366
- id: `hookPlugin:${scope}:${pluginId}`,
367
- kind: 'hookPlugin',
368
- pluginId,
369
- origin: 'config',
370
- scope,
371
- enabled: true,
372
- targets: ['claude-code', 'codex', 'opencode'],
373
- payload: {
374
- packageName: pluginId,
375
- config: pluginConfig
376
- }
377
- } satisfies Extract<WorkspaceAsset, { kind: 'hookPlugin' }>))
378
- }
379
-
380
- const createClaudeNativePluginAssets = (
381
- enabledPlugins: Record<string, boolean>
382
- ) => {
383
- return Object.entries(enabledPlugins).map(([pluginId, enabled]) => ({
384
- id: `nativePlugin:claude-code:${pluginId}`,
385
- kind: 'nativePlugin',
386
- pluginId,
387
- origin: 'config',
388
- scope: 'project',
389
- enabled,
390
- targets: ['claude-code'],
391
- payload: {
392
- name: pluginId,
393
- enabled
394
- }
395
- } satisfies Extract<WorkspaceAsset, { kind: 'nativePlugin' }>))
396
- }
397
-
398
- const dedupeDocumentAssets = <TAsset extends Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>>(
399
- assets: TAsset[],
400
- enabledPlugins: Record<string, boolean>
401
- ) => assets.filter((asset) => isPluginEnabled(enabledPlugins, asset.pluginId))
402
-
403
- const compareDocumentAssetPriority = (
404
- left: Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>,
405
- right: Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>
406
- ) => {
407
- const originDiff = assetOriginPriority[left.origin] - assetOriginPriority[right.origin]
408
- if (originDiff !== 0) return originDiff
409
- return left.payload.definition.path.localeCompare(right.payload.definition.path)
410
- }
411
-
412
- const dedupeDocumentAssetsByIdentifier = <TAsset extends Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>>(
413
- assets: TAsset[],
414
- resolveIdentifier: (asset: TAsset) => string
415
- ) => {
416
- const selected = new Map<string, TAsset>()
417
-
418
- for (const asset of [...assets].sort(compareDocumentAssetPriority)) {
419
- const identifier = resolveIdentifier(asset)
420
- if (!selected.has(identifier)) selected.set(identifier, asset)
421
- }
422
-
423
- return Array.from(selected.values()).sort(compareDocumentAssetPriority)
424
- }
425
-
426
- const resolveRuleIdentifier = (
427
- path: string,
428
- explicitName?: string
429
- ) => resolveDocumentName(path, explicitName)
430
-
431
- const resolveSkillIdentifier = (
432
- path: string,
433
- explicitName?: string
434
- ) => resolveDocumentName(path, explicitName, ['skill.md'])
435
-
436
- const pickSpecAsset = (
437
- bundle: WorkspaceAssetBundle,
438
- name: string
439
- ) => {
440
- const assets = bundle.specs.filter((asset) => {
441
- const definition = asset.payload.definition
442
- return resolveSpecIdentifier(definition.path, definition.attributes.name) === name
443
- })
444
- return assets.find(asset => asset.origin === 'project') ?? assets[0]
445
- }
446
-
447
- const pickEntityAsset = (
448
- bundle: WorkspaceAssetBundle,
449
- name: string
450
- ) => {
451
- const assets = bundle.entities.filter((asset) => {
452
- const definition = asset.payload.definition
453
- const identifier = resolveDocumentName(definition.path, definition.attributes.name, ['readme.md', 'index.json'])
454
- return identifier === name
455
- })
456
- return assets.find(asset => asset.origin === 'project') ?? assets[0]
457
- }
458
-
459
- const filterSkillAssets = (
460
- skills: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>,
461
- selection?: WorkspaceSkillSelection
462
- ) => {
463
- if (selection == null) return skills
464
-
465
- const include = selection.include != null && selection.include.length > 0
466
- ? new Set(selection.include)
467
- : undefined
468
- const exclude = new Set(selection.exclude ?? [])
469
-
470
- return skills.filter((skill) => {
471
- const name = basename(dirname(skill.payload.definition.path))
472
- return (include == null || include.has(name)) && !exclude.has(name)
473
- })
474
- }
475
-
476
- const dedupeSkillAssets = (skills: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>) => {
477
- const seen = new Set<string>()
478
- return skills.filter((skill) => {
479
- if (seen.has(skill.payload.definition.path)) return false
480
- seen.add(skill.payload.definition.path)
481
- return true
482
- })
483
- }
484
-
485
- const resolveSelectedRuleAssets = async (
486
- bundle: WorkspaceAssetBundle,
487
- patterns: string[]
488
- ) => {
489
- const matchedPaths = new Set(
490
- (await glob(patterns, { cwd: bundle.cwd, absolute: true }))
491
- .map(normalizePath)
492
- )
493
- return bundle.rules.filter((asset) => matchedPaths.has(normalizePath(asset.payload.definition.path)))
494
- }
495
-
496
- const toDocumentDefinitions = <TDefinition>(
497
- assets: Array<WorkspaceDocumentAsset<TDefinition>>
498
- ) => assets.map(asset => asset.payload.definition)
499
-
500
- type WorkspaceDocumentAsset<TDefinition> = Extract<
501
- WorkspaceAsset,
502
- { kind: 'rule' | 'spec' | 'entity' | 'skill' }
503
- > & {
504
- payload: WorkspaceDocumentPayload<TDefinition & { path: string }>
505
- }
506
-
507
- const toPromptAssetIds = (assets: Array<{ id: string }>) => uniqueValues(assets.map(asset => asset.id))
508
-
509
- export async function resolveWorkspaceAssetBundle(params: {
510
- cwd: string
511
- configs?: [Config?, Config?]
512
- }): Promise<WorkspaceAssetBundle> {
513
- const [config, userConfig] = params.configs ?? await readConfigForWorkspace(params.cwd)
514
- const enabledPlugins = mergeRecord(config?.enabledPlugins, userConfig?.enabledPlugins)
515
- const extraKnownMarketplaces = mergeRecord(config?.extraKnownMarketplaces, userConfig?.extraKnownMarketplaces)
516
- const loader = new DefinitionLoader(params.cwd)
517
-
518
- const [
519
- rawRules,
520
- rawSpecs,
521
- rawEntities,
522
- rawSkills,
523
- pluginMcpAssets,
524
- openCodeOverlayAssets
525
- ] = await Promise.all([
526
- loader.loadDefaultRules(),
527
- loader.loadDefaultSpecs(),
528
- loader.loadDefaultEntities(),
529
- loader.loadDefaultSkills(),
530
- loadPluginMcpAssets(params.cwd, enabledPlugins),
531
- loadOpenCodeOverlayAssets(params.cwd, enabledPlugins)
532
- ])
533
-
534
- const assets: WorkspaceAsset[] = []
535
-
536
- const rules = dedupeDocumentAssetsByIdentifier(
537
- dedupeDocumentAssets(
538
- rawRules.map((definition) => createDocumentAsset({ cwd: params.cwd, kind: 'rule', definition })),
539
- enabledPlugins
540
- ),
541
- asset => resolveRuleIdentifier(asset.payload.definition.path, asset.payload.definition.attributes.name)
542
- )
543
- const specs = dedupeDocumentAssetsByIdentifier(
544
- dedupeDocumentAssets(
545
- rawSpecs.map((definition) => createDocumentAsset({ cwd: params.cwd, kind: 'spec', definition })),
546
- enabledPlugins
547
- ),
548
- asset => resolveSpecIdentifier(asset.payload.definition.path, asset.payload.definition.attributes.name)
549
- )
550
- const entities = dedupeDocumentAssetsByIdentifier(
551
- dedupeDocumentAssets(
552
- rawEntities.map((definition) => createDocumentAsset({ cwd: params.cwd, kind: 'entity', definition })),
553
- enabledPlugins
554
- ),
555
- asset => resolveDocumentName(
556
- asset.payload.definition.path,
557
- asset.payload.definition.attributes.name,
558
- ['readme.md', 'index.json']
559
- )
560
- )
561
- const skills = dedupeDocumentAssetsByIdentifier(
562
- dedupeDocumentAssets(
563
- rawSkills.map((definition) => createDocumentAsset({ cwd: params.cwd, kind: 'skill', definition })),
564
- enabledPlugins
565
- ),
566
- asset => resolveSkillIdentifier(asset.payload.definition.path, asset.payload.definition.attributes.name)
567
- )
568
-
569
- assets.push(...rules, ...specs, ...entities, ...skills)
570
-
571
- const mcpServers = new Map<string, Extract<WorkspaceAsset, { kind: 'mcpServer' }>>()
572
- const userMcpServers = userConfig?.mcpServers ?? {}
573
- for (const [name, serverConfig] of Object.entries(userMcpServers)) {
574
- mcpServers.set(name, {
575
- id: `mcpServer:user:${name}`,
576
- kind: 'mcpServer',
577
- origin: 'config',
578
- scope: 'user',
579
- enabled: true,
580
- targets: ['claude-code', 'codex', 'opencode'],
581
- payload: {
582
- name,
583
- config: serverConfig
584
- }
585
- })
586
- }
587
- for (const asset of pluginMcpAssets) {
588
- mcpServers.set(asset.payload.name, asset)
589
- }
590
- for (const [name, serverConfig] of Object.entries(config?.mcpServers ?? {})) {
591
- mcpServers.set(name, {
592
- id: `mcpServer:project:${name}`,
593
- kind: 'mcpServer',
594
- origin: 'config',
595
- scope: 'project',
596
- enabled: true,
597
- targets: ['claude-code', 'codex', 'opencode'],
598
- payload: {
599
- name,
600
- config: serverConfig
601
- }
602
- })
603
- }
604
- assets.push(...mcpServers.values())
605
-
606
- const hookPlugins = [
607
- ...createHookPluginAssets(userConfig?.plugins, enabledPlugins, 'user'),
608
- ...createHookPluginAssets(config?.plugins, enabledPlugins, 'project')
609
- ]
610
- const claudeNativePlugins = createClaudeNativePluginAssets(enabledPlugins)
611
- assets.push(...hookPlugins, ...claudeNativePlugins, ...openCodeOverlayAssets)
612
-
613
- return {
614
- cwd: params.cwd,
615
- assets,
616
- rules,
617
- specs,
618
- entities,
619
- skills,
620
- mcpServers: Object.fromEntries(mcpServers.entries()),
621
- hookPlugins,
622
- enabledPlugins,
623
- extraKnownMarketplaces,
624
- defaultIncludeMcpServers: uniqueValues([
625
- ...(config?.defaultIncludeMcpServers ?? []),
626
- ...(userConfig?.defaultIncludeMcpServers ?? [])
627
- ]),
628
- defaultExcludeMcpServers: uniqueValues([
629
- ...(config?.defaultExcludeMcpServers ?? []),
630
- ...(userConfig?.defaultExcludeMcpServers ?? [])
631
- ])
632
- }
633
- }
634
-
635
- export async function resolvePromptAssetSelection(
636
- params: {
637
- bundle: WorkspaceAssetBundle
638
- type: 'spec' | 'entity' | undefined
639
- name?: string
640
- input?: {
641
- skills?: WorkspaceSkillSelection
642
- }
643
- }
644
- ): Promise<[PromptAssetResolution, ResolvedPromptAssetOptions]> {
645
- const loader = new DefinitionLoader(params.bundle.cwd)
646
- const options: ResolvedPromptAssetOptions = {}
647
- const systemPromptParts: string[] = []
648
-
649
- const entities = params.type !== 'entity'
650
- ? toDocumentDefinitions(params.bundle.entities)
651
- : []
652
- const skills = toDocumentDefinitions(
653
- filterSkillAssets(params.bundle.skills, params.input?.skills)
654
- )
655
- const rules = toDocumentDefinitions(params.bundle.rules)
656
- const specs = toDocumentDefinitions(params.bundle.specs)
657
-
658
- const promptAssetIds = new Set<string>([
659
- ...toPromptAssetIds(params.bundle.rules),
660
- ...(params.type !== 'entity' ? toPromptAssetIds(params.bundle.entities) : []),
661
- ...toPromptAssetIds(params.bundle.specs),
662
- ...toPromptAssetIds(filterSkillAssets(params.bundle.skills, params.input?.skills))
663
- ])
664
-
665
- const targetSkillsAssets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>> = []
666
- let targetBody = ''
667
- let targetToolsFilter: Filter | undefined
668
- let targetMcpServersFilter: Filter | undefined
669
- let selectedSkillAssets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>> = []
670
-
671
- if (params.input?.skills?.include != null && params.input.skills.include.length > 0) {
672
- selectedSkillAssets = dedupeSkillAssets(
673
- filterSkillAssets(params.bundle.skills, { include: params.input.skills.include })
674
- )
675
- }
676
-
677
- if (params.type && params.name) {
678
- const targetAsset = params.type === 'spec'
679
- ? pickSpecAsset(params.bundle, params.name)
680
- : pickEntityAsset(params.bundle, params.name)
681
-
682
- if (targetAsset == null) {
683
- throw new Error(`Failed to load ${params.type} ${params.name}`)
684
- }
685
-
686
- const { definition } = targetAsset.payload
687
- const { attributes, body } = definition
688
- promptAssetIds.add(targetAsset.id)
689
-
690
- if (attributes.rules) {
691
- const matchedRuleAssets = await resolveSelectedRuleAssets(params.bundle, attributes.rules)
692
- rules.push(
693
- ...matchedRuleAssets.map((asset) => ({
694
- ...asset.payload.definition,
695
- attributes: {
696
- ...asset.payload.definition.attributes,
697
- always: true
698
- }
699
- }))
700
- )
701
- for (const asset of matchedRuleAssets) {
702
- promptAssetIds.add(asset.id)
703
- }
704
- }
705
-
706
- if (attributes.skills) {
707
- for (const skillAsset of params.bundle.skills) {
708
- const skillName = basename(dirname(skillAsset.payload.definition.path))
709
- if (!attributes.skills.includes(skillName)) continue
710
- targetSkillsAssets.push(skillAsset)
711
- promptAssetIds.add(skillAsset.id)
712
- }
713
- }
714
-
715
- targetBody = body
716
- targetToolsFilter = attributes.tools
717
- targetMcpServersFilter = attributes.mcpServers
718
- }
719
-
720
- const targetSkills = toDocumentDefinitions(targetSkillsAssets)
721
- const selectedSkillsPrompt = toDocumentDefinitions(
722
- selectedSkillAssets.filter(
723
- skill => !targetSkillsAssets.some(target => target.payload.definition.path === skill.payload.definition.path)
724
- )
725
- )
726
-
727
- systemPromptParts.push(loader.generateRulesPrompt(rules))
728
- systemPromptParts.push(loader.generateSkillsPrompt(targetSkills))
729
- systemPromptParts.push(loader.generateSkillsPrompt(selectedSkillsPrompt))
730
- systemPromptParts.push(loader.generateEntitiesRoutePrompt(entities))
731
- systemPromptParts.push(loader.generateSkillsRoutePrompt(skills))
732
- systemPromptParts.push(loader.generateSpecRoutePrompt(specs))
733
- systemPromptParts.push(targetBody)
734
-
735
- if (targetToolsFilter) {
736
- options.tools = targetToolsFilter
737
- }
738
- if (targetMcpServersFilter) {
739
- options.mcpServers = targetMcpServersFilter
740
- }
741
-
742
- options.systemPrompt = systemPromptParts.join('\n\n')
743
- options.promptAssetIds = Array.from(promptAssetIds)
744
-
745
- return [
746
- {
747
- rules,
748
- targetSkills,
749
- entities,
750
- skills,
751
- specs,
752
- targetBody,
753
- promptAssetIds: Array.from(promptAssetIds)
754
- },
755
- options
756
- ]
757
- }
758
-
759
- const resolveMcpServerSelection = (
760
- bundle: WorkspaceAssetBundle,
761
- selection: WorkspaceMcpSelection | undefined
762
- ) => {
763
- const include = selection?.include ?? (
764
- bundle.defaultIncludeMcpServers.length > 0 ? bundle.defaultIncludeMcpServers : undefined
765
- )
766
- const exclude = selection?.exclude ?? (
767
- bundle.defaultExcludeMcpServers.length > 0 ? bundle.defaultExcludeMcpServers : undefined
768
- )
769
-
770
- return {
771
- include,
772
- exclude
773
- }
774
- }
775
-
776
- export function buildAdapterAssetPlan(params: {
777
- adapter: WorkspaceAssetAdapter
778
- bundle: WorkspaceAssetBundle
779
- options: {
780
- mcpServers?: WorkspaceMcpSelection
781
- skills?: WorkspaceSkillSelection
782
- promptAssetIds?: string[]
783
- }
784
- }): AdapterAssetPlan {
785
- const diagnostics: AssetDiagnostic[] = []
786
- const promptAssetIdSet = new Set(params.options.promptAssetIds ?? [])
787
- const mcpSelection = resolveMcpServerSelection(params.bundle, params.options.mcpServers)
788
- const selectedMcpServerNames = Object.keys(params.bundle.mcpServers).filter((name) => {
789
- if (mcpSelection.include != null && !mcpSelection.include.includes(name)) return false
790
- if (mcpSelection.exclude?.includes(name)) return false
791
- return true
792
- })
793
- const mcpServers = Object.fromEntries(
794
- selectedMcpServerNames.map((name) => [name, params.bundle.mcpServers[name].payload.config])
795
- )
796
-
797
- for (const assetId of promptAssetIdSet) {
798
- diagnostics.push({
799
- assetId,
800
- adapter: params.adapter,
801
- status: 'prompt',
802
- reason: 'Mapped into the generated system prompt.'
803
- })
804
- }
805
-
806
- for (const name of selectedMcpServerNames) {
807
- diagnostics.push({
808
- assetId: params.bundle.mcpServers[name].id,
809
- adapter: params.adapter,
810
- status: params.adapter === 'claude-code' ? 'native' : 'translated',
811
- reason: params.adapter === 'claude-code'
812
- ? 'Mapped into native MCP settings.'
813
- : 'Translated into adapter-specific MCP configuration.'
814
- })
815
- }
816
-
817
- for (const hookPlugin of params.bundle.hookPlugins) {
818
- const nativeHookReason = params.adapter === 'claude-code'
819
- ? 'Mapped into the isolated Claude Code native hooks bridge under .ai/.mock/.claude/settings.json.'
820
- : params.adapter === 'codex'
821
- ? 'Mapped into the isolated Codex native hooks bridge for SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, and Stop.'
822
- : 'Mapped into the isolated OpenCode native hook plugin bridge under .ai/.mock/.config/opencode/plugins.'
823
- diagnostics.push({
824
- assetId: hookPlugin.id,
825
- adapter: params.adapter,
826
- status: 'native',
827
- reason: nativeHookReason
828
- })
829
- }
830
-
831
- const overlays: AdapterOverlayEntry[] = []
832
- if (params.adapter === 'opencode') {
833
- const skillAssets = filterSkillAssets(params.bundle.skills, params.options.skills)
834
- for (const skillAsset of skillAssets) {
835
- overlays.push({
836
- assetId: skillAsset.id,
837
- kind: 'skill',
838
- sourcePath: dirname(skillAsset.payload.definition.path),
839
- targetPath: `skills/${basename(dirname(skillAsset.payload.definition.path))}`
840
- })
841
- diagnostics.push({
842
- assetId: skillAsset.id,
843
- adapter: 'opencode',
844
- status: 'native',
845
- reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native skill.'
846
- })
847
- }
848
-
849
- for (const asset of params.bundle.assets) {
850
- if (!['nativePlugin', 'agent', 'command', 'mode'].includes(asset.kind)) continue
851
- if (!asset.targets.includes('opencode')) continue
852
-
853
- const payload = asset.payload as WorkspaceOverlayPayload
854
- overlays.push({
855
- assetId: asset.id,
856
- kind: asset.kind,
857
- sourcePath: payload.sourcePath,
858
- targetPath: payload.targetSubpath
859
- })
860
- diagnostics.push({
861
- assetId: asset.id,
862
- adapter: 'opencode',
863
- status: 'native',
864
- reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native OpenCode asset.'
865
- })
866
- }
867
- }
868
-
869
- if (params.adapter !== 'claude-code') {
870
- for (const asset of params.bundle.assets) {
871
- if (asset.kind !== 'nativePlugin' || !asset.enabled || !asset.targets.includes('claude-code')) continue
872
- diagnostics.push({
873
- assetId: asset.id,
874
- adapter: params.adapter,
875
- status: 'skipped',
876
- reason: 'Claude marketplace plugin settings do not have a native mapping for this adapter.'
877
- })
878
- }
879
- }
880
-
881
- if (params.adapter === 'codex') {
882
- for (const asset of params.bundle.assets) {
883
- if (!['nativePlugin', 'agent', 'command', 'mode'].includes(asset.kind)) continue
884
- if (asset.targets.includes('codex')) continue
885
- if (asset.kind === 'nativePlugin' && asset.targets.includes('claude-code')) continue
886
- diagnostics.push({
887
- assetId: asset.id,
888
- adapter: 'codex',
889
- status: 'skipped',
890
- reason: 'No stable native Codex mapping exists for this asset kind in V1.'
891
- })
892
- }
893
- }
894
-
895
- return {
896
- adapter: params.adapter,
897
- diagnostics,
898
- mcpServers,
899
- overlays,
900
- native: params.adapter === 'claude-code'
901
- ? {
902
- enabledPlugins: params.bundle.enabledPlugins,
903
- extraKnownMarketplaces: params.bundle.extraKnownMarketplaces
904
- }
905
- : params.adapter === 'codex' && params.bundle.hookPlugins.length > 0
906
- ? {
907
- codexHooks: {
908
- supportedEvents: [
909
- 'SessionStart',
910
- 'UserPromptSubmit',
911
- 'PreToolUse',
912
- 'PostToolUse',
913
- 'Stop'
914
- ]
915
- }
916
- }
917
- : {}
918
- }
919
- }