use std::fs;
use std::path::{Path, PathBuf};
pub const SCRYER_START_SENTINEL: &str = "<!-- scryer:start -->";
pub const SCRYER_END_SENTINEL: &str = "<!-- scryer:end -->";
pub fn generate_agent_instructions() -> String {
format!(
r#"{SCRYER_START_SENTINEL}
# Code Intelligence (Scryer MCP & CLI)
This repository is indexed by **Scryer**, a Tree-Sitter and Tree-sitter Stack Graphs code intelligence server and CLI.
Scryer provides deterministic AST retrieval, call graph analysis, and cross-file definition resolution across Rust, Python, and TypeScript.
## Navigation Guidelines & Exploration Best Practices
- **Work Outside-In with AST Outlines**: When exploring unfamiliar code, run `get_file_outline` first to inspect structural declarations and exact line bounds without loading implementation bodies.
- **Targeted Line Reading**: Once you identify the target symbol or function from an outline or definition jump, read only that specific slice or function body to understand or edit logic.
- **Symbol & Navigation Priority**: Do NOT use `grep` or `ripgrep` as the primary way to locate symbol definitions, callers, or references in code—use `resolve_definition`, `find_references`, and `get_file_outline` first.
- **Avoid Loading Full Source Files for Scouting**: Do NOT dump entire large source files into context to search for symbols. Reserve whole-file reading for configs, documentation, tests, or when you explicitly need the full file context.
- **Verify Contracts**: Do NOT guess complex struct fields, enum variants, or method signatures when writing or refactoring code; inspect them with `get_type_contract`.
- **Grep for Non-Structural Searches**: Use `grep` or text search for string literals, error logs, user-facing messages, comments, environment variables, non-code files (Markdown, JSON, YAML, TOML, Dockerfiles), or as a fallback when AST indexing does not resolve a symbol.
## Invocation Modes (Choose the mode supported by your environment)
### 1. Direct MCP Tools (Codex, Cursor, Claude Code, Windsurf)
If Scryer MCP tools are available directly in your agent environment, invoke them by name:
- `get_file_outline(file_path: "src/main.rs")`
- `resolve_definition(file_path: "src/main.rs", line: 42, column: 10)`
- `find_references(symbol: "MySymbol")`
- `get_type_contract(type_name: "MyType")`
### 2. MCP Gateway Tools (Google Antigravity / Gemini CLI)
In Antigravity, Scryer tools are lazily loaded. Dispatch all tool calls through the `call_mcp_tool` gateway:
```json
call_mcp_tool(
ServerName: "Scryer",
ToolName: "get_file_outline",
Arguments: {{ "file_path": "src/main.rs" }}
)
```
### 3. CLI Subcommands & Shell Fallback (Terminal / Non-MCP Environments)
If your agent operates via shell commands or lacks direct MCP tooling, run Scryer tools directly via the `scryer` CLI binary:
```bash
# Discover all tools and flags
scryer --help
scryer get-file-outline --help
# Direct tool invocations (kebab-case or snake_case)
scryer get-file-outline src/main.rs
scryer get-file-outline --file-path src/main.rs --compact
scryer get-type-contract MyType
scryer resolve-definition src/main.rs 42 10
scryer find-references MySymbol
scryer trace-call-hierarchy my_func --direction callers
scryer list-projects
```
## Tool Selection Priority
1. `get_file_outline` (`scryer get-file-outline`) — Inspect functions, structs, traits, enums, and signatures in a file without reading implementation bodies.
2. `get_type_contract` (`scryer get-type-contract`) — View fields, enum variants, and trait implementations for a specific type.
3. `get_enclosing_scope` (`scryer get-enclosing-scope`) — Find the smallest enclosing function, impl, or type at a given file and line.
4. `resolve_definition` (`scryer resolve-definition`) — Jump directly to a symbol's definition across files using precise line/column coordinates.
5. `find_references` (`scryer find-references`) — Find all usages of a symbol in the active project; set `scope_level: "dependencies"` to track workspace usages of external symbols, or `"workspace_all"` across all projects.
6. `get_symbol_docs` (`scryer get-symbol-docs`) — Inspect public documentation, signatures, and code examples for external crate or workspace symbols.
7. `get_crate_outline` (`scryer get-crate-outline`) — Orient yourself in an external crate's public API structure.
8. `search_dependency_symbols` (`scryer search-dependency-symbols`) — Find unknown or partially-named types, traits, or functions across dependencies.
9. `trace_call_hierarchy` (`scryer trace-call-hierarchy`) — Trace inbound callers or outbound callees of a function or method (up to depth 5).
10. `calculate_blast_radius` (`scryer calculate-blast-radius`) — Assess downstream impact, dependents, and affected files before refactoring or deleting symbols.
11. `query_adrs` / `get_symbol_invariants` (`scryer query-adrs`) — Search Architectural Decision Records and design invariants.
12. `record_adr` / `delete_adr` — Record or remove Architectural Decision Records in `docs/adr/`.
## Project & Index Management
- `list_projects` (`scryer list-projects`) / `switch_active_project` — List registered projects and change the session's active project.
- `index_workspace` (`scryer index`) / `get_indexing_status` (`scryer status`) — Trigger an incremental or full re-index and check scanner progress. Pass `project: "0"` or `"dependencies"` to inspect the global Cargo dependency cache.
- `delete_project` (`scryer delete`) — Unregister a project and delete its index data (source files are not touched).
## Payload Budgets & Selective Querying
- Responses enforce a default 1,000-token budget to protect context windows.
- To receive full output without truncation, pass `no_truncate: true` or specify a custom `max_tokens: N`.
- Prefer narrower queries and pagination over large dumps:
- `get_indexing_status`: `project` (e.g. `"0"` or `"dependencies"` for the external Cargo cache).
- `get_file_outline`: `query`, `kinds` (e.g. `["struct"]`), `start_line`/`end_line`, `compact: true`, `offset`/`limit`.
- `find_references`: `file_filter`, `role`, `scope_level`, `offset`/`limit`.
- `get_crate_outline`: `module_path`, `version`.
- `search_dependency_symbols`: `crate_name`, `kind`, `offset`/`limit`.
- `trace_call_hierarchy`: `file_filter`, `limit`.
- `calculate_blast_radius`: `include_symbols: false` (to inspect affected files only), `limit`.
- `query_adrs` / `get_symbol_invariants`: `status`, `compact: true`, `offset`/`limit`.
{SCRYER_END_SENTINEL}"#
)
}
pub fn resolve_agents_file(target: &Path) -> PathBuf {
if target.is_dir() {
target.join("AGENTS.md")
} else {
target.to_path_buf()
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum InstallOutcome {
Created,
Appended,
Updated,
AlreadyUpToDate,
}
pub fn apply_instructions_to_content(content: &str) -> anyhow::Result<(String, InstallOutcome)> {
let instructions = generate_agent_instructions();
let has_start = content.contains(SCRYER_START_SENTINEL);
let has_end = content.contains(SCRYER_END_SENTINEL);
match (has_start, has_end) {
(true, true) => {
let start_idx = content
.find(SCRYER_START_SENTINEL)
.expect("start sentinel checked");
let end_idx = content
.find(SCRYER_END_SENTINEL)
.expect("end sentinel checked")
+ SCRYER_END_SENTINEL.len();
if start_idx >= end_idx {
anyhow::bail!(
"Malformed sentinels: `{}` appears after `{}`",
SCRYER_START_SENTINEL,
SCRYER_END_SENTINEL
);
}
let existing_block = &content[start_idx..end_idx];
if existing_block.trim() == instructions.trim() {
Ok((content.to_string(), InstallOutcome::AlreadyUpToDate))
} else {
let mut new_content = String::new();
new_content.push_str(&content[..start_idx]);
new_content.push_str(&instructions);
new_content.push_str(&content[end_idx..]);
Ok((new_content, InstallOutcome::Updated))
}
}
(true, false) => {
anyhow::bail!(
"Malformed AGENTS.md: found `{}` but missing closing `{}`",
SCRYER_START_SENTINEL,
SCRYER_END_SENTINEL
);
}
(false, true) => {
anyhow::bail!(
"Malformed AGENTS.md: found closing `{}` but missing `{}`",
SCRYER_END_SENTINEL,
SCRYER_START_SENTINEL
);
}
(false, false) => {
if content.trim().is_empty() {
Ok((instructions, InstallOutcome::Created))
} else {
let trimmed = content.trim_end();
let new_content = format!("{trimmed}\n\n{instructions}\n");
Ok((new_content, InstallOutcome::Appended))
}
}
}
}
pub fn install_instructions(
target_path: &Path,
check_only: bool,
) -> anyhow::Result<InstallOutcome> {
let file_path = resolve_agents_file(target_path);
if !file_path.exists() {
if check_only {
anyhow::bail!("Target file `{}` does not exist", file_path.display());
}
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)?;
}
let instructions = format!("{}\n", generate_agent_instructions());
fs::write(&file_path, instructions)?;
return Ok(InstallOutcome::Created);
}
let existing_content = fs::read_to_string(&file_path)?;
let (new_content, outcome) = apply_instructions_to_content(&existing_content)?;
if check_only {
if outcome == InstallOutcome::AlreadyUpToDate {
return Ok(InstallOutcome::AlreadyUpToDate);
} else {
anyhow::bail!(
"Scryer instructions in `{}` are missing or outdated",
file_path.display()
);
}
}
if outcome != InstallOutcome::AlreadyUpToDate {
fs::write(&file_path, new_content)?;
}
Ok(outcome)
}
pub fn generate_agent_skill() -> String {
r#"---
name: scryer
description: >-
Code intelligence and navigation using the Scryer MCP server.
MANDATORY: Execute FIRST whenever exploring, navigating, locating symbols,
finding references, or preparing to edit code in Rust, Python, or TypeScript.
---
# Scryer Code Intelligence Navigation Playbook
Always prioritize Scryer code intelligence tools before reading raw source code or performing refactors.
## Invocation in Antigravity
In Antigravity, MCP tools are lazy-loaded. Invoke all Scryer tools via `call_mcp_tool`:
```json
call_mcp_tool(
ServerName: "Scryer",
ToolName: "...",
Arguments: { ... }
)
```
If MCP tools are unavailable, use the `scryer` CLI binary directly (`scryer --help`, `scryer get-file-outline <file>`).
## Navigation & Editing Workflows
### 1. Work Outside-In: Locate Functions, Types, or Methods to Edit
Avoid loading entire files to locate code.
1. Run `get_file_outline`:
```json
call_mcp_tool(
ServerName: "Scryer",
ToolName: "get_file_outline",
Arguments: {
"file_path": "path/to/file.rs",
"query": "target_symbol_or_function_name"
}
)
```
2. Note the exact `start_line` and `end_line` returned in the `OutlineSymbol`.
3. Call `view_file` targeting **only** that exact slice `[start_line, end_line]` to obtain the verbatim code required for `replace_file_content`.
### 2. Inspecting Data Structures & Interfaces
Before reading an entire file to understand a struct, enum, or trait:
1. Run `get_type_contract`:
```json
call_mcp_tool(
ServerName: "Scryer",
ToolName: "get_type_contract",
Arguments: {
"type_name": "MyStruct"
}
)
```
2. Inspect fields, variants, and trait methods without loading implementation bodies into context.
### 3. Finding Usages & Call Sites (Blast Radius)
Before renaming, modifying signatures, or deleting symbols:
1. Run `find_references`:
```json
call_mcp_tool(
ServerName: "Scryer",
ToolName: "find_references",
Arguments: {
"symbol": "MySymbol",
"scope_level": "project"
}
)
```
2. To see caller chains up to 5 levels deep:
Run `trace_call_hierarchy` with `direction: "callers"`.
"#
.to_string()
}
pub fn install_skill(project_root: &Path, check_only: bool) -> anyhow::Result<InstallOutcome> {
let skill_path = project_root.join(".agents/skills/scryer/SKILL.md");
let content = generate_agent_skill();
if !skill_path.exists() {
if check_only {
anyhow::bail!("Skill file `{}` does not exist", skill_path.display());
}
if let Some(parent) = skill_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&skill_path, &content)?;
return Ok(InstallOutcome::Created);
}
let existing = fs::read_to_string(&skill_path)?;
if existing.trim() == content.trim() {
return Ok(InstallOutcome::AlreadyUpToDate);
}
if check_only {
anyhow::bail!("Skill in `{}` is outdated", skill_path.display());
}
fs::write(&skill_path, content)?;
Ok(InstallOutcome::Updated)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_instructions_contain_sentinels() {
let text = generate_agent_instructions();
assert!(text.starts_with(SCRYER_START_SENTINEL));
assert!(text.ends_with(SCRYER_END_SENTINEL));
assert!(text.contains("get_file_outline"));
assert!(text.contains("calculate_blast_radius"));
assert!(text.contains("call_mcp_tool"));
assert!(text.contains("Codex"));
assert!(text.contains("scryer get-file-outline"));
assert!(text.contains("scryer --help"));
}
#[test]
fn test_skill_contains_gateway_and_cli() {
let text = generate_agent_skill();
assert!(text.contains("call_mcp_tool"));
assert!(text.contains("scryer get-file-outline"));
assert!(text.contains("ServerName: \"Scryer\""));
}
#[test]
fn test_apply_to_empty() {
let (content, outcome) = apply_instructions_to_content("").unwrap();
assert_eq!(outcome, InstallOutcome::Created);
assert!(content.contains(SCRYER_START_SENTINEL));
}
#[test]
fn test_apply_append_existing() {
let initial = "# My Project\n\nExisting guidelines.\n";
let (content, outcome) = apply_instructions_to_content(initial).unwrap();
assert_eq!(outcome, InstallOutcome::Appended);
assert!(content.starts_with("# My Project\n\nExisting guidelines."));
assert!(content.contains(SCRYER_START_SENTINEL));
assert!(content.contains(SCRYER_END_SENTINEL));
}
#[test]
fn test_apply_update_existing() {
let initial = format!(
"# Header\n\n{}\nOld instructions here\n{}\n\n# Footer\n",
SCRYER_START_SENTINEL, SCRYER_END_SENTINEL
);
let (content, outcome) = apply_instructions_to_content(&initial).unwrap();
assert_eq!(outcome, InstallOutcome::Updated);
assert!(content.starts_with("# Header\n\n"));
assert!(content.ends_with("\n\n# Footer\n"));
assert!(content.contains("get_file_outline"));
assert!(!content.contains("Old instructions here"));
}
#[test]
fn test_apply_idempotent() {
let initial = "# Header\n\n";
let (first, _) = apply_instructions_to_content(initial).unwrap();
let (second, outcome) = apply_instructions_to_content(&first).unwrap();
assert_eq!(outcome, InstallOutcome::AlreadyUpToDate);
assert_eq!(first, second);
}
}