gitcast 1.0.5__tar.gz → 1.0.7__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 (89) hide show
  1. {gitcast-1.0.5 → gitcast-1.0.7}/.env.example +0 -0
  2. {gitcast-1.0.5 → gitcast-1.0.7}/LICENSE +0 -0
  3. {gitcast-1.0.5 → gitcast-1.0.7}/MANIFEST.in +0 -0
  4. {gitcast-1.0.5/gitcast.egg-info → gitcast-1.0.7}/PKG-INFO +77 -50
  5. {gitcast-1.0.5 → gitcast-1.0.7}/README.md +0 -0
  6. {gitcast-1.0.5 → gitcast-1.0.7}/ai/__init__.py +0 -0
  7. {gitcast-1.0.5 → gitcast-1.0.7}/ai/formatter.py +0 -0
  8. {gitcast-1.0.5 → gitcast-1.0.7}/ai/generator.py +0 -0
  9. {gitcast-1.0.5 → gitcast-1.0.7}/ai/prompts.py +0 -0
  10. {gitcast-1.0.5 → gitcast-1.0.7}/ai/viral_patterns.py +0 -0
  11. {gitcast-1.0.5 → gitcast-1.0.7}/api/__init__.py +0 -0
  12. {gitcast-1.0.5 → gitcast-1.0.7}/api/analytics.py +0 -0
  13. {gitcast-1.0.5 → gitcast-1.0.7}/api/auth.py +0 -0
  14. {gitcast-1.0.5 → gitcast-1.0.7}/api/auth_middleware.py +0 -0
  15. {gitcast-1.0.5 → gitcast-1.0.7}/api/auth_routes.py +0 -0
  16. {gitcast-1.0.5 → gitcast-1.0.7}/api/monitoring.py +0 -0
  17. {gitcast-1.0.5 → gitcast-1.0.7}/api/payload.py +0 -0
  18. gitcast-1.0.7/api/ratelimit.py +18 -0
  19. {gitcast-1.0.5 → gitcast-1.0.7}/api/routes.py +22 -41
  20. {gitcast-1.0.5 → gitcast-1.0.7}/api/server.py +0 -0
  21. {gitcast-1.0.5 → gitcast-1.0.7}/api/validators.py +0 -0
  22. {gitcast-1.0.5 → gitcast-1.0.7}/assets/__init__.py +0 -0
  23. {gitcast-1.0.5 → gitcast-1.0.7}/assets/favicon-16x16.png +0 -0
  24. {gitcast-1.0.5 → gitcast-1.0.7}/assets/favicon-32x32.png +0 -0
  25. {gitcast-1.0.5 → gitcast-1.0.7}/assets/favicon-64x64.png +0 -0
  26. {gitcast-1.0.5 → gitcast-1.0.7}/assets/favicon.ico +0 -0
  27. {gitcast-1.0.5 → gitcast-1.0.7}/assets/icon.png +0 -0
  28. {gitcast-1.0.5 → gitcast-1.0.7}/cli/.env.example +0 -0
  29. {gitcast-1.0.5 → gitcast-1.0.7}/cli/__init__.py +0 -0
  30. {gitcast-1.0.5 → gitcast-1.0.7}/cli/gitcast.py +5 -9
  31. {gitcast-1.0.5 → gitcast-1.0.7}/config/__init__.py +0 -0
  32. {gitcast-1.0.5 → gitcast-1.0.7}/config/settings.py +0 -0
  33. {gitcast-1.0.5 → gitcast-1.0.7}/core/__init__.py +0 -0
  34. {gitcast-1.0.5 → gitcast-1.0.7}/core/capture.py +0 -0
  35. {gitcast-1.0.5 → gitcast-1.0.7}/core/codebase_reader.py +0 -0
  36. {gitcast-1.0.5 → gitcast-1.0.7}/core/framing.py +0 -0
  37. {gitcast-1.0.5 → gitcast-1.0.7}/core/hotkey.py +0 -0
  38. {gitcast-1.0.5 → gitcast-1.0.7}/core/log_stream.py +0 -0
  39. {gitcast-1.0.5 → gitcast-1.0.7}/core/ocr.py +0 -0
  40. {gitcast-1.0.5 → gitcast-1.0.7}/core/screenshot_session.py +0 -0
  41. {gitcast-1.0.5 → gitcast-1.0.7}/core/security.py +0 -0
  42. {gitcast-1.0.5 → gitcast-1.0.7}/core/tray.py +0 -0
  43. gitcast-1.0.7/core/trigger.py +96 -0
  44. {gitcast-1.0.5 → gitcast-1.0.7}/docs/api-keys.md +0 -0
  45. {gitcast-1.0.5 → gitcast-1.0.7}/docs/article-generator.md +0 -0
  46. {gitcast-1.0.5 → gitcast-1.0.7}/docs/capture-flow.md +0 -0
  47. {gitcast-1.0.5 → gitcast-1.0.7}/docs/changelog.md +0 -0
  48. {gitcast-1.0.5 → gitcast-1.0.7}/docs/contributing.md +0 -0
  49. {gitcast-1.0.5 → gitcast-1.0.7}/docs/dashboard.md +0 -0
  50. {gitcast-1.0.5 → gitcast-1.0.7}/docs/insights.md +0 -0
  51. {gitcast-1.0.5 → gitcast-1.0.7}/docs/introduction.md +0 -0
  52. {gitcast-1.0.5 → gitcast-1.0.7}/docs/post-formats.md +0 -0
  53. {gitcast-1.0.5 → gitcast-1.0.7}/docs/providers.md +0 -0
  54. gitcast-1.0.7/docs/pypi-errors.md +121 -0
  55. {gitcast-1.0.5 → gitcast-1.0.7}/docs/quickstart.md +0 -0
  56. {gitcast-1.0.5 → gitcast-1.0.7}/docs/sprint-mode.md +0 -0
  57. {gitcast-1.0.5 → gitcast-1.0.7/gitcast.egg-info}/PKG-INFO +77 -50
  58. {gitcast-1.0.5 → gitcast-1.0.7}/gitcast.egg-info/SOURCES.txt +6 -0
  59. {gitcast-1.0.5 → gitcast-1.0.7}/gitcast.egg-info/dependency_links.txt +0 -0
  60. {gitcast-1.0.5 → gitcast-1.0.7}/gitcast.egg-info/entry_points.txt +0 -1
  61. {gitcast-1.0.5 → gitcast-1.0.7}/gitcast.egg-info/requires.txt +0 -0
  62. {gitcast-1.0.5 → gitcast-1.0.7}/gitcast.egg-info/top_level.txt +0 -0
  63. {gitcast-1.0.5 → gitcast-1.0.7}/publisher/__init__.py +0 -0
  64. {gitcast-1.0.5 → gitcast-1.0.7}/publisher/clipboard.py +0 -0
  65. {gitcast-1.0.5 → gitcast-1.0.7}/publisher/twitter.py +0 -0
  66. {gitcast-1.0.5 → gitcast-1.0.7}/pyproject.toml +0 -0
  67. {gitcast-1.0.5 → gitcast-1.0.7}/requirements.txt +0 -0
  68. {gitcast-1.0.5 → gitcast-1.0.7}/setup.cfg +4 -4
  69. {gitcast-1.0.5 → gitcast-1.0.7}/setup.py +33 -1
  70. {gitcast-1.0.5 → gitcast-1.0.7}/storage/__init__.py +0 -0
  71. {gitcast-1.0.5 → gitcast-1.0.7}/storage/cleanup.py +0 -0
  72. {gitcast-1.0.5 → gitcast-1.0.7}/storage/engagement.py +0 -0
  73. {gitcast-1.0.5 → gitcast-1.0.7}/storage/insights.py +0 -0
  74. {gitcast-1.0.5 → gitcast-1.0.7}/storage/key_manager.py +0 -0
  75. {gitcast-1.0.5 → gitcast-1.0.7}/storage/logger.py +0 -0
  76. {gitcast-1.0.5 → gitcast-1.0.7}/storage/metrics.py +0 -0
  77. {gitcast-1.0.5 → gitcast-1.0.7}/storage/sprint.py +0 -0
  78. {gitcast-1.0.5 → gitcast-1.0.7}/storage/streak.py +0 -0
  79. {gitcast-1.0.5 → gitcast-1.0.7}/storage/supabase_client.py +0 -0
  80. {gitcast-1.0.5 → gitcast-1.0.7}/storage/tone_memory.py +0 -0
  81. gitcast-1.0.7/tests/test_keys.py +86 -0
  82. gitcast-1.0.7/tests/test_ocr_fix.py +77 -0
  83. {gitcast-1.0.5 → gitcast-1.0.7}/ui/__init__.py +0 -0
  84. gitcast-1.0.7/ui/popup.py +12 -0
  85. gitcast-1.0.7/ui/review.py +25 -0
  86. {gitcast-1.0.5 → gitcast-1.0.7}/web/__init__.py +0 -0
  87. {gitcast-1.0.5 → gitcast-1.0.7}/web/index.html +0 -0
  88. {gitcast-1.0.5 → gitcast-1.0.7}/web/landing.html +0 -0
  89. 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.7
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
@@ -67,16 +67,12 @@ def main():
67
67
  # open browser
68
68
  webbrowser.open("http://localhost:8000")
69
69
 
70
- # start tray and hotkey
70
+ print("[OK] server running at http://localhost:8000")
71
+ print("[OK] browser opened")
72
+ print("[OK] hotkey: Ctrl+Shift+P")
73
+
71
74
  from core.tray import run_tray
72
- import sys as _sys
73
- import os as _os
74
- _parent = _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__)))
75
- if _os.path.exists(_os.path.join(_parent, "main.py")):
76
- _sys.path.insert(0, _parent)
77
- elif _os.path.exists(_os.path.join(_os.getcwd(), "main.py")):
78
- _sys.path.insert(0, _os.getcwd())
79
- from main import on_trigger
75
+ from core.trigger import on_trigger
80
76
  run_tray(trigger_callback=on_trigger)
81
77
  return
82
78
 
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,96 @@
1
+ import threading
2
+ from core.capture import run_capture
3
+ from core.ocr import run_ocr
4
+ from api.payload import build_payload
5
+ from ui.popup import show_popup
6
+ from config.settings import is_sprint_mode
7
+ from storage.sprint import log_sprint_capture
8
+ from ai.generator import generate_posts
9
+ from ui.review import show_review
10
+ from publisher.twitter import publish_post
11
+ from storage.logger import log_post
12
+ import asyncio
13
+ import webbrowser
14
+
15
+
16
+ def on_trigger():
17
+ """
18
+ Called every time the hotkey fires.
19
+ Full capture → generate → review flow.
20
+ """
21
+ print("[Gitcast] Hotkey fired — starting capture...")
22
+
23
+ capture = run_capture()
24
+ ocr = run_ocr(capture["screenshot"]["path"])
25
+
26
+ if is_sprint_mode():
27
+ log_sprint_capture(
28
+ git_diff=capture["git_diff"].get("diff", ""),
29
+ ocr_text=ocr.get("text", ""),
30
+ raw_thought="",
31
+ timestamp=capture["screenshot"]["timestamp"],
32
+ )
33
+ print("[Gitcast] Sprint Mode — capture logged silently.")
34
+ return
35
+
36
+ def on_submit(raw_thought):
37
+ print(f"[Gitcast] Raw thought: '{raw_thought}'")
38
+ payload = build_payload(
39
+ raw_thought=raw_thought,
40
+ ocr_result=ocr,
41
+ capture_result=capture,
42
+ )
43
+ payload["use_vision_fallback"] = False
44
+ payload["screenshot_b64"] = None
45
+
46
+ def generate_and_show():
47
+ variations = asyncio.run(generate_posts(payload))
48
+
49
+ def on_publish(post_text, format_key,
50
+ screenshot_path):
51
+ print(f"[Gitcast] Publishing: {format_key}")
52
+ result = publish_post(
53
+ post_text, screenshot_path)
54
+ if result.get("success"):
55
+ if result.get("fallback"):
56
+ print("[Gitcast] Copied to clipboard")
57
+ else:
58
+ print(f"[Gitcast] Published: "
59
+ f"{result.get('tweet_url')}")
60
+ log_post(
61
+ post_text=post_text,
62
+ format_key=format_key,
63
+ screenshot_path=screenshot_path,
64
+ tweet_url=result.get(
65
+ "tweet_url", ""),
66
+ tweet_id=result.get(
67
+ "tweet_id", ""),
68
+ fallback=result.get(
69
+ "fallback", False),
70
+ )
71
+ else:
72
+ print(f"[Gitcast] Error: "
73
+ f"{result.get('error')}")
74
+
75
+ def on_close():
76
+ print("[Gitcast] Review closed.")
77
+
78
+ show_review(
79
+ payload=payload,
80
+ variations=variations,
81
+ on_publish=on_publish,
82
+ on_close=on_close,
83
+ )
84
+
85
+ threading.Thread(
86
+ target=generate_and_show,
87
+ daemon=True
88
+ ).start()
89
+
90
+ def on_dismiss():
91
+ print("[Gitcast] Capture dismissed.")
92
+
93
+ show_popup(
94
+ on_submit=on_submit,
95
+ on_dismiss=on_dismiss
96
+ )
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