intelwatch 1.2.0 → 1.3.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/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
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Apollo Provider — International company enrichment (BYOK).
3
+ *
4
+ * API: https://apolloapi.io/developers
5
+ * Requires APOLLO_API_KEY.
6
+ *
7
+ * Provides:
8
+ * - Domain → company enrichment (name, sector, employee count, revenue, tech)
9
+ * - Good for .com / international where Pappers doesn't work
10
+ * - Complements Clearbit with similar enrichment capabilities
11
+ *
12
+ * All Apollo queries are Pro-only (license gate in registry.js).
13
+ */
14
+
15
+ import { fetch } from '../utils/fetcher.js';
16
+
17
+ const APOLLO_API = 'https://api.apollo.io/v1';
18
+
19
+ function getApiKey() {
20
+ return process.env.APOLLO_API_KEY || null;
21
+ }
22
+
23
+ const apolloProvider = {
24
+ name: 'apollo',
25
+ country: 'INTL',
26
+ description: 'Apollo.io — Domain-based company enrichment (sector, size, tech, revenue)',
27
+
28
+ isAvailable() {
29
+ return !!getApiKey();
30
+ },
31
+
32
+ /**
33
+ * Search companies by name or domain.
34
+ * @param {string} query
35
+ * @param {{ count?: number }} options
36
+ * @returns {Promise<{ results: Array, error: string|null }>}
37
+ */
38
+ async search(query, options = {}) {
39
+ const apiKey = getApiKey();
40
+ if (!apiKey) {
41
+ return { results: [], error: 'APOLLO_API_KEY not set.' };
42
+ }
43
+
44
+ try {
45
+ const body = {
46
+ q_organization_name: query,
47
+ per_page: Math.min(options.count || 10, 25),
48
+ };
49
+
50
+ const resp = await fetch(`${APOLLO_API}/mixed_companies/search`, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ 'Cache-Control': 'no-cache',
55
+ 'X-Api-Key': apiKey,
56
+ },
57
+ data: JSON.stringify(body),
58
+ timeout: 15000,
59
+ });
60
+
61
+ const data = typeof resp.data === 'string' ? JSON.parse(resp.data) : resp.data;
62
+ const orgs = data?.organizations || [];
63
+
64
+ const results = orgs.map(c => ({
65
+ name: c.name,
66
+ domain: c.primary_domain,
67
+ sector: c.industry,
68
+ employeeRange: c.estimated_num_employees ? `${c.estimated_num_employees}` : null,
69
+ estimatedRevenue: c.annual_revenue_printed,
70
+ description: c.short_description,
71
+ foundedYear: c.founded_year,
72
+ location: [c.city, c.state, c.country].filter(Boolean).join(', '),
73
+ country: c.country,
74
+ logo: c.logo_url,
75
+ url: c.website_url || (c.primary_domain ? `https://${c.primary_domain}` : null),
76
+ linkedin: c.linkedin_url,
77
+ phone: c.phone,
78
+ source: 'apollo',
79
+ }));
80
+
81
+ return { results, error: null };
82
+ } catch (err) {
83
+ return { results: [], error: err.message };
84
+ }
85
+ },
86
+
87
+ /**
88
+ * Get company profile by domain.
89
+ * @param {string} domain — e.g. "stripe.com"
90
+ * @param {{ preview?: boolean }} options
91
+ * @returns {Promise<{ data: object|null, error: string|null }>}
92
+ */
93
+ async getProfile(domain, options = {}) {
94
+ const apiKey = getApiKey();
95
+ if (!apiKey) {
96
+ return { data: null, error: 'APOLLO_API_KEY not set.' };
97
+ }
98
+
99
+ try {
100
+ const cleanDomain = domain.replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0];
101
+
102
+ const resp = await fetch(`${APOLLO_API}/organizations/enrich`, {
103
+ method: 'GET',
104
+ headers: {
105
+ 'Cache-Control': 'no-cache',
106
+ 'X-Api-Key': apiKey,
107
+ },
108
+ params: { domain: cleanDomain },
109
+ timeout: 15000,
110
+ });
111
+
112
+ const data = typeof resp.data === 'string' ? JSON.parse(resp.data) : resp.data;
113
+ const c = data?.organization;
114
+
115
+ if (!c) {
116
+ return { data: null, error: 'Company not found on Apollo' };
117
+ }
118
+
119
+ const profile = {
120
+ name: c.name,
121
+ legalName: c.name,
122
+ domain: c.primary_domain || cleanDomain,
123
+ sector: c.industry,
124
+ subIndustry: c.sub_industry,
125
+ employeeRange: c.estimated_num_employees ? `${c.estimated_num_employees}` : null,
126
+ estimatedRevenue: c.annual_revenue_printed,
127
+ raised: c.total_funding_printed,
128
+ description: c.short_description,
129
+ foundedYear: c.founded_year,
130
+ location: [c.city, c.state, c.country].filter(Boolean).join(', '),
131
+ country: c.country,
132
+ logo: c.logo_url,
133
+ techStack: c.current_technologies?.map(t => t.name) || [],
134
+ tags: c.keywords || [],
135
+ url: c.website_url || `https://${cleanDomain}`,
136
+ linkedin: c.linkedin_url,
137
+ twitter: c.twitter_url,
138
+ facebook: c.facebook_url,
139
+ phone: c.phone,
140
+ source: 'apollo',
141
+ };
142
+
143
+ return { data: profile, error: null };
144
+ } catch (err) {
145
+ return { data: null, error: err.message };
146
+ }
147
+ },
148
+
149
+ /**
150
+ * No subsidiary data from Apollo.
151
+ */
152
+ async getSubsidiaries() {
153
+ return {
154
+ subsidiaries: [],
155
+ error: 'Apollo does not provide subsidiary data.',
156
+ };
157
+ },
158
+
159
+ /**
160
+ * Quick lookup by domain.
161
+ */
162
+ async lookup(companyNameOrDomain) {
163
+ if (!this.isAvailable()) return null;
164
+ const domain = companyNameOrDomain.includes('.')
165
+ ? companyNameOrDomain
166
+ : `${companyNameOrDomain.toLowerCase().replace(/\s+/g, '')}.com`;
167
+ const result = await this.getProfile(domain, { preview: true });
168
+ return result.data;
169
+ },
170
+ };
171
+
172
+ export default apolloProvider;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Clearbit Provider — International company enrichment (BYOK).
3
+ *
4
+ * API: https://dashboard.clearbit.com/docs
5
+ * Requires CLEARBIT_API_KEY.
6
+ *
7
+ * Provides:
8
+ * - Domain → company enrichment (name, sector, employee count, revenue range, tech stack)
9
+ * - Good for .com / international where Pappers doesn't work
10
+ * - Complements OpenCorporates (OC = registry data, Clearbit = market data)
11
+ *
12
+ * This is a scaffold ready for BYOK integration.
13
+ */
14
+
15
+ import { fetch } from '../utils/fetcher.js';
16
+
17
+ const CLEARBIT_API = 'https://company.clearbit.com/v2';
18
+
19
+ function getApiKey() {
20
+ return process.env.CLEARBIT_API_KEY || null;
21
+ }
22
+
23
+ const clearbitProvider = {
24
+ name: 'clearbit',
25
+ country: 'INTL',
26
+ description: 'Clearbit — Domain-based company enrichment (sector, size, tech, revenue)',
27
+
28
+ isAvailable() {
29
+ return !!getApiKey();
30
+ },
31
+
32
+ /**
33
+ * Search is not the primary Clearbit use case — use domain enrichment instead.
34
+ * Falls back to domain lookup if query looks like a domain.
35
+ */
36
+ async search(query, options = {}) {
37
+ // Clearbit's primary API is domain-based, not name-based
38
+ if (query.includes('.')) {
39
+ // Looks like a domain — use enrichment
40
+ const profile = await this.getProfile(query);
41
+ if (profile.data) {
42
+ return { results: [profile.data], error: null };
43
+ }
44
+ }
45
+
46
+ return {
47
+ results: [],
48
+ error: 'Clearbit requires a domain for lookup. Try with a domain (e.g., company.com).',
49
+ };
50
+ },
51
+
52
+ /**
53
+ * Get company profile by domain.
54
+ * @param {string} domain — e.g. "stripe.com"
55
+ * @param {{ preview?: boolean }} options
56
+ * @returns {Promise<{ data: object|null, error: string|null }>}
57
+ */
58
+ async getProfile(domain, options = {}) {
59
+ const apiKey = getApiKey();
60
+ if (!apiKey) {
61
+ return { data: null, error: 'CLEARBIT_API_KEY not set.' };
62
+ }
63
+
64
+ try {
65
+ // Clean domain
66
+ const cleanDomain = domain.replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0];
67
+
68
+ const resp = await fetch(`${CLEARBIT_API}/companies/find?domain=${cleanDomain}`, {
69
+ headers: {
70
+ Authorization: `Bearer ${apiKey}`,
71
+ Accept: 'application/json',
72
+ },
73
+ timeout: 15000,
74
+ });
75
+
76
+ if (resp.status === 404) {
77
+ return { data: null, error: 'Company not found on Clearbit' };
78
+ }
79
+
80
+ const c = typeof resp.data === 'string' ? JSON.parse(resp.data) : resp.data;
81
+
82
+ const profile = {
83
+ name: c.name,
84
+ legalName: c.legalName,
85
+ domain: c.domain,
86
+ sector: c.category?.sector,
87
+ industry: c.category?.industry,
88
+ subIndustry: c.category?.subIndustry,
89
+ employeeRange: c.metrics?.employeesRange,
90
+ estimatedRevenue: c.metrics?.estimatedAnnualRevenue,
91
+ raised: c.metrics?.raised,
92
+ description: c.description,
93
+ foundedYear: c.foundedYear,
94
+ location: c.geo ? `${c.geo.city}, ${c.geo.country}` : null,
95
+ country: c.geo?.country,
96
+ logo: c.logo,
97
+ techStack: c.tech || [],
98
+ tags: c.tags || [],
99
+ url: `https://${c.domain}`,
100
+ linkedin: c.linkedin?.handle ? `https://linkedin.com/company/${c.linkedin.handle}` : null,
101
+ twitter: c.twitter?.handle ? `https://twitter.com/${c.twitter.handle}` : null,
102
+ facebook: c.facebook?.handle ? `https://facebook.com/${c.facebook.handle}` : null,
103
+ source: 'clearbit',
104
+ };
105
+
106
+ return { data: profile, error: null };
107
+ } catch (err) {
108
+ return { data: null, error: err.message };
109
+ }
110
+ },
111
+
112
+ /**
113
+ * No subsidiary data from Clearbit.
114
+ */
115
+ async getSubsidiaries() {
116
+ return {
117
+ subsidiaries: [],
118
+ error: 'Clearbit does not provide subsidiary data.',
119
+ };
120
+ },
121
+
122
+ /**
123
+ * Quick lookup by domain.
124
+ */
125
+ async lookup(companyNameOrDomain) {
126
+ if (!this.isAvailable()) return null;
127
+ // Best used with domains
128
+ const domain = companyNameOrDomain.includes('.')
129
+ ? companyNameOrDomain
130
+ : `${companyNameOrDomain.toLowerCase().replace(/\s+/g, '')}.com`;
131
+ const result = await this.getProfile(domain, { preview: true });
132
+ return result.data;
133
+ },
134
+ };
135
+
136
+ export default clearbitProvider;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Provider auto-registration.
3
+ *
4
+ * Import this module once at CLI startup to register all company data providers.
5
+ *
6
+ * Usage:
7
+ * import './providers/index.js'; // registers all
8
+ * import { resolveProvider } from './providers/registry.js'; // use
9
+ */
10
+
11
+ import { registerProvider } from './registry.js';
12
+ import pappersProvider from './pappers.js';
13
+ import opencorporatesProvider from './opencorporates.js';
14
+ import clearbitProvider from './clearbit.js';
15
+ import apolloProvider from './apollo.js';
16
+
17
+ registerProvider('pappers', pappersProvider);
18
+ registerProvider('opencorporates', opencorporatesProvider);
19
+ registerProvider('clearbit', clearbitProvider);
20
+ registerProvider('apollo', apolloProvider);
21
+
22
+ export {
23
+ detectCountry,
24
+ resolveProvider,
25
+ searchCompany,
26
+ getCompanyProfile,
27
+ getSubsidiaries,
28
+ lookupCompany,
29
+ listProviders,
30
+ } from './registry.js';
@@ -0,0 +1,159 @@
1
+ /**
2
+ * OpenCorporates Provider — International company data.
3
+ *
4
+ * API: https://api.opencorporates.com/v0.4/
5
+ * Free tier: 500 req/month, basic data (name, jurisdiction, status, incorporation date)
6
+ * Pro tier (BYOK): full data, officers, filings, statements
7
+ *
8
+ * This is a scaffold with real API integration for search and basic profile.
9
+ * Full enrichment (officers, filings) can be added incrementally.
10
+ */
11
+
12
+ import { fetch } from '../utils/fetcher.js';
13
+
14
+ const OC_API = 'https://api.opencorporates.com/v0.4';
15
+
16
+ function getApiKey() {
17
+ return process.env.OPENCORPORATES_API_KEY || null;
18
+ }
19
+
20
+ const opencorporatesProvider = {
21
+ name: 'opencorporates',
22
+ country: 'INTL',
23
+ description: 'OpenCorporates — International company registry (180+ jurisdictions)',
24
+
25
+ /**
26
+ * OpenCorporates has a free tier (no key needed, 500/month).
27
+ * With API key → higher limits + more data.
28
+ */
29
+ isAvailable() {
30
+ // Always available (free tier exists), but limited without key
31
+ return true;
32
+ },
33
+
34
+ /**
35
+ * Search companies by name.
36
+ * @param {string} query
37
+ * @param {{ count?: number, jurisdiction?: string }} options
38
+ * @returns {Promise<{ results: Array, error: string|null }>}
39
+ */
40
+ async search(query, options = {}) {
41
+ try {
42
+ const params = new URLSearchParams({ q: query });
43
+ if (options.jurisdiction) params.set('jurisdiction_code', options.jurisdiction);
44
+ if (options.count) params.set('per_page', String(Math.min(options.count, 30)));
45
+ const apiKey = getApiKey();
46
+ if (apiKey) params.set('api_token', apiKey);
47
+
48
+ const resp = await fetch(`${OC_API}/companies/search?${params}`, {
49
+ headers: { Accept: 'application/json' },
50
+ timeout: 15000,
51
+ });
52
+
53
+ const data = typeof resp.data === 'string' ? JSON.parse(resp.data) : resp.data;
54
+ const companies = data?.results?.companies || [];
55
+
56
+ const results = companies.map(({ company: c }) => ({
57
+ name: c.name,
58
+ companyNumber: c.company_number,
59
+ jurisdiction: c.jurisdiction_code,
60
+ status: c.current_status,
61
+ incorporationDate: c.incorporation_date,
62
+ companyType: c.company_type,
63
+ registeredAddress: c.registered_address_in_full,
64
+ url: c.opencorporates_url,
65
+ source: 'opencorporates',
66
+ }));
67
+
68
+ return { results, error: null };
69
+ } catch (err) {
70
+ return { results: [], error: err.message };
71
+ }
72
+ },
73
+
74
+ /**
75
+ * Get company profile by jurisdiction + company number.
76
+ * @param {string} identifier — format: "jurisdiction/company_number" e.g. "gb/12345678"
77
+ * @param {{ preview?: boolean }} options
78
+ * @returns {Promise<{ data: object|null, error: string|null }>}
79
+ */
80
+ async getProfile(identifier, options = {}) {
81
+ try {
82
+ // Identifier can be "gb/12345678" or just a company number
83
+ const path = identifier.includes('/') ? identifier : `us_${identifier}`;
84
+ const params = new URLSearchParams();
85
+ const apiKey = getApiKey();
86
+ if (apiKey) params.set('api_token', apiKey);
87
+
88
+ const resp = await fetch(`${OC_API}/companies/${path}?${params}`, {
89
+ headers: { Accept: 'application/json' },
90
+ timeout: 15000,
91
+ });
92
+
93
+ const data = typeof resp.data === 'string' ? JSON.parse(resp.data) : resp.data;
94
+ const c = data?.results?.company;
95
+
96
+ if (!c) return { data: null, error: 'Company not found' };
97
+
98
+ const profile = {
99
+ name: c.name,
100
+ companyNumber: c.company_number,
101
+ jurisdiction: c.jurisdiction_code,
102
+ status: c.current_status,
103
+ incorporationDate: c.incorporation_date,
104
+ dissolutionDate: c.dissolution_date,
105
+ companyType: c.company_type,
106
+ registeredAddress: c.registered_address_in_full,
107
+ previousNames: (c.previous_names || []).map(pn => pn.company_name),
108
+ agentName: c.agent_name,
109
+ agentAddress: c.agent_address,
110
+ url: c.opencorporates_url,
111
+ source: 'opencorporates',
112
+ // Officers and filings available with Pro key
113
+ officerCount: c.number_of_employees || null,
114
+ ...(options.preview ? {} : {
115
+ officers: (c.officers || []).map(o => ({
116
+ name: o.officer?.name,
117
+ position: o.officer?.position,
118
+ startDate: o.officer?.start_date,
119
+ endDate: o.officer?.end_date,
120
+ })),
121
+ filings: (c.filings || []).slice(0, 20).map(f => ({
122
+ title: f.filing?.title,
123
+ date: f.filing?.date,
124
+ type: f.filing?.filing_type,
125
+ url: f.filing?.opencorporates_url,
126
+ })),
127
+ }),
128
+ };
129
+
130
+ return { data: profile, error: null };
131
+ } catch (err) {
132
+ return { data: null, error: err.message };
133
+ }
134
+ },
135
+
136
+ /**
137
+ * Subsidiary lookup — not directly supported by OC free tier.
138
+ * Returns empty with a note.
139
+ */
140
+ async getSubsidiaries(parentName, parentId, options = {}) {
141
+ return {
142
+ subsidiaries: [],
143
+ error: 'OpenCorporates does not provide subsidiary data in the free tier. Use a jurisdiction-specific provider for subsidiary analysis.',
144
+ };
145
+ },
146
+
147
+ /**
148
+ * Quick lookup for competitor tracker.
149
+ */
150
+ async lookup(companyName) {
151
+ const result = await this.search(companyName, { count: 1 });
152
+ if (result.results.length > 0) {
153
+ return result.results[0];
154
+ }
155
+ return null;
156
+ },
157
+ };
158
+
159
+ export default opencorporatesProvider;