Skip to main content

sqlite_graphrag/commands/
list.rs

1use crate::cli::MemoryType;
2use crate::errors::AppError;
3use crate::output::{self, OutputFormat};
4use crate::paths::AppPaths;
5use crate::storage::connection::open_ro;
6use crate::storage::memories;
7use serde::Serialize;
8
9#[derive(clap::Args)]
10pub struct ListArgs {
11    #[arg(long, default_value = "global")]
12    pub namespace: Option<String>,
13    /// Filter by memory.type. Note: distinct from graph entity_type
14    /// (project/tool/person/file/concept/incident/decision/memory/dashboard/issue_tracker)
15    /// used in --entities-file.
16    #[arg(long, value_enum)]
17    pub r#type: Option<MemoryType>,
18    #[arg(long, default_value = "50")]
19    pub limit: usize,
20    #[arg(long, default_value = "0")]
21    pub offset: usize,
22    #[arg(long, value_enum, default_value = "json")]
23    pub format: OutputFormat,
24    #[arg(long, help = "No-op; JSON is always emitted on stdout")]
25    pub json: bool,
26    #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
27    pub db: Option<String>,
28}
29
30#[derive(Serialize)]
31struct ListItem {
32    id: i64,
33    /// Alias semântico de `id` para contrato documentado em SKILL.md e AGENT_PROTOCOL.md.
34    memory_id: i64,
35    name: String,
36    namespace: String,
37    #[serde(rename = "type")]
38    memory_type: String,
39    description: String,
40    snippet: String,
41    updated_at: i64,
42    /// Timestamp RFC 3339 UTC paralelo a `updated_at`.
43    updated_at_iso: String,
44}
45
46#[derive(Serialize)]
47struct ListResponse {
48    items: Vec<ListItem>,
49    /// Tempo total de execução em milissegundos desde início do handler até serialização.
50    elapsed_ms: u64,
51}
52
53pub fn run(args: ListArgs) -> Result<(), AppError> {
54    let inicio = std::time::Instant::now();
55    let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
56    let paths = AppPaths::resolve(args.db.as_deref())?;
57    // v1.0.22 P1: padroniza exit code 4 com mensagem amigável quando DB não existe.
58    if !paths.db.exists() {
59        return Err(AppError::NotFound(
60            crate::i18n::erros::banco_nao_encontrado(&paths.db.display().to_string()),
61        ));
62    }
63    let conn = open_ro(&paths.db)?;
64
65    let memory_type_str = args.r#type.map(|t| t.as_str());
66    let rows = memories::list(&conn, &namespace, memory_type_str, args.limit, args.offset)?;
67
68    let items: Vec<ListItem> = rows
69        .into_iter()
70        .map(|r| {
71            let snippet: String = r.body.chars().take(200).collect();
72            let updated_at_iso = crate::tz::epoch_para_iso(r.updated_at);
73            ListItem {
74                id: r.id,
75                memory_id: r.id,
76                name: r.name,
77                namespace: r.namespace,
78                memory_type: r.memory_type,
79                description: r.description,
80                snippet,
81                updated_at: r.updated_at,
82                updated_at_iso,
83            }
84        })
85        .collect();
86
87    match args.format {
88        OutputFormat::Json => output::emit_json(&ListResponse {
89            items,
90            elapsed_ms: inicio.elapsed().as_millis() as u64,
91        })?,
92        OutputFormat::Text | OutputFormat::Markdown => {
93            for item in &items {
94                output::emit_text(&format!("{}: {}", item.name, item.snippet));
95            }
96        }
97    }
98    Ok(())
99}
100
101#[cfg(test)]
102mod testes {
103    use super::*;
104
105    #[test]
106    fn list_response_serializa_items_e_elapsed_ms() {
107        let resp = ListResponse {
108            items: vec![ListItem {
109                id: 1,
110                memory_id: 1,
111                name: "teste-memoria".to_string(),
112                namespace: "global".to_string(),
113                memory_type: "note".to_string(),
114                description: "descricao de teste".to_string(),
115                snippet: "corpo resumido".to_string(),
116                updated_at: 1_745_000_000,
117                updated_at_iso: "2025-04-19T00:00:00Z".to_string(),
118            }],
119            elapsed_ms: 7,
120        };
121        let json = serde_json::to_value(&resp).unwrap();
122        assert!(json["items"].is_array());
123        assert_eq!(json["items"].as_array().unwrap().len(), 1);
124        assert_eq!(json["items"][0]["name"], "teste-memoria");
125        assert_eq!(json["items"][0]["memory_id"], 1);
126        assert_eq!(json["elapsed_ms"], 7);
127    }
128
129    #[test]
130    fn list_response_items_vazio_serializa_array_vazio() {
131        let resp = ListResponse {
132            items: vec![],
133            elapsed_ms: 0,
134        };
135        let json = serde_json::to_value(&resp).unwrap();
136        assert!(json["items"].is_array());
137        assert_eq!(json["items"].as_array().unwrap().len(), 0);
138        assert_eq!(json["elapsed_ms"], 0);
139    }
140
141    #[test]
142    fn list_item_memory_id_igual_a_id() {
143        let item = ListItem {
144            id: 42,
145            memory_id: 42,
146            name: "memoria-alias".to_string(),
147            namespace: "projeto".to_string(),
148            memory_type: "fact".to_string(),
149            description: "desc".to_string(),
150            snippet: "snip".to_string(),
151            updated_at: 0,
152            updated_at_iso: "1970-01-01T00:00:00Z".to_string(),
153        };
154        let json = serde_json::to_value(&item).unwrap();
155        assert_eq!(
156            json["id"], json["memory_id"],
157            "id e memory_id devem ser iguais"
158        );
159    }
160
161    #[test]
162    fn snippet_truncado_em_200_chars() {
163        let body_longo: String = "a".repeat(300);
164        let snippet: String = body_longo.chars().take(200).collect();
165        assert_eq!(snippet.len(), 200, "snippet deve ter exatamente 200 chars");
166    }
167
168    #[test]
169    fn updated_at_iso_epoch_zero_gera_utc_valido() {
170        let iso = crate::tz::epoch_para_iso(0);
171        assert!(
172            iso.starts_with("1970-01-01T00:00:00"),
173            "epoch 0 deve mapear para 1970-01-01, obtido: {iso}"
174        );
175        assert!(
176            iso.contains('+') || iso.contains('-'),
177            "deve conter sinal de offset, obtido: {iso}"
178        );
179    }
180}