catalyst-core 0.3.0-beta.1 → 0.3.0-beta.3

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 (33) hide show
  1. package/dist/globals.d.ts +7 -0
  2. package/dist/native/androidProject/app/build.gradle.kts +45 -4
  3. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +86 -0
  4. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +8 -0
  5. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +173 -13
  6. package/dist/native/androidProject/settings.gradle.kts +10 -0
  7. package/dist/native/androidSetup.js +8 -6
  8. package/dist/native/bridge/constants/NativeInterfaces.js +15 -1
  9. package/dist/native/bridge/hooks/index.js +7 -0
  10. package/dist/native/bridge/hooks/useAI.js +94 -0
  11. package/dist/native/buildAndroid/assets.js +1 -1
  12. package/dist/native/buildAndroid/index.js +32 -0
  13. package/dist/native/buildIos/assets.js +1 -1
  14. package/dist/offline/catalyst-sw.js +1 -0
  15. package/dist/scripts/build.js +15 -11
  16. package/dist/scripts/generateOfflineManifest.js +1 -0
  17. package/dist/scripts/scriptUtils.js +1 -0
  18. package/dist/scripts/serve.js +24 -22
  19. package/dist/scripts/start.js +1 -3
  20. package/dist/server/expressServer.js +47 -0
  21. package/dist/server/renderer/document/Head.jsx +2 -0
  22. package/dist/server/renderer/extract.js +1 -0
  23. package/dist/server/renderer/handler.jsx +29 -11
  24. package/dist/vite/inject-cache-key-plugin.js +7 -0
  25. package/dist/vite/loadCustomViteConfig.js +1 -0
  26. package/dist/vite/manifest-categorization-plugin.js +2 -0
  27. package/dist/vite/node-loader.mjs +6 -0
  28. package/dist/vite/vite.config.js +83 -1
  29. package/mcp_v2/knowledge-base.json +52 -4
  30. package/mcp_v2/mcp.js +36 -4
  31. package/mcp_v2/tools/build.js +46 -18
  32. package/mcp_v2/tools/github.js +11 -0
  33. package/package.json +3 -3
@@ -0,0 +1,7 @@
1
+ // Injected by webpack DefinePlugin in base.babel.js at build time.
2
+ // Reflects which @catalyst/* packages are present in the app's node_modules.
3
+ declare const __CATALYST_PACKAGES__: {
4
+ readonly ai: boolean
5
+ readonly webAILocal: boolean
6
+ readonly nativeAILocal: boolean
7
+ }
@@ -60,6 +60,22 @@ fun isNotificationsEnabled(): Boolean {
60
60
  }
61
61
  }
62
62
 
63
+ fun isAIEnabled(): Boolean {
64
+ return try {
65
+ if (configPath == null) return false
66
+ val configFile = File(configPath!!)
67
+ if (!configFile.exists()) return false
68
+
69
+ val json = JSONObject(configFile.readText())
70
+ if (!json.has("WEBVIEW_CONFIG")) return false
71
+
72
+ val webviewConfig = json.getJSONObject("WEBVIEW_CONFIG")
73
+ webviewConfig.optJSONObject("ai")?.optBoolean("enabled", false) ?: false
74
+ } catch (e: Exception) {
75
+ false
76
+ }
77
+ }
78
+
63
79
  fun getLocalIpAddress(): String {
64
80
  return NetworkInterface.getNetworkInterfaces().toList()
65
81
  .flatMap { it.inetAddresses.toList() }
@@ -151,6 +167,7 @@ android {
151
167
 
152
168
  kotlinOptions {
153
169
  jvmTarget = "11"
170
+ freeCompilerArgs += "-Xskip-metadata-version-check"
154
171
  }
155
172
 
156
173
  buildFeatures {
@@ -168,8 +185,9 @@ android {
168
185
  excludes.add("META-INF/io.netty.versions.properties")
169
186
  }
170
187
  jniLibs {
171
- // Enable 16KB page size alignment for all native libraries
172
- useLegacyPackaging = false
188
+ // libLiteRtClGlAccelerator.so must be extracted to disk for GPU dlopen().
189
+ // This must live in the app module — AGP ignores useLegacyPackaging in library modules.
190
+ useLegacyPackaging = isAIEnabled()
173
191
  }
174
192
  }
175
193
 
@@ -186,7 +204,7 @@ android {
186
204
  }
187
205
  }
188
206
 
189
- // Conditional source sets based on notifications config
207
+ // Conditional source sets based on feature config
190
208
  sourceSets {
191
209
  getByName("main") {
192
210
  if (isNotificationsEnabled()) {
@@ -196,6 +214,20 @@ android {
196
214
  logger.info("SourceSet selected: noFcm (notifications disabled)")
197
215
  java.srcDirs("src/noFcm/java")
198
216
  }
217
+ // AI has no source-set swap — CatalystAIBridge self-registers via ServiceLoader
218
+ // when @catalyst/cloud-ai is on the classpath.
219
+ }
220
+ }
221
+ }
222
+
223
+ configurations.all {
224
+ resolutionStrategy.eachDependency {
225
+ if (requested.group == "org.jetbrains.kotlin" &&
226
+ (requested.name == "kotlin-stdlib" ||
227
+ requested.name == "kotlin-stdlib-jdk7" ||
228
+ requested.name == "kotlin-stdlib-jdk8" ||
229
+ requested.name == "kotlin-reflect")) {
230
+ useVersion("2.0.21")
199
231
  }
200
232
  }
201
233
  }
@@ -217,7 +249,16 @@ dependencies {
217
249
  implementation("io.ktor:ktor-server-core:3.0.3")
218
250
  implementation("io.ktor:ktor-server-netty:3.0.3")
219
251
  implementation("io.ktor:ktor-server-content-negotiation:3.0.3")
220
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
252
+ implementation("io.ktor:ktor-server-sse:3.0.3")
253
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
254
+
255
+ // Native AI — compileOnly always (for types), implementation only when ai.enabled=true (for runtime bundling)
256
+ if (project.findProject(":catalyst-cloud-ai") != null) {
257
+ compileOnly(project(":catalyst-cloud-ai"))
258
+ if (isAIEnabled()) {
259
+ implementation(project(":catalyst-cloud-ai"))
260
+ }
261
+ }
221
262
 
222
263
  // Security detection dependencies
223
264
  implementation("com.scottyab:rootbeer-lib:0.1.1") // Root detection
@@ -16,12 +16,15 @@ import androidx.credentials.exceptions.GetCredentialCancellationException
16
16
  import androidx.credentials.exceptions.GetCredentialException
17
17
  import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
18
18
  import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
19
+ // LiteRT imports removed — AI logic lives in catalyst-ai (CatalystAIBridge plugin)
19
20
  import io.yourname.androidproject.MainActivity
20
21
  import io.yourname.androidproject.utils.*
21
22
  import kotlinx.coroutines.*
22
23
  import org.json.JSONArray
23
24
  import org.json.JSONObject
25
+ import java.io.File
24
26
  import java.util.Properties
27
+ import java.util.ServiceLoader
25
28
  import java.util.concurrent.atomic.AtomicBoolean
26
29
 
27
30
  private data class FilePickerOptions(
@@ -151,6 +154,61 @@ class NativeBridge(
151
154
 
152
155
  private var networkMonitor: NetworkMonitor? = null
153
156
 
157
+ // AI plugin — loaded via reflection so NativeBridge.kt compiles with no compile-time dep on
158
+ // :catalyst-ai. When the module is absent the try block returns null gracefully.
159
+ private val aiBridge: Any? = try {
160
+ @Suppress("UNCHECKED_CAST")
161
+ run {
162
+ val bridgeClass = Class.forName("io.catalyst.nativeai.AIBridge")
163
+ val callbacksClass = Class.forName("io.catalyst.nativeai.AIBridgeCallbacks")
164
+ @Suppress("UNCHECKED_CAST")
165
+ val loader = ServiceLoader.load(bridgeClass as Class<Any>)
166
+ val bridge = loader.iterator().takeIf { it.hasNext() }?.next() ?: return@run null
167
+
168
+ val callbacksProxy = java.lang.reflect.Proxy.newProxyInstance(
169
+ bridgeClass.classLoader,
170
+ arrayOf(callbacksClass)
171
+ ) { _, method, args ->
172
+ val a = args ?: emptyArray()
173
+ when (method.name) {
174
+ "onReady" -> {
175
+ val payload = org.json.JSONObject().apply {
176
+ put("url", a[0]); put("port", a[1]); put("sessionId", a[2])
177
+ }
178
+ BridgeUtils.notifyWebJson(webView, BridgeUtils.WebEvents.ON_AI_READY, payload)
179
+ }
180
+ "onProgress" -> {
181
+ val payload = org.json.JSONObject().apply {
182
+ put("phase", a[0]); put("percent", a[1])
183
+ put("bytesLoaded", a[2]); put("bytesTotal", a[3]); put("detail", a[4])
184
+ }
185
+ BridgeUtils.notifyWebJson(webView, BridgeUtils.WebEvents.ON_AI_PROGRESS, payload)
186
+ }
187
+ "onLog" -> {
188
+ val payload = org.json.JSONObject().apply { put("message", a[0]) }
189
+ BridgeUtils.notifyWebJson(webView, BridgeUtils.WebEvents.ON_AI_LOG, payload)
190
+ }
191
+ "onError" -> BridgeUtils.notifyWebError(webView, BridgeUtils.WebEvents.ON_AI_ERROR, a[0] as String)
192
+ "isFrameworkServerRunning" -> FrameworkServerUtils.isRunning()
193
+ "getFrameworkServerPort" -> FrameworkServerUtils.getServerPort()
194
+ "getFrameworkServerSessionId" -> FrameworkServerUtils.getSessionId()
195
+ "setNativeAiSupplier" -> {
196
+ @Suppress("UNCHECKED_CAST")
197
+ FrameworkServerUtils.setNativeAiSupplier(
198
+ a[0] as suspend (String, org.json.JSONObject, String?) -> Pair<String, kotlinx.coroutines.flow.Flow<String>>
199
+ )
200
+ }
201
+ "setNativeSystemPrompt" -> FrameworkServerUtils.setNativeSystemPrompt(a[0] as String)
202
+ else -> null
203
+ }
204
+ }
205
+
206
+ bridge.javaClass.getMethod("attach", android.app.Activity::class.java, android.webkit.WebView::class.java, callbacksClass)
207
+ .invoke(bridge, mainActivity, webView, callbacksProxy)
208
+ bridge
209
+ }
210
+ } catch (_: Throwable) { null }
211
+
154
212
  // Video stream manager
155
213
  private var nativeCameraManager: NativeCameraManager? = null
156
214
  private var webViewOriginalBackground: Int = android.graphics.Color.WHITE
@@ -1301,6 +1359,34 @@ class NativeBridge(
1301
1359
  }
1302
1360
  }
1303
1361
 
1362
+ // -------------------------------------------------------------------------
1363
+ // Native AI bridge methods — delegated to AIBridge plugin (ServiceLoader)
1364
+ // -------------------------------------------------------------------------
1365
+
1366
+ @JavascriptInterface
1367
+ fun isAIAvailable(): Boolean = aiBridge != null
1368
+
1369
+ @JavascriptInterface
1370
+ fun initAI(optionsRaw: String?) {
1371
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_AI_ERROR, "initialize AI") {
1372
+ aiBridge?.javaClass?.getMethod("initAI", String::class.java)?.invoke(aiBridge, optionsRaw)
1373
+ }
1374
+ }
1375
+
1376
+ @JavascriptInterface
1377
+ fun generateNative(optionsRaw: String?) {
1378
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_AI_ERROR, "generate with native AI") {
1379
+ aiBridge?.javaClass?.getMethod("generateNative", String::class.java)?.invoke(aiBridge, optionsRaw)
1380
+ }
1381
+ }
1382
+
1383
+ @JavascriptInterface
1384
+ fun clearNativeConversation() {
1385
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_AI_ERROR, "clear native conversation") {
1386
+ aiBridge?.javaClass?.getMethod("clearConversation")?.invoke(aiBridge)
1387
+ }
1388
+ }
1389
+
1304
1390
  /**
1305
1391
  * Cleanup method to be called when the bridge is being destroyed
1306
1392
  */
@@ -97,6 +97,14 @@ object BridgeUtils {
97
97
  ON_TRANSITION_CANCELLED("ON_TRANSITION_CANCELLED"),
98
98
  ON_TRANSITION_TIMEOUT("ON_TRANSITION_TIMEOUT"),
99
99
 
100
+ // Native AI events
101
+ ON_AI_READY("ON_AI_READY"),
102
+ ON_AI_ERROR("ON_AI_ERROR"),
103
+ ON_AI_TOKEN("ON_AI_TOKEN"),
104
+ ON_AI_DONE("ON_AI_DONE"),
105
+ ON_AI_PROGRESS("ON_AI_PROGRESS"),
106
+ ON_AI_LOG("ON_AI_LOG"),
107
+
100
108
  // Perf telemetry
101
109
  CATALYST_PERF_EVENT("CATALYST_PERF_EVENT"),
102
110
  }
@@ -7,15 +7,21 @@ import io.ktor.http.*
7
7
  import io.ktor.server.application.*
8
8
  import io.ktor.server.engine.*
9
9
  import io.ktor.server.netty.*
10
+ import io.ktor.server.request.*
10
11
  import io.ktor.server.response.*
11
12
  import io.ktor.server.routing.*
13
+ import io.ktor.http.ContentType
14
+ import io.ktor.server.response.respondTextWriter
15
+ import io.ktor.server.sse.*
12
16
  import kotlinx.coroutines.*
17
+ import kotlinx.coroutines.flow.Flow
18
+ import org.json.JSONObject
13
19
  import java.io.File
14
20
  import java.io.IOException
15
21
  import java.net.InetSocketAddress
16
22
  import java.nio.channels.ServerSocketChannel
17
23
  import java.security.SecureRandom
18
- import java.util.*
24
+ import java.util.UUID
19
25
  import java.util.concurrent.ConcurrentHashMap
20
26
 
21
27
  /**
@@ -36,6 +42,7 @@ object FrameworkServerUtils {
36
42
  private const val FRAMEWORK_PORT_RANGE_START = 3000
37
43
  private const val FRAMEWORK_PORT_RANGE_END = 3099
38
44
  private const val SESSION_TIMEOUT_MS = 30 * 60 * 1000L // 30 minutes
45
+ private const val AI_GENERATE_TIMEOUT_MS = 120 * 1000L // 2 minutes
39
46
 
40
47
  // Server state
41
48
  private var server: EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration>? = null
@@ -49,7 +56,14 @@ object FrameworkServerUtils {
49
56
  // File management
50
57
  private val servedFiles = ConcurrentHashMap<String, ServedFile>()
51
58
  private var cacheDirectory: File? = null
52
-
59
+
60
+ // Native AI — supplier set by NativeBridge before POST /ai/stream is called
61
+ // Returns (conversationId, tokenFlow) so the route can emit conversationId as an SSE event
62
+ private var nativeAiSupplier: (suspend (prompt: String, genConfig: JSONObject, conversationId: String?) -> Pair<String, Flow<String>>)? = null
63
+
64
+ // System prompt injected into every native AI request (built from attachmentComponents at initAI time)
65
+ private var nativeSystemPrompt: String = ""
66
+
53
67
  // Coroutine scope for server operations
54
68
  private val serverScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
55
69
 
@@ -60,7 +74,7 @@ object FrameworkServerUtils {
60
74
  val fileId: String,
61
75
  val createdAt: Long = System.currentTimeMillis()
62
76
  )
63
-
77
+
64
78
  /**
65
79
  * Initialize and start the framework server
66
80
  * @param context Android context for file operations
@@ -223,7 +237,21 @@ object FrameworkServerUtils {
223
237
  * Get current session ID
224
238
  */
225
239
  fun getSessionId(): String = sessionId
226
-
240
+
241
+ /**
242
+ * Register the native AI supplier. Called by NativeBridge once LiteRT-LM is ready.
243
+ * The supplier receives the prompt, gen-config, and optional incomingConversationId,
244
+ * and returns (activeConversationId, tokenFlow) so stateful sessions can be chained.
245
+ */
246
+ fun setNativeAiSupplier(supplier: suspend (prompt: String, genConfig: JSONObject, conversationId: String?) -> Pair<String, Flow<String>>) {
247
+ nativeAiSupplier = supplier
248
+ }
249
+
250
+ fun setNativeSystemPrompt(systemPrompt: String) {
251
+ nativeSystemPrompt = systemPrompt
252
+ Log.d(TAG, "System prompt set (${systemPrompt.length} chars)")
253
+ }
254
+
227
255
  // Private implementation methods
228
256
 
229
257
  private fun initializeCacheDirectory(context: Context) {
@@ -291,7 +319,145 @@ object FrameworkServerUtils {
291
319
 
292
320
  private fun startKtorServer() {
293
321
  server = embeddedServer(Netty, port = serverPort) {
322
+ install(SSE)
294
323
  routing {
324
+ // Native AI streaming endpoint — POST body: { "prompt": "...", "genConfig": {...} }
325
+ // Writes SSE manually over chunked response (Ktor SSE plugin only supports GET).
326
+ post("/framework-$sessionId/ai/stream") {
327
+ call.response.header(HttpHeaders.AccessControlAllowOrigin, allowedOrigin)
328
+ call.response.header(HttpHeaders.AccessControlAllowHeaders, "*")
329
+ call.response.header(HttpHeaders.ContentType, "text/event-stream")
330
+ call.response.header(HttpHeaders.CacheControl, "no-cache")
331
+ call.response.header("X-Accel-Buffering", "no")
332
+
333
+ val body = try {
334
+ JSONObject(call.receiveText())
335
+ } catch (e: Exception) {
336
+ JSONObject()
337
+ }
338
+ val userPrompt = body.optString("prompt", "")
339
+ val genConfig = body.optJSONObject("genConfig") ?: JSONObject()
340
+ val incomingConvId = body.optString("conversationId", "").takeIf { it.isNotBlank() }
341
+ if (nativeSystemPrompt.isNotBlank()) Log.d(TAG, "Injecting system prompt (${nativeSystemPrompt.length} chars)")
342
+ val prompt = if (nativeSystemPrompt.isNotBlank()) "$nativeSystemPrompt\n\n$userPrompt" else userPrompt
343
+
344
+ call.respondTextWriter(contentType = ContentType.Text.EventStream) {
345
+ val supplier = nativeAiSupplier
346
+ if (supplier == null) {
347
+ write("data: {\"error\":\"Native AI not initialised — call initAI() first\"}\n\n")
348
+ flush()
349
+ } else if (prompt.isBlank()) {
350
+ write("data: {\"error\":\"prompt is empty\"}\n\n")
351
+ flush()
352
+ } else {
353
+ try {
354
+ val (activeConvId, tokenFlow) = supplier(prompt, genConfig, incomingConvId)
355
+ write("data: {\"conversationId\":${JSONObject.quote(activeConvId)}}\n\n")
356
+ flush()
357
+ tokenFlow.collect { token ->
358
+ write("data: {\"token\":${JSONObject.quote(token)}}\n\n")
359
+ flush()
360
+ }
361
+ write("data: {\"done\":true}\n\n")
362
+ flush()
363
+ } catch (e: java.io.IOException) {
364
+ // Client disconnected — channel closed, nothing to write
365
+ Log.d(TAG, "Native AI stream: client disconnected (${e.message})")
366
+ } catch (e: Exception) {
367
+ Log.e(TAG, "Native AI stream error", e)
368
+ try {
369
+ write("data: {\"error\":${JSONObject.quote(e.message ?: "stream error")}}\n\n")
370
+ flush()
371
+ } catch (_: java.io.IOException) {
372
+ // Channel already closed, ignore
373
+ }
374
+ }
375
+ }
376
+ }
377
+ }
378
+
379
+ // OPTIONS preflight for ai/stream
380
+ options("/framework-$sessionId/ai/stream") {
381
+ call.response.header(HttpHeaders.AccessControlAllowOrigin, allowedOrigin)
382
+ call.response.header(HttpHeaders.AccessControlAllowMethods, "POST, OPTIONS")
383
+ call.response.header(HttpHeaders.AccessControlAllowHeaders, "*")
384
+ call.response.header(HttpHeaders.AccessControlMaxAge, "86400")
385
+ call.respond(HttpStatusCode.OK)
386
+ }
387
+
388
+ // Native AI non-streaming endpoint — POST body: { "prompt": "...", "genConfig": {...} }
389
+ // Drains nativeAiSupplier's tokenFlow fully, then returns a single JSON response.
390
+ post("/framework-$sessionId/ai/generate") {
391
+ call.response.header(HttpHeaders.AccessControlAllowOrigin, allowedOrigin)
392
+ call.response.header(HttpHeaders.AccessControlAllowHeaders, "*")
393
+
394
+ val body = try {
395
+ JSONObject(call.receiveText())
396
+ } catch (e: Exception) {
397
+ JSONObject()
398
+ }
399
+ val userPrompt = body.optString("prompt", "")
400
+ val genConfig = body.optJSONObject("genConfig") ?: JSONObject()
401
+ val incomingConvId = body.optString("conversationId", "").takeIf { it.isNotBlank() }
402
+ if (nativeSystemPrompt.isNotBlank()) Log.d(TAG, "Injecting system prompt (${nativeSystemPrompt.length} chars)")
403
+ val prompt = if (nativeSystemPrompt.isNotBlank()) "$nativeSystemPrompt\n\n$userPrompt" else userPrompt
404
+
405
+ val supplier = nativeAiSupplier
406
+ if (supplier == null) {
407
+ call.respondText(
408
+ JSONObject().put("error", "Native AI not initialised — call initAI() first").toString(),
409
+ ContentType.Application.Json,
410
+ HttpStatusCode.ServiceUnavailable
411
+ )
412
+ return@post
413
+ }
414
+ if (prompt.isBlank()) {
415
+ call.respondText(
416
+ JSONObject().put("error", "prompt is empty").toString(),
417
+ ContentType.Application.Json,
418
+ HttpStatusCode.BadRequest
419
+ )
420
+ return@post
421
+ }
422
+
423
+ try {
424
+ val (activeConvId, tokenFlow) = supplier(prompt, genConfig, incomingConvId)
425
+ val output = StringBuilder()
426
+ var tokenCount = 0
427
+ withTimeout(AI_GENERATE_TIMEOUT_MS) {
428
+ tokenFlow.collect { token -> output.append(token); tokenCount++ }
429
+ }
430
+ val responseJson = JSONObject()
431
+ .put("output", output.toString())
432
+ .put("conversationId", activeConvId)
433
+ .put("tokenCount", tokenCount)
434
+ call.respondText(responseJson.toString(), ContentType.Application.Json, HttpStatusCode.OK)
435
+ } catch (e: TimeoutCancellationException) {
436
+ Log.e(TAG, "Native AI generate timed out after ${AI_GENERATE_TIMEOUT_MS}ms", e)
437
+ call.respondText(
438
+ JSONObject().put("error", "generate timed out after ${AI_GENERATE_TIMEOUT_MS / 1000}s").toString(),
439
+ ContentType.Application.Json,
440
+ HttpStatusCode.GatewayTimeout
441
+ )
442
+ } catch (e: Exception) {
443
+ Log.e(TAG, "Native AI generate error", e)
444
+ call.respondText(
445
+ JSONObject().put("error", (e.message ?: "generate error")).toString(),
446
+ ContentType.Application.Json,
447
+ HttpStatusCode.InternalServerError
448
+ )
449
+ }
450
+ }
451
+
452
+ // OPTIONS preflight for ai/generate
453
+ options("/framework-$sessionId/ai/generate") {
454
+ call.response.header(HttpHeaders.AccessControlAllowOrigin, allowedOrigin)
455
+ call.response.header(HttpHeaders.AccessControlAllowMethods, "POST, OPTIONS")
456
+ call.response.header(HttpHeaders.AccessControlAllowHeaders, "*")
457
+ call.response.header(HttpHeaders.AccessControlMaxAge, "86400")
458
+ call.respond(HttpStatusCode.OK)
459
+ }
460
+
295
461
  get("/framework-$sessionId/file-{fileId}") {
296
462
  val fileId = call.parameters["fileId"]
297
463
 
@@ -385,22 +551,16 @@ object FrameworkServerUtils {
385
551
  val expiredFiles = servedFiles.filter { (_, servedFile) ->
386
552
  currentTime - servedFile.createdAt > SESSION_TIMEOUT_MS
387
553
  }
388
-
389
554
  if (expiredFiles.isNotEmpty()) {
390
555
  Log.d(TAG, "Cleaning up ${expiredFiles.size} expired files")
391
-
392
- expiredFiles.forEach { (fileId, _) ->
393
- removeServedFile(fileId)
394
- }
556
+ expiredFiles.forEach { (fileId, _) -> removeServedFile(fileId) }
395
557
  }
558
+
396
559
  }
397
560
 
398
561
  private fun cleanupAllFiles() {
399
562
  Log.d(TAG, "Cleaning up all served files (${servedFiles.size} files)")
400
-
401
- servedFiles.keys.toList().forEach { fileId ->
402
- removeServedFile(fileId)
403
- }
563
+ servedFiles.keys.toList().forEach { fileId -> removeServedFile(fileId) }
404
564
 
405
565
  // Clean cache directory
406
566
  cacheDirectory?.listFiles()?.forEach { file ->
@@ -33,3 +33,13 @@ dependencyResolutionManagement {
33
33
 
34
34
  rootProject.name = "Catalyst Application"
35
35
  include(":app")
36
+
37
+ // @catalyst/cloud-ai — include when the package is installed.
38
+ // The npm package ships an /android library module for native AI; wire it here so Gradle picks it up.
39
+ // After running: npm install @catalyst/cloud-ai && npm run sync-packages -- --packages cloud-ai
40
+ // Also set ai.enabled=true in your app config to enable useLegacyPackaging for GPU dlopen().
41
+ val cloudAiDir = File(rootDir, "../node_modules/@catalyst/cloud-ai/android")
42
+ if (cloudAiDir.exists()) {
43
+ include(":catalyst-cloud-ai")
44
+ project(":catalyst-cloud-ai").projectDir = cloudAiDir
45
+ }
@@ -209,13 +209,15 @@ async function checkEmulator(ADB_PATH) {
209
209
  }
210
210
  async function startEmulator(EMULATOR_PATH, androidConfig) {
211
211
  progress.log(`Starting emulator: ${androidConfig.emulatorName}...`);
212
- // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - EMULATOR_PATH is derived from SDK configuration and emulatorName is local native setup config.
213
- // eslint-disable-next-line security/detect-child-process
214
- (0, _child_process.exec)(`${EMULATOR_PATH} -avd ${androidConfig.emulatorName} -read-only > /dev/null &`, error => {
215
- if (error) {
216
- progress.log(`Error starting emulator: ${error}`, "error");
217
- }
212
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - spawn with an argv array (no shell), so androidConfig.emulatorName can't be interpreted as shell syntax even though it comes from a locally-editable config file.
213
+ const child = (0, _child_process.spawn)(EMULATOR_PATH, ["-avd", androidConfig.emulatorName, "-read-only"], {
214
+ detached: true,
215
+ stdio: "ignore"
216
+ });
217
+ child.on("error", error => {
218
+ progress.log(`Error starting emulator: ${error}`, "error");
218
219
  });
220
+ child.unref();
219
221
  }
220
222
  async function updateLocalProperties(sdkPath) {
221
223
  progress.start("properties");
@@ -48,7 +48,10 @@ const NATIVE_COMMANDS = exports.NATIVE_COMMANDS = {
48
48
  // Security commands
49
49
  SET_SCREEN_SECURE: "setScreenSecure",
50
50
  GET_SCREEN_SECURE: "getScreenSecure",
51
- CLEAR_WEB_DATA: "clearWebData"
51
+ CLEAR_WEB_DATA: "clearWebData",
52
+ // Native AI commands
53
+ INIT_AI: "initAI",
54
+ GENERATE_NATIVE: "generateNative"
52
55
  };
53
56
 
54
57
  // Callback Interfaces: Native → Web (incoming events/callbacks)
@@ -105,6 +108,13 @@ const NATIVE_CALLBACKS = exports.NATIVE_CALLBACKS = {
105
108
  ON_SCREEN_SECURE_ERROR: "ON_SCREEN_SECURE_ERROR",
106
109
  ON_WEB_DATA_CLEARED: "ON_WEB_DATA_CLEARED",
107
110
  ON_WEB_DATA_CLEAR_ERROR: "ON_WEB_DATA_CLEAR_ERROR",
111
+ // Native AI callbacks
112
+ ON_AI_READY: "ON_AI_READY",
113
+ ON_AI_TOKEN: "ON_AI_TOKEN",
114
+ ON_AI_DONE: "ON_AI_DONE",
115
+ ON_AI_ERROR: "ON_AI_ERROR",
116
+ ON_AI_PROGRESS: "ON_AI_PROGRESS",
117
+ ON_AI_LOG: "ON_AI_LOG",
108
118
  // Perf telemetry callbacks (native → web)
109
119
  CATALYST_PERF_EVENT: "CATALYST_PERF_EVENT"
110
120
  };
@@ -150,6 +160,10 @@ const INTERFACE_CATEGORIES = exports.INTERFACE_CATEGORIES = {
150
160
  TRANSITION: {
151
161
  commands: [NATIVE_COMMANDS.START_TRANSITION, NATIVE_COMMANDS.COMMIT_TRANSITION, NATIVE_COMMANDS.CANCEL_TRANSITION],
152
162
  callbacks: [NATIVE_CALLBACKS.ON_TRANSITION_COMMITTED, NATIVE_CALLBACKS.ON_TRANSITION_CANCELLED, NATIVE_CALLBACKS.ON_TRANSITION_TIMEOUT]
163
+ },
164
+ AI: {
165
+ commands: [NATIVE_COMMANDS.INIT_AI, NATIVE_COMMANDS.GENERATE_NATIVE],
166
+ callbacks: [NATIVE_CALLBACKS.ON_AI_READY, NATIVE_CALLBACKS.ON_AI_TOKEN, NATIVE_CALLBACKS.ON_AI_DONE, NATIVE_CALLBACKS.ON_AI_ERROR, NATIVE_CALLBACKS.ON_AI_PROGRESS, NATIVE_CALLBACKS.ON_AI_LOG]
153
167
  }
154
168
  };
155
169
 
@@ -27,6 +27,12 @@ Object.defineProperty(exports, "sanitizeFilePickerOptions", {
27
27
  return _useFilePicker.sanitizeFilePickerOptions;
28
28
  }
29
29
  });
30
+ Object.defineProperty(exports, "useAI", {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _useAI.useAI;
34
+ }
35
+ });
30
36
  Object.defineProperty(exports, "useCamera", {
31
37
  enumerable: true,
32
38
  get: function () {
@@ -111,6 +117,7 @@ Object.defineProperty(exports, "useVideoStream", {
111
117
  return _useVideoStream.useVideoStream;
112
118
  }
113
119
  });
120
+ var _useAI = require("./useAI.js");
114
121
  var _useGoogleSignIn = require("./useGoogleSignIn.js");
115
122
  var _useCamera = require("./useCamera.js");
116
123
  var _useIntent = require("./useIntent.js");
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useAI = useAI;
7
+ /* global __CATALYST_PACKAGES__ */
8
+
9
+ function getBrowserConfig() {
10
+ try {
11
+ const raw = process.env.AI_PUBLIC_CONFIG;
12
+ if (!raw) return null;
13
+ const parsed = JSON.parse(raw);
14
+ return parsed?.browser || null;
15
+ } catch (_) {
16
+ return null;
17
+ }
18
+ }
19
+ function resolveMode(provider) {
20
+ if (provider === "transformers") return "local";
21
+ if (provider === "native") return "native";
22
+ return "cloud";
23
+ }
24
+ function isNativeAIAvailable() {
25
+ const nb = typeof window !== "undefined" && window.NativeBridge;
26
+ return nb && typeof nb.isAIAvailable === "function" && nb.isAIAvailable();
27
+ }
28
+
29
+ // __CATALYST_PACKAGES__ is substituted by webpack's DefinePlugin / Vite's `define` in
30
+ // client bundles only. catalyst-core is externalized (nodeExternals) from the SSR
31
+ // bundle, so on the server this file is loaded directly by Node — including via Vite's
32
+ // ssrLoadModule, which executes real ESM with no bundler transform and no `require`
33
+ // shim. useAI() always discards the SSR result anyway (returns emptyHook() when
34
+ // `window` is undefined), so only ever attempt the client-side `require` — a bare
35
+ // `require()` reached during SSR would throw ReferenceError rather than a catchable
36
+ // MODULE_NOT_FOUND, since this package is "type": "module".
37
+ const _pkg = typeof window !== "undefined" && (typeof __CATALYST_PACKAGES__ === "undefined" || __CATALYST_PACKAGES__.ai) ? (() => {
38
+ try {
39
+ return require(/* webpackIgnore: true */"catalyst-ai");
40
+ } catch (_) {
41
+ return null;
42
+ }
43
+ })() : null;
44
+
45
+ // provider: "transformers" → useWebAI (catalyst-ai)
46
+ // "native" → useNativeAI (catalyst-ai, falls back to useCloudAI if bridge unavailable)
47
+ // anything else → useCloudAI (catalyst-ai, default)
48
+ function useAI(options = {}) {
49
+ const {
50
+ provider
51
+ } = options;
52
+ const config = getBrowserConfig();
53
+ const resolvedProvider = provider || config?.provider;
54
+ const mode = resolveMode(resolvedProvider);
55
+ const cloudResult = _pkg ? _pkg.useCloudAI(options) : emptyHook();
56
+ const webResult = _pkg ? _pkg.useWebAI(options) : emptyHook();
57
+ const nativeResult = _pkg ? _pkg.useNativeAI({
58
+ ...options,
59
+ enabled: mode === "native"
60
+ }) : emptyHook();
61
+ if (typeof window === "undefined") return emptyHook();
62
+ if (!_pkg) {
63
+ console.error("\n[catalyst-core] useAI requires catalyst-ai.\n" + "Run: npm install catalyst-ai\n");
64
+ return emptyHook();
65
+ }
66
+ if (mode === "local") return webResult;
67
+ if (mode === "native" && isNativeAIAvailable()) return nativeResult;
68
+ return cloudResult;
69
+ }
70
+
71
+ // Returned when catalyst-ai is missing — keeps hook shape stable so callers don't crash
72
+ function emptyHook() {
73
+ return {
74
+ output: "",
75
+ streaming: false,
76
+ loading: false,
77
+ error: null,
78
+ modelReady: false,
79
+ downloadProgress: null,
80
+ nativeDownloadProgress: null,
81
+ nativeLogs: [],
82
+ metrics: null,
83
+ isLocal: false,
84
+ isNative: false,
85
+ isWeb: false,
86
+ generate: () => {},
87
+ cancel: () => {},
88
+ reset: () => {},
89
+ clearError: () => {},
90
+ conversationId: null,
91
+ getSessionMetrics: () => null,
92
+ resetSessionMetrics: () => {}
93
+ };
94
+ }