@vibe-forge/core 0.7.4 → 0.7.5
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 +1 -1
- package/src/adapter/type.ts +5 -0
- package/src/controllers/task/generate-adapter-query-options.ts +10 -203
- package/src/controllers/task/prepare.ts +7 -1
- package/src/controllers/task/run.ts +52 -12
- package/src/hooks/bridge.ts +368 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/loader.ts +5 -1
- package/src/hooks/native.ts +116 -0
- package/src/hooks/runtime.ts +2 -2
- package/src/hooks/type.ts +30 -5
- package/src/utils/workspace-assets.ts +919 -0
|
@@ -0,0 +1,919 @@
|
|
|
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
|
+
}
|