1#![warn(missing_docs)]
2#![warn(clippy::unwrap_used)]
3#![allow(unknown_lints)]
4
5pub mod bootstrap;
12pub mod cli;
13pub mod main_dispatch;
14pub mod print_mode;
15pub mod services;
16pub mod setup_wizard;
17pub mod store;
18
19pub(crate) mod app;
21pub(crate) mod context;
22pub mod extensions; pub(crate) mod infra;
24pub(crate) mod media;
25pub(crate) mod prompt;
26pub(crate) mod rpc_mode;
27pub(crate) mod skills;
28pub mod storage; pub use storage::packages::PackageManager;
31pub use storage::packages::ResourceKind;
32pub mod tui; pub(crate) mod ui;
34pub(crate) mod util;
35
36pub fn build_oxi_engine() -> anyhow::Result<oxi_sdk::Oxi> {
53 let paths = services::OxiPaths::default_paths()?;
54 services::build_oxi(&paths)
55}
56
57pub async fn run_port_check() -> anyhow::Result<()> {
64 let oxi = build_oxi_engine()?;
65 let ports = oxi.ports();
66
67 let entries = ports.state.list("").await?;
69 println!("[state] entries: {}", entries.len());
70
71 let providers = ports.auth.list_providers().await?;
73 println!("[auth] providers with credentials: {:?}", providers);
74
75 let keys = ports.config.list()?;
77 println!("[config] keys: {}", keys.len());
78
79 let skills = ports.skills.list().await?;
81 println!("[skills] {} skill(s) discovered", skills.len());
82 for s in &skills {
83 println!(" - {}: {}", s.name, s.description);
84 }
85
86 let _ = ports
88 .event_bus
89 .publish(&"port-check".to_string(), serde_json::json!({"ok": true}))
90 .await;
91 println!("[event-bus] publish ok (noop bus if not registered)");
92
93 println!("\nport check: ok");
94 Ok(())
95}
96
97#[derive(Debug, Clone)]
99pub struct CompactionContext {
100 pub messages_count: usize,
102 pub tokens_before: usize,
104 pub target_tokens: usize,
106 pub strategy: String,
108}
109
110impl CompactionContext {
111 pub fn new(
113 messages_count: usize,
114 tokens_before: usize,
115 target_tokens: usize,
116 strategy: impl Into<String>,
117 ) -> Self {
118 Self {
119 messages_count,
120 tokens_before,
121 target_tokens,
122 strategy: strategy.into(),
123 }
124 }
125
126 pub fn compression_ratio(&self) -> f32 {
128 if self.tokens_before == 0 {
129 return 1.0;
130 }
131 self.target_tokens as f32 / self.tokens_before as f32
132 }
133}
134
135use crate::store::settings::Settings;
137use anyhow::{Error, Result};
138use oxi_agent::{Agent, AgentConfig, AgentEvent};
139use parking_lot::RwLock;
140use skills::SkillManager;
141use std::sync::Arc;
142
143pub struct App {
152 oxi: oxi_sdk::Oxi,
153 agent: Arc<Agent>,
154 settings: Settings,
155 skills: RwLock<SkillManager>,
156 active_skills: RwLock<Vec<String>>,
157 wasm_ext: Option<std::sync::Arc<crate::extensions::WasmExtensionManager>>,
158 questionnaire_bridge:
159 Option<std::sync::Arc<oxi_agent::tools::questionnaire::QuestionnaireBridge>>,
160}
161
162fn build_system_prompt(
165 thinking_level: crate::store::settings::ThinkingLevel,
166 skill_contents: &[String],
167) -> String {
168 let skills: Vec<prompt::system_prompt::Skill> = skill_contents
169 .iter()
170 .enumerate()
171 .map(|(i, content)| prompt::system_prompt::Skill {
172 name: format!("skill-{}", i),
173 content: content.clone(),
174 })
175 .collect();
176
177 let options = prompt::system_prompt::BuildSystemPromptOptions {
178 custom_prompt: prompt::system_prompt::thinking_level_prompt(thinking_level),
179 skills,
180 cwd: std::env::current_dir()
181 .map(|p| p.to_string_lossy().to_string())
182 .unwrap_or_default(),
183 ..Default::default()
184 };
185
186 prompt::system_prompt::build_system_prompt(&options)
187}
188
189impl App {
192 pub async fn from_oxi(oxi: oxi_sdk::Oxi, settings: Settings) -> Result<Self> {
199 let model_id = settings.effective_model(None).unwrap_or_default();
200 let provider_name = settings
201 .effective_provider(None)
202 .unwrap_or_else(|| model_id.split('/').next().unwrap_or("").to_string());
203
204 let api_key = oxi.ports().auth.get_api_key(&provider_name).await?;
206
207 let skills_dir = SkillManager::skills_dir().unwrap_or_else(|_| {
208 dirs::home_dir()
209 .unwrap_or_default()
210 .join(".oxi")
211 .join("skills")
212 });
213 let skills = SkillManager::load_from_dir(&skills_dir).unwrap_or_else(|e| {
214 tracing::debug!("Skills not loaded: {}", e);
215 SkillManager::new()
216 });
217
218 let system_prompt = build_system_prompt(settings.thinking_level, &[]);
219 let compaction_strategy = if settings.auto_compaction {
220 oxi_sdk::CompactionStrategy::Threshold(0.8)
221 } else {
222 oxi_sdk::CompactionStrategy::Disabled
223 };
224
225 let config = AgentConfig {
226 name: "oxi".to_string(),
227 description: Some("oxi CLI agent".to_string()),
228 model_id: model_id.clone(),
229 system_prompt: Some(system_prompt),
230 max_iterations: 10,
231 timeout_seconds: settings.tool_timeout_seconds,
232 temperature: settings.effective_temperature(),
233 max_tokens: settings.effective_max_tokens(),
234 compaction_strategy,
235 compaction_instruction: None,
236 context_window: 128_000,
237 api_key,
238 workspace_dir: Some(
239 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
240 ),
241 output_mode: None,
242 provider_options: None,
243 };
244
245 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
247 let agent = oxi
248 .agent(config)
249 .workspace(cwd)
250 .build()
251 .map_err(|e| Error::msg(format!("agent build failed: {e}")))?;
252 let agent = Arc::new(agent);
253
254 let bridge =
255 std::sync::Arc::new(oxi_agent::tools::questionnaire::QuestionnaireBridge::new());
256 let questionnaire_tool =
257 oxi_agent::tools::questionnaire::QuestionnaireTool::new(bridge.clone());
258 agent
259 .tools()
260 .register_arc(std::sync::Arc::new(questionnaire_tool));
261
262 Ok(Self {
263 oxi,
264 agent,
265 settings,
266 skills: RwLock::new(skills),
267 active_skills: RwLock::new(Vec::new()),
268 wasm_ext: None,
269 questionnaire_bridge: Some(bridge),
270 })
271 }
272
273 pub fn settings(&self) -> &Settings {
275 &self.settings
276 }
277
278 pub fn set_wasm_ext(
280 &mut self,
281 ext: Option<std::sync::Arc<crate::extensions::WasmExtensionManager>>,
282 ) {
283 self.wasm_ext = ext;
284 }
285
286 pub fn wasm_ext(&self) -> Option<&std::sync::Arc<crate::extensions::WasmExtensionManager>> {
288 self.wasm_ext.as_ref()
289 }
290
291 pub fn agent(&self) -> Arc<Agent> {
293 Arc::clone(&self.agent)
294 }
295
296 pub fn agent_tools(&self) -> Arc<oxi_agent::ToolRegistry> {
298 self.agent.tools()
299 }
300
301 pub fn questionnaire_bridge(
303 &self,
304 ) -> Option<&std::sync::Arc<oxi_agent::tools::questionnaire::QuestionnaireBridge>> {
305 self.questionnaire_bridge.as_ref()
306 }
307
308 pub fn skills(&self) -> parking_lot::RwLockReadGuard<'_, SkillManager> {
310 self.skills.read()
311 }
312
313 pub fn activate_skill(&self, name: &str) -> Result<(), String> {
315 {
316 let skills = self.skills.read();
317 if skills.get(name).is_none() {
318 return Err(format!("Skill '{}' not found", name));
319 }
320 }
321 let name_lower = name.to_lowercase();
322 {
323 let mut active = self.active_skills.write();
324 if !active.contains(&name_lower) {
325 active.push(name_lower);
326 }
327 }
328 self.rebuild_system_prompt();
329 Ok(())
330 }
331
332 pub fn deactivate_skill(&self, name: &str) {
334 let name_lower = name.to_lowercase();
335 {
336 let mut active = self.active_skills.write();
337 active.retain(|n| n != &name_lower);
338 }
339 self.rebuild_system_prompt();
340 }
341
342 pub fn active_skills(&self) -> Vec<String> {
344 self.active_skills.read().clone()
345 }
346
347 fn rebuild_system_prompt(&self) {
349 let active = self.active_skills.read();
350 let skills = self.skills.read();
351 let contents: Vec<String> = active
352 .iter()
353 .filter_map(|name| skills.get(name).map(|s| s.content.clone()))
354 .collect();
355 let prompt = build_system_prompt(self.settings.thinking_level, &contents);
356 self.agent.set_system_prompt(prompt);
357 }
358
359 pub fn agent_state(&self) -> oxi_agent::AgentState {
361 self.agent.state()
362 }
363
364 pub async fn run_prompt(&self, prompt: String) -> Result<String> {
366 let (response, _events) = self.agent.run(prompt).await?;
367 Ok(response.content)
368 }
369
370 pub async fn run_prompt_with_events<F>(&self, prompt: String, on_event: F) -> Result<String>
372 where
373 F: FnMut(AgentEvent) + Send + 'static,
374 {
375 self.agent.run_streaming(prompt, on_event).await?;
376 let state = self.agent_state();
377 for msg in state.messages.iter().rev() {
378 if let oxi_sdk::Message::Assistant(a) = msg {
379 return Ok(a.text_content());
380 }
381 }
382 Ok(String::new())
383 }
384
385 pub fn reset(&self) {
387 self.agent.reset();
388 }
389
390 pub async fn switch_model(&self, model_id: &str) -> anyhow::Result<()> {
392 let parts: Vec<&str> = model_id.split('/').collect();
393 let provider = parts
394 .first()
395 .map(|s| s.to_string())
396 .unwrap_or_else(|| "anthropic".to_string());
397 let api_key = self.oxi.ports().auth.get_api_key(&provider).await?;
398 let _ = self.agent.switch_model(model_id, api_key);
399 Ok(())
400 }
401
402 pub fn model_id(&self) -> String {
404 self.agent.model_id()
405 }
406}