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.
@@ -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 };