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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadTrackers, listSnapshots, loadSnapshot, saveReport } from '../storage.js';
|
|
3
|
+
import { generateMarkdownReport } from '../report/markdown.js';
|
|
4
|
+
import { generateHtmlReport } from '../report/html.js';
|
|
5
|
+
import { generateJsonReport } from '../report/json.js';
|
|
6
|
+
import { error, success, warn } from '../utils/display.js';
|
|
7
|
+
import { writeFileSync } from 'fs';
|
|
8
|
+
import { diffCompetitorSnapshots } from '../trackers/competitor.js';
|
|
9
|
+
import { diffKeywordSnapshots } from '../trackers/keyword.js';
|
|
10
|
+
import { diffBrandSnapshots } from '../trackers/brand.js';
|
|
11
|
+
import { computeThreatScore } from '../trackers/competitor.js';
|
|
12
|
+
|
|
13
|
+
export async function runReport(options) {
|
|
14
|
+
const format = options.format || 'md';
|
|
15
|
+
const trackers = loadTrackers();
|
|
16
|
+
|
|
17
|
+
if (trackers.length === 0) {
|
|
18
|
+
warn('No trackers configured. Use `intelwatch track` to add one.');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Build report data
|
|
23
|
+
const reportData = {
|
|
24
|
+
generatedAt: new Date().toISOString(),
|
|
25
|
+
competitors: [],
|
|
26
|
+
keywords: [],
|
|
27
|
+
brands: [],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
for (const tracker of trackers) {
|
|
31
|
+
const snapshots = listSnapshots(tracker.id);
|
|
32
|
+
if (snapshots.length === 0) continue;
|
|
33
|
+
|
|
34
|
+
const latest = loadSnapshot(snapshots[snapshots.length - 1].filepath);
|
|
35
|
+
const prev = snapshots.length > 1 ? loadSnapshot(snapshots[snapshots.length - 2].filepath) : null;
|
|
36
|
+
|
|
37
|
+
let changes = [];
|
|
38
|
+
if (tracker.type === 'competitor') {
|
|
39
|
+
changes = diffCompetitorSnapshots(prev, latest);
|
|
40
|
+
const threatScore = computeThreatScore(tracker, changes);
|
|
41
|
+
reportData.competitors.push({
|
|
42
|
+
tracker,
|
|
43
|
+
snapshot: latest,
|
|
44
|
+
changes,
|
|
45
|
+
threatScore,
|
|
46
|
+
prevSnapshot: prev,
|
|
47
|
+
});
|
|
48
|
+
} else if (tracker.type === 'keyword') {
|
|
49
|
+
changes = diffKeywordSnapshots(prev, latest);
|
|
50
|
+
reportData.keywords.push({ tracker, snapshot: latest, changes });
|
|
51
|
+
} else if (tracker.type === 'brand') {
|
|
52
|
+
changes = diffBrandSnapshots(prev, latest);
|
|
53
|
+
reportData.brands.push({ tracker, snapshot: latest, changes });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let content;
|
|
58
|
+
let ext;
|
|
59
|
+
|
|
60
|
+
if (format === 'html') {
|
|
61
|
+
content = generateHtmlReport(reportData);
|
|
62
|
+
ext = 'html';
|
|
63
|
+
} else if (format === 'json') {
|
|
64
|
+
content = generateJsonReport(reportData);
|
|
65
|
+
ext = 'json';
|
|
66
|
+
} else {
|
|
67
|
+
content = generateMarkdownReport(reportData);
|
|
68
|
+
ext = 'md';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (options.output) {
|
|
72
|
+
writeFileSync(options.output, content, 'utf8');
|
|
73
|
+
success(`Report saved to: ${options.output}`);
|
|
74
|
+
} else if (format === 'html') {
|
|
75
|
+
const filename = `report-${new Date().toISOString().slice(0, 10)}.html`;
|
|
76
|
+
const filepath = saveReport(filename, content);
|
|
77
|
+
success(`HTML report saved to: ${filepath}`);
|
|
78
|
+
console.log(chalk.gray('Open in your browser to view.'));
|
|
79
|
+
} else {
|
|
80
|
+
console.log(content);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createTracker } from '../storage.js';
|
|
2
|
+
import { success, warn, error } from '../utils/display.js';
|
|
3
|
+
|
|
4
|
+
export async function trackPerson(name, options) {
|
|
5
|
+
const org = options.org || null;
|
|
6
|
+
|
|
7
|
+
const { tracker, created } = createTracker('person', {
|
|
8
|
+
personName: name,
|
|
9
|
+
name,
|
|
10
|
+
org,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (created) {
|
|
14
|
+
success(`Person tracker created: ${tracker.id}`);
|
|
15
|
+
console.log(` Person: "${name}"`);
|
|
16
|
+
if (org) console.log(` Org : "${org}"`);
|
|
17
|
+
console.log(`\nRun ${chalk_cyan('intelwatch check')} to fetch the first snapshot.`);
|
|
18
|
+
} else {
|
|
19
|
+
warn(`Tracker already exists: ${tracker.id}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return tracker;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function trackCompetitor(url, options) {
|
|
26
|
+
let normalizedUrl = url;
|
|
27
|
+
if (!normalizedUrl.startsWith('http')) {
|
|
28
|
+
normalizedUrl = 'https://' + normalizedUrl;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
new URL(normalizedUrl);
|
|
32
|
+
} catch {
|
|
33
|
+
error(`Invalid URL: ${url}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const name = options.name || new URL(normalizedUrl).hostname.replace('www.', '');
|
|
38
|
+
|
|
39
|
+
const { tracker, created } = createTracker('competitor', {
|
|
40
|
+
url: normalizedUrl,
|
|
41
|
+
name,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (created) {
|
|
45
|
+
success(`Competitor tracker created: ${chalk_green(tracker.id)}`);
|
|
46
|
+
console.log(` Name : ${tracker.name}`);
|
|
47
|
+
console.log(` URL : ${tracker.url}`);
|
|
48
|
+
console.log(`\nRun ${chalk_cyan('intelwatch check')} to fetch the first snapshot.`);
|
|
49
|
+
} else {
|
|
50
|
+
warn(`Tracker already exists: ${tracker.id}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return tracker;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function trackKeyword(keyword, options) {
|
|
57
|
+
const engine = options.engine || 'google';
|
|
58
|
+
|
|
59
|
+
const { tracker, created } = createTracker('keyword', {
|
|
60
|
+
keyword,
|
|
61
|
+
engine,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (created) {
|
|
65
|
+
success(`Keyword tracker created: ${tracker.id}`);
|
|
66
|
+
console.log(` Keyword: "${tracker.keyword}"`);
|
|
67
|
+
console.log(` Engine : ${tracker.engine}`);
|
|
68
|
+
console.log(`\nRun ${chalk_cyan('intelwatch check')} to fetch the first SERP snapshot.`);
|
|
69
|
+
} else {
|
|
70
|
+
warn(`Tracker already exists: ${tracker.id}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return tracker;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function trackBrand(brandName, options) {
|
|
77
|
+
const { tracker, created } = createTracker('brand', {
|
|
78
|
+
brandName,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (created) {
|
|
82
|
+
success(`Brand tracker created: ${tracker.id}`);
|
|
83
|
+
console.log(` Brand: "${tracker.brandName}"`);
|
|
84
|
+
console.log(`\nRun ${chalk_cyan('intelwatch check')} to fetch the first mentions snapshot.`);
|
|
85
|
+
} else {
|
|
86
|
+
warn(`Tracker already exists: ${tracker.id}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return tracker;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Inline chalk helpers to avoid import cycle issues
|
|
93
|
+
function chalk_green(str) { return `\x1b[32m${str}\x1b[0m`; }
|
|
94
|
+
function chalk_cyan(str) { return `\x1b[36m${str}\x1b[0m`; }
|
package/src/config.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
4
|
+
import { BASE_DIR, ensureDirectories } from './storage.js';
|
|
5
|
+
|
|
6
|
+
const CONFIG_FILE = join(BASE_DIR, 'config.yml');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
notifications: {
|
|
10
|
+
webhook: null,
|
|
11
|
+
email: null,
|
|
12
|
+
events: [
|
|
13
|
+
'competitor.new_page',
|
|
14
|
+
'competitor.price_change',
|
|
15
|
+
'keyword.position_change',
|
|
16
|
+
'brand.new_mention',
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
scraping: {
|
|
20
|
+
delay_min: 1000,
|
|
21
|
+
delay_max: 2500,
|
|
22
|
+
retries: 3,
|
|
23
|
+
user_agent_rotate: true,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function loadConfig() {
|
|
28
|
+
ensureDirectories();
|
|
29
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
30
|
+
return { ...DEFAULT_CONFIG };
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const raw = readFileSync(CONFIG_FILE, 'utf8');
|
|
34
|
+
const parsed = parseYaml(raw);
|
|
35
|
+
return deepMerge(DEFAULT_CONFIG, parsed || {});
|
|
36
|
+
} catch {
|
|
37
|
+
return { ...DEFAULT_CONFIG };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function saveConfig(config) {
|
|
42
|
+
ensureDirectories();
|
|
43
|
+
writeFileSync(CONFIG_FILE, stringifyYaml(config), 'utf8');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function updateConfig(updates) {
|
|
47
|
+
const current = loadConfig();
|
|
48
|
+
const merged = deepMerge(current, updates);
|
|
49
|
+
saveConfig(merged);
|
|
50
|
+
return merged;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function deepMerge(base, override) {
|
|
54
|
+
const result = { ...base };
|
|
55
|
+
for (const [key, value] of Object.entries(override)) {
|
|
56
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && base[key] && typeof base[key] === 'object') {
|
|
57
|
+
result[key] = deepMerge(base[key], value);
|
|
58
|
+
} else {
|
|
59
|
+
result[key] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { CONFIG_FILE };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { trackCompetitor, trackKeyword, trackBrand, trackPerson } from './commands/track.js';
|
|
3
|
+
import { runDiscover } from './commands/discover.js';
|
|
4
|
+
import { runCheck } from './commands/check.js';
|
|
5
|
+
import { runDigest } from './commands/digest.js';
|
|
6
|
+
import { runDiff } from './commands/diff.js';
|
|
7
|
+
import { runReport } from './commands/report.js';
|
|
8
|
+
import { runHistory } from './commands/history.js';
|
|
9
|
+
import { runCompare } from './commands/compare.js';
|
|
10
|
+
import { setupNotifications } from './commands/notify.js';
|
|
11
|
+
import { listTrackers, removeTrackerCmd } from './commands/list.js';
|
|
12
|
+
import { runAISummary } from './commands/ai-summary.js';
|
|
13
|
+
import { runPitch } from './commands/pitch.js';
|
|
14
|
+
|
|
15
|
+
const program = new Command();
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.name('intelwatch')
|
|
19
|
+
.description('Competitive intelligence CLI — track competitors, keywords, and brand mentions from the terminal')
|
|
20
|
+
.version('1.0.0');
|
|
21
|
+
|
|
22
|
+
// ─── track ────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const track = program.command('track')
|
|
25
|
+
.description('Add a new tracker');
|
|
26
|
+
|
|
27
|
+
track
|
|
28
|
+
.command('competitor <url>')
|
|
29
|
+
.description('Track a competitor website')
|
|
30
|
+
.option('--name <alias>', 'Friendly name for this competitor')
|
|
31
|
+
.action(async (url, options) => {
|
|
32
|
+
await trackCompetitor(url, options);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
track
|
|
36
|
+
.command('keyword <keyword>')
|
|
37
|
+
.description('Track keyword rankings in Google SERP')
|
|
38
|
+
.option('--engine <engine>', 'Search engine (google)', 'google')
|
|
39
|
+
.action(async (keyword, options) => {
|
|
40
|
+
await trackKeyword(keyword, options);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
track
|
|
44
|
+
.command('brand <name>')
|
|
45
|
+
.description('Track brand mentions across the web')
|
|
46
|
+
.action(async (name, options) => {
|
|
47
|
+
await trackBrand(name, options);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
track
|
|
51
|
+
.command('person <name>')
|
|
52
|
+
.description('Track press mentions and social presence for a person or public figure')
|
|
53
|
+
.option('--org <org>', 'Filter results by organization affiliation (for disambiguation)')
|
|
54
|
+
.action(async (name, options) => {
|
|
55
|
+
await trackPerson(name, options);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ─── list ─────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
program
|
|
61
|
+
.command('list')
|
|
62
|
+
.description('List all active trackers')
|
|
63
|
+
.action(() => {
|
|
64
|
+
listTrackers();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ─── remove ───────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
program
|
|
70
|
+
.command('remove <tracker-id>')
|
|
71
|
+
.description('Remove a tracker')
|
|
72
|
+
.action((id) => {
|
|
73
|
+
removeTrackerCmd(id);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─── check ────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
program
|
|
79
|
+
.command('check')
|
|
80
|
+
.description('Run checks for all (or one) tracker(s)')
|
|
81
|
+
.option('--tracker <id>', 'Only check this specific tracker')
|
|
82
|
+
.action(async (options) => {
|
|
83
|
+
await runCheck(options);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ─── digest ───────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
program
|
|
89
|
+
.command('digest')
|
|
90
|
+
.description('Show a summary of all changes across all trackers')
|
|
91
|
+
.action(async () => {
|
|
92
|
+
await runDigest();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ─── diff ─────────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
program
|
|
98
|
+
.command('diff <tracker-id>')
|
|
99
|
+
.description('Show detailed diff for one tracker')
|
|
100
|
+
.option('--days <n>', 'Compare with snapshot from N days ago')
|
|
101
|
+
.action(async (trackerId, options) => {
|
|
102
|
+
await runDiff(trackerId, options);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ─── report ───────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
program
|
|
108
|
+
.command('report')
|
|
109
|
+
.description('Generate a full intelligence report')
|
|
110
|
+
.option('--format <format>', 'Output format: md, html, json', 'md')
|
|
111
|
+
.option('--output <file>', 'Write report to file')
|
|
112
|
+
.action(async (options) => {
|
|
113
|
+
await runReport(options);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ─── history ──────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
program
|
|
119
|
+
.command('history <tracker-id>')
|
|
120
|
+
.description('Show historical snapshots for a tracker')
|
|
121
|
+
.option('--limit <n>', 'Number of snapshots to show', '20')
|
|
122
|
+
.action((trackerId, options) => {
|
|
123
|
+
runHistory(trackerId, options);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ─── compare ──────────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
program
|
|
129
|
+
.command('compare <tracker1> <tracker2>')
|
|
130
|
+
.description('Side-by-side comparison of two competitor trackers')
|
|
131
|
+
.action((id1, id2) => {
|
|
132
|
+
runCompare(id1, id2);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ─── ai-summary ───────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
program
|
|
138
|
+
.command('ai-summary')
|
|
139
|
+
.description('Generate an AI-powered intelligence brief for all competitor trackers')
|
|
140
|
+
.option('--tracker <id>', 'Only summarize this specific tracker')
|
|
141
|
+
.action(async (options) => {
|
|
142
|
+
await runAISummary(options);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ─── pitch ────────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
program
|
|
148
|
+
.command('pitch <tracker-id>')
|
|
149
|
+
.description('Generate a sales-ready competitive pitch document')
|
|
150
|
+
.option('--for <your-site>', 'Your product or site name', 'your product')
|
|
151
|
+
.option('--format <format>', 'Output format: md, html', 'md')
|
|
152
|
+
.option('--output <file>', 'Save pitch to file')
|
|
153
|
+
.action(async (trackerId, options) => {
|
|
154
|
+
await runPitch(trackerId, options);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ─── discover ─────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
program
|
|
160
|
+
.command('discover <url>')
|
|
161
|
+
.description('Discover competitors for a given URL using web search and AI scoring')
|
|
162
|
+
.option('--auto-track', 'Automatically create competitor trackers for top 5 results')
|
|
163
|
+
.option('--ai', 'Use AI for smarter query generation and relevance scoring (requires API key)')
|
|
164
|
+
.action(async (url, options) => {
|
|
165
|
+
await runDiscover(url, options);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ─── notify ───────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
program
|
|
171
|
+
.command('notify')
|
|
172
|
+
.description('Configure notifications')
|
|
173
|
+
.option('--setup', 'Interactive notification setup')
|
|
174
|
+
.action(async (options) => {
|
|
175
|
+
if (options.setup) {
|
|
176
|
+
await setupNotifications(options);
|
|
177
|
+
} else {
|
|
178
|
+
console.log('Use `intelwatch notify --setup` to configure notifications.');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
export { program };
|