repr-cli 0.2.17__tar.gz → 0.2.19__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. {repr_cli-0.2.17 → repr_cli-0.2.19}/PKG-INFO +1 -1
  2. {repr_cli-0.2.17 → repr_cli-0.2.19}/pyproject.toml +1 -1
  3. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/__init__.py +1 -1
  4. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/cli.py +185 -51
  5. repr_cli-0.2.19/repr/dashboard/dist/assets/index-B-aCjaCw.js +384 -0
  6. repr_cli-0.2.19/repr/dashboard/dist/assets/index-C7Gzxc4f.js +384 -0
  7. repr_cli-0.2.19/repr/dashboard/dist/assets/index-CQdMXo6g.js +391 -0
  8. repr_cli-0.2.19/repr/dashboard/dist/assets/index-Cs8ofFGd.js +384 -0
  9. repr_cli-0.2.19/repr/dashboard/dist/assets/index-DwN0SeMc.css +1 -0
  10. repr_cli-0.2.19/repr/dashboard/dist/assets/index-YFch_e0S.js +384 -0
  11. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/dashboard/dist/index.html +2 -2
  12. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/dashboard/server.py +191 -0
  13. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr_cli.egg-info/PKG-INFO +1 -1
  14. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr_cli.egg-info/SOURCES.txt +6 -0
  15. {repr_cli-0.2.17 → repr_cli-0.2.19}/LICENSE +0 -0
  16. {repr_cli-0.2.17 → repr_cli-0.2.19}/README.md +0 -0
  17. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/__main__.py +0 -0
  18. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/api.py +0 -0
  19. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/auth.py +0 -0
  20. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/change_synthesis.py +0 -0
  21. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/config.py +0 -0
  22. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/configure.py +0 -0
  23. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/cron.py +0 -0
  24. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/dashboard/__init__.py +0 -0
  25. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/dashboard/build.py +0 -0
  26. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/dashboard/dist/assets/index-BYFVbEev.css +0 -0
  27. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/dashboard/dist/assets/index-BrrhyJFO.css +0 -0
  28. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/dashboard/dist/assets/index-CcEg74ts.js +0 -0
  29. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/dashboard/dist/assets/index-Cerc-iA_.js +0 -0
  30. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/dashboard/dist/assets/index-CjVcBW2L.css +0 -0
  31. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/dashboard/dist/assets/index-Dfl3mR5E.js +0 -0
  32. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/dashboard/dist/favicon.svg +0 -0
  33. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/dashboard/manager.py +0 -0
  34. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/db.py +0 -0
  35. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/discovery.py +0 -0
  36. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/doctor.py +0 -0
  37. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/extractor.py +0 -0
  38. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/hooks.py +0 -0
  39. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/keychain.py +0 -0
  40. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/llm.py +0 -0
  41. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/loaders/__init__.py +0 -0
  42. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/loaders/base.py +0 -0
  43. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/loaders/claude_code.py +0 -0
  44. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/loaders/clawdbot.py +0 -0
  45. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/loaders/gemini_antigravity.py +0 -0
  46. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/mcp_server.py +0 -0
  47. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/models.py +0 -0
  48. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/openai_analysis.py +0 -0
  49. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/privacy.py +0 -0
  50. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/session_extractor.py +0 -0
  51. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/storage.py +0 -0
  52. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/story_synthesis.py +0 -0
  53. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/telemetry.py +0 -0
  54. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/templates.py +0 -0
  55. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/timeline.py +0 -0
  56. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/tools.py +0 -0
  57. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/ui.py +0 -0
  58. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr/updater.py +0 -0
  59. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr_cli.egg-info/dependency_links.txt +0 -0
  60. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr_cli.egg-info/entry_points.txt +0 -0
  61. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr_cli.egg-info/requires.txt +0 -0
  62. {repr_cli-0.2.17 → repr_cli-0.2.19}/repr_cli.egg-info/top_level.txt +0 -0
  63. {repr_cli-0.2.17 → repr_cli-0.2.19}/setup.cfg +0 -0
  64. {repr_cli-0.2.17 → repr_cli-0.2.19}/setup.py +0 -0
  65. {repr_cli-0.2.17 → repr_cli-0.2.19}/tests/test_deduplication.py +0 -0
  66. {repr_cli-0.2.17 → repr_cli-0.2.19}/tests/test_environment_variables.py +0 -0
  67. {repr_cli-0.2.17 → repr_cli-0.2.19}/tests/test_network_sandboxing.py +0 -0
  68. {repr_cli-0.2.17 → repr_cli-0.2.19}/tests/test_privacy_guarantees.py +0 -0
  69. {repr_cli-0.2.17 → repr_cli-0.2.19}/tests/test_profile_export.py +0 -0
  70. {repr_cli-0.2.17 → repr_cli-0.2.19}/tests/test_repo_identity.py +0 -0
  71. {repr_cli-0.2.17 → repr_cli-0.2.19}/tests/test_stories_review.py +0 -0
  72. {repr_cli-0.2.17 → repr_cli-0.2.19}/tests/test_token_budget.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: repr-cli
3
- Version: 0.2.17
3
+ Version: 0.2.19
4
4
  Summary: A beautiful, privacy-first CLI that analyzes your code repositories and generates a compelling developer profile
5
5
  Author-email: Repr <hello@repr.dev>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "repr-cli"
7
- version = "0.2.17"
7
+ version = "0.2.19"
8
8
  description = "A beautiful, privacy-first CLI that analyzes your code repositories and generates a compelling developer profile"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -10,6 +10,6 @@ try:
10
10
  __version__ = version("repr-cli")
11
11
  except Exception:
12
12
  # Fallback for PyInstaller builds where metadata isn't available
13
- __version__ = "0.2.17"
13
+ __version__ = "0.2.18"
14
14
  __author__ = "Repr"
15
15
  __email__ = "hello@repr.dev"
@@ -933,24 +933,40 @@ def generate(
933
933
  console.print(" repr generate --local")
934
934
  raise typer.Exit(1)
935
935
 
936
- if not local and not cloud:
937
- # Check config for default mode
938
- llm_config = get_llm_config()
939
- default_mode = llm_config.get("default", "local")
936
+ # Resolve LLM mode and model from config
937
+ llm_config = get_llm_config()
938
+ default_mode = llm_config.get("default", "local")
939
+ byok_provider = None
940
940
 
941
+ if not local and not cloud:
941
942
  if default_mode == "local":
942
943
  local = True
943
944
  elif default_mode == "cloud" and is_authenticated() and is_cloud_allowed():
944
945
  cloud = True
946
+ elif default_mode.startswith("byok:"):
947
+ byok_provider = default_mode.split(":", 1)[1]
945
948
  else:
946
949
  # Fallback: local if not signed in or cloud not allowed
947
950
  local = True
948
-
951
+
952
+ # Determine model and provider string for display
953
+ if byok_provider:
954
+ from .config import get_byok_config, BYOK_PROVIDERS
955
+ byok_cfg = get_byok_config(byok_provider)
956
+ model_name = byok_cfg.get("model") if byok_cfg else None
957
+ provider_name = BYOK_PROVIDERS.get(byok_provider, {}).get("name", byok_provider)
958
+ mode_str = f"{provider_name}: {model_name}" if model_name else provider_name
959
+ elif local:
960
+ provider_name = llm_config.get("local_provider") or "Ollama"
961
+ model_name = llm_config.get("local_model") or "llama3.2"
962
+ mode_str = f"{provider_name}: {model_name}"
963
+ else:
964
+ model_name = llm_config.get("cloud_model") or "gpt-4o-mini"
965
+ mode_str = f"Cloud: {model_name}"
966
+
949
967
  if not json_output:
950
968
  print_header()
951
969
 
952
- mode_str = "local LLM" if local else "cloud LLM"
953
-
954
970
  # Build timeframe string for display
955
971
  if commits:
956
972
  timeframe_str = f"specific commits"
@@ -1073,13 +1089,8 @@ def generate(
1073
1089
  if not json_output:
1074
1090
  console.print(f" Batch {current}/{total}")
1075
1091
 
1076
- # Determine model based on mode (respect config default if no flag given)
1077
- llm_config = get_llm_config()
1078
- use_local = local or (not cloud and llm_config.get("default") == "local")
1079
- if use_local:
1080
- model = llm_config.get("local_model") or "llama3.2"
1081
- else:
1082
- model = None # Use default cloud model
1092
+ # Use model_name resolved earlier (handles local, cloud, and BYOK)
1093
+ model = model_name
1083
1094
 
1084
1095
  try:
1085
1096
  # Run synthesis sync
@@ -1981,6 +1992,165 @@ def push(
1981
1992
  print_success(f"Pushed {pushed}/{len(stories_payload)} stories")
1982
1993
 
1983
1994
 
1995
+ @app.command()
1996
+ def publish(
1997
+ story_id: Optional[str] = typer.Argument(None, help="Story ID to publish (omit for batch publish)"),
1998
+ all_stories: bool = typer.Option(False, "--all", "-a", help="Republish all stories, including already-pushed"),
1999
+ repo: Optional[str] = typer.Option(None, "--repo", "-r", help="Publish stories from specific repository"),
2000
+ visibility: Optional[str] = typer.Option(None, "--visibility", "-v", help="Override visibility: public, private, connections"),
2001
+ dry_run: bool = typer.Option(False, "--dry-run", help="Preview what would be published"),
2002
+ ):
2003
+ """
2004
+ Publish stories to repr.dev.
2005
+
2006
+ Publish = Upload to cloud with current visibility (or override).
2007
+
2008
+ Supports three scopes:
2009
+ - Global: Publish all unpushed stories (or all with --all)
2010
+ - Repo: Publish stories from a specific repository (--repo)
2011
+ - Story: Publish a single story by ID
2012
+
2013
+ Examples:
2014
+ repr publish # Unpushed stories only
2015
+ repr publish --all # All stories (re-publish)
2016
+ repr publish --repo myproject # Stories from specific repo
2017
+ repr publish 01HXYZ123 # Single story by ID
2018
+ repr publish --visibility public # Publish as public
2019
+ repr publish --repo myproject --dry-run # Preview
2020
+ """
2021
+ from .privacy import check_cloud_permission, log_cloud_operation
2022
+ from .api import push_stories_batch, APIError, AuthError
2023
+ from .db import get_db
2024
+
2025
+ # Check authentication
2026
+ if not get_access_token():
2027
+ print_error("Not authenticated")
2028
+ print_info("Run 'repr login' to authenticate")
2029
+ raise typer.Exit(1)
2030
+
2031
+ allowed, reason = check_cloud_permission("push")
2032
+ if not allowed:
2033
+ print_error("Publishing blocked")
2034
+ print_info(reason)
2035
+ raise typer.Exit(1)
2036
+
2037
+ # Validate visibility if provided
2038
+ valid_visibilities = {"public", "private", "connections"}
2039
+ if visibility and visibility not in valid_visibilities:
2040
+ print_error(f"Invalid visibility: {visibility}")
2041
+ print_info(f"Valid options: {', '.join(valid_visibilities)}")
2042
+ raise typer.Exit(1)
2043
+
2044
+ db = get_db()
2045
+
2046
+ # Determine which stories to publish based on scope
2047
+ if story_id:
2048
+ # Single story mode
2049
+ story = db.get_story(story_id)
2050
+ if not story:
2051
+ print_error(f"Story not found: {story_id}")
2052
+ raise typer.Exit(1)
2053
+ all_stories_list = [story]
2054
+ scope_desc = f"story {story_id[:8]}..."
2055
+ elif repo:
2056
+ # Repo mode - get stories from specific repo
2057
+ projects = db.list_projects()
2058
+ project_ids = [p["id"] for p in projects if p["name"] == repo]
2059
+ if not project_ids:
2060
+ print_error(f"Repository not found: {repo}")
2061
+ print_info("Use 'repr repos list' to see tracked repositories")
2062
+ raise typer.Exit(1)
2063
+ all_stories_list = [s for s in db.list_stories(limit=10000) if s.project_id in project_ids]
2064
+ scope_desc = f"repo '{repo}'"
2065
+ else:
2066
+ # Global mode
2067
+ all_stories_list = db.list_stories(limit=10000)
2068
+ scope_desc = "all repositories"
2069
+
2070
+ if not all_stories_list:
2071
+ print_info(f"No stories found for {scope_desc}")
2072
+ raise typer.Exit()
2073
+
2074
+ # Filter to unpushed unless --all is specified
2075
+ # Note: For now we don't track pushed_at in the schema, so --all just means republish everything
2076
+ # In future, we could filter by pushed_at column
2077
+ stories_to_publish = all_stories_list
2078
+
2079
+ if not stories_to_publish:
2080
+ print_info("No stories to publish")
2081
+ raise typer.Exit()
2082
+
2083
+ console.print(f"Found [bold]{len(stories_to_publish)}[/] stories from {scope_desc}")
2084
+ console.print()
2085
+
2086
+ if dry_run:
2087
+ console.print("[dim]Preview (dry-run):[/]")
2088
+ for story in stories_to_publish[:20]: # Limit preview to 20
2089
+ vis = visibility or story.visibility or "private"
2090
+ console.print(f" • [{vis}] {story.title[:55]}...")
2091
+ if len(stories_to_publish) > 20:
2092
+ console.print(f" ... and {len(stories_to_publish) - 20} more")
2093
+ console.print()
2094
+ console.print("Run without --dry-run to publish")
2095
+ raise typer.Exit()
2096
+
2097
+ # Build batch payload
2098
+ vis_label = visibility or "default"
2099
+ console.print(f"Publishing with visibility: [bold]{vis_label}[/]...")
2100
+
2101
+ stories_payload = []
2102
+ for story in stories_to_publish:
2103
+ payload = story.model_dump(mode="json")
2104
+ # Use override visibility or story's current visibility
2105
+ payload["visibility"] = visibility or story.visibility or "private"
2106
+ payload["client_id"] = story.id
2107
+ stories_payload.append(payload)
2108
+
2109
+ # Push all stories in batch with progress
2110
+ console.print(f"Publishing {len(stories_payload)} stories...")
2111
+ console.print()
2112
+
2113
+ with BatchProgress() as progress:
2114
+ try:
2115
+ result = asyncio.run(push_stories_batch(stories_payload))
2116
+ pushed = result.get("pushed", 0)
2117
+ failed = result.get("failed", 0)
2118
+ results = result.get("results", [])
2119
+
2120
+ # Display results
2121
+ for i, story_result in enumerate(results):
2122
+ story_title = stories_to_publish[i].title[:50] if i < len(stories_to_publish) else "Unknown"
2123
+ if story_result.get("success"):
2124
+ console.print(f" [{BRAND_SUCCESS}]✓[/] {story_title}")
2125
+ else:
2126
+ error_msg = story_result.get("error", "Unknown error")
2127
+ console.print(f" [{BRAND_ERROR}]✗[/] {story_title}: {error_msg}")
2128
+
2129
+ except (APIError, AuthError) as e:
2130
+ print_error(f"Publish failed: {e}")
2131
+ raise typer.Exit(1)
2132
+
2133
+ # Log operation
2134
+ if pushed > 0:
2135
+ log_cloud_operation(
2136
+ operation="publish",
2137
+ destination="repr.dev",
2138
+ payload_summary={
2139
+ "stories_published": pushed,
2140
+ "visibility": visibility or "default",
2141
+ "scope": "story" if story_id else ("repo" if repo else "global"),
2142
+ "repo": repo,
2143
+ },
2144
+ bytes_sent=0,
2145
+ )
2146
+
2147
+ console.print()
2148
+ if failed > 0:
2149
+ print_warning(f"Published {pushed}/{len(stories_payload)} stories ({failed} failed)")
2150
+ else:
2151
+ print_success(f"Published {pushed} stories to repr.dev")
2152
+
2153
+
1984
2154
  @app.command()
1985
2155
  def sync():
1986
2156
  """
@@ -3724,45 +3894,9 @@ def profile_link():
3724
3894
 
3725
3895
 
3726
3896
  # =============================================================================
3727
- # PUBLISH/UNPUBLISH COMMANDS
3897
+ # UNPUBLISH COMMAND
3728
3898
  # =============================================================================
3729
3899
 
3730
- @app.command("publish")
3731
- def publish_story(
3732
- story_id: str = typer.Argument(..., help="Story ID to publish"),
3733
- visibility: str = typer.Option("public", "--visibility", "-v", help="Visibility: public, friends, private"),
3734
- ):
3735
- """
3736
- Set story visibility.
3737
-
3738
- Examples:
3739
- repr publish abc123
3740
- repr publish abc123 --visibility friends
3741
- """
3742
- from .api import set_story_visibility, AuthError
3743
-
3744
- if visibility not in ("public", "friends", "private"):
3745
- print_error(f"Invalid visibility: {visibility}")
3746
- print_info("Valid options: public, friends, private")
3747
- raise typer.Exit(1)
3748
-
3749
- if not is_authenticated():
3750
- print_error("Not authenticated")
3751
- print_info("Run `repr login` first")
3752
- raise typer.Exit(1)
3753
-
3754
- try:
3755
- with create_spinner(f"Setting visibility to {visibility}..."):
3756
- result = asyncio.run(set_story_visibility(story_id, visibility))
3757
- print_success(f"Story visibility set to {visibility}")
3758
- except AuthError as e:
3759
- print_error(str(e))
3760
- raise typer.Exit(1)
3761
- except APIError as e:
3762
- print_error(f"API error: {e}")
3763
- raise typer.Exit(1)
3764
-
3765
-
3766
3900
  @app.command("unpublish")
3767
3901
  def unpublish_story(
3768
3902
  story_id: str = typer.Argument(..., help="Story ID to unpublish"),