claude-memory-agent 2.2.4 → 3.0.0
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.
- package/hooks/auto_capture.py +58 -1
- package/hooks/grounding-hook-v2.py +129 -0
- package/hooks/grounding-hook.py +95 -0
- package/hooks/session_end_hook.py +35 -0
- package/hooks/session_start.py +56 -0
- package/main.py +165 -0
- package/mcp_proxy.py +307 -0
- package/mcp_server_full.py +497 -0
- package/package.json +1 -1
- package/services/native_memory_sync.py +66 -310
|
@@ -1,31 +1,36 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""One-way sync: Claude Code's native MEMORY.md -> MCP vector DB.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Native MEMORY.md is owned exclusively by Claude Code's auto memory.
|
|
4
|
+
This module ingests its contents into the MCP vector DB at session end
|
|
5
|
+
so they become searchable via semantic search.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
The MCP-to-Native direction has been removed to avoid competing with
|
|
8
|
+
Claude Code for the 200-line MEMORY.md budget.
|
|
8
9
|
|
|
9
|
-
Dedup: markdown_syncs.content_hash prevents
|
|
10
|
+
Dedup: markdown_syncs.content_hash prevents duplicate imports.
|
|
10
11
|
"""
|
|
11
12
|
import hashlib
|
|
13
|
+
import json
|
|
12
14
|
import logging
|
|
13
15
|
import re
|
|
14
16
|
from datetime import datetime
|
|
15
17
|
from pathlib import Path
|
|
16
|
-
from typing import Dict, Any, List
|
|
18
|
+
from typing import Dict, Any, List
|
|
17
19
|
|
|
18
20
|
from services.native_memory_paths import get_native_memory_md, list_native_memory_files
|
|
19
21
|
|
|
20
22
|
logger = logging.getLogger(__name__)
|
|
21
23
|
|
|
24
|
+
# Source tag used for native->MCP memories
|
|
25
|
+
NATIVE_SOURCE_TAG = "source=native_memory_md"
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
# Legacy markers (used only for stripping during import)
|
|
28
|
+
_MCP_SYNC_START = "<!-- MCP-SYNCED START -->"
|
|
29
|
+
_MCP_SYNC_END = "<!-- MCP-SYNCED END -->"
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"""
|
|
31
|
+
|
|
32
|
+
def _ensure_markdown_syncs_table(conn):
|
|
33
|
+
"""Create the markdown_syncs table if it doesn't exist yet."""
|
|
29
34
|
conn.execute("""
|
|
30
35
|
CREATE TABLE IF NOT EXISTS markdown_syncs (
|
|
31
36
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -44,226 +49,70 @@ def _ensure_markdown_syncs_table(conn):
|
|
|
44
49
|
conn.commit()
|
|
45
50
|
|
|
46
51
|
|
|
47
|
-
# Markers for the MCP-synced section in native MEMORY.md
|
|
48
|
-
MCP_SYNC_START = "<!-- MCP-SYNCED START -->"
|
|
49
|
-
MCP_SYNC_END = "<!-- MCP-SYNCED END -->"
|
|
50
|
-
|
|
51
|
-
# Budget: native auto memory loads first 200 lines, leave 20 headroom
|
|
52
|
-
MAX_NATIVE_LINES = 180
|
|
53
|
-
|
|
54
|
-
# Source tag used for native->MCP memories
|
|
55
|
-
NATIVE_SOURCE_TAG = "source=native_memory_md"
|
|
56
|
-
|
|
57
|
-
|
|
58
52
|
def _content_hash(text: str) -> str:
|
|
59
53
|
"""Generate a short hash for dedup tracking."""
|
|
60
54
|
return hashlib.md5(text.strip().encode("utf-8")).hexdigest()[:16]
|
|
61
55
|
|
|
62
56
|
|
|
63
|
-
# ── Direction A: MCP -> Native ────────────────────────────────────────────
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
async def sync_mcp_to_native(
|
|
67
|
-
db,
|
|
68
|
-
project_path: str,
|
|
69
|
-
min_importance: int = 7,
|
|
70
|
-
) -> Dict[str, Any]:
|
|
71
|
-
"""Sync high-importance MCP memories into the native MEMORY.md fenced section.
|
|
72
|
-
|
|
73
|
-
1. Query MCP DB for important memories for this project
|
|
74
|
-
2. Skip already-synced (via markdown_syncs with file_type='mcp_to_native')
|
|
75
|
-
3. Read native MEMORY.md, check line budget
|
|
76
|
-
4. Replace content between MCP-SYNCED markers (non-destructive)
|
|
77
|
-
5. Record synced entries in markdown_syncs
|
|
78
|
-
|
|
79
|
-
Args:
|
|
80
|
-
db: DatabaseService instance
|
|
81
|
-
project_path: Absolute project path (e.g. C:\\xampp\\htdocs\\server)
|
|
82
|
-
min_importance: Minimum importance threshold (default 7)
|
|
83
|
-
|
|
84
|
-
Returns:
|
|
85
|
-
Dict with sync results
|
|
86
|
-
"""
|
|
87
|
-
from services.database import normalize_path
|
|
88
|
-
norm_path = normalize_path(project_path)
|
|
89
|
-
|
|
90
|
-
_ensure_markdown_syncs_table(db.conn)
|
|
91
|
-
cursor = db.conn.cursor()
|
|
92
|
-
|
|
93
|
-
# 1. Query high-importance decisions, preferences, and successful errors
|
|
94
|
-
try:
|
|
95
|
-
cursor.execute("""
|
|
96
|
-
SELECT id, type, content, importance, success, created_at
|
|
97
|
-
FROM memories
|
|
98
|
-
WHERE importance >= ?
|
|
99
|
-
AND (project_path = ? OR project_path IS NULL)
|
|
100
|
-
AND (
|
|
101
|
-
type IN ('decision', 'preference')
|
|
102
|
-
OR (type = 'error' AND success = 1)
|
|
103
|
-
)
|
|
104
|
-
ORDER BY importance DESC, created_at DESC
|
|
105
|
-
LIMIT 30
|
|
106
|
-
""", (min_importance, norm_path))
|
|
107
|
-
memories = [dict(row) for row in cursor.fetchall()]
|
|
108
|
-
except Exception as e:
|
|
109
|
-
logger.error(f"MCP->Native: failed to query memories: {e}")
|
|
110
|
-
return {"success": False, "error": str(e)}
|
|
111
|
-
|
|
112
|
-
if not memories:
|
|
113
|
-
return {"success": True, "synced": 0, "reason": "no qualifying memories"}
|
|
114
|
-
|
|
115
|
-
# 2. Check which are already synced
|
|
116
|
-
already_synced_ids = set()
|
|
117
|
-
try:
|
|
118
|
-
cursor.execute("""
|
|
119
|
-
SELECT memory_id FROM markdown_syncs
|
|
120
|
-
WHERE file_type = 'mcp_to_native'
|
|
121
|
-
AND project_path = ?
|
|
122
|
-
""", (norm_path,))
|
|
123
|
-
already_synced_ids = {row["memory_id"] for row in cursor.fetchall()}
|
|
124
|
-
except Exception as e:
|
|
125
|
-
logger.warning(f"MCP->Native: failed to check synced IDs: {e}")
|
|
126
|
-
|
|
127
|
-
# Also get all synced content hashes for this direction
|
|
128
|
-
already_synced_hashes = set()
|
|
129
|
-
try:
|
|
130
|
-
cursor.execute("""
|
|
131
|
-
SELECT content_hash FROM markdown_syncs
|
|
132
|
-
WHERE file_type = 'mcp_to_native'
|
|
133
|
-
AND project_path = ?
|
|
134
|
-
""", (norm_path,))
|
|
135
|
-
already_synced_hashes = {row["content_hash"] for row in cursor.fetchall()}
|
|
136
|
-
except Exception:
|
|
137
|
-
pass
|
|
138
|
-
|
|
139
|
-
# Filter to new memories only
|
|
140
|
-
new_memories = []
|
|
141
|
-
for mem in memories:
|
|
142
|
-
h = _content_hash(mem["content"])
|
|
143
|
-
if mem["id"] not in already_synced_ids and h not in already_synced_hashes:
|
|
144
|
-
new_memories.append(mem)
|
|
145
|
-
|
|
146
|
-
if not new_memories:
|
|
147
|
-
return {"success": True, "synced": 0, "reason": "all already synced"}
|
|
148
|
-
|
|
149
|
-
# 3. Read native MEMORY.md
|
|
150
|
-
native_md_path = get_native_memory_md(project_path)
|
|
151
|
-
|
|
152
|
-
if native_md_path.exists():
|
|
153
|
-
existing_content = native_md_path.read_text(encoding="utf-8")
|
|
154
|
-
else:
|
|
155
|
-
existing_content = ""
|
|
156
|
-
|
|
157
|
-
# Count lines outside the MCP-synced section
|
|
158
|
-
user_content = _strip_mcp_section(existing_content)
|
|
159
|
-
user_line_count = len(user_content.splitlines())
|
|
160
|
-
|
|
161
|
-
if user_line_count >= MAX_NATIVE_LINES:
|
|
162
|
-
logger.warning(
|
|
163
|
-
f"MCP->Native: MEMORY.md already at {user_line_count} lines "
|
|
164
|
-
f"(budget {MAX_NATIVE_LINES}), skipping sync"
|
|
165
|
-
)
|
|
166
|
-
return {
|
|
167
|
-
"success": True,
|
|
168
|
-
"synced": 0,
|
|
169
|
-
"reason": f"line budget exceeded ({user_line_count}/{MAX_NATIVE_LINES})",
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
# 4. Build the fenced section content
|
|
173
|
-
# Include previously synced memories too (rebuild full section)
|
|
174
|
-
all_for_section = memories # all qualifying, not just new
|
|
175
|
-
section_lines = _build_mcp_section(all_for_section)
|
|
176
|
-
|
|
177
|
-
# Check combined line count
|
|
178
|
-
total_lines = user_line_count + len(section_lines)
|
|
179
|
-
if total_lines > MAX_NATIVE_LINES:
|
|
180
|
-
# Trim section to fit budget
|
|
181
|
-
budget = MAX_NATIVE_LINES - user_line_count
|
|
182
|
-
if budget < 5:
|
|
183
|
-
return {
|
|
184
|
-
"success": True,
|
|
185
|
-
"synced": 0,
|
|
186
|
-
"reason": "not enough line budget for MCP section",
|
|
187
|
-
}
|
|
188
|
-
section_lines = section_lines[:budget]
|
|
189
|
-
|
|
190
|
-
# 5. Replace or append the fenced section
|
|
191
|
-
section_text = "\n".join(section_lines)
|
|
192
|
-
updated_content = _replace_mcp_section(user_content, section_text)
|
|
193
|
-
|
|
194
|
-
# Write back
|
|
195
|
-
native_md_path.parent.mkdir(parents=True, exist_ok=True)
|
|
196
|
-
native_md_path.write_text(updated_content, encoding="utf-8")
|
|
197
|
-
|
|
198
|
-
# 6. Record in markdown_syncs
|
|
199
|
-
now = datetime.now().isoformat()
|
|
200
|
-
synced_count = 0
|
|
201
|
-
for mem in new_memories:
|
|
202
|
-
h = _content_hash(mem["content"])
|
|
203
|
-
try:
|
|
204
|
-
cursor.execute("""
|
|
205
|
-
INSERT INTO markdown_syncs (file_type, file_path, memory_id, project_path, synced_at, content_hash)
|
|
206
|
-
VALUES ('mcp_to_native', ?, ?, ?, ?, ?)
|
|
207
|
-
""", (str(native_md_path), mem["id"], norm_path, now, h))
|
|
208
|
-
synced_count += 1
|
|
209
|
-
except Exception as e:
|
|
210
|
-
logger.warning(f"MCP->Native: failed to record sync for memory {mem['id']}: {e}")
|
|
211
|
-
|
|
212
|
-
db.conn.commit()
|
|
213
|
-
|
|
214
|
-
return {
|
|
215
|
-
"success": True,
|
|
216
|
-
"synced": synced_count,
|
|
217
|
-
"total_in_section": len(all_for_section),
|
|
218
|
-
"file": str(native_md_path),
|
|
219
|
-
"user_lines": user_line_count,
|
|
220
|
-
"total_lines": len(updated_content.splitlines()),
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def _build_mcp_section(memories: list) -> List[str]:
|
|
225
|
-
"""Build the fenced MCP-SYNCED section lines."""
|
|
226
|
-
lines = [
|
|
227
|
-
MCP_SYNC_START,
|
|
228
|
-
"## Synced from MCP Memory DB",
|
|
229
|
-
"",
|
|
230
|
-
]
|
|
231
|
-
for mem in memories:
|
|
232
|
-
mtype = mem.get("type", "chunk")
|
|
233
|
-
content = mem.get("content", "").replace("\n", " ").strip()
|
|
234
|
-
# Truncate long entries
|
|
235
|
-
if len(content) > 200:
|
|
236
|
-
content = content[:197] + "..."
|
|
237
|
-
importance = mem.get("importance", 5)
|
|
238
|
-
lines.append(f"- [{mtype}] {content} (importance: {importance})")
|
|
239
|
-
|
|
240
|
-
lines.append("")
|
|
241
|
-
lines.append(MCP_SYNC_END)
|
|
242
|
-
return lines
|
|
243
|
-
|
|
244
|
-
|
|
245
57
|
def _strip_mcp_section(content: str) -> str:
|
|
246
|
-
"""Remove
|
|
58
|
+
"""Remove any legacy MCP-SYNCED fenced section from content."""
|
|
247
59
|
pattern = re.compile(
|
|
248
|
-
re.escape(
|
|
60
|
+
re.escape(_MCP_SYNC_START) + r".*?" + re.escape(_MCP_SYNC_END),
|
|
249
61
|
re.DOTALL,
|
|
250
62
|
)
|
|
251
63
|
stripped = pattern.sub("", content)
|
|
252
|
-
# Clean up extra blank lines left behind
|
|
253
64
|
stripped = re.sub(r"\n{3,}", "\n\n", stripped)
|
|
254
65
|
return stripped.rstrip("\n") + "\n" if stripped.strip() else ""
|
|
255
66
|
|
|
256
67
|
|
|
257
|
-
def
|
|
258
|
-
"""
|
|
259
|
-
# Ensure user content ends with a newline
|
|
260
|
-
if user_content and not user_content.endswith("\n"):
|
|
261
|
-
user_content += "\n"
|
|
68
|
+
def _parse_markdown_entries(content: str, source_file: str = "MEMORY.md") -> List[Dict[str, Any]]:
|
|
69
|
+
"""Parse markdown into discrete entries for import.
|
|
262
70
|
|
|
263
|
-
|
|
71
|
+
Splits on ## headers. Each section becomes one entry.
|
|
72
|
+
Bullet groups under a section are kept together.
|
|
73
|
+
Very short entries (< 20 chars) are skipped.
|
|
74
|
+
"""
|
|
75
|
+
if not content.strip():
|
|
76
|
+
return []
|
|
264
77
|
|
|
78
|
+
entries = []
|
|
79
|
+
lines = content.splitlines()
|
|
265
80
|
|
|
266
|
-
|
|
81
|
+
current_section = ""
|
|
82
|
+
current_lines: list = []
|
|
83
|
+
|
|
84
|
+
for line in lines:
|
|
85
|
+
if line.startswith("## "):
|
|
86
|
+
# Flush previous section
|
|
87
|
+
if current_lines:
|
|
88
|
+
text = "\n".join(current_lines).strip()
|
|
89
|
+
if len(text) >= 20:
|
|
90
|
+
entries.append({
|
|
91
|
+
"content": text,
|
|
92
|
+
"section": current_section,
|
|
93
|
+
"source_file": source_file,
|
|
94
|
+
})
|
|
95
|
+
current_section = line[3:].strip()
|
|
96
|
+
current_lines = [line]
|
|
97
|
+
elif line.startswith("# ") and not current_lines:
|
|
98
|
+
continue
|
|
99
|
+
else:
|
|
100
|
+
current_lines.append(line)
|
|
101
|
+
|
|
102
|
+
# Flush last section
|
|
103
|
+
if current_lines:
|
|
104
|
+
text = "\n".join(current_lines).strip()
|
|
105
|
+
if len(text) >= 20:
|
|
106
|
+
entries.append({
|
|
107
|
+
"content": text,
|
|
108
|
+
"section": current_section,
|
|
109
|
+
"source_file": source_file,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return entries
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ── Native -> MCP (the only sync direction) ──────────────────────────
|
|
267
116
|
|
|
268
117
|
|
|
269
118
|
async def sync_native_to_mcp(
|
|
@@ -275,7 +124,7 @@ async def sync_native_to_mcp(
|
|
|
275
124
|
|
|
276
125
|
1. Read native MEMORY.md + topic files
|
|
277
126
|
2. Parse into entries (## sections or bullet groups)
|
|
278
|
-
3.
|
|
127
|
+
3. Strip any legacy MCP-SYNCED section (avoid circular import)
|
|
279
128
|
4. Hash each entry, check markdown_syncs for file_type='native_to_mcp'
|
|
280
129
|
5. For new/changed entries: generate embedding, store in MCP DB
|
|
281
130
|
6. Record in markdown_syncs
|
|
@@ -306,9 +155,8 @@ async def sync_native_to_mcp(
|
|
|
306
155
|
logger.warning(f"Native->MCP: failed to read {fpath}: {e}")
|
|
307
156
|
continue
|
|
308
157
|
|
|
309
|
-
# Strip
|
|
158
|
+
# Strip any legacy MCP-synced section
|
|
310
159
|
clean = _strip_mcp_section(raw)
|
|
311
|
-
|
|
312
160
|
parsed = _parse_markdown_entries(clean, source_file=fpath.name)
|
|
313
161
|
entries.extend(parsed)
|
|
314
162
|
|
|
@@ -346,31 +194,25 @@ async def sync_native_to_mcp(
|
|
|
346
194
|
|
|
347
195
|
for entry in new_entries:
|
|
348
196
|
try:
|
|
349
|
-
# Generate embedding
|
|
350
197
|
emb_result = await embeddings.generate_embedding(entry["content"])
|
|
351
198
|
if hasattr(emb_result, "ok") and not emb_result.ok:
|
|
352
199
|
logger.warning(f"Native->MCP: embedding failed for entry: {emb_result.error_message}")
|
|
353
200
|
errors.append(entry["content"][:50])
|
|
354
201
|
continue
|
|
355
202
|
|
|
356
|
-
# Extract the embedding vector
|
|
357
203
|
embedding = emb_result.embedding if hasattr(emb_result, "embedding") else emb_result
|
|
358
204
|
|
|
359
|
-
# Build tags
|
|
360
205
|
tags = [NATIVE_SOURCE_TAG]
|
|
361
206
|
if entry.get("source_file"):
|
|
362
207
|
tags.append(f"file={entry['source_file']}")
|
|
363
208
|
if entry.get("section"):
|
|
364
209
|
tags.append(f"section={entry['section']}")
|
|
365
210
|
|
|
366
|
-
# Store in MCP DB
|
|
367
|
-
import json
|
|
368
211
|
embedding_json = json.dumps(
|
|
369
212
|
embedding if isinstance(embedding, list) else embedding.tolist()
|
|
370
213
|
if hasattr(embedding, "tolist") else list(embedding)
|
|
371
214
|
)
|
|
372
215
|
|
|
373
|
-
# Use only columns guaranteed to exist in the active DB schema
|
|
374
216
|
cursor.execute("""
|
|
375
217
|
INSERT INTO memories (type, content, embedding, project_path, importance, tags, created_at)
|
|
376
218
|
VALUES ('chunk', ?, ?, ?, 6, ?, ?)
|
|
@@ -383,7 +225,6 @@ async def sync_native_to_mcp(
|
|
|
383
225
|
))
|
|
384
226
|
memory_id = cursor.lastrowid
|
|
385
227
|
|
|
386
|
-
# Record in markdown_syncs
|
|
387
228
|
cursor.execute("""
|
|
388
229
|
INSERT INTO markdown_syncs (file_type, file_path, memory_id, project_path, synced_at, content_hash)
|
|
389
230
|
VALUES ('native_to_mcp', ?, ?, ?, ?, ?)
|
|
@@ -409,88 +250,3 @@ async def sync_native_to_mcp(
|
|
|
409
250
|
"new_entries": len(new_entries),
|
|
410
251
|
"errors": errors if errors else None,
|
|
411
252
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
def _parse_markdown_entries(content: str, source_file: str = "MEMORY.md") -> List[Dict[str, Any]]:
|
|
415
|
-
"""Parse markdown into discrete entries for import.
|
|
416
|
-
|
|
417
|
-
Splits on ## headers. Each section becomes one entry.
|
|
418
|
-
Bullet groups under a section are kept together.
|
|
419
|
-
Very short entries (< 20 chars) are skipped.
|
|
420
|
-
"""
|
|
421
|
-
if not content.strip():
|
|
422
|
-
return []
|
|
423
|
-
|
|
424
|
-
entries = []
|
|
425
|
-
lines = content.splitlines()
|
|
426
|
-
|
|
427
|
-
current_section = ""
|
|
428
|
-
current_lines = []
|
|
429
|
-
|
|
430
|
-
for line in lines:
|
|
431
|
-
if line.startswith("## "):
|
|
432
|
-
# Flush previous section
|
|
433
|
-
if current_lines:
|
|
434
|
-
text = "\n".join(current_lines).strip()
|
|
435
|
-
if len(text) >= 20:
|
|
436
|
-
entries.append({
|
|
437
|
-
"content": text,
|
|
438
|
-
"section": current_section,
|
|
439
|
-
"source_file": source_file,
|
|
440
|
-
})
|
|
441
|
-
current_section = line[3:].strip()
|
|
442
|
-
current_lines = [line]
|
|
443
|
-
elif line.startswith("# ") and not current_lines:
|
|
444
|
-
# Top-level heading, skip (it's the file title)
|
|
445
|
-
continue
|
|
446
|
-
else:
|
|
447
|
-
current_lines.append(line)
|
|
448
|
-
|
|
449
|
-
# Flush last section
|
|
450
|
-
if current_lines:
|
|
451
|
-
text = "\n".join(current_lines).strip()
|
|
452
|
-
if len(text) >= 20:
|
|
453
|
-
entries.append({
|
|
454
|
-
"content": text,
|
|
455
|
-
"section": current_section,
|
|
456
|
-
"source_file": source_file,
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
return entries
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
# ── Combined sync ─────────────────────────────────────────────────────────
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
async def sync_bidirectional(
|
|
466
|
-
db,
|
|
467
|
-
embeddings,
|
|
468
|
-
project_path: str,
|
|
469
|
-
) -> Dict[str, Any]:
|
|
470
|
-
"""Run both sync directions. Used at session end.
|
|
471
|
-
|
|
472
|
-
Args:
|
|
473
|
-
db: DatabaseService instance
|
|
474
|
-
embeddings: EmbeddingService instance (needed for native->MCP)
|
|
475
|
-
project_path: Absolute project path
|
|
476
|
-
|
|
477
|
-
Returns:
|
|
478
|
-
Dict with results from both directions
|
|
479
|
-
"""
|
|
480
|
-
results = {}
|
|
481
|
-
|
|
482
|
-
# Direction A: MCP -> Native (fast)
|
|
483
|
-
try:
|
|
484
|
-
results["mcp_to_native"] = await sync_mcp_to_native(db, project_path)
|
|
485
|
-
except Exception as e:
|
|
486
|
-
logger.error(f"Bidirectional sync MCP->Native failed: {e}")
|
|
487
|
-
results["mcp_to_native"] = {"success": False, "error": str(e)}
|
|
488
|
-
|
|
489
|
-
# Direction B: Native -> MCP (needs embeddings)
|
|
490
|
-
try:
|
|
491
|
-
results["native_to_mcp"] = await sync_native_to_mcp(db, embeddings, project_path)
|
|
492
|
-
except Exception as e:
|
|
493
|
-
logger.error(f"Bidirectional sync Native->MCP failed: {e}")
|
|
494
|
-
results["native_to_mcp"] = {"success": False, "error": str(e)}
|
|
495
|
-
|
|
496
|
-
return results
|