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/__init__.py
CHANGED
repr/api.py
CHANGED
|
@@ -23,6 +23,16 @@ def _get_user_url() -> str:
|
|
|
23
23
|
return f"{get_api_base()}/user"
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def _get_stories_url() -> str:
|
|
27
|
+
return f"{get_api_base()}/stories"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_public_settings_url() -> str:
|
|
31
|
+
"""Get URL for public profile settings endpoint."""
|
|
32
|
+
# This endpoint is under /api/public not /api/cli
|
|
33
|
+
return f"{get_api_base().replace('/api/cli', '/api/public')}/settings"
|
|
34
|
+
|
|
35
|
+
|
|
26
36
|
class APIError(Exception):
|
|
27
37
|
"""API request error."""
|
|
28
38
|
pass
|
|
@@ -34,7 +44,7 @@ def _get_headers() -> dict[str, str]:
|
|
|
34
44
|
return {
|
|
35
45
|
"Authorization": f"Bearer {token}",
|
|
36
46
|
"Content-Type": "application/json",
|
|
37
|
-
"User-Agent": "repr-cli/0.1.
|
|
47
|
+
"User-Agent": "repr-cli/0.1.1",
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
|
|
@@ -261,3 +271,119 @@ def sync_get_user_info() -> dict[str, Any]:
|
|
|
261
271
|
"""
|
|
262
272
|
import asyncio
|
|
263
273
|
return asyncio.run(get_user_info())
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
async def get_stories(
|
|
277
|
+
repo_name: str | None = None,
|
|
278
|
+
since: str | None = None,
|
|
279
|
+
technologies: str | None = None,
|
|
280
|
+
limit: int = 50,
|
|
281
|
+
offset: int = 0,
|
|
282
|
+
) -> dict[str, Any]:
|
|
283
|
+
"""
|
|
284
|
+
Get commit stories from repr.dev.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
repo_name: Filter by repository name
|
|
288
|
+
since: Filter by date (ISO format or human-readable like "1 week ago")
|
|
289
|
+
technologies: Comma-separated list of technologies to filter by
|
|
290
|
+
limit: Maximum number of stories to return
|
|
291
|
+
offset: Pagination offset
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Dict with 'stories' list and 'total' count
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
APIError: If request fails
|
|
298
|
+
AuthError: If not authenticated
|
|
299
|
+
"""
|
|
300
|
+
async with httpx.AsyncClient() as client:
|
|
301
|
+
try:
|
|
302
|
+
params: dict[str, Any] = {
|
|
303
|
+
"limit": limit,
|
|
304
|
+
"offset": offset,
|
|
305
|
+
}
|
|
306
|
+
if repo_name:
|
|
307
|
+
params["repo_name"] = repo_name
|
|
308
|
+
if since:
|
|
309
|
+
params["since"] = since
|
|
310
|
+
if technologies:
|
|
311
|
+
params["technologies"] = technologies
|
|
312
|
+
|
|
313
|
+
response = await client.get(
|
|
314
|
+
_get_stories_url(),
|
|
315
|
+
headers=_get_headers(),
|
|
316
|
+
params=params,
|
|
317
|
+
timeout=30,
|
|
318
|
+
)
|
|
319
|
+
response.raise_for_status()
|
|
320
|
+
return response.json()
|
|
321
|
+
|
|
322
|
+
except httpx.HTTPStatusError as e:
|
|
323
|
+
if e.response.status_code == 401:
|
|
324
|
+
raise AuthError("Session expired. Please run 'repr login' again.")
|
|
325
|
+
raise APIError(f"Failed to get stories: {e.response.status_code}")
|
|
326
|
+
except httpx.RequestError as e:
|
|
327
|
+
raise APIError(f"Network error: {str(e)}")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
async def push_story(story_data: dict[str, Any]) -> dict[str, Any]:
|
|
331
|
+
"""
|
|
332
|
+
Push a commit story to repr.dev.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
story_data: Story data including summary, technologies, repo info, etc.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Created/updated story data
|
|
339
|
+
|
|
340
|
+
Raises:
|
|
341
|
+
APIError: If request fails
|
|
342
|
+
AuthError: If not authenticated
|
|
343
|
+
"""
|
|
344
|
+
async with httpx.AsyncClient() as client:
|
|
345
|
+
try:
|
|
346
|
+
response = await client.post(
|
|
347
|
+
_get_stories_url(),
|
|
348
|
+
headers=_get_headers(),
|
|
349
|
+
json=story_data,
|
|
350
|
+
timeout=60,
|
|
351
|
+
)
|
|
352
|
+
response.raise_for_status()
|
|
353
|
+
return response.json()
|
|
354
|
+
|
|
355
|
+
except httpx.HTTPStatusError as e:
|
|
356
|
+
if e.response.status_code == 401:
|
|
357
|
+
raise AuthError("Session expired. Please run 'repr login' again.")
|
|
358
|
+
raise APIError(f"Failed to push story: {e.response.status_code}")
|
|
359
|
+
except httpx.RequestError as e:
|
|
360
|
+
raise APIError(f"Network error: {str(e)}")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
async def get_public_profile_settings() -> dict[str, Any]:
|
|
364
|
+
"""
|
|
365
|
+
Get the current user's public profile settings.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Dict with username, is_profile_public, profile_url, etc.
|
|
369
|
+
|
|
370
|
+
Raises:
|
|
371
|
+
APIError: If request fails
|
|
372
|
+
AuthError: If not authenticated
|
|
373
|
+
"""
|
|
374
|
+
async with httpx.AsyncClient() as client:
|
|
375
|
+
try:
|
|
376
|
+
response = await client.get(
|
|
377
|
+
_get_public_settings_url(),
|
|
378
|
+
headers=_get_headers(),
|
|
379
|
+
timeout=30,
|
|
380
|
+
)
|
|
381
|
+
response.raise_for_status()
|
|
382
|
+
return response.json()
|
|
383
|
+
|
|
384
|
+
except httpx.HTTPStatusError as e:
|
|
385
|
+
if e.response.status_code == 401:
|
|
386
|
+
raise AuthError("Session expired. Please run 'repr login' again.")
|
|
387
|
+
raise APIError(f"Failed to get profile settings: {e.response.status_code}")
|
|
388
|
+
except httpx.RequestError as e:
|
|
389
|
+
raise APIError(f"Network error: {str(e)}")
|
repr/auth.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Authentication via device code flow.
|
|
3
|
+
|
|
4
|
+
Tokens are stored securely in OS keychain (see keychain.py).
|
|
3
5
|
"""
|
|
4
6
|
|
|
5
7
|
import asyncio
|
|
@@ -149,7 +151,9 @@ async def poll_for_token(device_code: str, interval: int = POLL_INTERVAL) -> Tok
|
|
|
149
151
|
|
|
150
152
|
def save_token(token_response: TokenResponse) -> None:
|
|
151
153
|
"""
|
|
152
|
-
Save authentication token
|
|
154
|
+
Save authentication token securely.
|
|
155
|
+
|
|
156
|
+
Token is stored in OS keychain, config only stores a reference.
|
|
153
157
|
|
|
154
158
|
Args:
|
|
155
159
|
token_response: Token response from successful auth
|
|
@@ -163,7 +167,10 @@ def save_token(token_response: TokenResponse) -> None:
|
|
|
163
167
|
|
|
164
168
|
|
|
165
169
|
def logout() -> None:
|
|
166
|
-
"""Clear authentication and logout.
|
|
170
|
+
"""Clear authentication and logout.
|
|
171
|
+
|
|
172
|
+
Removes token from keychain and clears config reference.
|
|
173
|
+
"""
|
|
167
174
|
clear_auth()
|
|
168
175
|
|
|
169
176
|
|
|
@@ -193,6 +200,63 @@ def require_auth() -> str:
|
|
|
193
200
|
return auth["access_token"]
|
|
194
201
|
|
|
195
202
|
|
|
203
|
+
def migrate_plaintext_auth() -> bool:
|
|
204
|
+
"""
|
|
205
|
+
Migrate plaintext auth tokens to keychain storage.
|
|
206
|
+
|
|
207
|
+
Called on first run after upgrade to detect and migrate old tokens.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
True if migration occurred
|
|
211
|
+
"""
|
|
212
|
+
from .keychain import store_secret
|
|
213
|
+
import json
|
|
214
|
+
from .config import CONFIG_FILE, save_config
|
|
215
|
+
|
|
216
|
+
if not CONFIG_FILE.exists():
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
with open(CONFIG_FILE, "r") as f:
|
|
221
|
+
config = json.load(f)
|
|
222
|
+
|
|
223
|
+
auth = config.get("auth")
|
|
224
|
+
if not auth:
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
# Check if already migrated (has keychain ref)
|
|
228
|
+
if auth.get("token_keychain_ref"):
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
# Check for plaintext token
|
|
232
|
+
plaintext_token = auth.get("access_token")
|
|
233
|
+
if not plaintext_token:
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
# Migrate to keychain
|
|
237
|
+
user_id = auth.get("user_id", "unknown")[:8]
|
|
238
|
+
keychain_ref = f"auth_token_{user_id}"
|
|
239
|
+
store_secret(keychain_ref, plaintext_token)
|
|
240
|
+
|
|
241
|
+
# Update config (remove plaintext, add reference)
|
|
242
|
+
del config["auth"]["access_token"]
|
|
243
|
+
config["auth"]["token_keychain_ref"] = keychain_ref
|
|
244
|
+
|
|
245
|
+
# Handle litellm key if present
|
|
246
|
+
litellm_key = auth.get("litellm_api_key")
|
|
247
|
+
if litellm_key:
|
|
248
|
+
litellm_ref = f"litellm_key_{user_id}"
|
|
249
|
+
store_secret(litellm_ref, litellm_key)
|
|
250
|
+
del config["auth"]["litellm_api_key"]
|
|
251
|
+
config["auth"]["litellm_keychain_ref"] = litellm_ref
|
|
252
|
+
|
|
253
|
+
save_config(config)
|
|
254
|
+
return True
|
|
255
|
+
|
|
256
|
+
except Exception:
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
|
|
196
260
|
class AuthFlow:
|
|
197
261
|
"""
|
|
198
262
|
Manages the device code authentication flow with progress callbacks.
|