"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.AudioTranscribeTool = exports.ImageAnalyzeTool = void 0;
const child_process_1 = require("child_process");
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const ollama_1 = require("ollama");
const visionModels_1 = require("../models/visionModels");
const pathResolution_1 = require("./pathResolution");
const MAX_IMAGE_BYTES = 10_000_000;
const MAX_TRANSCRIPT_CHARS = 50_000;
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp']);
exports.ImageAnalyzeTool = {
    name: 'image_analyze',
    description: 'Analyze a local image with an Ollama vision-capable model. Use this for attached image files.',
    parameters: {
        type: 'object',
        properties: {
            path: { type: 'string', description: 'Absolute or project-relative path to an image file' },
            prompt: { type: 'string', description: 'Question or instruction for the image analysis' },
            model: { type: 'string', description: 'Vision-capable Ollama model to use, for example llava or qwen2-vl' },
            host: { type: 'string', description: 'Optional Ollama host URL' },
        },
        required: ['path'],
    },
    isReadOnly: true,
    async execute(input) {
        const filePath = (0, pathResolution_1.resolveProjectReadPath)(input.path);
        if (!filePath)
            return { success: false, output: 'Path is outside the project directory', error: 'path outside project' };
        if (!isImagePath(filePath))
            return { success: false, output: 'File does not look like a supported image type.', error: 'unsupported image type' };
        try {
            const stat = await fs.stat(filePath);
            if (stat.size > MAX_IMAGE_BYTES) {
                return { success: false, output: `Image exceeds ${MAX_IMAGE_BYTES} bytes.`, error: 'image too large' };
            }
            const image = await fs.readFile(filePath);
            const client = new ollama_1.Ollama({ host: sanitizeString(input.host) || process.env.OLLAMA_HOST || 'http://localhost:11434' });
            const model = await resolveVisionModel(input, client);
            if (!model) {
                return { success: false, output: 'No vision model was provided and no installed Ollama vision model could be auto-detected. Set HARNESS_VISION_MODEL to a model such as llava:latest.', error: 'missing vision model' };
            }
            const response = await client.chat({
                model,
                stream: false,
                messages: [{
                        role: 'user',
                        content: sanitizeString(input.prompt) || 'Describe this image and call out details relevant to the user request.',
                        images: [image.toString('base64')],
                    }],
            });
            return { success: true, output: response.message?.content || '(empty image analysis response)' };
        }
        catch (error) {
            const msg = error instanceof Error ? error.message : String(error);
            return { success: false, output: `Image analysis failed: ${msg}`, error: msg };
        }
    },
};
async function resolveVisionModel(input, client) {
    const configuredModel = sanitizeString(input.model) || process.env.HARNESS_VISION_MODEL || '';
    // List installed models once — used both as a validity check on the
    // configured/selected model AND as a fallback source when nothing
    // explicit is set. Without this validation, a configured model that
    // isn't actually installed (e.g. settings carries `qwen2-vl` but only
    // `llava` is pulled) sends every image_analyze call to its death.
    let installed = [];
    try {
        const response = await client.list();
        installed = response.models.map((model) => model.name);
    }
    catch {
        // If we can't list, fall back to the configured value as a best-effort.
        if (configuredModel)
            return configuredModel;
        return '';
    }
    const isInstalled = (name) => {
        if (!name)
            return false;
        if (installed.includes(name))
            return true;
        // Ollama tags models as "name:tag"; accept a configured bare name
        // when any installed model shares the same prefix.
        const bare = name.split(':')[0];
        return installed.some((entry) => entry === bare || entry.startsWith(`${bare}:`));
    };
    if (configuredModel && isInstalled(configuredModel))
        return configuredModel;
    const selectedModel = sanitizeString(process.env.OLLAMA_MODEL);
    if (selectedModel && (0, visionModels_1.isVisionCapableModelName)(selectedModel) && isInstalled(selectedModel))
        return selectedModel;
    // Configured/selected model wasn't installed. Auto-fall-back to whichever
    // vision-capable model IS installed so the call succeeds instead of
    // looping with `model not found`.
    return (0, visionModels_1.findInstalledVisionModel)(installed) ?? '';
}
exports.AudioTranscribeTool = {
    name: 'audio_transcribe',
    description: 'Transcribe a local audio file using HARNESS_AUDIO_TRANSCRIBE_COMMAND. Use this for attached audio files before analysis.',
    parameters: {
        type: 'object',
        properties: {
            path: { type: 'string', description: 'Absolute or project-relative path to an audio file' },
        },
        required: ['path'],
    },
    isReadOnly: false,
    async execute(input) {
        const filePath = (0, pathResolution_1.resolveProjectReadPath)(input.path);
        if (!filePath)
            return { success: false, output: 'Path is outside the project directory', error: 'path outside project' };
        const commandTemplate = process.env.HARNESS_AUDIO_TRANSCRIBE_COMMAND;
        if (!commandTemplate) {
            return {
                success: false,
                output: 'No audio transcription command is configured. Set HARNESS_AUDIO_TRANSCRIBE_COMMAND with {input}, for example: whisper {input} --model base --output_format txt --output_dir -',
                error: 'missing transcription command',
            };
        }
        try {
            await fs.stat(filePath);
            const { command, args } = buildCommand(commandTemplate, filePath);
            const transcript = await runTranscriptCommand(command, args);
            const output = transcript.length > MAX_TRANSCRIPT_CHARS
                ? transcript.slice(0, MAX_TRANSCRIPT_CHARS) + '\n...(truncated)'
                : transcript;
            return { success: true, output: output || '(empty transcript)' };
        }
        catch (error) {
            const msg = error instanceof Error ? error.message : String(error);
            return { success: false, output: `Audio transcription failed: ${msg}`, error: msg };
        }
    },
};
function isImagePath(filePath) {
    return IMAGE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
}
function sanitizeString(value) {
    return String(value ?? '').trim().slice(0, 2000);
}
function buildCommand(template, inputPath) {
    const parts = splitCommandLine(template.replaceAll('{input}', inputPath));
    if (parts.length === 0)
        throw new Error('Transcription command is empty');
    return { command: parts[0], args: parts.slice(1) };
}
function splitCommandLine(commandLine) {
    const parts = [];
    let current = '';
    let quote = null;
    for (let index = 0; index < commandLine.length; index++) {
        const char = commandLine[index];
        if ((char === '"' || char === "'") && !quote) {
            quote = char;
            continue;
        }
        if (char === quote) {
            quote = null;
            continue;
        }
        if (/\s/.test(char) && !quote) {
            if (current) {
                parts.push(current);
                current = '';
            }
            continue;
        }
        current += char;
    }
    if (current)
        parts.push(current);
    return parts;
}
function runTranscriptCommand(command, args) {
    return new Promise((resolve, reject) => {
        (0, child_process_1.execFile)(command, args, { timeout: 120_000, maxBuffer: MAX_TRANSCRIPT_CHARS * 4 }, (error, stdout, stderr) => {
            if (error) {
                reject(new Error(stderr?.trim() || error.message));
                return;
            }
            resolve(stdout.trim() || stderr.trim());
        });
    });
}
//# sourceMappingURL=multimodalTools.js.map