diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index e744edd88..87fceb3a2 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -928,6 +928,8 @@ list(APPEND QET_SRC_FILES ${QET_DIR}/sources/scripting/scriptlibrary.h ${QET_DIR}/sources/scripting/scriptmanagerdialog.cpp ${QET_DIR}/sources/scripting/scriptmanagerdialog.h + ${QET_DIR}/sources/scripting/assistantinfo.cpp + ${QET_DIR}/sources/scripting/assistantinfo.h ) if(QET_SPACEMOUSE_ENABLED) diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 33075da0e..e014bb41a 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -20,6 +20,7 @@ #include "scripting/qetscripting.h" #include "scripting/scriptlibrary.h" #include "scripting/scriptmanagerdialog.h" +#include "scripting/assistantinfo.h" #endif #include #include @@ -1376,6 +1377,7 @@ void QETDiagramEditor::setUpMenu() rebuildScriptActions(); connect(&ScriptLibrary::instance(), &ScriptLibrary::changed, this, &QETDiagramEditor::rebuildScriptActions); + AssistantInfo::watch(); #endif #ifdef QET_EXPORT_PROJECT_DB menu_project -> addSeparator(); diff --git a/sources/scripting/assistantinfo.cpp b/sources/scripting/assistantinfo.cpp new file mode 100644 index 000000000..97d236722 --- /dev/null +++ b/sources/scripting/assistantinfo.cpp @@ -0,0 +1,143 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech 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 2 of the License, or + (at your option) any later version. + + QElectroTech 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 QElectroTech. If not, see . +*/ +#include "assistantinfo.h" + +#include "qetscriptapi.h" +#include "scriptlibrary.h" +#include "../qetapp.h" +#include "../qetversion.h" +#include "../utils/qetsettings.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + QJsonObject s_live; ///< empty while live mode is closed + bool s_running = true; +} + +namespace AssistantInfo { + +QString path() +{ + return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + + QStringLiteral("/qet-assistant.json"); +} + +/** + @brief watch + Write the file now, again whenever the stored scripts change, and mark + it stopped when QElectroTech quits. Called by every editor window as it + opens; acts once. +*/ +void watch() +{ + static bool watching = false; + if (watching) return; + watching = true; + QObject::connect(&ScriptLibrary::instance(), &ScriptLibrary::changed, &write); + QObject::connect(qApp, &QCoreApplication::aboutToQuit, &markStopped); + write(); +} + +void write() +{ + QJsonArray scripts, refused; + for (const ScriptLibrary::Script &s : ScriptLibrary::instance().scripts()) { + const ScriptHeader &h = s.header; + scripts.append(QJsonObject{ + {QStringLiteral("id"), h.id}, + {QStringLiteral("name"), h.name}, + {QStringLiteral("file"), s.path}, + {QStringLiteral("icon"), h.icon}, + {QStringLiteral("tooltip"), h.tooltip}, + {QStringLiteral("shortcut"), h.shortcut}, + {QStringLiteral("context"), h.context}, + {QStringLiteral("action_id"), ScriptLibrary::actionId(h.id)}}); + } + for (const QString &e : ScriptLibrary::instance().errors()) refused.append(e); + + const QJsonObject info{ + {QStringLiteral("format"), 1}, + {QStringLiteral("qelectrotech_version"), QetVersion::displayedVersion()}, + {QStringLiteral("program"), QCoreApplication::applicationFilePath()}, + {QStringLiteral("pid"), QCoreApplication::applicationPid()}, + {QStringLiteral("running"), s_running}, + {QStringLiteral("written"), QDateTime::currentDateTimeUtc().toString(Qt::ISODate)}, + {QStringLiteral("folders"), QJsonObject{ + {QStringLiteral("data"), QETApp::dataDir()}, + {QStringLiteral("config"), QETApp::configDir()}, + {QStringLiteral("scripts"), ScriptLibrary::folder()}, + {QStringLiteral("elements_common"), QETApp::commonElementsDir()}, + {QStringLiteral("elements_company"), QETApp::companyElementsDir()}, + {QStringLiteral("elements_custom"), QETApp::customElementsDir()}, + {QStringLiteral("titleblocks_common"), QETApp::commonTitleBlockTemplatesDir()}, + {QStringLiteral("titleblocks_company"), QETApp::companyTitleBlockTemplatesDir()}, + {QStringLiteral("titleblocks_custom"), QETApp::customTitleBlockTemplatesDir()}}}, + {QStringLiteral("features"), QJsonObject{ +#ifdef QET_HAS_SCRIPTING + {QStringLiteral("scripting_available"), true}, +#else + {QStringLiteral("scripting_available"), false}, +#endif + {QStringLiteral("scripting_enabled"), QetSettings::scriptingEnabled()}, + {QStringLiteral("live_mode_open"), !s_live.isEmpty()}}}, + {QStringLiteral("script_api"), QJsonArray::fromStringList(QetScriptApi::signatures())}, + {QStringLiteral("stored_scripts"), scripts}, + {QStringLiteral("refused_scripts"), refused}, + {QStringLiteral("live"), s_live.isEmpty() ? QJsonValue() : QJsonValue(s_live)}}; + + const QString file_path = path(); + QDir().mkpath(QFileInfo(file_path).path()); + QSaveFile file(file_path); + if (!file.open(QIODevice::WriteOnly)) return; + file.write(QJsonDocument(info).toJson(QJsonDocument::Indented)); + if (file.commit()) + QFile::setPermissions(file_path, QFile::ReadOwner | QFile::WriteOwner); +} + +/** + @brief setLive + The live channel's details while it is open (socket, token), or an + empty object once it closes. +*/ +void setLive(const QJsonObject &live) +{ + s_live = live; + write(); +} + +/** + @brief markStopped + QElectroTech is quitting: the file stays, as a record of where things + are, but says nothing is listening any more. +*/ +void markStopped() +{ + s_running = false; + s_live = QJsonObject(); + write(); +} + +} diff --git a/sources/scripting/assistantinfo.h b/sources/scripting/assistantinfo.h new file mode 100644 index 000000000..2243b3deb --- /dev/null +++ b/sources/scripting/assistantinfo.h @@ -0,0 +1,50 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech 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 2 of the License, or + (at your option) any later version. + + QElectroTech 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 QElectroTech. If not, see . +*/ +#ifndef ASSISTANTINFO_H +#define ASSISTANTINFO_H + +#include +#include + +/** + @brief qet-assistant.json: what an AI assistant (through the qet MCP + server) needs to know about this QElectroTech, in one file. + + Where its folders are -- data, settings, scripts, the element and title + block collections --, which features are switched on, every call a + script can make, the stored scripts, and, while live mode is open, how + to reach it. QElectroTech rewrites it when it starts and whenever one of + those changes, so the MCP server reads facts instead of guessing them + per platform -- wrong as soon as QElectroTech is started with + --data-dir or --config-dir. + + It always lives in the standard data folder for the platform, even + when --data-dir moves the data elsewhere: that is the one place the + server can look without being told. Readable by the user only: while + live mode is open it holds the channel's token. +*/ +namespace AssistantInfo +{ + QString path(); + void watch(); + void write(); + void setLive(const QJsonObject &live); + void markStopped(); +} + +#endif // ASSISTANTINFO_H diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index a5fdec93f..21bc07a3b 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -132,9 +132,19 @@ int QetScriptApi::folioCount() const all of them. */ QStringList QetScriptApi::apiSignatures() const +{ + return signatures(); +} + +/** + @brief QetScriptApi::signatures + apiSignatures() without a script running: what qet-assistant.json lists + for an assistant before it has run anything. +*/ +QStringList QetScriptApi::signatures() { QStringList list; - const QMetaObject *meta = metaObject(); + const QMetaObject *meta = &staticMetaObject; for (int i = meta->methodOffset(); i < meta->methodCount(); ++i) { const QMetaMethod method = meta->method(i); if (method.methodType() != QMetaMethod::Method diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index 44abbfe2a..1186d77ec 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -581,6 +581,7 @@ class QetScriptApi : public QObject // -- what a script can call, read from this class itself -- Q_INVOKABLE QStringList apiSignatures() const; + static QStringList signatures(); void setUndoGrouped(bool grouped); diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp index 206735c64..e43f46c6c 100644 --- a/sources/ui/configpage/generalconfigurationpage.cpp +++ b/sources/ui/configpage/generalconfigurationpage.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "generalconfigurationpage.h" +#include "../../scripting/assistantinfo.h" #include "../../qetapp.h" #include "../../qeticons.h" @@ -307,6 +308,8 @@ void GeneralConfigurationPage::applyConf() if (ui->m_enable_scripting->isEnabled()) { QetSettings::setScriptingEnabled(ui->m_enable_scripting->isChecked()); } + //What an assistant reads about this QElectroTech follows the change + AssistantInfo::write(); //GENERIC PANEL settings.setValue("genericpanel/folio",ui->m_use_folio_label->isChecked());