Skip to content

Commit 71de67c

Browse files
fix: harden initialized config and REPL status
1 parent 21edf09 commit 71de67c

5 files changed

Lines changed: 122 additions & 45 deletions

File tree

‎src/claude_credentials.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,8 @@ pub struct CredentialStatus {
641641
pub has_inference_scope: bool,
642642
/// Recorded subscription type, when present.
643643
pub subscription_type: Option<String>,
644+
/// Recorded rate-limit tier, when present.
645+
pub rate_limit_tier: Option<String>,
644646
}
645647

646648
/// Inspect the shared Claude credential store without refreshing tokens.
@@ -676,6 +678,7 @@ fn status_from_oauth(oauth: &ClaudeAiOauth, now_ms: i64) -> CredentialStatus {
676678
expires_soon: now_ms < oauth.expires_at && now_ms + REFRESH_BUFFER_MS >= oauth.expires_at,
677679
has_inference_scope: oauth.scopes.iter().any(|scope| scope == "user:inference"),
678680
subscription_type: oauth.subscription_type.clone(),
681+
rate_limit_tier: oauth.rate_limit_tier.clone(),
679682
}
680683
}
681684

@@ -1128,6 +1131,7 @@ mod tests {
11281131
assert!(!far.expires_soon);
11291132
assert!(far.has_inference_scope);
11301133
assert_eq!(far.subscription_type.as_deref(), Some("pro"));
1134+
assert_eq!(far.rate_limit_tier, None);
11311135

11321136
let expired = ClaudeAiOauth {
11331137
expires_at: 100,

‎src/cli/commands/init.rs‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,12 @@ providers:
129129
opencode:
130130
base_url: https://opencode.ai/zen/go/v1
131131
# api_key: ${OPENCODE_API_KEY} # or ${OPENCODE_GO_API_KEY}
132-
# Generic remote OpenAI-compatible endpoint
133-
openai-compatible:
134-
base_url: https://api.example.com/v1
135-
# api_key: ${OPENAI_COMPATIBLE_API_KEY}
136-
# model: my-model-name
132+
# Generic remote OpenAI-compatible endpoint. Uncomment and replace the
133+
# placeholder before selecting `openai-compatible` as the proxy target.
134+
# openai-compatible:
135+
# base_url: https://your-provider.example/v1
136+
# api_key: ${OPENAI_COMPATIBLE_API_KEY}
137+
# model: my-model-name
137138
# Ollama for local LLM inference
138139
ollama:
139140
base_url: http://localhost:11434

‎src/cli/repl/slash.rs‎

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,46 +1353,37 @@ pub fn slash_context(messages: &[serde_json::Value], current_model: &str) -> Sla
13531353
}
13541354

13551355
pub fn slash_login() -> SlashCommandResult {
1356-
if openclaudia::claude_credentials::has_claude_code_credentials() {
1357-
println!("\n✓ Authenticated via Claude Code credentials.");
1358-
println!(" File: ~/.claude/.credentials.json");
1359-
// crosslink #932: previously this used `Handle::current()` which
1360-
// panics when called outside a runtime. `try_current` mirrors the
1361-
// pattern used by `slash_model` above. When called from a worker
1362-
// thread that is itself driving the runtime, `block_on` would also
1363-
// panic — degrade gracefully to "details unavailable" rather than
1364-
// crashing the REPL.
1365-
match tokio::runtime::Handle::try_current() {
1366-
Ok(handle) => {
1367-
let load_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1368-
handle.block_on(openclaudia::claude_credentials::load_credentials())
1369-
}));
1370-
if let Ok(Ok(creds)) = load_result {
1371-
println!(
1372-
" Type: {}",
1373-
creds.subscription_type.as_deref().unwrap_or("unknown")
1374-
);
1375-
println!(
1376-
" Tier: {}",
1377-
creds.rate_limit_tier.as_deref().unwrap_or("default")
1378-
);
1379-
} else {
1380-
println!(
1381-
" (credential details unavailable — cannot block on current runtime)"
1382-
);
1383-
}
1384-
}
1385-
Err(_) => {
1386-
println!(" (credential details unavailable — no tokio runtime active)");
1356+
match openclaudia::claude_credentials::peek_credentials() {
1357+
Ok(Some(status)) => {
1358+
println!("\n✓ Authenticated via Claude Code credentials.");
1359+
println!(" File: ~/.claude/.credentials.json");
1360+
println!(
1361+
" Type: {}",
1362+
status.subscription_type.as_deref().unwrap_or("unknown")
1363+
);
1364+
println!(
1365+
" Tier: {}",
1366+
status.rate_limit_tier.as_deref().unwrap_or("default")
1367+
);
1368+
if status.expired {
1369+
println!(" Status: expired (auto-refreshes on next use)");
1370+
} else if status.expires_soon {
1371+
println!(" Status: valid, expiring soon");
1372+
} else {
1373+
println!(" Status: valid");
13871374
}
13881375
}
1389-
} else {
1390-
println!("\n✗ Not authenticated via Claude Code.");
1391-
println!(" To log in:");
1392-
println!(" 1. Install Claude Code: npm install -g @anthropic-ai/claude-code");
1393-
println!(" 2. Run: claude");
1394-
println!(" 3. Complete the login flow");
1395-
println!(" 4. Restart OpenClaudia");
1376+
Ok(None) => {
1377+
println!("\n✗ Not authenticated via Claude Code.");
1378+
println!(" To log in:");
1379+
println!(" 1. Install Claude Code: npm install -g @anthropic-ai/claude-code");
1380+
println!(" 2. Run: claude");
1381+
println!(" 3. Complete the login flow");
1382+
println!(" 4. Restart OpenClaudia");
1383+
}
1384+
Err(error) => {
1385+
println!("\n✗ Could not read Claude Code credentials: {error}");
1386+
}
13961387
}
13971388
println!();
13981389
SlashCommandResult::Handled

‎src/slash_commands.rs‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,12 @@ const CORE: &[SlashCommand] = &[
113113
const MEMORY: &[SlashCommand] = &[
114114
cmd("/memory", "Show auto-learning stats"),
115115
cmd("/memory patterns", "Show learned coding patterns"),
116-
cmd("/memory errors", "Show known error patterns"),
116+
cmd("/memory errors <path>", "Show known errors for a file"),
117117
cmd("/memory prefs", "Show learned preferences"),
118-
cmd("/memory files", "Show file co-edit relationships"),
118+
cmd(
119+
"/memory files <path>",
120+
"Show co-edit relationships for a file",
121+
),
119122
cmd(
120123
"/memory reset",
121124
"Reset all learned data (with confirmation)",
@@ -442,6 +445,23 @@ mod tests {
442445
}
443446
}
444447

448+
#[test]
449+
fn memory_file_queries_advertise_required_paths() {
450+
let invocations: Vec<&str> = all_commands().map(|c| c.invocation).collect();
451+
for invocation in ["/memory errors <path>", "/memory files <path>"] {
452+
assert!(
453+
invocations.contains(&invocation),
454+
"memory query must advertise its required path: {invocation}"
455+
);
456+
}
457+
for stale in ["/memory errors", "/memory files"] {
458+
assert!(
459+
!invocations.contains(&stale),
460+
"memory query must not advertise an argument-free form: {stale}"
461+
);
462+
}
463+
}
464+
445465
#[test]
446466
fn tui_table_excludes_legacy_repl_only_commands() {
447467
let invocations: Vec<&str> = all_tui_commands().map(|c| c.invocation).collect();

‎tests/cli_exit_status_e2e.rs‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,56 @@ fn run_auth_with_stdin(input: &str) -> Output {
180180
child.wait_with_output().expect("openclaudia auth must run")
181181
}
182182

183+
#[test]
184+
fn legacy_repl_login_reads_status_without_nesting_a_runtime() {
185+
let cwd = tempfile::tempdir().expect("cwd tempdir");
186+
let home = tempfile::tempdir().expect("home tempdir");
187+
write_local_provider_config(&cwd);
188+
write_claude_oauth_credentials(&home.path().join(".claude"));
189+
190+
let mut child = isolated_command(&cwd, &home)
191+
.args([
192+
"--tui-mode",
193+
"--dangerously-skip-permissions",
194+
"--target",
195+
"local",
196+
"--model",
197+
"local-test-model",
198+
])
199+
.stdin(Stdio::piped())
200+
.stdout(Stdio::piped())
201+
.stderr(Stdio::piped())
202+
.spawn()
203+
.expect("legacy REPL must spawn");
204+
child
205+
.stdin
206+
.take()
207+
.expect("legacy REPL stdin")
208+
.write_all(b"/login\n/exit\n")
209+
.expect("legacy REPL should accept slash commands");
210+
211+
let output = child.wait_with_output().expect("legacy REPL must exit");
212+
let combined = format!(
213+
"{}{}",
214+
String::from_utf8_lossy(&output.stdout),
215+
String::from_utf8_lossy(&output.stderr)
216+
);
217+
assert!(
218+
output.status.success(),
219+
"legacy REPL should exit cleanly after /login; got {combined:?}"
220+
);
221+
assert!(
222+
combined.contains("Authenticated via Claude Code credentials")
223+
&& combined.contains("Type: max")
224+
&& combined.contains("Tier: max"),
225+
"/login should report synchronous credential status; got {combined:?}"
226+
);
227+
assert!(
228+
!combined.contains("panicked") && !combined.contains("Cannot start a runtime"),
229+
"/login must not attempt to nest a Tokio runtime; got {combined:?}"
230+
);
231+
}
232+
183233
fn readme_cli_command_invocations() -> Vec<String> {
184234
let readme = include_str!("../README.md");
185235
let section = readme
@@ -2707,6 +2757,17 @@ fn init_refuses_overwrite_unless_force_and_creates_documented_tree() {
27072757
config.contains("OpenClaudia Configuration") && !config.contains("sentinel"),
27082758
"init --force must replace the previous config contents"
27092759
);
2760+
2761+
let output = isolated_command(&cwd, &home)
2762+
.arg("config")
2763+
.output()
2764+
.expect("openclaudia config must run after init");
2765+
assert!(
2766+
output.status.success(),
2767+
"the generated config must load without edits; stdout={:?} stderr={:?}",
2768+
String::from_utf8_lossy(&output.stdout),
2769+
String::from_utf8_lossy(&output.stderr)
2770+
);
27102771
}
27112772

27122773
#[cfg(unix)]

0 commit comments

Comments
 (0)