cli: extract journal and misc commands, complete split
Move remaining extractable handlers into cli/journal.rs and cli/misc.rs. Functions depending on main.rs helpers (cmd_journal_tail, cmd_journal_write, cmd_load_context, cmd_cursor, cmd_daemon, cmd_digest, cmd_experience_mine, cmd_apply_agent) remain in main.rs — next step is moving those helpers to library code. main.rs: 3130 → 1331 lines (57% reduction). cli/ total: 1860 lines across 6 focused files. Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
f423cf22df
commit
8640d50990
4 changed files with 417 additions and 405 deletions
55
poc-memory/src/cli/journal.rs
Normal file
55
poc-memory/src/cli/journal.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
// cli/journal.rs — journal subcommand handlers
|
||||||
|
|
||||||
|
use crate::store;
|
||||||
|
use crate::store::StoreView;
|
||||||
|
|
||||||
|
pub fn cmd_tail(n: usize, full: bool) -> Result<(), String> {
|
||||||
|
let path = crate::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);
|
||||||
|
|
||||||
|
// Read all entries, keep last N
|
||||||
|
let mut entries: Vec<crate::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 = crate::store::Node::from_capnp_migrate(node_reader)?;
|
||||||
|
entries.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = entries.len().saturating_sub(n);
|
||||||
|
for node in &entries[start..] {
|
||||||
|
let ts = if node.timestamp > 0 && node.timestamp < 4_000_000_000 {
|
||||||
|
crate::store::format_datetime(node.timestamp)
|
||||||
|
} else {
|
||||||
|
format!("(raw:{})", node.timestamp)
|
||||||
|
};
|
||||||
|
let del = if node.deleted { " [DELETED]" } else { "" };
|
||||||
|
if full {
|
||||||
|
eprintln!("--- {} (v{}) {} via {} w={:.3}{} ---",
|
||||||
|
node.key, node.version, ts, node.provenance, node.weight, del);
|
||||||
|
eprintln!("{}\n", node.content);
|
||||||
|
} else {
|
||||||
|
let preview = crate::util::first_n_chars(&node.content, 100).replace('\n', "\\n");
|
||||||
|
eprintln!(" {} v{} w={:.2}{}",
|
||||||
|
ts, node.version, node.weight, del);
|
||||||
|
eprintln!(" {} via {}", node.key, node.provenance);
|
||||||
|
if !preview.is_empty() {
|
||||||
|
eprintln!(" {}", preview);
|
||||||
|
}
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
210
poc-memory/src/cli/misc.rs
Normal file
210
poc-memory/src/cli/misc.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
// cli/misc.rs — misc subcommand handlers
|
||||||
|
|
||||||
|
use crate::store;
|
||||||
|
use crate::store::StoreView;
|
||||||
|
|
||||||
|
pub fn cmd_search(terms: &[String], pipeline_args: &[String], expand: bool, full: bool, debug: bool, fuzzy: bool, content: bool) -> Result<(), String> {
|
||||||
|
use crate::store::StoreView;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
// Parse pipeline stages (unified: algorithms, filters, transforms, generators)
|
||||||
|
let stages: Vec<crate::search::Stage> = if pipeline_args.is_empty() {
|
||||||
|
vec![crate::search::Stage::Algorithm(crate::search::AlgoStage::parse("spread").unwrap())]
|
||||||
|
} else {
|
||||||
|
pipeline_args.iter()
|
||||||
|
.map(|a| crate::search::Stage::parse(a))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if pipeline needs full Store (has filters/transforms/generators)
|
||||||
|
let needs_store = stages.iter().any(|s| !matches!(s, crate::search::Stage::Algorithm(_)));
|
||||||
|
// Check if pipeline starts with a generator (doesn't need seed terms)
|
||||||
|
let has_generator = stages.first().map(|s| matches!(s, crate::search::Stage::Generator(_))).unwrap_or(false);
|
||||||
|
|
||||||
|
if terms.is_empty() && !has_generator {
|
||||||
|
return Err("search requires terms or a generator stage (e.g. 'all')".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let query: String = terms.join(" ");
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
let names: Vec<String> = stages.iter().map(|s| format!("{}", s)).collect();
|
||||||
|
println!("[search] pipeline: {}", names.join(" → "));
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_results = if expand { 15 } else { 5 };
|
||||||
|
|
||||||
|
if needs_store {
|
||||||
|
// Full Store path — needed for filter/transform/generator stages
|
||||||
|
let store = crate::store::Store::load()?;
|
||||||
|
let graph = store.build_graph();
|
||||||
|
|
||||||
|
let seeds = if has_generator {
|
||||||
|
vec![] // generator will produce its own result set
|
||||||
|
} else {
|
||||||
|
let terms_map: BTreeMap<String, f64> = query.split_whitespace()
|
||||||
|
.map(|t| (t.to_lowercase(), 1.0))
|
||||||
|
.collect();
|
||||||
|
let (seeds, _) = crate::search::match_seeds_opts(&terms_map, &store, fuzzy, content);
|
||||||
|
seeds
|
||||||
|
};
|
||||||
|
|
||||||
|
let raw = crate::search::run_query(&stages, seeds, &graph, &store, debug, max_results);
|
||||||
|
|
||||||
|
if raw.is_empty() {
|
||||||
|
eprintln!("No results");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, (key, score)) in raw.iter().enumerate().take(max_results) {
|
||||||
|
let weight = store.nodes.get(key).map(|n| n.weight).unwrap_or(0.0);
|
||||||
|
println!("{:2}. [{:.2}/{:.2}] {}", i + 1, score, weight, key);
|
||||||
|
if full {
|
||||||
|
if let Some(node) = store.nodes.get(key) {
|
||||||
|
println!();
|
||||||
|
for line in node.content.lines() {
|
||||||
|
println!(" {}", line);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fast MmapView path — algorithm-only pipeline
|
||||||
|
let view = crate::store::AnyView::load()?;
|
||||||
|
let graph = crate::graph::build_graph_fast(&view);
|
||||||
|
|
||||||
|
let terms_map: BTreeMap<String, f64> = query.split_whitespace()
|
||||||
|
.map(|t| (t.to_lowercase(), 1.0))
|
||||||
|
.collect();
|
||||||
|
let (seeds, direct_hits) = crate::search::match_seeds_opts(&terms_map, &view, fuzzy, content);
|
||||||
|
|
||||||
|
if seeds.is_empty() {
|
||||||
|
eprintln!("No results for '{}'", query);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
println!("[search] {} seeds from query '{}'", seeds.len(), query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract AlgoStages from the unified stages
|
||||||
|
let algo_stages: Vec<&crate::search::AlgoStage> = stages.iter()
|
||||||
|
.filter_map(|s| match s {
|
||||||
|
crate::search::Stage::Algorithm(a) => Some(a),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let algo_owned: Vec<crate::search::AlgoStage> = algo_stages.into_iter().cloned().collect();
|
||||||
|
|
||||||
|
let raw = crate::search::run_pipeline(&algo_owned, seeds, &graph, &view, debug, max_results);
|
||||||
|
|
||||||
|
let results: Vec<crate::search::SearchResult> = raw.into_iter()
|
||||||
|
.map(|(key, activation)| {
|
||||||
|
let is_direct = direct_hits.contains(&key);
|
||||||
|
crate::search::SearchResult { key, activation, is_direct, snippet: None }
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if results.is_empty() {
|
||||||
|
eprintln!("No results for '{}'", query);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log retrieval
|
||||||
|
crate::store::Store::log_retrieval_static(&query,
|
||||||
|
&results.iter().map(|r| r.key.clone()).collect::<Vec<_>>());
|
||||||
|
|
||||||
|
let bump_keys: Vec<&str> = results.iter().take(max_results).map(|r| r.key.as_str()).collect();
|
||||||
|
let _ = crate::lookups::bump_many(&bump_keys);
|
||||||
|
|
||||||
|
for (i, r) in results.iter().enumerate().take(max_results) {
|
||||||
|
let marker = if r.is_direct { "→" } else { " " };
|
||||||
|
let weight = view.node_weight(&r.key);
|
||||||
|
println!("{}{:2}. [{:.2}/{:.2}] {}", marker, i + 1, r.activation, weight, r.key);
|
||||||
|
if full {
|
||||||
|
if let Some(content) = view.node_content(&r.key) {
|
||||||
|
println!();
|
||||||
|
for line in content.lines() {
|
||||||
|
println!(" {}", line);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cmd_status() -> Result<(), String> {
|
||||||
|
// If stdout is a tty and daemon is running, launch TUI
|
||||||
|
if std::io::IsTerminal::is_terminal(&std::io::stdout()) {
|
||||||
|
// Try TUI first — falls back if daemon not running
|
||||||
|
match crate::tui::run_tui() {
|
||||||
|
Ok(()) => return Ok(()),
|
||||||
|
Err(_) => {} // fall through to text output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let store = crate::store::Store::load()?;
|
||||||
|
let g = store.build_graph();
|
||||||
|
|
||||||
|
let mut type_counts = std::collections::HashMap::new();
|
||||||
|
for node in store.nodes.values() {
|
||||||
|
*type_counts.entry(format!("{:?}", node.node_type)).or_insert(0usize) += 1;
|
||||||
|
}
|
||||||
|
let mut types: Vec<_> = type_counts.iter().collect();
|
||||||
|
types.sort_by_key(|(_, c)| std::cmp::Reverse(**c));
|
||||||
|
|
||||||
|
println!("Nodes: {} Relations: {}", store.nodes.len(), store.relations.len());
|
||||||
|
print!("Types:");
|
||||||
|
for (t, c) in &types {
|
||||||
|
let label = match t.as_str() {
|
||||||
|
"Semantic" => "semantic",
|
||||||
|
"EpisodicSession" | "EpisodicDaily" | "EpisodicWeekly" | "EpisodicMonthly"
|
||||||
|
=> "episodic",
|
||||||
|
_ => t,
|
||||||
|
};
|
||||||
|
print!(" {}={}", label, c);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
println!("Graph edges: {} Communities: {}",
|
||||||
|
g.edge_count(), g.community_count());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cmd_log() -> Result<(), String> {
|
||||||
|
let store = crate::store::Store::load()?;
|
||||||
|
for event in store.retrieval_log.iter().rev().take(20) {
|
||||||
|
println!("[{}] q=\"{}\" → {} results",
|
||||||
|
event.timestamp, event.query, event.results.len());
|
||||||
|
for r in &event.results {
|
||||||
|
println!(" {}", r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cmd_params() -> Result<(), String> {
|
||||||
|
let store = crate::store::Store::load()?;
|
||||||
|
println!("decay_factor: {}", store.params.decay_factor);
|
||||||
|
println!("use_boost: {}", store.params.use_boost);
|
||||||
|
println!("prune_threshold: {}", store.params.prune_threshold);
|
||||||
|
println!("edge_decay: {}", store.params.edge_decay);
|
||||||
|
println!("max_hops: {}", store.params.max_hops);
|
||||||
|
println!("min_activation: {}", store.params.min_activation);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cmd_query(expr: &[String]) -> Result<(), String> {
|
||||||
|
if expr.is_empty() {
|
||||||
|
return Err("query requires an expression (try: poc-memory query --help)".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let query_str = expr.join(" ");
|
||||||
|
let store = crate::store::Store::load()?;
|
||||||
|
let graph = store.build_graph();
|
||||||
|
crate::query_parser::run_query(&store, &graph, &query_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -7,3 +7,5 @@ pub mod graph;
|
||||||
pub mod node;
|
pub mod node;
|
||||||
pub mod agent;
|
pub mod agent;
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
|
pub mod journal;
|
||||||
|
pub mod misc;
|
||||||
|
|
|
||||||
|
|
@ -731,13 +731,13 @@ fn main() {
|
||||||
let result = match cli.command {
|
let result = match cli.command {
|
||||||
// Core
|
// Core
|
||||||
Command::Search { query, pipeline, expand, full, debug, fuzzy, content }
|
Command::Search { query, pipeline, expand, full, debug, fuzzy, content }
|
||||||
=> cmd_search(&query, &pipeline, expand, full, debug, fuzzy, content),
|
=> cli::misc::cmd_search(&query, &pipeline, expand, full, debug, fuzzy, content),
|
||||||
Command::Render { key } => cli::node::cmd_render(&key),
|
Command::Render { key } => cli::node::cmd_render(&key),
|
||||||
Command::Write { key } => cli::node::cmd_write(&key),
|
Command::Write { key } => cli::node::cmd_write(&key),
|
||||||
Command::History { full, key } => cli::node::cmd_history(&key, full),
|
Command::History { full, key } => cli::node::cmd_history(&key, full),
|
||||||
Command::Tail { n, full } => cmd_tail(n, full),
|
Command::Tail { n, full } => cli::journal::cmd_tail(n, full),
|
||||||
Command::Status => cmd_status(),
|
Command::Status => cli::misc::cmd_status(),
|
||||||
Command::Query { expr } => cmd_query(&expr),
|
Command::Query { expr } => cli::misc::cmd_query(&expr),
|
||||||
Command::Used { key } => cli::node::cmd_used(&key),
|
Command::Used { key } => cli::node::cmd_used(&key),
|
||||||
Command::Wrong { key, context } => cli::node::cmd_wrong(&key, &context),
|
Command::Wrong { key, context } => cli::node::cmd_wrong(&key, &context),
|
||||||
Command::NotRelevant { key } => cli::node::cmd_not_relevant(&key),
|
Command::NotRelevant { key } => cli::node::cmd_not_relevant(&key),
|
||||||
|
|
@ -825,8 +825,8 @@ fn main() {
|
||||||
AdminCmd::Import { files } => cli::admin::cmd_import(&files),
|
AdminCmd::Import { files } => cli::admin::cmd_import(&files),
|
||||||
AdminCmd::Export { files, all } => cli::admin::cmd_export(&files, all),
|
AdminCmd::Export { files, all } => cli::admin::cmd_export(&files, all),
|
||||||
AdminCmd::LoadContext { stats } => cmd_load_context(stats),
|
AdminCmd::LoadContext { stats } => cmd_load_context(stats),
|
||||||
AdminCmd::Log => cmd_log(),
|
AdminCmd::Log => cli::misc::cmd_log(),
|
||||||
AdminCmd::Params => cmd_params(),
|
AdminCmd::Params => cli::misc::cmd_params(),
|
||||||
AdminCmd::LookupBump { keys } => cli::node::cmd_lookup_bump(&keys),
|
AdminCmd::LookupBump { keys } => cli::node::cmd_lookup_bump(&keys),
|
||||||
AdminCmd::Lookups { date } => cli::node::cmd_lookups(date.as_deref()),
|
AdminCmd::Lookups { date } => cli::node::cmd_lookups(date.as_deref()),
|
||||||
},
|
},
|
||||||
|
|
@ -840,140 +840,6 @@ fn main() {
|
||||||
|
|
||||||
// ── Command implementations ─────────────────────────────────────────
|
// ── Command implementations ─────────────────────────────────────────
|
||||||
|
|
||||||
fn cmd_search(terms: &[String], pipeline_args: &[String], expand: bool, full: bool, debug: bool, fuzzy: bool, content: bool) -> Result<(), String> {
|
|
||||||
use store::StoreView;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
// Parse pipeline stages (unified: algorithms, filters, transforms, generators)
|
|
||||||
let stages: Vec<search::Stage> = if pipeline_args.is_empty() {
|
|
||||||
vec![search::Stage::Algorithm(search::AlgoStage::parse("spread").unwrap())]
|
|
||||||
} else {
|
|
||||||
pipeline_args.iter()
|
|
||||||
.map(|a| search::Stage::parse(a))
|
|
||||||
.collect::<Result<Vec<_>, _>>()?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if pipeline needs full Store (has filters/transforms/generators)
|
|
||||||
let needs_store = stages.iter().any(|s| !matches!(s, search::Stage::Algorithm(_)));
|
|
||||||
// Check if pipeline starts with a generator (doesn't need seed terms)
|
|
||||||
let has_generator = stages.first().map(|s| matches!(s, search::Stage::Generator(_))).unwrap_or(false);
|
|
||||||
|
|
||||||
if terms.is_empty() && !has_generator {
|
|
||||||
return Err("search requires terms or a generator stage (e.g. 'all')".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let query: String = terms.join(" ");
|
|
||||||
|
|
||||||
if debug {
|
|
||||||
let names: Vec<String> = stages.iter().map(|s| format!("{}", s)).collect();
|
|
||||||
println!("[search] pipeline: {}", names.join(" → "));
|
|
||||||
}
|
|
||||||
|
|
||||||
let max_results = if expand { 15 } else { 5 };
|
|
||||||
|
|
||||||
if needs_store {
|
|
||||||
// Full Store path — needed for filter/transform/generator stages
|
|
||||||
let store = store::Store::load()?;
|
|
||||||
let graph = store.build_graph();
|
|
||||||
|
|
||||||
let seeds = if has_generator {
|
|
||||||
vec![] // generator will produce its own result set
|
|
||||||
} else {
|
|
||||||
let terms_map: BTreeMap<String, f64> = query.split_whitespace()
|
|
||||||
.map(|t| (t.to_lowercase(), 1.0))
|
|
||||||
.collect();
|
|
||||||
let (seeds, _) = search::match_seeds_opts(&terms_map, &store, fuzzy, content);
|
|
||||||
seeds
|
|
||||||
};
|
|
||||||
|
|
||||||
let raw = search::run_query(&stages, seeds, &graph, &store, debug, max_results);
|
|
||||||
|
|
||||||
if raw.is_empty() {
|
|
||||||
eprintln!("No results");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i, (key, score)) in raw.iter().enumerate().take(max_results) {
|
|
||||||
let weight = store.nodes.get(key).map(|n| n.weight).unwrap_or(0.0);
|
|
||||||
println!("{:2}. [{:.2}/{:.2}] {}", i + 1, score, weight, key);
|
|
||||||
if full {
|
|
||||||
if let Some(node) = store.nodes.get(key) {
|
|
||||||
println!();
|
|
||||||
for line in node.content.lines() {
|
|
||||||
println!(" {}", line);
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fast MmapView path — algorithm-only pipeline
|
|
||||||
let view = store::AnyView::load()?;
|
|
||||||
let graph = graph::build_graph_fast(&view);
|
|
||||||
|
|
||||||
let terms_map: BTreeMap<String, f64> = query.split_whitespace()
|
|
||||||
.map(|t| (t.to_lowercase(), 1.0))
|
|
||||||
.collect();
|
|
||||||
let (seeds, direct_hits) = search::match_seeds_opts(&terms_map, &view, fuzzy, content);
|
|
||||||
|
|
||||||
if seeds.is_empty() {
|
|
||||||
eprintln!("No results for '{}'", query);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if debug {
|
|
||||||
println!("[search] {} seeds from query '{}'", seeds.len(), query);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract AlgoStages from the unified stages
|
|
||||||
let algo_stages: Vec<&search::AlgoStage> = stages.iter()
|
|
||||||
.filter_map(|s| match s {
|
|
||||||
search::Stage::Algorithm(a) => Some(a),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let algo_owned: Vec<search::AlgoStage> = algo_stages.into_iter().cloned().collect();
|
|
||||||
|
|
||||||
let raw = search::run_pipeline(&algo_owned, seeds, &graph, &view, debug, max_results);
|
|
||||||
|
|
||||||
let results: Vec<search::SearchResult> = raw.into_iter()
|
|
||||||
.map(|(key, activation)| {
|
|
||||||
let is_direct = direct_hits.contains(&key);
|
|
||||||
search::SearchResult { key, activation, is_direct, snippet: None }
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if results.is_empty() {
|
|
||||||
eprintln!("No results for '{}'", query);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log retrieval
|
|
||||||
store::Store::log_retrieval_static(&query,
|
|
||||||
&results.iter().map(|r| r.key.clone()).collect::<Vec<_>>());
|
|
||||||
|
|
||||||
let bump_keys: Vec<&str> = results.iter().take(max_results).map(|r| r.key.as_str()).collect();
|
|
||||||
let _ = lookups::bump_many(&bump_keys);
|
|
||||||
|
|
||||||
for (i, r) in results.iter().enumerate().take(max_results) {
|
|
||||||
let marker = if r.is_direct { "→" } else { " " };
|
|
||||||
let weight = view.node_weight(&r.key);
|
|
||||||
println!("{}{:2}. [{:.2}/{:.2}] {}", marker, i + 1, r.activation, weight, r.key);
|
|
||||||
if full {
|
|
||||||
if let Some(content) = view.node_content(&r.key) {
|
|
||||||
println!();
|
|
||||||
for line in content.lines() {
|
|
||||||
println!(" {}", line);
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn install_default_file(data_dir: &std::path::Path, name: &str, content: &str) -> Result<(), String> {
|
fn install_default_file(data_dir: &std::path::Path, name: &str, content: &str) -> Result<(), String> {
|
||||||
let path = data_dir.join(name);
|
let path = data_dir.join(name);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
|
|
@ -984,66 +850,6 @@ fn install_default_file(data_dir: &std::path::Path, name: &str, content: &str) -
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_status() -> Result<(), String> {
|
|
||||||
// If stdout is a tty and daemon is running, launch TUI
|
|
||||||
if std::io::IsTerminal::is_terminal(&std::io::stdout()) {
|
|
||||||
// Try TUI first — falls back if daemon not running
|
|
||||||
match tui::run_tui() {
|
|
||||||
Ok(()) => return Ok(()),
|
|
||||||
Err(_) => {} // fall through to text output
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let store = store::Store::load()?;
|
|
||||||
let g = store.build_graph();
|
|
||||||
|
|
||||||
let mut type_counts = std::collections::HashMap::new();
|
|
||||||
for node in store.nodes.values() {
|
|
||||||
*type_counts.entry(format!("{:?}", node.node_type)).or_insert(0usize) += 1;
|
|
||||||
}
|
|
||||||
let mut types: Vec<_> = type_counts.iter().collect();
|
|
||||||
types.sort_by_key(|(_, c)| std::cmp::Reverse(**c));
|
|
||||||
|
|
||||||
println!("Nodes: {} Relations: {}", store.nodes.len(), store.relations.len());
|
|
||||||
print!("Types:");
|
|
||||||
for (t, c) in &types {
|
|
||||||
let label = match t.as_str() {
|
|
||||||
"Semantic" => "semantic",
|
|
||||||
"EpisodicSession" | "EpisodicDaily" | "EpisodicWeekly" | "EpisodicMonthly"
|
|
||||||
=> "episodic",
|
|
||||||
_ => t,
|
|
||||||
};
|
|
||||||
print!(" {}={}", label, c);
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
println!("Graph edges: {} Communities: {}",
|
|
||||||
g.edge_count(), g.community_count());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cmd_log() -> Result<(), String> {
|
|
||||||
let store = store::Store::load()?;
|
|
||||||
for event in store.retrieval_log.iter().rev().take(20) {
|
|
||||||
println!("[{}] q=\"{}\" → {} results",
|
|
||||||
event.timestamp, event.query, event.results.len());
|
|
||||||
for r in &event.results {
|
|
||||||
println!(" {}", r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cmd_params() -> Result<(), String> {
|
|
||||||
let store = store::Store::load()?;
|
|
||||||
println!("decay_factor: {}", store.params.decay_factor);
|
|
||||||
println!("use_boost: {}", store.params.use_boost);
|
|
||||||
println!("prune_threshold: {}", store.params.prune_threshold);
|
|
||||||
println!("edge_decay: {}", store.params.edge_decay);
|
|
||||||
println!("max_hops: {}", store.params.max_hops);
|
|
||||||
println!("min_activation: {}", store.params.min_activation);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply links from a single agent result JSON file.
|
/// Apply links from a single agent result JSON file.
|
||||||
/// Returns (links_applied, errors).
|
/// Returns (links_applied, errors).
|
||||||
fn apply_agent_file(
|
fn apply_agent_file(
|
||||||
|
|
@ -1176,200 +982,6 @@ fn get_group_content(group: &config::ContextGroup, store: &store::Store, cfg: &c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_load_context(stats: bool) -> Result<(), String> {
|
|
||||||
let cfg = config::get();
|
|
||||||
let store = store::Store::load()?;
|
|
||||||
|
|
||||||
if stats {
|
|
||||||
let mut total_words = 0;
|
|
||||||
let mut total_entries = 0;
|
|
||||||
println!("{:<25} {:>6} {:>8}", "GROUP", "ITEMS", "WORDS");
|
|
||||||
println!("{}", "-".repeat(42));
|
|
||||||
|
|
||||||
for group in &cfg.context_groups {
|
|
||||||
let entries = get_group_content(group, &store, cfg);
|
|
||||||
let words: usize = entries.iter()
|
|
||||||
.map(|(_, c)| c.split_whitespace().count())
|
|
||||||
.sum();
|
|
||||||
let count = entries.len();
|
|
||||||
println!("{:<25} {:>6} {:>8}", group.label, count, words);
|
|
||||||
total_words += words;
|
|
||||||
total_entries += count;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{}", "-".repeat(42));
|
|
||||||
println!("{:<25} {:>6} {:>8}", "TOTAL", total_entries, total_words);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("=== MEMORY SYSTEM ({}) ===", cfg.assistant_name);
|
|
||||||
println!();
|
|
||||||
|
|
||||||
for group in &cfg.context_groups {
|
|
||||||
let entries = get_group_content(group, &store, cfg);
|
|
||||||
if !entries.is_empty() && group.source == config::ContextSource::Journal {
|
|
||||||
println!("--- recent journal entries ({}/{}) ---",
|
|
||||||
entries.len(), cfg.journal_max);
|
|
||||||
}
|
|
||||||
for (key, content) in entries {
|
|
||||||
if group.source == config::ContextSource::Journal {
|
|
||||||
println!("## {}", key);
|
|
||||||
} else {
|
|
||||||
println!("--- {} ({}) ---", key, group.label);
|
|
||||||
}
|
|
||||||
println!("{}\n", content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("=== END MEMORY LOAD ===");
|
|
||||||
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_tail(n: usize, full: bool) -> Result<(), String> {
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Read all entries, keep last N
|
|
||||||
let mut entries: Vec<store::Node> = Vec::new();
|
|
||||||
while let Ok(msg) = capnp::serialize::read_message(&mut reader, capnp::message::ReaderOptions::new()) {
|
|
||||||
let log = msg.get_root::<poc_memory::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)?;
|
|
||||||
entries.push(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let start = entries.len().saturating_sub(n);
|
|
||||||
for node in &entries[start..] {
|
|
||||||
let ts = if node.timestamp > 0 && node.timestamp < 4_000_000_000 {
|
|
||||||
store::format_datetime(node.timestamp)
|
|
||||||
} else {
|
|
||||||
format!("(raw:{})", node.timestamp)
|
|
||||||
};
|
|
||||||
let del = if node.deleted { " [DELETED]" } else { "" };
|
|
||||||
if full {
|
|
||||||
eprintln!("--- {} (v{}) {} via {} w={:.3}{} ---",
|
|
||||||
node.key, node.version, ts, node.provenance, node.weight, del);
|
|
||||||
eprintln!("{}\n", node.content);
|
|
||||||
} else {
|
|
||||||
let preview = util::first_n_chars(&node.content, 100).replace('\n', "\\n");
|
|
||||||
eprintln!(" {} v{} w={:.2}{}",
|
|
||||||
ts, node.version, node.weight, del);
|
|
||||||
eprintln!(" {} via {}", node.key, node.provenance);
|
|
||||||
if !preview.is_empty() {
|
|
||||||
eprintln!(" {}", preview);
|
|
||||||
}
|
|
||||||
eprintln!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cmd_journal_write(text: &[String]) -> Result<(), String> {
|
|
||||||
if text.is_empty() {
|
|
||||||
return Err("journal-write requires text".into());
|
|
||||||
}
|
|
||||||
let text = text.join(" ");
|
|
||||||
|
|
||||||
let timestamp = store::format_datetime(store::now_epoch());
|
|
||||||
|
|
||||||
let slug: String = text.split_whitespace()
|
|
||||||
.take(6)
|
|
||||||
.map(|w| w.to_lowercase()
|
|
||||||
.chars().filter(|c| c.is_alphanumeric() || *c == '-')
|
|
||||||
.collect::<String>())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("-");
|
|
||||||
let slug = if slug.len() > 50 { &slug[..50] } else { &slug };
|
|
||||||
|
|
||||||
let key = format!("journal#j-{}-{}", timestamp.to_lowercase().replace(':', "-"), slug);
|
|
||||||
|
|
||||||
let content = format!("## {}\n\n{}", timestamp, text);
|
|
||||||
|
|
||||||
let source_ref = find_current_transcript();
|
|
||||||
|
|
||||||
let mut store = store::Store::load()?;
|
|
||||||
|
|
||||||
let mut node = store::new_node(&key, &content);
|
|
||||||
node.node_type = store::NodeType::EpisodicSession;
|
|
||||||
node.provenance = "journal".to_string();
|
|
||||||
if let Some(src) = source_ref {
|
|
||||||
node.source_ref = src;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.upsert_node(node)?;
|
|
||||||
store.save()?;
|
|
||||||
|
|
||||||
let word_count = text.split_whitespace().count();
|
|
||||||
println!("Appended entry at {} ({} words)", timestamp, word_count);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cmd_journal_tail(n: usize, full: bool, level: u8) -> Result<(), String> {
|
|
||||||
let store = store::Store::load()?;
|
|
||||||
|
|
||||||
if level == 0 {
|
|
||||||
journal_tail_entries(&store, n, full)
|
|
||||||
} else {
|
|
||||||
let node_type = match level {
|
|
||||||
1 => store::NodeType::EpisodicDaily,
|
|
||||||
2 => store::NodeType::EpisodicWeekly,
|
|
||||||
_ => store::NodeType::EpisodicMonthly,
|
|
||||||
};
|
|
||||||
journal_tail_digests(&store, node_type, n, full)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn journal_tail_entries(store: &store::Store, n: usize, full: bool) -> Result<(), String> {
|
fn journal_tail_entries(store: &store::Store, n: usize, full: bool) -> Result<(), String> {
|
||||||
let date_re = regex::Regex::new(r"(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2})").unwrap();
|
let date_re = regex::Regex::new(r"(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2})").unwrap();
|
||||||
let key_date_re = regex::Regex::new(r"j-(\d{4}-\d{2}-\d{2}[t-]\d{2}-\d{2})").unwrap();
|
let key_date_re = regex::Regex::new(r"j-(\d{4}-\d{2}-\d{2}[t-]\d{2}-\d{2})").unwrap();
|
||||||
|
|
@ -1464,17 +1076,6 @@ fn extract_title(content: &str) -> String {
|
||||||
String::from("(untitled)")
|
String::from("(untitled)")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_query(expr: &[String]) -> Result<(), String> {
|
|
||||||
if expr.is_empty() {
|
|
||||||
return Err("query requires an expression (try: poc-memory query --help)".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let query_str = expr.join(" ");
|
|
||||||
let store = store::Store::load()?;
|
|
||||||
let graph = store.build_graph();
|
|
||||||
query::run_query(&store, &graph, &query_str)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cmd_apply_agent(process_all: bool) -> Result<(), String> {
|
fn cmd_apply_agent(process_all: bool) -> Result<(), String> {
|
||||||
let results_dir = store::memory_dir().join("agent-results");
|
let results_dir = store::memory_dir().join("agent-results");
|
||||||
|
|
||||||
|
|
@ -1584,3 +1185,147 @@ fn cmd_daemon(sub: DaemonCmd) -> Result<(), String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cmd_cursor(sub: CursorCmd) -> Result<(), String> {
|
||||||
|
match sub {
|
||||||
|
CursorCmd::Show => {
|
||||||
|
let store = crate::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 = crate::store::Store::load()?;
|
||||||
|
let bare = crate::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 = crate::store::Store::load()?;
|
||||||
|
cursor::move_temporal(&store, true)
|
||||||
|
}
|
||||||
|
CursorCmd::Back => {
|
||||||
|
let store = crate::store::Store::load()?;
|
||||||
|
cursor::move_temporal(&store, false)
|
||||||
|
}
|
||||||
|
CursorCmd::Up => {
|
||||||
|
let store = crate::store::Store::load()?;
|
||||||
|
cursor::move_up(&store)
|
||||||
|
}
|
||||||
|
CursorCmd::Down => {
|
||||||
|
let store = crate::store::Store::load()?;
|
||||||
|
cursor::move_down(&store)
|
||||||
|
}
|
||||||
|
CursorCmd::Clear => cursor::clear(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_journal_tail(n: usize, full: bool, level: u8) -> Result<(), String> {
|
||||||
|
let store = store::Store::load()?;
|
||||||
|
|
||||||
|
if level == 0 {
|
||||||
|
journal_tail_entries(&store, n, full)
|
||||||
|
} else {
|
||||||
|
let node_type = match level {
|
||||||
|
1 => store::NodeType::EpisodicDaily,
|
||||||
|
2 => store::NodeType::EpisodicWeekly,
|
||||||
|
_ => store::NodeType::EpisodicMonthly,
|
||||||
|
};
|
||||||
|
journal_tail_digests(&store, node_type, n, full)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_journal_write(text: &[String]) -> Result<(), String> {
|
||||||
|
if text.is_empty() {
|
||||||
|
return Err("journal-write requires text".into());
|
||||||
|
}
|
||||||
|
let text = text.join(" ");
|
||||||
|
|
||||||
|
let timestamp = store::format_datetime(store::now_epoch());
|
||||||
|
|
||||||
|
let slug: String = text.split_whitespace()
|
||||||
|
.take(6)
|
||||||
|
.map(|w| w.to_lowercase()
|
||||||
|
.chars().filter(|c| c.is_alphanumeric() || *c == '-')
|
||||||
|
.collect::<String>())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("-");
|
||||||
|
let slug = if slug.len() > 50 { &slug[..50] } else { &slug };
|
||||||
|
|
||||||
|
let key = format!("journal#j-{}-{}", timestamp.to_lowercase().replace(':', "-"), slug);
|
||||||
|
|
||||||
|
let content = format!("## {}\n\n{}", timestamp, text);
|
||||||
|
|
||||||
|
let source_ref = find_current_transcript();
|
||||||
|
|
||||||
|
let mut store = store::Store::load()?;
|
||||||
|
|
||||||
|
let mut node = store::new_node(&key, &content);
|
||||||
|
node.node_type = store::NodeType::EpisodicSession;
|
||||||
|
node.provenance = "journal".to_string();
|
||||||
|
if let Some(src) = source_ref {
|
||||||
|
node.source_ref = src;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.upsert_node(node)?;
|
||||||
|
store.save()?;
|
||||||
|
|
||||||
|
let word_count = text.split_whitespace().count();
|
||||||
|
println!("Appended entry at {} ({} words)", timestamp, word_count);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_load_context(stats: bool) -> Result<(), String> {
|
||||||
|
let cfg = config::get();
|
||||||
|
let store = store::Store::load()?;
|
||||||
|
|
||||||
|
if stats {
|
||||||
|
let mut total_words = 0;
|
||||||
|
let mut total_entries = 0;
|
||||||
|
println!("{:<25} {:>6} {:>8}", "GROUP", "ITEMS", "WORDS");
|
||||||
|
println!("{}", "-".repeat(42));
|
||||||
|
|
||||||
|
for group in &cfg.context_groups {
|
||||||
|
let entries = get_group_content(group, &store, cfg);
|
||||||
|
let words: usize = entries.iter()
|
||||||
|
.map(|(_, c)| c.split_whitespace().count())
|
||||||
|
.sum();
|
||||||
|
let count = entries.len();
|
||||||
|
println!("{:<25} {:>6} {:>8}", group.label, count, words);
|
||||||
|
total_words += words;
|
||||||
|
total_entries += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", "-".repeat(42));
|
||||||
|
println!("{:<25} {:>6} {:>8}", "TOTAL", total_entries, total_words);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("=== MEMORY SYSTEM ({}) ===", cfg.assistant_name);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
for group in &cfg.context_groups {
|
||||||
|
let entries = get_group_content(group, &store, cfg);
|
||||||
|
if !entries.is_empty() && group.source == config::ContextSource::Journal {
|
||||||
|
println!("--- recent journal entries ({}/{}) ---",
|
||||||
|
entries.len(), cfg.journal_max);
|
||||||
|
}
|
||||||
|
for (key, content) in entries {
|
||||||
|
if group.source == config::ContextSource::Journal {
|
||||||
|
println!("## {}", key);
|
||||||
|
} else {
|
||||||
|
println!("--- {} ({}) ---", key, group.label);
|
||||||
|
}
|
||||||
|
println!("{}\n", content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("=== END MEMORY LOAD ===");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue