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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//! `tm memory <action>` dispatcher.
//!
//! Why (issue #4837): a thin translation layer — clap args in, library call
//! out, report rendered. All import logic lives in
//! [`trusty_mpm::core::memory_import`] so it is testable without a CLI.
//! What: [`memory`] routes [`MemoryAction`] — `import` here,
//! `import-auto-memory` (#7685) in [`super::memory_auto_import`], and the no-MCP
//! `recall`/`remember`/`note` verbs (#8352) in [`super::memory_verbs`] — and renders
//! the result either as the machine-readable JSON report (`--json`) or a
//! per-file human summary.
//! Exits non-zero when any file failed, so a script can gate on it.
//! Test: `cli_parses_memory_import*` in `tests.rs`; the import behaviour is
//! covered by `core::memory_import::tests`.
use anyhow::Context as _;
use trusty_mpm::core::memory_import::{ImportOptions, ImportReport, ImportStatus};
// #8352: the no-MCP palace verbs share this dispatcher.
use trusty_mpm::core::memory_verbs::MemoryVerb;
use crate::cli::MemoryAction;
/// Run a `tm memory` action.
///
/// Why: keeps `main.rs`'s dispatch table one line per command group.
/// What: builds [`ImportOptions`] from the parsed flags, runs the import, and
/// prints the report. Returns `Err` when at least one file failed so the
/// process exits non-zero.
/// Test: `cli_parses_memory_import`, `cli_parses_memory_import_dry_run_json`.
pub(crate) async fn memory(action: MemoryAction) -> anyhow::Result<()> {
match action {
MemoryAction::Import {
dir,
palace,
dry_run,
refresh,
json,
allow_secret_like,
memory_socket,
} => {
let opts = ImportOptions {
dir,
palace,
dry_run,
refresh,
allow_secret_like,
memory_socket,
};
let report = trusty_mpm::core::memory_import::run_import(&opts).await?;
if json {
println!(
"{}",
serde_json::to_string_pretty(&report).context("serialise import report")?
);
} else {
print_summary(&report);
}
if report.failed > 0 {
anyhow::bail!("{} file(s) failed to import", report.failed);
}
Ok(())
}
// #7685: its own file — see `memory_auto_import`.
MemoryAction::ImportAutoMemory {
project,
palace,
json,
memory_socket,
} => {
super::memory_auto_import::import_auto_memory(project, palace, json, memory_socket)
.await
}
// #8352: the no-MCP verbs — their own file, see `memory_verbs`.
MemoryAction::Recall {
query,
palace,
top_k,
room,
wing,
min_score,
json,
memory_socket,
} => {
let verb = MemoryVerb::Recall {
query,
top_k,
room,
wing,
min_score,
};
super::memory_verbs::run(verb, palace, memory_socket, json).await
}
MemoryAction::Remember {
text,
palace,
room,
tags,
json,
memory_socket,
} => {
let verb = MemoryVerb::Remember { text, room, tags };
super::memory_verbs::run(verb, palace, memory_socket, json).await
}
MemoryAction::Note {
content,
palace,
room,
tags,
json,
memory_socket,
} => {
let verb = MemoryVerb::Note {
content,
room,
tags,
};
super::memory_verbs::run(verb, palace, memory_socket, json).await
}
}
}
/// Render the human-readable summary.
///
/// Why: the JSON report is for scripts; an operator running this by hand wants
/// one line per file plus a total, not 148 JSON objects.
/// What: prints `<status> <file> [drawer_id] [detail]` per file, then a count
/// line. Nothing here is parsed by anything — `--json` is the machine surface.
/// Test: exercised indirectly by the CLI parse tests; output is cosmetic.
fn print_summary(report: &ImportReport) {
for file in &report.files {
let status = match file.status {
ImportStatus::Created => "created",
ImportStatus::Skipped => "skipped",
ImportStatus::Refreshed => "refreshed",
ImportStatus::WouldCreate => "would-create",
ImportStatus::WouldSkip => "would-skip",
ImportStatus::WouldRefresh => "would-refresh",
ImportStatus::Failed => "FAILED",
};
let drawer = file.drawer_id.as_deref().unwrap_or("-");
let detail = file.detail.as_deref().unwrap_or("");
println!("{status:<12} {:<48} {drawer} {detail}", file.file);
}
let mode = if report.dry_run { " (dry run)" } else { "" };
println!(
"\n{} file(s) in {}{mode}: {} created, {} refreshed, {} skipped, {} failed → palace {}",
report.total,
report.dir,
report.created,
report.refreshed,
report.skipped,
report.failed,
report.palace,
);
}