/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { Kind, BaseTool, BaseToolInvocation, } from './tools.js';
import { spawn } from 'node:child_process';
import { StringDecoder } from 'node:string_decoder';
import { connectAndDiscover } from './mcp-client.js';
import { McpClientManager } from './mcp-client-manager.js';
import { DiscoveredMCPTool } from './mcp-tool.js';
import { parse } from 'shell-quote';
import { ToolErrorType } from './tool-error.js';
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
import { DebugLogger } from '../debug/index.js';
import { normalizeToolName } from './toolNameUtils.js';
export class DiscoveredTool extends BaseTool {
    config;
    description;
    parameterSchema;
    constructor(config, name, description, parameterSchema) {
        const discoveryCmd = config.getToolDiscoveryCommand();
        const callCommand = config.getToolCallCommand();
        description += `

This tool was discovered from the project by executing the command \`${discoveryCmd}\` on project root.
When called, this tool will execute the command \`${callCommand} ${name}\` on project root.
Tool discovery and call commands can be configured in project or user settings.

When called, the tool call command is executed as a subprocess.
On success, tool output is returned as a json string.
Otherwise, the following information is returned:

Stdout: Output on stdout stream. Can be \`(empty)\` or partial.
Stderr: Output on stderr stream. Can be \`(empty)\` or partial.
Error: Error or \`(none)\` if no error was reported for the subprocess.
Exit Code: Exit code or \`(none)\` if terminated by signal.
Signal: Signal number or \`(none)\` if no signal was received.
`;
        super(name, name, description, Kind.Other, parameterSchema, false, // isOutputMarkdown
        false);
        this.config = config;
        this.description = description;
        this.parameterSchema = parameterSchema;
    }
    build(params) {
        return new DiscoveredToolInvocation(this, params, this.messageBus);
    }
    async execute(params, _signal, _updateOutput) {
        const callCommand = this.config.getToolCallCommand();
        const child = spawn(callCommand, [this.name]);
        child.stdin.write(JSON.stringify(params));
        child.stdin.end();
        let stdout = '';
        let stderr = '';
        let error = null;
        let code = null;
        let signal = null;
        await new Promise((resolve) => {
            const onStdout = (data) => {
                stdout += data?.toString();
            };
            const onStderr = (data) => {
                stderr += data?.toString();
            };
            const onError = (err) => {
                error = err;
            };
            const onClose = (_code, _signal) => {
                code = _code;
                signal = _signal;
                cleanup();
                resolve();
            };
            const cleanup = () => {
                child.stdout.removeListener('data', onStdout);
                child.stderr.removeListener('data', onStderr);
                child.removeListener('error', onError);
                child.removeListener('close', onClose);
                if (child.connected) {
                    child.disconnect();
                }
            };
            child.stdout.on('data', onStdout);
            child.stderr.on('data', onStderr);
            child.on('error', onError);
            child.on('close', onClose);
        });
        // if there is any error, non-zero exit code, signal, or stderr, return error details instead of stdout
        if (error || code !== 0 || signal || stderr) {
            const llmContent = [
                `Stdout: ${stdout || '(empty)'}`,
                `Stderr: ${stderr || '(empty)'}`,
                `Error: ${error ?? '(none)'}`,
                `Exit Code: ${code ?? '(none)'}`,
                `Signal: ${signal ?? '(none)'}`,
            ].join('\n');
            return {
                llmContent,
                returnDisplay: llmContent,
                error: {
                    message: llmContent,
                    type: ToolErrorType.DISCOVERED_TOOL_EXECUTION_ERROR,
                },
            };
        }
        return {
            llmContent: stdout,
            returnDisplay: stdout,
        };
    }
}
class DiscoveredToolInvocation extends BaseToolInvocation {
    tool;
    constructor(tool, params, messageBus) {
        super(params, messageBus);
        this.tool = tool;
    }
    getDescription() {
        return safeJsonStringify(this.params);
    }
    async execute(signal, updateOutput) {
        return this.tool.execute(this.params, signal, updateOutput);
    }
}
export class ToolRegistry {
    tools = new Map();
    config;
    mcpClientManager;
    logger = new DebugLogger('llxprt:tool-registry');
    discoveryLock = null;
    constructor(config, eventEmitter) {
        this.config = config;
        this.mcpClientManager = new McpClientManager(this.config.getMcpServers() ?? {}, this.config.getMcpServerCommand(), this, this.config.getPromptRegistry(), this.config.getDebugMode(), this.config.getWorkspaceContext(), eventEmitter);
    }
    /**
     * Sets the message bus for the registry and all registered tools.
     * Message bus wiring now happens unconditionally via Config, so this is kept
     * only for API completeness/future overrides.
     */
    setMessageBus() {
        // Message bus is accessed from config, so this method is currently a no-op.
        // Tools access the message bus via config.getMessageBus() when needed.
        // This method exists for API completeness and future extensibility.
    }
    getToolGovernance() {
        const ephemerals = typeof this.config.getEphemeralSettings === 'function'
            ? this.config.getEphemeralSettings() || {}
            : {};
        const allowedRaw = Array.isArray(ephemerals['tools.allowed'])
            ? ephemerals['tools.allowed']
            : [];
        const disabledRaw = Array.isArray(ephemerals['tools.disabled'])
            ? ephemerals['tools.disabled']
            : Array.isArray(ephemerals['disabled-tools'])
                ? ephemerals['disabled-tools']
                : [];
        const excludedRaw = this.config.getExcludeTools?.() ?? [];
        return {
            allowed: new Set(allowedRaw.map((name) => normalizeToolName(name) || name)),
            disabled: new Set(disabledRaw.map((name) => normalizeToolName(name) || name)),
            excluded: new Set(excludedRaw.map((name) => normalizeToolName(name) || name)),
        };
    }
    isToolActive(toolName, governance) {
        const canonical = normalizeToolName(toolName) || toolName;
        if (governance.excluded.has(canonical)) {
            return false;
        }
        if (governance.disabled.has(canonical)) {
            return false;
        }
        if (governance.allowed.size > 0 && !governance.allowed.has(canonical)) {
            return false;
        }
        return true;
    }
    /**
     * Registers a tool definition.
     * @param tool - The tool object containing schema and execution logic.
     */
    registerTool(tool) {
        this.registerToolIntoMap(tool, this.tools);
    }
    /**
     * Builds a new tool map with only non-discovered tools (core tools).
     * This is used for atomic updates to avoid race conditions.
     */
    buildCoreToolsMap() {
        const coreTools = new Map();
        for (const [name, tool] of this.tools.entries()) {
            if (!(tool instanceof DiscoveredTool || tool instanceof DiscoveredMCPTool)) {
                coreTools.set(name, tool);
            }
        }
        return coreTools;
    }
    /**
     * Removes all tools from a specific MCP server.
     * @param serverName The name of the server to remove tools from.
     */
    removeMcpToolsByServer(serverName) {
        for (const [name, tool] of this.tools.entries()) {
            if (tool instanceof DiscoveredMCPTool && tool.serverName === serverName) {
                this.tools.delete(name);
            }
        }
    }
    /**
     * Discovers tools from project (if available and configured).
     * Can be called multiple times to update discovered tools.
     * This will discover tools from the command line and from MCP servers.
     * Uses truly atomic updates to prevent race conditions.
     */
    async discoverAllTools() {
        await this.withDiscoveryLock(async () => {
            const newTools = this.buildCoreToolsMap();
            await this.config.getPromptRegistry().clear();
            await this.discoverAndRegisterToolsFromCommand(newTools);
            const previousTools = this.tools;
            try {
                this.tools = newTools;
                await this.mcpClientManager.discoverAllMcpTools(this.config);
            }
            catch (error) {
                this.tools = previousTools;
                throw error;
            }
        });
    }
    /**
     * Discovers tools from project (if available and configured).
     * Can be called multiple times to update discovered tools.
     * This will NOT discover tools from the command line, only from MCP servers.
     * Uses truly atomic updates to prevent race conditions.
     */
    async discoverMcpTools() {
        await this.withDiscoveryLock(async () => {
            const newTools = this.buildCoreToolsMap();
            for (const tool of this.tools.values()) {
                if (tool instanceof DiscoveredTool &&
                    !(tool instanceof DiscoveredMCPTool)) {
                    this.registerToolIntoMap(tool, newTools);
                }
            }
            await this.config.getPromptRegistry().clear();
            const previousTools = this.tools;
            try {
                this.tools = newTools;
                await this.mcpClientManager.discoverAllMcpTools(this.config);
            }
            catch (error) {
                this.tools = previousTools;
                throw error;
            }
        });
    }
    /**
     * Restarts all MCP servers and re-discovers tools.
     */
    async restartMcpServers() {
        await this.discoverMcpTools();
    }
    /**
     * Discover or re-discover tools for a single MCP server.
     * Uses truly atomic updates to prevent race conditions.
     * @param serverName - The name of the server to discover tools from.
     */
    async discoverToolsForServer(serverName) {
        await this.withDiscoveryLock(async () => {
            const newTools = new Map();
            for (const tool of this.tools.values()) {
                if (!(tool instanceof DiscoveredMCPTool && tool.serverName === serverName)) {
                    this.registerToolIntoMap(tool, newTools);
                }
            }
            await this.config.getPromptRegistry().removePromptsByServer(serverName);
            const mcpServers = this.config.getMcpServers() ?? {};
            const serverConfig = mcpServers[serverName];
            const previousTools = this.tools;
            try {
                this.tools = newTools;
                if (serverConfig) {
                    const tempRegistry = Object.create(this);
                    tempRegistry.registerTool = (tool) => {
                        this.registerToolIntoMap(tool, newTools);
                    };
                    await connectAndDiscover(serverName, serverConfig, tempRegistry, this.config.getPromptRegistry(), this.config.getDebugMode(), this.config.getWorkspaceContext(), this.config);
                }
            }
            catch (error) {
                this.tools = previousTools;
                throw error;
            }
        });
    }
    async discoverAndRegisterToolsFromCommand(targetMap) {
        const discoveryCmd = this.config.getToolDiscoveryCommand();
        if (!discoveryCmd) {
            return;
        }
        try {
            const cmdParts = parse(discoveryCmd);
            if (cmdParts.length === 0) {
                throw new Error('Tool discovery command is empty or contains only whitespace.');
            }
            const proc = spawn(cmdParts[0], cmdParts.slice(1));
            let stdout = '';
            const stdoutDecoder = new StringDecoder('utf8');
            let stderr = '';
            const stderrDecoder = new StringDecoder('utf8');
            let sizeLimitExceeded = false;
            const MAX_STDOUT_SIZE = 10 * 1024 * 1024; // 10MB limit
            const MAX_STDERR_SIZE = 10 * 1024 * 1024; // 10MB limit
            let stdoutByteLength = 0;
            let stderrByteLength = 0;
            proc.stdout.on('data', (data) => {
                if (sizeLimitExceeded)
                    return;
                if (stdoutByteLength + data.length > MAX_STDOUT_SIZE) {
                    sizeLimitExceeded = true;
                    proc.kill();
                    return;
                }
                stdoutByteLength += data.length;
                stdout += stdoutDecoder.write(data);
            });
            proc.stderr.on('data', (data) => {
                if (sizeLimitExceeded)
                    return;
                if (stderrByteLength + data.length > MAX_STDERR_SIZE) {
                    sizeLimitExceeded = true;
                    proc.kill();
                    return;
                }
                stderrByteLength += data.length;
                stderr += stderrDecoder.write(data);
            });
            await new Promise((resolve, reject) => {
                proc.on('error', reject);
                proc.on('close', (code) => {
                    stdout += stdoutDecoder.end();
                    stderr += stderrDecoder.end();
                    if (sizeLimitExceeded) {
                        return reject(new Error(`Tool discovery command output exceeded size limit of ${MAX_STDOUT_SIZE} bytes.`));
                    }
                    if (code !== 0) {
                        this.logger.error(() => `Command failed with code ${code}`);
                        this.logger.error(() => stderr);
                        return reject(new Error(`Tool discovery command failed with exit code ${code}`));
                    }
                    resolve();
                });
            });
            // execute discovery command and extract function declarations (w/ or w/o "tool" wrappers)
            const functions = [];
            const discoveredItems = JSON.parse(stdout.trim());
            if (!discoveredItems || !Array.isArray(discoveredItems)) {
                throw new Error('Tool discovery command did not return a JSON array of tools.');
            }
            for (const tool of discoveredItems) {
                if (tool && typeof tool === 'object') {
                    if (Array.isArray(tool['function_declarations'])) {
                        functions.push(...tool['function_declarations']);
                    }
                    else if (Array.isArray(tool['functionDeclarations'])) {
                        functions.push(...tool['functionDeclarations']);
                    }
                    else if (tool['name']) {
                        functions.push(tool);
                    }
                }
            }
            // register each function as a tool
            for (const func of functions) {
                if (!func.name) {
                    this.logger.warn(() => 'Discovered a tool with no name. Skipping.');
                    continue;
                }
                const parameters = func.parametersJsonSchema &&
                    typeof func.parametersJsonSchema === 'object' &&
                    !Array.isArray(func.parametersJsonSchema)
                    ? func.parametersJsonSchema
                    : {};
                this.registerToolIntoMap(new DiscoveredTool(this.config, `discovered_tool_${func.name}`, func.description ?? '', parameters), targetMap);
            }
        }
        catch (e) {
            this.logger.error(() => `Tool discovery command "${discoveryCmd}" failed:`, { error: e });
            throw e;
        }
    }
    /**
     * Retrieves the list of tool schemas (FunctionDeclaration array).
     * Extracts the declarations from the ToolListUnion structure.
     * Includes discovered (vs registered) tools if configured.
     * Filters out disabled tools based on ephemeral settings.
     * @returns An array of FunctionDeclarations.
     */
    getFunctionDeclarations() {
        const governance = this.getToolGovernance();
        const declarations = [];
        this.tools.forEach((tool) => {
            if (this.isToolActive(tool.name, governance)) {
                declarations.push(tool.schema);
            }
        });
        return declarations;
    }
    /**
     * Retrieves a filtered list of tool schemas based on a list of tool names.
     * @param toolNames - An array of tool names to include.
     * @returns An array of FunctionDeclarations for the specified tools.
     */
    getFunctionDeclarationsFiltered(toolNames) {
        const governance = this.getToolGovernance();
        const declarations = [];
        for (const name of toolNames) {
            const tool = this.tools.get(name);
            if (tool && this.isToolActive(tool.name, governance)) {
                declarations.push(tool.schema);
            }
        }
        return declarations;
    }
    /**
     * Returns an array of all registered and discovered tool names.
     */
    getAllToolNames() {
        return Array.from(this.tools.keys());
    }
    /**
     * Returns an array of all registered and discovered tool instances.
     */
    getAllTools() {
        const tools = Array.from(this.tools.values()).sort((a, b) => a.displayName.localeCompare(b.displayName));
        return tools;
    }
    /**
     * Returns an array of enabled tool instances (excludes disabled tools).
     */
    getEnabledTools() {
        const governance = this.getToolGovernance();
        return Array.from(this.tools.values())
            .filter((tool) => this.isToolActive(tool.name, governance))
            .sort((a, b) => a.displayName.localeCompare(b.displayName));
    }
    /**
     * Returns an array of tools registered from a specific MCP server.
     */
    getToolsByServer(serverName) {
        const serverTools = [];
        for (const tool of this.tools.values()) {
            if (tool?.serverName === serverName) {
                serverTools.push(tool);
            }
        }
        return serverTools.sort((a, b) => a.name.localeCompare(b.name));
    }
    /**
     * Get the definition of a specific tool.
     * @param name The name of the tool to retrieve
     * @param context Optional context to inject into the tool instance
     */
    getTool(name, context) {
        // Try original name first (most common case)
        let tool = this.tools.get(name);
        // If not found, try normalized name for fuzzy matching
        if (!tool) {
            const normalizedName = normalizeToolName(name);
            if (normalizedName && normalizedName !== name) {
                tool = this.tools.get(normalizedName);
            }
        }
        if (!tool) {
            return undefined;
        }
        const governance = this.getToolGovernance();
        if (!this.isToolActive(tool.name, governance)) {
            return undefined;
        }
        if (context) {
            // Inject context into tool instance
            if ('context' in tool) {
                tool.context = context;
            }
        }
        return tool;
    }
    registerToolIntoMap(tool, targetMap) {
        if (typeof tool
            .setMessageBus === 'function') {
            tool.setMessageBus(this.config.getMessageBus());
        }
        // Normalize the tool name for consistent storage and lookup
        const normalizedName = normalizeToolName(tool.name) || tool.name;
        if (targetMap.has(normalizedName)) {
            // For non-MCP tools, log warning and overwrite
            if (!(tool instanceof DiscoveredMCPTool)) {
                this.logger.warn(() => `Tool with name "${tool.name}" (normalized: "${normalizedName}") is already registered. Overwriting.`);
            }
            // For MCP tools, we assume they already have unique names from generateMcpToolName(serverName, toolName)
            // so we simply overwrite (this should not happen in normal operation)
        }
        // Store the tool with the normalized name for consistent lookup
        targetMap.set(normalizedName, tool);
    }
    async withDiscoveryLock(task) {
        while (this.discoveryLock) {
            await this.discoveryLock;
        }
        let release = () => { };
        this.discoveryLock = new Promise((resolve) => {
            release = () => resolve();
        });
        try {
            return await task();
        }
        finally {
            release();
            this.discoveryLock = null;
        }
    }
}
//# sourceMappingURL=tool-registry.js.map