debtpilot 0.1.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.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: debtpilot
3
+ Version: 0.1.0
4
+ Summary: Track technical debt before it blows up in production
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/YOUR_GITHUB_USERNAME/debtpilot
7
+ Project-URL: Issues, https://github.com/YOUR_GITHUB_USERNAME/debtpilot/issues
8
+ Keywords: technical-debt,code-quality,developer-tools,cli,linting
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Quality Assurance
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: rich>=13.0
22
+ Provides-Extra: daemon
23
+ Requires-Dist: websockets>=12.0; extra == "daemon"
24
+ Requires-Dist: watchdog>=4.0; extra == "daemon"
25
+ Provides-Extra: notify
26
+ Requires-Dist: win10toast>=0.9; platform_system == "Windows" and extra == "notify"
27
+ Requires-Dist: plyer>=2.1; extra == "notify"
28
+ Provides-Extra: full
29
+ Requires-Dist: debtpilot[daemon,notify]; extra == "full"
30
+
31
+ # DebtPilot
32
+
33
+ > Track technical debt before it blows up in production.
34
+
35
+ Every IDE catches syntax errors. DebtPilot does something no IDE can —
36
+ it knows that `payments/stripe.js` has been edited 30 times by 4 different
37
+ developers, has a TODO from 8 months ago, and a complexity score of 24.
38
+ That combination is your next production incident.
39
+
40
+ ---
41
+
42
+ ## Install
43
+
44
+ pip install debtpilot
45
+
46
+ Full install (file watcher + live notifications):
47
+
48
+ pip install "debtpilot[full]"
49
+
50
+ ---
51
+
52
+ ## Quickstart — 4 commands, done
53
+
54
+ 1. Scan your project
55
+ debtpilot scan .
56
+
57
+ 2. See the report
58
+ debtpilot report
59
+
60
+ 3. Block bad commits automatically
61
+ debtpilot hook --install
62
+
63
+ 4. Start live background watching
64
+ debtpilot start --dir .
65
+
66
+ ---
67
+
68
+ ## What it does
69
+
70
+ | Command | What happens |
71
+ |---|---|
72
+ | `debtpilot scan .` | Scores every file 0-100 for technical debt |
73
+ | `debtpilot report` | Shows top debt files in a ranked table |
74
+ | `debtpilot file <path>` | Deep breakdown of one file |
75
+ | `debtpilot ai <path>` | AI explains exactly what to fix (needs Gemini key) |
76
+ | `debtpilot predict` | Predicts which file will cause your next bug |
77
+ | `debtpilot dashboard` | Generates an HTML dashboard — open in browser |
78
+ | `debtpilot report-pdf` | Generates a print-ready PDF report |
79
+ | `debtpilot alerts` | Shows active danger alerts |
80
+ | `debtpilot todos` | Lists stale TODOs by age |
81
+ | `debtpilot hook --install` | Blocks commits above the debt threshold |
82
+ | `debtpilot start` | Starts live file watcher + WebSocket server |
83
+ | `debtpilot status` | Full health check of your DebtPilot setup |
84
+ | `debtpilot digest --send` | Sends weekly email summary to your team |
85
+
86
+ ---
87
+
88
+ ## Debt Score (0-100)
89
+
90
+ | Grade | Score | Meaning |
91
+ |-------|-------|---------|
92
+ | A | 0-25 | Healthy |
93
+ | B | 26-45 | Watch |
94
+ | C | 46-65 | Concerning |
95
+ | D | 66-80 | Dangerous |
96
+ | F | 81-100 | Critical — fix before next deploy |
97
+
98
+ Score = weighted combination of: git churn (30%) + complexity (25%) +
99
+ lint issues (20%) + stale TODOs (15%) + staleness (10%).
100
+
101
+ ---
102
+
103
+ ## AI Fix Suggestions (optional)
104
+
105
+ Get a free Gemini API key at aistudio.google.com then:
106
+
107
+ debtpilot config --gemini-key YOUR_KEY
108
+ debtpilot ai src/payments.py
109
+
110
+ DebtPilot sends the file context to Gemini and gets back exact fix
111
+ suggestions with code examples and effort estimates.
112
+
113
+ ---
114
+
115
+ ## Weekly Email Digest
116
+
117
+ debtpilot config --gmail you@gmail.com --gmail-password YOUR_APP_PASS --recipients team@company.com
118
+ debtpilot digest --send
119
+
120
+ Use a Gmail App Password from: myaccount.google.com/apppasswords
121
+
122
+ ---
123
+
124
+ ## Auto-start on Windows login
125
+
126
+ debtpilot autostart --install --dir .
127
+
128
+ DebtPilot will start silently in the background every time Windows starts.
129
+ No manual debtpilot start needed.
130
+
131
+ ---
132
+
133
+ ## Requirements
134
+
135
+ - Python 3.10 or higher
136
+ - git in PATH
137
+ - pylint / mypy / eslint — optional, for richer lint scores
138
+
139
+ ---
140
+
141
+ ## License
142
+
143
+ MIT
@@ -0,0 +1,20 @@
1
+ debtpilot_core/__init__.py,sha256=7MH_x0z0JSLcdSAxEfT2-UMCNUfGl5Hm3jFbmtTTcpw,129
2
+ debtpilot_core/ai_advisor.py,sha256=MBrmwIXDY4iclAXQyK-tSu2YL3ygUa0KT3qL6GRzO5E,6813
3
+ debtpilot_core/autostart.py,sha256=06X-waesS_7GM1HoHkW_7XGHJ_M3ic1sF0K0M--D-18,8934
4
+ debtpilot_core/cli.py,sha256=4UjYThoFz1PH1-Y2hR2zgh5Sji88LRzfWRXR2pTIpyU,26874
5
+ debtpilot_core/daemon.py,sha256=T40hRan2ONQKQKEYslDoNGFyK010wO8gPXNt-2deIDM,9300
6
+ debtpilot_core/dashboard.py,sha256=j9m2fGWRSF6bPhe4KGVZSQR9KQh99AiPkIp6rhoQu5o,11944
7
+ debtpilot_core/database.py,sha256=jWAH9p2TtkcYGphK24ra2J72hiZaQT0RO0fcNnpAl6Q,9322
8
+ debtpilot_core/digest.py,sha256=B_CIQf-CyOgBhKV_RDg4nt8pJgwBfe8KJzRFTt4FDo8,9865
9
+ debtpilot_core/hooks.py,sha256=2Fcro5AopZWMKFno11MJfgrWHyWRJ6Gs1YX9E2vY-I8,5169
10
+ debtpilot_core/linter.py,sha256=SntpTxopFEmnXNGU4Ktkdcrr8n7Ey5XbXA2Jf421EwI,4871
11
+ debtpilot_core/notifier.py,sha256=CgmeKxa3ZVW9H0rD22wGn_e_4--j7vVLG0vdhv7_qKs,7979
12
+ debtpilot_core/predictor.py,sha256=Eo5bEmQgi0sKVj4rKPC4XcRGYLAfNlsgM2Ql6aB2mr4,7828
13
+ debtpilot_core/reporter.py,sha256=_nFYq6jdYdKeDpnzyHxbndcXjjXjfQwoRgQGuPf5fEE,8590
14
+ debtpilot_core/scanner.py,sha256=qoXYw9xrk8NDDAWDi2xPJebjFEUu1Zt1AO3MMNwQIko,6541
15
+ debtpilot_core/scorer.py,sha256=7heZFLW-u-P4S00obpPjy9zw_erDY2ZjnBIS9ZExoXo,4321
16
+ debtpilot-0.1.0.dist-info/METADATA,sha256=lrjRqwz1iVsFmhu4MYlo7Y5cXk_6Wbvabi0OSIlSDhM,4240
17
+ debtpilot-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
18
+ debtpilot-0.1.0.dist-info/entry_points.txt,sha256=aVse1t0vJT44p9SxUH8IlTqTi66qQQvvT-jVtld1d1M,54
19
+ debtpilot-0.1.0.dist-info/top_level.txt,sha256=sJNOnUKGzPh8dq_N45_RGTlrPafldV3W1Aw8IMInsgU,15
20
+ debtpilot-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ debtpilot = debtpilot_core.cli:main
@@ -0,0 +1 @@
1
+ debtpilot_core
@@ -0,0 +1,6 @@
1
+ """
2
+ DebtPilot — Track technical debt before it blows up in production.
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+ __author__ = "DebtPilot"
@@ -0,0 +1,206 @@
1
+ """
2
+ DebtPilot - AI Fix Suggestions using Google Gemini (free API).
3
+ Sends risky code to Gemini and gets back exact fix suggestions.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import urllib.request
9
+ import urllib.error
10
+ from pathlib import Path
11
+ from typing import Dict, List, Optional
12
+
13
+ GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"
14
+
15
+
16
+ def get_api_key() -> Optional[str]:
17
+ # First check environment variable
18
+ key = os.environ.get("DEBTPILOT_GEMINI_KEY")
19
+ if key:
20
+ return key
21
+ # Then check config file
22
+ config_path = Path.home() / ".debtpilot" / "config.json"
23
+ if config_path.exists():
24
+ try:
25
+ data = json.loads(config_path.read_text())
26
+ return data.get("gemini_api_key")
27
+ except Exception:
28
+ pass
29
+ return None
30
+
31
+
32
+ def save_api_key(api_key: str) -> None:
33
+ config_dir = Path.home() / ".debtpilot"
34
+ config_dir.mkdir(parents=True, exist_ok=True)
35
+ config_path = config_dir / "config.json"
36
+ data = {}
37
+ if config_path.exists():
38
+ try:
39
+ data = json.loads(config_path.read_text())
40
+ except Exception:
41
+ pass
42
+ data["gemini_api_key"] = api_key
43
+ config_path.write_text(json.dumps(data, indent=2))
44
+
45
+
46
+ def _call_gemini(prompt: str, api_key: str) -> str:
47
+ payload = json.dumps({
48
+ "contents": [{"parts": [{"text": prompt}]}],
49
+ "generationConfig": {"temperature": 0.3, "maxOutputTokens": 1500}
50
+ }).encode()
51
+
52
+ req = urllib.request.Request(
53
+ f"{GEMINI_API_URL}?key={api_key}",
54
+ data=payload,
55
+ headers={"Content-Type": "application/json"},
56
+ method="POST",
57
+ )
58
+ try:
59
+ with urllib.request.urlopen(req, timeout=30) as r:
60
+ result = json.loads(r.read())
61
+ return result["candidates"][0]["content"]["parts"][0]["text"]
62
+ except urllib.error.HTTPError as e:
63
+ body = e.read().decode()
64
+ raise RuntimeError(f"Gemini API error {e.code}: {body}")
65
+ except Exception as e:
66
+ raise RuntimeError(f"Failed to call Gemini: {e}")
67
+
68
+
69
+ def analyze_file(
70
+ filepath: str,
71
+ score_data: Dict,
72
+ lint_issues: List[Dict],
73
+ todos: List[Dict],
74
+ complexity: Dict,
75
+ ) -> Dict:
76
+ """
77
+ Send file context to Gemini and get back structured fix suggestions.
78
+ Returns dict with summary, fixes, and priority actions.
79
+ """
80
+ api_key = get_api_key()
81
+ if not api_key:
82
+ return {
83
+ "error": "No Gemini API key configured. Run: debtpilot config --gemini-key YOUR_KEY",
84
+ "summary": "AI advisor not configured. The rest of DebtPilot works without it.",
85
+ "fixes": [],
86
+ "priority_actions": ["Run: debtpilot config --gemini-key YOUR_KEY to enable AI suggestions"],
87
+ "risk_level": "UNKNOWN",
88
+ "estimated_fix_time": "N/A",
89
+ "ai_available": False,
90
+ }
91
+
92
+ # Read file content (first 150 lines to stay within token limits)
93
+ try:
94
+ lines = Path(filepath).read_text(encoding="utf-8", errors="ignore").splitlines()
95
+ code_preview = "\n".join(lines[:150])
96
+ total_lines = len(lines)
97
+ except OSError:
98
+ code_preview = "(could not read file)"
99
+ total_lines = 0
100
+
101
+ # Build lint summary
102
+ errors = [i for i in lint_issues if i.get("severity") == "error"][:5]
103
+ warnings = [i for i in lint_issues if i.get("severity") == "warning"][:5]
104
+ lint_summary = "\n".join(
105
+ [f" ERROR L{i['line']}: {i['message']}" for i in errors] +
106
+ [f" WARNING L{i['line']}: {i['message']}" for i in warnings]
107
+ ) or " None detected"
108
+
109
+ # Build TODO summary
110
+ todo_summary = "\n".join(
111
+ [f" L{t['line']} ({t.get('age_days', 0)} days old): {t.get('text', '')}" for t in todos[:5]]
112
+ ) or " None"
113
+
114
+ # Build complexity summary
115
+ funcs = complexity.get("functions", [])[:5]
116
+ complexity_summary = "\n".join(
117
+ [f" {f['name']}() at L{f['line']}: complexity {f['complexity']}" for f in funcs]
118
+ ) or f" avg={complexity.get('avg_complexity', 0)}"
119
+
120
+ prompt = f"""You are a senior software engineer doing a technical debt review.
121
+
122
+ FILE: {filepath}
123
+ TOTAL LINES: {total_lines}
124
+ DEBT SCORE: {score_data.get('total', 0)}/100 (Grade {score_data.get('grade', '?')} — {score_data.get('label', '')})
125
+
126
+ SUB-SCORES:
127
+ Churn (how often edited by many people): {score_data.get('churn_score', 0)}/100
128
+ Complexity (how hard to understand): {score_data.get('complexity_score', 0)}/100
129
+ Lint issues: {score_data.get('lint_score', 0)}/100
130
+ Stale TODOs: {score_data.get('todo_score', 0)}/100
131
+ Staleness: {score_data.get('staleness_score', 0)}/100
132
+
133
+ LINT ISSUES:
134
+ {lint_summary}
135
+
136
+ STALE TODOs:
137
+ {todo_summary}
138
+
139
+ COMPLEX FUNCTIONS:
140
+ {complexity_summary}
141
+
142
+ CODE PREVIEW (first 150 lines):
143
+ {code_preview}
144
+
145
+ Respond ONLY with a valid JSON object in this exact format, no markdown, no explanation outside the JSON:
146
+ {{
147
+ "summary": "2-3 sentence plain English summary of the main problems",
148
+ "risk_level": "LOW or MEDIUM or HIGH or CRITICAL",
149
+ "fixes": [
150
+ {{
151
+ "title": "short title of the fix",
152
+ "problem": "what is wrong and why it matters",
153
+ "solution": "exactly what to do",
154
+ "code_example": "short before/after code snippet if relevant, else empty string",
155
+ "effort": "15min or 1hr or half-day or 1day"
156
+ }}
157
+ ],
158
+ "priority_actions": [
159
+ "action 1 — most important thing to do first",
160
+ "action 2",
161
+ "action 3"
162
+ ],
163
+ "estimated_fix_time": "total estimated time to resolve all issues"
164
+ }}"""
165
+
166
+ raw = _call_gemini(prompt, api_key)
167
+
168
+ # Strip markdown code fences if present
169
+ raw = raw.strip()
170
+ if raw.startswith("```"):
171
+ raw = raw.split("```")[1]
172
+ if raw.startswith("json"):
173
+ raw = raw[4:]
174
+ raw = raw.strip()
175
+
176
+ try:
177
+ return json.loads(raw)
178
+ except json.JSONDecodeError:
179
+ return {
180
+ "summary": raw[:500],
181
+ "risk_level": "UNKNOWN",
182
+ "fixes": [],
183
+ "priority_actions": [],
184
+ "estimated_fix_time": "unknown",
185
+ "parse_error": True,
186
+ }
187
+
188
+
189
+ def quick_explain(code_snippet: str, question: str) -> str:
190
+ """Ask Gemini a quick question about a code snippet."""
191
+ api_key = get_api_key()
192
+ if not api_key:
193
+ return "No Gemini API key configured. Run: debtpilot config --gemini-key YOUR_KEY"
194
+
195
+ prompt = f"""You are a helpful senior developer. Answer concisely.
196
+
197
+ Question: {question}
198
+
199
+ Code:{code_snippet[:1000]}
200
+
201
+ Give a direct, practical answer in 3-5 sentences maximum."""
202
+
203
+ try:
204
+ return _call_gemini(prompt, api_key)
205
+ except RuntimeError as e:
206
+ return f"AI error: {e}"
@@ -0,0 +1,255 @@
1
+ """
2
+ DebtPilot - Windows Auto-startup Manager.
3
+ Registers DebtPilot daemon with Windows Task Scheduler so it starts
4
+ automatically on login — no manual 'debtpilot start' ever needed again.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import json
10
+ import subprocess
11
+ from pathlib import Path
12
+ from typing import Dict, Optional
13
+
14
+
15
+ TASK_NAME = "DebtPilotDaemon"
16
+ CONFIG_PATH = Path.home() / ".debtpilot" / "config.json"
17
+
18
+
19
+ def _get_config() -> Dict:
20
+ if CONFIG_PATH.exists():
21
+ try:
22
+ return json.loads(CONFIG_PATH.read_text())
23
+ except Exception:
24
+ pass
25
+ return {}
26
+
27
+
28
+ def _save_config(data: Dict) -> None:
29
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
30
+ existing = _get_config()
31
+ existing.update(data)
32
+ CONFIG_PATH.write_text(json.dumps(existing, indent=2))
33
+
34
+
35
+ def _get_python_path() -> str:
36
+ return sys.executable
37
+
38
+
39
+ def _get_debtpilot_path() -> Optional[str]:
40
+ """Find the debtpilot CLI script path."""
41
+ import shutil
42
+ path = shutil.which("debtpilot")
43
+ if path:
44
+ return path
45
+ # Fallback: construct from Python path
46
+ scripts = Path(sys.executable).parent / "Scripts" / "debtpilot.exe"
47
+ if scripts.exists():
48
+ return str(scripts)
49
+ return None
50
+
51
+
52
+ def _get_project_dir() -> str:
53
+ config = _get_config()
54
+ return config.get("watched_project", str(Path.home()))
55
+
56
+
57
+ # ── Windows Task Scheduler ───────────────────────────────────────────────────
58
+
59
+ def _create_task_xml(python_path: str, project_dir: str, port: int) -> str:
60
+ """Generate the XML for Windows Task Scheduler."""
61
+ debtpilot = _get_debtpilot_path() or f"{Path(python_path).parent}\\Scripts\\debtpilot.exe"
62
+
63
+ return f"""<?xml version="1.0" encoding="UTF-16"?>
64
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
65
+ <RegistrationInfo>
66
+ <Description>DebtPilot — Live technical debt monitoring daemon</Description>
67
+ <Author>DebtPilot</Author>
68
+ </RegistrationInfo>
69
+ <Triggers>
70
+ <LogonTrigger>
71
+ <Enabled>true</Enabled>
72
+ </LogonTrigger>
73
+ </Triggers>
74
+ <Principals>
75
+ <Principal id="Author">
76
+ <LogonType>InteractiveToken</LogonType>
77
+ <RunLevel>LeastPrivilege</RunLevel>
78
+ </Principal>
79
+ </Principals>
80
+ <Settings>
81
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
82
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
83
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
84
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
85
+ <Priority>7</Priority>
86
+ </Settings>
87
+ <Actions Context="Author">
88
+ <Exec>
89
+ <Command>{debtpilot}</Command>
90
+ <Arguments>start --dir "{project_dir}" --port {port}</Arguments>
91
+ <WorkingDirectory>{project_dir}</WorkingDirectory>
92
+ </Exec>
93
+ </Actions>
94
+ </Task>"""
95
+
96
+
97
+ def install_autostart(project_dir: Optional[str] = None, port: int = 57017) -> Dict:
98
+ """
99
+ Register DebtPilot with Windows Task Scheduler.
100
+ Runs automatically on every login.
101
+ """
102
+ if sys.platform != "win32":
103
+ return {
104
+ "success": False,
105
+ "error": "Autostart is currently Windows-only. On Mac/Linux, add 'debtpilot start' to your shell profile."
106
+ }
107
+
108
+ project_dir = project_dir or str(Path.cwd())
109
+ python_path = _get_python_path()
110
+
111
+ # Save project dir to config
112
+ _save_config({"watched_project": project_dir, "daemon_port": port})
113
+
114
+ # Write the task XML to a temp file
115
+ xml_content = _create_task_xml(python_path, project_dir, port)
116
+ xml_path = Path.home() / ".debtpilot" / "task.xml"
117
+ xml_path.parent.mkdir(parents=True, exist_ok=True)
118
+
119
+ try:
120
+ xml_path.write_text(xml_content, encoding="utf-16")
121
+ except Exception as e:
122
+ return {"success": False, "error": f"Could not write task file: {e}"}
123
+
124
+ # Register with schtasks
125
+ try:
126
+ result = subprocess.run(
127
+ ["schtasks", "/Create", "/TN", TASK_NAME,
128
+ "/XML", str(xml_path), "/F"],
129
+ capture_output=True, text=True, timeout=30
130
+ )
131
+ if result.returncode == 0:
132
+ _save_config({"autostart": True})
133
+ return {
134
+ "success": True,
135
+ "message": (
136
+ f"✅ DebtPilot will now start automatically on every login.\n"
137
+ f" Watching: {project_dir}\n"
138
+ f" Port: {port}\n"
139
+ f" Task name: {TASK_NAME}"
140
+ ),
141
+ "project_dir": project_dir,
142
+ "port": port,
143
+ }
144
+ else:
145
+ # Try running as admin via PowerShell elevation
146
+ return _install_with_elevation(xml_path, project_dir, port)
147
+ except FileNotFoundError:
148
+ return {"success": False, "error": "schtasks not found. Are you on Windows?"}
149
+ except Exception as e:
150
+ return {"success": False, "error": str(e)}
151
+
152
+
153
+ def _install_with_elevation(xml_path: Path, project_dir: str, port: int) -> Dict:
154
+ """Try to install task with admin elevation via PowerShell."""
155
+ try:
156
+ ps_command = (
157
+ f'Start-Process schtasks -ArgumentList '
158
+ f'"/Create /TN {TASK_NAME} /XML `"{xml_path}`" /F" '
159
+ f'-Verb RunAs -Wait'
160
+ )
161
+ result = subprocess.run(
162
+ ["powershell", "-Command", ps_command],
163
+ capture_output=True, text=True, timeout=30
164
+ )
165
+ if result.returncode == 0:
166
+ _save_config({"autostart": True})
167
+ return {
168
+ "success": True,
169
+ "message": (
170
+ f"✅ Autostart installed with admin rights.\n"
171
+ f" Watching: {project_dir}"
172
+ ),
173
+ }
174
+ else:
175
+ return {
176
+ "success": False,
177
+ "error": (
178
+ "Could not register with Task Scheduler.\n"
179
+ "Try running your terminal as Administrator and retry:\n"
180
+ " debtpilot autostart --install"
181
+ )
182
+ }
183
+ except Exception as e:
184
+ return {"success": False, "error": str(e)}
185
+
186
+
187
+ def uninstall_autostart() -> Dict:
188
+ """Remove DebtPilot from Windows Task Scheduler."""
189
+ if sys.platform != "win32":
190
+ return {"success": False, "error": "Not on Windows"}
191
+
192
+ try:
193
+ result = subprocess.run(
194
+ ["schtasks", "/Delete", "/TN", TASK_NAME, "/F"],
195
+ capture_output=True, text=True, timeout=15
196
+ )
197
+ if result.returncode == 0:
198
+ _save_config({"autostart": False})
199
+ return {"success": True, "message": "✅ DebtPilot autostart removed."}
200
+ else:
201
+ return {"success": False, "error": "Task not found or could not be removed."}
202
+ except Exception as e:
203
+ return {"success": False, "error": str(e)}
204
+
205
+
206
+ def autostart_status() -> Dict:
207
+ """Check if autostart is registered."""
208
+ if sys.platform != "win32":
209
+ return {"installed": False, "platform": sys.platform}
210
+
211
+ try:
212
+ result = subprocess.run(
213
+ ["schtasks", "/Query", "/TN", TASK_NAME, "/FO", "LIST"],
214
+ capture_output=True, text=True, timeout=10
215
+ )
216
+ if result.returncode == 0:
217
+ config = _get_config()
218
+ # Parse next run time from output
219
+ next_run = ""
220
+ for line in result.stdout.splitlines():
221
+ if "Next Run Time" in line:
222
+ next_run = line.split(":", 1)[-1].strip()
223
+ return {
224
+ "installed": True,
225
+ "task_name": TASK_NAME,
226
+ "project_dir": config.get("watched_project", "unknown"),
227
+ "port": config.get("daemon_port", 57017),
228
+ "next_run": next_run,
229
+ }
230
+ else:
231
+ return {"installed": False}
232
+ except Exception as e:
233
+ return {"installed": False, "error": str(e)}
234
+
235
+
236
+ def run_now(project_dir: Optional[str] = None, port: int = 57017) -> Dict:
237
+ """Start the task immediately via Task Scheduler."""
238
+ if sys.platform != "win32":
239
+ return {"success": False, "error": "Not on Windows"}
240
+ try:
241
+ result = subprocess.run(
242
+ ["schtasks", "/Run", "/TN", TASK_NAME],
243
+ capture_output=True, text=True, timeout=10
244
+ )
245
+ return {
246
+ "success": result.returncode == 0,
247
+ "message": "Daemon started." if result.returncode == 0 else result.stderr
248
+ }
249
+ except Exception as e:
250
+ return {"success": False, "error": str(e)}
251
+
252
+
253
+ def set_watched_project(project_dir: str) -> None:
254
+ """Update which project directory the daemon watches."""
255
+ _save_config({"watched_project": str(Path(project_dir).resolve())})