/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import { type CommandContext, type SlashCommand, CommandKind, } from './types.js'; import { MessageType } from '../types.js'; import type { ToolInfo } from '@vybestack/llxprt-code-agents'; import type { SettingsService } from '@vybestack/llxprt-code-settings'; import { type CommandArgumentSchema } from './schema/types.js'; const toolsSchema: CommandArgumentSchema = [ { kind: 'value', name: 'subcommand', description: 'Choose tools subcommand', options: [ { value: 'list', description: 'List tools with status badges' }, { value: 'disable', description: 'Disable a tool by name' }, { value: 'enable', description: 'Enable a tool by name' }, { value: 'desc', description: 'List tools with descriptions' }, { value: 'descriptions', description: 'Alias for desc' }, ], }, ]; const normalizeToolName = (name: string): string => name.trim().toLowerCase(); // Tokenizes quoted/unquoted args. The pattern is passed to RegExp via an // identifier so it is not a static literal flagged by sonarjs/regular-expr. const ARG_TOKEN_PATTERN = '(?:[^\\s"\']+|"[^"]*"|\'[^\']*\')+'; const ARG_TOKEN_REGEX = new RegExp(ARG_TOKEN_PATTERN, 'g'); function stripQuotes(value: string): string { const trimmed = value.trim(); if ( (trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'")) ) { return trimmed.slice(1, -1).trim(); } return trimmed; } function getSettingsService(context: CommandContext): SettingsService | null { const config = context.services.config; if (config && typeof config.getSettingsService === 'function') { return config.getSettingsService(); } return null; } function readToolLists(context: CommandContext): { disabled: Set; allowed: Set; } { const settings = getSettingsService(context); const config = context.services.config; const read = (key: string): unknown => { if (settings) { return settings.get(key); } if (config && typeof config.getEphemeralSetting === 'function') { return config.getEphemeralSetting(key); } return undefined; }; const disabled = Array.isArray(read('tools.disabled')) ? new Set((read('tools.disabled') as string[]).map(normalizeToolName)) : new Set(); const allowed = Array.isArray(read('tools.allowed')) ? new Set((read('tools.allowed') as string[]).map(normalizeToolName)) : new Set(); const legacy = read('disabled-tools'); if (Array.isArray(legacy)) { for (const name of legacy as string[]) { disabled.add(normalizeToolName(name)); } } return { disabled, allowed }; } function persistToolLists( context: CommandContext, disabled: Set, allowed: Set, ): void { const disabledList = Array.from(new Set(disabled)).map((name) => name); const allowedList = Array.from(new Set(allowed)).map((name) => name); const config = context.services.config; const settings = getSettingsService(context); if (settings) { settings.set('tools.disabled', disabledList); settings.set('disabled-tools', disabledList); settings.set('tools.allowed', allowedList); } if (config) { if (typeof config.setEphemeralSetting === 'function') { config.setEphemeralSetting('tools.disabled', disabledList); config.setEphemeralSetting('disabled-tools', disabledList); config.setEphemeralSetting('tools.allowed', allowedList); } if (typeof config.getEphemeralSettings === 'function') { const ephemerals: unknown = config.getEphemeralSettings(); if (ephemerals !== null && typeof ephemerals === 'object') { const ephemeralSettings = ephemerals as Record; ephemeralSettings['tools.disabled'] = disabledList; ephemeralSettings['disabled-tools'] = disabledList; ephemeralSettings['tools.allowed'] = allowedList; } } } } function buildStatusLine( tool: ToolInfo, disabledSet: Set, allowedSet: Set, showDescriptions: boolean, ): string { const canonical = normalizeToolName(tool.name); const isExplicitlyAllowed = allowedSet.size === 0 || allowedSet.has(canonical); const isDisabled = disabledSet.has(canonical) || !isExplicitlyAllowed; const statusLabel = isDisabled ? '[disabled]' : '[enabled]'; const display = tool.displayName ?? tool.name; if (!showDescriptions || !tool.description) { return ` - ${display} ${statusLabel}`; } const descLines = tool.description.trim().split('\n'); const body = descLines.map((line) => ` ${line}`).join('\n'); return ` - ${display} (${tool.name}) ${statusLabel}\n${body}`; } function formatListMessage( tools: readonly ToolInfo[], disabledSet: Set, allowedSet: Set, showDescriptions: boolean, ): string { const filtered = tools.filter((tool) => tool.source !== 'mcp'); if (filtered.length === 0) { return 'Available LLxprt Code tools:\n\n No tools available\n'; } const lines = filtered.map((tool) => buildStatusLine(tool, disabledSet, allowedSet, showDescriptions), ); const disabledCount = disabledSet.size; const summary = `\nDisabled tools: ${disabledCount}`; return `Available LLxprt Code tools:\n\n${lines.join('\n')}\n${summary}`; } function resolveToolByName( identifier: string, tools: readonly ToolInfo[], ): ToolInfo | null { const normalized = normalizeToolName(identifier); const canonicalMap = new Map(); const friendlyMap = new Map(); for (const tool of tools) { canonicalMap.set(normalizeToolName(tool.name), tool); friendlyMap.set(normalizeToolName(tool.displayName ?? tool.name), tool); } if (canonicalMap.has(normalized)) { return canonicalMap.get(normalized)!; } if (friendlyMap.has(normalized)) { return friendlyMap.get(normalized)!; } return null; } async function handleToggleTool( context: CommandContext, subcommand: string, remainder: string, disabled: Set, allowed: Set, tools: readonly ToolInfo[], ): Promise { const identifier = stripQuotes(remainder); if (!identifier) { context.ui.addItem( { type: MessageType.ERROR, text: `Usage: /tools ${subcommand} `, }, Date.now(), ); return; } const target = resolveToolByName(identifier, tools); if (!target || target.source === 'mcp') { context.ui.addItem( { type: MessageType.ERROR, text: `Tool "${identifier}" not found.`, }, Date.now(), ); return; } const canonical = normalizeToolName(target.name); const display = target.displayName ?? target.name; let feedback: string; if (subcommand === 'disable') { disabled.add(canonical); allowed.delete(canonical); feedback = `Disabled tool '${display}'.`; } else { disabled.delete(canonical); if (allowed.size > 0) { allowed.add(canonical); } feedback = `Enabled tool '${display}'.`; } persistToolLists(context, disabled, allowed); const config = context.services.config; const agentClient = typeof config?.getAgentClient === 'function' ? config.getAgentClient() : undefined; if (agentClient && typeof agentClient.setTools === 'function') { try { await agentClient.setTools(); } catch (error) { context.ui.addItem( { type: MessageType.INFO, text: `Warning: failed to refresh tool schema after ${ subcommand === 'disable' ? 'disabling' : 'enabling' } '${display}': ${error instanceof Error ? error.message : String(error)}`, }, Date.now(), ); } } context.ui.addItem({ type: MessageType.INFO, text: feedback }, Date.now()); } export const toolsCommand: SlashCommand = { name: 'tools', description: 'List, enable, or disable LLxprt Code tools', kind: CommandKind.BUILT_IN, schema: toolsSchema, action: async (context: CommandContext, args = ''): Promise => { const agent = context.services.agent; if (!agent) { context.ui.addItem( { type: MessageType.ERROR, text: 'Could not retrieve tools from the agent.', }, Date.now(), ); return; } const raw = args.trim(); const tokens = raw.match(ARG_TOKEN_REGEX) ?? []; const rawSubcommand = tokens.shift(); const subcommand = (rawSubcommand ?? 'list').toLowerCase(); const remainder = raw.length > 0 && rawSubcommand ? raw.slice(raw.indexOf(rawSubcommand) + rawSubcommand.length).trim() : tokens.join(' '); const { disabled, allowed } = readToolLists(context); const tools = agent.tools.list(); const showDescriptions = subcommand === 'desc' || subcommand === 'descriptions'; if (subcommand === 'list' || showDescriptions) { const message = formatListMessage( tools, disabled, allowed, showDescriptions, ); context.ui.addItem({ type: MessageType.INFO, text: message }); return; } if (subcommand === 'disable' || subcommand === 'enable') { await handleToggleTool( context, subcommand, remainder, disabled, allowed, tools, ); return; } const message = formatListMessage(tools, disabled, allowed, false); context.ui.addItem({ type: MessageType.INFO, text: message }); }, };