luckyd-code 1.2.2__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 (127) hide show
  1. luckyd_code/__init__.py +54 -0
  2. luckyd_code/__main__.py +5 -0
  3. luckyd_code/_agent_loop.py +551 -0
  4. luckyd_code/_data_dir.py +73 -0
  5. luckyd_code/agent.py +38 -0
  6. luckyd_code/analytics/__init__.py +18 -0
  7. luckyd_code/analytics/reporter.py +195 -0
  8. luckyd_code/analytics/scanner.py +443 -0
  9. luckyd_code/analytics/smells.py +316 -0
  10. luckyd_code/analytics/trends.py +303 -0
  11. luckyd_code/api.py +473 -0
  12. luckyd_code/audit_daemon.py +845 -0
  13. luckyd_code/autonomous_fixer.py +473 -0
  14. luckyd_code/background.py +159 -0
  15. luckyd_code/backup.py +237 -0
  16. luckyd_code/brain/__init__.py +84 -0
  17. luckyd_code/brain/assembler.py +100 -0
  18. luckyd_code/brain/chunker.py +345 -0
  19. luckyd_code/brain/constants.py +73 -0
  20. luckyd_code/brain/embedder.py +163 -0
  21. luckyd_code/brain/graph.py +311 -0
  22. luckyd_code/brain/indexer.py +316 -0
  23. luckyd_code/brain/parser.py +140 -0
  24. luckyd_code/brain/retriever.py +234 -0
  25. luckyd_code/cli.py +894 -0
  26. luckyd_code/cli_commands/__init__.py +1 -0
  27. luckyd_code/cli_commands/audit.py +120 -0
  28. luckyd_code/cli_commands/background.py +83 -0
  29. luckyd_code/cli_commands/brain.py +87 -0
  30. luckyd_code/cli_commands/config.py +75 -0
  31. luckyd_code/cli_commands/dispatcher.py +695 -0
  32. luckyd_code/cli_commands/sessions.py +41 -0
  33. luckyd_code/cli_entry.py +147 -0
  34. luckyd_code/cli_utils.py +112 -0
  35. luckyd_code/config.py +205 -0
  36. luckyd_code/context.py +214 -0
  37. luckyd_code/cost_tracker.py +209 -0
  38. luckyd_code/error_reporter.py +508 -0
  39. luckyd_code/exceptions.py +39 -0
  40. luckyd_code/export.py +126 -0
  41. luckyd_code/feedback_analyzer.py +290 -0
  42. luckyd_code/file_watcher.py +258 -0
  43. luckyd_code/git/__init__.py +11 -0
  44. luckyd_code/git/auto_commit.py +157 -0
  45. luckyd_code/git/tools.py +85 -0
  46. luckyd_code/hooks.py +236 -0
  47. luckyd_code/indexer.py +280 -0
  48. luckyd_code/init.py +39 -0
  49. luckyd_code/keybindings.py +77 -0
  50. luckyd_code/log.py +55 -0
  51. luckyd_code/mcp/__init__.py +6 -0
  52. luckyd_code/mcp/client.py +184 -0
  53. luckyd_code/memory/__init__.py +19 -0
  54. luckyd_code/memory/manager.py +339 -0
  55. luckyd_code/metrics/__init__.py +5 -0
  56. luckyd_code/model_registry.py +131 -0
  57. luckyd_code/orchestrator.py +204 -0
  58. luckyd_code/permissions/__init__.py +1 -0
  59. luckyd_code/permissions/manager.py +103 -0
  60. luckyd_code/planner.py +361 -0
  61. luckyd_code/plugins.py +91 -0
  62. luckyd_code/py.typed +0 -0
  63. luckyd_code/retry.py +57 -0
  64. luckyd_code/router.py +417 -0
  65. luckyd_code/sandbox.py +156 -0
  66. luckyd_code/self_critique.py +2 -0
  67. luckyd_code/self_improve.py +274 -0
  68. luckyd_code/sessions.py +114 -0
  69. luckyd_code/settings.py +72 -0
  70. luckyd_code/skills/__init__.py +8 -0
  71. luckyd_code/skills/review.py +22 -0
  72. luckyd_code/skills/security.py +17 -0
  73. luckyd_code/tasks/__init__.py +1 -0
  74. luckyd_code/tasks/manager.py +102 -0
  75. luckyd_code/templates/icon-192.png +0 -0
  76. luckyd_code/templates/icon-512.png +0 -0
  77. luckyd_code/templates/index.html +1965 -0
  78. luckyd_code/templates/manifest.json +14 -0
  79. luckyd_code/templates/src/app.js +694 -0
  80. luckyd_code/templates/src/body.html +767 -0
  81. luckyd_code/templates/src/cdn.txt +2 -0
  82. luckyd_code/templates/src/style.css +474 -0
  83. luckyd_code/templates/sw.js +31 -0
  84. luckyd_code/templates/test.html +6 -0
  85. luckyd_code/themes.py +48 -0
  86. luckyd_code/tools/__init__.py +97 -0
  87. luckyd_code/tools/agent_tools.py +65 -0
  88. luckyd_code/tools/bash.py +360 -0
  89. luckyd_code/tools/brain_tools.py +137 -0
  90. luckyd_code/tools/browser.py +369 -0
  91. luckyd_code/tools/datetime_tool.py +34 -0
  92. luckyd_code/tools/dockerfile_gen.py +212 -0
  93. luckyd_code/tools/file_ops.py +381 -0
  94. luckyd_code/tools/game_gen.py +360 -0
  95. luckyd_code/tools/git_tools.py +130 -0
  96. luckyd_code/tools/git_worktree.py +63 -0
  97. luckyd_code/tools/path_validate.py +64 -0
  98. luckyd_code/tools/project_gen.py +187 -0
  99. luckyd_code/tools/readme_gen.py +227 -0
  100. luckyd_code/tools/registry.py +157 -0
  101. luckyd_code/tools/shell_detect.py +109 -0
  102. luckyd_code/tools/web.py +89 -0
  103. luckyd_code/tools/youtube.py +187 -0
  104. luckyd_code/tools_bridge.py +144 -0
  105. luckyd_code/undo.py +126 -0
  106. luckyd_code/update.py +60 -0
  107. luckyd_code/verify.py +360 -0
  108. luckyd_code/web_app.py +176 -0
  109. luckyd_code/web_routes/__init__.py +23 -0
  110. luckyd_code/web_routes/background.py +73 -0
  111. luckyd_code/web_routes/brain.py +109 -0
  112. luckyd_code/web_routes/cost.py +12 -0
  113. luckyd_code/web_routes/files.py +133 -0
  114. luckyd_code/web_routes/memories.py +94 -0
  115. luckyd_code/web_routes/misc.py +67 -0
  116. luckyd_code/web_routes/project.py +48 -0
  117. luckyd_code/web_routes/review.py +20 -0
  118. luckyd_code/web_routes/sessions.py +44 -0
  119. luckyd_code/web_routes/settings.py +43 -0
  120. luckyd_code/web_routes/static.py +70 -0
  121. luckyd_code/web_routes/update.py +19 -0
  122. luckyd_code/web_routes/ws.py +237 -0
  123. luckyd_code-1.2.2.dist-info/METADATA +297 -0
  124. luckyd_code-1.2.2.dist-info/RECORD +127 -0
  125. luckyd_code-1.2.2.dist-info/WHEEL +4 -0
  126. luckyd_code-1.2.2.dist-info/entry_points.txt +3 -0
  127. luckyd_code-1.2.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,109 @@
1
+ """Knowledge graph / brain routes."""
2
+
3
+ from fastapi import APIRouter, Request
4
+ from fastapi.responses import JSONResponse
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ @router.get("/api/brain")
10
+ async def brain_status(request: Request):
11
+ from ..brain import KnowledgeGraph, Retriever, VectorIndexer
12
+ brain = KnowledgeGraph()
13
+ brain.load()
14
+
15
+ rag_available = False
16
+ try:
17
+ idx = VectorIndexer()
18
+ rag_available = idx.load()
19
+ except Exception:
20
+ pass
21
+
22
+ if not brain.nodes and not rag_available:
23
+ return {"status": "empty", "message": "Knowledge graph is empty. Use /api/brain/rebuild to index your codebase."}
24
+
25
+ result = {
26
+ "symbols": brain.stats.get("node_count", 0),
27
+ "relations": brain.stats.get("edge_count", 0),
28
+ "files_parsed": brain.stats.get("files_parsed", 0),
29
+ }
30
+ if brain.stats.get("last_built"):
31
+ import time
32
+ result["last_built"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(brain.stats["last_built"]))
33
+
34
+ if rag_available:
35
+ try:
36
+ r = Retriever()
37
+ info = r.stats()
38
+ vec = info.get("vector", {})
39
+ result["rag_chunks"] = vec.get("chunks", 0)
40
+ result["rag_files"] = vec.get("files", 0)
41
+ except Exception:
42
+ pass
43
+
44
+ return result
45
+
46
+
47
+ @router.post("/api/brain/rebuild")
48
+ async def brain_rebuild(request: Request):
49
+ from ..brain import rebuild_project
50
+ import os
51
+ result = rebuild_project(os.getcwd())
52
+
53
+ state = request.app.state.web_state
54
+ if state.knowledge_graph:
55
+ state.knowledge_graph.load()
56
+
57
+ return {
58
+ "status": "ok",
59
+ "chunks": result.get("chunks", 0),
60
+ "files": result.get("files", 0),
61
+ "symbols": result.get("node_count", 0),
62
+ "files_parsed": result.get("files_parsed", 0),
63
+ }
64
+
65
+
66
+ @router.get("/api/brain/search")
67
+ async def brain_search(request: Request, q: str = "", max_results: int = 5):
68
+ if not q:
69
+ return {"results": []}
70
+ try:
71
+ from ..brain import Retriever
72
+ r = Retriever()
73
+ results = r.search(q, k=max_results)
74
+ formatted = []
75
+ for res in results:
76
+ formatted.append({
77
+ "content": res.get("content", "")[:500],
78
+ "file": res.get("file", ""),
79
+ "score": res.get("score", 0),
80
+ })
81
+ return {"results": formatted}
82
+ except Exception as e:
83
+ return JSONResponse({"error": str(e)}, status_code=500)
84
+
85
+
86
+ @router.get("/api/brain/stats")
87
+ async def brain_stats(request: Request):
88
+ try:
89
+ from ..brain import Retriever
90
+ r = Retriever()
91
+ info = r.stats()
92
+ return info
93
+ except Exception as e:
94
+ return JSONResponse({"error": str(e)}, status_code=500)
95
+
96
+
97
+ @router.get("/api/brain/dependents")
98
+ async def brain_dependents(request: Request, symbol: str = ""):
99
+ """Find all nodes that depend on a symbol in the knowledge graph."""
100
+ if not symbol:
101
+ return JSONResponse({"error": "symbol parameter required"}, status_code=400)
102
+ try:
103
+ from ..brain import KnowledgeGraph
104
+ kg = KnowledgeGraph()
105
+ kg.load()
106
+ deps = kg.find_dependents(symbol)
107
+ return {"symbol": symbol, "dependents": deps, "count": len(deps)}
108
+ except Exception as e:
109
+ return JSONResponse({"error": str(e)}, status_code=500)
@@ -0,0 +1,12 @@
1
+ """Cost tracking route."""
2
+
3
+ from fastapi import APIRouter, Request
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.get("/api/cost")
9
+ async def get_cost(request: Request):
10
+ from ..cost_tracker import CostTracker
11
+ tracker = CostTracker()
12
+ return tracker.get_stats()
@@ -0,0 +1,133 @@
1
+ """File browsing, reading, writing, and editing routes."""
2
+
3
+ from pathlib import Path
4
+
5
+ from fastapi import APIRouter, Request
6
+ from fastapi.responses import JSONResponse
7
+ from pydantic import BaseModel
8
+
9
+ from ..tools import path_validate
10
+
11
+ router = APIRouter()
12
+
13
+ MAX_MESSAGE_LENGTH = 10000
14
+ MAX_READ_BYTES = 1_000_000 # 1 MB
15
+ MAX_WRITE_BYTES = 10_485_760 # 10 MB — matches CLI WriteTool
16
+
17
+
18
+ def _safe_resolve(path: str):
19
+ """Wrap safe_resolve so callers get None instead of a raw ValueError."""
20
+ try:
21
+ return path_validate.safe_resolve(path)
22
+ except ValueError:
23
+ return None
24
+
25
+
26
+ class WriteData(BaseModel):
27
+ path: str = ""
28
+ content: str = ""
29
+
30
+
31
+ class EditData(BaseModel):
32
+ path: str = ""
33
+ old_string: str = ""
34
+ new_string: str = ""
35
+
36
+
37
+ @router.get("/api/tools")
38
+ async def list_tools(request: Request):
39
+ """List available tool names."""
40
+ state = request.app.state.web_state
41
+ tools = state.registry.list_tools()
42
+ tool_objects = [{"name": t["function"]["name"]} for t in tools]
43
+ return {"tools": tool_objects, "count": len(tools)}
44
+
45
+
46
+ @router.get("/api/files")
47
+ async def list_files(request: Request, dir: str = "."):
48
+ """List directory contents."""
49
+ safe = _safe_resolve(dir)
50
+ if safe is None:
51
+ return JSONResponse({"error": "Access denied: path traversal detected"}, status_code=403)
52
+ path = Path(safe)
53
+ if not path.exists():
54
+ return JSONResponse({"error": f"Directory not found: {dir}"}, status_code=404)
55
+ if not path.is_dir():
56
+ return JSONResponse({"error": f"Not a directory: {dir}"}, status_code=400)
57
+
58
+ try:
59
+ items = []
60
+ for child in sorted(path.iterdir()):
61
+ try:
62
+ items.append({
63
+ "name": child.name,
64
+ "is_dir": child.is_dir(),
65
+ "size": child.stat().st_size if child.is_file() else 0,
66
+ })
67
+ except OSError:
68
+ continue
69
+ parent = str(path.parent) if path.parent != path else str(path)
70
+ return {"files": items, "current": str(path), "parent": parent}
71
+ except PermissionError:
72
+ return JSONResponse({"error": "Permission denied"}, status_code=403)
73
+
74
+
75
+ @router.get("/api/read-file")
76
+ async def read_file(request: Request, path: str = ""):
77
+ if not path:
78
+ return JSONResponse({"error": "path parameter required"}, status_code=400)
79
+ safe = _safe_resolve(path)
80
+ if safe is None:
81
+ return JSONResponse({"error": "Access denied: path traversal detected"}, status_code=403)
82
+ file_path = Path(safe)
83
+ if not file_path.exists():
84
+ return JSONResponse({"error": f"File not found: {path}"}, status_code=404)
85
+ if not file_path.is_file():
86
+ return JSONResponse({"error": f"Not a file: {path}"}, status_code=400)
87
+ try:
88
+ # Check size via stat before reading into memory
89
+ if file_path.stat().st_size > MAX_READ_BYTES:
90
+ return JSONResponse({"error": "File too large (max 1 MB)"}, status_code=413)
91
+ content = file_path.read_text(encoding="utf-8")
92
+ return {"content": content, "path": str(file_path), "size": len(content)}
93
+ except Exception as e:
94
+ return JSONResponse({"error": f"Failed to read file: {e}"}, status_code=500)
95
+
96
+
97
+ @router.post("/api/write-file")
98
+ async def write_file(request: Request, data: WriteData):
99
+ if not data.path:
100
+ return JSONResponse({"error": "path required"}, status_code=400)
101
+ if len(data.content) > MAX_WRITE_BYTES:
102
+ return JSONResponse({"error": "Content too large (max 10 MB)"}, status_code=413)
103
+ safe = _safe_resolve(data.path)
104
+ if safe is None:
105
+ return JSONResponse({"error": "Access denied: path traversal detected"}, status_code=403)
106
+ file_path = Path(safe)
107
+ try:
108
+ file_path.parent.mkdir(parents=True, exist_ok=True)
109
+ file_path.write_text(data.content, encoding="utf-8")
110
+ return {"status": "written", "path": str(file_path), "size": len(data.content)}
111
+ except Exception as e:
112
+ return JSONResponse({"error": f"Failed to write file: {e}"}, status_code=500)
113
+
114
+
115
+ @router.post("/api/edit-file")
116
+ async def edit_file(request: Request, data: EditData):
117
+ if not data.path or not data.old_string:
118
+ return JSONResponse({"error": "path and old_string required"}, status_code=400)
119
+ safe = _safe_resolve(data.path)
120
+ if safe is None:
121
+ return JSONResponse({"error": "Access denied: path traversal detected"}, status_code=403)
122
+ file_path = Path(safe)
123
+ if not file_path.exists():
124
+ return JSONResponse({"error": f"File not found: {data.path}"}, status_code=404)
125
+ try:
126
+ content = file_path.read_text(encoding="utf-8")
127
+ if data.old_string not in content:
128
+ return JSONResponse({"error": "old_string not found in file", "content": content}, status_code=400)
129
+ new_content = content.replace(data.old_string, data.new_string, 1)
130
+ file_path.write_text(new_content, encoding="utf-8")
131
+ return {"status": "edited", "path": str(file_path), "replacements": 1}
132
+ except Exception as e:
133
+ return JSONResponse({"error": f"Failed to edit file: {e}"}, status_code=500)
@@ -0,0 +1,94 @@
1
+ """Memory (MEMORY.md and named memories) routes."""
2
+
3
+ from fastapi import APIRouter, Request
4
+ from fastapi.responses import JSONResponse
5
+ from pydantic import BaseModel
6
+
7
+ from ..web_app import memory_module
8
+
9
+ router = APIRouter()
10
+
11
+
12
+ # --- Project Memory (MEMORY.md) ---
13
+
14
+ @router.get("/api/memory")
15
+ async def get_memory(request: Request):
16
+ state = request.app.state.web_state
17
+ md = memory_module.load_claude_md()
18
+ return {"claude_md": md, "message_count": state.context.count_messages()}
19
+
20
+
21
+ class MemorySave(BaseModel):
22
+ content: str = ""
23
+
24
+
25
+ @router.post("/api/memory/save")
26
+ async def save_memory(request: Request, data: MemorySave):
27
+ memory_module.save_claude_md(data.content)
28
+ # Update context so the change is reflected immediately.
29
+ # The <claude-md> block may also contain session memories — preserve them.
30
+ state = request.app.state.web_state
31
+ session_suffix = ""
32
+ for m in state.context.messages:
33
+ if isinstance(m.get("content"), str) and m["content"].startswith("<claude-md>"):
34
+ inner = m["content"][len("<claude-md>"):-len("</claude-md>")]
35
+ if "<memories>" in inner:
36
+ session_suffix = "\n\n" + inner[inner.index("<memories>"):]
37
+ break
38
+ merged = data.content + session_suffix
39
+ for i, m in enumerate(state.context.messages):
40
+ if isinstance(m.get("content"), str) and m["content"].startswith("<claude-md>"):
41
+ state.context.messages[i]["content"] = f"<claude-md>{merged}</claude-md>"
42
+ break
43
+ else:
44
+ state.context.messages.insert(1, {
45
+ "role": "user",
46
+ "content": f"<claude-md>{merged}</claude-md>",
47
+ })
48
+ return {"status": "saved"}
49
+
50
+
51
+ # --- Named memories ---
52
+
53
+ @router.get("/api/memories")
54
+ async def list_memories(request: Request, q: str = ""):
55
+ state = request.app.state.web_state
56
+ mgr = state.web_memory_mgr
57
+ if q:
58
+ results = mgr.search_memories(q)
59
+ return {"memories": results}
60
+ all_memories = mgr.list_memories()
61
+ return {"memories": all_memories}
62
+
63
+
64
+ class NamedMemorySave(BaseModel):
65
+ name: str
66
+ content: str
67
+
68
+
69
+ @router.post("/api/memories/save")
70
+ async def save_memory_web(request: Request, data: NamedMemorySave):
71
+ state = request.app.state.web_state
72
+ mgr = state.web_memory_mgr
73
+ mgr.save_memory(data.name, data.content)
74
+ return {"status": "ok", "name": data.name}
75
+
76
+
77
+ @router.delete("/api/memories/{name}")
78
+ async def delete_memory_web(request: Request, name: str):
79
+ state = request.app.state.web_state
80
+ mgr = state.web_memory_mgr
81
+ ok = mgr.delete_memory(name)
82
+ if ok:
83
+ return {"status": "ok", "name": name}
84
+ return JSONResponse({"error": "Memory not found"}, status_code=404)
85
+
86
+
87
+ @router.get("/api/memories/{name}")
88
+ async def get_memory_web(request: Request, name: str):
89
+ state = request.app.state.web_state
90
+ mgr = state.web_memory_mgr
91
+ content = mgr.load_memory(name)
92
+ if content:
93
+ return {"name": name, "content": content}
94
+ return JSONResponse({"error": "Memory not found"}, status_code=404)
@@ -0,0 +1,67 @@
1
+ """Miscellaneous routes: clear, undo, compact, context info."""
2
+
3
+ from fastapi import APIRouter, Request
4
+ from fastapi.responses import JSONResponse
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ @router.post("/api/clear")
10
+ async def clear_context(request: Request):
11
+ try:
12
+ state = request.app.state.web_state
13
+ context = state.context
14
+ memory_module = state.memory_module
15
+ context.reset()
16
+ # Re-inject merged memory block
17
+ from ..memory import MemoryManager
18
+ mgr = MemoryManager()
19
+ md = memory_module.load_claude_md()
20
+ session_memories = mgr.get_all_memories_formatted()
21
+ if md and session_memories:
22
+ merged = md + "\n\n" + session_memories
23
+ elif session_memories:
24
+ merged = session_memories
25
+ else:
26
+ merged = md or ""
27
+ if merged:
28
+ context.messages.insert(1, {
29
+ "role": "user",
30
+ "content": f"<claude-md>{merged}</claude-md>",
31
+ })
32
+ return {"status": "cleared"}
33
+ except Exception as e:
34
+ return JSONResponse({"error": str(e)}, status_code=500)
35
+
36
+
37
+ @router.post("/api/undo")
38
+ async def undo():
39
+ try:
40
+ from ..undo import undo_last
41
+ result = undo_last()
42
+ return {"status": result}
43
+ except Exception as e:
44
+ return JSONResponse({"error": str(e)}, status_code=500)
45
+
46
+
47
+ @router.post("/api/compact")
48
+ async def compact(request: Request):
49
+ try:
50
+ state = request.app.state.web_state
51
+ result = state.context.compact(state.config, state.config.model)
52
+ return {"status": result}
53
+ except Exception as e:
54
+ return JSONResponse({"error": str(e)}, status_code=500)
55
+
56
+
57
+ @router.get("/api/context")
58
+ async def context_info(request: Request):
59
+ try:
60
+ context = request.app.state.web_state.context
61
+ return {
62
+ "message_count": context.count_messages(),
63
+ "max_messages": context.max_messages,
64
+ "estimated_tokens": context.estimate_tokens(),
65
+ }
66
+ except Exception as e:
67
+ return JSONResponse({"error": str(e)}, status_code=500)
@@ -0,0 +1,48 @@
1
+ """Project initialization, indexing, tasks, and plans routes."""
2
+
3
+ from fastapi import APIRouter, Request
4
+
5
+ from .. import tasks, planner, init as project_init
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ @router.post("/api/init")
11
+ async def init_project():
12
+ result = project_init.init_project()
13
+ return {"status": "ok", "message": result}
14
+
15
+
16
+ @router.post("/api/index")
17
+ async def reindex_project(request: Request):
18
+ from ..indexer import index_project
19
+ project_context = index_project()
20
+ state = request.app.state.web_state
21
+ if project_context and state.context:
22
+ new_content = f"<project-context>\n{project_context}\n</project-context>"
23
+ replaced = False
24
+ for i, m in enumerate(state.context.messages):
25
+ content = str(m.get("content", ""))
26
+ if content.startswith("<project-context>") and content.endswith("</project-context>"):
27
+ state.context.messages[i]["content"] = new_content
28
+ replaced = True
29
+ break
30
+ if not replaced:
31
+ state.context.messages.insert(1, {
32
+ "role": "user",
33
+ "content": new_content,
34
+ })
35
+ return {"status": "ok", "items": project_context.count("\n") + 1}
36
+ return {"status": "ok", "items": 0}
37
+
38
+
39
+ @router.get("/api/tasks")
40
+ async def list_tasks(status: str = ""):
41
+ result = tasks.list_tasks(status or None)
42
+ return {"tasks": result}
43
+
44
+
45
+ @router.get("/api/plans")
46
+ async def list_plans():
47
+ plans = planner.list_plans()
48
+ return {"plans": plans}
@@ -0,0 +1,20 @@
1
+ """Code review and security review routes."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from ..skills import review as review_skill
6
+ from ..skills import security as security_skill
7
+
8
+ router = APIRouter()
9
+
10
+
11
+ @router.get("/api/review")
12
+ async def review_code():
13
+ diff = review_skill.review_changes()
14
+ return {"diff": diff}
15
+
16
+
17
+ @router.get("/api/security-review")
18
+ async def security_review():
19
+ analysis = security_skill.security_review()
20
+ return {"analysis": analysis}
@@ -0,0 +1,44 @@
1
+ """Session management routes."""
2
+
3
+ from fastapi import APIRouter, Request
4
+ from pydantic import BaseModel
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ class SessionSave(BaseModel):
10
+ name: str
11
+
12
+
13
+ class SessionLoad(BaseModel):
14
+ name: str
15
+
16
+
17
+ @router.get("/api/sessions")
18
+ async def sessions_list():
19
+ from ..sessions import list_sessions
20
+ result = list_sessions()
21
+ return {"sessions": result}
22
+
23
+
24
+ @router.post("/api/sessions/save")
25
+ async def sessions_save(request: Request, data: SessionSave):
26
+ from ..sessions import save_session
27
+ state = request.app.state.web_state
28
+ result = save_session(data.name, state.context)
29
+ return {"status": "ok", "message": result}
30
+
31
+
32
+ @router.post("/api/sessions/load")
33
+ async def sessions_load(request: Request, data: SessionLoad):
34
+ from ..sessions import load_session
35
+ state = request.app.state.web_state
36
+ result = load_session(data.name, state.context)
37
+ return {"status": "ok", "message": result}
38
+
39
+
40
+ @router.delete("/api/sessions/{name}")
41
+ async def sessions_delete(name: str):
42
+ from ..sessions import delete_session
43
+ result = delete_session(name)
44
+ return {"status": "ok", "message": result}
@@ -0,0 +1,43 @@
1
+ """Settings and model configuration routes."""
2
+
3
+ from fastapi import APIRouter, Request
4
+ from pydantic import BaseModel
5
+
6
+ from .. import settings as cfg
7
+
8
+ router = APIRouter()
9
+
10
+
11
+ @router.get("/api/settings")
12
+ async def get_settings(request: Request):
13
+ return cfg.load_settings()
14
+
15
+
16
+ class SettingUpdate(BaseModel):
17
+ key: str
18
+ value: str
19
+
20
+
21
+ @router.post("/api/settings")
22
+ async def set_settings(data: SettingUpdate):
23
+ cfg.save_setting(data.key, data.value)
24
+ return {"status": "ok", "key": data.key, "value": data.value}
25
+
26
+
27
+ @router.get("/api/models")
28
+ async def list_models():
29
+ from ..model_registry import format_model_list, get_unique_model_count
30
+ return {"models": format_model_list(), "count": get_unique_model_count()}
31
+
32
+
33
+ class ModelSet(BaseModel):
34
+ model: str
35
+
36
+
37
+ @router.post("/api/models/set")
38
+ async def set_model(data: ModelSet):
39
+ from ..config import Config
40
+ c = Config()
41
+ c.model = data.model
42
+ c.save()
43
+ return {"status": "ok", "model": data.model}
@@ -0,0 +1,70 @@
1
+ """Static / frontend routes."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from fastapi import APIRouter
7
+ from fastapi.responses import HTMLResponse, JSONResponse
8
+
9
+ router = APIRouter()
10
+
11
+ TEMPLATES = Path(__file__).resolve().parent.parent / "templates"
12
+
13
+
14
+ @router.get("/")
15
+ async def index():
16
+ path = TEMPLATES / "index.html"
17
+ if path.exists():
18
+ return HTMLResponse(
19
+ path.read_text(encoding="utf-8"),
20
+ headers={
21
+ "Cache-Control": "no-store, no-cache, must-revalidate",
22
+ "Pragma": "no-cache",
23
+ "Expires": "0",
24
+ },
25
+ )
26
+ return HTMLResponse("<h1>DeepSeek Code Web UI</h1><p>Template not found.</p>")
27
+
28
+
29
+ @router.get("/manifest.json")
30
+ async def manifest():
31
+ path = TEMPLATES / "manifest.json"
32
+ if path.exists():
33
+ return JSONResponse(json.loads(path.read_text(encoding="utf-8")))
34
+ return JSONResponse({}, status_code=404)
35
+
36
+
37
+ @router.get("/sw.js")
38
+ async def service_worker():
39
+ path = TEMPLATES / "sw.js"
40
+ if path.exists():
41
+ from fastapi.responses import Response
42
+ return Response(path.read_bytes(), media_type="application/javascript")
43
+ return Response(status_code=404)
44
+
45
+
46
+ @router.get("/icon-192.png")
47
+ async def icon_192():
48
+ path = TEMPLATES / "icon-192.png"
49
+ if path.exists():
50
+ from fastapi.responses import Response
51
+ return Response(path.read_bytes(), media_type="image/png")
52
+ return Response(status_code=404)
53
+
54
+
55
+ @router.get("/icon-512.png")
56
+ async def icon_512():
57
+ path = TEMPLATES / "icon-512.png"
58
+ if path.exists():
59
+ from fastapi.responses import Response
60
+ return Response(path.read_bytes(), media_type="image/png")
61
+ return Response(status_code=404)
62
+
63
+
64
+ @router.get("/favicon.ico")
65
+ async def favicon():
66
+ path = TEMPLATES / "icon-192.png"
67
+ if path.exists():
68
+ from fastapi.responses import Response
69
+ return Response(path.read_bytes(), media_type="image/png")
70
+ return Response(status_code=404)
@@ -0,0 +1,19 @@
1
+ """Update check and self-update routes."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from .. import update as updater
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ @router.get("/api/update/check")
11
+ async def check_updates():
12
+ result = updater.get_version()
13
+ return {"version": result, "update_available": False}
14
+
15
+
16
+ @router.post("/api/update")
17
+ async def do_update():
18
+ result = updater.do_update()
19
+ return {"status": "ok", "message": result}