@elizaos/capacitor-bun-runtime 2.0.3-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/ElizaosCapacitorBunRuntime.podspec +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +127 -0
  4. package/dist/esm/definitions.d.ts +136 -0
  5. package/dist/esm/definitions.d.ts.map +1 -0
  6. package/dist/esm/definitions.js +14 -0
  7. package/dist/esm/definitions.js.map +1 -0
  8. package/dist/esm/index.d.ts +9 -0
  9. package/dist/esm/index.d.ts.map +1 -0
  10. package/dist/esm/index.js +11 -0
  11. package/dist/esm/index.js.map +1 -0
  12. package/dist/esm/web.d.ts +19 -0
  13. package/dist/esm/web.d.ts.map +1 -0
  14. package/dist/esm/web.js +44 -0
  15. package/dist/esm/web.js.map +1 -0
  16. package/dist/plugin.cjs.js +63 -0
  17. package/dist/plugin.cjs.js.map +1 -0
  18. package/dist/plugin.js +66 -0
  19. package/dist/plugin.js.map +1 -0
  20. package/ios/Sources/ElizaBunRuntimePlugin/BridgeInstaller.swift +94 -0
  21. package/ios/Sources/ElizaBunRuntimePlugin/ElizaBunRuntime.swift +705 -0
  22. package/ios/Sources/ElizaBunRuntimePlugin/ElizaBunRuntimePlugin.swift +1109 -0
  23. package/ios/Sources/ElizaBunRuntimePlugin/FullBunEngineHost.swift +677 -0
  24. package/ios/Sources/ElizaBunRuntimePlugin/JSContextHelpers.swift +226 -0
  25. package/ios/Sources/ElizaBunRuntimePlugin/SandboxPaths.swift +46 -0
  26. package/ios/Sources/ElizaBunRuntimePlugin/bridge/CryptoBridge.swift +238 -0
  27. package/ios/Sources/ElizaBunRuntimePlugin/bridge/ElizaSqliteVecBridge.m +28 -0
  28. package/ios/Sources/ElizaBunRuntimePlugin/bridge/FSBridge.swift +270 -0
  29. package/ios/Sources/ElizaBunRuntimePlugin/bridge/HTTPBridge.swift +153 -0
  30. package/ios/Sources/ElizaBunRuntimePlugin/bridge/HTTPServerBridge.swift +32 -0
  31. package/ios/Sources/ElizaBunRuntimePlugin/bridge/LlamaBridge.swift +233 -0
  32. package/ios/Sources/ElizaBunRuntimePlugin/bridge/LlamaBridgeImpl.swift +1863 -0
  33. package/ios/Sources/ElizaBunRuntimePlugin/bridge/LogBridge.swift +36 -0
  34. package/ios/Sources/ElizaBunRuntimePlugin/bridge/PathsBridge.swift +41 -0
  35. package/ios/Sources/ElizaBunRuntimePlugin/bridge/ProcessBridge.swift +80 -0
  36. package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteBridge.swift +406 -0
  37. package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteBridgeInstaller.swift +17 -0
  38. package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteVecLoader.swift +66 -0
  39. package/ios/Sources/ElizaBunRuntimePlugin/bridge/UIBridge.swift +72 -0
  40. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlChinesePhonemizer.swift +313 -0
  41. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlConfiguration.swift +28 -0
  42. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlEngine.swift +325 -0
  43. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlHindiPhonemizer.swift +150 -0
  44. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlJapanesePhonemizer.swift +209 -0
  45. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlLatinPhonemizer.swift +374 -0
  46. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlModel.swift +87 -0
  47. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlPhonemizer.swift +679 -0
  48. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlPronunciationDicts.swift +131 -0
  49. package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlSupport.swift +24 -0
  50. package/ios/Tests/llama-bridge-smoke-main.swift +92 -0
  51. package/package.json +68 -0
  52. package/src/bridge-contract.test.ts +127 -0
  53. package/src/definitions.d.ts +136 -0
  54. package/src/definitions.d.ts.map +1 -0
  55. package/src/definitions.ts +152 -0
  56. package/src/index.d.ts +9 -0
  57. package/src/index.d.ts.map +1 -0
  58. package/src/index.ts +16 -0
  59. package/src/web.d.ts +19 -0
  60. package/src/web.d.ts.map +1 -0
  61. package/src/web.ts +80 -0
@@ -0,0 +1,36 @@
1
+ import Foundation
2
+ import JavaScriptCore
3
+ import os.log
4
+
5
+ /// Implements `log(level, message)` from `BRIDGE_CONTRACT.md` via `os_log`.
6
+ public final class LogBridge {
7
+ private let logger: OSLog
8
+
9
+ public init(subsystem: String = "ai.eliza.bun.runtime", category: String = "agent") {
10
+ self.logger = OSLog(subsystem: subsystem, category: category)
11
+ }
12
+
13
+ public func install(into ctx: JSContext) {
14
+ ctx.installBridgeFunction(name: "log") { args in
15
+ guard args.count >= 2,
16
+ let level = args[0].toString(),
17
+ let message = args[1].toString() else {
18
+ return NSNull()
19
+ }
20
+ self.log(level: level, message: message)
21
+ return NSNull()
22
+ }
23
+ }
24
+
25
+ private func log(level: String, message: String) {
26
+ let type: OSLogType
27
+ switch level.lowercased() {
28
+ case "debug": type = .debug
29
+ case "info": type = .info
30
+ case "warn", "warning": type = .default
31
+ case "error": type = .error
32
+ default: type = .default
33
+ }
34
+ os_log("%{public}@", log: logger, type: type, message)
35
+ }
36
+ }
@@ -0,0 +1,41 @@
1
+ import Foundation
2
+ import JavaScriptCore
3
+
4
+ /// Implements the `paths_*` host functions from `BRIDGE_CONTRACT.md`.
5
+ public final class PathsBridge {
6
+ private let paths: SandboxPaths
7
+ private weak var context: JSContext?
8
+
9
+ public init(paths: SandboxPaths) {
10
+ self.paths = paths
11
+ }
12
+
13
+ public func install(into ctx: JSContext) {
14
+ self.context = ctx
15
+
16
+ ctx.installBridgeFunction(name: "paths_app_support") { _ in
17
+ return self.paths.appSupport.path
18
+ }
19
+ ctx.installBridgeFunction(name: "paths_documents") { _ in
20
+ return self.paths.documents.path
21
+ }
22
+ ctx.installBridgeFunction(name: "paths_caches") { _ in
23
+ return self.paths.caches.path
24
+ }
25
+ ctx.installBridgeFunction(name: "paths_tmp") { _ in
26
+ return self.paths.tmp.path
27
+ }
28
+ ctx.installBridgeFunction(name: "paths_bundle") { _ in
29
+ return self.paths.bundle.path
30
+ }
31
+ ctx.installBridgeFunction(name: "paths_bundle_resource") { args in
32
+ guard args.count >= 2,
33
+ let name = args[0].toString(),
34
+ let ext = args[1].toString() else {
35
+ return NSNull()
36
+ }
37
+ let url = Bundle.main.url(forResource: name, withExtension: ext.isEmpty ? nil : ext)
38
+ return url?.path ?? NSNull()
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,80 @@
1
+ import Foundation
2
+ import JavaScriptCore
3
+
4
+ /// Implements `now_ns`, `argv`, `env_*`, `exit` from `BRIDGE_CONTRACT.md`.
5
+ public final class ProcessBridge {
6
+ private var env: [String: String] = [:]
7
+ private var argv: [String] = ["bun", "public/agent/agent-bundle.js"]
8
+ /// Snapshot of the monotonic clock origin for `now_ns`.
9
+ private let monotonicEpoch: UInt64
10
+ private weak var owner: ElizaBunRuntime?
11
+
12
+ public init(initialArgv: [String], initialEnv: [String: String], owner: ElizaBunRuntime) {
13
+ var merged = ProcessInfo.processInfo.environment
14
+ for (k, v) in initialEnv {
15
+ merged[k] = v
16
+ }
17
+ self.env = IosRuntimePolicy.sanitizeEnvironment(merged)
18
+ if !initialArgv.isEmpty {
19
+ self.argv = initialArgv
20
+ }
21
+ self.monotonicEpoch = Self.machRaw()
22
+ self.owner = owner
23
+ }
24
+
25
+ public func install(into ctx: JSContext) {
26
+ ctx.installBridgeFunction(name: "now_ns") { _ in
27
+ return NSNumber(value: Self.elapsedNanos(since: self.monotonicEpoch))
28
+ }
29
+
30
+ ctx.installBridgeFunction(name: "argv") { _ in
31
+ return self.argv
32
+ }
33
+
34
+ ctx.installBridgeFunction(name: "env_get") { args in
35
+ guard let key = args.first?.toString() else { return NSNull() }
36
+ return self.env[key] ?? NSNull()
37
+ }
38
+
39
+ ctx.installBridgeFunction(name: "env_set") { args in
40
+ guard args.count >= 2,
41
+ let key = args[0].toString(),
42
+ let value = args[1].toString() else {
43
+ return NSNull()
44
+ }
45
+ self.env[key] = value
46
+ return NSNull()
47
+ }
48
+
49
+ ctx.installBridgeFunction(name: "env_keys") { _ in
50
+ return Array(self.env.keys)
51
+ }
52
+
53
+ ctx.installBridgeFunction(name: "exit") { args in
54
+ let code = args.first?.toNumber()?.intValue ?? 0
55
+ NSLog("[ElizaBunRuntime] agent called exit(\(code))")
56
+ // We do not actually call `Foundation.exit()` here. Killing the
57
+ // host process from the agent is rude. Instead, tear down the
58
+ // JSContext and signal the plugin so it surfaces a stopped state
59
+ // to the React UI.
60
+ self.owner?.handleAgentExit(code: code)
61
+ return NSNull()
62
+ }
63
+ }
64
+
65
+ // MARK: - Monotonic clock
66
+
67
+ private static func machRaw() -> UInt64 {
68
+ // mach_absolute_time returns ticks. Convert to nanoseconds via
69
+ // mach_timebase_info.
70
+ var info = mach_timebase_info_data_t()
71
+ mach_timebase_info(&info)
72
+ let ticks = mach_absolute_time()
73
+ return ticks &* UInt64(info.numer) / UInt64(max(info.denom, 1))
74
+ }
75
+
76
+ private static func elapsedNanos(since epoch: UInt64) -> Double {
77
+ let now = machRaw()
78
+ return Double(now &- epoch)
79
+ }
80
+ }
@@ -0,0 +1,406 @@
1
+ import Foundation
2
+ import JavaScriptCore
3
+ import SQLite3
4
+
5
+ // SQLite's transient/static destructor sentinels for `sqlite3_bind_*`.
6
+ // SQLITE_TRANSIENT forces sqlite3 to copy the bound bytes; SQLITE_STATIC
7
+ // promises the buffer outlives the statement. We always use TRANSIENT for
8
+ // safety since the Swift bridge passes Data slices that may be released.
9
+ private let SQLITE_TRANSIENT_BRIDGE = unsafeBitCast(
10
+ OpaquePointer(bitPattern: -1),
11
+ to: sqlite3_destructor_type.self
12
+ )
13
+
14
+ /// Implements the `sqlite_*` host functions from `BRIDGE_CONTRACT.md`.
15
+ ///
16
+ /// PGlite cannot run inside JSContext on iOS 16.4+ (WebAssembly is disabled),
17
+ /// so the agent's plugin-sql is rebased onto SQLite via the polyfill's
18
+ /// `pglite-shim.ts`. This bridge exposes the raw libsqlite3 surface that
19
+ /// shim consumes.
20
+ ///
21
+ /// Threading: every host-function body runs on the JSContext queue
22
+ /// (`ai.eliza.bun.runtime`). libsqlite3 is opened with `SQLITE_OPEN_FULLMUTEX`
23
+ /// so the same handle can be safely re-entered from that queue without extra
24
+ /// locking. Cross-queue use is forbidden by the contract.
25
+ ///
26
+ /// Resource ownership:
27
+ /// - Open handles live in `databases[Int]` keyed by monotonic IDs.
28
+ /// - Prepared statements live in `statements[Int]` keyed by monotonic IDs.
29
+ /// - All allocations are freed when the JS side calls `sqlite_close` or
30
+ /// `sqlite_finalize`; the bridge also closes everything on `shutdown()`.
31
+ public final class SqliteBridge {
32
+ private weak var context: JSContext?
33
+ private var nextDbId: Int = 1
34
+ private var nextStmtId: Int = 1
35
+ private var databases: [Int: OpaquePointer] = [:]
36
+ private var statements: [Int: StatementState] = [:]
37
+
38
+ private struct StatementState {
39
+ let stmt: OpaquePointer
40
+ weak var dbValue: AnyObject?
41
+ let dbId: Int
42
+ }
43
+
44
+ public init() {}
45
+
46
+ public func install(into ctx: JSContext) {
47
+ self.context = ctx
48
+
49
+ ctx.installBridgeFunction(name: "sqlite_open") { args in
50
+ return self.open(args: args, ctx: ctx)
51
+ }
52
+
53
+ ctx.installBridgeFunction(name: "sqlite_close") { args in
54
+ guard let id = args.first?.toNumber()?.intValue else { return false }
55
+ return self.closeDb(id: id)
56
+ }
57
+
58
+ ctx.installBridgeFunction(name: "sqlite_exec") { args in
59
+ return self.exec(args: args)
60
+ }
61
+
62
+ ctx.installBridgeFunction(name: "sqlite_query") { args in
63
+ return self.query(args: args, ctx: ctx)
64
+ }
65
+
66
+ ctx.installBridgeFunction(name: "sqlite_prepare") { args in
67
+ return self.prepare(args: args)
68
+ }
69
+
70
+ ctx.installBridgeFunction(name: "sqlite_step") { args in
71
+ return self.step(args: args, ctx: ctx)
72
+ }
73
+
74
+ ctx.installBridgeFunction(name: "sqlite_finalize") { args in
75
+ guard let id = args.first?.toNumber()?.intValue else { return false }
76
+ return self.finalizeStmt(id: id)
77
+ }
78
+
79
+ ctx.installBridgeFunction(name: "sqlite_version") { _ in
80
+ return self.version()
81
+ }
82
+ }
83
+
84
+ /// Closes every open DB and statement. Call when the runtime shuts down.
85
+ public func shutdown() {
86
+ for (id, _) in statements {
87
+ _ = finalizeStmt(id: id)
88
+ }
89
+ for (id, _) in databases {
90
+ _ = closeDb(id: id)
91
+ }
92
+ }
93
+
94
+ // MARK: - Open / close
95
+
96
+ private func open(args: [JSValue], ctx: JSContext) -> Any? {
97
+ guard let opts = args.first, opts.isObject else {
98
+ return ["error": "sqlite_open: missing options"]
99
+ }
100
+ guard let path = opts.forProperty("path")?.toString() else {
101
+ return ["error": "sqlite_open: missing path"]
102
+ }
103
+ let readonly = opts.forProperty("readonly")?.toBool() ?? false
104
+ let timeoutMs = opts.forProperty("timeout_ms")?.toNumber()?.intValue ?? 0
105
+
106
+ // SqliteVecLoader will call sqlite3_vec_init on the resulting handle
107
+ // when the static lib is linked. The flag set ensures multi-threaded
108
+ // safety, which we need for re-entrant access from the same queue.
109
+ var handle: OpaquePointer?
110
+ let flags: Int32 = readonly
111
+ ? (SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX)
112
+ : (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX)
113
+
114
+ let rc = sqlite3_open_v2(path, &handle, flags, nil)
115
+ if rc != SQLITE_OK {
116
+ let message = handle != nil ? String(cString: sqlite3_errmsg(handle)) : "open failed (rc=\(rc))"
117
+ if let h = handle { sqlite3_close_v2(h) }
118
+ return ["error": "sqlite_open: \(message)"]
119
+ }
120
+ guard let db = handle else {
121
+ return ["error": "sqlite_open: sqlite3_open_v2 returned nil handle"]
122
+ }
123
+
124
+ if timeoutMs > 0 {
125
+ sqlite3_busy_timeout(db, Int32(timeoutMs))
126
+ }
127
+
128
+ // Activate sqlite-vec on this handle. The loader skips registration when the
129
+ // extension isn't statically linked.
130
+ SqliteVecLoader.shared.register(on: db)
131
+
132
+ let id = nextDbId
133
+ nextDbId += 1
134
+ databases[id] = db
135
+ return ["db_id": id]
136
+ }
137
+
138
+ private func closeDb(id: Int) -> Bool {
139
+ guard let db = databases.removeValue(forKey: id) else { return false }
140
+ // Finalize any statements that still reference this DB before closing,
141
+ // otherwise sqlite3_close_v2 leaks the prepared-statement memory until
142
+ // the orphans GC themselves.
143
+ let owned = statements.compactMap { (stmtId, state) -> Int? in
144
+ state.dbId == id ? stmtId : nil
145
+ }
146
+ for sid in owned {
147
+ _ = finalizeStmt(id: sid)
148
+ }
149
+ sqlite3_close_v2(db)
150
+ return true
151
+ }
152
+
153
+ // MARK: - Exec
154
+
155
+ private func exec(args: [JSValue]) -> Any? {
156
+ guard args.count >= 2,
157
+ let id = args[0].toNumber()?.intValue,
158
+ let sql = args[1].toString() else {
159
+ return ["error": "sqlite_exec: missing args"]
160
+ }
161
+ guard let db = databases[id] else {
162
+ return ["error": "sqlite_exec: invalid db_id \(id)"]
163
+ }
164
+ var errPtr: UnsafeMutablePointer<Int8>?
165
+ let rc = sqlite3_exec(db, sql, nil, nil, &errPtr)
166
+ if rc != SQLITE_OK {
167
+ let msg = errPtr != nil ? String(cString: errPtr!) : String(cString: sqlite3_errmsg(db))
168
+ if let p = errPtr { sqlite3_free(p) }
169
+ return ["error": "sqlite_exec: \(msg)"]
170
+ }
171
+ if let p = errPtr { sqlite3_free(p) }
172
+ return ["rows_affected": Int(sqlite3_changes(db))]
173
+ }
174
+
175
+ // MARK: - Query (one-shot)
176
+
177
+ private func query(args: [JSValue], ctx: JSContext) -> Any? {
178
+ guard args.count >= 2,
179
+ let id = args[0].toNumber()?.intValue,
180
+ let sql = args[1].toString() else {
181
+ return ["error": "sqlite_query: missing args"]
182
+ }
183
+ guard let db = databases[id] else {
184
+ return ["error": "sqlite_query: invalid db_id \(id)"]
185
+ }
186
+ let params: [JSValue] = (args.count >= 3 && args[2].isArray)
187
+ ? (args[2].toArray()?.compactMap { item -> JSValue? in
188
+ if let v = item as? JSValue { return v }
189
+ return JSValue(object: item, in: ctx)
190
+ } ?? [])
191
+ : []
192
+
193
+ var stmt: OpaquePointer?
194
+ var rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nil)
195
+ if rc != SQLITE_OK {
196
+ let msg = String(cString: sqlite3_errmsg(db))
197
+ return ["error": "sqlite_query.prepare: \(msg)"]
198
+ }
199
+ defer { if let s = stmt { sqlite3_finalize(s) } }
200
+ guard let s = stmt else {
201
+ return ["error": "sqlite_query.prepare: returned nil statement"]
202
+ }
203
+
204
+ if let bindErr = bindParams(stmt: s, params: params, ctx: ctx) {
205
+ return ["error": "sqlite_query.bind: \(bindErr)"]
206
+ }
207
+
208
+ let columnCount = Int(sqlite3_column_count(s))
209
+ var columns: [String] = []
210
+ columns.reserveCapacity(columnCount)
211
+ for i in 0..<columnCount {
212
+ if let cName = sqlite3_column_name(s, Int32(i)) {
213
+ columns.append(String(cString: cName))
214
+ } else {
215
+ columns.append("column\(i)")
216
+ }
217
+ }
218
+
219
+ var rows: [[Any?]] = []
220
+ while true {
221
+ rc = sqlite3_step(s)
222
+ if rc == SQLITE_DONE { break }
223
+ if rc != SQLITE_ROW {
224
+ let msg = String(cString: sqlite3_errmsg(db))
225
+ return ["error": "sqlite_query.step: \(msg)"]
226
+ }
227
+ rows.append(readRow(stmt: s, columnCount: columnCount, ctx: ctx))
228
+ }
229
+
230
+ return ["columns": columns, "rows": rows]
231
+ }
232
+
233
+ // MARK: - Prepare / step / finalize
234
+
235
+ private func prepare(args: [JSValue]) -> Any? {
236
+ guard args.count >= 2,
237
+ let id = args[0].toNumber()?.intValue,
238
+ let sql = args[1].toString() else {
239
+ return ["error": "sqlite_prepare: missing args"]
240
+ }
241
+ guard let db = databases[id] else {
242
+ return ["error": "sqlite_prepare: invalid db_id \(id)"]
243
+ }
244
+ var stmt: OpaquePointer?
245
+ let rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nil)
246
+ if rc != SQLITE_OK {
247
+ let msg = String(cString: sqlite3_errmsg(db))
248
+ return ["error": "sqlite_prepare: \(msg)"]
249
+ }
250
+ guard let s = stmt else {
251
+ return ["error": "sqlite_prepare: returned nil statement"]
252
+ }
253
+ let sid = nextStmtId
254
+ nextStmtId += 1
255
+ statements[sid] = StatementState(stmt: s, dbValue: nil, dbId: id)
256
+ return ["stmt_id": sid]
257
+ }
258
+
259
+ private func step(args: [JSValue], ctx: JSContext) -> Any? {
260
+ guard let stmtId = args.first?.toNumber()?.intValue else {
261
+ return ["error": "sqlite_step: missing stmt_id"]
262
+ }
263
+ guard let state = statements[stmtId] else {
264
+ return ["error": "sqlite_step: invalid stmt_id \(stmtId)"]
265
+ }
266
+ let stmt = state.stmt
267
+
268
+ // Re-bind on every call where params are provided. SQLite's
269
+ // sqlite3_reset clears the row cursor but preserves bindings; we
270
+ // explicitly clear+rebind to give callers a fresh-call illusion.
271
+ if args.count >= 2 && args[1].isArray {
272
+ sqlite3_reset(stmt)
273
+ sqlite3_clear_bindings(stmt)
274
+ let params: [JSValue] = args[1].toArray()?.compactMap { item -> JSValue? in
275
+ if let v = item as? JSValue { return v }
276
+ return JSValue(object: item, in: ctx)
277
+ } ?? []
278
+ if let bindErr = bindParams(stmt: stmt, params: params, ctx: ctx) {
279
+ return ["error": "sqlite_step.bind: \(bindErr)"]
280
+ }
281
+ }
282
+
283
+ let rc = sqlite3_step(stmt)
284
+ if rc == SQLITE_DONE {
285
+ return ["done": true]
286
+ }
287
+ if rc == SQLITE_ROW {
288
+ let columnCount = Int(sqlite3_column_count(stmt))
289
+ let row = readRow(stmt: stmt, columnCount: columnCount, ctx: ctx)
290
+ return ["done": false, "row": row]
291
+ }
292
+ guard let db = databases[state.dbId] else {
293
+ return ["error": "sqlite_step: db handle gone"]
294
+ }
295
+ let msg = String(cString: sqlite3_errmsg(db))
296
+ return ["error": "sqlite_step: \(msg)"]
297
+ }
298
+
299
+ private func finalizeStmt(id: Int) -> Bool {
300
+ guard let state = statements.removeValue(forKey: id) else { return false }
301
+ sqlite3_finalize(state.stmt)
302
+ return true
303
+ }
304
+
305
+ // MARK: - Version
306
+
307
+ private func version() -> [String: Any] {
308
+ var out: [String: Any] = [
309
+ "sqlite": String(cString: sqlite3_libversion()),
310
+ ]
311
+ if let v = SqliteVecLoader.shared.versionString {
312
+ out["sqlite_vec"] = v
313
+ }
314
+ return out
315
+ }
316
+
317
+ // MARK: - Binding + reading
318
+
319
+ /// Binds positional parameters (1-indexed in SQLite) to `stmt`.
320
+ /// Returns an error message on failure or `nil` on success.
321
+ private func bindParams(stmt: OpaquePointer, params: [JSValue], ctx: JSContext) -> String? {
322
+ for (i, value) in params.enumerated() {
323
+ let idx = Int32(i + 1)
324
+ let rc: Int32
325
+ if value.isNullish {
326
+ rc = sqlite3_bind_null(stmt, idx)
327
+ } else if value.isBoolean {
328
+ rc = sqlite3_bind_int64(stmt, idx, value.toBool() ? 1 : 0)
329
+ } else if value.isNumber {
330
+ // JS numbers are doubles. Detect integers and bind as INT64
331
+ // to preserve exact representation for IDs and timestamps.
332
+ let dbl = value.toDouble()
333
+ if dbl.truncatingRemainder(dividingBy: 1) == 0
334
+ && dbl >= -9.2233720368547758e18
335
+ && dbl <= 9.2233720368547758e18 {
336
+ rc = sqlite3_bind_int64(stmt, idx, Int64(dbl))
337
+ } else {
338
+ rc = sqlite3_bind_double(stmt, idx, dbl)
339
+ }
340
+ } else if value.isString {
341
+ let s = value.toString() ?? ""
342
+ rc = sqlite3_bind_text(stmt, idx, s, -1, SQLITE_TRANSIENT_BRIDGE)
343
+ } else if let data = value.toData() {
344
+ // Treat any TypedArray / ArrayBuffer-backed value as a BLOB.
345
+ if data.isEmpty {
346
+ rc = sqlite3_bind_zeroblob(stmt, idx, 0)
347
+ } else {
348
+ rc = data.withUnsafeBytes { (raw: UnsafeRawBufferPointer) -> Int32 in
349
+ guard let base = raw.baseAddress else {
350
+ return SQLITE_INTERNAL
351
+ }
352
+ return sqlite3_bind_blob(stmt, idx, base, Int32(data.count), SQLITE_TRANSIENT_BRIDGE)
353
+ }
354
+ }
355
+ } else {
356
+ // Fall back to string coercion for unexpected JS types
357
+ // (Date, Object, etc.). The shim layer typically pre-converts
358
+ // these into ISO-8601 strings or JSON.
359
+ let s = value.toString() ?? "null"
360
+ rc = sqlite3_bind_text(stmt, idx, s, -1, SQLITE_TRANSIENT_BRIDGE)
361
+ }
362
+ if rc != SQLITE_OK {
363
+ return "bind index \(idx): rc=\(rc)"
364
+ }
365
+ }
366
+ return nil
367
+ }
368
+
369
+ /// Reads one row of a stepped statement into a Swift array. NULL → NSNull,
370
+ /// INTEGER → Int, FLOAT → Double, TEXT → String, BLOB → Uint8Array JSValue.
371
+ private func readRow(stmt: OpaquePointer, columnCount: Int, ctx: JSContext) -> [Any?] {
372
+ var row: [Any?] = []
373
+ row.reserveCapacity(columnCount)
374
+ for i in 0..<columnCount {
375
+ let col = Int32(i)
376
+ let type = sqlite3_column_type(stmt, col)
377
+ switch type {
378
+ case SQLITE_INTEGER:
379
+ row.append(NSNumber(value: sqlite3_column_int64(stmt, col)))
380
+ case SQLITE_FLOAT:
381
+ row.append(NSNumber(value: sqlite3_column_double(stmt, col)))
382
+ case SQLITE_TEXT:
383
+ if let cs = sqlite3_column_text(stmt, col) {
384
+ row.append(String(cString: cs))
385
+ } else {
386
+ row.append(NSNull())
387
+ }
388
+ case SQLITE_BLOB:
389
+ let n = Int(sqlite3_column_bytes(stmt, col))
390
+ if n == 0 {
391
+ row.append(ctx.newUint8Array(Data()))
392
+ } else if let raw = sqlite3_column_blob(stmt, col) {
393
+ let data = Data(bytes: raw, count: n)
394
+ row.append(ctx.newUint8Array(data))
395
+ } else {
396
+ row.append(ctx.newUint8Array(Data()))
397
+ }
398
+ case SQLITE_NULL:
399
+ row.append(NSNull())
400
+ default:
401
+ row.append(NSNull())
402
+ }
403
+ }
404
+ return row
405
+ }
406
+ }
@@ -0,0 +1,17 @@
1
+ import Foundation
2
+ import JavaScriptCore
3
+
4
+ /// One-shot installer for the SQLite bridge module.
5
+ ///
6
+ /// The main `BridgeInstaller` should call `SqliteBridgeInstaller.install(into:)`
7
+ /// once during runtime startup, alongside the other bridge installations.
8
+ /// Returns the bridge instance so the caller can invoke `shutdown()` during
9
+ /// runtime teardown.
10
+ public enum SqliteBridgeInstaller {
11
+ @discardableResult
12
+ public static func install(into ctx: JSContext) -> SqliteBridge {
13
+ let bridge = SqliteBridge()
14
+ bridge.install(into: ctx)
15
+ return bridge
16
+ }
17
+ }
@@ -0,0 +1,66 @@
1
+ import Foundation
2
+ import SQLite3
3
+
4
+ /// Conditional loader for the `sqlite-vec` (https://github.com/asg017/sqlite-vec)
5
+ /// extension.
6
+ ///
7
+ /// When the static library is linked into the binary, the symbol
8
+ /// `sqlite3_vec_init` is available and we invoke it on each opened DB so
9
+ /// the `vec0` virtual table module and the `vec_distance_*` SQL helpers
10
+ /// are registered.
11
+ ///
12
+ /// When sqlite-vec is *not* linked (e.g. simulator builds without the
13
+ /// vendor-deps step), the weak C shim reports unavailable, the loader is a
14
+ /// skipped, and `versionString` is nil. Vector queries against tables that need
15
+ /// the extension will fail with SQLite's standard "no such module: vec0"
16
+ /// error.
17
+ public final class SqliteVecLoader {
18
+ public static let shared = SqliteVecLoader()
19
+
20
+ /// Reported by `bridge.sqlite_version()`. `nil` when the extension is
21
+ /// not statically linked.
22
+ public private(set) var versionString: String?
23
+
24
+ private let available: Bool
25
+
26
+ private init() {
27
+ self.available = eliza_sqlite_vec_is_available() == 1
28
+ if let cstr = eliza_sqlite_vec_version() {
29
+ self.versionString = String(cString: cstr)
30
+ }
31
+ }
32
+
33
+ /// Returns true iff the static lib is linked. Useful for logging and
34
+ /// for the `sqlite_version` host function.
35
+ public var isAvailable: Bool { available }
36
+
37
+ /// Calls `sqlite3_vec_init` on the given DB handle. Skipped when the
38
+ /// extension isn't linked. Errors during init are surfaced through
39
+ /// stderr-only logging because we don't have a way to fail the open
40
+ /// — the rest of the DB still works without vec0.
41
+ public func register(on db: OpaquePointer) {
42
+ guard available else { return }
43
+ var errPtr: UnsafeMutablePointer<Int8>? = nil
44
+ let rc = eliza_sqlite_vec_register(db, &errPtr)
45
+ if rc != SQLITE_OK {
46
+ let msg = errPtr != nil ? String(cString: errPtr!) : "sqlite3_vec_init rc=\(rc)"
47
+ if let p = errPtr { sqlite3_free(p) }
48
+ // We deliberately don't propagate — the bridge consumer
49
+ // already logged the open, and vec queries that depend on
50
+ // the extension will fail loudly at their own call sites.
51
+ NSLog("[eliza-sqlite-vec] init failed: %@", msg)
52
+ }
53
+ }
54
+ }
55
+
56
+ @_silgen_name("eliza_sqlite_vec_is_available")
57
+ private func eliza_sqlite_vec_is_available() -> Int32
58
+
59
+ @_silgen_name("eliza_sqlite_vec_version")
60
+ private func eliza_sqlite_vec_version() -> UnsafePointer<CChar>?
61
+
62
+ @_silgen_name("eliza_sqlite_vec_register")
63
+ private func eliza_sqlite_vec_register(
64
+ _ db: OpaquePointer?,
65
+ _ pzErrMsg: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
66
+ ) -> Int32