repr-cli 0.2.15__py3-none-any.whl → 0.2.17__py3-none-any.whl

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 (43) hide show
  1. repr/__init__.py +1 -1
  2. repr/api.py +363 -62
  3. repr/auth.py +47 -38
  4. repr/change_synthesis.py +478 -0
  5. repr/cli.py +4103 -267
  6. repr/config.py +119 -11
  7. repr/configure.py +889 -0
  8. repr/cron.py +419 -0
  9. repr/dashboard/__init__.py +9 -0
  10. repr/dashboard/build.py +126 -0
  11. repr/dashboard/dist/assets/index-BYFVbEev.css +1 -0
  12. repr/dashboard/dist/assets/index-BrrhyJFO.css +1 -0
  13. repr/dashboard/dist/assets/index-CcEg74ts.js +270 -0
  14. repr/dashboard/dist/assets/index-Cerc-iA_.js +377 -0
  15. repr/dashboard/dist/assets/index-CjVcBW2L.css +1 -0
  16. repr/dashboard/dist/assets/index-Dfl3mR5E.js +377 -0
  17. repr/dashboard/dist/favicon.svg +4 -0
  18. repr/dashboard/dist/index.html +14 -0
  19. repr/dashboard/manager.py +234 -0
  20. repr/dashboard/server.py +1298 -0
  21. repr/db.py +980 -0
  22. repr/hooks.py +3 -2
  23. repr/loaders/__init__.py +22 -0
  24. repr/loaders/base.py +156 -0
  25. repr/loaders/claude_code.py +287 -0
  26. repr/loaders/clawdbot.py +313 -0
  27. repr/loaders/gemini_antigravity.py +381 -0
  28. repr/mcp_server.py +1196 -0
  29. repr/models.py +503 -0
  30. repr/openai_analysis.py +25 -0
  31. repr/session_extractor.py +481 -0
  32. repr/storage.py +360 -0
  33. repr/story_synthesis.py +1296 -0
  34. repr/templates.py +68 -4
  35. repr/timeline.py +710 -0
  36. repr/tools.py +17 -8
  37. {repr_cli-0.2.15.dist-info → repr_cli-0.2.17.dist-info}/METADATA +50 -10
  38. repr_cli-0.2.17.dist-info/RECORD +52 -0
  39. {repr_cli-0.2.15.dist-info → repr_cli-0.2.17.dist-info}/WHEEL +1 -1
  40. {repr_cli-0.2.15.dist-info → repr_cli-0.2.17.dist-info}/entry_points.txt +1 -0
  41. repr_cli-0.2.15.dist-info/RECORD +0 -26
  42. {repr_cli-0.2.15.dist-info → repr_cli-0.2.17.dist-info}/licenses/LICENSE +0 -0
  43. {repr_cli-0.2.15.dist-info → repr_cli-0.2.17.dist-info}/top_level.txt +0 -0
repr/config.py CHANGED
@@ -2,8 +2,10 @@
2
2
  Configuration management for ~/.repr/ directory.
3
3
  """
4
4
 
5
+ import hashlib
5
6
  import json
6
7
  import os
8
+ import subprocess
7
9
  import tempfile
8
10
  from datetime import datetime
9
11
  from pathlib import Path
@@ -108,11 +110,17 @@ DEFAULT_CONFIG = {
108
110
  },
109
111
  "generation": {
110
112
  "batch_size": 5, # Commits per story
111
- "auto_generate_on_hook": False, # Auto-generate when hook runs
113
+ "auto_generate_on_hook": False, # Auto-generate when hook runs (prefer cron)
112
114
  "default_template": "resume", # Default story template
113
115
  "token_limit": 100000, # Max tokens per cloud request
114
116
  "max_commits_per_batch": 50, # Max commits per request
115
117
  },
118
+ "cron": {
119
+ "installed": False,
120
+ "paused": False,
121
+ "interval_hours": None,
122
+ "min_commits": None,
123
+ },
116
124
  "publish": {
117
125
  "on_generate": "never", # "never", "prompt", "always"
118
126
  "on_sync": "prompt", # "never", "prompt", "always"
@@ -120,7 +128,9 @@ DEFAULT_CONFIG = {
120
128
  "privacy": {
121
129
  "lock_local_only": False, # Disable cloud features entirely
122
130
  "lock_permanent": False, # Make local-only lock irreversible
123
- "profile_visibility": "public", # "public", "unlisted", "private"
131
+ "profile_visibility": "private", # "public", "private", "connections"
132
+ "repos_default_visibility": "private", # Default visibility for new repos
133
+ "stories_default_visibility": "private", # Default visibility for new stories
124
134
  "telemetry_enabled": False, # Opt-in telemetry
125
135
  },
126
136
  "tracked_repos": [], # List of {"path": str, "last_sync": str|None, "hook_installed": bool, "paused": bool}
@@ -278,29 +288,31 @@ def set_auth(
278
288
  access_token: str,
279
289
  user_id: str,
280
290
  email: str,
291
+ username: str | None = None,
281
292
  litellm_api_key: str | None = None,
282
293
  ) -> None:
283
294
  """Store authentication info securely."""
284
295
  from .keychain import store_secret
285
-
296
+
286
297
  config = load_config()
287
-
298
+
288
299
  # Store token in keychain
289
300
  keychain_ref = f"auth_token_{user_id[:8]}"
290
301
  store_secret(keychain_ref, access_token)
291
-
302
+
292
303
  config["auth"] = {
293
304
  "token_keychain_ref": keychain_ref,
294
305
  "user_id": user_id,
295
306
  "email": email,
307
+ "username": username,
296
308
  "authenticated_at": datetime.now().isoformat(),
297
309
  }
298
-
310
+
299
311
  if litellm_api_key:
300
312
  litellm_ref = f"litellm_key_{user_id[:8]}"
301
313
  store_secret(litellm_ref, litellm_api_key)
302
314
  config["auth"]["litellm_keychain_ref"] = litellm_ref
303
-
315
+
304
316
  save_config(config)
305
317
 
306
318
 
@@ -486,12 +498,17 @@ BYOK_PROVIDERS = {
486
498
  },
487
499
  "anthropic": {
488
500
  "name": "Anthropic",
489
- "default_model": "claude-3-sonnet-20240229",
501
+ "default_model": "claude-sonnet-4-20250514",
490
502
  "base_url": "https://api.anthropic.com/v1",
491
503
  },
504
+ "gemini": {
505
+ "name": "Google Gemini",
506
+ "default_model": "gemini-1.5-flash",
507
+ "base_url": "https://generativelanguage.googleapis.com/v1beta",
508
+ },
492
509
  "groq": {
493
510
  "name": "Groq",
494
- "default_model": "llama-3.1-70b-versatile",
511
+ "default_model": "llama-3.3-70b-versatile",
495
512
  "base_url": "https://api.groq.com/openai/v1",
496
513
  },
497
514
  "together": {
@@ -499,6 +516,11 @@ BYOK_PROVIDERS = {
499
516
  "default_model": "meta-llama/Llama-3-70b-chat-hf",
500
517
  "base_url": "https://api.together.xyz/v1",
501
518
  },
519
+ "openrouter": {
520
+ "name": "OpenRouter",
521
+ "default_model": "anthropic/claude-3.5-sonnet",
522
+ "base_url": "https://openrouter.ai/api/v1",
523
+ },
502
524
  }
503
525
 
504
526
 
@@ -617,14 +639,100 @@ def set_profile_config(**kwargs) -> None:
617
639
  config = load_config()
618
640
  if "profile" not in config:
619
641
  config["profile"] = DEFAULT_CONFIG["profile"].copy()
620
-
642
+
621
643
  for key, value in kwargs.items():
622
644
  if key in DEFAULT_CONFIG["profile"]:
623
645
  config["profile"][key] = value
624
-
646
+
625
647
  save_config(config)
626
648
 
627
649
 
650
+ # Word lists for mnemonic username generation
651
+ _ADJECTIVES = [
652
+ "swift", "brave", "calm", "keen", "bold", "wise", "warm", "cool",
653
+ "bright", "quiet", "noble", "vivid", "lucid", "agile", "witty",
654
+ "gentle", "steady", "clever", "humble", "serene", "cosmic", "lunar",
655
+ "stellar", "golden", "silver", "crystal", "amber", "azure", "coral",
656
+ "crimson", "emerald", "ivory", "jade", "onyx", "pearl", "ruby",
657
+ ]
658
+
659
+ _NOUNS = [
660
+ "falcon", "wolf", "bear", "hawk", "tiger", "lion", "eagle", "raven",
661
+ "phoenix", "dragon", "panther", "cobra", "viper", "lynx", "fox",
662
+ "mountain", "river", "ocean", "forest", "desert", "glacier", "canyon",
663
+ "meadow", "valley", "summit", "thunder", "storm", "aurora", "comet",
664
+ "nebula", "quasar", "pulsar", "nova", "orbit", "zenith", "vertex",
665
+ ]
666
+
667
+
668
+ def get_gpg_fingerprint() -> str | None:
669
+ """Get the GPG signing key fingerprint from git config."""
670
+ try:
671
+ # First check if git has a signing key configured
672
+ result = subprocess.run(
673
+ ["git", "config", "--get", "user.signingkey"],
674
+ capture_output=True,
675
+ text=True,
676
+ timeout=5,
677
+ )
678
+ if result.returncode != 0 or not result.stdout.strip():
679
+ return None
680
+
681
+ key_id = result.stdout.strip()
682
+
683
+ # Get the full fingerprint from GPG
684
+ result = subprocess.run(
685
+ ["gpg", "--fingerprint", "--with-colons", key_id],
686
+ capture_output=True,
687
+ text=True,
688
+ timeout=5,
689
+ )
690
+ if result.returncode != 0:
691
+ return None
692
+
693
+ # Parse the fingerprint from colon-delimited output
694
+ for line in result.stdout.split("\n"):
695
+ if line.startswith("fpr:"):
696
+ return line.split(":")[9]
697
+
698
+ return None
699
+ except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
700
+ return None
701
+
702
+
703
+ def derive_mnemonic_from_key(fingerprint: str) -> str:
704
+ """Derive a deterministic human-friendly name from a key fingerprint."""
705
+ # Hash the fingerprint to get consistent bytes
706
+ hash_bytes = hashlib.sha256(fingerprint.encode()).digest()
707
+
708
+ # Use first bytes to select adjective and noun
709
+ adj_idx = hash_bytes[0] % len(_ADJECTIVES)
710
+ noun_idx = hash_bytes[1] % len(_NOUNS)
711
+
712
+ return f"{_ADJECTIVES[adj_idx]}-{_NOUNS[noun_idx]}"
713
+
714
+
715
+ def get_or_generate_username() -> str:
716
+ """
717
+ Get username with fallback chain:
718
+ 1. Profile username (claimed or set)
719
+ 2. GPG-derived mnemonic (deterministic from signing key)
720
+ 3. None (caller should use git author or other fallback)
721
+ """
722
+ profile = get_profile_config()
723
+
724
+ # 1. Check for set username
725
+ if username := profile.get("username"):
726
+ return username
727
+
728
+ # 2. Try to derive from GPG key
729
+ if fingerprint := get_gpg_fingerprint():
730
+ return derive_mnemonic_from_key(fingerprint)
731
+
732
+ # 3. No deterministic identity available
733
+ return None
734
+
735
+
628
736
  def get_skip_patterns() -> list[str]:
629
737
  """Get list of patterns to skip during discovery."""
630
738
  config = load_config()