zarz 0.3.4-alpha → 0.5.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/config.rs DELETED
@@ -1,249 +0,0 @@
1
- use anyhow::{Context, Result};
2
- use crossterm::style::{Color, Stylize};
3
- use dialoguer::{theme::ColorfulTheme, Select};
4
- use serde::{Deserialize, Serialize};
5
- use std::fs;
6
- use std::io::{self, Write};
7
- use std::path::PathBuf;
8
-
9
- #[derive(Debug, Clone, Serialize, Deserialize, Default)]
10
- pub struct Config {
11
- #[serde(skip_serializing_if = "Option::is_none")]
12
- pub anthropic_api_key: Option<String>,
13
- #[serde(skip_serializing_if = "Option::is_none")]
14
- pub openai_api_key: Option<String>,
15
- #[serde(skip_serializing_if = "Option::is_none")]
16
- pub glm_api_key: Option<String>,
17
- }
18
-
19
- impl Config {
20
- /// Get the path to the config file (~/.zarz/config.toml)
21
- pub fn config_path() -> Result<PathBuf> {
22
- let home = dirs::home_dir()
23
- .context("Could not determine home directory")?;
24
- Ok(home.join(".zarz").join("config.toml"))
25
- }
26
-
27
- /// Load config from file, or return default if file doesn't exist
28
- pub fn load() -> Result<Self> {
29
- let path = Self::config_path()?;
30
-
31
- if !path.exists() {
32
- return Ok(Self::default());
33
- }
34
-
35
- let content = fs::read_to_string(&path)
36
- .context("Failed to read config file")?;
37
-
38
- let config: Config = toml::from_str(&content)
39
- .context("Failed to parse config file")?;
40
-
41
- Ok(config)
42
- }
43
-
44
- /// Save config to file
45
- pub fn save(&self) -> Result<()> {
46
- let path = Self::config_path()?;
47
-
48
- // Create parent directory if it doesn't exist
49
- if let Some(parent) = path.parent() {
50
- fs::create_dir_all(parent)
51
- .context("Failed to create config directory")?;
52
- }
53
-
54
- let content = toml::to_string_pretty(self)
55
- .context("Failed to serialize config")?;
56
-
57
- fs::write(&path, content)
58
- .context("Failed to write config file")?;
59
-
60
- Ok(())
61
- }
62
-
63
- /// Check if at least one API key is configured
64
- pub fn has_api_key(&self) -> bool {
65
- self.anthropic_api_key.is_some() || self.openai_api_key.is_some() || self.glm_api_key.is_some()
66
- }
67
-
68
- /// Interactive setup to get API keys from user
69
- pub fn interactive_setup() -> Result<Self> {
70
- let theme = ColorfulTheme::default();
71
-
72
- println!(
73
- "\n{}\n",
74
- "ZarzCLI Setup".bold().with(Color::Cyan)
75
- );
76
- println!(
77
- "{}",
78
- "Choose a provider to configure. Use the arrow keys and press Enter.".with(Color::DarkGrey)
79
- );
80
- println!(
81
- "{}",
82
- "API keys are displayed while typing so you can verify them, then hidden before storing.".with(Color::DarkGrey)
83
- );
84
- println!(
85
- "{}\n",
86
- "You can configure additional providers later with `zarz config --reset`.".with(Color::DarkGrey)
87
- );
88
-
89
- let options = vec![
90
- "Anthropic Claude (recommended for coding)".bold().with(Color::Yellow).to_string(),
91
- "OpenAI GPT".bold().with(Color::Yellow).to_string(),
92
- "GLM (Z.AI - International GLM-4.6)".bold().with(Color::Yellow).to_string(),
93
- ];
94
-
95
- let selection = Select::with_theme(&theme)
96
- .with_prompt("Select a provider to set up")
97
- .items(&options)
98
- .default(0)
99
- .interact()?;
100
-
101
- let mut config = Self::default();
102
- let mut enabled = Vec::new();
103
-
104
- match selection {
105
- 0 => {
106
- let key = Self::prompt_for_key("Anthropic API key")?;
107
- config.anthropic_api_key = Some(key);
108
- enabled.push("Anthropic Claude");
109
- println!("{}\n", "✓ Anthropic ready".with(Color::Green));
110
- }
111
- 1 => {
112
- let key = Self::prompt_for_key("OpenAI API key")?;
113
- config.openai_api_key = Some(key);
114
- enabled.push("OpenAI GPT");
115
- println!("{}\n", "✓ OpenAI ready".with(Color::Green));
116
- }
117
- _ => {
118
- let key = Self::prompt_for_key("GLM API key")?;
119
- config.glm_api_key = Some(key);
120
- enabled.push("GLM 4.6");
121
- println!("{}\n", "✓ GLM ready".with(Color::Green));
122
- }
123
- }
124
-
125
- if !config.has_api_key() {
126
- anyhow::bail!("At least one API key is required to use ZarzCLI");
127
- }
128
-
129
- config.save()?;
130
- println!(
131
- "{} {}\n",
132
- "✅".with(Color::Green),
133
- format!(
134
- "Configuration saved to {}",
135
- Self::config_path()?.display()
136
- )
137
- .bold()
138
- );
139
- println!(
140
- "{}",
141
- format!("Enabled providers: {}", enabled.join(", ")).with(Color::Green)
142
- );
143
- println!(
144
- "{}\n",
145
- "Run `zarz` any time to start chatting.".with(Color::DarkGrey)
146
- );
147
-
148
- Ok(config)
149
- }
150
-
151
- fn prompt_for_key(label: &str) -> Result<String> {
152
- loop {
153
- print!("Enter your {}: ", label);
154
- io::stdout().flush().ok();
155
-
156
- let mut key = String::new();
157
- io::stdin()
158
- .read_line(&mut key)
159
- .context("Failed to read API key from stdin")?;
160
-
161
- let trimmed = key.trim();
162
- if trimmed.is_empty() {
163
- println!("{}", "Key cannot be empty. Please try again.".with(Color::Red));
164
- continue;
165
- }
166
-
167
- println!("{}", "Key captured ✔".with(Color::Green));
168
- println!(
169
- "{}",
170
- "(The key is now stored securely and will no longer be displayed.)".with(Color::DarkGrey)
171
- );
172
-
173
- return Ok(trimmed.to_string());
174
- }
175
- }
176
-
177
- /// Get Anthropic API key from config or environment
178
- pub fn get_anthropic_key(&self) -> Option<String> {
179
- std::env::var("ANTHROPIC_API_KEY")
180
- .ok()
181
- .or_else(|| self.anthropic_api_key.clone())
182
- }
183
-
184
- /// Get OpenAI API key from config or environment
185
- pub fn get_openai_key(&self) -> Option<String> {
186
- std::env::var("OPENAI_API_KEY")
187
- .ok()
188
- .or_else(|| self.openai_api_key.clone())
189
- }
190
-
191
- /// Get GLM API key from config or environment
192
- pub fn get_glm_key(&self) -> Option<String> {
193
- std::env::var("GLM_API_KEY")
194
- .ok()
195
- .or_else(|| self.glm_api_key.clone())
196
- }
197
-
198
- /// Get default provider based on available API keys
199
- /// Priority: Anthropic > OpenAI > GLM
200
- pub fn get_default_provider(&self) -> Option<crate::cli::Provider> {
201
- if self.get_anthropic_key().is_some() {
202
- Some(crate::cli::Provider::Anthropic)
203
- } else if self.get_openai_key().is_some() {
204
- Some(crate::cli::Provider::OpenAi)
205
- } else if self.get_glm_key().is_some() {
206
- Some(crate::cli::Provider::Glm)
207
- } else {
208
- None
209
- }
210
- }
211
-
212
- /// Apply config to environment variables
213
- pub fn apply_to_env(&self) {
214
- if let Some(key) = &self.anthropic_api_key {
215
- if std::env::var("ANTHROPIC_API_KEY").is_err() {
216
- unsafe { std::env::set_var("ANTHROPIC_API_KEY", key); }
217
- }
218
- }
219
- if let Some(key) = &self.openai_api_key {
220
- if std::env::var("OPENAI_API_KEY").is_err() {
221
- unsafe { std::env::set_var("OPENAI_API_KEY", key); }
222
- }
223
- }
224
- if let Some(key) = &self.glm_api_key {
225
- if std::env::var("GLM_API_KEY").is_err() {
226
- unsafe { std::env::set_var("GLM_API_KEY", key); }
227
- }
228
- }
229
- }
230
-
231
- /// Remove all stored API keys from disk
232
- pub fn clear_api_keys(&mut self) -> Result<bool> {
233
- let mut removed = false;
234
-
235
- if self.anthropic_api_key.take().is_some() {
236
- removed = true;
237
- }
238
- if self.openai_api_key.take().is_some() {
239
- removed = true;
240
- }
241
- if self.glm_api_key.take().is_some() {
242
- removed = true;
243
- }
244
-
245
- self.save()?;
246
-
247
- Ok(removed)
248
- }
249
- }
@@ -1,183 +0,0 @@
1
- use std::fs;
2
- use std::path::{PathBuf};
3
- use std::time::{SystemTime, UNIX_EPOCH};
4
-
5
- use anyhow::{Context, Result};
6
- use chrono::{DateTime, Utc};
7
- use serde::{Deserialize, Serialize};
8
-
9
- use crate::cli::Provider;
10
- use crate::session::{Message, MessageRole, Session};
11
- use crate::config::Config;
12
-
13
- #[derive(Debug, Clone, Serialize, Deserialize)]
14
- pub struct ConversationSnapshot {
15
- pub id: String,
16
- pub title: String,
17
- pub created_at: DateTime<Utc>,
18
- pub updated_at: DateTime<Utc>,
19
- pub provider: String,
20
- pub model: String,
21
- pub working_directory: PathBuf,
22
- pub message_count: usize,
23
- pub messages: Vec<Message>,
24
- }
25
-
26
- #[derive(Debug, Clone)]
27
- pub struct ConversationSummary {
28
- pub id: String,
29
- pub title: String,
30
- pub updated_at: DateTime<Utc>,
31
- pub provider: String,
32
- pub model: String,
33
- pub message_count: usize,
34
- }
35
-
36
- pub struct ConversationStore;
37
-
38
- impl ConversationStore {
39
- fn storage_dir() -> Result<PathBuf> {
40
- let config_path = Config::config_path()?;
41
- let dir = config_path
42
- .parent()
43
- .map(|p| p.join("sessions"))
44
- .unwrap_or_else(|| PathBuf::from(".zarz/sessions"));
45
- fs::create_dir_all(&dir)
46
- .with_context(|| format!("Failed to create session storage at {}", dir.display()))?;
47
- Ok(dir)
48
- }
49
-
50
- fn generate_id() -> String {
51
- let now = Utc::now();
52
- let nanos = now.timestamp_subsec_nanos();
53
- let since_epoch = SystemTime::now()
54
- .duration_since(UNIX_EPOCH)
55
- .map(|d| d.as_nanos())
56
- .unwrap_or(0);
57
- format!("{}-{:09}-{:x}", now.format("%Y%m%d-%H%M%S"), nanos, since_epoch)
58
- }
59
-
60
- fn derive_title(messages: &[Message]) -> String {
61
- const DEFAULT_TITLE: &str = "Untitled session";
62
- let candidate = messages
63
- .iter()
64
- .find_map(|msg| match msg.role {
65
- MessageRole::User => {
66
- msg.content
67
- .lines()
68
- .find(|line| !line.trim().is_empty())
69
- .map(|line| line.trim())
70
- .map(str::to_string)
71
- }
72
- _ => None,
73
- })
74
- .unwrap_or_else(|| DEFAULT_TITLE.to_string());
75
-
76
- let trimmed = candidate.trim();
77
- let title = if trimmed.is_empty() {
78
- DEFAULT_TITLE.to_string()
79
- } else {
80
- trimmed.to_string()
81
- };
82
-
83
- if title.len() > 80 {
84
- format!("{}…", &title[..80])
85
- } else {
86
- title
87
- }
88
- }
89
-
90
- pub fn save_session(session: &mut Session, provider: Provider, model: &str) -> Result<()> {
91
- if session.conversation_history.is_empty() {
92
- return Ok(());
93
- }
94
-
95
- let now = Utc::now();
96
- let id = session
97
- .storage_id
98
- .clone()
99
- .unwrap_or_else(Self::generate_id);
100
- let created_at = session
101
- .created_at
102
- .unwrap_or_else(|| {
103
- let ts = now;
104
- session.created_at = Some(ts);
105
- ts
106
- });
107
-
108
- let title = session
109
- .title
110
- .clone()
111
- .filter(|t| !t.trim().is_empty())
112
- .unwrap_or_else(|| Self::derive_title(&session.conversation_history));
113
-
114
- session.storage_id = Some(id.clone());
115
- session.title = Some(title.clone());
116
- session.updated_at = Some(now);
117
-
118
- let snapshot = ConversationSnapshot {
119
- id: id.clone(),
120
- title,
121
- created_at,
122
- updated_at: now,
123
- provider: provider.as_str().to_string(),
124
- model: model.to_string(),
125
- working_directory: session.working_directory.clone(),
126
- message_count: session.conversation_history.len(),
127
- messages: session.conversation_history.clone(),
128
- };
129
-
130
- let dir = Self::storage_dir()?;
131
- let path = dir.join(format!("{id}.json"));
132
- let data = serde_json::to_string_pretty(&snapshot)
133
- .context("Failed to serialize conversation snapshot")?;
134
- fs::write(&path, data)
135
- .with_context(|| format!("Failed to write conversation snapshot to {}", path.display()))?;
136
-
137
- Ok(())
138
- }
139
-
140
- pub fn list_summaries() -> Result<Vec<ConversationSummary>> {
141
- let dir = Self::storage_dir()?;
142
- if !dir.exists() {
143
- return Ok(Vec::new());
144
- }
145
-
146
- let mut summaries = Vec::new();
147
- for entry in fs::read_dir(&dir).with_context(|| format!("Failed to read {}", dir.display()))? {
148
- let entry = entry?;
149
- if !entry.file_type()?.is_file() {
150
- continue;
151
- }
152
- let content = fs::read_to_string(entry.path());
153
- let Ok(content) = content else {
154
- continue;
155
- };
156
- let snapshot: Result<ConversationSnapshot, _> = serde_json::from_str(&content);
157
- let Ok(snapshot) = snapshot else {
158
- continue;
159
- };
160
- summaries.push(ConversationSummary {
161
- id: snapshot.id,
162
- title: snapshot.title,
163
- updated_at: snapshot.updated_at,
164
- provider: snapshot.provider,
165
- model: snapshot.model,
166
- message_count: snapshot.message_count,
167
- });
168
- }
169
-
170
- summaries.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
171
- Ok(summaries)
172
- }
173
-
174
- pub fn load_snapshot(id: &str) -> Result<ConversationSnapshot> {
175
- let dir = Self::storage_dir()?;
176
- let path = dir.join(format!("{id}.json"));
177
- let data = fs::read_to_string(&path)
178
- .with_context(|| format!("Failed to read session file {}", path.display()))?;
179
- let snapshot: ConversationSnapshot =
180
- serde_json::from_str(&data).context("Failed to parse stored session data")?;
181
- Ok(snapshot)
182
- }
183
- }
package/src/executor.rs DELETED
@@ -1,166 +0,0 @@
1
- use anyhow::{Context, Result};
2
- use std::process::Stdio;
3
- use tokio::io::{AsyncBufReadExt, BufReader};
4
- use tokio::process::Command;
5
-
6
- pub struct CommandExecutor;
7
-
8
- #[allow(dead_code)]
9
- #[derive(Debug)]
10
- pub struct CommandResult {
11
- pub stdout: String,
12
- pub stderr: String,
13
- pub exit_code: i32,
14
- pub success: bool,
15
- }
16
-
17
- impl CommandExecutor {
18
- #[allow(dead_code)]
19
- pub async fn execute(command: &str) -> Result<CommandResult> {
20
- let (shell, flag) = if cfg!(target_os = "windows") {
21
- ("cmd", "/C")
22
- } else {
23
- ("sh", "-c")
24
- };
25
-
26
- let mut child = Command::new(shell)
27
- .arg(flag)
28
- .arg(command)
29
- .stdout(Stdio::piped())
30
- .stderr(Stdio::piped())
31
- .spawn()
32
- .with_context(|| format!("Failed to execute command: {}", command))?;
33
-
34
- let stdout = child
35
- .stdout
36
- .take()
37
- .context("Failed to capture stdout")?;
38
-
39
- let stderr = child
40
- .stderr
41
- .take()
42
- .context("Failed to capture stderr")?;
43
-
44
- let mut stdout_lines = BufReader::new(stdout).lines();
45
- let mut stderr_lines = BufReader::new(stderr).lines();
46
-
47
- let stdout_handle = tokio::spawn(async move {
48
- let mut output = String::new();
49
- while let Ok(Some(line)) = stdout_lines.next_line().await {
50
- output.push_str(&line);
51
- output.push('\n');
52
- }
53
- output
54
- });
55
-
56
- let stderr_handle = tokio::spawn(async move {
57
- let mut output = String::new();
58
- while let Ok(Some(line)) = stderr_lines.next_line().await {
59
- output.push_str(&line);
60
- output.push('\n');
61
- }
62
- output
63
- });
64
-
65
- let stdout_output = stdout_handle
66
- .await
67
- .context("Failed to join stdout task")?;
68
-
69
- let stderr_output = stderr_handle
70
- .await
71
- .context("Failed to join stderr task")?;
72
-
73
- let status = child
74
- .wait()
75
- .await
76
- .context("Failed to wait for command")?;
77
-
78
- let exit_code = status.code().unwrap_or(-1);
79
- let success = status.success();
80
-
81
- Ok(CommandResult {
82
- stdout: stdout_output,
83
- stderr: stderr_output,
84
- exit_code,
85
- success,
86
- })
87
- }
88
-
89
- #[allow(dead_code)]
90
- pub async fn execute_streaming<F>(command: &str, mut on_output: F) -> Result<CommandResult>
91
- where
92
- F: FnMut(&str) + Send,
93
- {
94
- let (shell, flag) = if cfg!(target_os = "windows") {
95
- ("cmd", "/C")
96
- } else {
97
- ("sh", "-c")
98
- };
99
-
100
- let mut child = Command::new(shell)
101
- .arg(flag)
102
- .arg(command)
103
- .stdout(Stdio::piped())
104
- .stderr(Stdio::piped())
105
- .spawn()
106
- .with_context(|| format!("Failed to execute command: {}", command))?;
107
-
108
- let stdout = child
109
- .stdout
110
- .take()
111
- .context("Failed to capture stdout")?;
112
-
113
- let stderr = child
114
- .stderr
115
- .take()
116
- .context("Failed to capture stderr")?;
117
-
118
- let mut stdout_lines = BufReader::new(stdout).lines();
119
- let mut stderr_lines = BufReader::new(stderr).lines();
120
-
121
- let mut stdout_output = String::new();
122
- let mut stderr_output = String::new();
123
-
124
- loop {
125
- tokio::select! {
126
- result = stdout_lines.next_line() => {
127
- match result {
128
- Ok(Some(line)) => {
129
- on_output(&line);
130
- stdout_output.push_str(&line);
131
- stdout_output.push('\n');
132
- }
133
- Ok(None) => break,
134
- Err(_) => break,
135
- }
136
- }
137
- result = stderr_lines.next_line() => {
138
- match result {
139
- Ok(Some(line)) => {
140
- on_output(&line);
141
- stderr_output.push_str(&line);
142
- stderr_output.push('\n');
143
- }
144
- Ok(None) => {},
145
- Err(_) => {},
146
- }
147
- }
148
- }
149
- }
150
-
151
- let status = child
152
- .wait()
153
- .await
154
- .context("Failed to wait for command")?;
155
-
156
- let exit_code = status.code().unwrap_or(-1);
157
- let success = status.success();
158
-
159
- Ok(CommandResult {
160
- stdout: stdout_output,
161
- stderr: stderr_output,
162
- exit_code,
163
- success,
164
- })
165
- }
166
- }