sgr_agent_tools/
prepend.rs1use std::sync::Arc;
7
8use schemars::JsonSchema;
9use serde::Deserialize;
10use serde_json::Value;
11use sgr_agent_core::agent_tool::{Tool, ToolError, ToolOutput, parse_args};
12use sgr_agent_core::context::AgentContext;
13use sgr_agent_core::schema::json_schema_for;
14
15use crate::backend::FileBackend;
16use crate::helpers::backend_err;
17
18pub struct PrependTool<B: FileBackend>(pub Arc<B>);
19
20#[derive(Deserialize, JsonSchema)]
21struct PrependArgs {
22 path: String,
24 header: String,
26}
27
28#[async_trait::async_trait]
29impl<B: FileBackend> Tool for PrependTool<B> {
30 fn name(&self) -> &str {
31 "prepend_to_file"
32 }
33 fn description(&self) -> &str {
34 "Prepend header text to a file. Body is preserved byte-for-byte (never re-typed). \
35 Use for adding YAML frontmatter to existing files without risking body corruption."
36 }
37 fn parameters_schema(&self) -> Value {
38 json_schema_for::<PrependArgs>()
39 }
40 async fn execute(&self, args: Value, _ctx: &mut AgentContext) -> Result<ToolOutput, ToolError> {
41 let a: PrependArgs = parse_args(&args)?;
42
43 let raw = self
45 .0
46 .read(&a.path, false, 0, 0)
47 .await
48 .map_err(backend_err)?;
49 let body = if raw.starts_with("$ ") {
51 raw.find('\n').map(|i| &raw[i + 1..]).unwrap_or(&raw)
52 } else {
53 &raw
54 };
55
56 let header = a.header.trim_end_matches('\n');
58 let combined = format!("{}\n{}", header, body);
59
60 self.0
61 .write(&a.path, &combined, 0, 0)
62 .await
63 .map_err(backend_err)?;
64
65 Ok(ToolOutput::text(format!(
66 "Prepended {} bytes to {} (body preserved)",
67 header.len(),
68 a.path
69 )))
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::mock_fs::MockFs;
77 use sgr_agent_core::agent_tool::Tool;
78
79 #[tokio::test]
80 async fn test_prepend_frontmatter() {
81 let fs = Arc::new(MockFs::new());
82 fs.add_file("doc.md", "# My Document\n\nSome content here.");
83 let tool = PrependTool(fs.clone());
84 let mut ctx = AgentContext::new();
85 let result = tool
86 .execute(
87 serde_json::json!({
88 "path": "doc.md",
89 "header": "---\ntitle: My Document\ntype: note\n---"
90 }),
91 &mut ctx,
92 )
93 .await
94 .unwrap();
95 assert!(result.content.contains("Prepended"));
96 let content = fs.content("doc.md").unwrap();
97 assert!(content.starts_with("---\ntitle: My Document"));
98 assert!(content.contains("# My Document\n\nSome content here."));
99 }
100
101 #[tokio::test]
102 async fn test_prepend_to_empty() {
103 let fs = Arc::new(MockFs::new());
104 fs.add_file("empty.md", "");
105 let tool = PrependTool(fs.clone());
106 let mut ctx = AgentContext::new();
107 let _ = tool
108 .execute(
109 serde_json::json!({"path": "empty.md", "header": "---\ntitle: New\n---"}),
110 &mut ctx,
111 )
112 .await
113 .unwrap();
114 let content = fs.content("empty.md").unwrap();
115 assert!(content.starts_with("---\ntitle: New\n---"));
116 }
117}