delimit-cli 4.1.42 → 4.1.44

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.
@@ -30,7 +30,7 @@ OWN_REPOS = [
30
30
  "delimit-ai/delimit-quickstart",
31
31
  ]
32
32
 
33
- INTERNAL_USERS = set() # configured at runtime
33
+ INTERNAL_USERS = set(filter(None, os.environ.get("DELIMIT_INTERNAL_USERS", "").split(",")))
34
34
 
35
35
  COMPETITOR_ACTIONS = [
36
36
  "tufin/oasdiff-action",
@@ -35,6 +35,59 @@ def _is_test_mode() -> bool:
35
35
  logger = logging.getLogger("delimit.governance")
36
36
 
37
37
 
38
+ # ── LED-263: Beta CTA for conversion ────────────────────────────────
39
+ # Tools that should show a beta signup prompt on successful results.
40
+ _BETA_CTA_TOOLS = frozenset({"lint", "scan", "activate", "diff", "quickstart"})
41
+
42
+ _BETA_CTA = {
43
+ "text": "Like what you see? Join the beta for priority support and full governance.",
44
+ "url": "https://app.delimit.ai",
45
+ "action": "star_repo_or_signup",
46
+ }
47
+
48
+
49
+ def _is_beta_user() -> bool:
50
+ """Check if the current user is already tracked as a founding/beta user."""
51
+ try:
52
+ from ai.founding_users import _load_founding_users
53
+ data = _load_founding_users()
54
+ if data.get("users"):
55
+ return True
56
+ except Exception:
57
+ pass
58
+ # Also check if a Pro license is active (paying users don't need the CTA)
59
+ try:
60
+ from ai.license import get_license
61
+ lic = get_license()
62
+ if lic.get("tier", "free") != "free":
63
+ return True
64
+ except Exception:
65
+ pass
66
+ return False
67
+
68
+
69
+ def _result_is_successful(result: Dict[str, Any]) -> bool:
70
+ """Return True if a tool result looks like a success (no errors)."""
71
+ if result.get("error"):
72
+ return False
73
+ if result.get("status") in ("error", "failed", "blocked"):
74
+ return False
75
+ if result.get("governance_blocked"):
76
+ return False
77
+ return True
78
+
79
+
80
+ def _maybe_beta_cta(tool_name: str, result: Dict[str, Any]) -> Optional[Dict[str, str]]:
81
+ """Return a beta CTA dict if the tool qualifies and the user is not already signed up."""
82
+ if tool_name not in _BETA_CTA_TOOLS:
83
+ return None
84
+ if not _result_is_successful(result):
85
+ return None
86
+ if _is_beta_user():
87
+ return None
88
+ return dict(_BETA_CTA)
89
+
90
+
38
91
  def _ledger_list_items(project_path: str = ".") -> Dict[str, Any]:
39
92
  """Indirection layer so tests can patch governance-local ledger hooks."""
40
93
  import ai.ledger_manager as _lm
@@ -676,6 +729,11 @@ def govern(tool_name: str, result: Dict[str, Any], project_path: str = ".") -> D
676
729
  if "next_steps" not in governed_result:
677
730
  governed_result["next_steps"] = []
678
731
 
732
+ # LED-263: Beta CTA on successful lint/scan/activate/diff results
733
+ cta = _maybe_beta_cta(clean_name, governed_result)
734
+ if cta:
735
+ governed_result["beta_cta"] = cta
736
+
679
737
  return governed_result
680
738
 
681
739
 
@@ -1,2 +1,95 @@
1
- # key_resolver Pro module (stubbed in npm package)
2
- # Full implementation available on delimit.ai server
1
+ """Auto-resolve API keys from multiple sources.
2
+
3
+ Priority: env var -> secrets broker -> return None (free fallback).
4
+
5
+ Every MCP tool that depends on an external service should use this module
6
+ so it works out of the box without API keys, with enhanced functionality
7
+ unlocked when keys are available.
8
+ """
9
+
10
+ import json
11
+ import logging
12
+ import os
13
+ import shutil
14
+ import subprocess
15
+ from pathlib import Path
16
+ from typing import Optional, Tuple
17
+
18
+ logger = logging.getLogger("delimit.ai.key_resolver")
19
+
20
+ SECRETS_DIR = Path.home() / ".delimit" / "secrets"
21
+
22
+
23
+ def get_key(name: str, env_var: str = "", _secrets_dir: Optional[Path] = None) -> Tuple[Optional[str], str]:
24
+ """Get an API key. Returns (key, source) or (None, "not_found").
25
+
26
+ Sources checked in order:
27
+ 1. Environment variable (explicit *env_var*, then common conventions)
28
+ 2. ~/.delimit/secrets/{name}.json
29
+ 3. None (free fallback)
30
+ """
31
+ # 1. Env var — explicit, then common patterns
32
+ candidates = [env_var] if env_var else []
33
+ candidates += [
34
+ f"{name.upper()}_TOKEN",
35
+ f"{name.upper()}_API_KEY",
36
+ f"{name.upper()}_KEY",
37
+ ]
38
+ for var in candidates:
39
+ if not var:
40
+ continue
41
+ val = os.environ.get(var)
42
+ if val:
43
+ return val, "env"
44
+
45
+ # 2. Secrets broker
46
+ secrets_dir = _secrets_dir if _secrets_dir is not None else SECRETS_DIR
47
+ secrets_file = secrets_dir / f"{name.lower()}.json"
48
+ if secrets_file.exists():
49
+ try:
50
+ data = json.loads(secrets_file.read_text())
51
+ for field in ("value", "api_key", "token", "key"):
52
+ if data.get(field):
53
+ return data[field], "secrets_broker"
54
+ except Exception:
55
+ logger.debug("Failed to read secrets file %s", secrets_file)
56
+
57
+ # 3. Not found
58
+ return None, "not_found"
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Convenience wrappers
63
+ # ---------------------------------------------------------------------------
64
+
65
+ def get_figma_token() -> Tuple[Optional[str], str]:
66
+ """Resolve Figma API token."""
67
+ return get_key("figma", "FIGMA_TOKEN")
68
+
69
+
70
+ def get_trivy_path() -> Tuple[Optional[str], str]:
71
+ """Check if Trivy binary is available on PATH."""
72
+ path = shutil.which("trivy")
73
+ return (path, "installed") if path else (None, "not_found")
74
+
75
+
76
+ def get_playwright() -> Tuple[bool, str]:
77
+ """Check whether Playwright is usable (Python package installed)."""
78
+ try:
79
+ import playwright # noqa: F401
80
+ return True, "installed"
81
+ except ImportError:
82
+ return False, "not_found"
83
+
84
+
85
+ def get_puppeteer() -> Tuple[bool, str]:
86
+ """Check whether puppeteer (npx) is available for screenshot fallback."""
87
+ try:
88
+ result = subprocess.run(
89
+ ["npx", "puppeteer", "--version"],
90
+ capture_output=True,
91
+ timeout=15,
92
+ )
93
+ return (True, "installed") if result.returncode == 0 else (False, "not_found")
94
+ except Exception:
95
+ return False, "not_found"
@@ -91,10 +91,20 @@ def _register_venture(info: Dict[str, str]):
91
91
  VENTURES_FILE.write_text(json.dumps(ventures, indent=2))
92
92
 
93
93
 
94
+ CENTRAL_LEDGER_DIR = Path.home() / ".delimit" / "ledger"
95
+
96
+
94
97
  def _project_ledger_dir(project_path: str = ".") -> Path:
95
- """Get the ledger directory for the current project."""
96
- p = Path(project_path).resolve()
97
- return p / ".delimit" / "ledger"
98
+ """Get the ledger directory ALWAYS uses central ~/.delimit/ledger/.
99
+
100
+ Cross-model handoff fix: Codex and Gemini were writing to $PWD/.delimit/ledger/
101
+ which caused ledger fragmentation. All models must use the same central location
102
+ so Claude, Codex, and Gemini see the same items.
103
+
104
+ The central ledger at ~/.delimit/ledger/ is the source of truth.
105
+ Per-project .delimit/ dirs are for policies and config only, not ledger state.
106
+ """
107
+ return CENTRAL_LEDGER_DIR
98
108
 
99
109
 
100
110
  def _ensure(project_path: str = "."):