intelwatch 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [1.1.0] — 2026-03-02 (in progress)
6
+
7
+ ### Added
8
+ - **discover** — Automatic competitor discovery from a URL (analyzes site, searches similar businesses)
9
+ - **track person** — New tracker type for people and public figures (press + social mentions)
10
+ - Social media monitoring via Brave Search (Twitter/X, Reddit, LinkedIn)
11
+ - Pappers API integration (BYOK) — French company data (SIREN, CA, dirigeants, effectifs)
12
+
13
+ ## [1.0.0] — 2026-03-02
14
+
15
+ ### Added
16
+ - Initial release
17
+ - **track competitor** — Track a competitor website (tech stack, pages, SEO, security, press)
18
+ - **track keyword** — Track a keyword in search engine results (SERP positions)
19
+ - **track brand** — Track brand mentions across press and web
20
+ - **list** — List all active trackers
21
+ - **remove** — Remove a tracker
22
+ - **check** — Fetch fresh snapshots for all or specific trackers
23
+ - **digest** — Summary of recent changes across all trackers
24
+ - **diff** — Compare two snapshots of the same tracker
25
+ - **report** — Generate a full intelligence report in markdown
26
+ - **history** — View snapshot history for a tracker
27
+ - **compare** — Side-by-side comparison of multiple competitors
28
+ - **notify** — Send alerts when significant changes are detected
29
+ - **ai-summary** — AI-powered intelligence brief from tracker data (BYOK)
30
+ - **pitch** — AI-generated competitive sales pitch against a tracked competitor (BYOK)
31
+ - Deep site analysis: tech detection, page crawl, key pages, job listings, social profiles
32
+ - Press & reputation monitoring via Brave Search API (with Google scraping fallback)
33
+ - Sentiment analysis (French + English) for press mentions
34
+ - BYOK AI: supports OpenAI (`gpt-4o-mini` default) and Anthropic (`claude-3-5-haiku-latest`)
35
+ - Cost tracking for AI operations
36
+
37
+ ### Fixed
38
+ - Array headers bug in tech-detect.js
39
+ - Anthropic model name updated to `claude-3-5-haiku-latest`
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # intelwatch
2
+
3
+ > Competitive intelligence from the terminal. Track competitors, keywords, and brand mentions — no expensive SaaS required.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g intelwatch
9
+ # or from source:
10
+ npm link
11
+ ```
12
+
13
+ **Requirements:** Node.js >=18
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # Add trackers
19
+ intelwatch track competitor https://competitor.com --name "Acme Corp"
20
+ intelwatch track keyword "audit SEO"
21
+ intelwatch track brand "Recognity"
22
+
23
+ # Run checks
24
+ intelwatch check
25
+
26
+ # See what changed
27
+ intelwatch digest
28
+
29
+ # Full report
30
+ intelwatch report --format html
31
+ ```
32
+
33
+ ## Commands
34
+
35
+ ### Tracker Management
36
+
37
+ #### `intelwatch track competitor <url> [--name <alias>]`
38
+ Tracks a competitor website. Captures:
39
+ - Pages found (via link extraction)
40
+ - Pricing page content and price changes
41
+ - Technology stack (35+ technologies detected)
42
+ - Open job positions (/careers, /jobs)
43
+ - Social links
44
+ - Meta title/description changes on key pages
45
+
46
+ ```bash
47
+ intelwatch track competitor https://acme.com --name "Acme"
48
+ intelwatch track competitor https://rival.io
49
+ ```
50
+
51
+ #### `intelwatch track keyword <keyword> [--engine google]`
52
+ Tracks Google SERP rankings for a keyword. Records top 20 results, detects position changes, new entrants/exits, and featured snippet holders.
53
+
54
+ ```bash
55
+ intelwatch track keyword "project management software"
56
+ intelwatch track keyword "audit SEO" --engine google
57
+ ```
58
+
59
+ #### `intelwatch track brand <name>`
60
+ Tracks brand mentions across Google News and recent web results. Detects sentiment (positive/negative) and categorizes mentions (press, blog, forum, social, review).
61
+
62
+ ```bash
63
+ intelwatch track brand "Recognity"
64
+ intelwatch track brand "My Company Name"
65
+ ```
66
+
67
+ ### Listing & Removing
68
+
69
+ ```bash
70
+ intelwatch list # List all trackers
71
+ intelwatch remove <tracker-id> # Remove a tracker
72
+ ```
73
+
74
+ ### Checking & Diffs
75
+
76
+ ```bash
77
+ intelwatch check # Check all trackers
78
+ intelwatch check --tracker acme-com # Check one tracker
79
+
80
+ intelwatch diff acme-com # Compare last 2 snapshots
81
+ intelwatch diff acme-com --days 7 # Compare with 7 days ago
82
+ ```
83
+
84
+ ### Reports
85
+
86
+ ```bash
87
+ intelwatch digest # Quick summary table
88
+ intelwatch report # Markdown report (stdout)
89
+ intelwatch report --format html # HTML report (saved to ~/.intelwatch/reports/)
90
+ intelwatch report --format json # JSON report (stdout)
91
+ intelwatch report --format md --output ./weekly.md # Custom output file
92
+ ```
93
+
94
+ ### History & Comparison
95
+
96
+ ```bash
97
+ intelwatch history acme-com # Show snapshot history
98
+ intelwatch history acme-com --limit 10 # Last 10 snapshots
99
+
100
+ intelwatch compare acme-com rival-com # Side-by-side comparison
101
+ ```
102
+
103
+ ### Notifications
104
+
105
+ ```bash
106
+ intelwatch notify --setup # Interactive setup (Slack, Discord webhook)
107
+ ```
108
+
109
+ Notifications config is stored at `~/.intelwatch/config.yml`:
110
+
111
+ ```yaml
112
+ notifications:
113
+ webhook: https://hooks.slack.com/services/xxx/yyy/zzz
114
+ events:
115
+ - competitor.new_page
116
+ - competitor.price_change
117
+ - keyword.position_change
118
+ - brand.new_mention
119
+ - brand.negative_mention
120
+ ```
121
+
122
+ ## Data Storage
123
+
124
+ All data is stored locally in `~/.intelwatch/`:
125
+
126
+ ```
127
+ ~/.intelwatch/
128
+ ├── config.yml # Notification settings
129
+ ├── trackers.json # Active trackers
130
+ ├── snapshots/ # Historical snapshots (JSON)
131
+ └── reports/ # Generated HTML reports
132
+ ```
133
+
134
+ ## Technology Detection
135
+
136
+ Detects 35+ technologies via headers, meta tags, scripts, HTML patterns:
137
+
138
+ | Category | Technologies |
139
+ |----------|-------------|
140
+ | CMS | WordPress, Drupal, Joomla |
141
+ | E-commerce | Shopify, Magento |
142
+ | Website Builder | Wix, Squarespace, Webflow |
143
+ | JS Framework | React, Vue.js, Angular, Next.js, Nuxt.js, Gatsby, Svelte |
144
+ | JS Library | jQuery |
145
+ | CSS Framework | Bootstrap, Tailwind CSS |
146
+ | Analytics | Google Analytics, Google Tag Manager, Facebook Pixel, Hotjar |
147
+ | CRM/Marketing | HubSpot, Mailchimp, Intercom |
148
+ | CDN/Security | Cloudflare |
149
+ | Web Server | nginx, Apache |
150
+ | Hosting | Vercel, Netlify |
151
+ | Backend | PHP, Django, Ruby on Rails, Node.js/Express |
152
+ | Payment | Stripe |
153
+
154
+ ## Sentiment Analysis
155
+
156
+ English and French word lists for positive/negative detection in brand mentions. Categorizes mentions as: press, blog, forum, social, or review.
157
+
158
+ ## Design Principles
159
+
160
+ - **No external APIs** — everything via respectful web scraping
161
+ - **Respectful scraping** — 1-2s delays, user-agent rotation, retry backoff
162
+ - **Graceful degradation** — saves what it can if a check partially fails
163
+ - **Local-first** — all data stays on your machine
164
+
165
+ ## Tests
166
+
167
+ ```bash
168
+ npm test
169
+ ```
170
+
171
+ 40 tests covering storage logic, technology detection, and sentiment analysis.
172
+
173
+ ## License
174
+
175
+ MIT
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from '../src/index.js';
4
+
5
+ program.parseAsync(process.argv).catch(err => {
6
+ console.error('Fatal error:', err.message);
7
+ process.exit(1);
8
+ });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "intelwatch",
3
+ "version": "1.0.0",
4
+ "description": "Competitive intelligence CLI — track competitors, keywords, and brand mentions from the terminal",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "intelwatch": "./bin/intelwatch.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node --test test/*.test.js",
12
+ "start": "node bin/intelwatch.js"
13
+ },
14
+ "dependencies": {
15
+ "axios": "^1.7.9",
16
+ "chalk": "^5.3.0",
17
+ "cheerio": "^1.0.0",
18
+ "cli-table3": "^0.6.5",
19
+ "commander": "^12.1.0",
20
+ "inquirer": "^10.2.2",
21
+ "yaml": "^2.6.1"
22
+ },
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "author": "Recognity <anthony@recognity.fr> (https://recognity.fr)",
27
+ "homepage": "https://recognity.fr",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/recognity/intelwatch.git"
31
+ },
32
+ "keywords": [
33
+ "competitive-intelligence",
34
+ "osint",
35
+ "cli",
36
+ "competitor",
37
+ "brand-monitoring",
38
+ "press",
39
+ "sentiment",
40
+ "ai",
41
+ "pappers"
42
+ ]
43
+ }
@@ -0,0 +1,130 @@
1
+ import { loadConfig } from '../config.js';
2
+
3
+ /**
4
+ * Resolve AI provider config: env vars take priority, then config file.
5
+ * Returns null if no key is configured.
6
+ */
7
+ export function getAIConfig() {
8
+ const envOpenAI = process.env.OPENAI_API_KEY;
9
+ const envAnthropic = process.env.ANTHROPIC_API_KEY;
10
+
11
+ if (envOpenAI) {
12
+ return { provider: 'openai', apiKey: envOpenAI, model: 'gpt-4o-mini' };
13
+ }
14
+ if (envAnthropic) {
15
+ return { provider: 'anthropic', apiKey: envAnthropic, model: 'claude-haiku-4-5-20251001' };
16
+ }
17
+
18
+ // Fall back to ~/.intelwatch/config.yml ai section
19
+ try {
20
+ const config = loadConfig();
21
+ const ai = config.ai;
22
+ if (ai?.api_key) {
23
+ const provider = ai.provider || 'openai';
24
+ const defaultModel = provider === 'anthropic' ? 'claude-haiku-4-5-20251001' : 'gpt-4o-mini';
25
+ return {
26
+ provider,
27
+ apiKey: ai.api_key,
28
+ model: ai.model || defaultModel,
29
+ };
30
+ }
31
+ } catch {
32
+ // config load failure — no AI
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ export function hasAIKey() {
39
+ return getAIConfig() !== null;
40
+ }
41
+
42
+ /**
43
+ * Call the AI with a system + user prompt. Returns the response text.
44
+ * Throws on API errors.
45
+ */
46
+ export async function callAI(systemPrompt, userPrompt, options = {}) {
47
+ const aiConfig = getAIConfig();
48
+ if (!aiConfig) {
49
+ throw new Error(
50
+ 'No AI API key configured. Set OPENAI_API_KEY or ANTHROPIC_API_KEY, ' +
51
+ 'or add ai.api_key to ~/.intelwatch/config.yml'
52
+ );
53
+ }
54
+
55
+ const { provider, apiKey, model } = aiConfig;
56
+ const maxTokens = options.maxTokens || 1000;
57
+
58
+ if (provider === 'anthropic') {
59
+ return callAnthropic(apiKey, model, systemPrompt, userPrompt, maxTokens);
60
+ }
61
+ return callOpenAI(apiKey, model, systemPrompt, userPrompt, maxTokens);
62
+ }
63
+
64
+ async function callOpenAI(apiKey, model, systemPrompt, userPrompt, maxTokens) {
65
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
66
+ method: 'POST',
67
+ headers: {
68
+ Authorization: `Bearer ${apiKey}`,
69
+ 'Content-Type': 'application/json',
70
+ },
71
+ body: JSON.stringify({
72
+ model,
73
+ max_tokens: maxTokens,
74
+ messages: [
75
+ { role: 'system', content: systemPrompt },
76
+ { role: 'user', content: userPrompt },
77
+ ],
78
+ }),
79
+ });
80
+
81
+ if (!res.ok) {
82
+ const body = await res.text();
83
+ throw new Error(`OpenAI API ${res.status}: ${body.slice(0, 200)}`);
84
+ }
85
+
86
+ const data = await res.json();
87
+ return data.choices[0].message.content.trim();
88
+ }
89
+
90
+ async function callAnthropic(apiKey, model, systemPrompt, userPrompt, maxTokens) {
91
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
92
+ method: 'POST',
93
+ headers: {
94
+ 'x-api-key': apiKey,
95
+ 'anthropic-version': '2023-06-01',
96
+ 'Content-Type': 'application/json',
97
+ },
98
+ body: JSON.stringify({
99
+ model,
100
+ max_tokens: maxTokens,
101
+ system: systemPrompt,
102
+ messages: [{ role: 'user', content: userPrompt }],
103
+ }),
104
+ });
105
+
106
+ if (!res.ok) {
107
+ const body = await res.text();
108
+ throw new Error(`Anthropic API ${res.status}: ${body.slice(0, 200)}`);
109
+ }
110
+
111
+ const data = await res.json();
112
+ return data.content[0].text.trim();
113
+ }
114
+
115
+ /**
116
+ * Rough cost estimate for display (assumes 4 chars ≈ 1 token).
117
+ */
118
+ export function estimateCost(inputChars, outputChars, provider = 'openai') {
119
+ const inputTokens = Math.ceil(inputChars / 4);
120
+ const outputTokens = Math.ceil(outputChars / 4);
121
+
122
+ // gpt-4o-mini: $0.15/1M in, $0.60/1M out
123
+ // claude-haiku: $0.25/1M in, $1.25/1M out
124
+ const rates = provider === 'anthropic'
125
+ ? { in: 0.25 / 1_000_000, out: 1.25 / 1_000_000 }
126
+ : { in: 0.15 / 1_000_000, out: 0.60 / 1_000_000 };
127
+
128
+ const cost = inputTokens * rates.in + outputTokens * rates.out;
129
+ return { inputTokens, outputTokens, cost: cost.toFixed(5) };
130
+ }
@@ -0,0 +1,147 @@
1
+ import chalk from 'chalk';
2
+ import { loadTrackers, getTracker, listSnapshots, loadSnapshot } from '../storage.js';
3
+ import { diffCompetitorSnapshots } from '../trackers/competitor.js';
4
+ import { header, section, error, warn } from '../utils/display.js';
5
+ import { callAI, hasAIKey, getAIConfig } from '../ai/client.js';
6
+
7
+ export async function runAISummary(options = {}) {
8
+ if (!hasAIKey()) {
9
+ error('No AI API key configured.');
10
+ console.log(chalk.gray('Set OPENAI_API_KEY or ANTHROPIC_API_KEY env var, or add to ~/.intelwatch/config.yml:'));
11
+ console.log(chalk.gray(' ai:\n api_key: sk-xxx\n provider: openai # or anthropic'));
12
+ process.exit(1);
13
+ }
14
+
15
+ let trackers;
16
+ if (options.tracker) {
17
+ const t = getTracker(options.tracker);
18
+ if (!t) {
19
+ error(`Tracker not found: ${options.tracker}`);
20
+ process.exit(1);
21
+ }
22
+ trackers = [t];
23
+ } else {
24
+ trackers = loadTrackers().filter(t => t.type === 'competitor');
25
+ }
26
+
27
+ if (trackers.length === 0) {
28
+ warn('No competitor trackers found. Use `intelwatch track competitor <url>` to add one.');
29
+ return;
30
+ }
31
+
32
+ const aiConfig = getAIConfig();
33
+ const dateStr = new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
34
+
35
+ header(`📊 Weekly Intelligence Brief — ${dateStr}`);
36
+ console.log(chalk.gray(`Provider: ${aiConfig.provider} / ${aiConfig.model}\n`));
37
+
38
+ for (const tracker of trackers) {
39
+ const snapshots = listSnapshots(tracker.id);
40
+ if (snapshots.length === 0) {
41
+ console.log(chalk.yellow(`⚠ ${tracker.name || tracker.url}: No snapshots yet. Run \`intelwatch check\` first.\n`));
42
+ continue;
43
+ }
44
+
45
+ const latest = loadSnapshot(snapshots[snapshots.length - 1].filepath);
46
+ const prev = snapshots.length > 1 ? loadSnapshot(snapshots[snapshots.length - 2].filepath) : null;
47
+ const changes = diffCompetitorSnapshots(prev, latest);
48
+
49
+ const name = tracker.name || tracker.url;
50
+ section(`${name}:`);
51
+
52
+ try {
53
+ const brief = await generateCompetitorBrief(tracker, latest, changes);
54
+ console.log('\n' + brief + '\n');
55
+ } catch (err) {
56
+ console.log(chalk.red(` AI error: ${err.message}\n`));
57
+ }
58
+ }
59
+ }
60
+
61
+ async function generateCompetitorBrief(tracker, snapshot, changes) {
62
+ const name = tracker.name || tracker.url;
63
+
64
+ const systemPrompt =
65
+ 'You are a competitive intelligence analyst. Write concise, actionable intelligence briefs. ' +
66
+ 'Be specific and data-driven, not vague. Write in plain English prose (no bullet lists). ' +
67
+ 'Keep the analysis to 2-4 sentences, then add one Recommendation sentence.';
68
+
69
+ const context = buildSnapshotContext(snapshot, changes);
70
+
71
+ const userPrompt =
72
+ `Write an intelligence brief for competitor: ${name}\n\n` +
73
+ `Data:\n${context}\n\n` +
74
+ `Format exactly:\n` +
75
+ `${name} ([domain]):\n` +
76
+ `[2-4 sentence narrative covering their current state, recent activity, and notable signals. ` +
77
+ `Reference specific data points — press headlines, tech stack issues, job growth, ratings.]\n\n` +
78
+ `Recommendation: [1 sentence actionable advice for competing against them right now.]`;
79
+
80
+ return await callAI(systemPrompt, userPrompt, { maxTokens: 450 });
81
+ }
82
+
83
+ function buildSnapshotContext(snap, changes) {
84
+ const lines = [];
85
+
86
+ lines.push(`URL: ${snap.url}`);
87
+ lines.push(`Last checked: ${snap.checkedAt}`);
88
+ lines.push(`Pages found: ${snap.pageCount || 0}`);
89
+
90
+ if (snap.techStack?.length) {
91
+ lines.push(`Tech stack: ${snap.techStack.map(t => t.name).join(', ')}`);
92
+ }
93
+
94
+ if (snap.jobs?.estimatedOpenings) {
95
+ lines.push(`Open jobs: ~${snap.jobs.estimatedOpenings}`);
96
+ }
97
+
98
+ if (snap.pricing?.prices?.length) {
99
+ lines.push(`Pricing: ${snap.pricing.prices.slice(0, 5).join(', ')}`);
100
+ }
101
+
102
+ if (snap.performance) {
103
+ const p = snap.performance;
104
+ lines.push(`Performance: load ${p.loadTime}ms, TTFB ${p.ttfb}ms`);
105
+ }
106
+
107
+ if (snap.security) {
108
+ const s = snap.security;
109
+ const issues = [];
110
+ if (!s.hsts) issues.push('no HSTS');
111
+ if (!s.httpsRedirect) issues.push('no HTTPS redirect');
112
+ if (issues.length) lines.push(`Security issues: ${issues.join(', ')}`);
113
+ }
114
+
115
+ if (snap.seoSignals) {
116
+ const seo = snap.seoSignals;
117
+ const signals = [];
118
+ if (seo.missingAlt > 0) signals.push(`${seo.missingAlt} images without alt`);
119
+ if (seo.htmlSize) signals.push(`${Math.round(seo.htmlSize / 1024)}KB uncompressed HTML`);
120
+ if (seo.brokenLinks > 0) signals.push(`${seo.brokenLinks} broken links`);
121
+ if (signals.length) lines.push(`SEO signals: ${signals.join(', ')}`);
122
+ }
123
+
124
+ if (snap.press?.articles?.length) {
125
+ const p = snap.press;
126
+ lines.push(
127
+ `Press mentions: ${p.totalCount} total ` +
128
+ `(${p.sentimentBreakdown?.positive || 0} positive, ${p.sentimentBreakdown?.negative || 0} negative)`
129
+ );
130
+ const headlines = p.articles.slice(0, 3).map(a => `"${a.title}"`).join('; ');
131
+ lines.push(`Recent headlines: ${headlines}`);
132
+ }
133
+
134
+ if (snap.reputation?.platforms?.length) {
135
+ const ratings = snap.reputation.platforms.map(p => `${p.platform}: ${p.rating}/5`).join(', ');
136
+ lines.push(`Ratings: ${ratings}`);
137
+ }
138
+
139
+ if (changes.length > 0) {
140
+ const changeSummary = changes.slice(0, 8).map(c => ` ${c.type}: ${c.field} — ${c.value}`).join('\n');
141
+ lines.push(`\nRecent changes:\n${changeSummary}`);
142
+ } else {
143
+ lines.push('Recent changes: none detected');
144
+ }
145
+
146
+ return lines.join('\n');
147
+ }