"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const child_process_1 = require("child_process");
const job_store_1 = require("./job-store");
const agent_runner_1 = require("./agent-runner");
const agent_session_runner_1 = require("./agent-session-runner");
const agents_1 = require("./agents");
const security_1 = require("./tools/security");
const orchestrator_1 = require("./orchestrator");
const atlas_1 = require("./atlas");
const llm_1 = require("./llm");
const app = (0, express_1.default)();
app.use((0, cors_1.default)());
const startTime = new Date();
// Raw body capture for webhook HMAC — must come before express.json()
app.use('/webhook/gitea', express_1.default.raw({ type: '*/*' }));
app.use(express_1.default.json());
const PORT = process.env.PORT || 3333;
// ---------------------------------------------------------------------------
// Build ToolContext from environment variables
// ---------------------------------------------------------------------------
function ensureWorkspace(repo) {
    const base = process.env.WORKSPACE_BASE || '/workspaces';
    if (!repo) {
        const dir = path.join(base, 'default');
        fs.mkdirSync(dir, { recursive: true });
        return dir;
    }
    if (security_1.PROTECTED_GITEA_REPOS.has(repo)) {
        throw new Error(`SECURITY: Repo "${repo}" is a protected Vibn platform repo. ` +
            `Agents cannot clone or work in this workspace.`);
    }
    const dir = path.join(base, repo.replace('/', '_'));
    const gitea = {
        apiUrl: process.env.GITEA_API_URL || '',
        apiToken: process.env.GITEA_API_TOKEN || '',
        username: process.env.GITEA_USERNAME || ''
    };
    if (!fs.existsSync(path.join(dir, '.git'))) {
        fs.mkdirSync(dir, { recursive: true });
        const authedUrl = `${gitea.apiUrl}/${repo}.git`
            .replace('https://', `https://${gitea.username}:${gitea.apiToken}@`);
        try {
            (0, child_process_1.execSync)(`git clone "${authedUrl}" "${dir}"`, { stdio: 'pipe' });
        }
        catch {
            // Repo may not exist yet — just init
            (0, child_process_1.execSync)(`git init`, { cwd: dir, stdio: 'pipe' });
            (0, child_process_1.execSync)(`git remote add origin "${authedUrl}"`, { cwd: dir, stdio: 'pipe' });
        }
    }
    return dir;
}
function buildContext(repo) {
    const workspaceRoot = ensureWorkspace(repo);
    return {
        workspaceRoot,
        gitea: {
            apiUrl: process.env.GITEA_API_URL || '',
            apiToken: process.env.GITEA_API_TOKEN || '',
            username: process.env.GITEA_USERNAME || ''
        },
        coolify: {
            apiUrl: process.env.COOLIFY_API_URL || '',
            apiToken: process.env.COOLIFY_API_TOKEN || ''
        },
        memoryUpdates: []
    };
}
// ---------------------------------------------------------------------------
// Routes
// ---------------------------------------------------------------------------
// Health check
app.get('/health', (_req, res) => {
    res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// ---------------------------------------------------------------------------
// GitHub mirror — clone a public GitHub repo and push to Gitea as-is
// ---------------------------------------------------------------------------
app.post('/api/mirror', async (req, res) => {
    const { github_url, gitea_repo, project_name, github_token } = req.body;
    if (!github_url || !gitea_repo) {
        res.status(400).json({ error: '"github_url" and "gitea_repo" are required' });
        return;
    }
    const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
    const fs = await Promise.resolve().then(() => __importStar(require('fs')));
    const path = await Promise.resolve().then(() => __importStar(require('path')));
    const os = await Promise.resolve().then(() => __importStar(require('os')));
    const mirrorId = `mirror_${Date.now()}`;
    const tmpDir = path.join(os.tmpdir(), mirrorId);
    const gitea = {
        apiUrl: process.env.GITEA_API_URL || '',
        apiToken: process.env.GITEA_API_TOKEN || '',
        username: process.env.GITEA_USERNAME || ''
    };
    try {
        // Build authenticated Gitea push URL
        // GITEA_API_URL is like https://git.vibnai.com — strip /api/v1 if present
        const giteaBase = gitea.apiUrl.replace(/\/api\/v1\/?$/, '');
        const authedPushUrl = `${giteaBase}/${gitea_repo}.git`
            .replace('https://', `https://${gitea.username}:${gitea.apiToken}@`);
        console.log(`[mirror] Cloning ${github_url} → ${tmpDir}`);
        fs.mkdirSync(tmpDir, { recursive: true });
        // Build authenticated clone URL for private repos
        let cloneUrl = github_url;
        if (github_token) {
            cloneUrl = github_url.replace('https://', `https://${github_token}@`);
        }
        // Mirror-clone the GitHub repo (preserves all branches + tags)
        execSync(`git clone --mirror "${cloneUrl}" "${tmpDir}/.git"`, {
            stdio: 'pipe',
            timeout: 120000
        });
        execSync(`git config --bool core.bare false`, { cwd: tmpDir, stdio: 'pipe' });
        execSync(`git checkout`, { cwd: tmpDir, stdio: 'pipe' });
        // Point origin at Gitea and push all refs
        execSync(`git remote set-url origin "${authedPushUrl}"`, { cwd: tmpDir, stdio: 'pipe' });
        execSync(`git push --mirror origin`, { cwd: tmpDir, stdio: 'pipe', timeout: 120000 });
        console.log(`[mirror] Pushed ${gitea_repo} successfully`);
        res.json({ success: true, gitea_repo, github_url });
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        console.error(`[mirror] Failed:`, msg);
        res.status(500).json({ error: 'Mirror failed', details: msg });
    }
    finally {
        // Clean up temp dir
        try {
            const { execSync: rm } = await Promise.resolve().then(() => __importStar(require('child_process')));
            rm(`rm -rf "${tmpDir}"`, { stdio: 'pipe' });
        }
        catch { /* best effort */ }
    }
});
// List available agents
app.get('/api/agents', (_req, res) => {
    const agents = Object.values(agents_1.AGENTS).map(a => ({
        name: a.name,
        description: a.description,
        tools: a.tools.map(t => t.name)
    }));
    res.json(agents);
});
// Get server status and job statistics
app.get('/api/status', (_req, res) => {
    const allJobs = (0, job_store_1.listJobs)(Infinity);
    const total_jobs = allJobs.length;
    const by_status = {
        queued: 0,
        running: 0,
        completed: 0,
        failed: 0,
    };
    for (const job of allJobs) {
        by_status[job.status] = (by_status[job.status] || 0) + 1;
    }
    const uptime_seconds = Math.floor((new Date().getTime() - startTime.getTime()) / 1000);
    const agents = Object.values(agents_1.AGENTS).map(a => a.name);
    res.json({
        total_jobs,
        by_status,
        uptime_seconds,
        agents,
    });
});
// Submit a new job
app.post('/api/agent/run', async (req, res) => {
    const { agent: agentName, task, repo } = req.body;
    if (!agentName || !task) {
        res.status(400).json({ error: '"agent" and "task" are required' });
        return;
    }
    const agentConfig = agents_1.AGENTS[agentName];
    if (!agentConfig) {
        const available = Object.keys(agents_1.AGENTS).join(', ');
        res.status(400).json({ error: `Unknown agent "${agentName}". Available: ${available}` });
        return;
    }
    const job = (0, job_store_1.createJob)(agentName, task, repo);
    res.status(202).json({ jobId: job.id, status: job.status });
    // Run agent asynchronously
    const ctx = buildContext(repo);
    (0, agent_runner_1.runAgent)(job, agentConfig, task, ctx)
        .then(result => {
        (0, job_store_1.updateJob)(job.id, {
            status: 'completed',
            result: result.finalText,
            progress: `Done — ${result.turns} turns, ${result.toolCallCount} tool calls`
        });
    })
        .catch(err => {
        (0, job_store_1.updateJob)(job.id, {
            status: 'failed',
            error: err instanceof Error ? err.message : String(err),
            progress: 'Agent failed'
        });
    });
});
// Check job status
app.get('/api/jobs/:id', (req, res) => {
    const job = (0, job_store_1.getJob)(req.params.id);
    if (!job) {
        res.status(404).json({ error: 'Job not found' });
        return;
    }
    res.json(job);
});
// ---------------------------------------------------------------------------
// Orchestrator — persistent chat with full project context
// ---------------------------------------------------------------------------
app.post('/orchestrator/chat', async (req, res) => {
    const { message, session_id, history, knowledge_context } = req.body;
    if (!message) {
        res.status(400).json({ error: '"message" is required' });
        return;
    }
    const sessionId = session_id || `session_${Date.now()}`;
    const ctx = buildContext();
    try {
        const result = await (0, orchestrator_1.orchestratorChat)(sessionId, message, ctx, {
            preloadedHistory: history,
            knowledgeContext: knowledge_context
        });
        res.json(result);
    }
    catch (err) {
        res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
    }
});
app.get('/orchestrator/sessions', (_req, res) => {
    res.json((0, orchestrator_1.listSessions)());
});
app.delete('/orchestrator/sessions/:id', (req, res) => {
    (0, orchestrator_1.clearSession)(req.params.id);
    res.json({ cleared: req.params.id });
});
// ---------------------------------------------------------------------------
// Atlas — PRD discovery agent
// ---------------------------------------------------------------------------
app.post('/atlas/chat', async (req, res) => {
    const { message, session_id, history, is_init, } = req.body;
    if (!message) {
        res.status(400).json({ error: '"message" is required' });
        return;
    }
    const sessionId = session_id || `atlas_${Date.now()}`;
    const ctx = buildContext();
    try {
        const result = await (0, atlas_1.atlasChat)(sessionId, message, ctx, {
            preloadedHistory: history,
            isInit: is_init,
        });
        res.json(result);
    }
    catch (err) {
        res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
    }
});
app.get('/atlas/sessions', (_req, res) => {
    res.json((0, atlas_1.listAtlasSessions)());
});
app.delete('/atlas/sessions/:id', (req, res) => {
    (0, atlas_1.clearAtlasSession)(req.params.id);
    res.json({ cleared: req.params.id });
});
// List recent jobs
app.get('/api/jobs', (req, res) => {
    const limit = parseInt(req.query.limit || '20', 10);
    res.json((0, job_store_1.listJobs)(limit));
});
// Gitea webhook endpoint — triggers agent from an issue event
app.post('/webhook/gitea', (req, res) => {
    const event = req.headers['x-gitea-event'];
    const rawBody = req.body;
    // Verify HMAC-SHA256 signature
    const webhookSecret = process.env.WEBHOOK_SECRET;
    if (webhookSecret) {
        const sig = req.headers['x-gitea-signature'];
        const expected = crypto
            .createHmac('sha256', webhookSecret)
            .update(rawBody)
            .digest('hex');
        if (!sig || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
            res.status(401).json({ error: 'Invalid webhook signature' });
            return;
        }
    }
    const body = JSON.parse(rawBody.toString('utf8'));
    let task = null;
    let agentName = 'Coder';
    let repo;
    if (event === 'issues' && body.action === 'opened') {
        const issue = body.issue;
        repo = `${body.repository?.owner?.login}/${body.repository?.name}`;
        const labels = (issue.labels || []).map((l) => l.name);
        if (labels.includes('agent:pm')) {
            agentName = 'PM';
        }
        else if (labels.includes('agent:marketing')) {
            agentName = 'Marketing';
        }
        else if (labels.includes('agent:coder')) {
            agentName = 'Coder';
        }
        else {
            // No agent label — ignore
            res.json({ ignored: true, reason: 'no agent label on issue' });
            return;
        }
        task = `You have been assigned to resolve a Gitea issue in the repo ${repo}.\n\nIssue #${issue.number}: ${issue.title}\n\nDescription:\n${issue.body || '(no description)'}\n\nWhen done, close the issue by calling gitea_close_issue.`;
    }
    else if (event === 'push') {
        res.json({ ignored: true, reason: 'push events not auto-processed' });
        return;
    }
    else {
        res.json({ ignored: true, event });
        return;
    }
    if (!task) {
        res.json({ ignored: true });
        return;
    }
    const agentConfig = agents_1.AGENTS[agentName];
    const job = (0, job_store_1.createJob)(agentName, task, repo);
    res.status(202).json({ jobId: job.id, agent: agentName, event });
    const ctx = buildContext(repo);
    (0, agent_runner_1.runAgent)(job, agentConfig, task, ctx)
        .then(result => {
        (0, job_store_1.updateJob)(job.id, {
            status: 'completed',
            result: result.finalText,
            progress: `Done — ${result.turns} turns, ${result.toolCallCount} tool calls`
        });
    })
        .catch(err => {
        (0, job_store_1.updateJob)(job.id, {
            status: 'failed',
            error: err instanceof Error ? err.message : String(err),
            progress: 'Agent failed'
        });
    });
});
// ---------------------------------------------------------------------------
// Agent Execute — VIBN Build > Code > Agent tab
//
// Receives a task from the VIBN frontend, runs the Coder agent against
// the project's Gitea repo, and streams progress back to the VIBN DB
// via PATCH /api/projects/[id]/agent/sessions/[sid].
//
// This endpoint returns immediately (202) and runs the agent async so
// the browser can close without killing the loop.
// ---------------------------------------------------------------------------
// Track active sessions for stop support
const activeSessions = new Map();
app.post('/agent/execute', async (req, res) => {
    const { sessionId, projectId, appName, appPath, giteaRepo, task, continueTask, autoApprove, coolifyAppUuid, } = req.body;
    if (!sessionId || !projectId || !appPath || !task) {
        res.status(400).json({ error: 'sessionId, projectId, appPath and task are required' });
        return;
    }
    const vibnApiUrl = process.env.VIBN_API_URL ?? 'https://vibnai.com';
    // Register session as active
    const sessionState = { stopped: false };
    activeSessions.set(sessionId, sessionState);
    // Respond immediately — execution is async
    res.status(202).json({ sessionId, status: 'running' });
    // Build workspace context — clone/update the Gitea repo if provided
    let ctx;
    try {
        ctx = buildContext(giteaRepo);
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        console.error('[agent/execute] buildContext failed:', msg);
        // Notify VIBN DB of failure
        fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ status: 'failed', error: msg }),
        }).catch(() => { });
        activeSessions.delete(sessionId);
        return;
    }
    // Capture repo root before scoping to appPath — needed for git commit in auto-approve
    const repoRoot = ctx.workspaceRoot;
    // Scope workspace to the app subdirectory so the agent works there naturally
    if (appPath) {
        const path = require('path');
        ctx.workspaceRoot = path.join(ctx.workspaceRoot, appPath);
        const fs = require('fs');
        fs.mkdirSync(ctx.workspaceRoot, { recursive: true });
    }
    const agentConfig = agents_1.AGENTS['Coder'];
    if (!agentConfig) {
        fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ status: 'failed', error: 'Coder agent not registered' }),
        }).catch(() => { });
        activeSessions.delete(sessionId);
        return;
    }
    // If continuing a previous task, combine into a single prompt so the agent
    // understands what was already attempted.
    const effectiveTask = continueTask
        ? `Original task: ${task}\n\nFollow-up instruction: ${continueTask}`
        : task;
    // Run the streaming agent loop (fire and forget)
    (0, agent_session_runner_1.runSessionAgent)(agentConfig, effectiveTask, ctx, {
        sessionId,
        projectId,
        vibnApiUrl,
        appPath,
        repoRoot,
        isStopped: () => sessionState.stopped,
        autoApprove: autoApprove ?? true,
        giteaRepo,
        coolifyAppUuid,
        coolifyApiUrl: process.env.COOLIFY_API_URL,
        coolifyApiToken: process.env.COOLIFY_API_TOKEN,
    })
        .catch(err => {
        const msg = err instanceof Error ? err.message : String(err);
        console.error(`[agent/execute] session ${sessionId} crashed:`, msg);
        fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ status: 'failed', error: msg }),
        }).catch(() => { });
    })
        .finally(() => {
        activeSessions.delete(sessionId);
    });
});
app.post('/agent/stop', (req, res) => {
    const { sessionId } = req.body;
    if (!sessionId) {
        res.status(400).json({ error: 'sessionId required' });
        return;
    }
    const session = activeSessions.get(sessionId);
    if (session) {
        session.stopped = true;
        res.json({ ok: true, message: 'Stop signal sent — agent will halt after current step.' });
    }
    else {
        res.json({ ok: true, message: 'Session not active (may have already completed).' });
    }
});
// ---------------------------------------------------------------------------
// Agent Approve — commit and push agent's changes to Gitea, trigger deploy
//
// Called by vibn-frontend after the user reviews changed files and clicks
// "Approve & commit". The agent runner does git add/commit/push in the
// workspace where the agent was working.
// ---------------------------------------------------------------------------
app.post('/agent/approve', async (req, res) => {
    const { giteaRepo, commitMessage, coolifyApiUrl, coolifyApiToken, coolifyAppUuid } = req.body;
    if (!giteaRepo || !commitMessage) {
        res.status(400).json({ error: 'giteaRepo and commitMessage are required' });
        return;
    }
    try {
        // Resolve the workspace root for this repo (does NOT re-clone if already present)
        const workspaceRoot = ensureWorkspace(giteaRepo);
        // Configure git identity for this commit
        const gitea = {
            username: process.env.GITEA_USERNAME || 'agent',
            apiToken: process.env.GITEA_API_TOKEN || '',
            apiUrl: process.env.GITEA_API_URL || '',
        };
        const { execSync: exec } = require('child_process');
        const gitOpts = { cwd: workspaceRoot, stdio: 'pipe' };
        // Ensure git identity
        try {
            exec('git config user.email "agent@vibnai.com"', gitOpts);
            exec('git config user.name "VIBN Agent"', gitOpts);
        }
        catch { /* already set */ }
        // Stage all changes
        exec('git add -A', gitOpts);
        // Check if there is anything to commit
        let status;
        try {
            status = exec('git status --porcelain', gitOpts).toString().trim();
        }
        catch {
            status = '';
        }
        if (!status) {
            res.json({ ok: true, committed: false, message: 'Nothing to commit — working tree is clean.' });
            return;
        }
        // Commit
        exec(`git commit -m ${JSON.stringify(commitMessage)}`, gitOpts);
        // Push — use token auth embedded in remote URL
        const authedUrl = `${gitea.apiUrl}/${giteaRepo}.git`
            .replace('https://', `https://${gitea.username}:${gitea.apiToken}@`);
        exec(`git push "${authedUrl}" HEAD:main`, gitOpts);
        // Optionally trigger a Coolify redeploy
        let deployed = false;
        if (coolifyApiUrl && coolifyApiToken && coolifyAppUuid) {
            try {
                const deployRes = await fetch(`${coolifyApiUrl}/api/v1/applications/${coolifyAppUuid}/start`, {
                    method: 'POST',
                    headers: { Authorization: `Bearer ${coolifyApiToken}` },
                });
                deployed = deployRes.ok;
            }
            catch { /* deploy trigger is best-effort */ }
        }
        res.json({ ok: true, committed: true, deployed, message: `Committed and pushed: "${commitMessage}"` });
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        console.error('[agent/approve]', msg);
        res.status(500).json({ error: msg });
    }
});
// ---------------------------------------------------------------------------
// Generate — thin structured-generation endpoint (no session, no system prompt)
// Use this for one-shot tasks like architecture recommendations.
// ---------------------------------------------------------------------------
app.post('/generate', async (req, res) => {
    const { prompt, model, region } = req.body;
    if (!prompt) {
        res.status(400).json({ error: '"prompt" is required' });
        return;
    }
    // Allow overriding CLAUDE_REGION per-request for testing
    const prevRegion = process.env.CLAUDE_REGION;
    if (region)
        process.env.CLAUDE_REGION = region;
    try {
        const llm = (0, llm_1.createLLM)(model ?? 'A', { temperature: 0.3 });
        const messages = [
            { role: 'user', content: prompt }
        ];
        const response = await llm.chat(messages, [], 8192);
        res.json({ reply: response.content ?? '', model: llm.modelId });
    }
    catch (err) {
        res.status(500).json({ error: err instanceof Error ? err.message : String(err), model });
    }
    finally {
        if (region)
            process.env.CLAUDE_REGION = prevRegion ?? '';
    }
});
// ---------------------------------------------------------------------------
// Error handler
// ---------------------------------------------------------------------------
app.use((err, _req, res, _next) => {
    console.error(err.stack);
    res.status(500).json({ error: err.message });
});
// ---------------------------------------------------------------------------
// Start
// ---------------------------------------------------------------------------
app.listen(PORT, () => {
    console.log(`AgentRunner listening on port ${PORT}`);
    console.log(`Agents available: ${Object.keys(agents_1.AGENTS).join(', ')}`);
    if (!process.env.GOOGLE_API_KEY) {
        console.warn('WARNING: GOOGLE_API_KEY is not set — agents will fail');
    }
});
