use std::collections::HashMap;
use std::io::IsTerminal;
use std::path::Path;
use anyhow::Result;
use infigraph_core::Infigraph;
use infigraph_languages::bundled_registry;
pub(crate) fn cmd_init(root: &Path, group: Option<&str>, quick: bool, yes: bool) -> Result<()> {
let registry = bundled_registry()?;
let mut prism = Infigraph::open(root, registry)?;
prism.init()?;
println!("Initialized infigraph in {}", root.display());
println!("Graph database created at .infigraph/graph/");
if !quick {
let languages = detect_languages(root);
if !languages.is_empty() {
let lang_parts: Vec<String> = languages
.iter()
.map(|(name, count)| format!("{name} ({count} files)"))
.collect();
println!("\nDetected languages: {}", lang_parts.join(", "));
} else {
println!("\nNo source files detected.");
}
let suggestions = suggest_ignore_entries(root, &languages);
if !suggestions.is_empty() {
println!("\nSuggested .infigraphignore entries:");
for entry in &suggestions {
println!(" {entry}");
}
let ignore_path = root.join(".infigraphignore");
if !ignore_path.exists() {
let should_write = if yes {
true
} else if std::io::stdin().is_terminal() {
print!("\nWrite .infigraphignore with these entries? [Y/n] ");
use std::io::Write;
std::io::stdout().flush()?;
let answer = read_line_stdin();
answer.is_empty() || answer.starts_with('y') || answer.starts_with('Y')
} else {
true
};
if should_write {
let content = format!(
"# Auto-generated by infigraph init\n{}\n",
suggestions.join("\n")
);
std::fs::write(&ignore_path, content)?;
println!(" Wrote .infigraphignore");
}
} else {
println!(" .infigraphignore already exists, skipping.");
}
}
let should_index = if yes {
true
} else if std::io::stdin().is_terminal() {
print!("\nRun initial index? [Y/n] ");
use std::io::Write;
std::io::stdout().flush()?;
let answer = read_line_stdin();
answer.is_empty() || answer.starts_with('y') || answer.starts_with('Y')
} else {
false
};
if should_index {
println!();
crate::index::cmd_index(root, false, false)?;
}
}
let group_context = if let Some(group_name) = group {
use infigraph_core::multi::Registry;
let reg = Registry::load().unwrap_or_default();
if let Some(g) = reg.groups.get(group_name) {
let repo_list: Vec<String> = g
.repos
.iter()
.map(|r| {
reg.repos
.get(r)
.map(|e| format!("- `{}` — {}", r, e.path.display()))
.unwrap_or_else(|| format!("- `{}`", r))
})
.collect();
Some(format!(
"\n## This Repo's Group: `{}`\n\nThis repo is part of the `{}` microservice group. Other repos in this group:\n{}\n\nUse `group_query` with group name `{}` to query across all repos.\nUse `group_sync` then `group_deps` to find cross-service HTTP dependencies.\n",
group_name, group_name, repo_list.join("\n"), group_name
))
} else {
println!(
" Warning: group '{}' not found in registry. Skipping group context.",
group_name
);
None
}
} else {
None
};
write_agent_instructions(root, group_context.as_deref())?;
Ok(())
}
fn read_line_stdin() -> String {
let mut buf = String::new();
let _ = std::io::stdin().read_line(&mut buf);
buf.trim().to_string()
}
fn detect_languages(root: &Path) -> Vec<(String, usize)> {
let ext_map: HashMap<&str, &str> = [
("rs", "Rust"),
("py", "Python"),
("js", "JavaScript"),
("ts", "TypeScript"),
("tsx", "TSX"),
("jsx", "JSX"),
("java", "Java"),
("kt", "Kotlin"),
("go", "Go"),
("rb", "Ruby"),
("c", "C"),
("cpp", "C++"),
("h", "C/C++ Header"),
("cs", "C#"),
("swift", "Swift"),
("scala", "Scala"),
("php", "PHP"),
("lua", "Lua"),
("zig", "Zig"),
("dart", "Dart"),
("ex", "Elixir"),
("exs", "Elixir"),
("hs", "Haskell"),
("pl", "Perl"),
("r", "R"),
("sql", "SQL"),
("proto", "Protocol Buffers"),
("graphql", "GraphQL"),
("gql", "GraphQL"),
("yaml", "YAML"),
("yml", "YAML"),
("toml", "TOML"),
("json", "JSON"),
("md", "Markdown"),
("sh", "Shell"),
("bash", "Shell"),
("zsh", "Shell"),
]
.into_iter()
.collect();
let mut counts: HashMap<String, usize> = HashMap::new();
walk_and_count(root, root, &ext_map, &mut counts, 0);
let mut sorted: Vec<(String, usize)> = counts.into_iter().collect();
sorted.sort_by_key(|b| std::cmp::Reverse(b.1));
sorted
}
fn walk_and_count(
dir: &Path,
_root: &Path,
ext_map: &HashMap<&str, &str>,
counts: &mut HashMap<String, usize>,
depth: usize,
) {
if depth > 20 {
return;
}
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with('.') {
continue;
}
if path.is_dir() {
match name_str.as_ref() {
"node_modules" | "target" | "dist" | "build" | "vendor" | "__pycache__"
| "venv" | ".venv" | "out" | "bin" | "obj" | "generated" | "third_party"
| "CMakeFiles" => continue,
_ => {}
}
walk_and_count(&path, _root, ext_map, counts, depth + 1);
} else if path.is_file() {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if let Some(&lang) = ext_map.get(ext) {
*counts.entry(lang.to_string()).or_insert(0) += 1;
}
}
}
}
}
fn suggest_ignore_entries(root: &Path, languages: &[(String, usize)]) -> Vec<String> {
let mut suggestions = Vec::new();
let has_py = languages.iter().any(|(lang, _)| lang == "Python");
if root.join("package.json").exists() {
if root.join("node_modules").exists() {
suggestions.push("node_modules/".to_string());
}
if root.join("dist").exists() {
suggestions.push("dist/".to_string());
}
}
if root.join("Cargo.toml").exists() && root.join("target").exists() {
suggestions.push("target/".to_string());
}
if root.join("requirements.txt").exists() || root.join("pyproject.toml").exists() {
if root.join("venv").exists() {
suggestions.push("venv/".to_string());
}
if root.join(".venv").exists() {
suggestions.push(".venv/".to_string());
}
}
if (root.join("build.gradle").exists()
|| root.join("build.gradle.kts").exists()
|| root.join("pom.xml").exists())
&& root.join("build").exists()
{
suggestions.push("build/".to_string());
}
if has_py {
suggestions.push("__pycache__/".to_string());
}
if (root.join("go.mod").exists() || root.join("Gemfile").exists())
&& root.join("vendor").exists()
{
suggestions.push("vendor/".to_string());
}
suggestions
}
pub(crate) fn infigraph_instructions() -> &'static str {
r#"# Infigraph — Code Intelligence
This project is indexed by Infigraph. Use Infigraph tools FIRST for all code tasks. Fall back to grep/read only if Infigraph returns nothing or for non-code files.
## Rules
1. Check `list_projects` before indexing — don't re-index
2. **`search`** for ALL code search — hybrid BM25+vector+grep in one call, auto-escalates
3. **`get_doc_context`** before editing any function — returns source+callers+callees in one call
4. **`trace_callers`** / **`find_all_references`** before refactoring — never grep for callers
5. **`trace_callees`** / **`transitive_impact`** for blast radius — never manually trace call chains
6. Read files directly only for non-code files (configs, docs, manifests) or edit tool line-number context
## Workflows
- **Find code:** `search` → if need symbol detail: `get_code_snippet` or `symbol_context`
- **Before editing:** `get_doc_context`
- **Before refactoring:** `find_all_references` → `transitive_impact` → edit
- **Onboarding:** `index_project` → `get_architecture` → `get_stats`
- **Multi-repo:** `group_create` → `group_add` × N → `group_index` → `group_sync` → `group_link`
> Each tool description says what it replaces — check descriptions when unsure which tool to use.
## Session Continuity — MANDATORY
- **On session start:** MUST call `get_latest_session` to resume prior context
- **After context compaction:** if you see "continued from a previous conversation" or a compaction summary, IMMEDIATELY call `save_session` with whatever context survived before doing anything else
- **MUST call `save_session` IMMEDIATELY (before responding to the user)** when ANY of these occur. No session-end signal exists — if you don't save now, context is lost forever:
1. **Finding** — root cause identified, discovered a bug, learned how something works
2. **Milestone** — bug fixed and verified, feature committed, test passing, build green
3. **Decision** — chose an approach, ruled something out, changed strategy
4. **Task done** — any pending task from a prior session is completed
5. **Periodic** — if you have NOT called `save_session` in the last 5 exchanges with the user, call it NOW regardless of whether anything dramatic happened. This is a hard rule, not a suggestion.
- Do NOT defer saves ("I'll save later"). Do NOT batch them. Do NOT wait for user to ask.
- "Later" does not exist — context compaction or session end can happen at any moment.
- Same-day saves merge: summary/pending_tasks overwrite, decisions append, files_touched union
- **Narrative dumps:** On every `save_session`, include `narrative` field with full session story — what was explored, found, reasoned, decided, and why. Chronological prose, not terse bullets. Written to `.infigraph/sessions/session_YYYY-MM-DD.md` and embedded for semantic search. On session start, if `get_latest_session` shows a narrative log path, read it when structured fields aren't enough context.
"#
}
pub(crate) struct AgentInstructionTarget {
pub path: &'static str,
pub wrapper: fn(&str) -> String,
pub label: &'static str,
}
pub(crate) fn wrap_plain(content: &str) -> String {
content.to_string()
}
pub(crate) fn wrap_cursor_mdc(content: &str) -> String {
format!(
"---\ndescription: Infigraph code intelligence — use Infigraph MCP tools for all code navigation\nglobs: \nalwaysApply: true\n---\n\n{content}"
)
}
pub(crate) fn wrap_kiro_rule(content: &str) -> String {
format!("---\nname: infigraph\ndescription: Use Infigraph MCP tools for code navigation\ntype: always\n---\n\n{content}")
}
pub(crate) const AGENT_INSTRUCTION_TARGETS: &[AgentInstructionTarget] = &[
AgentInstructionTarget {
path: ".cursor/rules/infigraph.mdc",
wrapper: wrap_cursor_mdc,
label: "Cursor",
},
AgentInstructionTarget {
path: ".github/copilot-instructions.md",
wrapper: wrap_plain,
label: "GitHub Copilot",
},
AgentInstructionTarget {
path: ".windsurf/rules/infigraph.md",
wrapper: wrap_plain,
label: "Windsurf",
},
AgentInstructionTarget {
path: ".kiro/rules/infigraph.md",
wrapper: wrap_kiro_rule,
label: "Kiro",
},
AgentInstructionTarget {
path: "AGENTS.md",
wrapper: wrap_plain,
label: "Codex/OpenAI",
},
AgentInstructionTarget {
path: "GEMINI.md",
wrapper: wrap_plain,
label: "Gemini CLI",
},
];
pub(crate) fn write_agent_instructions(
root: &std::path::Path,
group_context: Option<&str>,
) -> Result<()> {
let base = infigraph_instructions();
let instructions = match group_context {
Some(ctx) => format!("{base}\n{ctx}"),
None => base.to_string(),
};
let marker = "<!-- infigraph-instructions -->";
let mut written = Vec::new();
for target in AGENT_INSTRUCTION_TARGETS {
let file_path = root.join(target.path);
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent)?;
}
let wrapped = (target.wrapper)(&instructions);
let block = format!("{marker}\n{wrapped}\n{marker}");
let existing = std::fs::read_to_string(&file_path).unwrap_or_default();
let new_content = if existing.contains(marker) {
let start = existing.find(marker).unwrap();
let after_first = &existing[start + marker.len()..];
let end = after_first
.find(marker)
.map(|p| start + marker.len() + p + marker.len())
.unwrap_or(existing.len());
format!("{}{}{}", &existing[..start], block, &existing[end..])
} else if existing.is_empty() {
block
} else {
format!("{existing}\n\n{block}")
};
std::fs::write(&file_path, new_content)?;
written.push(target.label);
}
if !written.is_empty() {
println!(" Wrote agent instructions for: {}", written.join(", "));
}
Ok(())
}