delimit-cli 3.9.1 → 3.9.3

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.
@@ -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
@@ -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 (replaces simple next_steps).
283
+ """Route every tool result through governance. This IS the loop.
254
284
 
255
- Governance:
256
- 1. Checks result against rules (thresholds, policies)
257
- 2. Auto-creates ledger items for failures/warnings
258
- 3. Adds next_steps to keep the AI building
259
- 4. Loops back to governance via ledger_context suggestion
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
- "version": "3.9.1",
3
+ "version": "3.9.3",
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": [
@@ -58,6 +58,7 @@
58
58
  "js-yaml": "^4.1.0",
59
59
  "minimatch": "^5.1.0"
60
60
  },
61
+ "proModuleVersion": "3.8.2",
61
62
  "engines": {
62
63
  "node": ">=14.0.0"
63
64
  }