intelwatch 1.3.0 → 1.5.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.
- package/CHANGELOG.md +15 -0
- package/README.md +72 -118
- package/ROADMAP-PREMIUM.md +17 -17
- package/package.json +2 -2
- package/src/ai/client.js +82 -3
- package/src/commands/compare.js +323 -0
- package/src/commands/discover.js +3 -3
- package/src/commands/profile.js +90 -28
- package/src/index.js +5 -4
- package/src/providers/annuaire-entreprises.js +83 -0
- package/src/providers/index.js +2 -0
- package/src/providers/registry.js +156 -50
- package/src/scrapers/annuaire-entreprises.js +461 -0
- package/src/scrapers/searxng-search.js +486 -0
- package/src/trackers/competitor.js +3 -3
- package/src/trackers/keyword.js +3 -3
- package/src/trackers/person.js +3 -3
- package/src/utils/fetcher.js +123 -4
- package/src/utils/parser.js +5 -3
- package/Endrix-Intelwatch-DueDil.pdf +0 -0
- package/src/scrapers/brave-search.js +0 -281
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Annuaire Entreprises Provider — France only, 100% free, no API key.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the src/scrapers/annuaire-entreprises.js into the provider interface.
|
|
5
|
+
* This is the zero-cost fallback when Pappers is unavailable or returns 401.
|
|
6
|
+
*
|
|
7
|
+
* Data available:
|
|
8
|
+
* ✓ SIREN, SIRET, nom, NAF, nature juridique, adresse, effectifs
|
|
9
|
+
* ✓ Dirigeants (nom, prénom, qualité)
|
|
10
|
+
* ✓ Finances (CA, résultat net par année)
|
|
11
|
+
* ✓ Catégorie entreprise (PME/ETI/GE)
|
|
12
|
+
* ✗ UBO, BODACC, procédures collectives, mandats croisés, finances consolidées
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
annuaireSearchByName,
|
|
17
|
+
annuaireGetBySiren,
|
|
18
|
+
annuaireGetFullDossier,
|
|
19
|
+
annuaireLookup,
|
|
20
|
+
} from '../scrapers/annuaire-entreprises.js';
|
|
21
|
+
|
|
22
|
+
const annuaireEntreprisesProvider = {
|
|
23
|
+
name: 'annuaire-entreprises',
|
|
24
|
+
country: 'FR',
|
|
25
|
+
description: 'Annuaire Entreprises (data.gouv.fr) — French company registry, 100% free, no API key required',
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Always available — this is a free, public API with no authentication.
|
|
29
|
+
*/
|
|
30
|
+
isAvailable() {
|
|
31
|
+
return true;
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Search companies by name.
|
|
36
|
+
* @param {string} query
|
|
37
|
+
* @param {{ count?: number }} options
|
|
38
|
+
* @returns {Promise<{ results: Array, error: string|null }>}
|
|
39
|
+
*/
|
|
40
|
+
async search(query, options = {}) {
|
|
41
|
+
return annuaireSearchByName(query, options);
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get company profile by SIREN.
|
|
46
|
+
* @param {string} siren
|
|
47
|
+
* @param {{ preview?: boolean }} options
|
|
48
|
+
* @returns {Promise<{ data: object|null, error: string|null, fromCache?: boolean }>}
|
|
49
|
+
*/
|
|
50
|
+
async getProfile(siren, options = {}) {
|
|
51
|
+
if (options.preview) {
|
|
52
|
+
return annuaireGetBySiren(siren);
|
|
53
|
+
}
|
|
54
|
+
// Full dossier
|
|
55
|
+
return annuaireGetFullDossier(siren);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get subsidiaries — not available from Annuaire Entreprises.
|
|
60
|
+
* Returns empty with a descriptive note.
|
|
61
|
+
* @param {string} parentName
|
|
62
|
+
* @param {string} parentSiren
|
|
63
|
+
* @param {object} options
|
|
64
|
+
* @returns {Promise<{ subsidiaries: Array, error: string|null }>}
|
|
65
|
+
*/
|
|
66
|
+
async getSubsidiaries(parentName, parentSiren, options = {}) {
|
|
67
|
+
return {
|
|
68
|
+
subsidiaries: [],
|
|
69
|
+
error: 'La recherche de filiales n\'est pas disponible via l\'Annuaire Entreprises. Utilisez Pappers pour cette fonctionnalité.',
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Quick lookup for competitor tracker (name → basic company info).
|
|
75
|
+
* @param {string} companyName
|
|
76
|
+
* @returns {Promise<object|null>}
|
|
77
|
+
*/
|
|
78
|
+
async lookup(companyName) {
|
|
79
|
+
return annuaireLookup(companyName);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export default annuaireEntreprisesProvider;
|
package/src/providers/index.js
CHANGED
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
import { registerProvider } from './registry.js';
|
|
12
12
|
import pappersProvider from './pappers.js';
|
|
13
|
+
import annuaireEntreprisesProvider from './annuaire-entreprises.js';
|
|
13
14
|
import opencorporatesProvider from './opencorporates.js';
|
|
14
15
|
import clearbitProvider from './clearbit.js';
|
|
15
16
|
import apolloProvider from './apollo.js';
|
|
16
17
|
|
|
17
18
|
registerProvider('pappers', pappersProvider);
|
|
19
|
+
registerProvider('annuaire-entreprises', annuaireEntreprisesProvider);
|
|
18
20
|
registerProvider('opencorporates', opencorporatesProvider);
|
|
19
21
|
registerProvider('clearbit', clearbitProvider);
|
|
20
22
|
registerProvider('apollo', apolloProvider);
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { isPro, requirePro, getLimits, gatePro } from '../license.js';
|
|
21
21
|
|
|
22
22
|
// Providers that require Pro license for any API call
|
|
23
|
+
// Note: 'pappers' is Pro-only, but 'annuaire-entreprises' is free — used as FR fallback
|
|
23
24
|
const PRO_ONLY_PROVIDERS = new Set(['pappers', 'apollo', 'clearbit']);
|
|
24
25
|
|
|
25
26
|
// ── TLD → Country mapping ────────────────────────────────────────────────────
|
|
@@ -65,6 +66,9 @@ const PROVIDER_MAP = {
|
|
|
65
66
|
// All others → apollo for enrichment (extensible: add 'GB': 'companieshouse', etc.)
|
|
66
67
|
};
|
|
67
68
|
|
|
69
|
+
// France-specific fallback chain: Pappers (deep, paid) → Annuaire Entreprises (basic, free)
|
|
70
|
+
const FR_FALLBACK_CHAIN = ['pappers', 'annuaire-entreprises'];
|
|
71
|
+
|
|
68
72
|
// Fallback chain for international domains (tried in order, first available wins)
|
|
69
73
|
const INTL_FALLBACK_CHAIN = ['apollo', 'clearbit', 'opencorporates'];
|
|
70
74
|
|
|
@@ -176,46 +180,93 @@ async function attemptFranceHandoff(intlProfileData, options = {}) {
|
|
|
176
180
|
|
|
177
181
|
// French company detected on international TLD — get deep data from Pappers
|
|
178
182
|
const pappersProvider = providers['pappers'];
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
if (pappersProvider && pappersProvider.isAvailable()) {
|
|
184
|
+
const companyName = extractCompanyName(intlProfileData);
|
|
185
|
+
if (!companyName) {
|
|
186
|
+
return {
|
|
187
|
+
data: { ...intlProfileData, _handoff: 'no_company_name' },
|
|
188
|
+
handoff: false,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
// Search Pappers by company name to find the SIREN
|
|
194
|
+
const searchResult = await pappersProvider.search(companyName, { count: 1 });
|
|
195
|
+
const topResult = searchResult?.results?.[0];
|
|
196
|
+
|
|
197
|
+
if (!topResult?.siren) {
|
|
198
|
+
return {
|
|
199
|
+
data: { ...intlProfileData, _handoff: 'pappers_no_match' },
|
|
200
|
+
handoff: false,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Get full Pappers profile by SIREN
|
|
205
|
+
const isPreview = options.preview || false;
|
|
206
|
+
const pappersProfile = isPreview
|
|
207
|
+
? await pappersProvider.getProfile(topResult.siren, { preview: true })
|
|
208
|
+
: await pappersProvider.getProfile(topResult.siren, { preview: false });
|
|
209
|
+
|
|
210
|
+
// Check for 401 → try Annuaire Entreprises fallback
|
|
211
|
+
if (pappersProfile.error && /401|unauthorized|forbidden|key/i.test(pappersProfile.error)) {
|
|
212
|
+
return await attemptAnnuaireFallback(intlProfileData, companyName);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const pappersData = pappersProfile?.data;
|
|
216
|
+
const merged = mergeWithPappers(intlProfileData, pappersData);
|
|
217
|
+
return { data: merged, handoff: true };
|
|
218
|
+
} catch {
|
|
219
|
+
return await attemptAnnuaireFallback(intlProfileData, extractCompanyName(intlProfileData));
|
|
220
|
+
}
|
|
184
221
|
}
|
|
185
222
|
|
|
186
|
-
|
|
187
|
-
|
|
223
|
+
// Pappers unavailable → try Annuaire Entreprises fallback
|
|
224
|
+
return await attemptAnnuaireFallback(intlProfileData, extractCompanyName(intlProfileData));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Attempt France handoff via Annuaire Entreprises (free fallback).
|
|
229
|
+
* @param {object} intlProfileData
|
|
230
|
+
* @param {string|null} companyName
|
|
231
|
+
* @returns {Promise<{ data: object, handoff: boolean }>}
|
|
232
|
+
*/
|
|
233
|
+
async function attemptAnnuaireFallback(intlProfileData, companyName) {
|
|
234
|
+
const annuaireProvider = providers['annuaire-entreprises'];
|
|
235
|
+
if (!annuaireProvider || !companyName) {
|
|
188
236
|
return {
|
|
189
|
-
data: { ...intlProfileData, _handoff: 'no_company_name' },
|
|
237
|
+
data: { ...intlProfileData, _handoff: companyName ? 'annuaire_unavailable' : 'no_company_name' },
|
|
190
238
|
handoff: false,
|
|
191
239
|
};
|
|
192
240
|
}
|
|
193
241
|
|
|
194
242
|
try {
|
|
195
|
-
|
|
196
|
-
const searchResult = await pappersProvider.search(companyName, { count: 1 });
|
|
243
|
+
const searchResult = await annuaireProvider.search(companyName, { count: 1 });
|
|
197
244
|
const topResult = searchResult?.results?.[0];
|
|
198
245
|
|
|
199
246
|
if (!topResult?.siren) {
|
|
200
247
|
return {
|
|
201
|
-
data: { ...intlProfileData, _handoff: '
|
|
248
|
+
data: { ...intlProfileData, _handoff: 'annuaire_no_match' },
|
|
202
249
|
handoff: false,
|
|
203
250
|
};
|
|
204
251
|
}
|
|
205
252
|
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
const pappersProfile = isPreview
|
|
209
|
-
? await pappersProvider.getProfile(topResult.siren, { preview: true })
|
|
210
|
-
: await pappersProvider.getProfile(topResult.siren, { preview: false });
|
|
253
|
+
const annuaireProfile = await annuaireProvider.getProfile(topResult.siren, { preview: true });
|
|
254
|
+
const annuaireData = annuaireProfile?.data;
|
|
211
255
|
|
|
212
|
-
|
|
213
|
-
|
|
256
|
+
if (!annuaireData) {
|
|
257
|
+
return {
|
|
258
|
+
data: { ...intlProfileData, _handoff: 'annuaire_no_data' },
|
|
259
|
+
handoff: false,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
214
262
|
|
|
263
|
+
const merged = mergeWithPappers(intlProfileData, annuaireData);
|
|
264
|
+
merged.source = 'annuaire-entreprises+' + (intlProfileData.source || 'international');
|
|
265
|
+
merged._handoff = 'annuaire_fallback';
|
|
215
266
|
return { data: merged, handoff: true };
|
|
216
267
|
} catch {
|
|
217
268
|
return {
|
|
218
|
-
data: { ...intlProfileData, _handoff: '
|
|
269
|
+
data: { ...intlProfileData, _handoff: 'annuaire_error' },
|
|
219
270
|
handoff: false,
|
|
220
271
|
};
|
|
221
272
|
}
|
|
@@ -259,13 +310,28 @@ export function detectCountry(domainOrUrl) {
|
|
|
259
310
|
|
|
260
311
|
/**
|
|
261
312
|
* Get the best provider for a domain/country.
|
|
313
|
+
* For France: tries Pappers first, falls back to Annuaire Entreprises (free).
|
|
314
|
+
* For international: uses the INTL_FALLBACK_CHAIN.
|
|
262
315
|
* @param {string} domainOrUrl
|
|
263
316
|
* @returns {{ provider: object|null, providerName: string, country: string }}
|
|
264
317
|
*/
|
|
265
318
|
export function resolveProvider(domainOrUrl) {
|
|
266
319
|
const country = detectCountry(domainOrUrl);
|
|
267
|
-
const mapped = PROVIDER_MAP[country];
|
|
268
320
|
|
|
321
|
+
// France: try Pappers first, fallback to Annuaire Entreprises
|
|
322
|
+
if (country === 'FR') {
|
|
323
|
+
for (const name of FR_FALLBACK_CHAIN) {
|
|
324
|
+
const p = providers[name];
|
|
325
|
+
if (p && p.isAvailable()) {
|
|
326
|
+
return { provider: p, providerName: name, country };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Last resort: annuaire-entreprises is always available, but be explicit
|
|
330
|
+
const fallback = providers['annuaire-entreprises'];
|
|
331
|
+
return { provider: fallback || null, providerName: 'annuaire-entreprises', country };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const mapped = PROVIDER_MAP[country];
|
|
269
335
|
if (mapped && providers[mapped]) {
|
|
270
336
|
return { provider: providers[mapped], providerName: mapped, country };
|
|
271
337
|
}
|
|
@@ -291,33 +357,52 @@ export function resolveProvider(domainOrUrl) {
|
|
|
291
357
|
* @param {object} options — { count, preview }
|
|
292
358
|
*/
|
|
293
359
|
export async function searchCompany(query, domainOrUrl, options = {}) {
|
|
294
|
-
// ── SIREN/SIRET direct routing → Pappers
|
|
360
|
+
// ── SIREN/SIRET direct routing → France (Pappers → Annuaire Entreprises fallback) ──
|
|
295
361
|
if (isSirenOrSiret(query)) {
|
|
296
|
-
const pappersP = providers['pappers'];
|
|
297
|
-
const providerName = 'pappers';
|
|
298
362
|
const country = 'FR';
|
|
299
363
|
|
|
300
|
-
if
|
|
364
|
+
// Try Pappers first (if Pro + key available)
|
|
365
|
+
const pappersP = providers['pappers'];
|
|
366
|
+
if (isPro() && pappersP && pappersP.isAvailable()) {
|
|
367
|
+
const results = await pappersP.search(query, options);
|
|
368
|
+
// Check for 401 or auth errors → fallback
|
|
369
|
+
if (results.error && /401|unauthorized|forbidden|key/i.test(results.error)) {
|
|
370
|
+
// Pappers 401 — fall through to Annuaire Entreprises
|
|
371
|
+
} else {
|
|
372
|
+
return { ...results, provider: 'pappers', country, _routing: 'siren_direct' };
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Fallback: Annuaire Entreprises (free, always available)
|
|
377
|
+
const annuaireP = providers['annuaire-entreprises'];
|
|
378
|
+
if (annuaireP) {
|
|
379
|
+
const results = await annuaireP.search(query, options);
|
|
301
380
|
return {
|
|
302
|
-
results
|
|
303
|
-
provider:
|
|
381
|
+
...results,
|
|
382
|
+
provider: 'annuaire-entreprises',
|
|
304
383
|
country,
|
|
305
|
-
|
|
306
|
-
|
|
384
|
+
_routing: 'siren_direct_fallback',
|
|
385
|
+
_fallbackNote: 'Pappers non disponible, données issues de l\'Annuaire Entreprises (data.gouv.fr)',
|
|
307
386
|
};
|
|
308
387
|
}
|
|
309
388
|
|
|
310
|
-
|
|
389
|
+
// Neither provider available
|
|
390
|
+
if (!isPro()) {
|
|
311
391
|
return {
|
|
312
392
|
results: [],
|
|
313
|
-
provider:
|
|
393
|
+
provider: 'pappers',
|
|
314
394
|
country,
|
|
315
|
-
error:
|
|
395
|
+
error: `Business Data (pappers) requires an Intelwatch Pro license. Annuaire Entreprises fallback not registered.`,
|
|
396
|
+
licenseRequired: true,
|
|
316
397
|
};
|
|
317
398
|
}
|
|
318
399
|
|
|
319
|
-
|
|
320
|
-
|
|
400
|
+
return {
|
|
401
|
+
results: [],
|
|
402
|
+
provider: 'pappers',
|
|
403
|
+
country,
|
|
404
|
+
error: `No France provider available. Pappers API key not configured and Annuaire Entreprises not registered.`,
|
|
405
|
+
};
|
|
321
406
|
}
|
|
322
407
|
|
|
323
408
|
const { provider, providerName, country } = resolveProvider(domainOrUrl);
|
|
@@ -363,45 +448,66 @@ export async function searchCompany(query, domainOrUrl, options = {}) {
|
|
|
363
448
|
* @param {object} options — { preview }
|
|
364
449
|
*/
|
|
365
450
|
export async function getCompanyProfile(identifier, domainOrUrl, options = {}) {
|
|
366
|
-
// ── SIREN/SIRET direct routing → Pappers
|
|
451
|
+
// ── SIREN/SIRET direct routing → France (Pappers → Annuaire Entreprises fallback) ──
|
|
367
452
|
if (isSirenOrSiret(identifier)) {
|
|
368
|
-
const pappersP = providers['pappers'];
|
|
369
|
-
const providerName = 'pappers';
|
|
370
453
|
const country = 'FR';
|
|
371
454
|
const tier = isPro() ? 'pro' : 'free';
|
|
372
455
|
const isPreview = options.preview || !isPro();
|
|
373
456
|
|
|
374
|
-
if
|
|
457
|
+
// Try Pappers first (if Pro + key available)
|
|
458
|
+
const pappersP = providers['pappers'];
|
|
459
|
+
if (isPro() && pappersP && pappersP.isAvailable()) {
|
|
460
|
+
const profile = await pappersP.getProfile(identifier, { ...options, preview: isPreview });
|
|
461
|
+
// Check for 401 or auth errors → fallback
|
|
462
|
+
if (profile.error && /401|unauthorized|forbidden|key/i.test(profile.error)) {
|
|
463
|
+
// Pappers 401 — fall through to Annuaire Entreprises
|
|
464
|
+
} else {
|
|
465
|
+
return {
|
|
466
|
+
...profile,
|
|
467
|
+
provider: 'pappers',
|
|
468
|
+
country,
|
|
469
|
+
tier,
|
|
470
|
+
isPreview,
|
|
471
|
+
_routing: 'siren_direct',
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Fallback: Annuaire Entreprises (free, no license required)
|
|
477
|
+
const annuaireP = providers['annuaire-entreprises'];
|
|
478
|
+
if (annuaireP) {
|
|
479
|
+
const profile = await annuaireP.getProfile(identifier, { ...options, preview: true });
|
|
375
480
|
return {
|
|
376
|
-
|
|
377
|
-
provider:
|
|
481
|
+
...profile,
|
|
482
|
+
provider: 'annuaire-entreprises',
|
|
378
483
|
country,
|
|
379
|
-
tier,
|
|
484
|
+
tier: 'free',
|
|
380
485
|
isPreview: true,
|
|
381
|
-
|
|
382
|
-
|
|
486
|
+
_routing: 'siren_direct_fallback',
|
|
487
|
+
_fallbackNote: profile.data?._fallbackNote || 'Pappers non disponible, profil issu de l\'Annuaire Entreprises (data.gouv.fr). Données financières limitées (CA, résultat net). UBO, BODACC et mandats croisés non disponibles.',
|
|
383
488
|
};
|
|
384
489
|
}
|
|
385
490
|
|
|
386
|
-
|
|
491
|
+
// Neither provider available
|
|
492
|
+
if (!isPro()) {
|
|
387
493
|
return {
|
|
388
494
|
data: null,
|
|
389
|
-
provider:
|
|
495
|
+
provider: 'pappers',
|
|
390
496
|
country,
|
|
391
497
|
tier,
|
|
392
|
-
isPreview,
|
|
393
|
-
error:
|
|
498
|
+
isPreview: true,
|
|
499
|
+
error: `Business Data (pappers) requires an Intelwatch Pro license. Annuaire Entreprises fallback not registered.`,
|
|
500
|
+
licenseRequired: true,
|
|
394
501
|
};
|
|
395
502
|
}
|
|
396
503
|
|
|
397
|
-
const profile = await pappersP.getProfile(identifier, { ...options, preview: isPreview });
|
|
398
504
|
return {
|
|
399
|
-
|
|
400
|
-
provider:
|
|
505
|
+
data: null,
|
|
506
|
+
provider: 'pappers',
|
|
401
507
|
country,
|
|
402
508
|
tier,
|
|
403
509
|
isPreview,
|
|
404
|
-
|
|
510
|
+
error: `No France provider available. Pappers API key not configured and Annuaire Entreprises not registered.`,
|
|
405
511
|
};
|
|
406
512
|
}
|
|
407
513
|
|