capacitor-native-agent 0.9.3 → 0.9.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.
@@ -0,0 +1,70 @@
1
+ package com.t6x.plugins.nativeagent
2
+
3
+ import java.util.concurrent.CopyOnWriteArrayList
4
+ import uniffi.native_agent_ffi.NativeAgentHandle
5
+
6
+ /**
7
+ * Listener interface for in-process Kotlin code that wants to observe
8
+ * raw FFI events alongside the WebView listener. Mirrors the shape of
9
+ * uniffi.native_agent_ffi.NativeEventCallback but is registered through
10
+ * NativeAgentBridge instead of the FFI itself, so multiple consumers
11
+ * can subscribe at once.
12
+ */
13
+ interface NativeEventListener {
14
+ fun onEvent(eventType: String, payloadJson: String)
15
+ }
16
+
17
+ /**
18
+ * Process-wide accessor that exposes the live NativeAgentHandle and a
19
+ * multi-listener event fan-out to other modules in the same Android
20
+ * process — most notably the STOMP transport service in theshell.
21
+ *
22
+ * The single FFI-side NativeEventCallback registered by NativeAgentPlugin
23
+ * forwards every event into [dispatch] in addition to its existing
24
+ * notifyListeners("nativeAgentEvent", ...) call, so the WebView pipeline
25
+ * is byte-equivalent to before this bridge existed.
26
+ *
27
+ * Listeners are strong-referenced; callers MUST call
28
+ * [removeNativeEventListener] when their lifecycle ends.
29
+ */
30
+ object NativeAgentBridge {
31
+
32
+ @Volatile
33
+ private var handleRef: NativeAgentHandle? = null
34
+
35
+ private val listeners = CopyOnWriteArrayList<NativeEventListener>()
36
+
37
+ /**
38
+ * The live FFI handle, or null if NativeAgent has not been
39
+ * initialized yet (or has been destroyed).
40
+ */
41
+ @JvmStatic
42
+ fun handle(): NativeAgentHandle? = handleRef
43
+
44
+ @JvmStatic
45
+ fun addNativeEventListener(listener: NativeEventListener) {
46
+ listeners.addIfAbsent(listener)
47
+ }
48
+
49
+ @JvmStatic
50
+ fun removeNativeEventListener(listener: NativeEventListener) {
51
+ listeners.remove(listener)
52
+ }
53
+
54
+ internal fun setHandle(h: NativeAgentHandle?) {
55
+ handleRef = h
56
+ }
57
+
58
+ internal fun dispatch(eventType: String, payloadJson: String) {
59
+ for (l in listeners) {
60
+ try {
61
+ l.onEvent(eventType, payloadJson)
62
+ } catch (t: Throwable) {
63
+ android.util.Log.w(
64
+ "NativeAgentBridge",
65
+ "listener threw on $eventType: ${t.message}",
66
+ )
67
+ }
68
+ }
69
+ }
70
+ }
@@ -98,6 +98,7 @@ class NativeAgentPlugin : Plugin() {
98
98
  data.put("eventType", eventType)
99
99
  data.put("payloadJson", payloadJson)
100
100
  notifyListeners("nativeAgentEvent", data)
101
+ NativeAgentBridge.dispatch(eventType, payloadJson)
101
102
  }
102
103
  })
103
104
  h.setNotifier(NativeNotifierImpl(context.applicationContext))
@@ -109,6 +110,7 @@ class NativeAgentPlugin : Plugin() {
109
110
  }
110
111
  h.persistConfig()
111
112
  handle = h
113
+ NativeAgentBridge.setHandle(h)
112
114
  context
113
115
  .getSharedPreferences(STORAGE_FILE, android.content.Context.MODE_PRIVATE)
114
116
  .edit()
@@ -530,6 +532,7 @@ class NativeAgentPlugin : Plugin() {
530
532
 
531
533
  override fun handleOnDestroy() {
532
534
  scope.cancel()
535
+ NativeAgentBridge.setHandle(null)
533
536
  handle = null
534
537
  }
535
538
 
@@ -0,0 +1,64 @@
1
+ import Foundation
2
+
3
+ /// Listener protocol for in-process Swift code that wants to observe
4
+ /// raw FFI events alongside the WebView listener. Mirrors the shape of
5
+ /// the UniFFI-generated NativeEventCallback protocol but is registered
6
+ /// through NativeAgentBridge instead of the FFI itself, so multiple
7
+ /// consumers can subscribe at once.
8
+ public protocol NativeEventListener: AnyObject {
9
+ func onEvent(eventType: String, payloadJson: String)
10
+ }
11
+
12
+ /// Process-wide accessor that exposes the live NativeAgentHandle and a
13
+ /// multi-listener event fan-out to other modules in the same iOS
14
+ /// process — most notably the STOMP transport in theshell.
15
+ ///
16
+ /// The single FFI-side NativeEventCallback registered by NativeAgentPlugin
17
+ /// (NativeAgentEventBridge in NativeAgentPlugin.swift) forwards every
18
+ /// event into `dispatch` in addition to its existing notifyListeners
19
+ /// call, so the WebView pipeline is byte-equivalent to before this
20
+ /// bridge existed.
21
+ ///
22
+ /// Listeners are strong-referenced by ObjectIdentifier; callers MUST
23
+ /// call `removeNativeEventListener` when their lifecycle ends.
24
+ public enum NativeAgentBridge {
25
+
26
+ private static let lock = NSLock()
27
+ private static var handleRef: NativeAgentHandle?
28
+ private static var listeners: [ObjectIdentifier: NativeEventListener] = [:]
29
+
30
+ /// The live FFI handle, or nil if NativeAgent has not been
31
+ /// initialized yet (or has been destroyed).
32
+ public static func handle() -> NativeAgentHandle? {
33
+ lock.lock()
34
+ defer { lock.unlock() }
35
+ return handleRef
36
+ }
37
+
38
+ public static func addNativeEventListener(_ listener: NativeEventListener) {
39
+ lock.lock()
40
+ defer { lock.unlock() }
41
+ listeners[ObjectIdentifier(listener)] = listener
42
+ }
43
+
44
+ public static func removeNativeEventListener(_ listener: NativeEventListener) {
45
+ lock.lock()
46
+ defer { lock.unlock() }
47
+ listeners.removeValue(forKey: ObjectIdentifier(listener))
48
+ }
49
+
50
+ internal static func setHandle(_ h: NativeAgentHandle?) {
51
+ lock.lock()
52
+ defer { lock.unlock() }
53
+ handleRef = h
54
+ }
55
+
56
+ internal static func dispatch(eventType: String, payloadJson: String) {
57
+ lock.lock()
58
+ let snapshot = Array(listeners.values)
59
+ lock.unlock()
60
+ for l in snapshot {
61
+ l.onEvent(eventType: eventType, payloadJson: payloadJson)
62
+ }
63
+ }
64
+ }
@@ -66,6 +66,10 @@ public class NativeAgentPlugin: CAPPlugin, CAPBridgedPlugin {
66
66
 
67
67
  private var handle: NativeAgentHandle?
68
68
 
69
+ deinit {
70
+ NativeAgentBridge.setHandle(nil)
71
+ }
72
+
69
73
  // ── Helper ──────────────────────────────────────────────────────────────
70
74
 
71
75
  /// Resolves a `files://` prefixed path to an absolute iOS path
@@ -173,6 +177,7 @@ public class NativeAgentPlugin: CAPPlugin, CAPBridgedPlugin {
173
177
  }
174
178
  try h.persistConfig()
175
179
  self.handle = h
180
+ NativeAgentBridge.setHandle(h)
176
181
  UserDefaults.standard.set(
177
182
  self.resolveConfigPath(workspacePath: resolvedWorkspacePath),
178
183
  forKey: Self.configPathKey
@@ -837,5 +842,6 @@ class NativeAgentEventBridge: NativeEventCallback {
837
842
  "eventType": eventType,
838
843
  "payloadJson": payloadJson,
839
844
  ])
845
+ NativeAgentBridge.dispatch(eventType: eventType, payloadJson: payloadJson)
840
846
  }
841
847
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-native-agent",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "Native AI agent loop for Capacitor — runs LLM completions, tool execution, and cron jobs in native Rust, enabling background execution.",
5
5
  "main": "dist/esm/index.js",
6
6
  "types": "dist/esm/index.d.ts",