/**
 * API Client for Vigthoria Backend
 * Connects to coder.vigthoria.io API endpoints
 */
import axios from 'axios';
import { execFile } from 'child_process';
import { randomUUID } from 'crypto';
import fs from 'fs';
import https from 'https';
import net from 'net';
import path from 'path';
import WebSocket from 'ws';
import { globSync } from 'glob';
import { buildSemanticContext } from './context-ranker.js';
import { getChangedFiles } from './workspace-cache.js';
import { isSubstantiveAgentAnswer, isToolEvidenceStubAnswer } from './agentRunOutcome.js';
import { isV3WorkspaceRootAliasPath, joinV3WorkspacePath, normalizeV3WorkspaceRelativePath } from './v3-workspace-path.js';
import { buildExecutionHints, isPlannerBuildTask, resolvePlannerAgentTimeoutMs } from './requestIntent.js';
import { isV3StreamKeepaliveEvent } from './v3-stream-events.js';
import { resolveV3ContextCharLimit, WORKSPACE_FILE_CHAR_CAP, } from './contextBudget.js';
import { isLocalTestfarmMode, fetchWithServiceTimeout } from './localTestMode.js';
import { buildClientManifest } from './clientManifest.js';
export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
export const VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE = 'Vigthoria Server is temporarily not available. Please try again later. If the issue persists, please contact support.';
export class CLIError extends Error {
    category;
    statusCode;
    endpoint;
    walletUrl;
    topupUrl;
    balance;
    constructor(message, category, opts) {
        super(message);
        this.name = 'CLIError';
        this.category = category;
        this.statusCode = opts?.statusCode;
        this.endpoint = opts?.endpoint;
        this.walletUrl = opts?.walletUrl;
        this.topupUrl = opts?.topupUrl;
        this.balance = opts?.balance;
        if (opts?.cause)
            this.cause = opts.cause;
    }
}
/** Classify an axios or fetch error into a structured CLIError. */
export function classifyError(error, fallbackCategory = 'network') {
    if (error instanceof CLIError)
        return error;
    const axErr = error;
    const status = axErr?.response?.status;
    const endpoint = axErr?.config?.url || axErr?.config?.baseURL || '';
    const responseData = axErr?.response?.data;
    const nestedError = responseData?.error;
    const message = responseData
        ? typeof nestedError === 'string'
            ? nestedError
            : typeof responseData.message === 'string'
                ? responseData.message
                : typeof nestedError === 'object' && nestedError && typeof nestedError.message === 'string'
                    ? nestedError.message
                    : error.message
        : error.message || String(error);
    if (status === 402 || responseData?.errorCode === 'INSUFFICIENT_CREDITS' || responseData?.errorCode === 'WALLET_REQUIRED' || responseData?.errorCode === 'CLOUD_CREDIT_REQUIRED') {
        const walletUrl = String(responseData?.walletUrl || responseData?.topupUrl || VIGTHORIA_HUB_CREDITS_URL);
        const balance = typeof responseData?.balance === 'number' ? responseData.balance : undefined;
        const creditMessage = typeof responseData?.message === 'string'
            ? responseData.message
            : `Your cloud credit balance is too low. Add credits: ${walletUrl}`;
        return new CLIError(creditMessage, 'credits', { statusCode: status || 402, endpoint, walletUrl, topupUrl: walletUrl, balance });
    }
    if (status === 401 || status === 403) {
        // Distinguish repo/community auth from model auth
        if (/community|repo/i.test(endpoint)) {
            return new CLIError(message, 'repo_session', { statusCode: status, endpoint });
        }
        return new CLIError(message, 'auth', { statusCode: status, endpoint });
    }
    if (status && status >= 500) {
        return new CLIError(message, 'model_backend', { statusCode: status, endpoint });
    }
    if (/timeout|ETIMEDOUT|ESOCKETTIMEDOUT|aborted/i.test(message)) {
        return new CLIError(message, 'timeout', { endpoint });
    }
    if (/ECONNREFUSED|ENOTFOUND|ENETUNREACH|EAI_AGAIN|fetch failed/i.test(message)) {
        return new CLIError(message, 'network', { endpoint });
    }
    return new CLIError(message, fallbackCategory, { statusCode: status, endpoint, cause: error instanceof Error ? error : undefined });
}
/** Format a CLIError for user-facing display. */
export function formatCLIError(err) {
    const tag = `[${err.category}]`;
    switch (err.category) {
        case 'auth':
            return `${tag} Authentication failed${err.statusCode ? ` (${err.statusCode})` : ''}. Run: vigthoria login`;
        case 'repo_session':
            return `${tag} Repository session expired or missing${err.statusCode ? ` (${err.statusCode})` : ''}. This does not affect AI commands. Re-authenticate repo with: vigthoria repo list`;
        case 'model_backend':
            return VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE;
        case 'bridge':
            return `${tag} Bridge connection error: ${err.message}`;
        case 'network':
            return `${tag} Network error: ${err.message}. Check your internet connection.`;
        case 'timeout':
            return `${tag} Request timed out: ${err.message}`;
        case 'parsing':
            return `${tag} Response parsing error: ${err.message}`;
        case 'tool_execution':
            return `${tag} Tool execution error: ${err.message}`;
        case 'credits':
            return `${tag} ${err.message}${err.walletUrl ? `\nTop up credits: ${err.walletUrl}` : ''}`;
        default:
            return `${tag} ${err.message}`;
    }
}
export function sanitizeUserFacingPathText(input) {
    let text = String(input || '');
    if (!text) {
        return text;
    }
    // Path token used for "<service>/<sub>" captures.  Excludes whitespace and
    // any character that commonly terminates a path in CLI output.
    const pathToken = String.raw `[^\s"'\)\]\}<>]+`;
    // The replacements are applied in order — more specific patterns first.
    // Every entry must cover the case WITH AND WITHOUT a trailing slash so
    // bracketed paths like `[/tmp/vig-remote-xyz]` are also caught.
    const replacements = [
        // ── Temp-workspace directories used for remote (Windows/macOS) clients
        [new RegExp(String.raw `/var/www/\.vigthoria/v3-temp/vig-remote-[A-Za-z0-9._-]+(?:/${pathToken})?`, 'gi'), 'workspace'],
        [new RegExp(String.raw `\.vigthoria/v3-temp/vig-remote-[A-Za-z0-9._-]+(?:/${pathToken})?`, 'gi'), 'workspace'],
        [new RegExp(String.raw `/tmp/vig-remote(?:-server)?-[A-Za-z0-9._-]+(?:/${pathToken})?`, 'gi'), 'workspace'],
        [new RegExp(String.raw `/var/tmp/vig-remote(?:-server)?-[A-Za-z0-9._-]+(?:/${pathToken})?`, 'gi'), 'workspace'],
        [new RegExp(String.raw `/tmp/vig-fork-[A-Za-z0-9._-]+(?:/${pathToken})?`, 'gi'), 'workspace'],
        // ── Bare temp basenames after their /tmp/ prefix is already gone
        [/\bvig-remote(?:-server)?-[A-Za-z0-9._-]+/gi, 'workspace'],
        [/\bvig-fork-[A-Za-z0-9._-]+/gi, 'workspace'],
        // ── V3 Code Agent installation tree and sibling services
        [new RegExp(String.raw `/var/www/V3-Code-Agent(?:/${pathToken})?`, 'gi'), '[Vigthoria Agent]'],
        [new RegExp(String.raw `/var/www/(?:agent-vigthoria|vigthoria-model-router|vigthoria-devtools-bridge|vigthoria-hyper-loop|vigthoria-mcp-server|vigthoria-template-service|vigthoria-security-devops|vigthoria-coder|vigthoria-code|Vigthoria-Code-2|vigthoria-asset-storage|vigthoria-storage-service|vigthoria-hosted)(?:/${pathToken})?`, 'gi'), '[Vigthoria service]'],
        // ── User workspace mount + long-term storage backing path
        [new RegExp(String.raw `/var/www/(?:vigthoria-user-workspaces|\.vigthoria)(?:/${pathToken})?`, 'gi'), 'workspace'],
        [new RegExp(String.raw `/var/lib/vigthoria-workspaces(?:/${pathToken})?`, 'gi'), 'workspace'],
        [new RegExp(String.raw `/mnt/storage-box/vigthoria(?:/${pathToken})?`, 'gi'), '[Vigthoria storage]'],
        // ── Home / root leaks
        [/\/home\/[A-Za-z0-9._-]+/gi, '[home]'],
        [new RegExp(String.raw `/root(?:/${pathToken})?`, 'gi'), '[home]'],
        // ── Backstop: any other /var/www tree
        [new RegExp(String.raw `/var/www(?:/${pathToken})?`, 'gi'), '[Vigthoria service]'],
        // ── Generic temp roots (after the more specific vig- patterns above)
        [new RegExp(String.raw `/tmp(?:/${pathToken})?`, 'gi'), '[temp]'],
        [new RegExp(String.raw `/var/tmp(?:/${pathToken})?`, 'gi'), '[temp]'],
    ];
    text = text.replace(/\\/g, '/');
    for (const [pattern, replacement] of replacements) {
        text = text.replace(pattern, replacement);
    }
    // Normalise abstract boundary URIs for human-friendly display.
    text = text
        .replace(/vigthoria:\/\/workspace\/?/gi, 'workspace/')
        .replace(/vigthoria:\/\/temp\/?/gi, '[temp]/')
        .replace(/vigthoria:\/\/(?:agent|service|storage|home|server-internal)\/?[^\s"'\)\]\}<>]*/gi, '[internal]');
    // Strip server-internal phrasing so the LLM/CLI never says "server filesystem".
    text = text
        .replace(/outside bound workspace root(?:\s+[^\s.,;:)\]]+)?/gi, 'outside the workspace')
        .replace(/\bbound workspace root(?:\s+[^\s.,;:)\]]+)?/gi, 'workspace')
        .replace(/\boutside the active workspace sandbox(?::\s*[^\s.,;:)\]]+)?/gi, 'outside the workspace')
        .replace(/\bescapes workspace root(?:\s+[^\s.,;:)\]]+)?/gi, 'escapes the workspace')
        .replace(/\boutside allowed roots[^.]*\./gi, 'outside the workspace.')
        .replace(/\bAllowed (?:server )?roots:[^\n]*/gi, 'Allowed locations: workspace only')
        .replace(/\bBound server workspace:\s*\S+/gi, 'Bound workspace: workspace/')
        .replace(/\bserver filesystem\b/gi, 'workspace sandbox')
        .replace(/\bon the server\b/gi, 'in the workspace')
        .replace(/Blocked: shell operator '\\|' is not supported; run commands directly without shell piping or redirection/gi, 'Blocked command syntax from remote runner. Re-run locally using a single command, or ask for approval to run an equivalent local command.')
        .replace(/command references a path outside the workspace\. Use paths relative to the workspace root[^.]*\./gi, 'The command path must resolve inside your local project workspace. Use local workspace-relative paths.')
        .replace(/\[(?:\s*)\]/g, '[workspace]')
        .replace(/(^|\s)\.\//g, '$1')
        .replace(/([^:])\/\/+/g, '$1/')
        .replace(/[ \t]+/g, ' ');
    return text;
}
export function sanitizeUserFacingErrorText(input) {
    const raw = String(input || '').trim();
    if (!raw) {
        return '';
    }
    const withoutTags = raw.replace(/<[^>]+>/g, ' ');
    return sanitizeUserFacingPathText(withoutTags)
        .replace(/\\[nr]/g, ' ')
        .replace(/https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?(?:\/[^\s'")\]]*)?/gi, 'internal AI service')
        .replace(/\b(?:deepseek|openrouter)\/[A-Za-z0-9._/-]+\b/gi, 'cloud model')
        .replace(/\s+/g, ' ')
        .trim();
}
const TRUSTED_TOKEN_HOST_PATTERN = /(^|\.)vigthoria\.io$/i;
function isLoopbackHost(hostname) {
    const host = String(hostname || '').toLowerCase();
    return host === 'localhost' || host === '127.0.0.1';
}
function isTrustedTokenDestination(rawUrl) {
    try {
        const parsed = new URL(rawUrl);
        const host = parsed.hostname.toLowerCase();
        return TRUSTED_TOKEN_HOST_PATTERN.test(host) || isLoopbackHost(host);
    }
    catch {
        return false;
    }
}
function resolveAxiosRequestUrl(req) {
    const direct = String(req?.url || '').trim();
    const base = String(req?.baseURL || '').trim();
    if (!direct && !base)
        return '';
    if (/^https?:\/\//i.test(direct))
        return direct;
    if (base) {
        try {
            return new URL(direct || '', base).toString();
        }
        catch {
            return base;
        }
    }
    return direct;
}
export function isServerRuntime() {
    if (process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1') {
        return true;
    }
    const host = String(process.env.HOSTNAME || '').toLowerCase();
    const cwd = String(process.cwd() || '').toLowerCase();
    return host.includes('ubuntu') || cwd.startsWith('/var/www');
}
export function describeUpstreamStatus(status) {
    if (status >= 500)
        return 'upstream internal error';
    if (status === 429)
        return 'rate limited';
    if (status === 404)
        return 'endpoint not found';
    if (status === 403)
        return 'forbidden';
    if (status === 401)
        return 'unauthorized';
    if (status >= 400)
        return 'bad request';
    return 'ok';
}
export function propagateError(err) {
    const status = typeof err?.statusCode === 'number'
        ? err.statusCode
        : typeof err?.status === 'number'
            ? err.status
            : typeof err?.response?.status === 'number'
                ? err.response.status
                : 500;
    const endpoint = err?.endpoint || err?.config?.url || err?.details?.endpoint || 'unknown';
    const message = sanitizeUserFacingErrorText(String(err?.message || 'API request failed'));
    throw {
        code: status,
        message,
        isAuthError: status === 401 || status === 403,
        details: {
            ...(err?.details && typeof err.details === 'object' ? err.details : {}),
            endpoint,
            status,
            originalCode: err?.code,
        },
    };
}
// Stream health policy — see matching comment in src/commands/chat.ts.
// No overall wall-clock timeout for agentic requests (duration is
// unpredictable and must never kill a healthy, actively-streaming request).
// Only silence (zero bytes for a stretch, see DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS)
// is a legitimate failure signal for SSE/chunked streams.
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
    // Agentic tasks are open-ended by design — a build/refactor/analysis can
    // legitimately take minutes or hours depending on scope. There must be NO
    // overall wall-clock cap that kills an actively-streaming, healthy request.
    // The only correct failure signal for SSE/streamed responses is silence:
    // see DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS below, which aborts only when zero
    // bytes arrive for a stretch — never based on total elapsed duration.
    const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
    if (!rawValue) {
        return 0; // disabled — no overall duration cap
    }
    const parsed = Number.parseInt(rawValue, 10);
    return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
})();
const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
    const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS;
    if (!rawValue) {
        return 60_000; // 60s with zero new stream data => abort/retry
    }
    const parsed = Number.parseInt(rawValue, 10);
    return Number.isFinite(parsed) && parsed >= 0 ? parsed : 60_000;
})();
const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
    const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS;
    if (!rawValue) {
        return 0;
    }
    const parsed = Number.parseInt(rawValue, 10);
    return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
})();
export class APIClient {
    client;
    modelRouterClient;
    selfHostedModelRouterClient;
    config;
    logger;
    ws = null;
    vigFlowTokens = new Map();
    _httpsAgent = null;
    lastChatTransportErrors = [];
    clientToolErrors = [];
    pendingV3ClientToolTasks = new Set();
    /** Tracks files mutated via client_tool_request during an active V3 SSE stream. */
    activeV3StreamedFiles = null;
    activeV3StreamContext = null;
    constructor(config, logger) {
        this.config = config;
        this.logger = logger;
        // Create HTTPS agent with proper settings for cross-platform compatibility
        const httpsAgent = new https.Agent({
            rejectUnauthorized: true, // Enforce SSL verification
            keepAlive: true,
            timeout: 30000,
        });
        this._httpsAgent = httpsAgent;
        // Main Vigthoria Coder API (coder.vigthoria.io)
        this.client = axios.create({
            baseURL: config.get('apiUrl'),
            timeout: 300000, // 5 minutes — covers review, fix, and agent relay flows
            httpsAgent,
            headers: {
                'Content-Type': 'application/json',
                'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.9'}`,
            },
        });
        // Direct AI Models API - bypasses Coder for direct model access
        // This is the centralized Vigthoria API that routes to Model Router
        this.modelRouterClient = axios.create({
            baseURL: config.get('modelsApiUrl') || 'https://api.vigthoria.io',
            timeout: 180000, // 3 minutes for model inference
            httpsAgent,
            headers: {
                'Content-Type': 'application/json',
                'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.9'}`,
            },
        });
        // Optional dedicated Vigthoria model endpoint for internal/server deployments only.
        const selfHostedModelsApiUrl = this.getSelfHostedModelsApiUrl();
        this.selfHostedModelRouterClient = selfHostedModelsApiUrl ? axios.create({
            baseURL: selfHostedModelsApiUrl,
            timeout: 240000,
            headers: {
                'Content-Type': 'application/json',
                'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.9'}`,
            },
        }) : null;
        // Add auth interceptor
        this.client.interceptors.request.use((req) => {
            const token = this.getAccessToken();
            const destination = resolveAxiosRequestUrl(req);
            if (token && isTrustedTokenDestination(destination)) {
                req.headers.Authorization = `Bearer ${token}`;
                req.headers.Cookie = `vigthoria-auth-token=${token}`;
            }
            return req;
        });
        this.modelRouterClient.interceptors.request.use((req) => {
            const token = this.getAccessToken();
            const destination = resolveAxiosRequestUrl(req);
            if (token && isTrustedTokenDestination(destination)) {
                req.headers.Authorization = `Bearer ${token}`;
                req.headers.Cookie = `vigthoria-auth-token=${token}`;
            }
            return req;
        });
        this.selfHostedModelRouterClient?.interceptors.request.use((req) => {
            const token = this.getAccessToken();
            const destination = resolveAxiosRequestUrl(req);
            if (token && isTrustedTokenDestination(destination)) {
                req.headers.Authorization = `Bearer ${token}`;
            }
            return req;
        });
        // Add response interceptors for token refresh + structured errors
        const createAuthRetryInterceptor = (client) => {
            client.interceptors.response.use((res) => res, async (error) => {
                if (error.response?.status === 401) {
                    const refreshed = await this.refreshToken();
                    if (refreshed && error.config) {
                        return client.request(error.config);
                    }
                    throw classifyError(error, 'auth');
                }
                throw classifyError(error);
            });
        };
        createAuthRetryInterceptor(this.client);
        createAuthRetryInterceptor(this.modelRouterClient);
        if (this.selfHostedModelRouterClient) {
            createAuthRetryInterceptor(this.selfHostedModelRouterClient);
        }
    }
    /**
     * Destroy keep-alive sockets so the Node.js event loop can drain
     * naturally.  Call this before exiting commands that run HTTP probes
     * (e.g. `status`) to avoid the libuv UV_HANDLE_CLOSING assertion
     * on Windows / Node 25+.
     */
    destroy() {
        if (this._httpsAgent) {
            this._httpsAgent.destroy();
            this._httpsAgent = null;
        }
        if (this.ws) {
            try {
                this.ws.close();
            }
            catch { /* ok */ }
            this.ws = null;
        }
    }
    /** Exposed for Balanced 4B agent routing (local inference :8016). */
    getSelfHostedModelsApiUrl() {
        const configuredUrl = process.env.VIGTHORIA_SELF_HOSTED_MODELS_API_URL
            || this.config.get('selfHostedModelsApiUrl');
        if (!configuredUrl) {
            return null;
        }
        const normalizedUrl = String(configuredUrl).trim().replace(/\/$/, '');
        return normalizedUrl || null;
    }
    // Authentication - Uses Vigthoria Coder /api/login endpoint
    async login(email, password) {
        try {
            const response = await this.client.post('/api/login', { email, password });
            if (!response.data.success) {
                throw new Error(response.data.error || 'Login failed');
            }
            const { token, user } = response.data;
            this.config.setAuth({
                token,
                userId: user.id,
                email: user.email,
            });
            // Set subscription from user data
            this.config.setSubscription({
                plan: user.subscription?.plan || 'community',
                status: 'active',
                expiresAt: undefined,
            });
            return true;
        }
        catch (error) {
            const axiosError = error;
            const message = axiosError.response?.data?.error || error.message;
            this.logger.error('Login failed:', message);
            return false;
        }
    }
    async loginWithToken(token) {
        try {
            // Validate token by making a request to user info endpoint
            this.config.set('authToken', token);
            const headers = {
                Authorization: `Bearer ${token}`,
                Cookie: `vigthoria-auth-token=${token}`,
            };
            const candidateEndpoints = [
                '/api/user/profile',
                '/api/user/info',
                '/api/auth/me',
            ];
            for (const endpoint of candidateEndpoints) {
                try {
                    const response = await this.client.get(endpoint, { headers });
                    const profile = this.extractUserProfile(response.data);
                    if (!profile) {
                        continue;
                    }
                    this.config.setAuth({
                        token,
                        userId: profile.id,
                        email: profile.email,
                    });
                    this.config.setSubscription({
                        plan: profile.plan,
                        status: 'active',
                        expiresAt: undefined,
                    });
                    return true;
                }
                catch (error) {
                    const axiosError = error;
                    if (axiosError.response?.status && axiosError.response.status !== 404) {
                        throw error;
                    }
                }
            }
            this.config.clearAuth();
            return false;
        }
        catch (error) {
            this.config.clearAuth();
            this.logger.error('Token validation failed:', error.message);
            return false;
        }
    }
    extractUserProfile(data) {
        if (!data) {
            return null;
        }
        const rawUser = data.user || data;
        const userId = rawUser.id;
        const email = rawUser.email;
        if (!userId || !email) {
            return null;
        }
        const plan = data.subscription?.plan
            || rawUser.subscription?.plan
            || rawUser.subscription_plan
            || data.subscription_plan
            || 'community';
        return {
            id: userId,
            email,
            plan,
        };
    }
    async refreshToken() {
        const refreshToken = this.config.get('refreshToken');
        if (!refreshToken)
            return false;
        try {
            const response = await this.client.post('/api/token/refresh', {
                refresh_token: refreshToken
            });
            const { token, access_token, refresh_token } = response.data;
            this.config.set('authToken', token || access_token);
            if (refresh_token) {
                this.config.set('refreshToken', refresh_token);
            }
            return true;
        }
        catch {
            this.config.clearAuth();
            return false;
        }
    }
    async getSubscriptionStatus() {
        try {
            const response = await this.client.get('/api/user/subscription');
            const data = response.data;
            this.config.setSubscription({
                plan: data.plan || data.subscription_plan || 'community',
                status: data.status || 'active',
                expiresAt: data.expiresAt || data.expires_at
            });
        }
        catch (error) {
            this.logger.debug('Failed to get subscription status:', error.message);
        }
    }
    getAccessToken() {
        return process.env.VIGTHORIA_TOKEN
            || process.env.VIGTHORIA_AUTH_TOKEN
            || this.config.get('authToken')
            || null;
    }
    /**
     * Validate the current auth token against the Coder API.
     * By default this fails open on network errors to keep offline commands usable.
     */
    async validateToken(options = {}) {
        const allowNetworkFailOpen = options.allowNetworkFailOpen !== false;
        const enforceTokenShape = options.enforceTokenShape !== false;
        const explicitEnvToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
        const token = this.getAccessToken();
        if (isLocalTestfarmMode()) {
            if (!token) {
                return { valid: false, error: 'Local test mode requires VIGTHORIA_LOCAL_TEST_TOKEN or VIGTHORIA_AUTH_TOKEN' };
            }
            const expected = String(process.env.VIGTHORIA_LOCAL_TEST_TOKEN || '').trim();
            if (expected && token !== expected) {
                return { valid: false, error: 'VIGTHORIA_LOCAL_TEST_TOKEN mismatch' };
            }
            return { valid: true };
        }
        if (!token) {
            return { valid: false, error: 'No auth token configured. Run: vigthoria login' };
        }
        // Fast-fail obviously malformed ENV override tokens so invalid-token checks
        // don't get masked by unrelated transport outages. Persisted login tokens may
        // be non-JWT in some deployments and must still be gateway-validated server-side.
        if (enforceTokenShape && explicitEnvToken) {
            const looksLikeJwt = token.split('.').length === 3;
            if (!looksLikeJwt || token.length < 40) {
                return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
            }
        }
        const headers = {
            Authorization: `Bearer ${token}`,
            Cookie: `vigthoria-auth-token=${token}`,
        };
        const canonicalBaseUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
        // Probe protected canonical endpoints in parallel so stale local endpoint overrides
        // cannot mask an invalid gateway token during preflight.
        const results = await Promise.allSettled([
            axios.get(`${canonicalBaseUrl}/api/user/profile`, { timeout: 5000, headers, httpsAgent: this._httpsAgent ?? undefined }),
            axios.get(`${canonicalBaseUrl}/api/user/subscription`, { timeout: 5000, headers, httpsAgent: this._httpsAgent ?? undefined }),
        ]);
        for (const r of results) {
            if (r.status === 'fulfilled')
                return { valid: true };
        }
        const sawUnauthorized = results.some((r) => r.status === 'rejected' && ((r.reason?.response?.status === 401) || (r.reason?.response?.status === 403) || (r.reason instanceof CLIError && r.reason.category === 'auth')));
        if (sawUnauthorized) {
            // For persisted CLI sessions, attempt one refresh before failing auth.
            if (!explicitEnvToken) {
                const refreshed = await this.refreshToken();
                if (refreshed) {
                    const retryToken = this.getAccessToken();
                    if (retryToken) {
                        const retryHeaders = {
                            Authorization: `Bearer ${retryToken}`,
                            Cookie: `vigthoria-auth-token=${retryToken}`,
                        };
                        const retryResults = await Promise.allSettled([
                            axios.get(`${canonicalBaseUrl}/api/user/profile`, { timeout: 5000, headers: retryHeaders, httpsAgent: this._httpsAgent ?? undefined }),
                            axios.get(`${canonicalBaseUrl}/api/user/subscription`, { timeout: 5000, headers: retryHeaders, httpsAgent: this._httpsAgent ?? undefined }),
                        ]);
                        for (const rr of retryResults) {
                            if (rr.status === 'fulfilled')
                                return { valid: true };
                        }
                    }
                }
            }
            return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
        }
        if (explicitEnvToken || !allowNetworkFailOpen) {
            return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
        }
        // Both unreachable — don't assume the stored token is bad when running offline.
        return { valid: true };
    }
    isDirectV3AgentBaseUrl(baseUrl) {
        try {
            const parsed = new URL(String(baseUrl));
            return parsed.port === '8030' || parsed.port.endsWith('8030');
        }
        catch {
            return /(?:127\.0\.0\.1|localhost|172\.19\.0\.1|host\.lan):(?:\d*8030|8030)/.test(String(baseUrl));
        }
    }
    getV3AgentBaseUrls(preferLocal = false) {
        const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
        const localTestMode = isLocalTestfarmMode();
        const includeLoopbackV3Agent = process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1'
            || (preferLocal && isServerRuntime());
        const localCandidates = [
            process.env.VIGTHORIA_V3_AGENT_URL,
            process.env.V3_AGENT_URL,
            ...(includeLoopbackV3Agent ? ['http://127.0.0.1:8030'] : []),
        ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
        const remoteCandidates = localTestMode
            ? [configuredApiUrl]
            : [configuredApiUrl, 'https://coder.vigthoria.io'];
        const normalizedRemote = remoteCandidates
            .filter(Boolean)
            .map((url) => String(url).replace(/\/$/, ''));
        const urls = preferLocal && localCandidates.length > 0
            ? [...localCandidates, ...normalizedRemote]
            : [...normalizedRemote, ...localCandidates];
        return [...new Set(urls)];
    }
    getV3AgentRunUrl(baseUrl) {
        if (this.isDirectV3AgentBaseUrl(baseUrl)) {
            return `${baseUrl}/api/agent/run`;
        }
        return `${baseUrl}/api/v3-agent/run`;
    }
    getV3AgentContinueUrl(baseUrl) {
        if (this.isDirectV3AgentBaseUrl(baseUrl)) {
            return `${baseUrl}/api/agent/continue`;
        }
        return `${baseUrl}/api/v3-agent/continue`;
    }
    getV3AgentBackgroundUrl(baseUrl, suffix = '') {
        const cleanSuffix = suffix ? `/${suffix.replace(/^\/+/, '')}` : '';
        if (this.isDirectV3AgentBaseUrl(baseUrl)) {
            return `${baseUrl}/api/agent/background${cleanSuffix}`;
        }
        return `${baseUrl}/api/v3-agent/background${cleanSuffix}`;
    }
    getOperatorBaseUrls() {
        const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
        const urls = [
            process.env.VIGTHORIA_OPERATOR_URL,
            process.env.OPERATOR_URL,
            'http://127.0.0.1:4009',
            configuredModelsApiUrl,
            'https://api.vigthoria.io',
        ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
        return [...new Set(urls)];
    }
    getOperatorStreamUrl(baseUrl) {
        return `${baseUrl}/api/operator/stream`;
    }
    getMcpBaseUrls() {
        const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
        const localTestMode = isLocalTestfarmMode();
        const urls = localTestMode
            ? [
                process.env.VIGTHORIA_MCP_URL,
                process.env.MCP_SERVER_URL,
                configuredApiUrl,
            ]
            : [
                process.env.VIGTHORIA_MCP_URL,
                process.env.MCP_SERVER_URL,
                'http://127.0.0.1:4008',
                configuredApiUrl,
            ];
        return [...new Set(urls.filter(Boolean).map((url) => String(url).replace(/\/$/, '')))];
    }
    getVigFlowBaseUrls() {
        const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
        // Put the remote gateway first, since local VigFlow servers are
        // rarely running for end-user CLI installations. This avoids
        // wasting connection-attempt time on 127.0.0.1 and hitting the
        // remote gateway only after the local attempts have already
        // errored — which surfaces as a confusing "last error" 404 in
        // some setups.
        const urls = [
            process.env.VIGTHORIA_VIGFLOW_URL,
            process.env.VIGFLOW_URL,
            process.env.WORKFLOW_BUILDER_URL,
            `${configuredApiUrl}/api/vigflow`,
            'http://127.0.0.1:5060',
            'http://127.0.0.1:5050',
        ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
        return [...new Set(urls)];
    }
    getTemplateServiceBaseUrls() {
        const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
        const urls = [
            process.env.VIGTHORIA_TEMPLATE_SERVICE_URL,
            process.env.TEMPLATE_SERVICE_URL,
            `${configuredApiUrl}/api/template-service`,
            'http://127.0.0.1:4011',
        ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
        return [...new Set(urls)];
    }
    isFrontendTask(message = '', context = {}) {
        // Never treat analysis-only tasks as frontend tasks — preview gate
        // should not fire for read-only inspection prompts.
        if (this.isAnalysisOnlyTask(message, context)) {
            return false;
        }
        const prompt = String(message || '');
        const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
        const hasHtmlEntry = expectedFiles.some((filePath) => /\.html?$/i.test(filePath));
        return /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|pricing|showcase|hero|responsive)/i.test(prompt)
            || hasHtmlEntry;
    }
    /**
     * Returns true when the prompt describes a read-only / analysis task.
     * Used to suppress preview gate and other write-oriented side effects.
     */
    isAnalysisOnlyTask(message = '', context = {}) {
        const prompt = String(message || '').toLowerCase();
        const taskType = String(context.agentTaskType || '').toLowerCase();
        if (taskType === 'debugging' || taskType === 'analysis' || taskType === 'analysis_only')
            return true;
        if (context.workflowType === 'analysis_only')
            return true;
        return /^(what|which|how many|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|find|count|read|look at|tell me|diagnos)/i.test(prompt.trim());
    }
    normalizeWorkspaceRelativePath(filePath) {
        return String(filePath || '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
    }
    collectAgentStateSyncPaths(rootPath) {
        const paths = [];
        const progressPath = path.join(rootPath, '.vigthoria', 'agent-state', 'progress.json');
        if (!fs.existsSync(progressPath)) {
            return paths;
        }
        paths.push('.vigthoria/agent-state/progress.json');
        const tasksDir = path.join(rootPath, '.vigthoria', 'agent-state', 'tasks');
        if (!fs.existsSync(tasksDir)) {
            return paths;
        }
        try {
            for (const entry of fs.readdirSync(tasksDir, { withFileTypes: true })) {
                if (!entry.isFile() || !entry.name.endsWith('.json')) {
                    continue;
                }
                paths.push(`.vigthoria/agent-state/tasks/${entry.name}`);
            }
        }
        catch {
            // Ignore unreadable task snapshots.
        }
        return paths;
    }
    listFrontendWorkspaceFiles(rootPath) {
        const results = [];
        const stack = [rootPath];
        while (stack.length > 0) {
            const current = stack.pop();
            if (!current) {
                continue;
            }
            for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
                if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.vigthoria') {
                    continue;
                }
                const fullPath = path.join(current, entry.name);
                if (entry.isDirectory()) {
                    stack.push(fullPath);
                    continue;
                }
                if (!entry.isFile()) {
                    continue;
                }
                const relativePath = this.normalizeWorkspaceRelativePath(path.relative(rootPath, fullPath));
                if (relativePath) {
                    results.push(relativePath);
                }
            }
        }
        results.sort((left, right) => left.localeCompare(right));
        return results;
    }
    chooseFrontendPreviewEntry(rootPath, htmlPaths, expectedFiles = []) {
        const normalizedExpected = expectedFiles
            .map((filePath) => this.normalizeWorkspaceRelativePath(filePath))
            .filter((filePath) => /\.html?$/i.test(filePath));
        for (const expectedPath of normalizedExpected) {
            if (htmlPaths.includes(expectedPath)) {
                return expectedPath;
            }
        }
        const scored = htmlPaths.map((relativePath) => {
            const baseName = path.posix.basename(relativePath).toLowerCase();
            const depth = relativePath.split('/').length - 1;
            let score = 0;
            const normalizedPath = relativePath.toLowerCase();
            const topLevelDir = normalizedPath.split('/')[0];
            if (['dist', 'build', 'out'].includes(topLevelDir)) {
                score += 320;
            }
            else if (topLevelDir === 'public') {
                score += 180;
            }
            if (normalizedPath === 'index.html') {
                score += 200;
            }
            if (baseName === 'index.html') {
                score += 120;
            }
            if (/(landing|home|main|dashboard|app)/i.test(baseName)) {
                score += 60;
            }
            if (depth === 0) {
                score += 40;
            }
            return { relativePath, score, depth };
        }).sort((left, right) => right.score - left.score || left.depth - right.depth || left.relativePath.localeCompare(right.relativePath));
        return scored[0]?.relativePath || null;
    }
    extractLinkedFrontendAssets(html, entryPath) {
        const css = new Set();
        const js = new Set();
        const entryDir = path.posix.dirname(this.normalizeWorkspaceRelativePath(entryPath) || '.');
        const normalizeAsset = (assetPath) => {
            const clean = String(assetPath || '').trim();
            if (!clean || clean.startsWith('http://') || clean.startsWith('https://') || clean.startsWith('//') || clean.startsWith('data:') || clean.startsWith('#')) {
                return null;
            }
            const withoutQuery = clean.split('?')[0].split('#')[0];
            if (!withoutQuery) {
                return null;
            }
            if (withoutQuery.startsWith('/')) {
                return this.normalizeWorkspaceRelativePath(withoutQuery.slice(1));
            }
            return this.normalizeWorkspaceRelativePath(path.posix.normalize(path.posix.join(entryDir, withoutQuery)));
        };
        const cssPattern = /<link[^>]+href=["']([^"']+\.css(?:[?#][^"']*)?)["'][^>]*>/gi;
        const jsPattern = /<script[^>]+src=["']([^"']+\.(?:js|mjs|cjs)(?:[?#][^"']*)?)["'][^>]*>/gi;
        let match;
        while ((match = cssPattern.exec(String(html || ''))) !== null) {
            const resolved = normalizeAsset(match[1]);
            if (resolved) {
                css.add(resolved);
            }
        }
        while ((match = jsPattern.exec(String(html || ''))) !== null) {
            const resolved = normalizeAsset(match[1]);
            if (resolved) {
                js.add(resolved);
            }
        }
        return {
            css: Array.from(css),
            js: Array.from(js),
        };
    }
    async buildFrontendForPreview(rootPath) {
        const packagePath = path.join(rootPath, 'package.json');
        if (!fs.existsSync(packagePath)) {
            return { attempted: false, succeeded: false };
        }
        try {
            const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
            if (!packageJson.scripts?.build) {
                return { attempted: false, succeeded: false };
            }
        }
        catch (error) {
            return { attempted: false, succeeded: false, error: `Invalid package.json: ${error?.message || String(error)}` };
        }
        const npmCommand = process.platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : 'npm';
        const npmArgs = process.platform === 'win32'
            ? ['/d', '/s', '/c', 'npm run build']
            : ['run', 'build'];
        return await new Promise((resolve) => {
            execFile(npmCommand, npmArgs, { cwd: rootPath, timeout: 150_000, windowsHide: true, maxBuffer: 2 * 1024 * 1024 }, (error) => {
                if (error) {
                    resolve({ attempted: true, succeeded: false, error: sanitizeUserFacingErrorText(error.message).slice(0, 300) });
                    return;
                }
                resolve({ attempted: true, succeeded: true });
            });
        });
    }
    gatherFrontendPreviewArtifacts(message = '', context = {}) {
        const rootPath = this.resolveAgentTargetPath(context);
        if (!rootPath || !fs.existsSync(rootPath)) {
            return { error: 'Frontend preview gate requires a readable project workspace.' };
        }
        const workspaceFiles = this.listFrontendWorkspaceFiles(rootPath);
        const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
        const htmlPaths = workspaceFiles.filter((filePath) => /\.html?$/i.test(filePath));
        if (htmlPaths.length === 0) {
            return { error: 'Frontend preview gate requires at least one HTML entry file in the workspace.' };
        }
        const htmlPath = this.chooseFrontendPreviewEntry(rootPath, htmlPaths, expectedFiles);
        if (!htmlPath) {
            return { error: 'Frontend preview gate could not determine a primary HTML entry file.' };
        }
        const html = fs.readFileSync(path.join(rootPath, htmlPath), 'utf8');
        const linkedAssets = this.extractLinkedFrontendAssets(html, htmlPath);
        const cssFiles = workspaceFiles.filter((filePath) => /\.css$/i.test(filePath));
        const jsFiles = workspaceFiles.filter((filePath) => /\.(?:js|mjs|cjs)$/i.test(filePath));
        const expectedCss = expectedFiles
            .map((filePath) => this.normalizeWorkspaceRelativePath(filePath))
            .filter((filePath) => /\.css$/i.test(filePath));
        const expectedJs = expectedFiles
            .map((filePath) => this.normalizeWorkspaceRelativePath(filePath))
            .filter((filePath) => /\.(?:js|mjs|cjs)$/i.test(filePath));
        const chooseExisting = (candidates, fallback) => {
            const selected = new Set();
            for (const candidate of [...candidates, ...fallback]) {
                const normalized = this.normalizeWorkspaceRelativePath(candidate);
                if (!normalized || selected.has(normalized)) {
                    continue;
                }
                if (!fs.existsSync(path.join(rootPath, normalized))) {
                    continue;
                }
                selected.add(normalized);
            }
            return Array.from(selected);
        };
        const selectedCssFiles = chooseExisting(linkedAssets.css, [...expectedCss, ...cssFiles]).slice(0, 12);
        const selectedJsFiles = chooseExisting(linkedAssets.js, [...expectedJs, ...jsFiles]).slice(0, 12);
        const css = selectedCssFiles.map((filePath) => fs.readFileSync(path.join(rootPath, filePath), 'utf8')).join('\n\n');
        const js = selectedJsFiles.map((filePath) => fs.readFileSync(path.join(rootPath, filePath), 'utf8')).join('\n\n');
        return { htmlPath, html, css, js, cssPaths: selectedCssFiles, jsPaths: selectedJsFiles };
    }
    async captureFrontendPreviewScreenshot(entryAbsolutePath, screenshotPath) {
        try {
            const puppeteerModule = await import('puppeteer').catch(() => null);
            const puppeteer = puppeteerModule?.default || puppeteerModule;
            if (!puppeteer || typeof puppeteer.launch !== 'function') {
                return { captured: false, error: 'puppeteer-unavailable' };
            }
            const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] });
            try {
                const page = await browser.newPage();
                await page.setViewport({ width: 1440, height: 960, deviceScaleFactor: 1 });
                await page.goto(`file://${entryAbsolutePath}`, { waitUntil: 'networkidle0', timeout: 20000 });
                await page.screenshot({ path: screenshotPath, fullPage: true });
            }
            finally {
                await browser.close();
            }
            return { captured: true };
        }
        catch (error) {
            return { captured: false, error: error?.message || String(error) };
        }
    }
    evaluateFrontendVisualProof(summary = {}, artifacts = {}) {
        const reasons = [];
        const cssPaths = Array.isArray(artifacts.cssPaths) ? artifacts.cssPaths : [];
        const html = String(artifacts.html || '');
        const css = String(artifacts.css || '');
        const js = String(artifacts.js || '');
        if (!summary.hasViewportMeta) {
            reasons.push('missing viewport metadata');
        }
        const hasStyleEvidence = cssPaths.length > 0 || /<style[\s>]/i.test(html) || css.trim().length > 0;
        if (!hasStyleEvidence) {
            reasons.push('missing stylesheet or inline style evidence');
        }
        const hasInteractionOrResponsiveEvidence = Boolean(summary.hasResponsiveSignals) || Boolean(summary.hasInteractiveSignals) || /<script[\s>]/i.test(html) || js.trim().length > 0;
        if (!hasInteractionOrResponsiveEvidence) {
            reasons.push('missing responsive or interactive evidence');
        }
        return {
            ok: reasons.length === 0,
            reasons,
        };
    }
    async persistFrontendPreviewArtifacts(previewGate, rootPath, context = {}) {
        if (!previewGate.required || !previewGate.entryPath) {
            return previewGate;
        }
        try {
            const artifactsDir = path.join(rootPath, '.vigthoria', 'proof', 'preview');
            fs.mkdirSync(artifactsDir, { recursive: true });
            const contextId = String(context.contextId || 'preview').trim() || 'preview';
            const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
            const baseName = `${timestamp}-${contextId}`;
            const manifestPath = path.join(artifactsDir, `${baseName}.json`);
            const screenshotPath = path.join(artifactsDir, `${baseName}.png`);
            const entryAbsolutePath = path.join(rootPath, previewGate.entryPath);
            const previewFileUrl = `file://${entryAbsolutePath}`;
            const screenshot = await this.captureFrontendPreviewScreenshot(entryAbsolutePath, screenshotPath);
            const manifest = {
                schemaVersion: 1,
                createdAt: new Date().toISOString(),
                contextId: context.contextId || null,
                workspaceRoot: rootPath,
                entryPath: previewGate.entryPath,
                previewFileUrl,
                previewCommand: `vigthoria preview -p ${rootPath}`,
                previewGate,
                validationArtifacts: {
                    summary: previewGate.summary || {},
                    modes: previewGate.modes || {},
                    backendUrl: previewGate.backendUrl || null,
                    processingTimeMs: previewGate.processingTimeMs || null,
                    assetPaths: previewGate.assetPaths || { css: [], js: [] },
                },
                screenshot: {
                    path: screenshot.captured ? screenshotPath : null,
                    captured: screenshot.captured,
                    error: screenshot.error || null,
                },
            };
            fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
            return {
                ...previewGate,
                artifacts: {
                    manifestPath,
                    screenshotPath: screenshot.captured ? screenshotPath : undefined,
                    previewFileUrl,
                    screenshotCaptured: screenshot.captured,
                    screenshotError: screenshot.error,
                },
            };
        }
        catch (error) {
            return {
                ...previewGate,
                artifacts: {
                    ...(previewGate.artifacts || {}),
                    screenshotCaptured: false,
                    screenshotError: error?.message || String(error),
                },
            };
        }
    }
    async runTemplateServicePreviewGate(message = '', context = {}) {
        if (!this.isFrontendTask(message, context)) {
            return { required: false, passed: true };
        }
        const rootPath = this.resolveAgentTargetPath(context);
        let artifacts = this.gatherFrontendPreviewArtifacts(message, context);
        let buildResult = null;
        if (rootPath && artifacts.error?.includes('at least one HTML entry file')) {
            buildResult = await this.buildFrontendForPreview(rootPath);
            if (buildResult.attempted) {
                artifacts = this.gatherFrontendPreviewArtifacts(message, context);
            }
        }
        if (artifacts.error || !artifacts.htmlPath || typeof artifacts.html !== 'string') {
            const buildDetail = buildResult?.attempted
                ? ` Build retry ${buildResult.succeeded ? 'completed but produced no HTML entry' : `failed: ${buildResult.error || 'unknown build error'}`}.`
                : '';
            return {
                required: true,
                passed: false,
                error: `${artifacts.error || 'Frontend preview gate could not collect frontend artifacts.'}${buildDetail}`,
            };
        }
        // Cap artifact sizes to prevent 413 Payload Too Large
        const PREVIEW_MAX_ARTIFACT_BYTES = 250 * 1024;
        const html = artifacts.html.slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
        const css = (artifacts.css || '').slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
        const js = (artifacts.js || '').slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
        const errors = [];
        for (const baseUrl of this.getTemplateServiceBaseUrls()) {
            try {
                const proofPayload = JSON.stringify({
                    vision: String(context.rawPrompt || message || '').slice(0, 2000),
                    html,
                    css,
                    js,
                    entryPath: artifacts.htmlPath,
                    workspaceName: path.basename(rootPath),
                });
                // Skip preview proof if payload is still too large (> 4 MB)
                if (Buffer.byteLength(proofPayload, 'utf8') > 4 * 1024 * 1024) {
                    return {
                        required: true,
                        passed: true,
                        error: 'Preview proof skipped: payload exceeds size limit.',
                    };
                }
                const internalServiceKey = process.env.VIGTHORIA_TEMPLATE_SERVICE_KEY
                    || process.env.V3_SERVICE_KEY;
                const authToken = this.getAccessToken();
                const response = await fetch(`${baseUrl}/preview-proof`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        Accept: 'application/json',
                        ...(authToken ? {
                            Authorization: `Bearer ${authToken}`,
                            Cookie: `vigthoria-auth-token=${authToken}`,
                        } : {}),
                        ...(internalServiceKey ? { 'x-vigthoria-service-key': internalServiceKey } : {}),
                    },
                    body: proofPayload,
                });
                if (!response.ok) {
                    const errorText = await response.text().catch(() => '');
                    throw new Error(`Template preview proof ${response.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
                }
                const payload = await response.json();
                const modes = payload?.modes || {};
                const summary = payload?.summary || {};
                const visualProof = this.evaluateFrontendVisualProof(summary, artifacts);
                const passed = payload?.success === true
                    && modes?.design?.ready === true
                    && modes?.live?.ready === true
                    && modes?.production?.ready === true
                    && visualProof.ok;
                const previewGate = {
                    required: true,
                    passed,
                    backendUrl: baseUrl,
                    entryPath: artifacts.htmlPath,
                    assetPaths: {
                        css: artifacts.cssPaths || [],
                        js: artifacts.jsPaths || [],
                    },
                    modes,
                    summary,
                    processingTimeMs: Number(payload?.processing_time || 0) || undefined,
                    error: passed ? undefined : (visualProof.ok ? 'Template Service preview modes did not all report ready.' : `Frontend preview proof is missing required visual evidence: ${visualProof.reasons.join(', ')}.`),
                };
                return await this.persistFrontendPreviewArtifacts(previewGate, rootPath, context);
            }
            catch (error) {
                errors.push(`${baseUrl}: ${error?.message || String(error)}`);
            }
        }
        return await this.persistFrontendPreviewArtifacts({
            required: true,
            passed: false,
            entryPath: artifacts.htmlPath,
            assetPaths: {
                css: artifacts.cssPaths || [],
                js: artifacts.jsPaths || [],
            },
            error: errors.join(' | ') || 'No Template Service preview endpoint was reachable.',
        }, rootPath, context);
    }
    getMcpContextCreateUrl(baseUrl) {
        return `${baseUrl}/mcp/context/create`;
    }
    getMcpContextUrl(baseUrl, contextId) {
        return `${baseUrl}/mcp/context/${encodeURIComponent(contextId)}`;
    }
    async getMcpHeaders() {
        const headers = {
            'Content-Type': 'application/json',
            Accept: 'application/json',
        };
        const authToken = this.getAccessToken();
        if (authToken) {
            headers.Authorization = `Bearer ${authToken}`;
            headers.Cookie = `vigthoria-auth-token=${authToken}`;
            headers['X-Vigthoria-Token'] = authToken;
            headers['X-Auth-Token'] = authToken;
        }
        return headers;
    }
    async getV3AgentHeaders() {
        const headers = {
            'Content-Type': 'application/json',
            Accept: 'text/event-stream',
        };
        const authToken = this.getAccessToken();
        if (authToken) {
            headers.Authorization = `Bearer ${authToken}`;
            headers.Cookie = `vigthoria-auth-token=${authToken}`;
        }
        const serviceKey = process.env.VIGTHORIA_V3_SERVICE_KEY
            || process.env.V3_SERVICE_KEY
            || process.env.HYPERLOOP_SERVICE_KEY
            || this.config.get('v3ServiceKey');
        if (serviceKey) {
            headers['X-Service-Key'] = serviceKey;
        }
        return headers;
    }
    /**
     * Ensure the V3 service key is available in config.
     * If not already set via env or stored config, fetches it from the Coder API
     * using the current user's auth token and caches it for this session and beyond.
     */
    async ensureV3ServiceKey() {
        const existing = process.env.VIGTHORIA_V3_SERVICE_KEY
            || process.env.V3_SERVICE_KEY
            || process.env.HYPERLOOP_SERVICE_KEY
            || this.config.get('v3ServiceKey');
        if (existing)
            return;
        try {
            const token = this.getAccessToken();
            if (!token)
                return;
            const response = await this.client.get('/api/cli/v3-service-key', {
                headers: { Authorization: `Bearer ${token}` },
                timeout: 6000,
            });
            const key = response.data?.v3ServiceKey;
            if (key) {
                this.config.set('v3ServiceKey', key);
                this.logger.debug('V3 service key retrieved and cached from Coder API.');
            }
        }
        catch {
            // Non-fatal: fall through; the health check will surface the auth error.
        }
    }
    async runV3HealthCheck() {
        const endpoints = this.getV3AgentBaseUrls(false);
        const headers = await this.getV3AgentHeaders();
        const failures = [];
        for (const baseUrl of endpoints) {
            const runUrl = this.getV3AgentRunUrl(baseUrl);
            // Use the lightweight GET /health endpoint — no LLM invocation, responds instantly.
            // For local 8030 the path is /health directly; for remote it's /api/v3-agent/health via proxy.
            const healthUrl = this.isDirectV3AgentBaseUrl(baseUrl)
                ? `${baseUrl}/health`
                : `${baseUrl}/api/v3-agent/health`;
            for (let attempt = 1; attempt <= 3; attempt += 1) {
                try {
                    const response = await fetch(healthUrl, {
                        method: 'GET',
                        headers,
                        signal: AbortSignal.timeout(8000),
                    });
                    if (response.status === 401 || response.status === 403) {
                        return {
                            healthy: false,
                            endpoint: runUrl,
                            error: 'V3 service rejected authentication. Please re-login to refresh your token.',
                        };
                    }
                    // 200 OK or 405 Method Not Allowed both mean V3 is reachable.
                    if (response.ok || response.status === 405) {
                        return { healthy: true, endpoint: runUrl };
                    }
                    failures.push(`${healthUrl} attempt ${attempt}: HTTP ${response.status}`);
                }
                catch (error) {
                    const reason = sanitizeUserFacingErrorText(error?.message || String(error)).slice(0, 160);
                    failures.push(`${healthUrl} attempt ${attempt}: ${reason || 'network error'}`);
                }
                if (attempt < 3) {
                    await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
                }
            }
        }
        return {
            healthy: false,
            endpoint: endpoints[0] ? this.getV3AgentRunUrl(endpoints[0]) : 'unknown',
            error: `V3 service is not reachable during startup preflight. Checked ${endpoints.length} endpoint(s). ${failures.slice(-3).join(' | ')}`,
        };
    }
    async runV3AgentAuthPreflight(baseUrl, body, executionContext) {
        const endpoint = this.getV3AgentRunUrl(baseUrl);
        const healthEndpoint = this.isDirectV3AgentBaseUrl(baseUrl)
            ? `${baseUrl}/health`
            : `${baseUrl}/api/v3-agent/health`;
        const headers = await this.getV3AgentHeaders();
        headers['X-Vigthoria-Preflight'] = '1';
        if (executionContext.mcpContextId) {
            headers['X-MCP-Context-Id'] = String(executionContext.mcpContextId);
        }
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), 12000);
        try {
            const response = await fetch(healthEndpoint, {
                method: 'GET',
                headers,
                signal: controller.signal,
            });
            if (response.status === 401 || response.status === 403) {
                const errorText = await response.text().catch(() => '');
                return {
                    ok: false,
                    status: response.status,
                    endpoint: healthEndpoint,
                    reason: sanitizeUserFacingErrorText(errorText).slice(0, 200) || 'Unauthorized',
                };
            }
            // Any non-auth response means token acceptance passed at the V3 service layer.
            return { ok: true, status: response.status, endpoint: healthEndpoint };
        }
        catch (error) {
            const reason = sanitizeUserFacingErrorText(error?.message || String(error)).slice(0, 200);
            // Preflight should not block agent execution on transient timeout/abort network conditions.
            if (/aborted|timeout|timed out|network/i.test(reason)) {
                return {
                    ok: true,
                    status: 0,
                    endpoint: healthEndpoint,
                    reason,
                };
            }
            return {
                ok: false,
                status: 0,
                endpoint: healthEndpoint,
                reason,
            };
        }
        finally {
            clearTimeout(timeoutId);
        }
    }
    async executeV3AgentRunRequest(baseUrl, body, executionContext, signal) {
        const makeRequest = async () => {
            const headers = await this.getV3AgentHeaders();
            if (executionContext.mcpContextId) {
                headers['X-MCP-Context-Id'] = String(executionContext.mcpContextId);
            }
            return fetch(this.getV3AgentRunUrl(baseUrl), {
                method: 'POST',
                headers,
                body: JSON.stringify(body),
                signal,
            });
        };
        let response = await makeRequest();
        if (response.status !== 401) {
            return response;
        }
        const refreshed = await this.refreshToken();
        if (!refreshed) {
            return response;
        }
        response = await makeRequest();
        return response;
    }
    async getVigFlowAccessToken(baseUrl) {
        const cachedToken = this.vigFlowTokens.get(baseUrl);
        if (cachedToken) {
            return cachedToken;
        }
        const authToken = this.getAccessToken();
        if (!authToken) {
            throw new Error('Not authenticated. Run vigthoria login first.');
        }
        const response = await axios.post(`${baseUrl}/api/auth/sso`, { token: authToken }, {
            headers: {
                'Content-Type': 'application/json',
            },
            timeout: 30000,
        });
        const payload = response.data;
        const vigFlowToken = String(payload.token || '').trim();
        if (!vigFlowToken) {
            throw new Error('VigFlow SSO response did not include a token.');
        }
        this.vigFlowTokens.set(baseUrl, vigFlowToken);
        return vigFlowToken;
    }
    async getVigFlowHeaders(baseUrl) {
        const token = await this.getVigFlowAccessToken(baseUrl);
        return {
            'Content-Type': 'application/json',
            Accept: 'application/json',
            Authorization: `Bearer ${token}`,
        };
    }
    async withVigFlow(operation, action) {
        let lastError = null;
        for (const baseUrl of this.getVigFlowBaseUrls()) {
            try {
                const headers = await this.getVigFlowHeaders(baseUrl);
                return await action(baseUrl, headers);
            }
            catch (error) {
                lastError = error;
                this.logger.debug(`VigFlow ${operation} via ${baseUrl} failed:`, lastError.message);
            }
        }
        // Throw a clean message instead of the raw ECONNREFUSED from the last URL tried
        throw new Error(`No VigFlow backend available for ${operation}. The workflow service is not deployed or not reachable.`);
    }
    /**
     * Build the correct sub-path for VigFlow endpoints.
     * Local servers (e.g. localhost:5060) need `/api/…` prefix.
     * The remote gateway URL already ends with `/api/vigflow`, so appending
     * another `/api/…` would double the prefix and cause 404s.
     */
    vigFlowEndpoint(baseUrl, subPath) {
        if (/\/api\/vigflow\/?$/i.test(baseUrl)) {
            // Remote gateway – subPath like '/templates' is enough
            return `${baseUrl.replace(/\/$/, '')}${subPath}`;
        }
        return `${baseUrl}/api${subPath}`;
    }
    async listVigFlowTemplates(options = {}) {
        return this.withVigFlow('list templates', async (baseUrl, headers) => {
            const query = new URLSearchParams();
            if (options.category) {
                query.set('category', options.category);
            }
            if (options.search) {
                query.set('search', options.search);
            }
            const url = `${this.vigFlowEndpoint(baseUrl, '/templates')}${query.size > 0 ? `?${query.toString()}` : ''}`;
            const response = await axios.get(url, {
                headers,
                timeout: 30000,
            });
            const payload = response.data;
            return Array.isArray(payload.templates) ? payload.templates : [];
        });
    }
    async listVigFlowWorkflows() {
        return this.withVigFlow('list workflows', async (baseUrl, headers) => {
            const response = await axios.get(this.vigFlowEndpoint(baseUrl, '/workflows'), {
                headers,
                timeout: 30000,
            });
            const payload = response.data;
            return Array.isArray(payload.workflows) ? payload.workflows : [];
        });
    }
    async resolveVigFlowWorkflow(selector) {
        const normalizedSelector = String(selector || '').trim();
        if (!normalizedSelector) {
            throw new Error('Workflow selector is required. Provide a workflow id or name.');
        }
        const workflows = await this.listVigFlowWorkflows();
        const byId = workflows.find((workflow) => workflow.id === normalizedSelector);
        if (byId) {
            return {
                id: byId.id,
                name: byId.name,
                selector: normalizedSelector,
                matchedBy: 'id',
            };
        }
        const loweredSelector = normalizedSelector.toLowerCase();
        const exactNameMatches = workflows.filter((workflow) => String(workflow.name || '').trim().toLowerCase() === loweredSelector);
        if (exactNameMatches.length === 1) {
            return {
                id: exactNameMatches[0].id,
                name: exactNameMatches[0].name,
                selector: normalizedSelector,
                matchedBy: 'name',
            };
        }
        if (exactNameMatches.length > 1) {
            throw new Error(`Multiple workflows matched the name "${normalizedSelector}". Use the workflow id instead.`);
        }
        const searchMatches = workflows.filter((workflow) => String(workflow.name || '').toLowerCase().includes(loweredSelector));
        if (searchMatches.length === 1) {
            return {
                id: searchMatches[0].id,
                name: searchMatches[0].name,
                selector: normalizedSelector,
                matchedBy: 'search',
            };
        }
        if (searchMatches.length > 1) {
            throw new Error(`Multiple workflows partially matched "${normalizedSelector}". Use a full workflow name or id.`);
        }
        throw new Error(`No VigFlow workflow matched "${normalizedSelector}".`);
    }
    async useVigFlowTemplate(templateId, options = {}) {
        return this.withVigFlow('use template', async (baseUrl, headers) => {
            const response = await axios.post(`${this.vigFlowEndpoint(baseUrl, `/templates/${encodeURIComponent(templateId)}/use`)}`, {
                name: options.name,
                variables: options.variables || {},
            }, {
                headers,
                timeout: 30000,
            });
            const payload = response.data;
            if (!payload.workflow?.id) {
                throw new Error('VigFlow use-template response did not include a workflow id.');
            }
            return payload.workflow;
        });
    }
    async runVigFlowWorkflow(workflowId, options = {}) {
        return this.withVigFlow('run workflow', async (baseUrl, headers) => {
            const response = await axios.post(`${this.vigFlowEndpoint(baseUrl, `/executions/run/${encodeURIComponent(workflowId)}`)}`, {
                data: options.data || {},
                options: options.executionOptions || {},
            }, {
                headers,
                timeout: 60000,
            });
            const payload = response.data;
            if (!payload.executionId) {
                throw new Error('VigFlow run response did not include an execution id.');
            }
            return payload;
        });
    }
    async getVigFlowExecutionStatus(executionId) {
        return this.withVigFlow('execution status', async (baseUrl, headers) => {
            const response = await axios.get(`${this.vigFlowEndpoint(baseUrl, `/executions/${encodeURIComponent(executionId)}`)}`, {
                headers,
                timeout: 30000,
            });
            const payload = response.data;
            if (!payload.execution?.id) {
                throw new Error('VigFlow execution response did not include execution details.');
            }
            return payload.execution;
        });
    }
    /** Legacy cap — prefer resolveV3ContextCharLimit() for token-aligned budgets. */
    static V3_CONTEXT_CHAR_LIMIT = 100_000;
    static HYDRATION_PRIORITY_BASENAMES = new Set([
        'package.json', 'index.html', 'game.js', 'main.js', 'app.js', 'script.js',
        'styles.css', 'style.css', 'README.md', 'manifest.json',
    ]);
    /** Keep critical workspace files for server hydration when context must shrink. */
    compactWorkspaceFilesToBudget(files, budgetChars) {
        const entries = Object.entries(files);
        const rank = (filePath) => {
            const normalized = filePath.replace(/\\/g, '/');
            const base = normalized.split('/').pop() || '';
            if (APIClient.HYDRATION_PRIORITY_BASENAMES.has(base))
                return 0;
            if (/^\.vigthoria\/agent-state\//i.test(normalized))
                return 1;
            if (/\.(html?|js|mjs|cjs|css|json|ts|tsx|jsx)$/i.test(base))
                return 2;
            return 3;
        };
        entries.sort((a, b) => rank(a[0]) - rank(b[0]) || a[0].localeCompare(b[0]));
        const trimmed = {};
        let used = 2;
        for (const [filePath, content] of entries) {
            const maxContentLen = rank(filePath) <= 1 ? content.length : Math.min(content.length, WORKSPACE_FILE_CHAR_CAP);
            const clipped = content.length > maxContentLen ? `${content.slice(0, maxContentLen)}\n/* … truncated for remote context … */` : content;
            const entryLen = JSON.stringify(filePath).length + 1 + JSON.stringify(clipped).length + 1;
            if (used + entryLen > budgetChars) {
                continue;
            }
            trimmed[filePath] = clipped;
            used += entryLen;
        }
        return trimmed;
    }
    buildV3AgentContext(context = {}) {
        const resolvedContext = this.ensureExecutionContext(context);
        const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
        const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
        const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
        const promptFocus = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || resolvedContext.prompt || '');
        const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, promptFocus);
        const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
        const resolvedModel = this.resolvePermittedModelId(requestedModel);
        const localWorkspaceName = this.getDisplayWorkspaceName(localWorkspacePath);
        const localWorkspaceRef = localWorkspaceName ? `vigthoria://local-workspace/${localWorkspaceName}` : null;
        const executionSurface = String(resolvedContext.executionSurface || resolvedContext.clientSurface || 'cli');
        const localMachineCapable = resolvedContext.localMachineCapable !== false;
        const clientToolExecution = resolvedContext.clientToolExecution === false
            ? false
            : (resolvedContext.clientToolExecution === true
                || (localMachineCapable && ['cli', 'fork', 'local-ide', 'desktop', 'local'].includes(executionSurface)));
        const publicRuntimeEnvironment = this.buildPublicRuntimeEnvironment(resolvedContext.agentRuntime, {
            localWorkspacePath,
            serverWorkspacePath,
        });
        // When the server cannot directly access the workspace (e.g. Windows
        // client), use the local path as a hint and flag that the workspace
        // files are provided inline in localWorkspaceSummary.workspaceFiles.
        const effectiveWorkspacePath = serverWorkspacePath || null;
        const needsHydration = !serverWorkspacePath && !!localWorkspacePath;
        const agentTaskType = resolvedContext.agentTaskType || 'general';
        const rawPrompt = resolvedContext.rawPrompt || resolvedContext.prompt || '';
        const executionHints = (resolvedContext.executionHints && typeof resolvedContext.executionHints === 'object')
            ? { ...resolvedContext.executionHints }
            : buildExecutionHints(agentTaskType, rawPrompt, {
                fastPath: resolvedContext.fastPath,
            });
        if (!executionHints.task_kind) {
            executionHints.task_kind = agentTaskType;
        }
        if (!executionHints.classify_from) {
            executionHints.classify_from = rawPrompt;
        }
        if (resolvedContext.workflowType === 'analysis_only' || resolvedContext.workflowType === 'full') {
            executionHints.workflow_type = resolvedContext.workflowType;
            if (resolvedContext.workflowType === 'analysis_only') {
                executionHints.requires_file_changes = false;
            }
        }
        const payload = {
            workspace: this.buildPublicWorkspaceDescriptor(resolvedContext.workspace, {
                localWorkspacePath,
                serverWorkspacePath,
            }),
            activeFile: resolvedContext.activeFile || null,
            history: resolvedContext.history || [],
            agentTaskType,
            workflowType: executionHints.workflow_type,
            rawPrompt,
            contextualPrompt: resolvedContext.contextualPrompt || '',
            executionHints,
            model: resolvedModel,
            requestedModel,
            requestedModelResolved: resolvedModel,
            agentExecutionPolicy: resolvedContext.agentExecutionPolicy || null,
            legacyFallbackAllowed: resolvedContext.legacyFallbackAllowed === true,
            executionSurface: resolvedContext.executionSurface || 'cli',
            clientSurface: resolvedContext.clientSurface || 'cli',
            localMachineCapable,
            clientToolExecution,
            client_tool_execution: clientToolExecution,
            runtimeEnvironment: publicRuntimeEnvironment,
            workspacePath: effectiveWorkspacePath,
            projectPath: effectiveWorkspacePath,
            targetPath: effectiveWorkspacePath,
            // Never send a client-local absolute path to the V3 model boundary.
            // The server only needs a stable label; file contents are carried by
            // localWorkspaceSummary.workspaceFiles and hydrated into a temp workspace.
            localWorkspacePath: serverWorkspacePath ? serverWorkspacePath : localWorkspaceRef,
            localWorkspaceRef,
            localWorkspaceName,
            localWorkspaceSummary,
            // Signal to the server that the workspace filesystem is not locally
            // accessible — it must hydrate a temp directory from the provided
            // workspaceFiles before the agent starts using tools.
            workspaceHydrationRequired: needsHydration,
            contextId: resolvedContext.contextId,
            traceId: resolvedContext.traceId,
            mcpContextId: resolvedContext.mcpContextId || null,
            mcp_context_id: resolvedContext.mcpContextId || null,
            requestStartedAt: resolvedContext.requestStartedAt,
            subscriptionPlan: this.config.getNormalizedPlan() || null,
            email: this.config.get('email') || null,
            vigthoriaBrain: resolvedContext.vigthoriaBrain || null,
            vigthoria_brain: resolvedContext.vigthoriaBrain || null,
            codebaseContext: resolvedContext.codebaseContext || '',
            accountBrainContext: resolvedContext.accountBrainContext || '',
            projectMemory: resolvedContext.projectMemory || '',
            clientManifest: buildClientManifest(resolvedContext.clientManifest && typeof resolvedContext.clientManifest === 'object'
                ? resolvedContext.clientManifest
                : {}),
        };
        return this.compactV3Context(payload);
    }
    /**
     * Compact a V3 context payload so the serialized JSON stays under
     * the server's character limit.  Progressively sheds bulk:
     *   1. Trim workspaceFiles values to fit budget
     *   2. Drop workspaceFiles entirely
     *   3. Truncate history
     *   4. Truncate file list
     *   5. Drop readmeExcerpt
     */
    compactV3Context(payload) {
        const LIMIT = resolveV3ContextCharLimit();
        const phases = [];
        const compactionMeta = {
            applied: false,
            beforeChars: 0,
            afterChars: 0,
            charLimit: LIMIT,
            estimatedInputBudget: Math.floor(LIMIT / 4),
            phases,
        };
        if (payload.vigthoriaBrain) {
            payload.vigthoriaBrain = this.dedupeBrainPayload(payload.vigthoriaBrain);
        }
        if (payload.vigthoria_brain) {
            payload.vigthoria_brain = payload.vigthoriaBrain || this.dedupeBrainPayload(payload.vigthoria_brain);
        }
        let json = JSON.stringify(payload);
        compactionMeta.beforeChars = json.length;
        if (json.length <= LIMIT) {
            compactionMeta.afterChars = json.length;
            payload.contextBudgetMeta = compactionMeta;
            return json;
        }
        compactionMeta.applied = true;
        phases.push('over_limit');
        this.logger.debug(`Workspace context ${json.length} chars exceeds ${LIMIT}; applying token-aligned compaction.`);
        const summary = payload.localWorkspaceSummary;
        const hydrationRequired = payload.workspaceHydrationRequired === true;
        const finish = () => {
            compactionMeta.afterChars = json.length;
            payload.contextBudgetMeta = compactionMeta;
            return json;
        };
        // Phase 1 — truncate history before touching workspace file hydration
        if (Array.isArray(payload.history) && payload.history.length > 6) {
            payload.history = payload.history.slice(-6);
            phases.push('trim_history_6');
            json = JSON.stringify(payload);
            if (json.length <= LIMIT)
                return finish();
        }
        // Phase 2 — shrink workspaceFiles (priority order) to fit
        if (summary?.workspaceFiles && typeof summary.workspaceFiles === 'object') {
            const overhead = json.length - JSON.stringify(summary.workspaceFiles).length;
            const budget = LIMIT - overhead - 1024;
            if (budget > 0) {
                summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, budget);
                summary.workspaceFilesCompaction = Object.keys(summary.workspaceFiles).length > 0 ? 'priority-trimmed' : 'empty';
            }
            else if (hydrationRequired) {
                summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 8_000);
                summary.workspaceFilesCompaction = 'priority-minimal';
            }
            else {
                delete summary.workspaceFiles;
            }
            json = JSON.stringify(payload);
            if (json.length <= LIMIT)
                return finish();
        }
        // Phase 3 — drop readme before dropping hydration payload
        if (summary?.readmeExcerpt) {
            delete summary.readmeExcerpt;
            json = JSON.stringify(payload);
            if (json.length <= LIMIT)
                return finish();
        }
        // Phase 4 — never fully drop workspaceFiles when remote hydration is required
        if (summary?.workspaceFiles && !hydrationRequired) {
            delete summary.workspaceFiles;
            json = JSON.stringify(payload);
            if (json.length <= LIMIT)
                return finish();
        }
        if (hydrationRequired && summary?.workspaceFiles && typeof summary.workspaceFiles === 'object') {
            summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 4_000);
            summary.workspaceFilesCompaction = 'priority-minimal';
            json = JSON.stringify(payload);
            if (json.length <= LIMIT)
                return finish();
        }
        // Phase 5 — truncate history to last 3 messages
        if (Array.isArray(payload.history) && payload.history.length > 3) {
            payload.history = payload.history.slice(-3);
            json = JSON.stringify(payload);
            if (json.length <= LIMIT)
                return finish();
        }
        // Phase 6 — truncate file list
        if (summary?.files && Array.isArray(summary.files) && summary.files.length > 15) {
            summary.files = summary.files.slice(0, 15);
            json = JSON.stringify(payload);
            if (json.length <= LIMIT)
                return finish();
        }
        // Phase 7 — drop history entirely
        if (Array.isArray(payload.history) && payload.history.length > 0) {
            payload.history = [];
            json = JSON.stringify(payload);
            if (json.length <= LIMIT)
                return finish();
        }
        // Phase 8 — drop localWorkspaceSummary metadata but keep hydration files for remote clients
        if (payload.localWorkspaceSummary) {
            const keepFiles = hydrationRequired && summary?.workspaceFiles && typeof summary.workspaceFiles === 'object'
                ? summary.workspaceFiles
                : undefined;
            payload.localWorkspaceSummary = {
                path: summary?.path,
                name: summary?.name,
                fileCount: summary?.fileCount,
                files: Array.isArray(summary?.files) ? summary.files.slice(0, 20) : [],
                ...(keepFiles ? { workspaceFiles: keepFiles, workspaceFilesCompaction: summary?.workspaceFilesCompaction || 'priority-minimal' } : {}),
            };
            json = JSON.stringify(payload);
        }
        if (hydrationRequired && json.length > LIMIT) {
            process.stderr.write('Warning: workspace context is still large after compaction; remote agent may have partial project visibility. Consider closing other context or using /compact.\n');
        }
        return finish();
    }
    /** Normalize common Unix-only commands before Windows cmd/powershell execution. */
    normalizeWindowsRunCommand(command, serverRoot = '') {
        let cmd = String(command || '').trim();
        if (!cmd)
            return cmd;
        if (serverRoot) {
            const escapedRoot = serverRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            cmd = cmd.replace(new RegExp(`^\\s*cd\\s+(?:[\"']?${escapedRoot}[\"']?)\\s*(?:&&|;)\\s*`, 'i'), '');
        }
        if (/^\/[^\s/]/.test(cmd) && !/^\/[a-z]:/i.test(cmd)) {
            throw new Error('Command starts with "/" which Windows interprets as an invalid switch. Use workspace-relative paths without a leading slash.');
        }
        const replacements = [
            [/^ls(\s|$)/i, 'dir$1'],
            [/^cat\s+/i, 'type '],
            [/^pwd\s*$/i, 'cd'],
            [/^rm\s+-rf?\s+/i, 'rmdir /s /q '],
            [/^rm\s+/i, 'del '],
            [/^cp\s+/i, 'copy '],
            [/^mv\s+/i, 'move '],
        ];
        for (const [pattern, replacement] of replacements) {
            cmd = cmd.replace(pattern, replacement);
        }
        return cmd;
    }
    resolveRunCommandCwd(rootPath, args, serverRoot) {
        const cwdRaw = args.cwd ?? args.path ?? '.';
        let target = this.resolveV3ClientToolPath(rootPath, cwdRaw, serverRoot ? [serverRoot] : []);
        if (!target) {
            target = this.resolveV3ClientToolPath(rootPath, '.', serverRoot ? [serverRoot] : []);
        }
        if (!target) {
            return path.resolve(rootPath);
        }
        try {
            if (fs.existsSync(target.absolutePath) && fs.statSync(target.absolutePath).isFile()) {
                return path.dirname(target.absolutePath);
            }
        }
        catch {
            // fall through to directory cwd
        }
        return target.absolutePath;
    }
    /** Remove duplicate functionIndex entries that poison model context after compaction. */
    dedupeBrainPayload(brain) {
        if (!brain || typeof brain !== 'object') {
            return brain;
        }
        const copy = { ...brain };
        const dedupeIndexEntries = (entries) => {
            const seen = new Set();
            const unique = [];
            for (const entry of entries) {
                if (!entry || typeof entry !== 'object')
                    continue;
                const file = String(entry.file || '')
                    .replace(/\\/g, '/')
                    .replace(/^home\/user\//i, '')
                    .toLowerCase();
                const name = String(entry.name || '');
                const line = String(entry.line ?? '');
                const key = `${name}::${file}::${line}`;
                if (seen.has(key))
                    continue;
                seen.add(key);
                unique.push(entry);
            }
            return unique;
        };
        if (copy.functionIndex && typeof copy.functionIndex === 'object') {
            const deduped = {};
            for (const [symbol, entries] of Object.entries(copy.functionIndex)) {
                deduped[symbol] = Array.isArray(entries) ? dedupeIndexEntries(entries) : [];
            }
            copy.functionIndex = deduped;
        }
        if (copy.files && typeof copy.files === 'object') {
            const normalized = {};
            for (const [filePath, meta] of Object.entries(copy.files)) {
                const norm = String(filePath).replace(/\\/g, '/').replace(/^home\/user\//i, '');
                if (!(norm in normalized)) {
                    normalized[norm] = meta;
                }
            }
            copy.files = normalized;
        }
        return copy;
    }
    buildMinimalV3AgentContext(context = {}) {
        const resolvedContext = this.ensureExecutionContext(context);
        const targetPath = this.resolveAgentTargetPath(resolvedContext)
            || resolvedContext.targetPath
            || resolvedContext.projectPath
            || resolvedContext.workspacePath
            || resolvedContext.projectRoot
            || process.cwd();
        const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
        const localWorkspaceName = this.getDisplayWorkspaceName(targetPath);
        const localWorkspaceRef = localWorkspaceName ? `vigthoria://local-workspace/${localWorkspaceName}` : null;
        const rawBrain = resolvedContext.vigthoriaBrain || resolvedContext.vigthoria_brain || null;
        const dedupedBrain = rawBrain ? this.dedupeBrainPayload(rawBrain) : null;
        const agentTaskType = resolvedContext.agentTaskType || 'general';
        const rawPrompt = resolvedContext.rawPrompt || resolvedContext.prompt || '';
        const executionHints = (resolvedContext.executionHints && typeof resolvedContext.executionHints === 'object')
            ? { ...resolvedContext.executionHints }
            : buildExecutionHints(agentTaskType, rawPrompt, {
                fastPath: resolvedContext.fastPath,
            });
        if (!executionHints.task_kind) {
            executionHints.task_kind = agentTaskType;
        }
        if (!executionHints.classify_from) {
            executionHints.classify_from = rawPrompt;
        }
        if (resolvedContext.workflowType === 'analysis_only' || resolvedContext.workflowType === 'full') {
            executionHints.workflow_type = resolvedContext.workflowType;
            if (resolvedContext.workflowType === 'analysis_only') {
                executionHints.requires_file_changes = false;
            }
        }
        return JSON.stringify({
            workspace: this.buildPublicWorkspaceDescriptor(resolvedContext.workspace, {
                localWorkspacePath: targetPath,
                serverWorkspacePath,
            }),
            activeFile: resolvedContext.activeFile || null,
            history: resolvedContext.history || [],
            agentTaskType,
            workflowType: executionHints.workflow_type,
            rawPrompt,
            contextualPrompt: resolvedContext.contextualPrompt || '',
            executionHints,
            executionSurface: resolvedContext.executionSurface || 'cli',
            clientSurface: resolvedContext.clientSurface || 'cli',
            localMachineCapable: resolvedContext.localMachineCapable !== false,
            runtimeEnvironment: this.buildPublicRuntimeEnvironment(resolvedContext.agentRuntime, {
                localWorkspacePath: targetPath,
                serverWorkspacePath,
            }),
            workspacePath: serverWorkspacePath || null,
            projectPath: serverWorkspacePath || null,
            targetPath: serverWorkspacePath || null,
            localWorkspacePath: serverWorkspacePath || localWorkspaceRef,
            localWorkspaceRef,
            localWorkspaceName,
            contextId: resolvedContext.contextId,
            traceId: resolvedContext.traceId,
            requestStartedAt: resolvedContext.requestStartedAt,
            vigthoriaBrain: dedupedBrain,
            vigthoria_brain: dedupedBrain,
        });
    }
    extractEmergencyAppName(message = '', fallback = 'Signal Desk') {
        const match = String(message || '').match(/called\s+([A-Z][A-Za-z0-9&\- ]{2,40})/i);
        return match?.[1]?.trim() || fallback;
    }
    materializeEmergencySaaSWorkspace(message = '', context = {}) {
        const rootPath = this.resolveAgentTargetPath(context);
        if (!rootPath) {
            return null;
        }
        fs.mkdirSync(rootPath, { recursive: true });
        const appName = this.extractEmergencyAppName(message);
        const html = `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${appName}</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <div class="app-shell">
    <aside class="sidebar">
      <div class="brand">${appName}</div>
      <button class="menu-toggle" id="menu-toggle" aria-label="Toggle navigation">Menu</button>
      <nav>
        <a href="#dashboard" class="nav-link active">Dashboard</a>
        <a href="#team" class="nav-link">Team</a>
        <a href="#billing" class="nav-link">Billing</a>
        <a href="#settings" class="nav-link">Settings</a>
      </nav>
    </aside>
    <main class="content">
      <section class="hero-card panel active-panel" id="dashboard">
        <div class="hero-copy">
          <p class="eyebrow">Dashboard</p>
          <h1>${appName} revenue command center</h1>
          <p>Track login activity, campaign velocity, billing state, and team performance from one responsive SaaS workspace.</p>
        </div>
        <form class="login-card">
          <h2>Login</h2>
          <label>Email<input type="email" placeholder="ops@${appName.toLowerCase().replace(/[^a-z0-9]+/g, '') || 'signaldesk'}.io"></label>
          <label>Password<input type="password" placeholder="Enter password"></label>
          <button type="submit">Enter dashboard</button>
        </form>
      </section>

      <section class="stats-grid">
        <article class="stat-card"><span>MRR</span><strong>$284K</strong><em>+12.4%</em></article>
        <article class="stat-card"><span>Activation</span><strong>74%</strong><em>+6.1%</em></article>
        <article class="stat-card"><span>Team Seats</span><strong>128</strong><em>8 pending</em></article>
        <article class="stat-card"><span>Churn Risk</span><strong>2.1%</strong><em>Low</em></article>
      </section>

      <section class="workspace-grid">
        <article class="panel chart-panel">
          <div class="panel-header">
            <h2>Analytics</h2>
            <button id="open-modal" type="button">Add campaign</button>
          </div>
          <div class="chart-bars" aria-label="Revenue chart">
            <div class="bar" style="--value: 52%"><span>Mon</span></div>
            <div class="bar" style="--value: 68%"><span>Tue</span></div>
            <div class="bar" style="--value: 74%"><span>Wed</span></div>
            <div class="bar" style="--value: 59%"><span>Thu</span></div>
            <div class="bar" style="--value: 88%"><span>Fri</span></div>
          </div>
        </article>

        <article class="panel activity-panel">
          <div class="panel-header"><h2>Activity Feed</h2><span>Live</span></div>
          <ul class="activity-feed">
            <li><strong>Billing</strong><span>Enterprise invoice paid</span></li>
            <li><strong>Team</strong><span>New strategist invited to workspace</span></li>
            <li><strong>Dashboard</strong><span>KPI threshold updated for activation alerts</span></li>
          </ul>
        </article>

        <article class="panel" id="team">
          <div class="panel-header"><h2>Team Management</h2><span>Owners and operators</span></div>
          <div class="team-list">
            <div><strong>Ana</strong><span>Growth lead</span></div>
            <div><strong>Marcus</strong><span>Billing admin</span></div>
            <div><strong>Lina</strong><span>Lifecycle analyst</span></div>
          </div>
        </article>

        <article class="panel" id="billing">
          <div class="panel-header"><h2>Billing</h2><span>Current plan</span></div>
          <div class="billing-card">
            <strong>Scale Annual</strong>
            <p>Renews on 12 Oct with usage-based analytics overages.</p>
            <button type="button" class="secondary-action">Update payment method</button>
          </div>
        </article>

        <article class="panel" id="settings">
          <div class="panel-header"><h2>Settings</h2><span>Automation and alerts</span></div>
          <form class="settings-form">
            <label>Alert threshold<input type="number" value="18"></label>
            <label>Weekly digest<select><option>Enabled</option><option>Paused</option></select></label>
            <button type="submit">Save settings</button>
          </form>
        </article>
      </section>
    </main>
  </div>

  <dialog id="campaign-modal">
    <form method="dialog" class="modal-form">
      <h2>Launch campaign</h2>
      <label>Name<input type="text" placeholder="Retention push"></label>
      <label>Owner<input type="text" placeholder="Lina"></label>
      <menu>
        <button value="cancel">Cancel</button>
        <button value="confirm">Create</button>
      </menu>
    </form>
  </dialog>

  <script src="scripts.js"></script>
</body>
</html>
`;
        const css = `:root {
  --bg: #f2ede4;
  --ink: #18222f;
  --muted: #5c6674;
  --panel: rgba(255, 255, 255, 0.82);
  --line: rgba(24, 34, 47, 0.08);
  --accent: #b6542c;
  --accent-strong: #7f3417;
  --shadow: 0 24px 60px rgba(24, 34, 47, 0.12);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: "Georgia", "Times New Roman", serif;
  color: var(--ink);
  background:
    radial-gradient(circle at top left, rgba(182, 84, 44, 0.18), transparent 28%),
    radial-gradient(circle at bottom right, rgba(24, 34, 47, 0.14), transparent 30%),
    var(--bg);
}

.app-shell {
  min-height: 100vh;
  display: grid;
  grid-template-columns: 260px 1fr;
}

.sidebar {
  padding: 2rem 1.25rem;
  background: rgba(24, 34, 47, 0.94);
  color: #f7f2eb;
  position: sticky;
  top: 0;
  min-height: 100vh;
}

.brand {
  font-size: 1.6rem;
  font-weight: 700;
  margin-bottom: 1.5rem;
}

.menu-toggle {
  display: none;
  margin-bottom: 1rem;
}

nav {
  display: grid;
  gap: 0.6rem;
}

.nav-link {
  color: inherit;
  text-decoration: none;
  padding: 0.8rem 0.95rem;
  border-radius: 999px;
  transition: transform 0.25s ease, background-color 0.25s ease;
}

.nav-link:hover,
.nav-link.active {
  background: rgba(255, 255, 255, 0.12);
  transform: translateX(4px);
}

.content {
  padding: 2rem;
}

.hero-card,
.panel,
.stat-card,
.login-card,
dialog {
  background: var(--panel);
  backdrop-filter: blur(16px);
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
}

.hero-card {
  display: grid;
  grid-template-columns: 1.3fr 0.9fr;
  gap: 1.5rem;
  border-radius: 32px;
  padding: 2rem;
  margin-bottom: 1.5rem;
}

.eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--accent-strong);
  font-size: 0.78rem;
}

.hero-card h1,
.panel h2,
.login-card h2 {
  margin: 0 0 0.75rem;
}

.login-card,
.panel,
.stat-card {
  border-radius: 24px;
}

.login-card,
.settings-form,
.modal-form {
  display: grid;
  gap: 0.85rem;
}

.stats-grid,
.workspace-grid {
  display: grid;
  gap: 1rem;
}

.stats-grid {
  grid-template-columns: repeat(4, minmax(0, 1fr));
  margin-bottom: 1rem;
}

.workspace-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.stat-card,
.panel {
  padding: 1.2rem;
  animation: riseIn 0.7s ease forwards;
}

.stat-card span,
.panel-header span,
.activity-feed span,
.team-list span,
.billing-card p {
  color: var(--muted);
}

.panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1rem;
}

.chart-bars {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 0.9rem;
  align-items: end;
  min-height: 220px;
}

.bar {
  position: relative;
  min-height: 180px;
  border-radius: 20px 20px 8px 8px;
  background: linear-gradient(180deg, rgba(182, 84, 44, 0.92), rgba(127, 52, 23, 0.68));
  transform-origin: bottom;
  transform: scaleY(calc(var(--value) / 100));
  transition: transform 0.6s ease;
}

.bar span {
  position: absolute;
  left: 50%;
  bottom: -1.6rem;
  transform: translateX(-50%);
}

.activity-feed,
.team-list {
  display: grid;
  gap: 0.8rem;
  padding: 0;
  margin: 0;
  list-style: none;
}

.activity-feed li,
.team-list div,
.billing-card {
  padding: 0.9rem 1rem;
  border-radius: 18px;
  background: rgba(255, 255, 255, 0.7);
  border: 1px solid var(--line);
}

label {
  display: grid;
  gap: 0.35rem;
  font-size: 0.95rem;
}

input,
select,
button {
  font: inherit;
}

input,
select {
  width: 100%;
  padding: 0.85rem 1rem;
  border-radius: 14px;
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.92);
}

button {
  border: none;
  border-radius: 999px;
  padding: 0.85rem 1.2rem;
  background: var(--accent);
  color: #fff9f3;
  cursor: pointer;
  transition: transform 0.25s ease, background-color 0.25s ease;
}

button:hover {
  background: var(--accent-strong);
  transform: translateY(-2px);
}

.secondary-action,
menu button:first-child {
  background: rgba(24, 34, 47, 0.12);
  color: var(--ink);
}

dialog {
  border-radius: 28px;
  padding: 0;
  width: min(420px, calc(100% - 2rem));
}

dialog::backdrop {
  background: rgba(24, 34, 47, 0.3);
}

.modal-form {
  padding: 1.4rem;
}

menu {
  display: flex;
  justify-content: flex-end;
  gap: 0.75rem;
  padding: 0;
  margin: 0.5rem 0 0;
}

@keyframes riseIn {
  from {
    opacity: 0;
    transform: translateY(18px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (max-width: 980px) {
  .app-shell,
  .hero-card,
  .stats-grid,
  .workspace-grid {
    grid-template-columns: 1fr;
  }

  .sidebar {
    position: static;
    min-height: auto;
  }

  .menu-toggle {
    display: inline-flex;
  }

  nav {
    display: none;
  }

  nav.is-open {
    display: grid;
  }
}
`;
        const js = `document.addEventListener('DOMContentLoaded', () => {
  const menuToggle = document.getElementById('menu-toggle');
  const nav = document.querySelector('nav');
  const modal = document.getElementById('campaign-modal');
  const openModal = document.getElementById('open-modal');
  const navLinks = document.querySelectorAll('.nav-link');

  menuToggle?.addEventListener('click', () => nav?.classList.toggle('is-open'));
  openModal?.addEventListener('click', () => modal?.showModal());
  modal?.addEventListener('close', () => document.body.classList.remove('modal-open'));

  navLinks.forEach((link) => {
    link.addEventListener('click', (event) => {
      event.preventDefault();
      navLinks.forEach((entry) => entry.classList.remove('active'));
      link.classList.add('active');
      document.querySelector(link.getAttribute('href'))?.scrollIntoView({ behavior: 'smooth', block: 'start' });
      nav?.classList.remove('is-open');
    });
  });

  document.querySelectorAll('.bar').forEach((bar, index) => {
    bar.animate([
      { transform: 'scaleY(0.15)' },
      { transform: getComputedStyle(bar).transform || 'scaleY(1)' }
    ], { duration: 600 + index * 80, fill: 'forwards', easing: 'ease-out' });
  });
});
`;
        fs.writeFileSync(path.join(rootPath, 'index.html'), `${html.trimEnd()}\n`, 'utf8');
        fs.writeFileSync(path.join(rootPath, 'styles.css'), `${css.trimEnd()}\n`, 'utf8');
        fs.writeFileSync(path.join(rootPath, 'scripts.js'), `${js.trimEnd()}\n`, 'utf8');
        return appName;
    }
    ensureExecutionContext(context = {}) {
        const existingId = String(context.contextId || context.traceId || '').trim();
        const contextId = existingId || `vig-${Date.now()}-${randomUUID().slice(0, 8)}`;
        return {
            ...context,
            contextId,
            traceId: context.traceId || contextId,
            requestStartedAt: context.requestStartedAt || new Date().toISOString(),
        };
    }
    async bindExecutionContext(context = {}) {
        const executionContext = this.ensureExecutionContext(context);
        const headers = await this.getMcpHeaders();
        const localWorkspacePath = this.resolveAgentTargetPath(executionContext);
        const workspacePath = this.resolveServerBindableWorkspacePath(executionContext);
        const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, String(executionContext.rawPrompt || executionContext.contextualPrompt || executionContext.prompt || ''));
        const metadata = {
            source: 'vigthoria-cli',
            sharedContextId: executionContext.contextId,
            traceId: executionContext.traceId,
            executionSurface: executionContext.executionSurface || 'cli',
            clientSurface: executionContext.clientSurface || 'cli',
            workspacePath: workspacePath || null,
            localWorkspacePath: localWorkspacePath || null,
            localWorkspaceSummary,
            runtimeEnvironment: executionContext.agentRuntime || null,
            requestStartedAt: executionContext.requestStartedAt,
            subscriptionPlan: this.config.getNormalizedPlan() || null,
            email: this.config.get('email') || null,
        };
        const data = {
            sharedContextId: executionContext.contextId,
            traceId: executionContext.traceId,
            requestStartedAt: executionContext.requestStartedAt,
            workspacePath: workspacePath || null,
            projectPath: workspacePath || null,
            targetPath: workspacePath || null,
            localWorkspacePath: localWorkspacePath || null,
            localWorkspaceSummary,
            runtimeEnvironment: executionContext.agentRuntime || null,
            activeFile: executionContext.activeFile || null,
            executionSurface: executionContext.executionSurface || 'cli',
            clientSurface: executionContext.clientSurface || 'cli',
            userId: this.config.get('userId') || null,
            email: this.config.get('email') || null,
        };
        const updateExisting = async (binding) => {
            const response = await fetch(this.getMcpContextUrl(binding.backendUrl, binding.mcpContextId), {
                method: 'PUT',
                headers,
                body: JSON.stringify({ data }),
            });
            if (!response.ok) {
                const errorText = await response.text().catch(() => '');
                throw new Error(`MCP context update ${response.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
            }
            return {
                ...executionContext,
                localWorkspacePath: localWorkspacePath || executionContext.localWorkspacePath || null,
                targetPath: localWorkspacePath || executionContext.targetPath || executionContext.projectPath || executionContext.workspacePath || null,
                projectPath: localWorkspacePath || executionContext.projectPath || executionContext.targetPath || executionContext.workspacePath || null,
                workspacePath: localWorkspacePath || executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || null,
                mcpContextId: binding.mcpContextId,
                mcpContextBackendUrl: binding.backendUrl,
            };
        };
        if (executionContext.mcpContextId && executionContext.mcpContextBackendUrl) {
            try {
                return await updateExisting({
                    mcpContextId: String(executionContext.mcpContextId),
                    backendUrl: String(executionContext.mcpContextBackendUrl),
                });
            }
            catch (error) {
                this.logger.debug('Failed to refresh existing MCP context binding:', error.message);
            }
        }
        for (const baseUrl of this.getMcpBaseUrls()) {
            try {
                const createResponse = await fetch(this.getMcpContextCreateUrl(baseUrl), {
                    method: 'POST',
                    headers,
                    body: JSON.stringify({
                        userId: this.config.get('userId') || this.config.get('email') || 'vigthoria-cli',
                        metadata,
                    }),
                });
                if (!createResponse.ok) {
                    const errorText = await createResponse.text().catch(() => '');
                    throw new Error(`MCP context create ${createResponse.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
                }
                const payload = await createResponse.json();
                const mcpContextId = String(payload.contextId || '').trim();
                if (!mcpContextId) {
                    throw new Error('MCP context create response did not include a contextId');
                }
                return await updateExisting({ mcpContextId, backendUrl: baseUrl });
            }
            catch (error) {
                this.logger.debug(`Failed to bind MCP context via ${baseUrl}:`, error.message);
            }
        }
        return {
            ...executionContext,
            localWorkspacePath: localWorkspacePath || executionContext.localWorkspacePath || null,
            targetPath: localWorkspacePath || executionContext.targetPath || executionContext.projectPath || executionContext.workspacePath || null,
            projectPath: localWorkspacePath || executionContext.projectPath || executionContext.targetPath || executionContext.workspacePath || null,
            workspacePath: localWorkspacePath || executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || null,
        };
    }
    isServerManagedTempWorkspacePath(pathValue) {
        const normalized = String(pathValue || '').replace(/\\/g, '/');
        return /(?:^|\/)v3-temp\/vig-remote-/i.test(normalized)
            || /(?:^|\/)tmp\/vig-remote(?:-server)?-/i.test(normalized)
            || /(?:^|\/)var\/tmp\/vig-remote(?:-server)?-/i.test(normalized);
    }
    resolveAgentTargetPath(context = {}) {
        const explicitLocal = String(context.localWorkspacePath || '').trim();
        if (explicitLocal
            && !explicitLocal.startsWith('vigthoria://')
            && (this.isLikelyWindowsPath(explicitLocal) || path.isAbsolute(explicitLocal))) {
            return explicitLocal;
        }
        const candidate = context.targetPath || context.projectPath || context.workspacePath || context.projectRoot || process.cwd();
        if (this.isServerManagedTempWorkspacePath(candidate) && explicitLocal && !explicitLocal.startsWith('vigthoria://')) {
            return explicitLocal;
        }
        if (this.isServerManagedTempWorkspacePath(candidate)) {
            const preserved = String(context.__localWorkspacePath || context.clientWorkspacePath || '').trim();
            if (preserved && !preserved.startsWith('vigthoria://')) {
                return preserved;
            }
        }
        return candidate;
    }
    isLikelyWindowsPath(pathValue) {
        return /^[a-zA-Z]:[\\/]/.test(pathValue) || /^\\\\/.test(pathValue);
    }
    resolveServerBindableWorkspacePath(context = {}) {
        const candidate = this.resolveAgentTargetPath(context);
        if (!candidate || !path.isAbsolute(candidate) || !fs.existsSync(candidate)) {
            return '';
        }
        const configuredRoots = (process.env.VIGTHORIA_SERVER_WORKSPACE_ROOTS || '/var/www/vigthoria-user-workspaces,/var/lib/vigthoria-workspaces')
            .split(',')
            .map((entry) => entry.trim())
            .filter(Boolean);
        const resolvedCandidate = fs.realpathSync(candidate);
        for (const root of configuredRoots) {
            if (!fs.existsSync(root)) {
                continue;
            }
            const resolvedRoot = fs.realpathSync(root);
            try {
                if (path.relative(resolvedRoot, resolvedCandidate) === '' || !path.relative(resolvedRoot, resolvedCandidate).startsWith('..')) {
                    return resolvedCandidate;
                }
            }
            catch {
                // Ignore malformed path relationships.
            }
        }
        return '';
    }
    getDisplayWorkspaceName(workspacePath) {
        const cleaned = String(workspacePath || '').trim().replace(/[\\/]+$/g, '');
        if (!cleaned) {
            return null;
        }
        const parts = cleaned.split(/[\\/]+/).filter(Boolean);
        return parts.length > 0 ? parts[parts.length - 1] : cleaned;
    }
    buildPublicWorkspaceDescriptor(workspace, paths = {}) {
        const localName = this.getDisplayWorkspaceName(paths.localWorkspacePath || '');
        const isServerBindable = !!paths.serverWorkspacePath;
        const descriptor = {
            name: localName,
            path: isServerBindable ? paths.serverWorkspacePath : (localName ? `vigthoria://local-workspace/${localName}` : null),
            scheme: isServerBindable ? 'server-bindable' : 'local-workspace',
        };
        if (workspace && typeof workspace === 'object') {
            for (const [key, value] of Object.entries(workspace)) {
                if (key === 'path' || key === 'workspacePath' || key === 'projectPath' || key === 'targetPath' || key === 'cwd') {
                    continue;
                }
                descriptor[key] = value;
            }
        }
        return descriptor.name || descriptor.path ? descriptor : null;
    }
    buildPublicRuntimeEnvironment(runtime, paths = {}) {
        if (!runtime || typeof runtime !== 'object') {
            return null;
        }
        const localName = this.getDisplayWorkspaceName(paths.localWorkspacePath || '');
        const serverBindableWorkspace = !!paths.serverWorkspacePath || runtime.serverBindableWorkspace === true;
        return {
            osPlatform: runtime.osPlatform || null,
            platform: runtime.platform || null,
            nodeVersion: runtime.nodeVersion || null,
            directPromptMode: runtime.directPromptMode === true,
            serverBindableWorkspace,
            machineScope: serverBindableWorkspace ? 'server-workspace' : 'local-machine',
            workspaceName: localName,
            workspacePath: serverBindableWorkspace ? paths.serverWorkspacePath || null : (localName ? `vigthoria://local-workspace/${localName}` : null),
            cwd: serverBindableWorkspace ? paths.serverWorkspacePath || null : (localName ? `vigthoria://local-workspace/${localName}` : null),
        };
    }
    buildLocalWorkspaceSummary(rootPath, requestFocus = '') {
        if (!rootPath || !fs.existsSync(rootPath)) {
            return null;
        }
        try {
            const summary = {
                path: 'vigthoria://workspace/',
                name: this.getDisplayWorkspaceName(rootPath) || path.basename(rootPath),
                files: [],
            };
            const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
            const candidatePaths = snapshot.paths.filter((entry) => !/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(entry));
            const agentStatePaths = this.collectAgentStateSyncPaths(rootPath);
            let orderedPaths = [...agentStatePaths, ...candidatePaths];
            try {
                const ranked = buildSemanticContext(rootPath, requestFocus, 60).topFiles.map((file) => file.path);
                const changes = getChangedFiles(rootPath, candidatePaths);
                const ordered = new Set();
                // Resume state must outrank semantic source ranking. The previous
                // implementation initialized orderedPaths with agentStatePaths, then
                // replaced it here with candidatePaths only. Since candidatePaths
                // intentionally excludes .vigthoria, every successful semantic pass
                // silently dropped progress.json and forced /continue to re-plan.
                for (const filePath of agentStatePaths)
                    ordered.add(filePath);
                for (const filePath of changes.changed)
                    ordered.add(filePath);
                for (const filePath of ranked)
                    ordered.add(filePath);
                for (const filePath of changes.unchanged)
                    ordered.add(filePath);
                orderedPaths = Array.from(ordered);
                summary.semanticRank = ranked.slice(0, 20);
                summary.changeSummary = {
                    changed: changes.changed.length,
                    unchanged: changes.unchanged.length,
                    total: changes.total,
                };
            }
            catch {
                orderedPaths = [...agentStatePaths, ...candidatePaths];
            }
            summary.fileCount = snapshot.fileCount;
            summary.files = orderedPaths.slice(0, 40);
            const packageJsonPath = path.join(rootPath, 'package.json');
            if (fs.existsSync(packageJsonPath)) {
                const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
                summary.packageJson = {
                    name: pkg.name || null,
                    version: pkg.version || null,
                    scripts: Object.keys(pkg.scripts || {}).slice(0, 12),
                    dependencies: Object.keys(pkg.dependencies || {}).slice(0, 20),
                    devDependencies: Object.keys(pkg.devDependencies || {}).slice(0, 20),
                };
            }
            const readmePath = path.join(rootPath, 'README.md');
            if (fs.existsSync(readmePath)) {
                summary.readmeExcerpt = fs.readFileSync(readmePath, 'utf8').slice(0, 2500);
            }
            // Hydrate workspace: include actual file contents so the V3 server
            // can populate the remote workspace before the agent starts.
            summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, orderedPaths);
            return summary;
        }
        catch (error) {
            this.logger.debug('Failed to build local workspace summary:', error.message);
            return null;
        }
    }
    /**
     * Collect text file contents from the workspace for V3 agent hydration.
     * Budget: up to ~2 MB total, per-file cap 200 KB, skip binary extensions.
     */
    collectWorkspaceFileContents(rootPath, filePaths) {
        const MAX_TOTAL_BYTES = 2 * 1024 * 1024;
        const MAX_FILE_BYTES = 200 * 1024;
        const BINARY_EXTENSIONS = new Set([
            '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp', '.avif',
            '.mp3', '.mp4', '.wav', '.ogg', '.webm', '.flac', '.aac',
            '.zip', '.gz', '.tar', '.rar', '.7z', '.bz2',
            '.exe', '.dll', '.so', '.dylib', '.bin', '.dat',
            '.woff', '.woff2', '.ttf', '.eot', '.otf',
            '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
            '.db', '.sqlite', '.sqlite3',
            '.pyc', '.pyo', '.class', '.o', '.obj',
            '.DS_Store', '.lock',
        ]);
        const result = {};
        let totalBytes = 0;
        for (const relativePath of filePaths) {
            if (totalBytes >= MAX_TOTAL_BYTES)
                break;
            const ext = path.extname(relativePath).toLowerCase();
            if (BINARY_EXTENSIONS.has(ext))
                continue;
            if (/(^|[\/\\])\.(git|hg)([\/\\]|$)/.test(relativePath))
                continue;
            const normalizedPath = relativePath.replace(/\\/g, '/');
            const isAgentStateSync = /^\.vigthoria\/agent-state\//i.test(normalizedPath);
            if (!isAgentStateSync && /(^|[\/\\])\.vigthoria([\/\\]|$)/.test(relativePath))
                continue;
            const absolutePath = path.join(rootPath, relativePath);
            try {
                const stat = fs.statSync(absolutePath);
                if (!stat.isFile() || stat.size > MAX_FILE_BYTES || stat.size === 0)
                    continue;
                const content = fs.readFileSync(absolutePath, 'utf8');
                // Skip likely binary content (high ratio of non-printable chars)
                if (/[\x00-\x08\x0e-\x1f]/.test(content.slice(0, 512)))
                    continue;
                result[relativePath] = content;
                totalBytes += Buffer.byteLength(content, 'utf8');
            }
            catch {
                // Skip unreadable files
            }
        }
        return result;
    }
    hasAgentWorkspaceOutput(context = {}) {
        try {
            const root = this.resolveAgentTargetPath(context);
            if (!root || !fs.existsSync(root)) {
                return false;
            }
            const stack = [root];
            while (stack.length > 0) {
                const current = stack.pop();
                if (!current)
                    continue;
                const entries = fs.readdirSync(current, { withFileTypes: true });
                for (const entry of entries) {
                    if (entry.name === '.git' || entry.name === 'node_modules') {
                        continue;
                    }
                    const fullPath = path.join(current, entry.name);
                    if (entry.isFile()) {
                        return true;
                    }
                    if (entry.isDirectory()) {
                        stack.push(fullPath);
                    }
                }
            }
        }
        catch {
            return false;
        }
        return false;
    }
    getAgentWorkspaceSnapshot(rootPath) {
        const stack = [rootPath];
        let fileCount = 0;
        const entries = [];
        while (stack.length > 0) {
            const current = stack.pop();
            if (!current) {
                continue;
            }
            for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
                if (entry.name === '.git' || entry.name === 'node_modules') {
                    continue;
                }
                const fullPath = path.join(current, entry.name);
                if (entry.isDirectory()) {
                    stack.push(fullPath);
                    continue;
                }
                if (!entry.isFile()) {
                    continue;
                }
                fileCount += 1;
                const stat = fs.statSync(fullPath);
                entries.push(`${path.relative(rootPath, fullPath)}:${stat.size}:${stat.mtimeMs}`);
            }
        }
        entries.sort();
        return {
            fileCount,
            paths: entries.map((entry) => entry.split(':', 1)[0]),
            signature: entries.join('|'),
        };
    }
    async waitForAgentWorkspaceSettle(context = {}, options = {}) {
        const rootPath = this.resolveAgentTargetPath(context);
        if (!rootPath || !fs.existsSync(rootPath)) {
            return;
        }
        const timeoutMs = options.timeoutMs || 15000;
        const pollMs = options.pollMs || 300;
        const stableMs = options.stableMs || 1200;
        const expectedFiles = Array.isArray(options.expectedFiles)
            ? options.expectedFiles.map((entry) => String(entry || '').trim()).filter(Boolean)
            : [];
        const start = Date.now();
        let stableSince = 0;
        let lastSignature = '';
        while (Date.now() - start < timeoutMs) {
            const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
            if (snapshot.fileCount === 0) {
                stableSince = 0;
                lastSignature = '';
                await new Promise((resolve) => setTimeout(resolve, pollMs));
                continue;
            }
            if (expectedFiles.length > 0 && !expectedFiles.every((filePath) => snapshot.paths.includes(filePath))) {
                stableSince = 0;
                lastSignature = snapshot.signature;
                await new Promise((resolve) => setTimeout(resolve, pollMs));
                continue;
            }
            if (snapshot.signature === lastSignature) {
                if (!stableSince) {
                    stableSince = Date.now();
                }
                if (Date.now() - stableSince >= stableMs) {
                    return;
                }
            }
            else {
                lastSignature = snapshot.signature;
                stableSince = Date.now();
            }
            await new Promise((resolve) => setTimeout(resolve, pollMs));
        }
    }
    extractExpectedWorkspaceFiles(message = '', context = {}) {
        const candidates = new Set();
        const addMatches = (value) => {
            const text = String(value || '');
            const patterns = [
                /`([^`]+\.(?:html|css|js|jsx|ts|tsx|json|md|py|sh))`/gi,
                /\b([A-Za-z0-9_./-]+\.(?:html|css|js|jsx|ts|tsx|json|md|py|sh))\b/g,
            ];
            for (const pattern of patterns) {
                let match;
                while ((match = pattern.exec(text)) !== null) {
                    const filePath = match[1].trim().replace(/^\.\//, '');
                    if (filePath && !filePath.startsWith('http://') && !filePath.startsWith('https://')) {
                        candidates.add(filePath);
                    }
                }
            }
        };
        addMatches(message);
        addMatches(context.rawMessage);
        addMatches(context.agentPrompt);
        return Array.from(candidates);
    }
    captureV3AgentStreamMutation(event, streamedFiles, serverRoot) {
        if (!event || !streamedFiles) {
            return;
        }
        if (event.type === 'file_mutation' && typeof event.path === 'string') {
            const filePath = this.normalizeAgentWorkspaceRelativePath(event.path, serverRoot || undefined);
            if (!filePath) {
                return;
            }
            if (event.action === 'delete') {
                delete streamedFiles[filePath];
                return;
            }
            if (typeof event.content === 'string') {
                streamedFiles[filePath] = event.content;
            }
            return;
        }
        // workspace_snapshot is handled by applyV3AgentStreamEventToWorkspace only —
        // never count hydrated baselines as "files changed".
        // Only count confirmed mutations — not workspace snapshots (hydrated baseline)
        // or tool_call previews that may still fail client-side / read-only guards.
        if (event.type === 'tool_result' && event.success) {
            const name = String(event.name || event.tool || '').trim();
            const args = event.arguments || {};
            const targetPath = typeof event.target === 'string'
                ? event.target
                : (typeof args.path === 'string' ? args.path : '');
            if ((name === 'write_file' || name === 'edit_file') && targetPath) {
                const filePath = this.normalizeAgentWorkspaceRelativePath(targetPath, serverRoot || undefined);
                if (filePath && typeof args.content === 'string') {
                    streamedFiles[filePath] = args.content;
                }
            }
        }
    }
    applyV3AgentStreamEventToWorkspace(event, context = {}, serverRoot) {
        if (!event || !['file_mutation', 'workspace_snapshot'].includes(String(event.type || ''))) {
            return false;
        }
        const rootPath = this.resolveAgentTargetPath(context);
        if (!rootPath) {
            return false;
        }
        if (event.type === 'workspace_snapshot' && event.files && typeof event.files === 'object') {
            let applied = false;
            for (const [rawPath, content] of Object.entries(event.files)) {
                if (typeof content !== 'string') {
                    continue;
                }
                applied = this.writeV3AgentWorkspaceFile(rootPath, rawPath, content, serverRoot || undefined) || applied;
            }
            return applied;
        }
        if (event.type === 'file_mutation' && typeof event.path === 'string') {
            const relativePath = this.normalizeAgentWorkspaceRelativePath(event.path, serverRoot || undefined);
            if (!relativePath) {
                return false;
            }
            if (event.action === 'delete') {
                return this.deleteV3AgentWorkspaceFile(rootPath, relativePath);
            }
            if (typeof event.content === 'string') {
                return this.writeV3AgentWorkspaceFile(rootPath, relativePath, event.content, rootPath);
            }
        }
        return false;
    }
    resolveV3ClientToolPath(rootPath, rawPath, sourceRoots = []) {
        const raw = String(rawPath || '.').trim();
        const roots = [rootPath, ...sourceRoots.filter(Boolean)];
        let relativePath = '';
        for (const sourceRoot of roots) {
            relativePath = this.normalizeAgentWorkspaceRelativePath(raw, sourceRoot);
            if (relativePath || raw === '.' || raw === '') {
                break;
            }
        }
        if (!relativePath && raw !== '.' && raw !== '') {
            const treatAsWorkspaceRoot = roots.some((sourceRoot) => isV3WorkspaceRootAliasPath(raw, sourceRoot));
            if (!treatAsWorkspaceRoot) {
                return null;
            }
        }
        const absolutePath = relativePath ? joinV3WorkspacePath(rootPath, relativePath) : path.resolve(rootPath);
        const resolvedRoot = path.resolve(rootPath);
        if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
            return null;
        }
        return { relativePath: relativePath || '.', absolutePath };
    }
    recordV3ClientToolMutation(event, context, result) {
        if (!result.success) {
            return;
        }
        const name = String(event.name || event.tool || '').trim();
        if (name !== 'write_file' && name !== 'edit_file') {
            return;
        }
        const args = event.arguments || {};
        const rootPath = this.resolveAgentTargetPath(context);
        if (!rootPath) {
            return;
        }
        const target = this.resolveV3ClientToolPath(rootPath, args.path || '.');
        if (!target) {
            return;
        }
        const bucket = this.activeV3StreamedFiles || context.__v3StreamedFiles;
        if (!bucket) {
            return;
        }
        if (name === 'write_file' && typeof args.content === 'string') {
            bucket[target.relativePath] = args.content;
            return;
        }
        if (name === 'edit_file') {
            try {
                bucket[target.relativePath] = fs.readFileSync(target.absolutePath, 'utf8');
            }
            catch {
                // ignore read failures; success was already reported by the client tool
            }
        }
    }
    async executeV3ClientToolRequest(event, context = {}) {
        const rootPath = this.resolveAgentTargetPath(context);
        if (!rootPath) {
            return { success: false, output: '', error: 'No local workspace path is available for client tool execution.' };
        }
        const name = String(event.name || event.tool || '').trim();
        const args = event.arguments || {};
        const serverRoot = String(context.__v3ServerWorkspaceRoot || '').trim();
        const isRunCommand = name === 'run_command';
        const pathArg = isRunCommand ? (args.cwd ?? args.path ?? '.') : (args.path || args.cwd || '.');
        let target = this.resolveV3ClientToolPath(rootPath, pathArg, serverRoot ? [serverRoot] : []);
        if (!target && isRunCommand) {
            target = this.resolveV3ClientToolPath(rootPath, '.', serverRoot ? [serverRoot] : []);
        }
        if (!target) {
            return { success: false, output: '', error: 'Tool path is outside the workspace.' };
        }
        try {
            if (name === 'write_file') {
                if (typeof args.content !== 'string')
                    return { success: false, output: '', error: 'write_file requires string content.' };
                fs.mkdirSync(path.dirname(target.absolutePath), { recursive: true });
                fs.writeFileSync(target.absolutePath, args.content, 'utf8');
                const result = { success: true, output: `Updated ${target.relativePath}` };
                this.recordV3ClientToolMutation(event, context, result);
                return result;
            }
            if (name === 'edit_file') {
                const oldString = typeof args.old_string === 'string' ? args.old_string : String(args.old_text || '');
                const newString = typeof args.new_string === 'string' ? args.new_string : String(args.new_text || '');
                if (!oldString)
                    return { success: false, output: '', error: 'edit_file requires old_string.' };
                const existing = fs.readFileSync(target.absolutePath, 'utf8');
                const count = existing.split(oldString).length - 1;
                if (count !== 1)
                    return { success: false, output: '', error: `old_string found ${count} times; expected exactly once.` };
                fs.writeFileSync(target.absolutePath, existing.replace(oldString, newString), 'utf8');
                const result = { success: true, output: `Edited ${target.relativePath}` };
                this.recordV3ClientToolMutation(event, context, result);
                return result;
            }
            if (name === 'read_file') {
                return { success: true, output: fs.readFileSync(target.absolutePath, 'utf8') };
            }
            if (name === 'list_directory') {
                const entries = fs.readdirSync(target.absolutePath, { withFileTypes: true })
                    .filter((entry) => !['node_modules', '.git'].includes(entry.name))
                    .map((entry) => `${entry.isDirectory() ? '[DIR] ' : ''}${entry.name}`);
                return { success: true, output: entries.join('\n') || '(empty)' };
            }
            if (name === 'glob' || name === 'search_files' || name === 'grep') {
                const rawPattern = String(args.pattern || args.query || '').trim();
                const pattern = rawPattern.toLowerCase();
                const matches = [];
                const walk = (dir) => {
                    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
                        if (['node_modules', '.git'].includes(entry.name))
                            continue;
                        const absolute = path.join(dir, entry.name);
                        if (entry.isDirectory()) {
                            walk(absolute);
                            continue;
                        }
                        const relative = path.relative(rootPath, absolute).replace(/\\/g, '/');
                        if (name !== 'glob' && pattern) {
                            try {
                                const content = fs.readFileSync(absolute, 'utf8');
                                if (relative.toLowerCase().includes(pattern.replace(/\*/g, '')) || content.toLowerCase().includes(pattern)) {
                                    matches.push(`${relative}: ${content.split('\n').find((line) => line.toLowerCase().includes(pattern))?.trim() || '(match)'}`);
                                }
                            }
                            catch {
                                // skip unreadable/binary files
                            }
                        }
                    }
                };
                const searchRoot = fs.statSync(target.absolutePath).isDirectory() ? target.absolutePath : rootPath;
                if (name === 'glob') {
                    const normalizedPattern = rawPattern.replace(/\\/g, '/') || '**/*';
                    const globbed = globSync(normalizedPattern, {
                        cwd: searchRoot,
                        nodir: true,
                        dot: false,
                        ignore: ['**/node_modules/**', '**/.git/**'],
                    })
                        .map((entry) => String(entry).replace(/\\/g, '/'))
                        .sort((left, right) => left.localeCompare(right));
                    return { success: true, output: globbed.slice(0, 200).join('\n') || '(no matches)' };
                }
                walk(searchRoot);
                return { success: true, output: matches.slice(0, 100).join('\n') || '(no matches)' };
            }
            if (name === 'syntax_check') {
                if (!fs.existsSync(target.absolutePath)) {
                    return {
                        success: false,
                        output: '',
                        error: `File not found: ${target.relativePath}. Write the file before running syntax_check.`,
                    };
                }
                const stat = fs.statSync(target.absolutePath);
                if (stat.isDirectory()) {
                    return {
                        success: false,
                        output: '',
                        error: `Expected a file, but path is a directory: ${target.relativePath}`,
                    };
                }
                const ext = path.extname(target.absolutePath).toLowerCase();
                if (ext === '.json') {
                    JSON.parse(fs.readFileSync(target.absolutePath, 'utf8'));
                    return { success: true, output: `Syntax check passed: ${target.relativePath}` };
                }
                if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
                    const { execSync } = await import('child_process');
                    try {
                        execSync(`node --check "${target.absolutePath}"`, { stdio: 'pipe' });
                        return { success: true, output: `Syntax check passed: ${target.relativePath}` };
                    }
                    catch (error) {
                        const stderr = error?.stderr?.toString?.() || error?.message || String(error);
                        return { success: false, output: '', error: stderr.trim() || `Syntax check failed: ${target.relativePath}` };
                    }
                }
                fs.readFileSync(target.absolutePath, 'utf8');
                return { success: true, output: `Syntax check passed: ${target.relativePath}` };
            }
            if (name === 'run_command') {
                let command = String(args.command || '').trim();
                if (!command)
                    return { success: false, output: '', error: 'run_command requires command.' };
                try {
                    if (process.platform === 'win32') {
                        command = this.normalizeWindowsRunCommand(command, serverRoot);
                    }
                }
                catch (error) {
                    return { success: false, output: '', error: error.message };
                }
                const cwd = this.resolveRunCommandCwd(rootPath, args, serverRoot);
                const { exec, execFile } = await import('child_process');
                return await new Promise((resolve) => {
                    const timeout = Number(args.timeout || 30000);
                    const finish = (error, stdout, stderr) => {
                        const stdoutText = String(stdout || '');
                        const stderrText = String(stderr || '');
                        const combined = [stdoutText, stderrText].filter(Boolean).join('\n').slice(0, 12000);
                        resolve({
                            success: !error,
                            output: combined,
                            error: error ? String(error.message || error) : '',
                        });
                    };
                    if (process.platform === 'win32') {
                        const commandProcessor = process.env.ComSpec
                            || path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'cmd.exe');
                        execFile(commandProcessor, ['/d', '/s', '/c', command], {
                            cwd, timeout, maxBuffer: 1024 * 1024, env: process.env, windowsHide: true,
                        }, finish);
                        return;
                    }
                    exec(command, { cwd, timeout, maxBuffer: 1024 * 1024, env: process.env }, finish);
                });
            }
            return { success: false, output: '', error: `Unsupported client tool: ${name}` };
        }
        catch (error) {
            return { success: false, output: '', error: error.message };
        }
    }
    async handleV3ClientToolRequest(event, context = {}, responseUrl) {
        const contextId = String(event.context_id || context.contextId || '').trim();
        const callId = String(event.call_id || '').trim();
        const toolName = String(event.tool || event.name || 'client_tool').trim();
        if (!contextId || !callId)
            return;
        const backendUrl = responseUrl ? new URL(responseUrl).origin : (context.mcpContextBackendUrl || null);
        try {
            const result = await this.executeV3ClientToolRequest(event, context);
            try {
                await this.submitClientToolResult(contextId, callId, result, backendUrl);
            }
            catch (submitError) {
                const submitMessage = submitError.message || String(submitError);
                if (result.success) {
                    this.recordClientToolError(`Local ${toolName} succeeded on your machine, but the Vigthoria agent could not accept the result (${submitMessage}). Your workspace may not have synced to the server.`);
                    return;
                }
                throw submitError;
            }
            if (!result.success && result.error) {
                this.recordClientToolError(`Local ${toolName} failed: ${result.error}`);
            }
        }
        catch (error) {
            this.recordClientToolError(`Local tool bridge failed for ${toolName}: ${error.message}`);
            throw error;
        }
    }
    writeV3AgentWorkspaceFile(rootPath, rawPath, content, sourceRoot) {
        const relativePath = this.normalizeAgentWorkspaceRelativePath(rawPath, sourceRoot || rootPath);
        if (!relativePath) {
            return false;
        }
        const absolutePath = joinV3WorkspacePath(rootPath, relativePath);
        const resolvedRoot = path.resolve(rootPath);
        if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
            this.logger.warn(`Refusing to write V3 file outside workspace: ${rawPath}`);
            return false;
        }
        try {
            fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
            if (fs.existsSync(absolutePath)) {
                const existing = fs.readFileSync(absolutePath, 'utf8');
                if (existing === content) {
                    return false;
                }
            }
            fs.writeFileSync(absolutePath, content, 'utf8');
            return true;
        }
        catch (error) {
            this.logger.warn(`Failed to write V3 file ${relativePath}: ${error.message}`);
            return false;
        }
    }
    deleteV3AgentWorkspaceFile(rootPath, rawPath) {
        const relativePath = this.normalizeAgentWorkspaceRelativePath(rawPath, rootPath);
        if (!relativePath) {
            return false;
        }
        const absolutePath = joinV3WorkspacePath(rootPath, relativePath);
        const resolvedRoot = path.resolve(rootPath);
        if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
            this.logger.warn(`Refusing to delete V3 file outside workspace: ${rawPath}`);
            return false;
        }
        try {
            if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile()) {
                fs.unlinkSync(absolutePath);
                return true;
            }
        }
        catch (error) {
            this.logger.warn(`Failed to delete V3 file ${relativePath}: ${error.message}`);
        }
        return false;
    }
    recoverAgentWorkspaceFiles(context = {}, streamedFiles = {}, expectedFiles = []) {
        const rootPath = this.resolveAgentTargetPath(context);
        if (!rootPath || Object.keys(streamedFiles).length === 0) {
            return;
        }
        if (!fs.existsSync(rootPath)) {
            try {
                fs.mkdirSync(rootPath, { recursive: true });
            }
            catch {
                return;
            }
        }
        const targets = expectedFiles.length > 0
            ? Array.from(new Set([...expectedFiles, ...Object.keys(streamedFiles)]))
            : Object.keys(streamedFiles);
        for (const targetPath of targets) {
            const relativePath = this.normalizeAgentWorkspaceRelativePath(targetPath, rootPath);
            if (!relativePath) {
                continue;
            }
            const content = streamedFiles[relativePath];
            if (typeof content !== 'string') {
                continue;
            }
            this.writeV3AgentWorkspaceFile(rootPath, relativePath, content, rootPath);
        }
    }
    normalizeAgentWorkspaceRelativePath(rawPath, rootPath) {
        return normalizeV3WorkspaceRelativePath(rawPath, rootPath);
    }
    async ensureAgentFrontendPolish(message = '', context = {}) {
        const rootPath = this.resolveAgentTargetPath(context);
        if (!rootPath || !fs.existsSync(rootPath)) {
            return;
        }
        const prompt = String(message || '');
        const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
        const looksLikeFrontendTask = /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|pricing|showcase)/i.test(prompt)
            || expectedFiles.some((filePath) => /\.(html|css|js)$/i.test(filePath));
        if (!looksLikeFrontendTask) {
            return;
        }
        const htmlPath = path.join(rootPath, 'index.html');
        if (!fs.existsSync(htmlPath)) {
            return;
        }
        const html = fs.readFileSync(htmlPath, 'utf8');
        const ensuredAssets = this.ensureReferencedFrontendAssets(rootPath, html, prompt, 'Vigthoria CLI');
        const cssPath = ensuredAssets.cssPath;
        const jsPath = ensuredAssets.jsPath;
        let nextHtml = ensuredAssets.html;
        if (!cssPath || !fs.existsSync(cssPath)) {
            return;
        }
        let css = fs.readFileSync(cssPath, 'utf8');
        let js = fs.existsSync(jsPath) ? fs.readFileSync(jsPath, 'utf8') : '';
        const keyframesBlocks = Array.from(js.matchAll(/@keyframes[\s\S]*?\n\}/g)).map((match) => match[0]);
        if (keyframesBlocks.length > 0) {
            const migrated = keyframesBlocks.filter((block) => !css.includes(block));
            if (migrated.length > 0) {
                css = `${css.trimEnd()}\n\n/* Vigthoria CLI Recovered CSS */\n${migrated.join('\n\n')}\n`;
            }
            js = js.replace(/\n?@keyframes[\s\S]*?\n\}/g, '').trim();
            fs.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
            fs.writeFileSync(jsPath, js ? `${js.trimEnd()}\n` : '', 'utf8');
        }
        const wantsPricing = /(pay-as-you-go|pricing)/i.test(prompt);
        if (wantsPricing && !/(id="pricing"|Pay-as-you-go|pay-as-you-go|pricing tiers)/i.test(nextHtml)) {
            nextHtml = this.injectSectionBeforeFooter(nextHtml, `
    <section id="pricing" class="module pricing">
        <h2>Pay-as-you-go Pricing</h2>
        <p>Start with the modules you need, scale usage by workload, and keep enterprise-grade visibility across teams.</p>
        <div class="pricing-grid">
            <article>
                <h3>Builder</h3>
                <p>Usage-based coding, workflow, and storage for product teams shipping fast.</p>
            </article>
            <article>
                <h3>Studio</h3>
                <p>Metered voice and music generation with predictable controls for creative operations.</p>
            </article>
            <article>
                <h3>Scale</h3>
                <p>Hosting, finance, and orchestration capacity that expands with customer demand.</p>
            </article>
        </div>
    </section>`);
            nextHtml = this.injectNavLink(nextHtml, 'pricing', 'Pricing');
        }
        const wantsTrust = /(trust|security|secure)/i.test(prompt);
        if (wantsTrust && !/(id="trust"|id="security"|Trust and Security|Security)/i.test(nextHtml)) {
            nextHtml = this.injectSectionBeforeFooter(nextHtml, `
    <section id="trust" class="module trust">
        <h2>Trust and Security</h2>
        <p>Role-aware access, auditable workflows, and isolated infrastructure keep sensitive workloads controlled from prototype to production.</p>
        <ul class="trust-list">
            <li>Authenticated access across CLI, Code Fork, and hosted workspaces.</li>
            <li>Scalable storage and hosting paths aligned with enterprise deployment requirements.</li>
            <li>Operational visibility for pricing, usage, and automation governance.</li>
        </ul>
    </section>`);
            nextHtml = this.injectNavLink(nextHtml, 'trust', 'Trust');
        }
        const repairedAssets = this.replaceMissingLocalAssetReferences(rootPath, nextHtml, css);
        nextHtml = repairedAssets.html;
        css = repairedAssets.css;
        if (nextHtml !== html) {
            fs.writeFileSync(htmlPath, `${nextHtml.trimEnd()}\n`, 'utf8');
            nextHtml = fs.readFileSync(htmlPath, 'utf8');
        }
        if (css !== fs.readFileSync(cssPath, 'utf8')) {
            fs.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
        }
        if (/classList\.add\('hidden'\)|classList\.add\("hidden"\)|classList\.add\('revealed'\)|classList\.add\("revealed"\)/.test(js)
            && !/\.hidden\b|\.revealed\b/.test(css)) {
            css = `${css.trimEnd()}\n\n/* Vigthoria CLI Visibility States */\n.hidden {\n    opacity: 0;\n    transform: translateY(24px);\n}\n\n.revealed {\n    opacity: 1;\n    transform: translateY(0);\n    transition: opacity 0.7s ease, transform 0.7s ease;\n}\n`;
            fs.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
        }
        if (/^\s*sections\./m.test(js) && !/(?:const|let|var)\s+sections\s*=/.test(js)) {
            js = `const sections = document.querySelectorAll('section');\n\n${js.trimStart()}`;
            fs.writeFileSync(jsPath, `${js.trimEnd()}\n`, 'utf8');
        }
        if (!/@media|matchMedia|mobile-nav|hamburger|menu-toggle/i.test(`${nextHtml}\n${css}\n${js}`)) {
            css = `${css.trimEnd()}\n\n/* Vigthoria CLI Responsive Baseline */\n@media (max-width: 900px) {\n    .nav-links, .nav-list, nav ul {\n        display: none;\n        flex-direction: column;\n        gap: 0.75rem;\n    }\n\n    .nav-links.is-open, .nav-list.is-open, nav ul.is-open {\n        display: flex;\n    }\n\n    .mobile-nav-toggle, #menu-toggle, .menu-toggle {\n        display: inline-flex;\n    }\n}\n`;
            fs.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
        }
        const combined = `${nextHtml}\n${css}\n${js}`;
        if (/IntersectionObserver|motion-reveal|\.revealed\b|vigCliFadeIn|classList\.add\('is-visible'\)|classList\.add\("is-visible"\)/i.test(combined)) {
            return;
        }
        const cssMarker = '/* Vigthoria CLI Motion Enhancement */';
        const jsMarker = '/* Vigthoria CLI Motion Enhancement */';
        if (!css.includes(cssMarker)) {
            fs.appendFileSync(cssPath, `\n\n${cssMarker}\n.hero, section {\n    opacity: 0;\n    transform: translateY(24px);\n    animation: vigCliFadeIn 0.8s ease forwards;\n}\n\nsection {\n    animation-delay: 0.12s;\n}\n\nbutton, .cta, a {\n    transition: transform 0.25s ease, opacity 0.25s ease;\n}\n\nbutton:hover, .cta:hover, a:hover {\n    transform: translateY(-2px);\n}\n\n.motion-reveal {\n    opacity: 0;\n    transform: translateY(24px);\n    transition: opacity 0.7s ease, transform 0.7s ease;\n}\n\n.motion-reveal.is-visible {\n    opacity: 1;\n    transform: translateY(0);\n}\n\n@keyframes vigCliFadeIn {\n    from {\n        opacity: 0;\n        transform: translateY(24px);\n    }\n    to {\n        opacity: 1;\n        transform: translateY(0);\n    }\n}\n`, 'utf8');
        }
        if (!js.includes(jsMarker)) {
            fs.appendFileSync(jsPath, `\n\n${jsMarker}\ndocument.addEventListener('DOMContentLoaded', () => {\n    const revealTargets = document.querySelectorAll('section, .hero, .project-grid > *, .journal-preview > *');\n    if (typeof IntersectionObserver !== 'function') {\n        revealTargets.forEach((element) => element.classList.add('is-visible'));\n        return;\n    }\n\n    const observer = new IntersectionObserver((entries) => {\n        entries.forEach((entry) => {\n            if (entry.isIntersecting) {\n                entry.target.classList.add('is-visible');\n                observer.unobserve(entry.target);\n            }\n        });\n    }, { threshold: 0.16 });\n\n    revealTargets.forEach((element, index) => {\n        element.classList.add('motion-reveal');\n        element.style.transitionDelay = String(Math.min(index * 60, 320)) + 'ms';\n        observer.observe(element);\n    });\n});\n`, 'utf8');
        }
    }
    injectSectionBeforeFooter(html, sectionMarkup) {
        if (/<footer[\s>]/i.test(html)) {
            return html.replace(/<footer/i, `${sectionMarkup}\n    <footer`);
        }
        if (/<\/body>/i.test(html)) {
            return html.replace(/\s*<\/body>/i, `${sectionMarkup}\n</body>`);
        }
        return `${html.trimEnd()}\n${sectionMarkup}\n`;
    }
    injectNavLink(html, sectionId, label) {
        if (new RegExp(`href=\"#${sectionId}\"`, 'i').test(html)) {
            return html;
        }
        if (/<\/ul>/i.test(html)) {
            return html.replace(/\s*<\/ul>/i, `\n            <li><a href="#${sectionId}">${label}</a></li>\n        </ul>`);
        }
        return html;
    }
    ensureReferencedFrontendAssets(rootPath, html, prompt, surfaceLabel) {
        const localAssetPath = (rawPath) => {
            const candidate = String(rawPath || '').trim();
            if (!candidate || /^https?:\/\//i.test(candidate) || /^data:/i.test(candidate) || candidate.startsWith('#')) {
                return '';
            }
            return candidate.split('?')[0].split('#')[0].replace(/^\//, '');
        };
        const ensureTextAsset = (relativePath, content) => {
            const normalized = localAssetPath(relativePath);
            if (!normalized) {
                return '';
            }
            const absolutePath = path.join(rootPath, normalized);
            if (!fs.existsSync(absolutePath)) {
                fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
                fs.writeFileSync(absolutePath, content, 'utf8');
            }
            return absolutePath;
        };
        const cssRefs = Array.from(html.matchAll(/<link[^>]+href=["']([^"']+\.css(?:\?[^"']*)?)["']/gi))
            .map((match) => localAssetPath(match[1]))
            .filter(Boolean);
        const jsRefs = Array.from(html.matchAll(/<script[^>]+src=["']([^"']+\.(?:js|mjs)(?:\?[^"']*)?)["']/gi))
            .map((match) => localAssetPath(match[1]))
            .filter(Boolean);
        let cssPath = cssRefs.map((ref) => path.join(rootPath, ref)).find((candidate) => fs.existsSync(candidate)) || '';
        let jsPath = jsRefs.map((ref) => path.join(rootPath, ref)).find((candidate) => fs.existsSync(candidate)) || '';
        const cssTemplate = this.buildFallbackFrontendCss(surfaceLabel);
        const jsTemplate = this.buildFallbackFrontendJs(surfaceLabel);
        if (!cssPath && cssRefs.length > 0) {
            cssPath = ensureTextAsset(cssRefs[0], cssTemplate);
        }
        if (!jsPath && jsRefs.length > 0) {
            jsPath = ensureTextAsset(jsRefs[0], jsTemplate);
        }
        if (!cssPath) {
            cssPath = ensureTextAsset('styles.css', cssTemplate);
            if (cssPath && !/<link[^>]+href=["'][^"']*styles\.css/i.test(html)) {
                html = /<\/head>/i.test(html)
                    ? html.replace(/<\/head>/i, '    <link rel="stylesheet" href="styles.css">\n</head>')
                    : html;
            }
        }
        if (!jsPath && /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|responsive|animated)/i.test(prompt)) {
            jsPath = ensureTextAsset('scripts.js', jsTemplate);
            if (jsPath && !/<script[^>]+src=["'][^"']*scripts\.js/i.test(html)) {
                html = /<\/body>/i.test(html)
                    ? html.replace(/<\/body>/i, '    <script src="scripts.js"></script>\n</body>')
                    : `${html.trimEnd()}\n<script src="scripts.js"></script>\n`;
            }
        }
        return {
            html,
            cssPath,
            jsPath: jsPath || path.join(rootPath, 'scripts.js'),
        };
    }
    buildFallbackFrontendCss(surfaceLabel) {
        return `/* ${surfaceLabel} Frontend Baseline */
:root {
    color-scheme: light;
    --bg: #f4efe5;
    --surface: rgba(255, 255, 255, 0.82);
    --text: #1f2933;
    --muted: #52606d;
    --accent: #c36f3c;
    --accent-strong: #8f3f19;
    --border: rgba(31, 41, 51, 0.12);
}

* {
    box-sizing: border-box;
}

body {
    margin: 0;
    font-family: "Georgia", "Times New Roman", serif;
    color: var(--text);
    background: radial-gradient(circle at top, rgba(195, 111, 60, 0.18), transparent 42%), var(--bg);
}

header, nav, main, section, footer {
    width: 100%;
}

.container, .shell, .content {
    width: min(1120px, calc(100% - 2rem));
    margin: 0 auto;
}

.hero, section, footer {
    opacity: 0;
    transform: translateY(24px);
    animation: vigFallbackFadeIn 0.8s ease forwards;
}

.hero {
    min-height: 72vh;
    padding: 6rem 0 4rem;
}

.nav-list, nav ul {
    display: flex;
    gap: 1rem;
    list-style: none;
    padding: 0;
    margin: 0;
}

.mobile-nav-toggle, #menu-toggle, .menu-toggle {
    display: none;
}

.cta, button, a {
    transition: transform 0.25s ease, opacity 0.25s ease, background-color 0.25s ease;
}

.cta:hover, button:hover, a:hover {
    transform: translateY(-2px);
}

.motion-reveal {
    opacity: 0;
    transform: translateY(24px);
    transition: opacity 0.7s ease, transform 0.7s ease;
}

.motion-reveal.is-visible {
    opacity: 1;
    transform: translateY(0);
}

@media (max-width: 900px) {
    .nav-list, nav ul {
        display: none;
        flex-direction: column;
        padding: 1rem;
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: 1rem;
    }

    .nav-list.is-open, nav ul.is-open {
        display: flex;
    }

    .mobile-nav-toggle, #menu-toggle, .menu-toggle {
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
}

@keyframes vigFallbackFadeIn {
    from {
        opacity: 0;
        transform: translateY(24px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}
`;
    }
    buildFallbackFrontendJs(surfaceLabel) {
        return `/* ${surfaceLabel} Frontend Baseline */
document.addEventListener('DOMContentLoaded', () => {
    const toggle = document.querySelector('.mobile-nav-toggle, #menu-toggle, .menu-toggle');
    const nav = document.querySelector('.nav-list, nav ul');
    if (toggle && nav) {
        toggle.addEventListener('click', () => {
            nav.classList.toggle('is-open');
        });
    }

    const revealTargets = document.querySelectorAll('section, .hero, .card, article, .project-grid > *, .journal-preview > *');
    if (typeof IntersectionObserver !== 'function') {
        revealTargets.forEach((element) => element.classList.add('is-visible'));
        return;
    }

    const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
            if (entry.isIntersecting) {
                entry.target.classList.add('is-visible');
                observer.unobserve(entry.target);
            }
        });
    }, { threshold: 0.16 });

    revealTargets.forEach((element) => {
        element.classList.add('motion-reveal');
        observer.observe(element);
    });
});
`;
    }
    replaceMissingLocalAssetReferences(rootPath, html, css) {
        const hasLocalAsset = (assetPath) => {
            const candidate = String(assetPath || '').trim();
            if (!candidate || /^https?:\/\//i.test(candidate) || /^data:/i.test(candidate) || candidate.startsWith('#')) {
                return true;
            }
            const sanitized = candidate.split('?')[0].split('#')[0].replace(/^\//, '');
            return !sanitized || fs.existsSync(path.join(rootPath, sanitized));
        };
        const repairedHtml = html.replace(/(<img\b[^>]*\ssrc=["'])([^"']+)(["'][^>]*>)/gi, (match, prefix, assetPath, suffix) => {
            if (hasLocalAsset(assetPath)) {
                return match;
            }
            const altText = (String(suffix).match(/\salt=["']([^"']*)["']/i)?.[1] || 'Visual highlight').trim() || 'Visual highlight';
            const safeLabel = altText.replace(/[<&>]/g, '');
            const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800"><defs><linearGradient id="heroGradient" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0f172a"/><stop offset="1" stop-color="#2563eb"/></linearGradient></defs><rect width="1200" height="800" rx="40" ry="40" fill="url(#heroGradient)"/><circle cx="220" cy="210" r="130" fill="rgba(255,255,255,0.08)"/><circle cx="980" cy="600" r="170" fill="rgba(255,255,255,0.12)"/><text x="84" y="700" fill="#ffffff" font-family="Arial, sans-serif" font-size="64" font-weight="700">${safeLabel}</text></svg>`;
            return `${prefix}data:image/svg+xml;utf8,${encodeURIComponent(svg)}${suffix}`;
        });
        const repairedCss = css.replace(/url\((["']?)([^)"']+)\1\)/gi, (match, quote, assetPath) => {
            if (hasLocalAsset(assetPath)) {
                return match;
            }
            return 'linear-gradient(135deg, rgba(15, 23, 42, 0.92), rgba(37, 99, 235, 0.78))';
        });
        return { html: repairedHtml, css: repairedCss };
    }
    formatV3AgentResponse(data) {
        const result = data?.result || {};
        if (typeof result === 'string') {
            const text = this.sanitizeV3AgentResponseText(result);
            if (isSubstantiveAgentAnswer(text))
                return text;
        }
        if (typeof result?.summary === 'string' && result.summary.trim()) {
            const text = this.sanitizeV3AgentResponseText(result.summary);
            if (isSubstantiveAgentAnswer(text))
                return text;
        }
        if (typeof result?.message === 'string' && result.message.trim()) {
            const text = this.sanitizeV3AgentResponseText(result.message);
            if (isSubstantiveAgentAnswer(text))
                return text;
        }
        if (Array.isArray(data?.events)) {
            const completionEvent = [...data.events].reverse().find((event) => event && event.type === 'complete' && typeof event.summary === 'string' && event.summary.trim());
            if (completionEvent) {
                const text = this.sanitizeV3AgentResponseText(completionEvent.summary);
                if (isSubstantiveAgentAnswer(text))
                    return text;
            }
            const messageEvent = [...data.events].reverse().find((event) => event && event.type === 'message' && typeof event.content === 'string' && event.content.trim());
            if (messageEvent) {
                const text = this.sanitizeV3AgentResponseText(messageEvent.content);
                if (isSubstantiveAgentAnswer(text))
                    return text;
            }
            // Synthesize a grounded answer from the tool-call evidence the
            // agent produced, rather than dumping the raw event trace.
            const answer = this.synthesizeAnswerFromV3Events(data.events);
            if (answer) {
                return answer;
            }
        }
        // Last resort: if data has files written, report them.
        if (data?.files && typeof data.files === 'object' && Object.keys(data.files).length > 0) {
            const fileList = Object.keys(data.files).join(', ');
            return `Agent wrote workspace files: ${sanitizeUserFacingPathText(fileList)}`;
        }
        return '';
    }
    sanitizeV3AgentResponseText(input) {
        let text = sanitizeUserFacingPathText(String(input || ''));
        text = text
            .replace(/<think>/gi, '<thinking>')
            .replace(/<\/think>/gi, '</thinking>')
            .replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
            .replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
            .replace(/<tool_code>[\s\S]*?<\/tool_code>/gi, '')
            .replace(/<read_file>\s*<path>[\s\S]*?<\/path>\s*<\/read_file>/gi, '')
            .replace(/<(?:grep|list_directory|search_files|glob|tool_result)\b[\s\S]*?<\/(?:grep|list_directory|search_files|glob|tool_result)>/gi, '')
            .replace(/bootstrap discovery completed\s*[—-]\s*inspect the most relevant files next\.?/gi, '')
            .replace(/```json\s*\[\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*\]\s*```/gi, '')
            .replace(/```json\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*```/gi, '')
            .replace(/^\s*(?:json\s*)?\[\s*\{[\s\S]*?"tool"[\s\S]*$/gim, '')
            .replace(/^\s*(?:list_dir|read_file|write_file|edit_file|glob|grep|bash)\s*$/gim, '')
            .replace(/\n{3,}/g, '\n\n')
            .trim();
        return text;
    }
    /**
     * Build a human-readable answer from the tool results in a V3 event
     * stream when the server didn't emit a proper complete/message event.
     */
    synthesizeAnswerFromV3Events(events) {
        const toolResults = [];
        const filesRead = [];
        const readExcerpts = [];
        const filesWritten = [];
        const assistantFragments = [];
        const pendingReadPaths = new Map();
        let readIndex = 0;
        for (const event of events) {
            if (!event)
                continue;
            if (event.type === 'tool_call') {
                const name = String(event.name || event.tool || '').trim();
                if (name === 'read_file') {
                    const target = event.arguments?.path || event.arguments?.file_path || event.target;
                    if (target)
                        pendingReadPaths.set(readIndex++, String(target));
                }
            }
            if (event.type === 'tool_result' && event.success && typeof event.output === 'string') {
                const name = String(event.name || event.tool || 'unknown_tool').trim();
                const output = event.output.trim();
                if (name === 'read_file') {
                    const target = typeof event.target === 'string'
                        ? event.target
                        : (event.arguments?.path || event.arguments?.file_path || filesRead[filesRead.length] || 'file');
                    if (target)
                        filesRead.push(sanitizeUserFacingPathText(String(target)));
                    if (output.length > 120 && !/^"[^"]+"$/.test(output)) {
                        const header = sanitizeUserFacingPathText(String(target || 'file'));
                        const body = sanitizeUserFacingPathText(output.slice(0, 2500));
                        readExcerpts.push(`${header}\n${body}${output.length > 2500 ? '\n…' : ''}`);
                    }
                    else if (output.length > 0) {
                        toolResults.push(`[${name}] ${sanitizeUserFacingPathText(output.slice(0, 300))}`);
                    }
                }
                else if ((name === 'write_file' || name === 'create_file') && typeof event.target === 'string') {
                    filesWritten.push(sanitizeUserFacingPathText(event.target));
                }
                else {
                    const excerpt = output.length > 300 ? output.slice(-300) : output;
                    toolResults.push(`[${name}] ${sanitizeUserFacingPathText(excerpt)}`);
                }
            }
            if (event.type === 'assistant' && typeof event.content === 'string' && event.content.trim()) {
                const cleaned = this.sanitizeV3AgentResponseText(event.content.trim());
                if (cleaned)
                    assistantFragments.push(cleaned);
            }
            // Some servers emit 'text' events for incremental assistant text
            if (event.type === 'text' && typeof event.content === 'string' && event.content.trim()) {
                const cleaned = this.sanitizeV3AgentResponseText(event.content.trim());
                if (cleaned)
                    assistantFragments.push(cleaned);
            }
            // Some servers emit content_block_delta for streamed text
            if (event.type === 'content_block_delta' && typeof event.delta?.text === 'string' && event.delta.text.trim()) {
                const cleaned = this.sanitizeV3AgentResponseText(event.delta.text.trim());
                if (cleaned)
                    assistantFragments.push(cleaned);
            }
        }
        // Concatenate ALL assistant text fragments in order — keeps full
        // multi-turn reasoning instead of only the last fragment.
        const fullAssistantText = assistantFragments.join('\n\n').trim();
        if (isSubstantiveAgentAnswer(fullAssistantText)) {
            return fullAssistantText;
        }
        if (readExcerpts.length > 0) {
            return '';
        }
        // Otherwise build a summary from tool evidence — but never treat stubs as answers.
        const sections = [];
        if (filesRead.length > 0) {
            sections.push(`Files inspected: ${filesRead.join(', ')}`);
        }
        if (filesWritten.length > 0) {
            sections.push(`Files written: ${filesWritten.join(', ')}`);
        }
        if (toolResults.length > 0) {
            sections.push(toolResults.slice(-5).join('\n'));
        }
        const synthesized = sections.join('\n\n');
        return isToolEvidenceStubAnswer(synthesized) ? '' : synthesized;
    }
    sanitizeV3AgentEventForUser(event) {
        const sanitizeValue = (value) => {
            if (typeof value === 'string') {
                return sanitizeUserFacingPathText(value);
            }
            if (Array.isArray(value)) {
                return value.map((entry) => sanitizeValue(entry));
            }
            if (value && typeof value === 'object') {
                const copy = {};
                for (const [key, entry] of Object.entries(value)) {
                    copy[key] = sanitizeValue(entry);
                }
                return copy;
            }
            return value;
        };
        return sanitizeValue(event);
    }
    resolveAgentChangedFiles(data) {
        const streamed = data?.files && typeof data.files === 'object' ? data.files : {};
        const clientMutations = this.activeV3StreamedFiles && typeof this.activeV3StreamedFiles === 'object'
            ? this.activeV3StreamedFiles
            : {};
        return { ...streamed, ...clientMutations };
    }
    finalizeV3AgentWorkflowResponse(data, response) {
        const changedFiles = this.resolveAgentChangedFiles(data);
        this.activeV3StreamedFiles = null;
        this.activeV3StreamContext = null;
        return {
            ...response,
            changedFiles,
        };
    }
    async collectV3AgentStream(response, context = {}) {
        if (!response.body || typeof response.body.getReader !== 'function') {
            return response.json();
        }
        this.clearAgentRunDiagnostics();
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';
        const events = [];
        let final = null;
        let streamDone = false;
        let contextId = response.headers.get('x-context-id') || String(context.contextId || '').trim() || null;
        let serverWorkspaceRoot = null;
        const streamedFiles = {};
        this.activeV3StreamedFiles = streamedFiles;
        this.activeV3StreamContext = context;
        context.__v3StreamedFiles = streamedFiles;
        const idleTimeoutMs = (() => {
            const base = context.agentIdleTimeoutMs ?? DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS;
            if (base <= 0) {
                return 0;
            }
            if (!isPlannerBuildTask(String(context.agentTaskType || ''), String(context.rawPrompt || ''))) {
                return base;
            }
            const raw = process.env.VIGTHORIA_AGENT_PLANNER_IDLE_TIMEOUT_MS || process.env.V3_AGENT_PLANNER_IDLE_TIMEOUT_MS;
            if (!raw) {
                return base;
            }
            const extended = Number.parseInt(raw, 10);
            return Number.isFinite(extended) && extended > 0 ? Math.max(base, extended) : base;
        })();
        try {
            while (true) {
                let chunk;
                try {
                    if (idleTimeoutMs <= 0) {
                        chunk = await reader.read();
                    }
                    else {
                        chunk = await new Promise((resolve, reject) => {
                            let idleTimer = null;
                            // Idle/silence detection (NOT a task-duration timeout): this timer
                            // only ever fires after idleTimeoutMs with ZERO new bytes on the
                            // wire. What we do next depends on WHY it's silent:
                            //  - A client-side tool is genuinely still running (e.g. a local
                            //    build/install the CLI itself launched) -- its own duration is
                            //    unpredictable by design, so we reschedule indefinitely; this
                            //    is known, bounded-elsewhere work, not "the agent went dark".
                            //  - Otherwise, silence really does mean nothing is coming from
                            //    the agent, so after a generous grace window we recover
                            //    (if the workspace already has output) or fail cleanly.
                            let idleRescheduleCount = 0;
                            const maxIdleReschedules = 20; // ~20 min of genuine zero-byte silence
                            const clearIdleTimer = () => {
                                if (idleTimer) {
                                    clearTimeout(idleTimer);
                                    idleTimer = null;
                                }
                            };
                            const scheduleIdleTimer = () => {
                                clearIdleTimer();
                                idleTimer = setTimeout(() => {
                                    if (this.pendingV3ClientToolTasks.size > 0) {
                                        // Known local work in progress -- never cap this, its own
                                        // completion will produce the next chunk/event.
                                        scheduleIdleTimer();
                                        return;
                                    }
                                    if (this.hasAgentWorkspaceOutput(context)) {
                                        const stalledError = new Error('V3 agent stream stalled after writing workspace output');
                                        stalledError.name = 'AbortError';
                                        stalledError.partialData = {
                                            task_id: events.find((event) => event && event.task_id)?.task_id || null,
                                            context_id: contextId,
                                            result: final,
                                            events,
                                            files: streamedFiles,
                                        };
                                        clearIdleTimer();
                                        reject(stalledError);
                                        return;
                                    }
                                    if (idleRescheduleCount >= maxIdleReschedules) {
                                        const timeoutError = new Error(`V3 agent stream timed out after ${Math.round((idleTimeoutMs * (maxIdleReschedules + 1)) / 1000)}s with no data ` +
                                            '(model likely stopped after a thinking block without emitting a tool call or final answer).');
                                        timeoutError.name = 'AbortError';
                                        timeoutError.partialData = {
                                            task_id: events.find((event) => event && event.task_id)?.task_id || null,
                                            context_id: contextId,
                                            result: final,
                                            events,
                                            files: streamedFiles,
                                        };
                                        clearIdleTimer();
                                        reject(timeoutError);
                                        return;
                                    }
                                    idleRescheduleCount += 1;
                                    scheduleIdleTimer();
                                }, idleTimeoutMs);
                            };
                            scheduleIdleTimer();
                            reader.read()
                                .then((result) => {
                                clearIdleTimer();
                                resolve(result);
                            })
                                .catch((error) => {
                                clearIdleTimer();
                                reject(error);
                            });
                        });
                    }
                }
                catch (error) {
                    if (error && error.name === 'AbortError') {
                        error.partialData = {
                            task_id: events.find((event) => event && event.task_id)?.task_id || null,
                            context_id: contextId,
                            result: final,
                            events,
                            files: streamedFiles,
                        };
                    }
                    throw error;
                }
                const { done, value } = chunk;
                if (done) {
                    break;
                }
                buffer += decoder.decode(value, { stream: true });
                const frames = buffer.split(/\r?\n\r?\n/);
                buffer = frames.pop() || '';
                for (const frame of frames) {
                    const lines = frame.split(/\r?\n/);
                    const dataLines = [];
                    let explicitEventType = null;
                    for (const rawLine of lines) {
                        const line = rawLine.trimEnd();
                        if (!line || line.startsWith(':')) {
                            continue;
                        }
                        if (line.startsWith('event:')) {
                            explicitEventType = line.slice(6).trim() || null;
                            continue;
                        }
                        if (line.startsWith('data:')) {
                            dataLines.push(line.slice(5).trimStart());
                        }
                    }
                    const payload = dataLines.join('\n').trim();
                    if (!payload) {
                        continue;
                    }
                    if (payload === '[DONE]') {
                        streamDone = true;
                        break;
                    }
                    let event;
                    try {
                        event = JSON.parse(payload);
                    }
                    catch {
                        event = {
                            type: explicitEventType || 'message',
                            content: payload,
                        };
                    }
                    if (explicitEventType && (!event.type || event.type === 'message')) {
                        event.type = explicitEventType;
                    }
                    const userEvent = this.sanitizeV3AgentEventForUser(event);
                    events.push(userEvent);
                    if (!contextId && typeof event.context_id === 'string' && event.context_id.trim()) {
                        contextId = event.context_id.trim();
                    }
                    if (!serverWorkspaceRoot && event.type === 'context' && typeof event.workspace_root === 'string' && event.workspace_root.trim()) {
                        serverWorkspaceRoot = event.workspace_root.trim();
                        context.__v3ServerWorkspaceRoot = serverWorkspaceRoot;
                    }
                    if (event.type === 'client_tool_request') {
                        const toolTask = this.handleV3ClientToolRequest(event, context, response.url).catch((error) => {
                            this.logger.warn(`Failed to handle V3 client tool request: ${error.message}`);
                        });
                        this.pendingV3ClientToolTasks.add(toolTask);
                        void toolTask.finally(() => {
                            this.pendingV3ClientToolTasks.delete(toolTask);
                        });
                    }
                    this.captureV3AgentStreamMutation(event, streamedFiles, serverWorkspaceRoot);
                    this.applyV3AgentStreamEventToWorkspace(event, context, serverWorkspaceRoot);
                    // Empty workspace soft guard: the first list_directory result can be
                    // transiently empty before remote workspace hydration catches up.
                    // Avoid hard-failing here; let the stream continue.
                    if (event.type === 'tool_result'
                        && event.name === 'list_directory'
                        && event.success === true
                        && serverWorkspaceRoot
                        && typeof event.output === 'string') {
                        const listOutput = event.output.trim();
                        const looksEmpty = listOutput === `[${serverWorkspaceRoot}]`
                            || listOutput === `[${serverWorkspaceRoot}/]`
                            || listOutput === `[${serverWorkspaceRoot}]\\n`
                            || /^\[\/tmp\/vig-remote-server-[^\]]+\]\s*$/.test(listOutput);
                        if (looksEmpty) {
                            const localPath = this.resolveAgentTargetPath(context);
                            const localHasFiles = localPath && fs.existsSync(localPath) && this.hasAgentWorkspaceOutput({ ...context, projectPath: localPath, targetPath: localPath });
                            if (localHasFiles) {
                                this.logger?.debug?.('V3 remote workspace initially empty; waiting for hydration instead of aborting.');
                            }
                        }
                    }
                    if (typeof context.onStreamEvent === 'function' && !isV3StreamKeepaliveEvent(userEvent)) {
                        try {
                            context.onStreamEvent(userEvent);
                        }
                        catch {
                            // Ignore UI callback failures; never break the agent stream for them.
                        }
                    }
                    if (event.type === 'error') {
                        if (event.checkpointed && event.task_id) {
                            // Agent checkpointed — return data so caller can auto-continue
                            return {
                                task_id: event.task_id,
                                context_id: contextId,
                                result: final || event,
                                events,
                                files: streamedFiles,
                                partial: true,
                                checkpointed: true,
                                checkpointed_task_id: event.task_id,
                            };
                        }
                        if (this.hasAgentWorkspaceOutput(context)) {
                            return {
                                task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
                                context_id: contextId,
                                result: final || event,
                                events,
                                files: streamedFiles,
                                partial: true,
                                terminal_error: {
                                    message: event.message || 'V3 agent returned an error',
                                    code: event.code || event.error_code || null,
                                },
                            };
                        }
                        throw new Error(event.message || 'V3 agent returned an error');
                    }
                    if (event.type === 'complete' || event.type === 'message') {
                        final = event;
                    }
                }
                if (streamDone) {
                    break;
                }
            }
            await Promise.allSettled([...this.pendingV3ClientToolTasks]);
            return {
                task_id: events.find((event) => event && event.task_id)?.task_id || null,
                context_id: contextId,
                result: final,
                events,
                files: streamedFiles,
                serverWorkspaceRoot: serverWorkspaceRoot || null,
                clientToolErrors: this.getClientToolErrors(),
            };
        }
        finally {
            await Promise.allSettled([...this.pendingV3ClientToolTasks]);
        }
    }
    async runV3AgentWorkflow(message, context = {}) {
        const executionContext = await this.bindExecutionContext(context);
        const requestedTimeoutMs = Number(executionContext.agentTimeoutMs ?? DEFAULT_V3_AGENT_TIMEOUT_MS);
        const baseTimeoutMs = Number.isFinite(requestedTimeoutMs) && requestedTimeoutMs > 0 ? requestedTimeoutMs : 0;
        const expectedFiles = this.extractExpectedWorkspaceFiles(message, executionContext);
        const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
        const resolvedModel = this.resolvePermittedModelId(requestedModel);
        const preferLocalV3 = /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|responsive|animated|create the required project files and write them to the workspace)/i.test(message)
            && context.localMachineCapable !== false;
        const timeoutMs = resolvePlannerAgentTimeoutMs(baseTimeoutMs, String(executionContext.agentTaskType || ''), String(executionContext.rawPrompt || message || ''));
        const maxAttempts = preferLocalV3 ? 2 : 1;
        let lastErrors = [];
        for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
            const errors = [];
            const useRelaxedAttempt = preferLocalV3 && attempt > 1;
            const requestExecutionContext = useRelaxedAttempt
                ? this.ensureExecutionContext({
                    ...executionContext,
                    contextId: '',
                    traceId: '',
                    history: [],
                    mcpContextId: null,
                    mcpContextBackendUrl: null,
                    agentExecutionPolicy: null,
                    legacyFallbackAllowed: true,
                })
                : executionContext;
            const buildRequestBody = (contextIdOverride) => ({
                request: message,
                model: resolvedModel,
                preferred_model: resolvedModel,
                requested_model: requestedModel,
                routing_policy: useRelaxedAttempt ? null : requestExecutionContext.agentExecutionPolicy || null,
                strict_mode: useRelaxedAttempt ? false : requestExecutionContext.legacyFallbackAllowed !== true,
                context: useRelaxedAttempt
                    ? this.buildMinimalV3AgentContext(requestExecutionContext)
                    : this.buildV3AgentContext(requestExecutionContext),
                context_id: contextIdOverride ?? requestExecutionContext.contextId,
                mcp_context_id: useRelaxedAttempt ? null : requestExecutionContext.mcpContextId || null,
                stream: true,
            });
            for (const baseUrl of this.getV3AgentBaseUrls(preferLocalV3)) {
                let contextIdOverride;
                const authPreflight = await this.runV3AgentAuthPreflight(baseUrl, buildRequestBody(), requestExecutionContext);
                if (!authPreflight.ok) {
                    errors.push(`V3 auth preflight ${authPreflight.status} [${authPreflight.endpoint}]: ${authPreflight.reason || 'Unauthorized'}`);
                    continue;
                }
                for (let contextRetry = 0; contextRetry < 2; contextRetry += 1) {
                    const requestBody = buildRequestBody(contextIdOverride);
                    const controller = new AbortController();
                    const timeoutId = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
                    try {
                        const response = await this.executeV3AgentRunRequest(baseUrl, requestBody, requestExecutionContext, controller.signal);
                        if (!response.ok) {
                            const errorText = await response.text().catch(() => '');
                            const sanitized = sanitizeUserFacingErrorText(errorText).slice(0, 200);
                            const isContextCollision = response.status === 409 && /already in progress/i.test(errorText);
                            const isTransientWorkspaceHydration = /remote workspace is empty|workspace sync failure/i.test(errorText);
                            if ((isContextCollision || isTransientWorkspaceHydration) && contextRetry === 0) {
                                contextIdOverride = `${requestExecutionContext.contextId || 'vig'}-retry-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
                                this.logger?.debug?.(`V3 transient agent error on ${baseUrl}; retrying with fresh context id.`);
                                continue;
                            }
                            throw new Error(`V3 agent ${response.status}: ${sanitized}`);
                        }
                        const data = await this.collectV3AgentStream(response, requestExecutionContext);
                        // Auto-continuation: if the agent checkpointed (budget exceeded), continue automatically
                        if (data.checkpointed && data.checkpointed_task_id) {
                            const maxContinuations = 10;
                            let continuationData = data;
                            let continuations = 0;
                            while (continuationData.checkpointed && continuationData.checkpointed_task_id && continuations < maxContinuations) {
                                continuations++;
                                if (typeof requestExecutionContext.onStreamEvent === 'function') {
                                    try {
                                        requestExecutionContext.onStreamEvent({
                                            type: 'message',
                                            content: `Auto-continuing task (phase ${continuations + 1})...`,
                                        });
                                    }
                                    catch { /* ignore */ }
                                }
                                const continueBody = {
                                    task_id: continuationData.checkpointed_task_id,
                                    context: requestBody.context,
                                    context_id: requestBody.context_id,
                                    mcp_context_id: requestBody.mcp_context_id,
                                    stream: true,
                                };
                                const continueController = new AbortController();
                                const continueTimeoutId = timeoutMs > 0 ? setTimeout(() => continueController.abort(), timeoutMs) : null;
                                try {
                                    const continueHeaders = await this.getV3AgentHeaders();
                                    const continueResponse = await fetch(this.getV3AgentContinueUrl(baseUrl), {
                                        method: 'POST',
                                        headers: { ...continueHeaders, 'Content-Type': 'application/json' },
                                        body: JSON.stringify(continueBody),
                                        signal: continueController.signal,
                                    });
                                    if (!continueResponse.ok) {
                                        break; // Fall through to normal completion with partial data
                                    }
                                    continuationData = await this.collectV3AgentStream(continueResponse, requestExecutionContext);
                                }
                                catch {
                                    break; // Fall through to normal completion with partial data
                                }
                                finally {
                                    if (continueTimeoutId)
                                        clearTimeout(continueTimeoutId);
                                }
                            }
                            // Use the final continuation data for workspace recovery
                            this.recoverAgentWorkspaceFiles(executionContext, continuationData.files || {}, expectedFiles);
                            await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
                            await this.ensureAgentFrontendPolish(message, executionContext);
                            const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
                            const finalContextId = continuationData.context_id || data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
                            return this.finalizeV3AgentWorkflowResponse(continuationData, {
                                content: this.formatV3AgentResponse(continuationData) || this.formatV3AgentResponse(data),
                                taskId: continuationData.task_id || data.task_id || null,
                                contextId: finalContextId,
                                backendUrl: baseUrl,
                                partial: continuationData.checkpointed === true,
                                metadata: { source: 'v3-agent', mode: 'agent', contextId: finalContextId, continuations, previewGate },
                            });
                        }
                        const contextId = data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
                        const mcpContextId = response.headers.get('x-mcp-context-id') || requestExecutionContext.mcpContextId || null;
                        this.recoverAgentWorkspaceFiles(executionContext, data.files || {}, expectedFiles);
                        if (data.terminal_error) {
                            // A pre-existing local project is not proof that this run produced
                            // valid output. Preserve real streamed mutations, but never run or
                            // report a preview gate after a terminal planner/executor error.
                            const failedPreviewGate = {
                                required: false,
                                passed: false,
                                skipped: true,
                                error: 'Agent execution failed before preview validation.',
                            };
                            return this.finalizeV3AgentWorkflowResponse(data, {
                                content: '',
                                taskId: data.task_id || null,
                                contextId,
                                backendUrl: baseUrl,
                                partial: true,
                                metadata: {
                                    source: 'v3-agent',
                                    mode: 'agent',
                                    contextId,
                                    mcpContextId,
                                    previewGate: failedPreviewGate,
                                    workflowError: data.terminal_error,
                                },
                            });
                        }
                        await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
                        await this.ensureAgentFrontendPolish(message, executionContext);
                        const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
                        if (previewGate.required && previewGate.passed !== true && !this.hasAgentWorkspaceOutput(executionContext)) {
                            errors.push(`${baseUrl}: workflow rejected the result - ${previewGate.error || 'Workspace changes were not fully validated.'}`);
                            continue;
                        }
                        return this.finalizeV3AgentWorkflowResponse(data, {
                            content: this.formatV3AgentResponse(data),
                            taskId: data.task_id || null,
                            contextId,
                            backendUrl: baseUrl,
                            metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
                        });
                    }
                    catch (error) {
                        if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
                            this.recoverAgentWorkspaceFiles(executionContext, error.partialData.files || {}, expectedFiles);
                            await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
                            await this.ensureAgentFrontendPolish(message, executionContext);
                            const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
                            return this.finalizeV3AgentWorkflowResponse(error.partialData, {
                                content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
                                taskId: error.partialData.task_id || null,
                                contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null,
                                backendUrl: baseUrl,
                                partial: true,
                                metadata: { source: 'v3-agent', mode: 'agent', partial: true, contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null, mcpContextId: requestExecutionContext.mcpContextId || executionContext.mcpContextId || null, previewGate },
                            });
                        }
                        errors.push(`${baseUrl}: ${error?.message || String(error)}`);
                    }
                    finally {
                        if (timeoutId)
                            clearTimeout(timeoutId);
                    }
                }
            }
            lastErrors = errors;
            const shouldRetry = attempt < maxAttempts
                && preferLocalV3
                && !this.hasAgentWorkspaceOutput(executionContext)
                && errors.some((entry) => /terminated|abort|timed out|timeout|ECONNRESET|socket hang up|workflow rejected the result|incomplete result|preview gate requires at least one html entry file/i.test(entry));
            if (!shouldRetry) {
                break;
            }
            await new Promise((resolve) => setTimeout(resolve, 1500));
        }
        const errors = lastErrors;
        const onlyUnauthorizedErrors = errors.length > 0 && errors.every((entry) => /V3 agent 401:|V3 agent 401 \[|V3 auth preflight 401|V3 auth preflight 403/i.test(entry));
        const usingStoredConfigToken = !process.env.VIGTHORIA_TOKEN
            && !process.env.VIGTHORIA_AUTH_TOKEN
            && Boolean(this.config.get('authToken'));
        if (onlyUnauthorizedErrors && usingStoredConfigToken) {
            const gatewayTokenCheck = await this.validateToken({ allowNetworkFailOpen: true, enforceTokenShape: true });
            if (!gatewayTokenCheck.valid) {
                this.config.clearAuth();
                throw new Error('V3 agent authentication failed. The stored CLI login token is invalid or expired. Run vigthoria login again.');
            }
            const rejectedEndpoints = Array.from(new Set(errors
                .map((entry) => {
                const match = entry.match(/\[(https?:\/\/[^\]]+)\]/i);
                return match ? match[1] : null;
            })
                .filter((entry) => Boolean(entry))));
            const endpointSummary = rejectedEndpoints.length > 0
                ? ` Rejected V3 endpoints: ${rejectedEndpoints.join(', ')}.`
                : '';
            throw new Error(`V3 agent authentication failed at the V3 service layer while your gateway login token is still valid.${endpointSummary} Please retry shortly.`);
        }
        if (preferLocalV3
            && !this.hasAgentWorkspaceOutput(executionContext)
            && /(saas|dashboard|analytics|billing|team management|activity feed)/i.test(message)) {
            const appName = this.materializeEmergencySaaSWorkspace(message, executionContext);
            if (appName) {
                await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles: ['index.html', 'styles.css', 'scripts.js'] });
                await this.ensureAgentFrontendPolish(message, executionContext);
                const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
                return {
                    content: `Recovered a local SaaS workspace scaffold for ${appName} after repeated V3 materialization failures.`,
                    taskId: null,
                    contextId: executionContext.contextId || null,
                    backendUrl: 'local-emergency-scaffold',
                    partial: true,
                    metadata: { source: 'v3-agent-emergency-scaffold', mode: 'agent', previewGate, emergencyScaffold: true },
                };
            }
        }
        throw new Error(errors.join(' | '));
    }
    async startV3BackgroundJob(message, context = {}) {
        await this.ensureV3ServiceKey();
        const executionContext = await this.bindExecutionContext({
            ...context,
            backgroundJob: true,
            clientToolExecution: false,
        });
        const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
        const resolvedModel = this.resolvePermittedModelId(requestedModel);
        const body = {
            request: message,
            context: this.buildV3AgentContext({
                ...executionContext,
                backgroundJob: true,
                clientToolExecution: false,
            }),
            stream: false,
            model: resolvedModel,
            mcp_context_id: executionContext.mcpContextId || undefined,
        };
        const errors = [];
        for (const baseUrl of this.getV3AgentBaseUrls(false)) {
            try {
                const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl), {
                    method: 'POST',
                    headers: await this.getV3AgentHeaders(),
                    body: JSON.stringify(body),
                });
                if (!response.ok) {
                    errors.push(`${baseUrl}: ${response.status} ${await response.text().catch(() => '')}`);
                    continue;
                }
                return await response.json();
            }
            catch (error) {
                errors.push(`${baseUrl}: ${error?.message || String(error)}`);
            }
        }
        throw new Error(`Unable to start background job: ${errors.join('; ')}`);
    }
    async listV3BackgroundJobs(limit = 20) {
        await this.ensureV3ServiceKey();
        const errors = [];
        for (const baseUrl of this.getV3AgentBaseUrls(false)) {
            try {
                const response = await fetch(`${this.getV3AgentBackgroundUrl(baseUrl)}?limit=${encodeURIComponent(String(limit))}`, {
                    method: 'GET',
                    headers: await this.getV3AgentHeaders(),
                });
                if (!response.ok) {
                    errors.push(`${baseUrl}: ${response.status}`);
                    continue;
                }
                const data = await response.json();
                return Array.isArray(data.jobs) ? data.jobs : [];
            }
            catch (error) {
                errors.push(`${baseUrl}: ${error?.message || String(error)}`);
            }
        }
        throw new Error(`Unable to list background jobs: ${errors.join('; ')}`);
    }
    async getV3BackgroundJob(jobId) {
        await this.ensureV3ServiceKey();
        const errors = [];
        for (const baseUrl of this.getV3AgentBaseUrls(false)) {
            try {
                const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, encodeURIComponent(jobId)), {
                    method: 'GET',
                    headers: await this.getV3AgentHeaders(),
                });
                if (!response.ok) {
                    errors.push(`${baseUrl}: ${response.status}`);
                    continue;
                }
                return await response.json();
            }
            catch (error) {
                errors.push(`${baseUrl}: ${error?.message || String(error)}`);
            }
        }
        throw new Error(`Unable to get background job: ${errors.join('; ')}`);
    }
    async cancelV3BackgroundJob(jobId) {
        await this.ensureV3ServiceKey();
        const errors = [];
        for (const baseUrl of this.getV3AgentBaseUrls(false)) {
            try {
                const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, `${encodeURIComponent(jobId)}/cancel`), {
                    method: 'POST',
                    headers: await this.getV3AgentHeaders(),
                });
                if (!response.ok) {
                    errors.push(`${baseUrl}: ${response.status}`);
                    continue;
                }
                return await response.json();
            }
            catch (error) {
                errors.push(`${baseUrl}: ${error?.message || String(error)}`);
            }
        }
        throw new Error(`Unable to cancel background job: ${errors.join('; ')}`);
    }
    async getV3BackgroundJobFiles(jobId) {
        await this.ensureV3ServiceKey();
        const errors = [];
        for (const baseUrl of this.getV3AgentBaseUrls(false)) {
            try {
                const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, `${encodeURIComponent(jobId)}/files`), {
                    method: 'GET',
                    headers: await this.getV3AgentHeaders(),
                });
                if (!response.ok) {
                    errors.push(`${baseUrl}: ${response.status}`);
                    continue;
                }
                return await response.json();
            }
            catch (error) {
                errors.push(`${baseUrl}: ${error?.message || String(error)}`);
            }
        }
        throw new Error(`Unable to fetch background job files: ${errors.join('; ')}`);
    }
    async applyV3BackgroundJobFiles(jobId, context = {}) {
        const data = await this.getV3BackgroundJobFiles(jobId);
        const rootPath = this.resolveAgentTargetPath({
            ...context,
            workspacePath: context.workspacePath || data.local_workspace_path,
            projectPath: context.projectPath || data.local_workspace_path,
            targetPath: context.targetPath || data.local_workspace_path,
        });
        if (!rootPath) {
            throw new Error('No local workspace path available for applying background job files.');
        }
        const applied = [];
        const skipped = [];
        for (const [relativePath, content] of Object.entries(data.files || {})) {
            if (typeof content !== 'string') {
                skipped.push(relativePath);
                continue;
            }
            if (this.writeV3AgentWorkspaceFile(rootPath, relativePath, content, rootPath)) {
                applied.push(relativePath);
            }
            else {
                skipped.push(relativePath);
            }
        }
        return { applied, skipped, status: data.status };
    }
    formatOperatorResponse(data = {}) {
        // If the server returned a direct answer field, prefer it (for lookup tasks)
        if (typeof data.answer === 'string' && data.answer.trim()) {
            return data.answer.trim();
        }
        if (typeof data.result === 'string' && data.result.trim()) {
            return data.result.trim();
        }
        const lines = [];
        if (data.summary) {
            lines.push(String(data.summary).trim());
        }
        if (Array.isArray(data.completed_agents) && data.completed_agents.length > 0) {
            lines.push(`Completed agents: ${data.completed_agents.join(', ')}`);
        }
        if (data.details) {
            const detailText = JSON.stringify(data.details, null, 2);
            if (detailText && detailText !== '{}') {
                lines.push(detailText.length > 12000 ? `${detailText.slice(0, 12000)}\n\n[Operator output truncated]` : detailText);
            }
        }
        return lines.filter(Boolean).join('\n\n').trim() || 'Operator workflow completed.';
    }
    async runOperatorWorkflow(message, context = {}) {
        const executionContext = await this.bindExecutionContext(context);
        const requestedOperatorTimeoutMs = Number(context.operatorTimeoutMs ?? DEFAULT_OPERATOR_TIMEOUT_MS);
        const timeoutMs = Number.isFinite(requestedOperatorTimeoutMs) && requestedOperatorTimeoutMs > 0
            ? requestedOperatorTimeoutMs
            : 0;
        const errors = [];
        const authToken = this.config.get('authToken');
        // Collect a lightweight workspace file listing so the operator can
        // ground its analysis in real files rather than returning files_analyzed: 0.
        const workspacePath = executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd();
        const workspaceSummary = this.buildLocalWorkspaceSummary(workspacePath);
        const requestedModel = String(executionContext.model || 'code');
        const resolvedModel = this.resolveModelId(requestedModel);
        for (const baseUrl of this.getOperatorBaseUrls()) {
            const controller = new AbortController();
            const timeoutId = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
            try {
                const response = await fetch(this.getOperatorStreamUrl(baseUrl), {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        Accept: 'text/event-stream',
                        ...(executionContext.mcpContextId ? { 'X-MCP-Context-Id': String(executionContext.mcpContextId) } : {}),
                        ...(authToken ? {
                            Authorization: `Bearer ${authToken}`,
                            Cookie: `vigthoria-auth-token=${authToken}`,
                        } : {}),
                    },
                    body: JSON.stringify({
                        prompt: message,
                        task: message,
                        raw_prompt: executionContext.rawPrompt || null,
                        model: requestedModel,
                        preferred_model: resolvedModel,
                        context_id: executionContext.contextId,
                        trace_id: executionContext.traceId,
                        mcp_context_id: executionContext.mcpContextId || null,
                        context: {
                            workspace: { path: workspacePath },
                            workspace_path: workspacePath,
                            workspace_summary: workspaceSummary,
                            model: resolvedModel,
                            history: executionContext.history || [],
                            executionSurface: executionContext.executionSurface || 'cli',
                            clientSurface: executionContext.clientSurface || 'cli',
                            contextId: executionContext.contextId,
                            traceId: executionContext.traceId,
                            mcpContextId: executionContext.mcpContextId || null,
                            mcp_context_id: executionContext.mcpContextId || null,
                            rawPrompt: executionContext.rawPrompt || null,
                            requestStartedAt: executionContext.requestStartedAt,
                            vigthoriaBrain: executionContext.vigthoriaBrain || null,
                            vigthoria_brain: executionContext.vigthoriaBrain || null,
                        },
                        workflow_type: executionContext.workflowType || 'full',
                        options: {
                            stream: true,
                            save_to_vigflow: executionContext.savePlanToVigFlow === true,
                        },
                    }),
                    signal: controller.signal,
                });
                if (!response.ok) {
                    const errorText = await response.text().catch(() => '');
                    throw new Error(`Operator stream ${response.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
                }
                if (!response.body || typeof response.body.getReader !== 'function') {
                    const fallbackData = await response.json();
                    return {
                        content: this.formatOperatorResponse(fallbackData),
                        workflowId: fallbackData.workflow_id || null,
                        contextId: fallbackData.context_id || response.headers.get('x-context-id') || executionContext.contextId || null,
                        backendUrl: baseUrl,
                        savedWorkflow: fallbackData.saved_workflow || null,
                        metadata: {
                            source: 'operator',
                            mode: 'operator',
                            contextId: fallbackData.context_id || response.headers.get('x-context-id') || executionContext.contextId || null,
                            mcpContextId: fallbackData.mcp_context_id || response.headers.get('x-mcp-context-id') || executionContext.mcpContextId || null,
                        },
                    };
                }
                const reader = response.body.getReader();
                const decoder = new TextDecoder();
                let buffer = '';
                let workflowId = null;
                let contextId = response.headers.get('x-context-id') || executionContext.contextId || null;
                let mcpContextId = response.headers.get('x-mcp-context-id') || executionContext.mcpContextId || null;
                let resultPayload = null;
                let savedWorkflow = null;
                while (true) {
                    const { done, value } = await reader.read();
                    if (done) {
                        break;
                    }
                    buffer += decoder.decode(value, { stream: true });
                    const chunks = buffer.split('\n\n');
                    buffer = chunks.pop() || '';
                    for (const chunk of chunks) {
                        const lines = chunk.split('\n');
                        let eventName = 'message';
                        const dataLines = [];
                        for (const line of lines) {
                            if (line.startsWith('event:')) {
                                eventName = line.slice(6).trim();
                            }
                            else if (line.startsWith('data:')) {
                                dataLines.push(line.slice(5).trim());
                            }
                        }
                        if (dataLines.length === 0) {
                            continue;
                        }
                        let payload = null;
                        try {
                            payload = JSON.parse(dataLines.join('\n'));
                        }
                        catch {
                            payload = { raw: dataLines.join('\n') };
                        }
                        if (payload.workflow_id && !workflowId) {
                            workflowId = payload.workflow_id;
                        }
                        if (!contextId && typeof payload.context_id === 'string' && payload.context_id.trim()) {
                            contextId = payload.context_id.trim();
                        }
                        if (!mcpContextId && typeof payload.mcp_context_id === 'string' && payload.mcp_context_id.trim()) {
                            mcpContextId = payload.mcp_context_id.trim();
                        }
                        if (!savedWorkflow && payload.saved_workflow && typeof payload.saved_workflow === 'object') {
                            savedWorkflow = payload.saved_workflow;
                        }
                        if (typeof context.onStreamEvent === 'function') {
                            try {
                                context.onStreamEvent({ type: eventName, ...payload, context_id: payload.context_id || contextId, mcp_context_id: payload.mcp_context_id || mcpContextId });
                            }
                            catch {
                                // UI callback failures must not break operator execution.
                            }
                        }
                        if (eventName === 'result') {
                            resultPayload = payload;
                        }
                        if (eventName === 'error') {
                            throw new Error(payload.error || payload.message || 'Operator workflow failed');
                        }
                    }
                }
                return {
                    content: this.formatOperatorResponse(resultPayload || {}),
                    workflowId,
                    contextId,
                    backendUrl: baseUrl,
                    savedWorkflow,
                    metadata: { source: 'operator', mode: 'operator', contextId, mcpContextId, savedWorkflowId: savedWorkflow?.id || null },
                };
            }
            catch (error) {
                const isAbort = error?.name === 'AbortError' || error?.code === 'ABORT_ERR';
                if (isAbort) {
                    const mins = Math.round(timeoutMs / 60000);
                    throw new CLIError(`Operator workflow timed out after ${mins} minute(s). You can increase the timeout with VIGTHORIA_OPERATOR_TIMEOUT_MS.`, 'timeout', error);
                }
                errors.push(`${baseUrl}: ${error?.message || String(error)}`);
            }
            finally {
                if (timeoutId)
                    clearTimeout(timeoutId);
            }
        }
        throw new CLIError(`Operator workflow failed on all endpoints: ${errors.join(' | ')}`, 'model_backend');
    }
    /**
     * Chat API - Direct Vigthoria Models API Architecture
     *
     * PRIMARY: api.vigthoria.io - Direct access to Vigthoria Model Router
     *          This is the centralized public API for all AI operations.
     *          Works for both authenticated and unauthenticated users.
     *
     * FALLBACK: Coder Cloud API - For authenticated users only
     *
     * NO localhost fallbacks - CLI is for external users, not server-side!
     */
    async chat(messages, model, useLocal = false) {
        this.lastChatTransportErrors = [];
        const resolvedModel = this.resolveModelId(model);
        const candidateModels = this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()
            ? [this.getSelfHostedFallbackModelId(resolvedModel, model)]
            : [resolvedModel];
        const fallbackModel = this.getFallbackModelId(resolvedModel);
        if (fallbackModel && fallbackModel !== resolvedModel) {
            candidateModels.push(fallbackModel);
        }
        for (const candidateModel of candidateModels) {
            const response = await this.tryChatWithModel(messages, candidateModel, model);
            if (response) {
                if (candidateModel !== resolvedModel) {
                    this.logger.debug(`Recovered chat request using fallback model: ${candidateModel}`);
                }
                return response;
            }
        }
        // Keep route diagnostics internal. Users only need to know the Vigthoria service is temporarily unavailable.
        if (this.lastChatTransportErrors.length > 0) {
            this.logger.debug(`Chat transport failures: ${this.lastChatTransportErrors.slice(0, 4).join(' | ')}`);
        }
        throw new CLIError(VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE, 'model_backend');
    }
    shouldSkipCloudRoutes(resolvedModel) {
        return this.shouldSimulateCloudFailure() && this.isCloudModelId(resolvedModel);
    }
    async tryChatWithModel(messages, resolvedModel, requestedModel) {
        const routeFailures = [];
        const preferSelfHostedFirst = this.isSelfHostedPreferredModel(resolvedModel, requestedModel);
        if (preferSelfHostedFirst) {
            const selfHostedResponse = await this.trySelfHostedChatWithModel(messages, resolvedModel, requestedModel);
            if (selfHostedResponse) {
                return selfHostedResponse;
            }
        }
        // STRATEGY 1: Direct Vigthoria Models API (api.vigthoria.io)
        // Cloud aliases must be charged through Coder wallet gates first.
        if (!this.shouldSkipCloudRoutes(resolvedModel) && !this.isCloudModelId(resolvedModel)) {
            try {
                this.logger.debug(`Direct Vigthoria Models API: ${resolvedModel}`);
                const token = this.getAccessToken();
                const response = await this.modelRouterClient.post('/v1/chat/completions', {
                    model: resolvedModel,
                    messages,
                    max_tokens: this.config.get('preferences').maxTokens,
                    temperature: 0.7,
                    stream: false,
                }, {
                    headers: token ? { Authorization: `Bearer ${token}` } : {},
                });
                if (response.data.choices && response.data.choices.length > 0) {
                    const content = response.data.choices[0].message?.content || response.data.choices[0].text;
                    if (typeof content !== 'string' || !content.trim()) {
                        this.logger.debug(`Direct API returned empty message content for ${resolvedModel}`);
                    }
                    else {
                        return {
                            id: response.data.id || `vigthoria-${Date.now()}`,
                            message: content,
                            model: response.data.model || resolvedModel || requestedModel,
                            usage: response.data.usage,
                        };
                    }
                }
                this.logger.debug(`Direct API returned no choices for ${resolvedModel}`);
            }
            catch (error) {
                const errMsg = error.response?.data?.error || error.message || 'Unknown error';
                this.logger.debug(`Direct Vigthoria Models API failed for ${resolvedModel}: ${errMsg}`);
                routeFailures.push(`models:${String(errMsg).slice(0, 120)}`);
            }
        }
        else {
            if (this.isCloudModelId(resolvedModel)) {
                this.logger.debug(`Cloud model ${resolvedModel} routed through Coder wallet gate first; skipping direct models API route.`);
            }
            else {
                this.logger.debug(`Simulating cloud failure for ${resolvedModel}; skipping cloud-backed public API route.`);
            }
        }
        // STRATEGY 2: Vigthoria Cloud API via Coder (authenticated users only)
        if (this.config.isAuthenticated() && !this.shouldSkipCloudRoutes(resolvedModel)) {
            try {
                this.logger.debug(`Vigthoria Cloud API fallback: ${resolvedModel}`);
                const response = await this.client.post('/api/ai/chat', {
                    messages,
                    model: resolvedModel,
                    maxTokens: this.config.get('preferences').maxTokens,
                    temperature: 0.7,
                    cloudConsent: this.isCloudModelId(resolvedModel),
                });
                if (response.data.success !== false) {
                    const content = response.data.response || response.data.message || response.data.content;
                    if (typeof content !== 'string' || !content.trim()) {
                        this.logger.debug(`Cloud API returned empty message content for ${resolvedModel}`);
                    }
                    else {
                        return {
                            id: response.data.id || `vigthoria-coder-${Date.now()}`,
                            message: content,
                            model: response.data.model || resolvedModel || requestedModel,
                            usage: response.data.usage,
                        };
                    }
                }
                this.logger.debug(`Cloud API returned success=false for ${resolvedModel}`);
            }
            catch (error) {
                const errMsg = error.response?.data?.error || error.message || 'Unknown error';
                this.logger.debug(`Vigthoria Cloud API failed for ${resolvedModel}: ${errMsg}`);
                routeFailures.push(`coder:${String(errMsg).slice(0, 120)}`);
            }
            try {
                this.logger.debug(`Canonical Vigthoria Cloud fallback: ${resolvedModel}`);
                const token = this.getAccessToken();
                const response = await axios.post('https://coder.vigthoria.io/api/ai/chat', {
                    messages,
                    model: resolvedModel,
                    maxTokens: this.config.get('preferences').maxTokens,
                    temperature: 0.7,
                    cloudConsent: this.isCloudModelId(resolvedModel),
                }, {
                    timeout: 180000,
                    httpsAgent: this._httpsAgent ?? undefined,
                    headers: token ? { Authorization: `Bearer ${token}`, Cookie: `vigthoria-auth-token=${token}` } : {},
                });
                if (response.data?.success !== false) {
                    const content = response.data.response || response.data.message || response.data.content;
                    if (typeof content === 'string' && content.trim()) {
                        return {
                            id: response.data.id || `vigthoria-coder-canonical-${Date.now()}`,
                            message: content,
                            model: response.data.model || resolvedModel || requestedModel,
                            usage: response.data.usage,
                        };
                    }
                }
            }
            catch (error) {
                const errMsg = error.response?.data?.error || error.message || 'Unknown error';
                this.logger.debug(`Canonical Vigthoria Cloud fallback failed for ${resolvedModel}: ${errMsg}`);
                routeFailures.push(`coder-canonical:${String(errMsg).slice(0, 120)}`);
            }
        }
        if (!preferSelfHostedFirst) {
            const selfHostedResponse = await this.trySelfHostedChatWithModel(messages, resolvedModel, requestedModel);
            if (selfHostedResponse) {
                return selfHostedResponse;
            }
        }
        if (routeFailures.length > 0) {
            this.lastChatTransportErrors = routeFailures;
        }
        return null;
    }
    async trySelfHostedChatWithModel(messages, resolvedModel, requestedModel) {
        if (!this.selfHostedModelRouterClient || !this.shouldTrySelfHostedFallback(resolvedModel, requestedModel)) {
            return null;
        }
        const selfHostedModel = this.getSelfHostedFallbackModelId(resolvedModel, requestedModel);
        try {
            this.logger.debug(`Dedicated Vigthoria model endpoint fallback: ${selfHostedModel}`);
            const response = await this.selfHostedModelRouterClient.post('/v1/chat/completions', {
                model: selfHostedModel,
                messages,
                max_tokens: this.config.get('preferences').maxTokens,
                temperature: 0.7,
                stream: false,
            }, {
                headers: {
                    'x-agent-mode': 'true',
                },
            });
            if (response.data.choices && response.data.choices.length > 0) {
                const content = response.data.choices[0].message?.content || response.data.choices[0].text;
                if (typeof content !== 'string' || !content.trim()) {
                    this.logger.debug(`Dedicated Vigthoria model endpoint returned empty message content for ${selfHostedModel}`);
                    return null;
                }
                return {
                    id: response.data.id || `vigthoria-dedicated-${Date.now()}`,
                    message: content,
                    model: response.data.model || selfHostedModel,
                    usage: response.data.usage,
                };
            }
            this.logger.debug(`Dedicated Vigthoria model endpoint returned no choices for ${selfHostedModel}`);
        }
        catch (error) {
            const errMsg = error.response?.data?.error || error.message || 'Unknown error';
            this.logger.debug(`Dedicated Vigthoria model endpoint fallback failed for ${selfHostedModel}: ${errMsg}`);
        }
        return null;
    }
    getFallbackModelId(resolvedModel) {
        const cloudModels = new Set([
            'deepseek-v3.1:671b-cloud',
            'moonshotai/kimi-k2.5',
            'vigthoria-cloud-pro',
            'vigthoria-cloud-k2',
            'vigthoria-cloud-ultra',
        ]);
        if (cloudModels.has(resolvedModel)) {
            return 'vigthoria-v3-code-35b';
        }
        return null;
    }
    isCloudModelId(resolvedModel) {
        return resolvedModel === 'deepseek-v3.1:671b-cloud'
            || resolvedModel === 'moonshotai/kimi-k2.5'
            || resolvedModel === 'vigthoria-cloud-pro'
            || resolvedModel === 'vigthoria-cloud-k2'
            || resolvedModel === 'vigthoria-cloud-ultra';
    }
    canUseCloudModel() {
        return this.config.hasCloudAccess();
    }
    resolvePermittedModelId(shortName) {
        const normalizedRequested = String(shortName || '').trim().toLowerCase();
        const blockedModels = new Set(['fast', 'mini', 'creative', 'creative-v3', 'creative-v4']);
        if (blockedModels.has(normalizedRequested)) {
            this.logger.debug(`Blocked governed model ${shortName}; using fallback vigthoria-v3-code-35b`);
            return 'vigthoria-v3-code-35b';
        }
        const resolvedModel = this.resolveModelId(shortName);
        if (this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()) {
            const fallbackModel = this.getSelfHostedFallbackModelId(resolvedModel, shortName);
            this.logger.debug(`Blocked unauthorized cloud model ${shortName}; using fallback ${fallbackModel}`);
            return fallbackModel;
        }
        return resolvedModel;
    }
    shouldSimulateCloudFailure() {
        return process.env.VIGTHORIA_SIMULATE_CLOUD_FAILURE === '1';
    }
    shouldTrySelfHostedFallback(resolvedModel, requestedModel) {
        if (!this.selfHostedModelRouterClient) {
            return false;
        }
        const normalizedRequested = String(requestedModel || '').toLowerCase();
        return this.isSelfHostedPreferredModel(resolvedModel, requestedModel)
            || normalizedRequested === 'cloud'
            || normalizedRequested === 'cloud-reason'
            || normalizedRequested === 'ultra';
    }
    isSelfHostedPreferredModel(resolvedModel, requestedModel) {
        const normalizedRequested = String(requestedModel || '').toLowerCase();
        const selfHostedModels = new Set([
            'vigthoria-v3-code-35b',
            'vigthoria-v3-code-35b:latest',
            'vigthoria-v3-code-9b',
            'vigthoria-v3-code-9b:latest',
            'vigthoria-v3-balanced-4b',
            'vigthoria-v3-balanced-4b:latest',
            'qwen3-coder:latest',
        ]);
        return selfHostedModels.has(resolvedModel)
            || normalizedRequested === 'agent'
            || normalizedRequested === 'code'
            || normalizedRequested === 'code-30b'
            || normalizedRequested === 'code-35b'
            || normalizedRequested === 'code-9b'
            || normalizedRequested === 'balanced'
            || normalizedRequested === 'balanced-4b'
            || normalizedRequested === 'pro';
    }
    getSelfHostedFallbackModelId(resolvedModel, requestedModel) {
        if (this.isSelfHostedPreferredModel(resolvedModel, requestedModel)) {
            return resolvedModel === 'qwen3-coder:latest' ? 'vigthoria-v3-code-35b' : resolvedModel;
        }
        return 'vigthoria-v3-code-35b';
    }
    // Streaming chat
    async *chatStream(messages, model) {
        const wsUrl = this.config.get('wsUrl');
        const token = this.config.get('authToken');
        return new Promise((resolve, reject) => {
            const ws = new WebSocket(`${wsUrl}/chat`, {
                headers: { Authorization: `Bearer ${token}` },
            });
            ws.on('open', () => {
                ws.send(JSON.stringify({
                    type: 'chat',
                    messages,
                    model: this.resolvePermittedModelId(model),
                    stream: true,
                }));
            });
            ws.on('message', (data) => {
                const chunk = JSON.parse(data.toString());
                // Yield chunk - but this pattern won't work directly
                // Need to use event emitter pattern instead
            });
            ws.on('error', (error) => {
                reject(error);
            });
            ws.on('close', () => {
                resolve(undefined);
            });
        });
    }
    // Non-streaming alternative with callback
    async chatWithCallback(messages, model, onChunk, onDone, onError) {
        const wsUrl = this.config.get('wsUrl');
        const token = this.config.get('authToken');
        return new Promise((resolve, reject) => {
            const ws = new WebSocket(`${wsUrl}/chat`, {
                headers: { Authorization: `Bearer ${token}` },
            });
            ws.on('open', () => {
                ws.send(JSON.stringify({
                    type: 'chat',
                    messages,
                    model: this.resolvePermittedModelId(model),
                    stream: true,
                }));
            });
            ws.on('message', (data) => {
                try {
                    const chunk = JSON.parse(data.toString());
                    if (chunk.type === 'content') {
                        onChunk(chunk.content);
                    }
                    else if (chunk.type === 'done') {
                        onDone();
                        ws.close();
                        resolve();
                    }
                    else if (chunk.type === 'error') {
                        onError(new Error(chunk.error));
                        ws.close();
                        reject(new Error(chunk.error));
                    }
                }
                catch (e) {
                    // Raw text chunk
                    onChunk(data.toString());
                }
            });
            ws.on('error', (error) => {
                onError(error);
                reject(error);
            });
            ws.on('close', () => {
                resolve();
            });
        });
    }
    // ─── Chat completion helper ────────────────────────────────────────
    // Routes all AI file commands through the Model Router
    // (/v1/chat/completions on api.vigthoria.io) which is the only
    // backend that reliably accepts our auth token.
    async chatComplete(systemPrompt, userPrompt, model, maxTokens) {
        const resolvedModel = model ? this.resolvePermittedModelId(model) : 'vigthoria-v3-code-35b';
        const response = await this.modelRouterClient.post('/v1/chat/completions', {
            model: resolvedModel,
            messages: [
                { role: 'system', content: systemPrompt },
                { role: 'user', content: userPrompt },
            ],
            max_tokens: maxTokens || this.config.get('preferences').maxTokens || 4096,
            temperature: 0.3,
            stream: false,
        });
        const content = response.data.choices?.[0]?.message?.content || response.data.choices?.[0]?.text || '';
        return typeof content === 'string' ? content : '';
    }
    // Code operations - Using Vigthoria Centralized API (via Model Router)
    async generateCode(prompt, language, model) {
        const isNonHtmlLang = !['html', 'css'].includes(language.toLowerCase());
        const wordCount = prompt.trim().split(/\s+/).length;
        // Prepend a forceful scope-enforcement instruction so the model
        // doesn't expand a small task into an oversized glossy page.
        const buildScopedPrompt = (retry) => {
            if (retry && isNonHtmlLang) {
                // Ultra-minimal retry prompt — no long instruction block
                return `OUTPUT ONLY RAW ${language.toUpperCase()} CODE. MAXIMUM 10 LINES. NO DOM. NO HTML. NO CSS. NO ANIMATION. NO VALIDATION. NO COMMENTS.\n\n${prompt}`;
            }
            const lines = [
                'IMPORTANT — MANDATORY SCOPE CONSTRAINTS (violation = failure):',
                `1. You MUST output ONLY ${language.toUpperCase()} code. Do NOT output HTML, CSS, or DOM manipulation unless the language is html/css.`,
                `2. The target language is: ${language}. Do not switch to a different language.`,
                '3. Output ONLY what the user explicitly asked for. Nothing more.',
                '4. If the prompt is ≤ 15 words, produce ≤ 80 lines of code maximum.',
                '5. If the user says "tiny", "small", "simple", "minimal", or "basic", produce ≤ 50 lines.',
                '6. NEVER add any of these unless the user EXPLICITLY requests them:',
                '   - Hero sections, CTAs, testimonials, pricing tables, footers, navbars',
                '   - CSS animations, gradients, neon effects, glass-morphism, particles',
                '   - Google Fonts, Font Awesome, external CDN links, icon libraries',
                '   - DOM manipulation, document.createElement, innerHTML',
                '   - Responsive breakpoints, media queries (unless asked)',
                '   - Multiple pages or components when one was requested',
                '   - Input validation, error handling beyond what was requested',
                '7. Prefer inline styles or a small <style> block ONLY if the language is html. No CSS frameworks.',
                '8. Return raw code only — no markdown fences, no explanations, no comments about what could be added.',
                '9. Match the complexity of the request: a "hello world" is 1-5 lines, a "multiply function" is 3-5 lines, a "button" is 5-15 lines.',
            ];
            if (isNonHtmlLang) {
                lines.push(`10. This is a PURE ${language.toUpperCase()} task. ABSOLUTELY NO document.*, window.*, DOM, createElement, innerHTML, querySelector, addEventListener. Output a plain ${language} function/module ONLY.`);
            }
            lines.push('', prompt);
            return lines.join('\n');
        };
        // First attempt — route through Model Router chat completions
        const systemPrompt = `You are a code generator. Output ONLY raw ${language} code. No markdown fences, no explanations, no commentary. Just the code.`;
        let code = await this.chatComplete(systemPrompt, buildScopedPrompt(false), model);
        // Strip markdown fences if model included them
        code = code.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
        // Client-side validation: reject DOM-polluted or over-engineered responses for non-HTML languages
        const needsRetry = isNonHtmlLang && (this.codeContainsDomPollution(code) ||
            this.codeIsOverEngineered(code, prompt));
        if (needsRetry) {
            // Retry once with stronger constraint — via Model Router
            code = await this.chatComplete(systemPrompt, buildScopedPrompt(true), model);
            code = code.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
            // If still polluted, strip DOM code client-side
            if (this.codeContainsDomPollution(code)) {
                code = this.stripDomPollution(code, language);
            }
        }
        // Final cleanup: for non-HTML, if still over-engineered after retry,
        // extract only the first complete function/class definition.
        if (isNonHtmlLang && this.codeIsOverEngineered(code, prompt)) {
            code = this.extractFirstFunction(code);
        }
        // Ensure balanced braces — model sometimes truncates closing braces
        code = this.ensureBalancedBraces(code);
        return code;
    }
    /**
     * Ensure code has balanced curly braces by appending missing closing braces.
     */
    ensureBalancedBraces(code) {
        // Count braces/parens/brackets outside strings and comments
        let braces = 0, parens = 0, brackets = 0;
        let inStr = null;
        let inLine = false, inBlock = false;
        for (let i = 0; i < code.length; i++) {
            const ch = code[i], nx = code[i + 1] || '';
            if (inLine) {
                if (ch === '\n')
                    inLine = false;
                continue;
            }
            if (inBlock) {
                if (ch === '*' && nx === '/') {
                    inBlock = false;
                    i++;
                }
                continue;
            }
            if (inStr) {
                if (ch === inStr && code[i - 1] !== '\\')
                    inStr = null;
                continue;
            }
            if (ch === '/' && nx === '/') {
                inLine = true;
                continue;
            }
            if (ch === '/' && nx === '*') {
                inBlock = true;
                continue;
            }
            if (ch === '"' || ch === "'" || ch === '`') {
                inStr = ch;
                continue;
            }
            if (ch === '{')
                braces++;
            else if (ch === '}')
                braces--;
            else if (ch === '(')
                parens++;
            else if (ch === ')')
                parens--;
            else if (ch === '[')
                brackets++;
            else if (ch === ']')
                brackets--;
        }
        let result = code.trimEnd();
        for (let i = 0; i < braces; i++)
            result += '\n}';
        for (let i = 0; i < parens; i++)
            result += ')';
        for (let i = 0; i < brackets; i++)
            result += ']';
        return braces > 0 || parens > 0 || brackets > 0 ? result : code;
    }
    /**
     * Quick JS/TS syntax validation using Node's built-in parser.
     * Returns true if the code parses without errors.
     */
    validateJsSyntax(code) {
        try {
            // Use Function constructor to check syntax without executing
            new Function(code);
            return true;
        }
        catch {
            return false;
        }
    }
    /**
     * Extract the first complete function/class from code.
     * Used as last-resort when the model keeps over-producing.
     */
    extractFirstFunction(code) {
        const lines = code.split('\n');
        const funcStart = lines.findIndex(l => /^(export\s+)?(const\s+\w+\s*=\s*(\(|function)|function\s+\w+|class\s+\w+|def\s+\w+|fn\s+\w+|func\s+\w+)/.test(l.trim()));
        if (funcStart === -1)
            return code;
        let braceDepth = 0;
        let foundOpen = false;
        const result = [];
        for (let i = funcStart; i < lines.length; i++) {
            const line = lines[i];
            result.push(line);
            braceDepth += (line.match(/{/g) || []).length;
            braceDepth -= (line.match(/}/g) || []).length;
            if (braceDepth > 0)
                foundOpen = true;
            if (foundOpen && braceDepth <= 0) {
                break;
            }
        }
        // If we reached the end without balancing braces, add closing
        if (braceDepth > 0) {
            result.push('};');
        }
        return result.join('\n');
    }
    /**
     * Detect if code is excessively over-engineered for a short prompt.
     * E.g. a "multiply function" request producing 20+ lines.
     */
    codeIsOverEngineered(code, prompt) {
        const wordCount = prompt.trim().split(/\s+/).length;
        const lineCount = code.trim().split('\n').length;
        // Short prompt (≤15 words) should not produce more than 15 lines
        if (wordCount <= 15 && lineCount > 15)
            return true;
        return false;
    }
    /**
     * Detect if generated code contains DOM/HTML pollution inappropriate
     * for a pure programming language like JavaScript, Python, etc.
     */
    codeContainsDomPollution(code) {
        const domPatterns = /document\.(createElement|querySelector|getElementById|getElementsBy|body|head|addEventListener)|innerHTML|\.style\.(cssText|position|transform|animation)|@keyframes|\.appendChild|\.removeChild|window\.(onload|addEventListener)|CSSAnimation|new\s+Animation\b|\.getAnimations\s*\(|\.animate\s*\(/;
        return domPatterns.test(code);
    }
    /**
     * Strip DOM pollution from generated code, keeping only the pure logic.
     * Used as last-resort fallback when the model repeatedly ignores constraints.
     */
    stripDomPollution(code, language) {
        const lines = code.split('\n');
        const cleanLines = [];
        let insideDomBlock = false;
        let braceDepth = 0;
        for (const line of lines) {
            // Detect start of DOM blocks
            if (/document\.|\.style\.|\.appendChild|\.removeChild|\.textContent\s*=|@keyframes|addEventListener|CSSAnimation|\.getAnimations|\.animate\s*\(/.test(line) && !insideDomBlock) {
                insideDomBlock = true;
                braceDepth = 0;
            }
            if (insideDomBlock) {
                braceDepth += (line.match(/{/g) || []).length;
                braceDepth -= (line.match(/}/g) || []).length;
                if (braceDepth <= 0) {
                    insideDomBlock = false;
                }
                continue; // Skip DOM lines
            }
            cleanLines.push(line);
        }
        const cleaned = cleanLines.join('\n').trim();
        // If we stripped too aggressively (less than 2 lines left), return a minimal stub
        if (cleaned.split('\n').length < 2) {
            return `// Auto-generated: model returned DOM code for a ${language} task.\n// Please regenerate or provide more specific instructions.`;
        }
        return cleaned;
    }
    // Senior Developer Mode - Planning + Generation + Quality Check
    async generateProject(prompt, projectType, model) {
        const sysPrompt = [
            `You are a senior developer. Generate a complete ${projectType} project.`,
            'Return a JSON object with these fields:',
            '  "code": the full source code as a string,',
            '  "plan": an object describing the architecture,',
            '  "quality": { "lineCount": number, "score": number (0-100) }',
            'Return ONLY the JSON object, no markdown fences.',
        ].join('\n');
        const raw = await this.chatComplete(sysPrompt, prompt, model, 8192);
        try {
            const cleaned = raw.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
            const parsed = JSON.parse(cleaned);
            return {
                code: parsed.code || raw,
                plan: parsed.plan,
                quality: parsed.quality,
            };
        }
        catch {
            return { code: raw.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim() };
        }
    }
    async explainCode(code, language) {
        const sysPrompt = [
            `You are a code explainer. Explain the following ${language} code clearly and concisely.`,
            'Focus on what it does, how it works, and any notable patterns or potential issues.',
            'Format your response as clean Markdown:',
            '- Use ## headers for major sections (e.g. ## Overview, ## How It Works, ## Key Details).',
            '- Use bullet points (- or *) for all lists. Do NOT use numbered lists.',
            '- Wrap code references in backticks.',
            '- Keep paragraphs short (2-3 sentences max).',
            '- Do NOT use raw HTML or excessive blank lines.',
            '- Do NOT nest numbered lists inside sections.',
        ].join('\n');
        return this.chatComplete(sysPrompt, code);
    }
    async reviewCode(code, language) {
        const sysPrompt = [
            `You are a senior code reviewer for ${language}. Analyze the code and return a JSON object with:`,
            '  "score": number 0-100,',
            '  "issues": [{ "type": string, "line": number, "message": string, "severity": "error"|"warning"|"info" }],',
            '  "suggestions": [string]',
            'Rules:',
            '- Return concrete, line-specific issues with severity.',
            '- Every issue MUST reference a line number.',
            '- Report each distinct bug ONCE. Do NOT report the same bug multiple times with different wording.',
            '- For trivial/short code (< 10 lines), report ONLY actual bugs. Do NOT pad with style, robustness, or best-practice suggestions.',
            '- If you find a real bug (wrong operator, logic error, type mismatch), report ONLY that bug. Do NOT also suggest input validation, type checking, or error handling unless those are ACTUAL bugs.',
            '- Prioritize REAL BUGS: wrong operators, logic errors, off-by-one, type mismatches.',
            '- Do NOT suggest adding error handling, input validation, or documentation as issues unless the user explicitly asked for a style review.',
            '- Return ONLY the JSON object, no markdown fences or extra text.',
        ].join('\n');
        let raw = {};
        try {
            const result = await this.chatComplete(sysPrompt, code);
            const cleaned = result.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
            raw = JSON.parse(cleaned);
        }
        catch {
            // If parsing fails, return minimal review
        }
        const score = typeof raw.score === 'number' ? raw.score : 0;
        const issues = Array.isArray(raw.issues) ? raw.issues : [];
        const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
        // Merge client-side heuristics, but with tight dedup to avoid
        // redundant over-reporting when the model already found the bug.
        const modelFoundError = issues.some(i => i.severity === 'error');
        const heuristic = this.heuristicCodeIssues(code, language);
        for (const h of heuristic) {
            // If the model already found a real error, skip non-error heuristics
            // entirely — they're just padding (style, robustness, etc.)
            if (modelFoundError && h.severity !== 'error')
                continue;
            // Semantic duplicate check: same line + (similar type OR overlapping
            // keywords in the message).  This catches cases where the model
            // and heuristic describe the same bug with different wording.
            const hWords = new Set(h.message.toLowerCase().split(/\W+/).filter(w => w.length > 3));
            const hTypeNorm = h.type.toLowerCase().replace(/[^a-z]/g, '');
            const isSemanticallyDuplicate = issues.some((existing) => {
                if (existing.line !== h.line)
                    return false;
                // Normalize types: "logic-error", "logic_error", "logic" all match
                const eTypeNorm = existing.type.toLowerCase().replace(/[^a-z]/g, '');
                if (eTypeNorm === hTypeNorm || eTypeNorm.startsWith(hTypeNorm) || hTypeNorm.startsWith(eTypeNorm))
                    return true;
                // Both errors on same line about the same category of problem
                if (existing.severity === 'error' && h.severity === 'error')
                    return true;
                // Check keyword overlap — if ≥2 significant words match, it's the same finding
                const eWords = existing.message.toLowerCase().split(/\W+/).filter(w => w.length > 3);
                let overlap = 0;
                for (const w of eWords) {
                    if (hWords.has(w))
                        overlap++;
                }
                return overlap >= 2;
            });
            if (!isSemanticallyDuplicate) {
                issues.push(h);
            }
        }
        // Sort: errors first, then warnings, then info
        const severityOrder = { error: 0, warning: 1, info: 2 };
        issues.sort((a, b) => (severityOrder[a.severity] ?? 3) - (severityOrder[b.severity] ?? 3));
        // Prevent contradictory output: low score but zero issues.
        if (score < 50 && issues.length === 0) {
            issues.push({
                type: 'quality',
                line: 1,
                message: `The analysis returned a low quality score (${score}/100) but did not enumerate specific issues. Re-run the review or inspect the file manually.`,
                severity: 'warning',
            });
        }
        return { score, issues, suggestions };
    }
    /**
     * Lightweight client-side heuristic scan: catches common code smells
     * AND logic/arithmetic bugs so review never returns "score 30, no issues".
     */
    heuristicCodeIssues(code, language) {
        const issues = [];
        const lines = code.split('\n');
        for (let i = 0; i < lines.length; i++) {
            const line = lines[i];
            const lineNum = i + 1;
            // console.log left in production code
            if (/\bconsole\.(log|debug|info)\b/.test(line) && !/\/\//.test(line.slice(0, line.indexOf('console')))) {
                issues.push({ type: 'quality', line: lineNum, message: 'console.log/debug statement — remove or replace with proper logging.', severity: 'warning' });
            }
            // TODO/FIXME/HACK comments
            if (/\b(TODO|FIXME|HACK|XXX)\b/.test(line)) {
                issues.push({ type: 'maintainability', line: lineNum, message: `Unresolved ${line.match(/\b(TODO|FIXME|HACK|XXX)\b/)?.[0]} comment.`, severity: 'info' });
            }
            // Empty catch blocks
            if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(line)) {
                issues.push({ type: 'error-handling', line: lineNum, message: 'Empty catch block — errors are silently swallowed.', severity: 'warning' });
            }
            // Very long lines (> 200 chars)
            if (line.length > 200) {
                issues.push({ type: 'style', line: lineNum, message: `Line exceeds 200 characters (${line.length}).`, severity: 'info' });
            }
            // Arithmetic / logic bugs: function named 'add' but using subtraction
            if (/\breturn\s+\w+\s*-\s*\w+/.test(line)) {
                // Check if the enclosing function name implies addition
                for (let j = i; j >= Math.max(0, i - 5); j--) {
                    if (/function\s+add\b/i.test(lines[j]) || /\badd\s*[=(]/.test(lines[j])) {
                        issues.push({ type: 'logic', line: lineNum, message: 'Subtraction operator in a function named "add" — likely should be addition (+).', severity: 'error' });
                        break;
                    }
                }
            }
            // Multiplication where addition is expected
            if (/\breturn\s+\w+\s*\*\s*\w+/.test(line)) {
                for (let j = i; j >= Math.max(0, i - 5); j--) {
                    if (/function\s+add\b/i.test(lines[j]) || /\badd\s*[=(]/.test(lines[j])) {
                        issues.push({ type: 'logic', line: lineNum, message: 'Multiplication operator in a function named "add" — likely should be addition (+).', severity: 'error' });
                        break;
                    }
                }
            }
            // Division where subtraction/addition is expected in subtract
            if (/\breturn\s+\w+\s*\+\s*\w+/.test(line)) {
                for (let j = i; j >= Math.max(0, i - 5); j--) {
                    if (/function\s+subtract\b/i.test(lines[j]) || /\bsubtract\s*[=(]/.test(lines[j])) {
                        issues.push({ type: 'logic', line: lineNum, message: 'Addition operator in a function named "subtract" — likely should be subtraction (-).', severity: 'error' });
                        break;
                    }
                }
            }
            // Accumulator/reducer bug: subtraction used in a reduce/total/sum context
            // e.g. "sum - item.price" in a function named calculateTotal, getTotal, etc.
            if (/\w+\s*-\s*\w+\.\s*(price|cost|amount|value|total|quantity|count)\b/.test(line) || /\w+\.\s*(price|cost|amount|value|total)\s*-\s*/.test(line)) {
                // Check enclosing function/method name for total/sum/aggregate semantics
                for (let j = i; j >= Math.max(0, i - 10); j--) {
                    if (/(function\s+)?(calculate|calc|get|compute|sum|total|aggregate|accumulate)\s*(Total|Sum|Cost|Price|Amount|All)?\s*[=(]/i.test(lines[j]) || /\.(reduce|forEach|map)\s*\(/.test(lines[j])) {
                        // Also check if this looks like an accumulator pattern (sum - something instead of sum + something)
                        if (/\w+\s*-\s*\w+\.\s*(price|cost|amount|value|total|quantity)\b/.test(line)) {
                            issues.push({ type: 'logic', line: lineNum, message: 'Subtraction used in an accumulation context (total/sum/reduce) — likely should be addition (+).', severity: 'error' });
                        }
                        break;
                    }
                }
            }
            // Mismatched function name vs operator: sum/total function using wrong operator
            if (/\breturn\b/.test(line)) {
                for (let j = i; j >= Math.max(0, i - 8); j--) {
                    const funcMatch = lines[j].match(/function\s+(\w+)/i) || lines[j].match(/(?:const|let|var)\s+(\w+)\s*=/i);
                    if (funcMatch) {
                        const funcName = funcMatch[1].toLowerCase();
                        if ((funcName.includes('total') || funcName.includes('sum') || funcName.includes('add')) && /\s-\s/.test(line) && !/\s\+\s/.test(line)) {
                            const alreadyReported = issues.some(iss => iss.line === lineNum && iss.type === 'logic');
                            if (!alreadyReported) {
                                issues.push({ type: 'logic', line: lineNum, message: `Subtraction operator in "${funcMatch[1]}" — function name implies addition. Likely should use + instead of -.`, severity: 'error' });
                            }
                        }
                        break;
                    }
                }
            }
            // Off-by-one: comparing with === where <= or >= may be needed
            // (not implemented yet — would require more complex AST analysis)
            // Limit to 15 heuristic issues
            if (issues.length >= 15)
                break;
        }
        return issues;
    }
    async fixCode(code, language, fixType) {
        // Client-side syntax pre-check: detect obvious errors and include
        // them in the request so the model has concrete signals.
        const syntaxHints = fixType === 'bugs' || fixType === 'syntax' ? this.detectSyntaxErrors(code, language) : '';
        // Run heuristic logic analysis, but ONLY pass hints relevant to the fixType
        const heuristicIssues = this.heuristicCodeIssues(code, language);
        const relevantIssues = heuristicIssues.filter(issue => {
            if (fixType === 'bugs' || fixType === 'logic')
                return issue.type === 'logic' || issue.severity === 'error';
            if (fixType === 'syntax')
                return issue.type !== 'logic' && issue.severity === 'error';
            if (fixType === 'style')
                return issue.type === 'style' || issue.type === 'quality';
            if (fixType === 'security')
                return issue.type === 'security';
            if (fixType === 'performance')
                return issue.type === 'performance';
            return true; // 'all' or unknown fixType: include everything
        });
        const logicHints = relevantIssues
            .map(i => `Line ${i.line}: [${i.type}] ${i.message}`)
            .join('\n// ');
        const allHints = [syntaxHints, logicHints].filter(Boolean).join('\n// ');
        // Build a constraint preamble that's stricter for syntax-only mode.
        let preamble;
        if (fixType === 'syntax') {
            preamble = allHints
                ? `// SYNTAX ERRORS DETECTED BY CLIENT:\n// ${allHints}\n// RULES: Fix ONLY bracket/parenthesis/brace mismatches and keyword typos. Do NOT rename functions, do NOT add comments, do NOT restructure or reformat. Output the corrected code ONLY.\n\n`
                : '';
        }
        else {
            preamble = allHints
                ? `// BUGS DETECTED BY STATIC ANALYSIS — YOU MUST FIX THESE:\n// ${allHints}\n// IMPORTANT: Fix ONLY these specific bugs. Do not add comments, do not restructure the code, do not add or remove lines beyond the minimal fix.\n\n`
                : '';
        }
        const augmentedCode = preamble ? `${preamble}${code}` : code;
        const sysPrompt = [
            `You are a ${language} code fixer. Fix the code for: ${fixType}.`,
            'Return a JSON object with:',
            '  "fixed": the COMPLETE corrected source code as a string (not a snippet — the full file),',
            '  "changes": [{ "line": number, "before": string, "after": string, "reason": string }]',
            'Rules:',
            '- The "fixed" field MUST contain the entire corrected source code with ALL lines, including unchanged lines.',
            '- The "fixed" code MUST have balanced braces, parentheses, and brackets.',
            '- Fix ONLY the issues related to the fix type.',
            '- Do not add comments, do not restructure beyond the minimal fix.',
            '- Return ONLY the JSON object, no markdown fences.',
        ].join('\n');
        let raw = {};
        try {
            const result = await this.chatComplete(sysPrompt, augmentedCode);
            const cleaned = result.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
            raw = JSON.parse(cleaned);
        }
        catch {
            // If parsing fails, fall through to client-side handling
        }
        let fixed = typeof raw.fixed === 'string' ? raw.fixed : (typeof raw.code === 'string' ? raw.code : code);
        let changes = Array.isArray(raw.changes) ? raw.changes : [];
        // If server returned no changes but we found issues, strip
        // our injected comment prefix from the returned code and attempt
        // a basic client-side repair.
        if (changes.length === 0 && preamble && fixed === augmentedCode) {
            fixed = code; // restore original
        }
        // Strip the injected comment block if it leaked into the output
        if (fixed.startsWith('// BUGS DETECTED BY STATIC ANALYSIS') || fixed.startsWith('// SYNTAX ERRORS DETECTED BY CLIENT')) {
            const idx = fixed.indexOf('\n\n');
            if (idx !== -1) {
                fixed = fixed.slice(idx + 2);
            }
        }
        // For syntax-only fixes, strip any comments the model added that
        // weren't in the original code (e.g. "// Fixed mismatched parentheses").
        if (fixType === 'syntax' && fixed !== code) {
            fixed = this.stripInjectedComments(code, fixed, language);
        }
        // Safety net: for syntax fixes, ensure the fix didn't make bracket
        // balance worse.  If the original had more closing delimiters than
        // the fix, append the missing ones.
        if (fixType === 'syntax' && fixed !== code) {
            fixed = this.repairBracketBalance(code, fixed);
        }
        // Final bracket-balance guarantee — ensure the emitted code has
        // balanced braces/parens/brackets regardless of what the model returned.
        fixed = this.ensureBalancedBraces(fixed);
        // For JS/TS syntax fixes, validate the output actually parses.
        // If it doesn't, attempt a more aggressive bracket repair.
        if ((fixType === 'syntax' || fixType === 'bugs') && fixed !== code) {
            const lang = language.toLowerCase();
            if (['javascript', 'js', 'typescript', 'ts'].includes(lang)) {
                if (!this.validateJsSyntax(fixed)) {
                    // Try once more: strip any remaining injected comments and re-balance
                    let repaired = this.stripInjectedComments(code, fixed, language);
                    repaired = this.ensureBalancedBraces(repaired);
                    if (this.validateJsSyntax(repaired)) {
                        fixed = repaired;
                    }
                    // If still invalid, return the best-effort fix — better than
                    // silently reverting to the original broken code.
                }
            }
        }
        // If there are still no changes but the fixed code differs, compute
        // a semantic diff using LCS so inserted/removed lines don't cause
        // every subsequent line to appear as changed.
        if (changes.length === 0 && fixed !== code) {
            // Use a clean reason string — strip verbose static-analysis hints
            const cleanReason = relevantIssues.length > 0
                ? relevantIssues.map(i => i.message).join('; ')
                : 'AI-suggested fix';
            changes = this.computeSemanticDiff(code, fixed, cleanReason);
        }
        return { fixed, changes };
    }
    /**
     * Compute a semantic diff between original and fixed code using
     * Longest Common Subsequence (LCS) to avoid the line-shift inflation
     * bug where inserting one line flags all subsequent lines as changed.
     */
    computeSemanticDiff(original, fixed, reason) {
        const origLines = original.split('\n');
        const fixedLines = fixed.split('\n');
        const changes = [];
        // Build LCS table
        const m = origLines.length;
        const n = fixedLines.length;
        const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
        for (let i = 1; i <= m; i++) {
            for (let j = 1; j <= n; j++) {
                if (origLines[i - 1] === fixedLines[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        // Backtrack to find the diff
        let i = m, j = n;
        const ops = [];
        while (i > 0 || j > 0) {
            if (i > 0 && j > 0 && origLines[i - 1] === fixedLines[j - 1]) {
                ops.push({ type: 'keep', origLine: i, fixedLine: j, text: origLines[i - 1] });
                i--;
                j--;
            }
            else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
                ops.push({ type: 'add', fixedLine: j, text: fixedLines[j - 1] });
                j--;
            }
            else {
                ops.push({ type: 'remove', origLine: i, text: origLines[i - 1] });
                i--;
            }
        }
        ops.reverse();
        // Merge adjacent remove+add pairs into single change entries
        let idx = 0;
        while (idx < ops.length) {
            const op = ops[idx];
            if (op.type === 'remove') {
                // Look ahead for a matching 'add' immediately after
                if (idx + 1 < ops.length && ops[idx + 1].type === 'add') {
                    changes.push({
                        line: op.origLine,
                        before: op.text,
                        after: ops[idx + 1].text,
                        reason: reason || 'AI-suggested fix',
                    });
                    idx += 2;
                    continue;
                }
                changes.push({
                    line: op.origLine,
                    before: op.text,
                    after: '(line removed)',
                    reason: reason || 'AI-suggested fix',
                });
            }
            else if (op.type === 'add') {
                changes.push({
                    line: op.fixedLine,
                    before: '(new line)',
                    after: op.text,
                    reason: reason || 'AI-suggested fix',
                });
            }
            idx++;
        }
        if (changes.length === 0) {
            changes.push({ line: 1, before: '(whitespace changes)', after: '(see fixed file)', reason: reason || 'AI-suggested fix' });
        }
        return changes;
    }
    /**
     * Lightweight client-side syntax error detection.
     * Returns a human-readable description of obvious errors, or empty string.
     */
    detectSyntaxErrors(code, language) {
        const lang = language.toLowerCase();
        const errors = [];
        if (lang === 'javascript' || lang === 'typescript' || lang === 'js' || lang === 'ts') {
            // Bracket matching
            let braces = 0, brackets = 0, parens = 0;
            // Track string context (simple heuristic — not a full parser)
            let inString = null;
            let inLineComment = false;
            let inBlockComment = false;
            for (let i = 0; i < code.length; i++) {
                const ch = code[i];
                const next = code[i + 1] || '';
                if (inLineComment) {
                    if (ch === '\n')
                        inLineComment = false;
                    continue;
                }
                if (inBlockComment) {
                    if (ch === '*' && next === '/') {
                        inBlockComment = false;
                        i++;
                    }
                    continue;
                }
                if (inString) {
                    if (ch === inString && code[i - 1] !== '\\')
                        inString = null;
                    continue;
                }
                if (ch === '/' && next === '/') {
                    inLineComment = true;
                    continue;
                }
                if (ch === '/' && next === '*') {
                    inBlockComment = true;
                    continue;
                }
                if (ch === '"' || ch === "'" || ch === '`') {
                    inString = ch;
                    continue;
                }
                if (ch === '{')
                    braces++;
                else if (ch === '}')
                    braces--;
                else if (ch === '[')
                    brackets++;
                else if (ch === ']')
                    brackets--;
                else if (ch === '(')
                    parens++;
                else if (ch === ')')
                    parens--;
            }
            if (braces !== 0)
                errors.push(`Mismatched braces (balance: ${braces > 0 ? '+' : ''}${braces})`);
            if (brackets !== 0)
                errors.push(`Mismatched brackets (balance: ${brackets > 0 ? '+' : ''}${brackets})`);
            if (parens !== 0)
                errors.push(`Mismatched parentheses (balance: ${parens > 0 ? '+' : ''}${parens})`);
            // Detect obvious keyword typos
            const lines = code.split('\n');
            for (let li = 0; li < lines.length; li++) {
                const line = lines[li];
                if (/\bfuncion\b|\bfuction\b|\bfuntion\b/i.test(line))
                    errors.push(`Line ${li + 1}: Misspelled 'function'`);
                if (/\bretrun\b/i.test(line))
                    errors.push(`Line ${li + 1}: Misspelled 'return'`);
                if (/\bconts\b|\bcosnt\b/i.test(line))
                    errors.push(`Line ${li + 1}: Misspelled 'const'`);
            }
        }
        if (lang === 'python' || lang === 'py') {
            const lines = code.split('\n');
            for (let li = 0; li < lines.length; li++) {
                const line = lines[li];
                if (/^\s*(def|class|if|elif|else|for|while|try|except|with)\b/.test(line) && !line.trimEnd().endsWith(':') && !line.trimEnd().endsWith('\\')) {
                    errors.push(`Line ${li + 1}: Missing colon after '${line.trim().split(/\s/)[0]}'`);
                }
            }
        }
        return errors.join('; ');
    }
    /**
     * Strip comment lines that the model added during a fix but were not
     * present in the original code.  Used for syntax-only fixes where the
     * model tends to annotate its changes with "// Fixed ..." comments.
     */
    stripInjectedComments(original, fixed, language) {
        const lang = language.toLowerCase();
        // Only handle JS/TS/Python single-line comment patterns for now
        let wholeLineRe;
        let inlineRe;
        if (lang === 'python' || lang === 'py') {
            wholeLineRe = /^\s*#\s/;
            inlineRe = /\s+#\s.*$/;
        }
        else {
            wholeLineRe = /^\s*\/\/\s/;
            inlineRe = /\s+\/\/\s.*$/;
        }
        const origLines = original.split('\n');
        const origSet = new Set(origLines.map(l => l.trim()));
        const fixedLines = fixed.split('\n');
        const result = [];
        for (let idx = 0; idx < fixedLines.length; idx++) {
            let line = fixedLines[idx];
            // Remove whole-line comments not in original
            if (wholeLineRe.test(line) && !origSet.has(line.trim())) {
                continue;
            }
            // Strip inline trailing comments the model added.
            // Only strip if the corresponding original line at the same position
            // didn't have an inline comment at all.
            if (inlineRe.test(line)) {
                const origLine = idx < origLines.length ? origLines[idx] : '';
                if (!inlineRe.test(origLine)) {
                    line = line.replace(inlineRe, '');
                }
            }
            result.push(line);
        }
        return result.join('\n');
    }
    /**
     * Ensure the fixed code hasn't lost closing delimiters relative to the
     * original.  Counts {, }, (, ), [, ] outside strings/comments and if
     * the fix has fewer closers than the original, appends the missing ones.
     */
    repairBracketBalance(original, fixed) {
        const count = (src) => {
            let braces = 0, parens = 0, brackets = 0;
            let inStr = null;
            let inLine = false, inBlock = false;
            for (let i = 0; i < src.length; i++) {
                const ch = src[i], nx = src[i + 1] || '';
                if (inLine) {
                    if (ch === '\n')
                        inLine = false;
                    continue;
                }
                if (inBlock) {
                    if (ch === '*' && nx === '/') {
                        inBlock = false;
                        i++;
                    }
                    continue;
                }
                if (inStr) {
                    if (ch === inStr && src[i - 1] !== '\\')
                        inStr = null;
                    continue;
                }
                if (ch === '/' && nx === '/') {
                    inLine = true;
                    continue;
                }
                if (ch === '/' && nx === '*') {
                    inBlock = true;
                    continue;
                }
                if (ch === '"' || ch === "'" || ch === '`') {
                    inStr = ch;
                    continue;
                }
                if (ch === '{')
                    braces++;
                else if (ch === '}')
                    braces--;
                else if (ch === '(')
                    parens++;
                else if (ch === ')')
                    parens--;
                else if (ch === '[')
                    brackets++;
                else if (ch === ']')
                    brackets--;
            }
            return { braces, parens, brackets };
        };
        const orig = count(original);
        const fix = count(fixed);
        const append = [];
        // If original was balanced (or close) but fix is missing closers,
        // append them.  Only act when the fix is *more unbalanced* than original.
        if (fix.braces > orig.braces) {
            for (let i = 0; i < fix.braces - orig.braces; i++)
                append.push('}');
        }
        if (fix.parens > orig.parens) {
            for (let i = 0; i < fix.parens - orig.parens; i++)
                append.push(')');
        }
        if (fix.brackets > orig.brackets) {
            for (let i = 0; i < fix.brackets - orig.brackets; i++)
                append.push(']');
        }
        if (append.length > 0) {
            return fixed.trimEnd() + '\n' + append.join('\n');
        }
        return fixed;
    }
    // Model resolution - maps Vigthoria model names to internal IDs
    // INTERNAL USE ONLY - users see only Vigthoria branding
    resolveModelId(shortName) {
        const modelMap = {
            // ═══════════════════════════════════════════════════════════════
            // Vigthoria server infrastructure models
            // ═══════════════════════════════════════════════════════════════
            'fast': 'vigthoria-v3-balanced-4b',
            'mini': 'vigthoria-mini-0.6b',
            'balanced': 'vigthoria-v3-balanced-4b',
            'balanced-4b': 'vigthoria-v3-balanced-4b',
            'creative': 'vigthoria-creative-9b-v4',
            // Code Models - 35B is the default powerhouse
            'code': 'vigthoria-v3-code-35b', // Vigthoria server infrastructure 35B model
            'code-30b': 'vigthoria-v3-code-35b',
            'code-35b': 'vigthoria-v3-code-35b',
            'code-8b': 'vigthoria-v3-code-9b',
            'code-9b': 'vigthoria-v3-code-9b',
            'pro': 'vigthoria-v3-code-35b',
            'agent': 'vigthoria-v3-code-35b',
            'vigthoria-code': 'vigthoria-v3-code-35b',
            'vigthoria-agent': 'vigthoria-v3-code-35b',
            // ═══════════════════════════════════════════════════════════════
            // VIGTHORIA CLOUD - current billing catalog aliases
            // ═══════════════════════════════════════════════════════════════
            'cloud': 'vigthoria-cloud-balanced',
            'cloud-code': 'vigthoria-cloud-code',
            'cloud-balanced': 'vigthoria-cloud-balanced',
            'cloud-fast': 'vigthoria-cloud-fast',
            'cloud-power': 'vigthoria-cloud-power',
            'cloud-maximum': 'vigthoria-cloud-maximum',
            'cloud-reason': 'vigthoria-cloud-power',
            'ultra': 'vigthoria-cloud-maximum',
            // Legacy aliases kept for backward compatibility
            'cloud-pro': 'vigthoria-cloud-power',
            'cloud-k2': 'vigthoria-cloud-power',
            'cloud-ultra': 'vigthoria-cloud-maximum',
        };
        // If already a full model ID, return as-is
        if (shortName.includes('vigthoria') || shortName.includes('/') || shortName.includes(':')) {
            if (modelMap[shortName]) {
                return modelMap[shortName];
            }
            return shortName;
        }
        return modelMap[shortName] || 'vigthoria-v3-code-35b';
    }
    async getCoderHealth() {
        try {
            const response = await this.client.get('/api/health', { timeout: 5000 });
            const ok = response.data?.status === 'ok' || response.data?.healthy === true;
            return {
                name: 'Coder API',
                endpoint: `${this.config.get('apiUrl')}/api/health`,
                ok,
                details: { health: response.data },
            };
        }
        catch (error) {
            return {
                name: 'Coder API',
                endpoint: `${this.config.get('apiUrl')}/api/health`,
                ok: false,
                error: error.message,
            };
        }
    }
    /**
     * Fast preflight for local agent loop — probes model backends in parallel
     * so users see a clear pass/fail before "Planning..." hangs on slow routes.
     */
    async runChatModelPreflight(requestedModel = 'agent') {
        const routes = [];
        const probes = [
            {
                name: 'Vigthoria Coder API',
                run: async () => {
                    const health = await this.getCoderHealth();
                    return { ok: health.ok, error: health.error };
                },
            },
            {
                name: 'Vigthoria Models API',
                run: async () => {
                    const health = await this.getModelsHealth();
                    return { ok: health.ok, error: health.error };
                },
            },
        ];
        const selfHostedHealth = await this.getSelfHostedHealth();
        if (selfHostedHealth) {
            const sh = selfHostedHealth;
            probes.unshift({
                name: sh.name || 'Dedicated Models API',
                run: async () => ({ ok: sh.ok, error: sh.error }),
            });
        }
        if (this.isSelfHostedPreferredModel(this.resolveModelId(requestedModel), requestedModel)) {
            // Agent/code models should also accept V3 health as a usable inference path.
            probes.push({
                name: 'V3 Agent API',
                run: async () => {
                    const v3 = await this.runV3HealthCheck();
                    return { ok: v3.healthy, error: v3.error };
                },
            });
        }
        const results = await Promise.all(probes.map(async (probe) => {
            try {
                const result = await probe.run();
                routes.push({ name: probe.name, ok: result.ok, error: result.error });
                return result.ok;
            }
            catch (error) {
                const message = error?.message || String(error);
                routes.push({ name: probe.name, ok: false, error: message });
                return false;
            }
        }));
        const healthy = results.some(Boolean);
        const inferenceRoutes = routes.filter((route) => route.name !== 'V3 Agent API');
        const firstHealthy = inferenceRoutes.find((route) => route.ok) || routes.find((route) => route.ok);
        return {
            healthy,
            endpoint: firstHealthy?.name || 'api.vigthoria.io',
            error: healthy
                ? undefined
                : 'No Vigthoria model backend responded during preflight. Run `vigthoria login` and check your internet connection.',
            routes,
        };
    }
    mapPreflightEndpointToRoute(endpoint) {
        const normalized = String(endpoint || '').toLowerCase();
        if (normalized.includes('coder'))
            return 'coder';
        if (normalized.includes('models'))
            return 'models';
        if (normalized.includes('dedicated') || normalized.includes('self-hosted') || normalized.includes('selfhosted')) {
            return 'selfhosted';
        }
        return null;
    }
    getLastChatTransportErrors() {
        return this.lastChatTransportErrors;
    }
    getClientToolErrors() {
        return [...this.clientToolErrors];
    }
    clearAgentRunDiagnostics() {
        this.clientToolErrors = [];
        this.pendingV3ClientToolTasks.clear();
    }
    recordClientToolError(message) {
        const trimmed = sanitizeUserFacingErrorText(String(message || '')).slice(0, 280);
        if (trimmed && !this.clientToolErrors.includes(trimmed)) {
            this.clientToolErrors.push(trimmed);
        }
    }
    async submitClientToolResult(contextId, callId, result, backendUrl) {
        const trimmedContextId = String(contextId || '').trim();
        const trimmedCallId = String(callId || '').trim();
        if (!trimmedContextId || !trimmedCallId) {
            return;
        }
        const baseUrl = String(backendUrl || this.getV3AgentBaseUrls()[0] || '').replace(/\/$/, '');
        if (!baseUrl) {
            return;
        }
        const headers = await this.getV3AgentHeaders();
        const endpoint = this.isDirectV3AgentBaseUrl(baseUrl)
            ? `${baseUrl}/api/agent/client-tool-result`
            : `${baseUrl}/api/v3-agent/client-tool-result`;
        const body = JSON.stringify({
            context_id: trimmedContextId,
            call_id: trimmedCallId,
            success: result.success === true,
            output: String(result.output || ''),
            error: String(result.error || ''),
        });
        for (let attempt = 0; attempt < 3; attempt += 1) {
            const response = await fetch(endpoint, {
                method: 'POST',
                headers: { ...headers, 'Content-Type': 'application/json' },
                body,
            });
            if (response.ok) {
                return;
            }
            const errorText = await response.text().catch(() => '');
            const isPendingRace = response.status === 404 && /no pending client tool call/i.test(errorText);
            if (isPendingRace && attempt < 2) {
                await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
                continue;
            }
            if (response.status === 404 && /no pending client tool call/i.test(errorText)) {
                // Server already consumed or timed out this call — treat as idempotent when local work succeeded.
                return;
            }
            throw new Error(`Client tool result rejected (${response.status}): ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
        }
    }
    isHealthyServicePayload(payload) {
        const status = String(payload?.status || '').toLowerCase();
        return payload?.healthy === true
            || status === 'healthy'
            || status === 'ok'
            || status === 'ready'
            || status === 'degraded';
    }
    extractModelCount(payload) {
        if (Array.isArray(payload?.data))
            return payload.data.length;
        if (Array.isArray(payload?.models))
            return payload.models.length;
        if (Array.isArray(payload?.model_list))
            return payload.model_list.length;
        if (typeof payload?.total === 'number')
            return payload.total;
        return 0;
    }
    async probeModelList(client) {
        const candidates = ['/v1/models', '/models', '/api/models', '/api/tags'];
        for (const endpoint of candidates) {
            try {
                const response = await client.get(endpoint, { timeout: 5000 });
                const modelCount = this.extractModelCount(response.data);
                if (modelCount > 0) {
                    return { modelCount, endpoint };
                }
            }
            catch {
                // try next endpoint
            }
        }
        return { modelCount: 0, endpoint: candidates[0] };
    }
    async getModelsHealth() {
        const modelsApiUrl = this.config.get('modelsApiUrl');
        try {
            const [healthResponse, modelProbe] = await Promise.all([
                this.modelRouterClient.get('/health', { timeout: 5000 }),
                this.probeModelList(this.modelRouterClient),
            ]);
            const healthOk = this.isHealthyServicePayload(healthResponse.data);
            const modelCount = modelProbe.modelCount;
            const ok = healthOk || modelCount > 0;
            return {
                name: 'Models API',
                endpoint: `${modelsApiUrl}/health`,
                ok,
                details: {
                    health: healthResponse.data,
                    modelCount,
                    modelsEndpoint: modelProbe.endpoint,
                },
            };
        }
        catch (error) {
            return {
                name: 'Models API',
                endpoint: `${modelsApiUrl}/health`,
                ok: false,
                error: error.message,
            };
        }
    }
    async getSelfHostedHealth() {
        const selfHostedModelsApiUrl = this.getSelfHostedModelsApiUrl();
        if (!selfHostedModelsApiUrl || !this.selfHostedModelRouterClient) {
            return null;
        }
        try {
            const [healthResponse, modelProbe] = await Promise.all([
                this.selfHostedModelRouterClient.get('/health', { timeout: 5000 }),
                this.probeModelList(this.selfHostedModelRouterClient),
            ]);
            const healthOk = this.isHealthyServicePayload(healthResponse.data);
            const ok = healthOk || modelProbe.modelCount > 0;
            return {
                name: 'Vigthoria Models API',
                endpoint: `${selfHostedModelsApiUrl}/health`,
                ok,
                details: {
                    health: healthResponse.data,
                    modelCount: modelProbe.modelCount,
                    modelsEndpoint: modelProbe.endpoint,
                },
            };
        }
        catch (error) {
            return {
                name: 'Vigthoria Models API',
                endpoint: `${selfHostedModelsApiUrl}/health`,
                ok: false,
                error: error.message,
            };
        }
    }
    async getV3AgentHealth() {
        const baseUrl = this.getV3AgentBaseUrls()[0];
        // Try multiple health endpoint patterns — the V3 backend may expose
        // different paths depending on whether it's local (8030) or remote.
        const candidates = [
            `${baseUrl}/api/v3-agent/health`,
            `${baseUrl}/api/health`,
            `${baseUrl}/health`,
        ];
        const headers = await this.getV3AgentHeaders();
        for (const endpoint of candidates) {
            try {
                const controller = new AbortController();
                const timer = setTimeout(() => controller.abort(), 3000);
                const response = await fetch(endpoint, {
                    method: 'GET',
                    headers,
                    signal: controller.signal,
                });
                clearTimeout(timer);
                if (response.ok) {
                    const data = await response.json().catch(() => ({}));
                    return {
                        name: 'V3 Agent',
                        endpoint,
                        ok: true,
                        details: { health: data },
                    };
                }
                // 404 means this path doesn't exist — try next candidate
                if (response.status === 404)
                    continue;
                // 405 Method Not Allowed — endpoint exists but rejects GET.
                // Treat as reachable since the run endpoint (POST) will work.
                if (response.status === 405) {
                    return {
                        name: 'V3 Agent',
                        endpoint,
                        ok: true,
                        details: { health: { reachable: true, note: 'Health endpoint returned 405 but run endpoint is available' } },
                    };
                }
                // Other error
                throw new Error(`V3 health ${response.status}`);
            }
            catch (error) {
                if (error instanceof Error && error.message.startsWith('V3 health')) {
                    return {
                        name: 'V3 Agent',
                        endpoint,
                        ok: false,
                        error: error.message,
                    };
                }
                // Network/timeout error — try next candidate
            }
        }
        // Last resort: probe the run endpoint with OPTIONS
        const runUrl = this.getV3AgentRunUrl(baseUrl);
        try {
            const controller = new AbortController();
            const timer = setTimeout(() => controller.abort(), 2000);
            const probe = await fetch(runUrl, { method: 'OPTIONS', headers, signal: controller.signal });
            clearTimeout(timer);
            if (probe.ok || probe.status === 204 || probe.status === 405) {
                return {
                    name: 'V3 Agent',
                    endpoint: runUrl,
                    ok: true,
                    details: { health: { reachable: true, method: 'OPTIONS' } },
                };
            }
        }
        catch {
            // final attempt failed
        }
        return {
            name: 'V3 Agent',
            endpoint: candidates[0],
            ok: false,
            error: 'No V3 agent health endpoint responded.',
        };
    }
    async getHyperLoopHealth() {
        const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
        const endpoint = process.env.VIGTHORIA_HYPERLOOP_URL || `${configuredApiUrl}/api/hyperloop/health`;
        try {
            const token = this.getAccessToken();
            const response = await fetchWithServiceTimeout(endpoint, {
                method: 'GET',
                headers: token ? { Authorization: `Bearer ${token}` } : undefined,
            });
            if (!response.ok) {
                throw new Error(`Hyper Loop health ${response.status}`);
            }
            const data = await response.json();
            const ok = data?.status === 'healthy' || data?.status === 'ok' || data?.healthy === true;
            return {
                name: 'Hyper Loop',
                endpoint,
                ok,
                details: { health: data },
            };
        }
        catch (error) {
            return {
                name: 'Hyper Loop',
                endpoint,
                ok: false,
                error: error instanceof Error ? error.message : String(error),
            };
        }
    }
    async getRepoMemoryHealth(context = {}) {
        const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
        const endpoint = process.env.VIGTHORIA_HYPERLOOP_EXECUTE_URL || `${configuredApiUrl}/api/hyperloop/execute`;
        const modulesEndpoint = process.env.VIGTHORIA_HYPERLOOP_MODULES_URL || `${configuredApiUrl}/api/hyperloop/modules`;
        const token = this.getAccessToken();
        const projectPath = this.resolveAgentTargetPath(context);
        try {
            const modulesResponse = await fetchWithServiceTimeout(modulesEndpoint, {
                method: 'GET',
                headers: {
                    ...(token ? { Authorization: `Bearer ${token}` } : {}),
                },
            });
            if (!modulesResponse.ok) {
                throw new Error(`Repo memory modules ${modulesResponse.status}`);
            }
            const modulesData = await modulesResponse.json();
            const modules = Array.isArray(modulesData?.modules) ? modulesData.modules : [];
            const compactor = modules.find((entry) => entry && entry.name === 'repo_context_compactor');
            let compactContextLength = 0;
            try {
                const probeResponse = await fetchWithServiceTimeout(endpoint, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        ...(token ? { Authorization: `Bearer ${token}` } : {}),
                    },
                    body: JSON.stringify({
                        module: 'repo_context_compactor',
                        payload: {
                            path: projectPath,
                            request_focus: 'Capability truth status probe',
                            max_files: 20,
                            max_chars: 800,
                        },
                        context: {
                            project_path: projectPath,
                            source: 'vigthoria-cli-capability-truth',
                        },
                    }),
                });
                if (probeResponse.ok) {
                    const probeData = await probeResponse.json();
                    compactContextLength = String(probeData?.result?.compact_context || '').length;
                }
            }
            catch {
                // Availability is determined by the module registry; direct compactor output is supplemental only.
            }
            const ok = Boolean(compactor && compactor.initialized !== false);
            return {
                name: 'Repo Memory',
                endpoint: modulesEndpoint,
                ok,
                details: {
                    projectPath,
                    compactContextLength,
                    moduleRegistered: Boolean(compactor),
                },
                ...(ok ? {} : { error: 'repo_context_compactor is not registered in Hyper Loop' }),
            };
        }
        catch (error) {
            return {
                name: 'Repo Memory',
                endpoint: modulesEndpoint,
                ok: false,
                error: error instanceof Error ? error.message : String(error),
            };
        }
    }
    async runSelfHealingCycle(_originalPrompt, _workspacePath, _context = {}) {
        return {
            healingAttempted: false,
            passed: true,
            tool: 'disabled',
        };
    }
    async attemptV3ServiceRecovery(reason = '', _options = {}) {
        const safeReason = sanitizeUserFacingErrorText(reason || 'unknown failure');
        const isTransient = /timed out|aborted|econnreset|fetch failed|network|503|502|504|socket hang up/i.test(safeReason);
        if (!isTransient) {
            return {
                recovered: false,
                message: safeReason ? `Recovery unavailable: ${safeReason}` : 'Recovery unavailable',
            };
        }
        const health = await this.runV3HealthCheck();
        if (!health.healthy) {
            return {
                recovered: false,
                message: health.error || 'V3 service is still unreachable',
            };
        }
        return {
            recovered: true,
            message: 'V3 service is reachable again — retrying the workflow once.',
        };
    }
    async getDevtoolsBridgeStatus() {
        const host = process.env.VIGTHORIA_DEVTOOLS_BRIDGE_HOST || '127.0.0.1';
        const port = Number.parseInt(process.env.VIGTHORIA_DEVTOOLS_BRIDGE_PORT || '4016', 10);
        const endpoint = `ws://${host}:${port}/ws`;
        const healthEndpoint = `http://${host}:${port}/health`;
        try {
            const controller = new AbortController();
            const timer = setTimeout(() => controller.abort(), 1500);
            const response = await fetch(healthEndpoint, {
                signal: controller.signal,
                headers: { Accept: 'application/json' },
            });
            clearTimeout(timer);
            if (response.ok) {
                return {
                    name: 'DevTools Bridge',
                    endpoint,
                    ok: true,
                    details: { host, port, healthEndpoint },
                };
            }
            return {
                name: 'DevTools Bridge',
                endpoint,
                ok: false,
                error: `HTTP ${response.status}`,
            };
        }
        catch {
            // Fall back to a raw TCP probe for older bridge builds without /health.
        }
        return new Promise((resolve) => {
            const socket = net.connect({ host, port, timeout: 1500 }, () => {
                socket.end();
                resolve({
                    name: 'DevTools Bridge',
                    endpoint,
                    ok: true,
                    details: { host, port },
                });
            });
            socket.on('timeout', () => {
                socket.destroy();
                resolve({
                    name: 'DevTools Bridge',
                    endpoint,
                    ok: false,
                    error: 'Connection timed out',
                });
            });
            socket.on('error', (error) => {
                resolve({
                    name: 'DevTools Bridge',
                    endpoint,
                    ok: false,
                    error: error.message,
                });
            });
        });
    }
    async getCapabilityTruthStatus(context = {}) {
        // Wrap each probe with its own 6 s timeout so they always resolve
        // before the outer 8 s race in auth.ts, producing real error messages
        // (ECONNREFUSED, 404, etc.) instead of the generic "Timed out (8s)".
        const withTimeout = (p, name) => Promise.race([
            p,
            new Promise(resolve => setTimeout(() => resolve({ name, endpoint: '', ok: false, error: 'Service not reachable (6 s timeout)' }), 6000)),
        ]);
        const [v3Agent, hyperLoop, repoMemory, devtoolsBridge] = await Promise.all([
            withTimeout(this.getV3AgentHealth(), 'V3 Agent'),
            withTimeout(this.getHyperLoopHealth(), 'Hyper Loop'),
            withTimeout(this.getRepoMemoryHealth(context), 'Repo Memory'),
            withTimeout(this.getDevtoolsBridgeStatus(), 'DevTools Bridge'),
        ]);
        return {
            overallOk: v3Agent.ok && (hyperLoop.ok || repoMemory.ok),
            v3Agent,
            hyperLoop,
            repoMemory,
            devtoolsBridge,
        };
    }
    async getHealthStatus() {
        const [coder, models, selfHosted] = await Promise.all([
            this.getCoderHealth(),
            this.getModelsHealth(),
            this.getSelfHostedHealth(),
        ]);
        return {
            overallOk: coder.ok && models.ok,
            coder,
            models,
            selfHosted,
        };
    }
    // Health check
    async healthCheck() {
        const status = await this.getHealthStatus();
        return status.overallOk;
    }
}
