Commit all current changes
This commit is contained in:
@@ -70,6 +70,7 @@ export default function WorkspacesPage() {
|
||||
const [newWorkspaceName, setNewWorkspaceName] = useState('');
|
||||
const [newWorkspaceType, setNewWorkspaceType] = useState<'host' | 'chroot'>('chroot');
|
||||
const [newWorkspaceTemplate, setNewWorkspaceTemplate] = useState('');
|
||||
const [newWorkspacePath, setNewWorkspacePath] = useState('');
|
||||
const [skillsFilter, setSkillsFilter] = useState('');
|
||||
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
|
||||
const [workspaceTab, setWorkspaceTab] = useState<'overview' | 'skills' | 'environment' | 'template' | 'build'>('overview');
|
||||
@@ -264,10 +265,15 @@ export default function WorkspacesPage() {
|
||||
try {
|
||||
setCreating(true);
|
||||
const workspaceType = newWorkspaceTemplate ? 'chroot' : newWorkspaceType;
|
||||
const hostPath =
|
||||
workspaceType === 'host'
|
||||
? (newWorkspacePath.trim() || `workspaces/${newWorkspaceName}`)
|
||||
: undefined;
|
||||
const created = await createWorkspace({
|
||||
name: newWorkspaceName,
|
||||
workspace_type: workspaceType,
|
||||
template: newWorkspaceTemplate || undefined,
|
||||
path: hostPath,
|
||||
});
|
||||
|
||||
// Refresh workspace list immediately after creation so it appears in the UI
|
||||
@@ -294,6 +300,7 @@ export default function WorkspacesPage() {
|
||||
}
|
||||
setShowNewWorkspaceDialog(false);
|
||||
setNewWorkspaceName('');
|
||||
setNewWorkspacePath('');
|
||||
setNewWorkspaceTemplate('');
|
||||
setSelectedWorkspace(workspaceToShow);
|
||||
} catch (err) {
|
||||
@@ -1112,6 +1119,22 @@ export default function WorkspacesPage() {
|
||||
: 'Creates isolated Linux filesystem'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!newWorkspaceTemplate && newWorkspaceType === 'host' && (
|
||||
<div>
|
||||
<label className="text-xs text-white/40 mb-2 block">Path (relative to /home/trav)</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={`workspaces/${newWorkspaceName || 'my-workspace'}`}
|
||||
value={newWorkspacePath}
|
||||
onChange={(e) => setNewWorkspacePath(e.target.value)}
|
||||
className="w-full px-3 py-2.5 rounded-lg bg-black/20 border border-white/[0.06] text-sm text-white placeholder:text-white/25 focus:outline-none focus:border-indigo-500/50"
|
||||
/>
|
||||
<p className="text-xs text-white/35 mt-2">
|
||||
Leave blank to use <span className="text-white/70">workspaces/{newWorkspaceName || 'name'}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 border-t border-white/[0.06] flex items-center justify-end gap-2">
|
||||
|
||||
@@ -184,13 +184,11 @@ fn get_anthropic_key_from_opencode_auth() -> Option<String> {
|
||||
// Check for API key first
|
||||
let auth_type = anthropic_auth.get("type").and_then(|v| v.as_str());
|
||||
match auth_type {
|
||||
Some("api_key") | Some("api") => {
|
||||
anthropic_auth
|
||||
.get("key")
|
||||
.or_else(|| anthropic_auth.get("api_key"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
Some("api_key") | Some("api") => anthropic_auth
|
||||
.get("key")
|
||||
.or_else(|| anthropic_auth.get("api_key"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
Some("oauth") => {
|
||||
// Return OAuth access token - Claude CLI can use this
|
||||
anthropic_auth
|
||||
@@ -1348,7 +1346,9 @@ async fn get_provider_for_backend(
|
||||
Ok(Json(BackendProviderResponse {
|
||||
configured: true,
|
||||
provider_type: Some("anthropic".to_string()),
|
||||
provider_name: anthropic_config.and_then(|c| c.name).or_else(|| Some("Anthropic".to_string())),
|
||||
provider_name: anthropic_config
|
||||
.and_then(|c| c.name)
|
||||
.or_else(|| Some("Anthropic".to_string())),
|
||||
api_key,
|
||||
oauth,
|
||||
has_credentials,
|
||||
@@ -1894,25 +1894,28 @@ async fn oauth_callback_inner(
|
||||
// 2. The old format: code#state
|
||||
// 3. Just the code
|
||||
let input = req.code.trim();
|
||||
let (code_string, state_string): (String, Option<String>) = if let Ok(url) = url::Url::parse(input) {
|
||||
// Parse as URL
|
||||
let code = url.query_pairs()
|
||||
.find(|(k, _)| k == "code")
|
||||
.map(|(_, v)| v.to_string());
|
||||
let state = url.query_pairs()
|
||||
.find(|(k, _)| k == "state")
|
||||
.map(|(_, v)| v.to_string());
|
||||
(code.unwrap_or_default(), state)
|
||||
} else if input.contains('#') {
|
||||
// Old format: code#state
|
||||
let mut parts = input.splitn(2, '#');
|
||||
let code = parts.next().unwrap_or(input).to_string();
|
||||
let state = parts.next().map(|s| s.to_string());
|
||||
(code, state)
|
||||
} else {
|
||||
// Just the code
|
||||
(input.to_string(), None)
|
||||
};
|
||||
let (code_string, state_string): (String, Option<String>) =
|
||||
if let Ok(url) = url::Url::parse(input) {
|
||||
// Parse as URL
|
||||
let code = url
|
||||
.query_pairs()
|
||||
.find(|(k, _)| k == "code")
|
||||
.map(|(_, v)| v.to_string());
|
||||
let state = url
|
||||
.query_pairs()
|
||||
.find(|(k, _)| k == "state")
|
||||
.map(|(_, v)| v.to_string());
|
||||
(code.unwrap_or_default(), state)
|
||||
} else if input.contains('#') {
|
||||
// Old format: code#state
|
||||
let mut parts = input.splitn(2, '#');
|
||||
let code = parts.next().unwrap_or(input).to_string();
|
||||
let state = parts.next().map(|s| s.to_string());
|
||||
(code, state)
|
||||
} else {
|
||||
// Just the code
|
||||
(input.to_string(), None)
|
||||
};
|
||||
|
||||
if code_string.is_empty() {
|
||||
return Err((
|
||||
|
||||
@@ -113,11 +113,12 @@ pub async fn get_backend_config(
|
||||
.ok_or_else(|| (StatusCode::NOT_FOUND, format!("Backend {} not found", id)))?;
|
||||
drop(registry);
|
||||
|
||||
let config_entry = state
|
||||
.backend_configs
|
||||
.get(&id)
|
||||
.await
|
||||
.ok_or_else(|| (StatusCode::NOT_FOUND, format!("Backend {} not configured", id)))?;
|
||||
let config_entry = state.backend_configs.get(&id).await.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("Backend {} not configured", id),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut settings = config_entry.settings.clone();
|
||||
|
||||
@@ -169,10 +170,12 @@ pub async fn update_backend_config(
|
||||
|
||||
let updated_settings = match id.as_str() {
|
||||
"opencode" => {
|
||||
let settings = req
|
||||
.settings
|
||||
.as_object()
|
||||
.ok_or_else(|| (StatusCode::BAD_REQUEST, "Invalid settings payload".to_string()))?;
|
||||
let settings = req.settings.as_object().ok_or_else(|| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid settings payload".to_string(),
|
||||
)
|
||||
})?;
|
||||
let base_url = settings
|
||||
.get("base_url")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -197,15 +200,12 @@ pub async fn update_backend_config(
|
||||
"claudecode" => {
|
||||
let mut settings = req.settings.clone();
|
||||
if let Some(api_key) = settings.get("api_key").and_then(|v| v.as_str()) {
|
||||
let store = state
|
||||
.secrets
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Secrets store not available".to_string(),
|
||||
)
|
||||
})?;
|
||||
let store = state.secrets.as_ref().ok_or_else(|| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Secrets store not available".to_string(),
|
||||
)
|
||||
})?;
|
||||
store
|
||||
.set_secret("claudecode", "api_key", api_key, None)
|
||||
.await
|
||||
|
||||
@@ -3641,7 +3641,7 @@ async fn run_single_control_turn(
|
||||
tracing::warn!("Failed to prepare mission workspace: {}", e);
|
||||
ws.path.clone()
|
||||
}
|
||||
};
|
||||
};
|
||||
(dir, Some(ws))
|
||||
} else {
|
||||
(
|
||||
|
||||
@@ -169,16 +169,12 @@ async fn resolve_path_for_workspace(
|
||||
path: &str,
|
||||
mission_id: Option<uuid::Uuid>,
|
||||
) -> Result<PathBuf, (StatusCode, String)> {
|
||||
let workspace = state
|
||||
.workspaces
|
||||
.get(workspace_id)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("Workspace {} not found", workspace_id),
|
||||
)
|
||||
})?;
|
||||
let workspace = state.workspaces.get(workspace_id).await.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("Workspace {} not found", workspace_id),
|
||||
)
|
||||
})?;
|
||||
|
||||
let workspace_root = workspace.path.canonicalize().map_err(|e| {
|
||||
(
|
||||
|
||||
@@ -19,7 +19,9 @@ use tokio_util::sync::CancellationToken;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::agents::{AgentContext, AgentRef, AgentResult, TerminalReason};
|
||||
use crate::backend::claudecode::client::{ClaudeCodeClient, ClaudeCodeConfig, ClaudeEvent, ContentBlock, StreamEvent};
|
||||
use crate::backend::claudecode::client::{
|
||||
ClaudeCodeClient, ClaudeCodeConfig, ClaudeEvent, ContentBlock, StreamEvent,
|
||||
};
|
||||
use crate::config::Config;
|
||||
use crate::mcp::McpRegistry;
|
||||
use crate::secrets::SecretsStore;
|
||||
@@ -574,7 +576,10 @@ fn get_claudecode_cli_path_from_config(_app_working_dir: &std::path::Path) -> Op
|
||||
if let Some(settings) = config.get("settings") {
|
||||
if let Some(cli_path) = settings.get("cli_path").and_then(|v| v.as_str()) {
|
||||
if !cli_path.is_empty() {
|
||||
tracing::info!("Using Claude Code CLI path from backend config: {}", cli_path);
|
||||
tracing::info!(
|
||||
"Using Claude Code CLI path from backend config: {}",
|
||||
cli_path
|
||||
);
|
||||
return Some(cli_path.to_string());
|
||||
}
|
||||
}
|
||||
@@ -596,8 +601,8 @@ pub async fn run_claudecode_turn(
|
||||
secrets: Option<Arc<SecretsStore>>,
|
||||
app_working_dir: &std::path::Path,
|
||||
) -> AgentResult {
|
||||
use std::collections::HashMap;
|
||||
use super::ai_providers::get_anthropic_api_key_for_claudecode;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Try to get API key from Anthropic provider configured for Claude Code backend
|
||||
let api_key = if let Some(key) = get_anthropic_api_key_for_claudecode(app_working_dir) {
|
||||
@@ -665,8 +670,7 @@ pub async fn run_claudecode_turn(
|
||||
mission_id: Some(mission_id),
|
||||
resumable: true,
|
||||
});
|
||||
return AgentResult::failure(err_msg, 0)
|
||||
.with_terminal_reason(TerminalReason::LlmError);
|
||||
return AgentResult::failure(err_msg, 0).with_terminal_reason(TerminalReason::LlmError);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -156,9 +156,7 @@ pub async fn serve(config: Config) -> anyhow::Result<()> {
|
||||
];
|
||||
let backend_configs = Arc::new(
|
||||
crate::backend_config::BackendConfigStore::new(
|
||||
config
|
||||
.working_dir
|
||||
.join(".openagent/backend_config.json"),
|
||||
config.working_dir.join(".openagent/backend_config.json"),
|
||||
backend_defaults,
|
||||
)
|
||||
.await,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! Provides endpoints to query and update system components like OpenCode
|
||||
//! and oh-my-opencode.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -25,6 +26,54 @@ use super::routes::AppState;
|
||||
/// Default repo path for Open Agent source
|
||||
const OPEN_AGENT_REPO_PATH: &str = "/opt/open_agent/vaduz-v1";
|
||||
|
||||
fn resolve_opencode_binary_path() -> PathBuf {
|
||||
if let Ok(path) = std::env::var("OPENCODE_BIN_PATH") {
|
||||
if !path.trim().is_empty() {
|
||||
return PathBuf::from(path);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(path) = std::env::var("OPENCODE_BIN") {
|
||||
if !path.trim().is_empty() {
|
||||
return PathBuf::from(path);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
let candidate = PathBuf::from(home).join(".opencode/bin/opencode");
|
||||
if candidate.exists() {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
PathBuf::from("/usr/local/bin/opencode")
|
||||
}
|
||||
|
||||
fn resolve_claude_code_binary_path() -> Option<PathBuf> {
|
||||
for key in ["CLAUDE_CLI_PATH", "CLAUDE_CODE_BIN_PATH", "CLAUDE_BIN_PATH"] {
|
||||
if let Ok(path) = std::env::var(key) {
|
||||
let trimmed = path.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let candidate = PathBuf::from(trimmed);
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_claude_code_version(line: &str) -> Option<String> {
|
||||
for token in line.split_whitespace() {
|
||||
let cleaned = token.trim_matches(|c: char| !c.is_ascii_digit() && c != '.');
|
||||
if cleaned.chars().next().is_some_and(|c| c.is_ascii_digit()) {
|
||||
return Some(cleaned.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Information about a system component.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ComponentInfo {
|
||||
@@ -89,7 +138,10 @@ pub fn routes() -> Router<Arc<AppState>> {
|
||||
.route("/components", get(get_components))
|
||||
.route("/components/:name/update", post(update_component))
|
||||
.route("/plugins/installed", get(get_installed_plugins))
|
||||
.route("/plugins/:package/update", post(update_plugin))
|
||||
.route(
|
||||
"/plugins/:package/update",
|
||||
get(update_plugin).post(update_plugin),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get information about all system components.
|
||||
@@ -130,6 +182,9 @@ async fn get_components(State(state): State<Arc<AppState>>) -> Json<SystemCompon
|
||||
|
||||
/// Get OpenCode version and status.
|
||||
async fn get_opencode_info(config: &crate::config::Config) -> ComponentInfo {
|
||||
let opencode_path = resolve_opencode_binary_path();
|
||||
let opencode_path_display = opencode_path.display().to_string();
|
||||
|
||||
// Try to get version from the health endpoint
|
||||
let client = reqwest::Client::new();
|
||||
let health_url = format!("{}/global/health", config.opencode_base_url);
|
||||
@@ -155,7 +210,7 @@ async fn get_opencode_info(config: &crate::config::Config) -> ComponentInfo {
|
||||
version,
|
||||
installed: true,
|
||||
update_available,
|
||||
path: Some("/usr/local/bin/opencode".to_string()),
|
||||
path: Some(opencode_path_display.clone()),
|
||||
status,
|
||||
};
|
||||
}
|
||||
@@ -164,7 +219,7 @@ async fn get_opencode_info(config: &crate::config::Config) -> ComponentInfo {
|
||||
}
|
||||
|
||||
// Fallback: try to run opencode --version
|
||||
match Command::new("opencode").arg("--version").output().await {
|
||||
match Command::new(&opencode_path).arg("--version").output().await {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version_str = String::from_utf8_lossy(&output.stdout);
|
||||
let version = version_str.lines().next().map(|l| {
|
||||
@@ -185,7 +240,7 @@ async fn get_opencode_info(config: &crate::config::Config) -> ComponentInfo {
|
||||
version,
|
||||
installed: true,
|
||||
update_available,
|
||||
path: Some("/usr/local/bin/opencode".to_string()),
|
||||
path: Some(opencode_path_display.clone()),
|
||||
status,
|
||||
}
|
||||
}
|
||||
@@ -202,15 +257,28 @@ async fn get_opencode_info(config: &crate::config::Config) -> ComponentInfo {
|
||||
|
||||
/// Get Claude Code version and status.
|
||||
async fn get_claude_code_info() -> ComponentInfo {
|
||||
let cli_path = which_claude_code().await;
|
||||
|
||||
// Try to run claude --version to check if it's installed
|
||||
match Command::new("claude").arg("--version").output().await {
|
||||
let Some(cli_path) = cli_path else {
|
||||
return ComponentInfo {
|
||||
name: "claude_code".to_string(),
|
||||
version: None,
|
||||
installed: false,
|
||||
update_available: None,
|
||||
path: None,
|
||||
status: ComponentStatus::NotInstalled,
|
||||
};
|
||||
};
|
||||
|
||||
match Command::new(&cli_path).arg("--version").output().await {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version_str = String::from_utf8_lossy(&output.stdout);
|
||||
// Parse version from output like "claude 1.0.3"
|
||||
let version = version_str
|
||||
.lines()
|
||||
.next()
|
||||
.map(|l| l.trim().replace("claude ", "").replace("Claude ", ""));
|
||||
.and_then(parse_claude_code_version)
|
||||
.or_else(|| version_str.lines().next().map(|l| l.trim().to_string()));
|
||||
|
||||
let update_available = check_claude_code_update(version.as_deref()).await;
|
||||
let status = if update_available.is_some() {
|
||||
@@ -224,7 +292,7 @@ async fn get_claude_code_info() -> ComponentInfo {
|
||||
version,
|
||||
installed: true,
|
||||
update_available,
|
||||
path: which_claude_code().await,
|
||||
path: Some(cli_path),
|
||||
status,
|
||||
}
|
||||
}
|
||||
@@ -241,13 +309,13 @@ async fn get_claude_code_info() -> ComponentInfo {
|
||||
|
||||
/// Find the path to the Claude Code binary.
|
||||
async fn which_claude_code() -> Option<String> {
|
||||
if let Some(path) = resolve_claude_code_binary_path() {
|
||||
return Some(path.display().to_string());
|
||||
}
|
||||
|
||||
let output = Command::new("which").arg("claude").output().await.ok()?;
|
||||
if output.status.success() {
|
||||
Some(
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.trim()
|
||||
.to_string(),
|
||||
)
|
||||
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -809,18 +877,59 @@ fn stream_opencode_update() -> impl Stream<Item = Result<Event, std::convert::In
|
||||
progress: Some(50),
|
||||
}).unwrap()));
|
||||
|
||||
// Copy to /usr/local/bin
|
||||
let opencode_path = resolve_opencode_binary_path();
|
||||
let opencode_path_display = opencode_path.display().to_string();
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
|
||||
let install_result = Command::new("install")
|
||||
.args(["-m", "0755", &format!("{}/.opencode/bin/opencode", home), "/usr/local/bin/opencode"])
|
||||
.output()
|
||||
.await;
|
||||
let source_path = PathBuf::from(format!("{}/.opencode/bin/opencode", home));
|
||||
|
||||
if let Some(parent) = opencode_path.parent() {
|
||||
if let Err(e) = tokio::fs::create_dir_all(parent).await {
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "error".to_string(),
|
||||
message: format!(
|
||||
"Failed to create install directory {}: {}",
|
||||
parent.display(),
|
||||
e
|
||||
),
|
||||
progress: None,
|
||||
}).unwrap()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !source_path.exists() {
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "error".to_string(),
|
||||
message: format!(
|
||||
"OpenCode binary not found at {} after install script.",
|
||||
source_path.display()
|
||||
),
|
||||
progress: None,
|
||||
}).unwrap()));
|
||||
return;
|
||||
}
|
||||
|
||||
let install_result = if opencode_path == source_path {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Command::new("install")
|
||||
.args([
|
||||
"-m",
|
||||
"0755",
|
||||
source_path.to_str().unwrap_or(""),
|
||||
&opencode_path_display,
|
||||
])
|
||||
.output()
|
||||
.await,
|
||||
)
|
||||
};
|
||||
|
||||
match install_result {
|
||||
Ok(output) if output.status.success() => {
|
||||
None => {
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "log".to_string(),
|
||||
message: "Binary installed, restarting service...".to_string(),
|
||||
message: format!("Binary already installed at {}, restarting service...", opencode_path_display),
|
||||
progress: Some(80),
|
||||
}).unwrap()));
|
||||
|
||||
@@ -844,21 +953,102 @@ fn stream_opencode_update() -> impl Stream<Item = Result<Event, std::convert::In
|
||||
Ok(output) => {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "error".to_string(),
|
||||
message: format!("Failed to restart service: {}", stderr),
|
||||
progress: None,
|
||||
event_type: "log".to_string(),
|
||||
message: format!(
|
||||
"OpenCode installed at {}, but failed to restart opencode.service: {}. Please run: sudo systemctl restart opencode.service",
|
||||
opencode_path_display,
|
||||
stderr.trim()
|
||||
),
|
||||
progress: Some(95),
|
||||
}).unwrap()));
|
||||
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "complete".to_string(),
|
||||
message: "OpenCode updated; manual restart required.".to_string(),
|
||||
progress: Some(100),
|
||||
}).unwrap()));
|
||||
}
|
||||
Err(e) => {
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "error".to_string(),
|
||||
message: format!("Failed to restart service: {}", e),
|
||||
progress: None,
|
||||
event_type: "log".to_string(),
|
||||
message: format!(
|
||||
"OpenCode installed at {}, but failed to restart opencode.service: {}. Please run: sudo systemctl restart opencode.service",
|
||||
opencode_path_display,
|
||||
e
|
||||
),
|
||||
progress: Some(95),
|
||||
}).unwrap()));
|
||||
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "complete".to_string(),
|
||||
message: "OpenCode updated; manual restart required.".to_string(),
|
||||
progress: Some(100),
|
||||
}).unwrap()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(output) => {
|
||||
Some(Ok(output)) if output.status.success() => {
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "log".to_string(),
|
||||
message: format!("Binary installed at {}, restarting service...", opencode_path_display),
|
||||
progress: Some(80),
|
||||
}).unwrap()));
|
||||
|
||||
// Restart the opencode service
|
||||
let restart_result = Command::new("systemctl")
|
||||
.args(["restart", "opencode.service"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
match restart_result {
|
||||
Ok(output) if output.status.success() => {
|
||||
// Wait a moment for the service to start
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "complete".to_string(),
|
||||
message: "OpenCode updated successfully!".to_string(),
|
||||
progress: Some(100),
|
||||
}).unwrap()));
|
||||
}
|
||||
Ok(output) => {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "log".to_string(),
|
||||
message: format!(
|
||||
"OpenCode installed at {}, but failed to restart opencode.service: {}. Please run: sudo systemctl restart opencode.service",
|
||||
opencode_path_display,
|
||||
stderr.trim()
|
||||
),
|
||||
progress: Some(95),
|
||||
}).unwrap()));
|
||||
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "complete".to_string(),
|
||||
message: "OpenCode updated; manual restart required.".to_string(),
|
||||
progress: Some(100),
|
||||
}).unwrap()));
|
||||
}
|
||||
Err(e) => {
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "log".to_string(),
|
||||
message: format!(
|
||||
"OpenCode installed at {}, but failed to restart opencode.service: {}. Please run: sudo systemctl restart opencode.service",
|
||||
opencode_path_display,
|
||||
e
|
||||
),
|
||||
progress: Some(95),
|
||||
}).unwrap()));
|
||||
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "complete".to_string(),
|
||||
message: "OpenCode updated; manual restart required.".to_string(),
|
||||
progress: Some(100),
|
||||
}).unwrap()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Ok(output)) => {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "error".to_string(),
|
||||
@@ -866,7 +1056,7 @@ fn stream_opencode_update() -> impl Stream<Item = Result<Event, std::convert::In
|
||||
progress: None,
|
||||
}).unwrap()));
|
||||
}
|
||||
Err(e) => {
|
||||
Some(Err(e)) => {
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "error".to_string(),
|
||||
message: format!("Failed to install binary: {}", e),
|
||||
@@ -934,19 +1124,26 @@ fn stream_claude_code_update() -> impl Stream<Item = Result<Event, std::convert:
|
||||
progress: Some(80),
|
||||
}).unwrap()));
|
||||
|
||||
let verify_path = which_claude_code()
|
||||
.await
|
||||
.unwrap_or_else(|| "claude".to_string());
|
||||
|
||||
// Verify installation
|
||||
let verify_result = Command::new("claude")
|
||||
let verify_result = Command::new(&verify_path)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await;
|
||||
|
||||
match verify_result {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version = String::from_utf8_lossy(&output.stdout)
|
||||
let version_line = String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.next()
|
||||
.map(|l| l.trim().to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
let version = parse_claude_code_version(&version_line)
|
||||
.unwrap_or_else(|| version_line);
|
||||
|
||||
yield Ok(Event::default().data(serde_json::to_string(&UpdateProgressEvent {
|
||||
event_type: "complete".to_string(),
|
||||
|
||||
@@ -148,9 +148,7 @@ pub enum ContentBlock {
|
||||
is_error: bool,
|
||||
},
|
||||
#[serde(rename = "thinking")]
|
||||
Thinking {
|
||||
thinking: String,
|
||||
},
|
||||
Thinking { thinking: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@@ -208,8 +206,7 @@ pub struct ClaudeCodeConfig {
|
||||
impl Default for ClaudeCodeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cli_path: std::env::var("CLAUDE_CLI_PATH")
|
||||
.unwrap_or_else(|_| "claude".to_string()),
|
||||
cli_path: std::env::var("CLAUDE_CLI_PATH").unwrap_or_else(|_| "claude".to_string()),
|
||||
api_key: std::env::var("ANTHROPIC_API_KEY").ok(),
|
||||
default_model: None,
|
||||
}
|
||||
@@ -299,7 +296,11 @@ impl ClaudeCodeClient {
|
||||
|
||||
let mut child = cmd.spawn().map_err(|e| {
|
||||
error!("Failed to spawn Claude CLI: {}", e);
|
||||
anyhow!("Failed to spawn Claude CLI: {}. Is it installed at '{}'?", e, self.config.cli_path)
|
||||
anyhow!(
|
||||
"Failed to spawn Claude CLI: {}. Is it installed at '{}'?",
|
||||
e,
|
||||
self.config.cli_path
|
||||
)
|
||||
})?;
|
||||
|
||||
// Write message to stdin
|
||||
|
||||
@@ -265,9 +265,7 @@ fn convert_claude_event(
|
||||
ClaudeEvent::Result(res) => {
|
||||
if res.is_error || res.subtype == "error" {
|
||||
results.push(ExecutionEvent::Error {
|
||||
message: res
|
||||
.result
|
||||
.unwrap_or_else(|| "Unknown error".to_string()),
|
||||
message: res.result.unwrap_or_else(|| "Unknown error".to_string()),
|
||||
});
|
||||
} else {
|
||||
debug!(
|
||||
|
||||
@@ -21,7 +21,11 @@ fn default_enabled() -> bool {
|
||||
}
|
||||
|
||||
impl BackendConfigEntry {
|
||||
pub fn new(id: impl Into<String>, name: impl Into<String>, settings: serde_json::Value) -> Self {
|
||||
pub fn new(
|
||||
id: impl Into<String>,
|
||||
name: impl Into<String>,
|
||||
settings: serde_json::Value,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
name: name.into(),
|
||||
|
||||
@@ -259,19 +259,38 @@ fn default_timeout_from_env() -> Duration {
|
||||
Duration::from_secs_f64(DEFAULT_COMMAND_TIMEOUT_SECS)
|
||||
}
|
||||
|
||||
fn parse_timeout(args: &Value) -> Duration {
|
||||
if let Some(ms) = args.get("timeout_ms").and_then(|v| v.as_u64()) {
|
||||
return Duration::from_millis(ms.max(1));
|
||||
}
|
||||
if let Some(secs) = args.get("timeout_secs").and_then(|v| v.as_u64()) {
|
||||
return Duration::from_secs(secs.max(1));
|
||||
}
|
||||
if let Some(secs) = args.get("timeout").and_then(|v| v.as_f64()) {
|
||||
if secs > 0.0 {
|
||||
return Duration::from_secs_f64(secs);
|
||||
fn max_timeout_from_env() -> Option<Duration> {
|
||||
if let Ok(raw) = env::var("OPEN_AGENT_COMMAND_TIMEOUT_MAX_SECS") {
|
||||
if let Ok(value) = raw.parse::<f64>() {
|
||||
if value > 0.0 {
|
||||
return Some(Duration::from_secs_f64(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
default_timeout_from_env()
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_timeout(args: &Value) -> Duration {
|
||||
let mut timeout = if let Some(ms) = args.get("timeout_ms").and_then(|v| v.as_u64()) {
|
||||
Duration::from_millis(ms.max(1))
|
||||
} else if let Some(secs) = args.get("timeout_secs").and_then(|v| v.as_u64()) {
|
||||
Duration::from_secs(secs.max(1))
|
||||
} else if let Some(secs) = args.get("timeout").and_then(|v| v.as_f64()) {
|
||||
if secs > 0.0 {
|
||||
Duration::from_secs_f64(secs)
|
||||
} else {
|
||||
default_timeout_from_env()
|
||||
}
|
||||
} else {
|
||||
default_timeout_from_env()
|
||||
};
|
||||
|
||||
if let Some(max) = max_timeout_from_env() {
|
||||
if timeout > max {
|
||||
timeout = max;
|
||||
}
|
||||
}
|
||||
timeout
|
||||
}
|
||||
|
||||
fn parse_env(args: &Value) -> HashMap<String, String> {
|
||||
|
||||
@@ -579,6 +579,11 @@ fn opencode_entry_from_mcp(
|
||||
merged_env
|
||||
.entry("WORKING_DIR".to_string())
|
||||
.or_insert_with(|| workspace_dir.to_string_lossy().to_string());
|
||||
if config.name == "workspace" {
|
||||
merged_env
|
||||
.entry("OPEN_AGENT_COMMAND_TIMEOUT_MAX_SECS".to_string())
|
||||
.or_insert_with(|| "55".to_string());
|
||||
}
|
||||
if workspace_type == WorkspaceType::Chroot {
|
||||
if let Some(name) = workspace_root.file_name().and_then(|n| n.to_str()) {
|
||||
if !name.trim().is_empty() {
|
||||
@@ -739,7 +744,10 @@ fn claude_entry_from_mcp(
|
||||
entry.insert("command".to_string(), json!(command));
|
||||
entry.insert("args".to_string(), json!(args));
|
||||
|
||||
if let Some(env) = opencode_entry.get("environment").and_then(|v| v.as_object()) {
|
||||
if let Some(env) = opencode_entry
|
||||
.get("environment")
|
||||
.and_then(|v| v.as_object())
|
||||
{
|
||||
entry.insert("env".to_string(), serde_json::Value::Object(env.clone()));
|
||||
}
|
||||
|
||||
@@ -1544,14 +1552,8 @@ pub async fn prepare_mission_workspace_with_skills(
|
||||
library: Option<&LibraryStore>,
|
||||
mission_id: Uuid,
|
||||
) -> anyhow::Result<PathBuf> {
|
||||
prepare_mission_workspace_with_skills_backend(
|
||||
workspace,
|
||||
mcp,
|
||||
library,
|
||||
mission_id,
|
||||
"opencode",
|
||||
)
|
||||
.await
|
||||
prepare_mission_workspace_with_skills_backend(workspace, mcp, library, mission_id, "opencode")
|
||||
.await
|
||||
}
|
||||
|
||||
/// Prepare a workspace directory for a mission with skill and tool syncing for a specific backend.
|
||||
@@ -1575,8 +1577,9 @@ pub async fn prepare_mission_workspace_with_skills_backend(
|
||||
if backend_id == "claudecode" {
|
||||
if let Some(lib) = library {
|
||||
let context = format!("mission-{}", mission_id);
|
||||
let skill_names =
|
||||
resolve_workspace_skill_names(workspace, lib).await.unwrap_or_default();
|
||||
let skill_names = resolve_workspace_skill_names(workspace, lib)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let skills = collect_skill_contents(&skill_names, &context, lib).await;
|
||||
skill_contents = Some(skills);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user