repr-cli 0.2.11__tar.gz → 0.2.13__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.
- {repr_cli-0.2.11/repr_cli.egg-info → repr_cli-0.2.13}/PKG-INFO +1 -1
- {repr_cli-0.2.11 → repr_cli-0.2.13}/pyproject.toml +1 -1
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/__main__.py +2 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/auth.py +17 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/doctor.py +2 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/hooks.py +99 -1
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/llm.py +2 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/privacy.py +2 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/telemetry.py +2 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/templates.py +60 -56
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/ui.py +2 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/updater.py +2 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13/repr_cli.egg-info}/PKG-INFO +1 -1
- {repr_cli-0.2.11 → repr_cli-0.2.13}/setup.py +2 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/LICENSE +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/README.md +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/__init__.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/api.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/cli.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/config.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/discovery.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/extractor.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/keychain.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/openai_analysis.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/storage.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr/tools.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr_cli.egg-info/SOURCES.txt +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr_cli.egg-info/dependency_links.txt +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr_cli.egg-info/entry_points.txt +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr_cli.egg-info/requires.txt +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/repr_cli.egg-info/top_level.txt +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/setup.cfg +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/tests/test_environment_variables.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/tests/test_network_sandboxing.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/tests/test_privacy_guarantees.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/tests/test_profile_export.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/tests/test_repo_identity.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/tests/test_stories_review.py +0 -0
- {repr_cli-0.2.11 → repr_cli-0.2.13}/tests/test_token_budget.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "repr-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.13"
|
|
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"}
|
|
@@ -5,12 +5,15 @@ Tokens are stored securely in OS keychain (see keychain.py).
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
|
+
import platform
|
|
9
|
+
import socket
|
|
8
10
|
import time
|
|
9
11
|
from dataclasses import dataclass
|
|
10
12
|
|
|
11
13
|
import httpx
|
|
12
14
|
|
|
13
15
|
from .config import set_auth, clear_auth, get_auth, is_authenticated, get_api_base
|
|
16
|
+
from .telemetry import get_device_id
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
def _get_device_code_url() -> str:
|
|
@@ -25,6 +28,16 @@ POLL_INTERVAL = 5 # seconds
|
|
|
25
28
|
MAX_POLL_TIME = 600 # 10 minutes
|
|
26
29
|
|
|
27
30
|
|
|
31
|
+
def _get_device_name() -> str:
|
|
32
|
+
"""Get a friendly device name for display."""
|
|
33
|
+
try:
|
|
34
|
+
hostname = socket.gethostname()
|
|
35
|
+
system = platform.system()
|
|
36
|
+
return f"{hostname} ({system})"
|
|
37
|
+
except Exception:
|
|
38
|
+
return "Unknown Device"
|
|
39
|
+
|
|
40
|
+
|
|
28
41
|
@dataclass
|
|
29
42
|
class DeviceCodeResponse:
|
|
30
43
|
"""Response from device code request."""
|
|
@@ -106,6 +119,8 @@ async def poll_for_token(device_code: str, interval: int = POLL_INTERVAL) -> Tok
|
|
|
106
119
|
json={
|
|
107
120
|
"device_code": device_code,
|
|
108
121
|
"client_id": "repr-cli",
|
|
122
|
+
"device_id": get_device_id(),
|
|
123
|
+
"device_name": _get_device_name(),
|
|
109
124
|
},
|
|
110
125
|
timeout=30,
|
|
111
126
|
)
|
|
@@ -314,6 +329,8 @@ class AuthFlow:
|
|
|
314
329
|
json={
|
|
315
330
|
"device_code": device_code_response.device_code,
|
|
316
331
|
"client_id": "repr-cli",
|
|
332
|
+
"device_id": get_device_id(),
|
|
333
|
+
"device_name": _get_device_name(),
|
|
317
334
|
},
|
|
318
335
|
timeout=30,
|
|
319
336
|
)
|
|
@@ -423,6 +423,8 @@ def queue_commit(repo_path: Path, commit_sha: str, message: str | None = None) -
|
|
|
423
423
|
"""Add a commit to the queue.
|
|
424
424
|
|
|
425
425
|
Uses file locking to handle concurrent commits safely.
|
|
426
|
+
If auto_generate_on_hook is enabled and queue size meets batch_size,
|
|
427
|
+
triggers background story generation.
|
|
426
428
|
|
|
427
429
|
Args:
|
|
428
430
|
repo_path: Path to repository root
|
|
@@ -435,6 +437,8 @@ def queue_commit(repo_path: Path, commit_sha: str, message: str | None = None) -
|
|
|
435
437
|
queue_path = get_queue_path(repo_path)
|
|
436
438
|
lock_path = queue_path.with_suffix(".lock")
|
|
437
439
|
|
|
440
|
+
queue_size = 0
|
|
441
|
+
|
|
438
442
|
try:
|
|
439
443
|
fd = _acquire_lock(lock_path)
|
|
440
444
|
try:
|
|
@@ -452,7 +456,7 @@ def queue_commit(repo_path: Path, commit_sha: str, message: str | None = None) -
|
|
|
452
456
|
})
|
|
453
457
|
|
|
454
458
|
save_queue(repo_path, queue)
|
|
455
|
-
|
|
459
|
+
queue_size = len(queue)
|
|
456
460
|
|
|
457
461
|
finally:
|
|
458
462
|
_release_lock(fd)
|
|
@@ -460,6 +464,100 @@ def queue_commit(repo_path: Path, commit_sha: str, message: str | None = None) -
|
|
|
460
464
|
except QueueLockError:
|
|
461
465
|
# Could not get lock, skip queuing
|
|
462
466
|
return False
|
|
467
|
+
|
|
468
|
+
# Check if we should auto-generate stories
|
|
469
|
+
_maybe_auto_generate(repo_path, queue_size)
|
|
470
|
+
|
|
471
|
+
return True
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _maybe_auto_generate(repo_path: Path, queue_size: int) -> None:
|
|
475
|
+
"""Check if auto-generation should be triggered and spawn background process.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
repo_path: Path to repository root
|
|
479
|
+
queue_size: Current number of commits in queue
|
|
480
|
+
"""
|
|
481
|
+
from .config import load_config
|
|
482
|
+
|
|
483
|
+
config = load_config()
|
|
484
|
+
generation_config = config.get("generation", {})
|
|
485
|
+
|
|
486
|
+
# Check if auto-generation is enabled
|
|
487
|
+
if not generation_config.get("auto_generate_on_hook", False):
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
batch_size = generation_config.get("batch_size", 5)
|
|
491
|
+
|
|
492
|
+
# Only trigger if queue size meets batch threshold
|
|
493
|
+
if queue_size < batch_size:
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
# Spawn background generation process
|
|
497
|
+
_spawn_background_generate(repo_path)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _spawn_background_generate(repo_path: Path) -> None:
|
|
501
|
+
"""Spawn a background process to generate stories for a repo.
|
|
502
|
+
|
|
503
|
+
Uses subprocess.Popen with detached process to not block the git hook.
|
|
504
|
+
Logs to ~/.repr/logs/auto_generate.log
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
repo_path: Path to repository to generate stories for
|
|
508
|
+
"""
|
|
509
|
+
import subprocess
|
|
510
|
+
import sys
|
|
511
|
+
from .config import REPR_HOME
|
|
512
|
+
|
|
513
|
+
# Ensure log directory exists
|
|
514
|
+
log_dir = REPR_HOME / "logs"
|
|
515
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
516
|
+
log_file = log_dir / "auto_generate.log"
|
|
517
|
+
|
|
518
|
+
# Build command - use sys.executable to find repr command
|
|
519
|
+
# repr generate --repo <path> --local --json
|
|
520
|
+
cmd = [
|
|
521
|
+
sys.executable, "-m", "repr",
|
|
522
|
+
"generate",
|
|
523
|
+
"--repo", str(repo_path),
|
|
524
|
+
"--local", # Always use local LLM for auto-generation
|
|
525
|
+
"--json",
|
|
526
|
+
]
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
# Open log file for appending
|
|
530
|
+
with open(log_file, "a") as log:
|
|
531
|
+
log.write(f"\n[{datetime.now().isoformat()}] Auto-generating for {repo_path}\n")
|
|
532
|
+
log.flush()
|
|
533
|
+
|
|
534
|
+
# Spawn detached background process
|
|
535
|
+
# On Unix, use start_new_session=True to fully detach
|
|
536
|
+
# On Windows, use DETACHED_PROCESS flag
|
|
537
|
+
if sys.platform == "win32":
|
|
538
|
+
DETACHED_PROCESS = 0x00000008
|
|
539
|
+
subprocess.Popen(
|
|
540
|
+
cmd,
|
|
541
|
+
stdout=log,
|
|
542
|
+
stderr=log,
|
|
543
|
+
creationflags=DETACHED_PROCESS,
|
|
544
|
+
close_fds=True,
|
|
545
|
+
)
|
|
546
|
+
else:
|
|
547
|
+
subprocess.Popen(
|
|
548
|
+
cmd,
|
|
549
|
+
stdout=log,
|
|
550
|
+
stderr=log,
|
|
551
|
+
start_new_session=True,
|
|
552
|
+
close_fds=True,
|
|
553
|
+
)
|
|
554
|
+
except Exception as e:
|
|
555
|
+
# Silently fail - don't block the git commit
|
|
556
|
+
try:
|
|
557
|
+
with open(log_file, "a") as log:
|
|
558
|
+
log.write(f"[{datetime.now().isoformat()}] Error spawning auto-generate: {e}\n")
|
|
559
|
+
except Exception:
|
|
560
|
+
pass
|
|
463
561
|
|
|
464
562
|
|
|
465
563
|
def dequeue_commits(repo_path: Path, commit_shas: list[str]) -> int:
|
|
@@ -9,103 +9,107 @@ Provides different prompts for generating stories based on use case:
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from typing import Any
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StoryOutput(BaseModel):
|
|
16
|
+
"""Structured output for a generated story."""
|
|
17
|
+
summary: str = Field(description="One-line technical summary of the work (max 120 chars, no fluff)")
|
|
18
|
+
content: str = Field(description="Full technical description in markdown")
|
|
12
19
|
|
|
13
20
|
|
|
14
21
|
# Template definitions
|
|
15
22
|
TEMPLATES = {
|
|
16
23
|
"resume": {
|
|
17
24
|
"name": "Resume",
|
|
18
|
-
"description": "
|
|
19
|
-
"system_prompt": """
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
"description": "Technical work log for resumes and portfolios",
|
|
26
|
+
"system_prompt": """Extract technical work from commits. Be direct and specific.
|
|
27
|
+
|
|
28
|
+
Output JSON with:
|
|
29
|
+
- summary: One line, max 120 chars. State what was done technically. No adjectives, no fluff.
|
|
30
|
+
Good: "Added JWT refresh token rotation with Redis session store"
|
|
31
|
+
Bad: "Enhanced authentication system with improved security"
|
|
32
|
+
- content: Markdown with technical details. What was built, how, what tech.
|
|
33
|
+
|
|
34
|
+
Rules:
|
|
35
|
+
- Name specific technologies, libraries, patterns
|
|
36
|
+
- Describe the implementation, not the benefit
|
|
37
|
+
- No marketing language (enhanced, streamlined, robust, seamless)
|
|
38
|
+
- No resume verbs (spearheaded, leveraged, drove)
|
|
39
|
+
- If there's a metric, include it. If not, don't invent one.""",
|
|
40
|
+
"user_prompt_template": """Repository: {repo_name}
|
|
30
41
|
|
|
31
|
-
Repository: {repo_name}
|
|
32
42
|
Commits:
|
|
33
43
|
{commits_summary}
|
|
34
44
|
|
|
35
|
-
|
|
45
|
+
Output JSON with summary and content.""",
|
|
36
46
|
},
|
|
37
47
|
|
|
38
48
|
"changelog": {
|
|
39
49
|
"name": "Changelog",
|
|
40
50
|
"description": "Technical change documentation for release notes",
|
|
41
|
-
"system_prompt": """
|
|
51
|
+
"system_prompt": """Extract changes from commits for a changelog. Be specific.
|
|
52
|
+
|
|
53
|
+
Output JSON with:
|
|
54
|
+
- summary: One line describing the main change (max 120 chars)
|
|
55
|
+
- content: Markdown changelog with categories (Added/Changed/Fixed/Removed)
|
|
56
|
+
|
|
57
|
+
Rules:
|
|
58
|
+
- List actual changes, not benefits
|
|
59
|
+
- Include file/module names when relevant
|
|
60
|
+
- No fluff words (improved, enhanced, better)""",
|
|
61
|
+
"user_prompt_template": """Repository: {repo_name}
|
|
42
62
|
|
|
43
|
-
Focus on:
|
|
44
|
-
- What changed (features, fixes, improvements)
|
|
45
|
-
- Why it matters (user impact, developer experience)
|
|
46
|
-
- Breaking changes or migration notes
|
|
47
|
-
- Technical details relevant to other developers
|
|
48
|
-
|
|
49
|
-
Use conventional changelog format with categories:
|
|
50
|
-
- Added: New features
|
|
51
|
-
- Changed: Changes to existing functionality
|
|
52
|
-
- Fixed: Bug fixes
|
|
53
|
-
- Removed: Removed features
|
|
54
|
-
- Security: Security improvements""",
|
|
55
|
-
"user_prompt_template": """Generate changelog entries from these commits:
|
|
56
|
-
|
|
57
|
-
Repository: {repo_name}
|
|
58
63
|
Commits:
|
|
59
64
|
{commits_summary}
|
|
60
65
|
|
|
61
|
-
|
|
66
|
+
Output JSON with summary and content.""",
|
|
62
67
|
},
|
|
63
68
|
|
|
64
69
|
"narrative": {
|
|
65
70
|
"name": "Narrative",
|
|
66
|
-
"description": "
|
|
67
|
-
"system_prompt": """
|
|
71
|
+
"description": "Technical narrative for blogs or case studies",
|
|
72
|
+
"system_prompt": """Write a technical narrative from commits.
|
|
73
|
+
|
|
74
|
+
Output JSON with:
|
|
75
|
+
- summary: One-line description of what was built (max 120 chars)
|
|
76
|
+
- content: Markdown narrative explaining the technical work
|
|
68
77
|
|
|
69
78
|
Focus on:
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
- Results and lessons learned
|
|
79
|
+
- What problem was solved
|
|
80
|
+
- How it was implemented technically
|
|
81
|
+
- What decisions were made and why
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"user_prompt_template": """Tell the story of this development work:
|
|
83
|
+
No marketing language. Write like you're explaining to another engineer.""",
|
|
84
|
+
"user_prompt_template": """Repository: {repo_name}
|
|
78
85
|
|
|
79
|
-
Repository: {repo_name}
|
|
80
86
|
Commits:
|
|
81
87
|
{commits_summary}
|
|
82
88
|
|
|
83
|
-
|
|
89
|
+
Output JSON with summary and content.""",
|
|
84
90
|
},
|
|
85
91
|
|
|
86
92
|
"interview": {
|
|
87
93
|
"name": "Interview Prep",
|
|
88
|
-
"description": "
|
|
89
|
-
"system_prompt": """
|
|
94
|
+
"description": "Technical interview preparation",
|
|
95
|
+
"system_prompt": """Extract technical work for interview prep.
|
|
90
96
|
|
|
91
|
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
- Action: What you did specifically
|
|
95
|
-
- Result: The outcome and impact
|
|
97
|
+
Output JSON with:
|
|
98
|
+
- summary: One-line technical summary (max 120 chars)
|
|
99
|
+
- content: Markdown with situation/task/action/result format
|
|
96
100
|
|
|
97
101
|
Focus on:
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
- Specific technical decisions made
|
|
103
|
+
- Problems encountered and solutions
|
|
104
|
+
- Technologies and patterns used
|
|
105
|
+
|
|
106
|
+
No resume language. Be specific about what you actually did.""",
|
|
107
|
+
"user_prompt_template": """Repository: {repo_name}
|
|
103
108
|
|
|
104
|
-
Repository: {repo_name}
|
|
105
109
|
Commits:
|
|
106
110
|
{commits_summary}
|
|
107
111
|
|
|
108
|
-
|
|
112
|
+
Output JSON with summary and content.""",
|
|
109
113
|
},
|
|
110
114
|
}
|
|
111
115
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|