delimit-cli 3.14.28 → 3.14.29

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 (47) hide show
  1. package/gateway/ai/backends/deploy_bridge.py +56 -2
  2. package/gateway/ai/backends/gateway_core.py +212 -1
  3. package/gateway/ai/backends/generate_bridge.py +84 -13
  4. package/gateway/ai/backends/governance_bridge.py +63 -16
  5. package/gateway/ai/backends/memory_bridge.py +77 -76
  6. package/gateway/ai/backends/ops_bridge.py +76 -6
  7. package/gateway/ai/backends/os_bridge.py +23 -3
  8. package/gateway/ai/backends/repo_bridge.py +156 -17
  9. package/gateway/ai/backends/tools_design.py +116 -9
  10. package/gateway/ai/backends/tools_infra.py +200 -72
  11. package/gateway/ai/backends/tools_real.py +8 -0
  12. package/gateway/ai/backends/ui_bridge.py +115 -5
  13. package/gateway/ai/backends/vault_bridge.py +69 -114
  14. package/gateway/ai/content_engine.py +1276 -0
  15. package/gateway/ai/context_fs.py +193 -0
  16. package/gateway/ai/daemon.py +500 -0
  17. package/gateway/ai/data_plane.py +291 -0
  18. package/gateway/ai/deliberation.py +1033 -6
  19. package/gateway/ai/events.py +39 -0
  20. package/gateway/ai/founding_users.py +162 -0
  21. package/gateway/ai/governance.py +698 -4
  22. package/gateway/ai/inbox_daemon.py +78 -17
  23. package/gateway/ai/integrations/__init__.py +1 -0
  24. package/gateway/ai/integrations/opensage_wrapper.py +288 -0
  25. package/gateway/ai/key_resolver.py +95 -0
  26. package/gateway/ai/ledger_manager.py +289 -1
  27. package/gateway/ai/license.py +62 -4
  28. package/gateway/ai/license_core.py +208 -7
  29. package/gateway/ai/local_server.py +215 -0
  30. package/gateway/ai/loop_engine.py +408 -0
  31. package/gateway/ai/mcp_bridge.py +178 -0
  32. package/gateway/ai/release_sync.py +2 -2
  33. package/gateway/ai/screen_record.py +374 -0
  34. package/gateway/ai/secrets_broker.py +235 -0
  35. package/gateway/ai/social.py +189 -27
  36. package/gateway/ai/social_target.py +1635 -0
  37. package/gateway/ai/supabase_sync.py +190 -0
  38. package/gateway/ai/tracing.py +195 -0
  39. package/gateway/core/contract_ledger.py +1 -1
  40. package/gateway/core/dependency_graph.py +1 -1
  41. package/gateway/core/dependency_manifest.py +1 -1
  42. package/gateway/core/diff_engine_v2.py +272 -78
  43. package/gateway/core/event_backbone.py +2 -2
  44. package/gateway/core/event_schema.py +1 -1
  45. package/gateway/core/impact_analyzer.py +1 -1
  46. package/gateway/core/policy_engine.py +4 -0
  47. 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
- """Story build remains a stub requires Storybook installed."""
61
- return {"tool": "story.build", "project_path": project_path, "status": "not_available",
62
- "message": "Storybook build requires Storybook installed. Run: npx storybook init"}
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
- return _call("testsmith", "create_testsmith_server", "_tool_coverage",
79
- {"project_path": project_path, "threshold": threshold}, "test.coverage")
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
- Bridge to delimit-vault package.
3
- Tier 2 Platform tools artifact and credential storage.
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 asyncio
7
+ import hashlib
10
8
  import logging
11
9
  from pathlib import Path
12
- from typing import Any, Dict, Optional
13
- from .async_utils import run_async
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
- VAULT_PACKAGE = Path(os.environ.get("DELIMIT_HOME", str(Path.home() / ".delimit"))) / "server" / "packages" / "delimit-vault"
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 search(query: str) -> Dict[str, Any]:
89
- """Search vault entries."""
90
- srv = _get_server()
91
- if srv is None:
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 health() -> Dict[str, Any]:
104
- """Check vault health."""
105
- srv = _get_server()
106
- if srv is None:
107
- return {"status": "unavailable", "error": "Vault server not initialized"}
108
- try:
109
- async def _do():
110
- await _ensure_initialized(srv)
111
- return await srv._handle_health()
112
- result = run_async(_do())
113
- return _extract_text(result)
114
- except Exception as e:
115
- return {"status": "unavailable", "error": str(e)}
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
- """Get vault snapshot."""
120
- srv = _get_server()
121
- if srv is None:
122
- return {"error": "Vault server unavailable"}
123
- try:
124
- async def _do():
125
- await _ensure_initialized(srv)
126
- return await srv._handle_snapshot({"task_id": task_id})
127
- result = run_async(_do())
128
- return _extract_text(result)
129
- except Exception as e:
130
- return {"error": f"Vault snapshot failed: {e}"}
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
+ }