@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.
- package/ElizaosCapacitorBunRuntime.podspec +54 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/esm/definitions.d.ts +136 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +14 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +19 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +44 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +63 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +66 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/ElizaBunRuntimePlugin/BridgeInstaller.swift +94 -0
- package/ios/Sources/ElizaBunRuntimePlugin/ElizaBunRuntime.swift +705 -0
- package/ios/Sources/ElizaBunRuntimePlugin/ElizaBunRuntimePlugin.swift +1109 -0
- package/ios/Sources/ElizaBunRuntimePlugin/FullBunEngineHost.swift +677 -0
- package/ios/Sources/ElizaBunRuntimePlugin/JSContextHelpers.swift +226 -0
- package/ios/Sources/ElizaBunRuntimePlugin/SandboxPaths.swift +46 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/CryptoBridge.swift +238 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/ElizaSqliteVecBridge.m +28 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/FSBridge.swift +270 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/HTTPBridge.swift +153 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/HTTPServerBridge.swift +32 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/LlamaBridge.swift +233 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/LlamaBridgeImpl.swift +1863 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/LogBridge.swift +36 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/PathsBridge.swift +41 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/ProcessBridge.swift +80 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteBridge.swift +406 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteBridgeInstaller.swift +17 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteVecLoader.swift +66 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/UIBridge.swift +72 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlChinesePhonemizer.swift +313 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlConfiguration.swift +28 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlEngine.swift +325 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlHindiPhonemizer.swift +150 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlJapanesePhonemizer.swift +209 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlLatinPhonemizer.swift +374 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlModel.swift +87 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlPhonemizer.swift +679 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlPronunciationDicts.swift +131 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlSupport.swift +24 -0
- package/ios/Tests/llama-bridge-smoke-main.swift +92 -0
- package/package.json +68 -0
- package/src/bridge-contract.test.ts +127 -0
- package/src/definitions.d.ts +136 -0
- package/src/definitions.d.ts.map +1 -0
- package/src/definitions.ts +152 -0
- package/src/index.d.ts +9 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.ts +16 -0
- package/src/web.d.ts +19 -0
- package/src/web.d.ts.map +1 -0
- 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
|