get-claudia 1.28.2 → 1.28.4

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/bin/index.js CHANGED
@@ -415,7 +415,7 @@ async function main() {
415
415
 
416
416
  mcpConfig.mcpServers['claudia-memory'] = {
417
417
  command: pythonCmd,
418
- args: ['-m', 'claudia_memory.mcp.server'],
418
+ args: ['-m', 'claudia_memory', '--project-dir', '${workspaceFolder}'],
419
419
  _description: 'Claudia memory system with vector search'
420
420
  };
421
421
  writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2));
@@ -13,6 +13,7 @@ import json
13
13
  import logging
14
14
  import os
15
15
  import sqlite3
16
+ import sys
16
17
  import threading
17
18
  from contextlib import contextmanager
18
19
  from datetime import datetime
@@ -62,18 +63,34 @@ class Database:
62
63
  except ImportError:
63
64
  logger.debug("sqlite_vec package not installed")
64
65
  except Exception as e:
65
- logger.debug(f"sqlite_vec package failed: {e}")
66
+ logger.warning(f"sqlite_vec package installed but load() failed: {e}")
66
67
 
67
68
  # Method 2: Try native extension loading (for systems with pre-installed sqlite-vec)
68
69
  if not loaded:
69
70
  try:
70
71
  conn.enable_load_extension(True)
71
- sqlite_vec_paths = [
72
- "vec0", # If installed system-wide
73
- "/usr/local/lib/sqlite-vec/vec0",
74
- "/opt/homebrew/lib/sqlite-vec/vec0",
75
- str(Path.home() / ".local" / "lib" / "sqlite-vec" / "vec0"),
76
- ]
72
+ sqlite_vec_paths = ["vec0"] # System-wide
73
+
74
+ if sys.platform == "win32":
75
+ # Try to find vec0.dll in the sqlite-vec package directory
76
+ try:
77
+ import sqlite_vec as _sv
78
+ pkg_dir = Path(_sv.__file__).parent
79
+ for dll in pkg_dir.rglob("vec0*"):
80
+ if dll.suffix in (".dll", ".so"):
81
+ sqlite_vec_paths.append(str(dll.with_suffix("")))
82
+ except ImportError:
83
+ pass
84
+ sqlite_vec_paths.extend([
85
+ str(Path(sys.executable).parent / "DLLs" / "vec0"),
86
+ str(Path.home() / ".local" / "lib" / "sqlite-vec" / "vec0"),
87
+ ])
88
+ else:
89
+ sqlite_vec_paths.extend([
90
+ "/usr/local/lib/sqlite-vec/vec0",
91
+ "/opt/homebrew/lib/sqlite-vec/vec0",
92
+ str(Path.home() / ".local" / "lib" / "sqlite-vec" / "vec0"),
93
+ ])
77
94
 
78
95
  for path in sqlite_vec_paths:
79
96
  try:
@@ -92,10 +109,18 @@ class Database:
92
109
  logger.debug(f"Extension loading failed: {e}")
93
110
 
94
111
  if not loaded:
95
- logger.warning(
96
- "sqlite-vec not available. Vector search will be disabled. "
97
- "Install with: pip install sqlite-vec"
98
- )
112
+ if sys.platform == "win32":
113
+ logger.warning(
114
+ "sqlite-vec not available. Vector search will be disabled. "
115
+ "Install with: pip install sqlite-vec "
116
+ "If already installed but failing, ensure your Python and "
117
+ "sqlite-vec architectures match (both 64-bit or both 32-bit)."
118
+ )
119
+ else:
120
+ logger.warning(
121
+ "sqlite-vec not available. Vector search will be disabled. "
122
+ "Install with: pip install sqlite-vec"
123
+ )
99
124
 
100
125
  self._local.connection = conn
101
126
 
@@ -66,9 +66,33 @@ from ..services.remember import (
66
66
  remember_fact,
67
67
  remember_message,
68
68
  )
69
+ from ..embeddings import get_embedding_service
69
70
 
70
71
  logger = logging.getLogger(__name__)
71
72
 
73
+
74
+ def _coerce_arg(arguments: Dict[str, Any], key: str, expected_type: type = list) -> None:
75
+ """Coerce a tool argument from JSON string to expected type in-place.
76
+
77
+ LLMs sometimes serialize array parameters as JSON strings instead of
78
+ native arrays. This transparently parses them back so handler code
79
+ can assume native types.
80
+ """
81
+ value = arguments.get(key)
82
+ if isinstance(value, str):
83
+ try:
84
+ parsed = json.loads(value)
85
+ if isinstance(parsed, expected_type):
86
+ arguments[key] = parsed
87
+ else:
88
+ logger.warning(
89
+ f"Coercion: '{key}' parsed to {type(parsed).__name__}, "
90
+ f"expected {expected_type.__name__}"
91
+ )
92
+ except (json.JSONDecodeError, TypeError):
93
+ logger.warning(f"Could not parse '{key}' as JSON: {value[:100]}")
94
+
95
+
72
96
  # Initialize the MCP server
73
97
  server = Server("claudia-memory")
74
98
 
@@ -94,7 +118,7 @@ async def list_tools() -> ListToolsResult:
94
118
  "default": "fact",
95
119
  },
96
120
  "about": {
97
- "type": "array",
121
+ "type": ["array", "string"],
98
122
  "items": {"type": "string"},
99
123
  "description": "Entity names this memory relates to (people, projects, etc.)",
100
124
  },
@@ -139,7 +163,7 @@ async def list_tools() -> ListToolsResult:
139
163
  "default": 10,
140
164
  },
141
165
  "types": {
142
- "type": "array",
166
+ "type": ["array", "string"],
143
167
  "items": {"type": "string"},
144
168
  "description": "Filter by memory types (fact, preference, observation, learning, commitment)",
145
169
  },
@@ -153,7 +177,7 @@ async def list_tools() -> ListToolsResult:
153
177
  "default": False,
154
178
  },
155
179
  "ids": {
156
- "type": "array",
180
+ "type": ["array", "string"],
157
181
  "items": {"type": "integer"},
158
182
  "description": "Fetch specific memories by ID (skips search). Use after a compact search to get full content.",
159
183
  },
@@ -232,7 +256,7 @@ async def list_tools() -> ListToolsResult:
232
256
  "default": 5,
233
257
  },
234
258
  "types": {
235
- "type": "array",
259
+ "type": ["array", "string"],
236
260
  "items": {"type": "string"},
237
261
  "description": "Filter by type (reminder, suggestion, warning, insight)",
238
262
  },
@@ -268,7 +292,7 @@ async def list_tools() -> ListToolsResult:
268
292
  "description": "Description of the entity",
269
293
  },
270
294
  "aliases": {
271
- "type": "array",
295
+ "type": ["array", "string"],
272
296
  "items": {"type": "string"},
273
297
  "description": "Alternative names or spellings",
274
298
  },
@@ -287,7 +311,7 @@ async def list_tools() -> ListToolsResult:
287
311
  "description": "Search query",
288
312
  },
289
313
  "types": {
290
- "type": "array",
314
+ "type": ["array", "string"],
291
315
  "items": {"type": "string"},
292
316
  "description": "Filter by entity types",
293
317
  },
@@ -354,7 +378,7 @@ async def list_tools() -> ListToolsResult:
354
378
  ),
355
379
  },
356
380
  "facts": {
357
- "type": "array",
381
+ "type": ["array", "string"],
358
382
  "items": {
359
383
  "type": "object",
360
384
  "properties": {
@@ -388,7 +412,7 @@ async def list_tools() -> ListToolsResult:
388
412
  "description": "Structured facts, preferences, observations, learnings extracted from the session",
389
413
  },
390
414
  "commitments": {
391
- "type": "array",
415
+ "type": ["array", "string"],
392
416
  "items": {
393
417
  "type": "object",
394
418
  "properties": {
@@ -416,7 +440,7 @@ async def list_tools() -> ListToolsResult:
416
440
  "description": "Commitments or promises made during the session",
417
441
  },
418
442
  "entities": {
419
- "type": "array",
443
+ "type": ["array", "string"],
420
444
  "items": {
421
445
  "type": "object",
422
446
  "properties": {
@@ -437,7 +461,7 @@ async def list_tools() -> ListToolsResult:
437
461
  "description": "New or updated entities mentioned during the session",
438
462
  },
439
463
  "relationships": {
440
- "type": "array",
464
+ "type": ["array", "string"],
441
465
  "items": {
442
466
  "type": "object",
443
467
  "properties": {
@@ -451,12 +475,12 @@ async def list_tools() -> ListToolsResult:
451
475
  "description": "Relationships between entities observed during the session",
452
476
  },
453
477
  "key_topics": {
454
- "type": "array",
478
+ "type": ["array", "string"],
455
479
  "items": {"type": "string"},
456
480
  "description": "Main topics discussed in the session",
457
481
  },
458
482
  "reflections": {
459
- "type": "array",
483
+ "type": ["array", "string"],
460
484
  "items": {
461
485
  "type": "object",
462
486
  "properties": {
@@ -492,7 +516,7 @@ async def list_tools() -> ListToolsResult:
492
516
  ),
493
517
  },
494
518
  },
495
- "required": ["episode_id", "narrative"],
519
+ "required": ["narrative"],
496
520
  },
497
521
  ),
498
522
  Tool(
@@ -524,7 +548,7 @@ async def list_tools() -> ListToolsResult:
524
548
  "description": "Semantic search query (optional). If omitted, returns recent high-importance reflections.",
525
549
  },
526
550
  "types": {
527
- "type": "array",
551
+ "type": ["array", "string"],
528
552
  "items": {
529
553
  "type": "string",
530
554
  "enum": ["observation", "pattern", "learning", "question"],
@@ -570,7 +594,7 @@ async def list_tools() -> ListToolsResult:
570
594
  "type": "object",
571
595
  "properties": {
572
596
  "operations": {
573
- "type": "array",
597
+ "type": ["array", "string"],
574
598
  "description": "Array of operations to execute in order",
575
599
  "items": {
576
600
  "type": "object",
@@ -775,12 +799,12 @@ async def list_tools() -> ListToolsResult:
775
799
  "description": "Brief summary of the document",
776
800
  },
777
801
  "about": {
778
- "type": "array",
802
+ "type": ["array", "string"],
779
803
  "items": {"type": "string"},
780
804
  "description": "Entity names this document relates to",
781
805
  },
782
806
  "memory_ids": {
783
- "type": "array",
807
+ "type": ["array", "string"],
784
808
  "items": {"type": "integer"},
785
809
  "description": "Memory IDs to link as sourced from this document",
786
810
  },
@@ -1158,6 +1182,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1158
1182
  """Handle tool calls"""
1159
1183
  try:
1160
1184
  if name == "memory.remember":
1185
+ _coerce_arg(arguments, "about")
1161
1186
  memory_id = remember_fact(
1162
1187
  content=arguments["content"],
1163
1188
  memory_type=arguments.get("type", "fact"),
@@ -1187,6 +1212,8 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1187
1212
  )
1188
1213
 
1189
1214
  elif name == "memory.recall":
1215
+ _coerce_arg(arguments, "types")
1216
+ _coerce_arg(arguments, "ids")
1190
1217
  # Direct fetch by IDs (skip search)
1191
1218
  if "ids" in arguments and arguments["ids"]:
1192
1219
  results = fetch_by_ids(arguments["ids"])
@@ -1339,6 +1366,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1339
1366
  )
1340
1367
 
1341
1368
  elif name == "memory.predictions":
1369
+ _coerce_arg(arguments, "types")
1342
1370
  predictions = get_predictions(
1343
1371
  limit=arguments.get("limit", 5),
1344
1372
  prediction_types=arguments.get("types"),
@@ -1364,6 +1392,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1364
1392
  )
1365
1393
 
1366
1394
  elif name == "memory.entity":
1395
+ _coerce_arg(arguments, "aliases")
1367
1396
  entity_id = remember_entity(
1368
1397
  name=arguments["name"],
1369
1398
  entity_type=arguments.get("type", "person"),
@@ -1380,6 +1409,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1380
1409
  )
1381
1410
 
1382
1411
  elif name == "memory.search_entities":
1412
+ _coerce_arg(arguments, "types")
1383
1413
  results = search_entities(
1384
1414
  query=arguments["query"],
1385
1415
  entity_types=arguments.get("types"),
@@ -1426,19 +1456,30 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1426
1456
  )
1427
1457
 
1428
1458
  elif name == "memory.end_session":
1429
- episode_id = arguments["episode_id"]
1459
+ # Coerce all array fields (LLMs may send JSON strings)
1460
+ for field in ("facts", "commitments", "entities", "relationships", "key_topics", "reflections"):
1461
+ _coerce_arg(arguments, field)
1430
1462
 
1431
- # Auto-create episode if it doesn't exist (handles skipped buffer_turn)
1463
+ # Handle missing or invalid episode_id: auto-create
1464
+ episode_id = arguments.get("episode_id")
1432
1465
  svc = get_remember_service()
1433
- episode = svc.db.get_one("episodes", where="id = ?", where_params=(episode_id,))
1434
- if not episode:
1466
+ if episode_id is None:
1435
1467
  from datetime import datetime
1436
- new_id = svc.db.insert("episodes", {
1468
+ episode_id = svc.db.insert("episodes", {
1437
1469
  "started_at": datetime.utcnow().isoformat(),
1438
- "source": arguments.get("source", "claude_code"),
1470
+ "source": "claude_code",
1439
1471
  })
1440
- logger.info(f"Auto-created episode {new_id} (requested {episode_id} did not exist)")
1441
- episode_id = new_id
1472
+ logger.info(f"Auto-created episode {episode_id} (no episode_id provided)")
1473
+ else:
1474
+ episode = svc.db.get_one("episodes", where="id = ?", where_params=(episode_id,))
1475
+ if not episode:
1476
+ from datetime import datetime
1477
+ new_id = svc.db.insert("episodes", {
1478
+ "started_at": datetime.utcnow().isoformat(),
1479
+ "source": arguments.get("source", "claude_code"),
1480
+ })
1481
+ logger.info(f"Auto-created episode {new_id} (requested {episode_id} did not exist)")
1482
+ episode_id = new_id
1442
1483
 
1443
1484
  result = end_session(
1444
1485
  episode_id=episode_id,
@@ -1489,6 +1530,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1489
1530
  )
1490
1531
 
1491
1532
  elif name == "memory.reflections":
1533
+ _coerce_arg(arguments, "types")
1492
1534
  action = arguments.get("action", "get")
1493
1535
  limit = arguments.get("limit", 10)
1494
1536
  types = arguments.get("types")
@@ -1578,7 +1620,35 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1578
1620
  )
1579
1621
 
1580
1622
  elif name == "memory.batch":
1623
+ _coerce_arg(arguments, "operations")
1581
1624
  operations = arguments.get("operations", [])
1625
+
1626
+ # --- Pass 1: Collect all texts that need embeddings ---
1627
+ embed_tasks = [] # list of (index, text) for parallel embedding
1628
+ for i, op in enumerate(operations):
1629
+ op_type = op.get("op")
1630
+ if op_type == "remember":
1631
+ embed_tasks.append((i, op["content"]))
1632
+ elif op_type == "entity":
1633
+ # Only new entities need embeddings; collect optimistically
1634
+ embed_text = f"{op['name']}. {op.get('description') or ''}"
1635
+ embed_tasks.append((i, embed_text))
1636
+
1637
+ # --- Parallel embedding pass ---
1638
+ embeddings_map = {} # index -> embedding
1639
+ if embed_tasks:
1640
+ try:
1641
+ emb_svc = get_embedding_service()
1642
+ texts = [text for _, text in embed_tasks]
1643
+ all_embeddings = await emb_svc.embed_batch(texts)
1644
+ for (idx, _), emb in zip(embed_tasks, all_embeddings):
1645
+ if emb is not None:
1646
+ embeddings_map[idx] = emb
1647
+ except Exception as e:
1648
+ logger.warning(f"Batch parallel embedding failed, falling back to per-op: {e}")
1649
+ # embeddings_map stays empty; remember_fact/entity will embed individually
1650
+
1651
+ # --- Pass 2: Execute operations with pre-computed embeddings ---
1582
1652
  results = []
1583
1653
  for i, op in enumerate(operations):
1584
1654
  op_type = op.get("op")
@@ -1590,6 +1660,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1590
1660
  entity_type=op.get("type", "person"),
1591
1661
  description=op.get("description"),
1592
1662
  aliases=op.get("aliases"),
1663
+ _precomputed_embedding=embeddings_map.get(i),
1593
1664
  )
1594
1665
  op_result["success"] = True
1595
1666
  op_result["entity_id"] = entity_id
@@ -1601,6 +1672,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1601
1672
  importance=op.get("importance", 1.0),
1602
1673
  source=op.get("source"),
1603
1674
  source_context=op.get("source_context"),
1675
+ _precomputed_embedding=embeddings_map.get(i),
1604
1676
  )
1605
1677
  op_result["success"] = True
1606
1678
  op_result["memory_id"] = memory_id
@@ -1716,6 +1788,8 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1716
1788
  )
1717
1789
 
1718
1790
  elif name == "memory.file":
1791
+ _coerce_arg(arguments, "about")
1792
+ _coerce_arg(arguments, "memory_ids")
1719
1793
  doc_svc = get_document_service()
1720
1794
  result = doc_svc.file_document_from_text(
1721
1795
  content=arguments["content"],
@@ -141,6 +141,7 @@ class RememberService:
141
141
  source_context: Optional[str] = None,
142
142
  metadata: Optional[Dict] = None,
143
143
  origin_type: Optional[str] = None,
144
+ _precomputed_embedding: Optional[List[float]] = None,
144
145
  ) -> Optional[int]:
145
146
  """
146
147
  Store a discrete fact/memory.
@@ -217,8 +218,8 @@ class RememberService:
217
218
 
218
219
  memory_id = self.db.insert("memories", insert_data)
219
220
 
220
- # Generate and store embedding
221
- embedding = embed_sync(content)
221
+ # Store embedding (use precomputed if available, otherwise generate)
222
+ embedding = _precomputed_embedding or embed_sync(content)
222
223
  if embedding:
223
224
  try:
224
225
  self.db.execute(
@@ -263,6 +264,7 @@ class RememberService:
263
264
  description: Optional[str] = None,
264
265
  aliases: Optional[List[str]] = None,
265
266
  metadata: Optional[Dict] = None,
267
+ _precomputed_embedding: Optional[List[float]] = None,
266
268
  ) -> int:
267
269
  """
268
270
  Create or update an entity.
@@ -326,9 +328,9 @@ class RememberService:
326
328
  },
327
329
  )
328
330
 
329
- # Generate and store embedding
331
+ # Store embedding (use precomputed if available, otherwise generate)
330
332
  embed_text = f"{name}. {description or ''}"
331
- embedding = embed_sync(embed_text)
333
+ embedding = _precomputed_embedding or embed_sync(embed_text)
332
334
  if embedding:
333
335
  try:
334
336
  self.db.execute(
@@ -1512,12 +1514,12 @@ def remember_message(content: str, role: str = "user", **kwargs) -> Dict[str, An
1512
1514
 
1513
1515
 
1514
1516
  def remember_fact(content: str, **kwargs) -> Optional[int]:
1515
- """Store a discrete fact"""
1517
+ """Store a discrete fact. Pass _precomputed_embedding to skip Ollama call."""
1516
1518
  return get_remember_service().remember_fact(content, **kwargs)
1517
1519
 
1518
1520
 
1519
1521
  def remember_entity(name: str, **kwargs) -> int:
1520
- """Create or update an entity"""
1522
+ """Create or update an entity. Pass _precomputed_embedding to skip Ollama call."""
1521
1523
  return get_remember_service().remember_entity(name, **kwargs)
1522
1524
 
1523
1525