Skip to content
Merged
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
60 changes: 60 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ indicatif = "0.17"
# Misc
uuid = { version = "1", features = ["v4"] }
dirs = "5"
toml = "0.8"

[profile.release]
opt-level = 3
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,13 @@ Ephemeral ──promote──▶ Stable ──gc──▶ Deprecated
| `memora session start \| end \| current \| list` | Bracket a tool's run so events are recorded for replay. |
| `memora replay [--session ID] [--step]` | Walk through a recorded session's event stream. |
| `memora export --to <FORMAT> [...]` | Render the working set to `claude-code`, `cursor`, `cline`, `openai-assistant`, or `json`. |
| `memora gc [--threshold T] [--aggressive] [--dry-run]` | Two-phase importance-scored garbage collection. |
| `memora remote add NAME URL \| list \| remove NAME` | Manage filesystem remotes. |
| `memora push REMOTE [BRANCH]` | Push a branch to a configured remote (fast-forward only).|
| `memora pull REMOTE [BRANCH]` | Copy remote commits and update `refs/remotes/<remote>/<branch>`. |

Future phases add `import`, `gc`, `push`, `pull`. See `SPEC.md` for the
full roadmap.
This is the full v0.1 surface. Future versions will add `import`,
embeddings-driven semantic merge, and a real network transport.

---

Expand Down
26 changes: 20 additions & 6 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,10 @@ Status: **format version 1 (pre-1.0, may change).**
├── memora.db # SQLite database (the object store)
├── refs/
│ ├── heads/ # one file per branch; contents = commit id
│ └── remotes/ # remote-tracking refs (reserved, unused in v1)
│ └── remotes/ # one dir per remote, with branch refs inside
├── objects/ # reserved for future content-addressed blobs
└── sessions/ # reserved for future replay sessions
└── <session-id>/
├── events.jsonl
└── snapshots/
└── sessions/ # session events live in SQLite; this dir stores
# only `CURRENT` (active session marker)
```

### `HEAD`
Expand All @@ -40,9 +38,11 @@ One line, no trailing whitespace required:

### `config`

TOML. Minimum keys for v1:
TOML. Round-tripped through `crate::config::Config` so adding /
removing remotes produces a tidy diff. Minimum keys for v1:

```toml
# memora config (format v1)
[core]
format_version = 1
default_branch = "main"
Expand All @@ -51,6 +51,13 @@ default_branch = "main"
name = "human"
```

Optional `[remote.<name>]` sections are added by `memora remote add`:

```toml
[remote.origin]
url = "/abs/path/to/another/project"
```

Tools may add their own sections (e.g. `[claude_code]`) without breaking
compatibility, as long as `[core]` keys remain valid.

Expand All @@ -59,6 +66,13 @@ compatibility, as long as `[core]` keys remain valid.
Either empty (a branch with no commits yet) or a single line containing the
full commit sha256 the branch points at.

### `refs/remotes/<remote>/<branch>`

Same format as `refs/heads/<branch>`. Written by `memora pull` and
`memora push` to record the most recently observed tip of a remote
branch. Resolvable as `<remote>/<branch>` (e.g. `origin/main`) anywhere
that takes a revision.

---

## SQLite schema (v1)
Expand Down
72 changes: 72 additions & 0 deletions crates/memora-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ pub enum Command {

/// Export the working set into another tool's format.
Export(ExportArgs),

/// Run garbage collection on the working set.
Gc(GcArgs),

/// Manage configured remotes (`add`, `list`, `remove`).
Remote(RemoteArgs),

/// Push a branch to a configured remote.
Push(PushArgs),

/// Pull a branch from a configured remote.
Pull(PullArgs),
}

/// Arguments for `memora init`.
Expand Down Expand Up @@ -347,3 +359,63 @@ pub struct ExportArgs {
#[arg(long, value_name = "T")]
pub min_confidence: Option<f32>,
}

/// Arguments for `memora gc`.
#[derive(Debug, clap::Args)]
pub struct GcArgs {
/// Importance threshold below which nodes are marked `Deprecated`.
#[arg(long, default_value_t = 0.3)]
pub threshold: f32,

/// Mark and sweep in one pass instead of two.
#[arg(long)]
pub aggressive: bool,

/// Show what would happen without mutating anything.
#[arg(long = "dry-run")]
pub dry_run: bool,
}

/// Arguments for `memora remote`.
#[derive(Debug, clap::Args)]
pub struct RemoteArgs {
#[command(subcommand)]
pub command: RemoteCommand,
}

/// `memora remote` subcommands.
#[derive(Debug, clap::Subcommand)]
pub enum RemoteCommand {
/// Add (or overwrite) a named remote pointing at a filesystem path.
Add {
/// Remote name, e.g. `origin`.
name: String,
/// Filesystem path to another `.memora/`-bearing project.
url: String,
},
/// List configured remotes.
List,
/// Remove a remote and its tracking refs.
Remove {
/// Remote name.
name: String,
},
}

/// Arguments for `memora push`.
#[derive(Debug, clap::Args)]
pub struct PushArgs {
/// Remote name.
pub remote: String,
/// Branch to push. Defaults to the current branch.
pub branch: Option<String>,
}

/// Arguments for `memora pull`.
#[derive(Debug, clap::Args)]
pub struct PullArgs {
/// Remote name.
pub remote: String,
/// Branch to pull. Defaults to the current branch.
pub branch: Option<String>,
}
100 changes: 100 additions & 0 deletions crates/memora-cli/src/commands/gc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! `memora gc` — importance-scored garbage collection of the working set.

use std::env;

use anyhow::Result;

use memora_core::{GcAction, GcOptions, ImportanceWeights, Repository};

use crate::cli::GcArgs;
use crate::ui::{bold, dim, green, red, short_id, yellow};

/// Entry point for the `gc` subcommand.
pub fn run(args: GcArgs) -> Result<()> {
anyhow::ensure!(
(0.0..=1.0).contains(&args.threshold),
"--threshold must be in [0.0, 1.0] (got {})",
args.threshold
);

let cwd = env::current_dir()?;
let repo = Repository::open_from(&cwd)?;
let report = repo.gc(GcOptions {
threshold: args.threshold,
weights: ImportanceWeights::default(),
aggressive: args.aggressive,
dry_run: args.dry_run,
})?;

let title = if args.dry_run {
bold(yellow("gc — dry run"))
} else {
bold(green("gc"))
};
println!(
"{} threshold={:.2}{}",
title,
report.threshold,
if report.aggressive {
dim(" --aggressive").to_string()
} else {
String::new()
}
);
println!(
" {} swept · {} marked · {} kept",
report.swept(),
report.marked(),
report.kept(),
);

let mut sweeps = Vec::new();
let mut marks = Vec::new();
for action in &report.actions {
match action {
GcAction::Sweep { node } => sweeps.push(node),
GcAction::Mark { node, score } => marks.push((node, *score)),
GcAction::Keep { .. } => {}
}
}
if !sweeps.is_empty() {
println!("\n{}", bold(red("removed:")));
for n in sweeps {
println!(
" {} [{}] {}",
dim(short_id(&n.id)),
n.kind,
truncate(&n.content, 80)
);
}
}
if !marks.is_empty() {
println!("\n{}", bold(yellow("marked deprecated:")));
for (n, score) in marks {
println!(
" {} [{}] (score {:.2}) {}",
dim(short_id(&n.id)),
n.kind,
score,
truncate(&n.content, 80)
);
}
}
if args.dry_run && (report.marked() > 0 || report.swept() > 0) {
println!(
"\n{}",
dim("re-run without --dry-run to apply these changes.")
);
}
Ok(())
}

fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let mut out: String = s.chars().take(max - 1).collect();
out.push('…');
out
}
}
8 changes: 8 additions & 0 deletions crates/memora-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ mod branch;
mod commit;
mod diff;
mod export;
mod gc;
mod init;
mod log;
mod merge;
mod promote;
mod pull;
mod push;
mod remote;
mod replay;
mod rollback;
mod session;
Expand All @@ -40,5 +44,9 @@ pub fn dispatch(cli: Cli) -> Result<()> {
Command::Session(args) => session::run(args),
Command::Replay(args) => replay::run(args),
Command::Export(args) => export::run(args),
Command::Gc(args) => gc::run(args),
Command::Remote(args) => remote::run(args),
Command::Push(args) => push::run(args),
Command::Pull(args) => pull::run(args),
}
}
Loading
Loading