From e0404617d25385b151c8ae8106f2bd5d471ab407 Mon Sep 17 00:00:00 2001 From: Mark Kats Date: Thu, 17 Sep 2026 21:53:09 +0300 Subject: [PATCH] chore: keep widget sources only --- .agents/plugins/marketplace.json | 20 + .gitignore | 12 + CHATGPT-SETUP.md | 100 + LICENSE | 37 + README.md | 189 ++ .../shd-mcp-plugin/.codex-plugin/plugin.json | 49 + plugins/shd-mcp-plugin/.mcp.json | 9 + plugins/shd-mcp-plugin/README.md | 99 + plugins/shd-mcp-plugin/assets/logo.png | Bin 0 -> 60059 bytes .../skills/shd-active-projects/SKILL.md | 41 + .../shd-active-projects/agents/openai.yaml | 5 + .../references/project-widget.md | 8 + .../skills/shd-agents-operations/SKILL.md | 43 + .../shd-agents-operations/agents/openai.yaml | 5 + .../references/agent-operations.md | 14 + .../skills/shd-airbase-audit/SKILL.md | 39 + .../shd-airbase-audit/agents/openai.yaml | 5 + .../references/audit-contract.md | 20 + .../skills/shd-analytics/SKILL.md | 33 + .../skills/shd-analytics/agents/openai.yaml | 5 + .../references/analytics-contract.md | 9 + .../skills/shd-discussions/SKILL.md | 26 + .../skills/shd-discussions/agents/openai.yaml | 5 + .../references/discussion-contract.md | 14 + .../skills/shd-documents/SKILL.md | 29 + .../skills/shd-documents/agents/openai.yaml | 5 + .../references/document-safety.md | 14 + .../skills/shd-entity-resolution/SKILL.md | 26 + .../shd-entity-resolution/agents/openai.yaml | 5 + .../references/entity-resolution.md | 8 + .../skills/shd-estimate-management/SKILL.md | 28 + .../agents/openai.yaml | 5 + .../references/estimate-safety.md | 7 + .../skills/shd-event-positions/SKILL.md | 25 + .../shd-event-positions/agents/openai.yaml | 5 + .../references/scheduling-safety.md | 7 + .../skills/shd-finance-crm/SKILL.md | 41 + .../skills/shd-finance-crm/agents/openai.yaml | 5 + .../references/finance-crm-audit.md | 6 + .../shd-financial-account-audit/SKILL.md | 30 + .../agents/openai.yaml | 5 + .../references/finance-audit.md | 8 + .../shd-mcp-plugin/skills/shd-gitea/SKILL.md | 24 + .../skills/shd-gitea/agents/openai.yaml | 5 + .../shd-gitea/references/gitea-contract.md | 11 + .../skills/shd-inventory/SKILL.md | 26 + .../skills/shd-inventory/agents/openai.yaml | 5 + .../references/inventory-safety.md | 14 + .../skills/shd-notifications/SKILL.md | 25 + .../shd-notifications/agents/openai.yaml | 5 + .../references/notification-contract.md | 11 + .../skills/shd-organizations-acl/SKILL.md | 26 + .../shd-organizations-acl/agents/openai.yaml | 5 + .../references/organization-acl.md | 13 + .../skills/shd-project-files/SKILL.md | 30 + .../shd-project-files/agents/openai.yaml | 5 + .../references/file-actions.md | 5 + .../skills/shd-project-status/SKILL.md | 41 + .../shd-project-status/agents/openai.yaml | 5 + .../references/context-sections.md | 5 + .../skills/shd-realtime-activity/SKILL.md | 23 + .../shd-realtime-activity/agents/openai.yaml | 5 + .../references/realtime-contract.md | 11 + .../skills/shd-routing/SKILL.md | 66 + .../skills/shd-routing/agents/openai.yaml | 5 + .../shd-routing/references/module-map.md | 25 + .../skills/shd-safe-bulk-change/SKILL.md | 30 + .../shd-safe-bulk-change/agents/openai.yaml | 5 + .../references/write-preflight.md | 7 + .../skills/shd-status-page/SKILL.md | 22 + .../skills/shd-status-page/agents/openai.yaml | 5 + .../references/status-page-safety.md | 14 + .../skills/shd-task-workflow/SKILL.md | 44 + .../shd-task-workflow/agents/openai.yaml | 5 + .../references/task-contract.md | 20 + .../skills/shd-terms-contracts/SKILL.md | 24 + .../shd-terms-contracts/agents/openai.yaml | 5 + .../references/terms-contract.md | 11 + .../skills/shd-wiki-management/SKILL.md | 26 + .../shd-wiki-management/agents/openai.yaml | 5 + .../references/wiki-safety.md | 7 + .../_shared/universal-widget-config.json | 1099 ++++++++++ .../widgets/_shared/universal-widget.html | 109 + .../widgets/_shared/widget-theme.css | 1236 +++++++++++ .../widgets/_shared/write-actions.css | 142 ++ .../widgets/_shared/write-actions.js | 454 +++++ .../widgets/acl-matrix/widget.json | 16 + .../widgets/active-projects/source.html | 1812 +++++++++++++++++ .../widgets/active-projects/widget.json | 16 + .../widgets/activity/widget.json | 16 + .../widgets/agents-health/widget.json | 16 + .../shd-mcp-plugin/widgets/agents/widget.json | 16 + .../widgets/airbase-preview/widget.json | 16 + .../widgets/airbase/widget.json | 16 + .../controller-observability/source.html | 1493 ++++++++++++++ .../controller-observability/widget.json | 16 + .../widgets/crm-funnel/widget.json | 16 + .../shd-mcp-plugin/widgets/crm/widget.json | 16 + .../widgets/discussions/widget.json | 16 + .../documents-completeness/widget.json | 16 + .../widgets/documents/widget.json | 16 + .../shd-mcp-plugin/widgets/files/widget.json | 16 + .../widgets/finance-dashboard/widget.json | 16 + .../widgets/finance/widget.json | 16 + .../widgets/gitea-board/widget.json | 16 + .../shd-mcp-plugin/widgets/gitea/widget.json | 16 + .../widgets/inventory-warnings/widget.json | 16 + .../widgets/inventory/widget.json | 16 + .../widgets/notes-tree/widget.json | 16 + .../shd-mcp-plugin/widgets/notes/widget.json | 16 + .../widgets/notifications/widget.json | 16 + .../widgets/organizations/widget.json | 16 + .../pdf-scheme-editor-progress/widget.json | 16 + .../widgets/pdf-scheme-editor/widget.json | 16 + .../widgets/project-overview/widget.json | 16 + .../widgets/project-timeline/widget.json | 16 + .../widgets/proposal-approvals/widget.json | 16 + .../widgets/proposals/widget.json | 16 + .../widgets/schedule-calendar/widget.json | 16 + .../widgets/scheduling/widget.json | 16 + .../widgets/status-page/widget.json | 16 + .../widgets/task-kanban/widget.json | 16 + .../shd-mcp-plugin/widgets/tasks/widget.json | 16 + .../widgets/team-workload/widget.json | 16 + scripts/generate-widget-artifacts.mjs | 342 ++++ 125 files changed, 8935 insertions(+) create mode 100644 .agents/plugins/marketplace.json create mode 100644 .gitignore create mode 100644 CHATGPT-SETUP.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 plugins/shd-mcp-plugin/.codex-plugin/plugin.json create mode 100644 plugins/shd-mcp-plugin/.mcp.json create mode 100644 plugins/shd-mcp-plugin/README.md create mode 100644 plugins/shd-mcp-plugin/assets/logo.png create mode 100644 plugins/shd-mcp-plugin/skills/shd-active-projects/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-active-projects/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-active-projects/references/project-widget.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-agents-operations/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-agents-operations/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-agents-operations/references/agent-operations.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-airbase-audit/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-airbase-audit/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-airbase-audit/references/audit-contract.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-analytics/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-analytics/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-analytics/references/analytics-contract.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-discussions/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-discussions/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-discussions/references/discussion-contract.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-documents/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-documents/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-documents/references/document-safety.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-entity-resolution/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-entity-resolution/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-entity-resolution/references/entity-resolution.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-estimate-management/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-estimate-management/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-estimate-management/references/estimate-safety.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-event-positions/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-event-positions/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-event-positions/references/scheduling-safety.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-finance-crm/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-finance-crm/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-finance-crm/references/finance-crm-audit.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-financial-account-audit/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-financial-account-audit/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-financial-account-audit/references/finance-audit.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-gitea/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-gitea/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-gitea/references/gitea-contract.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-inventory/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-inventory/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-inventory/references/inventory-safety.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-notifications/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-notifications/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-notifications/references/notification-contract.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-organizations-acl/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-organizations-acl/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-organizations-acl/references/organization-acl.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-project-files/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-project-files/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-project-files/references/file-actions.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-project-status/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-project-status/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-project-status/references/context-sections.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-realtime-activity/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-realtime-activity/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-realtime-activity/references/realtime-contract.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-routing/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-routing/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-routing/references/module-map.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-safe-bulk-change/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-safe-bulk-change/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-safe-bulk-change/references/write-preflight.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-status-page/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-status-page/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-status-page/references/status-page-safety.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-task-workflow/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-task-workflow/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-task-workflow/references/task-contract.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-terms-contracts/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-terms-contracts/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-terms-contracts/references/terms-contract.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-wiki-management/SKILL.md create mode 100644 plugins/shd-mcp-plugin/skills/shd-wiki-management/agents/openai.yaml create mode 100644 plugins/shd-mcp-plugin/skills/shd-wiki-management/references/wiki-safety.md create mode 100644 plugins/shd-mcp-plugin/widgets/_shared/universal-widget-config.json create mode 100644 plugins/shd-mcp-plugin/widgets/_shared/universal-widget.html create mode 100644 plugins/shd-mcp-plugin/widgets/_shared/widget-theme.css create mode 100644 plugins/shd-mcp-plugin/widgets/_shared/write-actions.css create mode 100644 plugins/shd-mcp-plugin/widgets/_shared/write-actions.js create mode 100644 plugins/shd-mcp-plugin/widgets/acl-matrix/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/active-projects/source.html create mode 100644 plugins/shd-mcp-plugin/widgets/active-projects/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/activity/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/agents-health/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/agents/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/airbase-preview/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/airbase/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/controller-observability/source.html create mode 100644 plugins/shd-mcp-plugin/widgets/controller-observability/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/crm-funnel/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/crm/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/discussions/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/documents-completeness/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/documents/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/files/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/finance-dashboard/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/finance/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/gitea-board/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/gitea/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/inventory-warnings/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/inventory/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/notes-tree/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/notes/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/notifications/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/organizations/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/pdf-scheme-editor-progress/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/pdf-scheme-editor/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/project-overview/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/project-timeline/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/proposal-approvals/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/proposals/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/schedule-calendar/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/scheduling/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/status-page/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/task-kanban/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/tasks/widget.json create mode 100644 plugins/shd-mcp-plugin/widgets/team-workload/widget.json create mode 100644 scripts/generate-widget-artifacts.mjs diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..346194d --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "shd-public", + "interface": { + "displayName": "SHD Public Plugins" + }, + "plugins": [ + { + "name": "shd-mcp-plugin", + "source": { + "source": "local", + "path": "./plugins/shd-mcp-plugin" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4db27e --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.env +.env.* +!.env.example +.DS_Store +.idea/ +.vscode/ +*.log +__pycache__/ +*.py[cod] + +# MCP widget runtime artifacts are generated from the sources before serving. +/plugins/shd-mcp-plugin/widgets/*/v1/ diff --git a/CHATGPT-SETUP.md b/CHATGPT-SETUP.md new file mode 100644 index 0000000..cbc20a0 --- /dev/null +++ b/CHATGPT-SETUP.md @@ -0,0 +1,100 @@ +# SHD MCP in ChatGPT + +This file describes the user-side connection. The repository cannot create an +app inside another person’s ChatGPT account and never stores OAuth tokens. + +## Custom app + +1. Open ChatGPT settings and enable Developer mode for Apps/Connectors. +2. Create a custom app with this MCP endpoint: + + `https://shd.xyz.su/mcp` + +3. Complete the SHD OAuth sign-in and consent screen. +4. Start a new chat or refresh the app connection. +5. Test with: `Покажи активные проекты с датой завершения и статусом.` + +The server publishes OAuth 2.1 discovery for the root resource and supports +ChatGPT Client ID Metadata Documents (CIMD), PKCE S256 and the +`private_key_jwt` token-endpoint authentication method. Keep the app URL as +`https://shd.xyz.su/mcp`; the retired `/mcp/system-data` resource is not a +valid replacement for the root MCP resource. + +The expected flow is: + +1. ChatGPT calls `shd_list_projects` with `archived: false`. +2. ChatGPT calls the corresponding module render tool after the list tool + before the final response when MCP Apps UI is supported: + `shd_render_projects_widget`, `shd_render_documents_widget`, + `shd_render_tasks_widget`, `shd_render_finance_widget`, + `shd_render_crm_widget`, `shd_render_discussions_widget`, + `shd_render_notifications_widget`, `shd_render_scheduling_widget`, + `shd_render_inventory_widget`, `shd_render_agents_widget`, + `shd_render_status_page_widget`, `shd_render_airbase_widget`, + `shd_render_notes_widget`, `shd_render_project_overview_widget`, + `shd_render_files_widget`, `shd_render_proposals_widget`, + `shd_render_activity_widget`, `shd_render_organizations_widget`, + `shd_render_gitea_widget`, `shd_render_schemes_widget`, `shd_render_task_kanban_widget`, + `shd_render_team_workload_widget`, `shd_render_project_timeline_widget`, + `shd_render_crm_funnel_widget`, `shd_render_finance_dashboard_widget`, + `shd_render_proposal_approvals_widget`, `shd_render_documents_completeness_widget`, + `shd_render_agents_health_widget`, `shd_render_schedule_calendar_widget`, + `shd_render_acl_matrix_widget`, `shd_render_inventory_warnings_widget`, + `shd_render_gitea_board_widget`, `shd_render_schemes_progress_widget`, + `shd_render_notes_tree_widget` or `shd_render_airbase_preview_widget`. +3. The selected widget renders a register, can call the matching detail tool for + one selected row and exposes controlled write actions when declared by the + render metadata. +4. For controller requests, use the explicit controller data→render pair: + `shd_get_controller_source_inventory` → `shd_render_controller_devices_widget`, + `shd_get_home_live_state` → `shd_render_controller_live_state_widget`, + `shd_get_controller_source_history` → `shd_render_controller_history_widget`, + or `shd_get_controller_state_statistics` → + `shd_render_controller_statistics_widget`. Pass the complete data result and + preserve `project`, `controller` and `item_id` in `meta.sourceArgs`. + +The widget resources are versioned at `ui://shd/active-projects/v1.html`, +`ui://shd/documents/v1.html`, `ui://shd/tasks/v1.html`, +`ui://shd/finance/v1.html`, `ui://shd/crm/v1.html`, +`ui://shd/discussions/v1.html`, `ui://shd/notifications/v1.html`, +`ui://shd/scheduling/v1.html`, `ui://shd/inventory/v1.html`, +`ui://shd/agents/v1.html`, `ui://shd/status-page/v1.html`, +`ui://shd/controller-observability/v1.html`, +`ui://shd/airbase/v1.html`, `ui://shd/notes/v1.html`, +`ui://shd/project-overview/v1.html`, `ui://shd/files/v1.html`, +`ui://shd/proposals/v1.html`, +`ui://shd/activity/v1.html`, `ui://shd/organizations/v1.html`, +`ui://shd/gitea/v1.html`, `ui://shd/pdf-scheme-editor/v1.html`, +`ui://shd/task-kanban/v1.html`, `ui://shd/team-workload/v1.html`, +`ui://shd/project-timeline/v1.html`, `ui://shd/crm-funnel/v1.html`, +`ui://shd/finance-dashboard/v1.html`, `ui://shd/proposal-approvals/v1.html`, +`ui://shd/documents-completeness/v1.html`, `ui://shd/agents-health/v1.html`, +`ui://shd/schedule-calendar/v1.html`, `ui://shd/acl-matrix/v1.html`, +`ui://shd/inventory-warnings/v1.html`, `ui://shd/gitea-board/v1.html`, +`ui://shd/pdf-scheme-editor-progress/v1.html`, `ui://shd/notes-tree/v1.html` and +`ui://shd/airbase-preview/v1.html`. + +The MCP tools remain usable without the widget. A failed OAuth connection is +an account/endpoint configuration problem, not a missing token in this repo. + +## Public directory + +Directory submission is a separate optional release step. It is needed for +public discovery and reviewed installation, not for internal use or a custom +app added manually in ChatGPT. Before submission, the owner must provide the +production HTTPS endpoint, OAuth metadata, privacy/support URLs, accurate app +metadata, test prompts and any review credentials requested by OpenAI. + +Use the official [submission guide](https://developers.openai.com/plugins/deploy/submission) +and [plugin guidelines](https://developers.openai.com/plugins/app-guidelines). + +## License choice + +This repository currently uses the proprietary SHD MCP Plugin License. Public +visibility means the source can be viewed; it does not grant permission to +redistribute, resell, modify or use it outside an authorized SHD account. + +MIT would grant anyone permission to use, copy, modify, publish, sublicense and +sell the plugin, including forks, while retaining only the copyright notice and +license text. Switching to MIT is a product/legal decision and is not done +implicitly by publishing the repository. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b964915 --- /dev/null +++ b/LICENSE @@ -0,0 +1,37 @@ +SHD MCP PLUGIN LICENSE + +Copyright (c) 2026 SHD + +This license applies to the SHD MCP Plugin package, its Skills, manifests, +documentation and SHD branding in this repository. The SHD MCP service and +its backend are separate systems and remain subject to their own terms. + +1. Permission + +SHD grants a limited, non-exclusive, non-transferable permission to install and +use an unmodified copy of this plugin with an authorized SHD account and a +compatible MCP client. + +2. Restrictions + +Unless SHD gives prior written permission, you may not sell, sublicense, +redistribute, publish modified versions of, remove notices from, or use this +package and its branding to provide a competing service. Public access to this +repository does not transfer copyright, trademark or other intellectual +property rights. + +3. No credentials or access grant + +This package contains no access token and grants no access to SHD data. Access +is controlled by SHD authentication, organization membership and server-side +permissions. + +4. Warranty and liability + +The package is provided "as is" to the maximum extent permitted by law, +without warranties of any kind. SHD is not liable for indirect, incidental or +consequential damages arising from its use. + +5. Third-party materials + +Third-party software and assets, if any, remain subject to their own licenses. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d8e4db0 --- /dev/null +++ b/README.md @@ -0,0 +1,189 @@ +# SHD MCP Plugin + +Public installable plugin package for SHD project workflows. It combines Codex +Skills with the authenticated SHD MCP server and keeps business logic, ACLs, +OAuth and persisted data in SHD itself. + +## What the user installs + +The package contains: + +- `.codex-plugin/plugin.json` — plugin metadata; +- `.mcp.json` — the official SHD Streamable HTTP MCP endpoint; +- `skills/` — routing, project, ProjectBase, task, file, finance, CRM, + analytics, estimates, scheduling, entity resolution, Wiki, discussions, + documents, organizations/ACL, notifications, inventory, agents, status-page, + realtime/activity, Terms/contracts and Gitea workflows; +- `assets/` — plugin branding; +- `widgets/` — versioned MCP Apps resources with manifest, checksum and + provenance metadata; +- `.agents/plugins/marketplace.json` — a ready local marketplace entry. + +It does not contain the SHD Laravel application, database code or credentials. + +## Install in Codex + +Clone this repository and register its marketplace: + +```bash +git clone https://github.com/bulava92/shd-mcp-plugin.git +cd shd-mcp-plugin +codex plugin marketplace add "$PWD" +codex plugin add shd-mcp-plugin@shd-public +``` + +After installation, start a new Codex thread so the plugin Skills are loaded. + +## ChatGPT custom app + +ChatGPT uses the same SHD MCP server connection, but it is added separately in +the ChatGPT account: open Settings → Apps/Connectors → Developer mode, create +a custom app and set the MCP URL to `https://shd.xyz.su/mcp`. Complete the SHD +OAuth flow and refresh the app after server metadata changes. + +The active-projects result can render an inline MCP Apps widget. It is a small +dashboard with: + +- `Проект → Дата завершения → Статус`; +- sorting by the nearest deadline; +- local search by project name/code; +- a refresh action, a project-details panel and controlled project actions. + +If the host does not support MCP Apps UI, the data tool and the plain Markdown +table remain fully usable. + +The backend serves the generated widget artifact at +`ui://shd/active-projects/v1.html`. Generate the ignored runtime files before +starting the backend: + +```sh +node scripts/generate-widget-artifacts.mjs +``` + +The source is kept in `widgets/active-projects/source.html` and +`widgets/active-projects/widget.json`; generated `v1/` files are not committed. + +The same data/render contract is available for Documents, Tasks, +Finance, CRM, Discussions, Notifications, Scheduling, Inventory, Agents, +Status Page, AirBase, Notes, Project overview, Files, Proposals, Activity, +Organizations and access, Gitea Issues and PDF Scheme Editor: + +Controller observability uses one parameterized resource for device inventory, +current state, server-computed statistics, source history, cross-controller +comparisons, expert settings, complete project topology, telemetry health and +source audit: + +- `shd_render_controller_history_widget` → `ui://shd/controller-observability/v1.html`; +- `shd_render_controller_statistics_widget` → `ui://shd/controller-observability/v1.html`; +- `shd_render_controller_devices_widget` → `ui://shd/controller-observability/v1.html`; +- `shd_render_controller_live_state_widget` → `ui://shd/controller-observability/v1.html`; +- `shd_render_controller_compare_widget` → `ui://shd/controller-observability/v1.html`; +- `shd_render_controller_settings_widget` → `ui://shd/controller-observability/v1.html`; +- `shd_render_controller_topology_widget` → `ui://shd/controller-observability/v1.html`; +- `shd_render_controller_telemetry_health_widget` → `ui://shd/controller-observability/v1.html`; +- `shd_render_controller_audit_widget` → `ui://shd/controller-observability/v1.html`. + +The controller topology keeps Loxone/WB source controllers separate from +Raspberry Pi and mini PC controller rows without source inventory; it does not +substitute the separate agent register for the project controller list. +The agent overview is the inverse boundary: it lists agents/runtime access and +does not become a controller list merely because an agent runs on a controller. + +- `shd_render_documents_widget` → `ui://shd/documents/v1.html`; +- `shd_render_tasks_widget` → `ui://shd/tasks/v1.html`; +- `shd_render_finance_widget` → `ui://shd/finance/v1.html`; +- `shd_render_crm_widget` → `ui://shd/crm/v1.html`; +- `shd_render_discussions_widget` → `ui://shd/discussions/v1.html`; +- `shd_render_notifications_widget` → `ui://shd/notifications/v1.html`; +- `shd_render_scheduling_widget` → `ui://shd/scheduling/v1.html`; +- `shd_render_inventory_widget` → `ui://shd/inventory/v1.html`; +- `shd_render_agents_widget` → `ui://shd/agents/v1.html`; +- `shd_render_status_page_widget` → `ui://shd/status-page/v1.html`; +- `shd_render_airbase_widget` → `ui://shd/airbase/v1.html`; +- `shd_render_notes_widget` → `ui://shd/notes/v1.html`; +- `shd_render_project_overview_widget` → `ui://shd/project-overview/v1.html`; +- `shd_render_files_widget` → `ui://shd/files/v1.html`; +- `shd_render_proposals_widget` → `ui://shd/proposals/v1.html`; +- `shd_render_activity_widget` → `ui://shd/activity/v1.html`; +- `shd_render_organizations_widget` → `ui://shd/organizations/v1.html`; +- `shd_render_gitea_widget` → `ui://shd/gitea/v1.html`; +- `shd_render_schemes_widget` → `ui://shd/pdf-scheme-editor/v1.html`; +- `shd_render_task_kanban_widget` → `ui://shd/task-kanban/v1.html`; +- `shd_render_team_workload_widget` → `ui://shd/team-workload/v1.html`; +- `shd_render_project_timeline_widget` → `ui://shd/project-timeline/v1.html`; +- `shd_render_crm_funnel_widget` → `ui://shd/crm-funnel/v1.html`; +- `shd_render_finance_dashboard_widget` → `ui://shd/finance-dashboard/v1.html`; +- `shd_render_proposal_approvals_widget` → `ui://shd/proposal-approvals/v1.html`; +- `shd_render_documents_completeness_widget` → `ui://shd/documents-completeness/v1.html`; +- `shd_render_agents_health_widget` → `ui://shd/agents-health/v1.html`; +- `shd_render_schedule_calendar_widget` → `ui://shd/schedule-calendar/v1.html`; +- `shd_render_acl_matrix_widget` → `ui://shd/acl-matrix/v1.html`; +- `shd_render_inventory_warnings_widget` → `ui://shd/inventory-warnings/v1.html`; +- `shd_render_gitea_board_widget` → `ui://shd/gitea-board/v1.html`; +- `shd_render_schemes_progress_widget` → `ui://shd/pdf-scheme-editor-progress/v1.html`; +- `shd_render_notes_tree_widget` → `ui://shd/notes-tree/v1.html`; +- `shd_render_airbase_preview_widget` → `ui://shd/airbase-preview/v1.html`. + +Each widget accepts normalized records from its list tool, keeps the plain +structured response as a fallback and uses the MCP Apps bridge before the +`window.openai` compatibility API. + +The official OpenAI directory is optional. It is useful for public discovery, +one-click installation and review of a public app; it is not required for an +internal team or a manually added ChatGPT custom app. See the official +[MCP server guide](https://developers.openai.com/plugins/build/mcp-server), +[UI guide](https://developers.openai.com/plugins/build/chatgpt-ui), and +[submission guide](https://developers.openai.com/plugins/deploy/submission) +when public listing is the goal. + +## Connect SHD + +Installation and authorization are separate steps: + +1. The client reads `.mcp.json` and opens the SHD OAuth flow. +2. The user signs in to SHD and approves the requested MCP connection. +3. SHD issues the client an authorization; the client stores it securely. +4. Every tool call is checked again by SHD for organization, project and + module permissions. + +The user does not need to put a token in this repository. A personal access +token may be used only if the selected MCP client and SHD deployment +explicitly support it; enter it in that client's secure connection settings, +never in `.mcp.json`, Skills or an issue. + +SHD OAuth supports ChatGPT CIMD clients with PKCE S256 and +`private_key_jwt` client authentication. The server validates the client +assertion against ChatGPT's published JWKS; the plugin package never stores +the assertion, access token or refresh token. + +For ChatGPT or another MCP client without Codex plugin installation, add the +same HTTPS endpoint as a custom MCP app and complete OAuth there. The public +repository packages the workflows; it does not grant access to the server. + +## Troubleshooting + +- **Plugin installs but tools are unavailable:** reconnect SHD OAuth and start + a new thread. +- **Projects are missing:** the SHD account or organization lacks access, or + the project is archived; verify permissions in SHD. +- **A write is rejected:** the server ACL, required role, version or conflict + check rejected it. Do not bypass the error with a different token. +- **A module is not covered by a dedicated skill:** use `shd-routing`; the MCP + server still exposes the current authenticated tool catalog, while skills + provide focused workflow and safety guidance for common module operations. +- **Another SHD installation is required:** use its approved MCP URL and its + OAuth resource in the client's secure connection configuration. + +## Development + +Keep changes inside the plugin package. The authenticated MCP server, OAuth +deployment and runtime verification remain private server-side concerns. Do not +place credentials, private host details or backend implementation in Skills or +widget assets. + +## License + +The repository is public source, but the plugin and SHD branding are distributed +under the proprietary [SHD MCP Plugin License](LICENSE). Public visibility does +not grant rights to redistribute, resell, modify or reuse the package outside +the permitted SHD use. diff --git a/plugins/shd-mcp-plugin/.codex-plugin/plugin.json b/plugins/shd-mcp-plugin/.codex-plugin/plugin.json new file mode 100644 index 0000000..14e1af4 --- /dev/null +++ b/plugins/shd-mcp-plugin/.codex-plugin/plugin.json @@ -0,0 +1,49 @@ +{ + "name": "shd-mcp-plugin", + "version": "0.5.1", + "description": "Ready-to-install SHD workflows backed by the authenticated SHD MCP server.", + "author": { + "name": "SHD", + "url": "https://shd.xyz.su" + }, + "homepage": "https://shd.xyz.su", + "repository": "https://github.com/bulava92/shd-mcp-plugin", + "license": "Proprietary", + "keywords": [ + "SHD", + "projects", + "ProjectBase", + "tasks", + "finance", + "CRM", + "documents", + "inventory", + "organizations", + "notifications", + "realtime", + "Gitea", + "MCP" + ], + "skills": "./skills/", + "interface": { + "displayName": "SHD MCP", + "shortDescription": "SHD projects and operations", + "longDescription": "Installable SHD MCP workflows for projects, ProjectBase, tasks, files, finance, CRM, documents, access control, inventory, notifications, realtime, status monitoring and linked Gitea operations.", + "developerName": "SHD", + "category": "Productivity", + "capabilities": [ + "Read", + "Write" + ], + "websiteURL": "https://shd.xyz.su", + "brandColor": "#1E88E5", + "composerIcon": "./assets/logo.png", + "logo": "./assets/logo.png", + "defaultPrompt": [ + "Покажи активные проекты с датой завершения и статусом.", + "Сделай сводку по проекту и его рискам.", + "Проверь ProjectBase без изменения данных." + ] + }, + "mcpServers": "./.mcp.json" +} diff --git a/plugins/shd-mcp-plugin/.mcp.json b/plugins/shd-mcp-plugin/.mcp.json new file mode 100644 index 0000000..4e7ada6 --- /dev/null +++ b/plugins/shd-mcp-plugin/.mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "shd": { + "type": "http", + "url": "https://shd.xyz.su/mcp", + "oauth_resource": "https://shd.xyz.su/mcp" + } + } +} diff --git a/plugins/shd-mcp-plugin/README.md b/plugins/shd-mcp-plugin/README.md new file mode 100644 index 0000000..71f0282 --- /dev/null +++ b/plugins/shd-mcp-plugin/README.md @@ -0,0 +1,99 @@ +# SHD MCP plugin package + +This directory is the installable Codex plugin. It packages reusable SHD +workflow Skills and the official SHD MCP connection; it does not contain the +SHD backend or a second MCP server. + +The plugin requires an SHD account authorized for the requested projects and +modules. Skills do not grant permissions or bypass server-side ACLs. + +## Included workflows + +- active projects with completion dates and statuses; +- project status and risk summaries; +- ProjectBase schema and data-quality audits; +- guarded task workflows; +- project file inspection and actions; +- finance and CRM read/audit workflows; +- finance account, balance, payment and settlement audits; +- analytics, comparisons, reports and task/proposal metrics; +- entity resolution and duplicate-candidate analysis; +- estimates, proposals and estimate-document-contract workflows; +- scheduling events, slots and booking positions; +- Wiki, notes, revisions and note-database workflows; +- discussions, channels, topics, messages and attachments; +- documents, templates, PDFs, revisions and public-link workflows; +- organizations, members, invitations, ACL and two-factor policies; +- notifications, preferences and read-state workflows; +- inventory, assets, stock, procurement and stocktake workflows; +- agent/controller diagnostics, tunnels, endpoints and runtime actions; +- status-page monitors, groups, history and incidents; +- realtime sessions, event cursors and operational activity; +- Terms documents, revisions, completeness and contract discussions; +- linked Gitea issues, labels, comments and attachments; +- safe bulk-change planning and validation rules. + +When the connected MCP host supports MCP Apps UI, the 44 SHD render tools have +inline interactive widgets: active projects, module registers and specialized +Kanban, workload, timeline, funnel, dashboard, calendar, matrix, tree and +Project DB views. Each widget sorts or filters normalized records, shows +status/value fields, can load one selected record’s details where supported and +exposes only the write actions declared by its render metadata. A write always +requires an explicit form submission; destructive actions also require a +confirmation checkbox. After a successful mutation, the widget refreshes its +source list. +The same tools still return normal structured data for hosts that do not render +widgets. + +The controller observability render tools share the versioned +`widgets/controller-observability/v1/` resource. It exposes controller device +inventory and profiles, normalized live states, server-computed binary/numeric +statistics, explicit native vs Influx/Grafana provenance, universal comparison +presets, complete controller topology, telemetry health and operation audit. +Expert-setting controls are enabled only for source-provided writable descriptors. + +The active-projects source is `widgets/active-projects/source.html` with its +resource metadata in `widgets/active-projects/widget.json`. Before starting +the backend, generate the ignored runtime files with: + +```sh +node scripts/generate-widget-artifacts.mjs +``` + +The backend then reads the generated `widgets/active-projects/v1/` artifact for +the `ui://shd/active-projects/v1.html` resource. + +The standard module resources are generated from the single +`widgets/_shared/universal-widget.html` source by +`scripts/generate-widget-artifacts.mjs`. Each generated artifact still keeps +its own URI, manifest, SHA256 checksum and provenance note: this preserves +resource cache keys and module-scope registration while removing duplicated +HTML, bridge and action runtime code. The generator also injects the shared +`widgets/_shared/widget-theme.css` and `widgets/_shared/write-actions.css` +into the specialized `active-projects` and `controller-observability` +renderers, so every widget uses the same spacing, surfaces, controls, tables +and responsive rules without changing their data contracts. The generator +supports `--check` for a no-write consistency check. Module copy and fallback +tool configuration lives in the sibling +`widgets/_shared/universal-widget-config.json` source and is injected into +each universal standalone artifact by the generator. For an older template +that still contains the inline configuration block, run `--extract-config` +once and then `--migrate-template` once. The specialized applications remain +separate only where their interaction contracts differ. Render tools are +deliberately separate from list tools so data access and UI rendering remain +auditable. + +The generated `widgets/*/v1/` directories are ignored and must not be +committed. A fresh checkout or deployment must run the generator before the +MCP backend starts; `--check` can then verify that the local runtime output is +current. + +## MCP connection + +The package points to `https://shd.xyz.su/mcp` and requests OAuth for that MCP +resource. The client stores the resulting authorization; this repository never +contains an access token. + +If a client or SHD installation uses another official endpoint, change the +connection in `.mcp.json` in that client's secure configuration. Do not commit +tokens, cookies, client secrets or private host credentials. diff --git a/plugins/shd-mcp-plugin/assets/logo.png b/plugins/shd-mcp-plugin/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9318b9901d5bb846e460fbdc48cef233f1b86c63 GIT binary patch literal 60059 zcmce6WmHt(_xCUiG1Lr=v`9-UQeR6DfMNkO{1B_#v|LApyoq>)leq@;V027zH1 z7-pzHzt_+E=e{`W-nI66JwlRb=iwxPy@*0caZuNvt?4@ zlS}fO)q<0X=8T`!(h0Tye%CK-rkwEjT&T1PL#}ajY~!kIbw=eZ!e$cNCJc_zzu)fk z0sweK%ADIGkd;%&`pNpq$v&4t%3f1UHeoUhK*@>DP@+|R|7A>_68Z%CQ;V`Iy&MnO z%!W03Yh_RMfhhH&7uti+y%+NT$3-+^5n!BS5DK_Z-07Mspe$=ylkJ$eCJEJe z_EvGJx~8L}qt-THb-AXcZ8$Oe1-@UavoY~uR z(~yJ-c`H{^p9G~tK2CU8qAgx*HOvmH7YejC%(c-~W*?>m9}kVKETT*WIINI-mFjM% ztJ-hCo?!Gca%ec}f={MX`G459nv{W3gdWQbovmPhSmijGuLJ?ts z0PM${h9MVp0EFJlt6%S0-jFwi{}g9Zkjs1)E((e33FphU#9Wiog_o%f64)+(p=!~3 zEYv?-dM?dnCcjqXuU(p|w$ajPc-%UcJAhW2c)tVW z357WKJp_60N#O1HY$~(7Go`YbypFP<(#c))!z~*EUMS%yY%51K|EepBa})8ij-AG9j?icE9> zza6?TWPa=4`dfquQ7m95M;n2L!6wchliT&q#0!T(mUH&?*(tVR6Ye0mUxM$IyUqJi zaz{(43DR!nv&N?kJ|t0Uibn371$<5g=M_J{pUZ*@|K&Y5D*FdH4ypD35AmTq=!583 ztiG8{eI9!ps3k7N44nHXZbwH0LIPu9yP zq-d;@Ba#LaS{X9F=IRY`A)qRaG5DZ#^!*O2TwD4O`Sz58?9P*4I7_{gtgUy&l!FSC z`A@CWatP8NULe=VGKnLPp>6p0^HNGkDJ(@i)8iErljGg{@vH64Qy!Nq-JSn{1t zW;rK3B;~0JzC0C-GF6FuMK}X(FfNDAWPMX0Q<@l`HwexC zeX8n-Z#_m9RCG0j^9B@hW^uUb^y3^3#NF98#_?ltDf{02q>m4=t5lbvdBGE%lM@eo zFKDPJe&{*?t?lzo;<%|d?SjdEgT`pvcuyo;y-wL#=RD&Ye8J)9D7dGOF)CA*9rI>) zY7E3pa^}m<)hq)P3PyS}!y6b*#B8qWc>;1fFHQ`f=zI>WKPbiZe07h;TpDL|P$;bj zFgqwxlRKi-|8~LfY}`fH{MXfyJ=f*>Ek@ zlnebwqex@y{%|nB=bP70*EiI@G<&`YpQ2;77XS4W5d??{AvD6!)ZzaP zkceQ#kHo27r}lF0_C#4#CzoHl-0TaZy4Y{$$hV|Dly?b7v$wn4(lz-^Bt}+p0CdfSVsd$p|Edrl` zbAk0m5UU^N*O?xUU(=~h+G$lbXovf&RxZ6qJDC`D{)_TMYt6_uMgqE7YA4u(DH|#d z7Ci5yh}@nn-Ck0i@r0Tg%wB??l1uB^dRExxNcUFAd6^Z*2R*6CkE9!l$wHrt-;hUo zB1BShPSUP*0cJ`y-*DfS3l&n=z4EoMgkyHKjo<%k{a}pe^AMQZrsag!&tMaamq8Zc z;*vM)`yY0;fQ z?T6d77(q&?BpJU1x`!6osdZjQwQ}U?(d1>n@OxWCcALwJiNL(*5lQoFip2D6FuAPr zbt2yt+8qR&0iD#ecdhSurwrv_TskgL?bRpY>~>U1Ln3s5?k)q^UayBRM_FJtdM&a! z=GESQE&K+&@TFz4`_hh$!1a;{x8A_bS!{3V=;^L!Hc*1kLwp8gJOjBQLBqR$?~Xq3 za&UasciU;bI=`67iVZ0$zM--~

lnJ8VB(TXW5JD&Iht@$O2BHqH)xL-Nm2tJ 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�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 + + + + + SHD MCP + + + + +

+
SHD MCP

SHD

+
+
+
+ +
+ + + + diff --git a/plugins/shd-mcp-plugin/widgets/_shared/widget-theme.css b/plugins/shd-mcp-plugin/widgets/_shared/widget-theme.css new file mode 100644 index 0000000..6129164 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/_shared/widget-theme.css @@ -0,0 +1,1236 @@ +:root { + color-scheme: light dark; + --shd-bg: #f3f6f9; + --shd-surface: #ffffff; + --shd-surface-raised: #ffffff; + --shd-surface-subtle: #eef2f6; + --shd-line: #d6dee7; + --shd-line-strong: #b9c5d1; + --shd-ink: #17202a; + --shd-muted: #66717e; + --shd-accent: #496581; + --shd-accent-strong: #35516d; + --shd-accent-soft: rgba(73, 101, 129, .12); + --shd-on-accent: #ffffff; + --shd-good: #19734b; + --shd-good-soft: #e2f3eb; + --shd-warning: #946000; + --shd-warning-soft: #fff2cf; + --shd-danger: #b42318; + --shd-danger-soft: #ffebe9; + --shd-shadow: 0 1px 2px rgba(15, 23, 42, .05), 0 12px 30px rgba(15, 23, 42, .08); + --shd-shadow-soft: 0 1px 2px rgba(15, 23, 42, .05); + --shd-radius-sm: 10px; + --shd-radius-md: 14px; + --shd-radius-lg: 20px; + --shd-space-1: 4px; + --shd-space-2: 8px; + --shd-space-3: 12px; + --shd-space-4: 16px; + --shd-space-5: 20px; + --shd-space-6: 24px; + --shd-space-7: 32px; + + /* Compatibility aliases for existing widget renderers and write actions. */ + --bg: var(--shd-bg); + --surface: var(--shd-surface); + --panel: var(--shd-surface); + --hover: var(--shd-surface-subtle); + --surface-hover: var(--shd-surface-subtle); + --line: var(--shd-line); + --border: var(--shd-line); + --ink: var(--shd-ink); + --text: var(--shd-ink); + --muted: var(--shd-muted); + --accent: var(--shd-accent); + --soft: var(--shd-accent-soft); + --accent-soft: var(--shd-accent-soft); + --good: var(--shd-good); + --success: var(--shd-good); + --success-soft: var(--shd-good-soft); + --warning: var(--shd-warning); + --warning-soft: var(--shd-warning-soft); + --bad: var(--shd-danger); + --danger: var(--shd-danger); + --danger-soft: var(--shd-danger-soft); + --button: var(--shd-on-accent); + --shadow: var(--shd-shadow); +} + +@media (prefers-color-scheme: dark) { + :root { + --shd-bg: #15181c; + --shd-surface: #1e2328; + --shd-surface-raised: #262c32; + --shd-surface-subtle: #2d343b; + --shd-line: #3d464f; + --shd-line-strong: #56616c; + --shd-ink: #f1f4f7; + --shd-muted: #a5adb6; + --shd-accent: #d8b56a; + --shd-accent-strong: #edc97d; + --shd-accent-soft: rgba(216, 181, 106, .16); + --shd-on-accent: #211b10; + --shd-good: #83d2a4; + --shd-good-soft: rgba(71, 164, 111, .22); + --shd-warning: #f2c66d; + --shd-warning-soft: rgba(196, 139, 40, .22); + --shd-danger: #ff9a93; + --shd-danger-soft: rgba(196, 61, 50, .22); + --shd-shadow: 0 1px 2px rgba(0, 0, 0, .22), 0 16px 34px rgba(0, 0, 0, .24); + --shd-shadow-soft: 0 1px 2px rgba(0, 0, 0, .22); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + min-width: 0; + background: var(--shd-bg); +} + +body { + min-width: 0; + margin: 0; + background: var(--shd-bg); + color: var(--shd-ink); + font: 14px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +button, +input, +select, +textarea { + font: inherit; +} + +button { + color: inherit; +} + +.shell, +.app { + width: 100%; + max-width: 1120px; + margin: 0 auto; + padding: clamp(14px, 2.5vw, 28px); +} + +.card, +.app { + min-width: 0; + padding: clamp(18px, 2.6vw, 30px); + border: 1px solid var(--shd-line); + border-radius: var(--shd-radius-lg); + background: var(--shd-surface); + box-shadow: var(--shd-shadow); +} + +.card > * + *, +.app > * + *, +.shell > * + *, +.visual > * + * { + margin-block-start: var(--shd-space-5); +} + +.card > .eyebrow + h1, +.card > h1 + .lede, +.app > .header + .toolbar { + margin-block-start: var(--shd-space-1); +} + +.card > .summary:empty, +.card > .status:empty, +.app > .summary:empty, +.app > .status:empty { + display: none; +} + +.eyebrow { + color: var(--shd-muted); + font-size: 11px; + font-weight: 750; + letter-spacing: .12em; + text-transform: uppercase; +} + +h1, +h2, +h3, +p { + overflow-wrap: anywhere; +} + +h1 { + margin: 0; + font-size: clamp(22px, 2.5vw, 30px); + line-height: 1.15; + letter-spacing: -.02em; +} + +.lede, +.subtitle { + margin: 0; + color: var(--shd-muted); +} + +.header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--shd-space-5); +} + +.toolbar { + display: flex; + align-items: center; + gap: var(--shd-space-2); + flex-wrap: wrap; +} + +.toolbar > .search { + flex: 1 1 220px; +} + +.search, +.toolbar input, +.toolbar select, +.setting input, +.setting select, +.shd-widget-actions-select, +.shd-widget-actions-input { + min-width: 0; + border: 1px solid var(--shd-line-strong); + border-radius: var(--shd-radius-sm); + background: var(--shd-bg); + color: var(--shd-ink); + outline: 0; +} + +.search, +.toolbar input, +.toolbar select { + padding: 10px 12px; +} + +.toolbar select { + min-width: 150px; +} + +.search:focus, +.toolbar input:focus, +.toolbar select:focus, +.setting input:focus, +.setting select:focus, +.shd-widget-actions-select:focus, +.shd-widget-actions-input:focus { + border-color: var(--shd-accent); + box-shadow: 0 0 0 3px var(--shd-accent-soft); +} + +button { + cursor: pointer; +} + +button:focus-visible, +[role="button"]:focus-visible { + outline: 3px solid var(--shd-accent-soft); + outline-offset: 2px; +} + +.button, +.refresh, +.toolbar button, +.actions button, +.inline-action { + border: 1px solid var(--shd-line-strong); + border-radius: var(--shd-radius-sm); + padding: 9px 13px; + background: var(--shd-surface-raised); + color: var(--shd-ink); + font-weight: 650; + white-space: nowrap; +} + +.button:hover, +.toolbar button:hover, +.actions button:hover, +.inline-action:hover { + border-color: var(--shd-accent); + background: var(--shd-surface-subtle); +} + +.refresh, +.toolbar .primary, +.actions button, +.inline-action, +.app #refresh { + border-color: var(--shd-accent); + background: var(--shd-accent); + color: var(--shd-on-accent); +} + +.refresh:hover, +.toolbar .primary:hover, +.actions button:hover, +.inline-action:hover, +.app #refresh:hover { + border-color: var(--shd-accent-strong); + background: var(--shd-accent-strong); +} + +.refresh:disabled, +.toolbar button:disabled, +.actions button:disabled, +.button:disabled, +.inline-action:disabled { + cursor: wait; + opacity: .58; +} + +.status { + min-height: 0; + color: var(--shd-muted); +} + +.status.error, +.error { + color: var(--shd-danger); +} + +.summary { + display: flex; + flex-wrap: wrap; + gap: var(--shd-space-2); +} + +.summary span, +.summary-item, +.pill { + display: inline-flex; + align-items: center; + max-width: 100%; + min-height: 26px; + padding: 4px 9px; + overflow: hidden; + border: 1px solid transparent; + border-radius: 999px; + background: var(--shd-surface-subtle); + color: var(--shd-muted); + font-size: 12px; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.summary strong, +.summary-item strong { + margin-inline-end: 4px; + color: var(--shd-ink); + font-weight: 700; +} + +.pill.good, +.pill.up, +.pill.ok, +.pill.active, +.status-done { + border-color: var(--shd-line); + background: var(--shd-good-soft); + color: var(--shd-good); +} + +.pill.bad, +.pill.down, +.pill.error, +.pill.overdue, +.status-danger { + border-color: var(--shd-line); + background: var(--shd-danger-soft); + color: var(--shd-danger); +} + +.status-progress { + background: var(--shd-accent-soft); + color: var(--shd-accent-strong); +} + +.status-neutral { + background: var(--shd-surface-subtle); + color: var(--shd-muted); +} + +.status-warning { + background: var(--shd-warning-soft); + color: var(--shd-warning); +} + +.table-wrap, +.table, +.visual, +.matrix-wrap { + min-width: 0; + overflow: auto; + border: 1px solid var(--shd-line-strong); + border-radius: var(--shd-radius-md); + background: var(--shd-surface-raised); + box-shadow: var(--shd-shadow-soft); +} + +.table-wrap, +.visual { + max-height: 520px; +} + +.visual { + padding: var(--shd-space-3); +} + +table { + width: 100%; + min-width: 560px; + border-collapse: separate; + border-spacing: 0; +} + +th, +td { + padding: 13px 15px; + border-bottom: 1px solid var(--shd-line); + text-align: left; + vertical-align: top; +} + +th { + position: sticky; + top: 0; + z-index: 1; + background: var(--shd-surface-subtle); + color: var(--shd-muted); + font-size: 12px; + font-weight: 700; + letter-spacing: .01em; +} + +tbody tr:last-child td, +tr:last-child td { + border-bottom: 0; +} + +tbody tr:hover, +.record:hover, +.record:focus, +.project-row:hover { + background: var(--shd-accent-soft); +} + +.record { + cursor: pointer; + outline: 0; +} + +.title, +.project-title, +.visual-title { + font-weight: 700; +} + +.sub, +.project-code, +.visual-date, +.visual-value, +.muted, +.loading { + color: var(--shd-muted); +} + +.sub, +.project-code { + display: block; + margin-top: var(--shd-space-1); + font-size: 12px; +} + +.empty { + padding: var(--shd-space-6) var(--shd-space-4); + color: var(--shd-muted); + text-align: center; +} + +.visual[hidden], +.details[hidden], +.error[hidden] { + display: none; +} + +.visual-card { + display: block; + width: 100%; + min-width: 0; + padding: 14px 15px; + border: 1px solid var(--shd-line); + border-radius: var(--shd-radius-sm); + background: var(--shd-surface-raised); + color: inherit; + text-align: left; + cursor: pointer; +} + +.visual-card:hover, +.visual-card:focus { + border-color: var(--shd-accent); + background: var(--shd-surface-subtle); + outline: 0; +} + +.visual-title { + display: block; + overflow-wrap: anywhere; +} + +.visual-date, +.visual-value { + display: block; + margin-top: var(--shd-space-1); + font-size: 12px; +} + +.visual-line { + display: flex; + align-items: center; + gap: var(--shd-space-2); + flex-wrap: wrap; + margin-top: var(--shd-space-2); +} + +.grid, +.metric-grid, +.preview-grid, +.calendar, +.kanban { + display: grid; + min-width: 0; + gap: var(--shd-space-4); +} + +#content { + display: grid; + min-width: 0; + gap: var(--shd-space-5); +} + +.grid, +.preview-grid { + grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); +} + +.metric-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.panel, +.metric, +.progress-card, +.preview-card, +.kanban-col, +.calendar-day, +.detail { + min-width: 0; + border: 1px solid var(--shd-line); + border-radius: var(--shd-radius-md); + background: var(--shd-surface-raised); + box-shadow: var(--shd-shadow-soft); +} + +.panel { + padding: var(--shd-space-5); +} + +.panel h2, +.panel h3 { + margin: 0 0 var(--shd-space-3); + color: var(--shd-ink); + font-size: 16px; + line-height: 1.25; +} + +.panel p { + margin: 0; + color: var(--shd-muted); +} + +.metric { + padding: var(--shd-space-4); +} + +.metric-label { + display: block; + color: var(--shd-muted); + font-size: 12px; + font-weight: 600; +} + +.metric-value { + display: block; + margin-top: var(--shd-space-2); + color: var(--shd-ink); + font-size: clamp(22px, 3vw, 30px); + font-weight: 750; + letter-spacing: -.02em; + overflow-wrap: anywhere; +} + +.metric p { + margin: var(--shd-space-2) 0 0; + color: var(--shd-ink); + font-size: clamp(20px, 2.7vw, 28px); + font-weight: 750; + letter-spacing: -.02em; +} + +.kanban { + grid-auto-flow: column; + grid-auto-columns: minmax(210px, 1fr); + align-items: start; + overflow-x: auto; +} + +.kanban-col { + min-width: 210px; + padding: var(--shd-space-3); +} + +.kanban-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--shd-space-2); + margin-bottom: var(--shd-space-3); + font-weight: 700; +} + +.kanban-list, +.source-list, +.tree { + display: grid; + gap: var(--shd-space-2); +} + +.metric-grid + .kanban-list { + margin-top: var(--shd-space-1); +} + +.timeline { + display: grid; + gap: 0; +} + +.timeline-item { + display: grid; + position: relative; + grid-template-columns: 18px minmax(0, 1fr); + gap: var(--shd-space-3); + padding-bottom: var(--shd-space-4); +} + +.timeline-rail { + position: absolute; + top: 16px; + bottom: 0; + left: 8px; + width: 2px; + background: var(--shd-line); +} + +.timeline-item:last-child .timeline-rail { + display: none; +} + +.timeline-dot { + z-index: 1; + width: 16px; + height: 16px; + border: 3px solid var(--shd-accent-soft); + border-radius: 50%; + background: var(--shd-accent); +} + +.timeline-content { + min-width: 0; +} + +.timeline-meta { + margin-bottom: var(--shd-space-2); + color: var(--shd-muted); + font-size: 12px; +} + +.funnel { + display: grid; + gap: var(--shd-space-3); +} + +.funnel-row { + display: grid; + grid-template-columns: minmax(100px, 1fr) minmax(120px, 2fr) auto; + align-items: center; + gap: var(--shd-space-3); +} + +progress { + width: 100%; + height: 10px; + overflow: hidden; + border: 0; + border-radius: 999px; + background: var(--shd-surface-subtle); + accent-color: var(--shd-accent); +} + +progress::-webkit-progress-bar { + border-radius: 999px; + background: var(--shd-surface-subtle); +} + +progress::-webkit-progress-value { + border-radius: 999px; + background: var(--shd-accent); +} + +progress::-moz-progress-bar { + border-radius: 999px; + background: var(--shd-accent); +} + +.progress-card { + display: grid; + gap: var(--shd-space-2); + padding: var(--shd-space-4); +} + +.preview-card { + padding: var(--shd-space-4); +} + +.preview-fields { + display: grid; + gap: var(--shd-space-2); + margin-top: var(--shd-space-3); +} + +.preview-field { + overflow-wrap: anywhere; + font-size: 12px; +} + +.preview-field strong { + color: var(--shd-muted); + font-weight: 600; +} + +.calendar { + grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); +} + +.calendar-day { + display: grid; + align-content: start; + gap: var(--shd-space-3); + padding: var(--shd-space-3); +} + +.calendar-head { + font-weight: 700; +} + +.matrix-wrap { + background: var(--shd-surface-raised); +} + +.matrix-table { + min-width: 600px; +} + +.matrix-table th, +.matrix-table td { + padding: 12px 14px; +} + +.tree-children { + display: grid; + gap: var(--shd-space-2); + margin-left: var(--shd-space-3); + padding-left: var(--shd-space-4); + border-left: 1px solid var(--shd-line); +} + +.details { + min-width: 0; + padding: var(--shd-space-5); + border: 1px solid var(--shd-line-strong); + border-radius: var(--shd-radius-md); + background: var(--shd-surface-subtle); + box-shadow: var(--shd-shadow-soft); +} + +.detail-head, +.details-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--shd-space-4); +} + +.details h2, +.details-title { + margin: 0; + font-size: 18px; + line-height: 1.25; +} + +.details-grid, +.grid { + margin-top: var(--shd-space-4); +} + +.detail { + padding: var(--shd-space-3); +} + +.label, +.detail-label { + display: block; + margin-bottom: var(--shd-space-1); + color: var(--shd-muted); + font-size: 11px; +} + +.value, +.detail-value { + display: block; + overflow-wrap: anywhere; + white-space: pre-wrap; +} + +.close { + border: 0; + background: transparent; + color: var(--shd-muted); + font-size: 20px; +} + +.chart { + min-height: 176px; + margin: var(--shd-space-3) 0 0; + padding: var(--shd-space-2); + overflow: hidden; + border: 1px solid var(--shd-line); + border-radius: var(--shd-radius-sm); + background: var(--shd-surface-subtle); +} + +.chart svg { + display: block; + width: 100%; + height: 160px; +} + +.chart-empty { + display: grid; + height: 156px; + place-items: center; + color: var(--shd-muted); + font-size: 12px; +} + +.axis { + fill: var(--shd-muted); + font-size: 10px; +} + +.chart-grid-line { + stroke: var(--shd-line); + stroke-dasharray: 3 4; + stroke-width: 1; +} + +.chart-axis-line { + stroke: var(--shd-line-strong); + stroke-width: 1; +} + +.chart-x-label, +.chart-y-label { + fill: var(--shd-muted); + font-size: 10px; + font-variant-numeric: tabular-nums; +} + +.chart-line { + fill: none; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.4; + vector-effect: non-scaling-stroke; +} + +.line-a, +.line-b { + fill: none; +} + +.line-a { + stroke: #5e91c0; +} + +.line-b { + stroke: #d48655; +} + +.line-c { + stroke: #73b987; +} + +.line-d { + stroke: #b58bd9; +} + +.line-e { + stroke: #d9ad5c; +} + +.line-f { + stroke: #63b9c9; +} + +.comparison-chart { + min-height: 260px; +} + +.comparison-chart svg { + height: 232px; +} + +.chart-legend { + display: grid; + gap: var(--shd-space-2); + margin-top: var(--shd-space-3); +} + +.chart-legend-row { + display: grid; + grid-template-columns: auto minmax(150px, 1fr) repeat(4, auto); + align-items: center; + gap: var(--shd-space-3); + min-width: 0; + padding: 10px 12px; + border: 1px solid var(--shd-line); + border-radius: var(--shd-radius-sm); + background: var(--shd-surface-subtle); +} + +.chart-swatch { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.chart-swatch.line-a { + background: #5e91c0; +} + +.chart-swatch.line-b { + background: #d48655; +} + +.chart-swatch.line-c { + background: #73b987; +} + +.chart-swatch.line-d { + background: #b58bd9; +} + +.chart-swatch.line-e { + background: #d9ad5c; +} + +.chart-swatch.line-f { + background: #63b9c9; +} + +.chart-legend-name, +.chart-legend-stat { + min-width: 0; + overflow-wrap: anywhere; +} + +.chart-legend-name { + color: var(--shd-ink); + font-weight: 700; +} + +.chart-legend-meta { + display: block; + margin-top: var(--shd-space-1); + color: var(--shd-muted); + font-size: 11px; + font-weight: 400; +} + +.chart-legend-stat { + color: var(--shd-ink); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.chart-legend-stat-label { + margin-right: var(--shd-space-1); + color: var(--shd-muted); + font-size: 11px; +} + +.source-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--shd-space-3); + min-width: 0; + padding: 10px 12px; + border: 1px solid var(--shd-line); + border-radius: var(--shd-radius-sm); + background: var(--shd-surface-subtle); +} + +.source-row > * { + min-width: 0; + overflow-wrap: anywhere; +} + +.setting { + display: grid; + grid-template-columns: minmax(130px, 1fr) minmax(150px, 1fr); + align-items: center; + gap: var(--shd-space-3); + padding: var(--shd-space-3) 0; + border-bottom: 1px solid var(--shd-line); +} + +.setting:last-child { + border-bottom: 0; +} + +.setting label { + font-weight: 700; +} + +.setting small { + display: block; + color: var(--shd-muted); + font-weight: 400; +} + +.setting input, +.setting select { + width: 100%; + padding: 9px 10px; +} + +.setting input:disabled, +.setting select:disabled { + opacity: .6; +} + +.actions { + display: flex; + justify-content: flex-end; + gap: var(--shd-space-2); + margin-top: var(--shd-space-4); +} + +.inline-action { + padding: 6px 9px; + font-size: 12px; +} + +/* Active-projects keeps its own data renderer but shares this visual contract. */ +.app { + max-width: 1040px; +} + +.app .header h1 { + font-size: clamp(21px, 2.4vw, 28px); +} + +.app .subtitle { + margin-top: var(--shd-space-1); + font-size: 13px; +} + +.app .table { + overflow: hidden; +} + +.app .table-head, +.app .project-row { + display: grid; + grid-template-columns: minmax(0, 1.7fr) minmax(130px, .8fr) minmax(110px, .7fr); + align-items: center; + gap: var(--shd-space-3); +} + +.app .table-head { + padding: 11px 16px; + background: var(--shd-surface-subtle); + color: var(--shd-muted); + font-size: 11px; + font-weight: 700; + letter-spacing: .04em; + text-transform: uppercase; +} + +.app .project-row { + width: 100%; + padding: 14px 16px; + border: 0; + border-top: 1px solid var(--shd-line); + background: var(--shd-surface-raised); + text-align: left; +} + +.app .project-name, +.app .project-title { + min-width: 0; +} + +.app .project-title { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.app .deadline { + color: var(--shd-ink); + font-size: 13px; +} + +.app .deadline.missing { + color: var(--shd-muted); +} + +.app .deadline.overdue { + color: var(--shd-danger); + font-weight: 700; +} + +.app .status { + display: inline-flex; + width: fit-content; + max-width: 100%; + padding: 5px 9px; + overflow: hidden; + border-radius: 999px; + font-size: 12px; + font-weight: 650; + text-overflow: ellipsis; + white-space: nowrap; +} + +.app .details { + background: var(--shd-surface-subtle); +} + +@media (max-width: 720px) { + .shell, + .app { + padding: 12px; + } + + .card, + .app { + padding: 16px; + border-radius: var(--shd-radius-md); + } + + .header { + display: block; + } + + .header .button, + .header #refresh { + margin-top: var(--shd-space-3); + } + + .toolbar > * { + flex: 1 1 180px; + } + + .toolbar .refresh, + .toolbar button, + .toolbar select { + flex-grow: 0; + } + + .grid, + .preview-grid { + grid-template-columns: 1fr; + } + + .metric-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .chart-legend-row { + grid-template-columns: auto minmax(0, 1fr); + } + + .chart-legend-stat { + grid-column: 2; + } + + .setting { + grid-template-columns: 1fr; + } + + .funnel-row { + grid-template-columns: 1fr auto; + } + + .funnel-row progress { + grid-column: 1 / -1; + grid-row: 2; + } + + .app .table-head, + .app .project-row { + grid-template-columns: minmax(0, 1.4fr) minmax(100px, .8fr) minmax(90px, .7fr); + gap: var(--shd-space-2); + padding-right: 10px; + padding-left: 10px; + } + + .details-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 440px) { + .metric-grid, + .details-grid { + grid-template-columns: 1fr; + } + + .detail-head, + .details-header { + display: block; + } + + .close { + margin-top: var(--shd-space-2); + } +} diff --git a/plugins/shd-mcp-plugin/widgets/_shared/write-actions.css b/plugins/shd-mcp-plugin/widgets/_shared/write-actions.css new file mode 100644 index 0000000..0f96251 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/_shared/write-actions.css @@ -0,0 +1,142 @@ +.shd-widget-actions { + min-width: 0; + padding: var(--shd-space-5, 20px); + border: 1px solid var(--shd-line-strong, var(--line, rgba(31, 41, 55, .16))); + border-radius: var(--shd-radius-md, 14px); + background: var(--shd-surface-raised, var(--surface, #e8edf2)); + color: var(--shd-ink, var(--ink, #1d1f25)); + box-shadow: var(--shd-shadow-soft, none); +} + +.shd-widget-actions[hidden] { + display: none; +} + +.shd-widget-actions-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--shd-space-3, 12px); + margin-bottom: var(--shd-space-3, 12px); +} + +.shd-widget-actions-selection { + overflow: hidden; + color: var(--shd-muted, var(--muted, #616670)); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.shd-widget-actions-select, +.shd-widget-actions-input { + width: 100%; + padding: 9px 10px; + border: 1px solid var(--shd-line-strong, var(--line, rgba(31, 41, 55, .16))); + border-radius: var(--shd-radius-sm, 10px); + background: var(--shd-bg, var(--bg, #f8fafc)); + color: var(--shd-ink, var(--ink, #1d1f25)); +} + +.shd-widget-actions-select:focus, +.shd-widget-actions-input:focus { + border-color: var(--shd-accent, var(--accent, #535963)); + outline: 0; + box-shadow: 0 0 0 3px var(--shd-accent-soft, var(--soft, rgba(75, 85, 99, .12))); +} + +.shd-widget-actions-form { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: var(--shd-space-3, 12px); + margin-top: var(--shd-space-3, 12px); +} + +.shd-widget-actions-field { + display: grid; + gap: var(--shd-space-1, 4px); + color: var(--shd-muted, var(--muted, #616670)); + font-size: 11px; +} + +.shd-widget-actions-field:has(textarea) { + grid-column: span 2; +} + +.shd-widget-actions-field input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--shd-accent, var(--accent, #535963)); +} + +.shd-widget-actions-confirm { + display: flex; + grid-column: 1 / -1; + align-items: center; + gap: var(--shd-space-2, 8px); + color: var(--shd-ink, var(--ink, #1d1f25)); + font-size: 12px; +} + +.shd-widget-actions-warning, +.shd-widget-actions-hint { + grid-column: 1 / -1; + color: var(--shd-muted, var(--muted, #616670)); + font-size: 12px; +} + +.shd-widget-actions-danger { + color: var(--shd-danger, var(--bad, #b42318)); +} + +.shd-widget-actions-status { + min-height: 18px; + margin-top: var(--shd-space-3, 12px); + color: var(--shd-muted, var(--muted, #616670)); + font-size: 12px; +} + +.shd-widget-actions-status-error { + color: var(--shd-danger, var(--bad, #b42318)); +} + +.shd-widget-actions-buttons { + display: flex; + justify-content: flex-end; + gap: var(--shd-space-2, 8px); + margin-top: var(--shd-space-4, 16px); +} + +.shd-widget-actions-buttons button { + padding: 9px 12px; + border: 1px solid var(--shd-line-strong, var(--line, rgba(31, 41, 55, .16))); + border-radius: var(--shd-radius-sm, 10px); + background: var(--shd-surface-subtle, var(--bg, #f8fafc)); + color: var(--shd-ink, var(--ink, #1d1f25)); + cursor: pointer; +} + +.shd-widget-actions-buttons button:hover { + border-color: var(--shd-accent, var(--accent, #535963)); + background: var(--shd-surface, var(--hover, #e1e8ef)); +} + +.shd-widget-actions-buttons button:disabled { + cursor: wait; + opacity: .62; +} + +@media (max-width: 600px) { + .shd-widget-actions-field:has(textarea) { + grid-column: auto; + } + + .shd-widget-actions-header { + display: block; + } + + .shd-widget-actions-selection { + display: block; + margin-top: var(--shd-space-1, 4px); + } +} diff --git a/plugins/shd-mcp-plugin/widgets/_shared/write-actions.js b/plugins/shd-mcp-plugin/widgets/_shared/write-actions.js new file mode 100644 index 0000000..df7cce1 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/_shared/write-actions.js @@ -0,0 +1,454 @@ +(function () { + 'use strict'; + + if (window.__SHD_WIDGET_WRITE_ACTIONS__) return; + window.__SHD_WIDGET_WRITE_ACTIONS__ = true; + + var copy = { + ru: { + actions: 'Действия', + chooseAction: 'Выберите действие', + selectRecord: 'Сначала выберите запись.', + submit: 'Сохранить', + cancel: 'Отмена', + confirm: 'Подтверждаю действие', + warning: 'Проверьте данные перед записью.', + destructive: 'Операция может изменить или удалить данные.', + required: 'Заполните обязательное поле.', + invalidJson: 'Введите корректный JSON.', + success: 'Изменения сохранены.', + failure: 'Не удалось сохранить изменения.', + loading: 'Сохраняю…', + noActions: 'Для этого виджета нет доступных действий.', + refreshed: 'Список обновлён.', + fieldValue: 'Значение', + }, + en: { + actions: 'Actions', + chooseAction: 'Choose an action', + selectRecord: 'Select a record first.', + submit: 'Save', + cancel: 'Cancel', + confirm: 'I confirm this action', + warning: 'Review the values before writing.', + destructive: 'This operation may change or remove data.', + required: 'Fill in the required field.', + invalidJson: 'Enter valid JSON.', + success: 'Changes saved.', + failure: 'Could not save changes.', + loading: 'Saving…', + noActions: 'No actions are available for this widget.', + refreshed: 'List refreshed.', + fieldValue: 'Value', + }, + }; + + var state = { + widget: {}, + records: [], + selected: null, + action: null, + inputs: {}, + busy: false, + lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en', + }; + var pending = new Map(); + var nextRequestId = 1; + var elements = {}; + + function t(key) { + return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key; + } + + function object(value) { + return Boolean(value && typeof value === 'object' && !Array.isArray(value)); + } + + function normalize(value) { + if (!value) return null; + if (value.structuredContent) return normalize(value.structuredContent); + if (value.toolOutput) return normalize(value.toolOutput); + if (value.toolInput) return normalize(value.toolInput); + if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments); + return value; + } + + function widgetFrom(value) { + var payload = normalize(value) || {}; + var meta = object(payload.meta) ? payload.meta : {}; + return object(meta.widget) ? meta.widget : {}; + } + + function recordsFrom(value) { + var payload = normalize(value) || {}; + var data = Array.isArray(payload.items) ? payload.items : payload.projects; + if (!Array.isArray(data)) data = payload.data; + if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data; + return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : []; + } + + function readPath(value, path) { + if (!value || path === undefined || path === null) return undefined; + var parts = String(path).split('.'); + var current = value; + for (var index = 0; index < parts.length; index += 1) { + if (current === undefined || current === null) return undefined; + current = current[parts[index]]; + } + return current; + } + + function firstValue(paths) { + var list = Array.isArray(paths) ? paths : [paths]; + var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs]; + for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) { + var sourceObject = sources[sourceIndex]; + if (!sourceObject) continue; + for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) { + var value = readPath(sourceObject, list[pathIndex]); + if (value !== undefined && value !== null && value !== '') return value; + } + } + return undefined; + } + + function text(value) { + if (value === undefined || value === null) return ''; + if (typeof value === 'object') return JSON.stringify(value); + return String(value); + } + + function label(value) { + if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue'); + return String(value || t('fieldValue')); + } + + function actionLabel(action) { + return label(action && action.label) || action.id || action.tool || t('actions'); + } + + function hasTargets(action) { + return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target)); + } + + function actionList() { + return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) { + return action && action.tool; + }) : []; + } + + function request(method, params) { + var openai = typeof window !== 'undefined' ? window.openai : undefined; + if (method === 'tools/call' && openai && typeof openai.callTool === 'function') { + return openai.callTool(params.name, params.arguments || {}); + } + var id = 'actions-' + nextRequestId++; + window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*'); + return new Promise(function (resolve, reject) { + var timeout = window.setTimeout(function () { + pending.delete(id); + reject(new Error(t('failure'))); + }, 15000); + pending.set(id, { resolve: resolve, reject: reject, timeout: timeout }); + }); + } + + function unwrap(value) { + var payload = normalize(value); + if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure')); + if (payload && payload.error) throw new Error(text(payload.error)); + return payload || value; + } + + function make(tag, value, className) { + var node = document.createElement(tag); + if (className) node.className = className; + if (value !== undefined) node.textContent = value; + return node; + } + + function ensurePanel() { + if (elements.panel) return; + var root = document.querySelector('main') || document.body; + var panel = make('section', undefined, 'shd-widget-actions'); + panel.hidden = true; + panel.setAttribute('aria-live', 'polite'); + var header = make('div', undefined, 'shd-widget-actions-header'); + header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection')); + var select = document.createElement('select'); + select.className = 'shd-widget-actions-select'; + select.setAttribute('aria-label', t('actions')); + var form = make('form', undefined, 'shd-widget-actions-form'); + var status = make('div', '', 'shd-widget-actions-status'); + var buttons = make('div', undefined, 'shd-widget-actions-buttons'); + var submit = make('button', t('submit'), 'shd-widget-actions-submit'); + submit.type = 'submit'; + var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel'); + cancel.type = 'button'; + buttons.append(submit, cancel); + panel.append(header, select, form, status, buttons); + root.append(panel); + elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel }; + select.addEventListener('change', function () { + var selected = actionList().find(function (action) { return action.id === select.value; }); + state.action = selected || null; + renderForm(); + }); + cancel.addEventListener('click', function () { + state.action = null; + select.value = ''; + renderForm(); + }); + form.addEventListener('submit', function (event) { + event.preventDefault(); + submitAction(); + }); + } + + function inputValue(fieldConfig) { + var value = firstValue(fieldConfig.source || []); + if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default; + return value; + } + + function createInput(fieldConfig) { + var type = fieldConfig.type || 'text'; + var input; + if (type === 'textarea' || type === 'json') { + input = document.createElement('textarea'); + input.rows = type === 'json' ? 4 : 3; + if (type === 'json') input.placeholder = '{ }'; + } else if (type === 'select') { + input = document.createElement('select'); + (fieldConfig.options || []).forEach(function (option) { + var item = document.createElement('option'); + item.value = String(option[0]); + item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]); + input.append(item); + }); + } else { + input = document.createElement('input'); + input.type = type === 'number' || type === 'email' ? type : 'text'; + } + input.className = 'shd-widget-actions-input'; + input.name = fieldConfig.name; + var value = inputValue(fieldConfig); + if (type === 'checkbox') { + input.type = 'checkbox'; + input.checked = value === true || value === 'true' || value === 1 || value === '1'; + } else if (value !== undefined && value !== null) { + input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value); + } + if (fieldConfig.required) input.required = true; + return input; + } + + function renderForm() { + ensurePanel(); + var selectedActionId = state.action ? state.action.id : ''; + elements.select.replaceChildren(); + var placeholder = document.createElement('option'); + placeholder.value = ''; + placeholder.textContent = t('chooseAction'); + elements.select.append(placeholder); + actionList().forEach(function (action) { + var option = document.createElement('option'); + option.value = action.id; + option.textContent = actionLabel(action); + elements.select.append(option); + }); + elements.select.value = selectedActionId; + elements.select.hidden = actionList().length === 0; + elements.form.replaceChildren(); + elements.status.textContent = ''; + elements.submit.disabled = state.busy; + var action = state.action; + if (!action) { + elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : ''; + elements.submit.hidden = true; + elements.cancel.hidden = true; + if (actionList().length && !state.selected && actionList().every(hasTargets)) { + elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint')); + } + return; + } + elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : ''; + elements.submit.hidden = false; + elements.cancel.hidden = false; + if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint')); + (action.fields || []).forEach(function (fieldConfig) { + if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return; + var wrapper = make('label', undefined, 'shd-widget-actions-field'); + wrapper.append(make('span', label(fieldConfig.label))); + wrapper.append(createInput(fieldConfig)); + elements.form.append(wrapper); + }); + if (action.confirm) { + var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm'); + var confirm = document.createElement('input'); + confirm.type = 'checkbox'; + confirm.name = '__confirm'; + confirm.required = true; + confirmLabel.append(confirm, make('span', t('confirm'))); + elements.form.append(confirmLabel); + } + var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning'); + elements.form.append(notice); + } + + function coerce(value, type) { + if (type === 'number') { + var number = Number(value); + return Number.isFinite(number) ? number : value; + } + if (type === 'checkbox') return Boolean(value); + if (type === 'json') { + try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); } + } + return value; + } + + function targetType(name) { + return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text'; + } + + function targetList(action) { + if (Array.isArray(action.targets)) return action.targets; + return action.target ? [action.target] : []; + } + + function collectArguments(action) { + var args = Object.assign({}, action.fixed || {}); + (action.context || []).forEach(function (name) { + var value = firstValue([name]); + if (value !== undefined && value !== null && value !== '') args[name] = value; + }); + targetList(action).forEach(function (targetConfig) { + var value = firstValue(targetConfig.source || targetConfig.name); + if (value === undefined || value === null || value === '') throw new Error(t('selectRecord')); + args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name)); + }); + (action.fields || []).forEach(function (fieldConfig) { + if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return; + var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) { + return node.getAttribute('name') === fieldConfig.name; + }); + if (!input) return; + var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim(); + if (raw === '' && !input.checked && !fieldConfig.required) return; + if (raw === '' && fieldConfig.required) throw new Error(t('required')); + args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text'); + }); + if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10); + if (action.confirmArg) args.confirm = true; + return args; + } + + function setStatus(message, error) { + elements.status.textContent = message || ''; + elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : ''); + } + + function submitAction() { + if (state.busy || !state.action) return; + var action = state.action; + var args; + try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; } + state.busy = true; + elements.submit.disabled = true; + elements.cancel.disabled = true; + setStatus(t('loading'), false); + request('tools/call', { name: action.tool, arguments: args }).then(function (result) { + unwrap(result); + setStatus(t('success'), false); + var refresh = document.getElementById('refresh'); + if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250); + }).catch(function (error) { + setStatus(error && error.message ? error.message : t('failure'), true); + }).finally(function () { + state.busy = false; + elements.submit.disabled = false; + elements.cancel.disabled = false; + }); + } + + function recordFromNode(node) { + var data = node && node.dataset ? node.dataset : {}; + var recordId = String(data.shdRecordId || '').toLowerCase(); + if (recordId) { + var exact = state.records.find(function (item) { + return text(item.id || item.code || item.key || item.number).toLowerCase() === recordId; + }); + if (exact) return exact; + } + var recordTitle = String(data.shdRecordTitle || '').toLowerCase(); + if (recordTitle) { + var titled = state.records.find(function (item) { + return text(item.title || item.name || item.display_name || item.number).toLowerCase() === recordTitle; + }); + if (titled) return titled; + } + var ariaLabel = node && node.getAttribute ? String(node.getAttribute('aria-label') || '').toLowerCase() : ''; + return state.records.find(function (item) { + var title = text(item.title || item.name || item.display_name || item.number).toLowerCase(); + return title && title === ariaLabel; + }) || null; + } + + function update(value) { + var payload = normalize(value) || {}; + var widget = widgetFrom(payload); + if (Object.keys(widget).length) state.widget = widget; + var records = recordsFrom(payload); + if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) { + state.records = records; + if (state.selected && !state.records.some(function (item) { + var selectedId = text(state.selected.id || state.selected.code || state.selected.key || state.selected.number).toLowerCase(); + var itemId = text(item.id || item.code || item.key || item.number).toLowerCase(); + return selectedId && itemId && selectedId === itemId; + })) { + state.selected = null; + state.action = null; + } + } + ensurePanel(); + var actions = actionList(); + elements.panel.hidden = actions.length === 0; + if (actions.length && !state.action) renderForm(); + else if (actions.length) renderForm(); + } + + document.addEventListener('click', function (event) { + var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null; + if (!node || (elements.panel && elements.panel.contains(node))) return; + var item = recordFromNode(node); + if (!item) return; + state.selected = item; + ensurePanel(); + renderForm(); + }, true); + + window.addEventListener('shd-widget-data', function (event) { + update(event && event.detail); + }); + + window.__SHD_WIDGET_ACTIONS_UPDATE__ = update; + + window.addEventListener('message', function (event) { + if (event.source !== window.parent) return; + var message = event.data; + if (!message || message.jsonrpc !== '2.0') return; + if (message.id !== undefined && pending.has(message.id)) { + var requestState = pending.get(message.id); + pending.delete(message.id); + window.clearTimeout(requestState.timeout); + if (message.error) requestState.reject(new Error(message.error.message || t('failure'))); + else requestState.resolve(message.result); + return; + } + }, { passive: true }); + + ensurePanel(); + var initial = typeof window !== 'undefined' && (window.__SHD_WIDGET_STATE__ || (window.openai && (window.openai.toolOutput || window.openai.toolInput))); + if (initial) update(initial); +}()); diff --git a/plugins/shd-mcp-plugin/widgets/acl-matrix/widget.json b/plugins/shd-mcp-plugin/widgets/acl-matrix/widget.json new file mode 100644 index 0000000..fca5dc1 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/acl-matrix/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-acl-matrix", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/acl-matrix/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/active-projects/source.html b/plugins/shd-mcp-plugin/widgets/active-projects/source.html new file mode 100644 index 0000000..360f1f6 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/active-projects/source.html @@ -0,0 +1,1812 @@ + + + + + + + + + +
+
+
+

Активные проекты

+

Сроки завершения и текущие статусы

+
+ +
+
+ +
+
+
+
+ Проект + Дата завершения + Статус +
+
+ + +
+ +
+ + + + diff --git a/plugins/shd-mcp-plugin/widgets/active-projects/widget.json b/plugins/shd-mcp-plugin/widgets/active-projects/widget.json new file mode 100644 index 0000000..c6692ea --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/active-projects/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-active-projects", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/active-projects/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/activity/widget.json b/plugins/shd-mcp-plugin/widgets/activity/widget.json new file mode 100644 index 0000000..6ee4e2f --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/activity/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-activity", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/activity/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/agents-health/widget.json b/plugins/shd-mcp-plugin/widgets/agents-health/widget.json new file mode 100644 index 0000000..cf18d53 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/agents-health/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-agents-health", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/agents-health/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/agents/widget.json b/plugins/shd-mcp-plugin/widgets/agents/widget.json new file mode 100644 index 0000000..c47e6de --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/agents/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-agents", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/agents/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/airbase-preview/widget.json b/plugins/shd-mcp-plugin/widgets/airbase-preview/widget.json new file mode 100644 index 0000000..07f4cf0 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/airbase-preview/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-airbase-preview", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/airbase-preview/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/airbase/widget.json b/plugins/shd-mcp-plugin/widgets/airbase/widget.json new file mode 100644 index 0000000..46bff01 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/airbase/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-airbase", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/airbase/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/controller-observability/source.html b/plugins/shd-mcp-plugin/widgets/controller-observability/source.html new file mode 100644 index 0000000..f6dbc13 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/controller-observability/source.html @@ -0,0 +1,1493 @@ + + + + + + SHD controller observability + + + +
+
SHD MCP · controller observability
+

Контроллеры

+
+
+
+ + + diff --git a/plugins/shd-mcp-plugin/widgets/controller-observability/widget.json b/plugins/shd-mcp-plugin/widgets/controller-observability/widget.json new file mode 100644 index 0000000..4f5b937 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/controller-observability/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-controller-observability", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/controller-observability/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/crm-funnel/widget.json b/plugins/shd-mcp-plugin/widgets/crm-funnel/widget.json new file mode 100644 index 0000000..708f917 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/crm-funnel/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-crm-funnel", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/crm-funnel/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/crm/widget.json b/plugins/shd-mcp-plugin/widgets/crm/widget.json new file mode 100644 index 0000000..9f8f763 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/crm/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-crm", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "uri": "ui://shd/crm/v1.html", + "mediaType": "text/html;profile=mcp-app" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/discussions/widget.json b/plugins/shd-mcp-plugin/widgets/discussions/widget.json new file mode 100644 index 0000000..1ddcf5b --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/discussions/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-discussions", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/discussions/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/documents-completeness/widget.json b/plugins/shd-mcp-plugin/widgets/documents-completeness/widget.json new file mode 100644 index 0000000..e1156ff --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/documents-completeness/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-documents-completeness", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/documents-completeness/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/documents/widget.json b/plugins/shd-mcp-plugin/widgets/documents/widget.json new file mode 100644 index 0000000..ae27d7d --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/documents/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-documents", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "uri": "ui://shd/documents/v1.html", + "mediaType": "text/html;profile=mcp-app" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/files/widget.json b/plugins/shd-mcp-plugin/widgets/files/widget.json new file mode 100644 index 0000000..75db914 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/files/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-files", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/files/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/finance-dashboard/widget.json b/plugins/shd-mcp-plugin/widgets/finance-dashboard/widget.json new file mode 100644 index 0000000..a8b576c --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/finance-dashboard/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-finance-dashboard", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/finance-dashboard/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/finance/widget.json b/plugins/shd-mcp-plugin/widgets/finance/widget.json new file mode 100644 index 0000000..915a354 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/finance/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-finance", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "uri": "ui://shd/finance/v1.html", + "mediaType": "text/html;profile=mcp-app" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/gitea-board/widget.json b/plugins/shd-mcp-plugin/widgets/gitea-board/widget.json new file mode 100644 index 0000000..95a2f8e --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/gitea-board/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-gitea-board", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/gitea-board/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/gitea/widget.json b/plugins/shd-mcp-plugin/widgets/gitea/widget.json new file mode 100644 index 0000000..42d1fa2 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/gitea/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-gitea", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/gitea/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/inventory-warnings/widget.json b/plugins/shd-mcp-plugin/widgets/inventory-warnings/widget.json new file mode 100644 index 0000000..7ca5e75 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/inventory-warnings/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-inventory-warnings", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/inventory-warnings/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/inventory/widget.json b/plugins/shd-mcp-plugin/widgets/inventory/widget.json new file mode 100644 index 0000000..865b811 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/inventory/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-inventory", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/inventory/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/notes-tree/widget.json b/plugins/shd-mcp-plugin/widgets/notes-tree/widget.json new file mode 100644 index 0000000..30f643a --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/notes-tree/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-notes-tree", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/notes-tree/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/notes/widget.json b/plugins/shd-mcp-plugin/widgets/notes/widget.json new file mode 100644 index 0000000..be679f2 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/notes/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-notes", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/notes/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/notifications/widget.json b/plugins/shd-mcp-plugin/widgets/notifications/widget.json new file mode 100644 index 0000000..6604c9e --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/notifications/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-notifications", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/notifications/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/organizations/widget.json b/plugins/shd-mcp-plugin/widgets/organizations/widget.json new file mode 100644 index 0000000..6631872 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/organizations/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-organizations", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/organizations/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/pdf-scheme-editor-progress/widget.json b/plugins/shd-mcp-plugin/widgets/pdf-scheme-editor-progress/widget.json new file mode 100644 index 0000000..c0f6fe4 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/pdf-scheme-editor-progress/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-pdf-scheme-editor-progress", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/pdf-scheme-editor-progress/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/pdf-scheme-editor/widget.json b/plugins/shd-mcp-plugin/widgets/pdf-scheme-editor/widget.json new file mode 100644 index 0000000..a5d1a45 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/pdf-scheme-editor/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-pdf-scheme-editor", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/pdf-scheme-editor/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/project-overview/widget.json b/plugins/shd-mcp-plugin/widgets/project-overview/widget.json new file mode 100644 index 0000000..8578006 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/project-overview/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-project-overview", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/project-overview/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/project-timeline/widget.json b/plugins/shd-mcp-plugin/widgets/project-timeline/widget.json new file mode 100644 index 0000000..8f86645 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/project-timeline/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-project-timeline", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/project-timeline/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/proposal-approvals/widget.json b/plugins/shd-mcp-plugin/widgets/proposal-approvals/widget.json new file mode 100644 index 0000000..9f73756 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/proposal-approvals/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-proposal-approvals", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/proposal-approvals/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/proposals/widget.json b/plugins/shd-mcp-plugin/widgets/proposals/widget.json new file mode 100644 index 0000000..5db474e --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/proposals/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-proposals", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/proposals/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/schedule-calendar/widget.json b/plugins/shd-mcp-plugin/widgets/schedule-calendar/widget.json new file mode 100644 index 0000000..c019e4d --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/schedule-calendar/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-schedule-calendar", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/schedule-calendar/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/scheduling/widget.json b/plugins/shd-mcp-plugin/widgets/scheduling/widget.json new file mode 100644 index 0000000..6878458 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/scheduling/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-scheduling", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/scheduling/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/status-page/widget.json b/plugins/shd-mcp-plugin/widgets/status-page/widget.json new file mode 100644 index 0000000..9aeb302 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/status-page/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-status-page", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/status-page/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/task-kanban/widget.json b/plugins/shd-mcp-plugin/widgets/task-kanban/widget.json new file mode 100644 index 0000000..8150139 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/task-kanban/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-task-kanban", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/task-kanban/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/tasks/widget.json b/plugins/shd-mcp-plugin/widgets/tasks/widget.json new file mode 100644 index 0000000..b5e2ea6 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/tasks/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-tasks", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "uri": "ui://shd/tasks/v1.html", + "mediaType": "text/html;profile=mcp-app" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/plugins/shd-mcp-plugin/widgets/team-workload/widget.json b/plugins/shd-mcp-plugin/widgets/team-workload/widget.json new file mode 100644 index 0000000..1b9c638 --- /dev/null +++ b/plugins/shd-mcp-plugin/widgets/team-workload/widget.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "name": "shd-team-workload", + "version": "1.0.0", + "resources": [ + { + "file": "index.html", + "mediaType": "text/html;profile=mcp-app", + "uri": "ui://shd/team-workload/v1.html" + } + ], + "license": "../../../../../LICENSE", + "source": { + "repository": "https://github.com/bulava92/shd-mcp-plugin" + } +} diff --git a/scripts/generate-widget-artifacts.mjs b/scripts/generate-widget-artifacts.mjs new file mode 100644 index 0000000..ceb9f6f --- /dev/null +++ b/scripts/generate-widget-artifacts.mjs @@ -0,0 +1,342 @@ +import { createHash } from 'node:crypto'; +import { gzipSync } from 'node:zlib'; +import { + existsSync, + mkdirSync, + readFileSync, + readdirSync, + writeFileSync, +} from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { runInNewContext } from 'node:vm'; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const widgetsRoot = join(repositoryRoot, 'plugins', 'shd-mcp-plugin', 'widgets'); +const templatePath = join(widgetsRoot, '_shared', 'universal-widget.html'); +const configPath = join(widgetsRoot, '_shared', 'universal-widget-config.json'); +const themePath = join(widgetsRoot, '_shared', 'widget-theme.css'); +const actionsCssPath = join(widgetsRoot, '_shared', 'write-actions.css'); +const actionsJsPath = join(widgetsRoot, '_shared', 'write-actions.js'); +const bootstrapPath = join(widgetsRoot, 'airbase-preview', 'v1', 'index.html'); +const moduleToken = '__SHD_WIDGET_MODULE__'; +const configToken = '__SHD_WIDGET_CONFIG__'; +const themeToken = '__SHD_WIDGET_THEME_CSS__'; +const actionsCssToken = '__SHD_WIDGET_ACTIONS_CSS__'; +const actionsJsToken = '__SHD_WIDGET_ACTIONS_JS__'; +const engineName = 'shd-universal-v1'; +const checkOnly = process.argv.includes('--check'); +const extractConfigOnly = process.argv.includes('--extract-config'); +const migrateTemplate = process.argv.includes('--migrate-template'); +const excludedWidgets = new Set(['active-projects', 'controller-observability']); +const themedSpecializedWidgets = new Set(['active-projects', 'controller-observability']); +const runtimeModuleByArtifact = new Map([ + ['pdf-scheme-editor-progress', 'schemes-progress'], +]); + +function fail(message) { + throw new Error(`[widget-artifacts] ${message}`); +} + +function readWidgetTheme() { + if (!existsSync(themePath)) fail(`widget theme is missing: ${themePath}`); + const theme = readFileSync(themePath, 'utf8').trim(); + if (!theme) fail(`widget theme is empty: ${themePath}`); + return theme; +} + +function readActionsCss() { + if (!existsSync(actionsCssPath)) fail(`widget actions CSS is missing: ${actionsCssPath}`); + const css = readFileSync(actionsCssPath, 'utf8').trim(); + if (!css) fail(`widget actions CSS is empty: ${actionsCssPath}`); + return css; +} + +function readActionsJs() { + if (!existsSync(actionsJsPath)) fail(`widget actions JavaScript is missing: ${actionsJsPath}`); + const source = readFileSync(actionsJsPath, 'utf8').trim(); + if (!source) fail(`widget actions JavaScript is empty: ${actionsJsPath}`); + return source; +} + +function ensureCanonicalTemplate() { + if (existsSync(templatePath)) return; + if (checkOnly) fail(`canonical template is missing: ${templatePath}`); + if (!existsSync(bootstrapPath)) fail(`bootstrap widget is missing: ${bootstrapPath}`); + + const source = readFileSync(bootstrapPath, 'utf8'); + const template = source + .replace( + 'data-widget-module="airbase-preview"', + `data-shd-widget-engine="${engineName}" data-widget-module="${moduleToken}"`, + ) + .replace( + "var DEFAULT_MODULE='airbase-preview'", + `var DEFAULT_MODULE='${moduleToken}'`, + ); + if (template === source || !template.includes(moduleToken)) { + fail('could not bootstrap the universal template from airbase-preview'); + } + writeFileSync(templatePath, template); +} + +function isRecord(value) { + return value && typeof value === 'object' && !Array.isArray(value); +} + +function extractWidgetConfig(template) { + const start = template.indexOf(' var M='); + const end = template.indexOf(' var pending=', start); + if (start < 0 || end < 0 || end <= start) { + fail('could not locate the legacy M/S configuration block in the canonical template'); + } + + const block = template.slice(start, end); + let extracted; + try { + extracted = runInNewContext(`(() => { ${block}; return { copy: M, defaults: S }; })()`); + } catch (error) { + fail(`could not evaluate the legacy M/S configuration block: ${error instanceof Error ? error.message : String(error)}`); + } + if (!isRecord(extracted) || !isRecord(extracted.copy) || !isRecord(extracted.defaults)) { + fail('legacy M/S configuration block did not produce copy/defaults objects'); + } + return extracted; +} + +function readWidgetConfig() { + if (!existsSync(configPath)) return null; + let config; + try { + config = JSON.parse(readFileSync(configPath, 'utf8')); + } catch (error) { + fail(`widget config is not valid JSON: ${error instanceof Error ? error.message : String(error)}`); + } + if (!isRecord(config)) fail('widget config must be a JSON object'); + if (config.engine !== engineName) fail(`widget config engine must be ${engineName}`); + if (!isRecord(config.copy) || !isRecord(config.defaults)) { + fail('widget config must contain copy and defaults objects'); + } + return config; +} + +function ensureWidgetConfig(template) { + const existing = readWidgetConfig(); + if (existing) return existing; + if (checkOnly) fail(`widget config is missing: ${configPath}`); + + const extracted = extractWidgetConfig(template); + const config = { + version: 1, + engine: engineName, + copy: extracted.copy, + defaults: extracted.defaults, + }; + writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`); + return config; +} + +function migrateCanonicalTemplate(template) { + if (template.includes(configToken)) return template; + if (!migrateTemplate) { + fail(`canonical template must contain ${configToken}; run with --migrate-template once`); + } + + const start = template.indexOf(' var M='); + const end = template.indexOf(' var pending=', start); + if (start < 0 || end < 0 || end <= start) { + fail('could not locate the legacy M/S configuration block for migration'); + } + const replacement = [ + ' var MODULE_CONFIG=__SHD_WIDGET_CONFIG__;', + ' var M=MODULE_CONFIG.copy;', + ' var S=MODULE_CONFIG.defaults;', + ].join('\n') + '\n'; + const migrated = `${template.slice(0, start)}${replacement}${template.slice(end)}`; + writeFileSync(templatePath, migrated); + return migrated; +} + +function readUniversalWidgets() { + return readdirSync(widgetsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && !excludedWidgets.has(entry.name) && entry.name !== '_shared') + .map((entry) => entry.name) + .filter((name) => existsSync(join(widgetsRoot, name, 'widget.json'))) + .sort(); +} + +function expectedArtifact(path, content) { + if (checkOnly) { + const current = existsSync(path) ? readFileSync(path, 'utf8') : null; + if (current !== content) fail(`generated file is stale: ${path}`); + return; + } + if (!existsSync(path) || readFileSync(path, 'utf8') !== content) writeFileSync(path, content); +} + +function injectActionsJs(html, actionsJs) { + let rendered = html.replaceAll(actionsJsToken, actionsJs); + const legacyPattern = /[\s\S]*?<\/script>/i; + if (legacyPattern.test(rendered)) { + rendered = rendered.replace( + legacyPattern, + ``, + ); + } + return rendered; +} + +function renderHtml(template, module, config, theme, actionsCss, actionsJs) { + const serializedConfig = JSON.stringify({ copy: config.copy, defaults: config.defaults }) + .replaceAll('<', '\\u003c'); + const html = injectActionsJs(template + .replaceAll(moduleToken, module) + .replaceAll(configToken, serializedConfig) + .replaceAll(themeToken, theme) + .replaceAll(actionsCssToken, actionsCss), actionsJs); + if (html.includes(moduleToken)) fail(`unresolved module token for ${module}`); + if (html.includes(configToken)) fail(`unresolved config token for ${module}`); + if (html.includes(themeToken)) fail(`unresolved theme token for ${module}`); + if (html.includes(actionsCssToken)) fail(`unresolved actions CSS token for ${module}`); + if (html.includes(actionsJsToken)) fail(`unresolved actions JavaScript token for ${module}`); + if (!html.includes(`data-shd-widget-engine="${engineName}"`)) fail(`engine marker is missing for ${module}`); + if (!html.includes(`data-widget-module="${module}"`)) fail(`module marker is missing for ${module}`); + if (!html.includes(`var DEFAULT_MODULE='${module}'`)) fail(`default module is missing for ${module}`); + return html; +} + +function renderSpecializedHtml(source, module, theme, actionsCss, actionsJs) { + const stylePattern = /]*>[\s\S]*?<\/style>/i; + if (!stylePattern.test(source)) fail(`specialized widget has no inline style block: ${module}`); + + let html = source.replace( + stylePattern, + ``, + ); + const actionsPattern = /[\s\S]*?<\/style>/i; + if (actionsPattern.test(html)) { + html = html.replace( + actionsPattern, + ``, + ); + } + html = injectActionsJs(html, actionsJs); + if (!html.includes(`data-shd-widget-engine="${engineName}"`)) { + const mainPattern = /]*>/i; + const main = html.match(mainPattern)?.[0]; + if (!main) fail(`specialized widget has no main element: ${module}`); + html = html.replace(mainPattern, main.replace(/>$/, ` data-shd-widget-engine="${engineName}" data-widget-module="${module}">`)); + } else { + html = html.replace(/data-widget-module="[^"]*"/, `data-widget-module="${module}"`); + } + if (!html.includes(`data-shd-widget-engine="${engineName}"`)) fail(`engine marker is missing for ${module}`); + if (!html.includes(`data-widget-module="${module}"`)) fail(`module marker is missing for ${module}`); + return html; +} + +function manifestFor(widgetPath, html, module, kind = 'universal') { + const manifestPath = join(dirname(widgetPath), 'widget.json'); + if (!existsSync(manifestPath)) fail(`manifest is missing: ${manifestPath}`); + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); + const resource = Array.isArray(manifest.resources) + ? manifest.resources.find((item) => item?.file === 'index.html') + : null; + if (!resource) fail(`index.html resource is missing from ${manifestPath}`); + + const bytes = Buffer.byteLength(html, 'utf8'); + const digest = createHash('sha256').update(html, 'utf8').digest('hex'); + resource.bytes = bytes; + resource.gzipBytes = gzipSync(Buffer.from(html, 'utf8'), { mtime: 0 }).length; + resource.sha256 = digest; + const source = kind === 'universal' + ? { + ...(manifest.source || {}), + path: 'plugins/shd-mcp-plugin/widgets/_shared/universal-widget.html', + config: 'plugins/shd-mcp-plugin/widgets/_shared/universal-widget-config.json', + theme: 'plugins/shd-mcp-plugin/widgets/_shared/widget-theme.css', + actions: 'plugins/shd-mcp-plugin/widgets/_shared/write-actions.css', + generatedBy: 'scripts/generate-widget-artifacts.mjs', + engine: engineName, + } + : { + ...(manifest.source || {}), + path: `plugins/shd-mcp-plugin/widgets/${module}/source.html`, + theme: 'plugins/shd-mcp-plugin/widgets/_shared/widget-theme.css', + ...(html.includes('data-shd-widget-write-actions="js"') + ? { actions: 'plugins/shd-mcp-plugin/widgets/_shared/write-actions.css' } + : {}), + generatedBy: 'scripts/generate-widget-artifacts.mjs', + engine: engineName, + }; + manifest.source = source; + if (manifest.source.module && manifest.source.module !== module) delete manifest.source.module; + return { manifest, resource, digest }; +} + +function upstreamFor(resource, kind = 'universal', manifest = {}) { + const heading = `# ${manifest.name || 'SHD widget'}`; + const code = String.fromCharCode(96); + const sourceDescription = kind === 'universal' + ? `The standalone HTML is generated from ${code}widgets/_shared/universal-widget.html${code}, ${code}widgets/_shared/universal-widget-config.json${code} and ${code}widgets/_shared/widget-theme.css${code} by ${code}scripts/generate-widget-artifacts.mjs${code} using the ${code}${engineName}${code} engine.` + : `The specialized renderer remains in its module directory, while its shared visual layer is injected from ${code}widgets/_shared/widget-theme.css${code} by ${code}scripts/generate-widget-artifacts.mjs${code}.`; + return `${heading}\n\nThis artifact is served by the SHD MCP backend as ${resource.uri}.\n\n${sourceDescription} It uses the MCP Apps bridge first and keeps ${code}window.openai${code} as a compatibility extension; it has no external script or network dependency. The module supplies its data and view configuration through the render-tool metadata.\n`; +} + +function main() { + ensureCanonicalTemplate(); + let template = readFileSync(templatePath, 'utf8'); + const theme = readWidgetTheme(); + const actionsCss = readActionsCss(); + const actionsJs = readActionsJs(); + const configExisted = existsSync(configPath); + const config = ensureWidgetConfig(template); + if (extractConfigOnly) { + console.log(`${configExisted ? 'checked' : 'extracted'} widget config: ${configPath}`); + return; + } + template = migrateCanonicalTemplate(template); + if (!template.includes(moduleToken)) fail(`canonical template must contain ${moduleToken}`); + if (!template.includes(configToken)) fail(`canonical template must contain ${configToken}`); + + const widgets = readUniversalWidgets(); + if (widgets.length === 0) fail('no universal widget artifacts found'); + let generated = 0; + for (const artifactName of widgets) { + const module = runtimeModuleByArtifact.get(artifactName) || artifactName; + const widgetPath = join(widgetsRoot, artifactName, 'v1'); + mkdirSync(widgetPath, { recursive: true }); + const html = renderHtml(template, module, config, theme, actionsCss, actionsJs); + const { manifest, resource, digest } = manifestFor(widgetPath, html, module, 'universal'); + const manifestText = `${JSON.stringify(manifest, null, 2)}\n`; + expectedArtifact(join(widgetPath, 'index.html'), html); + expectedArtifact(join(widgetPath, 'manifest.json'), manifestText); + expectedArtifact(join(widgetPath, 'SHA256SUMS'), `${digest} index.html\n`); + expectedArtifact(join(widgetPath, 'UPSTREAM.md'), upstreamFor(resource, 'universal', manifest)); + generated += 1; + } + + let specialized = 0; + for (const artifactName of [...themedSpecializedWidgets].sort()) { + const widgetPath = join(widgetsRoot, artifactName, 'v1'); + mkdirSync(widgetPath, { recursive: true }); + const sourcePath = join(widgetsRoot, artifactName, 'source.html'); + const source = readFileSync(sourcePath, 'utf8'); + const html = renderSpecializedHtml(source, artifactName, theme, actionsCss, actionsJs); + const { manifest, resource, digest } = manifestFor(widgetPath, html, artifactName, 'specialized'); + const manifestText = `${JSON.stringify(manifest, null, 2)}\n`; + expectedArtifact(join(widgetPath, 'index.html'), html); + expectedArtifact(join(widgetPath, 'manifest.json'), manifestText); + expectedArtifact(join(widgetPath, 'SHA256SUMS'), `${digest} index.html\n`); + expectedArtifact(join(widgetPath, 'UPSTREAM.md'), upstreamFor(resource, 'specialized', manifest)); + specialized += 1; + } + + console.log(`${checkOnly ? 'checked' : 'generated'} ${generated} universal and ${specialized} specialized widget artifacts; engine=${engineName}`); +} + +try { + main(); +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; +}