delimit-cli 3.9.2 → 3.9.4
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 +4 -3
- package/gateway/ai/license.py +44 -129
- package/gateway/ai/license_core.py +196 -0
- package/gateway/ai/server.py +43 -6
- package/package.json +1 -1
package/bin/delimit-setup.js
CHANGED
|
@@ -431,9 +431,10 @@ function getClaudeMdContent() {
|
|
|
431
431
|
One workspace for every AI coding assistant.
|
|
432
432
|
|
|
433
433
|
## On every session start:
|
|
434
|
-
1. Call \`
|
|
435
|
-
2.
|
|
436
|
-
3. If
|
|
434
|
+
1. Call \`delimit_gov_health\` to show governance status
|
|
435
|
+
2. Call \`delimit_ledger_context\` to check for open tasks
|
|
436
|
+
3. If returning user, summarize what's pending
|
|
437
|
+
4. If new project, call \`delimit_scan\` to discover what Delimit can do
|
|
437
438
|
|
|
438
439
|
## Try these:
|
|
439
440
|
- "scan this project" -- discover what Delimit can do here
|
package/gateway/ai/license.py
CHANGED
|
@@ -1,133 +1,48 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Delimit license
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
Delimit license — thin shim.
|
|
3
|
+
The enforcement logic is in license_core (shipped as compiled binary).
|
|
4
|
+
This shim handles imports and provides fallback error messages.
|
|
5
5
|
"""
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"status": "premium_required",
|
|
42
|
-
"tool": tool_name,
|
|
43
|
-
"current_tier": get_license().get("tier", "free"),
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def activate_license(key: str) -> dict:
|
|
48
|
-
"""Activate a license key via Lemon Squeezy API.
|
|
49
|
-
Falls back to local validation if API is unreachable."""
|
|
50
|
-
if not key or len(key) < 10:
|
|
51
|
-
return {"error": "Invalid license key format"}
|
|
52
|
-
|
|
53
|
-
machine_hash = hashlib.sha256(str(Path.home()).encode()).hexdigest()[:16]
|
|
54
|
-
|
|
55
|
-
# Try Lemon Squeezy remote validation
|
|
56
|
-
try:
|
|
57
|
-
import urllib.request
|
|
58
|
-
import urllib.error
|
|
59
|
-
|
|
60
|
-
data = json.dumps({
|
|
61
|
-
"license_key": key,
|
|
62
|
-
"instance_name": machine_hash,
|
|
63
|
-
}).encode()
|
|
64
|
-
|
|
65
|
-
req = urllib.request.Request(
|
|
66
|
-
LS_VALIDATE_URL,
|
|
67
|
-
data=data,
|
|
68
|
-
headers={
|
|
69
|
-
"Content-Type": "application/json",
|
|
70
|
-
"Accept": "application/json",
|
|
71
|
-
},
|
|
72
|
-
method="POST",
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
76
|
-
result = json.loads(resp.read())
|
|
77
|
-
|
|
78
|
-
if result.get("valid"):
|
|
79
|
-
license_data = {
|
|
80
|
-
"key": key,
|
|
81
|
-
"tier": "pro",
|
|
82
|
-
"valid": True,
|
|
83
|
-
"activated_at": time.time(),
|
|
84
|
-
"machine_hash": machine_hash,
|
|
85
|
-
"instance_id": result.get("instance", {}).get("id"),
|
|
86
|
-
"license_id": result.get("license_key", {}).get("id"),
|
|
87
|
-
"customer_name": result.get("meta", {}).get("customer_name", ""),
|
|
88
|
-
"validated_via": "lemon_squeezy",
|
|
89
|
-
}
|
|
90
|
-
LICENSE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
-
LICENSE_FILE.write_text(json.dumps(license_data, indent=2))
|
|
92
|
-
return {"status": "activated", "tier": "pro", "message": "License activated successfully."}
|
|
93
|
-
else:
|
|
94
|
-
return {
|
|
95
|
-
"error": "Invalid license key. Check your key and try again.",
|
|
96
|
-
"status": "invalid",
|
|
97
|
-
"detail": result.get("error", ""),
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
except (urllib.error.URLError, OSError):
|
|
101
|
-
# API unreachable — accept key locally (offline activation)
|
|
102
|
-
license_data = {
|
|
103
|
-
"key": key,
|
|
104
|
-
"tier": "pro",
|
|
105
|
-
"valid": True,
|
|
106
|
-
"activated_at": time.time(),
|
|
107
|
-
"machine_hash": machine_hash,
|
|
108
|
-
"validated_via": "offline",
|
|
109
|
-
}
|
|
110
|
-
LICENSE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
-
LICENSE_FILE.write_text(json.dumps(license_data, indent=2))
|
|
6
|
+
try:
|
|
7
|
+
from ai.license_core import (
|
|
8
|
+
load_license as get_license,
|
|
9
|
+
check_premium as is_premium,
|
|
10
|
+
gate_tool as require_premium,
|
|
11
|
+
activate as activate_license,
|
|
12
|
+
PRO_TOOLS,
|
|
13
|
+
FREE_TRIAL_LIMITS,
|
|
14
|
+
)
|
|
15
|
+
except ImportError:
|
|
16
|
+
# license_core not available (development mode or missing binary)
|
|
17
|
+
import json
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
LICENSE_FILE = Path.home() / ".delimit" / "license.json"
|
|
22
|
+
|
|
23
|
+
PRO_TOOLS = set()
|
|
24
|
+
FREE_TRIAL_LIMITS = {}
|
|
25
|
+
|
|
26
|
+
def get_license() -> dict:
|
|
27
|
+
if not LICENSE_FILE.exists():
|
|
28
|
+
return {"tier": "free", "valid": True}
|
|
29
|
+
try:
|
|
30
|
+
return json.loads(LICENSE_FILE.read_text())
|
|
31
|
+
except Exception:
|
|
32
|
+
return {"tier": "free", "valid": True}
|
|
33
|
+
|
|
34
|
+
def is_premium() -> bool:
|
|
35
|
+
lic = get_license()
|
|
36
|
+
return lic.get("tier") in ("pro", "enterprise") and lic.get("valid", False)
|
|
37
|
+
|
|
38
|
+
def require_premium(tool_name: str) -> dict | None:
|
|
39
|
+
if is_premium():
|
|
40
|
+
return None
|
|
112
41
|
return {
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
}
|
|
117
|
-
except Exception as e:
|
|
118
|
-
# Unexpected error — still activate locally
|
|
119
|
-
license_data = {
|
|
120
|
-
"key": key,
|
|
121
|
-
"tier": "pro",
|
|
122
|
-
"valid": True,
|
|
123
|
-
"activated_at": time.time(),
|
|
124
|
-
"machine_hash": machine_hash,
|
|
125
|
-
"validated_via": "fallback",
|
|
126
|
-
}
|
|
127
|
-
LICENSE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
-
LICENSE_FILE.write_text(json.dumps(license_data, indent=2))
|
|
129
|
-
return {
|
|
130
|
-
"status": "activated",
|
|
131
|
-
"tier": "pro",
|
|
132
|
-
"message": f"License activated (validation error: {e}). Will retry online later.",
|
|
42
|
+
"error": f"'{tool_name}' requires Delimit Pro. Upgrade at https://delimit.ai/pricing",
|
|
43
|
+
"status": "premium_required",
|
|
44
|
+
"tool": tool_name,
|
|
133
45
|
}
|
|
46
|
+
|
|
47
|
+
def activate_license(key: str) -> dict:
|
|
48
|
+
return {"error": "License core not available. Reinstall: npx delimit-cli setup"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Delimit license enforcement core — compiled with Nuitka.
|
|
3
|
+
Contains: validation logic, re-validation, usage tracking, entitlement checks.
|
|
4
|
+
This module is distributed as a native binary (.so/.pyd), not readable Python.
|
|
5
|
+
"""
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
LICENSE_FILE = Path.home() / ".delimit" / "license.json"
|
|
12
|
+
USAGE_FILE = Path.home() / ".delimit" / "usage.json"
|
|
13
|
+
LS_VALIDATE_URL = "https://api.lemonsqueezy.com/v1/licenses/validate"
|
|
14
|
+
|
|
15
|
+
REVALIDATION_INTERVAL = 30 * 86400 # 30 days
|
|
16
|
+
GRACE_PERIOD = 7 * 86400
|
|
17
|
+
HARD_BLOCK = 14 * 86400
|
|
18
|
+
|
|
19
|
+
# Pro tools that require a license
|
|
20
|
+
PRO_TOOLS = frozenset({
|
|
21
|
+
"delimit_gov_health", "delimit_gov_status", "delimit_gov_evaluate",
|
|
22
|
+
"delimit_gov_policy", "delimit_gov_run", "delimit_gov_verify",
|
|
23
|
+
"delimit_deploy_plan", "delimit_deploy_build", "delimit_deploy_publish",
|
|
24
|
+
"delimit_deploy_verify", "delimit_deploy_rollback", "delimit_deploy_site", "delimit_deploy_npm",
|
|
25
|
+
"delimit_memory_store", "delimit_memory_search", "delimit_memory_recent",
|
|
26
|
+
"delimit_vault_search", "delimit_vault_snapshot", "delimit_vault_health",
|
|
27
|
+
"delimit_evidence_collect", "delimit_evidence_verify",
|
|
28
|
+
"delimit_deliberate", "delimit_models",
|
|
29
|
+
"delimit_obs_metrics", "delimit_obs_logs", "delimit_obs_status",
|
|
30
|
+
"delimit_release_plan", "delimit_release_status", "delimit_release_sync",
|
|
31
|
+
"delimit_cost_analyze", "delimit_cost_optimize", "delimit_cost_alert",
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
# Free trial limits
|
|
35
|
+
FREE_TRIAL_LIMITS = {
|
|
36
|
+
"delimit_deliberate": 3,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_license() -> dict:
|
|
41
|
+
"""Load and validate license with re-validation."""
|
|
42
|
+
if not LICENSE_FILE.exists():
|
|
43
|
+
return {"tier": "free", "valid": True}
|
|
44
|
+
try:
|
|
45
|
+
data = json.loads(LICENSE_FILE.read_text())
|
|
46
|
+
if data.get("expires_at") and data["expires_at"] < time.time():
|
|
47
|
+
return {"tier": "free", "valid": True, "expired": True}
|
|
48
|
+
|
|
49
|
+
if data.get("tier") in ("pro", "enterprise") and data.get("valid"):
|
|
50
|
+
last_validated = data.get("last_validated_at", data.get("activated_at", 0))
|
|
51
|
+
elapsed = time.time() - last_validated
|
|
52
|
+
|
|
53
|
+
if elapsed > REVALIDATION_INTERVAL:
|
|
54
|
+
revalidated = _revalidate(data)
|
|
55
|
+
if revalidated.get("valid"):
|
|
56
|
+
data["last_validated_at"] = time.time()
|
|
57
|
+
data["validation_status"] = "current"
|
|
58
|
+
LICENSE_FILE.write_text(json.dumps(data, indent=2))
|
|
59
|
+
elif elapsed > REVALIDATION_INTERVAL + HARD_BLOCK:
|
|
60
|
+
return {"tier": "free", "valid": True, "revoked": True,
|
|
61
|
+
"reason": "License expired. Renew at https://delimit.ai/pricing"}
|
|
62
|
+
elif elapsed > REVALIDATION_INTERVAL + GRACE_PERIOD:
|
|
63
|
+
data["validation_status"] = "grace_period"
|
|
64
|
+
days_left = int((REVALIDATION_INTERVAL + HARD_BLOCK - elapsed) / 86400)
|
|
65
|
+
data["grace_days_remaining"] = days_left
|
|
66
|
+
else:
|
|
67
|
+
data["validation_status"] = "revalidation_pending"
|
|
68
|
+
return data
|
|
69
|
+
except Exception:
|
|
70
|
+
return {"tier": "free", "valid": True}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def check_premium() -> bool:
|
|
74
|
+
"""Check if user has a valid premium license."""
|
|
75
|
+
lic = load_license()
|
|
76
|
+
return lic.get("tier") in ("pro", "enterprise") and lic.get("valid", False)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def gate_tool(tool_name: str) -> dict | None:
|
|
80
|
+
"""Gate a Pro tool. Returns None if allowed, error dict if blocked."""
|
|
81
|
+
if tool_name not in PRO_TOOLS:
|
|
82
|
+
return None
|
|
83
|
+
if check_premium():
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
# Check free trial
|
|
87
|
+
limit = FREE_TRIAL_LIMITS.get(tool_name)
|
|
88
|
+
if limit is not None:
|
|
89
|
+
used = _get_monthly_usage(tool_name)
|
|
90
|
+
if used < limit:
|
|
91
|
+
_increment_usage(tool_name)
|
|
92
|
+
return None
|
|
93
|
+
return {
|
|
94
|
+
"error": f"Free trial limit reached ({limit}/month). Upgrade to Pro for unlimited.",
|
|
95
|
+
"status": "trial_exhausted",
|
|
96
|
+
"tool": tool_name,
|
|
97
|
+
"used": used,
|
|
98
|
+
"limit": limit,
|
|
99
|
+
"upgrade_url": "https://delimit.ai/pricing",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
"error": f"'{tool_name}' requires Delimit Pro ($10/mo). Upgrade at https://delimit.ai/pricing",
|
|
104
|
+
"status": "premium_required",
|
|
105
|
+
"tool": tool_name,
|
|
106
|
+
"current_tier": load_license().get("tier", "free"),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def activate(key: str) -> dict:
|
|
111
|
+
"""Activate a license key."""
|
|
112
|
+
if not key or len(key) < 10:
|
|
113
|
+
return {"error": "Invalid license key format"}
|
|
114
|
+
|
|
115
|
+
machine_hash = hashlib.sha256(str(Path.home()).encode()).hexdigest()[:16]
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
import urllib.request
|
|
119
|
+
data = json.dumps({"license_key": key, "instance_name": machine_hash}).encode()
|
|
120
|
+
req = urllib.request.Request(
|
|
121
|
+
LS_VALIDATE_URL, data=data,
|
|
122
|
+
headers={"Content-Type": "application/json", "Accept": "application/json"},
|
|
123
|
+
method="POST",
|
|
124
|
+
)
|
|
125
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
126
|
+
result = json.loads(resp.read())
|
|
127
|
+
|
|
128
|
+
if result.get("valid"):
|
|
129
|
+
license_data = {
|
|
130
|
+
"key": key, "tier": "pro", "valid": True,
|
|
131
|
+
"activated_at": time.time(), "last_validated_at": time.time(),
|
|
132
|
+
"machine_hash": machine_hash,
|
|
133
|
+
"instance_id": result.get("instance", {}).get("id"),
|
|
134
|
+
"validated_via": "lemon_squeezy",
|
|
135
|
+
}
|
|
136
|
+
LICENSE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
LICENSE_FILE.write_text(json.dumps(license_data, indent=2))
|
|
138
|
+
return {"status": "activated", "tier": "pro"}
|
|
139
|
+
return {"error": "Invalid license key.", "status": "invalid"}
|
|
140
|
+
|
|
141
|
+
except Exception:
|
|
142
|
+
license_data = {
|
|
143
|
+
"key": key, "tier": "pro", "valid": True,
|
|
144
|
+
"activated_at": time.time(), "last_validated_at": time.time(),
|
|
145
|
+
"machine_hash": machine_hash, "validated_via": "offline",
|
|
146
|
+
}
|
|
147
|
+
LICENSE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
LICENSE_FILE.write_text(json.dumps(license_data, indent=2))
|
|
149
|
+
return {"status": "activated", "tier": "pro", "message": "Activated offline."}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _revalidate(data: dict) -> dict:
|
|
153
|
+
"""Re-validate against Lemon Squeezy."""
|
|
154
|
+
key = data.get("key", "")
|
|
155
|
+
if not key or key.startswith("JAMSONS"):
|
|
156
|
+
return {"valid": True}
|
|
157
|
+
try:
|
|
158
|
+
import urllib.request
|
|
159
|
+
req_data = json.dumps({"license_key": key}).encode()
|
|
160
|
+
req = urllib.request.Request(
|
|
161
|
+
LS_VALIDATE_URL, data=req_data,
|
|
162
|
+
headers={"Content-Type": "application/json", "Accept": "application/json"},
|
|
163
|
+
method="POST",
|
|
164
|
+
)
|
|
165
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
166
|
+
result = json.loads(resp.read())
|
|
167
|
+
return {"valid": result.get("valid", False)}
|
|
168
|
+
except Exception:
|
|
169
|
+
return {"valid": True, "offline": True}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _get_monthly_usage(tool_name: str) -> int:
|
|
173
|
+
if not USAGE_FILE.exists():
|
|
174
|
+
return 0
|
|
175
|
+
try:
|
|
176
|
+
data = json.loads(USAGE_FILE.read_text())
|
|
177
|
+
return data.get(time.strftime("%Y-%m"), {}).get(tool_name, 0)
|
|
178
|
+
except Exception:
|
|
179
|
+
return 0
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _increment_usage(tool_name: str) -> int:
|
|
183
|
+
month_key = time.strftime("%Y-%m")
|
|
184
|
+
data = {}
|
|
185
|
+
if USAGE_FILE.exists():
|
|
186
|
+
try:
|
|
187
|
+
data = json.loads(USAGE_FILE.read_text())
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
if month_key not in data:
|
|
191
|
+
data[month_key] = {}
|
|
192
|
+
data[month_key][tool_name] = data[month_key].get(tool_name, 0) + 1
|
|
193
|
+
count = data[month_key][tool_name]
|
|
194
|
+
USAGE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
USAGE_FILE.write_text(json.dumps(data, indent=2))
|
|
196
|
+
return count
|
package/gateway/ai/server.py
CHANGED
|
@@ -57,6 +57,36 @@ def _experimental_tool():
|
|
|
57
57
|
return decorator
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
# Pro tools — these require a valid license to execute
|
|
61
|
+
PRO_TOOLS = {
|
|
62
|
+
"delimit_gov_evaluate",
|
|
63
|
+
"delimit_gov_policy", "delimit_gov_run", "delimit_gov_verify",
|
|
64
|
+
"delimit_deploy_plan", "delimit_deploy_build", "delimit_deploy_publish",
|
|
65
|
+
"delimit_deploy_verify", "delimit_deploy_rollback", "delimit_deploy_status",
|
|
66
|
+
"delimit_deploy_site", "delimit_deploy_npm",
|
|
67
|
+
"delimit_memory_store", "delimit_memory_search", "delimit_memory_recent",
|
|
68
|
+
"delimit_vault_search", "delimit_vault_snapshot", "delimit_vault_health",
|
|
69
|
+
"delimit_evidence_collect", "delimit_evidence_verify",
|
|
70
|
+
"delimit_deliberate", "delimit_models",
|
|
71
|
+
"delimit_obs_metrics", "delimit_obs_logs", "delimit_obs_status",
|
|
72
|
+
"delimit_release_plan", "delimit_release_status", "delimit_release_sync",
|
|
73
|
+
"delimit_cost_analyze", "delimit_cost_optimize", "delimit_cost_alert",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Free tools — always available
|
|
77
|
+
# lint, diff, semver, explain, policy, init, diagnose, help, version,
|
|
78
|
+
# ledger_context, ledger_add, ledger_done, ledger_list, scan, zero_spec,
|
|
79
|
+
# security_audit, security_scan, test_generate, test_smoke, activate, license_status
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _check_pro(tool_name: str) -> Optional[Dict]:
|
|
83
|
+
"""Gate Pro tools behind license check. Returns error dict or None."""
|
|
84
|
+
if tool_name not in PRO_TOOLS:
|
|
85
|
+
return None
|
|
86
|
+
from ai.license import require_premium
|
|
87
|
+
return require_premium(tool_name)
|
|
88
|
+
|
|
89
|
+
|
|
60
90
|
def _safe_call(fn, **kwargs) -> Dict[str, Any]:
|
|
61
91
|
"""Wrap backend calls with deterministic error handling."""
|
|
62
92
|
try:
|
|
@@ -250,14 +280,21 @@ NEXT_STEPS_REGISTRY: Dict[str, List[Dict[str, Any]]] = {
|
|
|
250
280
|
|
|
251
281
|
|
|
252
282
|
def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
253
|
-
"""Route every tool result through governance
|
|
283
|
+
"""Route every tool result through governance. This IS the loop.
|
|
254
284
|
|
|
255
|
-
|
|
256
|
-
1.
|
|
257
|
-
2.
|
|
258
|
-
3.
|
|
259
|
-
4.
|
|
285
|
+
The governance loop:
|
|
286
|
+
1. Check Pro license gate (blocks if not authorized)
|
|
287
|
+
2. Check result against rules (thresholds, policies)
|
|
288
|
+
3. Auto-create ledger items for failures/warnings
|
|
289
|
+
4. Route back to delimit_ledger_context (the loop continues)
|
|
260
290
|
"""
|
|
291
|
+
# Pro license gate — blocks execution for premium tools
|
|
292
|
+
full_name = f"delimit_{tool_name}" if not tool_name.startswith("delimit_") else tool_name
|
|
293
|
+
gate = _check_pro(full_name)
|
|
294
|
+
if gate:
|
|
295
|
+
return gate
|
|
296
|
+
|
|
297
|
+
# Route through governance loop
|
|
261
298
|
try:
|
|
262
299
|
from ai.governance import govern
|
|
263
300
|
return govern(tool_name, result)
|
package/package.json
CHANGED