greplm_core/proto.rs
1//! Wire protocol shared by the daemon and its clients.
2//!
3//! Messages are newline-delimited JSON over a Unix domain socket. Each request
4//! is one JSON object on a line; each response is one JSON object on a line.
5
6use std::path::PathBuf;
7
8use serde::{Deserialize, Serialize};
9
10use crate::search::{SearchQuery, SymbolQuery};
11
12/// Default socket path relative to a project's `.greplm` directory.
13pub const SOCKET_NAME: &str = "greplmd.sock";
14
15/// Machine-wide (per-user) socket for the global multi-root daemon. One daemon
16/// serves every project the user touches, lazily loading and evicting them, so
17/// running many agents across many repos costs a single background process.
18///
19/// Placed in the per-user runtime/temp dir so it's private to the user and
20/// cleared across reboots. Falls back through `XDG_RUNTIME_DIR` (Linux),
21/// `TMPDIR` (macOS), `~/.cache`, then the system temp dir.
22pub fn global_socket_path() -> PathBuf {
23 let base = std::env::var_os("XDG_RUNTIME_DIR")
24 .map(PathBuf::from)
25 .or_else(|| std::env::var_os("TMPDIR").map(PathBuf::from))
26 .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache")))
27 .unwrap_or_else(std::env::temp_dir);
28 base.join("greplm").join(SOCKET_NAME)
29}
30
31/// A request addressed to a specific project root, used by the global daemon
32/// (which serves many roots over one socket). The client resolves `root` to the
33/// project's `.greplm` ancestor before sending.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct RoutedRequest {
36 pub root: PathBuf,
37 pub req: Request,
38}
39
40/// A request from a client to the daemon.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(tag = "op", rename_all = "snake_case")]
43pub enum Request {
44 Ping,
45 Status,
46 Summary,
47 Reindex {
48 force: bool,
49 },
50 Search(SearchQuery),
51 Symbols(SymbolQuery),
52 Refs {
53 name: String,
54 limit: usize,
55 offset: usize,
56 },
57 /// Resolved references (definitions + call sites + imports) from the
58 /// structural reference index.
59 RefsResolved {
60 name: String,
61 limit: usize,
62 offset: usize,
63 },
64 /// Call sites that target a symbol (who calls it).
65 Callers {
66 name: String,
67 limit: usize,
68 offset: usize,
69 },
70 /// Call sites inside a symbol's body (what it calls).
71 Callees {
72 name: String,
73 limit: usize,
74 offset: usize,
75 },
76 /// Symbols transitively affected by changing a symbol (reverse call graph).
77 BlastRadius {
78 name: String,
79 depth: u32,
80 limit: usize,
81 },
82 /// Typed go-to-definition for the identifier at a source position.
83 Definition {
84 file: String,
85 line: u32,
86 col: u32,
87 },
88 /// Resolved references for the identifier at a source position.
89 ReferencesAt {
90 file: String,
91 line: u32,
92 col: u32,
93 },
94 /// Structural (AST) search by tree-sitter query or meta-variable pattern.
95 Structural {
96 pattern: String,
97 lang: String,
98 limit: usize,
99 offset: usize,
100 },
101 /// Build a token-budgeted context pack for a task.
102 ContextPack {
103 task: String,
104 budget: u64,
105 },
106 /// Git blame for a single line.
107 Blame {
108 file: String,
109 line: u32,
110 },
111 /// Commit history of a symbol's definition.
112 History {
113 name: String,
114 limit: usize,
115 },
116 /// Files (with symbols) changed since a revision.
117 ChangedSince {
118 rev: String,
119 },
120 Outline {
121 file: String,
122 },
123 Snippet {
124 file: String,
125 start: u32,
126 end: u32,
127 context: u32,
128 },
129}
130
131/// A response from the daemon to a client.
132///
133/// `result` is a pre-serialized JSON fragment ([`serde_json::value::RawValue`])
134/// rather than a `serde_json::Value` tree: the daemon serializes each result
135/// exactly once (typed struct -> JSON text) and the framing serializer embeds
136/// it verbatim, instead of building and then re-walking an intermediate
137/// `Value` tree for every response. Clients that want typed access parse the
138/// fragment directly into their target type; clients that just forward the
139/// payload (the MCP server) pass the text through untouched.
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct Response {
142 pub ok: bool,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub result: Option<Box<serde_json::value::RawValue>>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub error: Option<String>,
147}
148
149impl Response {
150 /// Build a success response by serializing `value` once.
151 pub fn json<T: serde::Serialize>(value: &T) -> Self {
152 match serde_json::value::to_raw_value(value) {
153 Ok(raw) => Response {
154 ok: true,
155 result: Some(raw),
156 error: None,
157 },
158 Err(e) => Response::err(e.to_string()),
159 }
160 }
161
162 pub fn err(message: impl Into<String>) -> Self {
163 Response {
164 ok: false,
165 result: None,
166 error: Some(message.into()),
167 }
168 }
169
170 /// The raw JSON text of the result (`"null"` when absent).
171 pub fn result_text(&self) -> &str {
172 self.result.as_deref().map(|r| r.get()).unwrap_or("null")
173 }
174}