Compare commits

...

36 Commits

Author SHA1 Message Date
c02661ff78 docs: add mintel architecture manifesto
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 11s
Monorepo Pipeline / 🧪 Test (push) Failing after 11s
Monorepo Pipeline / 🏗️ Build (push) Failing after 11s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-06-09 00:40:08 +02:00
66f28d9cf9 feat: add remotion video app to monorepo with UI integration
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 12s
Monorepo Pipeline / 🧪 Test (push) Failing after 12s
Monorepo Pipeline / 🏗️ Build (push) Failing after 11s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-06-09 00:34:25 +02:00
8bbbff7afc fix: update node version to 22 and add CI=true for pnpm, rename service to at-mintel-ui
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m4s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m14s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m20s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-06-08 18:41:00 +02:00
59747ee121 feat: add universal-ui docker service with caddy routing
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m6s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m16s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m21s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-06-08 18:30:23 +02:00
599ca63f06 feat: integrate Universal Design System with Web, Print, and Presentation examples
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m16s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m27s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m29s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-06-08 17:55:57 +02:00
9f8eab2ad2 fix(mcp): resolve port clash on 3001 and force pnpm@10 for Node 20 builds
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m32s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m19s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m11s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 6s
2026-05-26 11:58:50 +02:00
69c483ac46 fix(infra): use dynamic path for registry volume to prevent prune failure
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m23s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m16s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m33s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 43s
2026-05-06 12:24:19 +02:00
ac178ccbb0 fix(qa): downgrade to v3 artifacts and align cookies for Gitea runners
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m6s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m14s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m24s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 13s
2026-04-25 19:13:22 +02:00
3f16828bdf ci: restore secrets and upload-artifact
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 9s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m26s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m29s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m51s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-04-25 18:38:51 +02:00
0f7cd94e14 ci: comment out secrets in workflow_call
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m3s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m1s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m33s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-04-25 18:34:03 +02:00
2cc6512afa ci: restore prepare job with diagnostics and verbose install
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 7s
Monorepo Pipeline / 🧪 Test (push) Successful in 55s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m4s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m43s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-04-25 18:27:54 +02:00
151c98c884 ci: minimize prepare job for debugging
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 6s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m7s
Monorepo Pipeline / 🏗️ Build (push) Has started running
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 🧹 Lint (push) Has been cancelled
2026-04-25 18:24:29 +02:00
ba17d7940e ci: add more diagnostics to prepare job
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 🧹 Lint (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Monorepo Pipeline / 🧪 Test (push) Has been cancelled
2026-04-25 18:23:39 +02:00
46120dbd56 ci: remove docker cleanup from prepare job
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 5s
Monorepo Pipeline / 🧪 Test (push) Successful in 58s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
Monorepo Pipeline / 🧹 Lint (push) Has been cancelled
2026-04-25 18:22:11 +02:00
571c4797fa ci: add diagnostics to prepare job
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 58s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
Monorepo Pipeline / 🧹 Lint (push) Has been cancelled
Monorepo Pipeline / 🏗️ Build (push) Has been cancelled
2026-04-25 18:18:21 +02:00
c7d107a578 fix: use standard npm registry host
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m8s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m27s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m43s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-04-24 12:16:08 +02:00
5f50e98a0a fix: reorder steps and add workspace wipe
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m12s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m9s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m31s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-04-24 12:08:39 +02:00
45f831ca1c fix: improve registry auth robustness
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 40s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m48s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m40s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-04-24 11:59:01 +02:00
73356cca30 fix: harden docker prune steps
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m0s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m35s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m50s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-04-24 11:53:14 +02:00
b7e630c1d8 chore: harden runner hygiene with aggressive cleanup
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 42s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m14s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m53s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-04-24 11:05:56 +02:00
13c8bc29ae chore: add blender-mv utility
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 4s
Monorepo Pipeline / 🧪 Test (push) Successful in 54s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m43s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m14s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 8s
2026-04-21 18:47:53 +02:00
51a892fd76 fix(next-config): harden rewrite precedence
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m11s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m15s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 17s
2026-04-14 15:38:30 +02:00
9cc8e573ec fix(next-config): merge custom rewrites properly instead of overriding
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m7s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m13s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-04-14 14:48:02 +02:00
a80464abc0 chore: repair lockfile mismatch to unblock CI
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 56s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m18s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m28s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 39s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 43s
Monorepo Pipeline / 🚀 Release (push) Successful in 2m23s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 2m40s
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 7s
2026-04-13 15:44:09 +02:00
0b929d00a1 fix(next-config): move serverActions to experimental for Next.js 15+ compatibility
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 18s
Monorepo Pipeline / 🧪 Test (push) Failing after 10s
Monorepo Pipeline / 🏗️ Build (push) Failing after 10s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-04-13 12:30:31 +02:00
8ec2ecb454 fix(gatekeeper): bypass image optimizer for branding assets to fix missing assets in subpath deployments
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m13s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m13s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m30s
Monorepo Pipeline / 🚀 Release (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been cancelled
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been cancelled
2026-04-13 12:26:45 +02:00
2a9c5780c3 fix: gitea mcp issues
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 4s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m41s
Monorepo Pipeline / 🧹 Lint (push) Successful in 5m17s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m16s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 11s
2026-03-12 13:33:10 +01:00
fbfc9feab0 fix(next-config): add serverActions.allowedOrigins to support branch deployments
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 25s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m28s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m22s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m3s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-03-10 23:29:12 +01:00
8486261555 fix(mcp): refactor all mcp servers to use multi-session sse transport
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m1s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m44s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m55s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-03-10 13:32:16 +01:00
5e1f2669e6 feat(kabelfachmann-mcp): add local Ollama support for KABELFACHMANN_LLM_PROVIDER
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m4s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m53s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m2s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-03-08 14:01:37 +01:00
541f1c17b7 feat(mcps): add kabelfachmann MCP with Kabelhandbuch integration and remove legacy PM2 orchestration
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m6s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m52s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m1s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-03-08 01:01:43 +01:00
048fafa3db fix(payload-ai): remove phantom SCSS import and disable dynamic provider injection
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m3s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m34s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m56s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
- Remove non-existent ChatWindow.scss import causing Webpack resolution errors
- Disable dynamic ChatWindowProvider injection (now statically declared in host app)
- Revert build script to tsc (no SCSS copy needed)
2026-03-07 11:46:41 +01:00
77e2c4f9b6 ci: remove unused zod dependencies across mcp servers to satisfy depcheck validation
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m21s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m48s
Monorepo Pipeline / 🧹 Lint (push) Successful in 3m59s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 5s
2026-03-06 17:56:14 +01:00
4eb1aaf640 ci: fix strict TS overloaded parameter matching inside useChat and payload tool bindings
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m14s
Monorepo Pipeline / 🧹 Lint (push) Failing after 2m4s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m39s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-03-06 16:10:50 +01:00
61f2f83e0c ci: fix unhandled typescript exceptions and strict eslint errors caught by the pipeline
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 9s
Monorepo Pipeline / 🏗️ Build (push) Failing after 9s
Monorepo Pipeline / 🧪 Test (push) Failing after 9s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-03-06 15:49:45 +01:00
2dc61e4937 chore: finalize containerized MCP setup and fix memory-mcp native module issues
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 9s
Monorepo Pipeline / 🧪 Test (push) Failing after 9s
Monorepo Pipeline / 🏗️ Build (push) Failing after 8s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-03-06 15:20:52 +01:00
103 changed files with 14440 additions and 1972 deletions

View File

@@ -0,0 +1,5 @@
---
"@mintel/next-config": patch
---
fix: add serverActions.allowedOrigins to support branch deployments

18
.env
View File

@@ -3,6 +3,7 @@ IMAGE_TAG=v1.8.19
PROJECT_NAME=at-mintel
PROJECT_COLOR=#82ed20
GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582
GITEA_HOST=https://git.infra.mintel.me
OPENROUTER_API_KEY=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
ZYTE_API_KEY=1f0f74726f044f55aaafc7ead32cd489
REPLICATE_API_KEY=r8_W3grtpXMRfi0u3AM9VdkKbuWdZMmhwU2Tn0yt
@@ -11,6 +12,11 @@ DATA_FOR_SEO_API_KEY=bWFyY0BtaW50ZWwubWU6MjQ0YjBjZmIzOGY3NTIzZA==
DATA_FOR_SEO_LOGIN=marc@mintel.me
DATA_FOR_SEO_PASSWORD=244b0cfb38f7523d
# Kabelfachmann LLM Configuration
KABELFACHMANN_LLM_PROVIDER=openrouter
KABELFACHMANN_OLLAMA_MODEL=qwen3.5
KABELFACHMANN_OLLAMA_HOST=http://host.docker.internal:11434
# Authentication
GATEKEEPER_PASSWORD=mintel
AUTH_COOKIE_NAME=mintel_gatekeeper_session
@@ -41,3 +47,15 @@ SENTRY_DSN=
# Analytics (Umami)
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
UMAMI_USERNAME=mmintel
UMAMI_PASSWORD=Tim300493.
# MCP Services
QDRANT_URL=http://qdrant:6333
SERPBEAR_API_KEY=Tim300493.
GLITCHTIP_API_KEY=3d37db66fa1fc3a1cdcd1357728b056225aca950f0163b50c5e922344807ede9
# Corrected MCP Base URLs
UMAMI_BASE_URL=https://analytics.infra.mintel.me
GLITCHTIP_BASE_URL=https://errors.infra.mintel.me
SERPBEAR_BASE_URL=https://seo.infra.mintel.me

View File

@@ -34,29 +34,34 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- name: 🔐 Registry Auth
run: |
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN || secrets.MINTEL_PRIVATE_TOKEN || secrets.GITEA_PAT }}" >> .npmrc
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
echo "@mintel:registry=https://$REGISTRY" > .npmrc
echo "//$REGISTRY/:_authToken=${{ secrets.NPM_TOKEN || secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}" >> .npmrc
- name: 🔍 Diagnostics
run: |
echo "👤 User: $(id)"
echo "📂 Directory: $(pwd)"
echo "📂 Disk Space:"
df -h
- name: Install dependencies
run: |
pnpm store prune
pnpm install --no-frozen-lockfile
rm -rf .next .turbo node_modules || true
pnpm install --no-frozen-lockfile --reporter=append-only
- name: 📦 Archive dependencies
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: node_modules
path: |
node_modules
.npmrc
path: node_modules
retention-days: 1
static:
@@ -77,7 +82,7 @@ jobs:
with:
node-version: 20
- name: 📥 Restore dependencies
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: node_modules
- name: 🌐 HTML Validation
@@ -109,7 +114,7 @@ jobs:
with:
node-version: 20
- name: 📥 Restore dependencies
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: node_modules
- name: 🔍 Install Chromium
@@ -147,7 +152,7 @@ jobs:
with:
node-version: 20
- name: 📥 Restore dependencies
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: node_modules
- name: 📦 Depcheck
@@ -177,7 +182,7 @@ jobs:
with:
version: 10
- name: 📥 Restore dependencies
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: node_modules
- name: 🔍 Install Chromium

6
.gitignore vendored
View File

@@ -50,4 +50,8 @@ apps/web/out/estimations/
# Memory MCP
data/qdrant/
packages/memory-mcp/models/
packages/memory-mcp/models/
# Kabelfachmann MCP
packages/kabelfachmann-mcp/data/
packages/kabelfachmann-mcp/models/

46
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,46 @@
# Mintel Universal Design System - Architecture Manifesto
Dieses Dokument ist der absolute Nordstern für alle Entwickler und KI-Agenten, die an der Mintel Codebase arbeiten. Es definiert, wie wir UI-Komponenten bauen, isolieren und skalieren, ohne in eine "Lock-in"-Falle zu tappen.
## 1. Das Problem: Die Design System Falle
Wir bauen eine neutrale Basis (`@mintel/ui`), auf der Web, Print (PDF), Präsentationen und Remotion-Videos basieren.
Das größte Risiko: Komponenten werden im Laufe der Zeit so komplex (mit dutzenden `if/else`-Switches für Kunden-Sonderfälle), dass niemand sich mehr traut, sie anzufassen, aus Angst, andere Projekte zu zerstören.
## 2. Die Zwiebel-Architektur (Core vs. Components)
Um dieses Problem zu lösen, teilen wir unser UI-Paket logisch in zwei Ebenen:
### Tier 1: Primitives (Der Kern)
- **Was ist das?** Die absoluten Grundbausteine: `<Box>`, `<Text>`, `<Flex>`, `<Grid>`.
- **Die Regel:** Diese Komponenten haben *keine* Meinung. Sie dienen ausschließlich als saubere, typisierte Wrapper, um Tailwind-Klassen und Props durchzureichen. Sie dürfen sich architektonisch nie ändern.
- **Warum?** Weil Projekte im Notfall immer auf diese Bausteine zurückfallen können, um eigene, hochgradig individuelle Layouts zu bauen.
### Tier 2: Base Components (Der Mintel Standard)
- **Was ist das?** Fertige Komponenten wie `<Button>`, `<Card>`, `<HeroSection>`.
- **Die Regel:** Sie haben den "neutralen" Mintel-Standardlook. Sie bauen zu 100% auf Tier 1 Primitives auf.
## 3. Die 3 Goldenen Regeln gegen den "Lock-in"
Wenn ein spezifisches Projekt (z.B. ein neuer Kunde) eine Komponente braucht, die radikal vom Mintel-Standard abweicht, gelten folgende Eskalationsstufen:
### Eskalation 1: Compound Components
Komponenten dürfen niemals gigantische Props-Monster (`<Card hideHeader={true} showFooter={false} />`) werden.
Wir nutzen **Compound Components**: Das Projekt setzt die Card selbst aus `<Card.Header>`, `<Card.Body>` und `<Card.Footer>` zusammen. Braucht das Projekt den Header nicht, lässt es ihn einfach weg.
### Eskalation 2: Das `asChild` Pattern (Slot)
Wir sperren Projekte niemals in unsere DOM-Struktur ein. Wenn eine Komponente Logik enthält, nutzen wir das `asChild` (oder Slot) Pattern.
*Beispiel:* Ein `<Button asChild>` rendert kein `<button>` HTML-Tag, sondern reicht seine Tailwind-Styles und seine Accessibility-Logik transparent an das Kind-Element durch (z.B. einen Next.js `<Link>`).
### Eskalation 3: Der "Eject" (Der Shadcn-Weg)
Wenn eine `@mintel/ui` Komponente absolut nicht mehr für den Kunden X passt, wird sie **nicht** durch `if (kunde === 'X')`-Logik in der Basis "aufgebohrt".
Stattdessen **ejecten** (kopieren) wir den Source-Code der Komponente physisch in das Projekt von Kunde X (`apps/kunde-x/components/`). Ab diesem Moment "besitzt" das Projekt diese Komponente und kann sie nach Belieben modifizieren. Die neutrale Basis bleibt sauber!
## 4. Agenten & LLM Workflow
Jeder KI-Agent, der eine neue Komponente für `@mintel/ui` baut, muss folgende Checkliste abarbeiten:
1. **Ladle-First (TDD):** Keine Komponente wird ohne eine dazugehörige `.stories.tsx` Datei für den Ladle-Katalog akzeptiert. Ladle ist unsere Source of Truth.
2. **Keine Business-Logik:** Die Komponente darf keinen State (außer internen UI-State wie `isOpen`), keine API-Fetches und keine projektspezifischen Konstanten enthalten.
3. **Universalität prüfen:** Die Komponente darf keine reinen Browser-APIs (`window.location`) ohne Fallbacks nutzen, damit sie auch serverseitig in der Print-Engine (PDF) und in der Remotion-Engine gerendert werden kann.
4. **Tailwind pur:** Nutze ausschließlich Tailwind-Klassen. Keine externen CSS-Module oder `style`-Tags, wenn nicht physikalisch zwingend (z.B. bei dynamischer Frame-Interpolation in Videos).
---
*Wer dieses Manifest bricht, sabotiert die Skalierbarkeit des Monorepos. Halte dich an die Zwiebel-Architektur!*

24
apps/example-presentation/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@@ -0,0 +1,22 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
},
},
])

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>example-presentation</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,31 @@
{
"name": "example-presentation",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@mintel/ui": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"spectacle": "^10.0.0"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.18.2",
"vite": "^6.0.5"
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,184 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { Deck, Slide, Heading, Text as SpectacleText } from 'spectacle';
import { Box, Text, EnvironmentProvider } from '@mintel/ui';
function App() {
return (
<EnvironmentProvider env="presentation">
<Deck>
<Slide>
<Box className="flex flex-col items-center justify-center h-full">
<Heading>Mintel Universal Design System</Heading>
<SpectacleText>One Codebase. Web, Print, and Presentations.</SpectacleText>
<Box className="mt-8 p-4 bg-mintel-primary rounded-xl shadow-xl">
<Text className="text-white text-xl">
This Box and Text comes directly from @mintel/ui!
</Text>
</Box>
</Box>
</Slide>
</Deck>
</EnvironmentProvider>
);
}
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,111 @@
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
#root {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
body {
margin: 0;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}

View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

View File

@@ -0,0 +1,19 @@
{
"name": "example-print",
"version": "1.0.0",
"private": true,
"main": "index.js",
"scripts": {
"build:pdf": "ts-node src/generator.tsx"
},
"dependencies": {
"@mintel/ui": "workspace:*",
"@react-pdf/renderer": "^3.4.0",
"react": "^18.2.0"
},
"devDependencies": {
"ts-node": "^10.9.2",
"typescript": "^5.0.0",
"@types/react": "^18.2.0"
}
}

View File

@@ -0,0 +1,30 @@
import React from 'react';
import ReactPDF, { Document, Page } from '@react-pdf/renderer';
import { Box, Text, EnvironmentProvider } from '@mintel/ui';
const InvoiceDocument = () => (
<EnvironmentProvider env="print">
<Document>
<Page size="A4" style={{ padding: 40, backgroundColor: '#ffffff' }}>
<Box pdfStyle={{ marginBottom: 20, borderBottom: '2px solid #1E40AF', paddingBottom: 10 }}>
<Text pdfStyle={{ fontSize: 28, color: '#1E40AF', fontWeight: 'bold' }}>
Mintel Universal Design
</Text>
</Box>
<Box pdfStyle={{ padding: 20, backgroundColor: '#f3f4f6', borderRadius: 8 }}>
<Text pdfStyle={{ fontSize: 14, color: '#374151' }}>
This PDF is generated using the exact same React components as the Web application.
</Text>
</Box>
</Page>
</Document>
</EnvironmentProvider>
);
const generatePDF = async () => {
console.log('Generating Universal PDF...');
await ReactPDF.render(<InvoiceDocument />, `${__dirname}/output.pdf`);
console.log('Successfully saved to output.pdf');
};
generatePDF();

View File

@@ -0,0 +1,22 @@
{
"name": "@mintel/example-video",
"version": "1.0.0",
"description": "Remotion video project using Mintel UI",
"scripts": {
"start": "remotion studio",
"build": "remotion render"
},
"dependencies": {
"@mintel/ui": "workspace:*",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"remotion": "^4.0.0"
},
"devDependencies": {
"@remotion/cli": "^4.0.0",
"@remotion/tailwind": "^4.0.0",
"typescript": "^5.0.0",
"@types/react": "^18.0.0",
"tailwindcss": "^3.0.0"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -0,0 +1,4 @@
import { Config } from "@remotion/cli/config";
Config.setVideoImageFormat("jpeg");
Config.setOverwriteOutput(true);

View File

@@ -0,0 +1,40 @@
import React from "react";
import { useCurrentFrame, useVideoConfig, interpolate } from "remotion";
import { Box, Text } from "@mintel/ui";
import "../../ui/index.css"; // Ensure Tailwind CSS from UI package is loaded
export const MintelPromo: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Fade in over the first 1 second (30 frames)
const opacity = interpolate(frame, [0, fps], [0, 1], {
extrapolateRight: "clamp",
});
// Slide up over the first 1 second
const translateY = interpolate(frame, [0, fps], [50, 0], {
extrapolateRight: "clamp",
});
return (
<div style={{ flex: 1, backgroundColor: "#f3f4f6" }}>
<Box
className="w-full h-full flex items-center justify-center bg-gray-100"
style={{ opacity }}
>
<Box
className="bg-white p-12 rounded-3xl shadow-2xl flex flex-col items-center"
style={{ transform: `translateY(${translateY}px)` }}
>
<Text variant="h1" className="text-blue-600 mb-4 text-8xl font-bold">
Mintel Video
</Text>
<Text variant="body" className="text-gray-600 text-3xl">
Powered by Remotion & @mintel/ui
</Text>
</Box>
</Box>
</div>
);
};

View File

@@ -0,0 +1,18 @@
import React from "react";
import { Composition } from "remotion";
import { MintelPromo } from "./Composition";
export const RemotionRoot: React.FC = () => {
return (
<>
<Composition
id="MintelPromo"
component={MintelPromo}
durationInFrames={150}
fps={30}
width={1920}
height={1080}
/>
</>
);
};

View File

@@ -0,0 +1,4 @@
import { registerRoot } from "remotion";
import { RemotionRoot } from "./Root";
registerRoot(RemotionRoot);

View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{ts,tsx}",
"../../packages/ui/src/**/*.{ts,tsx}"
],
theme: {
extend: {},
},
plugins: [],
};

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}

41
apps/example-web/.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,5 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

View File

@@ -0,0 +1 @@
@AGENTS.md

View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

View File

@@ -0,0 +1,26 @@
{
"name": "example-web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"next": "16.2.7",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.7",
"tailwindcss": "^4",
"typescript": "^5"
}
}

4077
apps/example-web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
ignoredBuiltDependencies:
- sharp
- unrs-resolver

View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View File

@@ -0,0 +1,33 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
}

View File

@@ -0,0 +1,65 @@
import Image from "next/image";
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}

177
blender-mv.sh Executable file
View File

@@ -0,0 +1,177 @@
#!/usr/bin/env bash
# blender-mv: A smart file/directory mover that automatically updates Blender paths
set -e
print_usage() {
echo "Usage: blender-mv [OPTIONS] SOURCE DEST"
echo ""
echo "Options:"
echo " --search-project <dir> Scan this specific project directory for .blend files"
echo " to update (useful if you move an external asset folder,"
echo " and old .blend files referencing it are elsewhere)."
echo ""
echo "Example: blender-mv ./old_folder ./new_folder"
}
SEARCH_PROJECT=""
# Parse arguments
while [[ "$#" -gt 0 ]]; do
case $1 in
--search-project) SEARCH_PROJECT="$2"; shift 2 ;;
-h|--help) print_usage; exit 0 ;;
*)
if [[ -z "$SOURCE" ]]; then
SOURCE="$1"
elif [[ -z "$DEST" ]]; then
DEST="$1"
else
echo "Unknown parameter passed: $1"
print_usage
exit 1
fi
shift
;;
esac
done
if [[ -z "$SOURCE" || -z "$DEST" ]]; then
echo "Error: Both SOURCE and DEST must be provided."
print_usage
exit 1
fi
if [[ ! -e "$SOURCE" ]]; then
echo "Error: Source path '$SOURCE' does not exist."
exit 1
fi
# Function to get absolute normalized path (handles missing destination gracefully)
get_abs_path() {
python3 -c 'import os,sys; print(os.path.normpath(os.path.abspath(sys.argv[1])))' "$1" 2>/dev/null
}
# Resolve source before moving
ABS_SOURCE=$(get_abs_path "$SOURCE")
# Determine resulting target path
if [[ -d "$DEST" ]]; then
# If DEST is an existing dir, standard mv puts SOURCE inside it
BASENAME_SOURCE=$(basename "$SOURCE")
ABS_DEST=$(get_abs_path "$DEST/$BASENAME_SOURCE")
else
# Placed exactly at DEST (rename or target dir doesn't exist yet)
ABS_DEST=$(get_abs_path "$DEST")
fi
echo "Moving: $SOURCE -> $DEST"
mv "$SOURCE" "$DEST"
echo "Replacing paths in Blender's recent-files lists..."
# Find all recent-files.txt across Blender versions and replace the string
find "$HOME/Library/Application Support/Blender" -type f -name "recent-files.txt" 2>/dev/null | while read -r RECENT_FILE; do
# Using sed with alternative delimiter to avoid path interference
sed -i '' "s|${ABS_SOURCE}|${ABS_DEST}|g" "$RECENT_FILE"
done
# Prepare the embedded Python payload to update blend paths
TMP_PY=$(mktemp /tmp/blender_update_paths.XXXXXX.py)
cat << 'EOF' > "$TMP_PY"
import bpy
import sys
def replace_in_prop(prop, old_path, new_path):
if prop and isinstance(prop, str) and old_path in prop:
return prop.replace(old_path, new_path)
return prop
def run():
# Args are passed after `--`
if '--' not in sys.argv:
return
idx = sys.argv.index('--')
if len(sys.argv) <= idx + 2:
return
old_path = sys.argv[idx + 1]
new_path = sys.argv[idx + 2]
changed = False
# 1. Libraries (Linked objects/collections)
for lib in bpy.data.libraries:
new_fp = replace_in_prop(lib.filepath, old_path, new_path)
if new_fp != lib.filepath:
lib.filepath = new_fp
changed = True
# 2. Images (Textures, Env Maps)
for img in bpy.data.images:
new_fp = replace_in_prop(img.filepath, old_path, new_path)
if new_fp != img.filepath:
img.filepath = new_fp
changed = True
# 3. Cache files (Alembic/VDB)
for cache in bpy.data.cache_files:
new_fp = replace_in_prop(cache.filepath, old_path, new_path)
if new_fp != cache.filepath:
cache.filepath = new_fp
changed = True
# 4. Fonts
for font in bpy.data.fonts:
new_fp = replace_in_prop(font.filepath, old_path, new_path)
if new_fp != font.filepath:
font.filepath = new_fp
changed = True
# 5. Video strips
for scene in bpy.data.scenes:
if scene.sequence_editor:
for strip in scene.sequence_editor.sequences_all:
if strip.type in ('MOVIE', 'IMAGE'):
new_fp = replace_in_prop(strip.filepath, old_path, new_path)
if new_fp != strip.filepath:
strip.filepath = new_fp
changed = True
if changed:
print(f"[*] Updated paths in {bpy.data.filepath}")
bpy.ops.wm.save_mainfile()
try:
run()
except Exception as e:
print(f"[!] Error updating blend file paths: {e}")
EOF
# Determine what to search for blend files
if [[ -n "$SEARCH_PROJECT" ]]; then
SEARCH_ROOT=$(get_abs_path "$SEARCH_PROJECT")
echo "Scanning project root '$SEARCH_ROOT' for .blend files..."
elif [[ -d "$ABS_DEST" ]]; then
SEARCH_ROOT="$ABS_DEST"
echo "Scanning moved directory for .blend files..."
elif [[ "$ABS_DEST" == *.blend ]]; then
SEARCH_ROOT="$ABS_DEST"
echo "Updating moved .blend file..."
else
echo "No project root defined and moved object is not a directory or .blend file."
echo "Finished."
rm -f "$TMP_PY"
exit 0
fi
# Execute headless blender on all found blend files
if [[ -d "$SEARCH_ROOT" ]]; then
find "$SEARCH_ROOT" -type f -name "*.blend" -print0 | while IFS= read -r -d '' BLEND_FILE; do
blender -b "$BLEND_FILE" -P "$TMP_PY" -- "$ABS_SOURCE" "$ABS_DEST" >/dev/null 2>&1 || echo "Warning: Failed to process $BLEND_FILE"
done
elif [[ -f "$SEARCH_ROOT" && "$SEARCH_ROOT" == *.blend ]]; then
blender -b "$SEARCH_ROOT" -P "$TMP_PY" -- "$ABS_SOURCE" "$ABS_DEST" >/dev/null 2>&1 || echo "Warning: Failed to process $SEARCH_ROOT"
fi
rm -f "$TMP_PY"
echo "Success: Move complete and paths updated."

View File

@@ -3,14 +3,88 @@ services:
image: qdrant/qdrant:latest
container_name: qdrant-mcp
ports:
- "6333:6333"
- "6334:6334"
- "6335:6333"
- "6336:6334"
volumes:
- ./data/qdrant:/qdrant/storage
restart: unless-stopped
networks:
- mcp-network
gitea-mcp:
build:
context: ./packages/gitea-mcp
container_name: gitea-mcp
env_file:
- .env
ports:
- "3011:3001"
restart: unless-stopped
networks:
- mcp-network
memory-mcp:
build:
context: ./packages/memory-mcp
container_name: memory-mcp
env_file:
- .env
ports:
- "3002:3002"
depends_on:
- qdrant
restart: unless-stopped
networks:
- mcp-network
umami-mcp:
build:
context: ./packages/umami-mcp
container_name: umami-mcp
env_file:
- .env
ports:
- "3003:3003"
restart: unless-stopped
networks:
- mcp-network
serpbear-mcp:
build:
context: ./packages/serpbear-mcp
container_name: serpbear-mcp
env_file:
- .env
ports:
- "3004:3004"
restart: unless-stopped
networks:
- mcp-network
glitchtip-mcp:
build:
context: ./packages/glitchtip-mcp
container_name: glitchtip-mcp
env_file:
- .env
ports:
- "3005:3005"
restart: unless-stopped
networks:
- mcp-network
klz-payload-mcp:
build:
context: ./packages/klz-payload-mcp
container_name: klz-payload-mcp
env_file:
- .env
ports:
- "3006:3006"
restart: unless-stopped
networks:
- mcp-network
networks:
mcp-network:
driver: bridge

View File

@@ -22,6 +22,43 @@ services:
- "caddy=http://${TRAEFIK_HOST:-acquisition.localhost}"
- "caddy.reverse_proxy={{upstreams 3000}}"
at-mintel-ui:
image: node:22-alpine
working_dir: /app
volumes:
- .:/app
- ui_pnpm_store:/pnpm
environment:
- CI=true
networks:
- infra
restart: unless-stopped
command: >
sh -c "corepack enable && pnpm config set store-dir /pnpm && pnpm install --no-frozen-lockfile --ignore-scripts || true; echo 'Starting ladle...'; pnpm --filter @mintel/ui exec ladle serve --host 0.0.0.0 --port 61000"
labels:
- "caddy=http://ui.localhost"
- "caddy.reverse_proxy={{upstreams 61000}}"
at-mintel-video:
image: node:22-alpine
working_dir: /app
volumes:
- .:/app
- ui_pnpm_store:/pnpm
environment:
- CI=true
networks:
- infra
restart: unless-stopped
command: >
sh -c "corepack enable && pnpm config set store-dir /pnpm && pnpm install --no-frozen-lockfile --ignore-scripts || true; pnpm --filter @mintel/example-video exec remotion studio --host 0.0.0.0 --port 3001"
labels:
- "caddy=http://video.localhost"
- "caddy.reverse_proxy={{upstreams 3001}}"
networks:
infra:
external: true
volumes:
ui_pnpm_store:

View File

@@ -1,48 +0,0 @@
module.exports = {
apps: [
{
name: 'gitea-mcp',
script: 'node',
args: 'dist/start.js',
cwd: './packages/gitea-mcp',
watch: false,
},
{
name: 'memory-mcp',
script: 'node',
args: 'dist/start.js',
cwd: './packages/memory-mcp',
watch: false,
},
{
name: 'umami-mcp',
script: 'node',
args: 'dist/start.js',
cwd: './packages/umami-mcp',
watch: false,
},
{
name: 'serpbear-mcp',
script: 'node',
args: 'dist/start.js',
cwd: './packages/serpbear-mcp',
watch: false,
},
{
name: 'glitchtip-mcp',
script: 'node',
args: 'dist/start.js',
cwd: './packages/glitchtip-mcp',
watch: false,
},
{
name: 'klz-payload-mcp',
script: 'node',
args: 'dist/start.js',
cwd: './packages/klz-payload-mcp',
watch: false,
},
]
};

69
eslint-errors-2.txt Normal file
View File

@@ -0,0 +1,69 @@

/Users/marcmintel/Projects/at-mintel/packages/gitea-mcp/src/index.ts
 11:0 error Parsing error: Identifier expected

/Users/marcmintel/Projects/at-mintel/packages/glitchtip-mcp/src/index.ts
 124:19 warning 'res' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/klz-payload-mcp/src/index.ts
 39:18 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/memory-mcp/src/qdrant.test.ts
 7:52 warning 'text' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/page-audit/src/report.ts
 7:47 warning 'PageAuditData' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 7:62 warning 'AuditIssue' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/AiFieldButton.tsx
 11:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateSlugButton.tsx
 20:21 warning 'replaceState' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 21:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateThumbnailButton.tsx
 21:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/OptimizeButton.tsx
 5:10 warning 'Button' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/mcpAdapter.ts
 44:15 warning 'toolSchema' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/memoryDb.ts
 89:31 warning 'query' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/payloadLocal.ts
 3:40 warning 'User' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/types.ts
 1:15 warning 'Plugin' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/ConceptPDF.tsx
 4:18 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:10 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/EstimationPDF.tsx
 4:18 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:10 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 54:11 warning 'getPageNum' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/InfoPDF.tsx
 5:13 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 12:5 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SharedUI.tsx
 528:5 warning 'bankData' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SimpleLayout.tsx
 4:52 warning 'PDFText' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:26 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/seo-engine/src/report.ts
 5:3 warning 'TopicCluster' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 6:3 warning 'ContentGap' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 7:3 warning 'CompetitorRanking' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

✖ 28 problems (1 error, 27 warnings)


97
eslint-errors.txt Normal file
View File

@@ -0,0 +1,97 @@

/Users/marcmintel/Projects/at-mintel/packages/gitea-mcp/src/index.ts
 12:5 warning 'Resource' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 14:10 warning 'z' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 427:30 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
 745:50 error Unnecessary escape character: \/ no-useless-escape
 745:60 error Unnecessary escape character: \/ no-useless-escape
 799:54 error Unnecessary escape character: \/ no-useless-escape
 799:64 error Unnecessary escape character: \/ no-useless-escape

/Users/marcmintel/Projects/at-mintel/packages/glitchtip-mcp/src/index.ts
 124:19 warning 'res' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/klz-payload-mcp/src/index.ts
 39:18 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/memory-mcp/src/qdrant.test.ts
 7:52 warning 'text' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/page-audit/src/report.ts
 7:47 warning 'PageAuditData' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 7:62 warning 'AuditIssue' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/chatPlugin.ts
 1:15 warning 'Config' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 10:17 error 'config' is never reassigned. Use 'const' instead prefer-const
 48:37 warning 'req' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/ChatWindow/index.tsx
 43:5 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 44:63 warning 'setMessages' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/AiFieldButton.tsx
 11:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateSlugButton.tsx
 20:21 warning 'replaceState' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 21:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateThumbnailButton.tsx
 21:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/OptimizeButton.tsx
 5:10 warning 'Button' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/endpoints/chatEndpoint.ts
 96:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 100:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/mcpAdapter.ts
 44:15 warning 'toolSchema' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 53:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/memoryDb.ts
 50:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 88:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 89:31 warning 'query' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/payloadLocal.ts
 3:40 warning 'User' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 25:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 45:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 61:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 78:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 95:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/types.ts
 1:15 warning 'Plugin' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/ConceptPDF.tsx
 4:18 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:10 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/EstimationPDF.tsx
 4:18 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:10 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 54:11 warning 'getPageNum' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/InfoPDF.tsx
 5:13 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 12:5 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SharedUI.tsx
 528:5 warning 'bankData' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SimpleLayout.tsx
 4:52 warning 'PDFText' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:26 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/seo-engine/src/report.ts
 5:3 warning 'TopicCluster' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 6:3 warning 'ContentGap' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 7:3 warning 'CompetitorRanking' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

✖ 49 problems (16 errors, 33 warnings)
 1 error and 0 warnings potentially fixable with the `--fix` option.


View File

@@ -6,12 +6,12 @@
"build": "pnpm -r build",
"dev": "pnpm -r dev",
"dev:gatekeeper": "bash -c 'trap \"COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml down && COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml up --build --remove-orphans'",
"dev:mcps:up": "docker-compose -f docker-compose.mcps.yml up -d",
"dev:mcps:down": "docker-compose -f docker-compose.mcps.yml down && pm2 delete ecosystem.mcps.config.cjs || true",
"dev:mcps:up": "docker-compose -f docker-compose.mcps.yml up -d --build --remove-orphans",
"dev:mcps:down": "docker-compose -f docker-compose.mcps.yml down",
"dev:mcps:watch": "pnpm -r --filter=\"./packages/*-mcp\" exec tsc -w",
"dev:mcps": "npm run dev:mcps:up && pm2 start ecosystem.mcps.config.cjs --watch && npm run dev:mcps:watch",
"start:mcps:run": "pm2 start ecosystem.mcps.config.cjs",
"start:mcps": "npm run dev:mcps:up && npm run start:mcps:run",
"dev:mcps": "npm run dev:mcps:up && npm run dev:mcps:watch",
"start:mcps": "npm run dev:mcps:up",
"start:mcps:force": "docker-compose -f docker-compose.mcps.yml up -d --build --force-recreate --remove-orphans",
"lint": "pnpm -r --filter='./packages/**' --filter='./apps/**' lint",
"test": "pnpm -r test",
"changeset": "changeset",
@@ -75,4 +75,4 @@
"@sentry/nextjs": "10.38.0"
}
}
}
}

View File

@@ -1,7 +1,6 @@
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { ShieldCheck } from "lucide-react";
import Image from "next/image";
import { GateScene } from "../../components/gate-scene";
import { AnimatedLoginForm } from "../../components/animated-login-form";
@@ -165,14 +164,13 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
rel="noopener noreferrer"
className="w-14 h-14 bg-white rounded-2xl flex items-center justify-center shadow-lg shadow-black/[0.06] hover:scale-105 hover:shadow-black/10 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] border border-black/[0.06] hover:border-black/10"
>
<Image
<img
src="/gatekeeper/icon-white.svg"
alt="Mintel"
style={{ filter: "invert(1)" }}
width={28}
height={28}
className="w-7 h-7 opacity-80"
unoptimized
/>
</a>
</div>
@@ -214,14 +212,13 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
rel="noopener noreferrer"
className="opacity-30 transition-opacity hover:opacity-60"
>
<Image
<img
src="/gatekeeper/logo-white.svg"
alt={projectName}
width={120}
height={36}
className="h-5 sm:h-6 w-auto"
style={{ filter: "invert(1)" }}
unoptimized
/>
</a>
<p className="text-[7px] font-sans font-semibold text-black/25 uppercase tracking-[0.5em] text-center">

View File

@@ -2,7 +2,7 @@ FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
RUN corepack enable pnpm && pnpm install
RUN npm install -g pnpm@10.18.3 && pnpm install
COPY tsconfig.json ./
COPY src ./src
@@ -15,5 +15,5 @@ COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
# Use node to run the compiled index.js
ENTRYPOINT ["node", "dist/index.js"]
# Use node to run the compiled start.js
ENTRYPOINT ["node", "dist/start.js"]

View File

@@ -12,11 +12,10 @@
"@modelcontextprotocol/sdk": "^1.5.0",
"axios": "^1.7.2",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"zod": "^3.23.8"
"express": "^4.19.2"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/express": "^4.17.21",
"@types/node": "^20.14.10",
"typescript": "^5.5.3"
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,9 @@ import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
// Try to load .env.local first (contains credentials usually)
config({ path: resolve(__dirname, '../../../.env.local') });
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
// Fallback to .env (contains defaults)
config({ path: resolve(__dirname, '../../../.env') });
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
// Now boot the compiled MCP index
import('./index.js').catch(err => {

View File

@@ -0,0 +1,15 @@
FROM node:20-bookworm-slim AS builder
WORKDIR /app
COPY package.json ./
RUN npm install -g pnpm@10.18.3 && pnpm install --ignore-workspace
COPY tsconfig.json ./
COPY src ./src
RUN pnpm build
FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
ENTRYPOINT ["node", "dist/start.js"]

View File

@@ -13,10 +13,10 @@
"@modelcontextprotocol/sdk": "^1.5.0",
"axios": "^1.7.2",
"dotenv": "^17.3.1",
"express": "^5.2.1"
"express": "^4.19.2"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/express": "^4.17.21",
"@types/node": "^20.14.10",
"tsx": "^4.19.2",
"typescript": "^5.5.3"

View File

@@ -1,6 +1,7 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from 'express';
import express, { Request, Response } from 'express';
import crypto from 'crypto';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
@@ -23,7 +24,25 @@ const httpsAgent = new https.Agent({
const glitchtipClient = axios.create({
baseURL: `${GLITCHTIP_BASE_URL}/api/0`,
headers: { Authorization: `Bearer ${GLITCHTIP_API_KEY}` },
httpsAgent
httpsAgent,
timeout: 10000 // Add timeout to prevent hangs
});
// Logging middleware for GlitchTip client
glitchtipClient.interceptors.request.use((config) => {
console.error(`GlitchTip Request: ${config.method?.toUpperCase()} ${config.url}`);
return config;
}, (error) => {
console.error(`GlitchTip Request Error: ${error.message}`);
return Promise.reject(error);
});
glitchtipClient.interceptors.response.use((response) => {
return response;
}, (error) => {
const msg = error.response?.data?.detail || error.response?.data?.message || error.message;
console.error(`GlitchTip API Error (${error.config?.url}): ${msg}`);
return Promise.reject(error);
});
const LIST_PROJECTS_TOOL: Tool = {
@@ -87,48 +106,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "glitchtip_list_projects") {
try {
try {
if (request.params.name === "glitchtip_list_projects") {
const res = await glitchtipClient.get('/projects/');
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
}
}
if (request.params.name === "glitchtip_list_issues") {
const { organization_slug, project_slug, query, limit = 20 } = request.params.arguments as any;
try {
if (request.params.name === "glitchtip_list_issues") {
const { organization_slug, project_slug, query, limit = 20 } = request.params.arguments as any;
const res = await glitchtipClient.get(`/projects/${organization_slug}/${project_slug}/issues/`, {
params: { query, limit }
});
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
}
}
if (request.params.name === "glitchtip_get_issue_details") {
const { issue_id } = request.params.arguments as any;
try {
if (request.params.name === "glitchtip_get_issue_details") {
const { issue_id } = request.params.arguments as any;
const res = await glitchtipClient.get(`/issues/${issue_id}/`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
}
}
if (request.params.name === "glitchtip_update_issue") {
const { issue_id, status } = request.params.arguments as any;
try {
if (request.params.name === "glitchtip_update_issue") {
const { issue_id, status } = request.params.arguments as any;
const res = await glitchtipClient.put(`/issues/${issue_id}/`, { status });
return { content: [{ type: "text", text: `Issue ${issue_id} status updated to ${status}.` }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
}
}
throw new Error(`Unknown tool: ${request.params.name}`);
throw new Error(`Unknown tool: ${request.params.name}`);
} catch (e: any) {
const msg = e.response?.data?.detail || e.response?.data?.message || e.message;
return { isError: true, content: [{ type: "text", text: `GlitchTip Service Error: ${msg}` }] };
}
});
async function run() {
@@ -141,25 +149,65 @@ async function run() {
console.error('GlitchTip MCP server is running on stdio');
} else {
const app = express();
let transport: SSEServerTransport | null = null;
const transports = new Map<string, SSEServerTransport>();
app.get('/sse', async (req, res) => {
console.error('New SSE connection established');
transport = new SSEServerTransport('/message', res);
await server.connect(transport);
// Manual CORS middleware
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-mcp-protocol-version');
if (req.method === 'OPTIONS') {
res.status(204).end();
return;
}
next();
});
app.post('/message', async (req, res) => {
app.use((req, _res, next) => {
console.error(`${req.method} ${req.url}`);
next();
});
app.get('/sse', async (req, res) => {
const transport = new SSEServerTransport('/message', res as any);
const sessionId = transport.sessionId;
console.error(`New SSE connection: ${sessionId}`);
transports.set(sessionId, transport);
const heartbeatInterval = setInterval(() => {
res.write(": heartbeat\n\n");
}, 15000);
req.on('close', () => {
console.error(`SSE connection closed: ${sessionId}`);
clearInterval(heartbeatInterval);
transports.delete(sessionId);
});
await server.connect(transport);
await new Promise((resolve) => {
req.on('close', resolve);
});
});
app.post('/message', express.json(), async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
console.error(`POST /message/${sessionId} - Active transports: ${transports.size}`);
if (!transport) {
res.status(400).send('No active SSE connection');
console.error(`No transport found for session: ${sessionId}`);
res.status(400).send('No active SSE connection for this session');
return;
}
await transport.handlePostMessage(req, res);
});
const PORT = process.env.GLITCHTIP_MCP_PORT || 3005;
app.listen(PORT, () => {
console.error(`GlitchTip MCP server running on http://localhost:${PORT}/sse`);
const PORT = Number(process.env.GLITCHTIP_MCP_PORT) || 3005;
app.listen(PORT, '0.0.0.0', () => {
console.error(`GlitchTip MCP server running on http://0.0.0.0:${PORT}/sse`);
});
}
}

View File

@@ -4,8 +4,8 @@ import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
config({ path: resolve(__dirname, '../../../.env.local') });
config({ path: resolve(__dirname, '../../../.env') });
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
import('./index.js').catch(err => {
console.error('Failed to start GlitchTip MCP Server:', err);

View File

@@ -1,8 +1,14 @@
#!/bin/bash
set -e
# Configuration
REGISTRY_DATA="/mnt/HC_Volume_104575103/registry-data/docker/registry/v2"
# Find registry data dynamically (volume ID might change)
REGISTRY_DATA=$(find /mnt -maxdepth 5 -type d -path '*/registry-data/docker/registry/v2' | head -n 1)
if [ -z "$REGISTRY_DATA" ]; then
echo "❌ Registry data directory not found!"
exit 1
fi
KEEP_TAGS=3
echo "🏥 Starting Aggressive Registry & Docker Maintenance..."

View File

@@ -0,0 +1,15 @@
FROM node:20-bookworm-slim AS builder
WORKDIR /app
COPY package.json ./
RUN npm install -g pnpm@10.18.3 && pnpm install --ignore-workspace
COPY tsconfig.json ./
COPY src ./src
RUN pnpm build
FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
ENTRYPOINT ["node", "dist/start.js"]

View File

@@ -10,16 +10,15 @@
"dev": "tsx watch src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.27.1",
"@modelcontextprotocol/sdk": "^1.5.0",
"axios": "^1.7.2",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"zod": "^3.23.8"
"express": "^4.19.2"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/express": "^4.17.21",
"@types/node": "^20.14.10",
"typescript": "^5.5.3",
"tsx": "^4.19.2"
"tsx": "^4.19.2",
"typescript": "^5.5.3"
}
}

View File

@@ -7,6 +7,7 @@ import {
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import crypto from "crypto";
import https from "https";
const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://klz-cables.com";
@@ -588,25 +589,60 @@ async function run() {
console.error('KLZ Payload MCP server is running on stdio');
} else {
const app = express();
let transport: SSEServerTransport | null = null;
const transports = new Map<string, SSEServerTransport>();
app.get('/sse', async (req: Request, res: Response) => {
console.error('New SSE connection established');
transport = new SSEServerTransport('/message', res);
await server.connect(transport);
// Manual CORS middleware
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-mcp-protocol-version');
if (req.method === 'OPTIONS') {
res.status(204).end();
return;
}
next();
});
app.post('/message', async (req: Request, res: Response) => {
app.use((req, _res, next) => {
console.error(`${req.method} ${req.url}`);
next();
});
app.get('/sse', async (req, res) => {
const transport = new SSEServerTransport('/message', res as any);
const sessionId = transport.sessionId;
console.error(`New SSE connection: ${sessionId}`);
transports.set(sessionId, transport);
req.on('close', () => {
console.error(`SSE connection closed: ${sessionId}`);
transports.delete(sessionId);
});
await server.connect(transport);
await new Promise((resolve) => {
req.on('close', resolve);
});
});
app.post('/message', express.json(), async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
console.error(`POST /message/${sessionId} - Active transports: ${transports.size}`);
if (!transport) {
res.status(400).send('No active SSE connection');
console.error(`No transport found for session: ${sessionId}`);
res.status(400).send('No active SSE connection for this session');
return;
}
await transport.handlePostMessage(req, res);
});
const PORT = process.env.KLZ_PAYLOAD_MCP_PORT || 3006;
app.listen(PORT, () => {
console.error(`KLZ Payload MCP server running on http://localhost:${PORT}/sse`);
const PORT = Number(process.env.KLZ_PAYLOAD_MCP_PORT) || 3006;
app.listen(PORT, '0.0.0.0', () => {
console.error(`KLZ Payload MCP server running on http://0.0.0.0:${PORT}/sse`);
});
}
}

View File

@@ -4,8 +4,8 @@ import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
config({ path: resolve(__dirname, '../../../.env.local') });
config({ path: resolve(__dirname, '../../../.env') });
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
import('./index.js').catch(err => {
console.error('Failed to start KLZ Payload MCP Server:', err);

View File

@@ -0,0 +1,18 @@
FROM node:20-bookworm-slim AS builder
WORKDIR /app
COPY package.json ./
RUN npm install -g pnpm@10.18.3 && pnpm install --ignore-workspace
RUN for dir in $(find /app/node_modules -type d -name "sharp" | grep "node_modules/sharp$"); do \
echo "module.exports = {};" > "$dir/lib/index.js" || true; \
done
COPY tsconfig.json ./
COPY src ./src
RUN pnpm build
FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
ENTRYPOINT ["node", "dist/start.js"]

View File

@@ -14,15 +14,16 @@
"@modelcontextprotocol/sdk": "^1.5.0",
"@qdrant/js-client-rest": "^1.12.0",
"@xenova/transformers": "^2.17.2",
"onnxruntime-node": "^1.14.0",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"express": "^4.19.2",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/express": "^4.17.21",
"@types/node": "^20.14.10",
"tsx": "^4.19.1",
"typescript": "^5.5.3",
"vitest": "^2.1.3"
}
}
}

View File

@@ -1,6 +1,7 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import express from 'express';
import crypto from 'crypto';
import { z } from 'zod';
import { QdrantMemoryService } from './qdrant.js';
@@ -77,25 +78,65 @@ async function main() {
}
} else {
const app = express();
let transport: SSEServerTransport | null = null;
const transports = new Map<string, SSEServerTransport>();
app.get('/sse', async (req, res) => {
console.error('New SSE connection established');
transport = new SSEServerTransport('/message', res);
await server.connect(transport);
// Manual CORS middleware
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-mcp-protocol-version');
if (req.method === 'OPTIONS') {
res.status(204).end();
return;
}
next();
});
app.post('/message', async (req, res) => {
app.use((req, _res, next) => {
console.error(`${req.method} ${req.url}`);
next();
});
app.get('/sse', async (req, res) => {
const transport = new SSEServerTransport('/message', res as any);
const sessionId = transport.sessionId;
console.error(`New SSE connection: ${sessionId}`);
transports.set(sessionId, transport);
const heartbeatInterval = setInterval(() => {
res.write(": heartbeat\n\n");
}, 15000);
req.on('close', () => {
console.error(`SSE connection closed: ${sessionId}`);
clearInterval(heartbeatInterval);
transports.delete(sessionId);
});
await server.connect(transport);
await new Promise((resolve) => {
req.on('close', resolve);
});
});
app.post('/message', express.json(), async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
console.error(`POST /message/${sessionId} - Active transports: ${transports.size}`);
if (!transport) {
res.status(400).send('No active SSE connection');
console.error(`No transport found for session: ${sessionId}`);
res.status(400).send('No active SSE connection for this session');
return;
}
await transport.handlePostMessage(req, res);
});
const PORT = process.env.MEMORY_MCP_PORT || 3002;
app.listen(PORT, async () => {
console.error(`Memory MCP server running on http://localhost:${PORT}/sse`);
const PORT = Number(process.env.MEMORY_MCP_PORT) || 3002;
app.listen(PORT, '0.0.0.0', async () => {
console.error(`Memory MCP server running on http://0.0.0.0:${PORT}/sse`);
// Initialize dependencies in SSE mode on startup
try {
await qdrantService.initialize();

View File

@@ -5,9 +5,9 @@ import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
// Try to load .env.local first (contains credentials usually)
config({ path: resolve(__dirname, '../../../.env.local') });
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
// Fallback to .env (contains defaults)
config({ path: resolve(__dirname, '../../../.env') });
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
// Now boot the compiled MCP index
import('./index.js').catch(err => {

View File

@@ -53,6 +53,31 @@ const withMintelConfig = (config) => {
let nextConfig = { ...baseNextConfig, ...config };
// Explicitly merge rewrites
nextConfig.rewrites = async () => {
const defaultRewrites = await baseNextConfig.rewrites();
let customRewrites = {};
if (typeof config.rewrites === 'function') {
customRewrites = await config.rewrites();
} else if (Array.isArray(config.rewrites)) {
customRewrites = config.rewrites;
}
if (Array.isArray(customRewrites)) {
return {
beforeFiles: [...customRewrites, ...defaultRewrites],
afterFiles: [],
fallback: [],
};
}
return {
beforeFiles: [...(customRewrites.beforeFiles || []), ...defaultRewrites],
afterFiles: customRewrites.afterFiles || [],
fallback: customRewrites.fallback || [],
};
};
if (hasI18nConfig) {
const withNextIntl = createNextIntlPlugin();
nextConfig = withNextIntl(nextConfig);
@@ -71,3 +96,4 @@ const withMintelConfig = (config) => {
};
export default withMintelConfig;

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/payload-ai",
"version": "1.9.17",
"version": "1.9.18",
"description": "Reusable Payload CMS AI Extensions",
"type": "module",
"scripts": {
@@ -11,12 +11,13 @@
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./components/*": "./dist/components/*",
"./actions/*": "./dist/actions/*",
"./globals/*": "./dist/globals/*",
"./endpoints/*": "./dist/endpoints/*",
"./utils/*": "./dist/utils/*",
"./tools/*": "./dist/tools/*"
"./components/FieldGenerators/*": "./dist/components/FieldGenerators/*.js",
"./components/*": "./dist/components/*.js",
"./actions/*": "./dist/actions/*.js",
"./globals/*": "./dist/globals/*.js",
"./endpoints/*": "./dist/endpoints/*.js",
"./utils/*": "./dist/utils/*.js",
"./tools/*": "./dist/tools/*.js"
},
"peerDependencies": {
"@payloadcms/next": ">=3.0.0",

View File

@@ -1,90 +1,98 @@
import type { Config, Plugin } from 'payload'
import { AIChatPermissionsCollection } from './collections/AIChatPermissions.js'
import type { PayloadChatPluginConfig } from './types.js'
import { optimizePostEndpoint } from './endpoints/optimizeEndpoint.js'
import { generateSlugEndpoint, generateThumbnailEndpoint, generateSingleFieldEndpoint } from './endpoints/generateEndpoints.js'
import type { Plugin } from "payload";
import { AIChatPermissionsCollection } from "./collections/AIChatPermissions.js";
import type { PayloadChatPluginConfig } from "./types.js";
import { optimizePostEndpoint } from "./endpoints/optimizeEndpoint.js";
import {
generateSlugEndpoint,
generateThumbnailEndpoint,
generateSingleFieldEndpoint,
} from "./endpoints/generateEndpoints.js";
export const payloadChatPlugin =
(pluginOptions: PayloadChatPluginConfig): Plugin =>
(incomingConfig) => {
let config = { ...incomingConfig }
(pluginOptions: PayloadChatPluginConfig): Plugin =>
(incomingConfig) => {
const config = { ...incomingConfig };
// If disabled, return config untouched
if (pluginOptions.enabled === false) {
return config
}
// If disabled, return config untouched
if (pluginOptions.enabled === false) {
return config;
}
// 1. Inject the Permissions Collection into the Schema
const existingCollections = config.collections || []
// 1. Inject the Permissions Collection into the Schema
const existingCollections = config.collections || [];
const mcpServers = pluginOptions.mcpServers || []
const mcpServers = pluginOptions.mcpServers || [];
// Dynamically populate the select options for Collections and MCP Servers
const permissionCollection = { ...AIChatPermissionsCollection }
const collectionField = permissionCollection.fields.find(f => 'name' in f && f.name === 'allowedCollections') as any
if (collectionField) {
collectionField.options = existingCollections.map(c => ({
label: c.labels?.singular || c.slug,
value: c.slug
}))
}
// Dynamically populate the select options for Collections and MCP Servers
const permissionCollection = { ...AIChatPermissionsCollection };
const collectionField = permissionCollection.fields.find(
(f) => "name" in f && f.name === "allowedCollections",
) as any;
if (collectionField) {
collectionField.options = existingCollections.map((c) => ({
label: c.labels?.singular || c.slug,
value: c.slug,
}));
}
const mcpField = permissionCollection.fields.find(f => 'name' in f && f.name === 'allowedMcpServers') as any
if (mcpField) {
mcpField.options = mcpServers.map(s => ({
label: s.name,
value: s.name
}))
}
const mcpField = permissionCollection.fields.find(
(f) => "name" in f && f.name === "allowedMcpServers",
) as any;
if (mcpField) {
mcpField.options = mcpServers.map((s) => ({
label: s.name,
value: s.name,
}));
}
config.collections = [...existingCollections, permissionCollection]
config.collections = [...existingCollections, permissionCollection];
// 2. Register Custom API Endpoint for the AI Chat
config.endpoints = [
...(config.endpoints || []),
{
path: '/api/mcp-chat',
method: 'post',
handler: async (req) => {
// Fallback simple handler while developing endpoint logic
return Response.json({ message: "Chat endpoint active" })
},
},
{
path: '/api/mintel-ai/optimize',
method: 'post',
handler: optimizePostEndpoint,
},
{
path: '/api/mintel-ai/generate-slug',
method: 'post',
handler: generateSlugEndpoint,
},
{
path: '/api/mintel-ai/generate-thumbnail',
method: 'post',
handler: generateThumbnailEndpoint,
},
{
path: '/api/mintel-ai/generate-single-field',
method: 'post',
handler: generateSingleFieldEndpoint,
},
]
// 2. Register Custom API Endpoint for the AI Chat
config.endpoints = [
...(config.endpoints || []),
{
path: "/api/mcp-chat",
method: "post",
handler: async (_req) => {
// Fallback simple handler while developing endpoint logic
return Response.json({ message: "Chat endpoint active" });
},
},
{
path: "/api/mintel-ai/optimize",
method: "post",
handler: optimizePostEndpoint,
},
{
path: "/api/mintel-ai/generate-slug",
method: "post",
handler: generateSlugEndpoint,
},
{
path: "/api/mintel-ai/generate-thumbnail",
method: "post",
handler: generateThumbnailEndpoint,
},
{
path: "/api/mintel-ai/generate-single-field",
method: "post",
handler: generateSingleFieldEndpoint,
},
];
// 3. Inject Chat React Component into Admin UI
if (pluginOptions.renderChatBubble !== false) {
config.admin = {
...(config.admin || {}),
components: {
...(config.admin?.components || {}),
providers: [
...(config.admin?.components?.providers || []),
'@mintel/payload-ai/components/ChatWindow#ChatWindowProvider',
],
},
}
}
// 3. Inject Chat React Component into Admin UI
if (pluginOptions.renderChatBubble !== false) {
config.admin = {
...(config.admin || {}),
components: {
...(config.admin?.components || {}),
providers: [
...(config.admin?.components?.providers || []),
"@mintel/payload-ai/components/ChatWindow/index#ChatWindowProvider",
],
},
};
}
return config
}
return config;
};

View File

@@ -1,136 +1,156 @@
'use client'
"use client";
import React, { useState, useEffect } from 'react'
import { useChat } from '@ai-sdk/react'
import './ChatWindow.scss'
import React, { useState, useEffect } from "react";
import { useChat } from "@ai-sdk/react";
export const ChatWindowProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<>
{children}
<ChatWindow />
</>
)
}
export const ChatWindowProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
return (
<>
{children}
<ChatWindow />
</>
);
};
const ChatWindow: React.FC = () => {
const [isOpen, setIsOpen] = useState(false)
const [pageContext, setPageContext] = useState<any>({ url: '' })
const [isOpen, setIsOpen] = useState(false);
const [pageContext, setPageContext] = useState<any>({ url: "" });
useEffect(() => {
if (typeof window !== 'undefined') {
const path = window.location.pathname;
let collectionSlug = null;
let id = null;
// Payload admin URLs are usually /admin/collections/:slug/:id
const match = path.match(/\/collections\/([^/]+)(?:\/([^/]+))?/);
if (match) {
collectionSlug = match[1];
if (match[2] && match[2] !== 'create') {
id = match[2];
}
}
setPageContext({
url: window.location.href,
title: document.title,
collectionSlug,
id
})
useEffect(() => {
if (typeof window !== "undefined") {
const path = window.location.pathname;
let collectionSlug = null;
let id = null;
// Payload admin URLs are usually /admin/collections/:slug/:id
const match = path.match(/\/collections\/([^/]+)(?:\/([^/]+))?/);
if (match) {
collectionSlug = match[1];
if (match[2] && match[2] !== "create") {
id = match[2];
}
}, [isOpen]) // Refresh context when chat is opened
}
// @ts-ignore - AI hook version mismatch between core and react packages
const { messages, input, handleInputChange, handleSubmit, setMessages } = useChat({
api: '/api/mcp-chat',
initialMessages: [],
body: {
pageContext
}
} as any)
setPageContext({
url: window.location.href,
title: document.title,
collectionSlug,
id,
});
}
}, [isOpen]); // Refresh context when chat is opened
// Basic implementation to toggle chat window and submit messages
return (
<div className="payload-mcp-chat-container">
<button
className="payload-mcp-chat-toggle"
onClick={() => setIsOpen(!isOpen)}
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: "/api/mcp-chat",
initialMessages: [],
body: {
pageContext,
},
} as any) as any;
// Basic implementation to toggle chat window and submit messages
return (
<div className="payload-mcp-chat-container">
<button
className="payload-mcp-chat-toggle"
onClick={() => setIsOpen(!isOpen)}
style={{
position: "fixed",
bottom: "20px",
right: "20px",
zIndex: 9999,
padding: "12px 24px",
backgroundColor: "#000",
color: "#fff",
borderRadius: "8px",
border: "none",
cursor: "pointer",
fontWeight: "bold",
}}
>
{isOpen ? "Close AI Chat" : "Ask AI"}
</button>
{isOpen && (
<div
className="payload-mcp-chat-window"
style={{
position: "fixed",
bottom: "80px",
right: "20px",
width: "450px",
height: "650px",
backgroundColor: "#fff",
border: "1px solid #eaeaea",
borderRadius: "12px",
zIndex: 9999,
display: "flex",
flexDirection: "column",
boxShadow: "0 10px 40px rgba(0,0,0,0.1)",
}}
>
<div
className="chat-header"
style={{
padding: "16px",
borderBottom: "1px solid #eaeaea",
backgroundColor: "#f9f9f9",
borderTopLeftRadius: "12px",
borderTopRightRadius: "12px",
}}
>
<h3 style={{ margin: 0, fontSize: "16px" }}>Payload MCP Chat</h3>
</div>
<div
className="chat-messages"
style={{ flex: 1, padding: "16px", overflowY: "auto" }}
>
{messages.map((m: any) => (
<div
key={m.id}
style={{
position: 'fixed',
bottom: '20px',
right: '20px',
zIndex: 9999,
padding: '12px 24px',
backgroundColor: '#000',
color: '#fff',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
fontWeight: 'bold'
marginBottom: "12px",
textAlign: m.role === "user" ? "right" : "left",
}}
>
{isOpen ? 'Close AI Chat' : 'Ask AI'}
</button>
{isOpen && (
>
<div
className="payload-mcp-chat-window"
style={{
position: 'fixed',
bottom: '80px',
right: '20px',
width: '450px',
height: '650px',
backgroundColor: '#fff',
border: '1px solid #eaeaea',
borderRadius: '12px',
zIndex: 9999,
display: 'flex',
flexDirection: 'column',
boxShadow: '0 10px 40px rgba(0,0,0,0.1)'
}}
style={{
display: "inline-block",
padding: "8px 12px",
borderRadius: "8px",
backgroundColor: m.role === "user" ? "#000" : "#f0f0f0",
color: m.role === "user" ? "#fff" : "#000",
maxWidth: "80%",
}}
>
<div className="chat-header" style={{ padding: '16px', borderBottom: '1px solid #eaeaea', backgroundColor: '#f9f9f9', borderTopLeftRadius: '12px', borderTopRightRadius: '12px' }}>
<h3 style={{ margin: 0, fontSize: '16px' }}>Payload MCP Chat</h3>
</div>
<div className="chat-messages" style={{ flex: 1, padding: '16px', overflowY: 'auto' }}>
{messages.map((m: any) => (
<div key={m.id} style={{
marginBottom: '12px',
textAlign: m.role === 'user' ? 'right' : 'left'
}}>
<div style={{
display: 'inline-block',
padding: '8px 12px',
borderRadius: '8px',
backgroundColor: m.role === 'user' ? '#000' : '#f0f0f0',
color: m.role === 'user' ? '#fff' : '#000',
maxWidth: '80%'
}}>
{m.role === 'user' ? 'G: ' : 'AI: '}
{m.content}
</div>
</div>
))}
</div>
<form onSubmit={handleSubmit} style={{ padding: '16px', borderTop: '1px solid #eaeaea' }}>
<input
value={input}
placeholder="Ask me anything or use /commands..."
onChange={handleInputChange}
style={{
width: '100%',
padding: '12px',
borderRadius: '8px',
border: '1px solid #eaeaea',
boxSizing: 'border-box'
}}
/>
</form>
{m.role === "user" ? "G: " : "AI: "}
{m.content}
</div>
)}
</div>
))}
</div>
<form
onSubmit={handleSubmit}
style={{ padding: "16px", borderTop: "1px solid #eaeaea" }}
>
<input
value={input}
placeholder="Ask me anything or use /commands..."
onChange={handleInputChange}
style={{
width: "100%",
padding: "12px",
borderRadius: "8px",
border: "1px solid #eaeaea",
boxSizing: "border-box",
}}
/>
</form>
</div>
)
}
)}
</div>
);
};

View File

@@ -1,115 +1,142 @@
import { streamText } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'
import { generatePayloadLocalTools } from '../tools/payloadLocal.js'
import { createMcpTools } from '../tools/mcpAdapter.js'
import { generateMemoryTools } from '../tools/memoryDb.js'
import type { PayloadRequest } from 'payload'
import { streamText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
import { generatePayloadLocalTools } from "../tools/payloadLocal.js";
import { createMcpTools } from "../tools/mcpAdapter.js";
import { generateMemoryTools } from "../tools/memoryDb.js";
import type { PayloadRequest } from "payload";
const openrouter = createOpenAI({
baseURL: 'https://openrouter.ai/api/v1',
apiKey: process.env.OPENROUTER_API_KEY || 'dummy_key',
})
baseURL: "https://openrouter.ai/api/v1",
apiKey: process.env.OPENROUTER_API_KEY || "dummy_key",
});
export const handleMcpChat = async (req: PayloadRequest) => {
if (!req.user) {
return Response.json({ error: 'Unauthorized. You must be logged in to use AI Chat.' }, { status: 401 })
if (!req.user) {
return Response.json(
{ error: "Unauthorized. You must be logged in to use AI Chat." },
{ status: 401 },
);
}
const { messages, pageContext } = ((await req.json?.()) || {
messages: [],
}) as { messages: any[]; pageContext?: any };
// 1. Check AI Permissions for req.user
// Look up the collection for permissions
const permissionsQuery = await req.payload.find({
collection: "ai-chat-permissions" as any,
where: {
or: [
{ targetUser: { equals: req.user.id } },
{ targetRole: { equals: req.user.role || "admin" } },
],
},
limit: 10,
});
const allowedCollections = new Set<string>();
const allowedMcpServers = new Set<string>();
for (const perm of permissionsQuery.docs) {
if (perm.allowedCollections) {
perm.allowedCollections.forEach((c: string) => allowedCollections.add(c));
}
const { messages, pageContext } = (await req.json?.() || { messages: [] }) as { messages: any[], pageContext?: any }
// 1. Check AI Permissions for req.user
// Look up the collection for permissions
const permissionsQuery = await req.payload.find({
collection: 'ai-chat-permissions' as any,
where: {
or: [
{ targetUser: { equals: req.user.id } },
{ targetRole: { equals: req.user.role || 'admin' } }
]
},
limit: 10
})
const allowedCollections = new Set<string>()
const allowedMcpServers = new Set<string>()
for (const perm of permissionsQuery.docs) {
if (perm.allowedCollections) {
perm.allowedCollections.forEach((c: string) => allowedCollections.add(c))
}
if (perm.allowedMcpServers) {
perm.allowedMcpServers.forEach((s: string) => allowedMcpServers.add(s))
}
if (perm.allowedMcpServers) {
perm.allowedMcpServers.forEach((s: string) => allowedMcpServers.add(s));
}
}
let accessCollections = Array.from(allowedCollections)
if (accessCollections.length === 0) {
// Fallback or demo config if not configured yet
accessCollections = ['users', 'pages', 'posts', 'products', 'leads', 'media']
let accessCollections = Array.from(allowedCollections);
if (accessCollections.length === 0) {
// Fallback or demo config if not configured yet
accessCollections = [
"users",
"pages",
"posts",
"products",
"leads",
"media",
];
}
let activeTools: Record<string, any> = {};
// 2. Generate Payload Local Tools
if (accessCollections.length > 0) {
const payloadTools = generatePayloadLocalTools(
req.payload,
req,
accessCollections,
);
activeTools = { ...activeTools, ...payloadTools };
}
// 3. Connect External MCPs
if (Array.from(allowedMcpServers).includes("gitea")) {
try {
const { tools: giteaTools } = await createMcpTools({
name: "gitea",
command: "npx",
args: [
"-y",
"@modelcontextprotocol/server-gitea",
"--url",
"https://git.mintel.int",
"--token",
process.env.GITEA_TOKEN || "",
],
});
activeTools = { ...activeTools, ...giteaTools };
} catch (e) {
console.error("Failed to connect to Gitea MCP", e);
}
}
let activeTools: Record<string, any> = {}
// 4. Inject Memory Database Tools
// We provide the user ID so memory is partitioned per user
const memoryTools = generateMemoryTools(req.user.id);
activeTools = { ...activeTools, ...memoryTools };
// 2. Generate Payload Local Tools
if (accessCollections.length > 0) {
const payloadTools = generatePayloadLocalTools(req.payload, req, accessCollections)
activeTools = { ...activeTools, ...payloadTools }
}
// 3. Connect External MCPs
if (Array.from(allowedMcpServers).includes('gitea')) {
try {
const { tools: giteaTools } = await createMcpTools({
name: 'gitea',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-gitea', '--url', 'https://git.mintel.int', '--token', process.env.GITEA_TOKEN || '']
})
activeTools = { ...activeTools, ...giteaTools }
} catch (e) {
console.error('Failed to connect to Gitea MCP', e)
}
}
// 4. Inject Memory Database Tools
// We provide the user ID so memory is partitioned per user
const memoryTools = generateMemoryTools(req.user.id)
activeTools = { ...activeTools, ...memoryTools }
// 5. Build prompt to ensure it asks before saving
const memorySystemPrompt = `
// 5. Build prompt to ensure it asks before saving
const memorySystemPrompt = `
You have access to a long-term vector memory database (Qdrant).
If the user says "speicher das", "merk dir das", "vergiss das nicht" etc., you MUST use the save_memory tool.
If the user shares important context but doesn't explicitly ask you to remember it, you should ask "Soll ich mir das für die Zukunft merken?" before saving it. Do not ask for trivial things.
`
`;
const contextContextStr = pageContext ? `
const contextContextStr = pageContext
? `
Current User Context:
URL: ${pageContext.url || 'Unknown'}
Title: ${pageContext.title || 'Unknown'}
Collection: ${pageContext.collectionSlug || 'None'}
Document ID: ${pageContext.id || 'None'}
URL: ${pageContext.url || "Unknown"}
Title: ${pageContext.title || "Unknown"}
Collection: ${pageContext.collectionSlug || "None"}
Document ID: ${pageContext.id || "None"}
You can use this to understand what the user is currently looking at.
` : ''
`
: "";
try {
const result = streamText({
// @ts-ignore - AI SDK type mismatch
model: openrouter('google/gemini-3.0-flash'),
messages,
tools: activeTools,
// @ts-ignore - AI SDK type mismatch with maxSteps
maxSteps: 10,
system: `You are a helpful Payload CMS Agent orchestrating the local Mintel ecosystem.
try {
const result = streamText({
model: openrouter("google/gemini-3.0-flash"),
messages,
tools: activeTools,
// @ts-expect-error - AI SDK type mismatch with maxSteps
maxSteps: 10,
system: `You are a helpful Payload CMS Agent orchestrating the local Mintel ecosystem.
You only have access to tools explicitly granted by the Admin.
You can completely control Payload CMS (read, create, update, delete documents).
If you need more details to fulfill a request (e.g. creating a blog post), you can ask the user.
${contextContextStr}
${memorySystemPrompt}`
})
${memorySystemPrompt}`,
});
return result.toTextStreamResponse()
} catch (error) {
console.error("AI Error:", error)
return Response.json({ error: 'Failed to process AI request' }, { status: 500 })
}
}
return result.toTextStreamResponse();
} catch (error) {
console.error("AI Error:", error);
return Response.json(
{ error: "Failed to process AI request" },
{ status: 500 },
);
}
};

View File

@@ -4,126 +4,157 @@ import * as path from "node:path";
import * as os from "node:os";
async function getOrchestrator() {
const OPENROUTER_KEY =
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
const OPENROUTER_KEY =
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
if (!OPENROUTER_KEY) {
throw new Error(
"Missing OPENROUTER_API_KEY in .env (Required for AI generation)",
);
}
const importDynamic = new Function("modulePath", "return import(modulePath)");
const { AiBlogPostOrchestrator } = await importDynamic(
"@mintel/content-engine",
if (!OPENROUTER_KEY) {
throw new Error(
"Missing OPENROUTER_API_KEY in .env (Required for AI generation)",
);
}
return new AiBlogPostOrchestrator({
apiKey: OPENROUTER_KEY,
replicateApiKey: REPLICATE_KEY,
model: "google/gemini-3-flash-preview",
});
const importDynamic = new Function("modulePath", "return import(modulePath)");
const { AiBlogPostOrchestrator } = await importDynamic(
"@mintel/content-engine",
);
return new AiBlogPostOrchestrator({
apiKey: OPENROUTER_KEY,
replicateApiKey: REPLICATE_KEY,
model: "google/gemini-3-flash-preview",
});
}
export const generateSlugEndpoint = async (req: PayloadRequest) => {
try {
let body: any = {};
try {
const { title, draftContent, oldSlug, instructions } = (await req.json?.() || {}) as any;
const orchestrator = await getOrchestrator();
const newSlug = await orchestrator.generateSlug(
draftContent,
title,
instructions,
);
if (oldSlug && oldSlug !== newSlug) {
await req.payload.create({
collection: "redirects" as any,
data: {
from: oldSlug,
to: newSlug,
},
});
}
return Response.json({ success: true, slug: newSlug });
} catch (e: any) {
return Response.json({ success: false, error: e.message }, { status: 500 });
if (req.body) body = (await req.json?.()) || {};
} catch {
/* ignore */
}
}
const { title, draftContent, oldSlug, instructions } = body;
const orchestrator = await getOrchestrator();
const newSlug = await orchestrator.generateSlug(
draftContent,
title,
instructions,
);
if (oldSlug && oldSlug !== newSlug) {
await req.payload.create({
collection: "redirects" as any,
data: {
from: oldSlug,
to: newSlug,
},
});
}
return Response.json({ success: true, slug: newSlug });
} catch (e: any) {
return Response.json({ success: false, error: e.message }, { status: 500 });
}
};
export const generateThumbnailEndpoint = async (req: PayloadRequest) => {
try {
let body: any = {};
try {
const { draftContent, title, instructions } = (await req.json?.() || {}) as any;
const OPENROUTER_KEY =
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY in .env");
if (!REPLICATE_KEY) throw new Error("Missing REPLICATE_API_KEY in .env");
const importDynamic = new Function("modulePath", "return import(modulePath)");
const { AiBlogPostOrchestrator } = await importDynamic("@mintel/content-engine");
const { ThumbnailGenerator } = await importDynamic("@mintel/thumbnail-generator");
const orchestrator = new AiBlogPostOrchestrator({
apiKey: OPENROUTER_KEY,
replicateApiKey: REPLICATE_KEY,
model: "google/gemini-3-flash-preview",
});
const tg = new ThumbnailGenerator({ replicateApiKey: REPLICATE_KEY });
const prompt = await orchestrator.generateVisualPrompt(
draftContent || title || "Technology",
instructions,
);
const tmpPath = path.join(os.tmpdir(), `mintel-thumb-${Date.now()}.png`);
await tg.generateImage(prompt, tmpPath);
const fileData = await fs.readFile(tmpPath);
const stat = await fs.stat(tmpPath);
const fileName = path.basename(tmpPath);
const newMedia = await req.payload.create({
collection: "media" as any,
data: {
alt: title ? `Thumbnail for ${title}` : "AI Generated Thumbnail",
},
file: {
data: fileData,
name: fileName,
mimetype: "image/png",
size: stat.size,
},
});
await fs.unlink(tmpPath).catch(() => { });
return Response.json({ success: true, mediaId: newMedia.id });
} catch (e: any) {
return Response.json({ success: false, error: e.message }, { status: 500 });
if (req.body) body = (await req.json?.()) || {};
} catch {
/* ignore */
}
}
const { draftContent, title, instructions } = body;
const OPENROUTER_KEY =
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY in .env");
if (!REPLICATE_KEY) throw new Error("Missing REPLICATE_API_KEY in .env");
const importDynamic = new Function(
"modulePath",
"return import(modulePath)",
);
const { AiBlogPostOrchestrator } = await importDynamic(
"@mintel/content-engine",
);
const { ThumbnailGenerator } = await importDynamic(
"@mintel/thumbnail-generator",
);
const orchestrator = new AiBlogPostOrchestrator({
apiKey: OPENROUTER_KEY,
replicateApiKey: REPLICATE_KEY,
model: "google/gemini-3-flash-preview",
});
const tg = new ThumbnailGenerator({ replicateApiKey: REPLICATE_KEY });
const prompt = await orchestrator.generateVisualPrompt(
draftContent || title || "Technology",
instructions,
);
const tmpPath = path.join(os.tmpdir(), `mintel-thumb-${Date.now()}.png`);
await tg.generateImage(prompt, tmpPath);
const fileData = await fs.readFile(tmpPath);
const stat = await fs.stat(tmpPath);
const fileName = path.basename(tmpPath);
const newMedia = await req.payload.create({
collection: "media" as any,
data: {
alt: title ? `Thumbnail for ${title}` : "AI Generated Thumbnail",
},
file: {
data: fileData,
name: fileName,
mimetype: "image/png",
size: stat.size,
},
});
await fs.unlink(tmpPath).catch(() => {});
return Response.json({ success: true, mediaId: newMedia.id });
} catch (e: any) {
return Response.json({ success: false, error: e.message }, { status: 500 });
}
};
export const generateSingleFieldEndpoint = async (req: PayloadRequest) => {
try {
let body: any = {};
try {
const { documentTitle, documentContent, fieldName, fieldDescription, instructions } = (await req.json?.() || {}) as any;
if (req.body) body = (await req.json?.()) || {};
} catch {
/* ignore */
}
const {
documentTitle,
documentContent,
fieldName,
fieldDescription,
instructions,
} = body;
const OPENROUTER_KEY =
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY");
const OPENROUTER_KEY =
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY");
const contextDocsData = await req.payload.find({
collection: "context-files" as any,
limit: 100,
});
const projectContext = contextDocsData.docs
.map((doc: any) => `--- ${doc.filename} ---\n${doc.content}`)
.join("\n\n");
const contextDocsData = await req.payload.find({
collection: "context-files" as any,
limit: 100,
});
const projectContext = contextDocsData.docs
.map((doc: any) => `--- ${doc.filename} ---\n${doc.content}`)
.join("\n\n");
const prompt = `You are an expert AI assistant perfectly trained for generating exact data values for CMS components.
const prompt = `You are an expert AI assistant perfectly trained for generating exact data values for CMS components.
PROJECT STRATEGY & CONTEXT:
${projectContext}
@@ -138,21 +169,21 @@ CRITICAL RULES:
3. If the field implies a diagram or flow, output RAW Mermaid.js code.
4. If it's standard text, write professional B2B German. No quotes, no conversational filler.`;
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${OPENROUTER_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "google/gemini-3-flash-preview",
messages: [{ role: "user", content: prompt }],
}),
});
const data = await res.json();
const text = data.choices?.[0]?.message?.content?.trim() || "";
return Response.json({ success: true, text });
} catch (e: any) {
return Response.json({ success: false, error: e.message }, { status: 500 });
}
}
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${OPENROUTER_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "google/gemini-3-flash-preview",
messages: [{ role: "user", content: prompt }],
}),
});
const data = await res.json();
const text = data.choices?.[0]?.message?.content?.trim() || "";
return Response.json({ success: true, text });
} catch (e: any) {
return Response.json({ success: false, error: e.message }, { status: 500 });
}
};

View File

@@ -1,75 +1,108 @@
import { PayloadRequest } from 'payload'
import { PayloadRequest } from "payload";
import { parseMarkdownToLexical } from "../utils/lexicalParser.js";
export const optimizePostEndpoint = async (req: PayloadRequest) => {
try {
let body: any = {};
try {
const { draftContent, instructions } = (await req.json?.() || {}) as { draftContent: string; instructions?: string };
if (!draftContent) {
return Response.json({ error: 'Missing draftContent' }, { status: 400 })
}
const globalAiSettings = (await req.payload.findGlobal({ slug: "ai-settings" })) as any;
const customSources =
globalAiSettings?.customSources?.map((s: any) => s.sourceName) || [];
const OPENROUTER_KEY =
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
if (!OPENROUTER_KEY) {
return Response.json({ error: "OPENROUTER_KEY not found in environment." }, { status: 500 })
}
// Dynamically import to avoid bundling it into client components that might accidentally import this file
const importDynamic = new Function("modulePath", "return import(modulePath)");
const { AiBlogPostOrchestrator } = await importDynamic("@mintel/content-engine");
const orchestrator = new AiBlogPostOrchestrator({
apiKey: OPENROUTER_KEY,
replicateApiKey: REPLICATE_KEY,
model: "google/gemini-3-flash-preview",
});
const contextDocsData = await req.payload.find({
collection: "context-files" as any,
limit: 100,
});
const projectContext = contextDocsData.docs.map((doc: any) => doc.content);
const optimizedMarkdown = await orchestrator.optimizeDocument({
content: draftContent,
projectContext,
availableComponents: [],
instructions,
internalLinks: [],
customSources,
});
if (!optimizedMarkdown || typeof optimizedMarkdown !== "string") {
return Response.json({ error: "AI returned invalid markup." }, { status: 500 })
}
const blocks = parseMarkdownToLexical(optimizedMarkdown);
return Response.json({
success: true,
lexicalAST: {
root: {
type: "root",
format: "",
indent: 0,
version: 1,
children: blocks,
direction: "ltr",
},
},
})
} catch (error: any) {
console.error("Failed to optimize post in endpoint:", error);
return Response.json({
success: false,
error: error.message || "An unknown error occurred during optimization.",
}, { status: 500 })
if (req.body) {
// req.json() acts as a method in Next.js/Payload req wrapper
body = (await req.json?.()) || {};
}
} catch (e) {
// Ignore JSON parse error, body remains empty
}
}
const { draftContent, instructions } = body as {
draftContent?: string;
instructions?: string;
};
if (!draftContent) {
return Response.json(
{ success: false, error: "Missing draftContent" },
{ status: 400 },
);
}
const globalAiSettings = (await req.payload.findGlobal({
slug: "ai-settings",
})) as any;
const customSources =
globalAiSettings?.customSources?.map((s: any) => s.sourceName) || [];
const OPENROUTER_KEY =
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
if (!OPENROUTER_KEY) {
return Response.json(
{ error: "OPENROUTER_KEY not found in environment." },
{ status: 500 },
);
}
// Dynamically import to avoid bundling it into client components that might accidentally import this file
const importDynamic = new Function(
"modulePath",
"return import(modulePath)",
);
const { AiBlogPostOrchestrator } = await importDynamic(
"@mintel/content-engine",
);
const orchestrator = new AiBlogPostOrchestrator({
apiKey: OPENROUTER_KEY,
replicateApiKey: REPLICATE_KEY,
model: "google/gemini-3-flash-preview",
});
const contextDocsData = await req.payload.find({
collection: "context-files" as any,
limit: 100,
});
const projectContext = contextDocsData.docs.map((doc: any) => doc.content);
const optimizedMarkdown = await orchestrator.optimizeDocument({
content: draftContent,
projectContext,
availableComponents: [],
instructions,
internalLinks: [],
customSources,
});
if (!optimizedMarkdown || typeof optimizedMarkdown !== "string") {
return Response.json(
{ error: "AI returned invalid markup." },
{ status: 500 },
);
}
const blocks = parseMarkdownToLexical(optimizedMarkdown);
return Response.json({
success: true,
lexicalAST: {
root: {
type: "root",
format: "",
indent: 0,
version: 1,
children: blocks,
direction: "ltr",
},
},
});
} catch (error: any) {
console.error("Failed to optimize post in endpoint:", error);
return Response.json(
{
success: false,
error:
error.message || "An unknown error occurred during optimization.",
},
{ status: 500 },
);
}
};

View File

@@ -1,65 +1,68 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { tool } from 'ai'
import { z } from 'zod'
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { tool, jsonSchema } from "ai";
/**
* Connects to an external MCP Server and maps its tools to Vercel AI SDK Tools.
*/
export async function createMcpTools(mcpConfig: { name: string, url?: string, command?: string, args?: string[] }) {
let transport
export async function createMcpTools(mcpConfig: {
name: string;
url?: string;
command?: string;
args?: string[];
}) {
let transport;
// Support both HTTP/SSE and STDIO transports
if (mcpConfig.url) {
transport = new SSEClientTransport(new URL(mcpConfig.url))
} else if (mcpConfig.command) {
transport = new StdioClientTransport({
command: mcpConfig.command,
args: mcpConfig.args || [],
})
} else {
throw new Error('Invalid MCP config: Must provide either URL or Command.')
}
// Support both HTTP/SSE and STDIO transports
if (mcpConfig.url) {
transport = new SSEClientTransport(new URL(mcpConfig.url));
} else if (mcpConfig.command) {
transport = new StdioClientTransport({
command: mcpConfig.command,
args: mcpConfig.args || [],
});
} else {
throw new Error("Invalid MCP config: Must provide either URL or Command.");
}
const client = new Client(
{ name: `payload-ai-client-${mcpConfig.name}`, version: '1.0.0' },
{ capabilities: {} }
)
const client = new Client(
{ name: `payload-ai-client-${mcpConfig.name}`, version: "1.0.0" },
{ capabilities: {} },
);
await client.connect(transport)
await client.connect(transport);
// Fetch available tools from the external MCP server
const toolListResult = await client.listTools()
const externalTools = toolListResult.tools || []
// Fetch available tools from the external MCP server
const toolListResult = await client.listTools();
const externalTools = toolListResult.tools || [];
const aiSdkTools: Record<string, any> = {}
const aiSdkTools: Record<string, any> = {};
// Map each external tool to a Vercel AI SDK Tool
for (const extTool of externalTools) {
// Basic conversion of JSON Schema to Zod for the AI SDK
// Note: For a production ready adapter, you might need a more robust jsonSchemaToZod converter
// or use AI SDK's new experimental generateSchema feature if available.
// Here we use a generic `z.any()` as a fallback since AI SDK requires a Zod schema.
const toolSchema = extTool.inputSchema as Record<string, any>
// Map each external tool to a Vercel AI SDK Tool
for (const extTool of externalTools) {
// Basic conversion of JSON Schema to Zod for the AI SDK
// Note: For a production ready adapter, you might need a more robust jsonSchemaToZod converter
// or use AI SDK's new experimental generateSchema feature if available.
// Here we use a generic `z.any()` as a fallback since AI SDK requires a Zod schema.
const toolSchema = extTool.inputSchema as Record<string, any>;
// We create a simplified parameter parser.
// An ideal approach uses `jsonSchemaToZod` library or native AI SDK JSON schema support
// (introduced recently in `ai` package).
// We create a simplified parameter parser.
// An ideal approach uses `jsonSchemaToZod` library or native AI SDK JSON schema support
// (introduced recently in `ai` package).
aiSdkTools[`${mcpConfig.name}_${extTool.name}`] = tool({
description: `[From ${mcpConfig.name}] ${extTool.description || extTool.name}`,
parameters: z.any().describe('JSON matching the original MCP input_schema'), // Simplify for prototype
// @ts-ignore - AI strict mode overload bug with implicit zod inferences
execute: async (args: any) => {
const result = await client.callTool({
name: extTool.name,
arguments: args
})
return result
}
})
}
aiSdkTools[`${mcpConfig.name}_${extTool.name}`] = tool({
description: `[From ${mcpConfig.name}] ${extTool.description || extTool.name}`,
parameters: jsonSchema(toolSchema as any),
// @ts-expect-error - AI strict mode overload bug with implicit zod inferences
execute: async (args: any) => {
const result = await client.callTool({
name: extTool.name,
arguments: args,
});
return result;
},
});
}
return { tools: aiSdkTools, client }
return { tools: aiSdkTools, client };
}

View File

@@ -1,37 +1,39 @@
import { tool } from 'ai'
import { z } from 'zod'
import { QdrantClient } from '@qdrant/js-client-rest'
import { tool } from "ai";
import { z } from "zod";
import { QdrantClient } from "@qdrant/js-client-rest";
// Qdrant initialization
// This requires the user to have Qdrant running and QDRANT_URL/QDRANT_API_KEY environment variables set
const qdrantClient = new QdrantClient({
url: process.env.QDRANT_URL || 'http://localhost:6333',
apiKey: process.env.QDRANT_API_KEY,
})
url: process.env.QDRANT_URL || "http://localhost:6333",
apiKey: process.env.QDRANT_API_KEY,
});
const MEMORY_COLLECTION = 'mintel_ai_memory'
const MEMORY_COLLECTION = "mintel_ai_memory";
// Ensure collection exists on load
async function initQdrant() {
try {
const res = await qdrantClient.getCollections()
const exists = res.collections.find((c: any) => c.name === MEMORY_COLLECTION)
if (!exists) {
await qdrantClient.createCollection(MEMORY_COLLECTION, {
vectors: {
size: 1536, // typical embedding size, adjust based on the embedding model used
distance: 'Cosine',
},
})
console.log(`Qdrant collection '${MEMORY_COLLECTION}' created.`)
}
} catch (error) {
console.error('Failed to initialize Qdrant memory collection:', error)
try {
const res = await qdrantClient.getCollections();
const exists = res.collections.find(
(c: any) => c.name === MEMORY_COLLECTION,
);
if (!exists) {
await qdrantClient.createCollection(MEMORY_COLLECTION, {
vectors: {
size: 1536, // typical embedding size, adjust based on the embedding model used
distance: "Cosine",
},
});
console.log(`Qdrant collection '${MEMORY_COLLECTION}' created.`);
}
} catch (error) {
console.error("Failed to initialize Qdrant memory collection:", error);
}
}
// Call init, but don't block
initQdrant()
initQdrant();
/**
* Returns memory tools for the AI SDK.
@@ -40,76 +42,99 @@ initQdrant()
* by a utility function, or we use Qdrant's FastEmbed (if running their specialized container).
*/
export const generateMemoryTools = (userId: string | number) => {
return {
save_memory: tool({
description: 'Save an important preference, fact, or instruction about the user to long-term memory. Only use this when explicitly asked or when it is clearly a long-term preference.',
parameters: z.object({
fact: z.string().describe('The fact or instruction to remember.'),
category: z.string().optional().describe('An optional category like "preference", "rule", or "project_detail".'),
}),
// @ts-ignore - AI SDK strict mode bug
execute: async ({ fact, category }: { fact: string; category?: string }) => {
// In a real scenario, you MUST generate embeddings for the 'fact' string here
// using OpenAI or another embedding provider before inserting into Qdrant.
// const embedding = await generateEmbedding(fact)
return {
save_memory: tool({
description:
"Save an important preference, fact, or instruction about the user to long-term memory. Only use this when explicitly asked or when it is clearly a long-term preference.",
parameters: z.object({
fact: z.string().describe("The fact or instruction to remember."),
category: z
.string()
.optional()
.describe(
'An optional category like "preference", "rule", or "project_detail".',
),
}),
// @ts-expect-error - AI SDK strict mode bug
execute: async ({
fact,
category,
}: {
fact: string;
category?: string;
}) => {
// In a real scenario, you MUST generate embeddings for the 'fact' string here
// using OpenAI or another embedding provider before inserting into Qdrant.
// const embedding = await generateEmbedding(fact)
try {
// Mock embedding payload for demonstration
const mockEmbedding = new Array(1536).fill(0).map(() => Math.random())
try {
// Mock embedding payload for demonstration
const mockEmbedding = new Array(1536)
.fill(0)
.map(() => Math.random());
await qdrantClient.upsert(MEMORY_COLLECTION, {
wait: true,
points: [
{
id: crypto.randomUUID(),
vector: mockEmbedding,
payload: {
userId: String(userId), // Partition memory by user
fact,
category,
createdAt: new Date().toISOString(),
},
},
],
})
return { success: true, message: `Successfully remembered: "${fact}"` }
} catch (error) {
console.error("Qdrant save error:", error)
return { success: false, error: 'Failed to save to memory database.' }
}
await qdrantClient.upsert(MEMORY_COLLECTION, {
wait: true,
points: [
{
id: crypto.randomUUID(),
vector: mockEmbedding,
payload: {
userId: String(userId), // Partition memory by user
fact,
category,
createdAt: new Date().toISOString(),
},
},
],
});
return {
success: true,
message: `Successfully remembered: "${fact}"`,
};
} catch (error) {
console.error("Qdrant save error:", error);
return {
success: false,
error: "Failed to save to memory database.",
};
}
},
}),
search_memory: tool({
description:
"Search the user's long-term memory for past factual context, preferences, or rules.",
parameters: z.object({
query: z.string().describe("The search string to find in memory."),
}),
// @ts-expect-error - AI SDK strict mode bug
execute: async ({ query }: { query: string }) => {
// Generate embedding for query
const mockQueryEmbedding = new Array(1536)
.fill(0)
.map(() => Math.random());
try {
const results = await qdrantClient.search(MEMORY_COLLECTION, {
vector: mockQueryEmbedding,
limit: 5,
filter: {
must: [
{
key: "userId",
match: { value: String(userId) },
},
],
},
}),
});
search_memory: tool({
description: 'Search the user\'s long-term memory for past factual context, preferences, or rules.',
parameters: z.object({
query: z.string().describe('The search string to find in memory.'),
}),
// @ts-ignore - AI SDK strict mode bug
execute: async ({ query }: { query: string }) => {
// Generate embedding for query
const mockQueryEmbedding = new Array(1536).fill(0).map(() => Math.random())
try {
const results = await qdrantClient.search(MEMORY_COLLECTION, {
vector: mockQueryEmbedding,
limit: 5,
filter: {
must: [
{
key: 'userId',
match: { value: String(userId) }
}
]
}
})
return results.map((r: any) => r.payload?.fact || '')
} catch (error) {
console.error("Qdrant search error:", error)
return []
}
}
})
}
}
return results.map((r: any) => r.payload?.fact || "");
} catch (error) {
console.error("Qdrant search error:", error);
return [];
}
},
}),
};
};

View File

@@ -1,107 +1,137 @@
import { tool } from 'ai'
import { z } from 'zod'
import type { Payload, PayloadRequest, User } from 'payload'
import { tool } from "ai";
import { z } from "zod";
import type { Payload, PayloadRequest, User } from "payload";
export const generatePayloadLocalTools = (
payload: Payload,
req: PayloadRequest,
allowedCollections: string[]
payload: Payload,
req: PayloadRequest,
allowedCollections: string[],
) => {
const tools: Record<string, any> = {}
const tools: Record<string, any> = {};
for (const collectionSlug of allowedCollections) {
const slugKey = collectionSlug.replace(/-/g, '_')
for (const collectionSlug of allowedCollections) {
const slugKey = collectionSlug.replace(/-/g, "_");
// 1. Read (Find) Tool
tools[`read_${slugKey}`] = tool({
description: `Read/Find documents from the Payload CMS collection: ${collectionSlug}`,
parameters: z.object({
limit: z.number().optional().describe('Number of documents to return, max 100.'),
page: z.number().optional().describe('Page number for pagination.'),
// Simple string-based query for demo purposes. For a robust implementation,
// we'd map this to Payload's where query logic using a structured Zod schema.
query: z.string().optional().describe('Optional text to search within the collection.'),
}),
// @ts-ignore - AI SDK strict mode type inference bug
execute: async ({ limit = 10, page = 1, query }: { limit?: number; page?: number; query?: string }) => {
const where = query ? { id: { equals: query } } : undefined // Placeholder logic
// 1. Read (Find) Tool
tools[`read_${slugKey}`] = tool({
description: `Read/Find documents from the Payload CMS collection: ${collectionSlug}`,
parameters: z.object({
limit: z
.number()
.optional()
.describe("Number of documents to return, max 100."),
page: z.number().optional().describe("Page number for pagination."),
// Simple string-based query for demo purposes. For a robust implementation,
// we'd map this to Payload's where query logic using a structured Zod schema.
query: z
.string()
.optional()
.describe("Optional text to search within the collection."),
}),
// @ts-expect-error - AI SDK strict mode type inference bug
execute: async ({
limit = 10,
page = 1,
query,
}: {
limit?: number;
page?: number;
query?: string;
}) => {
const where = query ? { id: { equals: query } } : undefined; // Placeholder logic
return await payload.find({
collection: collectionSlug as any,
limit: Math.min(limit, 100),
page,
where,
req, // Crucial for passing the user context and respecting access control!
})
},
})
return await payload.find({
collection: collectionSlug as any,
limit: Math.min(limit, 100),
page,
where,
req, // Crucial for passing the user context and respecting access control!
});
},
});
// 2. Read by ID Tool
tools[`read_${slugKey}_by_id`] = tool({
description: `Get a specific document by its ID from the ${collectionSlug} collection.`,
parameters: z.object({
id: z.union([z.string(), z.number()]).describe('The ID of the document.'),
}),
// @ts-ignore - AI SDK strict mode type inference bug
execute: async ({ id }: { id: string | number }) => {
return await payload.findByID({
collection: collectionSlug as any,
id,
req, // Enforce access control
})
},
})
// 2. Read by ID Tool
tools[`read_${slugKey}_by_id`] = tool({
description: `Get a specific document by its ID from the ${collectionSlug} collection.`,
parameters: z.object({
id: z
.union([z.string(), z.number()])
.describe("The ID of the document."),
}),
// @ts-expect-error - AI SDK strict mode type inference bug
execute: async ({ id }: { id: string | number }) => {
return await payload.findByID({
collection: collectionSlug as any,
id,
req, // Enforce access control
});
},
});
// 3. Create Tool
tools[`create_${slugKey}`] = tool({
description: `Create a new document in the ${collectionSlug} collection.`,
parameters: z.object({
data: z.record(z.any()).describe('A JSON object containing the data to insert.'),
}),
// @ts-ignore - AI SDK strict mode type inference bug
execute: async ({ data }: { data: Record<string, any> }) => {
return await payload.create({
collection: collectionSlug as any,
data,
req, // Enforce access control
})
},
})
// 3. Create Tool
tools[`create_${slugKey}`] = tool({
description: `Create a new document in the ${collectionSlug} collection.`,
parameters: z.object({
data: z
.record(z.any())
.describe("A JSON object containing the data to insert."),
}),
// @ts-expect-error - AI SDK strict mode type inference bug
execute: async ({ data }: { data: Record<string, any> }) => {
return await payload.create({
collection: collectionSlug as any,
data,
req, // Enforce access control
});
},
});
// 4. Update Tool
tools[`update_${slugKey}`] = tool({
description: `Update an existing document in the ${collectionSlug} collection.`,
parameters: z.object({
id: z.union([z.string(), z.number()]).describe('The ID of the document to update.'),
data: z.record(z.any()).describe('A JSON object containing the fields to update.'),
}),
// @ts-ignore - AI SDK strict mode type inference bug
execute: async ({ id, data }: { id: string | number; data: Record<string, any> }) => {
return await payload.update({
collection: collectionSlug as any,
id,
data,
req, // Enforce access control
})
},
})
// 4. Update Tool
tools[`update_${slugKey}`] = tool({
description: `Update an existing document in the ${collectionSlug} collection.`,
parameters: z.object({
id: z
.union([z.string(), z.number()])
.describe("The ID of the document to update."),
data: z
.record(z.any())
.describe("A JSON object containing the fields to update."),
}),
// @ts-expect-error - AI SDK strict mode type inference bug
execute: async ({
id,
data,
}: {
id: string | number;
data: Record<string, any>;
}) => {
return await payload.update({
collection: collectionSlug as any,
id,
data,
req, // Enforce access control
});
},
});
// 5. Delete Tool
tools[`delete_${slugKey}`] = tool({
description: `Delete a document from the ${collectionSlug} collection by ID.`,
parameters: z.object({
id: z.union([z.string(), z.number()]).describe('The ID of the document to delete.'),
}),
// @ts-ignore - AI SDK strict mode type inference bug
execute: async ({ id }: { id: string | number }) => {
return await payload.delete({
collection: collectionSlug as any,
id,
req, // Enforce access control
})
},
})
}
// 5. Delete Tool
tools[`delete_${slugKey}`] = tool({
description: `Delete a document from the ${collectionSlug} collection by ID.`,
parameters: z.object({
id: z
.union([z.string(), z.number()])
.describe("The ID of the document to delete."),
}),
// @ts-expect-error - AI SDK strict mode type inference bug
execute: async ({ id }: { id: string | number }) => {
return await payload.delete({
collection: collectionSlug as any,
id,
req, // Enforce access control
});
},
});
}
return tools
}
return tools;
};

View File

@@ -0,0 +1,15 @@
FROM node:20-bookworm-slim AS builder
WORKDIR /app
COPY package.json ./
RUN npm install -g pnpm@10.18.3 && pnpm install --ignore-workspace
COPY tsconfig.json ./
COPY src ./src
RUN pnpm build
FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
ENTRYPOINT ["node", "dist/start.js"]

View File

@@ -13,13 +13,12 @@
"@modelcontextprotocol/sdk": "^1.5.0",
"axios": "^1.7.2",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"zod": "^3.23.8"
"express": "^4.19.2"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/express": "^4.17.21",
"@types/node": "^20.14.10",
"typescript": "^5.5.3",
"tsx": "^4.19.2"
"tsx": "^4.19.2",
"typescript": "^5.5.3"
}
}

View File

@@ -1,6 +1,7 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from 'express';
import crypto from 'crypto';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
@@ -18,10 +19,28 @@ if (!SERPBEAR_API_KEY) {
const serpbearClient = axios.create({
baseURL: `${SERPBEAR_BASE_URL}/api`,
headers: { apiKey: SERPBEAR_API_KEY },
headers: { 'x-api-key': SERPBEAR_API_KEY || '' },
httpsAgent: new https.Agent({
rejectUnauthorized: false,
}),
timeout: 10000
});
// Logging middleware for SerpBear client
serpbearClient.interceptors.request.use((config) => {
console.error(`SerpBear Request: ${config.method?.toUpperCase()} ${config.url}`);
return config;
}, (error) => {
console.error(`SerpBear Request Error: ${error.message}`);
return Promise.reject(error);
});
serpbearClient.interceptors.response.use((response) => {
return response;
}, (error) => {
const msg = error.response?.data?.error || error.response?.data?.message || error.message;
console.error(`SerpBear API Error (${error.config?.url}): ${msg}`);
return Promise.reject(error);
});
@@ -125,21 +144,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "serpbear_list_domains") {
try {
try {
if (request.params.name === "serpbear_list_domains") {
const res = await serpbearClient.get('/domains');
const domains = (res.data.domains || []).map((d: any) => ({
id: d.id, domain: d.domain, keywords: d.keywordCount
}));
return { content: [{ type: "text", text: JSON.stringify(domains, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
}
}
if (request.params.name === "serpbear_get_keywords") {
const { domain_id } = request.params.arguments as any;
try {
if (request.params.name === "serpbear_get_keywords") {
const { domain_id } = request.params.arguments as any;
const res = await serpbearClient.get('/keywords', { params: { domain: domain_id } });
const keywords = (res.data.keywords || []).map((k: any) => ({
id: k.id,
@@ -151,55 +166,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
change: k.position_change ?? null,
}));
return { content: [{ type: "text", text: JSON.stringify(keywords, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
}
}
if (request.params.name === "serpbear_add_keywords") {
const { domain_id, keywords, country = 'de', device = 'desktop' } = request.params.arguments as any;
try {
if (request.params.name === "serpbear_add_keywords") {
const { domain_id, keywords, country = 'de', device = 'desktop' } = request.params.arguments as any;
const res = await serpbearClient.post('/keywords', {
domain: domain_id,
keywords: keywords.map((kw: string) => ({ keyword: kw, country, device })),
});
return { content: [{ type: "text", text: `Added ${keywords.length} keywords. Result: ${JSON.stringify(res.data)}` }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
}
}
if (request.params.name === "serpbear_delete_keywords") {
const { keyword_ids } = request.params.arguments as any;
try {
if (request.params.name === "serpbear_delete_keywords") {
const { keyword_ids } = request.params.arguments as any;
await serpbearClient.delete('/keywords', { data: { ids: keyword_ids } });
return { content: [{ type: "text", text: `Deleted ${keyword_ids.length} keywords.` }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
}
}
if (request.params.name === "serpbear_refresh_keywords") {
const { keyword_ids } = request.params.arguments as any;
try {
if (request.params.name === "serpbear_refresh_keywords") {
const { keyword_ids } = request.params.arguments as any;
await serpbearClient.post('/keywords/refresh', { ids: keyword_ids });
return { content: [{ type: "text", text: `Triggered refresh for ${keyword_ids.length} keywords.` }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
}
}
if (request.params.name === "serpbear_get_keyword_history") {
const { keyword_id } = request.params.arguments as any;
try {
if (request.params.name === "serpbear_get_keyword_history") {
const { keyword_id } = request.params.arguments as any;
const res = await serpbearClient.get(`/keywords/${keyword_id}/history`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (e: any) {
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
}
}
throw new Error(`Unknown tool: ${request.params.name}`);
throw new Error(`Unknown tool: ${request.params.name}`);
} catch (e: any) {
const msg = e.response?.data?.error || e.response?.data?.message || e.message;
return { isError: true, content: [{ type: "text", text: `SerpBear Service Error: ${msg}` }] };
}
});
// --- Express / SSE Server ---
@@ -213,25 +213,65 @@ async function run() {
console.error('SerpBear MCP server is running on stdio');
} else {
const app = express();
let transport: SSEServerTransport | null = null;
const transports = new Map<string, SSEServerTransport>();
app.get('/sse', async (req, res) => {
console.error('New SSE connection established');
transport = new SSEServerTransport('/message', res);
await server.connect(transport);
// Manual CORS middleware
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-mcp-protocol-version');
if (req.method === 'OPTIONS') {
res.status(204).end();
return;
}
next();
});
app.post('/message', async (req, res) => {
app.use((req, _res, next) => {
console.error(`${req.method} ${req.url}`);
next();
});
app.get('/sse', async (req, res) => {
const transport = new SSEServerTransport('/message', res as any);
const sessionId = transport.sessionId;
console.error(`New SSE connection: ${sessionId}`);
transports.set(sessionId, transport);
const heartbeatInterval = setInterval(() => {
res.write(": heartbeat\n\n");
}, 15000);
req.on('close', () => {
console.error(`SSE connection closed: ${sessionId}`);
clearInterval(heartbeatInterval);
transports.delete(sessionId);
});
await server.connect(transport);
await new Promise((resolve) => {
req.on('close', resolve);
});
});
app.post('/message', express.json(), async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
console.error(`POST /message/${sessionId} - Active transports: ${transports.size}`);
if (!transport) {
res.status(400).send('No active SSE connection');
console.error(`No transport found for session: ${sessionId}`);
res.status(400).send('No active SSE connection for this session');
return;
}
await transport.handlePostMessage(req, res);
});
const PORT = process.env.SERPBEAR_MCP_PORT || 3004;
app.listen(PORT, () => {
console.error(`SerpBear MCP server running on http://localhost:${PORT}/sse`);
const PORT = Number(process.env.SERPBEAR_MCP_PORT) || 3004;
app.listen(PORT, '0.0.0.0', () => {
console.error(`SerpBear MCP server running on http://0.0.0.0:${PORT}/sse`);
});
}
}

View File

@@ -4,8 +4,8 @@ import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
config({ path: resolve(__dirname, '../../../.env.local') });
config({ path: resolve(__dirname, '../../../.env') });
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
import('./index.js').catch(err => {
console.error('Failed to start SerpBear MCP Server:', err);

26
packages/ui/package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "@mintel/ui",
"version": "1.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"ladle": "ladle serve",
"build": "ladle build"
},
"dependencies": {
"@react-pdf/renderer": "^3.4.0",
"framer-motion": "^11.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@ladle/react": "^4.0.0",
"tailwindcss": "^3.4.0",
"postcss": "^8.4.0",
"autoprefixer": "^10.4.0",
"typescript": "^5.0.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0"
}
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { View } from '@react-pdf/renderer';
import { motion, HTMLMotionProps } from 'framer-motion';
import { useEnvironment } from '../context/EnvironmentContext';
export interface BoxProps extends HTMLMotionProps<"div"> {
children?: React.ReactNode;
pdfStyle?: any; // Specifically for PDF if Tailwind mapping is not enough
}
export const Box: React.FC<BoxProps> = ({ children, className, style, pdfStyle, ...props }) => {
const env = useEnvironment();
if (env === 'print') {
return <View style={pdfStyle || style}>{children}</View>;
}
// Web and Presentation get Framer Motion by default
return (
<motion.div className={className} style={style} {...props}>
{children}
</motion.div>
);
};

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { Text as PdfText } from '@react-pdf/renderer';
import { motion, HTMLMotionProps } from 'framer-motion';
import { useEnvironment } from '../context/EnvironmentContext';
export interface TextProps extends HTMLMotionProps<"span"> {
children?: React.ReactNode;
pdfStyle?: any;
}
export const Text: React.FC<TextProps> = ({ children, className, style, pdfStyle, ...props }) => {
const env = useEnvironment();
if (env === 'print') {
return <PdfText style={pdfStyle || style}>{children}</PdfText>;
}
return (
<motion.span className={className} style={style} {...props}>
{children}
</motion.span>
);
};

View File

@@ -0,0 +1,17 @@
import React, { createContext, useContext } from 'react';
export type Environment = 'web' | 'print' | 'presentation';
const EnvironmentContext = createContext<Environment>('web');
export const EnvironmentProvider: React.FC<{ env: Environment; children: React.ReactNode }> = ({ env, children }) => {
return (
<EnvironmentContext.Provider value={env}>
{children}
</EnvironmentContext.Provider>
);
};
export const useEnvironment = () => {
return useContext(EnvironmentContext);
};

3
packages/ui/src/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { Box } from './components/universal/Box';
export { Text } from './components/universal/Text';
export { EnvironmentProvider, useEnvironment } from './context/EnvironmentContext';

View File

@@ -0,0 +1,17 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
colors: {
mintel: {
primary: '#1E40AF',
accent: '#82ed20',
}
}
},
},
plugins: [],
}

View File

@@ -0,0 +1,15 @@
FROM node:20-bookworm-slim AS builder
WORKDIR /app
COPY package.json ./
RUN npm install -g pnpm@10.18.3 && pnpm install --ignore-workspace
COPY tsconfig.json ./
COPY src ./src
RUN pnpm build
FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
ENTRYPOINT ["node", "dist/start.js"]

View File

@@ -13,13 +13,12 @@
"@modelcontextprotocol/sdk": "^1.5.0",
"axios": "^1.7.2",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"zod": "^3.23.8"
"express": "^4.19.2"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/express": "^4.17.21",
"@types/node": "^20.14.10",
"typescript": "^5.5.3",
"tsx": "^4.19.2"
"tsx": "^4.19.2",
"typescript": "^5.5.3"
}
}

Some files were not shown because too many files have changed in this diff Show More