@fugood/bricks-ctor 2.25.0-beta.45 → 2.25.0-beta.47
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/__tests__/config-diff.test.js +39 -0
- package/compile/__tests__/util.test.js +22 -0
- package/compile/config-diff.ts +64 -11
- package/compile/index.ts +3 -3
- package/compile/util.ts +16 -0
- package/package.json +3 -3
- package/skills/bricks-ctor/references/simulator.md +3 -2
- package/tools/mcp-tools/__tests__/huggingface.test.ts +49 -0
- package/tools/mcp-tools/__tests__/icons.test.ts +21 -0
- package/tools/mcp-tools/huggingface.ts +23 -13
- package/tools/mcp-tools/icons.ts +23 -7
|
@@ -54,6 +54,45 @@ describe('computeConfigChange', () => {
|
|
|
54
54
|
})
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
+
test('ignores object fields omitted by JSON.stringify', () => {
|
|
58
|
+
const before = { subspace_map: { S: { title: 'Home' } } }
|
|
59
|
+
const after = {
|
|
60
|
+
subspace_map: {
|
|
61
|
+
S: {
|
|
62
|
+
title: 'Home',
|
|
63
|
+
description: undefined,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
expect(computeConfigChange(before, after)).toEqual({ status: 'ok', ops: [], opCount: 0 })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('emits added values in JSON-compatible shape', () => {
|
|
71
|
+
const before = { m: {} }
|
|
72
|
+
const after = {
|
|
73
|
+
m: {
|
|
74
|
+
nested: {
|
|
75
|
+
keep: 1,
|
|
76
|
+
drop: undefined,
|
|
77
|
+
},
|
|
78
|
+
items: [undefined, 2],
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
const change = computeConfigChange(before, after)
|
|
82
|
+
expect(change.ops).toEqual([
|
|
83
|
+
{ op: 'set', path: ['m', 'nested'], value: { keep: 1 } },
|
|
84
|
+
{ op: 'set', path: ['m', 'items'], value: [null, 2] },
|
|
85
|
+
])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('compares array undefined entries like JSON nulls', () => {
|
|
89
|
+
expect(computeConfigChange({ items: [null] }, { items: [undefined] })).toEqual({
|
|
90
|
+
status: 'ok',
|
|
91
|
+
ops: [],
|
|
92
|
+
opCount: 0,
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
57
96
|
test('reports no_baseline / unavailable for missing sides', () => {
|
|
58
97
|
expect(computeConfigChange(null, { a: 1 })).toEqual({ status: 'no_baseline' })
|
|
59
98
|
expect(computeConfigChange({ a: 1 }, null)).toEqual({ status: 'unavailable' })
|
|
@@ -64,6 +64,28 @@ describe('validateConfig', () => {
|
|
|
64
64
|
test('does not throw when error/output are falsy and inputs are empty', () => {
|
|
65
65
|
expect(() => validateConfig(baseConfig())).not.toThrow()
|
|
66
66
|
})
|
|
67
|
+
|
|
68
|
+
test('throws when error and output target the same id', () => {
|
|
69
|
+
const config = baseConfig({ output: 'shared', error: 'shared' })
|
|
70
|
+
expect(() => validateConfig(config)).toThrow(/key: error\/output/)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('throws when output collides with an outputs target id', () => {
|
|
74
|
+
const config = baseConfig({ output: 'shared', outputs: { x: ['shared'] } })
|
|
75
|
+
expect(() => validateConfig(config)).toThrow(/key: output\/outputs/)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('throws when error collides with an outputs target id', () => {
|
|
79
|
+
const config = baseConfig({ error: 'shared', outputs: { x: ['shared'] } })
|
|
80
|
+
expect(() => validateConfig(config)).toThrow(/key: error\/outputs/)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// The same id reused across *different* outputs entries is a supported last-wins case
|
|
84
|
+
// (see generateCalulationMap test below) and must stay allowed.
|
|
85
|
+
test('allows the same id across multiple outputs entries', () => {
|
|
86
|
+
const config = baseConfig({ outputs: { first: ['pb1'], second: ['pb1'] } })
|
|
87
|
+
expect(() => validateConfig(config)).not.toThrow()
|
|
88
|
+
})
|
|
67
89
|
})
|
|
68
90
|
|
|
69
91
|
// generateCalulationMap now seeds command ids from the owning calc id; tests that don't
|
package/compile/config-diff.ts
CHANGED
|
@@ -27,17 +27,60 @@ export type ConfigChange =
|
|
|
27
27
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
28
28
|
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
29
29
|
|
|
30
|
+
const isJsonDroppedValue = (value: unknown) =>
|
|
31
|
+
value === undefined || typeof value === 'function' || typeof value === 'symbol'
|
|
32
|
+
|
|
33
|
+
const toJsonComparableScalar = (value: unknown) => {
|
|
34
|
+
if (typeof value === 'number' && !Number.isFinite(value)) return null
|
|
35
|
+
return value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const toJsonCompatibleValue = (value: unknown): unknown => {
|
|
39
|
+
if (isJsonDroppedValue(value)) return undefined
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return value.map((item) => (isJsonDroppedValue(item) ? null : toJsonCompatibleValue(item)))
|
|
42
|
+
}
|
|
43
|
+
if (isRecord(value)) {
|
|
44
|
+
return Object.entries(value).reduce((acc, [key, item]) => {
|
|
45
|
+
if (!isJsonDroppedValue(item)) acc[key] = toJsonCompatibleValue(item)
|
|
46
|
+
return acc
|
|
47
|
+
}, {})
|
|
48
|
+
}
|
|
49
|
+
return toJsonComparableScalar(value)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const hasJsonObjectKey = (value: Record<string, unknown>, key: string) =>
|
|
53
|
+
Object.prototype.hasOwnProperty.call(value, key) && !isJsonDroppedValue(value[key])
|
|
54
|
+
|
|
55
|
+
const getJsonArrayItem = (value: unknown[], index: number) => {
|
|
56
|
+
const item = value[index]
|
|
57
|
+
return isJsonDroppedValue(item) ? null : item
|
|
58
|
+
}
|
|
59
|
+
|
|
30
60
|
const deepEqual = (a: unknown, b: unknown): boolean => {
|
|
61
|
+
a = toJsonComparableScalar(a)
|
|
62
|
+
b = toJsonComparableScalar(b)
|
|
31
63
|
if (a === b) return true
|
|
32
64
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
33
|
-
|
|
65
|
+
if (a.length !== b.length) return false
|
|
66
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
67
|
+
if (!deepEqual(getJsonArrayItem(a, index), getJsonArrayItem(b, index))) return false
|
|
68
|
+
}
|
|
69
|
+
return true
|
|
34
70
|
}
|
|
35
71
|
if (isRecord(a) && isRecord(b)) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
72
|
+
let comparableAKeys = 0
|
|
73
|
+
for (const key of Object.keys(a)) {
|
|
74
|
+
if (isJsonDroppedValue(a[key])) continue
|
|
75
|
+
comparableAKeys += 1
|
|
76
|
+
if (!hasJsonObjectKey(b, key) || !deepEqual(a[key], b[key])) return false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let comparableBKeys = 0
|
|
80
|
+
for (const key of Object.keys(b)) {
|
|
81
|
+
if (!isJsonDroppedValue(b[key])) comparableBKeys += 1
|
|
82
|
+
}
|
|
83
|
+
return comparableAKeys === comparableBKeys
|
|
41
84
|
}
|
|
42
85
|
return false
|
|
43
86
|
}
|
|
@@ -64,22 +107,32 @@ const diffInto = (
|
|
|
64
107
|
if (isRecord(before) && isRecord(after)) {
|
|
65
108
|
const keys = new Set([...Object.keys(before), ...Object.keys(after)])
|
|
66
109
|
for (const key of keys) {
|
|
110
|
+
const beforeHasKey = hasJsonObjectKey(before, key)
|
|
111
|
+
const afterHasKey = hasJsonObjectKey(after, key)
|
|
112
|
+
if (!beforeHasKey && !afterHasKey) continue
|
|
113
|
+
|
|
67
114
|
const nextPath = [...currentPath, key]
|
|
68
|
-
if (!
|
|
69
|
-
else if (!
|
|
70
|
-
|
|
115
|
+
if (!afterHasKey) ops.push({ op: 'unset', path: nextPath })
|
|
116
|
+
else if (!beforeHasKey) {
|
|
117
|
+
ops.push({ op: 'set', path: nextPath, value: toJsonCompatibleValue(after[key]) })
|
|
118
|
+
} else diffInto(before[key], after[key], nextPath, ops)
|
|
71
119
|
}
|
|
72
120
|
return
|
|
73
121
|
}
|
|
74
122
|
|
|
75
123
|
if (Array.isArray(before) && Array.isArray(after) && before.length === after.length) {
|
|
76
124
|
for (let index = 0; index < after.length; index += 1) {
|
|
77
|
-
diffInto(
|
|
125
|
+
diffInto(
|
|
126
|
+
getJsonArrayItem(before, index),
|
|
127
|
+
getJsonArrayItem(after, index),
|
|
128
|
+
[...currentPath, index],
|
|
129
|
+
ops,
|
|
130
|
+
)
|
|
78
131
|
}
|
|
79
132
|
return
|
|
80
133
|
}
|
|
81
134
|
|
|
82
|
-
ops.push({ op: 'set', path: currentPath, value: after })
|
|
135
|
+
ops.push({ op: 'set', path: currentPath, value: toJsonCompatibleValue(after) })
|
|
83
136
|
}
|
|
84
137
|
|
|
85
138
|
// Diff two compiled configs. `before == null` means there was no prior build to compare
|
package/compile/index.ts
CHANGED
|
@@ -714,9 +714,9 @@ const compileAutomation = (automationMap: AutomationMap) =>
|
|
|
714
714
|
const recordConfigChange = async (previousConfig: unknown, config: unknown) => {
|
|
715
715
|
if (previousConfig == null) return
|
|
716
716
|
if (!isTruthyEnv(process.env.BRICKS_CTOR_ENABLE_EDITING_TOOLS)) return
|
|
717
|
-
// The baseline was parsed from JSON;
|
|
718
|
-
//
|
|
719
|
-
const change = computeConfigChange(previousConfig,
|
|
717
|
+
// The baseline was parsed from JSON; `computeConfigChange` applies the same
|
|
718
|
+
// JSON-omitted-field rules lazily so compile avoids cloning the full config.
|
|
719
|
+
const change = computeConfigChange(previousConfig, config)
|
|
720
720
|
if (change.status !== 'ok') return
|
|
721
721
|
await appendEditRecord(process.cwd(), {
|
|
722
722
|
ts: new Date().toISOString(),
|
package/compile/util.ts
CHANGED
|
@@ -27,6 +27,22 @@ export const validateConfig = (config: ScriptConfig) => {
|
|
|
27
27
|
if (Object.values(config.outputs).some((value) => value.some((id) => config.inputs[id]))) {
|
|
28
28
|
throw new Error(`${errorMsg}. key: outputs`)
|
|
29
29
|
}
|
|
30
|
+
// The same data-node id reused across the output-side targets (output / error / outputs)
|
|
31
|
+
// also collides: generateCalulationMap spreads their node objects last-wins, so the later
|
|
32
|
+
// one silently overwrites the earlier wiring (e.g. error == output drops the error change
|
|
33
|
+
// link). The checks above only compare against `inputs`, so guard the output-side pairs
|
|
34
|
+
// too. (The same id appearing in multiple `outputs` entries is a supported last-wins case
|
|
35
|
+
// and stays allowed.)
|
|
36
|
+
if (config.error && config.output && config.error === config.output) {
|
|
37
|
+
throw new Error(`${errorMsg}. key: error/output`)
|
|
38
|
+
}
|
|
39
|
+
const outputsIds = Object.values(config.outputs).flat()
|
|
40
|
+
if (config.output && outputsIds.includes(config.output)) {
|
|
41
|
+
throw new Error(`${errorMsg}. key: output/outputs`)
|
|
42
|
+
}
|
|
43
|
+
if (config.error && outputsIds.includes(config.error)) {
|
|
44
|
+
throw new Error(`${errorMsg}. key: error/outputs`)
|
|
45
|
+
}
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
const padding = 15
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fugood/bricks-ctor",
|
|
3
|
-
"version": "2.25.0-beta.
|
|
3
|
+
"version": "2.25.0-beta.47",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc --noEmit",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"@babel/parser": "7.28.5",
|
|
12
12
|
"@babel/traverse": "7.28.5",
|
|
13
13
|
"@babel/types": "7.28.5",
|
|
14
|
-
"@fugood/bricks-cli": "^2.25.0-beta.
|
|
14
|
+
"@fugood/bricks-cli": "^2.25.0-beta.46",
|
|
15
15
|
"@huggingface/gguf": "^0.3.2",
|
|
16
16
|
"@iarna/toml": "^3.0.0",
|
|
17
17
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"oxfmt": "^0.36.0"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "95f194a3fb6331c0f25a879e55675e3ff331a412"
|
|
33
33
|
}
|
|
@@ -46,7 +46,7 @@ These work, but with browser caveats:
|
|
|
46
46
|
| WebView / WebCrawler | Subject to browser CORS — a load/fetch that works on device may be blocked |
|
|
47
47
|
| On-device AI (LLM / STT / VAD / Vector Store / Reranker) | Runs **single-threaded** — far slower than the device, not representative of real latency. Also subject to the model fallbacks below |
|
|
48
48
|
| On-device database (SQLite — `GENERATOR_SQLITE`) | Runs for real on the in-memory WASM `sqlite-vec` build — `execute` / `query` / `transaction` / batch all work. `storageType: file` is transparently treated as in-memory, so nothing persists across reloads (see above) |
|
|
49
|
-
| Scene3D /
|
|
49
|
+
| Scene3D / Sketch / WebRTC | Supported |
|
|
50
50
|
|
|
51
51
|
Feature availability also varies across the device platforms themselves (iOS / tvOS / Android / the desktop OSes). When a deployment targets a specific platform's capability, confirm it on that platform.
|
|
52
52
|
|
|
@@ -89,6 +89,7 @@ So that camera and AI features are usable without device permissions, multi-giga
|
|
|
89
89
|
| Brick / Generator | In the Simulator | Does NOT prove |
|
|
90
90
|
|-------------------|------------------|----------------|
|
|
91
91
|
| Camera (`BRICK_CAMERA`) | A 3D mock canvas, no camera permission prompt. `takePicture` snapshots the canvas; recording produces a placeholder clip | Real camera feed, focus, recording, permission flow |
|
|
92
|
+
| Maps (`BRICK_MAPS`) | A real interactive map on free OpenStreetMap-based tiles — no Google Maps API key needed. Markers, path polyline, the six themes / map types (approximated with free tile sets + CSS tints), and the zoom / pan / navigate / focus / reset / fit actions all work | Google / Apple Maps rendering, exact `customMapStyle` / theme styling (approximated), traffic / buildings / indoors layers, real device geolocation |
|
|
92
93
|
| Thermal Printer (`GENERATOR_THERMAL_PRINTER`) | A simulated printer — `init` / `checkStatus` / `scan` fake per-driver status and discovered devices (ESC/POS, Star, TSC, Castles); `print` renders an approximate on-screen receipt. A bottom-left bubble shows live status with a fault toggle to exercise error wiring. Print results can be exported as PNG via `bricks-cli` (see below) | Real device connection, actual paper output, exact native driver status codes |
|
|
93
94
|
| LLM (`GENERATOR_LLM`) | Swapped to a tiny local stand-in model | Output quality / latency of your real model |
|
|
94
95
|
| Reranker — GGML (`GENERATOR_RERANKER`) | Swapped to a small local multilingual reranker model | Ranking quality / latency of your real model |
|
|
@@ -119,7 +120,7 @@ The PNG is the same approximate receipt the on-screen preview shows (rendered fr
|
|
|
119
120
|
|
|
120
121
|
### Running the real implementation instead
|
|
121
122
|
|
|
122
|
-
Each substituted brick/generator can be switched back to its real implementation per item: open the **gear (Simulator settings)** in the editor's preview toolbar, uncheck the item, and **Apply**. Apply persists the choice and reloads the preview so it takes effect (a plain refresh won't). Use this to, e.g., point a Vector Store at a real API key in the preview. The browser limits above still apply, and **Buttress stays disabled regardless** — there's no backend for it here.
|
|
123
|
+
Each substituted brick/generator can be switched back to its real implementation per item: open the **gear (Simulator settings)** in the editor's preview toolbar, uncheck the item, and **Apply**. Apply persists the choice and reloads the preview so it takes effect (a plain refresh won't). Use this to, e.g., point a Vector Store at a real API key in the preview, or render the real Google/Apple Maps brick (which needs a Maps API key on web). The browser limits above still apply, and **Buttress stays disabled regardless** — there's no backend for it here.
|
|
123
124
|
|
|
124
125
|
The Thermal Printer is the exception: it has no real web implementation to switch to (the native drivers can't run in a browser), so it is **always simulated** and is not in the gear list.
|
|
125
126
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { buildGGUFSplitFiles } from '../huggingface'
|
|
2
|
+
|
|
3
|
+
declare const describe: (name: string, fn: () => void) => void
|
|
4
|
+
declare const it: (name: string, fn: () => void) => void
|
|
5
|
+
declare const expect: (actual: unknown) => { toEqual: (expected: unknown) => void }
|
|
6
|
+
|
|
7
|
+
describe('buildGGUFSplitFiles', () => {
|
|
8
|
+
it('builds split GGUF files with indexed sibling metadata lookup', () => {
|
|
9
|
+
const siblings = [
|
|
10
|
+
{
|
|
11
|
+
rfilename: 'other-file.txt',
|
|
12
|
+
size: 999,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
rfilename: 'model-00001-of-00003.gguf',
|
|
16
|
+
size: 1,
|
|
17
|
+
lfs: { sha256: 'one' },
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
rfilename: 'model-00002-of-00003.gguf',
|
|
21
|
+
size: 2,
|
|
22
|
+
lfs: { sha256: 'two' },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
rfilename: 'model-00002-of-00003.gguf',
|
|
26
|
+
size: 22,
|
|
27
|
+
lfs: { sha256: 'two-late' },
|
|
28
|
+
},
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
expect(buildGGUFSplitFiles('model-00001-of-00003.gguf', '00003', siblings)).toEqual([
|
|
32
|
+
{
|
|
33
|
+
rfilename: 'model-00001-of-00003.gguf',
|
|
34
|
+
size: 1,
|
|
35
|
+
lfs: { sha256: 'one' },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
rfilename: 'model-00002-of-00003.gguf',
|
|
39
|
+
size: 2,
|
|
40
|
+
lfs: { sha256: 'two' },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
rfilename: 'model-00003-of-00003.gguf',
|
|
44
|
+
size: undefined,
|
|
45
|
+
lfs: undefined,
|
|
46
|
+
},
|
|
47
|
+
])
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { searchIcons } from '../icons'
|
|
2
|
+
|
|
3
|
+
declare const describe: (name: string, fn: () => void) => void
|
|
4
|
+
declare const test: (name: string, fn: () => void) => void
|
|
5
|
+
declare const expect: (actual: unknown) => {
|
|
6
|
+
toBe: (expected: unknown) => void
|
|
7
|
+
toHaveLength: (expected: number) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('icon_search helpers', () => {
|
|
11
|
+
test('searches the selected style corpus directly', () => {
|
|
12
|
+
const results = searchIcons('x', 10, 'brands')
|
|
13
|
+
|
|
14
|
+
expect(results).toHaveLength(10)
|
|
15
|
+
expect(results.every((result) => result.item.styles.includes('brands'))).toBe(true)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('keeps unstyled searches capped by limit', () => {
|
|
19
|
+
expect(searchIcons('arrow', 5)).toHaveLength(5)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -269,6 +269,28 @@ const fetchHFModelDetails = async (modelId: string): Promise<HFModel> => {
|
|
|
269
269
|
// Example: Mixtral-8x22B-v0.1.IQ3_XS-00001-of-00005.gguf
|
|
270
270
|
const ggufSplitPattern = /-(\d{5})-of-(\d{5})\.gguf$/
|
|
271
271
|
|
|
272
|
+
export const buildGGUFSplitFiles = (
|
|
273
|
+
filename: string,
|
|
274
|
+
splitTotal: string,
|
|
275
|
+
siblings: HFSibling[],
|
|
276
|
+
): HFSibling[] => {
|
|
277
|
+
const siblingByFilename = new Map<string, HFSibling>()
|
|
278
|
+
for (const sibling of siblings) {
|
|
279
|
+
if (!siblingByFilename.has(sibling.rfilename)) siblingByFilename.set(sibling.rfilename, sibling)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return Array.from({ length: Number(splitTotal) }, (_, i) => {
|
|
283
|
+
const split = String(i + 1).padStart(5, '0')
|
|
284
|
+
const splitRFilename = filename.replace(ggufSplitPattern, `-${split}-of-${splitTotal}.gguf`)
|
|
285
|
+
const sibling = siblingByFilename.get(splitRFilename)
|
|
286
|
+
return {
|
|
287
|
+
rfilename: splitRFilename,
|
|
288
|
+
size: sibling?.size,
|
|
289
|
+
lfs: sibling?.lfs,
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
272
294
|
export function register(server: McpServer) {
|
|
273
295
|
server.tool(
|
|
274
296
|
'huggingface_search',
|
|
@@ -657,19 +679,7 @@ export function register(server: McpServer) {
|
|
|
657
679
|
|
|
658
680
|
if (isSplit) {
|
|
659
681
|
const [, , splitTotal] = matched!
|
|
660
|
-
const splitFiles =
|
|
661
|
-
const split = String(i + 1).padStart(5, '0')
|
|
662
|
-
const splitRFilename = filename.replace(
|
|
663
|
-
ggufSplitPattern,
|
|
664
|
-
`-${split}-of-${splitTotal}.gguf`,
|
|
665
|
-
)
|
|
666
|
-
const sibling = siblings.find((sb) => sb.rfilename === splitRFilename)
|
|
667
|
-
return {
|
|
668
|
-
rfilename: splitRFilename,
|
|
669
|
-
size: sibling?.size,
|
|
670
|
-
lfs: sibling?.lfs,
|
|
671
|
-
}
|
|
672
|
-
})
|
|
682
|
+
const splitFiles = buildGGUFSplitFiles(filename, splitTotal, siblings)
|
|
673
683
|
|
|
674
684
|
const first = splitFiles[0]
|
|
675
685
|
const rest = splitFiles.slice(1)
|
package/tools/mcp-tools/icons.ts
CHANGED
|
@@ -36,11 +36,31 @@ const iconList = Object.entries(glyphmap as Record<string, number>).map(([name,
|
|
|
36
36
|
return { name, code, styles }
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
const
|
|
39
|
+
const fuseOptions = {
|
|
40
40
|
keys: ['name'],
|
|
41
41
|
threshold: 0.3,
|
|
42
42
|
includeScore: true,
|
|
43
|
-
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const iconFuse = new Fuse(iconList, fuseOptions)
|
|
46
|
+
const iconFuseByStyle = new Map<IconStyle, Fuse<(typeof iconList)[number]>>()
|
|
47
|
+
|
|
48
|
+
function getStyleFuse(style: IconStyle) {
|
|
49
|
+
let fuse = iconFuseByStyle.get(style)
|
|
50
|
+
if (!fuse) {
|
|
51
|
+
fuse = new Fuse(
|
|
52
|
+
iconList.filter((icon) => icon.styles.includes(style)),
|
|
53
|
+
fuseOptions,
|
|
54
|
+
)
|
|
55
|
+
iconFuseByStyle.set(style, fuse)
|
|
56
|
+
}
|
|
57
|
+
return fuse
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function searchIcons(query: string, limit: number, style?: IconStyle) {
|
|
61
|
+
if (style) return getStyleFuse(style).search(query, { limit })
|
|
62
|
+
return iconFuse.search(query, { limit })
|
|
63
|
+
}
|
|
44
64
|
|
|
45
65
|
export function register(server: McpServer) {
|
|
46
66
|
server.tool(
|
|
@@ -54,11 +74,7 @@ export function register(server: McpServer) {
|
|
|
54
74
|
.describe('Filter by icon style'),
|
|
55
75
|
},
|
|
56
76
|
async ({ query, limit, style }) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (style) {
|
|
60
|
-
results = results.filter((r) => r.item.styles.includes(style)).slice(0, limit)
|
|
61
|
-
}
|
|
77
|
+
const results = searchIcons(query, limit, style)
|
|
62
78
|
|
|
63
79
|
const icons = results.map((r) => ({
|
|
64
80
|
name: r.item.name,
|