Skip to main content

oxios_kernel/kernel_handle/
project_api.rs

1//! Project API — Project management system calls (RFC-011).
2//!
3//! Provides API endpoints for:
4//! - Listing and querying Projects
5//! - CRUD operations on Projects
6//! - Project-Memory association (link/unlink)
7
8use std::sync::Arc;
9
10use anyhow::{Context, Result};
11use chrono::Utc;
12use serde::{Deserialize, Serialize};
13use uuid::Uuid;
14
15use crate::project::{Project, ProjectManager, ProjectSource};
16
17/// Serialized Project info for API responses.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[allow(missing_docs)]
20pub struct ProjectInfo {
21    pub id: String,
22    pub name: String,
23    pub description: String,
24    pub source: String,
25    pub tags: Vec<String>,
26    pub emoji: String,
27    pub memory_visible: bool,
28    /// RFC-025: Mounts this Project references.
29    pub mount_ids: Vec<String>,
30    /// RFC-025: Custom instructions injected into the system prompt.
31    pub instructions: String,
32    pub created_at: String,
33    pub updated_at: String,
34    pub last_active_at: String,
35}
36
37impl From<&Project> for ProjectInfo {
38    fn from(project: &Project) -> Self {
39        Self {
40            id: project.id.to_string(),
41            name: project.name.clone(),
42            description: project.description.clone(),
43            source: project.source.to_string(),
44            tags: project.tags.clone(),
45            emoji: project.emoji.clone(),
46            memory_visible: project.memory_visible,
47            mount_ids: project.mount_ids.iter().map(|m| m.to_string()).collect(),
48            instructions: project.instructions.clone(),
49            created_at: project.created_at.to_rfc3339(),
50            updated_at: project.updated_at.to_rfc3339(),
51            last_active_at: project.last_active_at.to_rfc3339(),
52        }
53    }
54}
55
56/// Project system calls.
57///
58/// All methods return `Result` for operations that can fail,
59/// and `Option` for lookup operations.
60#[allow(dead_code)]
61pub struct ProjectApi {
62    /// Project manager for Project lifecycle.
63    pub(crate) project_manager: Arc<ProjectManager>,
64}
65
66impl ProjectApi {
67    /// Create a new ProjectApi.
68    pub fn new(project_manager: Arc<ProjectManager>) -> Self {
69        Self { project_manager }
70    }
71
72    /// List all Projects.
73    pub fn list_projects(&self) -> Vec<ProjectInfo> {
74        self.project_manager
75            .list_projects()
76            .iter()
77            .map(ProjectInfo::from)
78            .collect()
79    }
80
81    /// Get Project details by ID.
82    pub fn get_project(&self, id: &str) -> Option<ProjectInfo> {
83        let project_id = Uuid::parse_str(id).ok()?;
84        self.project_manager
85            .get_project(project_id)
86            .as_ref()
87            .map(ProjectInfo::from)
88    }
89
90    /// Create a new project.
91    pub fn create_project(
92        &self,
93        name: String,
94        tags: Vec<String>,
95        emoji: Option<String>,
96        description: Option<String>,
97    ) -> Result<ProjectInfo> {
98        let project = self.project_manager.create_project(
99            name,
100            tags,
101            emoji,
102            description,
103            ProjectSource::Manual,
104        )?;
105        Ok(ProjectInfo::from(&project))
106    }
107
108    /// Update a project. Only non-None fields are changed.
109    #[allow(clippy::too_many_arguments)]
110    pub fn update_project(
111        &self,
112        id: &str,
113        name: Option<String>,
114        tags: Option<Vec<String>>,
115        emoji: Option<String>,
116        description: Option<String>,
117        memory_visible: Option<bool>,
118    ) -> Result<ProjectInfo> {
119        let project_id = Uuid::parse_str(id).context("Invalid project ID")?;
120
121        let mut project =
122            self.project_manager
123                .update_project(project_id, name, tags, emoji, description)?;
124
125        // memory_visible requires separate save (not part of update_project signature)
126        if let Some(visible) = memory_visible {
127            project.memory_visible = visible;
128            project.updated_at = Utc::now();
129            self.project_manager.save_project(&project)?;
130        }
131
132        Ok(ProjectInfo::from(&project))
133    }
134
135    /// Remove a project.
136    pub fn remove_project(&self, id: &str) -> Result<()> {
137        let project_id = Uuid::parse_str(id).context("Invalid project ID")?;
138        self.project_manager.remove_project(project_id)
139    }
140
141    /// Link a memory to a project.
142    pub fn link_memory(&self, project_id: &str, memory_id: &str) -> Result<()> {
143        let pid = Uuid::parse_str(project_id).context("Invalid project ID")?;
144        self.project_manager.link_memory(pid, memory_id)
145    }
146
147    /// Unlink a memory from a project.
148    pub fn unlink_memory(&self, project_id: &str, memory_id: &str) -> Result<()> {
149        let pid = Uuid::parse_str(project_id).context("Invalid project ID")?;
150        self.project_manager.unlink_memory(pid, memory_id)
151    }
152
153    /// Get all memory IDs linked to a project.
154    pub fn get_project_memory_ids(&self, project_id: &str) -> Result<Vec<String>> {
155        let pid = Uuid::parse_str(project_id).context("Invalid project ID")?;
156        self.project_manager.get_project_memory_ids(pid)
157    }
158
159    /// Update a Project's RFC-025 bundle fields (mount_ids, instructions).
160    pub fn update_project_bundle(
161        &self,
162        id: &str,
163        mount_ids: Option<Vec<String>>,
164        instructions: Option<String>,
165    ) -> Result<ProjectInfo> {
166        let pid = Uuid::parse_str(id).context("Invalid project ID")?;
167        // Reject invalid UUIDs rather than silently dropping them. Collecting
168        // all the bad IDs lets the caller see every offender in one error.
169        let mount_ids = match mount_ids {
170            Some(ids) => {
171                let mut parsed = Vec::with_capacity(ids.len());
172                let mut bad = Vec::new();
173                for s in ids {
174                    match uuid::Uuid::parse_str(&s) {
175                        Ok(u) => parsed.push(u),
176                        Err(_) => bad.push(s),
177                    }
178                }
179                if !bad.is_empty() {
180                    anyhow::bail!("Invalid mount ID(s): {}", bad.join(", "));
181                }
182                Some(parsed)
183            }
184            None => None,
185        };
186        let project = self
187            .project_manager
188            .update_project_bundle(pid, mount_ids, instructions)?;
189        Ok(ProjectInfo::from(&project))
190    }
191}