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
api/payload.py ADDED
@@ -0,0 +1,253 @@
1
+ import base64
2
+ from pathlib import Path
3
+ from config.settings import get_project_narrative
4
+ from core.log_stream import stream_log
5
+
6
+
7
+ # ── Payload builder ───────────────────────────────────────────────────────────
8
+
9
+ def build_payload(
10
+ raw_thought: str,
11
+ ocr_result: dict,
12
+ capture_result: dict,
13
+ format_keys: list = None,
14
+ multi_screenshots: list = None,
15
+ ) -> dict:
16
+ """
17
+ Assembles the full payload from all captured context.
18
+ Supports single or multi-screenshot sessions.
19
+ """
20
+ if format_keys is None:
21
+ format_keys = ["deep_tech", "linkedin", "pr_generator", "quick_win"]
22
+
23
+ narrative = get_project_narrative()
24
+ git_diff = capture_result.get("git_diff", {"diff": "", "success": False})
25
+
26
+ # Handle single or multi-shot
27
+ if multi_screenshots:
28
+ screenshots = multi_screenshots
29
+ primary_shot = screenshots[0]
30
+ else:
31
+ # Normalize single shot to a list of one
32
+ primary_shot = capture_result["screenshot"]
33
+ screenshots = [{
34
+ "path": primary_shot["path"],
35
+ "purpose": "general",
36
+ "ocr_text": ocr_result.get("text") or ocr_result.get("raw_text", ""),
37
+ "confidence": ocr_result.get("confidence", 0.0),
38
+ "timestamp": primary_shot.get("timestamp", ""),
39
+ "index": 1
40
+ }]
41
+
42
+ # Decide vision fallback based on primary shot or all shots
43
+ # For now, we'll use OCR if any shot is reliable, but usually it's per-shot.
44
+ # LLM will see OCR text for each shot.
45
+ use_vision = ocr_result.get("use_vision_fallback", False) if not multi_screenshots else False
46
+
47
+ # encode primary screenshot as base64 for vision fallback
48
+ screenshot_b64 = None
49
+ if use_vision and primary_shot.get("path"):
50
+ screenshot_b64 = _encode_image(primary_shot["path"])
51
+
52
+ # build the structured user message
53
+ user_message = _build_user_message(
54
+ raw_thought=raw_thought,
55
+ screenshots=screenshots,
56
+ git_diff=git_diff,
57
+ narrative=narrative,
58
+ use_vision=use_vision,
59
+ )
60
+
61
+ # joined OCR text for legacy/summary access
62
+ all_ocr = "\n\n".join([s.get("ocr_text", "") for s in screenshots if s.get("ocr_text")])
63
+
64
+ stream_log(
65
+ "Payload",
66
+ "OK",
67
+ f"assembled payload: {len(screenshots)} screenshot(s), {len(all_ocr)} OCR chars",
68
+ )
69
+
70
+ return {
71
+ "raw_thought": raw_thought.strip(),
72
+ "ocr_text": all_ocr,
73
+ "screenshots": screenshots,
74
+ "git_diff": git_diff.get("diff", ""),
75
+ "git_diff_available": git_diff.get("success", False) and bool(git_diff.get("diff")),
76
+ "narrative": narrative,
77
+ "use_vision_fallback": use_vision,
78
+ "screenshot_b64": screenshot_b64,
79
+ "screenshot_path": primary_shot.get("path", ""),
80
+ "user_message": user_message,
81
+ "format_keys": format_keys,
82
+ "timestamp": primary_shot.get("timestamp", ""),
83
+ "working_dir": capture_result.get("working_dir", ""),
84
+ }
85
+
86
+
87
+ # ── User message builder ──────────────────────────────────────────────────────
88
+
89
+ def _build_user_message(
90
+ raw_thought: str,
91
+ screenshots: list,
92
+ git_diff: dict,
93
+ narrative: str,
94
+ use_vision: bool,
95
+ ) -> str:
96
+ """
97
+ Builds the user-turn message that gets sent to the LLM.
98
+ Structures multiple screenshots with their purpose tags.
99
+ """
100
+ parts = []
101
+ parts.append("Here is the context for this build update:\n")
102
+
103
+ # developer's raw thought
104
+ if raw_thought.strip():
105
+ parts.append(f"## Developer's raw thought\n{raw_thought.strip()}")
106
+
107
+ # Screenshots with Purpose Tags and OCR
108
+ parts.append("## Screen context")
109
+ for s in screenshots:
110
+ idx = s.get("index", 1)
111
+ purpose = s.get("purpose", "general")
112
+ parts.append(f"### Screenshot {idx} ({purpose})")
113
+
114
+ ocr = s.get("ocr_text", "").strip()
115
+ if ocr:
116
+ parts.append(f"Visible text:\n{ocr}")
117
+ else:
118
+ parts.append("[No text detected or OCR failed]")
119
+
120
+ # git diff
121
+ diff_text = git_diff.get("diff", "") if isinstance(git_diff, dict) else ""
122
+ if diff_text:
123
+ parts.append(f"## Git diff (recent code changes)\n```\n{diff_text.strip()}\n```")
124
+
125
+ # project narrative
126
+ if narrative:
127
+ parts.append(f"## Project context\n{narrative}")
128
+
129
+ parts.append(
130
+ "\nUsing the context above, generate the post now. "
131
+ "Follow the format and rules in the system prompt exactly."
132
+ )
133
+
134
+ return "\n\n".join(parts)
135
+
136
+
137
+ # ── Sprint Mode payload ───────────────────────────────────────────────────────
138
+
139
+ def build_sprint_payload(sprint_log_entries: list) -> dict:
140
+ """
141
+ Builds the payload for Sprint Mode — takes the full list of
142
+ silent captures and assembles them into one batched message
143
+ for the sprint summary prompt.
144
+ """
145
+ parts = ["You have been given a log of captures from a coding sprint.\n"]
146
+
147
+ for i, entry in enumerate(sprint_log_entries, 1):
148
+ parts.append(f"--- Capture {i} of {len(sprint_log_entries)} ---")
149
+
150
+ if entry.get("raw_thought"):
151
+ parts.append(f"Developer thought: {entry['raw_thought']}")
152
+
153
+ if entry.get("git_diff"):
154
+ parts.append(f"Code changes:\n```\n{entry['git_diff'][:500]}\n```")
155
+
156
+ if entry.get("ocr_text"):
157
+ parts.append(f"Screen context: {entry['ocr_text'][:300]}")
158
+
159
+ if entry.get("timestamp"):
160
+ parts.append(f"Time: {entry['timestamp']}")
161
+
162
+ parts.append(
163
+ "\nSynthesize all of the above into a compelling sprint thread. "
164
+ "Follow the format and rules in the system prompt exactly."
165
+ )
166
+
167
+ narrative = get_project_narrative()
168
+
169
+ return {
170
+ "user_message": "\n\n".join(parts),
171
+ "format_keys": ["sprint_summary"],
172
+ "narrative": narrative,
173
+ "num_captures": len(sprint_log_entries),
174
+ "use_vision_fallback": False,
175
+ "screenshot_b64": None,
176
+ }
177
+
178
+
179
+ # ── Image encoder ─────────────────────────────────────────────────────────────
180
+
181
+ def _encode_image(image_path: str) -> str:
182
+ """
183
+ Encodes an image file as a base64 string for the Gemini vision API.
184
+ Returns empty string if encoding fails.
185
+ """
186
+ try:
187
+ with open(image_path, "rb") as f:
188
+ return base64.b64encode(f.read()).decode("utf-8")
189
+ except Exception as e:
190
+ stream_log("Payload", "WARN", f"image encoding failed: {e}")
191
+ return ""
192
+
193
+
194
+ # ── Payload validator ─────────────────────────────────────────────────────────
195
+
196
+ def validate_payload(payload: dict) -> tuple[bool, list[str]]:
197
+ """
198
+ Checks the payload has the minimum viable content to generate a post.
199
+ Returns (is_valid, list_of_warnings).
200
+ """
201
+ warnings = []
202
+
203
+ if not payload.get("raw_thought"):
204
+ warnings.append("No raw thought provided — post quality will be lower.")
205
+
206
+ if not payload.get("git_diff_available"):
207
+ warnings.append("No git diff — post will rely on OCR and raw thought only.")
208
+
209
+ if not payload.get("ocr_text") and not payload.get("use_vision_fallback"):
210
+ warnings.append("No OCR text and no vision fallback — very limited context.")
211
+
212
+ if not payload.get("narrative"):
213
+ warnings.append("No project narrative set — posts will lack mission context.")
214
+
215
+ # payload is valid as long as there's at least a raw thought
216
+ is_valid = bool(payload.get("raw_thought"))
217
+
218
+ return is_valid, warnings
219
+
220
+
221
+ # ── Test ──────────────────────────────────────────────────────────────────────
222
+
223
+ if __name__ == "__main__":
224
+ from core.capture import run_capture
225
+ from core.ocr import run_ocr
226
+ from config.settings import set_project_narrative
227
+
228
+ # set a test narrative
229
+ set_project_narrative("an AI-powered build-in-public automation tool for developers")
230
+
231
+ print("[Payload] Running capture pipeline...")
232
+ capture = run_capture()
233
+ ocr = run_ocr(capture["screenshot"]["path"])
234
+
235
+ payload = build_payload(
236
+ raw_thought="just got OCR working and it's reading the screen reliably",
237
+ ocr_result=ocr,
238
+ capture_result=capture,
239
+ )
240
+
241
+ is_valid, warnings = validate_payload(payload)
242
+
243
+ print("\n=== PAYLOAD RESULT ===")
244
+ print(f"Valid: {is_valid}")
245
+ print(f"Warnings: {warnings if warnings else 'none'}")
246
+ print(f"Raw thought: {payload['raw_thought']}")
247
+ print(f"OCR text length: {len(payload['ocr_text'])} chars")
248
+ print(f"Git diff available: {payload['git_diff_available']}")
249
+ print(f"Use vision: {payload['use_vision_fallback']}")
250
+ print(f"Narrative: {payload['narrative']}")
251
+ print(f"Format keys: {payload['format_keys']}")
252
+ print(f"\nUser message preview:")
253
+ print(payload["user_message"][:600])
api/ratelimit.py ADDED
@@ -0,0 +1,9 @@
1
+ from slowapi import Limiter
2
+ from slowapi.util import get_remote_address
3
+
4
+
5
+ limiter = Limiter(key_func=get_remote_address, default_limits=["60/minute"], headers_enabled=False)
6
+
7
+
8
+ if __name__ == "__main__":
9
+ print("[RateLimit] Default limit: 60/minute")