mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2025-11-30 22:43:00 -05:00
feat: Add execution command tool (#273)
This commit is contained in:
@ -132,6 +132,7 @@ add_qtc_plugin(QodeAssist
|
||||
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
|
||||
tools/EditFileTool.hpp tools/EditFileTool.cpp
|
||||
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
|
||||
tools/ExecuteTerminalCommandTool.hpp tools/ExecuteTerminalCommandTool.cpp
|
||||
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
|
||||
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
|
||||
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
||||
|
||||
@ -108,6 +108,8 @@ const char CA_ALLOW_FILE_SYSTEM_WRITE[] = "QodeAssist.caAllowFileSystemWrite";
|
||||
const char CA_ALLOW_ACCESS_OUTSIDE_PROJECT[] = "QodeAssist.caAllowAccessOutsideProject";
|
||||
const char CA_ENABLE_EDIT_FILE_TOOL[] = "QodeAssist.caEnableEditFileTool";
|
||||
const char CA_ENABLE_BUILD_PROJECT_TOOL[] = "QodeAssist.caEnableBuildProjectTool";
|
||||
const char CA_ENABLE_TERMINAL_COMMAND_TOOL[] = "QodeAssist.caEnableTerminalCommandTool";
|
||||
const char CA_ALLOWED_TERMINAL_COMMANDS[] = "QodeAssist.caAllowedTerminalCommands";
|
||||
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
|
||||
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
|
||||
|
||||
@ -89,6 +89,22 @@ ToolsSettings::ToolsSettings()
|
||||
"project. This feature is under testing and may have unexpected behavior."));
|
||||
enableBuildProjectTool.setDefaultValue(false);
|
||||
|
||||
enableTerminalCommandTool.setSettingsKey(Constants::CA_ENABLE_TERMINAL_COMMAND_TOOL);
|
||||
enableTerminalCommandTool.setLabelText(Tr::tr("Enable Terminal Command Tool (Experimental)"));
|
||||
enableTerminalCommandTool.setToolTip(
|
||||
Tr::tr("Enable the experimental execute_terminal_command tool that allows AI to execute "
|
||||
"terminal commands from the allowed list. This feature is under testing and may have "
|
||||
"unexpected behavior."));
|
||||
enableTerminalCommandTool.setDefaultValue(false);
|
||||
|
||||
allowedTerminalCommands.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS);
|
||||
allowedTerminalCommands.setLabelText(Tr::tr("Allowed Terminal Commands"));
|
||||
allowedTerminalCommands.setToolTip(
|
||||
Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute. "
|
||||
"Example: git, ls, cat, grep, cmake"));
|
||||
allowedTerminalCommands.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
allowedTerminalCommands.setDefaultValue("git, ls, cat, grep");
|
||||
|
||||
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
|
||||
|
||||
readSettings();
|
||||
@ -113,7 +129,12 @@ ToolsSettings::ToolsSettings()
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Experimental Features")),
|
||||
Column{enableEditFileTool, enableBuildProjectTool, autoApplyFileEdits}},
|
||||
Column{
|
||||
enableEditFileTool,
|
||||
enableBuildProjectTool,
|
||||
enableTerminalCommandTool,
|
||||
allowedTerminalCommands,
|
||||
autoApplyFileEdits}},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
@ -144,6 +165,8 @@ void ToolsSettings::resetSettingsToDefaults()
|
||||
resetAspect(autoApplyFileEdits);
|
||||
resetAspect(enableEditFileTool);
|
||||
resetAspect(enableBuildProjectTool);
|
||||
resetAspect(enableTerminalCommandTool);
|
||||
resetAspect(allowedTerminalCommands);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,6 +40,8 @@ public:
|
||||
// Experimental features
|
||||
Utils::BoolAspect enableEditFileTool{this};
|
||||
Utils::BoolAspect enableBuildProjectTool{this};
|
||||
Utils::BoolAspect enableTerminalCommandTool{this};
|
||||
Utils::StringAspect allowedTerminalCommands{this};
|
||||
Utils::BoolAspect autoApplyFileEdits{this};
|
||||
|
||||
private:
|
||||
|
||||
238
tools/ExecuteTerminalCommandTool.cpp
Normal file
238
tools/ExecuteTerminalCommandTool.cpp
Normal file
@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ExecuteTerminalCommandTool.hpp"
|
||||
|
||||
#include <logger/Logger.hpp>
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <QDir>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QPromise>
|
||||
#include <QRegularExpression>
|
||||
#include <QSharedPointer>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
ExecuteTerminalCommandTool::ExecuteTerminalCommandTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QString ExecuteTerminalCommandTool::name() const
|
||||
{
|
||||
return "execute_terminal_command";
|
||||
}
|
||||
|
||||
QString ExecuteTerminalCommandTool::stringName() const
|
||||
{
|
||||
return "Executing terminal command";
|
||||
}
|
||||
|
||||
QString ExecuteTerminalCommandTool::description() const
|
||||
{
|
||||
const QStringList allowed = getAllowedCommands();
|
||||
const QString allowedList = allowed.isEmpty() ? "none" : allowed.join(", ");
|
||||
|
||||
return QString(
|
||||
"Execute a terminal command in the project directory. "
|
||||
"Only commands from the allowed list can be executed. "
|
||||
"Currently allowed commands: %1. "
|
||||
"The command will be executed in the root directory of the active project. "
|
||||
"Returns the command output (stdout and stderr) or an error message if the command fails.")
|
||||
.arg(allowedList);
|
||||
}
|
||||
|
||||
QJsonObject ExecuteTerminalCommandTool::getDefinition(LLMCore::ToolSchemaFormat format) const
|
||||
{
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
|
||||
const QStringList allowed = getAllowedCommands();
|
||||
const QString allowedList = allowed.isEmpty() ? "none" : allowed.join(", ");
|
||||
|
||||
QJsonObject properties;
|
||||
properties["command"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description",
|
||||
QString("The terminal command to execute. Must be one of the allowed commands: %1")
|
||||
.arg(allowedList)}};
|
||||
|
||||
properties["args"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description", "Optional arguments for the command (default: empty)"}};
|
||||
|
||||
definition["properties"] = properties;
|
||||
definition["required"] = QJsonArray{"command"};
|
||||
|
||||
switch (format) {
|
||||
case LLMCore::ToolSchemaFormat::OpenAI:
|
||||
return customizeForOpenAI(definition);
|
||||
case LLMCore::ToolSchemaFormat::Claude:
|
||||
return customizeForClaude(definition);
|
||||
case LLMCore::ToolSchemaFormat::Ollama:
|
||||
return customizeForOllama(definition);
|
||||
case LLMCore::ToolSchemaFormat::Google:
|
||||
return customizeForGoogle(definition);
|
||||
}
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
LLMCore::ToolPermissions ExecuteTerminalCommandTool::requiredPermissions() const
|
||||
{
|
||||
return LLMCore::ToolPermission::None;
|
||||
}
|
||||
|
||||
QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
const QString command = input.value("command").toString().trimmed();
|
||||
const QString args = input.value("args").toString().trimmed();
|
||||
|
||||
if (command.isEmpty()) {
|
||||
LOG_MESSAGE("ExecuteTerminalCommandTool: Command is empty");
|
||||
return QtFuture::makeReadyFuture(QString("Error: Command parameter is required."));
|
||||
}
|
||||
|
||||
if (!isCommandAllowed(command)) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' is not allowed")
|
||||
.arg(command));
|
||||
const QStringList allowed = getAllowedCommands();
|
||||
const QString allowedList = allowed.isEmpty() ? "none" : allowed.join(", ");
|
||||
return QtFuture::makeReadyFuture(
|
||||
QString("Error: Command '%1' is not in the allowed list. Allowed commands: %2")
|
||||
.arg(command)
|
||||
.arg(allowedList));
|
||||
}
|
||||
|
||||
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||
QString workingDir;
|
||||
|
||||
if (project) {
|
||||
workingDir = project->projectDirectory().toString();
|
||||
LOG_MESSAGE(
|
||||
QString("ExecuteTerminalCommandTool: Working directory is '%1'").arg(workingDir));
|
||||
} else {
|
||||
LOG_MESSAGE("ExecuteTerminalCommandTool: No active project, using current directory");
|
||||
workingDir = QDir::currentPath();
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Executing command '%1' with args '%2'")
|
||||
.arg(command)
|
||||
.arg(args));
|
||||
|
||||
auto promise = QSharedPointer<QPromise<QString>>::create();
|
||||
QFuture<QString> future = promise->future();
|
||||
promise->start();
|
||||
|
||||
QProcess *process = new QProcess();
|
||||
process->setWorkingDirectory(workingDir);
|
||||
process->setProcessChannelMode(QProcess::MergedChannels);
|
||||
|
||||
QObject::connect(
|
||||
process,
|
||||
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
[process, promise, command, args](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
const QString output = QString::fromUtf8(process->readAll());
|
||||
|
||||
if (exitStatus == QProcess::NormalExit) {
|
||||
if (exitCode == 0) {
|
||||
LOG_MESSAGE(
|
||||
QString("ExecuteTerminalCommandTool: Command completed successfully"));
|
||||
promise->addResult(
|
||||
QString("Command '%1 %2' executed successfully.\n\nOutput:\n%3")
|
||||
.arg(command)
|
||||
.arg(args)
|
||||
.arg(output.isEmpty() ? "(no output)" : output));
|
||||
} else {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command failed with exit "
|
||||
"code %1")
|
||||
.arg(exitCode));
|
||||
promise->addResult(
|
||||
QString("Command '%1 %2' failed with exit code %3.\n\nOutput:\n%4")
|
||||
.arg(command)
|
||||
.arg(args)
|
||||
.arg(exitCode)
|
||||
.arg(output.isEmpty() ? "(no output)" : output));
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE("ExecuteTerminalCommandTool: Command crashed or was terminated");
|
||||
const QString error = process->errorString();
|
||||
promise->addResult(
|
||||
QString("Command '%1 %2' crashed or was terminated.\n\nError: %3\n\nOutput:\n%4")
|
||||
.arg(command)
|
||||
.arg(args)
|
||||
.arg(error)
|
||||
.arg(output.isEmpty() ? "(no output)" : output));
|
||||
}
|
||||
|
||||
promise->finish();
|
||||
process->deleteLater();
|
||||
});
|
||||
|
||||
QObject::connect(process, &QProcess::errorOccurred, [process, promise, command, args](
|
||||
QProcess::ProcessError error) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Process error occurred: %1").arg(error));
|
||||
const QString errorString = process->errorString();
|
||||
promise->addResult(QString("Error executing command '%1 %2': %3")
|
||||
.arg(command)
|
||||
.arg(args)
|
||||
.arg(errorString));
|
||||
promise->finish();
|
||||
process->deleteLater();
|
||||
});
|
||||
|
||||
if (args.isEmpty()) {
|
||||
process->start(command, QStringList());
|
||||
} else {
|
||||
QStringList argList = args.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
||||
process->start(command, argList);
|
||||
}
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
bool ExecuteTerminalCommandTool::isCommandAllowed(const QString &command) const
|
||||
{
|
||||
const QStringList allowed = getAllowedCommands();
|
||||
return allowed.contains(command, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
QStringList ExecuteTerminalCommandTool::getAllowedCommands() const
|
||||
{
|
||||
const QString commandsStr
|
||||
= Settings::toolsSettings().allowedTerminalCommands().trimmed();
|
||||
|
||||
if (commandsStr.isEmpty()) {
|
||||
return QStringList();
|
||||
}
|
||||
|
||||
QStringList commands = commandsStr.split(',', Qt::SkipEmptyParts);
|
||||
for (QString &cmd : commands) {
|
||||
cmd = cmd.trimmed();
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
47
tools/ExecuteTerminalCommandTool.hpp
Normal file
47
tools/ExecuteTerminalCommandTool.hpp
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <llmcore/BaseTool.hpp>
|
||||
#include <QObject>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
class ExecuteTerminalCommandTool : public LLMCore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ExecuteTerminalCommandTool(QObject *parent = nullptr);
|
||||
|
||||
QString name() const override;
|
||||
QString stringName() const override;
|
||||
QString description() const override;
|
||||
QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override;
|
||||
LLMCore::ToolPermissions requiredPermissions() const override;
|
||||
|
||||
QFuture<QString> executeAsync(const QJsonObject &input = QJsonObject()) override;
|
||||
|
||||
private:
|
||||
bool isCommandAllowed(const QString &command) const;
|
||||
QStringList getAllowedCommands() const;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
#include "BuildProjectTool.hpp"
|
||||
#include "CreateNewFileTool.hpp"
|
||||
#include "EditFileTool.hpp"
|
||||
#include "ExecuteTerminalCommandTool.hpp"
|
||||
#include "FindAndReadFileTool.hpp"
|
||||
#include "GetIssuesListTool.hpp"
|
||||
#include "ListProjectFilesTool.hpp"
|
||||
@ -50,6 +51,7 @@ void ToolsFactory::registerTools()
|
||||
registerTool(new CreateNewFileTool(this));
|
||||
registerTool(new EditFileTool(this));
|
||||
registerTool(new BuildProjectTool(this));
|
||||
registerTool(new ExecuteTerminalCommandTool(this));
|
||||
registerTool(new ProjectSearchTool(this));
|
||||
registerTool(new FindAndReadFileTool(this));
|
||||
|
||||
@ -100,6 +102,11 @@ QJsonArray ToolsFactory::getToolsDefinitions(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (it.value()->name() == "execute_terminal_command"
|
||||
&& !settings.enableTerminalCommandTool()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto requiredPerms = it.value()->requiredPermissions();
|
||||
|
||||
if (filter != LLMCore::RunToolsFilter::ALL) {
|
||||
|
||||
Reference in New Issue
Block a user