mindroot 9.2.0__py3-none-any.whl → 9.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mindroot/coreplugins/admin/__init__.py +3 -1
- mindroot/coreplugins/admin/agent_router.py +250 -7
- mindroot/coreplugins/admin/asset_manager.py +164 -0
- mindroot/coreplugins/admin/command_router.py +236 -1
- mindroot/coreplugins/admin/mcp_catalog_routes.py +156 -0
- mindroot/coreplugins/admin/mcp_publish_routes.py +450 -0
- mindroot/coreplugins/admin/mcp_registry_routes.py +495 -0
- mindroot/coreplugins/admin/mcp_routes.py +216 -0
- mindroot/coreplugins/admin/mod.py +62 -0
- mindroot/coreplugins/admin/oauth_callback_router.py +84 -0
- mindroot/coreplugins/admin/persona_handler.py +15 -6
- mindroot/coreplugins/admin/persona_router.py +158 -2
- mindroot/coreplugins/admin/plugin_manager.py +63 -0
- mindroot/coreplugins/admin/plugin_router.py +1 -1
- mindroot/coreplugins/admin/plugin_router_fixed.py +23 -0
- mindroot/coreplugins/admin/plugin_router_new_not_working.py +145 -0
- mindroot/coreplugins/admin/plugin_routes.py +114 -0
- mindroot/coreplugins/admin/registry_settings_routes.py +140 -0
- mindroot/coreplugins/admin/router.py +116 -15
- mindroot/coreplugins/admin/service_models.py +1 -1
- mindroot/coreplugins/admin/settings_router.py +1 -0
- mindroot/coreplugins/admin/static/css/admin-custom.css +357 -2
- mindroot/coreplugins/admin/static/css/dark.css +1 -0
- mindroot/coreplugins/admin/static/css/default.css +4 -0
- mindroot/coreplugins/admin/static/js/about-info.js +367 -0
- mindroot/coreplugins/admin/static/js/agent-form.js +83 -3
- mindroot/coreplugins/admin/static/js/api-key-script.js +307 -0
- mindroot/coreplugins/admin/static/js/mcp-manager.js +348 -0
- mindroot/coreplugins/admin/static/js/mcp-publisher.js +780 -0
- mindroot/coreplugins/admin/static/js/persona-editor.js +34 -5
- mindroot/coreplugins/admin/static/js/plugin-toggle.js +1 -1
- mindroot/coreplugins/admin/static/js/recommended-plugin-install.js +63 -0
- mindroot/coreplugins/admin/static/js/registry-auth-section.js +132 -0
- mindroot/coreplugins/admin/static/js/registry-manager-base.js +613 -0
- mindroot/coreplugins/admin/static/js/registry-manager-old.js +385 -0
- mindroot/coreplugins/admin/static/js/registry-manager-publish-old-delete.js +166 -0
- mindroot/coreplugins/admin/static/js/registry-manager.js +351 -0
- mindroot/coreplugins/admin/static/js/registry-publish-section.js +377 -0
- mindroot/coreplugins/admin/static/js/registry-search-section.js +400 -0
- mindroot/coreplugins/admin/static/js/registry-search-section.js.bak +3 -0
- mindroot/coreplugins/admin/static/js/registry-settings.js +69 -0
- mindroot/coreplugins/admin/static/js/registry-shared-services.js +857 -0
- mindroot/coreplugins/admin/static/js/registry-simple-sections.js +85 -0
- mindroot/coreplugins/admin/static/js/secure-widget-manager.js +438 -0
- mindroot/coreplugins/admin/static/logo.png +0 -0
- mindroot/coreplugins/admin/templates/admin.jinja2 +275 -110
- mindroot/coreplugins/agent/Assistant/agent.json +27 -11
- mindroot/coreplugins/agent/agent.py +2 -2
- mindroot/coreplugins/agent/command_parser.py +25 -10
- mindroot/coreplugins/agent/templates/system.jinja2 +0 -12
- mindroot/coreplugins/chat/__init__.py +4 -1
- mindroot/coreplugins/chat/router.py +132 -20
- mindroot/coreplugins/chat/router_dedup_patch.py +20 -0
- mindroot/coreplugins/chat/services.py +31 -1
- mindroot/coreplugins/chat/static/css/action-fix.css +32 -0
- mindroot/coreplugins/chat/static/css/admin-custom.css +5 -3
- mindroot/coreplugins/chat/static/css/dark.css +24 -3
- mindroot/coreplugins/chat/static/css/default.css +24 -3
- mindroot/coreplugins/chat/static/css/main.css +1 -0
- mindroot/coreplugins/chat/static/js/action.js +137 -60
- mindroot/coreplugins/chat/static/js/chat-history.js +3 -0
- mindroot/coreplugins/chat/static/js/chat.js +59 -16
- mindroot/coreplugins/chat/static/js/chat.js.diff +221 -0
- mindroot/coreplugins/chat/static/js/chatform.js +2 -2
- mindroot/coreplugins/chat/static/site.webmanifest +1 -1
- mindroot/coreplugins/chat/templates/chat.jinja2 +3 -3
- mindroot/coreplugins/chat/widget_manager.py +139 -0
- mindroot/coreplugins/chat/widget_routes.py +287 -0
- mindroot/coreplugins/check_list/inject/admin.jinja2 +1 -1
- mindroot/coreplugins/email/__init__.py +2 -0
- mindroot/coreplugins/email/email_provider.py +2 -2
- mindroot/coreplugins/email/mod.py +100 -0
- mindroot/coreplugins/email/services.py +5 -3
- mindroot/coreplugins/email/smtp_handler.py +9 -3
- mindroot/coreplugins/email/test_email_service.py +75 -0
- mindroot/coreplugins/env_manager/mod.py +61 -25
- mindroot/coreplugins/home/router.py +37 -2
- mindroot/coreplugins/home/static/imgs/logo.png +0 -0
- mindroot/coreplugins/home/static/imgs/logo.png.bak +0 -0
- mindroot/coreplugins/home/static/imgs/logo_teal.png +0 -0
- mindroot/coreplugins/home/static/imgs/logo_teal2.png +0 -0
- mindroot/coreplugins/home/static/imgs/logo_teal_detailed.png +0 -0
- mindroot/coreplugins/home/static/imgs/logo_teal_python.png +0 -0
- mindroot/coreplugins/home/templates/home.jinja2 +15 -6
- mindroot/coreplugins/index/handlers/plugin_ops.py +1 -1
- mindroot/coreplugins/index/indices/default/index.json +6 -6
- mindroot/coreplugins/jwt_auth/middleware.py +47 -1
- mindroot/coreplugins/jwt_auth/mod.py +40 -17
- mindroot/coreplugins/l8n/__init__.py +6 -0
- mindroot/coreplugins/l8n/debug_loader.py +85 -0
- mindroot/coreplugins/l8n/debug_middleware.py +74 -0
- mindroot/coreplugins/l8n/l8n_constants.py +19 -0
- mindroot/coreplugins/l8n/language_detection.py +183 -0
- mindroot/coreplugins/l8n/middleware.py +151 -0
- mindroot/coreplugins/l8n/mod.py +277 -0
- mindroot/coreplugins/l8n/monkey_patch_to_delete.py +186 -0
- mindroot/coreplugins/l8n/test_enhanced.py +298 -0
- mindroot/coreplugins/l8n/test_l8n.py +95 -0
- mindroot/coreplugins/l8n/test_l8n_standalone.py +251 -0
- mindroot/coreplugins/l8n/test_middleware.py +272 -0
- mindroot/coreplugins/l8n/utils.py +232 -0
- mindroot/coreplugins/mcp_/__init__.py +14 -0
- mindroot/coreplugins/mcp_/catalog_commands.py +328 -0
- mindroot/coreplugins/mcp_/catalog_manager.py +263 -0
- mindroot/coreplugins/mcp_/dynamic_commands.py +154 -0
- mindroot/coreplugins/mcp_/mcp_manager.py +1031 -0
- mindroot/coreplugins/mcp_/mod.py +367 -0
- mindroot/coreplugins/mcp_/oauth_storage.py +144 -0
- mindroot/coreplugins/mcp_/server_installer.py +79 -0
- mindroot/coreplugins/mcp_/setup.py +26 -0
- mindroot/coreplugins/mcp_/test_dynamic_commands.py +134 -0
- mindroot/coreplugins/mcp_/testmcpclient.py +92 -0
- mindroot/coreplugins/persona/mod.py +12 -7
- mindroot/coreplugins/signup/templates/signup.jinja2 +1 -1
- mindroot/coreplugins/subscriptions/__init__.py +1 -0
- mindroot/coreplugins/subscriptions/mod.py +14 -3
- mindroot/coreplugins/subscriptions/router.py +3 -0
- mindroot/coreplugins/user_service/__init__.py +1 -2
- mindroot/coreplugins/user_service/admin_init.py +1 -0
- mindroot/coreplugins/user_service/email_service.py +72 -17
- mindroot/coreplugins/user_service/mod.py +10 -2
- mindroot/coreplugins/user_service/password_reset_service.py +180 -27
- mindroot/coreplugins/user_service/router.py +84 -22
- mindroot/lib/auth/api_key.py +28 -0
- mindroot/lib/cli/plugins.py +94 -0
- mindroot/lib/plugins/default_plugin_manifest.json +20 -0
- mindroot/lib/plugins/installation.py +5 -5
- mindroot/lib/plugins/l8n_static_handler.py +225 -0
- mindroot/lib/plugins/loader.py +33 -3
- mindroot/lib/plugins/loader_with_l8n.py +281 -0
- mindroot/lib/plugins/manifest.py +238 -17
- mindroot/lib/providers/commands.py +3 -1
- mindroot/lib/route_decorators.py +5 -5
- mindroot/lib/templates.py +183 -11
- mindroot/lib/utils/merge_arrays.py +1 -1
- mindroot/migrate.py +49 -0
- mindroot/registry/data_access.py +1 -1
- mindroot/server.py +47 -13
- mindroot/server_missing_normal_args.py +197 -0
- mindroot/server_prev.py +173 -0
- {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/METADATA +7 -2
- {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/RECORD +147 -114
- mindroot/coreplugins/admin/static/favicon/about.txt +0 -6
- mindroot/coreplugins/admin/static/favicon/android-chrome-512x512.png +0 -0
- mindroot/coreplugins/admin/static/favicon/apple-touch-icon.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon-16x16.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon-32x32.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon.ico +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/about.txt +0 -6
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/android-chrome-192x192.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/android-chrome-512x512.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/apple-touch-icon.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon-16x16.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon-32x32.png +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon.ico +0 -0
- mindroot/coreplugins/admin/static/favicon/favicon_io (1)/site.webmanifest +0 -1
- mindroot/coreplugins/admin/static/favicon/logo.png +0 -0
- mindroot/coreplugins/admin/static/favicon/site.webmanifest +0 -1
- mindroot/coreplugins/admin/static/js/backup/agent-editor.js +0 -186
- mindroot/coreplugins/admin/static/js/backup/agent-form.js +0 -1133
- mindroot/coreplugins/admin/static/js/backup/agent-list.js +0 -94
- mindroot/coreplugins/chat/static/favicon/about.txt +0 -6
- mindroot/coreplugins/chat/static/favicon/android-chrome-192x192.png +0 -0
- mindroot/coreplugins/chat/static/favicon/android-chrome-512x512.png +0 -0
- mindroot/coreplugins/chat/static/favicon/apple-touch-icon.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon-16x16.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon-32x32.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon.ico +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/about.txt +0 -6
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/android-chrome-192x192.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/android-chrome-512x512.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/apple-touch-icon.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon-16x16.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon-32x32.png +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon.ico +0 -0
- mindroot/coreplugins/chat/static/favicon/favicon_io (1)/site.webmanifest +0 -1
- mindroot/coreplugins/chat/static/favicon/logo.png +0 -0
- mindroot/coreplugins/chat/static/favicon/site.webmanifest +0 -1
- mindroot/coreplugins/index/default.json +0 -76
- mindroot/coreplugins/user_service/file_trigger_service.py +0 -72
- mindroot/coreplugins/user_service/hooks.py +0 -23
- /mindroot/coreplugins/{admin/static/favicon/android-chrome-192x192.png → home/static/imgs/backuplogo.png} +0 -0
- {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/WHEEL +0 -0
- {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/entry_points.txt +0 -0
- {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/licenses/LICENSE +0 -0
- {mindroot-9.2.0.dist-info → mindroot-9.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
import os
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from typing import Optional, List, Dict, Any
|
|
5
|
+
from lib.route_decorators import requires_role
|
|
6
|
+
|
|
7
|
+
# Import MCP components - prefer new mcp_ module, fallback to legacy if needed
|
|
8
|
+
try:
|
|
9
|
+
from mindroot.coreplugins.mcp_.mod import mcp_manager, MCPServer
|
|
10
|
+
except ImportError:
|
|
11
|
+
try:
|
|
12
|
+
from mindroot.coreplugins.mcp.mod import mcp_manager, MCPServer
|
|
13
|
+
except ImportError:
|
|
14
|
+
# Mock objects if MCP plugin is not fully installed, to prevent startup crash
|
|
15
|
+
mcp_manager = None
|
|
16
|
+
MCPServer = None
|
|
17
|
+
|
|
18
|
+
# Create router with admin role requirement
|
|
19
|
+
router = APIRouter(
|
|
20
|
+
dependencies=[requires_role('admin')]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
class McpLocalTestRequest(BaseModel):
|
|
24
|
+
name: str
|
|
25
|
+
command: str
|
|
26
|
+
args: List[str] = []
|
|
27
|
+
env: Dict[str, str] = {}
|
|
28
|
+
secrets: Optional[Dict[str, str]] = None
|
|
29
|
+
|
|
30
|
+
class McpServerRequest(BaseModel):
|
|
31
|
+
server_name: str
|
|
32
|
+
|
|
33
|
+
class McpConnectRequest(BaseModel):
|
|
34
|
+
server_name: str
|
|
35
|
+
secrets: Optional[Dict[str, str]] = None
|
|
36
|
+
|
|
37
|
+
class McpServerAddRequest(BaseModel):
|
|
38
|
+
name: str
|
|
39
|
+
description: str
|
|
40
|
+
command: Optional[str] = None
|
|
41
|
+
args: List[str] = []
|
|
42
|
+
env: dict = {}
|
|
43
|
+
transport: str = "stdio"
|
|
44
|
+
url: Optional[str] = None
|
|
45
|
+
# New URL fields
|
|
46
|
+
provider_url: Optional[str] = None
|
|
47
|
+
transport_url: Optional[str] = None
|
|
48
|
+
transport_type: Optional[str] = None # "sse" | "streamable_http"
|
|
49
|
+
# OAuth 2.0 fields
|
|
50
|
+
auth_type: str = "none"
|
|
51
|
+
auth_headers: Dict[str, str] = {}
|
|
52
|
+
authorization_server_url: Optional[str] = None
|
|
53
|
+
client_id: Optional[str] = None
|
|
54
|
+
client_secret: Optional[str] = None
|
|
55
|
+
scopes: List[str] = []
|
|
56
|
+
redirect_uri: Optional[str] = None
|
|
57
|
+
|
|
58
|
+
class McpOAuthCallbackRequest(BaseModel):
|
|
59
|
+
server_name: str
|
|
60
|
+
code: str
|
|
61
|
+
state: Optional[str] = None
|
|
62
|
+
|
|
63
|
+
# --- MCP Server Management Routes ---
|
|
64
|
+
|
|
65
|
+
@router.get("/mcp/list")
|
|
66
|
+
async def list_mcp_servers():
|
|
67
|
+
"""List all configured MCP servers."""
|
|
68
|
+
if not mcp_manager:
|
|
69
|
+
raise HTTPException(status_code=501, detail="MCP Plugin not available")
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
servers = []
|
|
73
|
+
for name, server in mcp_manager.servers.items():
|
|
74
|
+
servers.append({
|
|
75
|
+
"name": name,
|
|
76
|
+
"description": server.description,
|
|
77
|
+
"status": server.status,
|
|
78
|
+
"transport": server.transport,
|
|
79
|
+
"command": server.command,
|
|
80
|
+
"args": server.args,
|
|
81
|
+
"capabilities": server.capabilities
|
|
82
|
+
})
|
|
83
|
+
return {"success": True, "data": servers}
|
|
84
|
+
except Exception as e:
|
|
85
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
86
|
+
|
|
87
|
+
@router.post("/mcp/add")
|
|
88
|
+
async def add_mcp_server(server_request: McpServerAddRequest):
|
|
89
|
+
"""Add a new MCP server configuration."""
|
|
90
|
+
if not mcp_manager:
|
|
91
|
+
raise HTTPException(status_code=501, detail="MCP Plugin not available")
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
# Create MCPServer object
|
|
95
|
+
# Validate required fields based on transport type
|
|
96
|
+
if server_request.transport == "stdio" and not server_request.command:
|
|
97
|
+
raise HTTPException(status_code=400, detail="Command is required for stdio transport")
|
|
98
|
+
|
|
99
|
+
if server_request.transport in ["http", "sse", "websocket"] and not server_request.url:
|
|
100
|
+
raise HTTPException(status_code=400, detail="URL is required for remote transports")
|
|
101
|
+
|
|
102
|
+
# More flexible OAuth validation - allow registry servers to be added without client_id initially
|
|
103
|
+
# The OAuth configuration might be provided by the registry or discovered during connection
|
|
104
|
+
if server_request.auth_type == "oauth2":
|
|
105
|
+
# Log the OAuth configuration for debugging
|
|
106
|
+
print(f"DEBUG: Adding OAuth server '{server_request.name}'")
|
|
107
|
+
print(f"DEBUG: client_id: {server_request.client_id}")
|
|
108
|
+
print(f"DEBUG: authorization_server_url: {server_request.authorization_server_url}")
|
|
109
|
+
print(f"DEBUG: scopes: {server_request.scopes}")
|
|
110
|
+
# Note: client_id validation removed to allow registry servers with dynamic OAuth discovery
|
|
111
|
+
|
|
112
|
+
server = MCPServer(
|
|
113
|
+
name=server_request.name,
|
|
114
|
+
description=server_request.description,
|
|
115
|
+
command=server_request.command or "",
|
|
116
|
+
args=server_request.args,
|
|
117
|
+
env=server_request.env,
|
|
118
|
+
transport=server_request.transport,
|
|
119
|
+
url=server_request.url,
|
|
120
|
+
auth_type=server_request.auth_type,
|
|
121
|
+
auth_headers=server_request.auth_headers,
|
|
122
|
+
authorization_server_url=server_request.authorization_server_url,
|
|
123
|
+
client_id=server_request.client_id,
|
|
124
|
+
client_secret=server_request.client_secret,
|
|
125
|
+
scopes=server_request.scopes,
|
|
126
|
+
redirect_uri=server_request.redirect_uri or f"{server_request.url}/oauth/callback" if server_request.url else None
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Use BASE_URL for redirect_uri if not explicitly provided
|
|
130
|
+
if not server.redirect_uri and server_request.auth_type == "oauth2":
|
|
131
|
+
base_url = os.getenv('BASE_URL', 'http://localhost:3000')
|
|
132
|
+
server.redirect_uri = f"{base_url}/mcp_oauth_cb"
|
|
133
|
+
|
|
134
|
+
mcp_manager.add_server(server_request.name, server)
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
"success": True,
|
|
138
|
+
"message": f"MCP server '{server_request.name}' added successfully."
|
|
139
|
+
}
|
|
140
|
+
except Exception as e:
|
|
141
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
142
|
+
|
|
143
|
+
@router.post("/mcp/remove")
|
|
144
|
+
async def remove_mcp_server(request: McpServerRequest):
|
|
145
|
+
"""Remove an MCP server configuration."""
|
|
146
|
+
if not mcp_manager:
|
|
147
|
+
raise HTTPException(status_code=501, detail="MCP Plugin not available")
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
# First disconnect if connected
|
|
151
|
+
if request.server_name in mcp_manager.sessions:
|
|
152
|
+
await mcp_manager.disconnect_server(request.server_name)
|
|
153
|
+
|
|
154
|
+
# Remove server configuration
|
|
155
|
+
mcp_manager.remove_server(request.server_name)
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"success": True,
|
|
159
|
+
"message": f"MCP server '{request.server_name}' removed successfully."
|
|
160
|
+
}
|
|
161
|
+
except Exception as e:
|
|
162
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
163
|
+
|
|
164
|
+
@router.post("/mcp/connect")
|
|
165
|
+
async def connect_mcp_server(request: McpConnectRequest):
|
|
166
|
+
"""Connect to an MCP server."""
|
|
167
|
+
if not mcp_manager:
|
|
168
|
+
raise HTTPException(status_code=501, detail="MCP Plugin not available")
|
|
169
|
+
|
|
170
|
+
# Persist secrets if provided
|
|
171
|
+
if request.secrets:
|
|
172
|
+
if request.server_name in mcp_manager.servers:
|
|
173
|
+
server = mcp_manager.servers[request.server_name]
|
|
174
|
+
if server.secrets is None:
|
|
175
|
+
server.secrets = {}
|
|
176
|
+
server.secrets.update(request.secrets)
|
|
177
|
+
# This will save the updated server config, including secrets
|
|
178
|
+
mcp_manager.save_config()
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
success = await mcp_manager.connect_server(request.server_name, secrets=request.secrets)
|
|
182
|
+
if success:
|
|
183
|
+
return {
|
|
184
|
+
"success": True,
|
|
185
|
+
"message": f"MCP server '{request.server_name}' connected successfully."
|
|
186
|
+
}
|
|
187
|
+
else:
|
|
188
|
+
print(f"DEBUG: Failed to connect to MCP server '{request.server_name}'")
|
|
189
|
+
raise HTTPException(
|
|
190
|
+
status_code=500,
|
|
191
|
+
detail=f"Failed to connect to MCP server '{request.server_name}'. Check logs for details."
|
|
192
|
+
)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@router.post("/mcp/disconnect")
|
|
198
|
+
async def disconnect_mcp_server(request: McpServerRequest):
|
|
199
|
+
"""Disconnect from an MCP server."""
|
|
200
|
+
if not mcp_manager:
|
|
201
|
+
raise HTTPException(status_code=501, detail="MCP Plugin not available")
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
success = await mcp_manager.disconnect_server(request.server_name)
|
|
205
|
+
if success:
|
|
206
|
+
return {
|
|
207
|
+
"success": True,
|
|
208
|
+
"message": f"MCP server '{request.server_name}' disconnected successfully."
|
|
209
|
+
}
|
|
210
|
+
else:
|
|
211
|
+
raise HTTPException(
|
|
212
|
+
status_code=500,
|
|
213
|
+
detail=f"Failed to disconnect from MCP server '{request.server_name}'."
|
|
214
|
+
)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
async def get_git_version_info(context=None):
|
|
6
|
+
"""Get git commit hash and date of last commit.
|
|
7
|
+
|
|
8
|
+
Returns a dictionary with commit hash and date, or None if not in a git repo.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
{ "get_git_version_info": {} }
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
# Get the current working directory or use a default path
|
|
15
|
+
repo_path = os.getcwd()
|
|
16
|
+
if '/files/mindroot' in repo_path or repo_path.endswith('mindroot'):
|
|
17
|
+
# We're likely in the right place
|
|
18
|
+
pass
|
|
19
|
+
else:
|
|
20
|
+
# Try to find mindroot directory
|
|
21
|
+
if os.path.exists('/files/mindroot'):
|
|
22
|
+
repo_path = '/files/mindroot'
|
|
23
|
+
else:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
# Get commit hash
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
['git', 'rev-parse', 'HEAD'],
|
|
29
|
+
cwd=repo_path,
|
|
30
|
+
capture_output=True,
|
|
31
|
+
text=True,
|
|
32
|
+
timeout=10
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if result.returncode != 0:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
commit_hash = result.stdout.strip()
|
|
39
|
+
|
|
40
|
+
# Get commit date
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
['git', 'log', '-1', '--format=%ci'],
|
|
43
|
+
cwd=repo_path,
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
timeout=10
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if result.returncode != 0:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
commit_date = result.stdout.strip()
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
'commit_hash': commit_hash,
|
|
56
|
+
'commit_date': commit_date,
|
|
57
|
+
'retrieved_at': datetime.now().isoformat()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
return None
|
|
62
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""OAuth callback router for MCP servers.
|
|
2
|
+
|
|
3
|
+
This router contains public routes that do not require authentication,
|
|
4
|
+
as external OAuth providers need to be able to redirect to these endpoints.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Request
|
|
8
|
+
from fastapi.responses import HTMLResponse
|
|
9
|
+
from lib.route_decorators import public_route
|
|
10
|
+
|
|
11
|
+
# Create router without dependencies - routes will be public
|
|
12
|
+
router = APIRouter()
|
|
13
|
+
|
|
14
|
+
@router.get("/mcp_oauth_cb")
|
|
15
|
+
@public_route()
|
|
16
|
+
async def mcp_oauth_callback(request: Request):
|
|
17
|
+
"""Handle OAuth callback for MCP servers.
|
|
18
|
+
|
|
19
|
+
This endpoint must be publicly accessible as external OAuth providers
|
|
20
|
+
will redirect to it without any authentication.
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
# Get query parameters
|
|
24
|
+
code = request.query_params.get('code')
|
|
25
|
+
state = request.query_params.get('state')
|
|
26
|
+
error = request.query_params.get('error')
|
|
27
|
+
|
|
28
|
+
if error:
|
|
29
|
+
# OAuth error occurred - avoid f-string issues
|
|
30
|
+
error_html = (
|
|
31
|
+
"<html><body>"
|
|
32
|
+
"<h2>OAuth Authorization Failed</h2>"
|
|
33
|
+
f"<p>Error: {error}</p>"
|
|
34
|
+
"<p>You can close this window.</p>"
|
|
35
|
+
"<script>window.close();</script>"
|
|
36
|
+
"</body></html>"
|
|
37
|
+
)
|
|
38
|
+
return HTMLResponse(error_html)
|
|
39
|
+
|
|
40
|
+
if code:
|
|
41
|
+
# Success - show completion page
|
|
42
|
+
state_value = state or ""
|
|
43
|
+
# Build HTML without f-string to avoid escaping issues
|
|
44
|
+
success_html = (
|
|
45
|
+
"<html><body>"
|
|
46
|
+
"<h2>OAuth Authorization Successful</h2>"
|
|
47
|
+
"<p>Authorization code received. You can close this window.</p>"
|
|
48
|
+
"<script>"
|
|
49
|
+
"if (window.opener) {"
|
|
50
|
+
"window.opener.postMessage({"
|
|
51
|
+
"type: 'oauth_callback',"
|
|
52
|
+
f"code: '{code}',"
|
|
53
|
+
f"state: '{state_value}'"
|
|
54
|
+
"}, '*');"
|
|
55
|
+
"}"
|
|
56
|
+
"setTimeout(() => window.close(), 2000);"
|
|
57
|
+
"</script>"
|
|
58
|
+
"</body></html>"
|
|
59
|
+
)
|
|
60
|
+
return HTMLResponse(success_html)
|
|
61
|
+
|
|
62
|
+
# No code or error - invalid callback
|
|
63
|
+
invalid_html = (
|
|
64
|
+
"<html><body>"
|
|
65
|
+
"<h2>Invalid OAuth Callback</h2>"
|
|
66
|
+
"<p>Missing authorization code or error parameter.</p>"
|
|
67
|
+
"<p>You can close this window.</p>"
|
|
68
|
+
"<script>window.close();</script>"
|
|
69
|
+
"</body></html>"
|
|
70
|
+
)
|
|
71
|
+
return HTMLResponse(invalid_html)
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
# Error handling - escape special characters
|
|
75
|
+
error_message = str(e).replace('"', '"').replace("'", "'")
|
|
76
|
+
error_html = (
|
|
77
|
+
"<html><body>"
|
|
78
|
+
"<h2>OAuth Callback Error</h2>"
|
|
79
|
+
f"<p>An error occurred processing the OAuth callback: {error_message}</p>"
|
|
80
|
+
"<p>You can close this window.</p>"
|
|
81
|
+
"<script>window.close();</script>"
|
|
82
|
+
"</body></html>"
|
|
83
|
+
)
|
|
84
|
+
return HTMLResponse(error_html)
|
|
@@ -27,7 +27,7 @@ def import_persona_from_index(index: str, persona: str):
|
|
|
27
27
|
print("Successfully imported persona", persona, "from index", index)
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def handle_persona_import(persona_data: dict, scope: str) -> str:
|
|
30
|
+
def handle_persona_import(persona_data: dict, scope: str, owner: str = None) -> str:
|
|
31
31
|
"""Handle importing a persona from embedded data in agent configuration.
|
|
32
32
|
Returns the persona name to be used in agent configuration.
|
|
33
33
|
|
|
@@ -57,13 +57,22 @@ def handle_persona_import(persona_data: dict, scope: str) -> str:
|
|
|
57
57
|
detail='Persona name required in persona data'
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
-
#
|
|
61
|
-
|
|
60
|
+
# For registry installs, use namespaced path
|
|
61
|
+
if owner and scope == 'registry':
|
|
62
|
+
persona_path = Path(f'personas/registry/{owner}/{persona_name}/persona.json')
|
|
63
|
+
return_name = f'registry/{owner}/{persona_name}'
|
|
64
|
+
else:
|
|
65
|
+
persona_path = Path('personas') / scope / persona_name / 'persona.json'
|
|
66
|
+
return_name = persona_name
|
|
62
67
|
|
|
63
68
|
# Check if persona already exists
|
|
64
69
|
if persona_path.exists():
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
# For registry personas with owner, we should allow overwrite to support updates
|
|
71
|
+
if owner and scope == 'registry':
|
|
72
|
+
logger.info(f"Overwriting existing registry persona '{persona_name}' for owner '{owner}'")
|
|
73
|
+
else:
|
|
74
|
+
logger.warning(f"Persona '{persona_name}' already exists in {scope} scope - skipping import")
|
|
75
|
+
return return_name
|
|
67
76
|
|
|
68
77
|
try:
|
|
69
78
|
# Create persona directory and save data
|
|
@@ -72,7 +81,7 @@ def handle_persona_import(persona_data: dict, scope: str) -> str:
|
|
|
72
81
|
json.dump(persona_data, f, indent=2)
|
|
73
82
|
|
|
74
83
|
logger.info(f"Successfully imported persona '{persona_name}' to {scope} scope")
|
|
75
|
-
return
|
|
84
|
+
return return_name
|
|
76
85
|
|
|
77
86
|
except Exception as e:
|
|
78
87
|
logger.error(f"Failed to import persona '{persona_name}': {str(e)}")
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
|
|
2
|
+
from fastapi.responses import Response
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
import json
|
|
4
5
|
import shutil
|
|
5
6
|
import traceback
|
|
7
|
+
from .asset_manager import asset_manager
|
|
6
8
|
|
|
7
9
|
router = APIRouter()
|
|
8
10
|
|
|
@@ -12,9 +14,13 @@ shared_dir = BASE_DIR / "shared"
|
|
|
12
14
|
local_dir.mkdir(parents=True, exist_ok=True)
|
|
13
15
|
shared_dir.mkdir(parents=True, exist_ok=True)
|
|
14
16
|
|
|
17
|
+
# Registry directory for namespaced personas
|
|
18
|
+
registry_dir = BASE_DIR / "registry"
|
|
19
|
+
registry_dir.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
|
|
15
21
|
@router.get('/personas/{scope}/{name}')
|
|
16
22
|
def read_persona(scope: str, name: str):
|
|
17
|
-
if scope not in ['local', 'shared']:
|
|
23
|
+
if scope not in ['local', 'shared', 'registry']:
|
|
18
24
|
raise HTTPException(status_code=400, detail='Invalid scope')
|
|
19
25
|
persona_path = BASE_DIR / scope / name / 'persona.json'
|
|
20
26
|
if not persona_path.exists():
|
|
@@ -28,13 +34,163 @@ def read_persona(scope: str, name: str):
|
|
|
28
34
|
|
|
29
35
|
@router.get('/personas/{scope}')
|
|
30
36
|
def list_personas(scope: str):
|
|
31
|
-
if scope not in ['local', 'shared']:
|
|
37
|
+
if scope not in ['local', 'shared', 'registry']:
|
|
32
38
|
raise HTTPException(status_code=400, detail='Invalid scope')
|
|
33
39
|
scope_dir = BASE_DIR / scope
|
|
34
40
|
personas = [p.name for p in scope_dir.iterdir() if p.is_dir()]
|
|
35
41
|
print(f"Read personas from dir {scope_dir}: {personas}")
|
|
36
42
|
return [{'name': name} for name in personas]
|
|
37
43
|
|
|
44
|
+
@router.get('/personas/{persona_path:path}')
|
|
45
|
+
def read_persona_by_path(persona_path: str):
|
|
46
|
+
"""Read persona by full path (supports registry/owner/name format)"""
|
|
47
|
+
try:
|
|
48
|
+
# Handle registry personas: registry/owner/name
|
|
49
|
+
if persona_path.startswith('registry/'):
|
|
50
|
+
full_path = BASE_DIR / persona_path / 'persona.json'
|
|
51
|
+
else:
|
|
52
|
+
# Handle simple names: check local first, then shared
|
|
53
|
+
full_path = BASE_DIR / 'local' / persona_path / 'persona.json'
|
|
54
|
+
if not full_path.exists():
|
|
55
|
+
full_path = BASE_DIR / 'shared' / persona_path / 'persona.json'
|
|
56
|
+
|
|
57
|
+
if not full_path.exists():
|
|
58
|
+
raise HTTPException(status_code=404, detail='Persona not found')
|
|
59
|
+
|
|
60
|
+
with open(full_path, 'r') as f:
|
|
61
|
+
return json.load(f)
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
raise HTTPException(status_code=404, detail=f'Persona not found: {str(e)}')
|
|
65
|
+
|
|
66
|
+
@router.post('/personas/registry')
|
|
67
|
+
def create_registry_persona(persona: str = Form(...), owner: str = Form(...)):
|
|
68
|
+
"""Create a registry persona with owner namespace"""
|
|
69
|
+
try:
|
|
70
|
+
persona_data = json.loads(persona)
|
|
71
|
+
persona_name = persona_data.get('name')
|
|
72
|
+
|
|
73
|
+
if not persona_name:
|
|
74
|
+
raise HTTPException(status_code=400, detail='Persona name is required')
|
|
75
|
+
if not owner:
|
|
76
|
+
raise HTTPException(status_code=400, detail='Owner is required for registry personas')
|
|
77
|
+
|
|
78
|
+
# Create registry persona path: personas/registry/owner/name/
|
|
79
|
+
persona_path = BASE_DIR / 'registry' / owner / persona_name / 'persona.json'
|
|
80
|
+
|
|
81
|
+
if persona_path.exists():
|
|
82
|
+
# Update existing persona instead of failing
|
|
83
|
+
print(f"Updating existing registry persona: {owner}/{persona_name}")
|
|
84
|
+
|
|
85
|
+
persona_path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
|
|
87
|
+
with open(persona_path, 'w') as f:
|
|
88
|
+
json.dump(persona_data, f, indent=2)
|
|
89
|
+
|
|
90
|
+
return {'status': 'success', 'path': f'registry/{owner}/{persona_name}'}
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"Error creating registry persona: {e}")
|
|
94
|
+
raise HTTPException(status_code=500, detail=f'Internal server error: {str(e)}')
|
|
95
|
+
|
|
96
|
+
@router.post('/personas/registry/with-assets')
|
|
97
|
+
def create_registry_persona_with_assets(persona: str = Form(...), owner: str = Form(...),
|
|
98
|
+
faceref: UploadFile = File(None), avatar: UploadFile = File(None)):
|
|
99
|
+
"""Create a registry persona with deduplicated asset storage"""
|
|
100
|
+
try:
|
|
101
|
+
print(f"Received persona data (first 200 chars): {persona[:200]}...")
|
|
102
|
+
print(f"Received owner: {owner}")
|
|
103
|
+
print(f"Received faceref: {faceref.filename if faceref else 'None'}")
|
|
104
|
+
print(f"Received avatar: {avatar.filename if avatar else 'None'}")
|
|
105
|
+
|
|
106
|
+
persona_data = json.loads(persona)
|
|
107
|
+
persona_name = persona_data.get('name')
|
|
108
|
+
|
|
109
|
+
if not persona_name:
|
|
110
|
+
raise HTTPException(status_code=400, detail='Persona name is required')
|
|
111
|
+
if not owner:
|
|
112
|
+
raise HTTPException(status_code=400, detail='Owner is required for registry personas')
|
|
113
|
+
|
|
114
|
+
# Store assets with deduplication
|
|
115
|
+
asset_hashes = {}
|
|
116
|
+
|
|
117
|
+
if faceref:
|
|
118
|
+
content = faceref.file.read()
|
|
119
|
+
file_hash, was_new = asset_manager.store_content(content, faceref.filename, "faceref")
|
|
120
|
+
asset_hashes['faceref'] = file_hash
|
|
121
|
+
|
|
122
|
+
if avatar:
|
|
123
|
+
content = avatar.file.read()
|
|
124
|
+
file_hash, was_new = asset_manager.store_content(content, avatar.filename, "avatar")
|
|
125
|
+
asset_hashes['avatar'] = file_hash
|
|
126
|
+
|
|
127
|
+
# Add asset references to persona data
|
|
128
|
+
persona_data['asset_hashes'] = asset_hashes
|
|
129
|
+
|
|
130
|
+
# For registry personas, update the name to include the owner namespace
|
|
131
|
+
# This ensures the chat UI can find the images at the correct path
|
|
132
|
+
if 'name' in persona_data:
|
|
133
|
+
persona_data['name'] = f"{owner}/{persona_name}"
|
|
134
|
+
|
|
135
|
+
# Create registry persona path first
|
|
136
|
+
persona_path = BASE_DIR / 'registry' / owner / persona_name / 'persona.json'
|
|
137
|
+
persona_path.parent.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
|
|
139
|
+
# ALSO copy assets to traditional locations for compatibility
|
|
140
|
+
if faceref:
|
|
141
|
+
faceref_path = persona_path.parent / 'faceref.png'
|
|
142
|
+
faceref_path.parent.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
with open(faceref_path, 'wb') as f:
|
|
144
|
+
faceref.file.seek(0) # Reset file pointer
|
|
145
|
+
f.write(faceref.file.read())
|
|
146
|
+
|
|
147
|
+
if avatar:
|
|
148
|
+
avatar_path = persona_path.parent / 'avatar.png'
|
|
149
|
+
avatar_path.parent.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
with open(avatar_path, 'wb') as f:
|
|
151
|
+
avatar.file.seek(0) # Reset file pointer
|
|
152
|
+
f.write(avatar.file.read())
|
|
153
|
+
|
|
154
|
+
with open(persona_path, 'w') as f:
|
|
155
|
+
json.dump(persona_data, f, indent=2)
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
'status': 'success',
|
|
159
|
+
'path': f'registry/{owner}/{persona_name}',
|
|
160
|
+
'asset_hashes': asset_hashes
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
except json.JSONDecodeError as e:
|
|
164
|
+
print(f"JSON decode error: {e}")
|
|
165
|
+
print(f"Invalid JSON received: {persona}")
|
|
166
|
+
raise HTTPException(status_code=400, detail=f'Invalid JSON in persona data: {str(e)}')
|
|
167
|
+
except Exception as e:
|
|
168
|
+
print(f"Error creating registry persona with assets: {e}")
|
|
169
|
+
import traceback
|
|
170
|
+
traceback.print_exc()
|
|
171
|
+
raise HTTPException(status_code=500, detail=f'Internal server error: {str(e)}')
|
|
172
|
+
|
|
173
|
+
@router.get('/assets/{asset_hash}')
|
|
174
|
+
def serve_asset(asset_hash: str):
|
|
175
|
+
"""Serve a deduplicated asset by hash"""
|
|
176
|
+
try:
|
|
177
|
+
asset_path = asset_manager.get_asset_path(asset_hash)
|
|
178
|
+
if not asset_path:
|
|
179
|
+
raise HTTPException(status_code=404, detail='Asset not found')
|
|
180
|
+
|
|
181
|
+
metadata = asset_manager.get_asset_metadata(asset_hash)
|
|
182
|
+
|
|
183
|
+
with open(asset_path, 'rb') as f:
|
|
184
|
+
content = f.read()
|
|
185
|
+
|
|
186
|
+
# Determine content type based on metadata
|
|
187
|
+
content_type = "image/png" if metadata and metadata.get('type') in ['avatar', 'faceref'] else "application/octet-stream"
|
|
188
|
+
|
|
189
|
+
return Response(content=content, media_type=content_type)
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
raise HTTPException(status_code=404, detail=f'Asset not found: {str(e)}')
|
|
193
|
+
|
|
38
194
|
@router.post('/personas/{scope}')
|
|
39
195
|
def create_persona(scope: str, persona: str = Form(...), faceref: UploadFile = File(None), avatar: UploadFile = File(None)):
|
|
40
196
|
try:
|
|
@@ -12,6 +12,7 @@ from lib.plugins import (
|
|
|
12
12
|
from lib.plugins.installation import download_github_files
|
|
13
13
|
from lib.streamcmd import stream_command_as_events
|
|
14
14
|
import asyncio
|
|
15
|
+
import httpx
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
router = APIRouter()
|
|
@@ -360,3 +361,65 @@ def discover_plugins(directory):
|
|
|
360
361
|
continue
|
|
361
362
|
|
|
362
363
|
return discovered
|
|
364
|
+
|
|
365
|
+
async def publish_plugin_from_github(repo: str, registry_token: str, registry_url: str):
|
|
366
|
+
"""
|
|
367
|
+
Fetches plugin_info.json from a GitHub repo and publishes it to the registry.
|
|
368
|
+
"""
|
|
369
|
+
plugin_info = None
|
|
370
|
+
# Try to fetch from 'main' and then 'master' branch
|
|
371
|
+
for branch in ['main', 'master']:
|
|
372
|
+
url = f"https://raw.githubusercontent.com/{repo}/{branch}/plugin_info.json"
|
|
373
|
+
async with httpx.AsyncClient() as client:
|
|
374
|
+
try:
|
|
375
|
+
response = await client.get(url)
|
|
376
|
+
if response.status_code == 200:
|
|
377
|
+
plugin_info = response.json()
|
|
378
|
+
break
|
|
379
|
+
except httpx.RequestError as e:
|
|
380
|
+
# This might happen if the repo is private or other network issues
|
|
381
|
+
print(f"Error fetching from {url}: {e}")
|
|
382
|
+
continue
|
|
383
|
+
|
|
384
|
+
if not plugin_info:
|
|
385
|
+
raise Exception(f"Could not find or access plugin_info.json in repo {repo} on 'main' or 'master' branch.")
|
|
386
|
+
|
|
387
|
+
# Construct the payload for the registry's /publish endpoint
|
|
388
|
+
publish_data = {
|
|
389
|
+
"title": plugin_info.get("name"),
|
|
390
|
+
"description": plugin_info.get("description", ""),
|
|
391
|
+
"category": "plugin",
|
|
392
|
+
"content_type": "mindroot_plugin",
|
|
393
|
+
"version": plugin_info.get("version", "0.1.0"),
|
|
394
|
+
"github_url": f"https://github.com/{repo}",
|
|
395
|
+
"pypi_module": plugin_info.get("pypi_module"),
|
|
396
|
+
"commands": plugin_info.get("commands", []),
|
|
397
|
+
"services": plugin_info.get("services", []),
|
|
398
|
+
"tags": plugin_info.get("tags", ["plugin"]),
|
|
399
|
+
"dependencies": plugin_info.get("dependencies", []),
|
|
400
|
+
"data": {
|
|
401
|
+
"plugin_info": plugin_info,
|
|
402
|
+
"installation": {
|
|
403
|
+
"type": "github",
|
|
404
|
+
"source_path": repo
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
# Post the data to the registry
|
|
410
|
+
publish_url = f"{registry_url}/publish"
|
|
411
|
+
headers = {
|
|
412
|
+
"Authorization": f"Bearer {registry_token}",
|
|
413
|
+
"Content-Type": "application/json"
|
|
414
|
+
}
|
|
415
|
+
async with httpx.AsyncClient() as client:
|
|
416
|
+
response = await client.post(publish_url, json=publish_data, headers=headers)
|
|
417
|
+
|
|
418
|
+
if response.status_code >= 400:
|
|
419
|
+
try:
|
|
420
|
+
error_detail = response.json().get("detail", response.text)
|
|
421
|
+
except:
|
|
422
|
+
error_detail = response.text
|
|
423
|
+
raise Exception(f"Failed to publish to registry: {response.status_code} - {error_detail}")
|
|
424
|
+
|
|
425
|
+
return response.json()
|
|
@@ -21,7 +21,7 @@ def update_plugins(request: PluginUpdateRequest):
|
|
|
21
21
|
with open('plugins.json', 'w') as file:
|
|
22
22
|
json.dump(plugins_data, file, indent=2)
|
|
23
23
|
|
|
24
|
-
plugins.load('plugin_manifest.json')
|
|
24
|
+
plugins.load('data/plugin_manifest.json')
|
|
25
25
|
|
|
26
26
|
return {"message": "Plugins updated successfully"}
|
|
27
27
|
except Exception as e:
|