repr-cli 0.2.8__py3-none-any.whl → 0.2.9__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/__main__.py +37 -0
- repr/cli.py +350 -218
- repr/doctor.py +37 -0
- repr/llm.py +37 -0
- repr/openai_analysis.py +32 -23
- repr/privacy.py +37 -0
- repr/telemetry.py +37 -0
- repr/templates.py +6 -1
- repr/ui.py +37 -0
- repr/updater.py +37 -0
- {repr_cli-0.2.8.dist-info → repr_cli-0.2.9.dist-info}/METADATA +19 -5
- repr_cli-0.2.9.dist-info/RECORD +26 -0
- repr_cli-0.2.8.dist-info/RECORD +0 -26
- {repr_cli-0.2.8.dist-info → repr_cli-0.2.9.dist-info}/WHEEL +0 -0
- {repr_cli-0.2.8.dist-info → repr_cli-0.2.9.dist-info}/entry_points.txt +0 -0
- {repr_cli-0.2.8.dist-info → repr_cli-0.2.9.dist-info}/licenses/LICENSE +0 -0
- {repr_cli-0.2.8.dist-info → repr_cli-0.2.9.dist-info}/top_level.txt +0 -0
repr/__main__.py
CHANGED
repr/cli.py
CHANGED
|
@@ -21,7 +21,8 @@ import os
|
|
|
21
21
|
import sys
|
|
22
22
|
from datetime import datetime, timedelta
|
|
23
23
|
from pathlib import Path
|
|
24
|
-
from typing import Optional, List
|
|
24
|
+
from typing import Optional, List, Dict
|
|
25
|
+
from collections import defaultdict
|
|
25
26
|
|
|
26
27
|
import typer
|
|
27
28
|
from rich.prompt import Confirm, Prompt
|
|
@@ -247,6 +248,81 @@ def init(
|
|
|
247
248
|
# GENERATE
|
|
248
249
|
# =============================================================================
|
|
249
250
|
|
|
251
|
+
def _parse_date_reference(date_str: str) -> str | None:
|
|
252
|
+
"""
|
|
253
|
+
Parse a date reference string into an ISO date string.
|
|
254
|
+
|
|
255
|
+
Supports:
|
|
256
|
+
- ISO dates: "2024-01-01"
|
|
257
|
+
- Day names: "monday", "tuesday", etc.
|
|
258
|
+
- Relative: "3 days ago", "2 weeks ago", "1 month ago"
|
|
259
|
+
|
|
260
|
+
Returns ISO date string or None if parsing fails.
|
|
261
|
+
"""
|
|
262
|
+
import re
|
|
263
|
+
from datetime import datetime, timedelta
|
|
264
|
+
|
|
265
|
+
date_str = date_str.lower().strip()
|
|
266
|
+
|
|
267
|
+
# Try ISO format first
|
|
268
|
+
try:
|
|
269
|
+
parsed = datetime.fromisoformat(date_str)
|
|
270
|
+
return parsed.isoformat()
|
|
271
|
+
except ValueError:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
# Day names (find previous occurrence)
|
|
275
|
+
day_names = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
|
|
276
|
+
if date_str in day_names:
|
|
277
|
+
today = datetime.now()
|
|
278
|
+
target_day = day_names.index(date_str)
|
|
279
|
+
current_day = today.weekday()
|
|
280
|
+
|
|
281
|
+
# Calculate days back to that day
|
|
282
|
+
days_back = (current_day - target_day) % 7
|
|
283
|
+
if days_back == 0:
|
|
284
|
+
days_back = 7 # Go to last week's occurrence
|
|
285
|
+
|
|
286
|
+
target_date = today - timedelta(days=days_back)
|
|
287
|
+
return target_date.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
|
|
288
|
+
|
|
289
|
+
# Relative time: "N days/weeks/months ago"
|
|
290
|
+
match = re.match(r"(\d+)\s+(day|days|week|weeks|month|months)\s+ago", date_str)
|
|
291
|
+
if match:
|
|
292
|
+
amount = int(match.group(1))
|
|
293
|
+
unit = match.group(2).rstrip("s") # Normalize to singular
|
|
294
|
+
|
|
295
|
+
if unit == "day":
|
|
296
|
+
delta = timedelta(days=amount)
|
|
297
|
+
elif unit == "week":
|
|
298
|
+
delta = timedelta(weeks=amount)
|
|
299
|
+
elif unit == "month":
|
|
300
|
+
delta = timedelta(days=amount * 30) # Approximate
|
|
301
|
+
else:
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
target_date = datetime.now() - delta
|
|
305
|
+
return target_date.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
|
|
306
|
+
|
|
307
|
+
# "yesterday" / "today"
|
|
308
|
+
if date_str == "yesterday":
|
|
309
|
+
target_date = datetime.now() - timedelta(days=1)
|
|
310
|
+
return target_date.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
|
|
311
|
+
if date_str == "today":
|
|
312
|
+
target_date = datetime.now()
|
|
313
|
+
return target_date.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
|
|
314
|
+
|
|
315
|
+
# "last week" / "last month"
|
|
316
|
+
if date_str == "last week":
|
|
317
|
+
target_date = datetime.now() - timedelta(weeks=1)
|
|
318
|
+
return target_date.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
|
|
319
|
+
if date_str == "last month":
|
|
320
|
+
target_date = datetime.now() - timedelta(days=30)
|
|
321
|
+
return target_date.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
|
|
322
|
+
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
|
|
250
326
|
@app.command()
|
|
251
327
|
def generate(
|
|
252
328
|
local: bool = typer.Option(
|
|
@@ -267,6 +343,14 @@ def generate(
|
|
|
267
343
|
None, "--commits",
|
|
268
344
|
help="Generate from specific commits (comma-separated)",
|
|
269
345
|
),
|
|
346
|
+
since_date: Optional[str] = typer.Option(
|
|
347
|
+
None, "--since",
|
|
348
|
+
help="Generate from commits since date (e.g., '2024-01-01', 'monday', '2 weeks ago')",
|
|
349
|
+
),
|
|
350
|
+
days: Optional[int] = typer.Option(
|
|
351
|
+
None, "--days",
|
|
352
|
+
help="Generate from commits in the last N days",
|
|
353
|
+
),
|
|
270
354
|
batch_size: int = typer.Option(
|
|
271
355
|
5, "--batch-size",
|
|
272
356
|
help="Commits per story",
|
|
@@ -283,6 +367,10 @@ def generate(
|
|
|
283
367
|
None, "--prompt", "-p",
|
|
284
368
|
help="Custom prompt to append",
|
|
285
369
|
),
|
|
370
|
+
json_output: bool = typer.Option(
|
|
371
|
+
False, "--json",
|
|
372
|
+
help="Output as JSON",
|
|
373
|
+
),
|
|
286
374
|
):
|
|
287
375
|
"""
|
|
288
376
|
Generate stories from commits.
|
|
@@ -290,6 +378,8 @@ def generate(
|
|
|
290
378
|
Examples:
|
|
291
379
|
repr generate --local
|
|
292
380
|
repr generate --cloud
|
|
381
|
+
repr generate --since "2 weeks ago"
|
|
382
|
+
repr generate --days 30
|
|
293
383
|
repr generate --template changelog
|
|
294
384
|
repr generate --commits abc123,def456
|
|
295
385
|
"""
|
|
@@ -299,6 +389,9 @@ def generate(
|
|
|
299
389
|
if cloud:
|
|
300
390
|
allowed, reason = check_cloud_permission("cloud_generation")
|
|
301
391
|
if not allowed:
|
|
392
|
+
if json_output:
|
|
393
|
+
print(json.dumps({"generated": 0, "stories": [], "error": f"Cloud generation blocked: {reason}"}, indent=2))
|
|
394
|
+
raise typer.Exit(1)
|
|
302
395
|
print_error("Cloud generation blocked")
|
|
303
396
|
print_info(reason)
|
|
304
397
|
console.print()
|
|
@@ -313,11 +406,24 @@ def generate(
|
|
|
313
406
|
else:
|
|
314
407
|
local = True
|
|
315
408
|
|
|
316
|
-
|
|
409
|
+
if not json_output:
|
|
410
|
+
print_header()
|
|
317
411
|
|
|
318
412
|
mode_str = "local LLM" if local else "cloud LLM"
|
|
319
|
-
|
|
320
|
-
|
|
413
|
+
|
|
414
|
+
# Build timeframe string for display
|
|
415
|
+
if commits:
|
|
416
|
+
timeframe_str = f"specific commits"
|
|
417
|
+
elif since_date:
|
|
418
|
+
timeframe_str = f"since {since_date}"
|
|
419
|
+
elif days:
|
|
420
|
+
timeframe_str = f"last {days} days"
|
|
421
|
+
else:
|
|
422
|
+
timeframe_str = "last 90 days"
|
|
423
|
+
|
|
424
|
+
if not json_output:
|
|
425
|
+
console.print(f"Generating stories ({mode_str}, {timeframe_str})...")
|
|
426
|
+
console.print()
|
|
321
427
|
|
|
322
428
|
# Get repos to analyze
|
|
323
429
|
if repo:
|
|
@@ -325,12 +431,18 @@ def generate(
|
|
|
325
431
|
else:
|
|
326
432
|
tracked = get_tracked_repos()
|
|
327
433
|
if not tracked:
|
|
434
|
+
if json_output:
|
|
435
|
+
print(json.dumps({"generated": 0, "stories": [], "error": "No repositories tracked"}, indent=2))
|
|
436
|
+
raise typer.Exit(1)
|
|
328
437
|
print_warning("No repositories tracked.")
|
|
329
438
|
print_info("Run `repr init` or `repr repos add <path>` first.")
|
|
330
439
|
raise typer.Exit(1)
|
|
331
440
|
repo_paths = [Path(r["path"]) for r in tracked if Path(r["path"]).exists()]
|
|
332
441
|
|
|
333
442
|
if not repo_paths:
|
|
443
|
+
if json_output:
|
|
444
|
+
print(json.dumps({"generated": 0, "stories": [], "error": "No valid repositories found"}, indent=2))
|
|
445
|
+
raise typer.Exit(1)
|
|
334
446
|
print_error("No valid repositories found")
|
|
335
447
|
raise typer.Exit(1)
|
|
336
448
|
|
|
@@ -339,21 +451,37 @@ def generate(
|
|
|
339
451
|
from .discovery import analyze_repo
|
|
340
452
|
|
|
341
453
|
total_stories = 0
|
|
454
|
+
all_stories = [] # Collect all generated stories for JSON output
|
|
342
455
|
|
|
343
456
|
for repo_path in repo_paths:
|
|
344
457
|
repo_info = analyze_repo(repo_path)
|
|
345
|
-
|
|
458
|
+
if not json_output:
|
|
459
|
+
console.print(f"[bold]{repo_info.name}[/]")
|
|
346
460
|
|
|
347
461
|
if commits:
|
|
348
462
|
# Specific commits
|
|
349
463
|
commit_shas = [s.strip() for s in commits.split(",")]
|
|
350
464
|
commit_list = get_commits_by_shas(repo_path, commit_shas)
|
|
351
465
|
else:
|
|
352
|
-
#
|
|
353
|
-
|
|
466
|
+
# Determine timeframe
|
|
467
|
+
timeframe_days = days if days is not None else 90 # Default 90 days
|
|
468
|
+
since_str = None
|
|
469
|
+
|
|
470
|
+
# Parse natural language date if provided
|
|
471
|
+
if since_date:
|
|
472
|
+
since_str = _parse_date_reference(since_date)
|
|
473
|
+
|
|
474
|
+
# Recent commits within timeframe
|
|
475
|
+
commit_list = get_commits_with_diffs(
|
|
476
|
+
repo_path,
|
|
477
|
+
count=500, # Higher limit when filtering by time
|
|
478
|
+
days=timeframe_days,
|
|
479
|
+
since=since_str,
|
|
480
|
+
)
|
|
354
481
|
|
|
355
482
|
if not commit_list:
|
|
356
|
-
|
|
483
|
+
if not json_output:
|
|
484
|
+
console.print(f" [{BRAND_MUTED}]No commits found[/]")
|
|
357
485
|
continue
|
|
358
486
|
|
|
359
487
|
# Dry run: show what would be sent
|
|
@@ -434,8 +562,10 @@ def generate(
|
|
|
434
562
|
)
|
|
435
563
|
|
|
436
564
|
for story in stories:
|
|
437
|
-
|
|
565
|
+
if not json_output:
|
|
566
|
+
console.print(f" • {story['summary']}")
|
|
438
567
|
total_stories += 1
|
|
568
|
+
all_stories.append(story)
|
|
439
569
|
|
|
440
570
|
# Log cloud operation if using cloud
|
|
441
571
|
if cloud and stories:
|
|
@@ -450,7 +580,12 @@ def generate(
|
|
|
450
580
|
bytes_sent=len(str(commit_list)) // 2, # Rough estimate
|
|
451
581
|
)
|
|
452
582
|
|
|
453
|
-
|
|
583
|
+
if not json_output:
|
|
584
|
+
console.print()
|
|
585
|
+
|
|
586
|
+
if json_output:
|
|
587
|
+
print(json.dumps({"generated": total_stories, "stories": all_stories}, indent=2, default=str))
|
|
588
|
+
return
|
|
454
589
|
|
|
455
590
|
if dry_run:
|
|
456
591
|
console.print()
|
|
@@ -465,7 +600,7 @@ def generate(
|
|
|
465
600
|
])
|
|
466
601
|
|
|
467
602
|
|
|
468
|
-
def
|
|
603
|
+
async def _generate_stories_async(
|
|
469
604
|
commits: list[dict],
|
|
470
605
|
repo_info,
|
|
471
606
|
batch_size: int,
|
|
@@ -473,7 +608,7 @@ def _generate_stories(
|
|
|
473
608
|
template: str = "resume",
|
|
474
609
|
custom_prompt: str | None = None,
|
|
475
610
|
) -> list[dict]:
|
|
476
|
-
"""Generate stories from commits using LLM."""
|
|
611
|
+
"""Generate stories from commits using LLM (async implementation)."""
|
|
477
612
|
from .openai_analysis import get_openai_client, extract_commit_batch
|
|
478
613
|
from .templates import build_generation_prompt
|
|
479
614
|
|
|
@@ -485,186 +620,103 @@ def _generate_stories(
|
|
|
485
620
|
for i in range(0, len(commits), batch_size)
|
|
486
621
|
]
|
|
487
622
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
623
|
+
# Create client once, reuse for all batches
|
|
624
|
+
if local:
|
|
625
|
+
llm_config = get_llm_config()
|
|
626
|
+
client = get_openai_client(
|
|
627
|
+
api_key=llm_config.get("local_api_key") or "ollama",
|
|
628
|
+
base_url=llm_config.get("local_api_url") or "http://localhost:11434/v1",
|
|
629
|
+
)
|
|
630
|
+
model = llm_config.get("local_model") or llm_config.get("extraction_model") or "llama3.2"
|
|
631
|
+
else:
|
|
632
|
+
client = get_openai_client()
|
|
633
|
+
model = None # Use default
|
|
634
|
+
|
|
635
|
+
try:
|
|
636
|
+
for i, batch in enumerate(batches):
|
|
637
|
+
try:
|
|
638
|
+
# Build prompt with template
|
|
639
|
+
system_prompt, user_prompt = build_generation_prompt(
|
|
640
|
+
template_name=template,
|
|
641
|
+
repo_name=repo_info.name,
|
|
642
|
+
commits=batch,
|
|
643
|
+
custom_prompt=custom_prompt,
|
|
504
644
|
)
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
console.print(f" [{BRAND_MUTED}]Batch {i+1} failed: {e}[/]")
|
|
645
|
+
|
|
646
|
+
# Extract story from batch
|
|
647
|
+
content = await extract_commit_batch(
|
|
648
|
+
client=client,
|
|
649
|
+
commits=batch,
|
|
650
|
+
batch_num=i + 1,
|
|
651
|
+
total_batches=len(batches),
|
|
652
|
+
model=model,
|
|
653
|
+
system_prompt=system_prompt,
|
|
654
|
+
user_prompt=user_prompt,
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
if not content or content.startswith("[Batch"):
|
|
658
|
+
continue
|
|
659
|
+
|
|
660
|
+
# Extract summary (first non-empty line)
|
|
661
|
+
lines = [l.strip() for l in content.split("\n") if l.strip()]
|
|
662
|
+
summary = lines[0][:100] if lines else "Story"
|
|
663
|
+
# Clean up summary
|
|
664
|
+
summary = summary.lstrip("#-•* ").strip()
|
|
665
|
+
|
|
666
|
+
# Build metadata
|
|
667
|
+
commit_shas = [c["full_sha"] for c in batch]
|
|
668
|
+
first_date = min(c["date"] for c in batch)
|
|
669
|
+
last_date = max(c["date"] for c in batch)
|
|
670
|
+
total_files = sum(len(c.get("files", [])) for c in batch)
|
|
671
|
+
total_adds = sum(c.get("insertions", 0) for c in batch)
|
|
672
|
+
total_dels = sum(c.get("deletions", 0) for c in batch)
|
|
673
|
+
|
|
674
|
+
metadata = {
|
|
675
|
+
"summary": summary,
|
|
676
|
+
"repo_name": repo_info.name,
|
|
677
|
+
"repo_path": str(repo_info.path),
|
|
678
|
+
"commit_shas": commit_shas,
|
|
679
|
+
"first_commit_at": first_date,
|
|
680
|
+
"last_commit_at": last_date,
|
|
681
|
+
"files_changed": total_files,
|
|
682
|
+
"lines_added": total_adds,
|
|
683
|
+
"lines_removed": total_dels,
|
|
684
|
+
"generated_locally": local,
|
|
685
|
+
"template": template,
|
|
686
|
+
"needs_review": False,
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
# Save story
|
|
690
|
+
story_id = save_story(content, metadata)
|
|
691
|
+
metadata["id"] = story_id
|
|
692
|
+
stories.append(metadata)
|
|
693
|
+
|
|
694
|
+
except Exception as e:
|
|
695
|
+
console.print(f" [{BRAND_MUTED}]Batch {i+1} failed: {e}[/]")
|
|
696
|
+
finally:
|
|
697
|
+
# Properly close the async client
|
|
698
|
+
await client.close()
|
|
560
699
|
|
|
561
700
|
return stories
|
|
562
701
|
|
|
563
702
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
"""
|
|
582
|
-
_show_summary_since("this week", days=7, save=save)
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
@app.command()
|
|
586
|
-
def since(
|
|
587
|
-
date: str = typer.Argument(..., help="Date or time reference (e.g., 'monday', '2026-01-01', '3 days ago')"),
|
|
588
|
-
save: bool = typer.Option(
|
|
589
|
-
False, "--save",
|
|
590
|
-
help="Save as a permanent story",
|
|
591
|
-
),
|
|
592
|
-
):
|
|
593
|
-
"""
|
|
594
|
-
Show work since a specific date or time.
|
|
595
|
-
|
|
596
|
-
Examples:
|
|
597
|
-
repr since monday
|
|
598
|
-
repr since "3 days ago"
|
|
599
|
-
repr since 2026-01-01
|
|
600
|
-
"""
|
|
601
|
-
_show_summary_since(date, save=save)
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
@app.command()
|
|
605
|
-
def standup(
|
|
606
|
-
days: int = typer.Option(
|
|
607
|
-
3, "--days",
|
|
608
|
-
help="Number of days to look back",
|
|
609
|
-
),
|
|
610
|
-
):
|
|
611
|
-
"""
|
|
612
|
-
Quick summary for daily standup (last 3 days).
|
|
613
|
-
|
|
614
|
-
Example:
|
|
615
|
-
repr standup
|
|
616
|
-
repr standup --days 5
|
|
617
|
-
"""
|
|
618
|
-
_show_summary_since(f"last {days} days", days=days, save=False)
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
def _show_summary_since(label: str, days: int = None, save: bool = False):
|
|
622
|
-
"""Show summary of work since a date."""
|
|
623
|
-
from .tools import get_commits_with_diffs
|
|
624
|
-
from .discovery import analyze_repo
|
|
625
|
-
|
|
626
|
-
print_header()
|
|
627
|
-
|
|
628
|
-
tracked = get_tracked_repos()
|
|
629
|
-
if not tracked:
|
|
630
|
-
print_warning("No repositories tracked.")
|
|
631
|
-
print_info("Run `repr init` first.")
|
|
632
|
-
raise typer.Exit(1)
|
|
633
|
-
|
|
634
|
-
console.print(f"📊 Work since {label}")
|
|
635
|
-
console.print()
|
|
636
|
-
|
|
637
|
-
total_commits = 0
|
|
638
|
-
all_summaries = []
|
|
639
|
-
|
|
640
|
-
for repo_info in tracked:
|
|
641
|
-
repo_path = Path(repo_info["path"])
|
|
642
|
-
if not repo_path.exists():
|
|
643
|
-
continue
|
|
644
|
-
|
|
645
|
-
commits = get_commits_with_diffs(repo_path, count=100, days=days or 30)
|
|
646
|
-
if not commits:
|
|
647
|
-
continue
|
|
648
|
-
|
|
649
|
-
repo_name = repo_path.name
|
|
650
|
-
console.print(f"[bold]{repo_name}[/] ({len(commits)} commits):")
|
|
651
|
-
|
|
652
|
-
# Group commits by rough topic (simple heuristic)
|
|
653
|
-
for c in commits[:5]:
|
|
654
|
-
msg = c["message"].split("\n")[0][:60]
|
|
655
|
-
console.print(f" • {msg}")
|
|
656
|
-
|
|
657
|
-
if len(commits) > 5:
|
|
658
|
-
console.print(f" [{BRAND_MUTED}]... and {len(commits) - 5} more[/]")
|
|
659
|
-
|
|
660
|
-
total_commits += len(commits)
|
|
661
|
-
console.print()
|
|
662
|
-
|
|
663
|
-
console.print(f"Total: {total_commits} commits")
|
|
664
|
-
|
|
665
|
-
if not save:
|
|
666
|
-
console.print()
|
|
667
|
-
console.print(f"[{BRAND_MUTED}]This summary wasn't saved. Run with --save to create a story.[/]")
|
|
703
|
+
def _generate_stories(
|
|
704
|
+
commits: list[dict],
|
|
705
|
+
repo_info,
|
|
706
|
+
batch_size: int,
|
|
707
|
+
local: bool,
|
|
708
|
+
template: str = "resume",
|
|
709
|
+
custom_prompt: str | None = None,
|
|
710
|
+
) -> list[dict]:
|
|
711
|
+
"""Generate stories from commits using LLM."""
|
|
712
|
+
return asyncio.run(_generate_stories_async(
|
|
713
|
+
commits=commits,
|
|
714
|
+
repo_info=repo_info,
|
|
715
|
+
batch_size=batch_size,
|
|
716
|
+
local=local,
|
|
717
|
+
template=template,
|
|
718
|
+
custom_prompt=custom_prompt,
|
|
719
|
+
))
|
|
668
720
|
|
|
669
721
|
|
|
670
722
|
# =============================================================================
|
|
@@ -698,24 +750,37 @@ def stories(
|
|
|
698
750
|
|
|
699
751
|
console.print(f"[bold]Stories[/] ({len(story_list)} total)")
|
|
700
752
|
console.print()
|
|
701
|
-
|
|
753
|
+
|
|
754
|
+
# Group stories by repository
|
|
755
|
+
by_repo = defaultdict(list)
|
|
702
756
|
for story in story_list[:20]:
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
757
|
+
r_name = story.get("repo_name", "unknown")
|
|
758
|
+
by_repo[r_name].append(story)
|
|
759
|
+
|
|
760
|
+
# Sort repositories by their most recent story
|
|
761
|
+
sorted_repos = sorted(
|
|
762
|
+
by_repo.keys(),
|
|
763
|
+
key=lambda r: max(s.get("created_at", "") for s in by_repo[r]),
|
|
764
|
+
reverse=True
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
for repo_name in sorted_repos:
|
|
768
|
+
console.print(f"[bold]{repo_name}[/]")
|
|
769
|
+
for story in by_repo[repo_name]:
|
|
770
|
+
# Status indicator
|
|
771
|
+
if story.get("needs_review"):
|
|
772
|
+
status = f"[{BRAND_WARNING}]⚠[/]"
|
|
773
|
+
elif story.get("pushed_at"):
|
|
774
|
+
status = f"[{BRAND_SUCCESS}]✓[/]"
|
|
775
|
+
else:
|
|
776
|
+
status = f"[{BRAND_MUTED}]○[/]"
|
|
777
|
+
|
|
778
|
+
summary = story.get("summary", "Untitled")
|
|
779
|
+
created = format_relative_time(story.get("created_at", ""))
|
|
780
|
+
|
|
781
|
+
console.print(f" {status} {summary} [{BRAND_MUTED}]• {created}[/]")
|
|
717
782
|
console.print()
|
|
718
|
-
|
|
783
|
+
|
|
719
784
|
if len(story_list) > 20:
|
|
720
785
|
console.print(f"[{BRAND_MUTED}]... and {len(story_list) - 20} more[/]")
|
|
721
786
|
|
|
@@ -1025,15 +1090,20 @@ def pull():
|
|
|
1025
1090
|
def list_commits(
|
|
1026
1091
|
repo: Optional[str] = typer.Option(None, "--repo", help="Filter by repo name"),
|
|
1027
1092
|
limit: int = typer.Option(50, "--limit", "-n", help="Number of commits"),
|
|
1028
|
-
|
|
1093
|
+
days: Optional[int] = typer.Option(None, "--days", "-d", help="Show commits from last N days (e.g., 7 for week, 3 for standup)"),
|
|
1094
|
+
since: Optional[str] = typer.Option(None, "--since", help="Since date (e.g., '2024-01-01', 'monday', '2 weeks ago')"),
|
|
1029
1095
|
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
1030
1096
|
):
|
|
1031
1097
|
"""
|
|
1032
1098
|
List recent commits across all tracked repos.
|
|
1033
1099
|
|
|
1034
|
-
|
|
1100
|
+
Examples:
|
|
1035
1101
|
repr commits --limit 10
|
|
1036
1102
|
repr commits --repo myproject
|
|
1103
|
+
repr commits --days 7 # Last week
|
|
1104
|
+
repr commits --days 3 # For standup
|
|
1105
|
+
repr commits --since monday
|
|
1106
|
+
repr commits --since "2 weeks ago"
|
|
1037
1107
|
"""
|
|
1038
1108
|
from .tools import get_commits_with_diffs
|
|
1039
1109
|
|
|
@@ -1042,6 +1112,18 @@ def list_commits(
|
|
|
1042
1112
|
print_warning("No repositories tracked.")
|
|
1043
1113
|
raise typer.Exit(1)
|
|
1044
1114
|
|
|
1115
|
+
# Determine days filter
|
|
1116
|
+
filter_days = days if days is not None else 90
|
|
1117
|
+
|
|
1118
|
+
# Parse natural language date if --since provided
|
|
1119
|
+
since_str = None
|
|
1120
|
+
if since:
|
|
1121
|
+
since_str = _parse_date_reference(since)
|
|
1122
|
+
if not since_str:
|
|
1123
|
+
print_error(f"Could not parse date: {since}")
|
|
1124
|
+
print_info("Try: '2024-01-01', 'monday', '3 days ago', 'last week'")
|
|
1125
|
+
raise typer.Exit(1)
|
|
1126
|
+
|
|
1045
1127
|
all_commits = []
|
|
1046
1128
|
|
|
1047
1129
|
for repo_info in tracked:
|
|
@@ -1051,7 +1133,7 @@ def list_commits(
|
|
|
1051
1133
|
if not repo_path.exists():
|
|
1052
1134
|
continue
|
|
1053
1135
|
|
|
1054
|
-
commits = get_commits_with_diffs(repo_path, count=limit, days=
|
|
1136
|
+
commits = get_commits_with_diffs(repo_path, count=limit, days=filter_days, since=since_str)
|
|
1055
1137
|
for c in commits:
|
|
1056
1138
|
c["repo_name"] = repo_path.name
|
|
1057
1139
|
all_commits.extend(commits)
|
|
@@ -1068,7 +1150,15 @@ def list_commits(
|
|
|
1068
1150
|
print_info("No commits found")
|
|
1069
1151
|
raise typer.Exit()
|
|
1070
1152
|
|
|
1071
|
-
|
|
1153
|
+
# Build label for display
|
|
1154
|
+
if days:
|
|
1155
|
+
label = f"last {days} days"
|
|
1156
|
+
elif since:
|
|
1157
|
+
label = f"since {since}"
|
|
1158
|
+
else:
|
|
1159
|
+
label = "recent"
|
|
1160
|
+
|
|
1161
|
+
console.print(f"[bold]Commits ({label})[/] — {len(all_commits)} total")
|
|
1072
1162
|
console.print()
|
|
1073
1163
|
|
|
1074
1164
|
current_repo = None
|
|
@@ -2331,22 +2421,64 @@ def status(
|
|
|
2331
2421
|
|
|
2332
2422
|
|
|
2333
2423
|
@app.command()
|
|
2334
|
-
def mode(
|
|
2424
|
+
def mode(
|
|
2425
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
2426
|
+
):
|
|
2335
2427
|
"""
|
|
2336
2428
|
Show current execution mode and settings.
|
|
2337
|
-
|
|
2429
|
+
|
|
2338
2430
|
Example:
|
|
2339
2431
|
repr mode
|
|
2432
|
+
repr mode --json
|
|
2340
2433
|
"""
|
|
2341
2434
|
from .llm import get_llm_status
|
|
2342
|
-
|
|
2435
|
+
|
|
2343
2436
|
authenticated = is_authenticated()
|
|
2344
2437
|
privacy = get_privacy_settings()
|
|
2345
2438
|
llm_status = get_llm_status()
|
|
2346
|
-
|
|
2439
|
+
|
|
2440
|
+
# Determine mode
|
|
2441
|
+
if privacy.get("lock_local_only"):
|
|
2442
|
+
if privacy.get("lock_permanent"):
|
|
2443
|
+
mode = "LOCAL-ONLY"
|
|
2444
|
+
locked = True
|
|
2445
|
+
else:
|
|
2446
|
+
mode = "LOCAL-ONLY"
|
|
2447
|
+
locked = True
|
|
2448
|
+
elif authenticated:
|
|
2449
|
+
mode = "CLOUD"
|
|
2450
|
+
locked = False
|
|
2451
|
+
else:
|
|
2452
|
+
mode = "LOCAL-ONLY"
|
|
2453
|
+
locked = False
|
|
2454
|
+
|
|
2455
|
+
# Build LLM provider string
|
|
2456
|
+
if llm_status["local"]["available"]:
|
|
2457
|
+
llm_provider = f"{llm_status['local']['name']} ({llm_status['local']['model']})"
|
|
2458
|
+
else:
|
|
2459
|
+
llm_provider = "none"
|
|
2460
|
+
|
|
2461
|
+
# Check cloud allowed
|
|
2462
|
+
cloud_allowed = authenticated and is_cloud_allowed()
|
|
2463
|
+
|
|
2464
|
+
if json_output:
|
|
2465
|
+
import json
|
|
2466
|
+
output = {
|
|
2467
|
+
"mode": mode,
|
|
2468
|
+
"locked": locked,
|
|
2469
|
+
"llm_provider": llm_provider,
|
|
2470
|
+
"network_policy": "cloud-enabled" if authenticated else "local-only",
|
|
2471
|
+
"authenticated": authenticated,
|
|
2472
|
+
"cloud_generation_available": cloud_allowed,
|
|
2473
|
+
"sync_available": cloud_allowed,
|
|
2474
|
+
"publishing_available": cloud_allowed,
|
|
2475
|
+
}
|
|
2476
|
+
print(json.dumps(output, indent=2))
|
|
2477
|
+
return
|
|
2478
|
+
|
|
2347
2479
|
console.print("[bold]Mode[/]")
|
|
2348
2480
|
console.print()
|
|
2349
|
-
|
|
2481
|
+
|
|
2350
2482
|
if privacy.get("lock_local_only"):
|
|
2351
2483
|
if privacy.get("lock_permanent"):
|
|
2352
2484
|
console.print("Data boundary: local only (permanently locked)")
|
|
@@ -2356,22 +2488,22 @@ def mode():
|
|
|
2356
2488
|
console.print("Data boundary: cloud-enabled")
|
|
2357
2489
|
else:
|
|
2358
2490
|
console.print("Data boundary: local only")
|
|
2359
|
-
|
|
2491
|
+
|
|
2360
2492
|
console.print(f"Default inference: {llm_status['default_mode']}")
|
|
2361
|
-
|
|
2493
|
+
|
|
2362
2494
|
console.print()
|
|
2363
|
-
|
|
2495
|
+
|
|
2364
2496
|
# LLM info
|
|
2365
2497
|
if llm_status["local"]["available"]:
|
|
2366
2498
|
console.print(f"Local LLM: {llm_status['local']['name']} ({llm_status['local']['model']})")
|
|
2367
2499
|
else:
|
|
2368
2500
|
console.print(f"[{BRAND_MUTED}]Local LLM: not detected[/]")
|
|
2369
|
-
|
|
2501
|
+
|
|
2370
2502
|
console.print()
|
|
2371
|
-
|
|
2503
|
+
|
|
2372
2504
|
console.print("Available:")
|
|
2373
2505
|
console.print(f" [{BRAND_SUCCESS}]✓[/] Local generation")
|
|
2374
|
-
if
|
|
2506
|
+
if cloud_allowed:
|
|
2375
2507
|
console.print(f" [{BRAND_SUCCESS}]✓[/] Cloud generation")
|
|
2376
2508
|
console.print(f" [{BRAND_SUCCESS}]✓[/] Sync")
|
|
2377
2509
|
console.print(f" [{BRAND_SUCCESS}]✓[/] Publishing")
|
repr/doctor.py
CHANGED
|
@@ -456,3 +456,40 @@ def run_all_checks() -> DoctorReport:
|
|
|
456
456
|
recommendations=recommendations,
|
|
457
457
|
)
|
|
458
458
|
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
|
repr/llm.py
CHANGED
|
@@ -504,3 +504,40 @@ def get_effective_llm_mode() -> tuple[str, dict[str, Any]]:
|
|
|
504
504
|
"model": llm_config.get("local_model"),
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
|
repr/openai_analysis.py
CHANGED
|
@@ -122,6 +122,8 @@ async def extract_commit_batch(
|
|
|
122
122
|
batch_num: int,
|
|
123
123
|
total_batches: int,
|
|
124
124
|
model: str = None,
|
|
125
|
+
system_prompt: str = None,
|
|
126
|
+
user_prompt: str = None,
|
|
125
127
|
) -> str:
|
|
126
128
|
"""
|
|
127
129
|
Extraction phase: Extract accomplishments from a batch of commits.
|
|
@@ -132,6 +134,8 @@ async def extract_commit_batch(
|
|
|
132
134
|
batch_num: Current batch number (for context)
|
|
133
135
|
total_batches: Total number of batches
|
|
134
136
|
model: Model name to use (defaults to stored config or DEFAULT_EXTRACTION_MODEL)
|
|
137
|
+
system_prompt: Custom system prompt (optional, uses default if not provided)
|
|
138
|
+
user_prompt: Custom user prompt (optional, uses default if not provided)
|
|
135
139
|
|
|
136
140
|
Returns:
|
|
137
141
|
Summary of technical accomplishments in this batch
|
|
@@ -139,36 +143,40 @@ async def extract_commit_batch(
|
|
|
139
143
|
if not model:
|
|
140
144
|
llm_config = get_llm_config()
|
|
141
145
|
model = llm_config.get("extraction_model") or DEFAULT_EXTRACTION_MODEL
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
|
|
147
|
+
# Use provided prompts or build defaults
|
|
148
|
+
if not system_prompt or not user_prompt:
|
|
149
|
+
# Format commits for the prompt
|
|
150
|
+
commits_text = []
|
|
151
|
+
for commit in commits:
|
|
152
|
+
commit_text = f"""
|
|
146
153
|
Commit: {commit['sha']}
|
|
147
154
|
Date: {commit['date']}
|
|
148
155
|
Message: {commit['message']}
|
|
149
156
|
|
|
150
157
|
Files changed:"""
|
|
151
|
-
|
|
152
|
-
for file_info in commit['files'][:10]: # Limit files per commit
|
|
153
|
-
change_type = {
|
|
154
|
-
'A': 'Added',
|
|
155
|
-
'D': 'Deleted',
|
|
156
|
-
'M': 'Modified',
|
|
157
|
-
'R': 'Renamed'
|
|
158
|
-
}.get(file_info['change_type'], 'Changed')
|
|
159
158
|
|
|
160
|
-
|
|
159
|
+
for file_info in commit['files'][:10]: # Limit files per commit
|
|
160
|
+
change_type = {
|
|
161
|
+
'A': 'Added',
|
|
162
|
+
'D': 'Deleted',
|
|
163
|
+
'M': 'Modified',
|
|
164
|
+
'R': 'Renamed'
|
|
165
|
+
}.get(file_info['change_type'], 'Changed')
|
|
166
|
+
|
|
167
|
+
commit_text += f"\n {change_type}: {file_info['path']}"
|
|
168
|
+
|
|
169
|
+
if file_info['diff']:
|
|
170
|
+
# Truncate diff if too long (for token management)
|
|
171
|
+
diff = file_info['diff'][:2000]
|
|
172
|
+
commit_text += f"\n```diff\n{diff}\n```"
|
|
161
173
|
|
|
162
|
-
|
|
163
|
-
# Truncate diff if too long (for token management)
|
|
164
|
-
diff = file_info['diff'][:2000]
|
|
165
|
-
commit_text += f"\n```diff\n{diff}\n```"
|
|
174
|
+
commits_text.append(commit_text)
|
|
166
175
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
system_prompt = """You are analyzing a developer's actual code commits to extract specific technical accomplishments WITH the reasoning behind them.
|
|
176
|
+
commits_formatted = "\n\n---\n".join(commits_text)
|
|
177
|
+
|
|
178
|
+
if not system_prompt:
|
|
179
|
+
system_prompt = """You are analyzing a developer's actual code commits to extract specific technical accomplishments WITH the reasoning behind them.
|
|
172
180
|
|
|
173
181
|
Your job: Read the commit messages and diffs, then list CONCRETE technical accomplishments with SPECIFIC details AND infer WHY those decisions were made.
|
|
174
182
|
|
|
@@ -194,7 +202,8 @@ What NOT to do:
|
|
|
194
202
|
- Don't include process/methodology unless there's evidence
|
|
195
203
|
- Don't fabricate motivations that aren't supported by the code/commits"""
|
|
196
204
|
|
|
197
|
-
|
|
205
|
+
if not user_prompt:
|
|
206
|
+
user_prompt = f"""Analyze commits batch {batch_num}/{total_batches} and extract technical accomplishments:
|
|
198
207
|
|
|
199
208
|
{commits_formatted}
|
|
200
209
|
|
repr/privacy.py
CHANGED
|
@@ -331,3 +331,40 @@ def clear_audit_log() -> int:
|
|
|
331
331
|
|
|
332
332
|
return count
|
|
333
333
|
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
|
repr/telemetry.py
CHANGED
|
@@ -277,3 +277,40 @@ def get_pending_events() -> list[dict[str, Any]]:
|
|
|
277
277
|
"""
|
|
278
278
|
return _load_queue()
|
|
279
279
|
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
|
repr/templates.py
CHANGED
|
@@ -162,7 +162,12 @@ def format_commits_for_prompt(commits: list[dict[str, Any]]) -> str:
|
|
|
162
162
|
|
|
163
163
|
lines.append(f"- [{sha}] {msg}")
|
|
164
164
|
if files:
|
|
165
|
-
|
|
165
|
+
# Handle files as either list of dicts or list of strings
|
|
166
|
+
file_names = [
|
|
167
|
+
f["path"] if isinstance(f, dict) else f
|
|
168
|
+
for f in files[:5]
|
|
169
|
+
]
|
|
170
|
+
lines.append(f" Files: {', '.join(file_names)}")
|
|
166
171
|
if len(files) > 5:
|
|
167
172
|
lines.append(f" ... and {len(files) - 5} more files")
|
|
168
173
|
if c.get("insertions") or c.get("deletions"):
|
repr/ui.py
CHANGED
|
@@ -143,3 +143,40 @@ def confirm(message: str, default: bool = False) -> bool:
|
|
|
143
143
|
"""Prompt for confirmation."""
|
|
144
144
|
return Confirm.ask(message, default=default)
|
|
145
145
|
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
repr/updater.py
CHANGED
|
@@ -243,3 +243,40 @@ def perform_update(force: bool = False) -> bool:
|
|
|
243
243
|
|
|
244
244
|
return success
|
|
245
245
|
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repr-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
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
|
|
@@ -71,7 +71,7 @@ Turn commits into professional narratives for interviews, performance reviews, a
|
|
|
71
71
|
> *"I used repr to prep for my Meta interview in 30 minutes. Turned 2 years of commits into 8 STAR-format stories. Nailed every behavioral question."*
|
|
72
72
|
> **— Sarah, Senior Backend Engineer**
|
|
73
73
|
|
|
74
|
-
> *"Our sprint demos went from chaos to polished in 5 minutes. Just run `repr
|
|
74
|
+
> *"Our sprint demos went from chaos to polished in 5 minutes. Just run `repr commits --days 14` and export. Stakeholders love it."*
|
|
75
75
|
> **— Marcus, Engineering Manager**
|
|
76
76
|
|
|
77
77
|
> *"I run repr in a fully air-gapped environment. Zero network calls, 100% local. It's the only tool I trust for this."*
|
|
@@ -167,7 +167,7 @@ For full step-by-step guides, see the [documentation](https://repr.dev/docs/cli/
|
|
|
167
167
|
|
|
168
168
|
```bash
|
|
169
169
|
repr init ~/code
|
|
170
|
-
repr
|
|
170
|
+
repr commits --days 7
|
|
171
171
|
repr generate --local
|
|
172
172
|
```
|
|
173
173
|
|
|
@@ -186,7 +186,7 @@ repr review
|
|
|
186
186
|
### Weekly reflection
|
|
187
187
|
|
|
188
188
|
```bash
|
|
189
|
-
repr
|
|
189
|
+
repr commits --days 7
|
|
190
190
|
repr generate --local
|
|
191
191
|
repr story edit <id>
|
|
192
192
|
repr story feature <id>
|
|
@@ -204,6 +204,20 @@ repr story view <id>
|
|
|
204
204
|
|
|
205
205
|
[Full guide →](https://repr.dev/docs/cli/workflows/interview-prep)
|
|
206
206
|
|
|
207
|
+
### Generate from a specific timeframe
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# Last 30 days
|
|
211
|
+
repr generate --days 30 --local
|
|
212
|
+
|
|
213
|
+
# Since a specific date
|
|
214
|
+
repr generate --since 2024-01-01 --local
|
|
215
|
+
|
|
216
|
+
# Natural language dates
|
|
217
|
+
repr generate --since "2 weeks ago" --local
|
|
218
|
+
repr generate --since monday --local
|
|
219
|
+
```
|
|
220
|
+
|
|
207
221
|
### Publish your profile (optional)
|
|
208
222
|
|
|
209
223
|
```bash
|
|
@@ -274,7 +288,7 @@ repr llm use byok:openai
|
|
|
274
288
|
| **Local LLM** | `repr generate --local` | Talks only to your local endpoint. |
|
|
275
289
|
| **BYOK** | `repr llm add <provider>` | Calls your provider directly with your key. |
|
|
276
290
|
| **Cloud** | `repr generate --cloud` | Requires login; you initiate all network calls. |
|
|
277
|
-
| **Offline** | `repr
|
|
291
|
+
| **Offline** | `repr commits` / `repr stories` | Pure local operations. |
|
|
278
292
|
|
|
279
293
|
## Command help
|
|
280
294
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
repr/__init__.py,sha256=jraImidqaPxv03Uy76zPtnAcNnOl5KLZSXYBzxI85BI,446
|
|
2
|
+
repr/__main__.py,sha256=edYQ5TsuidoAKR1DSuTNcRKudj4lijHZGURb_CSem1M,164
|
|
3
|
+
repr/api.py,sha256=Rr6MEUkjf7LJ6TcxbdVstfpUM_mDpTKhllbFwy9jK2w,11893
|
|
4
|
+
repr/auth.py,sha256=-tqd2MMgFlowbhAqLHeSnVpDBkintkZ4kmPDZmczQFU,11682
|
|
5
|
+
repr/cli.py,sha256=jmIn86UxSAPLM7i2GWvmnoF2ewg_a69sQ3UGGxuc8e4,83784
|
|
6
|
+
repr/config.py,sha256=GZf5ucrBFIfOo9UtKE-DAZ9Ns1suAKG0jvUAY64oGIc,30601
|
|
7
|
+
repr/discovery.py,sha256=2RYmJleqV7TbxIMMYP2izkEBUeKH7U1F-U4KAUlUNww,14816
|
|
8
|
+
repr/doctor.py,sha256=cD-XLCVXfME0DsgOWU4VUOv28O4avjSQmMK6X_Kddyk,13441
|
|
9
|
+
repr/extractor.py,sha256=lGPN8gwTF_ZSezoQoPBMnf95nCJArGIteNiInfb39FM,10566
|
|
10
|
+
repr/hooks.py,sha256=DRpVXVv5Lesn9ARKHr-I91bUScab2It2TPjdwM38bT4,16864
|
|
11
|
+
repr/keychain.py,sha256=CpKU3tjFZVEPgiHiplSAtBQFDPA6qOSovv4IXXgJXbY,6957
|
|
12
|
+
repr/llm.py,sha256=kLcRq2wZTiGJKmkzUIBCpybD4S4efTGc87f_GL4IlDU,14635
|
|
13
|
+
repr/openai_analysis.py,sha256=KtluzntxRoBY_KylkMLPTb90UpV7fLJxDJ4UNOB5qwQ,26255
|
|
14
|
+
repr/privacy.py,sha256=r-HvQ4C56whI9Cbp4AHMwULvueBdYaO0pu3U1AoqB9M,9832
|
|
15
|
+
repr/storage.py,sha256=72nfFcR2Y98vpSjaO7zVHisq_Ln2UrHmGyDhEqEmDjU,14863
|
|
16
|
+
repr/telemetry.py,sha256=OR9w3v4VrVYu3C4Q0GQsX3-2JXSQvUhiYnkC6S5YOGc,6998
|
|
17
|
+
repr/templates.py,sha256=3YlhiUtnmsj6eQw3BPvvDty0-lf7oLxuYlbISE0KQrI,6733
|
|
18
|
+
repr/tools.py,sha256=QoGeti5Sye2wVuE-7UPxd_TDNXoen-xYfsFoT9rYRPs,20737
|
|
19
|
+
repr/ui.py,sha256=3Fc1ugwtfySIuB5LSE5R18tMHbz211TZNolVDlwKDAU,4031
|
|
20
|
+
repr/updater.py,sha256=sn3VEwtPkn1LLla2hGO53EmrY_ToRfMlDbFQawKhSZ4,7333
|
|
21
|
+
repr_cli-0.2.9.dist-info/licenses/LICENSE,sha256=tI16Ry3IQhjsde6weJ_in6czzWW2EF4Chz1uicyDLAA,1061
|
|
22
|
+
repr_cli-0.2.9.dist-info/METADATA,sha256=KA77WQLqSuOZIp9LMAVD24ev2aSAUZOt9glkbcNVYXk,11196
|
|
23
|
+
repr_cli-0.2.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
+
repr_cli-0.2.9.dist-info/entry_points.txt,sha256=SJoKgNB-fRy6O2T_lztFr9T3ND_BQl0ijWxNW-J7dUU,38
|
|
25
|
+
repr_cli-0.2.9.dist-info/top_level.txt,sha256=LNgPqdJPQnlicRve7uzI4a6rEUdcxHrNkUq_2w7eeiA,5
|
|
26
|
+
repr_cli-0.2.9.dist-info/RECORD,,
|
repr_cli-0.2.8.dist-info/RECORD
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
repr/__init__.py,sha256=jraImidqaPxv03Uy76zPtnAcNnOl5KLZSXYBzxI85BI,446
|
|
2
|
-
repr/__main__.py,sha256=M0ECtxOrmmYoYrYV5XI9UhDnOjWThxn0-PPysKs3RT0,127
|
|
3
|
-
repr/api.py,sha256=Rr6MEUkjf7LJ6TcxbdVstfpUM_mDpTKhllbFwy9jK2w,11893
|
|
4
|
-
repr/auth.py,sha256=-tqd2MMgFlowbhAqLHeSnVpDBkintkZ4kmPDZmczQFU,11682
|
|
5
|
-
repr/cli.py,sha256=xCz40ZRx_sOznTxS7GP4m0aArN7d7c4QCpFLh2t_FYU,78218
|
|
6
|
-
repr/config.py,sha256=GZf5ucrBFIfOo9UtKE-DAZ9Ns1suAKG0jvUAY64oGIc,30601
|
|
7
|
-
repr/discovery.py,sha256=2RYmJleqV7TbxIMMYP2izkEBUeKH7U1F-U4KAUlUNww,14816
|
|
8
|
-
repr/doctor.py,sha256=6cI21xXIlTNRzHi2fRfHpm__erO8jBZc6vge8-29ip4,13404
|
|
9
|
-
repr/extractor.py,sha256=lGPN8gwTF_ZSezoQoPBMnf95nCJArGIteNiInfb39FM,10566
|
|
10
|
-
repr/hooks.py,sha256=DRpVXVv5Lesn9ARKHr-I91bUScab2It2TPjdwM38bT4,16864
|
|
11
|
-
repr/keychain.py,sha256=CpKU3tjFZVEPgiHiplSAtBQFDPA6qOSovv4IXXgJXbY,6957
|
|
12
|
-
repr/llm.py,sha256=gTYloz7ONTpFQm73YFIVGOrjsk0iyocMTM4YkF4s4xI,14598
|
|
13
|
-
repr/openai_analysis.py,sha256=-9POoLF6B15_oBKJw_CjKH2DuWEIgIlOmtyjS4Gjbck,25764
|
|
14
|
-
repr/privacy.py,sha256=HITso2pzwN8R0Izh3SjUsrzcpjVw5bJEhbippAGeMiY,9795
|
|
15
|
-
repr/storage.py,sha256=72nfFcR2Y98vpSjaO7zVHisq_Ln2UrHmGyDhEqEmDjU,14863
|
|
16
|
-
repr/telemetry.py,sha256=7ANJJUB4Dd7A_HFVPqc92Gy77ruREzlmgayFQkwuC9s,6961
|
|
17
|
-
repr/templates.py,sha256=RQl7nUfy8IK6QFKzgpcebkBbQH0E_brbYh83pzym1TM,6530
|
|
18
|
-
repr/tools.py,sha256=QoGeti5Sye2wVuE-7UPxd_TDNXoen-xYfsFoT9rYRPs,20737
|
|
19
|
-
repr/ui.py,sha256=5jycUT-5Q0az4FFUzgarI8CfVAEEUPSEsT24Fad2kG8,3994
|
|
20
|
-
repr/updater.py,sha256=E2ZRfeQxA4_UrWmIphJkafrUHU2UGUpfINNVLuTIcfI,7296
|
|
21
|
-
repr_cli-0.2.8.dist-info/licenses/LICENSE,sha256=tI16Ry3IQhjsde6weJ_in6czzWW2EF4Chz1uicyDLAA,1061
|
|
22
|
-
repr_cli-0.2.8.dist-info/METADATA,sha256=5YVmEcJf4Dt2OfAxaIND95w-wkZ-XBhddndijKFjTfw,10898
|
|
23
|
-
repr_cli-0.2.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
-
repr_cli-0.2.8.dist-info/entry_points.txt,sha256=SJoKgNB-fRy6O2T_lztFr9T3ND_BQl0ijWxNW-J7dUU,38
|
|
25
|
-
repr_cli-0.2.8.dist-info/top_level.txt,sha256=LNgPqdJPQnlicRve7uzI4a6rEUdcxHrNkUq_2w7eeiA,5
|
|
26
|
-
repr_cli-0.2.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|