@elizaos/app-core 2.0.0-beta.1 → 2.0.0-beta.2
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/package.json +2 -2
- package/platforms/electrobun/native/macos/window-effects.mm +103 -0
- package/platforms/electrobun/package.json +9 -0
- package/platforms/electrobun/src/__stubs__/bun-ffi.ts +16 -0
- package/platforms/electrobun/src/libMacWindowEffects.dylib +0 -0
- package/platforms/electrobun/src/native/agent.ts +74 -3
- package/platforms/electrobun/src/native/desktop.ts +39 -6
- package/platforms/electrobun/src/native/mac-window-effects.ts +61 -1
- package/platforms/electrobun/src/native/permissions-shared.ts +3 -2
- package/platforms/electrobun/src/native/permissions.ts +11 -6
- package/platforms/electrobun/src/rpc-handlers.ts +7 -0
- package/platforms/electrobun/src/rpc-schema.ts +39 -4
- package/platforms/electrobun/src/runtime-permissions.ts +7 -1
- package/runtime/ensure-local-inference-handler.d.ts +1 -0
- package/runtime/ensure-local-inference-handler.d.ts.map +1 -1
- package/runtime/ensure-local-inference-handler.js +9 -0
- package/runtime/mode/remote-forwarder.d.ts.map +1 -1
- package/runtime/mode/remote-forwarder.js +1 -1
- package/runtime/mode/runtime-mode.d.ts +20 -2
- package/runtime/mode/runtime-mode.d.ts.map +1 -1
- package/runtime/mode/runtime-mode.js +69 -1
- package/scripts/aosp/stage-default-models.mjs +2 -2
- package/scripts/build-llama-cpp-dflash.mjs +75 -40
- package/scripts/kernel-patches/metal-kernels.mjs +357 -337
- package/scripts/lib/read-app-identity.mjs +5 -1
- package/services/local-inference/catalog.d.ts +2 -1
- package/services/local-inference/catalog.d.ts.map +1 -1
- package/services/local-inference/catalog.js +131 -12
- package/services/local-inference/downloader.d.ts +2 -0
- package/services/local-inference/downloader.d.ts.map +1 -1
- package/services/local-inference/downloader.js +300 -1
- package/services/local-inference/manifest/validator.d.ts.map +1 -1
- package/services/local-inference/manifest/validator.js +48 -0
- package/services/local-inference/providers.d.ts +1 -1
- package/services/local-inference/providers.js +6 -6
- package/services/local-inference/registry.d.ts.map +1 -1
- package/services/local-inference/registry.js +10 -1
- package/services/local-inference/types.d.ts +6 -0
- package/services/local-inference/types.d.ts.map +1 -1
- package/test/helpers/real-runtime.ts +21 -20
- package/platforms/electrobun/src/native/permissions-darwin.ts +0 -342
- package/platforms/electrobun/src/native/permissions-linux.ts +0 -34
- package/platforms/electrobun/src/native/permissions-win32.ts +0 -56
|
@@ -6,7 +6,8 @@ import path from "node:path";
|
|
|
6
6
|
* via regex (no TS evaluation, so callers stay bun-import-free).
|
|
7
7
|
*
|
|
8
8
|
* Used by desktop and mobile build scripts to forward identity into
|
|
9
|
-
* downstream env vars (`ELIZA_APP_NAME`, `ELIZA_APP_ID`, `ELIZA_URL_SCHEME
|
|
9
|
+
* downstream env vars (`ELIZA_APP_NAME`, `ELIZA_APP_ID`, `ELIZA_URL_SCHEME`,
|
|
10
|
+
* `ELIZA_NAMESPACE`).
|
|
10
11
|
*/
|
|
11
12
|
export function readAppIdentity(appDir) {
|
|
12
13
|
const cfgPath = path.join(appDir, "app.config.ts");
|
|
@@ -23,6 +24,7 @@ export function readAppIdentity(appDir) {
|
|
|
23
24
|
/desktop\s*:\s*\{[\s\S]*?urlScheme\s*:\s*["']([^"']+)["']/,
|
|
24
25
|
)?.[1];
|
|
25
26
|
const topLevelUrlScheme = src.match(/urlScheme:\s*["']([^"']+)["']/)?.[1];
|
|
27
|
+
const namespace = src.match(/namespace:\s*["']([^"']+)["']/)?.[1];
|
|
26
28
|
if (!appId || !appName) {
|
|
27
29
|
throw new Error(
|
|
28
30
|
`Could not parse appId/appName from ${cfgPath} (regex failed)`,
|
|
@@ -32,6 +34,7 @@ export function readAppIdentity(appDir) {
|
|
|
32
34
|
appId: desktopBundleId ?? appId,
|
|
33
35
|
appName,
|
|
34
36
|
urlScheme: desktopUrlScheme ?? topLevelUrlScheme ?? appId,
|
|
37
|
+
namespace: namespace ?? "eliza",
|
|
35
38
|
};
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -46,5 +49,6 @@ export function appIdentityEnv(appDir, existing = process.env) {
|
|
|
46
49
|
ELIZA_APP_NAME: existing.ELIZA_APP_NAME?.trim() || identity.appName,
|
|
47
50
|
ELIZA_APP_ID: existing.ELIZA_APP_ID?.trim() || identity.appId,
|
|
48
51
|
ELIZA_URL_SCHEME: existing.ELIZA_URL_SCHEME?.trim() || identity.urlScheme,
|
|
52
|
+
ELIZA_NAMESPACE: existing.ELIZA_NAMESPACE?.trim() || identity.namespace,
|
|
49
53
|
};
|
|
50
54
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* default per device tier (`lite-0_6b`, `mobile-1_7b`, `desktop-9b`,
|
|
6
6
|
* `pro-27b`, `server-h200`). The recommendation engine picks one of
|
|
7
7
|
* these tiers based on hardware. See
|
|
8
|
-
* `/Users/shawwalters/eliza-workspace/milady/packages/inference/AGENTS.md`
|
|
8
|
+
* `/Users/shawwalters/eliza-workspace/milady/eliza/packages/inference/AGENTS.md`
|
|
9
9
|
* §2 for the binding tier matrix.
|
|
10
10
|
*
|
|
11
11
|
* HF-search results from outside `elizalabs/eliza-1-*` MUST never be
|
|
@@ -52,5 +52,6 @@ export declare function findCatalogModel(id: string): CatalogModel | undefined;
|
|
|
52
52
|
* downloader e2e test suite can redirect all downloads without touching
|
|
53
53
|
* the catalog.
|
|
54
54
|
*/
|
|
55
|
+
export declare function buildHuggingFaceResolveUrlForPath(model: CatalogModel, filePath: string): string;
|
|
55
56
|
export declare function buildHuggingFaceResolveUrl(model: CatalogModel): string;
|
|
56
57
|
//# sourceMappingURL=catalog.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../../../../../src/services/local-inference/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../../../../../src/services/local-inference/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAsB,MAAM,SAAS,CAAC;AAEhE;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,WAAW,UAAU,EAAE,CAAC;AAEnD,eAAO,MAAM,gBAAgB,EAAE,aAAa,CAAC,YAAY,CAExD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,EAAE,YAAoC,CAAC;AAE9E;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,CAE1D,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED,2EAA2E;AAC3E,eAAO,MAAM,uBAAuB,EAAE,WAAW,CAAC,MAAM,CAEvD,CAAC;AA4EF,eAAO,MAAM,aAAa,EAAE,YAAY,EAsJvC,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAErE;AAED;;;;;;GAMG;AACH,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,YAAY,EACnB,QAAQ,EAAE,MAAM,GACf,MAAM,CAWR;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAEtE"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* default per device tier (`lite-0_6b`, `mobile-1_7b`, `desktop-9b`,
|
|
6
6
|
* `pro-27b`, `server-h200`). The recommendation engine picks one of
|
|
7
7
|
* these tiers based on hardware. See
|
|
8
|
-
* `/Users/shawwalters/eliza-workspace/milady/packages/inference/AGENTS.md`
|
|
8
|
+
* `/Users/shawwalters/eliza-workspace/milady/eliza/packages/inference/AGENTS.md`
|
|
9
9
|
* §2 for the binding tier matrix.
|
|
10
10
|
*
|
|
11
11
|
* HF-search results from outside `elizalabs/eliza-1-*` MUST never be
|
|
@@ -38,6 +38,62 @@ export function isDefaultEligibleId(id) {
|
|
|
38
38
|
}
|
|
39
39
|
/** Compatibility export for callers that need the Eliza-1 model id set. */
|
|
40
40
|
export const ELIZA_1_PLACEHOLDER_IDS = new Set(ELIZA_1_TIER_IDS);
|
|
41
|
+
const BASE_REQUIRED_KERNELS = [
|
|
42
|
+
"dflash",
|
|
43
|
+
"turbo3",
|
|
44
|
+
"turbo4",
|
|
45
|
+
"qjl_full",
|
|
46
|
+
"polarquant",
|
|
47
|
+
];
|
|
48
|
+
function requiredKernelsForContext(contextLength) {
|
|
49
|
+
return contextLength > 65536
|
|
50
|
+
? [...BASE_REQUIRED_KERNELS, "turbo3_tcq"]
|
|
51
|
+
: [...BASE_REQUIRED_KERNELS];
|
|
52
|
+
}
|
|
53
|
+
function drafterId(id) {
|
|
54
|
+
return `${id}-drafter`;
|
|
55
|
+
}
|
|
56
|
+
function runtimeFor(id, contextLength) {
|
|
57
|
+
return {
|
|
58
|
+
preferredBackend: "llama-server",
|
|
59
|
+
optimizations: {
|
|
60
|
+
parallel: contextLength >= 131072 ? 8 : 4,
|
|
61
|
+
flashAttention: true,
|
|
62
|
+
mlock: contextLength >= 131072,
|
|
63
|
+
requiresKernel: requiredKernelsForContext(contextLength),
|
|
64
|
+
},
|
|
65
|
+
dflash: {
|
|
66
|
+
drafterModelId: drafterId(id),
|
|
67
|
+
specType: "dflash",
|
|
68
|
+
contextSize: contextLength,
|
|
69
|
+
draftContextSize: Math.min(contextLength, 65536),
|
|
70
|
+
draftMin: 2,
|
|
71
|
+
draftMax: contextLength >= 131072 ? 8 : 6,
|
|
72
|
+
gpuLayers: "auto",
|
|
73
|
+
draftGpuLayers: "auto",
|
|
74
|
+
disableThinking: true,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function drafterCompanion(args) {
|
|
79
|
+
return {
|
|
80
|
+
id: drafterId(args.id),
|
|
81
|
+
displayName: `${args.displayName} drafter`,
|
|
82
|
+
hfRepo: `elizalabs/${args.id}`,
|
|
83
|
+
ggufFile: args.ggufFile,
|
|
84
|
+
params: args.params,
|
|
85
|
+
quant: "Eliza-1 drafter companion",
|
|
86
|
+
sizeGb: args.sizeGb,
|
|
87
|
+
minRamGb: args.minRamGb,
|
|
88
|
+
category: "drafter",
|
|
89
|
+
bucket: args.bucket,
|
|
90
|
+
hiddenFromCatalog: true,
|
|
91
|
+
runtimeRole: "dflash-drafter",
|
|
92
|
+
companionForModelId: args.id,
|
|
93
|
+
tokenizerFamily: "eliza1",
|
|
94
|
+
blurb: `${args.displayName} drafter companion.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
41
97
|
export const MODEL_CATALOG = [
|
|
42
98
|
// ─── Eliza-1 lite (low-RAM phones, CPU fallback) ────────────────────
|
|
43
99
|
{
|
|
@@ -45,80 +101,140 @@ export const MODEL_CATALOG = [
|
|
|
45
101
|
displayName: "Eliza-1 lite",
|
|
46
102
|
hfRepo: "elizalabs/eliza-1-lite-0_6b",
|
|
47
103
|
ggufFile: "text/eliza-1-lite-0_6b-32k.gguf",
|
|
104
|
+
bundleManifestFile: "eliza-1.manifest.json",
|
|
48
105
|
params: "1B",
|
|
49
|
-
quant: "
|
|
106
|
+
quant: "Eliza-1 optimized local runtime",
|
|
50
107
|
sizeGb: 0.5,
|
|
51
108
|
minRamGb: 2,
|
|
52
109
|
category: "chat",
|
|
53
110
|
bucket: "small",
|
|
54
111
|
contextLength: 32768,
|
|
55
112
|
tokenizerFamily: "eliza1",
|
|
56
|
-
|
|
113
|
+
companionModelIds: ["eliza-1-lite-0_6b-drafter"],
|
|
114
|
+
runtime: runtimeFor("eliza-1-lite-0_6b", 32768),
|
|
115
|
+
blurb: "Eliza-1 lite — fits low-RAM phones and CPU-only fallback with the optimized local runtime.",
|
|
57
116
|
},
|
|
117
|
+
drafterCompanion({
|
|
118
|
+
id: "eliza-1-lite-0_6b",
|
|
119
|
+
displayName: "Eliza-1 lite",
|
|
120
|
+
ggufFile: "dflash/drafter-lite-0_6b.gguf",
|
|
121
|
+
params: "1B",
|
|
122
|
+
sizeGb: 0.25,
|
|
123
|
+
minRamGb: 2,
|
|
124
|
+
bucket: "small",
|
|
125
|
+
}),
|
|
58
126
|
// ─── Eliza-1 mobile (modern phones) ─────────────────────────────────
|
|
59
127
|
{
|
|
60
128
|
id: "eliza-1-mobile-1_7b",
|
|
61
129
|
displayName: "Eliza-1 mobile",
|
|
62
130
|
hfRepo: "elizalabs/eliza-1-mobile-1_7b",
|
|
63
131
|
ggufFile: "text/eliza-1-mobile-1_7b-32k.gguf",
|
|
132
|
+
bundleManifestFile: "eliza-1.manifest.json",
|
|
64
133
|
params: "1.7B",
|
|
65
|
-
quant: "
|
|
134
|
+
quant: "Eliza-1 optimized local runtime",
|
|
66
135
|
sizeGb: 1.2,
|
|
67
136
|
minRamGb: 4,
|
|
68
137
|
category: "chat",
|
|
69
138
|
bucket: "small",
|
|
70
139
|
contextLength: 32768,
|
|
71
140
|
tokenizerFamily: "eliza1",
|
|
72
|
-
|
|
141
|
+
companionModelIds: ["eliza-1-mobile-1_7b-drafter"],
|
|
142
|
+
runtime: runtimeFor("eliza-1-mobile-1_7b", 32768),
|
|
143
|
+
blurb: "Eliza-1 mobile — modern phone default with text and voice prepared for the optimized local runtime.",
|
|
73
144
|
},
|
|
145
|
+
drafterCompanion({
|
|
146
|
+
id: "eliza-1-mobile-1_7b",
|
|
147
|
+
displayName: "Eliza-1 mobile",
|
|
148
|
+
ggufFile: "dflash/drafter-mobile-1_7b.gguf",
|
|
149
|
+
params: "1.7B",
|
|
150
|
+
sizeGb: 0.35,
|
|
151
|
+
minRamGb: 4,
|
|
152
|
+
bucket: "small",
|
|
153
|
+
}),
|
|
74
154
|
// ─── Eliza-1 desktop (laptops, 24GB phones, 48GB Mac) ───────────────
|
|
75
155
|
{
|
|
76
156
|
id: "eliza-1-desktop-9b",
|
|
77
157
|
displayName: "Eliza-1 desktop",
|
|
78
158
|
hfRepo: "elizalabs/eliza-1-desktop-9b",
|
|
79
159
|
ggufFile: "text/eliza-1-desktop-9b-64k.gguf",
|
|
160
|
+
bundleManifestFile: "eliza-1.manifest.json",
|
|
80
161
|
params: "9B",
|
|
81
|
-
quant: "
|
|
162
|
+
quant: "Eliza-1 optimized local runtime",
|
|
82
163
|
sizeGb: 5.4,
|
|
83
164
|
minRamGb: 12,
|
|
84
165
|
category: "chat",
|
|
85
166
|
bucket: "mid",
|
|
86
167
|
contextLength: 65536,
|
|
87
168
|
tokenizerFamily: "eliza1",
|
|
88
|
-
|
|
169
|
+
companionModelIds: ["eliza-1-desktop-9b-drafter"],
|
|
170
|
+
runtime: runtimeFor("eliza-1-desktop-9b", 65536),
|
|
171
|
+
blurb: "Eliza-1 desktop — laptop / 24 GB phone / 48 GB Mac default with text, voice, and vision in the optimized local runtime.",
|
|
89
172
|
},
|
|
173
|
+
drafterCompanion({
|
|
174
|
+
id: "eliza-1-desktop-9b",
|
|
175
|
+
displayName: "Eliza-1 desktop",
|
|
176
|
+
ggufFile: "dflash/drafter-desktop-9b.gguf",
|
|
177
|
+
params: "9B",
|
|
178
|
+
sizeGb: 0.8,
|
|
179
|
+
minRamGb: 12,
|
|
180
|
+
bucket: "mid",
|
|
181
|
+
}),
|
|
90
182
|
// ─── Eliza-1 pro (96GB+ Mac, high-VRAM desktop) ─────────────────────
|
|
91
183
|
{
|
|
92
184
|
id: "eliza-1-pro-27b",
|
|
93
185
|
displayName: "Eliza-1 pro",
|
|
94
186
|
hfRepo: "elizalabs/eliza-1-pro-27b",
|
|
95
187
|
ggufFile: "text/eliza-1-pro-27b-128k.gguf",
|
|
188
|
+
bundleManifestFile: "eliza-1.manifest.json",
|
|
96
189
|
params: "27B",
|
|
97
|
-
quant: "
|
|
190
|
+
quant: "Eliza-1 optimized local runtime",
|
|
98
191
|
sizeGb: 16.8,
|
|
99
192
|
minRamGb: 32,
|
|
100
193
|
category: "chat",
|
|
101
194
|
bucket: "large",
|
|
102
195
|
contextLength: 131072,
|
|
103
196
|
tokenizerFamily: "eliza1",
|
|
197
|
+
companionModelIds: ["eliza-1-pro-27b-drafter"],
|
|
198
|
+
runtime: runtimeFor("eliza-1-pro-27b", 131072),
|
|
104
199
|
blurb: "Eliza-1 pro — 96 GB+ Mac and high-VRAM desktop default. Fused text + voice + vision; longest-context Eliza-1 tier on workstation hardware.",
|
|
105
200
|
},
|
|
201
|
+
drafterCompanion({
|
|
202
|
+
id: "eliza-1-pro-27b",
|
|
203
|
+
displayName: "Eliza-1 pro",
|
|
204
|
+
ggufFile: "dflash/drafter-pro-27b.gguf",
|
|
205
|
+
params: "9B",
|
|
206
|
+
sizeGb: 1.2,
|
|
207
|
+
minRamGb: 32,
|
|
208
|
+
bucket: "large",
|
|
209
|
+
}),
|
|
106
210
|
// ─── Eliza-1 server (workstation / server) ──────────────────────────
|
|
107
211
|
{
|
|
108
212
|
id: "eliza-1-server-h200",
|
|
109
213
|
displayName: "Eliza-1 server",
|
|
110
214
|
hfRepo: "elizalabs/eliza-1-server-h200",
|
|
111
215
|
ggufFile: "text/eliza-1-server-h200-256k.gguf",
|
|
216
|
+
bundleManifestFile: "eliza-1.manifest.json",
|
|
112
217
|
params: "27B",
|
|
113
|
-
quant: "
|
|
218
|
+
quant: "Eliza-1 optimized local runtime",
|
|
114
219
|
sizeGb: 16.8,
|
|
115
220
|
minRamGb: 96,
|
|
116
221
|
category: "chat",
|
|
117
222
|
bucket: "large",
|
|
118
223
|
contextLength: 262144,
|
|
119
224
|
tokenizerFamily: "eliza1",
|
|
120
|
-
|
|
225
|
+
companionModelIds: ["eliza-1-server-h200-drafter"],
|
|
226
|
+
runtime: runtimeFor("eliza-1-server-h200", 262144),
|
|
227
|
+
blurb: "Eliza-1 server — H200-class workstation / server tier with the largest context window in the line.",
|
|
121
228
|
},
|
|
229
|
+
drafterCompanion({
|
|
230
|
+
id: "eliza-1-server-h200",
|
|
231
|
+
displayName: "Eliza-1 server",
|
|
232
|
+
ggufFile: "dflash/drafter-server-h200.gguf",
|
|
233
|
+
params: "9B",
|
|
234
|
+
sizeGb: 1.2,
|
|
235
|
+
minRamGb: 96,
|
|
236
|
+
bucket: "large",
|
|
237
|
+
}),
|
|
122
238
|
];
|
|
123
239
|
export function findCatalogModel(id) {
|
|
124
240
|
return MODEL_CATALOG.find((m) => m.id === id);
|
|
@@ -130,14 +246,17 @@ export function findCatalogModel(id) {
|
|
|
130
246
|
* downloader e2e test suite can redirect all downloads without touching
|
|
131
247
|
* the catalog.
|
|
132
248
|
*/
|
|
133
|
-
export function
|
|
249
|
+
export function buildHuggingFaceResolveUrlForPath(model, filePath) {
|
|
134
250
|
const base = process.env.ELIZA_HF_BASE_URL?.trim().replace(/\/+$/, "") ||
|
|
135
251
|
"https://huggingface.co";
|
|
136
252
|
// Encode each path segment separately so nested bundle layouts like
|
|
137
253
|
// `text/eliza-1-mobile-1_7b-32k.gguf` keep their slashes.
|
|
138
|
-
const encodedPath =
|
|
254
|
+
const encodedPath = filePath
|
|
139
255
|
.split("/")
|
|
140
256
|
.map((segment) => encodeURIComponent(segment))
|
|
141
257
|
.join("/");
|
|
142
258
|
return `${base}/${model.hfRepo}/resolve/main/${encodedPath}?download=true`;
|
|
143
259
|
}
|
|
260
|
+
export function buildHuggingFaceResolveUrl(model) {
|
|
261
|
+
return buildHuggingFaceResolveUrlForPath(model, model.ggufFile);
|
|
262
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"downloader.d.ts","sourceRoot":"","sources":["../../../../../../src/services/local-inference/downloader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"downloader.d.ts","sourceRoot":"","sources":["../../../../../../src/services/local-inference/downloader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAoBH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,WAAW,EAGZ,MAAM,SAAS,CAAC;AAUjB,KAAK,gBAAgB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;AAsPvD,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAC3D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA+B;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;;IAMtD,SAAS,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,IAAI;IAOjD,QAAQ,IAAI,WAAW,EAAE;IAUzB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAQlC;;;;OAIG;IACG,KAAK,CAAC,aAAa,EAAE,MAAM,GAAG,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IA2DvE,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAchC,OAAO,CAAC,IAAI;IAWZ,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,qBAAqB;IAwB7B,OAAO,CAAC,wBAAwB;IAkBhC,OAAO,CAAC,wBAAwB;IAYhC,OAAO,CAAC,qBAAqB;IAK7B,OAAO,CAAC,YAAY;YAQN,MAAM;YAgKN,YAAY;YAgJZ,kBAAkB;YAiHlB,cAAc;CAkC7B"}
|
|
@@ -21,7 +21,7 @@ import path from "node:path";
|
|
|
21
21
|
import { Readable } from "node:stream";
|
|
22
22
|
import { pipeline } from "node:stream/promises";
|
|
23
23
|
import { ensureDefaultAssignment } from "./assignments";
|
|
24
|
-
import { buildHuggingFaceResolveUrl, findCatalogModel } from "./catalog";
|
|
24
|
+
import { buildHuggingFaceResolveUrl, buildHuggingFaceResolveUrlForPath, findCatalogModel, } from "./catalog";
|
|
25
25
|
import { downloadsStagingDir, elizaModelsDir, localInferenceRoot, } from "./paths";
|
|
26
26
|
import { upsertElizaModel } from "./registry";
|
|
27
27
|
import { hashFile } from "./verify";
|
|
@@ -53,6 +53,126 @@ function finalFilename(model) {
|
|
|
53
53
|
const safe = model.id.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
54
54
|
return `${safe}.gguf`;
|
|
55
55
|
}
|
|
56
|
+
function bundleDirname(modelId) {
|
|
57
|
+
const safe = modelId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
58
|
+
return `${safe}.bundle`;
|
|
59
|
+
}
|
|
60
|
+
function bundleStagingFilename(modelId, filePath) {
|
|
61
|
+
const safePath = filePath.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
62
|
+
return stagingFilename(`${modelId}__${safePath}`);
|
|
63
|
+
}
|
|
64
|
+
function bundleTargetPath(root, filePath) {
|
|
65
|
+
if (!filePath ||
|
|
66
|
+
path.isAbsolute(filePath) ||
|
|
67
|
+
/^[a-zA-Z]:[\\/]/.test(filePath)) {
|
|
68
|
+
throw new Error(`Invalid bundle file path: ${filePath}`);
|
|
69
|
+
}
|
|
70
|
+
const resolvedRoot = path.resolve(root);
|
|
71
|
+
const target = path.resolve(resolvedRoot, filePath);
|
|
72
|
+
if (target !== resolvedRoot &&
|
|
73
|
+
!target.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
74
|
+
throw new Error(`Bundle file escapes install root: ${filePath}`);
|
|
75
|
+
}
|
|
76
|
+
return target;
|
|
77
|
+
}
|
|
78
|
+
function parseBundleFileEntry(value, kind) {
|
|
79
|
+
if (!value || typeof value !== "object") {
|
|
80
|
+
throw new Error(`Invalid Eliza-1 manifest file entry in files.${kind}`);
|
|
81
|
+
}
|
|
82
|
+
const entry = value;
|
|
83
|
+
if (typeof entry.path !== "string" || entry.path.length === 0) {
|
|
84
|
+
throw new Error(`Invalid Eliza-1 manifest file path in files.${kind}`);
|
|
85
|
+
}
|
|
86
|
+
if (typeof entry.sha256 !== "string" ||
|
|
87
|
+
!/^[a-f0-9]{64}$/.test(entry.sha256)) {
|
|
88
|
+
throw new Error(`Invalid Eliza-1 manifest sha256 for ${entry.path}`);
|
|
89
|
+
}
|
|
90
|
+
const parsed = {
|
|
91
|
+
path: entry.path,
|
|
92
|
+
sha256: entry.sha256,
|
|
93
|
+
};
|
|
94
|
+
if (typeof entry.ctx === "number")
|
|
95
|
+
parsed.ctx = entry.ctx;
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
function parseBundleManifestOrThrow(input, catalogEntry) {
|
|
99
|
+
if (!input || typeof input !== "object") {
|
|
100
|
+
throw new Error("Invalid Eliza-1 manifest: expected object");
|
|
101
|
+
}
|
|
102
|
+
const raw = input;
|
|
103
|
+
if (raw.id !== catalogEntry.id) {
|
|
104
|
+
throw new Error(`Invalid Eliza-1 manifest: id ${String(raw.id)} does not match ${catalogEntry.id}`);
|
|
105
|
+
}
|
|
106
|
+
if (typeof raw.version !== "string" || raw.version.length === 0) {
|
|
107
|
+
throw new Error("Invalid Eliza-1 manifest: missing version");
|
|
108
|
+
}
|
|
109
|
+
if (raw.defaultEligible !== true) {
|
|
110
|
+
throw new Error("Invalid Eliza-1 manifest: defaultEligible must be true");
|
|
111
|
+
}
|
|
112
|
+
if (!raw.files || typeof raw.files !== "object") {
|
|
113
|
+
throw new Error("Invalid Eliza-1 manifest: missing files");
|
|
114
|
+
}
|
|
115
|
+
const filesRaw = raw.files;
|
|
116
|
+
const files = {};
|
|
117
|
+
for (const kind of [
|
|
118
|
+
"text",
|
|
119
|
+
"voice",
|
|
120
|
+
"asr",
|
|
121
|
+
"vision",
|
|
122
|
+
"dflash",
|
|
123
|
+
"cache",
|
|
124
|
+
"embedding",
|
|
125
|
+
"vad",
|
|
126
|
+
"wakeword",
|
|
127
|
+
]) {
|
|
128
|
+
const value = filesRaw[kind];
|
|
129
|
+
if (value === undefined &&
|
|
130
|
+
(kind === "embedding" || kind === "vad" || kind === "wakeword")) {
|
|
131
|
+
files[kind] = [];
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!Array.isArray(value)) {
|
|
135
|
+
throw new Error(`Invalid Eliza-1 manifest: files.${kind} must be an array`);
|
|
136
|
+
}
|
|
137
|
+
files[kind] = value.map((entry) => parseBundleFileEntry(entry, kind));
|
|
138
|
+
}
|
|
139
|
+
for (const kind of ["text", "voice", "dflash", "cache"]) {
|
|
140
|
+
if (files[kind].length === 0) {
|
|
141
|
+
throw new Error(`Invalid Eliza-1 manifest: files.${kind} must be non-empty`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!files.text.some((entry) => entry.path === catalogEntry.ggufFile)) {
|
|
145
|
+
throw new Error(`Invalid Eliza-1 manifest: primary text file ${catalogEntry.ggufFile} is missing`);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
id: catalogEntry.id,
|
|
149
|
+
version: raw.version,
|
|
150
|
+
files,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function collectBundleFiles(manifest) {
|
|
154
|
+
const seen = new Map();
|
|
155
|
+
for (const kind of [
|
|
156
|
+
"text",
|
|
157
|
+
"voice",
|
|
158
|
+
"asr",
|
|
159
|
+
"vision",
|
|
160
|
+
"dflash",
|
|
161
|
+
"cache",
|
|
162
|
+
"embedding",
|
|
163
|
+
"vad",
|
|
164
|
+
"wakeword",
|
|
165
|
+
]) {
|
|
166
|
+
for (const entry of manifest.files[kind]) {
|
|
167
|
+
const current = seen.get(entry.path);
|
|
168
|
+
if (current && current.entry.sha256 !== entry.sha256) {
|
|
169
|
+
throw new Error(`Conflicting sha256 entries for bundle file ${entry.path}`);
|
|
170
|
+
}
|
|
171
|
+
seen.set(entry.path, { kind, entry });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return [...seen.values()];
|
|
175
|
+
}
|
|
56
176
|
async function ensureDirs() {
|
|
57
177
|
await fsp.mkdir(downloadsStagingDir(), { recursive: true });
|
|
58
178
|
await fsp.mkdir(elizaModelsDir(), { recursive: true });
|
|
@@ -236,6 +356,11 @@ export class Downloader {
|
|
|
236
356
|
async runJob(catalogEntry, record) {
|
|
237
357
|
try {
|
|
238
358
|
this.updateState(record, "downloading");
|
|
359
|
+
if (catalogEntry.bundleManifestFile &&
|
|
360
|
+
catalogEntry.runtimeRole !== "dflash-drafter") {
|
|
361
|
+
await this.runBundleJob(catalogEntry, record);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
239
364
|
const url = buildHuggingFaceResolveUrl(catalogEntry);
|
|
240
365
|
const httpClient = await this.loadHttpClient();
|
|
241
366
|
const startByte = record.job.received;
|
|
@@ -365,6 +490,180 @@ export class Downloader {
|
|
|
365
490
|
this.active.delete(record.job.modelId);
|
|
366
491
|
}
|
|
367
492
|
}
|
|
493
|
+
async runBundleJob(catalogEntry, record) {
|
|
494
|
+
if (!catalogEntry.bundleManifestFile) {
|
|
495
|
+
throw new Error(`[local-inference] ${catalogEntry.id} has no bundle manifest`);
|
|
496
|
+
}
|
|
497
|
+
const bundleRoot = path.join(elizaModelsDir(), bundleDirname(catalogEntry.id));
|
|
498
|
+
await fsp.mkdir(bundleRoot, { recursive: true });
|
|
499
|
+
const manifestPath = bundleTargetPath(bundleRoot, catalogEntry.bundleManifestFile);
|
|
500
|
+
const manifestDownloaded = await this.downloadRemotePath(catalogEntry, catalogEntry.bundleManifestFile, path.join(downloadsStagingDir(), bundleStagingFilename(catalogEntry.id, catalogEntry.bundleManifestFile)), manifestPath, record, 0);
|
|
501
|
+
const manifest = parseBundleManifestOrThrow(JSON.parse(await fsp.readFile(manifestPath, "utf8")), catalogEntry);
|
|
502
|
+
let completedBytes = manifestDownloaded.sizeBytes;
|
|
503
|
+
const downloaded = new Map();
|
|
504
|
+
for (const { entry } of collectBundleFiles(manifest)) {
|
|
505
|
+
const finalPath = bundleTargetPath(bundleRoot, entry.path);
|
|
506
|
+
const result = await this.downloadRemotePath(catalogEntry, entry.path, path.join(downloadsStagingDir(), bundleStagingFilename(catalogEntry.id, entry.path)), finalPath, record, completedBytes, entry.sha256);
|
|
507
|
+
downloaded.set(entry.path, result);
|
|
508
|
+
completedBytes += result.sizeBytes;
|
|
509
|
+
record.job.received = completedBytes;
|
|
510
|
+
record.job.total = Math.max(record.job.total, completedBytes);
|
|
511
|
+
this.throttleEmit(record);
|
|
512
|
+
}
|
|
513
|
+
const textEntry = manifest.files.text.find((entry) => entry.path === catalogEntry.ggufFile);
|
|
514
|
+
if (!textEntry) {
|
|
515
|
+
throw new Error(`[local-inference] Bundle missing primary text file ${catalogEntry.ggufFile}`);
|
|
516
|
+
}
|
|
517
|
+
const textFile = downloaded.get(textEntry.path);
|
|
518
|
+
if (!textFile) {
|
|
519
|
+
throw new Error(`[local-inference] Bundle did not install text file ${textEntry.path}`);
|
|
520
|
+
}
|
|
521
|
+
const now = new Date().toISOString();
|
|
522
|
+
const bundleMeta = {
|
|
523
|
+
bundleRoot,
|
|
524
|
+
manifestPath,
|
|
525
|
+
manifestSha256: manifestDownloaded.sha256,
|
|
526
|
+
bundleVersion: manifest.version,
|
|
527
|
+
bundleSizeBytes: completedBytes,
|
|
528
|
+
};
|
|
529
|
+
const installed = {
|
|
530
|
+
id: catalogEntry.id,
|
|
531
|
+
displayName: catalogEntry.displayName,
|
|
532
|
+
path: textFile.path,
|
|
533
|
+
sizeBytes: textFile.sizeBytes,
|
|
534
|
+
hfRepo: catalogEntry.hfRepo,
|
|
535
|
+
installedAt: now,
|
|
536
|
+
lastUsedAt: null,
|
|
537
|
+
source: "eliza-download",
|
|
538
|
+
sha256: textFile.sha256,
|
|
539
|
+
lastVerifiedAt: now,
|
|
540
|
+
...bundleMeta,
|
|
541
|
+
};
|
|
542
|
+
await upsertElizaModel(installed);
|
|
543
|
+
const companionId = catalogEntry.runtime?.dflash?.drafterModelId ??
|
|
544
|
+
catalogEntry.companionModelIds?.[0];
|
|
545
|
+
const companion = companionId ? findCatalogModel(companionId) : undefined;
|
|
546
|
+
if (companion) {
|
|
547
|
+
const drafterEntry = manifest.files.dflash.find((entry) => entry.path === companion.ggufFile);
|
|
548
|
+
if (!drafterEntry) {
|
|
549
|
+
throw new Error(`[local-inference] Bundle missing DFlash companion ${companion.ggufFile}`);
|
|
550
|
+
}
|
|
551
|
+
const drafterFile = downloaded.get(drafterEntry.path);
|
|
552
|
+
if (!drafterFile) {
|
|
553
|
+
throw new Error(`[local-inference] Bundle did not install DFlash companion ${drafterEntry.path}`);
|
|
554
|
+
}
|
|
555
|
+
await upsertElizaModel({
|
|
556
|
+
id: companion.id,
|
|
557
|
+
displayName: companion.displayName,
|
|
558
|
+
path: drafterFile.path,
|
|
559
|
+
sizeBytes: drafterFile.sizeBytes,
|
|
560
|
+
hfRepo: companion.hfRepo,
|
|
561
|
+
installedAt: now,
|
|
562
|
+
lastUsedAt: null,
|
|
563
|
+
source: "eliza-download",
|
|
564
|
+
sha256: drafterFile.sha256,
|
|
565
|
+
lastVerifiedAt: now,
|
|
566
|
+
runtimeRole: "dflash-drafter",
|
|
567
|
+
companionFor: catalogEntry.id,
|
|
568
|
+
...bundleMeta,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
await ensureDefaultAssignment(installed.id);
|
|
572
|
+
this.updateState(record, "completed");
|
|
573
|
+
record.job.received = completedBytes;
|
|
574
|
+
record.job.total = completedBytes;
|
|
575
|
+
this.rememberTerminalDownload(record.job);
|
|
576
|
+
this.emit({ type: "completed", job: { ...record.job } });
|
|
577
|
+
}
|
|
578
|
+
async downloadRemotePath(catalogEntry, remotePath, stagingPath, finalPath, record, baseBytes, expectedSha256) {
|
|
579
|
+
if (expectedSha256) {
|
|
580
|
+
try {
|
|
581
|
+
const stat = await fsp.stat(finalPath);
|
|
582
|
+
if (stat.isFile()) {
|
|
583
|
+
const currentSha256 = await hashFile(finalPath);
|
|
584
|
+
if (currentSha256 === expectedSha256) {
|
|
585
|
+
record.job.received = baseBytes + stat.size;
|
|
586
|
+
return {
|
|
587
|
+
path: finalPath,
|
|
588
|
+
sizeBytes: stat.size,
|
|
589
|
+
sha256: currentSha256,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
await fsp.rm(finalPath, { force: true });
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
// Missing files are downloaded below; unreadable stale files are
|
|
597
|
+
// treated as invalid and replaced by the fresh bundle artifact.
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
await fsp.rm(stagingPath, { force: true }).catch(() => undefined);
|
|
602
|
+
}
|
|
603
|
+
await fsp.mkdir(path.dirname(finalPath), { recursive: true });
|
|
604
|
+
await fsp.mkdir(path.dirname(stagingPath), { recursive: true });
|
|
605
|
+
let startByte = expectedSha256 ? await partialSize(stagingPath) : 0;
|
|
606
|
+
record.job.received = baseBytes + startByte;
|
|
607
|
+
const headers = {
|
|
608
|
+
"user-agent": "Eliza-LocalInference/1.0",
|
|
609
|
+
};
|
|
610
|
+
if (startByte > 0) {
|
|
611
|
+
headers.range = `bytes=${startByte}-`;
|
|
612
|
+
}
|
|
613
|
+
const httpClient = await this.loadHttpClient();
|
|
614
|
+
const url = buildHuggingFaceResolveUrlForPath(catalogEntry, remotePath);
|
|
615
|
+
const response = await httpClient.request(url, {
|
|
616
|
+
method: "GET",
|
|
617
|
+
headers,
|
|
618
|
+
signal: record.abortController.signal,
|
|
619
|
+
});
|
|
620
|
+
if (response.statusCode >= 400) {
|
|
621
|
+
throw new Error(`HTTP ${response.statusCode} from HuggingFace for ${catalogEntry.hfRepo}/${remotePath}`);
|
|
622
|
+
}
|
|
623
|
+
if (startByte > 0 && response.statusCode !== 206) {
|
|
624
|
+
startByte = 0;
|
|
625
|
+
record.job.received = baseBytes;
|
|
626
|
+
}
|
|
627
|
+
const contentLengthHeader = response.headers["content-length"];
|
|
628
|
+
const contentLength = Array.isArray(contentLengthHeader)
|
|
629
|
+
? Number.parseInt(contentLengthHeader[0] ?? "0", 10)
|
|
630
|
+
: Number.parseInt(contentLengthHeader ?? "0", 10);
|
|
631
|
+
if (Number.isFinite(contentLength) && contentLength > 0) {
|
|
632
|
+
record.job.total = Math.max(record.job.total, baseBytes + startByte + contentLength);
|
|
633
|
+
}
|
|
634
|
+
const writeStream = fs.createWriteStream(stagingPath, {
|
|
635
|
+
flags: startByte > 0 ? "a" : "w",
|
|
636
|
+
});
|
|
637
|
+
let lastSampleBytes = record.job.received;
|
|
638
|
+
let lastSampleAt = Date.now();
|
|
639
|
+
const bodyStream = Readable.from(response.body);
|
|
640
|
+
bodyStream.on("data", (chunk) => {
|
|
641
|
+
record.job.received += chunk.length;
|
|
642
|
+
const now = Date.now();
|
|
643
|
+
const elapsed = now - lastSampleAt;
|
|
644
|
+
if (elapsed >= 1000) {
|
|
645
|
+
record.job.bytesPerSec =
|
|
646
|
+
((record.job.received - lastSampleBytes) * 1000) / elapsed;
|
|
647
|
+
record.job.etaMs =
|
|
648
|
+
record.job.bytesPerSec > 0
|
|
649
|
+
? ((record.job.total - record.job.received) * 1000) /
|
|
650
|
+
record.job.bytesPerSec
|
|
651
|
+
: null;
|
|
652
|
+
lastSampleAt = now;
|
|
653
|
+
lastSampleBytes = record.job.received;
|
|
654
|
+
}
|
|
655
|
+
this.throttleEmit(record);
|
|
656
|
+
});
|
|
657
|
+
await pipeline(bodyStream, writeStream);
|
|
658
|
+
await fsp.rename(stagingPath, finalPath);
|
|
659
|
+
const stat = await fsp.stat(finalPath);
|
|
660
|
+
const sha256 = await hashFile(finalPath);
|
|
661
|
+
if (expectedSha256 && sha256 !== expectedSha256) {
|
|
662
|
+
await fsp.rm(finalPath, { force: true });
|
|
663
|
+
throw new Error(`SHA256 mismatch for bundle file ${remotePath}`);
|
|
664
|
+
}
|
|
665
|
+
return { path: finalPath, sizeBytes: stat.size, sha256 };
|
|
666
|
+
}
|
|
368
667
|
async loadHttpClient() {
|
|
369
668
|
const fetchImpl = globalThis.fetch;
|
|
370
669
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../../../../../../src/services/local-inference/manifest/validator.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAEV,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,UAAU,EACX,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,aAAa,CAAC;AAE5D;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAgBjE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAQnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAeT;
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../../../../../../src/services/local-inference/manifest/validator.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAEV,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,UAAU,EACX,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,aAAa,CAAC;AAE5D;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAgBjE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAQnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAeT;AAoHD;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,aAAa,CAAC,YAAY,CAAC,GAC5C,aAAa,CAAC,YAAY,CAAC,CAG7B"}
|