feat: Import OpenClaw memory, history and settings (#903)

* feat: Import OpenClaw memory, history and settings

* review fixes

* fix: address remaining code quality issues

1. Remove dead import_conversation() function - replaced by import_conversation_atomic()
2. Improve non-UTF-8 filename handling in list_agent_dbs() - log warning instead of silent 'unknown'
3. Remove emojis from CLI output per project style guide

Co-Authored-By: Claude Haiku 4.5 <[email protected]>

---------

Co-authored-by: Claude Haiku 4.5 <[email protected]>
This commit is contained in:
Nick Pismenkov
2026-03-10 18:37:10 -07:00
committed by GitHub
co-authored by Claude Haiku 4.5
parent b0214fef41
commit 26068db24b
23 changed files with 3753 additions and 1 deletions
+162
View File
@@ -0,0 +1,162 @@
//! Import command for migrating data from other AI systems.
use std::path::PathBuf;
use std::sync::Arc;
use clap::Subcommand;
#[cfg(feature = "import")]
use crate::import::ImportOptions;
#[cfg(feature = "import")]
use crate::import::openclaw::OpenClawImporter;
/// Import data from other AI systems.
#[derive(Subcommand, Debug, Clone)]
pub enum ImportCommand {
/// Import from OpenClaw (memory, history, settings, credentials)
#[cfg(feature = "import")]
Openclaw {
/// Path to OpenClaw directory (default: ~/.openclaw)
#[arg(long)]
path: Option<PathBuf>,
/// Dry-run mode: show what would be imported without writing
#[arg(long)]
dry_run: bool,
/// Re-embed memory if dimensions don't match target provider
#[arg(long)]
re_embed: bool,
/// User ID for imported data (default: 'default')
#[arg(long)]
user_id: Option<String>,
},
}
/// Run an import command.
#[cfg(feature = "import")]
pub async fn run_import_command(
cmd: &ImportCommand,
config: &crate::config::Config,
) -> anyhow::Result<()> {
match cmd {
ImportCommand::Openclaw {
path,
dry_run,
re_embed,
user_id,
} => run_import_openclaw(config, path.clone(), *dry_run, *re_embed, user_id.clone()).await,
}
}
/// Run the OpenClaw import.
#[cfg(feature = "import")]
async fn run_import_openclaw(
config: &crate::config::Config,
openclaw_path: Option<PathBuf>,
dry_run: bool,
re_embed: bool,
user_id: Option<String>,
) -> anyhow::Result<()> {
use secrecy::SecretString;
// Determine OpenClaw path
let openclaw_path = if let Some(path) = openclaw_path {
path
} else if let Some(path) = OpenClawImporter::detect() {
path
} else {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join(".openclaw")
};
let user_id = user_id.unwrap_or_else(|| "default".to_string());
println!("🔍 OpenClaw Import");
println!(" Path: {}", openclaw_path.display());
println!(" User: {}", user_id);
if dry_run {
println!(" Mode: DRY RUN (no data will be written)");
}
println!();
// Initialize database
let db = crate::db::connect_from_config(&config.database)
.await
.map_err(|e| anyhow::anyhow!("Failed to initialize database: {}", e))?;
// Initialize secrets store with master key from env or keychain
let secrets_crypto = if let Ok(master_key_hex) = std::env::var("SECRETS_MASTER_KEY") {
Arc::new(
crate::secrets::SecretsCrypto::new(SecretString::from(master_key_hex))
.map_err(|e| anyhow::anyhow!("Failed to initialize secrets: {}", e))?,
)
} else {
match crate::secrets::keychain::get_master_key().await {
Ok(key_bytes) => {
let key_hex: String = key_bytes.iter().map(|b| format!("{:02x}", b)).collect();
Arc::new(
crate::secrets::SecretsCrypto::new(SecretString::from(key_hex))
.map_err(|e| anyhow::anyhow!("Failed to initialize secrets: {}", e))?,
)
}
Err(_) => {
return Err(anyhow::anyhow!(
"No secrets master key found. Set SECRETS_MASTER_KEY env var or run 'ironclaw onboard' first."
));
}
}
};
let secrets: Arc<dyn crate::secrets::SecretsStore> = Arc::new(
crate::secrets::InMemorySecretsStore::new(secrets_crypto.clone()),
);
// Initialize workspace
let workspace = crate::workspace::Workspace::new_with_db(user_id.clone(), db.clone());
let opts = ImportOptions {
openclaw_path,
dry_run,
re_embed,
user_id,
};
let importer = OpenClawImporter::new(db, workspace, secrets, opts);
let stats = importer.import().await?;
// Print results
println!("Import Complete");
println!();
println!("Summary:");
println!(" Documents: {}", stats.documents);
println!(" Chunks: {}", stats.chunks);
println!(" Conversations: {}", stats.conversations);
println!(" Messages: {}", stats.messages);
println!(" Settings: {}", stats.settings);
println!(" Secrets: {}", stats.secrets);
if stats.skipped > 0 {
println!(" Skipped: {}", stats.skipped);
}
if stats.re_embed_queued > 0 {
println!(" Re-embed queued: {}", stats.re_embed_queued);
}
println!();
println!("Total imported: {}", stats.total_imported());
if dry_run {
println!();
println!("[DRY RUN] No data was written.");
}
Ok(())
}
#[cfg(not(feature = "import"))]
pub async fn run_import_command(
_cmd: &ImportCommand,
_config: &crate::config::Config,
) -> anyhow::Result<()> {
anyhow::bail!("Import feature not enabled. Compile with --features import")
}
+31
View File
@@ -14,6 +14,8 @@
mod completion;
mod config;
mod doctor;
#[cfg(feature = "import")]
pub mod import;
mod mcp;
pub mod memory;
pub mod oauth_defaults;
@@ -26,6 +28,8 @@ mod tool;
pub use completion::Completion;
pub use config::{ConfigCommand, run_config_command};
pub use doctor::run_doctor_command;
#[cfg(feature = "import")]
pub use import::{ImportCommand, run_import_command};
pub use mcp::{McpCommand, run_mcp_command};
pub use memory::MemoryCommand;
pub use memory::run_memory_command_with_db;
@@ -183,6 +187,15 @@ pub enum Command {
)]
Completion(Completion),
/// Import data from other AI systems
#[cfg(feature = "import")]
#[command(
subcommand,
about = "Import from other AI systems",
long_about = "Migrate data from other AI assistants like OpenClaw.\nExample: ironclaw import openclaw"
)]
Import(ImportCommand),
/// Run as a sandboxed worker inside a Docker container (internal use).
/// This is invoked automatically by the orchestrator, not by users directly.
#[command(hide = true)]
@@ -282,6 +295,7 @@ mod tests {
}
#[test]
#[cfg(feature = "import")]
fn test_help_output() {
let mut cmd = Cli::command();
let help = cmd.render_help().to_string();
@@ -289,9 +303,26 @@ mod tests {
}
#[test]
#[cfg(not(feature = "import"))]
fn test_help_output_without_import() {
let mut cmd = Cli::command();
let help = cmd.render_help().to_string();
assert_snapshot!(help);
}
#[test]
#[cfg(feature = "import")]
fn test_long_help_output() {
let mut cmd = Cli::command();
let help = cmd.render_long_help().to_string();
assert_snapshot!(help);
}
#[test]
#[cfg(not(feature = "import"))]
fn test_long_help_output_without_import() {
let mut cmd = Cli::command();
let help = cmd.render_long_help().to_string();
assert_snapshot!(help);
}
}
@@ -1,5 +1,6 @@
---
source: src/cli/mod.rs
assertion_line: 302
expression: help
---
Secure personal AI assistant that protects your data and expands its capabilities
@@ -19,6 +20,7 @@ Commands:
doctor Run diagnostics
status Show system status
completion Generate completions
import Import from other AI systems
help Print this message or the help of the given subcommand(s)
Options:
@@ -0,0 +1,32 @@
---
source: src/cli/mod.rs
assertion_line: 310
expression: help
---
Secure personal AI assistant that protects your data and expands its capabilities
Usage: ironclaw [OPTIONS] [COMMAND]
Commands:
run Run the AI agent
onboard Run interactive setup wizard
config Manage app configs
tool Manage WASM tools
registry Browse/install extensions
mcp Manage MCP servers
memory Manage workspace memory
pairing Manage DM pairing
service Manage OS service
doctor Run diagnostics
status Show system status
completion Generate completions
help Print this message or the help of the given subcommand(s)
Options:
--cli-only Run in interactive CLI mode only (disable other channels)
--no-db Skip database connection (for testing)
-m, --message <MESSAGE> Single message mode - send one message and exit
-c, --config <CONFIG> Configuration file path (optional, uses env vars by default)
--no-onboard Skip first-run onboarding check
-h, --help Print help (see more with '--help')
-V, --version Print version
@@ -1,5 +1,6 @@
---
source: src/cli/mod.rs
assertion_line: 318
expression: help
---
IronClaw is a secure AI assistant. Use 'ironclaw <subcommand> --help' for details.
@@ -22,6 +23,7 @@ Commands:
doctor Run diagnostics
status Show system status
completion Generate completions
import Import from other AI systems
help Print this message or the help of the given subcommand(s)
Options:
@@ -0,0 +1,48 @@
---
source: src/cli/mod.rs
assertion_line: 326
expression: help
---
IronClaw is a secure AI assistant. Use 'ironclaw <subcommand> --help' for details.
Examples:
ironclaw run # Start the agent
ironclaw config list # List configs
Usage: ironclaw [OPTIONS] [COMMAND]
Commands:
run Run the AI agent
onboard Run interactive setup wizard
config Manage app configs
tool Manage WASM tools
registry Browse/install extensions
mcp Manage MCP servers
memory Manage workspace memory
pairing Manage DM pairing
service Manage OS service
doctor Run diagnostics
status Show system status
completion Generate completions
help Print this message or the help of the given subcommand(s)
Options:
--cli-only
Run in interactive CLI mode only (disable other channels)
--no-db
Skip database connection (for testing)
-m, --message <MESSAGE>
Single message mode - send one message and exit
-c, --config <CONFIG>
Configuration file path (optional, uses env vars by default)
--no-onboard
Skip first-run onboarding check
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version