intelwatch 1.3.2 → 1.6.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.
@@ -1,8 +1,331 @@
1
1
  import chalk from 'chalk';
2
+ import Table from 'cli-table3';
2
3
  import { getTracker, loadLatestSnapshot } from '../storage.js';
3
4
  import { createTable, header, section, error, warn } from '../utils/display.js';
5
+ import { isSirenOrSiret } from '../providers/registry.js';
6
+ import { annuaireGetFullDossier } from '../scrapers/annuaire-entreprises.js';
7
+ import { pappersGetFullDossier, hasPappersKey } from '../scrapers/pappers.js';
8
+ import { isPro } from '../license.js';
9
+
10
+ // ── Helpers ──────────────────────────────────────────────────────────────────
11
+
12
+ function formatEuro(val) {
13
+ if (val == null) return null;
14
+ return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(val);
15
+ }
16
+
17
+ function na() {
18
+ return chalk.gray('—');
19
+ }
20
+
21
+ function cellVal(val, formatter) {
22
+ if (val == null) return na();
23
+ return formatter ? formatter(val) : String(val);
24
+ }
25
+
26
+ function resultatCell(val) {
27
+ if (val == null) return na();
28
+ const fmt = formatEuro(val);
29
+ return val >= 0 ? chalk.green(fmt) : chalk.red(fmt);
30
+ }
31
+
32
+ /**
33
+ * Resolve which provider function to use for fetching a full dossier.
34
+ * Mirrors profile.js logic: Pappers (Pro + key) → Annuaire Entreprises (free).
35
+ */
36
+ function resolveDossierFetcher() {
37
+ if (isPro() && hasPappersKey()) {
38
+ return { fetch: pappersGetFullDossier, name: 'pappers' };
39
+ }
40
+ return { fetch: annuaireGetFullDossier, name: 'annuaire-entreprises' };
41
+ }
42
+
43
+ /**
44
+ * Fetch a company dossier with Pappers → Annuaire Entreprises fallback.
45
+ * Returns { data, providerName, fromCache } or throws.
46
+ */
47
+ async function fetchDossier(siren) {
48
+ const { fetch: fetchFn, name: providerName } = resolveDossierFetcher();
49
+ const result = await fetchFn(siren);
50
+
51
+ if (result.error && providerName === 'pappers' && /401|unauthorized|forbidden/i.test(result.error)) {
52
+ // Fallback to Annuaire Entreprises
53
+ const fallback = await annuaireGetFullDossier(siren);
54
+ if (fallback.error || !fallback.data) {
55
+ throw new Error(`Both providers failed: ${result.error} / ${fallback.error}`);
56
+ }
57
+ return { data: fallback.data, providerName: 'annuaire-entreprises', fromCache: fallback.fromCache };
58
+ }
59
+
60
+ if (result.error || !result.data) {
61
+ throw new Error(result.error || `No data returned for SIREN ${siren}`);
62
+ }
63
+
64
+ return { data: result.data, providerName, fromCache: result.fromCache };
65
+ }
66
+
67
+ // ── Company Profile Comparison ────────────────────────────────────────────────
68
+
69
+ /**
70
+ * Compare two FR company profiles side-by-side.
71
+ * @param {string} siren1
72
+ * @param {string} siren2
73
+ */
74
+ export async function runCompareCompanies(siren1, siren2) {
75
+ // Validate SIREN format
76
+ for (const s of [siren1, siren2]) {
77
+ const cleaned = s.trim();
78
+ if (!/^\d{9}(\d{5})?$/.test(cleaned)) {
79
+ error(`Invalid SIREN/SIRET: ${s}. Expected 9 digits (SIREN) or 14 digits (SIRET).`);
80
+ process.exit(1);
81
+ }
82
+ }
83
+
84
+ // Normalize to 9-digit SIREN (strip SIRET establishment digits)
85
+ const s1 = siren1.trim().slice(0, 9);
86
+ const s2 = siren2.trim().slice(0, 9);
87
+
88
+ if (s1 === s2) {
89
+ error('Both SIRENs are identical — nothing to compare.');
90
+ process.exit(1);
91
+ }
92
+
93
+ header(`📊 Company Comparison: ${s1} vs ${s2}`);
94
+
95
+ // ── Fetch both dossiers in parallel ──────────────────────────────────────
96
+ console.log(chalk.gray(' Fetching company profiles...'));
97
+
98
+ const [res1, res2] = await Promise.allSettled([
99
+ fetchDossier(s1),
100
+ fetchDossier(s2),
101
+ ]);
102
+
103
+ if (res1.status === 'rejected') {
104
+ error(`Failed to fetch ${s1}: ${res1.reason.message}`);
105
+ process.exit(1);
106
+ }
107
+ if (res2.status === 'rejected') {
108
+ error(`Failed to fetch ${s2}: ${res2.reason.message}`);
109
+ process.exit(1);
110
+ }
111
+
112
+ const { data: d1, providerName: p1, fromCache: c1 } = res1.value;
113
+ const { data: d2, providerName: p2, fromCache: c2 } = res2.value;
114
+
115
+ // Provider info
116
+ const providerLabel = (name, cached) =>
117
+ name === 'annuaire-entreprises'
118
+ ? chalk.cyan('Annuaire Entreprises (data.gouv.fr)')
119
+ : chalk.magenta('Pappers') + (cached ? chalk.gray(' (cache)') : '');
120
+
121
+ console.log(chalk.gray(` Provider 1: ${p1}${c1 ? ' (cache)' : ''}`));
122
+ console.log(chalk.gray(` Provider 2: ${p2}${c2 ? ' (cache)' : ''}`));
123
+
124
+ // ── Identity comparison table ─────────────────────────────────────────────
125
+ const i1 = d1.identity || {};
126
+ const i2 = d2.identity || {};
127
+ const name1 = i1.name || s1;
128
+ const name2 = i2.name || s2;
129
+
130
+ // Truncate long names for column headers
131
+ const col1 = name1.length > 30 ? name1.slice(0, 27) + '...' : name1;
132
+ const col2 = name2.length > 30 ? name2.slice(0, 27) + '...' : name2;
133
+
134
+ section('\n📋 Identité');
135
+ const identityTable = new Table({
136
+ head: ['Critère', chalk.bold.white(col1), chalk.bold.white(col2)].map(h =>
137
+ typeof h === 'string' && h !== 'Critère' ? h : chalk.cyan.bold(h)
138
+ ),
139
+ style: { head: [], border: ['grey'] },
140
+ colAligns: ['left', 'left', 'left'],
141
+ colWidths: [20, 40, 40],
142
+ });
143
+
144
+ const identityRows = [
145
+ ['Nom', i1.name, i2.name],
146
+ ['SIREN', i1.siren, i2.siren],
147
+ ['NAF', i1.nafCode ? `${i1.nafCode} — ${i1.nafLabel || ''}`.trim() : null,
148
+ i2.nafCode ? `${i2.nafCode} — ${i2.nafLabel || ''}`.trim() : null],
149
+ ['Forme juridique', i1.formeJuridique, i2.formeJuridique],
150
+ ['Date création', i1.dateCreation, i2.dateCreation],
151
+ ['Capital', i1.capital != null ? formatEuro(i1.capital) : null,
152
+ i2.capital != null ? formatEuro(i2.capital) : null],
153
+ ['Effectifs', i1.effectifs, i2.effectifs],
154
+ ['Statut', i1.status, i2.status],
155
+ ['Ville', i1.ville, i2.ville],
156
+ ];
157
+
158
+ for (const [label, v1, v2] of identityRows) {
159
+ // Color-code status
160
+ let c1 = cellVal(v1);
161
+ let c2 = cellVal(v2);
162
+ if (label === 'Statut') {
163
+ c1 = v1 === 'Actif' ? chalk.green(v1) : (v1 ? chalk.red(v1) : na());
164
+ c2 = v2 === 'Actif' ? chalk.green(v2) : (v2 ? chalk.red(v2) : na());
165
+ }
166
+ identityTable.push([chalk.white(label), c1, c2]);
167
+ }
168
+ console.log(identityTable.toString());
169
+
170
+ // ── Financial comparison table ─────────────────────────────────────────────
171
+ const f1 = (d1.financialHistory || []).slice(0, 5);
172
+ const f2 = (d2.financialHistory || []).slice(0, 5);
173
+
174
+ if (f1.length > 0 || f2.length > 0) {
175
+ section('\n💶 Historique financier');
176
+ const finTable = new Table({
177
+ head: ['Année', chalk.bold.white(col1), '', chalk.bold.white(col2), ''].map((h, i) =>
178
+ i === 0 ? chalk.cyan.bold(h) : h
179
+ ),
180
+ style: { head: [], border: ['grey'] },
181
+ colAligns: ['left', 'right', 'right', 'right', 'right'],
182
+ });
183
+
184
+ // Merge years from both companies
185
+ const allYears = [...new Set([
186
+ ...f1.map(f => f.annee),
187
+ ...f2.map(f => f.annee),
188
+ ])].sort((a, b) => b - a);
189
+
190
+ const f1Map = Object.fromEntries(f1.map(f => [f.annee, f]));
191
+ const f2Map = Object.fromEntries(f2.map(f => [f.annee, f]));
192
+
193
+ for (const year of allYears) {
194
+ const a = f1Map[year] || {};
195
+ const b = f2Map[year] || {};
196
+
197
+ finTable.push([
198
+ chalk.white(year),
199
+ a.ca != null ? chalk.white(formatEuro(a.ca)) : na(),
200
+ a.resultat != null ? resultatCell(a.resultat) : na(),
201
+ b.ca != null ? chalk.white(formatEuro(b.ca)) : na(),
202
+ b.resultat != null ? resultatCell(b.resultat) : na(),
203
+ ]);
204
+ }
205
+
206
+ console.log(finTable.toString());
207
+ console.log(chalk.gray(' (CA | Résultat net) par année — vert = positif, rouge = négatif'));
208
+ }
209
+
210
+ // ── Latest financials snapshot (clean side-by-side) ────────────────────────
211
+ const last1 = f1[0] || {};
212
+ const last2 = f2[0] || {};
213
+
214
+ section('\n💶 Dernier exercice');
215
+ const lastFinTable = new Table({
216
+ head: ['Indicateur', chalk.bold.white(col1), chalk.bold.white(col2)].map(h =>
217
+ typeof h === 'string' && !['Chiffre d\'affaires', 'Résultat net', 'Capitaux propres', 'Indicateur'].includes(h)
218
+ ? h : chalk.cyan.bold(h)
219
+ ),
220
+ style: { head: [], border: ['grey'] },
221
+ colAligns: ['left', 'right', 'right'],
222
+ });
223
+
224
+ lastFinTable.push([
225
+ chalk.white('Année'),
226
+ cellVal(last1.annee),
227
+ cellVal(last2.annee),
228
+ ]);
229
+ lastFinTable.push([
230
+ chalk.white('Chiffre d\'affaires'),
231
+ cellVal(last1.ca, formatEuro),
232
+ cellVal(last2.ca, formatEuro),
233
+ ]);
234
+ lastFinTable.push([
235
+ chalk.white('Résultat net'),
236
+ resultatCell(last1.resultat),
237
+ resultatCell(last2.resultat),
238
+ ]);
239
+ lastFinTable.push([
240
+ chalk.white('Capitaux propres'),
241
+ last1.capitauxPropres != null ? (last1.capitauxPropres >= 0 ? chalk.white(formatEuro(last1.capitauxPropres)) : chalk.red(formatEuro(last1.capitauxPropres))) : na(),
242
+ last2.capitauxPropres != null ? (last2.capitauxPropres >= 0 ? chalk.white(formatEuro(last2.capitauxPropres)) : chalk.red(formatEuro(last2.capitauxPropres))) : na(),
243
+ ]);
244
+ console.log(lastFinTable.toString());
245
+
246
+ // ── Dirigeants comparison ─────────────────────────────────────────────────
247
+ const dir1 = d1.dirigeants || [];
248
+ const dir2 = d2.dirigeants || [];
249
+
250
+ section('\n👔 Dirigeants');
251
+ const dirTable = new Table({
252
+ head: [chalk.cyan.bold('Rôle'), chalk.bold.white(col1), chalk.bold.white(col2)],
253
+ style: { head: [], border: ['grey'] },
254
+ colAligns: ['left', 'left', 'left'],
255
+ colWidths: [20, 40, 40],
256
+ });
257
+
258
+ // Show all dirigeants with their roles
259
+ const maxDir = Math.max(dir1.length, dir2.length, 1);
260
+ for (let i = 0; i < maxDir; i++) {
261
+ const d1p = dir1[i] || {};
262
+ const d2p = dir2[i] || {};
263
+ const role1 = d1p.role || '';
264
+ const role2 = d2p.role || '';
265
+ const name1str = d1p.nom ? [d1p.prenom, d1p.nom].filter(Boolean).join(' ') : '';
266
+ const name2str = d2p.nom ? [d2p.prenom, d2p.nom].filter(Boolean).join(' ') : '';
267
+
268
+ if (!name1str && !name2str) continue;
269
+
270
+ dirTable.push([
271
+ chalk.white(role1 || role2 || `Dirigeant ${i + 1}`),
272
+ name1str ? chalk.white(name1str) : na(),
273
+ name2str ? chalk.white(name2str) : na(),
274
+ ]);
275
+ }
276
+
277
+ if (dir1.length === 0 && dir2.length === 0) {
278
+ console.log(chalk.gray(' Aucun dirigeant connu pour les deux entreprises.'));
279
+ } else {
280
+ console.log(dirTable.toString());
281
+ }
282
+
283
+ // ── Delta indicators ───────────────────────────────────────────────────────
284
+ if (last1.ca != null && last2.ca != null && last1.ca !== 0 && last2.ca !== 0) {
285
+ section('\n📈 Indicateurs différentiels');
286
+ const ratio = (last1.ca / last2.ca);
287
+ const leader = last1.ca >= last2.ca ? name1 : name2;
288
+ const ratioStr = ratio >= 1
289
+ ? `${(ratio).toFixed(2)}x plus grand`
290
+ : `${(1/ratio).toFixed(2)}x plus petit`;
291
+
292
+ console.log(chalk.white(` CA: ${leader} est ${ratioStr} en chiffre d'affaires.`));
293
+
294
+ if (last1.resultat != null && last2.resultat != null) {
295
+ const marge1 = last1.ca !== 0 ? ((last1.resultat / last1.ca) * 100).toFixed(1) : null;
296
+ const marge2 = last2.ca !== 0 ? ((last2.resultat / last2.ca) * 100).toFixed(1) : null;
297
+ if (marge1 && marge2) {
298
+ const m1Color = parseFloat(marge1) >= 0 ? chalk.green : chalk.red;
299
+ const m2Color = parseFloat(marge2) >= 0 ? chalk.green : chalk.red;
300
+ console.log(chalk.white(` Marge nette: ${name1} ${m1Color(marge1 + '%')} | ${name2} ${m2Color(marge2 + '%')}`));
301
+ }
302
+ }
303
+ }
304
+
305
+ // ── Footer ────────────────────────────────────────────────────────────────
306
+ console.log('');
307
+ const providerNote = p1 === 'annuaire-entreprises' || p2 === 'annuaire-entreprises'
308
+ ? chalk.gray(' Données issues de l\'Annuaire Entreprises (data.gouv.fr) — gratuites mais limitées.\n Configurez PAPPERS_API_KEY pour des données complètes (UBO, BODACC, mandats).')
309
+ : '';
310
+ if (providerNote) console.log(providerNote);
311
+ console.log('');
312
+ }
313
+
314
+ // ── Tracker Comparison (original) ────────────────────────────────────────────
4
315
 
5
316
  export function runCompare(id1, id2) {
317
+ // Detect SIREN/SIRET arguments → company comparison
318
+ if (isSirenOrSiret(id1) && isSirenOrSiret(id2)) {
319
+ return runCompareCompanies(id1, id2);
320
+ }
321
+
322
+ // If one is SIREN and the other is not → error
323
+ if (isSirenOrSiret(id1) || isSirenOrSiret(id2)) {
324
+ error('Both arguments must be SIREN/SIRET for company comparison, or tracker IDs for web tracker comparison.');
325
+ process.exit(1);
326
+ }
327
+
328
+ // ── Original tracker comparison ──────────────────────────────────────────
6
329
  const tracker1 = getTracker(id1);
7
330
  const tracker2 = getTracker(id2);
8
331
 
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { fetch } from '../utils/fetcher.js';
3
3
  import { load, extractMeta } from '../utils/parser.js';
4
- import { braveWebSearch } from '../scrapers/brave-search.js';
4
+ import { webSearch } from '../scrapers/searxng-search.js';
5
5
  import { callAI, hasAIKey } from '../ai/client.js';
6
6
  import { createTracker } from '../storage.js';
7
7
  import { header, section, success, warn, error } from '../utils/display.js';
@@ -75,7 +75,7 @@ export async function runDiscover(url, options) {
75
75
 
76
76
  for (const query of queries) {
77
77
  await new Promise(r => setTimeout(r, 600));
78
- const { results, error: searchError } = await braveWebSearch(query, { count: 20 });
78
+ const { results, error: searchError } = await webSearch(query, { count: 20 });
79
79
  if (searchError) {
80
80
  warn(` Search error for "${query}": ${searchError}`);
81
81
  continue;
@@ -105,7 +105,7 @@ export async function runDiscover(url, options) {
105
105
  }
106
106
 
107
107
  if (candidates.size === 0) {
108
- warn('No competitors found. Check that BRAVE_API_KEY is set and the URL is reachable.');
108
+ warn('No competitors found. Check that SEARXNG_URL or SERPER_API_KEY is set and the URL is reachable.');
109
109
  return;
110
110
  }
111
111
 
@@ -1,7 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import Table from 'cli-table3';
3
3
  import { pappersGetFullDossier, pappersSearchByName, pappersSearchSubsidiaries } from '../scrapers/pappers.js';
4
- import { searchPressMentions } from '../scrapers/brave-search.js';
4
+ import { annuaireGetFullDossier, annuaireSearchByName } from '../scrapers/annuaire-entreprises.js';
5
+ import { searchPressMentions } from '../scrapers/searxng-search.js';
5
6
  import { analyzeSite } from '../scrapers/site-analyzer.js';
6
7
  import { callAI, hasAIKey } from '../ai/client.js';
7
8
  import { header, section, warn, error } from '../utils/display.js';
@@ -9,24 +10,49 @@ import { generatePDF } from '@recognity/pdf-report';
9
10
  import { handleExport, formatForExport } from '../utils/export.js';
10
11
  import { setLanguage, getLanguage, t, getPrompt } from '../utils/i18n.js';
11
12
  import { isPro, printProUpgrade } from '../license.js';
13
+ import { hasPappersKey } from '../scrapers/pappers.js';
14
+
15
+ // ── Provider resolution: Pappers (Pro) → Annuaire Entreprises (free fallback) ──
16
+
17
+ /**
18
+ * Resolve the FR company data provider.
19
+ * Returns { provider, providerName } where provider is the scraper module to use.
20
+ */
21
+ function resolveFRProvider() {
22
+ if (isPro() && hasPappersKey()) {
23
+ return { providerName: 'pappers', searchByName: pappersSearchByName, getFullDossier: pappersGetFullDossier, searchSubsidiaries: pappersSearchSubsidiaries };
24
+ }
25
+ return { providerName: 'annuaire-entreprises', searchByName: annuaireSearchByName, getFullDossier: annuaireGetFullDossier, searchSubsidiaries: null };
26
+ }
12
27
 
13
28
  export async function runMA(sirenOrName, options) {
14
29
  const hasLicense = isPro();
15
30
  const isPreview = !!options.preview;
31
+ const frProvider = resolveFRProvider();
32
+ const isFallbackProvider = frProvider.providerName === 'annuaire-entreprises';
16
33
 
17
34
  // Set language from global option (passed from main program)
18
35
  if (options.parent?.opts()?.lang) {
19
36
  setLanguage(options.parent.opts().lang);
20
37
  }
21
38
 
39
+ // ── Provider info ────────────────────────────────────────────────────────
40
+ if (isFallbackProvider) {
41
+ console.log(chalk.cyan(' ℹ Provider: Annuaire Entreprises (data.gouv.fr) — 100% gratuit'));
42
+ console.log(chalk.gray(' Données basiques Sirene (CA, effectifs, dirigeants, adresse, NAF).'));
43
+ console.log(chalk.gray(' UBO, BODACC, procédures collectives, mandats croisés non disponibles.'));
44
+ console.log('');
45
+ }
46
+
22
47
  // ── License gate ───────────────────────────────────────────────────────────
23
- if (!hasLicense && !isPreview) {
48
+ // Annuaire Entreprises is free, so no license gate when using fallback
49
+ if (!isFallbackProvider && !hasLicense && !isPreview) {
24
50
  printProUpgrade('Deep Profile Due Diligence');
25
51
  console.log(chalk.gray(' Run with --preview for a limited preview (company identity + last year financials only).\n'));
26
52
  process.exit(1);
27
53
  }
28
54
 
29
- if (isPreview && !hasLicense) {
55
+ if (!isFallbackProvider && isPreview && !hasLicense) {
30
56
  console.log(chalk.yellow(' ⚡ PREVIEW MODE — Company identity + last year financials only'));
31
57
  printProUpgrade('Full company profile');
32
58
  }
@@ -36,7 +62,7 @@ export async function runMA(sirenOrName, options) {
36
62
 
37
63
  if (!/^\d{9}$/.test(sirenOrName)) {
38
64
  console.log(chalk.gray(` Searching for: "${sirenOrName}"...`));
39
- const { results, error: searchErr } = await pappersSearchByName(sirenOrName, { count: 1 });
65
+ const { results, error: searchErr } = await frProvider.searchByName(sirenOrName, { count: 1 });
40
66
  if (searchErr || !results.length) {
41
67
  error(`Company not found: ${searchErr || 'No results'}`);
42
68
  process.exit(1);
@@ -48,10 +74,20 @@ export async function runMA(sirenOrName, options) {
48
74
 
49
75
  // ── Fetch full dossier ─────────────────────────────────────────────────────
50
76
  console.log(chalk.gray(' Loading company data...'));
51
- const { data, error: dossierErr, fromCache } = await pappersGetFullDossier(siren);
77
+ const { data, error: dossierErr, fromCache } = await frProvider.getFullDossier(siren);
52
78
  if (fromCache) console.log(chalk.gray(' ✓ Loaded from cache (0 API credits)'));
53
79
 
54
80
  if (dossierErr || !data) {
81
+ // If Pappers returned 401, retry with Annuaire Entreprises fallback
82
+ if (!isFallbackProvider && dossierErr && /401|unauthorized|forbidden/i.test(dossierErr)) {
83
+ console.log(chalk.yellow(' ⚠ Pappers 401 — fallback vers l\'Annuaire Entreprises (data.gouv.fr)'));
84
+ const fallbackResult = await annuaireGetFullDossier(siren);
85
+ if (fallbackResult.error || !fallbackResult.data) {
86
+ error(`Failed to fetch dossier from both providers: ${dossierErr} / ${fallbackResult.error}`);
87
+ process.exit(1);
88
+ }
89
+ return renderDossier(fallbackResult.data, { ...options, isFallbackProvider: true, isPreview: true });
90
+ }
55
91
  error(`Failed to fetch dossier: ${dossierErr || 'Unknown error'}`);
56
92
  process.exit(1);
57
93
  }
@@ -59,7 +95,8 @@ export async function runMA(sirenOrName, options) {
59
95
  const { identity, financialHistory, consolidatedFinances, ubo, bodacc, dirigeants, representants, etablissements, proceduresCollectives } = data;
60
96
 
61
97
  // ── Header ─────────────────────────────────────────────────────────────────
62
- header(`🏢 Due Diligence Deep Profile — ${identity.name || siren}`);
98
+ const providerTag = isFallbackProvider ? chalk.cyan(' [Annuaire Entreprises]') : '';
99
+ header(`🏢 Due Diligence Deep Profile — ${identity.name || siren}${providerTag}`);
63
100
 
64
101
  // ── Company Identity ───────────────────────────────────────────────────────
65
102
  section(' 📋 Identité');
@@ -77,7 +114,7 @@ export async function runMA(sirenOrName, options) {
77
114
  if (identity.website) printRow('Site web', identity.website);
78
115
 
79
116
  // ── Preview mode stops here (one year of financials) ──────────────────────
80
- if (isPreview) {
117
+ if (isPreview || isFallbackProvider) {
81
118
  const lastFin = financialHistory[0];
82
119
  section(' 💶 Derniers résultats financiers (preview)');
83
120
  if (lastFin) {
@@ -89,8 +126,29 @@ export async function runMA(sirenOrName, options) {
89
126
  console.log(chalk.gray(' Données financières non disponibles.'));
90
127
  }
91
128
  console.log('');
92
- console.log(chalk.yellow(` ⚡ Accédez au rapport complet avec Intelwatch Deep Profile : ${LICENSE_URL}`));
129
+ if (isFallbackProvider) {
130
+ console.log(chalk.cyan(' ℹ Profil issu de l\'Annuaire Entreprises (data.gouv.fr) — données gratuites.'));
131
+ console.log(chalk.gray(' Pour les données complètes (UBO, BODACC, procédures, mandats), configurez PAPPERS_API_KEY.'));
132
+ } else {
133
+ console.log(chalk.yellow(` ⚡ Accédez au rapport complet avec Intelwatch Deep Profile : ${LICENSE_URL}`));
134
+ }
93
135
  console.log('');
136
+
137
+ // When using fallback provider, only render available sections
138
+ if (isFallbackProvider) {
139
+ // Dirigeants (available from Annuaire)
140
+ if (dirigeants.length > 0) {
141
+ section(` 👔 Dirigeants (${dirigeants.length})`);
142
+ for (const d of dirigeants) {
143
+ const name = [d.prenom, d.nom].filter(Boolean).join(' ');
144
+ console.log('');
145
+ console.log(' ' + chalk.white.bold(name) + chalk.gray(` — ${d.role || '?'}`));
146
+ }
147
+ console.log('');
148
+ }
149
+ console.log(chalk.gray(' Sections non disponibles via Annuaire Entreprises: UBO, BODACC, procédures collectives, mandats croisés, filiales.'));
150
+ return;
151
+ }
94
152
  return;
95
153
  }
96
154
 
@@ -309,9 +367,9 @@ export async function runMA(sirenOrName, options) {
309
367
  // Additional M&A-focused search to catch acquisitions/deals (dorks: quality M&A sources only)
310
368
  const MA_SITE_DORKS = '(site:fusacq.com OR site:cfnews.net OR site:lesechos.fr OR site:maddyness.com OR site:agefi.fr)';
311
369
  try {
312
- const { braveWebSearch } = await import('../scrapers/brave-search.js');
370
+ const { webSearch } = await import('../scrapers/searxng-search.js');
313
371
  await new Promise(r => setTimeout(r, 600));
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 });
372
+ const maSearch = await webSearch(`"${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 });
315
373
  for (const r of (maSearch.results || [])) {
316
374
  const text = ((r.title || '') + ' ' + (r.snippet || '')).toLowerCase();
317
375
  if (!text.includes(brandName.toLowerCase())) continue;
@@ -407,11 +465,11 @@ export async function runMA(sirenOrName, options) {
407
465
 
408
466
  // Company website M&A articles via Brave (more reliable than crawling)
409
467
  try {
410
- const { braveWebSearch: braveSearch3 } = await import('../scrapers/brave-search.js');
468
+ const { webSearch } = await import('../scrapers/searxng-search.js');
411
469
  const domain = (() => { try { return new URL(companyDomain).hostname; } catch { return ''; } })();
412
470
  if (domain) {
413
471
  await new Promise(r => setTimeout(r, 600));
414
- const siteSearch = await braveSearch3(
472
+ const siteSearch = await webSearch(
415
473
  `site:${domain} acquisition OR rapprochement OR capital OR croissance OR partenariat OR intègre`,
416
474
  { count: 10 },
417
475
  );
@@ -438,9 +496,9 @@ export async function runMA(sirenOrName, options) {
438
496
 
439
497
  // LinkedIn posts via Brave
440
498
  try {
441
- const { braveWebSearch: braveSearch2 } = await import('../scrapers/brave-search.js');
499
+ const { webSearch } = await import('../scrapers/searxng-search.js');
442
500
  await new Promise(r => setTimeout(r, 600));
443
- const linkedinSearch = await braveSearch2(
501
+ const linkedinSearch = await webSearch(
444
502
  `site:linkedin.com "${brandName}" acquisition OR croissance OR chiffre OR recrutement OR partenariat`,
445
503
  { count: 10 },
446
504
  );
@@ -547,7 +605,7 @@ export async function runMA(sirenOrName, options) {
547
605
  let aiAnalysis = null;
548
606
  if (options.ai) {
549
607
  section(' 🤖 Analyse IA — Due Diligence');
550
- if (!hasAIKey()) {
608
+ if (!hasAIKey() && !options.uncensored) {
551
609
  warn(' No AI API key. Set OPENAI_API_KEY or ANTHROPIC_API_KEY.');
552
610
  } else {
553
611
  console.log(chalk.gray(' Generating AI due diligence analysis (JSON)...'));
@@ -818,7 +876,7 @@ OBLIGATOIRE :
818
876
  - aiComment: 3-4 sentences. Compare deposited (62M€ 2024) vs announced/projected. Be specific. If multiple revenue targets exist (e.g. 100M€ and 300M€), explain both.
819
877
  - aiComment: 3-4 sentences comparing deposited vs announced/projected, discussing growth sustainability and outlook`;
820
878
 
821
- const raw = await callAI(systemPrompt, userPrompt, { maxTokens: 8192 });
879
+ const raw = await callAI(systemPrompt, userPrompt, { maxTokens: 8192, uncensored: options.uncensored });
822
880
  aiAnalysis = extractAIJSON(raw);
823
881
 
824
882
  // M&A History: Merging code-built events with AI events instead of overwriting