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.
- package/.agents/skills/tauri-agent-tools/SKILL.md +59 -14
- package/.agents/skills/tauri-bridge-setup/SKILL.md +10 -4
- package/.agents/skills/tauri-debug-quickstart/SKILL.md +10 -6
- package/AGENTS.md +13 -3
- package/README.md +33 -5
- package/dist/bridge/client.d.ts +21 -0
- package/dist/bridge/client.js +38 -2
- package/dist/bridge/client.js.map +1 -1
- package/dist/bridge/tokenDiscovery.js +5 -2
- package/dist/bridge/tokenDiscovery.js.map +1 -1
- package/dist/cli.js +5 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/bundle.d.ts +2 -0
- package/dist/commands/bundle.js +212 -0
- package/dist/commands/bundle.js.map +1 -0
- package/dist/commands/capabilitiesAudit.js +14 -1
- package/dist/commands/capabilitiesAudit.js.map +1 -1
- package/dist/commands/capture.d.ts +11 -0
- package/dist/commands/capture.js +161 -156
- package/dist/commands/capture.js.map +1 -1
- package/dist/commands/check.js +2 -2
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/consoleMonitor.js +3 -3
- package/dist/commands/consoleMonitor.js.map +1 -1
- package/dist/commands/dom.js +2 -2
- package/dist/commands/dom.js.map +1 -1
- package/dist/commands/health.js +19 -1
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/interact/click.js +2 -2
- package/dist/commands/interact/click.js.map +1 -1
- package/dist/commands/interact/scroll.js +3 -3
- package/dist/commands/interact/scroll.js.map +1 -1
- package/dist/commands/ipcMonitor.js +41 -7
- package/dist/commands/ipcMonitor.js.map +1 -1
- package/dist/commands/logs.d.ts +30 -0
- package/dist/commands/logs.js +279 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/mutations.js +3 -3
- package/dist/commands/mutations.js.map +1 -1
- package/dist/commands/probe.js +49 -9
- package/dist/commands/probe.js.map +1 -1
- package/dist/commands/processTree.js +116 -4
- package/dist/commands/processTree.js.map +1 -1
- package/dist/commands/rustLogs.js +3 -3
- package/dist/commands/rustLogs.js.map +1 -1
- package/dist/commands/screenshot.js +2 -2
- package/dist/commands/screenshot.js.map +1 -1
- package/dist/commands/shared.d.ts +37 -0
- package/dist/commands/shared.js +62 -3
- package/dist/commands/shared.js.map +1 -1
- package/dist/commands/sidecarReplay.js +46 -1
- package/dist/commands/sidecarReplay.js.map +1 -1
- package/dist/commands/sidecarTap.js +23 -8
- package/dist/commands/sidecarTap.js.map +1 -1
- package/dist/commands/snapshot.js +2 -2
- package/dist/commands/snapshot.js.map +1 -1
- package/dist/commands/storeInspect.js +2 -2
- package/dist/commands/storeInspect.js.map +1 -1
- package/dist/commands/wait.js +3 -3
- package/dist/commands/wait.js.map +1 -1
- package/dist/commands/webviewAttach.js +17 -1
- package/dist/commands/webviewAttach.js.map +1 -1
- package/dist/platform/macos.js +35 -15
- package/dist/platform/macos.js.map +1 -1
- package/dist/schemas/bridge.d.ts +14 -0
- package/dist/schemas/bridge.js +3 -0
- package/dist/schemas/bridge.js.map +1 -1
- package/dist/util/logMerge.d.ts +42 -0
- package/dist/util/logMerge.js +133 -0
- package/dist/util/logMerge.js.map +1 -0
- package/dist/util/mergeByTimestamp.d.ts +18 -0
- package/dist/util/mergeByTimestamp.js +30 -0
- package/dist/util/mergeByTimestamp.js.map +1 -0
- package/dist/util/psTree.d.ts +28 -0
- package/dist/util/psTree.js +76 -0
- package/dist/util/psTree.js.map +1 -0
- package/dist/util/redactText.d.ts +23 -0
- package/dist/util/redactText.js +377 -0
- package/dist/util/redactText.js.map +1 -0
- package/examples/tauri-bridge/src/dev_bridge.rs +199 -12
- package/package.json +3 -2
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
|
257
|
-
|
|
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 {
|
|
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.
|
|
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
|
-
"
|
|
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
|
},
|
package/rust-bridge/README.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|