delimit-cli 2.4.0 → 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/.dockerignore +7 -0
- package/.github/workflows/ci.yml +22 -0
- package/CODE_OF_CONDUCT.md +48 -0
- package/CONTRIBUTING.md +67 -0
- package/Dockerfile +9 -0
- package/LICENSE +21 -0
- package/README.md +18 -69
- package/SECURITY.md +42 -0
- package/adapters/gemini-forge.js +11 -0
- package/adapters/gemini-jamsons.js +152 -0
- package/bin/delimit-cli.js +8 -0
- package/bin/delimit-setup.js +258 -0
- package/gateway/ai/backends/__init__.py +0 -0
- package/gateway/ai/backends/async_utils.py +21 -0
- package/gateway/ai/backends/deploy_bridge.py +150 -0
- package/gateway/ai/backends/gateway_core.py +261 -0
- package/gateway/ai/backends/generate_bridge.py +38 -0
- package/gateway/ai/backends/governance_bridge.py +196 -0
- package/gateway/ai/backends/intel_bridge.py +59 -0
- package/gateway/ai/backends/memory_bridge.py +93 -0
- package/gateway/ai/backends/ops_bridge.py +137 -0
- package/gateway/ai/backends/os_bridge.py +82 -0
- package/gateway/ai/backends/repo_bridge.py +117 -0
- package/gateway/ai/backends/ui_bridge.py +118 -0
- package/gateway/ai/backends/vault_bridge.py +129 -0
- package/gateway/ai/server.py +1182 -0
- package/gateway/core/__init__.py +3 -0
- package/gateway/core/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/auto_baseline.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/ci_formatter.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/contract_ledger.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/dependency_graph.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/dependency_manifest.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/diff_engine_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/event_backbone.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/event_schema.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/explainer.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway_v3.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/impact_analyzer.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/policy_engine.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry_v3.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/semver_classifier.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/spec_detector.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/surface_bridge.cpython-310.pyc +0 -0
- package/gateway/core/auto_baseline.py +304 -0
- package/gateway/core/ci_formatter.py +283 -0
- package/gateway/core/complexity_analyzer.py +386 -0
- package/gateway/core/contract_ledger.py +345 -0
- package/gateway/core/dependency_graph.py +218 -0
- package/gateway/core/dependency_manifest.py +223 -0
- package/gateway/core/diff_engine_v2.py +477 -0
- package/gateway/core/diff_engine_v2.py.bak +426 -0
- package/gateway/core/event_backbone.py +268 -0
- package/gateway/core/event_schema.py +258 -0
- package/gateway/core/explainer.py +438 -0
- package/gateway/core/gateway.py +128 -0
- package/gateway/core/gateway_v2.py +154 -0
- package/gateway/core/gateway_v3.py +224 -0
- package/gateway/core/impact_analyzer.py +163 -0
- package/gateway/core/policies/default.yml +13 -0
- package/gateway/core/policies/relaxed.yml +48 -0
- package/gateway/core/policies/strict.yml +55 -0
- package/gateway/core/policy_engine.py +464 -0
- package/gateway/core/registry.py +52 -0
- package/gateway/core/registry_v2.py +132 -0
- package/gateway/core/registry_v3.py +134 -0
- package/gateway/core/semver_classifier.py +152 -0
- package/gateway/core/spec_detector.py +130 -0
- package/gateway/core/surface_bridge.py +307 -0
- package/gateway/core/zero_spec/__init__.py +4 -0
- package/gateway/core/zero_spec/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/detector.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/express_extractor.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/fastapi_extractor.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/nestjs_extractor.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/detector.py +353 -0
- package/gateway/core/zero_spec/express_extractor.py +483 -0
- package/gateway/core/zero_spec/fastapi_extractor.py +254 -0
- package/gateway/core/zero_spec/nestjs_extractor.py +369 -0
- package/gateway/tasks/__init__.py +1 -0
- package/gateway/tasks/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy_v3.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/explain_diff.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/explain_diff_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api_v3.cpython-310.pyc +0 -0
- package/gateway/tasks/check_policy.py +177 -0
- package/gateway/tasks/check_policy_v2.py +255 -0
- package/gateway/tasks/check_policy_v3.py +255 -0
- package/gateway/tasks/explain_diff.py +305 -0
- package/gateway/tasks/explain_diff_v2.py +267 -0
- package/gateway/tasks/validate_api.py +131 -0
- package/gateway/tasks/validate_api_v2.py +208 -0
- package/gateway/tasks/validate_api_v3.py +163 -0
- package/package.json +2 -2
- package/adapters/codex-skill.js +0 -87
- package/adapters/cursor-extension.js +0 -190
- package/adapters/gemini-action.js +0 -93
- package/adapters/openai-function.js +0 -112
- package/adapters/xai-plugin.js +0 -151
- package/test-decision-engine.js +0 -181
- package/test-hook.js +0 -27
- package/tests/cli.test.js +0 -359
- package/tests/fixtures/openapi-changed.yaml +0 -56
- package/tests/fixtures/openapi.yaml +0 -87
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bridge to repo-level tools: repodoctor, configsentry, evidencepack, securitygate.
|
|
3
|
+
Tier 3 Extended — repository health, config audit, evidence, security.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
import asyncio
|
|
10
|
+
import importlib
|
|
11
|
+
import logging
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
from .async_utils import run_async
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("delimit.ai.repo_bridge")
|
|
17
|
+
|
|
18
|
+
PACKAGES = Path("/home/delimit/.delimit_suite/packages")
|
|
19
|
+
|
|
20
|
+
# Add PACKAGES dir so `from shared.base_server import BaseMCPServer` resolves
|
|
21
|
+
_packages = str(PACKAGES)
|
|
22
|
+
if _packages not in sys.path:
|
|
23
|
+
sys.path.insert(0, _packages)
|
|
24
|
+
|
|
25
|
+
_servers = {}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _call(pkg: str, factory_name: str, method: str, args: Dict, tool_label: str) -> Dict[str, Any]:
|
|
29
|
+
try:
|
|
30
|
+
srv = _servers.get(pkg)
|
|
31
|
+
if srv is None:
|
|
32
|
+
mod = importlib.import_module(f"{pkg}.server")
|
|
33
|
+
factory = getattr(mod, factory_name)
|
|
34
|
+
srv = factory()
|
|
35
|
+
# Some servers need async initialization (e.g. evidencepack)
|
|
36
|
+
init_fn = getattr(srv, "initialize", None)
|
|
37
|
+
if init_fn and asyncio.iscoroutinefunction(init_fn):
|
|
38
|
+
run_async(init_fn())
|
|
39
|
+
_servers[pkg] = srv
|
|
40
|
+
fn = getattr(srv, method, None)
|
|
41
|
+
if fn is None:
|
|
42
|
+
return {"tool": tool_label, "status": "not_implemented", "error": f"Method {method} not found"}
|
|
43
|
+
result = run_async(fn(args, None))
|
|
44
|
+
return json.loads(result) if isinstance(result, str) else result
|
|
45
|
+
except Exception as e:
|
|
46
|
+
return {"tool": tool_label, "error": str(e)}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ─── RepoDoctor ────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
def diagnose(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
52
|
+
return _call("repodoctor", "create_repodoctor_server", "_tool_health_check",
|
|
53
|
+
{"repository_path": target, **(options or {})}, "repo.diagnose")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def analyze(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
57
|
+
return _call("repodoctor", "create_repodoctor_server", "_tool_snapshot",
|
|
58
|
+
{"repository_path": target, **(options or {})}, "repo.analyze")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ─── ConfigSentry ───────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
def config_validate(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
64
|
+
return _call("configsentry", "create_configsentry_server", "_tool_validate",
|
|
65
|
+
{"repository_path": target, **(options or {})}, "config.validate")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def config_audit(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
69
|
+
return _call("configsentry", "create_configsentry_server", "_tool_env_audit",
|
|
70
|
+
{"repository_path": target, **(options or {})}, "config.audit")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ─── EvidencePack ───────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def evidence_collect(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
76
|
+
result = _call("evidencepack", "create_evidencepack_server", "_tool_list",
|
|
77
|
+
{"limit": 20, **(options or {})}, "evidence.collect")
|
|
78
|
+
# Provide a clear message when no evidence bundles exist yet
|
|
79
|
+
if isinstance(result, dict) and result.get("total_bundles", -1) == 0:
|
|
80
|
+
result["message"] = (
|
|
81
|
+
"No evidence collected yet. Use evidence.begin to start a collection, "
|
|
82
|
+
"evidence.capture to add items, and evidence.finalize to create a bundle."
|
|
83
|
+
)
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def evidence_verify(bundle_id: Optional[str] = None, bundle_path: Optional[str] = None, options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
88
|
+
args = {**(options or {})}
|
|
89
|
+
if bundle_id:
|
|
90
|
+
args["bundle_id"] = bundle_id
|
|
91
|
+
if bundle_path:
|
|
92
|
+
args["bundle_path"] = bundle_path
|
|
93
|
+
if not bundle_id and not bundle_path:
|
|
94
|
+
return {"tool": "evidence.verify", "status": "no_input", "message": "Provide bundle_id or bundle_path to verify"}
|
|
95
|
+
return _call("evidencepack", "create_evidencepack_server", "_tool_validate",
|
|
96
|
+
args, "evidence.verify")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ─── SecurityGate ───────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
_INTERNAL_TOKEN = os.environ.get("DELIMIT_INTERNAL_BRIDGE_TOKEN", "delimit-internal-bridge")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def security_scan(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
105
|
+
result = _call("securitygate", "create_securitygate_server", "_tool_scan",
|
|
106
|
+
{"target": target, "authorization_token": _INTERNAL_TOKEN, **(options or {})}, "security.scan")
|
|
107
|
+
# Guard against fabricated/hardcoded CVE data from stub implementations
|
|
108
|
+
vulns = result.get("vulnerabilities", [])
|
|
109
|
+
if vulns and any("CVE-2023-12345" in str(v.get("id", "")) for v in vulns):
|
|
110
|
+
return {"tool": "security.scan", "status": "not_available",
|
|
111
|
+
"error": "Security scanner returned placeholder data. Install a real vulnerability scanner."}
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def security_audit(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
116
|
+
return _call("securitygate", "create_securitygate_server", "_tool_audit",
|
|
117
|
+
{"target": target, "authorization_token": _INTERNAL_TOKEN, **(options or {})}, "security.audit")
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bridge to UI tooling: designsystem, storybook, testsmith, docsweaver.
|
|
3
|
+
UI/DX tools for component development and testing.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import json
|
|
8
|
+
import asyncio
|
|
9
|
+
import importlib
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
from .async_utils import run_async
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("delimit.ai.ui_bridge")
|
|
16
|
+
|
|
17
|
+
PACKAGES = Path("/home/delimit/.delimit_suite/packages")
|
|
18
|
+
|
|
19
|
+
# Add PACKAGES dir so `from shared.base_server import BaseMCPServer` resolves
|
|
20
|
+
_packages = str(PACKAGES)
|
|
21
|
+
if _packages not in sys.path:
|
|
22
|
+
sys.path.insert(0, _packages)
|
|
23
|
+
|
|
24
|
+
_servers = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _call(pkg: str, factory_name: str, method: str, args: Dict, tool_label: str) -> Dict[str, Any]:
|
|
28
|
+
"""Call a _tool_* method on a BaseMCPServer-derived package."""
|
|
29
|
+
try:
|
|
30
|
+
srv = _servers.get(pkg)
|
|
31
|
+
if srv is None:
|
|
32
|
+
mod = importlib.import_module(f"{pkg}.server")
|
|
33
|
+
factory = getattr(mod, factory_name)
|
|
34
|
+
srv = factory()
|
|
35
|
+
_servers[pkg] = srv
|
|
36
|
+
fn = getattr(srv, method, None)
|
|
37
|
+
if fn is None:
|
|
38
|
+
return {"tool": tool_label, "status": "not_implemented", "error": f"Method {method} not found"}
|
|
39
|
+
result = run_async(fn(args, None))
|
|
40
|
+
return json.loads(result) if isinstance(result, str) else result
|
|
41
|
+
except Exception as e:
|
|
42
|
+
return {"tool": tool_label, "error": str(e)}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ─── DesignSystem (custom classes, no BaseMCPServer) ───────────────────
|
|
46
|
+
# designsystem uses DesignSystemGenerator, not the _tool_* pattern.
|
|
47
|
+
# Provide graceful pass-through until refactored.
|
|
48
|
+
|
|
49
|
+
def design_validate_responsive(project_path: str, check_types: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
50
|
+
return {"tool": "design.validate_responsive", "project_path": project_path, "status": "pass-through"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def design_extract_tokens(figma_file_key: str, token_types: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
54
|
+
return {"tool": "design.extract_tokens", "figma_file_key": figma_file_key, "status": "pass-through"}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def design_generate_component(component_name: str, figma_node_id: Optional[str] = None, output_path: Optional[str] = None) -> Dict[str, Any]:
|
|
58
|
+
return {"tool": "design.generate_component", "component_name": component_name, "status": "pass-through"}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def design_generate_tailwind(figma_file_key: str, output_path: Optional[str] = None) -> Dict[str, Any]:
|
|
62
|
+
return {"tool": "design.generate_tailwind", "figma_file_key": figma_file_key, "status": "pass-through"}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def design_component_library(project_path: str, output_format: str = "json") -> Dict[str, Any]:
|
|
66
|
+
return {"tool": "design.component_library", "project_path": project_path, "status": "pass-through"}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ─── Storybook (custom classes, no BaseMCPServer) ─────────────────────
|
|
70
|
+
|
|
71
|
+
def story_generate(component_path: str, story_name: Optional[str] = None, variants: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
72
|
+
return {"tool": "story.generate", "component_path": component_path, "status": "pass-through"}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def story_visual_test(url: str, project_path: Optional[str] = None, threshold: float = 0.05) -> Dict[str, Any]:
|
|
76
|
+
return {"tool": "story.visual_test", "url": url, "status": "pass-through"}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def story_build(project_path: str, output_dir: Optional[str] = None) -> Dict[str, Any]:
|
|
80
|
+
return {"tool": "story.build", "project_path": project_path, "status": "pass-through"}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def story_accessibility_test(project_path: str, standards: str = "WCAG2AA") -> Dict[str, Any]:
|
|
84
|
+
return {"tool": "story.accessibility_test", "project_path": project_path, "status": "pass-through"}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ─── TestSmith (BaseMCPServer pattern) ─────────────────────────────────
|
|
88
|
+
|
|
89
|
+
def test_generate(project_path: str, source_files: Optional[List[str]] = None, framework: str = "jest") -> Dict[str, Any]:
|
|
90
|
+
return _call("testsmith", "create_testsmith_server", "_tool_generate",
|
|
91
|
+
{"project_path": project_path, "source_files": source_files, "framework": framework}, "test.generate")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_coverage(project_path: str, threshold: int = 80) -> Dict[str, Any]:
|
|
95
|
+
return _call("testsmith", "create_testsmith_server", "_tool_coverage",
|
|
96
|
+
{"project_path": project_path, "threshold": threshold}, "test.coverage")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_smoke(project_path: str, test_suite: Optional[str] = None) -> Dict[str, Any]:
|
|
100
|
+
result = _call("testsmith", "create_testsmith_server", "_tool_smoke",
|
|
101
|
+
{"project_path": project_path}, "test.smoke")
|
|
102
|
+
# Guard against stub that says "passed" with 0 tests actually run
|
|
103
|
+
if result.get("tests_run", -1) == 0 and result.get("passed") is True:
|
|
104
|
+
return {"tool": "test.smoke", "status": "no_tests",
|
|
105
|
+
"error": "No smoke tests configured. The test runner found 0 tests to execute."}
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ─── DocsWeaver (BaseMCPServer pattern) ────────────────────────────────
|
|
110
|
+
|
|
111
|
+
def docs_generate(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
112
|
+
return _call("docsweaver", "create_docsweaver_server", "_tool_generate",
|
|
113
|
+
{"project_path": target, "doc_types": ["api", "readme"], **(options or {})}, "docs.generate")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def docs_validate(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
117
|
+
return _call("docsweaver", "create_docsweaver_server", "_tool_validate",
|
|
118
|
+
{"docs_path": target, **(options or {})}, "docs.validate")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bridge to delimit-vault package.
|
|
3
|
+
Tier 2 Platform tools — artifact and credential storage.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import json
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
from .async_utils import run_async
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("delimit.ai.vault_bridge")
|
|
15
|
+
|
|
16
|
+
VAULT_PACKAGE = Path("/home/delimit/.delimit_suite/packages/delimit-vault")
|
|
17
|
+
|
|
18
|
+
_server = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_server():
|
|
22
|
+
"""Return the vault server instance (lazy — no async init here).
|
|
23
|
+
|
|
24
|
+
The Qdrant client uses aiohttp which is bound to the event loop it was
|
|
25
|
+
created on. ``run_async`` creates a *new* loop per call, so we must NOT
|
|
26
|
+
call ``_initialize_clients()`` here. Instead each bridge method calls
|
|
27
|
+
``_ensure_initialized()`` inside the *same* ``run_async`` invocation that
|
|
28
|
+
performs the actual operation, keeping the aiohttp session alive for the
|
|
29
|
+
duration of the request.
|
|
30
|
+
"""
|
|
31
|
+
global _server
|
|
32
|
+
if _server is not None:
|
|
33
|
+
return _server
|
|
34
|
+
pkg_path = str(VAULT_PACKAGE / "delimit_vault_mcp")
|
|
35
|
+
if pkg_path not in sys.path:
|
|
36
|
+
sys.path.insert(0, pkg_path)
|
|
37
|
+
if str(VAULT_PACKAGE) not in sys.path:
|
|
38
|
+
sys.path.insert(0, str(VAULT_PACKAGE))
|
|
39
|
+
try:
|
|
40
|
+
from delimit_vault_mcp.server import DelimitVaultServer
|
|
41
|
+
_server = DelimitVaultServer()
|
|
42
|
+
return _server
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.warning(f"Failed to init vault server: {e}")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def _ensure_initialized(srv):
|
|
49
|
+
"""(Re-)initialize Qdrant client on the *current* event loop.
|
|
50
|
+
|
|
51
|
+
Because ``run_async`` may create a fresh event loop for each bridge call,
|
|
52
|
+
the previous aiohttp session becomes invalid. We unconditionally
|
|
53
|
+
re-initialize to bind the session to the current loop.
|
|
54
|
+
"""
|
|
55
|
+
await srv._initialize_clients()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _extract_text(result) -> Dict[str, Any]:
|
|
59
|
+
"""Extract text from MCP TextContent objects or raw results."""
|
|
60
|
+
if isinstance(result, str):
|
|
61
|
+
try:
|
|
62
|
+
return json.loads(result)
|
|
63
|
+
except json.JSONDecodeError:
|
|
64
|
+
return {"result": result}
|
|
65
|
+
if isinstance(result, dict):
|
|
66
|
+
return result
|
|
67
|
+
if isinstance(result, list):
|
|
68
|
+
# Handle list of TextContent objects
|
|
69
|
+
texts = []
|
|
70
|
+
for item in result:
|
|
71
|
+
if hasattr(item, "text"):
|
|
72
|
+
try:
|
|
73
|
+
texts.append(json.loads(item.text))
|
|
74
|
+
except (json.JSONDecodeError, TypeError):
|
|
75
|
+
texts.append({"text": str(item.text)})
|
|
76
|
+
else:
|
|
77
|
+
texts.append(str(item))
|
|
78
|
+
return texts[0] if len(texts) == 1 else {"results": texts}
|
|
79
|
+
if hasattr(result, "text"):
|
|
80
|
+
try:
|
|
81
|
+
return json.loads(result.text)
|
|
82
|
+
except (json.JSONDecodeError, TypeError):
|
|
83
|
+
return {"text": str(result.text)}
|
|
84
|
+
return {"result": str(result)}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def search(query: str) -> Dict[str, Any]:
|
|
88
|
+
"""Search vault entries."""
|
|
89
|
+
srv = _get_server()
|
|
90
|
+
if srv is None:
|
|
91
|
+
return {"error": "Vault server unavailable", "results": []}
|
|
92
|
+
try:
|
|
93
|
+
async def _do():
|
|
94
|
+
await _ensure_initialized(srv)
|
|
95
|
+
return await srv._handle_search({"query": query})
|
|
96
|
+
result = run_async(_do())
|
|
97
|
+
return _extract_text(result)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
return {"error": f"Vault search failed: {e}", "results": []}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def health() -> Dict[str, Any]:
|
|
103
|
+
"""Check vault health."""
|
|
104
|
+
srv = _get_server()
|
|
105
|
+
if srv is None:
|
|
106
|
+
return {"status": "unavailable", "error": "Vault server not initialized"}
|
|
107
|
+
try:
|
|
108
|
+
async def _do():
|
|
109
|
+
await _ensure_initialized(srv)
|
|
110
|
+
return await srv._handle_health()
|
|
111
|
+
result = run_async(_do())
|
|
112
|
+
return _extract_text(result)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
return {"status": "unavailable", "error": str(e)}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def snapshot(task_id: str = "vault-snapshot") -> Dict[str, Any]:
|
|
118
|
+
"""Get vault snapshot."""
|
|
119
|
+
srv = _get_server()
|
|
120
|
+
if srv is None:
|
|
121
|
+
return {"error": "Vault server unavailable"}
|
|
122
|
+
try:
|
|
123
|
+
async def _do():
|
|
124
|
+
await _ensure_initialized(srv)
|
|
125
|
+
return await srv._handle_snapshot({"task_id": task_id})
|
|
126
|
+
result = run_async(_do())
|
|
127
|
+
return _extract_text(result)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
return {"error": f"Vault snapshot failed: {e}"}
|