Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions crates/goose-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -558,9 +558,13 @@ enum SessionCommand {
)]
relays: Vec<String>,
},
#[command(about = "Import a session from JSON or an encrypted Nostr share link")]
#[command(
about = "Import a session from JSON, a Claude Code / Codex / Pi .jsonl, or an encrypted Nostr share link"
)]
Import {
#[arg(help = "Path to a JSON session export, or a goose://sessions/nostr share link")]
#[arg(
help = "Path to a goose session export, a Claude Code, Codex, or Pi .jsonl transcript, or a goose://sessions/nostr share link"
)]
input: String,

#[arg(long = "nostr", help = "Treat input as an encrypted Nostr share link")]
Expand Down
9 changes: 9 additions & 0 deletions crates/goose-cli/src/commands/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,15 @@ pub async fn handle_session_import(input: String, nostr: bool) -> Result<()> {
.with_context(|| format!("Failed to read session import file: {input}"))?
};

let format = goose::session::import_formats::detect_format(&json);
let label = match format {
goose::session::import_formats::ImportFormat::Goose => "goose",
goose::session::import_formats::ImportFormat::ClaudeCode => "Claude Code",
goose::session::import_formats::ImportFormat::Codex => "Codex",
goose::session::import_formats::ImportFormat::Pi => "Pi",
};
println!("Detected format: {}", label);

let session_manager = SessionManager::instance();
let session = session_manager
.import_session(&json, Some(SessionType::User))
Expand Down
24 changes: 16 additions & 8 deletions crates/goose/src/providers/formats/openai_responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,27 @@ fn reasoning_from_summary(summary: &[SummaryText]) -> Option<MessageContent> {
#[serde(rename_all = "snake_case")]
pub enum ResponseOutputItem {
Reasoning {
id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
id: Option<String>,
#[serde(default)]
summary: Vec<SummaryText>,
},
Message {
id: String,
status: String,
// `id` and `status` are required when the OpenAI API emits these
// items, but Codex rollout files (which reuse the same shape on
// disk) sometimes omit them. Keep deserialization permissive.
#[serde(default, skip_serializing_if = "Option::is_none")]
id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
status: Option<String>,
role: String,
content: Vec<ResponseContentBlock>,
},
FunctionCall {
id: String,
status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
call_id: Option<String>,
name: String,
Expand Down Expand Up @@ -658,7 +666,7 @@ pub fn responses_api_to_message(response: &ResponsesApiResponse) -> anyhow::Resu
arguments,
..
} => {
let request_id = call_id.as_ref().unwrap_or(id).clone();
let request_id = call_id.clone().or_else(|| id.clone()).unwrap_or_default();
let parsed_args = if arguments.is_empty() {
json!({})
} else {
Expand Down Expand Up @@ -1178,8 +1186,8 @@ mod tests {
status: "completed".to_string(),
model: "gpt-5.3-codex".to_string(),
output: vec![ResponseOutputItem::FunctionCall {
id: "fc_123".to_string(),
status: "completed".to_string(),
id: Some("fc_123".to_string()),
status: Some("completed".to_string()),
call_id: Some("call_abc".to_string()),
name: "test__get_person_zip_code".to_string(),
arguments: r#"{"name":"Alice Burns"}"#.to_string(),
Expand Down
Loading
Loading