// kbot Discovery Agent — autonomous outreach for any project
//
// Scans HN, GitHub Issues, Reddit for conversations relevant to your project.
// Drafts authentic technical responses. Posts them autonomously.
// Learns what works. Adjusts over time.
//
// Usage:
//   kbot discovery start           # start autonomous discovery loop
//   kbot discovery start --dry-run # find + draft but don't post
//   kbot discovery status          # show what's been found/posted
//   kbot discovery auth            # configure HN/Reddit/GitHub credentials
//   kbot discovery log             # show recent activity
//
// The user teaches kbot once: project name, description, topics of expertise,
// and platform credentials. Then kbot runs autonomously.
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { execSync } from 'node:child_process';
// ── Paths ──
const DISCOVERY_DIR = join(homedir(), '.kbot', 'discovery');
const CONFIG_FILE = join(DISCOVERY_DIR, 'config.json');
const STATE_FILE = join(DISCOVERY_DIR, 'state.json');
const LOG_FILE = join(DISCOVERY_DIR, 'activity.log');
function ensureDir() {
    if (!existsSync(DISCOVERY_DIR))
        mkdirSync(DISCOVERY_DIR, { recursive: true });
}
// ── Config ──
export function loadConfig() {
    if (!existsSync(CONFIG_FILE))
        return null;
    try {
        return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
    }
    catch {
        return null;
    }
}
export function saveConfig(config) {
    ensureDir();
    writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
}
function loadState() {
    if (!existsSync(STATE_FILE))
        return { totalScans: 0, totalFound: 0, totalPosted: 0, totalSkipped: 0, lastScan: '', posts: [] };
    try {
        return JSON.parse(readFileSync(STATE_FILE, 'utf8'));
    }
    catch {
        return { totalScans: 0, totalFound: 0, totalPosted: 0, totalSkipped: 0, lastScan: '', posts: [] };
    }
}
function saveState(state) {
    ensureDir();
    writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}
function log(msg) {
    ensureDir();
    const line = `[${new Date().toISOString().slice(0, 19)}] ${msg}\n`;
    appendFileSync(LOG_FILE, line);
    console.log(msg);
}
// ── Scanning ──
async function fetchJson(url) {
    const res = await fetch(url, {
        headers: { 'User-Agent': 'kbot-discovery/1.0' },
        signal: AbortSignal.timeout(10000),
    });
    if (!res.ok)
        throw new Error(`HTTP ${res.status}`);
    return res.json();
}
async function scanHN(topics) {
    const opportunities = [];
    const queries = topics.map(t => t.replace(/\s+/g, '+'));
    for (const q of queries.slice(0, 4)) {
        try {
            const data = await fetchJson(`https://hn.algolia.com/api/v1/search_by_date?query=${q}&tags=(story,show_hn,ask_hn)&hitsPerPage=5`);
            for (const hit of (data.hits || [])) {
                if (hit.points >= 1) {
                    opportunities.push({
                        source: 'hn',
                        title: hit.title,
                        url: `https://news.ycombinator.com/item?id=${hit.objectID}`,
                        snippet: hit.title,
                        foundAt: new Date().toISOString(),
                    });
                }
            }
        }
        catch { /* skip failed queries */ }
    }
    // Also search comments for questions/requests
    for (const q of queries.slice(0, 2)) {
        try {
            const data = await fetchJson(`https://hn.algolia.com/api/v1/search_by_date?query=${q}&tags=comment&hitsPerPage=3`);
            for (const hit of (data.hits || [])) {
                if (hit.comment_text?.length > 50) {
                    opportunities.push({
                        source: 'hn',
                        title: hit.story_title || 'HN thread',
                        url: `https://news.ycombinator.com/item?id=${hit.objectID}`,
                        snippet: hit.comment_text.slice(0, 200),
                        foundAt: new Date().toISOString(),
                    });
                }
            }
        }
        catch { /* skip */ }
    }
    return opportunities;
}
async function scanGitHub(topics) {
    const opportunities = [];
    for (const topic of topics.slice(0, 3)) {
        try {
            const q = encodeURIComponent(`${topic} in:title,body state:open`);
            const data = await fetchJson(`https://api.github.com/search/issues?q=${q}&sort=created&order=desc&per_page=5`);
            for (const item of (data.items || [])) {
                opportunities.push({
                    source: 'github',
                    title: item.title,
                    url: item.html_url,
                    snippet: (item.body || '').slice(0, 200),
                    foundAt: new Date().toISOString(),
                });
            }
        }
        catch { /* skip */ }
    }
    return opportunities;
}
async function scanReddit(topics) {
    const opportunities = [];
    const subs = ['programming', 'commandline', 'artificial', 'LocalLLaMA', 'ChatGPTCoding'];
    for (const sub of subs.slice(0, 3)) {
        try {
            const data = await fetchJson(`https://www.reddit.com/r/${sub}/new.json?limit=5`);
            for (const post of (data.data?.children || [])) {
                const d = post.data;
                const matchesTopic = topics.some(t => d.title.toLowerCase().includes(t.toLowerCase()) ||
                    d.selftext.toLowerCase().includes(t.toLowerCase()));
                if (matchesTopic) {
                    opportunities.push({
                        source: 'reddit',
                        title: d.title,
                        url: `https://www.reddit.com${d.permalink}`,
                        snippet: d.selftext.slice(0, 200),
                        foundAt: new Date().toISOString(),
                    });
                }
            }
        }
        catch { /* skip */ }
    }
    return opportunities;
}
// ── Analysis (Local Ollama) ──
async function analyzeOpportunity(opp, config) {
    const prompt = `You are evaluating whether to comment on this post. You represent the project "${config.projectName}" — ${config.projectDescription}.

Post title: "${opp.title}"
Post snippet: "${opp.snippet}"
Platform: ${opp.source}

Rules:
- Only respond if you have GENUINE technical insight to add
- Never be promotional — no "check out my tool" or "we built something similar"
- Share specific technical knowledge, ask good questions, or add nuance
- If the post is about medical, legal, financial, or political topics → NOT RELEVANT
- If you can't add real value → NOT RELEVANT

Reply in this EXACT JSON format (no markdown, no explanation):
{"relevant": true/false, "reasoning": "why", "draft": "your comment text"}`;
    try {
        const res = await fetch(`${config.ollamaUrl}/api/generate`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                model: config.ollamaModel,
                prompt,
                stream: false,
                options: { num_predict: 500, temperature: 0.7 },
            }),
        });
        if (!res.ok)
            return { relevant: false, draft: '', reasoning: 'Ollama unavailable' };
        const data = await res.json();
        const raw = (data.response || '')
            .replace(/<think>[\s\S]*?<\/think>/g, '')
            .trim();
        const jsonMatch = raw.match(/\{[\s\S]*\}/);
        if (jsonMatch) {
            const parsed = JSON.parse(jsonMatch[0]);
            return {
                relevant: !!parsed.relevant,
                draft: parsed.draft || '',
                reasoning: parsed.reasoning || '',
            };
        }
    }
    catch { /* analysis failed */ }
    return { relevant: false, draft: '', reasoning: 'Analysis failed' };
}
// ── Posting ──
async function postToHN(url, comment, config) {
    if (!config.hnCookie)
        return false;
    const idMatch = url.match(/id=(\d+)/);
    if (!idMatch)
        return false;
    const itemId = idMatch[1];
    try {
        // Fetch page for HMAC token
        const pageRes = await fetch(`https://news.ycombinator.com/item?id=${itemId}`, {
            headers: { 'Cookie': config.hnCookie },
        });
        const html = await pageRes.text();
        // Verify logged in
        if (config.hnUsername && !html.includes(config.hnUsername)) {
            log('HN cookie expired — run `kbot discovery auth` to refresh');
            return false;
        }
        // Extract HMAC
        const hmacMatch = html.match(/name="hmac"\s+value="([^"]+)"/)
            || html.match(/value="([^"]+)"\s+name="hmac"/)
            || html.match(/hmac.*?value="([a-f0-9]{40,})"/);
        if (!hmacMatch) {
            log('No HMAC found on HN page');
            return false;
        }
        // Post comment
        const postRes = await fetch('https://news.ycombinator.com/comment', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Cookie': config.hnCookie,
                'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
                'Origin': 'https://news.ycombinator.com',
                'Referer': `https://news.ycombinator.com/item?id=${itemId}`,
            },
            body: `parent=${itemId}&goto=item%3Fid%3D${itemId}&hmac=${encodeURIComponent(hmacMatch[1])}&text=${encodeURIComponent(comment)}`,
            redirect: 'manual',
        });
        return postRes.status === 302 || postRes.status === 200;
    }
    catch {
        return false;
    }
}
async function postToGitHub(url, comment) {
    // Use gh CLI — already authenticated
    const match = url.match(/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)/);
    if (!match)
        return false;
    try {
        execSync(`gh issue comment ${match[2]} --repo ${match[1]} --body "${comment.replace(/"/g, '\\"')}"`, {
            timeout: 15000, stdio: 'pipe',
        });
        return true;
    }
    catch {
        return false;
    }
}
// ── Main Discovery Loop ──
export async function runDiscoveryCycle(config) {
    const state = loadState();
    state.totalScans++;
    state.lastScan = new Date().toISOString();
    log(`Scan #${state.totalScans} — searching ${config.topics.join(', ')}...`);
    // Scan all platforms
    const [hnOpps, ghOpps, redditOpps] = await Promise.allSettled([
        scanHN(config.topics),
        scanGitHub(config.topics),
        scanReddit(config.topics),
    ]);
    const allOpps = [
        ...(hnOpps.status === 'fulfilled' ? hnOpps.value : []),
        ...(ghOpps.status === 'fulfilled' ? ghOpps.value : []),
        ...(redditOpps.status === 'fulfilled' ? redditOpps.value : []),
    ];
    // Dedup by URL
    const seen = new Set(state.posts.map(p => p.url));
    const fresh = allOpps.filter(o => !seen.has(o.url));
    log(`Found ${allOpps.length} total, ${fresh.length} new`);
    state.totalFound += fresh.length;
    let posted = 0;
    let skipped = 0;
    for (const opp of fresh.slice(0, config.maxPostsPerCycle * 3)) {
        if (posted >= config.maxPostsPerCycle)
            break;
        const analysis = await analyzeOpportunity(opp, config);
        if (!analysis.relevant) {
            log(`  SKIP: ${opp.title.slice(0, 50)} — ${analysis.reasoning.slice(0, 60)}`);
            skipped++;
            state.totalSkipped++;
            continue;
        }
        log(`  RELEVANT: ${opp.title.slice(0, 50)}`);
        log(`  Draft: ${analysis.draft.slice(0, 80)}...`);
        if (config.dryRun) {
            log('  (dry run — not posting)');
            state.posts.push({
                timestamp: new Date().toISOString(),
                url: opp.url, title: opp.title,
                comment: analysis.draft, platform: opp.source,
                success: false, error: 'dry_run',
            });
            continue;
        }
        // Post
        let success = false;
        let error;
        if (opp.source === 'hn') {
            success = await postToHN(opp.url, analysis.draft, config);
            if (!success)
                error = 'hn_post_failed';
        }
        else if (opp.source === 'github') {
            success = await postToGitHub(opp.url, analysis.draft);
            if (!success)
                error = 'github_post_failed';
        }
        else {
            error = 'platform_unsupported';
        }
        state.posts.push({
            timestamp: new Date().toISOString(),
            url: opp.url, title: opp.title,
            comment: analysis.draft, platform: opp.source,
            success, error,
        });
        if (success) {
            posted++;
            state.totalPosted++;
            log(`  POSTED to ${opp.source}: ${opp.title.slice(0, 50)}`);
        }
        else {
            log(`  FAILED: ${error}`);
        }
        // Cooldown between posts (10 seconds)
        if (posted < config.maxPostsPerCycle) {
            await new Promise(r => setTimeout(r, 10000));
        }
    }
    // Keep only last 200 post records
    if (state.posts.length > 200)
        state.posts = state.posts.slice(-200);
    saveState(state);
    log(`Cycle done: ${fresh.length} found, ${posted} posted, ${skipped} skipped`);
    return { found: fresh.length, posted, skipped };
}
export function getDiscoveryState() {
    return loadState();
}
export function getRecentLog(lines = 20) {
    if (!existsSync(LOG_FILE))
        return 'No activity yet.';
    const content = readFileSync(LOG_FILE, 'utf8');
    return content.split('\n').filter(Boolean).slice(-lines).join('\n');
}
//# sourceMappingURL=discovery.js.map