gitcast 1.0.5__tar.gz → 1.0.6__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.
Files changed (86) hide show
  1. {gitcast-1.0.5 → gitcast-1.0.6}/.env.example +0 -0
  2. {gitcast-1.0.5 → gitcast-1.0.6}/LICENSE +0 -0
  3. {gitcast-1.0.5 → gitcast-1.0.6}/MANIFEST.in +0 -0
  4. {gitcast-1.0.5/gitcast.egg-info → gitcast-1.0.6}/PKG-INFO +77 -50
  5. {gitcast-1.0.5 → gitcast-1.0.6}/README.md +0 -0
  6. {gitcast-1.0.5 → gitcast-1.0.6}/ai/__init__.py +0 -0
  7. {gitcast-1.0.5 → gitcast-1.0.6}/ai/formatter.py +0 -0
  8. {gitcast-1.0.5 → gitcast-1.0.6}/ai/generator.py +0 -0
  9. {gitcast-1.0.5 → gitcast-1.0.6}/ai/prompts.py +0 -0
  10. {gitcast-1.0.5 → gitcast-1.0.6}/ai/viral_patterns.py +0 -0
  11. {gitcast-1.0.5 → gitcast-1.0.6}/api/__init__.py +0 -0
  12. {gitcast-1.0.5 → gitcast-1.0.6}/api/analytics.py +0 -0
  13. {gitcast-1.0.5 → gitcast-1.0.6}/api/auth.py +0 -0
  14. {gitcast-1.0.5 → gitcast-1.0.6}/api/auth_middleware.py +0 -0
  15. {gitcast-1.0.5 → gitcast-1.0.6}/api/auth_routes.py +0 -0
  16. {gitcast-1.0.5 → gitcast-1.0.6}/api/monitoring.py +0 -0
  17. {gitcast-1.0.5 → gitcast-1.0.6}/api/payload.py +0 -0
  18. gitcast-1.0.6/api/ratelimit.py +18 -0
  19. {gitcast-1.0.5 → gitcast-1.0.6}/api/routes.py +22 -41
  20. {gitcast-1.0.5 → gitcast-1.0.6}/api/server.py +0 -0
  21. {gitcast-1.0.5 → gitcast-1.0.6}/api/validators.py +0 -0
  22. {gitcast-1.0.5 → gitcast-1.0.6}/assets/__init__.py +0 -0
  23. {gitcast-1.0.5 → gitcast-1.0.6}/assets/favicon-16x16.png +0 -0
  24. {gitcast-1.0.5 → gitcast-1.0.6}/assets/favicon-32x32.png +0 -0
  25. {gitcast-1.0.5 → gitcast-1.0.6}/assets/favicon-64x64.png +0 -0
  26. {gitcast-1.0.5 → gitcast-1.0.6}/assets/favicon.ico +0 -0
  27. {gitcast-1.0.5 → gitcast-1.0.6}/assets/icon.png +0 -0
  28. {gitcast-1.0.5 → gitcast-1.0.6}/cli/.env.example +0 -0
  29. {gitcast-1.0.5 → gitcast-1.0.6}/cli/__init__.py +0 -0
  30. {gitcast-1.0.5 → gitcast-1.0.6}/cli/gitcast.py +0 -0
  31. {gitcast-1.0.5 → gitcast-1.0.6}/config/__init__.py +0 -0
  32. {gitcast-1.0.5 → gitcast-1.0.6}/config/settings.py +0 -0
  33. {gitcast-1.0.5 → gitcast-1.0.6}/core/__init__.py +0 -0
  34. {gitcast-1.0.5 → gitcast-1.0.6}/core/capture.py +0 -0
  35. {gitcast-1.0.5 → gitcast-1.0.6}/core/codebase_reader.py +0 -0
  36. {gitcast-1.0.5 → gitcast-1.0.6}/core/framing.py +0 -0
  37. {gitcast-1.0.5 → gitcast-1.0.6}/core/hotkey.py +0 -0
  38. {gitcast-1.0.5 → gitcast-1.0.6}/core/log_stream.py +0 -0
  39. {gitcast-1.0.5 → gitcast-1.0.6}/core/ocr.py +0 -0
  40. {gitcast-1.0.5 → gitcast-1.0.6}/core/screenshot_session.py +0 -0
  41. {gitcast-1.0.5 → gitcast-1.0.6}/core/security.py +0 -0
  42. {gitcast-1.0.5 → gitcast-1.0.6}/core/tray.py +0 -0
  43. {gitcast-1.0.5 → gitcast-1.0.6}/docs/api-keys.md +0 -0
  44. {gitcast-1.0.5 → gitcast-1.0.6}/docs/article-generator.md +0 -0
  45. {gitcast-1.0.5 → gitcast-1.0.6}/docs/capture-flow.md +0 -0
  46. {gitcast-1.0.5 → gitcast-1.0.6}/docs/changelog.md +0 -0
  47. {gitcast-1.0.5 → gitcast-1.0.6}/docs/contributing.md +0 -0
  48. {gitcast-1.0.5 → gitcast-1.0.6}/docs/dashboard.md +0 -0
  49. {gitcast-1.0.5 → gitcast-1.0.6}/docs/insights.md +0 -0
  50. {gitcast-1.0.5 → gitcast-1.0.6}/docs/introduction.md +0 -0
  51. {gitcast-1.0.5 → gitcast-1.0.6}/docs/post-formats.md +0 -0
  52. {gitcast-1.0.5 → gitcast-1.0.6}/docs/providers.md +0 -0
  53. gitcast-1.0.6/docs/pypi-errors.md +68 -0
  54. {gitcast-1.0.5 → gitcast-1.0.6}/docs/quickstart.md +0 -0
  55. {gitcast-1.0.5 → gitcast-1.0.6}/docs/sprint-mode.md +0 -0
  56. {gitcast-1.0.5 → gitcast-1.0.6/gitcast.egg-info}/PKG-INFO +77 -50
  57. {gitcast-1.0.5 → gitcast-1.0.6}/gitcast.egg-info/SOURCES.txt +3 -0
  58. {gitcast-1.0.5 → gitcast-1.0.6}/gitcast.egg-info/dependency_links.txt +0 -0
  59. {gitcast-1.0.5 → gitcast-1.0.6}/gitcast.egg-info/entry_points.txt +0 -1
  60. {gitcast-1.0.5 → gitcast-1.0.6}/gitcast.egg-info/requires.txt +0 -0
  61. {gitcast-1.0.5 → gitcast-1.0.6}/gitcast.egg-info/top_level.txt +0 -0
  62. {gitcast-1.0.5 → gitcast-1.0.6}/publisher/__init__.py +0 -0
  63. {gitcast-1.0.5 → gitcast-1.0.6}/publisher/clipboard.py +0 -0
  64. {gitcast-1.0.5 → gitcast-1.0.6}/publisher/twitter.py +0 -0
  65. {gitcast-1.0.5 → gitcast-1.0.6}/pyproject.toml +0 -0
  66. {gitcast-1.0.5 → gitcast-1.0.6}/requirements.txt +0 -0
  67. {gitcast-1.0.5 → gitcast-1.0.6}/setup.cfg +4 -4
  68. {gitcast-1.0.5 → gitcast-1.0.6}/setup.py +33 -1
  69. {gitcast-1.0.5 → gitcast-1.0.6}/storage/__init__.py +0 -0
  70. {gitcast-1.0.5 → gitcast-1.0.6}/storage/cleanup.py +0 -0
  71. {gitcast-1.0.5 → gitcast-1.0.6}/storage/engagement.py +0 -0
  72. {gitcast-1.0.5 → gitcast-1.0.6}/storage/insights.py +0 -0
  73. {gitcast-1.0.5 → gitcast-1.0.6}/storage/key_manager.py +0 -0
  74. {gitcast-1.0.5 → gitcast-1.0.6}/storage/logger.py +0 -0
  75. {gitcast-1.0.5 → gitcast-1.0.6}/storage/metrics.py +0 -0
  76. {gitcast-1.0.5 → gitcast-1.0.6}/storage/sprint.py +0 -0
  77. {gitcast-1.0.5 → gitcast-1.0.6}/storage/streak.py +0 -0
  78. {gitcast-1.0.5 → gitcast-1.0.6}/storage/supabase_client.py +0 -0
  79. {gitcast-1.0.5 → gitcast-1.0.6}/storage/tone_memory.py +0 -0
  80. gitcast-1.0.6/tests/test_keys.py +86 -0
  81. gitcast-1.0.6/tests/test_ocr_fix.py +77 -0
  82. {gitcast-1.0.5 → gitcast-1.0.6}/ui/__init__.py +0 -0
  83. {gitcast-1.0.5 → gitcast-1.0.6}/web/__init__.py +0 -0
  84. {gitcast-1.0.5 → gitcast-1.0.6}/web/index.html +0 -0
  85. {gitcast-1.0.5 → gitcast-1.0.6}/web/landing.html +0 -0
  86. gitcast-1.0.5/api/ratelimit.py +0 -9
File without changes
File without changes
File without changes
@@ -1,50 +1,77 @@
1
- Metadata-Version: 2.1
2
- Name: gitcast
3
- Version: 1.0.5
4
- Summary: git diff → published post. under 60 seconds.
5
- Home-page: https://github.com/drizzy765/gitcast
6
- Author: Timilehin Agoro
7
- Author-email: agorotimilehi05@gmail.com
8
- License: MIT
9
- Platform: UNKNOWN
10
- Requires-Python: >=3.10
11
- Description-Content-Type: text/markdown
12
- License-File: LICENSE
13
-
14
- # Gitcast
15
-
16
- On-demand, privacy-first desktop utility that captures active window context,
17
- extracts code changes, and generates platform-ready X (Twitter) posts with one keystroke.
18
-
19
- ## Install
20
-
21
- ```bash
22
- pip install gitcast
23
- ```
24
-
25
- ## Setup
26
-
27
- 1. Install Tesseract OCR:
28
- - Windows: https://github.com/UB-Mannheim/tesseract/wiki
29
- - Mac: `brew install tesseract`
30
- - Linux: `sudo apt install tesseract-ocr`
31
-
32
- 2. Add your API keys:
33
- ```bash
34
- gitcast --setup
35
- ```
36
- (opens `.env` file for editing)
37
-
38
- 3. Run:
39
- ```bash
40
- gitcast
41
- ```
42
-
43
- Dashboard opens at http://127.0.0.1:8000
44
- Press Ctrl+Shift+P from anywhere to capture.
45
-
46
- ## Stack
47
- - Python 3.11+
48
- - FastAPI, pynput, mss, Tesseract, Groq API, Tweepy
49
-
50
-
1
+ Metadata-Version: 2.4
2
+ Name: gitcast
3
+ Version: 1.0.6
4
+ Summary: git diff → published post. under 60 seconds.
5
+ Home-page: https://github.com/drizzy765/gitcast
6
+ Author: Timilehin Agoro
7
+ Author-email: agorotimilehi05@gmail.com
8
+ License: MIT
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pynput
13
+ Requires-Dist: pystray
14
+ Requires-Dist: Pillow
15
+ Requires-Dist: fastapi
16
+ Requires-Dist: uvicorn
17
+ Requires-Dist: python-dotenv
18
+ Requires-Dist: groq
19
+ Requires-Dist: tweepy
20
+ Requires-Dist: mss
21
+ Requires-Dist: pytesseract
22
+ Requires-Dist: httpx
23
+ Requires-Dist: supabase
24
+ Requires-Dist: cryptography
25
+ Requires-Dist: python-jose[cryptography]
26
+ Requires-Dist: rich
27
+ Requires-Dist: keyboard
28
+ Requires-Dist: slowapi
29
+ Requires-Dist: limits
30
+ Requires-Dist: sentry-sdk[fastapi]
31
+ Requires-Dist: posthog
32
+ Dynamic: author
33
+ Dynamic: author-email
34
+ Dynamic: description
35
+ Dynamic: description-content-type
36
+ Dynamic: home-page
37
+ Dynamic: license
38
+ Dynamic: license-file
39
+ Dynamic: requires-dist
40
+ Dynamic: requires-python
41
+ Dynamic: summary
42
+
43
+ # Gitcast
44
+
45
+ On-demand, privacy-first desktop utility that captures active window context,
46
+ extracts code changes, and generates platform-ready X (Twitter) posts with one keystroke.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install gitcast
52
+ ```
53
+
54
+ ## Setup
55
+
56
+ 1. Install Tesseract OCR:
57
+ - Windows: https://github.com/UB-Mannheim/tesseract/wiki
58
+ - Mac: `brew install tesseract`
59
+ - Linux: `sudo apt install tesseract-ocr`
60
+
61
+ 2. Add your API keys:
62
+ ```bash
63
+ gitcast --setup
64
+ ```
65
+ (opens `.env` file for editing)
66
+
67
+ 3. Run:
68
+ ```bash
69
+ gitcast
70
+ ```
71
+
72
+ Dashboard opens at http://127.0.0.1:8000
73
+ Press Ctrl+Shift+P from anywhere to capture.
74
+
75
+ ## Stack
76
+ - Python 3.11+
77
+ - FastAPI, pynput, mss, Tesseract, Groq API, Tweepy
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
@@ -0,0 +1,18 @@
1
+ from slowapi import Limiter
2
+ from slowapi.util import get_remote_address
3
+ from fastapi import Request
4
+
5
+
6
+ def get_client_ip(request: Request) -> str:
7
+ client_host = request.client.host if request.client else None
8
+ if client_host in ["127.0.0.1", "localhost", "::1"]:
9
+ return None
10
+ return get_remote_address(request)
11
+
12
+
13
+ limiter = Limiter(key_func=get_client_ip, default_limits=["60/minute"], headers_enabled=False)
14
+
15
+
16
+ if __name__ == "__main__":
17
+ print("[RateLimit] Default limit: 60/minute (bypassed for localhost)")
18
+
@@ -541,8 +541,7 @@ def _update_settings_for_user(user_id: str, values: dict) -> dict:
541
541
 
542
542
 
543
543
  @router.get("/keys/status")
544
- def get_ai_keys_status():
545
- user_id = LOCAL_USER_ID
544
+ def get_ai_keys_status(user_id: str = Depends(get_current_user)):
546
545
  if _is_local_user(user_id):
547
546
  env_values = dotenv_values(_env_path())
548
547
  return {
@@ -668,9 +667,8 @@ def add_to_waitlist(request: WaitlistRequest):
668
667
 
669
668
  @router.patch("/settings")
670
669
  @router.post("/settings")
671
- def update_settings(update: SettingsUpdate):
670
+ def update_settings(update: SettingsUpdate, user_id: str = Depends(get_current_user)):
672
671
  """Updates user project settings."""
673
- user_id = LOCAL_USER_ID
674
672
  values = update.dict(exclude_unset=True)
675
673
  if "project_narrative" in values and values["project_narrative"] is not None:
676
674
  values["project_narrative"] = sanitize_text(values["project_narrative"])
@@ -680,9 +678,8 @@ def update_settings(update: SettingsUpdate):
680
678
 
681
679
  @router.post("/publish")
682
680
  @limiter.limit("10/minute")
683
- async def publish(request: Request, body: PublishRequest):
681
+ async def publish(request: Request, body: PublishRequest, user_id: str = Depends(get_current_user)):
684
682
  """Publishes a post to X (Twitter)."""
685
- user_id = LOCAL_USER_ID
686
683
  from publisher.twitter import publish_post
687
684
  try:
688
685
  post_text = sanitize_text(body.post_text)
@@ -783,8 +780,7 @@ async def get_framed_screenshot(filename: str):
783
780
 
784
781
  @router.post("/generate", response_model=GenerateResponse)
785
782
  @limiter.limit("10/minute")
786
- async def generate(request: Request, body: GenerateRequest):
787
- user_id = LOCAL_USER_ID
783
+ async def generate(request: Request, body: GenerateRequest, user_id: str = Depends(get_current_user)):
788
784
  payload = body.dict()
789
785
  for key, value in list(payload.items()):
790
786
  if isinstance(value, str):
@@ -866,9 +862,8 @@ def get_current_draft():
866
862
 
867
863
  @router.post("/chat")
868
864
  @limiter.limit("20/minute")
869
- async def chat_refine(request: Request, body: ChatRequest):
865
+ async def chat_refine(request: Request, body: ChatRequest, user_id: str = Depends(get_current_user)):
870
866
  """Refines a post variation using AI chat."""
871
- user_id = LOCAL_USER_ID
872
867
  if not CURRENT_DRAFT.exists():
873
868
  raise HTTPException(status_code=400, detail="No active draft to refine.")
874
869
 
@@ -926,9 +921,8 @@ async def chat_refine(request: Request, body: ChatRequest):
926
921
 
927
922
 
928
923
  @router.post("/cli/trigger")
929
- async def cli_trigger(request: CliTriggerRequest):
924
+ async def cli_trigger(request: CliTriggerRequest, user_id: str = Depends(get_current_user)):
930
925
  """Triggers a capture workflow with a specific thought from the CLI."""
931
- user_id = LOCAL_USER_ID
932
926
  from core.capture import run_capture
933
927
  from core.ocr import run_ocr
934
928
  from api.payload import build_payload
@@ -964,9 +958,8 @@ async def cli_trigger(request: CliTriggerRequest):
964
958
 
965
959
 
966
960
  @router.post("/capture/trigger")
967
- async def ui_trigger_capture(request: CaptureTriggerRequest):
961
+ async def ui_trigger_capture(request: CaptureTriggerRequest, user_id: str = Depends(get_current_user)):
968
962
  """Triggers a capture workflow from the UI."""
969
- user_id = LOCAL_USER_ID
970
963
  from core.capture import run_capture
971
964
  from core.ocr import run_ocr
972
965
  from api.payload import build_payload
@@ -1045,9 +1038,8 @@ def list_screenshots():
1045
1038
 
1046
1039
 
1047
1040
  @router.post("/screenshot/recommend")
1048
- async def recommend_screenshot(request: RecommendRequest):
1041
+ async def recommend_screenshot(request: RecommendRequest, user_id: str = Depends(get_current_user)):
1049
1042
  """Uses AI to recommend where to post a specific screenshot."""
1050
- user_id = LOCAL_USER_ID
1051
1043
  from config.settings import STORAGE_DIR
1052
1044
  path = STORAGE_DIR / "screenshots" / sanitize_text(request.filename)
1053
1045
  if not path.exists():
@@ -1137,16 +1129,14 @@ def get_formats():
1137
1129
 
1138
1130
 
1139
1131
  @router.get("/settings/plan")
1140
- def get_plan():
1141
- user_id = LOCAL_USER_ID
1132
+ def get_plan(user_id: str = Depends(get_current_user)):
1142
1133
  settings = _settings_for_user(user_id)
1143
1134
  preferred = settings.get("preferred_providers") or {}
1144
1135
  return {"plan": preferred.get("twitter_plan", get_twitter_plan())}
1145
1136
 
1146
1137
 
1147
1138
  @router.put("/settings/plan")
1148
- def update_plan(update: PlanUpdate):
1149
- user_id = LOCAL_USER_ID
1139
+ def update_plan(update: PlanUpdate, user_id: str = Depends(get_current_user)):
1150
1140
  plan = sanitize_text(update.plan).lower()
1151
1141
  settings = _settings_for_user(user_id)
1152
1142
  preferred = settings.get("preferred_providers") or {}
@@ -1156,14 +1146,12 @@ def update_plan(update: PlanUpdate):
1156
1146
 
1157
1147
 
1158
1148
  @router.get("/settings")
1159
- def get_all_settings():
1160
- user_id = LOCAL_USER_ID
1149
+ def get_all_settings(user_id: str = Depends(get_current_user)):
1161
1150
  return _settings_for_user(user_id)
1162
1151
 
1163
1152
 
1164
1153
  @router.post("/settings/sprint/toggle")
1165
- def sprint_toggle():
1166
- user_id = LOCAL_USER_ID
1154
+ def sprint_toggle(user_id: str = Depends(get_current_user)):
1167
1155
  settings = _settings_for_user(user_id)
1168
1156
  new_state = not bool(settings.get("sprint_mode"))
1169
1157
  _update_settings_for_user(user_id, {"sprint_mode": new_state})
@@ -1171,15 +1159,13 @@ def sprint_toggle():
1171
1159
 
1172
1160
 
1173
1161
  @router.get("/settings/keys")
1174
- def get_keys_status():
1175
- user_id = LOCAL_USER_ID
1176
- return get_ai_keys_status()
1162
+ def get_keys_status(user_id: str = Depends(get_current_user)):
1163
+ return get_ai_keys_status(user_id=user_id)
1177
1164
 
1178
1165
 
1179
1166
  @router.get("/diagnose")
1180
- async def diagnose_connectivity():
1167
+ async def diagnose_connectivity(user_id: str = Depends(get_current_user)):
1181
1168
  """Diagnoses network and authentication status of all configured AI providers."""
1182
- user_id = LOCAL_USER_ID
1183
1169
  import ai.generator
1184
1170
  import httpx
1185
1171
 
@@ -1248,9 +1234,8 @@ async def diagnose_connectivity():
1248
1234
 
1249
1235
  @router.post("/article/generate")
1250
1236
  @limiter.limit("5/minute")
1251
- async def generate_article(request: Request, body: ArticleGenerateRequest):
1237
+ async def generate_article(request: Request, body: ArticleGenerateRequest, user_id: str = Depends(get_current_user)):
1252
1238
  """Generates a full technical article from sprint context."""
1253
- user_id = LOCAL_USER_ID
1254
1239
  if not CURRENT_DRAFT.exists():
1255
1240
  raise HTTPException(status_code=400, detail="No active draft to generate article from.")
1256
1241
 
@@ -1286,9 +1271,8 @@ async def generate_article(request: Request, body: ArticleGenerateRequest):
1286
1271
 
1287
1272
 
1288
1273
  @router.post("/article/refine")
1289
- async def refine_article(request: ArticleRefineRequest):
1274
+ async def refine_article(request: ArticleRefineRequest, user_id: str = Depends(get_current_user)):
1290
1275
  """Refines an article draft based on user instructions."""
1291
- user_id = LOCAL_USER_ID
1292
1276
  current_article = sanitize_text(request.current_article)
1293
1277
  instruction = sanitize_text(request.instruction)
1294
1278
  injection = check_prompt_injection(instruction)
@@ -1316,8 +1300,7 @@ def thread_split(request: ThreadSplitRequest):
1316
1300
 
1317
1301
 
1318
1302
  @router.post("/posts/verify")
1319
- async def verify_logged_post(request: PostVerifyRequest):
1320
- user_id = LOCAL_USER_ID
1303
+ async def verify_logged_post(request: PostVerifyRequest, user_id: str = Depends(get_current_user)):
1321
1304
  post_id = sanitize_text(request.post_id)
1322
1305
  post_url = sanitize_text(request.post_url or "")
1323
1306
  result = await safe_update_post(post_id, {
@@ -1333,8 +1316,7 @@ async def verify_logged_post(request: PostVerifyRequest):
1333
1316
 
1334
1317
 
1335
1318
  @router.post("/posts/decline")
1336
- async def decline_logged_post(request: PostDeclineRequest):
1337
- user_id = LOCAL_USER_ID
1319
+ async def decline_logged_post(request: PostDeclineRequest, user_id: str = Depends(get_current_user)):
1338
1320
  result = await safe_update_post(sanitize_text(request.post_id), {"declined": True})
1339
1321
  if not result:
1340
1322
  raise HTTPException(status_code=404, detail="post not found")
@@ -1347,8 +1329,7 @@ async def unverified_posts():
1347
1329
 
1348
1330
 
1349
1331
  @router.post("/metrics/save")
1350
- async def save_post_metrics(request: MetricsSaveRequest):
1351
- user_id = LOCAL_USER_ID
1332
+ async def save_post_metrics(request: MetricsSaveRequest, user_id: str = Depends(get_current_user)):
1352
1333
  values = {
1353
1334
  "impressions": request.impressions,
1354
1335
  "likes": request.likes,
@@ -1423,9 +1404,9 @@ async def get_post_metrics(post_id: str):
1423
1404
 
1424
1405
 
1425
1406
  @router.get("/insights")
1426
- async def get_insights():
1407
+ async def get_insights(user_id: str = Depends(get_current_user)):
1427
1408
  now = time.time()
1428
- cache_key = f"data:{LOCAL_USER_ID}"
1409
+ cache_key = f"data:{user_id}"
1429
1410
  if _INSIGHTS_CACHE.get(cache_key) is not None and now - _INSIGHTS_CACHE["timestamp"] < 3600:
1430
1411
  return _INSIGHTS_CACHE[cache_key]
1431
1412
 
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
File without changes
File without changes
File without changes
@@ -0,0 +1,68 @@
1
+ # PyPI Release & Execution Issues Log
2
+
3
+ This document records the issues faced during the testing, packaging, and execution of `gitcast` on PyPI, along with their resolutions and current active status.
4
+
5
+ ---
6
+
7
+ ## 1. Resolved Issues
8
+
9
+ ### Fixed in `gitcast==1.0.6`
10
+
11
+ #### Issue 1.4: FastAPI Server Rate Limiting on Localhost
12
+ * **Symptom**: Browser console logs API call failures when calling `api/article/generate` or other endpoints via the dashboard UI with a 429 Too Many Requests status code.
13
+ * **Root Cause**: The FastAPI server used slowapi's default `get_remote_address` rate-limiting key, which rate-limited local requests when they exceeded the threshold (e.g. 5/minute for article generation).
14
+ * **Resolution**: Replaced the default `get_remote_address` with a custom `get_client_ip` function in `api/ratelimit.py`. This function returns `None` for any localhost client (`127.0.0.1`, `localhost`, `::1`), which tells slowapi to bypass rate limiting completely for local executions.
15
+
16
+ #### Issue 1.5: BYOK (Bring Your Own Key) Feature Ignored Authenticated Users
17
+ * **Symptom**: When a user logged in via GitHub/Supabase and configured their own provider keys (BYOK), the server would still hit rate limits or token exhaustion on the developer's default keys.
18
+ * **Root Cause**: In `api/routes.py`, 19 routes hardcoded `user_id = LOCAL_USER_ID`. This caused the server to completely ignore the user ID of logged-in users, preventing their custom BYOK keys (stored in the Supabase DB) from being resolved and loaded.
19
+ * **Resolution**: Updated all 19 endpoints in `api/routes.py` to accept and use the dynamically resolved user ID using FastAPI's dependency injection (`user_id: str = Depends(get_current_user)`). This allows logged-in users to load their custom keys dynamically.
20
+
21
+ ### Fixed in `gitcast==1.0.5`
22
+
23
+ #### Issue 1.1: Supabase Configuration Load Failure
24
+ * **Symptom**: Running `gitcast` from an external directory (e.g. `C:\Users\USER\Documents\snakegame`) outputs:
25
+ ```text
26
+ [STORAGE] Supabase get_posts failed: SUPABASE_URL and SUPABASE_SERVICE_KEY must be configured. Falling back to local JSON.
27
+ ```
28
+ * **Root Cause**: The dynamic resolution of `BASE_DIR = Path(__file__).resolve().parent.parent` in `settings.py` evaluated to Python's virtualenv `site-packages/gitcast/` directory rather than the active workspace directory. As a result, the `.env` file containing the Supabase keys was not found.
29
+ * **Resolution**: Replaced the direct loading of `BASE_DIR / ".env"` with a multi-path environment file loader. It now searches the following locations in order of priority:
30
+ 1. A custom path defined in `GITCAST_ENV_PATH`
31
+ 2. The current working directory (`os.getcwd() / ".env"`)
32
+ 3. The parent directory of the active virtual environment (`sys.prefix / ".." / ".env"`), resolving to `context-engine/.env`
33
+ 4. Standard user directories (`~/.gitcast/.env` and `~/.gitcast.env`)
34
+ 5. The fallback package directory (`BASE_DIR / ".env"`)
35
+
36
+ #### Issue 1.2: UI Settings/BYOK Writes to Package Internals
37
+ * **Symptom**: Saving API keys via the dashboard (BYOK feature) or running `gitcast --setup` would write to `site-packages/gitcast/.env`, which was volatile (deleted on package updates) and sometimes generated permission access errors.
38
+ * **Resolution**: Updated `api/routes.py` and `cli/gitcast.py` to write to the resolved active `.env` file, falling back to a persistent user-home folder `~/.gitcast/.env` if no local env file existed.
39
+
40
+ #### Issue 1.3: Package Build Failure (`bdist_wheel` Invalid Command)
41
+ * **Symptom**: Attempting to package the build output using `python setup.py sdist bdist_wheel` generated the error:
42
+ ```text
43
+ error: invalid command 'bdist_wheel'
44
+ ```
45
+ * **Root Cause**: The `wheel` package was not installed in the python environment, causing `setuptools` to not register the command.
46
+ * **Resolution**: Installed `wheel` package (`pip install wheel`) prior to building, enabling setuptools to build the binary wheel package successfully.
47
+
48
+ ---
49
+
50
+ ## 2. Active Issues (To be Addressed)
51
+
52
+ ### Issue 2.1: Post Generation Fails silently (gitcast>=1.0.5)
53
+ * **Symptom**: `gitcast` starts up successfully, the FastAPI server listens on `http://127.0.0.1:8000`, the pygame modules load, and the CLI runs, but the application does not generate posts.
54
+ * **Console Logs**:
55
+ ```text
56
+ (venv) PS C:\Users\USER\Documents\snakegame> gitcast
57
+ [Monitoring] Sentry not configured - skipping
58
+
59
+ [Auth] Session Token: 549cfdc2-c987-416d-8a70-8b0dc5f2315b
60
+ [Server] Starting Gitcast API on http://127.0.0.1:8000
61
+
62
+ [Auth] Session Token: 549cfdc2-c987-416d-8a70-8b0dc5f2315b
63
+ [Server] Starting Gitcast API on http://127.0.0.1:8000
64
+ pygame 2.6.1 (SDL 2.28.4, Python 3.10.0)
65
+ Hello from the pygame community. https://www.pygame.org/contribute.html
66
+ ```
67
+ * **Status**: Logged and waiting for future triage. The application starts, but key bindings or endpoints triggers are failing to generate the actual post output.
68
+
File without changes
File without changes
@@ -1,50 +1,77 @@
1
- Metadata-Version: 2.1
2
- Name: gitcast
3
- Version: 1.0.5
4
- Summary: git diff → published post. under 60 seconds.
5
- Home-page: https://github.com/drizzy765/gitcast
6
- Author: Timilehin Agoro
7
- Author-email: agorotimilehi05@gmail.com
8
- License: MIT
9
- Platform: UNKNOWN
10
- Requires-Python: >=3.10
11
- Description-Content-Type: text/markdown
12
- License-File: LICENSE
13
-
14
- # Gitcast
15
-
16
- On-demand, privacy-first desktop utility that captures active window context,
17
- extracts code changes, and generates platform-ready X (Twitter) posts with one keystroke.
18
-
19
- ## Install
20
-
21
- ```bash
22
- pip install gitcast
23
- ```
24
-
25
- ## Setup
26
-
27
- 1. Install Tesseract OCR:
28
- - Windows: https://github.com/UB-Mannheim/tesseract/wiki
29
- - Mac: `brew install tesseract`
30
- - Linux: `sudo apt install tesseract-ocr`
31
-
32
- 2. Add your API keys:
33
- ```bash
34
- gitcast --setup
35
- ```
36
- (opens `.env` file for editing)
37
-
38
- 3. Run:
39
- ```bash
40
- gitcast
41
- ```
42
-
43
- Dashboard opens at http://127.0.0.1:8000
44
- Press Ctrl+Shift+P from anywhere to capture.
45
-
46
- ## Stack
47
- - Python 3.11+
48
- - FastAPI, pynput, mss, Tesseract, Groq API, Tweepy
49
-
50
-
1
+ Metadata-Version: 2.4
2
+ Name: gitcast
3
+ Version: 1.0.6
4
+ Summary: git diff → published post. under 60 seconds.
5
+ Home-page: https://github.com/drizzy765/gitcast
6
+ Author: Timilehin Agoro
7
+ Author-email: agorotimilehi05@gmail.com
8
+ License: MIT
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pynput
13
+ Requires-Dist: pystray
14
+ Requires-Dist: Pillow
15
+ Requires-Dist: fastapi
16
+ Requires-Dist: uvicorn
17
+ Requires-Dist: python-dotenv
18
+ Requires-Dist: groq
19
+ Requires-Dist: tweepy
20
+ Requires-Dist: mss
21
+ Requires-Dist: pytesseract
22
+ Requires-Dist: httpx
23
+ Requires-Dist: supabase
24
+ Requires-Dist: cryptography
25
+ Requires-Dist: python-jose[cryptography]
26
+ Requires-Dist: rich
27
+ Requires-Dist: keyboard
28
+ Requires-Dist: slowapi
29
+ Requires-Dist: limits
30
+ Requires-Dist: sentry-sdk[fastapi]
31
+ Requires-Dist: posthog
32
+ Dynamic: author
33
+ Dynamic: author-email
34
+ Dynamic: description
35
+ Dynamic: description-content-type
36
+ Dynamic: home-page
37
+ Dynamic: license
38
+ Dynamic: license-file
39
+ Dynamic: requires-dist
40
+ Dynamic: requires-python
41
+ Dynamic: summary
42
+
43
+ # Gitcast
44
+
45
+ On-demand, privacy-first desktop utility that captures active window context,
46
+ extracts code changes, and generates platform-ready X (Twitter) posts with one keystroke.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install gitcast
52
+ ```
53
+
54
+ ## Setup
55
+
56
+ 1. Install Tesseract OCR:
57
+ - Windows: https://github.com/UB-Mannheim/tesseract/wiki
58
+ - Mac: `brew install tesseract`
59
+ - Linux: `sudo apt install tesseract-ocr`
60
+
61
+ 2. Add your API keys:
62
+ ```bash
63
+ gitcast --setup
64
+ ```
65
+ (opens `.env` file for editing)
66
+
67
+ 3. Run:
68
+ ```bash
69
+ gitcast
70
+ ```
71
+
72
+ Dashboard opens at http://127.0.0.1:8000
73
+ Press Ctrl+Shift+P from anywhere to capture.
74
+
75
+ ## Stack
76
+ - Python 3.11+
77
+ - FastAPI, pynput, mss, Tesseract, Groq API, Tweepy
@@ -52,6 +52,7 @@ docs/insights.md
52
52
  docs/introduction.md
53
53
  docs/post-formats.md
54
54
  docs/providers.md
55
+ docs/pypi-errors.md
55
56
  docs/quickstart.md
56
57
  docs/sprint-mode.md
57
58
  gitcast.egg-info/PKG-INFO
@@ -74,6 +75,8 @@ storage/sprint.py
74
75
  storage/streak.py
75
76
  storage/supabase_client.py
76
77
  storage/tone_memory.py
78
+ tests/test_keys.py
79
+ tests/test_ocr_fix.py
77
80
  ui/__init__.py
78
81
  web/__init__.py
79
82
  web/index.html
@@ -1,3 +1,2 @@
1
1
  [console_scripts]
2
2
  gitcast = cli.gitcast:main
3
-
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,4 +1,4 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -1,6 +1,38 @@
1
1
  import os
2
+ import shutil
2
3
  from setuptools import setup, find_packages
3
4
 
5
+ # Monkeypatch copystat, os.link, chmod, and utime to avoid "Operation not permitted" errors under WSL mounts
6
+ try:
7
+ if hasattr(os, 'link'):
8
+ del os.link
9
+
10
+ orig_chmod = os.chmod
11
+ def patched_chmod(path, mode, *args, **kwargs):
12
+ try:
13
+ orig_chmod(path, mode, *args, **kwargs)
14
+ except OSError:
15
+ pass
16
+ os.chmod = patched_chmod
17
+
18
+ orig_utime = os.utime
19
+ def patched_utime(path, times=None, *args, **kwargs):
20
+ try:
21
+ orig_utime(path, times, *args, **kwargs)
22
+ except OSError:
23
+ pass
24
+ os.utime = patched_utime
25
+
26
+ orig_copystat = shutil.copystat
27
+ def patched_copystat(src, dst, *args, **kwargs):
28
+ try:
29
+ orig_copystat(src, dst, *args, **kwargs)
30
+ except OSError:
31
+ pass
32
+ shutil.copystat = patched_copystat
33
+ except Exception:
34
+ pass
35
+
4
36
  # Read the contents of README.md
5
37
  this_directory = os.path.abspath(os.path.dirname(__file__))
6
38
  with open(os.path.join(this_directory, "README.md"), encoding="utf-8") as f:
@@ -19,7 +51,7 @@ if os.path.exists(requirements_path):
19
51
 
20
52
  setup(
21
53
  name="gitcast",
22
- version="1.0.5",
54
+ version="1.0.6",
23
55
  description="git diff → published post. under 60 seconds.",
24
56
  long_description=long_description,
25
57
  long_description_content_type="text/markdown",
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
@@ -0,0 +1,86 @@
1
+ import asyncio
2
+ import httpx
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ # Add project root to path so we can import config
8
+ sys.path.append(str(Path(__file__).resolve().parent.parent))
9
+
10
+ from config.settings import (
11
+ GROQ_API_KEY, GEMINI_API_KEY,
12
+ MOONSHOT_API_KEY, CEREBRAS_API_KEY, OPENROUTER_API_KEY
13
+ )
14
+
15
+ async def test_provider(name, url, headers, body, is_gemini=False):
16
+ print(f"Testing {name.upper()}...", end=" ", flush=True)
17
+ async with httpx.AsyncClient(timeout=10) as client:
18
+ try:
19
+ if is_gemini:
20
+ res = await client.post(f"{url}?key={GEMINI_API_KEY}", json=body)
21
+ else:
22
+ res = await client.post(url, headers=headers, json=body)
23
+
24
+ if res.status_code == 200:
25
+ print("✅ WORKING")
26
+ return True
27
+ else:
28
+ print(f"❌ FAILED ({res.status_code})")
29
+ try:
30
+ error_msg = res.json().get('error', {}).get('message', res.text[:100])
31
+ print(f" Reason: {error_msg}")
32
+ except:
33
+ print(f" Reason: {res.text[:100]}")
34
+ return False
35
+ except Exception as e:
36
+ print(f"❌ ERROR: {str(e)}")
37
+ return False
38
+
39
+ async def main():
40
+ print("=== Context Engine API Key Connectivity Test ===\n")
41
+
42
+ # 1. Groq
43
+ await test_provider(
44
+ "Groq",
45
+ "https://api.groq.com/openai/v1/chat/completions",
46
+ {"Authorization": f"Bearer {GROQ_API_KEY}"},
47
+ {"model": "llama-3.3-70b-versatile", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
48
+ )
49
+
50
+ # 2. OpenRouter
51
+ await test_provider(
52
+ "OpenRouter",
53
+ "https://openrouter.ai/api/v1/chat/completions",
54
+ {"Authorization": f"Bearer {OPENROUTER_API_KEY}"},
55
+ {"model": "qwen/qwen3-coder:free", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
56
+ )
57
+
58
+ # 3. Kimi (Moonshot)
59
+ await test_provider(
60
+ "Moonshot (Kimi)",
61
+ "https://api.moonshot.cn/v1/chat/completions",
62
+ {"Authorization": f"Bearer {MOONSHOT_API_KEY}"},
63
+ {"model": "moonshot-v1-8k", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
64
+ )
65
+
66
+ # 4. Cerebras
67
+ await test_provider(
68
+ "Cerebras",
69
+ "https://api.cerebras.ai/v1/chat/completions",
70
+ {"Authorization": f"Bearer {CEREBRAS_API_KEY}"},
71
+ {"model": "llama3.3-70b", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
72
+ )
73
+
74
+ # 5. Gemini
75
+ await test_provider(
76
+ "Gemini",
77
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent",
78
+ {},
79
+ {"contents": [{"parts": [{"text": "hi"}]}]},
80
+ is_gemini=True
81
+ )
82
+
83
+ print("\nTest complete.")
84
+
85
+ if __name__ == "__main__":
86
+ asyncio.run(main())
@@ -0,0 +1,77 @@
1
+ import json
2
+ import asyncio
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ sys.path.append(str(Path(__file__).resolve().parent.parent))
7
+
8
+ from api.payload import build_payload
9
+ from api.routes import generate_article, ArticleGenerateRequest
10
+ from config.settings import CURRENT_DRAFT
11
+ from fastapi import HTTPException
12
+
13
+ async def test_ocr_text_in_payload():
14
+ print("Testing ocr_text in build_payload...")
15
+ ocr_result = {"text": "Detected Text", "confidence": 0.9}
16
+ capture_result = {
17
+ "screenshot": {"path": "test.png", "timestamp": "123"},
18
+ "git_diff": {"diff": "some diff", "success": True}
19
+ }
20
+
21
+ payload = build_payload(
22
+ raw_thought="my thought",
23
+ ocr_result=ocr_result,
24
+ capture_result=capture_result
25
+ )
26
+
27
+ assert "ocr_text" in payload
28
+ assert payload["ocr_text"] == "Detected Text"
29
+ print("✅ build_payload includes ocr_text")
30
+
31
+ async def test_generate_article_robustness():
32
+ print("Testing generate_article robustness with missing ocr_text in draft...")
33
+ original_draft = CURRENT_DRAFT.read_text(encoding="utf-8") if CURRENT_DRAFT.exists() else None
34
+ # Mock CURRENT_DRAFT
35
+ draft_data = {
36
+ "payload": {
37
+ "user_message": "hello",
38
+ # "ocr_text" is missing
39
+ "git_diff": "diff"
40
+ },
41
+ "variations": {},
42
+ "timestamp": "123",
43
+ "status": "ready"
44
+ }
45
+
46
+ try:
47
+ with open(CURRENT_DRAFT, "w", encoding="utf-8") as f:
48
+ json.dump(draft_data, f)
49
+
50
+ request = ArticleGenerateRequest(include_codebase=False)
51
+
52
+ try:
53
+ # This will still try to call AI, but we want to see if it passes the user_msg construction
54
+ # We can't easily mock the AI call here without more setup,
55
+ # but the KeyError would happen BEFORE the AI call.
56
+
57
+ # To avoid actual AI call, we might just check the logic in routes.py
58
+ # by calling the function and expecting it to fail at AI call but NOT with KeyError.
59
+ await generate_article(request)
60
+ except HTTPException as e:
61
+ if "Article generation failed" in e.detail:
62
+ print("✅ generate_article passed KeyError (failed at AI call as expected)")
63
+ else:
64
+ print(f"❌ generate_article failed with: {e.detail}")
65
+ except KeyError as e:
66
+ print(f"❌ generate_article still has KeyError: {e}")
67
+ except Exception as e:
68
+ print(f"✅ generate_article passed KeyError (failed with: {e})")
69
+ finally:
70
+ if original_draft is None:
71
+ CURRENT_DRAFT.unlink(missing_ok=True)
72
+ else:
73
+ CURRENT_DRAFT.write_text(original_draft, encoding="utf-8")
74
+
75
+ if __name__ == "__main__":
76
+ asyncio.run(test_ocr_text_in_payload())
77
+ asyncio.run(test_generate_article_robustness())
File without changes
File without changes
File without changes
File without changes
@@ -1,9 +0,0 @@
1
- from slowapi import Limiter
2
- from slowapi.util import get_remote_address
3
-
4
-
5
- limiter = Limiter(key_func=get_remote_address, default_limits=["60/minute"], headers_enabled=False)
6
-
7
-
8
- if __name__ == "__main__":
9
- print("[RateLimit] Default limit: 60/minute")