@elizaos/plugin-capacitor-bridge 2.0.3-beta.2 → 2.0.3-beta.4
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/README.md +5 -2
- package/dist/android/bridge.d.ts +39 -0
- package/dist/android/bridge.js +151 -0
- package/dist/chunk-E7Y447TQ.js +690 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-Q2XW27TY.js +1234 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +39 -0
- package/dist/ios/bridge.d.ts +3 -0
- package/dist/ios/bridge.js +2913 -0
- package/dist/mobile-device-bridge-bootstrap.d.ts +106 -0
- package/dist/mobile-device-bridge-bootstrap.js +21 -0
- package/dist/shared/fs-shim.d.ts +93 -0
- package/dist/shared/fs-shim.js +13 -0
- package/package.json +4 -4
|
@@ -0,0 +1,2913 @@
|
|
|
1
|
+
import {
|
|
2
|
+
installMobileFsShim,
|
|
3
|
+
wrapMobileFsOpen,
|
|
4
|
+
wrapMobileFsPath,
|
|
5
|
+
wrapMobileFsTwoPaths
|
|
6
|
+
} from "../chunk-E7Y447TQ.js";
|
|
7
|
+
import {
|
|
8
|
+
__export
|
|
9
|
+
} from "../chunk-MLKGABMK.js";
|
|
10
|
+
|
|
11
|
+
// src/ios/bridge.ts
|
|
12
|
+
import { Buffer } from "buffer";
|
|
13
|
+
import crypto from "crypto";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import process2 from "process";
|
|
16
|
+
import { Readable } from "stream";
|
|
17
|
+
import {
|
|
18
|
+
ChannelType,
|
|
19
|
+
createMessageMemory,
|
|
20
|
+
ModelType,
|
|
21
|
+
stringToUuid
|
|
22
|
+
} from "@elizaos/core";
|
|
23
|
+
|
|
24
|
+
// src/shared/fs-proxy.ts
|
|
25
|
+
import * as realFs from "fs";
|
|
26
|
+
|
|
27
|
+
// src/shared/fs-promises-proxy.ts
|
|
28
|
+
var fs_promises_proxy_exports = {};
|
|
29
|
+
__export(fs_promises_proxy_exports, {
|
|
30
|
+
access: () => access2,
|
|
31
|
+
appendFile: () => appendFile2,
|
|
32
|
+
chmod: () => chmod2,
|
|
33
|
+
chown: () => chown2,
|
|
34
|
+
copyFile: () => copyFile2,
|
|
35
|
+
cp: () => cp2,
|
|
36
|
+
default: () => fs_promises_proxy_default,
|
|
37
|
+
lchmod: () => lchmod2,
|
|
38
|
+
lchown: () => lchown2,
|
|
39
|
+
link: () => link2,
|
|
40
|
+
lstat: () => lstat2,
|
|
41
|
+
lutimes: () => lutimes2,
|
|
42
|
+
mkdir: () => mkdir2,
|
|
43
|
+
mkdtemp: () => mkdtemp2,
|
|
44
|
+
open: () => open2,
|
|
45
|
+
opendir: () => opendir2,
|
|
46
|
+
readFile: () => readFile2,
|
|
47
|
+
readdir: () => readdir2,
|
|
48
|
+
readlink: () => readlink2,
|
|
49
|
+
realpath: () => realpath2,
|
|
50
|
+
rename: () => rename2,
|
|
51
|
+
rm: () => rm2,
|
|
52
|
+
rmdir: () => rmdir2,
|
|
53
|
+
stat: () => stat2,
|
|
54
|
+
symlink: () => symlink2,
|
|
55
|
+
truncate: () => truncate2,
|
|
56
|
+
unlink: () => unlink2,
|
|
57
|
+
utimes: () => utimes2,
|
|
58
|
+
watch: () => watch2,
|
|
59
|
+
writeFile: () => writeFile2
|
|
60
|
+
});
|
|
61
|
+
import * as realPromises from "fs/promises";
|
|
62
|
+
var MODULE_NAME = "mobile-fs-promises-proxy";
|
|
63
|
+
function wrapPath(fn, mode) {
|
|
64
|
+
return wrapMobileFsPath(MODULE_NAME, fn, mode);
|
|
65
|
+
}
|
|
66
|
+
function wrapOpen(fn) {
|
|
67
|
+
return wrapMobileFsOpen(MODULE_NAME, fn);
|
|
68
|
+
}
|
|
69
|
+
function wrapTwoPaths(fn, srcMode, dstMode) {
|
|
70
|
+
return wrapMobileFsTwoPaths(MODULE_NAME, fn, srcMode, dstMode);
|
|
71
|
+
}
|
|
72
|
+
var access2 = wrapPath(realPromises.access, "read");
|
|
73
|
+
var appendFile2 = wrapPath(realPromises.appendFile, "write");
|
|
74
|
+
var chmod2 = wrapPath(realPromises.chmod, "write");
|
|
75
|
+
var chown2 = wrapPath(realPromises.chown, "write");
|
|
76
|
+
var copyFile2 = wrapTwoPaths(realPromises.copyFile, "read", "write");
|
|
77
|
+
var cp2 = wrapTwoPaths(realPromises.cp, "read", "write");
|
|
78
|
+
var lchmod2 = wrapPath(realPromises.lchmod, "write");
|
|
79
|
+
var lchown2 = wrapPath(realPromises.lchown, "write");
|
|
80
|
+
var link2 = wrapTwoPaths(realPromises.link, "read", "write");
|
|
81
|
+
var lstat2 = wrapPath(realPromises.lstat, "read");
|
|
82
|
+
var lutimes2 = wrapPath(realPromises.lutimes, "write");
|
|
83
|
+
var mkdir2 = wrapPath(realPromises.mkdir, "write");
|
|
84
|
+
var mkdtemp2 = wrapPath(realPromises.mkdtemp, "write");
|
|
85
|
+
var open2 = wrapOpen(realPromises.open);
|
|
86
|
+
var opendir2 = wrapPath(realPromises.opendir, "read");
|
|
87
|
+
var readdir2 = wrapPath(realPromises.readdir, "read");
|
|
88
|
+
var readFile2 = wrapPath(realPromises.readFile, "read");
|
|
89
|
+
var readlink2 = wrapPath(realPromises.readlink, "read");
|
|
90
|
+
var realpath2 = wrapPath(realPromises.realpath, "read");
|
|
91
|
+
var rename2 = wrapTwoPaths(realPromises.rename, "write", "write");
|
|
92
|
+
var rm2 = wrapPath(realPromises.rm, "write");
|
|
93
|
+
var rmdir2 = wrapPath(realPromises.rmdir, "write");
|
|
94
|
+
var stat2 = wrapPath(realPromises.stat, "read");
|
|
95
|
+
var symlink2 = wrapTwoPaths(realPromises.symlink, "read", "write");
|
|
96
|
+
var truncate2 = wrapPath(realPromises.truncate, "write");
|
|
97
|
+
var unlink2 = wrapPath(realPromises.unlink, "write");
|
|
98
|
+
var utimes2 = wrapPath(realPromises.utimes, "write");
|
|
99
|
+
var watch2 = wrapPath(realPromises.watch, "read");
|
|
100
|
+
var writeFile2 = wrapPath(realPromises.writeFile, "write");
|
|
101
|
+
var promisesDefault = {
|
|
102
|
+
...realPromises,
|
|
103
|
+
access: access2,
|
|
104
|
+
appendFile: appendFile2,
|
|
105
|
+
chmod: chmod2,
|
|
106
|
+
chown: chown2,
|
|
107
|
+
copyFile: copyFile2,
|
|
108
|
+
cp: cp2,
|
|
109
|
+
lchmod: lchmod2,
|
|
110
|
+
lchown: lchown2,
|
|
111
|
+
link: link2,
|
|
112
|
+
lstat: lstat2,
|
|
113
|
+
lutimes: lutimes2,
|
|
114
|
+
mkdir: mkdir2,
|
|
115
|
+
mkdtemp: mkdtemp2,
|
|
116
|
+
open: open2,
|
|
117
|
+
opendir: opendir2,
|
|
118
|
+
readdir: readdir2,
|
|
119
|
+
readFile: readFile2,
|
|
120
|
+
readlink: readlink2,
|
|
121
|
+
realpath: realpath2,
|
|
122
|
+
rename: rename2,
|
|
123
|
+
rm: rm2,
|
|
124
|
+
rmdir: rmdir2,
|
|
125
|
+
stat: stat2,
|
|
126
|
+
symlink: symlink2,
|
|
127
|
+
truncate: truncate2,
|
|
128
|
+
unlink: unlink2,
|
|
129
|
+
utimes: utimes2,
|
|
130
|
+
watch: watch2,
|
|
131
|
+
writeFile: writeFile2
|
|
132
|
+
};
|
|
133
|
+
var fs_promises_proxy_default = promisesDefault;
|
|
134
|
+
|
|
135
|
+
// src/shared/fs-proxy.ts
|
|
136
|
+
var MODULE_NAME2 = "mobile-fs-proxy";
|
|
137
|
+
function wrapPath2(fn, mode) {
|
|
138
|
+
return wrapMobileFsPath(MODULE_NAME2, fn, mode);
|
|
139
|
+
}
|
|
140
|
+
function wrapOpen2(fn) {
|
|
141
|
+
return wrapMobileFsOpen(MODULE_NAME2, fn);
|
|
142
|
+
}
|
|
143
|
+
function wrapTwoPaths2(fn, srcMode, dstMode) {
|
|
144
|
+
return wrapMobileFsTwoPaths(MODULE_NAME2, fn, srcMode, dstMode);
|
|
145
|
+
}
|
|
146
|
+
var constants2 = realFs.constants;
|
|
147
|
+
var promises = fs_promises_proxy_exports;
|
|
148
|
+
var access4 = wrapPath2(realFs.access, "read");
|
|
149
|
+
var accessSync2 = wrapPath2(realFs.accessSync, "read");
|
|
150
|
+
var appendFile4 = wrapPath2(realFs.appendFile, "write");
|
|
151
|
+
var appendFileSync2 = wrapPath2(realFs.appendFileSync, "write");
|
|
152
|
+
var chmod4 = wrapPath2(realFs.chmod, "write");
|
|
153
|
+
var chmodSync2 = wrapPath2(realFs.chmodSync, "write");
|
|
154
|
+
var chown4 = wrapPath2(realFs.chown, "write");
|
|
155
|
+
var chownSync2 = wrapPath2(realFs.chownSync, "write");
|
|
156
|
+
var copyFile4 = wrapTwoPaths2(realFs.copyFile, "read", "write");
|
|
157
|
+
var copyFileSync2 = wrapTwoPaths2(realFs.copyFileSync, "read", "write");
|
|
158
|
+
var cp4 = wrapTwoPaths2(realFs.cp, "read", "write");
|
|
159
|
+
var cpSync2 = wrapTwoPaths2(realFs.cpSync, "read", "write");
|
|
160
|
+
var createReadStream2 = wrapPath2(realFs.createReadStream, "read");
|
|
161
|
+
var createWriteStream2 = wrapPath2(realFs.createWriteStream, "write");
|
|
162
|
+
var existsSync2 = wrapPath2(realFs.existsSync, "read");
|
|
163
|
+
var lchmod4 = wrapPath2(realFs.lchmod, "write");
|
|
164
|
+
var lchmodSync2 = wrapPath2(realFs.lchmodSync, "write");
|
|
165
|
+
var lchown4 = wrapPath2(realFs.lchown, "write");
|
|
166
|
+
var lchownSync2 = wrapPath2(realFs.lchownSync, "write");
|
|
167
|
+
var link4 = wrapTwoPaths2(realFs.link, "read", "write");
|
|
168
|
+
var linkSync2 = wrapTwoPaths2(realFs.linkSync, "read", "write");
|
|
169
|
+
var lstat4 = wrapPath2(realFs.lstat, "read");
|
|
170
|
+
var lstatSync2 = wrapPath2(realFs.lstatSync, "read");
|
|
171
|
+
var lutimes4 = wrapPath2(realFs.lutimes, "write");
|
|
172
|
+
var lutimesSync2 = wrapPath2(realFs.lutimesSync, "write");
|
|
173
|
+
var mkdir4 = wrapPath2(realFs.mkdir, "write");
|
|
174
|
+
var mkdirSync2 = wrapPath2(realFs.mkdirSync, "write");
|
|
175
|
+
var mkdtemp4 = wrapPath2(realFs.mkdtemp, "write");
|
|
176
|
+
var mkdtempSync2 = wrapPath2(realFs.mkdtempSync, "write");
|
|
177
|
+
var open4 = wrapOpen2(realFs.open);
|
|
178
|
+
var openSync2 = wrapOpen2(realFs.openSync);
|
|
179
|
+
var opendir4 = wrapPath2(realFs.opendir, "read");
|
|
180
|
+
var opendirSync2 = wrapPath2(realFs.opendirSync, "read");
|
|
181
|
+
var readdir4 = wrapPath2(realFs.readdir, "read");
|
|
182
|
+
var readdirSync2 = wrapPath2(realFs.readdirSync, "read");
|
|
183
|
+
var readFile4 = wrapPath2(realFs.readFile, "read");
|
|
184
|
+
var readFileSync2 = wrapPath2(realFs.readFileSync, "read");
|
|
185
|
+
var readlink4 = wrapPath2(realFs.readlink, "read");
|
|
186
|
+
var readlinkSync2 = wrapPath2(realFs.readlinkSync, "read");
|
|
187
|
+
var realpath4 = wrapPath2(realFs.realpath, "read");
|
|
188
|
+
var realpathSync2 = wrapPath2(realFs.realpathSync, "read");
|
|
189
|
+
var rename4 = wrapTwoPaths2(realFs.rename, "write", "write");
|
|
190
|
+
var renameSync2 = wrapTwoPaths2(realFs.renameSync, "write", "write");
|
|
191
|
+
var rm4 = wrapPath2(realFs.rm, "write");
|
|
192
|
+
var rmSync2 = wrapPath2(realFs.rmSync, "write");
|
|
193
|
+
var rmdir4 = wrapPath2(realFs.rmdir, "write");
|
|
194
|
+
var rmdirSync2 = wrapPath2(realFs.rmdirSync, "write");
|
|
195
|
+
var stat4 = wrapPath2(realFs.stat, "read");
|
|
196
|
+
var statSync2 = wrapPath2(realFs.statSync, "read");
|
|
197
|
+
var symlink4 = wrapTwoPaths2(realFs.symlink, "read", "write");
|
|
198
|
+
var symlinkSync2 = wrapTwoPaths2(realFs.symlinkSync, "read", "write");
|
|
199
|
+
var truncate4 = wrapPath2(realFs.truncate, "write");
|
|
200
|
+
var truncateSync2 = wrapPath2(realFs.truncateSync, "write");
|
|
201
|
+
var unlink4 = wrapPath2(realFs.unlink, "write");
|
|
202
|
+
var unlinkSync2 = wrapPath2(realFs.unlinkSync, "write");
|
|
203
|
+
var utimes4 = wrapPath2(realFs.utimes, "write");
|
|
204
|
+
var utimesSync2 = wrapPath2(realFs.utimesSync, "write");
|
|
205
|
+
var watch4 = wrapPath2(realFs.watch, "read");
|
|
206
|
+
var watchFile2 = wrapPath2(realFs.watchFile, "read");
|
|
207
|
+
var writeFile4 = wrapPath2(realFs.writeFile, "write");
|
|
208
|
+
var writeFileSync2 = wrapPath2(realFs.writeFileSync, "write");
|
|
209
|
+
var fsDefault = {
|
|
210
|
+
...realFs,
|
|
211
|
+
constants: constants2,
|
|
212
|
+
promises,
|
|
213
|
+
access: access4,
|
|
214
|
+
accessSync: accessSync2,
|
|
215
|
+
appendFile: appendFile4,
|
|
216
|
+
appendFileSync: appendFileSync2,
|
|
217
|
+
chmod: chmod4,
|
|
218
|
+
chmodSync: chmodSync2,
|
|
219
|
+
chown: chown4,
|
|
220
|
+
chownSync: chownSync2,
|
|
221
|
+
copyFile: copyFile4,
|
|
222
|
+
copyFileSync: copyFileSync2,
|
|
223
|
+
cp: cp4,
|
|
224
|
+
cpSync: cpSync2,
|
|
225
|
+
createReadStream: createReadStream2,
|
|
226
|
+
createWriteStream: createWriteStream2,
|
|
227
|
+
existsSync: existsSync2,
|
|
228
|
+
lchmod: lchmod4,
|
|
229
|
+
lchmodSync: lchmodSync2,
|
|
230
|
+
lchown: lchown4,
|
|
231
|
+
lchownSync: lchownSync2,
|
|
232
|
+
link: link4,
|
|
233
|
+
linkSync: linkSync2,
|
|
234
|
+
lstat: lstat4,
|
|
235
|
+
lstatSync: lstatSync2,
|
|
236
|
+
lutimes: lutimes4,
|
|
237
|
+
lutimesSync: lutimesSync2,
|
|
238
|
+
mkdir: mkdir4,
|
|
239
|
+
mkdirSync: mkdirSync2,
|
|
240
|
+
mkdtemp: mkdtemp4,
|
|
241
|
+
mkdtempSync: mkdtempSync2,
|
|
242
|
+
open: open4,
|
|
243
|
+
openSync: openSync2,
|
|
244
|
+
opendir: opendir4,
|
|
245
|
+
opendirSync: opendirSync2,
|
|
246
|
+
readdir: readdir4,
|
|
247
|
+
readdirSync: readdirSync2,
|
|
248
|
+
readFile: readFile4,
|
|
249
|
+
readFileSync: readFileSync2,
|
|
250
|
+
readlink: readlink4,
|
|
251
|
+
readlinkSync: readlinkSync2,
|
|
252
|
+
realpath: realpath4,
|
|
253
|
+
realpathSync: realpathSync2,
|
|
254
|
+
rename: rename4,
|
|
255
|
+
renameSync: renameSync2,
|
|
256
|
+
rm: rm4,
|
|
257
|
+
rmSync: rmSync2,
|
|
258
|
+
rmdir: rmdir4,
|
|
259
|
+
rmdirSync: rmdirSync2,
|
|
260
|
+
stat: stat4,
|
|
261
|
+
statSync: statSync2,
|
|
262
|
+
symlink: symlink4,
|
|
263
|
+
symlinkSync: symlinkSync2,
|
|
264
|
+
truncate: truncate4,
|
|
265
|
+
truncateSync: truncateSync2,
|
|
266
|
+
unlink: unlink4,
|
|
267
|
+
unlinkSync: unlinkSync2,
|
|
268
|
+
utimes: utimes4,
|
|
269
|
+
utimesSync: utimesSync2,
|
|
270
|
+
watch: watch4,
|
|
271
|
+
watchFile: watchFile2,
|
|
272
|
+
writeFile: writeFile4,
|
|
273
|
+
writeFileSync: writeFileSync2
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// src/ios/model-grind.ts
|
|
277
|
+
var GRIND_PHRASE = "Eliza local voice end to end check, one two three.";
|
|
278
|
+
function now() {
|
|
279
|
+
return typeof performance !== "undefined" ? performance.now() : Number(process.hrtime.bigint() / 1000000n);
|
|
280
|
+
}
|
|
281
|
+
function wordErrorRate(reference, hypothesis) {
|
|
282
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter(Boolean);
|
|
283
|
+
const ref = norm(reference);
|
|
284
|
+
const hyp = norm(hypothesis);
|
|
285
|
+
if (ref.length === 0) return hyp.length === 0 ? 0 : 1;
|
|
286
|
+
const dp = Array.from({ length: hyp.length + 1 }, (_v, j) => j);
|
|
287
|
+
for (let i = 1; i <= ref.length; i++) {
|
|
288
|
+
let prev = dp[0];
|
|
289
|
+
dp[0] = i;
|
|
290
|
+
for (let j = 1; j <= hyp.length; j++) {
|
|
291
|
+
const tmp = dp[j];
|
|
292
|
+
dp[j] = ref[i - 1] === hyp[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
|
|
293
|
+
prev = tmp;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return dp[hyp.length] / ref.length;
|
|
297
|
+
}
|
|
298
|
+
function decodeWavToPcm(bytes) {
|
|
299
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
300
|
+
if (bytes.length < 44 || view.getUint32(0, false) !== 1380533830) {
|
|
301
|
+
throw new Error("not a RIFF/WAV payload");
|
|
302
|
+
}
|
|
303
|
+
let offset = 12;
|
|
304
|
+
let fmt = 1;
|
|
305
|
+
let channels = 1;
|
|
306
|
+
let sampleRate = 24e3;
|
|
307
|
+
let bitsPerSample = 16;
|
|
308
|
+
let dataOffset = -1;
|
|
309
|
+
let dataLen = 0;
|
|
310
|
+
while (offset + 8 <= bytes.length) {
|
|
311
|
+
const id = view.getUint32(offset, false);
|
|
312
|
+
const size = view.getUint32(offset + 4, true);
|
|
313
|
+
const body = offset + 8;
|
|
314
|
+
if (id === 1718449184) {
|
|
315
|
+
fmt = view.getUint16(body, true);
|
|
316
|
+
channels = view.getUint16(body + 2, true) || 1;
|
|
317
|
+
sampleRate = view.getUint32(body + 4, true) || 24e3;
|
|
318
|
+
bitsPerSample = view.getUint16(body + 14, true) || 16;
|
|
319
|
+
} else if (id === 1684108385) {
|
|
320
|
+
dataOffset = body;
|
|
321
|
+
dataLen = size;
|
|
322
|
+
}
|
|
323
|
+
offset = body + size + size % 2;
|
|
324
|
+
}
|
|
325
|
+
if (dataOffset < 0) throw new Error("WAV has no data chunk");
|
|
326
|
+
const pcm = [];
|
|
327
|
+
const isFloat = fmt === 3 || bitsPerSample === 32;
|
|
328
|
+
const step = bitsPerSample / 8 * channels;
|
|
329
|
+
for (let p = dataOffset; p + step <= dataOffset + dataLen && p + step <= bytes.length; p += step) {
|
|
330
|
+
const sample = isFloat ? view.getFloat32(p, true) : view.getInt16(p, true) / 32768;
|
|
331
|
+
pcm.push(sample);
|
|
332
|
+
}
|
|
333
|
+
return { pcm, sampleRate };
|
|
334
|
+
}
|
|
335
|
+
function resamplePcm(pcm, from, to) {
|
|
336
|
+
if (from === to || pcm.length === 0) return pcm;
|
|
337
|
+
const ratio = to / from;
|
|
338
|
+
const out = new Array(Math.max(1, Math.floor(pcm.length * ratio)));
|
|
339
|
+
for (let i = 0; i < out.length; i++) {
|
|
340
|
+
const src = i / ratio;
|
|
341
|
+
const i0 = Math.floor(src);
|
|
342
|
+
const i1 = Math.min(pcm.length - 1, i0 + 1);
|
|
343
|
+
const frac = src - i0;
|
|
344
|
+
out[i] = pcm[i0] * (1 - frac) + pcm[i1] * frac;
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
function availGb(hw) {
|
|
349
|
+
const v = Number(hw.available_ram_gb ?? hw.free_ram_gb ?? NaN);
|
|
350
|
+
return Number.isFinite(v) ? v : null;
|
|
351
|
+
}
|
|
352
|
+
async function runModelGrind(deps) {
|
|
353
|
+
const startedAtEpochMs = Date.now();
|
|
354
|
+
const t0 = now();
|
|
355
|
+
const models = [];
|
|
356
|
+
let device = {};
|
|
357
|
+
let beforeAvailGb = null;
|
|
358
|
+
try {
|
|
359
|
+
device = await deps.hardwareInfo();
|
|
360
|
+
beforeAvailGb = availGb(device);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
device = { error: error instanceof Error ? error.message : String(error) };
|
|
363
|
+
}
|
|
364
|
+
let minAvailGb = beforeAvailGb;
|
|
365
|
+
const trackMem = async () => {
|
|
366
|
+
try {
|
|
367
|
+
const a = availGb(await deps.hardwareInfo());
|
|
368
|
+
if (a !== null && (minAvailGb === null || a < minAvailGb)) minAvailGb = a;
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
{
|
|
373
|
+
const r = { model: "text", ok: false };
|
|
374
|
+
let availPreLoad = null;
|
|
375
|
+
try {
|
|
376
|
+
availPreLoad = availGb(await deps.hardwareInfo());
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
const lt = now();
|
|
381
|
+
const state = await deps.ensureTextModelLoaded("TEXT_SMALL");
|
|
382
|
+
const contextId = typeof state.contextId === "number" && Number.isFinite(state.contextId) ? state.contextId : null;
|
|
383
|
+
if (contextId == null) {
|
|
384
|
+
throw new Error("Text model load returned no contextId");
|
|
385
|
+
}
|
|
386
|
+
r.loadMs = Math.round(now() - lt);
|
|
387
|
+
let availPostLoad = null;
|
|
388
|
+
try {
|
|
389
|
+
availPostLoad = availGb(await deps.hardwareInfo());
|
|
390
|
+
} catch {
|
|
391
|
+
}
|
|
392
|
+
r.loadDetail = {
|
|
393
|
+
loadResult: state,
|
|
394
|
+
availPreLoadGb: availPreLoad,
|
|
395
|
+
availPostLoadGb: availPostLoad,
|
|
396
|
+
loadUsedGb: availPreLoad !== null && availPostLoad !== null ? Math.round((availPreLoad - availPostLoad) * 1e3) / 1e3 : null
|
|
397
|
+
};
|
|
398
|
+
const gt = now();
|
|
399
|
+
const res = await deps.callIosHost(
|
|
400
|
+
"llama_generate",
|
|
401
|
+
{
|
|
402
|
+
context_id: contextId,
|
|
403
|
+
prompt: "User: Say hello in one short sentence.\nAssistant:",
|
|
404
|
+
max_tokens: 48,
|
|
405
|
+
temperature: 0.7,
|
|
406
|
+
top_p: 0.9,
|
|
407
|
+
top_k: 40,
|
|
408
|
+
stop: ["<|im_end|>", "<|endoftext|>", "\nUser:"]
|
|
409
|
+
},
|
|
410
|
+
12e4
|
|
411
|
+
);
|
|
412
|
+
r.inferMs = Math.round(now() - gt);
|
|
413
|
+
const outTokens = Number(res.outputTokens ?? res.tokens ?? 0);
|
|
414
|
+
const text = String(res.text ?? "");
|
|
415
|
+
const tps = r.inferMs > 0 ? outTokens * 1e3 / r.inferMs : 0;
|
|
416
|
+
r.throughput = {
|
|
417
|
+
kind: "tokens_per_sec",
|
|
418
|
+
value: Math.round(tps * 10) / 10
|
|
419
|
+
};
|
|
420
|
+
r.detail = {
|
|
421
|
+
outputTokens: outTokens,
|
|
422
|
+
sample: text.slice(0, 120),
|
|
423
|
+
mtp: res.specAccepted ?? res.mtpAccepted ?? null
|
|
424
|
+
};
|
|
425
|
+
r.ok = outTokens > 0 && text.trim().length > 0;
|
|
426
|
+
if (!r.ok) r.error = "text model returned empty output";
|
|
427
|
+
} catch (error) {
|
|
428
|
+
r.error = error instanceof Error ? error.message : String(error);
|
|
429
|
+
let availAtFail = null;
|
|
430
|
+
try {
|
|
431
|
+
availAtFail = availGb(await deps.hardwareInfo());
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
r.loadDetail = r.loadDetail ?? {
|
|
435
|
+
availPreLoadGb: availPreLoad,
|
|
436
|
+
availAtFailureGb: availAtFail,
|
|
437
|
+
usedBeforeFailureGb: availPreLoad !== null && availAtFail !== null ? Math.round((availPreLoad - availAtFail) * 1e3) / 1e3 : null
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
await trackMem();
|
|
441
|
+
models.push(r);
|
|
442
|
+
}
|
|
443
|
+
let ttsWav = null;
|
|
444
|
+
{
|
|
445
|
+
const r = { model: "tts", ok: false };
|
|
446
|
+
try {
|
|
447
|
+
const st = now();
|
|
448
|
+
ttsWav = await deps.synthesizeTts(GRIND_PHRASE);
|
|
449
|
+
r.inferMs = Math.round(now() - st);
|
|
450
|
+
const { pcm, sampleRate } = decodeWavToPcm(ttsWav.bytes);
|
|
451
|
+
const audioSec = pcm.length / sampleRate;
|
|
452
|
+
const rtf = audioSec > 0 ? r.inferMs / 1e3 / audioSec : 0;
|
|
453
|
+
r.throughput = { kind: "rtf", value: Math.round(rtf * 1e3) / 1e3 };
|
|
454
|
+
r.detail = {
|
|
455
|
+
audioSec: Math.round(audioSec * 100) / 100,
|
|
456
|
+
samples: pcm.length,
|
|
457
|
+
sampleRate
|
|
458
|
+
};
|
|
459
|
+
r.ok = pcm.length > sampleRate * 0.3 && audioSec > 0.3;
|
|
460
|
+
if (!r.ok) r.error = "TTS produced too little audio";
|
|
461
|
+
} catch (error) {
|
|
462
|
+
r.error = error instanceof Error ? error.message : String(error);
|
|
463
|
+
}
|
|
464
|
+
await trackMem();
|
|
465
|
+
models.push(r);
|
|
466
|
+
}
|
|
467
|
+
{
|
|
468
|
+
const r = { model: "asr", ok: false };
|
|
469
|
+
try {
|
|
470
|
+
if (!ttsWav) throw new Error("no TTS audio to transcribe (TTS failed)");
|
|
471
|
+
const { pcm, sampleRate } = decodeWavToPcm(ttsWav.bytes);
|
|
472
|
+
const pcm16k = resamplePcm(pcm, sampleRate, 16e3);
|
|
473
|
+
const at = now();
|
|
474
|
+
const transcript = await deps.transcribeAsr(pcm16k, 16e3);
|
|
475
|
+
r.inferMs = Math.round(now() - at);
|
|
476
|
+
const wer = wordErrorRate(GRIND_PHRASE, transcript);
|
|
477
|
+
r.throughput = { kind: "wer", value: Math.round(wer * 1e3) / 1e3 };
|
|
478
|
+
r.detail = { reference: GRIND_PHRASE, hypothesis: transcript, wer };
|
|
479
|
+
r.ok = transcript.trim().length > 0 && wer <= 0.5;
|
|
480
|
+
if (!r.ok)
|
|
481
|
+
r.error = `ASR round-trip WER too high (${wer.toFixed(2)}) or empty`;
|
|
482
|
+
} catch (error) {
|
|
483
|
+
r.error = error instanceof Error ? error.message : String(error);
|
|
484
|
+
}
|
|
485
|
+
await trackMem();
|
|
486
|
+
models.push(r);
|
|
487
|
+
}
|
|
488
|
+
let afterAvailGb = null;
|
|
489
|
+
try {
|
|
490
|
+
afterAvailGb = availGb(await deps.hardwareInfo());
|
|
491
|
+
} catch {
|
|
492
|
+
}
|
|
493
|
+
const peakUsedDeltaGb = beforeAvailGb !== null && minAvailGb !== null ? Math.round((beforeAvailGb - minAvailGb) * 1e3) / 1e3 : null;
|
|
494
|
+
const passed = models.filter((m) => m.ok).length;
|
|
495
|
+
const finishedAtEpochMs = Date.now();
|
|
496
|
+
return {
|
|
497
|
+
startedAtEpochMs,
|
|
498
|
+
finishedAtEpochMs,
|
|
499
|
+
totalMs: Math.round(now() - t0),
|
|
500
|
+
bundleDir: deps.bundleDir,
|
|
501
|
+
device,
|
|
502
|
+
memory: { beforeAvailGb, afterAvailGb, peakUsedDeltaGb },
|
|
503
|
+
models,
|
|
504
|
+
overall: {
|
|
505
|
+
allPassed: passed === models.length,
|
|
506
|
+
passed,
|
|
507
|
+
failed: models.length - passed
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/ios/bridge.ts
|
|
513
|
+
async function loadAgentModule() {
|
|
514
|
+
const [{ bootElizaRuntime }, { dispatchRoute }] = await Promise.all([
|
|
515
|
+
import("@elizaos/agent/runtime"),
|
|
516
|
+
import("@elizaos/agent/api")
|
|
517
|
+
]);
|
|
518
|
+
return { bootElizaRuntime, dispatchRoute };
|
|
519
|
+
}
|
|
520
|
+
var IOS_NATIVE_LLAMA_PROVIDER = "capacitor-llama";
|
|
521
|
+
var IOS_NATIVE_LLAMA_DEVICE_ID = "ios-native-llama";
|
|
522
|
+
var IOS_NATIVE_LLAMA_PRIORITY = 0;
|
|
523
|
+
var ELIZA_1_HF_REPO = "elizaos/eliza-1";
|
|
524
|
+
var IOS_NATIVE_CATALOG_MODELS = [
|
|
525
|
+
{
|
|
526
|
+
id: "eliza-1-2b",
|
|
527
|
+
displayName: "eliza-1-2B",
|
|
528
|
+
hfRepo: ELIZA_1_HF_REPO,
|
|
529
|
+
hfPath: "bundles/2b/text/eliza-1-2b-128k.gguf",
|
|
530
|
+
ggufFile: "text/eliza-1-2b-128k.gguf",
|
|
531
|
+
sizeGb: 1.4,
|
|
532
|
+
minRamGb: 4,
|
|
533
|
+
params: "2B",
|
|
534
|
+
bucket: "small",
|
|
535
|
+
contextLength: 131072
|
|
536
|
+
}
|
|
537
|
+
];
|
|
538
|
+
var IOS_NATIVE_ASSIGNMENT_SLOTS = /* @__PURE__ */ new Set([
|
|
539
|
+
"TEXT_SMALL",
|
|
540
|
+
"TEXT_LARGE",
|
|
541
|
+
"TEXT_TO_SPEECH",
|
|
542
|
+
"TRANSCRIPTION"
|
|
543
|
+
]);
|
|
544
|
+
var IOS_NATIVE_NO_THINK_SYSTEM = "Answer with final user-visible content only. Do not include private reasoning, analysis tags, or <think> blocks.";
|
|
545
|
+
var TEXT_GENERATION_MODEL_TYPES = [
|
|
546
|
+
ModelType.TEXT_NANO,
|
|
547
|
+
ModelType.TEXT_SMALL,
|
|
548
|
+
ModelType.TEXT_MEDIUM,
|
|
549
|
+
ModelType.TEXT_LARGE,
|
|
550
|
+
ModelType.RESPONSE_HANDLER,
|
|
551
|
+
ModelType.ACTION_PLANNER,
|
|
552
|
+
ModelType.TEXT_COMPLETION
|
|
553
|
+
];
|
|
554
|
+
var nativeLlamaState = {
|
|
555
|
+
contextId: null,
|
|
556
|
+
modelId: null,
|
|
557
|
+
modelPath: null,
|
|
558
|
+
loadedAt: null,
|
|
559
|
+
status: "idle"
|
|
560
|
+
};
|
|
561
|
+
var nativeDownloadState = /* @__PURE__ */ new Map();
|
|
562
|
+
var pendingHostCalls = /* @__PURE__ */ new Map();
|
|
563
|
+
var hostProtocolWrite = null;
|
|
564
|
+
var nextHostCallId = 1;
|
|
565
|
+
function normalizeHeaderRecord(value) {
|
|
566
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
567
|
+
const out = {};
|
|
568
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
569
|
+
if (typeof raw === "string") out[key] = raw;
|
|
570
|
+
else if (typeof raw === "number" || typeof raw === "boolean") {
|
|
571
|
+
out[key] = String(raw);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return out;
|
|
575
|
+
}
|
|
576
|
+
function isSafeLocalPath(path2) {
|
|
577
|
+
return path2.startsWith("/") && !path2.startsWith("//") && !/^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(path2);
|
|
578
|
+
}
|
|
579
|
+
function normalizeMethod(value) {
|
|
580
|
+
const method = (typeof value === "string" ? value : "GET").trim().toUpperCase();
|
|
581
|
+
if (!/^[A-Z]{1,16}$/.test(method)) {
|
|
582
|
+
throw new Error("Unsupported HTTP method");
|
|
583
|
+
}
|
|
584
|
+
return method;
|
|
585
|
+
}
|
|
586
|
+
function argvValue(argv, flag) {
|
|
587
|
+
const index = argv.indexOf(flag);
|
|
588
|
+
if (index < 0) return null;
|
|
589
|
+
const value = argv[index + 1];
|
|
590
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
591
|
+
}
|
|
592
|
+
function setProcessEnv(key, value, overwrite = true) {
|
|
593
|
+
if (!key || typeof value !== "string" || value.length === 0) return;
|
|
594
|
+
if (!overwrite && process2.env[key]) return;
|
|
595
|
+
try {
|
|
596
|
+
process2.env[key] = value;
|
|
597
|
+
} catch {
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function hydrateIosEnvFromArgv(argv = process2.argv) {
|
|
601
|
+
const rawEnv = argvValue(argv, "--eliza-ios-env-json");
|
|
602
|
+
if (rawEnv) {
|
|
603
|
+
try {
|
|
604
|
+
const parsed = JSON.parse(rawEnv);
|
|
605
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
606
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
607
|
+
if (typeof value === "string") setProcessEnv(key, value, true);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
} catch (error) {
|
|
611
|
+
console.error(
|
|
612
|
+
"[ios-bridge] failed to parse argv env envelope:",
|
|
613
|
+
error instanceof Error ? error.message : String(error)
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const appSupportDir = argvValue(argv, "--eliza-ios-app-support-dir") || process2.env.ELIZA_IOS_APP_SUPPORT_DIR || process2.env.ELIZA_HOME || null;
|
|
618
|
+
const bundlePath = argvValue(argv, "--eliza-ios-agent-bundle") || process2.env.ELIZA_IOS_AGENT_BUNDLE || null;
|
|
619
|
+
if (appSupportDir) {
|
|
620
|
+
setProcessEnv("HOME", appSupportDir, true);
|
|
621
|
+
setProcessEnv("ELIZA_HOME", appSupportDir, true);
|
|
622
|
+
setProcessEnv("ELIZA_STATE_DIR", appSupportDir, true);
|
|
623
|
+
setProcessEnv("ELIZA_IOS_APP_SUPPORT_DIR", appSupportDir, true);
|
|
624
|
+
setProcessEnv("MOBILE_WORKSPACE_ROOT", appSupportDir, true);
|
|
625
|
+
setProcessEnv(
|
|
626
|
+
"ELIZA_WORKSPACE_DIR",
|
|
627
|
+
path.join(appSupportDir, "workspace"),
|
|
628
|
+
true
|
|
629
|
+
);
|
|
630
|
+
setProcessEnv(
|
|
631
|
+
"PGLITE_DATA_DIR",
|
|
632
|
+
path.join(appSupportDir, ".elizadb"),
|
|
633
|
+
true
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
if (bundlePath) {
|
|
637
|
+
setProcessEnv("ELIZA_IOS_AGENT_BUNDLE", bundlePath, true);
|
|
638
|
+
const assetDir = path.dirname(bundlePath);
|
|
639
|
+
setProcessEnv("ELIZA_IOS_AGENT_ASSET_DIR", assetDir, true);
|
|
640
|
+
setProcessEnv("ELIZA_IOS_AGENT_PUBLIC_DIR", path.dirname(assetDir), true);
|
|
641
|
+
}
|
|
642
|
+
return { appSupportDir, bundlePath };
|
|
643
|
+
}
|
|
644
|
+
async function startIosBridgeBackend() {
|
|
645
|
+
const argvEnv = hydrateIosEnvFromArgv();
|
|
646
|
+
const mobileWorkspaceRoot = process2.env.MOBILE_WORKSPACE_ROOT || argvEnv.appSupportDir || process2.env.ELIZA_WORKSPACE_DIR || process2.env.ELIZA_STATE_DIR || process2.env.ELIZA_HOME || (process2.env.HOME ? `${process2.env.HOME}/Library/Application Support/Eliza/workspace` : "/tmp/eliza-workspace");
|
|
647
|
+
installMobileFsShim(mobileWorkspaceRoot);
|
|
648
|
+
globalThis.__ELIZA_DISABLE_DIRECT_RUN = true;
|
|
649
|
+
process2.env.ELIZA_PLATFORM = process2.env.ELIZA_PLATFORM || "ios";
|
|
650
|
+
process2.env.ELIZA_MOBILE_PLATFORM = process2.env.ELIZA_MOBILE_PLATFORM || "ios";
|
|
651
|
+
process2.env.ELIZA_IOS_LOCAL_BACKEND = process2.env.ELIZA_IOS_LOCAL_BACKEND || "1";
|
|
652
|
+
process2.env.ELIZA_DISABLE_DIRECT_RUN = process2.env.ELIZA_DISABLE_DIRECT_RUN || "1";
|
|
653
|
+
process2.env.ELIZA_HEADLESS = process2.env.ELIZA_HEADLESS || "1";
|
|
654
|
+
process2.env.ELIZA_IOS_BRIDGE_TRANSPORT = process2.env.ELIZA_IOS_BRIDGE_TRANSPORT || "bun-host-ipc";
|
|
655
|
+
process2.env.ELIZA_VAULT_BACKEND = process2.env.ELIZA_VAULT_BACKEND || "file";
|
|
656
|
+
process2.env.ELIZA_DISABLE_VAULT_PROFILE_RESOLVER = process2.env.ELIZA_DISABLE_VAULT_PROFILE_RESOLVER || "1";
|
|
657
|
+
process2.env.ELIZA_DISABLE_AGENT_WALLET_BOOTSTRAP = process2.env.ELIZA_DISABLE_AGENT_WALLET_BOOTSTRAP || "1";
|
|
658
|
+
process2.env.LOG_LEVEL = process2.env.LOG_LEVEL || "error";
|
|
659
|
+
const { bootElizaRuntime, dispatchRoute } = await loadAgentModule();
|
|
660
|
+
const runtime = await bootElizaRuntime();
|
|
661
|
+
installIosNativeLlamaHandlers(runtime);
|
|
662
|
+
maybeAutoRunModelGrind();
|
|
663
|
+
return {
|
|
664
|
+
runtime,
|
|
665
|
+
dispatchRoute,
|
|
666
|
+
conversations: /* @__PURE__ */ new Map(),
|
|
667
|
+
close: async () => {
|
|
668
|
+
await unloadNativeLlamaModel().catch(() => void 0);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function maybeAutoRunModelGrind() {
|
|
673
|
+
if (process2.env.ELIZA_IOS_RUN_MODEL_GRIND !== "1") return;
|
|
674
|
+
void (async () => {
|
|
675
|
+
const deadline = Date.now() + 12e4;
|
|
676
|
+
while (hostProtocolWrite == null && Date.now() < deadline) {
|
|
677
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
678
|
+
}
|
|
679
|
+
if (hostProtocolWrite == null) {
|
|
680
|
+
console.error("[model-grind] native host never wired; skipping");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
try {
|
|
684
|
+
const report = await runModelGrind({
|
|
685
|
+
callIosHost,
|
|
686
|
+
ensureTextModelLoaded: (slot) => ensureNativeModelLoaded(slot),
|
|
687
|
+
synthesizeTts: async (text) => ({
|
|
688
|
+
bytes: await synthesizeNativeIosLocalTts({ text }),
|
|
689
|
+
sampleRate: 24e3
|
|
690
|
+
}),
|
|
691
|
+
transcribeAsr: (pcm, sampleRate) => transcribeNativeIosLocalAsr({ pcm, sampleRate }),
|
|
692
|
+
hardwareInfo: () => nativeHardwareInfo(),
|
|
693
|
+
bundleDir: nativeVoiceBundleDir()
|
|
694
|
+
});
|
|
695
|
+
const json = JSON.stringify(report);
|
|
696
|
+
console.log(`[model-grind] REPORT ${json}`);
|
|
697
|
+
const supportDir = process2.env.ELIZA_IOS_APP_SUPPORT_DIR?.trim();
|
|
698
|
+
if (supportDir) {
|
|
699
|
+
try {
|
|
700
|
+
writeFileSync2(
|
|
701
|
+
path.join(supportDir, "model-grind-report.json"),
|
|
702
|
+
`${JSON.stringify(report, null, 2)}
|
|
703
|
+
`
|
|
704
|
+
);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
console.error(
|
|
707
|
+
"[model-grind] report write failed:",
|
|
708
|
+
error instanceof Error ? error.message : error
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
} catch (error) {
|
|
713
|
+
console.error(
|
|
714
|
+
"[model-grind] grind failed:",
|
|
715
|
+
error instanceof Error ? error.message : error
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
})();
|
|
719
|
+
}
|
|
720
|
+
function startIosBridgeHost() {
|
|
721
|
+
const host = {
|
|
722
|
+
backend: null,
|
|
723
|
+
bootError: null,
|
|
724
|
+
backendPromise: null
|
|
725
|
+
};
|
|
726
|
+
return host;
|
|
727
|
+
}
|
|
728
|
+
function ensureIosBridgeBackendStarted(host) {
|
|
729
|
+
if (host.backendPromise) return host.backendPromise;
|
|
730
|
+
host.backendPromise = startIosBridgeBackend().then(
|
|
731
|
+
(backend) => {
|
|
732
|
+
host.backend = backend;
|
|
733
|
+
return backend;
|
|
734
|
+
},
|
|
735
|
+
(error) => {
|
|
736
|
+
host.bootError = error;
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
);
|
|
740
|
+
host.backendPromise.catch(() => {
|
|
741
|
+
return void 0;
|
|
742
|
+
});
|
|
743
|
+
return host.backendPromise;
|
|
744
|
+
}
|
|
745
|
+
async function awaitIosBridgeBackend(host, timeoutMs, label) {
|
|
746
|
+
if (host.backend) return host.backend;
|
|
747
|
+
if (host.bootError) {
|
|
748
|
+
throw host.bootError instanceof Error ? host.bootError : new Error(String(host.bootError));
|
|
749
|
+
}
|
|
750
|
+
const result = await timeoutAfter(
|
|
751
|
+
ensureIosBridgeBackendStarted(host),
|
|
752
|
+
timeoutMs,
|
|
753
|
+
`${label} backend startup`
|
|
754
|
+
);
|
|
755
|
+
if (isTimeoutMarker(result)) {
|
|
756
|
+
throw new Error(`${result.label} timed out after ${result.timeoutMs}ms`);
|
|
757
|
+
}
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
760
|
+
function splitPathAndQuery(rawPath) {
|
|
761
|
+
const qIndex = rawPath.indexOf("?");
|
|
762
|
+
if (qIndex < 0) return { pathname: rawPath, query: {} };
|
|
763
|
+
const pathname = rawPath.slice(0, qIndex);
|
|
764
|
+
const params = new URLSearchParams(rawPath.slice(qIndex + 1));
|
|
765
|
+
const query = {};
|
|
766
|
+
for (const key of params.keys()) {
|
|
767
|
+
const all = params.getAll(key);
|
|
768
|
+
query[key] = all.length <= 1 ? all[0] ?? "" : all;
|
|
769
|
+
}
|
|
770
|
+
return { pathname, query };
|
|
771
|
+
}
|
|
772
|
+
function payloadBodyAsRaw(payload) {
|
|
773
|
+
if (typeof payload.bodyBase64 === "string") {
|
|
774
|
+
return Buffer.from(payload.bodyBase64, "base64");
|
|
775
|
+
}
|
|
776
|
+
if (payload.bodyEncoding === "base64" && typeof payload.body === "string") {
|
|
777
|
+
return Buffer.from(payload.body, "base64");
|
|
778
|
+
}
|
|
779
|
+
return payload.body;
|
|
780
|
+
}
|
|
781
|
+
function statusTextForCode(status) {
|
|
782
|
+
if (status === 200) return "OK";
|
|
783
|
+
if (status === 201) return "Created";
|
|
784
|
+
if (status === 204) return "No Content";
|
|
785
|
+
if (status === 400) return "Bad Request";
|
|
786
|
+
if (status === 401) return "Unauthorized";
|
|
787
|
+
if (status === 403) return "Forbidden";
|
|
788
|
+
if (status === 404) return "Not Found";
|
|
789
|
+
if (status === 504) return "Gateway Timeout";
|
|
790
|
+
if (status === 500) return "Internal Server Error";
|
|
791
|
+
return "";
|
|
792
|
+
}
|
|
793
|
+
function bridgeStatus(values = {}) {
|
|
794
|
+
return {
|
|
795
|
+
ready: values.ready ?? true,
|
|
796
|
+
engine: "bun",
|
|
797
|
+
transport: "bun-host-ipc",
|
|
798
|
+
bridgeVersion: "bun-ios:3",
|
|
799
|
+
...values.phase ? { phase: values.phase } : {},
|
|
800
|
+
...values.error ? { error: values.error } : {}
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function timeoutResponse(label, timeoutMs) {
|
|
804
|
+
const body = JSON.stringify({
|
|
805
|
+
error: `${label} timed out after ${timeoutMs}ms`,
|
|
806
|
+
code: "timeout",
|
|
807
|
+
timeoutMs
|
|
808
|
+
});
|
|
809
|
+
return {
|
|
810
|
+
status: 504,
|
|
811
|
+
statusText: statusTextForCode(504),
|
|
812
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
813
|
+
body,
|
|
814
|
+
bodyBase64: Buffer.from(body, "utf8").toString("base64"),
|
|
815
|
+
bodyEncoding: "utf-8"
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
function timeoutAfter(promise, timeoutMs, label) {
|
|
819
|
+
if (!timeoutMs || timeoutMs <= 0) return promise;
|
|
820
|
+
const jsTimeoutMs = Math.max(100, timeoutMs - 500);
|
|
821
|
+
return new Promise((resolve, reject) => {
|
|
822
|
+
const timer = setTimeout(() => {
|
|
823
|
+
resolve({ __timeout: true, timeoutMs: jsTimeoutMs, label });
|
|
824
|
+
}, jsTimeoutMs);
|
|
825
|
+
promise.then(
|
|
826
|
+
(value) => {
|
|
827
|
+
clearTimeout(timer);
|
|
828
|
+
resolve(value);
|
|
829
|
+
},
|
|
830
|
+
(error) => {
|
|
831
|
+
clearTimeout(timer);
|
|
832
|
+
reject(error);
|
|
833
|
+
}
|
|
834
|
+
);
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
function bridgeTimeoutMs(value) {
|
|
838
|
+
return typeof value === "number" && value > 0 ? Math.min(value, 30 * 6e4) : void 0;
|
|
839
|
+
}
|
|
840
|
+
function isTimeoutMarker(value) {
|
|
841
|
+
return Boolean(
|
|
842
|
+
value && typeof value === "object" && "__timeout" in value && value.__timeout === true
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
async function fetchBackend(backend, payload) {
|
|
846
|
+
const rawPath = typeof payload.path === "string" ? payload.path.trim() : "";
|
|
847
|
+
if (!rawPath || !isSafeLocalPath(rawPath)) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
"iOS bridge http_request requires a path that starts with / and is not an absolute URL"
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
const method = normalizeMethod(payload.method);
|
|
853
|
+
const headers = normalizeHeaderRecord(payload.headers);
|
|
854
|
+
const timeoutMs = bridgeTimeoutMs(payload.timeoutMs);
|
|
855
|
+
const { pathname, query } = splitPathAndQuery(rawPath);
|
|
856
|
+
const direct = await timeoutAfter(
|
|
857
|
+
handleDirectCoreRoute(backend, method, rawPath, payload),
|
|
858
|
+
timeoutMs,
|
|
859
|
+
`${method} ${pathname}`
|
|
860
|
+
);
|
|
861
|
+
if (isTimeoutMarker(direct)) {
|
|
862
|
+
return timeoutResponse(direct.label, direct.timeoutMs);
|
|
863
|
+
}
|
|
864
|
+
if (direct) return direct;
|
|
865
|
+
const result = await timeoutAfter(
|
|
866
|
+
backend.dispatchRoute({
|
|
867
|
+
runtime: backend.runtime,
|
|
868
|
+
method,
|
|
869
|
+
path: pathname,
|
|
870
|
+
headers,
|
|
871
|
+
query,
|
|
872
|
+
body: payloadBodyAsRaw(payload),
|
|
873
|
+
inProcess: true,
|
|
874
|
+
isAuthorized: () => true
|
|
875
|
+
}),
|
|
876
|
+
timeoutMs,
|
|
877
|
+
`${method} ${pathname}`
|
|
878
|
+
);
|
|
879
|
+
if (isTimeoutMarker(result)) {
|
|
880
|
+
return timeoutResponse(result.label, result.timeoutMs);
|
|
881
|
+
}
|
|
882
|
+
if (result) {
|
|
883
|
+
const responseHeaders = result.headers ?? {};
|
|
884
|
+
let bodyBytes;
|
|
885
|
+
if (result.body == null) {
|
|
886
|
+
bodyBytes = Buffer.alloc(0);
|
|
887
|
+
} else if (typeof result.body === "string") {
|
|
888
|
+
bodyBytes = Buffer.from(result.body, "utf8");
|
|
889
|
+
} else if (Buffer.isBuffer(result.body)) {
|
|
890
|
+
bodyBytes = result.body;
|
|
891
|
+
} else if (result.body instanceof Uint8Array) {
|
|
892
|
+
bodyBytes = Buffer.from(result.body);
|
|
893
|
+
} else {
|
|
894
|
+
bodyBytes = Buffer.from(JSON.stringify(result.body), "utf8");
|
|
895
|
+
if (!Object.keys(responseHeaders).some(
|
|
896
|
+
(k) => k.toLowerCase() === "content-type"
|
|
897
|
+
)) {
|
|
898
|
+
responseHeaders["content-type"] = "application/json; charset=utf-8";
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
status: result.status,
|
|
903
|
+
statusText: statusTextForCode(result.status),
|
|
904
|
+
headers: responseHeaders,
|
|
905
|
+
body: bodyBytes.toString("utf8"),
|
|
906
|
+
bodyBase64: bodyBytes.toString("base64"),
|
|
907
|
+
bodyEncoding: "utf-8"
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
return jsonResponse(404, {
|
|
911
|
+
error: `No iOS local route for ${method} ${pathname}`,
|
|
912
|
+
code: "not_found"
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
function parseJsonBody(body) {
|
|
916
|
+
try {
|
|
917
|
+
return JSON.parse(body);
|
|
918
|
+
} catch {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function sanitizeLocalInferenceSpeechText(input) {
|
|
923
|
+
let text = input.normalize("NFKC");
|
|
924
|
+
text = text.replace(/<think\b[^>]*>[\s\S]*?(?:<\/think>|$)/gi, " ");
|
|
925
|
+
text = text.replace(
|
|
926
|
+
/<(analysis|reasoning|tool_calls?|tools?)\b[^>]*>[\s\S]*?(?:<\/\1>|$)/gi,
|
|
927
|
+
" "
|
|
928
|
+
);
|
|
929
|
+
text = text.replace(/```[\s\S]*?```/g, " ");
|
|
930
|
+
text = text.replace(/`([^`]+)`/g, "$1");
|
|
931
|
+
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
932
|
+
text = text.replace(/<[^>\n]+>/g, " ");
|
|
933
|
+
text = text.replace(/\bhttps?:\/\/\S+/gi, " ");
|
|
934
|
+
return text.replace(/\s+/g, " ").trim();
|
|
935
|
+
}
|
|
936
|
+
function normalizeAudioBytes(value) {
|
|
937
|
+
if (value instanceof Uint8Array) {
|
|
938
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
939
|
+
}
|
|
940
|
+
if (value instanceof ArrayBuffer) {
|
|
941
|
+
return new Uint8Array(value);
|
|
942
|
+
}
|
|
943
|
+
if (ArrayBuffer.isView(value)) {
|
|
944
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
945
|
+
}
|
|
946
|
+
throw new Error("TEXT_TO_SPEECH returned a non-binary payload");
|
|
947
|
+
}
|
|
948
|
+
function sniffAudioContentType(bytes) {
|
|
949
|
+
if (bytes.length >= 12 && bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 65 && bytes[10] === 86 && bytes[11] === 69) {
|
|
950
|
+
return "audio/wav";
|
|
951
|
+
}
|
|
952
|
+
if (bytes.length >= 3 && bytes[0] === 73 && bytes[1] === 68 && bytes[2] === 51) {
|
|
953
|
+
return "audio/mpeg";
|
|
954
|
+
}
|
|
955
|
+
if (bytes.length >= 2 && bytes[0] === 255 && (bytes[1] & 224) === 224) {
|
|
956
|
+
return "audio/mpeg";
|
|
957
|
+
}
|
|
958
|
+
return "application/octet-stream";
|
|
959
|
+
}
|
|
960
|
+
function optionalString(value) {
|
|
961
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
962
|
+
}
|
|
963
|
+
function optionalPositiveNumber(value) {
|
|
964
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
965
|
+
}
|
|
966
|
+
function jsonResponse(status, body) {
|
|
967
|
+
const text = JSON.stringify(body);
|
|
968
|
+
return {
|
|
969
|
+
status,
|
|
970
|
+
statusText: statusTextForCode(status),
|
|
971
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
972
|
+
body: text,
|
|
973
|
+
bodyBase64: Buffer.from(text, "utf8").toString("base64"),
|
|
974
|
+
bodyEncoding: "utf-8"
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function bytesResponse(status, bytes, headers) {
|
|
978
|
+
const body = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
979
|
+
return {
|
|
980
|
+
status,
|
|
981
|
+
statusText: statusTextForCode(status),
|
|
982
|
+
headers,
|
|
983
|
+
body: body.toString("utf8"),
|
|
984
|
+
bodyBase64: body.toString("base64"),
|
|
985
|
+
bodyEncoding: "utf-8"
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function runtimeAgentName(runtime) {
|
|
989
|
+
const character = runtime.character;
|
|
990
|
+
return typeof character?.name === "string" && character.name.trim() ? character.name.trim() : "Eliza";
|
|
991
|
+
}
|
|
992
|
+
function parseRequestBody(payload) {
|
|
993
|
+
const raw = payloadBodyAsRaw(payload);
|
|
994
|
+
if (!raw) return {};
|
|
995
|
+
if (Buffer.isBuffer(raw)) {
|
|
996
|
+
const parsed = parseJsonBody(raw.toString("utf8"));
|
|
997
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
998
|
+
}
|
|
999
|
+
if (typeof raw === "string") {
|
|
1000
|
+
const parsed = parseJsonBody(raw);
|
|
1001
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1002
|
+
}
|
|
1003
|
+
return typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
1004
|
+
}
|
|
1005
|
+
function createIosConversation(backend, input = {}) {
|
|
1006
|
+
const id = crypto.randomUUID();
|
|
1007
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
1008
|
+
const metadata = input.metadata && typeof input.metadata === "object" && !Array.isArray(input.metadata) ? input.metadata : void 0;
|
|
1009
|
+
const conversation = {
|
|
1010
|
+
id,
|
|
1011
|
+
title: typeof input.title === "string" && input.title.trim() ? input.title.trim() : "New Chat",
|
|
1012
|
+
roomId: stringToUuid(`ios-conv-${id}`),
|
|
1013
|
+
createdAt: now2,
|
|
1014
|
+
updatedAt: now2,
|
|
1015
|
+
...metadata ? { metadata } : {}
|
|
1016
|
+
};
|
|
1017
|
+
backend.conversations.set(id, conversation);
|
|
1018
|
+
return conversation;
|
|
1019
|
+
}
|
|
1020
|
+
function installHostCallProtocol(write2) {
|
|
1021
|
+
hostProtocolWrite = write2;
|
|
1022
|
+
}
|
|
1023
|
+
function tryHandleHostResultLine(line) {
|
|
1024
|
+
if (!line.includes('"host_result"')) return false;
|
|
1025
|
+
let parsed;
|
|
1026
|
+
try {
|
|
1027
|
+
parsed = JSON.parse(line);
|
|
1028
|
+
} catch {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
if (parsed.type !== "host_result" || typeof parsed.id !== "string") {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
const pending = pendingHostCalls.get(parsed.id);
|
|
1035
|
+
if (!pending) return true;
|
|
1036
|
+
pendingHostCalls.delete(parsed.id);
|
|
1037
|
+
clearTimeout(pending.timeout);
|
|
1038
|
+
const envelope = parsed.envelope && typeof parsed.envelope === "object" ? parsed.envelope : parsed;
|
|
1039
|
+
if (envelope.ok === false) {
|
|
1040
|
+
pending.reject(
|
|
1041
|
+
new Error(
|
|
1042
|
+
typeof envelope.error === "string" ? envelope.error : "Native host call failed"
|
|
1043
|
+
)
|
|
1044
|
+
);
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
pending.resolve(envelope.result);
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
function callIosHost(method, payload, timeoutMs = 12e4) {
|
|
1051
|
+
const writeHostMessage = hostProtocolWrite;
|
|
1052
|
+
if (!writeHostMessage) {
|
|
1053
|
+
return Promise.reject(
|
|
1054
|
+
new Error("iOS native host-call protocol is not installed")
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
const id = `host-${nextHostCallId++}`;
|
|
1058
|
+
const boundedTimeout = Math.max(1e3, Math.min(timeoutMs, 30 * 6e4));
|
|
1059
|
+
return new Promise((resolve, reject) => {
|
|
1060
|
+
const timeout = setTimeout(() => {
|
|
1061
|
+
pendingHostCalls.delete(id);
|
|
1062
|
+
reject(
|
|
1063
|
+
new Error(
|
|
1064
|
+
`Native iOS host call ${method} timed out after ${boundedTimeout}ms`
|
|
1065
|
+
)
|
|
1066
|
+
);
|
|
1067
|
+
}, boundedTimeout);
|
|
1068
|
+
pendingHostCalls.set(id, { resolve, reject, timeout });
|
|
1069
|
+
writeHostMessage({
|
|
1070
|
+
type: "host_call",
|
|
1071
|
+
id,
|
|
1072
|
+
method,
|
|
1073
|
+
payload,
|
|
1074
|
+
timeoutMs: boundedTimeout
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
function resolveMobileStateDir() {
|
|
1079
|
+
const explicit = process2.env.ELIZA_STATE_DIR || process2.env.ELIZA_STATE_DIR || process2.env.ELIZA_HOME;
|
|
1080
|
+
if (explicit?.trim()) return explicit.trim();
|
|
1081
|
+
if (process2.env.HOME?.trim()) {
|
|
1082
|
+
return path.join(process2.env.HOME.trim(), ".eliza");
|
|
1083
|
+
}
|
|
1084
|
+
return "/tmp/eliza";
|
|
1085
|
+
}
|
|
1086
|
+
function localInferenceRootPath() {
|
|
1087
|
+
return path.join(resolveMobileStateDir(), "local-inference");
|
|
1088
|
+
}
|
|
1089
|
+
function localInferenceRegistryPath() {
|
|
1090
|
+
return path.join(localInferenceRootPath(), "registry.json");
|
|
1091
|
+
}
|
|
1092
|
+
function localInferenceAssignmentsPath() {
|
|
1093
|
+
return path.join(localInferenceRootPath(), "assignments.json");
|
|
1094
|
+
}
|
|
1095
|
+
function localInferenceRoutingPath() {
|
|
1096
|
+
return path.join(localInferenceRootPath(), "routing.json");
|
|
1097
|
+
}
|
|
1098
|
+
function localInferenceModelsPath() {
|
|
1099
|
+
return path.join(localInferenceRootPath(), "models");
|
|
1100
|
+
}
|
|
1101
|
+
function bundledLocalInferenceModelsPath() {
|
|
1102
|
+
const assetDir = process2.env.ELIZA_IOS_AGENT_ASSET_DIR?.trim();
|
|
1103
|
+
if (!assetDir) return null;
|
|
1104
|
+
return path.join(assetDir, "models");
|
|
1105
|
+
}
|
|
1106
|
+
function localInferenceDownloadsPath() {
|
|
1107
|
+
return path.join(localInferenceRootPath(), "downloads");
|
|
1108
|
+
}
|
|
1109
|
+
function readJsonObjectFile(filePath) {
|
|
1110
|
+
try {
|
|
1111
|
+
const parsed = JSON.parse(readFileSync2(filePath, "utf8"));
|
|
1112
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1113
|
+
} catch {
|
|
1114
|
+
return {};
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function writeJsonObjectFile(filePath, value) {
|
|
1118
|
+
mkdirSync2(path.dirname(filePath), { recursive: true });
|
|
1119
|
+
writeFileSync2(filePath, `${JSON.stringify(value, null, 2)}
|
|
1120
|
+
`, "utf8");
|
|
1121
|
+
}
|
|
1122
|
+
function iosNativeCatalogById(modelId) {
|
|
1123
|
+
return IOS_NATIVE_CATALOG_MODELS.find((entry) => entry.id === modelId) ?? null;
|
|
1124
|
+
}
|
|
1125
|
+
function iosNativeCatalogByFile(filePath) {
|
|
1126
|
+
const normalized = filePath.replaceAll("\\", "/").toLowerCase();
|
|
1127
|
+
return IOS_NATIVE_CATALOG_MODELS.find((entry) => {
|
|
1128
|
+
const target = entry.ggufFile.toLowerCase();
|
|
1129
|
+
return normalized.endsWith(`/${target}`) || path.basename(normalized) === path.basename(target);
|
|
1130
|
+
}) ?? null;
|
|
1131
|
+
}
|
|
1132
|
+
function nativeCatalogModelPayload(model) {
|
|
1133
|
+
return {
|
|
1134
|
+
id: model.id,
|
|
1135
|
+
displayName: model.displayName,
|
|
1136
|
+
hfRepo: model.hfRepo,
|
|
1137
|
+
ggufFile: model.ggufFile,
|
|
1138
|
+
params: model.params,
|
|
1139
|
+
quant: "Q8_0",
|
|
1140
|
+
sizeGb: model.sizeGb,
|
|
1141
|
+
minRamGb: model.minRamGb,
|
|
1142
|
+
category: "chat",
|
|
1143
|
+
bucket: model.bucket,
|
|
1144
|
+
blurb: "Eliza-1 on-device GGUF bundle for iPhone local inference.",
|
|
1145
|
+
contextLength: model.contextLength,
|
|
1146
|
+
gpuLayers: "auto",
|
|
1147
|
+
publishStatus: "published",
|
|
1148
|
+
sourceModel: {
|
|
1149
|
+
finetuned: false,
|
|
1150
|
+
components: {
|
|
1151
|
+
text: { repo: model.hfRepo, file: model.hfPath }
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
function modelDownloadUrl(model) {
|
|
1157
|
+
const encodedPath = model.hfPath.split("/").map((part) => encodeURIComponent(part)).join("/");
|
|
1158
|
+
return `https://huggingface.co/${model.hfRepo}/resolve/main/${encodedPath}`;
|
|
1159
|
+
}
|
|
1160
|
+
function nativeModelTargetPath(model) {
|
|
1161
|
+
return path.join(localInferenceModelsPath(), model.ggufFile);
|
|
1162
|
+
}
|
|
1163
|
+
function installedModelForCatalogEntry(model, filePath, sizeBytes, installedAt, source) {
|
|
1164
|
+
return {
|
|
1165
|
+
id: model.id,
|
|
1166
|
+
displayName: model.displayName,
|
|
1167
|
+
path: filePath,
|
|
1168
|
+
sizeBytes,
|
|
1169
|
+
installedAt,
|
|
1170
|
+
lastUsedAt: null,
|
|
1171
|
+
source,
|
|
1172
|
+
hfRepo: model.hfRepo,
|
|
1173
|
+
bundleVerifiedAt: installedAt
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
function upsertInstalledModel(model) {
|
|
1177
|
+
const existing = readInstalledModels().filter(
|
|
1178
|
+
(entry) => entry.id !== model.id && entry.path !== model.path
|
|
1179
|
+
);
|
|
1180
|
+
writeJsonObjectFile(localInferenceRegistryPath(), {
|
|
1181
|
+
version: 1,
|
|
1182
|
+
models: [...existing, model],
|
|
1183
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
function positiveInteger(value) {
|
|
1187
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
1188
|
+
return Math.floor(value);
|
|
1189
|
+
}
|
|
1190
|
+
if (typeof value === "string" && value.trim()) {
|
|
1191
|
+
const parsed = Number(value);
|
|
1192
|
+
if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
|
|
1193
|
+
}
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
function readAssignments() {
|
|
1197
|
+
const parsed = readJsonObjectFile(localInferenceAssignmentsPath());
|
|
1198
|
+
const raw = parsed.assignments;
|
|
1199
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
1200
|
+
const out = {};
|
|
1201
|
+
for (const [slot, modelId] of Object.entries(raw)) {
|
|
1202
|
+
if (typeof modelId === "string" && modelId.trim()) {
|
|
1203
|
+
out[slot] = modelId.trim();
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return out;
|
|
1207
|
+
}
|
|
1208
|
+
function writeAssignments(assignments) {
|
|
1209
|
+
writeJsonObjectFile(localInferenceAssignmentsPath(), {
|
|
1210
|
+
version: 1,
|
|
1211
|
+
assignments,
|
|
1212
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
function scanGgufFiles(root) {
|
|
1216
|
+
const models = [];
|
|
1217
|
+
const visit = (dir, depth) => {
|
|
1218
|
+
if (depth > 5 || models.length >= 200) return;
|
|
1219
|
+
let entries = [];
|
|
1220
|
+
try {
|
|
1221
|
+
entries = readdirSync2(dir);
|
|
1222
|
+
} catch {
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
for (const entry of entries) {
|
|
1226
|
+
const fullPath = path.join(dir, entry);
|
|
1227
|
+
let stats;
|
|
1228
|
+
try {
|
|
1229
|
+
stats = statSync2(fullPath);
|
|
1230
|
+
} catch {
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
if (stats.isDirectory()) {
|
|
1234
|
+
visit(fullPath, depth + 1);
|
|
1235
|
+
} else if (stats.isFile() && entry.toLowerCase().endsWith(".gguf")) {
|
|
1236
|
+
const catalogModel = iosNativeCatalogByFile(fullPath);
|
|
1237
|
+
if (catalogModel) {
|
|
1238
|
+
models.push(
|
|
1239
|
+
installedModelForCatalogEntry(
|
|
1240
|
+
catalogModel,
|
|
1241
|
+
fullPath,
|
|
1242
|
+
stats.size,
|
|
1243
|
+
new Date(stats.mtimeMs).toISOString(),
|
|
1244
|
+
"ios-bundled"
|
|
1245
|
+
)
|
|
1246
|
+
);
|
|
1247
|
+
} else {
|
|
1248
|
+
const id = path.basename(entry, path.extname(entry));
|
|
1249
|
+
models.push({
|
|
1250
|
+
id,
|
|
1251
|
+
displayName: id,
|
|
1252
|
+
path: fullPath,
|
|
1253
|
+
sizeBytes: stats.size,
|
|
1254
|
+
installedAt: new Date(stats.mtimeMs).toISOString(),
|
|
1255
|
+
lastUsedAt: null,
|
|
1256
|
+
source: "external-scan"
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
visit(root, 0);
|
|
1263
|
+
return models;
|
|
1264
|
+
}
|
|
1265
|
+
function normalizeInstalledModelPath(rawPath) {
|
|
1266
|
+
const trimmed = rawPath.trim();
|
|
1267
|
+
if (!trimmed || trimmed.includes("\0")) return null;
|
|
1268
|
+
const currentRoot = localInferenceRootPath();
|
|
1269
|
+
const candidates = /* @__PURE__ */ new Set([
|
|
1270
|
+
trimmed,
|
|
1271
|
+
trimmed.replace(/^\/private\/var\//, "/var/")
|
|
1272
|
+
]);
|
|
1273
|
+
const marker = "/local-inference/";
|
|
1274
|
+
const markerIndex = trimmed.indexOf(marker);
|
|
1275
|
+
if (markerIndex >= 0) {
|
|
1276
|
+
const relativePath = trimmed.slice(markerIndex + marker.length);
|
|
1277
|
+
if (relativePath && !relativePath.startsWith("/") && !relativePath.split(/[\\/]+/).includes("..")) {
|
|
1278
|
+
candidates.add(path.join(currentRoot, relativePath));
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
for (const candidate of candidates) {
|
|
1282
|
+
try {
|
|
1283
|
+
if (existsSync2(candidate)) return candidate;
|
|
1284
|
+
} catch {
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
function readInstalledModels() {
|
|
1290
|
+
const parsed = readJsonObjectFile(localInferenceRegistryPath());
|
|
1291
|
+
const rawModels = Array.isArray(parsed.models) ? parsed.models : [];
|
|
1292
|
+
const fromRegistry = rawModels.map((entry) => {
|
|
1293
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
const record = entry;
|
|
1297
|
+
if (typeof record.id !== "string" || typeof record.path !== "string") {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
const modelPath = normalizeInstalledModelPath(record.path);
|
|
1301
|
+
if (!modelPath) return null;
|
|
1302
|
+
return {
|
|
1303
|
+
id: record.id,
|
|
1304
|
+
displayName: typeof record.displayName === "string" ? record.displayName : record.id,
|
|
1305
|
+
path: modelPath,
|
|
1306
|
+
sizeBytes: positiveInteger(record.sizeBytes) ?? 0,
|
|
1307
|
+
installedAt: typeof record.installedAt === "string" ? record.installedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1308
|
+
lastUsedAt: typeof record.lastUsedAt === "string" ? record.lastUsedAt : null,
|
|
1309
|
+
source: typeof record.source === "string" ? record.source : void 0,
|
|
1310
|
+
hfRepo: typeof record.hfRepo === "string" ? record.hfRepo : void 0,
|
|
1311
|
+
bundleVerifiedAt: typeof record.bundleVerifiedAt === "string" ? record.bundleVerifiedAt : void 0,
|
|
1312
|
+
dimensions: positiveInteger(record.dimensions) ?? void 0,
|
|
1313
|
+
embeddingDimension: positiveInteger(record.embeddingDimension) ?? void 0,
|
|
1314
|
+
embeddingDimensions: positiveInteger(record.embeddingDimensions) ?? void 0
|
|
1315
|
+
};
|
|
1316
|
+
}).filter((entry) => Boolean(entry));
|
|
1317
|
+
const bundledModelsPath = bundledLocalInferenceModelsPath();
|
|
1318
|
+
const scanned = [
|
|
1319
|
+
...bundledModelsPath ? scanGgufFiles(bundledModelsPath) : [],
|
|
1320
|
+
...scanGgufFiles(localInferenceModelsPath())
|
|
1321
|
+
];
|
|
1322
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1323
|
+
for (const model of [...scanned, ...fromRegistry]) {
|
|
1324
|
+
byId.set(model.id, model);
|
|
1325
|
+
}
|
|
1326
|
+
return [...byId.values()];
|
|
1327
|
+
}
|
|
1328
|
+
function isEmbeddingModel(model) {
|
|
1329
|
+
const lowered = model.id.toLowerCase();
|
|
1330
|
+
return lowered.includes("embed") || lowered.includes("bge-") || lowered.includes("nomic") || lowered.includes("gte-") || lowered.includes("e5-");
|
|
1331
|
+
}
|
|
1332
|
+
function resolveAssignedModel(slot) {
|
|
1333
|
+
const installed = readInstalledModels().filter(
|
|
1334
|
+
(model) => !isEmbeddingModel(model)
|
|
1335
|
+
);
|
|
1336
|
+
const assignments = readAssignments();
|
|
1337
|
+
const assigned = assignments[slot];
|
|
1338
|
+
if (assigned) {
|
|
1339
|
+
const model = installed.find((entry) => entry.id === assigned);
|
|
1340
|
+
if (model) return model;
|
|
1341
|
+
}
|
|
1342
|
+
if (nativeLlamaState.modelPath) {
|
|
1343
|
+
const current = installed.find(
|
|
1344
|
+
(entry) => entry.path === nativeLlamaState.modelPath
|
|
1345
|
+
);
|
|
1346
|
+
if (current) return current;
|
|
1347
|
+
}
|
|
1348
|
+
return installed.sort((left, right) => {
|
|
1349
|
+
const leftUsed = left.lastUsedAt ? Date.parse(left.lastUsedAt) : 0;
|
|
1350
|
+
const rightUsed = right.lastUsedAt ? Date.parse(right.lastUsedAt) : 0;
|
|
1351
|
+
if (rightUsed !== leftUsed) return rightUsed - leftUsed;
|
|
1352
|
+
return (right.sizeBytes ?? 0) - (left.sizeBytes ?? 0);
|
|
1353
|
+
})[0] ?? null;
|
|
1354
|
+
}
|
|
1355
|
+
function nativeLlamaContextSize() {
|
|
1356
|
+
return positiveInteger(process2.env.ELIZA_IOS_LLAMA_CONTEXT_SIZE) ?? positiveInteger(process2.env.ELIZA_IOS_LLAMA_CONTEXT_SIZE) ?? positiveInteger(process2.env.ELIZA_LOCAL_CONTEXT_SIZE) ?? 4096;
|
|
1357
|
+
}
|
|
1358
|
+
function isMetalLoadError(error) {
|
|
1359
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1360
|
+
return /metal/i.test(message) || /MTLLibraryErrorDomain/.test(message) || /ggml_metal/i.test(message);
|
|
1361
|
+
}
|
|
1362
|
+
async function shouldUseNativeLlamaGpu() {
|
|
1363
|
+
const explicit = process2.env.ELIZA_IOS_LLAMA_USE_GPU;
|
|
1364
|
+
if (explicit === "1" || explicit?.toLowerCase() === "true") return true;
|
|
1365
|
+
if (explicit === "0" || explicit?.toLowerCase() === "false") return false;
|
|
1366
|
+
const hardware = await nativeHardwareInfo();
|
|
1367
|
+
return hardware.metal_supported === true && hardware.is_simulator !== true;
|
|
1368
|
+
}
|
|
1369
|
+
async function loadNativeLlamaModel(model, useGpu) {
|
|
1370
|
+
const result = await callIosHost(
|
|
1371
|
+
"llama_load_model",
|
|
1372
|
+
{
|
|
1373
|
+
path: model.path,
|
|
1374
|
+
modelId: model.id,
|
|
1375
|
+
context_size: nativeLlamaContextSize(),
|
|
1376
|
+
use_gpu: useGpu
|
|
1377
|
+
},
|
|
1378
|
+
10 * 6e4
|
|
1379
|
+
);
|
|
1380
|
+
return result && typeof result === "object" && !Array.isArray(result) ? result : {};
|
|
1381
|
+
}
|
|
1382
|
+
async function ensureNativeModelLoaded(slot) {
|
|
1383
|
+
const model = resolveAssignedModel(slot);
|
|
1384
|
+
if (!model) {
|
|
1385
|
+
throw new Error(
|
|
1386
|
+
`[ios-native-llama] No local GGUF model is installed under ${path.join(
|
|
1387
|
+
localInferenceRootPath(),
|
|
1388
|
+
"models"
|
|
1389
|
+
)}. Download or install a model before using local generation.`
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
if (nativeLlamaState.contextId != null && nativeLlamaState.modelPath === model.path && nativeLlamaState.status === "ready") {
|
|
1393
|
+
return nativeLlamaState;
|
|
1394
|
+
}
|
|
1395
|
+
await unloadNativeLlamaModel();
|
|
1396
|
+
nativeLlamaState.status = "loading";
|
|
1397
|
+
nativeLlamaState.modelId = model.id;
|
|
1398
|
+
nativeLlamaState.modelPath = model.path;
|
|
1399
|
+
nativeLlamaState.loadedAt = null;
|
|
1400
|
+
delete nativeLlamaState.error;
|
|
1401
|
+
try {
|
|
1402
|
+
let requestedGpu = await shouldUseNativeLlamaGpu();
|
|
1403
|
+
let record;
|
|
1404
|
+
try {
|
|
1405
|
+
record = await loadNativeLlamaModel(model, requestedGpu);
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
if (!requestedGpu || !isMetalLoadError(error)) throw error;
|
|
1408
|
+
requestedGpu = false;
|
|
1409
|
+
record = await loadNativeLlamaModel(model, false);
|
|
1410
|
+
}
|
|
1411
|
+
const contextId = positiveInteger(record.context_id) ?? positiveInteger(record.contextId);
|
|
1412
|
+
if (contextId == null) {
|
|
1413
|
+
throw new Error("Native llama load returned no context_id");
|
|
1414
|
+
}
|
|
1415
|
+
nativeLlamaState.contextId = contextId;
|
|
1416
|
+
nativeLlamaState.loadedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1417
|
+
nativeLlamaState.status = "ready";
|
|
1418
|
+
return nativeLlamaState;
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
nativeLlamaState.contextId = null;
|
|
1421
|
+
nativeLlamaState.loadedAt = null;
|
|
1422
|
+
nativeLlamaState.status = "error";
|
|
1423
|
+
nativeLlamaState.error = error instanceof Error ? error.message : String(error);
|
|
1424
|
+
throw error;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
async function unloadNativeLlamaModel() {
|
|
1428
|
+
const contextId = nativeLlamaState.contextId;
|
|
1429
|
+
nativeLlamaState.contextId = null;
|
|
1430
|
+
nativeLlamaState.loadedAt = null;
|
|
1431
|
+
nativeLlamaState.status = "idle";
|
|
1432
|
+
if (contextId != null) {
|
|
1433
|
+
await callIosHost("llama_free", { context_id: contextId }, 3e4).catch(
|
|
1434
|
+
() => void 0
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
nativeLlamaState.modelId = null;
|
|
1438
|
+
nativeLlamaState.modelPath = null;
|
|
1439
|
+
delete nativeLlamaState.error;
|
|
1440
|
+
}
|
|
1441
|
+
function flattenChatParamsForPrompt(params) {
|
|
1442
|
+
if (typeof params.prompt === "string" && params.prompt.length > 0) {
|
|
1443
|
+
return renderChatMlPrompt(IOS_NATIVE_NO_THINK_SYSTEM, [
|
|
1444
|
+
{ role: "user", content: params.prompt }
|
|
1445
|
+
]);
|
|
1446
|
+
}
|
|
1447
|
+
const systemBlocks = [IOS_NATIVE_NO_THINK_SYSTEM];
|
|
1448
|
+
if (typeof params.system === "string" && params.system) {
|
|
1449
|
+
systemBlocks.push(params.system);
|
|
1450
|
+
}
|
|
1451
|
+
const chatMessages = [];
|
|
1452
|
+
const messages = params.messages ?? [];
|
|
1453
|
+
for (const message of messages) {
|
|
1454
|
+
const role = message.role === "system" || message.role === "assistant" || message.role === "tool" ? message.role : "user";
|
|
1455
|
+
if (typeof message.content === "string") {
|
|
1456
|
+
if (message.content) {
|
|
1457
|
+
if (role === "system") systemBlocks.push(message.content);
|
|
1458
|
+
else chatMessages.push({ role, content: message.content });
|
|
1459
|
+
}
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
if (Array.isArray(message.content)) {
|
|
1463
|
+
const text = message.content.map(
|
|
1464
|
+
(part) => part && typeof part === "object" && "text" in part ? String(part.text ?? "") : ""
|
|
1465
|
+
).filter(Boolean).join("\n");
|
|
1466
|
+
if (text) {
|
|
1467
|
+
if (role === "system") systemBlocks.push(text);
|
|
1468
|
+
else chatMessages.push({ role, content: text });
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
return renderChatMlPrompt(systemBlocks.join("\n\n"), chatMessages);
|
|
1473
|
+
}
|
|
1474
|
+
function renderChatMlPrompt(system, messages) {
|
|
1475
|
+
const blocks = [`<|im_start|>system
|
|
1476
|
+
${system.trim()}
|
|
1477
|
+
<|im_end|>`];
|
|
1478
|
+
for (const message of messages) {
|
|
1479
|
+
const role = message.role === "assistant" || message.role === "tool" ? message.role : "user";
|
|
1480
|
+
const content = message.content.trim();
|
|
1481
|
+
if (content) blocks.push(`<|im_start|>${role}
|
|
1482
|
+
${content}
|
|
1483
|
+
<|im_end|>`);
|
|
1484
|
+
}
|
|
1485
|
+
blocks.push("<|im_start|>assistant\n<think>\n\n</think>\n\n");
|
|
1486
|
+
return blocks.join("\n");
|
|
1487
|
+
}
|
|
1488
|
+
function stripReasoningBlocks(raw) {
|
|
1489
|
+
return raw.replace(/<think\b[^>]*>[\s\S]*?<\/think>/gi, "").replace(/^[\s\S]*?<\/think>/i, "").replace(/<think\b[^>]*>[\s\S]*$/gi, "").replace(/\/?\bno_think\b/gi, "").trim();
|
|
1490
|
+
}
|
|
1491
|
+
function cleanIosNativeConversationReply(raw) {
|
|
1492
|
+
const withoutTokens = stripReasoningBlocks(raw).split("<|im_end|>")[0].split("<|im_start|>")[0].replace(/^\s*(assistant|eliza)\s*:\s*/i, "").trim();
|
|
1493
|
+
const compact = withoutTokens.replace(/\s+/g, " ").trim();
|
|
1494
|
+
if (!compact) return "";
|
|
1495
|
+
const firstSentence = compact.match(/^(.{12,280}?[.!?])(?:\s|$)/u)?.[1];
|
|
1496
|
+
return (firstSentence ?? compact).trim();
|
|
1497
|
+
}
|
|
1498
|
+
async function maybeGenerateIosNativeConversationReply(runtime, prompt) {
|
|
1499
|
+
const installed = readInstalledModels().filter(
|
|
1500
|
+
(model) => !isEmbeddingModel(model)
|
|
1501
|
+
);
|
|
1502
|
+
if (installed.length === 0) return null;
|
|
1503
|
+
const startedAt = Date.now();
|
|
1504
|
+
const raw = await runtime.useModel(
|
|
1505
|
+
ModelType.TEXT_SMALL,
|
|
1506
|
+
{
|
|
1507
|
+
messages: [
|
|
1508
|
+
{
|
|
1509
|
+
role: "system",
|
|
1510
|
+
content: "Eliza-1 is running locally on this iPhone. Reply with one natural sentence under 10 words."
|
|
1511
|
+
},
|
|
1512
|
+
{ role: "user", content: prompt }
|
|
1513
|
+
],
|
|
1514
|
+
maxTokens: 32,
|
|
1515
|
+
temperature: 0,
|
|
1516
|
+
stopSequences: ["<|im_end|>", "<|im_start|>"]
|
|
1517
|
+
},
|
|
1518
|
+
IOS_NATIVE_LLAMA_PROVIDER
|
|
1519
|
+
);
|
|
1520
|
+
const text = cleanIosNativeConversationReply(raw);
|
|
1521
|
+
if (!text) {
|
|
1522
|
+
throw new Error("Native llama returned an empty response");
|
|
1523
|
+
}
|
|
1524
|
+
return {
|
|
1525
|
+
text,
|
|
1526
|
+
reply: text,
|
|
1527
|
+
localInference: {
|
|
1528
|
+
provider: IOS_NATIVE_LLAMA_PROVIDER,
|
|
1529
|
+
modelId: nativeLlamaState.modelId ?? installed[0]?.id ?? null,
|
|
1530
|
+
mode: "ios_native_conversation",
|
|
1531
|
+
latencyMs: Date.now() - startedAt
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
function isStructuredGenerationSlot(slot) {
|
|
1536
|
+
return slot === ModelType.RESPONSE_HANDLER || slot === ModelType.ACTION_PLANNER || slot === ModelType.TEXT_COMPLETION;
|
|
1537
|
+
}
|
|
1538
|
+
function mergeStopSequences(values) {
|
|
1539
|
+
const requested = Array.isArray(values) ? values.filter((value) => typeof value === "string") : [];
|
|
1540
|
+
return Array.from(/* @__PURE__ */ new Set([...requested, "<|im_end|>", "<|endoftext|>"]));
|
|
1541
|
+
}
|
|
1542
|
+
function makeIosNativeGenerateHandler(slot) {
|
|
1543
|
+
return async (_runtime, params) => {
|
|
1544
|
+
const state = await ensureNativeModelLoaded(slot);
|
|
1545
|
+
if (state.contextId == null) {
|
|
1546
|
+
throw new Error(
|
|
1547
|
+
"[ios-native-llama] model load did not produce a context"
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
const prompt = flattenChatParamsForPrompt(params);
|
|
1551
|
+
const structuredSlot = isStructuredGenerationSlot(slot);
|
|
1552
|
+
const requestedMaxTokens = positiveInteger(params.maxTokens) ?? 256;
|
|
1553
|
+
const maxTokens = Math.min(requestedMaxTokens, structuredSlot ? 256 : 128);
|
|
1554
|
+
const result = await callIosHost(
|
|
1555
|
+
"llama_generate",
|
|
1556
|
+
{
|
|
1557
|
+
context_id: state.contextId,
|
|
1558
|
+
prompt,
|
|
1559
|
+
max_tokens: maxTokens,
|
|
1560
|
+
temperature: typeof params.temperature === "number" ? params.temperature : structuredSlot ? 0.2 : 0.4,
|
|
1561
|
+
top_p: typeof params.topP === "number" ? params.topP : 0.95,
|
|
1562
|
+
top_k: positiveInteger(params.topK) ?? 40,
|
|
1563
|
+
stop: mergeStopSequences(params.stopSequences)
|
|
1564
|
+
},
|
|
1565
|
+
Math.max(12e4, maxTokens * 2e3)
|
|
1566
|
+
);
|
|
1567
|
+
const record = result && typeof result === "object" && !Array.isArray(result) ? result : {};
|
|
1568
|
+
const text = typeof record.text === "string" ? record.text : String(result ?? "");
|
|
1569
|
+
const cleanedText = stripReasoningBlocks(text);
|
|
1570
|
+
if (params.onStreamChunk && cleanedText) {
|
|
1571
|
+
await params.onStreamChunk(cleanedText, crypto.randomUUID(), cleanedText);
|
|
1572
|
+
}
|
|
1573
|
+
return cleanedText;
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
function installIosNativeLlamaHandlers(runtime) {
|
|
1577
|
+
const flagged = runtime;
|
|
1578
|
+
if (flagged.__iosNativeLlamaHandlersInstalled) return;
|
|
1579
|
+
const runtimeWithRegistration = runtime;
|
|
1580
|
+
if (typeof runtimeWithRegistration.registerModel !== "function") return;
|
|
1581
|
+
for (const modelType of TEXT_GENERATION_MODEL_TYPES) {
|
|
1582
|
+
runtimeWithRegistration.registerModel(
|
|
1583
|
+
modelType,
|
|
1584
|
+
makeIosNativeGenerateHandler(modelType),
|
|
1585
|
+
IOS_NATIVE_LLAMA_PROVIDER,
|
|
1586
|
+
IOS_NATIVE_LLAMA_PRIORITY
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
flagged.__iosNativeLlamaHandlersInstalled = true;
|
|
1590
|
+
}
|
|
1591
|
+
async function nativeHardwareInfo() {
|
|
1592
|
+
try {
|
|
1593
|
+
const result = await callIosHost("llama_hardware_info", {}, 1e4);
|
|
1594
|
+
return result && typeof result === "object" && !Array.isArray(result) ? result : {};
|
|
1595
|
+
} catch (error) {
|
|
1596
|
+
return {
|
|
1597
|
+
backend: "unknown",
|
|
1598
|
+
total_ram_gb: 0,
|
|
1599
|
+
available_ram_gb: 0,
|
|
1600
|
+
cpu_cores: 0,
|
|
1601
|
+
is_simulator: process2.env.SIMULATOR_DEVICE_NAME ? true : void 0,
|
|
1602
|
+
metal_supported: false,
|
|
1603
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
async function nativeLlamaDeviceStatus() {
|
|
1608
|
+
const hardware = await nativeHardwareInfo();
|
|
1609
|
+
const totalRamGb = Number(hardware.total_ram_gb ?? 0);
|
|
1610
|
+
const cpuCores = Number(hardware.cpu_cores ?? 0);
|
|
1611
|
+
const metalSupported = hardware.metal_supported === true;
|
|
1612
|
+
return {
|
|
1613
|
+
enabled: true,
|
|
1614
|
+
connected: true,
|
|
1615
|
+
transport: "bun-host-ipc",
|
|
1616
|
+
devices: [
|
|
1617
|
+
{
|
|
1618
|
+
deviceId: IOS_NATIVE_LLAMA_DEVICE_ID,
|
|
1619
|
+
capabilities: {
|
|
1620
|
+
platform: "ios",
|
|
1621
|
+
deviceModel: hardware.is_simulator === true ? "iOS Simulator" : "iOS Device",
|
|
1622
|
+
totalRamGb,
|
|
1623
|
+
cpuCores,
|
|
1624
|
+
gpu: {
|
|
1625
|
+
backend: "metal",
|
|
1626
|
+
available: metalSupported
|
|
1627
|
+
}
|
|
1628
|
+
},
|
|
1629
|
+
loadedPath: nativeLlamaState.modelPath,
|
|
1630
|
+
connectedSince: nativeLlamaState.loadedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1631
|
+
}
|
|
1632
|
+
],
|
|
1633
|
+
primaryDeviceId: IOS_NATIVE_LLAMA_DEVICE_ID,
|
|
1634
|
+
pendingRequests: pendingHostCalls.size,
|
|
1635
|
+
modelPath: nativeLlamaState.modelPath
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
function nativeLlamaActiveSnapshot() {
|
|
1639
|
+
return {
|
|
1640
|
+
modelId: nativeLlamaState.modelId,
|
|
1641
|
+
modelPath: nativeLlamaState.modelPath,
|
|
1642
|
+
loadedAt: nativeLlamaState.loadedAt,
|
|
1643
|
+
status: nativeLlamaState.status,
|
|
1644
|
+
provider: IOS_NATIVE_LLAMA_PROVIDER,
|
|
1645
|
+
transport: "bun-host-ipc",
|
|
1646
|
+
...nativeLlamaState.error ? { error: nativeLlamaState.error } : {}
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
async function nativeLocalInferenceProviders() {
|
|
1650
|
+
const installed = readInstalledModels();
|
|
1651
|
+
return {
|
|
1652
|
+
providers: [
|
|
1653
|
+
{
|
|
1654
|
+
id: IOS_NATIVE_LLAMA_PROVIDER,
|
|
1655
|
+
label: "Eliza-1 on-device runtime (iOS)",
|
|
1656
|
+
kind: "local",
|
|
1657
|
+
description: "Runs Eliza-1 natively through the full Bun host IPC bridge.",
|
|
1658
|
+
supportedSlots: ["TEXT_SMALL", "TEXT_LARGE"],
|
|
1659
|
+
configureHref: null,
|
|
1660
|
+
enableState: {
|
|
1661
|
+
enabled: true,
|
|
1662
|
+
reason: "Native iOS llama bridge connected"
|
|
1663
|
+
},
|
|
1664
|
+
registeredSlots: ["TEXT_SMALL", "TEXT_LARGE"],
|
|
1665
|
+
transport: "bun-host-ipc"
|
|
1666
|
+
},
|
|
1667
|
+
{
|
|
1668
|
+
id: "eliza-local-inference",
|
|
1669
|
+
label: "Eliza-1 local inference",
|
|
1670
|
+
kind: "local",
|
|
1671
|
+
description: "Eliza-1 bundles installed in this agent state directory.",
|
|
1672
|
+
supportedSlots: ["TEXT_SMALL", "TEXT_LARGE", "TEXT_EMBEDDING"],
|
|
1673
|
+
configureHref: "#local-inference-panel",
|
|
1674
|
+
enableState: {
|
|
1675
|
+
enabled: installed.length > 0,
|
|
1676
|
+
reason: installed.length > 0 ? "Eliza-1 bundle installed" : "No Eliza-1 bundle installed"
|
|
1677
|
+
},
|
|
1678
|
+
registeredSlots: installed.length > 0 ? ["TEXT_SMALL", "TEXT_LARGE"] : []
|
|
1679
|
+
}
|
|
1680
|
+
]
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
function nativeCatalogModels() {
|
|
1684
|
+
const curated = IOS_NATIVE_CATALOG_MODELS.map(nativeCatalogModelPayload);
|
|
1685
|
+
const curatedIds = new Set(
|
|
1686
|
+
IOS_NATIVE_CATALOG_MODELS.map((model) => model.id)
|
|
1687
|
+
);
|
|
1688
|
+
const installedCustom = readInstalledModels().filter((model) => !isEmbeddingModel(model) && !curatedIds.has(model.id)).map((model) => {
|
|
1689
|
+
const sizeGb = Math.max(0.1, (model.sizeBytes ?? 0) / 1024 ** 3);
|
|
1690
|
+
return {
|
|
1691
|
+
id: model.id,
|
|
1692
|
+
displayName: model.displayName ?? model.id,
|
|
1693
|
+
hfRepo: model.hfRepo ?? "elizaos/eliza-1",
|
|
1694
|
+
ggufFile: path.basename(model.path),
|
|
1695
|
+
params: "2B",
|
|
1696
|
+
quant: "Q8_0",
|
|
1697
|
+
sizeGb,
|
|
1698
|
+
minRamGb: 4,
|
|
1699
|
+
category: "chat",
|
|
1700
|
+
bucket: sizeGb <= 1 ? "small" : "mid",
|
|
1701
|
+
blurb: "Installed Eliza-1 on-device GGUF bundle.",
|
|
1702
|
+
contextLength: 128e3,
|
|
1703
|
+
gpuLayers: "auto",
|
|
1704
|
+
publishStatus: "published"
|
|
1705
|
+
};
|
|
1706
|
+
});
|
|
1707
|
+
return [...curated, ...installedCustom];
|
|
1708
|
+
}
|
|
1709
|
+
function nativeDownloadJobs() {
|
|
1710
|
+
const jobs = Array.from(nativeDownloadState.values());
|
|
1711
|
+
const trackedModelIds = new Set(jobs.map((job) => job.modelId));
|
|
1712
|
+
for (const model of readInstalledModels().filter(
|
|
1713
|
+
(entry) => !isEmbeddingModel(entry)
|
|
1714
|
+
)) {
|
|
1715
|
+
if (trackedModelIds.has(model.id)) continue;
|
|
1716
|
+
jobs.push(nativeDownloadJobForInstalledModel(model));
|
|
1717
|
+
}
|
|
1718
|
+
return jobs;
|
|
1719
|
+
}
|
|
1720
|
+
function nativeDownloadStatus(model) {
|
|
1721
|
+
const bytes = model?.sizeBytes ?? 0;
|
|
1722
|
+
return {
|
|
1723
|
+
state: model ? "completed" : "missing",
|
|
1724
|
+
receivedBytes: bytes,
|
|
1725
|
+
totalBytes: bytes,
|
|
1726
|
+
percent: model ? 100 : null,
|
|
1727
|
+
bytesPerSec: 0,
|
|
1728
|
+
etaMs: model ? 0 : null,
|
|
1729
|
+
updatedAt: model?.lastUsedAt ?? model?.installedAt ?? null,
|
|
1730
|
+
errors: []
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
function nativeDownloadJobForInstalledModel(model) {
|
|
1734
|
+
const installedAt = model.installedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1735
|
+
const updatedAt = model.lastUsedAt ?? model.bundleVerifiedAt ?? installedAt;
|
|
1736
|
+
const bytes = model.sizeBytes ?? 0;
|
|
1737
|
+
return {
|
|
1738
|
+
jobId: `installed:${model.id}`,
|
|
1739
|
+
modelId: model.id,
|
|
1740
|
+
state: "completed",
|
|
1741
|
+
received: bytes,
|
|
1742
|
+
total: bytes,
|
|
1743
|
+
bytesPerSec: 0,
|
|
1744
|
+
etaMs: 0,
|
|
1745
|
+
startedAt: installedAt,
|
|
1746
|
+
updatedAt
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
function updateNativeDownloadJob(modelId, patch) {
|
|
1750
|
+
const current = nativeDownloadState.get(modelId);
|
|
1751
|
+
if (!current) {
|
|
1752
|
+
throw new Error(`No native download job is registered for ${modelId}`);
|
|
1753
|
+
}
|
|
1754
|
+
const next = {
|
|
1755
|
+
...current,
|
|
1756
|
+
...patch,
|
|
1757
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1758
|
+
};
|
|
1759
|
+
nativeDownloadState.set(modelId, next);
|
|
1760
|
+
return next;
|
|
1761
|
+
}
|
|
1762
|
+
function writeDownloadChunk(writer, chunk) {
|
|
1763
|
+
return new Promise((resolve, reject) => {
|
|
1764
|
+
writer.write(Buffer.from(chunk), (error) => {
|
|
1765
|
+
if (error) reject(error);
|
|
1766
|
+
else resolve();
|
|
1767
|
+
});
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
function closeDownloadWriter(writer) {
|
|
1771
|
+
return new Promise((resolve, reject) => {
|
|
1772
|
+
const onError = (error) => {
|
|
1773
|
+
writer.off("error", onError);
|
|
1774
|
+
reject(error);
|
|
1775
|
+
};
|
|
1776
|
+
writer.once("error", onError);
|
|
1777
|
+
writer.end(() => {
|
|
1778
|
+
writer.off("error", onError);
|
|
1779
|
+
resolve();
|
|
1780
|
+
});
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
async function runNativeModelDownload(model) {
|
|
1784
|
+
const startedMs = Date.now();
|
|
1785
|
+
const totalEstimate = Math.round(model.sizeGb * 1024 ** 3);
|
|
1786
|
+
updateNativeDownloadJob(model.id, {
|
|
1787
|
+
state: "downloading",
|
|
1788
|
+
total: totalEstimate
|
|
1789
|
+
});
|
|
1790
|
+
mkdirSync2(path.dirname(nativeModelTargetPath(model)), { recursive: true });
|
|
1791
|
+
mkdirSync2(localInferenceDownloadsPath(), { recursive: true });
|
|
1792
|
+
const partialPath = path.join(
|
|
1793
|
+
localInferenceDownloadsPath(),
|
|
1794
|
+
`${model.id}.part`
|
|
1795
|
+
);
|
|
1796
|
+
const response = await fetch(modelDownloadUrl(model), { redirect: "follow" });
|
|
1797
|
+
if (!response.ok) {
|
|
1798
|
+
throw new Error(
|
|
1799
|
+
`Failed to download ${model.id}: HTTP ${response.status} ${response.statusText}`
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
if (!response.body) {
|
|
1803
|
+
throw new Error(`Failed to download ${model.id}: response body is empty`);
|
|
1804
|
+
}
|
|
1805
|
+
const contentLength = positiveInteger(response.headers.get("content-length")) ?? totalEstimate;
|
|
1806
|
+
updateNativeDownloadJob(model.id, { total: contentLength });
|
|
1807
|
+
const writer = createWriteStream2(partialPath);
|
|
1808
|
+
let received = 0;
|
|
1809
|
+
try {
|
|
1810
|
+
const reader = response.body.getReader();
|
|
1811
|
+
while (true) {
|
|
1812
|
+
const { done, value } = await reader.read();
|
|
1813
|
+
if (done) break;
|
|
1814
|
+
if (!value) continue;
|
|
1815
|
+
received += value.byteLength;
|
|
1816
|
+
await writeDownloadChunk(writer, value);
|
|
1817
|
+
const elapsedSeconds = Math.max(1, (Date.now() - startedMs) / 1e3);
|
|
1818
|
+
const bytesPerSec = Math.round(received / elapsedSeconds);
|
|
1819
|
+
const remaining = Math.max(0, contentLength - received);
|
|
1820
|
+
updateNativeDownloadJob(model.id, {
|
|
1821
|
+
received,
|
|
1822
|
+
bytesPerSec,
|
|
1823
|
+
etaMs: bytesPerSec > 0 ? Math.round(remaining / bytesPerSec * 1e3) : null
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
await closeDownloadWriter(writer);
|
|
1827
|
+
const targetPath = nativeModelTargetPath(model);
|
|
1828
|
+
renameSync2(partialPath, targetPath);
|
|
1829
|
+
const stats = statSync2(targetPath);
|
|
1830
|
+
const installedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1831
|
+
upsertInstalledModel(
|
|
1832
|
+
installedModelForCatalogEntry(
|
|
1833
|
+
model,
|
|
1834
|
+
targetPath,
|
|
1835
|
+
stats.size,
|
|
1836
|
+
installedAt,
|
|
1837
|
+
"eliza-download"
|
|
1838
|
+
)
|
|
1839
|
+
);
|
|
1840
|
+
updateNativeDownloadJob(model.id, {
|
|
1841
|
+
state: "completed",
|
|
1842
|
+
received: stats.size,
|
|
1843
|
+
total: stats.size,
|
|
1844
|
+
bytesPerSec: 0,
|
|
1845
|
+
etaMs: 0
|
|
1846
|
+
});
|
|
1847
|
+
} catch (error) {
|
|
1848
|
+
updateNativeDownloadJob(model.id, {
|
|
1849
|
+
state: "failed",
|
|
1850
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1851
|
+
});
|
|
1852
|
+
writer.destroy();
|
|
1853
|
+
rmSync2(partialPath, { force: true });
|
|
1854
|
+
throw error;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
function startNativeModelDownload(modelId) {
|
|
1858
|
+
const model = iosNativeCatalogById(modelId);
|
|
1859
|
+
if (!model) throw new Error(`Unsupported iOS local model: ${modelId}`);
|
|
1860
|
+
const installed = readInstalledModels().find(
|
|
1861
|
+
(entry) => entry.id === model.id
|
|
1862
|
+
);
|
|
1863
|
+
if (installed) {
|
|
1864
|
+
const job2 = nativeDownloadJobForInstalledModel(installed);
|
|
1865
|
+
nativeDownloadState.set(model.id, job2);
|
|
1866
|
+
return job2;
|
|
1867
|
+
}
|
|
1868
|
+
const existing = nativeDownloadState.get(model.id);
|
|
1869
|
+
if (existing && (existing.state === "queued" || existing.state === "downloading")) {
|
|
1870
|
+
return existing;
|
|
1871
|
+
}
|
|
1872
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
1873
|
+
const job = {
|
|
1874
|
+
jobId: `ios-download:${model.id}:${Date.now()}`,
|
|
1875
|
+
modelId: model.id,
|
|
1876
|
+
state: "queued",
|
|
1877
|
+
received: 0,
|
|
1878
|
+
total: Math.round(model.sizeGb * 1024 ** 3),
|
|
1879
|
+
bytesPerSec: 0,
|
|
1880
|
+
etaMs: null,
|
|
1881
|
+
startedAt: now2,
|
|
1882
|
+
updatedAt: now2
|
|
1883
|
+
};
|
|
1884
|
+
nativeDownloadState.set(model.id, job);
|
|
1885
|
+
void runNativeModelDownload(model).catch(() => {
|
|
1886
|
+
});
|
|
1887
|
+
return job;
|
|
1888
|
+
}
|
|
1889
|
+
function nativeTextReadiness() {
|
|
1890
|
+
const installed = readInstalledModels().filter(
|
|
1891
|
+
(model) => !isEmbeddingModel(model)
|
|
1892
|
+
);
|
|
1893
|
+
const assignments = readAssignments();
|
|
1894
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
1895
|
+
const slots = {};
|
|
1896
|
+
for (const slot of ["TEXT_SMALL", "TEXT_LARGE"]) {
|
|
1897
|
+
const assignedModelId = assignments[slot] ?? installed[0]?.id ?? null;
|
|
1898
|
+
const model = assignedModelId == null ? null : installed.find((entry) => entry.id === assignedModelId) ?? null;
|
|
1899
|
+
const active = model != null && nativeLlamaState.modelId === model.id && nativeLlamaState.status === "ready";
|
|
1900
|
+
const downloaded = model != null;
|
|
1901
|
+
slots[slot] = {
|
|
1902
|
+
slot,
|
|
1903
|
+
assigned: assignedModelId != null,
|
|
1904
|
+
assignedModelId,
|
|
1905
|
+
displayName: model?.displayName ?? model?.id ?? null,
|
|
1906
|
+
primaryDownloaded: downloaded,
|
|
1907
|
+
downloaded,
|
|
1908
|
+
active,
|
|
1909
|
+
ready: active,
|
|
1910
|
+
state: active ? "active" : downloaded ? "downloaded" : "missing",
|
|
1911
|
+
requiredModelIds: assignedModelId ? [assignedModelId] : [],
|
|
1912
|
+
missingModelIds: downloaded || !assignedModelId ? [] : [assignedModelId],
|
|
1913
|
+
installedBytes: model?.sizeBytes ?? 0,
|
|
1914
|
+
expectedBytes: model?.sizeBytes ?? 0,
|
|
1915
|
+
download: nativeDownloadStatus(model),
|
|
1916
|
+
errors: nativeLlamaState.error ? [nativeLlamaState.error] : []
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
return { updatedAt: now2, slots };
|
|
1920
|
+
}
|
|
1921
|
+
function hasNativeLocalTtsExecutor() {
|
|
1922
|
+
return hostProtocolWrite != null;
|
|
1923
|
+
}
|
|
1924
|
+
function hasNativeVoiceBundle(bundleDir) {
|
|
1925
|
+
const ttsDir = path.join(bundleDir, "tts");
|
|
1926
|
+
try {
|
|
1927
|
+
const coreml = path.join(ttsDir, "kokoro-coreml", "kokoro_5s.mlmodelc");
|
|
1928
|
+
const voice = path.join(ttsDir, "kokoro-coreml", "voices", "af_heart.json");
|
|
1929
|
+
if (statSync2(coreml).isDirectory() && statSync2(voice).isFile()) return true;
|
|
1930
|
+
} catch {
|
|
1931
|
+
}
|
|
1932
|
+
try {
|
|
1933
|
+
for (const entry of readdirSync2(ttsDir)) {
|
|
1934
|
+
if (/^omnivoice-base-.*\.gguf$/i.test(entry)) return true;
|
|
1935
|
+
if (entry === "kokoro") {
|
|
1936
|
+
try {
|
|
1937
|
+
for (const k of readdirSync2(path.join(ttsDir, "kokoro"))) {
|
|
1938
|
+
if (/\.gguf$/i.test(k) || k === "model_q4.onnx") return true;
|
|
1939
|
+
}
|
|
1940
|
+
} catch {
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
} catch {
|
|
1945
|
+
return false;
|
|
1946
|
+
}
|
|
1947
|
+
return false;
|
|
1948
|
+
}
|
|
1949
|
+
function nativeVoiceBundleDir() {
|
|
1950
|
+
const modelsRoots = [
|
|
1951
|
+
path.join(localInferenceRootPath(), "models"),
|
|
1952
|
+
bundledLocalInferenceModelsPath()
|
|
1953
|
+
].filter((root) => Boolean(root));
|
|
1954
|
+
let bundleDir = null;
|
|
1955
|
+
const visit = (dir, depth) => {
|
|
1956
|
+
if (depth > 6 || bundleDir) return;
|
|
1957
|
+
let entries = [];
|
|
1958
|
+
try {
|
|
1959
|
+
entries = readdirSync2(dir);
|
|
1960
|
+
} catch {
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
for (const entry of entries) {
|
|
1964
|
+
const fullPath = path.join(dir, entry);
|
|
1965
|
+
let stats;
|
|
1966
|
+
try {
|
|
1967
|
+
stats = statSync2(fullPath);
|
|
1968
|
+
} catch {
|
|
1969
|
+
continue;
|
|
1970
|
+
}
|
|
1971
|
+
if (stats.isDirectory()) {
|
|
1972
|
+
if (entry.endsWith(".bundle") && hasNativeVoiceBundle(fullPath)) {
|
|
1973
|
+
bundleDir = fullPath;
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
visit(fullPath, depth + 1);
|
|
1977
|
+
if (bundleDir) return;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
for (const root of modelsRoots) {
|
|
1982
|
+
visit(root, 0);
|
|
1983
|
+
if (bundleDir) return bundleDir;
|
|
1984
|
+
}
|
|
1985
|
+
return null;
|
|
1986
|
+
}
|
|
1987
|
+
function nativeVoiceReadiness() {
|
|
1988
|
+
const modelsRoots = [
|
|
1989
|
+
path.join(localInferenceRootPath(), "models"),
|
|
1990
|
+
bundledLocalInferenceModelsPath()
|
|
1991
|
+
].filter((root) => Boolean(root));
|
|
1992
|
+
let installedFiles = 0;
|
|
1993
|
+
let modelId = null;
|
|
1994
|
+
const visit = (dir, depth) => {
|
|
1995
|
+
if (depth > 6 || installedFiles > 0) return;
|
|
1996
|
+
let entries = [];
|
|
1997
|
+
try {
|
|
1998
|
+
entries = readdirSync2(dir);
|
|
1999
|
+
} catch {
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
for (const entry of entries) {
|
|
2003
|
+
const fullPath = path.join(dir, entry);
|
|
2004
|
+
let stats;
|
|
2005
|
+
try {
|
|
2006
|
+
stats = statSync2(fullPath);
|
|
2007
|
+
} catch {
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
if (stats.isDirectory()) {
|
|
2011
|
+
visit(fullPath, depth + 1);
|
|
2012
|
+
if (installedFiles > 0) return;
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
const normalized = fullPath.split(path.sep).join("/");
|
|
2016
|
+
if (stats.isFile() && /\/(tts|voice|asr|vad)\//i.test(normalized) && /\.(gguf|bin|json)$/i.test(entry)) {
|
|
2017
|
+
const markerIndex = normalized.indexOf(".bundle/");
|
|
2018
|
+
if (markerIndex >= 0) {
|
|
2019
|
+
const bundlePath = fullPath.slice(0, markerIndex + ".bundle".length);
|
|
2020
|
+
if (hasNativeVoiceBundle(bundlePath)) {
|
|
2021
|
+
installedFiles += 1;
|
|
2022
|
+
const match = normalized.match(/models\/([^/]+\.bundle)\//);
|
|
2023
|
+
modelId = match?.[1]?.replace(/\.bundle$/, "") ?? null;
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
for (const root of modelsRoots) {
|
|
2031
|
+
visit(root, 0);
|
|
2032
|
+
if (installedFiles > 0) break;
|
|
2033
|
+
}
|
|
2034
|
+
if (installedFiles > 0) {
|
|
2035
|
+
if (!hasNativeLocalTtsExecutor()) {
|
|
2036
|
+
return {
|
|
2037
|
+
status: "unavailable",
|
|
2038
|
+
installedFiles,
|
|
2039
|
+
modelId,
|
|
2040
|
+
message: "Eliza-1 voice assets are installed. This build is missing the iOS local voice playback engine."
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
return {
|
|
2044
|
+
status: "assets-ready",
|
|
2045
|
+
installedFiles,
|
|
2046
|
+
modelId,
|
|
2047
|
+
message: "Local voice assets are installed. Voice engine will warm on first playback."
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
return {
|
|
2051
|
+
status: "missing",
|
|
2052
|
+
installedFiles: 0,
|
|
2053
|
+
modelId: null,
|
|
2054
|
+
message: "Eliza-1 voice assets are not installed in this iOS build."
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
function routingPreferencesSnapshot() {
|
|
2058
|
+
const parsed = readJsonObjectFile(localInferenceRoutingPath());
|
|
2059
|
+
const preferences = parsed.preferences && typeof parsed.preferences === "object" ? parsed.preferences : { preferredProvider: {}, policy: {} };
|
|
2060
|
+
return {
|
|
2061
|
+
registrations: ["TEXT_SMALL", "TEXT_LARGE"].map((modelType) => ({
|
|
2062
|
+
modelType,
|
|
2063
|
+
provider: IOS_NATIVE_LLAMA_PROVIDER,
|
|
2064
|
+
priority: IOS_NATIVE_LLAMA_PRIORITY,
|
|
2065
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2066
|
+
})),
|
|
2067
|
+
preferences
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
async function nativeHubSnapshot() {
|
|
2071
|
+
const hardware = await nativeHardwareInfo();
|
|
2072
|
+
return {
|
|
2073
|
+
catalog: nativeCatalogModels(),
|
|
2074
|
+
installed: readInstalledModels(),
|
|
2075
|
+
active: nativeLlamaActiveSnapshot(),
|
|
2076
|
+
downloads: nativeDownloadJobs(),
|
|
2077
|
+
device: await nativeLlamaDeviceStatus(),
|
|
2078
|
+
providers: (await nativeLocalInferenceProviders()).providers,
|
|
2079
|
+
hardware: {
|
|
2080
|
+
totalRamGb: Number(hardware.total_ram_gb ?? 0),
|
|
2081
|
+
freeRamGb: Number(hardware.available_ram_gb ?? 0),
|
|
2082
|
+
gpu: {
|
|
2083
|
+
backend: "metal",
|
|
2084
|
+
available: hardware.metal_supported === true
|
|
2085
|
+
},
|
|
2086
|
+
cpuCores: Number(hardware.cpu_cores ?? 0),
|
|
2087
|
+
platform: "ios",
|
|
2088
|
+
arch: hardware.is_simulator === true ? "simulator" : "arm64",
|
|
2089
|
+
appleSilicon: hardware.metal_supported === true,
|
|
2090
|
+
recommendedBucket: "small",
|
|
2091
|
+
source: "ios-native-llama"
|
|
2092
|
+
},
|
|
2093
|
+
assignments: readAssignments(),
|
|
2094
|
+
textReadiness: nativeTextReadiness(),
|
|
2095
|
+
voiceReadiness: nativeVoiceReadiness()
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
async function synthesizeNativeIosLocalTts(request) {
|
|
2099
|
+
const bundleDir = nativeVoiceBundleDir();
|
|
2100
|
+
if (!bundleDir) {
|
|
2101
|
+
throw new Error("No Eliza-1 voice bundle is installed");
|
|
2102
|
+
}
|
|
2103
|
+
const sampleRate = optionalPositiveNumber(request.sampleRate);
|
|
2104
|
+
const result = await callIosHost(
|
|
2105
|
+
"eliza_tts_synthesize",
|
|
2106
|
+
{
|
|
2107
|
+
bundleDir,
|
|
2108
|
+
text: request.text,
|
|
2109
|
+
...request.voice || request.voiceId ? { speakerPresetId: request.voice ?? request.voiceId } : {},
|
|
2110
|
+
maxSamples: sampleRate ? Math.round(sampleRate * 60) : 24e3 * 60
|
|
2111
|
+
},
|
|
2112
|
+
18e4
|
|
2113
|
+
);
|
|
2114
|
+
const record = result && typeof result === "object" && !Array.isArray(result) ? result : {};
|
|
2115
|
+
const audioFilePath = record.audioFilePath;
|
|
2116
|
+
if (typeof audioFilePath === "string" && audioFilePath.trim()) {
|
|
2117
|
+
const resolvedAudioFilePath = audioFilePath.trim();
|
|
2118
|
+
const bytes = readFileSync2(resolvedAudioFilePath);
|
|
2119
|
+
rmSync2(resolvedAudioFilePath, { force: true });
|
|
2120
|
+
return normalizeAudioBytes(bytes);
|
|
2121
|
+
}
|
|
2122
|
+
const audioBase64 = record.audioBase64;
|
|
2123
|
+
if (typeof audioBase64 !== "string" || audioBase64.length === 0) {
|
|
2124
|
+
throw new Error("Native iOS local TTS returned no audio");
|
|
2125
|
+
}
|
|
2126
|
+
return normalizeAudioBytes(Buffer.from(audioBase64, "base64"));
|
|
2127
|
+
}
|
|
2128
|
+
async function handleNativeIosLocalTtsRoute(method, rawPath, payload) {
|
|
2129
|
+
const { pathname } = splitPathAndQuery(rawPath);
|
|
2130
|
+
if (method !== "POST" || pathname !== "/api/tts/local-inference") {
|
|
2131
|
+
return null;
|
|
2132
|
+
}
|
|
2133
|
+
const body = parseRequestBody(payload);
|
|
2134
|
+
const text = typeof body.text === "string" ? sanitizeLocalInferenceSpeechText(body.text) : "";
|
|
2135
|
+
if (!text) {
|
|
2136
|
+
return jsonResponse(400, { error: "Missing text" });
|
|
2137
|
+
}
|
|
2138
|
+
const voiceReadiness = nativeVoiceReadiness();
|
|
2139
|
+
if (voiceReadiness.status !== "ready" && voiceReadiness.status !== "engine-ready" && voiceReadiness.status !== "assets-ready") {
|
|
2140
|
+
return jsonResponse(503, {
|
|
2141
|
+
error: voiceReadiness.message,
|
|
2142
|
+
code: voiceReadiness.status === "unavailable" ? "ios_local_tts_executor_missing" : "ios_local_voice_assets_missing",
|
|
2143
|
+
voiceReadiness
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
const request = {
|
|
2147
|
+
text,
|
|
2148
|
+
...optionalString(body.voice) ? { voice: optionalString(body.voice) } : {},
|
|
2149
|
+
...optionalString(body.voiceId) ? { voice: optionalString(body.voiceId) } : {},
|
|
2150
|
+
...optionalString(body.model) ? { model: optionalString(body.model) } : {},
|
|
2151
|
+
...optionalString(body.modelId) ? { modelId: optionalString(body.modelId) } : {},
|
|
2152
|
+
...optionalPositiveNumber(body.speed) ? { speed: optionalPositiveNumber(body.speed) } : {},
|
|
2153
|
+
...optionalPositiveNumber(body.sampleRate) ? { sampleRate: optionalPositiveNumber(body.sampleRate) } : {},
|
|
2154
|
+
...optionalString(body.format) ? { format: optionalString(body.format) } : {}
|
|
2155
|
+
};
|
|
2156
|
+
try {
|
|
2157
|
+
const bytes = await synthesizeNativeIosLocalTts(request);
|
|
2158
|
+
if (bytes.length === 0) {
|
|
2159
|
+
return jsonResponse(502, {
|
|
2160
|
+
error: "Local inference TEXT_TO_SPEECH returned empty audio"
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
return bytesResponse(200, bytes, {
|
|
2164
|
+
"content-type": sniffAudioContentType(bytes),
|
|
2165
|
+
"cache-control": "no-store",
|
|
2166
|
+
"content-length": String(bytes.byteLength)
|
|
2167
|
+
});
|
|
2168
|
+
} catch (error) {
|
|
2169
|
+
return jsonResponse(502, {
|
|
2170
|
+
error: `Local inference TTS error: ${error instanceof Error ? error.message : String(error)}`,
|
|
2171
|
+
code: "ios_local_tts_failed"
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
async function transcribeNativeIosLocalAsr(request) {
|
|
2176
|
+
const bundleDir = nativeVoiceBundleDir();
|
|
2177
|
+
if (!bundleDir) {
|
|
2178
|
+
throw new Error("No Eliza-1 voice bundle is installed");
|
|
2179
|
+
}
|
|
2180
|
+
const sampleRate = optionalPositiveNumber(request.sampleRate) ?? 16e3;
|
|
2181
|
+
const result = await callIosHost(
|
|
2182
|
+
"eliza_asr_transcribe",
|
|
2183
|
+
{
|
|
2184
|
+
bundleDir,
|
|
2185
|
+
pcm: request.pcm,
|
|
2186
|
+
sampleRate
|
|
2187
|
+
},
|
|
2188
|
+
18e4
|
|
2189
|
+
);
|
|
2190
|
+
const record = result && typeof result === "object" && !Array.isArray(result) ? result : {};
|
|
2191
|
+
const text = record.text;
|
|
2192
|
+
if (typeof text !== "string") {
|
|
2193
|
+
throw new Error("Native iOS local ASR returned no transcript");
|
|
2194
|
+
}
|
|
2195
|
+
return text;
|
|
2196
|
+
}
|
|
2197
|
+
function parsePcmFloatArray(value) {
|
|
2198
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
2199
|
+
return null;
|
|
2200
|
+
}
|
|
2201
|
+
const pcm = [];
|
|
2202
|
+
for (const sample of value) {
|
|
2203
|
+
if (typeof sample !== "number" || !Number.isFinite(sample)) {
|
|
2204
|
+
return null;
|
|
2205
|
+
}
|
|
2206
|
+
pcm.push(sample);
|
|
2207
|
+
}
|
|
2208
|
+
return pcm;
|
|
2209
|
+
}
|
|
2210
|
+
async function handleNativeIosLocalAsrRoute(method, rawPath, payload) {
|
|
2211
|
+
const { pathname } = splitPathAndQuery(rawPath);
|
|
2212
|
+
if (method !== "POST" || pathname !== "/api/asr/local-inference") {
|
|
2213
|
+
return null;
|
|
2214
|
+
}
|
|
2215
|
+
const body = parseRequestBody(payload);
|
|
2216
|
+
const pcm = parsePcmFloatArray(body.pcm ?? body.audio);
|
|
2217
|
+
if (!pcm) {
|
|
2218
|
+
return null;
|
|
2219
|
+
}
|
|
2220
|
+
const voiceReadiness = nativeVoiceReadiness();
|
|
2221
|
+
if (voiceReadiness.status !== "ready" && voiceReadiness.status !== "engine-ready" && voiceReadiness.status !== "assets-ready") {
|
|
2222
|
+
return jsonResponse(503, {
|
|
2223
|
+
error: voiceReadiness.message,
|
|
2224
|
+
code: voiceReadiness.status === "unavailable" ? "ios_local_asr_executor_missing" : "ios_local_voice_assets_missing",
|
|
2225
|
+
voiceReadiness
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
const request = {
|
|
2229
|
+
pcm,
|
|
2230
|
+
...optionalPositiveNumber(body.sampleRate) ? { sampleRate: optionalPositiveNumber(body.sampleRate) } : {}
|
|
2231
|
+
};
|
|
2232
|
+
try {
|
|
2233
|
+
const text = await transcribeNativeIosLocalAsr(request);
|
|
2234
|
+
return jsonResponse(200, { text });
|
|
2235
|
+
} catch (error) {
|
|
2236
|
+
return jsonResponse(502, {
|
|
2237
|
+
error: `Local inference ASR error: ${error instanceof Error ? error.message : String(error)}`,
|
|
2238
|
+
code: "ios_local_asr_failed"
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
async function handleNativeIosLocalInferenceRoute(method, rawPath, payload) {
|
|
2243
|
+
const { pathname } = splitPathAndQuery(rawPath);
|
|
2244
|
+
if (method === "GET" && pathname === "/api/local-inference/device") {
|
|
2245
|
+
return jsonResponse(200, await nativeLlamaDeviceStatus());
|
|
2246
|
+
}
|
|
2247
|
+
if (method === "GET" && pathname === "/api/local-inference/providers") {
|
|
2248
|
+
return jsonResponse(200, await nativeLocalInferenceProviders());
|
|
2249
|
+
}
|
|
2250
|
+
if (method === "GET" && pathname === "/api/local-inference/hardware") {
|
|
2251
|
+
return jsonResponse(200, (await nativeHubSnapshot()).hardware);
|
|
2252
|
+
}
|
|
2253
|
+
if (method === "GET" && pathname === "/api/local-inference/catalog") {
|
|
2254
|
+
return jsonResponse(200, { models: nativeCatalogModels() });
|
|
2255
|
+
}
|
|
2256
|
+
if (method === "GET" && pathname === "/api/local-inference/installed") {
|
|
2257
|
+
return jsonResponse(200, { models: readInstalledModels() });
|
|
2258
|
+
}
|
|
2259
|
+
if (method === "GET" && pathname === "/api/local-inference/downloads") {
|
|
2260
|
+
return jsonResponse(200, { downloads: nativeDownloadJobs() });
|
|
2261
|
+
}
|
|
2262
|
+
if (method === "POST" && pathname === "/api/local-inference/downloads") {
|
|
2263
|
+
const body = parseRequestBody(payload);
|
|
2264
|
+
const modelId = typeof body.modelId === "string" ? body.modelId : body.spec && typeof body.spec === "object" && !Array.isArray(body.spec) && typeof body.spec.id === "string" ? body.spec.id : "";
|
|
2265
|
+
if (!modelId) {
|
|
2266
|
+
return jsonResponse(400, { error: "Missing modelId" });
|
|
2267
|
+
}
|
|
2268
|
+
try {
|
|
2269
|
+
return jsonResponse(200, { job: startNativeModelDownload(modelId) });
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
return jsonResponse(400, {
|
|
2272
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
if (method === "GET" && pathname === "/api/local-inference/routing") {
|
|
2277
|
+
return jsonResponse(200, routingPreferencesSnapshot());
|
|
2278
|
+
}
|
|
2279
|
+
if (method === "GET" && pathname === "/api/local-inference/assignments") {
|
|
2280
|
+
return jsonResponse(200, { assignments: readAssignments() });
|
|
2281
|
+
}
|
|
2282
|
+
if (method === "POST" && pathname === "/api/local-inference/assignments") {
|
|
2283
|
+
const body = parseRequestBody(payload);
|
|
2284
|
+
const slot = typeof body.slot === "string" ? body.slot : "";
|
|
2285
|
+
const modelId = typeof body.modelId === "string" ? body.modelId : null;
|
|
2286
|
+
if (!IOS_NATIVE_ASSIGNMENT_SLOTS.has(slot)) {
|
|
2287
|
+
return jsonResponse(400, {
|
|
2288
|
+
error: `Unsupported assignment slot: ${slot}`
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
if (modelId) {
|
|
2292
|
+
const installed = readInstalledModels();
|
|
2293
|
+
const known = installed.some((model) => model.id === modelId) || iosNativeCatalogById(modelId) != null || nativeDownloadState.has(modelId);
|
|
2294
|
+
if (!known) {
|
|
2295
|
+
return jsonResponse(404, { error: `Unknown local model: ${modelId}` });
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
const assignments = readAssignments();
|
|
2299
|
+
if (modelId) assignments[slot] = modelId;
|
|
2300
|
+
else delete assignments[slot];
|
|
2301
|
+
writeAssignments(assignments);
|
|
2302
|
+
return jsonResponse(200, { assignments });
|
|
2303
|
+
}
|
|
2304
|
+
if (method === "GET" && pathname === "/api/local-inference/active") {
|
|
2305
|
+
return jsonResponse(200, nativeLlamaActiveSnapshot());
|
|
2306
|
+
}
|
|
2307
|
+
if (method === "POST" && pathname === "/api/local-inference/active") {
|
|
2308
|
+
const body = parseRequestBody(payload);
|
|
2309
|
+
const modelId = typeof body.modelId === "string" ? body.modelId : "";
|
|
2310
|
+
const installed = readInstalledModels();
|
|
2311
|
+
const target = installed.find((model) => model.id === modelId);
|
|
2312
|
+
if (!target) {
|
|
2313
|
+
return jsonResponse(404, { error: `Model not installed: ${modelId}` });
|
|
2314
|
+
}
|
|
2315
|
+
mkdirSync2(localInferenceRootPath(), { recursive: true });
|
|
2316
|
+
nativeLlamaState.modelId = target.id;
|
|
2317
|
+
nativeLlamaState.modelPath = target.path;
|
|
2318
|
+
await ensureNativeModelLoaded("TEXT_SMALL");
|
|
2319
|
+
return jsonResponse(200, nativeLlamaActiveSnapshot());
|
|
2320
|
+
}
|
|
2321
|
+
if (method === "DELETE" && pathname === "/api/local-inference/active") {
|
|
2322
|
+
await unloadNativeLlamaModel();
|
|
2323
|
+
return jsonResponse(200, nativeLlamaActiveSnapshot());
|
|
2324
|
+
}
|
|
2325
|
+
if (method === "GET" && pathname === "/api/local-inference/hub") {
|
|
2326
|
+
return jsonResponse(200, await nativeHubSnapshot());
|
|
2327
|
+
}
|
|
2328
|
+
const verifyMatch = pathname.match(
|
|
2329
|
+
/^\/api\/local-inference\/installed\/([^/]+)\/verify$/
|
|
2330
|
+
);
|
|
2331
|
+
if (method === "POST" && verifyMatch?.[1]) {
|
|
2332
|
+
const id = decodeURIComponent(verifyMatch[1]);
|
|
2333
|
+
const model = readInstalledModels().find((entry) => entry.id === id);
|
|
2334
|
+
if (!model)
|
|
2335
|
+
return jsonResponse(404, { error: `Model not installed: ${id}` });
|
|
2336
|
+
return jsonResponse(200, {
|
|
2337
|
+
ok: true,
|
|
2338
|
+
modelId: model.id,
|
|
2339
|
+
path: model.path,
|
|
2340
|
+
sizeBytes: model.sizeBytes ?? 0,
|
|
2341
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
return null;
|
|
2345
|
+
}
|
|
2346
|
+
async function handleBufferedLocalInferenceRoute(method, rawPath, payload) {
|
|
2347
|
+
const { pathname } = splitPathAndQuery(rawPath);
|
|
2348
|
+
if (!pathname.startsWith("/api/local-inference/")) return null;
|
|
2349
|
+
if (method === "GET" && (pathname === "/api/local-inference/downloads/stream" || pathname === "/api/local-inference/device/stream")) {
|
|
2350
|
+
return jsonResponse(501, {
|
|
2351
|
+
error: "Streaming local-inference endpoints are not available over the iOS stdio bridge",
|
|
2352
|
+
code: "streaming_not_supported"
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
const native = await handleNativeIosLocalInferenceRoute(
|
|
2356
|
+
method,
|
|
2357
|
+
rawPath,
|
|
2358
|
+
payload
|
|
2359
|
+
);
|
|
2360
|
+
if (native) return native;
|
|
2361
|
+
return null;
|
|
2362
|
+
}
|
|
2363
|
+
async function ensureConversationConnection(backend, conversation) {
|
|
2364
|
+
const runtime = backend.runtime;
|
|
2365
|
+
const userId = stringToUuid("ios-local-user");
|
|
2366
|
+
if (typeof runtime.ensureConnection === "function") {
|
|
2367
|
+
await runtime.ensureConnection({
|
|
2368
|
+
entityId: userId,
|
|
2369
|
+
roomId: conversation.roomId,
|
|
2370
|
+
worldId: stringToUuid("ios-local-world"),
|
|
2371
|
+
userName: "User",
|
|
2372
|
+
source: "ios-local",
|
|
2373
|
+
channelId: "ios-local-chat",
|
|
2374
|
+
type: ChannelType.DM,
|
|
2375
|
+
messageServerId: stringToUuid("ios-local-server"),
|
|
2376
|
+
metadata: { ownership: { ownerId: userId } }
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
return userId;
|
|
2380
|
+
}
|
|
2381
|
+
async function handleDirectConversationMessage(backend, conversation, input) {
|
|
2382
|
+
const prompt = typeof input.text === "string" ? input.text : typeof input.message === "string" ? input.message : typeof input.prompt === "string" ? input.prompt : "";
|
|
2383
|
+
if (!prompt.trim()) throw new Error("message text is required");
|
|
2384
|
+
const runtime = backend.runtime;
|
|
2385
|
+
const userId = await ensureConversationConnection(backend, conversation);
|
|
2386
|
+
const channelType = typeof input.channelType === "string" && Object.values(ChannelType).includes(input.channelType) ? input.channelType : ChannelType.DM;
|
|
2387
|
+
const metadata = input.metadata && typeof input.metadata === "object" && !Array.isArray(input.metadata) ? input.metadata : void 0;
|
|
2388
|
+
const message = createMessageMemory({
|
|
2389
|
+
id: crypto.randomUUID(),
|
|
2390
|
+
entityId: userId,
|
|
2391
|
+
roomId: conversation.roomId,
|
|
2392
|
+
content: {
|
|
2393
|
+
text: prompt,
|
|
2394
|
+
source: "ios-local",
|
|
2395
|
+
channelType,
|
|
2396
|
+
...metadata ? { metadata } : {}
|
|
2397
|
+
}
|
|
2398
|
+
});
|
|
2399
|
+
try {
|
|
2400
|
+
await runtime.createMemory?.(message, "messages");
|
|
2401
|
+
} catch {
|
|
2402
|
+
}
|
|
2403
|
+
const nativeReply = await maybeGenerateIosNativeConversationReply(
|
|
2404
|
+
backend.runtime,
|
|
2405
|
+
prompt
|
|
2406
|
+
).catch((error) => ({
|
|
2407
|
+
text: error instanceof Error ? `The local Eliza-1 model is installed, but generation failed: ${error.message}` : "The local Eliza-1 model is installed, but generation failed.",
|
|
2408
|
+
reply: error instanceof Error ? `The local Eliza-1 model is installed, but generation failed: ${error.message}` : "The local Eliza-1 model is installed, but generation failed.",
|
|
2409
|
+
localInference: {
|
|
2410
|
+
provider: IOS_NATIVE_LLAMA_PROVIDER,
|
|
2411
|
+
mode: "ios_native_conversation",
|
|
2412
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2413
|
+
}
|
|
2414
|
+
}));
|
|
2415
|
+
if (nativeReply) {
|
|
2416
|
+
const agentName2 = runtimeAgentName(backend.runtime);
|
|
2417
|
+
conversation.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2418
|
+
conversation.lastUserText = prompt.trim();
|
|
2419
|
+
conversation.lastAssistantText = typeof nativeReply.text === "string" ? nativeReply.text : "";
|
|
2420
|
+
conversation.lastAgentName = agentName2;
|
|
2421
|
+
return {
|
|
2422
|
+
...nativeReply,
|
|
2423
|
+
agentName: agentName2,
|
|
2424
|
+
conversationId: conversation.id
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
if (!runtime.messageService?.handleMessage) {
|
|
2428
|
+
throw new Error("runtime.messageService is not available");
|
|
2429
|
+
}
|
|
2430
|
+
const chunks = [];
|
|
2431
|
+
try {
|
|
2432
|
+
await runtime.messageService.handleMessage(
|
|
2433
|
+
runtime,
|
|
2434
|
+
message,
|
|
2435
|
+
async (content) => {
|
|
2436
|
+
if (content?.text) chunks.push(content.text);
|
|
2437
|
+
return [];
|
|
2438
|
+
}
|
|
2439
|
+
);
|
|
2440
|
+
} catch (err) {
|
|
2441
|
+
chunks.push(
|
|
2442
|
+
err instanceof Error ? `The local agent started, but generation is unavailable: ${err.message}` : "The local agent started, but generation is unavailable."
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
const text = stripReasoningBlocks(chunks.join("")).trim();
|
|
2446
|
+
const agentName = runtimeAgentName(backend.runtime);
|
|
2447
|
+
conversation.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2448
|
+
conversation.lastUserText = prompt.trim();
|
|
2449
|
+
conversation.lastAssistantText = text;
|
|
2450
|
+
conversation.lastAgentName = agentName;
|
|
2451
|
+
return {
|
|
2452
|
+
text,
|
|
2453
|
+
reply: text,
|
|
2454
|
+
agentName,
|
|
2455
|
+
conversationId: conversation.id
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
function cachedConversationMessageResult(conversation, input) {
|
|
2459
|
+
const prompt = typeof input.text === "string" ? input.text : typeof input.message === "string" ? input.message : typeof input.prompt === "string" ? input.prompt : "";
|
|
2460
|
+
if (!conversation.lastAssistantText || !conversation.lastUserText || conversation.lastUserText !== prompt.trim()) {
|
|
2461
|
+
return null;
|
|
2462
|
+
}
|
|
2463
|
+
return {
|
|
2464
|
+
text: conversation.lastAssistantText,
|
|
2465
|
+
reply: conversation.lastAssistantText,
|
|
2466
|
+
agentName: conversation.lastAgentName ?? "Eliza",
|
|
2467
|
+
conversationId: conversation.id,
|
|
2468
|
+
cached: true
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
function sseEvent(payload) {
|
|
2472
|
+
return `data: ${JSON.stringify(payload)}
|
|
2473
|
+
|
|
2474
|
+
`;
|
|
2475
|
+
}
|
|
2476
|
+
function bufferedConversationStreamResponse(result) {
|
|
2477
|
+
const text = typeof result.text === "string" ? result.text : "";
|
|
2478
|
+
const agentName = typeof result.agentName === "string" && result.agentName.trim() ? result.agentName : "Eliza";
|
|
2479
|
+
const body = [
|
|
2480
|
+
...text ? [sseEvent({ type: "token", text, fullText: text })] : [],
|
|
2481
|
+
sseEvent({
|
|
2482
|
+
type: "done",
|
|
2483
|
+
fullText: text,
|
|
2484
|
+
agentName,
|
|
2485
|
+
completed: true,
|
|
2486
|
+
...typeof result.failureKind === "string" ? { failureKind: result.failureKind } : {},
|
|
2487
|
+
...result.localInference && typeof result.localInference === "object" && !Array.isArray(result.localInference) ? { localInference: result.localInference } : {}
|
|
2488
|
+
})
|
|
2489
|
+
].join("");
|
|
2490
|
+
return {
|
|
2491
|
+
status: 200,
|
|
2492
|
+
statusText: statusTextForCode(200),
|
|
2493
|
+
headers: {
|
|
2494
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
2495
|
+
"cache-control": "no-cache, no-transform",
|
|
2496
|
+
connection: "keep-alive"
|
|
2497
|
+
},
|
|
2498
|
+
body,
|
|
2499
|
+
bodyBase64: Buffer.from(body, "utf8").toString("base64"),
|
|
2500
|
+
bodyEncoding: "utf-8"
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
async function handleDirectCoreRoute(backend, method, rawPath, payload) {
|
|
2504
|
+
const { pathname } = splitPathAndQuery(rawPath);
|
|
2505
|
+
if (method === "GET" && pathname === "/api/health") {
|
|
2506
|
+
return jsonResponse(200, {
|
|
2507
|
+
ready: true,
|
|
2508
|
+
runtime: "ok",
|
|
2509
|
+
database: "ok",
|
|
2510
|
+
plugins: {
|
|
2511
|
+
loaded: Array.isArray(
|
|
2512
|
+
backend.runtime.plugins
|
|
2513
|
+
) ? backend.runtime.plugins?.length ?? 0 : 0,
|
|
2514
|
+
failed: 0
|
|
2515
|
+
},
|
|
2516
|
+
coordinator: "not_wired",
|
|
2517
|
+
agentState: "running",
|
|
2518
|
+
agentName: runtimeAgentName(backend.runtime),
|
|
2519
|
+
startedAt: null,
|
|
2520
|
+
uptime: 0,
|
|
2521
|
+
iosBridge: "bun"
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
if (method === "GET" && pathname === "/api/status") {
|
|
2525
|
+
return jsonResponse(200, {
|
|
2526
|
+
state: "running",
|
|
2527
|
+
agentName: runtimeAgentName(backend.runtime),
|
|
2528
|
+
model: null,
|
|
2529
|
+
canRespond: true,
|
|
2530
|
+
startedAt: null,
|
|
2531
|
+
uptime: 0,
|
|
2532
|
+
startup: { phase: "running", runtimePhase: "running" },
|
|
2533
|
+
cloud: {
|
|
2534
|
+
connectionStatus: "disconnected",
|
|
2535
|
+
activeAgentId: null,
|
|
2536
|
+
cloudProvisioned: false,
|
|
2537
|
+
hasApiKey: false
|
|
2538
|
+
},
|
|
2539
|
+
pendingRestart: false,
|
|
2540
|
+
pendingRestartReasons: [],
|
|
2541
|
+
iosBridge: "bun"
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
if (method === "GET" && pathname === "/api/apps/runs") {
|
|
2545
|
+
return jsonResponse(200, []);
|
|
2546
|
+
}
|
|
2547
|
+
if (method === "GET" && pathname === "/api/first-run/status") {
|
|
2548
|
+
return jsonResponse(200, {
|
|
2549
|
+
complete: true,
|
|
2550
|
+
cloudProvisioned: false,
|
|
2551
|
+
deploymentTarget: "local"
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
if (method === "POST" && pathname === "/api/first-run") {
|
|
2555
|
+
return jsonResponse(200, {
|
|
2556
|
+
ok: true,
|
|
2557
|
+
complete: true,
|
|
2558
|
+
deploymentTarget: "local"
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
if (method === "GET" && pathname === "/api/auth/me") {
|
|
2562
|
+
return jsonResponse(200, {
|
|
2563
|
+
identity: {
|
|
2564
|
+
id: "local-agent",
|
|
2565
|
+
displayName: "Local Agent",
|
|
2566
|
+
kind: "machine"
|
|
2567
|
+
},
|
|
2568
|
+
session: { id: "local", kind: "local", expiresAt: null },
|
|
2569
|
+
access: {
|
|
2570
|
+
mode: "local",
|
|
2571
|
+
passwordConfigured: false,
|
|
2572
|
+
ownerConfigured: false
|
|
2573
|
+
}
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
if (method === "GET" && pathname === "/api/auth/status") {
|
|
2577
|
+
return jsonResponse(200, {
|
|
2578
|
+
required: false,
|
|
2579
|
+
pairingEnabled: false,
|
|
2580
|
+
expiresAt: null,
|
|
2581
|
+
authenticated: true,
|
|
2582
|
+
localAccess: true
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
if (method === "POST" && pathname === "/api/dev/model-grind") {
|
|
2586
|
+
const report = await runModelGrind({
|
|
2587
|
+
callIosHost,
|
|
2588
|
+
ensureTextModelLoaded: (slot) => ensureNativeModelLoaded(slot),
|
|
2589
|
+
synthesizeTts: async (text) => ({
|
|
2590
|
+
bytes: await synthesizeNativeIosLocalTts({ text }),
|
|
2591
|
+
sampleRate: 24e3
|
|
2592
|
+
}),
|
|
2593
|
+
transcribeAsr: (pcm, sampleRate) => transcribeNativeIosLocalAsr({ pcm, sampleRate }),
|
|
2594
|
+
hardwareInfo: () => nativeHardwareInfo(),
|
|
2595
|
+
bundleDir: nativeVoiceBundleDir()
|
|
2596
|
+
});
|
|
2597
|
+
return jsonResponse(report.overall.allPassed ? 200 : 207, report);
|
|
2598
|
+
}
|
|
2599
|
+
const localTts = await handleNativeIosLocalTtsRoute(method, rawPath, payload);
|
|
2600
|
+
if (localTts) return localTts;
|
|
2601
|
+
const localAsr = await handleNativeIosLocalAsrRoute(method, rawPath, payload);
|
|
2602
|
+
if (localAsr) return localAsr;
|
|
2603
|
+
const localInference = await handleBufferedLocalInferenceRoute(
|
|
2604
|
+
method,
|
|
2605
|
+
rawPath,
|
|
2606
|
+
payload
|
|
2607
|
+
);
|
|
2608
|
+
if (localInference) return localInference;
|
|
2609
|
+
if (method === "GET" && pathname === "/api/conversations") {
|
|
2610
|
+
return jsonResponse(200, {
|
|
2611
|
+
conversations: Array.from(backend.conversations.values()).sort(
|
|
2612
|
+
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
2613
|
+
)
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
if (method === "POST" && pathname === "/api/conversations") {
|
|
2617
|
+
const conversation = createIosConversation(
|
|
2618
|
+
backend,
|
|
2619
|
+
parseRequestBody(payload)
|
|
2620
|
+
);
|
|
2621
|
+
return jsonResponse(200, { conversation });
|
|
2622
|
+
}
|
|
2623
|
+
const messageMatch = pathname.match(
|
|
2624
|
+
/^\/api\/conversations\/([^/]+)\/messages$/
|
|
2625
|
+
);
|
|
2626
|
+
const messageStreamMatch = pathname.match(
|
|
2627
|
+
/^\/api\/conversations\/([^/]+)\/messages\/stream$/
|
|
2628
|
+
);
|
|
2629
|
+
if (method === "GET" && messageMatch) {
|
|
2630
|
+
return jsonResponse(200, { messages: [] });
|
|
2631
|
+
}
|
|
2632
|
+
if (method === "POST" && messageStreamMatch) {
|
|
2633
|
+
const conversationId = decodeURIComponent(messageStreamMatch[1] ?? "");
|
|
2634
|
+
const conversation = backend.conversations.get(conversationId);
|
|
2635
|
+
if (!conversation) {
|
|
2636
|
+
return jsonResponse(404, { error: "Conversation not found" });
|
|
2637
|
+
}
|
|
2638
|
+
const body = parseRequestBody(payload);
|
|
2639
|
+
const result = cachedConversationMessageResult(conversation, body) ?? await handleDirectConversationMessage(backend, conversation, body);
|
|
2640
|
+
return bufferedConversationStreamResponse(result);
|
|
2641
|
+
}
|
|
2642
|
+
if (method === "POST" && messageMatch) {
|
|
2643
|
+
const conversationId = decodeURIComponent(messageMatch[1] ?? "");
|
|
2644
|
+
const conversation = backend.conversations.get(conversationId);
|
|
2645
|
+
if (!conversation) {
|
|
2646
|
+
return jsonResponse(404, { error: "Conversation not found" });
|
|
2647
|
+
}
|
|
2648
|
+
const result = await handleDirectConversationMessage(
|
|
2649
|
+
backend,
|
|
2650
|
+
conversation,
|
|
2651
|
+
parseRequestBody(payload)
|
|
2652
|
+
);
|
|
2653
|
+
return jsonResponse(200, result);
|
|
2654
|
+
}
|
|
2655
|
+
return null;
|
|
2656
|
+
}
|
|
2657
|
+
async function sendMessage(backend, payload) {
|
|
2658
|
+
const input = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
2659
|
+
const message = typeof input.message === "string" ? input.message : "";
|
|
2660
|
+
if (!message.trim()) throw new Error("send_message requires message");
|
|
2661
|
+
let conversationId = typeof input.conversationId === "string" && input.conversationId.trim() ? input.conversationId.trim() : "";
|
|
2662
|
+
if (!conversationId) {
|
|
2663
|
+
conversationId = createIosConversation(backend, {
|
|
2664
|
+
title: "iOS Local Chat"
|
|
2665
|
+
}).id;
|
|
2666
|
+
}
|
|
2667
|
+
const conversation = backend.conversations.get(conversationId);
|
|
2668
|
+
if (!conversation) throw new Error("Conversation not found");
|
|
2669
|
+
const result = await timeoutAfter(
|
|
2670
|
+
handleDirectConversationMessage(backend, conversation, {
|
|
2671
|
+
text: message,
|
|
2672
|
+
channelType: typeof input.channelType === "string" ? input.channelType : "DM",
|
|
2673
|
+
...input.metadata && typeof input.metadata === "object" && !Array.isArray(input.metadata) ? { metadata: input.metadata } : {}
|
|
2674
|
+
}),
|
|
2675
|
+
bridgeTimeoutMs(input.timeoutMs),
|
|
2676
|
+
"send_message"
|
|
2677
|
+
);
|
|
2678
|
+
if (isTimeoutMarker(result)) {
|
|
2679
|
+
throw new Error(`${result.label} timed out after ${result.timeoutMs}ms`);
|
|
2680
|
+
}
|
|
2681
|
+
return { ...result, conversationId, response: result };
|
|
2682
|
+
}
|
|
2683
|
+
async function dispatchBridgeRequest(host, request) {
|
|
2684
|
+
const method = typeof request.method === "string" ? request.method : "";
|
|
2685
|
+
const payload = request.payload && typeof request.payload === "object" ? request.payload : {};
|
|
2686
|
+
switch (method) {
|
|
2687
|
+
case "status":
|
|
2688
|
+
if (host.backend) {
|
|
2689
|
+
return bridgeStatus();
|
|
2690
|
+
}
|
|
2691
|
+
if (host.bootError) {
|
|
2692
|
+
return bridgeStatus({
|
|
2693
|
+
ready: false,
|
|
2694
|
+
phase: "error",
|
|
2695
|
+
error: host.bootError instanceof Error ? host.bootError.message : String(host.bootError)
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
if (payload.timeoutMs !== void 0) {
|
|
2699
|
+
await awaitIosBridgeBackend(
|
|
2700
|
+
host,
|
|
2701
|
+
bridgeTimeoutMs(payload.timeoutMs),
|
|
2702
|
+
"status"
|
|
2703
|
+
);
|
|
2704
|
+
return bridgeStatus();
|
|
2705
|
+
}
|
|
2706
|
+
return bridgeStatus({ ready: false, phase: "starting" });
|
|
2707
|
+
case "http_request":
|
|
2708
|
+
case "http_fetch": {
|
|
2709
|
+
const backendForFetch = await awaitIosBridgeBackend(
|
|
2710
|
+
host,
|
|
2711
|
+
bridgeTimeoutMs(payload.timeoutMs),
|
|
2712
|
+
method
|
|
2713
|
+
);
|
|
2714
|
+
return fetchBackend(
|
|
2715
|
+
backendForFetch,
|
|
2716
|
+
request.payload ?? {}
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
case "send_message": {
|
|
2720
|
+
const backendForMessage = await awaitIosBridgeBackend(
|
|
2721
|
+
host,
|
|
2722
|
+
bridgeTimeoutMs(payload.timeoutMs),
|
|
2723
|
+
method
|
|
2724
|
+
);
|
|
2725
|
+
return sendMessage(backendForMessage, request.payload);
|
|
2726
|
+
}
|
|
2727
|
+
default:
|
|
2728
|
+
throw new Error(`Unknown iOS bridge method: ${method || "(missing)"}`);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
function reserveStdoutForBridgeProtocol() {
|
|
2732
|
+
const stderrWrite = process2.stderr.write.bind(process2.stderr);
|
|
2733
|
+
const originalStdoutWrite = process2.stdout.write.bind(process2.stdout);
|
|
2734
|
+
const originalConsoleLog = console.log.bind(console);
|
|
2735
|
+
const originalConsoleInfo = console.info.bind(console);
|
|
2736
|
+
const originalConsoleDebug = console.debug.bind(console);
|
|
2737
|
+
const assignments = [];
|
|
2738
|
+
const tryAssign = (target, key, value, restore) => {
|
|
2739
|
+
try {
|
|
2740
|
+
target[key] = value;
|
|
2741
|
+
assignments.push(restore);
|
|
2742
|
+
} catch {
|
|
2743
|
+
}
|
|
2744
|
+
};
|
|
2745
|
+
const writeToStderr = (chunk, encoding, cb) => {
|
|
2746
|
+
if (typeof encoding === "function") {
|
|
2747
|
+
return stderrWrite(chunk, encoding);
|
|
2748
|
+
}
|
|
2749
|
+
if (encoding) {
|
|
2750
|
+
return cb ? stderrWrite(chunk, encoding, cb) : stderrWrite(chunk, encoding);
|
|
2751
|
+
}
|
|
2752
|
+
return cb ? stderrWrite(chunk, cb) : stderrWrite(chunk);
|
|
2753
|
+
};
|
|
2754
|
+
const stdoutWriteToStderr = ((chunk, encoding, cb) => writeToStderr(
|
|
2755
|
+
chunk,
|
|
2756
|
+
encoding,
|
|
2757
|
+
cb
|
|
2758
|
+
));
|
|
2759
|
+
tryAssign(process2.stdout, "write", stdoutWriteToStderr, () => {
|
|
2760
|
+
process2.stdout.write = originalStdoutWrite;
|
|
2761
|
+
});
|
|
2762
|
+
tryAssign(
|
|
2763
|
+
console,
|
|
2764
|
+
"log",
|
|
2765
|
+
((...args) => console.error(...args)),
|
|
2766
|
+
() => {
|
|
2767
|
+
console.log = originalConsoleLog;
|
|
2768
|
+
}
|
|
2769
|
+
);
|
|
2770
|
+
tryAssign(
|
|
2771
|
+
console,
|
|
2772
|
+
"info",
|
|
2773
|
+
((...args) => console.error(...args)),
|
|
2774
|
+
() => {
|
|
2775
|
+
console.info = originalConsoleInfo;
|
|
2776
|
+
}
|
|
2777
|
+
);
|
|
2778
|
+
tryAssign(
|
|
2779
|
+
console,
|
|
2780
|
+
"debug",
|
|
2781
|
+
((...args) => console.error(...args)),
|
|
2782
|
+
() => {
|
|
2783
|
+
console.debug = originalConsoleDebug;
|
|
2784
|
+
}
|
|
2785
|
+
);
|
|
2786
|
+
return () => {
|
|
2787
|
+
for (let i = assignments.length - 1; i >= 0; i -= 1) {
|
|
2788
|
+
try {
|
|
2789
|
+
assignments[i]?.();
|
|
2790
|
+
} catch {
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
async function runIosBridgeCli(argv = process2.argv) {
|
|
2796
|
+
if (!argv.includes("--stdio")) {
|
|
2797
|
+
throw new Error("ios-bridge currently supports --stdio only");
|
|
2798
|
+
}
|
|
2799
|
+
const protocolWrite = process2.stdout.write.bind(process2.stdout);
|
|
2800
|
+
const restoreStdout = reserveStdoutForBridgeProtocol();
|
|
2801
|
+
const writeProtocolLine = (value) => {
|
|
2802
|
+
protocolWrite(`${JSON.stringify(value)}
|
|
2803
|
+
`);
|
|
2804
|
+
};
|
|
2805
|
+
installHostCallProtocol(writeProtocolLine);
|
|
2806
|
+
const host = startIosBridgeHost();
|
|
2807
|
+
process2.on("unhandledRejection", (reason) => {
|
|
2808
|
+
console.error(
|
|
2809
|
+
"[ios-bridge] unhandled rejection:",
|
|
2810
|
+
reason instanceof Error ? reason.stack || reason.message : reason
|
|
2811
|
+
);
|
|
2812
|
+
});
|
|
2813
|
+
process2.on("uncaughtException", (error) => {
|
|
2814
|
+
console.error(
|
|
2815
|
+
"[ios-bridge] uncaught exception:",
|
|
2816
|
+
error.stack || error.message
|
|
2817
|
+
);
|
|
2818
|
+
});
|
|
2819
|
+
writeProtocolLine({
|
|
2820
|
+
type: "ready",
|
|
2821
|
+
ok: true,
|
|
2822
|
+
result: bridgeStatus({ ready: true })
|
|
2823
|
+
});
|
|
2824
|
+
const shutdown = async () => {
|
|
2825
|
+
try {
|
|
2826
|
+
if (host.backend) {
|
|
2827
|
+
await host.backend.close();
|
|
2828
|
+
}
|
|
2829
|
+
} catch {
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
let stopBridge = null;
|
|
2833
|
+
const stopPromise = new Promise((resolve) => {
|
|
2834
|
+
stopBridge = resolve;
|
|
2835
|
+
});
|
|
2836
|
+
process2.once("SIGINT", () => stopBridge?.());
|
|
2837
|
+
process2.once("SIGTERM", () => stopBridge?.());
|
|
2838
|
+
const keepAlive = setInterval(() => {
|
|
2839
|
+
}, 2147483647);
|
|
2840
|
+
const handleLine = async (line) => {
|
|
2841
|
+
if (!line.trim()) return;
|
|
2842
|
+
let parsed;
|
|
2843
|
+
try {
|
|
2844
|
+
parsed = JSON.parse(line);
|
|
2845
|
+
} catch (err) {
|
|
2846
|
+
writeProtocolLine({
|
|
2847
|
+
id: null,
|
|
2848
|
+
ok: false,
|
|
2849
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2850
|
+
});
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
const id = parsed.id ?? null;
|
|
2854
|
+
try {
|
|
2855
|
+
const result = await dispatchBridgeRequest(host, parsed);
|
|
2856
|
+
writeProtocolLine({ id, ok: true, result });
|
|
2857
|
+
} catch (err) {
|
|
2858
|
+
writeProtocolLine({
|
|
2859
|
+
id,
|
|
2860
|
+
ok: false,
|
|
2861
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
};
|
|
2865
|
+
let pending = Promise.resolve();
|
|
2866
|
+
let bufferedInput = "";
|
|
2867
|
+
const stdin = process2.stdin;
|
|
2868
|
+
stdin.setEncoding?.("utf8");
|
|
2869
|
+
stdin.on("data", (chunk) => {
|
|
2870
|
+
bufferedInput += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
2871
|
+
for (; ; ) {
|
|
2872
|
+
const newline = bufferedInput.indexOf("\n");
|
|
2873
|
+
if (newline < 0) break;
|
|
2874
|
+
const line = bufferedInput.slice(0, newline).replace(/\r$/, "");
|
|
2875
|
+
bufferedInput = bufferedInput.slice(newline + 1);
|
|
2876
|
+
if (tryHandleHostResultLine(line)) continue;
|
|
2877
|
+
pending = pending.then(() => handleLine(line)).catch((err) => {
|
|
2878
|
+
writeProtocolLine({
|
|
2879
|
+
id: null,
|
|
2880
|
+
ok: false,
|
|
2881
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2882
|
+
});
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
});
|
|
2886
|
+
stdin.once("end", () => {
|
|
2887
|
+
if (bufferedInput.trim()) {
|
|
2888
|
+
const line = bufferedInput;
|
|
2889
|
+
bufferedInput = "";
|
|
2890
|
+
if (!tryHandleHostResultLine(line)) {
|
|
2891
|
+
pending = pending.then(() => handleLine(line));
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
stopBridge?.();
|
|
2895
|
+
});
|
|
2896
|
+
stdin.once("error", (err) => {
|
|
2897
|
+
writeProtocolLine({
|
|
2898
|
+
id: null,
|
|
2899
|
+
ok: false,
|
|
2900
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2901
|
+
});
|
|
2902
|
+
stopBridge?.();
|
|
2903
|
+
});
|
|
2904
|
+
stdin.resume?.();
|
|
2905
|
+
await stopPromise;
|
|
2906
|
+
clearInterval(keepAlive);
|
|
2907
|
+
await pending.catch(() => void 0);
|
|
2908
|
+
restoreStdout();
|
|
2909
|
+
await shutdown();
|
|
2910
|
+
}
|
|
2911
|
+
export {
|
|
2912
|
+
runIosBridgeCli
|
|
2913
|
+
};
|