lean_ctx/proxy_setup/
claude.rs1use std::path::Path;
4
5use super::util::{is_local_lean_ctx_url, is_proxy_reachable};
6
7pub fn anthropic_api_key_available(home: &Path) -> bool {
18 for var in ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"] {
20 if std::env::var(var).is_ok_and(|v| !v.trim().is_empty()) {
21 return true;
22 }
23 }
24
25 let settings_path = crate::core::editor_registry::claude_state_dir(home).join("settings.json");
28 let Ok(content) = std::fs::read_to_string(&settings_path) else {
29 return false;
30 };
31 let Ok(doc) = crate::core::jsonc::parse_jsonc(&content) else {
32 return false;
33 };
34
35 if doc
36 .get("apiKeyHelper")
37 .and_then(|v| v.as_str())
38 .is_some_and(|v| !v.trim().is_empty())
39 {
40 return true;
41 }
42
43 let env = doc.get("env");
44 ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"]
45 .iter()
46 .any(|key| {
47 env.and_then(|e| e.get(*key))
48 .and_then(|v| v.as_str())
49 .is_some_and(|v| !v.trim().is_empty())
50 })
51}
52
53fn warn_claude_subscription_skip() {
56 eprintln!(" \u{26a0} Claude Code: no ANTHROPIC_API_KEY detected (Pro/Max subscription?).");
57 eprintln!(" The proxy forwards your credential upstream but never injects one, and a");
58 eprintln!(" subscription token only authenticates against api.anthropic.com directly.");
59 eprintln!(" Leaving ANTHROPIC_BASE_URL untouched so Claude Code keeps working.");
60 eprintln!(" Savings on a subscription: use the lean-ctx MCP tools (ctx_read /");
61 eprintln!(" ctx_search / ctx_shell). Pay-as-you-go? Set ANTHROPIC_API_KEY, then run:");
62 eprintln!(" lean-ctx proxy enable");
63}
64
65pub(crate) fn uninstall_claude_env(home: &Path, quiet: bool) {
66 use crate::core::config::Config;
67
68 let settings_dir = crate::core::editor_registry::claude_state_dir(home);
69 let settings_path = settings_dir.join("settings.json");
70 let existing = match std::fs::read_to_string(&settings_path) {
71 Ok(s) if !s.trim().is_empty() => s,
72 _ => return,
73 };
74 let mut doc: serde_json::Value = match crate::core::jsonc::parse_jsonc(&existing) {
75 Ok(v) => v,
76 Err(_) => return,
77 };
78
79 let Some(env_obj) = doc.get_mut("env").and_then(|e| e.as_object_mut()) else {
80 return;
81 };
82
83 if !env_obj.contains_key("ANTHROPIC_BASE_URL") {
84 return;
85 }
86
87 let cfg = Config::load();
88 if let Some(ref upstream) = cfg.proxy.anthropic_upstream {
89 env_obj.insert(
90 "ANTHROPIC_BASE_URL".to_string(),
91 serde_json::Value::String(upstream.clone()),
92 );
93 if !quiet {
94 println!(" ✓ Restored ANTHROPIC_BASE_URL → {upstream} in Claude Code settings");
95 }
96 } else {
97 env_obj.remove("ANTHROPIC_BASE_URL");
98 if env_obj.is_empty() {
99 doc.as_object_mut().map(|o| o.remove("env"));
100 }
101 if !quiet {
102 println!(" ✓ Removed ANTHROPIC_BASE_URL from Claude Code settings");
103 }
104 }
105
106 let content = serde_json::to_string_pretty(&doc).unwrap_or_default();
107 let _ = std::fs::write(&settings_path, content + "\n");
108}
109
110pub(crate) fn install_claude_env(home: &Path, port: u16, quiet: bool) {
111 install_claude_env_inner(home, port, quiet, false);
112}
113
114pub(crate) fn install_claude_env_inner(home: &Path, port: u16, quiet: bool, force: bool) {
115 use crate::core::config::{Config, is_local_proxy_url, normalize_url_opt};
116
117 let base = format!("http://127.0.0.1:{port}");
118
119 let settings_dir = crate::core::editor_registry::claude_state_dir(home);
120 let settings_path = settings_dir.join("settings.json");
121 let existing = std::fs::read_to_string(&settings_path).unwrap_or_default();
122 let mut doc: serde_json::Value = if existing.trim().is_empty() {
123 serde_json::json!({})
124 } else {
125 match crate::core::jsonc::parse_jsonc(&existing) {
126 Ok(v) => v,
127 Err(_) => return,
128 }
129 };
130
131 let current_url = doc
132 .get("env")
133 .and_then(|e| e.get("ANTHROPIC_BASE_URL"))
134 .and_then(|v| v.as_str())
135 .unwrap_or("")
136 .to_string();
137
138 if !force && !anthropic_api_key_available(home) {
144 if is_local_lean_ctx_url(¤t_url) {
146 let cfg = Config::load();
147 if let Some(env_obj) = doc.get_mut("env").and_then(|e| e.as_object_mut()) {
148 if let Some(ref upstream) = cfg.proxy.anthropic_upstream {
149 env_obj.insert(
150 "ANTHROPIC_BASE_URL".to_string(),
151 serde_json::Value::String(upstream.clone()),
152 );
153 } else {
154 env_obj.remove("ANTHROPIC_BASE_URL");
155 if env_obj.is_empty() {
156 doc.as_object_mut().map(|o| o.remove("env"));
157 }
158 }
159 let out = serde_json::to_string_pretty(&doc).unwrap_or_default();
160 let _ = std::fs::write(&settings_path, out + "\n");
161 }
162 }
163 if !quiet {
164 warn_claude_subscription_skip();
165 }
166 return;
167 }
168
169 if current_url == base {
170 if !quiet {
171 println!(" Claude Code proxy env already configured");
172 }
173 return;
174 }
175
176 if let Some(upstream) = normalize_url_opt(¤t_url)
178 && !is_local_proxy_url(&upstream)
179 {
180 if Config::load_global().proxy.anthropic_upstream.is_none()
181 && let Err(e) =
182 Config::update_global(|c| c.proxy.anthropic_upstream = Some(upstream.clone()))
183 {
184 tracing::warn!("could not persist proxy upstream: {e}");
185 }
186
187 if !force {
188 if !quiet {
189 eprintln!(" \u{26a0} Custom endpoint detected: {upstream}");
190 eprintln!(
191 " Skipping proxy URL write. Use `lean-ctx proxy enable --force` to override."
192 );
193 }
194 return;
195 }
196 if !quiet {
197 println!(" Overriding custom endpoint (--force): {upstream}");
198 }
199 }
200
201 if !is_proxy_reachable(port) {
202 if !quiet {
203 println!(" Skipping Claude Code proxy env (proxy not running on port {port})");
204 }
205 return;
206 }
207
208 if let Some(env_obj) = doc.as_object_mut().and_then(|o| {
209 o.entry("env")
210 .or_insert(serde_json::json!({}))
211 .as_object_mut()
212 }) {
213 env_obj.insert(
214 "ANTHROPIC_BASE_URL".to_string(),
215 serde_json::Value::String(base),
216 );
217 }
218
219 let _ = std::fs::create_dir_all(&settings_dir);
220 let content = serde_json::to_string_pretty(&doc).unwrap_or_default();
221 let _ = std::fs::write(&settings_path, content + "\n");
222 if !quiet {
223 println!(" Configured ANTHROPIC_BASE_URL in Claude Code settings");
224 }
225}