delimit-cli 3.13.1 → 3.13.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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.13.3] - 2026-03-27
4
+
5
+ ### Changed
6
+ - Postinstall message now shows full quick-start guide (init, lint, setup) with links to dashboard and docs
7
+
3
8
  ## [3.13.1] - 2026-03-27
4
9
 
5
10
  ### Added
package/README.md CHANGED
@@ -6,6 +6,7 @@ Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, govern
6
6
  [![GitHub Action](https://img.shields.io/badge/GitHub%20Action-v1.6.0-blue)](https://github.com/marketplace/actions/delimit-api-governance)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
8
  [![Glama](https://glama.ai/mcp/servers/delimit-ai/delimit-mcp-server/badges/score.svg)](https://glama.ai/mcp/servers/delimit-ai/delimit-mcp-server)
9
+ [![API Governance](https://delimit-ai.github.io/badge/pass.svg)](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">
@@ -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
- for c in /usr/bin/${toolName} /usr/local/bin/${toolName} $HOME/.local/bin/${toolName}; do
539
- [ -x "$c" ] && exec "$c" "$@"
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
- echo "[Delimit] ${toolName} not found" >&2; exit 127
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 9: Done
721
- step(10, 'Done!');
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
- log(' Try it now:');
734
- log(` ${bold('$ claude')}`);
735
- log('');
736
- log(` Then say: ${blue('"scan this project"')}`);
737
- log('');
738
- log(' Or try:');
739
- log(` ${dim('-')} "lint my API spec" ${dim(' catch breaking changes')}`);
740
- log(` ${dim('-')} "add to ledger: set up CI pipeline" ${dim('— track tasks across sessions')}`);
741
- log(` ${dim('-')} "deliberate [question]" ${dim('— multi-model AI consensus')}`);
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(` ${dim('Docs:')} https://delimit.ai/docs`);
748
- log(` ${dim('GitHub:')} https://github.com/delimit-ai/delimit-mcp-server`);
798
+ log(` ${bold('Keep Building.')}`);
749
799
  log('');
750
800
  }
751
801
 
@@ -167,12 +167,13 @@ function installClaudeHooks(tool, hookConfig) {
167
167
  }
168
168
  }
169
169
 
170
- // PreToolUse hook for file edits
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|Bash',
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 (!existing) {
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.1",
4
+ "version": "3.13.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": [
@@ -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": "echo '\\nRun: npx delimit-cli setup\\n'",
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,46 @@
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 with quick start
8
+ const v = require('../package.json').version;
9
+ console.log('');
10
+ console.log(' \x1b[1m\x1b[35mDelimit\x1b[0m v' + v + ' installed');
11
+ console.log('');
12
+ console.log(' Quick start:');
13
+ console.log(' \x1b[32mdelimit init\x1b[0m Auto-detect framework, set policy, first lint');
14
+ console.log(' \x1b[32mdelimit lint\x1b[0m Check for breaking API changes');
15
+ console.log(' \x1b[32mdelimit setup\x1b[0m Install MCP governance for AI assistants');
16
+ console.log('');
17
+ console.log(' Dashboard: \x1b[36mhttps://app.delimit.ai\x1b[0m');
18
+ console.log(' Docs: \x1b[36mhttps://delimit.ai/docs\x1b[0m');
19
+ console.log('');
20
+
21
+ // Anonymous telemetry ping — no PII, just "someone installed"
22
+ try {
23
+ const https = require('https');
24
+ const data = JSON.stringify({
25
+ event: 'install',
26
+ version: require('../package.json').version,
27
+ node: process.version,
28
+ platform: process.platform,
29
+ arch: process.arch,
30
+ ts: new Date().toISOString()
31
+ });
32
+ const req = https.request({
33
+ hostname: 'delimit.ai',
34
+ path: '/api/telemetry',
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'application/json',
38
+ 'Content-Length': Buffer.byteLength(data)
39
+ },
40
+ timeout: 3000
41
+ });
42
+ req.on('error', () => {}); // silent fail
43
+ req.on('timeout', () => { req.destroy(); });
44
+ req.write(data);
45
+ req.end();
46
+ } 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()