@vibe-forge/workspace-assets 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.
- package/AGENTS.md +38 -0
- package/LICENSE +21 -0
- package/__tests__/workspace-assets.spec.ts +279 -0
- package/package.json +42 -0
- package/src/adapter-asset-plan.ts +174 -0
- package/src/bundle.ts +185 -0
- package/src/document-assets.ts +201 -0
- package/src/helpers.ts +35 -0
- package/src/index.ts +3 -0
- package/src/internal-types.ts +41 -0
- package/src/plugin-assets.ts +178 -0
- package/src/prompt-selection.ts +151 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Workspace Assets 包说明
|
|
2
|
+
|
|
3
|
+
`@vibe-forge/workspace-assets` 承载 workspace asset bundle 发现、prompt asset 选择与 adapter asset plan 组装。
|
|
4
|
+
|
|
5
|
+
## 什么时候先看这里
|
|
6
|
+
|
|
7
|
+
- `.ai/rules`、`.ai/specs`、`.ai/entities`、`.ai/skills` 没有被正确投影到 workspace bundle
|
|
8
|
+
- 默认/显式 MCP server 选择结果不对
|
|
9
|
+
- promptAssetIds、system prompt 资产选择不对
|
|
10
|
+
- adapter native asset plan 或 opencode overlay 异常
|
|
11
|
+
|
|
12
|
+
## 入口
|
|
13
|
+
|
|
14
|
+
- `src/bundle.ts`
|
|
15
|
+
- `resolveWorkspaceAssetBundle()`
|
|
16
|
+
- `src/prompt-selection.ts`
|
|
17
|
+
- `resolvePromptAssetSelection()`
|
|
18
|
+
- `src/adapter-asset-plan.ts`
|
|
19
|
+
- `buildAdapterAssetPlan()`
|
|
20
|
+
- `__tests__/workspace-assets.spec.ts`
|
|
21
|
+
|
|
22
|
+
## 当前边界
|
|
23
|
+
|
|
24
|
+
- 本包负责:
|
|
25
|
+
- workspace asset bundle 组装
|
|
26
|
+
- prompt asset 选择
|
|
27
|
+
- adapter asset plan 组装
|
|
28
|
+
- 本包不负责:
|
|
29
|
+
- 定义文档发现与解析
|
|
30
|
+
- cache 存储
|
|
31
|
+
- task 生命周期编排
|
|
32
|
+
|
|
33
|
+
## 维护约定
|
|
34
|
+
|
|
35
|
+
- 只维护 workspace asset 领域逻辑;定义文档读取留在 `@vibe-forge/definition-loader`,cache 留在 `@vibe-forge/utils`。
|
|
36
|
+
- 文档路径规范化与命名规则复用 `@vibe-forge/utils/document-path`,不要在本包重复维护。
|
|
37
|
+
- 共享 contract 继续依赖 `@vibe-forge/types`,不要把 task / hooks / mcp 逻辑反向塞进来。
|
|
38
|
+
- 新增 asset 类型、prompt 选择规则或 adapter 投影时,先补 `__tests__/workspace-assets.spec.ts`。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present Vibe-Forge.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
|
|
6
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
buildAdapterAssetPlan,
|
|
10
|
+
resolvePromptAssetSelection,
|
|
11
|
+
resolveWorkspaceAssetBundle
|
|
12
|
+
} from '#~/index.js'
|
|
13
|
+
|
|
14
|
+
const tempDirs: string[] = []
|
|
15
|
+
|
|
16
|
+
const createWorkspace = async () => {
|
|
17
|
+
const dir = await mkdtemp(join(tmpdir(), 'workspace-assets-'))
|
|
18
|
+
tempDirs.push(dir)
|
|
19
|
+
return dir
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const writeDocument = async (filePath: string, content: string) => {
|
|
23
|
+
await mkdir(dirname(filePath), { recursive: true })
|
|
24
|
+
await writeFile(filePath, content)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('workspace assets', () => {
|
|
32
|
+
it('prefers project prompt assets over plugin assets with the same identifier', async () => {
|
|
33
|
+
const workspace = await createWorkspace()
|
|
34
|
+
|
|
35
|
+
await writeDocument(
|
|
36
|
+
join(workspace, '.ai/specs/release.md'),
|
|
37
|
+
'---\ndescription: 项目发布流程\n---\n执行项目发布'
|
|
38
|
+
)
|
|
39
|
+
await writeDocument(
|
|
40
|
+
join(workspace, '.ai/plugins/demo/specs/release/index.md'),
|
|
41
|
+
'---\ndescription: 插件发布流程\n---\n执行插件发布'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
45
|
+
cwd: workspace,
|
|
46
|
+
configs: [undefined, undefined],
|
|
47
|
+
useDefaultVibeForgeMcpServer: false
|
|
48
|
+
})
|
|
49
|
+
const [data, resolvedOptions] = await resolvePromptAssetSelection({
|
|
50
|
+
bundle,
|
|
51
|
+
type: 'spec',
|
|
52
|
+
name: 'release'
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
expect(data.targetBody).toContain('执行项目发布')
|
|
56
|
+
expect(data.targetBody).not.toContain('执行插件发布')
|
|
57
|
+
expect(data.specs).toHaveLength(1)
|
|
58
|
+
expect(resolvedOptions.systemPrompt).toContain('项目发布流程')
|
|
59
|
+
expect(resolvedOptions.systemPrompt).not.toContain('插件发布流程')
|
|
60
|
+
expect(resolvedOptions.promptAssetIds).toEqual(
|
|
61
|
+
expect.arrayContaining([
|
|
62
|
+
'spec:.ai/specs/release.md'
|
|
63
|
+
])
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('treats enabledPlugins as a global asset switch', async () => {
|
|
68
|
+
const workspace = await createWorkspace()
|
|
69
|
+
|
|
70
|
+
await writeDocument(
|
|
71
|
+
join(workspace, '.ai.config.json'),
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
plugins: {
|
|
74
|
+
logger: {}
|
|
75
|
+
},
|
|
76
|
+
enabledPlugins: {
|
|
77
|
+
logger: false,
|
|
78
|
+
demo: false
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
await writeDocument(
|
|
83
|
+
join(workspace, '.ai/plugins/demo/skills/research/SKILL.md'),
|
|
84
|
+
'---\ndescription: 检索资料\n---\n阅读 README.md'
|
|
85
|
+
)
|
|
86
|
+
await writeDocument(
|
|
87
|
+
join(workspace, '.ai/plugins/demo/rules/review.md'),
|
|
88
|
+
'---\ndescription: 评审规则\n---\n必须检查风险'
|
|
89
|
+
)
|
|
90
|
+
await writeDocument(
|
|
91
|
+
join(workspace, '.ai/plugins/demo/mcp/browser.json'),
|
|
92
|
+
JSON.stringify({ command: 'npx', args: ['browser-server'] })
|
|
93
|
+
)
|
|
94
|
+
await writeDocument(
|
|
95
|
+
join(workspace, '.ai/plugins/demo/opencode/commands/review.md'),
|
|
96
|
+
'# review'
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
100
|
+
cwd: workspace,
|
|
101
|
+
configs: [{
|
|
102
|
+
plugins: {
|
|
103
|
+
logger: {}
|
|
104
|
+
},
|
|
105
|
+
enabledPlugins: {
|
|
106
|
+
logger: false,
|
|
107
|
+
demo: false
|
|
108
|
+
}
|
|
109
|
+
}, undefined],
|
|
110
|
+
useDefaultVibeForgeMcpServer: false
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
expect(bundle.skills).toHaveLength(0)
|
|
114
|
+
expect(bundle.rules).toHaveLength(0)
|
|
115
|
+
expect(Object.keys(bundle.mcpServers)).toHaveLength(0)
|
|
116
|
+
expect(bundle.hookPlugins).toHaveLength(0)
|
|
117
|
+
expect(bundle.assets.some((asset: (typeof bundle.assets)[number]) => asset.pluginId === 'demo' && asset.enabled))
|
|
118
|
+
.toBe(false)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('builds codex diagnostics for prompt, mcp, native hooks, and unsupported claude native plugins', async () => {
|
|
122
|
+
const workspace = await createWorkspace()
|
|
123
|
+
|
|
124
|
+
await writeDocument(
|
|
125
|
+
join(workspace, '.ai.config.json'),
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
plugins: {
|
|
128
|
+
logger: {}
|
|
129
|
+
},
|
|
130
|
+
enabledPlugins: {
|
|
131
|
+
logger: true
|
|
132
|
+
},
|
|
133
|
+
mcpServers: {
|
|
134
|
+
docs: {
|
|
135
|
+
command: 'npx',
|
|
136
|
+
args: ['docs-server']
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
)
|
|
141
|
+
await writeDocument(
|
|
142
|
+
join(workspace, '.ai/skills/research/SKILL.md'),
|
|
143
|
+
'---\ndescription: 检索资料\n---\n阅读 README.md'
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
147
|
+
cwd: workspace,
|
|
148
|
+
configs: [{
|
|
149
|
+
plugins: {
|
|
150
|
+
logger: {}
|
|
151
|
+
},
|
|
152
|
+
enabledPlugins: {
|
|
153
|
+
logger: true
|
|
154
|
+
},
|
|
155
|
+
mcpServers: {
|
|
156
|
+
docs: {
|
|
157
|
+
command: 'npx',
|
|
158
|
+
args: ['docs-server']
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}, undefined],
|
|
162
|
+
useDefaultVibeForgeMcpServer: false
|
|
163
|
+
})
|
|
164
|
+
const [, resolvedOptions] = await resolvePromptAssetSelection({
|
|
165
|
+
bundle,
|
|
166
|
+
type: undefined,
|
|
167
|
+
name: undefined,
|
|
168
|
+
input: {
|
|
169
|
+
skills: {
|
|
170
|
+
include: ['research']
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
const plan = buildAdapterAssetPlan({
|
|
175
|
+
adapter: 'codex',
|
|
176
|
+
bundle,
|
|
177
|
+
options: {
|
|
178
|
+
promptAssetIds: resolvedOptions.promptAssetIds,
|
|
179
|
+
mcpServers: resolvedOptions.mcpServers,
|
|
180
|
+
skills: {
|
|
181
|
+
include: ['research']
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
expect(plan.mcpServers).toHaveProperty('docs')
|
|
187
|
+
expect(plan.native.codexHooks?.supportedEvents).toEqual([
|
|
188
|
+
'SessionStart',
|
|
189
|
+
'UserPromptSubmit',
|
|
190
|
+
'PreToolUse',
|
|
191
|
+
'PostToolUse',
|
|
192
|
+
'Stop'
|
|
193
|
+
])
|
|
194
|
+
expect(plan.diagnostics).toEqual(expect.arrayContaining([
|
|
195
|
+
expect.objectContaining({
|
|
196
|
+
adapter: 'codex',
|
|
197
|
+
status: 'prompt'
|
|
198
|
+
}),
|
|
199
|
+
expect.objectContaining({
|
|
200
|
+
adapter: 'codex',
|
|
201
|
+
status: 'native',
|
|
202
|
+
assetId: 'hookPlugin:project:logger'
|
|
203
|
+
}),
|
|
204
|
+
expect.objectContaining({
|
|
205
|
+
adapter: 'codex',
|
|
206
|
+
status: 'translated',
|
|
207
|
+
assetId: 'mcpServer:project:docs'
|
|
208
|
+
}),
|
|
209
|
+
expect.objectContaining({
|
|
210
|
+
adapter: 'codex',
|
|
211
|
+
status: 'skipped',
|
|
212
|
+
assetId: 'nativePlugin:claude-code:logger'
|
|
213
|
+
})
|
|
214
|
+
]))
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('builds opencode overlays for skills and native commands', async () => {
|
|
218
|
+
const workspace = await createWorkspace()
|
|
219
|
+
|
|
220
|
+
await writeDocument(
|
|
221
|
+
join(workspace, '.ai/skills/research/SKILL.md'),
|
|
222
|
+
'---\ndescription: 检索资料\n---\n阅读 README.md'
|
|
223
|
+
)
|
|
224
|
+
await writeDocument(
|
|
225
|
+
join(workspace, '.ai/plugins/demo/opencode/commands/review.md'),
|
|
226
|
+
'# review'
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
const bundle = await resolveWorkspaceAssetBundle({
|
|
230
|
+
cwd: workspace,
|
|
231
|
+
configs: [undefined, undefined],
|
|
232
|
+
useDefaultVibeForgeMcpServer: false
|
|
233
|
+
})
|
|
234
|
+
const plan = buildAdapterAssetPlan({
|
|
235
|
+
adapter: 'opencode',
|
|
236
|
+
bundle,
|
|
237
|
+
options: {
|
|
238
|
+
skills: {
|
|
239
|
+
include: ['research']
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
expect(plan.overlays).toEqual(expect.arrayContaining([
|
|
245
|
+
expect.objectContaining({
|
|
246
|
+
kind: 'skill',
|
|
247
|
+
targetPath: 'skills/research'
|
|
248
|
+
}),
|
|
249
|
+
expect.objectContaining({
|
|
250
|
+
kind: 'command',
|
|
251
|
+
targetPath: 'commands/review.md'
|
|
252
|
+
})
|
|
253
|
+
]))
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('adds the built-in Vibe Forge MCP server when enabled and omits it when disabled', async () => {
|
|
257
|
+
const workspace = await createWorkspace()
|
|
258
|
+
|
|
259
|
+
const enabledBundle = await resolveWorkspaceAssetBundle({
|
|
260
|
+
cwd: workspace,
|
|
261
|
+
configs: [undefined, undefined],
|
|
262
|
+
useDefaultVibeForgeMcpServer: true
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
expect(enabledBundle.mcpServers).toHaveProperty('vibe-forge')
|
|
266
|
+
expect(enabledBundle.mcpServers['vibe-forge']?.payload.config).toEqual(expect.objectContaining({
|
|
267
|
+
command: process.execPath,
|
|
268
|
+
args: [expect.stringMatching(/packages\/mcp\/cli\.js$/)]
|
|
269
|
+
}))
|
|
270
|
+
|
|
271
|
+
const disabledBundle = await resolveWorkspaceAssetBundle({
|
|
272
|
+
cwd: workspace,
|
|
273
|
+
configs: [undefined, undefined],
|
|
274
|
+
useDefaultVibeForgeMcpServer: false
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
expect(disabledBundle.mcpServers).not.toHaveProperty('vibe-forge')
|
|
278
|
+
})
|
|
279
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vibe-forge/workspace-assets",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Workspace asset resolution and adapter asset planning for Vibe Forge",
|
|
5
|
+
"imports": {
|
|
6
|
+
"#~/*.js": {
|
|
7
|
+
"__vibe-forge__": {
|
|
8
|
+
"default": "./src/*.ts"
|
|
9
|
+
},
|
|
10
|
+
"default": {
|
|
11
|
+
"import": "./dist/*.mjs",
|
|
12
|
+
"require": "./dist/*.js"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"__vibe-forge__": {
|
|
19
|
+
"default": "./src/index.ts"
|
|
20
|
+
},
|
|
21
|
+
"default": {
|
|
22
|
+
"import": "./dist/index.mjs",
|
|
23
|
+
"require": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"fast-glob": "^3.3.3",
|
|
30
|
+
"js-yaml": "^4.1.1",
|
|
31
|
+
"@vibe-forge/config": "^0.8.0",
|
|
32
|
+
"@vibe-forge/definition-loader": "^0.8.0",
|
|
33
|
+
"@vibe-forge/utils": "^0.8.0",
|
|
34
|
+
"@vibe-forge/types": "^0.8.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/js-yaml": "^4.0.9"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "pnpm -C ../.. exec vitest run --workspace vitest.workspace.ts --project bundler packages/workspace-assets/__tests__"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { basename, dirname } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
AdapterAssetPlan,
|
|
5
|
+
AssetDiagnostic,
|
|
6
|
+
WorkspaceAssetAdapter,
|
|
7
|
+
WorkspaceAssetBundle,
|
|
8
|
+
WorkspaceMcpSelection,
|
|
9
|
+
WorkspaceSkillSelection
|
|
10
|
+
} from '@vibe-forge/types'
|
|
11
|
+
|
|
12
|
+
import { filterSkillAssets } from './document-assets'
|
|
13
|
+
import { isOpenCodeOverlayAsset } from './internal-types'
|
|
14
|
+
|
|
15
|
+
const resolveMcpServerSelection = (
|
|
16
|
+
bundle: WorkspaceAssetBundle,
|
|
17
|
+
selection: WorkspaceMcpSelection | undefined
|
|
18
|
+
) => {
|
|
19
|
+
const include = selection?.include ?? (
|
|
20
|
+
bundle.defaultIncludeMcpServers.length > 0 ? bundle.defaultIncludeMcpServers : undefined
|
|
21
|
+
)
|
|
22
|
+
const exclude = selection?.exclude ?? (
|
|
23
|
+
bundle.defaultExcludeMcpServers.length > 0 ? bundle.defaultExcludeMcpServers : undefined
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
include,
|
|
28
|
+
exclude
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildAdapterAssetPlan(params: {
|
|
33
|
+
adapter: WorkspaceAssetAdapter
|
|
34
|
+
bundle: WorkspaceAssetBundle
|
|
35
|
+
options: {
|
|
36
|
+
mcpServers?: WorkspaceMcpSelection
|
|
37
|
+
skills?: WorkspaceSkillSelection
|
|
38
|
+
promptAssetIds?: string[]
|
|
39
|
+
}
|
|
40
|
+
}): AdapterAssetPlan {
|
|
41
|
+
const diagnostics: AssetDiagnostic[] = []
|
|
42
|
+
const promptAssetIdSet = new Set(params.options.promptAssetIds ?? [])
|
|
43
|
+
const mcpSelection = resolveMcpServerSelection(params.bundle, params.options.mcpServers)
|
|
44
|
+
const selectedMcpServerNames = Object.keys(params.bundle.mcpServers).filter((name) => {
|
|
45
|
+
if (mcpSelection.include != null && !mcpSelection.include.includes(name)) return false
|
|
46
|
+
if (mcpSelection.exclude?.includes(name)) return false
|
|
47
|
+
return true
|
|
48
|
+
})
|
|
49
|
+
const mcpServers = Object.fromEntries(
|
|
50
|
+
selectedMcpServerNames.map((name) => [name, params.bundle.mcpServers[name].payload.config])
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
for (const assetId of promptAssetIdSet) {
|
|
54
|
+
diagnostics.push({
|
|
55
|
+
assetId,
|
|
56
|
+
adapter: params.adapter,
|
|
57
|
+
status: 'prompt',
|
|
58
|
+
reason: 'Mapped into the generated system prompt.'
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const name of selectedMcpServerNames) {
|
|
63
|
+
diagnostics.push({
|
|
64
|
+
assetId: params.bundle.mcpServers[name].id,
|
|
65
|
+
adapter: params.adapter,
|
|
66
|
+
status: params.adapter === 'claude-code' ? 'native' : 'translated',
|
|
67
|
+
reason: params.adapter === 'claude-code'
|
|
68
|
+
? 'Mapped into native MCP settings.'
|
|
69
|
+
: 'Translated into adapter-specific MCP configuration.'
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const hookPlugin of params.bundle.hookPlugins) {
|
|
74
|
+
const nativeHookReason = params.adapter === 'claude-code'
|
|
75
|
+
? 'Mapped into the isolated Claude Code native hooks bridge under .ai/.mock/.claude/settings.json.'
|
|
76
|
+
: params.adapter === 'codex'
|
|
77
|
+
? 'Mapped into the isolated Codex native hooks bridge for SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, and Stop.'
|
|
78
|
+
: 'Mapped into the isolated OpenCode native hook plugin bridge under .ai/.mock/.config/opencode/plugins.'
|
|
79
|
+
diagnostics.push({
|
|
80
|
+
assetId: hookPlugin.id,
|
|
81
|
+
adapter: params.adapter,
|
|
82
|
+
status: 'native',
|
|
83
|
+
reason: nativeHookReason
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const overlays: AdapterAssetPlan['overlays'] = []
|
|
88
|
+
if (params.adapter === 'opencode') {
|
|
89
|
+
const skillAssets = filterSkillAssets(params.bundle.skills, params.options.skills)
|
|
90
|
+
for (const skillAsset of skillAssets) {
|
|
91
|
+
overlays.push({
|
|
92
|
+
assetId: skillAsset.id,
|
|
93
|
+
kind: 'skill',
|
|
94
|
+
sourcePath: dirname(skillAsset.payload.definition.path),
|
|
95
|
+
targetPath: `skills/${basename(dirname(skillAsset.payload.definition.path))}`
|
|
96
|
+
})
|
|
97
|
+
diagnostics.push({
|
|
98
|
+
assetId: skillAsset.id,
|
|
99
|
+
adapter: 'opencode',
|
|
100
|
+
status: 'native',
|
|
101
|
+
reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native skill.'
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const asset of params.bundle.assets) {
|
|
106
|
+
if (!isOpenCodeOverlayAsset(asset)) continue
|
|
107
|
+
if (!asset.targets.includes('opencode')) continue
|
|
108
|
+
|
|
109
|
+
overlays.push({
|
|
110
|
+
assetId: asset.id,
|
|
111
|
+
kind: asset.kind,
|
|
112
|
+
sourcePath: asset.payload.sourcePath,
|
|
113
|
+
targetPath: asset.payload.targetSubpath
|
|
114
|
+
})
|
|
115
|
+
diagnostics.push({
|
|
116
|
+
assetId: asset.id,
|
|
117
|
+
adapter: 'opencode',
|
|
118
|
+
status: 'native',
|
|
119
|
+
reason: 'Mirrored into OPENCODE_CONFIG_DIR as a native OpenCode asset.'
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (params.adapter !== 'claude-code') {
|
|
125
|
+
for (const asset of params.bundle.assets) {
|
|
126
|
+
if (asset.kind !== 'nativePlugin' || !asset.enabled || !asset.targets.includes('claude-code')) continue
|
|
127
|
+
diagnostics.push({
|
|
128
|
+
assetId: asset.id,
|
|
129
|
+
adapter: params.adapter,
|
|
130
|
+
status: 'skipped',
|
|
131
|
+
reason: 'Claude marketplace plugin settings do not have a native mapping for this adapter.'
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (params.adapter === 'codex') {
|
|
137
|
+
for (const asset of params.bundle.assets) {
|
|
138
|
+
if (!['nativePlugin', 'agent', 'command', 'mode'].includes(asset.kind)) continue
|
|
139
|
+
if (asset.targets.includes('codex')) continue
|
|
140
|
+
if (asset.kind === 'nativePlugin' && asset.targets.includes('claude-code')) continue
|
|
141
|
+
diagnostics.push({
|
|
142
|
+
assetId: asset.id,
|
|
143
|
+
adapter: 'codex',
|
|
144
|
+
status: 'skipped',
|
|
145
|
+
reason: 'No stable native Codex mapping exists for this asset kind in V1.'
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
adapter: params.adapter,
|
|
152
|
+
diagnostics,
|
|
153
|
+
mcpServers,
|
|
154
|
+
overlays,
|
|
155
|
+
native: params.adapter === 'claude-code'
|
|
156
|
+
? {
|
|
157
|
+
enabledPlugins: params.bundle.enabledPlugins,
|
|
158
|
+
extraKnownMarketplaces: params.bundle.extraKnownMarketplaces
|
|
159
|
+
}
|
|
160
|
+
: params.adapter === 'codex' && params.bundle.hookPlugins.length > 0
|
|
161
|
+
? {
|
|
162
|
+
codexHooks: {
|
|
163
|
+
supportedEvents: [
|
|
164
|
+
'SessionStart',
|
|
165
|
+
'UserPromptSubmit',
|
|
166
|
+
'PreToolUse',
|
|
167
|
+
'PostToolUse',
|
|
168
|
+
'Stop'
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
: {}
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/bundle.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildConfigJsonVariables,
|
|
5
|
+
DEFAULT_VIBE_FORGE_MCP_SERVER_NAME,
|
|
6
|
+
loadConfig,
|
|
7
|
+
resolveDefaultVibeForgeMcpServerConfig
|
|
8
|
+
} from '@vibe-forge/config'
|
|
9
|
+
import { DefinitionLoader } from '@vibe-forge/definition-loader'
|
|
10
|
+
import type {
|
|
11
|
+
Config,
|
|
12
|
+
WorkspaceAsset,
|
|
13
|
+
WorkspaceAssetBundle
|
|
14
|
+
} from '@vibe-forge/types'
|
|
15
|
+
import {
|
|
16
|
+
resolveDocumentName,
|
|
17
|
+
resolveSpecIdentifier
|
|
18
|
+
} from '@vibe-forge/utils'
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
createDocumentAsset,
|
|
22
|
+
dedupeDocumentAssets,
|
|
23
|
+
dedupeDocumentAssetsByIdentifier,
|
|
24
|
+
resolveRuleIdentifier,
|
|
25
|
+
resolveSkillIdentifier
|
|
26
|
+
} from './document-assets'
|
|
27
|
+
import { mergeRecord, uniqueValues } from './helpers'
|
|
28
|
+
import {
|
|
29
|
+
createClaudeNativePluginAssets,
|
|
30
|
+
createHookPluginAssets,
|
|
31
|
+
loadOpenCodeOverlayAssets,
|
|
32
|
+
loadPluginMcpAssets
|
|
33
|
+
} from './plugin-assets'
|
|
34
|
+
|
|
35
|
+
const readConfigForWorkspace = async (cwd: string) => {
|
|
36
|
+
return loadConfig<Config>({
|
|
37
|
+
cwd,
|
|
38
|
+
jsonVariables: buildConfigJsonVariables(cwd, process.env)
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function resolveWorkspaceAssetBundle(params: {
|
|
43
|
+
cwd: string
|
|
44
|
+
configs?: [Config?, Config?]
|
|
45
|
+
useDefaultVibeForgeMcpServer?: boolean
|
|
46
|
+
}): Promise<WorkspaceAssetBundle> {
|
|
47
|
+
const [config, userConfig] = params.configs ?? await readConfigForWorkspace(params.cwd)
|
|
48
|
+
const enabledPlugins = mergeRecord(config?.enabledPlugins, userConfig?.enabledPlugins)
|
|
49
|
+
const extraKnownMarketplaces = mergeRecord(config?.extraKnownMarketplaces, userConfig?.extraKnownMarketplaces)
|
|
50
|
+
const loader = new DefinitionLoader(params.cwd)
|
|
51
|
+
|
|
52
|
+
const [
|
|
53
|
+
rawRules,
|
|
54
|
+
rawSpecs,
|
|
55
|
+
rawEntities,
|
|
56
|
+
rawSkills,
|
|
57
|
+
pluginMcpAssets,
|
|
58
|
+
openCodeOverlayAssets
|
|
59
|
+
] = await Promise.all([
|
|
60
|
+
loader.loadDefaultRules(),
|
|
61
|
+
loader.loadDefaultSpecs(),
|
|
62
|
+
loader.loadDefaultEntities(),
|
|
63
|
+
loader.loadDefaultSkills(),
|
|
64
|
+
loadPluginMcpAssets(params.cwd, enabledPlugins),
|
|
65
|
+
loadOpenCodeOverlayAssets(params.cwd, enabledPlugins)
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
const assets: WorkspaceAsset[] = []
|
|
69
|
+
|
|
70
|
+
const rules = dedupeDocumentAssetsByIdentifier(
|
|
71
|
+
dedupeDocumentAssets(
|
|
72
|
+
rawRules.map((definition) => createDocumentAsset({ cwd: params.cwd, kind: 'rule', definition })),
|
|
73
|
+
enabledPlugins
|
|
74
|
+
),
|
|
75
|
+
asset => resolveRuleIdentifier(asset.payload.definition.path, asset.payload.definition.attributes.name)
|
|
76
|
+
)
|
|
77
|
+
const specs = dedupeDocumentAssetsByIdentifier(
|
|
78
|
+
dedupeDocumentAssets(
|
|
79
|
+
rawSpecs.map((definition) => createDocumentAsset({ cwd: params.cwd, kind: 'spec', definition })),
|
|
80
|
+
enabledPlugins
|
|
81
|
+
),
|
|
82
|
+
asset => resolveSpecIdentifier(asset.payload.definition.path, asset.payload.definition.attributes.name)
|
|
83
|
+
)
|
|
84
|
+
const entities = dedupeDocumentAssetsByIdentifier(
|
|
85
|
+
dedupeDocumentAssets(
|
|
86
|
+
rawEntities.map((definition) => createDocumentAsset({ cwd: params.cwd, kind: 'entity', definition })),
|
|
87
|
+
enabledPlugins
|
|
88
|
+
),
|
|
89
|
+
asset =>
|
|
90
|
+
resolveDocumentName(
|
|
91
|
+
asset.payload.definition.path,
|
|
92
|
+
asset.payload.definition.attributes.name,
|
|
93
|
+
['readme.md', 'index.json']
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
const skills = dedupeDocumentAssetsByIdentifier(
|
|
97
|
+
dedupeDocumentAssets(
|
|
98
|
+
rawSkills.map((definition) => createDocumentAsset({ cwd: params.cwd, kind: 'skill', definition })),
|
|
99
|
+
enabledPlugins
|
|
100
|
+
),
|
|
101
|
+
asset => resolveSkillIdentifier(asset.payload.definition.path, asset.payload.definition.attributes.name)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
assets.push(...rules, ...specs, ...entities, ...skills)
|
|
105
|
+
|
|
106
|
+
const mcpServers = new Map<string, Extract<WorkspaceAsset, { kind: 'mcpServer' }>>()
|
|
107
|
+
if (params.useDefaultVibeForgeMcpServer !== false) {
|
|
108
|
+
const defaultVibeForgeMcpServer = resolveDefaultVibeForgeMcpServerConfig()
|
|
109
|
+
if (defaultVibeForgeMcpServer != null) {
|
|
110
|
+
mcpServers.set(DEFAULT_VIBE_FORGE_MCP_SERVER_NAME, {
|
|
111
|
+
id: `mcpServer:fallback:${DEFAULT_VIBE_FORGE_MCP_SERVER_NAME}`,
|
|
112
|
+
kind: 'mcpServer',
|
|
113
|
+
origin: 'fallback',
|
|
114
|
+
scope: 'adapter',
|
|
115
|
+
enabled: true,
|
|
116
|
+
targets: ['claude-code', 'codex', 'opencode'],
|
|
117
|
+
payload: {
|
|
118
|
+
name: DEFAULT_VIBE_FORGE_MCP_SERVER_NAME,
|
|
119
|
+
config: defaultVibeForgeMcpServer
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const userMcpServers = userConfig?.mcpServers ?? {}
|
|
125
|
+
for (const [name, serverConfig] of Object.entries(userMcpServers)) {
|
|
126
|
+
mcpServers.set(name, {
|
|
127
|
+
id: `mcpServer:user:${name}`,
|
|
128
|
+
kind: 'mcpServer',
|
|
129
|
+
origin: 'config',
|
|
130
|
+
scope: 'user',
|
|
131
|
+
enabled: true,
|
|
132
|
+
targets: ['claude-code', 'codex', 'opencode'],
|
|
133
|
+
payload: {
|
|
134
|
+
name,
|
|
135
|
+
config: serverConfig
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
for (const asset of pluginMcpAssets) {
|
|
140
|
+
mcpServers.set(asset.payload.name, asset)
|
|
141
|
+
}
|
|
142
|
+
for (const [name, serverConfig] of Object.entries(config?.mcpServers ?? {})) {
|
|
143
|
+
mcpServers.set(name, {
|
|
144
|
+
id: `mcpServer:project:${name}`,
|
|
145
|
+
kind: 'mcpServer',
|
|
146
|
+
origin: 'config',
|
|
147
|
+
scope: 'project',
|
|
148
|
+
enabled: true,
|
|
149
|
+
targets: ['claude-code', 'codex', 'opencode'],
|
|
150
|
+
payload: {
|
|
151
|
+
name,
|
|
152
|
+
config: serverConfig
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
assets.push(...mcpServers.values())
|
|
157
|
+
|
|
158
|
+
const hookPlugins = [
|
|
159
|
+
...createHookPluginAssets(userConfig?.plugins, enabledPlugins, 'user'),
|
|
160
|
+
...createHookPluginAssets(config?.plugins, enabledPlugins, 'project')
|
|
161
|
+
]
|
|
162
|
+
const claudeNativePlugins = createClaudeNativePluginAssets(enabledPlugins)
|
|
163
|
+
assets.push(...hookPlugins, ...claudeNativePlugins, ...openCodeOverlayAssets)
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
cwd: params.cwd,
|
|
167
|
+
assets,
|
|
168
|
+
rules,
|
|
169
|
+
specs,
|
|
170
|
+
entities,
|
|
171
|
+
skills,
|
|
172
|
+
mcpServers: Object.fromEntries(mcpServers.entries()),
|
|
173
|
+
hookPlugins,
|
|
174
|
+
enabledPlugins,
|
|
175
|
+
extraKnownMarketplaces,
|
|
176
|
+
defaultIncludeMcpServers: uniqueValues([
|
|
177
|
+
...(config?.defaultIncludeMcpServers ?? []),
|
|
178
|
+
...(userConfig?.defaultIncludeMcpServers ?? [])
|
|
179
|
+
]),
|
|
180
|
+
defaultExcludeMcpServers: uniqueValues([
|
|
181
|
+
...(config?.defaultExcludeMcpServers ?? []),
|
|
182
|
+
...(userConfig?.defaultExcludeMcpServers ?? [])
|
|
183
|
+
])
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { basename, dirname } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { glob } from 'fast-glob'
|
|
4
|
+
import type {
|
|
5
|
+
RuleReference,
|
|
6
|
+
SkillSelection,
|
|
7
|
+
WorkspaceAsset,
|
|
8
|
+
WorkspaceAssetAdapter,
|
|
9
|
+
WorkspaceAssetKind,
|
|
10
|
+
WorkspaceAssetBundle,
|
|
11
|
+
WorkspaceSkillSelection
|
|
12
|
+
} from '@vibe-forge/types'
|
|
13
|
+
import {
|
|
14
|
+
normalizePath,
|
|
15
|
+
resolveDocumentName,
|
|
16
|
+
resolveRelativePath,
|
|
17
|
+
resolveSpecIdentifier
|
|
18
|
+
} from '@vibe-forge/utils'
|
|
19
|
+
|
|
20
|
+
import type { WorkspaceDocumentAsset, WorkspaceDocumentPayload } from './internal-types'
|
|
21
|
+
import {
|
|
22
|
+
assetOriginPriority,
|
|
23
|
+
isPluginEnabled,
|
|
24
|
+
resolvePluginIdFromPath,
|
|
25
|
+
toAssetScope
|
|
26
|
+
} from './helpers'
|
|
27
|
+
|
|
28
|
+
const isLocalRuleReference = (
|
|
29
|
+
rule: RuleReference
|
|
30
|
+
): rule is Extract<RuleReference, { path: string }> => (
|
|
31
|
+
rule != null &&
|
|
32
|
+
typeof rule === 'object' &&
|
|
33
|
+
'path' in rule &&
|
|
34
|
+
typeof rule.path === 'string'
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
export const createDocumentAsset = <
|
|
38
|
+
TKind extends Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>,
|
|
39
|
+
TDefinition,
|
|
40
|
+
>(
|
|
41
|
+
params: {
|
|
42
|
+
cwd: string
|
|
43
|
+
kind: TKind
|
|
44
|
+
definition: TDefinition & { path: string }
|
|
45
|
+
targets?: WorkspaceAssetAdapter[]
|
|
46
|
+
}
|
|
47
|
+
): Extract<WorkspaceAsset, { kind: TKind }> => {
|
|
48
|
+
const pluginId = resolvePluginIdFromPath(params.cwd, params.definition.path)
|
|
49
|
+
const origin: WorkspaceAsset['origin'] = pluginId == null ? 'project' : 'plugin'
|
|
50
|
+
return {
|
|
51
|
+
id: `${params.kind}:${resolveRelativePath(params.cwd, params.definition.path)}`,
|
|
52
|
+
kind: params.kind,
|
|
53
|
+
pluginId,
|
|
54
|
+
origin,
|
|
55
|
+
scope: toAssetScope(origin),
|
|
56
|
+
enabled: true,
|
|
57
|
+
targets: params.targets ?? ['claude-code', 'codex', 'opencode'],
|
|
58
|
+
payload: {
|
|
59
|
+
definition: params.definition as any,
|
|
60
|
+
sourcePath: params.definition.path
|
|
61
|
+
}
|
|
62
|
+
} as Extract<WorkspaceAsset, { kind: TKind }>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const dedupeDocumentAssets = <
|
|
66
|
+
TAsset extends Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>
|
|
67
|
+
>(
|
|
68
|
+
assets: TAsset[],
|
|
69
|
+
enabledPlugins: Record<string, boolean>
|
|
70
|
+
) => assets.filter((asset) => isPluginEnabled(enabledPlugins, asset.pluginId))
|
|
71
|
+
|
|
72
|
+
const compareDocumentAssetPriority = (
|
|
73
|
+
left: Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>,
|
|
74
|
+
right: Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>
|
|
75
|
+
) => {
|
|
76
|
+
const originDiff = assetOriginPriority[left.origin] - assetOriginPriority[right.origin]
|
|
77
|
+
if (originDiff !== 0) return originDiff
|
|
78
|
+
return left.payload.definition.path.localeCompare(right.payload.definition.path)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const dedupeDocumentAssetsByIdentifier = <
|
|
82
|
+
TAsset extends Extract<WorkspaceAsset, { kind: 'rule' | 'spec' | 'entity' | 'skill' }>
|
|
83
|
+
>(
|
|
84
|
+
assets: TAsset[],
|
|
85
|
+
resolveIdentifier: (asset: TAsset) => string
|
|
86
|
+
) => {
|
|
87
|
+
const selected = new Map<string, TAsset>()
|
|
88
|
+
|
|
89
|
+
for (const asset of [...assets].sort(compareDocumentAssetPriority)) {
|
|
90
|
+
const identifier = resolveIdentifier(asset)
|
|
91
|
+
if (!selected.has(identifier)) selected.set(identifier, asset)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return Array.from(selected.values()).sort(compareDocumentAssetPriority)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const resolveRuleIdentifier = (
|
|
98
|
+
path: string,
|
|
99
|
+
explicitName?: string
|
|
100
|
+
) => resolveDocumentName(path, explicitName)
|
|
101
|
+
|
|
102
|
+
export const resolveSkillIdentifier = (
|
|
103
|
+
path: string,
|
|
104
|
+
explicitName?: string
|
|
105
|
+
) => resolveDocumentName(path, explicitName, ['skill.md'])
|
|
106
|
+
|
|
107
|
+
export const pickSpecAsset = (
|
|
108
|
+
bundle: WorkspaceAssetBundle,
|
|
109
|
+
name: string
|
|
110
|
+
): Extract<WorkspaceAsset, { kind: 'spec' }> | undefined => {
|
|
111
|
+
const assets = bundle.specs.filter((asset) => {
|
|
112
|
+
const definition = asset.payload.definition
|
|
113
|
+
return resolveSpecIdentifier(definition.path, definition.attributes.name) === name
|
|
114
|
+
})
|
|
115
|
+
return assets.find(asset => asset.origin === 'project') ?? assets[0]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const pickEntityAsset = (
|
|
119
|
+
bundle: WorkspaceAssetBundle,
|
|
120
|
+
name: string
|
|
121
|
+
): Extract<WorkspaceAsset, { kind: 'entity' }> | undefined => {
|
|
122
|
+
const assets = bundle.entities.filter((asset) => {
|
|
123
|
+
const definition = asset.payload.definition
|
|
124
|
+
const identifier = resolveDocumentName(definition.path, definition.attributes.name, ['readme.md', 'index.json'])
|
|
125
|
+
return identifier === name
|
|
126
|
+
})
|
|
127
|
+
return assets.find(asset => asset.origin === 'project') ?? assets[0]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const filterSkillAssets = (
|
|
131
|
+
skills: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>,
|
|
132
|
+
selection?: WorkspaceSkillSelection
|
|
133
|
+
): Array<Extract<WorkspaceAsset, { kind: 'skill' }>> => {
|
|
134
|
+
if (selection == null) return skills
|
|
135
|
+
|
|
136
|
+
const include = selection.include != null && selection.include.length > 0
|
|
137
|
+
? new Set(selection.include)
|
|
138
|
+
: undefined
|
|
139
|
+
const exclude = new Set(selection.exclude ?? [])
|
|
140
|
+
|
|
141
|
+
return skills.filter((skill) => {
|
|
142
|
+
const name = basename(dirname(skill.payload.definition.path))
|
|
143
|
+
return (include == null || include.has(name)) && !exclude.has(name)
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const dedupeSkillAssets = (
|
|
148
|
+
skills: Array<Extract<WorkspaceAsset, { kind: 'skill' }>>
|
|
149
|
+
): Array<Extract<WorkspaceAsset, { kind: 'skill' }>> => {
|
|
150
|
+
const seen = new Set<string>()
|
|
151
|
+
return skills.filter((skill) => {
|
|
152
|
+
if (seen.has(skill.payload.definition.path)) return false
|
|
153
|
+
seen.add(skill.payload.definition.path)
|
|
154
|
+
return true
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const resolveRulePatterns = (rules: RuleReference[]) => (
|
|
159
|
+
rules.flatMap((rule) => {
|
|
160
|
+
if (typeof rule === 'string') return [rule]
|
|
161
|
+
if (isLocalRuleReference(rule)) return [rule.path]
|
|
162
|
+
return []
|
|
163
|
+
})
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
export const resolveIncludedSkillNames = (selection: string[] | SkillSelection) => (
|
|
167
|
+
Array.isArray(selection)
|
|
168
|
+
? selection
|
|
169
|
+
: selection.type === 'include'
|
|
170
|
+
? selection.list
|
|
171
|
+
: []
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
export const resolveExcludedSkillNames = (selection: string[] | SkillSelection) => (
|
|
175
|
+
Array.isArray(selection)
|
|
176
|
+
? []
|
|
177
|
+
: selection.type === 'exclude'
|
|
178
|
+
? selection.list
|
|
179
|
+
: []
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
export const resolveSelectedRuleAssets = async (
|
|
183
|
+
bundle: WorkspaceAssetBundle,
|
|
184
|
+
patterns: string[]
|
|
185
|
+
): Promise<Array<Extract<WorkspaceAsset, { kind: 'rule' }>>> => {
|
|
186
|
+
const matchedPaths = new Set(
|
|
187
|
+
(await glob(patterns, { cwd: bundle.cwd, absolute: true }))
|
|
188
|
+
.map(normalizePath)
|
|
189
|
+
)
|
|
190
|
+
return bundle.rules.filter((asset) => matchedPaths.has(normalizePath(asset.payload.definition.path)))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const toDocumentDefinitions = <TDefinition>(
|
|
194
|
+
assets: Array<WorkspaceDocumentAsset<TDefinition>>
|
|
195
|
+
) => assets.map(asset => asset.payload.definition)
|
|
196
|
+
|
|
197
|
+
export const toPromptAssetIds = (assets: Array<{ id: string }>) => (
|
|
198
|
+
Array.from(new Set(assets.map(asset => asset.id)))
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
export type { WorkspaceDocumentAsset, WorkspaceDocumentPayload }
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { WorkspaceAsset } from '@vibe-forge/types'
|
|
2
|
+
import { resolveRelativePath } from '@vibe-forge/utils'
|
|
3
|
+
|
|
4
|
+
export const resolvePluginIdFromPath = (cwd: string, path: string) => {
|
|
5
|
+
const relativePath = resolveRelativePath(cwd, path)
|
|
6
|
+
const match = relativePath.match(/^\.ai\/plugins\/([^/]+)\//)
|
|
7
|
+
return match?.[1]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const isPluginEnabled = (
|
|
11
|
+
enabledPlugins: Record<string, boolean>,
|
|
12
|
+
pluginId?: string
|
|
13
|
+
) => pluginId == null || enabledPlugins[pluginId] !== false
|
|
14
|
+
|
|
15
|
+
export const mergeRecord = <T>(left?: Record<string, T>, right?: Record<string, T>) => ({
|
|
16
|
+
...(left ?? {}),
|
|
17
|
+
...(right ?? {})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const uniqueValues = (values: string[]) => Array.from(new Set(values.filter(Boolean)))
|
|
21
|
+
|
|
22
|
+
export const assetOriginPriority: Record<WorkspaceAsset['origin'], number> = {
|
|
23
|
+
project: 0,
|
|
24
|
+
plugin: 1,
|
|
25
|
+
config: 2,
|
|
26
|
+
fallback: 3
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const toAssetScope = (origin: WorkspaceAsset['origin']): WorkspaceAsset['scope'] => (
|
|
30
|
+
origin === 'config'
|
|
31
|
+
? 'project'
|
|
32
|
+
: origin === 'fallback'
|
|
33
|
+
? 'adapter'
|
|
34
|
+
: 'workspace'
|
|
35
|
+
)
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { WorkspaceAsset } from '@vibe-forge/types'
|
|
2
|
+
|
|
3
|
+
export interface WorkspaceDocumentPayload<TDefinition> {
|
|
4
|
+
definition: TDefinition
|
|
5
|
+
sourcePath: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface WorkspaceOverlayPayload {
|
|
9
|
+
sourcePath: string
|
|
10
|
+
entryName: string
|
|
11
|
+
targetSubpath: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type WorkspaceOpenCodeOverlayAsset =
|
|
15
|
+
| (
|
|
16
|
+
& Extract<WorkspaceAsset, { kind: 'nativePlugin' }>
|
|
17
|
+
& { payload: WorkspaceOverlayPayload }
|
|
18
|
+
)
|
|
19
|
+
| Extract<WorkspaceAsset, { kind: 'agent' | 'command' | 'mode' }>
|
|
20
|
+
|
|
21
|
+
export type WorkspaceDocumentAsset<TDefinition> =
|
|
22
|
+
& Extract<
|
|
23
|
+
WorkspaceAsset,
|
|
24
|
+
{ kind: 'rule' | 'spec' | 'entity' | 'skill' }
|
|
25
|
+
>
|
|
26
|
+
& {
|
|
27
|
+
payload: WorkspaceDocumentPayload<TDefinition & { path: string }>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const isOverlayPayload = (payload: unknown): payload is WorkspaceOverlayPayload => (
|
|
31
|
+
payload != null &&
|
|
32
|
+
typeof payload === 'object' &&
|
|
33
|
+
typeof (payload as WorkspaceOverlayPayload).sourcePath === 'string' &&
|
|
34
|
+
typeof (payload as WorkspaceOverlayPayload).entryName === 'string' &&
|
|
35
|
+
typeof (payload as WorkspaceOverlayPayload).targetSubpath === 'string'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
export const isOpenCodeOverlayAsset = (asset: WorkspaceAsset): asset is WorkspaceOpenCodeOverlayAsset => (
|
|
39
|
+
(asset.kind === 'nativePlugin' || asset.kind === 'agent' || asset.kind === 'command' || asset.kind === 'mode') &&
|
|
40
|
+
isOverlayPayload(asset.payload)
|
|
41
|
+
)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { basename, extname } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { glob } from 'fast-glob'
|
|
5
|
+
import type { Config, WorkspaceAsset, WorkspaceAssetAdapter } from '@vibe-forge/types'
|
|
6
|
+
import { resolveRelativePath } from '@vibe-forge/utils'
|
|
7
|
+
import yaml from 'js-yaml'
|
|
8
|
+
|
|
9
|
+
import type { WorkspaceOpenCodeOverlayAsset } from './internal-types'
|
|
10
|
+
import {
|
|
11
|
+
isPluginEnabled,
|
|
12
|
+
resolvePluginIdFromPath
|
|
13
|
+
} from './helpers'
|
|
14
|
+
|
|
15
|
+
const parseStructuredDocument = async (path: string) => {
|
|
16
|
+
const raw = await readFile(path, 'utf8')
|
|
17
|
+
const extension = extname(path).toLowerCase()
|
|
18
|
+
if (extension === '.yaml' || extension === '.yml') {
|
|
19
|
+
return yaml.load(raw)
|
|
20
|
+
}
|
|
21
|
+
return JSON.parse(raw)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const loadPluginMcpAssets = async (
|
|
25
|
+
cwd: string,
|
|
26
|
+
enabledPlugins: Record<string, boolean>
|
|
27
|
+
): Promise<Array<Extract<WorkspaceAsset, { kind: 'mcpServer' }>>> => {
|
|
28
|
+
const paths = await glob([
|
|
29
|
+
'.ai/plugins/*/mcp/*.json',
|
|
30
|
+
'.ai/plugins/*/mcp/*.yaml',
|
|
31
|
+
'.ai/plugins/*/mcp/*.yml'
|
|
32
|
+
], {
|
|
33
|
+
cwd,
|
|
34
|
+
absolute: true
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const entries = await Promise.all(paths.map(async (path) => {
|
|
38
|
+
const pluginId = resolvePluginIdFromPath(cwd, path)
|
|
39
|
+
if (!isPluginEnabled(enabledPlugins, pluginId)) return undefined
|
|
40
|
+
|
|
41
|
+
const parsed = await parseStructuredDocument(path)
|
|
42
|
+
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined
|
|
43
|
+
|
|
44
|
+
const record = parsed as Record<string, unknown>
|
|
45
|
+
const name = typeof record.name === 'string' && record.name.trim() !== ''
|
|
46
|
+
? record.name.trim()
|
|
47
|
+
: basename(path, extname(path))
|
|
48
|
+
const { name: _name, ...config } = record
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
id: `mcpServer:${resolveRelativePath(cwd, path)}`,
|
|
52
|
+
kind: 'mcpServer',
|
|
53
|
+
pluginId,
|
|
54
|
+
origin: 'plugin',
|
|
55
|
+
scope: 'workspace',
|
|
56
|
+
enabled: true,
|
|
57
|
+
targets: ['claude-code', 'codex', 'opencode'],
|
|
58
|
+
payload: {
|
|
59
|
+
name,
|
|
60
|
+
config: config as NonNullable<Config['mcpServers']>[string]
|
|
61
|
+
}
|
|
62
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'mcpServer' }>
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
return entries.filter((entry): entry is NonNullable<typeof entry> => entry != null)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const loadOpenCodeOverlayAssets = async (
|
|
69
|
+
cwd: string,
|
|
70
|
+
enabledPlugins: Record<string, boolean>
|
|
71
|
+
): Promise<WorkspaceOpenCodeOverlayAsset[]> => {
|
|
72
|
+
const paths = await glob([
|
|
73
|
+
'.ai/plugins/*/opencode/plugins/*',
|
|
74
|
+
'.ai/plugins/*/opencode/agents/*',
|
|
75
|
+
'.ai/plugins/*/opencode/commands/*',
|
|
76
|
+
'.ai/plugins/*/opencode/modes/*'
|
|
77
|
+
], {
|
|
78
|
+
cwd,
|
|
79
|
+
absolute: true,
|
|
80
|
+
onlyFiles: false
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return paths
|
|
84
|
+
.map((path) => {
|
|
85
|
+
const relativePath = resolveRelativePath(cwd, path)
|
|
86
|
+
const match = relativePath.match(/^\.ai\/plugins\/([^/]+)\/opencode\/(plugins|agents|commands|modes)\/([^/]+)$/)
|
|
87
|
+
if (!match) return undefined
|
|
88
|
+
|
|
89
|
+
const [, pluginId, rawFolder, entryName] = match
|
|
90
|
+
if (!isPluginEnabled(enabledPlugins, pluginId)) return undefined
|
|
91
|
+
|
|
92
|
+
const base = {
|
|
93
|
+
pluginId,
|
|
94
|
+
origin: 'plugin' as const,
|
|
95
|
+
scope: 'workspace' as const,
|
|
96
|
+
enabled: true,
|
|
97
|
+
targets: ['opencode'] as WorkspaceAssetAdapter[],
|
|
98
|
+
payload: {
|
|
99
|
+
sourcePath: path,
|
|
100
|
+
entryName,
|
|
101
|
+
targetSubpath: `${rawFolder}/${entryName}`
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (rawFolder === 'plugins') {
|
|
106
|
+
return {
|
|
107
|
+
id: `nativePlugin:${relativePath}`,
|
|
108
|
+
kind: 'nativePlugin',
|
|
109
|
+
...base
|
|
110
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'nativePlugin' }>
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (rawFolder === 'agents') {
|
|
114
|
+
return {
|
|
115
|
+
id: `agent:${relativePath}`,
|
|
116
|
+
kind: 'agent',
|
|
117
|
+
...base
|
|
118
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'agent' }>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (rawFolder === 'commands') {
|
|
122
|
+
return {
|
|
123
|
+
id: `command:${relativePath}`,
|
|
124
|
+
kind: 'command',
|
|
125
|
+
...base
|
|
126
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'command' }>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
id: `mode:${relativePath}`,
|
|
131
|
+
kind: 'mode',
|
|
132
|
+
...base
|
|
133
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'mode' }>
|
|
134
|
+
})
|
|
135
|
+
.filter((entry): entry is NonNullable<typeof entry> => entry != null)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const createHookPluginAssets = (
|
|
139
|
+
config: Config['plugins'],
|
|
140
|
+
enabledPlugins: Record<string, boolean>,
|
|
141
|
+
scope: Extract<WorkspaceAsset['scope'], 'project' | 'user'>
|
|
142
|
+
): Array<Extract<WorkspaceAsset, { kind: 'hookPlugin' }>> => {
|
|
143
|
+
if (config == null || Array.isArray(config)) return [] as Array<Extract<WorkspaceAsset, { kind: 'hookPlugin' }>>
|
|
144
|
+
|
|
145
|
+
return Object.entries(config)
|
|
146
|
+
.filter((entry) => enabledPlugins[entry[0]] !== false)
|
|
147
|
+
.map(([pluginId, pluginConfig]) => ({
|
|
148
|
+
id: `hookPlugin:${scope}:${pluginId}`,
|
|
149
|
+
kind: 'hookPlugin',
|
|
150
|
+
pluginId,
|
|
151
|
+
origin: 'config',
|
|
152
|
+
scope,
|
|
153
|
+
enabled: true,
|
|
154
|
+
targets: ['claude-code', 'codex', 'opencode'],
|
|
155
|
+
payload: {
|
|
156
|
+
packageName: pluginId,
|
|
157
|
+
config: pluginConfig
|
|
158
|
+
}
|
|
159
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'hookPlugin' }>))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const createClaudeNativePluginAssets = (
|
|
163
|
+
enabledPlugins: Record<string, boolean>
|
|
164
|
+
): Array<Extract<WorkspaceAsset, { kind: 'nativePlugin' }>> => {
|
|
165
|
+
return Object.entries(enabledPlugins).map(([pluginId, enabled]) => ({
|
|
166
|
+
id: `nativePlugin:claude-code:${pluginId}`,
|
|
167
|
+
kind: 'nativePlugin',
|
|
168
|
+
pluginId,
|
|
169
|
+
origin: 'config',
|
|
170
|
+
scope: 'project',
|
|
171
|
+
enabled,
|
|
172
|
+
targets: ['claude-code'],
|
|
173
|
+
payload: {
|
|
174
|
+
name: pluginId,
|
|
175
|
+
enabled
|
|
176
|
+
}
|
|
177
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'nativePlugin' }>))
|
|
178
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { basename, dirname } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { DefinitionLoader } from '@vibe-forge/definition-loader'
|
|
4
|
+
import type {
|
|
5
|
+
Filter,
|
|
6
|
+
PromptAssetResolution,
|
|
7
|
+
ResolvedPromptAssetOptions,
|
|
8
|
+
WorkspaceAsset,
|
|
9
|
+
WorkspaceAssetBundle,
|
|
10
|
+
WorkspaceSkillSelection
|
|
11
|
+
} from '@vibe-forge/types'
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
dedupeSkillAssets,
|
|
15
|
+
filterSkillAssets,
|
|
16
|
+
pickEntityAsset,
|
|
17
|
+
pickSpecAsset,
|
|
18
|
+
resolveExcludedSkillNames,
|
|
19
|
+
resolveIncludedSkillNames,
|
|
20
|
+
resolveRulePatterns,
|
|
21
|
+
resolveSelectedRuleAssets,
|
|
22
|
+
toDocumentDefinitions,
|
|
23
|
+
toPromptAssetIds
|
|
24
|
+
} from './document-assets'
|
|
25
|
+
|
|
26
|
+
export async function resolvePromptAssetSelection(
|
|
27
|
+
params: {
|
|
28
|
+
bundle: WorkspaceAssetBundle
|
|
29
|
+
type: 'spec' | 'entity' | undefined
|
|
30
|
+
name?: string
|
|
31
|
+
input?: {
|
|
32
|
+
skills?: WorkspaceSkillSelection
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
): Promise<[PromptAssetResolution, ResolvedPromptAssetOptions]> {
|
|
36
|
+
const loader = new DefinitionLoader(params.bundle.cwd)
|
|
37
|
+
const options: ResolvedPromptAssetOptions = {}
|
|
38
|
+
const systemPromptParts: string[] = []
|
|
39
|
+
|
|
40
|
+
const entities = params.type !== 'entity'
|
|
41
|
+
? toDocumentDefinitions(params.bundle.entities)
|
|
42
|
+
: []
|
|
43
|
+
const skills = toDocumentDefinitions(
|
|
44
|
+
filterSkillAssets(params.bundle.skills, params.input?.skills)
|
|
45
|
+
)
|
|
46
|
+
const rules = toDocumentDefinitions(params.bundle.rules)
|
|
47
|
+
const specs = toDocumentDefinitions(params.bundle.specs)
|
|
48
|
+
|
|
49
|
+
const promptAssetIds = new Set<string>([
|
|
50
|
+
...toPromptAssetIds(params.bundle.rules),
|
|
51
|
+
...(params.type !== 'entity' ? toPromptAssetIds(params.bundle.entities) : []),
|
|
52
|
+
...toPromptAssetIds(params.bundle.specs),
|
|
53
|
+
...toPromptAssetIds(filterSkillAssets(params.bundle.skills, params.input?.skills))
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
const targetSkillsAssets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>> = []
|
|
57
|
+
let targetBody = ''
|
|
58
|
+
let targetToolsFilter: Filter | undefined
|
|
59
|
+
let targetMcpServersFilter: Filter | undefined
|
|
60
|
+
let selectedSkillAssets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>> = []
|
|
61
|
+
|
|
62
|
+
if (params.input?.skills?.include != null && params.input.skills.include.length > 0) {
|
|
63
|
+
selectedSkillAssets = dedupeSkillAssets(
|
|
64
|
+
filterSkillAssets(params.bundle.skills, { include: params.input.skills.include })
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (params.type && params.name) {
|
|
69
|
+
const targetAsset = params.type === 'spec'
|
|
70
|
+
? pickSpecAsset(params.bundle, params.name)
|
|
71
|
+
: pickEntityAsset(params.bundle, params.name)
|
|
72
|
+
|
|
73
|
+
if (targetAsset == null) {
|
|
74
|
+
throw new Error(`Failed to load ${params.type} ${params.name}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { definition } = targetAsset.payload
|
|
78
|
+
const { attributes, body } = definition
|
|
79
|
+
promptAssetIds.add(targetAsset.id)
|
|
80
|
+
|
|
81
|
+
if (attributes.rules) {
|
|
82
|
+
const matchedRuleAssets = await resolveSelectedRuleAssets(params.bundle, resolveRulePatterns(attributes.rules))
|
|
83
|
+
rules.push(
|
|
84
|
+
...matchedRuleAssets.map((asset) => ({
|
|
85
|
+
...asset.payload.definition,
|
|
86
|
+
attributes: {
|
|
87
|
+
...asset.payload.definition.attributes,
|
|
88
|
+
always: true
|
|
89
|
+
}
|
|
90
|
+
}))
|
|
91
|
+
)
|
|
92
|
+
for (const asset of matchedRuleAssets) {
|
|
93
|
+
promptAssetIds.add(asset.id)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (attributes.skills) {
|
|
98
|
+
const includedSkillNames = new Set(resolveIncludedSkillNames(attributes.skills))
|
|
99
|
+
const excludedSkillNames = new Set(resolveExcludedSkillNames(attributes.skills))
|
|
100
|
+
for (const skillAsset of params.bundle.skills) {
|
|
101
|
+
const skillName = basename(dirname(skillAsset.payload.definition.path))
|
|
102
|
+
if (includedSkillNames.size > 0 && !includedSkillNames.has(skillName)) continue
|
|
103
|
+
if (excludedSkillNames.has(skillName)) continue
|
|
104
|
+
targetSkillsAssets.push(skillAsset)
|
|
105
|
+
promptAssetIds.add(skillAsset.id)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
targetBody = body
|
|
110
|
+
targetToolsFilter = attributes.tools
|
|
111
|
+
targetMcpServersFilter = attributes.mcpServers
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const targetSkills = toDocumentDefinitions(targetSkillsAssets)
|
|
115
|
+
const selectedSkillsPrompt = toDocumentDefinitions(
|
|
116
|
+
selectedSkillAssets.filter(
|
|
117
|
+
skill => !targetSkillsAssets.some(target => target.payload.definition.path === skill.payload.definition.path)
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
systemPromptParts.push(loader.generateRulesPrompt(rules))
|
|
122
|
+
systemPromptParts.push(loader.generateSkillsPrompt(targetSkills))
|
|
123
|
+
systemPromptParts.push(loader.generateSkillsPrompt(selectedSkillsPrompt))
|
|
124
|
+
systemPromptParts.push(loader.generateEntitiesRoutePrompt(entities))
|
|
125
|
+
systemPromptParts.push(loader.generateSkillsRoutePrompt(skills))
|
|
126
|
+
systemPromptParts.push(loader.generateSpecRoutePrompt(specs))
|
|
127
|
+
systemPromptParts.push(targetBody)
|
|
128
|
+
|
|
129
|
+
if (targetToolsFilter) {
|
|
130
|
+
options.tools = targetToolsFilter
|
|
131
|
+
}
|
|
132
|
+
if (targetMcpServersFilter) {
|
|
133
|
+
options.mcpServers = targetMcpServersFilter
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
options.systemPrompt = systemPromptParts.join('\n\n')
|
|
137
|
+
options.promptAssetIds = Array.from(promptAssetIds)
|
|
138
|
+
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
rules,
|
|
142
|
+
targetSkills,
|
|
143
|
+
entities,
|
|
144
|
+
skills,
|
|
145
|
+
specs,
|
|
146
|
+
targetBody,
|
|
147
|
+
promptAssetIds: Array.from(promptAssetIds)
|
|
148
|
+
},
|
|
149
|
+
options
|
|
150
|
+
]
|
|
151
|
+
}
|