Skip to content

Commit 1c63fd0

Browse files
author
Greyforge Admin
committed
Import YAML session exports
1 parent 7954d02 commit 1c63fd0

2 files changed

Lines changed: 90 additions & 39 deletions

File tree

src/cortex-cli/src/import_cmd.rs

Lines changed: 49 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Session import command for Cortex CLI.
22
//!
3-
//! Imports a session from a portable JSON format (exported or shared).
3+
//! Imports a session from a portable JSON or YAML format (exported or shared).
44
55
use anyhow::{Context, Result, bail};
66
use clap::Parser;
@@ -21,10 +21,10 @@ use crate::export_cmd::{ExportMessage, SessionExport};
2121
/// Maximum depth for processing messages to prevent stack overflow from deeply nested structures.
2222
const MAX_PROCESSING_DEPTH: usize = 10000;
2323

24-
/// Import a session from JSON format.
24+
/// Import a session from JSON or YAML format.
2525
#[derive(Debug, Parser)]
2626
pub struct ImportCommand {
27-
/// Path to the JSON file to import, URL to fetch, or "-" for stdin
27+
/// Path to the JSON/YAML file to import, URL to fetch, or "-" for stdin
2828
#[arg(value_name = "FILE_OR_URL")]
2929
pub source: String,
3030

@@ -50,7 +50,7 @@ impl ImportCommand {
5050
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
5151

5252
// Read the export data
53-
let (json_content, is_from_url) = if self.source == "-" {
53+
let (export_content, is_from_url) = if self.source == "-" {
5454
// Read from stdin
5555
use std::io::Read;
5656
let mut content = String::new();
@@ -70,41 +70,7 @@ impl ImportCommand {
7070
};
7171

7272
// Parse the export with helpful error messages
73-
let export: SessionExport = serde_json::from_str(&json_content).map_err(|e| {
74-
// Create a helpful error message with content preview
75-
let preview_len = json_content.len().min(200);
76-
let content_preview = &json_content[..preview_len];
77-
let truncated = if json_content.len() > 200 {
78-
"..."
79-
} else {
80-
""
81-
};
82-
83-
let source_type = if is_from_url { "URL" } else { "file" };
84-
85-
// Detect common non-JSON content types
86-
let hint = if content_preview.trim_start().starts_with("<!DOCTYPE")
87-
|| content_preview.trim_start().starts_with("<html")
88-
{
89-
"\nHint: The URL returned HTML content, not JSON. Make sure the URL points directly to a JSON export file."
90-
} else if content_preview.trim_start().starts_with("<?xml") {
91-
"\nHint: The URL returned XML content, not JSON. Make sure the URL points directly to a JSON export file."
92-
} else if content_preview.is_empty() {
93-
"\nHint: The response was empty. Make sure the URL is accessible and returns JSON content."
94-
} else {
95-
"\nHint: Ensure the file contains valid JSON. Check for syntax errors like missing commas, unclosed brackets, or invalid characters."
96-
};
97-
98-
anyhow::anyhow!(
99-
"Failed to parse JSON from {}: {}\n\nReceived content (first {} bytes):\n{}{}\n{}",
100-
source_type,
101-
e,
102-
preview_len,
103-
content_preview,
104-
truncated,
105-
hint
106-
)
107-
})?;
73+
let export = parse_session_export(&export_content, is_from_url)?;
10874

10975
// Validate version
11076
if export.version != 1 {
@@ -236,6 +202,50 @@ impl ImportCommand {
236202
}
237203
}
238204

205+
fn parse_session_export(content: &str, is_from_url: bool) -> Result<SessionExport> {
206+
match serde_json::from_str(content) {
207+
Ok(export) => return Ok(export),
208+
Err(json_err) => {
209+
let yaml_err = match serde_yaml::from_str(content) {
210+
Ok(export) => return Ok(export),
211+
Err(err) => err,
212+
};
213+
214+
let content_preview: String = content.chars().take(200).collect();
215+
let truncated = if content.chars().count() > 200 {
216+
"..."
217+
} else {
218+
""
219+
};
220+
221+
let source_type = if is_from_url { "URL" } else { "file" };
222+
223+
// Detect common non-export content types
224+
let hint = if content_preview.trim_start().starts_with("<!DOCTYPE")
225+
|| content_preview.trim_start().starts_with("<html")
226+
{
227+
"\nHint: The URL returned HTML content, not a JSON or YAML export file. Make sure the URL points directly to a session export file."
228+
} else if content_preview.trim_start().starts_with("<?xml") {
229+
"\nHint: The URL returned XML content, not a JSON or YAML export file. Make sure the URL points directly to a session export file."
230+
} else if content_preview.is_empty() {
231+
"\nHint: The response was empty. Make sure the URL is accessible and returns a JSON or YAML export file."
232+
} else {
233+
"\nHint: Ensure the file contains valid JSON or YAML exported by `cortex export`."
234+
};
235+
236+
bail!(
237+
"Failed to parse session export from {} as JSON or YAML. JSON error: {}. YAML error: {}\n\nReceived content (first 200 characters):\n{}{}\n{}",
238+
source_type,
239+
json_err,
240+
yaml_err,
241+
content_preview,
242+
truncated,
243+
hint
244+
)
245+
}
246+
}
247+
}
248+
239249
/// Fetch content from a URL.
240250
async fn fetch_url(url: &str) -> Result<String> {
241251
// Use curl for fetching
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use std::process::Command;
2+
3+
use tempfile::tempdir;
4+
5+
#[test]
6+
fn import_accepts_yaml_session_export() {
7+
let home_dir = tempdir().unwrap();
8+
let export_file = home_dir.path().join("session.yaml");
9+
std::fs::write(
10+
&export_file,
11+
r#"version: 1
12+
session:
13+
id: "550e8400-e29b-41d4-a716-446655440000"
14+
title: "YAML import"
15+
created_at: "2024-01-01T00:00:00Z"
16+
cwd: "/tmp"
17+
model: "test-model"
18+
messages:
19+
- role: "user"
20+
content: "hello from yaml"
21+
"#,
22+
)
23+
.unwrap();
24+
25+
let output = Command::new(env!("CARGO_BIN_EXE_Cortex"))
26+
.args(["import", export_file.to_str().unwrap()])
27+
.env("HOME", home_dir.path())
28+
.env_remove("CORTEX_HOME")
29+
.output()
30+
.unwrap();
31+
32+
let stdout = String::from_utf8_lossy(&output.stdout);
33+
let stderr = String::from_utf8_lossy(&output.stderr);
34+
let combined = format!("{stdout}{stderr}");
35+
assert!(
36+
output.status.success(),
37+
"import failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
38+
);
39+
assert!(combined.contains("Imported session as:"));
40+
assert!(combined.contains("Messages: 1"));
41+
}

0 commit comments

Comments
 (0)