delimit-cli 3.13.0 → 3.13.2
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 +14 -0
- package/README.md +1 -0
- package/bin/delimit-setup.js +66 -16
- package/lib/cross-model-hooks.js +33 -4
- package/package.json +3 -2
- package/scripts/postinstall.js +35 -0
- package/scripts/weekly-tweet.py +191 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.13.1] - 2026-03-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `delimit init` guided onboarding wizard with framework auto-detection (Express, NestJS, FastAPI, Django, Flask, Fastify, Hono, Next.js)
|
|
7
|
+
- Interactive preset selection (strict/default/relaxed) with context-aware defaults
|
|
8
|
+
- First lint runs automatically after init — see governance results in under 1 second
|
|
9
|
+
- Zero-Spec baseline auto-saved for FastAPI/Express/NestJS projects on first init
|
|
10
|
+
- GitHub Action workflow generation with confirmation prompt
|
|
11
|
+
- `--yes` flag for non-interactive CI usage
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `delimit init` now detects CI provider (GitHub Actions, GitLab) and adapts workflow generation
|
|
15
|
+
- OpenAPI spec detection expanded to 17 common file locations
|
|
16
|
+
|
|
3
17
|
## [3.12.0] - 2026-03-26
|
|
4
18
|
|
|
5
19
|
### Added
|
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, govern
|
|
|
6
6
|
[](https://github.com/marketplace/actions/delimit-api-governance)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
[](https://glama.ai/mcp/servers/delimit-ai/delimit-mcp-server)
|
|
9
|
+
[](https://github.com/marketplace/actions/delimit-api-governance)
|
|
9
10
|
|
|
10
11
|
<p align="center">
|
|
11
12
|
<img src="docs/demo.gif" alt="Delimit detecting breaking API changes" width="700">
|
package/bin/delimit-setup.js
CHANGED
|
@@ -47,6 +47,27 @@ function findGitDir(startDir) {
|
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Recursively find OpenAPI/Swagger spec files, ignoring node_modules.
|
|
52
|
+
*/
|
|
53
|
+
function findSpecFiles(dir, depth = 0) {
|
|
54
|
+
if (depth > 5) return [];
|
|
55
|
+
const results = [];
|
|
56
|
+
try {
|
|
57
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'vendor') continue;
|
|
60
|
+
const fullPath = path.join(dir, entry.name);
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
results.push(...findSpecFiles(fullPath, depth + 1));
|
|
63
|
+
} else if (/^(openapi|swagger)[^/]*\.(ya?ml|json)$/i.test(entry.name)) {
|
|
64
|
+
results.push(path.relative(process.cwd(), fullPath));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch {}
|
|
68
|
+
return results;
|
|
69
|
+
}
|
|
70
|
+
|
|
50
71
|
async function main() {
|
|
51
72
|
log('');
|
|
52
73
|
log(blue(' ____ ________ ______ _____________'));
|
|
@@ -535,10 +556,17 @@ printf " \${MAGENTA}\${BOLD}[Delimit]\${RESET} \${MAGENTA}═══════
|
|
|
535
556
|
sleep 0.08
|
|
536
557
|
printf " \${GREEN}\${BOLD}[Delimit]\${RESET} \${GREEN}✓ Allowed\${RESET}\\n"
|
|
537
558
|
echo ""
|
|
538
|
-
|
|
539
|
-
|
|
559
|
+
# Find real binary — check common paths then fallback to PATH search (excluding shim dir)
|
|
560
|
+
SELF="$(readlink -f "$0" 2>/dev/null || echo "$0")"
|
|
561
|
+
for c in /usr/bin/${toolName} /usr/local/bin/${toolName} "$HOME/.local/bin/${toolName}" "$(npm bin -g 2>/dev/null)/${toolName}"; do
|
|
562
|
+
[ -x "$c" ] && [ "$(readlink -f "$c" 2>/dev/null)" != "$SELF" ] && exec "$c" "$@"
|
|
540
563
|
done
|
|
541
|
-
|
|
564
|
+
# Last resort: search PATH excluding shim directory
|
|
565
|
+
REAL=$(PATH=$(echo "$PATH" | tr ':' '\\n' | grep -v '.delimit/shims' | tr '\\n' ':') command -v ${toolName} 2>/dev/null)
|
|
566
|
+
[ -x "$REAL" ] && exec "$REAL" "$@"
|
|
567
|
+
echo "[Delimit] ${toolName} not found in PATH" >&2
|
|
568
|
+
echo " Install: npm install -g @anthropic-ai/claude-code" >&2
|
|
569
|
+
exit 127
|
|
542
570
|
`;
|
|
543
571
|
|
|
544
572
|
for (const [tool, display] of [['claude', 'Claude'], ['codex', 'Codex'], ['gemini', 'Gemini CLI']]) {
|
|
@@ -717,8 +745,28 @@ echo "[Delimit] ${toolName} not found" >&2; exit 127
|
|
|
717
745
|
}
|
|
718
746
|
log('');
|
|
719
747
|
|
|
720
|
-
// Step
|
|
721
|
-
step(10, '
|
|
748
|
+
// Step 10: Auto-detect OpenAPI specs
|
|
749
|
+
step(10, 'Scanning for API specs...');
|
|
750
|
+
|
|
751
|
+
let detectedSpecs = [];
|
|
752
|
+
try {
|
|
753
|
+
const { minimatch } = (() => { try { return require('minimatch'); } catch { return { minimatch: null }; } })();
|
|
754
|
+
// Simple recursive glob for spec files
|
|
755
|
+
detectedSpecs = findSpecFiles(process.cwd());
|
|
756
|
+
} catch {}
|
|
757
|
+
|
|
758
|
+
if (detectedSpecs.length > 0) {
|
|
759
|
+
log(` ${green('✓')} Found ${detectedSpecs.length} API spec(s):`);
|
|
760
|
+
detectedSpecs.forEach(s => log(` ${s}`));
|
|
761
|
+
log('');
|
|
762
|
+
log(` Try: ${bold(`npx delimit-cli lint ${detectedSpecs[0]}`)}`);
|
|
763
|
+
} else {
|
|
764
|
+
log(` ${dim(' No OpenAPI/Swagger specs found in current directory')}`);
|
|
765
|
+
}
|
|
766
|
+
log('');
|
|
767
|
+
|
|
768
|
+
// Step 11: Done
|
|
769
|
+
step(11, 'Done!');
|
|
722
770
|
log('');
|
|
723
771
|
log(` ${green('Delimit is installed.')} Your AI now has persistent memory and governance.`);
|
|
724
772
|
log('');
|
|
@@ -730,22 +778,24 @@ echo "[Delimit] ${toolName} not found" >&2; exit 127
|
|
|
730
778
|
log(` ${green('✓')} ${tools.join(', ')}`);
|
|
731
779
|
|
|
732
780
|
log('');
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
log('');
|
|
736
|
-
log(`
|
|
737
|
-
log('');
|
|
738
|
-
log(
|
|
739
|
-
log(`
|
|
740
|
-
log(`
|
|
741
|
-
log(
|
|
781
|
+
|
|
782
|
+
// "What's next" box
|
|
783
|
+
log(' \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510');
|
|
784
|
+
log(` \u2502 ${bold('What\'s next:')} \u2502`);
|
|
785
|
+
log(' \u2502 \u2502');
|
|
786
|
+
log(` \u2502 1. ${blue('npx delimit-cli lint')} \u2502`);
|
|
787
|
+
log(` \u2502 2. ${blue('npx delimit-cli doctor')} \u2502`);
|
|
788
|
+
log(` \u2502 3. Add the GitHub Action to your repo \u2502`);
|
|
789
|
+
log(' \u2502 \u2502');
|
|
790
|
+
log(` \u2502 Docs: ${dim('https://delimit.ai/docs')} \u2502`);
|
|
791
|
+
log(` \u2502 Try: ${dim('https://delimit.ai/try')} \u2502`);
|
|
792
|
+
log(' \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518');
|
|
742
793
|
log('');
|
|
743
794
|
log(` ${dim('Config:')} ${MCP_CONFIG}`);
|
|
744
795
|
log(` ${dim('Server:')} ${actualServer}`);
|
|
745
796
|
log(` ${dim('Agents:')} ${AGENTS_DIR}`);
|
|
746
797
|
log('');
|
|
747
|
-
log(` ${
|
|
748
|
-
log(` ${dim('GitHub:')} https://github.com/delimit-ai/delimit-mcp-server`);
|
|
798
|
+
log(` ${bold('Keep Building.')}`);
|
|
749
799
|
log('');
|
|
750
800
|
}
|
|
751
801
|
|
package/lib/cross-model-hooks.js
CHANGED
|
@@ -167,12 +167,13 @@ function installClaudeHooks(tool, hookConfig) {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
// PreToolUse hook
|
|
170
|
+
// PreToolUse hook — scoped to Edit/Write on OpenAPI/Swagger spec files
|
|
171
171
|
if (hookConfig.pre_tool) {
|
|
172
172
|
const preToolHook = {
|
|
173
173
|
type: 'command',
|
|
174
|
-
command: `${npxCmd} hook pre-tool`,
|
|
175
|
-
matcher: 'Edit|Write
|
|
174
|
+
command: `${npxCmd} hook pre-tool $TOOL_NAME`,
|
|
175
|
+
matcher: 'Edit|Write',
|
|
176
|
+
if: "Edit && (path_matches('**/openapi*') || path_matches('**/swagger*') || path_matches('**/*.yaml') || path_matches('**/*.yml'))",
|
|
176
177
|
};
|
|
177
178
|
if (!config.hooks.PreToolUse) {
|
|
178
179
|
config.hooks.PreToolUse = [];
|
|
@@ -180,12 +181,40 @@ function installClaudeHooks(tool, hookConfig) {
|
|
|
180
181
|
const existing = config.hooks.PreToolUse.find(
|
|
181
182
|
h => h.command && h.command.includes('delimit-cli hook pre-tool')
|
|
182
183
|
);
|
|
183
|
-
if (
|
|
184
|
+
if (existing) {
|
|
185
|
+
// Upgrade existing hook to include conditional `if` field
|
|
186
|
+
if (!existing.if) {
|
|
187
|
+
existing.matcher = preToolHook.matcher;
|
|
188
|
+
existing.command = preToolHook.command;
|
|
189
|
+
existing.if = preToolHook.if;
|
|
190
|
+
changes.push('PreToolUse (upgraded)');
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
184
193
|
config.hooks.PreToolUse.push(preToolHook);
|
|
185
194
|
changes.push('PreToolUse');
|
|
186
195
|
}
|
|
187
196
|
}
|
|
188
197
|
|
|
198
|
+
// PreToolUse hook — pre-commit governance on git commit/push
|
|
199
|
+
if (hookConfig.pre_commit) {
|
|
200
|
+
const preCommitHook = {
|
|
201
|
+
type: 'command',
|
|
202
|
+
command: `${npxCmd} hook pre-commit`,
|
|
203
|
+
matcher: 'Bash',
|
|
204
|
+
if: "Bash && (input_contains('git commit') || input_contains('git push'))",
|
|
205
|
+
};
|
|
206
|
+
if (!config.hooks.PreToolUse) {
|
|
207
|
+
config.hooks.PreToolUse = [];
|
|
208
|
+
}
|
|
209
|
+
const existing = config.hooks.PreToolUse.find(
|
|
210
|
+
h => h.command && h.command.includes('delimit-cli hook pre-commit')
|
|
211
|
+
);
|
|
212
|
+
if (!existing) {
|
|
213
|
+
config.hooks.PreToolUse.push(preCommitHook);
|
|
214
|
+
changes.push('PreCommit');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
189
218
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
190
219
|
return changes;
|
|
191
220
|
}
|
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": "3.13.
|
|
4
|
+
"version": "3.13.2",
|
|
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": [
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"lib/",
|
|
10
10
|
"adapters/",
|
|
11
11
|
"gateway/",
|
|
12
|
+
"scripts/",
|
|
12
13
|
"server.json",
|
|
13
14
|
"README.md",
|
|
14
15
|
"LICENSE",
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
"delimit-cli": "./bin/delimit-cli.js"
|
|
20
21
|
},
|
|
21
22
|
"scripts": {
|
|
22
|
-
"postinstall": "
|
|
23
|
+
"postinstall": "node scripts/postinstall.js",
|
|
23
24
|
"test": "node --test tests/setup-onboarding.test.js tests/setup-matrix.test.js tests/config-export-import.test.js tests/cross-model-hooks.test.js"
|
|
24
25
|
},
|
|
25
26
|
"keywords": [
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Postinstall — anonymous install ping + setup hint.
|
|
4
|
+
* No PII. Silent fail. Never blocks install.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Print setup hint
|
|
8
|
+
console.log('\n Run: npx delimit-cli setup\n');
|
|
9
|
+
|
|
10
|
+
// Anonymous telemetry ping — no PII, just "someone installed"
|
|
11
|
+
try {
|
|
12
|
+
const https = require('https');
|
|
13
|
+
const data = JSON.stringify({
|
|
14
|
+
event: 'install',
|
|
15
|
+
version: require('../package.json').version,
|
|
16
|
+
node: process.version,
|
|
17
|
+
platform: process.platform,
|
|
18
|
+
arch: process.arch,
|
|
19
|
+
ts: new Date().toISOString()
|
|
20
|
+
});
|
|
21
|
+
const req = https.request({
|
|
22
|
+
hostname: 'delimit.ai',
|
|
23
|
+
path: '/api/telemetry',
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
'Content-Length': Buffer.byteLength(data)
|
|
28
|
+
},
|
|
29
|
+
timeout: 3000
|
|
30
|
+
});
|
|
31
|
+
req.on('error', () => {}); // silent fail
|
|
32
|
+
req.on('timeout', () => { req.destroy(); });
|
|
33
|
+
req.write(data);
|
|
34
|
+
req.end();
|
|
35
|
+
} catch (e) { /* silent fail */ }
|
|
@@ -0,0 +1,191 @@
|
|
|
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()
|