Initial CodeCraft: web-based coding harness for AI models
Rust backend (axum/tokio): agent loop with streaming, tools (fs, bash, web_fetch, todo, ask_user, subagents, skills, craft snapshots/compare), permissions with modes and rules, Anthropic + OpenAI-compatible providers, MCP client (stdio/HTTP, elicitation), SQLite sessions, usage statistics, compaction, commented TOML config editable via toml_edit. React PWA: chat with live streaming, permission/question cards, modes (build/plan/craft), settings for providers/models/prices/MCP/permissions, raw config editor, statistics dashboard, i18n (en/de). Docker setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
f3a2fe3816
62 files changed
+21835
No files matched your search
@@ -0,0 +1,154 @@
|
||||
//! Built-in tools. Each tool declares a JSON schema, whether it is read-only
|
||||
//! (allowed in plan mode) and how to run it.
|
||||
|
||||
pub mod agent;
|
||||
pub mod bash;
|
||||
pub mod craft;
|
||||
pub mod fs;
|
||||
pub mod web;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::provider::ToolDef;
|
||||
use crate::types::ImageData;
|
||||
use anyhow::{Result, bail};
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ToolOutput {
|
||||
pub content: String,
|
||||
pub images: Vec<ImageData>,
|
||||
pub meta: Option<Value>,
|
||||
pub is_error: bool,
|
||||
}
|
||||
|
||||
impl ToolOutput {
|
||||
pub fn text(s: impl Into<String>) -> Self {
|
||||
Self { content: s.into(), ..Default::default() }
|
||||
}
|
||||
pub fn error(s: impl Into<String>) -> Self {
|
||||
Self { content: s.into(), is_error: true, ..Default::default() }
|
||||
}
|
||||
pub fn with_meta(mut self, meta: Value) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ToolCtx {
|
||||
pub app: Arc<App>,
|
||||
pub session_id: String,
|
||||
pub call_id: String,
|
||||
pub cwd: PathBuf,
|
||||
pub cancel: CancellationToken,
|
||||
/// Agent mode running this tool (subagents inherit restrictions).
|
||||
pub agent: String,
|
||||
}
|
||||
|
||||
impl ToolCtx {
|
||||
/// Resolve a user/model supplied path against the session cwd and make sure
|
||||
/// it stays inside the workspace.
|
||||
pub fn resolve(&self, p: &str) -> Result<PathBuf> {
|
||||
let p = p.trim();
|
||||
let raw = if Path::new(p).is_absolute() { PathBuf::from(p) } else { self.cwd.join(p) };
|
||||
let norm = normalize(&raw);
|
||||
let ws = normalize(&self.app.workspace());
|
||||
if !norm.starts_with(&ws) && !norm.starts_with("/tmp") {
|
||||
bail!("path {} is outside the workspace {}", norm.display(), ws.display());
|
||||
}
|
||||
Ok(norm)
|
||||
}
|
||||
|
||||
pub fn progress(&self, output: impl Into<String>) {
|
||||
self.app.emit(crate::types::Event::ToolProgress {
|
||||
session_id: self.session_id.clone(),
|
||||
call_id: self.call_id.clone(),
|
||||
output: output.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Lexical normalization (no symlink resolution, works for non-existing files).
|
||||
pub fn normalize(p: &Path) -> PathBuf {
|
||||
let mut out = PathBuf::new();
|
||||
for c in p.components() {
|
||||
match c {
|
||||
Component::ParentDir => {
|
||||
out.pop();
|
||||
}
|
||||
Component::CurDir => {}
|
||||
other => out.push(other.as_os_str()),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Tool: Send + Sync {
|
||||
fn name(&self) -> &str;
|
||||
fn description(&self) -> String;
|
||||
fn schema(&self) -> Value;
|
||||
/// Read-only tools are available in plan mode and never modify state.
|
||||
fn read_only(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Category used by the "accept_edits" permission mode.
|
||||
fn is_edit(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Short human title for permission prompts.
|
||||
fn title(&self, input: &Value) -> String {
|
||||
let _ = input;
|
||||
self.name().to_string()
|
||||
}
|
||||
/// Optional unified diff shown in permission prompts.
|
||||
async fn preview(&self, _input: &Value, _ctx: &ToolCtx) -> Option<String> {
|
||||
None
|
||||
}
|
||||
async fn run(&self, input: Value, ctx: &ToolCtx) -> Result<ToolOutput>;
|
||||
|
||||
fn def(&self) -> ToolDef {
|
||||
ToolDef { name: self.name().into(), description: self.description(), input_schema: self.schema() }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn builtin_tools() -> Vec<Arc<dyn Tool>> {
|
||||
vec![
|
||||
Arc::new(fs::Read),
|
||||
Arc::new(fs::Write),
|
||||
Arc::new(fs::Edit),
|
||||
Arc::new(fs::List),
|
||||
Arc::new(fs::Glob),
|
||||
Arc::new(fs::Grep),
|
||||
Arc::new(bash::Bash),
|
||||
Arc::new(web::WebFetch),
|
||||
Arc::new(agent::TodoWrite),
|
||||
Arc::new(agent::AskUser),
|
||||
Arc::new(agent::Task),
|
||||
Arc::new(craft::BrowserSnapshot),
|
||||
Arc::new(craft::ImageCompare),
|
||||
Arc::new(crate::prompts::SkillTool),
|
||||
]
|
||||
}
|
||||
|
||||
/// Helper for reading typed arguments.
|
||||
pub fn arg_str<'a>(input: &'a Value, key: &str) -> Result<&'a str> {
|
||||
input
|
||||
.get(key)
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("missing required string argument '{key}'"))
|
||||
}
|
||||
|
||||
pub fn truncate(s: &str, max: usize) -> String {
|
||||
if s.len() <= max {
|
||||
return s.to_string();
|
||||
}
|
||||
let mut cut = max;
|
||||
while !s.is_char_boundary(cut) {
|
||||
cut -= 1;
|
||||
}
|
||||
format!("{}\n\n[... output truncated: {} of {} bytes shown ...]", &s[..cut], cut, s.len())
|
||||
}
|
||||
Reference in new issue
Block a user