From cb9a5460588c74f81d1d311d667acb98ff91da7e Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Tue, 17 Mar 2026 16:26:32 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20saar=20CLI=20personality=20--=20?= =?UTF-8?q?=E2=9C=93=20checkmark,=20exceptions=20highlighted,=20token=20co?= =?UTF-8?q?unt=20front=20and=20center?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Header: 'saar v0.5.14 · [repo]' -- clean, no filler - Exceptions row: bold white (not cyan) -- the demo money shot - File list: aligned columns, tokens visible inline - Closing: '✓ ~911 tokens · your AI knows your codebase' + getsaar.com - Bumps to v0.5.14 --- pyproject.toml | 2 +- saar/__init__.py | 2 +- saar/commands/extract.py | 46 +++++++++++++++++++++++++--------------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e538fb9..ea8ae37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "saar" -version = "0.5.13" +version = "0.5.14" description = "Extract the essence of your codebase. Auto-generate AGENTS.md, CLAUDE.md, .cursorrules and more." readme = "README.md" license = "MIT" diff --git a/saar/__init__.py b/saar/__init__.py index 1b19224..308b6f1 100644 --- a/saar/__init__.py +++ b/saar/__init__.py @@ -1,3 +1,3 @@ """Saar -- extract the essence of your codebase.""" -__version__ = "0.5.13" +__version__ = "0.5.14" diff --git a/saar/commands/extract.py b/saar/commands/extract.py index 0eb9f46..418a26c 100644 --- a/saar/commands/extract.py +++ b/saar/commands/extract.py @@ -130,16 +130,18 @@ def write_with_markers(target: Path, generated: str, *, force: bool, console=Non if not target.exists(): target.write_text(wrapped, encoding="utf-8") - _tok = f" ~{len(wrapped)//4} tokens" if "AGENTS" in target.name else "" - _con.print(f" [green]wrote[/green] {_display_path(target)} [dim]({_line_count(wrapped)} lines{_tok})[/dim]") + _lines = _line_count(wrapped) + _tok = f" [dim]~{len(wrapped)//4} tokens[/dim]" if "AGENTS" in target.name else "" + _con.print(f" [green]wrote[/green] {_display_path(target):<32} [dim]{_lines} lines[/dim]{_tok}") return existing = target.read_text(encoding="utf-8") if force: target.write_text(wrapped, encoding="utf-8") - _tok = f" ~{len(wrapped)//4} tokens" if "AGENTS" in target.name else "" - _con.print(f" [green]wrote[/green] {_display_path(target)} [dim]({_line_count(wrapped)} lines{_tok})[/dim]") + _lines = _line_count(wrapped) + _tok = f" [dim]~{len(wrapped)//4} tokens[/dim]" if "AGENTS" in target.name else "" + _con.print(f" [green]wrote[/green] {_display_path(target):<32} [dim]{_lines} lines[/dim]{_tok}") return start_idx = existing.find(_MARKER_START) @@ -155,8 +157,9 @@ def write_with_markers(target: Path, generated: str, *, force: bool, console=Non after = after.replace(_MARKER_START, "").replace(_MARKER_END, "").lstrip("\n") final = before + wrapped + ("\n" + after if after.strip() else "") target.write_text(final, encoding="utf-8") - _tok = f" ~{len(final)//4} tokens" if "AGENTS" in target.name else "" - _con.print(f" [green]updated[/green] {_display_path(target)} [dim]({_line_count(final)} lines, manual edits preserved{_tok})[/dim]") + _lines = _line_count(final) + _tok = f" [dim]~{len(final)//4} tokens[/dim]" if "AGENTS" in target.name else "" + _con.print(f" [green]updated[/green] {_display_path(target):<32} [dim]{_lines} lines[/dim]{_tok}") def _handle_unmarked_file(target: Path, existing: str, wrapped: str, force: bool, console=None) -> None: @@ -221,6 +224,10 @@ def show_detection_summary(dna, no_interview: bool) -> bool: Returns True to proceed, False if user says detections are wrong. Always returns True in CI / --no-interview mode. + + Design: the Exceptions row is the money shot -- it shows what AI gets wrong + without saar (generic ValueError) vs what it now knows (OCIAPIError). + Make it visually distinct from the rest of the table. """ from rich.table import Table @@ -231,10 +238,14 @@ def show_detection_summary(dna, no_interview: bool) -> bool: rows = _build_summary_rows(dna) table = Table(show_header=False, box=None, padding=(0, 2)) - table.add_column(style="dim", width=18) + table.add_column(style="dim", width=20) table.add_column() for label, value in rows: - table.add_row(label, f"[cyan]{value}[/cyan]") + # Exceptions is the demo moment -- make it stand out + if label == "Exceptions": + table.add_row(label, f"[bold white]{value}[/bold white]") + else: + table.add_row(label, f"[cyan]{value}[/cyan]") console.print(table) for warning in getattr(dna, "analysis_warnings", []): @@ -387,7 +398,7 @@ def cmd_extract( import saar as _saar_pkg _ver = getattr(_saar_pkg, "__version__", "") console.print() - console.print(f" [bold]saar[/bold] [dim]v{_ver}[/dim] · analyzing [bold cyan]{repo_path.name}[/bold cyan]") + console.print(f" [bold]saar[/bold] [dim]v{_ver}[/dim] [dim]·[/dim] [bold cyan]{repo_path.name}[/bold cyan]") console.print(f" [dim]{'─' * 44}[/dim]") if include: console.print(f" [dim]subset: {' '.join(include)}[/dim]") @@ -453,22 +464,23 @@ def cmd_extract( if index: run_oci_indexing(repo_path) - # -- upgraded closing summary with token count -- - # Count tokens in AGENTS.md for the summary + # -- closing: token count front and center, clean file list -- _agents_path = (output or repo_path) / "AGENTS.md" - _token_note = "" + _tokens = 0 if _agents_path.exists(): try: - _chars = len(_agents_path.read_text(encoding="utf-8")) - _tokens = _chars // 4 # ~4 chars/token approximation - _token_note = f" [dim]{_tokens} tokens[/dim]" + _tokens = len(_agents_path.read_text(encoding="utf-8")) // 4 except Exception: pass console.print() console.print(f" [dim]{'─' * 44}[/dim]") - console.print(f" [bold green]Your AI knows your codebase.[/bold green]{_token_note}") - console.print(" [dim]Load AGENTS.md before your next Claude session.[/dim]") + if _tokens: + console.print(f" [bold green]✓[/bold green] [bold]~{_tokens} tokens[/bold] [dim]· your AI knows your codebase[/dim]") + else: + console.print(" [bold green]✓[/bold green] [bold]Your AI knows your codebase.[/bold]") + console.print(" [dim]getsaar.com[/dim]") + console.print() # -- post-extract dogfood check: warn on contradictions in tribal knowledge -- # SA006 catches stale facts like "cli.py is 1514 lines" contradicting "cli.py is 68 lines". From 61b6e08abb206b04458542e4c5b3f49226564a74 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Tue, 17 Mar 2026 16:34:50 -0400 Subject: [PATCH 2/3] fix: professional CLI output -- remove decorations, align with industry standard Industry standard audit (gh, bun, cargo, vercel): - No decorative divider lines -- borders are for tables, not CLI output - No getsaar.com in execution path -- marketing belongs in --help, not hot path - No repeated info -- file list already shows what was written; closing is brief - Capitalize verbs: Wrote/Updated (not wrote/updated) -- consistent with gh/cargo - Closing: 'AGENTS.md ready. ~920 tokens.' -- informative, not self-congratulatory - Keep Exceptions row bold white -- the demo money shot stays Uniformity across commands: - extract now matches the lighter style of lint/diff/add/stats - All commands use same 2-space indent, same color scheme Bumps to v0.5.14. 548 tests passing. --- saar/commands/extract.py | 27 ++++++++++----------------- tests/test_cli.py | 4 ++-- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/saar/commands/extract.py b/saar/commands/extract.py index 418a26c..c8a25ae 100644 --- a/saar/commands/extract.py +++ b/saar/commands/extract.py @@ -130,18 +130,16 @@ def write_with_markers(target: Path, generated: str, *, force: bool, console=Non if not target.exists(): target.write_text(wrapped, encoding="utf-8") - _lines = _line_count(wrapped) - _tok = f" [dim]~{len(wrapped)//4} tokens[/dim]" if "AGENTS" in target.name else "" - _con.print(f" [green]wrote[/green] {_display_path(target):<32} [dim]{_lines} lines[/dim]{_tok}") + _tok = f" [dim]({len(wrapped)//4} tokens)[/dim]" if "AGENTS" in target.name else "" + _con.print(f" [green]Wrote[/green] [bold]{_display_path(target)}[/bold] [dim]{_line_count(wrapped)} lines[/dim]{_tok}") return existing = target.read_text(encoding="utf-8") if force: target.write_text(wrapped, encoding="utf-8") - _lines = _line_count(wrapped) - _tok = f" [dim]~{len(wrapped)//4} tokens[/dim]" if "AGENTS" in target.name else "" - _con.print(f" [green]wrote[/green] {_display_path(target):<32} [dim]{_lines} lines[/dim]{_tok}") + _tok = f" [dim]({len(wrapped)//4} tokens)[/dim]" if "AGENTS" in target.name else "" + _con.print(f" [green]Wrote[/green] [bold]{_display_path(target)}[/bold] [dim]{_line_count(wrapped)} lines[/dim]{_tok}") return start_idx = existing.find(_MARKER_START) @@ -157,9 +155,8 @@ def write_with_markers(target: Path, generated: str, *, force: bool, console=Non after = after.replace(_MARKER_START, "").replace(_MARKER_END, "").lstrip("\n") final = before + wrapped + ("\n" + after if after.strip() else "") target.write_text(final, encoding="utf-8") - _lines = _line_count(final) - _tok = f" [dim]~{len(final)//4} tokens[/dim]" if "AGENTS" in target.name else "" - _con.print(f" [green]updated[/green] {_display_path(target):<32} [dim]{_lines} lines[/dim]{_tok}") + _tok = f" [dim]({len(final)//4} tokens)[/dim]" if "AGENTS" in target.name else "" + _con.print(f" [green]Updated[/green] [bold]{_display_path(target)}[/bold] [dim]{_line_count(final)} lines[/dim]{_tok}") def _handle_unmarked_file(target: Path, existing: str, wrapped: str, force: bool, console=None) -> None: @@ -398,8 +395,7 @@ def cmd_extract( import saar as _saar_pkg _ver = getattr(_saar_pkg, "__version__", "") console.print() - console.print(f" [bold]saar[/bold] [dim]v{_ver}[/dim] [dim]·[/dim] [bold cyan]{repo_path.name}[/bold cyan]") - console.print(f" [dim]{'─' * 44}[/dim]") + console.print(f" [dim]Analyzing[/dim] [bold]{repo_path.name}[/bold][dim]...[/dim]") if include: console.print(f" [dim]subset: {' '.join(include)}[/dim]") @@ -464,7 +460,7 @@ def cmd_extract( if index: run_oci_indexing(repo_path) - # -- closing: token count front and center, clean file list -- + # -- clean closing: what was written, token count as secondary info -- _agents_path = (output or repo_path) / "AGENTS.md" _tokens = 0 if _agents_path.exists(): @@ -474,13 +470,10 @@ def cmd_extract( pass console.print() - console.print(f" [dim]{'─' * 44}[/dim]") if _tokens: - console.print(f" [bold green]✓[/bold green] [bold]~{_tokens} tokens[/bold] [dim]· your AI knows your codebase[/dim]") + console.print(f" [dim]AGENTS.md ready. ~{_tokens} tokens.[/dim]") else: - console.print(" [bold green]✓[/bold green] [bold]Your AI knows your codebase.[/bold]") - console.print(" [dim]getsaar.com[/dim]") - console.print() + console.print(" [dim]AGENTS.md ready.[/dim]") # -- post-extract dogfood check: warn on contradictions in tribal knowledge -- # SA006 catches stale facts like "cli.py is 1514 lines" contradicting "cli.py is 68 lines". diff --git a/tests/test_cli.py b/tests/test_cli.py index 489eb0f..d71172e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -98,7 +98,7 @@ def test_skips_existing_without_force(self, tmp_repo: Path, tmp_path: Path): runner.invoke(app, ["extract", str(tmp_repo), "--format", "claude", "-o", str(output_dir)]) result = runner.invoke(app, ["extract", str(tmp_repo), "--format", "claude", "-o", str(output_dir)]) assert result.exit_code == 0 - assert "updated" in result.stdout or "wrote" in result.stdout + assert "Updated" in result.stdout or "Wrote" in result.stdout def test_force_overwrites(self, tmp_repo: Path, tmp_path: Path): """--force does a clean overwrite.""" @@ -107,7 +107,7 @@ def test_force_overwrites(self, tmp_repo: Path, tmp_path: Path): runner.invoke(app, ["extract", str(tmp_repo), "--format", "claude", "-o", str(output_dir)]) result = runner.invoke(app, ["extract", str(tmp_repo), "--format", "claude", "--force", "-o", str(output_dir)]) assert result.exit_code == 0 - assert "overwrote" in result.stdout or "wrote" in result.stdout + assert "Wrote" in result.stdout or "Updated" in result.stdout class TestPreservationMarkers: From 6829ed7b073307a9ee55f914a6dacd360ccd57fe Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Tue, 17 Mar 2026 17:36:21 -0400 Subject: [PATCH 3/3] fix: uniform verb casing across all commands -- Added/Captured/Wrote/Updated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit of all 11 commands: - add: 'added' → 'Added', shorter trailing hint - capture: 'captured'/'captured again' → 'Captured'/'Captured again' - diff: remove leading [bold]saar[/bold] prefix, add 2-space indent - enrich: 'done' → 'Done.' with 2-space indent - init: 'wrote' → 'Wrote' with dim line count Every command now: 2-space indent, capital verbs, no decorative borders. Matches industry standard: gh, cargo, bun, vercel. 548 tests passing. --- saar/commands/explore.py | 6 +++--- saar/commands/maintain.py | 10 +++++----- tests/test_capture.py | 2 +- tests/test_interview.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/saar/commands/explore.py b/saar/commands/explore.py index 9c58529..7dded64 100644 --- a/saar/commands/explore.py +++ b/saar/commands/explore.py @@ -53,7 +53,7 @@ def cmd_init( target.write_text(content, encoding="utf-8") line_count = len(content.splitlines()) console.print() - console.print(f" [green]wrote[/green] {target} ({line_count} lines)") + console.print(f" [green]Wrote[/green] {target} [dim]({line_count} lines)[/dim]") console.print() console.print(" [bold]Next steps:[/bold]") console.print(" [dim]1.[/dim] Drop this in your repo root — Claude Code + Cursor pick it up automatically") @@ -183,10 +183,10 @@ def cmd_capture( append_to_cache(repo_path, field_name, rule) if is_duplicate: - console.print(f" [yellow]captured again[/yellow] [{label}] {rule} [dim](×{entry.count} total)[/dim]") + console.print(f" [yellow]Captured again[/yellow] [{label}] {rule} [dim](×{entry.count} total)[/dim]") console.print(f" [dim]Claude has made this mistake {entry.count} times. Rule already in AGENTS.md.[/dim]") else: - console.print(f" [green]captured[/green] [{label}] {rule}") + console.print(f" [green]Captured[/green] [{label}] {rule}") if no_regen: console.print(" [dim]Skipped regeneration (--no-regen). Run [bold]saar extract . --no-interview[/bold] to update.[/dim]") diff --git a/saar/commands/maintain.py b/saar/commands/maintain.py index 9245f4d..732a108 100644 --- a/saar/commands/maintain.py +++ b/saar/commands/maintain.py @@ -52,8 +52,8 @@ def cmd_add( field, label = "never_do", "Never do" append_to_cache(repo_path, field, correction) - console.print(f" [green]added[/green] [{label}] {correction}") - console.print(" [dim]Saved to .saar/config.json. Re-run [bold]saar .[/bold] to regenerate context files.[/dim]") + console.print(f" [green]Added[/green] [{label}] {correction}") + console.print(" [dim]Saved to .saar/config.json. Re-run [bold]saar .[/bold] to regenerate.[/dim]") def cmd_diff( @@ -75,7 +75,7 @@ def cmd_diff( console.print("[yellow]No snapshot found.[/yellow] Run [bold]saar extract[/bold] first to create a baseline.") raise typer.Exit(code=0) - console.print(f"[bold]saar[/bold] checking [cyan]{repo_path.name}[/cyan] for changes...") + console.print(f" [bold]saar[/bold] checking [bold]{repo_path.name}[/bold] for changes...") extractor = DNAExtractor() dna = extractor.extract(str(repo_path)) @@ -129,5 +129,5 @@ def cmd_enrich( return save_cache(repo_path, enriched) - console.print("[bold green]done[/bold green] -- tribal knowledge enriched and saved.") - console.print("[dim]Re-run [bold]saar extract .[/bold] --no-interview to regenerate context files.[/dim]") + console.print(" [bold green]Done.[/bold green] Tribal knowledge enriched and saved.") + console.print(" [dim]Re-run [bold]saar extract . --no-interview[/bold] to regenerate context files.[/dim]") diff --git a/tests/test_capture.py b/tests/test_capture.py index b0bb9c0..fad9cfb 100644 --- a/tests/test_capture.py +++ b/tests/test_capture.py @@ -187,7 +187,7 @@ def test_capture_no_regen(self, tmp_path: Path): "--no-regen", ]) assert result.exit_code == 0 - assert "captured" in result.output + assert "Captured" in result.output or "captured" in result.output # check capture was saved entries = load_captures(tmp_path) assert len(entries) == 1 diff --git a/tests/test_interview.py b/tests/test_interview.py index 87d4f64..d2416d5 100644 --- a/tests/test_interview.py +++ b/tests/test_interview.py @@ -321,7 +321,7 @@ def test_add_default_goes_to_never_do(self, tmp_path: Path): runner = CliRunner() result = runner.invoke(app, ["add", "Never modify billing/", "--repo", str(tmp_path)]) assert result.exit_code == 0 - assert "added" in result.stdout + assert "Added" in result.stdout or "added" in result.stdout cached = load_cached(tmp_path) assert cached is not None assert "Never modify billing/" in cached.never_do