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.
@@ -0,0 +1,294 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { loadConfig, updateConfig, CONFIG_FILE } from '../config.js';
4
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { homedir } from 'os';
7
+
8
+ const ENV_FILE = join(homedir(), '.intelwatch', '.env');
9
+
10
+ // ─── Banner ────────────────────────────────────────────────────────────────────
11
+
12
+ function printBanner() {
13
+ console.log();
14
+ console.log(chalk.cyan(' ╔══════════════════════════════════════════════════╗'));
15
+ console.log(chalk.cyan(' ║') + chalk.bold.white(' Intelwatch Setup Wizard ') + chalk.cyan('║'));
16
+ console.log(chalk.cyan(' ║') + chalk.gray(' Configure your API keys and search provider ') + chalk.cyan('║'));
17
+ console.log(chalk.cyan(' ╚══════════════════════════════════════════════════╝'));
18
+ console.log();
19
+ }
20
+
21
+ // ─── Mask helper ───────────────────────────────────────────────────────────────
22
+
23
+ function mask(key) {
24
+ if (!key || key.length < 8) return key ? '••••••••' : '';
25
+ return key.slice(0, 4) + '••••' + key.slice(-4);
26
+ }
27
+
28
+ // ─── Detect existing values ────────────────────────────────────────────────────
29
+
30
+ function detectCurrentValues() {
31
+ const config = loadConfig();
32
+ const envKeys = loadEnvFile();
33
+
34
+ const ai = config.ai || {};
35
+ const search = config.search || {};
36
+
37
+ return {
38
+ aiProvider: ai.provider || process.env.AI_PROVIDER || envKeys.AI_PROVIDER || '',
39
+ aiApiKey: ai.api_key || process.env.GEMINI_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENROUTER_API_KEY || envKeys.GEMINI_API_KEY || envKeys.OPENAI_API_KEY || envKeys.ANTHROPIC_API_KEY || envKeys.OPENROUTER_API_KEY || '',
40
+ aiModel: ai.model || '',
41
+ pappersKey: process.env.PAPPERS_API_KEY || envKeys.PAPPERS_API_KEY || search.pappers_api_key || '',
42
+ searchProvider: search.provider || process.env.SEARCH_PROVIDER || envKeys.SEARCH_PROVIDER || '',
43
+ searchKey: search.api_key || process.env.SERPAPI_KEY || process.env.SERPER_API_KEY || process.env.BRAVE_API_KEY || envKeys.SERPER_API_KEY || envKeys.BRAVE_API_KEY || '',
44
+ searxngUrl: search.searxng_url || process.env.SEARXNG_URL || envKeys.SEARXNG_URL || '',
45
+ };
46
+ }
47
+
48
+ // ─── .env file helpers ─────────────────────────────────────────────────────────
49
+
50
+ function loadEnvFile() {
51
+ if (!existsSync(ENV_FILE)) return {};
52
+ try {
53
+ const raw = readFileSync(ENV_FILE, 'utf8');
54
+ const result = {};
55
+ for (const line of raw.split('\n')) {
56
+ const trimmed = line.trim();
57
+ if (!trimmed || trimmed.startsWith('#')) continue;
58
+ const eq = trimmed.indexOf('=');
59
+ if (eq === -1) continue;
60
+ const key = trimmed.slice(0, eq).trim();
61
+ const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
62
+ result[key] = val;
63
+ }
64
+ return result;
65
+ } catch {
66
+ return {};
67
+ }
68
+ }
69
+
70
+ function saveEnvFile(values) {
71
+ const lines = [
72
+ '# Intelwatch environment configuration',
73
+ '# Generated by `intelwatch setup`',
74
+ '',
75
+ ];
76
+ for (const [key, val] of Object.entries(values)) {
77
+ if (val) {
78
+ lines.push(`${key}="${val}"`);
79
+ }
80
+ }
81
+ lines.push('');
82
+ writeFileSync(ENV_FILE, lines.join('\n'), 'utf8');
83
+ }
84
+
85
+ // ─── Main wizard ───────────────────────────────────────────────────────────────
86
+
87
+ export async function runSetup() {
88
+ printBanner();
89
+
90
+ const current = detectCurrentValues();
91
+
92
+ // ── Step 1: AI Provider & Key ──────────────────────────────────────────────
93
+
94
+ console.log(chalk.bold.white(' Step 1 / 3 : AI Provider'));
95
+ console.log(chalk.gray(' Required for ai-summary, pitch, discover --ai, profile --ai'));
96
+ console.log();
97
+
98
+ const aiAnswers = await inquirer.prompt([
99
+ {
100
+ type: 'list',
101
+ name: 'aiProvider',
102
+ message: 'Which AI provider do you want to use?',
103
+ choices: [
104
+ { name: 'Google Gemini (recommended, best value)', value: 'google' },
105
+ { name: 'OpenAI', value: 'openai' },
106
+ { name: 'Anthropic', value: 'anthropic' },
107
+ { name: 'OpenRouter (multi-provider gateway)', value: 'openrouter' },
108
+ { name: 'Skip — I\'ll use local Ollama only', value: 'skip' },
109
+ ],
110
+ default: current.aiProvider || 'google',
111
+ },
112
+ {
113
+ type: 'input',
114
+ name: 'aiApiKey',
115
+ message: (answers) => {
116
+ if (answers.aiProvider === 'skip') return 'Skipped';
117
+ const labels = { google: 'Gemini', openai: 'OpenAI', anthropic: 'Anthropic', openrouter: 'OpenRouter' };
118
+ return `${labels[answers.aiProvider]} API Key:`;
119
+ },
120
+ when: (answers) => answers.aiProvider !== 'skip',
121
+ default: current.aiApiKey || undefined,
122
+ filter: (val) => val.trim(),
123
+ validate: (val) => val.length > 0 ? true : 'API key is required (or press Ctrl+C to skip)',
124
+ },
125
+ {
126
+ type: 'input',
127
+ name: 'aiModel',
128
+ message: 'Default model (leave empty for provider default):',
129
+ when: (answers) => answers.aiProvider !== 'skip',
130
+ default: current.aiModel || undefined,
131
+ },
132
+ ]);
133
+
134
+ // ── Step 2: Pappers API Key ────────────────────────────────────────────────
135
+
136
+ console.log();
137
+ console.log(chalk.bold.white(' Step 2 / 3 : Company Data'));
138
+ console.log(chalk.gray(' Used by profile, compare — SIREN/SIRET lookups'));
139
+ console.log(chalk.yellow(' ⚠ Without a Pappers key, intelwatch falls back to free DataGouv APIs'));
140
+ console.log(chalk.gray(' (recherche-entreprises.api.gouv.fr, BODACC) — slower, less detailed.'));
141
+ console.log();
142
+
143
+ const pappersAnswers = await inquirer.prompt([
144
+ {
145
+ type: 'input',
146
+ name: 'pappersKey',
147
+ message: 'Pappers API Key (optional — press Enter to skip):',
148
+ default: current.pappersKey || undefined,
149
+ filter: (val) => val.trim(),
150
+ },
151
+ ]);
152
+
153
+ // ── Step 3: Web Search ─────────────────────────────────────────────────────
154
+
155
+ console.log();
156
+ console.log(chalk.bold.white(' Step 3 / 3 : Web Search'));
157
+ console.log(chalk.gray(' Used by discover, track keyword, track brand, track person'));
158
+ console.log();
159
+
160
+ const searchAnswers = await inquirer.prompt([
161
+ {
162
+ type: 'list',
163
+ name: 'searchProvider',
164
+ message: 'Which search backend do you want to use?',
165
+ choices: [
166
+ { name: 'SearXNG (self-hosted, free)', value: 'searxng' },
167
+ { name: 'Serper.dev (Google SERP API)', value: 'serper' },
168
+ { name: 'Brave Search API', value: 'brave' },
169
+ { name: 'Skip — I\'ll configure later', value: 'skip' },
170
+ ],
171
+ default: current.searchProvider || 'searxng',
172
+ },
173
+ {
174
+ type: 'input',
175
+ name: 'searxngUrl',
176
+ message: 'SearXNG instance URL:',
177
+ when: (answers) => answers.searchProvider === 'searxng',
178
+ default: current.searxngUrl || 'http://localhost:8888',
179
+ validate: (val) => {
180
+ if (!val.trim()) return 'URL is required';
181
+ try { new URL(val.trim()); return true; } catch { return 'Invalid URL'; }
182
+ },
183
+ filter: (val) => val.trim().replace(/\/+$/, ''),
184
+ },
185
+ {
186
+ type: 'input',
187
+ name: 'searchApiKey',
188
+ message: (answers) => {
189
+ const labels = { serper: 'Serper', brave: 'Brave' };
190
+ return `${labels[answers.searchProvider]} API Key:`;
191
+ },
192
+ when: (answers) => ['serper', 'brave'].includes(answers.searchProvider),
193
+ default: current.searchKey || undefined,
194
+ filter: (val) => val.trim(),
195
+ validate: (val) => val.length > 0 ? true : 'API key is required',
196
+ },
197
+ ]);
198
+
199
+ // ── Save everything ────────────────────────────────────────────────────────
200
+
201
+ console.log();
202
+ console.log(chalk.cyan(' Saving configuration...'));
203
+
204
+ // Build config.yml updates
205
+ const configUpdates = {};
206
+
207
+ if (aiAnswers.aiProvider && aiAnswers.aiProvider !== 'skip') {
208
+ configUpdates.ai = {
209
+ provider: aiAnswers.aiProvider,
210
+ api_key: aiAnswers.aiApiKey,
211
+ };
212
+ if (aiAnswers.aiModel) {
213
+ configUpdates.ai.model = aiAnswers.aiModel;
214
+ }
215
+ }
216
+
217
+ configUpdates.search = {
218
+ provider: searchAnswers.searchProvider === 'skip' ? undefined : searchAnswers.searchProvider,
219
+ };
220
+
221
+ if (searchAnswers.searchProvider === 'searxng' && searchAnswers.searxngUrl) {
222
+ configUpdates.search.searxng_url = searchAnswers.searxngUrl;
223
+ }
224
+ if (['serper', 'brave'].includes(searchAnswers.searchProvider) && searchAnswers.searchApiKey) {
225
+ configUpdates.search.api_key = searchAnswers.searchApiKey;
226
+ }
227
+
228
+ if (pappersAnswers.pappersKey) {
229
+ configUpdates.search.pappers_api_key = pappersAnswers.pappersKey;
230
+ }
231
+
232
+ // Clean up undefined
233
+ for (const [k, v] of Object.entries(configUpdates.search)) {
234
+ if (v === undefined) delete configUpdates.search[k];
235
+ }
236
+
237
+ updateConfig(configUpdates);
238
+
239
+ // Also write ~/.intelwatch/.env for shell-level env vars
240
+ const envValues = {};
241
+ if (aiAnswers.aiProvider === 'google' && aiAnswers.aiApiKey) envValues.GEMINI_API_KEY = aiAnswers.aiApiKey;
242
+ if (aiAnswers.aiProvider === 'openai' && aiAnswers.aiApiKey) envValues.OPENAI_API_KEY = aiAnswers.aiApiKey;
243
+ if (aiAnswers.aiProvider === 'anthropic' && aiAnswers.aiApiKey) envValues.ANTHROPIC_API_KEY = aiAnswers.aiApiKey;
244
+ if (aiAnswers.aiProvider === 'openrouter' && aiAnswers.aiApiKey) envValues.OPENROUTER_API_KEY = aiAnswers.aiApiKey;
245
+ if (pappersAnswers.pappersKey) envValues.PAPPERS_API_KEY = pappersAnswers.pappersKey;
246
+ if (searchAnswers.searchProvider === 'searxng' && searchAnswers.searxngUrl) envValues.SEARXNG_URL = searchAnswers.searxngUrl;
247
+ if (searchAnswers.searchProvider === 'serper' && searchAnswers.searchApiKey) envValues.SERPER_API_KEY = searchAnswers.searchApiKey;
248
+ if (searchAnswers.searchProvider === 'brave' && searchAnswers.searchApiKey) envValues.BRAVE_API_KEY = searchAnswers.searchApiKey;
249
+
250
+ saveEnvFile(envValues);
251
+
252
+ // ── Summary ────────────────────────────────────────────────────────────────
253
+
254
+ console.log();
255
+ console.log(chalk.green(' ✓ Configuration saved!'));
256
+ console.log();
257
+ console.log(chalk.white(' ┌─ Summary ──────────────────────────────────────┐'));
258
+
259
+ if (aiAnswers.aiProvider && aiAnswers.aiProvider !== 'skip') {
260
+ const providerLabels = { google: 'Google Gemini', openai: 'OpenAI', anthropic: 'Anthropic', openrouter: 'OpenRouter' };
261
+ console.log(chalk.white(' │') + chalk.bold(' AI:') + ` ${providerLabels[aiAnswers.aiProvider]} ${chalk.gray(mask(aiAnswers.aiApiKey))}`);
262
+ if (aiAnswers.aiModel) {
263
+ console.log(chalk.white(' │') + chalk.gray(` Model: ${aiAnswers.aiModel}`));
264
+ }
265
+ } else {
266
+ console.log(chalk.white(' │') + chalk.yellow(' AI: Skipped (Ollama only)'));
267
+ }
268
+
269
+ if (pappersAnswers.pappersKey) {
270
+ console.log(chalk.white(' │') + chalk.bold(' Pappers:') + ` ${chalk.gray(mask(pappersAnswers.pappersKey))}`);
271
+ } else {
272
+ console.log(chalk.white(' │') + chalk.bold(' Pappers:') + ` ${chalk.yellow('Not configured — using free DataGouv fallback')}`);
273
+ }
274
+
275
+ const searchLabels = { searxng: 'SearXNG', serper: 'Serper', brave: 'Brave' };
276
+ if (searchAnswers.searchProvider && searchAnswers.searchProvider !== 'skip') {
277
+ console.log(chalk.white(' │') + chalk.bold(' Search:') + ` ${searchLabels[searchAnswers.searchProvider]}`);
278
+ if (searchAnswers.searxngUrl) {
279
+ console.log(chalk.white(' │') + chalk.gray(` ${searchAnswers.searxngUrl}`));
280
+ }
281
+ if (searchAnswers.searchApiKey) {
282
+ console.log(chalk.white(' │') + chalk.gray(` Key: ${mask(searchAnswers.searchApiKey)}`));
283
+ }
284
+ } else {
285
+ console.log(chalk.white(' │') + chalk.yellow(' Search: Not configured'));
286
+ }
287
+
288
+ console.log(chalk.white(' └─────────────────────────────────────────────────┘'));
289
+ console.log();
290
+ console.log(chalk.gray(` Config: ${CONFIG_FILE}`));
291
+ console.log(chalk.gray(` Env: ${ENV_FILE}`));
292
+ console.log(chalk.gray(` Tip: Source the .env in your shell or add it to .bashrc`));
293
+ console.log();
294
+ }
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ 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 { runSetup } from './commands/setup.js';
15
16
  import { saveLicenseKey, isPro, _resetCache } from './license.js';
16
17
  import chalk from 'chalk';
17
18
 
@@ -138,10 +139,10 @@ program
138
139
  // ─── compare ──────────────────────────────────────────────────────────────────
139
140
 
140
141
  program
141
- .command('compare <tracker1> <tracker2>')
142
- .description('Side-by-side comparison of two competitor trackers')
143
- .action((id1, id2) => {
144
- runCompare(id1, id2);
142
+ .command('compare <id1> <id2>')
143
+ .description('Side-by-side comparison of two competitor trackers OR two company profiles (SIREN/SIRET)')
144
+ .action(async (id1, id2) => {
145
+ await runCompare(id1, id2);
145
146
  });
146
147
 
147
148
  // ─── ai-summary ───────────────────────────────────────────────────────────────
@@ -173,6 +174,7 @@ program
173
174
  .description('Deep company profile — due diligence report (requires Pro license)')
174
175
  .option('--preview', 'Run limited preview: company identity + last year financials only')
175
176
  .option('--ai', 'Generate an AI-powered due diligence summary (requires AI API key)')
177
+ .option('--uncensored', 'Run AI analysis on a local Ollama instance (uncensored OSINT)')
176
178
  .option('--format <type>', 'Output format: terminal (default) or pdf')
177
179
  .option('--output <path>', 'Output file path for PDF')
178
180
  .option('--export <format>', 'Export structured data (json, csv, xls, pdf)')
@@ -205,6 +207,16 @@ program
205
207
  }
206
208
  });
207
209
 
210
+ // ─── setup ────────────────────────────────────────────────────────────────────
211
+
212
+ program
213
+ .command('setup')
214
+ .alias('init')
215
+ .description('Interactive setup wizard — configure API keys and search provider')
216
+ .action(async () => {
217
+ await runSetup();
218
+ });
219
+
208
220
  // ─── auth ─────────────────────────────────────────────────────────────────────
209
221
 
210
222
  program
@@ -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;
@@ -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);