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 +39 -0
- package/README.md +175 -0
- package/bin/intelwatch.js +8 -0
- package/package.json +43 -0
- package/src/ai/client.js +130 -0
- package/src/commands/ai-summary.js +147 -0
- package/src/commands/check.js +267 -0
- package/src/commands/compare.js +124 -0
- package/src/commands/diff.js +118 -0
- package/src/commands/digest.js +156 -0
- package/src/commands/discover.js +301 -0
- package/src/commands/history.js +60 -0
- package/src/commands/list.js +43 -0
- package/src/commands/notify.js +121 -0
- package/src/commands/pitch.js +156 -0
- package/src/commands/report.js +82 -0
- package/src/commands/track.js +94 -0
- package/src/config.js +65 -0
- package/src/index.js +182 -0
- package/src/report/html.js +499 -0
- package/src/report/json.js +44 -0
- package/src/report/markdown.js +156 -0
- package/src/scrapers/brave-search.js +268 -0
- package/src/scrapers/google-news.js +111 -0
- package/src/scrapers/google.js +113 -0
- package/src/scrapers/pappers.js +119 -0
- package/src/scrapers/site-analyzer.js +252 -0
- package/src/storage.js +168 -0
- package/src/trackers/brand.js +76 -0
- package/src/trackers/competitor.js +268 -0
- package/src/trackers/keyword.js +121 -0
- package/src/trackers/person.js +132 -0
- package/src/utils/display.js +102 -0
- package/src/utils/fetcher.js +82 -0
- package/src/utils/parser.js +110 -0
- package/src/utils/sentiment.js +95 -0
- package/src/utils/tech-detect.js +94 -0
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
|
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
|
+
}
|
package/src/ai/client.js
ADDED
|
@@ -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
|
+
}
|