tauri-agent-tools 0.7.1 → 0.9.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.
Files changed (82) hide show
  1. package/.agents/skills/tauri-agent-tools/SKILL.md +59 -14
  2. package/.agents/skills/tauri-bridge-setup/SKILL.md +10 -4
  3. package/.agents/skills/tauri-debug-quickstart/SKILL.md +10 -6
  4. package/AGENTS.md +13 -3
  5. package/README.md +33 -5
  6. package/dist/bridge/client.d.ts +21 -0
  7. package/dist/bridge/client.js +38 -2
  8. package/dist/bridge/client.js.map +1 -1
  9. package/dist/bridge/tokenDiscovery.js +5 -2
  10. package/dist/bridge/tokenDiscovery.js.map +1 -1
  11. package/dist/cli.js +5 -1
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/bundle.d.ts +2 -0
  14. package/dist/commands/bundle.js +212 -0
  15. package/dist/commands/bundle.js.map +1 -0
  16. package/dist/commands/capabilitiesAudit.js +14 -1
  17. package/dist/commands/capabilitiesAudit.js.map +1 -1
  18. package/dist/commands/capture.d.ts +11 -0
  19. package/dist/commands/capture.js +161 -156
  20. package/dist/commands/capture.js.map +1 -1
  21. package/dist/commands/check.js +2 -2
  22. package/dist/commands/check.js.map +1 -1
  23. package/dist/commands/consoleMonitor.js +3 -3
  24. package/dist/commands/consoleMonitor.js.map +1 -1
  25. package/dist/commands/dom.js +2 -2
  26. package/dist/commands/dom.js.map +1 -1
  27. package/dist/commands/health.js +19 -1
  28. package/dist/commands/health.js.map +1 -1
  29. package/dist/commands/interact/click.js +2 -2
  30. package/dist/commands/interact/click.js.map +1 -1
  31. package/dist/commands/interact/scroll.js +3 -3
  32. package/dist/commands/interact/scroll.js.map +1 -1
  33. package/dist/commands/ipcMonitor.js +41 -7
  34. package/dist/commands/ipcMonitor.js.map +1 -1
  35. package/dist/commands/logs.d.ts +30 -0
  36. package/dist/commands/logs.js +279 -0
  37. package/dist/commands/logs.js.map +1 -0
  38. package/dist/commands/mutations.js +3 -3
  39. package/dist/commands/mutations.js.map +1 -1
  40. package/dist/commands/probe.js +49 -9
  41. package/dist/commands/probe.js.map +1 -1
  42. package/dist/commands/processTree.js +116 -4
  43. package/dist/commands/processTree.js.map +1 -1
  44. package/dist/commands/rustLogs.js +3 -3
  45. package/dist/commands/rustLogs.js.map +1 -1
  46. package/dist/commands/screenshot.js +2 -2
  47. package/dist/commands/screenshot.js.map +1 -1
  48. package/dist/commands/shared.d.ts +37 -0
  49. package/dist/commands/shared.js +62 -3
  50. package/dist/commands/shared.js.map +1 -1
  51. package/dist/commands/sidecarReplay.js +46 -1
  52. package/dist/commands/sidecarReplay.js.map +1 -1
  53. package/dist/commands/sidecarTap.js +23 -8
  54. package/dist/commands/sidecarTap.js.map +1 -1
  55. package/dist/commands/snapshot.js +2 -2
  56. package/dist/commands/snapshot.js.map +1 -1
  57. package/dist/commands/storeInspect.js +2 -2
  58. package/dist/commands/storeInspect.js.map +1 -1
  59. package/dist/commands/wait.js +3 -3
  60. package/dist/commands/wait.js.map +1 -1
  61. package/dist/commands/webviewAttach.js +17 -1
  62. package/dist/commands/webviewAttach.js.map +1 -1
  63. package/dist/platform/macos.js +35 -15
  64. package/dist/platform/macos.js.map +1 -1
  65. package/dist/schemas/bridge.d.ts +14 -0
  66. package/dist/schemas/bridge.js +3 -0
  67. package/dist/schemas/bridge.js.map +1 -1
  68. package/dist/util/logMerge.d.ts +42 -0
  69. package/dist/util/logMerge.js +133 -0
  70. package/dist/util/logMerge.js.map +1 -0
  71. package/dist/util/mergeByTimestamp.d.ts +18 -0
  72. package/dist/util/mergeByTimestamp.js +30 -0
  73. package/dist/util/mergeByTimestamp.js.map +1 -0
  74. package/dist/util/psTree.d.ts +28 -0
  75. package/dist/util/psTree.js +76 -0
  76. package/dist/util/psTree.js.map +1 -0
  77. package/dist/util/redactText.d.ts +23 -0
  78. package/dist/util/redactText.js +377 -0
  79. package/dist/util/redactText.js.map +1 -0
  80. package/examples/tauri-bridge/src/dev_bridge.rs +199 -12
  81. package/package.json +3 -2
  82. package/rust-bridge/README.md +2 -2
@@ -6,7 +6,7 @@ use std::io::{BufRead, BufReader};
6
6
  use std::process::{Command, Stdio};
7
7
  use std::sync::{Arc, Condvar, Mutex};
8
8
  use std::thread;
9
- use std::time::Instant;
9
+ use std::time::{Duration, Instant};
10
10
  use tauri::{AppHandle, Manager};
11
11
  use tiny_http::{Header, Response, Server};
12
12
  use tracing_subscriber::layer::SubscriberExt;
@@ -14,7 +14,7 @@ use tracing_subscriber::util::SubscriberInitExt;
14
14
 
15
15
  /// Bridge protocol version exposed via `GET /version`. Bumped whenever the
16
16
  /// HTTP surface changes shape so CLI clients can feature-detect.
17
- pub const BRIDGE_VERSION: &str = "0.7.0";
17
+ pub const BRIDGE_VERSION: &str = "0.8.0";
18
18
 
19
19
  #[derive(Deserialize)]
20
20
  struct EvalRequest {
@@ -27,6 +27,12 @@ struct EvalRequest {
27
27
  #[derive(Deserialize)]
28
28
  struct LogRequest {
29
29
  token: String,
30
+ #[serde(default)]
31
+ cursor: Option<u64>,
32
+ #[serde(default, rename = "waitMs")]
33
+ wait_ms: Option<u64>,
34
+ #[serde(default)]
35
+ limit: Option<usize>,
30
36
  }
31
37
 
32
38
  #[derive(Serialize)]
@@ -36,6 +42,7 @@ struct EvalResponse {
36
42
 
37
43
  #[derive(Clone, Serialize)]
38
44
  pub struct LogEntry {
45
+ pub id: u64,
39
46
  pub timestamp: u64,
40
47
  pub level: String,
41
48
  pub target: String,
@@ -46,6 +53,10 @@ pub struct LogEntry {
46
53
  #[derive(Serialize)]
47
54
  struct LogResponse {
48
55
  entries: Vec<LogEntry>,
56
+ #[serde(skip_serializing_if = "Option::is_none")]
57
+ cursor: Option<u64>,
58
+ #[serde(skip_serializing_if = "Option::is_none")]
59
+ dropped: Option<u64>,
49
60
  }
50
61
 
51
62
  #[derive(Deserialize)]
@@ -234,28 +245,113 @@ pub fn register_sidecar(
234
245
 
235
246
  /// Ring buffer for log entries. Thread-safe, capped at 1000 entries.
236
247
  pub struct LogBuffer {
237
- entries: Mutex<VecDeque<LogEntry>>,
248
+ state: Mutex<LogBufferState>,
249
+ notify: Condvar,
250
+ }
251
+
252
+ struct LogBufferState {
253
+ entries: VecDeque<LogEntry>,
254
+ next_id: u64,
238
255
  }
239
256
 
240
257
  impl LogBuffer {
241
258
  pub fn new() -> Self {
242
259
  Self {
243
- entries: Mutex::new(VecDeque::new()),
260
+ state: Mutex::new(LogBufferState {
261
+ entries: VecDeque::new(),
262
+ next_id: 1,
263
+ }),
264
+ notify: Condvar::new(),
244
265
  }
245
266
  }
246
267
 
247
- pub fn push(&self, entry: LogEntry) {
248
- let mut buf = self.entries.lock().unwrap();
249
- if buf.len() >= 1000 {
250
- buf.pop_front();
268
+ pub fn push(&self, mut entry: LogEntry) {
269
+ let mut state = self.state.lock().unwrap();
270
+ entry.id = state.next_id;
271
+ state.next_id += 1;
272
+ if state.entries.len() >= 1000 {
273
+ state.entries.pop_front();
251
274
  }
252
- buf.push_back(entry);
275
+ state.entries.push_back(entry);
276
+ self.notify.notify_all();
253
277
  }
254
278
 
255
279
  pub fn drain(&self) -> Vec<LogEntry> {
256
- let mut buf = self.entries.lock().unwrap();
257
- buf.drain(..).collect()
280
+ let mut state = self.state.lock().unwrap();
281
+ state.entries.drain(..).collect()
282
+ }
283
+
284
+ pub fn read_since(&self, cursor: u64, limit: usize) -> (Vec<LogEntry>, u64, u64) {
285
+ let state = self.state.lock().unwrap();
286
+ read_since_locked(&state, cursor, limit)
258
287
  }
288
+
289
+ fn wait_until_entries_since(&self, cursor: u64, timeout: Duration) {
290
+ if timeout.is_zero() {
291
+ return;
292
+ }
293
+ let state = self.state.lock().unwrap();
294
+ if has_entries_since(&state, cursor) {
295
+ return;
296
+ }
297
+ let _ = self
298
+ .notify
299
+ .wait_timeout_while(state, timeout, |state| !has_entries_since(state, cursor))
300
+ .unwrap();
301
+ }
302
+ }
303
+
304
+ fn read_since_locked(
305
+ state: &LogBufferState,
306
+ cursor: u64,
307
+ limit: usize,
308
+ ) -> (Vec<LogEntry>, u64, u64) {
309
+ let capped_limit = limit.clamp(1, 1000);
310
+ let dropped = if cursor == 0 {
311
+ 0
312
+ } else {
313
+ state
314
+ .entries
315
+ .front()
316
+ .map(|entry| entry.id.saturating_sub(cursor.saturating_add(1)))
317
+ .unwrap_or(0)
318
+ };
319
+ let entries: Vec<LogEntry> = state
320
+ .entries
321
+ .iter()
322
+ .filter(|entry| entry.id > cursor)
323
+ .take(capped_limit)
324
+ .cloned()
325
+ .collect();
326
+ let next_cursor = entries.last().map(|entry| entry.id).unwrap_or(cursor);
327
+ (entries, next_cursor, dropped)
328
+ }
329
+
330
+ fn has_entries_since(state: &LogBufferState, cursor: u64) -> bool {
331
+ state
332
+ .entries
333
+ .back()
334
+ .map(|entry| entry.id > cursor)
335
+ .unwrap_or(false)
336
+ }
337
+
338
+ fn respond_log_cursor_request(
339
+ request: tiny_http::Request,
340
+ buffer: Arc<LogBuffer>,
341
+ cursor: u64,
342
+ wait_ms: u64,
343
+ limit: usize,
344
+ ) {
345
+ buffer.wait_until_entries_since(cursor, Duration::from_millis(wait_ms));
346
+ let (entries, cursor, dropped) = buffer.read_since(cursor, limit);
347
+ let resp = LogResponse {
348
+ entries,
349
+ cursor: Some(cursor),
350
+ dropped: Some(dropped),
351
+ };
352
+ let json = serde_json::to_string(&resp).unwrap();
353
+ let header = Header::from_bytes("Content-Type", "application/json").unwrap();
354
+ let _ = request.respond(Response::from_string(json).with_header(header));
259
355
  }
260
356
 
261
357
  /// A tracing layer that captures log events into a `LogBuffer`.
@@ -278,6 +374,7 @@ where
278
374
  event.record(&mut visitor);
279
375
 
280
376
  let entry = LogEntry {
377
+ id: 0,
281
378
  timestamp: std::time::SystemTime::now()
282
379
  .duration_since(std::time::UNIX_EPOCH)
283
380
  .unwrap_or_default()
@@ -372,6 +469,7 @@ pub fn spawn_sidecar_monitored(
372
469
  for line in reader.lines() {
373
470
  let Ok(line) = line else { break };
374
471
  buffer.push(LogEntry {
472
+ id: 0,
375
473
  timestamp: std::time::SystemTime::now()
376
474
  .duration_since(std::time::UNIX_EPOCH)
377
475
  .unwrap_or_default()
@@ -394,6 +492,7 @@ pub fn spawn_sidecar_monitored(
394
492
  for line in reader.lines() {
395
493
  let Ok(line) = line else { break };
396
494
  buffer.push(LogEntry {
495
+ id: 0,
397
496
  timestamp: std::time::SystemTime::now()
398
497
  .duration_since(std::time::UNIX_EPOCH)
399
498
  .unwrap_or_default()
@@ -489,6 +588,10 @@ fn build_eval_callback_js(js: &str, request_id: &str) -> String {
489
588
  /// registry is what powers the `/process` and `/health` endpoints' visibility
490
589
  /// into the process tree. Callers that don't spawn sidecars can ignore the
491
590
  /// registry handle.
591
+ ///
592
+ /// `/logs` supports legacy drain mode and v0.8 cursor mode. Cursor requests
593
+ /// are non-draining and may long-poll on a worker thread, so concurrent
594
+ /// consumers no longer empty each other's buffers.
492
595
  pub fn start_bridge(
493
596
  app: &AppHandle,
494
597
  ) -> Result<(u16, Arc<LogBuffer>, Arc<SidecarRegistry>), String> {
@@ -620,8 +723,29 @@ pub fn start_bridge(
620
723
  continue;
621
724
  }
622
725
 
726
+ if let Some(cursor) = log_req.cursor {
727
+ let wait_ms = log_req.wait_ms.unwrap_or(0).min(25_000);
728
+ let limit = log_req.limit.unwrap_or(1000).clamp(1, 1000);
729
+ let buffer = server_log_buffer.clone();
730
+ if wait_ms > 0 {
731
+ // Long-poll on a worker thread: the bridge serves
732
+ // requests serially, so waiting inline would stall
733
+ // every other endpoint for up to waitMs.
734
+ thread::spawn(move || {
735
+ respond_log_cursor_request(request, buffer, cursor, wait_ms, limit);
736
+ });
737
+ } else {
738
+ respond_log_cursor_request(request, buffer, cursor, wait_ms, limit);
739
+ }
740
+ continue;
741
+ }
742
+
623
743
  let entries = server_log_buffer.drain();
624
- let resp = LogResponse { entries };
744
+ let resp = LogResponse {
745
+ entries,
746
+ cursor: None,
747
+ dropped: None,
748
+ };
625
749
  let json = serde_json::to_string(&resp).unwrap();
626
750
  let header = Header::from_bytes("Content-Type", "application/json").unwrap();
627
751
  let _ = request.respond(Response::from_string(json).with_header(header));
@@ -912,6 +1036,69 @@ mod tests {
912
1036
  assert!(EVAL_TIMEOUT_MESSAGE.contains("Re-copy"));
913
1037
  assert!(EVAL_TIMEOUT_MESSAGE.contains("dev_bridge.rs"));
914
1038
  }
1039
+
1040
+ #[test]
1041
+ fn log_buffer_assigns_monotonic_ids_and_cursor_reads_do_not_drain() {
1042
+ let buffer = LogBuffer::new();
1043
+ buffer.push(test_log_entry("first"));
1044
+ buffer.push(test_log_entry("second"));
1045
+
1046
+ let (entries, cursor, dropped) = buffer.read_since(0, 1000);
1047
+ assert_eq!(
1048
+ entries.iter().map(|entry| entry.id).collect::<Vec<_>>(),
1049
+ vec![1, 2]
1050
+ );
1051
+ assert_eq!(cursor, 2);
1052
+ assert_eq!(dropped, 0);
1053
+
1054
+ let drained = buffer.drain();
1055
+ assert_eq!(
1056
+ drained.iter().map(|entry| entry.id).collect::<Vec<_>>(),
1057
+ vec![1, 2]
1058
+ );
1059
+ }
1060
+
1061
+ #[test]
1062
+ fn log_buffer_read_since_honors_limit_and_advances_to_last_returned_id() {
1063
+ let buffer = LogBuffer::new();
1064
+ buffer.push(test_log_entry("first"));
1065
+ buffer.push(test_log_entry("second"));
1066
+ buffer.push(test_log_entry("third"));
1067
+
1068
+ let (entries, cursor, dropped) = buffer.read_since(0, 2);
1069
+ assert_eq!(
1070
+ entries.iter().map(|entry| entry.id).collect::<Vec<_>>(),
1071
+ vec![1, 2]
1072
+ );
1073
+ assert_eq!(cursor, 2);
1074
+ assert_eq!(dropped, 0);
1075
+ }
1076
+
1077
+ #[test]
1078
+ fn log_buffer_reports_entries_evicted_past_the_cursor() {
1079
+ let buffer = LogBuffer::new();
1080
+ for index in 0..1002 {
1081
+ buffer.push(test_log_entry(&format!("entry-{index}")));
1082
+ }
1083
+
1084
+ let (entries, cursor, dropped) = buffer.read_since(1, 1000);
1085
+ assert_eq!(entries.first().map(|entry| entry.id), Some(3));
1086
+ assert_eq!(entries.last().map(|entry| entry.id), Some(1002));
1087
+ assert_eq!(entries.len(), 1000);
1088
+ assert_eq!(cursor, 1002);
1089
+ assert_eq!(dropped, 1);
1090
+ }
1091
+
1092
+ fn test_log_entry(message: &str) -> LogEntry {
1093
+ LogEntry {
1094
+ id: 0,
1095
+ timestamp: 1,
1096
+ level: "info".to_string(),
1097
+ target: "test".to_string(),
1098
+ message: message.to_string(),
1099
+ source: "rust".to_string(),
1100
+ }
1101
+ }
915
1102
  }
916
1103
 
917
1104
  /// Read declared capabilities from tauri.conf.json via `app.config()`. Returns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tauri-agent-tools",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "Agent-driven inspection toolkit for Tauri desktop apps",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,8 @@
14
14
  "test:watch": "vitest",
15
15
  "dev": "tsc --watch",
16
16
  "typecheck": "tsc --noEmit",
17
- "lint": "tsc --noEmit && eslint src/",
17
+ "check:bridge-parity": "node scripts/check-bridge-parity.mjs",
18
+ "lint": "npm run check:bridge-parity && tsc --noEmit && eslint src/",
18
19
  "lint:fix": "eslint src/ --fix",
19
20
  "prepublishOnly": "npm run build"
20
21
  },
@@ -85,11 +85,11 @@ tauri-agent-tools eval "document.title"
85
85
  1. Bridge starts an HTTP server on a random localhost port
86
86
  2. A token file with `{ port, token, pid }` is written to `/tmp/`
87
87
  3. `tauri-agent-tools` discovers the token file and authenticates via the token
88
- 4. The bridge exposes four endpoints: `POST /eval` (JS evaluation), `POST /logs` (Rust log retrieval), `POST /describe` (bridge metadata), and `GET /version` (unauthenticated health check)
88
+ 4. The bridge exposes `POST /eval` (JS evaluation), `POST /logs` (Rust log retrieval), `POST /describe` (bridge metadata), `GET /version` (unauthenticated health check), and the v0.7 diagnostics endpoints `POST /process`, `POST /capabilities`, `POST /devtools`, and `POST /health`
89
89
  5. `/eval` accepts an optional `window` field to target specific webview windows (defaults to `"main"`)
90
90
  6. The injected JS evaluates the expression, then calls back into Rust via `window.__TAURI_INTERNALS__.invoke("__dev_bridge_result", { id, value })` to deliver the result, falling back to `window.__TAURI__.core.invoke()` for older/global-enabled apps
91
91
  7. The HTTP handler thread waits for the result (up to 5 seconds) and returns it as JSON
92
- 8. `/logs` drains the ring buffer of captured `tracing` events and returns them as JSON
92
+ 8. `/logs` returns captured `tracing` events as JSON — a bare `{ token }` request drains the ring buffer (legacy mode), while a request with `{ cursor, waitMs, limit }` (v0.8+) reads without draining so multiple consumers can tail concurrently; cursor requests may long-poll up to `waitMs` and report `dropped` entries evicted past the cursor
93
93
  9. `/describe` returns PID, window labels, and capabilities
94
94
  10. The token file is cleaned up when the app exits
95
95