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.
@@ -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 (fs.existsSync(gatewaySource)) {
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 (fs.existsSync(gatewaySource)) {
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 (stub returns plan status)."""
159
- plans = _list_plans(app=app, env=env)
160
- if not plans:
161
- return {"app": app, "env": env, "status": "no_deploys", "healthy": False}
162
- latest = plans[0]
163
- return {
164
- "app": app,
165
- "env": env,
166
- "plan_id": latest["plan_id"],
167
- "status": latest["status"],
168
- "healthy": latest["status"] in ("published", "planned"),
169
- "message": "Health check is a stub — no real endpoint verification yet.",
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]: