Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Agents that consult the semantic graph instead of grepping the filesystem make *

- **Fewer tool calls** — agents navigate call chains (`codegraph_flow`), not grep
- **Local & fast** — full re-index 139 files in ~190 ms, nothing leaves your machine
- **Works everywhere** — 14 languages, 6 storage backends, 24 MCP tools, one binary
- **Works everywhere** — 17 languages, 6 storage backends, 42 MCP tools, one binary
- **Semantic, not syntactic** — symbols have global IDs; edges derived from call chains with markers (`LOOP`, `IF_TRUE`, `RETURN`, …)
- **Binary analysis** — builds call graphs directly from ELF/PE/Mach-O binaries via [radare2](docs/binary-analysis.md), no source needed

Expand Down Expand Up @@ -59,7 +59,7 @@ The agent binds the workspace with `codegraph_init {"path": ...}` and gets tools
CodeGraph supports parsing both **source code** (via tree-sitter) and **configuration/document** files:

### Source Code Languages
14 languages: TypeScript · TSX · JavaScript · Python · Go · Rust · Java · C · C++ · C# · Ruby · PHP · Scala · Swift · Lua
17 languages: TypeScript · TSX · JavaScript · Python · Go · Rust · Java · C · C++ · C# · Ruby · PHP · Scala · Swift · Lua · Svelte · Astro

### Configuration/Document Formats

Expand Down
13 changes: 9 additions & 4 deletions crates/codegraph-installer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Multi-agent installer. One target per file in `targets/`.

pub mod skills;
pub mod targets;
pub mod workflow;

use anyhow::Result;
use camino::Utf8PathBuf;
Expand Down Expand Up @@ -51,6 +53,11 @@ pub trait AgentTarget: Send + Sync {
fn detect(&self, opts: &InstallOpts) -> DetectStatus;
fn install(&self, opts: &InstallOpts) -> Result<InstallReport>;
fn uninstall(&self, opts: &InstallOpts) -> Result<InstallReport>;
/// Thư mục cài skills của agent (None = agent không hỗ trợ skills).
fn skills_dir(&self, opts: &InstallOpts) -> Option<Utf8PathBuf> {
let _ = opts;
None
}
}

pub fn registry() -> Vec<Arc<dyn AgentTarget>> {
Expand All @@ -61,13 +68,11 @@ pub fn registry() -> Vec<Arc<dyn AgentTarget>> {
Arc::new(targets::opencode::OpencodeTarget),
Arc::new(targets::hermes::HermesTarget),
Arc::new(targets::antigravity::AntigravityTarget),
Arc::new(targets::zcode::ZcodeTarget),
]
}

/// Project-scoped targets only (skip ones that only support global config).
/// Alias của `registry()` — mọi target hiện đều hỗ trợ cả hai scope.
pub fn project_registry() -> Vec<Arc<dyn AgentTarget>> {
registry()
.into_iter()
.filter(|t| t.id() != "codex")
.collect()
}
91 changes: 91 additions & 0 deletions crates/codegraph-installer/src/skills.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! CodeGraph skills shipped with the binary (`skills/*/SKILL.md` in the repo,
//! embedded at compile time) and helpers to install/uninstall them into an
//! agent's skills directory.

use anyhow::Result;
use camino::Utf8PathBuf;

/// `(skill_name, SKILL.md content)` for every skill shipped with codegraph.
pub const SKILLS: &[(&str, &str)] = &[
(
"codegraph-code-review",
include_str!("../../../skills/codegraph-code-review/SKILL.md"),
),
(
"codegraph-development-workflow",
include_str!("../../../skills/codegraph-development-workflow/SKILL.md"),
),
(
"codegraph-figma-to-frontend",
include_str!("../../../skills/codegraph-figma-to-frontend/SKILL.md"),
),
(
"codegraph-reverse-binary",
include_str!("../../../skills/codegraph-reverse-binary/SKILL.md"),
),
(
"codegraph-security-review",
include_str!("../../../skills/codegraph-security-review/SKILL.md"),
),
];

/// Ghi mọi skill vào `dir/<name>/SKILL.md` (skip nếu nội dung đã khớp).
/// Trả về danh sách file thực sự ghi.
pub fn install_skills(dir: &Utf8PathBuf) -> Result<Vec<Utf8PathBuf>> {
let mut written = Vec::new();
for (name, content) in SKILLS {
let skill_md = dir.join(name).join("SKILL.md");
let existing = std::fs::read_to_string(skill_md.as_std_path()).ok();
if existing.as_deref() == Some(content) {
continue;
}
if let Some(parent) = skill_md.parent() {
std::fs::create_dir_all(parent.as_std_path())?;
}
std::fs::write(skill_md.as_std_path(), content)?;
written.push(skill_md);
}
Ok(written)
}

/// Xoá thư mục skill nếu SKILL.md trong đó khớp nội dung mình ship (chỉ xoá
/// những gì codegraph đã cài; skill do user tự viết thì giữ nguyên).
pub fn uninstall_skills(dir: &Utf8PathBuf) -> Result<Vec<Utf8PathBuf>> {
let mut removed = Vec::new();
for (name, content) in SKILLS {
let skill_dir = dir.join(name);
let skill_md = skill_dir.join("SKILL.md");
let existing = std::fs::read_to_string(skill_md.as_std_path()).ok();
if existing.as_deref() != Some(content) {
continue;
}
match std::fs::remove_dir_all(skill_dir.as_std_path()) {
Ok(()) => removed.push(skill_dir),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(e.into()),
}
}
Ok(removed)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn skills_roundtrip() {
let d = tempfile::tempdir().unwrap();
let dir = Utf8PathBuf::from_path_buf(d.path().to_path_buf()).unwrap();
assert_eq!(install_skills(&dir).unwrap().len(), SKILLS.len());
// Idempotent.
assert!(install_skills(&dir).unwrap().is_empty());
assert_eq!(uninstall_skills(&dir).unwrap().len(), SKILLS.len());
assert!(uninstall_skills(&dir).unwrap().is_empty());
// Skill do user tự sửa thì không bị xoá.
let custom = dir.join(SKILLS[0].0).join("SKILL.md");
std::fs::create_dir_all(custom.parent().unwrap().as_std_path()).unwrap();
std::fs::write(custom.as_std_path(), "# my own version\n").unwrap();
assert!(uninstall_skills(&dir).unwrap().is_empty());
assert!(custom.exists());
}
}
10 changes: 10 additions & 0 deletions crates/codegraph-installer/src/targets/antigravity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,16 @@ impl AgentTarget for AntigravityTarget {
Ok(InstallReport::Unchanged)
}
}

fn skills_dir(&self, opts: &InstallOpts) -> Option<Utf8PathBuf> {
if opts.global {
None
} else {
opts.project_root
.as_ref()
.map(|r| r.join(".agents").join("skills"))
}
}
}

const AGENTS_MD_CONTENT: &str = r#"---
Expand Down
15 changes: 13 additions & 2 deletions crates/codegraph-installer/src/targets/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub struct ClaudeTarget;
impl ClaudeTarget {
fn settings_path(&self, opts: &InstallOpts) -> Option<Utf8PathBuf> {
if opts.global {
let home = dirs::home_dir()?;
let home = opts.home_dir()?;
Utf8PathBuf::from_path_buf(home.join(".claude").join("settings.json")).ok()
} else {
let root = opts.project_root.as_ref()?;
Expand All @@ -19,7 +19,7 @@ impl ClaudeTarget {
}
fn instructions_path(&self, opts: &InstallOpts) -> Option<Utf8PathBuf> {
if opts.global {
let home = dirs::home_dir()?;
let home = opts.home_dir()?;
Utf8PathBuf::from_path_buf(home.join(".claude").join("CLAUDE.md")).ok()
} else {
opts.project_root.as_ref().map(|r| r.join("CLAUDE.md"))
Expand Down Expand Up @@ -134,6 +134,17 @@ impl AgentTarget for ClaudeTarget {
Ok(InstallReport::Updated(removed))
}
}

fn skills_dir(&self, opts: &InstallOpts) -> Option<Utf8PathBuf> {
if opts.global {
let home = opts.home_dir()?;
Utf8PathBuf::from_path_buf(home.join(".claude").join("skills")).ok()
} else {
opts.project_root
.as_ref()
.map(|r| r.join(".claude").join("skills"))
}
}
}

fn serve_args(opts: &InstallOpts) -> Value {
Expand Down
67 changes: 50 additions & 17 deletions crates/codegraph-installer/src/targets/codex.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Codex CLI — config at `~/.codex/config.toml`, instructions at `~/.codex/AGENTS.md`.
//! Uses toml_edit to preserve sibling tables and user formatting.
//! Codex CLI — config at `~/.codex/config.toml` (global) hoặc
//! `<project>/.codex/config.toml` (project-scoped), instructions at
//! `AGENTS.md` cạnh đó. Uses toml_edit to preserve sibling tables and user
//! formatting.

use crate::{AgentTarget, DetectStatus, InstallOpts, InstallReport, INSTRUCTIONS_MD};
use anyhow::Result;
Expand All @@ -9,13 +11,26 @@ use toml_edit::{value, Array, DocumentMut, Item, Table};
pub struct CodexTarget;

impl CodexTarget {
fn config_path(&self) -> Option<Utf8PathBuf> {
let home = dirs::home_dir()?;
Utf8PathBuf::from_path_buf(home.join(".codex").join("config.toml")).ok()
/// Thư mục `.codex` cho scope hiện tại (global = trong home).
fn codex_dir(&self, opts: &InstallOpts) -> Option<Utf8PathBuf> {
if opts.global {
let home = opts.home_dir()?;
Utf8PathBuf::from_path_buf(home.join(".codex")).ok()
} else {
opts.project_root.as_ref().map(|r| r.join(".codex"))
}
}
fn agents_path(&self) -> Option<Utf8PathBuf> {
let home = dirs::home_dir()?;
Utf8PathBuf::from_path_buf(home.join(".codex").join("AGENTS.md")).ok()
fn config_path(&self, opts: &InstallOpts) -> Option<Utf8PathBuf> {
self.codex_dir(opts).map(|d| d.join("config.toml"))
}
/// AGENTS.md: global thì nằm trong `.codex/`, project thì ở root (dùng
/// chung cho mọi agent đọc AGENTS.md của repo).
fn agents_path(&self, opts: &InstallOpts) -> Option<Utf8PathBuf> {
if opts.global {
self.codex_dir(opts).map(|d| d.join("AGENTS.md"))
} else {
opts.project_root.as_ref().map(|r| r.join("AGENTS.md"))
}
}
}

Expand All @@ -27,13 +42,19 @@ impl AgentTarget for CodexTarget {
"Codex CLI"
}

fn detect(&self, _opts: &InstallOpts) -> DetectStatus {
let Some(p) = self.config_path() else {
fn detect(&self, opts: &InstallOpts) -> DetectStatus {
let Some(dir) = self.codex_dir(opts) else {
return DetectStatus::NotFound;
};
if !p.exists() {
if !dir.exists() {
return DetectStatus::NotFound;
}
let Some(p) = self.config_path(opts) else {
return DetectStatus::Found;
};
if !p.exists() {
return DetectStatus::Found;
}
let Ok(text) = std::fs::read_to_string(p.as_std_path()) else {
return DetectStatus::Found;
};
Expand All @@ -54,7 +75,7 @@ impl AgentTarget for CodexTarget {

fn install(&self, opts: &InstallOpts) -> Result<InstallReport> {
let config = self
.config_path()
.config_path(opts)
.ok_or_else(|| anyhow::anyhow!("no codex config path"))?;
let text = std::fs::read_to_string(config.as_std_path()).unwrap_or_default();
let mut doc: DocumentMut = if text.is_empty() {
Expand Down Expand Up @@ -98,13 +119,21 @@ impl AgentTarget for CodexTarget {
std::fs::write(config.as_std_path(), doc.to_string())?;
written.push(config);
}
if let Some(md) = self.agents_path() {
if let Some(md) = self.agents_path(opts) {
let want = INSTRUCTIONS_MD;
let existing = std::fs::read_to_string(md.as_std_path()).ok();
if existing.as_deref() != Some(INSTRUCTIONS_MD) {
// AGENTS.md ở project root dùng chung với các agent khác — chỉ ghi
// khi chưa tồn tại, không đè nội dung user đang có.
let should_write = if opts.global {
existing.as_deref() != Some(want)
} else {
existing.is_none()
};
if should_write {
if let Some(parent) = md.parent() {
std::fs::create_dir_all(parent.as_std_path())?;
}
std::fs::write(md.as_std_path(), INSTRUCTIONS_MD)?;
std::fs::write(md.as_std_path(), want)?;
written.push(md);
}
}
Expand All @@ -115,8 +144,8 @@ impl AgentTarget for CodexTarget {
}
}

fn uninstall(&self, _opts: &InstallOpts) -> Result<InstallReport> {
let Some(config) = self.config_path() else {
fn uninstall(&self, opts: &InstallOpts) -> Result<InstallReport> {
let Some(config) = self.config_path(opts) else {
return Ok(InstallReport::Unchanged);
};
if !config.exists() {
Expand All @@ -137,4 +166,8 @@ impl AgentTarget for CodexTarget {
Ok(InstallReport::Unchanged)
}
}

fn skills_dir(&self, opts: &InstallOpts) -> Option<Utf8PathBuf> {
self.codex_dir(opts).map(|d| d.join("skills"))
}
}
11 changes: 11 additions & 0 deletions crates/codegraph-installer/src/targets/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,15 @@ impl AgentTarget for CursorTarget {
Ok(InstallReport::Updated(removed))
}
}

fn skills_dir(&self, opts: &InstallOpts) -> Option<Utf8PathBuf> {
if opts.global {
let home = opts.home_dir()?;
Utf8PathBuf::from_path_buf(home.join(".cursor").join("skills")).ok()
} else {
opts.project_root
.as_ref()
.map(|r| r.join(".cursor").join("skills"))
}
}
}
1 change: 1 addition & 0 deletions crates/codegraph-installer/src/targets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pub mod cursor;
pub mod hermes;
pub(crate) mod jsonutil;
pub mod opencode;
pub mod zcode;
11 changes: 11 additions & 0 deletions crates/codegraph-installer/src/targets/opencode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,15 @@ impl AgentTarget for OpencodeTarget {
Ok(InstallReport::Unchanged)
}
}

fn skills_dir(&self, opts: &InstallOpts) -> Option<Utf8PathBuf> {
if opts.global {
let cfg = dirs::config_dir()?;
Utf8PathBuf::from_path_buf(cfg.join("opencode").join("skills")).ok()
} else {
opts.project_root
.as_ref()
.map(|r| r.join(".opencode").join("skills"))
}
}
}
Loading
Loading