create-screenfix 0.1.0

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 (34) hide show
  1. package/bin/create-screenfix.js +40 -0
  2. package/lib/doctor.js +120 -0
  3. package/lib/init.js +165 -0
  4. package/lib/uninstall.js +67 -0
  5. package/lib/update.js +62 -0
  6. package/package.json +35 -0
  7. package/python/build/lib/screenfix/__init__.py +3 -0
  8. package/python/build/lib/screenfix/annotation_window.py +222 -0
  9. package/python/build/lib/screenfix/clipboard_watcher.py +164 -0
  10. package/python/build/lib/screenfix/config.py +66 -0
  11. package/python/build/lib/screenfix/daemon.py +204 -0
  12. package/python/build/lib/screenfix/mcp_server.py +318 -0
  13. package/python/build/lib/screenfix/task_tracker.py +129 -0
  14. package/python/pyproject.toml +24 -0
  15. package/python/requirements.txt +5 -0
  16. package/python/screenfix/__init__.py +3 -0
  17. package/python/screenfix/annotation_window.py +222 -0
  18. package/python/screenfix/clipboard_watcher.py +164 -0
  19. package/python/screenfix/config.py +66 -0
  20. package/python/screenfix/daemon.py +204 -0
  21. package/python/screenfix/mcp_server.py +318 -0
  22. package/python/screenfix/task_tracker.py +129 -0
  23. package/python/screenfix.egg-info/PKG-INFO +11 -0
  24. package/python/screenfix.egg-info/SOURCES.txt +14 -0
  25. package/python/screenfix.egg-info/dependency_links.txt +1 -0
  26. package/python/screenfix.egg-info/entry_points.txt +3 -0
  27. package/python/screenfix.egg-info/requires.txt +5 -0
  28. package/python/screenfix.egg-info/top_level.txt +1 -0
  29. package/templates/commands/screenfix-list-tasks.md +6 -0
  30. package/templates/commands/screenfix-start.md +8 -0
  31. package/templates/commands/screenfix-status.md +6 -0
  32. package/templates/commands/screenfix-stop.md +6 -0
  33. package/templates/commands/screenfix-tasks-next.md +14 -0
  34. package/templates/commands/screenfix-tasks-yolo.md +14 -0
@@ -0,0 +1,318 @@
1
+ """MCP server for ScreenFix."""
2
+
3
+ import asyncio
4
+ import base64
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from mcp.server import Server
14
+ from mcp.server.stdio import stdio_server
15
+ from mcp.types import Tool, TextContent, ImageContent
16
+
17
+ from .config import config
18
+ from .task_tracker import get_tasks, get_pending_tasks, mark_task_complete
19
+
20
+
21
+ STATE_FILE = Path.home() / ".config" / "screenfix" / "state.json"
22
+
23
+
24
+ def get_daemon_state() -> dict:
25
+ """Read the daemon state file."""
26
+ if not STATE_FILE.exists():
27
+ return {"running": False, "listening": False}
28
+
29
+ try:
30
+ with open(STATE_FILE, "r") as f:
31
+ state = json.load(f)
32
+
33
+ pid = state.get("pid")
34
+ if pid:
35
+ try:
36
+ os.kill(pid, 0)
37
+ state["running"] = True
38
+ except OSError:
39
+ state["running"] = False
40
+ else:
41
+ state["running"] = False
42
+
43
+ return state
44
+ except (json.JSONDecodeError, IOError):
45
+ return {"running": False, "listening": False}
46
+
47
+
48
+ def get_screenshots() -> list[dict]:
49
+ """Get list of screenshots."""
50
+ save_dir = Path(config.save_directory)
51
+ if not save_dir.exists():
52
+ return []
53
+
54
+ screenshots = []
55
+ for f in save_dir.glob("*.png"):
56
+ screenshots.append({
57
+ "path": str(f),
58
+ "filename": f.name,
59
+ "modified": f.stat().st_mtime,
60
+ })
61
+
62
+ screenshots.sort(key=lambda x: x["modified"], reverse=True)
63
+ return screenshots
64
+
65
+
66
+ def get_last_screenshot() -> dict | None:
67
+ """Get the most recent screenshot."""
68
+ screenshots = get_screenshots()
69
+ return screenshots[0] if screenshots else None
70
+
71
+
72
+ def start_daemon() -> tuple[bool, str]:
73
+ """Start the daemon as a background process."""
74
+ state = get_daemon_state()
75
+ if state.get("running"):
76
+ return True, "Daemon is already running"
77
+
78
+ try:
79
+ log_dir = Path.home() / ".config" / "screenfix"
80
+ log_dir.mkdir(parents=True, exist_ok=True)
81
+ log_file = log_dir / "daemon.log"
82
+
83
+ with open(log_file, "a") as log:
84
+ subprocess.Popen(
85
+ [sys.executable, "-m", "screenfix.daemon"],
86
+ cwd=os.getcwd(),
87
+ stdout=log,
88
+ stderr=log,
89
+ start_new_session=True,
90
+ )
91
+
92
+ time.sleep(0.5)
93
+
94
+ new_state = get_daemon_state()
95
+ if new_state.get("running"):
96
+ return True, f"Daemon started (PID: {new_state.get('pid')}). Use Cmd+Ctrl+Shift+4 to capture."
97
+ else:
98
+ return True, f"Daemon starting... Check logs at {log_file}"
99
+
100
+ except Exception as e:
101
+ return False, f"Failed to start daemon: {e}"
102
+
103
+
104
+ def stop_daemon() -> tuple[bool, str]:
105
+ """Stop the daemon."""
106
+ state = get_daemon_state()
107
+ if not state.get("running"):
108
+ return True, "Daemon is not running"
109
+
110
+ pid = state.get("pid")
111
+ if pid:
112
+ try:
113
+ os.kill(pid, 15)
114
+ return True, "Daemon stopped"
115
+ except OSError as e:
116
+ return False, f"Failed to stop daemon: {e}"
117
+
118
+ return False, "Could not find daemon PID"
119
+
120
+
121
+ def create_server() -> Server:
122
+ """Create the MCP server."""
123
+ server = Server("screenfix")
124
+
125
+ @server.list_tools()
126
+ async def list_tools() -> list[Tool]:
127
+ return [
128
+ Tool(
129
+ name="start_daemon",
130
+ description="Start ScreenFix daemon. Use Cmd+Ctrl+Shift+4 to capture screenshots instantly.",
131
+ inputSchema={"type": "object", "properties": {}, "required": []},
132
+ ),
133
+ Tool(
134
+ name="stop_daemon",
135
+ description="Stop the ScreenFix daemon",
136
+ inputSchema={"type": "object", "properties": {}, "required": []},
137
+ ),
138
+ Tool(
139
+ name="get_status",
140
+ description="Get ScreenFix status",
141
+ inputSchema={"type": "object", "properties": {}, "required": []},
142
+ ),
143
+ Tool(
144
+ name="get_last_screenshot",
145
+ description="Get the most recent screenshot with its image and related task",
146
+ inputSchema={
147
+ "type": "object",
148
+ "properties": {
149
+ "include_image": {
150
+ "type": "boolean",
151
+ "description": "Include image data",
152
+ "default": True,
153
+ }
154
+ },
155
+ "required": [],
156
+ },
157
+ ),
158
+ Tool(
159
+ name="list_screenshots",
160
+ description="List all screenshots",
161
+ inputSchema={
162
+ "type": "object",
163
+ "properties": {
164
+ "limit": {"type": "integer", "default": 10}
165
+ },
166
+ "required": [],
167
+ },
168
+ ),
169
+ Tool(
170
+ name="get_tasks",
171
+ description="Get tasks from tasks.md",
172
+ inputSchema={
173
+ "type": "object",
174
+ "properties": {
175
+ "pending_only": {"type": "boolean", "default": False}
176
+ },
177
+ "required": [],
178
+ },
179
+ ),
180
+ Tool(
181
+ name="complete_task",
182
+ description="Mark a task as complete",
183
+ inputSchema={
184
+ "type": "object",
185
+ "properties": {
186
+ "task_text": {"type": "string", "description": "Task text to mark complete"}
187
+ },
188
+ "required": ["task_text"],
189
+ },
190
+ ),
191
+ Tool(
192
+ name="read_screenshot",
193
+ description="Read a specific screenshot by path",
194
+ inputSchema={
195
+ "type": "object",
196
+ "properties": {
197
+ "path": {"type": "string", "description": "Path to screenshot"}
198
+ },
199
+ "required": ["path"],
200
+ },
201
+ ),
202
+ ]
203
+
204
+ @server.call_tool()
205
+ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent | ImageContent]:
206
+
207
+ if name == "start_daemon":
208
+ success, message = start_daemon()
209
+ return [TextContent(type="text", text=message)]
210
+
211
+ elif name == "stop_daemon":
212
+ success, message = stop_daemon()
213
+ return [TextContent(type="text", text=message)]
214
+
215
+ elif name == "get_status":
216
+ state = get_daemon_state()
217
+ screenshots = get_screenshots()
218
+ tasks = get_pending_tasks()
219
+
220
+ status = "Running" if state.get("running") else "Not running"
221
+ text = f"""ScreenFix Status:
222
+ - Daemon: {status}
223
+ - Screenshots: {len(screenshots)}
224
+ - Pending tasks: {len(tasks)}
225
+
226
+ Use Cmd+Ctrl+Shift+4 to capture (instant, no delay)"""
227
+ return [TextContent(type="text", text=text)]
228
+
229
+ elif name == "get_last_screenshot":
230
+ include_image = arguments.get("include_image", True)
231
+ screenshot = get_last_screenshot()
232
+
233
+ if not screenshot:
234
+ return [TextContent(type="text", text="No screenshots found. Use Cmd+Ctrl+Shift+4 to capture.")]
235
+
236
+ result = [TextContent(type="text", text=f"Screenshot: {screenshot['path']}")]
237
+
238
+ if include_image and os.path.exists(screenshot["path"]):
239
+ with open(screenshot["path"], "rb") as f:
240
+ image_data = base64.standard_b64encode(f.read()).decode("utf-8")
241
+ result.append(ImageContent(type="image", data=image_data, mimeType="image/png"))
242
+
243
+ tasks = get_tasks()
244
+ for task in tasks:
245
+ if task.get("screenshot") == screenshot["path"]:
246
+ result.append(TextContent(
247
+ type="text",
248
+ text=f"\nTask: {task['text']}\nStatus: {'Done' if task['completed'] else 'Pending'}"
249
+ ))
250
+ break
251
+
252
+ return result
253
+
254
+ elif name == "list_screenshots":
255
+ limit = arguments.get("limit", 10)
256
+ screenshots = get_screenshots()[:limit]
257
+
258
+ if not screenshots:
259
+ return [TextContent(type="text", text="No screenshots found.")]
260
+
261
+ lines = ["Screenshots:"]
262
+ for i, s in enumerate(screenshots, 1):
263
+ lines.append(f"{i}. {s['filename']}")
264
+
265
+ return [TextContent(type="text", text="\n".join(lines))]
266
+
267
+ elif name == "get_tasks":
268
+ pending_only = arguments.get("pending_only", False)
269
+ tasks = get_pending_tasks() if pending_only else get_tasks()
270
+
271
+ if not tasks:
272
+ return [TextContent(type="text", text="No tasks found.")]
273
+
274
+ lines = ["Tasks:"]
275
+ for i, task in enumerate(tasks, 1):
276
+ status = "[x]" if task["completed"] else "[ ]"
277
+ lines.append(f"{i}. {status} {task['text']}")
278
+
279
+ return [TextContent(type="text", text="\n".join(lines))]
280
+
281
+ elif name == "complete_task":
282
+ task_text = arguments.get("task_text", "")
283
+ if mark_task_complete(task_text):
284
+ return [TextContent(type="text", text=f"Completed: {task_text}")]
285
+ return [TextContent(type="text", text=f"Task not found: {task_text}")]
286
+
287
+ elif name == "read_screenshot":
288
+ path = arguments.get("path", "")
289
+ if not path or not os.path.exists(path):
290
+ return [TextContent(type="text", text=f"Not found: {path}")]
291
+
292
+ with open(path, "rb") as f:
293
+ image_data = base64.standard_b64encode(f.read()).decode("utf-8")
294
+
295
+ return [
296
+ TextContent(type="text", text=f"Screenshot: {path}"),
297
+ ImageContent(type="image", data=image_data, mimeType="image/png"),
298
+ ]
299
+
300
+ return [TextContent(type="text", text=f"Unknown tool: {name}")]
301
+
302
+ return server
303
+
304
+
305
+ async def run_server():
306
+ """Run the MCP server."""
307
+ server = create_server()
308
+ async with stdio_server() as (read_stream, write_stream):
309
+ await server.run(read_stream, write_stream, server.create_initialization_options())
310
+
311
+
312
+ def main():
313
+ """Main entry point."""
314
+ asyncio.run(run_server())
315
+
316
+
317
+ if __name__ == "__main__":
318
+ main()
@@ -0,0 +1,129 @@
1
+ """Task tracker for managing tasks.md file."""
2
+
3
+ import os
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ from .config import config
8
+
9
+
10
+ def add_task(instruction: str, screenshot_path: Optional[str] = None) -> str:
11
+ """
12
+ Add a task to the tasks.md file.
13
+
14
+ Args:
15
+ instruction: Task instruction text
16
+ screenshot_path: Optional path to related screenshot
17
+
18
+ Returns:
19
+ The formatted task entry that was added
20
+ """
21
+ tasks_path = Path(config.tasks_file)
22
+
23
+ # Create parent directory if needed
24
+ tasks_path.parent.mkdir(parents=True, exist_ok=True)
25
+
26
+ # Create file with header if it doesn't exist
27
+ if not tasks_path.exists():
28
+ with open(tasks_path, "w") as f:
29
+ f.write("# Tasks for Claude Code\n\n")
30
+
31
+ # Format the task entry
32
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
33
+
34
+ # Build task lines
35
+ task_lines = [f"- [ ] {instruction.strip()}"]
36
+
37
+ # Add screenshot reference if provided
38
+ if screenshot_path:
39
+ task_lines.append(f" - Screenshot: `{screenshot_path}`")
40
+
41
+ task_lines.append(f" - Added: {timestamp}")
42
+ task_lines.append("") # Blank line after entry
43
+
44
+ task_entry = "\n".join(task_lines) + "\n"
45
+
46
+ # Append to file
47
+ with open(tasks_path, "a") as f:
48
+ f.write(task_entry)
49
+
50
+ return task_entry
51
+
52
+
53
+ def get_tasks() -> list[dict]:
54
+ """
55
+ Read all tasks from tasks.md.
56
+
57
+ Returns:
58
+ List of task dictionaries with keys: text, completed, screenshot, added
59
+ """
60
+ tasks_path = Path(config.tasks_file)
61
+
62
+ if not tasks_path.exists():
63
+ return []
64
+
65
+ tasks = []
66
+ current_task = None
67
+
68
+ with open(tasks_path, "r") as f:
69
+ for line in f:
70
+ # Check for task line
71
+ if line.startswith("- [ ] ") or line.startswith("- [x] "):
72
+ if current_task:
73
+ tasks.append(current_task)
74
+ completed = line.startswith("- [x] ")
75
+ current_task = {
76
+ "text": line[6:].strip(),
77
+ "completed": completed,
78
+ "screenshot": None,
79
+ "added": None,
80
+ }
81
+ elif current_task and line.strip().startswith("- Screenshot:"):
82
+ # Extract screenshot path
83
+ path = line.strip()[14:].strip().strip("`")
84
+ current_task["screenshot"] = path
85
+ elif current_task and line.strip().startswith("- Added:"):
86
+ # Extract timestamp
87
+ current_task["added"] = line.strip()[9:].strip()
88
+
89
+ # Don't forget the last task
90
+ if current_task:
91
+ tasks.append(current_task)
92
+
93
+ return tasks
94
+
95
+
96
+ def get_pending_tasks() -> list[dict]:
97
+ """Get only incomplete tasks."""
98
+ return [t for t in get_tasks() if not t["completed"]]
99
+
100
+
101
+ def mark_task_complete(task_text: str) -> bool:
102
+ """
103
+ Mark a task as complete by its text.
104
+
105
+ Args:
106
+ task_text: The task text to find and mark complete
107
+
108
+ Returns:
109
+ True if task was found and marked, False otherwise
110
+ """
111
+ tasks_path = Path(config.tasks_file)
112
+
113
+ if not tasks_path.exists():
114
+ return False
115
+
116
+ with open(tasks_path, "r") as f:
117
+ content = f.read()
118
+
119
+ # Find and replace the checkbox
120
+ search = f"- [ ] {task_text}"
121
+ replace = f"- [x] {task_text}"
122
+
123
+ if search in content:
124
+ content = content.replace(search, replace, 1)
125
+ with open(tasks_path, "w") as f:
126
+ f.write(content)
127
+ return True
128
+
129
+ return False
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: screenfix
3
+ Version: 0.1.0
4
+ Summary: Screenshot capture tool for Claude Code via MCP
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: mcp>=1.0.0
8
+ Requires-Dist: pyobjc-core>=10.0
9
+ Requires-Dist: pyobjc-framework-Cocoa>=10.0
10
+ Requires-Dist: pyobjc-framework-Quartz>=10.0
11
+ Requires-Dist: Pillow>=10.0.0
@@ -0,0 +1,14 @@
1
+ pyproject.toml
2
+ screenfix/__init__.py
3
+ screenfix/annotation_window.py
4
+ screenfix/clipboard_watcher.py
5
+ screenfix/config.py
6
+ screenfix/daemon.py
7
+ screenfix/mcp_server.py
8
+ screenfix/task_tracker.py
9
+ screenfix.egg-info/PKG-INFO
10
+ screenfix.egg-info/SOURCES.txt
11
+ screenfix.egg-info/dependency_links.txt
12
+ screenfix.egg-info/entry_points.txt
13
+ screenfix.egg-info/requires.txt
14
+ screenfix.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ screenfix-daemon = screenfix.daemon:main
3
+ screenfix-mcp = screenfix.mcp_server:main
@@ -0,0 +1,5 @@
1
+ mcp>=1.0.0
2
+ pyobjc-core>=10.0
3
+ pyobjc-framework-Cocoa>=10.0
4
+ pyobjc-framework-Quartz>=10.0
5
+ Pillow>=10.0.0
@@ -0,0 +1 @@
1
+ screenfix
@@ -0,0 +1,6 @@
1
+ ---
2
+ allowed-tools: mcp__screenfix__get_tasks, mcp__screenfix__complete_task
3
+ description: View and manage ScreenFix tasks
4
+ ---
5
+
6
+ Get all tasks using `mcp__screenfix__get_tasks`. Show pending tasks and their screenshots.
@@ -0,0 +1,8 @@
1
+ ---
2
+ allowed-tools: mcp__screenfix__start_daemon, mcp__screenfix__get_status
3
+ description: Start the ScreenFix daemon
4
+ ---
5
+
6
+ Start the ScreenFix daemon using `mcp__screenfix__start_daemon`. After starting, check the status.
7
+
8
+ Once running, use **Cmd+Ctrl+Shift+4** to capture screenshots instantly (no delay).
@@ -0,0 +1,6 @@
1
+ ---
2
+ allowed-tools: mcp__screenfix__get_status
3
+ description: Check ScreenFix status
4
+ ---
5
+
6
+ Check the current status of ScreenFix using `mcp__screenfix__get_status`.
@@ -0,0 +1,6 @@
1
+ ---
2
+ allowed-tools: mcp__screenfix__stop_daemon
3
+ description: Stop the ScreenFix daemon
4
+ ---
5
+
6
+ Stop the running ScreenFix daemon using `mcp__screenfix__stop_daemon`.
@@ -0,0 +1,14 @@
1
+ ---
2
+ allowed-tools: mcp__screenfix__get_tasks, mcp__screenfix__get_last_screenshot, mcp__screenfix__read_screenshot, mcp__screenfix__complete_task, Read, Edit, Write, Bash, Glob, Grep
3
+ description: Execute the next pending ScreenFix task
4
+ ---
5
+
6
+ Get all pending tasks using `mcp__screenfix__get_tasks` with `pending_only: true`.
7
+
8
+ Take only the FIRST pending task:
9
+ 1. Read the associated screenshot using `mcp__screenfix__read_screenshot`
10
+ 2. Analyze the screenshot and the task instructions
11
+ 3. Implement the fix described in the instructions
12
+ 4. Mark the task complete using `mcp__screenfix__complete_task`
13
+
14
+ Execute one task at a time.
@@ -0,0 +1,14 @@
1
+ ---
2
+ allowed-tools: mcp__screenfix__get_tasks, mcp__screenfix__get_last_screenshot, mcp__screenfix__read_screenshot, mcp__screenfix__complete_task, Read, Edit, Write, Bash, Glob, Grep
3
+ description: Execute all pending ScreenFix tasks automatically
4
+ ---
5
+
6
+ Get all pending tasks using `mcp__screenfix__get_tasks` with `pending_only: true`.
7
+
8
+ For each pending task:
9
+ 1. Read the associated screenshot using `mcp__screenfix__read_screenshot`
10
+ 2. Analyze the screenshot and the task instructions
11
+ 3. Implement the fix described in the instructions
12
+ 4. Mark the task complete using `mcp__screenfix__complete_task`
13
+
14
+ Execute all tasks without asking for confirmation. Just do it.