Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ it end-to-end: facts seeded through ICM are recalled with 100% accuracy
by Claude Code, Gemini CLI, Copilot CLI, Cursor Agent, and Aider —
**98% cross-agent efficiency** on the standard test.

If you want isolation (per-project, per-tool, etc.) pass `--db <path>`
or set `ICM_DB_PATH`; each path is an independent corpus.
If you want isolation (per-project, per-tool, etc.) pass `--db <path>`,
set `ICM_DB`, or use `icm init --per-project` to create a project-local
database under `.icm/`; each path is an independent corpus.

## Install

Expand All @@ -116,10 +117,18 @@ Re-run the install command to upgrade to the latest release. To pin a version, p
## Setup

```bash
# Auto-detect and configure all supported tools
# Auto-detect and configure all supported tools (global database)
icm init

# Per-project database (stores memories in .icm/memories.db)
icm init --per-project
```

`--per-project` creates a project-local `.icm/config.toml` at the git
root, so all `icm` commands run from within the project automatically
use an isolated database. Combine with global `icm init` — global
settings (tools, hooks) are unaffected; only the database is scoped.

Configures **17 tools** in one command ([full integration guide](docs/integrations.md)):

| Tool | MCP | Hooks | CLI | Skills |
Expand Down Expand Up @@ -427,12 +436,25 @@ Changing the model automatically re-creates the vector index (existing embedding

Single SQLite file. No external services, no network dependency.

Default (global) database location:

```
~/Library/Application Support/dev.icm.icm/memories.db # macOS
~/.local/share/dev.icm.icm/memories.db # Linux
C:\Users\<user>\AppData\Local\icm\icm\data\memories.db # Windows
```

Per-project database (created by `icm init --per-project`):

```
<project-root>/.icm/memories.db
```

ICM auto-detects a project-local `.icm/config.toml` from the current
working directory. A relative `[store].path` is resolved against the
git root, so all `icm` commands within the project tree use the
scoped database without needing `--db` on every invocation.

### Configuration

```bash
Expand Down
11 changes: 11 additions & 0 deletions config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,22 @@
# Linux: ~/.config/icm/config.toml
# Windows: C:\Users\<user>\AppData\Roaming\icm\icm\config\config.toml
# Or override with: ICM_CONFIG=/path/to/config.toml
#
# Database path resolution (highest priority first):
# 1. --db CLI flag
# 2. ICM_DB environment variable
# 3. [store].path in this config file
# 4. <project-root>/.icm/config.toml [store].path (auto-detected via git root)
# 5. <project-root>/.icm/memories.db (auto-detected via git root)
# 6. Platform default data directory (see src/main.rs default_db_path)

[store]
# SQLite database path (default: platform data dir).
# Uncomment to use a custom location:
# path = "/custom/path/to/memories.db"
#
# For per-project databases, set ICM_DB or create a .icm/config.toml
# at your project root via `icm init --per-project`.

[memory]
default_importance = "medium"
Expand Down
184 changes: 165 additions & 19 deletions crates/icm-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,8 @@ enum Commands {

/// Also write project-level instruction files into the current
/// directory (`CLAUDE.md`, `AGENTS.md`, `.windsurfrules`,
/// `.aider.conventions.md`, `.github/copilot-instructions.md`).
/// `.aider.conventions.md`, `.github/copilot-instructions.md`)
/// and set up a project-local database under `.icm/`.
/// Default behavior writes only to global per-tool paths
/// (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, etc.) so init
/// doesn't pollute every project tree.
Expand Down Expand Up @@ -1044,9 +1045,93 @@ fn default_db_path() -> PathBuf {
.unwrap_or_else(|| PathBuf::from("memories.db"))
}

fn open_store(db: Option<PathBuf>, embedding_dims: usize) -> Result<SqliteStore> {
let path = db.unwrap_or_else(default_db_path);
SqliteStore::with_dims(&path, embedding_dims).context("failed to open database")
/// Detect the project root (git repository root) from the current directory.
fn detect_project_root() -> Option<PathBuf> {
std::process::Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
.ok()
.and_then(|output| {
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if path.is_empty() { None } else { Some(PathBuf::from(path)) }
} else {
None
}
})
}

/// Resolve database path using hierarchical resolution:
///
/// 1. `--db` CLI flag (highest priority)
/// 2. `$ICM_DB` environment variable
/// 3. Global config `[store].path` from config file
/// 4. Project-local `.icm/config.toml` `[store].path` at git root
/// 5. Project-local `.icm/memories.db` at git root (if file exists)
/// 6. Default platform data directory
fn resolve_db_path(cli_db: Option<PathBuf>, cfg: &config::Config) -> PathBuf {
// 1. --db CLI flag
if let Some(db) = cli_db {
return db;
}

// 2. $ICM_DB env var
if let Ok(env_db) = std::env::var("ICM_DB") {
let path = PathBuf::from(env_db);
if !path.as_os_str().is_empty() {
return path;
}
}

// 3. Global config [store].path
if let Some(config_path) = &cfg.store.path {
let path = PathBuf::from(config_path);
if !path.as_os_str().is_empty() {
return path;
}
}

// 4. Project-local .icm/ directory (at git root)
if let Some(project_root) = detect_project_root() {
let icm_dir = project_root.join(".icm");
if icm_dir.is_dir() {
// 4a. .icm/config.toml with [store].path
let project_cfg = icm_dir.join("config.toml");
if project_cfg.exists() {
if let Ok(content) = std::fs::read_to_string(&project_cfg) {
if let Ok(value) = content.parse::<toml::Value>() {
if let Some(path_str) = value
.get("store")
.and_then(|s| s.get("path"))
.and_then(|p| p.as_str())
{
let path = if Path::new(path_str).is_absolute() {
PathBuf::from(path_str)
} else {
project_root.join(path_str)
};
if !path.as_os_str().is_empty() {
return path;
}
}
}
}
}

// 4b. .icm/memories.db (if file exists)
let project_db = icm_dir.join("memories.db");
if project_db.exists() {
return project_db;
}
}
}

// 5. Default platform data dir
default_db_path()
}

fn open_store(db: PathBuf, embedding_dims: usize) -> Result<SqliteStore> {
SqliteStore::with_dims(&db, embedding_dims).context("failed to open database")
}

#[cfg(feature = "embeddings")]
Expand Down Expand Up @@ -1112,7 +1197,9 @@ fn main() -> Result<()> {
}
}
let cli_db: Option<PathBuf> = cli.db.into_iter().next();
let db_path = cli_db.clone().unwrap_or_else(default_db_path);
let db_path = resolve_db_path(cli_db.clone(), &cfg);
// Keep cli_db for later config display (it's cloned so the
// original is still available for cmd_config below).

// `icm uninstall` must NOT open the SQLite store: a default
// `open_store` call would recreate the DB directory and WAL/SHM files
Expand All @@ -1125,7 +1212,7 @@ fn main() -> Result<()> {
std::process::exit(code);
}

let store = open_store(cli_db, embedding_dims)?;
let store = open_store(db_path.clone(), embedding_dims)?;

match command {
Commands::Store {
Expand Down Expand Up @@ -1364,7 +1451,7 @@ fn main() -> Result<()> {
mode,
force,
per_project,
} => cmd_init(mode, force, per_project),
} => cmd_init(mode, force, per_project, &db_path),
Commands::Doctor => cmd_doctor(),
Commands::Uninstall(_) => unreachable!("dispatched before open_store"),
Commands::Extract {
Expand Down Expand Up @@ -1428,7 +1515,7 @@ fn main() -> Result<()> {
println!("{result}");
Ok(())
}
Commands::Config => cmd_config(),
Commands::Config => cmd_config(cli_db, &cfg),
Commands::Upgrade { apply, check } => upgrade::cmd_upgrade(apply, check),
Commands::Bench { count } => cmd_bench(count),
Commands::BenchRecall {
Expand Down Expand Up @@ -3320,7 +3407,7 @@ pub(crate) fn cmd_matches_icm_pattern(cmd: &str, pattern: &str) -> bool {
cmd.contains(&format!("{pattern}.exe"))
}

fn cmd_init(mode: InitMode, force: bool, per_project: bool) -> Result<()> {
fn cmd_init(mode: InitMode, force: bool, per_project: bool, db_path: &Path) -> Result<()> {
let icm_bin = std::env::current_exe().context("cannot determine icm binary path")?;
let icm_bin_str = portable_command_path(&icm_bin);
let home = home_dir_str()?;
Expand Down Expand Up @@ -4042,9 +4129,35 @@ Do this BEFORE responding to the user. Not optional.
manifest.save(&manifest_path)?;
}

// --- Project-local .icm/ setup ---
// When --per-project is set, create a project-local database config
// so ICM uses a separate database per project. This creates:
// <git-root>/.icm/config.toml with [store] path = ".icm/memories.db"
// On subsequent invocations, the resolver will pick this up.
if per_project {
let project_root = detect_project_root()
.or_else(|| std::env::current_dir().ok());
if let Some(root) = project_root {
let icm_dir = root.join(".icm");
if !icm_dir.is_dir() {
std::fs::create_dir_all(&icm_dir)
.with_context(|| format!("creating {}", icm_dir.display()))?;
let project_cfg = icm_dir.join("config.toml");
std::fs::write(
&project_cfg,
"[store]\npath = \".icm/memories.db\"\n",
)
.with_context(|| format!("writing {}", project_cfg.display()))?;
println!("[project] created project-local .icm/ at {}", root.display());
} else {
println!("[project] .icm/ already exists at {}", root.display());
}
}
}

println!();
println!(" binary: {icm_bin_str}");
println!(" db: {}", default_db_path().display());
println!(" db: {}", db_path.display());
if !manifest.is_empty() {
println!(
" manifest: {} ({} entr{})",
Expand Down Expand Up @@ -5064,18 +5177,51 @@ fn inject_opencode_mcp_server(config_path: &Path, name: &str, icm_bin: &str) ->
Ok("configured".into())
}

fn cmd_config() -> Result<()> {
let cfg = config::load_config()?;
fn cmd_config(cli_db: Option<PathBuf>, cfg: &config::Config) -> Result<()> {
println!("Config: {}", config::show_config_path());
println!();
println!("[store]");
println!(
" path = {}",
cfg.store
.path
.as_deref()
.unwrap_or("(default platform path)")
);
let env_db = std::env::var("ICM_DB").ok();
let project_root = detect_project_root();
let resolved = resolve_db_path(cli_db, cfg);
println!(" resolved = {}", resolved.display());
println!(" path (config) = {}", cfg.store.path.as_deref().unwrap_or("(not set)"));
if let Some(ref env) = env_db {
println!(" ICM_DB (env) = {env}");
} else {
println!(" ICM_DB (env) = (not set)");
}
if let Some(root) = &project_root {
println!();
println!("[project]");
println!(" root = {}", root.display());
let icm_dir = root.join(".icm");
if icm_dir.is_dir() {
println!(" .icm/ exists");
let project_cfg = icm_dir.join("config.toml");
if project_cfg.exists() {
if let Ok(content) = std::fs::read_to_string(&project_cfg) {
if let Ok(value) = content.parse::<toml::Value>() {
if let Some(path_str) = value
.get("store")
.and_then(|s| s.get("path"))
.and_then(|p| p.as_str())
{
println!(" .icm/config.toml [store].path = {path_str}");
}
}
}
}
let project_db = icm_dir.join("memories.db");
if project_db.exists() {
println!(" .icm/memories.db exists");
} else {
println!(" .icm/memories.db (not found)");
}
} else {
println!(" .icm/ (not found)");
}
}
println!();
println!("[memory]");
println!(" default_importance = {}", cfg.memory.default_importance);
Expand Down
Loading
Loading