app
Chip Fetcher (cf)
Public Made by Adomby adom
Crawls the web on your behalf to gather everything about the chips you care about: datasheets, symbols, footprints, 3D models, stock and pricing, straight from the manufacturer first, into your EDA tool. Say "cf" to your AI.
← Commit history
sync latest source from canonical working tree (rewired Send-to-KiCad)
1 file changed
+103−9
src/deliver.rs+103−9@@ -9,7 +9,16 @@ use std::path::Path; use std::process::Command; fn ad(verb: &str, payload: Value) -> Result<Value, String> {- let out = Command::new("adom-desktop")+ let mut cmd = Command::new("adom-desktop");+ // When the relay has >1 client (e.g. laptop + a cloud VM), a bare verb is+ // ambiguous. Honor ADOM_DESKTOP_TARGET to pin the host; single-client setups+ // need nothing.+ if let Ok(t) = std::env::var("ADOM_DESKTOP_TARGET") {+ if !t.is_empty() {+ cmd.args(["--target", &t]);+ }+ }+ let out = cmd .args([verb, &payload.to_string()]) .output() .map_err(|e| format!("adom-desktop unreachable: {e}"))?;@@ -64,16 +73,29 @@ pub fn deliver(mpn: &str, dir: &Path, target: &str) -> Value { } if target == "kicad" {- let mut steps = Vec::new();- if sym.is_file() {- steps.push(ad("send_files", json!({ "filePaths": [sym.to_string_lossy()], "targetApp": "kicad", "destinationFolder": "symbols" })));- steps.push(ad("kicad_install_library", json!({ "libraryPath": sym.to_string_lossy(), "libraryName": mpn, "libraryType": "symbol" })));+ // Real bridge verbs (verified), not the old send_files theater: the symbol+ // installs into the Adom library AND opens on the Symbol Editor canvas; the+ // footprint installs into the Adom footprint library (deterministic).+ let mut results: Vec<(&str, Result<Value, String>)> = Vec::new();+ if let Ok(content) = std::fs::read(&sym) {+ results.push(("symbol", ad("kicad_install_symbol", json!({+ "fileName": format!("{mpn}.kicad_sym"),+ "fileContent": b64(&content),+ "symbolName": mpn,+ "openEditor": true,+ "timeoutSeconds": 60,+ })))); }- if md.is_file() {- steps.push(ad("send_files", json!({ "filePaths": [md.to_string_lossy()], "targetApp": "kicad", "destinationFolder": "footprints" })));- steps.push(ad("kicad_install_library", json!({ "libraryPath": md.to_string_lossy(), "libraryName": mpn, "libraryType": "footprint" })));+ if let Ok(content) = std::fs::read(&md) {+ results.push(("footprint", ad("kicad_install_footprint", json!({+ "fileName": format!("{mpn}.kicad_mod"),+ "fileContent": b64(&content),+ "footprintName": mpn,+ "openEditor": false,+ "timeoutSeconds": 60,+ })))); }- return summarize("KiCad", "symbol + footprint", &steps);+ return summarize_kicad_chip(mpn, dir, results); } let (tool, app, folder) = match target {@@ -124,3 +146,75 @@ fn summarize(tool: &str, what: &str, results: &[Result<Value, String>]) -> Value } json!({ "ok": true, "target": tool, "message": format!("Delivered {what} to {tool}.") }) }++/// Standard base64 (no external crate) — for sending file content through the bridge.+fn b64(data: &[u8]) -> String {+ const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";+ let mut out = String::with_capacity((data.len() + 2) / 3 * 4);+ for chunk in data.chunks(3) {+ let b0 = chunk[0];+ let b1 = *chunk.get(1).unwrap_or(&0);+ let b2 = *chunk.get(2).unwrap_or(&0);+ out.push(T[(b0 >> 2) as usize] as char);+ out.push(T[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);+ out.push(if chunk.len() > 1 { T[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char } else { '=' });+ out.push(if chunk.len() > 2 { T[(b2 & 0x3f) as usize] as char } else { '=' });+ }+ out+}++/// True if a single bridge response confirms real success (the bridge's `success`+/// flag, or its confirmation text — handles AD wrapping the result in `output`).+fn bridge_ok(r: &Result<Value, String>) -> bool {+ match r {+ Err(_) => false,+ Ok(v) => {+ let s = v.to_string().to_lowercase();+ v.get("success").and_then(|b| b.as_bool()) == Some(true)+ || s.contains("loaded in the symbol editor")+ || (s.contains("added to") && s.contains("library"))+ || (s.contains("installed") && s.contains("library"))+ }+ }+}++/// A descriptive paste-into-Claude prompt — always offered so the user can finish+/// the delivery in their own AI thread if anything is off.+fn claude_prompt_chip(mpn: &str, dir: &Path) -> String {+ format!(+ "I fetched the KiCad CAD for chip \"{mpn}\" (files in `{}`: {mpn}.kicad_sym and {mpn}.kicad_mod). Please \+ install them into my KiCad \"Adom\" symbol + footprint libraries and open the symbol in the KiCad Symbol \+ Editor so I can see it — use the adom-desktop KiCad bridge (`kicad_install_symbol` with openEditor:true, \+ and `kicad_install_footprint`), then confirm they actually landed.",+ dir.display()+ )+}++/// Summarize the chip's KiCad delivery across symbol + footprint. Verifies REAL+/// per-artifact success and always includes a paste-into-Claude prompt fallback.+fn summarize_kicad_chip(mpn: &str, dir: &Path, results: Vec<(&str, Result<Value, String>)>) -> Value {+ let prompt = claude_prompt_chip(mpn, dir);+ // Offline / unreachable short-circuit (clear, actionable).+ for (_what, r) in &results {+ let offline = match r {+ Err(e) => e.contains("unreachable"),+ Ok(v) => looks_offline(v),+ };+ if offline {+ return json!({ "ok": false, "target": "KiCad", "offline": true, "claudePrompt": prompt,+ "message": "Adom Desktop isn't connected. Open it on your laptop and retry — or paste the prompt into Claude." });+ }+ }+ let delivered: Vec<&str> = results.iter().filter(|(_, r)| bridge_ok(r)).map(|(w, _)| *w).collect();+ let failed: Vec<&str> = results.iter().filter(|(_, r)| !bridge_ok(r)).map(|(w, _)| *w).collect();+ if failed.is_empty() && !delivered.is_empty() {+ json!({ "ok": true, "target": "KiCad", "claudePrompt": prompt, "delivered": delivered,+ "message": format!("Delivered {} for {mpn} to KiCad (symbol opened in the Symbol Editor).", delivered.join(" + ")) })+ } else {+ json!({ "ok": false, "target": "KiCad", "claudePrompt": prompt,+ "delivered": delivered, "failed": failed,+ "message": format!("KiCad delivery didn't fully confirm (ok: {}; unconfirmed: {}). Paste the prompt into Claude to finish it.",+ if delivered.is_empty() { "none".into() } else { delivered.join("+") },+ if failed.is_empty() { "none".into() } else { failed.join("+") }) })+ }+}