@fugood/bricks-project 2.24.0-beta.4 → 2.24.0-beta.41
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/compile/action-name-map.ts +38 -0
- package/compile/index.ts +429 -160
- package/compile/util.ts +2 -0
- package/package.json +7 -3
- package/package.json.bak +27 -0
- package/skills/{bricks-project → bricks-ctor}/SKILL.md +2 -2
- package/skills/{bricks-project → bricks-ctor}/rules/animation.md +1 -1
- package/skills/{bricks-project → bricks-ctor}/rules/architecture-patterns.md +7 -0
- package/skills/bricks-ctor/rules/automations.md +221 -0
- package/skills/{bricks-project → bricks-ctor}/rules/buttress.md +10 -7
- package/skills/{bricks-project → bricks-ctor}/rules/data-calculation.md +1 -1
- package/skills/{bricks-project → bricks-ctor}/rules/local-sync.md +2 -2
- package/skills/{bricks-project → bricks-ctor}/rules/media-flow.md +3 -3
- package/skills/{bricks-project → bricks-ctor}/rules/remote-data-bank.md +6 -6
- package/skills/{bricks-project → bricks-ctor}/rules/standby-transition.md +1 -1
- package/skills/bricks-design/LICENSE.txt +180 -0
- package/skills/bricks-design/SKILL.md +66 -0
- package/tools/deploy.ts +66 -12
- package/tools/icons/fa6pro-meta.json +3669 -26125
- package/tools/mcp-server.ts +11 -878
- package/tools/mcp-tools/compile.ts +91 -0
- package/tools/mcp-tools/huggingface.ts +762 -0
- package/tools/mcp-tools/icons.ts +70 -0
- package/tools/mcp-tools/lottie.ts +102 -0
- package/tools/mcp-tools/media.ts +110 -0
- package/tools/postinstall.ts +137 -40
- package/tools/preview-main.mjs +146 -9
- package/tools/preview.ts +30 -2
- package/tools/pull.ts +37 -19
- package/tsconfig.json +16 -0
- package/types/animation.ts +4 -0
- package/types/automation.ts +4 -1
- package/types/brick-base.ts +1 -1
- package/types/bricks/Camera.ts +48 -13
- package/types/bricks/Chart.ts +10 -4
- package/types/bricks/GenerativeMedia.ts +30 -14
- package/types/bricks/Icon.ts +9 -5
- package/types/bricks/Image.ts +10 -6
- package/types/bricks/Items.ts +29 -15
- package/types/bricks/Lottie.ts +15 -7
- package/types/bricks/Maps.ts +16 -8
- package/types/bricks/QrCode.ts +9 -5
- package/types/bricks/Rect.ts +45 -6
- package/types/bricks/RichText.ts +9 -5
- package/types/bricks/Rive.ts +21 -11
- package/types/bricks/Slideshow.ts +20 -10
- package/types/bricks/Svg.ts +8 -4
- package/types/bricks/Text.ts +9 -5
- package/types/bricks/TextInput.ts +23 -13
- package/types/bricks/Video.ts +11 -7
- package/types/bricks/VideoStreaming.ts +8 -4
- package/types/bricks/WebRtcStream.ts +7 -3
- package/types/bricks/WebView.ts +12 -8
- package/types/canvas.ts +4 -2
- package/types/common.ts +19 -12
- package/types/data-calc-command.ts +2 -0
- package/types/data-calc.ts +1 -0
- package/types/data.ts +2 -0
- package/types/generators/AlarmClock.ts +17 -11
- package/types/generators/Assistant.ts +69 -18
- package/types/generators/BleCentral.ts +31 -11
- package/types/generators/BlePeripheral.ts +11 -7
- package/types/generators/CanvasMap.ts +10 -6
- package/types/generators/CastlesPay.ts +15 -7
- package/types/generators/DataBank.ts +44 -9
- package/types/generators/File.ts +109 -30
- package/types/generators/GraphQl.ts +12 -6
- package/types/generators/Http.ts +33 -10
- package/types/generators/HttpServer.ts +23 -15
- package/types/generators/Information.ts +9 -5
- package/types/generators/Intent.ts +15 -5
- package/types/generators/Iterator.ts +15 -11
- package/types/generators/Keyboard.ts +27 -13
- package/types/generators/LlmAnthropicCompat.ts +33 -11
- package/types/generators/LlmAppleBuiltin.ts +25 -10
- package/types/generators/LlmGgml.ts +140 -31
- package/types/generators/LlmMediaTekNeuroPilot.ts +235 -0
- package/types/generators/LlmMlx.ts +227 -0
- package/types/generators/LlmOnnx.ts +34 -14
- package/types/generators/LlmOpenAiCompat.ts +47 -11
- package/types/generators/LlmQualcommAiEngine.ts +45 -13
- package/types/generators/Mcp.ts +375 -34
- package/types/generators/McpServer.ts +58 -19
- package/types/generators/MediaFlow.ts +38 -12
- package/types/generators/MqttBroker.ts +29 -11
- package/types/generators/MqttClient.ts +19 -9
- package/types/generators/Question.ts +13 -9
- package/types/generators/RealtimeTranscription.ts +108 -19
- package/types/generators/RerankerGgml.ts +43 -12
- package/types/generators/SerialPort.ts +18 -10
- package/types/generators/SoundPlayer.ts +10 -4
- package/types/generators/SoundRecorder.ts +24 -9
- package/types/generators/SpeechToTextGgml.ts +52 -18
- package/types/generators/SpeechToTextOnnx.ts +18 -11
- package/types/generators/SpeechToTextPlatform.ts +15 -7
- package/types/generators/SqLite.ts +20 -10
- package/types/generators/Step.ts +9 -5
- package/types/generators/SttAppleBuiltin.ts +22 -9
- package/types/generators/Tcp.ts +13 -9
- package/types/generators/TcpServer.ts +20 -14
- package/types/generators/TextToSpeechAppleBuiltin.ts +21 -8
- package/types/generators/TextToSpeechGgml.ts +29 -11
- package/types/generators/TextToSpeechOnnx.ts +19 -12
- package/types/generators/TextToSpeechOpenAiLike.ts +14 -8
- package/types/generators/ThermalPrinter.ts +13 -9
- package/types/generators/Tick.ts +11 -7
- package/types/generators/Udp.ts +17 -8
- package/types/generators/VadGgml.ts +51 -14
- package/types/generators/VadOnnx.ts +42 -12
- package/types/generators/VadTraditional.ts +28 -13
- package/types/generators/VectorStore.ts +33 -12
- package/types/generators/Watchdog.ts +19 -10
- package/types/generators/WebCrawler.ts +11 -7
- package/types/generators/WebRtc.ts +30 -16
- package/types/generators/WebSocket.ts +11 -7
- package/types/generators/index.ts +2 -0
- package/types/subspace.ts +3 -0
- package/types/system.ts +1 -1
- package/utils/calc.ts +12 -8
- package/utils/event-props.ts +833 -1022
- package/utils/id.ts +4 -0
- package/api/index.ts +0 -1
- package/api/instance.ts +0 -213
- package/skills/bricks-project/rules/automations.md +0 -175
- package/types/generators/TextToSpeechApple.ts +0 -113
- package/types/generators/TtsAppleBuiltin.ts +0 -105
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import * as TOON from '@toon-format/toon'
|
|
4
|
+
import Fuse from 'fuse.js'
|
|
5
|
+
import glyphmap from '../icons/fa6pro-glyphmap.json'
|
|
6
|
+
import glyphmapMeta from '../icons/fa6pro-meta.json'
|
|
7
|
+
|
|
8
|
+
type IconStyle = 'brands' | 'duotone' | 'light' | 'regular' | 'solid' | 'thin'
|
|
9
|
+
|
|
10
|
+
// Bitmask bits for icon family membership (compact metadata format)
|
|
11
|
+
const STYLE_BITS: Record<IconStyle, number> = {
|
|
12
|
+
brands: 1,
|
|
13
|
+
thin: 2,
|
|
14
|
+
light: 4,
|
|
15
|
+
regular: 8,
|
|
16
|
+
solid: 16,
|
|
17
|
+
duotone: 256,
|
|
18
|
+
}
|
|
19
|
+
const ALL_STYLES = Object.keys(STYLE_BITS) as IconStyle[]
|
|
20
|
+
const iconMeta = glyphmapMeta as Record<string, number>
|
|
21
|
+
|
|
22
|
+
const iconList = Object.entries(glyphmap as Record<string, number>).map(([name, code]) => {
|
|
23
|
+
const bits = iconMeta[name] || 0
|
|
24
|
+
const styles = ALL_STYLES.filter((s) => bits & STYLE_BITS[s])
|
|
25
|
+
return { name, code, styles }
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const iconFuse = new Fuse(iconList, {
|
|
29
|
+
keys: ['name'],
|
|
30
|
+
threshold: 0.3,
|
|
31
|
+
includeScore: true,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export function register(server: McpServer) {
|
|
35
|
+
server.tool(
|
|
36
|
+
'icon_search',
|
|
37
|
+
{
|
|
38
|
+
query: z.string().describe('Search keywords for FontAwesome 6 Pro icons'),
|
|
39
|
+
limit: z.number().min(1).max(100).optional().default(10),
|
|
40
|
+
style: z
|
|
41
|
+
.enum(['brands', 'duotone', 'light', 'regular', 'solid', 'thin'])
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Filter by icon style'),
|
|
44
|
+
},
|
|
45
|
+
async ({ query, limit, style }) => {
|
|
46
|
+
let results = iconFuse.search(query, { limit: style ? limit * 3 : limit })
|
|
47
|
+
|
|
48
|
+
if (style) {
|
|
49
|
+
results = results.filter((r) => r.item.styles.includes(style)).slice(0, limit)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const icons = results.map((r) => ({
|
|
53
|
+
name: r.item.name,
|
|
54
|
+
code: r.item.code,
|
|
55
|
+
unicode: `U+${r.item.code.toString(16).toUpperCase()}`,
|
|
56
|
+
styles: r.item.styles,
|
|
57
|
+
score: r.score,
|
|
58
|
+
}))
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: 'text',
|
|
64
|
+
text: TOON.encode({ count: icons.length, icons }),
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import * as TOON from '@toon-format/toon'
|
|
4
|
+
|
|
5
|
+
const LOTTIEFILES_API_URL = 'https://lottiefiles.com/api'
|
|
6
|
+
|
|
7
|
+
export function register(server: McpServer) {
|
|
8
|
+
server.tool(
|
|
9
|
+
'lottie_search',
|
|
10
|
+
{
|
|
11
|
+
query: z.string().describe('Search keywords for animations'),
|
|
12
|
+
page: z.number().min(1).optional().default(1),
|
|
13
|
+
limit: z.number().min(1).max(100).optional().default(10),
|
|
14
|
+
},
|
|
15
|
+
async ({ query, page, limit }) => {
|
|
16
|
+
try {
|
|
17
|
+
const url = new URL(`${LOTTIEFILES_API_URL}/search/get-animations`)
|
|
18
|
+
url.searchParams.set('query', query)
|
|
19
|
+
url.searchParams.set('page', String(page))
|
|
20
|
+
url.searchParams.set('limit', String(limit))
|
|
21
|
+
url.searchParams.set('format', 'json')
|
|
22
|
+
|
|
23
|
+
const response = await fetch(url.toString())
|
|
24
|
+
const data = await response.json()
|
|
25
|
+
const animations = data?.data?.data ?? []
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
content: [
|
|
29
|
+
{
|
|
30
|
+
type: 'text',
|
|
31
|
+
text: TOON.encode({ count: animations.length, animations }),
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
}
|
|
35
|
+
} catch (err: any) {
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: 'text', text: `Failed to search animations: ${err.message}` }],
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
server.tool(
|
|
44
|
+
'lottie_get_details',
|
|
45
|
+
{
|
|
46
|
+
id: z.string().describe('Animation file ID'),
|
|
47
|
+
},
|
|
48
|
+
async ({ id }) => {
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(`${LOTTIEFILES_API_URL}/animations/get-animation-data`)
|
|
51
|
+
url.searchParams.set('fileId', id)
|
|
52
|
+
url.searchParams.set('format', 'json')
|
|
53
|
+
|
|
54
|
+
const response = await fetch(url.toString())
|
|
55
|
+
const data = await response.json()
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: 'text', text: TOON.encode(data) }],
|
|
59
|
+
}
|
|
60
|
+
} catch (err: any) {
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: 'text', text: `Failed to get animation: ${err.message}` }],
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
server.tool(
|
|
69
|
+
'lottie_popular',
|
|
70
|
+
{
|
|
71
|
+
page: z.number().min(1).optional().default(1),
|
|
72
|
+
limit: z.number().min(1).max(100).optional().default(10),
|
|
73
|
+
},
|
|
74
|
+
async ({ page, limit }) => {
|
|
75
|
+
try {
|
|
76
|
+
const url = new URL(
|
|
77
|
+
`${LOTTIEFILES_API_URL}/iconscout/popular-animations-weekly?api=%26sort%3Dpopular`,
|
|
78
|
+
)
|
|
79
|
+
url.searchParams.set('page', String(page))
|
|
80
|
+
url.searchParams.set('limit', String(limit))
|
|
81
|
+
url.searchParams.set('format', 'json')
|
|
82
|
+
|
|
83
|
+
const response = await fetch(url.toString())
|
|
84
|
+
const data = await response.json()
|
|
85
|
+
const animations = data?.popularWeeklyData?.data ?? []
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: 'text',
|
|
91
|
+
text: TOON.encode({ count: animations.length, animations }),
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
}
|
|
95
|
+
} catch (err: any) {
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: 'text', text: `Failed to get popular animations: ${err.message}` }],
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { $ } from 'bun'
|
|
4
|
+
|
|
5
|
+
const runBricks = async (projectDir: string, ...args: string[]) => {
|
|
6
|
+
try {
|
|
7
|
+
return await $`bunx bricks ${args}`.cwd(projectDir).text()
|
|
8
|
+
} catch (err: any) {
|
|
9
|
+
throw new Error(err.stderr?.toString() || err.message)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function register(server: McpServer, projectDir: string) {
|
|
14
|
+
server.tool('media_boxes', {}, async () => {
|
|
15
|
+
try {
|
|
16
|
+
const output = await runBricks(projectDir, 'media', 'boxes')
|
|
17
|
+
return { content: [{ type: 'text', text: output }] }
|
|
18
|
+
} catch (err: any) {
|
|
19
|
+
return { content: [{ type: 'text', text: `Failed to list media boxes: ${err.message}` }] }
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
server.tool('media_get_box', { id: z.string().describe('Media box ID') }, async ({ id }) => {
|
|
24
|
+
try {
|
|
25
|
+
const output = await runBricks(projectDir, 'media', 'box', id)
|
|
26
|
+
return { content: [{ type: 'text', text: output }] }
|
|
27
|
+
} catch (err: any) {
|
|
28
|
+
return { content: [{ type: 'text', text: `Failed to get media box: ${err.message}` }] }
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
server.tool(
|
|
33
|
+
'media_files',
|
|
34
|
+
{
|
|
35
|
+
boxId: z.string().describe('Media box ID'),
|
|
36
|
+
types: z.string().describe('Comma-separated file types to include').optional(),
|
|
37
|
+
userTag: z.array(z.string()).describe('Filter by user tags').optional(),
|
|
38
|
+
limit: z.number().describe('Limit results').optional(),
|
|
39
|
+
offset: z.number().describe('Offset results').optional(),
|
|
40
|
+
},
|
|
41
|
+
async ({ boxId, types, userTag, limit, offset }) => {
|
|
42
|
+
try {
|
|
43
|
+
const args = ['media', 'files', boxId]
|
|
44
|
+
if (types) args.push('-t', types)
|
|
45
|
+
if (userTag) userTag.forEach((tag) => args.push('-u', tag))
|
|
46
|
+
if (limit != null) args.push('-l', String(limit))
|
|
47
|
+
if (offset != null) args.push('-o', String(offset))
|
|
48
|
+
const output = await runBricks(projectDir, ...args)
|
|
49
|
+
return { content: [{ type: 'text', text: output }] }
|
|
50
|
+
} catch (err: any) {
|
|
51
|
+
return { content: [{ type: 'text', text: `Failed to list media files: ${err.message}` }] }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
server.tool('media_file', { id: z.string().describe('Media file ID') }, async ({ id }) => {
|
|
57
|
+
try {
|
|
58
|
+
const output = await runBricks(projectDir, 'media', 'file', id)
|
|
59
|
+
return { content: [{ type: 'text', text: output }] }
|
|
60
|
+
} catch (err: any) {
|
|
61
|
+
return { content: [{ type: 'text', text: `Failed to get media file: ${err.message}` }] }
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
server.tool(
|
|
66
|
+
'media_upload_files',
|
|
67
|
+
{
|
|
68
|
+
boxId: z.string().describe('Target media box ID'),
|
|
69
|
+
files: z.array(z.string()).min(1).describe('File paths to upload'),
|
|
70
|
+
description: z.string().describe('File description').optional(),
|
|
71
|
+
userTags: z.array(z.string()).max(15).describe('User tags (max 15)').optional(),
|
|
72
|
+
imageVersion: z
|
|
73
|
+
.array(z.string())
|
|
74
|
+
.describe('Image resize specs as WxH or WxH:STRATEGY')
|
|
75
|
+
.optional(),
|
|
76
|
+
imageVersionType: z.enum(['jpg', 'png']).describe('Image output format').optional(),
|
|
77
|
+
enableAiAnalysis: z.boolean().describe('Enable AI analysis').optional(),
|
|
78
|
+
aiInstruction: z.string().describe('Custom AI analysis instruction').optional(),
|
|
79
|
+
concurrency: z.number().min(1).describe('Max concurrent uploads').optional(),
|
|
80
|
+
},
|
|
81
|
+
async ({
|
|
82
|
+
boxId,
|
|
83
|
+
files,
|
|
84
|
+
description,
|
|
85
|
+
userTags,
|
|
86
|
+
imageVersion,
|
|
87
|
+
imageVersionType,
|
|
88
|
+
enableAiAnalysis,
|
|
89
|
+
aiInstruction,
|
|
90
|
+
concurrency,
|
|
91
|
+
}) => {
|
|
92
|
+
try {
|
|
93
|
+
const args = ['media', 'upload', boxId, ...files]
|
|
94
|
+
if (description) args.push('-d', description)
|
|
95
|
+
if (userTags) userTags.forEach((tag) => args.push('-t', tag))
|
|
96
|
+
if (imageVersion) imageVersion.forEach((spec) => args.push('--image-version', spec))
|
|
97
|
+
if (imageVersionType) args.push('--image-version-type', imageVersionType)
|
|
98
|
+
if (enableAiAnalysis) args.push('--enable-ai-analysis')
|
|
99
|
+
if (aiInstruction) args.push('--ai-instruction', aiInstruction)
|
|
100
|
+
if (concurrency != null) args.push('--concurrency', String(concurrency))
|
|
101
|
+
const output = await runBricks(projectDir, ...args)
|
|
102
|
+
return { content: [{ type: 'text', text: output }] }
|
|
103
|
+
} catch (err: any) {
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: 'text', text: `Failed to upload media files: ${err.message}` }],
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
}
|
package/tools/postinstall.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { $ } from 'bun'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
cp,
|
|
4
|
+
lstat,
|
|
5
|
+
mkdir,
|
|
6
|
+
readFile,
|
|
7
|
+
readdir,
|
|
8
|
+
readlink,
|
|
9
|
+
rm,
|
|
10
|
+
stat,
|
|
11
|
+
symlink,
|
|
12
|
+
writeFile,
|
|
13
|
+
} from 'fs/promises'
|
|
14
|
+
import * as path from 'path'
|
|
3
15
|
import TOML from '@iarna/toml'
|
|
4
16
|
|
|
5
17
|
const cwd = process.cwd()
|
|
18
|
+
const projectSkillsDir = path.join(cwd, '.bricks', 'skills')
|
|
19
|
+
const compatibilitySkillLinks = [
|
|
20
|
+
path.join(cwd, '.claude', 'skills'),
|
|
21
|
+
path.join(cwd, '.codex', 'skills'),
|
|
22
|
+
]
|
|
6
23
|
|
|
7
24
|
async function exists(f: string) {
|
|
8
25
|
try {
|
|
@@ -13,26 +30,47 @@ async function exists(f: string) {
|
|
|
13
30
|
}
|
|
14
31
|
}
|
|
15
32
|
|
|
33
|
+
async function pathExists(f: string) {
|
|
34
|
+
try {
|
|
35
|
+
await lstat(f)
|
|
36
|
+
return true
|
|
37
|
+
} catch {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Migrate old projects: remove legacy project/ directory
|
|
43
|
+
const oldProjectDir = path.join(cwd, 'project')
|
|
44
|
+
if (await exists(oldProjectDir)) {
|
|
45
|
+
await rm(oldProjectDir, { recursive: true, force: true })
|
|
46
|
+
console.log('Removed legacy project/ directory')
|
|
47
|
+
}
|
|
48
|
+
|
|
16
49
|
// handle flag --skip-copy
|
|
17
50
|
const skipCopyProject = process.argv.includes('--skip-copy-project')
|
|
18
51
|
if (skipCopyProject) {
|
|
19
|
-
console.log('Skipping copy of files to
|
|
52
|
+
console.log('Skipping copy of files to ctor/')
|
|
20
53
|
} else {
|
|
21
54
|
const libFiles = ['types', 'utils', 'index.ts']
|
|
22
55
|
|
|
23
|
-
await $`mkdir -p ${cwd}/
|
|
24
|
-
await Promise.all(libFiles.map((file) => $`cp -r ${__dirname}/../${file} ${cwd}/
|
|
25
|
-
console.log('Copied files to
|
|
56
|
+
await $`mkdir -p ${cwd}/ctor`
|
|
57
|
+
await Promise.all(libFiles.map((file) => $`cp -r ${__dirname}/../${file} ${cwd}/ctor`))
|
|
58
|
+
console.log('Copied files to ctor/')
|
|
26
59
|
}
|
|
27
60
|
|
|
28
61
|
const projectMcpServer = {
|
|
29
62
|
command: 'bun',
|
|
30
|
-
args: [`${cwd}/node_modules/@fugood/bricks-
|
|
63
|
+
args: [`${cwd}/node_modules/@fugood/bricks-ctor/tools/mcp-server.ts`],
|
|
31
64
|
}
|
|
32
65
|
|
|
66
|
+
type CodexMcpConfig = {
|
|
67
|
+
mcp_servers: Record<string, typeof projectMcpServer>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Claude Code and AGENTS.md projects both use the shared project .mcp.json file.
|
|
33
71
|
const defaultMcpConfig = {
|
|
34
72
|
mcpServers: {
|
|
35
|
-
'bricks-
|
|
73
|
+
'bricks-ctor': projectMcpServer,
|
|
36
74
|
},
|
|
37
75
|
}
|
|
38
76
|
|
|
@@ -43,7 +81,8 @@ const handleMcpConfigOverride = async (mcpConfigPath: string) => {
|
|
|
43
81
|
try {
|
|
44
82
|
mcpConfig = JSON.parse(configStr)
|
|
45
83
|
if (!mcpConfig?.mcpServers) throw new Error('mcpServers is not defined')
|
|
46
|
-
mcpConfig.mcpServers['bricks-
|
|
84
|
+
mcpConfig.mcpServers['bricks-ctor'] = projectMcpServer
|
|
85
|
+
delete mcpConfig.mcpServers['bricks-project']
|
|
47
86
|
} catch {
|
|
48
87
|
mcpConfig = defaultMcpConfig
|
|
49
88
|
}
|
|
@@ -60,57 +99,115 @@ const hasClaudeCode = await exists(`${cwd}/CLAUDE.md`)
|
|
|
60
99
|
const hasAgentsMd = await exists(`${cwd}/AGENTS.md`)
|
|
61
100
|
|
|
62
101
|
if (hasClaudeCode || hasAgentsMd) {
|
|
102
|
+
// Keep the workspace-level JSON MCP config aligned for tools that read .mcp.json.
|
|
63
103
|
const mcpConfigPath = `${cwd}/.mcp.json`
|
|
64
104
|
await handleMcpConfigOverride(mcpConfigPath)
|
|
65
105
|
}
|
|
66
106
|
|
|
67
|
-
const
|
|
107
|
+
const copyMissingSkills = async (sourceDir: string, targetDir: string) => {
|
|
108
|
+
if (!(await exists(sourceDir))) return
|
|
109
|
+
|
|
110
|
+
const packageSkills = await readdir(sourceDir, { withFileTypes: true })
|
|
111
|
+
const skillsToInstall = packageSkills.filter(
|
|
112
|
+
(entry) => entry.isDirectory() && !entry.name.startsWith('.'),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
await mkdir(targetDir, { recursive: true })
|
|
116
|
+
|
|
117
|
+
await Promise.all(
|
|
118
|
+
skillsToInstall.map(async (entry) => {
|
|
119
|
+
const targetSkillDir = path.join(targetDir, entry.name)
|
|
120
|
+
if (await exists(targetSkillDir)) {
|
|
121
|
+
console.log(`Skill '${entry.name}' already exists, skipping`)
|
|
122
|
+
} else {
|
|
123
|
+
await cp(path.join(sourceDir, entry.name), targetSkillDir, { recursive: true })
|
|
124
|
+
console.log(`Installed skill '${entry.name}' to ${targetDir}/`)
|
|
125
|
+
}
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const migrateSkillsDir = async (legacySkillsDir: string, canonicalSkillsDir: string) => {
|
|
131
|
+
if (!(await pathExists(legacySkillsDir))) return
|
|
132
|
+
|
|
133
|
+
const legacyStats = await lstat(legacySkillsDir)
|
|
134
|
+
|
|
135
|
+
if (legacyStats.isSymbolicLink()) {
|
|
136
|
+
const linkTarget = await readlink(legacySkillsDir)
|
|
137
|
+
const resolvedTarget = path.resolve(path.dirname(legacySkillsDir), linkTarget)
|
|
138
|
+
if (resolvedTarget === canonicalSkillsDir) return
|
|
139
|
+
|
|
140
|
+
await copyMissingSkills(resolvedTarget, canonicalSkillsDir)
|
|
141
|
+
await rm(legacySkillsDir, { force: true, recursive: true })
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (legacyStats.isDirectory()) {
|
|
146
|
+
await copyMissingSkills(legacySkillsDir, canonicalSkillsDir)
|
|
147
|
+
await rm(legacySkillsDir, { force: true, recursive: true })
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.warn(`Skipping skills migration for ${legacySkillsDir}; expected a directory or symlink`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const ensureCompatibilitySkillLink = async (linkPath: string, targetDir: string) => {
|
|
155
|
+
await mkdir(path.dirname(linkPath), { recursive: true })
|
|
156
|
+
|
|
157
|
+
if (await pathExists(linkPath)) {
|
|
158
|
+
const linkStats = await lstat(linkPath)
|
|
159
|
+
if (linkStats.isSymbolicLink()) {
|
|
160
|
+
const linkTarget = await readlink(linkPath)
|
|
161
|
+
const resolvedTarget = path.resolve(path.dirname(linkPath), linkTarget)
|
|
162
|
+
if (resolvedTarget === targetDir) return
|
|
163
|
+
} else {
|
|
164
|
+
console.warn(
|
|
165
|
+
`Skipping skills symlink at ${linkPath}; path already exists and is not a symlink`,
|
|
166
|
+
)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const relativeTarget = path.relative(path.dirname(linkPath), targetDir)
|
|
172
|
+
const symlinkType = process.platform === 'win32' ? 'junction' : 'dir'
|
|
173
|
+
await symlink(relativeTarget, linkPath, symlinkType)
|
|
174
|
+
console.log(`Linked ${linkPath} -> ${relativeTarget}`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const setupSkills = async () => {
|
|
68
178
|
const packageSkillsDir = `${__dirname}/../skills`
|
|
179
|
+
await mkdir(projectSkillsDir, { recursive: true })
|
|
180
|
+
await copyMissingSkills(packageSkillsDir, projectSkillsDir)
|
|
69
181
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
await $`mkdir -p ${skillsDir}`
|
|
75
|
-
|
|
76
|
-
await Promise.all(
|
|
77
|
-
skillsToInstall.map(async (skill) => {
|
|
78
|
-
const targetSkillDir = `${skillsDir}/${skill}`
|
|
79
|
-
if (await exists(targetSkillDir)) {
|
|
80
|
-
console.log(`Skill '${skill}' already exists, skipping`)
|
|
81
|
-
} else {
|
|
82
|
-
await $`cp -r ${packageSkillsDir}/${skill} ${targetSkillDir}`
|
|
83
|
-
console.log(`Installed skill '${skill}' to ${skillsDir}/`)
|
|
84
|
-
}
|
|
85
|
-
}),
|
|
86
|
-
)
|
|
182
|
+
for (const linkPath of compatibilitySkillLinks) {
|
|
183
|
+
await migrateSkillsDir(linkPath, projectSkillsDir)
|
|
184
|
+
await ensureCompatibilitySkillLink(linkPath, projectSkillsDir)
|
|
87
185
|
}
|
|
88
186
|
}
|
|
89
187
|
|
|
90
|
-
if (hasClaudeCode) {
|
|
91
|
-
// Install skills
|
|
92
|
-
await setupSkills(
|
|
188
|
+
if (hasClaudeCode || hasAgentsMd) {
|
|
189
|
+
// Install project skills once and expose them through compatibility symlinks.
|
|
190
|
+
await setupSkills()
|
|
93
191
|
}
|
|
94
192
|
|
|
95
193
|
if (hasAgentsMd) {
|
|
96
|
-
//
|
|
97
|
-
// Currently no signal file for codex skills, so we just check if AGENTS.md exists
|
|
98
|
-
await setupSkills(`${cwd}/.codex/skills`)
|
|
99
|
-
|
|
194
|
+
// Codex stores its project-local MCP config in .codex/config.toml.
|
|
100
195
|
const defaultCodexMcpConfig = {
|
|
101
196
|
mcp_servers: {
|
|
102
|
-
'bricks-
|
|
197
|
+
'bricks-ctor': projectMcpServer,
|
|
103
198
|
},
|
|
104
199
|
}
|
|
105
200
|
|
|
106
201
|
const handleCodexMcpConfigOverride = async (mcpConfigPath: string) => {
|
|
107
|
-
let mcpConfig:
|
|
202
|
+
let mcpConfig: CodexMcpConfig | null = null
|
|
108
203
|
if (await exists(mcpConfigPath)) {
|
|
109
204
|
const configStr = await readFile(mcpConfigPath, 'utf-8')
|
|
110
205
|
try {
|
|
111
|
-
|
|
112
|
-
if (!
|
|
113
|
-
mcpConfig.mcp_servers
|
|
206
|
+
const parsed = TOML.parse(configStr) as Partial<CodexMcpConfig>
|
|
207
|
+
if (!parsed?.mcp_servers) throw new Error('mcp_servers is not defined')
|
|
208
|
+
mcpConfig = { mcp_servers: parsed.mcp_servers }
|
|
209
|
+
mcpConfig.mcp_servers['bricks-ctor'] = projectMcpServer
|
|
210
|
+
delete mcpConfig.mcp_servers['bricks-project']
|
|
114
211
|
} catch {
|
|
115
212
|
mcpConfig = defaultCodexMcpConfig
|
|
116
213
|
}
|
|
@@ -118,12 +215,12 @@ if (hasAgentsMd) {
|
|
|
118
215
|
mcpConfig = defaultCodexMcpConfig
|
|
119
216
|
}
|
|
120
217
|
|
|
121
|
-
await writeFile(mcpConfigPath, `${TOML.stringify(mcpConfig
|
|
218
|
+
await writeFile(mcpConfigPath, `${TOML.stringify(mcpConfig)}\n`)
|
|
122
219
|
|
|
123
220
|
console.log(`Updated ${mcpConfigPath}`)
|
|
124
221
|
}
|
|
125
222
|
|
|
126
|
-
//
|
|
223
|
+
// Keep the Codex TOML MCP config aligned with the same bricks-ctor server entry.
|
|
127
224
|
const codexConfigPath = `${cwd}/.codex/config.toml`
|
|
128
225
|
await handleCodexMcpConfigOverride(codexConfigPath)
|
|
129
226
|
}
|