gitcast 1.0.17__tar.gz → 1.0.18__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.
Files changed (89) hide show
  1. {gitcast-1.0.17/gitcast.egg-info → gitcast-1.0.18}/PKG-INFO +1 -1
  2. {gitcast-1.0.17 → gitcast-1.0.18}/ai/prompts.py +1 -0
  3. {gitcast-1.0.17 → gitcast-1.0.18}/api/payload.py +35 -29
  4. {gitcast-1.0.17 → gitcast-1.0.18}/api/routes.py +5 -1
  5. {gitcast-1.0.17 → gitcast-1.0.18/gitcast.egg-info}/PKG-INFO +1 -1
  6. {gitcast-1.0.17 → gitcast-1.0.18}/setup.py +1 -1
  7. gitcast-1.0.18/ui/popup.py +165 -0
  8. {gitcast-1.0.17 → gitcast-1.0.18}/web/index.html +31 -1
  9. gitcast-1.0.17/ui/popup.py +0 -12
  10. {gitcast-1.0.17 → gitcast-1.0.18}/.env.example +0 -0
  11. {gitcast-1.0.17 → gitcast-1.0.18}/LICENSE +0 -0
  12. {gitcast-1.0.17 → gitcast-1.0.18}/MANIFEST.in +0 -0
  13. {gitcast-1.0.17 → gitcast-1.0.18}/README.md +0 -0
  14. {gitcast-1.0.17 → gitcast-1.0.18}/ai/__init__.py +0 -0
  15. {gitcast-1.0.17 → gitcast-1.0.18}/ai/formatter.py +0 -0
  16. {gitcast-1.0.17 → gitcast-1.0.18}/ai/generator.py +0 -0
  17. {gitcast-1.0.17 → gitcast-1.0.18}/ai/viral_patterns.py +0 -0
  18. {gitcast-1.0.17 → gitcast-1.0.18}/api/__init__.py +0 -0
  19. {gitcast-1.0.17 → gitcast-1.0.18}/api/analytics.py +0 -0
  20. {gitcast-1.0.17 → gitcast-1.0.18}/api/auth.py +0 -0
  21. {gitcast-1.0.17 → gitcast-1.0.18}/api/auth_middleware.py +0 -0
  22. {gitcast-1.0.17 → gitcast-1.0.18}/api/auth_routes.py +0 -0
  23. {gitcast-1.0.17 → gitcast-1.0.18}/api/monitoring.py +0 -0
  24. {gitcast-1.0.17 → gitcast-1.0.18}/api/ratelimit.py +0 -0
  25. {gitcast-1.0.17 → gitcast-1.0.18}/api/server.py +0 -0
  26. {gitcast-1.0.17 → gitcast-1.0.18}/api/validators.py +0 -0
  27. {gitcast-1.0.17 → gitcast-1.0.18}/assets/__init__.py +0 -0
  28. {gitcast-1.0.17 → gitcast-1.0.18}/assets/favicon-16x16.png +0 -0
  29. {gitcast-1.0.17 → gitcast-1.0.18}/assets/favicon-32x32.png +0 -0
  30. {gitcast-1.0.17 → gitcast-1.0.18}/assets/favicon-64x64.png +0 -0
  31. {gitcast-1.0.17 → gitcast-1.0.18}/assets/favicon.ico +0 -0
  32. {gitcast-1.0.17 → gitcast-1.0.18}/assets/icon.png +0 -0
  33. {gitcast-1.0.17 → gitcast-1.0.18}/cli/.env.example +0 -0
  34. {gitcast-1.0.17 → gitcast-1.0.18}/cli/__init__.py +0 -0
  35. {gitcast-1.0.17 → gitcast-1.0.18}/cli/gitcast.py +0 -0
  36. {gitcast-1.0.17 → gitcast-1.0.18}/config/__init__.py +0 -0
  37. {gitcast-1.0.17 → gitcast-1.0.18}/config/settings.py +0 -0
  38. {gitcast-1.0.17 → gitcast-1.0.18}/core/__init__.py +0 -0
  39. {gitcast-1.0.17 → gitcast-1.0.18}/core/capture.py +0 -0
  40. {gitcast-1.0.17 → gitcast-1.0.18}/core/codebase_reader.py +0 -0
  41. {gitcast-1.0.17 → gitcast-1.0.18}/core/framing.py +0 -0
  42. {gitcast-1.0.17 → gitcast-1.0.18}/core/hotkey.py +0 -0
  43. {gitcast-1.0.17 → gitcast-1.0.18}/core/log_stream.py +0 -0
  44. {gitcast-1.0.17 → gitcast-1.0.18}/core/ocr.py +0 -0
  45. {gitcast-1.0.17 → gitcast-1.0.18}/core/screenshot_session.py +0 -0
  46. {gitcast-1.0.17 → gitcast-1.0.18}/core/security.py +0 -0
  47. {gitcast-1.0.17 → gitcast-1.0.18}/core/tray.py +0 -0
  48. {gitcast-1.0.17 → gitcast-1.0.18}/core/trigger.py +0 -0
  49. {gitcast-1.0.17 → gitcast-1.0.18}/docs/api-keys.md +0 -0
  50. {gitcast-1.0.17 → gitcast-1.0.18}/docs/article-generator.md +0 -0
  51. {gitcast-1.0.17 → gitcast-1.0.18}/docs/capture-flow.md +0 -0
  52. {gitcast-1.0.17 → gitcast-1.0.18}/docs/changelog.md +0 -0
  53. {gitcast-1.0.17 → gitcast-1.0.18}/docs/contributing.md +0 -0
  54. {gitcast-1.0.17 → gitcast-1.0.18}/docs/dashboard.md +0 -0
  55. {gitcast-1.0.17 → gitcast-1.0.18}/docs/insights.md +0 -0
  56. {gitcast-1.0.17 → gitcast-1.0.18}/docs/introduction.md +0 -0
  57. {gitcast-1.0.17 → gitcast-1.0.18}/docs/post-formats.md +0 -0
  58. {gitcast-1.0.17 → gitcast-1.0.18}/docs/providers.md +0 -0
  59. {gitcast-1.0.17 → gitcast-1.0.18}/docs/pypi-errors.md +0 -0
  60. {gitcast-1.0.17 → gitcast-1.0.18}/docs/quickstart.md +0 -0
  61. {gitcast-1.0.17 → gitcast-1.0.18}/docs/sprint-mode.md +0 -0
  62. {gitcast-1.0.17 → gitcast-1.0.18}/gitcast.egg-info/SOURCES.txt +0 -0
  63. {gitcast-1.0.17 → gitcast-1.0.18}/gitcast.egg-info/dependency_links.txt +0 -0
  64. {gitcast-1.0.17 → gitcast-1.0.18}/gitcast.egg-info/entry_points.txt +0 -0
  65. {gitcast-1.0.17 → gitcast-1.0.18}/gitcast.egg-info/requires.txt +0 -0
  66. {gitcast-1.0.17 → gitcast-1.0.18}/gitcast.egg-info/top_level.txt +0 -0
  67. {gitcast-1.0.17 → gitcast-1.0.18}/publisher/__init__.py +0 -0
  68. {gitcast-1.0.17 → gitcast-1.0.18}/publisher/clipboard.py +0 -0
  69. {gitcast-1.0.17 → gitcast-1.0.18}/publisher/twitter.py +0 -0
  70. {gitcast-1.0.17 → gitcast-1.0.18}/pyproject.toml +0 -0
  71. {gitcast-1.0.17 → gitcast-1.0.18}/requirements.txt +0 -0
  72. {gitcast-1.0.17 → gitcast-1.0.18}/setup.cfg +0 -0
  73. {gitcast-1.0.17 → gitcast-1.0.18}/storage/__init__.py +0 -0
  74. {gitcast-1.0.17 → gitcast-1.0.18}/storage/cleanup.py +0 -0
  75. {gitcast-1.0.17 → gitcast-1.0.18}/storage/engagement.py +0 -0
  76. {gitcast-1.0.17 → gitcast-1.0.18}/storage/insights.py +0 -0
  77. {gitcast-1.0.17 → gitcast-1.0.18}/storage/key_manager.py +0 -0
  78. {gitcast-1.0.17 → gitcast-1.0.18}/storage/logger.py +0 -0
  79. {gitcast-1.0.17 → gitcast-1.0.18}/storage/metrics.py +0 -0
  80. {gitcast-1.0.17 → gitcast-1.0.18}/storage/sprint.py +0 -0
  81. {gitcast-1.0.17 → gitcast-1.0.18}/storage/streak.py +0 -0
  82. {gitcast-1.0.17 → gitcast-1.0.18}/storage/supabase_client.py +0 -0
  83. {gitcast-1.0.17 → gitcast-1.0.18}/storage/tone_memory.py +0 -0
  84. {gitcast-1.0.17 → gitcast-1.0.18}/tests/test_keys.py +0 -0
  85. {gitcast-1.0.17 → gitcast-1.0.18}/tests/test_ocr_fix.py +0 -0
  86. {gitcast-1.0.17 → gitcast-1.0.18}/ui/__init__.py +0 -0
  87. {gitcast-1.0.17 → gitcast-1.0.18}/ui/review.py +0 -0
  88. {gitcast-1.0.17 → gitcast-1.0.18}/web/__init__.py +0 -0
  89. {gitcast-1.0.17 → gitcast-1.0.18}/web/landing.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitcast
3
- Version: 1.0.17
3
+ Version: 1.0.18
4
4
  Summary: git diff → published post. under 60 seconds.
5
5
  Home-page: https://github.com/drizzy765/gitcast
6
6
  Author: Timilehin Agoro
@@ -54,6 +54,7 @@ Rules:
54
54
  or the developer's raw thought. If the available
55
55
  context is thin or unclear, write a short, honest,
56
56
  low-key post rather than fabricating details.
57
+ - If a git diff is provided, it is the primary source of truth. Screen text is secondary and may be noisy — ignore any browser UI text.
57
58
  - If OCR text looks like browser UI, tab names, or
58
59
  garbled text rather than code, IGNORE it completely
59
60
  and rely only on the raw thought and git diff.
@@ -104,13 +104,6 @@ def _build_user_message(
104
104
  if raw_thought.strip():
105
105
  parts.append(f"## Developer's raw thought\n{raw_thought.strip()}")
106
106
 
107
- parts.append(
108
- "## Priority\n"
109
- "If a git diff is present below, treat it as "
110
- "the primary source of truth for what changed. "
111
- "Screen text is secondary context only."
112
- )
113
-
114
107
  # git diff section comes first
115
108
  diff_text = git_diff.get("diff", "") if isinstance(git_diff, dict) else ""
116
109
  if diff_text:
@@ -118,13 +111,6 @@ def _build_user_message(
118
111
 
119
112
  # check if OCR text is fragmented
120
113
  ocr_text = "\n\n".join([s.get("ocr_text", "") for s in screenshots if s.get("ocr_text")]).strip()
121
- if len(ocr_text.strip()) < 80:
122
- parts.append(
123
- "## Note\n"
124
- "Screen context is fragmented/unreliable — "
125
- "do not base the post on it. Rely on the "
126
- "developer's raw thought and git diff only."
127
- )
128
114
 
129
115
  # OCR section comes second, clearly labeled
130
116
  if ocr_text and not use_vision:
@@ -208,35 +194,55 @@ def _encode_image(image_path: str) -> str:
208
194
 
209
195
  # ── Payload validator ─────────────────────────────────────────────────────────
210
196
 
197
+ GENERIC_THOUGHTS = [
198
+ "captured via hotkey trigger",
199
+ "",
200
+ "test",
201
+ "testing",
202
+ "...",
203
+ "thought",
204
+ ]
205
+
211
206
  def validate_payload(payload: dict) -> tuple[bool, list[str]]:
212
207
  """
213
208
  Checks the payload has the minimum viable content to generate a post.
214
209
  Returns (is_valid, list_of_warnings).
215
210
  """
216
211
  warnings = []
212
+
217
213
  raw_thought = payload.get("raw_thought", "").strip()
218
214
  ocr_text = payload.get("ocr_text", "").strip()
215
+ git_diff = payload.get("git_diff", "").strip()
219
216
  git_diff_available = payload.get("git_diff_available", False)
220
217
 
221
- generic_thoughts = [
222
- "captured via hotkey trigger",
223
- "", "test", "testing",
224
- ]
225
- is_generic = raw_thought.lower() in generic_thoughts
226
-
227
- has_real_content = (
228
- not is_generic or
229
- len(ocr_text) > 100 or
230
- git_diff_available
218
+ is_generic_thought = (
219
+ raw_thought.lower() in GENERIC_THOUGHTS or
220
+ len(raw_thought) < 5
231
221
  )
222
+ has_real_ocr = len(ocr_text) >= 80
223
+ has_diff = git_diff_available and len(git_diff) > 20
224
+
225
+ # BLOCK generation if all three are weak
226
+ if is_generic_thought and not has_real_ocr and not has_diff:
227
+ return False, [
228
+ "Not enough context to generate a post. Please:\n"
229
+ "1. Capture from your code editor (not a browser)\n"
230
+ "2. Type a real thought about what you just built or fixed\n"
231
+ "3. Make sure you have uncommitted git changes for best results"
232
+ ]
233
+
234
+ # WARN but allow if thought is real but diff missing
235
+ if not has_diff:
236
+ warnings.append(
237
+ "No git diff — post will rely on your thought and screen context only."
238
+ )
232
239
 
233
- if not has_real_content:
240
+ if not raw_thought or is_generic_thought:
234
241
  warnings.append(
235
- "No meaningful context captured type a "
236
- "real thought or capture a code window")
242
+ "Generic thought detectedpost quality will be lower. Try describing what you actually fixed or built."
243
+ )
237
244
 
238
- is_valid = bool(raw_thought) # still allow generation
239
- return is_valid, warnings
245
+ return True, warnings
240
246
 
241
247
 
242
248
  # ── Test ──────────────────────────────────────────────────────────────────────
@@ -789,7 +789,11 @@ async def generate(request: Request, body: GenerateRequest, user_id: str = Depen
789
789
  if not is_valid:
790
790
  raise HTTPException(
791
791
  status_code=400,
792
- detail="Payload has no raw thought — cannot generate posts."
792
+ detail={
793
+ "error": "insufficient_context",
794
+ "message": warnings[0] if warnings else "Not enough context.",
795
+ "warnings": warnings,
796
+ }
793
797
  )
794
798
 
795
799
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitcast
3
- Version: 1.0.17
3
+ Version: 1.0.18
4
4
  Summary: git diff → published post. under 60 seconds.
5
5
  Home-page: https://github.com/drizzy765/gitcast
6
6
  Author: Timilehin Agoro
@@ -51,7 +51,7 @@ if os.path.exists(requirements_path):
51
51
 
52
52
  setup(
53
53
  name="gitcast",
54
- version="1.0.17",
54
+ version="1.0.18",
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,165 @@
1
+ import tkinter as tk
2
+ from tkinter import font as tkfont
3
+ import threading
4
+
5
+
6
+ # ── Constants ─────────────────────────────────────────────────────────────────
7
+
8
+ POPUP_WIDTH = 520
9
+ POPUP_HEIGHT = 120
10
+ BG_COLOR = "#1a1a1a"
11
+ BORDER_COLOR = "#333333"
12
+ TEXT_COLOR = "#f0f0f0"
13
+ PLACEHOLDER_COLOR = "#555555"
14
+ ACCENT_COLOR = "#6366f1" # indigo — matches tray icon
15
+ FONT_FAMILY = "Segoe UI"
16
+ FONT_SIZE = 13
17
+
18
+
19
+ # ── Popup window ──────────────────────────────────────────────────────────────
20
+
21
+ class CapturePopup:
22
+ def __init__(self, on_submit, on_dismiss):
23
+ """
24
+ Args:
25
+ on_submit: Callback called with the user's raw thought string.
26
+ on_dismiss: Callback called when the user hits Esc or closes.
27
+ """
28
+ self.on_submit = on_submit
29
+ self.on_dismiss = on_dismiss
30
+ self.result = None
31
+ self.root = None
32
+
33
+ def show(self):
34
+ """Build and display the popup. Blocks until submitted or dismissed."""
35
+ self.root = tk.Tk()
36
+ self.root.withdraw() # hide while building to prevent flicker
37
+
38
+ self._configure_window()
39
+ self._build_ui()
40
+ self._center_on_screen()
41
+ self._bind_keys()
42
+
43
+ self.root.deiconify() # show fully built window
44
+ self.root.focus_force()
45
+ self.entry.focus_set()
46
+
47
+ self.root.mainloop()
48
+
49
+ def _configure_window(self):
50
+ self.root.title("")
51
+ self.root.configure(bg=BG_COLOR)
52
+ self.root.overrideredirect(True) # removes title bar and border
53
+ self.root.attributes("-topmost", True) # float above all windows
54
+ self.root.resizable(False, False)
55
+
56
+ # draw a 1px border using a frame
57
+ self.root.configure(highlightbackground=BORDER_COLOR, highlightthickness=1)
58
+
59
+ def _build_ui(self):
60
+ main_frame = tk.Frame(self.root, bg=BG_COLOR, padx=20, pady=16)
61
+ main_frame.pack(fill=tk.BOTH, expand=True)
62
+
63
+ # label
64
+ label_font = tkfont.Font(family=FONT_FAMILY, size=10)
65
+ label = tk.Label(
66
+ main_frame,
67
+ text="What was the struggle or win?",
68
+ bg=BG_COLOR,
69
+ fg=PLACEHOLDER_COLOR,
70
+ font=label_font,
71
+ anchor="w",
72
+ )
73
+ label.pack(fill=tk.X, pady=(0, 8))
74
+
75
+ # accent line under label
76
+ accent_line = tk.Frame(main_frame, bg=ACCENT_COLOR, height=1)
77
+ accent_line.pack(fill=tk.X, pady=(0, 10))
78
+
79
+ # text input
80
+ entry_font = tkfont.Font(family=FONT_FAMILY, size=FONT_SIZE)
81
+ self.entry = tk.Entry(
82
+ main_frame,
83
+ font=entry_font,
84
+ bg=BG_COLOR,
85
+ fg=TEXT_COLOR,
86
+ insertbackground=ACCENT_COLOR, # cursor color
87
+ relief=tk.FLAT,
88
+ bd=0,
89
+ highlightthickness=0,
90
+ )
91
+ self.entry.pack(fill=tk.X)
92
+
93
+ # hint text at bottom
94
+ hint_font = tkfont.Font(family=FONT_FAMILY, size=9)
95
+ hint = tk.Label(
96
+ main_frame,
97
+ text="Enter to capture · Esc to cancel",
98
+ bg=BG_COLOR,
99
+ fg=PLACEHOLDER_COLOR,
100
+ font=hint_font,
101
+ anchor="e",
102
+ )
103
+ hint.pack(fill=tk.X, pady=(10, 0))
104
+
105
+ def _center_on_screen(self):
106
+ self.root.update_idletasks()
107
+ screen_w = self.root.winfo_screenwidth()
108
+ screen_h = self.root.winfo_screenheight()
109
+ x = (screen_w - POPUP_WIDTH) // 2
110
+ y = int(screen_h * 0.38) # slightly above center — feels more natural
111
+ self.root.geometry(f"{POPUP_WIDTH}x{POPUP_HEIGHT}+{x}+{y}")
112
+
113
+ def _bind_keys(self):
114
+ self.root.bind("<Return>", self._handle_submit)
115
+ self.root.bind("<Escape>", self._handle_dismiss)
116
+ self.root.bind("<FocusOut>", self._handle_focus_out)
117
+ self.root.protocol("WM_DELETE_WINDOW", self._handle_dismiss)
118
+
119
+ def _handle_submit(self, event=None):
120
+ text = self.entry.get().strip()
121
+ if not text:
122
+ # shake the entry to signal empty input
123
+ self._shake()
124
+ return
125
+ self.result = text
126
+ self.root.destroy()
127
+ self.on_submit(text)
128
+
129
+ def _handle_dismiss(self, event=None):
130
+ self.root.destroy()
131
+ self.on_dismiss()
132
+
133
+ def _handle_focus_out(self, event=None):
134
+ """Dismiss if user clicks away from the popup."""
135
+ self.root.destroy()
136
+ self.on_dismiss()
137
+
138
+ def _shake(self):
139
+ """Briefly shakes the window to signal empty input."""
140
+ x = self.root.winfo_x()
141
+ y = self.root.winfo_y()
142
+ for delta in [6, -6, 4, -4, 2, -2, 0]:
143
+ self.root.geometry(f"{POPUP_WIDTH}x{POPUP_HEIGHT}+{x + delta}+{y}")
144
+ self.root.update()
145
+ self.root.after(18)
146
+
147
+
148
+ # ── Public interface ──────────────────────────────────────────────────────────
149
+
150
+ def show_popup(on_submit, on_dismiss):
151
+ """
152
+ Shows the capture popup in a new thread so it doesn't block
153
+ the hotkey listener or the tray process.
154
+ """
155
+ def run():
156
+ try:
157
+ popup = CapturePopup(on_submit=on_submit, on_dismiss=on_dismiss)
158
+ popup.show()
159
+ except Exception as e:
160
+ print(f"[Gitcast] Popup display error: {e}")
161
+ on_dismiss()
162
+
163
+ thread = threading.Thread(target=run, daemon=True)
164
+ thread.start()
165
+ return thread
@@ -2637,6 +2637,17 @@
2637
2637
  }
2638
2638
  if (!res.ok) {
2639
2639
  const text = await res.text();
2640
+ let errorData = {};
2641
+ try {
2642
+ errorData = JSON.parse(text);
2643
+ } catch(e) {}
2644
+ const detail = errorData.detail || {};
2645
+ const error = typeof detail === 'object' ? (detail.error || '') : (detail || '');
2646
+
2647
+ if (error.includes('insufficient_context') || res.status === 400) {
2648
+ showContextWarning(detail.message || errorData.message || error || text);
2649
+ return null;
2650
+ }
2640
2651
  throw new Error(text || `Request failed with status ${res.status}`);
2641
2652
  }
2642
2653
  return res.json();
@@ -4989,7 +5000,15 @@
4989
5000
  }
4990
5001
  }
4991
5002
 
4992
- function appendLog(entry) {
5003
+ function appendLog(entry, level = null, message = null) {
5004
+ if (typeof entry === 'string') {
5005
+ entry = {
5006
+ module: entry,
5007
+ level: level,
5008
+ message: message,
5009
+ time: new Date().toLocaleTimeString()
5010
+ };
5011
+ }
4993
5012
  state.logs.push(entry);
4994
5013
  if (state.logs.length > 50) state.logs = state.logs.slice(-50);
4995
5014
  if (state.logsOpen) {
@@ -4997,6 +5016,17 @@
4997
5016
  }
4998
5017
  }
4999
5018
 
5019
+ function showContextWarning(message) {
5020
+ const editor = document.getElementById('post-editor') || document.getElementById('main-editor');
5021
+ if (editor) {
5022
+ editor.value = '';
5023
+ editor.placeholder = message ||
5024
+ 'Not enough context — capture from ' +
5025
+ 'your code editor and type a real thought.';
5026
+ }
5027
+ appendLog('GENERATOR', 'WARN', 'generation blocked — thin context');
5028
+ }
5029
+
5000
5030
  function renderLogs() {
5001
5031
  const body = document.getElementById('logs-body');
5002
5032
  if (!body) return;
@@ -1,12 +0,0 @@
1
- def show_popup(on_submit, on_dismiss):
2
- """
3
- Tkinter popup fallback/mock.
4
- Immediately submits a default thought to run the pipeline.
5
- """
6
- import threading
7
-
8
- def run():
9
- print("[Gitcast] Mock popup triggered.")
10
- on_submit("Captured via hotkey trigger")
11
-
12
- threading.Thread(target=run, daemon=True).start()
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
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
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
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
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