mneme-cli 0.4.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.
- mneme/__init__.py +8 -0
- mneme/__main__.py +5 -0
- mneme/config.py +103 -0
- mneme/core.py +6526 -0
- mneme/profiles/eu-mdr.md +196 -0
- mneme/profiles/iso-13485.md +182 -0
- mneme/profiles/mappings/dds.json +21 -0
- mneme/profiles/mappings/requirements.json +22 -0
- mneme/profiles/mappings/risk-register.json +24 -0
- mneme/profiles/mappings/test-cases.json +21 -0
- mneme/profiles/mappings/user-needs.json +19 -0
- mneme/server.py +312 -0
- mneme/templates/workspace/.gitignore +9 -0
- mneme/templates/workspace/AGENTS.md +706 -0
- mneme/templates/workspace/README.md +33 -0
- mneme/templates/workspace/inbox/.gitkeep +0 -0
- mneme/templates/workspace/index.md +18 -0
- mneme/templates/workspace/log.md +6 -0
- mneme/templates/workspace/profiles/README.md +109 -0
- mneme/templates/workspace/profiles/mappings/.gitkeep +0 -0
- mneme/templates/workspace/schema/entities.json +5 -0
- mneme/templates/workspace/schema/graph.json +6 -0
- mneme/templates/workspace/schema/tags.json +5 -0
- mneme/templates/workspace/sources/.gitkeep +0 -0
- mneme/templates/workspace/wiki/_templates/page.md +31 -0
- mneme/ui.html +1520 -0
- mneme_cli-0.4.0.dist-info/METADATA +499 -0
- mneme_cli-0.4.0.dist-info/RECORD +32 -0
- mneme_cli-0.4.0.dist-info/WHEEL +5 -0
- mneme_cli-0.4.0.dist-info/entry_points.txt +2 -0
- mneme_cli-0.4.0.dist-info/licenses/LICENSE +21 -0
- mneme_cli-0.4.0.dist-info/top_level.txt +1 -0
mneme/server.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
server.py - Mnemosyne local web UI server.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
cd mneme && python3 server.py
|
|
6
|
+
|
|
7
|
+
Runs on http://localhost:3141
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
import traceback
|
|
15
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
16
|
+
from urllib.parse import urlparse, parse_qs, unquote
|
|
17
|
+
|
|
18
|
+
from .config import (
|
|
19
|
+
WIKI_DIR,
|
|
20
|
+
SCHEMA_DIR,
|
|
21
|
+
LOG_FILE,
|
|
22
|
+
INDEX_FILE,
|
|
23
|
+
UI_FILE,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Lazy import core so memvid errors don't kill startup
|
|
27
|
+
try:
|
|
28
|
+
from .core import (
|
|
29
|
+
get_stats,
|
|
30
|
+
dual_search,
|
|
31
|
+
check_drift,
|
|
32
|
+
parse_frontmatter,
|
|
33
|
+
sync_all_pages,
|
|
34
|
+
)
|
|
35
|
+
CORE_OK = True
|
|
36
|
+
except Exception as e:
|
|
37
|
+
CORE_OK = False
|
|
38
|
+
CORE_ERROR = str(e)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
PORT = 3141
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _json_response(data):
|
|
45
|
+
return json.dumps(data, default=str).encode('utf-8')
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _error(msg, code=500):
|
|
49
|
+
return code, _json_response({'error': msg})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def handle_stats():
|
|
53
|
+
if not CORE_OK:
|
|
54
|
+
return 500, _json_response({'error': CORE_ERROR})
|
|
55
|
+
try:
|
|
56
|
+
data = get_stats()
|
|
57
|
+
return 200, _json_response(data)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
return 500, _json_response({'error': str(e), 'trace': traceback.format_exc()})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def handle_search(query: str):
|
|
63
|
+
if not CORE_OK:
|
|
64
|
+
return 500, _json_response({'error': CORE_ERROR})
|
|
65
|
+
if not query.strip():
|
|
66
|
+
return 400, _json_response({'error': 'Empty query'})
|
|
67
|
+
try:
|
|
68
|
+
t0 = time.time()
|
|
69
|
+
results = dual_search(query)
|
|
70
|
+
elapsed_ms = round((time.time() - t0) * 1000, 1)
|
|
71
|
+
return 200, _json_response({'results': results, 'count': len(results), 'query': query, 'elapsed_ms': elapsed_ms})
|
|
72
|
+
except Exception as e:
|
|
73
|
+
return 500, _json_response({'error': str(e), 'trace': traceback.format_exc()})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def handle_drift():
|
|
77
|
+
if not CORE_OK:
|
|
78
|
+
return 500, _json_response({'error': CORE_ERROR})
|
|
79
|
+
try:
|
|
80
|
+
data = check_drift()
|
|
81
|
+
return 200, _json_response(data)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
return 500, _json_response({'error': str(e), 'trace': traceback.format_exc()})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def handle_wiki_list():
|
|
87
|
+
"""Return a nested structure: {client: [page_slug, ...]}"""
|
|
88
|
+
import glob
|
|
89
|
+
from pathlib import Path
|
|
90
|
+
try:
|
|
91
|
+
pattern = os.path.join(WIKI_DIR, '**', '*.md')
|
|
92
|
+
all_pages = glob.glob(pattern, recursive=True)
|
|
93
|
+
tree = {}
|
|
94
|
+
for page in sorted(all_pages):
|
|
95
|
+
rel = os.path.relpath(page, WIKI_DIR)
|
|
96
|
+
parts = Path(rel).parts
|
|
97
|
+
# Skip _templates
|
|
98
|
+
if any(p.startswith('_templates') for p in parts):
|
|
99
|
+
continue
|
|
100
|
+
client = parts[0] if len(parts) > 1 else '_root'
|
|
101
|
+
slug = os.path.splitext(rel)[0] # path without .md
|
|
102
|
+
if client not in tree:
|
|
103
|
+
tree[client] = []
|
|
104
|
+
# Get title from frontmatter if possible
|
|
105
|
+
title = slug.split('/')[-1]
|
|
106
|
+
try:
|
|
107
|
+
with open(page, 'r', encoding='utf-8') as f:
|
|
108
|
+
content = f.read()
|
|
109
|
+
if CORE_OK:
|
|
110
|
+
fm, _ = parse_frontmatter(content)
|
|
111
|
+
title = fm.get('title', title)
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
tree[client].append({'slug': slug, 'title': title})
|
|
115
|
+
return 200, _json_response({'tree': tree})
|
|
116
|
+
except Exception as e:
|
|
117
|
+
return 500, _json_response({'error': str(e)})
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def handle_wiki_page(path_suffix: str):
|
|
121
|
+
"""
|
|
122
|
+
Read a specific wiki page. path_suffix is like 'demo-retail/sample-proposal'
|
|
123
|
+
(no .md extension - we add it).
|
|
124
|
+
"""
|
|
125
|
+
from pathlib import Path
|
|
126
|
+
|
|
127
|
+
clean = path_suffix.lstrip('/')
|
|
128
|
+
if not clean:
|
|
129
|
+
return 400, _json_response({'error': 'No path given'})
|
|
130
|
+
|
|
131
|
+
# Try with and without .md extension, restricted to WIKI_DIR only
|
|
132
|
+
wiki_base = Path(WIKI_DIR).resolve()
|
|
133
|
+
candidates = [
|
|
134
|
+
wiki_base / clean,
|
|
135
|
+
wiki_base / (clean + '.md'),
|
|
136
|
+
]
|
|
137
|
+
page_path = None
|
|
138
|
+
for c in candidates:
|
|
139
|
+
resolved = c.resolve()
|
|
140
|
+
# Bounds check: must be within WIKI_DIR
|
|
141
|
+
if not str(resolved).startswith(str(wiki_base)):
|
|
142
|
+
continue
|
|
143
|
+
if resolved.is_file() and str(resolved).endswith('.md'):
|
|
144
|
+
page_path = str(resolved)
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
if not page_path:
|
|
148
|
+
return 404, _json_response({'error': f'Page not found: {clean}'})
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
with open(page_path, 'r', encoding='utf-8') as f:
|
|
152
|
+
content = f.read()
|
|
153
|
+
frontmatter = {}
|
|
154
|
+
body = content
|
|
155
|
+
if CORE_OK:
|
|
156
|
+
frontmatter, body = parse_frontmatter(content)
|
|
157
|
+
return 200, _json_response({
|
|
158
|
+
'path': clean,
|
|
159
|
+
'frontmatter': frontmatter,
|
|
160
|
+
'body': body,
|
|
161
|
+
'raw': content,
|
|
162
|
+
})
|
|
163
|
+
except Exception as e:
|
|
164
|
+
return 500, _json_response({'error': str(e)})
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def handle_entities():
|
|
168
|
+
path = os.path.join(SCHEMA_DIR, 'entities.json')
|
|
169
|
+
if not os.path.exists(path):
|
|
170
|
+
return 404, _json_response({'error': 'entities.json not found'})
|
|
171
|
+
try:
|
|
172
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
173
|
+
data = json.load(f)
|
|
174
|
+
return 200, _json_response(data)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
return 500, _json_response({'error': str(e)})
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def handle_tags():
|
|
180
|
+
path = os.path.join(SCHEMA_DIR, 'tags.json')
|
|
181
|
+
if not os.path.exists(path):
|
|
182
|
+
return 404, _json_response({'error': 'tags.json not found'})
|
|
183
|
+
try:
|
|
184
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
185
|
+
data = json.load(f)
|
|
186
|
+
return 200, _json_response(data)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
return 500, _json_response({'error': str(e)})
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def handle_log():
|
|
192
|
+
if not os.path.exists(LOG_FILE):
|
|
193
|
+
return 404, _json_response({'error': 'log.md not found'})
|
|
194
|
+
try:
|
|
195
|
+
with open(LOG_FILE, 'r', encoding='utf-8') as f:
|
|
196
|
+
content = f.read()
|
|
197
|
+
return 200, _json_response({'content': content})
|
|
198
|
+
except Exception as e:
|
|
199
|
+
return 500, _json_response({'error': str(e)})
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def handle_sync():
|
|
203
|
+
if not CORE_OK:
|
|
204
|
+
return 500, _json_response({'error': CORE_ERROR})
|
|
205
|
+
try:
|
|
206
|
+
result = sync_all_pages()
|
|
207
|
+
return 200, _json_response(result)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
return 500, _json_response({'error': str(e), 'trace': traceback.format_exc()})
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class MnemeHandler(BaseHTTPRequestHandler):
|
|
213
|
+
|
|
214
|
+
def log_message(self, format, *args):
|
|
215
|
+
# Custom minimal logging
|
|
216
|
+
print(f' {self.command} {self.path} -> {args[1] if len(args) > 1 else "?"}')
|
|
217
|
+
|
|
218
|
+
def _send(self, code: int, body: bytes, content_type: str = 'application/json'):
|
|
219
|
+
self.send_response(code)
|
|
220
|
+
self.send_header('Content-Type', content_type)
|
|
221
|
+
self.send_header('Content-Length', str(len(body)))
|
|
222
|
+
self.send_header('Access-Control-Allow-Origin', 'http://localhost:3141')
|
|
223
|
+
self.end_headers()
|
|
224
|
+
self.wfile.write(body)
|
|
225
|
+
|
|
226
|
+
def do_GET(self):
|
|
227
|
+
parsed = urlparse(self.path)
|
|
228
|
+
path = parsed.path
|
|
229
|
+
qs = parse_qs(parsed.query)
|
|
230
|
+
|
|
231
|
+
# Serve UI
|
|
232
|
+
if path == '/' or path == '/index.html':
|
|
233
|
+
if os.path.exists(UI_FILE):
|
|
234
|
+
with open(UI_FILE, 'rb') as f:
|
|
235
|
+
body = f.read()
|
|
236
|
+
self._send(200, body, 'text/html; charset=utf-8')
|
|
237
|
+
else:
|
|
238
|
+
self._send(404, b'<h1>ui.html not found</h1>', 'text/html')
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
# API routing
|
|
242
|
+
if path == '/api/stats':
|
|
243
|
+
code, body = handle_stats()
|
|
244
|
+
self._send(code, body)
|
|
245
|
+
|
|
246
|
+
elif path == '/api/search':
|
|
247
|
+
query = qs.get('q', [''])[0]
|
|
248
|
+
code, body = handle_search(query)
|
|
249
|
+
self._send(code, body)
|
|
250
|
+
|
|
251
|
+
elif path == '/api/drift':
|
|
252
|
+
code, body = handle_drift()
|
|
253
|
+
self._send(code, body)
|
|
254
|
+
|
|
255
|
+
elif path == '/api/wiki':
|
|
256
|
+
code, body = handle_wiki_list()
|
|
257
|
+
self._send(code, body)
|
|
258
|
+
|
|
259
|
+
elif path.startswith('/api/wiki/'):
|
|
260
|
+
suffix = unquote(path[len('/api/wiki/'):])
|
|
261
|
+
code, body = handle_wiki_page(suffix)
|
|
262
|
+
self._send(code, body)
|
|
263
|
+
|
|
264
|
+
elif path == '/api/entities':
|
|
265
|
+
code, body = handle_entities()
|
|
266
|
+
self._send(code, body)
|
|
267
|
+
|
|
268
|
+
elif path == '/api/tags':
|
|
269
|
+
code, body = handle_tags()
|
|
270
|
+
self._send(code, body)
|
|
271
|
+
|
|
272
|
+
elif path == '/api/log':
|
|
273
|
+
code, body = handle_log()
|
|
274
|
+
self._send(code, body)
|
|
275
|
+
|
|
276
|
+
else:
|
|
277
|
+
self._send(404, _json_response({'error': f'Unknown endpoint: {path}'}))
|
|
278
|
+
|
|
279
|
+
def do_POST(self):
|
|
280
|
+
parsed = urlparse(self.path)
|
|
281
|
+
path = parsed.path
|
|
282
|
+
|
|
283
|
+
if path == '/api/sync':
|
|
284
|
+
code, body = handle_sync()
|
|
285
|
+
self._send(code, body)
|
|
286
|
+
else:
|
|
287
|
+
self._send(404, _json_response({'error': f'Unknown endpoint: {path}'}))
|
|
288
|
+
|
|
289
|
+
def do_OPTIONS(self):
|
|
290
|
+
self.send_response(200)
|
|
291
|
+
self.send_header('Access-Control-Allow-Origin', 'http://localhost:3141')
|
|
292
|
+
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
293
|
+
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
|
294
|
+
self.end_headers()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def main():
|
|
298
|
+
server = HTTPServer(('localhost', PORT), MnemeHandler)
|
|
299
|
+
print(f'Mnemosyne UI running at http://localhost:{PORT}')
|
|
300
|
+
print('Press Ctrl+C to stop')
|
|
301
|
+
if not CORE_OK:
|
|
302
|
+
print(f'[WARNING] core import failed: {CORE_ERROR}')
|
|
303
|
+
print('[WARNING] Some API endpoints will return errors. Memvid features disabled.')
|
|
304
|
+
try:
|
|
305
|
+
server.serve_forever()
|
|
306
|
+
except KeyboardInterrupt:
|
|
307
|
+
print('\nStopped.')
|
|
308
|
+
server.server_close()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
if __name__ == '__main__':
|
|
312
|
+
main()
|