gitcast 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. ai/__init__.py +0 -0
  2. ai/formatter.py +59 -0
  3. ai/generator.py +604 -0
  4. ai/prompts.py +197 -0
  5. ai/viral_patterns.py +75 -0
  6. api/__init__.py +0 -0
  7. api/analytics.py +48 -0
  8. api/auth.py +49 -0
  9. api/auth_middleware.py +129 -0
  10. api/auth_routes.py +117 -0
  11. api/monitoring.py +56 -0
  12. api/payload.py +253 -0
  13. api/ratelimit.py +9 -0
  14. api/routes.py +1565 -0
  15. api/server.py +162 -0
  16. api/validators.py +101 -0
  17. assets/__init__.py +1 -0
  18. assets/favicon-16x16.png +0 -0
  19. assets/favicon-32x32.png +0 -0
  20. assets/favicon-64x64.png +0 -0
  21. assets/favicon.ico +0 -0
  22. assets/icon.png +0 -0
  23. cli/.env.example +26 -0
  24. cli/__init__.py +1 -0
  25. cli/gitcast.py +79 -0
  26. config/__init__.py +0 -0
  27. config/settings.py +213 -0
  28. core/__init__.py +0 -0
  29. core/capture.py +258 -0
  30. core/codebase_reader.py +90 -0
  31. core/framing.py +86 -0
  32. core/hotkey.py +21 -0
  33. core/log_stream.py +50 -0
  34. core/ocr.py +173 -0
  35. core/screenshot_session.py +274 -0
  36. core/security.py +126 -0
  37. core/tray.py +54 -0
  38. gitcast-1.0.0.dist-info/LICENSE +21 -0
  39. gitcast-1.0.0.dist-info/METADATA +67 -0
  40. gitcast-1.0.0.dist-info/RECORD +61 -0
  41. gitcast-1.0.0.dist-info/WHEEL +5 -0
  42. gitcast-1.0.0.dist-info/entry_points.txt +2 -0
  43. gitcast-1.0.0.dist-info/top_level.txt +10 -0
  44. publisher/__init__.py +0 -0
  45. publisher/clipboard.py +44 -0
  46. publisher/twitter.py +100 -0
  47. storage/__init__.py +0 -0
  48. storage/cleanup.py +60 -0
  49. storage/engagement.py +114 -0
  50. storage/insights.py +203 -0
  51. storage/key_manager.py +45 -0
  52. storage/logger.py +208 -0
  53. storage/metrics.py +119 -0
  54. storage/sprint.py +40 -0
  55. storage/streak.py +0 -0
  56. storage/supabase_client.py +25 -0
  57. storage/tone_memory.py +139 -0
  58. ui/__init__.py +0 -0
  59. web/__init__.py +1 -0
  60. web/index.html +4994 -0
  61. web/landing.html +925 -0
@@ -0,0 +1,274 @@
1
+ import time
2
+ import sys
3
+ import webbrowser
4
+ import json
5
+ import threading
6
+ from pathlib import Path
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.live import Live
10
+ from rich.table import Table
11
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
12
+ from rich.prompt import Prompt
13
+ from rich import box
14
+ from pynput import keyboard
15
+ from core.capture import capture_active_window, detect_working_directory, get_git_diff
16
+ from core.ocr import run_ocr
17
+ from core.security import scan_for_secrets, delete_capture
18
+ from config.settings import STORAGE_DIR, CURRENT_DRAFT, get_project_narrative
19
+
20
+ console = Console()
21
+
22
+ class ScreenshotSession:
23
+ def __init__(self):
24
+ self.screenshots = []
25
+ self.max_shots = 6
26
+ self.done = False
27
+ self.cancelled = False
28
+ self.working_dir = detect_working_directory()
29
+ self.git_diff = get_git_diff(self.working_dir)
30
+ self.accent_color = "#6366f1"
31
+ self._lock = threading.Lock()
32
+ self._processing = False
33
+
34
+ def run(self):
35
+ """Starts the interactive terminal session."""
36
+ self.show_header()
37
+
38
+ # Start hotkey listener
39
+ with keyboard.Listener(on_press=self.on_press) as listener:
40
+ try:
41
+ while not self.done and not self.cancelled:
42
+ if len(self.screenshots) >= self.max_shots:
43
+ if not self._processing:
44
+ console.print(f"\n[bold {self.accent_color}]Limit of {self.max_shots} reached. Moving to final review...[/bold {self.accent_color}]")
45
+ time.sleep(1.5)
46
+ self.done = True
47
+ break
48
+ time.sleep(0.1)
49
+ except KeyboardInterrupt:
50
+ self.cancelled = True
51
+
52
+ # Stop listener to free up stdin for Prompts
53
+ listener.stop()
54
+
55
+ if self.cancelled:
56
+ console.print("\n[red]Session cancelled. No screenshots saved.[/red]")
57
+ return []
58
+
59
+ # Remove any screenshots that might have failed
60
+ self.screenshots = [s for s in self.screenshots if s.get("path")]
61
+
62
+ if not self.screenshots:
63
+ console.print("[yellow]No screenshots captured.[/yellow]")
64
+ return []
65
+
66
+ # BATCH REVIEW & TAGGING
67
+ # Now we ask for purposes and keep/discard all at once
68
+ self.review_and_tag_shots()
69
+
70
+ if self.screenshots:
71
+ self.finish_session()
72
+ return self.screenshots
73
+ else:
74
+ console.print("[yellow]All shots discarded. Nothing to post.[/yellow]")
75
+ return []
76
+
77
+ def show_header(self):
78
+ header = Panel(
79
+ f"Capture up to [bold]{self.max_shots}[/bold] screenshots. Tag them all at the end.\n\n"
80
+ f"[bold {self.accent_color}]Ctrl+S[/bold {self.accent_color}] → [white]Capture Now (Instant)[/white]\n"
81
+ f"[bold {self.accent_color}]Ctrl+D[/bold {self.accent_color}] → [white]Done & Review[/white]\n"
82
+ f"[bold {self.accent_color}]Ctrl+C[/bold {self.accent_color}] → [white]Cancel[/white]",
83
+ title=f"[bold {self.accent_color}]GITCAST[/bold {self.accent_color}] · Rapid Capture Mode",
84
+ expand=False,
85
+ border_style=self.accent_color,
86
+ box=box.ROUNDED,
87
+ padding=(1, 2)
88
+ )
89
+ console.print(header)
90
+
91
+ def on_press(self, key):
92
+ """Handles session hotkeys."""
93
+ try:
94
+ # Ctrl+S (\x13)
95
+ if hasattr(key, 'char') and key.char == '\x13':
96
+ if self._processing:
97
+ # Very brief message so it doesn't clutter
98
+ console.print("[dim]...waiting for current capture...[/dim]", end="\r")
99
+ else:
100
+ threading.Thread(target=self.take_screenshot, daemon=True).start()
101
+
102
+ # Ctrl+D (\x04)
103
+ elif hasattr(key, 'char') and key.char == '\x04':
104
+ if not self._processing:
105
+ self.done = True
106
+ return False
107
+
108
+ # Ctrl+C (\x03)
109
+ elif hasattr(key, 'char') and key.char == '\x03':
110
+ self.cancelled = True
111
+ return False
112
+ except Exception:
113
+ pass
114
+
115
+ def take_screenshot(self):
116
+ """Captures window silently and adds to queue."""
117
+ with self._lock:
118
+ self._processing = True
119
+ try:
120
+ # Minimal 1s countdown for focus shift
121
+ for i in range(1, 0, -1):
122
+ time.sleep(0.5)
123
+
124
+ shot = capture_active_window(delay=0.1)
125
+ if not shot["success"]:
126
+ console.print(f"[red]Capture failed: {shot['error']}[/red]")
127
+ return
128
+
129
+ # OCR and Security (Silent progress)
130
+ ocr = run_ocr(shot["path"])
131
+ security = scan_for_secrets(ocr["text"] or ocr["raw_text"])
132
+
133
+ if not security["clean"]:
134
+ console.print(f"\n[red]⚠️ Shot {len(self.screenshots)+1} blocked (sensitive content).[/red]")
135
+ delete_capture(shot["path"])
136
+ return
137
+
138
+ # Add to list with default purpose
139
+ screenshot_data = {
140
+ "path": shot["path"],
141
+ "purpose": "code",
142
+ "ocr_text": ocr["text"] or ocr["raw_text"],
143
+ "confidence": ocr["confidence"],
144
+ "timestamp": shot["timestamp"],
145
+ "index": len(self.screenshots) + 1
146
+ }
147
+ self.screenshots.append(screenshot_data)
148
+
149
+ # Non-blocking success message
150
+ console.print(f"[bold green]✓[/bold green] Captured Shot {len(self.screenshots)} · [dim]Press Ctrl+S for more or Ctrl+D to finish[/dim]")
151
+
152
+ finally:
153
+ self._processing = False
154
+
155
+ def review_and_tag_shots(self):
156
+ """Final phase where user tags and picks shots."""
157
+ console.print(Panel(
158
+ "REVIEW & TAG YOUR SESSION\n[dim]Assign purposes and decide what to keep[/dim]",
159
+ style=f"bold {self.accent_color}",
160
+ border_style=self.accent_color,
161
+ box=box.DOUBLE
162
+ ))
163
+
164
+ keeps = [True] * len(self.screenshots)
165
+
166
+ while True:
167
+ table = Table(show_header=True, border_style=self.accent_color, box=box.ROUNDED)
168
+ table.add_column("ID", justify="center")
169
+ table.add_column("Preview", width=40)
170
+ table.add_column("Purpose")
171
+ table.add_column("Status")
172
+
173
+ for i, (s, keep) in enumerate(zip(self.screenshots, keeps), 1):
174
+ status = "[green]Keep[/green]" if keep else "[red]Discard[/red]"
175
+ check = "[bold green]✓[/bold green]" if keep else "[ ]"
176
+ preview = (s["ocr_text"][:37].replace("\n", " ") + "...") if s["ocr_text"] else "No text"
177
+ table.add_row(f"{check} {i}", preview, f"[bold]{s['purpose']}[/bold]", status)
178
+
179
+ console.print(table)
180
+ console.print("[dim]Commands: [ID] to toggle status | [pID] to set purpose (e.g. p1) | [d] done[/dim]")
181
+ action = Prompt.ask("Action", default="d")
182
+
183
+ if action.lower() == 'd':
184
+ break
185
+
186
+ # Purpose setting (e.g. p1)
187
+ if action.lower().startswith('p') and len(action) > 1:
188
+ try:
189
+ idx = int(action[1:]) - 1
190
+ if 0 <= idx < len(self.screenshots):
191
+ purpose = Prompt.ask(
192
+ f"Purpose for Shot {idx+1}",
193
+ choices=["code", "terminal", "browser", "result", "other"],
194
+ default=self.screenshots[idx]["purpose"]
195
+ )
196
+ self.screenshots[idx]["purpose"] = purpose
197
+ except ValueError:
198
+ pass
199
+ continue
200
+
201
+ # Toggle keep/discard
202
+ try:
203
+ idx = int(action) - 1
204
+ if 0 <= idx < len(keeps):
205
+ keeps[idx] = not keeps[idx]
206
+ except ValueError:
207
+ console.print("[red]Invalid command.[/red]")
208
+
209
+ # Final cleanup
210
+ final_shots = []
211
+ for s, keep in zip(self.screenshots, keeps):
212
+ if keep:
213
+ final_shots.append(s)
214
+ else:
215
+ delete_capture(s["path"])
216
+
217
+ self.screenshots = final_shots
218
+
219
+ def finish_session(self):
220
+ """Saves session and opens dashboard."""
221
+ console.print(Panel(
222
+ "[bold green]✓ Session Complete[/bold green]\n"
223
+ "Opening dashboard to finish your post...",
224
+ border_style="green",
225
+ box=box.ROUNDED,
226
+ padding=(1, 2)
227
+ ))
228
+
229
+ self.save_session_and_generate()
230
+ webbrowser.open("http://localhost:8000")
231
+
232
+ def save_session_and_generate(self):
233
+ """Background generation of post variations."""
234
+ from api.payload import build_payload
235
+ from ai.generator import generate_posts
236
+ import asyncio
237
+
238
+ payload = build_payload(
239
+ raw_thought="",
240
+ ocr_result={},
241
+ capture_result={"working_dir": self.working_dir, "git_diff": self.git_diff, "screenshot": self.screenshots[0]},
242
+ multi_screenshots=self.screenshots
243
+ )
244
+
245
+ draft_data = {
246
+ "payload": payload,
247
+ "variations": {},
248
+ "timestamp": payload["timestamp"],
249
+ "status": "generating"
250
+ }
251
+ with open(CURRENT_DRAFT, "w", encoding="utf-8") as f:
252
+ json.dump(draft_data, f, indent=4)
253
+
254
+ def run_gen():
255
+ try:
256
+ loop = asyncio.new_event_loop()
257
+ asyncio.set_event_loop(loop)
258
+ variations = loop.run_until_complete(generate_posts(payload))
259
+ draft_data["variations"] = variations
260
+ draft_data["status"] = "ready"
261
+ with open(CURRENT_DRAFT, "w", encoding="utf-8") as f:
262
+ json.dump(draft_data, f, indent=4)
263
+ except Exception as e:
264
+ from core.log_stream import stream_log
265
+ stream_log("Generator", "ERROR", f"Hotkey background generation failed: {e}")
266
+ draft_data["status"] = "failed"
267
+ with open(CURRENT_DRAFT, "w", encoding="utf-8") as f:
268
+ json.dump(draft_data, f, indent=4)
269
+
270
+ threading.Thread(target=run_gen, daemon=True).start()
271
+
272
+ if __name__ == "__main__":
273
+ session = ScreenshotSession()
274
+ session.run()
core/security.py ADDED
@@ -0,0 +1,126 @@
1
+ import os
2
+ import re
3
+ from pathlib import Path
4
+ from cryptography.fernet import Fernet
5
+ from config.settings import ENCRYPTION_KEY_PATH
6
+ from api.analytics import track
7
+
8
+ # [Security] module for data protection and sensitive content scanning
9
+
10
+ def generate_key():
11
+ """Creates a new Fernet key and saves it to ENCRYPTION_KEY_PATH."""
12
+ key = Fernet.generate_key()
13
+ ENCRYPTION_KEY_PATH.parent.mkdir(parents=True, exist_ok=True)
14
+ with open(ENCRYPTION_KEY_PATH, "wb") as key_file:
15
+ key_file.write(key)
16
+
17
+ # Add to .gitignore if not already present
18
+ gitignore_path = Path(".gitignore")
19
+ if gitignore_path.exists():
20
+ with open(gitignore_path, "r") as f:
21
+ content = f.read()
22
+ if ".secret_key" not in content:
23
+ with open(gitignore_path, "a") as f:
24
+ f.write("\n.secret_key\n")
25
+ print("[Security] Added .secret_key to .gitignore")
26
+
27
+ return key
28
+
29
+ def load_key():
30
+ """Loads the existing key or generates a new one."""
31
+ if not ENCRYPTION_KEY_PATH.exists():
32
+ return generate_key()
33
+ with open(ENCRYPTION_KEY_PATH, "rb") as key_file:
34
+ return key_file.read()
35
+
36
+ def encrypt_file(path: str):
37
+ """Encrypts a file in place using Fernet symmetric encryption."""
38
+ key = load_key()
39
+ f = Fernet(key)
40
+ with open(path, "rb") as file:
41
+ file_data = file.read()
42
+ encrypted_data = f.encrypt(file_data)
43
+ with open(path, "wb") as file:
44
+ file.write(encrypted_data)
45
+ print(f"[Security] Encrypted: {path}")
46
+
47
+ def decrypt_file(path: str) -> bytes:
48
+ """Decrypts a file and returns the bytes without writing to disk."""
49
+ key = load_key()
50
+ f = Fernet(key)
51
+ with open(path, "rb") as file:
52
+ encrypted_data = file.read()
53
+ return f.decrypt(encrypted_data)
54
+
55
+ def scan_for_secrets(ocr_text: str) -> dict:
56
+ """
57
+ Scans OCR text for potential sensitive patterns.
58
+ Returns {"clean": bool, "matches": list}
59
+ """
60
+ patterns = [
61
+ r"KEY\s*=\s*['\"]?([a-zA-Z0-9_\-]{20,})['\"]?",
62
+ r"TOKEN\s*=\s*['\"]?([a-zA-Z0-9_\-]{20,})['\"]?",
63
+ r"SECRET\s*=\s*['\"]?([a-zA-Z0-9_\-]{20,})['\"]?",
64
+ r"PASSWORD\s*=\s*['\"]?([a-zA-Z0-9_\-]{20,})['\"]?",
65
+ r"Bearer\s+([a-zA-Z0-9_\-\.]{20,})",
66
+ r"sk-[a-zA-Z0-9]{20,}",
67
+ r"pk-[a-zA-Z0-9]{20,}",
68
+ r"[a-fA-F0-9]{32,}", # Long hex strings
69
+ ]
70
+
71
+ matches = []
72
+ for pattern in patterns:
73
+ found = re.findall(pattern, ocr_text, re.IGNORECASE)
74
+ if found:
75
+ matches.extend(found)
76
+
77
+ unique_matches = list(set(matches))
78
+ if unique_matches:
79
+ track("sensitive_content_blocked", {"pattern_count": len(unique_matches)})
80
+
81
+ return {
82
+ "clean": len(matches) == 0,
83
+ "matches": unique_matches
84
+ }
85
+
86
+ def delete_capture(screenshot_path: str):
87
+ """Securely deletes a screenshot from disk."""
88
+ try:
89
+ path = Path(screenshot_path)
90
+ if path.exists():
91
+ # Overwrite with random data before deleting (basic secure delete)
92
+ size = path.stat().st_size
93
+ with open(path, "wb") as f:
94
+ f.write(os.urandom(size))
95
+ path.unlink()
96
+ print(f"[Security] Securely deleted: {screenshot_path}")
97
+ except Exception as e:
98
+ print(f"[Security] Error deleting {screenshot_path}: {e}")
99
+
100
+ if __name__ == "__main__":
101
+ print("=== SECURITY MODULE TEST ===")
102
+
103
+ # Test encryption/decryption
104
+ test_file = "test_security.png"
105
+ with open(test_file, "wb") as f:
106
+ f.write(b"fake image data")
107
+
108
+ print("Original data: b'fake image data'")
109
+ encrypt_file(test_file)
110
+
111
+ try:
112
+ decrypted = decrypt_file(test_file)
113
+ print(f"Decrypted data: {decrypted}")
114
+ assert decrypted == b"fake image data"
115
+ print("Encryption/Decryption test: PASSED")
116
+ finally:
117
+ if os.path.exists(test_file):
118
+ os.remove(test_file)
119
+
120
+ # Test secret scanning
121
+ test_text = "My API key is KEY=sk-1234567890abcdef1234567890 and my token is Bearer some-long-token-string-here"
122
+ scan_results = scan_for_secrets(test_text)
123
+ print(f"Scan results: {scan_results}")
124
+ assert not scan_results["clean"]
125
+ assert len(scan_results["matches"]) >= 2
126
+ print("Secret scanning test: PASSED")
core/tray.py ADDED
@@ -0,0 +1,54 @@
1
+ import pystray
2
+ import os
3
+ from PIL import Image, ImageDraw
4
+ from pystray import MenuItem as item
5
+ import webbrowser
6
+ from .hotkey import start_hotkey_listener, stop_hotkey_listener
7
+
8
+ _icon = None
9
+
10
+ def create_image():
11
+ # Generate a simple icon
12
+ width = 64
13
+ height = 64
14
+ color1 = (0, 128, 255)
15
+ color2 = (255, 255, 255)
16
+ image = Image.new('RGB', (width, height), color1)
17
+ dc = ImageDraw.Draw(image)
18
+ dc.rectangle(
19
+ (width // 4, height // 4, width * 3 // 4, height * 3 // 4),
20
+ fill=color2
21
+ )
22
+ return image
23
+
24
+ def run_tray(trigger_callback):
25
+ global _icon
26
+ # Start the hotkey listener
27
+ start_hotkey_listener(trigger_callback)
28
+
29
+ def on_quit(icon, item):
30
+ stop_tray()
31
+ os._exit(0)
32
+
33
+ def trigger_action(icon, item):
34
+ trigger_callback()
35
+
36
+ def open_dashboard(icon, item):
37
+ webbrowser.open("http://127.0.0.1:8000")
38
+
39
+ image = create_image()
40
+ menu = pystray.Menu(
41
+ item('Dashboard', open_dashboard),
42
+ item('Trigger Capture (Ctrl+Alt+S)', trigger_action),
43
+ item('Quit', on_quit)
44
+ )
45
+
46
+ _icon = pystray.Icon("gitcast", image, "Gitcast", menu)
47
+ _icon.run()
48
+
49
+ def stop_tray():
50
+ global _icon
51
+ stop_hotkey_listener()
52
+ if _icon:
53
+ _icon.stop()
54
+ print("[Tray] Icon stopped.")
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Timilehin Agoro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.1
2
+ Name: gitcast
3
+ Version: 1.0.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
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: Pillow
13
+ Requires-Dist: cryptography
14
+ Requires-Dist: fastapi
15
+ Requires-Dist: groq
16
+ Requires-Dist: httpx
17
+ Requires-Dist: keyboard
18
+ Requires-Dist: limits
19
+ Requires-Dist: mss
20
+ Requires-Dist: posthog
21
+ Requires-Dist: pynput
22
+ Requires-Dist: pystray
23
+ Requires-Dist: pytesseract
24
+ Requires-Dist: python-dotenv
25
+ Requires-Dist: python-jose[cryptography]
26
+ Requires-Dist: rich
27
+ Requires-Dist: sentry-sdk[fastapi]
28
+ Requires-Dist: slowapi
29
+ Requires-Dist: supabase
30
+ Requires-Dist: tweepy
31
+ Requires-Dist: uvicorn
32
+
33
+ # Gitcast
34
+
35
+ On-demand, privacy-first desktop utility that captures active window context,
36
+ extracts code changes, and generates platform-ready X (Twitter) posts with one keystroke.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install gitcast
42
+ ```
43
+
44
+ ## Setup
45
+
46
+ 1. Install Tesseract OCR:
47
+ - Windows: https://github.com/UB-Mannheim/tesseract/wiki
48
+ - Mac: `brew install tesseract`
49
+ - Linux: `sudo apt install tesseract-ocr`
50
+
51
+ 2. Add your API keys:
52
+ ```bash
53
+ gitcast --setup
54
+ ```
55
+ (opens `.env` file for editing)
56
+
57
+ 3. Run:
58
+ ```bash
59
+ gitcast
60
+ ```
61
+
62
+ Dashboard opens at http://127.0.0.1:8000
63
+ Press Ctrl+Shift+P from anywhere to capture.
64
+
65
+ ## Stack
66
+ - Python 3.11+
67
+ - FastAPI, pynput, mss, Tesseract, Groq API, Tweepy
@@ -0,0 +1,61 @@
1
+ ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ ai/formatter.py,sha256=1bfPCzRiOsJ_M2_aOJsb2xCkx-SFUK2bVBCpZCRRdo4,2338
3
+ ai/generator.py,sha256=25qcPlRATAU3CpeojDQhTFS8RbhlsckAnjItBWLU1KY,24443
4
+ ai/prompts.py,sha256=uBa8FQsbd7TlUhrqe-MMku83sHr_ZmCcb1zkFO6jeP8,10304
5
+ ai/viral_patterns.py,sha256=tm7236kOAwtqtAp5fiQs4tEPrq_KE9Y1EUrZNyolh8c,3879
6
+ api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ api/analytics.py,sha256=3MfmZmP2kt77BZ5FQ7ce13TMQ2vJYbiDCMrBcx7sQyA,1198
8
+ api/auth.py,sha256=DM2ZtXHOCvZbRjaxp336B3mlzYKDUhSlXP06GQSOhvM,1498
9
+ api/auth_middleware.py,sha256=RuHD8YxHV_SizRXXKDpqDw2j5SHN5k6_N4erfakCdlU,4075
10
+ api/auth_routes.py,sha256=fYMAbL3DKyD4sANhFkCcAqveCgnSCPkYI_FH9nMaSp4,4023
11
+ api/monitoring.py,sha256=iWvqPYUiq6BATd1DMRKuYent38LJOUnW8oZWs_jH24s,1559
12
+ api/payload.py,sha256=jLxMZMM3s0-ZpqO0s662bY8cfHjgm7MVKUVaXmVX7jU,9651
13
+ api/ratelimit.py,sha256=7EeFRlsvQFc1mOnvd5BBzabT98pbbhkS61ujP4CputM,253
14
+ api/routes.py,sha256=DlKHeeYVzpdKamz3tfKNkyr3lbtKEOvAoWDq9uw7dlU,57814
15
+ api/server.py,sha256=N9f0qHksNiFCp6-c6yAJ1ClkBORXjupTAxaAeIUlC3g,5499
16
+ api/validators.py,sha256=ENVAw9AWGAS3g4Ro3UKFGRQdrCwOdxOyRQ8ggmqIdg4,2907
17
+ assets/__init__.py,sha256=L582TvLegqQlplr9qawaiDbbFBQT6HuZxvNZygQieoY,93
18
+ assets/favicon-16x16.png,sha256=3xo2x9dQmGsdJvWpCEK5d8WveCxW9wm1yS-7dN1GpIg,272
19
+ assets/favicon-32x32.png,sha256=91im3NSOf4sDViP9i2GfoTcfiKbuWwK3BOqguG62rVo,613
20
+ assets/favicon-64x64.png,sha256=VRwOkiPyjiCtA4Lu4b8PwjuWtWLmGatv5B6JkQA3UCc,1292
21
+ assets/favicon.ico,sha256=VUkA-33GjavIqwoKtQEHBNuUFwqCuWp0i3lmbeX3sj4,2916
22
+ assets/icon.png,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ cli/.env.example,sha256=0msnLTS8DyYpT9t-BuUgMWjADgvkIHdLAIqL0ex6OJE,955
24
+ cli/__init__.py,sha256=QHJaTQVcqGsvBTXZQN2GFSjZLYQImJg5Xwqj2DzR5IA,14
25
+ cli/gitcast.py,sha256=217btuKYK6ysC5Bi6bFKjqJ-qfVsdHxpn0ow8sSuCXE,2824
26
+ config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ config/settings.py,sha256=YNGPQLHGYj6Rst_1wUr0EhtHhOeKLGj0zSWThHHonQo,6406
28
+ core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ core/capture.py,sha256=QUg_j_M6RahKoLpNejpLId3Gj287af49jVlfqnxwrqE,8783
30
+ core/codebase_reader.py,sha256=7WkboJ3emE_6InN97vQpOu83vExO-iVaPkf3xDx1BHc,3503
31
+ core/framing.py,sha256=pGZQNWmYTINa-eDyUbY1NCkIwVZzuUmmnzrmR9kN9cg,2923
32
+ core/hotkey.py,sha256=V22jTfWdo1pgszY83uT75DrMr4wsbA_rIJKxw8R4Tmg,438
33
+ core/log_stream.py,sha256=aWo5F2cGVSwuGBg4AtrdGAcSm2qpLBcp-_caRfjm6nM,1227
34
+ core/ocr.py,sha256=P30R7lVHvvF-8lKGTKXgu-6r6lTenORt8ENiD0g76P8,6130
35
+ core/screenshot_session.py,sha256=9xZ8akYYc7WmK-FQeLuAwCEOY23iViSylf5RhUjlew4,10869
36
+ core/security.py,sha256=JU6d03pmgcZno3eozPjUar307xCOud8CcxAhvDRNQ5s,4258
37
+ core/tray.py,sha256=Gam9lylleXZYr-3OHJ7QxBfNj3hVpU5Nh_193I13XDc,1293
38
+ publisher/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
+ publisher/clipboard.py,sha256=oQrPPl9wZgtAd4DBz3HpgHSaLFpsNFHtoV9JT5vIkLE,1626
40
+ publisher/twitter.py,sha256=QUara_G-8jTPTkEYTGFrUbQxjSZCMLwRtPIdJNwRmfo,3355
41
+ storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
+ storage/cleanup.py,sha256=9UWuH4mjK2SySfOeYdOs1vB4Yq1ixLGFDWdT_bRT5Nc,2221
43
+ storage/engagement.py,sha256=m00ecVngQu6UElsd-Ax9s32ITUkT4Bf_RB-TqImM1BA,4120
44
+ storage/insights.py,sha256=i2TQRbJ7Tf6HEPDgCiCCGzYGklDcnGRC62ofN18ADms,7882
45
+ storage/key_manager.py,sha256=D2ceDV5judPaPP6aANwqE0Xa6w8DLc73qB3LYKbcfeE,1278
46
+ storage/logger.py,sha256=-J4r1ELkCxjWuUNsZbBQZ8gaN1vZSbEdNXDuVPZ3lPc,6496
47
+ storage/metrics.py,sha256=r1ZKr2OP82-Os-tyMPchZhaqO_6I5q7M60Z_hwfdiks,3976
48
+ storage/sprint.py,sha256=dsVIMe0BWhxV65popfg2zjjRoDWB6mJS-bl9FJ5y4Ww,1019
49
+ storage/streak.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
+ storage/supabase_client.py,sha256=IBaCcmWfj49aOS1jUdyhKkDzaDw1BEaH4LZ4LLORK0U,791
51
+ storage/tone_memory.py,sha256=hr6xOO20GhBwhZam_OHLRAypM0gKphTeXAnU3BZFb3g,4721
52
+ ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
+ web/__init__.py,sha256=eOuhaq0eKt3yb_CUJcLmQZorRDsoZ0c08yG4FgPrPzw,90
54
+ web/index.html,sha256=ap1AI_03XihVgDxh-YWE84OocXrYgkCRii3TSjuVFCY,210179
55
+ web/landing.html,sha256=ouCDxNpaFzM-hKsylty-T8JPXPBXP0q5c-SU_RXFpco,34469
56
+ gitcast-1.0.0.dist-info/LICENSE,sha256=BekKKCXOSkFiRzT17cdBpdSqjKhfXkNAYhroTwa1Y78,1072
57
+ gitcast-1.0.0.dist-info/METADATA,sha256=s4WUmFX_Fk-vqBI8YOVb_Dg4r1fts4gH1f-a0WevzOM,1508
58
+ gitcast-1.0.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
59
+ gitcast-1.0.0.dist-info/entry_points.txt,sha256=Qu4EahnnTiLiONI6OfXoanLMf2n-vhamtcDvqvAni_Q,45
60
+ gitcast-1.0.0.dist-info/top_level.txt,sha256=43Ln1zzXEtb4ZLGBypsfgc3N1bkEc3z3P7eo7hNvnjk,55
61
+ gitcast-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.42.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gitcast = cli.gitcast:main
@@ -0,0 +1,10 @@
1
+ ai
2
+ api
3
+ assets
4
+ cli
5
+ config
6
+ core
7
+ publisher
8
+ storage
9
+ ui
10
+ web
publisher/__init__.py ADDED
File without changes
publisher/clipboard.py ADDED
@@ -0,0 +1,44 @@
1
+ import webbrowser
2
+ from api.analytics import track
3
+
4
+
5
+ def copy_to_clipboard(post_text: str) -> dict:
6
+ """Copy post text to the system clipboard using tkinter."""
7
+ try:
8
+ import tkinter as tk
9
+
10
+ root = tk.Tk()
11
+ root.withdraw()
12
+ root.clipboard_clear()
13
+ root.clipboard_append(post_text)
14
+ root.update() # required for clipboard to persist after destroy
15
+ root.destroy()
16
+ print("[Publisher] Text copied to clipboard.")
17
+ track("post_copied", {"platform": "clipboard"})
18
+ return {"success": True, "method": "clipboard"}
19
+ except Exception as e:
20
+ print(f"[Publisher] Clipboard copy failed: {e}")
21
+ return {"success": False, "error": str(e)}
22
+
23
+
24
+ def open_x_compose(post_text: str) -> dict:
25
+ """Copy text to clipboard then open X compose page in the default browser."""
26
+ try:
27
+ clip_result = copy_to_clipboard(post_text)
28
+ if not clip_result["success"]:
29
+ return clip_result
30
+
31
+ compose_url = "https://twitter.com/compose/tweet"
32
+ webbrowser.open(compose_url)
33
+ print(f"[Publisher] Opened {compose_url} — paste your post from clipboard.")
34
+ return {"success": True, "method": "clipboard"}
35
+ except Exception as e:
36
+ print(f"[Publisher] Failed to open X compose: {e}")
37
+ return {"success": False, "error": str(e)}
38
+
39
+
40
+ if __name__ == "__main__":
41
+ sample = "🚀 Just shipped a new feature! Building in public with Context Engine. #buildinpublic #devtools"
42
+ print("[Publisher] Testing open_x_compose...")
43
+ result = open_x_compose(sample)
44
+ print(f"[Publisher] Result: {result}")