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.
- 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 +312 -18
- 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/bin/delimit-setup.js
CHANGED
|
@@ -174,8 +174,13 @@ async function main() {
|
|
|
174
174
|
fs.mkdirSync(path.join(DELIMIT_HOME, 'evidence'), { recursive: true });
|
|
175
175
|
|
|
176
176
|
// Copy the gateway core from our bundled copy
|
|
177
|
+
// Skip if server dirs are symlinks (dev machine using gateway source directly)
|
|
178
|
+
const serverAiDir = path.join(DELIMIT_HOME, 'server', 'ai');
|
|
179
|
+
const isDevSymlink = fs.existsSync(serverAiDir) && fs.lstatSync(serverAiDir).isSymbolicLink();
|
|
177
180
|
const gatewaySource = path.join(__dirname, '..', 'gateway');
|
|
178
|
-
if (
|
|
181
|
+
if (isDevSymlink) {
|
|
182
|
+
await logp(` ${green('✓')} Server linked to gateway source (dev mode)`);
|
|
183
|
+
} else if (fs.existsSync(gatewaySource)) {
|
|
179
184
|
copyDir(gatewaySource, path.join(DELIMIT_HOME, 'server'));
|
|
180
185
|
await logp(` ${green('✓')} Core engine installed`);
|
|
181
186
|
} else {
|
|
@@ -216,7 +221,8 @@ async function main() {
|
|
|
216
221
|
}
|
|
217
222
|
|
|
218
223
|
// Re-copy gateway source AFTER Pro modules to ensure full files aren't overwritten by stubs
|
|
219
|
-
if
|
|
224
|
+
// Skip if dev symlinks are in place
|
|
225
|
+
if (fs.existsSync(gatewaySource) && !isDevSymlink) {
|
|
220
226
|
copyDir(gatewaySource, path.join(DELIMIT_HOME, 'server'));
|
|
221
227
|
}
|
|
222
228
|
|
|
@@ -61,6 +61,10 @@ def dispatch_task(
|
|
|
61
61
|
tools_needed: Optional[List[str]] = None,
|
|
62
62
|
constraints: Optional[List[str]] = None,
|
|
63
63
|
context: str = "",
|
|
64
|
+
task_type: str = "",
|
|
65
|
+
venture: str = "",
|
|
66
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
67
|
+
external_key: str = "",
|
|
64
68
|
) -> Dict[str, Any]:
|
|
65
69
|
"""Create a tracked agent task.
|
|
66
70
|
|
|
@@ -78,6 +82,23 @@ def dispatch_task(
|
|
|
78
82
|
if priority not in VALID_PRIORITIES:
|
|
79
83
|
return {"error": f"priority must be one of: {', '.join(sorted(VALID_PRIORITIES))}"}
|
|
80
84
|
|
|
85
|
+
tasks = _load_tasks()
|
|
86
|
+
|
|
87
|
+
normalized_external_key = external_key.strip()
|
|
88
|
+
if normalized_external_key:
|
|
89
|
+
for existing in tasks.values():
|
|
90
|
+
if existing.get("external_key") != normalized_external_key:
|
|
91
|
+
continue
|
|
92
|
+
if existing.get("status") in ("dispatched", "in_progress", "handed_off", "done"):
|
|
93
|
+
prompt = _build_agent_prompt(existing)
|
|
94
|
+
return {
|
|
95
|
+
"status": "deduped",
|
|
96
|
+
"task_id": existing["id"],
|
|
97
|
+
"task": existing,
|
|
98
|
+
"agent_prompt": prompt,
|
|
99
|
+
"message": f"Task {existing['id']} already exists for {normalized_external_key}",
|
|
100
|
+
}
|
|
101
|
+
|
|
81
102
|
task_id = f"AGT-{uuid.uuid4().hex[:8].upper()}"
|
|
82
103
|
|
|
83
104
|
task = {
|
|
@@ -89,6 +110,10 @@ def dispatch_task(
|
|
|
89
110
|
"tools_needed": tools_needed or [],
|
|
90
111
|
"constraints": constraints or [],
|
|
91
112
|
"context": context.strip(),
|
|
113
|
+
"task_type": task_type.strip(),
|
|
114
|
+
"venture": venture.strip(),
|
|
115
|
+
"variables": variables or {},
|
|
116
|
+
"external_key": normalized_external_key,
|
|
92
117
|
"status": "dispatched",
|
|
93
118
|
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
94
119
|
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
@@ -97,7 +122,6 @@ def dispatch_task(
|
|
|
97
122
|
"handoffs": [],
|
|
98
123
|
}
|
|
99
124
|
|
|
100
|
-
tasks = _load_tasks()
|
|
101
125
|
tasks[task_id] = task
|
|
102
126
|
_save_tasks(tasks)
|
|
103
127
|
|
|
@@ -135,6 +159,11 @@ def _build_agent_prompt(task: Dict[str, Any]) -> str:
|
|
|
135
159
|
if task.get("context"):
|
|
136
160
|
lines.append(f"\n**Context:**\n{task['context']}")
|
|
137
161
|
|
|
162
|
+
if task.get("variables"):
|
|
163
|
+
lines.append("\n**Variables:**")
|
|
164
|
+
for key, value in task["variables"].items():
|
|
165
|
+
lines.append(f"- {key}: {value}")
|
|
166
|
+
|
|
138
167
|
if task.get("tools_needed"):
|
|
139
168
|
lines.append(f"\n**Tools needed:** {', '.join(task['tools_needed'])}")
|
|
140
169
|
|
|
@@ -447,7 +476,10 @@ def get_agent_dashboard() -> Dict[str, Any]:
|
|
|
447
476
|
"tasks": [
|
|
448
477
|
{"id": t["id"], "title": t["title"], "status": t["status"],
|
|
449
478
|
"priority": t.get("priority", "P1"),
|
|
450
|
-
"linked_ledger": t.get("linked_ledger_items", [])
|
|
479
|
+
"linked_ledger": t.get("linked_ledger_items", []),
|
|
480
|
+
"task_type": t.get("task_type", ""),
|
|
481
|
+
"venture": t.get("venture", ""),
|
|
482
|
+
"variables": t.get("variables", {})}
|
|
451
483
|
for t in model_tasks
|
|
452
484
|
],
|
|
453
485
|
}
|
|
@@ -154,20 +154,175 @@ def publish(app: str, git_ref: Optional[str] = None) -> Dict[str, Any]:
|
|
|
154
154
|
return latest
|
|
155
155
|
|
|
156
156
|
|
|
157
|
+
DEPLOY_TARGETS = [
|
|
158
|
+
{"name": "delimit.ai", "url": "https://delimit.ai", "kind": "vercel"},
|
|
159
|
+
{"name": "electricgrill.com", "url": "https://electricgrill.com", "kind": "vercel"},
|
|
160
|
+
{"name": "robotax.com", "url": "https://robotax.com", "kind": "vercel"},
|
|
161
|
+
{"name": "npm:delimit-cli", "url": "https://www.npmjs.com/package/delimit-cli", "kind": "npm"},
|
|
162
|
+
{"name": "github:delimit-mcp-server", "url": "https://github.com/delimit-ai/delimit-mcp-server", "kind": "github"},
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _check_http_health(url: str, timeout: int = 10) -> Dict[str, Any]:
|
|
167
|
+
"""Check HTTP health for a single URL. Returns status, response time, headers."""
|
|
168
|
+
import ssl
|
|
169
|
+
import time
|
|
170
|
+
import urllib.request
|
|
171
|
+
|
|
172
|
+
result: Dict[str, Any] = {"url": url, "healthy": False}
|
|
173
|
+
try:
|
|
174
|
+
ctx = ssl.create_default_context()
|
|
175
|
+
req = urllib.request.Request(url, method="GET", headers={"User-Agent": "delimit-deploy-verify/1.0"})
|
|
176
|
+
start = time.monotonic()
|
|
177
|
+
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
|
|
178
|
+
elapsed_ms = round((time.monotonic() - start) * 1000)
|
|
179
|
+
result["status_code"] = resp.status
|
|
180
|
+
result["response_time_ms"] = elapsed_ms
|
|
181
|
+
result["healthy"] = 200 <= resp.status < 400
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
result["error"] = str(exc)
|
|
184
|
+
result["status_code"] = None
|
|
185
|
+
result["response_time_ms"] = None
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _check_ssl_cert(hostname: str, port: int = 443, warn_days: int = 30) -> Dict[str, Any]:
|
|
190
|
+
"""Validate SSL certificate for a hostname. Checks expiry within warn_days."""
|
|
191
|
+
import socket
|
|
192
|
+
import ssl
|
|
193
|
+
|
|
194
|
+
result: Dict[str, Any] = {"hostname": hostname, "ssl_valid": False}
|
|
195
|
+
try:
|
|
196
|
+
ctx = ssl.create_default_context()
|
|
197
|
+
with socket.create_connection((hostname, port), timeout=10) as sock:
|
|
198
|
+
with ctx.wrap_socket(sock, server_hostname=hostname) as ssock:
|
|
199
|
+
cert = ssock.getpeercert()
|
|
200
|
+
if not cert:
|
|
201
|
+
result["error"] = "No certificate returned"
|
|
202
|
+
return result
|
|
203
|
+
not_after_str = cert.get("notAfter", "")
|
|
204
|
+
# Python ssl cert dates: 'Mon DD HH:MM:SS YYYY GMT'
|
|
205
|
+
not_after = datetime.strptime(not_after_str, "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
|
|
206
|
+
now = datetime.now(timezone.utc)
|
|
207
|
+
days_remaining = (not_after - now).days
|
|
208
|
+
result["ssl_valid"] = True
|
|
209
|
+
result["expires"] = not_after.isoformat()
|
|
210
|
+
result["days_remaining"] = days_remaining
|
|
211
|
+
result["expiry_warning"] = days_remaining < warn_days
|
|
212
|
+
if days_remaining < warn_days:
|
|
213
|
+
result["warning"] = f"SSL certificate expires in {days_remaining} days (threshold: {warn_days})"
|
|
214
|
+
# Extract issuer for diagnostics
|
|
215
|
+
issuer = dict(x[0] for x in cert.get("issuer", ()))
|
|
216
|
+
result["issuer"] = issuer.get("organizationName", issuer.get("commonName", "unknown"))
|
|
217
|
+
except Exception as exc:
|
|
218
|
+
result["error"] = str(exc)
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _check_npm_version(expected_version: Optional[str] = None) -> Dict[str, Any]:
|
|
223
|
+
"""Check the published npm version of delimit-cli."""
|
|
224
|
+
import subprocess
|
|
225
|
+
|
|
226
|
+
result: Dict[str, Any] = {"package": "delimit-cli", "healthy": False}
|
|
227
|
+
try:
|
|
228
|
+
proc = subprocess.run(
|
|
229
|
+
["npm", "view", "delimit-cli", "version"],
|
|
230
|
+
capture_output=True, text=True, timeout=15,
|
|
231
|
+
)
|
|
232
|
+
if proc.returncode == 0:
|
|
233
|
+
published = proc.stdout.strip()
|
|
234
|
+
result["published_version"] = published
|
|
235
|
+
result["healthy"] = True
|
|
236
|
+
if expected_version:
|
|
237
|
+
result["expected_version"] = expected_version
|
|
238
|
+
result["version_match"] = published == expected_version
|
|
239
|
+
if published != expected_version:
|
|
240
|
+
result["warning"] = f"Version mismatch: published={published}, expected={expected_version}"
|
|
241
|
+
else:
|
|
242
|
+
result["error"] = proc.stderr.strip() or "npm view returned non-zero"
|
|
243
|
+
except FileNotFoundError:
|
|
244
|
+
result["error"] = "npm not found on PATH"
|
|
245
|
+
except subprocess.TimeoutExpired:
|
|
246
|
+
result["error"] = "npm view timed out after 15s"
|
|
247
|
+
except Exception as exc:
|
|
248
|
+
result["error"] = str(exc)
|
|
249
|
+
return result
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _extract_hostname(url: str) -> str:
|
|
253
|
+
"""Extract hostname from a URL."""
|
|
254
|
+
from urllib.parse import urlparse
|
|
255
|
+
return urlparse(url).hostname or ""
|
|
256
|
+
|
|
257
|
+
|
|
157
258
|
def verify(app: str, env: str, git_ref: Optional[str] = None) -> Dict[str, Any]:
|
|
158
|
-
"""Verify deployment health
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
259
|
+
"""Verify deployment health with real HTTP checks, SSL validation, and npm version.
|
|
260
|
+
|
|
261
|
+
Checks every deployment target for:
|
|
262
|
+
- HTTP 2xx reachability and response time
|
|
263
|
+
- SSL certificate validity (warns if expiring within 30 days)
|
|
264
|
+
- npm published version (for npm targets)
|
|
265
|
+
|
|
266
|
+
Also cross-references local deploy plan status when available.
|
|
267
|
+
"""
|
|
268
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
269
|
+
checks: List[Dict[str, Any]] = []
|
|
270
|
+
all_healthy = True
|
|
271
|
+
warnings: List[str] = []
|
|
272
|
+
|
|
273
|
+
for target in DEPLOY_TARGETS:
|
|
274
|
+
entry: Dict[str, Any] = {"name": target["name"], "kind": target["kind"]}
|
|
275
|
+
|
|
276
|
+
# HTTP health
|
|
277
|
+
http = _check_http_health(target["url"])
|
|
278
|
+
entry["http"] = http
|
|
279
|
+
if not http.get("healthy"):
|
|
280
|
+
all_healthy = False
|
|
281
|
+
|
|
282
|
+
# SSL cert check
|
|
283
|
+
hostname = _extract_hostname(target["url"])
|
|
284
|
+
if hostname:
|
|
285
|
+
ssl_result = _check_ssl_cert(hostname)
|
|
286
|
+
entry["ssl"] = ssl_result
|
|
287
|
+
if ssl_result.get("expiry_warning"):
|
|
288
|
+
warnings.append(ssl_result.get("warning", f"SSL expiry warning for {hostname}"))
|
|
289
|
+
if not ssl_result.get("ssl_valid"):
|
|
290
|
+
all_healthy = False
|
|
291
|
+
|
|
292
|
+
# npm version check (only for npm targets)
|
|
293
|
+
if target["kind"] == "npm":
|
|
294
|
+
npm_result = _check_npm_version()
|
|
295
|
+
entry["npm"] = npm_result
|
|
296
|
+
if not npm_result.get("healthy"):
|
|
297
|
+
all_healthy = False
|
|
298
|
+
|
|
299
|
+
checks.append(entry)
|
|
300
|
+
|
|
301
|
+
# Cross-reference deploy plan if one exists
|
|
302
|
+
plan_info: Optional[Dict[str, Any]] = None
|
|
303
|
+
plans = _list_plans(app=app or None, env=env or None)
|
|
304
|
+
if plans:
|
|
305
|
+
latest = plans[0]
|
|
306
|
+
plan_info = {
|
|
307
|
+
"plan_id": latest["plan_id"],
|
|
308
|
+
"plan_status": latest["status"],
|
|
309
|
+
"updated_at": latest.get("updated_at"),
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
result: Dict[str, Any] = {
|
|
313
|
+
"app": app or "all",
|
|
314
|
+
"env": env or "production",
|
|
315
|
+
"verified_at": now,
|
|
316
|
+
"healthy": all_healthy,
|
|
317
|
+
"targets_checked": len(checks),
|
|
318
|
+
"targets_healthy": sum(1 for c in checks if c.get("http", {}).get("healthy")),
|
|
319
|
+
"checks": checks,
|
|
170
320
|
}
|
|
321
|
+
if warnings:
|
|
322
|
+
result["warnings"] = warnings
|
|
323
|
+
if plan_info:
|
|
324
|
+
result["deploy_plan"] = plan_info
|
|
325
|
+
return result
|
|
171
326
|
|
|
172
327
|
|
|
173
328
|
def rollback(app: str, env: str, to_sha: Optional[str] = None) -> Dict[str, Any]:
|