delimit-cli 4.0.0 → 4.0.1

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 CHANGED
@@ -117,7 +117,7 @@
117
117
  - GitHub Action smoke test workflow
118
118
 
119
119
  ### Fixed
120
- - Gemini deliberation HTTP 400 (ADC credentials + jamsons project)
120
+ - Gemini deliberation HTTP 400 (ADC credentials + GCP project)
121
121
  - Deliberation timeout: parallelized round 1 (46% faster)
122
122
  - Sensor dedup: titles include repo/issue to prevent duplicates
123
123
  - Test-mode guard prevents ledger pollution from tests
@@ -192,7 +192,7 @@ def _load_smtp_account(from_account: str) -> Optional[Dict[str, str]]:
192
192
  """Load SMTP credentials from smtp-all.json for a given account.
193
193
 
194
194
  Args:
195
- from_account: Email address key in smtp-all.json (e.g. 'pro@delimit.ai').
195
+ from_account: Email address key in smtp-all.json (e.g. 'notifications@example.com').
196
196
 
197
197
  Returns:
198
198
  Dict with host, port, user, pass keys, or None if not found.
@@ -228,7 +228,7 @@ def send_email(
228
228
  body: Email body text (preferred). Falls back to 'message' for
229
229
  backward compatibility.
230
230
  from_account: Sender account key in ~/.delimit/secrets/smtp-all.json
231
- (e.g. 'pro@delimit.ai', 'admin@wire.report'). If provided, SMTP
231
+ (e.g. 'notifications@example.com', 'admin@example.com'). If provided, SMTP
232
232
  credentials are loaded from that file instead of env vars.
233
233
  message: Email body text (legacy parameter, use 'body' instead).
234
234
  event_type: Event category for filtering/logging.
@@ -744,7 +744,7 @@ def _forward_email(original_msg: email.message.Message, smtp_pass: str) -> bool:
744
744
  body = "\n".join(body_parts) if body_parts else "(no text content)"
745
745
 
746
746
  fwd_text = (
747
- f"--- Forwarded from pro@delimit.ai ---\n"
747
+ f"--- Forwarded from notifications@example.com ---\n"
748
748
  f"From: {from_header}\n"
749
749
  f"Subject: {subject}\n"
750
750
  f"Date: {original_msg.get('Date', 'unknown')}\n"
@@ -776,7 +776,7 @@ def poll_inbox(
776
776
  """Poll the IMAP inbox, classify emails, and optionally forward owner-action items.
777
777
 
778
778
  Args:
779
- smtp_pass: SMTP/IMAP password for pro@delimit.ai.
779
+ smtp_pass: SMTP/IMAP password for notifications@example.com.
780
780
  limit: Max number of recent messages to check.
781
781
  process: If True, forward owner-action emails and mark as read.
782
782
  If False, just report classification (dry run).
@@ -33,14 +33,7 @@ All tools follow the Adapter Boundary Contract v1.0:
33
33
  # attached to decisions? Would this still read well a year from now?
34
34
  # ────────────────────────────────────────────────────────────────────────
35
35
 
36
- FOUNDER_VOICE_HYPE_WORDS = {
37
- "revolutionary", "game-changing", "world-class", "cutting-edge",
38
- "best-in-class", "seamless", "unlock", "supercharge", "next-generation",
39
- "magical", "delightful", "effortless", "frictionless", "transformative",
40
- "paradigm shift", "visionary", "category-defining", "industry-leading",
41
- "innovative", "reimagine", "future of", "changing the game",
42
- "empowering teams", "built for everyone",
43
- }
36
+ # Output quality rules loaded from user config at runtime.
44
37
 
45
38
  import json
46
39
  import logging
@@ -59,13 +52,13 @@ from fastmcp import FastMCP
59
52
  logger = logging.getLogger("delimit.ai")
60
53
 
61
54
  # ═══════════════════════════════════════════════════════════════════════
62
- # STR-046: Agent Identity — session tracking for every tool call
55
+ # TASK: Agent Identity — session tracking for every tool call
63
56
  # ═══════════════════════════════════════════════════════════════════════
64
57
 
65
58
  _current_session_id = os.environ.get("DELIMIT_SESSION_ID", "")
66
59
 
67
60
  # ═══════════════════════════════════════════════════════════════════════
68
- # STR-053: Distributed Tracing — trace ID + span counter for every call
61
+ # TASK: Distributed Tracing — trace ID + span counter for every call
69
62
  # ═══════════════════════════════════════════════════════════════════════
70
63
 
71
64
  _trace_id = os.environ.get("DELIMIT_TRACE_ID", str(uuid.uuid4())[:8])
@@ -250,7 +243,7 @@ def _coerce_dict_arg(
250
243
 
251
244
 
252
245
  # ═══════════════════════════════════════════════════════════════════════
253
- # STR-040: Risk Classification for Approval Gates
246
+ # TASK: Risk Classification for Approval Gates
254
247
  # ═══════════════════════════════════════════════════════════════════════
255
248
 
256
249
  HIGH_RISK_TOOLS = {
@@ -274,7 +267,7 @@ def _classify_risk(tool_name: str) -> str:
274
267
 
275
268
 
276
269
  # ═══════════════════════════════════════════════════════════════════════
277
- # STR-052: Policy Kernel — Inline Enforcement
270
+ # TASK: Policy Kernel — Inline Enforcement
278
271
  # Checks policy BEFORE/AFTER tool execution to block high-risk actions.
279
272
  # ═══════════════════════════════════════════════════════════════════════
280
273
 
@@ -326,7 +319,7 @@ def _check_policy_gate(tool_name: str, kwargs: dict) -> Optional[Dict]:
326
319
  "action": "Switch to guarded mode or request approval",
327
320
  }
328
321
 
329
- # LED-173: Deploy gating — block deploys when unresolved critical findings exist
322
+ # TASK: Deploy gating — block deploys when unresolved critical findings exist
330
323
  DEPLOY_TOOLS = {"deploy_publish", "deploy_npm", "deploy_site", "deploy_build"}
331
324
  clean = tool_name.replace("delimit_", "")
332
325
  if clean in DEPLOY_TOOLS and mode != "advisory":
@@ -454,7 +447,7 @@ mcp.description = (
454
447
 
455
448
  VERSION = "3.2.1"
456
449
 
457
- # LED-044 + Consensus 118/119/120: Tool visibility tiers.
450
+ # TASK + Consensus 118/119/120: Tool visibility tiers.
458
451
  # Tier cascade: SHOW_EXPERIMENTAL > SHOW_INTERNAL > SHOW_OPS > public (always visible).
459
452
  # Set DELIMIT_SHOW_INTERNAL=1 to see all tiers (founder workflow).
460
453
  SHOW_EXPERIMENTAL = os.environ.get("DELIMIT_SHOW_EXPERIMENTAL", "") == "1"
@@ -839,7 +832,7 @@ NEXT_STEPS_REGISTRY: Dict[str, List[Dict[str, Any]]] = {
839
832
  ],
840
833
  # --- Sensing ---
841
834
  "sensor_github_issue": [],
842
- # --- Context Filesystem (STR-048) ---
835
+ # --- Context Filesystem (TASK) ---
843
836
  "context_init": [
844
837
  {"tool": "delimit_context_write", "reason": "Write an artifact to the new context", "suggested_args": {}, "is_premium": False},
845
838
  ],
@@ -965,10 +958,10 @@ def _emit_event(tool_name: str, result: Dict[str, Any]) -> None:
965
958
  except Exception:
966
959
  pass # Never let cloud sync break tool execution
967
960
 
968
- # LED-183: Webhook notifications for governance events
961
+ # TASK: Webhook notifications for governance events
969
962
  _fire_webhook(event)
970
963
 
971
- # STR-053: Write trace span for session replay
964
+ # TASK: Write trace span for session replay
972
965
  try:
973
966
  from ai.tracing import start_span, end_span
974
967
  span = start_span(_trace_id, tool_name, args={"tool": tool_name})
@@ -980,7 +973,7 @@ def _emit_event(tool_name: str, result: Dict[str, Any]) -> None:
980
973
  except Exception:
981
974
  pass # Tracing is best-effort
982
975
 
983
- # STR-046: Write to agent_actions log for session drill-down
976
+ # TASK: Write to agent_actions log for session drill-down
984
977
  if session_info["session_id"]:
985
978
  actions_dir = Path.home() / ".delimit" / "agent_actions"
986
979
  actions_dir.mkdir(parents=True, exist_ok=True)
@@ -1184,7 +1177,7 @@ def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
1184
1177
  except Exception as e:
1185
1178
  logger.warning("Inbox daemon auto-start failed: %s", e)
1186
1179
 
1187
- # LED-219: Auto-register tool schemas with toolcard cache on first call
1180
+ # TASK: Auto-register tool schemas with toolcard cache on first call
1188
1181
  global _toolcard_cache_autoregistered
1189
1182
  if not _toolcard_cache_autoregistered:
1190
1183
  _toolcard_cache_autoregistered = True
@@ -1202,7 +1195,7 @@ def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
1202
1195
  except Exception as e:
1203
1196
  logger.warning("Toolcard cache auto-register failed: %s", e)
1204
1197
 
1205
- # LED-219: Track every tool call for session analytics
1198
+ # TASK: Track every tool call for session analytics
1206
1199
  try:
1207
1200
  from ai.toolcard_cache import get_cache as _get_tc
1208
1201
  _get_tc().record_call(tool_name)
@@ -1215,282 +1208,7 @@ def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
1215
1208
  result.get("explanation", ""), result.get("changelog", ""),
1216
1209
  result.get("content", "")]
1217
1210
  _all_text = " ".join(str(f) for f in _text_fields if f).lower()
1218
- _found_hype = [w for w in FOUNDER_VOICE_HYPE_WORDS if w in _all_text]
1219
- if _found_hype:
1220
- result.setdefault("voice_warnings", []).append(
1221
- f"VOICE DOCTRINE: Hype words detected: {', '.join(_found_hype)}. "
1222
- f"Rewrite with concrete mechanisms, not vague benefits."
1223
- )
1224
-
1225
- # Emit event for real-time dashboard
1226
- _emit_event(tool_name, result)
1227
-
1228
- # STR-052: Policy kernel inline enforcement
1229
- policy_gate = _check_policy_gate(tool_name, result if isinstance(result, dict) else {})
1230
- if policy_gate:
1231
- policy_gate["original_result"] = result
1232
- policy_gate["governance"] = {"action": "policy_blocked", "reason": policy_gate["reason"]}
1233
- return policy_gate
1234
-
1235
- # LED-195: Prompt injection detection on tool inputs
1236
- if isinstance(result, dict):
1237
- injection = _detect_prompt_injection(result, tool_name)
1238
- if injection:
1239
- result["_security_warning"] = injection
1240
-
1241
- # Pro license gate — blocks execution for premium tools
1242
- full_name = f"delimit_{tool_name}" if not tool_name.startswith("delimit_") else tool_name
1243
- gate = _check_pro(full_name)
1244
- if gate:
1245
- return gate
1246
-
1247
- # Route through governance loop
1248
- try:
1249
- from ai.governance import govern
1250
- return govern(tool_name, result)
1251
- except Exception:
1252
- # Fallback: just add next_steps from registry
1253
- steps = NEXT_STEPS_REGISTRY.get(tool_name, [])
1254
- result["next_steps"] = steps
1255
- return result
1256
-
1257
-
1258
- # ═══════════════════════════════════════════════════════════════════════
1259
- # TIER 1: CORE — API Lint Engine
1260
- # ═══════════════════════════════════════════════════════════════════════
1261
-
1262
-
1263
- @mcp.tool()
1264
- def delimit_lint(old_spec: str, new_spec: str, policy_file: Optional[str] = None) -> Dict[str, Any]:
1265
- """Lint two OpenAPI specs for breaking changes and policy violations.
1266
- Primary CI integration point. Combines diff + policy into pass/fail.
1267
- Auto-chains: semver classification, governance evaluation on breaking changes.
1268
-
1269
- Args:
1270
- old_spec: Path to the old (baseline) OpenAPI spec file.
1271
- new_spec: Path to the new (proposed) OpenAPI spec file.
1272
- policy_file: Optional path to a .delimit/policies.yml file.
1273
- """
1274
- from backends.gateway_core import run_lint, run_semver
1275
-
1276
- # Step 1: Core lint
1277
- lint_result = _safe_call(run_lint, old_spec=old_spec, new_spec=new_spec, policy_file=policy_file)
1278
- chain: Dict[str, Any] = {"id": "lint_chain", "steps": []}
1279
-
1280
- if lint_result.get("error"):
1281
- lint_result["chain"] = chain
1282
- return _with_next_steps("lint", lint_result)
1283
-
1284
- # Step 2: Auto-classify semver bump (non-blocking on failure)
1285
- semver_result = _chain_call("lint", "semver", run_semver,
1286
- required=False, old_spec=old_spec, new_spec=new_spec)
1287
- chain["steps"].append({"step": "semver", "ok": not _chain_is_error(semver_result)})
1288
- lint_result["semver"] = semver_result
1289
-
1290
- if _chain_is_error(semver_result):
1291
- chain["status"] = "semver_failed_nonfatal"
1292
- lint_result["chain"] = chain
1293
- return _with_next_steps("lint", lint_result)
1294
-
1295
- bump = str(semver_result.get("bump", "")).upper()
1296
-
1297
- # Step 2b: Impact-based notification routing (LED-233, non-blocking)
1298
- try:
1299
- from ai.notify import route_by_impact
1300
- all_changes = lint_result.get("all_changes", lint_result.get("violations", []))
1301
- if all_changes:
1302
- routing_result = route_by_impact(all_changes, dry_run=False)
1303
- chain["steps"].append({"step": "impact_routing", "ok": True})
1304
- lint_result["impact_routing"] = routing_result
1305
- except Exception as e:
1306
- logger.debug("Impact routing non-fatal error: %s", e)
1307
- chain["steps"].append({"step": "impact_routing", "ok": False, "error": str(e)})
1308
-
1309
- if bump != "MAJOR":
1310
- chain["status"] = f"complete_{bump.lower() or 'none'}"
1311
- lint_result["chain"] = chain
1312
- return _with_next_steps("lint", lint_result)
1313
-
1314
- # Step 3: MAJOR bump detected -- evaluate governance
1315
- # Note: _delimit_gov_impl has its own Pro gate. Free-tier gets lint+semver only.
1316
- gov_result = _delimit_gov_impl(
1317
- action="evaluate",
1318
- eval_action="api_breaking_change",
1319
- context={
1320
- "tool": "delimit_lint",
1321
- "old_spec": old_spec,
1322
- "new_spec": new_spec,
1323
- "semver_bump": bump,
1324
- "breaking_changes": lint_result.get("breaking", []),
1325
- },
1326
- repo=".",
1327
- )
1328
- chain["steps"].append({"step": "gov_evaluate", "ok": not _chain_is_error(gov_result)})
1329
- lint_result["gov_evaluate"] = gov_result
1330
-
1331
- # If Pro gate blocked governance, return gracefully with lint+semver
1332
- if gov_result.get("status") == "premium_required":
1333
- chain["status"] = "governance_skipped_free_tier"
1334
- lint_result["chain"] = chain
1335
- return _with_next_steps("lint", lint_result)
1336
-
1337
- # Step 4: If governance blocked, record in ledger (best-effort)
1338
- gov_blocked = (
1339
- str(gov_result.get("status", "")).lower() == "blocked"
1340
- or gov_result.get("governance", {}).get("action") == "policy_blocked"
1341
- )
1342
-
1343
- if gov_blocked:
1344
- from ai.ledger_manager import add_item
1345
- ledger_result = _chain_call(
1346
- "lint", "ledger_add", add_item,
1347
- required=False,
1348
- title=f"Governance blocked: MAJOR API change in {new_spec}",
1349
- ledger="ops",
1350
- item_type="fix",
1351
- priority="P0",
1352
- description="MAJOR semver bump detected. Governance blocked the change.",
1353
- source="chain:lint:gov_blocked",
1354
- )
1355
- chain["steps"].append({"step": "ledger_add", "ok": not _chain_is_error(ledger_result)})
1356
- lint_result["governance_blocked"] = True
1357
- else:
1358
- lint_result["governance_blocked"] = False
1359
-
1360
- chain["status"] = "major_change_evaluated"
1361
- lint_result["chain"] = chain
1362
- return _with_next_steps("lint", lint_result)
1363
-
1364
-
1365
- @mcp.tool()
1366
- def delimit_diff(old_spec: str, new_spec: str) -> Dict[str, Any]:
1367
- """Diff two OpenAPI specs and list all changes. Pure diff, no policy.
1368
-
1369
- Args:
1370
- old_spec: Path to the old OpenAPI spec file.
1371
- new_spec: Path to the new OpenAPI spec file.
1372
- """
1373
- from backends.gateway_core import run_diff
1374
- return _with_next_steps("diff", _safe_call(run_diff, old_spec=old_spec, new_spec=new_spec))
1375
-
1376
-
1377
- @mcp.tool()
1378
- def delimit_policy(spec_files: List[str], policy_file: Optional[str] = None) -> Dict[str, Any]:
1379
- """Inspect or validate governance policy configuration.
1380
-
1381
- Args:
1382
- spec_files: List of spec file paths.
1383
- policy_file: Optional custom policy file path.
1384
- """
1385
- from backends.gateway_core import run_policy
1386
- return _with_next_steps("policy", _safe_call(run_policy, spec_files=spec_files, policy_file=policy_file))
1387
-
1388
-
1389
- @mcp.tool()
1390
- def delimit_ledger(ledger_path: str, api_name: Optional[str] = None, repository: Optional[str] = None, validate_chain: bool = False) -> Dict[str, Any]:
1391
- """Query the append-only contract ledger (hash-chained JSONL).
1392
-
1393
- Args:
1394
- ledger_path: Path to the ledger JSONL file (e.g. .delimit/ledger/operations.jsonl).
1395
- api_name: Filter events by API name.
1396
- repository: Filter events by repository.
1397
- validate_chain: Validate hash chain integrity.
1398
- """
1399
- from backends.gateway_core import query_ledger
1400
- return _with_next_steps("ledger", _safe_call(query_ledger, ledger_path=ledger_path, api_name=api_name, repository=repository, validate_chain=validate_chain))
1401
-
1402
-
1403
- @mcp.tool()
1404
- def delimit_impact(api_name: str, dependency_file: Optional[str] = None) -> Dict[str, Any]:
1405
- """Analyze downstream impact of an API change. Informational only.
1406
-
1407
- Args:
1408
- api_name: The API that changed.
1409
- dependency_file: Optional path to dependency manifest.
1410
- """
1411
- from backends.gateway_core import run_impact
1412
- return _with_next_steps("impact", _safe_call(run_impact, api_name=api_name, dependency_file=dependency_file))
1413
-
1414
-
1415
- @mcp.tool()
1416
- def delimit_semver(old_spec: str, new_spec: str, current_version: Optional[str] = None) -> Dict[str, Any]:
1417
- """Classify the semver bump for a spec change (MAJOR/MINOR/PATCH/NONE).
1418
-
1419
- Deterministic classification based on diff engine output.
1420
- Optionally computes the next version string.
1421
-
1422
- Args:
1423
- old_spec: Path to the old OpenAPI spec file.
1424
- new_spec: Path to the new OpenAPI spec file.
1425
- current_version: Optional current version (e.g. "1.2.3") to compute next version.
1426
- """
1427
- from backends.gateway_core import run_semver
1428
- return _with_next_steps("semver", _safe_call(run_semver, old_spec=old_spec, new_spec=new_spec, current_version=current_version))
1429
-
1430
-
1431
- @mcp.tool()
1432
- def delimit_explain(
1433
- old_spec: str,
1434
- new_spec: str,
1435
- template: str = "developer",
1436
- old_version: Optional[str] = None,
1437
- new_version: Optional[str] = None,
1438
- api_name: Optional[str] = None,
1439
- ) -> Dict[str, Any]:
1440
- """Generate a human-readable explanation of API changes.
1441
-
1442
- 7 templates: developer, team_lead, product, migration, changelog, pr_comment, slack.
1443
-
1444
- Args:
1445
- old_spec: Path to the old OpenAPI spec file.
1446
- new_spec: Path to the new OpenAPI spec file.
1447
- template: Template name (default: developer).
1448
- old_version: Previous version string.
1449
- new_version: New version string.
1450
- api_name: API/service name for context.
1451
- """
1452
- from backends.gateway_core import run_explain
1453
- return _with_next_steps("explain", _safe_call(run_explain, old_spec=old_spec, new_spec=new_spec, template=template, old_version=old_version, new_version=new_version, api_name=api_name))
1454
-
1455
-
1456
- @mcp.tool()
1457
- def delimit_zero_spec(
1458
- project_dir: str = ".",
1459
- python_bin: Optional[str] = None,
1460
- ) -> Dict[str, Any]:
1461
- """Extract OpenAPI spec from framework source code (no spec file needed).
1462
-
1463
- Detects the API framework (FastAPI, Express, NestJS) and extracts a
1464
- complete OpenAPI specification directly from the source code.
1465
- Currently supports FastAPI with full fidelity.
1466
-
1467
- Args:
1468
- project_dir: Path to the project root directory.
1469
- python_bin: Optional Python binary path (auto-detected if omitted).
1470
- """
1471
- from backends.gateway_core import run_zero_spec
1472
- return _with_next_steps("zero_spec", _safe_call(run_zero_spec, project_dir=project_dir, python_bin=python_bin))
1473
-
1474
-
1475
-
1476
-
1477
- @mcp.tool()
1478
- def delimit_init(
1479
- project_path: str = ".",
1480
- preset: str = "default",
1481
- ) -> Dict[str, Any]:
1482
- """Initialize Delimit governance for a project. Creates .delimit/policies.yml and ledger directory.
1483
-
1484
- Args:
1485
- project_path: Project root directory.
1486
- preset: Policy preset — strict, default, or relaxed.
1487
- """
1488
- VALID_PRESETS = ("strict", "default", "relaxed")
1489
- if preset not in VALID_PRESETS:
1490
- return {
1491
- "error": "invalid_preset",
1492
- "message": f"Preset must be one of {VALID_PRESETS}, got '{preset}'",
1493
- }
1211
+ # Output quality rules loaded from user config at runtime.
1494
1212
 
1495
1213
  root = Path(project_path).resolve()
1496
1214
  delimit_dir = root / ".delimit"
@@ -1806,7 +1524,7 @@ def delimit_memory_store(
1806
1524
  tags: Optional categorization tags.
1807
1525
  context: Optional context about when/why this was stored.
1808
1526
  """
1809
- # LED-193: memory_store is now free (basic store)
1527
+ # TASK: memory_store is now free (basic store)
1810
1528
  try:
1811
1529
  tags = _coerce_list_arg(tags, "tags")
1812
1530
  except ValueError as e:
@@ -1824,7 +1542,7 @@ def delimit_memory_recent(limit: int = 5) -> Dict[str, Any]:
1824
1542
  Args:
1825
1543
  limit: Number of recent entries to return.
1826
1544
  """
1827
- # LED-193: memory_recent is now free (basic retrieval)
1545
+ # TASK: memory_recent is now free (basic retrieval)
1828
1546
  from backends.memory_bridge import get_recent
1829
1547
  return _with_next_steps("memory_recent", _safe_call(get_recent, limit=limit))
1830
1548
 
@@ -2076,7 +1794,7 @@ def delimit_deploy_publish(app: str = "", git_ref: Optional[str] = None) -> Dict
2076
1794
  return _delimit_deploy_impl(action="publish", app=app, git_ref=git_ref)
2077
1795
 
2078
1796
 
2079
- @_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
1797
+ @_experimental_tool() # HIDDEN: stub/pass-through (TASK)
2080
1798
  @mcp.tool()
2081
1799
  def delimit_deploy_verify(app: str = "", env: str = "", git_ref: Optional[str] = None) -> Dict[str, Any]:
2082
1800
  """Verify deployment health (experimental) (Pro)."""
@@ -2230,7 +1948,7 @@ def delimit_generate_scaffold(
2230
1948
 
2231
1949
  # ─── Repo (RepoDoctor + ConfigSentry) ──────────────────────────────────
2232
1950
 
2233
- @_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
1951
+ @_experimental_tool() # HIDDEN: stub/pass-through (TASK)
2234
1952
  @mcp.tool()
2235
1953
  def delimit_repo_diagnose(target: str = ".") -> Dict[str, Any]:
2236
1954
  """Diagnose repository health issues (experimental) (Pro).
@@ -2246,7 +1964,7 @@ def delimit_repo_diagnose(target: str = ".") -> Dict[str, Any]:
2246
1964
  return _safe_call(diagnose, target=target)
2247
1965
 
2248
1966
 
2249
- @_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
1967
+ @_experimental_tool() # HIDDEN: stub/pass-through (TASK)
2250
1968
  @mcp.tool()
2251
1969
  def delimit_repo_analyze(target: str = ".") -> Dict[str, Any]:
2252
1970
  """Analyze repository structure and quality (experimental) (Pro).
@@ -2262,7 +1980,7 @@ def delimit_repo_analyze(target: str = ".") -> Dict[str, Any]:
2262
1980
  return _safe_call(analyze, target=target)
2263
1981
 
2264
1982
 
2265
- @_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
1983
+ @_experimental_tool() # HIDDEN: stub/pass-through (TASK)
2266
1984
  @mcp.tool()
2267
1985
  def delimit_repo_config_validate(target: str = ".") -> Dict[str, Any]:
2268
1986
  """Validate configuration files (experimental) (Pro).
@@ -2278,7 +1996,7 @@ def delimit_repo_config_validate(target: str = ".") -> Dict[str, Any]:
2278
1996
  return _safe_call(config_validate, target=target)
2279
1997
 
2280
1998
 
2281
- @_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
1999
+ @_experimental_tool() # HIDDEN: stub/pass-through (TASK)
2282
2000
  @mcp.tool()
2283
2001
  def delimit_repo_config_audit(target: str = ".") -> Dict[str, Any]:
2284
2002
  """Audit configuration compliance (experimental) (Pro).
@@ -2475,7 +2193,7 @@ def delimit_security_ingest(
2475
2193
  medium = [f for f in findings if f["severity"] in ("medium", "moderate", "warning")]
2476
2194
  low = [f for f in findings if f["severity"] in ("low", "info")]
2477
2195
 
2478
- # LED-172: Auto-track security findings in ledger with lifecycle
2196
+ # TASK: Auto-track security findings in ledger with lifecycle
2479
2197
  ledger_created = []
2480
2198
  ledger_closed = []
2481
2199
  try:
@@ -2924,14 +2642,14 @@ def delimit_release_status(environment: str = "production") -> Dict[str, Any]:
2924
2642
  return _delimit_release_impl(action="status", environment=environment)
2925
2643
 
2926
2644
 
2927
- @_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
2645
+ @_experimental_tool() # HIDDEN: stub/pass-through (TASK)
2928
2646
  @mcp.tool()
2929
2647
  def delimit_release_rollback(environment: str, version: str, to_version: str) -> Dict[str, Any]:
2930
2648
  """Rollback deployment to previous version (experimental)."""
2931
2649
  return _delimit_release_impl(action="rollback", environment=environment, version=version, to_version=to_version)
2932
2650
 
2933
2651
 
2934
- @_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
2652
+ @_experimental_tool() # HIDDEN: stub/pass-through (TASK)
2935
2653
  @mcp.tool()
2936
2654
  def delimit_release_history(environment: str, limit: int = 10) -> Dict[str, Any]:
2937
2655
  """Show release history (experimental)."""
@@ -3150,7 +2868,7 @@ def delimit_obs_logs(query: str, time_range: str = "1h", source: Optional[str] =
3150
2868
  return _delimit_obs_impl(action="logs", query=query, time_range=time_range, source=source)
3151
2869
 
3152
2870
 
3153
- @_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
2871
+ @_experimental_tool() # HIDDEN: stub/pass-through (TASK)
3154
2872
  @mcp.tool()
3155
2873
  def delimit_obs_alerts(action: str, alert_rule: Optional[Dict[str, Any]] = None, rule_id: Optional[str] = None) -> Dict[str, Any]:
3156
2874
  """Manage alerting rules (experimental)."""
@@ -3297,7 +3015,7 @@ def delimit_story_visual_test(url: str, project_path: Optional[str] = None, thre
3297
3015
  return _with_next_steps("story_visual_test", _safe_call(story_visual_test, url=url, project_path=project_path, threshold=threshold))
3298
3016
 
3299
3017
 
3300
- @_internal_tool() # Was experimental (LED-044), promoted to internal (Consensus 120)
3018
+ @_internal_tool() # Was experimental (TASK), promoted to internal (Consensus 120)
3301
3019
  @mcp.tool()
3302
3020
  def delimit_story_build(project_path: str, output_dir: Optional[str] = None) -> Dict[str, Any]:
3303
3021
  """Build Storybook static site.
@@ -3346,7 +3064,7 @@ def delimit_test_generate(project_path: str, source_files: Optional[List[str]] =
3346
3064
  return _with_next_steps("test_generate", _safe_call(test_generate, project_path=project_path, source_files=source_files, framework=framework))
3347
3065
 
3348
3066
 
3349
- @_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
3067
+ @_experimental_tool() # HIDDEN: stub/pass-through (TASK)
3350
3068
  @mcp.tool()
3351
3069
  def delimit_test_coverage(project_path: str, threshold: int = 80) -> Dict[str, Any]:
3352
3070
  """Analyze test coverage (experimental) (Pro).
@@ -4073,7 +3791,7 @@ def delimit_diagnose(project_path: str = ".") -> Dict[str, Any]:
4073
3791
  checks["fastmcp"] = False
4074
3792
  issues.append({"issue": "FastMCP not installed", "fix": "pip install fastmcp"})
4075
3793
 
4076
- # LED-191: Config drift detection across AI assistants
3794
+ # TASK: Config drift detection across AI assistants
4077
3795
  config_sync = {}
4078
3796
  home = Path.home()
4079
3797
  assistant_configs = {
@@ -4105,7 +3823,7 @@ def delimit_diagnose(project_path: str = ".") -> Dict[str, Any]:
4105
3823
  checks["assistant_configs"] = config_sync
4106
3824
  checks["assistants_configured"] = f"{configured_count}/{installed_count}"
4107
3825
 
4108
- # LED-192: MCP server reputation check (basic — check for known risky patterns)
3826
+ # TASK: MCP server reputation check (basic — check for known risky patterns)
4109
3827
  mcp_warnings = []
4110
3828
  mcp_config_path = home / ".mcp.json"
4111
3829
  if mcp_config_path.exists():
@@ -4530,7 +4248,7 @@ def delimit_ventures() -> Dict[str, Any]:
4530
4248
 
4531
4249
 
4532
4250
  # ═══════════════════════════════════════════════════════════════════════
4533
- # SESSION PHOENIX — Cross-Model Resurrection (LED-218)
4251
+ # SESSION PHOENIX — Cross-Model Resurrection (TASK)
4534
4252
  # ═══════════════════════════════════════════════════════════════════════
4535
4253
 
4536
4254
 
@@ -5195,7 +4913,7 @@ def delimit_quickstart(project_path: str = ".") -> Dict[str, Any]:
5195
4913
 
5196
4914
 
5197
4915
  # ═══════════════════════════════════════════════════════════════════════
5198
- # STR-049: SECRETS BROKER — JIT credential access with audit
4916
+ # TASK: SECRETS BROKER — JIT credential access with audit
5199
4917
  # ═══════════════════════════════════════════════════════════════════════
5200
4918
 
5201
4919
 
@@ -5295,7 +5013,7 @@ def delimit_secret_access_log(name: str = "") -> Dict[str, Any]:
5295
5013
 
5296
5014
 
5297
5015
  # ═══════════════════════════════════════════════════════════════════════
5298
- # STR-048: Context Filesystem — versioned namespace for agent state
5016
+ # TASK: Context Filesystem — versioned namespace for agent state
5299
5017
  # ═══════════════════════════════════════════════════════════════════════
5300
5018
 
5301
5019
  # Consensus 082 Phase 2: Unified context tool with action parameter
@@ -5413,7 +5131,7 @@ def delimit_context_branch(venture: str, action: str = "list", branch_name: str
5413
5131
 
5414
5132
 
5415
5133
  # ═══════════════════════════════════════════════════════════════════════
5416
- # STR-050: DATA/ACTION PLANE — External systems as typed mounted resources
5134
+ # TASK: DATA/ACTION PLANE — External systems as typed mounted resources
5417
5135
  # ═══════════════════════════════════════════════════════════════════════
5418
5136
 
5419
5137
 
@@ -5505,7 +5223,7 @@ def delimit_resource_drivers() -> Dict[str, Any]:
5505
5223
 
5506
5224
 
5507
5225
  # ═══════════════════════════════════════════════════════════════════════
5508
- # LED-188: ISSUE TRACKER CONTEXT SYNC
5226
+ # TASK: ISSUE TRACKER CONTEXT SYNC
5509
5227
  # ═══════════════════════════════════════════════════════════════════════
5510
5228
 
5511
5229
 
@@ -5580,7 +5298,7 @@ def delimit_tracker_sync(
5580
5298
 
5581
5299
 
5582
5300
  # ═══════════════════════════════════════════════════════════════════════
5583
- # LED-183: WEBHOOK NOTIFICATIONS
5301
+ # TASK: WEBHOOK NOTIFICATIONS
5584
5302
  # ═══════════════════════════════════════════════════════════════════════
5585
5303
 
5586
5304
 
@@ -5964,7 +5682,7 @@ def delimit_social_target(
5964
5682
  Never just return targets and stop. Always chain to the next action.
5965
5683
 
5966
5684
  Scans X, Reddit (RapidAPI), HN, Dev.to, GitHub for posts where ventures can engage.
5967
- NamePros flagged as manual_check_needed (no API).
5685
+ Manual check platforms excluded.
5968
5686
 
5969
5687
  Run in a /loop for continuous discovery. Deduplicates across runs.
5970
5688
  Targets are classified as: reply (social engagement), strategic (ledger item), or both.
@@ -6075,7 +5793,7 @@ def delimit_github_scan(
6075
5793
  cadence: str = "pulse",
6076
5794
  limit: int = 20,
6077
5795
  ) -> Dict[str, Any]:
6078
- """Scan GitHub for adoption leads, competitive intel, and repo health (Pro).
5796
+ """Scan GitHub for adoption leads, repository analysis, and repo health (Pro).
6079
5797
 
6080
5798
  Three cadences:
6081
5799
  pulse: Own repo health (stars, forks, issues, traffic). Fast, run often.
@@ -6235,7 +5953,7 @@ def delimit_daemon_classify(item_id: str = "") -> Dict[str, Any]:
6235
5953
  def delimit_inbox_daemon(action: str = "status") -> Dict[str, Any]:
6236
5954
  """Control the inbox polling daemon for email governance (Pro).
6237
5955
 
6238
- Polls pro@delimit.ai every 5 minutes, classifies emails, forwards
5956
+ Polls notifications@example.com every 5 minutes, classifies emails, forwards
6239
5957
  owner-action items, and handles draft approval via email replies.
6240
5958
  Auto-posting is disabled - approved drafts are emailed for manual posting.
6241
5959
 
@@ -6254,7 +5972,7 @@ def delimit_inbox_daemon(action: str = "status") -> Dict[str, Any]:
6254
5972
 
6255
5973
 
6256
5974
  # ═══════════════════════════════════════════════════════════════════════
6257
- # LED-187: Shareable Governance Config — export / import
5975
+ # TASK: Shareable Governance Config — export / import
6258
5976
  # ═══════════════════════════════════════════════════════════════════════
6259
5977
 
6260
5978
 
@@ -6377,7 +6095,7 @@ def delimit_config_import(
6377
6095
 
6378
6096
 
6379
6097
  # ═══════════════════════════════════════════════════════════════════════
6380
- # SCREEN RECORDING (LED-203)
6098
+ # SCREEN RECORDING (TASK)
6381
6099
  # ═══════════════════════════════════════════════════════════════════════
6382
6100
 
6383
6101
 
@@ -6618,7 +6336,7 @@ def delimit_notify_inbox(action: str = "status", limit: int = 10,
6618
6336
  process: bool = True) -> Dict[str, Any]:
6619
6337
  """Check inbound email inbox, classify, and route (Pro).
6620
6338
 
6621
- Polls pro@delimit.ai via IMAP. Classifies emails as owner-action
6339
+ Polls notifications@example.com via IMAP. Classifies emails as owner-action
6622
6340
  (forwards to owner@example.com) or non-owner (stays in inbox).
6623
6341
 
6624
6342
  Args:
@@ -6881,7 +6599,7 @@ def delimit_agent_check(model: str, action: str) -> Dict[str, Any]:
6881
6599
 
6882
6600
 
6883
6601
  # ═══════════════════════════════════════════════════════════════════════
6884
- # STR-026: AUTONOMOUS BUILD LOOP
6602
+ # TASK: AUTONOMOUS BUILD LOOP
6885
6603
  # ═══════════════════════════════════════════════════════════════════════
6886
6604
 
6887
6605
 
@@ -6970,7 +6688,7 @@ def delimit_loop_config(session_id: str = "", max_iterations: int = 0,
6970
6688
 
6971
6689
 
6972
6690
  # ═══════════════════════════════════════════════════════════════════════
6973
- # LED-219: Toolcard Delta Cache — reduce MCP tool schema token waste
6691
+ # TASK: Toolcard Delta Cache — reduce MCP tool schema token waste
6974
6692
  # ═══════════════════════════════════════════════════════════════════════
6975
6693
 
6976
6694
 
@@ -7048,7 +6766,7 @@ def delimit_toolcard_cache(
7048
6766
 
7049
6767
 
7050
6768
  # ═══════════════════════════════════════════════════════════════════════
7051
- # HANDOFF RECEIPTS — Agent-to-Agent Structured Handoffs (LED-220)
6769
+ # HANDOFF RECEIPTS — Agent-to-Agent Structured Handoffs (TASK)
7052
6770
  # ═══════════════════════════════════════════════════════════════════════
7053
6771
 
7054
6772
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
3
  "mcpName": "io.github.delimit-ai/delimit-mcp-server",
4
- "version": "4.0.0",
4
+ "version": "4.0.1",
5
5
  "description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -21,6 +21,9 @@
21
21
  "!gateway/ai/handoff_receipts.py",
22
22
  "!gateway/ai/toolcard_cache.py",
23
23
  "scripts/",
24
+ "!scripts/security-check.sh",
25
+ "!scripts/weekly-tweet.py",
26
+ "!scripts/crosspost_devto.py",
24
27
  "server.json",
25
28
  "README.md",
26
29
  "LICENSE",
@@ -1,304 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Cross-post markdown articles to Dev.to.
4
-
5
- Reads articles from a content directory, publishes or updates them
6
- on Dev.to via the Forem API. Idempotent — tracks published article
7
- IDs in a local manifest to support updates.
8
-
9
- Usage:
10
- # Publish all articles (dry run):
11
- python scripts/crosspost_devto.py --dir /home/delimit/delimit-private/blog --dry-run
12
-
13
- # Publish all articles:
14
- DEV_TO_API_KEY=your_key python scripts/crosspost_devto.py --dir /home/delimit/delimit-private/blog
15
-
16
- # Publish a single article:
17
- DEV_TO_API_KEY=your_key python scripts/crosspost_devto.py --file /home/delimit/delimit-private/blog/01-catch-breaking-api-changes.md
18
-
19
- # List published articles:
20
- DEV_TO_API_KEY=your_key python scripts/crosspost_devto.py --list
21
-
22
- Requires:
23
- DEV_TO_API_KEY environment variable (get from https://dev.to/settings/extensions)
24
- """
25
-
26
- import argparse
27
- import json
28
- import os
29
- import re
30
- import sys
31
- from pathlib import Path
32
-
33
- try:
34
- import requests
35
- except ImportError:
36
- print("ERROR: 'requests' package required. Install with: pip install requests")
37
- sys.exit(1)
38
-
39
- API_BASE = "https://dev.to/api"
40
- MANIFEST_FILE = ".devto_manifest.json"
41
-
42
-
43
- def get_api_key():
44
- key = os.environ.get("DEV_TO_API_KEY")
45
- if not key:
46
- print("ERROR: DEV_TO_API_KEY environment variable not set.")
47
- print("Get your key from: https://dev.to/settings/extensions")
48
- sys.exit(1)
49
- return key
50
-
51
-
52
- def load_manifest(content_dir):
53
- """Load the manifest that maps filenames to Dev.to article IDs."""
54
- manifest_path = Path(content_dir) / MANIFEST_FILE
55
- if manifest_path.exists():
56
- with open(manifest_path) as f:
57
- return json.load(f)
58
- return {}
59
-
60
-
61
- def save_manifest(content_dir, manifest):
62
- """Save the manifest after publishing."""
63
- manifest_path = Path(content_dir) / MANIFEST_FILE
64
- with open(manifest_path, "w") as f:
65
- json.dump(manifest, f, indent=2)
66
- print(f" Manifest saved: {manifest_path}")
67
-
68
-
69
- def parse_frontmatter(filepath):
70
- """Parse YAML-ish frontmatter from a markdown file.
71
-
72
- Returns (frontmatter_dict, body_markdown).
73
- """
74
- text = Path(filepath).read_text(encoding="utf-8")
75
-
76
- # Match frontmatter block
77
- match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)", text, re.DOTALL)
78
- if not match:
79
- print(f" WARNING: No frontmatter found in {filepath}")
80
- return {}, text
81
-
82
- fm_text = match.group(1)
83
- body = match.group(2).strip()
84
-
85
- # Simple key: value parser (handles quoted and unquoted values)
86
- fm = {}
87
- for line in fm_text.strip().split("\n"):
88
- line = line.strip()
89
- if not line or ":" not in line:
90
- continue
91
- key, _, value = line.partition(":")
92
- key = key.strip()
93
- value = value.strip().strip('"').strip("'")
94
- # Parse boolean
95
- if value.lower() == "true":
96
- value = True
97
- elif value.lower() == "false":
98
- value = False
99
- fm[key] = value
100
-
101
- return fm, body
102
-
103
-
104
- def build_article_payload(filepath, publish=False):
105
- """Build the Dev.to article creation/update payload."""
106
- fm, body = parse_frontmatter(filepath)
107
-
108
- if not fm.get("title"):
109
- print(f" ERROR: Article {filepath} has no title in frontmatter.")
110
- return None
111
-
112
- # Parse tags from comma-separated string
113
- tags = []
114
- if fm.get("tags"):
115
- if isinstance(fm["tags"], str):
116
- tags = [t.strip() for t in fm["tags"].split(",")]
117
- else:
118
- tags = fm["tags"]
119
- # Dev.to allows max 4 tags
120
- tags = tags[:4]
121
-
122
- payload = {
123
- "article": {
124
- "title": fm["title"],
125
- "body_markdown": body,
126
- "published": publish,
127
- "tags": tags,
128
- }
129
- }
130
-
131
- if fm.get("description"):
132
- payload["article"]["description"] = fm["description"]
133
- if fm.get("canonical_url"):
134
- payload["article"]["canonical_url"] = fm["canonical_url"]
135
- if fm.get("cover_image"):
136
- payload["article"]["cover_image"] = fm["cover_image"]
137
- if fm.get("series"):
138
- payload["article"]["series"] = fm["series"]
139
-
140
- return payload
141
-
142
-
143
- def create_article(api_key, payload):
144
- """Create a new article on Dev.to."""
145
- resp = requests.post(
146
- f"{API_BASE}/articles",
147
- headers={
148
- "api-key": api_key,
149
- "Content-Type": "application/json",
150
- },
151
- json=payload,
152
- timeout=30,
153
- )
154
- resp.raise_for_status()
155
- return resp.json()
156
-
157
-
158
- def update_article(api_key, article_id, payload):
159
- """Update an existing article on Dev.to."""
160
- resp = requests.put(
161
- f"{API_BASE}/articles/{article_id}",
162
- headers={
163
- "api-key": api_key,
164
- "Content-Type": "application/json",
165
- },
166
- json=payload,
167
- timeout=30,
168
- )
169
- resp.raise_for_status()
170
- return resp.json()
171
-
172
-
173
- def list_articles(api_key):
174
- """List the authenticated user's articles."""
175
- resp = requests.get(
176
- f"{API_BASE}/articles/me/all",
177
- headers={"api-key": api_key},
178
- timeout=30,
179
- )
180
- resp.raise_for_status()
181
- return resp.json()
182
-
183
-
184
- def publish_file(api_key, filepath, manifest, content_dir, dry_run=False, publish=False):
185
- """Publish or update a single article."""
186
- filename = Path(filepath).name
187
- print(f"\nProcessing: {filename}")
188
-
189
- payload = build_article_payload(filepath, publish=publish)
190
- if not payload:
191
- return False
192
-
193
- title = payload["article"]["title"]
194
- existing_id = manifest.get(filename, {}).get("id")
195
-
196
- if dry_run:
197
- action = "UPDATE" if existing_id else "CREATE"
198
- status = "published" if publish else "draft"
199
- print(f" [DRY RUN] Would {action} ({status}): {title}")
200
- if existing_id:
201
- print(f" [DRY RUN] Existing article ID: {existing_id}")
202
- print(f" [DRY RUN] Tags: {payload['article'].get('tags', [])}")
203
- return True
204
-
205
- try:
206
- if existing_id:
207
- print(f" Updating article {existing_id}: {title}")
208
- result = update_article(api_key, existing_id, payload)
209
- else:
210
- print(f" Creating article: {title}")
211
- result = create_article(api_key, payload)
212
-
213
- article_id = result["id"]
214
- article_url = result.get("url", f"https://dev.to/delimit_ai/{result.get('slug', '')}")
215
-
216
- manifest[filename] = {
217
- "id": article_id,
218
- "url": article_url,
219
- "title": title,
220
- }
221
- save_manifest(content_dir, manifest)
222
-
223
- status = "PUBLISHED" if publish else "DRAFT"
224
- print(f" {status}: {article_url}")
225
- return True
226
-
227
- except requests.exceptions.HTTPError as e:
228
- print(f" ERROR: {e}")
229
- if e.response is not None:
230
- print(f" Response: {e.response.text[:500]}")
231
- return False
232
-
233
-
234
- def main():
235
- parser = argparse.ArgumentParser(description="Cross-post articles to Dev.to")
236
- parser.add_argument("--dir", help="Directory containing markdown articles")
237
- parser.add_argument("--file", help="Single markdown file to publish")
238
- parser.add_argument("--list", action="store_true", help="List published articles")
239
- parser.add_argument("--dry-run", action="store_true", help="Preview without publishing")
240
- parser.add_argument("--publish", action="store_true",
241
- help="Publish articles (default: save as draft)")
242
- args = parser.parse_args()
243
-
244
- if args.list:
245
- api_key = get_api_key()
246
- articles = list_articles(api_key)
247
- if not articles:
248
- print("No articles found.")
249
- return
250
- print(f"Found {len(articles)} articles:\n")
251
- for a in articles:
252
- status = "PUBLISHED" if a.get("published") else "DRAFT"
253
- print(f" [{status}] {a['title']}")
254
- print(f" URL: {a.get('url', 'N/A')}")
255
- print(f" ID: {a['id']}")
256
- print()
257
- return
258
-
259
- if not args.dir and not args.file:
260
- parser.print_help()
261
- print("\nError: specify --dir or --file")
262
- sys.exit(1)
263
-
264
- api_key = None
265
- if not args.dry_run:
266
- api_key = get_api_key()
267
-
268
- content_dir = args.dir or str(Path(args.file).parent)
269
- manifest = load_manifest(content_dir)
270
-
271
- files = []
272
- if args.file:
273
- files = [args.file]
274
- else:
275
- files = sorted(
276
- str(p) for p in Path(args.dir).glob("*.md")
277
- if not p.name.startswith(".")
278
- and p.name not in ("README.md", "DEVTO_SETUP.md")
279
- and not p.name.isupper() # skip ALL-CAPS docs like DEVTO_SETUP.md
280
- )
281
-
282
- if not files:
283
- print(f"No markdown files found in {args.dir}")
284
- sys.exit(1)
285
-
286
- print(f"Found {len(files)} article(s) to process")
287
- if args.dry_run:
288
- print("MODE: dry run (no API calls)")
289
- elif args.publish:
290
- print("MODE: publish (articles will be live)")
291
- else:
292
- print("MODE: draft (articles saved as drafts on Dev.to)")
293
-
294
- success = 0
295
- for filepath in files:
296
- if publish_file(api_key, filepath, manifest, content_dir,
297
- dry_run=args.dry_run, publish=args.publish):
298
- success += 1
299
-
300
- print(f"\nDone: {success}/{len(files)} articles processed successfully.")
301
-
302
-
303
- if __name__ == "__main__":
304
- main()
@@ -1,110 +0,0 @@
1
- #!/bin/bash
2
- # Pre-publish security check — blocks npm publish if secrets are found
3
- # Run: bash scripts/security-check.sh
4
-
5
- set -euo pipefail
6
-
7
- echo "🔍 Delimit pre-publish security scan..."
8
-
9
- FAIL=0
10
-
11
- # Pack to temp and scan the actual tarball contents
12
- TMPDIR=$(mktemp -d)
13
- npm pack --pack-destination "$TMPDIR" --quiet 2>/dev/null
14
- TARBALL=$(ls "$TMPDIR"/*.tgz)
15
- tar -xzf "$TARBALL" -C "$TMPDIR"
16
-
17
- # 1. Credential patterns
18
- echo -n " Credentials... "
19
- if grep -rEi '(password|passwd|secret|api_key|apikey)\s*[:=]\s*["\x27][^"\x27]{4,}' "$TMPDIR/package/" --include="*.py" --include="*.js" --include="*.json" 2>/dev/null | grep -v 'environ\|getenv\|process\.env\|os\.environ\|<configured\|example\|placeholder\|REDACTED\|\${credentials\|credentials\.\|security-scan-ignore'; then
20
- echo "❌ FOUND CREDENTIALS"
21
- FAIL=1
22
- else
23
- echo "✅ clean"
24
- fi
25
-
26
- # 2. Blocklist terms
27
- echo -n " Blocklist... "
28
- BLOCKLIST="jamsonsholdings|Bladabah|Domainvested26|Delimit26|home/jamsons|infracore|crypttrx|\.wr_env|delimitdev|typed-on-phone|em dash.*ai tell|PAIN_CATEGORIES|VENTURE_CONFIG|VENTURE_SUBREDDITS|karma_building"
29
- if grep -rEi "$BLOCKLIST" "$TMPDIR/package/" --include="*.py" --include="*.js" --include="*.json" 2>/dev/null; then
30
- echo "❌ BLOCKED TERMS FOUND"
31
- FAIL=1
32
- else
33
- echo "✅ clean"
34
- fi
35
-
36
- # 3. PII (email addresses that aren't examples)
37
- echo -n " PII... "
38
- if grep -rEi '[a-z0-9._%+-]+@(gmail|yahoo|hotmail|outlook|proton|jamsons|wire\.report|domainvested)' "$TMPDIR/package/" --include="*.py" --include="*.js" --include="*.json" 2>/dev/null | grep -v "example\|placeholder\|<configured\|noreply\|e\.g\.\|docstring\|Args:\|Credential resolution"; then
39
- echo "❌ PII FOUND"
40
- FAIL=1
41
- else
42
- echo "✅ clean"
43
- fi
44
-
45
- # 4. Internal ticket IDs
46
- echo -n " Internal ticket IDs... "
47
- if grep -rE "LED-[0-9]{3}|STR-[0-9]{3}" "$TMPDIR/package/" --include="*.py" --include="*.js" 2>/dev/null | grep -v "node_modules" | head -1; then
48
- echo " WARNING: Internal ticket IDs found (cosmetic, not blocking)"
49
- else
50
- echo "clean"
51
- fi
52
-
53
- # 5. Proprietary files that shouldn't ship
54
- echo -n " Proprietary files... "
55
- PROPRIETARY="social_target\.py|social\.py|founding_users\.py|inbox_daemon\.py|deliberation\.py|reddit_scanner\.py|github_scanner\.py|cross_model_audit\.py|session_phoenix\.py|handoff_receipts\.py|toolcard_cache\.py"
56
- if find "$TMPDIR/package/" -name "*.py" | grep -Ei "$PROPRIETARY" 2>/dev/null; then
57
- echo "❌ PROPRIETARY FILES IN PACKAGE"
58
- FAIL=1
59
- else
60
- echo "✅ clean"
61
- fi
62
-
63
- # Cleanup npm tarball
64
- rm -rf "$TMPDIR"
65
-
66
- # ── PyPI dist scan (if dist/ exists) ─────────────────────────────────
67
- PYPI_DIST="/home/delimit/delimit-gateway/dist"
68
- if [ -d "$PYPI_DIST" ] && ls "$PYPI_DIST"/*.tar.gz 1>/dev/null 2>&1; then
69
- echo ""
70
- echo "PyPI dist scan..."
71
- PYPI_TMPDIR=$(mktemp -d)
72
- PYPI_TARBALL=$(ls -t "$PYPI_DIST"/*.tar.gz | head -1)
73
- tar -xzf "$PYPI_TARBALL" -C "$PYPI_TMPDIR" 2>/dev/null
74
-
75
- echo -n " Credentials... "
76
- if grep -rEi '(password|passwd|secret|api_key|apikey)\s*[:=]\s*["\x27][^"\x27]{4,}' "$PYPI_TMPDIR/" --include="*.py" 2>/dev/null | grep -v 'environ\|getenv\|os\.environ\|<configured\|example\|placeholder\|REDACTED'; then
77
- echo "FOUND CREDENTIALS IN PYPI DIST"
78
- FAIL=1
79
- else
80
- echo "clean"
81
- fi
82
-
83
- echo -n " Blocklist... "
84
- if grep -rEi "$BLOCKLIST" "$PYPI_TMPDIR/" --include="*.py" 2>/dev/null; then
85
- echo "BLOCKED TERMS IN PYPI DIST"
86
- FAIL=1
87
- else
88
- echo "clean"
89
- fi
90
-
91
- echo -n " PII... "
92
- if grep -rEi '[a-z0-9._%+-]+@(gmail|yahoo|hotmail|outlook|proton|jamsons|wire\.report|domainvested)' "$PYPI_TMPDIR/" --include="*.py" 2>/dev/null | grep -v "example\|placeholder\|<configured\|noreply\|e\.g\.\|docstring"; then
93
- echo "PII IN PYPI DIST"
94
- FAIL=1
95
- else
96
- echo "clean"
97
- fi
98
-
99
- rm -rf "$PYPI_TMPDIR"
100
- fi
101
-
102
- if [ $FAIL -ne 0 ]; then
103
- echo ""
104
- echo "SECURITY CHECK FAILED -- do not publish"
105
- exit 1
106
- fi
107
-
108
- echo ""
109
- echo "All security checks passed"
110
- exit 0
@@ -1,191 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Weekly Activity Tweet for @delimit_ai.
4
-
5
- Gathers GitHub activity stats across delimit-ai repos and npm download
6
- counts, then posts a summary tweet via the Twitter API.
7
-
8
- Reads Twitter credentials from environment variables (for GitHub Actions).
9
- """
10
-
11
- import os
12
- import sys
13
- import json
14
- from datetime import datetime, timedelta, timezone
15
-
16
- import requests
17
- import tweepy
18
-
19
- ORG = "delimit-ai"
20
- NPM_PACKAGE = "delimit-cli"
21
- GITHUB_API = "https://api.github.com"
22
- NPM_API = "https://api.npmjs.org"
23
-
24
-
25
- def get_org_repos():
26
- """Fetch all public repos for the org."""
27
- repos = []
28
- page = 1
29
- while True:
30
- resp = requests.get(
31
- f"{GITHUB_API}/orgs/{ORG}/repos",
32
- params={"type": "public", "per_page": 100, "page": page},
33
- )
34
- if resp.status_code != 200:
35
- break
36
- batch = resp.json()
37
- if not batch:
38
- break
39
- repos.extend(batch)
40
- page += 1
41
- return repos
42
-
43
-
44
- def get_total_stars(repos):
45
- """Sum stargazers across all repos."""
46
- return sum(r.get("stargazers_count", 0) for r in repos)
47
-
48
-
49
- def get_commits_last_week(repos):
50
- """Count commits in the last 7 days across all repos."""
51
- since = (datetime.now(timezone.utc) - timedelta(days=7)).isoformat()
52
- total = 0
53
- for repo in repos:
54
- name = repo["full_name"]
55
- page = 1
56
- while True:
57
- resp = requests.get(
58
- f"{GITHUB_API}/repos/{name}/commits",
59
- params={"since": since, "per_page": 100, "page": page},
60
- )
61
- if resp.status_code != 200:
62
- break
63
- batch = resp.json()
64
- if not batch:
65
- break
66
- total += len(batch)
67
- if len(batch) < 100:
68
- break
69
- page += 1
70
- return total
71
-
72
-
73
- def get_prs_merged_last_week(repos):
74
- """Count PRs merged in the last 7 days."""
75
- since = (datetime.now(timezone.utc) - timedelta(days=7)).isoformat()
76
- total = 0
77
- for repo in repos:
78
- name = repo["full_name"]
79
- resp = requests.get(
80
- f"{GITHUB_API}/repos/{name}/pulls",
81
- params={"state": "closed", "sort": "updated", "direction": "desc", "per_page": 100},
82
- )
83
- if resp.status_code != 200:
84
- continue
85
- for pr in resp.json():
86
- if pr.get("merged_at") and pr["merged_at"] >= since:
87
- total += 1
88
- return total
89
-
90
-
91
- def get_issues_stats(repos):
92
- """Count issues opened and closed in the last 7 days."""
93
- since = (datetime.now(timezone.utc) - timedelta(days=7)).isoformat()
94
- opened = 0
95
- closed = 0
96
- for repo in repos:
97
- name = repo["full_name"]
98
- # Opened
99
- resp = requests.get(
100
- f"{GITHUB_API}/repos/{name}/issues",
101
- params={"state": "all", "since": since, "per_page": 100},
102
- )
103
- if resp.status_code == 200:
104
- for issue in resp.json():
105
- if issue.get("pull_request"):
106
- continue
107
- if issue.get("created_at", "") >= since:
108
- opened += 1
109
- if issue.get("closed_at") and issue["closed_at"] >= since:
110
- closed += 1
111
- return opened, closed
112
-
113
-
114
- def get_npm_downloads():
115
- """Get npm download count for the last week."""
116
- resp = requests.get(f"{NPM_API}/downloads/point/last-week/{NPM_PACKAGE}")
117
- if resp.status_code != 200:
118
- return 0
119
- return resp.json().get("downloads", 0)
120
-
121
-
122
- def format_tweet(npm_downloads, total_stars, prs_merged, commits):
123
- """Format the weekly summary tweet."""
124
- lines = ["This week at Delimit:", ""]
125
- if npm_downloads > 0:
126
- lines.append(f"\U0001f4e6 {npm_downloads:,} npm downloads")
127
- if total_stars > 0:
128
- lines.append(f"\u2b50 {total_stars:,} stars")
129
- if prs_merged > 0:
130
- lines.append(f"\U0001f500 {prs_merged:,} PRs merged")
131
- if commits > 0:
132
- lines.append(f"\U0001f6e0\ufe0f {commits:,} commits")
133
- lines.append("")
134
- lines.append("Keep Building.")
135
- lines.append("")
136
- lines.append("delimit.ai")
137
- return "\n".join(lines)
138
-
139
-
140
- def post_tweet(text):
141
- """Post tweet using tweepy with OAuth 1.0a credentials from env vars."""
142
- consumer_key = os.environ.get("TWITTER_CONSUMER_KEY")
143
- consumer_secret = os.environ.get("TWITTER_CONSUMER_SECRET")
144
- access_token = os.environ.get("TWITTER_ACCESS_TOKEN")
145
- access_token_secret = os.environ.get("TWITTER_ACCESS_TOKEN_SECRET")
146
-
147
- if not all([consumer_key, consumer_secret, access_token, access_token_secret]):
148
- print("ERROR: Missing Twitter credentials in environment variables.")
149
- sys.exit(1)
150
-
151
- client = tweepy.Client(
152
- consumer_key=consumer_key,
153
- consumer_secret=consumer_secret,
154
- access_token=access_token,
155
- access_token_secret=access_token_secret,
156
- )
157
- response = client.create_tweet(text=text)
158
- print(f"Tweet posted: https://x.com/delimit_ai/status/{response.data['id']}")
159
-
160
-
161
- def main():
162
- print("Gathering weekly stats for delimit-ai...")
163
-
164
- repos = get_org_repos()
165
- print(f" Found {len(repos)} public repos")
166
-
167
- npm_downloads = get_npm_downloads()
168
- print(f" npm downloads (last week): {npm_downloads}")
169
-
170
- total_stars = get_total_stars(repos)
171
- print(f" Total stars: {total_stars}")
172
-
173
- prs_merged = get_prs_merged_last_week(repos)
174
- print(f" PRs merged (last week): {prs_merged}")
175
-
176
- commits = get_commits_last_week(repos)
177
- print(f" Commits (last week): {commits}")
178
-
179
- # Don't tweet if there's nothing to report
180
- if npm_downloads == 0 and total_stars == 0 and prs_merged == 0 and commits == 0:
181
- print("No activity this week. Skipping tweet.")
182
- return
183
-
184
- tweet_text = format_tweet(npm_downloads, total_stars, prs_merged, commits)
185
- print(f"\nTweet ({len(tweet_text)} chars):\n{tweet_text}\n")
186
-
187
- post_tweet(tweet_text)
188
-
189
-
190
- if __name__ == "__main__":
191
- main()