feat: add tb-session CLI for Claude Code session search#2
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…on support Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…xtraction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ters Add FTS5 full-text search with BM25 scoring, dynamic SQL parameter building for branch/project/date filters, min-max relevance normalization, snippet extraction, and dual human-table/JSON output modes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix type mismatches in list command (u32 vs usize), remove invalid unwrap_or on non-Option field, wrap page in Some() for SessionList construction, fix pagination_hint call with correct types, remove unnecessary as_ref() in show command, rename into_match to to_match to follow Rust naming conventions for non-consuming methods, and wire all commands in main.rs dispatch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The else branch said "claude binary found in PATH" even when the binary was not found — the check() function renders ✓/✗, so labels should describe what's being checked, not assert the result. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The real format is {"version": 1, "entries": [...]} but the parser only
handled flat arrays, causing all index metadata to be silently discarded.
Now tries versioned format first, falls back to flat array.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Detect git worktrees and include sessions from all worktrees of the same repo when running `list` and `search`. Previously these commands filtered by exact `project_path = cwd`, making sessions from other worktrees invisible. - Add git::repo_paths() to discover all worktrees via `git worktree list` - Change list/search project filter from `= ?` to `IN (?, ?, ...)` - Ensure index freshness for each worktree path - Replace fragile str::replace COUNT query with shared WHERE clause builder Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ial chars FTS5 interprets `:`, `*`, `AND`, `OR` etc. as operators. Wrapping each term in double quotes forces literal matching, so URLs like https://github.com/... no longer cause "column not recognized" errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Always try UUID prefix match first, then fall back to name/summary search. Removes the looks_like_uuid heuristic for branching, which rejected short prefixes like 7-char IDs (e.g. 27b0337). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. message_count now uses messages.len() (actual user/assistant count) instead of line_count (all JSON lines including progress events) 2. Empty search query returns a clear error instead of crashing FTS5 3. session_id is now shell-escaped in osascript commands 4. open_in_terminal guards against non-macOS platforms Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Searches for sessions mentioning a PR by number or URL: tb-session search --pr 557 tb-session search --pr https://github.com/org/repo/pull/557 The positional query argument is now optional when --pr is provided. PR numbers are expanded to "pull/<number>" for GitHub URL matching. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Security: osascript now reads command via TB_SESSION_CMD env var instead of string interpolation, preventing AppleScript injection 2. Performance: show/resume use resolve_and_freshen (scoped to repo worktrees) instead of scanning all projects 3. UX: error when both positional query and --pr are provided Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…cases High priority (previously zero tests): - build_pr_query: PR number, URL passthrough, non-numeric input - shell_escape: plain, spaces, single quotes, double quotes, backslash, empty - build_osascript: iTerm.app, Terminal.app, unknown terminal fallback Medium priority (edge cases for changed logic): - sanitize_fts5_query: empty, whitespace-only, embedded quotes, FTS5 operators - parse_worktree_output: extracted from git.rs, tested with fixture strings - parser: zero-message session, non-user/assistant roles skipped, empty content handling for first_prompt/summary, summary not overwritten - looks_like_uuid: 7-char boundary, all-dashes, whitespace-padded Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Resume opens new terminal tab (not session-ending exec) - --pr filter for searching by PR number/URL - Worktree-aware scoping - Name/search term resume (not just UUIDs) - URLs work as search queries - Quick reference with common commands Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New terminal tabs don't inherit the parent process's cwd — they start in iTerm/Terminal's default directory. The previous logic skipped the cd when cwd matched the project_path, but that only applies to the direct exec path (TTY mode), not the osascript/new-tab path. Now: project_dir is always passed to open_in_terminal. The cwd == target skip only applies to the interactive (TTY) exec path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts: # Cargo.lock
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
trogulja
left a comment
There was a problem hiding this comment.
LGTM with one inline note on shell_escape.
Make sure to resolve the merge conflicts on Cargo.toml, scripts/bump.sh, and scripts/install.sh before merging — both tb-devctl and tb-session need to be in those lists.
| } | ||
|
|
||
| fn shell_escape(s: &str) -> String { | ||
| if s.contains(|c: char| c.is_whitespace() || c == '\'' || c == '"' || c == '\\') { |
There was a problem hiding this comment.
Security: shell_escape only triggers quoting for whitespace, ', ", and \. Shell metacharacters like $, backtick, ;, &, | pass through unquoted. Since the result is executed as a shell command by Terminal.app/iTerm via do script, a project path containing e.g. $(malicious) would trigger command substitution.
Fix: always single-quote unconditionally — the inner ' → '\'' replacement already handles embedded quotes:
fn shell_escape(s: &str) -> String {
if s.is_empty() {
return s.to_string();
}
format!("'{}'", s.replace('\'', "'\\''"))
}There was a problem hiding this comment.
Fixed in 39b33cc — shell_escape now always single-quotes unconditionally. Added test cases for $(), backtick, ;, &, and |.
…tion Shell metacharacters ($, `, ;, &, |) were passing through unquoted when the string didn't contain whitespace or quotes. Since the escaped value is executed as a shell command via Terminal.app/iTerm `do script`, a project path like `$(malicious)` could trigger command substitution. Always single-quote unconditionally with standard POSIX escaping. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rity # Conflicts: # Cargo.toml # scripts/bump.sh # scripts/install.sh
Summary
New
tb-sessionCLI tool that indexes and full-text searches Claude Code sessions via SQLite FTS5. Joins the cli-toolbox workspace alongside tb-prod, tb-sem, tb-bug, and tb-lf.Commands
search--branch,--pr,--project,--after/before)listshowresumeindexdoctorcache-clearprimeconfig~/.config/tb-session/config.tomlskill install~/.claude/skills/tb-session/Key features beyond basic search
listandsearchautomatically include sessions from all git worktrees of the same repo, not just the exact cwdclaude --resume--prfilter —tb-session search --pr 557finds sessions mentioning a PR by number or URL{version, entries}format for fast metadata loadingArchitecture
~/.cache/tb-session/index.dbdefine_error!since that requires reqwest)Resume: why a new tab?
claude --resumestarts a new interactive Claude Code session. This can't run inside an existing session —exec()would kill the current conversation. Whentb-session resumedetects it's running inside Claude (stdin is not a TTY), it opens a new terminal tab via osascript instead. The tab automatically:cd's into the session's original project directory (if different from cwd)claude --resume <full-session-id>with the resolved UUIDWhen run from a regular terminal, it exec's directly as expected.
Installation
After installing, rebuild your index:
Usage examples
Terminal usage
Prompts that trigger Claude to use tb-session
When the skill is installed, these prompts make Claude reach for
tb-sessionautomatically:Claude will run
tb-session search, pick the right session, andtb-session resumeit — opening a new terminal tab with the session.Test plan
--prfilter finds sessions by PR number and URLtb-session doctortb-session primeWorkspace changes
Cargo.toml: addedmodern_sqliteto rusqlite features, addedtb-sessionworkspace depscripts/install.sh: added tb-session to ALL_TOOLSscripts/bump.sh: added tb-session to VALID_TOOLSPost-merge TODO
tb-sessionon "resume session" prompts (currently Claude interprets "resume the session" as "continue the work" instead of usingtb-session resume)tb-sessionto the work project CLAUDE.md CLI Toolbox section with explicit instructions to prefer it over manual JSONL searchingtb-session skill installto deploy updated skill🤖 Generated with Claude Code