delimit-cli 4.5.2 → 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/README.md +13 -3
- package/gateway/ai/remote_resolve.py +422 -0
- package/gateway/ai/server.py +173 -111
- package/gateway/ai/social_capability/capability_validator.py +107 -13
- package/gateway/ai/social_capability/fit_floor.py +360 -0
- 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 +146 -5
- package/package.json +18 -2
- package/adapters/codex-security.js +0 -64
- package/adapters/codex-skill.js +0 -78
package/gateway/ai/x_ranker.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""X engagement ranker (LED-216 Phase 2
|
|
1
|
+
"""X engagement ranker (LED-216 Phase 2 + LED-1240 part B selectivity bar).
|
|
2
2
|
|
|
3
3
|
Filters and orders X (Twitter) candidate posts so we reply to the highest-
|
|
4
4
|
yield originals only. The score formula and the 7-day author dedupe are the
|
|
@@ -15,7 +15,19 @@ Filter pipeline applied in order:
|
|
|
15
15
|
2. ``lang == 'en'`` → drop non-English (US/UK builder community is the wedge)
|
|
16
16
|
3. ``is_retweet == False`` → drop retweets
|
|
17
17
|
4. dedupe authors we replied to in the last ``replied_authors_window_hours``
|
|
18
|
-
5.
|
|
18
|
+
5. **Engagement-score floor** (LED-1240 part B) — drop low-yield threads
|
|
19
|
+
outright. Configured via ``MIN_ENGAGEMENT_SCORE`` (default 1.5, was 0).
|
|
20
|
+
6. **Delimit-fit floor** (LED-1240 part B) — drop threads that don't
|
|
21
|
+
match Delimit-domain or orbit-with-context signals. High-engagement
|
|
22
|
+
threads above ``HIGH_ENGAGEMENT_OPPORTUNITY_COST`` pass through but
|
|
23
|
+
are flagged ``_human_only=True`` so callers do NOT auto-draft.
|
|
24
|
+
7. **Topic-coverage cooldown** (LED-1240 part B) — drop threads whose
|
|
25
|
+
matched signals overlap topics we drafted on inside the last 7 days.
|
|
26
|
+
8. sort by score DESC
|
|
27
|
+
|
|
28
|
+
Rejected candidates are logged to ``~/.delimit/x_rejected_targets.jsonl``
|
|
29
|
+
(append-only, one JSON line per rejection) so we can audit the bar over
|
|
30
|
+
time without changing the return shape.
|
|
19
31
|
|
|
20
32
|
Tolerant defaults: any field missing on a target is treated as "do not drop"
|
|
21
33
|
so partial Twttr241 payloads still rank instead of being silently filtered.
|
|
@@ -32,9 +44,29 @@ logger = logging.getLogger(__name__)
|
|
|
32
44
|
|
|
33
45
|
|
|
34
46
|
SOCIAL_LOG = Path.home() / ".delimit" / "social_log.jsonl"
|
|
47
|
+
REJECTED_TARGETS_LOG = Path.home() / ".delimit" / "x_rejected_targets.jsonl"
|
|
35
48
|
|
|
36
49
|
DEFAULT_WINDOW_HOURS = 24 * 7 # 7 days, per founder's anti-spammy directive
|
|
37
50
|
|
|
51
|
+
# LED-1240 part B selectivity bar.
|
|
52
|
+
# Founder directive 2026-05-05: raise the engagement floor and add a fit
|
|
53
|
+
# floor BEFORE we waste LLM tokens drafting for noise-grade threads.
|
|
54
|
+
#
|
|
55
|
+
# Raising MIN_ENGAGEMENT_SCORE from 0 → 1.5 cuts the long tail of
|
|
56
|
+
# ``_rank_score=0.0`` candidates we observed in the recent X target log
|
|
57
|
+
# (every recent X target with 0 likes / 0 retweets had score=0.0). The
|
|
58
|
+
# threshold is conservative — a single-hour-old post with 1.5 likes still
|
|
59
|
+
# passes — but eliminates the zero-engagement floor.
|
|
60
|
+
MIN_ENGAGEMENT_SCORE = 1.5
|
|
61
|
+
|
|
62
|
+
# Threads above this score pass the Delimit-fit floor even without a
|
|
63
|
+
# keyword match, but are flagged ``_human_only=True`` so the caller does
|
|
64
|
+
# NOT auto-draft. The orchestrator decides whether to surface them.
|
|
65
|
+
HIGH_ENGAGEMENT_OPPORTUNITY_COST = 50.0
|
|
66
|
+
|
|
67
|
+
# Topic-coverage cooldown — same window as author dedupe (7 days).
|
|
68
|
+
DEFAULT_COOLDOWN_DAYS = 7
|
|
69
|
+
|
|
38
70
|
|
|
39
71
|
# ---------------------------------------------------------------------------
|
|
40
72
|
# Helpers
|
|
@@ -213,10 +245,44 @@ def _is_retweet(target: Dict[str, Any]) -> bool:
|
|
|
213
245
|
return False
|
|
214
246
|
|
|
215
247
|
|
|
248
|
+
def _log_rejection(target: Dict[str, Any], score: float, reason: str,
|
|
249
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
250
|
+
log_path: Optional[Path] = None) -> None:
|
|
251
|
+
"""Append a single JSON line to the rejected-targets audit log.
|
|
252
|
+
|
|
253
|
+
Best-effort — never raises into the caller's pipeline.
|
|
254
|
+
"""
|
|
255
|
+
p = log_path or REJECTED_TARGETS_LOG
|
|
256
|
+
snippet = (target.get("content_snippet") or target.get("text") or "")[:200]
|
|
257
|
+
payload: Dict[str, Any] = {
|
|
258
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
259
|
+
"tweet_id": target.get("source_id") or target.get("id_str") or target.get("id") or "",
|
|
260
|
+
"fingerprint": target.get("fingerprint") or "",
|
|
261
|
+
"author": target.get("author") or "",
|
|
262
|
+
"score": round(float(score), 4),
|
|
263
|
+
"snippet": snippet,
|
|
264
|
+
"reason": reason,
|
|
265
|
+
}
|
|
266
|
+
if extra:
|
|
267
|
+
payload.update(extra)
|
|
268
|
+
try:
|
|
269
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
with open(p, "a", encoding="utf-8") as fh:
|
|
271
|
+
fh.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
272
|
+
except OSError as exc: # pragma: no cover — best-effort logging
|
|
273
|
+
logger.warning("x_ranker: failed to write rejection log %s: %s", p, exc)
|
|
274
|
+
|
|
275
|
+
|
|
216
276
|
def rank_x_targets(
|
|
217
277
|
targets: Iterable[Dict[str, Any]],
|
|
218
278
|
replied_authors_window_hours: int = DEFAULT_WINDOW_HOURS,
|
|
219
279
|
replied_authors: Optional[Set[str]] = None,
|
|
280
|
+
min_engagement_score: float = MIN_ENGAGEMENT_SCORE,
|
|
281
|
+
high_engagement_floor: float = HIGH_ENGAGEMENT_OPPORTUNITY_COST,
|
|
282
|
+
cooldown_days: int = DEFAULT_COOLDOWN_DAYS,
|
|
283
|
+
recent_topics: Optional[Set[str]] = None,
|
|
284
|
+
enable_fit_floor: bool = True,
|
|
285
|
+
log_rejections: bool = True,
|
|
220
286
|
) -> List[Dict[str, Any]]:
|
|
221
287
|
"""Filter and sort X targets by engagement.
|
|
222
288
|
|
|
@@ -227,18 +293,45 @@ def rank_x_targets(
|
|
|
227
293
|
replied_authors: explicit author set (lowercase, no leading ``@``).
|
|
228
294
|
When ``None``, the set is read from ``~/.delimit/social_log.jsonl``.
|
|
229
295
|
Tests inject an explicit set to avoid touching disk.
|
|
296
|
+
min_engagement_score: hard floor on the engagement-rate score.
|
|
297
|
+
Threads below this are dropped before fit-floor evaluation.
|
|
298
|
+
Default ``MIN_ENGAGEMENT_SCORE`` (1.5).
|
|
299
|
+
high_engagement_floor: score above which a thread can pass the
|
|
300
|
+
fit floor without a keyword match (flagged ``_human_only=True``).
|
|
301
|
+
cooldown_days: topic-coverage cooldown window in days.
|
|
302
|
+
recent_topics: explicit set of topic fingerprints already drafted on.
|
|
303
|
+
``None`` reads from ``social_log.jsonl``. Tests inject directly.
|
|
304
|
+
enable_fit_floor: kill switch for the LED-1240 part B gates. False
|
|
305
|
+
preserves the legacy behavior (no fit floor, no min-score, no
|
|
306
|
+
cooldown) — used by tests that target the legacy filter chain
|
|
307
|
+
in isolation. Default True.
|
|
308
|
+
log_rejections: when True, append rejection records to
|
|
309
|
+
``~/.delimit/x_rejected_targets.jsonl``. Tests disable this to
|
|
310
|
+
keep the founder's audit log clean.
|
|
230
311
|
|
|
231
312
|
Returns:
|
|
232
313
|
A new list sorted by engagement score DESC. Each item gets a
|
|
233
314
|
``_rank_score`` key for downstream observability. Filtered items are
|
|
234
315
|
dropped (not kept with score=0) so the caller can blindly slice the
|
|
235
|
-
first N.
|
|
316
|
+
first N. High-engagement-but-off-topic items survive with
|
|
317
|
+
``_human_only=True`` so the caller can route them to human review
|
|
318
|
+
without auto-drafting.
|
|
236
319
|
"""
|
|
237
320
|
if replied_authors is None:
|
|
238
321
|
replied_authors = _replied_authors_within(replied_authors_window_hours)
|
|
239
322
|
else:
|
|
240
323
|
replied_authors = {_normalize_handle(a) for a in replied_authors}
|
|
241
324
|
|
|
325
|
+
if enable_fit_floor and recent_topics is None:
|
|
326
|
+
try:
|
|
327
|
+
from ai.social_capability.fit_floor import _recent_topic_fingerprints
|
|
328
|
+
recent_topics = _recent_topic_fingerprints(cooldown_days=cooldown_days)
|
|
329
|
+
except Exception as exc: # pragma: no cover — tolerant fallback
|
|
330
|
+
logger.warning("x_ranker: cooldown bootstrap failed (%s) — proceeding without", exc)
|
|
331
|
+
recent_topics = set()
|
|
332
|
+
elif recent_topics is None:
|
|
333
|
+
recent_topics = set()
|
|
334
|
+
|
|
242
335
|
survivors: List[Dict[str, Any]] = []
|
|
243
336
|
for t in targets or []:
|
|
244
337
|
if not isinstance(t, dict):
|
|
@@ -260,17 +353,65 @@ def rank_x_targets(
|
|
|
260
353
|
if author_norm and author_norm in replied_authors:
|
|
261
354
|
continue
|
|
262
355
|
|
|
356
|
+
score = round(score_target(t), 4)
|
|
357
|
+
|
|
358
|
+
# 5. engagement-score floor (LED-1240 part B). Off by default for
|
|
359
|
+
# legacy callers that pass enable_fit_floor=False.
|
|
360
|
+
if enable_fit_floor and score < min_engagement_score:
|
|
361
|
+
if log_rejections:
|
|
362
|
+
_log_rejection(
|
|
363
|
+
t, score, "below_engagement_floor",
|
|
364
|
+
extra={"min_score": min_engagement_score},
|
|
365
|
+
)
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
# 6 + 7. Delimit-fit floor + topic cooldown.
|
|
263
369
|
scored = dict(t)
|
|
264
|
-
scored["_rank_score"] =
|
|
370
|
+
scored["_rank_score"] = score
|
|
371
|
+
|
|
372
|
+
if enable_fit_floor:
|
|
373
|
+
try:
|
|
374
|
+
from ai.social_capability.fit_floor import evaluate_fit
|
|
375
|
+
except Exception as exc: # pragma: no cover — tolerant fallback
|
|
376
|
+
logger.warning("x_ranker: fit_floor import failed (%s) — passing through", exc)
|
|
377
|
+
survivors.append(scored)
|
|
378
|
+
continue
|
|
379
|
+
|
|
380
|
+
text = t.get("content_snippet") or t.get("text") or ""
|
|
381
|
+
verdict = evaluate_fit(
|
|
382
|
+
text,
|
|
383
|
+
engagement_score=score,
|
|
384
|
+
high_engagement_floor=high_engagement_floor,
|
|
385
|
+
recent_topics=recent_topics,
|
|
386
|
+
)
|
|
387
|
+
if not verdict["passed"]:
|
|
388
|
+
if log_rejections:
|
|
389
|
+
_log_rejection(
|
|
390
|
+
t, score, verdict["reason"],
|
|
391
|
+
extra={
|
|
392
|
+
"matched_signals": verdict.get("matched_signals", []),
|
|
393
|
+
"topic_fingerprint": verdict.get("topic_fingerprint", []),
|
|
394
|
+
},
|
|
395
|
+
)
|
|
396
|
+
continue
|
|
397
|
+
scored["_fit_reason"] = verdict["reason"]
|
|
398
|
+
scored["_human_only"] = bool(verdict.get("human_only"))
|
|
399
|
+
scored["_matched_signals"] = verdict.get("matched_signals", [])
|
|
400
|
+
scored["_topic_fingerprint"] = verdict.get("topic_fingerprint", [])
|
|
401
|
+
|
|
265
402
|
survivors.append(scored)
|
|
266
403
|
|
|
267
|
-
#
|
|
404
|
+
# 8. sort score DESC, stable
|
|
268
405
|
survivors.sort(key=lambda x: x.get("_rank_score", 0.0), reverse=True)
|
|
269
406
|
return survivors
|
|
270
407
|
|
|
271
408
|
|
|
272
409
|
__all__ = [
|
|
273
410
|
"DEFAULT_WINDOW_HOURS",
|
|
411
|
+
"DEFAULT_COOLDOWN_DAYS",
|
|
412
|
+
"MIN_ENGAGEMENT_SCORE",
|
|
413
|
+
"HIGH_ENGAGEMENT_OPPORTUNITY_COST",
|
|
414
|
+
"REJECTED_TARGETS_LOG",
|
|
274
415
|
"score_target",
|
|
275
416
|
"rank_x_targets",
|
|
276
417
|
]
|
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.3",
|
|
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": [
|
|
@@ -85,7 +85,23 @@
|
|
|
85
85
|
"gemini-cli",
|
|
86
86
|
"cursor",
|
|
87
87
|
"ai-governance",
|
|
88
|
-
"ai-agents"
|
|
88
|
+
"ai-agents",
|
|
89
|
+
"ai-code-review",
|
|
90
|
+
"ci-governance",
|
|
91
|
+
"merge-gate",
|
|
92
|
+
"attestation",
|
|
93
|
+
"signed-attestation",
|
|
94
|
+
"sigstore",
|
|
95
|
+
"sbom",
|
|
96
|
+
"supply-chain-security",
|
|
97
|
+
"json-schema",
|
|
98
|
+
"json-schema-diff",
|
|
99
|
+
"policy-as-code",
|
|
100
|
+
"audit-trail",
|
|
101
|
+
"ai-coding-assistant",
|
|
102
|
+
"pull-request",
|
|
103
|
+
"pr-comment",
|
|
104
|
+
"developer-tools"
|
|
89
105
|
],
|
|
90
106
|
"author": "Delimit AI <hello@delimit.ai>",
|
|
91
107
|
"license": "MIT",
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Delimit Security Skill for Codex CLI
|
|
4
|
-
*
|
|
5
|
-
* Validates that Codex-generated code doesn't introduce security anti-patterns.
|
|
6
|
-
* Runs as a security validation skill.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
|
|
12
|
-
const SECURITY_PATTERNS = [
|
|
13
|
-
{ pattern: /eval\s*\(/g, severity: 'high', message: 'eval() usage detected — potential code injection' }, // nosec B-eval_usage: regex-pattern DEFINITION string for security scanner
|
|
14
|
-
{ pattern: /exec\s*\(/g, severity: 'medium', message: 'exec() usage — verify input sanitization' }, // nosec B-exec_usage: regex-pattern DEFINITION string for security scanner
|
|
15
|
-
{ pattern: /shell\s*=\s*True/g, severity: 'high', message: 'subprocess with shell=True — command injection risk' },
|
|
16
|
-
{ pattern: /dangerouslySetInnerHTML/g, severity: 'medium', message: 'dangerouslySetInnerHTML — XSS risk' }, // nosec B-dangerous_innerHTML: regex-pattern DEFINITION string
|
|
17
|
-
{ pattern: /password\s*=\s*["'][^"']+["']/gi, severity: 'high', message: 'Hardcoded password detected' },
|
|
18
|
-
{ pattern: /api[_-]?key\s*=\s*["'][A-Za-z0-9]{10,}["']/gi, severity: 'high', message: 'Hardcoded API key detected' },
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
function checkSecurity(context) {
|
|
22
|
-
const code = context.code || context.content || '';
|
|
23
|
-
if (!code) return { status: 'clean', findings: [] };
|
|
24
|
-
|
|
25
|
-
const findings = [];
|
|
26
|
-
for (const { pattern, severity, message } of SECURITY_PATTERNS) {
|
|
27
|
-
const matches = code.match(pattern);
|
|
28
|
-
if (matches) {
|
|
29
|
-
findings.push({ severity, message, count: matches.length });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Audit
|
|
34
|
-
try {
|
|
35
|
-
const auditDir = path.join(process.env.HOME || '', '.delimit', 'audit');
|
|
36
|
-
fs.mkdirSync(auditDir, { recursive: true });
|
|
37
|
-
const record = {
|
|
38
|
-
timestamp: new Date().toISOString(),
|
|
39
|
-
source: 'codex-security',
|
|
40
|
-
findings_count: findings.length,
|
|
41
|
-
high_count: findings.filter(f => f.severity === 'high').length,
|
|
42
|
-
};
|
|
43
|
-
const auditFile = path.join(auditDir, `${new Date().toISOString().split('T')[0]}.jsonl`);
|
|
44
|
-
fs.appendFileSync(auditFile, JSON.stringify(record) + '\n');
|
|
45
|
-
} catch {}
|
|
46
|
-
|
|
47
|
-
const hasHigh = findings.some(f => f.severity === 'high');
|
|
48
|
-
return {
|
|
49
|
-
status: hasHigh ? 'flagged' : findings.length > 0 ? 'warnings' : 'clean',
|
|
50
|
-
findings,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const context = process.argv[2] ? JSON.parse(process.argv[2]) : {};
|
|
55
|
-
const result = checkSecurity(context);
|
|
56
|
-
|
|
57
|
-
if (result.status === 'flagged') {
|
|
58
|
-
for (const f of result.findings) {
|
|
59
|
-
console.error(`[Delimit Security] ${f.severity.toUpperCase()}: ${f.message}`);
|
|
60
|
-
}
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
process.exit(0);
|
package/adapters/codex-skill.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Delimit Governance Skill for Codex CLI
|
|
4
|
-
*
|
|
5
|
-
* Runs as a validation skill triggered on pre-code-generation and pre-suggestion.
|
|
6
|
-
* Checks governance state and policy compliance before Codex executes actions.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
|
|
12
|
-
const DELIMIT_HOME = path.join(process.env.HOME || '', '.delimit');
|
|
13
|
-
const MODE_FILE = path.join(DELIMIT_HOME, 'enforcement_mode');
|
|
14
|
-
|
|
15
|
-
function getMode() {
|
|
16
|
-
try {
|
|
17
|
-
return fs.readFileSync(MODE_FILE, 'utf-8').trim();
|
|
18
|
-
} catch {
|
|
19
|
-
return 'guarded'; // Default
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function checkGovernance(context) {
|
|
24
|
-
const mode = getMode();
|
|
25
|
-
const warnings = [];
|
|
26
|
-
|
|
27
|
-
// Check if governance is initialized
|
|
28
|
-
const policiesFile = path.join(process.cwd(), '.delimit', 'policies.yml');
|
|
29
|
-
if (!fs.existsSync(policiesFile)) {
|
|
30
|
-
warnings.push('No .delimit/policies.yml — run: delimit init');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Check for sensitive file access
|
|
34
|
-
const sensitivePatterns = ['.env', 'credentials', '.ssh', 'secrets'];
|
|
35
|
-
const target = context.target || context.file || '';
|
|
36
|
-
for (const pattern of sensitivePatterns) {
|
|
37
|
-
if (target.includes(pattern)) {
|
|
38
|
-
if (mode === 'enforce') {
|
|
39
|
-
return { status: 'blocked', reason: `Access to sensitive path: ${target}` };
|
|
40
|
-
}
|
|
41
|
-
warnings.push(`Accessing sensitive path: ${target}`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Audit log
|
|
46
|
-
try {
|
|
47
|
-
const auditDir = path.join(DELIMIT_HOME, 'audit');
|
|
48
|
-
fs.mkdirSync(auditDir, { recursive: true });
|
|
49
|
-
const record = {
|
|
50
|
-
timestamp: new Date().toISOString(),
|
|
51
|
-
source: 'codex-skill',
|
|
52
|
-
mode,
|
|
53
|
-
context: typeof context === 'object' ? JSON.stringify(context).slice(0, 200) : String(context).slice(0, 200),
|
|
54
|
-
warnings,
|
|
55
|
-
};
|
|
56
|
-
const auditFile = path.join(auditDir, `${new Date().toISOString().split('T')[0]}.jsonl`);
|
|
57
|
-
fs.appendFileSync(auditFile, JSON.stringify(record) + '\n');
|
|
58
|
-
} catch {}
|
|
59
|
-
|
|
60
|
-
return { status: 'allowed', mode, warnings };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Entry point — read context from stdin or args
|
|
64
|
-
const context = process.argv[2] ? JSON.parse(process.argv[2]) : {};
|
|
65
|
-
const result = checkGovernance(context);
|
|
66
|
-
|
|
67
|
-
if (result.status === 'blocked') {
|
|
68
|
-
console.error(`[Delimit] BLOCKED: ${result.reason}`);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (result.warnings.length > 0) {
|
|
73
|
-
for (const w of result.warnings) {
|
|
74
|
-
console.error(`[Delimit] Warning: ${w}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
process.exit(0);
|