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
@@ -1,94 +1,95 @@
1
1
  """
2
- Bridge to delimit-memory package.
3
- Tier 2 Platform tools semantic memory search and store.
2
+ Memory bridge — file-based semantic memory store.
3
+ Stores memories as JSON files in ~/.delimit/memory/.
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
10
+ from datetime import datetime, timezone
11
+ from typing import Any, Dict, List, Optional
13
12
 
14
13
  logger = logging.getLogger("delimit.ai.memory_bridge")
15
14
 
16
- MEM_PACKAGE = Path(os.environ.get("DELIMIT_HOME", str(Path.home() / ".delimit"))) / "server" / "packages" / "delimit-memory"
17
-
18
- _server = None
19
-
20
-
21
- def _run_async(coro):
22
- """Run an async coroutine from sync code, handling nested event loops."""
23
- try:
24
- loop = asyncio.get_running_loop()
25
- except RuntimeError:
26
- loop = None
27
-
28
- if loop and loop.is_running():
29
- # We're inside an async context (e.g., FastMCP) — use a new thread
30
- import concurrent.futures
31
- with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
32
- return pool.submit(asyncio.run, coro).result(timeout=30)
33
- else:
34
- return asyncio.run(coro)
35
-
36
-
37
- def _get_server():
38
- global _server
39
- if _server is not None:
40
- return _server
41
- pkg_path = str(MEM_PACKAGE / "delimit_memory")
42
- if pkg_path not in sys.path:
43
- sys.path.insert(0, pkg_path)
44
- if str(MEM_PACKAGE) not in sys.path:
45
- sys.path.insert(0, str(MEM_PACKAGE))
46
- try:
47
- from delimit_memory.server import DelimitMemoryServer
48
- _server = DelimitMemoryServer()
49
- _run_async(_server._initialize_engine())
50
- return _server
51
- except Exception as e:
52
- logger.warning(f"Failed to init memory server: {e}")
53
- return None
15
+ MEMORY_DIR = Path.home() / ".delimit" / "memory"
54
16
 
55
17
 
56
- def search(query: str, limit: int = 10) -> Dict[str, Any]:
57
- """Semantic search across conversation memory."""
58
- srv = _get_server()
59
- if srv is None:
60
- return {"error": "Memory server unavailable", "results": []}
61
- try:
62
- result = _run_async(srv._handle_search({"query": query, "limit": limit}))
63
- return json.loads(result) if isinstance(result, str) else result
64
- except Exception as e:
65
- return {"error": f"Memory search failed: {e}", "results": []}
18
+ def _ensure_dir():
19
+ MEMORY_DIR.mkdir(parents=True, exist_ok=True)
66
20
 
67
21
 
68
22
  def store(content: str, tags: Optional[list] = None, context: Optional[str] = None) -> Dict[str, Any]:
69
23
  """Store a memory entry."""
70
- srv = _get_server()
71
- if srv is None:
72
- return {"error": "Memory server unavailable"}
73
- try:
74
- args = {"content": content}
75
- if tags:
76
- args["tags"] = tags
77
- if context:
78
- args["context"] = context
79
- result = _run_async(srv._handle_store(args))
80
- return json.loads(result) if isinstance(result, str) else result
81
- except Exception as e:
82
- return {"error": f"Memory store failed: {e}"}
24
+ _ensure_dir()
25
+
26
+ # Generate ID from content hash
27
+ mem_id = "mem-" + hashlib.sha256(content[:100].encode()).hexdigest()[:12]
28
+ ts = datetime.now(timezone.utc).isoformat()
29
+
30
+ entry = {
31
+ "id": mem_id,
32
+ "content": content,
33
+ "tags": tags or [],
34
+ "context": context or "",
35
+ "created_at": ts,
36
+ }
37
+
38
+ path = MEMORY_DIR / f"{mem_id}.json"
39
+ path.write_text(json.dumps(entry, indent=2))
40
+
41
+ return {"stored": mem_id, "path": str(path), "created_at": ts}
42
+
43
+
44
+ def search(query: str, limit: int = 10) -> Dict[str, Any]:
45
+ """Search memories by keyword matching."""
46
+ _ensure_dir()
47
+ query_lower = query.lower()
48
+ results = []
49
+
50
+ for f in sorted(MEMORY_DIR.glob("*.json"), reverse=True):
51
+ try:
52
+ entry = json.loads(f.read_text())
53
+ content = entry.get("content", "").lower()
54
+ tags = " ".join(entry.get("tags", [])).lower()
55
+ context = entry.get("context", "").lower()
56
+
57
+ # Simple keyword matching
58
+ if query_lower in content or query_lower in tags or query_lower in context:
59
+ results.append({
60
+ "id": entry.get("id", f.stem),
61
+ "content": entry.get("content", "")[:500],
62
+ "tags": entry.get("tags", []),
63
+ "created_at": entry.get("created_at", ""),
64
+ "relevance": content.count(query_lower),
65
+ })
66
+
67
+ if len(results) >= limit:
68
+ break
69
+ except Exception:
70
+ pass
71
+
72
+ results.sort(key=lambda r: r.get("relevance", 0), reverse=True)
73
+ return {"query": query, "results": results, "count": len(results)}
83
74
 
84
75
 
85
76
  def get_recent(limit: int = 5) -> Dict[str, Any]:
86
- """Get recent work summary."""
87
- srv = _get_server()
88
- if srv is None:
89
- return {"error": "Memory server unavailable", "results": []}
90
- try:
91
- result = _run_async(srv._handle_get_recent_work({"limit": limit}))
92
- return json.loads(result) if isinstance(result, str) else result
93
- except Exception as e:
94
- return {"error": f"Recent work failed: {e}", "results": []}
77
+ """Get recent memory entries."""
78
+ _ensure_dir()
79
+ entries = []
80
+
81
+ for f in sorted(MEMORY_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
82
+ if len(entries) >= limit:
83
+ break
84
+ try:
85
+ entry = json.loads(f.read_text())
86
+ entries.append({
87
+ "id": entry.get("id", f.stem),
88
+ "content": entry.get("content", "")[:500],
89
+ "tags": entry.get("tags", []),
90
+ "created_at": entry.get("created_at", ""),
91
+ })
92
+ except Exception:
93
+ pass
94
+
95
+ return {"results": entries, "count": len(entries)}
@@ -48,33 +48,72 @@ def _call(pkg: str, factory_name: str, method: str, args: Dict, tool_label: str)
48
48
  # ─── ReleasePilot (Governance Primitive) ────────────────────────────────
49
49
 
50
50
  def release_plan(environment: str, version: str, repository: str, services: Optional[List[str]] = None) -> Dict[str, Any]:
51
+ """Generate a release plan for the given environment and version."""
51
52
  return _call("releasepilot", "create_releasepilot_server", "_tool_plan",
52
53
  {"environment": environment, "version": version, "repository": repository, "services": services or []}, "release.plan")
53
54
 
54
55
 
55
56
  def release_validate(environment: str, version: str) -> Dict[str, Any]:
56
- return _call("releasepilot", "create_releasepilot_server", "_tool_validate",
57
- {"environment": environment, "version": version}, "release.validate")
57
+ """Validate release readiness by checking git tags, CHANGELOG, and package.json."""
58
+ import subprocess
59
+ checks = []
60
+ try:
61
+ tags = subprocess.run(["git", "tag", "-l"], capture_output=True, text=True, timeout=10)
62
+ tag_list = tags.stdout.strip().splitlines() if tags.returncode == 0 else []
63
+ has_tag = any(version in t for t in tag_list)
64
+ checks.append({"check": "git_tag", "passed": has_tag, "detail": f"Tag for {version} {'found' if has_tag else 'not found'}"})
65
+ except Exception:
66
+ checks.append({"check": "git_tag", "passed": False, "detail": "git not available"})
67
+ cl = Path("CHANGELOG.md")
68
+ checks.append({"check": "changelog", "passed": cl.exists(), "detail": str(cl) if cl.exists() else "CHANGELOG.md not found"})
69
+ pkg = Path("package.json")
70
+ if pkg.exists():
71
+ try:
72
+ data = json.loads(pkg.read_text())
73
+ pkg_ver = data.get("version", "")
74
+ match = pkg_ver == version.lstrip("v")
75
+ checks.append({"check": "package_version", "passed": match, "detail": f"package.json version={pkg_ver}"})
76
+ except Exception:
77
+ checks.append({"check": "package_version", "passed": False, "detail": "Failed to read package.json"})
78
+ passed = all(c["passed"] for c in checks)
79
+ return {"tool": "release.validate", "status": "pass" if passed else "fail", "environment": environment, "version": version, "checks": checks}
58
80
 
59
81
 
60
82
  def release_status(environment: str) -> Dict[str, Any]:
83
+ """Get current release status for the environment."""
61
84
  return _call("releasepilot", "create_releasepilot_server", "_tool_status",
62
85
  {"environment": environment}, "release.status")
63
86
 
64
87
 
65
88
  def release_rollback(environment: str, version: str, to_version: str) -> Dict[str, Any]:
89
+ """Roll back to a previous version in the specified environment."""
66
90
  return _call("releasepilot", "create_releasepilot_server", "_tool_rollback",
67
91
  {"environment": environment, "version": version, "to_version": to_version}, "release.rollback")
68
92
 
69
93
 
70
94
  def release_history(environment: str, limit: int = 10) -> Dict[str, Any]:
71
- return _call("releasepilot", "create_releasepilot_server", "_tool_history",
72
- {"environment": environment, "limit": limit}, "release.history")
95
+ """Show recent release history from git log."""
96
+ import subprocess
97
+ try:
98
+ result = subprocess.run(
99
+ ["git", "log", "--oneline", "--decorate", f"-{limit}"],
100
+ capture_output=True, text=True, timeout=10,
101
+ )
102
+ if result.returncode != 0:
103
+ return {"tool": "release.history", "status": "error", "error": result.stderr.strip()}
104
+ commits = result.stdout.strip().splitlines()
105
+ tags = subprocess.run(["git", "tag", "-l", "--sort=-creatordate"], capture_output=True, text=True, timeout=10)
106
+ tag_list = tags.stdout.strip().splitlines()[:limit] if tags.returncode == 0 else []
107
+ return {"tool": "release.history", "status": "ok", "environment": environment,
108
+ "recent_commits": commits, "tags": tag_list, "total_commits": len(commits)}
109
+ except Exception as e:
110
+ return {"tool": "release.history", "status": "error", "error": str(e)}
73
111
 
74
112
 
75
113
  # ─── CostGuard (Governance Primitive) ──────────────────────────────────
76
114
 
77
115
  def cost_analyze(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
116
+ """Analyze cloud infrastructure costs for the target path."""
78
117
  result = _call("costguard", "create_costguard_server", "_tool_analyze",
79
118
  {"target": target, **(options or {})}, "cost.analyze")
80
119
  # Guard against hardcoded fake AWS cost data from stub implementation
@@ -85,11 +124,13 @@ def cost_analyze(target: str = ".", options: Optional[Dict] = None) -> Dict[str,
85
124
 
86
125
 
87
126
  def cost_optimize(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
127
+ """Identify cost optimization opportunities for the target infrastructure."""
88
128
  return _call("costguard", "create_costguard_server", "_tool_optimize",
89
129
  {"target": target, **(options or {})}, "cost.optimize")
90
130
 
91
131
 
92
132
  def cost_alert(action: str = "list", options: Optional[Dict] = None) -> Dict[str, Any]:
133
+ """Manage cost alerts (list, create, delete)."""
93
134
  return _call("costguard", "create_costguard_server", "_tool_alerts",
94
135
  {"action": action, **(options or {})}, "cost.alert")
95
136
 
@@ -97,6 +138,7 @@ def cost_alert(action: str = "list", options: Optional[Dict] = None) -> Dict[str
97
138
  # ─── DataSteward (Governance Primitive) ────────────────────────────────
98
139
 
99
140
  def data_validate(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
141
+ """Run data integrity and validation checks on the target database."""
100
142
  result = _call("datasteward", "create_datasteward_server", "_tool_integrity_check",
101
143
  {"database_url": target, **(options or {})}, "data.validate")
102
144
  # Guard against stub that returns "passed" with 0 tables checked
@@ -107,11 +149,13 @@ def data_validate(target: str = ".", options: Optional[Dict] = None) -> Dict[str
107
149
 
108
150
 
109
151
  def data_migrate(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
152
+ """Check for pending database migrations and reporting status."""
110
153
  return _call("datasteward", "create_datasteward_server", "_tool_migration_status",
111
154
  {"database_url": target, **(options or {})}, "data.migrate")
112
155
 
113
156
 
114
157
  def data_backup(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
158
+ """Create a backup plan or execute backup for the target database."""
115
159
  return _call("datasteward", "create_datasteward_server", "_tool_backup_plan",
116
160
  {"database_url": target, **(options or {})}, "data.backup")
117
161
 
@@ -119,20 +163,46 @@ def data_backup(target: str = ".", options: Optional[Dict] = None) -> Dict[str,
119
163
  # ─── ObservabilityOps (Internal OS) ────────────────────────────────────
120
164
 
121
165
  def obs_metrics(query: str, time_range: str = "1h", source: Optional[str] = None) -> Dict[str, Any]:
166
+ """Query live system metrics (CPU, memory, disk, network)."""
122
167
  return _call("observabilityops", "create_observabilityops_server", "_tool_metrics",
123
168
  {"query": query, "time_range": time_range, "source": source}, "obs.metrics")
124
169
 
125
170
 
126
171
  def obs_logs(query: str, time_range: str = "1h", source: Optional[str] = None) -> Dict[str, Any]:
172
+ """Search and retrieve system or application logs."""
127
173
  return _call("observabilityops", "create_observabilityops_server", "_tool_logs",
128
174
  {"query": query, "time_range": time_range, "source": source}, "obs.logs")
129
175
 
130
176
 
131
177
  def obs_alerts(action: str, alert_rule: Optional[Dict] = None, rule_id: Optional[str] = None) -> Dict[str, Any]:
132
- return _call("observabilityops", "create_observabilityops_server", "_tool_alerts",
133
- {"action": action, "alert_rule": alert_rule, "rule_id": rule_id}, "obs.alerts")
178
+ """File-based alert management using ~/.delimit/alerts/."""
179
+ alerts_dir = Path(os.environ.get("DELIMIT_HOME", str(Path.home() / ".delimit"))) / "alerts"
180
+ alerts_dir.mkdir(parents=True, exist_ok=True)
181
+ if action == "list":
182
+ alerts = []
183
+ for fp in sorted(alerts_dir.glob("*.json")):
184
+ try:
185
+ alerts.append(json.loads(fp.read_text()))
186
+ except Exception:
187
+ pass
188
+ return {"tool": "obs.alerts", "status": "ok", "action": "list", "alerts": alerts, "total": len(alerts)}
189
+ elif action == "create" and alert_rule:
190
+ import time as _time
191
+ aid = f"alert-{int(_time.time())}"
192
+ alert_rule["id"] = aid
193
+ alert_rule["created_at"] = _time.time()
194
+ (alerts_dir / f"{aid}.json").write_text(json.dumps(alert_rule, indent=2))
195
+ return {"tool": "obs.alerts", "status": "created", "alert": alert_rule}
196
+ elif action == "delete" and rule_id:
197
+ fp = alerts_dir / f"{rule_id}.json"
198
+ if fp.exists():
199
+ fp.unlink()
200
+ return {"tool": "obs.alerts", "status": "deleted", "rule_id": rule_id}
201
+ return {"tool": "obs.alerts", "status": "not_found", "rule_id": rule_id}
202
+ return {"tool": "obs.alerts", "status": "unknown_action", "action": action}
134
203
 
135
204
 
136
205
  def obs_status() -> Dict[str, Any]:
206
+ """Get overall system health and observability status."""
137
207
  return _call("observabilityops", "create_observabilityops_server", "_tool_status",
138
208
  {}, "obs.status")
@@ -22,14 +22,30 @@ _NOT_INIT_MSG = (
22
22
  "or run the delimit_init tool with your project path."
23
23
  )
24
24
 
25
+ _DEPENDENCY_MSG = "delimit-os backend is not installed or not available in this environment."
26
+
27
+
28
+ def _is_initialized(path: str = ".") -> bool:
29
+ """A project is initialized if .delimit/policies.yml exists."""
30
+ return (Path(path).resolve() / ".delimit" / "policies.yml").is_file()
31
+
25
32
 
26
33
  def _ensure_os_path():
27
34
  if str(OS_PACKAGE) not in sys.path:
28
35
  sys.path.insert(0, str(OS_PACKAGE))
29
36
 
30
37
 
38
+ def _backend_unavailable(path: Optional[str] = None) -> Dict[str, Any]:
39
+ """Return a truthful error for missing OS backend support."""
40
+ if path and not _is_initialized(path):
41
+ return {"error": _NOT_INIT_MSG, "fallback": True}
42
+ return {"error": _DEPENDENCY_MSG, "fallback": True}
43
+
44
+
31
45
  def create_plan(operation: str, target: str, parameters: Optional[Dict] = None, require_approval: bool = True) -> Dict[str, Any]:
32
46
  """Create an execution plan via delimit-os."""
47
+ if not OS_PACKAGE.exists():
48
+ return _backend_unavailable(target)
33
49
  _ensure_os_path()
34
50
  try:
35
51
  from server import PLANS
@@ -54,11 +70,13 @@ def create_plan(operation: str, target: str, parameters: Optional[Dict] = None,
54
70
  PLANS[plan_id] = plan
55
71
  return plan
56
72
  except ImportError:
57
- return {"error": _NOT_INIT_MSG, "fallback": True}
73
+ return _backend_unavailable(target)
58
74
 
59
75
 
60
76
  def get_status() -> Dict[str, Any]:
61
77
  """Get current OS status."""
78
+ if not OS_PACKAGE.exists():
79
+ return {"status": "unavailable", "error": _DEPENDENCY_MSG}
62
80
  _ensure_os_path()
63
81
  try:
64
82
  from server import PLANS, TASKS, TOKENS
@@ -69,11 +87,13 @@ def get_status() -> Dict[str, Any]:
69
87
  "tokens": len(TOKENS),
70
88
  }
71
89
  except ImportError:
72
- return {"status": "unavailable", "error": _NOT_INIT_MSG}
90
+ return {"status": "unavailable", "error": _DEPENDENCY_MSG}
73
91
 
74
92
 
75
93
  def check_gates(plan_id: str) -> Dict[str, Any]:
76
94
  """Check governance gates for a plan."""
95
+ if not OS_PACKAGE.exists():
96
+ return {"error": _DEPENDENCY_MSG}
77
97
  _ensure_os_path()
78
98
  try:
79
99
  from server import PLANS
@@ -86,4 +106,4 @@ def check_gates(plan_id: str) -> Dict[str, Any]:
86
106
  "status": plan.get("status"),
87
107
  }
88
108
  except ImportError:
89
- return {"error": _NOT_INIT_MSG}
109
+ return {"error": _DEPENDENCY_MSG}
@@ -49,42 +49,143 @@ def _call(pkg: str, factory_name: str, method: str, args: Dict, tool_label: str)
49
49
  # ─── RepoDoctor ────────────────────────────────────────────────────────
50
50
 
51
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")
52
+ """Check for common repository issues."""
53
+ import subprocess
54
+ root = Path(target).resolve()
55
+ issues = []
56
+ if not (root / ".gitignore").exists():
57
+ issues.append({"severity": "warning", "issue": "No .gitignore file found"})
58
+ if not any((root / d).exists() for d in ["tests", "test", "__tests__", "spec"]):
59
+ issues.append({"severity": "warning", "issue": "No test directory found"})
60
+ if not any((root / f).exists() for f in [".github/workflows", ".gitlab-ci.yml", "Jenkinsfile", ".circleci"]):
61
+ issues.append({"severity": "info", "issue": "No CI configuration detected"})
62
+ # Check for large files
63
+ try:
64
+ result = subprocess.run(["git", "-C", str(root), "ls-files"], capture_output=True, text=True, timeout=10)
65
+ if result.returncode == 0:
66
+ for f in result.stdout.strip().splitlines():
67
+ fp = root / f
68
+ if fp.exists() and fp.stat().st_size > 5_000_000:
69
+ issues.append({"severity": "warning", "issue": f"Large file ({fp.stat().st_size // 1_000_000}MB): {f}"})
70
+ except Exception:
71
+ pass
72
+ status = "healthy" if not issues else ("warning" if all(i["severity"] != "error" for i in issues) else "unhealthy")
73
+ return {"tool": "repo.diagnose", "status": status, "target": str(root), "issues": issues, "total_issues": len(issues)}
54
74
 
55
75
 
56
76
  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")
77
+ """Analyze repository structure: file counts by type, configs, health."""
78
+ root = Path(target).resolve()
79
+ skip = {"node_modules", "dist", ".next", ".git", "__pycache__", "build", ".cache", "venv", ".venv"}
80
+ ext_counts: Dict[str, int] = {}
81
+ total = 0
82
+ for dirpath, dirnames, filenames in os.walk(root):
83
+ dirnames[:] = [d for d in dirnames if d not in skip]
84
+ for f in filenames:
85
+ ext = Path(f).suffix or "(no ext)"
86
+ ext_counts[ext] = ext_counts.get(ext, 0) + 1
87
+ total += 1
88
+ top = sorted(ext_counts.items(), key=lambda x: -x[1])[:15]
89
+ configs = {c: (root / c).exists() for c in [
90
+ ".gitignore", "package.json", "pyproject.toml", "Makefile", "Dockerfile",
91
+ "tsconfig.json", ".eslintrc.json", "jest.config.js", "pytest.ini", "setup.py",
92
+ ]}
93
+ has_tests = any((root / d).exists() for d in ["tests", "test", "__tests__", "spec"])
94
+ has_ci = any((root / f).exists() for f in [".github/workflows", ".gitlab-ci.yml", "Jenkinsfile"])
95
+ return {"tool": "repo.analyze", "status": "ok", "target": str(root), "total_files": total,
96
+ "file_types": dict(top), "configs_found": {k: v for k, v in configs.items() if v},
97
+ "has_tests": has_tests, "has_ci": has_ci}
59
98
 
60
99
 
61
100
  # ─── ConfigSentry ───────────────────────────────────────────────────────
62
101
 
63
102
  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")
103
+ """Validate JSON/YAML config files parse correctly."""
104
+ root = Path(target).resolve()
105
+ results = []
106
+ for ext, loader in [(".json", "json"), (".yaml", "yaml"), (".yml", "yaml")]:
107
+ for fp in root.glob(f"*{ext}"):
108
+ if fp.name.startswith(".") and ext == ".json" and "lock" in fp.name:
109
+ continue
110
+ try:
111
+ text = fp.read_text()
112
+ if loader == "json":
113
+ json.loads(text)
114
+ else:
115
+ try:
116
+ import yaml as _yaml
117
+ _yaml.safe_load(text)
118
+ except ImportError:
119
+ pass # skip YAML validation if pyyaml not installed
120
+ results.append({"file": fp.name, "valid": True})
121
+ except Exception as e:
122
+ results.append({"file": fp.name, "valid": False, "error": str(e)[:200]})
123
+ valid = sum(1 for r in results if r["valid"])
124
+ invalid = sum(1 for r in results if not r["valid"])
125
+ return {"tool": "config.validate", "status": "ok" if invalid == 0 else "issues_found",
126
+ "target": str(root), "files_checked": len(results), "valid": valid, "invalid": invalid, "details": results}
66
127
 
67
128
 
68
129
  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")
130
+ """Audit config files for security issues and staleness."""
131
+ root = Path(target).resolve()
132
+ findings = []
133
+ # Check for secrets in config files
134
+ secret_patterns = ["password", "secret", "api_key", "apikey", "token", "private_key"]
135
+ for ext in [".json", ".yaml", ".yml", ".env", ".toml", ".ini", ".cfg"]:
136
+ for fp in root.glob(f"*{ext}"):
137
+ try:
138
+ text = fp.read_text().lower()
139
+ for pat in secret_patterns:
140
+ if pat in text and fp.name != ".gitignore":
141
+ findings.append({"file": fp.name, "severity": "warning",
142
+ "issue": f"Possible secret pattern '{pat}' found"})
143
+ break
144
+ except Exception:
145
+ pass
146
+ # Check .env not in .gitignore
147
+ gitignore = root / ".gitignore"
148
+ if (root / ".env").exists() and gitignore.exists():
149
+ gi_text = gitignore.read_text()
150
+ if ".env" not in gi_text:
151
+ findings.append({"file": ".env", "severity": "error", "issue": ".env exists but not in .gitignore"})
152
+ elif (root / ".env").exists() and not gitignore.exists():
153
+ findings.append({"file": ".env", "severity": "error", "issue": ".env exists with no .gitignore"})
154
+ return {"tool": "config.audit", "status": "ok" if not findings else "issues_found",
155
+ "target": str(root), "findings": findings, "total_findings": len(findings)}
71
156
 
72
157
 
73
158
  # ─── EvidencePack ───────────────────────────────────────────────────────
74
159
 
75
160
  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
161
+ """Collect project evidence: git log, test files, configs, governance data."""
162
+ import subprocess, time as _time
163
+ root = Path(target).resolve()
164
+ evidence: Dict[str, Any] = {"collected_at": _time.time(), "target": str(root)}
165
+ # Git log
166
+ try:
167
+ r = subprocess.run(["git", "-C", str(root), "log", "--oneline", "-10"], capture_output=True, text=True, timeout=10)
168
+ evidence["git_log"] = r.stdout.strip().splitlines() if r.returncode == 0 else []
169
+ except Exception:
170
+ evidence["git_log"] = []
171
+ # Test files
172
+ test_dirs = [d for d in ["tests", "test", "__tests__", "spec"] if (root / d).exists()]
173
+ evidence["test_directories"] = test_dirs
174
+ # Configs
175
+ evidence["configs"] = [f.name for f in root.iterdir() if f.is_file() and (f.suffix in [".json", ".yaml", ".yml", ".toml"] or f.name.startswith("."))]
176
+ # Save bundle
177
+ ev_dir = Path(os.environ.get("DELIMIT_HOME", str(Path.home() / ".delimit"))) / "evidence"
178
+ ev_dir.mkdir(parents=True, exist_ok=True)
179
+ bundle_id = f"ev-{int(_time.time())}"
180
+ bundle_path = ev_dir / f"{bundle_id}.json"
181
+ evidence["bundle_id"] = bundle_id
182
+ bundle_path.write_text(json.dumps(evidence, indent=2))
183
+ return {"tool": "evidence.collect", "status": "ok", "bundle_id": bundle_id,
184
+ "bundle_path": str(bundle_path), "summary": {k: len(v) if isinstance(v, list) else v for k, v in evidence.items()}}
85
185
 
86
186
 
87
187
  def evidence_verify(bundle_id: Optional[str] = None, bundle_path: Optional[str] = None, options: Optional[Dict] = None) -> Dict[str, Any]:
188
+ """Verify the integrity and authenticity of a collected evidence bundle."""
88
189
  args = {**(options or {})}
89
190
  if bundle_id:
90
191
  args["bundle_id"] = bundle_id
@@ -92,6 +193,16 @@ def evidence_verify(bundle_id: Optional[str] = None, bundle_path: Optional[str]
92
193
  args["bundle_path"] = bundle_path
93
194
  if not bundle_id and not bundle_path:
94
195
  return {"tool": "evidence.verify", "status": "no_input", "message": "Provide bundle_id or bundle_path to verify"}
196
+ try:
197
+ importlib.import_module("evidencepack.server")
198
+ except (ImportError, ModuleNotFoundError):
199
+ return {
200
+ "tool": "evidence.verify",
201
+ "status": "not_available",
202
+ "error": "evidencepack backend is not installed or not available in this environment.",
203
+ "hint": "Install evidencepack to enable evidence bundle verification.",
204
+ **args,
205
+ }
95
206
  return _call("evidencepack", "create_evidencepack_server", "_tool_validate",
96
207
  args, "evidence.verify")
97
208
 
@@ -101,7 +212,29 @@ def evidence_verify(bundle_id: Optional[str] = None, bundle_path: Optional[str]
101
212
  _INTERNAL_TOKEN = os.environ.get("DELIMIT_INTERNAL_BRIDGE_TOKEN", "delimit-internal-bridge")
102
213
 
103
214
 
215
+ def _fallback_security_result(target: str, tool_label: str) -> Dict[str, Any]:
216
+ """Run the built-in local security audit when securitygate is unavailable."""
217
+ from .tools_infra import security_audit as local_security_audit
218
+
219
+ result = local_security_audit(target=target)
220
+ result["tool"] = tool_label
221
+ result.setdefault("status", "ok" if "error" not in result else "error")
222
+ result["fallback"] = True
223
+ result["hint"] = (
224
+ "Running basic security audit (free fallback). "
225
+ "For enhanced scanning with CVE detection, install securitygate: "
226
+ "pip install securitygate"
227
+ )
228
+ return result
229
+
230
+
104
231
  def security_scan(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
232
+ """Perform an enhanced security scan for CVEs and vulnerabilities."""
233
+ try:
234
+ importlib.import_module("securitygate.server")
235
+ except (ImportError, ModuleNotFoundError):
236
+ logger.warning("securitygate module not found, falling back to local security audit")
237
+ return _fallback_security_result(target=target, tool_label="security.scan")
105
238
  result = _call("securitygate", "create_securitygate_server", "_tool_scan",
106
239
  {"target": target, "authorization_token": _INTERNAL_TOKEN, **(options or {})}, "security.scan")
107
240
  # Guard against fabricated/hardcoded CVE data from stub implementations
@@ -113,5 +246,11 @@ def security_scan(target: str = ".", options: Optional[Dict] = None) -> Dict[str
113
246
 
114
247
 
115
248
  def security_audit(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
249
+ """Audit source code for dangerous patterns and hardcoded secrets."""
250
+ try:
251
+ importlib.import_module("securitygate.server")
252
+ except (ImportError, ModuleNotFoundError):
253
+ logger.warning("securitygate module not found, using built-in security audit")
254
+ return _fallback_security_result(target=target, tool_label="security.audit")
116
255
  return _call("securitygate", "create_securitygate_server", "_tool_audit",
117
256
  {"target": target, "authorization_token": _INTERNAL_TOKEN, **(options or {})}, "security.audit")