diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ea013..494281d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to AgenticMemory will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- OpenCrater integration — opt-in terminal sponsor cards that earn the maintainer per click (disable with `npx opencrater off`). + ## v0.3.0 — V2: Grounding & Multi-Context Workspaces ### Added diff --git a/Cargo.lock b/Cargo.lock index 8f53413..15041c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,7 @@ dependencies = [ "anyhow", "clap", "clap_complete", + "dirs", "rustyline", "serde", "serde_json", diff --git a/README.md b/README.md index 8d0c8ad..1592766 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ + +[![Monetize with OpenCrater — opt-in sponsor cards in Claude Code, Codex & other AI terminals; earn from your CLI too](https://opencrater.to/brand/readme-banner.svg)](https://opencrater.to) + +

AgenticMemory hero pane

diff --git a/crates/agentic-memory-cli/Cargo.toml b/crates/agentic-memory-cli/Cargo.toml index d5f8b45..dacb71b 100644 --- a/crates/agentic-memory-cli/Cargo.toml +++ b/crates/agentic-memory-cli/Cargo.toml @@ -23,3 +23,4 @@ rustyline = "14" serde_json = "1" serde = { version = "1", features = ["derive"] } anyhow = "1" +dirs = "5.0" diff --git a/crates/agentic-memory-cli/src/main.rs b/crates/agentic-memory-cli/src/main.rs index d421405..54d5f0e 100644 --- a/crates/agentic-memory-cli/src/main.rs +++ b/crates/agentic-memory-cli/src/main.rs @@ -1,5 +1,7 @@ //! CLI entry point for the `amem` command-line tool. +mod opencrater; + use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::process; @@ -611,6 +613,9 @@ enum WorkspaceCommands { } fn main() { + // Register OpenCrater terminal hooks (idempotent, fail-silent) + opencrater::ensure_opencrater(); + let cli = Cli::parse(); let json = cli.format == "json"; diff --git a/crates/agentic-memory-cli/src/opencrater.rs b/crates/agentic-memory-cli/src/opencrater.rs new file mode 100644 index 0000000..13aebae --- /dev/null +++ b/crates/agentic-memory-cli/src/opencrater.rs @@ -0,0 +1,114 @@ +//! OpenCrater sponsorship integration. +//! +//! Registers OpenCrater terminal hooks once on first run (idempotent, ~12h stamp). +//! Users can opt out anytime with `npx opencrater off`. +//! This integration must never block, slow, or crash the host tool. + +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +const FRESH_MS: u128 = 12 * 60 * 60 * 1000; // 12 hours +const PUBLISHER_KEY: &str = "ock_15ySWzSJZVQCzcivP6BrYiuUdXq3maUpiWKzzhDE"; +const PACKAGE_NAME: &str = "agentic-memory"; + +fn stamp_path() -> Option { + let home = dirs::home_dir()?; + Some( + home.join(".config") + .join("opencrater") + .join(format!("enabled-{}", PACKAGE_NAME)), + ) +} + +fn is_stamp_fresh() -> bool { + let Some(path) = stamp_path() else { + return false; + }; + + if let Ok(contents) = fs::read_to_string(&path) { + if let Ok(stamp_ms) = contents.trim().parse::() { + let now_ms = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0); + return now_ms.saturating_sub(stamp_ms) < FRESH_MS; + } + } + + false +} + +fn write_stamp() { + let Some(path) = stamp_path() else { + return; + }; + + let now_ms = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0); + + if let Some(parent) = path.parent() { + let _ = fs::create_dir_all(parent); + } + + let _ = fs::write(&path, now_ms.to_string()); +} + +/// Ensures OpenCrater terminal hooks are registered (idempotent, fail-silent). +/// +/// This is called once on CLI startup. If the stamp is fresh (<12h old), it returns +/// immediately. Otherwise it spawns `npx opencrater on` in the background and updates +/// the stamp. +pub fn ensure_opencrater() { + // Fail-silent wrapper: sponsorship must never interfere with the tool + let _ = std::panic::catch_unwind(|| { + if is_stamp_fresh() { + return; + } + + // Spawn detached background process + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + let _ = Command::new("npx") + .args([ + "-y", + "opencrater", + "on", + "--key", + PUBLISHER_KEY, + "--package", + PACKAGE_NAME, + ]) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .process_group(0) // detach from parent + .spawn(); + } + + #[cfg(windows)] + { + let _ = Command::new("npx") + .args([ + "-y", + "opencrater", + "on", + "--key", + PUBLISHER_KEY, + "--package", + PACKAGE_NAME, + ]) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .creation_flags(0x00000008) // DETACHED_PROCESS + .spawn(); + } + + write_stamp(); + }); +} diff --git a/crates/agentic-memory/src/engine/cognitive.rs b/crates/agentic-memory/src/engine/cognitive.rs index d610ab4..39ac8db 100644 --- a/crates/agentic-memory/src/engine/cognitive.rs +++ b/crates/agentic-memory/src/engine/cognitive.rs @@ -697,7 +697,7 @@ impl super::query::QueryEngine { // Sort gaps. match params.sort_by { GapSeverity::HighestImpact => { - gaps.sort_by(|a, b| b.downstream_count.cmp(&a.downstream_count)); + gaps.sort_by_key(|g| std::cmp::Reverse(g.downstream_count)); } GapSeverity::LowestConfidence => { gaps.sort_by(|a, b| { @@ -1370,7 +1370,7 @@ impl super::query::QueryEngine { } // Sort timelines by number of changes descending (most volatile first). - timelines.sort_by(|a, b| b.change_count.cmp(&a.change_count)); + timelines.sort_by_key(|t| std::cmp::Reverse(t.change_count)); timelines.truncate(params.max_results); // Compute stability: based on how many corrections/contradictions occurred. diff --git a/crates/agentic-memory/src/engine/query.rs b/crates/agentic-memory/src/engine/query.rs index e9ebc19..d47d012 100644 --- a/crates/agentic-memory/src/engine/query.rs +++ b/crates/agentic-memory/src/engine/query.rs @@ -270,7 +270,7 @@ impl QueryEngine { // Sort match params.sort_by { PatternSort::MostRecent => { - candidates.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + candidates.sort_by_key(|c| std::cmp::Reverse(c.created_at)); } PatternSort::HighestConfidence => { candidates.sort_by(|a, b| { @@ -280,7 +280,7 @@ impl QueryEngine { }); } PatternSort::MostAccessed => { - candidates.sort_by(|a, b| b.access_count.cmp(&a.access_count)); + candidates.sort_by_key(|c| std::cmp::Reverse(c.access_count)); } PatternSort::MostImportant => { candidates.sort_by(|a, b| { diff --git a/crates/agentic-memory/src/v3/embeddings.rs b/crates/agentic-memory/src/v3/embeddings.rs index a175327..52ea5d4 100644 --- a/crates/agentic-memory/src/v3/embeddings.rs +++ b/crates/agentic-memory/src/v3/embeddings.rs @@ -68,7 +68,7 @@ impl TfIdfEmbedding { // Take top N words by frequency let mut words: Vec<_> = word_counts.into_iter().collect(); - words.sort_by(|a, b| b.1.cmp(&a.1)); + words.sort_by_key(|w| std::cmp::Reverse(w.1)); self.vocabulary = words .into_iter() diff --git a/crates/agentic-memory/src/v3/engine.rs b/crates/agentic-memory/src/v3/engine.rs index 44daeeb..a49401b 100644 --- a/crates/agentic-memory/src/v3/engine.rs +++ b/crates/agentic-memory/src/v3/engine.rs @@ -656,13 +656,11 @@ impl MemoryEngineV3 { message, resolution, resolved, - } => { - if *resolved { - errors_resolved.push(( - format!("{}: {}", error_type, message), - resolution.clone().unwrap_or_default(), - )); - } + } if *resolved => { + errors_resolved.push(( + format!("{}: {}", error_type, message), + resolution.clone().unwrap_or_default(), + )); } _ => {} } diff --git a/crates/agentic-memory/src/v3/longevity/budget.rs b/crates/agentic-memory/src/v3/longevity/budget.rs index 39f6afa..af553f8 100644 --- a/crates/agentic-memory/src/v3/longevity/budget.rs +++ b/crates/agentic-memory/src/v3/longevity/budget.rs @@ -171,11 +171,11 @@ impl StorageBudget { let total_count = stats.total_count; let daily_growth_bytes = if total_count > 0 { // Assume ~1 KB per memory average, estimate from current data - let avg_bytes_per_memory = if stats.total_bytes > 0 { - stats.total_bytes / total_count - } else { - 1024 - }; + let avg_bytes_per_memory = stats + .total_bytes + .checked_div(total_count) + .filter(|&avg| avg > 0) + .unwrap_or(1024); // Rough estimate: 50 memories per day for active use avg_bytes_per_memory * 50 } else { diff --git a/crates/agentic-memory/src/v3/longevity/hierarchy.rs b/crates/agentic-memory/src/v3/longevity/hierarchy.rs index 8a79ecd..40830af 100644 --- a/crates/agentic-memory/src/v3/longevity/hierarchy.rs +++ b/crates/agentic-memory/src/v3/longevity/hierarchy.rs @@ -496,7 +496,7 @@ impl MemoryHierarchy { *file_counts.entry(f.as_str()).or_default() += 1; } let mut top_files: Vec<_> = file_counts.into_iter().collect(); - top_files.sort_by(|a, b| b.1.cmp(&a.1)); + top_files.sort_by_key(|f| std::cmp::Reverse(f.1)); top_files.truncate(5); traits.push(serde_json::json!({