@vibe-forge/workspace-assets 0.8.4 → 0.9.1-alpha.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/__tests__/__snapshots__/workspace-assets-rich.snapshot.json +355 -360
- package/__tests__/adapter-asset-plan.spec.ts +40 -66
- package/__tests__/bundle.spec.ts +44 -168
- package/__tests__/prompt-selection.spec.ts +14 -358
- package/__tests__/snapshot.ts +127 -75
- package/__tests__/test-helpers.ts +0 -11
- package/__tests__/workspace-assets.snapshot.spec.ts +103 -72
- package/package.json +10 -10
- package/src/adapter-asset-plan.ts +174 -0
- package/src/bundle.ts +178 -0
- package/src/document-assets.ts +191 -0
- package/src/helpers.ts +35 -0
- package/src/index.ts +3 -1368
- package/src/internal-types.ts +39 -1
- package/src/plugin-assets.ts +175 -0
- package/src/prompt-selection.ts +151 -0
- package/LICENSE +0 -21
package/src/internal-types.ts
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
1
|
import type { WorkspaceAsset } from '@vibe-forge/types'
|
|
2
2
|
|
|
3
|
-
export
|
|
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,175 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { basename, extname } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { Config, WorkspaceAsset, WorkspaceAssetAdapter } from '@vibe-forge/types'
|
|
5
|
+
import { resolveRelativePath } from '@vibe-forge/utils'
|
|
6
|
+
import { glob } from 'fast-glob'
|
|
7
|
+
import yaml from 'js-yaml'
|
|
8
|
+
|
|
9
|
+
import { isPluginEnabled, resolvePluginIdFromPath } from './helpers'
|
|
10
|
+
import type { WorkspaceOpenCodeOverlayAsset } from './internal-types'
|
|
11
|
+
|
|
12
|
+
const parseStructuredDocument = async (path: string) => {
|
|
13
|
+
const raw = await readFile(path, 'utf8')
|
|
14
|
+
const extension = extname(path).toLowerCase()
|
|
15
|
+
if (extension === '.yaml' || extension === '.yml') {
|
|
16
|
+
return yaml.load(raw)
|
|
17
|
+
}
|
|
18
|
+
return JSON.parse(raw)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const loadPluginMcpAssets = async (
|
|
22
|
+
cwd: string,
|
|
23
|
+
enabledPlugins: Record<string, boolean>
|
|
24
|
+
): Promise<Array<Extract<WorkspaceAsset, { kind: 'mcpServer' }>>> => {
|
|
25
|
+
const paths = await glob([
|
|
26
|
+
'.ai/plugins/*/mcp/*.json',
|
|
27
|
+
'.ai/plugins/*/mcp/*.yaml',
|
|
28
|
+
'.ai/plugins/*/mcp/*.yml'
|
|
29
|
+
], {
|
|
30
|
+
cwd,
|
|
31
|
+
absolute: true
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const entries = await Promise.all(paths.map(async (path) => {
|
|
35
|
+
const pluginId = resolvePluginIdFromPath(cwd, path)
|
|
36
|
+
if (!isPluginEnabled(enabledPlugins, pluginId)) return undefined
|
|
37
|
+
|
|
38
|
+
const parsed = await parseStructuredDocument(path)
|
|
39
|
+
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined
|
|
40
|
+
|
|
41
|
+
const record = parsed as Record<string, unknown>
|
|
42
|
+
const name = typeof record.name === 'string' && record.name.trim() !== ''
|
|
43
|
+
? record.name.trim()
|
|
44
|
+
: basename(path, extname(path))
|
|
45
|
+
const { name: _name, ...config } = record
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
id: `mcpServer:${resolveRelativePath(cwd, path)}`,
|
|
49
|
+
kind: 'mcpServer',
|
|
50
|
+
pluginId,
|
|
51
|
+
origin: 'plugin',
|
|
52
|
+
scope: 'workspace',
|
|
53
|
+
enabled: true,
|
|
54
|
+
targets: ['claude-code', 'codex', 'opencode'],
|
|
55
|
+
payload: {
|
|
56
|
+
name,
|
|
57
|
+
config: config as NonNullable<Config['mcpServers']>[string]
|
|
58
|
+
}
|
|
59
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'mcpServer' }>
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
return entries.filter((entry): entry is NonNullable<typeof entry> => entry != null)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const loadOpenCodeOverlayAssets = async (
|
|
66
|
+
cwd: string,
|
|
67
|
+
enabledPlugins: Record<string, boolean>
|
|
68
|
+
): Promise<WorkspaceOpenCodeOverlayAsset[]> => {
|
|
69
|
+
const paths = await glob([
|
|
70
|
+
'.ai/plugins/*/opencode/plugins/*',
|
|
71
|
+
'.ai/plugins/*/opencode/agents/*',
|
|
72
|
+
'.ai/plugins/*/opencode/commands/*',
|
|
73
|
+
'.ai/plugins/*/opencode/modes/*'
|
|
74
|
+
], {
|
|
75
|
+
cwd,
|
|
76
|
+
absolute: true,
|
|
77
|
+
onlyFiles: false
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return paths
|
|
81
|
+
.map((path) => {
|
|
82
|
+
const relativePath = resolveRelativePath(cwd, path)
|
|
83
|
+
const match = relativePath.match(/^\.ai\/plugins\/([^/]+)\/opencode\/(plugins|agents|commands|modes)\/([^/]+)$/)
|
|
84
|
+
if (!match) return undefined
|
|
85
|
+
|
|
86
|
+
const [, pluginId, rawFolder, entryName] = match
|
|
87
|
+
if (!isPluginEnabled(enabledPlugins, pluginId)) return undefined
|
|
88
|
+
|
|
89
|
+
const base = {
|
|
90
|
+
pluginId,
|
|
91
|
+
origin: 'plugin' as const,
|
|
92
|
+
scope: 'workspace' as const,
|
|
93
|
+
enabled: true,
|
|
94
|
+
targets: ['opencode'] as WorkspaceAssetAdapter[],
|
|
95
|
+
payload: {
|
|
96
|
+
sourcePath: path,
|
|
97
|
+
entryName,
|
|
98
|
+
targetSubpath: `${rawFolder}/${entryName}`
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (rawFolder === 'plugins') {
|
|
103
|
+
return {
|
|
104
|
+
id: `nativePlugin:${relativePath}`,
|
|
105
|
+
kind: 'nativePlugin',
|
|
106
|
+
...base
|
|
107
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'nativePlugin' }>
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (rawFolder === 'agents') {
|
|
111
|
+
return {
|
|
112
|
+
id: `agent:${relativePath}`,
|
|
113
|
+
kind: 'agent',
|
|
114
|
+
...base
|
|
115
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'agent' }>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (rawFolder === 'commands') {
|
|
119
|
+
return {
|
|
120
|
+
id: `command:${relativePath}`,
|
|
121
|
+
kind: 'command',
|
|
122
|
+
...base
|
|
123
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'command' }>
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
id: `mode:${relativePath}`,
|
|
128
|
+
kind: 'mode',
|
|
129
|
+
...base
|
|
130
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'mode' }>
|
|
131
|
+
})
|
|
132
|
+
.filter((entry): entry is NonNullable<typeof entry> => entry != null)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const createHookPluginAssets = (
|
|
136
|
+
config: Config['plugins'],
|
|
137
|
+
enabledPlugins: Record<string, boolean>,
|
|
138
|
+
scope: Extract<WorkspaceAsset['scope'], 'project' | 'user'>
|
|
139
|
+
): Array<Extract<WorkspaceAsset, { kind: 'hookPlugin' }>> => {
|
|
140
|
+
if (config == null || Array.isArray(config)) return [] as Array<Extract<WorkspaceAsset, { kind: 'hookPlugin' }>>
|
|
141
|
+
|
|
142
|
+
return Object.entries(config)
|
|
143
|
+
.filter((entry) => enabledPlugins[entry[0]] !== false)
|
|
144
|
+
.map(([pluginId, pluginConfig]) => ({
|
|
145
|
+
id: `hookPlugin:${scope}:${pluginId}`,
|
|
146
|
+
kind: 'hookPlugin',
|
|
147
|
+
pluginId,
|
|
148
|
+
origin: 'config',
|
|
149
|
+
scope,
|
|
150
|
+
enabled: true,
|
|
151
|
+
targets: ['claude-code', 'codex', 'opencode'],
|
|
152
|
+
payload: {
|
|
153
|
+
packageName: pluginId,
|
|
154
|
+
config: pluginConfig
|
|
155
|
+
}
|
|
156
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'hookPlugin' }>))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const createClaudeNativePluginAssets = (
|
|
160
|
+
enabledPlugins: Record<string, boolean>
|
|
161
|
+
): Array<Extract<WorkspaceAsset, { kind: 'nativePlugin' }>> => {
|
|
162
|
+
return Object.entries(enabledPlugins).map(([pluginId, enabled]) => ({
|
|
163
|
+
id: `nativePlugin:claude-code:${pluginId}`,
|
|
164
|
+
kind: 'nativePlugin',
|
|
165
|
+
pluginId,
|
|
166
|
+
origin: 'config',
|
|
167
|
+
scope: 'project',
|
|
168
|
+
enabled,
|
|
169
|
+
targets: ['claude-code'],
|
|
170
|
+
payload: {
|
|
171
|
+
name: pluginId,
|
|
172
|
+
enabled
|
|
173
|
+
}
|
|
174
|
+
} satisfies Extract<WorkspaceAsset, { kind: 'nativePlugin' }>))
|
|
175
|
+
}
|
|
@@ -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
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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.
|