Files
codecraft/server/src/mentions.rs
T
Richard ZinkandClaude Opus 5.5 aa1f8cf016 Add API tokens, CLI server API and shared core crate
- Personal API tokens (full/read scope, expiry, revoke) and CLI password login
- Cargo workspace: codecraft-core (types, providers, permission rules) shared by
  server and the upcoming terminal client; server re-exports the old paths
- CLI server API: /cli/config, /cli/skill, /cli/tool (server MCP tools),
  /llm/stream model gateway with budgets and usage per user
- Theme/language preferences mirrored to the server; personal memory endpoint
- Statistics: provider and mode tables stacked; CLI usage labelled
- cli/: work in progress (terminal client is being rewritten as a local TUI)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 23:18:50 +01:00

85 lines
3.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `@path` mentions in user prompts: the mentioned files (or folder listings)
//! are attached to the prompt, read with the regular read/list tools.
use crate::app::App;
use crate::tools::{Tool, ToolCtx};
use crate::types::Part;
use serde_json::json;
use std::path::PathBuf;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
const MAX_MENTIONS: usize = 10;
const MAX_CHARS: usize = 60_000;
/// `@token`s at the start of the text or after whitespace (not e-mail addresses).
pub fn find(text: &str) -> Vec<String> {
let re = regex::Regex::new(r"(?:^|\s)@([\w./~+\-]+)").unwrap();
let mut out: Vec<String> = Vec::new();
for c in re.captures_iter(text) {
let token = c[1].trim_end_matches(['.', ',', ';', ':', '!', '?', ')']).to_string();
if !token.is_empty() && !out.contains(&token) {
out.push(token);
}
}
out
}
/// Parts to append to a user prompt for its mentions.
pub async fn expand(app: &Arc<App>, session_id: &str, text: &str) -> Vec<Part> {
let tokens = find(text);
if tokens.is_empty() {
return vec![];
}
let Ok(session) = app.session(session_id) else { return vec![] };
let cwd = PathBuf::from(&session.cwd);
let ctx = ToolCtx {
app: app.clone(),
session_id: session_id.to_string(),
call_id: String::new(),
cwd: cwd.clone(),
cancel: CancellationToken::new(),
agent: session.agent.clone(),
roots: app.user_roots(session.owner_id.as_deref()),
sandbox: crate::sandbox::spec_for(app, session.owner_id.as_deref()),
clean_room: session.clean_room(app.config().craft.clean_room),
owner_id: None,
};
let mut parts = Vec::new();
for token in tokens.into_iter().take(MAX_MENTIONS) {
let Ok(path) = ctx.resolve_read(&token) else { continue };
if !path.exists() {
continue;
}
let res = if path.is_dir() {
crate::tools::fs::List.run(json!({"path": token}), &ctx).await
} else {
crate::tools::fs::Read.run(json!({"path": token}), &ctx).await
};
let Ok(out) = res else { continue };
let mut content = out.content;
if content.len() > MAX_CHARS {
let mut end = MAX_CHARS;
while !content.is_char_boundary(end) {
end -= 1;
}
content.truncate(end);
content.push_str("\n[… truncated – read the rest with the read tool]");
}
let kind = if path.is_dir() { "folder" } else { "file" };
parts.push(Part::Text { text: format!("<mentioned-{kind} path=\"{token}\">\n{content}\n</mentioned-{kind}>") });
parts.extend(out.images.into_iter().map(|image| Part::Image { image }));
}
parts
}
#[cfg(test)]
mod tests {
#[test]
fn finds_mentions() {
assert_eq!(super::find("look at @src/main.rs, and @README.md."), vec!["src/main.rs", "README.md"]);
assert!(super::find("mail me at a@b.de").is_empty());
assert_eq!(super::find("@docs/ please"), vec!["docs/"]);
}
}