nucleus-mcp 1.0.7 → 1.2.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.
Files changed (135) hide show
  1. package/package.json +2 -4
  2. package/.github/workflows/ci.yml +0 -72
  3. package/.smithery/shttp/module.js +0 -3
  4. package/.smithery/shttp/module.js.map +0 -7
  5. package/.smithery/stdio/index.cjs +0 -74
  6. package/.smithery/stdio/index.cjs.map +0 -7
  7. package/CHANGELOG.md +0 -65
  8. package/CODE_OF_CONDUCT.md +0 -34
  9. package/COMPARISON.md +0 -20
  10. package/CONTRIBUTING.md +0 -137
  11. package/DEVELOPMENT.md +0 -109
  12. package/LICENSE +0 -21
  13. package/README.md +0 -319
  14. package/ROADMAP.md +0 -24
  15. package/SECURITY.md +0 -88
  16. package/docs/COMMUNITY_FAQ.md +0 -48
  17. package/docs/COMPETITIVE_ANALYSIS.md +0 -90
  18. package/docs/DEMO_SCRIPT.md +0 -86
  19. package/docs/ENTERPRISE.md +0 -169
  20. package/docs/FAQ.md +0 -228
  21. package/docs/QUICK_START.md +0 -155
  22. package/docs/README.md +0 -67
  23. package/docs/RECURSIVE_SNAP_DEMO.gif +0 -0
  24. package/docs/social-preview-hq.png +0 -0
  25. package/docs/social-preview.png +0 -0
  26. package/examples/basic_usage.py +0 -84
  27. package/package.json.real +0 -24
  28. package/pyproject.toml +0 -50
  29. package/registry-server.json +0 -20
  30. package/scripts/demo_governance.py +0 -109
  31. package/scripts/sse_bridge.py +0 -87
  32. package/scripts/verify_windows_paths.py +0 -48
  33. package/server.json +0 -20
  34. package/src/mcp_server_nucleus/__init__.py +0 -8583
  35. package/src/mcp_server_nucleus/__main__.py +0 -9
  36. package/src/mcp_server_nucleus/cli.py +0 -1225
  37. package/src/mcp_server_nucleus/commitment_core.py +0 -146
  38. package/src/mcp_server_nucleus/commitment_ledger.py +0 -841
  39. package/src/mcp_server_nucleus/core/__init__.py +0 -6
  40. package/src/mcp_server_nucleus/core/orchestrator.py +0 -9
  41. package/src/mcp_server_nucleus/core/tool_registration_impl.py +0 -99
  42. package/src/mcp_server_nucleus/daemon/safety/lock.py +0 -121
  43. package/src/mcp_server_nucleus/deployment/render_integration.py +0 -4
  44. package/src/mcp_server_nucleus/hypervisor/__init__.py +0 -0
  45. package/src/mcp_server_nucleus/hypervisor/injector.py +0 -83
  46. package/src/mcp_server_nucleus/hypervisor/locker.py +0 -130
  47. package/src/mcp_server_nucleus/hypervisor/watchdog.py +0 -71
  48. package/src/mcp_server_nucleus/runtime/__init__.py +0 -58
  49. package/src/mcp_server_nucleus/runtime/agent.py +0 -517
  50. package/src/mcp_server_nucleus/runtime/agent_pool.py +0 -797
  51. package/src/mcp_server_nucleus/runtime/agents/base.py +0 -34
  52. package/src/mcp_server_nucleus/runtime/agents/oracle/manifest.json +0 -28
  53. package/src/mcp_server_nucleus/runtime/auth.py +0 -98
  54. package/src/mcp_server_nucleus/runtime/autopilot.py +0 -1057
  55. package/src/mcp_server_nucleus/runtime/broker.py +0 -126
  56. package/src/mcp_server_nucleus/runtime/budget.py +0 -153
  57. package/src/mcp_server_nucleus/runtime/capabilities/base.py +0 -20
  58. package/src/mcp_server_nucleus/runtime/capabilities/brain_ops.py +0 -253
  59. package/src/mcp_server_nucleus/runtime/capabilities/code_ops.py +0 -192
  60. package/src/mcp_server_nucleus/runtime/capabilities/depth_tracker.py +0 -81
  61. package/src/mcp_server_nucleus/runtime/capabilities/feature_map.py +0 -274
  62. package/src/mcp_server_nucleus/runtime/capabilities/marketing_engine.py +0 -205
  63. package/src/mcp_server_nucleus/runtime/capabilities/memory_ops.py +0 -79
  64. package/src/mcp_server_nucleus/runtime/capabilities/proof_system.py +0 -127
  65. package/src/mcp_server_nucleus/runtime/capabilities/render_ops.py +0 -80
  66. package/src/mcp_server_nucleus/runtime/capabilities/render_poller_cap.py +0 -266
  67. package/src/mcp_server_nucleus/runtime/capabilities/self_healing.py +0 -110
  68. package/src/mcp_server_nucleus/runtime/capabilities/strategy.py +0 -138
  69. package/src/mcp_server_nucleus/runtime/capabilities/synthesizer.py +0 -134
  70. package/src/mcp_server_nucleus/runtime/capabilities/web_ops.py +0 -94
  71. package/src/mcp_server_nucleus/runtime/common.py +0 -134
  72. package/src/mcp_server_nucleus/runtime/context_manager.py +0 -342
  73. package/src/mcp_server_nucleus/runtime/context_rules.json +0 -86
  74. package/src/mcp_server_nucleus/runtime/crdt_task_store.py +0 -349
  75. package/src/mcp_server_nucleus/runtime/critic.py +0 -150
  76. package/src/mcp_server_nucleus/runtime/daemon.py +0 -142
  77. package/src/mcp_server_nucleus/runtime/dashboard.py +0 -1087
  78. package/src/mcp_server_nucleus/runtime/depth_ops.py +0 -339
  79. package/src/mcp_server_nucleus/runtime/event_ops.py +0 -103
  80. package/src/mcp_server_nucleus/runtime/event_stream.py +0 -213
  81. package/src/mcp_server_nucleus/runtime/export.py +0 -64
  82. package/src/mcp_server_nucleus/runtime/factory.py +0 -594
  83. package/src/mcp_server_nucleus/runtime/federation.py +0 -1040
  84. package/src/mcp_server_nucleus/runtime/fetcher.py +0 -92
  85. package/src/mcp_server_nucleus/runtime/file_monitor.py +0 -208
  86. package/src/mcp_server_nucleus/runtime/firestore_bridge.py +0 -91
  87. package/src/mcp_server_nucleus/runtime/gcloud_ops.py +0 -186
  88. package/src/mcp_server_nucleus/runtime/hooks.py +0 -246
  89. package/src/mcp_server_nucleus/runtime/identity/gatekeeper.py +0 -106
  90. package/src/mcp_server_nucleus/runtime/identity/keygen.py +0 -147
  91. package/src/mcp_server_nucleus/runtime/identity/manifest.py +0 -62
  92. package/src/mcp_server_nucleus/runtime/identity/trust.py +0 -27
  93. package/src/mcp_server_nucleus/runtime/inspector.py +0 -74
  94. package/src/mcp_server_nucleus/runtime/installer.py +0 -81
  95. package/src/mcp_server_nucleus/runtime/ipc_auth.py +0 -377
  96. package/src/mcp_server_nucleus/runtime/lifecycle.py +0 -129
  97. package/src/mcp_server_nucleus/runtime/llm_client.py +0 -408
  98. package/src/mcp_server_nucleus/runtime/llm_tiers.json +0 -88
  99. package/src/mcp_server_nucleus/runtime/locking.py +0 -228
  100. package/src/mcp_server_nucleus/runtime/loops/fixer.py +0 -110
  101. package/src/mcp_server_nucleus/runtime/memory.py +0 -158
  102. package/src/mcp_server_nucleus/runtime/mounter.py +0 -210
  103. package/src/mcp_server_nucleus/runtime/mounter_ops.py +0 -250
  104. package/src/mcp_server_nucleus/runtime/nuke_protocol.py +0 -147
  105. package/src/mcp_server_nucleus/runtime/orchestrator.py +0 -370
  106. package/src/mcp_server_nucleus/runtime/orchestrator_unified.py +0 -263
  107. package/src/mcp_server_nucleus/runtime/orchestrator_v3.py +0 -617
  108. package/src/mcp_server_nucleus/runtime/plugin_loader.py +0 -84
  109. package/src/mcp_server_nucleus/runtime/policy.py +0 -117
  110. package/src/mcp_server_nucleus/runtime/profiling.py +0 -219
  111. package/src/mcp_server_nucleus/runtime/prometheus.py +0 -286
  112. package/src/mcp_server_nucleus/runtime/proposals.py +0 -101
  113. package/src/mcp_server_nucleus/runtime/publisher.py +0 -109
  114. package/src/mcp_server_nucleus/runtime/pulse.py +0 -516
  115. package/src/mcp_server_nucleus/runtime/registry.py +0 -82
  116. package/src/mcp_server_nucleus/runtime/render_ops.py +0 -83
  117. package/src/mcp_server_nucleus/runtime/schema_gen.py +0 -140
  118. package/src/mcp_server_nucleus/runtime/session_ops.py +0 -224
  119. package/src/mcp_server_nucleus/runtime/stdio_server.py +0 -633
  120. package/src/mcp_server_nucleus/runtime/storage.py +0 -83
  121. package/src/mcp_server_nucleus/runtime/strategy.py +0 -91
  122. package/src/mcp_server_nucleus/runtime/swarm.py +0 -170
  123. package/src/mcp_server_nucleus/runtime/sync_ops.py +0 -878
  124. package/src/mcp_server_nucleus/runtime/task_ingestion.py +0 -1020
  125. package/src/mcp_server_nucleus/runtime/task_ops.py +0 -449
  126. package/src/mcp_server_nucleus/runtime/task_scheduler.py +0 -488
  127. package/src/mcp_server_nucleus/runtime/team.py +0 -59
  128. package/src/mcp_server_nucleus/runtime/triggers.py +0 -237
  129. package/src/mcp_server_nucleus/runtime/vector_store.py +0 -125
  130. package/src/mcp_server_nucleus/runtime/watcher.py +0 -112
  131. package/src/mcp_server_nucleus/tool_tiers.py +0 -243
  132. package/tests/__init__.py +0 -1
  133. package/tests/conftest.py +0 -42
  134. package/tests/test_core.py +0 -235
  135. package/tests/test_search.py +0 -69
@@ -1,228 +0,0 @@
1
- """
2
- BrainLock: The Atomic Safety Layer for Nucleus.
3
- This module defines the locking primitives used to ensure data integrity across
4
- multiple Nucleus processes (Daemon, CLI, MCP Server).
5
-
6
- Strategic Role:
7
- - Ensures "Atomic State" (The Brain doesn't hallucinate due to race conditions).
8
- - Implements "Leased Locks" (Prevents deadlocks if a process crashes).
9
- - Future-Proofing: Abstract base class allows swapping 'fcntl' for 'Redis' later.
10
- """
11
-
12
- import abc
13
- import fcntl
14
- import time
15
- import os
16
- import contextlib
17
- import logging
18
- import subprocess
19
- import shutil
20
- from pathlib import Path
21
- from typing import Optional, Dict, Any, List
22
-
23
- logger = logging.getLogger(__name__)
24
-
25
- class BrainLock(abc.ABC):
26
- """
27
- Abstract Base Class for Nucleus Locking.
28
- Enforces the 'LockProvider' interface for Cloud/Local agnosticism.
29
- """
30
-
31
- @abc.abstractmethod
32
- def acquire(self, timeout: float = 5.0, metadata: Optional[Dict[str, str]] = None) -> bool:
33
- """Attempt to acquire the lock. Returns True if successful."""
34
- pass
35
-
36
- @abc.abstractmethod
37
- def release(self) -> None:
38
- """Release the lock."""
39
- pass
40
-
41
- @abc.abstractmethod
42
- def get_metadata(self) -> Dict[str, Any]:
43
- """Retrieve metadata associated with the lock."""
44
- pass
45
-
46
- def check_stale_locks(self, max_age_seconds: float = 3600):
47
- """Optional: Check for and cleanup stale locks."""
48
- pass
49
-
50
-
51
- @contextlib.contextmanager
52
- def section(self, timeout: float = 5.0, metadata: Optional[Dict[str, str]] = None):
53
- """Context manager for critical sections."""
54
- acquired = self.acquire(timeout, metadata=metadata)
55
- if not acquired:
56
- raise TimeoutError(f"Could not acquire lock on {self} after {timeout}s")
57
- try:
58
- yield
59
- finally:
60
- self.release()
61
-
62
- class FileBrainLock(BrainLock):
63
- """
64
- Local Implementation using UNIX `fcntl` + `xattr` for metadata.
65
- Used for the 'Local First' Sovereign OS.
66
- """
67
-
68
- def __init__(self, lock_path: str):
69
- self.lock_path = Path(lock_path)
70
- self.lock_file = None
71
- # Ensure directory exists
72
- self.lock_path.parent.mkdir(parents=True, exist_ok=True)
73
- self.xattr_available = shutil.which("xattr") is not None
74
-
75
- def _set_xattr(self, key: str, value: str):
76
- """Writes extended attribute if xattr is available."""
77
- if not self.xattr_available:
78
- return
79
-
80
- try:
81
- subprocess.run(
82
- ["xattr", "-w", key, str(value), str(self.lock_path)],
83
- check=True,
84
- capture_output=True
85
- )
86
- except subprocess.CalledProcessError as e:
87
- logger.warning(f"Failed to set xattr {key}: {e}")
88
-
89
- def _get_xattr(self, key: str) -> Optional[str]:
90
- """Reads extended attribute if xattr is available."""
91
- if not self.xattr_available:
92
- return None
93
-
94
- try:
95
- result = subprocess.run(
96
- ["xattr", "-p", key, str(self.lock_path)],
97
- check=True,
98
- capture_output=True,
99
- text=True
100
- )
101
- return result.stdout.strip()
102
- except subprocess.CalledProcessError:
103
- return None
104
-
105
- def _list_xattrs(self) -> List[str]:
106
- """Lists all xattrs on the file."""
107
- if not self.xattr_available:
108
- return []
109
-
110
- try:
111
- result = subprocess.run(
112
- ["xattr", str(self.lock_path)],
113
- check=True,
114
- capture_output=True,
115
- text=True
116
- )
117
- return result.stdout.strip().split("\n")
118
- except subprocess.CalledProcessError:
119
- return []
120
-
121
- def acquire(self, timeout: float = 5.0, metadata: Optional[Dict[str, str]] = None) -> bool:
122
- start_time = time.time()
123
-
124
- # Open the file if not already open
125
- if self.lock_file is None:
126
- self.lock_file = open(self.lock_path, 'w+')
127
-
128
- while True:
129
- try:
130
- # specific locking operation: LOCK_EX (Exclusive) | LOCK_NB (Non-Blocking)
131
- fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
132
-
133
- # Write PID for debugging (knowing who holds the lock)
134
- self.lock_file.seek(0)
135
- self.lock_file.truncate()
136
- self.lock_file.write(str(os.getpid()))
137
- self.lock_file.flush()
138
-
139
- # --- METADATA INJECTION (Phase 28) ---
140
- if metadata:
141
- # Always include timestamp if not present
142
- if "timestamp" not in metadata:
143
- metadata["timestamp"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
144
-
145
- for k, v in metadata.items():
146
- # Namespace keys with 'nucleus.lock.' if not already
147
- key = k if k.startswith("nucleus.lock.") else f"nucleus.lock.{k}"
148
- self._set_xattr(key, v)
149
-
150
- return True
151
- except (IOError, OSError):
152
- # Lock is held by another process
153
- if time.time() - start_time > timeout:
154
- logger.warning(f"Timeout waiting for lock: {self.lock_path}")
155
- return False
156
- time.sleep(0.1)
157
-
158
- def release(self) -> None:
159
- if self.lock_file:
160
- try:
161
- # Unlock
162
- fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_UN)
163
- self.lock_file.close()
164
- except ValueError:
165
- # File might be closed already
166
- pass
167
- finally:
168
- self.lock_file = None
169
-
170
- def get_metadata(self) -> Dict[str, Any]:
171
- """Retrieves all nucleus.lock.* metadata from the lock file."""
172
- if not self.lock_path.exists():
173
- return {}
174
-
175
- data = {}
176
- # Get PID from file content
177
- try:
178
- with open(self.lock_path, 'r') as f:
179
- data["pid"] = f.read().strip()
180
- except Exception:
181
- data["pid"] = "unknown"
182
-
183
- # Get xattrs
184
- if self.xattr_available:
185
- attrs = self._list_xattrs()
186
- for attr in attrs:
187
- if attr.startswith("nucleus.lock."):
188
- clean_key = attr.replace("nucleus.lock.", "")
189
- data[clean_key] = self._get_xattr(attr)
190
-
191
- return data
192
-
193
- def check_stale_locks(self, max_age_seconds: float = 86400):
194
- """
195
- Check for stale lock files.
196
- Note: fcntl locks are released by OS on process death.
197
- This simply cleans up old files to keep directory tidy.
198
- """
199
- try:
200
- if self.lock_path.exists():
201
- stat = self.lock_path.stat()
202
- age = time.time() - stat.st_mtime
203
- if age > max_age_seconds:
204
- # We could try to flock(LOCK_EX | LOCK_NB) to see if anyone holds it
205
- # If we can lock it, it was truly stale (or just unused), so we can theoretically delete it.
206
- # But dealing with race on deletion is tricky.
207
- # For V1, we simply Log it.
208
- logger.info(f"Old lock file detected: {self.lock_path} (Age: {age}s)")
209
- except Exception:
210
- pass
211
-
212
-
213
- def __repr__(self):
214
- return f"<FileBrainLock: {self.lock_path}>"
215
-
216
- # Factory Function for easy usage
217
- def get_lock(resource_name: str, base_dir: Optional[Path] = None) -> BrainLock:
218
- """
219
- Factory to get the appropriate lock for a resource.
220
- Currently defaults to FileBrainLock.
221
- """
222
- if base_dir is None:
223
- # Default to a .locks directory in the user's home or project root
224
- # For now, let's use the .brain/.locks directory if possible, or /tmp/nucleus
225
- base_dir = Path(os.environ.get("NUCLEUS_LOCK_DIR", "/tmp/nucleus_locks"))
226
-
227
- lock_path = base_dir / f"{resource_name}.lock"
228
- return FileBrainLock(str(lock_path))
@@ -1,110 +0,0 @@
1
-
2
- import subprocess
3
- import time
4
- import json
5
- from pathlib import Path
6
- from typing import Callable, Tuple, Dict, Any
7
-
8
- class FixerLoop:
9
- """
10
- Manages the 'Verify -> Diagnose -> Fix' cycle for autonomous code repair.
11
- Phase 4: Self-Healing v2
12
- """
13
-
14
- def __init__(
15
- self,
16
- target_file: str,
17
- verification_command: str,
18
- fixer_func: Callable[[str, str], str],
19
- max_retries: int = 3
20
- ):
21
- self.target_file = Path(target_file)
22
- self.verification_command = verification_command
23
- self.fixer_func = fixer_func
24
- self.max_retries = max_retries
25
- self.logs = []
26
-
27
- def _log(self, message: str):
28
- timestamp = time.strftime("%H:%M:%S")
29
- entry = f"[{timestamp}] {message}"
30
- print(entry)
31
- self.logs.append(entry)
32
-
33
- def _run_verification(self) -> Tuple[bool, str]:
34
- """Runs the verification command and returns (success, output)."""
35
- self._log(f"Running verification: {self.verification_command}")
36
- try:
37
- result = subprocess.run(
38
- self.verification_command,
39
- shell=True,
40
- capture_output=True,
41
- text=True,
42
- timeout=60
43
- )
44
- output = result.stdout + "\n" + result.stderr
45
- return (result.returncode == 0, output)
46
- except subprocess.TimeoutExpired:
47
- return (False, "Verification timed out after 60s")
48
- except Exception as e:
49
- return (False, f"Verification failed to run: {e}")
50
-
51
- def run(self) -> Dict[str, Any]:
52
- """Execute the repair loop."""
53
-
54
- if not self.target_file.exists():
55
- return {
56
- "status": "error",
57
- "message": f"Target file {self.target_file} not found"
58
- }
59
-
60
- self._log(f"Starting Fixer Loop for {self.target_file.name}")
61
-
62
- # Initial Verification
63
- success, output = self._run_verification()
64
- if success:
65
- self._log("Initial verification PASSED. No fix needed.")
66
- return {
67
- "status": "success",
68
- "message": "File passed verification initially.",
69
- "logs": self.logs
70
- }
71
-
72
- self._log("Initial verification FAILED. Starting repair cycles.")
73
-
74
- for attempt in range(1, self.max_retries + 1):
75
- self._log(f"--- Attempt {attempt}/{self.max_retries} ---")
76
-
77
- # Diagnose & Fix
78
- issues_context = f"Verification Command: {self.verification_command}\n\nOutput:\n{output[-2000:]}" # Truncate output
79
-
80
- self._log("Invoking Fixer Agent...")
81
- try:
82
- # Calls brain_fix_code(path, context)
83
- fix_result_json = self.fixer_func(str(self.target_file), issues_context)
84
- fix_result = json.loads(fix_result_json)
85
-
86
- if fix_result.get("status") == "error":
87
- self._log(f"Fixer Agent Error: {fix_result.get('message')}")
88
- # Continue anyway? No, probably fatal.
89
- # But maybe transient.
90
- except Exception as e:
91
- self._log(f"Exception calling fixer: {e}")
92
-
93
- # Verify Again
94
- success, output = self._run_verification()
95
- if success:
96
- self._log("✅ Verification PASSED after fix.")
97
- return {
98
- "status": "success",
99
- "message": f"Fixed in {attempt} attempts.",
100
- "logs": self.logs
101
- }
102
- else:
103
- self._log("❌ Verification FAILED. Retrying...")
104
-
105
- return {
106
- "status": "failure",
107
- "message": f"Failed to fix after {self.max_retries} attempts.",
108
- "last_output": output,
109
- "logs": self.logs
110
- }
@@ -1,158 +0,0 @@
1
-
2
- import os
3
- import subprocess
4
- from pathlib import Path
5
- from typing import Dict
6
- import logging
7
- import time
8
-
9
- # Configure logger
10
- logger = logging.getLogger("nucleus.memory")
11
-
12
- def get_brain_path() -> Path:
13
- """Get the brain path from environment variable."""
14
- brain_path = os.environ.get("NUCLEAR_BRAIN_PATH")
15
- if not brain_path:
16
- # Fallback for dev environment
17
- cwd = Path.cwd()
18
- if (cwd / ".brain").exists():
19
- return cwd / ".brain"
20
- for parent in cwd.parents:
21
- if (parent / ".brain").exists():
22
- return parent / ".brain"
23
- raise ValueError("NUCLEAR_BRAIN_PATH environment variable not set")
24
- return Path(brain_path)
25
-
26
- def _search_memory(query: str) -> Dict:
27
- """
28
- Search the 'Long-term Memory' (artifacts/memory and ledger/decisions.md) using ripgrep.
29
- """
30
- try:
31
- brain = get_brain_path()
32
- memory_dir = brain / "memory"
33
- ledger_dir = brain / "ledger"
34
-
35
- # Paths to search
36
- search_paths = [str(memory_dir)]
37
- if (ledger_dir / "decisions.md").exists():
38
- search_paths.append(str(ledger_dir / "decisions.md"))
39
-
40
- # Run ripgrep
41
- # -i: case insensitive
42
- # -C 2: 2 lines of context
43
- # --json: output as json
44
-
45
- # V1: Simple Text Search
46
- cmd_text = ["rg", "-i", "-n", "--no-heading", query] + search_paths
47
- try:
48
- result_text = subprocess.run(cmd_text, capture_output=True, text=True)
49
- snippets = []
50
- if result_text.stdout:
51
- snippets = result_text.stdout.strip().splitlines()
52
- except FileNotFoundError:
53
- # Fallback when ripgrep is not available
54
- snippets = []
55
- lowered = query.lower()
56
- for path_str in search_paths:
57
- path = Path(path_str)
58
- if path.is_file():
59
- files = [path]
60
- else:
61
- files = list(path.glob("*.md")) + list(path.glob("*.txt"))
62
- for file_path in files:
63
- try:
64
- for line in file_path.read_text().splitlines():
65
- if lowered in line.lower():
66
- snippets.append(f"{file_path}:{line}")
67
- except Exception:
68
- continue
69
-
70
- return {
71
- "query": query,
72
- "count": len(snippets),
73
- "results": snippets[:20] # Limit to 20 results
74
- }
75
-
76
- except Exception as e:
77
- logger.error(f"Memory search failed: {e}")
78
- return {"error": str(e)}
79
-
80
- def _read_memory(category: str) -> Dict:
81
- """
82
- Read specific memory categories (context, patterns, learnings).
83
- """
84
- try:
85
- brain = get_brain_path()
86
- memory_dir = brain / "memory"
87
-
88
- allowed_files = {
89
- "context": "context.md",
90
- "patterns": "patterns.md",
91
- "learnings": "learnings.md",
92
- "decisions": "decisions.md" # Actually in ledger, but handled here
93
- }
94
-
95
- if category not in allowed_files:
96
- return {"error": f"Invalid category. Allowed: {list(allowed_files.keys())}"}
97
-
98
- filename = allowed_files[category]
99
- file_path = memory_dir / filename
100
-
101
- if category == "decisions":
102
- file_path = brain / "ledger" / "decisions.md"
103
-
104
- if not file_path.exists():
105
- return {"error": f"Memory file {filename} not found."}
106
-
107
- content = file_path.read_text()
108
- return {
109
- "category": category,
110
- "path": str(file_path),
111
- "content": content
112
- }
113
-
114
- except Exception as e:
115
- logger.error(f"Read memory failed: {e}")
116
- return {"error": str(e)}
117
-
118
- def _write_memory(content: str, category: str = "learnings") -> str:
119
- """
120
- Write a memory engram to a specific category file in Markdown format.
121
- """
122
- try:
123
- brain = get_brain_path()
124
- memory_dir = brain / "memory"
125
- memory_dir.mkdir(parents=True, exist_ok=True)
126
-
127
- allowed_files = {
128
- "context": "context.md",
129
- "patterns": "patterns.md",
130
- "learnings": "learnings.md",
131
- "decisions": "decisions.md"
132
- }
133
-
134
- # Default to learnings if unknown category, or map "memory" to learnings
135
- if category == "memory":
136
- category = "learnings"
137
-
138
- target_file = allowed_files.get(category, "learnings.md")
139
-
140
- # Special case for decisions (in ledger)
141
- if category == "decisions":
142
- file_path = brain / "ledger" / "decisions.md"
143
- file_path.parent.mkdir(parents=True, exist_ok=True)
144
- else:
145
- file_path = memory_dir / target_file
146
-
147
- timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
148
- entry = f"\n\n## [{timestamp}] Engram\n{content}"
149
-
150
- mode = "a" if file_path.exists() else "w"
151
- with open(file_path, mode) as f:
152
- f.write(entry)
153
-
154
- return f"Memory written to {target_file}"
155
-
156
- except Exception as e:
157
- logger.error(f"Write memory failed: {e}")
158
- return f"Error writing memory: {str(e)}"