@fugood/bricks-project 2.24.0-beta.9 → 2.24.1-beta.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.
Files changed (125) hide show
  1. package/compile/action-name-map.ts +38 -0
  2. package/compile/index.ts +460 -158
  3. package/compile/util.ts +2 -0
  4. package/package.json +8 -3
  5. package/package.json.bak +28 -0
  6. package/skills/{bricks-project → bricks-ctor}/SKILL.md +2 -2
  7. package/skills/{bricks-project → bricks-ctor}/rules/animation.md +1 -1
  8. package/skills/{bricks-project → bricks-ctor}/rules/architecture-patterns.md +7 -0
  9. package/skills/{bricks-project → bricks-ctor}/rules/buttress.md +10 -7
  10. package/skills/{bricks-project → bricks-ctor}/rules/data-calculation.md +3 -2
  11. package/skills/{bricks-project → bricks-ctor}/rules/local-sync.md +2 -2
  12. package/skills/{bricks-project → bricks-ctor}/rules/media-flow.md +3 -3
  13. package/skills/{bricks-project → bricks-ctor}/rules/remote-data-bank.md +6 -6
  14. package/skills/{bricks-project → bricks-ctor}/rules/standby-transition.md +1 -1
  15. package/skills/bricks-design/LICENSE.txt +180 -0
  16. package/skills/bricks-design/SKILL.md +66 -0
  17. package/tools/_git-author.ts +29 -0
  18. package/tools/_shell.ts +173 -0
  19. package/tools/deploy.ts +91 -23
  20. package/tools/icons/fa6pro-meta.json +3669 -26125
  21. package/tools/mcp-server.ts +11 -878
  22. package/tools/mcp-tools/compile.ts +92 -0
  23. package/tools/mcp-tools/huggingface.ts +762 -0
  24. package/tools/mcp-tools/icons.ts +70 -0
  25. package/tools/mcp-tools/lottie.ts +102 -0
  26. package/tools/mcp-tools/media.ts +110 -0
  27. package/tools/postinstall.ts +143 -40
  28. package/tools/preview-main.mjs +135 -2
  29. package/tools/preview.ts +68 -33
  30. package/tools/pull.ts +56 -32
  31. package/tsconfig.json +16 -0
  32. package/types/animation.ts +4 -0
  33. package/types/automation.ts +4 -1
  34. package/types/brick-base.ts +1 -1
  35. package/types/bricks/Camera.ts +47 -12
  36. package/types/bricks/Chart.ts +9 -3
  37. package/types/bricks/GenerativeMedia.ts +29 -13
  38. package/types/bricks/Icon.ts +8 -4
  39. package/types/bricks/Image.ts +9 -5
  40. package/types/bricks/Items.ts +28 -14
  41. package/types/bricks/Lottie.ts +14 -6
  42. package/types/bricks/Maps.ts +15 -7
  43. package/types/bricks/QrCode.ts +8 -4
  44. package/types/bricks/Rect.ts +44 -5
  45. package/types/bricks/RichText.ts +8 -4
  46. package/types/bricks/Rive.ts +20 -10
  47. package/types/bricks/Slideshow.ts +19 -9
  48. package/types/bricks/Svg.ts +7 -3
  49. package/types/bricks/Text.ts +8 -4
  50. package/types/bricks/TextInput.ts +22 -12
  51. package/types/bricks/Video.ts +10 -6
  52. package/types/bricks/VideoStreaming.ts +7 -3
  53. package/types/bricks/WebRtcStream.ts +6 -2
  54. package/types/bricks/WebView.ts +11 -7
  55. package/types/canvas.ts +2 -0
  56. package/types/common.ts +15 -8
  57. package/types/data-calc-command.ts +2 -0
  58. package/types/data-calc.ts +1 -0
  59. package/types/data.ts +2 -0
  60. package/types/generators/AlarmClock.ts +16 -10
  61. package/types/generators/Assistant.ts +68 -17
  62. package/types/generators/BleCentral.ts +30 -10
  63. package/types/generators/BlePeripheral.ts +10 -6
  64. package/types/generators/CanvasMap.ts +9 -5
  65. package/types/generators/CastlesPay.ts +14 -6
  66. package/types/generators/DataBank.ts +43 -8
  67. package/types/generators/File.ts +108 -29
  68. package/types/generators/GraphQl.ts +11 -5
  69. package/types/generators/Http.ts +32 -9
  70. package/types/generators/HttpServer.ts +22 -14
  71. package/types/generators/Information.ts +8 -4
  72. package/types/generators/Intent.ts +14 -4
  73. package/types/generators/Iterator.ts +14 -10
  74. package/types/generators/Keyboard.ts +26 -12
  75. package/types/generators/LlmAnthropicCompat.ts +32 -10
  76. package/types/generators/LlmAppleBuiltin.ts +24 -9
  77. package/types/generators/LlmGgml.ts +139 -30
  78. package/types/generators/LlmMediaTekNeuroPilot.ts +235 -0
  79. package/types/generators/LlmMlx.ts +227 -0
  80. package/types/generators/LlmOnnx.ts +33 -13
  81. package/types/generators/LlmOpenAiCompat.ts +46 -10
  82. package/types/generators/LlmQualcommAiEngine.ts +44 -12
  83. package/types/generators/Mcp.ts +374 -33
  84. package/types/generators/McpServer.ts +57 -18
  85. package/types/generators/MediaFlow.ts +37 -11
  86. package/types/generators/MqttBroker.ts +28 -10
  87. package/types/generators/MqttClient.ts +18 -8
  88. package/types/generators/Question.ts +12 -8
  89. package/types/generators/RealtimeTranscription.ts +107 -18
  90. package/types/generators/RerankerGgml.ts +42 -11
  91. package/types/generators/SerialPort.ts +17 -9
  92. package/types/generators/SoundPlayer.ts +9 -3
  93. package/types/generators/SoundRecorder.ts +23 -8
  94. package/types/generators/SpeechToTextGgml.ts +51 -17
  95. package/types/generators/SpeechToTextOnnx.ts +17 -10
  96. package/types/generators/SpeechToTextPlatform.ts +14 -6
  97. package/types/generators/SqLite.ts +19 -9
  98. package/types/generators/Step.ts +8 -4
  99. package/types/generators/SttAppleBuiltin.ts +21 -8
  100. package/types/generators/Tcp.ts +12 -8
  101. package/types/generators/TcpServer.ts +19 -13
  102. package/types/generators/TextToSpeechAppleBuiltin.ts +20 -7
  103. package/types/generators/TextToSpeechGgml.ts +28 -10
  104. package/types/generators/TextToSpeechOnnx.ts +18 -11
  105. package/types/generators/TextToSpeechOpenAiLike.ts +13 -7
  106. package/types/generators/ThermalPrinter.ts +12 -8
  107. package/types/generators/Tick.ts +10 -6
  108. package/types/generators/Udp.ts +16 -7
  109. package/types/generators/VadGgml.ts +50 -13
  110. package/types/generators/VadOnnx.ts +41 -11
  111. package/types/generators/VadTraditional.ts +27 -12
  112. package/types/generators/VectorStore.ts +32 -11
  113. package/types/generators/Watchdog.ts +18 -9
  114. package/types/generators/WebCrawler.ts +10 -6
  115. package/types/generators/WebRtc.ts +29 -15
  116. package/types/generators/WebSocket.ts +10 -6
  117. package/types/generators/index.ts +2 -0
  118. package/types/subspace.ts +4 -0
  119. package/types/system.ts +1 -1
  120. package/utils/event-props.ts +833 -1022
  121. package/api/index.ts +0 -1
  122. package/api/instance.ts +0 -213
  123. package/types/generators/TextToSpeechApple.ts +0 -113
  124. package/types/generators/TtsAppleBuiltin.ts +0 -105
  125. /package/skills/{bricks-project → bricks-ctor}/rules/automations.md +0 -0
@@ -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 { sh } from '../_shell'
4
+
5
+ const runBricks = async (projectDir: string, ...args: string[]) => {
6
+ try {
7
+ return await sh`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
+ }
@@ -1,8 +1,24 @@
1
- import { $ } from 'bun'
2
- import { stat, readFile, writeFile, readdir } from 'fs/promises'
1
+ import {
2
+ cp,
3
+ lstat,
4
+ mkdir,
5
+ readFile,
6
+ readdir,
7
+ readlink,
8
+ rm,
9
+ stat,
10
+ symlink,
11
+ writeFile,
12
+ } from 'fs/promises'
13
+ import * as path from 'path'
3
14
  import TOML from '@iarna/toml'
4
15
 
5
16
  const cwd = process.cwd()
17
+ const projectSkillsDir = path.join(cwd, '.bricks', 'skills')
18
+ const compatibilitySkillLinks = [
19
+ path.join(cwd, '.claude', 'skills'),
20
+ path.join(cwd, '.codex', 'skills'),
21
+ ]
6
22
 
7
23
  async function exists(f: string) {
8
24
  try {
@@ -13,26 +29,54 @@ async function exists(f: string) {
13
29
  }
14
30
  }
15
31
 
32
+ async function pathExists(f: string) {
33
+ try {
34
+ await lstat(f)
35
+ return true
36
+ } catch {
37
+ return false
38
+ }
39
+ }
40
+
41
+ // Migrate old projects: remove legacy project/ directory
42
+ const oldProjectDir = path.join(cwd, 'project')
43
+ if (await exists(oldProjectDir)) {
44
+ await rm(oldProjectDir, { recursive: true, force: true })
45
+ console.log('Removed legacy project/ directory')
46
+ }
47
+
16
48
  // handle flag --skip-copy
17
49
  const skipCopyProject = process.argv.includes('--skip-copy-project')
18
50
  if (skipCopyProject) {
19
- console.log('Skipping copy of files to project/')
51
+ console.log('Skipping copy of files to ctor/')
20
52
  } else {
21
53
  const libFiles = ['types', 'utils', 'index.ts']
22
54
 
23
- await $`mkdir -p ${cwd}/project`
24
- await Promise.all(libFiles.map((file) => $`cp -r ${__dirname}/../${file} ${cwd}/project`))
25
- console.log('Copied files to project/')
55
+ const ctorDir = path.join(cwd, 'ctor')
56
+ await mkdir(ctorDir, { recursive: true })
57
+ await Promise.all(
58
+ libFiles.map((file) =>
59
+ cp(path.join(import.meta.dirname, '..', file), path.join(ctorDir, file), {
60
+ recursive: true,
61
+ }),
62
+ ),
63
+ )
64
+ console.log('Copied files to ctor/')
26
65
  }
27
66
 
28
67
  const projectMcpServer = {
29
68
  command: 'bun',
30
- args: [`${cwd}/node_modules/@fugood/bricks-project/tools/mcp-server.ts`],
69
+ args: [`${cwd}/node_modules/@fugood/bricks-ctor/tools/mcp-server.ts`],
70
+ }
71
+
72
+ type CodexMcpConfig = {
73
+ mcp_servers: Record<string, typeof projectMcpServer>
31
74
  }
32
75
 
76
+ // Claude Code and AGENTS.md projects both use the shared project .mcp.json file.
33
77
  const defaultMcpConfig = {
34
78
  mcpServers: {
35
- 'bricks-project': projectMcpServer,
79
+ 'bricks-ctor': projectMcpServer,
36
80
  },
37
81
  }
38
82
 
@@ -43,7 +87,8 @@ const handleMcpConfigOverride = async (mcpConfigPath: string) => {
43
87
  try {
44
88
  mcpConfig = JSON.parse(configStr)
45
89
  if (!mcpConfig?.mcpServers) throw new Error('mcpServers is not defined')
46
- mcpConfig.mcpServers['bricks-project'] = projectMcpServer
90
+ mcpConfig.mcpServers['bricks-ctor'] = projectMcpServer
91
+ delete mcpConfig.mcpServers['bricks-project']
47
92
  } catch {
48
93
  mcpConfig = defaultMcpConfig
49
94
  }
@@ -60,57 +105,115 @@ const hasClaudeCode = await exists(`${cwd}/CLAUDE.md`)
60
105
  const hasAgentsMd = await exists(`${cwd}/AGENTS.md`)
61
106
 
62
107
  if (hasClaudeCode || hasAgentsMd) {
108
+ // Keep the workspace-level JSON MCP config aligned for tools that read .mcp.json.
63
109
  const mcpConfigPath = `${cwd}/.mcp.json`
64
110
  await handleMcpConfigOverride(mcpConfigPath)
65
111
  }
66
112
 
67
- const setupSkills = async (skillsDir) => {
68
- const packageSkillsDir = `${__dirname}/../skills`
113
+ const copyMissingSkills = async (sourceDir: string, targetDir: string) => {
114
+ if (!(await exists(sourceDir))) return
69
115
 
70
- if (await exists(packageSkillsDir)) {
71
- const packageSkills = await readdir(packageSkillsDir)
72
- const skillsToInstall = packageSkills.filter((skill) => !skill.startsWith('.'))
116
+ const packageSkills = await readdir(sourceDir, { withFileTypes: true })
117
+ const skillsToInstall = packageSkills.filter(
118
+ (entry) => entry.isDirectory() && !entry.name.startsWith('.'),
119
+ )
73
120
 
74
- await $`mkdir -p ${skillsDir}`
121
+ await mkdir(targetDir, { recursive: true })
75
122
 
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
- )
123
+ await Promise.all(
124
+ skillsToInstall.map(async (entry) => {
125
+ const targetSkillDir = path.join(targetDir, entry.name)
126
+ if (await exists(targetSkillDir)) {
127
+ console.log(`Skill '${entry.name}' already exists, skipping`)
128
+ } else {
129
+ await cp(path.join(sourceDir, entry.name), targetSkillDir, { recursive: true })
130
+ console.log(`Installed skill '${entry.name}' to ${targetDir}/`)
131
+ }
132
+ }),
133
+ )
134
+ }
135
+
136
+ const migrateSkillsDir = async (legacySkillsDir: string, canonicalSkillsDir: string) => {
137
+ if (!(await pathExists(legacySkillsDir))) return
138
+
139
+ const legacyStats = await lstat(legacySkillsDir)
140
+
141
+ if (legacyStats.isSymbolicLink()) {
142
+ const linkTarget = await readlink(legacySkillsDir)
143
+ const resolvedTarget = path.resolve(path.dirname(legacySkillsDir), linkTarget)
144
+ if (resolvedTarget === canonicalSkillsDir) return
145
+
146
+ await copyMissingSkills(resolvedTarget, canonicalSkillsDir)
147
+ await rm(legacySkillsDir, { force: true, recursive: true })
148
+ return
149
+ }
150
+
151
+ if (legacyStats.isDirectory()) {
152
+ await copyMissingSkills(legacySkillsDir, canonicalSkillsDir)
153
+ await rm(legacySkillsDir, { force: true, recursive: true })
154
+ return
87
155
  }
156
+
157
+ console.warn(`Skipping skills migration for ${legacySkillsDir}; expected a directory or symlink`)
88
158
  }
89
159
 
90
- if (hasClaudeCode) {
91
- // Install skills that don't already exist in the project
92
- await setupSkills(`${cwd}/.claude/skills`)
160
+ const ensureCompatibilitySkillLink = async (linkPath: string, targetDir: string) => {
161
+ await mkdir(path.dirname(linkPath), { recursive: true })
162
+
163
+ if (await pathExists(linkPath)) {
164
+ const linkStats = await lstat(linkPath)
165
+ if (linkStats.isSymbolicLink()) {
166
+ const linkTarget = await readlink(linkPath)
167
+ const resolvedTarget = path.resolve(path.dirname(linkPath), linkTarget)
168
+ if (resolvedTarget === targetDir) return
169
+ } else {
170
+ console.warn(
171
+ `Skipping skills symlink at ${linkPath}; path already exists and is not a symlink`,
172
+ )
173
+ return
174
+ }
175
+ }
176
+
177
+ const relativeTarget = path.relative(path.dirname(linkPath), targetDir)
178
+ const symlinkType = process.platform === 'win32' ? 'junction' : 'dir'
179
+ await symlink(relativeTarget, linkPath, symlinkType)
180
+ console.log(`Linked ${linkPath} -> ${relativeTarget}`)
93
181
  }
94
182
 
95
- if (hasAgentsMd) {
96
- // Handle codex skills
97
- // Currently no signal file for codex skills, so we just check if AGENTS.md exists
98
- await setupSkills(`${cwd}/.codex/skills`)
183
+ const setupSkills = async () => {
184
+ const packageSkillsDir = path.join(import.meta.dirname, '..', 'skills')
185
+ await mkdir(projectSkillsDir, { recursive: true })
186
+ await copyMissingSkills(packageSkillsDir, projectSkillsDir)
187
+
188
+ for (const linkPath of compatibilitySkillLinks) {
189
+ await migrateSkillsDir(linkPath, projectSkillsDir)
190
+ await ensureCompatibilitySkillLink(linkPath, projectSkillsDir)
191
+ }
192
+ }
99
193
 
194
+ if (hasClaudeCode || hasAgentsMd) {
195
+ // Install project skills once and expose them through compatibility symlinks.
196
+ await setupSkills()
197
+ }
198
+
199
+ if (hasAgentsMd) {
200
+ // Codex stores its project-local MCP config in .codex/config.toml.
100
201
  const defaultCodexMcpConfig = {
101
202
  mcp_servers: {
102
- 'bricks-project': projectMcpServer,
203
+ 'bricks-ctor': projectMcpServer,
103
204
  },
104
205
  }
105
206
 
106
207
  const handleCodexMcpConfigOverride = async (mcpConfigPath: string) => {
107
- let mcpConfig: { mcp_servers: Record<string, typeof projectMcpServer> } | null = null
208
+ let mcpConfig: CodexMcpConfig | null = null
108
209
  if (await exists(mcpConfigPath)) {
109
210
  const configStr = await readFile(mcpConfigPath, 'utf-8')
110
211
  try {
111
- mcpConfig = TOML.parse(configStr)
112
- if (!mcpConfig?.mcp_servers) throw new Error('mcp_servers is not defined')
113
- mcpConfig.mcp_servers['bricks-project'] = projectMcpServer
212
+ const parsed = TOML.parse(configStr) as Partial<CodexMcpConfig>
213
+ if (!parsed?.mcp_servers) throw new Error('mcp_servers is not defined')
214
+ mcpConfig = { mcp_servers: parsed.mcp_servers }
215
+ mcpConfig.mcp_servers['bricks-ctor'] = projectMcpServer
216
+ delete mcpConfig.mcp_servers['bricks-project']
114
217
  } catch {
115
218
  mcpConfig = defaultCodexMcpConfig
116
219
  }
@@ -118,12 +221,12 @@ if (hasAgentsMd) {
118
221
  mcpConfig = defaultCodexMcpConfig
119
222
  }
120
223
 
121
- await writeFile(mcpConfigPath, `${TOML.stringify(mcpConfig, null, 2)}\n`)
224
+ await writeFile(mcpConfigPath, `${TOML.stringify(mcpConfig)}\n`)
122
225
 
123
226
  console.log(`Updated ${mcpConfigPath}`)
124
227
  }
125
228
 
126
- // Setup MCP config (.codex/config.toml)
229
+ // Keep the Codex TOML MCP config aligned with the same bricks-ctor server entry.
127
230
  const codexConfigPath = `${cwd}/.codex/config.toml`
128
231
  await handleCodexMcpConfigOverride(codexConfigPath)
129
232
  }