nc1709 1.15.4__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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
nc1709/web/server.py
ADDED
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NC1709 Web Dashboard Server
|
|
3
|
+
FastAPI-based local web server for the dashboard
|
|
4
|
+
"""
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, Optional, List
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Header, Depends
|
|
13
|
+
from fastapi.staticfiles import StaticFiles
|
|
14
|
+
from fastapi.responses import HTMLResponse, FileResponse
|
|
15
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
|
|
18
|
+
# Get the directory where this file is located
|
|
19
|
+
WEB_DIR = Path(__file__).parent
|
|
20
|
+
STATIC_DIR = WEB_DIR / "static"
|
|
21
|
+
TEMPLATES_DIR = WEB_DIR / "templates"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ChatMessage(BaseModel):
|
|
25
|
+
"""Chat message model"""
|
|
26
|
+
role: str # "user" or "assistant"
|
|
27
|
+
content: str
|
|
28
|
+
timestamp: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ChatRequest(BaseModel):
|
|
32
|
+
"""Chat request model"""
|
|
33
|
+
message: str
|
|
34
|
+
session_id: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PluginActionRequest(BaseModel):
|
|
38
|
+
"""Plugin action request model"""
|
|
39
|
+
plugin: str
|
|
40
|
+
action: str
|
|
41
|
+
params: Optional[Dict[str, Any]] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MCPToolRequest(BaseModel):
|
|
45
|
+
"""MCP tool request model"""
|
|
46
|
+
tool: str
|
|
47
|
+
arguments: Optional[Dict[str, Any]] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SearchRequest(BaseModel):
|
|
51
|
+
"""Search request model"""
|
|
52
|
+
query: str
|
|
53
|
+
n_results: Optional[int] = 5
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RemoteLLMRequest(BaseModel):
|
|
57
|
+
"""Remote LLM request model for API access"""
|
|
58
|
+
prompt: str
|
|
59
|
+
task_type: Optional[str] = "general"
|
|
60
|
+
system_prompt: Optional[str] = None
|
|
61
|
+
temperature: Optional[float] = 0.7
|
|
62
|
+
max_tokens: Optional[int] = None
|
|
63
|
+
stream: Optional[bool] = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AgentChatRequest(BaseModel):
|
|
67
|
+
"""Agent chat request - for local tool execution architecture"""
|
|
68
|
+
messages: List[Dict[str, str]] # Conversation history
|
|
69
|
+
cwd: str # Client's current working directory
|
|
70
|
+
tools: Optional[List[str]] = None # List of available tools on client
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class IndexCodeRequest(BaseModel):
|
|
74
|
+
"""Request to index code in the server's vector database"""
|
|
75
|
+
user_id: str # Unique user/session identifier
|
|
76
|
+
files: List[Dict[str, str]] # List of {"path": "...", "content": "...", "language": "..."}
|
|
77
|
+
project_name: Optional[str] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SearchCodeRequest(BaseModel):
|
|
81
|
+
"""Request to search indexed code"""
|
|
82
|
+
user_id: str # User identifier to search within their indexed code
|
|
83
|
+
query: str
|
|
84
|
+
n_results: Optional[int] = 5
|
|
85
|
+
project_name: Optional[str] = None # Optional: filter by project
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def create_app() -> FastAPI:
|
|
89
|
+
"""Create the FastAPI application
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Configured FastAPI app
|
|
93
|
+
"""
|
|
94
|
+
app = FastAPI(
|
|
95
|
+
title="NC1709 Dashboard",
|
|
96
|
+
description="Local web dashboard for NC1709 AI assistant",
|
|
97
|
+
version="1.0.0"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# CORS for local development
|
|
101
|
+
app.add_middleware(
|
|
102
|
+
CORSMiddleware,
|
|
103
|
+
allow_origins=["*"],
|
|
104
|
+
allow_credentials=True,
|
|
105
|
+
allow_methods=["*"],
|
|
106
|
+
allow_headers=["*"],
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Lazy-loaded components
|
|
110
|
+
_components = {}
|
|
111
|
+
|
|
112
|
+
def get_config():
|
|
113
|
+
if "config" not in _components:
|
|
114
|
+
from ..config import get_config
|
|
115
|
+
_components["config"] = get_config()
|
|
116
|
+
return _components["config"]
|
|
117
|
+
|
|
118
|
+
def get_session_manager():
|
|
119
|
+
if "session_manager" not in _components:
|
|
120
|
+
try:
|
|
121
|
+
from ..memory.sessions import SessionManager
|
|
122
|
+
_components["session_manager"] = SessionManager()
|
|
123
|
+
except ImportError:
|
|
124
|
+
_components["session_manager"] = None
|
|
125
|
+
return _components["session_manager"]
|
|
126
|
+
|
|
127
|
+
def get_project_indexer():
|
|
128
|
+
if "indexer" not in _components:
|
|
129
|
+
try:
|
|
130
|
+
from ..memory.indexer import ProjectIndexer
|
|
131
|
+
_components["indexer"] = ProjectIndexer(str(Path.cwd()))
|
|
132
|
+
except (ImportError, Exception):
|
|
133
|
+
_components["indexer"] = None
|
|
134
|
+
return _components["indexer"]
|
|
135
|
+
|
|
136
|
+
def get_plugin_manager():
|
|
137
|
+
if "plugin_manager" not in _components:
|
|
138
|
+
try:
|
|
139
|
+
from ..plugins import PluginManager
|
|
140
|
+
pm = PluginManager()
|
|
141
|
+
pm.discover_plugins()
|
|
142
|
+
pm.load_all()
|
|
143
|
+
_components["plugin_manager"] = pm
|
|
144
|
+
except ImportError:
|
|
145
|
+
_components["plugin_manager"] = None
|
|
146
|
+
return _components["plugin_manager"]
|
|
147
|
+
|
|
148
|
+
def get_mcp_manager():
|
|
149
|
+
if "mcp_manager" not in _components:
|
|
150
|
+
try:
|
|
151
|
+
from ..mcp import MCPManager
|
|
152
|
+
mm = MCPManager(name="nc1709", version="1.0.0")
|
|
153
|
+
mm.setup_default_tools()
|
|
154
|
+
_components["mcp_manager"] = mm
|
|
155
|
+
except ImportError:
|
|
156
|
+
_components["mcp_manager"] = None
|
|
157
|
+
return _components["mcp_manager"]
|
|
158
|
+
|
|
159
|
+
def get_global_vector_store():
|
|
160
|
+
"""Get or create a global vector store for all users' code"""
|
|
161
|
+
if "global_vector_store" not in _components:
|
|
162
|
+
try:
|
|
163
|
+
from ..memory.vector_store import VectorStore
|
|
164
|
+
# Store in server's data directory
|
|
165
|
+
import os
|
|
166
|
+
data_dir = os.path.expanduser("~/.nc1709_server/vector_db")
|
|
167
|
+
os.makedirs(data_dir, exist_ok=True)
|
|
168
|
+
_components["global_vector_store"] = VectorStore(persist_directory=data_dir)
|
|
169
|
+
except ImportError:
|
|
170
|
+
_components["global_vector_store"] = None
|
|
171
|
+
return _components["global_vector_store"]
|
|
172
|
+
|
|
173
|
+
def get_code_chunker():
|
|
174
|
+
"""Get code chunker for splitting code into indexable chunks"""
|
|
175
|
+
if "code_chunker" not in _components:
|
|
176
|
+
try:
|
|
177
|
+
from ..memory.embeddings import CodeChunker
|
|
178
|
+
_components["code_chunker"] = CodeChunker()
|
|
179
|
+
except ImportError:
|
|
180
|
+
_components["code_chunker"] = None
|
|
181
|
+
return _components["code_chunker"]
|
|
182
|
+
|
|
183
|
+
def get_reasoning_engine():
|
|
184
|
+
if "reasoning" not in _components:
|
|
185
|
+
try:
|
|
186
|
+
from ..reasoning_engine import ReasoningEngine
|
|
187
|
+
_components["reasoning"] = ReasoningEngine()
|
|
188
|
+
except ImportError:
|
|
189
|
+
_components["reasoning"] = None
|
|
190
|
+
return _components["reasoning"]
|
|
191
|
+
|
|
192
|
+
# =========================================================================
|
|
193
|
+
# Static files and main page
|
|
194
|
+
# =========================================================================
|
|
195
|
+
|
|
196
|
+
# Mount static files
|
|
197
|
+
if STATIC_DIR.exists():
|
|
198
|
+
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
|
199
|
+
|
|
200
|
+
@app.get("/", response_class=HTMLResponse)
|
|
201
|
+
async def index():
|
|
202
|
+
"""Serve the main dashboard page"""
|
|
203
|
+
index_file = TEMPLATES_DIR / "index.html"
|
|
204
|
+
if index_file.exists():
|
|
205
|
+
return FileResponse(index_file)
|
|
206
|
+
return HTMLResponse(content=get_fallback_html(), status_code=200)
|
|
207
|
+
|
|
208
|
+
# =========================================================================
|
|
209
|
+
# API Routes - System
|
|
210
|
+
# =========================================================================
|
|
211
|
+
|
|
212
|
+
@app.get("/api/status")
|
|
213
|
+
async def get_status():
|
|
214
|
+
"""Get system status"""
|
|
215
|
+
config = get_config()
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"status": "ok",
|
|
219
|
+
"version": "1.0.0",
|
|
220
|
+
"project": str(Path.cwd()),
|
|
221
|
+
"memory_enabled": config.get("memory.enabled", False),
|
|
222
|
+
"timestamp": datetime.now().isoformat()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@app.get("/api/config")
|
|
226
|
+
async def get_configuration():
|
|
227
|
+
"""Get current configuration"""
|
|
228
|
+
config = get_config()
|
|
229
|
+
return {
|
|
230
|
+
"config": config.config,
|
|
231
|
+
"config_path": str(config.config_path)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@app.post("/api/config")
|
|
235
|
+
async def update_config(updates: Dict[str, Any]):
|
|
236
|
+
"""Update configuration"""
|
|
237
|
+
config = get_config()
|
|
238
|
+
for key, value in updates.items():
|
|
239
|
+
config.set(key, value)
|
|
240
|
+
return {"status": "ok", "updated": list(updates.keys())}
|
|
241
|
+
|
|
242
|
+
# =========================================================================
|
|
243
|
+
# API Routes - Chat
|
|
244
|
+
# =========================================================================
|
|
245
|
+
|
|
246
|
+
@app.post("/api/chat")
|
|
247
|
+
async def chat(request: ChatRequest):
|
|
248
|
+
"""Send a chat message and get a response"""
|
|
249
|
+
reasoning = get_reasoning_engine()
|
|
250
|
+
if not reasoning:
|
|
251
|
+
raise HTTPException(status_code=503, detail="Reasoning engine not available")
|
|
252
|
+
|
|
253
|
+
context = {
|
|
254
|
+
"cwd": str(Path.cwd()),
|
|
255
|
+
"task_type": "general"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
response = reasoning.process_request(request.message, context)
|
|
260
|
+
|
|
261
|
+
# Save to session if available
|
|
262
|
+
session_mgr = get_session_manager()
|
|
263
|
+
if session_mgr and request.session_id:
|
|
264
|
+
session = session_mgr.load_session(request.session_id)
|
|
265
|
+
if session:
|
|
266
|
+
session_mgr.add_message(session, "user", request.message)
|
|
267
|
+
session_mgr.add_message(session, "assistant", response)
|
|
268
|
+
session_mgr.save_session(session)
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"response": response,
|
|
272
|
+
"timestamp": datetime.now().isoformat()
|
|
273
|
+
}
|
|
274
|
+
except Exception as e:
|
|
275
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
276
|
+
|
|
277
|
+
# =========================================================================
|
|
278
|
+
# API Routes - Sessions
|
|
279
|
+
# =========================================================================
|
|
280
|
+
|
|
281
|
+
@app.get("/api/sessions")
|
|
282
|
+
async def list_sessions():
|
|
283
|
+
"""List all sessions"""
|
|
284
|
+
session_mgr = get_session_manager()
|
|
285
|
+
if not session_mgr:
|
|
286
|
+
return {"sessions": [], "error": "Session manager not available"}
|
|
287
|
+
|
|
288
|
+
sessions = session_mgr.list_sessions(limit=50)
|
|
289
|
+
return {"sessions": sessions}
|
|
290
|
+
|
|
291
|
+
@app.get("/api/sessions/{session_id}")
|
|
292
|
+
async def get_session(session_id: str):
|
|
293
|
+
"""Get a specific session"""
|
|
294
|
+
session_mgr = get_session_manager()
|
|
295
|
+
if not session_mgr:
|
|
296
|
+
raise HTTPException(status_code=503, detail="Session manager not available")
|
|
297
|
+
|
|
298
|
+
session = session_mgr.load_session(session_id)
|
|
299
|
+
if not session:
|
|
300
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
"id": session.id,
|
|
304
|
+
"name": session.name,
|
|
305
|
+
"messages": [
|
|
306
|
+
{"role": m.role, "content": m.content, "timestamp": m.timestamp}
|
|
307
|
+
for m in session.messages
|
|
308
|
+
],
|
|
309
|
+
"created_at": session.created_at,
|
|
310
|
+
"updated_at": session.updated_at
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
@app.post("/api/sessions")
|
|
314
|
+
async def create_session():
|
|
315
|
+
"""Create a new session"""
|
|
316
|
+
session_mgr = get_session_manager()
|
|
317
|
+
if not session_mgr:
|
|
318
|
+
raise HTTPException(status_code=503, detail="Session manager not available")
|
|
319
|
+
|
|
320
|
+
session = session_mgr.start_session(project_path=str(Path.cwd()))
|
|
321
|
+
return {
|
|
322
|
+
"id": session.id,
|
|
323
|
+
"name": session.name,
|
|
324
|
+
"created_at": session.created_at
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@app.delete("/api/sessions/{session_id}")
|
|
328
|
+
async def delete_session(session_id: str):
|
|
329
|
+
"""Delete a session"""
|
|
330
|
+
session_mgr = get_session_manager()
|
|
331
|
+
if not session_mgr:
|
|
332
|
+
raise HTTPException(status_code=503, detail="Session manager not available")
|
|
333
|
+
|
|
334
|
+
success = session_mgr.delete_session(session_id)
|
|
335
|
+
if not success:
|
|
336
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
337
|
+
|
|
338
|
+
return {"status": "ok", "deleted": session_id}
|
|
339
|
+
|
|
340
|
+
# =========================================================================
|
|
341
|
+
# API Routes - Search/Index
|
|
342
|
+
# =========================================================================
|
|
343
|
+
|
|
344
|
+
@app.get("/api/index/status")
|
|
345
|
+
async def get_index_status():
|
|
346
|
+
"""Get project index status"""
|
|
347
|
+
try:
|
|
348
|
+
indexer = get_project_indexer()
|
|
349
|
+
if not indexer:
|
|
350
|
+
return {"indexed": False, "error": "Indexer not available"}
|
|
351
|
+
|
|
352
|
+
summary = indexer.get_project_summary()
|
|
353
|
+
return {
|
|
354
|
+
"indexed": summary["total_files"] > 0,
|
|
355
|
+
"total_files": summary["total_files"],
|
|
356
|
+
"total_chunks": summary["total_chunks"],
|
|
357
|
+
"languages": summary["languages"]
|
|
358
|
+
}
|
|
359
|
+
except Exception as e:
|
|
360
|
+
return {"indexed": False, "error": str(e)}
|
|
361
|
+
|
|
362
|
+
@app.post("/api/index")
|
|
363
|
+
async def index_project():
|
|
364
|
+
"""Index the current project"""
|
|
365
|
+
indexer = get_project_indexer()
|
|
366
|
+
if not indexer:
|
|
367
|
+
raise HTTPException(status_code=503, detail="Indexer not available")
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
stats = indexer.index_project(show_progress=False)
|
|
371
|
+
return {
|
|
372
|
+
"status": "ok",
|
|
373
|
+
"files_indexed": stats["files_indexed"],
|
|
374
|
+
"chunks_created": stats["chunks_created"],
|
|
375
|
+
"errors": len(stats.get("errors", []))
|
|
376
|
+
}
|
|
377
|
+
except Exception as e:
|
|
378
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
379
|
+
|
|
380
|
+
@app.post("/api/search")
|
|
381
|
+
async def search_code(request: SearchRequest):
|
|
382
|
+
"""Search indexed code"""
|
|
383
|
+
try:
|
|
384
|
+
indexer = get_project_indexer()
|
|
385
|
+
if not indexer:
|
|
386
|
+
raise HTTPException(status_code=503, detail="Indexer not available")
|
|
387
|
+
|
|
388
|
+
results = indexer.search(request.query, n_results=request.n_results)
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
"query": request.query,
|
|
392
|
+
"results": [
|
|
393
|
+
{
|
|
394
|
+
"content": r.get("content", ""),
|
|
395
|
+
"location": r.get("location", ""),
|
|
396
|
+
"language": r.get("language", ""),
|
|
397
|
+
"similarity": r.get("similarity", 0)
|
|
398
|
+
}
|
|
399
|
+
for r in results
|
|
400
|
+
]
|
|
401
|
+
}
|
|
402
|
+
except HTTPException:
|
|
403
|
+
raise
|
|
404
|
+
except Exception as e:
|
|
405
|
+
raise HTTPException(status_code=503, detail=str(e))
|
|
406
|
+
|
|
407
|
+
# =========================================================================
|
|
408
|
+
# API Routes - Plugins
|
|
409
|
+
# =========================================================================
|
|
410
|
+
|
|
411
|
+
@app.get("/api/plugins")
|
|
412
|
+
async def list_plugins():
|
|
413
|
+
"""List available plugins"""
|
|
414
|
+
pm = get_plugin_manager()
|
|
415
|
+
if not pm:
|
|
416
|
+
return {"plugins": [], "error": "Plugin manager not available"}
|
|
417
|
+
|
|
418
|
+
status = pm.get_status()
|
|
419
|
+
plugins = []
|
|
420
|
+
|
|
421
|
+
for name, info in status.items():
|
|
422
|
+
plugin = pm.get_plugin(name)
|
|
423
|
+
actions = list(plugin.actions.keys()) if plugin else []
|
|
424
|
+
|
|
425
|
+
plugins.append({
|
|
426
|
+
"name": name,
|
|
427
|
+
"version": info["version"],
|
|
428
|
+
"status": info["status"],
|
|
429
|
+
"builtin": info.get("builtin", False),
|
|
430
|
+
"actions": actions,
|
|
431
|
+
"error": info.get("error")
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
return {"plugins": plugins}
|
|
435
|
+
|
|
436
|
+
@app.post("/api/plugins/execute")
|
|
437
|
+
async def execute_plugin_action(request: PluginActionRequest):
|
|
438
|
+
"""Execute a plugin action"""
|
|
439
|
+
pm = get_plugin_manager()
|
|
440
|
+
if not pm:
|
|
441
|
+
raise HTTPException(status_code=503, detail="Plugin manager not available")
|
|
442
|
+
|
|
443
|
+
plugin = pm.get_plugin(request.plugin)
|
|
444
|
+
if not plugin:
|
|
445
|
+
raise HTTPException(status_code=404, detail=f"Plugin not found: {request.plugin}")
|
|
446
|
+
|
|
447
|
+
if request.action not in plugin.actions:
|
|
448
|
+
raise HTTPException(status_code=404, detail=f"Action not found: {request.action}")
|
|
449
|
+
|
|
450
|
+
result = pm.execute_action(request.plugin, request.action, **(request.params or {}))
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
"success": result.success,
|
|
454
|
+
"message": result.message,
|
|
455
|
+
"data": result.data,
|
|
456
|
+
"error": result.error
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
# =========================================================================
|
|
460
|
+
# API Routes - MCP
|
|
461
|
+
# =========================================================================
|
|
462
|
+
|
|
463
|
+
@app.get("/api/mcp/status")
|
|
464
|
+
async def get_mcp_status():
|
|
465
|
+
"""Get MCP status"""
|
|
466
|
+
mm = get_mcp_manager()
|
|
467
|
+
if not mm:
|
|
468
|
+
return {"available": False, "error": "MCP not available"}
|
|
469
|
+
|
|
470
|
+
status = mm.get_status()
|
|
471
|
+
return {
|
|
472
|
+
"available": True,
|
|
473
|
+
"server": status["server"],
|
|
474
|
+
"client": status["client"]
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
@app.get("/api/mcp/tools")
|
|
478
|
+
async def list_mcp_tools():
|
|
479
|
+
"""List MCP tools"""
|
|
480
|
+
mm = get_mcp_manager()
|
|
481
|
+
if not mm:
|
|
482
|
+
return {"tools": [], "error": "MCP not available"}
|
|
483
|
+
|
|
484
|
+
all_tools = mm.get_all_tools()
|
|
485
|
+
|
|
486
|
+
local_tools = [
|
|
487
|
+
{
|
|
488
|
+
"name": t.name,
|
|
489
|
+
"description": t.description,
|
|
490
|
+
"parameters": [
|
|
491
|
+
{"name": p.name, "type": p.type, "required": p.required}
|
|
492
|
+
for p in t.parameters
|
|
493
|
+
]
|
|
494
|
+
}
|
|
495
|
+
for t in all_tools["local"]
|
|
496
|
+
]
|
|
497
|
+
|
|
498
|
+
remote_tools = [
|
|
499
|
+
{"name": t.name, "description": t.description}
|
|
500
|
+
for t in all_tools["remote"]
|
|
501
|
+
]
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
"local": local_tools,
|
|
505
|
+
"remote": remote_tools
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
@app.post("/api/mcp/call")
|
|
509
|
+
async def call_mcp_tool(request: MCPToolRequest):
|
|
510
|
+
"""Call an MCP tool"""
|
|
511
|
+
mm = get_mcp_manager()
|
|
512
|
+
if not mm:
|
|
513
|
+
raise HTTPException(status_code=503, detail="MCP not available")
|
|
514
|
+
|
|
515
|
+
result = await mm.call_tool(request.tool, request.arguments)
|
|
516
|
+
|
|
517
|
+
if "error" in result:
|
|
518
|
+
raise HTTPException(status_code=400, detail=result["error"])
|
|
519
|
+
|
|
520
|
+
return result
|
|
521
|
+
|
|
522
|
+
# =========================================================================
|
|
523
|
+
# API Routes - Remote LLM Access (for remote clients)
|
|
524
|
+
# =========================================================================
|
|
525
|
+
|
|
526
|
+
def verify_api_key(x_api_key: Optional[str] = Header(None)):
|
|
527
|
+
"""Verify API key for remote access"""
|
|
528
|
+
config = get_config()
|
|
529
|
+
server_api_key = config.get("remote.api_key", None)
|
|
530
|
+
|
|
531
|
+
# If no API key is configured, allow access (open mode)
|
|
532
|
+
if not server_api_key:
|
|
533
|
+
return True
|
|
534
|
+
|
|
535
|
+
# If API key is configured, require it
|
|
536
|
+
if not x_api_key or x_api_key != server_api_key:
|
|
537
|
+
raise HTTPException(
|
|
538
|
+
status_code=401,
|
|
539
|
+
detail="Invalid or missing API key. Set X-API-Key header."
|
|
540
|
+
)
|
|
541
|
+
return True
|
|
542
|
+
|
|
543
|
+
@app.get("/api/remote/status")
|
|
544
|
+
async def remote_status(authorized: bool = Depends(verify_api_key)):
|
|
545
|
+
"""Check remote API status and available models"""
|
|
546
|
+
config = get_config()
|
|
547
|
+
return {
|
|
548
|
+
"status": "ok",
|
|
549
|
+
"server": "nc1709",
|
|
550
|
+
"version": "1.4.0",
|
|
551
|
+
"models": config.get("models", {}),
|
|
552
|
+
"ollama_url": config.get("ollama.base_url", "http://localhost:11434"),
|
|
553
|
+
"auth_required": bool(config.get("remote.api_key")),
|
|
554
|
+
"timestamp": datetime.now().isoformat()
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
@app.post("/api/remote/complete")
|
|
558
|
+
async def remote_complete(
|
|
559
|
+
request: RemoteLLMRequest,
|
|
560
|
+
authorized: bool = Depends(verify_api_key)
|
|
561
|
+
):
|
|
562
|
+
"""Remote LLM completion endpoint - allows remote clients to use your LLMs"""
|
|
563
|
+
try:
|
|
564
|
+
from ..llm_adapter import LLMAdapter, TaskType
|
|
565
|
+
|
|
566
|
+
llm = LLMAdapter(skip_health_check=True)
|
|
567
|
+
|
|
568
|
+
# Map task type string to enum
|
|
569
|
+
task_type_map = {
|
|
570
|
+
"reasoning": TaskType.REASONING,
|
|
571
|
+
"coding": TaskType.CODING,
|
|
572
|
+
"tools": TaskType.TOOLS,
|
|
573
|
+
"general": TaskType.GENERAL,
|
|
574
|
+
"fast": TaskType.FAST
|
|
575
|
+
}
|
|
576
|
+
task_type = task_type_map.get(request.task_type, TaskType.GENERAL)
|
|
577
|
+
|
|
578
|
+
response = llm.complete(
|
|
579
|
+
prompt=request.prompt,
|
|
580
|
+
task_type=task_type,
|
|
581
|
+
system_prompt=request.system_prompt,
|
|
582
|
+
temperature=request.temperature,
|
|
583
|
+
max_tokens=request.max_tokens,
|
|
584
|
+
stream=False # Streaming not supported over HTTP yet
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
"response": response,
|
|
589
|
+
"model": llm.get_model_info(task_type),
|
|
590
|
+
"timestamp": datetime.now().isoformat()
|
|
591
|
+
}
|
|
592
|
+
except Exception as e:
|
|
593
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
594
|
+
|
|
595
|
+
@app.post("/api/remote/chat")
|
|
596
|
+
async def remote_chat(
|
|
597
|
+
request: ChatRequest,
|
|
598
|
+
authorized: bool = Depends(verify_api_key)
|
|
599
|
+
):
|
|
600
|
+
"""Remote chat endpoint - uses full reasoning engine (legacy)"""
|
|
601
|
+
reasoning = get_reasoning_engine()
|
|
602
|
+
if not reasoning:
|
|
603
|
+
raise HTTPException(status_code=503, detail="Reasoning engine not available")
|
|
604
|
+
|
|
605
|
+
try:
|
|
606
|
+
response = reasoning.process_request(request.message)
|
|
607
|
+
return {
|
|
608
|
+
"response": response,
|
|
609
|
+
"timestamp": datetime.now().isoformat()
|
|
610
|
+
}
|
|
611
|
+
except Exception as e:
|
|
612
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
613
|
+
|
|
614
|
+
# =========================================================================
|
|
615
|
+
# API Routes - Server-Side Vector Database (Code Intelligence)
|
|
616
|
+
# =========================================================================
|
|
617
|
+
|
|
618
|
+
@app.post("/api/remote/index")
|
|
619
|
+
async def remote_index_code(
|
|
620
|
+
request: IndexCodeRequest,
|
|
621
|
+
authorized: bool = Depends(verify_api_key)
|
|
622
|
+
):
|
|
623
|
+
"""
|
|
624
|
+
Index user's code in the server's vector database.
|
|
625
|
+
This allows the server to learn from code patterns and provide better assistance.
|
|
626
|
+
"""
|
|
627
|
+
vector_store = get_global_vector_store()
|
|
628
|
+
chunker = get_code_chunker()
|
|
629
|
+
|
|
630
|
+
if not vector_store or not chunker:
|
|
631
|
+
raise HTTPException(
|
|
632
|
+
status_code=503,
|
|
633
|
+
detail="Vector database not available. Install chromadb and sentence-transformers."
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
indexed_count = 0
|
|
638
|
+
chunk_count = 0
|
|
639
|
+
|
|
640
|
+
for file_info in request.files:
|
|
641
|
+
file_path = file_info.get("path", "unknown")
|
|
642
|
+
content = file_info.get("content", "")
|
|
643
|
+
language = file_info.get("language", "text")
|
|
644
|
+
|
|
645
|
+
if not content:
|
|
646
|
+
continue
|
|
647
|
+
|
|
648
|
+
# Chunk the code
|
|
649
|
+
chunks = chunker.chunk_code(content, language=language)
|
|
650
|
+
|
|
651
|
+
# Add each chunk to vector store with user_id prefix for isolation
|
|
652
|
+
for i, chunk in enumerate(chunks):
|
|
653
|
+
doc_id = f"{request.user_id}:{file_path}:{i}"
|
|
654
|
+
metadata = {
|
|
655
|
+
"user_id": request.user_id,
|
|
656
|
+
"file_path": file_path,
|
|
657
|
+
"language": language,
|
|
658
|
+
"chunk_index": i,
|
|
659
|
+
"project_name": request.project_name or "default",
|
|
660
|
+
"indexed_at": datetime.now().isoformat()
|
|
661
|
+
}
|
|
662
|
+
vector_store.add(
|
|
663
|
+
documents=[chunk],
|
|
664
|
+
ids=[doc_id],
|
|
665
|
+
metadatas=[metadata]
|
|
666
|
+
)
|
|
667
|
+
chunk_count += 1
|
|
668
|
+
|
|
669
|
+
indexed_count += 1
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
"status": "ok",
|
|
673
|
+
"files_indexed": indexed_count,
|
|
674
|
+
"chunks_created": chunk_count,
|
|
675
|
+
"user_id": request.user_id,
|
|
676
|
+
"timestamp": datetime.now().isoformat()
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
except Exception as e:
|
|
680
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
681
|
+
|
|
682
|
+
@app.post("/api/remote/search")
|
|
683
|
+
async def remote_search_code(
|
|
684
|
+
request: SearchCodeRequest,
|
|
685
|
+
authorized: bool = Depends(verify_api_key)
|
|
686
|
+
):
|
|
687
|
+
"""
|
|
688
|
+
Search user's indexed code using semantic similarity.
|
|
689
|
+
Only searches within the user's own indexed code.
|
|
690
|
+
"""
|
|
691
|
+
vector_store = get_global_vector_store()
|
|
692
|
+
|
|
693
|
+
if not vector_store:
|
|
694
|
+
raise HTTPException(
|
|
695
|
+
status_code=503,
|
|
696
|
+
detail="Vector database not available"
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
try:
|
|
700
|
+
# Build filter for user's code only
|
|
701
|
+
where_filter = {"user_id": request.user_id}
|
|
702
|
+
if request.project_name:
|
|
703
|
+
where_filter["project_name"] = request.project_name
|
|
704
|
+
|
|
705
|
+
# Search
|
|
706
|
+
results = vector_store.query(
|
|
707
|
+
query_texts=[request.query],
|
|
708
|
+
n_results=request.n_results,
|
|
709
|
+
where=where_filter
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
# Format results
|
|
713
|
+
formatted_results = []
|
|
714
|
+
if results and results.get("documents"):
|
|
715
|
+
docs = results["documents"][0] if results["documents"] else []
|
|
716
|
+
metadatas = results["metadatas"][0] if results.get("metadatas") else []
|
|
717
|
+
distances = results["distances"][0] if results.get("distances") else []
|
|
718
|
+
|
|
719
|
+
for i, doc in enumerate(docs):
|
|
720
|
+
formatted_results.append({
|
|
721
|
+
"content": doc,
|
|
722
|
+
"file_path": metadatas[i].get("file_path", "unknown") if i < len(metadatas) else "unknown",
|
|
723
|
+
"language": metadatas[i].get("language", "text") if i < len(metadatas) else "text",
|
|
724
|
+
"similarity": 1 - (distances[i] if i < len(distances) else 0), # Convert distance to similarity
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
return {
|
|
728
|
+
"query": request.query,
|
|
729
|
+
"results": formatted_results,
|
|
730
|
+
"count": len(formatted_results),
|
|
731
|
+
"timestamp": datetime.now().isoformat()
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
except Exception as e:
|
|
735
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
736
|
+
|
|
737
|
+
@app.get("/api/remote/index/stats")
|
|
738
|
+
async def remote_index_stats(
|
|
739
|
+
user_id: str,
|
|
740
|
+
authorized: bool = Depends(verify_api_key)
|
|
741
|
+
):
|
|
742
|
+
"""Get indexing statistics for a user"""
|
|
743
|
+
vector_store = get_global_vector_store()
|
|
744
|
+
|
|
745
|
+
if not vector_store:
|
|
746
|
+
return {"indexed": False, "error": "Vector database not available"}
|
|
747
|
+
|
|
748
|
+
try:
|
|
749
|
+
# Get count of documents for this user
|
|
750
|
+
# Note: This is a simplified implementation
|
|
751
|
+
return {
|
|
752
|
+
"user_id": user_id,
|
|
753
|
+
"indexed": True,
|
|
754
|
+
"status": "active",
|
|
755
|
+
"timestamp": datetime.now().isoformat()
|
|
756
|
+
}
|
|
757
|
+
except Exception as e:
|
|
758
|
+
return {"indexed": False, "error": str(e)}
|
|
759
|
+
|
|
760
|
+
@app.post("/api/remote/agent")
|
|
761
|
+
async def remote_agent_chat(
|
|
762
|
+
request: AgentChatRequest,
|
|
763
|
+
authorized: bool = Depends(verify_api_key)
|
|
764
|
+
):
|
|
765
|
+
"""
|
|
766
|
+
Agent chat endpoint - returns LLM response with tool calls for LOCAL execution.
|
|
767
|
+
|
|
768
|
+
This is the correct architecture:
|
|
769
|
+
1. Client sends conversation history + context
|
|
770
|
+
2. Server runs LLM to generate response (may include tool calls)
|
|
771
|
+
3. Server returns raw LLM response (does NOT execute tools)
|
|
772
|
+
4. Client parses tool calls and executes them LOCALLY
|
|
773
|
+
5. Client sends tool results back for next iteration
|
|
774
|
+
"""
|
|
775
|
+
try:
|
|
776
|
+
from ..llm_adapter import LLMAdapter
|
|
777
|
+
from ..prompts.unified_prompt import get_unified_prompt
|
|
778
|
+
|
|
779
|
+
llm = LLMAdapter(skip_health_check=True)
|
|
780
|
+
|
|
781
|
+
# Build unified system prompt - no keyword detection needed
|
|
782
|
+
# The LLM understands user intent from natural language
|
|
783
|
+
system_prompt = get_unified_prompt(request.cwd)
|
|
784
|
+
|
|
785
|
+
# Build messages - prepend system prompt
|
|
786
|
+
messages = [{"role": "system", "content": system_prompt}] + request.messages
|
|
787
|
+
|
|
788
|
+
# Get LLM response (just thinking, no execution)
|
|
789
|
+
response = llm.chat(messages)
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
"response": response,
|
|
793
|
+
"timestamp": datetime.now().isoformat(),
|
|
794
|
+
"type": "agent_response"
|
|
795
|
+
}
|
|
796
|
+
except Exception as e:
|
|
797
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
798
|
+
|
|
799
|
+
# =========================================================================
|
|
800
|
+
# WebSocket for real-time chat
|
|
801
|
+
# =========================================================================
|
|
802
|
+
|
|
803
|
+
@app.websocket("/ws/chat")
|
|
804
|
+
async def websocket_chat(websocket: WebSocket):
|
|
805
|
+
"""WebSocket endpoint for real-time chat"""
|
|
806
|
+
await websocket.accept()
|
|
807
|
+
|
|
808
|
+
reasoning = get_reasoning_engine()
|
|
809
|
+
|
|
810
|
+
try:
|
|
811
|
+
while True:
|
|
812
|
+
data = await websocket.receive_text()
|
|
813
|
+
message = json.loads(data)
|
|
814
|
+
|
|
815
|
+
if message.get("type") == "chat":
|
|
816
|
+
user_msg = message.get("content", "")
|
|
817
|
+
|
|
818
|
+
# Send acknowledgment
|
|
819
|
+
await websocket.send_json({
|
|
820
|
+
"type": "ack",
|
|
821
|
+
"content": user_msg
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
if reasoning:
|
|
825
|
+
context = {"cwd": str(Path.cwd()), "task_type": "general"}
|
|
826
|
+
response = reasoning.process_request(user_msg, context)
|
|
827
|
+
|
|
828
|
+
await websocket.send_json({
|
|
829
|
+
"type": "response",
|
|
830
|
+
"content": response,
|
|
831
|
+
"timestamp": datetime.now().isoformat()
|
|
832
|
+
})
|
|
833
|
+
else:
|
|
834
|
+
await websocket.send_json({
|
|
835
|
+
"type": "error",
|
|
836
|
+
"content": "Reasoning engine not available"
|
|
837
|
+
})
|
|
838
|
+
|
|
839
|
+
except WebSocketDisconnect:
|
|
840
|
+
pass
|
|
841
|
+
except Exception as e:
|
|
842
|
+
await websocket.send_json({
|
|
843
|
+
"type": "error",
|
|
844
|
+
"content": str(e)
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
return app
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def get_fallback_html() -> str:
|
|
851
|
+
"""Get fallback HTML if template not found"""
|
|
852
|
+
return """
|
|
853
|
+
<!DOCTYPE html>
|
|
854
|
+
<html lang="en">
|
|
855
|
+
<head>
|
|
856
|
+
<meta charset="UTF-8">
|
|
857
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
858
|
+
<title>NC1709 Dashboard</title>
|
|
859
|
+
<style>
|
|
860
|
+
body {
|
|
861
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
862
|
+
background: #1a1a2e;
|
|
863
|
+
color: #eee;
|
|
864
|
+
margin: 0;
|
|
865
|
+
padding: 20px;
|
|
866
|
+
display: flex;
|
|
867
|
+
justify-content: center;
|
|
868
|
+
align-items: center;
|
|
869
|
+
min-height: 100vh;
|
|
870
|
+
}
|
|
871
|
+
.container {
|
|
872
|
+
text-align: center;
|
|
873
|
+
}
|
|
874
|
+
h1 { color: #00d9ff; }
|
|
875
|
+
p { color: #888; }
|
|
876
|
+
a { color: #00d9ff; }
|
|
877
|
+
</style>
|
|
878
|
+
</head>
|
|
879
|
+
<body>
|
|
880
|
+
<div class="container">
|
|
881
|
+
<h1>NC1709 Dashboard</h1>
|
|
882
|
+
<p>Dashboard is loading...</p>
|
|
883
|
+
<p>If this message persists, the frontend assets may not be installed.</p>
|
|
884
|
+
<p><a href="/api/status">Check API Status</a></p>
|
|
885
|
+
</div>
|
|
886
|
+
</body>
|
|
887
|
+
</html>
|
|
888
|
+
"""
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def run_server(host: str = "127.0.0.1", port: int = 8709, reload: bool = False, serve_remote: bool = False):
|
|
892
|
+
"""Run the web server
|
|
893
|
+
|
|
894
|
+
Args:
|
|
895
|
+
host: Host to bind to
|
|
896
|
+
port: Port to bind to
|
|
897
|
+
reload: Enable auto-reload for development
|
|
898
|
+
serve_remote: If True, bind to 0.0.0.0 for remote access
|
|
899
|
+
"""
|
|
900
|
+
import uvicorn
|
|
901
|
+
|
|
902
|
+
# If serving remote, bind to all interfaces
|
|
903
|
+
if serve_remote:
|
|
904
|
+
host = "0.0.0.0"
|
|
905
|
+
|
|
906
|
+
if serve_remote:
|
|
907
|
+
print(f"""
|
|
908
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
909
|
+
║ ║
|
|
910
|
+
║ NC1709 Remote Server ║
|
|
911
|
+
║ ║
|
|
912
|
+
║ 🌐 API Server running on port {port} ║
|
|
913
|
+
║ 📁 Project: {str(Path.cwd())[:40]}
|
|
914
|
+
║ ║
|
|
915
|
+
║ Remote clients can connect using: ║
|
|
916
|
+
║ NC1709_API_URL=http://YOUR_IP:{port} ║
|
|
917
|
+
║ ║
|
|
918
|
+
║ For public access, use a tunnel like ngrok: ║
|
|
919
|
+
║ ngrok http {port} ║
|
|
920
|
+
║ ║
|
|
921
|
+
║ Press Ctrl+C to stop ║
|
|
922
|
+
║ ║
|
|
923
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
924
|
+
""")
|
|
925
|
+
else:
|
|
926
|
+
print(f"""
|
|
927
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
928
|
+
║ ║
|
|
929
|
+
║ NC1709 Web Dashboard ║
|
|
930
|
+
║ ║
|
|
931
|
+
║ 🌐 Running at: http://{host}:{port} ║
|
|
932
|
+
║ 📁 Project: {str(Path.cwd())[:40]}
|
|
933
|
+
║ ║
|
|
934
|
+
║ Press Ctrl+C to stop ║
|
|
935
|
+
║ ║
|
|
936
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
937
|
+
""")
|
|
938
|
+
|
|
939
|
+
uvicorn.run(
|
|
940
|
+
"nc1709.web.server:create_app",
|
|
941
|
+
host=host,
|
|
942
|
+
port=port,
|
|
943
|
+
reload=reload,
|
|
944
|
+
factory=True,
|
|
945
|
+
log_level="info"
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
if __name__ == "__main__":
|
|
950
|
+
run_server()
|