json input format + required room_id on all room-scoped tools

This commit is contained in:
Damocles 2026-05-01 12:46:15 +02:00
parent ef461797ad
commit cc3451eef3
5 changed files with 260 additions and 241 deletions

View file

@ -12,7 +12,8 @@ use matrix_sdk::{
},
},
};
use serde_json::json;
use crate::claude::{short_eid, wire_event_from};
use crate::types::{FetchEventResult, MemberInfo, RoomInfo, WireEvent};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{UnixListener, UnixStream};
@ -166,10 +167,10 @@ async fn list_rooms(client: &Client) -> DaemonResponse {
.display_name()
.await
.map_or_else(|_| room.room_id().to_string(), |n| n.to_string());
rooms.push(json!({
"room_id": room.room_id().as_str(),
"name": name,
}));
rooms.push(RoomInfo {
room_id: room.room_id().as_str().to_owned(),
name,
});
}
DaemonResponse::ok(rooms)
}
@ -186,13 +187,11 @@ async fn list_room_members(client: &Client, room_id: &str) -> DaemonResponse {
Ok(m) => m,
Err(e) => return DaemonResponse::err(format!("failed to list members: {e}")),
};
let list: Vec<_> = members
let list: Vec<MemberInfo> = members
.iter()
.map(|m| {
json!({
"user_id": m.user_id().as_str(),
"display_name": m.display_name().unwrap_or_default(),
})
.map(|m| MemberInfo {
user_id: m.user_id().as_str().to_owned(),
display_name: m.display_name().unwrap_or_default().to_owned(),
})
.collect();
DaemonResponse::ok(list)
@ -282,48 +281,17 @@ async fn get_room_history(
};
}
let items: Vec<_> = tl.iter().map(timeline_item_to_json).collect();
let read_markers = timeline::compute_read_markers(&room, &tl, &own_user).await;
let items: Vec<WireEvent> = tl
.iter()
.map(|i| wire_event_from(i, &read_markers))
.collect();
DaemonResponse::ok(items)
} else {
DaemonResponse::err("event cache not available".to_owned())
}
}
fn timeline_item_to_json(item: &crate::types::TimelineItem) -> serde_json::Value {
match item {
crate::types::TimelineItem::Message {
event_id,
sender,
body,
is_self,
ts,
in_reply_to,
} => json!({
"kind": "message",
"event_id": event_id.as_str(),
"sender": sender.as_str(),
"body": body,
"is_self": is_self,
"ts": ts,
"in_reply_to": in_reply_to.as_ref().map(|e| e.as_str()),
}),
crate::types::TimelineItem::Reaction {
sender,
target_event_id,
key,
is_self,
ts,
} => json!({
"kind": "reaction",
"sender": sender.as_str(),
"target_event_id": target_event_id.as_str(),
"key": key,
"is_self": is_self,
"ts": ts,
}),
}
}
/// Fetch a specific event by ID via the homeserver `/context` endpoint.
/// Returns the event plus `context_before` events before it. Includes one
/// extra event as `earlier_handle` so the shard can page further backward
@ -381,7 +349,7 @@ async fn fetch_event(
Err(e) => return DaemonResponse::err(format!("event_with_context failed: {e}")),
};
let render = |raw: &matrix_sdk::deserialized_responses::TimelineEvent| -> Option<serde_json::Value> {
let render = |raw: &matrix_sdk::deserialized_responses::TimelineEvent| -> Option<WireEvent> {
let deserialized = raw.raw().deserialize().ok()?;
let AnySyncTimelineEvent::MessageLike(msg) = deserialized else {
return None;
@ -401,47 +369,52 @@ async fn fetch_event(
}
_ => None,
};
Some(json!({
"kind": "message",
"event_id": orig.event_id.as_str(),
"sender": orig.sender.as_str(),
"body": text.body,
"is_self": orig.sender == own_user,
"ts": ts,
"in_reply_to": in_reply_to,
}))
Some(WireEvent::Message {
event_id: orig.event_id.as_str().to_owned(),
event_id_short: short_eid(orig.event_id.as_str()),
sender: orig.sender.as_str().to_owned(),
is_self: orig.sender == own_user,
ts,
ts_human: format!("{} UTC", crate::timeline::format_ts(ts)),
body: text.body.clone(),
in_reply_to,
read_by: Vec::new(),
})
}
matrix_sdk::ruma::events::AnySyncMessageLikeEvent::Reaction(
matrix_sdk::ruma::events::SyncMessageLikeEvent::Original(orig),
) => {
let ms: u64 = orig.origin_server_ts.0.into();
let ts = (ms / 1000) as i64;
Some(json!({
"kind": "reaction",
"sender": orig.sender.as_str(),
"target_event_id": orig.content.relates_to.event_id.as_str(),
"key": orig.content.relates_to.key,
"is_self": orig.sender == own_user,
"ts": ts,
}))
Some(WireEvent::Reaction {
sender: orig.sender.as_str().to_owned(),
is_self: orig.sender == own_user,
ts,
ts_human: format!("{} UTC", crate::timeline::format_ts(ts)),
target_event_id: orig.content.relates_to.event_id.as_str().to_owned(),
target_event_id_short: short_eid(orig.content.relates_to.event_id.as_str()),
key: orig.content.relates_to.key.clone(),
})
}
_ => None,
}
};
// events_before is newest-first per matrix /context spec - reverse for chronological
let mut before: Vec<_> = response.events_before.iter().filter_map(render).collect();
let mut before: Vec<WireEvent> = response.events_before.iter().filter_map(render).collect();
before.reverse();
// The "earlier handle" is the oldest event we got, if we asked for more than 0
// It's used by the shard to page further back via another fetch_event call
// The "earlier handle" is the oldest event we got, used by the shard
// to page further back via another fetch_event call.
let earlier_handle = if context_before > 0 && before.len() > context_before as usize {
before.first().and_then(|e| e.get("event_id").and_then(|v| v.as_str()).map(String::from))
before.first().map(|e| match e {
WireEvent::Message { event_id, .. } => event_id.clone(),
WireEvent::Reaction { target_event_id, .. } => target_event_id.clone(),
})
} else {
None
};
let context_events: Vec<_> = if earlier_handle.is_some() {
// First event is the handle - skip it from the main events list
let context_events: Vec<WireEvent> = if earlier_handle.is_some() {
before.into_iter().skip(1).collect()
} else {
before
@ -449,9 +422,9 @@ async fn fetch_event(
let target = response.event.as_ref().and_then(render);
DaemonResponse::ok(json!({
"event": target,
"context_before": context_events,
"earlier_handle": earlier_handle,
}))
DaemonResponse::ok(FetchEventResult {
event: target,
context_before: context_events,
earlier_handle,
})
}