cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
// cli/node.rs — node subcommand handlers
|
|
|
|
|
//
|
|
|
|
|
// render, write, used, wrong, not-relevant, not-useful, gap,
|
|
|
|
|
// node-delete, node-rename, history, list-keys, list-edges,
|
|
|
|
|
// dump-json, lookup-bump, lookups.
|
|
|
|
|
|
|
|
|
|
use crate::store;
|
|
|
|
|
|
|
|
|
|
pub fn cmd_used(key: &[String]) -> Result<(), String> {
|
|
|
|
|
if key.is_empty() {
|
|
|
|
|
return Err("used requires a key".into());
|
|
|
|
|
}
|
poc-memory: POC_MEMORY_DRY_RUN=1 for agent testing
All mutating commands (write, delete, rename, link-add, journal write,
used, wrong, not-useful, gap) check POC_MEMORY_DRY_RUN after argument
validation but before mutation. If set, process exits silently — agent
tool calls are visible in the LLM output so we can see what it tried
to do without applying changes.
Read commands (render, search, graph link, journal tail) work normally
in dry-run mode so agents can still explore the graph.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:09:56 -04:00
|
|
|
super::check_dry_run();
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
let key = key.join(" ");
|
|
|
|
|
let mut store = store::Store::load()?;
|
|
|
|
|
let resolved = store.resolve_key(&key)?;
|
|
|
|
|
store.mark_used(&resolved);
|
|
|
|
|
|
|
|
|
|
// Also strengthen edges to this node — conscious-tier delta.
|
|
|
|
|
const DELTA: f32 = 0.01;
|
|
|
|
|
let mut strengthened = 0;
|
|
|
|
|
for rel in &mut store.relations {
|
|
|
|
|
if rel.deleted { continue; }
|
|
|
|
|
if rel.source_key == resolved || rel.target_key == resolved {
|
|
|
|
|
let old = rel.strength;
|
|
|
|
|
rel.strength = (rel.strength + DELTA).clamp(0.05, 0.95);
|
|
|
|
|
if (rel.strength - old).abs() > 0.001 {
|
|
|
|
|
rel.version += 1;
|
|
|
|
|
strengthened += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
store.save()?;
|
|
|
|
|
println!("Marked '{}' as used (strengthened {} edges)", resolved, strengthened);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn cmd_wrong(key: &str, context: &[String]) -> Result<(), String> {
|
|
|
|
|
let ctx = if context.is_empty() { None } else { Some(context.join(" ")) };
|
poc-memory: POC_MEMORY_DRY_RUN=1 for agent testing
All mutating commands (write, delete, rename, link-add, journal write,
used, wrong, not-useful, gap) check POC_MEMORY_DRY_RUN after argument
validation but before mutation. If set, process exits silently — agent
tool calls are visible in the LLM output so we can see what it tried
to do without applying changes.
Read commands (render, search, graph link, journal tail) work normally
in dry-run mode so agents can still explore the graph.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:09:56 -04:00
|
|
|
super::check_dry_run();
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
let mut store = store::Store::load()?;
|
|
|
|
|
let resolved = store.resolve_key(key)?;
|
|
|
|
|
store.mark_wrong(&resolved, ctx.as_deref());
|
|
|
|
|
store.save()?;
|
|
|
|
|
println!("Marked '{}' as wrong", resolved);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn cmd_not_relevant(key: &str) -> Result<(), String> {
|
|
|
|
|
let mut store = store::Store::load()?;
|
|
|
|
|
let resolved = store.resolve_key(key)?;
|
|
|
|
|
|
|
|
|
|
// Weaken all edges to this node — it was routed to incorrectly.
|
|
|
|
|
// Conscious-tier delta: 0.01 per edge.
|
|
|
|
|
const DELTA: f32 = -0.01;
|
|
|
|
|
let mut adjusted = 0;
|
|
|
|
|
for rel in &mut store.relations {
|
|
|
|
|
if rel.deleted { continue; }
|
|
|
|
|
if rel.source_key == resolved || rel.target_key == resolved {
|
|
|
|
|
let old = rel.strength;
|
|
|
|
|
rel.strength = (rel.strength + DELTA).clamp(0.05, 0.95);
|
|
|
|
|
if (rel.strength - old).abs() > 0.001 {
|
|
|
|
|
rel.version += 1;
|
|
|
|
|
adjusted += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
store.save()?;
|
|
|
|
|
println!("Not relevant: '{}' — weakened {} edges by {}", resolved, adjusted, DELTA.abs());
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn cmd_not_useful(key: &str) -> Result<(), String> {
|
poc-memory: POC_MEMORY_DRY_RUN=1 for agent testing
All mutating commands (write, delete, rename, link-add, journal write,
used, wrong, not-useful, gap) check POC_MEMORY_DRY_RUN after argument
validation but before mutation. If set, process exits silently — agent
tool calls are visible in the LLM output so we can see what it tried
to do without applying changes.
Read commands (render, search, graph link, journal tail) work normally
in dry-run mode so agents can still explore the graph.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:09:56 -04:00
|
|
|
// no args to validate
|
|
|
|
|
super::check_dry_run();
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
let mut store = store::Store::load()?;
|
|
|
|
|
let resolved = store.resolve_key(key)?;
|
|
|
|
|
// Same as wrong but with clearer semantics: node content is bad, edges are fine.
|
|
|
|
|
store.mark_wrong(&resolved, Some("not-useful"));
|
|
|
|
|
store.save()?;
|
|
|
|
|
println!("Not useful: '{}' — node weight reduced", resolved);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:16:55 -04:00
|
|
|
pub fn cmd_weight_set(key: &str, weight: f32) -> Result<(), String> {
|
|
|
|
|
super::check_dry_run();
|
|
|
|
|
let mut store = store::Store::load()?;
|
|
|
|
|
let resolved = store.resolve_key(key)?;
|
2026-03-25 01:55:21 -04:00
|
|
|
let (old, new) = store.set_weight(&resolved, weight)?;
|
|
|
|
|
println!("Weight: {} {:.2} → {:.2}", resolved, old, new);
|
|
|
|
|
store.save()?;
|
2026-03-20 12:16:55 -04:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
pub fn cmd_gap(description: &[String]) -> Result<(), String> {
|
|
|
|
|
if description.is_empty() {
|
|
|
|
|
return Err("gap requires a description".into());
|
|
|
|
|
}
|
poc-memory: POC_MEMORY_DRY_RUN=1 for agent testing
All mutating commands (write, delete, rename, link-add, journal write,
used, wrong, not-useful, gap) check POC_MEMORY_DRY_RUN after argument
validation but before mutation. If set, process exits silently — agent
tool calls are visible in the LLM output so we can see what it tried
to do without applying changes.
Read commands (render, search, graph link, journal tail) work normally
in dry-run mode so agents can still explore the graph.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:09:56 -04:00
|
|
|
super::check_dry_run();
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
let desc = description.join(" ");
|
|
|
|
|
let mut store = store::Store::load()?;
|
|
|
|
|
store.record_gap(&desc);
|
|
|
|
|
store.save()?;
|
|
|
|
|
println!("Recorded gap: {}", desc);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn cmd_list_keys(pattern: Option<&str>) -> Result<(), String> {
|
|
|
|
|
let store = store::Store::load()?;
|
|
|
|
|
let g = store.build_graph();
|
|
|
|
|
|
|
|
|
|
if let Some(pat) = pattern {
|
|
|
|
|
let pat_lower = pat.to_lowercase();
|
|
|
|
|
let (prefix, suffix, middle) = if pat_lower.starts_with('*') && pat_lower.ends_with('*') {
|
|
|
|
|
(None, None, Some(pat_lower.trim_matches('*').to_string()))
|
|
|
|
|
} else if pat_lower.starts_with('*') {
|
|
|
|
|
(None, Some(pat_lower.trim_start_matches('*').to_string()), None)
|
|
|
|
|
} else if pat_lower.ends_with('*') {
|
|
|
|
|
(Some(pat_lower.trim_end_matches('*').to_string()), None, None)
|
|
|
|
|
} else {
|
|
|
|
|
(None, None, Some(pat_lower.clone()))
|
|
|
|
|
};
|
|
|
|
|
let mut keys: Vec<_> = store.nodes.keys()
|
|
|
|
|
.filter(|k| {
|
|
|
|
|
let kl = k.to_lowercase();
|
|
|
|
|
if let Some(ref m) = middle { kl.contains(m.as_str()) }
|
|
|
|
|
else if let Some(ref p) = prefix { kl.starts_with(p.as_str()) }
|
|
|
|
|
else if let Some(ref s) = suffix { kl.ends_with(s.as_str()) }
|
|
|
|
|
else { true }
|
|
|
|
|
})
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect();
|
|
|
|
|
keys.sort();
|
|
|
|
|
for k in keys { println!("{}", k); }
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
crate::query_parser::run_query(&store, &g, "* | sort key asc")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn cmd_list_edges() -> Result<(), String> {
|
|
|
|
|
let store = store::Store::load()?;
|
|
|
|
|
for rel in &store.relations {
|
|
|
|
|
println!("{}\t{}\t{:.2}\t{:?}",
|
|
|
|
|
rel.source_key, rel.target_key, rel.strength, rel.rel_type);
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn cmd_dump_json() -> Result<(), String> {
|
|
|
|
|
let store = store::Store::load()?;
|
|
|
|
|
let json = serde_json::to_string_pretty(&store)
|
|
|
|
|
.map_err(|e| format!("serialize: {}", e))?;
|
|
|
|
|
println!("{}", json);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn cmd_node_delete(key: &[String]) -> Result<(), String> {
|
|
|
|
|
if key.is_empty() {
|
|
|
|
|
return Err("node-delete requires a key".into());
|
|
|
|
|
}
|
poc-memory: POC_MEMORY_DRY_RUN=1 for agent testing
All mutating commands (write, delete, rename, link-add, journal write,
used, wrong, not-useful, gap) check POC_MEMORY_DRY_RUN after argument
validation but before mutation. If set, process exits silently — agent
tool calls are visible in the LLM output so we can see what it tried
to do without applying changes.
Read commands (render, search, graph link, journal tail) work normally
in dry-run mode so agents can still explore the graph.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:09:56 -04:00
|
|
|
super::check_dry_run();
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
let key = key.join(" ");
|
|
|
|
|
let mut store = store::Store::load()?;
|
|
|
|
|
let resolved = store.resolve_key(&key)?;
|
|
|
|
|
store.delete_node(&resolved)?;
|
|
|
|
|
store.save()?;
|
|
|
|
|
println!("Deleted '{}'", resolved);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn cmd_node_rename(old_key: &str, new_key: &str) -> Result<(), String> {
|
poc-memory: POC_MEMORY_DRY_RUN=1 for agent testing
All mutating commands (write, delete, rename, link-add, journal write,
used, wrong, not-useful, gap) check POC_MEMORY_DRY_RUN after argument
validation but before mutation. If set, process exits silently — agent
tool calls are visible in the LLM output so we can see what it tried
to do without applying changes.
Read commands (render, search, graph link, journal tail) work normally
in dry-run mode so agents can still explore the graph.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:09:56 -04:00
|
|
|
// args are positional, always valid if present
|
|
|
|
|
super::check_dry_run();
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
let mut store = store::Store::load()?;
|
|
|
|
|
let old_resolved = store.resolve_key(old_key)?;
|
|
|
|
|
store.rename_node(&old_resolved, new_key)?;
|
|
|
|
|
store.save()?;
|
|
|
|
|
println!("Renamed '{}' → '{}'", old_resolved, new_key);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 13:47:14 -04:00
|
|
|
/// Render a node to a string: content + deduped footer links.
|
|
|
|
|
/// Used by both the CLI command and agent placeholders.
|
|
|
|
|
pub fn render_node(store: &store::Store, key: &str) -> Option<String> {
|
2026-03-25 01:55:21 -04:00
|
|
|
crate::hippocampus::memory::MemoryNode::from_store(store, key)
|
|
|
|
|
.map(|node| node.render())
|
2026-03-20 13:47:14 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn cmd_render(key: &[String]) -> Result<(), String> {
|
|
|
|
|
if key.is_empty() {
|
|
|
|
|
return Err("render requires a key".into());
|
|
|
|
|
}
|
|
|
|
|
let key = key.join(" ");
|
|
|
|
|
let store = store::Store::load()?;
|
|
|
|
|
let bare = store::strip_md_suffix(&key);
|
|
|
|
|
|
|
|
|
|
let rendered = render_node(&store, &bare)
|
|
|
|
|
.ok_or_else(|| format!("Node not found: {}", bare))?;
|
|
|
|
|
print!("{}", rendered);
|
2026-03-22 02:43:46 -04:00
|
|
|
|
2026-03-24 12:32:46 -04:00
|
|
|
// Mark as seen if we're inside a Claude session (not an agent subprocess —
|
|
|
|
|
// agents read the seen set but shouldn't write to it as a side effect of
|
|
|
|
|
// tool calls; only surface_agent_cycle should mark keys seen)
|
|
|
|
|
if std::env::var("POC_AGENT").is_err()
|
|
|
|
|
&& let Ok(session_id) = std::env::var("POC_SESSION_ID")
|
|
|
|
|
&& !session_id.is_empty()
|
|
|
|
|
{
|
move data home from ~/.claude/memory to ~/.consciousness
The consciousness project should stand independently of Claude Code.
All data, logs, sessions, and agent state now live under
~/.consciousness/ instead of being scattered across ~/.claude/memory/,
/tmp/claude-memory-search/, ~/.config/poc-memory/, and ~/.cache/.
Layout:
~/.consciousness/
*.capnp, *.bin, *.rkyv — store files
sessions/ — per-session state (seen sets, cookies)
logs/ — all logs (hook, agent, debug, dream)
agents/ — agent runtime state (pid files, output)
notifications/ — notification state
cache/ — transient data
Things that stay in ~/.claude/:
- projects/ (Claude Code transcripts)
- hooks/ (Claude Code hook system)
- telegram/ (shared integration)
- irc/ (shared integration)
- settings.json (Claude Code settings)
Debug log moves from /tmp/ to ~/.consciousness/logs/debug.log.
Session state moves from /tmp/claude-memory-search/ to sessions/.
Notifications move from ~/.claude/notifications/ to notifications/.
2026-03-27 21:05:15 -04:00
|
|
|
let state_dir = crate::store::memory_dir().join("sessions");
|
2026-03-24 12:32:46 -04:00
|
|
|
let seen_path = state_dir.join(format!("seen-{}", session_id));
|
|
|
|
|
if let Ok(mut f) = std::fs::OpenOptions::new()
|
|
|
|
|
.create(true).append(true).open(seen_path)
|
|
|
|
|
{
|
|
|
|
|
use std::io::Write;
|
|
|
|
|
let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S");
|
|
|
|
|
let _ = writeln!(f, "{}\t{}", ts, bare);
|
2026-03-22 02:43:46 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 13:39:48 -04:00
|
|
|
/// Check content for common inline reference problems:
|
|
|
|
|
/// - `poc-memory render key` embedded in content (render artifact, should be just `key`)
|
|
|
|
|
/// - `→ something` where something doesn't parse as a valid key
|
|
|
|
|
/// - `key` referencing a node that doesn't exist
|
|
|
|
|
fn validate_inline_refs(content: &str, store: &store::Store) -> Vec<String> {
|
|
|
|
|
let mut warnings = Vec::new();
|
|
|
|
|
|
|
|
|
|
for line in content.lines() {
|
|
|
|
|
// Check for render commands embedded in content
|
|
|
|
|
if line.contains("poc-memory render ") && !line.starts_with(" ") {
|
|
|
|
|
// Skip lines that look like CLI documentation/examples
|
|
|
|
|
if !line.contains("CLI") && !line.contains("equivalent") && !line.contains("tool") {
|
|
|
|
|
warnings.push(format!(
|
|
|
|
|
"render command in content (should be just `key`): {}",
|
|
|
|
|
line.chars().take(80).collect::<String>(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check → references
|
|
|
|
|
if let Some(rest) = line.trim().strip_prefix("→ ") {
|
|
|
|
|
// Extract the key (may be backtick-quoted)
|
|
|
|
|
let key = rest.trim().trim_matches('`').trim();
|
|
|
|
|
if !key.is_empty() && !store.nodes.contains_key(key) {
|
|
|
|
|
// Might be a poc-memory render artifact
|
|
|
|
|
if let Some(k) = key.strip_prefix("poc-memory render ") {
|
|
|
|
|
warnings.push(format!(
|
|
|
|
|
"render artifact in → reference (use `{}` not `poc-memory render {}`)", k, k,
|
|
|
|
|
));
|
|
|
|
|
} else if key.contains(' ') {
|
|
|
|
|
warnings.push(format!(
|
|
|
|
|
"→ reference doesn't look like a key: → {}", key,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
// Don't warn about missing keys — the target might be created later
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
warnings
|
|
|
|
|
}
|
|
|
|
|
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
pub fn cmd_history(key: &[String], full: bool) -> Result<(), String> {
|
|
|
|
|
if key.is_empty() {
|
|
|
|
|
return Err("history requires a key".into());
|
|
|
|
|
}
|
|
|
|
|
let raw_key = key.join(" ");
|
|
|
|
|
|
|
|
|
|
let store = store::Store::load()?;
|
|
|
|
|
let key = store.resolve_key(&raw_key).unwrap_or(raw_key);
|
|
|
|
|
drop(store);
|
|
|
|
|
|
|
|
|
|
let path = store::nodes_path();
|
|
|
|
|
if !path.exists() {
|
|
|
|
|
return Err("No node log found".into());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
use std::io::BufReader;
|
|
|
|
|
let file = std::fs::File::open(&path)
|
|
|
|
|
.map_err(|e| format!("open {}: {}", path.display(), e))?;
|
|
|
|
|
let mut reader = BufReader::new(file);
|
|
|
|
|
|
|
|
|
|
let mut versions: Vec<store::Node> = Vec::new();
|
|
|
|
|
while let Ok(msg) = capnp::serialize::read_message(&mut reader, capnp::message::ReaderOptions::new()) {
|
|
|
|
|
let log = msg.get_root::<crate::memory_capnp::node_log::Reader>()
|
|
|
|
|
.map_err(|e| format!("read log: {}", e))?;
|
|
|
|
|
for node_reader in log.get_nodes()
|
|
|
|
|
.map_err(|e| format!("get nodes: {}", e))? {
|
|
|
|
|
let node = store::Node::from_capnp_migrate(node_reader)?;
|
|
|
|
|
if node.key == key {
|
|
|
|
|
versions.push(node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if versions.is_empty() {
|
|
|
|
|
return Err(format!("No history found for '{}'", key));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
eprintln!("{} versions of '{}':\n", versions.len(), key);
|
|
|
|
|
for node in &versions {
|
|
|
|
|
let ts = if node.timestamp > 0 && node.timestamp < 4_000_000_000 {
|
|
|
|
|
store::format_datetime(node.timestamp)
|
|
|
|
|
} else {
|
|
|
|
|
format!("(raw:{})", node.timestamp)
|
|
|
|
|
};
|
2026-03-17 18:00:58 -04:00
|
|
|
let deleted_marker = if node.deleted { " DELETED" } else { "" };
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
let content_len = node.content.len();
|
|
|
|
|
if full {
|
2026-03-17 18:00:58 -04:00
|
|
|
eprintln!("=== v{} {} {}{} w={:.3} {}b ===",
|
|
|
|
|
node.version, ts, node.provenance, deleted_marker, node.weight, content_len);
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
eprintln!("{}", node.content);
|
|
|
|
|
} else {
|
|
|
|
|
let preview = crate::util::first_n_chars(&node.content, 120);
|
|
|
|
|
let preview = preview.replace('\n', "\\n");
|
2026-03-17 18:00:58 -04:00
|
|
|
eprintln!(" v{:<3} {} {:24} w={:.3} {}b{}",
|
|
|
|
|
node.version, ts, node.provenance, node.weight, content_len, deleted_marker);
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
eprintln!(" {}", preview);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 19:42:38 -04:00
|
|
|
if !full
|
|
|
|
|
&& let Some(latest) = versions.last() {
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
eprintln!("\n--- Latest content (v{}, {}) ---",
|
|
|
|
|
latest.version, latest.provenance);
|
|
|
|
|
print!("{}", latest.content);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn cmd_write(key: &[String]) -> Result<(), String> {
|
|
|
|
|
if key.is_empty() {
|
|
|
|
|
return Err("write requires a key (reads content from stdin)".into());
|
|
|
|
|
}
|
|
|
|
|
let raw_key = key.join(" ");
|
|
|
|
|
let mut content = String::new();
|
|
|
|
|
std::io::Read::read_to_string(&mut std::io::stdin(), &mut content)
|
|
|
|
|
.map_err(|e| format!("read stdin: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if content.trim().is_empty() {
|
|
|
|
|
return Err("No content on stdin".into());
|
|
|
|
|
}
|
poc-memory: POC_MEMORY_DRY_RUN=1 for agent testing
All mutating commands (write, delete, rename, link-add, journal write,
used, wrong, not-useful, gap) check POC_MEMORY_DRY_RUN after argument
validation but before mutation. If set, process exits silently — agent
tool calls are visible in the LLM output so we can see what it tried
to do without applying changes.
Read commands (render, search, graph link, journal tail) work normally
in dry-run mode so agents can still explore the graph.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:09:56 -04:00
|
|
|
super::check_dry_run();
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
|
|
|
|
|
let mut store = store::Store::load()?;
|
|
|
|
|
let key = store.resolve_key(&raw_key).unwrap_or(raw_key);
|
2026-03-20 13:39:48 -04:00
|
|
|
|
|
|
|
|
// Validate inline references: warn about render commands embedded
|
|
|
|
|
// in content (should be just `key`) and broken references.
|
|
|
|
|
let warnings = validate_inline_refs(&content, &store);
|
|
|
|
|
for w in &warnings {
|
|
|
|
|
eprintln!("warning: {}", w);
|
|
|
|
|
}
|
|
|
|
|
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
let result = store.upsert(&key, &content)?;
|
|
|
|
|
match result {
|
|
|
|
|
"unchanged" => println!("No change: '{}'", key),
|
|
|
|
|
"updated" => println!("Updated '{}' (v{})", key, store.nodes[&key].version),
|
|
|
|
|
_ => println!("Created '{}'", key),
|
|
|
|
|
}
|
|
|
|
|
if result != "unchanged" {
|
|
|
|
|
store.save()?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 23:51:06 -04:00
|
|
|
pub fn cmd_edit(key: &[String]) -> Result<(), String> {
|
|
|
|
|
if key.is_empty() {
|
|
|
|
|
return Err("edit requires a key".into());
|
|
|
|
|
}
|
|
|
|
|
let raw_key = key.join(" ");
|
|
|
|
|
let store = store::Store::load()?;
|
|
|
|
|
let key = store.resolve_key(&raw_key).unwrap_or(raw_key.clone());
|
|
|
|
|
|
|
|
|
|
let content = store.nodes.get(&key)
|
|
|
|
|
.map(|n| n.content.clone())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
let tmp = std::env::temp_dir().join(format!("poc-memory-edit-{}.md", key.replace('/', "_")));
|
|
|
|
|
std::fs::write(&tmp, &content)
|
|
|
|
|
.map_err(|e| format!("write temp file: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".into());
|
|
|
|
|
let status = std::process::Command::new(&editor)
|
|
|
|
|
.arg(&tmp)
|
|
|
|
|
.status()
|
|
|
|
|
.map_err(|e| format!("spawn {}: {}", editor, e))?;
|
|
|
|
|
|
|
|
|
|
if !status.success() {
|
|
|
|
|
let _ = std::fs::remove_file(&tmp);
|
|
|
|
|
return Err(format!("{} exited with {}", editor, status));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let new_content = std::fs::read_to_string(&tmp)
|
|
|
|
|
.map_err(|e| format!("read temp file: {}", e))?;
|
|
|
|
|
let _ = std::fs::remove_file(&tmp);
|
|
|
|
|
|
|
|
|
|
if new_content == content {
|
|
|
|
|
println!("No change: '{}'", key);
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if new_content.trim().is_empty() {
|
|
|
|
|
return Err("Content is empty, aborting".into());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drop(store);
|
|
|
|
|
let mut store = store::Store::load()?;
|
|
|
|
|
let result = store.upsert(&key, &new_content)?;
|
|
|
|
|
match result {
|
|
|
|
|
"unchanged" => println!("No change: '{}'", key),
|
|
|
|
|
"updated" => println!("Updated '{}' (v{})", key, store.nodes[&key].version),
|
|
|
|
|
_ => println!("Created '{}'", key),
|
|
|
|
|
}
|
|
|
|
|
if result != "unchanged" {
|
|
|
|
|
store.save()?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs:
render, write, used, wrong, not-relevant, not-useful, gap,
node-delete, node-rename, history, list-keys, list-edges,
dump-json, lookup-bump, lookups.
main.rs: 2518 → 2193 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-14 18:02:12 -04:00
|
|
|
pub fn cmd_lookup_bump(keys: &[String]) -> Result<(), String> {
|
|
|
|
|
if keys.is_empty() {
|
|
|
|
|
return Err("lookup-bump requires at least one key".into());
|
|
|
|
|
}
|
|
|
|
|
let keys: Vec<&str> = keys.iter().map(|s| s.as_str()).collect();
|
|
|
|
|
crate::lookups::bump_many(&keys)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn cmd_lookups(date: Option<&str>) -> Result<(), String> {
|
|
|
|
|
let date = date.map(|d| d.to_string())
|
|
|
|
|
.unwrap_or_else(|| chrono::Local::now().format("%Y-%m-%d").to_string());
|
|
|
|
|
|
|
|
|
|
let store = store::Store::load()?;
|
|
|
|
|
let keys: Vec<String> = store.nodes.values().map(|n| n.key.clone()).collect();
|
|
|
|
|
let resolved = crate::lookups::dump_resolved(&date, &keys)?;
|
|
|
|
|
|
|
|
|
|
if resolved.is_empty() {
|
|
|
|
|
println!("No lookups for {}", date);
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
println!("Lookups for {}:", date);
|
|
|
|
|
for (key, count) in &resolved {
|
|
|
|
|
println!(" {:4} {}", count, key);
|
|
|
|
|
}
|
|
|
|
|
println!("\n{} distinct keys, {} total lookups",
|
|
|
|
|
resolved.len(),
|
|
|
|
|
resolved.iter().map(|(_, c)| *c as u64).sum::<u64>());
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|