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.
- {gitcast-1.0.5 → gitcast-1.0.7}/.env.example +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/LICENSE +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/MANIFEST.in +0 -0
- {gitcast-1.0.5/gitcast.egg-info → gitcast-1.0.7}/PKG-INFO +77 -50
- {gitcast-1.0.5 → gitcast-1.0.7}/README.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/ai/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/ai/formatter.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/ai/generator.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/ai/prompts.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/ai/viral_patterns.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/api/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/api/analytics.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/api/auth.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/api/auth_middleware.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/api/auth_routes.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/api/monitoring.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/api/payload.py +0 -0
- gitcast-1.0.7/api/ratelimit.py +18 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/api/routes.py +22 -41
- {gitcast-1.0.5 → gitcast-1.0.7}/api/server.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/api/validators.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/assets/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/assets/favicon-16x16.png +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/assets/favicon-32x32.png +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/assets/favicon-64x64.png +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/assets/favicon.ico +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/assets/icon.png +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/cli/.env.example +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/cli/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/cli/gitcast.py +5 -9
- {gitcast-1.0.5 → gitcast-1.0.7}/config/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/config/settings.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/core/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/core/capture.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/core/codebase_reader.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/core/framing.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/core/hotkey.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/core/log_stream.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/core/ocr.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/core/screenshot_session.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/core/security.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/core/tray.py +0 -0
- gitcast-1.0.7/core/trigger.py +96 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/docs/api-keys.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/docs/article-generator.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/docs/capture-flow.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/docs/changelog.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/docs/contributing.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/docs/dashboard.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/docs/insights.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/docs/introduction.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/docs/post-formats.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/docs/providers.md +0 -0
- gitcast-1.0.7/docs/pypi-errors.md +121 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/docs/quickstart.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/docs/sprint-mode.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7/gitcast.egg-info}/PKG-INFO +77 -50
- {gitcast-1.0.5 → gitcast-1.0.7}/gitcast.egg-info/SOURCES.txt +6 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/gitcast.egg-info/dependency_links.txt +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/gitcast.egg-info/entry_points.txt +0 -1
- {gitcast-1.0.5 → gitcast-1.0.7}/gitcast.egg-info/requires.txt +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/gitcast.egg-info/top_level.txt +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/publisher/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/publisher/clipboard.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/publisher/twitter.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/pyproject.toml +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/requirements.txt +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/setup.cfg +4 -4
- {gitcast-1.0.5 → gitcast-1.0.7}/setup.py +33 -1
- {gitcast-1.0.5 → gitcast-1.0.7}/storage/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/storage/cleanup.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/storage/engagement.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/storage/insights.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/storage/key_manager.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/storage/logger.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/storage/metrics.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/storage/sprint.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/storage/streak.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/storage/supabase_client.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/storage/tone_memory.py +0 -0
- gitcast-1.0.7/tests/test_keys.py +86 -0
- gitcast-1.0.7/tests/test_ocr_fix.py +77 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/ui/__init__.py +0 -0
- gitcast-1.0.7/ui/popup.py +12 -0
- gitcast-1.0.7/ui/review.py +25 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/web/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/web/index.html +0 -0
- {gitcast-1.0.5 → gitcast-1.0.7}/web/landing.html +0 -0
- 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.
|
|
2
|
-
Name: gitcast
|
|
3
|
-
Version: 1.0.
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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:{
|
|
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
|
|
@@ -67,16 +67,12 @@ def main():
|
|
|
67
67
|
# open browser
|
|
68
68
|
webbrowser.open("http://localhost:8000")
|
|
69
69
|
|
|
70
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
File without changes
|