delimit-cli 4.5.1 → 4.5.3
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 +87 -0
- package/README.md +15 -5
- package/bin/delimit-cli.js +109 -24
- package/gateway/ai/content_engine.py +3 -4
- package/gateway/ai/inbox_classifier.py +215 -0
- package/gateway/ai/integrations/opensage_wrapper.py +4 -1
- package/gateway/ai/ledger_manager.py +218 -38
- package/gateway/ai/license.py +26 -0
- package/gateway/ai/notify.py +68 -3
- package/gateway/ai/reddit_proxy.py +93 -15
- package/gateway/ai/reddit_scanner.py +36 -18
- package/gateway/ai/remote_resolve.py +422 -0
- package/gateway/ai/server.py +301 -117
- package/gateway/ai/social_capability/__init__.py +6 -0
- package/gateway/ai/social_capability/capability_validator.py +367 -0
- package/gateway/ai/social_capability/current_capabilities.yaml +95 -0
- package/gateway/ai/social_capability/fit_floor.py +360 -0
- package/gateway/ai/social_queue.py +307 -0
- package/gateway/ai/supabase_sync.py +14 -2
- package/gateway/ai/swarm.py +29 -11
- package/gateway/ai/tui.py +6 -2
- package/gateway/ai/vendor_news/__init__.py +14 -0
- package/gateway/ai/vendor_news/drafter.py +562 -0
- package/gateway/ai/vendor_news/sensor.py +509 -0
- package/gateway/ai/vendor_news/watchlist.yaml +71 -0
- package/gateway/ai/x_ranker.py +417 -0
- package/lib/attest-mcp.js +487 -0
- package/lib/attest-telemetry.js +48 -0
- package/lib/delimit-home.js +35 -0
- package/lib/delimit-template.js +14 -0
- package/package.json +25 -3
- package/scripts/postinstall.js +89 -40
- package/adapters/codex-security.js +0 -64
- package/adapters/codex-skill.js +0 -78
- package/gateway/ai/content_grounding/__init__.py +0 -98
- package/gateway/ai/content_grounding/build.py +0 -350
- package/gateway/ai/content_grounding/consume.py +0 -280
- package/gateway/ai/content_grounding/features.py +0 -218
- package/gateway/ai/content_grounding/fixtures/fail/01_missing_evidence.json +0 -9
- package/gateway/ai/content_grounding/fixtures/fail/02_unknown_evidence_prefix.json +0 -9
- package/gateway/ai/content_grounding/fixtures/fail/03_banned_comparative.json +0 -17
- package/gateway/ai/content_grounding/fixtures/fail/04_banned_adoption.json +0 -17
- package/gateway/ai/content_grounding/fixtures/fail/05_aggregate_no_numeric.json +0 -17
- package/gateway/ai/content_grounding/fixtures/fail/06_unversioned_inference_rule.json +0 -18
- package/gateway/ai/content_grounding/fixtures/pass/01_feature_shipped.json +0 -18
- package/gateway/ai/content_grounding/fixtures/pass/02_aggregate_claim.json +0 -23
- package/gateway/ai/content_grounding/fixtures/pass/03_attestation.json +0 -16
- package/gateway/ai/content_grounding/schemas/claim.schema.json +0 -40
- package/gateway/ai/content_grounding/schemas/event.schema.json +0 -23
- package/gateway/ai/content_grounding/schemas.py +0 -276
- package/gateway/ai/content_grounding/telemetry.py +0 -221
- package/gateway/ai/inbox_drafts/__init__.py +0 -61
- package/gateway/ai/inbox_drafts/registry.py +0 -412
- package/gateway/ai/inbox_drafts/schema.py +0 -374
- package/gateway/ai/inbox_executor.py +0 -565
package/gateway/ai/server.py
CHANGED
|
@@ -1428,112 +1428,144 @@ def delimit_lint(old_spec: str, new_spec: str, policy_file: Optional[str] = None
|
|
|
1428
1428
|
recording evidence, triggering notifications, or enforcing governance.
|
|
1429
1429
|
Useful for CI preview comments ("what would block") without side effects.
|
|
1430
1430
|
|
|
1431
|
+
Spec arguments accept local paths or http(s) URLs. URLs are fetched
|
|
1432
|
+
once into a tempfile (size cap, SSRF guard) and the resolved local
|
|
1433
|
+
path is used for the rest of the pipeline.
|
|
1434
|
+
|
|
1431
1435
|
Args:
|
|
1432
|
-
old_spec: Path to the old (baseline) OpenAPI spec file.
|
|
1433
|
-
new_spec: Path to the new (proposed) OpenAPI spec file.
|
|
1436
|
+
old_spec: Path or URL to the old (baseline) OpenAPI spec file.
|
|
1437
|
+
new_spec: Path or URL to the new (proposed) OpenAPI spec file.
|
|
1434
1438
|
policy_file: Optional path to a .delimit/policies.yml file.
|
|
1435
1439
|
dry_run: If True, return violations without side effects (no evidence, no chains).
|
|
1436
1440
|
"""
|
|
1437
1441
|
from backends.gateway_core import run_lint, run_semver
|
|
1442
|
+
from ai.remote_resolve import RemoteResolveError, resolve_spec_input
|
|
1438
1443
|
|
|
1439
|
-
#
|
|
1440
|
-
|
|
1444
|
+
# Resolve any URL inputs up-front. Both contextmanagers share a
|
|
1445
|
+
# single ExitStack so cleanup happens once at the end.
|
|
1446
|
+
import contextlib as _contextlib
|
|
1447
|
+
try:
|
|
1448
|
+
with _contextlib.ExitStack() as stack:
|
|
1449
|
+
old_resolved, old_meta = stack.enter_context(resolve_spec_input(old_spec))
|
|
1450
|
+
new_resolved, new_meta = stack.enter_context(resolve_spec_input(new_spec))
|
|
1451
|
+
resolved_from = [old_meta["resolved_from"], new_meta["resolved_from"]]
|
|
1452
|
+
|
|
1453
|
+
# Step 1: Core lint (against resolved local paths)
|
|
1454
|
+
lint_result = _safe_call(
|
|
1455
|
+
run_lint,
|
|
1456
|
+
old_spec=old_resolved,
|
|
1457
|
+
new_spec=new_resolved,
|
|
1458
|
+
policy_file=policy_file,
|
|
1459
|
+
)
|
|
1441
1460
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1461
|
+
# Dry-run mode: return raw lint + semver, skip all chains and governance
|
|
1462
|
+
if dry_run:
|
|
1463
|
+
lint_result["dry_run"] = True
|
|
1464
|
+
lint_result["simulated"] = True
|
|
1465
|
+
# Still classify semver (informational, no side effects)
|
|
1466
|
+
semver_result = _safe_call(run_semver, old_spec=old_resolved, new_spec=new_resolved)
|
|
1467
|
+
if not semver_result.get("error"):
|
|
1468
|
+
lint_result["semver"] = semver_result
|
|
1469
|
+
lint_result["resolved_from"] = resolved_from
|
|
1470
|
+
return lint_result
|
|
1471
|
+
|
|
1472
|
+
chain: Dict[str, Any] = {"id": "lint_chain", "steps": []}
|
|
1473
|
+
|
|
1474
|
+
if lint_result.get("error"):
|
|
1475
|
+
lint_result["chain"] = chain
|
|
1476
|
+
lint_result["resolved_from"] = resolved_from
|
|
1477
|
+
return _with_next_steps("lint", lint_result)
|
|
1478
|
+
|
|
1479
|
+
# Step 2: Auto-classify semver bump (non-blocking on failure)
|
|
1480
|
+
semver_result = _chain_call("lint", "semver", run_semver,
|
|
1481
|
+
required=False, old_spec=old_resolved, new_spec=new_resolved)
|
|
1482
|
+
chain["steps"].append({"step": "semver", "ok": not _chain_is_error(semver_result)})
|
|
1449
1483
|
lint_result["semver"] = semver_result
|
|
1450
|
-
return lint_result
|
|
1451
1484
|
|
|
1452
|
-
|
|
1485
|
+
if _chain_is_error(semver_result):
|
|
1486
|
+
chain["status"] = "semver_failed_nonfatal"
|
|
1487
|
+
lint_result["chain"] = chain
|
|
1488
|
+
lint_result["resolved_from"] = resolved_from
|
|
1489
|
+
return _with_next_steps("lint", lint_result)
|
|
1453
1490
|
|
|
1454
|
-
|
|
1455
|
-
lint_result["chain"] = chain
|
|
1456
|
-
return _with_next_steps("lint", lint_result)
|
|
1457
|
-
|
|
1458
|
-
# Step 2: Auto-classify semver bump (non-blocking on failure)
|
|
1459
|
-
semver_result = _chain_call("lint", "semver", run_semver,
|
|
1460
|
-
required=False, old_spec=old_spec, new_spec=new_spec)
|
|
1461
|
-
chain["steps"].append({"step": "semver", "ok": not _chain_is_error(semver_result)})
|
|
1462
|
-
lint_result["semver"] = semver_result
|
|
1463
|
-
|
|
1464
|
-
if _chain_is_error(semver_result):
|
|
1465
|
-
chain["status"] = "semver_failed_nonfatal"
|
|
1466
|
-
lint_result["chain"] = chain
|
|
1467
|
-
return _with_next_steps("lint", lint_result)
|
|
1468
|
-
|
|
1469
|
-
bump = str(semver_result.get("bump", "")).upper()
|
|
1470
|
-
|
|
1471
|
-
# Step 2b: Impact-based notification routing (LED-233, non-blocking)
|
|
1472
|
-
try:
|
|
1473
|
-
from ai.notify import route_by_impact
|
|
1474
|
-
all_changes = lint_result.get("all_changes", lint_result.get("violations", []))
|
|
1475
|
-
if all_changes:
|
|
1476
|
-
routing_result = route_by_impact(all_changes, dry_run=False)
|
|
1477
|
-
chain["steps"].append({"step": "impact_routing", "ok": True})
|
|
1478
|
-
lint_result["impact_routing"] = routing_result
|
|
1479
|
-
except Exception as e:
|
|
1480
|
-
logger.debug("Impact routing non-fatal error: %s", e)
|
|
1481
|
-
chain["steps"].append({"step": "impact_routing", "ok": False, "error": str(e)})
|
|
1491
|
+
bump = str(semver_result.get("bump", "")).upper()
|
|
1482
1492
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
"
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1493
|
+
# Step 2b: Impact-based notification routing (LED-233, non-blocking)
|
|
1494
|
+
try:
|
|
1495
|
+
from ai.notify import route_by_impact
|
|
1496
|
+
all_changes = lint_result.get("all_changes", lint_result.get("violations", []))
|
|
1497
|
+
if all_changes:
|
|
1498
|
+
routing_result = route_by_impact(all_changes, dry_run=False)
|
|
1499
|
+
chain["steps"].append({"step": "impact_routing", "ok": True})
|
|
1500
|
+
lint_result["impact_routing"] = routing_result
|
|
1501
|
+
except Exception as e:
|
|
1502
|
+
logger.debug("Impact routing non-fatal error: %s", e)
|
|
1503
|
+
chain["steps"].append({"step": "impact_routing", "ok": False, "error": str(e)})
|
|
1504
|
+
|
|
1505
|
+
if bump != "MAJOR":
|
|
1506
|
+
chain["status"] = f"complete_{bump.lower() or 'none'}"
|
|
1507
|
+
lint_result["chain"] = chain
|
|
1508
|
+
lint_result["resolved_from"] = resolved_from
|
|
1509
|
+
return _with_next_steps("lint", lint_result)
|
|
1510
|
+
|
|
1511
|
+
# Step 3: MAJOR bump detected -- evaluate governance
|
|
1512
|
+
# Note: _delimit_gov_impl has its own Pro gate. Free-tier gets lint+semver only.
|
|
1513
|
+
# Pass the *original* user inputs (not the tempfile paths) into the
|
|
1514
|
+
# governance context so audit trails capture the real spec source.
|
|
1515
|
+
gov_result = _delimit_gov_impl(
|
|
1516
|
+
action="evaluate",
|
|
1517
|
+
eval_action="api_breaking_change",
|
|
1518
|
+
context={
|
|
1519
|
+
"tool": "delimit_lint",
|
|
1520
|
+
"old_spec": old_spec,
|
|
1521
|
+
"new_spec": new_spec,
|
|
1522
|
+
"semver_bump": bump,
|
|
1523
|
+
"breaking_changes": lint_result.get("breaking", []),
|
|
1524
|
+
},
|
|
1525
|
+
repo=".",
|
|
1526
|
+
)
|
|
1527
|
+
chain["steps"].append({"step": "gov_evaluate", "ok": not _chain_is_error(gov_result)})
|
|
1528
|
+
lint_result["gov_evaluate"] = gov_result
|
|
1529
|
+
|
|
1530
|
+
# If Pro gate blocked governance, return gracefully with lint+semver
|
|
1531
|
+
if gov_result.get("status") == "premium_required":
|
|
1532
|
+
chain["status"] = "governance_skipped_free_tier"
|
|
1533
|
+
lint_result["chain"] = chain
|
|
1534
|
+
lint_result["resolved_from"] = resolved_from
|
|
1535
|
+
return _with_next_steps("lint", lint_result)
|
|
1536
|
+
|
|
1537
|
+
# Step 4: If governance blocked, record in ledger (best-effort)
|
|
1538
|
+
gov_blocked = (
|
|
1539
|
+
str(gov_result.get("status", "")).lower() == "blocked"
|
|
1540
|
+
or gov_result.get("governance", {}).get("action") == "policy_blocked"
|
|
1541
|
+
)
|
|
1516
1542
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1543
|
+
if gov_blocked:
|
|
1544
|
+
from ai.ledger_manager import add_item
|
|
1545
|
+
ledger_result = _chain_call(
|
|
1546
|
+
"lint", "ledger_add", add_item,
|
|
1547
|
+
required=False,
|
|
1548
|
+
title=f"Governance blocked: MAJOR API change in {new_spec}",
|
|
1549
|
+
ledger="ops",
|
|
1550
|
+
item_type="fix",
|
|
1551
|
+
priority="P0",
|
|
1552
|
+
description="MAJOR semver bump detected. Governance blocked the change.",
|
|
1553
|
+
source="chain:lint:gov_blocked",
|
|
1554
|
+
)
|
|
1555
|
+
chain["steps"].append({"step": "ledger_add", "ok": not _chain_is_error(ledger_result)})
|
|
1556
|
+
lint_result["governance_blocked"] = True
|
|
1557
|
+
else:
|
|
1558
|
+
lint_result["governance_blocked"] = False
|
|
1533
1559
|
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1560
|
+
chain["status"] = "major_change_evaluated"
|
|
1561
|
+
lint_result["chain"] = chain
|
|
1562
|
+
lint_result["resolved_from"] = resolved_from
|
|
1563
|
+
return _with_next_steps("lint", lint_result)
|
|
1564
|
+
except RemoteResolveError as e:
|
|
1565
|
+
out = e.to_dict()
|
|
1566
|
+
out["old_spec"] = old_spec
|
|
1567
|
+
out["new_spec"] = new_spec
|
|
1568
|
+
return out
|
|
1537
1569
|
|
|
1538
1570
|
|
|
1539
1571
|
@mcp.tool()
|
|
@@ -2911,49 +2943,79 @@ def delimit_repo_diagnose(target: str = ".") -> Dict[str, Any]:
|
|
|
2911
2943
|
return _safe_call(diagnose, target=target)
|
|
2912
2944
|
|
|
2913
2945
|
|
|
2946
|
+
def _run_repo_tool_with_remote(
|
|
2947
|
+
target: str,
|
|
2948
|
+
backend_fn,
|
|
2949
|
+
pro_capability: str,
|
|
2950
|
+
) -> Dict[str, Any]:
|
|
2951
|
+
"""Shared wrapper for repo-bridge tools with remote-input support (LED-1237).
|
|
2952
|
+
|
|
2953
|
+
Resolves ``target`` (local path, ``owner/repo`` shorthand, or
|
|
2954
|
+
GitHub URL) into a local path, calls the backend, and merges the
|
|
2955
|
+
resolution metadata into the response so panel members can see
|
|
2956
|
+
what got cloned.
|
|
2957
|
+
"""
|
|
2958
|
+
from ai.license import require_premium
|
|
2959
|
+
gate = require_premium(pro_capability)
|
|
2960
|
+
if gate:
|
|
2961
|
+
return gate
|
|
2962
|
+
|
|
2963
|
+
from ai.remote_resolve import RemoteResolveError, resolve_repo_target
|
|
2964
|
+
|
|
2965
|
+
try:
|
|
2966
|
+
with resolve_repo_target(target) as (resolved_path, meta):
|
|
2967
|
+
result = _safe_call(backend_fn, target=resolved_path)
|
|
2968
|
+
if isinstance(result, dict):
|
|
2969
|
+
# Don't clobber a backend-supplied resolved_from field.
|
|
2970
|
+
for k, v in meta.items():
|
|
2971
|
+
result.setdefault(k, v)
|
|
2972
|
+
return result
|
|
2973
|
+
except RemoteResolveError as e:
|
|
2974
|
+
out = e.to_dict()
|
|
2975
|
+
# Preserve the user's original input for debuggability.
|
|
2976
|
+
out["target"] = target
|
|
2977
|
+
return out
|
|
2978
|
+
|
|
2979
|
+
|
|
2914
2980
|
@mcp.tool()
|
|
2915
2981
|
def delimit_repo_analyze(target: str = ".") -> Dict[str, Any]:
|
|
2916
2982
|
"""Analyze repository structure and quality (experimental) (Pro).
|
|
2917
2983
|
|
|
2984
|
+
Accepts a local path, an ``owner/repo`` shorthand
|
|
2985
|
+
(e.g. ``calcom/cal.com``), or a full GitHub URL. Remote inputs
|
|
2986
|
+
are shallow-cloned into a tempdir for the duration of the call.
|
|
2987
|
+
|
|
2918
2988
|
Args:
|
|
2919
|
-
target: Repository path.
|
|
2989
|
+
target: Repository path, owner/repo, or GitHub URL.
|
|
2920
2990
|
"""
|
|
2921
|
-
from ai.license import require_premium
|
|
2922
|
-
gate = require_premium("repo_analyze")
|
|
2923
|
-
if gate:
|
|
2924
|
-
return gate
|
|
2925
2991
|
from backends.repo_bridge import analyze
|
|
2926
|
-
return
|
|
2992
|
+
return _run_repo_tool_with_remote(target, analyze, "repo_analyze")
|
|
2927
2993
|
|
|
2928
2994
|
|
|
2929
2995
|
@mcp.tool()
|
|
2930
2996
|
def delimit_repo_config_validate(target: str = ".") -> Dict[str, Any]:
|
|
2931
2997
|
"""Validate configuration files (experimental) (Pro).
|
|
2932
2998
|
|
|
2999
|
+
Accepts a local path, ``owner/repo`` shorthand, or a GitHub URL.
|
|
3000
|
+
|
|
2933
3001
|
Args:
|
|
2934
|
-
target: Repository or config path.
|
|
3002
|
+
target: Repository or config path, owner/repo, or GitHub URL.
|
|
2935
3003
|
"""
|
|
2936
|
-
from ai.license import require_premium
|
|
2937
|
-
gate = require_premium("repo_config_validate")
|
|
2938
|
-
if gate:
|
|
2939
|
-
return gate
|
|
2940
3004
|
from backends.repo_bridge import config_validate
|
|
2941
|
-
return
|
|
3005
|
+
return _run_repo_tool_with_remote(target, config_validate, "repo_config_validate")
|
|
2942
3006
|
|
|
2943
3007
|
|
|
2944
3008
|
@mcp.tool()
|
|
2945
3009
|
def delimit_repo_config_audit(target: str = ".") -> Dict[str, Any]:
|
|
2946
3010
|
"""Audit configuration compliance (experimental) (Pro).
|
|
2947
3011
|
|
|
3012
|
+
Accepts a local path, ``owner/repo`` shorthand, or a GitHub URL.
|
|
3013
|
+
|
|
2948
3014
|
Args:
|
|
2949
|
-
target: Repository or config path.
|
|
3015
|
+
target: Repository or config path, owner/repo, or GitHub URL.
|
|
2950
3016
|
"""
|
|
2951
|
-
from ai.license import require_premium
|
|
2952
|
-
gate = require_premium("repo_config_audit")
|
|
2953
|
-
if gate:
|
|
2954
|
-
return gate
|
|
2955
3017
|
from backends.repo_bridge import config_audit
|
|
2956
|
-
return
|
|
3018
|
+
return _run_repo_tool_with_remote(target, config_audit, "repo_config_audit")
|
|
2957
3019
|
|
|
2958
3020
|
|
|
2959
3021
|
# ─── Security ───────────────────────────────────────────────────────────
|
|
@@ -7302,7 +7364,8 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
7302
7364
|
Categories: tip, changelog, insight, engagement.
|
|
7303
7365
|
Leave text empty to auto-generate from templates.
|
|
7304
7366
|
Every post provides value - tips, insights, governance wisdom.
|
|
7305
|
-
|
|
7367
|
+
Rate cap: 2 original posts per hour, 24 per day (founder-approved
|
|
7368
|
+
2026-04-30). Override via DELIMIT_HOURLY_TWEETS / DELIMIT_DAILY_TWEETS.
|
|
7306
7369
|
|
|
7307
7370
|
IMPORTANT - Platform tone rules (these are DIFFERENT per platform):
|
|
7308
7371
|
- Twitter: confident technical brand. Direct, professional, ALWAYS POSITIVE.
|
|
@@ -7323,10 +7386,10 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
7323
7386
|
draft: If True, save as draft for approval instead of posting immediately.
|
|
7324
7387
|
context: WHY this post should be made. Strategic reasoning shown in the approval email.
|
|
7325
7388
|
"""
|
|
7326
|
-
from ai.social import generate_post, post_tweet,
|
|
7389
|
+
from ai.social import generate_post, post_tweet, should_post_now, save_draft
|
|
7327
7390
|
|
|
7328
|
-
if not draft and not
|
|
7329
|
-
return {"status": "skipped", "reason": "
|
|
7391
|
+
if not draft and not should_post_now():
|
|
7392
|
+
return {"status": "skipped", "reason": "Rate cap hit (2/hr or 24/day). Wait or pass draft=True for email-approval flow."}
|
|
7330
7393
|
|
|
7331
7394
|
post = generate_post(category, text)
|
|
7332
7395
|
|
|
@@ -7443,9 +7506,16 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
7443
7506
|
_lines.append("Reply APPROVED to approve, CANCEL to reject.")
|
|
7444
7507
|
|
|
7445
7508
|
_handle = f"u/{_acct}"
|
|
7509
|
+
# LED-1129 Phase 2 — append [draft_id:<8>] token to subject so
|
|
7510
|
+
# the inbox daemon's draft_id fallback can match the approval
|
|
7511
|
+
# reply even when no LED/STR token is present.
|
|
7512
|
+
_reddit_subject = f"[Reddit Post] {_handle}: {_reddit_title[:60]}..."
|
|
7513
|
+
_reg_id = entry.get("registry_draft_id")
|
|
7514
|
+
if _reg_id:
|
|
7515
|
+
_reddit_subject = f"{_reddit_subject} [draft_id:{_reg_id[:8]}]"
|
|
7446
7516
|
email_result = send_email(
|
|
7447
7517
|
message="\n".join(_lines),
|
|
7448
|
-
subject=
|
|
7518
|
+
subject=_reddit_subject,
|
|
7449
7519
|
event_type="social_draft",
|
|
7450
7520
|
)
|
|
7451
7521
|
if email_result.get("delivered") and email_result.get("message_id"):
|
|
@@ -7487,9 +7557,16 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
7487
7557
|
_subject_type = "Tweet"
|
|
7488
7558
|
|
|
7489
7559
|
_handle = f"u/{_acct}" if platform == "reddit" else f"@{_acct}"
|
|
7560
|
+
# LED-1129 Phase 2 — append [draft_id:<8>] token to subject so the
|
|
7561
|
+
# inbox daemon's draft_id fallback can match the approval reply even
|
|
7562
|
+
# when no LED/STR token is present.
|
|
7563
|
+
_social_subject = f"[{_subject_type}] {_handle}: {post['text'][:60]}..."
|
|
7564
|
+
_reg_id = entry.get("registry_draft_id")
|
|
7565
|
+
if _reg_id:
|
|
7566
|
+
_social_subject = f"{_social_subject} [draft_id:{_reg_id[:8]}]"
|
|
7490
7567
|
email_result = send_email(
|
|
7491
7568
|
message="\n".join(_lines),
|
|
7492
|
-
subject=
|
|
7569
|
+
subject=_social_subject,
|
|
7493
7570
|
event_type="social_draft",
|
|
7494
7571
|
)
|
|
7495
7572
|
# Store the outbound Message-ID on the draft record so the
|
|
@@ -7529,6 +7606,55 @@ def delimit_social_accounts() -> Dict[str, Any]:
|
|
|
7529
7606
|
return _with_next_steps("social_accounts", {"accounts": accounts, "count": len(accounts)})
|
|
7530
7607
|
|
|
7531
7608
|
|
|
7609
|
+
@mcp.tool()
|
|
7610
|
+
def delimit_x_fetch(id_or_url: str = "", ids: str = "") -> Dict[str, Any]:
|
|
7611
|
+
"""LED-825: fetch tweets from X by id or URL via twttr241 (RapidAPI).
|
|
7612
|
+
|
|
7613
|
+
Inherits the LRU + SQLite cache + budget gate already wired for the
|
|
7614
|
+
social-target scanner, so repeated reads of the same tweet are free.
|
|
7615
|
+
|
|
7616
|
+
Args:
|
|
7617
|
+
id_or_url: A single status id ("2048825010371039648") OR a full
|
|
7618
|
+
x.com / twitter.com URL — the id is extracted automatically.
|
|
7619
|
+
Mutually exclusive with `ids`.
|
|
7620
|
+
ids: Comma-separated list of status ids OR URLs for a batch fetch.
|
|
7621
|
+
Each is normalized to a status id and fetched independently.
|
|
7622
|
+
|
|
7623
|
+
Returns:
|
|
7624
|
+
Single-fetch shape: {id, text, author, author_name, created_at,
|
|
7625
|
+
metrics: {favorite_count, retweet_count, reply_count,
|
|
7626
|
+
quote_count, bookmark_count, view_count}, url, from_cache}
|
|
7627
|
+
Batch shape: {tweets: [<single-shape>, ...], count}
|
|
7628
|
+
Errors: {error: <reason>}
|
|
7629
|
+
|
|
7630
|
+
Why this exists: WebFetch hits 402 on x.com (auth-walled), and going
|
|
7631
|
+
around to tweepy + the X API direct creds skips the cache + budget
|
|
7632
|
+
gate. This tool is the cheap, cached, governable read path.
|
|
7633
|
+
"""
|
|
7634
|
+
from ai.social_target import fetch_tweet_by_id, fetch_tweets_by_ids, extract_status_id
|
|
7635
|
+
|
|
7636
|
+
if ids:
|
|
7637
|
+
# Batch mode — accept commas, newlines, or whitespace as separators
|
|
7638
|
+
raw = [r.strip() for r in ids.replace("\n", ",").split(",") if r.strip()]
|
|
7639
|
+
normalized: List[str] = []
|
|
7640
|
+
for item in raw:
|
|
7641
|
+
sid = extract_status_id(item)
|
|
7642
|
+
if sid:
|
|
7643
|
+
normalized.append(sid)
|
|
7644
|
+
if not normalized:
|
|
7645
|
+
return _with_next_steps("x_fetch", {"error": "no valid ids/URLs in `ids`"})
|
|
7646
|
+
results = fetch_tweets_by_ids(normalized)
|
|
7647
|
+
return _with_next_steps("x_fetch", {"tweets": results, "count": len(results)})
|
|
7648
|
+
|
|
7649
|
+
if not id_or_url:
|
|
7650
|
+
return _with_next_steps("x_fetch", {"error": "provide either id_or_url or ids"})
|
|
7651
|
+
|
|
7652
|
+
sid = extract_status_id(id_or_url)
|
|
7653
|
+
if not sid:
|
|
7654
|
+
return _with_next_steps("x_fetch", {"error": f"could not parse status id from {id_or_url!r}"})
|
|
7655
|
+
return _with_next_steps("x_fetch", fetch_tweet_by_id(sid))
|
|
7656
|
+
|
|
7657
|
+
|
|
7532
7658
|
@mcp.tool()
|
|
7533
7659
|
def delimit_social_history(limit: int = 20, platform: str = "",
|
|
7534
7660
|
user: str = "", subreddit: str = "") -> Dict[str, Any]:
|
|
@@ -7985,6 +8111,64 @@ def delimit_social_daemon(action: str = "status") -> Dict[str, Any]:
|
|
|
7985
8111
|
else:
|
|
7986
8112
|
return _with_next_steps("social_daemon", get_daemon_status())
|
|
7987
8113
|
|
|
8114
|
+
|
|
8115
|
+
@mcp.tool()
|
|
8116
|
+
def delimit_self_repair_daemon(action: str = "status") -> Dict[str, Any]:
|
|
8117
|
+
"""Control the self-repair watcher daemon (LED-191, internal).
|
|
8118
|
+
|
|
8119
|
+
Polls function KPIs on a cadence (default 1h) and emits founder
|
|
8120
|
+
alerts on fresh breaches. Higher modes (diagnose/deliberate/apply/
|
|
8121
|
+
verify) chain through the watcher when configured per-function in
|
|
8122
|
+
~/.delimit/self_repair.yaml.
|
|
8123
|
+
|
|
8124
|
+
Idempotent start; circuit-breakered stop after 3 consecutive
|
|
8125
|
+
pass failures. Honors DELIMIT_SELF_REPAIR_PAUSE=1 at every pass
|
|
8126
|
+
without requiring a daemon restart.
|
|
8127
|
+
|
|
8128
|
+
Args:
|
|
8129
|
+
action: 'start' (begin polling), 'stop' (halt polling),
|
|
8130
|
+
'status' (running / last_pass / breaches_emitted /
|
|
8131
|
+
consecutive_failures).
|
|
8132
|
+
"""
|
|
8133
|
+
from ai.self_repair_daemon import (
|
|
8134
|
+
start_daemon as _sr_start,
|
|
8135
|
+
stop_daemon as _sr_stop,
|
|
8136
|
+
get_daemon_status as _sr_status,
|
|
8137
|
+
)
|
|
8138
|
+
|
|
8139
|
+
if action == "start":
|
|
8140
|
+
return _with_next_steps("self_repair_daemon", _sr_start())
|
|
8141
|
+
elif action == "stop":
|
|
8142
|
+
return _with_next_steps("self_repair_daemon", _sr_stop())
|
|
8143
|
+
else:
|
|
8144
|
+
return _with_next_steps("self_repair_daemon", _sr_status())
|
|
8145
|
+
|
|
8146
|
+
|
|
8147
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
8148
|
+
# LED-189: Corp dashboard — single-call session-start synthesis
|
|
8149
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
8150
|
+
|
|
8151
|
+
|
|
8152
|
+
@mcp.tool()
|
|
8153
|
+
def delimit_corp_dashboard() -> Dict[str, Any]:
|
|
8154
|
+
"""One-call corp status — replaces the 6-call session-start ritual (LED-189).
|
|
8155
|
+
|
|
8156
|
+
Returns daemon states (systemd + in-process), self-repair status,
|
|
8157
|
+
social/inbox activity, ledger pending counts, agent queue (audit-only),
|
|
8158
|
+
latest session, and a synthesized one-line summary like:
|
|
8159
|
+
|
|
8160
|
+
"Corp status: 3 daemons active (self-repair, inbox, social),
|
|
8161
|
+
12 ledger open, 2 approvals waiting, 4 breaches in 24h."
|
|
8162
|
+
|
|
8163
|
+
Every sub-section is failure-isolated: a partial failure returns
|
|
8164
|
+
{"error": "..."} for that key only and never crashes the whole call.
|
|
8165
|
+
Gateway-only — not shipped in the npm bundle.
|
|
8166
|
+
"""
|
|
8167
|
+
from ai.corp_dashboard import get_corp_dashboard
|
|
8168
|
+
result = get_corp_dashboard()
|
|
8169
|
+
return _with_next_steps("corp_dashboard", result)
|
|
8170
|
+
|
|
8171
|
+
|
|
7988
8172
|
# ═══════════════════════════════════════════════════════════════════════
|
|
7989
8173
|
# LED-187: Shareable Governance Config - export / import
|
|
7990
8174
|
# ═══════════════════════════════════════════════════════════════════════
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Social outreach capability-currency module (LED-216 Phase 1).
|
|
2
|
+
|
|
3
|
+
Curated capability inventory + draft-emit validator. Lives outside
|
|
4
|
+
``ai/social.py`` so the existing module stays focused on draft generation
|
|
5
|
+
and Python doesn't get a package/module name collision.
|
|
6
|
+
"""
|