rlhf-feedback-loop 0.6.6 → 0.6.7

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 CHANGED
@@ -1,30 +1,40 @@
1
- # RLHF Feedback Loop
1
+ # RLHF Feedback Loop — Agentic Control Plane & Context Engineering Studio
2
2
 
3
3
  [![CI](https://github.com/IgorGanapolsky/rlhf-feedback-loop/actions/workflows/ci.yml/badge.svg)](https://github.com/IgorGanapolsky/rlhf-feedback-loop/actions/workflows/ci.yml)
4
- [![npm](https://img.shields.io/npm/v/rlhf-feedback-loop)](https://www.npmjs.com/package/rlhf-feedback-loop)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
- [![MCP Ready](https://img.shields.io/badge/MCP-ready-black)](adapters/mcp/server-stdio.js)
7
- [![DPO Ready](https://img.shields.io/badge/DPO-ready-blue)](scripts/export-dpo-pairs.js)
4
+ [![Marketplace Ready](https://img.shields.io/badge/Anthropic_Marketplace-Ready-blue)](docs/ANTHROPIC_MARKETPLACE_STRATEGY.md)
5
+ [![GEO Optimized](https://img.shields.io/badge/GEO-optimized-orange)](docs/geo-strategy-for-ai-agents.md)
8
6
 
9
- **The complete RLHF data pipeline for AI coding agents.** Capture human feedback, build memory, generate prevention rules, and export DPO training pairs the full loop from thumbs up/down to model fine-tuning.
7
+ **Stop Vibe Coding. Start Context Engineering.** The RLHF Feedback Loop is the enterprise-grade **Agentic Control Plane** for AI workflows. We provide the operational layer to capture human preference signals, engineer high-density context packs, and enforce machine-readable guardrails to stop your agents from going "off-script."
10
8
 
11
- ## What This Is (and Isn't)
9
+ ## True Plug-and-Play: Zero-Config Integration
12
10
 
13
- This tool implements the **data collection and preference pipeline** side of RLHF the part that turns your daily interactions with AI agents into structured training data. Out of the box, it:
11
+ The RLHF Feedback Loop is now a **Universal Agent Skill**. You can drop it into any repository without manual setup.
14
12
 
15
- - **Captures** thumbs up/down feedback with context, tags, and rubric scores
16
- - **Remembers** via JSONL logs + LanceDB vector search across sessions
17
- - **Prevents** repeated mistakes with auto-generated guardrails
18
- - **Recalls** relevant past feedback mid-conversation (in-session context injection)
19
- - **Exports** DPO training pairs (prompt/chosen/rejected) for model fine-tuning
13
+ - **Zero-Config Discovery:** Automatically detects project context. If no local `.rlhf/` directory exists, it safely fallbacks to a project-scoped global store in `~/.rlhf/`.
14
+ - **Global Skill Installation:** Run one command to make RLHF available to all your agents across all projects.
15
+ - **Vibe-to-Verification (V2V):** Directly converts subjective "vibes" (thumbs up/down) into verifiable repository rules (`CLAUDE.md`).
20
16
 
21
- It does **not** update model weights in real-time. That's the fine-tuning step, which you do separately using the DPO pairs this tool exports. The full loop: capture feedback here → export DPO pairs → fine-tune with [TRL](https://github.com/huggingface/trl), [OpenPipe](https://openpipe.ai), or any DPO trainer → deploy improved model.
17
+ ### Quick Start (One Command)
22
18
 
23
- ## Architecture
19
+ ```bash
20
+ npx rlhf-feedback-loop install
21
+ ```
22
+
23
+ This will auto-detect your platforms (Claude, Codex, Gemini, Cursor) and install the RLHF skill globally.
24
+
25
+ - **Stop Regressions:** Automatically convert negative feedback into `CLAUDE.md` / `AGENTS.md` prevention rules.
26
+ - **Preference Data Engineering:** Capture high-density context (rubrics, guardrails, metadata) for DPO training.
27
+ - **Bayesian Scoring:** Use Thompson Sampling to handle evolving user preferences over time.
28
+
29
+ ## Use Cases
30
+
31
+ - **Automated Code Reviews:** Capture PR feedback to enforce team-specific style guides autonomously.
32
+ - **Self-Healing Multi-Agent Systems:** Share prevention rules across a swarm of agents to avoid systemic bottlenecks.
33
+ - **Custom LLM Fine-tuning:** Collect proprietary preference data to train smaller, faster models that perform like GPT-4 on your specific codebase.
24
34
 
25
- ![RLHF Architecture](docs/diagrams/rlhf-architecture-pb.png)
35
+ ## Generative Engine Optimization (GEO)
26
36
 
27
- ![Plugin Topology](docs/diagrams/plugin-topology-pb.png)
37
+ We are optimized for discovery by next-gen AI tools (Claude Code, Gemini CLI, Perplexity). See our [GEO Strategy](docs/geo-strategy-for-ai-agents.md) for how we use **Context-First Publishing** to remain the first-choice recommendation for agent reliability.
28
38
 
29
39
  ## Get Started
30
40
 
package/bin/cli.js CHANGED
@@ -6,11 +6,8 @@
6
6
  * npx rlhf-feedback-loop init # scaffold .rlhf/ config + .mcp.json
7
7
  * npx rlhf-feedback-loop capture # capture feedback
8
8
  * npx rlhf-feedback-loop export-dpo # export DPO training pairs
9
- * npx rlhf-feedback-loop stats # feedback analytics
10
- * npx rlhf-feedback-loop rules # generate prevention rules
11
- * npx rlhf-feedback-loop self-heal # run self-healing check + fix
12
- * npx rlhf-feedback-loop prove # run proof harness
13
- * npx rlhf-feedback-loop start-api # start HTTPS API server
9
+ * npx rlhf-feedback-loop stats # feedback analytics + Revenue-at-Risk
10
+ * npx rlhf-feedback-loop pro # upgrade to Cloud Pro
14
11
  */
15
12
 
16
13
  'use strict';
@@ -201,7 +198,7 @@ function capture() {
201
198
  const { captureFeedback, analyzeFeedback, feedbackSummary, writePreventionRules } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
202
199
 
203
200
  if (args.stats) {
204
- console.log(JSON.stringify(analyzeFeedback(), null, 2));
201
+ stats();
205
202
  return;
206
203
  }
207
204
 
@@ -248,7 +245,42 @@ function capture() {
248
245
 
249
246
  function stats() {
250
247
  const { analyzeFeedback } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
251
- console.log(JSON.stringify(analyzeFeedback(), null, 2));
248
+ const data = analyzeFeedback();
249
+
250
+ console.log('\n📊 RLHF Performance Metrics');
251
+ console.log('─'.repeat(50));
252
+ console.log(` Total Signals : ${data.total}`);
253
+ console.log(` Approval Rate : ${Math.round(data.approvalRate * 100)}%`);
254
+ console.log(` Recent Trend : ${Math.round(data.recentRate * 100)}%`);
255
+
256
+ // The Pitch: Revenue-at-Risk
257
+ const avgCostOfMistake = 2.50; // $2.50 per agent turn/fix
258
+ const revenueAtRisk = (data.totalNegative * avgCostOfMistake).toFixed(2);
259
+
260
+ if (data.totalNegative > 0) {
261
+ console.log('\n⚠️ REVENUE-AT-RISK ANALYSIS');
262
+ console.log(` Repeated Failures detected: ${data.totalNegative}`);
263
+ console.log(` Estimated Operational Loss: $${revenueAtRisk}`);
264
+ console.log(' Action Required: Run "npx rlhf-feedback-loop rules" to generate guardrails.');
265
+ console.log(' Strategic Recommendation: Upgrade to Cloud Pro to sync these rules across your team.');
266
+ console.log(' Run: npx rlhf-feedback-loop pro');
267
+ } else {
268
+ console.log('\n✅ System is currently high-reliability. No immediate revenue loss detected.');
269
+ }
270
+ }
271
+
272
+ function pro() {
273
+ const stripeUrl = 'https://buy.stripe.com/bJe14neyU4r4f0leOD3sI02';
274
+ console.log('\n🚀 RLHF Feedback Loop — Cloud Pro');
275
+ console.log('─'.repeat(50));
276
+ console.log('Unlock the full Agentic Control Plane:');
277
+ console.log(' - Hosted Team API (Shared memory across all repos)');
278
+ console.log(' - ShieldCortex Managed Context Packs');
279
+ console.log(' - Automated DPO Training Pipelines');
280
+ console.log(' - SOC2-ready Governance Dashboard');
281
+ console.log('\n👉 Complete your upgrade here:');
282
+ console.log(` ${stripeUrl}`);
283
+ console.log('\nOnce upgraded, run: npx rlhf-feedback-loop init --key=YOUR_PRO_KEY\n');
252
284
  }
253
285
 
254
286
  function summary() {
@@ -307,11 +339,38 @@ function prove() {
307
339
  }
308
340
 
309
341
  function serve() {
310
- // Start MCP server over stdio — used by `claude mcp add`, `codex mcp add`, `gemini mcp add`
342
+ const rlhfDir = path.join(CWD, '.rlhf');
343
+ if (!fs.existsSync(rlhfDir) && !fs.existsSync(path.join(CWD, '.claude', 'memory', 'feedback'))) {
344
+ // If not initialized, ensure global fallback exists
345
+ const projectName = path.basename(CWD) || 'default';
346
+ const globalDir = path.join(HOME, '.rlhf', 'projects', projectName);
347
+ if (!fs.existsSync(globalDir)) {
348
+ fs.mkdirSync(globalDir, { recursive: true });
349
+ }
350
+ }
351
+
352
+ // Start MCP server over stdio
311
353
  const mcpServer = path.join(PKG_ROOT, 'adapters', 'mcp', 'server-stdio.js');
312
354
  require(mcpServer);
313
355
  }
314
356
 
357
+ function install() {
358
+ console.log('Installing RLHF Feedback Loop as a global MCP skill...');
359
+ const results = [
360
+ setupClaude(),
361
+ setupCodex(),
362
+ setupGemini(),
363
+ setupCursor()
364
+ ];
365
+ const success = results.some(r => r === true);
366
+ if (success) {
367
+ console.log('\nSuccess! RLHF Feedback Loop is now available to your agents.');
368
+ console.log('Try asking your agent: "Capture positive feedback for this task"');
369
+ } else {
370
+ console.log('\nRLHF Feedback Loop is already configured.');
371
+ }
372
+ }
373
+
315
374
  function startApi() {
316
375
  const serverPath = path.join(PKG_ROOT, 'src', 'api', 'server.js');
317
376
  try {
@@ -329,32 +388,29 @@ function help() {
329
388
  console.log(' init Scaffold .rlhf/ config + MCP server in current project');
330
389
  console.log(' serve Start MCP server (stdio) — for claude/codex/gemini mcp add');
331
390
  console.log(' capture [flags] Capture feedback (--feedback=up|down --context="..." --tags="...")');
332
- console.log(' stats Show feedback analytics');
391
+ console.log(' stats Show feedback analytics + Revenue-at-Risk');
333
392
  console.log(' summary Human-readable feedback summary');
334
393
  console.log(' export-dpo Export DPO training pairs (prompt/chosen/rejected JSONL)');
335
394
  console.log(' rules Generate prevention rules from repeated failures');
336
395
  console.log(' self-heal Run self-healing check and auto-fix');
396
+ console.log(' pro Upgrade to Cloud Pro ($10/mo)');
337
397
  console.log(' prove [--target=X] Run proof harness (adapters|automation|attribution|lancedb|...)');
338
398
  console.log(' start-api Start the RLHF HTTPS API server');
339
399
  console.log(' help Show this help message');
340
400
  console.log('');
341
401
  console.log('Examples:');
342
402
  console.log(' npx rlhf-feedback-loop init');
343
- console.log(' npx rlhf-feedback-loop capture --feedback=up --context="all tests pass"');
344
- console.log(' npx rlhf-feedback-loop capture --feedback=down --context="broke prod" --what-went-wrong="no tests"');
345
- console.log(' npx rlhf-feedback-loop export-dpo');
346
403
  console.log(' npx rlhf-feedback-loop stats');
347
- console.log('');
348
- console.log('MCP install (one command per platform):');
349
- console.log(' claude mcp add rlhf -- npx -y rlhf-feedback-loop serve');
350
- console.log(' codex mcp add rlhf -- npx -y rlhf-feedback-loop serve');
351
- console.log(' gemini mcp add rlhf -- npx -y rlhf-feedback-loop serve');
404
+ console.log(' npx rlhf-feedback-loop pro');
352
405
  }
353
406
 
354
407
  switch (COMMAND) {
355
408
  case 'init':
356
409
  init();
357
410
  break;
411
+ case 'install':
412
+ install();
413
+ break;
358
414
  case 'serve':
359
415
  case 'mcp':
360
416
  serve();
@@ -379,6 +435,9 @@ switch (COMMAND) {
379
435
  case 'self-heal':
380
436
  selfHeal();
381
437
  break;
438
+ case 'pro':
439
+ pro();
440
+ break;
382
441
  case 'prove':
383
442
  prove();
384
443
  break;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rlhf-feedback-loop",
3
- "version": "0.6.6",
4
- "description": "Make your AI agent learn from mistakes. Capture thumbs up/down feedback, block repeated failures, export DPO training data. Works with ChatGPT, Claude, Codex, Gemini, Amp.",
3
+ "version": "0.6.7",
4
+ "description": "Production RLHF & DPO data pipeline for AI agents. Optimize agentic reliability with Feedback-Driven Development (FDD). Capture human preference signals, generate automated guardrails, and export DPO training pairs. Compatible with Claude, GPT-4, Gemini, and multi-agent systems.",
5
5
  "homepage": "https://github.com/IgorGanapolsky/rlhf-feedback-loop#readme",
6
6
  "repository": {
7
7
  "type": "git",
@@ -91,6 +91,13 @@
91
91
  "amp",
92
92
  "mcp",
93
93
  "model-context-protocol",
94
+ "agentic-reliability",
95
+ "feedback-driven-development",
96
+ "agentic-workflows",
97
+ "ai-search-optimization",
98
+ "generative-engine-optimization",
99
+ "fdd",
100
+ "geo",
94
101
  "agent-evaluation",
95
102
  "prompt-engineering",
96
103
  "context-engineering",
@@ -101,10 +108,12 @@
101
108
  ],
102
109
  "license": "MIT",
103
110
  "dependencies": {
104
- "@huggingface/transformers": "^3.8.1",
105
- "@lancedb/lancedb": "^0.26.2",
106
111
  "apache-arrow": "^18.1.0",
107
112
  "stripe": "^20.4.0"
108
113
  },
109
- "mcpName": "io.github.IgorGanapolsky/rlhf-feedback-loop"
114
+ "mcpName": "io.github.IgorGanapolsky/rlhf-feedback-loop",
115
+ "optionalDependencies": {
116
+ "@lancedb/lancedb": "^0.26.2",
117
+ "@huggingface/transformers": "^3.8.1"
118
+ }
110
119
  }
@@ -25,8 +25,9 @@ const crypto = require('crypto');
25
25
 
26
26
  const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY || '';
27
27
  const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET || '';
28
+ const GITHUB_MARKETPLACE_WEBHOOK_SECRET = process.env.GITHUB_MARKETPLACE_WEBHOOK_SECRET || '';
28
29
  const STRIPE_PRICE_ID = process.env.STRIPE_PRICE_ID || 'price_cloud_pro_49_monthly';
29
- const API_KEYS_PATH = path.resolve(
30
+ const API_KEYS_PATH = process.env._TEST_API_KEYS_PATH || path.resolve(
30
31
  __dirname,
31
32
  '../.claude/memory/feedback/api-keys.json'
32
33
  );
@@ -443,20 +444,106 @@ function verifyWebhookSignature(rawBody, signature) {
443
444
 
444
445
  // Constant-time comparison
445
446
  try {
446
- return crypto.timingSafeEqual(
447
- Buffer.from(expected, 'hex'),
448
- Buffer.from(parts.v1, 'hex')
449
- );
447
+ return crypto.timingSafeEqual(
448
+ Buffer.from(expected, 'hex'),
449
+ Buffer.from(parts.v1, 'hex')
450
+ );
450
451
  } catch {
451
- return false;
452
+ return false;
453
+ }
452
454
  }
453
- }
454
455
 
455
- // ---------------------------------------------------------------------------
456
- // Module exports
457
- // ---------------------------------------------------------------------------
456
+ /**
457
+ * Verify a GitHub Marketplace webhook signature.
458
+ * Returns true if valid, false if GITHUB_MARKETPLACE_WEBHOOK_SECRET is not set (local mode).
459
+ *
460
+ * @param {string|Buffer} rawBody - Raw request body bytes
461
+ * @param {string} signature - Value of x-hub-signature-256 header
462
+ * @returns {boolean}
463
+ */
464
+ function verifyGithubWebhookSignature(rawBody, signature) {
465
+ if (!GITHUB_MARKETPLACE_WEBHOOK_SECRET) {
466
+ // Local mode — skip signature verification
467
+ return true;
468
+ }
469
+
470
+ if (!signature || !rawBody) {
471
+ return false;
472
+ }
473
+
474
+ const hmac = crypto.createHmac('sha256', GITHUB_MARKETPLACE_WEBHOOK_SECRET);
475
+ const digest = Buffer.from('sha256=' + hmac.update(rawBody).digest('hex'), 'utf8');
476
+ const checksum = Buffer.from(signature, 'utf8');
477
+
478
+ // Constant-time comparison
479
+ return checksum.length === digest.length && crypto.timingSafeEqual(digest, checksum);
480
+ }
481
+
482
+ /**
483
+ * Handle a GitHub Marketplace webhook event.
484
+ *
485
+ * Supported actions:
486
+ * purchased — provision API key for the new customer
487
+ * changed — plan update (upgrade/downgrade)
488
+ * cancelled — disable all keys for that customer
489
+ *
490
+ * @param {object} event - Parsed GitHub Marketplace event object
491
+ * @returns {{ handled: boolean, action?: string, result?: object }}
492
+ */
493
+ function handleGithubWebhook(event) {
494
+ const { action, marketplace_purchase } = event;
495
+ if (!action || !marketplace_purchase) {
496
+ return { handled: false, reason: 'missing_payload_data' };
497
+ }
498
+
499
+ const account = marketplace_purchase.account;
500
+ if (!account || !account.id) {
501
+ return { handled: false, reason: 'missing_account_id' };
502
+ }
503
+
504
+ // Map GitHub account to customerId: github_<user|organization>_<id>
505
+ const customerId = `github_${account.type.toLowerCase()}_${account.id}`;
458
506
 
459
- module.exports = {
507
+ switch (action) {
508
+ case 'purchased': {
509
+ const result = provisionApiKey(customerId);
510
+ return {
511
+ handled: true,
512
+ action: 'provisioned_api_key',
513
+ result,
514
+ };
515
+ }
516
+
517
+ case 'cancelled': {
518
+ const result = disableCustomerKeys(customerId);
519
+ return {
520
+ handled: true,
521
+ action: 'disabled_customer_keys',
522
+ result,
523
+ };
524
+ }
525
+
526
+ case 'changed': {
527
+ // In this simple model, we just ensure a key exists and is active.
528
+ // Upgrades/downgrades don't change basic API access unless we had tiered features.
529
+ const result = provisionApiKey(customerId);
530
+ return {
531
+ handled: true,
532
+ action: 'plan_changed',
533
+ result,
534
+ };
535
+ }
536
+
537
+ default:
538
+ return { handled: false, reason: `unhandled_action:${action}` };
539
+ }
540
+ }
541
+
542
+ // ---------------------------------------------------------------------------
543
+ // Module exports
544
+ // ---------------------------------------------------------------------------
545
+
546
+ module.exports = {
460
547
  createCheckoutSession,
461
548
  provisionApiKey,
462
549
  validateApiKey,
@@ -464,8 +551,11 @@ module.exports = {
464
551
  disableCustomerKeys,
465
552
  handleWebhook,
466
553
  verifyWebhookSignature,
554
+ verifyGithubWebhookSignature,
555
+ handleGithubWebhook,
467
556
  loadKeyStore,
468
557
  // Expose for testing
469
558
  _API_KEYS_PATH: API_KEYS_PATH,
470
559
  _LOCAL_MODE: () => LOCAL_MODE,
471
- };
560
+ };
561
+
@@ -29,14 +29,43 @@ const DOMAIN_CATEGORIES = [
29
29
  'git-workflow', 'documentation', 'debugging', 'architecture', 'data-modeling',
30
30
  ];
31
31
 
32
+ const HOME = process.env.HOME || process.env.USERPROFILE || '';
33
+
32
34
  function getFeedbackPaths() {
33
- const feedbackDir = process.env.RLHF_FEEDBACK_DIR || DEFAULT_FEEDBACK_DIR;
35
+ if (process.env.RLHF_FEEDBACK_DIR) {
36
+ const d = process.env.RLHF_FEEDBACK_DIR;
37
+ return {
38
+ FEEDBACK_DIR: d,
39
+ FEEDBACK_LOG_PATH: path.join(d, 'feedback-log.jsonl'),
40
+ MEMORY_LOG_PATH: path.join(d, 'memory-log.jsonl'),
41
+ SUMMARY_PATH: path.join(d, 'feedback-summary.json'),
42
+ PREVENTION_RULES_PATH: path.join(d, 'prevention-rules.md'),
43
+ };
44
+ }
45
+
46
+ // Auto-discovery order:
47
+ // 1. .rlhf/ (Standard)
48
+ // 2. .claude/memory/feedback/ (Legacy Claude)
49
+ // 3. ~/.rlhf/projects/<cwd-basename>/ (Global fallback for true plug-and-play)
50
+
51
+ const localRlhf = path.join(process.cwd(), '.rlhf');
52
+ const localClaude = path.join(process.cwd(), '.claude', 'memory', 'feedback');
53
+
54
+ let baseDir = localRlhf;
55
+ if (!fs.existsSync(localRlhf) && fs.existsSync(localClaude)) {
56
+ baseDir = localClaude;
57
+ } else if (!fs.existsSync(localRlhf)) {
58
+ // Zero-Config Global Fallback
59
+ const projectName = path.basename(process.cwd()) || 'default';
60
+ baseDir = path.join(HOME, '.rlhf', 'projects', projectName);
61
+ }
62
+
34
63
  return {
35
- FEEDBACK_DIR: feedbackDir,
36
- FEEDBACK_LOG_PATH: path.join(feedbackDir, 'feedback-log.jsonl'),
37
- MEMORY_LOG_PATH: path.join(feedbackDir, 'memory-log.jsonl'),
38
- SUMMARY_PATH: path.join(feedbackDir, 'feedback-summary.json'),
39
- PREVENTION_RULES_PATH: path.join(feedbackDir, 'prevention-rules.md'),
64
+ FEEDBACK_DIR: baseDir,
65
+ FEEDBACK_LOG_PATH: path.join(baseDir, 'feedback-log.jsonl'),
66
+ MEMORY_LOG_PATH: path.join(baseDir, 'memory-log.jsonl'),
67
+ SUMMARY_PATH: path.join(baseDir, 'feedback-summary.json'),
68
+ PREVENTION_RULES_PATH: path.join(baseDir, 'prevention-rules.md'),
40
69
  };
41
70
  }
42
71
 
@@ -323,8 +323,10 @@ async function runProof(options = {}) {
323
323
  } catch (err) {
324
324
  addResult('fatal', false, { error: err.message });
325
325
  } finally {
326
- await new Promise((resolve) => server.close(resolve));
327
- fs.rmSync(tmpFeedbackDir, { recursive: true, force: true });
326
+ if (server) await new Promise((resolve) => server.close(resolve));
327
+ try {
328
+ fs.rmSync(tmpFeedbackDir, { recursive: true, force: true });
329
+ } catch (e) {}
328
330
  }
329
331
 
330
332
  if (writeArtifacts) {
@@ -140,8 +140,8 @@ async function runProof() {
140
140
  let vec03Status = 'fail';
141
141
  let vec03Evidence = '';
142
142
  try {
143
- const arrowSpec = PKG.dependencies['apache-arrow'] || '';
144
- const lanceSpec = PKG.dependencies['@lancedb/lancedb'] || '';
143
+ const arrowSpec = PKG.dependencies['apache-arrow'] || PKG.optionalDependencies['apache-arrow'] || '';
144
+ const lanceSpec = PKG.dependencies['@lancedb/lancedb'] || PKG.optionalDependencies['@lancedb/lancedb'] || '';
145
145
 
146
146
  // Check if spec pins to <= 18.1.0 (either "18.1.0", "^18.1.0", or "~18.1.0")
147
147
  const arrowVersion = arrowSpec.replace(/[\^~>=<]*/g, '').split('.').map(Number);
package/src/api/server.js CHANGED
@@ -37,6 +37,8 @@ const {
37
37
  recordUsage,
38
38
  handleWebhook,
39
39
  verifyWebhookSignature,
40
+ verifyGithubWebhookSignature,
41
+ handleGithubWebhook,
40
42
  loadKeyStore,
41
43
  } = require('../../scripts/billing');
42
44
 
@@ -240,6 +242,42 @@ function createApiServer() {
240
242
  return;
241
243
  }
242
244
 
245
+ // GitHub Marketplace webhook
246
+ if (req.method === 'POST' && pathname === '/v1/billing/github-webhook') {
247
+ try {
248
+ const rawBody = await new Promise((resolve, reject) => {
249
+ const chunks = [];
250
+ req.on('data', (c) => chunks.push(c));
251
+ req.on('end', () => resolve(Buffer.concat(chunks)));
252
+ req.on('error', reject);
253
+ });
254
+
255
+ const sig = req.headers['x-hub-signature-256'] || '';
256
+ if (!verifyGithubWebhookSignature(rawBody, sig)) {
257
+ sendJson(res, 400, { error: 'Invalid webhook signature' });
258
+ return;
259
+ }
260
+
261
+ let event;
262
+ try {
263
+ event = JSON.parse(rawBody.toString('utf-8'));
264
+ } catch {
265
+ sendJson(res, 400, { error: 'Invalid JSON in webhook body' });
266
+ return;
267
+ }
268
+
269
+ const result = handleGithubWebhook(event);
270
+ sendJson(res, 200, result);
271
+ } catch (err) {
272
+ if (err.statusCode) {
273
+ sendJson(res, err.statusCode, { error: err.message });
274
+ } else {
275
+ sendJson(res, 500, { error: err.message || 'Internal Server Error' });
276
+ }
277
+ }
278
+ return;
279
+ }
280
+
243
281
  if (!isAuthorized(req, expectedApiKey)) {
244
282
  sendJson(res, 401, { error: 'Unauthorized' });
245
283
  return;