@@ -135,7 +135,7 @@ enum SessionCommand {
|
||||
#[arg(short, long, help = "Regex for removing matched sessions (optional)")]
|
||||
regex: Option<String>,
|
||||
},
|
||||
#[command(about = "Export a session to Markdown format")]
|
||||
#[command(about = "Export a session")]
|
||||
Export {
|
||||
#[command(flatten)]
|
||||
identifier: Option<Identifier>,
|
||||
@@ -156,6 +156,16 @@ enum SessionCommand {
|
||||
)]
|
||||
format: String,
|
||||
},
|
||||
#[command(name = "diagnostics")]
|
||||
Diagnostics {
|
||||
/// Session ID to generate diagnostics for
|
||||
#[arg(short, long)]
|
||||
session_id: String,
|
||||
|
||||
/// Output path for the diagnostics zip file (optional, defaults to current directory)
|
||||
#[arg(short, long)]
|
||||
output: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
@@ -847,6 +857,10 @@ pub async fn cli() -> Result<()> {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
Some(SessionCommand::Diagnostics { session_id, output }) => {
|
||||
crate::commands::session::handle_diagnostics(&session_id, output).await?;
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
let session_start = std::time::Instant::now();
|
||||
let session_type = if resume { "resumed" } else { "new" };
|
||||
|
||||
@@ -2,10 +2,11 @@ use crate::session::message_to_markdown;
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use cliclack::{confirm, multiselect, select};
|
||||
use goose::session::{Session, SessionManager};
|
||||
use goose::session::{generate_diagnostics, Session, SessionManager};
|
||||
use goose::utils::safe_truncate;
|
||||
use regex::Regex;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const TRUNCATED_DESC_LENGTH: usize = 60;
|
||||
@@ -204,10 +205,39 @@ pub async fn handle_session_export(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
/// Convert a list of messages to markdown format for session export
|
||||
///
|
||||
/// This function handles the formatting of a complete session including headers,
|
||||
/// message organization, and proper tool request/response pairing.
|
||||
|
||||
pub async fn handle_diagnostics(session_id: &str, output_path: Option<PathBuf>) -> Result<()> {
|
||||
println!(
|
||||
"Generating diagnostics bundle for session '{}'...",
|
||||
session_id
|
||||
);
|
||||
|
||||
let diagnostics_data = generate_diagnostics(session_id).await.with_context(|| {
|
||||
format!(
|
||||
"Failed to write to generate diagnostics bundle for session '{}'",
|
||||
session_id
|
||||
)
|
||||
})?;
|
||||
|
||||
let output_file = if let Some(path) = output_path {
|
||||
path.clone()
|
||||
} else {
|
||||
PathBuf::from(format!("diagnostics_{}.zip", session_id))
|
||||
};
|
||||
|
||||
let mut file = fs::File::create(&output_file).context(format!(
|
||||
"Failed to create output file: {}",
|
||||
output_file.display()
|
||||
))?;
|
||||
|
||||
file.write_all(&diagnostics_data)
|
||||
.context("Failed to write diagnostics data")?;
|
||||
|
||||
println!("Diagnostics bundle saved to: {}", output_file.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn export_session_to_markdown(
|
||||
messages: Vec<goose::conversation::message::Message>,
|
||||
session_name: &String,
|
||||
|
||||
@@ -33,7 +33,6 @@ base64 = "0.21"
|
||||
config = { version = "0.14.1", features = ["toml"] }
|
||||
thiserror = "1.0"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
etcetera = "0.8.0"
|
||||
serde_yaml = "0.9.34"
|
||||
utoipa = { version = "4.1", features = ["axum_extras", "chrono"] }
|
||||
reqwest = { version = "0.12.9", features = ["json", "rustls-tls", "blocking", "multipart"], default-features = false }
|
||||
|
||||
@@ -322,7 +322,8 @@ derive_utoipa!(Icon as IconSchema);
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
super::routes::health::status,
|
||||
super::routes::status::status,
|
||||
super::routes::status::diagnostics,
|
||||
super::routes::config_management::backup_config,
|
||||
super::routes::config_management::recover_config,
|
||||
super::routes::config_management::validate_config,
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
use axum::{routing::get, Router};
|
||||
|
||||
#[utoipa::path(get, path = "/status",
|
||||
responses(
|
||||
(status = 200, description = "ok", body = String),
|
||||
)
|
||||
)]
|
||||
async fn status() -> String {
|
||||
"ok".to_string()
|
||||
}
|
||||
|
||||
pub fn routes() -> Router {
|
||||
Router::new().route("/status", get(status))
|
||||
}
|
||||
@@ -4,13 +4,13 @@ pub mod config_management;
|
||||
pub mod context;
|
||||
pub mod errors;
|
||||
pub mod extension;
|
||||
pub mod health;
|
||||
pub mod recipe;
|
||||
pub mod recipe_utils;
|
||||
pub mod reply;
|
||||
pub mod schedule;
|
||||
pub mod session;
|
||||
pub mod setup;
|
||||
pub mod status;
|
||||
pub mod utils;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -19,7 +19,7 @@ use axum::Router;
|
||||
// Function to configure all routes
|
||||
pub fn configure(state: Arc<crate::state::AppState>) -> Router {
|
||||
Router::new()
|
||||
.merge(health::routes())
|
||||
.merge(status::routes())
|
||||
.merge(reply::routes(state.clone()))
|
||||
.merge(agent::routes(state.clone()))
|
||||
.merge(audio::routes(state.clone()))
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
use axum::body::Body;
|
||||
use axum::http::HeaderValue;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{extract::Path, http::StatusCode, routing::get, Router};
|
||||
use goose::session::generate_diagnostics;
|
||||
|
||||
#[utoipa::path(get, path = "/status",
|
||||
responses(
|
||||
(status = 200, description = "ok", body = String),
|
||||
)
|
||||
)]
|
||||
async fn status() -> String {
|
||||
"ok".to_string()
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/diagnostics/{session_id}",
|
||||
responses(
|
||||
(status = 200, description = "Diagnostics zip file", content_type = "application/zip", body = Vec<u8>),
|
||||
(status = 500, description = "Failed to generate diagnostics"),
|
||||
)
|
||||
)]
|
||||
async fn diagnostics(Path(session_id): Path<String>) -> impl IntoResponse {
|
||||
match generate_diagnostics(&session_id).await {
|
||||
Ok(zip_data) => {
|
||||
let filename = format!("attachment; filename=\"diagnostics_{}.zip\"", session_id);
|
||||
let headers = [
|
||||
(
|
||||
http::header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/zip"),
|
||||
),
|
||||
(
|
||||
http::header::CONTENT_DISPOSITION,
|
||||
HeaderValue::from_str(&filename).map_err(|_e| StatusCode::BAD_REQUEST)?,
|
||||
),
|
||||
];
|
||||
|
||||
Ok((headers, Body::from(zip_data)))
|
||||
}
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
pub fn routes() -> Router {
|
||||
Router::new()
|
||||
.route("/status", get(status))
|
||||
.route("/diagnostics/{session_id}", get(diagnostics))
|
||||
}
|
||||
@@ -99,11 +99,14 @@ dashmap = "6.1"
|
||||
ahash = "0.8"
|
||||
tokio-util = "0.7.15"
|
||||
unicode-normalization = "0.1"
|
||||
zip = "0.6"
|
||||
sys-info = "0.9"
|
||||
|
||||
oauth2 = "5.0.0"
|
||||
schemars = { version = "1.0.4", default-features = false, features = ["derive"] }
|
||||
insta = "1.43.2"
|
||||
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["wincred"] }
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
use crate::config::paths::Paths;
|
||||
use crate::session::SessionManager;
|
||||
use std::fs::{self};
|
||||
use std::io::Cursor;
|
||||
use std::io::Write;
|
||||
use zip::write::FileOptions;
|
||||
use zip::ZipWriter;
|
||||
|
||||
pub async fn generate_diagnostics(session_id: &str) -> anyhow::Result<Vec<u8>> {
|
||||
let logs_dir = Paths::in_state_dir("logs");
|
||||
let config_dir = Paths::config_dir();
|
||||
let config_path = config_dir.join("config.yaml");
|
||||
|
||||
let system_info = format!(
|
||||
"App Version: {}\n\
|
||||
OS: {}\n\
|
||||
OS Version: {}\n\
|
||||
Architecture: {}\n\
|
||||
Timestamp: {}\n",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
std::env::consts::OS,
|
||||
sys_info::os_release().unwrap_or_else(|_| "unknown".to_string()),
|
||||
std::env::consts::ARCH,
|
||||
chrono::Utc::now().to_rfc3339()
|
||||
);
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
{
|
||||
let mut zip = ZipWriter::new(Cursor::new(&mut buffer));
|
||||
let options = FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
|
||||
|
||||
let mut log_files: Vec<_> = fs::read_dir(&logs_dir)?
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path().extension().is_some_and(|ext| ext == "jsonl"))
|
||||
.collect();
|
||||
|
||||
log_files.sort_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok()));
|
||||
|
||||
for entry in log_files.iter().rev().take(3) {
|
||||
let path = entry.path();
|
||||
let name = path.file_name().unwrap().to_str().unwrap();
|
||||
zip.start_file(format!("logs/{}", name), options)?;
|
||||
zip.write_all(&fs::read(&path)?)?;
|
||||
}
|
||||
|
||||
let session_data = SessionManager::export_session(session_id).await?;
|
||||
zip.start_file("session.json", options)?;
|
||||
zip.write_all(session_data.as_bytes())?;
|
||||
|
||||
if config_path.exists() {
|
||||
zip.start_file("config.yaml", options)?;
|
||||
zip.write_all(&fs::read(&config_path)?)?;
|
||||
}
|
||||
|
||||
zip.start_file("system.txt", options)?;
|
||||
zip.write_all(system_info.as_bytes())?;
|
||||
|
||||
zip.finish()?;
|
||||
}
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
mod diagnostics;
|
||||
pub mod extension_data;
|
||||
mod legacy;
|
||||
pub mod session_manager;
|
||||
|
||||
pub use diagnostics::generate_diagnostics;
|
||||
pub use extension_data::{EnabledExtensionsState, ExtensionData, ExtensionState, TodoState};
|
||||
pub use session_manager::{Session, SessionInsights, SessionManager};
|
||||
|
||||
Reference in New Issue
Block a user