delimit-cli 3.10.1 → 3.10.2

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.
@@ -3,6 +3,11 @@ Delimit license — thin shim.
3
3
  The enforcement logic is in license_core (shipped as compiled binary).
4
4
  This shim handles imports and provides fallback error messages.
5
5
  """
6
+ from pathlib import Path
7
+
8
+ # Always export LICENSE_FILE for tests and external access
9
+ LICENSE_FILE = Path.home() / ".delimit" / "license.json"
10
+
6
11
  try:
7
12
  from ai.license_core import (
8
13
  load_license as get_license,
@@ -20,8 +25,21 @@ except ImportError:
20
25
 
21
26
  LICENSE_FILE = Path.home() / ".delimit" / "license.json"
22
27
 
23
- PRO_TOOLS = set()
24
- FREE_TRIAL_LIMITS = {}
28
+ PRO_TOOLS = frozenset({
29
+ "delimit_gov_evaluate", "delimit_gov_policy", "delimit_gov_run", "delimit_gov_verify",
30
+ "delimit_os_plan", "delimit_os_status", "delimit_os_gates",
31
+ "delimit_deploy_plan", "delimit_deploy_build", "delimit_deploy_publish",
32
+ "delimit_deploy_verify", "delimit_deploy_rollback", "delimit_deploy_status",
33
+ "delimit_deploy_site", "delimit_deploy_npm",
34
+ "delimit_memory_store", "delimit_memory_search", "delimit_memory_recent",
35
+ "delimit_vault_search", "delimit_vault_snapshot", "delimit_vault_health",
36
+ "delimit_evidence_collect", "delimit_evidence_verify",
37
+ "delimit_deliberate", "delimit_models",
38
+ "delimit_obs_metrics", "delimit_obs_logs", "delimit_obs_status",
39
+ "delimit_release_plan", "delimit_release_status", "delimit_release_sync",
40
+ "delimit_cost_analyze", "delimit_cost_optimize", "delimit_cost_alert",
41
+ })
42
+ FREE_TRIAL_LIMITS = {"delimit_deliberate": 3}
25
43
 
26
44
  def get_license() -> dict:
27
45
  if not LICENSE_FILE.exists():
@@ -36,12 +54,16 @@ except ImportError:
36
54
  return lic.get("tier") in ("pro", "enterprise") and lic.get("valid", False)
37
55
 
38
56
  def require_premium(tool_name: str) -> dict | None:
57
+ full_name = tool_name if tool_name.startswith("delimit_") else f"delimit_{tool_name}"
58
+ if full_name not in PRO_TOOLS:
59
+ return None
39
60
  if is_premium():
40
61
  return None
41
62
  return {
42
63
  "error": f"'{tool_name}' requires Delimit Pro. Upgrade at https://delimit.ai/pricing",
43
64
  "status": "premium_required",
44
65
  "tool": tool_name,
66
+ "current_tier": get_license().get("tier", "free"),
45
67
  }
46
68
 
47
69
  def activate_license(key: str) -> dict:
@@ -1,196 +1,9 @@
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
1
+ """Delimit License Core — compiled binary required. Run: npx delimit-cli setup"""
9
2
  from pathlib import Path
10
-
11
3
  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
4
+ PRO_TOOLS = frozenset()
5
+ FREE_TRIAL_LIMITS = {}
6
+ def load_license(): return {"tier": "free", "valid": True}
7
+ def check_premium(): return False
8
+ def gate_tool(t): return None
9
+ def activate(k): return {"error": "License core not available. Run: npx delimit-cli setup"}
@@ -57,25 +57,11 @@ 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
- }
60
+ # Pro tools — single source of truth is license_core.py
61
+ # Import at module level; fallback to license.py shim if core unavailable
62
+ from ai.license import PRO_TOOLS
75
63
 
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,
64
+ # Free tools — everything NOT in PRO_TOOLS
79
65
  # security_audit, security_scan, test_generate, test_smoke, activate, license_status
80
66
 
81
67
 
@@ -567,30 +553,22 @@ def delimit_os_gates(plan_id: str) -> Dict[str, Any]:
567
553
 
568
554
  @mcp.tool()
569
555
  def delimit_gov_health(repo: str = ".") -> Dict[str, Any]:
570
- """Check governance system health (Pro).
556
+ """Check governance system health.
571
557
 
572
558
  Args:
573
559
  repo: Repository path to check.
574
560
  """
575
- from ai.license import require_premium
576
- gate = require_premium("gov_health")
577
- if gate:
578
- return gate
579
561
  from backends.governance_bridge import health
580
562
  return _with_next_steps("gov_health", _safe_call(health, repo=repo))
581
563
 
582
564
 
583
565
  @mcp.tool()
584
566
  def delimit_gov_status(repo: str = ".") -> Dict[str, Any]:
585
- """Get current governance status for a repository (Pro).
567
+ """Get current governance status for a repository.
586
568
 
587
569
  Args:
588
570
  repo: Repository path.
589
571
  """
590
- from ai.license import require_premium
591
- gate = require_premium("gov_status")
592
- if gate:
593
- return gate
594
572
  from backends.governance_bridge import status
595
573
  return _with_next_steps("gov_status", _safe_call(status, repo=repo))
596
574
 
@@ -1025,15 +1003,11 @@ def delimit_repo_config_audit(target: str = ".") -> Dict[str, Any]:
1025
1003
 
1026
1004
  @mcp.tool()
1027
1005
  def delimit_security_scan(target: str = ".") -> Dict[str, Any]:
1028
- """Scan for security vulnerabilities (Pro).
1006
+ """Scan for security vulnerabilities.
1029
1007
 
1030
1008
  Args:
1031
1009
  target: Repository or file path.
1032
1010
  """
1033
- from ai.license import require_premium
1034
- gate = require_premium("security_scan")
1035
- if gate:
1036
- return gate
1037
1011
  from backends.repo_bridge import security_scan
1038
1012
  return _with_next_steps("security_scan", _safe_call(security_scan, target=target))
1039
1013
 
@@ -1099,7 +1073,8 @@ def delimit_evidence_verify(bundle_id: Optional[str] = None, bundle_path: Option
1099
1073
 
1100
1074
  @mcp.tool()
1101
1075
  def delimit_release_plan(environment: str = "production", version: str = "", repository: str = ".", services: Optional[List[str]] = None) -> Dict[str, Any]:
1102
- """Generate a release plan from git history.
1076
+ """ (Pro).
1077
+ Generate a release plan from git history.
1103
1078
 
1104
1079
  Reads git log since last tag, counts commits and changed files,
1105
1080
  suggests a semver version, and generates a release checklist.
@@ -1111,6 +1086,10 @@ def delimit_release_plan(environment: str = "production", version: str = "", rep
1111
1086
  repository: Repository path (default: current directory).
1112
1087
  services: Optional service list.
1113
1088
  """
1089
+ from ai.license import require_premium
1090
+ gate = require_premium("release_plan")
1091
+ if gate:
1092
+ return gate
1114
1093
  from backends.tools_infra import release_plan
1115
1094
  return _with_next_steps("release_plan", _safe_call(release_plan, environment=environment, version=version, repository=repository, services=services))
1116
1095
 
@@ -1129,7 +1108,8 @@ def delimit_release_validate(environment: str, version: str) -> Dict[str, Any]:
1129
1108
 
1130
1109
  @mcp.tool()
1131
1110
  def delimit_release_status(environment: str = "production") -> Dict[str, Any]:
1132
- """Check release/deploy status from file-based tracker and git state.
1111
+ """ (Pro).
1112
+ Check release/deploy status from file-based tracker and git state.
1133
1113
 
1134
1114
  Shows latest deploy plan, current git tag, how many commits HEAD
1135
1115
  is ahead of the tag, and recent deploy history.
@@ -1137,6 +1117,10 @@ def delimit_release_status(environment: str = "production") -> Dict[str, Any]:
1137
1117
  Args:
1138
1118
  environment: Target environment (staging/production).
1139
1119
  """
1120
+ from ai.license import require_premium
1121
+ gate = require_premium("release_status")
1122
+ if gate:
1123
+ return gate
1140
1124
  from backends.tools_infra import release_status
1141
1125
  return _with_next_steps("release_status", _safe_call(release_status, environment=environment))
1142
1126
 
@@ -1170,22 +1154,32 @@ def delimit_release_history(environment: str, limit: int = 10) -> Dict[str, Any]
1170
1154
 
1171
1155
  @mcp.tool()
1172
1156
  def delimit_cost_analyze(target: str = ".") -> Dict[str, Any]:
1173
- """Analyze project costs by scanning Dockerfiles, dependencies, and cloud configs.
1157
+ """ (Pro).
1158
+ Analyze project costs by scanning Dockerfiles, dependencies, and cloud configs.
1174
1159
 
1175
1160
  Args:
1176
1161
  target: Project or infrastructure path to analyze.
1177
1162
  """
1163
+ from ai.license import require_premium
1164
+ gate = require_premium("cost_analyze")
1165
+ if gate:
1166
+ return gate
1178
1167
  from backends.tools_data import cost_analyze
1179
1168
  return _with_next_steps("cost_analyze", _safe_call(cost_analyze, target=target))
1180
1169
 
1181
1170
 
1182
1171
  @mcp.tool()
1183
1172
  def delimit_cost_optimize(target: str = ".") -> Dict[str, Any]:
1184
- """Find cost optimization opportunities: unused deps, oversized images, uncompressed assets.
1173
+ """ (Pro).
1174
+ Find cost optimization opportunities: unused deps, oversized images, uncompressed assets.
1185
1175
 
1186
1176
  Args:
1187
1177
  target: Project or infrastructure path to analyze.
1188
1178
  """
1179
+ from ai.license import require_premium
1180
+ gate = require_premium("cost_optimize")
1181
+ if gate:
1182
+ return gate
1189
1183
  from backends.tools_data import cost_optimize
1190
1184
  return _with_next_steps("cost_optimize", _safe_call(cost_optimize, target=target))
1191
1185
 
@@ -1193,7 +1187,8 @@ def delimit_cost_optimize(target: str = ".") -> Dict[str, Any]:
1193
1187
  @mcp.tool()
1194
1188
  def delimit_cost_alert(action: str = "list", name: Optional[str] = None,
1195
1189
  threshold: Optional[float] = None, alert_id: Optional[str] = None) -> Dict[str, Any]:
1196
- """Manage cost alerts (file-based). CRUD operations on spending thresholds.
1190
+ """ (Pro).
1191
+ Manage cost alerts (file-based). CRUD operations on spending thresholds.
1197
1192
 
1198
1193
  Args:
1199
1194
  action: Action (list/create/delete/toggle).
@@ -1201,6 +1196,10 @@ def delimit_cost_alert(action: str = "list", name: Optional[str] = None,
1201
1196
  threshold: Cost threshold in USD (required for create).
1202
1197
  alert_id: Alert ID (required for delete/toggle).
1203
1198
  """
1199
+ from ai.license import require_premium
1200
+ gate = require_premium("cost_alert")
1201
+ if gate:
1202
+ return gate
1204
1203
  from backends.tools_data import cost_alert
1205
1204
  return _with_next_steps("cost_alert", _safe_call(cost_alert, action=action, name=name, threshold=threshold, alert_id=alert_id))
1206
1205
 
@@ -1244,7 +1243,8 @@ def delimit_data_backup(target: str = ".") -> Dict[str, Any]:
1244
1243
 
1245
1244
  @mcp.tool()
1246
1245
  def delimit_obs_metrics(query: str = "system", time_range: str = "1h", source: Optional[str] = None) -> Dict[str, Any]:
1247
- """Query live system metrics (CPU, memory, disk I/O, network).
1246
+ """ (Pro).
1247
+ Query live system metrics (CPU, memory, disk I/O, network).
1248
1248
 
1249
1249
  Query types: cpu, memory, disk, io, network, system (default), all.
1250
1250
  Reads directly from /proc for real-time data.
@@ -1256,13 +1256,18 @@ def delimit_obs_metrics(query: str = "system", time_range: str = "1h", source: O
1256
1256
  time_range: Time range (e.g. "1h", "24h", "7d").
1257
1257
  source: Optional metrics source (prometheus, local).
1258
1258
  """
1259
+ from ai.license import require_premium
1260
+ gate = require_premium("obs_metrics")
1261
+ if gate:
1262
+ return gate
1259
1263
  from backends.tools_infra import obs_metrics
1260
1264
  return _with_next_steps("obs_metrics", _safe_call(obs_metrics, query=query, time_range=time_range, source=source))
1261
1265
 
1262
1266
 
1263
1267
  @mcp.tool()
1264
1268
  def delimit_obs_logs(query: str, time_range: str = "1h", source: Optional[str] = None) -> Dict[str, Any]:
1265
- """Search system and application logs.
1269
+ """ (Pro).
1270
+ Search system and application logs.
1266
1271
 
1267
1272
  Searches journalctl, /var/log/*, and application log directories.
1268
1273
  Returns matching log lines with source attribution.
@@ -1274,6 +1279,10 @@ def delimit_obs_logs(query: str, time_range: str = "1h", source: Optional[str] =
1274
1279
  time_range: Time range (5m, 15m, 1h, 6h, 24h, 7d).
1275
1280
  source: Log source path or integration name (journalctl, elasticsearch).
1276
1281
  """
1282
+ from ai.license import require_premium
1283
+ gate = require_premium("obs_logs")
1284
+ if gate:
1285
+ return gate
1277
1286
  from backends.tools_infra import obs_logs
1278
1287
  return _with_next_steps("obs_logs", _safe_call(obs_logs, query=query, time_range=time_range, source=source))
1279
1288
 
@@ -1293,12 +1302,17 @@ def delimit_obs_alerts(action: str, alert_rule: Optional[Dict[str, Any]] = None,
1293
1302
 
1294
1303
  @mcp.tool()
1295
1304
  def delimit_obs_status() -> Dict[str, Any]:
1296
- """System health check: disk space, memory, running services, uptime.
1305
+ """ (Pro).
1306
+ System health check: disk space, memory, running services, uptime.
1297
1307
 
1298
1308
  Checks disk usage, memory, process count, load average, and probes
1299
1309
  common service ports (Node, PostgreSQL, Redis, Nginx, etc.).
1300
1310
  No external integration needed.
1301
1311
  """
1312
+ from ai.license import require_premium
1313
+ gate = require_premium("obs_status")
1314
+ if gate:
1315
+ return gate
1302
1316
  from backends.tools_infra import obs_status
1303
1317
  return _with_next_steps("obs_status", _safe_call(obs_status))
1304
1318
 
@@ -1864,7 +1878,8 @@ def delimit_deploy_site(
1864
1878
  project_path: str = ".",
1865
1879
  message: str = "",
1866
1880
  ) -> Dict[str, Any]:
1867
- """Deploy a site — git commit, push, Vercel build, and deploy in one step.
1881
+ """ (Pro).
1882
+ Deploy a site — git commit, push, Vercel build, and deploy in one step.
1868
1883
 
1869
1884
  Handles the full chain: stages changes, commits, pushes to remote,
1870
1885
  builds with Vercel, deploys to production. No manual steps needed.
@@ -1873,6 +1888,10 @@ def delimit_deploy_site(
1873
1888
  project_path: Path to the site project (must have .vercel/ configured).
1874
1889
  message: Git commit message. Auto-generated if empty.
1875
1890
  """
1891
+ from ai.license import require_premium
1892
+ gate = require_premium("deploy_site")
1893
+ if gate:
1894
+ return gate
1876
1895
  from backends.tools_infra import deploy_site
1877
1896
  env_vars = {}
1878
1897
  # Auto-detect Delimit UI env vars
@@ -1892,7 +1911,8 @@ def delimit_deploy_npm(
1892
1911
  tag: str = "latest",
1893
1912
  dry_run: bool = False,
1894
1913
  ) -> Dict[str, Any]:
1895
- """Publish an npm package — bump version, publish to registry, verify.
1914
+ """ (Pro).
1915
+ Publish an npm package — bump version, publish to registry, verify.
1896
1916
 
1897
1917
  Full chain: check auth, bump version, npm publish, verify on registry,
1898
1918
  git commit + push the version bump. Use dry_run=true to preview first.
@@ -1903,6 +1923,10 @@ def delimit_deploy_npm(
1903
1923
  tag: npm dist-tag (default "latest").
1904
1924
  dry_run: If true, preview without actually publishing.
1905
1925
  """
1926
+ from ai.license import require_premium
1927
+ gate = require_premium("deploy_npm")
1928
+ if gate:
1929
+ return gate
1906
1930
  from backends.tools_infra import deploy_npm
1907
1931
  return _with_next_steps("deploy_npm", deploy_npm(project_path, bump, tag, dry_run))
1908
1932
 
@@ -2036,7 +2060,8 @@ def delimit_models(
2036
2060
  api_key: str = "",
2037
2061
  model_name: str = "",
2038
2062
  ) -> Dict[str, Any]:
2039
- """View and configure AI models for multi-model deliberation.
2063
+ """ (Pro).
2064
+ View and configure AI models for multi-model deliberation.
2040
2065
 
2041
2066
  Actions:
2042
2067
  - "list": show configured models and what's available
@@ -2052,6 +2077,10 @@ def delimit_models(
2052
2077
  api_key: API key for the provider (only used with action=add).
2053
2078
  model_name: Optional model name override (e.g. "gpt-4o", "claude-sonnet-4-5-20250514").
2054
2079
  """
2080
+ from ai.license import require_premium
2081
+ gate = require_premium("models")
2082
+ if gate:
2083
+ return gate
2055
2084
  from ai.deliberation import configure_models, get_models_config, MODELS_CONFIG, DEFAULT_MODELS
2056
2085
  import json as _json
2057
2086
 
@@ -2144,14 +2173,9 @@ def delimit_deliberate(
2144
2173
  max_rounds: int = 3,
2145
2174
  save_path: str = "",
2146
2175
  ) -> Dict[str, Any]:
2147
- """Run multi-model consensus via real AI-to-AI deliberation.
2148
-
2149
- This is the consensus tool. Models (Grok 4, Gemini, Codex) debate each other
2150
- directly until they reach unanimous agreement.
2176
+ """Run multi-model consensus via real AI-to-AI deliberation (Pro).
2151
2177
 
2152
- Modes:
2153
- - "dialogue": Short conversational turns like a group chat (default, 6 rounds)
2154
- - "debate": Long-form essays with full counter-arguments (3 rounds)
2178
+ Models (Grok 4, Gemini, Codex) debate each other directly until unanimous agreement.
2155
2179
 
2156
2180
  Args:
2157
2181
  question: The question to reach consensus on.
@@ -2160,6 +2184,10 @@ def delimit_deliberate(
2160
2184
  max_rounds: Maximum rounds (default 3 for debate, 6 for dialogue).
2161
2185
  save_path: Optional file path to save the full transcript.
2162
2186
  """
2187
+ from ai.license import require_premium
2188
+ gate = require_premium("deliberate")
2189
+ if gate:
2190
+ return gate
2163
2191
  from ai.deliberation import deliberate
2164
2192
  result = deliberate(
2165
2193
  question=question,
@@ -2271,7 +2299,8 @@ def _extract_deliberation_actions(result: Dict, question: str) -> List[Dict[str,
2271
2299
 
2272
2300
  @mcp.tool()
2273
2301
  def delimit_release_sync(action: str = "audit") -> Dict[str, Any]:
2274
- """Audit or sync all public surfaces for consistency.
2302
+ """ (Pro).
2303
+ Audit or sync all public surfaces for consistency.
2275
2304
 
2276
2305
  Checks GitHub repos, npm, site meta tags, CLAUDE.md, and releases
2277
2306
  against a central config. Reports what's stale and what needs updating.
@@ -2279,6 +2308,10 @@ def delimit_release_sync(action: str = "audit") -> Dict[str, Any]:
2279
2308
  Args:
2280
2309
  action: "audit" to check all surfaces, "config" to view/edit the release config.
2281
2310
  """
2311
+ from ai.license import require_premium
2312
+ gate = require_premium("release_sync")
2313
+ if gate:
2314
+ return gate
2282
2315
  from ai.release_sync import audit, get_release_config
2283
2316
  if action == "config":
2284
2317
  return get_release_config()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
- "version": "3.10.1",
3
+ "version": "3.10.2",
4
4
  "description": "One workspace for every AI coding assistant. Tasks, memory, and governance carry between Claude Code, Codex, and Gemini CLI.",
5
5
  "main": "index.js",
6
6
  "files": [