delimit-cli 3.15.14 → 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 +1 -1
- package/gateway/ai/license_core.py +2 -1
- package/gateway/ai/notify.py +12 -12
- package/gateway/ai/server.py +49 -332
- package/gateway/ai/swarm.py +2 -2
- package/gateway/core/contract_ledger.py +1 -1
- package/gateway/core/dependency_graph.py +1 -1
- package/gateway/core/dependency_manifest.py +1 -1
- package/gateway/core/event_backbone.py +2 -2
- package/gateway/core/event_schema.py +1 -1
- package/gateway/core/impact_analyzer.py +1 -1
- package/package.json +4 -1
- package/scripts/crosspost_devto.py +0 -304
- package/scripts/security-check.sh +0 -66
- package/scripts/weekly-tweet.py +0 -191
package/gateway/ai/swarm.py
CHANGED
|
@@ -39,7 +39,7 @@ DEFAULT_ROSTER = {
|
|
|
39
39
|
"fallback_model": "codex-gpt-5.4",
|
|
40
40
|
},
|
|
41
41
|
"ops": {
|
|
42
|
-
"role": "Strategy, Deliberation,
|
|
42
|
+
"role": "Strategy, Deliberation, Community, Analysis",
|
|
43
43
|
"default_model": "grok-4",
|
|
44
44
|
"fallback_model": "gemini-3.1-pro-preview",
|
|
45
45
|
},
|
|
@@ -274,7 +274,7 @@ APPROVAL_TIERS = {
|
|
|
274
274
|
"deploy_staging": "auto_approved",
|
|
275
275
|
"social_post": "founder_email",
|
|
276
276
|
"social_low_risk": "auto_after_consensus",
|
|
277
|
-
"
|
|
277
|
+
"community_issue": "founder_email",
|
|
278
278
|
"ledger_update": "auto_approved",
|
|
279
279
|
"code_commit": "auto_approved",
|
|
280
280
|
"security_audit": "auto_approved",
|
|
@@ -3,7 +3,7 @@ Delimit Contract Ledger
|
|
|
3
3
|
Reads, validates, and queries the append-only JSONL event ledger.
|
|
4
4
|
Optional SQLite index for fast lookups (never required for CI).
|
|
5
5
|
|
|
6
|
-
Per
|
|
6
|
+
Per Delimit Stability Contract:
|
|
7
7
|
- Deterministic outputs
|
|
8
8
|
- Append-only artifacts
|
|
9
9
|
- SQLite index is optional, not required for CI
|
|
@@ -5,7 +5,7 @@ Constructs a deterministic service dependency graph from manifests.
|
|
|
5
5
|
The graph maps each API/service to its downstream consumers,
|
|
6
6
|
enabling impact analysis when an API contract changes.
|
|
7
7
|
|
|
8
|
-
Per
|
|
8
|
+
Per Delimit Stability Contract:
|
|
9
9
|
- Deterministic outputs (sorted, reproducible)
|
|
10
10
|
- No telemetry
|
|
11
11
|
- Graceful degradation when manifests are missing
|
|
@@ -3,7 +3,7 @@ Delimit Event Backbone
|
|
|
3
3
|
Constructs ledger events, generates SHA-256 hashes, links hash chains,
|
|
4
4
|
and appends to the append-only JSONL ledger.
|
|
5
5
|
|
|
6
|
-
Per
|
|
6
|
+
Per Delimit Stability Contract:
|
|
7
7
|
- Deterministic outputs
|
|
8
8
|
- Append-only artifacts
|
|
9
9
|
- Fail-closed CI behavior (ledger failures never affect CI)
|
|
@@ -199,7 +199,7 @@ class EventBackbone:
|
|
|
199
199
|
This is the primary API for event generation. It is best-effort:
|
|
200
200
|
if the ledger write fails, the event is still returned but not persisted.
|
|
201
201
|
|
|
202
|
-
CRITICAL: This method NEVER raises exceptions. Per
|
|
202
|
+
CRITICAL: This method NEVER raises exceptions. Per Delimit Stability Contract,
|
|
203
203
|
ledger failures must not affect CI pass/fail outcome.
|
|
204
204
|
|
|
205
205
|
Returns:
|
|
@@ -3,7 +3,7 @@ Delimit Impact Analyzer
|
|
|
3
3
|
Determines downstream consumers affected by an API change
|
|
4
4
|
and produces informational impact summaries for CI output.
|
|
5
5
|
|
|
6
|
-
Per
|
|
6
|
+
Per Delimit Stability Contract:
|
|
7
7
|
- Impact analysis is INFORMATIONAL ONLY
|
|
8
8
|
- NEVER affects CI pass/fail outcome
|
|
9
9
|
- Deterministic outputs
|
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
|
+
"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,66 +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. Proprietary files that shouldn't ship
|
|
46
|
-
echo -n " Proprietary files... "
|
|
47
|
-
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"
|
|
48
|
-
if find "$TMPDIR/package/" -name "*.py" | grep -Ei "$PROPRIETARY" 2>/dev/null; then
|
|
49
|
-
echo "❌ PROPRIETARY FILES IN PACKAGE"
|
|
50
|
-
FAIL=1
|
|
51
|
-
else
|
|
52
|
-
echo "✅ clean"
|
|
53
|
-
fi
|
|
54
|
-
|
|
55
|
-
# Cleanup
|
|
56
|
-
rm -rf "$TMPDIR"
|
|
57
|
-
|
|
58
|
-
if [ $FAIL -ne 0 ]; then
|
|
59
|
-
echo ""
|
|
60
|
-
echo "❌ SECURITY CHECK FAILED — do not publish"
|
|
61
|
-
exit 1
|
|
62
|
-
fi
|
|
63
|
-
|
|
64
|
-
echo ""
|
|
65
|
-
echo "✅ All security checks passed"
|
|
66
|
-
exit 0
|
package/scripts/weekly-tweet.py
DELETED
|
@@ -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()
|