Skip to content
Merged
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
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install test dependencies
run: python -m pip install pytest
- name: Syntax / import smoke check
run: python -m py_compile server.py db.py analyze.py
run: |
python -m py_compile server.py db.py analyze.py
python -m chatview.cli --help
- name: Run test suite
run: python -m unittest discover -s tests -v
# 零依赖,无需 pip install
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.visual-ask/
.DS_Store
.cache/
.verify/
.claude/
.knowhow/
.superpowers/
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
[News](#news) · [快速开始](#快速开始) · [功能特性](#功能特性) · [使用指南](docs/USER_GUIDE.md) · [API 参考](#rest-api)

[![Homepage](https://img.shields.io/badge/首页-Distill_Yourself-0F766E?style=flat-square)](https://quantaalpha.com/Distill-Yourself/)
[![Python](https://img.shields.io/badge/Python-3.8+-3776AB?style=flat-square&logo=python&logoColor=white)](https://python.org)
[![Python](https://img.shields.io/badge/Python-3.9+-3776AB?style=flat-square&logo=python&logoColor=white)](https://python.org)
[![Claude Code + Codex](https://img.shields.io/badge/数据源-Claude_Code_%2B_Codex-F97316?style=flat-square)](.)
[![License](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](LICENSE)

Expand Down
6 changes: 6 additions & 0 deletions analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
cmd_refresh,
cmd_install_skill,
)
from chatview.commands.tool_calls import ( # noqa: F401
cmd_tool_search,
cmd_read_tool_event,
tool_search_data,
read_tool_event_data,
)
from chatview.commands.twin import ( # noqa: F401
cmd_twin_stats,
cmd_twin_budget,
Expand Down
155 changes: 125 additions & 30 deletions chatview/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import argparse
import io
import sys
from datetime import datetime

from chatview.commands.analysis import (
cmd_sessions,
Expand Down Expand Up @@ -35,7 +36,7 @@
cmd_profile_digest,
)
from chatview.commands.retrieval import (
cmd_search_plus, cmd_read_window, cmd_find_repeats, cmd_session_brief,
cmd_read_window, cmd_find_repeats, cmd_session_brief,
cmd_evidence_audit,
)
from chatview.commands.twin import (
Expand All @@ -56,6 +57,21 @@
cmd_twin_link,
cmd_twin_batch,
)
from chatview.commands.tool_calls import cmd_tool_search, cmd_read_tool_event


def _max_chars(value):
parsed = int(value)
if not 1 <= parsed <= 2000:
raise argparse.ArgumentTypeError("--max-chars must be between 1 and 2000")
return parsed


def _calendar_date(value):
try:
return datetime.strptime(value, "%Y-%m-%d").strftime("%Y-%m-%d")
except ValueError as exc:
raise argparse.ArgumentTypeError("expected YYYY-MM-DD") from exc


def main():
Expand All @@ -67,6 +83,7 @@ def main():
python3 analyze.py sessions --date 7d --source claude
python3 analyze.py read <session-id>
python3 analyze.py search "redis cache" --date 30d
python3 analyze.py search "redis cache" --start 2026-07-01 --end 2026-07-07
python3 analyze.py corrections --date 7d --project myproject
python3 analyze.py errors --date 30d
python3 analyze.py decisions --date 7d
Expand All @@ -80,6 +97,14 @@ def main():
shared.add_argument(
"--date", default="7d", help="Time filter: 1d, 7d, 30d, 90d, all (default: 7d)"
)
shared.add_argument(
"--start", type=_calendar_date, default=None,
help="Inclusive start date (YYYY-MM-DD); use with --end for an exact range",
)
shared.add_argument(
"--end", type=_calendar_date, default=None,
help="Inclusive end date (YYYY-MM-DD); use with --start for an exact range",
)
shared.add_argument(
"--source",
default="all",
Expand All @@ -89,7 +114,8 @@ def main():
"--project", default="", help="Filter by project name (substring match)"
)
shared.add_argument(
"--limit", type=int, default=50, help="Max results (default: 50)"
"--limit", type=int, default=50,
help="Max results per page (search default: 20; other commands: 50)",
)
shared.add_argument("--json", action="store_true", help="Output as JSON")
shared.add_argument(
Expand Down Expand Up @@ -117,7 +143,12 @@ def main():
)

p_search = sub.add_parser("search", parents=[shared], help="Search messages")
p_search.set_defaults(limit=20)
p_search.add_argument("query", help="Search query")
p_search.add_argument(
"--recall", choices=("normal", "high"), default="normal",
help="Retrieval breadth (default: normal)",
)
p_search.add_argument(
"--role",
choices=("all", "user", "assistant"),
Expand All @@ -134,50 +165,104 @@ def main():
"-A", "--after", type=int, help="Show N messages after each match"
)
p_search.add_argument(
"-n", "--line-number", action="store_true", help="Prefix context lines with session:idx:role"
)

p_search_plus = sub.add_parser(
"search-plus",
parents=[shared],
help="Hybrid higher-recall search with match reasons",
)
p_search_plus.add_argument("query", help="Search query")
p_search_plus.add_argument(
"-C", "--context", type=int, default=0, help="Show N messages before/after each match"
)
p_search_plus.add_argument(
"-B", "--before", type=int, help="Show N messages before each match"
"-n",
"--line-number",
action="store_true",
help="Prefix context lines with explicit session, idx, and role labels",
)
p_search_plus.add_argument(
"-A", "--after", type=int, help="Show N messages after each match"
p_search.add_argument(
"--format", choices=("standard", "lines", "jsonl"), default="standard",
help="Output format (default: standard)",
)
p_search_plus.add_argument(
"-n", "--line-number", action="store_true", help="Prefix context lines with session:idx:role"
p_search.add_argument("--page", type=int, default=1, help="1-based result page")
p_search.add_argument(
"--max-chars", type=_max_chars, default=500,
help="Maximum characters per result or inline context message (default: 500)",
)
p_search_plus.add_argument(
"--include-artifacts",
action="store_true",
help="Deprecated compatibility flag; artifacts are always marked and downranked",
p_search.add_argument(
"--evidence-only", action="store_true",
help="Exclude orchestration artifacts and meta-research prompts",
)

p_read_window = sub.add_parser(
"read-window",
parents=[shared],
help="Read a small message window around a match index",
help="Read a composable bounded message window around a match index",
)
p_read_window.add_argument("session", nargs="?", help="Session ID or partial match")
p_read_window.add_argument("session", nargs="?", help="Exact session ID")
p_read_window.add_argument("--idx", type=int, help="Target message index")
p_read_window.add_argument(
"--radius",
"-B",
"--before",
type=int,
default=2,
help="Actual indexed messages before the target (default: 2, max: 20)",
)
p_read_window.add_argument(
"-A",
"--after",
type=int,
default=2,
help="How many message indexes before/after to include",
help="Actual indexed messages after the target (default: 2, max: 20)",
)
p_read_window.add_argument(
"--include",
default="user:2000,assistant:600",
help=(
"Role projections as role:max-chars pairs "
"(default: user:2000,assistant:600)"
),
)
p_read_window.add_argument(
"--around",
default="",
help="Center the target projection on one unique exact string",
)
p_read_window.add_argument(
"--batch",
default="",
help='JSON list of {"session": "...", "idx": N, "radius": N} items',
help=(
'JSON list of {"sessionId":"...","idx":N,"before":N,'
'"after":N,"include":"user:2000,assistant:600"} items'
),
)

p_tool_search = sub.add_parser(
"tool-search",
parents=[shared],
help="Search bounded tool-call inputs and return raw JSONL locators",
)
p_tool_search.set_defaults(limit=20)
p_tool_search.add_argument("query", help="Tool input, command, path, or patch target")
p_tool_search.add_argument("--page", type=int, default=1, help="1-based result page")
p_tool_search.add_argument(
"--format", choices=("standard", "lines", "jsonl"), default="standard"
)
p_tool_search.add_argument(
"--max-chars", type=_max_chars, default=500,
help="Maximum characters per search result (default: 500)",
)

p_read_tool = sub.add_parser(
"read-tool-event",
parents=[shared],
help="Read one indexed tool call from its source JSONL with bounded output",
)
p_read_tool.add_argument("session", help="Exact session ID from tool-search")
p_read_tool.add_argument("--event-idx", type=int, required=True)
p_read_tool.add_argument(
"--include-result",
action="store_true",
help="Read the matching result from source JSONL without indexing it",
)
p_read_tool.add_argument(
"--around",
default="",
help="Center the bounded tool input projection around an exact anchor",
)
p_read_tool.add_argument(
"--max-chars", type=_max_chars, default=2000,
help="Maximum input/result characters to return (default/max: 2000)",
)

p_find_repeats = sub.add_parser(
Expand Down Expand Up @@ -436,13 +521,23 @@ def main():
if not args.command:
parser.print_help()
sys.exit(1)
if getattr(args, "start", "") or getattr(args, "end", ""):
explicit_date = any(
token == "--date" or token.startswith("--date=")
for token in sys.argv[1:]
)
if explicit_date:
parser.error("--date cannot be combined with --start or --end")
if args.start and args.end and args.start > args.end:
parser.error("--start must be on or before --end")

cmds = {
"sessions": cmd_sessions,
"read": cmd_read,
"search": cmd_search,
"search-plus": cmd_search_plus,
"read-window": cmd_read_window,
"tool-search": cmd_tool_search,
"read-tool-event": cmd_read_tool_event,
"find-repeats": cmd_find_repeats,
"session-brief": cmd_session_brief,
"queries": cmd_queries,
Expand Down
Loading
Loading