delimit-cli 4.5.5 → 4.5.6
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/gateway/ai/license.py +7 -0
- package/gateway/ai/server.py +418 -1
- package/package.json +1 -1
package/gateway/ai/license.py
CHANGED
|
@@ -26,10 +26,14 @@ try:
|
|
|
26
26
|
# Autonomous build loop
|
|
27
27
|
"delimit_next_task", "delimit_task_complete",
|
|
28
28
|
"delimit_loop_status", "delimit_loop_config",
|
|
29
|
+
# LED-1253: vendor-news riff MCP wrappers
|
|
30
|
+
"delimit_vendor_news_scan", "delimit_vendor_news_health",
|
|
31
|
+
"delimit_vendor_news_draft",
|
|
29
32
|
})
|
|
30
33
|
except ImportError:
|
|
31
34
|
# license_core not available (development mode or missing binary)
|
|
32
35
|
import json
|
|
36
|
+
import os
|
|
33
37
|
import time
|
|
34
38
|
from pathlib import Path
|
|
35
39
|
|
|
@@ -78,6 +82,9 @@ except ImportError:
|
|
|
78
82
|
# Autonomous build loop
|
|
79
83
|
"delimit_next_task", "delimit_task_complete",
|
|
80
84
|
"delimit_loop_status", "delimit_loop_config",
|
|
85
|
+
# LED-1253: vendor-news riff MCP wrappers
|
|
86
|
+
"delimit_vendor_news_scan", "delimit_vendor_news_health",
|
|
87
|
+
"delimit_vendor_news_draft",
|
|
81
88
|
})
|
|
82
89
|
FREE_TRIAL_LIMITS = {"delimit_deliberate": 3}
|
|
83
90
|
|
package/gateway/ai/server.py
CHANGED
|
@@ -51,7 +51,7 @@ import subprocess
|
|
|
51
51
|
import threading
|
|
52
52
|
import traceback
|
|
53
53
|
import uuid
|
|
54
|
-
from datetime import datetime, timezone
|
|
54
|
+
from datetime import datetime, timedelta, timezone
|
|
55
55
|
from pathlib import Path
|
|
56
56
|
from typing import Any, Dict, List, Optional, Union
|
|
57
57
|
|
|
@@ -7866,6 +7866,423 @@ def delimit_github_scan(
|
|
|
7866
7866
|
return _with_next_steps("github_scan", result)
|
|
7867
7867
|
|
|
7868
7868
|
|
|
7869
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
7870
|
+
# VENDOR NEWS RIFF SYSTEM - LED-1250 / LED-1253 (Pro)
|
|
7871
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
7872
|
+
#
|
|
7873
|
+
# Diagnostic + ad-hoc invocation surface for the vendor-news riff cron.
|
|
7874
|
+
# The cron at scripts/vendor_news_cron.py is the production firing path;
|
|
7875
|
+
# these tools expose the same backend functions (scan_vendor_news,
|
|
7876
|
+
# draft_vendor_riff) for in-session inspection, dry-runs, and one-off
|
|
7877
|
+
# drafting against a specific source tweet.
|
|
7878
|
+
|
|
7879
|
+
|
|
7880
|
+
@mcp.tool()
|
|
7881
|
+
def delimit_vendor_news_scan(dry_run: bool = False) -> Dict[str, Any]:
|
|
7882
|
+
"""Scan watchlisted vendor accounts for fresh, high-engagement posts and
|
|
7883
|
+
auto-draft brand-voice Delimit POV riffs (Pro, LED-1253).
|
|
7884
|
+
|
|
7885
|
+
Wraps ``ai.vendor_news.sensor.scan_vendor_news`` + ``draft_vendor_riff``
|
|
7886
|
+
in a single MCP-callable tool. This is the same code path the
|
|
7887
|
+
``vendor_news_cron.py`` runs every 30 minutes; surfacing it as an MCP
|
|
7888
|
+
tool lets the orchestrator invoke a scan on demand (e.g. when an X
|
|
7889
|
+
thread looks ride-able and the next cron tick is too far away).
|
|
7890
|
+
|
|
7891
|
+
When ``dry_run=True``, the sensor still polls (cache-friendly) but
|
|
7892
|
+
suppresses its JSONL log write AND skips the drafter entirely so no
|
|
7893
|
+
queue insertion happens and the per-vendor 24h rate cap is not
|
|
7894
|
+
consumed. Useful for "what would fire if I ran the cron now?"
|
|
7895
|
+
inspection without side effects.
|
|
7896
|
+
|
|
7897
|
+
Args:
|
|
7898
|
+
dry_run: If True, run the sensor only (no drafter, no queue,
|
|
7899
|
+
no rate-cap consumption). Default False.
|
|
7900
|
+
|
|
7901
|
+
Returns:
|
|
7902
|
+
Dict with ``stats``, ``triggered``, ``queued``, ``rejected``,
|
|
7903
|
+
``rate_capped``, ``errors`` — same shape as the cron summary.
|
|
7904
|
+
"""
|
|
7905
|
+
from ai.license import require_premium
|
|
7906
|
+
gate = require_premium("vendor_news_scan")
|
|
7907
|
+
if gate:
|
|
7908
|
+
return gate
|
|
7909
|
+
|
|
7910
|
+
try:
|
|
7911
|
+
from ai.vendor_news import scan_vendor_news, draft_vendor_riff
|
|
7912
|
+
except Exception as exc:
|
|
7913
|
+
return _with_next_steps("vendor_news_scan", {
|
|
7914
|
+
"error": "vendor_news_unavailable",
|
|
7915
|
+
"message": f"could not import ai.vendor_news: {exc}",
|
|
7916
|
+
})
|
|
7917
|
+
|
|
7918
|
+
try:
|
|
7919
|
+
scan = scan_vendor_news(dry_run=dry_run)
|
|
7920
|
+
except Exception as exc:
|
|
7921
|
+
return _with_next_steps("vendor_news_scan", {
|
|
7922
|
+
"error": "scan_failed",
|
|
7923
|
+
"message": str(exc),
|
|
7924
|
+
})
|
|
7925
|
+
|
|
7926
|
+
triggered = scan.get("triggered") or []
|
|
7927
|
+
stats = scan.get("stats") or {}
|
|
7928
|
+
|
|
7929
|
+
queued = 0
|
|
7930
|
+
rejected = 0
|
|
7931
|
+
rate_capped = 0
|
|
7932
|
+
drafter_errors: List[Dict[str, Any]] = []
|
|
7933
|
+
drafts: List[Dict[str, Any]] = []
|
|
7934
|
+
|
|
7935
|
+
if not dry_run:
|
|
7936
|
+
for tw in triggered:
|
|
7937
|
+
try:
|
|
7938
|
+
res = draft_vendor_riff(tw)
|
|
7939
|
+
except Exception as exc:
|
|
7940
|
+
drafter_errors.append({"id": tw.get("id"), "error": str(exc)})
|
|
7941
|
+
continue
|
|
7942
|
+
decision = res.get("decision")
|
|
7943
|
+
reason = res.get("reason", "")
|
|
7944
|
+
drafts.append({
|
|
7945
|
+
"source_id": tw.get("id"),
|
|
7946
|
+
"vendor": tw.get("vendor"),
|
|
7947
|
+
"decision": decision,
|
|
7948
|
+
"reason": reason,
|
|
7949
|
+
})
|
|
7950
|
+
if decision == "queue":
|
|
7951
|
+
queued += 1
|
|
7952
|
+
elif reason == "rate_capped":
|
|
7953
|
+
rate_capped += 1
|
|
7954
|
+
else:
|
|
7955
|
+
rejected += 1
|
|
7956
|
+
|
|
7957
|
+
result = {
|
|
7958
|
+
"stats": stats,
|
|
7959
|
+
"triggered": [
|
|
7960
|
+
{
|
|
7961
|
+
"id": t.get("id"),
|
|
7962
|
+
"vendor": t.get("vendor"),
|
|
7963
|
+
"url": t.get("url"),
|
|
7964
|
+
"trigger_reason": t.get("trigger_reason"),
|
|
7965
|
+
"metrics": t.get("metrics"),
|
|
7966
|
+
}
|
|
7967
|
+
for t in triggered
|
|
7968
|
+
],
|
|
7969
|
+
"queued": queued,
|
|
7970
|
+
"rejected": rejected,
|
|
7971
|
+
"rate_capped": rate_capped,
|
|
7972
|
+
"errors": list(scan.get("errors") or []) + drafter_errors,
|
|
7973
|
+
"drafts": drafts,
|
|
7974
|
+
"dry_run": bool(dry_run),
|
|
7975
|
+
}
|
|
7976
|
+
return _with_next_steps("vendor_news_scan", result)
|
|
7977
|
+
|
|
7978
|
+
|
|
7979
|
+
@mcp.tool()
|
|
7980
|
+
def delimit_vendor_news_health() -> Dict[str, Any]:
|
|
7981
|
+
"""Health check for the vendor-news riff system (Pro, LED-1253).
|
|
7982
|
+
|
|
7983
|
+
Returns a snapshot of:
|
|
7984
|
+
* cron installation status (greps ``crontab -l`` for vendor_news_cron.py)
|
|
7985
|
+
* last sensor run timestamp + stats (from sensor JSONL log)
|
|
7986
|
+
* last 24h queued P0 vendor_news_riff entries (from tweet_queue.json)
|
|
7987
|
+
* last 24h rejected entries grouped by reason (from rejected JSONL)
|
|
7988
|
+
* watchlist account count
|
|
7989
|
+
* estimated daily live-call budget consumption
|
|
7990
|
+
|
|
7991
|
+
Use this when you want to quickly answer "is the cron firing? are
|
|
7992
|
+
drafts landing? what's getting rejected?" without grepping logs.
|
|
7993
|
+
"""
|
|
7994
|
+
from ai.license import require_premium
|
|
7995
|
+
gate = require_premium("vendor_news_health")
|
|
7996
|
+
if gate:
|
|
7997
|
+
return gate
|
|
7998
|
+
|
|
7999
|
+
from ai.vendor_news.sensor import SENSOR_LOG_PATH, WATCHLIST_PATH, load_watchlist
|
|
8000
|
+
from ai.vendor_news.drafter import TWEET_QUEUE_PATH, REJECTED_LOG_PATH
|
|
8001
|
+
|
|
8002
|
+
out: Dict[str, Any] = {
|
|
8003
|
+
"cron_installed": False,
|
|
8004
|
+
"last_run_ts": None,
|
|
8005
|
+
"last_run_stats": None,
|
|
8006
|
+
"recent_queued_count_24h": 0,
|
|
8007
|
+
"recent_rejected_count_24h": 0,
|
|
8008
|
+
"recent_rejection_reasons_24h": {},
|
|
8009
|
+
"watchlist_account_count": 0,
|
|
8010
|
+
"daily_budget_used_estimate": 0,
|
|
8011
|
+
}
|
|
8012
|
+
|
|
8013
|
+
# 1) crontab check
|
|
8014
|
+
try:
|
|
8015
|
+
proc = subprocess.run(
|
|
8016
|
+
["crontab", "-l"],
|
|
8017
|
+
capture_output=True, text=True, timeout=5,
|
|
8018
|
+
)
|
|
8019
|
+
if proc.returncode == 0 and "vendor_news_cron.py" in (proc.stdout or ""):
|
|
8020
|
+
out["cron_installed"] = True
|
|
8021
|
+
except (FileNotFoundError, subprocess.SubprocessError, OSError):
|
|
8022
|
+
# crontab binary missing (containers, CI) — cron_installed stays False
|
|
8023
|
+
out["cron_installed"] = False
|
|
8024
|
+
|
|
8025
|
+
cutoff = datetime.now(timezone.utc) - timedelta(hours=24)
|
|
8026
|
+
|
|
8027
|
+
# 2) sensor log: last run + 24h budget estimate
|
|
8028
|
+
try:
|
|
8029
|
+
if SENSOR_LOG_PATH.exists():
|
|
8030
|
+
last_line = None
|
|
8031
|
+
budget_used = 0
|
|
8032
|
+
with open(SENSOR_LOG_PATH, "r", encoding="utf-8") as f:
|
|
8033
|
+
for line in f:
|
|
8034
|
+
line = line.strip()
|
|
8035
|
+
if not line:
|
|
8036
|
+
continue
|
|
8037
|
+
try:
|
|
8038
|
+
entry = json.loads(line)
|
|
8039
|
+
except (json.JSONDecodeError, ValueError):
|
|
8040
|
+
continue
|
|
8041
|
+
last_line = entry
|
|
8042
|
+
ts_raw = entry.get("ts")
|
|
8043
|
+
try:
|
|
8044
|
+
ts_norm = ts_raw[:-1] + "+00:00" if ts_raw and ts_raw.endswith("Z") else ts_raw
|
|
8045
|
+
ts = datetime.fromisoformat(ts_norm) if ts_norm else None
|
|
8046
|
+
except (ValueError, TypeError):
|
|
8047
|
+
ts = None
|
|
8048
|
+
if ts is not None:
|
|
8049
|
+
if ts.tzinfo is None:
|
|
8050
|
+
ts = ts.replace(tzinfo=timezone.utc)
|
|
8051
|
+
if ts >= cutoff:
|
|
8052
|
+
budget_used += int(entry.get("live_calls") or 0)
|
|
8053
|
+
if last_line:
|
|
8054
|
+
out["last_run_ts"] = last_line.get("ts")
|
|
8055
|
+
out["last_run_stats"] = {
|
|
8056
|
+
k: v for k, v in last_line.items()
|
|
8057
|
+
if k not in ("triggered_ids", "error_handles")
|
|
8058
|
+
}
|
|
8059
|
+
out["daily_budget_used_estimate"] = budget_used
|
|
8060
|
+
except OSError:
|
|
8061
|
+
pass
|
|
8062
|
+
|
|
8063
|
+
# 3) tweet_queue.json: 24h queued vendor_news_riff entries
|
|
8064
|
+
try:
|
|
8065
|
+
if TWEET_QUEUE_PATH.exists():
|
|
8066
|
+
data = json.loads(TWEET_QUEUE_PATH.read_text(encoding="utf-8"))
|
|
8067
|
+
if isinstance(data, list):
|
|
8068
|
+
count = 0
|
|
8069
|
+
for entry in data:
|
|
8070
|
+
if not isinstance(entry, dict):
|
|
8071
|
+
continue
|
|
8072
|
+
if entry.get("priority") != "P0":
|
|
8073
|
+
continue
|
|
8074
|
+
if entry.get("category") != "vendor_news_riff":
|
|
8075
|
+
continue
|
|
8076
|
+
added_raw = entry.get("added_at")
|
|
8077
|
+
try:
|
|
8078
|
+
added_norm = (
|
|
8079
|
+
added_raw[:-1] + "+00:00"
|
|
8080
|
+
if added_raw and added_raw.endswith("Z")
|
|
8081
|
+
else added_raw
|
|
8082
|
+
)
|
|
8083
|
+
added = datetime.fromisoformat(added_norm) if added_norm else None
|
|
8084
|
+
except (ValueError, TypeError):
|
|
8085
|
+
added = None
|
|
8086
|
+
if added is None:
|
|
8087
|
+
continue
|
|
8088
|
+
if added.tzinfo is None:
|
|
8089
|
+
added = added.replace(tzinfo=timezone.utc)
|
|
8090
|
+
if added >= cutoff:
|
|
8091
|
+
count += 1
|
|
8092
|
+
out["recent_queued_count_24h"] = count
|
|
8093
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
8094
|
+
pass
|
|
8095
|
+
|
|
8096
|
+
# 4) rejected JSONL: 24h count + reason histogram
|
|
8097
|
+
try:
|
|
8098
|
+
if REJECTED_LOG_PATH.exists():
|
|
8099
|
+
count = 0
|
|
8100
|
+
reasons: Dict[str, int] = {}
|
|
8101
|
+
with open(REJECTED_LOG_PATH, "r", encoding="utf-8") as f:
|
|
8102
|
+
for line in f:
|
|
8103
|
+
line = line.strip()
|
|
8104
|
+
if not line:
|
|
8105
|
+
continue
|
|
8106
|
+
try:
|
|
8107
|
+
entry = json.loads(line)
|
|
8108
|
+
except (json.JSONDecodeError, ValueError):
|
|
8109
|
+
continue
|
|
8110
|
+
ts_raw = entry.get("ts")
|
|
8111
|
+
try:
|
|
8112
|
+
ts_norm = (
|
|
8113
|
+
ts_raw[:-1] + "+00:00"
|
|
8114
|
+
if ts_raw and ts_raw.endswith("Z")
|
|
8115
|
+
else ts_raw
|
|
8116
|
+
)
|
|
8117
|
+
ts = datetime.fromisoformat(ts_norm) if ts_norm else None
|
|
8118
|
+
except (ValueError, TypeError):
|
|
8119
|
+
ts = None
|
|
8120
|
+
if ts is None:
|
|
8121
|
+
continue
|
|
8122
|
+
if ts.tzinfo is None:
|
|
8123
|
+
ts = ts.replace(tzinfo=timezone.utc)
|
|
8124
|
+
if ts < cutoff:
|
|
8125
|
+
continue
|
|
8126
|
+
count += 1
|
|
8127
|
+
reason = entry.get("reason") or "unknown"
|
|
8128
|
+
reasons[reason] = reasons.get(reason, 0) + 1
|
|
8129
|
+
out["recent_rejected_count_24h"] = count
|
|
8130
|
+
out["recent_rejection_reasons_24h"] = reasons
|
|
8131
|
+
except OSError:
|
|
8132
|
+
pass
|
|
8133
|
+
|
|
8134
|
+
# 5) watchlist account count
|
|
8135
|
+
try:
|
|
8136
|
+
cfg = load_watchlist()
|
|
8137
|
+
accounts = cfg.get("accounts") or []
|
|
8138
|
+
out["watchlist_account_count"] = len(accounts)
|
|
8139
|
+
except Exception:
|
|
8140
|
+
pass
|
|
8141
|
+
|
|
8142
|
+
return _with_next_steps("vendor_news_health", out)
|
|
8143
|
+
|
|
8144
|
+
|
|
8145
|
+
@mcp.tool()
|
|
8146
|
+
def delimit_vendor_news_draft(tweet_id: str = "", dry_run: bool = False) -> Dict[str, Any]:
|
|
8147
|
+
"""Draft a brand-voice Delimit-POV riff for a specific X tweet (Pro, LED-1253).
|
|
8148
|
+
|
|
8149
|
+
Fetches the source tweet via the cached twttr241 path (same one
|
|
8150
|
+
``delimit_x_fetch`` uses), shapes it into the dict ``draft_vendor_riff``
|
|
8151
|
+
expects, then runs the riff drafter end-to-end (rate cap, source-fit
|
|
8152
|
+
pre-filter, generator, capability validator, fit floor, queue insert).
|
|
8153
|
+
|
|
8154
|
+
When ``dry_run=True``, the underlying drafter still produces text and
|
|
8155
|
+
runs validators, but the queue insertion is skipped — useful for
|
|
8156
|
+
inspecting what the riff would look like without committing it. The
|
|
8157
|
+
24h per-vendor rate cap IS still consulted (we don't bypass it on
|
|
8158
|
+
dry runs because spurious dry runs would otherwise look like riffs
|
|
8159
|
+
in the rejected log).
|
|
8160
|
+
|
|
8161
|
+
Args:
|
|
8162
|
+
tweet_id: Source X tweet id (numeric string) or full x.com URL.
|
|
8163
|
+
dry_run: If True, suppress queue insertion. Default False.
|
|
8164
|
+
"""
|
|
8165
|
+
from ai.license import require_premium
|
|
8166
|
+
gate = require_premium("vendor_news_draft")
|
|
8167
|
+
if gate:
|
|
8168
|
+
return gate
|
|
8169
|
+
|
|
8170
|
+
raw = (tweet_id or "").strip()
|
|
8171
|
+
if not raw:
|
|
8172
|
+
return _with_next_steps("vendor_news_draft", {
|
|
8173
|
+
"error": "missing_tweet_id",
|
|
8174
|
+
"message": "tweet_id is required (status id or x.com URL)",
|
|
8175
|
+
})
|
|
8176
|
+
|
|
8177
|
+
# Normalize id (accept URL or bare id)
|
|
8178
|
+
try:
|
|
8179
|
+
from ai.social_target import extract_status_id, fetch_tweet_by_id
|
|
8180
|
+
except Exception as exc:
|
|
8181
|
+
return _with_next_steps("vendor_news_draft", {
|
|
8182
|
+
"error": "social_target_unavailable",
|
|
8183
|
+
"message": str(exc),
|
|
8184
|
+
})
|
|
8185
|
+
|
|
8186
|
+
sid = extract_status_id(raw)
|
|
8187
|
+
if not sid:
|
|
8188
|
+
return _with_next_steps("vendor_news_draft", {
|
|
8189
|
+
"error": "invalid_tweet_id",
|
|
8190
|
+
"message": f"could not parse status id from {raw!r}",
|
|
8191
|
+
})
|
|
8192
|
+
|
|
8193
|
+
fetched = fetch_tweet_by_id(sid)
|
|
8194
|
+
if not isinstance(fetched, dict) or fetched.get("error"):
|
|
8195
|
+
return _with_next_steps("vendor_news_draft", {
|
|
8196
|
+
"error": "fetch_failed",
|
|
8197
|
+
"message": (fetched or {}).get("error", "unknown fetch error"),
|
|
8198
|
+
"tweet_id": sid,
|
|
8199
|
+
})
|
|
8200
|
+
|
|
8201
|
+
# Map watchlist vendor metadata onto the source author. Falls back
|
|
8202
|
+
# gracefully if the tweet author isn't in the watchlist (e.g.
|
|
8203
|
+
# founder is testing a one-off riff against an off-watchlist post).
|
|
8204
|
+
try:
|
|
8205
|
+
from ai.vendor_news.sensor import load_watchlist
|
|
8206
|
+
from ai.vendor_news import draft_vendor_riff
|
|
8207
|
+
except Exception as exc:
|
|
8208
|
+
return _with_next_steps("vendor_news_draft", {
|
|
8209
|
+
"error": "vendor_news_unavailable",
|
|
8210
|
+
"message": str(exc),
|
|
8211
|
+
})
|
|
8212
|
+
|
|
8213
|
+
author = (fetched.get("author") or "").lstrip("@")
|
|
8214
|
+
vendor_name = ""
|
|
8215
|
+
products: List[str] = []
|
|
8216
|
+
try:
|
|
8217
|
+
cfg = load_watchlist()
|
|
8218
|
+
for acc in cfg.get("accounts") or []:
|
|
8219
|
+
if (acc.get("handle") or "").lstrip("@").lower() == author.lower():
|
|
8220
|
+
vendor_name = acc.get("vendor", "") or author
|
|
8221
|
+
products = list(acc.get("products") or [])
|
|
8222
|
+
break
|
|
8223
|
+
except Exception:
|
|
8224
|
+
pass
|
|
8225
|
+
if not vendor_name:
|
|
8226
|
+
vendor_name = author or "unknown"
|
|
8227
|
+
|
|
8228
|
+
triggered = {
|
|
8229
|
+
"id": fetched.get("id") or sid,
|
|
8230
|
+
"text": fetched.get("text") or "",
|
|
8231
|
+
"author": author,
|
|
8232
|
+
"url": fetched.get("url") or f"https://x.com/i/status/{sid}",
|
|
8233
|
+
"created_at": fetched.get("created_at") or "",
|
|
8234
|
+
"metrics": fetched.get("metrics") or {},
|
|
8235
|
+
"vendor": vendor_name,
|
|
8236
|
+
"products": products,
|
|
8237
|
+
"trigger_reason": "manual_draft",
|
|
8238
|
+
}
|
|
8239
|
+
|
|
8240
|
+
# On dry_run, route the queue write to a temp path so the real
|
|
8241
|
+
# tweet queue is untouched. Drafter still runs validator + fit gates.
|
|
8242
|
+
queue_path_override = None
|
|
8243
|
+
if dry_run:
|
|
8244
|
+
try:
|
|
8245
|
+
import tempfile as _tempfile
|
|
8246
|
+
tmp = _tempfile.NamedTemporaryFile(
|
|
8247
|
+
mode="w", suffix=".json", prefix="vendor_news_dry_",
|
|
8248
|
+
delete=False, encoding="utf-8",
|
|
8249
|
+
)
|
|
8250
|
+
tmp.write("[]")
|
|
8251
|
+
tmp.close()
|
|
8252
|
+
queue_path_override = Path(tmp.name)
|
|
8253
|
+
except Exception:
|
|
8254
|
+
queue_path_override = None
|
|
8255
|
+
|
|
8256
|
+
try:
|
|
8257
|
+
if queue_path_override is not None:
|
|
8258
|
+
res = draft_vendor_riff(triggered, queue_path=queue_path_override)
|
|
8259
|
+
else:
|
|
8260
|
+
res = draft_vendor_riff(triggered)
|
|
8261
|
+
except Exception as exc:
|
|
8262
|
+
return _with_next_steps("vendor_news_draft", {
|
|
8263
|
+
"error": "drafter_failed",
|
|
8264
|
+
"message": str(exc),
|
|
8265
|
+
"tweet_id": sid,
|
|
8266
|
+
})
|
|
8267
|
+
|
|
8268
|
+
out = {
|
|
8269
|
+
"decision": res.get("decision"),
|
|
8270
|
+
"text": res.get("text"),
|
|
8271
|
+
"reason": res.get("reason"),
|
|
8272
|
+
"queue_entry": res.get("queue_entry") if not dry_run else None,
|
|
8273
|
+
"validator_result": res.get("validator_result"),
|
|
8274
|
+
"fit_result": res.get("fit_result"),
|
|
8275
|
+
"source": {
|
|
8276
|
+
"id": triggered["id"],
|
|
8277
|
+
"author": author,
|
|
8278
|
+
"vendor": vendor_name,
|
|
8279
|
+
"url": triggered["url"],
|
|
8280
|
+
},
|
|
8281
|
+
"dry_run": bool(dry_run),
|
|
8282
|
+
}
|
|
8283
|
+
return _with_next_steps("vendor_news_draft", out)
|
|
8284
|
+
|
|
8285
|
+
|
|
7869
8286
|
# ═══════════════════════════════════════════════════════════════════════
|
|
7870
8287
|
# CONTENT ENGINE - Autonomous video + tweet pipeline (Pro)
|
|
7871
8288
|
# ═══════════════════════════════════════════════════════════════════════
|
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.5.
|
|
4
|
+
"version": "4.5.6",
|
|
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": [
|