Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
* text=auto eol=lf
*.jsonl text eol=lf
*.py text eol=lf
*.md text eol=lf
*.toml text eol=lf
*.lock text eol=lf
justfile text eol=lf
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: CI

on:
push:
pull_request:

jobs:
test:
name: test (${{ matrix.os }}, py${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.10", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: extractions/setup-just@v2
- run: just install
- run: just check
2 changes: 1 addition & 1 deletion cchat/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"""Claude Code Chat Browser CLI."""
"""Claude Code Chat Browser CLI."""
136 changes: 68 additions & 68 deletions cchat/cli.py
Original file line number Diff line number Diff line change
@@ -1,68 +1,68 @@
"""Argparse entry point for the cchat CLI tool."""
import argparse
import sys
from cchat import formatters
from cchat.commands import agents_cmd, files_cmd, line_cmd, lines_cmd, list_cmd, search_cmd, serve_cmd, spending_cmd, tokens_cmd, view_cmd
class _FullHelpParser(argparse.ArgumentParser):
"""ArgumentParser that prints full --help on errors instead of short usage."""
def error(self, message):
sys.stderr.write(f"error: {message}\n\n")
# If a subcommand was given, show its help instead of top-level
if self._subparsers is not None:
for action in self._subparsers._actions:
if isinstance(action, argparse._SubParsersAction):
for arg in sys.argv[1:]:
if arg in action.choices:
action.choices[arg].print_help(sys.stderr) # type: ignore[union-attr]
sys.exit(2)
self.print_help(sys.stderr)
sys.exit(2)
class _SubcommandHelpParser(argparse.ArgumentParser):
"""Subcommand parser that prints its own full --help on errors."""
def error(self, message):
sys.stderr.write(f"error: {message}\n\n")
self.print_help(sys.stderr)
sys.exit(2)
def main():
parser = _FullHelpParser(description="Claude Code Chat Browser")
parser.add_argument("--no-color", action="store_true", help="Disable colored output")
subparsers = parser.add_subparsers(dest="command", parser_class=_SubcommandHelpParser)
subparsers.required = True
list_cmd.register(subparsers)
view_cmd.register(subparsers)
line_cmd.register(subparsers)
lines_cmd.register(subparsers)
files_cmd.register(subparsers)
search_cmd.register(subparsers)
tokens_cmd.register(subparsers)
spending_cmd.register(subparsers)
agents_cmd.register(subparsers)
serve_cmd.register(subparsers)
args = parser.parse_args()
if args.no_color:
formatters.set_no_color(True)
try:
args.func(args)
except KeyboardInterrupt:
sys.exit(0)
except BrokenPipeError:
sys.exit(0)
if __name__ == "__main__":
main()
"""Argparse entry point for the cchat CLI tool."""

import argparse
import sys

from cchat import formatters
from cchat.commands import agents_cmd, files_cmd, line_cmd, lines_cmd, list_cmd, search_cmd, serve_cmd, spending_cmd, tokens_cmd, view_cmd


class _FullHelpParser(argparse.ArgumentParser):
"""ArgumentParser that prints full --help on errors instead of short usage."""

def error(self, message):
sys.stderr.write(f"error: {message}\n\n")
# If a subcommand was given, show its help instead of top-level
if self._subparsers is not None:
for action in self._subparsers._actions:
if isinstance(action, argparse._SubParsersAction):
for arg in sys.argv[1:]:
if arg in action.choices:
action.choices[arg].print_help(sys.stderr) # type: ignore[union-attr]
sys.exit(2)
self.print_help(sys.stderr)
sys.exit(2)


class _SubcommandHelpParser(argparse.ArgumentParser):
"""Subcommand parser that prints its own full --help on errors."""

def error(self, message):
sys.stderr.write(f"error: {message}\n\n")
self.print_help(sys.stderr)
sys.exit(2)


def main():
parser = _FullHelpParser(description="Claude Code Chat Browser")
parser.add_argument("--no-color", action="store_true", help="Disable colored output")

subparsers = parser.add_subparsers(dest="command", parser_class=_SubcommandHelpParser)
subparsers.required = True

list_cmd.register(subparsers)
view_cmd.register(subparsers)
line_cmd.register(subparsers)
lines_cmd.register(subparsers)
files_cmd.register(subparsers)
search_cmd.register(subparsers)
tokens_cmd.register(subparsers)
spending_cmd.register(subparsers)
agents_cmd.register(subparsers)
serve_cmd.register(subparsers)

args = parser.parse_args()

if args.no_color:
formatters.set_no_color(True)

try:
args.func(args)
except KeyboardInterrupt:
sys.exit(0)
except BrokenPipeError:
sys.exit(0)


if __name__ == "__main__":
main()
162 changes: 81 additions & 81 deletions cchat/commands/files_cmd.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,81 @@
"""List files modified in a conversation."""
from __future__ import annotations
import argparse
from collections import defaultdict
from cchat import formatters, parser, store
def register(subparsers: argparse._SubParsersAction) -> None:
p = subparsers.add_parser("files", help="List files modified in a conversation")
p.add_argument("conv", help="Conversation identifier (path, UUID, prefix, or slug)")
p.add_argument(
"--no-subagents",
action="store_true",
default=False,
help="Exclude subagent conversations",
)
p.add_argument("--json", action="store_true", default=False, help="Output as JSON")
p.add_argument("--no-color", action="store_true", default=False, help="Disable colored output")
p.set_defaults(func=run)
def _scan_file(path, file_counts, file_tools):
"""Scan a single JSONL file for file modifications."""
for _line_num, data in parser.parse_lines(path):
if data.get("type") != "assistant":
continue
mods = parser.extract_file_modifications(data)
if not mods:
continue
for mod in mods:
fp = mod["file_path"]
file_counts[fp] += 1
file_tools[fp].add(mod["tool"])
def run(args: argparse.Namespace) -> None:
if args.no_color:
formatters.set_no_color(True)
conv_path = store.resolve_conversation(args.conv)
file_counts: dict[str, int] = defaultdict(int)
file_tools: dict[str, set[str]] = defaultdict(set)
# Scan main conversation
_scan_file(conv_path, file_counts, file_tools)
# Scan subagents unless disabled
include_subagents = not args.no_subagents
if include_subagents:
for sa_path in store.get_subagent_paths(conv_path):
_scan_file(sa_path, file_counts, file_tools)
if not file_counts:
print("No file modifications found.")
return
# Sort by modification count descending
sorted_files = sorted(file_counts.items(), key=lambda x: x[1], reverse=True)
if args.json:
data = [
{
"file_path": fp,
"modifications": count,
"tools": sorted(file_tools[fp]),
}
for fp, count in sorted_files
]
print(formatters.format_json(data))
return
rows = [
[fp, str(count), ", ".join(sorted(file_tools[fp]))]
for fp, count in sorted_files
]
headers = ["FILE", "MODIFICATIONS", "TOOLS"]
print(formatters.format_table(rows, headers, no_color=args.no_color))
"""List files modified in a conversation."""

from __future__ import annotations

import argparse
from collections import defaultdict

from cchat import formatters, parser, store


def register(subparsers: argparse._SubParsersAction) -> None:
p = subparsers.add_parser("files", help="List files modified in a conversation")
p.add_argument("conv", help="Conversation identifier (path, UUID, prefix, or slug)")
p.add_argument(
"--no-subagents",
action="store_true",
default=False,
help="Exclude subagent conversations",
)
p.add_argument("--json", action="store_true", default=False, help="Output as JSON")
p.add_argument("--no-color", action="store_true", default=False, help="Disable colored output")
p.set_defaults(func=run)


def _scan_file(path, file_counts, file_tools):
"""Scan a single JSONL file for file modifications."""
for _line_num, data in parser.parse_lines(path):
if data.get("type") != "assistant":
continue
mods = parser.extract_file_modifications(data)
if not mods:
continue
for mod in mods:
fp = mod["file_path"]
file_counts[fp] += 1
file_tools[fp].add(mod["tool"])


def run(args: argparse.Namespace) -> None:
if args.no_color:
formatters.set_no_color(True)

conv_path = store.resolve_conversation(args.conv)

file_counts: dict[str, int] = defaultdict(int)
file_tools: dict[str, set[str]] = defaultdict(set)

# Scan main conversation
_scan_file(conv_path, file_counts, file_tools)

# Scan subagents unless disabled
include_subagents = not args.no_subagents
if include_subagents:
for sa_path in store.get_subagent_paths(conv_path):
_scan_file(sa_path, file_counts, file_tools)

if not file_counts:
print("No file modifications found.")
return

# Sort by modification count descending
sorted_files = sorted(file_counts.items(), key=lambda x: x[1], reverse=True)

if args.json:
data = [
{
"file_path": fp,
"modifications": count,
"tools": sorted(file_tools[fp]),
}
for fp, count in sorted_files
]
print(formatters.format_json(data))
return

rows = [
[fp, str(count), ", ".join(sorted(file_tools[fp]))]
for fp, count in sorted_files
]
headers = ["FILE", "MODIFICATIONS", "TOOLS"]
print(formatters.format_table(rows, headers, no_color=args.no_color))
Loading
Loading