1pub mod config;
2pub mod error;
3pub mod local;
4pub mod utils;
5
6pub use config::{RemoteAuth, WorkspaceConfig};
8pub use error::{Result, WorkspaceError};
9
10use async_trait::async_trait;
12use serde::{Deserialize, Serialize};
13use std::time::{Duration, Instant};
14
15#[async_trait]
17pub trait Workspace: Send + Sync + std::fmt::Debug {
18 async fn environment(&self) -> Result<EnvironmentInfo>;
20
21 fn metadata(&self) -> WorkspaceMetadata;
23
24 async fn invalidate_environment_cache(&self);
26
27 async fn list_files(
30 &self,
31 query: Option<&str>,
32 max_results: Option<usize>,
33 ) -> Result<Vec<String>>;
34
35 fn working_directory(&self) -> &std::path::Path;
37
38 async fn execute_tool(
40 &self,
41 tool_call: &steer_tools::ToolCall,
42 context: steer_tools::ExecutionContext,
43 ) -> Result<steer_tools::result::ToolResult>;
44
45 async fn available_tools(&self) -> Vec<steer_tools::ToolSchema>;
47
48 async fn requires_approval(&self, tool_name: &str) -> Result<bool>;
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct WorkspaceMetadata {
55 pub id: String,
56 pub workspace_type: WorkspaceType,
57 pub location: String, }
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub enum WorkspaceType {
63 Local,
64 Remote,
65}
66
67impl WorkspaceType {
68 pub fn as_str(&self) -> &'static str {
69 match self {
70 WorkspaceType::Local => "Local",
71 WorkspaceType::Remote => "Remote",
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78pub(crate) struct CachedEnvironment {
79 pub info: EnvironmentInfo,
80 pub cached_at: Instant,
81 pub ttl: Duration,
82}
83
84impl CachedEnvironment {
85 pub fn new(info: EnvironmentInfo, ttl: Duration) -> Self {
86 Self {
87 info,
88 cached_at: Instant::now(),
89 ttl,
90 }
91 }
92
93 pub fn is_expired(&self) -> bool {
94 self.cached_at.elapsed() > self.ttl
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct EnvironmentInfo {
101 pub working_directory: std::path::PathBuf,
102 pub is_git_repo: bool,
103 pub platform: String,
104 pub date: String,
105 pub directory_structure: String,
106 pub git_status: Option<String>,
107 pub readme_content: Option<String>,
108 pub claude_md_content: Option<String>,
109}
110
111impl EnvironmentInfo {
112 pub fn collect_for_path(path: &std::path::Path) -> Result<Self> {
114 use crate::utils::{DirectoryStructureUtils, EnvironmentUtils, GitStatusUtils};
115
116 let is_git_repo = EnvironmentUtils::is_git_repo(path);
117 let platform = EnvironmentUtils::get_platform().to_string();
118 let date = EnvironmentUtils::get_current_date();
119
120 let directory_structure = DirectoryStructureUtils::get_directory_structure(path, 3)?;
121
122 let git_status = if is_git_repo {
123 GitStatusUtils::get_git_status(path).ok()
124 } else {
125 None
126 };
127
128 let readme_content = EnvironmentUtils::read_readme(path);
129 let claude_md_content = EnvironmentUtils::read_claude_md(path);
130
131 Ok(Self {
132 working_directory: path.to_path_buf(),
133 is_git_repo,
134 platform,
135 date,
136 directory_structure,
137 git_status,
138 readme_content,
139 claude_md_content,
140 })
141 }
142
143 pub fn as_context(&self) -> String {
145 let mut context = format!(
146 "Here is useful information about the environment you are running in:\n<env>\nWorking directory: {}\nIs directory a git repo: {}\nPlatform: {}\nToday's date: {}\n</env>",
147 self.working_directory.display(),
148 self.is_git_repo,
149 self.platform,
150 self.date
151 );
152
153 if !self.directory_structure.is_empty() {
154 context.push_str(&format!("\n\n<file_structure>\nBelow is a snapshot of this project's file structure at the start of the conversation. The file structure may be filtered to omit `.gitignore`ed patterns. This snapshot will NOT update during the conversation.\n\n{}\n</file_structure>", self.directory_structure));
155 }
156
157 if let Some(ref git_status) = self.git_status {
158 context.push_str(&format!("\n<git_status>\nThis is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.\n\n{git_status}\n</git_status>"));
159 }
160
161 if let Some(ref readme) = self.readme_content {
162 context.push_str(&format!("\n<file name=\"README.md\">\nThis is the README.md file at the start of the conversation. Note that this README is a snapshot in time, and will not update during the conversation.\n\n{readme}\n</file>"));
163 }
164
165 if let Some(ref claude_md) = self.claude_md_content {
166 context.push_str(&format!("\n<file name=\"CLAUDE.md\">\nThis is the CLAUDE.md file at the start of the conversation. Note that this CLAUDE is a snapshot in time, and will not update during the conversation.\n\n{claude_md}\n</file>"));
167 }
168
169 context
170 }
171}