1use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use rig_core::completion::ToolAnnotations;
7use rig_core::tool::{
8 FileChange, FileState, FilesChanged, Scope, Tool, ToolContext, ToolExecutionError, Touches,
9 args_schema,
10};
11use schemars::JsonSchema;
12use serde::Deserialize;
13
14use crate::fs::{Previous, io_error};
15use crate::workspace::{Local, Stat, Workspace};
16use crate::{MAX_FILE_BYTES, blocking};
17
18#[derive(Clone, Debug)]
24pub struct Move {
25 pub workspace: Arc<dyn Workspace>,
27 pub base: Option<PathBuf>,
30 pub confine: bool,
33}
34
35impl Default for Move {
36 fn default() -> Self {
38 Self {
39 workspace: Arc::new(Local),
40 base: None,
41 confine: false,
42 }
43 }
44}
45
46#[derive(Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
48#[serde(deny_unknown_fields)]
49pub struct MoveArgs {
50 pub from: String,
52 pub to: String,
54 #[serde(default)]
56 pub overwrite: bool,
57}
58
59impl MoveArgs {
60 pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
62 Self {
63 from: from.into(),
64 to: to.into(),
65 overwrite: false,
66 }
67 }
68
69 pub fn overwrite(mut self) -> Self {
71 self.overwrite = true;
72 self
73 }
74}
75
76impl Tool for Move {
77 const NAME: &'static str = "move";
78 type Args = MoveArgs;
79 type Output = String;
80 type Error = ToolExecutionError;
81
82 fn description(&self) -> String {
83 "Move or rename a file, not with `mv` in `shell`, creating the new path's directory. \
84 Refuses directories, and an existing file at `to` unless `overwrite` is true."
85 .to_owned()
86 }
87
88 fn parameters(&self) -> serde_json::Value {
89 args_schema::<MoveArgs>()
90 }
91
92 fn annotations(&self) -> ToolAnnotations {
93 ToolAnnotations::default()
94 .with_destructive(true)
95 .with_idempotent(false)
96 .with_open_world(false)
97 }
98
99 fn footprint(&self, args: &MoveArgs) -> Touches {
100 let base = self.base.as_deref();
101 Touches::Writes(Scope::Paths(vec![
102 self.workspace.resolve(base, &args.from),
103 self.workspace.resolve(base, &args.to),
104 ]))
105 }
106
107 async fn call(
108 &self,
109 context: &mut ToolContext,
110 args: MoveArgs,
111 ) -> Result<String, ToolExecutionError> {
112 let base = self.base.as_deref();
113 let from = self.workspace.place(base, self.confine, &args.from)?;
114 let to = self.workspace.place(base, self.confine, &args.to)?;
115 let workspace = self.workspace.clone();
116 let overwrite = args.overwrite;
117 let changes = blocking(move || move_file(&*workspace, &from, &to, overwrite)).await?;
118 context.insert_result(FilesChanged(changes))?;
119 Ok(format!("Moved {} to {}.", args.from, args.to))
120 }
121}
122
123fn move_file(
126 workspace: &dyn Workspace,
127 from: &Path,
128 to: &Path,
129 overwrite: bool,
130) -> Result<Vec<FileChange>, ToolExecutionError> {
131 let (shown_from, shown_to) = (from.to_string_lossy(), to.to_string_lossy());
132 let size = match workspace
133 .stat(from)
134 .map_err(|error| io_error(&shown_from, error))?
135 {
136 Stat::File { size, .. } => size,
137 Stat::Missing => {
138 return Err(ToolExecutionError::not_found(format!(
139 "{shown_from}: no such file"
140 )));
141 }
142 Stat::Dir | Stat::Other => {
143 return Err(ToolExecutionError::invalid_args(format!(
144 "{shown_from} is not a regular file; `move` moves files only"
145 )));
146 }
147 };
148 if from == to {
149 return Err(ToolExecutionError::invalid_args(format!(
150 "{shown_from} and {shown_to} are the same file"
151 )));
152 }
153 match workspace
154 .stat(to)
155 .map_err(|error| io_error(&shown_to, error))?
156 {
157 Stat::Missing => {}
158 Stat::File { .. } if overwrite => {}
159 Stat::File { .. } => {
160 return Err(ToolExecutionError::invalid_args(format!(
161 "{shown_to} already exists; set `overwrite` to replace it"
162 )));
163 }
164 Stat::Dir | Stat::Other => {
165 return Err(ToolExecutionError::invalid_args(format!(
166 "{shown_to} is not a regular file; give the file's new path"
167 )));
168 }
169 }
170 let moved = (size <= MAX_FILE_BYTES)
172 .then(|| workspace.read(from, MAX_FILE_BYTES).ok())
173 .flatten()
174 .map(|contents| contents.bytes);
175 let before = workspace.previous(to);
176 workspace
177 .rename(from, to)
178 .map_err(|error| io_error(&shown_from, error))?;
179 let bytes = moved.as_deref().unwrap_or_default();
182 let mut created = Previous::change(before, &shown_to, Some(bytes));
183 let read = created.after.take().filter(|_| moved.is_some());
184 let state = read.unwrap_or_else(|| FileState::unread(size));
185 created.after = Some(state.clone());
186 let deleted =
187 FileChange::deleted(shown_from, state.lines.unwrap_or_default()).with_before(state);
188 Ok(vec![deleted, created])
189}
190
191#[cfg(test)]
192mod tests;