intelwatch 1.5.0 → 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.
package/README.md CHANGED
@@ -11,6 +11,8 @@
11
11
 
12
12
  ```bash
13
13
  npm install -g intelwatch
14
+ intelwatch setup
15
+
14
16
  # or run directly without installing:
15
17
  npx intelwatch profile kpmg.fr --ai
16
18
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intelwatch",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Competitive intelligence CLI — track competitors, keywords, and brand mentions from the terminal",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,30 @@
1
+ import fs from 'fs';
2
+
3
+ const file = 'README.md';
4
+ let content = fs.readFileSync(file, 'utf8');
5
+
6
+ const regexInstallation = /(## Installation[^#]+)/s;
7
+ const newInstallationBlock = \`## Installation
8
+
9
+ \`\`\`bash
10
+ npm install -g intelwatch
11
+
12
+ # 1. Run the interactive setup wizard to configure your APIs (OpenAI/Gemini, SearxNG, Pappers...)
13
+ intelwatch setup
14
+
15
+ # 2. Or run directly without installing (prompts will appear if needed):
16
+ npx intelwatch profile kpmg.fr --ai
17
+ \`\`\`
18
+
19
+ **Requirements:** Node.js >=18
20
+
21
+ ### ⚡ Auto-Fallback & Zero-Cost Capabilities (v1.5+)
22
+ Intelwatch is designed to be usable **for free** without any API keys:
23
+ - **Web Search**: Uses a local \`SearxNG\` or local \`Camofox\` instance natively if no API key is provided.
24
+ - **Company Identity (Due Diligence)**: Automatically falls back to the French Open Data **Annuaire Entreprises** (\`data.gouv.fr\`) if no Pappers API key is present.
25
+ - **Bot Bypass**: Fully integrated with the \`Camofox\` bypass engine (over port 9377) to scrape strict sites (Cloudflare/Datadome) safely.
26
+
27
+ \`;
28
+
29
+ content = content.replace(regexInstallation, newInstallationBlock);
30
+ fs.writeFileSync(file, content);
@@ -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
 
@@ -206,6 +207,16 @@ program
206
207
  }
207
208
  });
208
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
+
209
220
  // ─── auth ─────────────────────────────────────────────────────────────────────
210
221
 
211
222
  program