@fugood/bricks-project 2.23.3 → 2.23.5
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 +14 -0
- package/compile/index.ts +29 -0
- package/package.json +3 -3
- package/skills/bricks-project/rules/architecture-patterns.md +7 -0
- package/skills/bricks-project/rules/buttress.md +9 -6
- package/tools/mcp-server.ts +10 -880
- package/tools/mcp-tools/compile.ts +91 -0
- package/tools/mcp-tools/huggingface.ts +763 -0
- package/tools/mcp-tools/icons.ts +60 -0
- package/tools/mcp-tools/lottie.ts +102 -0
- package/tools/mcp-tools/media.ts +110 -0
- package/tools/postinstall.ts +113 -28
- package/tools/pull.ts +25 -9
- package/types/animation.ts +4 -0
- package/types/automation.ts +3 -0
- package/types/bricks/Camera.ts +33 -6
- package/types/bricks/GenerativeMedia.ts +5 -5
- package/types/bricks/Icon.ts +2 -2
- package/types/bricks/Image.ts +3 -3
- package/types/bricks/Items.ts +6 -6
- package/types/bricks/Lottie.ts +3 -3
- package/types/bricks/Maps.ts +3 -3
- package/types/bricks/QrCode.ts +3 -3
- package/types/bricks/Rect.ts +3 -3
- package/types/bricks/RichText.ts +2 -2
- package/types/bricks/Slideshow.ts +1 -1
- package/types/bricks/Svg.ts +2 -2
- package/types/bricks/Text.ts +3 -3
- package/types/bricks/TextInput.ts +10 -6
- package/types/bricks/Video.ts +3 -3
- package/types/bricks/VideoStreaming.ts +2 -2
- package/types/bricks/WebView.ts +3 -3
- package/types/canvas.ts +2 -0
- package/types/common.ts +5 -0
- 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 +4 -4
- package/types/generators/Assistant.ts +53 -8
- package/types/generators/BleCentral.ts +11 -3
- package/types/generators/BlePeripheral.ts +3 -3
- package/types/generators/CanvasMap.ts +3 -3
- package/types/generators/CastlesPay.ts +2 -2
- package/types/generators/DataBank.ts +29 -2
- package/types/generators/File.ts +62 -13
- package/types/generators/GraphQl.ts +2 -2
- package/types/generators/Http.ts +25 -6
- package/types/generators/HttpServer.ts +4 -4
- package/types/generators/Information.ts +1 -1
- package/types/generators/Intent.ts +7 -1
- package/types/generators/Iterator.ts +5 -5
- package/types/generators/Keyboard.ts +15 -5
- package/types/generators/LlmAnthropicCompat.ts +9 -3
- package/types/generators/LlmAppleBuiltin.ts +4 -4
- package/types/generators/LlmGgml.ts +63 -13
- package/types/generators/LlmMlx.ts +210 -0
- package/types/generators/LlmOnnx.ts +13 -4
- package/types/generators/LlmOpenAiCompat.ts +19 -3
- package/types/generators/LlmQualcommAiEngine.ts +29 -5
- package/types/generators/Mcp.ts +331 -16
- package/types/generators/McpServer.ts +34 -7
- package/types/generators/MediaFlow.ts +24 -6
- package/types/generators/MqttBroker.ts +9 -3
- package/types/generators/MqttClient.ts +10 -4
- package/types/generators/Question.ts +4 -4
- package/types/generators/RealtimeTranscription.ts +69 -10
- package/types/generators/RerankerGgml.ts +19 -5
- package/types/generators/SerialPort.ts +5 -5
- package/types/generators/SoundPlayer.ts +1 -1
- package/types/generators/SoundRecorder.ts +4 -4
- package/types/generators/SpeechToTextGgml.ts +27 -7
- package/types/generators/SpeechToTextOnnx.ts +3 -3
- package/types/generators/SpeechToTextPlatform.ts +3 -3
- package/types/generators/SqLite.ts +9 -5
- package/types/generators/Step.ts +2 -2
- package/types/generators/SttAppleBuiltin.ts +4 -4
- package/types/generators/Tcp.ts +3 -3
- package/types/generators/TcpServer.ts +5 -5
- package/types/generators/TextToSpeechAppleBuiltin.ts +3 -3
- package/types/generators/TextToSpeechGgml.ts +3 -3
- package/types/generators/TextToSpeechOnnx.ts +3 -3
- package/types/generators/TextToSpeechOpenAiLike.ts +3 -3
- package/types/generators/ThermalPrinter.ts +4 -4
- package/types/generators/Tick.ts +2 -2
- package/types/generators/Udp.ts +8 -3
- package/types/generators/VadGgml.ts +34 -5
- package/types/generators/VadOnnx.ts +27 -4
- package/types/generators/VadTraditional.ts +13 -7
- package/types/generators/VectorStore.ts +22 -5
- package/types/generators/Watchdog.ts +10 -5
- package/types/generators/WebCrawler.ts +3 -3
- package/types/generators/WebRtc.ts +14 -8
- package/types/generators/WebSocket.ts +4 -4
- package/types/generators/index.ts +1 -0
- package/types/subspace.ts +1 -0
- package/types/system.ts +1 -1
- package/utils/event-props.ts +104 -87
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
const iconMeta = glyphmapMeta as Record<IconStyle, string[]>
|
|
10
|
+
|
|
11
|
+
const iconList = Object.entries(glyphmap as Record<string, number>).map(([name, code]) => {
|
|
12
|
+
const styles = (Object.keys(iconMeta) as IconStyle[]).filter((style) =>
|
|
13
|
+
iconMeta[style].includes(name),
|
|
14
|
+
)
|
|
15
|
+
return { name, code, styles }
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const iconFuse = new Fuse(iconList, {
|
|
19
|
+
keys: ['name'],
|
|
20
|
+
threshold: 0.3,
|
|
21
|
+
includeScore: true,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export function register(server: McpServer) {
|
|
25
|
+
server.tool(
|
|
26
|
+
'icon_search',
|
|
27
|
+
{
|
|
28
|
+
query: z.string().describe('Search keywords for FontAwesome 6 Pro icons'),
|
|
29
|
+
limit: z.number().min(1).max(100).optional().default(10),
|
|
30
|
+
style: z
|
|
31
|
+
.enum(['brands', 'duotone', 'light', 'regular', 'solid', 'thin'])
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Filter by icon style'),
|
|
34
|
+
},
|
|
35
|
+
async ({ query, limit, style }) => {
|
|
36
|
+
let results = iconFuse.search(query, { limit: style ? limit * 3 : limit })
|
|
37
|
+
|
|
38
|
+
if (style) {
|
|
39
|
+
results = results.filter((r) => r.item.styles.includes(style)).slice(0, limit)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const icons = results.map((r) => ({
|
|
43
|
+
name: r.item.name,
|
|
44
|
+
code: r.item.code,
|
|
45
|
+
unicode: `U+${r.item.code.toString(16).toUpperCase()}`,
|
|
46
|
+
styles: r.item.styles,
|
|
47
|
+
score: r.score,
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: 'text',
|
|
54
|
+
text: TOON.encode({ count: icons.length, icons }),
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -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,6 +30,15 @@ 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
|
+
|
|
16
42
|
// handle flag --skip-copy
|
|
17
43
|
const skipCopyProject = process.argv.includes('--skip-copy-project')
|
|
18
44
|
if (skipCopyProject) {
|
|
@@ -34,6 +60,7 @@ type CodexMcpConfig = {
|
|
|
34
60
|
mcp_servers: Record<string, typeof projectMcpServer>
|
|
35
61
|
}
|
|
36
62
|
|
|
63
|
+
// Claude Code and AGENTS.md projects both use the shared project .mcp.json file.
|
|
37
64
|
const defaultMcpConfig = {
|
|
38
65
|
mcpServers: {
|
|
39
66
|
'bricks-project': projectMcpServer,
|
|
@@ -64,43 +91,101 @@ const hasClaudeCode = await exists(`${cwd}/CLAUDE.md`)
|
|
|
64
91
|
const hasAgentsMd = await exists(`${cwd}/AGENTS.md`)
|
|
65
92
|
|
|
66
93
|
if (hasClaudeCode || hasAgentsMd) {
|
|
94
|
+
// Keep the workspace-level JSON MCP config aligned for tools that read .mcp.json.
|
|
67
95
|
const mcpConfigPath = `${cwd}/.mcp.json`
|
|
68
96
|
await handleMcpConfigOverride(mcpConfigPath)
|
|
69
97
|
}
|
|
70
98
|
|
|
71
|
-
const
|
|
72
|
-
|
|
99
|
+
const copyMissingSkills = async (sourceDir: string, targetDir: string) => {
|
|
100
|
+
if (!(await exists(sourceDir))) return
|
|
101
|
+
|
|
102
|
+
const packageSkills = await readdir(sourceDir, { withFileTypes: true })
|
|
103
|
+
const skillsToInstall = packageSkills.filter(
|
|
104
|
+
(entry) => entry.isDirectory() && !entry.name.startsWith('.'),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
await mkdir(targetDir, { recursive: true })
|
|
108
|
+
|
|
109
|
+
await Promise.all(
|
|
110
|
+
skillsToInstall.map(async (entry) => {
|
|
111
|
+
const targetSkillDir = path.join(targetDir, entry.name)
|
|
112
|
+
if (await exists(targetSkillDir)) {
|
|
113
|
+
console.log(`Skill '${entry.name}' already exists, skipping`)
|
|
114
|
+
} else {
|
|
115
|
+
await cp(path.join(sourceDir, entry.name), targetSkillDir, { recursive: true })
|
|
116
|
+
console.log(`Installed skill '${entry.name}' to ${targetDir}/`)
|
|
117
|
+
}
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const migrateSkillsDir = async (legacySkillsDir: string, canonicalSkillsDir: string) => {
|
|
123
|
+
if (!(await pathExists(legacySkillsDir))) return
|
|
124
|
+
|
|
125
|
+
const legacyStats = await lstat(legacySkillsDir)
|
|
126
|
+
|
|
127
|
+
if (legacyStats.isSymbolicLink()) {
|
|
128
|
+
const linkTarget = await readlink(legacySkillsDir)
|
|
129
|
+
const resolvedTarget = path.resolve(path.dirname(legacySkillsDir), linkTarget)
|
|
130
|
+
if (resolvedTarget === canonicalSkillsDir) return
|
|
73
131
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await $`mkdir -p ${skillsDir}`
|
|
79
|
-
|
|
80
|
-
await Promise.all(
|
|
81
|
-
skillsToInstall.map(async (skill) => {
|
|
82
|
-
const targetSkillDir = `${skillsDir}/${skill}`
|
|
83
|
-
if (await exists(targetSkillDir)) {
|
|
84
|
-
console.log(`Skill '${skill}' already exists, skipping`)
|
|
85
|
-
} else {
|
|
86
|
-
await $`cp -r ${packageSkillsDir}/${skill} ${targetSkillDir}`
|
|
87
|
-
console.log(`Installed skill '${skill}' to ${skillsDir}/`)
|
|
88
|
-
}
|
|
89
|
-
}),
|
|
90
|
-
)
|
|
132
|
+
await copyMissingSkills(resolvedTarget, canonicalSkillsDir)
|
|
133
|
+
await rm(legacySkillsDir, { force: true, recursive: true })
|
|
134
|
+
return
|
|
91
135
|
}
|
|
136
|
+
|
|
137
|
+
if (legacyStats.isDirectory()) {
|
|
138
|
+
await copyMissingSkills(legacySkillsDir, canonicalSkillsDir)
|
|
139
|
+
await rm(legacySkillsDir, { force: true, recursive: true })
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.warn(`Skipping skills migration for ${legacySkillsDir}; expected a directory or symlink`)
|
|
92
144
|
}
|
|
93
145
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
146
|
+
const ensureCompatibilitySkillLink = async (linkPath: string, targetDir: string) => {
|
|
147
|
+
await mkdir(path.dirname(linkPath), { recursive: true })
|
|
148
|
+
|
|
149
|
+
if (await pathExists(linkPath)) {
|
|
150
|
+
const linkStats = await lstat(linkPath)
|
|
151
|
+
if (linkStats.isSymbolicLink()) {
|
|
152
|
+
const linkTarget = await readlink(linkPath)
|
|
153
|
+
const resolvedTarget = path.resolve(path.dirname(linkPath), linkTarget)
|
|
154
|
+
if (resolvedTarget === targetDir) return
|
|
155
|
+
} else {
|
|
156
|
+
console.warn(
|
|
157
|
+
`Skipping skills symlink at ${linkPath}; path already exists and is not a symlink`,
|
|
158
|
+
)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const relativeTarget = path.relative(path.dirname(linkPath), targetDir)
|
|
164
|
+
const symlinkType = process.platform === 'win32' ? 'junction' : 'dir'
|
|
165
|
+
await symlink(relativeTarget, linkPath, symlinkType)
|
|
166
|
+
console.log(`Linked ${linkPath} -> ${relativeTarget}`)
|
|
97
167
|
}
|
|
98
168
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
await
|
|
169
|
+
const setupSkills = async () => {
|
|
170
|
+
const packageSkillsDir = `${__dirname}/../skills`
|
|
171
|
+
await mkdir(projectSkillsDir, { recursive: true })
|
|
172
|
+
await copyMissingSkills(packageSkillsDir, projectSkillsDir)
|
|
173
|
+
|
|
174
|
+
await Promise.all(
|
|
175
|
+
compatibilitySkillLinks.map(async (linkPath) => {
|
|
176
|
+
await migrateSkillsDir(linkPath, projectSkillsDir)
|
|
177
|
+
await ensureCompatibilitySkillLink(linkPath, projectSkillsDir)
|
|
178
|
+
}),
|
|
179
|
+
)
|
|
180
|
+
}
|
|
103
181
|
|
|
182
|
+
if (hasClaudeCode || hasAgentsMd) {
|
|
183
|
+
// Install project skills once and expose them through compatibility symlinks.
|
|
184
|
+
await setupSkills()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (hasAgentsMd) {
|
|
188
|
+
// Codex stores its project-local MCP config in .codex/config.toml.
|
|
104
189
|
const defaultCodexMcpConfig = {
|
|
105
190
|
mcp_servers: {
|
|
106
191
|
'bricks-project': projectMcpServer,
|
|
@@ -128,7 +213,7 @@ if (hasAgentsMd) {
|
|
|
128
213
|
console.log(`Updated ${mcpConfigPath}`)
|
|
129
214
|
}
|
|
130
215
|
|
|
131
|
-
//
|
|
216
|
+
// Keep the Codex TOML MCP config aligned with the same bricks-project server entry.
|
|
132
217
|
const codexConfigPath = `${cwd}/.codex/config.toml`
|
|
133
218
|
await handleCodexMcpConfigOverride(codexConfigPath)
|
|
134
219
|
}
|
package/tools/pull.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { $ } from 'bun'
|
|
|
2
2
|
import { format } from 'prettier'
|
|
3
3
|
|
|
4
4
|
const cwd = process.cwd()
|
|
5
|
+
const args = process.argv.slice(2)
|
|
6
|
+
const force = args.includes('--force') || args.includes('-f')
|
|
5
7
|
|
|
6
8
|
// Check git status
|
|
7
9
|
const { exitCode } = await $`cd ${cwd} && git status`.nothrow()
|
|
@@ -9,8 +11,15 @@ const isGitRepo = exitCode === 0
|
|
|
9
11
|
|
|
10
12
|
if (isGitRepo) {
|
|
11
13
|
const unstagedChanges = await $`cd ${cwd} && git diff --name-only --diff-filter=ACMR`.text()
|
|
12
|
-
if (unstagedChanges)
|
|
13
|
-
|
|
14
|
+
if (unstagedChanges) {
|
|
15
|
+
if (force) {
|
|
16
|
+
console.log('Force mode: committing unstaged changes before pull...')
|
|
17
|
+
await $`cd ${cwd} && git add .`
|
|
18
|
+
await $`cd ${cwd} && git commit -m ${'chore(force-pull): saved unstaged changes before pull'}`
|
|
19
|
+
} else {
|
|
20
|
+
throw new Error('Unstaged changes found, please commit or stash your changes before pulling')
|
|
21
|
+
}
|
|
22
|
+
}
|
|
14
23
|
} else {
|
|
15
24
|
const confirmContinue = prompt(
|
|
16
25
|
'No git repository found, so it will not be safe to pull, continue? (y/n)',
|
|
@@ -25,6 +34,7 @@ const isModule = app.type === 'module'
|
|
|
25
34
|
const command = isModule ? 'module' : 'app'
|
|
26
35
|
|
|
27
36
|
// Fetch project files using CLI
|
|
37
|
+
console.log(`Pulling ${command} project (${app.id})...`)
|
|
28
38
|
const result = await $`bricks ${command} project-pull ${app.id} --json`.quiet().nothrow()
|
|
29
39
|
|
|
30
40
|
if (result.exitCode !== 0) {
|
|
@@ -40,7 +50,8 @@ if (result.exitCode !== 0) {
|
|
|
40
50
|
const { files, lastCommitId } = JSON.parse(result.stdout.toString())
|
|
41
51
|
|
|
42
52
|
let useMain = false
|
|
43
|
-
if (isGitRepo) {
|
|
53
|
+
if (isGitRepo && !force) {
|
|
54
|
+
console.log(`Checking commit ${lastCommitId}...`)
|
|
44
55
|
const found = (await $`cd ${cwd} && git rev-list -1 ${lastCommitId}`.nothrow().text())
|
|
45
56
|
.trim()
|
|
46
57
|
.match(/^[\da-f]{40}$/)
|
|
@@ -86,13 +97,18 @@ await Promise.all(
|
|
|
86
97
|
|
|
87
98
|
if (isGitRepo) {
|
|
88
99
|
await $`cd ${cwd} && git add .`
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
const hasChanges = !!(await $`cd ${cwd} && git diff --cached --name-only`.text()).trim()
|
|
101
|
+
if (hasChanges) {
|
|
102
|
+
let commitMsg = 'chore(project): apply file changes from BRICKS application'
|
|
103
|
+
if (force) commitMsg = `chore(force-pull): apply force pull-${command}`
|
|
104
|
+
else if (isModule) commitMsg = 'chore(project): apply file changes from BRICKS module'
|
|
105
|
+
await $`cd ${cwd} && git commit -m ${commitMsg}`
|
|
106
|
+
}
|
|
107
|
+
if (!force && !useMain) {
|
|
94
108
|
await $`cd ${cwd} && git merge main`
|
|
95
109
|
}
|
|
96
110
|
}
|
|
97
111
|
|
|
98
|
-
console.log(
|
|
112
|
+
console.log(
|
|
113
|
+
`${isModule ? 'Module' : 'App'} project pulled: ${files.length} files${force ? ' (force)' : ''}`,
|
|
114
|
+
)
|
package/types/animation.ts
CHANGED
|
@@ -61,8 +61,10 @@ export interface AnimationDecayConfig {
|
|
|
61
61
|
export interface AnimationDef {
|
|
62
62
|
__typename: 'Animation'
|
|
63
63
|
id: string
|
|
64
|
+
alias?: string
|
|
64
65
|
title: string
|
|
65
66
|
description?: string
|
|
67
|
+
hideShortRef?: boolean
|
|
66
68
|
runType?: 'once' | 'loop'
|
|
67
69
|
property:
|
|
68
70
|
| 'transform.translateX'
|
|
@@ -80,8 +82,10 @@ export interface AnimationDef {
|
|
|
80
82
|
export interface AnimationComposeDef {
|
|
81
83
|
__typename: 'AnimationCompose'
|
|
82
84
|
id: string
|
|
85
|
+
alias?: string
|
|
83
86
|
title: string
|
|
84
87
|
description?: string
|
|
88
|
+
hideShortRef?: boolean
|
|
85
89
|
runType?: 'once' | 'loop'
|
|
86
90
|
composeType: 'parallel' | 'sequence'
|
|
87
91
|
items: Array<() => Animation>
|
package/types/automation.ts
CHANGED
|
@@ -182,6 +182,7 @@ export interface TestCase {
|
|
|
182
182
|
__typename: 'TestCase'
|
|
183
183
|
id: string
|
|
184
184
|
name: string
|
|
185
|
+
hideShortRef?: boolean
|
|
185
186
|
run: TestMethodRun
|
|
186
187
|
exit_on_failed: boolean
|
|
187
188
|
commented: boolean
|
|
@@ -203,6 +204,7 @@ export interface AutomationTest {
|
|
|
203
204
|
__typename: 'AutomationTest'
|
|
204
205
|
id: string
|
|
205
206
|
title: string
|
|
207
|
+
hideShortRef?: boolean
|
|
206
208
|
timeout: number
|
|
207
209
|
trigger_type?: TestTriggerType
|
|
208
210
|
cron?: string // Cron expression when trigger_type is 'cron'
|
|
@@ -220,6 +222,7 @@ export interface AutomationTestMap {
|
|
|
220
222
|
__typename: 'AutomationTestMap'
|
|
221
223
|
id: string
|
|
222
224
|
title: string
|
|
225
|
+
hideShortRef?: boolean
|
|
223
226
|
createdAt: number
|
|
224
227
|
tests: AutomationTest[]
|
|
225
228
|
}
|
package/types/bricks/Camera.ts
CHANGED
|
@@ -167,15 +167,42 @@ Default property:
|
|
|
167
167
|
}
|
|
168
168
|
outlets?: {
|
|
169
169
|
/* Camera device and format information */
|
|
170
|
-
info?: () => Data
|
|
170
|
+
info?: () => Data<{ [key: string]: any }>
|
|
171
171
|
/* Picture taken result */
|
|
172
|
-
pictureTaken?: () => Data
|
|
172
|
+
pictureTaken?: () => Data<{
|
|
173
|
+
width?: number
|
|
174
|
+
height?: number
|
|
175
|
+
uri?: string
|
|
176
|
+
base64?: string
|
|
177
|
+
[key: string]: any
|
|
178
|
+
}>
|
|
173
179
|
/* Record video result */
|
|
174
|
-
recordVideo?: () => Data
|
|
180
|
+
recordVideo?: () => Data<{
|
|
181
|
+
uri?: string
|
|
182
|
+
[key: string]: any
|
|
183
|
+
}>
|
|
175
184
|
/* Barcode read result */
|
|
176
|
-
barcodeRead?: () => Data
|
|
185
|
+
barcodeRead?: () => Data<{
|
|
186
|
+
type?: string
|
|
187
|
+
data?: string
|
|
188
|
+
rawData?: string
|
|
189
|
+
bounds?: {
|
|
190
|
+
origin?: {
|
|
191
|
+
x?: number
|
|
192
|
+
y?: number
|
|
193
|
+
[key: string]: any
|
|
194
|
+
}
|
|
195
|
+
size?: {
|
|
196
|
+
width?: number
|
|
197
|
+
height?: number
|
|
198
|
+
[key: string]: any
|
|
199
|
+
}
|
|
200
|
+
[key: string]: any
|
|
201
|
+
}
|
|
202
|
+
[key: string]: any
|
|
203
|
+
}>
|
|
177
204
|
/* Faces detected result */
|
|
178
|
-
faceDetected?: () => Data
|
|
205
|
+
faceDetected?: () => Data<Array<{ [key: string]: any }>>
|
|
179
206
|
}
|
|
180
207
|
animation?: AnimationBasicEvents & {
|
|
181
208
|
stateChange?: Animation
|
|
@@ -188,7 +215,7 @@ Default property:
|
|
|
188
215
|
}
|
|
189
216
|
}
|
|
190
217
|
|
|
191
|
-
/* Camera view brick
|
|
218
|
+
/* Camera view brick */
|
|
192
219
|
export type BrickCamera = Brick &
|
|
193
220
|
BrickCameraDef & {
|
|
194
221
|
templateKey: 'BRICK_CAMERA'
|
|
@@ -209,15 +209,15 @@ Default property:
|
|
|
209
209
|
}
|
|
210
210
|
outlets?: {
|
|
211
211
|
/* Brick is pressing */
|
|
212
|
-
brickPressing?: () => Data
|
|
212
|
+
brickPressing?: () => Data<boolean>
|
|
213
213
|
/* Brick is focusing (Use TV Device with controller) */
|
|
214
|
-
brickFocusing?: () => Data
|
|
214
|
+
brickFocusing?: () => Data<boolean>
|
|
215
215
|
/* Generated media URL */
|
|
216
|
-
url?: () => Data
|
|
216
|
+
url?: () => Data<string>
|
|
217
217
|
/* Generated media error */
|
|
218
|
-
error?: () => Data
|
|
218
|
+
error?: () => Data<string>
|
|
219
219
|
/* Loading state */
|
|
220
|
-
loading?: () => Data
|
|
220
|
+
loading?: () => Data<boolean>
|
|
221
221
|
}
|
|
222
222
|
animation?: AnimationBasicEvents & {
|
|
223
223
|
generativeMediaOnPress?: Animation
|
package/types/bricks/Icon.ts
CHANGED
|
@@ -57,9 +57,9 @@ Default property:
|
|
|
57
57
|
}
|
|
58
58
|
outlets?: {
|
|
59
59
|
/* Brick is pressing */
|
|
60
|
-
brickPressing?: () => Data
|
|
60
|
+
brickPressing?: () => Data<boolean>
|
|
61
61
|
/* Brick is focusing (Use TV Device with controller) */
|
|
62
|
-
brickFocusing?: () => Data
|
|
62
|
+
brickFocusing?: () => Data<boolean>
|
|
63
63
|
}
|
|
64
64
|
animation?: AnimationBasicEvents & {
|
|
65
65
|
onPress?: Animation
|
package/types/bricks/Image.ts
CHANGED
|
@@ -71,9 +71,9 @@ Default property:
|
|
|
71
71
|
}
|
|
72
72
|
outlets?: {
|
|
73
73
|
/* Brick is pressing */
|
|
74
|
-
brickPressing?: () => Data
|
|
74
|
+
brickPressing?: () => Data<boolean>
|
|
75
75
|
/* Brick is focusing (Use TV Device with controller) */
|
|
76
|
-
brickFocusing?: () => Data
|
|
76
|
+
brickFocusing?: () => Data<boolean>
|
|
77
77
|
}
|
|
78
78
|
animation?: AnimationBasicEvents & {
|
|
79
79
|
onPress?: Animation
|
|
@@ -87,7 +87,7 @@ Default property:
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/* Image brick
|
|
90
|
+
/* Image brick */
|
|
91
91
|
export type BrickImage = Brick &
|
|
92
92
|
BrickImageDef & {
|
|
93
93
|
templateKey: 'BRICK_IMAGE'
|