intelwatch 1.1.0 → 1.1.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/CHANGELOG.md +21 -0
- package/ROADMAP-PREMIUM.md +100 -0
- package/package.json +2 -1
- package/src/commands/profile.js +1037 -32
- package/src/index.js +3 -1
- package/src/scrapers/brave-search.js +15 -2
- package/src/scrapers/pappers.js +231 -13
package/src/index.js
CHANGED
|
@@ -159,9 +159,11 @@ program
|
|
|
159
159
|
|
|
160
160
|
program
|
|
161
161
|
.command('profile <siren-or-name>')
|
|
162
|
-
.description('
|
|
162
|
+
.description('Deep company profile — due diligence report (requires Pro license)')
|
|
163
163
|
.option('--preview', 'Run limited preview: company identity + last year financials only')
|
|
164
164
|
.option('--ai', 'Generate an AI-powered due diligence summary (requires AI API key)')
|
|
165
|
+
.option('--format <type>', 'Output format: terminal (default) or pdf')
|
|
166
|
+
.option('--output <path>', 'Output file path for PDF')
|
|
165
167
|
.action(async (sirenOrName, options) => {
|
|
166
168
|
await runMA(sirenOrName, options);
|
|
167
169
|
});
|
|
@@ -148,11 +148,24 @@ export async function searchPressMentions(brandName, options = {}) {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// ── Relevance filter: drop results that don't actually mention the brand ──
|
|
152
|
+
const brandLower = brandName.toLowerCase().trim();
|
|
153
|
+
const brandWords = brandLower.split(/\s+/);
|
|
154
|
+
const filtered = mentions.filter(m => {
|
|
155
|
+
const text = ((m.title || '') + ' ' + (m.snippet || '') + ' ' + (m.domain || '')).toLowerCase();
|
|
156
|
+
// Must contain the exact brand name OR all words of the brand
|
|
157
|
+
if (text.includes(brandLower)) return true;
|
|
158
|
+
if (brandWords.length > 1 && brandWords.every(w => text.includes(w))) return true;
|
|
159
|
+
// Fuzzy: allow 1 char difference for short names (e.g. "Endrix" vs "Endrick" should be EXCLUDED)
|
|
160
|
+
return false;
|
|
161
|
+
});
|
|
162
|
+
|
|
151
163
|
return {
|
|
152
164
|
brandName,
|
|
153
165
|
checkedAt: new Date().toISOString(),
|
|
154
|
-
mentions,
|
|
155
|
-
mentionCount:
|
|
166
|
+
mentions: filtered,
|
|
167
|
+
mentionCount: filtered.length,
|
|
168
|
+
unfilteredCount: mentions.length,
|
|
156
169
|
error: news.error || web.error || null,
|
|
157
170
|
};
|
|
158
171
|
}
|
package/src/scrapers/pappers.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
2
5
|
|
|
3
6
|
const PAPPERS_API = 'https://api.pappers.fr/v1';
|
|
4
7
|
const PAPPERS_API_V2 = 'https://api.pappers.fr/v2';
|
|
@@ -27,7 +30,7 @@ export async function pappersSearchByName(name, options = {}) {
|
|
|
27
30
|
},
|
|
28
31
|
timeout: 10000,
|
|
29
32
|
});
|
|
30
|
-
return { results: resp.data.resultats || [], error: null };
|
|
33
|
+
return { results: resp.data.resultats || resp.data.entreprises || [], error: null };
|
|
31
34
|
} catch (err) {
|
|
32
35
|
return { results: [], error: err.message };
|
|
33
36
|
}
|
|
@@ -78,6 +81,10 @@ export async function pappersLookup(companyName) {
|
|
|
78
81
|
* and collective procedures.
|
|
79
82
|
*/
|
|
80
83
|
export async function pappersGetFullDossier(siren) {
|
|
84
|
+
// Check cache first
|
|
85
|
+
const cached = getCached(siren);
|
|
86
|
+
if (cached) return { data: cached, error: null, fromCache: true };
|
|
87
|
+
|
|
81
88
|
const apiKey = getApiKey();
|
|
82
89
|
if (!apiKey) return { data: null, error: 'No PAPPERS_API_KEY set' };
|
|
83
90
|
|
|
@@ -96,6 +103,17 @@ export async function pappersGetFullDossier(siren) {
|
|
|
96
103
|
resultat: f.resultat ?? null,
|
|
97
104
|
capitauxPropres: f.capitaux_propres ?? null,
|
|
98
105
|
effectif: f.effectif ?? null,
|
|
106
|
+
ebitda: f.excedent_brut_exploitation ?? null,
|
|
107
|
+
margeEbitda: f.taux_marge_EBITDA ?? null,
|
|
108
|
+
dettesFinancieres: f.dettes_financieres ?? null,
|
|
109
|
+
tresorerie: f.tresorerie ?? null,
|
|
110
|
+
fondsPropres: f.fonds_propres ?? null,
|
|
111
|
+
bfr: f.BFR ?? null,
|
|
112
|
+
ratioEndettement: f.ratio_endettement ?? null,
|
|
113
|
+
autonomieFinanciere: f.autonomie_financiere ?? null,
|
|
114
|
+
rentabiliteFP: f.rentabilite_fonds_propres ?? null,
|
|
115
|
+
margeNette: f.marge_nette ?? null,
|
|
116
|
+
capaciteAutofinancement: f.capacite_autofinancement ?? null,
|
|
99
117
|
}));
|
|
100
118
|
|
|
101
119
|
// UBO — bénéficiaires effectifs
|
|
@@ -108,14 +126,18 @@ export async function pappersGetFullDossier(siren) {
|
|
|
108
126
|
pourcentageVotes: b.pourcentage_votes ?? null,
|
|
109
127
|
}));
|
|
110
128
|
|
|
111
|
-
// BODACC publications — last
|
|
112
|
-
const bodacc = (d.publications_bodacc || []).slice(0,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
129
|
+
// BODACC publications — last 50 (captures M&A activity)
|
|
130
|
+
const bodacc = (d.publications_bodacc || []).slice(0, 50).map(p => {
|
|
131
|
+
const actes = (p.acte?.actes_publies || []).map(a => a.type_acte).filter(Boolean);
|
|
132
|
+
return {
|
|
133
|
+
date: p.date,
|
|
134
|
+
type: p.type,
|
|
135
|
+
tribunal: p.tribunal || null,
|
|
136
|
+
numero: p.numero_annonce || null,
|
|
137
|
+
description: actes.length ? actes.join(', ') : p.type || null,
|
|
138
|
+
details: p.acte?.descriptif || p.acte?.capital || null,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
119
141
|
|
|
120
142
|
// Dirigeants with their mandats in other companies
|
|
121
143
|
const dirigeants = (d.dirigeants || []).map(dir => ({
|
|
@@ -161,10 +183,53 @@ export async function pappersGetFullDossier(siren) {
|
|
|
161
183
|
dateRadiation: d.date_radiation || null,
|
|
162
184
|
};
|
|
163
185
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
186
|
+
// Consolidated financials (group level)
|
|
187
|
+
const consolidatedFinances = (d.finances_consolidees || []).slice(0, 5).map(f => ({
|
|
188
|
+
annee: f.annee,
|
|
189
|
+
ca: f.chiffre_affaires ?? null,
|
|
190
|
+
resultat: f.resultat ?? null,
|
|
191
|
+
capitauxPropres: f.capitaux_propres ?? null,
|
|
192
|
+
effectif: f.effectif ?? null,
|
|
193
|
+
ebitda: f.excedent_brut_exploitation ?? null,
|
|
194
|
+
margeEbitda: f.taux_marge_EBITDA ?? null,
|
|
195
|
+
dettesFinancieres: f.dettes_financieres ?? null,
|
|
196
|
+
tresorerie: f.tresorerie ?? null,
|
|
197
|
+
fondsPropres: f.fonds_propres ?? null,
|
|
198
|
+
bfr: f.BFR ?? null,
|
|
199
|
+
ratioEndettement: f.ratio_endettement ?? null,
|
|
200
|
+
autonomieFinanciere: f.autonomie_financiere ?? null,
|
|
201
|
+
rentabiliteFP: f.rentabilite_fonds_propres ?? null,
|
|
202
|
+
margeNette: f.marge_nette ?? null,
|
|
203
|
+
capaciteAutofinancement: f.capacite_autofinancement ?? null,
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
// Representants (dirigeants + corporate entities with mandats)
|
|
207
|
+
const representants = (d.representants || []).map(r => ({
|
|
208
|
+
nom: r.nom_complet || r.denomination || [r.prenom, r.nom].filter(Boolean).join(' ') || '?',
|
|
209
|
+
qualite: r.qualite || '',
|
|
210
|
+
siren: r.siren || null,
|
|
211
|
+
personneMorale: !!r.personne_morale,
|
|
212
|
+
}));
|
|
213
|
+
|
|
214
|
+
// Etablissements
|
|
215
|
+
const etablissements = (d.etablissements || []).map(e => ({
|
|
216
|
+
siret: e.siret,
|
|
217
|
+
type: e.type_etablissement,
|
|
218
|
+
adresse: [e.adresse_ligne_1, e.code_postal, e.ville].filter(Boolean).join(' '),
|
|
219
|
+
actif: !e.etablissement_cesse,
|
|
220
|
+
}));
|
|
221
|
+
|
|
222
|
+
// Extra fields
|
|
223
|
+
identity.objetSocial = d.objet_social || null;
|
|
224
|
+
identity.tvaIntra = d.numero_tva_intracommunautaire || null;
|
|
225
|
+
identity.rcs = d.numero_rcs || null;
|
|
226
|
+
identity.greffe = d.greffe || null;
|
|
227
|
+
identity.conventionCollective = d.conventions_collectives?.[0]?.nom || null;
|
|
228
|
+
identity.effectifTexte = d.effectif || null;
|
|
229
|
+
|
|
230
|
+
const result = { identity, financialHistory, consolidatedFinances, ubo, bodacc, dirigeants, representants, etablissements, proceduresCollectives };
|
|
231
|
+
setCache(siren, result);
|
|
232
|
+
return { data: result, error: null };
|
|
168
233
|
} catch (err) {
|
|
169
234
|
return { data: null, error: err.message };
|
|
170
235
|
}
|
|
@@ -216,3 +281,156 @@ function formatPappersDetail(d) {
|
|
|
216
281
|
formeJuridique: d.forme_juridique || null,
|
|
217
282
|
};
|
|
218
283
|
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Search for subsidiaries/related entities.
|
|
287
|
+
* Strategy 1: recherche-dirigeants — finds companies where the parent acts as corporate director.
|
|
288
|
+
* Strategy 2: name-search fallback.
|
|
289
|
+
* Returns array of entities with their latest financials, sorted by CA desc.
|
|
290
|
+
*/
|
|
291
|
+
export async function pappersSearchSubsidiaries(parentName, parentSiren) {
|
|
292
|
+
const apiKey = getApiKey();
|
|
293
|
+
if (!apiKey) return { subsidiaries: [], error: 'No PAPPERS_API_KEY set' };
|
|
294
|
+
|
|
295
|
+
// Check cache for subsidiary search results
|
|
296
|
+
const subsCacheKey = `subs_${parentSiren}`;
|
|
297
|
+
const cachedSubs = getCached(subsCacheKey);
|
|
298
|
+
if (cachedSubs?.subsidiaries) {
|
|
299
|
+
return { subsidiaries: cachedSubs.subsidiaries, total: cachedSubs.total || cachedSubs.subsidiaries.length, error: null, fromCache: true };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const searchName = parentName.replace(/\s*(GRP|SAS|SARL|SA|SCI|EURL|GROUP|GROUPE|HOLDING|SNC|SASU)\s*/gi, ' ').trim();
|
|
303
|
+
const nameNorm = searchName.toLowerCase();
|
|
304
|
+
|
|
305
|
+
// ── Strategy 1: recherche-dirigeants ──────────────────────────────────────
|
|
306
|
+
// Finds companies where an entity named like the parent is listed as dirigeant
|
|
307
|
+
try {
|
|
308
|
+
const resp = await axios.get(`${PAPPERS_API}/recherche-dirigeants`, {
|
|
309
|
+
params: { api_token: apiKey, q: searchName, par_page: 20 },
|
|
310
|
+
timeout: 15000,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const resultats = resp.data.resultats || [];
|
|
314
|
+
const subsidiaryMap = new Map();
|
|
315
|
+
|
|
316
|
+
for (const r of resultats) {
|
|
317
|
+
// Only match the PARENT entity as dirigeant (by SIREN if available, else exact name)
|
|
318
|
+
const dirigeantSiren = r.siren || '';
|
|
319
|
+
const dirigeantName = (r.nom_entreprise || r.denomination || r.nom_complet || '').toLowerCase().trim();
|
|
320
|
+
|
|
321
|
+
// Strict filter: must be the parent SIREN, or exact parent name match
|
|
322
|
+
const isParent = (dirigeantSiren === parentSiren) || (dirigeantName === nameNorm) || (dirigeantName === searchName.toLowerCase());
|
|
323
|
+
if (!isParent) continue;
|
|
324
|
+
|
|
325
|
+
for (const e of (r.entreprises || [])) {
|
|
326
|
+
if (e.siren && e.siren !== parentSiren && !subsidiaryMap.has(e.siren)) {
|
|
327
|
+
subsidiaryMap.set(e.siren, e);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (subsidiaryMap.size > 0) {
|
|
333
|
+
// Limit to 30 subsidiaries to save API credits
|
|
334
|
+
const entities = Array.from(subsidiaryMap.values()).slice(0, 30);
|
|
335
|
+
const subsidiaries = await fetchSubsidiaryDetails(apiKey, entities);
|
|
336
|
+
// Cache subsidiary search results
|
|
337
|
+
setCache(subsCacheKey, { subsidiaries, total: subsidiaryMap.size });
|
|
338
|
+
return { subsidiaries, total: subsidiaryMap.size, error: null };
|
|
339
|
+
}
|
|
340
|
+
} catch (_) {
|
|
341
|
+
// Fall through to name-search
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Strategy 2: name-search fallback ──────────────────────────────────────
|
|
345
|
+
try {
|
|
346
|
+
const resp = await axios.get(`${PAPPERS_API}/recherche`, {
|
|
347
|
+
params: { api_token: apiKey, q: searchName, par_page: 20 },
|
|
348
|
+
timeout: 15000,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const entities = (resp.data.entreprises || resp.data.resultats || [])
|
|
352
|
+
.filter(e => e.siren !== parentSiren);
|
|
353
|
+
|
|
354
|
+
const subsidiaries = await fetchSubsidiaryDetails(apiKey, entities);
|
|
355
|
+
// Cache fallback search results too
|
|
356
|
+
setCache(subsCacheKey, { subsidiaries, total: subsidiaries.length });
|
|
357
|
+
return { subsidiaries, error: null };
|
|
358
|
+
} catch (err) {
|
|
359
|
+
return { subsidiaries: [], error: err.message };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function fetchSubsidiaryDetails(apiKey, entities) {
|
|
364
|
+
const subsidiaries = [];
|
|
365
|
+
for (const e of entities) {
|
|
366
|
+
try {
|
|
367
|
+
// Check cache first to save API credits
|
|
368
|
+
const cached = getCached(e.siren);
|
|
369
|
+
let d;
|
|
370
|
+
if (cached?.identity) {
|
|
371
|
+
// Reconstruct from cached full dossier
|
|
372
|
+
d = { nom_entreprise: cached.identity.name, code_naf: cached.identity.nafCode, libelle_code_naf: cached.identity.nafLabel, siege: { ville: cached.identity.ville }, effectif: cached.identity.effectifTexte, entreprise_cessee: false, date_creation: cached.identity.dateCreation, finances: cached.financialHistory?.map(f => ({ chiffre_affaires: f.ca, resultat: f.resultat, annee: f.annee })) || [] };
|
|
373
|
+
} else {
|
|
374
|
+
const det = await axios.get(`${PAPPERS_API}/entreprise`, {
|
|
375
|
+
params: { api_token: apiKey, siren: e.siren },
|
|
376
|
+
timeout: 10000,
|
|
377
|
+
});
|
|
378
|
+
d = det.data;
|
|
379
|
+
// Cache subsidiary data to avoid re-fetching
|
|
380
|
+
setCache(e.siren, {
|
|
381
|
+
identity: { name: d.nom_entreprise, nafCode: d.code_naf, nafLabel: d.libelle_code_naf, ville: d.siege?.ville, effectifTexte: d.effectif, dateCreation: d.date_creation },
|
|
382
|
+
financialHistory: (d.finances || []).map(f => ({ ca: f.chiffre_affaires, resultat: f.resultat, annee: f.annee })),
|
|
383
|
+
_subCache: true,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
const fin = (d.finances || [])[0] || {};
|
|
387
|
+
subsidiaries.push({
|
|
388
|
+
siren: e.siren,
|
|
389
|
+
name: d.nom_entreprise || d.denomination || e.nom_entreprise || '?',
|
|
390
|
+
naf: d.code_naf || '',
|
|
391
|
+
nafLabel: d.libelle_code_naf || '',
|
|
392
|
+
ville: d.siege?.ville || '',
|
|
393
|
+
effectif: d.effectif || '',
|
|
394
|
+
ca: fin.chiffre_affaires ?? null,
|
|
395
|
+
resultat: fin.resultat ?? null,
|
|
396
|
+
annee: fin.annee || null,
|
|
397
|
+
status: d.entreprise_cessee ? 'Cessée' : 'Active',
|
|
398
|
+
dateCreation: d.date_creation || null,
|
|
399
|
+
});
|
|
400
|
+
} catch (_) {
|
|
401
|
+
subsidiaries.push({
|
|
402
|
+
siren: e.siren,
|
|
403
|
+
name: e.nom_entreprise || e.denomination || '?',
|
|
404
|
+
ville: e.siege?.ville || '',
|
|
405
|
+
ca: null, resultat: null, effectif: '', annee: null, status: '?',
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
subsidiaries.sort((a, b) => (b.ca || 0) - (a.ca || 0));
|
|
410
|
+
return subsidiaries;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── Local cache to save API credits ──────────────────────────────────────────
|
|
414
|
+
const CACHE_DIR = join(homedir(), '.intelwatch', 'cache', 'pappers');
|
|
415
|
+
const CACHE_TTL = 7 * 24 * 3600 * 1000; // 7 days
|
|
416
|
+
|
|
417
|
+
function ensureCacheDir() {
|
|
418
|
+
if (!existsSync(CACHE_DIR)) mkdirSync(CACHE_DIR, { recursive: true });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function getCached(siren) {
|
|
422
|
+
try {
|
|
423
|
+
const file = join(CACHE_DIR, `${siren}.json`);
|
|
424
|
+
if (!existsSync(file)) return null;
|
|
425
|
+
const data = JSON.parse(readFileSync(file, 'utf8'));
|
|
426
|
+
if (Date.now() - data._cachedAt > CACHE_TTL) return null;
|
|
427
|
+
return data;
|
|
428
|
+
} catch { return null; }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function setCache(siren, data) {
|
|
432
|
+
try {
|
|
433
|
+
ensureCacheDir();
|
|
434
|
+
writeFileSync(join(CACHE_DIR, `${siren}.json`), JSON.stringify({ ...data, _cachedAt: Date.now() }));
|
|
435
|
+
} catch { /* silent */ }
|
|
436
|
+
}
|