delimit-cli 3.14.28 → 3.14.30
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/delimit-setup.js +3 -6
- package/gateway/ai/backends/deploy_bridge.py +56 -2
- package/gateway/ai/backends/gateway_core.py +212 -1
- package/gateway/ai/backends/generate_bridge.py +84 -13
- package/gateway/ai/backends/governance_bridge.py +63 -16
- package/gateway/ai/backends/memory_bridge.py +77 -76
- package/gateway/ai/backends/ops_bridge.py +76 -6
- package/gateway/ai/backends/os_bridge.py +23 -3
- package/gateway/ai/backends/repo_bridge.py +156 -17
- package/gateway/ai/backends/tools_design.py +116 -9
- package/gateway/ai/backends/tools_infra.py +200 -72
- package/gateway/ai/backends/tools_real.py +8 -0
- package/gateway/ai/backends/ui_bridge.py +115 -5
- package/gateway/ai/backends/vault_bridge.py +69 -114
- package/gateway/ai/content_engine.py +1276 -0
- package/gateway/ai/context_fs.py +193 -0
- package/gateway/ai/daemon.py +500 -0
- package/gateway/ai/data_plane.py +291 -0
- package/gateway/ai/deliberation.py +1033 -6
- package/gateway/ai/events.py +39 -0
- package/gateway/ai/founding_users.py +162 -0
- package/gateway/ai/governance.py +698 -4
- package/gateway/ai/inbox_daemon.py +78 -17
- package/gateway/ai/integrations/__init__.py +1 -0
- package/gateway/ai/integrations/opensage_wrapper.py +288 -0
- package/gateway/ai/key_resolver.py +95 -0
- package/gateway/ai/ledger_manager.py +289 -1
- package/gateway/ai/license.py +62 -4
- package/gateway/ai/license_core.py +208 -7
- package/gateway/ai/local_server.py +215 -0
- package/gateway/ai/loop_engine.py +408 -0
- package/gateway/ai/mcp_bridge.py +178 -0
- package/gateway/ai/release_sync.py +2 -2
- package/gateway/ai/screen_record.py +374 -0
- package/gateway/ai/secrets_broker.py +235 -0
- package/gateway/ai/social.py +189 -27
- package/gateway/ai/social_target.py +1635 -0
- package/gateway/ai/supabase_sync.py +190 -0
- package/gateway/ai/tracing.py +195 -0
- package/gateway/core/contract_ledger.py +1 -1
- package/gateway/core/dependency_graph.py +1 -1
- package/gateway/core/dependency_manifest.py +1 -1
- package/gateway/core/diff_engine_v2.py +272 -78
- package/gateway/core/event_backbone.py +2 -2
- package/gateway/core/event_schema.py +1 -1
- package/gateway/core/impact_analyzer.py +1 -1
- package/gateway/core/policy_engine.py +4 -0
- package/package.json +1 -1
|
@@ -57,9 +57,90 @@ from .tools_design import (
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def story_build(project_path: str, output_dir: Optional[str] = None) -> Dict[str, Any]:
|
|
60
|
-
"""
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
"""Build Storybook static site. Works if Storybook is installed; helpful guidance otherwise."""
|
|
61
|
+
import shutil as _shutil
|
|
62
|
+
import subprocess as _subprocess
|
|
63
|
+
|
|
64
|
+
root = Path(project_path)
|
|
65
|
+
# Quick check: does the project have a Storybook config?
|
|
66
|
+
has_storybook_config = any(
|
|
67
|
+
(root / d).is_dir() for d in (".storybook",)
|
|
68
|
+
)
|
|
69
|
+
has_storybook_dep = False
|
|
70
|
+
pkg_json = root / "package.json"
|
|
71
|
+
if pkg_json.exists():
|
|
72
|
+
try:
|
|
73
|
+
pkg = json.loads(pkg_json.read_text())
|
|
74
|
+
all_deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
|
75
|
+
has_storybook_dep = any("storybook" in k for k in all_deps)
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
# Check if npx storybook is available
|
|
80
|
+
npx = _shutil.which("npx")
|
|
81
|
+
if not npx:
|
|
82
|
+
return {
|
|
83
|
+
"tool": "story.build",
|
|
84
|
+
"project_path": project_path,
|
|
85
|
+
"status": "no_npx",
|
|
86
|
+
"message": (
|
|
87
|
+
"npx is not available. Install Node.js and npm first, then:\n"
|
|
88
|
+
" 1. cd {project_path}\n"
|
|
89
|
+
" 2. npx storybook@latest init\n"
|
|
90
|
+
" 3. npx storybook build"
|
|
91
|
+
).format(project_path=project_path),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if not has_storybook_config and not has_storybook_dep:
|
|
95
|
+
return {
|
|
96
|
+
"tool": "story.build",
|
|
97
|
+
"project_path": project_path,
|
|
98
|
+
"status": "not_configured",
|
|
99
|
+
"message": (
|
|
100
|
+
"Storybook is not configured in this project. To set it up:\n"
|
|
101
|
+
" 1. cd {project_path}\n"
|
|
102
|
+
" 2. npx storybook@latest init\n"
|
|
103
|
+
" 3. npx storybook build\n\n"
|
|
104
|
+
"Alternatively, use `delimit_story_generate` to create story files "
|
|
105
|
+
"without installing Storybook."
|
|
106
|
+
).format(project_path=project_path),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Storybook is present -- attempt to build
|
|
110
|
+
cmd = ["npx", "storybook", "build"]
|
|
111
|
+
if output_dir:
|
|
112
|
+
cmd += ["-o", output_dir]
|
|
113
|
+
try:
|
|
114
|
+
result = _subprocess.run(
|
|
115
|
+
cmd, cwd=str(root), capture_output=True, timeout=120,
|
|
116
|
+
)
|
|
117
|
+
if result.returncode == 0:
|
|
118
|
+
out_dir = output_dir or str(root / "storybook-static")
|
|
119
|
+
return {
|
|
120
|
+
"tool": "story.build",
|
|
121
|
+
"status": "ok",
|
|
122
|
+
"project_path": project_path,
|
|
123
|
+
"output_dir": out_dir,
|
|
124
|
+
"message": f"Storybook built successfully to {out_dir}",
|
|
125
|
+
}
|
|
126
|
+
else:
|
|
127
|
+
stderr = result.stderr.decode(errors="replace")[:800]
|
|
128
|
+
return {
|
|
129
|
+
"tool": "story.build",
|
|
130
|
+
"status": "build_error",
|
|
131
|
+
"project_path": project_path,
|
|
132
|
+
"error": stderr,
|
|
133
|
+
"hint": "Ensure dependencies are installed (npm install) and Storybook config is valid.",
|
|
134
|
+
}
|
|
135
|
+
except _subprocess.TimeoutExpired:
|
|
136
|
+
return {
|
|
137
|
+
"tool": "story.build",
|
|
138
|
+
"status": "timeout",
|
|
139
|
+
"project_path": project_path,
|
|
140
|
+
"message": "Storybook build timed out after 120 seconds.",
|
|
141
|
+
}
|
|
142
|
+
except Exception as e:
|
|
143
|
+
return {"tool": "story.build", "status": "error", "error": str(e)}
|
|
63
144
|
|
|
64
145
|
|
|
65
146
|
def story_accessibility_test(project_path: str, standards: str = "WCAG2AA") -> Dict[str, Any]:
|
|
@@ -70,16 +151,43 @@ def story_accessibility_test(project_path: str, standards: str = "WCAG2AA") -> D
|
|
|
70
151
|
# ─── TestSmith (Real implementations — tools_real.py) ─────────────────
|
|
71
152
|
|
|
72
153
|
def test_generate(project_path: str, source_files: Optional[List[str]] = None, framework: str = "jest") -> Dict[str, Any]:
|
|
154
|
+
"""Generate test skeletons for source files using the specified framework."""
|
|
73
155
|
from .tools_real import test_generate as _real_test_generate
|
|
74
156
|
return _real_test_generate(project_path=project_path, source_files=source_files, framework=framework)
|
|
75
157
|
|
|
76
158
|
|
|
77
159
|
def test_coverage(project_path: str, threshold: int = 80) -> Dict[str, Any]:
|
|
78
|
-
|
|
79
|
-
|
|
160
|
+
"""Estimate test coverage by counting test vs source files and checking config."""
|
|
161
|
+
root = Path(project_path).resolve()
|
|
162
|
+
skip = {"node_modules", "dist", ".next", ".git", "__pycache__", "build", ".cache", "venv", ".venv"}
|
|
163
|
+
src_files, test_files = [], []
|
|
164
|
+
src_exts = {".py", ".js", ".ts", ".jsx", ".tsx"}
|
|
165
|
+
test_patterns = {"test_", "_test.", ".test.", ".spec.", "tests/", "test/", "__tests__/"}
|
|
166
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
167
|
+
dirnames[:] = [d for d in dirnames if d not in skip]
|
|
168
|
+
for f in filenames:
|
|
169
|
+
fp = os.path.join(dirpath, f)
|
|
170
|
+
ext = os.path.splitext(f)[1]
|
|
171
|
+
if ext not in src_exts:
|
|
172
|
+
continue
|
|
173
|
+
if any(p in fp for p in test_patterns):
|
|
174
|
+
test_files.append(fp)
|
|
175
|
+
else:
|
|
176
|
+
src_files.append(fp)
|
|
177
|
+
ratio = (len(test_files) / max(len(src_files), 1)) * 100
|
|
178
|
+
# Check for coverage config
|
|
179
|
+
cov_configs = [c for c in ["jest.config.js", "jest.config.ts", ".nycrc", "pytest.ini",
|
|
180
|
+
"pyproject.toml", "setup.cfg", ".coveragerc"] if (root / c).exists()]
|
|
181
|
+
meets_threshold = ratio >= threshold
|
|
182
|
+
return {"tool": "test.coverage", "status": "ok", "project_path": str(root),
|
|
183
|
+
"source_files": len(src_files), "test_files": len(test_files),
|
|
184
|
+
"estimated_coverage_ratio": round(ratio, 1), "threshold": threshold,
|
|
185
|
+
"meets_threshold": meets_threshold, "coverage_configs": cov_configs,
|
|
186
|
+
"note": "File-count estimate. Run test runner with --coverage for precise line coverage."}
|
|
80
187
|
|
|
81
188
|
|
|
82
189
|
def test_smoke(project_path: str, test_suite: Optional[str] = None) -> Dict[str, Any]:
|
|
190
|
+
"""Run smoke tests for the project using the detected test framework."""
|
|
83
191
|
from .tools_real import test_smoke as _real_test_smoke
|
|
84
192
|
return _real_test_smoke(project_path=project_path, test_suite=test_suite)
|
|
85
193
|
|
|
@@ -87,10 +195,12 @@ def test_smoke(project_path: str, test_suite: Optional[str] = None) -> Dict[str,
|
|
|
87
195
|
# ─── DocsWeaver (Real implementations — tools_real.py) ────────────────
|
|
88
196
|
|
|
89
197
|
def docs_generate(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
198
|
+
"""Generate API documentation from source code docstrings and comments."""
|
|
90
199
|
from .tools_real import docs_generate as _real_docs_generate
|
|
91
200
|
return _real_docs_generate(target=target, options=options)
|
|
92
201
|
|
|
93
202
|
|
|
94
203
|
def docs_validate(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
204
|
+
"""Validate documentation quality, coverage, and link integrity."""
|
|
95
205
|
from .tools_real import docs_validate as _real_docs_validate
|
|
96
206
|
return _real_docs_validate(target=target, options=options)
|
|
@@ -1,130 +1,85 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
Vault bridge — file-based artifact and snapshot storage.
|
|
3
|
+
Stores vault entries as JSON files in ~/.delimit/vault/.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import os
|
|
7
|
-
import sys
|
|
8
6
|
import json
|
|
9
|
-
import
|
|
7
|
+
import hashlib
|
|
10
8
|
import logging
|
|
11
9
|
from pathlib import Path
|
|
12
|
-
from
|
|
13
|
-
from
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
14
12
|
|
|
15
13
|
logger = logging.getLogger("delimit.ai.vault_bridge")
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
_server = None
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _get_server():
|
|
23
|
-
"""Return the vault server instance (lazy — no async init here).
|
|
24
|
-
|
|
25
|
-
The Qdrant client uses aiohttp which is bound to the event loop it was
|
|
26
|
-
created on. ``run_async`` creates a *new* loop per call, so we must NOT
|
|
27
|
-
call ``_initialize_clients()`` here. Instead each bridge method calls
|
|
28
|
-
``_ensure_initialized()`` inside the *same* ``run_async`` invocation that
|
|
29
|
-
performs the actual operation, keeping the aiohttp session alive for the
|
|
30
|
-
duration of the request.
|
|
31
|
-
"""
|
|
32
|
-
global _server
|
|
33
|
-
if _server is not None:
|
|
34
|
-
return _server
|
|
35
|
-
pkg_path = str(VAULT_PACKAGE / "delimit_vault_mcp")
|
|
36
|
-
if pkg_path not in sys.path:
|
|
37
|
-
sys.path.insert(0, pkg_path)
|
|
38
|
-
if str(VAULT_PACKAGE) not in sys.path:
|
|
39
|
-
sys.path.insert(0, str(VAULT_PACKAGE))
|
|
40
|
-
try:
|
|
41
|
-
from delimit_vault_mcp.server import DelimitVaultServer
|
|
42
|
-
_server = DelimitVaultServer()
|
|
43
|
-
return _server
|
|
44
|
-
except Exception as e:
|
|
45
|
-
logger.warning(f"Failed to init vault server: {e}")
|
|
46
|
-
return None
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
async def _ensure_initialized(srv):
|
|
50
|
-
"""(Re-)initialize Qdrant client on the *current* event loop.
|
|
51
|
-
|
|
52
|
-
Because ``run_async`` may create a fresh event loop for each bridge call,
|
|
53
|
-
the previous aiohttp session becomes invalid. We unconditionally
|
|
54
|
-
re-initialize to bind the session to the current loop.
|
|
55
|
-
"""
|
|
56
|
-
await srv._initialize_clients()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _extract_text(result) -> Dict[str, Any]:
|
|
60
|
-
"""Extract text from MCP TextContent objects or raw results."""
|
|
61
|
-
if isinstance(result, str):
|
|
62
|
-
try:
|
|
63
|
-
return json.loads(result)
|
|
64
|
-
except json.JSONDecodeError:
|
|
65
|
-
return {"result": result}
|
|
66
|
-
if isinstance(result, dict):
|
|
67
|
-
return result
|
|
68
|
-
if isinstance(result, list):
|
|
69
|
-
# Handle list of TextContent objects
|
|
70
|
-
texts = []
|
|
71
|
-
for item in result:
|
|
72
|
-
if hasattr(item, "text"):
|
|
73
|
-
try:
|
|
74
|
-
texts.append(json.loads(item.text))
|
|
75
|
-
except (json.JSONDecodeError, TypeError):
|
|
76
|
-
texts.append({"text": str(item.text)})
|
|
77
|
-
else:
|
|
78
|
-
texts.append(str(item))
|
|
79
|
-
return texts[0] if len(texts) == 1 else {"results": texts}
|
|
80
|
-
if hasattr(result, "text"):
|
|
81
|
-
try:
|
|
82
|
-
return json.loads(result.text)
|
|
83
|
-
except (json.JSONDecodeError, TypeError):
|
|
84
|
-
return {"text": str(result.text)}
|
|
85
|
-
return {"result": str(result)}
|
|
15
|
+
VAULT_DIR = Path.home() / ".delimit" / "vault"
|
|
86
16
|
|
|
87
17
|
|
|
88
|
-
def
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return {"error": "Vault server unavailable", "results": []}
|
|
93
|
-
try:
|
|
94
|
-
async def _do():
|
|
95
|
-
await _ensure_initialized(srv)
|
|
96
|
-
return await srv._handle_search({"query": query})
|
|
97
|
-
result = run_async(_do())
|
|
98
|
-
return _extract_text(result)
|
|
99
|
-
except Exception as e:
|
|
100
|
-
return {"error": f"Vault search failed: {e}", "results": []}
|
|
18
|
+
def _ensure_dir():
|
|
19
|
+
VAULT_DIR.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
(VAULT_DIR / "snapshots").mkdir(exist_ok=True)
|
|
21
|
+
(VAULT_DIR / "entries").mkdir(exist_ok=True)
|
|
101
22
|
|
|
102
23
|
|
|
103
|
-
def
|
|
104
|
-
"""
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
24
|
+
def search(query: str) -> Dict[str, Any]:
|
|
25
|
+
"""Search vault entries by keyword."""
|
|
26
|
+
_ensure_dir()
|
|
27
|
+
query_lower = query.lower()
|
|
28
|
+
results = []
|
|
29
|
+
|
|
30
|
+
for f in sorted((VAULT_DIR / "entries").glob("*.json"), reverse=True):
|
|
31
|
+
try:
|
|
32
|
+
entry = json.loads(f.read_text())
|
|
33
|
+
content = json.dumps(entry).lower()
|
|
34
|
+
if query_lower in content:
|
|
35
|
+
results.append({
|
|
36
|
+
"id": entry.get("id", f.stem),
|
|
37
|
+
"title": entry.get("title", f.stem),
|
|
38
|
+
"type": entry.get("type", "unknown"),
|
|
39
|
+
"created_at": entry.get("created_at", ""),
|
|
40
|
+
"preview": str(entry.get("content", ""))[:200],
|
|
41
|
+
})
|
|
42
|
+
if len(results) >= 10:
|
|
43
|
+
break
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
return {"query": query, "results": results, "count": len(results)}
|
|
116
48
|
|
|
117
49
|
|
|
118
50
|
def snapshot(task_id: str = "vault-snapshot") -> Dict[str, Any]:
|
|
119
|
-
"""
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
51
|
+
"""Create a vault snapshot."""
|
|
52
|
+
_ensure_dir()
|
|
53
|
+
ts = datetime.now(timezone.utc)
|
|
54
|
+
snap_id = f"snap-{ts.strftime('%Y%m%d_%H%M%S')}"
|
|
55
|
+
|
|
56
|
+
snapshot_data = {
|
|
57
|
+
"id": snap_id,
|
|
58
|
+
"task_id": task_id,
|
|
59
|
+
"label": snap_id,
|
|
60
|
+
"created_at": ts.isoformat(),
|
|
61
|
+
"entries_count": len(list((VAULT_DIR / "entries").glob("*.json"))),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
(VAULT_DIR / "snapshots" / f"{snap_id}.json").write_text(
|
|
65
|
+
json.dumps(snapshot_data, indent=2)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return {"snapshot_id": snap_id, "created_at": ts.isoformat()}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def health() -> Dict[str, Any]:
|
|
72
|
+
"""Check vault health."""
|
|
73
|
+
_ensure_dir()
|
|
74
|
+
|
|
75
|
+
entries_count = len(list((VAULT_DIR / "entries").glob("*.json")))
|
|
76
|
+
snapshots_count = len(list((VAULT_DIR / "snapshots").glob("*.json")))
|
|
77
|
+
total_size = sum(f.stat().st_size for f in VAULT_DIR.rglob("*") if f.is_file())
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
"status": "healthy",
|
|
81
|
+
"entries": entries_count,
|
|
82
|
+
"snapshots": snapshots_count,
|
|
83
|
+
"total_size_bytes": total_size,
|
|
84
|
+
"vault_path": str(VAULT_DIR),
|
|
85
|
+
}
|