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
|
@@ -1,2 +1,190 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""Supabase sync -- writes gateway data to cloud for dashboard access.
|
|
2
|
+
|
|
3
|
+
Writes are fire-and-forget (never blocks tool execution).
|
|
4
|
+
If Supabase is unreachable, data stays in local files (always the source of truth).
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import logging
|
|
9
|
+
import uuid
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Optional
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("delimit.supabase_sync")
|
|
14
|
+
|
|
15
|
+
_client = None
|
|
16
|
+
_init_attempted = False
|
|
17
|
+
SUPABASE_URL = os.environ.get("SUPABASE_URL", "")
|
|
18
|
+
SUPABASE_KEY = os.environ.get("SUPABASE_SERVICE_ROLE_KEY", "")
|
|
19
|
+
|
|
20
|
+
# Also check local secrets file
|
|
21
|
+
if not SUPABASE_URL:
|
|
22
|
+
secrets_file = Path.home() / ".delimit" / "secrets" / "supabase.json"
|
|
23
|
+
if secrets_file.exists():
|
|
24
|
+
try:
|
|
25
|
+
creds = json.loads(secrets_file.read_text())
|
|
26
|
+
SUPABASE_URL = creds.get("url", "")
|
|
27
|
+
SUPABASE_KEY = creds.get("service_role_key", "")
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_client():
|
|
33
|
+
"""Lazy-init Supabase client. Returns the SDK client, 'http' for fallback, or None."""
|
|
34
|
+
global _client, _init_attempted
|
|
35
|
+
if _client is not None:
|
|
36
|
+
return _client
|
|
37
|
+
if _init_attempted:
|
|
38
|
+
return _client # Already tried and failed, return cached result (may be None or "http")
|
|
39
|
+
_init_attempted = True
|
|
40
|
+
if not SUPABASE_URL or not SUPABASE_KEY:
|
|
41
|
+
return None
|
|
42
|
+
try:
|
|
43
|
+
from supabase import create_client
|
|
44
|
+
_client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
|
45
|
+
return _client
|
|
46
|
+
except ImportError:
|
|
47
|
+
logger.debug("supabase-py not installed, using HTTP fallback")
|
|
48
|
+
_client = "http"
|
|
49
|
+
return _client
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.warning(f"Supabase init failed: {e}")
|
|
52
|
+
_client = "http" # Fall back to HTTP rather than giving up entirely
|
|
53
|
+
return _client
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _http_post(table: str, data: dict, headers_extra: Optional[Dict] = None) -> bool:
|
|
57
|
+
"""POST to Supabase REST API without the SDK."""
|
|
58
|
+
import urllib.request
|
|
59
|
+
try:
|
|
60
|
+
url = f"{SUPABASE_URL}/rest/v1/{table}"
|
|
61
|
+
body = json.dumps(data).encode()
|
|
62
|
+
req = urllib.request.Request(url, data=body, method="POST")
|
|
63
|
+
req.add_header("Content-Type", "application/json")
|
|
64
|
+
req.add_header("apikey", SUPABASE_KEY)
|
|
65
|
+
req.add_header("Authorization", f"Bearer {SUPABASE_KEY}")
|
|
66
|
+
req.add_header("Prefer", "return=minimal")
|
|
67
|
+
if headers_extra:
|
|
68
|
+
for k, v in headers_extra.items():
|
|
69
|
+
req.add_header(k, v)
|
|
70
|
+
urllib.request.urlopen(req, timeout=5)
|
|
71
|
+
return True
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.debug(f"Supabase HTTP POST to {table} failed: {e}")
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _http_patch(table: str, query: str, data: dict) -> bool:
|
|
78
|
+
"""PATCH to Supabase REST API without the SDK."""
|
|
79
|
+
import urllib.request
|
|
80
|
+
try:
|
|
81
|
+
url = f"{SUPABASE_URL}/rest/v1/{table}?{query}"
|
|
82
|
+
body = json.dumps(data).encode()
|
|
83
|
+
req = urllib.request.Request(url, data=body, method="PATCH")
|
|
84
|
+
req.add_header("Content-Type", "application/json")
|
|
85
|
+
req.add_header("apikey", SUPABASE_KEY)
|
|
86
|
+
req.add_header("Authorization", f"Bearer {SUPABASE_KEY}")
|
|
87
|
+
req.add_header("Prefer", "return=minimal")
|
|
88
|
+
urllib.request.urlopen(req, timeout=5)
|
|
89
|
+
return True
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.debug(f"Supabase HTTP PATCH to {table} failed: {e}")
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def sync_event(event: dict):
|
|
96
|
+
"""Sync an event to Supabase (fire-and-forget).
|
|
97
|
+
|
|
98
|
+
Maps the gateway event dict to the Supabase events table schema:
|
|
99
|
+
id (uuid, required), type (text, required), tool (text, required),
|
|
100
|
+
ts, model, status, venture, detail, user_id, session_id
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
client = _get_client()
|
|
104
|
+
if client is None:
|
|
105
|
+
return
|
|
106
|
+
row = {
|
|
107
|
+
"id": str(uuid.uuid4()),
|
|
108
|
+
"type": event.get("type", "tool_call"),
|
|
109
|
+
"tool": event.get("tool", "unknown"),
|
|
110
|
+
"ts": event.get("ts", ""),
|
|
111
|
+
"model": event.get("model", ""),
|
|
112
|
+
"status": event.get("status", "ok"),
|
|
113
|
+
"venture": event.get("venture", ""),
|
|
114
|
+
"session_id": event.get("session_id", ""),
|
|
115
|
+
"user_id": event.get("user_id", ""),
|
|
116
|
+
}
|
|
117
|
+
# Include risk_level and trace info in detail field
|
|
118
|
+
detail_parts = []
|
|
119
|
+
if event.get("risk_level"):
|
|
120
|
+
detail_parts.append(f"risk={event['risk_level']}")
|
|
121
|
+
if event.get("trace_id"):
|
|
122
|
+
detail_parts.append(f"trace={event['trace_id']}")
|
|
123
|
+
if event.get("span_id"):
|
|
124
|
+
detail_parts.append(f"span={event['span_id']}")
|
|
125
|
+
if detail_parts:
|
|
126
|
+
row["detail"] = " ".join(detail_parts)
|
|
127
|
+
|
|
128
|
+
if client == "http":
|
|
129
|
+
_http_post("events", row)
|
|
130
|
+
else:
|
|
131
|
+
client.table("events").insert(row).execute()
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.debug(f"Event sync failed: {e}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def sync_ledger_item(item: dict):
|
|
137
|
+
"""Sync a ledger item to Supabase (upsert).
|
|
138
|
+
|
|
139
|
+
Maps the gateway ledger item to the Supabase ledger_items table schema:
|
|
140
|
+
id (text, required), title (text, required), priority, venture,
|
|
141
|
+
status, description, source, note, assignee
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
client = _get_client()
|
|
145
|
+
if client is None:
|
|
146
|
+
return
|
|
147
|
+
row = {
|
|
148
|
+
"id": item.get("id", ""),
|
|
149
|
+
"title": item.get("title", ""),
|
|
150
|
+
"priority": item.get("priority", "P1"),
|
|
151
|
+
"venture": item.get("venture", ""),
|
|
152
|
+
"status": item.get("status", "open"),
|
|
153
|
+
"description": item.get("description", ""),
|
|
154
|
+
"source": item.get("source", "mcp"),
|
|
155
|
+
}
|
|
156
|
+
if not row["id"] or not row["title"]:
|
|
157
|
+
return # Required fields missing
|
|
158
|
+
if client == "http":
|
|
159
|
+
_http_post(
|
|
160
|
+
"ledger_items",
|
|
161
|
+
row,
|
|
162
|
+
headers_extra={
|
|
163
|
+
"Prefer": "resolution=merge-duplicates,return=minimal",
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
client.table("ledger_items").upsert(row).execute()
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.debug(f"Ledger item sync failed: {e}")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def sync_ledger_update(item_id: str, status: str, note: str = ""):
|
|
173
|
+
"""Sync a ledger status update to Supabase."""
|
|
174
|
+
try:
|
|
175
|
+
client = _get_client()
|
|
176
|
+
if client is None:
|
|
177
|
+
return
|
|
178
|
+
update = {"status": status}
|
|
179
|
+
if note:
|
|
180
|
+
update["note"] = note
|
|
181
|
+
if status == "done":
|
|
182
|
+
from datetime import datetime, timezone
|
|
183
|
+
update["completed_at"] = datetime.now(timezone.utc).isoformat()
|
|
184
|
+
|
|
185
|
+
if client == "http":
|
|
186
|
+
_http_patch("ledger_items", f"id=eq.{item_id}", update)
|
|
187
|
+
else:
|
|
188
|
+
client.table("ledger_items").update(update).eq("id", item_id).execute()
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.debug(f"Ledger update sync failed: {e}")
|