repr-cli 0.1.0__py3-none-any.whl → 0.2.2__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/__main__.py +6 -0
- 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.2.dist-info/METADATA +263 -0
- repr_cli-0.2.2.dist-info/RECORD +24 -0
- {repr_cli-0.1.0.dist-info → repr_cli-0.2.2.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.2.dist-info}/WHEEL +0 -0
- {repr_cli-0.1.0.dist-info → repr_cli-0.2.2.dist-info}/entry_points.txt +0 -0
- {repr_cli-0.1.0.dist-info → repr_cli-0.2.2.dist-info}/top_level.txt +0 -0
repr/discovery.py
CHANGED
|
@@ -82,6 +82,11 @@ class RepoInfo:
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def is_git_repo(path: Path) -> bool:
|
|
86
|
+
"""Check if a path is a git repository."""
|
|
87
|
+
return (path / ".git").is_dir()
|
|
88
|
+
|
|
89
|
+
|
|
85
90
|
def should_skip_directory(path: Path, skip_patterns: list[str]) -> bool:
|
|
86
91
|
"""Check if a directory should be skipped."""
|
|
87
92
|
name = path.name
|
repr/doctor.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Health check and diagnostics for repr CLI.
|
|
3
|
+
|
|
4
|
+
Provides comprehensive system status and actionable recommendations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class CheckResult:
|
|
14
|
+
"""Result of a single health check."""
|
|
15
|
+
name: str
|
|
16
|
+
status: str # "ok", "warning", "error"
|
|
17
|
+
message: str
|
|
18
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
19
|
+
fix: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DoctorReport:
|
|
24
|
+
"""Complete health check report."""
|
|
25
|
+
checks: list[CheckResult]
|
|
26
|
+
overall_status: str # "healthy", "warnings", "issues"
|
|
27
|
+
recommendations: list[str]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check_auth() -> CheckResult:
|
|
31
|
+
"""Check authentication status."""
|
|
32
|
+
from .config import is_authenticated, get_auth
|
|
33
|
+
|
|
34
|
+
if is_authenticated():
|
|
35
|
+
auth = get_auth()
|
|
36
|
+
email = auth.get("email", "unknown") if auth else "unknown"
|
|
37
|
+
return CheckResult(
|
|
38
|
+
name="Authentication",
|
|
39
|
+
status="ok",
|
|
40
|
+
message=f"Signed in as {email}",
|
|
41
|
+
details={"email": email},
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
return CheckResult(
|
|
45
|
+
name="Authentication",
|
|
46
|
+
status="warning",
|
|
47
|
+
message="Not signed in",
|
|
48
|
+
details={},
|
|
49
|
+
fix="Run `repr login` to enable cloud features",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_local_llm() -> CheckResult:
|
|
54
|
+
"""Check local LLM availability."""
|
|
55
|
+
from .llm import detect_local_llm, test_local_llm
|
|
56
|
+
from .config import get_llm_config
|
|
57
|
+
|
|
58
|
+
llm_config = get_llm_config()
|
|
59
|
+
|
|
60
|
+
# Check configured endpoint first
|
|
61
|
+
if llm_config.get("local_api_url"):
|
|
62
|
+
result = test_local_llm(
|
|
63
|
+
url=llm_config["local_api_url"],
|
|
64
|
+
model=llm_config.get("local_model"),
|
|
65
|
+
)
|
|
66
|
+
if result.success:
|
|
67
|
+
return CheckResult(
|
|
68
|
+
name="Local LLM",
|
|
69
|
+
status="ok",
|
|
70
|
+
message=f"Connected to {result.endpoint}",
|
|
71
|
+
details={
|
|
72
|
+
"provider": result.provider,
|
|
73
|
+
"model": result.model,
|
|
74
|
+
"response_time_ms": result.response_time_ms,
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
return CheckResult(
|
|
79
|
+
name="Local LLM",
|
|
80
|
+
status="error",
|
|
81
|
+
message=f"Configured endpoint unreachable: {llm_config['local_api_url']}",
|
|
82
|
+
details={"error": result.error},
|
|
83
|
+
fix="Start your local LLM or run `repr llm configure`",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Try auto-detection
|
|
87
|
+
detected = detect_local_llm()
|
|
88
|
+
if detected:
|
|
89
|
+
return CheckResult(
|
|
90
|
+
name="Local LLM",
|
|
91
|
+
status="ok",
|
|
92
|
+
message=f"Detected {detected.name} at {detected.url}",
|
|
93
|
+
details={
|
|
94
|
+
"provider": detected.provider,
|
|
95
|
+
"url": detected.url,
|
|
96
|
+
"models": detected.models[:5], # First 5
|
|
97
|
+
"default_model": detected.default_model,
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
return CheckResult(
|
|
102
|
+
name="Local LLM",
|
|
103
|
+
status="warning",
|
|
104
|
+
message="No local LLM detected",
|
|
105
|
+
details={},
|
|
106
|
+
fix="Install Ollama (`ollama serve`) or run `repr llm configure`",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def check_tracked_repos() -> CheckResult:
|
|
111
|
+
"""Check tracked repositories."""
|
|
112
|
+
from .config import get_tracked_repos
|
|
113
|
+
|
|
114
|
+
tracked = get_tracked_repos()
|
|
115
|
+
|
|
116
|
+
if not tracked:
|
|
117
|
+
return CheckResult(
|
|
118
|
+
name="Tracked Repositories",
|
|
119
|
+
status="warning",
|
|
120
|
+
message="No repositories tracked",
|
|
121
|
+
details={},
|
|
122
|
+
fix="Run `repr init` or `repr repos add <path>`",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Check if repos exist
|
|
126
|
+
existing = []
|
|
127
|
+
missing = []
|
|
128
|
+
for repo in tracked:
|
|
129
|
+
path = Path(repo["path"])
|
|
130
|
+
if path.exists() and (path / ".git").exists():
|
|
131
|
+
existing.append(repo)
|
|
132
|
+
else:
|
|
133
|
+
missing.append(repo["path"])
|
|
134
|
+
|
|
135
|
+
if missing:
|
|
136
|
+
return CheckResult(
|
|
137
|
+
name="Tracked Repositories",
|
|
138
|
+
status="warning",
|
|
139
|
+
message=f"{len(existing)} repos accessible, {len(missing)} missing",
|
|
140
|
+
details={
|
|
141
|
+
"existing": len(existing),
|
|
142
|
+
"missing": missing,
|
|
143
|
+
},
|
|
144
|
+
fix=f"Remove missing repos: repr repos remove <path>",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return CheckResult(
|
|
148
|
+
name="Tracked Repositories",
|
|
149
|
+
status="ok",
|
|
150
|
+
message=f"{len(tracked)} repositories tracked",
|
|
151
|
+
details={
|
|
152
|
+
"count": len(tracked),
|
|
153
|
+
"repos": [Path(r["path"]).name for r in tracked[:5]],
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def check_hooks() -> CheckResult:
|
|
159
|
+
"""Check hook installation status."""
|
|
160
|
+
from .config import get_tracked_repos
|
|
161
|
+
from .hooks import is_hook_installed
|
|
162
|
+
|
|
163
|
+
tracked = get_tracked_repos()
|
|
164
|
+
|
|
165
|
+
if not tracked:
|
|
166
|
+
return CheckResult(
|
|
167
|
+
name="Git Hooks",
|
|
168
|
+
status="warning",
|
|
169
|
+
message="No repositories to check",
|
|
170
|
+
details={},
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
installed = 0
|
|
174
|
+
not_installed = []
|
|
175
|
+
|
|
176
|
+
for repo in tracked:
|
|
177
|
+
path = Path(repo["path"])
|
|
178
|
+
if path.exists() and is_hook_installed(path):
|
|
179
|
+
installed += 1
|
|
180
|
+
elif path.exists():
|
|
181
|
+
not_installed.append(Path(repo["path"]).name)
|
|
182
|
+
|
|
183
|
+
total = len([r for r in tracked if Path(r["path"]).exists()])
|
|
184
|
+
|
|
185
|
+
if installed == total:
|
|
186
|
+
return CheckResult(
|
|
187
|
+
name="Git Hooks",
|
|
188
|
+
status="ok",
|
|
189
|
+
message=f"Installed in {installed}/{total} repos",
|
|
190
|
+
details={"installed": installed, "total": total},
|
|
191
|
+
)
|
|
192
|
+
elif installed > 0:
|
|
193
|
+
return CheckResult(
|
|
194
|
+
name="Git Hooks",
|
|
195
|
+
status="warning",
|
|
196
|
+
message=f"Installed in {installed}/{total} repos",
|
|
197
|
+
details={
|
|
198
|
+
"installed": installed,
|
|
199
|
+
"total": total,
|
|
200
|
+
"missing": not_installed[:5],
|
|
201
|
+
},
|
|
202
|
+
fix="Run `repr hooks install --all` to install in all repos",
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
return CheckResult(
|
|
206
|
+
name="Git Hooks",
|
|
207
|
+
status="warning",
|
|
208
|
+
message="No hooks installed",
|
|
209
|
+
details={},
|
|
210
|
+
fix="Run `repr hooks install --all` to enable auto-tracking",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def check_story_queue() -> CheckResult:
|
|
215
|
+
"""Check commit queue status."""
|
|
216
|
+
from .config import get_tracked_repos, get_llm_config
|
|
217
|
+
from .hooks import load_queue
|
|
218
|
+
|
|
219
|
+
tracked = get_tracked_repos()
|
|
220
|
+
llm_config = get_llm_config()
|
|
221
|
+
batch_size = llm_config.get("batch_size", 5)
|
|
222
|
+
|
|
223
|
+
total_queued = 0
|
|
224
|
+
repos_with_queue = []
|
|
225
|
+
|
|
226
|
+
for repo in tracked:
|
|
227
|
+
path = Path(repo["path"])
|
|
228
|
+
if path.exists():
|
|
229
|
+
queue = load_queue(path)
|
|
230
|
+
if queue:
|
|
231
|
+
total_queued += len(queue)
|
|
232
|
+
repos_with_queue.append({
|
|
233
|
+
"name": path.name,
|
|
234
|
+
"count": len(queue),
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
if total_queued == 0:
|
|
238
|
+
return CheckResult(
|
|
239
|
+
name="Commit Queue",
|
|
240
|
+
status="ok",
|
|
241
|
+
message="No commits pending",
|
|
242
|
+
details={},
|
|
243
|
+
)
|
|
244
|
+
elif total_queued >= batch_size:
|
|
245
|
+
return CheckResult(
|
|
246
|
+
name="Commit Queue",
|
|
247
|
+
status="warning",
|
|
248
|
+
message=f"{total_queued} commits pending (batch size: {batch_size})",
|
|
249
|
+
details={
|
|
250
|
+
"total": total_queued,
|
|
251
|
+
"batch_size": batch_size,
|
|
252
|
+
"repos": repos_with_queue,
|
|
253
|
+
},
|
|
254
|
+
fix="Run `repr generate` to create stories from pending commits",
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
return CheckResult(
|
|
258
|
+
name="Commit Queue",
|
|
259
|
+
status="ok",
|
|
260
|
+
message=f"{total_queued} commits queued (batch: {batch_size})",
|
|
261
|
+
details={
|
|
262
|
+
"total": total_queued,
|
|
263
|
+
"batch_size": batch_size,
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def check_unpushed_stories() -> CheckResult:
|
|
269
|
+
"""Check for unpushed stories."""
|
|
270
|
+
from .storage import get_unpushed_stories, get_story_count
|
|
271
|
+
from .config import is_authenticated
|
|
272
|
+
|
|
273
|
+
total = get_story_count()
|
|
274
|
+
unpushed = get_unpushed_stories()
|
|
275
|
+
|
|
276
|
+
if total == 0:
|
|
277
|
+
return CheckResult(
|
|
278
|
+
name="Stories",
|
|
279
|
+
status="warning",
|
|
280
|
+
message="No stories generated yet",
|
|
281
|
+
details={},
|
|
282
|
+
fix="Run `repr generate` to create stories from your commits",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if not unpushed:
|
|
286
|
+
return CheckResult(
|
|
287
|
+
name="Stories",
|
|
288
|
+
status="ok",
|
|
289
|
+
message=f"{total} stories (all synced)",
|
|
290
|
+
details={"total": total, "unpushed": 0},
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if is_authenticated():
|
|
294
|
+
return CheckResult(
|
|
295
|
+
name="Stories",
|
|
296
|
+
status="warning",
|
|
297
|
+
message=f"{total} stories ({len(unpushed)} not pushed)",
|
|
298
|
+
details={"total": total, "unpushed": len(unpushed)},
|
|
299
|
+
fix="Run `repr push` to publish unpushed stories",
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
return CheckResult(
|
|
303
|
+
name="Stories",
|
|
304
|
+
status="ok",
|
|
305
|
+
message=f"{total} stories (local only)",
|
|
306
|
+
details={"total": total, "unpushed": len(unpushed)},
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def check_stories_needing_review() -> CheckResult:
|
|
311
|
+
"""Check for stories needing review."""
|
|
312
|
+
from .storage import get_stories_needing_review
|
|
313
|
+
|
|
314
|
+
needs_review = get_stories_needing_review()
|
|
315
|
+
|
|
316
|
+
if not needs_review:
|
|
317
|
+
return CheckResult(
|
|
318
|
+
name="Story Review",
|
|
319
|
+
status="ok",
|
|
320
|
+
message="No stories need review",
|
|
321
|
+
details={},
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
return CheckResult(
|
|
325
|
+
name="Story Review",
|
|
326
|
+
status="warning",
|
|
327
|
+
message=f"{len(needs_review)} stories need review",
|
|
328
|
+
details={
|
|
329
|
+
"count": len(needs_review),
|
|
330
|
+
"stories": [s.get("summary", s.get("id", ""))[:50] for s in needs_review[:3]],
|
|
331
|
+
},
|
|
332
|
+
fix="Run `repr stories review` to review flagged stories",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def check_privacy_settings() -> CheckResult:
|
|
337
|
+
"""Check privacy configuration."""
|
|
338
|
+
from .config import get_privacy_settings, is_cloud_allowed
|
|
339
|
+
|
|
340
|
+
privacy = get_privacy_settings()
|
|
341
|
+
|
|
342
|
+
if privacy.get("lock_permanent"):
|
|
343
|
+
return CheckResult(
|
|
344
|
+
name="Privacy",
|
|
345
|
+
status="ok",
|
|
346
|
+
message="Local-only mode (permanent)",
|
|
347
|
+
details={"lock_permanent": True},
|
|
348
|
+
)
|
|
349
|
+
elif privacy.get("lock_local_only"):
|
|
350
|
+
return CheckResult(
|
|
351
|
+
name="Privacy",
|
|
352
|
+
status="ok",
|
|
353
|
+
message="Local-only mode enabled",
|
|
354
|
+
details={"lock_local_only": True},
|
|
355
|
+
)
|
|
356
|
+
elif is_cloud_allowed():
|
|
357
|
+
return CheckResult(
|
|
358
|
+
name="Privacy",
|
|
359
|
+
status="ok",
|
|
360
|
+
message="Cloud features enabled",
|
|
361
|
+
details={
|
|
362
|
+
"telemetry": privacy.get("telemetry_enabled", False),
|
|
363
|
+
"visibility": privacy.get("profile_visibility", "public"),
|
|
364
|
+
},
|
|
365
|
+
)
|
|
366
|
+
else:
|
|
367
|
+
return CheckResult(
|
|
368
|
+
name="Privacy",
|
|
369
|
+
status="ok",
|
|
370
|
+
message="Local mode (not authenticated)",
|
|
371
|
+
details={},
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def check_byok() -> CheckResult:
|
|
376
|
+
"""Check BYOK provider configuration."""
|
|
377
|
+
from .config import list_byok_providers, get_byok_config
|
|
378
|
+
|
|
379
|
+
providers = list_byok_providers()
|
|
380
|
+
|
|
381
|
+
if not providers:
|
|
382
|
+
return CheckResult(
|
|
383
|
+
name="BYOK Providers",
|
|
384
|
+
status="ok",
|
|
385
|
+
message="No BYOK providers configured",
|
|
386
|
+
details={},
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Verify each provider has valid config
|
|
390
|
+
valid = []
|
|
391
|
+
invalid = []
|
|
392
|
+
|
|
393
|
+
for provider in providers:
|
|
394
|
+
config = get_byok_config(provider)
|
|
395
|
+
if config and config.get("api_key"):
|
|
396
|
+
valid.append(provider)
|
|
397
|
+
else:
|
|
398
|
+
invalid.append(provider)
|
|
399
|
+
|
|
400
|
+
if invalid:
|
|
401
|
+
return CheckResult(
|
|
402
|
+
name="BYOK Providers",
|
|
403
|
+
status="warning",
|
|
404
|
+
message=f"{len(valid)} valid, {len(invalid)} misconfigured",
|
|
405
|
+
details={
|
|
406
|
+
"valid": valid,
|
|
407
|
+
"invalid": invalid,
|
|
408
|
+
},
|
|
409
|
+
fix=f"Reconfigure: repr llm add {invalid[0]}",
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return CheckResult(
|
|
413
|
+
name="BYOK Providers",
|
|
414
|
+
status="ok",
|
|
415
|
+
message=f"{len(valid)} providers configured",
|
|
416
|
+
details={"providers": valid},
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def run_all_checks() -> DoctorReport:
|
|
421
|
+
"""
|
|
422
|
+
Run all health checks and generate report.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
DoctorReport with all check results
|
|
426
|
+
"""
|
|
427
|
+
checks = [
|
|
428
|
+
check_auth(),
|
|
429
|
+
check_local_llm(),
|
|
430
|
+
check_tracked_repos(),
|
|
431
|
+
check_hooks(),
|
|
432
|
+
check_story_queue(),
|
|
433
|
+
check_unpushed_stories(),
|
|
434
|
+
check_stories_needing_review(),
|
|
435
|
+
check_privacy_settings(),
|
|
436
|
+
check_byok(),
|
|
437
|
+
]
|
|
438
|
+
|
|
439
|
+
# Determine overall status
|
|
440
|
+
has_errors = any(c.status == "error" for c in checks)
|
|
441
|
+
has_warnings = any(c.status == "warning" for c in checks)
|
|
442
|
+
|
|
443
|
+
if has_errors:
|
|
444
|
+
overall_status = "issues"
|
|
445
|
+
elif has_warnings:
|
|
446
|
+
overall_status = "warnings"
|
|
447
|
+
else:
|
|
448
|
+
overall_status = "healthy"
|
|
449
|
+
|
|
450
|
+
# Collect recommendations
|
|
451
|
+
recommendations = [c.fix for c in checks if c.fix and c.status in ("warning", "error")]
|
|
452
|
+
|
|
453
|
+
return DoctorReport(
|
|
454
|
+
checks=checks,
|
|
455
|
+
overall_status=overall_status,
|
|
456
|
+
recommendations=recommendations,
|
|
457
|
+
)
|
|
458
|
+
|