Cersei

Code & AST Intelligence

LSP client and tree-sitter parsing for semantic code understanding.

Code & AST Intelligence

Cersei provides two layers of code intelligence available as both SDK library APIs and agent tools:

  • cersei-lsp — Language Server Protocol client for semantic operations (hover, definitions, references, symbols, diagnostics)
  • Tree-sitter modules in cersei-tools — AST-based import extraction, symbol discovery, dependency ranking, and bash command safety analysis

Both are used by Abstract CLI to give the agent deep understanding of codebases without reading every file.


cersei-lsp Crate

A standalone LSP client crate with on-demand server management. Spawns language servers lazily on first file access, communicates via JSON-RPC 2.0 over stdio.

Installation

[dependencies]
cersei-lsp = "0.1.6"

Architecture

LspManager (multi-server registry)
├── LspClient (rust-analyzer) ── JSON-RPC ── rust-analyzer process
├── LspClient (pyright) ── JSON-RPC ── pyright-langserver process
└── LspClient (gopls) ── JSON-RPC ── gopls process
  • LspManager routes files to the correct server by extension
  • LspClient manages a single server process with async I/O
  • Servers start on first file access and persist for the session
  • 13 built-in server configs, extensible with custom configs

Quick Start

use cersei_lsp::{LspManager, LspServerConfig};
use std::path::Path;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut mgr = LspManager::new("/path/to/project");
    mgr.register_builtins(); // Register 13 built-in servers

    // Hover: get type info at a position
    let hover = mgr.hover(Path::new("src/main.rs"), 10, 5).await?;
    println!("Hover: {:?}", hover);

    // Go-to-definition
    let defs = mgr.definition(Path::new("src/lib.rs"), 25, 12).await?;
    for loc in &defs {
        println!("Defined at: {loc}");
    }

    // Find all references
    let refs = mgr.references(Path::new("src/lib.rs"), 25, 12).await?;
    println!("Found {} references", refs.len());

    // Document symbols (file outline)
    let symbols = mgr.document_symbols(Path::new("src/lib.rs")).await?;
    for sym in &symbols {
        print!("{}", sym.format(0)); // Indented tree output
    }

    // Diagnostics (compiler errors/warnings)
    let diags = mgr.diagnostics(Path::new("src/main.rs")).await?;
    println!("{}", LspManager::format_diagnostics(&diags));

    mgr.shutdown_all().await;
    Ok(())
}

API Reference

LspServerConfig

Prop

Type

LspManager

MethodSignatureDescription
newfn new(working_dir: impl Into<PathBuf>) -> SelfCreate manager for a project
register_builtinsfn register_builtins(&mut self)Register 13 built-in servers
register_serverfn register_server(&mut self, config: LspServerConfig)Add a custom server
has_server_forfn has_server_for(&self, path: &Path) -> boolCheck if a server handles this file type
hoverasync fn hover(&mut self, path, line, col) -> LspResult<Option<String>>Get hover info (0-based)
definitionasync fn definition(&mut self, path, line, col) -> LspResult<Vec<String>>Go-to-definition locations
referencesasync fn references(&mut self, path, line, col) -> LspResult<Vec<String>>Find all references
document_symbolsasync fn document_symbols(&mut self, path) -> LspResult<Vec<SymbolInfo>>File outline
diagnosticsasync fn diagnostics(&mut self, path) -> LspResult<Vec<LspDiagnostic>>Compiler errors/warnings
shutdown_allasync fn shutdown_all(&self)Gracefully stop all servers

LspClient

Low-level client for a single server. Usually accessed through LspManager, but available for direct use:

use cersei_lsp::{LspClient, LspServerConfig};

let config = LspServerConfig::new(
    "rust-analyzer", "rust-analyzer",
    &["*.rs"], &[(".rs", "rust")]
);
let client = LspClient::new(config);
client.start(Path::new("/project")).await?;
client.initialize().await?;
client.open_document(Path::new("/project/src/main.rs")).await?;

let hover = client.hover(Path::new("/project/src/main.rs"), 10, 5).await?;
client.shutdown().await?;

Types

// Symbol from document_symbols()
pub struct SymbolInfo {
    pub name: String,      // e.g. "Config"
    pub kind: String,      // e.g. "struct", "function", "class"
    pub range: Range,      // Start/end position
    pub children: Vec<SymbolInfo>, // Nested symbols (methods, fields)
}

// Diagnostic from diagnostics()
pub struct LspDiagnostic {
    pub file: String,
    pub line: u32,          // 0-based
    pub col: u32,           // 0-based
    pub severity: DiagnosticSeverity, // Error, Warning, Information, Hint
    pub message: String,
    pub source: Option<String>,  // e.g. "rustc", "clippy"
    pub code: Option<String>,    // e.g. "E0308"
}

Built-in Server Configs

ServerCommandExtensionsLanguage ID
rust-analyzerrust-analyzer.rsrust
pyrightpyright-langserver.py, .pyipython
typescript-language-servertypescript-language-server --stdio.ts, .tsx, .js, .jsx, .mjs, .cjstypescript, javascript
goplsgopls.gogo
clangdclangd.c, .h, .cpp, .hpp, .cc, .cxxc, cpp
ruby-lspruby-lsp --stdio.rbruby
phpactorphpactor language-server.phpphp
lua-language-serverlua-language-server --stdio.lualua
bash-language-serverbash-language-server start.sh, .bashshellscript
sourcekit-lspsourcekit-lsp.swiftswift
omnisharpOmniSharp -lsp.cscsharp
jdtlsjdtls.javajava
zlszls.zigzig

Custom Server Example

use cersei_lsp::{LspManager, LspServerConfig};

let mut mgr = LspManager::new("/project");

// Add Elixir support
let mut elixir = LspServerConfig::new(
    "elixir-ls", "elixir-ls",
    &["*.ex", "*.exs"],
    &[(".ex", "elixir"), (".exs", "elixir")],
);
elixir.args = vec!["--stdio".to_string()];
mgr.register_server(elixir);

mgr.register_builtins(); // Also load defaults

Global Singleton

For long-running applications (like Abstract CLI), use the global manager:

use cersei_lsp::global_lsp_manager;

let mgr = global_lsp_manager(Path::new("/project"));
let mut guard = mgr.lock().await;
guard.register_builtins();
let symbols = guard.document_symbols(Path::new("src/lib.rs")).await?;

Tree-sitter Code Intelligence

Located in cersei_tools::tool_primitives::code_intel. Parses source files using native tree-sitter grammars to extract imports and symbols without running a language server.

Supported Languages

LanguageGrammar CrateImport NodesSymbol Nodes
Rusttree-sitter-rustuse_declarationfunction_item, struct_item, enum_item, mod_item, trait_item, type_item
TypeScript/JStree-sitter-typescriptimport_statement (source field)function_declaration, class_declaration, interface_declaration, type_alias_declaration, enum_declaration
Pythontree-sitter-pythonimport_statement, import_from_statementfunction_definition, class_definition
Gotree-sitter-goimport_declarationfunction_declaration, method_declaration, type_spec (struct/interface)

Analyzing a Single File

use cersei_tools::tool_primitives::code_intel::{analyze_file, Language};
use std::path::Path;

let source = r#"
use std::collections::HashMap;
use serde::Serialize;

pub struct Config {
    pub name: String,
}

pub fn load_config() -> Config {
    Config { name: "test".into() }
}
"#;

let intel = analyze_file(Path::new("config.rs"), source).unwrap();
assert_eq!(intel.language, Language::Rust);
assert_eq!(intel.imports.len(), 2);       // HashMap, Serialize
assert!(intel.symbols.iter().any(|s| s.name == "Config"));
assert!(intel.symbols.iter().any(|s| s.name == "load_config"));

for sym in &intel.symbols {
    println!("  {} {} (line {})", sym.kind.label(), sym.name, sym.line);
}
// Output:
//   struct Config (line 5)
//   fn load_config (line 9)

Scanning a Project

scan_project() discovers all source files, parses them, and returns the most important ones ranked by a dependency score:

use cersei_tools::tool_primitives::code_intel::{scan_project, format_project_intel};
use std::path::Path;

let intels = scan_project(Path::new("/path/to/project"), 20);

// Print ranked file summaries
println!("{}", format_project_intel(&intels));
// Output:
// - src/main.rs — fn main | imports: use crate::config, use std::sync::Arc
// - src/config.rs — struct Config, fn load_config | imports: use serde::Serialize
// - src/lib.rs — mod config, mod server, mod routes

Scoring Algorithm

Files are ranked by importance score:

FactorPointsDescription
Entry point filename+100main.rs, lib.rs, App.tsx, index.ts, main.py, etc.
Config filename+80package.json, Cargo.toml, tsconfig.json, etc.
Store/state path+60Path contains "store", "state", "context", "reducer"
Type definition path+40Path contains "types", "interfaces", or .d.ts extension
Import frequency+5 per importFiles imported by many others score higher
Symbol count+3 per symbolFiles with more definitions are more architecturally important

API Reference

Prop

Type

FileIntel

Prop

Type

Symbol

Prop

Type

SymbolKind: Function, Struct, Class, Interface, Enum, Module, Type, Constant


Bash Command Safety Analysis

Located in cersei_tools::tool_primitives::bash_safety. Uses tree-sitter to parse bash commands into ASTs and classify them by risk level.

Usage

use cersei_tools::tool_primitives::bash_safety::{analyze_command, is_safe, is_forbidden};

// Safe commands
assert!(is_safe("ls -la"));
assert!(is_safe("grep -r 'TODO' src/"));
assert!(is_safe("git status"));

// Dangerous commands
assert!(is_forbidden("sudo rm -rf /"));
assert!(is_forbidden("dd if=/dev/zero of=/dev/sda"));

// Detailed analysis
let analysis = analyze_command("git push origin main && rm temp.txt");
println!("Risk: {:?}", analysis.risk);        // High
println!("Reasons: {:?}", analysis.reasons);   // ["git push", "file deletion (rm)"]
println!("Commands: {:?}", analysis.commands);  // ["git", "rm"]
println!("Write paths: {:?}", analysis.write_paths); // ["temp.txt"]

Risk Levels

LevelDescriptionExample Commands
SafeRead-only, navigation, inspectionls, cat, grep, pwd, git status, echo
ModerateWrites files, runs buildsmkdir, cp, cargo build, npm install, git add
HighDestructive, network, permissionsrm, chmod, curl, ssh, git push
ForbiddenNever auto-approvesudo, rm -rf /, dd, mkfs

Detected Constructs

The analyzer detects these AST patterns:

ConstructDetectionRisk Impact
Command substitution $(...)command_substitution nodeModerate
Process substitution <(...)process_substitution nodeModerate
File redirection > filefile_redirect nodeModerate (extracts target path)
Pipeline cmd1 | cmd2pipeline nodeModerate
Privilege escalation sudocommand name checkForbidden
Destructive flags -rf /argument inspectionForbidden

API Reference

Prop

Type

BashAnalysis

Prop

Type


LSP Tool (Agent-Facing)

The LSP tool is automatically registered in Abstract CLI and available to any agent built with cersei-tools. It exposes all 5 LSP operations to the model.

Tool Schema

{
  "action": "hover | definition | references | symbols | diagnostics",
  "file": "/path/to/file.rs",
  "line": 10,
  "column": 5
}
  • line and column are 1-based (converted to 0-based internally)
  • file can be absolute or relative to working directory
  • line/column are required for hover, definition, references; optional for symbols and diagnostics

Example Agent Interaction

User: What type is the `config` variable on line 15 of src/main.rs?

Agent: [calls LSP tool with action=hover, file=src/main.rs, line=15, column=10]
       The `config` variable is of type `AppConfig` (defined in src/config.rs:17).

Integration in Abstract CLI

Abstract automatically leverages both systems:

  1. At startup: scan_project() runs tree-sitter analysis on top 20 files, injects dependency-ranked summaries into the system prompt as project_intel
  2. During exploration: The agent calls the LSP tool for semantic queries (hover, definitions, references)
  3. Before bash execution: analyze_command() classifies risk level for the permission system
  4. In the TUI: /graph overlay can visualize which LSP servers are available for the project

On this page