Skip to main content

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}