aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-ai/src/user_context/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/atuin-ai/src/user_context/mod.rs')
-rw-r--r--crates/atuin-ai/src/user_context/mod.rs68
1 files changed, 68 insertions, 0 deletions
diff --git a/crates/atuin-ai/src/user_context/mod.rs b/crates/atuin-ai/src/user_context/mod.rs
new file mode 100644
index 00000000..295efdec
--- /dev/null
+++ b/crates/atuin-ai/src/user_context/mod.rs
@@ -0,0 +1,68 @@
+//! User-authored context files (`TERMINAL.md`).
+//!
+//! Context files are markdown documents that can embed shell commands for
+//! dynamic content. Before each API request, context files are discovered
+//! by walking the filesystem, commands are executed, and the interpolated
+//! content is sent to the server as `config.user_contexts`.
+
+mod interpolate;
+mod walker;
+
+use std::path::Path;
+
+pub(crate) use walker::global_context_path;
+
+/// A fully resolved user context, ready to include in an API request.
+#[derive(Debug, Clone, serde::Serialize)]
+pub(crate) struct UserContext {
+ /// The path to the context file on disk.
+ pub path: String,
+ /// The interpolated content.
+ pub data: String,
+}
+
+/// Discover context files and interpolate embedded commands.
+///
+/// Walks from `start` up to the filesystem root looking for
+/// `.atuin/ai-context.md`, then checks `global_path`. Returns contexts
+/// ordered from most general (global/root) to most specific (deepest).
+pub(crate) async fn gather(
+ start: &Path,
+ global_path: Option<&Path>,
+ shell: &str,
+) -> Vec<UserContext> {
+ let raw_files = match walker::walk(start, global_path).await {
+ Ok(files) => files,
+ Err(e) => {
+ tracing::warn!("Failed to walk for context files: {e}");
+ return Vec::new();
+ }
+ };
+
+ if raw_files.is_empty() {
+ return Vec::new();
+ }
+
+ // Interpolate all files in parallel.
+ let mut handles = Vec::with_capacity(raw_files.len());
+ for file in raw_files {
+ let shell = shell.to_string();
+ handles.push(tokio::spawn(async move {
+ let data = interpolate::interpolate(&file.content, &shell).await;
+ UserContext {
+ path: file.path.to_string_lossy().to_string(),
+ data,
+ }
+ }));
+ }
+
+ let mut contexts = Vec::with_capacity(handles.len());
+ for handle in handles {
+ match handle.await {
+ Ok(ctx) => contexts.push(ctx),
+ Err(e) => tracing::warn!("Context interpolation task failed: {e}"),
+ }
+ }
+
+ contexts
+}