@vibe-forge/workspace-assets 1.0.0 → 2.0.1
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/__tests__/__snapshots__/workspace-assets-rich.snapshot.json +55 -0
- package/__tests__/adapter-asset-plan.spec.ts +220 -6
- package/__tests__/bundle.spec.ts +677 -2
- package/__tests__/prompt-selection.spec.ts +307 -0
- package/__tests__/skill-dependencies-cli.spec.ts +175 -0
- package/__tests__/snapshot.ts +1 -0
- package/__tests__/test-helpers.ts +9 -0
- package/__tests__/workspace-assets.snapshot.spec.ts +2 -2
- package/package.json +5 -5
- package/src/adapter-asset-plan.ts +42 -66
- package/src/asset-source.ts +13 -0
- package/src/bundle-internal.ts +242 -22
- package/src/bundle.ts +4 -0
- package/src/configured-skills.ts +99 -0
- package/src/home-bridge.ts +1 -0
- package/src/index.ts +3 -0
- package/src/prompt-selection.ts +44 -19
- package/src/selection-internal.ts +335 -1
- package/src/skill-dependencies.ts +361 -0
- package/src/skills-cli-dependency.ts +208 -0
- package/src/workspace-config.ts +132 -0
- package/src/workspace-prompt.ts +29 -0
- package/src/workspaces.ts +188 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
|
|
4
|
+
import type { Config, ConfiguredSkillInstallConfig, SkillsCliConfig } from '@vibe-forge/types'
|
|
5
|
+
import {
|
|
6
|
+
installProjectSkill,
|
|
7
|
+
normalizeProjectSkillInstall,
|
|
8
|
+
resolveConfiguredSkillInstalls as resolveDeclaredConfiguredSkillInstalls,
|
|
9
|
+
resolveProjectAiPath,
|
|
10
|
+
resolveSkillsCliRuntimeConfig
|
|
11
|
+
} from '@vibe-forge/utils'
|
|
12
|
+
import type { NormalizedProjectSkillInstall } from '@vibe-forge/utils'
|
|
13
|
+
|
|
14
|
+
const resolveConfiguredSkillsCliConfig = (configs: [Config?, Config?]) => {
|
|
15
|
+
const [projectConfig, userConfig] = configs
|
|
16
|
+
const merged = {
|
|
17
|
+
...(resolveSkillsCliRuntimeConfig(projectConfig) ?? {}),
|
|
18
|
+
...(resolveSkillsCliRuntimeConfig(userConfig) ?? {})
|
|
19
|
+
} satisfies SkillsCliConfig
|
|
20
|
+
|
|
21
|
+
return Object.keys(merged).length === 0 ? undefined : merged
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const resolveConfiguredSkillInstalls = (configs: [Config?, Config?]) => (
|
|
25
|
+
[
|
|
26
|
+
...resolveDeclaredConfiguredSkillInstalls(configs[0]?.skills),
|
|
27
|
+
...resolveDeclaredConfiguredSkillInstalls(configs[1]?.skills)
|
|
28
|
+
]
|
|
29
|
+
.map((item) => normalizeProjectSkillInstall(item as string | ConfiguredSkillInstallConfig))
|
|
30
|
+
.filter((item): item is NormalizedProjectSkillInstall => item != null)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const pathExists = async (targetPath: string) => {
|
|
34
|
+
try {
|
|
35
|
+
await access(targetPath)
|
|
36
|
+
return true
|
|
37
|
+
} catch {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ensureUniqueTargets = (skills: NormalizedProjectSkillInstall[]) => {
|
|
43
|
+
const seen = new Map<string, string>()
|
|
44
|
+
|
|
45
|
+
for (const skill of skills) {
|
|
46
|
+
const previous = seen.get(skill.targetDirName)
|
|
47
|
+
if (previous != null) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Configured skills "${previous}" and "${skill.ref}" resolve to the same target "${skill.targetDirName}"`
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
seen.set(skill.targetDirName, skill.ref)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const ensureConfiguredProjectSkills = async (params: {
|
|
57
|
+
configs: [Config?, Config?]
|
|
58
|
+
updateInstalledSkills?: boolean
|
|
59
|
+
workspaceFolder: string
|
|
60
|
+
}) => {
|
|
61
|
+
const installs = resolveConfiguredSkillInstalls(params.configs)
|
|
62
|
+
if (installs.length === 0) {
|
|
63
|
+
return []
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ensureUniqueTargets(installs)
|
|
67
|
+
|
|
68
|
+
const skillsCliConfig = resolveConfiguredSkillsCliConfig(params.configs)
|
|
69
|
+
const ensured: Array<{ dirName: string; skillPath: string }> = []
|
|
70
|
+
|
|
71
|
+
for (const skill of installs) {
|
|
72
|
+
const skillPath = resolveProjectAiPath(
|
|
73
|
+
params.workspaceFolder,
|
|
74
|
+
process.env,
|
|
75
|
+
'skills',
|
|
76
|
+
skill.targetDirName,
|
|
77
|
+
'SKILL.md'
|
|
78
|
+
)
|
|
79
|
+
if (params.updateInstalledSkills !== true && await pathExists(skillPath)) {
|
|
80
|
+
ensured.push({
|
|
81
|
+
dirName: skill.targetDirName,
|
|
82
|
+
skillPath
|
|
83
|
+
})
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ensured.push(
|
|
88
|
+
await installProjectSkill({
|
|
89
|
+
config: skillsCliConfig,
|
|
90
|
+
force: true,
|
|
91
|
+
registry: undefined,
|
|
92
|
+
skill,
|
|
93
|
+
workspaceFolder: params.workspaceFolder
|
|
94
|
+
})
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return ensured
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const HOME_BRIDGE_RESOLVED_BY = 'home-bridge'
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export { buildAdapterAssetPlan } from './adapter-asset-plan'
|
|
2
|
+
export { resolveWorkspaceAssetSource } from './asset-source'
|
|
2
3
|
export { resolveWorkspaceAssetBundle } from './bundle'
|
|
4
|
+
export { HOME_BRIDGE_RESOLVED_BY } from './home-bridge'
|
|
3
5
|
export { resolvePromptAssetSelection } from './prompt-selection'
|
|
6
|
+
export { findWorkspaceAsset, resolveConfiguredWorkspaceAssets } from './workspaces'
|
package/src/prompt-selection.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
/* eslint-disable max-lines -- prompt asset selection coordinates routing, overlays, and dependency expansion */
|
|
1
2
|
import type {
|
|
2
3
|
Definition,
|
|
4
|
+
Entity,
|
|
3
5
|
Filter,
|
|
4
6
|
PluginOverlayConfig,
|
|
5
7
|
Rule,
|
|
6
8
|
RuleReference,
|
|
7
9
|
SkillSelection,
|
|
10
|
+
Spec,
|
|
8
11
|
WorkspaceAsset,
|
|
9
12
|
WorkspaceAssetBundle,
|
|
10
13
|
WorkspaceMcpSelection,
|
|
@@ -23,14 +26,17 @@ import {
|
|
|
23
26
|
import {
|
|
24
27
|
definitionWithResolvedName,
|
|
25
28
|
pickDocumentAsset,
|
|
29
|
+
resolveEntityInheritance,
|
|
26
30
|
resolveExcludedSkillRefs,
|
|
27
31
|
resolveIncludedSkillRefs,
|
|
28
32
|
resolveNamedAssets,
|
|
29
33
|
resolvePluginOverlay,
|
|
30
34
|
resolveRuleSelection,
|
|
31
|
-
|
|
35
|
+
resolveSelectedSkillAssetsWithDependencies,
|
|
32
36
|
toDocumentDefinitions
|
|
33
37
|
} from './selection-internal'
|
|
38
|
+
import { expandSkillAssetDependenciesWithRemoteResolution } from './skill-dependencies'
|
|
39
|
+
import { generateWorkspaceRoutePrompt } from './workspace-prompt'
|
|
34
40
|
|
|
35
41
|
export async function resolvePromptAssetSelection(params: {
|
|
36
42
|
bundle: WorkspaceAssetBundle
|
|
@@ -57,6 +63,8 @@ export async function resolvePromptAssetSelection(params: {
|
|
|
57
63
|
let targetToolsFilter: Filter | undefined
|
|
58
64
|
let targetMcpServersFilter: Filter | undefined
|
|
59
65
|
let targetInstancePath: string | undefined
|
|
66
|
+
let targetAssetIds: string[] = []
|
|
67
|
+
let targetDefinition: Definition<Entity | Spec> | undefined
|
|
60
68
|
|
|
61
69
|
if (params.type && params.name) {
|
|
62
70
|
const baseTarget = params.type === 'spec'
|
|
@@ -77,18 +85,30 @@ export async function resolvePromptAssetSelection(params: {
|
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
pinnedTargetAsset = baseTarget
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
if (params.type === 'entity') {
|
|
89
|
+
const resolvedEntity = resolveEntityInheritance(
|
|
90
|
+
effectiveBundle,
|
|
91
|
+
baseTarget as Extract<WorkspaceAsset, { kind: 'entity' }>
|
|
92
|
+
)
|
|
93
|
+
targetDefinition = resolvedEntity.definition
|
|
94
|
+
targetAssetIds = resolvedEntity.assetIds
|
|
95
|
+
} else {
|
|
96
|
+
targetDefinition = baseTarget.payload.definition
|
|
97
|
+
targetAssetIds = [baseTarget.id]
|
|
98
|
+
}
|
|
99
|
+
targetBody = targetDefinition.body
|
|
100
|
+
targetToolsFilter = targetDefinition.attributes.tools
|
|
101
|
+
targetMcpServersFilter = targetDefinition.attributes.mcpServers
|
|
83
102
|
targetInstancePath = baseTarget.instancePath
|
|
84
103
|
options.assetBundle = effectiveBundle
|
|
85
104
|
}
|
|
86
105
|
|
|
87
|
-
const selectedSkillAssets =
|
|
106
|
+
const selectedSkillAssets = await resolveSelectedSkillAssetsWithDependencies(effectiveBundle, params.input?.skills)
|
|
88
107
|
const useNativeProjectSkills = supportsNativeProjectSkills(params.adapter)
|
|
89
108
|
const promptAssetIds = new Set<string>([
|
|
90
109
|
...effectiveBundle.rules.map(asset => asset.id),
|
|
91
110
|
...effectiveBundle.specs.map(asset => asset.id),
|
|
111
|
+
...effectiveBundle.workspaces.map(asset => asset.id),
|
|
92
112
|
...(useNativeProjectSkills ? [] : selectedSkillAssets.map(asset => asset.id)),
|
|
93
113
|
...(params.type !== 'entity' ? effectiveBundle.entities.map(asset => asset.id) : [])
|
|
94
114
|
])
|
|
@@ -101,8 +121,8 @@ export async function resolvePromptAssetSelection(params: {
|
|
|
101
121
|
const targetSkillsAssets: Array<Extract<WorkspaceAsset, { kind: 'skill' }>> = []
|
|
102
122
|
|
|
103
123
|
if (pinnedTargetAsset != null) {
|
|
104
|
-
promptAssetIds.add(
|
|
105
|
-
const attributes = pinnedTargetAsset.payload.definition.attributes
|
|
124
|
+
targetAssetIds.forEach(assetId => promptAssetIds.add(assetId))
|
|
125
|
+
const attributes = targetDefinition?.attributes ?? pinnedTargetAsset.payload.definition.attributes
|
|
106
126
|
|
|
107
127
|
if (attributes.rules != null) {
|
|
108
128
|
const selection = await resolveRuleSelection(
|
|
@@ -144,12 +164,18 @@ export async function resolvePromptAssetSelection(params: {
|
|
|
144
164
|
resolveNamedAssets(effectiveBundle.skills, excludedRefs, targetInstancePath).map(asset => asset.id)
|
|
145
165
|
)
|
|
146
166
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
167
|
+
const expandedTargetSkills = await expandSkillAssetDependenciesWithRemoteResolution({
|
|
168
|
+
allAssets: effectiveBundle.assets,
|
|
169
|
+
configs: effectiveBundle.configs ?? [undefined, undefined],
|
|
170
|
+
cwd: effectiveBundle.cwd,
|
|
171
|
+
excludedIds,
|
|
172
|
+
selectedAssets: includedAssets,
|
|
173
|
+
skillAssets: effectiveBundle.skills
|
|
174
|
+
})
|
|
175
|
+
expandedTargetSkills.forEach((asset) => {
|
|
176
|
+
targetSkillsAssets.push(asset)
|
|
177
|
+
promptAssetIds.add(asset.id)
|
|
178
|
+
})
|
|
153
179
|
}
|
|
154
180
|
|
|
155
181
|
const rules = Array.from(ruleDefinitions.values())
|
|
@@ -162,11 +188,13 @@ export async function resolvePromptAssetSelection(params: {
|
|
|
162
188
|
: []
|
|
163
189
|
const skills = toDocumentDefinitions(selectedSkillAssets)
|
|
164
190
|
const specs = toDocumentDefinitions(effectiveBundle.specs)
|
|
191
|
+
const workspaces = effectiveBundle.workspaces.map(asset => asset.payload)
|
|
165
192
|
|
|
166
193
|
options.systemPrompt = [
|
|
167
194
|
generateRulesPrompt(effectiveBundle.cwd, rules),
|
|
168
195
|
generateSkillsPrompt(effectiveBundle.cwd, targetSkills),
|
|
169
196
|
generateEntitiesRoutePrompt(entities),
|
|
197
|
+
generateWorkspaceRoutePrompt(effectiveBundle.cwd, workspaces),
|
|
170
198
|
useNativeProjectSkills ? '' : generateSkillsRoutePrompt(effectiveBundle.cwd, routedSkills),
|
|
171
199
|
generateSpecRoutePrompt(specs, { active: params.type === 'spec' }),
|
|
172
200
|
targetBody
|
|
@@ -174,12 +202,8 @@ export async function resolvePromptAssetSelection(params: {
|
|
|
174
202
|
.filter(section => section !== '')
|
|
175
203
|
.join('\n\n')
|
|
176
204
|
|
|
177
|
-
if (targetToolsFilter != null)
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
if (targetMcpServersFilter != null) {
|
|
181
|
-
options.mcpServers = targetMcpServersFilter
|
|
182
|
-
}
|
|
205
|
+
if (targetToolsFilter != null) options.tools = targetToolsFilter
|
|
206
|
+
if (targetMcpServersFilter != null) options.mcpServers = targetMcpServersFilter
|
|
183
207
|
options.promptAssetIds = Array.from(promptAssetIds)
|
|
184
208
|
|
|
185
209
|
return [
|
|
@@ -189,6 +213,7 @@ export async function resolvePromptAssetSelection(params: {
|
|
|
189
213
|
entities,
|
|
190
214
|
skills,
|
|
191
215
|
specs,
|
|
216
|
+
workspaces,
|
|
192
217
|
targetBody,
|
|
193
218
|
promptAssetIds: Array.from(promptAssetIds)
|
|
194
219
|
},
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Definition,
|
|
3
|
+
Entity,
|
|
4
|
+
EntityInheritance,
|
|
5
|
+
EntityInheritanceMode,
|
|
6
|
+
Filter,
|
|
3
7
|
PluginConfig,
|
|
4
8
|
PluginOverlayConfig,
|
|
5
9
|
Rule,
|
|
@@ -22,6 +26,7 @@ import {
|
|
|
22
26
|
isRemoteRuleReference,
|
|
23
27
|
parseScopedReference
|
|
24
28
|
} from '@vibe-forge/definition-core'
|
|
29
|
+
import { expandSkillAssetDependencies, expandSkillAssetDependenciesWithRemoteResolution } from './skill-dependencies'
|
|
25
30
|
|
|
26
31
|
type DocumentAssetKind = Extract<WorkspaceAssetKind, 'rule' | 'spec' | 'entity' | 'skill'>
|
|
27
32
|
type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetKind }> & {
|
|
@@ -29,8 +34,28 @@ type DocumentAsset<TDefinition> = Extract<WorkspaceAsset, { kind: DocumentAssetK
|
|
|
29
34
|
definition: TDefinition & { path: string }
|
|
30
35
|
}
|
|
31
36
|
}
|
|
37
|
+
type EntityAsset = Extract<WorkspaceAsset, { kind: 'entity' }>
|
|
38
|
+
type EntityInheritanceField = Exclude<keyof EntityInheritance, 'default'>
|
|
32
39
|
|
|
33
40
|
const ASSET_REFERENCE_PATH_SUFFIXES = ['.md', '.json', '.yaml', '.yml']
|
|
41
|
+
const ENTITY_INHERITANCE_FIELDS = ['prompt', 'tags', 'rules', 'skills', 'tools', 'mcpServers'] as const
|
|
42
|
+
const ENTITY_INHERITANCE_MODES = new Set<EntityInheritanceMode>(['append', 'prepend', 'merge', 'replace', 'none'])
|
|
43
|
+
const DEFAULT_CHILD_ENTITY_INHERITANCE: Record<EntityInheritanceField, EntityInheritanceMode> = {
|
|
44
|
+
prompt: 'append',
|
|
45
|
+
tags: 'merge',
|
|
46
|
+
rules: 'merge',
|
|
47
|
+
skills: 'merge',
|
|
48
|
+
tools: 'replace',
|
|
49
|
+
mcpServers: 'replace'
|
|
50
|
+
}
|
|
51
|
+
const PARENT_ENTITY_INHERITANCE: Record<EntityInheritanceField, EntityInheritanceMode> = {
|
|
52
|
+
prompt: 'append',
|
|
53
|
+
tags: 'merge',
|
|
54
|
+
rules: 'merge',
|
|
55
|
+
skills: 'merge',
|
|
56
|
+
tools: 'replace',
|
|
57
|
+
mcpServers: 'replace'
|
|
58
|
+
}
|
|
34
59
|
|
|
35
60
|
export const definitionWithResolvedName = <TDefinition>(
|
|
36
61
|
definition: Definition<TDefinition>,
|
|
@@ -122,6 +147,293 @@ export const resolveNamedAssets = <TAsset extends Extract<WorkspaceAsset, { kind
|
|
|
122
147
|
return selected
|
|
123
148
|
}
|
|
124
149
|
|
|
150
|
+
const normalizeEntityExtends = (value: Entity['extends']) => {
|
|
151
|
+
if (typeof value === 'string') return value.trim() !== '' ? [value.trim()] : []
|
|
152
|
+
if (!Array.isArray(value)) return []
|
|
153
|
+
|
|
154
|
+
return value
|
|
155
|
+
.map(ref => ref.trim())
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const parseEntityInheritanceMode = (
|
|
160
|
+
value: unknown,
|
|
161
|
+
label: string
|
|
162
|
+
): EntityInheritanceMode | undefined => {
|
|
163
|
+
if (value == null) return undefined
|
|
164
|
+
if (typeof value !== 'string' || !ENTITY_INHERITANCE_MODES.has(value as EntityInheritanceMode)) {
|
|
165
|
+
throw new Error(`Invalid entity inherit mode for ${label}: ${String(value)}`)
|
|
166
|
+
}
|
|
167
|
+
return value as EntityInheritanceMode
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const resolveEntityInheritanceModes = (
|
|
171
|
+
value: Entity['inherit'],
|
|
172
|
+
defaults: Record<EntityInheritanceField, EntityInheritanceMode>
|
|
173
|
+
) => {
|
|
174
|
+
const modes = { ...defaults }
|
|
175
|
+
if (value == null) return modes
|
|
176
|
+
|
|
177
|
+
if (typeof value === 'string') {
|
|
178
|
+
const defaultMode = parseEntityInheritanceMode(value, 'inherit')
|
|
179
|
+
for (const field of ENTITY_INHERITANCE_FIELDS) {
|
|
180
|
+
modes[field] = defaultMode ?? modes[field]
|
|
181
|
+
}
|
|
182
|
+
return modes
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const defaultMode = parseEntityInheritanceMode(value.default, 'inherit.default')
|
|
186
|
+
for (const field of ENTITY_INHERITANCE_FIELDS) {
|
|
187
|
+
modes[field] = parseEntityInheritanceMode(value[field], `inherit.${field}`) ?? defaultMode ?? modes[field]
|
|
188
|
+
}
|
|
189
|
+
return modes
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const toUniqueStrings = (values: string[]) => Array.from(new Set(values))
|
|
193
|
+
|
|
194
|
+
const toUniqueValues = <TValue>(values: TValue[], keyOf: (value: TValue) => string) => {
|
|
195
|
+
const seen = new Set<string>()
|
|
196
|
+
const result: TValue[] = []
|
|
197
|
+
|
|
198
|
+
for (const value of values) {
|
|
199
|
+
const key = keyOf(value)
|
|
200
|
+
if (seen.has(key)) continue
|
|
201
|
+
seen.add(key)
|
|
202
|
+
result.push(value)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const keyRuleReference = (rule: RuleReference) => (
|
|
209
|
+
typeof rule === 'string' ? `string:${rule}` : `object:${JSON.stringify(rule)}`
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
const qualifyEntityReference = (
|
|
213
|
+
asset: EntityAsset,
|
|
214
|
+
ref: string
|
|
215
|
+
) => {
|
|
216
|
+
const value = ref.trim()
|
|
217
|
+
if (value === '' || asset.scope == null) return value
|
|
218
|
+
if (parseScopedReference(value, { pathSuffixes: ASSET_REFERENCE_PATH_SUFFIXES }) != null) return value
|
|
219
|
+
if (
|
|
220
|
+
isPathLikeReference(value, {
|
|
221
|
+
pathSuffixes: ASSET_REFERENCE_PATH_SUFFIXES,
|
|
222
|
+
allowGlob: true
|
|
223
|
+
})
|
|
224
|
+
) {
|
|
225
|
+
return value
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return `${asset.scope}/${value}`
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const qualifyEntityRuleReferences = (
|
|
232
|
+
asset: EntityAsset,
|
|
233
|
+
rules: Entity['rules']
|
|
234
|
+
) => rules?.map(rule => typeof rule === 'string' ? qualifyEntityReference(asset, rule) : rule)
|
|
235
|
+
|
|
236
|
+
const qualifyEntitySkillSelection = (
|
|
237
|
+
asset: EntityAsset,
|
|
238
|
+
selection: Entity['skills']
|
|
239
|
+
): Entity['skills'] => {
|
|
240
|
+
if (selection == null) return undefined
|
|
241
|
+
if (Array.isArray(selection)) return selection.map(ref => qualifyEntityReference(asset, ref))
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
...selection,
|
|
245
|
+
list: selection.list.map(ref => qualifyEntityReference(asset, ref))
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const qualifyEntityInternalReferences = (
|
|
250
|
+
asset: EntityAsset,
|
|
251
|
+
definition: Definition<Entity>
|
|
252
|
+
): Definition<Entity> => ({
|
|
253
|
+
...definition,
|
|
254
|
+
attributes: {
|
|
255
|
+
...definition.attributes,
|
|
256
|
+
rules: qualifyEntityRuleReferences(asset, definition.attributes.rules),
|
|
257
|
+
skills: qualifyEntitySkillSelection(asset, definition.attributes.skills)
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const selectInheritedValue = <TValue>(
|
|
262
|
+
parent: TValue | undefined,
|
|
263
|
+
child: TValue | undefined,
|
|
264
|
+
mode: EntityInheritanceMode,
|
|
265
|
+
merge: (left: TValue, right: TValue) => TValue
|
|
266
|
+
) => {
|
|
267
|
+
if (mode === 'none') return child
|
|
268
|
+
if (mode === 'replace') return child ?? parent
|
|
269
|
+
if (parent == null) return child
|
|
270
|
+
if (child == null) return parent
|
|
271
|
+
return mode === 'prepend' ? merge(child, parent) : merge(parent, child)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const mergeEntityBody = (
|
|
275
|
+
parent: string,
|
|
276
|
+
child: string,
|
|
277
|
+
mode: EntityInheritanceMode
|
|
278
|
+
) => {
|
|
279
|
+
if (mode === 'none' || mode === 'replace') return child
|
|
280
|
+
|
|
281
|
+
const values = mode === 'prepend' ? [child, parent] : [parent, child]
|
|
282
|
+
return values
|
|
283
|
+
.map(value => value.trim())
|
|
284
|
+
.filter(Boolean)
|
|
285
|
+
.join('\n\n')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const getSkillIncludeRefs = (selection: Entity['skills']) => {
|
|
289
|
+
if (Array.isArray(selection)) return selection
|
|
290
|
+
return selection?.type === 'include' ? selection.list : undefined
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const mergeSkillSelections = (
|
|
294
|
+
parent: Entity['skills'],
|
|
295
|
+
child: Entity['skills']
|
|
296
|
+
): Entity['skills'] => {
|
|
297
|
+
const parentRefs = getSkillIncludeRefs(parent)
|
|
298
|
+
const childRefs = getSkillIncludeRefs(child)
|
|
299
|
+
if (parentRefs != null && childRefs != null) return toUniqueStrings([...parentRefs, ...childRefs])
|
|
300
|
+
|
|
301
|
+
return child ?? parent
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const mergeFilters = (
|
|
305
|
+
parent: Filter,
|
|
306
|
+
child: Filter
|
|
307
|
+
): Filter => {
|
|
308
|
+
const include = toUniqueStrings([
|
|
309
|
+
...(parent.include ?? []),
|
|
310
|
+
...(child.include ?? [])
|
|
311
|
+
])
|
|
312
|
+
const exclude = toUniqueStrings([
|
|
313
|
+
...(parent.exclude ?? []),
|
|
314
|
+
...(child.exclude ?? [])
|
|
315
|
+
])
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
...(include.length > 0 ? { include } : {}),
|
|
319
|
+
...(exclude.length > 0 ? { exclude } : {})
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const mergeEntityDefinitions = (
|
|
324
|
+
parent: Definition<Entity>,
|
|
325
|
+
child: Definition<Entity>,
|
|
326
|
+
modes: Record<EntityInheritanceField, EntityInheritanceMode>
|
|
327
|
+
): Definition<Entity> => ({
|
|
328
|
+
...child,
|
|
329
|
+
body: mergeEntityBody(parent.body, child.body, modes.prompt),
|
|
330
|
+
attributes: {
|
|
331
|
+
...parent.attributes,
|
|
332
|
+
...child.attributes,
|
|
333
|
+
name: child.attributes.name,
|
|
334
|
+
description: child.attributes.description ?? parent.attributes.description,
|
|
335
|
+
always: child.attributes.always ?? parent.attributes.always,
|
|
336
|
+
tags: selectInheritedValue(
|
|
337
|
+
parent.attributes.tags,
|
|
338
|
+
child.attributes.tags,
|
|
339
|
+
modes.tags,
|
|
340
|
+
(left, right) => toUniqueStrings([...left, ...right])
|
|
341
|
+
),
|
|
342
|
+
rules: selectInheritedValue(
|
|
343
|
+
parent.attributes.rules,
|
|
344
|
+
child.attributes.rules,
|
|
345
|
+
modes.rules,
|
|
346
|
+
(left, right) => toUniqueValues([...left, ...right], keyRuleReference)
|
|
347
|
+
),
|
|
348
|
+
skills: selectInheritedValue(parent.attributes.skills, child.attributes.skills, modes.skills, mergeSkillSelections),
|
|
349
|
+
tools: selectInheritedValue(parent.attributes.tools, child.attributes.tools, modes.tools, mergeFilters),
|
|
350
|
+
mcpServers: selectInheritedValue(
|
|
351
|
+
parent.attributes.mcpServers,
|
|
352
|
+
child.attributes.mcpServers,
|
|
353
|
+
modes.mcpServers,
|
|
354
|
+
mergeFilters
|
|
355
|
+
),
|
|
356
|
+
plugins: child.attributes.plugins
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
const uniqueAssetIds = (values: string[]) => toUniqueValues(values, value => value)
|
|
361
|
+
|
|
362
|
+
const formatEntityCycle = (stack: EntityAsset[], asset: EntityAsset) => (
|
|
363
|
+
[...stack.slice(stack.findIndex(item => item.id === asset.id)), asset]
|
|
364
|
+
.map(item => item.displayName)
|
|
365
|
+
.join(' -> ')
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
const createAvailableEntitiesMessage = (entities: EntityAsset[]) => (
|
|
369
|
+
entities
|
|
370
|
+
.map(asset => asset.displayName)
|
|
371
|
+
.sort((left, right) => left.localeCompare(right))
|
|
372
|
+
.join(', ')
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
export const resolveEntityInheritance = (
|
|
376
|
+
bundle: WorkspaceAssetBundle,
|
|
377
|
+
asset: EntityAsset
|
|
378
|
+
): {
|
|
379
|
+
assetIds: string[]
|
|
380
|
+
definition: Definition<Entity>
|
|
381
|
+
} => {
|
|
382
|
+
const resolveAsset = (
|
|
383
|
+
current: EntityAsset,
|
|
384
|
+
stack: EntityAsset[]
|
|
385
|
+
): {
|
|
386
|
+
assetIds: string[]
|
|
387
|
+
definition: Definition<Entity>
|
|
388
|
+
} => {
|
|
389
|
+
if (stack.some(item => item.id === current.id)) {
|
|
390
|
+
throw new Error(`Circular entity inheritance detected: ${formatEntityCycle(stack, current)}`)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const currentDefinition = definitionWithResolvedName(
|
|
394
|
+
current.payload.definition,
|
|
395
|
+
current.displayName,
|
|
396
|
+
current.instancePath
|
|
397
|
+
)
|
|
398
|
+
const qualifiedCurrentDefinition = qualifyEntityInternalReferences(current, currentDefinition)
|
|
399
|
+
const parentRefs = normalizeEntityExtends(qualifiedCurrentDefinition.attributes.extends)
|
|
400
|
+
if (parentRefs.length === 0) {
|
|
401
|
+
return {
|
|
402
|
+
assetIds: [current.id],
|
|
403
|
+
definition: qualifiedCurrentDefinition
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let inheritedBase: Definition<Entity> | undefined
|
|
408
|
+
const inheritedAssetIds: string[] = []
|
|
409
|
+
for (const ref of parentRefs) {
|
|
410
|
+
const parentAsset = findNamedAsset(bundle.entities, ref, current.instancePath)
|
|
411
|
+
if (parentAsset == null) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
`Failed to resolve entity ${ref}. Available entities: ${createAvailableEntitiesMessage(bundle.entities)}`
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const parent = resolveAsset(parentAsset, [...stack, current])
|
|
418
|
+
inheritedAssetIds.push(...parent.assetIds)
|
|
419
|
+
inheritedBase = inheritedBase == null
|
|
420
|
+
? parent.definition
|
|
421
|
+
: mergeEntityDefinitions(inheritedBase, parent.definition, PARENT_ENTITY_INHERITANCE)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
assetIds: uniqueAssetIds([...inheritedAssetIds, current.id]),
|
|
426
|
+
definition: mergeEntityDefinitions(
|
|
427
|
+
inheritedBase ?? qualifiedCurrentDefinition,
|
|
428
|
+
qualifiedCurrentDefinition,
|
|
429
|
+
resolveEntityInheritanceModes(qualifiedCurrentDefinition.attributes.inherit, DEFAULT_CHILD_ENTITY_INHERITANCE)
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return resolveAsset(asset, [])
|
|
435
|
+
}
|
|
436
|
+
|
|
125
437
|
const resolvePathMatchedRules = async (
|
|
126
438
|
bundle: WorkspaceAssetBundle,
|
|
127
439
|
ref: string
|
|
@@ -211,7 +523,29 @@ export const resolveSelectedSkillAssets = (
|
|
|
211
523
|
const excluded = new Set(
|
|
212
524
|
resolveNamedAssets(assets, selection.exclude).map(asset => asset.id)
|
|
213
525
|
)
|
|
214
|
-
return
|
|
526
|
+
return expandSkillAssetDependencies(assets, included, {
|
|
527
|
+
excludedIds: excluded
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export const resolveSelectedSkillAssetsWithDependencies = async (
|
|
532
|
+
bundle: WorkspaceAssetBundle,
|
|
533
|
+
selection?: WorkspaceSkillSelection
|
|
534
|
+
): Promise<Array<Extract<WorkspaceAsset, { kind: 'skill' }>>> => {
|
|
535
|
+
const included = selection?.include != null && selection.include.length > 0
|
|
536
|
+
? resolveNamedAssets(bundle.skills, selection.include)
|
|
537
|
+
: bundle.skills
|
|
538
|
+
const excluded = new Set(
|
|
539
|
+
resolveNamedAssets(bundle.skills, selection?.exclude).map(asset => asset.id)
|
|
540
|
+
)
|
|
541
|
+
return await expandSkillAssetDependenciesWithRemoteResolution({
|
|
542
|
+
allAssets: bundle.assets,
|
|
543
|
+
configs: bundle.configs ?? [undefined, undefined],
|
|
544
|
+
cwd: bundle.cwd,
|
|
545
|
+
excludedIds: excluded,
|
|
546
|
+
selectedAssets: included,
|
|
547
|
+
skillAssets: bundle.skills
|
|
548
|
+
})
|
|
215
549
|
}
|
|
216
550
|
|
|
217
551
|
export const resolveSelectedMcpNames = (
|