1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//! `tm memory recall|remember|note` — the no-MCP palace verbs (#8352).
//!
//! Why: a thin translation layer, the same shape `commands::memory` uses for
//! `import` — clap args in, one library call out, one report rendered. The verbs
//! themselves live in [`trusty_mpm::core::memory_verbs`] so they are testable
//! without a CLI, and so nothing about reaching the daemon is duplicated here.
//! What: [`run`] builds [`MemoryVerbOptions`], runs the verb, and prints either
//! the stable JSON envelope (`--json`) or a human summary. Returns `Err` — a
//! non-zero exit — when the palace could not be resolved, the socket could not
//! be derived, or the daemon did not answer.
//! Test: `cli_parses_memory_recall`, `cli_parses_memory_remember_with_tags`,
//! `cli_parses_memory_note` in `tests.rs`; the socket behaviour is covered by
//! `tests/memory_verbs_socket.rs`.
use std::path::PathBuf;
use anyhow::Context as _;
use serde_json::Value;
use trusty_mpm::core::memory_verbs::{MemoryVerb, MemoryVerbOptions, MemoryVerbOutcome, run_verb};
/// Longest slice of a recalled drawer printed per line.
///
/// Why: a recall hit can be a whole paragraph, and the human summary is an
/// index — `--json` is where the full text lives.
const SNIPPET_CHARS: usize = 160;
/// Run one no-MCP memory verb.
///
/// Why: keeps `commands::memory`'s dispatcher one arm per action.
/// What: see the module doc.
/// Test: `tests/memory_verbs_socket.rs`.
pub(crate) async fn run(
verb: MemoryVerb,
palace: Option<String>,
memory_socket: Option<PathBuf>,
json: bool,
) -> anyhow::Result<()> {
let opts = MemoryVerbOptions {
palace,
socket: memory_socket,
cwd: None,
};
let outcome = run_verb(&verb, &opts).await?;
if json {
println!(
"{}",
serde_json::to_string_pretty(&outcome).context("serialise the memory envelope")?
);
} else {
print_summary(&outcome);
}
Ok(())
}
/// Render the human summary.
///
/// Why: an operator running this by hand wants the hits, or the one line saying
/// what the write did — not the envelope. Nothing parses this; `--json` is the
/// machine surface.
/// What: a recall prints one line per hit plus a count; a write prints the
/// daemon's own `status`, its reason when it skipped, and the drawer id when it
/// stored. A palace index (the #6318 answer to a recall with no palace) prints
/// the hint the daemon supplied.
/// Test: cosmetic; exercised through `tests/memory_verbs_socket.rs`.
fn print_summary(outcome: &MemoryVerbOutcome) {
let palace = outcome.palace.as_deref().unwrap_or("(none resolved)");
match outcome.count {
Some(count) => {
let results = outcome
.result
.get("results")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
for hit in results {
let score = hit.get("score").and_then(Value::as_f64).unwrap_or(0.0);
let layer = hit.get("layer").and_then(Value::as_str).unwrap_or("-");
let content = hit.get("content").and_then(Value::as_str).unwrap_or("");
println!("[{score:.3}] {layer:<10} {}", snippet(content));
}
if let Some(hint) = outcome.result.get("hint").and_then(Value::as_str) {
println!("{hint}");
}
println!("\n{count} hit(s) from palace {palace}");
}
None => {
let status = outcome
.result
.get("status")
.and_then(Value::as_str)
.unwrap_or("stored");
let drawer = outcome
.result
.get("drawer_id")
.and_then(Value::as_str)
.unwrap_or("-");
match outcome.result.get("reason").and_then(Value::as_str) {
Some(reason) => println!("{status} → palace {palace}: {reason}"),
None => println!("{status} {drawer} → palace {palace}"),
}
}
}
}
/// One line of `content`, capped at [`SNIPPET_CHARS`] characters.
fn snippet(content: &str) -> String {
let first = content.lines().next().unwrap_or("").trim();
if first.chars().count() <= SNIPPET_CHARS {
return first.to_string();
}
let cut: String = first.chars().take(SNIPPET_CHARS).collect();
format!("{cut}…")
}