1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
use crate::error::SofosError;
use clap::Parser;
/// Default for the deprecated `--thinking-budget` flag. Kept as a named
/// const so `main.rs` can warn when the user supplies a value that
/// differs from this — anything else means the user expected the flag
/// to do something it no longer does. Removable when the flag itself
/// goes.
pub const THINKING_BUDGET_DEFAULT: u32 = 5120;
#[derive(Parser, Debug)]
#[command(
name = "sofos",
about = "An interactive AI coding assistant powered by Claude or OpenAI",
long_about = "Sofos is an AI-powered coding assistant (Claude / OpenAI) that can help you write code, edit files, and search the web.",
version
)]
pub struct Cli {
#[arg(long, env = "ANTHROPIC_API_KEY")]
pub api_key: Option<String>,
#[arg(long, env = "OPENAI_API_KEY")]
pub openai_api_key: Option<String>,
#[arg(long, env = "MORPH_API_KEY")]
pub morph_api_key: Option<String>,
/// Initial prompt to send (if not provided, starts interactive REPL)
#[arg(short, long)]
pub prompt: Option<String>,
/// Resume a previous conversation session
#[arg(short, long)]
pub resume: bool,
/// Check API connectivity and exit
#[arg(long)]
pub check_connection: bool,
#[arg(long, default_value = "claude-sonnet-4-6")]
pub model: String,
#[arg(long, default_value = "morph-v3-fast")]
pub morph_model: String,
/// Maximum output tokens per API response. 8192 is too low for
/// modern frontier models writing long files — a `write_file` call
/// with multi-KB content hits this limit mid-stream and truncates
/// the tool-call JSON, surfacing as "Missing 'path' parameter".
/// Claude Sonnet 4 and GPT-4.1 both support 32k+; smaller models
/// cap at their own limit so this is safe as a default.
/// Must be > 16384 when reasoning effort is enabled (the legacy
/// Anthropic thinking-budget ceiling); the default 32768 satisfies it.
#[arg(long, default_value = "32768")]
pub max_tokens: u32,
/// Reasoning effort: off, low, medium, high. Default `medium`.
/// `Off` skips reasoning entirely on OpenAI (effort=minimal,
/// summary suppressed) and disables Anthropic extended thinking on
/// non-adaptive models. Anthropic adaptive (Opus 4.7+) collapses
/// `Off` to the lowest accepted level (`low`).
#[arg(short = 'e', long, default_value = "medium")]
pub reasoning_effort: crate::api::ReasoningEffort,
/// Deprecated. The flag has no effect on any path: legacy Anthropic
/// uses a fixed per-tier budget (Low=1024, Medium=5120, High=16384),
/// adaptive Anthropic (Opus 4.7+) uses `output_config.effort`, and
/// OpenAI uses `reasoning.effort`. The flag still parses so older
/// scripts don't break; `main.rs` warns at startup when a non-default
/// value is supplied. Hidden from `--help`. Will be removed in a
/// future release. Use `--reasoning-effort` to control thinking depth.
#[arg(long, default_value_t = THINKING_BUDGET_DEFAULT, hide = true)]
pub thinking_budget: u32,
#[arg(short, long)]
pub verbose: bool,
/// Enable read-only mode
#[arg(short, long)]
pub safe_mode: bool,
}
impl Cli {
pub fn get_anthropic_api_key(&self) -> Result<String, SofosError> {
self.api_key
.clone()
.ok_or_else(|| SofosError::Config("ANTHROPIC_API_KEY not found".to_string()))
}
pub fn get_openai_api_key(&self) -> Result<String, SofosError> {
self.openai_api_key
.clone()
.ok_or_else(|| SofosError::Config("OPENAI_API_KEY not found".to_string()))
}
}