diff --git a/CMakeLists.txt b/CMakeLists.txt
index 27b57fa..8c8e11b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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
diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp
index 16faae5..3ce74f1 100644
--- a/settings/SettingsConstants.hpp
+++ b/settings/SettingsConstants.hpp
@@ -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";
diff --git a/settings/ToolsSettings.cpp b/settings/ToolsSettings.cpp
index 7ee1246..be698d1 100644
--- a/settings/ToolsSettings.cpp
+++ b/settings/ToolsSettings.cpp
@@ -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();
}
}
diff --git a/settings/ToolsSettings.hpp b/settings/ToolsSettings.hpp
index df02407..5fa2bf3 100644
--- a/settings/ToolsSettings.hpp
+++ b/settings/ToolsSettings.hpp
@@ -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:
diff --git a/tools/ExecuteTerminalCommandTool.cpp b/tools/ExecuteTerminalCommandTool.cpp
new file mode 100644
index 0000000..4440bc2
--- /dev/null
+++ b/tools/ExecuteTerminalCommandTool.cpp
@@ -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 .
+ */
+
+#include "ExecuteTerminalCommandTool.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+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 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>::create();
+ QFuture future = promise->future();
+ promise->start();
+
+ QProcess *process = new QProcess();
+ process->setWorkingDirectory(workingDir);
+ process->setProcessChannelMode(QProcess::MergedChannels);
+
+ QObject::connect(
+ process,
+ QOverload::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
+
diff --git a/tools/ExecuteTerminalCommandTool.hpp b/tools/ExecuteTerminalCommandTool.hpp
new file mode 100644
index 0000000..41d9e87
--- /dev/null
+++ b/tools/ExecuteTerminalCommandTool.hpp
@@ -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 .
+ */
+
+#pragma once
+
+#include
+#include
+
+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 executeAsync(const QJsonObject &input = QJsonObject()) override;
+
+private:
+ bool isCommandAllowed(const QString &command) const;
+ QStringList getAllowedCommands() const;
+};
+
+} // namespace QodeAssist::Tools
+
diff --git a/tools/ToolsFactory.cpp b/tools/ToolsFactory.cpp
index b8c7159..5a97d1f 100644
--- a/tools/ToolsFactory.cpp
+++ b/tools/ToolsFactory.cpp
@@ -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) {