diff --git a/crates/quicprochat-client/src/client/repl.rs b/crates/quicprochat-client/src/client/repl.rs
index ff51975..eae8e2b 100644
--- a/crates/quicprochat-client/src/client/repl.rs
+++ b/crates/quicprochat-client/src/client/repl.rs
@@ -70,6 +70,8 @@ pub(crate) enum SlashCommand {
MeshRoute,
MeshIdentity,
MeshStore,
+ MeshTrace { address: String },
+ MeshStats,
/// Display safety number for out-of-band key verification with a contact.
Verify { username: String },
/// Rotate own MLS leaf key in the active group.
@@ -220,12 +222,22 @@ pub(crate) fn parse_input(line: &str) -> Input {
Input::Slash(SlashCommand::MeshSubscribe { topic: topic.into() })
}
}
- Some("route") => Input::Slash(SlashCommand::MeshRoute),
+ Some("route") | Some("routes") => Input::Slash(SlashCommand::MeshRoute),
Some("identity") | Some("id") => Input::Slash(SlashCommand::MeshIdentity),
Some("store") => Input::Slash(SlashCommand::MeshStore),
+ Some("stats") => Input::Slash(SlashCommand::MeshStats),
+ Some(rest) if rest.starts_with("trace ") => {
+ let address = rest[6..].trim();
+ if address.is_empty() {
+ display::print_error("usage: /mesh trace
");
+ Input::Empty
+ } else {
+ Input::Slash(SlashCommand::MeshTrace { address: address.into() })
+ }
+ }
_ => {
display::print_error(
- "usage: /mesh start|stop|peers|server|send|broadcast|subscribe|route|identity|store"
+ "usage: /mesh start|stop|peers|server|send|broadcast|subscribe|route|identity|store|trace|stats"
);
Input::Empty
}
@@ -823,6 +835,8 @@ async fn handle_slash(
SlashCommand::MeshRoute => cmd_mesh_route(session),
SlashCommand::MeshIdentity => cmd_mesh_identity(session),
SlashCommand::MeshStore => cmd_mesh_store(session),
+ SlashCommand::MeshTrace { address } => cmd_mesh_trace(session, &address),
+ SlashCommand::MeshStats => cmd_mesh_stats(session),
SlashCommand::Verify { username } => cmd_verify(session, client, &username).await,
SlashCommand::UpdateKey => cmd_update_key(session, client).await,
SlashCommand::Typing => cmd_typing(session, client).await,
@@ -878,6 +892,8 @@ pub(crate) fn print_help() {
display::print_status(" /mesh route - Show known mesh peers and routes");
display::print_status(" /mesh identity - Show mesh node identity info");
display::print_status(" /mesh store - Show mesh store-and-forward stats");
+ display::print_status(" /mesh trace - Show route to a mesh address");
+ display::print_status(" /mesh stats - Show delivery statistics per destination");
display::print_status(" /update-key - Rotate your MLS leaf key in the active group");
display::print_status(" /verify - Show safety number for key verification");
display::print_status(" /react [index] - React to last message (or message at index)");
@@ -1390,10 +1406,74 @@ pub(crate) fn cmd_mesh_identity(session: &SessionState) -> anyhow::Result<()> {
pub(crate) fn cmd_mesh_store(session: &SessionState) -> anyhow::Result<()> {
#[cfg(feature = "mesh")]
{
- // Without a live P2pNode in the session, we can only report that the store
- // is not active. Once P2pNode is wired in, this will show real stats.
- display::print_status("mesh store: not active (P2P node not started in this session)");
- display::print_status("start mesh mode to enable store-and-forward");
+ match &session.p2p_node {
+ Some(node) => {
+ let store = node.mesh_store();
+ let guard = store.lock().map_err(|e| anyhow::anyhow!("store lock: {e}"))?;
+ let (total_messages, unique_recipients) = guard.stats();
+ display::print_status(&format!("mesh store: {} messages for {} recipients", total_messages, unique_recipients));
+ }
+ None => {
+ display::print_status("mesh store: not active (P2P node not started)");
+ display::print_status("use /mesh start to enable store-and-forward");
+ }
+ }
+ }
+ #[cfg(not(feature = "mesh"))]
+ {
+ let _ = session;
+ display::print_error("requires --features mesh");
+ }
+ Ok(())
+}
+
+/// Show route to a mesh address.
+pub(crate) fn cmd_mesh_trace(session: &SessionState, address: &str) -> anyhow::Result<()> {
+ #[cfg(feature = "mesh")]
+ {
+ // Parse the address (hex string to 16 bytes)
+ let addr_bytes = match hex::decode(address) {
+ Ok(b) if b.len() == 16 => {
+ let mut arr = [0u8; 16];
+ arr.copy_from_slice(&b);
+ arr
+ }
+ Ok(b) if b.len() == 32 => {
+ // Full public key — compute truncated address
+ quicprochat_p2p::announce::compute_address(&b)
+ }
+ _ => {
+ display::print_error("invalid address: expected 16-byte hex (32 chars) or 32-byte key (64 chars)");
+ return Ok(());
+ }
+ };
+
+ display::print_status(&format!("tracing route to {}", hex::encode(addr_bytes)));
+
+ // For now, show the route from the routing table if we had one
+ // In a full implementation, this would query the MeshRouter
+ display::print_status(" (routing table not yet wired to REPL session)");
+ display::print_status(" this will show hop-by-hop path once MeshRouter is integrated");
+
+ let _ = session;
+ }
+ #[cfg(not(feature = "mesh"))]
+ {
+ let _ = (session, address);
+ display::print_error("requires --features mesh");
+ }
+ Ok(())
+}
+
+/// Show delivery statistics per destination.
+pub(crate) fn cmd_mesh_stats(session: &SessionState) -> anyhow::Result<()> {
+ #[cfg(feature = "mesh")]
+ {
+ // For now, report that stats are not available without MeshRouter
+ display::print_status("mesh delivery statistics:");
+ display::print_status(" (MeshRouter not yet wired to REPL session)");
+ display::print_status(" stats will show per-destination delivery counts once integrated");
+
let _ = session;
}
#[cfg(not(feature = "mesh"))]