delimit-cli 4.1.25 → 4.1.27

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.
@@ -2560,6 +2560,183 @@ program
2560
2560
  }
2561
2561
  });
2562
2562
 
2563
+ // PR review command — review any GitHub PR for breaking API changes
2564
+ program
2565
+ .command('pr <url>')
2566
+ .description('Review a GitHub PR for breaking API changes')
2567
+ .action(async (url) => {
2568
+ console.log(chalk.bold('\n Delimit PR Review\n'));
2569
+
2570
+ // Parse GitHub PR URL: owner/repo#number or full URL
2571
+ let owner, repo, prNumber;
2572
+ const urlMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
2573
+ const shortMatch = url.match(/^([^/]+)\/([^#]+)#(\d+)$/);
2574
+ if (urlMatch) {
2575
+ [, owner, repo, prNumber] = urlMatch;
2576
+ } else if (shortMatch) {
2577
+ [, owner, repo, prNumber] = shortMatch;
2578
+ } else if (/^\d+$/.test(url)) {
2579
+ // Just a number — try current repo
2580
+ try {
2581
+ const remote = execSync('git remote get-url origin 2>/dev/null', { encoding: 'utf-8' }).trim();
2582
+ const remoteMatch = remote.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
2583
+ if (remoteMatch) {
2584
+ [, owner, repo] = remoteMatch;
2585
+ prNumber = url;
2586
+ }
2587
+ } catch {}
2588
+ }
2589
+
2590
+ if (!owner || !repo || !prNumber) {
2591
+ console.log(chalk.red(' Could not parse PR URL.'));
2592
+ console.log(chalk.gray(' Usage: npx delimit-cli pr owner/repo#123'));
2593
+ console.log(chalk.gray(' npx delimit-cli pr https://github.com/owner/repo/pull/123'));
2594
+ console.log(chalk.gray(' npx delimit-cli pr 123 (in a git repo)\n'));
2595
+ return;
2596
+ }
2597
+
2598
+ console.log(chalk.gray(` Reviewing ${owner}/${repo}#${prNumber}...\n`));
2599
+
2600
+ // Get PR changed files
2601
+ try {
2602
+ const filesJson = execSync(
2603
+ `gh api repos/${owner}/${repo}/pulls/${prNumber}/files --paginate 2>/dev/null`,
2604
+ { encoding: 'utf-8', timeout: 15000 }
2605
+ );
2606
+ const files = JSON.parse(filesJson);
2607
+ const specPatterns = ['openapi', 'swagger', 'api-spec', 'api_spec'];
2608
+ const specExts = ['.yaml', '.yml', '.json'];
2609
+ const specFiles = files.filter(f => {
2610
+ const name = f.filename.toLowerCase();
2611
+ return specExts.some(ext => name.endsWith(ext)) &&
2612
+ specPatterns.some(p => name.includes(p));
2613
+ });
2614
+
2615
+ if (specFiles.length === 0) {
2616
+ console.log(chalk.gray(' No OpenAPI/Swagger spec changes found in this PR.'));
2617
+ console.log(chalk.gray(' Delimit reviews PRs that modify API spec files.\n'));
2618
+ // Show what files were changed
2619
+ const apiFiles = files.filter(f => f.filename.includes('api') || f.filename.includes('spec'));
2620
+ if (apiFiles.length > 0) {
2621
+ console.log(chalk.gray(' API-related files changed:'));
2622
+ apiFiles.slice(0, 5).forEach(f => console.log(chalk.gray(` ${f.filename} (+${f.additions}/-${f.deletions})`)));
2623
+ console.log('');
2624
+ }
2625
+ return;
2626
+ }
2627
+
2628
+ console.log(` ${chalk.green('✓')} Found ${specFiles.length} spec file(s) changed:\n`);
2629
+ specFiles.forEach(f => {
2630
+ console.log(` ${chalk.cyan(f.filename)} (+${f.additions}/-${f.deletions})`);
2631
+ });
2632
+ console.log('');
2633
+
2634
+ // Fetch base and head versions of the first spec
2635
+ const specFile = specFiles[0];
2636
+ const tmpDir = path.join(os.tmpdir(), `delimit-pr-${Date.now()}`);
2637
+ fs.mkdirSync(tmpDir, { recursive: true });
2638
+
2639
+ // Get PR details for base/head refs
2640
+ const prJson = execSync(
2641
+ `gh api repos/${owner}/${repo}/pulls/${prNumber} 2>/dev/null`,
2642
+ { encoding: 'utf-8', timeout: 10000 }
2643
+ );
2644
+ const pr = JSON.parse(prJson);
2645
+ const baseSha = pr.base.sha;
2646
+ const headSha = pr.head.sha;
2647
+
2648
+ // Fetch both versions
2649
+ const basePath = path.join(tmpDir, 'base.yaml');
2650
+ const headPath = path.join(tmpDir, 'head.yaml');
2651
+
2652
+ try {
2653
+ const baseContent = execSync(
2654
+ `gh api repos/${owner}/${repo}/contents/${specFile.filename}?ref=${baseSha} -q '.content' 2>/dev/null`,
2655
+ { encoding: 'utf-8', timeout: 10000 }
2656
+ );
2657
+ fs.writeFileSync(basePath, Buffer.from(baseContent.trim(), 'base64').toString());
2658
+ } catch {
2659
+ console.log(chalk.yellow(' New spec file (no base version). Cannot diff.\n'));
2660
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2661
+ return;
2662
+ }
2663
+
2664
+ try {
2665
+ const headContent = execSync(
2666
+ `gh api repos/${owner}/${repo}/contents/${specFile.filename}?ref=${headSha} -q '.content' 2>/dev/null`,
2667
+ { encoding: 'utf-8', timeout: 10000 }
2668
+ );
2669
+ fs.writeFileSync(headPath, Buffer.from(headContent.trim(), 'base64').toString());
2670
+ } catch {
2671
+ console.log(chalk.red(' Could not fetch head version of spec.\n'));
2672
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2673
+ return;
2674
+ }
2675
+
2676
+ // Run lint
2677
+ console.log(chalk.gray(' Running governance pipeline...\n'));
2678
+ const bundledGateway = path.join(__dirname, '..', 'gateway');
2679
+ const serverDir = (continuityContext.serverDir && fs.existsSync(continuityContext.serverDir))
2680
+ ? continuityContext.serverDir
2681
+ : fs.existsSync(bundledGateway) ? bundledGateway : null;
2682
+
2683
+ if (!serverDir) {
2684
+ console.log(chalk.yellow(' Gateway not available. Run: npx delimit-cli setup\n'));
2685
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2686
+ return;
2687
+ }
2688
+
2689
+ try {
2690
+ const result = execSync(
2691
+ `python3 -c "
2692
+ import sys,json,yaml
2693
+ sys.path.insert(0,'${serverDir}')
2694
+ from core.diff_engine_v2 import OpenAPIDiffEngine
2695
+ old=yaml.safe_load(open('${basePath}'))
2696
+ new=yaml.safe_load(open('${headPath}'))
2697
+ engine=OpenAPIDiffEngine()
2698
+ changes=engine.compare(old,new)
2699
+ print(json.dumps({'changes':[{'type':c.type.value if hasattr(c.type,'value') else str(c.type),'path':c.path,'breaking':c.severity in ('high','critical','error'),'detail':c.message or c.details or ''} for c in changes]}))
2700
+ "`,
2701
+ { encoding: 'utf-8', timeout: 15000, cwd: serverDir }
2702
+ );
2703
+ const diff = JSON.parse(result);
2704
+ const breaking = (diff.changes || []).filter(c => c.breaking);
2705
+ const nonBreaking = (diff.changes || []).filter(c => !c.breaking);
2706
+
2707
+ if (breaking.length > 0) {
2708
+ console.log(chalk.red.bold(` ${breaking.length} BREAKING change(s) detected\n`));
2709
+ breaking.forEach(c => {
2710
+ console.log(` ${chalk.red('BREAK')} ${c.type}: ${c.path || ''}`);
2711
+ if (c.detail) console.log(chalk.gray(` ${c.detail}`));
2712
+ });
2713
+ } else {
2714
+ console.log(chalk.green.bold(' No breaking changes detected'));
2715
+ }
2716
+
2717
+ if (nonBreaking.length > 0) {
2718
+ console.log(chalk.gray(`\n ${nonBreaking.length} non-breaking change(s)`));
2719
+ }
2720
+
2721
+ // Semver classification
2722
+ const bump = breaking.length > 0 ? 'MAJOR' : nonBreaking.length > 0 ? 'MINOR' : 'NONE';
2723
+ console.log(`\n Semver: ${chalk.bold(bump)}`);
2724
+ console.log(` Total: ${(diff.changes || []).length} changes (${breaking.length} breaking, ${nonBreaking.length} compatible)\n`);
2725
+
2726
+ console.log(chalk.bold(' Add to your repo:'));
2727
+ console.log(chalk.green(` npx delimit-cli init`));
2728
+ console.log(chalk.gray(' Catches this on every PR automatically.\n'));
2729
+ } catch (e) {
2730
+ console.log(chalk.red(` Diff failed: ${e.message.split('\n')[0]}\n`));
2731
+ }
2732
+
2733
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2734
+ } catch (e) {
2735
+ console.log(chalk.red(` Error: ${e.message.split('\n')[0]}`));
2736
+ console.log(chalk.gray(' Make sure gh CLI is authenticated: gh auth login\n'));
2737
+ }
2738
+ });
2739
+
2563
2740
  // Try command — zero-risk demo with Markdown report artifact (LED-264)
2564
2741
  program
2565
2742
  .command('try')
@@ -3133,23 +3310,56 @@ program
3133
3310
  process.exit(1);
3134
3311
  }
3135
3312
 
3313
+ console.log(chalk.gray(' Validating license key...'));
3314
+
3315
+ // Validate against Lemon Squeezy API
3316
+ let validated = false;
3317
+ let licenseId = null;
3318
+ let customerEmail = '';
3319
+ try {
3320
+ const resp = await axios.post('https://api.lemonsqueezy.com/v1/licenses/validate', {
3321
+ license_key: key,
3322
+ }, {
3323
+ headers: { 'Accept': 'application/json' },
3324
+ timeout: 10000,
3325
+ });
3326
+ if (resp.data && resp.data.valid) {
3327
+ validated = true;
3328
+ licenseId = resp.data.license_key?.id;
3329
+ customerEmail = resp.data.meta?.customer_email || '';
3330
+ console.log(chalk.green(' License valid.'));
3331
+ } else {
3332
+ console.log(chalk.red(` License invalid: ${resp.data?.error || 'unknown error'}`));
3333
+ process.exit(1);
3334
+ }
3335
+ } catch (err) {
3336
+ // If API unreachable, accept locally (grace period)
3337
+ console.log(chalk.yellow(' Could not reach license server. Activating locally (7-day grace).'));
3338
+ validated = true;
3339
+ }
3340
+
3136
3341
  // Write license file
3137
3342
  const crypto = require('crypto');
3138
3343
  const machineHash = crypto.createHash('sha256').update(os.homedir()).digest('hex').slice(0, 16);
3139
3344
  const licenseData = {
3140
3345
  key: key,
3141
3346
  tier: 'pro',
3142
- valid: true,
3347
+ valid: validated,
3348
+ license_id: licenseId,
3349
+ customer_email: customerEmail,
3143
3350
  activated_at: Date.now() / 1000,
3144
3351
  machine_hash: machineHash,
3352
+ validated_at: Date.now() / 1000,
3145
3353
  };
3146
3354
 
3147
3355
  if (!fs.existsSync(licenseDir)) {
3148
3356
  fs.mkdirSync(licenseDir, { recursive: true });
3149
3357
  }
3150
3358
  fs.writeFileSync(licensePath, JSON.stringify(licenseData, null, 2));
3151
- console.log(chalk.green('License activated successfully.'));
3152
- console.log(chalk.dim('Tier: pro'));
3359
+ console.log(chalk.green('\n License activated successfully.'));
3360
+ console.log(chalk.dim(` Tier: pro`));
3361
+ if (customerEmail) console.log(chalk.dim(` Email: ${customerEmail}`));
3362
+ console.log('');
3153
3363
  });
3154
3364
 
3155
3365
  // ---------------------------------------------------------------------------
@@ -5756,16 +5756,108 @@ def delimit_quickstart(project_path: str = ".") -> Dict[str, Any]:
5756
5756
  "models": enabled_models,
5757
5757
  })
5758
5758
 
5759
+ # Step 6: First Governance Run -- show value with bundled example specs
5760
+ demo_result: Dict[str, Any] = {"skipped": False}
5761
+ examples_dir = Path(__file__).resolve().parent.parent / "examples"
5762
+ petstore_v1 = examples_dir / "petstore-v1.yaml"
5763
+ petstore_v2 = examples_dir / "petstore-v2.yaml"
5764
+ if petstore_v1.is_file() and petstore_v2.is_file():
5765
+ from backends.gateway_core import run_lint as _qs_run_lint, run_spec_health as _qs_run_spec_health
5766
+
5767
+ # 6a: Lint petstore v1 vs v2 to show breaking change detection
5768
+ try:
5769
+ lint_demo = _qs_run_lint(
5770
+ old_spec=str(petstore_v1),
5771
+ new_spec=str(petstore_v2),
5772
+ )
5773
+ breaking_count = len(lint_demo.get("breaking", lint_demo.get("violations", [])))
5774
+ total_changes = lint_demo.get("total_changes", 0)
5775
+ demo_result["lint"] = {
5776
+ "breaking_changes": breaking_count,
5777
+ "total_changes": total_changes,
5778
+ "status": lint_demo.get("status", "unknown"),
5779
+ "sample_violations": [
5780
+ v.get("message", v.get("type", "unknown"))
5781
+ for v in lint_demo.get("breaking", lint_demo.get("violations", []))[:3]
5782
+ ],
5783
+ }
5784
+ except Exception as e:
5785
+ demo_result["lint"] = {"error": str(e)}
5786
+
5787
+ # 6b: Spec health score on petstore v1
5788
+ try:
5789
+ health_demo = _qs_run_spec_health(spec_path=str(petstore_v1))
5790
+ demo_result["spec_health"] = {
5791
+ "score": health_demo.get("score", health_demo.get("overall_score")),
5792
+ "grade": health_demo.get("grade", health_demo.get("letter_grade")),
5793
+ "dimensions": {
5794
+ k: v for k, v in health_demo.get("dimensions", {}).items()
5795
+ } if health_demo.get("dimensions") else {},
5796
+ "recommendations_count": len(health_demo.get("recommendations", [])),
5797
+ }
5798
+ except Exception as e:
5799
+ demo_result["spec_health"] = {"error": str(e)}
5800
+ else:
5801
+ demo_result["skipped"] = True
5802
+ demo_result["reason"] = "Example specs not found"
5803
+
5804
+ steps_completed.append({
5805
+ "step": 6,
5806
+ "name": "First Governance Run (Demo)",
5807
+ "result": demo_result,
5808
+ })
5809
+
5810
+ # Step 7: Project Spec Discovery -- check if this project has OpenAPI specs
5811
+ project_specs: List[str] = []
5812
+ project_lint_result: Optional[Dict[str, Any]] = None
5813
+ spec_patterns = [
5814
+ "**/openapi.yaml", "**/openapi.yml", "**/openapi.json",
5815
+ "**/swagger.yaml", "**/swagger.yml", "**/swagger.json",
5816
+ ]
5817
+ for pattern in spec_patterns:
5818
+ for match in p.glob(pattern):
5819
+ rel = str(match.relative_to(p))
5820
+ if "node_modules" not in rel and ".next" not in rel and "venv" not in rel:
5821
+ project_specs.append(str(match))
5822
+ project_specs = list(set(project_specs))[:5]
5823
+
5824
+ if project_specs:
5825
+ # Run spec_health on the first discovered spec
5826
+ try:
5827
+ from backends.gateway_core import run_spec_health as _qs_health
5828
+ proj_health = _qs_health(spec_path=project_specs[0])
5829
+ project_lint_result = {
5830
+ "spec": project_specs[0],
5831
+ "score": proj_health.get("score", proj_health.get("overall_score")),
5832
+ "grade": proj_health.get("grade", proj_health.get("letter_grade")),
5833
+ }
5834
+ except Exception as e:
5835
+ project_lint_result = {"spec": project_specs[0], "error": str(e)}
5836
+
5837
+ steps_completed.append({
5838
+ "step": 7,
5839
+ "name": "Project Spec Discovery",
5840
+ "specs_found": len(project_specs),
5841
+ "spec_files": project_specs,
5842
+ "health_result": project_lint_result,
5843
+ })
5844
+
5759
5845
  # Build suggested next actions based on findings
5760
5846
  next_actions = []
5761
- if scan_result.get("findings"):
5762
- for f in scan_result["findings"]:
5763
- if f.get("type") == "openapi_specs":
5764
- next_actions.append("Run `delimit_lint` on your OpenAPI spec to check for breaking changes")
5765
- if f.get("type") == "security_concerns":
5766
- next_actions.append("Run `delimit_security_scan` to audit for vulnerabilities")
5767
- if f.get("type") == "tests_found":
5768
- next_actions.append("Run `delimit_test_smoke` to verify tests pass")
5847
+ if project_specs:
5848
+ next_actions.append(f"Run `delimit_spec_health` on {project_specs[0]} to see your full quality report")
5849
+ if len(project_specs) > 1:
5850
+ next_actions.append(f"You have {len(project_specs)} OpenAPI specs -- run `delimit_lint` to compare versions")
5851
+ next_actions.append("Add the Delimit GitHub Action to catch breaking changes on every PR")
5852
+ else:
5853
+ if scan_result.get("findings"):
5854
+ for f in scan_result["findings"]:
5855
+ if f.get("type") == "openapi_specs":
5856
+ next_actions.append("Run `delimit_lint` on your OpenAPI spec to check for breaking changes")
5857
+ if f.get("type") == "security_concerns":
5858
+ next_actions.append("Run `delimit_security_scan` to audit for vulnerabilities")
5859
+ if f.get("type") == "tests_found":
5860
+ next_actions.append("Run `delimit_test_smoke` to verify tests pass")
5769
5861
 
5770
5862
  if not deliberation_ready:
5771
5863
  next_actions.append("Add more AI models for multi-model deliberation: say 'configure delimit models'")
@@ -5773,16 +5865,54 @@ def delimit_quickstart(project_path: str = ".") -> Dict[str, Any]:
5773
5865
  next_actions.append("Say 'add to ledger: [task]' to start tracking work across sessions")
5774
5866
  next_actions.append("Say 'deliberate [question]' to get AI consensus on a decision")
5775
5867
 
5868
+ # Build the "wow moment" summary
5869
+ wow_moment: Dict[str, Any] = {}
5870
+ lint_data = demo_result.get("lint", {})
5871
+ health_data = demo_result.get("spec_health", {})
5872
+ if lint_data and not lint_data.get("error"):
5873
+ wow_moment["breaking_changes_caught"] = lint_data.get("breaking_changes", 0)
5874
+ wow_moment["total_api_changes"] = lint_data.get("total_changes", 0)
5875
+ wow_moment["sample_catches"] = lint_data.get("sample_violations", [])
5876
+ if health_data and not health_data.get("error"):
5877
+ wow_moment["spec_health_grade"] = health_data.get("grade")
5878
+ wow_moment["spec_health_score"] = health_data.get("score")
5879
+ wow_moment["governance_gates"] = [
5880
+ "Breaking change detection (CI/CD)",
5881
+ "Spec health scoring (quality)",
5882
+ "Policy enforcement (custom rules)",
5883
+ "Semver classification (automated)",
5884
+ "Contract ledger (audit trail)",
5885
+ ]
5886
+ if project_specs:
5887
+ wow_moment["your_project"] = {
5888
+ "specs_found": len(project_specs),
5889
+ "ready_to_govern": True,
5890
+ }
5891
+ if project_lint_result and not project_lint_result.get("error"):
5892
+ wow_moment["your_project"]["health_grade"] = project_lint_result.get("grade")
5893
+ wow_moment["your_project"]["health_score"] = project_lint_result.get("score")
5894
+
5895
+ bc = wow_moment.get("breaking_changes_caught", 0)
5896
+ grade = wow_moment.get("spec_health_grade", "N/A")
5897
+ msg_parts = [
5898
+ f"Quickstart complete! {len(steps_completed)} steps run.",
5899
+ f"Demo: {bc} breaking changes caught, spec health grade: {grade}.",
5900
+ f"5 governance gates ready.",
5901
+ ]
5902
+ if project_specs:
5903
+ msg_parts.append(f"Found {len(project_specs)} OpenAPI spec(s) in your project -- ready to govern.")
5904
+
5776
5905
  return _with_next_steps("quickstart", {
5777
5906
  "tool": "quickstart",
5778
5907
  "status": "complete",
5779
5908
  "project": str(p),
5780
5909
  "steps": steps_completed,
5910
+ "wow_moment": wow_moment,
5781
5911
  "environment": environment,
5782
5912
  "scan_findings": scan_result.get("findings", []),
5783
5913
  "scan_suggestions": scan_result.get("suggestions", []),
5784
5914
  "next_actions": next_actions,
5785
- "message": f"Quickstart complete! {len(steps_completed)} steps run. {len(next_actions)} suggested next actions.",
5915
+ "message": " ".join(msg_parts),
5786
5916
  })
5787
5917
 
5788
5918
 
@@ -6817,7 +6947,7 @@ def delimit_build_loop(action: str = "run", session_id: str = "") -> Dict[str, A
6817
6947
  """Execute the governed continuous build loop (LED-239).
6818
6948
 
6819
6949
  Requirements:
6820
- - root ledger in ~/.delimit is authoritative
6950
+ - root ledger in /root/.delimit is authoritative
6821
6951
  - select only build-safe open items
6822
6952
  - resolve venture + repo before dispatch
6823
6953
  - use Delimit swarm/governance as control plane
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.1.25",
4
+ "version": "4.1.27",
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": [