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.
- ai/__init__.py +0 -0
- ai/formatter.py +59 -0
- ai/generator.py +604 -0
- ai/prompts.py +197 -0
- ai/viral_patterns.py +75 -0
- api/__init__.py +0 -0
- api/analytics.py +48 -0
- api/auth.py +49 -0
- api/auth_middleware.py +129 -0
- api/auth_routes.py +117 -0
- api/monitoring.py +56 -0
- api/payload.py +253 -0
- api/ratelimit.py +9 -0
- api/routes.py +1565 -0
- api/server.py +162 -0
- api/validators.py +101 -0
- assets/__init__.py +1 -0
- assets/favicon-16x16.png +0 -0
- assets/favicon-32x32.png +0 -0
- assets/favicon-64x64.png +0 -0
- assets/favicon.ico +0 -0
- assets/icon.png +0 -0
- cli/.env.example +26 -0
- cli/__init__.py +1 -0
- cli/gitcast.py +79 -0
- config/__init__.py +0 -0
- config/settings.py +213 -0
- core/__init__.py +0 -0
- core/capture.py +258 -0
- core/codebase_reader.py +90 -0
- core/framing.py +86 -0
- core/hotkey.py +21 -0
- core/log_stream.py +50 -0
- core/ocr.py +173 -0
- core/screenshot_session.py +274 -0
- core/security.py +126 -0
- core/tray.py +54 -0
- gitcast-1.0.0.dist-info/LICENSE +21 -0
- gitcast-1.0.0.dist-info/METADATA +67 -0
- gitcast-1.0.0.dist-info/RECORD +61 -0
- gitcast-1.0.0.dist-info/WHEEL +5 -0
- gitcast-1.0.0.dist-info/entry_points.txt +2 -0
- gitcast-1.0.0.dist-info/top_level.txt +10 -0
- publisher/__init__.py +0 -0
- publisher/clipboard.py +44 -0
- publisher/twitter.py +100 -0
- storage/__init__.py +0 -0
- storage/cleanup.py +60 -0
- storage/engagement.py +114 -0
- storage/insights.py +203 -0
- storage/key_manager.py +45 -0
- storage/logger.py +208 -0
- storage/metrics.py +119 -0
- storage/sprint.py +40 -0
- storage/streak.py +0 -0
- storage/supabase_client.py +25 -0
- storage/tone_memory.py +139 -0
- ui/__init__.py +0 -0
- web/__init__.py +1 -0
- web/index.html +4994 -0
- 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