tauri-agent-tools 0.3.0 → 0.5.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 +24 -3
  2. package/.agents/skills/tauri-bridge-setup/SKILL.md +24 -2
  3. package/AGENTS.md +4 -2
  4. package/README.md +19 -1
  5. package/dist/bridge/client.d.ts +5 -2
  6. package/dist/bridge/client.js +26 -4
  7. package/dist/bridge/client.js.map +1 -1
  8. package/dist/bridge/tokenDiscovery.d.ts +1 -1
  9. package/dist/bridge/tokenDiscovery.js +3 -6
  10. package/dist/bridge/tokenDiscovery.js.map +1 -1
  11. package/dist/cli.js +10 -2
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/consoleMonitor.js +15 -10
  14. package/dist/commands/consoleMonitor.js.map +1 -1
  15. package/dist/commands/diff.js +2 -1
  16. package/dist/commands/diff.js.map +1 -1
  17. package/dist/commands/dom.js +19 -6
  18. package/dist/commands/dom.js.map +1 -1
  19. package/dist/commands/eval.js +6 -1
  20. package/dist/commands/eval.js.map +1 -1
  21. package/dist/commands/ipcMonitor.js +21 -9
  22. package/dist/commands/ipcMonitor.js.map +1 -1
  23. package/dist/commands/listWindows.js +5 -1
  24. package/dist/commands/listWindows.js.map +1 -1
  25. package/dist/commands/mutations.d.ts +2 -18
  26. package/dist/commands/mutations.js +3 -1
  27. package/dist/commands/mutations.js.map +1 -1
  28. package/dist/commands/pageState.js +2 -1
  29. package/dist/commands/pageState.js.map +1 -1
  30. package/dist/commands/rustLogs.d.ts +2 -0
  31. package/dist/commands/rustLogs.js +105 -0
  32. package/dist/commands/rustLogs.js.map +1 -0
  33. package/dist/commands/screenshot.js +12 -2
  34. package/dist/commands/screenshot.js.map +1 -1
  35. package/dist/commands/shared.d.ts +6 -0
  36. package/dist/commands/shared.js +11 -0
  37. package/dist/commands/shared.js.map +1 -1
  38. package/dist/commands/snapshot.js +12 -5
  39. package/dist/commands/snapshot.js.map +1 -1
  40. package/dist/commands/storage.js +26 -22
  41. package/dist/commands/storage.js.map +1 -1
  42. package/dist/commands/wait.js +27 -5
  43. package/dist/commands/wait.js.map +1 -1
  44. package/dist/platform/detect.d.ts +2 -1
  45. package/dist/platform/detect.js +28 -7
  46. package/dist/platform/detect.js.map +1 -1
  47. package/dist/platform/hyprland.d.ts +11 -0
  48. package/dist/platform/hyprland.js +51 -0
  49. package/dist/platform/hyprland.js.map +1 -0
  50. package/dist/platform/macos.d.ts +2 -1
  51. package/dist/platform/macos.js +3 -1
  52. package/dist/platform/macos.js.map +1 -1
  53. package/dist/platform/wayland.d.ts +2 -1
  54. package/dist/platform/wayland.js +4 -3
  55. package/dist/platform/wayland.js.map +1 -1
  56. package/dist/platform/x11.d.ts +2 -1
  57. package/dist/platform/x11.js +17 -18
  58. package/dist/platform/x11.js.map +1 -1
  59. package/dist/schemas/bridge.d.ts +120 -0
  60. package/dist/schemas/bridge.js +38 -0
  61. package/dist/schemas/bridge.js.map +1 -0
  62. package/dist/schemas/commands.d.ts +245 -0
  63. package/dist/schemas/commands.js +65 -0
  64. package/dist/schemas/commands.js.map +1 -0
  65. package/dist/schemas/dom.d.ts +22 -0
  66. package/dist/schemas/dom.js +22 -0
  67. package/dist/schemas/dom.js.map +1 -0
  68. package/dist/schemas/index.d.ts +11 -0
  69. package/dist/schemas/index.js +12 -0
  70. package/dist/schemas/index.js.map +1 -0
  71. package/dist/schemas/platform.d.ts +112 -0
  72. package/dist/schemas/platform.js +45 -0
  73. package/dist/schemas/platform.js.map +1 -0
  74. package/dist/types.d.ts +3 -12
  75. package/dist/util/exec.js +2 -4
  76. package/dist/util/exec.js.map +1 -1
  77. package/dist/util/image.d.ts +2 -1
  78. package/dist/util/image.js.map +1 -1
  79. package/examples/tauri-bridge/Cargo.toml +2 -0
  80. package/examples/tauri-bridge/src/dev_bridge.rs +232 -7
  81. package/package.json +5 -3
  82. package/rust-bridge/README.md +9 -5
@@ -1,12 +1,15 @@
1
1
  use rand::Rng;
2
2
  use serde::{Deserialize, Serialize};
3
- use std::collections::HashMap;
3
+ use std::collections::{HashMap, VecDeque};
4
4
  use std::fs;
5
- use std::io::Write;
5
+ use std::io::{BufRead, BufReader, Write};
6
+ use std::process::{Command, Stdio};
6
7
  use std::sync::{Arc, Condvar, Mutex};
7
8
  use std::thread;
8
9
  use tauri::{AppHandle, Manager};
9
10
  use tiny_http::{Header, Response, Server};
11
+ use tracing_subscriber::layer::SubscriberExt;
12
+ use tracing_subscriber::util::SubscriberInitExt;
10
13
 
11
14
  #[derive(Deserialize)]
12
15
  struct EvalRequest {
@@ -14,11 +17,30 @@ struct EvalRequest {
14
17
  token: String,
15
18
  }
16
19
 
20
+ #[derive(Deserialize)]
21
+ struct LogRequest {
22
+ token: String,
23
+ }
24
+
17
25
  #[derive(Serialize)]
18
26
  struct EvalResponse {
19
27
  result: serde_json::Value,
20
28
  }
21
29
 
30
+ #[derive(Clone, Serialize)]
31
+ pub struct LogEntry {
32
+ pub timestamp: u64,
33
+ pub level: String,
34
+ pub target: String,
35
+ pub message: String,
36
+ pub source: String,
37
+ }
38
+
39
+ #[derive(Serialize)]
40
+ struct LogResponse {
41
+ entries: Vec<LogEntry>,
42
+ }
43
+
22
44
  #[derive(Serialize)]
23
45
  struct TokenFile {
24
46
  port: u16,
@@ -26,6 +48,172 @@ struct TokenFile {
26
48
  pid: u32,
27
49
  }
28
50
 
51
+ /// Ring buffer for log entries. Thread-safe, capped at 1000 entries.
52
+ pub struct LogBuffer {
53
+ entries: Mutex<VecDeque<LogEntry>>,
54
+ }
55
+
56
+ impl LogBuffer {
57
+ pub fn new() -> Self {
58
+ Self {
59
+ entries: Mutex::new(VecDeque::new()),
60
+ }
61
+ }
62
+
63
+ pub fn push(&self, entry: LogEntry) {
64
+ let mut buf = self.entries.lock().unwrap();
65
+ if buf.len() >= 1000 {
66
+ buf.pop_front();
67
+ }
68
+ buf.push_back(entry);
69
+ }
70
+
71
+ pub fn drain(&self) -> Vec<LogEntry> {
72
+ let mut buf = self.entries.lock().unwrap();
73
+ buf.drain(..).collect()
74
+ }
75
+ }
76
+
77
+ /// A tracing layer that captures log events into a `LogBuffer`.
78
+ struct BridgeLogLayer {
79
+ buffer: Arc<LogBuffer>,
80
+ }
81
+
82
+ impl<S> tracing_subscriber::Layer<S> for BridgeLogLayer
83
+ where
84
+ S: tracing::Subscriber,
85
+ {
86
+ fn on_event(
87
+ &self,
88
+ event: &tracing::Event<'_>,
89
+ _ctx: tracing_subscriber::layer::Context<'_, S>,
90
+ ) {
91
+ let mut visitor = MessageVisitor {
92
+ message: String::new(),
93
+ };
94
+ event.record(&mut visitor);
95
+
96
+ let entry = LogEntry {
97
+ timestamp: std::time::SystemTime::now()
98
+ .duration_since(std::time::UNIX_EPOCH)
99
+ .unwrap_or_default()
100
+ .as_millis() as u64,
101
+ level: event.metadata().level().to_string().to_lowercase(),
102
+ target: event.metadata().target().to_string(),
103
+ message: visitor.message,
104
+ source: "rust".to_string(),
105
+ };
106
+
107
+ self.buffer.push(entry);
108
+ }
109
+ }
110
+
111
+ struct MessageVisitor {
112
+ message: String,
113
+ }
114
+
115
+ impl tracing::field::Visit for MessageVisitor {
116
+ fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
117
+ if field.name() == "message" {
118
+ self.message = format!("{:?}", value);
119
+ // Remove surrounding quotes if present
120
+ if self.message.starts_with('"') && self.message.ends_with('"') {
121
+ self.message = self.message[1..self.message.len() - 1].to_string();
122
+ }
123
+ }
124
+ }
125
+
126
+ fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
127
+ if field.name() == "message" {
128
+ self.message = value.to_string();
129
+ }
130
+ }
131
+ }
132
+
133
+ /// Create a tracing layer that captures logs into the given buffer.
134
+ /// Use this if you already have a tracing subscriber and want to add log capture.
135
+ ///
136
+ /// ```rust
137
+ /// use tracing_subscriber::layer::SubscriberExt;
138
+ /// use tracing_subscriber::util::SubscriberInitExt;
139
+ ///
140
+ /// let buffer = std::sync::Arc::new(dev_bridge::LogBuffer::new());
141
+ /// tracing_subscriber::registry()
142
+ /// .with(dev_bridge::create_log_layer(buffer.clone()))
143
+ /// .with(tracing_subscriber::fmt::layer())
144
+ /// .init();
145
+ /// ```
146
+ pub fn create_log_layer(
147
+ buffer: Arc<LogBuffer>,
148
+ ) -> impl tracing_subscriber::Layer<tracing_subscriber::Registry> {
149
+ BridgeLogLayer { buffer }
150
+ }
151
+
152
+ /// Spawn a sidecar process with monitored stdout/stderr.
153
+ /// Lines from stdout are logged as "info", lines from stderr as "warn".
154
+ /// Returns the `std::process::Child` handle.
155
+ pub fn spawn_sidecar_monitored(
156
+ name: &str,
157
+ command: &str,
158
+ args: &[&str],
159
+ log_buffer: &Arc<LogBuffer>,
160
+ ) -> Result<std::process::Child, String> {
161
+ let mut child = Command::new(command)
162
+ .args(args)
163
+ .stdout(Stdio::piped())
164
+ .stderr(Stdio::piped())
165
+ .spawn()
166
+ .map_err(|e| format!("Failed to spawn sidecar {name}: {e}"))?;
167
+
168
+ let source = format!("sidecar:{name}");
169
+
170
+ // Monitor stdout
171
+ if let Some(stdout) = child.stdout.take() {
172
+ let buffer = log_buffer.clone();
173
+ let source = source.clone();
174
+ thread::spawn(move || {
175
+ let reader = BufReader::new(stdout);
176
+ for line in reader.lines() {
177
+ let Ok(line) = line else { break };
178
+ buffer.push(LogEntry {
179
+ timestamp: std::time::SystemTime::now()
180
+ .duration_since(std::time::UNIX_EPOCH)
181
+ .unwrap_or_default()
182
+ .as_millis() as u64,
183
+ level: "info".to_string(),
184
+ target: "stdout".to_string(),
185
+ message: line,
186
+ source: source.clone(),
187
+ });
188
+ }
189
+ });
190
+ }
191
+
192
+ // Monitor stderr
193
+ if let Some(stderr) = child.stderr.take() {
194
+ let buffer = log_buffer.clone();
195
+ let source = source.clone();
196
+ thread::spawn(move || {
197
+ let reader = BufReader::new(stderr);
198
+ for line in reader.lines() {
199
+ let Ok(line) = line else { break };
200
+ buffer.push(LogEntry {
201
+ timestamp: std::time::SystemTime::now()
202
+ .duration_since(std::time::UNIX_EPOCH)
203
+ .unwrap_or_default()
204
+ .as_millis() as u64,
205
+ level: "warn".to_string(),
206
+ target: "stderr".to_string(),
207
+ message: line,
208
+ source: source.clone(),
209
+ });
210
+ }
211
+ });
212
+ }
213
+
214
+ Ok(child)
215
+ }
216
+
29
217
  /// Shared state for pending eval results.
30
218
  /// The HTTP handler thread waits on the Condvar; the Tauri command inserts
31
219
  /// the result and signals.
@@ -47,8 +235,9 @@ pub fn __dev_bridge_result(
47
235
  }
48
236
 
49
237
  /// Start the development bridge HTTP server.
50
- /// Returns the port number on success.
51
- pub fn start_bridge(app: &AppHandle) -> Result<u16, String> {
238
+ /// Returns the port number and log buffer on success.
239
+ /// The log buffer can be used with `spawn_sidecar_monitored()` to capture sidecar output.
240
+ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
52
241
  let server =
53
242
  Server::http("127.0.0.1:0").map_err(|e| format!("Failed to start bridge: {e}"))?;
54
243
  let port = server
@@ -80,6 +269,13 @@ pub fn start_bridge(app: &AppHandle) -> Result<u16, String> {
80
269
  let _ = fs::remove_file(&cleanup_path);
81
270
  });
82
271
 
272
+ // Create log buffer and install tracing layer
273
+ let log_buffer = Arc::new(LogBuffer::new());
274
+ let layer = BridgeLogLayer {
275
+ buffer: log_buffer.clone(),
276
+ };
277
+ let _ = tracing_subscriber::registry().with(layer).try_init();
278
+
83
279
  // Create shared pending-results state and register it with Tauri
84
280
  let pending = Arc::new(PendingResults {
85
281
  results: Mutex::new(HashMap::new()),
@@ -89,13 +285,17 @@ pub fn start_bridge(app: &AppHandle) -> Result<u16, String> {
89
285
 
90
286
  let app_handle = app.clone();
91
287
  let expected_token = token.clone();
288
+ let server_log_buffer = log_buffer.clone();
92
289
 
93
290
  thread::spawn(move || {
94
291
  // Keep _guard alive for the lifetime of the server thread
95
292
  let _cleanup = _guard;
96
293
 
97
294
  for request in server.incoming_requests() {
98
- if request.method().as_str() != "POST" || request.url() != "/eval" {
295
+ let is_post = request.method().as_str() == "POST";
296
+ let url = request.url().to_string();
297
+
298
+ if !is_post || (url != "/eval" && url != "/logs") {
99
299
  let _ = request.respond(Response::from_string("Not found").with_status_code(404));
100
300
  continue;
101
301
  }
@@ -108,7 +308,32 @@ pub fn start_bridge(app: &AppHandle) -> Result<u16, String> {
108
308
  continue;
109
309
  }
110
310
 
111
- // Parse request
311
+ // Handle /logs endpoint
312
+ if url == "/logs" {
313
+ let log_req: LogRequest = match serde_json::from_str(&body) {
314
+ Ok(r) => r,
315
+ Err(_) => {
316
+ let _ = request
317
+ .respond(Response::from_string("Invalid JSON").with_status_code(400));
318
+ continue;
319
+ }
320
+ };
321
+
322
+ if log_req.token != expected_token {
323
+ let _ = request
324
+ .respond(Response::from_string("Unauthorized").with_status_code(401));
325
+ continue;
326
+ }
327
+
328
+ let entries = server_log_buffer.drain();
329
+ let resp = LogResponse { entries };
330
+ let json = serde_json::to_string(&resp).unwrap();
331
+ let header = Header::from_bytes("Content-Type", "application/json").unwrap();
332
+ let _ = request.respond(Response::from_string(json).with_header(header));
333
+ continue;
334
+ }
335
+
336
+ // Handle /eval endpoint
112
337
  let eval_req: EvalRequest = match serde_json::from_str(&body) {
113
338
  Ok(r) => r,
114
339
  Err(_) => {
@@ -214,5 +439,5 @@ pub fn start_bridge(app: &AppHandle) -> Result<u16, String> {
214
439
  eprintln!("Dev bridge started on port {port}");
215
440
  eprintln!("Token file: {token_path}");
216
441
 
217
- Ok(port)
442
+ Ok((port, log_buffer))
218
443
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tauri-agent-tools",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Agent-driven inspection toolkit for Tauri desktop apps",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,8 @@
13
13
  "test": "vitest run",
14
14
  "test:watch": "vitest",
15
15
  "dev": "tsc --watch",
16
- "lint": "eslint src/",
16
+ "typecheck": "tsc --noEmit",
17
+ "lint": "tsc --noEmit && eslint src/",
17
18
  "lint:fix": "eslint src/ --fix",
18
19
  "prepublishOnly": "npm run build"
19
20
  },
@@ -43,7 +44,8 @@
43
44
  "access": "public"
44
45
  },
45
46
  "dependencies": {
46
- "commander": "^14.0.0"
47
+ "commander": "^14.0.0",
48
+ "zod": "^3.25.76"
47
49
  },
48
50
  "devDependencies": {
49
51
  "@eslint/js": "^9.0.0",
@@ -14,6 +14,8 @@ serde_json = "1"
14
14
  scopeguard = "1"
15
15
  rand = "0.8"
16
16
  uuid = { version = "1", features = ["v4"] }
17
+ tracing = "0.1"
18
+ tracing-subscriber = "0.3"
17
19
  ```
18
20
 
19
21
  ### 2. Copy the bridge module
@@ -39,7 +41,7 @@ fn main() {
39
41
  builder
40
42
  .setup(|app| {
41
43
  if cfg!(debug_assertions) {
42
- if let Err(e) = dev_bridge::start_bridge(app.handle()) {
44
+ if let Err(e) = dev_bridge::start_bridge(app.handle()).map(|_| ()) {
43
45
  eprintln!("Warning: Failed to start dev bridge: {e}");
44
46
  }
45
47
  }
@@ -80,10 +82,12 @@ tauri-agent-tools eval "document.title"
80
82
  1. Bridge starts an HTTP server on a random localhost port
81
83
  2. A token file with `{ port, token, pid }` is written to `/tmp/`
82
84
  3. `tauri-agent-tools` discovers the token file and authenticates via the token
83
- 4. Requests are `POST /eval { js, token }` the bridge injects JS into the webview
84
- 5. The injected JS evaluates the expression, then calls back into Rust via `window.__TAURI__.core.invoke("__dev_bridge_result", { id, value })` to deliver the result
85
- 6. The HTTP handler thread waits for the result (up to 5 seconds) and returns it as JSON
86
- 7. The token file is cleaned up when the app exits
85
+ 4. Requests are `POST /eval { js, token }` (JS evaluation) or `POST /logs { token }` (Rust log retrieval)
86
+ 5. For `/eval`, the bridge injects JS into the webview
87
+ 6. The injected JS evaluates the expression, then calls back into Rust via `window.__TAURI__.core.invoke("__dev_bridge_result", { id, value })` to deliver the result
88
+ 7. The HTTP handler thread waits for the result (up to 5 seconds) and returns it as JSON
89
+ 8. For `/logs`, the bridge drains its ring buffer of captured `tracing` events and returns them as JSON
90
+ 9. The token file is cleaned up when the app exits
87
91
 
88
92
  ## Security
89
93