tauri-agent-tools 0.3.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/.agents/skills/tauri-agent-tools/SKILL.md +23 -3
- package/.agents/skills/tauri-bridge-setup/SKILL.md +24 -2
- package/AGENTS.md +4 -2
- package/README.md +17 -1
- package/dist/bridge/client.d.ts +5 -2
- package/dist/bridge/client.js +26 -4
- package/dist/bridge/client.js.map +1 -1
- package/dist/bridge/tokenDiscovery.d.ts +1 -1
- package/dist/bridge/tokenDiscovery.js +3 -6
- package/dist/bridge/tokenDiscovery.js.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/consoleMonitor.js +15 -10
- package/dist/commands/consoleMonitor.js.map +1 -1
- package/dist/commands/diff.js +2 -1
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/dom.js +19 -6
- package/dist/commands/dom.js.map +1 -1
- package/dist/commands/eval.js +6 -1
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/ipcMonitor.js +21 -9
- package/dist/commands/ipcMonitor.js.map +1 -1
- package/dist/commands/listWindows.js +5 -1
- package/dist/commands/listWindows.js.map +1 -1
- package/dist/commands/mutations.d.ts +2 -18
- package/dist/commands/mutations.js +3 -1
- package/dist/commands/mutations.js.map +1 -1
- package/dist/commands/pageState.js +2 -1
- package/dist/commands/pageState.js.map +1 -1
- package/dist/commands/rustLogs.d.ts +2 -0
- package/dist/commands/rustLogs.js +105 -0
- package/dist/commands/rustLogs.js.map +1 -0
- package/dist/commands/screenshot.js +12 -2
- package/dist/commands/screenshot.js.map +1 -1
- package/dist/commands/shared.d.ts +6 -0
- package/dist/commands/shared.js +11 -0
- package/dist/commands/shared.js.map +1 -1
- package/dist/commands/snapshot.js +12 -5
- package/dist/commands/snapshot.js.map +1 -1
- package/dist/commands/storage.js +26 -22
- package/dist/commands/storage.js.map +1 -1
- package/dist/commands/wait.js +27 -5
- package/dist/commands/wait.js.map +1 -1
- package/dist/platform/macos.d.ts +2 -1
- package/dist/platform/macos.js +3 -1
- package/dist/platform/macos.js.map +1 -1
- package/dist/platform/wayland.d.ts +2 -1
- package/dist/platform/wayland.js +4 -3
- package/dist/platform/wayland.js.map +1 -1
- package/dist/platform/x11.d.ts +2 -1
- package/dist/platform/x11.js +17 -18
- package/dist/platform/x11.js.map +1 -1
- package/dist/schemas/bridge.d.ts +120 -0
- package/dist/schemas/bridge.js +38 -0
- package/dist/schemas/bridge.js.map +1 -0
- package/dist/schemas/commands.d.ts +245 -0
- package/dist/schemas/commands.js +65 -0
- package/dist/schemas/commands.js.map +1 -0
- package/dist/schemas/dom.d.ts +22 -0
- package/dist/schemas/dom.js +22 -0
- package/dist/schemas/dom.js.map +1 -0
- package/dist/schemas/index.d.ts +11 -0
- package/dist/schemas/index.js +12 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/platform.d.ts +61 -0
- package/dist/schemas/platform.js +34 -0
- package/dist/schemas/platform.js.map +1 -0
- package/dist/types.d.ts +2 -11
- package/dist/util/exec.js +2 -4
- package/dist/util/exec.js.map +1 -1
- package/dist/util/image.d.ts +2 -1
- package/dist/util/image.js.map +1 -1
- package/examples/tauri-bridge/Cargo.toml +2 -0
- package/examples/tauri-bridge/src/dev_bridge.rs +232 -7
- package/package.json +5 -3
- 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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
+
"version": "0.4.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
|
-
"
|
|
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",
|
package/rust-bridge/README.md
CHANGED
|
@@ -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 }`
|
|
84
|
-
5.
|
|
85
|
-
6. The
|
|
86
|
-
7. The
|
|
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
|
|