use serde_json::{json, Value};
use super::ToolDefinition;
fn read_only(title: &str) -> Value {
json!({
"readOnlyHint": true,
"title": title
})
}
fn def(name: &str, title: &str, description: &str, input_schema: Value) -> ToolDefinition {
ToolDefinition {
name: name.to_string(),
description: description.to_string(),
input_schema,
annotations: Some(read_only(title)),
meta: None,
}
}
fn def_always_load(
name: &str,
title: &str,
description: &str,
input_schema: Value,
) -> ToolDefinition {
ToolDefinition {
name: name.to_string(),
description: description.to_string(),
input_schema,
annotations: Some(read_only(title)),
meta: Some(json!({ "anthropic/alwaysLoad": true })),
}
}
pub fn explore_call_budget(total_nodes: u64) -> u8 {
match total_nodes {
0..=5_000 => 3,
5_001..=20_000 => 4,
20_001..=80_000 => 5,
80_001..=250_000 => 7,
_ => 10,
}
}
pub fn context_description(node_count: u64, budget: u8) -> String {
format!(
"Build an AI-ready context for a task description. Returns relevant symbols, \
relationships, and optionally code snippets.\n\n\
CALL BUDGET: {budget} calls maximum for this project ({node_count} nodes). \
Stop after {budget} calls. If the question is not fully answered, synthesise \
from what you have — do not exceed the budget."
)
}
pub fn get_tool_definitions_with_budget(node_count: u64, budget: u8) -> Vec<ToolDefinition> {
let mut defs = get_tool_definitions();
for def in &mut defs {
if def.name == "tokensave_context" {
def.description = context_description(node_count, budget);
}
}
defs
}
pub fn get_tool_definitions() -> Vec<ToolDefinition> {
let mut definitions = vec![
def_search(),
def_context(),
def_callers(),
def_callees(),
def_impact(),
def_node(),
def_status(),
def_files(),
def_affected(),
def_dead_code(),
def_diff_context(),
def_module_api(),
def_circular(),
def_hotspots(),
def_similar(),
def_rename_preview(),
def_unused_imports(),
def_rank(),
def_largest(),
def_coupling(),
def_inheritance_depth(),
def_distribution(),
def_recursion(),
def_complexity(),
def_doc_coverage(),
def_god_class(),
def_changelog(),
def_port_status(),
def_port_order(),
def_commit_context(),
def_pr_context(),
def_simplify_scan(),
def_test_map(),
def_type_hierarchy(),
def_branch_search(),
def_branch_diff(),
def_branch_list(),
def_str_replace(),
def_multi_str_replace(),
def_insert_at(),
def_ast_grep_rewrite(),
def_gini(),
def_dependency_depth(),
def_health(),
def_dsm(),
def_test_risk(),
def_session_start(),
def_session_end(),
def_body(),
def_todos(),
def_callers_for(),
def_by_qualified_name(),
def_signature(),
def_impls(),
def_diagnose(),
def_derives(),
def_run_affected_tests(),
def_record_decision(),
def_record_code_area(),
def_session_recall(),
def_read(),
def_outline(),
def_implementations(),
def_unsafe_patterns(),
def_diagnostics(),
def_config(),
def_signature_search(),
def_constructors(),
def_field_sites(),
];
if !ast_grep_available() {
definitions.retain(|d| d.name != "tokensave_ast_grep_rewrite");
}
debug_assert!(
!definitions.is_empty(),
"get_tool_definitions returned empty list"
);
debug_assert!(
definitions.iter().all(|d| d.name.starts_with("tokensave_")),
"all tool definitions must have 'tokensave_' prefix"
);
definitions
}
pub fn ast_grep_available() -> bool {
use std::sync::OnceLock;
static AVAILABLE: OnceLock<bool> = OnceLock::new();
*AVAILABLE.get_or_init(|| {
std::process::Command::new("ast-grep")
.arg("--version")
.output()
.is_ok_and(|out| out.status.success())
})
}
fn def_search() -> ToolDefinition {
def_always_load(
"tokensave_search",
"Search Symbols",
"Search for symbols (functions, structs, traits, etc.) in the code graph by name or keyword.",
json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query string to match against symbol names"
},
"limit": {
"type": "number",
"description": "Maximum number of results to return (default: 10)"
}
},
"required": ["query"]
}),
)
}
fn def_context() -> ToolDefinition {
def_always_load(
"tokensave_context",
"Task Context",
&context_description(0, 3),
json!({
"type": "object",
"properties": {
"task": {
"type": "string",
"description": "Natural language description of the task or question"
},
"max_nodes": {
"type": "number",
"description": "Maximum number of symbols to include (default: 20)"
},
"include_code": {
"type": "boolean",
"description": "If true, include source code snippets for key symbols (default: false)"
},
"max_code_blocks": {
"type": "number",
"description": "Maximum number of code snippets when include_code is true (default: 5)"
},
"mode": {
"type": "string",
"enum": ["explore", "plan"],
"description": "Context mode: 'explore' (default) for general exploration, 'plan' for implementation planning (adds extension points, dependency order, test coverage)"
},
"keywords": {
"type": "array",
"items": { "type": "string" },
"description": "Extra search keywords for synonym expansion. Use this when the task uses conceptual terms that may not match symbol names — e.g. for 'authentication', pass [\"login\", \"session\", \"credential\", \"token\", \"auth\"]. The graph is searched for each keyword independently."
},
"exclude_node_ids": {
"type": "array",
"items": { "type": "string" },
"description": "Node IDs to exclude from results (pass seen_node_ids from previous call for session deduplication)"
},
"merge_adjacent": {
"type": "boolean",
"description": "When true, merge code blocks from the same file whose line ranges are adjacent or overlapping (default: false)"
},
"max_per_file": {
"type": "number",
"description": "Maximum symbols from a single file in results. Prevents one large file from dominating (default: max_nodes/3, minimum 3)"
}
},
"required": ["task"]
}),
)
}
fn def_status() -> ToolDefinition {
def_always_load(
"tokensave_status",
"Graph Status",
"Return aggregate statistics about the code graph (node/edge/file counts, DB size, etc.).",
json!({
"type": "object",
"properties": {}
}),
)
}
fn def_callers_for() -> ToolDefinition {
def(
"tokensave_callers_for",
"Bulk callers",
"Returns the caller set of every supplied node ID in one round-trip. \
Useful for clustering or similarity queries that need many caller \
sets at once. Returns a map of {node_id: [caller_id, …]}. Defaults \
to `calls` edges; pass `kind` to filter by `uses`, `type_of`, etc.",
json!({
"type": "object",
"properties": {
"node_ids": {
"type": "array",
"items": { "type": "string" },
"description": "Node IDs to look up callers for."
},
"kind": {
"type": "string",
"description": "Edge kind to filter by (default: \"calls\"). Pass an empty string to match all kinds."
},
"max_per_item": {
"type": "number",
"description": "Cap callers per item (default: 1000)."
}
},
"required": ["node_ids"]
}),
)
}
fn def_by_qualified_name() -> ToolDefinition {
def(
"tokensave_by_qualified_name",
"Lookup by qualified name",
"Look up nodes by their qualified name. Multiple rows can share a \
qualified name (overloads, generics, separate impl blocks). Useful \
for cross-run lookups where the content-hash node ID has changed.",
json!({
"type": "object",
"properties": {
"qualified_name": {
"type": "string",
"description": "The exact qualified name to look up."
}
},
"required": ["qualified_name"]
}),
)
}
fn def_impls() -> ToolDefinition {
def(
"tokensave_impls",
"Trait Implementations",
"List `impl` blocks matching a trait, a type, or both. With no filter \
returns every impl in the graph (use sparingly). Both arguments \
accept short names (e.g. `Display`) or qualified names. Surfaces \
information that is otherwise hard to query: trait-method dispatch \
targets, which types satisfy a given trait, and which traits a type \
implements.",
json!({
"type": "object",
"properties": {
"trait": {
"type": "string",
"description": "Trait name to filter by (short or qualified). Omit to include all traits."
},
"type": {
"type": "string",
"description": "Implementing type to filter by (short or qualified). Omit to include all types."
},
"limit": {
"type": "number",
"description": "Maximum number of results to return (default: 100)."
}
}
}),
)
}
fn def_signature() -> ToolDefinition {
def(
"tokensave_signature",
"Signature",
"Return the signature-level metadata for symbols matching a qualified \
name — visibility, signature string (generics, params, return type, \
where clauses), docstring, async flag, and kind. No bodies. Use this \
instead of reading source files when you only need the public-API \
surface of a function, method, or type. Multiple rows can be \
returned (overloads, separate impls).",
json!({
"type": "object",
"properties": {
"qualified_name": {
"type": "string",
"description": "The exact qualified name to look up."
},
"node_id": {
"type": "string",
"description": "Optional: look up a single node by its ID instead of qualified_name."
}
}
}),
)
}
fn def_callers() -> ToolDefinition {
def(
"tokensave_callers",
"Callers",
"Find all callers of a given node (function, method, etc.) up to a specified depth.",
json!({
"type": "object",
"properties": {
"node_id": {
"type": "string",
"description": "The unique node ID to find callers for"
},
"max_depth": {
"type": "number",
"description": "Maximum traversal depth (default: 3)"
}
},
"required": ["node_id"]
}),
)
}
fn def_callees() -> ToolDefinition {
def(
"tokensave_callees",
"Callees",
"Find all callees of a given node (function, method, etc.) up to a \
specified depth. When a callee resolves to a trait method, the \
concrete impl methods reachable through that trait are also \
returned, tagged with `dispatch_via_trait: true` and a `dispatch_from` \
pointing at the trait method. Pass `resolve_dispatch: false` to \
disable this behaviour and get only direct call edges.",
json!({
"type": "object",
"properties": {
"node_id": {
"type": "string",
"description": "The unique node ID to find callees for"
},
"max_depth": {
"type": "number",
"description": "Maximum traversal depth (default: 3)"
},
"resolve_dispatch": {
"type": "boolean",
"description": "If true (default), append concrete impl methods for any trait-method callee."
}
},
"required": ["node_id"]
}),
)
}
fn def_impact() -> ToolDefinition {
def(
"tokensave_impact",
"Impact Radius",
"Compute the impact radius of a node: all symbols that directly or indirectly depend on it.",
json!({
"type": "object",
"properties": {
"node_id": {
"type": "string",
"description": "The unique node ID to compute impact for"
},
"max_depth": {
"type": "number",
"description": "Maximum traversal depth (default: 3)"
}
},
"required": ["node_id"]
}),
)
}
fn def_node() -> ToolDefinition {
def(
"tokensave_node",
"Node Details",
"Retrieve detailed information about a single node by its ID.",
json!({
"type": "object",
"properties": {
"node_id": {
"type": "string",
"description": "The unique node ID to retrieve"
}
},
"required": ["node_id"]
}),
)
}
fn def_files() -> ToolDefinition {
def(
"tokensave_files",
"File List",
"List indexed project files. Use to explore file structure without reading file contents.",
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Filter to files under this directory path"
},
"pattern": {
"type": "string",
"description": "Filter files matching this glob pattern (e.g. '**/*.rs')"
},
"format": {
"type": "string",
"enum": ["flat", "grouped"],
"description": "Output format: flat (one per line) or grouped by directory (default: grouped)"
}
}
}),
)
}
fn def_affected() -> ToolDefinition {
def(
"tokensave_affected",
"Affected Tests",
"Find test files affected by changed source files via dependency graph traversal.",
json!({
"type": "object",
"properties": {
"files": {
"type": "array",
"items": { "type": "string" },
"description": "List of changed file paths to analyze"
},
"depth": {
"type": "number",
"description": "Maximum dependency traversal depth (default: 5)"
},
"filter": {
"type": "string",
"description": "Custom glob pattern for test files (default: common test patterns)"
}
},
"required": ["files"]
}),
)
}
fn def_dead_code() -> ToolDefinition {
def(
"tokensave_dead_code",
"Dead Code",
"Find symbols with no incoming edges (potentially unreachable code). \
Always excludes `main` and `test*` functions. By default also excludes \
`pub` items (they may be referenced outside the indexed scope) — pass \
`include_public: true` to audit pub items with zero indexed callers, \
which is what you want for workspace-internal cleanup.",
json!({
"type": "object",
"properties": {
"kinds": {
"type": "array",
"items": { "type": "string" },
"description": "Node kinds to check (default: [\"function\", \"method\"])"
},
"include_public": {
"type": "boolean",
"description": "When true, do NOT exclude pub items. Default false."
}
}
}),
)
}
fn def_diff_context() -> ToolDefinition {
def(
"tokensave_diff_context",
"Diff Context",
"Given changed file paths, return semantic context: which symbols were modified, what depends on them, and affected tests.",
json!({
"type": "object",
"properties": {
"files": {
"type": "array",
"items": { "type": "string" },
"description": "List of changed file paths"
},
"depth": {
"type": "number",
"description": "Maximum impact traversal depth (default: 2)"
}
},
"required": ["files"]
}),
)
}
fn def_module_api() -> ToolDefinition {
def(
"tokensave_module_api",
"Module API",
"Show the public API surface of a file or directory: all pub symbols sorted by file and line.",
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "File path or directory prefix to inspect"
}
},
"required": ["path"]
}),
)
}
fn def_circular() -> ToolDefinition {
def(
"tokensave_circular",
"Circular Deps",
"Detect circular dependencies between files in the code graph.",
json!({
"type": "object",
"properties": {
"max_depth": {
"type": "number",
"description": "Maximum cycle detection depth (default: 10)"
}
}
}),
)
}
fn def_hotspots() -> ToolDefinition {
def(
"tokensave_hotspots",
"Hotspots",
"Find symbols with the highest connectivity (most incoming + outgoing edges).",
json!({
"type": "object",
"properties": {
"limit": {
"type": "number",
"description": "Maximum number of hotspots to return (default: 10)"
}
}
}),
)
}
fn def_similar() -> ToolDefinition {
def(
"tokensave_similar",
"Similar Symbols",
"Find symbols with similar names using full-text search and substring matching.",
json!({
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Symbol name to find similar matches for"
},
"limit": {
"type": "number",
"description": "Maximum number of results (default: 10)"
}
},
"required": ["symbol"]
}),
)
}
fn def_rename_preview() -> ToolDefinition {
def(
"tokensave_rename_preview",
"References",
"Show all references to a symbol -- all edges where the node appears as source or target.",
json!({
"type": "object",
"properties": {
"node_id": {
"type": "string",
"description": "The unique node ID to find references for"
}
},
"required": ["node_id"]
}),
)
}
fn def_unused_imports() -> ToolDefinition {
def(
"tokensave_unused_imports",
"Unused Imports",
"Find import/use nodes that are never referenced by any other node.",
json!({
"type": "object",
"properties": {}
}),
)
}
fn def_rank() -> ToolDefinition {
def(
"tokensave_rank",
"Rank",
"Rank nodes by edge count for a given relationship type (calls, implements, extends, etc.).",
json!({
"type": "object",
"properties": {
"edge_kind": {
"type": "string",
"enum": ["implements", "extends", "calls", "uses", "contains", "annotates", "derives_macro"],
"description": "The relationship type to rank by (e.g. 'implements' to find most-implemented interfaces)"
},
"direction": {
"type": "string",
"enum": ["incoming", "outgoing"],
"description": "Edge direction: 'incoming' ranks targets (default, e.g. most-implemented interface), 'outgoing' ranks sources (e.g. class that implements the most interfaces)"
},
"node_kind": {
"type": "string",
"description": "Optional filter for node kind (e.g. 'interface', 'class', 'trait', 'function', 'method')"
},
"path": {
"type": "string",
"description": "Filter to files under this directory path (e.g. 'src/main/java')"
},
"limit": {
"type": "number",
"description": "Maximum number of results to return (default: 10)"
}
},
"required": ["edge_kind"]
}),
)
}
fn def_largest() -> ToolDefinition {
def(
"tokensave_largest",
"Largest Symbols",
"Rank nodes by size (line count). Find the largest classes, longest methods, biggest enums, etc.",
json!({
"type": "object",
"properties": {
"node_kind": {
"type": "string",
"description": "Filter by node kind (e.g. 'class', 'method', 'function', 'interface', 'enum', 'struct')"
},
"path": {
"type": "string",
"description": "Filter to files under this directory path (e.g. 'src/main/java')"
},
"limit": {
"type": "number",
"description": "Maximum number of results to return (default: 10)"
}
}
}),
)
}
fn def_coupling() -> ToolDefinition {
def(
"tokensave_coupling",
"Coupling",
"Rank files by coupling: fan_in (most depended on) or fan_out (most dependencies).",
json!({
"type": "object",
"properties": {
"direction": {
"type": "string",
"enum": ["fan_in", "fan_out"],
"description": "fan_in: files depended on by the most others. fan_out: files that depend on the most others (default: fan_in)"
},
"path": {
"type": "string",
"description": "Filter to files under this directory path (e.g. 'src/main/java')"
},
"limit": {
"type": "number",
"description": "Maximum number of results to return (default: 10)"
}
}
}),
)
}
fn def_inheritance_depth() -> ToolDefinition {
def(
"tokensave_inheritance_depth",
"Inheritance Depth",
"Find the deepest class/interface inheritance hierarchies by walking extends chains.",
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Filter to files under this directory path (e.g. 'src/main/java')"
},
"limit": {
"type": "number",
"description": "Maximum number of results to return (default: 10)"
}
}
}),
)
}
fn def_distribution() -> ToolDefinition {
def(
"tokensave_distribution",
"Distribution",
"Show node kind distribution (classes, methods, fields, etc.) per file or directory.",
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Directory or file path prefix to filter (e.g. 'src/main/java/com/example'). Omit for entire codebase."
},
"summary": {
"type": "boolean",
"description": "If true, aggregate counts across all matching files instead of per-file breakdown (default: false)"
}
}
}),
)
}
fn def_recursion() -> ToolDefinition {
def(
"tokensave_recursion",
"Recursion",
"Detect recursive and mutually-recursive call cycles in the call graph.",
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Filter to files under this directory path (e.g. 'src/main/java')"
},
"limit": {
"type": "number",
"description": "Maximum number of cycles to return (default: 10)"
}
}
}),
)
}
fn def_complexity() -> ToolDefinition {
def(
"tokensave_complexity",
"Complexity",
"Rank functions/methods by composite complexity score (lines + fan-out + fan-in).",
json!({
"type": "object",
"properties": {
"node_kind": {
"type": "string",
"description": "Filter by node kind (default: function and method)"
},
"path": {
"type": "string",
"description": "Filter to files under this directory path (e.g. 'src/main/java')"
},
"limit": {
"type": "number",
"description": "Maximum number of results to return (default: 10)"
}
}
}),
)
}
fn def_doc_coverage() -> ToolDefinition {
def(
"tokensave_doc_coverage",
"Doc Coverage",
"Find public symbols missing documentation (docstrings).",
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Directory or file path prefix to filter (e.g. 'src/main'). Omit for entire codebase."
},
"limit": {
"type": "number",
"description": "Maximum number of results to return (default: 50)"
}
}
}),
)
}
fn def_god_class() -> ToolDefinition {
def(
"tokensave_god_class",
"God Classes",
"Find classes with the most members (methods + fields).",
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Filter to files under this directory path (e.g. 'src/main/java')"
},
"limit": {
"type": "number",
"description": "Maximum number of results to return (default: 10)"
}
}
}),
)
}
fn def_changelog() -> ToolDefinition {
def(
"tokensave_changelog",
"Changelog",
"Generate a semantic diff/changelog between two git refs, categorizing symbols as added, removed, or modified.",
json!({
"type": "object",
"properties": {
"from_ref": {
"type": "string",
"description": "Starting git ref (commit, branch, tag)"
},
"to_ref": {
"type": "string",
"description": "Ending git ref (commit, branch, tag)"
}
},
"required": ["from_ref", "to_ref"]
}),
)
}
fn def_port_status() -> ToolDefinition {
def(
"tokensave_port_status",
"Port Status",
"Compare symbols between source and target directories to track porting progress.",
json!({
"type": "object",
"properties": {
"source_dir": {
"type": "string",
"description": "Path prefix for source code (e.g. 'src/python/')"
},
"target_dir": {
"type": "string",
"description": "Path prefix for target code (e.g. 'src/rust/')"
},
"kinds": {
"type": "array",
"items": { "type": "string" },
"description": "Node kinds to compare (default: [\"function\", \"method\", \"class\", \"struct\", \"interface\", \"trait\", \"enum\", \"module\"])"
}
},
"required": ["source_dir", "target_dir"]
}),
)
}
fn def_port_order() -> ToolDefinition {
def(
"tokensave_port_order",
"Port Order",
"Topological sort of symbols in a directory -- port leaves first, dependents after.",
json!({
"type": "object",
"properties": {
"source_dir": {
"type": "string",
"description": "Path prefix for source code (e.g. 'src/python/')"
},
"kinds": {
"type": "array",
"items": { "type": "string" },
"description": "Node kinds to include (default: [\"function\", \"method\", \"class\", \"struct\", \"interface\", \"trait\", \"enum\", \"module\"])"
},
"limit": {
"type": "number",
"description": "Maximum number of symbols to return (default: 50)"
}
},
"required": ["source_dir"]
}),
)
}
fn def_commit_context() -> ToolDefinition {
def(
"tokensave_commit_context",
"Commit Context",
"Semantic summary of uncommitted changes for drafting a commit message. Returns changed symbols, file roles, and recent commit style.",
json!({
"type": "object",
"properties": {
"staged_only": {
"type": "boolean",
"description": "If true, only analyze staged changes (default: false = all uncommitted changes)"
}
}
}),
)
}
fn def_pr_context() -> ToolDefinition {
def(
"tokensave_pr_context",
"PR Context",
"Semantic summary of changes between two git refs for drafting a pull request description.",
json!({
"type": "object",
"properties": {
"base_ref": {
"type": "string",
"description": "Base branch or ref to compare against (default: 'main')"
},
"head_ref": {
"type": "string",
"description": "Head branch or ref (default: 'HEAD')"
}
}
}),
)
}
fn def_simplify_scan() -> ToolDefinition {
def(
"tokensave_simplify_scan",
"Simplify Scan",
"Quality analysis of changed files: duplications, dead code, coupling, and complexity hotspots.",
json!({
"type": "object",
"properties": {
"files": {
"type": "array",
"items": { "type": "string" },
"description": "Changed file paths to analyze"
}
},
"required": ["files"]
}),
)
}
fn def_test_map() -> ToolDefinition {
def(
"tokensave_test_map",
"Test Map",
"Map source symbols to their test functions. Shows which tests cover which source code.",
json!({
"type": "object",
"properties": {
"file": {
"type": "string",
"description": "Source file path to find test coverage for"
},
"node_id": {
"type": "string",
"description": "Specific node ID to find test coverage for (alternative to file)"
}
}
}),
)
}
fn def_type_hierarchy() -> ToolDefinition {
def(
"tokensave_type_hierarchy",
"Type Hierarchy",
"Show the full type hierarchy for a trait/interface/class: all implementors and extenders, recursively.",
json!({
"type": "object",
"properties": {
"node_id": {
"type": "string",
"description": "The type node ID to build the hierarchy for"
},
"max_depth": {
"type": "number",
"description": "Maximum inheritance depth to traverse (default: 5)"
}
},
"required": ["node_id"]
}),
)
}
fn def_branch_search() -> ToolDefinition {
def(
"tokensave_branch_search",
"Cross-Branch Search",
"Search for symbols in another branch's code graph. Opens the target branch's DB and runs a search query against it.",
json!({
"type": "object",
"properties": {
"branch": {
"type": "string",
"description": "Branch name to search in (must be tracked via `tokensave branch add`)"
},
"query": {
"type": "string",
"description": "Search query string to match against symbol names"
},
"limit": {
"type": "number",
"description": "Maximum number of results to return (default: 10)"
}
},
"required": ["branch", "query"]
}),
)
}
fn def_branch_diff() -> ToolDefinition {
def(
"tokensave_branch_diff",
"Branch Diff",
"Compare the code graphs of two branches. Shows symbols added, removed, and changed (signature differs) between base and head.",
json!({
"type": "object",
"properties": {
"base": {
"type": "string",
"description": "Base branch name (e.g. 'main'). Defaults to the project's default branch."
},
"head": {
"type": "string",
"description": "Head branch name (e.g. 'feature/foo'). Defaults to the current branch."
},
"file": {
"type": "string",
"description": "Optional file path filter — only show diffs for symbols in this file"
},
"kind": {
"type": "string",
"description": "Optional kind filter — only show diffs for this symbol kind (e.g. 'function', 'struct')"
}
}
}),
)
}
fn def_branch_list() -> ToolDefinition {
def(
"tokensave_branch_list",
"List Tracked Branches",
"List all tracked branches with their DB sizes, parent branch, and last sync time. Returns an empty list if multi-branch is not active.",
json!({
"type": "object",
"properties": {}
}),
)
}
fn def_str_replace() -> ToolDefinition {
ToolDefinition {
name: "tokensave_str_replace".to_string(),
description: "Replace a unique string in a file with new content. Fails if the old string is not found or matches more than once. This is the safest edit primitive — use this instead of sed/awk.".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Absolute or project-relative file path"
},
"old_str": {
"type": "string",
"description": "Exact string to find and replace. Must match exactly once in the file."
},
"new_str": {
"type": "string",
"description": "Replacement string"
}
},
"required": ["path", "old_str", "new_str"]
}),
annotations: Some(json!({
"readOnlyHint": false,
"title": "Edit File"
})),
meta: None,
}
}
fn def_multi_str_replace() -> ToolDefinition {
ToolDefinition {
name: "tokensave_multi_str_replace".to_string(),
description: "Apply multiple string replacements atomically in a single file. All replacements must match exactly once. If any replacement fails (0 or >1 matches), the entire operation is aborted and no changes are made.".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Absolute or project-relative file path"
},
"replacements": {
"type": "array",
"description": "Array of [old_str, new_str] pairs to replace",
"items": {
"type": "array",
"items": {"type": "string"},
"minItems": 2,
"maxItems": 2
}
}
},
"required": ["path", "replacements"]
}),
annotations: Some(json!({
"readOnlyHint": false,
"title": "Multi-Edit File"
})),
meta: None,
}
}
fn def_insert_at() -> ToolDefinition {
ToolDefinition {
name: "tokensave_insert_at".to_string(),
description: "Insert content before or after a unique anchor in a file. The anchor can be a unique string or a 1-indexed line number. Fails if the anchor matches more than one line.".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Absolute or project-relative file path"
},
"anchor": {
"type": "string",
"description": "Unique string or line number (1-indexed) to insert at"
},
"content": {
"type": "string",
"description": "Content to insert"
},
"before": {
"type": "boolean",
"description": "If true, insert before the anchor line; if false, insert after (default: false)"
}
},
"required": ["path", "anchor", "content"]
}),
annotations: Some(json!({
"readOnlyHint": false,
"title": "Insert Into File"
})),
meta: None,
}
}
fn def_gini() -> ToolDefinition {
def(
"tokensave_gini",
"Gini Inequality",
"Compute inequality (Gini coefficient) for any metric across files or symbols. Detects god files and uneven complexity distribution.",
json!({
"type": "object",
"properties": {
"metric": {
"type": "string",
"enum": ["complexity", "lines", "fan_in", "fan_out", "members"],
"description": "Metric to measure inequality for (default: complexity)"
},
"scope": {
"type": "string",
"enum": ["file", "symbol"],
"description": "Aggregate per file or per symbol (default: file)"
},
"path": {
"type": "string",
"description": "Filter to files under this directory path"
},
"limit": {
"type": "number",
"description": "Number of top outliers to return (default: 10)"
}
}
}),
)
}
fn def_dependency_depth() -> ToolDefinition {
def(
"tokensave_dependency_depth",
"Dependency Depth",
"Show the longest file-level dependency chains. Files at the end of long chains are fragile to upstream changes.",
json!({
"type": "object",
"properties": {
"limit": {
"type": "number",
"description": "Maximum number of chains to return (default: 10)"
},
"path": {
"type": "string",
"description": "Filter to files under this directory path"
}
}
}),
)
}
fn def_health() -> ToolDefinition {
def(
"tokensave_health",
"Health Score",
"Get quality signal (0-10000) with root cause breakdown (acyclicity, depth, equality, redundancy, modularity). Quality signal = geometric mean of 5 dimensions — maximize this ONE number.",
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Filter to files under this directory path"
},
"details": {
"type": "boolean",
"description": "If true, include full dimension breakdown (default: false)"
}
}
}),
)
}
fn def_dsm() -> ToolDefinition {
def(
"tokensave_dsm",
"Design Structure Matrix",
"Get the Design Structure Matrix: file dependency summary showing clusters, density, and layering violations.",
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Filter to files under this directory path"
},
"format": {
"type": "string",
"enum": ["stats", "clusters", "matrix"],
"description": "Output format (default: stats)"
},
"max_files": {
"type": "number",
"description": "Maximum files in matrix format (default: 30)"
}
}
}),
)
}
fn def_test_risk() -> ToolDefinition {
def(
"tokensave_test_risk",
"Test Risk",
"Find high-risk source symbols with weak or no test coverage. Risk = (complexity + 1) × (fan_in + 1) × untested_multiplier. Answers: where should the next test go?",
json!({
"type": "object",
"properties": {
"limit": {
"type": "number",
"description": "Maximum number of results to return (default: 20)"
},
"path": {
"type": "string",
"description": "Filter to files under this directory path"
},
"include_tested": {
"type": "boolean",
"description": "Include already-tested functions in results (default: false)"
}
}
}),
)
}
fn def_derives() -> ToolDefinition {
def(
"tokensave_derives",
"Derives on Type",
"List `#[derive(...)]` macros attached to a type and the trait + \
method names each one synthesizes. Prevents dead-end searches for \
autogenerated symbols (e.g. `.clone()` from `#[derive(Clone)]`). \
Well-known derives (`Debug`, `Clone`, `Copy`, `Default`, `PartialEq`, \
`Eq`, `PartialOrd`, `Ord`, `Hash`, `Serialize`, `Deserialize`, \
`Display`, `Error`) carry full trait + method info; unknown / \
proc-macro derives surface with `well_known: false` so callers can \
still see the derive name.",
json!({
"type": "object",
"properties": {
"qualified_name": {
"type": "string",
"description": "The type's qualified name (or short name — same lookup as tokensave_by_qualified_name)."
},
"node_id": {
"type": "string",
"description": "Optional: look up the type by node ID instead."
}
}
}),
)
}
fn def_diagnose() -> ToolDefinition {
def(
"tokensave_diagnose",
"Diagnose Cargo Output",
"Parse raw `cargo check` / `cargo clippy` stderr text and map each \
diagnostic to the smallest containing graph node, with callers \
pre-attached so you can see what the failing code is reachable \
from. Diagnostics without a `--> file:line:col` span are dropped. \
Pass the full stderr capture; you do not need to pre-filter.",
json!({
"type": "object",
"properties": {
"cargo_output": {
"type": "string",
"description": "Raw stderr text from `cargo check` / `cargo clippy` / `rustc`."
},
"severity": {
"type": "string",
"enum": ["error", "warning", "all"],
"description": "Filter by severity (default: all)."
},
"include_callers": {
"type": "boolean",
"description": "Attach up to 5 callers per diagnostic (default: true)."
},
"max_diagnostics": {
"type": "number",
"description": "Cap on diagnostics in the response (default: 50)."
}
},
"required": ["cargo_output"]
}),
)
}
fn def_run_affected_tests() -> ToolDefinition {
def(
"tokensave_run_affected_tests",
"Run Affected Tests",
"Run `cargo test` for tests that cover the symbols in `changed_paths` \
(or, if omitted, the files changed in the working tree). Closes the \
loop opened by `tokensave_test_map` / `tokensave_test_risk` — emits \
pass/fail per test alongside the source nodes each test covers. \
Output is the libtest summary parsed into JSON.",
json!({
"type": "object",
"properties": {
"changed_paths": {
"type": "array",
"items": {"type": "string"},
"description": "Explicit file paths to compute affected tests from. Defaults to `git diff --name-only` against the working tree."
},
"profile": {
"type": "string",
"enum": ["debug", "release"],
"description": "Cargo profile (default: debug)."
},
"timeout_secs": {
"type": "number",
"description": "Maximum wall time before the cargo subprocess is killed (default: 300)."
},
"max_tests": {
"type": "number",
"description": "Cap on tests dispatched in a single invocation (default: 100)."
}
}
}),
)
}
fn def_ast_grep_rewrite() -> ToolDefinition {
ToolDefinition {
name: "tokensave_ast_grep_rewrite".to_string(),
description: "Perform structural code rewrite using ast-grep. The pattern and rewrite use ast-grep's SGPattern syntax. Fails if ast-grep is not installed.".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Absolute or project-relative file path"
},
"pattern": {
"type": "string",
"description": "ast-grep search pattern (SGPattern syntax)"
},
"rewrite": {
"type": "string",
"description": "ast-grep rewrite rule"
}
},
"required": ["path", "pattern", "rewrite"]
}),
annotations: Some(json!({
"readOnlyHint": false,
"title": "AST Structural Rewrite"
})),
meta: None,
}
}
fn def_session_start() -> ToolDefinition {
ToolDefinition {
name: "tokensave_session_start".to_string(),
description: "Save current health metrics as baseline for later comparison via session_end. Call this before starting work.".to_string(),
input_schema: json!({
"type": "object",
"properties": {}
}),
annotations: Some(json!({
"readOnlyHint": false,
"title": "Session Start"
})),
meta: None,
}
}
fn def_session_end() -> ToolDefinition {
def(
"tokensave_session_end",
"Session End",
"Re-scan and compare current health against session baseline (saved by session_start). Returns diff showing what improved or degraded.",
json!({
"type": "object",
"properties": {}
}),
)
}
fn def_body() -> ToolDefinition {
def(
"tokensave_body",
"Symbol Body",
"Return the full source body of a symbol by name (function, struct, const, etc.). \
Collapses search + node lookup + file read into a single call. \
When the name is ambiguous, returns multiple matches ranked by relevance.",
json!({
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Symbol name to look up (e.g. 'resolve_provider_api_key', 'CCH_SEED', 'GraphStats'). Qualified names are also accepted."
},
"limit": {
"type": "number",
"description": "Maximum number of matching bodies to return when the name is ambiguous (default: 3, max: 20)"
}
},
"required": ["symbol"]
}),
)
}
fn def_todos() -> ToolDefinition {
def(
"tokensave_todos",
"TODOs and FIXMEs",
"Find TODO, FIXME, XXX, HACK, WIP, NOTE, and unimplemented markers across the project. \
Each result includes the marker kind, file, line, the comment text, and the enclosing \
symbol name (function/method) for quick orientation.",
json!({
"type": "object",
"properties": {
"kinds": {
"type": "array",
"items": { "type": "string" },
"description": "Marker kinds to include (default: TODO, FIXME, XXX, HACK, WIP, NOTE, UNIMPLEMENTED). Matched case-insensitively."
},
"path": {
"type": "string",
"description": "Filter to files under this directory path (relative to project root)"
},
"limit": {
"type": "number",
"description": "Maximum number of markers to return (default: 200, max: 2000)"
}
}
}),
)
}
fn def_record_decision() -> ToolDefinition {
ToolDefinition {
name: "tokensave_record_decision".to_string(),
description: "Persist a design or architecture decision so it can be recalled in a future session via tokensave_session_recall. Use for choices the agent or user would otherwise have to re-explain (e.g. \"use JWT for auth — session tokens flagged by legal\"). Stored in the per-project DB.".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "The decision itself, in one sentence (e.g. \"use JWT for auth\")."
},
"reason": {
"type": "string",
"description": "Optional reason / context (e.g. \"session tokens flagged by legal\")."
},
"files": {
"type": "array",
"items": { "type": "string" },
"description": "File paths that the decision applies to."
},
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "Free-form tags for grouping (e.g. \"security\", \"performance\")."
}
},
"required": ["text"]
}),
annotations: Some(json!({
"readOnlyHint": false,
"title": "Record Decision"
})),
meta: None,
}
}
fn def_record_code_area() -> ToolDefinition {
ToolDefinition {
name: "tokensave_record_code_area".to_string(),
description: "Record that the agent has been working in a code area (a file or directory). The first call sets an optional description; subsequent calls bump the touch counter and update last_touched_at. Recall with tokensave_session_recall.".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "File or directory path (project-relative)."
},
"description": {
"type": "string",
"description": "Optional short description of what this area is or what was changed."
}
},
"required": ["path"]
}),
annotations: Some(json!({
"readOnlyHint": false,
"title": "Record Code Area"
})),
meta: None,
}
}
fn def_session_recall() -> ToolDefinition {
def(
"tokensave_session_recall",
"Session Recall",
"Recall persisted decisions (and optionally code areas) from past sessions. When `query` is provided, runs FTS5 search across decision text and reason. When omitted, returns the most recent decisions newest-first. Pair with tokensave_record_decision.",
json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "FTS5 query string (e.g. \"auth OR session\"). Omit for newest-first listing."
},
"since": {
"type": "number",
"description": "Unix timestamp; only return decisions made at-or-after this time."
},
"limit": {
"type": "number",
"description": "Maximum decisions to return (default: 20, max: 200)."
},
"include_code_areas": {
"type": "boolean",
"description": "If true, also return the top-touched code areas (default: false)."
}
}
}),
)
}
fn def_field_sites() -> ToolDefinition {
def(
"tokensave_field_sites",
"Field Read/Write Sites",
"Find every read and write site of a named field across the codebase. \
Returns two arrays: write_sites (assignments to the field) and \
read_sites (everything else). Each entry includes file, line, \
enclosing symbol, and a source snippet. Useful when renaming, \
removing, or adding an invariant to a field — the write-site list \
is the exact blast radius. Pattern matches `.<field>` references; \
field-by-name is shorthand for any struct's same-named field, while \
`Struct::field` form narrows to a specific declaration.",
json!({
"type": "object",
"properties": {
"field": {
"type": "string",
"description": "Field name. Bare name ('last_sync_at') matches across structs; qualified form ('GraphStats::last_sync_at') narrows to one struct's field."
},
"writes_only": {
"type": "boolean",
"description": "When true, returns only write_sites and omits reads. Default false."
},
"limit": {
"type": "number",
"description": "Maximum sites per kind (default: 200, max: 2000)."
}
},
"required": ["field"]
}),
)
}
fn def_constructors() -> ToolDefinition {
def(
"tokensave_constructors",
"Struct Literal Sites",
"Find every place a given struct is instantiated as a literal \
({ field: value, ... }). Each result includes the file, line, the \
field list present in that literal, and the set of fields missing \
relative to the struct's current definition (from the graph). The \
missing-fields list is the typical refactor signal: after adding a \
required field, this tool surfaces every site that needs updating, \
before cargo even compiles. Currently best-effort for Rust source; \
pattern matching ignores `match` arms and `if let` patterns.",
json!({
"type": "object",
"properties": {
"struct": {
"type": "string",
"description": "Struct name to search literal sites of (e.g. 'GraphStats', 'Config')."
},
"limit": {
"type": "number",
"description": "Maximum number of literal sites to return (default: 100, max: 1000)."
}
},
"required": ["struct"]
}),
)
}
fn def_signature_search() -> ToolDefinition {
def(
"tokensave_signature_search",
"Signature Search",
"Find functions and methods by signature shape: return type, parameter \
substring, async, or path. Searches the cached `signature` column on \
every Function/Method node. Substring-matched with case-sensitive \
compare; combine multiple criteria for narrower hits. Use \
tokensave_search for plain name lookups; this tool is for refactor \
questions like 'find every function returning Result<_, MyError>' or \
'every async fn taking &mut self'.",
json!({
"type": "object",
"properties": {
"returns": {
"type": "string",
"description": "Substring that must appear in the return-type portion of the signature (after '->'). E.g. 'Result<', 'impl Future', 'Vec<u32>'."
},
"params": {
"type": "array",
"items": { "type": "string" },
"description": "Substrings that must all appear in the parameter list portion of the signature. E.g. ['&mut self'], ['i32', 'String']."
},
"async": {
"type": "boolean",
"description": "When true, only return functions marked async. When false, exclude them. Omit to ignore async-ness."
},
"path": {
"type": "string",
"description": "Filter to symbols defined under this directory."
},
"limit": {
"type": "number",
"description": "Maximum matches to return (default: 50, max: 500)."
}
}
}),
)
}
fn def_config() -> ToolDefinition {
def(
"tokensave_config",
"Config File Query",
"Query TOML or JSON config files by dotted key path. Use 'path' for a \
single file (e.g. Cargo.toml, tsconfig.json, pyproject.toml) or 'glob' \
to query the same key across multiple files. The 'key' is dot-separated \
(e.g. 'package.version', 'dependencies.tokio'). Returns each match's \
file, parsed value, and the line where the key is defined. Format is \
detected from extension: .toml → TOML, .json → JSON. \
\n\nDoes not query the code graph — pure filesystem + parser. Works \
on uninitialized projects.",
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Project-relative path to a single config file (e.g. 'Cargo.toml'). Mutually exclusive with 'glob'."
},
"glob": {
"type": "string",
"description": "Glob pattern to match multiple config files (e.g. '**/Cargo.toml', 'crates/*/Cargo.toml'). Mutually exclusive with 'path'."
},
"key": {
"type": "string",
"description": "Dot-separated key path (e.g. 'package.version', 'dependencies.tokio.version'). Required."
}
},
"required": ["key"]
}),
)
}
fn def_diagnostics() -> ToolDefinition {
def(
"tokensave_diagnostics",
"Compile / Type-Check Diagnostics",
"Run the project's type-checker (cargo check for Rust, tsc for \
TypeScript, pyright for Python) and return structured errors and \
warnings. Each diagnostic includes file, line range, level, code, \
message, driver, and the enclosing graph node when one can be \
resolved. Replaces the recurring 'run cargo → parse text → read \
file' loop with a single structured response. \
\n\nNote: the cargo target dir is forced to .tokensave/target/ so \
we don't race with the user's interactive cargo runs. The first \
call against a fresh tree builds dependencies from scratch, which \
can take several minutes on large workspaces; subsequent calls \
are sub-second. Build scripts and proc macros from the project \
execute as part of cargo check — same trust model as running it \
manually.",
json!({
"type": "object",
"properties": {
"scope": {
"type": "string",
"enum": ["workspace", "package", "file"],
"description": "Run scope. Default 'workspace'. 'package' requires `name`; 'file' requires `path` and currently runs workspace + post-filter (cargo has no native single-file mode)."
},
"name": {
"type": "string",
"description": "Package name when scope='package' (e.g. 'tokensave', 'serde-json')."
},
"path": {
"type": "string",
"description": "Project-relative file path when scope='file'."
}
}
}),
)
}
fn def_unsafe_patterns() -> ToolDefinition {
def(
"tokensave_unsafe_patterns",
"Risky Pattern Finder",
"Find unwrap(), expect(), panic!(), todo!(), unimplemented!(), and unsafe \
{ } sites across the project. Each match includes the file, line, kind, \
enclosing symbol, the source line, and an in_test flag derived from the \
path. Use this in security/quality reviews to surface panic sites before \
a release. Defaults to all kinds; pass `kinds` to narrow.",
json!({
"type": "object",
"properties": {
"kinds": {
"type": "array",
"items": { "type": "string" },
"description": "Subset of patterns to search. Default: ['unwrap', 'expect', 'panic', 'todo', 'unimplemented', 'unsafe_block']."
},
"path": {
"type": "string",
"description": "Filter to files under this directory (relative to project root)."
},
"exclude_tests": {
"type": "boolean",
"description": "When true, skips files whose path looks like a test (default: false)."
},
"limit": {
"type": "number",
"description": "Maximum number of matches to return (default: 200, max: 2000)."
}
}
}),
)
}
fn def_implementations() -> ToolDefinition {
def(
"tokensave_implementations",
"Trait / Method Implementations",
"Find every type implementing a given trait, or every body of a given \
method name. The 'trait' form returns each implementing type plus the \
methods on its impl block. The 'method' form returns every function/ \
method named X across the project, grouped by enclosing type when \
present. Each result includes file, signature, and the method body.",
json!({
"type": "object",
"properties": {
"trait": {
"type": "string",
"description": "Trait name to look up implementations of (e.g. 'LanguageExtractor', 'Display'). Mutually exclusive with 'method'."
},
"method": {
"type": "string",
"description": "Method or function name to find every implementation of (e.g. 'extensions', 'count_complexity'). Mutually exclusive with 'trait'."
},
"limit": {
"type": "number",
"description": "Maximum number of implementations to return (default: 20, max: 200)"
}
}
}),
)
}
fn def_outline() -> ToolDefinition {
def(
"tokensave_outline",
"File Outline",
"Flat list of every top-level symbol defined in a file (functions, structs, \
enums, traits, classes, impls, etc.) — like a table of contents. Sorted by \
line number; no code bodies. Optional 'kinds' filter narrows to specific \
node kinds. Use this as the cheapest way to orient before zooming into a \
large file with tokensave_node, tokensave_body, or tokensave_read.",
json!({
"type": "object",
"properties": {
"file": {
"type": "string",
"description": "Project-relative path to the file (e.g. 'src/sync.rs')."
},
"kinds": {
"type": "array",
"items": { "type": "string" },
"description": "Optional filter on node kinds. Common values: 'function', 'struct', 'enum', 'trait', 'impl', 'class', 'method', 'const'. Case-insensitive. Default: all kinds."
}
},
"required": ["file"]
}),
)
}
fn def_read() -> ToolDefinition {
def(
"tokensave_read",
"Read File (mode-aware)",
"Read a file or its symbol map. Modes: 'full' (entire file), 'lines' \
(1-based inclusive byte-range slice via the 'lines' arg, e.g. '120-180'), \
'map' (flat list of every top-level symbol from the graph — no source \
bytes touched), 'signatures' (functions and types with their cached \
signature). Cross-session cached: a re-call on an unchanged file returns \
a tiny stub with 'unchanged: true'.",
json!({
"type": "object",
"properties": {
"file": {
"type": "string",
"description": "Project-relative or absolute path to the file (e.g. 'src/sync.rs')."
},
"mode": {
"type": "string",
"enum": ["full", "lines", "map", "signatures"],
"description": "Read mode. Default: 'full'."
},
"lines": {
"type": "string",
"description": "Required when mode='lines'. Format 'A-B' or single 'A' (1-based, inclusive). E.g. '120-180' or '42'."
}
},
"required": ["file"]
}),
)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::unreadable_literal)]
mod tests {
use super::*;
#[test]
fn test_explore_call_budget_tiers() {
assert_eq!(explore_call_budget(0), 3);
assert_eq!(explore_call_budget(5000), 3);
assert_eq!(explore_call_budget(5001), 4);
assert_eq!(explore_call_budget(20000), 4);
assert_eq!(explore_call_budget(20001), 5);
assert_eq!(explore_call_budget(80000), 5);
assert_eq!(explore_call_budget(80001), 7);
assert_eq!(explore_call_budget(250000), 7);
assert_eq!(explore_call_budget(250001), 10);
}
#[test]
fn test_context_description_contains_budget() {
let desc = context_description(5000, 4);
assert!(
desc.contains("4 calls maximum"),
"description should contain budget: {desc}"
);
assert!(
desc.contains("5000 nodes"),
"description should contain node count: {desc}"
);
}
#[test]
fn test_get_tool_definitions_with_budget() {
let defs = get_tool_definitions_with_budget(10000, 4);
let context_tool = defs.iter().find(|d| d.name == "tokensave_context").unwrap();
assert!(context_tool.description.contains("4 calls maximum"));
assert!(context_tool.description.contains("10000 nodes"));
}
}