tauri-agent-tools 0.6.0 → 0.7.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 (76) hide show
  1. package/.agents/skills/tauri-agent-tools/SKILL.md +94 -5
  2. package/.agents/skills/tauri-bridge-setup/SKILL.md +45 -13
  3. package/.agents/skills/tauri-debug-quickstart/SKILL.md +80 -0
  4. package/README.md +71 -3
  5. package/dist/bridge/client.d.ts +17 -1
  6. package/dist/bridge/client.js +82 -1
  7. package/dist/bridge/client.js.map +1 -1
  8. package/dist/cli.js +25 -0
  9. package/dist/cli.js.map +1 -1
  10. package/dist/commands/appPaths.d.ts +2 -0
  11. package/dist/commands/appPaths.js +97 -0
  12. package/dist/commands/appPaths.js.map +1 -0
  13. package/dist/commands/capabilitiesAudit.d.ts +2 -0
  14. package/dist/commands/capabilitiesAudit.js +105 -0
  15. package/dist/commands/capabilitiesAudit.js.map +1 -0
  16. package/dist/commands/configInspect.d.ts +2 -0
  17. package/dist/commands/configInspect.js +223 -0
  18. package/dist/commands/configInspect.js.map +1 -0
  19. package/dist/commands/diagnose.d.ts +2 -0
  20. package/dist/commands/diagnose.js +311 -0
  21. package/dist/commands/diagnose.js.map +1 -0
  22. package/dist/commands/forensics.d.ts +2 -0
  23. package/dist/commands/forensics.js +331 -0
  24. package/dist/commands/forensics.js.map +1 -0
  25. package/dist/commands/health.d.ts +2 -0
  26. package/dist/commands/health.js +39 -0
  27. package/dist/commands/health.js.map +1 -0
  28. package/dist/commands/osLogs.d.ts +2 -0
  29. package/dist/commands/osLogs.js +130 -0
  30. package/dist/commands/osLogs.js.map +1 -0
  31. package/dist/commands/processTree.d.ts +2 -0
  32. package/dist/commands/processTree.js +45 -0
  33. package/dist/commands/processTree.js.map +1 -0
  34. package/dist/commands/sidecarReplay.d.ts +7 -0
  35. package/dist/commands/sidecarReplay.js +93 -0
  36. package/dist/commands/sidecarReplay.js.map +1 -0
  37. package/dist/commands/sidecarTap.d.ts +2 -0
  38. package/dist/commands/sidecarTap.js +118 -0
  39. package/dist/commands/sidecarTap.js.map +1 -0
  40. package/dist/commands/webviewAttach.d.ts +2 -0
  41. package/dist/commands/webviewAttach.js +64 -0
  42. package/dist/commands/webviewAttach.js.map +1 -0
  43. package/dist/platform/oslog/darwin.d.ts +21 -0
  44. package/dist/platform/oslog/darwin.js +72 -0
  45. package/dist/platform/oslog/darwin.js.map +1 -0
  46. package/dist/platform/oslog/linux.d.ts +16 -0
  47. package/dist/platform/oslog/linux.js +47 -0
  48. package/dist/platform/oslog/linux.js.map +1 -0
  49. package/dist/platform/oslog/windows.d.ts +15 -0
  50. package/dist/platform/oslog/windows.js +16 -0
  51. package/dist/platform/oslog/windows.js.map +1 -0
  52. package/dist/schemas/bridge.d.ts +222 -0
  53. package/dist/schemas/bridge.js +44 -0
  54. package/dist/schemas/bridge.js.map +1 -1
  55. package/dist/schemas/osLog.d.ts +34 -0
  56. package/dist/schemas/osLog.js +18 -0
  57. package/dist/schemas/osLog.js.map +1 -0
  58. package/dist/schemas/sidecar.d.ts +33 -0
  59. package/dist/schemas/sidecar.js +17 -0
  60. package/dist/schemas/sidecar.js.map +1 -0
  61. package/dist/schemas/tauriConfig.d.ts +825 -0
  62. package/dist/schemas/tauriConfig.js +102 -0
  63. package/dist/schemas/tauriConfig.js.map +1 -0
  64. package/dist/util/ndjson.d.ts +37 -0
  65. package/dist/util/ndjson.js +82 -0
  66. package/dist/util/ndjson.js.map +1 -0
  67. package/dist/util/tauriConfig.d.ts +63 -0
  68. package/dist/util/tauriConfig.js +235 -0
  69. package/dist/util/tauriConfig.js.map +1 -0
  70. package/examples/frontend-stub/index.html +1 -0
  71. package/examples/tauri-bridge/Cargo.toml +6 -0
  72. package/examples/tauri-bridge/build.rs +3 -0
  73. package/examples/tauri-bridge/icons/icon.png +0 -0
  74. package/examples/tauri-bridge/src/dev_bridge.rs +424 -11
  75. package/examples/tauri-bridge/tauri.conf.json +25 -0
  76. package/package.json +3 -1
@@ -2,15 +2,20 @@ use rand::Rng;
2
2
  use serde::{Deserialize, Serialize};
3
3
  use std::collections::{HashMap, VecDeque};
4
4
  use std::fs;
5
- use std::io::{BufRead, BufReader, Write};
5
+ 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
10
  use tauri::{AppHandle, Manager};
10
11
  use tiny_http::{Header, Response, Server};
11
12
  use tracing_subscriber::layer::SubscriberExt;
12
13
  use tracing_subscriber::util::SubscriberInitExt;
13
14
 
15
+ /// Bridge protocol version exposed via `GET /version`. Bumped whenever the
16
+ /// HTTP surface changes shape so CLI clients can feature-detect.
17
+ pub const BRIDGE_VERSION: &str = "0.7.0";
18
+
14
19
  #[derive(Deserialize)]
15
20
  struct EvalRequest {
16
21
  js: String,
@@ -69,6 +74,71 @@ struct VersionResponse {
69
74
  endpoints: Vec<String>,
70
75
  }
71
76
 
77
+ #[derive(Deserialize)]
78
+ struct AuthedRequest {
79
+ token: String,
80
+ }
81
+
82
+ #[derive(Serialize, Clone)]
83
+ struct SidecarSummary {
84
+ name: String,
85
+ pid: u32,
86
+ #[serde(skip_serializing_if = "Option::is_none")]
87
+ exe: Option<String>,
88
+ args: Vec<String>,
89
+ alive: Option<bool>,
90
+ }
91
+
92
+ #[derive(Serialize)]
93
+ struct ProcessResponse {
94
+ tauri: TauriProcessInfo,
95
+ sidecars: Vec<SidecarSummary>,
96
+ }
97
+
98
+ #[derive(Serialize)]
99
+ struct TauriProcessInfo {
100
+ pid: u32,
101
+ #[serde(skip_serializing_if = "Option::is_none")]
102
+ exe: Option<String>,
103
+ args: Vec<String>,
104
+ uptime_ms: u64,
105
+ }
106
+
107
+ #[derive(Serialize, Clone)]
108
+ struct CapabilityEntry {
109
+ identifier: String,
110
+ #[serde(skip_serializing_if = "Option::is_none")]
111
+ description: Option<String>,
112
+ windows: Vec<String>,
113
+ permissions: Vec<String>,
114
+ }
115
+
116
+ #[derive(Serialize)]
117
+ struct CapabilitiesResponse {
118
+ /// Capabilities as declared in tauri.conf.json (best-effort: tauri 2 stores
119
+ /// these as either bare strings or inline objects; we surface both shapes).
120
+ declared: Vec<CapabilityEntry>,
121
+ /// Window labels currently registered with Tauri.
122
+ windows: Vec<String>,
123
+ }
124
+
125
+ #[derive(Serialize)]
126
+ struct DevtoolsResponse {
127
+ platform: String,
128
+ inspectable: bool,
129
+ #[serde(skip_serializing_if = "Option::is_none")]
130
+ url: Option<String>,
131
+ hint: String,
132
+ }
133
+
134
+ #[derive(Serialize)]
135
+ struct HealthResponse {
136
+ uptime_ms: u64,
137
+ webview_ready: bool,
138
+ sidecars_alive: bool,
139
+ sidecars: Vec<SidecarSummary>,
140
+ }
141
+
72
142
  #[derive(Serialize)]
73
143
  struct TokenFile {
74
144
  port: u16,
@@ -76,6 +146,92 @@ struct TokenFile {
76
146
  pid: u32,
77
147
  }
78
148
 
149
+ /// Per-process sidecar metadata captured at spawn time. Used by `/process`
150
+ /// and `/health` so an external diagnostic tool can see the process tree
151
+ /// without scraping `ps`.
152
+ struct SidecarRecord {
153
+ name: String,
154
+ pid: u32,
155
+ exe: Option<String>,
156
+ args: Vec<String>,
157
+ }
158
+
159
+ /// Thread-safe registry of sidecars known to this bridge. Populated by
160
+ /// `spawn_sidecar_monitored` automatically; users with their own spawn flow
161
+ /// can call `register_sidecar` after spawning. Aliveness is computed at
162
+ /// request time via a cheap signal-0 check.
163
+ pub struct SidecarRegistry {
164
+ records: Mutex<Vec<SidecarRecord>>,
165
+ }
166
+
167
+ impl SidecarRegistry {
168
+ pub fn new() -> Self {
169
+ Self {
170
+ records: Mutex::new(Vec::new()),
171
+ }
172
+ }
173
+
174
+ fn add(&self, record: SidecarRecord) {
175
+ let mut recs = self.records.lock().unwrap();
176
+ recs.push(record);
177
+ }
178
+
179
+ fn snapshot(&self) -> Vec<SidecarSummary> {
180
+ let recs = self.records.lock().unwrap();
181
+ recs.iter()
182
+ .map(|r| SidecarSummary {
183
+ name: r.name.clone(),
184
+ pid: r.pid,
185
+ exe: r.exe.clone(),
186
+ args: r.args.clone(),
187
+ alive: pid_alive(r.pid),
188
+ })
189
+ .collect()
190
+ }
191
+ }
192
+
193
+ impl Default for SidecarRegistry {
194
+ fn default() -> Self {
195
+ Self::new()
196
+ }
197
+ }
198
+
199
+ /// Best-effort liveness probe for a sidecar PID. Returns `Some(true)` if the
200
+ /// process is running, `Some(false)` if it has exited, and `None` when we
201
+ /// can't determine (e.g., on Windows where we don't ship a probe in v1).
202
+ #[cfg(unix)]
203
+ fn pid_alive(pid: u32) -> Option<bool> {
204
+ // SAFETY: libc::kill with signal 0 just checks process existence and never
205
+ // delivers a signal. Returns 0 on success, -1 on error (e.g., ESRCH).
206
+ let rc = unsafe { libc::kill(pid as libc::pid_t, 0) };
207
+ Some(rc == 0)
208
+ }
209
+
210
+ #[cfg(not(unix))]
211
+ fn pid_alive(_pid: u32) -> Option<bool> {
212
+ None
213
+ }
214
+
215
+ /// Register a sidecar process with the bridge so it shows up in `/process`
216
+ /// and `/health` responses. Callers that use `spawn_sidecar_monitored` get
217
+ /// this for free; callers who spawn their own children can register them
218
+ /// here. Idempotent in the sense that re-registering a name is allowed
219
+ /// (both entries will be reported).
220
+ pub fn register_sidecar(
221
+ registry: &Arc<SidecarRegistry>,
222
+ name: &str,
223
+ pid: u32,
224
+ exe: Option<String>,
225
+ args: Vec<String>,
226
+ ) {
227
+ registry.add(SidecarRecord {
228
+ name: name.to_string(),
229
+ pid,
230
+ exe,
231
+ args,
232
+ });
233
+ }
234
+
79
235
  /// Ring buffer for log entries. Thread-safe, capped at 1000 entries.
80
236
  pub struct LogBuffer {
81
237
  entries: Mutex<VecDeque<LogEntry>>,
@@ -179,12 +335,15 @@ pub fn create_log_layer(
179
335
 
180
336
  /// Spawn a sidecar process with monitored stdout/stderr.
181
337
  /// Lines from stdout are logged as "info", lines from stderr as "warn".
182
- /// Returns the `std::process::Child` handle.
338
+ /// Returns the `std::process::Child` handle. If a `SidecarRegistry` is
339
+ /// supplied (recommended), the child is also recorded for `/process` and
340
+ /// `/health` responses; pass `None` to opt out of registry tracking.
183
341
  pub fn spawn_sidecar_monitored(
184
342
  name: &str,
185
343
  command: &str,
186
344
  args: &[&str],
187
345
  log_buffer: &Arc<LogBuffer>,
346
+ registry: Option<&Arc<SidecarRegistry>>,
188
347
  ) -> Result<std::process::Child, String> {
189
348
  let mut child = Command::new(command)
190
349
  .args(args)
@@ -193,6 +352,15 @@ pub fn spawn_sidecar_monitored(
193
352
  .spawn()
194
353
  .map_err(|e| format!("Failed to spawn sidecar {name}: {e}"))?;
195
354
 
355
+ if let Some(reg) = registry {
356
+ reg.add(SidecarRecord {
357
+ name: name.to_string(),
358
+ pid: child.id(),
359
+ exe: Some(command.to_string()),
360
+ args: args.iter().map(|s| s.to_string()).collect(),
361
+ });
362
+ }
363
+
196
364
  let source = format!("sidecar:{name}");
197
365
 
198
366
  // Monitor stdout
@@ -263,9 +431,16 @@ pub fn __dev_bridge_result(
263
431
  }
264
432
 
265
433
  /// Start the development bridge HTTP server.
266
- /// Returns the port number and log buffer on success.
267
- /// The log buffer can be used with `spawn_sidecar_monitored()` to capture sidecar output.
268
- pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
434
+ ///
435
+ /// Returns the bound port, a shared log buffer, and a sidecar registry. Both
436
+ /// the buffer and registry are intended to be passed back to
437
+ /// `spawn_sidecar_monitored` for any sidecar processes you launch; the
438
+ /// registry is what powers the `/process` and `/health` endpoints' visibility
439
+ /// into the process tree. Callers that don't spawn sidecars can ignore the
440
+ /// registry handle.
441
+ pub fn start_bridge(
442
+ app: &AppHandle,
443
+ ) -> Result<(u16, Arc<LogBuffer>, Arc<SidecarRegistry>), String> {
269
444
  let server =
270
445
  Server::http("127.0.0.1:0").map_err(|e| format!("Failed to start bridge: {e}"))?;
271
446
  let port = server
@@ -311,27 +486,47 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
311
486
  });
312
487
  app.manage(pending.clone());
313
488
 
489
+ // Sidecar registry — exposed to integrators via the return tuple and
490
+ // consulted by /process and /health.
491
+ let sidecar_registry = Arc::new(SidecarRegistry::new());
492
+ app.manage(sidecar_registry.clone());
493
+
494
+ // Capture process start metadata once so /process and /health don't pay
495
+ // for the lookup on every request.
496
+ let start_instant = Instant::now();
497
+ let tauri_pid = std::process::id();
498
+ let tauri_exe = std::env::current_exe()
499
+ .ok()
500
+ .and_then(|p| p.to_str().map(|s| s.to_string()));
501
+ let tauri_args: Vec<String> = std::env::args().collect();
502
+
314
503
  let app_handle = app.clone();
315
504
  let expected_token = token.clone();
316
505
  let server_log_buffer = log_buffer.clone();
506
+ let server_registry = sidecar_registry.clone();
317
507
 
318
508
  thread::spawn(move || {
319
509
  // Keep _guard alive for the lifetime of the server thread
320
510
  let _cleanup = _guard;
321
511
 
322
- for request in server.incoming_requests() {
512
+ for mut request in server.incoming_requests() {
323
513
  let is_post = request.method().as_str() == "POST";
324
514
  let url = request.url().to_string();
325
515
 
326
- // Handle GET /version (no auth needed)
516
+ // Handle GET /version (no auth needed). Clients feature-detect
517
+ // newer endpoints by checking the `endpoints` array.
327
518
  if url == "/version" && request.method().as_str() == "GET" {
328
519
  let resp = VersionResponse {
329
- version: "0.6.0".to_string(),
520
+ version: BRIDGE_VERSION.to_string(),
330
521
  endpoints: vec![
331
522
  "/eval".to_string(),
332
523
  "/logs".to_string(),
333
524
  "/describe".to_string(),
334
525
  "/version".to_string(),
526
+ "/process".to_string(),
527
+ "/capabilities".to_string(),
528
+ "/devtools".to_string(),
529
+ "/health".to_string(),
335
530
  ],
336
531
  };
337
532
  let json = serde_json::to_string(&resp).unwrap();
@@ -340,7 +535,11 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
340
535
  continue;
341
536
  }
342
537
 
343
- if !is_post || (url != "/eval" && url != "/logs" && url != "/describe") {
538
+ let known_post = matches!(
539
+ url.as_str(),
540
+ "/eval" | "/logs" | "/describe" | "/process" | "/capabilities" | "/devtools" | "/health"
541
+ );
542
+ if !is_post || !known_post {
344
543
  let _ = request.respond(Response::from_string("Not found").with_status_code(404));
345
544
  continue;
346
545
  }
@@ -378,6 +577,113 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
378
577
  continue;
379
578
  }
380
579
 
580
+ // Handle /process endpoint — Tauri PID + sidecar registry snapshot.
581
+ if url == "/process" {
582
+ let req: AuthedRequest = match serde_json::from_str(&body) {
583
+ Ok(r) => r,
584
+ Err(_) => {
585
+ let _ = request
586
+ .respond(Response::from_string("Invalid JSON").with_status_code(400));
587
+ continue;
588
+ }
589
+ };
590
+ if req.token != expected_token {
591
+ let _ = request
592
+ .respond(Response::from_string("Unauthorized").with_status_code(401));
593
+ continue;
594
+ }
595
+ let resp = ProcessResponse {
596
+ tauri: TauriProcessInfo {
597
+ pid: tauri_pid,
598
+ exe: tauri_exe.clone(),
599
+ args: tauri_args.clone(),
600
+ uptime_ms: start_instant.elapsed().as_millis() as u64,
601
+ },
602
+ sidecars: server_registry.snapshot(),
603
+ };
604
+ let json = serde_json::to_string(&resp).unwrap();
605
+ let header = Header::from_bytes("Content-Type", "application/json").unwrap();
606
+ let _ = request.respond(Response::from_string(json).with_header(header));
607
+ continue;
608
+ }
609
+
610
+ // Handle /capabilities endpoint — declared Tauri capability set per window.
611
+ if url == "/capabilities" {
612
+ let req: AuthedRequest = match serde_json::from_str(&body) {
613
+ Ok(r) => r,
614
+ Err(_) => {
615
+ let _ = request
616
+ .respond(Response::from_string("Invalid JSON").with_status_code(400));
617
+ continue;
618
+ }
619
+ };
620
+ if req.token != expected_token {
621
+ let _ = request
622
+ .respond(Response::from_string("Unauthorized").with_status_code(401));
623
+ continue;
624
+ }
625
+
626
+ let windows: Vec<String> = app_handle.webview_windows().keys().cloned().collect();
627
+ let declared = collect_declared_capabilities(&app_handle);
628
+ let resp = CapabilitiesResponse { declared, windows };
629
+ let json = serde_json::to_string(&resp).unwrap();
630
+ let header = Header::from_bytes("Content-Type", "application/json").unwrap();
631
+ let _ = request.respond(Response::from_string(json).with_header(header));
632
+ continue;
633
+ }
634
+
635
+ // Handle /devtools endpoint — inspector URL or platform hint.
636
+ if url == "/devtools" {
637
+ let req: AuthedRequest = match serde_json::from_str(&body) {
638
+ Ok(r) => r,
639
+ Err(_) => {
640
+ let _ = request
641
+ .respond(Response::from_string("Invalid JSON").with_status_code(400));
642
+ continue;
643
+ }
644
+ };
645
+ if req.token != expected_token {
646
+ let _ = request
647
+ .respond(Response::from_string("Unauthorized").with_status_code(401));
648
+ continue;
649
+ }
650
+ let resp = devtools_response();
651
+ let json = serde_json::to_string(&resp).unwrap();
652
+ let header = Header::from_bytes("Content-Type", "application/json").unwrap();
653
+ let _ = request.respond(Response::from_string(json).with_header(header));
654
+ continue;
655
+ }
656
+
657
+ // Handle /health endpoint — quick "is this app sick" check.
658
+ if url == "/health" {
659
+ let req: AuthedRequest = match serde_json::from_str(&body) {
660
+ Ok(r) => r,
661
+ Err(_) => {
662
+ let _ = request
663
+ .respond(Response::from_string("Invalid JSON").with_status_code(400));
664
+ continue;
665
+ }
666
+ };
667
+ if req.token != expected_token {
668
+ let _ = request
669
+ .respond(Response::from_string("Unauthorized").with_status_code(401));
670
+ continue;
671
+ }
672
+ let sidecars = server_registry.snapshot();
673
+ let sidecars_alive = sidecars.iter().all(|s| matches!(s.alive, Some(true) | None));
674
+ let webview_ready = !app_handle.webview_windows().is_empty();
675
+ let resp = HealthResponse {
676
+ uptime_ms: start_instant.elapsed().as_millis() as u64,
677
+ webview_ready,
678
+ sidecars_alive,
679
+ sidecars,
680
+ };
681
+ let json = serde_json::to_string(&resp).unwrap();
682
+ let header = Header::from_bytes("Content-Type", "application/json").unwrap();
683
+ let _ = request.respond(Response::from_string(json).with_header(header));
684
+ continue;
685
+ }
686
+
381
687
  // Handle /describe endpoint
382
688
  if url == "/describe" {
383
689
  let desc_req: DescribeRequest = match serde_json::from_str(&body) {
@@ -522,8 +828,115 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
522
828
  }
523
829
  });
524
830
 
525
- eprintln!("Dev bridge started on port {port}");
831
+ eprintln!("Dev bridge {BRIDGE_VERSION} started on port {port}");
526
832
  eprintln!("Token file: {token_path}");
527
833
 
528
- Ok((port, log_buffer))
834
+ Ok((port, log_buffer, sidecar_registry))
835
+ }
836
+
837
+ /// Read declared capabilities from tauri.conf.json via `app.config()`. Returns
838
+ /// a flat list of capability entries. Tauri 2 lets capabilities be either bare
839
+ /// permission identifiers (strings) or full inline definitions; we surface
840
+ /// both as `CapabilityEntry` rows with `permissions` populated where possible.
841
+ fn collect_declared_capabilities(app: &AppHandle) -> Vec<CapabilityEntry> {
842
+ let config = app.config();
843
+ let security = &config.app.security;
844
+ let mut out = Vec::new();
845
+ for cap in &security.capabilities {
846
+ let raw = serde_json::to_value(cap).unwrap_or(serde_json::Value::Null);
847
+ match &raw {
848
+ serde_json::Value::String(s) => {
849
+ // Capability declared by reference to a JSON file. We don't have
850
+ // the resolved contents at runtime here, but we surface the
851
+ // identifier so callers know what was requested.
852
+ out.push(CapabilityEntry {
853
+ identifier: s.clone(),
854
+ description: None,
855
+ windows: Vec::new(),
856
+ permissions: Vec::new(),
857
+ });
858
+ }
859
+ serde_json::Value::Object(map) => {
860
+ let identifier = map
861
+ .get("identifier")
862
+ .and_then(|v| v.as_str())
863
+ .unwrap_or("<inline>")
864
+ .to_string();
865
+ let description = map
866
+ .get("description")
867
+ .and_then(|v| v.as_str())
868
+ .map(|s| s.to_string());
869
+ let windows: Vec<String> = map
870
+ .get("windows")
871
+ .and_then(|v| v.as_array())
872
+ .map(|arr| {
873
+ arr.iter()
874
+ .filter_map(|v| v.as_str().map(|s| s.to_string()))
875
+ .collect()
876
+ })
877
+ .unwrap_or_default();
878
+ let permissions: Vec<String> = map
879
+ .get("permissions")
880
+ .and_then(|v| v.as_array())
881
+ .map(|arr| {
882
+ arr.iter()
883
+ .filter_map(|v| match v {
884
+ serde_json::Value::String(s) => Some(s.clone()),
885
+ serde_json::Value::Object(o) => o
886
+ .get("identifier")
887
+ .and_then(|i| i.as_str())
888
+ .map(|s| s.to_string()),
889
+ _ => None,
890
+ })
891
+ .collect()
892
+ })
893
+ .unwrap_or_default();
894
+ out.push(CapabilityEntry {
895
+ identifier,
896
+ description,
897
+ windows,
898
+ permissions,
899
+ });
900
+ }
901
+ _ => {}
902
+ }
903
+ }
904
+ out
905
+ }
906
+
907
+ /// Build a `/devtools` response for the current platform. v1 emits useful
908
+ /// hints rather than always producing a hot inspector URL — Safari attach on
909
+ /// macOS requires UI activation, Windows WebView2 needs a launch-time arg.
910
+ fn devtools_response() -> DevtoolsResponse {
911
+ if cfg!(target_os = "macos") {
912
+ DevtoolsResponse {
913
+ platform: "wkwebview".to_string(),
914
+ inspectable: cfg!(debug_assertions),
915
+ url: None,
916
+ hint: "Open Safari > Develop > <Mac name> > <App name> to attach. \
917
+ Requires the app to be built with debug_assertions (i.e., `tauri dev`)."
918
+ .to_string(),
919
+ }
920
+ } else if cfg!(target_os = "windows") {
921
+ let port = std::env::var("WEBVIEW2_REMOTE_DEBUGGING_PORT").ok();
922
+ let url = port.as_ref().map(|p| format!("http://127.0.0.1:{p}"));
923
+ DevtoolsResponse {
924
+ platform: "webview2".to_string(),
925
+ inspectable: url.is_some(),
926
+ url,
927
+ hint: "Set WEBVIEW2_REMOTE_DEBUGGING_PORT=9222 before launching, \
928
+ then open http://127.0.0.1:9222 in Chrome/Edge to inspect."
929
+ .to_string(),
930
+ }
931
+ } else {
932
+ let inspector = std::env::var("WEBKIT_INSPECTOR_SERVER").ok();
933
+ DevtoolsResponse {
934
+ platform: "webkitgtk".to_string(),
935
+ inspectable: inspector.is_some(),
936
+ url: inspector.as_ref().map(|s| format!("http://{s}")),
937
+ hint: "Export WEBKIT_INSPECTOR_SERVER=127.0.0.1:9222 before launching, \
938
+ then open http://127.0.0.1:9222 to inspect."
939
+ .to_string(),
940
+ }
941
+ }
529
942
  }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://schema.tauri.app/config/2",
3
+ "productName": "tauri-dev-bridge-example",
4
+ "version": "0.1.0",
5
+ "identifier": "com.tauri-agent-tools.bridge-example",
6
+ "build": {
7
+ "frontendDist": "../frontend-stub"
8
+ },
9
+ "app": {
10
+ "windows": [
11
+ {
12
+ "label": "main",
13
+ "title": "Bridge Example",
14
+ "width": 800,
15
+ "height": 600
16
+ }
17
+ ],
18
+ "security": {
19
+ "csp": null
20
+ }
21
+ },
22
+ "bundle": {
23
+ "active": false
24
+ }
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tauri-agent-tools",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Agent-driven inspection toolkit for Tauri desktop apps",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,6 +44,8 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
+ "ajv": "^8.20.0",
48
+ "ajv-formats": "^3.0.1",
47
49
  "commander": "^14.0.0",
48
50
  "zod": "^3.25.76"
49
51
  },