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 @@
+
+[](https://opencrater.to)
+
+
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!({