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::path::PathBuf;
9use std::sync::Arc;
10
11use anyhow::{Context, Result};
12use chrono::Utc;
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16use crate::project::{Project, ProjectManager, ProjectSource};
17
18/// Serialized Project info for API responses.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[allow(missing_docs)]
21pub struct ProjectInfo {
22    pub id: String,
23    pub name: String,
24    pub description: String,
25    pub source: String,
26    pub paths: Vec<String>,
27    pub tags: Vec<String>,
28    pub emoji: String,
29    pub memory_visible: bool,
30    pub created_at: String,
31    pub updated_at: String,
32    pub last_active_at: String,
33}
34
35impl From<&Project> for ProjectInfo {
36    fn from(project: &Project) -> Self {
37        Self {
38            id: project.id.to_string(),
39            name: project.name.clone(),
40            description: project.description.clone(),
41            source: project.source.to_string(),
42            paths: project
43                .paths
44                .iter()
45                .map(|p| p.to_string_lossy().to_string())
46                .collect(),
47            tags: project.tags.clone(),
48            emoji: project.emoji.clone(),
49            memory_visible: project.memory_visible,
50            created_at: project.created_at.to_rfc3339(),
51            updated_at: project.updated_at.to_rfc3339(),
52            last_active_at: project.last_active_at.to_rfc3339(),
53        }
54    }
55}
56
57/// Project system calls.
58///
59/// All methods return `Result` for operations that can fail,
60/// and `Option` for lookup operations.
61#[allow(dead_code)]
62pub struct ProjectApi {
63    /// Project manager for Project lifecycle.
64    pub(crate) project_manager: Arc<ProjectManager>,
65}
66
67impl ProjectApi {
68    /// Create a new ProjectApi.
69    pub fn new(project_manager: Arc<ProjectManager>) -> Self {
70        Self { project_manager }
71    }
72
73    /// List all Projects.
74    pub fn list_projects(&self) -> Vec<ProjectInfo> {
75        self.project_manager
76            .list_projects()
77            .iter()
78            .map(ProjectInfo::from)
79            .collect()
80    }
81
82    /// Get Project details by ID.
83    pub fn get_project(&self, id: &str) -> Option<ProjectInfo> {
84        let project_id = Uuid::parse_str(id).ok()?;
85        self.project_manager
86            .get_project(project_id)
87            .as_ref()
88            .map(ProjectInfo::from)
89    }
90
91    /// Create a new project.
92    pub fn create_project(
93        &self,
94        name: String,
95        paths: Vec<String>,
96        tags: Vec<String>,
97        emoji: Option<String>,
98        description: Option<String>,
99    ) -> Result<ProjectInfo> {
100        let paths: Vec<PathBuf> = paths.into_iter().map(PathBuf::from).collect();
101        let project = self.project_manager.create_project(
102            name,
103            paths,
104            tags,
105            emoji,
106            description,
107            ProjectSource::Manual,
108        )?;
109        Ok(ProjectInfo::from(&project))
110    }
111
112    /// Update a project. Only non-None fields are changed.
113    #[allow(clippy::too_many_arguments)]
114    pub fn update_project(
115        &self,
116        id: &str,
117        name: Option<String>,
118        paths: Option<Vec<String>>,
119        tags: Option<Vec<String>>,
120        emoji: Option<String>,
121        description: Option<String>,
122        memory_visible: Option<bool>,
123    ) -> Result<ProjectInfo> {
124        let project_id = Uuid::parse_str(id).context("Invalid project ID")?;
125        let paths = paths.map(|v| v.into_iter().map(PathBuf::from).collect());
126
127        let mut project = self.project_manager.update_project(
128            project_id,
129            name,
130            paths,
131            tags,
132            emoji,
133            description,
134        )?;
135
136        // memory_visible requires separate save (not part of update_project signature)
137        if let Some(visible) = memory_visible {
138            project.memory_visible = visible;
139            project.updated_at = Utc::now();
140            self.project_manager.save_project(&project)?;
141        }
142
143        Ok(ProjectInfo::from(&project))
144    }
145
146    /// Remove a project.
147    pub fn remove_project(&self, id: &str) -> Result<()> {
148        let project_id = Uuid::parse_str(id).context("Invalid project ID")?;
149        self.project_manager.remove_project(project_id)
150    }
151
152    /// Link a memory to a project.
153    pub fn link_memory(&self, project_id: &str, memory_id: &str) -> Result<()> {
154        let pid = Uuid::parse_str(project_id).context("Invalid project ID")?;
155        self.project_manager.link_memory(pid, memory_id)
156    }
157
158    /// Unlink a memory from a project.
159    pub fn unlink_memory(&self, project_id: &str, memory_id: &str) -> Result<()> {
160        let pid = Uuid::parse_str(project_id).context("Invalid project ID")?;
161        self.project_manager.unlink_memory(pid, memory_id)
162    }
163
164    /// Get all memory IDs linked to a project.
165    pub fn get_project_memory_ids(&self, project_id: &str) -> Result<Vec<String>> {
166        let pid = Uuid::parse_str(project_id).context("Invalid project ID")?;
167        self.project_manager.get_project_memory_ids(pid)
168    }
169}