mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2025-12-01 06:52:53 -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/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
|
||||||
tools/EditFileTool.hpp tools/EditFileTool.cpp
|
tools/EditFileTool.hpp tools/EditFileTool.cpp
|
||||||
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
|
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
|
||||||
|
tools/ExecuteTerminalCommandTool.hpp tools/ExecuteTerminalCommandTool.cpp
|
||||||
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
|
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
|
||||||
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
|
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
|
||||||
tools/FileSearchUtils.hpp tools/FileSearchUtils.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_ALLOW_ACCESS_OUTSIDE_PROJECT[] = "QodeAssist.caAllowAccessOutsideProject";
|
||||||
const char CA_ENABLE_EDIT_FILE_TOOL[] = "QodeAssist.caEnableEditFileTool";
|
const char CA_ENABLE_EDIT_FILE_TOOL[] = "QodeAssist.caEnableEditFileTool";
|
||||||
const char CA_ENABLE_BUILD_PROJECT_TOOL[] = "QodeAssist.caEnableBuildProjectTool";
|
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_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
|
||||||
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
|
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."));
|
"project. This feature is under testing and may have unexpected behavior."));
|
||||||
enableBuildProjectTool.setDefaultValue(false);
|
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");
|
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
|
||||||
|
|
||||||
readSettings();
|
readSettings();
|
||||||
@ -113,7 +129,12 @@ ToolsSettings::ToolsSettings()
|
|||||||
Space{8},
|
Space{8},
|
||||||
Group{
|
Group{
|
||||||
title(Tr::tr("Experimental Features")),
|
title(Tr::tr("Experimental Features")),
|
||||||
Column{enableEditFileTool, enableBuildProjectTool, autoApplyFileEdits}},
|
Column{
|
||||||
|
enableEditFileTool,
|
||||||
|
enableBuildProjectTool,
|
||||||
|
enableTerminalCommandTool,
|
||||||
|
allowedTerminalCommands,
|
||||||
|
autoApplyFileEdits}},
|
||||||
Stretch{1}};
|
Stretch{1}};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -144,6 +165,8 @@ void ToolsSettings::resetSettingsToDefaults()
|
|||||||
resetAspect(autoApplyFileEdits);
|
resetAspect(autoApplyFileEdits);
|
||||||
resetAspect(enableEditFileTool);
|
resetAspect(enableEditFileTool);
|
||||||
resetAspect(enableBuildProjectTool);
|
resetAspect(enableBuildProjectTool);
|
||||||
|
resetAspect(enableTerminalCommandTool);
|
||||||
|
resetAspect(allowedTerminalCommands);
|
||||||
writeSettings();
|
writeSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,8 @@ public:
|
|||||||
// Experimental features
|
// Experimental features
|
||||||
Utils::BoolAspect enableEditFileTool{this};
|
Utils::BoolAspect enableEditFileTool{this};
|
||||||
Utils::BoolAspect enableBuildProjectTool{this};
|
Utils::BoolAspect enableBuildProjectTool{this};
|
||||||
|
Utils::BoolAspect enableTerminalCommandTool{this};
|
||||||
|
Utils::StringAspect allowedTerminalCommands{this};
|
||||||
Utils::BoolAspect autoApplyFileEdits{this};
|
Utils::BoolAspect autoApplyFileEdits{this};
|
||||||
|
|
||||||
private:
|
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 "BuildProjectTool.hpp"
|
||||||
#include "CreateNewFileTool.hpp"
|
#include "CreateNewFileTool.hpp"
|
||||||
#include "EditFileTool.hpp"
|
#include "EditFileTool.hpp"
|
||||||
|
#include "ExecuteTerminalCommandTool.hpp"
|
||||||
#include "FindAndReadFileTool.hpp"
|
#include "FindAndReadFileTool.hpp"
|
||||||
#include "GetIssuesListTool.hpp"
|
#include "GetIssuesListTool.hpp"
|
||||||
#include "ListProjectFilesTool.hpp"
|
#include "ListProjectFilesTool.hpp"
|
||||||
@ -50,6 +51,7 @@ void ToolsFactory::registerTools()
|
|||||||
registerTool(new CreateNewFileTool(this));
|
registerTool(new CreateNewFileTool(this));
|
||||||
registerTool(new EditFileTool(this));
|
registerTool(new EditFileTool(this));
|
||||||
registerTool(new BuildProjectTool(this));
|
registerTool(new BuildProjectTool(this));
|
||||||
|
registerTool(new ExecuteTerminalCommandTool(this));
|
||||||
registerTool(new ProjectSearchTool(this));
|
registerTool(new ProjectSearchTool(this));
|
||||||
registerTool(new FindAndReadFileTool(this));
|
registerTool(new FindAndReadFileTool(this));
|
||||||
|
|
||||||
@ -100,6 +102,11 @@ QJsonArray ToolsFactory::getToolsDefinitions(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (it.value()->name() == "execute_terminal_command"
|
||||||
|
&& !settings.enableTerminalCommandTool()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const auto requiredPerms = it.value()->requiredPermissions();
|
const auto requiredPerms = it.value()->requiredPermissions();
|
||||||
|
|
||||||
if (filter != LLMCore::RunToolsFilter::ALL) {
|
if (filter != LLMCore::RunToolsFilter::ALL) {
|
||||||
|
|||||||
Reference in New Issue
Block a user