gitcast 1.0.18__tar.gz → 1.0.20__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.18/gitcast.egg-info → gitcast-1.0.20}/PKG-INFO +2 -1
- {gitcast-1.0.18 → gitcast-1.0.20}/ai/generator.py +9 -1
- {gitcast-1.0.18 → gitcast-1.0.20}/ai/prompts.py +1 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/api/routes.py +53 -45
- {gitcast-1.0.18 → gitcast-1.0.20}/cli/gitcast.py +6 -2
- {gitcast-1.0.18 → gitcast-1.0.20}/core/hotkey.py +9 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/core/screenshot_session.py +0 -1
- {gitcast-1.0.18 → gitcast-1.0.20}/core/tray.py +1 -1
- gitcast-1.0.20/core/trigger.py +158 -0
- {gitcast-1.0.18 → gitcast-1.0.20/gitcast.egg-info}/PKG-INFO +2 -1
- {gitcast-1.0.18 → gitcast-1.0.20}/gitcast.egg-info/SOURCES.txt +1 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/gitcast.egg-info/requires.txt +1 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/requirements.txt +1 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/setup.py +1 -1
- gitcast-1.0.20/tests/test_validation.py +50 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/ui/popup.py +6 -4
- {gitcast-1.0.18 → gitcast-1.0.20}/ui/review.py +0 -1
- {gitcast-1.0.18 → gitcast-1.0.20}/web/index.html +364 -63
- gitcast-1.0.18/core/trigger.py +0 -122
- {gitcast-1.0.18 → gitcast-1.0.20}/.env.example +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/LICENSE +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/MANIFEST.in +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/README.md +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/ai/__init__.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/ai/formatter.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/ai/viral_patterns.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/api/__init__.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/api/analytics.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/api/auth.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/api/auth_middleware.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/api/auth_routes.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/api/monitoring.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/api/payload.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/api/ratelimit.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/api/server.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/api/validators.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/assets/__init__.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/assets/favicon-16x16.png +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/assets/favicon-32x32.png +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/assets/favicon-64x64.png +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/assets/favicon.ico +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/assets/icon.png +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/cli/.env.example +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/cli/__init__.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/config/__init__.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/config/settings.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/core/__init__.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/core/capture.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/core/codebase_reader.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/core/framing.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/core/log_stream.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/core/ocr.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/core/security.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/docs/api-keys.md +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/docs/article-generator.md +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/docs/capture-flow.md +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/docs/changelog.md +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/docs/contributing.md +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/docs/dashboard.md +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/docs/insights.md +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/docs/introduction.md +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/docs/post-formats.md +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/docs/providers.md +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/docs/pypi-errors.md +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/docs/quickstart.md +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/docs/sprint-mode.md +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/gitcast.egg-info/dependency_links.txt +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/gitcast.egg-info/entry_points.txt +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/gitcast.egg-info/top_level.txt +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/publisher/__init__.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/publisher/clipboard.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/publisher/twitter.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/pyproject.toml +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/setup.cfg +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/storage/__init__.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/storage/cleanup.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/storage/engagement.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/storage/insights.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/storage/key_manager.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/storage/logger.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/storage/metrics.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/storage/sprint.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/storage/streak.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/storage/supabase_client.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/storage/tone_memory.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/tests/test_keys.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/tests/test_ocr_fix.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/ui/__init__.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/web/__init__.py +0 -0
- {gitcast-1.0.18 → gitcast-1.0.20}/web/landing.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitcast
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.20
|
|
4
4
|
Summary: git diff → published post. under 60 seconds.
|
|
5
5
|
Home-page: https://github.com/drizzy765/gitcast
|
|
6
6
|
Author: Timilehin Agoro
|
|
@@ -29,6 +29,7 @@ Requires-Dist: slowapi
|
|
|
29
29
|
Requires-Dist: limits
|
|
30
30
|
Requires-Dist: sentry-sdk[fastapi]
|
|
31
31
|
Requires-Dist: posthog
|
|
32
|
+
Requires-Dist: plyer
|
|
32
33
|
Dynamic: author
|
|
33
34
|
Dynamic: author-email
|
|
34
35
|
Dynamic: description
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import asyncio
|
|
2
3
|
import httpx
|
|
3
4
|
import uuid
|
|
@@ -11,6 +12,7 @@ from config.settings import (
|
|
|
11
12
|
MOONSHOT_API_KEY,
|
|
12
13
|
CEREBRAS_API_KEY,
|
|
13
14
|
OPENROUTER_API_KEY,
|
|
15
|
+
DEEPSEEK_API_KEY,
|
|
14
16
|
GROQ_MODEL,
|
|
15
17
|
MOONSHOT_MODEL,
|
|
16
18
|
GEMINI_MODEL,
|
|
@@ -25,7 +27,7 @@ PROVIDERS = {
|
|
|
25
27
|
"base_url": "https://api.groq.com/openai/v1",
|
|
26
28
|
"api_key": GROQ_API_KEY,
|
|
27
29
|
"model": GROQ_MODEL,
|
|
28
|
-
"tasks": ["quick_win", "struggle", "linkedin", "deep_tech", "pr_generator"]
|
|
30
|
+
"tasks": ["quick_win", "struggle", "linkedin", "deep_tech", "pr_generator", "x_post"]
|
|
29
31
|
},
|
|
30
32
|
"kimi": {
|
|
31
33
|
"base_url": "https://api.moonshot.cn/v1",
|
|
@@ -33,6 +35,12 @@ PROVIDERS = {
|
|
|
33
35
|
"model": MOONSHOT_MODEL,
|
|
34
36
|
"tasks": ["article", "sprint_summary"]
|
|
35
37
|
},
|
|
38
|
+
"deepseek": {
|
|
39
|
+
"base_url": os.getenv("DEEPSEEK_API_BASE", "https://api.deepseek.com/v1"),
|
|
40
|
+
"api_key": DEEPSEEK_API_KEY,
|
|
41
|
+
"model": os.getenv("DEEPSEEK_MODEL", "deepseek-chat"),
|
|
42
|
+
"tasks": ["pr_desc"]
|
|
43
|
+
},
|
|
36
44
|
"cerebras": {
|
|
37
45
|
"base_url": "https://api.cerebras.ai/v1",
|
|
38
46
|
"api_key": CEREBRAS_API_KEY,
|
|
@@ -65,6 +65,7 @@ Rules:
|
|
|
65
65
|
- No generic filler: "excited to share", "game changer",
|
|
66
66
|
"cutting-edge", "pushing boundaries", "unparalleled",
|
|
67
67
|
"the intersection of X and Y", "delving into".
|
|
68
|
+
- NEVER use marketing-heavy, hype-filled, or clickbait phrases like "Revolutionizing Snake Game Development", "unprecedented performance gains", "Join the conversation", or similar corporate/AI clichés.
|
|
68
69
|
- Keep it grounded and specific. Vague posts get ignored.
|
|
69
70
|
- If there is a code snippet in the context, reference
|
|
70
71
|
the actual function name, variable, or error.
|
|
@@ -132,8 +132,10 @@ class ProviderKeyRemove(BaseModel):
|
|
|
132
132
|
|
|
133
133
|
|
|
134
134
|
class ChatRequest(BaseModel):
|
|
135
|
-
|
|
135
|
+
instruction: str
|
|
136
|
+
current_post: str
|
|
136
137
|
format_key: str
|
|
138
|
+
tab: Optional[str] = None
|
|
137
139
|
|
|
138
140
|
|
|
139
141
|
class CliTriggerRequest(BaseModel):
|
|
@@ -859,62 +861,68 @@ def get_current_draft():
|
|
|
859
861
|
raise HTTPException(status_code=500, detail=f"Error reading draft: {e}")
|
|
860
862
|
|
|
861
863
|
|
|
864
|
+
REFINE_PROVIDER = {
|
|
865
|
+
"article": "kimi",
|
|
866
|
+
"x_post": "groq",
|
|
867
|
+
"quick_win": "groq",
|
|
868
|
+
"linkedin": "groq",
|
|
869
|
+
"pr_desc": "deepseek",
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
|
|
862
873
|
@router.post("/chat")
|
|
863
874
|
@limiter.limit("20/minute")
|
|
864
875
|
async def chat_refine(request: Request, body: ChatRequest, user_id: str = Depends(get_current_user)):
|
|
865
876
|
"""Refines a post variation using AI chat."""
|
|
866
|
-
|
|
867
|
-
|
|
877
|
+
instruction = sanitize_text(body.instruction)
|
|
878
|
+
current_post = body.current_post
|
|
879
|
+
format_key = sanitize_text(body.format_key)
|
|
868
880
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
881
|
+
refinement_system = f"""You are refining a
|
|
882
|
+
{format_key} post for a developer.
|
|
883
|
+
The user's instruction is: {instruction}
|
|
884
|
+
Apply it to the post below. Return ONLY the
|
|
885
|
+
refined post text — no explanation, no preamble,
|
|
886
|
+
no markdown wrapper. Just the improved post."""
|
|
874
887
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
try:
|
|
883
|
-
platform_prompt = get_prompt(format_key)
|
|
884
|
-
except Exception as e:
|
|
885
|
-
platform_prompt = f"Format/Platform key: {format_key}"
|
|
886
|
-
|
|
887
|
-
refinement_system_prompt = (
|
|
888
|
-
"You are a social media manager helping a developer refine a post.\n"
|
|
889
|
-
f"The post format/platform is: '{format_key}'.\n"
|
|
890
|
-
"Adhere to the platform rules and guidelines below:\n"
|
|
891
|
-
"--- START PLATFORM RULES ---\n"
|
|
892
|
-
f"{platform_prompt}\n"
|
|
893
|
-
"--- END PLATFORM RULES ---\n\n"
|
|
894
|
-
"The user will provide instructions on how to change the existing draft. "
|
|
895
|
-
"Adhere strictly to the platform rules above while applying the user's changes."
|
|
896
|
-
)
|
|
897
|
-
|
|
898
|
-
refinement_user_message = (
|
|
899
|
-
f"Original Context:\n{draft['payload']['user_message']}\n\n"
|
|
900
|
-
f"Current Draft for '{format_key}':\n{current_text}\n\n"
|
|
901
|
-
f"User Instruction: {message}\n\n"
|
|
902
|
-
"Output ONLY the revised post text. No preamble."
|
|
903
|
-
)
|
|
888
|
+
refinement_user = f"""Current post:
|
|
889
|
+
{current_post}
|
|
890
|
+
|
|
891
|
+
Instruction: {instruction}
|
|
892
|
+
|
|
893
|
+
Return the refined post now:"""
|
|
904
894
|
|
|
905
895
|
try:
|
|
906
|
-
|
|
896
|
+
refined_text = await _ai_call(
|
|
907
897
|
format_key,
|
|
908
|
-
|
|
909
|
-
|
|
898
|
+
refinement_system,
|
|
899
|
+
refinement_user,
|
|
910
900
|
user_id=user_id,
|
|
911
901
|
)
|
|
912
902
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
903
|
+
if CURRENT_DRAFT.exists():
|
|
904
|
+
try:
|
|
905
|
+
with open(CURRENT_DRAFT, "r", encoding="utf-8") as f:
|
|
906
|
+
draft = json.load(f)
|
|
907
|
+
|
|
908
|
+
if "variations" not in draft:
|
|
909
|
+
draft["variations"] = {}
|
|
910
|
+
|
|
911
|
+
# Map incoming public format_key to backend variation key
|
|
912
|
+
variation_key = format_key
|
|
913
|
+
if format_key == "x_post":
|
|
914
|
+
variation_key = "deep_tech"
|
|
915
|
+
elif format_key == "pr_desc":
|
|
916
|
+
variation_key = "pr_generator"
|
|
917
|
+
|
|
918
|
+
draft["variations"][variation_key] = refined_text
|
|
919
|
+
|
|
920
|
+
with open(CURRENT_DRAFT, "w", encoding="utf-8") as f:
|
|
921
|
+
json.dump(draft, f, indent=4)
|
|
922
|
+
except Exception as e:
|
|
923
|
+
print(f"Error updating draft file: {e}")
|
|
924
|
+
|
|
925
|
+
return {"success": True, "new_text": refined_text, "refined_post": refined_text}
|
|
918
926
|
except Exception as e:
|
|
919
927
|
raise HTTPException(status_code=500, detail=f"AI refinement failed: {e}")
|
|
920
928
|
|
|
@@ -5,6 +5,8 @@ import subprocess
|
|
|
5
5
|
import httpx
|
|
6
6
|
import webbrowser
|
|
7
7
|
|
|
8
|
+
_browser_opened = False
|
|
9
|
+
|
|
8
10
|
def main():
|
|
9
11
|
if "--setup" in sys.argv:
|
|
10
12
|
from config.settings import get_active_env_path
|
|
@@ -65,7 +67,10 @@ def main():
|
|
|
65
67
|
time.sleep(0.5)
|
|
66
68
|
|
|
67
69
|
# open browser
|
|
68
|
-
|
|
70
|
+
global _browser_opened
|
|
71
|
+
if not _browser_opened:
|
|
72
|
+
webbrowser.open("http://localhost:8000")
|
|
73
|
+
_browser_opened = True
|
|
69
74
|
|
|
70
75
|
# ASCII Logo
|
|
71
76
|
print(" ██████╗ ██╗████████╗ ██████╗ █████╗ ███████╗████████╗")
|
|
@@ -116,7 +121,6 @@ def main():
|
|
|
116
121
|
|
|
117
122
|
if response.status_code == 200:
|
|
118
123
|
print("[Gitcast] Context captured. Opening Draft Room...")
|
|
119
|
-
webbrowser.open("http://localhost:8000")
|
|
120
124
|
else:
|
|
121
125
|
print(f"[Gitcast Error] Failed to trigger: {response.text}")
|
|
122
126
|
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
from pynput import keyboard
|
|
2
|
+
import time
|
|
2
3
|
|
|
3
4
|
_listener = None
|
|
5
|
+
_last_trigger_time = 0
|
|
6
|
+
COOLDOWN_SECONDS = 3
|
|
4
7
|
|
|
5
8
|
def start_hotkey_listener(callback):
|
|
6
9
|
global _listener
|
|
7
10
|
def on_activate():
|
|
11
|
+
global _last_trigger_time
|
|
12
|
+
now = time.time()
|
|
13
|
+
if now - _last_trigger_time < COOLDOWN_SECONDS:
|
|
14
|
+
print("[Hotkey] cooldown — ignoring rapid repeat")
|
|
15
|
+
return
|
|
16
|
+
_last_trigger_time = now
|
|
8
17
|
callback()
|
|
9
18
|
|
|
10
19
|
_listener = keyboard.GlobalHotKeys({
|
|
@@ -0,0 +1,158 @@
|
|
|
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, get_project_narrative, set_project_narrative
|
|
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
|
+
|
|
14
|
+
_capture_lock = threading.Event()
|
|
15
|
+
_capture_lock.set() # start as "not capturing"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def on_trigger():
|
|
19
|
+
# prevent concurrent captures
|
|
20
|
+
if not _capture_lock.is_set():
|
|
21
|
+
print("[Trigger] capture already in progress — ignoring")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
_capture_lock.clear() # mark as capturing
|
|
25
|
+
try:
|
|
26
|
+
_run_trigger()
|
|
27
|
+
finally:
|
|
28
|
+
_capture_lock.set() # release lock
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _show_tray_notification(title, message):
|
|
32
|
+
try:
|
|
33
|
+
from plyer import notification
|
|
34
|
+
notification.notify(
|
|
35
|
+
title=title,
|
|
36
|
+
message=message,
|
|
37
|
+
app_name="Gitcast",
|
|
38
|
+
timeout=4,
|
|
39
|
+
)
|
|
40
|
+
except Exception:
|
|
41
|
+
print(f"[Trigger] {title}: {message}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _generate_and_open(raw_thought, capture, ocr):
|
|
45
|
+
payload = build_payload(
|
|
46
|
+
raw_thought=raw_thought,
|
|
47
|
+
ocr_result=ocr,
|
|
48
|
+
capture_result=capture,
|
|
49
|
+
)
|
|
50
|
+
payload["use_vision_fallback"] = False
|
|
51
|
+
payload["screenshot_b64"] = None
|
|
52
|
+
|
|
53
|
+
def generate_and_show():
|
|
54
|
+
try:
|
|
55
|
+
print('[Trigger] calling generate_posts...')
|
|
56
|
+
print("[Trigger] generating posts...")
|
|
57
|
+
variations = asyncio.run(generate_posts(payload))
|
|
58
|
+
print(f'[Trigger] got {len(variations)} variations')
|
|
59
|
+
print(f'[Trigger] keys: {list(variations.keys())}')
|
|
60
|
+
for k, v in variations.items():
|
|
61
|
+
print(f'[Trigger] {k}: {v[:50]}...')
|
|
62
|
+
|
|
63
|
+
def on_publish(post_text, format_key, screenshot_path):
|
|
64
|
+
print(f"[Gitcast] Publishing: {format_key}")
|
|
65
|
+
result = publish_post(post_text, screenshot_path)
|
|
66
|
+
if result.get("success"):
|
|
67
|
+
if result.get("fallback"):
|
|
68
|
+
print("[Gitcast] Copied to clipboard")
|
|
69
|
+
else:
|
|
70
|
+
print(f"[Gitcast] Published: {result.get('tweet_url')}")
|
|
71
|
+
log_post(
|
|
72
|
+
post_text=post_text,
|
|
73
|
+
format_key=format_key,
|
|
74
|
+
screenshot_path=screenshot_path,
|
|
75
|
+
tweet_url=result.get("tweet_url", ""),
|
|
76
|
+
tweet_id=result.get("tweet_id", ""),
|
|
77
|
+
fallback=result.get("fallback", False),
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
print(f"[Gitcast] Error: {result.get('error')}")
|
|
81
|
+
|
|
82
|
+
def on_close():
|
|
83
|
+
print("[Gitcast] Review closed.")
|
|
84
|
+
|
|
85
|
+
show_review(
|
|
86
|
+
payload=payload,
|
|
87
|
+
variations=variations,
|
|
88
|
+
on_publish=on_publish,
|
|
89
|
+
on_close=on_close,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
_show_tray_notification(
|
|
93
|
+
"Gitcast",
|
|
94
|
+
"Post ready — check dashboard"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
print(f'[Trigger] generation error: {e}')
|
|
99
|
+
import traceback
|
|
100
|
+
traceback.print_exc()
|
|
101
|
+
|
|
102
|
+
threading.Thread(
|
|
103
|
+
target=generate_and_show,
|
|
104
|
+
daemon=True
|
|
105
|
+
).start()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _run_trigger():
|
|
109
|
+
"""The actual trigger logic — moved from on_trigger."""
|
|
110
|
+
print("[Trigger] on_trigger fired successfully")
|
|
111
|
+
print("[Gitcast] Hotkey fired — starting capture...")
|
|
112
|
+
|
|
113
|
+
capture = run_capture()
|
|
114
|
+
print("[Trigger] capture complete")
|
|
115
|
+
ocr = run_ocr(capture["screenshot"]["path"])
|
|
116
|
+
print("[Trigger] OCR complete")
|
|
117
|
+
|
|
118
|
+
if is_sprint_mode():
|
|
119
|
+
log_sprint_capture(
|
|
120
|
+
git_diff=capture["git_diff"].get("diff", ""),
|
|
121
|
+
ocr_text=ocr.get("text", ""),
|
|
122
|
+
raw_thought="",
|
|
123
|
+
timestamp=capture["screenshot"]["timestamp"],
|
|
124
|
+
)
|
|
125
|
+
print("[Gitcast] Sprint Mode — capture logged silently.")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
narrative = get_project_narrative()
|
|
129
|
+
|
|
130
|
+
if narrative:
|
|
131
|
+
# silent mode — no popup
|
|
132
|
+
# use narrative as the raw thought
|
|
133
|
+
raw_thought = narrative
|
|
134
|
+
print("[Trigger] using project narrative — skipping popup")
|
|
135
|
+
_generate_and_open(raw_thought, capture, ocr)
|
|
136
|
+
|
|
137
|
+
else:
|
|
138
|
+
# first time or no narrative — show popup once
|
|
139
|
+
# but frame it as "what are you building?"
|
|
140
|
+
# not "what was the struggle or win?"
|
|
141
|
+
print("[Trigger] no narrative set — showing setup popup")
|
|
142
|
+
|
|
143
|
+
def on_submit(thought):
|
|
144
|
+
from config.settings import set_project_narrative
|
|
145
|
+
# save as project narrative for future
|
|
146
|
+
set_project_narrative(thought)
|
|
147
|
+
print(f"[Trigger] narrative saved: '{thought}'")
|
|
148
|
+
_generate_and_open(thought, capture, ocr)
|
|
149
|
+
|
|
150
|
+
def on_dismiss():
|
|
151
|
+
print("[Trigger] popup dismissed")
|
|
152
|
+
|
|
153
|
+
# update popup text to ask for narrative
|
|
154
|
+
show_popup(
|
|
155
|
+
on_submit=on_submit,
|
|
156
|
+
on_dismiss=on_dismiss,
|
|
157
|
+
prompt_text="What are you building? (saved for future captures)"
|
|
158
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitcast
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.20
|
|
4
4
|
Summary: git diff → published post. under 60 seconds.
|
|
5
5
|
Home-page: https://github.com/drizzy765/gitcast
|
|
6
6
|
Author: Timilehin Agoro
|
|
@@ -29,6 +29,7 @@ Requires-Dist: slowapi
|
|
|
29
29
|
Requires-Dist: limits
|
|
30
30
|
Requires-Dist: sentry-sdk[fastapi]
|
|
31
31
|
Requires-Dist: posthog
|
|
32
|
+
Requires-Dist: plyer
|
|
32
33
|
Dynamic: author
|
|
33
34
|
Dynamic: author-email
|
|
34
35
|
Dynamic: description
|
|
@@ -51,7 +51,7 @@ if os.path.exists(requirements_path):
|
|
|
51
51
|
|
|
52
52
|
setup(
|
|
53
53
|
name="gitcast",
|
|
54
|
-
version="1.0.
|
|
54
|
+
version="1.0.20",
|
|
55
55
|
description="git diff → published post. under 60 seconds.",
|
|
56
56
|
long_description=long_description,
|
|
57
57
|
long_description_content_type="text/markdown",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
# Add project root to sys.path
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
6
|
+
|
|
7
|
+
from api.payload import validate_payload
|
|
8
|
+
|
|
9
|
+
def test_validation():
|
|
10
|
+
# Test 1 — thin context (should BLOCK)
|
|
11
|
+
payload_thin = {
|
|
12
|
+
"raw_thought": "Captured via hotkey trigger",
|
|
13
|
+
"ocr_text": "noisy ocr text", # length < 80
|
|
14
|
+
"git_diff": "",
|
|
15
|
+
"git_diff_available": False
|
|
16
|
+
}
|
|
17
|
+
is_valid, warnings = validate_payload(payload_thin)
|
|
18
|
+
print("Thin context result:", is_valid, warnings)
|
|
19
|
+
assert not is_valid
|
|
20
|
+
assert len(warnings) == 1
|
|
21
|
+
assert "Not enough context" in warnings[0]
|
|
22
|
+
|
|
23
|
+
# Test 2 — real context (should GENERATE)
|
|
24
|
+
payload_real = {
|
|
25
|
+
"raw_thought": "fixed score not resetting on game over",
|
|
26
|
+
"ocr_text": "game snake over score resetting",
|
|
27
|
+
"git_diff": "diff --git a/snake.py b/snake.py\n+score = 0",
|
|
28
|
+
"git_diff_available": True
|
|
29
|
+
}
|
|
30
|
+
is_valid, warnings = validate_payload(payload_real)
|
|
31
|
+
print("Real context result:", is_valid, warnings)
|
|
32
|
+
assert is_valid
|
|
33
|
+
assert not warnings or "Not enough context" not in warnings[0]
|
|
34
|
+
|
|
35
|
+
# Test 3 - real thought but missing diff (should WARN but allow)
|
|
36
|
+
payload_warn = {
|
|
37
|
+
"raw_thought": "fixed score not resetting on game over",
|
|
38
|
+
"ocr_text": "game snake over score resetting",
|
|
39
|
+
"git_diff": "",
|
|
40
|
+
"git_diff_available": False
|
|
41
|
+
}
|
|
42
|
+
is_valid, warnings = validate_payload(payload_warn)
|
|
43
|
+
print("Warn context result:", is_valid, warnings)
|
|
44
|
+
assert is_valid
|
|
45
|
+
assert any("No git diff" in w for w in warnings)
|
|
46
|
+
|
|
47
|
+
print("✅ All validation tests passed!")
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
test_validation()
|
|
@@ -19,14 +19,16 @@ FONT_SIZE = 13
|
|
|
19
19
|
# ── Popup window ──────────────────────────────────────────────────────────────
|
|
20
20
|
|
|
21
21
|
class CapturePopup:
|
|
22
|
-
def __init__(self, on_submit, on_dismiss):
|
|
22
|
+
def __init__(self, on_submit, on_dismiss, prompt_text=None):
|
|
23
23
|
"""
|
|
24
24
|
Args:
|
|
25
25
|
on_submit: Callback called with the user's raw thought string.
|
|
26
26
|
on_dismiss: Callback called when the user hits Esc or closes.
|
|
27
|
+
prompt_text: Optional custom prompt text.
|
|
27
28
|
"""
|
|
28
29
|
self.on_submit = on_submit
|
|
29
30
|
self.on_dismiss = on_dismiss
|
|
31
|
+
self.prompt_text = prompt_text or "What was the struggle or win?"
|
|
30
32
|
self.result = None
|
|
31
33
|
self.root = None
|
|
32
34
|
|
|
@@ -64,7 +66,7 @@ class CapturePopup:
|
|
|
64
66
|
label_font = tkfont.Font(family=FONT_FAMILY, size=10)
|
|
65
67
|
label = tk.Label(
|
|
66
68
|
main_frame,
|
|
67
|
-
text=
|
|
69
|
+
text=self.prompt_text,
|
|
68
70
|
bg=BG_COLOR,
|
|
69
71
|
fg=PLACEHOLDER_COLOR,
|
|
70
72
|
font=label_font,
|
|
@@ -147,14 +149,14 @@ class CapturePopup:
|
|
|
147
149
|
|
|
148
150
|
# ── Public interface ──────────────────────────────────────────────────────────
|
|
149
151
|
|
|
150
|
-
def show_popup(on_submit, on_dismiss):
|
|
152
|
+
def show_popup(on_submit, on_dismiss, prompt_text=None):
|
|
151
153
|
"""
|
|
152
154
|
Shows the capture popup in a new thread so it doesn't block
|
|
153
155
|
the hotkey listener or the tray process.
|
|
154
156
|
"""
|
|
155
157
|
def run():
|
|
156
158
|
try:
|
|
157
|
-
popup = CapturePopup(on_submit=on_submit, on_dismiss=on_dismiss)
|
|
159
|
+
popup = CapturePopup(on_submit=on_submit, on_dismiss=on_dismiss, prompt_text=prompt_text)
|
|
158
160
|
popup.show()
|
|
159
161
|
except Exception as e:
|
|
160
162
|
print(f"[Gitcast] Popup display error: {e}")
|