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,192 +0,0 @@
1
- from typing import List, Dict, Any
2
- import os
3
- import subprocess
4
- from pathlib import Path
5
- from .base import Capability
6
-
7
- class CodeOps(Capability):
8
- @property
9
- def name(self) -> str:
10
- return "code_ops"
11
-
12
- @property
13
- def description(self) -> str:
14
- return "FileSystem and Shell Access for Coding Agents."
15
-
16
- def get_tools(self) -> List[Dict[str, Any]]:
17
- return [
18
- {
19
- "name": "code_read_file",
20
- "description": "Read file contents.",
21
- "parameters": {
22
- "type": "object",
23
- "properties": {
24
- "path": {"type": "string", "description": "Absolute or relative path"}
25
- },
26
- "required": ["path"]
27
- }
28
- },
29
- {
30
- "name": "code_write_file",
31
- "description": "Write or overwrite file contents. Creates directories if needed.",
32
- "parameters": {
33
- "type": "object",
34
- "properties": {
35
- "path": {"type": "string", "description": "Absolute or relative path"},
36
- "content": {"type": "string"}
37
- },
38
- "required": ["path", "content"]
39
- }
40
- },
41
- {
42
- "name": "code_run_command",
43
- "description": "Execute a shell command.",
44
- "parameters": {
45
- "type": "object",
46
- "properties": {
47
- "command": {"type": "string"},
48
- "timeout": {"type": "integer", "default": 30}
49
- },
50
- "required": ["command"]
51
- }
52
- },
53
- {
54
- "name": "code_list_files",
55
- "description": "List files in directory.",
56
- "parameters": {
57
- "type": "object",
58
- "properties": {
59
- "path": {"type": "string", "default": "."}
60
- }
61
- }
62
- }
63
- ]
64
-
65
- def _resolve_path(self, path_str: str) -> Path:
66
- """
67
- Self-Healing Path Resolver.
68
- """
69
- cwd = Path(os.getcwd())
70
- path = Path(path_str)
71
-
72
- # 1. Try path as-is
73
- if path.exists():
74
- return path
75
-
76
- # 2. Try relative to CWD
77
- if not path.is_absolute():
78
- candidate = cwd / path
79
- if candidate.exists():
80
- return candidate
81
-
82
- # 3. Try relative to Project Root (BRAIN_PATH.parent)
83
- brain_path = Path(os.environ.get("NUCLEUS_BRAIN_PATH", "/Users/lokeshgarg/ai-mvp-backend/.brain"))
84
- project_root = brain_path.parent
85
-
86
- if not path.is_absolute():
87
- candidate = project_root / path
88
- if candidate.exists():
89
- return candidate
90
- else:
91
- # If absolute but invalid, try re-rooting
92
- # e.g. /gentlequest-blog/... -> PROJECT_ROOT/gentlequest-blog/...
93
- if 'ai-mvp-backend' in path_str:
94
- relative = path_str.split('ai-mvp-backend')[-1].lstrip('/')
95
- candidate = project_root / relative
96
- if candidate.exists():
97
- return candidate
98
- else:
99
- # Try just the relative part of the absolute path
100
- # e.g. /foo/bar -> PROJECT_ROOT/foo/bar
101
- relative = path_str.lstrip('/')
102
- candidate = project_root / relative
103
- if candidate.exists():
104
- return candidate
105
-
106
- return path
107
-
108
- def execute_tool(self, tool_name: str, args: Dict) -> str:
109
- """Execute local filesystem/shell operations."""
110
- cwd = os.getcwd()
111
-
112
- if tool_name == "code_read_file":
113
- path = self._resolve_path(args['path'])
114
-
115
- if not path.exists():
116
- return f"Error: File not found: {path} (in {cwd})"
117
- return path.read_text(encoding='utf-8')
118
-
119
- elif tool_name == "code_write_file":
120
- path_str = args['path']
121
- path = Path(path_str)
122
-
123
- # ============================================================
124
- # ENTERPRISE PATH RESOLUTION (Fix for Read-Only Filesystem Bug)
125
- # ============================================================
126
- # Problem: Agent subprocess may not have NUCLEUS_BRAIN_PATH set,
127
- # causing relative paths like 'gentlequest-blog/...' to resolve to '/'
128
- # Solution: Robust fallback chain with explicit validation
129
-
130
- PROJECT_ROOT = Path("/Users/lokeshgarg/ai-mvp-backend")
131
-
132
- if not path.is_absolute():
133
- resolved_root = None
134
-
135
- # Option 1: Use NUCLEUS_BRAIN_PATH if valid
136
- brain_path_str = os.environ.get("NUCLEUS_BRAIN_PATH", "")
137
- if brain_path_str and ".brain" in brain_path_str:
138
- brain_path = Path(brain_path_str)
139
- if brain_path.exists():
140
- resolved_root = brain_path.parent
141
-
142
- # Option 2: Fallback to hardcoded PROJECT_ROOT
143
- if not resolved_root:
144
- resolved_root = PROJECT_ROOT
145
-
146
- path = resolved_root / path
147
- # Log resolution for debugging
148
- print(f"[CodeOps] Path resolved: {path_str} → {path}")
149
-
150
- # Ensure parent directory exists
151
- path.parent.mkdir(parents=True, exist_ok=True)
152
- path.write_text(args['content'], encoding='utf-8')
153
- return f"✅ Wrote {len(args['content'])} bytes to {path}"
154
-
155
-
156
- elif tool_name == "code_run_command":
157
- cmd = args['command']
158
- timeout = args.get('timeout', 30)
159
- try:
160
- result = subprocess.run(
161
- cmd,
162
- shell=True,
163
- capture_output=True,
164
- text=True,
165
- cwd=cwd,
166
- timeout=timeout
167
- )
168
- return f"Exit Code: {result.returncode}\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"
169
- except subprocess.TimeoutExpired:
170
- return f"Error: Command timed out after {timeout}s"
171
- except Exception as e:
172
- return f"Error: {str(e)}"
173
-
174
- elif tool_name == "code_list_files":
175
- path = Path(args.get('path', '.'))
176
- if not path.is_absolute():
177
- path = Path(cwd) / path
178
-
179
- if not path.exists():
180
- return f"Error: Path not found: {path}"
181
-
182
- try:
183
- # Use 'ls -F' style output for simplicity
184
- entries = []
185
- for p in path.iterdir():
186
- kind = "/" if p.is_dir() else ""
187
- entries.append(f"{p.name}{kind}")
188
- return "\n".join(sorted(entries))
189
- except Exception as e:
190
- return f"Error listing files: {str(e)}"
191
-
192
- return f"Tool {tool_name} not found in CodeOps."
@@ -1,81 +0,0 @@
1
- from typing import List, Dict, Any
2
- import os
3
- from .base import Capability
4
- # Import core logic from top-level package
5
- # Import core logic lazily inside methods to avoid circular dependency
6
- # from ... import _depth_push, _depth_pop, _depth_show, _depth_reset
7
-
8
- class DepthTracker(Capability):
9
- def __init__(self):
10
- # Determine brain path (fallback for agent runtime if env not set)
11
- if not os.environ.get("NUCLEAR_BRAIN_PATH"):
12
- # Default used in verification/dev
13
- os.environ["NUCLEAR_BRAIN_PATH"] = "/Users/lokeshgarg/.gemini/antigravity/brain/7c654df4-b83e-43f9-8620-f15868ec39d1"
14
-
15
- @property
16
- def name(self) -> str:
17
- return "depth_tracker"
18
-
19
- @property
20
- def description(self) -> str:
21
- return "Tools for tracking conversation depth and preventing rabbit holes."
22
-
23
- def get_tools(self) -> List[Dict[str, Any]]:
24
- return [
25
- {
26
- "name": "brain_depth_push",
27
- "description": "Go deeper into a subtopic. Tracks position in conversation tree.",
28
- "parameters": {
29
- "type": "object",
30
- "properties": {
31
- "topic": {"type": "string", "description": "What you're diving into"}
32
- },
33
- "required": ["topic"]
34
- }
35
- },
36
- {
37
- "name": "brain_depth_pop",
38
- "description": "Come back up one level in the conversation tree.",
39
- "parameters": {"type": "object", "properties": {}}
40
- },
41
- {
42
- "name": "brain_depth_show",
43
- "description": "Show current depth state with visual indicator and tree.",
44
- "parameters": {"type": "object", "properties": {}}
45
- },
46
- {
47
- "name": "brain_depth_reset",
48
- "description": "Reset depth to root level (0). Clears all levels.",
49
- "parameters": {"type": "object", "properties": {}}
50
- }
51
- ]
52
-
53
- def execute_tool(self, tool_name: str, args: Dict) -> str:
54
- # Lazy import to avoid circular dependency
55
- from ... import _depth_push, _depth_pop, _depth_show, _depth_reset
56
-
57
- if tool_name == "brain_depth_push":
58
- result = _depth_push(args.get("topic"))
59
- if "error" in result:
60
- return f"Error: {result['error']}"
61
- return f"Descended to Level {result.get('current_depth')}: '{result.get('topic')}'.\n{result.get('warning', '')}\n{result.get('indicator')}"
62
-
63
- elif tool_name == "brain_depth_pop":
64
- result = _depth_pop()
65
- if "error" in result:
66
- return f"Error: {result['error']}"
67
- return f"{result.get('message')}\n{result.get('indicator')}"
68
-
69
- elif tool_name == "brain_depth_show":
70
- result = _depth_show()
71
- if "error" in result:
72
- return f"Error: {result['error']}"
73
- return f"{result.get('indicator')}\nStatus: {result.get('status')}\nPath: {result.get('breadcrumbs')}\n\n{result.get('tree')}"
74
-
75
- elif tool_name == "brain_depth_reset":
76
- result = _depth_reset()
77
- if "error" in result:
78
- return f"Error: {result['error']}"
79
- return result.get('message', 'Reset complete')
80
-
81
- return f"Tool {tool_name} not found in DepthTracker."
@@ -1,274 +0,0 @@
1
- from typing import List, Dict, Any, Optional
2
- import json
3
- import os
4
- import time
5
- from pathlib import Path
6
- from .base import Capability
7
-
8
- class FeatureMap(Capability):
9
- def __init__(self):
10
- # Determine brain path
11
- brain_path_str = os.environ.get("NUCLEAR_BRAIN_PATH")
12
- if not brain_path_str:
13
- # Fallback for dev/verification if not set (though highly recommended)
14
- brain_path_str = "/Users/lokeshgarg/.gemini/antigravity/brain/7c654df4-b83e-43f9-8620-f15868ec39d1"
15
-
16
- self.brain_path = Path(brain_path_str)
17
- self.features_dir = self.brain_path / "features"
18
-
19
- @property
20
- def name(self) -> str:
21
- return "feature_map"
22
-
23
- @property
24
- def description(self) -> str:
25
- return "Manage the feature inventory to prevent 'feature amnesia'. Track what exists, status, and how to test."
26
-
27
- def get_tools(self) -> List[Dict[str, Any]]:
28
- return [
29
- {
30
- "name": "brain_add_feature",
31
- "description": "Add a new feature to the product's feature map.",
32
- "parameters": {
33
- "type": "object",
34
- "properties": {
35
- "product": {"type": "string", "description": "'gentlequest' or 'nucleus'"},
36
- "name": {"type": "string", "description": "Human-readable feature name"},
37
- "description": {"type": "string", "description": "What the feature does"},
38
- "source": {"type": "string", "description": "Where it lives (e.g., 'gentlequest_app', 'pypi_mcp')"},
39
- "version": {"type": "string", "description": "Which version it shipped in"},
40
- "status": {"type": "string", "description": "development/staged/production/released"},
41
- "how_to_test": {"type": "array", "items": {"type": "string"}, "description": "List of test steps"},
42
- "expected_result": {"type": "string", "description": "What should happen when testing"},
43
- "tags": {"type": "array", "items": {"type": "string"}, "description": "Searchable tags"}
44
- },
45
- "required": ["product", "name", "description", "source", "version", "how_to_test", "expected_result"]
46
- }
47
- },
48
- {
49
- "name": "brain_list_features",
50
- "description": "List all features, optionally filtered.",
51
- "parameters": {
52
- "type": "object",
53
- "properties": {
54
- "product": {"type": "string", "description": "Filter by product"},
55
- "status": {"type": "string", "description": "Filter by status"},
56
- "tag": {"type": "string", "description": "Filter by tag"}
57
- }
58
- }
59
- },
60
- {
61
- "name": "brain_get_feature",
62
- "description": "Get a specific feature by ID.",
63
- "parameters": {
64
- "type": "object",
65
- "properties": {
66
- "feature_id": {"type": "string", "description": "The feature ID (snake_case)"}
67
- },
68
- "required": ["feature_id"]
69
- }
70
- },
71
- {
72
- "name": "brain_update_feature",
73
- "description": "Update a feature's fields.",
74
- "parameters": {
75
- "type": "object",
76
- "properties": {
77
- "feature_id": {"type": "string", "description": "Feature to update"},
78
- "status": {"type": "string", "description": "New status"},
79
- "description": {"type": "string", "description": "New description"},
80
- "version": {"type": "string", "description": "New version"}
81
- },
82
- "required": ["feature_id"]
83
- }
84
- },
85
- {
86
- "name": "brain_mark_validated",
87
- "description": "Mark a feature as validated after testing.",
88
- "parameters": {
89
- "type": "object",
90
- "properties": {
91
- "feature_id": {"type": "string", "description": "Feature that was tested"},
92
- "result": {"type": "string", "description": "'passed' or 'failed'"}
93
- },
94
- "required": ["feature_id", "result"]
95
- }
96
- },
97
- {
98
- "name": "brain_search_features",
99
- "description": "Search features by name, description, or tags.",
100
- "parameters": {
101
- "type": "object",
102
- "properties": {
103
- "query": {"type": "string", "description": "Search query"}
104
- },
105
- "required": ["query"]
106
- }
107
- }
108
- ]
109
-
110
- def execute_tool(self, tool_name: str, args: Dict) -> Any:
111
- try:
112
- if tool_name == "brain_add_feature":
113
- return self._add_feature(args)
114
- elif tool_name == "brain_list_features":
115
- return self._list_features(args)
116
- elif tool_name == "brain_get_feature":
117
- return self._get_feature(args.get("feature_id"))
118
- elif tool_name == "brain_update_feature":
119
- return self._update_feature(args)
120
- elif tool_name == "brain_mark_validated":
121
- return self._mark_validated(args.get("feature_id"), args.get("result"))
122
- elif tool_name == "brain_search_features":
123
- return self._search_features(args.get("query"))
124
- return f"Tool {tool_name} not found"
125
- except Exception as e:
126
- return f"Error executing {tool_name}: {str(e)}"
127
-
128
- # --- Internal Helpers ---
129
-
130
- def _get_store_path(self, product: str) -> Path:
131
- return self.features_dir / f"{product}.json"
132
-
133
- def _load_store(self, product: str) -> Dict:
134
- path = self._get_store_path(product)
135
- if not path.exists():
136
- return {"product": product, "features": []}
137
- try:
138
- return json.loads(path.read_text())
139
- except Exception:
140
- return {"product": product, "features": []}
141
-
142
- def _save_store(self, product: str, data: Dict):
143
- path = self._get_store_path(product)
144
- # atomic write
145
- path.parent.mkdir(parents=True, exist_ok=True)
146
- path.write_text(json.dumps(data, indent=2))
147
-
148
- def _generate_id(self, name: str) -> str:
149
- # Simple snake_case conversion
150
- return name.lower().replace(" ", "_").replace("-", "_")
151
-
152
- # --- Core Logic ---
153
-
154
- def _add_feature(self, args: Dict) -> Dict:
155
- product = args.get("product").lower()
156
- if product not in ["gentlequest", "nucleus"]:
157
- # Default to gentlequest if unknown product?
158
- # Or enforce schema. Let's strictly enforce supported products for MVP
159
- if product not in ["gentlequest", "nucleus"]:
160
- # Just treat it as a new product file
161
- pass
162
-
163
- store = self._load_store(product)
164
-
165
- feature_id = self._generate_id(args.get("name"))
166
-
167
- # Check if exists
168
- for f in store["features"]:
169
- if f["id"] == feature_id:
170
- return {"error": f"Feature '{feature_id}' already exists. Use update."}
171
-
172
- new_feature = {
173
- "id": feature_id,
174
- "name": args.get("name"),
175
- "description": args.get("description"),
176
- "product": product,
177
- "source": args.get("source"),
178
- "version": args.get("version"),
179
- "status": args.get("status", "development"),
180
- "how_to_test": args.get("how_to_test", []),
181
- "expected_result": args.get("expected_result", ""),
182
- "tags": args.get("tags", []),
183
- "created_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
184
- "last_validated": None,
185
- "validation_result": None
186
- }
187
-
188
- store["features"].append(new_feature)
189
- store["total_features"] = len(store["features"])
190
- store["last_updated"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
191
-
192
- self._save_store(product, store)
193
- return {"success": True, "feature": new_feature}
194
-
195
- def _list_features(self, args: Dict) -> List[Dict]:
196
- products = ["gentlequest", "nucleus"]
197
- target_product = args.get("product")
198
- if target_product:
199
- products = [target_product]
200
-
201
- all_features = []
202
- for p in products:
203
- store = self._load_store(p)
204
- all_features.extend(store.get("features", []))
205
-
206
- # Filters
207
- status = args.get("status")
208
- tag = args.get("tag")
209
-
210
- if status:
211
- all_features = [f for f in all_features if f.get("status") == status]
212
- if tag:
213
- all_features = [f for f in all_features if tag in f.get("tags", [])]
214
-
215
- return all_features[:50] # Limit output
216
-
217
- def _get_find_feature(self, feature_id: str) -> Optional[Dict]:
218
- # Helper to search across all products
219
- for p in ["gentlequest", "nucleus"]:
220
- store = self._load_store(p)
221
- for f in store.get("features", []):
222
- if f["id"] == feature_id:
223
- return f, p, store
224
- return None, None, None
225
-
226
- def _get_feature(self, feature_id: str) -> Any:
227
- f, _, _ = self._get_find_feature(feature_id)
228
- if f:
229
- return f
230
- return {"error": f"Feature '{feature_id}' not found"}
231
-
232
- def _update_feature(self, args: Dict) -> Any:
233
- feature_id = args.get("feature_id")
234
- f, product, store = self._get_find_feature(feature_id)
235
-
236
- if not f:
237
- return {"error": f"Feature '{feature_id}' not found"}
238
-
239
- # Update fields
240
- valid_updates = ["status", "description", "version"]
241
- updated = False
242
- for key in valid_updates:
243
- if key in args:
244
- f[key] = args[key]
245
- updated = True
246
-
247
- if updated:
248
- self._save_store(product, store)
249
- return {"success": True, "feature": f}
250
- return {"success": False, "message": "No changes made"}
251
-
252
- def _mark_validated(self, feature_id: str, result: str) -> Any:
253
- f, product, store = self._get_find_feature(feature_id)
254
- if not f:
255
- return {"error": f"Feature '{feature_id}' not found"}
256
-
257
- f["last_validated"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
258
- f["validation_result"] = result
259
-
260
- self._save_store(product, store)
261
- return {"success": True, "feature_id": feature_id, "result": result, "timestamp": f["last_validated"]}
262
-
263
- def _search_features(self, query: str) -> List[Dict]:
264
- query = query.lower()
265
- all_features = self._list_features({})
266
-
267
- matches = []
268
- for f in all_features:
269
- text = f"{f['name']} {f['description']} {' '.join(f.get('tags', []))}".lower()
270
- if query in text:
271
- matches.append(f)
272
-
273
- # Sort by relevance? For now just return matches
274
- return matches[:20]