intelwatch 1.2.0 → 1.3.0

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.
@@ -0,0 +1,44 @@
1
+ # CHANGELOG-DRAFT — intelwatch v1.3.0
2
+
3
+ ## ✨ New Features
4
+
5
+ ### International Provider Architecture (`src/providers/`)
6
+ - **Provider Registry** (`registry.js`) — adapts company data source based on domain TLD
7
+ - `.fr` → Pappers (full French company data)
8
+ - `.com`, `.de`, `.uk`, etc. → OpenCorporates (180+ jurisdictions)
9
+ - Clearbit available as BYOK enrichment layer (domain → sector, size, revenue, tech)
10
+ - **Auto-detection** : `detectCountry()` maps TLD to ISO country code, `resolveProvider()` picks the best provider
11
+ - **Unified API** : `searchCompany()`, `getCompanyProfile()`, `getSubsidiaries()`, `lookupCompany()` — same interface, any provider
12
+ - **License-integrated** : full profile = Pro only, subsidiaries = Pro only, preview mode for Free tier
13
+ - **Pappers provider** (`pappers.js`) — thin adapter wrapping existing `scrapers/pappers.js`
14
+ - **OpenCorporates provider** (`opencorporates.js`) — real API integration with free tier (500 req/month, no key needed) + BYOK for higher limits
15
+ - **Clearbit provider** (`clearbit.js`) — scaffold with real API integration (BYOK: `CLEARBIT_API_KEY`)
16
+ - Competitor tracker refactored: uses `lookupCompany()` instead of direct Pappers import → works for any TLD
17
+ - Backward compat: `pappers` key still present in competitor snapshots for existing data
18
+
19
+ ### Freemium License Gate
20
+ - **Centralized `src/license.js`** — single module for all Pro/Free tier logic
21
+ - License detection: `INTELWATCH_PRO_KEY` env → `INTELWATCH_LICENSE_KEY` env → `~/.intelwatch-license` file
22
+ - **Free tier:** JSON + CSV (50 rows), Reddit/HN (5 results), Pappers preview only
23
+ - **Pro tier:** JSON + CSV + XLS + PDF (unlimited), Reddit/HN (100), full profiles + subsidiaries
24
+
25
+ ### Export System (CSV, XLS, PDF)
26
+ - `handleExport()` with license-aware gating — XLS/PDF throw `LICENSE_REQUIRED`
27
+ - CSV capped at 50 rows on Free tier with warning
28
+
29
+ ### Reddit & Hacker News Mentions
30
+ - `src/scrapers/reddit-hn.js` — Reddit JSON API + HN Algolia API
31
+ - Results capped per license tier (Free: 5, Pro: 100)
32
+ - Brand tracker fetches Google News + Reddit + HN in parallel
33
+
34
+ ## 🐛 Bug Fixes
35
+ - `handleError()` crash on null/undefined — graceful handling added
36
+ - Provider registry returns `tier`/`isPreview` even when provider unavailable
37
+
38
+ ## 🧪 Tests — 136 pass, 0 fail (was 40)
39
+ - `test/providers.test.js` — 31 tests: detectCountry (11 TLDs), resolveProvider (5), listProviders (3), interface compliance (3), searchCompany (3), getCompanyProfile (2), getSubsidiaries license gate (1), individual provider availability (3)
40
+ - `test/license.test.js` — 19 tests
41
+ - `test/export.test.js` — 33 tests (incl. LICENSE_REQUIRED gates)
42
+ - `test/i18n.test.js` — 6 tests
43
+ - `test/error-handler.test.js` — 5 tests
44
+ - `test/reddit-hn.test.js` — 4 tests
package/CHANGELOG.md CHANGED
@@ -167,3 +167,18 @@ All notable changes to this project will be documented in this file.
167
167
  ### Fixed
168
168
  - Array headers bug in tech-detect.js
169
169
  - Anthropic model name updated to `claude-3-5-haiku-latest`
170
+
171
+ ## [1.3.0] - 2026-03-21
172
+ ### Added
173
+ - Pro License Paywall: Gated advanced features (PDF/XLS export, Deep Profile, International OSINT).
174
+ - Stripe Payment Link integration for Intelwatch Pro subscriptions.
175
+ - International Smart Routing: `.fr` hits Pappers, international hits Apollo/Clearbit/OpenCorporates.
176
+ - France Handoff: International companies based in France are handed off to Pappers for deeper financial data.
177
+ - Reddit JSON API & HackerNews Algolia integration for digital OSINT and sentiment tracking.
178
+ - M&A Deep Dorks: Restricts Brave Search queries to specialized PE/M&A news sources (cfnews, lesechos, fusacq, etc.)
179
+ - Export capabilities unified under `--export <json|csv|xls|pdf>` flag.
180
+
181
+ ### Fixed
182
+ - M&A History PDF generation bug: Stopped truncating AI timeline events, full timeline is now preserved.
183
+ - Group Structure Classification: Prevented PE Funds (BPIFrance, IK Partners, etc.) from being improperly categorized as operational subsidiaries in the AI due diligence report.
184
+ - Fixed `pdfData` passthrough bug that caused empty PDF exports.
Binary file
package/RELEASE.md ADDED
@@ -0,0 +1,15 @@
1
+ ✅ `intelwatch@1.2.0` est publié sur npm !
2
+
3
+ ```bash
4
+ npm install -g intelwatch
5
+ ```
6
+
7
+ ### Ce qui est en ligne :
8
+ - Les bugs de parsing AI corrigés en prod
9
+ - L'export JSON et CSV natif pour les rapports
10
+ - Les `--lang fr` qui adaptent les rapports AI
11
+ - La détection de 56 technos (Nuxt, Vercel, Tailwind, etc.)
12
+
13
+ **Note Git** : J'ai fait le commit local de la v1.2.0, mais GitHub a refusé mon push (`403 Permission to Recognity/intelwatch.git denied to ashroth1`). Tu pourras faire un `git push origin main --tags` depuis ta machine quand tu as le temps.
14
+
15
+ Dis-moi si tu veux que le sous-agent attaque un autre CLI ou une autre feature d'intelwatch (ex: Inpi, ou détection SaaS) cette nuit !
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intelwatch",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Competitive intelligence CLI — track competitors, keywords, and brand mentions from the terminal",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -8,7 +8,7 @@
8
8
  "intelwatch": "./bin/intelwatch.js"
9
9
  },
10
10
  "scripts": {
11
- "test": "node --test test/*.test.js",
11
+ "test": "node --test test/storage.test.js test/sentiment.test.js test/tech-detect.test.js test/export.test.js test/i18n.test.js test/error-handler.test.js test/reddit-hn.test.js test/license.test.js test/providers.test.js",
12
12
  "start": "node bin/intelwatch.js"
13
13
  },
14
14
  "dependencies": {
@@ -19,6 +19,7 @@
19
19
  "cli-table3": "^0.6.5",
20
20
  "commander": "^12.1.0",
21
21
  "inquirer": "^10.2.2",
22
+ "xlsx": "^0.18.5",
22
23
  "yaml": "^2.6.1"
23
24
  },
24
25
  "engines": {
@@ -6,13 +6,12 @@ import { analyzeSite } from '../scrapers/site-analyzer.js';
6
6
  import { callAI, hasAIKey } from '../ai/client.js';
7
7
  import { header, section, warn, error } from '../utils/display.js';
8
8
  import { generatePDF } from '@recognity/pdf-report';
9
- import { exportToJSON, exportToCSV, formatForExport } from '../utils/export.js';
9
+ import { handleExport, formatForExport } from '../utils/export.js';
10
10
  import { setLanguage, getLanguage, t, getPrompt } from '../utils/i18n.js';
11
-
12
- const LICENSE_URL = 'https://recognity.fr/tools/intelwatch';
11
+ import { isPro, printProUpgrade } from '../license.js';
13
12
 
14
13
  export async function runMA(sirenOrName, options) {
15
- const hasLicense = !!process.env.INTELWATCH_LICENSE_KEY;
14
+ const hasLicense = isPro();
16
15
  const isPreview = !!options.preview;
17
16
 
18
17
  // Set language from global option (passed from main program)
@@ -22,17 +21,14 @@ export async function runMA(sirenOrName, options) {
22
21
 
23
22
  // ── License gate ───────────────────────────────────────────────────────────
24
23
  if (!hasLicense && !isPreview) {
25
- console.log(chalk.yellow.bold('\n ⚡ Deep Profile Due Diligence — Module Premium\n'));
26
- console.log(chalk.red(' The Deep Profile requires an Intelwatch Deep Profile license.'));
27
- console.log(chalk.gray(` Get yours at ${LICENSE_URL}\n`));
28
- console.log(chalk.gray(' Run with --preview for a limited preview (company identity + last year financials only).'));
29
- console.log('');
24
+ printProUpgrade('Deep Profile Due Diligence');
25
+ console.log(chalk.gray(' Run with --preview for a limited preview (company identity + last year financials only).\n'));
30
26
  process.exit(1);
31
27
  }
32
28
 
33
29
  if (isPreview && !hasLicense) {
34
30
  console.log(chalk.yellow(' ⚡ PREVIEW MODE — Company identity + last year financials only'));
35
- console.log(chalk.gray(` Upgrade to Intelwatch Deep Profile for full due diligence: ${LICENSE_URL}\n`));
31
+ printProUpgrade('Full company profile');
36
32
  }
37
33
 
38
34
  // ── SIREN or name lookup ───────────────────────────────────────────────────
@@ -310,11 +306,12 @@ export async function runMA(sirenOrName, options) {
310
306
  const press = await searchPressMentions(brandName);
311
307
  pressResults = press.mentions || [];
312
308
 
313
- // Additional M&A-focused search to catch acquisitions/deals
309
+ // Additional M&A-focused search to catch acquisitions/deals (dorks: quality M&A sources only)
310
+ const MA_SITE_DORKS = '(site:fusacq.com OR site:cfnews.net OR site:lesechos.fr OR site:maddyness.com OR site:agefi.fr)';
314
311
  try {
315
312
  const { braveWebSearch } = await import('../scrapers/brave-search.js');
316
313
  await new Promise(r => setTimeout(r, 600));
317
- const maSearch = await braveWebSearch(`"${brandName}" acquisition OR rachat OR "entrée au capital" OR "prise de participation"`, { count: 10 });
314
+ const maSearch = await braveWebSearch(`"${brandName}" (acquisition OR LBO OR rachat OR "levée de fonds" OR "entrée au capital" OR "prise de participation") ${MA_SITE_DORKS}`, { count: 10 });
318
315
  for (const r of (maSearch.results || [])) {
319
316
  const text = ((r.title || '') + ' ' + (r.snippet || '')).toLowerCase();
320
317
  if (!text.includes(brandName.toLowerCase())) continue;
@@ -823,20 +820,30 @@ OBLIGATOIRE :
823
820
  const raw = await callAI(systemPrompt, userPrompt, { maxTokens: 3500 });
824
821
  aiAnalysis = extractAIJSON(raw);
825
822
 
826
- // Merge: take descriptions from AI, keep dates/types/targets from code-built array
827
- if (aiAnalysis && codeBuiltMaHistory.length > 0) {
823
+ // M&A History: Merging code-built events with AI events instead of overwriting
824
+ if (aiAnalysis) {
828
825
  const aiMa = aiAnalysis.maHistory || [];
829
- for (const codeEntry of codeBuiltMaHistory) {
830
- const targetKey = (codeEntry.target || '').toLowerCase().split(' ')[0];
831
- const aiMatch = aiMa.find(a => {
832
- const aTarget = (a.target || '').toLowerCase();
833
- return aTarget.includes(targetKey) || targetKey.includes(aTarget.split(' ')[0]);
834
- });
835
- if (aiMatch?.description && !codeEntry.description) {
836
- codeEntry.description = aiMatch.description;
826
+
827
+ // Add AI identified M&A events that aren't in codeBuiltMaHistory
828
+ const mergedMaHistory = [...codeBuiltMaHistory];
829
+
830
+ for (const aiEntry of aiMa) {
831
+ const targetKey = (aiEntry.target || '').toLowerCase().split(' ')[0];
832
+ const exists = mergedMaHistory.some(c => (c.target || '').toLowerCase().includes(targetKey));
833
+
834
+ if (!exists && targetKey.length > 2) {
835
+ mergedMaHistory.push({
836
+ date: aiEntry.date || aiEntry.year || 'Unknown',
837
+ target: aiEntry.target,
838
+ type: aiEntry.type || 'Acquisition',
839
+ description: aiEntry.description || aiEntry.rationale || ''
840
+ });
837
841
  }
838
842
  }
839
- aiAnalysis.maHistory = codeBuiltMaHistory;
843
+
844
+ // Sort by date (descending string comparison is mostly ok for YYYY-MM)
845
+ mergedMaHistory.sort((a, b) => b.date.localeCompare(a.date));
846
+ aiAnalysis.maHistory = mergedMaHistory;
840
847
  }
841
848
 
842
849
  if (aiAnalysis) {
@@ -1307,7 +1314,7 @@ OBLIGATOIRE :
1307
1314
  financialHistory,
1308
1315
  subsidiaries: subsidiariesData,
1309
1316
  aiAnalysis,
1310
- groupStructure: pdFriendlyData?.groupStructure,
1317
+ groupStructure: aiAnalysis?.groupStructure,
1311
1318
  summary: `${identity.name || siren} — ${identity.formeJuridique || ''}, ${identity.nafLabel || ''}. Created ${identity.dateCreation || '?'}.`,
1312
1319
  executiveSummary: aiAnalysis?.executiveSummary,
1313
1320
  strengths: aiAnalysis?.strengths || [],
@@ -1319,17 +1326,16 @@ OBLIGATOIRE :
1319
1326
  language: getLanguage()
1320
1327
  };
1321
1328
 
1322
- const formatted = formatForExport(profileData, 'profile');
1323
-
1324
- if (options.export.toLowerCase() === 'json') {
1325
- const result = exportToJSON(formatted, options.output);
1326
- console.log(chalk.green(`\n ✅ ${result}\n`));
1327
- } else if (options.export.toLowerCase() === 'csv') {
1328
- const result = exportToCSV(formatted, options.output);
1329
- console.log(chalk.green(`\n ✅ ${result}\n`));
1330
- } else {
1331
- console.log(chalk.yellow(`\n ⚠️ Unsupported export format: ${options.export}. Use 'json' or 'csv'.\n`));
1332
- }
1329
+ const result = await handleExport(options.export, profileData, {
1330
+ pdfData: pdfData,
1331
+ output: options.output,
1332
+ commandType: 'profile',
1333
+ pdfOptions: {
1334
+ type: 'intel-report',
1335
+ title: `Profile ${identity.name || siren}`,
1336
+ },
1337
+ });
1338
+ console.log(chalk.green(`\n ${result}\n`));
1333
1339
  } catch (e) {
1334
1340
  console.error(chalk.red(`\n ❌ Export failed: ${e.message}\n`));
1335
1341
  }
@@ -9,7 +9,7 @@ import { diffCompetitorSnapshots } from '../trackers/competitor.js';
9
9
  import { diffKeywordSnapshots } from '../trackers/keyword.js';
10
10
  import { diffBrandSnapshots } from '../trackers/brand.js';
11
11
  import { computeThreatScore } from '../trackers/competitor.js';
12
- import { exportToJSON, exportToCSV, formatForExport } from '../utils/export.js';
12
+ import { handleExport, formatForExport } from '../utils/export.js';
13
13
  import { setLanguage, getLanguage } from '../utils/i18n.js';
14
14
 
15
15
  export async function runReport(options = {}) {
@@ -87,20 +87,18 @@ export async function runReport(options = {}) {
87
87
  console.log(content);
88
88
  }
89
89
 
90
- // ── Export raw data ────────────────────────────────────────────────────────
90
+ // ── Export raw data (json, csv, xls, pdf) ──────────────────────────────────
91
91
  if (options.export) {
92
92
  try {
93
- const formatted = formatForExport(reportData, 'report');
94
-
95
- if (options.export.toLowerCase() === 'json') {
96
- const result = exportToJSON(formatted, options.output ? options.output.replace(/\.[^.]+$/, '-data.json') : null);
97
- console.log(chalk.green(`\n ✅ ${result}\n`));
98
- } else if (options.export.toLowerCase() === 'csv') {
99
- const result = exportToCSV(formatted, options.output ? options.output.replace(/\.[^.]+$/, '-data.csv') : null);
100
- console.log(chalk.green(`\n ✅ ${result}\n`));
101
- } else {
102
- console.log(chalk.yellow(`\n ⚠️ Unsupported export format: ${options.export}. Use 'json' or 'csv'.\n`));
103
- }
93
+ const result = await handleExport(options.export, reportData, {
94
+ output: options.output,
95
+ commandType: 'report',
96
+ pdfOptions: {
97
+ type: 'intel-report',
98
+ title: 'IntelWatch Intelligence Report',
99
+ },
100
+ });
101
+ console.log(chalk.green(`\n ✅ ${result}\n`));
104
102
  } catch (e) {
105
103
  console.error(chalk.red(`\n ❌ Export failed: ${e.message}\n`));
106
104
  }
package/src/index.js CHANGED
@@ -12,6 +12,11 @@ import { listTrackers, removeTrackerCmd } from './commands/list.js';
12
12
  import { runAISummary } from './commands/ai-summary.js';
13
13
  import { runPitch } from './commands/pitch.js';
14
14
  import { runMA } from './commands/profile.js';
15
+ import { saveLicenseKey, isPro, _resetCache } from './license.js';
16
+ import chalk from 'chalk';
17
+
18
+ // Register company data providers (Pappers, OpenCorporates, Clearbit, Apollo)
19
+ import './providers/index.js';
15
20
 
16
21
  const program = new Command();
17
22
 
@@ -81,7 +86,7 @@ program
81
86
  .command('check')
82
87
  .description('Run checks for all (or one) tracker(s)')
83
88
  .option('--tracker <id>', 'Only check this specific tracker')
84
- .option('--export <format>', 'Export results (json, csv)')
89
+ .option('--export <format>', 'Export results (json, csv, xls, pdf)')
85
90
  .option('--output <file>', 'Output file path for export')
86
91
  .action(async (options) => {
87
92
  await runCheck(options);
@@ -92,7 +97,7 @@ program
92
97
  program
93
98
  .command('digest')
94
99
  .description('Show a summary of all changes across all trackers')
95
- .option('--export <format>', 'Export results (json, csv)')
100
+ .option('--export <format>', 'Export results (json, csv, xls, pdf)')
96
101
  .option('--output <file>', 'Output file path for export')
97
102
  .action(async (options) => {
98
103
  await runDigest(options);
@@ -115,7 +120,7 @@ program
115
120
  .description('Generate a full intelligence report')
116
121
  .option('--format <format>', 'Output format: md, html, json', 'md')
117
122
  .option('--output <file>', 'Write report to file')
118
- .option('--export <format>', 'Export raw data (json, csv)')
123
+ .option('--export <format>', 'Export raw data (json, csv, xls, pdf)')
119
124
  .action(async (options) => {
120
125
  await runReport(options);
121
126
  });
@@ -170,7 +175,7 @@ program
170
175
  .option('--ai', 'Generate an AI-powered due diligence summary (requires AI API key)')
171
176
  .option('--format <type>', 'Output format: terminal (default) or pdf')
172
177
  .option('--output <path>', 'Output file path for PDF')
173
- .option('--export <format>', 'Export structured data (json, csv)')
178
+ .option('--export <format>', 'Export structured data (json, csv, xls, pdf)')
174
179
  .action(async (sirenOrName, options) => {
175
180
  await runMA(sirenOrName, options);
176
181
  });
@@ -200,4 +205,25 @@ program
200
205
  }
201
206
  });
202
207
 
208
+ // ─── auth ─────────────────────────────────────────────────────────────────────
209
+
210
+ program
211
+ .command('auth <key>')
212
+ .description('Activate Pro license')
213
+ .action((key) => {
214
+ try {
215
+ const file = saveLicenseKey(key);
216
+ _resetCache();
217
+ if (isPro()) {
218
+ console.log(chalk.green(' ✅ Pro license activated!'));
219
+ console.log(chalk.gray(` Saved to ${file}`));
220
+ } else {
221
+ console.log(chalk.red(' ❌ License key appears invalid.'));
222
+ }
223
+ } catch (err) {
224
+ console.error(chalk.red(` ❌ ${err.message}`));
225
+ process.exit(1);
226
+ }
227
+ });
228
+
203
229
  export { program };
package/src/license.js ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Freemium license gate for Intelwatch.
3
+ *
4
+ * License check order:
5
+ * 1. process.env.INTELWATCH_PRO_KEY
6
+ * 2. ~/.intelwatch-license (file containing license key)
7
+ *
8
+ * Pro features:
9
+ * - XLS / PDF export
10
+ * - Unlimited CSV rows (Free: capped at 50)
11
+ * - Unlimited Reddit/HN results (Free: capped at 5)
12
+ * - Full Pappers company profile (Free: --preview only)
13
+ * - Full brand mention history
14
+ *
15
+ * The key is validated as a non-empty string. Actual server-side
16
+ * validation can be added later (e.g. license.recognity.fr/verify).
17
+ */
18
+
19
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
20
+ import { join } from 'path';
21
+ import { homedir } from 'os';
22
+ import chalk from 'chalk';
23
+
24
+ const LICENSE_FILE = join(homedir(), '.intelwatch-license');
25
+ const LICENSE_FILE_ALT = join(homedir(), '.intelwatch-pro');
26
+ const LICENSE_URL = 'https://recognity.fr/tools/intelwatch';
27
+
28
+ // ── Limits ───────────────────────────────────────────────────────────────────
29
+
30
+ export const FREE_LIMITS = {
31
+ csvMaxRows: 50,
32
+ redditMaxResults: 5,
33
+ hnMaxResults: 5,
34
+ pappersFullProfile: false,
35
+ exportFormats: ['json', 'csv'],
36
+ };
37
+
38
+ export const PRO_LIMITS = {
39
+ csvMaxRows: Infinity,
40
+ redditMaxResults: 100,
41
+ hnMaxResults: 100,
42
+ pappersFullProfile: true,
43
+ exportFormats: ['json', 'csv', 'xls', 'xlsx', 'excel', 'pdf'],
44
+ };
45
+
46
+ // ── Cache ────────────────────────────────────────────────────────────────────
47
+
48
+ let _cachedKey = undefined;
49
+
50
+ function readLicenseKey() {
51
+ if (_cachedKey !== undefined) return _cachedKey;
52
+
53
+ // 1. Environment variable
54
+ if (process.env.INTELWATCH_PRO_KEY) {
55
+ _cachedKey = process.env.INTELWATCH_PRO_KEY.trim();
56
+ return _cachedKey;
57
+ }
58
+
59
+ // 2. Legacy env var (backward compat with profile.js)
60
+ if (process.env.INTELWATCH_LICENSE_KEY) {
61
+ _cachedKey = process.env.INTELWATCH_LICENSE_KEY.trim();
62
+ return _cachedKey;
63
+ }
64
+
65
+ // 3. License file (~/.intelwatch-license or ~/.intelwatch-pro)
66
+ for (const file of [LICENSE_FILE, LICENSE_FILE_ALT]) {
67
+ if (existsSync(file)) {
68
+ try {
69
+ const content = readFileSync(file, 'utf8').trim();
70
+ if (content) {
71
+ _cachedKey = content;
72
+ return _cachedKey;
73
+ }
74
+ } catch {
75
+ // Ignore read errors
76
+ }
77
+ }
78
+ }
79
+
80
+ _cachedKey = null;
81
+ return _cachedKey;
82
+ }
83
+
84
+ // ── Public API ───────────────────────────────────────────────────────────────
85
+
86
+ /**
87
+ * Check if the user has a Pro license.
88
+ */
89
+ export function isPro() {
90
+ return !!readLicenseKey();
91
+ }
92
+
93
+ /**
94
+ * Get the current license key (or null).
95
+ */
96
+ export function getLicenseKey() {
97
+ return readLicenseKey();
98
+ }
99
+
100
+ /**
101
+ * Get the limits for the current tier.
102
+ */
103
+ export function getLimits() {
104
+ return isPro() ? PRO_LIMITS : FREE_LIMITS;
105
+ }
106
+
107
+ /**
108
+ * Assert that a Pro feature is available. Throws if not.
109
+ * @param {string} featureName — human-readable feature name for the error message
110
+ */
111
+ export function requirePro(featureName) {
112
+ if (isPro()) return;
113
+ const msg = `${featureName} requires an Intelwatch Pro license.`;
114
+ const err = new Error(msg);
115
+ err.code = 'LICENSE_REQUIRED';
116
+ err.featureName = featureName;
117
+ throw err;
118
+ }
119
+
120
+ /**
121
+ * Print a Pro upgrade message to stderr (non-blocking, doesn't throw).
122
+ * @param {string} featureName
123
+ */
124
+ export function printProUpgrade(featureName) {
125
+ console.error('');
126
+ console.error(chalk.yellow(` ⚡ ${featureName} — Pro Feature`));
127
+ console.error(chalk.gray(` Upgrade to Intelwatch Pro for full access.`));
128
+ console.error(chalk.gray(` ${LICENSE_URL}`));
129
+ console.error(chalk.gray(` Set INTELWATCH_PRO_KEY or create ~/.intelwatch-license`));
130
+ console.error('');
131
+ }
132
+
133
+ /**
134
+ * Gate a Pro feature: if not Pro, print upgrade message and return false.
135
+ * @param {string} featureName
136
+ * @returns {boolean} true if Pro, false otherwise
137
+ */
138
+ export function gatePro(featureName) {
139
+ if (isPro()) return true;
140
+ printProUpgrade(featureName);
141
+ return false;
142
+ }
143
+
144
+ /**
145
+ * Truncate an array to Free-tier limit with a warning.
146
+ * @param {Array} data
147
+ * @param {number} freeLimit
148
+ * @param {string} featureName
149
+ * @returns {Array}
150
+ */
151
+ export function applyFreeLimit(data, freeLimit, featureName) {
152
+ if (!Array.isArray(data)) return data;
153
+ if (isPro() || data.length <= freeLimit) return data;
154
+
155
+ console.error(chalk.yellow(` ⚠️ Free tier: showing ${freeLimit}/${data.length} results. Upgrade to Pro for unlimited ${featureName}.`));
156
+ return data.slice(0, freeLimit);
157
+ }
158
+
159
+ /**
160
+ * Save a license key to ~/.intelwatch-pro and refresh the cache.
161
+ * @param {string} key
162
+ */
163
+ export function saveLicenseKey(key) {
164
+ const trimmed = (key || '').trim();
165
+ if (!trimmed) {
166
+ throw new Error('License key cannot be empty.');
167
+ }
168
+ writeFileSync(LICENSE_FILE_ALT, trimmed + '\n', 'utf8');
169
+ _cachedKey = undefined; // bust cache
170
+ return LICENSE_FILE_ALT;
171
+ }
172
+
173
+ /**
174
+ * Print a clean paywall block and exit the process (non-throwing).
175
+ * Use this instead of requirePro when you want a user-friendly exit.
176
+ * @param {string} featureName
177
+ */
178
+ export function printPaywallAndExit(featureName) {
179
+ console.error('');
180
+ console.error(chalk.red(' 🔒 This is a Pro feature!'));
181
+ console.error(chalk.yellow(` "${featureName}" requires an Intelwatch Pro license.`));
182
+ console.error('');
183
+ console.error(chalk.gray(' Upgrade at ') + chalk.cyan.underline(LICENSE_URL));
184
+ console.error(chalk.gray(' Then run: ') + chalk.white('intelwatch auth <key>'));
185
+ console.error('');
186
+ process.exit(0);
187
+ }
188
+
189
+ /**
190
+ * Reset cached license (for testing).
191
+ */
192
+ export function _resetCache() {
193
+ _cachedKey = undefined;
194
+ }