Skip to main content

comindly_api/
lib.rs

1//! The requests and answers of `/api/v1`, `/health` and `/ready`: what the server writes and what
2//! the CLI and the web app read. Grouped by area and re-exported here.
3
4use serde::{Deserialize, Serialize};
5
6pub mod agents;
7pub mod answers;
8#[cfg(feature = "client")]
9pub mod client;
10pub mod conventions;
11pub mod credentials;
12pub mod doctor;
13pub mod events;
14pub mod graph;
15pub mod hosting;
16pub mod jobs;
17pub mod ledger;
18pub mod manifests;
19pub mod model;
20pub mod preflight;
21pub mod prompts;
22pub mod search;
23pub mod server;
24pub mod team;
25pub mod view;
26
27mod account;
28mod browse;
29mod records;
30mod repos;
31mod settings;
32
33pub use account::*;
34pub use browse::*;
35pub use records::*;
36pub use repos::*;
37pub use settings::*;
38
39/// What most changes answer: done, and a line to show.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct Done {
42    pub ok: bool,
43    pub message: String,
44}
45
46impl Done {
47    pub fn new(message: impl Into<String>) -> Done {
48        Done {
49            ok: true,
50            message: message.into(),
51        }
52    }
53}
54
55/// Every failed request: `{"error": {"code", "message"}}`.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ErrorBody {
58    pub error: ErrorDetail,
59}
60
61/// Why a request failed; `host` and `username` when a clone needs an access token.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ErrorDetail {
64    pub code: String,
65    pub message: String,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub host: Option<String>,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub username: Option<String>,
70}
71
72/// `GET /health`: the process answers.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct Health {
75    pub status: String,
76    pub version: String,
77}
78
79/// `GET /ready`: an index is loaded and, on a server, the stores answer.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct Ready {
82    pub ready: bool,
83    pub index: bool,
84    pub stores: bool,
85}
86
87/// `{ id }`: revoke a token, erase a record.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct IdRequest {
90    pub id: String,
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    /// Reads `json` as `T` and writes it back unchanged, keys in the same order.
98    fn same<T: Serialize + serde::de::DeserializeOwned>(json: &str) {
99        let v: T = serde_json::from_str(json).unwrap();
100        assert_eq!(serde_json::to_string(&v).unwrap(), json);
101    }
102
103    #[test]
104    fn answers_keep_their_wire_shape() {
105        same::<Me>(r#"{"mode":"local"}"#);
106        same::<Me>(
107            r#"{"mode":"server","signed_in":false,"email":null,"role":null,"sign_in":"email","sign_in_methods":["oidc","email","code"],"oidc_label":"Microsoft","public_url":"http://localhost:1","gateway_url":"https://mcp.example.com/mcp"}"#,
108        );
109        let older: Me = serde_json::from_str(
110            r#"{"mode":"server","signed_in":false,"email":null,"role":null,"sign_in":"code","public_url":"http://localhost:1"}"#,
111        )
112        .unwrap();
113        assert!(matches!(older, Me::Server { sign_in_methods, .. } if sign_in_methods.is_empty()));
114        same::<DeviceToken>(r#"{"status":"pending"}"#);
115        same::<DeviceToken>(
116            r#"{"status":"approved","token":"t","email":"a@example.com","expires":null}"#,
117        );
118        same::<Secrets>(
119            r#"{"secrets":[{"source":"deployment","name":"llm_api_key","label":"LLM API key"},{"name":"git:github.com","source":"stored","hint":"…abcd","updated":"2026-01-01T00:00:00+00:00","updated_by":"a@example.com","label":"GitHub token for github.com"}],"can_store":true}"#,
120        );
121        same::<IndexState>(r#"{"running":false,"events":[],"queued":[]}"#);
122        same::<IndexState>(
123            r#"{"id":"j","running":false,"cancelled":false,"ok":true,"status":"succeeded","events":[],"started":"s","finished":"f","queued":[{"id":"k","kind":"index","position":1,"started_by":null,"created":"c"}]}"#,
124        );
125        same::<Jobs>(
126            r#"{"jobs":[{"id":"k","kind":"index","started_by":null,"status":"queued","created":"c","started":null,"finished":null,"error":null,"position":1}]}"#,
127        );
128        same::<Invited>(r#"{"ok":true,"message":"Invitation emailed to a@example.com"}"#);
129        same::<ErrorBody>(r#"{"error":{"code":"not_found","message":"no such job"}}"#);
130        same::<Neighbourhood>(r#"{"nodes":[],"edges":[]}"#);
131    }
132}