repr-cli 0.1.0__py3-none-any.whl → 0.2.1__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.
- repr/__init__.py +1 -1
- repr/api.py +127 -1
- repr/auth.py +66 -2
- repr/cli.py +2143 -663
- repr/config.py +658 -32
- repr/discovery.py +5 -0
- repr/doctor.py +458 -0
- repr/hooks.py +634 -0
- repr/keychain.py +255 -0
- repr/llm.py +506 -0
- repr/openai_analysis.py +92 -21
- repr/privacy.py +333 -0
- repr/storage.py +527 -0
- repr/templates.py +229 -0
- repr/tools.py +202 -0
- repr/ui.py +79 -364
- repr_cli-0.2.1.dist-info/METADATA +263 -0
- repr_cli-0.2.1.dist-info/RECORD +23 -0
- {repr_cli-0.1.0.dist-info → repr_cli-0.2.1.dist-info}/licenses/LICENSE +1 -1
- repr/analyzer.py +0 -915
- repr/highlights.py +0 -712
- repr_cli-0.1.0.dist-info/METADATA +0 -326
- repr_cli-0.1.0.dist-info/RECORD +0 -18
- {repr_cli-0.1.0.dist-info → repr_cli-0.2.1.dist-info}/WHEEL +0 -0
- {repr_cli-0.1.0.dist-info → repr_cli-0.2.1.dist-info}/entry_points.txt +0 -0
- {repr_cli-0.1.0.dist-info → repr_cli-0.2.1.dist-info}/top_level.txt +0 -0
repr/templates.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Story generation templates.
|
|
3
|
+
|
|
4
|
+
Provides different prompts for generating stories based on use case:
|
|
5
|
+
- resume: Professional accomplishment summaries
|
|
6
|
+
- changelog: Technical change documentation
|
|
7
|
+
- narrative: Storytelling format
|
|
8
|
+
- interview: Behavioral interview preparation
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Template definitions
|
|
15
|
+
TEMPLATES = {
|
|
16
|
+
"resume": {
|
|
17
|
+
"name": "Resume",
|
|
18
|
+
"description": "Professional accomplishment summaries for resumes and portfolios",
|
|
19
|
+
"system_prompt": """You are helping a developer document their work accomplishments for a professional resume or portfolio.
|
|
20
|
+
|
|
21
|
+
Focus on:
|
|
22
|
+
- Quantifiable impact where possible (performance improvements, user metrics, etc.)
|
|
23
|
+
- Technical complexity and problem-solving
|
|
24
|
+
- Leadership and collaboration
|
|
25
|
+
- Technologies and skills demonstrated
|
|
26
|
+
|
|
27
|
+
Write in first person, using action verbs. Keep it concise and impactful.
|
|
28
|
+
Format: 2-3 bullet points per accomplishment, each 1-2 sentences.""",
|
|
29
|
+
"user_prompt_template": """Based on these commits, write professional accomplishment summaries:
|
|
30
|
+
|
|
31
|
+
Repository: {repo_name}
|
|
32
|
+
Commits:
|
|
33
|
+
{commits_summary}
|
|
34
|
+
|
|
35
|
+
Generate 1-3 accomplishment summaries suitable for a resume.""",
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
"changelog": {
|
|
39
|
+
"name": "Changelog",
|
|
40
|
+
"description": "Technical change documentation for release notes",
|
|
41
|
+
"system_prompt": """You are writing technical changelog entries for a software project.
|
|
42
|
+
|
|
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
|
+
Commits:
|
|
59
|
+
{commits_summary}
|
|
60
|
+
|
|
61
|
+
Write changelog entries grouped by category.""",
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
"narrative": {
|
|
65
|
+
"name": "Narrative",
|
|
66
|
+
"description": "Storytelling format for blogs or case studies",
|
|
67
|
+
"system_prompt": """You are helping a developer tell the story of their work in an engaging narrative format.
|
|
68
|
+
|
|
69
|
+
Focus on:
|
|
70
|
+
- The challenge or problem being solved
|
|
71
|
+
- The approach and decision-making process
|
|
72
|
+
- Obstacles encountered and how they were overcome
|
|
73
|
+
- Results and lessons learned
|
|
74
|
+
|
|
75
|
+
Write in a conversational, engaging tone suitable for a blog post or case study.
|
|
76
|
+
Use present tense for engagement. Include technical details but make it accessible.""",
|
|
77
|
+
"user_prompt_template": """Tell the story of this development work:
|
|
78
|
+
|
|
79
|
+
Repository: {repo_name}
|
|
80
|
+
Commits:
|
|
81
|
+
{commits_summary}
|
|
82
|
+
|
|
83
|
+
Write a narrative (2-3 paragraphs) that would work as a blog post section.""",
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
"interview": {
|
|
87
|
+
"name": "Interview Prep",
|
|
88
|
+
"description": "Behavioral interview preparation with STAR format",
|
|
89
|
+
"system_prompt": """You are helping a developer prepare for behavioral interviews using the STAR method.
|
|
90
|
+
|
|
91
|
+
Format each accomplishment as:
|
|
92
|
+
- Situation: Context and background
|
|
93
|
+
- Task: What needed to be done
|
|
94
|
+
- Action: What you did specifically
|
|
95
|
+
- Result: The outcome and impact
|
|
96
|
+
|
|
97
|
+
Focus on:
|
|
98
|
+
- Technical decision-making
|
|
99
|
+
- Problem-solving approach
|
|
100
|
+
- Collaboration and communication
|
|
101
|
+
- Quantifiable results""",
|
|
102
|
+
"user_prompt_template": """Create interview-ready stories from these commits:
|
|
103
|
+
|
|
104
|
+
Repository: {repo_name}
|
|
105
|
+
Commits:
|
|
106
|
+
{commits_summary}
|
|
107
|
+
|
|
108
|
+
Generate 1-2 STAR-format stories for behavioral interviews.""",
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Default template
|
|
113
|
+
DEFAULT_TEMPLATE = "resume"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_template(name: str) -> dict[str, Any] | None:
|
|
117
|
+
"""
|
|
118
|
+
Get a template by name.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
name: Template name
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Template dict or None if not found
|
|
125
|
+
"""
|
|
126
|
+
return TEMPLATES.get(name.lower())
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def list_templates() -> list[dict[str, str]]:
|
|
130
|
+
"""
|
|
131
|
+
List all available templates.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of template info dicts
|
|
135
|
+
"""
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
"name": key,
|
|
139
|
+
"display_name": tmpl["name"],
|
|
140
|
+
"description": tmpl["description"],
|
|
141
|
+
}
|
|
142
|
+
for key, tmpl in TEMPLATES.items()
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def format_commits_for_prompt(commits: list[dict[str, Any]]) -> str:
|
|
147
|
+
"""
|
|
148
|
+
Format commit list for inclusion in prompt.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
commits: List of commit dicts
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Formatted string for prompt
|
|
155
|
+
"""
|
|
156
|
+
lines = []
|
|
157
|
+
for c in commits:
|
|
158
|
+
sha = c.get("sha", c.get("full_sha", ""))[:7]
|
|
159
|
+
msg = c.get("message", "").split("\n")[0][:80]
|
|
160
|
+
date = c.get("date", "")[:10]
|
|
161
|
+
files = c.get("files", [])
|
|
162
|
+
|
|
163
|
+
lines.append(f"- [{sha}] {msg}")
|
|
164
|
+
if files:
|
|
165
|
+
lines.append(f" Files: {', '.join(files[:5])}")
|
|
166
|
+
if len(files) > 5:
|
|
167
|
+
lines.append(f" ... and {len(files) - 5} more files")
|
|
168
|
+
if c.get("insertions") or c.get("deletions"):
|
|
169
|
+
lines.append(f" Changes: +{c.get('insertions', 0)}/-{c.get('deletions', 0)}")
|
|
170
|
+
|
|
171
|
+
return "\n".join(lines)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def build_generation_prompt(
|
|
175
|
+
template_name: str,
|
|
176
|
+
repo_name: str,
|
|
177
|
+
commits: list[dict[str, Any]],
|
|
178
|
+
custom_prompt: str | None = None,
|
|
179
|
+
) -> tuple[str, str]:
|
|
180
|
+
"""
|
|
181
|
+
Build the system and user prompts for story generation.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
template_name: Name of template to use
|
|
185
|
+
repo_name: Repository name
|
|
186
|
+
commits: List of commit dicts
|
|
187
|
+
custom_prompt: Optional custom prompt to append
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Tuple of (system_prompt, user_prompt)
|
|
191
|
+
"""
|
|
192
|
+
template = get_template(template_name)
|
|
193
|
+
if not template:
|
|
194
|
+
template = TEMPLATES[DEFAULT_TEMPLATE]
|
|
195
|
+
|
|
196
|
+
system_prompt = template["system_prompt"]
|
|
197
|
+
|
|
198
|
+
# Format commits
|
|
199
|
+
commits_summary = format_commits_for_prompt(commits)
|
|
200
|
+
|
|
201
|
+
# Build user prompt
|
|
202
|
+
user_prompt = template["user_prompt_template"].format(
|
|
203
|
+
repo_name=repo_name,
|
|
204
|
+
commits_summary=commits_summary,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Append custom prompt if provided
|
|
208
|
+
if custom_prompt:
|
|
209
|
+
user_prompt += f"\n\nAdditional instructions: {custom_prompt}"
|
|
210
|
+
|
|
211
|
+
return system_prompt, user_prompt
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def get_template_help() -> str:
|
|
215
|
+
"""
|
|
216
|
+
Get help text describing all templates.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Formatted help string
|
|
220
|
+
"""
|
|
221
|
+
lines = ["Available templates:\n"]
|
|
222
|
+
|
|
223
|
+
for key, tmpl in TEMPLATES.items():
|
|
224
|
+
lines.append(f" {key}")
|
|
225
|
+
lines.append(f" {tmpl['description']}")
|
|
226
|
+
lines.append("")
|
|
227
|
+
|
|
228
|
+
return "\n".join(lines)
|
|
229
|
+
|
repr/tools.py
CHANGED
|
@@ -112,6 +112,7 @@ def get_commits_with_diffs(
|
|
|
112
112
|
days: int = 365,
|
|
113
113
|
max_diff_lines_per_file: int = 50,
|
|
114
114
|
max_files_per_commit: int = 10,
|
|
115
|
+
since: str | None = None,
|
|
115
116
|
) -> list[dict[str, Any]]:
|
|
116
117
|
"""
|
|
117
118
|
Get commits with actual diff content for LLM analysis.
|
|
@@ -122,6 +123,7 @@ def get_commits_with_diffs(
|
|
|
122
123
|
days: Number of days to look back (default 365 for 1 year)
|
|
123
124
|
max_diff_lines_per_file: Maximum diff lines to include per file
|
|
124
125
|
max_files_per_commit: Maximum files to include per commit
|
|
126
|
+
since: Optional SHA or date string to start from (only analyze commits after this)
|
|
125
127
|
|
|
126
128
|
Returns:
|
|
127
129
|
List of commit objects with diffs
|
|
@@ -133,11 +135,31 @@ def get_commits_with_diffs(
|
|
|
133
135
|
cutoff_date = datetime.now() - timedelta(days=days)
|
|
134
136
|
cutoff_timestamp = cutoff_date.timestamp()
|
|
135
137
|
|
|
138
|
+
# Parse since parameter if provided
|
|
139
|
+
since_timestamp: float | None = None
|
|
140
|
+
since_sha: str | None = None
|
|
141
|
+
if since:
|
|
142
|
+
# Try to parse as date first
|
|
143
|
+
try:
|
|
144
|
+
since_date = datetime.fromisoformat(since.replace("Z", "+00:00"))
|
|
145
|
+
since_timestamp = since_date.timestamp()
|
|
146
|
+
except ValueError:
|
|
147
|
+
# Treat as SHA
|
|
148
|
+
since_sha = since
|
|
149
|
+
|
|
136
150
|
for commit in repo.iter_commits(max_count=count):
|
|
137
151
|
# Stop if we've gone past the time window
|
|
138
152
|
if commit.committed_date < cutoff_timestamp:
|
|
139
153
|
break
|
|
140
154
|
|
|
155
|
+
# Stop if we've reached the since SHA
|
|
156
|
+
if since_sha and commit.hexsha.startswith(since_sha):
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
# Skip commits before the since timestamp
|
|
160
|
+
if since_timestamp and commit.committed_date <= since_timestamp:
|
|
161
|
+
break
|
|
162
|
+
|
|
141
163
|
# Get files changed with diffs
|
|
142
164
|
files = []
|
|
143
165
|
parent = commit.parents[0] if commit.parents else None
|
|
@@ -444,3 +466,183 @@ def get_dependencies(
|
|
|
444
466
|
"""
|
|
445
467
|
return detect_dependencies(repo_path)
|
|
446
468
|
|
|
469
|
+
|
|
470
|
+
def get_commits_by_shas(
|
|
471
|
+
repo_path: Path,
|
|
472
|
+
shas: list[str],
|
|
473
|
+
max_diff_lines_per_file: int = 50,
|
|
474
|
+
max_files_per_commit: int = 10,
|
|
475
|
+
) -> list[dict[str, Any]]:
|
|
476
|
+
"""
|
|
477
|
+
Get specific commits by their SHAs with diffs.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
repo_path: Path to the repository
|
|
481
|
+
shas: List of commit SHAs (full or short)
|
|
482
|
+
max_diff_lines_per_file: Maximum diff lines to include per file
|
|
483
|
+
max_files_per_commit: Maximum files to include per commit
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
List of commit objects with diffs in the order of SHAs provided
|
|
487
|
+
"""
|
|
488
|
+
repo = Repo(repo_path)
|
|
489
|
+
commits = []
|
|
490
|
+
|
|
491
|
+
for sha in shas:
|
|
492
|
+
try:
|
|
493
|
+
commit = repo.commit(sha)
|
|
494
|
+
except Exception:
|
|
495
|
+
# Skip invalid SHAs
|
|
496
|
+
continue
|
|
497
|
+
|
|
498
|
+
# Get files changed with diffs
|
|
499
|
+
files = []
|
|
500
|
+
parent = commit.parents[0] if commit.parents else None
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
# Get diff for this commit
|
|
504
|
+
if parent:
|
|
505
|
+
diffs = parent.diff(commit, create_patch=True)
|
|
506
|
+
else:
|
|
507
|
+
# Initial commit - show all files as additions
|
|
508
|
+
diffs = commit.diff(None, create_patch=True)
|
|
509
|
+
|
|
510
|
+
for diff_item in list(diffs)[:max_files_per_commit]:
|
|
511
|
+
file_path = diff_item.b_path or diff_item.a_path
|
|
512
|
+
if not file_path:
|
|
513
|
+
continue
|
|
514
|
+
|
|
515
|
+
# Get the diff text
|
|
516
|
+
diff_text = ""
|
|
517
|
+
if diff_item.diff:
|
|
518
|
+
try:
|
|
519
|
+
diff_text = diff_item.diff.decode('utf-8', errors='ignore')
|
|
520
|
+
except:
|
|
521
|
+
diff_text = str(diff_item.diff)
|
|
522
|
+
|
|
523
|
+
# Truncate diff if too long
|
|
524
|
+
diff_lines = diff_text.split('\n')
|
|
525
|
+
if len(diff_lines) > max_diff_lines_per_file:
|
|
526
|
+
diff_text = '\n'.join(diff_lines[:max_diff_lines_per_file])
|
|
527
|
+
diff_text += f"\n... ({len(diff_lines) - max_diff_lines_per_file} more lines)"
|
|
528
|
+
|
|
529
|
+
files.append({
|
|
530
|
+
"path": file_path,
|
|
531
|
+
"change_type": diff_item.change_type, # A=added, D=deleted, M=modified, R=renamed
|
|
532
|
+
"diff": diff_text,
|
|
533
|
+
})
|
|
534
|
+
except (GitCommandError, Exception):
|
|
535
|
+
# If we can't get diff, just include file list
|
|
536
|
+
for filename in commit.stats.files.keys():
|
|
537
|
+
if len(files) >= max_files_per_commit:
|
|
538
|
+
break
|
|
539
|
+
files.append({
|
|
540
|
+
"path": filename,
|
|
541
|
+
"change_type": "M",
|
|
542
|
+
"diff": "",
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
commits.append({
|
|
546
|
+
"sha": commit.hexsha[:8],
|
|
547
|
+
"full_sha": commit.hexsha,
|
|
548
|
+
"message": commit.message.strip(),
|
|
549
|
+
"author": commit.author.name,
|
|
550
|
+
"author_email": commit.author.email,
|
|
551
|
+
"date": datetime.fromtimestamp(commit.committed_date).isoformat(),
|
|
552
|
+
"files": files,
|
|
553
|
+
"insertions": commit.stats.total["insertions"],
|
|
554
|
+
"deletions": commit.stats.total["deletions"],
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
return commits
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def suggest_related_commits(
|
|
561
|
+
repo_path: Path,
|
|
562
|
+
limit: int = 50,
|
|
563
|
+
min_overlap: int = 2,
|
|
564
|
+
) -> list[dict[str, Any]]:
|
|
565
|
+
"""
|
|
566
|
+
Suggest groups of related commits based on file overlap.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
repo_path: Path to the repository
|
|
570
|
+
limit: Maximum commits to analyze
|
|
571
|
+
min_overlap: Minimum number of overlapping files to consider commits related
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
List of suggested commit groups with overlap info
|
|
575
|
+
"""
|
|
576
|
+
from collections import defaultdict
|
|
577
|
+
|
|
578
|
+
# Get recent commits with file info
|
|
579
|
+
commits = get_commits_with_diffs(repo_path, count=limit, days=90)
|
|
580
|
+
|
|
581
|
+
if len(commits) < 2:
|
|
582
|
+
return []
|
|
583
|
+
|
|
584
|
+
# Build file -> commits mapping
|
|
585
|
+
file_commits: dict[str, list[int]] = defaultdict(list)
|
|
586
|
+
for idx, commit in enumerate(commits):
|
|
587
|
+
files = commit.get('files', [])
|
|
588
|
+
for file_info in files:
|
|
589
|
+
file_path = file_info.get('path', '')
|
|
590
|
+
if file_path:
|
|
591
|
+
file_commits[file_path].append(idx)
|
|
592
|
+
|
|
593
|
+
# Find commit pairs with overlapping files
|
|
594
|
+
overlap_scores: dict[tuple[int, int], int] = defaultdict(int)
|
|
595
|
+
overlapping_files: dict[tuple[int, int], set[str]] = defaultdict(set)
|
|
596
|
+
|
|
597
|
+
for file_path, commit_indices in file_commits.items():
|
|
598
|
+
if len(commit_indices) > 1:
|
|
599
|
+
# These commits touch the same file
|
|
600
|
+
for i in range(len(commit_indices)):
|
|
601
|
+
for j in range(i + 1, len(commit_indices)):
|
|
602
|
+
idx1, idx2 = commit_indices[i], commit_indices[j]
|
|
603
|
+
pair = (min(idx1, idx2), max(idx1, idx2))
|
|
604
|
+
overlap_scores[pair] += 1
|
|
605
|
+
overlapping_files[pair].add(file_path)
|
|
606
|
+
|
|
607
|
+
# Filter by minimum overlap and create suggestions
|
|
608
|
+
suggestions = []
|
|
609
|
+
seen_commits = set()
|
|
610
|
+
|
|
611
|
+
for (idx1, idx2), score in sorted(overlap_scores.items(), key=lambda x: -x[1]):
|
|
612
|
+
if score < min_overlap:
|
|
613
|
+
continue
|
|
614
|
+
|
|
615
|
+
# Skip if commits already in a group
|
|
616
|
+
if idx1 in seen_commits or idx2 in seen_commits:
|
|
617
|
+
continue
|
|
618
|
+
|
|
619
|
+
# Find other related commits
|
|
620
|
+
related = [idx1, idx2]
|
|
621
|
+
for idx3 in range(len(commits)):
|
|
622
|
+
if idx3 in seen_commits or idx3 == idx1 or idx3 == idx2:
|
|
623
|
+
continue
|
|
624
|
+
|
|
625
|
+
# Check overlap with any commit in the group
|
|
626
|
+
overlap_with_group = False
|
|
627
|
+
for group_idx in related:
|
|
628
|
+
pair = (min(idx3, group_idx), max(idx3, group_idx))
|
|
629
|
+
if pair in overlap_scores and overlap_scores[pair] >= min_overlap:
|
|
630
|
+
overlap_with_group = True
|
|
631
|
+
break
|
|
632
|
+
|
|
633
|
+
if overlap_with_group:
|
|
634
|
+
related.append(idx3)
|
|
635
|
+
|
|
636
|
+
if len(related) >= 2:
|
|
637
|
+
suggestion = {
|
|
638
|
+
'commit_shas': [commits[i]['full_sha'] for i in related],
|
|
639
|
+
'commit_count': len(related),
|
|
640
|
+
'overlap_score': score,
|
|
641
|
+
'overlapping_files': list(overlapping_files[(idx1, idx2)]),
|
|
642
|
+
'summary': f"{len(related)} related commits on {', '.join(list(overlapping_files[(idx1, idx2)])[:3])}",
|
|
643
|
+
'first_commit_message': commits[related[0]]['message'].split('\n')[0],
|
|
644
|
+
}
|
|
645
|
+
suggestions.append(suggestion)
|
|
646
|
+
seen_commits.update(related)
|
|
647
|
+
|
|
648
|
+
return suggestions[:10] # Return top 10 suggestions
|