delimit-cli 4.1.43 → 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.
- package/CHANGELOG.md +27 -0
- package/README.md +46 -5
- package/bin/delimit-cli.js +1523 -208
- package/bin/delimit-setup.js +8 -2
- package/gateway/ai/agent_dispatch.py +34 -2
- package/gateway/ai/backends/deploy_bridge.py +167 -12
- package/gateway/ai/content_engine.py +1276 -2
- package/gateway/ai/github_scanner.py +1 -1
- package/gateway/ai/governance.py +58 -0
- package/gateway/ai/key_resolver.py +95 -2
- package/gateway/ai/ledger_manager.py +13 -3
- package/gateway/ai/loop_engine.py +220 -349
- package/gateway/ai/notify.py +1786 -2
- package/gateway/ai/reddit_scanner.py +45 -1
- package/gateway/ai/screen_record.py +1 -1
- package/gateway/ai/secrets_broker.py +5 -1
- package/gateway/ai/social_cache.py +341 -0
- package/gateway/ai/social_daemon.py +41 -10
- package/gateway/ai/supabase_sync.py +190 -2
- package/gateway/ai/tui.py +594 -36
- package/gateway/core/zero_spec/express_extractor.py +2 -2
- package/gateway/core/zero_spec/nestjs_extractor.py +40 -9
- package/gateway/requirements.txt +3 -6
- package/package.json +4 -3
- package/scripts/demo-v420-clean.sh +267 -0
- package/scripts/demo-v420-deliberation.sh +217 -0
- package/scripts/demo-v420.sh +55 -0
- package/scripts/postinstall.js +4 -3
- package/scripts/publish-ci-guard.sh +30 -0
- package/scripts/record-and-upload.sh +132 -0
- package/scripts/release.sh +126 -0
- package/scripts/sync-gateway.sh +100 -0
- package/scripts/youtube-upload.py +141 -0
package/gateway/ai/governance.py
CHANGED
|
@@ -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
|
-
|
|
2
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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 = "."):
|