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 tools;
33pub mod tui; pub(crate) mod ui;
35pub(crate) mod util;
36
37pub fn build_oxi_engine() -> anyhow::Result<oxi_sdk::Oxi> {
54 let paths = services::OxiPaths::default_paths()?;
55 services::build_oxi(&paths)
56}
57
58pub async fn run_port_check() -> anyhow::Result<()> {
65 let oxi = build_oxi_engine()?;
66 let ports = oxi.ports();
67
68 let entries = ports.state.list("").await?;
70 println!("[state] entries: {}", entries.len());
71
72 let providers = ports.auth.list_providers().await?;
74 println!("[auth] providers with credentials: {:?}", providers);
75
76 let keys = ports.config.list()?;
78 println!("[config] keys: {}", keys.len());
79
80 let skills = ports.skills.list().await?;
82 println!("[skills] {} skill(s) discovered", skills.len());
83 for s in &skills {
84 println!(" - {}: {}", s.name, s.description);
85 }
86
87 let _ = ports
89 .event_bus
90 .publish(&"port-check".to_string(), serde_json::json!({"ok": true}))
91 .await;
92 println!("[event-bus] publish ok (noop bus if not registered)");
93
94 println!("\nport check: ok");
95 Ok(())
96}
97
98#[derive(Debug, Clone)]
100pub struct CompactionContext {
101 pub messages_count: usize,
103 pub tokens_before: usize,
105 pub target_tokens: usize,
107 pub strategy: String,
109}
110
111impl CompactionContext {
112 pub fn new(
114 messages_count: usize,
115 tokens_before: usize,
116 target_tokens: usize,
117 strategy: impl Into<String>,
118 ) -> Self {
119 Self {
120 messages_count,
121 tokens_before,
122 target_tokens,
123 strategy: strategy.into(),
124 }
125 }
126
127 pub fn compression_ratio(&self) -> f32 {
129 if self.tokens_before == 0 {
130 return 1.0;
131 }
132 self.target_tokens as f32 / self.tokens_before as f32
133 }
134}
135
136use crate::store::settings::Settings;
138use anyhow::{Error, Result};
139use oxi_agent::{Agent, AgentConfig, AgentEvent};
140use parking_lot::RwLock;
141use skills::SkillManager;
142use std::sync::Arc;
143
144pub struct App {
153 oxi: oxi_sdk::Oxi,
154 agent: Arc<Agent>,
155 settings: Settings,
156 skills: RwLock<SkillManager>,
157 active_skills: RwLock<Vec<String>>,
158 wasm_ext: Option<std::sync::Arc<crate::extensions::WasmExtensionManager>>,
159 questionnaire_bridge:
160 Option<std::sync::Arc<oxi_agent::tools::questionnaire::QuestionnaireBridge>>,
161 issue_store: Option<crate::store::issues::FileIssueStore>,
165}
166
167fn build_system_prompt(
170 thinking_level: crate::store::settings::ThinkingLevel,
171 skill_contents: &[String],
172) -> String {
173 let skills: Vec<prompt::system_prompt::Skill> = skill_contents
174 .iter()
175 .enumerate()
176 .map(|(i, content)| prompt::system_prompt::Skill {
177 name: format!("skill-{}", i),
178 content: content.clone(),
179 })
180 .collect();
181
182 let options = prompt::system_prompt::BuildSystemPromptOptions {
183 custom_prompt: prompt::system_prompt::thinking_level_prompt(thinking_level),
184 skills,
185 cwd: std::env::current_dir()
186 .map(|p| p.to_string_lossy().to_string())
187 .unwrap_or_default(),
188 ..Default::default()
189 };
190
191 prompt::system_prompt::build_system_prompt(&options)
192}
193
194impl App {
197 pub async fn from_oxi(oxi: oxi_sdk::Oxi, settings: Settings) -> Result<Self> {
204 let model_id = settings.effective_model(None).unwrap_or_default();
205 let provider_name = settings
206 .effective_provider(None)
207 .unwrap_or_else(|| model_id.split('/').next().unwrap_or("").to_string());
208
209 let api_key = oxi.ports().auth.get_api_key(&provider_name).await?;
211
212 let skills_dir = SkillManager::skills_dir().unwrap_or_else(|_| {
213 dirs::home_dir()
214 .unwrap_or_default()
215 .join(".oxi")
216 .join("skills")
217 });
218 let skills = SkillManager::load_from_dir(&skills_dir).unwrap_or_else(|e| {
219 tracing::debug!("Skills not loaded: {}", e);
220 SkillManager::new()
221 });
222
223 let system_prompt = build_system_prompt(settings.thinking_level, &[]);
224 let compaction_strategy = if settings.auto_compaction {
225 oxi_sdk::CompactionStrategy::Threshold(0.8)
226 } else {
227 oxi_sdk::CompactionStrategy::Disabled
228 };
229
230 let config = AgentConfig {
231 name: "oxi".to_string(),
232 description: Some("oxi CLI agent".to_string()),
233 model_id: model_id.clone(),
234 system_prompt: Some(system_prompt),
235 timeout_seconds: settings.tool_timeout_seconds,
236 temperature: settings.effective_temperature(),
237 max_tokens: settings.effective_max_tokens(),
238 compaction_strategy,
239 compaction_instruction: None,
240 context_window: 128_000,
241 api_key,
242 workspace_dir: Some(
243 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
244 ),
245 output_mode: None,
246 provider_options: None,
247 };
248
249 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
251 let agent = oxi
252 .agent(config)
253 .workspace(cwd)
254 .build()
255 .map_err(|e| Error::msg(format!("agent build failed: {e}")))?;
256 let agent = Arc::new(agent);
257
258 let bridge =
259 std::sync::Arc::new(oxi_agent::tools::questionnaire::QuestionnaireBridge::new());
260 let questionnaire_tool =
261 oxi_agent::tools::questionnaire::QuestionnaireTool::new(bridge.clone());
262 agent
263 .tools()
264 .register_arc(std::sync::Arc::new(questionnaire_tool));
265
266 let issue_store = std::env::current_dir()
271 .ok()
272 .map(|cwd| crate::store::issues::FileIssueStore::open_from_cwd(&cwd))
273 .and_then(|r| {
274 r.map_err(|e| tracing::warn!("issue store unavailable: {e}"))
275 .ok()
276 });
277
278 if let Some(store) = issue_store.clone() {
280 let tool = std::sync::Arc::new(crate::tools::IssueTool::new(store));
281 agent.tools().register_arc(tool);
282 }
283
284 Ok(Self {
285 oxi,
286 agent,
287 settings,
288 skills: RwLock::new(skills),
289 active_skills: RwLock::new(Vec::new()),
290 wasm_ext: None,
291 questionnaire_bridge: Some(bridge),
292 issue_store,
293 })
294 }
295
296 pub fn settings(&self) -> &Settings {
298 &self.settings
299 }
300
301 pub fn set_wasm_ext(
303 &mut self,
304 ext: Option<std::sync::Arc<crate::extensions::WasmExtensionManager>>,
305 ) {
306 self.wasm_ext = ext;
307 }
308
309 pub fn wasm_ext(&self) -> Option<&std::sync::Arc<crate::extensions::WasmExtensionManager>> {
311 self.wasm_ext.as_ref()
312 }
313
314 pub fn issue_store(&self) -> Option<crate::store::issues::FileIssueStore> {
316 self.issue_store.clone()
317 }
318
319 pub fn agent(&self) -> Arc<Agent> {
321 Arc::clone(&self.agent)
322 }
323
324 pub fn agent_tools(&self) -> Arc<oxi_agent::ToolRegistry> {
326 self.agent.tools()
327 }
328
329 pub fn questionnaire_bridge(
331 &self,
332 ) -> Option<&std::sync::Arc<oxi_agent::tools::questionnaire::QuestionnaireBridge>> {
333 self.questionnaire_bridge.as_ref()
334 }
335
336 pub fn skills(&self) -> parking_lot::RwLockReadGuard<'_, SkillManager> {
338 self.skills.read()
339 }
340
341 pub fn activate_skill(&self, name: &str) -> Result<(), String> {
343 {
344 let skills = self.skills.read();
345 if skills.get(name).is_none() {
346 return Err(format!("Skill '{}' not found", name));
347 }
348 }
349 let name_lower = name.to_lowercase();
350 {
351 let mut active = self.active_skills.write();
352 if !active.contains(&name_lower) {
353 active.push(name_lower);
354 }
355 }
356 self.rebuild_system_prompt();
357 Ok(())
358 }
359
360 pub fn deactivate_skill(&self, name: &str) {
362 let name_lower = name.to_lowercase();
363 {
364 let mut active = self.active_skills.write();
365 active.retain(|n| n != &name_lower);
366 }
367 self.rebuild_system_prompt();
368 }
369
370 pub fn active_skills(&self) -> Vec<String> {
372 self.active_skills.read().clone()
373 }
374
375 fn rebuild_system_prompt(&self) {
377 let active = self.active_skills.read();
378 let skills = self.skills.read();
379 let contents: Vec<String> = active
380 .iter()
381 .filter_map(|name| skills.get(name).map(|s| s.content.clone()))
382 .collect();
383 let prompt = build_system_prompt(self.settings.thinking_level, &contents);
384 self.agent.set_system_prompt(prompt);
385 }
386
387 pub fn agent_state(&self) -> oxi_agent::AgentState {
389 self.agent.state()
390 }
391
392 pub async fn run_prompt(&self, prompt: String) -> Result<String> {
394 let (response, _events) = self.agent.run(prompt).await?;
395 Ok(response.content)
396 }
397
398 pub async fn run_prompt_with_events<F>(&self, prompt: String, on_event: F) -> Result<String>
400 where
401 F: FnMut(AgentEvent) + Send + 'static,
402 {
403 self.agent.run_streaming(prompt, on_event).await?;
404 let state = self.agent_state();
405 for msg in state.messages.iter().rev() {
406 if let oxi_sdk::Message::Assistant(a) = msg {
407 return Ok(a.text_content());
408 }
409 }
410 Ok(String::new())
411 }
412
413 pub fn reset(&self) {
415 self.agent.reset();
416 }
417
418 pub async fn switch_model(&self, model_id: &str) -> anyhow::Result<()> {
420 let parts: Vec<&str> = model_id.split('/').collect();
421 let provider = parts
422 .first()
423 .map(|s| s.to_string())
424 .unwrap_or_else(|| "anthropic".to_string());
425 let api_key = self.oxi.ports().auth.get_api_key(&provider).await?;
426 let _ = self.agent.switch_model(model_id, api_key);
427 Ok(())
428 }
429
430 pub fn model_id(&self) -> String {
432 self.agent.model_id()
433 }
434}