Skip to content

Commit 7e4b6c3

Browse files
committed
feat: saar extract --index -- one-command OCI bootstrap (OPE-173)
The current setup path took 2+ hours of debugging (OPE-166). This makes it one flag on a command the user already runs. saar extract ./my-repo --index Flow: 1. Check ~/.saar/config.yaml for OCI API key 2. If no key: show how to get one, exit gracefully 3. Detect git remote URL (SSH -> HTTPS conversion automatic) 4. POST /api/v1/repos -- add repo to OCI 5. POST /api/v1/repos/{id}/index -- trigger indexing with progress 6. Save repo_id to .saar/config.json for future MCP use 7. Extract always completes -- OCI failure is non-fatal New module: saar/oci_client.py - load/save ~/.saar/config.yaml (oci_api_key, oci_base_url) - get_api_key() / get_base_url() with defaults - detect_git_url(): SSH -> HTTPS conversion, graceful on failure - detect_default_branch(): reads HEAD, falls back to 'main' - add_repository() / poll_until_indexed(): clean HTTP wrappers - save_repo_id() / load_repo_id(): .saar/config.json integration - OCIAuthError / OCIAPIError: clean error hierarchy saar/cli.py: - --index flag on extract command - _run_oci_indexing() helper: all OCI logic isolated, never breaks extract Added tests/test_oci_client.py with 25 tests: - Config read/write (round-trip, comments, corrupt file) - Repo ID persistence (save/load, preserves existing fields) - Git URL detection (SSH conversion, HTTPS passthrough, failure modes) - Branch detection (success, failure fallback) - CLI: no key shows guidance, OCI failure doesn't break extract, no git remote shows guidance, happy path shows indexed count, repo_id saved on success
1 parent 72222f5 commit 7e4b6c3

6 files changed

Lines changed: 701 additions & 3 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "saar"
7-
version = "0.3.6"
7+
version = "0.3.7"
88
description = "Extract the essence of your codebase. Auto-generate AGENTS.md, CLAUDE.md, .cursorrules and more."
99
readme = "README.md"
1010
license = "MIT"

saar/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Saar -- extract the essence of your codebase."""
22

3-
__version__ = "0.3.6"
3+
__version__ = "0.3.7"

saar/cli.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,11 @@ def extract(
320320
help="Max lines in generated file (default 100). 0 = unlimited. --verbose overrides to 0.",
321321
min=0,
322322
),
323+
index: bool = typer.Option(
324+
False,
325+
"--index",
326+
help="After extraction, index this repo into OCI for AI-powered semantic search via MCP. Requires OCI API key in ~/.saar/config.yaml.",
327+
),
323328
) -> None:
324329
"""Analyze a codebase and extract its architectural DNA."""
325330
log_level = logging.DEBUG if verbose else logging.WARNING
@@ -410,6 +415,10 @@ def extract(
410415
except Exception:
411416
pass # snapshot failure must never break extract
412417

418+
# -- optional OCI indexing (--index flag) --
419+
if index:
420+
_run_oci_indexing(repo_path, console)
421+
413422
console.print("[bold green]done[/bold green]")
414423

415424

@@ -611,3 +620,103 @@ def _write_with_markers(
611620

612621
target.write_text(before + wrapped + after, encoding="utf-8")
613622
console.print(f" [green]updated[/green] {target} (preserved manual edits)")
623+
624+
625+
def _run_oci_indexing(repo_path: Path, console) -> None:
626+
"""Handle the --index flag: add repo to OCI and trigger indexing.
627+
628+
Fails gracefully -- a failure here must never prevent saar extract
629+
from completing successfully. The AGENTS.md has already been written.
630+
"""
631+
from saar.oci_client import (
632+
get_api_key, get_base_url,
633+
detect_git_url, detect_default_branch,
634+
add_repository, poll_until_indexed,
635+
save_repo_id, load_repo_id,
636+
OCIAuthError, OCIAPIError,
637+
)
638+
639+
console.print()
640+
console.print("[bold]OCI indexing[/bold]")
641+
642+
# -- check for API key --
643+
api_key = get_api_key()
644+
if not api_key:
645+
console.print(
646+
" [yellow]No OCI API key found.[/yellow]\n"
647+
" 1. Go to [link=https://opencodeintel.com/dashboard/api-keys]opencodeintel.com/dashboard/api-keys[/link]\n"
648+
" 2. Generate a key and save it:\n"
649+
" [dim]echo 'oci_api_key: ci_your_key_here' >> ~/.saar/config.yaml[/dim]\n"
650+
" 3. Re-run with [bold]--index[/bold]"
651+
)
652+
return
653+
654+
base_url = get_base_url()
655+
656+
# -- detect git URL --
657+
git_url = detect_git_url(repo_path)
658+
if not git_url:
659+
console.print(
660+
" [yellow]Could not detect git remote URL.[/yellow]\n"
661+
" Make sure this repo has an 'origin' remote:\n"
662+
" [dim]git remote add origin https://github.com/you/your-repo.git[/dim]"
663+
)
664+
return
665+
666+
branch = detect_default_branch(repo_path)
667+
repo_name = repo_path.name
668+
669+
console.print(f" repo: [cyan]{git_url}[/cyan]")
670+
console.print(f" branch: [cyan]{branch}[/cyan]")
671+
672+
try:
673+
# Check if already indexed -- avoid duplicate repos
674+
existing_repo_id = load_repo_id(repo_path)
675+
if existing_repo_id:
676+
console.print(f" [dim]Already in OCI (repo_id: {existing_repo_id[:8]}...). Re-indexing...[/dim]")
677+
repo_id = existing_repo_id
678+
else:
679+
# Add repo
680+
console.print(" Adding to OCI...")
681+
repo = add_repository(
682+
name=repo_name,
683+
git_url=git_url,
684+
branch=branch,
685+
api_key=api_key,
686+
base_url=base_url,
687+
)
688+
repo_id = repo.get("id") or repo.get("repo_id")
689+
if not repo_id:
690+
raise OCIAPIError("No repo_id returned from API")
691+
save_repo_id(repo_path, repo_id)
692+
console.print(f" [green]Added[/green] (id: {repo_id[:8]}...)")
693+
694+
# Trigger indexing
695+
console.print(" Indexing...")
696+
697+
def on_tick(elapsed: int, status: str) -> None:
698+
console.print(f" [dim] {elapsed}s -- {status}...[/dim]", end="\r")
699+
700+
result = poll_until_indexed(
701+
repo_id=repo_id,
702+
api_key=api_key,
703+
base_url=base_url,
704+
on_tick=on_tick,
705+
)
706+
707+
functions = result.get("total_functions") or result.get("function_count", 0)
708+
console.print(f"\n [green]Indexed[/green] {functions:,} functions")
709+
console.print(
710+
" [dim]Use codeintel:search_code in Claude Desktop / Claude Code "
711+
"to query this repo via MCP.[/dim]"
712+
)
713+
714+
except OCIAuthError as e:
715+
console.print(f" [red]Auth error:[/red] {e}")
716+
console.print(" Get a new key at [link=https://opencodeintel.com/dashboard/api-keys]opencodeintel.com/dashboard/api-keys[/link]")
717+
except OCIAPIError as e:
718+
console.print(f" [yellow]OCI indexing skipped:[/yellow] {e}")
719+
console.print(" AGENTS.md was still generated successfully.")
720+
except Exception as e:
721+
console.print(f" [yellow]OCI indexing skipped:[/yellow] {e}")
722+
console.print(" AGENTS.md was still generated successfully.")

0 commit comments

Comments
 (0)