cursor: spatial memory navigation

Persistent cursor into the knowledge graph with navigation:
- temporal: forward/back among same-type nodes by timestamp
- hierarchical: up/down the digest tree (journal→daily→weekly→monthly)
- spatial: graph neighbor display at every position

The cursor file (~/.claude/memory/cursor) holds a single node key.
Show displays: temporal arrows, hierarchy links, semantic neighbors,
and full content. Date extraction from both timestamps and key names
handles the mixed-timestamp data gracefully.

This is the start of place cells — spatial awareness of position
in your own knowledge.
This commit is contained in:
ProofOfConcept 2026-03-13 22:31:23 -04:00
parent abce1bba16
commit 7c1b96293f
3 changed files with 409 additions and 0 deletions

View file

@ -199,6 +199,12 @@ EXAMPLES:
#[command(subcommand, name = "graph")]
GraphCmd(GraphCmd),
// ── Cursor (spatial memory) ──────────────────────────────────────
/// Navigate the memory graph with a persistent cursor
#[command(subcommand)]
Cursor(CursorCmd),
// ── Agents ────────────────────────────────────────────────────────
/// Agent and daemon operations
@ -239,6 +245,27 @@ enum NodeCmd {
Dump,
}
#[derive(Subcommand)]
enum CursorCmd {
/// Show current cursor position with context
Show,
/// Set cursor to a node key
Set {
/// Node key
key: Vec<String>,
},
/// Move cursor forward in time
Forward,
/// Move cursor backward in time
Back,
/// Move up the digest hierarchy (journal→daily→weekly→monthly)
Up,
/// Move down the digest hierarchy (to first child)
Down,
/// Clear the cursor
Clear,
}
#[derive(Subcommand)]
enum JournalCmd {
/// Write a journal entry to the store
@ -730,6 +757,9 @@ fn main() {
=> cmd_organize(&term, threshold, key_only, anchor),
},
// Cursor
Command::Cursor(sub) => cmd_cursor(sub),
// Agent
Command::Agent(sub) => match sub {
AgentCmd::Daemon(sub) => cmd_daemon(sub),
@ -2167,6 +2197,45 @@ fn cmd_load_context(stats: bool) -> Result<(), String> {
Ok(())
}
fn cmd_cursor(sub: CursorCmd) -> Result<(), String> {
match sub {
CursorCmd::Show => {
let store = store::Store::load()?;
cursor::show(&store)
}
CursorCmd::Set { key } => {
if key.is_empty() {
return Err("cursor set requires a key".into());
}
let key = key.join(" ");
let store = store::Store::load()?;
let bare = store::strip_md_suffix(&key);
if !store.nodes.contains_key(&bare) {
return Err(format!("Node not found: {}", bare));
}
cursor::set(&bare)?;
cursor::show(&store)
}
CursorCmd::Forward => {
let store = store::Store::load()?;
cursor::move_temporal(&store, true)
}
CursorCmd::Back => {
let store = store::Store::load()?;
cursor::move_temporal(&store, false)
}
CursorCmd::Up => {
let store = store::Store::load()?;
cursor::move_up(&store)
}
CursorCmd::Down => {
let store = store::Store::load()?;
cursor::move_down(&store)
}
CursorCmd::Clear => cursor::clear(),
}
}
fn cmd_render(key: &[String]) -> Result<(), String> {
if key.is_empty() {
return Err("render requires a key".into());