Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a9dd06fc6 | |||
| 985e532c61 | |||
| 04462f21e1 | |||
| 11babf3347 | |||
| 3de92accc4 | |||
| c8961411ec | |||
| 42ea30c11f | |||
| 7b2cc3723a | |||
| 0fc4ddbc28 | |||
| 9f8eab2ad2 | |||
| 69c483ac46 | |||
| ac178ccbb0 | |||
| 3f16828bdf | |||
| 0f7cd94e14 | |||
| 2cc6512afa | |||
| 151c98c884 | |||
| ba17d7940e | |||
| 46120dbd56 | |||
| 571c4797fa | |||
| c7d107a578 | |||
| 5f50e98a0a | |||
| 45f831ca1c | |||
| 73356cca30 | |||
| b7e630c1d8 | |||
| 13c8bc29ae | |||
| 51a892fd76 | |||
| 9cc8e573ec |
5
.env
5
.env
@@ -59,3 +59,8 @@ GLITCHTIP_API_KEY=3d37db66fa1fc3a1cdcd1357728b056225aca950f0163b50c5e922344807ed
|
||||
UMAMI_BASE_URL=https://analytics.infra.mintel.me
|
||||
GLITCHTIP_BASE_URL=https://errors.infra.mintel.me
|
||||
SERPBEAR_BASE_URL=https://seo.infra.mintel.me
|
||||
|
||||
# Vikunja MCP
|
||||
VIKUNJA_API_TOKEN=tk_468a6f7a59b329aa42796378e55075b79eb4d3d4
|
||||
VIKUNJA_BASE_URL=https://tasks.infra.mintel.me
|
||||
|
||||
|
||||
44
.gitea/workflows/analytics-mailer.yml
Normal file
44
.gitea/workflows/analytics-mailer.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Monthly Analytics Mailer
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 8 1 * *' # Run at 08:00 on the 1st of every month
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
send-reports:
|
||||
name: 📊 Send Monthly Reports
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
UMAMI_BASE_URL: ${{ secrets.UMAMI_BASE_URL }}
|
||||
UMAMI_USERNAME: ${{ secrets.UMAMI_USERNAME }}
|
||||
UMAMI_PASSWORD: ${{ secrets.UMAMI_PASSWORD }}
|
||||
MAILGUN_API_KEY: ${{ secrets.MAILGUN_API_KEY }}
|
||||
MAILGUN_DOMAIN: ${{ secrets.MAILGUN_DOMAIN }}
|
||||
MAILGUN_SENDER: ${{ secrets.MAILGUN_SENDER }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node_version: 20
|
||||
|
||||
- name: Enable pnpm
|
||||
run: corepack enable && corepack prepare pnpm@10.2.0 --activate
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts --no-color
|
||||
|
||||
- name: Build mail templates
|
||||
run: pnpm --filter @mintel/mail build
|
||||
|
||||
- name: Build analytics mailer
|
||||
run: pnpm --filter @mintel/analytics-mailer build
|
||||
|
||||
- name: Run Analytics Mailer
|
||||
run: pnpm --filter @mintel/analytics-mailer run start
|
||||
@@ -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
|
||||
|
||||
14
apps/analytics-mailer/config.json
Normal file
14
apps/analytics-mailer/config.json
Normal file
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"websiteId": "d773ea10-a3b3-4ccf-9024-987e14c4d669",
|
||||
"domain": "e-tib.com",
|
||||
"clientEmail": "marc@mintel.me",
|
||||
"clientName": "E-TIB GmbH"
|
||||
},
|
||||
{
|
||||
"websiteId": "59a7db94-0100-4c7e-98ef-99f45b17f9c3",
|
||||
"domain": "klz-cables.com",
|
||||
"clientEmail": "marc@mintel.me",
|
||||
"clientName": "KLZ"
|
||||
}
|
||||
]
|
||||
26
apps/analytics-mailer/package.json
Normal file
26
apps/analytics-mailer/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@mintel/analytics-mailer",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm --clean",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mintel/mail": "workspace:*",
|
||||
"axios": "^1.7.9",
|
||||
"dotenv": "^16.4.7",
|
||||
"form-data": "^4.0.1",
|
||||
"mailgun.js": "^10.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mintel/eslint-config": "workspace:*",
|
||||
"@mintel/tsconfig": "workspace:*",
|
||||
"@types/node": "^22.10.2",
|
||||
"tsup": "^8.3.5",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
143
apps/analytics-mailer/src/index.ts
Normal file
143
apps/analytics-mailer/src/index.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import axios from "axios";
|
||||
import { render } from "@mintel/mail";
|
||||
import { MonthlyAnalyticsTemplate } from "@mintel/mail";
|
||||
import Mailgun from "mailgun.js";
|
||||
import formData from "form-data";
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const UMAMI_BASE_URL = process.env.UMAMI_BASE_URL || "https://umami.infra.mintel.me";
|
||||
const UMAMI_USERNAME = process.env.UMAMI_USERNAME;
|
||||
const UMAMI_PASSWORD = process.env.UMAMI_PASSWORD;
|
||||
const UMAMI_API_KEY = process.env.UMAMI_API_KEY;
|
||||
|
||||
const MAILGUN_API_KEY = process.env.MAILGUN_API_KEY;
|
||||
const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN;
|
||||
const MAILGUN_SENDER = process.env.MAILGUN_SENDER || "reports@mintel.me";
|
||||
const MAILGUN_URL = process.env.MAILGUN_URL || "https://api.eu.mailgun.net";
|
||||
|
||||
interface ConfigClient {
|
||||
websiteId: string;
|
||||
domain: string;
|
||||
clientEmail: string;
|
||||
clientName: string;
|
||||
}
|
||||
|
||||
async function getUmamiToken(): Promise<string> {
|
||||
if (UMAMI_API_KEY) {
|
||||
return UMAMI_API_KEY;
|
||||
}
|
||||
if (!UMAMI_USERNAME || !UMAMI_PASSWORD) {
|
||||
throw new Error("Missing UMAMI_USERNAME and UMAMI_PASSWORD or UMAMI_API_KEY");
|
||||
}
|
||||
const res = await axios.post(`${UMAMI_BASE_URL}/api/auth/login`, {
|
||||
username: UMAMI_USERNAME,
|
||||
password: UMAMI_PASSWORD,
|
||||
});
|
||||
return res.data.token;
|
||||
}
|
||||
|
||||
async function fetchUmamiData(api: any, websiteId: string, startAt: number, endAt: number) {
|
||||
const [statsRes, pagesRes, refRes] = await Promise.all([
|
||||
api.get(`/websites/${websiteId}/stats`, { params: { startAt, endAt } }),
|
||||
api.get(`/websites/${websiteId}/metrics`, { params: { type: "url", limit: 5, startAt, endAt } }),
|
||||
api.get(`/websites/${websiteId}/metrics`, { params: { type: "referrer", limit: 3, startAt, endAt } }),
|
||||
]);
|
||||
|
||||
const stats = statsRes.data;
|
||||
const topPages = (pagesRes.data || []).map((p: any) => ({ url: p.x, views: p.y }));
|
||||
const topReferrers = (refRes.data || []).map((r: any) => ({ url: r.x, visitors: r.y }));
|
||||
|
||||
return {
|
||||
totalVisitors: stats.visitors?.value || 0,
|
||||
totalPageviews: stats.pageviews?.value || 0,
|
||||
topPages,
|
||||
topReferrers,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const isDryRun = process.argv.includes("--dry-run");
|
||||
|
||||
// Load config
|
||||
const configPath = path.resolve(process.cwd(), "config.json");
|
||||
const configRaw = await fs.readFile(configPath, "utf-8");
|
||||
const clients: ConfigClient[] = JSON.parse(configRaw);
|
||||
|
||||
// Setup Umami API
|
||||
const token = await getUmamiToken();
|
||||
const headers = UMAMI_API_KEY ? { "x-umami-api-key": token } : { Authorization: `Bearer ${token}` };
|
||||
const api = axios.create({ baseURL: `${UMAMI_BASE_URL}/api`, headers });
|
||||
|
||||
// Setup Mailgun
|
||||
const mailgun = new Mailgun(formData);
|
||||
const mg = mailgun.client({ username: "api", key: MAILGUN_API_KEY!, url: MAILGUN_URL });
|
||||
|
||||
// Time range calculation (previous month)
|
||||
const now = new Date();
|
||||
// Ensure we get the correct previous month relative to current local time, but use UTC boundary or local? Umami uses ms timestamps.
|
||||
// Standard way: First day of previous month, last day of previous month.
|
||||
const startOfPrevMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
const endOfPrevMonth = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
|
||||
|
||||
const startAt = startOfPrevMonth.getTime();
|
||||
const endAt = endOfPrevMonth.getTime();
|
||||
|
||||
const monthName = startOfPrevMonth.toLocaleString("de-DE", { month: "long" });
|
||||
const year = startOfPrevMonth.getFullYear().toString();
|
||||
|
||||
console.log(`Generating reports for ${monthName} ${year}...`);
|
||||
|
||||
for (const client of clients) {
|
||||
if (client.websiteId === "replace-with-umami-website-id") continue;
|
||||
|
||||
console.log(`Processing ${client.domain} (${client.clientName})...`);
|
||||
|
||||
try {
|
||||
const data = await fetchUmamiData(api, client.websiteId, startAt, endAt);
|
||||
|
||||
const html = await render(MonthlyAnalyticsTemplate({
|
||||
clientName: client.clientName,
|
||||
domain: client.domain,
|
||||
month: monthName,
|
||||
year,
|
||||
totalVisitors: data.totalVisitors,
|
||||
totalPageviews: data.totalPageviews,
|
||||
topPages: data.topPages,
|
||||
topReferrers: data.topReferrers,
|
||||
}));
|
||||
|
||||
const subject = `Ihr Website-Report für ${monthName} ${year} - ${client.domain}`;
|
||||
|
||||
if (isDryRun) {
|
||||
console.log(`[DRY-RUN] Would send email to ${client.clientEmail} for ${client.domain}`);
|
||||
const debugPath = path.resolve(process.cwd(), `dry-run-${client.domain}.html`);
|
||||
await fs.writeFile(debugPath, html);
|
||||
console.log(`[DRY-RUN] Saved HTML preview to ${debugPath}`);
|
||||
} else {
|
||||
if (!MAILGUN_API_KEY) {
|
||||
throw new Error("MAILGUN_API_KEY is missing for live run.");
|
||||
}
|
||||
await mg.messages.create(MAILGUN_DOMAIN!, {
|
||||
from: MAILGUN_SENDER,
|
||||
to: [client.clientEmail],
|
||||
subject,
|
||||
html,
|
||||
});
|
||||
console.log(`Successfully sent to ${client.clientEmail}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`Failed to process client ${client.domain}:`, e.response?.data || e.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Done.");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
10
apps/analytics-mailer/tsconfig.json
Normal file
10
apps/analytics-mailer/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@mintel/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
177
blender-mv.sh
Executable file
177
blender-mv.sh
Executable 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."
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "3001:3001"
|
||||
- "3011:3001"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcp-network
|
||||
@@ -85,6 +85,30 @@ services:
|
||||
networks:
|
||||
- mcp-network
|
||||
|
||||
vikunja-mcp:
|
||||
build:
|
||||
context: ./packages/vikunja-mcp
|
||||
container_name: vikunja-mcp
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "3007:3007"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcp-network
|
||||
|
||||
outline-mcp:
|
||||
build:
|
||||
context: ./packages/outline-mcp
|
||||
container_name: outline-mcp
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "3008:3008"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcp-network
|
||||
|
||||
networks:
|
||||
mcp-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM node:20-bookworm-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN corepack enable pnpm && pnpm install --ignore-workspace
|
||||
RUN npm install -g pnpm@10.18.3 && pnpm install --ignore-workspace
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN pnpm build
|
||||
|
||||
2
packages/infra/docker/config.yml
Normal file
2
packages/infra/docker/config.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
service:
|
||||
allowiconchanges: false
|
||||
42
packages/infra/docker/vikunja-compose.yml
Normal file
42
packages/infra/docker/vikunja-compose.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
POSTGRES_USER: vikunja
|
||||
POSTGRES_PASSWORD: mintel_vikunja_DB_2026!
|
||||
POSTGRES_DB: vikunja
|
||||
volumes:
|
||||
- ./data/db:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
vikunja:
|
||||
image: vikunja/vikunja:latest
|
||||
environment:
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: mintel_vikunja_DB_2026!
|
||||
VIKUNJA_DATABASE_TYPE: postgres
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
VIKUNJA_SERVICE_PUBLICURL: https://tasks.infra.mintel.me/
|
||||
VIKUNJA_SERVICE_ENABLEREGISTRATION: true
|
||||
VIKUNJA_SERVICE_ALLOWICONCHANGES: false
|
||||
VIKUNJA_SERVICE_JWTSECRET: d2FzaHViZXJzZWN1cmVqd3RzZWNyZXQyMDI2IQ==
|
||||
volumes:
|
||||
- ./data/files:/app/vikunja/files
|
||||
- ./config.yml:/etc/vikunja/config.yml:ro
|
||||
depends_on:
|
||||
- db
|
||||
restart: always
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.vikunja.rule=Host(`tasks.infra.mintel.me`)"
|
||||
- "traefik.http.routers.vikunja.entrypoints=websecure"
|
||||
- "traefik.http.routers.vikunja.tls.certresolver=le"
|
||||
- "traefik.http.services.vikunja.loadbalancer.server.port=3456"
|
||||
networks:
|
||||
- default
|
||||
- infra
|
||||
|
||||
networks:
|
||||
infra:
|
||||
external: true
|
||||
@@ -56,7 +56,17 @@ fi
|
||||
# 3. Global Docker Pruning
|
||||
echo "🧹 Pruning Docker resources..."
|
||||
docker system prune -af --filter "until=24h"
|
||||
|
||||
echo "🧹 Pruning BuildKit builder containers and cache..."
|
||||
# Remove orphaned buildkit containers to release volume locks
|
||||
docker rm -f $(docker ps -aq --filter name=buildx_buildkit_builder) 2>/dev/null || true
|
||||
docker builder prune -af || true
|
||||
docker buildx prune -af || true
|
||||
|
||||
echo "🧹 Pruning Docker volumes..."
|
||||
docker volume prune -f
|
||||
# Force remove dangling buildx state volumes that might bypass standard prune
|
||||
docker volume ls -q | grep buildx | xargs -r docker volume rm 2>/dev/null || true
|
||||
|
||||
echo "✅ Optimization complete!"
|
||||
df -h /mnt/HC_Volume_104796416
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM node:20-bookworm-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN corepack enable pnpm && pnpm install --ignore-workspace
|
||||
RUN npm install -g pnpm@10.18.3 && pnpm install --ignore-workspace
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN pnpm build
|
||||
|
||||
@@ -25,3 +25,4 @@ export * from "./templates/ConfirmationMessage";
|
||||
export * from "./templates/FollowUpTemplate";
|
||||
export * from "./templates/ProjectEstimateTemplate";
|
||||
export * from "./templates/SiteAuditTemplate";
|
||||
export * from "./templates/MonthlyAnalyticsTemplate";
|
||||
|
||||
205
packages/mail/src/templates/MonthlyAnalyticsTemplate.tsx
Normal file
205
packages/mail/src/templates/MonthlyAnalyticsTemplate.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import * as React from "react";
|
||||
import { Heading, Section, Text, Row, Column } from "@react-email/components";
|
||||
import { MintelLayout } from "../layouts/MintelLayout";
|
||||
|
||||
export interface MonthlyAnalyticsTemplateProps {
|
||||
clientName: string;
|
||||
domain: string;
|
||||
month: string;
|
||||
year: string;
|
||||
totalVisitors: number;
|
||||
totalPageviews: number;
|
||||
topPages: { url: string; views: number }[];
|
||||
topReferrers: { url: string; visitors: number }[];
|
||||
}
|
||||
|
||||
export const MonthlyAnalyticsTemplate = ({
|
||||
clientName,
|
||||
domain,
|
||||
month,
|
||||
year,
|
||||
totalVisitors,
|
||||
totalPageviews,
|
||||
topPages,
|
||||
topReferrers,
|
||||
}: MonthlyAnalyticsTemplateProps) => {
|
||||
const preview = `Ihr Website-Report für ${month} ${year}: ${domain}`;
|
||||
|
||||
return (
|
||||
<MintelLayout preview={preview}>
|
||||
<Heading style={h1}>Monatsreport: {month} {year}</Heading>
|
||||
<Text style={intro}>
|
||||
Hallo {clientName},<br /><br />
|
||||
hier ist der monatliche Performance-Report für Ihre Website ({domain}).
|
||||
Die Daten zeigen, wie viele Menschen Ihre Seite besucht haben und welche Inhalte am beliebtesten waren.
|
||||
</Text>
|
||||
|
||||
<Section style={metricsContainer}>
|
||||
<Row>
|
||||
<Column style={metricColumn}>
|
||||
<Text style={metricLabel}>BESUCHER</Text>
|
||||
<Text style={metricValue}>{totalVisitors.toLocaleString('de-DE')}</Text>
|
||||
</Column>
|
||||
<Column style={metricColumn}>
|
||||
<Text style={metricLabel}>SEITENAUFRUFE</Text>
|
||||
<Text style={metricValue}>{totalPageviews.toLocaleString('de-DE')}</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
<Section style={auditContainer}>
|
||||
<Heading as="h2" style={h2}>Top Seiten</Heading>
|
||||
{topPages.map((page, i) => (
|
||||
<Row key={i} style={highlightRow}>
|
||||
<Column style={bulletCol}>
|
||||
<div style={bullet} />
|
||||
</Column>
|
||||
<Column>
|
||||
<Text style={highlightText}>{page.url}</Text>
|
||||
</Column>
|
||||
<Column align="right">
|
||||
<Text style={highlightNumber}>{page.views.toLocaleString('de-DE')} Aufrufe</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
))}
|
||||
</Section>
|
||||
|
||||
<Section style={auditContainer}>
|
||||
<Heading as="h2" style={h2}>Top Besucherquellen</Heading>
|
||||
{topReferrers.length > 0 ? topReferrers.map((ref, i) => (
|
||||
<Row key={i} style={highlightRow}>
|
||||
<Column style={bulletCol}>
|
||||
<div style={bullet} />
|
||||
</Column>
|
||||
<Column>
|
||||
<Text style={highlightText}>{ref.url || "Direktaufruf"}</Text>
|
||||
</Column>
|
||||
<Column align="right">
|
||||
<Text style={highlightNumber}>{ref.visitors.toLocaleString('de-DE')} Besucher</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
)) : (
|
||||
<Text style={highlightText}>Keine externen Quellen verzeichnet.</Text>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Text style={bodyText}>
|
||||
Haben Sie Fragen zu diesen Zahlen oder möchten Sie die Reichweite Ihrer Seite weiter ausbauen?
|
||||
Antworten Sie einfach auf diese E-Mail.
|
||||
</Text>
|
||||
|
||||
<Text style={footerText}>
|
||||
Beste Grüße,<br />
|
||||
<strong>Marc Mintel</strong><br />
|
||||
Digitaler Architekt
|
||||
</Text>
|
||||
</MintelLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonthlyAnalyticsTemplate;
|
||||
|
||||
const h1 = {
|
||||
fontSize: "28px",
|
||||
fontWeight: "900",
|
||||
margin: "0 0 24px",
|
||||
color: "#ffffff",
|
||||
letterSpacing: "-0.04em",
|
||||
};
|
||||
|
||||
const h2 = {
|
||||
fontSize: "14px",
|
||||
fontWeight: "900",
|
||||
textTransform: "uppercase" as const,
|
||||
color: "#444444",
|
||||
margin: "0 0 16px",
|
||||
letterSpacing: "0.1em",
|
||||
};
|
||||
|
||||
const intro = {
|
||||
fontSize: "16px",
|
||||
lineHeight: "24px",
|
||||
color: "#cccccc",
|
||||
margin: "0 0 32px",
|
||||
};
|
||||
|
||||
const metricsContainer = {
|
||||
backgroundColor: "#151515",
|
||||
padding: "32px",
|
||||
borderRadius: "8px",
|
||||
marginBottom: "32px",
|
||||
border: "1px solid #222222",
|
||||
textAlign: "center" as const,
|
||||
};
|
||||
|
||||
const metricColumn = {
|
||||
width: "50%",
|
||||
};
|
||||
|
||||
const metricLabel = {
|
||||
fontSize: "12px",
|
||||
fontWeight: "bold",
|
||||
color: "#888888",
|
||||
letterSpacing: "0.1em",
|
||||
margin: "0 0 8px",
|
||||
};
|
||||
|
||||
const metricValue = {
|
||||
fontSize: "36px",
|
||||
fontWeight: "900",
|
||||
color: "#4CAF50",
|
||||
margin: "0",
|
||||
letterSpacing: "-0.02em",
|
||||
};
|
||||
|
||||
const auditContainer = {
|
||||
backgroundColor: "#151515",
|
||||
padding: "32px",
|
||||
borderRadius: "8px",
|
||||
marginBottom: "32px",
|
||||
border: "1px solid #222222",
|
||||
};
|
||||
|
||||
const highlightRow = {
|
||||
marginBottom: "12px",
|
||||
};
|
||||
|
||||
const bulletCol = {
|
||||
width: "24px",
|
||||
verticalAlign: "top" as const,
|
||||
};
|
||||
|
||||
const bullet = {
|
||||
width: "6px",
|
||||
height: "6px",
|
||||
backgroundColor: "#4CAF50",
|
||||
marginTop: "8px",
|
||||
};
|
||||
|
||||
const highlightText = {
|
||||
fontSize: "15px",
|
||||
color: "#ffffff",
|
||||
margin: "0",
|
||||
lineHeight: "22px",
|
||||
wordBreak: "break-all" as const,
|
||||
};
|
||||
|
||||
const highlightNumber = {
|
||||
fontSize: "15px",
|
||||
color: "#888888",
|
||||
margin: "0",
|
||||
whiteSpace: "nowrap" as const,
|
||||
};
|
||||
|
||||
const bodyText = {
|
||||
fontSize: "16px",
|
||||
lineHeight: "24px",
|
||||
color: "#888888",
|
||||
margin: "0 0 32px",
|
||||
};
|
||||
|
||||
const footerText = {
|
||||
fontSize: "14px",
|
||||
color: "#666666",
|
||||
lineHeight: "20px",
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM node:20-bookworm-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN corepack enable pnpm && pnpm install --ignore-workspace
|
||||
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
|
||||
|
||||
@@ -8,11 +8,6 @@ import path from "node:path";
|
||||
export const baseNextConfig = {
|
||||
output: "standalone",
|
||||
turbopack: {},
|
||||
experimental: {
|
||||
serverActions: {
|
||||
allowedOrigins: ["*.klz-cables.com", "*.branch.klz-cables.com", "localhost:3000", "*.mintel.me"],
|
||||
},
|
||||
},
|
||||
images: {
|
||||
dangerouslyAllowSVG: true,
|
||||
contentDispositionType: "attachment",
|
||||
@@ -58,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);
|
||||
@@ -76,3 +96,4 @@ const withMintelConfig = (config) => {
|
||||
};
|
||||
|
||||
export default withMintelConfig;
|
||||
|
||||
|
||||
15
packages/outline-mcp/Dockerfile
Normal file
15
packages/outline-mcp/Dockerfile
Normal 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"]
|
||||
26
packages/outline-mcp/package.json
Normal file
26
packages/outline-mcp/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@mintel/outline-mcp",
|
||||
"version": "1.0.0",
|
||||
"description": "Outline MCP server for Mintel infrastructure",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/start.js",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"test:unit": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||
"axios": "^1.7.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^4.19.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.14.10",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.5.3",
|
||||
"vitest": "^2.1.3"
|
||||
}
|
||||
}
|
||||
91
packages/outline-mcp/src/index.test.ts
Normal file
91
packages/outline-mcp/src/index.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import axios from 'axios';
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
|
||||
// Mock environment
|
||||
process.env.OUTLINE_API_TOKEN = 'test-token';
|
||||
process.env.OUTLINE_BASE_URL = 'https://outline.test';
|
||||
|
||||
// Mock axios
|
||||
vi.mock('axios', () => {
|
||||
const mockPost = vi.fn();
|
||||
return {
|
||||
default: {
|
||||
create: vi.fn(() => ({
|
||||
post: mockPost
|
||||
}))
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
describe('Outline MCP Server Tools', () => {
|
||||
let mockPost: any;
|
||||
let requestHandler: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Extract the mock
|
||||
const axiosInstance = axios.create();
|
||||
mockPost = axiosInstance.post;
|
||||
|
||||
// Mock Server setRequestHandler
|
||||
let capturedHandler: any;
|
||||
vi.spyOn(Server.prototype, 'setRequestHandler').mockImplementation((schema: any, handler: any) => {
|
||||
// The first call is usually ListToolsRequestSchema, second is CallToolRequestSchema
|
||||
// We just store the last one which is CallToolRequestSchema
|
||||
capturedHandler = handler;
|
||||
return {} as any;
|
||||
});
|
||||
|
||||
// Re-import to trigger registration
|
||||
vi.resetModules();
|
||||
await import('./index.js');
|
||||
|
||||
requestHandler = capturedHandler;
|
||||
});
|
||||
|
||||
it('should search documents', async () => {
|
||||
mockPost.mockResolvedValueOnce({ data: { data: [{ id: '1', title: 'Test' }] } });
|
||||
|
||||
const result = await requestHandler({
|
||||
params: {
|
||||
name: 'outline_search_documents',
|
||||
arguments: { query: 'test' }
|
||||
}
|
||||
});
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith('/api/documents.search', { query: 'test', collectionId: undefined });
|
||||
expect(result.content[0].text).toContain('Test');
|
||||
});
|
||||
|
||||
it('should get a document', async () => {
|
||||
mockPost.mockResolvedValueOnce({ data: { data: { id: 'doc1', title: 'Doc 1' } } });
|
||||
|
||||
const result = await requestHandler({
|
||||
params: {
|
||||
name: 'outline_get_document',
|
||||
arguments: { id: 'doc1' }
|
||||
}
|
||||
});
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith('/api/documents.info', { id: 'doc1' });
|
||||
expect(result.content[0].text).toContain('Doc 1');
|
||||
});
|
||||
|
||||
it('should create a document', async () => {
|
||||
mockPost.mockResolvedValueOnce({ data: { data: { id: 'new-doc' } } });
|
||||
|
||||
const result = await requestHandler({
|
||||
params: {
|
||||
name: 'outline_create_document',
|
||||
arguments: { collectionId: 'col1', title: 'New Doc', text: 'Hello', publish: true }
|
||||
}
|
||||
});
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith('/api/documents.create', {
|
||||
collectionId: 'col1', title: 'New Doc', text: 'Hello', publish: true
|
||||
});
|
||||
expect(result.content[0].text).toContain('new-doc');
|
||||
});
|
||||
});
|
||||
184
packages/outline-mcp/src/index.ts
Normal file
184
packages/outline-mcp/src/index.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import axios from "axios";
|
||||
|
||||
// Environment variables
|
||||
const OUTLINE_API_TOKEN = process.env.OUTLINE_API_TOKEN;
|
||||
const OUTLINE_BASE_URL = process.env.OUTLINE_BASE_URL;
|
||||
|
||||
if (!OUTLINE_API_TOKEN || !OUTLINE_BASE_URL) {
|
||||
console.error("Missing required environment variables: OUTLINE_API_TOKEN or OUTLINE_BASE_URL");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: OUTLINE_BASE_URL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${OUTLINE_API_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
// Define tools
|
||||
const tools: Tool[] = [
|
||||
{
|
||||
name: "outline_search_documents",
|
||||
description: "Search for documents in Outline.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search query" },
|
||||
collectionId: { type: "string", description: "Optional collection ID to scope search" },
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "outline_get_document",
|
||||
description: "Get a document by its ID.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "The ID of the document" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "outline_create_document",
|
||||
description: "Create a new document.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
collectionId: { type: "string", description: "The ID of the collection" },
|
||||
title: { type: "string", description: "Document title" },
|
||||
text: { type: "string", description: "Document content in Markdown" },
|
||||
publish: { type: "boolean", description: "Whether to publish the document" },
|
||||
},
|
||||
required: ["collectionId", "title", "text"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "outline_update_document",
|
||||
description: "Update an existing document.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "The ID of the document" },
|
||||
title: { type: "string", description: "New title" },
|
||||
text: { type: "string", description: "New content in Markdown" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "outline_create_collection",
|
||||
description: "Create a new collection.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Collection name" },
|
||||
description: { type: "string", description: "Collection description" },
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: "at-mintel-outline-mcp",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return { tools };
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
switch (request.params.name) {
|
||||
case "outline_search_documents": {
|
||||
const { query, collectionId } = request.params.arguments as any;
|
||||
const res = await axiosInstance.post("/api/documents.search", {
|
||||
query,
|
||||
collectionId,
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
||||
};
|
||||
}
|
||||
case "outline_get_document": {
|
||||
const { id } = request.params.arguments as any;
|
||||
const res = await axiosInstance.post("/api/documents.info", { id });
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
||||
};
|
||||
}
|
||||
case "outline_create_document": {
|
||||
const { collectionId, title, text, publish } = request.params.arguments as any;
|
||||
const res = await axiosInstance.post("/api/documents.create", {
|
||||
collectionId,
|
||||
title,
|
||||
text,
|
||||
publish: publish ?? true,
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
||||
};
|
||||
}
|
||||
case "outline_update_document": {
|
||||
const { id, title, text } = request.params.arguments as any;
|
||||
const res = await axiosInstance.post("/api/documents.update", {
|
||||
id,
|
||||
title,
|
||||
text,
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
||||
};
|
||||
}
|
||||
case "outline_create_collection": {
|
||||
const { name, description } = request.params.arguments as any;
|
||||
const res = await axiosInstance.post("/api/collections.create", {
|
||||
name,
|
||||
description,
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMsg = error.response?.data ? JSON.stringify(error.response.data) : error.message;
|
||||
return {
|
||||
content: [{ type: "text", text: `API Error: ${errorMsg}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Outline MCP Server running on stdio");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
13
packages/outline-mcp/src/start.ts
Normal file
13
packages/outline-mcp/src/start.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { config } from 'dotenv';
|
||||
import { resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
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 Outline MCP Server:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
16
packages/outline-mcp/tsconfig.json
Normal file
16
packages/outline-mcp/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM node:20-bookworm-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN corepack enable pnpm && pnpm install --ignore-workspace
|
||||
RUN npm install -g pnpm@10.18.3 && pnpm install --ignore-workspace
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN pnpm build
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM node:20-bookworm-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN corepack enable pnpm && pnpm install --ignore-workspace
|
||||
RUN npm install -g pnpm@10.18.3 && pnpm install --ignore-workspace
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN pnpm build
|
||||
|
||||
15
packages/vikunja-mcp/Dockerfile
Normal file
15
packages/vikunja-mcp/Dockerfile
Normal 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"]
|
||||
30
packages/vikunja-mcp/package.json
Normal file
30
packages/vikunja-mcp/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@mintel/vikunja-mcp",
|
||||
"version": "1.0.0",
|
||||
"description": "Vikunja MCP server for Mintel infrastructure",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/start.js",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"test:unit": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||
"axios": "^1.7.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^4.19.2",
|
||||
"form-data": "^4.0.5",
|
||||
"marked": "^18.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/form-data": "^2.5.2",
|
||||
"@types/marked": "^6.0.0",
|
||||
"@types/node": "^20.14.10",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.5.3",
|
||||
"vitest": "^2.1.3"
|
||||
}
|
||||
}
|
||||
205
packages/vikunja-mcp/src/index.test.ts
Normal file
205
packages/vikunja-mcp/src/index.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock axios
|
||||
const mockGet = vi.fn();
|
||||
const mockPost = vi.fn();
|
||||
const mockPut = vi.fn();
|
||||
const mockDelete = vi.fn();
|
||||
|
||||
vi.mock('axios', () => {
|
||||
return {
|
||||
default: {
|
||||
create: vi.fn().mockImplementation(() => ({
|
||||
get: mockGet,
|
||||
post: mockPost,
|
||||
put: mockPut,
|
||||
delete: mockDelete,
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Set env vars
|
||||
process.env.VIKUNJA_API_TOKEN = 'test-token';
|
||||
process.env.VIKUNJA_BASE_URL = 'https://tasks.test.me';
|
||||
|
||||
// Mock express to avoid starting a real server
|
||||
vi.mock('express', () => {
|
||||
const mockApp = {
|
||||
use: vi.fn(),
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
listen: vi.fn().mockImplementation((port, host, cb) => cb && cb()),
|
||||
};
|
||||
const mockExpress = () => mockApp;
|
||||
(mockExpress as any).json = () => vi.fn();
|
||||
return { default: mockExpress };
|
||||
});
|
||||
|
||||
let server: any;
|
||||
|
||||
describe('Vikunja MCP Server - New Tools (RED)', () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
if (!server) {
|
||||
const mod = await import('./index.js');
|
||||
server = mod.server;
|
||||
}
|
||||
});
|
||||
|
||||
it('should support vikunja_create_project', async () => {
|
||||
mockPut.mockResolvedValueOnce({
|
||||
data: { id: 10, title: 'New Project', description: 'Testing' }
|
||||
});
|
||||
|
||||
const handler = (server as any)._requestHandlers.get('tools/call');
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
const response = await handler({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'vikunja_create_project',
|
||||
arguments: {
|
||||
title: 'New Project',
|
||||
description: 'Testing',
|
||||
hex_color: 'ff0000',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPut).toHaveBeenCalledWith('/projects', {
|
||||
title: 'New Project',
|
||||
description: 'Testing',
|
||||
hex_color: 'ff0000',
|
||||
});
|
||||
expect(response.isError).toBeUndefined();
|
||||
expect(JSON.parse(response.content[0].text)).toEqual({
|
||||
id: 10,
|
||||
title: 'New Project',
|
||||
description: 'Testing',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support vikunja_get_task', async () => {
|
||||
mockGet.mockResolvedValueOnce({
|
||||
data: { id: 42, title: 'My Task', project_id: 1 }
|
||||
});
|
||||
|
||||
const handler = (server as any)._requestHandlers.get('tools/call');
|
||||
const response = await handler({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'vikunja_get_task',
|
||||
arguments: {
|
||||
task_id: 42,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockGet).toHaveBeenCalledWith('/tasks/42');
|
||||
expect(response.isError).toBeUndefined();
|
||||
expect(JSON.parse(response.content[0].text)).toEqual({
|
||||
id: 42,
|
||||
title: 'My Task',
|
||||
project_id: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should support vikunja_delete_task', async () => {
|
||||
mockDelete.mockResolvedValueOnce({
|
||||
data: { message: 'success' }
|
||||
});
|
||||
|
||||
const handler = (server as any)._requestHandlers.get('tools/call');
|
||||
const response = await handler({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'vikunja_delete_task',
|
||||
arguments: {
|
||||
task_id: 42,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockDelete).toHaveBeenCalledWith('/tasks/42');
|
||||
expect(response.isError).toBeUndefined();
|
||||
expect(JSON.parse(response.content[0].text)).toEqual({
|
||||
message: 'success',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support vikunja_create_task_comment', async () => {
|
||||
mockPut.mockResolvedValueOnce({
|
||||
data: { id: 1, comment: 'New Comment', task_id: 42 }
|
||||
});
|
||||
|
||||
const handler = (server as any)._requestHandlers.get('tools/call');
|
||||
const response = await handler({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'vikunja_create_task_comment',
|
||||
arguments: {
|
||||
task_id: 42,
|
||||
comment: 'New Comment',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockPut).toHaveBeenCalledWith('/tasks/42/comments', {
|
||||
comment: 'New Comment',
|
||||
});
|
||||
expect(response.isError).toBeUndefined();
|
||||
expect(JSON.parse(response.content[0].text)).toEqual({
|
||||
id: 1,
|
||||
comment: 'New Comment',
|
||||
task_id: 42,
|
||||
});
|
||||
});
|
||||
|
||||
it('should support vikunja_get_task_comments', async () => {
|
||||
mockGet.mockResolvedValueOnce({
|
||||
data: [{ id: 1, comment: 'New Comment', task_id: 42 }]
|
||||
});
|
||||
|
||||
const handler = (server as any)._requestHandlers.get('tools/call');
|
||||
const response = await handler({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'vikunja_get_task_comments',
|
||||
arguments: {
|
||||
task_id: 42,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockGet).toHaveBeenCalledWith('/tasks/42/comments');
|
||||
expect(response.isError).toBeUndefined();
|
||||
expect(JSON.parse(response.content[0].text)).toEqual([
|
||||
{ id: 1, comment: 'New Comment', task_id: 42 }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support vikunja_create_task and convert markdown description to HTML', async () => {
|
||||
mockPut.mockResolvedValueOnce({
|
||||
data: { id: 11, title: 'Markdown Task', description: '<p><strong>Bold</strong> and <em>italic</em></p>\n' }
|
||||
});
|
||||
|
||||
const handler = (server as any)._requestHandlers.get('tools/call');
|
||||
const response = await handler({
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'vikunja_create_task',
|
||||
arguments: {
|
||||
project_id: 1,
|
||||
title: 'Markdown Task',
|
||||
description: '**Bold** and *italic*'
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockPut).toHaveBeenCalledWith('/projects/1/tasks', expect.objectContaining({
|
||||
title: 'Markdown Task',
|
||||
description: '<p><strong>Bold</strong> and <em>italic</em></p>\n'
|
||||
}));
|
||||
expect(response.isError).toBeUndefined();
|
||||
});
|
||||
});
|
||||
421
packages/vikunja-mcp/src/index.ts
Normal file
421
packages/vikunja-mcp/src/index.ts
Normal file
@@ -0,0 +1,421 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import express from 'express';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import axios from "axios";
|
||||
import https from "https";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { marked } from "marked";
|
||||
|
||||
const VIKUNJA_BASE_URL = process.env.VIKUNJA_BASE_URL || "https://tasks.infra.mintel.me";
|
||||
const VIKUNJA_API_TOKEN = process.env.VIKUNJA_API_TOKEN;
|
||||
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
if (!VIKUNJA_API_TOKEN) {
|
||||
console.error("Warning: VIKUNJA_API_TOKEN is not set.");
|
||||
}
|
||||
|
||||
function getApi() {
|
||||
if (!VIKUNJA_API_TOKEN) {
|
||||
throw new Error("VIKUNJA_API_TOKEN is not configured.");
|
||||
}
|
||||
return axios.create({
|
||||
baseURL: `${VIKUNJA_BASE_URL}/api/v1`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${VIKUNJA_API_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
httpsAgent,
|
||||
timeout: 10000
|
||||
});
|
||||
}
|
||||
|
||||
// --- Tool Definitions ---
|
||||
const GET_PROJECTS_TOOL: Tool = {
|
||||
name: "vikunja_get_projects",
|
||||
description: "List all projects in Vikunja",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
};
|
||||
|
||||
const GET_TASKS_TOOL: Tool = {
|
||||
name: "vikunja_get_tasks",
|
||||
description: "Get all tasks for a specific project",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
project_id: { type: "number", description: "The ID of the project" },
|
||||
},
|
||||
required: ["project_id"],
|
||||
},
|
||||
};
|
||||
|
||||
const CREATE_TASK_TOOL: Tool = {
|
||||
name: "vikunja_create_task",
|
||||
description: "Create a new task in a project",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
project_id: { type: "number", description: "The ID of the project to add the task to" },
|
||||
title: { type: "string", description: "The title of the task" },
|
||||
description: { type: "string", description: "Optional description of the task" },
|
||||
parent_task_id: { type: "number", description: "Optional ID of a parent task to create a subtask" },
|
||||
due_date: { type: "string", description: "ISO 8601 Date string for due date" },
|
||||
start_date: { type: "string", description: "ISO 8601 Date string for start date" },
|
||||
end_date: { type: "string", description: "ISO 8601 Date string for end date" },
|
||||
priority: { type: "number", description: "Priority level (e.g. 1-5)" },
|
||||
percent_done: { type: "number", description: "Progress percentage" },
|
||||
hex_color: { type: "string", description: "Hex color code" },
|
||||
is_favorite: { type: "boolean", description: "Mark as favorite" }
|
||||
},
|
||||
required: ["project_id", "title"],
|
||||
},
|
||||
};
|
||||
|
||||
const UPDATE_TASK_TOOL: Tool = {
|
||||
name: "vikunja_update_task",
|
||||
description: "Update a task (e.g., mark as done)",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
task_id: { type: "number", description: "The ID of the task to update" },
|
||||
done: { type: "boolean", description: "Set to true to mark the task as done" },
|
||||
title: { type: "string", description: "Update the title" },
|
||||
description: { type: "string", description: "Update the description" },
|
||||
due_date: { type: "string", description: "ISO 8601 Date string for due date" },
|
||||
start_date: { type: "string", description: "ISO 8601 Date string for start date" },
|
||||
end_date: { type: "string", description: "ISO 8601 Date string for end date" },
|
||||
priority: { type: "number", description: "Priority level (e.g. 1-5)" },
|
||||
percent_done: { type: "number", description: "Progress percentage (0-1 or 0-100 depending on API version, try decimals like 0.5 first)" },
|
||||
hex_color: { type: "string", description: "Hex color code" },
|
||||
is_favorite: { type: "boolean", description: "Mark as favorite" }
|
||||
},
|
||||
required: ["task_id"],
|
||||
},
|
||||
};
|
||||
|
||||
const CREATE_PROJECT_TOOL: Tool = {
|
||||
name: "vikunja_create_project",
|
||||
description: "Create a new project (list) in Vikunja",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string", description: "The title of the project" },
|
||||
description: { type: "string", description: "Optional description of the project" },
|
||||
hex_color: { type: "string", description: "Optional hex color without the '#' prefix" }
|
||||
},
|
||||
required: ["title"]
|
||||
}
|
||||
};
|
||||
|
||||
const GET_TASK_TOOL: Tool = {
|
||||
name: "vikunja_get_task",
|
||||
description: "Get details for a specific task by ID",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
task_id: { type: "number", description: "The ID of the task" }
|
||||
},
|
||||
required: ["task_id"]
|
||||
}
|
||||
};
|
||||
|
||||
const DELETE_TASK_TOOL: Tool = {
|
||||
name: "vikunja_delete_task",
|
||||
description: "Delete a task by ID",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
task_id: { type: "number", description: "The ID of the task to delete" }
|
||||
},
|
||||
required: ["task_id"]
|
||||
}
|
||||
};
|
||||
|
||||
const CREATE_TASK_COMMENT_TOOL: Tool = {
|
||||
name: "vikunja_create_task_comment",
|
||||
description: "Add a comment to a task",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
task_id: { type: "number", description: "The ID of the task to comment on" },
|
||||
comment: { type: "string", description: "The comment text" }
|
||||
},
|
||||
required: ["task_id", "comment"]
|
||||
}
|
||||
};
|
||||
|
||||
const GET_TASK_COMMENTS_TOOL: Tool = {
|
||||
name: "vikunja_get_task_comments",
|
||||
description: "Get all comments for a specific task",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
task_id: { type: "number", description: "The ID of the task" }
|
||||
},
|
||||
required: ["task_id"]
|
||||
}
|
||||
};
|
||||
|
||||
const GET_TASK_ATTACHMENT_TOOL: Tool = {
|
||||
name: "vikunja_get_task_attachment",
|
||||
description: "Download a task attachment to a local file",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
task_id: { type: "number", description: "The ID of the task" },
|
||||
attachment_id: { type: "number", description: "The ID of the attachment" },
|
||||
save_path: { type: "string", description: "Absolute path to save the file to" }
|
||||
},
|
||||
required: ["task_id", "attachment_id", "save_path"]
|
||||
}
|
||||
};
|
||||
|
||||
const CREATE_TASK_ATTACHMENT_TOOL: Tool = {
|
||||
name: "vikunja_upload_task_attachment",
|
||||
description: "Upload an attachment file to a specific task",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
task_id: { type: "number", description: "The ID of the task" },
|
||||
file_path: { type: "string", description: "Absolute path to the local file to upload" }
|
||||
},
|
||||
required: ["task_id", "file_path"]
|
||||
}
|
||||
};
|
||||
|
||||
// --- Server Setup ---
|
||||
export const server = new Server(
|
||||
{ name: "vikunja-mcp", version: "1.0.0" },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
GET_PROJECTS_TOOL,
|
||||
GET_TASKS_TOOL,
|
||||
CREATE_TASK_TOOL,
|
||||
UPDATE_TASK_TOOL,
|
||||
CREATE_PROJECT_TOOL,
|
||||
GET_TASK_TOOL,
|
||||
DELETE_TASK_TOOL,
|
||||
CREATE_TASK_COMMENT_TOOL,
|
||||
GET_TASK_COMMENTS_TOOL,
|
||||
GET_TASK_ATTACHMENT_TOOL,
|
||||
CREATE_TASK_ATTACHMENT_TOOL
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
const api = getApi();
|
||||
|
||||
if (request.params.name === "vikunja_get_projects") {
|
||||
const res = await api.get('/projects');
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_get_tasks") {
|
||||
const { project_id } = request.params.arguments as any;
|
||||
const res = await api.get(`/projects/${project_id}/tasks`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_create_task") {
|
||||
const {
|
||||
project_id, title, description, parent_task_id,
|
||||
due_date, start_date, end_date, priority, percent_done, hex_color, is_favorite
|
||||
} = request.params.arguments as any;
|
||||
|
||||
const payload: any = {
|
||||
title,
|
||||
description: description ? await marked.parse(description) : ""
|
||||
};
|
||||
|
||||
if (parent_task_id !== undefined) payload.parent_task_id = parent_task_id;
|
||||
if (due_date !== undefined) payload.due_date = due_date;
|
||||
if (start_date !== undefined) payload.start_date = start_date;
|
||||
if (end_date !== undefined) payload.end_date = end_date;
|
||||
if (priority !== undefined) payload.priority = priority;
|
||||
if (percent_done !== undefined) payload.percent_done = percent_done;
|
||||
if (hex_color !== undefined) payload.hex_color = hex_color;
|
||||
if (is_favorite !== undefined) payload.is_favorite = is_favorite;
|
||||
|
||||
const res = await api.put(`/projects/${project_id}/tasks`, payload);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_update_task") {
|
||||
const {
|
||||
task_id, done, title, description,
|
||||
due_date, start_date, end_date,
|
||||
priority, percent_done, hex_color, is_favorite
|
||||
} = request.params.arguments as any;
|
||||
|
||||
const payload: any = {};
|
||||
if (done !== undefined) payload.done = done;
|
||||
if (title !== undefined) payload.title = title;
|
||||
if (description !== undefined) payload.description = await marked.parse(description);
|
||||
if (due_date !== undefined) payload.due_date = due_date;
|
||||
if (start_date !== undefined) payload.start_date = start_date;
|
||||
if (end_date !== undefined) payload.end_date = end_date;
|
||||
if (priority !== undefined) payload.priority = priority;
|
||||
if (percent_done !== undefined) payload.percent_done = percent_done;
|
||||
if (hex_color !== undefined) payload.hex_color = hex_color;
|
||||
if (is_favorite !== undefined) payload.is_favorite = is_favorite;
|
||||
|
||||
const res = await api.post(`/tasks/${task_id}`, payload);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_create_project") {
|
||||
const { title, description, hex_color } = request.params.arguments as any;
|
||||
// Vikunja standard: creating a new root resource uses PUT /projects in this version
|
||||
const res = await api.put('/projects', {
|
||||
title,
|
||||
description: description || "",
|
||||
hex_color: hex_color || ""
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_get_task") {
|
||||
const { task_id } = request.params.arguments as any;
|
||||
const res = await api.get(`/tasks/${task_id}`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_delete_task") {
|
||||
const { task_id } = request.params.arguments as any;
|
||||
const res = await api.delete(`/tasks/${task_id}`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_create_task_comment") {
|
||||
const { task_id, comment } = request.params.arguments as any;
|
||||
// Vikunja standard: task sub-resources (like comments) are typically appended using PUT /tasks/{id}/comments
|
||||
const res = await api.put(`/tasks/${task_id}/comments`, { comment });
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_get_task_comments") {
|
||||
const { task_id } = request.params.arguments as any;
|
||||
const res = await api.get(`/tasks/${task_id}/comments`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_get_task_attachment") {
|
||||
const { task_id, attachment_id, save_path } = request.params.arguments as any;
|
||||
const res = await api.get(`/tasks/${task_id}/attachments/${attachment_id}`, {
|
||||
responseType: 'arraybuffer'
|
||||
});
|
||||
fs.writeFileSync(save_path, Buffer.from(res.data));
|
||||
return { content: [{ type: "text", text: `Successfully saved attachment to ${save_path}` }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "vikunja_upload_task_attachment") {
|
||||
const { task_id, file_path } = request.params.arguments as any;
|
||||
if (!fs.existsSync(file_path)) {
|
||||
throw new Error(`File not found: ${file_path}`);
|
||||
}
|
||||
|
||||
const buffer = fs.readFileSync(file_path);
|
||||
const blob = new Blob([buffer]);
|
||||
const formData = new FormData();
|
||||
formData.append('files', blob, path.basename(file_path));
|
||||
|
||||
const res = await api.put(`/tasks/${task_id}/attachments`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return { content: [{ type: "text", text: `Successfully uploaded attachment to task ${task_id}.\nResponse: ${JSON.stringify(res.data)}` }] };
|
||||
}
|
||||
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
} catch (e: any) {
|
||||
const msg = e.response?.data?.message || e.message;
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: "text", text: `MCP Service Error: ${msg}` }]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// --- Express / SSE Server Setup ---
|
||||
async function run() {
|
||||
const isStdio = process.argv.includes('--stdio');
|
||||
|
||||
if (isStdio) {
|
||||
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('Vikunja MCP server is running on stdio');
|
||||
} else {
|
||||
const app = express();
|
||||
const transports = new Map<string, SSEServerTransport>();
|
||||
|
||||
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.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);
|
||||
});
|
||||
|
||||
app.post('/message', express.json(), async (req, res) => {
|
||||
const sessionId = req.query.sessionId as string;
|
||||
const transport = transports.get(sessionId);
|
||||
|
||||
if (!transport) {
|
||||
res.status(400).send('No active SSE connection for this session');
|
||||
return;
|
||||
}
|
||||
await transport.handlePostMessage(req, res);
|
||||
});
|
||||
|
||||
const PORT = Number(process.env.VIKUNJA_MCP_PORT) || 3007;
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.error(`Vikunja MCP server running on http://0.0.0.0:${PORT}/sse`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
13
packages/vikunja-mcp/src/start.ts
Normal file
13
packages/vikunja-mcp/src/start.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { config } from 'dotenv';
|
||||
import { resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
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 Vikunja MCP Server:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
16
packages/vikunja-mcp/tsconfig.json
Normal file
16
packages/vikunja-mcp/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
231
pnpm-lock.yaml
generated
231
pnpm-lock.yaml
generated
@@ -104,6 +104,43 @@ importers:
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
apps/analytics-mailer:
|
||||
dependencies:
|
||||
'@mintel/mail':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/mail
|
||||
axios:
|
||||
specifier: ^1.7.9
|
||||
version: 1.13.5
|
||||
dotenv:
|
||||
specifier: ^16.4.7
|
||||
version: 16.6.1
|
||||
form-data:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.5
|
||||
mailgun.js:
|
||||
specifier: ^10.3.0
|
||||
version: 10.4.0
|
||||
devDependencies:
|
||||
'@mintel/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/eslint-config
|
||||
'@mintel/tsconfig':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/tsconfig
|
||||
'@types/node':
|
||||
specifier: ^22.10.2
|
||||
version: 22.19.10
|
||||
tsup:
|
||||
specifier: ^8.3.5
|
||||
version: 8.5.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
|
||||
tsx:
|
||||
specifier: ^4.19.2
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.7.2
|
||||
version: 5.9.3
|
||||
|
||||
apps/sample-website:
|
||||
dependencies:
|
||||
'@mintel/next-observability':
|
||||
@@ -235,7 +272,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.0.5
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
packages/content-engine:
|
||||
dependencies:
|
||||
@@ -537,7 +574,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.0.4
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
packages/meme-generator:
|
||||
dependencies:
|
||||
@@ -754,6 +791,37 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
|
||||
packages/outline-mcp:
|
||||
dependencies:
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: ^1.5.0
|
||||
version: 1.27.1(zod@3.25.76)
|
||||
axios:
|
||||
specifier: ^1.7.2
|
||||
version: 1.13.5
|
||||
dotenv:
|
||||
specifier: ^17.3.1
|
||||
version: 17.3.1
|
||||
express:
|
||||
specifier: ^4.19.2
|
||||
version: 4.22.1
|
||||
devDependencies:
|
||||
'@types/express':
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.25
|
||||
'@types/node':
|
||||
specifier: ^20.14.10
|
||||
version: 20.19.33
|
||||
tsx:
|
||||
specifier: ^4.19.2
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.5.3
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
|
||||
packages/page-audit:
|
||||
dependencies:
|
||||
commander:
|
||||
@@ -897,7 +965,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.0.5
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
packages/serpbear-mcp:
|
||||
dependencies:
|
||||
@@ -979,6 +1047,49 @@ importers:
|
||||
specifier: ^5.5.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/vikunja-mcp:
|
||||
dependencies:
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: ^1.5.0
|
||||
version: 1.27.1(zod@3.25.76)
|
||||
axios:
|
||||
specifier: ^1.7.2
|
||||
version: 1.13.5
|
||||
dotenv:
|
||||
specifier: ^17.3.1
|
||||
version: 17.3.1
|
||||
express:
|
||||
specifier: ^4.19.2
|
||||
version: 4.22.1
|
||||
form-data:
|
||||
specifier: ^4.0.5
|
||||
version: 4.0.5
|
||||
marked:
|
||||
specifier: ^18.0.5
|
||||
version: 18.0.5
|
||||
devDependencies:
|
||||
'@types/express':
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.25
|
||||
'@types/form-data':
|
||||
specifier: ^2.5.2
|
||||
version: 2.5.2
|
||||
'@types/marked':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
'@types/node':
|
||||
specifier: ^20.14.10
|
||||
version: 20.19.33
|
||||
tsx:
|
||||
specifier: ^4.19.2
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.5.3
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
|
||||
packages:
|
||||
|
||||
'@acemir/cssom@0.9.31':
|
||||
@@ -2754,95 +2865,111 @@ packages:
|
||||
|
||||
'@react-email/body@0.0.11':
|
||||
resolution: {integrity: sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg==}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/button@0.0.19':
|
||||
resolution: {integrity: sha512-HYHrhyVGt7rdM/ls6FuuD6XE7fa7bjZTJqB2byn6/oGsfiEZaogY77OtoLL/mrQHjHjZiJadtAMSik9XLcm7+A==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/code-block@0.0.11':
|
||||
resolution: {integrity: sha512-4D43p+LIMjDzm66gTDrZch0Flkip5je91mAT7iGs6+SbPyalHgIA+lFQoQwhz/VzHHLxuD0LV6gwmU/WUQ2WEg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/code-inline@0.0.5':
|
||||
resolution: {integrity: sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/column@0.0.13':
|
||||
resolution: {integrity: sha512-Lqq17l7ShzJG/d3b1w/+lVO+gp2FM05ZUo/nW0rjxB8xBICXOVv6PqjDnn3FXKssvhO5qAV20lHM6S+spRhEwQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/components@0.0.33':
|
||||
resolution: {integrity: sha512-/GKdT3YijT1iEWPAXF644jr12w5xVgzUr0zlbZGt2KOkGeFHNZUCL5UtRopmnjrH/Fayf8Gjv6q/4E2cZgDtdQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/container@0.0.15':
|
||||
resolution: {integrity: sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/font@0.0.9':
|
||||
resolution: {integrity: sha512-4zjq23oT9APXkerqeslPH3OZWuh5X4crHK6nx82mVHV2SrLba8+8dPEnWbaACWTNjOCbcLIzaC9unk7Wq2MIXw==}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/head@0.0.12':
|
||||
resolution: {integrity: sha512-X2Ii6dDFMF+D4niNwMAHbTkeCjlYYnMsd7edXOsi0JByxt9wNyZ9EnhFiBoQdqkE+SMDcu8TlNNttMrf5sJeMA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/heading@0.0.15':
|
||||
resolution: {integrity: sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/hr@0.0.11':
|
||||
resolution: {integrity: sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/html@0.0.11':
|
||||
resolution: {integrity: sha512-qJhbOQy5VW5qzU74AimjAR9FRFQfrMa7dn4gkEXKMB/S9xZN8e1yC1uA9C15jkXI/PzmJ0muDIWmFwatm5/+VA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/img@0.0.11':
|
||||
resolution: {integrity: sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/link@0.0.12':
|
||||
resolution: {integrity: sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/markdown@0.0.14':
|
||||
resolution: {integrity: sha512-5IsobCyPkb4XwnQO8uFfGcNOxnsg3311GRXhJ3uKv51P7Jxme4ycC/MITnwIZ10w2zx7HIyTiqVzTj4XbuIHbg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/preview@0.0.12':
|
||||
resolution: {integrity: sha512-g/H5fa9PQPDK6WUEG7iTlC19sAktI23qyoiJtMLqQiXFCfWeQMhqjLGKeLSKkfzszqmfJCjZtpSiKtBoOdxp3Q==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
@@ -2856,24 +2983,28 @@ packages:
|
||||
'@react-email/row@0.0.12':
|
||||
resolution: {integrity: sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/section@0.0.16':
|
||||
resolution: {integrity: sha512-FjqF9xQ8FoeUZYKSdt8sMIKvoT9XF8BrzhT3xiFKdEMwYNbsDflcjfErJe3jb7Wj/es/lKTbV5QR1dnLzGpL3w==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/tailwind@1.0.4':
|
||||
resolution: {integrity: sha512-tJdcusncdqgvTUYZIuhNC6LYTfL9vNTSQpwWdTCQhQ1lsrNCEE4OKCSdzSV3S9F32pi0i0xQ+YPJHKIzGjdTSA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/text@0.0.11':
|
||||
resolution: {integrity: sha512-a7nl/2KLpRHOYx75YbYZpWspUbX1DFY7JIZbOv5x0QU8SvwDbJt+Hm01vG34PffFyYvHEXrc6Qnip2RTjljNjg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
@@ -3406,6 +3537,10 @@ packages:
|
||||
'@types/express@4.17.25':
|
||||
resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==}
|
||||
|
||||
'@types/form-data@2.5.2':
|
||||
resolution: {integrity: sha512-tfmcyHn1Pp9YHAO5r40+UuZUPAZbUEgqTel3EuEKpmF9hPkXgR4l41853raliXnb4gwyPNoQOfvgGGlHN5WSog==}
|
||||
deprecated: This is a stub types definition. form-data provides its own type definitions, so you do not need this installed.
|
||||
|
||||
'@types/fs-extra@11.0.4':
|
||||
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
|
||||
|
||||
@@ -3433,6 +3568,10 @@ packages:
|
||||
'@types/long@4.0.2':
|
||||
resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==}
|
||||
|
||||
'@types/marked@6.0.0':
|
||||
resolution: {integrity: sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA==}
|
||||
deprecated: This is a stub types definition. marked provides its own type definitions, so you do not need this installed.
|
||||
|
||||
'@types/mime@1.3.5':
|
||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||
|
||||
@@ -4132,6 +4271,9 @@ packages:
|
||||
bare-url@2.3.2:
|
||||
resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==}
|
||||
|
||||
base-64@1.0.0:
|
||||
resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==}
|
||||
|
||||
base64-arraybuffer@1.0.2:
|
||||
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
@@ -4150,6 +4292,7 @@ packages:
|
||||
basic-ftp@5.2.0:
|
||||
resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
deprecated: Security vulnerability fixed in 5.2.1, please upgrade
|
||||
|
||||
better-path-resolve@1.0.0:
|
||||
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
|
||||
@@ -6303,6 +6446,10 @@ packages:
|
||||
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
mailgun.js@10.4.0:
|
||||
resolution: {integrity: sha512-YrdaZEAJwwjXGBTfZTNQ1LM7tmkdUaz2NpZEu7+zULcG4Wrlhd7cWSNZW0bxT3bP48k5N0mZWz8C2f9gc2+Geg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
map-stream@0.1.0:
|
||||
resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==}
|
||||
|
||||
@@ -6311,6 +6458,11 @@ packages:
|
||||
engines: {node: '>= 18'}
|
||||
hasBin: true
|
||||
|
||||
marked@18.0.5:
|
||||
resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==}
|
||||
engines: {node: '>= 20'}
|
||||
hasBin: true
|
||||
|
||||
marked@7.0.4:
|
||||
resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==}
|
||||
engines: {node: '>= 16'}
|
||||
@@ -8177,6 +8329,9 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
url-join@4.0.1:
|
||||
resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==}
|
||||
|
||||
use-context-selector@2.0.0:
|
||||
resolution: {integrity: sha512-owfuSmUNd3eNp3J9CdDl0kMgfidV+MkDvHPpvthN5ThqM+ibMccNE0k+Iq7TWC6JPFvGZqanqiGCuQx6DyV24g==}
|
||||
peerDependencies:
|
||||
@@ -8217,10 +8372,12 @@ packages:
|
||||
|
||||
uuid@10.0.0:
|
||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
vali-date@1.0.0:
|
||||
@@ -11355,6 +11512,10 @@ snapshots:
|
||||
'@types/qs': 6.14.0
|
||||
'@types/serve-static': 1.15.10
|
||||
|
||||
'@types/form-data@2.5.2':
|
||||
dependencies:
|
||||
form-data: 4.0.5
|
||||
|
||||
'@types/fs-extra@11.0.4':
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.4
|
||||
@@ -11382,6 +11543,10 @@ snapshots:
|
||||
|
||||
'@types/long@4.0.2': {}
|
||||
|
||||
'@types/marked@6.0.0':
|
||||
dependencies:
|
||||
marked: 18.0.5
|
||||
|
||||
'@types/mime@1.3.5': {}
|
||||
|
||||
'@types/ms@2.1.0':
|
||||
@@ -12179,6 +12344,8 @@ snapshots:
|
||||
dependencies:
|
||||
bare-path: 3.0.0
|
||||
|
||||
base-64@1.0.0: {}
|
||||
|
||||
base64-arraybuffer@1.0.2: {}
|
||||
|
||||
base64-js@0.0.8: {}
|
||||
@@ -14662,10 +14829,20 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
mailgun.js@10.4.0:
|
||||
dependencies:
|
||||
axios: 1.13.5
|
||||
base-64: 1.0.0
|
||||
url-join: 4.0.1
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
map-stream@0.1.0: {}
|
||||
|
||||
marked@14.0.0: {}
|
||||
|
||||
marked@18.0.5: {}
|
||||
|
||||
marked@7.0.4: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
@@ -16863,6 +17040,8 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
url-join@4.0.1: {}
|
||||
|
||||
use-context-selector@2.0.0(react@19.2.4)(scheduler@0.25.0):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
@@ -16946,15 +17125,16 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite-node@3.2.4(@types/node@20.19.33)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
|
||||
vite-node@3.2.4(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 5.4.21(@types/node@20.19.33)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
vite: 7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
@@ -16963,16 +17143,19 @@ snapshots:
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-node@3.2.4(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
|
||||
vite-node@3.2.4(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
@@ -16981,6 +17164,8 @@ snapshots:
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite@5.4.21(@types/node@20.19.33)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
|
||||
dependencies:
|
||||
@@ -17024,6 +17209,24 @@ snapshots:
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.2
|
||||
|
||||
vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rollup: 4.57.1
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.10
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.6.1
|
||||
lightningcss: 1.30.2
|
||||
sass: 1.97.3
|
||||
terser: 5.46.0
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.2
|
||||
|
||||
vitest@2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.9
|
||||
@@ -17100,7 +17303,7 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
@@ -17123,7 +17326,7 @@ snapshots:
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 5.4.21(@types/node@20.19.33)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
vite-node: 3.2.4(@types/node@20.19.33)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
vite-node: 3.2.4(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/debug': 4.1.12
|
||||
@@ -17132,6 +17335,7 @@ snapshots:
|
||||
happy-dom: 20.5.3
|
||||
jsdom: 27.4.0(canvas@3.2.1)
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
@@ -17141,8 +17345,10 @@ snapshots:
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
@@ -17165,7 +17371,7 @@ snapshots:
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
vite-node: 3.2.4(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||
vite-node: 3.2.4(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/debug': 4.1.12
|
||||
@@ -17174,6 +17380,7 @@ snapshots:
|
||||
happy-dom: 20.5.3
|
||||
jsdom: 27.4.0(canvas@3.2.1)
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
@@ -17183,6 +17390,8 @@ snapshots:
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user