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 +2 -0
- package/package.json +1 -1
- package/patch_readme.js +30 -0
- package/src/commands/setup.js +294 -0
- package/src/index.js +11 -0
package/README.md
CHANGED
package/package.json
CHANGED
package/patch_readme.js
ADDED
|
@@ -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
|