mindroot 9.3.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.
Files changed (183) hide show
  1. mindroot/coreplugins/admin/__init__.py +3 -1
  2. mindroot/coreplugins/admin/agent_router.py +250 -7
  3. mindroot/coreplugins/admin/asset_manager.py +164 -0
  4. mindroot/coreplugins/admin/command_router.py +236 -1
  5. mindroot/coreplugins/admin/mcp_catalog_routes.py +156 -0
  6. mindroot/coreplugins/admin/mcp_publish_routes.py +450 -0
  7. mindroot/coreplugins/admin/mcp_registry_routes.py +495 -0
  8. mindroot/coreplugins/admin/mcp_routes.py +216 -0
  9. mindroot/coreplugins/admin/mod.py +62 -0
  10. mindroot/coreplugins/admin/oauth_callback_router.py +84 -0
  11. mindroot/coreplugins/admin/persona_handler.py +15 -6
  12. mindroot/coreplugins/admin/persona_router.py +158 -2
  13. mindroot/coreplugins/admin/plugin_manager.py +63 -0
  14. mindroot/coreplugins/admin/plugin_router_fixed.py +23 -0
  15. mindroot/coreplugins/admin/plugin_router_new_not_working.py +145 -0
  16. mindroot/coreplugins/admin/plugin_routes.py +114 -0
  17. mindroot/coreplugins/admin/registry_settings_routes.py +140 -0
  18. mindroot/coreplugins/admin/router.py +116 -15
  19. mindroot/coreplugins/admin/service_models.py +1 -1
  20. mindroot/coreplugins/admin/settings_router.py +1 -0
  21. mindroot/coreplugins/admin/static/css/admin-custom.css +357 -2
  22. mindroot/coreplugins/admin/static/css/dark.css +1 -0
  23. mindroot/coreplugins/admin/static/css/default.css +4 -0
  24. mindroot/coreplugins/admin/static/js/about-info.js +367 -0
  25. mindroot/coreplugins/admin/static/js/agent-form.js +83 -3
  26. mindroot/coreplugins/admin/static/js/api-key-script.js +307 -0
  27. mindroot/coreplugins/admin/static/js/mcp-manager.js +348 -0
  28. mindroot/coreplugins/admin/static/js/mcp-publisher.js +780 -0
  29. mindroot/coreplugins/admin/static/js/persona-editor.js +34 -5
  30. mindroot/coreplugins/admin/static/js/plugin-toggle.js +1 -1
  31. mindroot/coreplugins/admin/static/js/recommended-plugin-install.js +63 -0
  32. mindroot/coreplugins/admin/static/js/registry-auth-section.js +132 -0
  33. mindroot/coreplugins/admin/static/js/registry-manager-base.js +613 -0
  34. mindroot/coreplugins/admin/static/js/registry-manager-old.js +385 -0
  35. mindroot/coreplugins/admin/static/js/registry-manager-publish-old-delete.js +166 -0
  36. mindroot/coreplugins/admin/static/js/registry-manager.js +351 -0
  37. mindroot/coreplugins/admin/static/js/registry-publish-section.js +377 -0
  38. mindroot/coreplugins/admin/static/js/registry-search-section.js +400 -0
  39. mindroot/coreplugins/admin/static/js/registry-search-section.js.bak +3 -0
  40. mindroot/coreplugins/admin/static/js/registry-settings.js +69 -0
  41. mindroot/coreplugins/admin/static/js/registry-shared-services.js +857 -0
  42. mindroot/coreplugins/admin/static/js/registry-simple-sections.js +85 -0
  43. mindroot/coreplugins/admin/static/js/secure-widget-manager.js +438 -0
  44. mindroot/coreplugins/admin/static/logo.png +0 -0
  45. mindroot/coreplugins/admin/templates/admin.jinja2 +275 -110
  46. mindroot/coreplugins/agent/Assistant/agent.json +27 -11
  47. mindroot/coreplugins/agent/agent.py +2 -2
  48. mindroot/coreplugins/agent/command_parser.py +25 -10
  49. mindroot/coreplugins/agent/templates/system.jinja2 +0 -12
  50. mindroot/coreplugins/chat/__init__.py +4 -1
  51. mindroot/coreplugins/chat/router.py +132 -20
  52. mindroot/coreplugins/chat/router_dedup_patch.py +20 -0
  53. mindroot/coreplugins/chat/services.py +31 -1
  54. mindroot/coreplugins/chat/static/css/action-fix.css +32 -0
  55. mindroot/coreplugins/chat/static/css/admin-custom.css +5 -3
  56. mindroot/coreplugins/chat/static/css/dark.css +24 -3
  57. mindroot/coreplugins/chat/static/css/default.css +24 -3
  58. mindroot/coreplugins/chat/static/css/main.css +1 -0
  59. mindroot/coreplugins/chat/static/js/action.js +137 -60
  60. mindroot/coreplugins/chat/static/js/chat-history.js +3 -0
  61. mindroot/coreplugins/chat/static/js/chat.js +59 -16
  62. mindroot/coreplugins/chat/static/js/chat.js.diff +221 -0
  63. mindroot/coreplugins/chat/static/js/chatform.js +2 -2
  64. mindroot/coreplugins/chat/static/site.webmanifest +1 -1
  65. mindroot/coreplugins/chat/templates/chat.jinja2 +3 -3
  66. mindroot/coreplugins/chat/widget_manager.py +139 -0
  67. mindroot/coreplugins/chat/widget_routes.py +287 -0
  68. mindroot/coreplugins/check_list/inject/admin.jinja2 +1 -1
  69. mindroot/coreplugins/email/__init__.py +2 -0
  70. mindroot/coreplugins/email/email_provider.py +2 -2
  71. mindroot/coreplugins/email/mod.py +100 -0
  72. mindroot/coreplugins/email/services.py +5 -3
  73. mindroot/coreplugins/email/smtp_handler.py +9 -3
  74. mindroot/coreplugins/email/test_email_service.py +75 -0
  75. mindroot/coreplugins/env_manager/mod.py +61 -25
  76. mindroot/coreplugins/home/router.py +37 -2
  77. mindroot/coreplugins/home/static/imgs/logo.png +0 -0
  78. mindroot/coreplugins/home/static/imgs/logo.png.bak +0 -0
  79. mindroot/coreplugins/home/static/imgs/logo_teal.png +0 -0
  80. mindroot/coreplugins/home/static/imgs/logo_teal2.png +0 -0
  81. mindroot/coreplugins/home/static/imgs/logo_teal_detailed.png +0 -0
  82. mindroot/coreplugins/home/static/imgs/logo_teal_python.png +0 -0
  83. mindroot/coreplugins/home/templates/home.jinja2 +15 -6
  84. mindroot/coreplugins/index/indices/default/index.json +6 -6
  85. mindroot/coreplugins/jwt_auth/middleware.py +47 -2
  86. mindroot/coreplugins/jwt_auth/mod.py +40 -17
  87. mindroot/coreplugins/l8n/__init__.py +6 -0
  88. mindroot/coreplugins/l8n/debug_loader.py +85 -0
  89. mindroot/coreplugins/l8n/debug_middleware.py +74 -0
  90. mindroot/coreplugins/l8n/l8n_constants.py +19 -0
  91. mindroot/coreplugins/l8n/language_detection.py +183 -0
  92. mindroot/coreplugins/l8n/middleware.py +151 -0
  93. mindroot/coreplugins/l8n/mod.py +277 -0
  94. mindroot/coreplugins/l8n/monkey_patch_to_delete.py +186 -0
  95. mindroot/coreplugins/l8n/test_enhanced.py +298 -0
  96. mindroot/coreplugins/l8n/test_l8n.py +95 -0
  97. mindroot/coreplugins/l8n/test_l8n_standalone.py +251 -0
  98. mindroot/coreplugins/l8n/test_middleware.py +272 -0
  99. mindroot/coreplugins/l8n/utils.py +232 -0
  100. mindroot/coreplugins/mcp_/__init__.py +14 -0
  101. mindroot/coreplugins/mcp_/catalog_commands.py +328 -0
  102. mindroot/coreplugins/mcp_/catalog_manager.py +263 -0
  103. mindroot/coreplugins/mcp_/dynamic_commands.py +154 -0
  104. mindroot/coreplugins/mcp_/mcp_manager.py +1031 -0
  105. mindroot/coreplugins/mcp_/mod.py +367 -0
  106. mindroot/coreplugins/mcp_/oauth_storage.py +144 -0
  107. mindroot/coreplugins/mcp_/server_installer.py +79 -0
  108. mindroot/coreplugins/mcp_/setup.py +26 -0
  109. mindroot/coreplugins/mcp_/test_dynamic_commands.py +134 -0
  110. mindroot/coreplugins/mcp_/testmcpclient.py +92 -0
  111. mindroot/coreplugins/persona/mod.py +12 -7
  112. mindroot/coreplugins/signup/templates/signup.jinja2 +1 -1
  113. mindroot/coreplugins/subscriptions/__init__.py +1 -0
  114. mindroot/coreplugins/subscriptions/mod.py +14 -3
  115. mindroot/coreplugins/subscriptions/router.py +3 -0
  116. mindroot/coreplugins/user_service/__init__.py +1 -2
  117. mindroot/coreplugins/user_service/admin_init.py +1 -0
  118. mindroot/coreplugins/user_service/email_service.py +72 -17
  119. mindroot/coreplugins/user_service/mod.py +10 -2
  120. mindroot/coreplugins/user_service/router.py +2 -0
  121. mindroot/lib/auth/api_key.py +28 -0
  122. mindroot/lib/cli/plugins.py +94 -0
  123. mindroot/lib/plugins/default_plugin_manifest.json +20 -0
  124. mindroot/lib/plugins/installation.py +5 -5
  125. mindroot/lib/plugins/l8n_static_handler.py +225 -0
  126. mindroot/lib/plugins/loader.py +33 -3
  127. mindroot/lib/plugins/loader_with_l8n.py +281 -0
  128. mindroot/lib/plugins/manifest.py +236 -24
  129. mindroot/lib/providers/commands.py +3 -1
  130. mindroot/lib/route_decorators.py +5 -5
  131. mindroot/lib/templates.py +183 -11
  132. mindroot/lib/utils/merge_arrays.py +1 -1
  133. mindroot/migrate.py +39 -20
  134. mindroot/registry/data_access.py +1 -1
  135. mindroot/server.py +42 -13
  136. mindroot/server_missing_normal_args.py +197 -0
  137. mindroot/server_prev.py +173 -0
  138. {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/METADATA +7 -2
  139. {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/RECORD +144 -112
  140. mindroot/coreplugins/admin/static/favicon/about.txt +0 -6
  141. mindroot/coreplugins/admin/static/favicon/android-chrome-512x512.png +0 -0
  142. mindroot/coreplugins/admin/static/favicon/apple-touch-icon.png +0 -0
  143. mindroot/coreplugins/admin/static/favicon/favicon-16x16.png +0 -0
  144. mindroot/coreplugins/admin/static/favicon/favicon-32x32.png +0 -0
  145. mindroot/coreplugins/admin/static/favicon/favicon.ico +0 -0
  146. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/about.txt +0 -6
  147. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/android-chrome-192x192.png +0 -0
  148. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/android-chrome-512x512.png +0 -0
  149. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/apple-touch-icon.png +0 -0
  150. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon-16x16.png +0 -0
  151. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon-32x32.png +0 -0
  152. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon.ico +0 -0
  153. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/site.webmanifest +0 -1
  154. mindroot/coreplugins/admin/static/favicon/logo.png +0 -0
  155. mindroot/coreplugins/admin/static/favicon/site.webmanifest +0 -1
  156. mindroot/coreplugins/admin/static/js/backup/agent-editor.js +0 -186
  157. mindroot/coreplugins/admin/static/js/backup/agent-form.js +0 -1133
  158. mindroot/coreplugins/admin/static/js/backup/agent-list.js +0 -94
  159. mindroot/coreplugins/chat/static/favicon/about.txt +0 -6
  160. mindroot/coreplugins/chat/static/favicon/android-chrome-192x192.png +0 -0
  161. mindroot/coreplugins/chat/static/favicon/android-chrome-512x512.png +0 -0
  162. mindroot/coreplugins/chat/static/favicon/apple-touch-icon.png +0 -0
  163. mindroot/coreplugins/chat/static/favicon/favicon-16x16.png +0 -0
  164. mindroot/coreplugins/chat/static/favicon/favicon-32x32.png +0 -0
  165. mindroot/coreplugins/chat/static/favicon/favicon.ico +0 -0
  166. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/about.txt +0 -6
  167. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/android-chrome-192x192.png +0 -0
  168. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/android-chrome-512x512.png +0 -0
  169. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/apple-touch-icon.png +0 -0
  170. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon-16x16.png +0 -0
  171. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon-32x32.png +0 -0
  172. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon.ico +0 -0
  173. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/site.webmanifest +0 -1
  174. mindroot/coreplugins/chat/static/favicon/logo.png +0 -0
  175. mindroot/coreplugins/chat/static/favicon/site.webmanifest +0 -1
  176. mindroot/coreplugins/index/default.json +0 -76
  177. mindroot/coreplugins/user_service/file_trigger_service.py +0 -12
  178. mindroot/coreplugins/user_service/hooks.py +0 -23
  179. /mindroot/coreplugins/{admin/static/favicon/android-chrome-192x192.png → home/static/imgs/backuplogo.png} +0 -0
  180. {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/WHEEL +0 -0
  181. {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/entry_points.txt +0 -0
  182. {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/licenses/LICENSE +0 -0
  183. {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,242 @@
1
1
  import json
2
2
  import os
3
3
  import shutil
4
+ import tempfile
5
+ import logging
4
6
  from datetime import datetime
7
+ from pathlib import Path
5
8
 
6
9
  # Central definition of manifest file location
7
10
  MANIFEST_FILE = 'data/plugin_manifest.json'
8
11
 
12
+ # Setup logging
13
+ logger = logging.getLogger(__name__)
14
+
15
+ def _get_absolute_paths():
16
+ """Get absolute paths for all manifest-related files.
17
+
18
+ Returns:
19
+ tuple: (manifest_abs_path, root_manifest_abs_path, data_dir_abs_path)
20
+ """
21
+ cwd = os.getcwd()
22
+ manifest_abs_path = os.path.abspath(os.path.join(cwd, MANIFEST_FILE))
23
+ root_manifest_abs_path = os.path.abspath(os.path.join(cwd, 'plugin_manifest.json'))
24
+ data_dir_abs_path = os.path.abspath(os.path.join(cwd, 'data'))
25
+
26
+ return manifest_abs_path, root_manifest_abs_path, data_dir_abs_path
27
+
28
+ def _backup_manifest(manifest_path):
29
+ """Create a backup of the existing manifest file.
30
+
31
+ Args:
32
+ manifest_path (str): Absolute path to the manifest file
33
+
34
+ Returns:
35
+ str: Path to backup file, or None if backup failed
36
+ """
37
+ if not os.path.exists(manifest_path):
38
+ return None
39
+
40
+ try:
41
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
42
+ backup_path = f"{manifest_path}.backup_{timestamp}"
43
+ shutil.copy2(manifest_path, backup_path)
44
+ logger.info(f"Created manifest backup: {backup_path}")
45
+ return backup_path
46
+ except Exception as e:
47
+ logger.error(f"Failed to create manifest backup: {e}")
48
+ return None
49
+
50
+ def _atomic_write_manifest(manifest_path, manifest_data):
51
+ """Atomically write manifest data to file.
52
+
53
+ Args:
54
+ manifest_path (str): Absolute path to the manifest file
55
+ manifest_data (dict): Manifest data to write
56
+
57
+ Returns:
58
+ bool: True if successful, False otherwise
59
+ """
60
+ try:
61
+ # Ensure directory exists
62
+ os.makedirs(os.path.dirname(manifest_path), exist_ok=True)
63
+
64
+ # Write to temporary file first
65
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.tmp',
66
+ dir=os.path.dirname(manifest_path),
67
+ delete=False) as tmp_file:
68
+ json.dump(manifest_data, tmp_file, indent=2)
69
+ tmp_path = tmp_file.name
70
+
71
+ # Atomic move to final location
72
+ shutil.move(tmp_path, manifest_path)
73
+ logger.debug(f"Atomically wrote manifest to: {manifest_path}")
74
+ return True
75
+
76
+ except Exception as e:
77
+ logger.error(f"Failed to atomically write manifest: {e}")
78
+ # Clean up temp file if it exists
79
+ try:
80
+ if 'tmp_path' in locals() and os.path.exists(tmp_path):
81
+ os.unlink(tmp_path)
82
+ except:
83
+ pass
84
+ return False
85
+
86
+ def _validate_manifest(manifest_path):
87
+ """Validate that a manifest file exists and contains valid JSON.
88
+
89
+ Args:
90
+ manifest_path (str): Absolute path to the manifest file
91
+
92
+ Returns:
93
+ tuple: (is_valid, manifest_data_or_none)
94
+ """
95
+ if not os.path.exists(manifest_path):
96
+ return False, None
97
+
98
+ try:
99
+ with open(manifest_path, 'r') as f:
100
+ manifest_data = json.load(f)
101
+
102
+ # Basic structure validation
103
+ if not isinstance(manifest_data, dict):
104
+ logger.warning(f"Manifest is not a dict: {manifest_path}")
105
+ return False, None
106
+
107
+ if 'plugins' not in manifest_data:
108
+ logger.warning(f"Manifest missing 'plugins' key: {manifest_path}")
109
+ return False, None
110
+
111
+ return True, manifest_data
112
+
113
+ except json.JSONDecodeError as e:
114
+ logger.warning(f"Manifest contains invalid JSON: {manifest_path} - {e}")
115
+ return False, None
116
+ except Exception as e:
117
+ logger.error(f"Error validating manifest: {manifest_path} - {e}")
118
+ return False, None
119
+
120
+ def _migrate_manifest_from_root():
121
+ """Migrate manifest from root directory to data directory if needed.
122
+
123
+ This function consolidates the migration logic and uses absolute paths.
124
+
125
+ Returns:
126
+ bool: True if migration was successful or not needed, False if failed
127
+ """
128
+ manifest_abs_path, root_manifest_abs_path, data_dir_abs_path = _get_absolute_paths()
129
+
130
+ logger.debug(f"Checking for manifest migration:")
131
+ logger.debug(f" Target: {manifest_abs_path}")
132
+ logger.debug(f" Source: {root_manifest_abs_path}")
133
+
134
+ # If target already exists and is valid, no migration needed
135
+ is_valid, _ = _validate_manifest(manifest_abs_path)
136
+ if is_valid:
137
+ logger.debug(f"Valid manifest already exists at: {manifest_abs_path}")
138
+ return True
139
+
140
+ # If target exists but is invalid, back it up
141
+ if os.path.exists(manifest_abs_path):
142
+ logger.warning(f"Invalid manifest found at {manifest_abs_path}, backing up")
143
+ _backup_manifest(manifest_abs_path)
144
+
145
+ # Check if source manifest exists and is valid
146
+ is_valid, manifest_data = _validate_manifest(root_manifest_abs_path)
147
+ if is_valid:
148
+ logger.info(f"Migrating manifest from {root_manifest_abs_path} to {manifest_abs_path}")
149
+
150
+ # Ensure data directory exists
151
+ os.makedirs(data_dir_abs_path, exist_ok=True)
152
+
153
+ # Atomic write to new location
154
+ if _atomic_write_manifest(manifest_abs_path, manifest_data):
155
+ # Remove old file only after successful write
156
+ try:
157
+ os.unlink(root_manifest_abs_path)
158
+ logger.info(f"Successfully migrated manifest and removed old file")
159
+ return True
160
+ except Exception as e:
161
+ logger.warning(f"Manifest migrated but failed to remove old file: {e}")
162
+ return True
163
+ else:
164
+ logger.error(f"Failed to write migrated manifest")
165
+ return False
166
+
167
+ logger.debug(f"No valid manifest found to migrate from {root_manifest_abs_path}")
168
+ return True # Not an error - just no migration needed
169
+
170
+ def create_default_plugin_manifest():
171
+ """Create a new default manifest file.
172
+
173
+ This function first attempts migration, then creates from default template if needed.
174
+ """
175
+ manifest_abs_path, _, data_dir_abs_path = _get_absolute_paths()
176
+
177
+ logger.info(f"Creating default plugin manifest at: {manifest_abs_path}")
178
+
179
+ # First attempt migration from root directory
180
+ if _migrate_manifest_from_root():
181
+ # Check if migration was successful
182
+ is_valid, _ = _validate_manifest(manifest_abs_path)
183
+ if is_valid:
184
+ logger.info(f"Manifest successfully migrated from root directory")
185
+ return
186
+
187
+ # Migration didn't work, create from default template
188
+ logger.info(f"Creating manifest from default template")
189
+
190
+ # Backup existing file if it exists (even if invalid)
191
+ if os.path.exists(manifest_abs_path):
192
+ _backup_manifest(manifest_abs_path)
193
+
194
+ try:
195
+ # Read default template
196
+ default_manifest_path = os.path.join(os.path.dirname(__file__), 'default_plugin_manifest.json')
197
+ with open(default_manifest_path, 'r') as f:
198
+ default_manifest = json.load(f)
199
+
200
+ # Ensure data directory exists
201
+ os.makedirs(data_dir_abs_path, exist_ok=True)
202
+
203
+ # Atomic write
204
+ if _atomic_write_manifest(manifest_abs_path, default_manifest):
205
+ logger.info(f"Successfully created default manifest")
206
+ else:
207
+ logger.error(f"Failed to create default manifest")
208
+ raise Exception("Failed to write default manifest")
209
+
210
+ except Exception as e:
211
+ logger.error(f"Failed to create default manifest: {e}")
212
+ raise
213
+
9
214
  def load_plugin_manifest():
10
215
  """Load the plugin manifest file.
11
216
 
12
217
  Returns:
13
218
  dict: The manifest data structure
14
219
  """
15
- abs_path = os.path.abspath(MANIFEST_FILE)
16
- if not os.path.exists(MANIFEST_FILE):
17
- create_default_plugin_manifest()
18
-
19
- with open(MANIFEST_FILE, 'r') as f:
20
- return json.load(f)
220
+ manifest_abs_path, _, _ = _get_absolute_paths()
221
+
222
+ # Validate existing manifest
223
+ is_valid, manifest_data = _validate_manifest(manifest_abs_path)
224
+
225
+ if is_valid:
226
+ logger.debug(f"Loaded valid manifest from: {manifest_abs_path}")
227
+ return manifest_data
228
+
229
+ # Manifest is missing or invalid, create default
230
+ logger.warning(f"Manifest missing or invalid at {manifest_abs_path}, creating default")
231
+ create_default_plugin_manifest()
232
+
233
+ # Load the newly created manifest
234
+ is_valid, manifest_data = _validate_manifest(manifest_abs_path)
235
+ if is_valid:
236
+ return manifest_data
237
+ else:
238
+ logger.error(f"Failed to create valid default manifest")
239
+ raise Exception("Could not load or create valid plugin manifest")
21
240
 
22
241
  def save_plugin_manifest(manifest):
23
242
  """Save the plugin manifest file.
@@ -25,8 +244,17 @@ def save_plugin_manifest(manifest):
25
244
  Args:
26
245
  manifest (dict): The manifest data structure to save
27
246
  """
28
- with open(MANIFEST_FILE, 'w') as f:
29
- json.dump(manifest, f, indent=2)
247
+ manifest_abs_path, _, _ = _get_absolute_paths()
248
+
249
+ # Backup existing manifest before saving
250
+ _backup_manifest(manifest_abs_path)
251
+
252
+ # Atomic write
253
+ if not _atomic_write_manifest(manifest_abs_path, manifest):
254
+ logger.error(f"Failed to save plugin manifest")
255
+ raise Exception("Failed to save plugin manifest")
256
+
257
+ logger.debug(f"Successfully saved manifest to: {manifest_abs_path}")
30
258
 
31
259
  def update_plugin_manifest(plugin_name, source, source_path, remote_source=None, version="0.0.1", metadata=None):
32
260
  """Update or add a plugin entry in the manifest.
@@ -67,22 +295,6 @@ def update_plugin_manifest(plugin_name, source, source_path, remote_source=None,
67
295
 
68
296
  save_plugin_manifest(manifest)
69
297
 
70
- def create_default_plugin_manifest():
71
- """Create a new default manifest file."""
72
- # First check if there's an existing manifest in the root directory
73
- old_manifest_path = 'plugin_manifest.json'
74
- if os.path.exists(old_manifest_path):
75
- # Move existing manifest from root to data directory
76
- os.makedirs(os.path.dirname(MANIFEST_FILE), exist_ok=True)
77
- shutil.move(old_manifest_path, MANIFEST_FILE)
78
- else:
79
- # No existing manifest, create from default template
80
- # read from default_plugin_manifest.json in same dir as this file
81
- default_manifest_path = os.path.join(os.path.dirname(__file__), 'default_plugin_manifest.json')
82
- with open(default_manifest_path, 'r') as f:
83
- default_manifest = json.load(f)
84
- os.makedirs(os.path.dirname(MANIFEST_FILE), exist_ok=True)
85
- save_plugin_manifest(default_manifest)
86
298
  def toggle_plugin_state(plugin_name, enabled):
87
299
  """Toggle a plugin's enabled state.
88
300
 
@@ -1,6 +1,7 @@
1
1
  import inspect
2
2
  import os
3
3
  from . import ProviderManager
4
+ from mindroot.lib.utils.debug import debug_box
4
5
 
5
6
  command_manager = ProviderManager()
6
7
 
@@ -14,7 +15,8 @@ def command(*, flags=[]):
14
15
  raise ValueError("Cannot determine module of function")
15
16
 
16
17
  module_name = os.path.basename(os.path.dirname(module.__file__))
17
-
18
+ if module_name == 'l8n':
19
+ debug_box("registering l8n command!" + name)
18
20
  command_manager.register_function(name, module_name, func, signature, docstring, flags)
19
21
  return func
20
22
  return decorator
@@ -9,23 +9,23 @@ public_routes: Set[str] = set()
9
9
  public_static: Set[str] = set()
10
10
 
11
11
  def public_route():
12
- # include route path/function name
13
- print("public_route decorator called", public_routes)
12
+ """Decorator to mark a route as public (no authentication required)"""
14
13
  def decorator(func):
15
14
  @wraps(func)
16
15
  async def wrapper(*args, **kwargs):
17
- print("public route !!!!!!!!!!!")
16
+ # Mark the request as a public route for any middleware that needs to know
18
17
  request: Request = next((arg for arg in args if isinstance(arg, Request)), None)
19
18
  if request:
20
19
  request.state.public_route = True
21
20
  return await func(*args, **kwargs)
21
+
22
+ # Mark the function as a public route so the startup hook can find it
22
23
  wrapper.__public_route__ = True
23
- public_routes.add(func.__name__)
24
- print("wrapper.__public_route__", wrapper.__public_route__)
25
24
  return wrapper
26
25
  return decorator
27
26
 
28
27
  def add_public_static(path_start):
28
+ """Add a path prefix to the public static routes"""
29
29
  public_static.add(path_start)
30
30
 
31
31
  # New role-based dependency functions
mindroot/lib/templates.py CHANGED
@@ -4,12 +4,133 @@ from jinja2 import Environment, FileSystemLoader, ChoiceLoader
4
4
  from .plugins import list_enabled, get_plugin_path
5
5
  from .parent_templates import get_parent_templates_env
6
6
  import traceback
7
+ import sys
8
+
9
+ # Import l8n translation functions
10
+ try:
11
+ from mindroot.coreplugins.l8n.utils import replace_placeholders, get_localized_file_path, extract_plugin_root
12
+ from mindroot.coreplugins.l8n.mod import *
13
+ from mindroot.coreplugins.l8n.middleware import get_request_language
14
+ from mindroot.coreplugins.l8n.language_detection import get_fallback_language
15
+ L8N_AVAILABLE = True
16
+ except ImportError as e:
17
+ trace = traceback.format_exc()
18
+ print(f"L8n not available: {e} \n{trace}")
19
+ L8N_AVAILABLE = False
20
+ sys.exit(1)
7
21
 
8
22
  # TODO: get_parent_templates_env(plugins):
9
23
  # jinja2 environment containing only 1 template per plugin file name,
10
24
  # the first one that is found from plugins with that name,
11
25
  # need to look at how jinja2 loaders work
12
26
 
27
+ def get_current_language():
28
+ """Get the current language for translation."""
29
+ if not L8N_AVAILABLE:
30
+ return 'en'
31
+
32
+ try:
33
+ # Try to get language from request context
34
+ lang = get_request_language()
35
+ return get_fallback_language(lang) if lang else 'en'
36
+ except Exception as e:
37
+ print(f"Error getting current language: {e}")
38
+ return 'en'
39
+
40
+ def apply_translations_to_content(content, template_path=None):
41
+ """Apply l8n translations to template content.
42
+
43
+ Args:
44
+ content (str): Template content with __TRANSLATE_key__ placeholders
45
+ template_path (str): Path to the template file for plugin context
46
+
47
+ Returns:
48
+ str or None: Content with translations applied, or None if translations are incomplete
49
+ """
50
+ if not L8N_AVAILABLE or not content:
51
+ return content
52
+
53
+ try:
54
+ current_language = get_current_language()
55
+
56
+ # If we have a template path, use it for plugin context
57
+ if template_path:
58
+ # Check if translation failed (missing translations)
59
+ translated = replace_placeholders(content, current_language, template_path)
60
+ if translated is None:
61
+ return None # Signal that translations are incomplete
62
+ return translated
63
+ else:
64
+ # Fallback: try to replace without plugin context
65
+ return replace_placeholders(content, current_language)
66
+ except Exception as e:
67
+ print(f"Error applying translations: {e}")
68
+ return content
69
+
70
+ def check_for_localized_template(template_path):
71
+ """Check if a localized version of a template exists.
72
+
73
+ Args:
74
+ template_path (str): Original template path
75
+
76
+ Returns:
77
+ str: Path to localized template if it exists, None otherwise
78
+ """
79
+ if not L8N_AVAILABLE:
80
+ return None
81
+
82
+ try:
83
+ localized_path = get_localized_file_path(template_path)
84
+ if localized_path.exists():
85
+ return str(localized_path)
86
+ except Exception as e:
87
+ print(f"Error checking for localized template: {e}")
88
+
89
+ return None
90
+
91
+ def load_template_with_translation(template_path):
92
+ """Load a template and apply translations if available and complete.
93
+
94
+ If translations are missing for the current language, this function
95
+ will fall back to serving the original template to avoid showing
96
+ __TRANSLATE_key__ placeholders to users.
97
+
98
+ Args:
99
+ template_path (str): Path to the template file
100
+
101
+ Returns:
102
+ str: Template content with translations applied, or original content
103
+ if translations are incomplete for the current language
104
+ """
105
+ # First check for localized version
106
+ localized_path = check_for_localized_template(template_path)
107
+
108
+ if localized_path:
109
+ # Load localized template and apply translations
110
+ try:
111
+ with open(localized_path, 'r', encoding='utf-8') as f:
112
+ content = f.read()
113
+
114
+ # Apply translations - if None is returned, translations are incomplete
115
+ translated_content = apply_translations_to_content(content, localized_path)
116
+ if translated_content is None:
117
+ # Fall back to original template
118
+ print(f"L8n: Falling back to original template due to missing translations: {template_path}")
119
+ with open(template_path, 'r', encoding='utf-8') as f:
120
+ return f.read()
121
+ return translated_content
122
+ except Exception as e:
123
+ print(f"Error loading localized template {localized_path}: {e}")
124
+
125
+ # Fallback to original template
126
+ try:
127
+ with open(template_path, 'r', encoding='utf-8') as f:
128
+ content = f.read()
129
+ return content # Don't apply translations to original templates
130
+ except Exception as e:
131
+ print(f"Error loading template {template_path}: {e}")
132
+ return ""
133
+
13
134
  def setup_template_environment(plugins=None):
14
135
  """Set up Jinja2 environment with multiple template paths.
15
136
 
@@ -106,7 +227,6 @@ async def find_parent_template(page_name, plugins):
106
227
  return rel_path
107
228
  return alt_path
108
229
  return None
109
-
110
230
  async def find_plugin_template(page_name, plugins):
111
231
  """Find a template in a plugin's templates directory.
112
232
 
@@ -148,7 +268,7 @@ async def find_plugin_template(page_name, plugins):
148
268
  return None, None
149
269
 
150
270
  async def load_plugin_templates(page_name, plugins):
151
- """Load templates from plugins.
271
+ """Load templates from plugins with translation support.
152
272
 
153
273
  Args:
154
274
  page_name (str): Name of the template page
@@ -180,9 +300,11 @@ async def load_plugin_templates(page_name, plugins):
180
300
 
181
301
  for path in inject_paths:
182
302
  if os.path.exists(path):
183
- with open(path) as f:
303
+ # Load template content with translation support
304
+ content = load_template_with_translation(path)
305
+ if content:
184
306
  #print(f"Found inject template at: {path}")
185
- templates.append({'type': 'inject', 'template': env.from_string(f.read())})
307
+ templates.append({'type': 'inject', 'template': env.from_string(content)})
186
308
  break
187
309
  #else:
188
310
  #
@@ -198,9 +320,11 @@ async def load_plugin_templates(page_name, plugins):
198
320
 
199
321
  for path in override_paths:
200
322
  if os.path.exists(path):
201
- with open(path) as f:
323
+ # Load template content with translation support
324
+ content = load_template_with_translation(path)
325
+ if content:
202
326
  #print(f"Found override template at: {path}")
203
- templates.append({'type': 'override', 'template': env.from_string(f.read())})
327
+ templates.append({'type': 'override', 'template': env.from_string(content)})
204
328
  break
205
329
 
206
330
  except Exception as e:
@@ -232,7 +356,7 @@ async def collect_content(template, blocks, template_type, data):
232
356
  return content
233
357
 
234
358
  async def render_combined_template(page_name, plugins, context):
235
- """Render combined template with injections and overrides.
359
+ """Render combined template with injections and overrides, including translation support.
236
360
 
237
361
  Args:
238
362
  page_name (str): Name of the template page
@@ -242,7 +366,37 @@ async def render_combined_template(page_name, plugins, context):
242
366
  Returns:
243
367
  str: Rendered HTML
244
368
  """
245
- parent_template = parent_env.get_template(f"{page_name}.jinja2")
369
+ # Load parent template with translation support
370
+ parent_template = None
371
+ parent_template_path = None
372
+
373
+ # Find the parent template file path
374
+ # We need to search through the parent_env loaders to find the actual file
375
+ for loader in parent_env.loader.loaders:
376
+ for template_dir in loader.searchpath:
377
+ potential_path = os.path.join(template_dir, f"{page_name}.jinja2")
378
+ if os.path.exists(potential_path):
379
+ parent_template_path = potential_path
380
+ break
381
+ if parent_template_path:
382
+ break
383
+
384
+ if parent_template_path:
385
+ # Load the parent template with translation support
386
+ parent_content = load_template_with_translation(parent_template_path)
387
+ if parent_content:
388
+ # Create a template object from the translated content
389
+ parent_template = env.from_string(parent_content)
390
+ # We need to preserve the original template name for Jinja2 inheritance
391
+ parent_template.name = f"{page_name}.jinja2"
392
+ parent_template.filename = parent_template_path
393
+ else:
394
+ # Fallback to loading without translation if something went wrong
395
+ parent_template = parent_env.get_template(f"{page_name}.jinja2")
396
+ else:
397
+ # Fallback to original method if we can't find the file path
398
+ parent_template = parent_env.get_template(f"{page_name}.jinja2")
399
+
246
400
  print(f"parent_template", parent_template)
247
401
  child_templates = await load_plugin_templates(page_name, plugins)
248
402
  parent_blocks = parent_template.blocks.keys()
@@ -307,7 +461,7 @@ async def render_combined_template(page_name, plugins, context):
307
461
  return rendered_html
308
462
 
309
463
  async def render_direct_template(template_path, context):
310
- """Render a template directly without combining with a parent template.
464
+ """Render a template directly without combining with a parent template, with translation support.
311
465
 
312
466
  Args:
313
467
  template_path (str): Path to the template
@@ -320,6 +474,25 @@ async def render_direct_template(template_path, context):
320
474
  context = context or {}
321
475
 
322
476
  try:
477
+ # Check if we need to load with translation support
478
+ template_full_path = None
479
+ for loader in env.loader.loaders:
480
+ for template_dir in loader.searchpath:
481
+ potential_path = os.path.join(template_dir, template_path)
482
+ if os.path.exists(potential_path):
483
+ template_full_path = potential_path
484
+ break
485
+ if template_full_path:
486
+ break
487
+
488
+ if template_full_path:
489
+ # Load with translation support
490
+ content = load_template_with_translation(template_full_path)
491
+ if content:
492
+ template = env.from_string(content)
493
+ return template.render(**context)
494
+
495
+ # Fallback to normal template loading
323
496
  template = env.get_template(template_path)
324
497
  return template.render(**context)
325
498
  except Exception as e:
@@ -327,7 +500,7 @@ async def render_direct_template(template_path, context):
327
500
  return f"<h1>Error rendering template</h1><p>{str(e)}</p>"
328
501
 
329
502
  async def render(page_name, context):
330
- """Render a template with plugin injections and overrides.
503
+ """Render a template with plugin injections, overrides, and translation support.
331
504
  If no parent template exists, tries to render a template directly from a plugin.
332
505
 
333
506
  Args:
@@ -368,4 +541,3 @@ async def render(page_name, context):
368
541
  trace = traceback.format_exc()
369
542
  print(f"Error rendering {page_name}: {e}\n\n{trace}")
370
543
  return f"<h1>Error Rendering Page</h1><p>{str(e)}</p><pre>{trace}</pre>"
371
-
@@ -141,7 +141,7 @@ def merge_json_arrays(data, partial=False):
141
141
  pass
142
142
 
143
143
  # Combine all parsed arrays into a single list
144
- if arrays:
144
+ if len(arrays) > 0:
145
145
  return list(chain.from_iterable(arrays))
146
146
 
147
147
  # If all else fails, try the original json.loads as fallback