yaver-feedback-react-native 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # @yaver/feedback-react-native
1
+ # yaver-feedback-react-native
2
2
 
3
3
  Visual feedback SDK for Yaver. Lets testers and developers shake their phone (or tap a floating button) to capture screenshots, record voice notes, and send bug reports directly to a Yaver agent running on a dev machine. Built for vibe coding workflows where feedback needs to flow fast.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @yaver/feedback-react-native
8
+ npm install yaver-feedback-react-native
9
9
  ```
10
10
 
11
11
  ### Peer dependencies
@@ -26,7 +26,7 @@ npm install react-native-audio-recorder-player
26
26
  ## Quick Start
27
27
 
28
28
  ```tsx
29
- import { YaverFeedback, BlackBox, FeedbackModal } from '@yaver/feedback-react-native';
29
+ import { YaverFeedback, BlackBox, FeedbackModal } from 'yaver-feedback-react-native';
30
30
 
31
31
  // Initialize once at app startup
32
32
  YaverFeedback.init({
@@ -164,7 +164,7 @@ yaver sdk-token create --expires 24h
164
164
  Continuous streaming of app events to the agent. The agent keeps a ring buffer (last 1000 events per device) and injects context into fix prompts — so the AI agent already knows what the app was doing when you ask for a fix.
165
165
 
166
166
  ```tsx
167
- import { BlackBox } from '@yaver/feedback-react-native';
167
+ import { BlackBox } from 'yaver-feedback-react-native';
168
168
 
169
169
  // Start streaming (call after YaverFeedback.init)
170
170
  BlackBox.start({
@@ -219,6 +219,102 @@ BlackBox.isStreaming; // check if active
219
219
  | `state` | State mutations | `BlackBox.stateChange('theme toggled')` |
220
220
  | `render` | Component render with duration | `BlackBox.render('FlatList', 32.1)` |
221
221
 
222
+ ## Remote Reload (Agent Command Channel)
223
+
224
+ The SDK maintains a persistent SSE connection to the agent that receives commands. The primary use case: a vibe coder is away from their desk, coding on their phone via the Yaver mobile app, and wants to hot-reload the third-party app (with this SDK) running on the same or a different device.
225
+
226
+ ```
227
+ Yaver Mobile App Agent Third-Party App (SDK)
228
+ tap "Reload"
229
+ ───POST /dev/reload-app──► process reload
230
+ ├─ dev server reload
231
+ └─ BroadcastCommand("reload")
232
+ ───SSE push──────────────► BlackBox receives command
233
+ ├─ onReload() callback
234
+ └─ DevSettings.reload()
235
+ ```
236
+
237
+ Works over both direct LAN and relay connections since it's standard HTTP/SSE.
238
+
239
+ ### Setup
240
+
241
+ ```tsx
242
+ import { YaverFeedback, BlackBox } from 'yaver-feedback-react-native';
243
+
244
+ YaverFeedback.init({
245
+ authToken: 'your-sdk-token',
246
+ agentUrl: 'http://192.168.1.10:18080',
247
+
248
+ // Called when the vibe coder triggers reload from Yaver mobile app
249
+ onReload: () => {
250
+ console.log('Remote reload triggered!');
251
+ // Default behavior if omitted: DevSettings.reload() in dev mode
252
+ },
253
+
254
+ // Called when agent pushes a new native bundle
255
+ onReloadBundle: (bundleUrl, assetsUrl) => {
256
+ console.log('New bundle available at:', bundleUrl);
257
+ // Default: no-op. Implement custom bundle loading if needed.
258
+ },
259
+ });
260
+
261
+ // Start BlackBox — this also connects the command channel
262
+ BlackBox.start({ appName: 'AcmeStore' });
263
+ ```
264
+
265
+ ### Custom Command Handlers
266
+
267
+ For advanced use cases, register handlers directly on BlackBox:
268
+
269
+ ```tsx
270
+ // Listen for any command from the agent
271
+ const unsubscribe = BlackBox.onCommand((cmd) => {
272
+ switch (cmd.command) {
273
+ case 'reload':
274
+ // Hot reload from dev server
275
+ DevSettings.reload();
276
+ break;
277
+ case 'reload_bundle':
278
+ // Fetch new native bundle
279
+ const { bundleUrl, assetsUrl } = cmd.data;
280
+ loadNewBundle(bundleUrl, assetsUrl);
281
+ break;
282
+ default:
283
+ console.log('Unknown command:', cmd.command);
284
+ }
285
+ });
286
+
287
+ // Check connection status
288
+ BlackBox.isCommandChannelConnected; // boolean
289
+
290
+ // Clean up
291
+ unsubscribe();
292
+ ```
293
+
294
+ ### Triggering Reload from P2PClient
295
+
296
+ The SDK can also trigger reload programmatically (e.g., from a "Reload" button in the Feedback modal):
297
+
298
+ ```tsx
299
+ import { P2PClient } from 'yaver-feedback-react-native';
300
+
301
+ const client = new P2PClient('http://192.168.1.10:18080', 'your-token');
302
+
303
+ // Hot reload (dev server restart)
304
+ await client.reloadApp('dev');
305
+
306
+ // Rebuild native bundle + push to all connected SDK devices
307
+ await client.reloadApp('bundle');
308
+ ```
309
+
310
+ ### How It Works
311
+
312
+ 1. `BlackBox.start()` connects to `/blackbox/command-stream` via SSE
313
+ 2. The connection auto-reconnects on disconnect (5s backoff)
314
+ 3. When the agent receives `POST /dev/reload` or `POST /dev/reload-app`, it broadcasts a command to all connected SDK sessions
315
+ 4. The SDK invokes `onReload` / `onReloadBundle` callbacks, or falls back to `DevSettings.reload()` in dev mode
316
+ 5. Events are still sent via batch POST to `/blackbox/events` (unchanged)
317
+
222
318
  ### Resilience
223
319
 
224
320
  - Failed flushes re-add events to the buffer (capped at 2x maxBufferSize)
@@ -258,7 +354,7 @@ YaverFeedback.init({
258
354
  ### Manual discovery
259
355
 
260
356
  ```typescript
261
- import { YaverDiscovery } from '@yaver/feedback-react-native';
357
+ import { YaverDiscovery } from 'yaver-feedback-react-native';
262
358
 
263
359
  // Full discovery (Convex → stored → LAN scan)
264
360
  const result = await YaverDiscovery.discover({
@@ -281,7 +377,7 @@ await YaverDiscovery.clear();
281
377
  A full-screen UI for discovering and connecting to a Yaver agent. Shows connection status, URL/token inputs, auto-discover button, and a Start/Stop testing toggle with recording timer.
282
378
 
283
379
  ```tsx
284
- import { YaverConnectionScreen } from '@yaver/feedback-react-native';
380
+ import { YaverConnectionScreen } from 'yaver-feedback-react-native';
285
381
 
286
382
  function App() {
287
383
  return (
@@ -445,6 +541,10 @@ YaverFeedback.init({
445
541
  feedbackMode: 'batch', // 'live' | 'narrated' | 'batch' (default: 'batch')
446
542
  agentCommentaryLevel: 0, // 0-10 (default: 0, only relevant in live mode)
447
543
  maxCapturedErrors: 5, // Error ring buffer size (default: 5)
544
+
545
+ // Remote reload callbacks (from Yaver mobile app or another SDK device)
546
+ onReload: () => { ... }, // Called on hot reload command (default: DevSettings.reload())
547
+ onReloadBundle: (url, assets) => { ... },// Called on native bundle rebuild command
448
548
  });
449
549
  ```
450
550
 
@@ -463,7 +563,7 @@ YaverFeedback.init({ authToken, trigger: 'shake' });
463
563
  A small draggable "Y" button overlays the app. Tap to open the feedback modal.
464
564
 
465
565
  ```tsx
466
- import { FloatingButton, FeedbackModal, YaverFeedback } from '@yaver/feedback-react-native';
566
+ import { FloatingButton, FeedbackModal, YaverFeedback } from 'yaver-feedback-react-native';
467
567
 
468
568
  function App() {
469
569
  return (
@@ -481,7 +581,7 @@ function App() {
481
581
  Trigger feedback collection programmatically from anywhere in your app.
482
582
 
483
583
  ```typescript
484
- import { YaverFeedback } from '@yaver/feedback-react-native';
584
+ import { YaverFeedback } from 'yaver-feedback-react-native';
485
585
 
486
586
  // In a button handler, debug menu, etc.
487
587
  YaverFeedback.startReport();
@@ -492,7 +592,7 @@ YaverFeedback.startReport();
492
592
  For direct communication with the Yaver agent beyond feedback:
493
593
 
494
594
  ```typescript
495
- import { P2PClient } from '@yaver/feedback-react-native';
595
+ import { P2PClient } from 'yaver-feedback-react-native';
496
596
 
497
597
  const client = new P2PClient('http://192.168.1.10:18080', 'your-token');
498
598
 
@@ -608,6 +708,8 @@ YaverFeedback.setEnabled(false);
608
708
  | `wrapConsole()` | Intercept console.log/warn/error |
609
709
  | `unwrapConsole()` | Restore original console methods |
610
710
  | `wrapErrorHandler(next?)` | Pass-through error handler with real-time streaming |
711
+ | `onCommand(handler)` | Register handler for agent commands (reload, etc.). Returns unsubscribe fn |
712
+ | `isCommandChannelConnected` | Whether the SSE command channel is connected (getter) |
611
713
 
612
714
  ### P2PClient
613
715
 
@@ -622,6 +724,7 @@ YaverFeedback.setEnabled(false);
622
724
  | `getArtifactUrl(buildId)` | Get download URL for a build artifact |
623
725
  | `voiceStatus()` | Get voice capability info |
624
726
  | `transcribeVoice(audioUri)` | Send audio for transcription |
727
+ | `reloadApp(mode)` | Trigger remote reload: `'dev'` (hot reload) or `'bundle'` (rebuild + push) |
625
728
  | `startTestSession()` | Start autonomous test session |
626
729
  | `stopTestSession()` | Stop test session |
627
730
  | `getTestSession()` | Get test session status + fixes |
@@ -655,8 +758,10 @@ The SDK communicates with these agent HTTP endpoints:
655
758
  | `/feedback` | POST | Upload feedback bundle (multipart) |
656
759
  | `/feedback/stream` | POST | Stream live feedback events |
657
760
  | `/blackbox/events` | POST | Batch stream Black Box events |
761
+ | `/blackbox/command-stream` | GET | SSE command channel (agent pushes reload, etc.) |
658
762
  | `/blackbox/subscribe` | GET | SSE live log stream |
659
763
  | `/blackbox/context` | GET | Get generated prompt context |
764
+ | `/dev/reload-app` | POST | Trigger remote reload of SDK-connected apps |
660
765
  | `/builds` | GET/POST | List or start builds |
661
766
  | `/voice/status` | GET | Voice capability info |
662
767
  | `/voice/transcribe` | POST | Send audio for transcription |
@@ -667,23 +772,24 @@ The SDK communicates with these agent HTTP endpoints:
667
772
  ## Architecture
668
773
 
669
774
  ```
670
- ┌──────────────────┐ HTTP (Bearer auth) ┌──────────────────┐
671
- │ Your App │────────────────────────────►│ Yaver Agent │
672
- │ + Feedback SDK │ feedback, blackbox events │ (Go CLI) │
673
- │ + BlackBox │ screenshots, voice, video │ on your machine │
674
- │ │◄────────────────────────────│ │
675
- │ │ fixes, build status, voice │ runs AI agent │
676
- └──────────────────┘ └──────────────────┘
677
- │ │
678
- Auth only Auth only
679
- ▼ ▼
680
- ┌─────────────────────────────────────────────────────────────────────┐
681
- │ Convex Backend │
682
- Token validation + device registry (no task data stored)
683
- └─────────────────────────────────────────────────────────────────────┘
684
- ```
685
-
686
- All feedback data flows P2P between your app and the agent. Convex handles only auth and device discovery.
775
+ ┌──────────────────┐ HTTP (Bearer auth) ┌──────────────────┐ HTTP/SSE ┌──────────────────┐
776
+ │ Your App │────────────────────────────►│ Yaver Agent │◄────────────────── Yaver Mobile │
777
+ │ + Feedback SDK │ feedback, blackbox events │ (Go CLI) │ tasks, reload │ App (phone) │
778
+ │ + BlackBox │ screenshots, voice, video │ on your machine │ commands │ vibe coding │
779
+ │ │◄────────────────────────────│ │ │ │
780
+ │ │ reload commands (SSE), │ runs AI agent │ │ │
781
+ │ │ fixes, build status, voice │ │ │ │
782
+ └──────────────────┘ └──────────────────┘ └──────────────────┘
783
+
784
+ │ Auth only │ Auth only │
785
+ ▼ ▼ ▼
786
+ ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────┐
787
+ Convex Backend
788
+ │ Token validation + device registry (no task data stored) │
789
+ └──────────────────────────────────────────────────────────────────────────────────────────────────────────┘
790
+ ```
791
+
792
+ All feedback data flows P2P between your app and the agent. The Yaver mobile app can trigger remote reloads that reach your app via the agent's command channel. Convex handles only auth and device discovery.
687
793
 
688
794
  ## License
689
795
 
@@ -0,0 +1,188 @@
1
+ package io.yaver.feedback;
2
+
3
+ import android.app.Activity;
4
+ import android.content.Context;
5
+ import android.content.SharedPreferences;
6
+ import android.os.Handler;
7
+ import android.os.Looper;
8
+ import android.util.Log;
9
+
10
+ import androidx.annotation.NonNull;
11
+
12
+ import com.facebook.react.bridge.Promise;
13
+ import com.facebook.react.bridge.ReactApplicationContext;
14
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
15
+ import com.facebook.react.bridge.ReactMethod;
16
+ import com.facebook.react.bridge.ReadableMap;
17
+ import com.facebook.react.bridge.WritableMap;
18
+ import com.facebook.react.bridge.Arguments;
19
+ import com.facebook.react.ReactApplication;
20
+ import com.facebook.react.ReactInstanceManager;
21
+
22
+ import java.io.File;
23
+ import java.io.FileOutputStream;
24
+ import java.io.InputStream;
25
+ import java.net.HttpURLConnection;
26
+ import java.net.URL;
27
+ import java.util.concurrent.Executors;
28
+
29
+ /**
30
+ * Hot reload native module for the Yaver Feedback SDK (Android).
31
+ *
32
+ * Downloads a Hermes bytecode bundle from the agent, saves it to the app's
33
+ * files directory, and recreates the React Native context to load the new bundle.
34
+ *
35
+ * Supports N reloads — each reload recreates the ReactContext with the updated bundle.
36
+ */
37
+ public class YaverHotReloadModule extends ReactContextBaseJavaModule {
38
+
39
+ private static final String TAG = "YaverHotReload";
40
+ private static final String MODULE_NAME = "YaverHotReload";
41
+ private static final String BUNDLE_DIR = "yaver-hot-reload";
42
+ private static final String BUNDLE_FILE = "index.android.bundle";
43
+ private static final String PREFS_NAME = "yaver_hot_reload";
44
+ private static final String PREFS_KEY_BUNDLE = "bundle_path";
45
+
46
+ public YaverHotReloadModule(ReactApplicationContext context) {
47
+ super(context);
48
+ }
49
+
50
+ @Override
51
+ @NonNull
52
+ public String getName() {
53
+ return MODULE_NAME;
54
+ }
55
+
56
+ /**
57
+ * Download a Hermes bundle from the agent and trigger a bridge reload.
58
+ */
59
+ @ReactMethod
60
+ public void loadBundle(String urlString, ReadableMap headers, Promise promise) {
61
+ Executors.newSingleThreadExecutor().execute(() -> {
62
+ try {
63
+ URL url = new URL(urlString);
64
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
65
+ conn.setConnectTimeout(60000);
66
+ conn.setReadTimeout(60000);
67
+
68
+ // Set auth headers
69
+ if (headers != null) {
70
+ if (headers.hasKey("Authorization")) {
71
+ conn.setRequestProperty("Authorization", headers.getString("Authorization"));
72
+ }
73
+ }
74
+
75
+ int responseCode = conn.getResponseCode();
76
+ if (responseCode != 200) {
77
+ promise.reject("HTTP_ERROR", "Status " + responseCode);
78
+ return;
79
+ }
80
+
81
+ InputStream is = conn.getInputStream();
82
+ File dir = new File(getReactApplicationContext().getFilesDir(), BUNDLE_DIR);
83
+ if (!dir.exists()) dir.mkdirs();
84
+ File bundleFile = new File(dir, BUNDLE_FILE);
85
+
86
+ FileOutputStream fos = new FileOutputStream(bundleFile);
87
+ byte[] buffer = new byte[8192];
88
+ int bytesRead;
89
+ int totalBytes = 0;
90
+ while ((bytesRead = is.read(buffer)) != -1) {
91
+ fos.write(buffer, 0, bytesRead);
92
+ totalBytes += bytesRead;
93
+ }
94
+ fos.close();
95
+ is.close();
96
+ conn.disconnect();
97
+
98
+ Log.i(TAG, "saved " + totalBytes + " bytes to " + bundleFile.getAbsolutePath());
99
+
100
+ // Validate Hermes bytecode (magic bytes at offset 4)
101
+ if (totalBytes >= 12) {
102
+ java.io.RandomAccessFile raf = new java.io.RandomAccessFile(bundleFile, "r");
103
+ raf.seek(4);
104
+ int magic = Integer.reverseBytes(raf.readInt());
105
+ if (magic == 0x1F1903C1) {
106
+ int bcVersion = Integer.reverseBytes(raf.readInt());
107
+ Log.i(TAG, "Hermes bytecode BC" + bcVersion);
108
+ } else {
109
+ Log.w(TAG, "not Hermes bytecode (magic=0x" + Integer.toHexString(magic) + ")");
110
+ }
111
+ raf.close();
112
+ }
113
+
114
+ // Save bundle path to SharedPreferences for next app launch
115
+ SharedPreferences prefs = getReactApplicationContext()
116
+ .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
117
+ prefs.edit().putString(PREFS_KEY_BUNDLE, bundleFile.getAbsolutePath()).apply();
118
+
119
+ WritableMap result = Arguments.createMap();
120
+ result.putBoolean("loaded", true);
121
+ result.putInt("size", totalBytes);
122
+ promise.resolve(result);
123
+
124
+ // Reload the bridge on the main thread
125
+ new Handler(Looper.getMainLooper()).post(() -> reloadBridge());
126
+
127
+ } catch (Exception e) {
128
+ Log.e(TAG, "download failed", e);
129
+ promise.reject("DOWNLOAD_FAILED", e.getMessage(), e);
130
+ }
131
+ });
132
+ }
133
+
134
+ @ReactMethod
135
+ public void hasBundle(Promise promise) {
136
+ File bundleFile = getSavedBundleFile(getReactApplicationContext());
137
+ promise.resolve(bundleFile != null && bundleFile.exists());
138
+ }
139
+
140
+ @ReactMethod
141
+ public void clearBundle(Promise promise) {
142
+ File dir = new File(getReactApplicationContext().getFilesDir(), BUNDLE_DIR);
143
+ if (dir.exists()) {
144
+ for (File f : dir.listFiles()) f.delete();
145
+ dir.delete();
146
+ }
147
+ SharedPreferences prefs = getReactApplicationContext()
148
+ .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
149
+ prefs.edit().remove(PREFS_KEY_BUNDLE).apply();
150
+ promise.resolve(true);
151
+ }
152
+
153
+ /**
154
+ * Recreate the React Native context with the new bundle.
155
+ */
156
+ private void reloadBridge() {
157
+ Activity activity = getCurrentActivity();
158
+ if (activity == null) {
159
+ Log.e(TAG, "no current activity");
160
+ return;
161
+ }
162
+
163
+ if (activity.getApplication() instanceof ReactApplication) {
164
+ ReactApplication app = (ReactApplication) activity.getApplication();
165
+ ReactInstanceManager manager = app.getReactNativeHost().getReactInstanceManager();
166
+ Log.i(TAG, "recreating React context with new bundle");
167
+ manager.recreateReactContextInBackground();
168
+ } else {
169
+ Log.e(TAG, "Application does not implement ReactApplication");
170
+ }
171
+ }
172
+
173
+ // MARK: - Static helpers for Application/MainApplication
174
+
175
+ /**
176
+ * Returns the hot-reloaded bundle file if it exists.
177
+ * Call from MainApplication.getJSBundleFile() to load the hot bundle on startup.
178
+ */
179
+ public static File getSavedBundleFile(Context context) {
180
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
181
+ String path = prefs.getString(PREFS_KEY_BUNDLE, null);
182
+ if (path != null) {
183
+ File f = new File(path);
184
+ if (f.exists()) return f;
185
+ }
186
+ return null;
187
+ }
188
+ }
@@ -0,0 +1,33 @@
1
+ package io.yaver.feedback;
2
+
3
+ import androidx.annotation.NonNull;
4
+
5
+ import com.facebook.react.ReactPackage;
6
+ import com.facebook.react.bridge.NativeModule;
7
+ import com.facebook.react.bridge.ReactApplicationContext;
8
+ import com.facebook.react.uimanager.ViewManager;
9
+
10
+ import java.util.ArrayList;
11
+ import java.util.Collections;
12
+ import java.util.List;
13
+
14
+ /**
15
+ * React Native package that registers the YaverHotReload native module.
16
+ * Auto-linked via the Expo config plugin.
17
+ */
18
+ public class YaverHotReloadPackage implements ReactPackage {
19
+
20
+ @NonNull
21
+ @Override
22
+ public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
23
+ List<NativeModule> modules = new ArrayList<>();
24
+ modules.add(new YaverHotReloadModule(reactContext));
25
+ return modules;
26
+ }
27
+
28
+ @NonNull
29
+ @Override
30
+ public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
31
+ return Collections.emptyList();
32
+ }
33
+ }