remove and cleanup unused code (#4074)

This commit is contained in:
Zane
2025-08-14 16:27:53 -07:00
committed by GitHub
parent c498fa018d
commit f609f27db0
72 changed files with 1 additions and 4792 deletions
-26
View File
@@ -7,7 +7,6 @@ use crate::commands::bench::agent_generator;
use crate::commands::configure::handle_configure;
use crate::commands::info::handle_info;
use crate::commands::mcp::run_server;
use crate::commands::project::{handle_project_default, handle_projects_interactive};
use crate::commands::recipe::{handle_deeplink, handle_list, handle_validate};
// Import the new handlers from commands::schedule
use crate::commands::schedule::{
@@ -387,14 +386,6 @@ enum Command {
builtins: Vec<String>,
},
/// Open the last project directory
#[command(about = "Open the last project directory", visible_alias = "p")]
Project {},
/// List recent project directories
#[command(about = "List recent project directories", visible_alias = "ps")]
Projects,
/// Execute commands from an instruction file
#[command(about = "Execute commands from an instruction file or stdin")]
Run {
@@ -700,18 +691,11 @@ pub struct RecipeInfo {
pub async fn cli() -> Result<()> {
let cli = Cli::parse();
// Track the current directory in projects.json
if let Err(e) = crate::project_tracker::update_project_tracker(None, None) {
eprintln!("Warning: Failed to update project tracker: {}", e);
}
let command_name = match &cli.command {
Some(Command::Configure {}) => "configure",
Some(Command::Info { .. }) => "info",
Some(Command::Mcp { .. }) => "mcp",
Some(Command::Session { .. }) => "session",
Some(Command::Project {}) => "project",
Some(Command::Projects) => "projects",
Some(Command::Run { .. }) => "run",
Some(Command::Schedule { .. }) => "schedule",
Some(Command::Update { .. }) => "update",
@@ -862,16 +846,6 @@ pub async fn cli() -> Result<()> {
}
};
}
Some(Command::Project {}) => {
// Default behavior: offer to resume the last project
handle_project_default()?;
return Ok(());
}
Some(Command::Projects) => {
// Interactive project selection
handle_projects_interactive()?;
return Ok(());
}
Some(Command::Run {
instructions,
-1
View File
@@ -2,7 +2,6 @@ pub mod bench;
pub mod configure;
pub mod info;
pub mod mcp;
pub mod project;
pub mod recipe;
pub mod schedule;
pub mod session;
-304
View File
@@ -1,304 +0,0 @@
use anyhow::Result;
use chrono::DateTime;
use cliclack::{self, intro, outro};
use std::path::Path;
use crate::project_tracker::ProjectTracker;
use goose::utils::safe_truncate;
/// Format a DateTime for display
fn format_date(date: DateTime<chrono::Utc>) -> String {
// Format: "2025-05-08 18:15:30"
date.format("%Y-%m-%d %H:%M:%S").to_string()
}
/// Handle the default project command
///
/// Offers options to resume the most recently accessed project
pub fn handle_project_default() -> Result<()> {
let tracker = ProjectTracker::load()?;
let mut projects = tracker.list_projects();
if projects.is_empty() {
// If no projects exist, just start a new one in the current directory
println!("No previous projects found. Starting a new session in the current directory.");
let mut command = std::process::Command::new("goose");
command.arg("session");
let status = command.status()?;
if !status.success() {
println!("Failed to run Goose. Exit code: {:?}", status.code());
}
return Ok(());
}
// Sort projects by last_accessed (newest first)
projects.sort_by(|a, b| b.last_accessed.cmp(&a.last_accessed));
// Get the most recent project
let project = &projects[0];
let project_dir = &project.path;
// Check if the directory exists
if !Path::new(project_dir).exists() {
println!(
"Most recent project directory '{}' no longer exists.",
project_dir
);
return Ok(());
}
// Format the path for display
let path = Path::new(project_dir);
let components: Vec<_> = path.components().collect();
let len = components.len();
let short_path = if len <= 2 {
project_dir.clone()
} else {
let mut path_str = String::new();
path_str.push_str("...");
for component in components.iter().skip(len - 2) {
path_str.push('/');
path_str.push_str(component.as_os_str().to_string_lossy().as_ref());
}
path_str
};
// Ask the user what they want to do
let _ = intro("Goose Project Manager");
let current_dir = std::env::current_dir()?;
let current_dir_display = current_dir.display();
let choice = cliclack::select("Choose an option:")
.item(
"resume",
format!("Resume project with session: {}", short_path),
"Continue with the previous session",
)
.item(
"fresh",
format!("Resume project with fresh session: {}", short_path),
"Change to the project directory but start a new session",
)
.item(
"new",
format!(
"Start new project in current directory: {}",
current_dir_display
),
"Stay in the current directory and start a new session",
)
.interact()?;
match choice {
"resume" => {
let _ = outro(format!("Changing to directory: {}", project_dir));
// Get the session ID if available
let session_id = project.last_session_id.clone();
// Change to the project directory
std::env::set_current_dir(project_dir)?;
// Build the command to run Goose
let mut command = std::process::Command::new("goose");
command.arg("session");
if let Some(id) = session_id {
command.arg("--name").arg(&id).arg("--resume");
println!("Resuming session: {}", id);
}
// Execute the command
let status = command.status()?;
if !status.success() {
println!("Failed to run Goose. Exit code: {:?}", status.code());
}
}
"fresh" => {
let _ = outro(format!(
"Changing to directory: {} with a fresh session",
project_dir
));
// Change to the project directory
std::env::set_current_dir(project_dir)?;
// Build the command to run Goose with a fresh session
let mut command = std::process::Command::new("goose");
command.arg("session");
// Execute the command
let status = command.status()?;
if !status.success() {
println!("Failed to run Goose. Exit code: {:?}", status.code());
}
}
"new" => {
let _ = outro("Starting a new session in the current directory");
// Build the command to run Goose
let mut command = std::process::Command::new("goose");
command.arg("session");
// Execute the command
let status = command.status()?;
if !status.success() {
println!("Failed to run Goose. Exit code: {:?}", status.code());
}
}
_ => {
let _ = outro("Operation canceled");
}
}
Ok(())
}
/// Handle the interactive projects command
///
/// Shows a list of projects and lets the user select one to resume
pub fn handle_projects_interactive() -> Result<()> {
let tracker = ProjectTracker::load()?;
let mut projects = tracker.list_projects();
if projects.is_empty() {
println!("No projects found.");
return Ok(());
}
// Sort projects by last_accessed (newest first)
projects.sort_by(|a, b| b.last_accessed.cmp(&a.last_accessed));
// Format project paths for display
let project_choices: Vec<(String, String)> = projects
.iter()
.enumerate()
.map(|(i, project)| {
let path = Path::new(&project.path);
let components: Vec<_> = path.components().collect();
let len = components.len();
let short_path = if len <= 2 {
project.path.clone()
} else {
let mut path_str = String::new();
path_str.push_str("...");
for component in components.iter().skip(len - 2) {
path_str.push('/');
path_str.push_str(component.as_os_str().to_string_lossy().as_ref());
}
path_str
};
// Include last instruction if available (truncated)
let instruction_preview =
project
.last_instruction
.as_ref()
.map_or(String::new(), |instr| {
let truncated = safe_truncate(instr, 40);
format!(" [{}]", truncated)
});
let formatted_date = format_date(project.last_accessed);
(
format!("{}", i + 1), // Value to return
format!("{} ({}){}", short_path, formatted_date, instruction_preview), // Display text with instruction
)
})
.collect();
// Let the user select a project
let _ = intro("Goose Project Manager");
let mut select = cliclack::select("Select a project:");
// Add each project as an option
for (value, display) in &project_choices {
select = select.item(value, display, "");
}
// Add a cancel option
let cancel_value = String::from("cancel");
select = select.item(&cancel_value, "Cancel", "Don't resume any project");
let selected = select.interact()?;
if selected == "cancel" {
let _ = outro("Project selection canceled.");
return Ok(());
}
// Parse the selected index
let index = selected.parse::<usize>().unwrap_or(0);
if index == 0 || index > projects.len() {
let _ = outro("Invalid selection.");
return Ok(());
}
// Get the selected project
let project = &projects[index - 1];
let project_dir = &project.path;
// Check if the directory exists
if !Path::new(project_dir).exists() {
let _ = outro(format!(
"Project directory '{}' no longer exists.",
project_dir
));
return Ok(());
}
// Ask if the user wants to resume the session or start a new one
let session_id = project.last_session_id.clone();
let has_previous_session = session_id.is_some();
// Change to the project directory first
std::env::set_current_dir(project_dir)?;
let _ = outro(format!("Changed to directory: {}", project_dir));
// Only ask about resuming if there's a previous session
let resume_session = if has_previous_session {
let session_choice = cliclack::select("What would you like to do?")
.item(
"resume",
"Resume previous session",
"Continue with the previous session",
)
.item(
"new",
"Start new session",
"Start a fresh session in this project directory",
)
.interact()?;
session_choice == "resume"
} else {
false
};
// Build the command to run Goose
let mut command = std::process::Command::new("goose");
command.arg("session");
if resume_session {
if let Some(id) = session_id {
command.arg("--name").arg(&id).arg("--resume");
println!("Resuming session: {}", id);
}
} else {
println!("Starting new session");
}
// Execute the command
let status = command.status()?;
if !status.success() {
println!("Failed to run Goose. Exit code: {:?}", status.code());
}
Ok(())
}
-1
View File
@@ -3,7 +3,6 @@ use once_cell::sync::Lazy;
pub mod cli;
pub mod commands;
pub mod logging;
pub mod project_tracker;
pub mod recipes;
pub mod scenario_tests;
pub mod session;
-146
View File
@@ -1,146 +0,0 @@
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use etcetera::{choose_app_strategy, AppStrategy};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
/// Structure to track project information
#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectInfo {
/// The absolute path to the project directory
pub path: String,
/// Last time the project was accessed
pub last_accessed: DateTime<Utc>,
/// Last instruction sent to goose (if available)
pub last_instruction: Option<String>,
/// Last session ID associated with this project
pub last_session_id: Option<String>,
}
/// Structure to hold all tracked projects
#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectTracker {
projects: HashMap<String, ProjectInfo>,
}
/// Project information with path as a separate field for easier access
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectInfoDisplay {
/// The absolute path to the project directory
pub path: String,
/// Last time the project was accessed
pub last_accessed: DateTime<Utc>,
/// Last instruction sent to goose (if available)
pub last_instruction: Option<String>,
/// Last session ID associated with this project
pub last_session_id: Option<String>,
}
impl ProjectTracker {
/// Get the path to the projects.json file
fn get_projects_file() -> Result<PathBuf> {
let projects_file = choose_app_strategy(crate::APP_STRATEGY.clone())
.context("goose requires a home dir")?
.in_data_dir("projects.json");
// Ensure data directory exists
if let Some(parent) = projects_file.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
Ok(projects_file)
}
/// Load the project tracker from the projects.json file
pub fn load() -> Result<Self> {
let projects_file = Self::get_projects_file()?;
if projects_file.exists() {
let file_content = fs::read_to_string(&projects_file)?;
let tracker: ProjectTracker = serde_json::from_str(&file_content)
.context("Failed to parse projects.json file")?;
Ok(tracker)
} else {
// If the file doesn't exist, create a new empty tracker
Ok(ProjectTracker {
projects: HashMap::new(),
})
}
}
/// Save the project tracker to the projects.json file
pub fn save(&self) -> Result<()> {
let projects_file = Self::get_projects_file()?;
let json = serde_json::to_string_pretty(self)?;
fs::write(projects_file, json)?;
Ok(())
}
/// Update project information for the current directory
///
/// # Arguments
/// * `project_dir` - The project directory to update
/// * `instruction` - Optional instruction that was sent to goose
/// * `session_id` - Optional session ID associated with this project
pub fn update_project(
&mut self,
project_dir: &Path,
instruction: Option<&str>,
session_id: Option<&str>,
) -> Result<()> {
let dir_str = project_dir.to_string_lossy().to_string();
// Create or update the project entry
let project_info = self.projects.entry(dir_str.clone()).or_insert(ProjectInfo {
path: dir_str,
last_accessed: Utc::now(),
last_instruction: None,
last_session_id: None,
});
// Update the last accessed time
project_info.last_accessed = Utc::now();
// Update the last instruction if provided
if let Some(instr) = instruction {
project_info.last_instruction = Some(instr.to_string());
}
// Update the session ID if provided
if let Some(id) = session_id {
project_info.last_session_id = Some(id.to_string());
}
self.save()
}
/// List all tracked projects
///
/// Returns a vector of ProjectInfoDisplay objects
pub fn list_projects(&self) -> Vec<ProjectInfoDisplay> {
self.projects
.values()
.map(|info| ProjectInfoDisplay {
path: info.path.clone(),
last_accessed: info.last_accessed,
last_instruction: info.last_instruction.clone(),
last_session_id: info.last_session_id.clone(),
})
.collect()
}
}
/// Update the project tracker with the current directory and optional instruction
///
/// # Arguments
/// * `instruction` - Optional instruction that was sent to goose
/// * `session_id` - Optional session ID associated with this project
pub fn update_project_tracker(instruction: Option<&str>, session_id: Option<&str>) -> Result<()> {
let current_dir = std::env::current_dir()?;
let mut tracker = ProjectTracker::load()?;
tracker.update_project(&current_dir, instruction, session_id)
}
-34
View File
@@ -370,7 +370,6 @@ impl Session {
cancel_token: CancellationToken,
) -> Result<()> {
let cancel_token = cancel_token.clone();
let message_text = message.as_concat_text();
self.push_message(message);
// Get the provider from the agent for description generation
@@ -392,24 +391,6 @@ impl Session {
.await?;
}
// Track the current directory and last instruction in projects.json
let session_id = self
.session_file
.as_ref()
.and_then(|p| p.file_stem())
.and_then(|s| s.to_str())
.map(|s| s.to_string());
if let Err(e) = crate::project_tracker::update_project_tracker(
Some(&message_text),
session_id.as_deref(),
) {
eprintln!(
"Warning: Failed to update project tracker with instruction: {}",
e
);
}
self.process_agent_response(false, cancel_token).await?;
Ok(())
}
@@ -488,21 +469,6 @@ impl Session {
self.push_message(Message::user().with_text(&content));
// Track the current directory and last instruction in projects.json
let session_id = self
.session_file
.as_ref()
.and_then(|p| p.file_stem())
.and_then(|s| s.to_str())
.map(|s| s.to_string());
if let Err(e) = crate::project_tracker::update_project_tracker(
Some(&content),
session_id.as_deref(),
) {
eprintln!("Warning: Failed to update project tracker with instruction: {}", e);
}
let provider = self.agent.provider().await?;
// Persist messages with provider for automatic description generation
-2
View File
@@ -5,7 +5,6 @@ pub mod config_management;
pub mod context;
pub mod extension;
pub mod health;
pub mod project;
pub mod recipe;
pub mod reply;
pub mod schedule;
@@ -29,6 +28,5 @@ pub fn configure(state: Arc<crate::state::AppState>) -> Router {
.merge(recipe::routes(state.clone()))
.merge(session::routes(state.clone()))
.merge(schedule::routes(state.clone()))
.merge(project::routes(state.clone()))
.merge(setup::routes(state.clone()))
}
-358
View File
@@ -1,358 +0,0 @@
use super::utils::verify_secret_key;
use std::sync::Arc;
use crate::state::AppState;
use axum::{
extract::{Path, State},
http::{HeaderMap, StatusCode},
routing::{delete, get, post, put},
Json, Router,
};
use goose::project::{Project, ProjectMetadata};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateProjectRequest {
/// Display name of the project
pub name: String,
/// Optional description of the project
pub description: Option<String>,
/// Default working directory for sessions in this project
#[schema(value_type = String)]
pub default_directory: std::path::PathBuf,
}
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateProjectRequest {
/// Display name of the project
pub name: Option<String>,
/// Optional description of the project
pub description: Option<Option<String>>,
/// Default working directory for sessions in this project
#[schema(value_type = String)]
pub default_directory: Option<std::path::PathBuf>,
}
#[derive(Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProjectListResponse {
/// List of available project metadata objects
pub projects: Vec<ProjectMetadata>,
}
#[derive(Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProjectResponse {
/// Project details
pub project: Project,
}
#[utoipa::path(
get,
path = "/projects",
responses(
(status = 200, description = "List of available projects retrieved successfully", body = ProjectListResponse),
(status = 401, description = "Unauthorized - Invalid or missing API key"),
(status = 500, description = "Internal server error")
),
security(
("api_key" = [])
),
tag = "Project Management"
)]
// List all available projects
async fn list_projects(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<ProjectListResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
let projects =
goose::project::list_projects().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(ProjectListResponse { projects }))
}
#[utoipa::path(
get,
path = "/projects/{project_id}",
params(
("project_id" = String, Path, description = "Unique identifier for the project")
),
responses(
(status = 200, description = "Project details retrieved successfully", body = ProjectResponse),
(status = 401, description = "Unauthorized - Invalid or missing API key"),
(status = 404, description = "Project not found"),
(status = 500, description = "Internal server error")
),
security(
("api_key" = [])
),
tag = "Project Management"
)]
// Get a specific project details
async fn get_project_details(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Path(project_id): Path<String>,
) -> Result<Json<ProjectResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
let project = goose::project::get_project(&project_id).map_err(|e| {
if e.to_string().contains("not found") {
StatusCode::NOT_FOUND
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
})?;
Ok(Json(ProjectResponse { project }))
}
#[utoipa::path(
post,
path = "/projects",
request_body = CreateProjectRequest,
responses(
(status = 201, description = "Project created successfully", body = ProjectResponse),
(status = 401, description = "Unauthorized - Invalid or missing API key"),
(status = 400, description = "Invalid request - Bad input parameters"),
(status = 500, description = "Internal server error")
),
security(
("api_key" = [])
),
tag = "Project Management"
)]
// Create a new project
async fn create_project(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(create_req): Json<CreateProjectRequest>,
) -> Result<Json<ProjectResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
// Validate input
if create_req.name.trim().is_empty() {
return Err(StatusCode::BAD_REQUEST);
}
let project = goose::project::create_project(
create_req.name,
create_req.description,
create_req.default_directory,
)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(ProjectResponse { project }))
}
#[utoipa::path(
put,
path = "/projects/{project_id}",
params(
("project_id" = String, Path, description = "Unique identifier for the project")
),
request_body = UpdateProjectRequest,
responses(
(status = 200, description = "Project updated successfully", body = ProjectResponse),
(status = 401, description = "Unauthorized - Invalid or missing API key"),
(status = 404, description = "Project not found"),
(status = 500, description = "Internal server error")
),
security(
("api_key" = [])
),
tag = "Project Management"
)]
// Update a project
async fn update_project(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Path(project_id): Path<String>,
Json(update_req): Json<UpdateProjectRequest>,
) -> Result<Json<ProjectResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
let project = goose::project::update_project(
&project_id,
update_req.name,
update_req.description,
update_req.default_directory,
)
.map_err(|e| {
if e.to_string().contains("not found") {
StatusCode::NOT_FOUND
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
})?;
Ok(Json(ProjectResponse { project }))
}
#[utoipa::path(
delete,
path = "/projects/{project_id}",
params(
("project_id" = String, Path, description = "Unique identifier for the project")
),
responses(
(status = 204, description = "Project deleted successfully"),
(status = 401, description = "Unauthorized - Invalid or missing API key"),
(status = 404, description = "Project not found"),
(status = 500, description = "Internal server error")
),
security(
("api_key" = [])
),
tag = "Project Management"
)]
// Delete a project
async fn delete_project(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Path(project_id): Path<String>,
) -> Result<StatusCode, StatusCode> {
verify_secret_key(&headers, &state)?;
goose::project::delete_project(&project_id).map_err(|e| {
if e.to_string().contains("not found") {
StatusCode::NOT_FOUND
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
})?;
Ok(StatusCode::NO_CONTENT)
}
#[utoipa::path(
post,
path = "/projects/{project_id}/sessions/{session_id}",
params(
("project_id" = String, Path, description = "Unique identifier for the project"),
("session_id" = String, Path, description = "Unique identifier for the session to add")
),
responses(
(status = 204, description = "Session added to project successfully"),
(status = 401, description = "Unauthorized - Invalid or missing API key"),
(status = 404, description = "Project or session not found"),
(status = 500, description = "Internal server error")
),
security(
("api_key" = [])
),
tag = "Project Management"
)]
// Add session to project
async fn add_session_to_project(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Path((project_id, session_id)): Path<(String, String)>,
) -> Result<StatusCode, StatusCode> {
verify_secret_key(&headers, &state)?;
// Add the session to project
goose::project::add_session_to_project(&project_id, &session_id).map_err(|e| {
if e.to_string().contains("not found") {
StatusCode::NOT_FOUND
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
})?;
// Also update session metadata to include the project_id
let session_path =
goose::session::get_path(goose::session::Identifier::Name(session_id.clone()))
.map_err(|_| StatusCode::NOT_FOUND)?;
let mut metadata =
goose::session::read_metadata(&session_path).map_err(|_| StatusCode::NOT_FOUND)?;
metadata.project_id = Some(project_id);
tokio::task::spawn(async move {
if let Err(e) = goose::session::update_metadata(&session_path, &metadata).await {
tracing::error!("Failed to update session metadata: {}", e);
}
});
Ok(StatusCode::NO_CONTENT)
}
#[utoipa::path(
delete,
path = "/projects/{project_id}/sessions/{session_id}",
params(
("project_id" = String, Path, description = "Unique identifier for the project"),
("session_id" = String, Path, description = "Unique identifier for the session to remove")
),
responses(
(status = 204, description = "Session removed from project successfully"),
(status = 401, description = "Unauthorized - Invalid or missing API key"),
(status = 404, description = "Project or session not found"),
(status = 500, description = "Internal server error")
),
security(
("api_key" = [])
),
tag = "Project Management"
)]
// Remove session from project
async fn remove_session_from_project(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Path((project_id, session_id)): Path<(String, String)>,
) -> Result<StatusCode, StatusCode> {
verify_secret_key(&headers, &state)?;
// Remove from project
goose::project::remove_session_from_project(&project_id, &session_id).map_err(|e| {
if e.to_string().contains("not found") {
StatusCode::NOT_FOUND
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
})?;
// Also update session metadata to remove the project_id
let session_path =
goose::session::get_path(goose::session::Identifier::Name(session_id.clone()))
.map_err(|_| StatusCode::NOT_FOUND)?;
let mut metadata =
goose::session::read_metadata(&session_path).map_err(|_| StatusCode::NOT_FOUND)?;
// Only update if this session was actually in this project
if metadata.project_id.as_deref() == Some(&project_id) {
metadata.project_id = None;
tokio::task::spawn(async move {
if let Err(e) = goose::session::update_metadata(&session_path, &metadata).await {
tracing::error!("Failed to update session metadata: {}", e);
}
});
}
Ok(StatusCode::NO_CONTENT)
}
// Configure routes for this module
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/projects", get(list_projects))
.route("/projects", post(create_project))
.route("/projects/{project_id}", get(get_project_details))
.route("/projects/{project_id}", put(update_project))
.route("/projects/{project_id}", delete(delete_project))
.route(
"/projects/{project_id}/sessions/{session_id}",
post(add_session_to_project),
)
.route(
"/projects/{project_id}/sessions/{session_id}",
delete(remove_session_from_project),
)
.with_state(state)
}
@@ -262,7 +262,6 @@ mod tests {
working_dir: PathBuf::from(working_dir),
description: "Test session".to_string(),
schedule_id: Some("test_job".to_string()),
project_id: None,
total_tokens: Some(100),
input_tokens: Some(50),
output_tokens: Some(50),
@@ -651,7 +650,6 @@ mod tests {
comprehensive_metadata.schedule_id,
Some("test_job".to_string())
);
assert!(comprehensive_metadata.project_id.is_none());
assert_eq!(comprehensive_metadata.total_tokens, Some(100));
assert_eq!(comprehensive_metadata.input_tokens, Some(50));
assert_eq!(comprehensive_metadata.output_tokens, Some(50));
-1
View File
@@ -5,7 +5,6 @@ pub mod conversation;
pub mod model;
pub mod oauth;
pub mod permission;
pub mod project;
pub mod prompt_template;
pub mod providers;
pub mod recipe;
-68
View File
@@ -1,68 +0,0 @@
pub mod storage;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use utoipa::ToSchema;
/// Main project structure that holds project metadata and associated sessions
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct Project {
/// Unique identifier for the project
pub id: String,
/// Display name of the project
pub name: String,
/// Optional description of the project
pub description: Option<String>,
/// Default working directory for sessions in this project
#[schema(value_type = String, example = "/home/user/projects/my-project")]
pub default_directory: PathBuf,
/// When the project was created
pub created_at: DateTime<Utc>,
/// When the project was last updated
pub updated_at: DateTime<Utc>,
/// List of session IDs associated with this project
pub session_ids: Vec<String>,
}
/// Simplified project metadata for listing
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProjectMetadata {
/// Unique identifier for the project
pub id: String,
/// Display name of the project
pub name: String,
/// Optional description of the project
pub description: Option<String>,
/// Default working directory for sessions in this project
#[schema(value_type = String)]
pub default_directory: PathBuf,
/// Number of sessions in this project
pub session_count: usize,
/// When the project was created
pub created_at: DateTime<Utc>,
/// When the project was last updated
pub updated_at: DateTime<Utc>,
}
impl From<&Project> for ProjectMetadata {
fn from(project: &Project) -> Self {
ProjectMetadata {
id: project.id.clone(),
name: project.name.clone(),
description: project.description.clone(),
default_directory: project.default_directory.clone(),
session_count: project.session_ids.len(),
created_at: project.created_at,
updated_at: project.updated_at,
}
}
}
// Re-export storage functions
pub use storage::{
add_session_to_project, create_project, delete_project, ensure_project_dir, get_project,
list_projects, remove_session_from_project, update_project,
};
-239
View File
@@ -1,239 +0,0 @@
use crate::project::{Project, ProjectMetadata};
use anyhow::{anyhow, Context, Result};
use chrono::Utc;
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
use serde_json;
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use tracing::{error, info};
const APP_NAME: &str = "goose";
/// Ensure the project directory exists and return its path
pub fn ensure_project_dir() -> Result<PathBuf> {
let app_strategy = AppStrategyArgs {
top_level_domain: "Block".to_string(),
author: "Block".to_string(),
app_name: APP_NAME.to_string(),
};
let data_dir = choose_app_strategy(app_strategy)
.context("goose requires a home dir")?
.data_dir()
.join("projects");
if !data_dir.exists() {
fs::create_dir_all(&data_dir)?;
}
Ok(data_dir)
}
/// Generate a unique project ID
fn generate_project_id() -> String {
use rand::Rng;
let timestamp = Utc::now().timestamp();
let random: u32 = rand::thread_rng().gen();
format!("proj_{}_{}", timestamp, random)
}
/// Get the path for a specific project file
fn get_project_path(project_id: &str) -> Result<PathBuf> {
let project_dir = ensure_project_dir()?;
Ok(project_dir.join(format!("{}.json", project_id)))
}
/// Create a new project
pub fn create_project(
name: String,
description: Option<String>,
default_directory: PathBuf,
) -> Result<Project> {
let project_dir = ensure_project_dir()?;
// Validate the default directory exists
if !default_directory.exists() {
return Err(anyhow!(
"Default directory does not exist: {:?}",
default_directory
));
}
let now = Utc::now();
let project = Project {
id: generate_project_id(),
name,
description,
default_directory,
created_at: now,
updated_at: now,
session_ids: Vec::new(),
};
// Save the project
let project_path = project_dir.join(format!("{}.json", project.id));
let mut file = File::create(&project_path)?;
let json = serde_json::to_string_pretty(&project)?;
file.write_all(json.as_bytes())?;
info!("Created project {} at {:?}", project.id, project_path);
Ok(project)
}
/// Update an existing project
pub fn update_project(
project_id: &str,
name: Option<String>,
description: Option<Option<String>>,
default_directory: Option<PathBuf>,
) -> Result<Project> {
let project_path = get_project_path(project_id)?;
if !project_path.exists() {
return Err(anyhow!("Project not found: {}", project_id));
}
// Read existing project
let mut project: Project = serde_json::from_reader(File::open(&project_path)?)?;
// Update fields
if let Some(new_name) = name {
project.name = new_name;
}
if let Some(new_description) = description {
project.description = new_description;
}
if let Some(new_directory) = default_directory {
if !new_directory.exists() {
return Err(anyhow!(
"Default directory does not exist: {:?}",
new_directory
));
}
project.default_directory = new_directory;
}
project.updated_at = Utc::now();
// Save updated project
let mut file = File::create(&project_path)?;
let json = serde_json::to_string_pretty(&project)?;
file.write_all(json.as_bytes())?;
info!("Updated project {}", project_id);
Ok(project)
}
/// Delete a project (does not delete associated sessions)
pub fn delete_project(project_id: &str) -> Result<()> {
let project_path = get_project_path(project_id)?;
if !project_path.exists() {
return Err(anyhow!("Project not found: {}", project_id));
}
fs::remove_file(&project_path)?;
info!("Deleted project {}", project_id);
Ok(())
}
/// List all projects
pub fn list_projects() -> Result<Vec<ProjectMetadata>> {
let project_dir = ensure_project_dir()?;
let mut projects = Vec::new();
if let Ok(entries) = fs::read_dir(&project_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
match serde_json::from_reader::<_, Project>(File::open(&path)?) {
Ok(project) => {
projects.push(ProjectMetadata::from(&project));
}
Err(e) => {
error!("Failed to read project file {:?}: {}", path, e);
}
}
}
}
}
// Sort by updated_at descending
projects.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
Ok(projects)
}
/// Get a specific project
pub fn get_project(project_id: &str) -> Result<Project> {
let project_path = get_project_path(project_id)?;
if !project_path.exists() {
return Err(anyhow!("Project not found: {}", project_id));
}
let project: Project = serde_json::from_reader(File::open(&project_path)?)?;
Ok(project)
}
/// Add a session to a project
pub fn add_session_to_project(project_id: &str, session_id: &str) -> Result<()> {
let project_path = get_project_path(project_id)?;
if !project_path.exists() {
return Err(anyhow!("Project not found: {}", project_id));
}
// Read project
let mut project: Project = serde_json::from_reader(File::open(&project_path)?)?;
// Check if session already exists in project
if project.session_ids.contains(&session_id.to_string()) {
return Ok(()); // Already added
}
// Add session and update timestamp
project.session_ids.push(session_id.to_string());
project.updated_at = Utc::now();
// Save updated project
let mut file = File::create(&project_path)?;
let json = serde_json::to_string_pretty(&project)?;
file.write_all(json.as_bytes())?;
info!("Added session {} to project {}", session_id, project_id);
Ok(())
}
/// Remove a session from a project
pub fn remove_session_from_project(project_id: &str, session_id: &str) -> Result<()> {
let project_path = get_project_path(project_id)?;
if !project_path.exists() {
return Err(anyhow!("Project not found: {}", project_id));
}
// Read project
let mut project: Project = serde_json::from_reader(File::open(&project_path)?)?;
// Remove session
let original_len = project.session_ids.len();
project.session_ids.retain(|id| id != session_id);
if project.session_ids.len() == original_len {
return Ok(()); // Session wasn't in project
}
project.updated_at = Utc::now();
// Save updated project
let mut file = File::create(&project_path)?;
let json = serde_json::to_string_pretty(&project)?;
file.write_all(json.as_bytes())?;
info!("Removed session {} from project {}", session_id, project_id);
Ok(())
}
-1
View File
@@ -1291,7 +1291,6 @@ async fn run_scheduled_job_internal(
working_dir: current_dir.clone(),
description: String::new(),
schedule_id: Some(job.id.clone()),
project_id: None,
message_count: all_session_messages.len(),
total_tokens: None,
input_tokens: None,
+1 -5
View File
@@ -49,8 +49,7 @@ pub struct SessionMetadata {
pub description: String,
/// ID of the schedule that triggered this session, if any
pub schedule_id: Option<String>,
/// ID of the project this session belongs to, if any
pub project_id: Option<String>,
/// Number of messages in the session
pub message_count: usize,
/// The total number of tokens used in the session. Retrieved from the provider's last usage.
@@ -78,7 +77,6 @@ impl<'de> Deserialize<'de> for SessionMetadata {
description: String,
message_count: usize,
schedule_id: Option<String>, // For backward compatibility
project_id: Option<String>, // For backward compatibility
total_tokens: Option<i32>,
input_tokens: Option<i32>,
output_tokens: Option<i32>,
@@ -100,7 +98,6 @@ impl<'de> Deserialize<'de> for SessionMetadata {
description: helper.description,
message_count: helper.message_count,
schedule_id: helper.schedule_id,
project_id: helper.project_id,
total_tokens: helper.total_tokens,
input_tokens: helper.input_tokens,
output_tokens: helper.output_tokens,
@@ -125,7 +122,6 @@ impl SessionMetadata {
working_dir,
description: String::new(),
schedule_id: None,
project_id: None,
message_count: 0,
total_tokens: None,
input_tokens: None,
-1
View File
@@ -405,7 +405,6 @@ pub fn create_test_session_metadata(message_count: usize, working_dir: &str) ->
working_dir: PathBuf::from(working_dir),
description: "Test session".to_string(),
schedule_id: Some("test_job".to_string()),
project_id: None,
total_tokens: Some(100),
input_tokens: Some(50),
output_tokens: Some(50),