Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24625c20f2 | |||
| 9a92874011 | |||
| a54c3a784b | |||
| 7b975ed88f | |||
| 1a5a60304f | |||
| 48374528ad | |||
| 541da2db20 | |||
| 39bb862fd6 | |||
| 09a85000bb | |||
| b6e8628970 | |||
| 53ad9f23d8 | |||
| 7a9dd06fc6 | |||
| 985e532c61 | |||
| 04462f21e1 | |||
| 11babf3347 | |||
| 3de92accc4 | |||
| c8961411ec | |||
| 42ea30c11f | |||
| 7b2cc3723a | |||
| 0fc4ddbc28 | |||
| 9f8eab2ad2 | |||
| 69c483ac46 | |||
| ac178ccbb0 | |||
| 3f16828bdf |
5
.env
5
.env
@@ -59,3 +59,8 @@ GLITCHTIP_API_KEY=3d37db66fa1fc3a1cdcd1357728b056225aca950f0163b50c5e922344807ed
|
|||||||
UMAMI_BASE_URL=https://analytics.infra.mintel.me
|
UMAMI_BASE_URL=https://analytics.infra.mintel.me
|
||||||
GLITCHTIP_BASE_URL=https://errors.infra.mintel.me
|
GLITCHTIP_BASE_URL=https://errors.infra.mintel.me
|
||||||
SERPBEAR_BASE_URL=https://seo.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
|
||||||
|
|
||||||
|
|||||||
73
.gitea/workflows/analytics-mailer.yml
Normal file
73
.gitea/workflows/analytics-mailer.yml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
name: Analytics Mailer
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run weekly on Monday at 08:00 UTC
|
||||||
|
- cron: '0 8 * * 1'
|
||||||
|
# Run monthly on the 1st day of the month at 08:30 UTC
|
||||||
|
- cron: '30 8 1 * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
interval:
|
||||||
|
description: 'Interval to run (weekly or monthly)'
|
||||||
|
required: true
|
||||||
|
default: 'weekly'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- weekly
|
||||||
|
- monthly
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
send-reports:
|
||||||
|
name: 📊 Send Analytics 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 }}
|
||||||
|
SMTP_HOST: ${{ secrets.SMTP_HOST }}
|
||||||
|
SMTP_PORT: ${{ secrets.SMTP_PORT }}
|
||||||
|
SMTP_USER: ${{ secrets.SMTP_USER }}
|
||||||
|
SMTP_PASS: ${{ secrets.SMTP_PASS }}
|
||||||
|
SMTP_SENDER: ${{ secrets.SMTP_SENDER }}
|
||||||
|
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
|
||||||
|
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
|
||||||
|
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: Determine Interval
|
||||||
|
id: set-interval
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||||
|
if [ "${{ github.event.schedule }}" = "0 8 * * 1" ]; then
|
||||||
|
echo "interval=weekly" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "interval=monthly" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "interval=${{ github.event.inputs.interval }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run Analytics Mailer
|
||||||
|
run: pnpm --filter @mintel/analytics-mailer run start -- --interval=${{ steps.set-interval.outputs.interval }}
|
||||||
@@ -11,19 +11,19 @@ on:
|
|||||||
description: 'The internal project name for notifications'
|
description: 'The internal project name for notifications'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
# secrets:
|
secrets:
|
||||||
# GOTIFY_URL:
|
GOTIFY_URL:
|
||||||
# required: true
|
required: true
|
||||||
# GOTIFY_TOKEN:
|
GOTIFY_TOKEN:
|
||||||
# required: true
|
required: true
|
||||||
# GATEKEEPER_PASSWORD:
|
GATEKEEPER_PASSWORD:
|
||||||
# required: true
|
required: true
|
||||||
# NPM_TOKEN:
|
NPM_TOKEN:
|
||||||
# required: false
|
required: false
|
||||||
# MINTEL_PRIVATE_TOKEN:
|
MINTEL_PRIVATE_TOKEN:
|
||||||
# required: false
|
required: false
|
||||||
# GITEA_PAT:
|
GITEA_PAT:
|
||||||
# required: false
|
required: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
@@ -57,10 +57,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
rm -rf .next .turbo node_modules || true
|
rm -rf .next .turbo node_modules || true
|
||||||
pnpm install --no-frozen-lockfile --reporter=append-only
|
pnpm install --no-frozen-lockfile --reporter=append-only
|
||||||
- name: 📦 Archive dependencies (Debug)
|
- name: 📦 Archive dependencies
|
||||||
run: |
|
uses: actions/upload-artifact@v3
|
||||||
echo "📦 node_modules size:"
|
with:
|
||||||
du -sh node_modules || echo "No node_modules"
|
name: node_modules
|
||||||
|
path: node_modules
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
static:
|
static:
|
||||||
name: 🔍 Static Analysis
|
name: 🔍 Static Analysis
|
||||||
@@ -80,7 +82,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- name: 📥 Restore dependencies
|
- name: 📥 Restore dependencies
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: node_modules
|
name: node_modules
|
||||||
- name: 🌐 HTML Validation
|
- name: 🌐 HTML Validation
|
||||||
@@ -112,7 +114,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- name: 📥 Restore dependencies
|
- name: 📥 Restore dependencies
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: node_modules
|
name: node_modules
|
||||||
- name: 🔍 Install Chromium
|
- name: 🔍 Install Chromium
|
||||||
@@ -150,7 +152,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- name: 📥 Restore dependencies
|
- name: 📥 Restore dependencies
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: node_modules
|
name: node_modules
|
||||||
- name: 📦 Depcheck
|
- name: 📦 Depcheck
|
||||||
@@ -180,7 +182,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: 📥 Restore dependencies
|
- name: 📥 Restore dependencies
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: node_modules
|
name: node_modules
|
||||||
- name: 🔍 Install Chromium
|
- name: 🔍 Install Chromium
|
||||||
|
|||||||
1
apps/analytics-mailer/.gitignore
vendored
Normal file
1
apps/analytics-mailer/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dry-run-*.html
|
||||||
34
apps/analytics-mailer/config.json
Normal file
34
apps/analytics-mailer/config.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"websiteId": "59a7db94-0100-4c7e-98ef-99f45b17f9c3",
|
||||||
|
"domain": "klz-cables.com",
|
||||||
|
"clientEmail": "klaus.mintel@klz-cables.com, michael.bodemer@klz-cables.com, marc@mintel.me",
|
||||||
|
"clientName": "KLZ Cables",
|
||||||
|
"greeting": "Hallo Klaus, hallo Michael,",
|
||||||
|
"interval": "monthly"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"websiteId": "b9035279-72f1-49bc-909d-b073d184c388",
|
||||||
|
"domain": "mintel.me",
|
||||||
|
"clientEmail": "marc@mintel.me",
|
||||||
|
"clientName": "Mintel.me",
|
||||||
|
"greeting": "Hallo Team Mintel,",
|
||||||
|
"interval": "monthly"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"websiteId": "caa402bc-b2d2-4349-a254-84c0dd2a4460",
|
||||||
|
"domain": "mb-grid-solutions.com",
|
||||||
|
"clientEmail": "marc@mintel.me",
|
||||||
|
"clientName": "MB Grid Solutions",
|
||||||
|
"greeting": "Hallo Marc,",
|
||||||
|
"interval": "monthly"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"websiteId": "d773ea10-a3b3-4ccf-9024-987e14c4d669",
|
||||||
|
"domain": "e-tib.com",
|
||||||
|
"clientEmail": "d.joseph@e-tib.com, marc@mintel.me",
|
||||||
|
"clientName": "E-TIB GmbH",
|
||||||
|
"greeting": "Hallo Danny,",
|
||||||
|
"interval": "monthly"
|
||||||
|
}
|
||||||
|
]
|
||||||
27
apps/analytics-mailer/package.json
Normal file
27
apps/analytics-mailer/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"nodemailer": "^9.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@mintel/eslint-config": "workspace:*",
|
||||||
|
"@mintel/tsconfig": "workspace:*",
|
||||||
|
"@types/node": "^22.19.10",
|
||||||
|
"@types/nodemailer": "^8.0.1",
|
||||||
|
"tsup": "^8.3.5",
|
||||||
|
"tsx": "^4.19.2",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vitest": "^4.1.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
28
apps/analytics-mailer/src/index.test.ts
Normal file
28
apps/analytics-mailer/src/index.test.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { fetchUmamiData } from "./index";
|
||||||
|
|
||||||
|
describe("fetchUmamiData", () => {
|
||||||
|
it("fetches entry and exit pages correctly", async () => {
|
||||||
|
const apiMock = {
|
||||||
|
get: vi.fn().mockImplementation((url, config) => {
|
||||||
|
if (url.includes("/stats")) return Promise.resolve({ data: { visitors: 10, visits: 10, pageviews: 10, bounces: 5, totaltime: 100 } });
|
||||||
|
if (url.includes("/pageviews")) return Promise.resolve({ data: { pageviews: [] } });
|
||||||
|
if (url.includes("/metrics")) {
|
||||||
|
if (config.params.type === "path") return Promise.resolve({ data: [{ x: "/path", y: 5 }] });
|
||||||
|
if (config.params.type === "referrer") return Promise.resolve({ data: [{ x: "google", y: 3 }] });
|
||||||
|
if (config.params.type === "entry") return Promise.resolve({ data: [{ x: "/entry", y: 4 }] });
|
||||||
|
if (config.params.type === "exit") return Promise.resolve({ data: [{ x: "/exit", y: 2 }] });
|
||||||
|
}
|
||||||
|
return Promise.resolve({ data: {} });
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await fetchUmamiData(apiMock, "test-id", 0, 1000, "Test");
|
||||||
|
|
||||||
|
// We expect the new entry and exit pages to be parsed and returned correctly
|
||||||
|
expect(result.topEntryPages).toBeDefined();
|
||||||
|
expect(result.topEntryPages[0]).toEqual({ url: "/entry", visitors: 4 });
|
||||||
|
expect(result.topExitPages).toBeDefined();
|
||||||
|
expect(result.topExitPages[0]).toEqual({ url: "/exit", visitors: 2 });
|
||||||
|
});
|
||||||
|
});
|
||||||
216
apps/analytics-mailer/src/index.ts
Normal file
216
apps/analytics-mailer/src/index.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import axios from "axios";
|
||||||
|
import { render } from "@mintel/mail";
|
||||||
|
import { AnalyticsTemplate } from "@mintel/mail";
|
||||||
|
import { getLast7Days, getThisMonth, getThisYear } from "./utils/time-calculator";
|
||||||
|
import nodemailer from "nodemailer";
|
||||||
|
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 SMTP_HOST = process.env.SMTP_HOST || "mail.infra.mintel.me";
|
||||||
|
const SMTP_PORT = parseInt(process.env.SMTP_PORT || "587", 10);
|
||||||
|
const SMTP_USER = process.env.SMTP_USER;
|
||||||
|
const SMTP_PASS = process.env.SMTP_PASS;
|
||||||
|
const SMTP_SENDER = process.env.SMTP_SENDER || "reports@mintel.me";
|
||||||
|
|
||||||
|
const GOTIFY_URL = process.env.GOTIFY_URL;
|
||||||
|
const GOTIFY_TOKEN = process.env.GOTIFY_TOKEN;
|
||||||
|
|
||||||
|
interface ConfigClient {
|
||||||
|
websiteId: string;
|
||||||
|
domain: string;
|
||||||
|
clientEmail: string;
|
||||||
|
clientName: string;
|
||||||
|
greeting?: string;
|
||||||
|
interval?: "weekly" | "monthly";
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
import https from "https";
|
||||||
|
|
||||||
|
export async function fetchUmamiData(api: any, websiteId: string, startAt: number, endAt: number, label: string) {
|
||||||
|
let unit = "day";
|
||||||
|
if (label === "Dieses Jahr") unit = "month";
|
||||||
|
|
||||||
|
const [statsRes, pagesRes, refRes, viewsRes, entryRes, exitRes] = await Promise.all([
|
||||||
|
api.get(`/websites/${websiteId}/stats`, { params: { startAt, endAt } }),
|
||||||
|
api.get(`/websites/${websiteId}/metrics`, { params: { type: "path", limit: 5, startAt, endAt } }),
|
||||||
|
api.get(`/websites/${websiteId}/metrics`, { params: { type: "referrer", limit: 5, startAt, endAt } }),
|
||||||
|
api.get(`/websites/${websiteId}/pageviews`, { params: { startAt, endAt, unit, timezone: "Europe/Berlin" } }).catch(() => ({ data: { pageviews: [] } })),
|
||||||
|
api.get(`/websites/${websiteId}/metrics`, { params: { type: "entry", limit: 5, startAt, endAt } }),
|
||||||
|
api.get(`/websites/${websiteId}/metrics`, { params: { type: "exit", limit: 5, startAt, endAt } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const stats = statsRes.data;
|
||||||
|
const topPages = (pagesRes.data || []).map((p: any) => ({ url: p.x, views: p.y || 0 }));
|
||||||
|
const topReferrers = (refRes.data || []).map((r: any) => ({ url: r.x, visitors: r.y || 0 }));
|
||||||
|
const topEntryPages = (entryRes.data || []).map((p: any) => ({ url: p.x, visitors: p.y || 0 }));
|
||||||
|
const topExitPages = (exitRes.data || []).map((p: any) => ({ url: p.x, visitors: p.y || 0 }));
|
||||||
|
|
||||||
|
const visitors = typeof stats.visitors === "object" ? stats.visitors?.value || 0 : stats.visitors || 0;
|
||||||
|
const visits = typeof stats.visits === "object" ? stats.visits?.value || 0 : stats.visits || 0;
|
||||||
|
const views = typeof stats.pageviews === "object" ? stats.pageviews?.value || 0 : stats.pageviews || 0;
|
||||||
|
const bounces = typeof stats.bounces === "object" ? stats.bounces?.value || 0 : stats.bounces || 0;
|
||||||
|
const totaltime = typeof stats.totaltime === "object" ? stats.totaltime?.value || 0 : stats.totaltime || 0;
|
||||||
|
|
||||||
|
const bounceRate = visits > 0 ? Math.round((bounces / visits) * 100) : 0;
|
||||||
|
|
||||||
|
let visitDuration = "0s";
|
||||||
|
if (visits > 0) {
|
||||||
|
const avgSeconds = Math.round(totaltime / visits);
|
||||||
|
if (avgSeconds < 60) {
|
||||||
|
visitDuration = `${avgSeconds}s`;
|
||||||
|
} else {
|
||||||
|
visitDuration = `${Math.floor(avgSeconds / 60)}m ${avgSeconds % 60}s`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartData = (viewsRes.data?.pageviews || []).map((pv: any) => ({
|
||||||
|
label: pv.x,
|
||||||
|
value: pv.y || 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
visitors,
|
||||||
|
visits,
|
||||||
|
views,
|
||||||
|
bounceRate,
|
||||||
|
visitDuration,
|
||||||
|
topPages,
|
||||||
|
topReferrers,
|
||||||
|
topEntryPages,
|
||||||
|
topExitPages,
|
||||||
|
chartData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const isDryRun = process.argv.includes("--dry-run");
|
||||||
|
const intervalArg = process.argv.find(a => a.startsWith("--interval="));
|
||||||
|
const runInterval = intervalArg ? intervalArg.split("=")[1] : "monthly";
|
||||||
|
|
||||||
|
if (runInterval !== "weekly" && runInterval !== "monthly") {
|
||||||
|
throw new Error("Invalid interval. Use --interval=weekly or --interval=monthly");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
||||||
|
const api = axios.create({ baseURL: `${UMAMI_BASE_URL}/api`, headers, httpsAgent });
|
||||||
|
|
||||||
|
// Setup SMTP Transporter (Nodemailer)
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: SMTP_HOST,
|
||||||
|
port: SMTP_PORT,
|
||||||
|
secure: SMTP_PORT === 465,
|
||||||
|
auth: SMTP_USER && SMTP_PASS ? {
|
||||||
|
user: SMTP_USER,
|
||||||
|
pass: SMTP_PASS,
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeRanges = runInterval === "weekly"
|
||||||
|
? [getLast7Days(), getThisMonth(), getThisYear()]
|
||||||
|
: [getThisMonth(), getThisYear()];
|
||||||
|
|
||||||
|
console.log(`Generating reports...`);
|
||||||
|
|
||||||
|
for (const client of clients) {
|
||||||
|
if (client.websiteId === "replace-with-umami-website-id") continue;
|
||||||
|
const clientInterval = client.interval || "monthly";
|
||||||
|
if (clientInterval !== runInterval) {
|
||||||
|
console.log(`Skipping ${client.domain} (runs ${clientInterval}, currently executing ${runInterval})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Processing ${client.domain} (${client.clientName})...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const periods = await Promise.all(
|
||||||
|
timeRanges.map((tr) => fetchUmamiData(api, client.websiteId, tr.startAt, tr.endAt, tr.label))
|
||||||
|
);
|
||||||
|
|
||||||
|
const html = await render(AnalyticsTemplate({
|
||||||
|
greeting: client.greeting || `Hallo ${client.clientName},`,
|
||||||
|
domain: client.domain,
|
||||||
|
periods,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const subject = `Performance-Report - ${client.domain}`;
|
||||||
|
|
||||||
|
const recipients = client.clientEmail.split(',').map(e => e.trim()).filter(e => e.length > 0);
|
||||||
|
|
||||||
|
if (isDryRun) {
|
||||||
|
for (const recipient of recipients) {
|
||||||
|
console.log(`[DRY-RUN] Would send individual email to: ${recipient} (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 (!SMTP_HOST) {
|
||||||
|
throw new Error("SMTP_HOST is missing for live run.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const recipient of recipients) {
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: SMTP_SENDER,
|
||||||
|
to: recipient,
|
||||||
|
subject,
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
console.log(`Successfully sent to ${recipient}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
const errorMsg = e.response?.data || e.message;
|
||||||
|
console.error(`Failed to process client ${client.domain}:`, errorMsg);
|
||||||
|
|
||||||
|
if (GOTIFY_URL && GOTIFY_TOKEN) {
|
||||||
|
try {
|
||||||
|
await axios.post(`${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`, {
|
||||||
|
title: `❌ Analytics Mailer Error (${client.domain})`,
|
||||||
|
message: `Failed to generate or send report for ${client.domain}.\n\nError:\n${errorMsg}`,
|
||||||
|
priority: 8
|
||||||
|
});
|
||||||
|
} catch (gotifyErr: any) {
|
||||||
|
console.error("Failed to push to Gotify:", gotifyErr.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Done.");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error("Fatal error:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
19
apps/analytics-mailer/src/utils/time-calculator.ts
Normal file
19
apps/analytics-mailer/src/utils/time-calculator.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export function getLast7Days() {
|
||||||
|
const endAt = Date.now();
|
||||||
|
const startAt = endAt - 7 * 24 * 60 * 60 * 1000;
|
||||||
|
return { label: "Letzte 7 Tage", startAt, endAt };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThisMonth() {
|
||||||
|
const now = new Date();
|
||||||
|
const startAt = new Date(now.getFullYear(), now.getMonth(), 1).getTime();
|
||||||
|
const endAt = Date.now();
|
||||||
|
return { label: "Dieser Monat", startAt, endAt };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThisYear() {
|
||||||
|
const now = new Date();
|
||||||
|
const startAt = new Date(now.getFullYear(), 0, 1).getTime();
|
||||||
|
const endAt = Date.now();
|
||||||
|
return { label: "Dieses Jahr", startAt, endAt };
|
||||||
|
}
|
||||||
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/**/*"]
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- "3001:3001"
|
- "3011:3001"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- mcp-network
|
- mcp-network
|
||||||
@@ -85,6 +85,30 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- mcp-network
|
- 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:
|
networks:
|
||||||
mcp-network:
|
mcp-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
"@next/eslint-plugin-next": "16.1.6",
|
"@next/eslint-plugin-next": "16.1.6",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/node": "^20.17.16",
|
"@types/node": "^20.19.33",
|
||||||
"@types/react": "^19.2.10",
|
"@types/react": "^19.2.10",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ FROM node:20-alpine AS builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json pnpm-lock.yaml* ./
|
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 tsconfig.json ./
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
FROM node:20-bookworm-slim AS builder
|
FROM node:20-bookworm-slim AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json ./
|
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 tsconfig.json ./
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
RUN pnpm build
|
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
|
# 3. Global Docker Pruning
|
||||||
echo "🧹 Pruning Docker resources..."
|
echo "🧹 Pruning Docker resources..."
|
||||||
docker system prune -af --filter "until=24h"
|
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
|
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!"
|
echo "✅ Optimization complete!"
|
||||||
df -h /mnt/HC_Volume_104796416
|
df -h /mnt/HC_Volume_104796416
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Configuration
|
# Find registry data dynamically (volume ID might change)
|
||||||
REGISTRY_DATA="/mnt/HC_Volume_104575103/registry-data/docker/registry/v2"
|
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
|
KEEP_TAGS=3
|
||||||
|
|
||||||
echo "🏥 Starting Aggressive Registry & Docker Maintenance..."
|
echo "🏥 Starting Aggressive Registry & Docker Maintenance..."
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
FROM node:20-bookworm-slim AS builder
|
FROM node:20-bookworm-slim AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json ./
|
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 tsconfig.json ./
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ export * from "./templates/ConfirmationMessage";
|
|||||||
export * from "./templates/FollowUpTemplate";
|
export * from "./templates/FollowUpTemplate";
|
||||||
export * from "./templates/ProjectEstimateTemplate";
|
export * from "./templates/ProjectEstimateTemplate";
|
||||||
export * from "./templates/SiteAuditTemplate";
|
export * from "./templates/SiteAuditTemplate";
|
||||||
|
export * from "./templates/AnalyticsTemplate";
|
||||||
|
|||||||
197
packages/mail/src/layouts/GatekeeperLayout.tsx
Normal file
197
packages/mail/src/layouts/GatekeeperLayout.tsx
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Html,
|
||||||
|
Preview,
|
||||||
|
Section,
|
||||||
|
Text,
|
||||||
|
Img,
|
||||||
|
} from "@react-email/components";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface GatekeeperLayoutProps {
|
||||||
|
preview: string;
|
||||||
|
projectName?: string;
|
||||||
|
subTitle?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GatekeeperLayout = ({ preview, projectName = "MINTEL", subTitle = "INFRASTRUCTURE", children }: GatekeeperLayoutProps) => {
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>{preview}</Preview>
|
||||||
|
<Body style={main}>
|
||||||
|
<Container style={container}>
|
||||||
|
|
||||||
|
{/* Top Icon Box */}
|
||||||
|
<Section style={iconBoxSection}>
|
||||||
|
<div style={iconBox}>
|
||||||
|
<Img
|
||||||
|
src="https://mintel.me/favicon-32x32.png"
|
||||||
|
width={28}
|
||||||
|
height={28}
|
||||||
|
alt="Mintel"
|
||||||
|
style={iconImg}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Title Area */}
|
||||||
|
<Section style={titleSection}>
|
||||||
|
<Text style={gatekeeperTitle}>
|
||||||
|
{projectName} <span style={{ color: "rgba(0,0,0,0.3)" }}>GATEKEEPER</span>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<table align="center" width="100%" border={0} cellPadding={0} cellSpacing={0} role="presentation">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td width="30%" align="right"><div style={lineGradientRight} /></td>
|
||||||
|
<td width="40%" align="center">
|
||||||
|
<Text style={gatekeeperSub}>
|
||||||
|
{subTitle}
|
||||||
|
</Text>
|
||||||
|
</td>
|
||||||
|
<td width="30%" align="left"><div style={lineGradientLeft} /></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Main Content Area */}
|
||||||
|
<Section style={contentSection}>
|
||||||
|
{children}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Footer Area */}
|
||||||
|
<Section style={footerSection}>
|
||||||
|
<div style={lineGradientCenter} />
|
||||||
|
<div style={{ marginTop: "24px" }}>
|
||||||
|
<Img
|
||||||
|
src="https://mintel.me/favicon-32x32.png"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
alt={projectName}
|
||||||
|
style={{ opacity: 0.3, margin: "0 auto", display: "block" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Text style={footerText}>
|
||||||
|
© {new Date().getFullYear()} MINTEL
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const main = {
|
||||||
|
backgroundColor: "#f5f5f7",
|
||||||
|
color: "#111111",
|
||||||
|
fontFamily:
|
||||||
|
'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
|
||||||
|
WebkitFontSmoothing: "antialiased" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const container = {
|
||||||
|
margin: "0 auto",
|
||||||
|
padding: "40px 20px 80px",
|
||||||
|
maxWidth: "420px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconBoxSection = {
|
||||||
|
textAlign: "center" as const,
|
||||||
|
marginBottom: "40px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconBox = {
|
||||||
|
width: "56px",
|
||||||
|
height: "56px",
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
borderRadius: "16px",
|
||||||
|
margin: "0 auto",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
border: "1px solid rgba(0,0,0,0.06)",
|
||||||
|
boxShadow: "0 10px 15px -3px rgba(0,0,0,0.06), 0 4px 6px -2px rgba(0,0,0,0.03)",
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconImg = {
|
||||||
|
margin: "14px auto",
|
||||||
|
display: "block",
|
||||||
|
opacity: 0.8,
|
||||||
|
filter: "grayscale(100%) brightness(0)",
|
||||||
|
};
|
||||||
|
|
||||||
|
const titleSection = {
|
||||||
|
textAlign: "center" as const,
|
||||||
|
marginBottom: "32px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const gatekeeperTitle = {
|
||||||
|
fontSize: "11px",
|
||||||
|
fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif',
|
||||||
|
fontWeight: "bold",
|
||||||
|
textTransform: "uppercase" as const,
|
||||||
|
letterSpacing: "0.5em",
|
||||||
|
color: "rgba(0,0,0,0.8)",
|
||||||
|
margin: "0 0 12px 0",
|
||||||
|
};
|
||||||
|
|
||||||
|
const gatekeeperSub = {
|
||||||
|
fontSize: "8px",
|
||||||
|
fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif',
|
||||||
|
textTransform: "uppercase" as const,
|
||||||
|
letterSpacing: "0.35em",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "rgba(0,0,0,0.3)",
|
||||||
|
margin: "0",
|
||||||
|
};
|
||||||
|
|
||||||
|
const lineGradientRight = {
|
||||||
|
height: "1px",
|
||||||
|
width: "32px",
|
||||||
|
backgroundColor: "rgba(0,0,0,0.1)",
|
||||||
|
marginLeft: "auto",
|
||||||
|
};
|
||||||
|
|
||||||
|
const lineGradientLeft = {
|
||||||
|
height: "1px",
|
||||||
|
width: "32px",
|
||||||
|
backgroundColor: "rgba(0,0,0,0.1)",
|
||||||
|
marginRight: "auto",
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentSection = {
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
padding: "32px",
|
||||||
|
borderRadius: "24px",
|
||||||
|
border: "1px solid rgba(0,0,0,0.06)",
|
||||||
|
boxShadow: "0 4px 6px -1px rgba(0,0,0,0.03)",
|
||||||
|
marginBottom: "32px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const footerSection = {
|
||||||
|
textAlign: "center" as const,
|
||||||
|
paddingTop: "24px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const lineGradientCenter = {
|
||||||
|
height: "1px",
|
||||||
|
width: "64px",
|
||||||
|
backgroundColor: "rgba(0,0,0,0.1)",
|
||||||
|
margin: "0 auto",
|
||||||
|
};
|
||||||
|
|
||||||
|
const footerText = {
|
||||||
|
fontSize: "7px",
|
||||||
|
fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "rgba(0,0,0,0.25)",
|
||||||
|
textTransform: "uppercase" as const,
|
||||||
|
letterSpacing: "0.5em",
|
||||||
|
marginTop: "20px",
|
||||||
|
};
|
||||||
41
packages/mail/src/templates/AnalyticsTemplate.test.tsx
Normal file
41
packages/mail/src/templates/AnalyticsTemplate.test.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { render } from "@react-email/components";
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { AnalyticsTemplate, PeriodData } from "./AnalyticsTemplate";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
describe("AnalyticsTemplate", () => {
|
||||||
|
it("renders entry and exit pages correctly", async () => {
|
||||||
|
const period: PeriodData = {
|
||||||
|
label: "Test Period",
|
||||||
|
visitors: 100,
|
||||||
|
visits: 120,
|
||||||
|
views: 500,
|
||||||
|
bounceRate: 50,
|
||||||
|
visitDuration: "2m",
|
||||||
|
topPages: [{ url: "/home", views: 50 }],
|
||||||
|
topReferrers: [{ url: "google.com", visitors: 30 }],
|
||||||
|
// These fields will cause a TS error because they don't exist yet,
|
||||||
|
// but we force it to see it fail.
|
||||||
|
topEntryPages: [{ url: "/entry", visitors: 40 }],
|
||||||
|
topExitPages: [{ url: "/exit", visitors: 20 }],
|
||||||
|
chartData: [],
|
||||||
|
} as any; // Cast as any to bypass TS compilation before we update the type
|
||||||
|
|
||||||
|
const html = await render(
|
||||||
|
<AnalyticsTemplate
|
||||||
|
greeting="Hallo Kunde,"
|
||||||
|
domain="example.com"
|
||||||
|
periods={[period]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert that the new sections for entry and exit pages exist.
|
||||||
|
expect(html).toContain("Häufigste Einstiege");
|
||||||
|
expect(html).toContain("Häufigste Ausstiege");
|
||||||
|
expect(html).toContain("/entry");
|
||||||
|
expect(html).toContain("/exit");
|
||||||
|
|
||||||
|
// Assert that the badges have updated line heights
|
||||||
|
expect(html).toContain('line-height:1.5');
|
||||||
|
});
|
||||||
|
});
|
||||||
378
packages/mail/src/templates/AnalyticsTemplate.tsx
Normal file
378
packages/mail/src/templates/AnalyticsTemplate.tsx
Normal file
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
|||||||
FROM node:20-bookworm-slim AS builder
|
FROM node:20-bookworm-slim AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json ./
|
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 \
|
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; \
|
echo "module.exports = {};" > "$dir/lib/index.js" || true; \
|
||||||
done
|
done
|
||||||
|
|||||||
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
|
FROM node:20-bookworm-slim AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json ./
|
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 tsconfig.json ./
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
FROM node:20-bookworm-slim AS builder
|
FROM node:20-bookworm-slim AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json ./
|
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 tsconfig.json ./
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
RUN pnpm build
|
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/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
392
pnpm-lock.yaml
generated
392
pnpm-lock.yaml
generated
@@ -53,7 +53,7 @@ importers:
|
|||||||
specifier: ^16.3.2
|
specifier: ^16.3.2
|
||||||
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.17.16
|
specifier: ^20.19.33
|
||||||
version: 20.19.33
|
version: 20.19.33
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19.2.10
|
specifier: ^19.2.10
|
||||||
@@ -104,6 +104,46 @@ importers:
|
|||||||
specifier: ^4.0.18
|
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)
|
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
|
||||||
|
nodemailer:
|
||||||
|
specifier: ^9.0.3
|
||||||
|
version: 9.0.3
|
||||||
|
devDependencies:
|
||||||
|
'@mintel/eslint-config':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/eslint-config
|
||||||
|
'@mintel/tsconfig':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/tsconfig
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^22.19.10
|
||||||
|
version: 22.19.10
|
||||||
|
'@types/nodemailer':
|
||||||
|
specifier: ^8.0.1
|
||||||
|
version: 8.0.1
|
||||||
|
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
|
||||||
|
vitest:
|
||||||
|
specifier: ^4.1.10
|
||||||
|
version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(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))
|
||||||
|
|
||||||
apps/sample-website:
|
apps/sample-website:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mintel/next-observability':
|
'@mintel/next-observability':
|
||||||
@@ -235,7 +275,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.0.5
|
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(vitest@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:
|
packages/content-engine:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -537,7 +577,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.0.4
|
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(vitest@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:
|
packages/meme-generator:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -596,7 +636,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.1.3
|
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)
|
version: 2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@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/next-config:
|
packages/next-config:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -752,7 +792,38 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.0
|
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)
|
version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@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(vitest@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:
|
packages/page-audit:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -897,7 +968,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.0.5
|
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(vitest@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:
|
packages/serpbear-mcp:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -979,6 +1050,49 @@ importers:
|
|||||||
specifier: ^5.5.3
|
specifier: ^5.5.3
|
||||||
version: 5.9.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(vitest@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:
|
packages:
|
||||||
|
|
||||||
'@acemir/cssom@0.9.31':
|
'@acemir/cssom@0.9.31':
|
||||||
@@ -2754,95 +2868,111 @@ packages:
|
|||||||
|
|
||||||
'@react-email/body@0.0.11':
|
'@react-email/body@0.0.11':
|
||||||
resolution: {integrity: sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg==}
|
resolution: {integrity: sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg==}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/button@0.0.19':
|
'@react-email/button@0.0.19':
|
||||||
resolution: {integrity: sha512-HYHrhyVGt7rdM/ls6FuuD6XE7fa7bjZTJqB2byn6/oGsfiEZaogY77OtoLL/mrQHjHjZiJadtAMSik9XLcm7+A==}
|
resolution: {integrity: sha512-HYHrhyVGt7rdM/ls6FuuD6XE7fa7bjZTJqB2byn6/oGsfiEZaogY77OtoLL/mrQHjHjZiJadtAMSik9XLcm7+A==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/code-block@0.0.11':
|
'@react-email/code-block@0.0.11':
|
||||||
resolution: {integrity: sha512-4D43p+LIMjDzm66gTDrZch0Flkip5je91mAT7iGs6+SbPyalHgIA+lFQoQwhz/VzHHLxuD0LV6gwmU/WUQ2WEg==}
|
resolution: {integrity: sha512-4D43p+LIMjDzm66gTDrZch0Flkip5je91mAT7iGs6+SbPyalHgIA+lFQoQwhz/VzHHLxuD0LV6gwmU/WUQ2WEg==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/code-inline@0.0.5':
|
'@react-email/code-inline@0.0.5':
|
||||||
resolution: {integrity: sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==}
|
resolution: {integrity: sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/column@0.0.13':
|
'@react-email/column@0.0.13':
|
||||||
resolution: {integrity: sha512-Lqq17l7ShzJG/d3b1w/+lVO+gp2FM05ZUo/nW0rjxB8xBICXOVv6PqjDnn3FXKssvhO5qAV20lHM6S+spRhEwQ==}
|
resolution: {integrity: sha512-Lqq17l7ShzJG/d3b1w/+lVO+gp2FM05ZUo/nW0rjxB8xBICXOVv6PqjDnn3FXKssvhO5qAV20lHM6S+spRhEwQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/components@0.0.33':
|
'@react-email/components@0.0.33':
|
||||||
resolution: {integrity: sha512-/GKdT3YijT1iEWPAXF644jr12w5xVgzUr0zlbZGt2KOkGeFHNZUCL5UtRopmnjrH/Fayf8Gjv6q/4E2cZgDtdQ==}
|
resolution: {integrity: sha512-/GKdT3YijT1iEWPAXF644jr12w5xVgzUr0zlbZGt2KOkGeFHNZUCL5UtRopmnjrH/Fayf8Gjv6q/4E2cZgDtdQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/container@0.0.15':
|
'@react-email/container@0.0.15':
|
||||||
resolution: {integrity: sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==}
|
resolution: {integrity: sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/font@0.0.9':
|
'@react-email/font@0.0.9':
|
||||||
resolution: {integrity: sha512-4zjq23oT9APXkerqeslPH3OZWuh5X4crHK6nx82mVHV2SrLba8+8dPEnWbaACWTNjOCbcLIzaC9unk7Wq2MIXw==}
|
resolution: {integrity: sha512-4zjq23oT9APXkerqeslPH3OZWuh5X4crHK6nx82mVHV2SrLba8+8dPEnWbaACWTNjOCbcLIzaC9unk7Wq2MIXw==}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/head@0.0.12':
|
'@react-email/head@0.0.12':
|
||||||
resolution: {integrity: sha512-X2Ii6dDFMF+D4niNwMAHbTkeCjlYYnMsd7edXOsi0JByxt9wNyZ9EnhFiBoQdqkE+SMDcu8TlNNttMrf5sJeMA==}
|
resolution: {integrity: sha512-X2Ii6dDFMF+D4niNwMAHbTkeCjlYYnMsd7edXOsi0JByxt9wNyZ9EnhFiBoQdqkE+SMDcu8TlNNttMrf5sJeMA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/heading@0.0.15':
|
'@react-email/heading@0.0.15':
|
||||||
resolution: {integrity: sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==}
|
resolution: {integrity: sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/hr@0.0.11':
|
'@react-email/hr@0.0.11':
|
||||||
resolution: {integrity: sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==}
|
resolution: {integrity: sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/html@0.0.11':
|
'@react-email/html@0.0.11':
|
||||||
resolution: {integrity: sha512-qJhbOQy5VW5qzU74AimjAR9FRFQfrMa7dn4gkEXKMB/S9xZN8e1yC1uA9C15jkXI/PzmJ0muDIWmFwatm5/+VA==}
|
resolution: {integrity: sha512-qJhbOQy5VW5qzU74AimjAR9FRFQfrMa7dn4gkEXKMB/S9xZN8e1yC1uA9C15jkXI/PzmJ0muDIWmFwatm5/+VA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/img@0.0.11':
|
'@react-email/img@0.0.11':
|
||||||
resolution: {integrity: sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==}
|
resolution: {integrity: sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/link@0.0.12':
|
'@react-email/link@0.0.12':
|
||||||
resolution: {integrity: sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==}
|
resolution: {integrity: sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/markdown@0.0.14':
|
'@react-email/markdown@0.0.14':
|
||||||
resolution: {integrity: sha512-5IsobCyPkb4XwnQO8uFfGcNOxnsg3311GRXhJ3uKv51P7Jxme4ycC/MITnwIZ10w2zx7HIyTiqVzTj4XbuIHbg==}
|
resolution: {integrity: sha512-5IsobCyPkb4XwnQO8uFfGcNOxnsg3311GRXhJ3uKv51P7Jxme4ycC/MITnwIZ10w2zx7HIyTiqVzTj4XbuIHbg==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/preview@0.0.12':
|
'@react-email/preview@0.0.12':
|
||||||
resolution: {integrity: sha512-g/H5fa9PQPDK6WUEG7iTlC19sAktI23qyoiJtMLqQiXFCfWeQMhqjLGKeLSKkfzszqmfJCjZtpSiKtBoOdxp3Q==}
|
resolution: {integrity: sha512-g/H5fa9PQPDK6WUEG7iTlC19sAktI23qyoiJtMLqQiXFCfWeQMhqjLGKeLSKkfzszqmfJCjZtpSiKtBoOdxp3Q==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
@@ -2856,24 +2986,28 @@ packages:
|
|||||||
'@react-email/row@0.0.12':
|
'@react-email/row@0.0.12':
|
||||||
resolution: {integrity: sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ==}
|
resolution: {integrity: sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/section@0.0.16':
|
'@react-email/section@0.0.16':
|
||||||
resolution: {integrity: sha512-FjqF9xQ8FoeUZYKSdt8sMIKvoT9XF8BrzhT3xiFKdEMwYNbsDflcjfErJe3jb7Wj/es/lKTbV5QR1dnLzGpL3w==}
|
resolution: {integrity: sha512-FjqF9xQ8FoeUZYKSdt8sMIKvoT9XF8BrzhT3xiFKdEMwYNbsDflcjfErJe3jb7Wj/es/lKTbV5QR1dnLzGpL3w==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/tailwind@1.0.4':
|
'@react-email/tailwind@1.0.4':
|
||||||
resolution: {integrity: sha512-tJdcusncdqgvTUYZIuhNC6LYTfL9vNTSQpwWdTCQhQ1lsrNCEE4OKCSdzSV3S9F32pi0i0xQ+YPJHKIzGjdTSA==}
|
resolution: {integrity: sha512-tJdcusncdqgvTUYZIuhNC6LYTfL9vNTSQpwWdTCQhQ1lsrNCEE4OKCSdzSV3S9F32pi0i0xQ+YPJHKIzGjdTSA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
'@react-email/text@0.0.11':
|
'@react-email/text@0.0.11':
|
||||||
resolution: {integrity: sha512-a7nl/2KLpRHOYx75YbYZpWspUbX1DFY7JIZbOv5x0QU8SvwDbJt+Hm01vG34PffFyYvHEXrc6Qnip2RTjljNjg==}
|
resolution: {integrity: sha512-a7nl/2KLpRHOYx75YbYZpWspUbX1DFY7JIZbOv5x0QU8SvwDbJt+Hm01vG34PffFyYvHEXrc6Qnip2RTjljNjg==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
|
||||||
@@ -3406,6 +3540,10 @@ packages:
|
|||||||
'@types/express@4.17.25':
|
'@types/express@4.17.25':
|
||||||
resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==}
|
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':
|
'@types/fs-extra@11.0.4':
|
||||||
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
|
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
|
||||||
|
|
||||||
@@ -3433,6 +3571,10 @@ packages:
|
|||||||
'@types/long@4.0.2':
|
'@types/long@4.0.2':
|
||||||
resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==}
|
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':
|
'@types/mime@1.3.5':
|
||||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||||
|
|
||||||
@@ -3457,6 +3599,9 @@ packages:
|
|||||||
'@types/node@22.19.10':
|
'@types/node@22.19.10':
|
||||||
resolution: {integrity: sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw==}
|
resolution: {integrity: sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw==}
|
||||||
|
|
||||||
|
'@types/nodemailer@8.0.1':
|
||||||
|
resolution: {integrity: sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==}
|
||||||
|
|
||||||
'@types/parse-json@4.0.2':
|
'@types/parse-json@4.0.2':
|
||||||
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
|
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
|
||||||
|
|
||||||
@@ -3688,6 +3833,9 @@ packages:
|
|||||||
'@vitest/expect@4.0.18':
|
'@vitest/expect@4.0.18':
|
||||||
resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==}
|
resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==}
|
||||||
|
|
||||||
|
'@vitest/expect@4.1.10':
|
||||||
|
resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==}
|
||||||
|
|
||||||
'@vitest/mocker@2.1.9':
|
'@vitest/mocker@2.1.9':
|
||||||
resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
|
resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3721,6 +3869,17 @@ packages:
|
|||||||
vite:
|
vite:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@vitest/mocker@4.1.10':
|
||||||
|
resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==}
|
||||||
|
peerDependencies:
|
||||||
|
msw: ^2.4.9
|
||||||
|
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
msw:
|
||||||
|
optional: true
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vitest/pretty-format@2.1.9':
|
'@vitest/pretty-format@2.1.9':
|
||||||
resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==}
|
resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==}
|
||||||
|
|
||||||
@@ -3730,6 +3889,9 @@ packages:
|
|||||||
'@vitest/pretty-format@4.0.18':
|
'@vitest/pretty-format@4.0.18':
|
||||||
resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==}
|
resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==}
|
||||||
|
|
||||||
|
'@vitest/pretty-format@4.1.10':
|
||||||
|
resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==}
|
||||||
|
|
||||||
'@vitest/runner@2.1.9':
|
'@vitest/runner@2.1.9':
|
||||||
resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==}
|
resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==}
|
||||||
|
|
||||||
@@ -3739,6 +3901,9 @@ packages:
|
|||||||
'@vitest/runner@4.0.18':
|
'@vitest/runner@4.0.18':
|
||||||
resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==}
|
resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==}
|
||||||
|
|
||||||
|
'@vitest/runner@4.1.10':
|
||||||
|
resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==}
|
||||||
|
|
||||||
'@vitest/snapshot@2.1.9':
|
'@vitest/snapshot@2.1.9':
|
||||||
resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
|
resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
|
||||||
|
|
||||||
@@ -3748,6 +3913,9 @@ packages:
|
|||||||
'@vitest/snapshot@4.0.18':
|
'@vitest/snapshot@4.0.18':
|
||||||
resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==}
|
resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==}
|
||||||
|
|
||||||
|
'@vitest/snapshot@4.1.10':
|
||||||
|
resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==}
|
||||||
|
|
||||||
'@vitest/spy@2.1.9':
|
'@vitest/spy@2.1.9':
|
||||||
resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
|
resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
|
||||||
|
|
||||||
@@ -3757,6 +3925,9 @@ packages:
|
|||||||
'@vitest/spy@4.0.18':
|
'@vitest/spy@4.0.18':
|
||||||
resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==}
|
resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==}
|
||||||
|
|
||||||
|
'@vitest/spy@4.1.10':
|
||||||
|
resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==}
|
||||||
|
|
||||||
'@vitest/ui@4.0.18':
|
'@vitest/ui@4.0.18':
|
||||||
resolution: {integrity: sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==}
|
resolution: {integrity: sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3771,6 +3942,9 @@ packages:
|
|||||||
'@vitest/utils@4.0.18':
|
'@vitest/utils@4.0.18':
|
||||||
resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==}
|
resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==}
|
||||||
|
|
||||||
|
'@vitest/utils@4.1.10':
|
||||||
|
resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==}
|
||||||
|
|
||||||
'@vladfrangu/async_event_emitter@2.4.7':
|
'@vladfrangu/async_event_emitter@2.4.7':
|
||||||
resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==}
|
resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==}
|
||||||
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
||||||
@@ -4150,6 +4324,7 @@ packages:
|
|||||||
basic-ftp@5.2.0:
|
basic-ftp@5.2.0:
|
||||||
resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==}
|
resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
|
deprecated: Security vulnerability fixed in 5.2.1, please upgrade
|
||||||
|
|
||||||
better-path-resolve@1.0.0:
|
better-path-resolve@1.0.0:
|
||||||
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
|
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
|
||||||
@@ -6311,6 +6486,11 @@ packages:
|
|||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
marked@18.0.5:
|
||||||
|
resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==}
|
||||||
|
engines: {node: '>= 20'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
marked@7.0.4:
|
marked@7.0.4:
|
||||||
resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==}
|
resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==}
|
||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
@@ -6593,6 +6773,10 @@ packages:
|
|||||||
node-releases@2.0.27:
|
node-releases@2.0.27:
|
||||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||||
|
|
||||||
|
nodemailer@9.0.3:
|
||||||
|
resolution: {integrity: sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
normalize-path@3.0.0:
|
normalize-path@3.0.0:
|
||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -7646,6 +7830,9 @@ packages:
|
|||||||
std-env@3.10.0:
|
std-env@3.10.0:
|
||||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||||
|
|
||||||
|
std-env@4.2.0:
|
||||||
|
resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
|
||||||
|
|
||||||
stop-iteration-iterator@1.1.0:
|
stop-iteration-iterator@1.1.0:
|
||||||
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -7932,6 +8119,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
|
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
tinyrainbow@3.1.0:
|
||||||
|
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
tinyspy@3.0.2:
|
tinyspy@3.0.2:
|
||||||
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
|
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@@ -8217,10 +8408,12 @@ packages:
|
|||||||
|
|
||||||
uuid@10.0.0:
|
uuid@10.0.0:
|
||||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
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
|
hasBin: true
|
||||||
|
|
||||||
uuid@9.0.1:
|
uuid@9.0.1:
|
||||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
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
|
hasBin: true
|
||||||
|
|
||||||
vali-date@1.0.0:
|
vali-date@1.0.0:
|
||||||
@@ -8403,6 +8596,47 @@ packages:
|
|||||||
jsdom:
|
jsdom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vitest@4.1.10:
|
||||||
|
resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==}
|
||||||
|
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@edge-runtime/vm': '*'
|
||||||
|
'@opentelemetry/api': ^1.9.0
|
||||||
|
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||||
|
'@vitest/browser-playwright': 4.1.10
|
||||||
|
'@vitest/browser-preview': 4.1.10
|
||||||
|
'@vitest/browser-webdriverio': 4.1.10
|
||||||
|
'@vitest/coverage-istanbul': 4.1.10
|
||||||
|
'@vitest/coverage-v8': 4.1.10
|
||||||
|
'@vitest/ui': 4.1.10
|
||||||
|
happy-dom: '*'
|
||||||
|
jsdom: '*'
|
||||||
|
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@edge-runtime/vm':
|
||||||
|
optional: true
|
||||||
|
'@opentelemetry/api':
|
||||||
|
optional: true
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-playwright':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-preview':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-webdriverio':
|
||||||
|
optional: true
|
||||||
|
'@vitest/coverage-istanbul':
|
||||||
|
optional: true
|
||||||
|
'@vitest/coverage-v8':
|
||||||
|
optional: true
|
||||||
|
'@vitest/ui':
|
||||||
|
optional: true
|
||||||
|
happy-dom:
|
||||||
|
optional: true
|
||||||
|
jsdom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
vizion@2.2.1:
|
vizion@2.2.1:
|
||||||
resolution: {integrity: sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==}
|
resolution: {integrity: sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
@@ -11355,6 +11589,10 @@ snapshots:
|
|||||||
'@types/qs': 6.14.0
|
'@types/qs': 6.14.0
|
||||||
'@types/serve-static': 1.15.10
|
'@types/serve-static': 1.15.10
|
||||||
|
|
||||||
|
'@types/form-data@2.5.2':
|
||||||
|
dependencies:
|
||||||
|
form-data: 4.0.5
|
||||||
|
|
||||||
'@types/fs-extra@11.0.4':
|
'@types/fs-extra@11.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/jsonfile': 6.1.4
|
'@types/jsonfile': 6.1.4
|
||||||
@@ -11382,6 +11620,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/long@4.0.2': {}
|
'@types/long@4.0.2': {}
|
||||||
|
|
||||||
|
'@types/marked@6.0.0':
|
||||||
|
dependencies:
|
||||||
|
marked: 18.0.5
|
||||||
|
|
||||||
'@types/mime@1.3.5': {}
|
'@types/mime@1.3.5': {}
|
||||||
|
|
||||||
'@types/ms@2.1.0':
|
'@types/ms@2.1.0':
|
||||||
@@ -11410,6 +11652,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
|
'@types/nodemailer@8.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.19.33
|
||||||
|
|
||||||
'@types/parse-json@4.0.2': {}
|
'@types/parse-json@4.0.2': {}
|
||||||
|
|
||||||
'@types/pg-pool@2.0.7':
|
'@types/pg-pool@2.0.7':
|
||||||
@@ -11665,6 +11911,15 @@ snapshots:
|
|||||||
chai: 6.2.2
|
chai: 6.2.2
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
|
|
||||||
|
'@vitest/expect@4.1.10':
|
||||||
|
dependencies:
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
|
'@types/chai': 5.2.3
|
||||||
|
'@vitest/spy': 4.1.10
|
||||||
|
'@vitest/utils': 4.1.10
|
||||||
|
chai: 6.2.2
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.33)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0))':
|
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.33)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 2.1.9
|
'@vitest/spy': 2.1.9
|
||||||
@@ -11705,6 +11960,14 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
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)
|
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)
|
||||||
|
|
||||||
|
'@vitest/mocker@4.1.10(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:
|
||||||
|
'@vitest/spy': 4.1.10
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
magic-string: 0.30.21
|
||||||
|
optionalDependencies:
|
||||||
|
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)
|
||||||
|
|
||||||
'@vitest/pretty-format@2.1.9':
|
'@vitest/pretty-format@2.1.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
tinyrainbow: 1.2.0
|
tinyrainbow: 1.2.0
|
||||||
@@ -11717,6 +11980,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
|
|
||||||
|
'@vitest/pretty-format@4.1.10':
|
||||||
|
dependencies:
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
'@vitest/runner@2.1.9':
|
'@vitest/runner@2.1.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/utils': 2.1.9
|
'@vitest/utils': 2.1.9
|
||||||
@@ -11733,6 +12000,11 @@ snapshots:
|
|||||||
'@vitest/utils': 4.0.18
|
'@vitest/utils': 4.0.18
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
'@vitest/runner@4.1.10':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/utils': 4.1.10
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
'@vitest/snapshot@2.1.9':
|
'@vitest/snapshot@2.1.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/pretty-format': 2.1.9
|
'@vitest/pretty-format': 2.1.9
|
||||||
@@ -11751,6 +12023,13 @@ snapshots:
|
|||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
'@vitest/snapshot@4.1.10':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 4.1.10
|
||||||
|
'@vitest/utils': 4.1.10
|
||||||
|
magic-string: 0.30.21
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
'@vitest/spy@2.1.9':
|
'@vitest/spy@2.1.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
tinyspy: 3.0.2
|
tinyspy: 3.0.2
|
||||||
@@ -11761,6 +12040,8 @@ snapshots:
|
|||||||
|
|
||||||
'@vitest/spy@4.0.18': {}
|
'@vitest/spy@4.0.18': {}
|
||||||
|
|
||||||
|
'@vitest/spy@4.1.10': {}
|
||||||
|
|
||||||
'@vitest/ui@4.0.18(vitest@4.0.18)':
|
'@vitest/ui@4.0.18(vitest@4.0.18)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/utils': 4.0.18
|
'@vitest/utils': 4.0.18
|
||||||
@@ -11769,7 +12050,7 @@ snapshots:
|
|||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
sirv: 3.0.2
|
sirv: 3.0.2
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.1.0
|
||||||
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)
|
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)
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -11790,6 +12071,12 @@ snapshots:
|
|||||||
'@vitest/pretty-format': 4.0.18
|
'@vitest/pretty-format': 4.0.18
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
|
|
||||||
|
'@vitest/utils@4.1.10':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 4.1.10
|
||||||
|
convert-source-map: 2.0.0
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
'@vladfrangu/async_event_emitter@2.4.7': {}
|
'@vladfrangu/async_event_emitter@2.4.7': {}
|
||||||
|
|
||||||
'@webassemblyjs/ast@1.14.1':
|
'@webassemblyjs/ast@1.14.1':
|
||||||
@@ -13855,7 +14142,7 @@ snapshots:
|
|||||||
|
|
||||||
happy-dom@20.5.3:
|
happy-dom@20.5.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.33
|
'@types/node': 22.19.10
|
||||||
'@types/whatwg-mimetype': 3.0.2
|
'@types/whatwg-mimetype': 3.0.2
|
||||||
'@types/ws': 8.18.1
|
'@types/ws': 8.18.1
|
||||||
entities: 6.0.1
|
entities: 6.0.1
|
||||||
@@ -14666,6 +14953,8 @@ snapshots:
|
|||||||
|
|
||||||
marked@14.0.0: {}
|
marked@14.0.0: {}
|
||||||
|
|
||||||
|
marked@18.0.5: {}
|
||||||
|
|
||||||
marked@7.0.4: {}
|
marked@7.0.4: {}
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
@@ -14899,6 +15188,8 @@ snapshots:
|
|||||||
|
|
||||||
node-releases@2.0.27: {}
|
node-releases@2.0.27: {}
|
||||||
|
|
||||||
|
nodemailer@9.0.3: {}
|
||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
normalize-svg-path@1.1.0:
|
normalize-svg-path@1.1.0:
|
||||||
@@ -16240,6 +16531,8 @@ snapshots:
|
|||||||
|
|
||||||
std-env@3.10.0: {}
|
std-env@3.10.0: {}
|
||||||
|
|
||||||
|
std-env@4.2.0: {}
|
||||||
|
|
||||||
stop-iteration-iterator@1.1.0:
|
stop-iteration-iterator@1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
@@ -16589,6 +16882,8 @@ snapshots:
|
|||||||
|
|
||||||
tinyrainbow@3.0.3: {}
|
tinyrainbow@3.0.3: {}
|
||||||
|
|
||||||
|
tinyrainbow@3.1.0: {}
|
||||||
|
|
||||||
tinyspy@3.0.2: {}
|
tinyspy@3.0.2: {}
|
||||||
|
|
||||||
tinyspy@4.0.4: {}
|
tinyspy@4.0.4: {}
|
||||||
@@ -16946,15 +17241,16 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- 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:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 2.0.3
|
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:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
|
- jiti
|
||||||
- less
|
- less
|
||||||
- lightningcss
|
- lightningcss
|
||||||
- sass
|
- sass
|
||||||
@@ -16963,16 +17259,19 @@ snapshots:
|
|||||||
- sugarss
|
- sugarss
|
||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- 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:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 2.0.3
|
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:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
|
- jiti
|
||||||
- less
|
- less
|
||||||
- lightningcss
|
- lightningcss
|
||||||
- sass
|
- sass
|
||||||
@@ -16981,6 +17280,8 @@ snapshots:
|
|||||||
- sugarss
|
- sugarss
|
||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- terser
|
||||||
|
- tsx
|
||||||
|
- yaml
|
||||||
|
|
||||||
vite@5.4.21(@types/node@20.19.33)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
|
vite@5.4.21(@types/node@20.19.33)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17024,7 +17325,25 @@ snapshots:
|
|||||||
tsx: 4.21.0
|
tsx: 4.21.0
|
||||||
yaml: 2.8.2
|
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):
|
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(vitest@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:
|
dependencies:
|
||||||
'@vitest/expect': 2.1.9
|
'@vitest/expect': 2.1.9
|
||||||
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.33)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0))
|
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.33)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0))
|
||||||
@@ -17062,7 +17381,7 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- terser
|
||||||
|
|
||||||
vitest@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):
|
vitest@2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@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:
|
dependencies:
|
||||||
'@vitest/expect': 2.1.9
|
'@vitest/expect': 2.1.9
|
||||||
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0))
|
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0))
|
||||||
@@ -17100,7 +17419,7 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- 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(vitest@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:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/expect': 3.2.4
|
'@vitest/expect': 3.2.4
|
||||||
@@ -17123,7 +17442,7 @@ snapshots:
|
|||||||
tinypool: 1.1.1
|
tinypool: 1.1.1
|
||||||
tinyrainbow: 2.0.0
|
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: 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
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/debug': 4.1.12
|
'@types/debug': 4.1.12
|
||||||
@@ -17132,6 +17451,7 @@ snapshots:
|
|||||||
happy-dom: 20.5.3
|
happy-dom: 20.5.3
|
||||||
jsdom: 27.4.0(canvas@3.2.1)
|
jsdom: 27.4.0(canvas@3.2.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
- jiti
|
||||||
- less
|
- less
|
||||||
- lightningcss
|
- lightningcss
|
||||||
- msw
|
- msw
|
||||||
@@ -17141,8 +17461,10 @@ snapshots:
|
|||||||
- sugarss
|
- sugarss
|
||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- 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(vitest@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:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/expect': 3.2.4
|
'@vitest/expect': 3.2.4
|
||||||
@@ -17165,7 +17487,7 @@ snapshots:
|
|||||||
tinypool: 1.1.1
|
tinypool: 1.1.1
|
||||||
tinyrainbow: 2.0.0
|
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: 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
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/debug': 4.1.12
|
'@types/debug': 4.1.12
|
||||||
@@ -17174,6 +17496,7 @@ snapshots:
|
|||||||
happy-dom: 20.5.3
|
happy-dom: 20.5.3
|
||||||
jsdom: 27.4.0(canvas@3.2.1)
|
jsdom: 27.4.0(canvas@3.2.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
- jiti
|
||||||
- less
|
- less
|
||||||
- lightningcss
|
- lightningcss
|
||||||
- msw
|
- msw
|
||||||
@@ -17183,6 +17506,8 @@ snapshots:
|
|||||||
- sugarss
|
- sugarss
|
||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- 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):
|
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:
|
dependencies:
|
||||||
@@ -17225,6 +17550,37 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
|
vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(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:
|
||||||
|
'@vitest/expect': 4.1.10
|
||||||
|
'@vitest/mocker': 4.1.10(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))
|
||||||
|
'@vitest/pretty-format': 4.1.10
|
||||||
|
'@vitest/runner': 4.1.10
|
||||||
|
'@vitest/snapshot': 4.1.10
|
||||||
|
'@vitest/spy': 4.1.10
|
||||||
|
'@vitest/utils': 4.1.10
|
||||||
|
es-module-lexer: 2.0.0
|
||||||
|
expect-type: 1.3.0
|
||||||
|
magic-string: 0.30.21
|
||||||
|
obug: 2.1.1
|
||||||
|
pathe: 2.0.3
|
||||||
|
picomatch: 4.0.3
|
||||||
|
std-env: 4.2.0
|
||||||
|
tinybench: 2.9.0
|
||||||
|
tinyexec: 1.0.2
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
tinyrainbow: 3.1.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)
|
||||||
|
why-is-node-running: 2.3.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
'@types/node': 22.19.10
|
||||||
|
'@vitest/ui': 4.0.18(vitest@4.0.18)
|
||||||
|
happy-dom: 20.5.3
|
||||||
|
jsdom: 27.4.0(canvas@3.2.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- msw
|
||||||
|
|
||||||
vizion@2.2.1:
|
vizion@2.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
async: 2.6.4
|
async: 2.6.4
|
||||||
|
|||||||
Reference in New Issue
Block a user