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