Skip to content
Open
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
21 changes: 16 additions & 5 deletions src-tauri/src/acp/binary_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,17 +293,29 @@ async fn ensure_binary_with_progress(
std::fs::create_dir_all(&extract_dir)
.map_err(|e| AcpError::DownloadFailed(format!("failed to create extract dir: {e}")))?;

on_progress("Extracting archive...");
let final_path = dir.join(&bin_name);
if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
on_progress("Extracting archive...");
extract_tar_gz(&archive_path, &extract_dir)?;
} else if archive_url.ends_with(".tar.bz2") || archive_url.ends_with(".tbz2") {
on_progress("Extracting archive...");
extract_tar_bz2(&archive_path, &extract_dir)?;
} else if archive_url.ends_with(".zip") {
on_progress("Extracting archive...");
extract_zip(&archive_path, &extract_dir)?;
} else {
return Err(AcpError::DownloadFailed(format!(
"unsupported archive format: {archive_url}"
)));
on_progress("Installing executable...");
std::fs::copy(&archive_path, &final_path)
.map_err(|e| AcpError::DownloadFailed(format!("failed to copy binary: {e}")))?;
if !is_binary_file_compatible(&final_path) {
let _ = std::fs::remove_file(&final_path);
return Err(AcpError::DownloadFailed(
"downloaded binary format is invalid for current platform".into(),
));
}
set_executable_permissions(&final_path)?;
on_progress("Binary installed successfully");
return Ok(final_path);
}

// Find the binary in extracted files and move to final location.
Expand All @@ -312,7 +324,6 @@ async fn ensure_binary_with_progress(
AcpError::DownloadFailed(format!("binary '{bin_name}' not found in archive"))
})?;

let final_path = dir.join(&bin_name);
std::fs::copy(&extracted_bin, &final_path)
.map_err(|e| AcpError::DownloadFailed(format!("failed to copy binary: {e}")))?;

Expand Down
68 changes: 68 additions & 0 deletions src-tauri/src/acp/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pub fn all_acp_agents() -> Vec<AgentType> {
AgentType::OpenClaw,
AgentType::OpenCode,
AgentType::Cline,
AgentType::Grok,
]
}

Expand All @@ -91,6 +92,7 @@ pub fn registry_id_for(agent_type: AgentType) -> &'static str {
AgentType::OpenClaw => "openclaw-acp",
AgentType::OpenCode => "opencode",
AgentType::Cline => "cline",
AgentType::Grok => "grok",
}
}

Expand All @@ -102,6 +104,7 @@ pub fn from_registry_id(id: &str) -> Option<AgentType> {
"openclaw-acp" => Some(AgentType::OpenClaw),
"opencode" => Some(AgentType::OpenCode),
"cline" => Some(AgentType::Cline),
"grok" => Some(AgentType::Grok),
_ => None,
}
}
Expand Down Expand Up @@ -238,5 +241,70 @@ pub fn get_agent_meta(agent_type: AgentType) -> AcpAgentMeta {
],
},
},
AgentType::Grok => AcpAgentMeta {
agent_type,
name: "Grok",
description: "xAI's coding agent CLI",
distribution: AgentDistribution::Binary {
version: "0.1.210",
cmd: "grok",
args: &["agent", "stdio"],
env: &[],
platforms: &[
PlatformBinary {
platform: "darwin-aarch64",
url: "https://storage.googleapis.com/grok-build-public-artifacts/cli/grok-0.1.210-macos-aarch64",
},
PlatformBinary {
platform: "darwin-x86_64",
url: "https://storage.googleapis.com/grok-build-public-artifacts/cli/grok-0.1.210-macos-x86_64",
},
PlatformBinary {
platform: "linux-aarch64",
url: "https://storage.googleapis.com/grok-build-public-artifacts/cli/grok-0.1.210-linux-aarch64",
},
PlatformBinary {
platform: "linux-x86_64",
url: "https://storage.googleapis.com/grok-build-public-artifacts/cli/grok-0.1.210-linux-x86_64",
},
PlatformBinary {
platform: "windows-x86_64",
url: "https://storage.googleapis.com/grok-build-public-artifacts/cli/grok-0.1.210-windows-x86_64.exe",
},
],
},
},
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn grok_is_registered_as_binary_stdio_agent() {
assert!(all_acp_agents().contains(&AgentType::Grok));
assert_eq!(registry_id_for(AgentType::Grok), "grok");
assert_eq!(from_registry_id("grok"), Some(AgentType::Grok));

let meta = get_agent_meta(AgentType::Grok);
assert_eq!(meta.name, "Grok");
assert_eq!(meta.registry_version(), Some("0.1.210"));

match meta.distribution {
AgentDistribution::Binary {
cmd,
args,
env,
platforms,
..
} => {
assert_eq!(cmd, "grok");
assert_eq!(args, &["agent", "stdio"]);
assert!(env.is_empty());
assert!(platforms.iter().any(|p| p.platform == "darwin-aarch64"));
}
other => panic!("expected binary distribution, got {other:?}"),
}
}
}
68 changes: 64 additions & 4 deletions src-tauri/src/commands/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use crate::web::event_bridge::EventEmitter;

const ACP_AGENTS_UPDATED_EVENT: &str = "app://acp-agents-updated";
const NPM_PREFIX_TIMEOUT: Duration = Duration::from_millis(1500);
const GROK_CLI_BASE_URL: &str = "https://storage.googleapis.com/grok-build-public-artifacts/cli";

static NPM_GLOBAL_PREFIX_CACHE: tokio::sync::OnceCell<PathBuf> = tokio::sync::OnceCell::const_new();

Expand Down Expand Up @@ -385,6 +386,49 @@ async fn detect_local_version(agent_type: AgentType) -> Option<String> {
}
}

async fn detect_registry_version(agent_type: AgentType) -> Option<String> {
if agent_type == AgentType::Grok {
return fetch_grok_channel_version("stable").await;
}

registry::get_agent_meta(agent_type)
.registry_version()
.map(ToString::to_string)
}

async fn fetch_grok_channel_version(channel: &str) -> Option<String> {
let url = format!("{GROK_CLI_BASE_URL}/{channel}");
let resp = reqwest::get(url).await.ok()?;
if !resp.status().is_success() {
return None;
}
let body = resp.text().await.ok()?;
normalize_version_candidate(body.trim())
}

fn grok_artifact_platform(platform: &str) -> Option<&'static str> {
match platform {
"darwin-aarch64" => Some("macos-aarch64"),
"darwin-x86_64" => Some("macos-x86_64"),
"linux-aarch64" => Some("linux-aarch64"),
"linux-x86_64" => Some("linux-x86_64"),
"windows-x86_64" => Some("windows-x86_64"),
_ => None,
}
}

async fn grok_binary_release(platform: &str) -> Option<(String, String)> {
let version = fetch_grok_channel_version("stable").await?;
let artifact_platform = grok_artifact_platform(platform)?;
let suffix = if platform.starts_with("windows-") {
".exe"
} else {
""
};
let url = format!("{GROK_CLI_BASE_URL}/grok-{version}-{artifact_platform}{suffix}");
Some((version, url))
}

/// Official npm registry URL – used to bypass local mirror configurations that
/// may not have synced niche packages like `@agentclientprotocol/*`.
const NPM_OFFICIAL_REGISTRY: &str = "https://registry.npmjs.org";
Expand Down Expand Up @@ -1579,6 +1623,14 @@ pub(crate) fn skill_storage_spec(agent_type: AgentType) -> Option<SkillStorageSp
".claude/skills",
],
}),
AgentType::Grok => Some(SkillStorageSpec {
kind: SkillStorageKind::SkillDirectoryOnly,
global_dirs: vec![
home_dir_or_default().join(".grok").join("skills"),
home_dir_or_default().join(".agents").join("skills"),
],
project_rel_dirs: vec![".grok/skills", ".agents/skills"],
}),
}
}

Expand Down Expand Up @@ -2069,7 +2121,7 @@ fn cascade_update_agent_config(
serde_json::to_string(&patch).map_err(|e| AcpError::protocol(e.to_string()))?;
persist_agent_local_config_json(agent_type, Some(&patch_str))?;
}
AgentType::Cline => {}
AgentType::Cline | AgentType::Grok => {}
}
Ok(())
}
Expand Down Expand Up @@ -2477,6 +2529,7 @@ pub(crate) async fn acp_list_agents_core(db: &AppDatabase) -> Result<Vec<AcpAgen
}
}
let sort_order = setting.map(|m| m.sort_order).unwrap_or(idx as i32);
let registry_version = detect_registry_version(agent_type).await;
// Persist detected version to DB for binary agents (npx written during install/upgrade)
if dist_type == "binary" {
let _ = agent_setting_service::set_installed_version(
Expand Down Expand Up @@ -2510,7 +2563,7 @@ pub(crate) async fn acp_list_agents_core(db: &AppDatabase) -> Result<Vec<AcpAgen
agents.push(AcpAgentInfo {
agent_type,
registry_id: registry::registry_id_for(agent_type).to_string(),
registry_version: meta.registry_version().map(ToString::to_string),
registry_version,
name: meta.name.to_string(),
description: meta.description.to_string(),
available,
Expand Down Expand Up @@ -2864,6 +2917,13 @@ pub(crate) async fn acp_download_agent_binary_core(
meta.name
))
})?;
let (version, archive_url) = if agent_type == AgentType::Grok {
grok_binary_release(platform)
.await
.unwrap_or_else(|| (version.to_string(), fallback.url.to_string()))
} else {
(version.to_string(), fallback.url.to_string())
};

emit_agent_install_event(
emitter,
Expand All @@ -2876,8 +2936,8 @@ pub(crate) async fn acp_download_agent_binary_core(
let task_id_clone = task_id.clone();
let _ = binary_cache::ensure_binary_for_agent_with_progress(
agent_type,
version,
fallback.url,
&version,
&archive_url,
cmd,
move |msg| {
emit_agent_install_event(
Expand Down
9 changes: 8 additions & 1 deletion src-tauri/src/commands/conversations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::parsers::claude::ClaudeParser;
use crate::parsers::cline::ClineParser;
use crate::parsers::codex::CodexParser;
use crate::parsers::gemini::GeminiParser;
use crate::parsers::grok::GrokParser;
use crate::parsers::openclaw::OpenClawParser;
use crate::parsers::opencode::OpenCodeParser;
use crate::parsers::{path_eq_for_matching, AgentParser, ParseError};
Expand Down Expand Up @@ -72,6 +73,7 @@ fn list_conversations_sync(
(AgentType::Gemini, Box::new(GeminiParser::new())),
(AgentType::OpenClaw, Box::new(OpenClawParser::new())),
(AgentType::Cline, Box::new(ClineParser::new())),
(AgentType::Grok, Box::new(GrokParser::new())),
];

for (at, parser) in &parsers {
Expand Down Expand Up @@ -173,6 +175,7 @@ pub async fn get_conversation(
AgentType::Gemini => Box::new(GeminiParser::new()),
AgentType::OpenClaw => Box::new(OpenClawParser::new()),
AgentType::Cline => Box::new(ClineParser::new()),
AgentType::Grok => Box::new(GrokParser::new()),
};

parser
Expand Down Expand Up @@ -307,6 +310,7 @@ pub async fn get_folder_conversation_core(
AgentType::Gemini => Box::new(GeminiParser::new()),
AgentType::OpenClaw => Box::new(OpenClawParser::new()),
AgentType::Cline => Box::new(ClineParser::new()),
AgentType::Grok => Box::new(GrokParser::new()),
};
match parser.get_conversation(&eid) {
Ok(d) => Ok((d.turns, d.session_stats, None)),
Expand All @@ -318,7 +322,10 @@ pub async fn get_folder_conversation_core(
// and started_at from the parsed conversation list.
if matches!(
at,
AgentType::OpenClaw | AgentType::Cline | AgentType::Gemini
AgentType::OpenClaw
| AgentType::Cline
| AgentType::Gemini
| AgentType::Grok
) {
if let Ok(all) = parser.list_conversations() {
// Filter by folder_path first, then find the closest
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/commands/experts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ fn supported_agents() -> Vec<AgentType> {
AgentType::Gemini,
AgentType::OpenClaw,
AgentType::Cline,
AgentType::Grok,
];
ALL.iter()
.filter(|a| skill_storage_spec(**a).is_some())
Expand Down
Loading