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.
- luckyd_code/__init__.py +54 -0
- luckyd_code/__main__.py +5 -0
- luckyd_code/_agent_loop.py +551 -0
- luckyd_code/_data_dir.py +73 -0
- luckyd_code/agent.py +38 -0
- luckyd_code/analytics/__init__.py +18 -0
- luckyd_code/analytics/reporter.py +195 -0
- luckyd_code/analytics/scanner.py +443 -0
- luckyd_code/analytics/smells.py +316 -0
- luckyd_code/analytics/trends.py +303 -0
- luckyd_code/api.py +473 -0
- luckyd_code/audit_daemon.py +845 -0
- luckyd_code/autonomous_fixer.py +473 -0
- luckyd_code/background.py +159 -0
- luckyd_code/backup.py +237 -0
- luckyd_code/brain/__init__.py +84 -0
- luckyd_code/brain/assembler.py +100 -0
- luckyd_code/brain/chunker.py +345 -0
- luckyd_code/brain/constants.py +73 -0
- luckyd_code/brain/embedder.py +163 -0
- luckyd_code/brain/graph.py +311 -0
- luckyd_code/brain/indexer.py +316 -0
- luckyd_code/brain/parser.py +140 -0
- luckyd_code/brain/retriever.py +234 -0
- luckyd_code/cli.py +894 -0
- luckyd_code/cli_commands/__init__.py +1 -0
- luckyd_code/cli_commands/audit.py +120 -0
- luckyd_code/cli_commands/background.py +83 -0
- luckyd_code/cli_commands/brain.py +87 -0
- luckyd_code/cli_commands/config.py +75 -0
- luckyd_code/cli_commands/dispatcher.py +695 -0
- luckyd_code/cli_commands/sessions.py +41 -0
- luckyd_code/cli_entry.py +147 -0
- luckyd_code/cli_utils.py +112 -0
- luckyd_code/config.py +205 -0
- luckyd_code/context.py +214 -0
- luckyd_code/cost_tracker.py +209 -0
- luckyd_code/error_reporter.py +508 -0
- luckyd_code/exceptions.py +39 -0
- luckyd_code/export.py +126 -0
- luckyd_code/feedback_analyzer.py +290 -0
- luckyd_code/file_watcher.py +258 -0
- luckyd_code/git/__init__.py +11 -0
- luckyd_code/git/auto_commit.py +157 -0
- luckyd_code/git/tools.py +85 -0
- luckyd_code/hooks.py +236 -0
- luckyd_code/indexer.py +280 -0
- luckyd_code/init.py +39 -0
- luckyd_code/keybindings.py +77 -0
- luckyd_code/log.py +55 -0
- luckyd_code/mcp/__init__.py +6 -0
- luckyd_code/mcp/client.py +184 -0
- luckyd_code/memory/__init__.py +19 -0
- luckyd_code/memory/manager.py +339 -0
- luckyd_code/metrics/__init__.py +5 -0
- luckyd_code/model_registry.py +131 -0
- luckyd_code/orchestrator.py +204 -0
- luckyd_code/permissions/__init__.py +1 -0
- luckyd_code/permissions/manager.py +103 -0
- luckyd_code/planner.py +361 -0
- luckyd_code/plugins.py +91 -0
- luckyd_code/py.typed +0 -0
- luckyd_code/retry.py +57 -0
- luckyd_code/router.py +417 -0
- luckyd_code/sandbox.py +156 -0
- luckyd_code/self_critique.py +2 -0
- luckyd_code/self_improve.py +274 -0
- luckyd_code/sessions.py +114 -0
- luckyd_code/settings.py +72 -0
- luckyd_code/skills/__init__.py +8 -0
- luckyd_code/skills/review.py +22 -0
- luckyd_code/skills/security.py +17 -0
- luckyd_code/tasks/__init__.py +1 -0
- luckyd_code/tasks/manager.py +102 -0
- luckyd_code/templates/icon-192.png +0 -0
- luckyd_code/templates/icon-512.png +0 -0
- luckyd_code/templates/index.html +1965 -0
- luckyd_code/templates/manifest.json +14 -0
- luckyd_code/templates/src/app.js +694 -0
- luckyd_code/templates/src/body.html +767 -0
- luckyd_code/templates/src/cdn.txt +2 -0
- luckyd_code/templates/src/style.css +474 -0
- luckyd_code/templates/sw.js +31 -0
- luckyd_code/templates/test.html +6 -0
- luckyd_code/themes.py +48 -0
- luckyd_code/tools/__init__.py +97 -0
- luckyd_code/tools/agent_tools.py +65 -0
- luckyd_code/tools/bash.py +360 -0
- luckyd_code/tools/brain_tools.py +137 -0
- luckyd_code/tools/browser.py +369 -0
- luckyd_code/tools/datetime_tool.py +34 -0
- luckyd_code/tools/dockerfile_gen.py +212 -0
- luckyd_code/tools/file_ops.py +381 -0
- luckyd_code/tools/game_gen.py +360 -0
- luckyd_code/tools/git_tools.py +130 -0
- luckyd_code/tools/git_worktree.py +63 -0
- luckyd_code/tools/path_validate.py +64 -0
- luckyd_code/tools/project_gen.py +187 -0
- luckyd_code/tools/readme_gen.py +227 -0
- luckyd_code/tools/registry.py +157 -0
- luckyd_code/tools/shell_detect.py +109 -0
- luckyd_code/tools/web.py +89 -0
- luckyd_code/tools/youtube.py +187 -0
- luckyd_code/tools_bridge.py +144 -0
- luckyd_code/undo.py +126 -0
- luckyd_code/update.py +60 -0
- luckyd_code/verify.py +360 -0
- luckyd_code/web_app.py +176 -0
- luckyd_code/web_routes/__init__.py +23 -0
- luckyd_code/web_routes/background.py +73 -0
- luckyd_code/web_routes/brain.py +109 -0
- luckyd_code/web_routes/cost.py +12 -0
- luckyd_code/web_routes/files.py +133 -0
- luckyd_code/web_routes/memories.py +94 -0
- luckyd_code/web_routes/misc.py +67 -0
- luckyd_code/web_routes/project.py +48 -0
- luckyd_code/web_routes/review.py +20 -0
- luckyd_code/web_routes/sessions.py +44 -0
- luckyd_code/web_routes/settings.py +43 -0
- luckyd_code/web_routes/static.py +70 -0
- luckyd_code/web_routes/update.py +19 -0
- luckyd_code/web_routes/ws.py +237 -0
- luckyd_code-1.2.2.dist-info/METADATA +297 -0
- luckyd_code-1.2.2.dist-info/RECORD +127 -0
- luckyd_code-1.2.2.dist-info/WHEEL +4 -0
- luckyd_code-1.2.2.dist-info/entry_points.txt +3 -0
- 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}
|