|
1 | | -//! OpenClaw config migration support |
| 1 | +//! OpenClaw detection |
2 | 2 | //! |
3 | | -//! Provides best-effort migration from OpenClaw's JSON5 config format to LocalGPT's TOML format. |
4 | | -
|
5 | | -use anyhow::Result; |
6 | | -use serde::Deserialize; |
7 | | -use std::fs; |
8 | | -use std::path::PathBuf; |
9 | | -use tracing::{debug, info, warn}; |
10 | | - |
11 | | -use super::{AnthropicConfig, ClaudeCliConfig, Config, OpenAIConfig}; |
12 | | - |
13 | | -/// OpenClaw config structure (partial - only fields we can migrate) |
14 | | -#[derive(Debug, Deserialize)] |
15 | | -#[serde(rename_all = "camelCase")] |
16 | | -struct OpenClawConfig { |
17 | | - #[serde(default)] |
18 | | - agents: Option<OpenClawAgentsConfig>, |
19 | | - |
20 | | - #[serde(default)] |
21 | | - models: Option<OpenClawModelsConfig>, |
22 | | -} |
23 | | - |
24 | | -#[derive(Debug, Deserialize)] |
25 | | -#[serde(rename_all = "camelCase")] |
26 | | -struct OpenClawAgentsConfig { |
27 | | - #[serde(default)] |
28 | | - defaults: Option<OpenClawAgentDefaults>, |
29 | | -} |
30 | | - |
31 | | -#[derive(Debug, Deserialize)] |
32 | | -#[serde(rename_all = "camelCase")] |
33 | | -struct OpenClawAgentDefaults { |
34 | | - #[serde(default)] |
35 | | - workspace: Option<String>, |
36 | | - |
37 | | - #[serde(default)] |
38 | | - model: Option<String>, |
39 | | - |
40 | | - #[serde(default)] |
41 | | - context_window: Option<usize>, |
42 | | -} |
43 | | - |
44 | | -#[derive(Debug, Deserialize)] |
45 | | -#[serde(rename_all = "camelCase")] |
46 | | -struct OpenClawModelsConfig { |
47 | | - #[serde(default)] |
48 | | - openai: Option<OpenClawOpenAIConfig>, |
49 | | - |
50 | | - #[serde(default)] |
51 | | - anthropic: Option<OpenClawAnthropicConfig>, |
52 | | -} |
53 | | - |
54 | | -#[derive(Debug, Deserialize)] |
55 | | -#[serde(rename_all = "camelCase")] |
56 | | -struct OpenClawOpenAIConfig { |
57 | | - #[serde(default)] |
58 | | - api_key: Option<String>, |
59 | | -} |
60 | | - |
61 | | -#[derive(Debug, Deserialize)] |
62 | | -#[serde(rename_all = "camelCase")] |
63 | | -struct OpenClawAnthropicConfig { |
64 | | - #[serde(default)] |
65 | | - api_key: Option<String>, |
66 | | -} |
67 | | - |
68 | | -/// Path to OpenClaw's config file |
69 | | -pub fn openclaw_config_path() -> Result<PathBuf> { |
70 | | - let base = directories::BaseDirs::new() |
71 | | - .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; |
72 | | - |
73 | | - Ok(base.home_dir().join(".openclaw").join("config.json5")) |
74 | | -} |
75 | | - |
76 | | -/// Try to load and migrate OpenClaw config |
77 | | -pub fn try_migrate_openclaw_config() -> Option<Config> { |
78 | | - let path = match openclaw_config_path() { |
79 | | - Ok(p) => p, |
80 | | - Err(_) => return None, |
81 | | - }; |
82 | | - |
83 | | - if !path.exists() { |
84 | | - debug!("No OpenClaw config found at {:?}", path); |
85 | | - return None; |
| 3 | +//! Checks for an existing OpenClaw installation and returns a notice |
| 4 | +//! directing users to the migration guide. |
| 5 | +
|
| 6 | +/// Check if an OpenClaw data directory exists at ~/.openclaw. |
| 7 | +/// |
| 8 | +/// Returns a formatted notice string if detected, `None` otherwise. |
| 9 | +pub fn check_openclaw_detected() -> Option<String> { |
| 10 | + let base = directories::BaseDirs::new()?; |
| 11 | + let openclaw_dir = base.home_dir().join(".openclaw"); |
| 12 | + |
| 13 | + if openclaw_dir.exists() { |
| 14 | + Some( |
| 15 | + "Note: OpenClaw data detected at ~/.openclaw\n \ |
| 16 | + Migration guide: https://localgpt.app/docs/openclaw-migration" |
| 17 | + .to_string(), |
| 18 | + ) |
| 19 | + } else { |
| 20 | + None |
86 | 21 | } |
87 | | - |
88 | | - info!("Found OpenClaw config at {:?}, attempting migration", path); |
89 | | - |
90 | | - let content = match fs::read_to_string(&path) { |
91 | | - Ok(c) => c, |
92 | | - Err(e) => { |
93 | | - warn!("Failed to read OpenClaw config: {}", e); |
94 | | - return None; |
95 | | - } |
96 | | - }; |
97 | | - |
98 | | - let openclaw_config: OpenClawConfig = match json5::from_str(&content) { |
99 | | - Ok(c) => c, |
100 | | - Err(e) => { |
101 | | - warn!("Failed to parse OpenClaw config: {}", e); |
102 | | - return None; |
103 | | - } |
104 | | - }; |
105 | | - |
106 | | - Some(convert_openclaw_config(openclaw_config)) |
107 | | -} |
108 | | - |
109 | | -/// Convert OpenClaw config to LocalGPT config |
110 | | -fn convert_openclaw_config(oc: OpenClawConfig) -> Config { |
111 | | - let mut config = Config::default(); |
112 | | - |
113 | | - // Migrate agent defaults |
114 | | - if let Some(agents) = oc.agents |
115 | | - && let Some(defaults) = agents.defaults |
116 | | - { |
117 | | - if let Some(workspace) = defaults.workspace { |
118 | | - // Convert OpenClaw workspace path to LocalGPT XDG format |
119 | | - let workspace = workspace.replace("~/.openclaw/", "~/.local/share/localgpt/"); |
120 | | - config.memory.workspace = workspace; |
121 | | - } |
122 | | - |
123 | | - if let Some(model) = defaults.model { |
124 | | - config.agent.default_model = model; |
125 | | - } |
126 | | - |
127 | | - if let Some(context_window) = defaults.context_window { |
128 | | - config.agent.context_window = context_window; |
129 | | - } |
130 | | - } |
131 | | - |
132 | | - // Migrate model configs (API keys) |
133 | | - if let Some(models) = oc.models { |
134 | | - if let Some(openai) = models.openai |
135 | | - && let Some(api_key) = openai.api_key |
136 | | - { |
137 | | - config.providers.openai = Some(OpenAIConfig { |
138 | | - api_key, |
139 | | - base_url: "https://api.openai.com/v1".to_string(), |
140 | | - }); |
141 | | - } |
142 | | - |
143 | | - if let Some(anthropic) = models.anthropic |
144 | | - && let Some(api_key) = anthropic.api_key |
145 | | - { |
146 | | - config.providers.anthropic = Some(AnthropicConfig { |
147 | | - api_key, |
148 | | - base_url: "https://api.anthropic.com".to_string(), |
149 | | - }); |
150 | | - } |
151 | | - } |
152 | | - |
153 | | - // Always enable Claude CLI as default (OpenClaw uses it too) |
154 | | - if config.providers.claude_cli.is_none() { |
155 | | - config.providers.claude_cli = Some(ClaudeCliConfig { |
156 | | - command: "claude".to_string(), |
157 | | - model: "opus".to_string(), |
158 | | - }); |
159 | | - } |
160 | | - |
161 | | - info!("Migrated OpenClaw config successfully"); |
162 | | - config |
163 | | -} |
164 | | - |
165 | | -/// Check if OpenClaw workspace exists and can be reused |
166 | | -pub fn has_openclaw_workspace() -> bool { |
167 | | - let base = match directories::BaseDirs::new() { |
168 | | - Some(b) => b, |
169 | | - None => return false, |
170 | | - }; |
171 | | - |
172 | | - let workspace = base.home_dir().join(".openclaw").join("workspace"); |
173 | | - workspace.exists() |
174 | 22 | } |
0 commit comments