@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.
@@ -1,3 +1,41 @@
1
1
  import type { WorkspaceAsset } from '@vibe-forge/types'
2
2
 
3
- export const isOpenCodeOverlayAsset = (asset: WorkspaceAsset) => asset.kind === 'nativePlugin'
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.