diff --git a/README.md b/README.md index 8edd9b5..5b599cb 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,9 @@ Claude Code persists conversations as JSONL under `~/.claude/projects//`, ``, etc.) are hidden but preserved on disk. -- **Edit a session** — open any session and surgically edit it. -- **Delete individual messages** — pick one and drop it. -- **Delete ranges, from-top, from-bottom** — bulk trim by range, prefix, or suffix. -- **Turn-level auto-pair** — deleting a user message also deletes its assistant response (and vice versa). Marking any message in a turn deletes the whole turn cleanly. -- **tool_use ↔ tool_result safety** — tool calls always travel with their results, so resume never breaks. -- **Token counts per message** — tiktoken `cl100k_base`, with `usage` metadata as fallback when present. -- **Atomic save with `.bak` backup** — write `.tmp`, fsync, rename. Backup written every save. -- **Concurrent-open detection** — `lsof` check refuses to save while Claude Code holds the file. Override with `--force`. -- **LLM-friendly non-interactive CLI** — `list`, `search`, `show`, `info`, `delete` subcommands with `--json` output. Other agents (Claude Code, Codex, scripts) can drive every action a human can. -- **Cross-platform** — macOS and Linux. Windows lsof equivalent deferred. +- **Edit your session data to manage context** — surgically delete noise (long tool dumps, stale exploration, leaked secrets) from a Claude Code session before resuming, so you keep useful history without burning context window. +- **Safe deletes** — turn-level auto-pair removes a user prompt with its assistant reply; tool_use ↔ tool_result blocks always travel together. Atomic save with `.bak` backup; refuses to write while Claude Code has the file open (`--force` to override). +- **TUI + scriptable CLI** — interactive ratatui browser with fuzzy search, plus `list / search / show / info / delete / update` subcommands with `--json` output so other agents (Claude Code, Codex, scripts) can drive every action. ## Install @@ -85,6 +75,7 @@ cc-session search [--json] [--limit N] cc-session show [--json] [--full] [--include-hidden] cc-session info [--json] cc-session delete --indices 3,5,7 [--from-top N] [--from-bottom N] [--range lo..hi] [--dry-run] [--force] [--json] +cc-session update [--version v0.2.0] ``` `` accepts a full path, a session UUID, or a unique substring of one. Indices are 0-based positions in the raw JSONL (use `cc-session show --json` to map text → index). Auto-pair always extends the delete set to keep `tool_use`/`tool_result` blocks together; `paired_added` in the output reports what was added. diff --git a/src/cli.rs b/src/cli.rs index fccefa8..c1c2045 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -474,6 +474,212 @@ pub fn info(projects_dir: &Path, target: &str, json: bool) -> Result<()> { Ok(()) } +// ---------- agent guide ---------- + +pub const AGENT_GUIDE: &str = r#"# cc-session agent guide + +You are an LLM driving cc-session non-interactively. This guide is the +single source of truth for how to use it. Read it once, then operate. + +## What this CLI does + +Edits Claude Code session JSONL files at ~/.claude/projects//.jsonl. +It can browse, search, inspect, and surgically delete messages from any session +while keeping tool_use/tool_result pairs and conversational turns intact. + +## Standard workflow + +1. Discover sessions: + cc-session list --json --limit 20 + cc-session search "" --json --limit 10 +2. Inspect one session: + cc-session info --json + cc-session show --json +3. Plan an edit (always dry-run first): + cc-session delete --indices 4,6 --dry-run --json +4. Apply: + cc-session delete --indices 4,6 --json + Pass --force only if the session is currently open in Claude Code; this + bypasses the lsof safety check. +5. (Optional) self-update: + cc-session update [--version v0.2.0] + +## Target argument () + +For show / info / delete the first positional arg accepts: + - a full filesystem path to a .jsonl file + - a full session UUID (preferred — unambiguous) + - any unique substring of a session UUID (8+ chars usually fine) +If a substring matches multiple sessions, the command errors and lists the +candidates. Pass a longer prefix to disambiguate. + +## Index semantics + +Indices are 0-based positions in the raw JSONL (one per line). Use +`cc-session show --json` to map message text -> index. Note: + - "Visible" messages (user / assistant text) are a subset; system messages, + tool_use blocks, tool_result blocks, attachments, and harness wrappers + (, , etc.) are hidden by default. Pass + --include-hidden to see them in `show`. + - Indices DO shift after a successful delete. Always re-run `show` between + deletes if you are picking by index. + +## Auto-pair (always on) + +Two safety extensions run on every delete request: + 1. tool_use <-> tool_result blocks always travel together. Marking either + side pulls the other. + 2. Turn-level pairing: a "turn" = visible user msg + every message that + follows it until the next visible user msg. Marking ANY message in a + turn marks the whole turn (user prompt + assistant reply + intermediate + tool calls). + +The delete output reports `requested` (what you asked) and `paired_added` +(what auto-pair added). Always inspect both before applying. + +## delete output JSON + + { + "path": "", + "backup": ".bak | null when --dry-run", + "requested": [int, ...], // sorted, what you asked + "after_auto_pair": [int, ...], // sorted, final delete set + "paired_added": [int, ...], // sorted, set diff + "total_messages_before": int, + "total_messages_after": int, + "dry_run": bool, + "saved": bool, + "warnings": [str, ...] // e.g. orphan tool_results + } + +## show output JSON (per message) + + { + "index": int, + "role": "user" | "assistant" | "system" | ..., + "type": "", + "timestamp": ISO8601 | null, + "tokens": int, // tiktoken cl100k_base + "visible": bool, + "has_tool_use": bool, + "has_tool_result": bool, + "tool_use_ids": [str, ...], + "tool_result_ids": [str, ...], + "text": str, // 400-char preview by default + "truncated": bool // true when text was clipped + } + +## info output JSON + + { + "path", "project", "session_id", "title", "modified", "size", + "total_messages", "visible_messages", "user_messages", "assistant_messages", + "tool_use_count", "tool_result_count", + "orphan_result_indices": [int, ...], + "estimated_tokens": int + } + +## list / search output JSON (per entry) + + { "project", "session_id", "title", "modified", "size", "path" } + +## Selection flags for delete + +You may combine any/all; the union is taken before auto-pair runs. + --indices 3,5,7 // exact indices (comma-separated) + --range lo..hi // inclusive range, both ints + --from-top N // first N messages + --from-bottom N // last N messages + +At least one selection flag is required. + +## Safety guarantees + + - Atomic save: writes .tmp, fsync, rename to . + - Backup: every save first writes .bak (overwriting any prior bak). + - Concurrent-open: if `lsof` reports the file is open by another process, + save returns SaveError::Conflict ("file is open by another process; close + Claude Code or pass --force"). On non-unix or when lsof is missing, this + check is skipped with a stderr warning. + - Round-trip: untouched messages save byte-equal — unknown JSONL fields + are preserved verbatim via `serde(flatten)`. + +## Exit codes + + 0 success + 1 generic error (parse failure, conflict, IO error, ambiguous target, ...) + 2+ reserved for future structured errors +Always inspect stderr on non-zero exit for the human-readable cause. + +## Environment overrides + + CC_SESSION_VERSION pin a specific release (used by `update`). + CC_SESSION_INSTALL_DIR where install.sh drops the binary. + CC_SESSION_INSTALLER_URL override installer URL for `update` (testing). + +## Useful examples (one-liners an agent can paste) + + # delete top 50 messages of a long session, dry run first + cc-session delete --from-top 50 --dry-run --json + cc-session delete --from-top 50 --json + + # purge messages 200..280 inclusive + cc-session delete --range 200..280 --dry-run --json + + # remove a single off-topic exchange (turn-pair pulls the assistant reply) + cc-session delete --indices 14 --dry-run --json + + # find a session about "auth middleware" and inspect + cc-session search "auth middleware" --json --limit 1 + cc-session show --json + +## Things this CLI will NOT do + + - Edit message contents in place. + - Reorder messages. + - Merge or split sessions. + - Apply changes while Claude Code is actively writing to the file + (refuses unless --force). +"#; + +// ---------- update ---------- + +const INSTALLER_URL: &str = "https://get-claude-code-session-editor.harshiitkgp.in/install.sh"; + +pub fn update(version: Option<&str>) -> Result<()> { + use std::process::{Command, Stdio}; + + let installer_url = + std::env::var("CC_SESSION_INSTALLER_URL").unwrap_or_else(|_| INSTALLER_URL.to_string()); + + println!("fetching installer: {installer_url}"); + + let mut curl = Command::new("curl") + .args(["-fsSL", &installer_url]) + .stdout(Stdio::piped()) + .spawn() + .context("failed to spawn curl (is it installed?)")?; + + let curl_stdout = curl.stdout.take().expect("curl stdout"); + + let mut sh = Command::new("sh"); + sh.stdin(curl_stdout); + if let Some(v) = version { + sh.env("CC_SESSION_VERSION", v); + } + let status = sh.status().context("failed to spawn sh")?; + + let curl_status = curl.wait().context("curl wait failed")?; + if !curl_status.success() { + bail!("curl exited with status {curl_status}"); + } + if !status.success() { + bail!("installer exited with status {status}"); + } + println!("update complete."); + Ok(()) +} + // ---------- helpers ---------- fn resolve_target(projects_dir: &Path, target: &str) -> Result { diff --git a/src/main.rs b/src/main.rs index f3028ec..ad3250f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -95,6 +95,16 @@ enum Command { #[arg(long)] json: bool, }, + /// Self-update to the latest release (or a specific version). + Update { + /// Install a specific tag (e.g. `v0.2.0`). Default: latest. + #[arg(long)] + version: Option, + }, + /// Print a structured agent guide: workflow, JSON shapes, env vars, + /// exit codes. Designed for LLMs and scripts to read once and operate + /// autonomously. + AgentGuide, } fn main() -> anyhow::Result<()> { @@ -145,6 +155,11 @@ fn main() -> anyhow::Result<()> { json, ), Some(Command::Info { target, json }) => cli::info(&projects_dir, &target, json), + Some(Command::Update { version }) => cli::update(version.as_deref()), + Some(Command::AgentGuide) => { + print!("{}", cli::AGENT_GUIDE); + Ok(()) + } } }