Skip to content
Merged
27 changes: 17 additions & 10 deletions .codex/config.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
[mcp_servers.tokensave]
args = ["serve"]
args = [
"serve",
"--path",
".",
]
command = "/home/zack/projects/tokensave/target/debug/tokensave"

[mcp_servers.tokensave.tools.tokensave_affected]
Expand Down Expand Up @@ -83,6 +87,12 @@ approval_mode = "auto"
[mcp_servers.tokensave.tools.tokensave_dsm]
approval_mode = "auto"

[mcp_servers.tokensave.tools.tokensave_fact_feedback]
approval_mode = "auto"

[mcp_servers.tokensave.tools.tokensave_fact_store]
approval_mode = "auto"

[mcp_servers.tokensave.tools.tokensave_field_sites]
approval_mode = "auto"

Expand Down Expand Up @@ -128,6 +138,12 @@ approval_mode = "auto"
[mcp_servers.tokensave.tools.tokensave_largest]
approval_mode = "auto"

[mcp_servers.tokensave.tools.tokensave_memory_status]
approval_mode = "auto"

[mcp_servers.tokensave.tools.tokensave_message_search]
approval_mode = "auto"

[mcp_servers.tokensave.tools.tokensave_module_api]
approval_mode = "auto"

Expand Down Expand Up @@ -155,12 +171,6 @@ approval_mode = "auto"
[mcp_servers.tokensave.tools.tokensave_read]
approval_mode = "auto"

[mcp_servers.tokensave.tools.tokensave_record_code_area]
approval_mode = "auto"

[mcp_servers.tokensave.tools.tokensave_record_decision]
approval_mode = "auto"

[mcp_servers.tokensave.tools.tokensave_recursion]
approval_mode = "auto"

Expand All @@ -185,9 +195,6 @@ approval_mode = "auto"
[mcp_servers.tokensave.tools.tokensave_session_end]
approval_mode = "auto"

[mcp_servers.tokensave.tools.tokensave_session_recall]
approval_mode = "auto"

[mcp_servers.tokensave.tools.tokensave_session_start]
approval_mode = "auto"

Expand Down
6 changes: 1 addition & 5 deletions .cursor/mcp.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
{
"mcpServers": {
"tokensave": {
"args": [
"serve"
],
"command": "/home/zack/projects/tokensave/target/debug/tokensave",
"type": "stdio"
"command": "/home/zack/projects/tokensave/scripts/tokensave-dev-mcp.sh"
}
}
}
15 changes: 13 additions & 2 deletions .cursor/permissions.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
"tokensave:tokensave_impls",
"tokensave:tokensave_diagnose",
"tokensave:tokensave_derives",
"tokensave:tokensave_session_recall",
"tokensave:tokensave_read",
"tokensave:tokensave_outline",
"tokensave:tokensave_implementations",
Expand All @@ -64,6 +63,18 @@
"tokensave:tokensave_field_sites",
"tokensave:tokensave_call_chain",
"tokensave:tokensave_file_dependents",
"tokensave:tokensave_find_exact_symbol"
"tokensave:tokensave_find_exact_symbol",
"tokensave:tokensave_fact_store",
"tokensave:tokensave_fact_feedback",
"tokensave:tokensave_memory_status",
"tokensave:tokensave_str_replace",
"tokensave:tokensave_multi_str_replace",
"tokensave:tokensave_insert_at",
"tokensave:tokensave_session_start",
"tokensave:tokensave_session_end",
"tokensave:tokensave_run_affected_tests",
"tokensave:tokensave_replace_symbol",
"tokensave:tokensave_insert_at_symbol",
"tokensave:tokensave_message_search"
]
}
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## Learned User Preferences

- User prefers fresh, tool-backed verification for setup/configuration work and often asks agents to prove changes actually work.
- User wants repo-native project tooling used for codebase review, planning, and durable decision capture when available.
- User prefers local checkout tooling over global installs during active development, so tool behavior reflects the current branch.
- For unshipped PR branch work, replace in-progress designs directly rather than adding compatibility shims for old branch-only behavior.
- When the user asks to remember preferences or decisions, persist concise durable facts using the project memory system when available.

## Workspace Guidance

- Keep persistent guidance general and durable; avoid recording transient branch state, temporary schema numbers, or moment-in-time tool status here.
- Store detailed implementation decisions in the project memory system or PR docs instead of expanding this file with narrow session notes.

## Prefer tokensave MCP tools

Expand Down
17 changes: 17 additions & 0 deletions scripts/tokensave-dev-mcp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail

# Development MCP launcher for Cursor.
#
# Stdio MCP servers cannot hot-reload their tool definitions after Cursor has
# connected. This wrapper keeps development fast by running the current
# worktree source with cargo, so restarting/reconnecting the MCP server in
# Cursor picks up code changes without installing a new global binary.

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"

MANIFEST="$REPO_ROOT/Cargo.toml"
PROJECT_ROOT="${TOKENSAVE_DEV_PROJECT_ROOT:-$REPO_ROOT}"

exec cargo run --quiet --manifest-path "$MANIFEST" -- serve --path "$PROJECT_ROOT" "$@"
138 changes: 98 additions & 40 deletions src/agents/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::errors::{Result, TokenSaveError};
use super::{
backup_config_file, load_json_file_strict, load_toml_file, safe_write_json_file, tool_names,
write_toml_file, AgentIntegration, DoctorCounters, HealthcheckContext, InstallContext,
InstallScope,
};

/// `OpenAI` Codex CLI agent.
Expand All @@ -40,7 +41,7 @@ impl AgentIntegration for CodexIntegration {
std::fs::create_dir_all(&codex_dir).ok();
let config_path = codex_dir.join("config.toml");

install_mcp_server(&config_path, &ctx.tokensave_bin)?;
install_mcp_server(&config_path, &ctx.tokensave_bin, InstallScope::Global)?;

let agents_md = codex_dir.join("AGENTS.md");
install_prompt_rules(&agents_md)?;
Expand All @@ -61,8 +62,19 @@ impl AgentIntegration for CodexIntegration {

fn install_local(&self, ctx: &InstallContext, project_path: &Path) -> Result<()> {
let codex_dir = project_path.join(".codex");
for path in [
codex_dir.join("config.toml"),
codex_dir.join("hooks.json"),
project_path.join("AGENTS.md"),
] {
super::ensure_project_local_safe_path(project_path, &path)?;
}
std::fs::create_dir_all(&codex_dir).ok();
install_mcp_server(&codex_dir.join("config.toml"), &ctx.tokensave_bin)?;
install_mcp_server(
&codex_dir.join("config.toml"),
&ctx.tokensave_bin,
InstallScope::ProjectLocal,
)?;
install_prompt_rules(&project_path.join("AGENTS.md"))?;
install_hooks(&codex_dir.join("hooks.json"), &ctx.tokensave_bin)?;
print_hook_trust_guidance();
Expand All @@ -88,11 +100,21 @@ impl AgentIntegration for CodexIntegration {

fn healthcheck(&self, dc: &mut DoctorCounters, ctx: &HealthcheckContext) {
eprintln!("\n\x1b[1mCodex CLI integration\x1b[0m");
let codex_dir = ctx.home.join(".codex");
let config_path = codex_dir.join("config.toml");
doctor_check_config(dc, &config_path);
doctor_check_prompt(dc, &codex_dir);
doctor_check_hooks(dc, &codex_dir.join("hooks.json"));
let local_codex_dir = ctx.project_path.join(".codex");
if local_codex_dir.join("config.toml").exists()
|| local_codex_dir.join("hooks.json").exists()
|| local_agents_md_has_tokensave(&ctx.project_path.join("AGENTS.md"))
{
Comment thread
ScriptedAlchemy marked this conversation as resolved.
doctor_check_config(dc, &local_codex_dir.join("config.toml"));
doctor_check_prompt_file(dc, &ctx.project_path.join("AGENTS.md"));
doctor_check_hooks(dc, &local_codex_dir.join("hooks.json"));
} else {
let codex_dir = ctx.home.join(".codex");
let config_path = codex_dir.join("config.toml");
doctor_check_config(dc, &config_path);
doctor_check_prompt_file(dc, &codex_dir.join("AGENTS.md"));
doctor_check_hooks(dc, &codex_dir.join("hooks.json"));
}
}

fn is_detected(&self, home: &Path) -> bool {
Expand All @@ -118,12 +140,19 @@ impl AgentIntegration for CodexIntegration {
}
}

fn local_agents_md_has_tokensave(path: &Path) -> bool {
path.exists()
&& std::fs::read_to_string(path)
.unwrap_or_default()
.contains("## Prefer tokensave MCP tools")
}

// ---------------------------------------------------------------------------
// Install helpers
// ---------------------------------------------------------------------------

/// Register MCP server and auto-approve tools in ~/.codex/config.toml.
fn install_mcp_server(config_path: &Path, tokensave_bin: &str) -> Result<()> {
fn install_mcp_server(config_path: &Path, tokensave_bin: &str, scope: InstallScope) -> Result<()> {
let mut config = load_toml_file(config_path)?;

// Ensure [mcp_servers.tokensave] exists
Expand All @@ -146,10 +175,23 @@ fn install_mcp_server(config_path: &Path, tokensave_bin: &str) -> Result<()> {
"command".to_string(),
toml::Value::String(tokensave_bin.to_string()),
);
server_table.insert(
"args".to_string(),
toml::Value::Array(vec![toml::Value::String("serve".to_string())]),
);
let args = match scope {
InstallScope::Global => vec![toml::Value::String("serve".to_string())],
InstallScope::ProjectLocal => vec![
toml::Value::String("serve".to_string()),
toml::Value::String("--path".to_string()),
toml::Value::String(".".to_string()),
],
};
server_table.insert("args".to_string(), toml::Value::Array(args));
if scope == InstallScope::Global {
let mut env_table = toml::map::Map::new();
env_table.insert(
"TOKENSAVE_ENABLE_GLOBAL_DB".to_string(),
toml::Value::String("1".to_string()),
);
server_table.insert("env".to_string(), toml::Value::Table(env_table));
}

// Auto-approve all tokensave tools so Codex doesn't prompt for each one
let mut tools_table = toml::map::Map::new();
Expand Down Expand Up @@ -304,7 +346,7 @@ fn install_codex_hook_event(

let handler = json!({
"type": "command",
"command": format!("{} {subcommand}", shell_quote(tokensave_bin)),
"command": super::hook_command(tokensave_bin, subcommand),
"timeout": timeout,
});
let mut group = json!({ "hooks": [handler] });
Expand All @@ -327,10 +369,6 @@ fn group_has_subcommand(group: &serde_json::Value, subcommand: &str) -> bool {
})
}

fn shell_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "'\\''"))
}

/// Codex requires non-managed command hooks to be trusted via `/hooks` before
/// they run; newly installed/changed hooks are skipped until trusted.
fn print_hook_trust_guidance() {
Expand Down Expand Up @@ -540,19 +578,24 @@ fn doctor_check_config(dc: &mut DoctorCounters, config_path: &Path) {
}

/// Check AGENTS.md contains tokensave rules.
fn doctor_check_prompt(dc: &mut DoctorCounters, codex_dir: &Path) {
let agents_md = codex_dir.join("AGENTS.md");
fn doctor_check_prompt_file(dc: &mut DoctorCounters, agents_md: &Path) {
if agents_md.exists() {
let has_rules = std::fs::read_to_string(&agents_md)
let has_rules = std::fs::read_to_string(agents_md)
.unwrap_or_default()
.contains("tokensave");
if has_rules {
dc.pass("AGENTS.md contains tokensave rules");
dc.pass(&format!(
"AGENTS.md contains tokensave rules in {}",
agents_md.display()
));
} else {
dc.fail("AGENTS.md missing tokensave rules — run `tokensave install --agent codex`");
dc.fail(&format!(
"AGENTS.md missing tokensave rules in {} — run `tokensave install --local --agent codex` or `tokensave install --agent codex`",
agents_md.display()
));
}
} else {
dc.warn("~/.codex/AGENTS.md does not exist");
dc.warn(&format!("{} does not exist", agents_md.display()));
}
}

Expand All @@ -567,31 +610,46 @@ fn doctor_check_hooks(dc: &mut DoctorCounters, hooks_path: &Path) {
return;
}
let hooks = super::load_json_file(hooks_path);
let has_session_start = hooks["hooks"]["SessionStart"]
.as_array()
.is_some_and(|groups| {
groups.iter().any(|group| {
group["hooks"].as_array().is_some_and(|handlers| {
handlers.iter().any(|h| {
h["command"]
.as_str()
.is_some_and(|c| c.contains("hook-codex-session-start"))
})
})
})
});
if has_session_start {
let expected = [
("SessionStart", "hook-codex-session-start"),
("UserPromptSubmit", "hook-codex-user-prompt-submit"),
("SubagentStart", "hook-codex-subagent-start"),
("PostToolUse", "hook-codex-post-tool-use"),
];
let missing: Vec<&str> = expected
.iter()
.filter_map(|(event, command)| {
(!codex_hook_present(&hooks, event, command)).then_some(*event)
})
.collect();
if missing.is_empty() {
dc.pass(&format!(
"Lifecycle hooks registered in {}",
"All {} Codex lifecycle hooks registered in {}",
expected.len(),
hooks_path.display()
));
dc.info(
"Codex skips new/changed command hooks until trusted — run `/hooks` in Codex to trust the tokensave hooks",
);
} else {
dc.warn(&format!(
"tokensave hooks NOT registered in {} — run `tokensave install --agent codex`",
hooks_path.display()
"tokensave hook(s) missing for {} in {} — run `tokensave install --local --agent codex` or `tokensave install --agent codex`",
missing.join(", "),
hooks_path.display(),
));
}
}

fn codex_hook_present(hooks: &serde_json::Value, event: &str, command: &str) -> bool {
hooks["hooks"][event].as_array().is_some_and(|groups| {
groups.iter().any(|group| {
group["hooks"].as_array().is_some_and(|handlers| {
handlers.iter().any(|h| {
h["command"]
.as_str()
.is_some_and(|value| value.contains(command))
})
})
})
})
}
11 changes: 9 additions & 2 deletions src/agents/copilot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,15 @@ fn install_prompt_rules(instructions_path: &Path) -> Result<()> {
faster than file reads.\n\n\
If a code analysis question cannot be fully answered by tokensave MCP tools, \
try querying the SQLite database directly at `.tokensave/tokensave.db` \
(tables: `nodes`, `edges`, `files`). Use SQL to answer complex structural queries \
that go beyond what the built-in tools expose.\n"
(tables: `nodes`, `edges`, `files`, `memory_facts`, `memory_entities`, \
`memory_feedback_events`). Use SQL to answer complex structural queries \
that go beyond what the built-in tools expose.\n\n\
For durable project/user facts, prefer `tokensave_fact_store`, \
`tokensave_fact_feedback`, and `tokensave_memory_status` over ad-hoc notes. \
Do not store secrets, credentials, or unnecessary PII in persistent facts.\n\n\
If you find a gap where tokensave could answer a question natively, propose opening \
an issue at https://github.com/aovestdipaperino/tokensave. Remind the user to strip \
sensitive or proprietary code from any issue text before submitting.\n"
)
.map_err(|e| crate::errors::TokenSaveError::Config {
message: format!("failed to write {}: {e}", instructions_path.display()),
Expand Down
Loading
Loading