Compare commits

..

15 Commits

Author SHA1 Message Date
efd1341762 fix: add canvas build deps for gatekeeper x86 build
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 8s
Monorepo Pipeline / 🧪 Test (push) Failing after 7s
Monorepo Pipeline / 🏗️ Build (push) Failing after 7s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-26 23:10:47 +01:00
36a952db56 chore: sync versions to v1.8.21
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 9s
Monorepo Pipeline / 🧪 Test (push) Failing after 7s
Monorepo Pipeline / 🏗️ Build (push) Failing after 7s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-26 19:39:04 +01:00
8c637f0220 chore: trigger x86 ci build
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m31s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m27s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m30s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-26 19:05:19 +01:00
6dd97e7a6b chore: trigger x86 build for mb-grid and mintel.me
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m17s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m22s
Monorepo Pipeline / 🏗️ Build (push) Failing after 1m25s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-26 18:45:47 +01:00
9f426470bb fix(ci): update build platform from arm64 to amd64 for the new x86 server
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 50s
Monorepo Pipeline / 🏗️ Build (push) Failing after 56s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m8s
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-26 17:42:57 +01:00
960914ebb8 feat: content engine usw 2026-02-25 12:43:57 +01:00
a55a5bb834 fix: prevent .env changes during tagging and improve pre-push hook feedback
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 6s
Monorepo Pipeline / 🏗️ Build (push) Successful in 3m8s
Monorepo Pipeline / 🧪 Test (push) Successful in 4m16s
Monorepo Pipeline / 🧹 Lint (push) Successful in 5m34s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
2026-02-23 14:10:04 +01:00
0aaf858f5b chore: sync versions to v1.8.20
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m3s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m57s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m38s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 1m12s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 1m42s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 1m31s
Monorepo Pipeline / 🐳 Build Image Processor (push) Successful in 2m50s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 8m14s
Monorepo Pipeline / 🚀 Release (push) Successful in 9m13s
2026-02-23 14:03:27 +01:00
ec562c1b2c fix: imgproxy issues 2026-02-23 14:03:17 +01:00
02e15c3f4a chore: sync versions to v1.8.19
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 3m54s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m12s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m42s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 1m7s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 1m43s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 1m37s
Monorepo Pipeline / 🐳 Build Image Processor (push) Successful in 2m47s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 6m39s
Monorepo Pipeline / 🚀 Release (push) Successful in 7m18s
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 8s
2026-02-23 00:52:35 +01:00
cd4c2193ce feat: implement legacy imgproxy compatibility and URL mapping
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m56s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m32s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-23 00:14:13 +01:00
df7a464e03 fix(ci): sync lockfile and remove deleted model scripts
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 5m26s
Monorepo Pipeline / 🏗️ Build (push) Successful in 7m18s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m5s
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Successful in 1m8s
Monorepo Pipeline / 🐳 Build Build-Base (push) Successful in 1m43s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Successful in 1m27s
Monorepo Pipeline / 🐳 Build Image Processor (push) Successful in 2m38s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Successful in 5m49s
Monorepo Pipeline / 🚀 Release (push) Successful in 6m24s
2026-02-22 23:40:30 +01:00
e2e0653de6 chore(image-processor): use Gemini 3 Flash Preview
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🏗️ Build (push) Failing after 23s
Monorepo Pipeline / 🧹 Lint (push) Failing after 8s
Monorepo Pipeline / 🧪 Test (push) Failing after 21s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-22 23:31:44 +01:00
590ae6f69b chore: sync versions to v1.8.16
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Failing after 29s
Monorepo Pipeline / 🧹 Lint (push) Failing after 21s
Monorepo Pipeline / 🏗️ Build (push) Failing after 8s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
2026-02-22 23:24:30 +01:00
2a169f1dfc feat(image-processor): switch to OpenRouter Vision for smart crop and remove heavy models 2026-02-22 23:24:22 +01:00
63 changed files with 904 additions and 615 deletions

View File

@@ -24,5 +24,3 @@ coverage
**/.pnpm-store
.gitea
**/.gitea
models
**/models

2
.env
View File

@@ -1,5 +1,5 @@
# Project
IMAGE_TAG=v1.8.12
IMAGE_TAG=v1.8.19
PROJECT_NAME=at-mintel
PROJECT_COLOR=#82ed20
GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582

View File

@@ -1,5 +1,5 @@
# Project
IMAGE_TAG=v1.8.15
IMAGE_TAG=v1.8.21
PROJECT_NAME=sample-website
PROJECT_COLOR=#82ed20

View File

@@ -214,7 +214,7 @@ jobs:
with:
context: .
file: ${{ matrix.file }}
platforms: linux/arm64
platforms: linux/amd64
pull: true
provenance: false
push: true

View File

@@ -35,8 +35,9 @@ do
# Push the updated tag directly (using --no-verify to avoid recursion)
git push origin "$TAG" --force --no-verify
echo "✨ All done! Hook integrated the sync and pushed for you."
exit 1 # Still exit 1 to abort the original (now outdated) push attempt
echo "✨ Success! The hook synchronized the versions and pushed the updated tag for you."
echo " Note: The original push command was aborted in favor of the auto-push. This is normal."
exit 0 # Change to exit 0 to not show as an 'error' in vscode/terminal, though original push will still be technically 'failed' by git
else
echo "✨ Versions already in sync for $TAG."
fi

View File

@@ -81,3 +81,4 @@ Client websites scaffolded via the CLI use a **tag-based deployment** strategy:
See the [`@mintel/infra`](packages/infra/README.md) package for detailed template documentation.
Trigger rebuilding for x86 architecture.

View File

@@ -1,44 +1,39 @@
FROM node:20.18-bookworm-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN apt-get update && apt-get install -y \
build-essential \
python3 \
libcairo2-dev \
libpango1.0-dev \
libjpeg-dev \
libgif-dev \
librsvg2-dev \
libexpat1 \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g pnpm@10.30.1
FROM base AS build
WORKDIR /app
COPY . .
# Note: Canvas needs build tools on Debian
RUN apt-get update && apt-get install -y python3 make g++ libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
# Delete the prebuilt binary and force a clean rebuild from source for the correct container architecture
ENV npm_config_arch=arm64
ENV npm_config_target_arch=arm64
# We only need standard pnpm install now, no C++ tools needed for basic Sharp
RUN pnpm install --frozen-lockfile
RUN for dir in $(find /app/node_modules -type d -path "*/@tensorflow/tfjs-node"); do \
cd $dir && \
rm -rf lib/napi-v8/* && \
npm_config_build_from_source=true npm_config_arch=arm64 npm_config_target_arch=arm64 npm run install; \
done
# Generate models explicitly for Docker
RUN ls -la packages/image-processor/scripts || true
RUN pnpm dlx tsx packages/image-processor/scripts/download-models.ts
RUN pnpm --filter @mintel/image-processor build
RUN pnpm --filter image-service build
# Generated locally for caching
FROM base
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/apps/image-service/node_modules ./apps/image-service/node_modules
COPY --from=build /app/packages/image-processor/node_modules ./packages/image-processor/node_modules
# Make sure directories exist to prevent COPY errors
RUN mkdir -p /app/packages/image-processor/models /app/apps/image-service/dist
COPY --from=build /app/apps/image-service/dist ./apps/image-service/dist
COPY --from=build /app/apps/image-service/package.json ./apps/image-service/package.json
COPY --from=build /app/packages/image-processor/dist ./packages/image-processor/dist
COPY --from=build /app/packages/image-processor/package.json ./packages/image-processor/package.json
COPY --from=build /app/packages/image-processor/models ./packages/image-processor/models
# Instead of copying node_modules which contains native C++ bindings for canvas and tfjs-node,
# we copy the package.json files and install natively in the final stage so the bindings are correct.
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY apps/image-service/package.json ./apps/image-service/package.json
COPY packages/image-processor/package.json ./packages/image-processor/package.json
# Need runtime dependencies for canvas/sharp on Debian
RUN apt-get update && apt-get install -y libcairo2 libpango-1.0-0 libjpeg62-turbo libgif7 librsvg2-2 && rm -rf /var/lib/apt/lists/*
RUN pnpm install --frozen-lockfile --filter image-service...
COPY --from=build /app/apps/image-service/dist ./apps/image-service/dist
COPY --from=build /app/packages/image-processor/dist ./packages/image-processor/dist
COPY --from=build /app/packages/image-processor/models ./packages/image-processor/models
EXPOSE 8080
WORKDIR /app/apps/image-service

View File

@@ -1,6 +1,6 @@
{
"name": "image-service",
"version": "1.8.15",
"version": "1.8.21",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,45 +1,51 @@
import Fastify from "fastify";
import { processImageWithSmartCrop } from "@mintel/image-processor";
import {
processImageWithSmartCrop,
parseImgproxyOptions,
mapUrl,
} from "@mintel/image-processor";
const fastify = Fastify({
logger: true,
});
fastify.get("/unsafe/:options/:urlSafeB64", async (request, reply) => {
// Compatibility endpoint for old imgproxy calls (optional, but requested by some systems sometimes)
// For now, replacing logic in clients is preferred. So we just redirect or error.
return reply
.status(400)
.send({ error: "Legacy imgproxy API not supported. Use /process" });
});
fastify.get("/process", async (request, reply) => {
const query = request.query as {
url?: string;
w?: string;
h?: string;
q?: string;
format?: string;
const { options, urlSafeB64 } = request.params as {
options: string;
urlSafeB64: string;
};
const { url } = query;
const width = parseInt(query.w || "800", 10);
const height = parseInt(query.h || "600", 10);
const quality = parseInt(query.q || "80", 10);
const format = (query.format || "webp") as "webp" | "jpeg" | "png" | "avif";
if (!url) {
return reply.status(400).send({ error: 'Parameter "url" is required' });
// urlSafeB64 might be "plain/http://..." or a Base64 string
let url = "";
if (urlSafeB64.startsWith("plain/")) {
url = urlSafeB64.substring(6);
} else {
try {
url = Buffer.from(urlSafeB64, "base64").toString("utf-8");
} catch (e) {
return reply.status(400).send({ error: "Invalid Base64 URL" });
}
}
const parsedOptions = parseImgproxyOptions(options);
const mappedUrl = mapUrl(url, process.env.IMGPROXY_URL_MAPPING);
return handleProcessing(mappedUrl, parsedOptions, reply);
});
// Helper to avoid duplication
async function handleProcessing(url: string, options: any, reply: any) {
const width = options.width || 800;
const height = options.height || 600;
const quality = options.quality || 80;
const format = options.format || "webp";
try {
const response = await fetch(url);
if (!response.ok) {
return reply
.status(response.status)
.send({
error: `Failed to fetch source image: ${response.statusText}`,
});
return reply.status(response.status).send({
error: `Failed to fetch source image: ${response.statusText}`,
});
}
const arrayBuffer = await response.arrayBuffer();
@@ -61,6 +67,29 @@ fastify.get("/process", async (request, reply) => {
.status(500)
.send({ error: "Internal Server Error processing image" });
}
}
fastify.get("/process", async (request, reply) => {
const query = request.query as {
url?: string;
w?: string;
h?: string;
q?: string;
format?: string;
};
const { url } = query;
const width = parseInt(query.w || "800", 10);
const height = parseInt(query.h || "600", 10);
const quality = parseInt(query.q || "80", 10);
const format = (query.format || "webp") as any;
if (!url) {
return reply.status(400).send({ error: 'Parameter "url" is required' });
}
const mappedUrl = mapUrl(url, process.env.IMGPROXY_URL_MAPPING);
return handleProcessing(mappedUrl, { width, height, quality, format }, reply);
});
fastify.get("/health", async () => {

View File

@@ -1,6 +1,6 @@
{
"name": "sample-website",
"version": "1.8.15",
"version": "1.8.21",
"private": true,
"type": "module",
"scripts": {

14
optimize-images.sh Normal file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
# Ghost Image Optimizer
# Target directory for Ghost content
TARGET_DIR="/home/deploy/sites/marisas.world/content/images"
echo "Starting image optimization for $TARGET_DIR..."
# Find all original images, excluding the 'size/' directory where Ghost stores thumbnails
# Resize images larger than 2500px down to 2500px width
# Compress JPEG/PNG to 80% quality
find "$TARGET_DIR" -type d -name "size" -prune -o \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" \) -type f -exec mogrify -resize '2500x>' -quality 80 {} +
echo "Optimization complete."

View File

@@ -57,7 +57,7 @@
"pino-pretty": "^13.1.3",
"require-in-the-middle": "^8.0.1"
},
"version": "1.8.15",
"version": "1.8.21",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",

View File

@@ -2,7 +2,7 @@
"name": "acquisition-manager",
"description": "Custom High-Fidelity Management for Directus",
"icon": "extension",
"version": "1.8.15",
"version": "1.8.21",
"type": "module",
"keywords": [
"directus",

View File

@@ -1,6 +1,6 @@
{
"name": "acquisition",
"version": "1.8.15",
"version": "1.8.21",
"type": "module",
"directus:extension": {
"type": "endpoint",

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/cli",
"version": "1.8.15",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/cloner",
"version": "1.8.15",
"version": "1.8.21",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/cms-infra",
"version": "1.8.15",
"version": "1.8.21",
"private": true,
"type": "module",
"scripts": {

View File

@@ -2,7 +2,7 @@
"name": "company-manager",
"description": "Custom High-Fidelity Management for Directus",
"icon": "extension",
"version": "1.8.15",
"version": "1.8.21",
"type": "module",
"keywords": [
"directus",

View File

@@ -0,0 +1,38 @@
{
"name": "@mintel/concept-engine",
"version": "1.8.21",
"private": true,
"description": "AI-powered web project concept generation and analysis",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"concept": "./dist/cli.js"
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest",
"clean": "rm -rf dist",
"lint": "eslint src --ext .ts",
"concept": "tsx src/cli.ts run"
},
"dependencies": {
"@crawlee/cheerio": "^3.11.2",
"@mintel/journaling": "workspace:*",
"@mintel/page-audit": "workspace:*",
"axios": "^1.7.9",
"cheerio": "1.0.0-rc.12",
"commander": "^13.1.0",
"dotenv": "^16.4.7",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/node": "^20.17.17",
"tsup": "^8.3.6",
"tsx": "^4.19.2",
"typescript": "^5.7.3",
"vitest": "^3.0.5"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@mintel/content-engine",
"version": "1.8.15",
"private": true,
"version": "1.8.21",
"private": false,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",

View File

@@ -17,6 +17,7 @@ export interface OptimizationTask {
availableComponents?: ComponentDefinition[];
instructions?: string;
internalLinks?: { title: string; slug: string }[];
customSources?: string[];
}
export interface OptimizeFileOptions {
@@ -211,7 +212,32 @@ export class AiBlogPostOrchestrator {
console.log(`✅ Saved optimized file to: ${finalPath}`);
}
private async generateVisualPrompt(content: string): Promise<string> {
async generateSlug(content: string, title?: string, instructions?: string): Promise<string> {
const response = await this.openai.chat.completions.create({
model: "google/gemini-2.5-flash",
messages: [
{
role: "system",
content: `You generate SEO-optimized URL slugs for B2B blog posts based on the provided content.
Return ONLY a JSON object with a single string field "slug".
Example: {"slug": "how-to-optimize-react-performance"}
Rules: Use lowercase letters, numbers, and hyphens only. No special characters. Keep it concise (2-5 words).`,
},
{ role: "user", content: `Title: ${title || "Unknown"}\n\nContent:\n${content.slice(0, 3000)}...${instructions ? `\n\nEDITOR INSTRUCTIONS:\nPlease strictly follow these instructions from the editor when generating the slug:\n${instructions}` : ""}` },
],
response_format: { type: "json_object" },
});
try {
const parsed = JSON.parse(response.choices[0].message.content || '{"slug": ""}');
let slug = parsed.slug || "new-post";
return slug.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
} catch {
return "new-post";
}
}
public async generateVisualPrompt(content: string, instructions?: string): Promise<string> {
const response = await this.openai.chat.completions.create({
model: this.model,
messages: [
@@ -227,7 +253,7 @@ FOCUS: The core metaphor or technical concept of the article.
Example output: "A complex network of glowing fiber optic nodes forming a recursive pyramid structure, technical blue lineart style."`,
},
{ role: "user", content: content.slice(0, 5000) },
{ role: "user", content: `${content.slice(0, 5000)}${instructions ? `\n\nEDITOR INSTRUCTIONS:\nPlease strictly follow these instructions from the editor when generating the visual prompt:\n${instructions}` : ""}` },
],
max_tokens: 100,
});
@@ -303,6 +329,7 @@ Example output: "A complex network of glowing fiber optic nodes forming a recurs
);
const realPosts = await this.researchAgent.fetchRealSocialPosts(
task.content.slice(0, 500),
task.customSources
);
socialPosts.push(...realPosts);
}
@@ -470,7 +497,6 @@ BLOG POST BEST PRACTICES (MANDATORY):
- MEME DIVERSITY: Du MUSST ZWINGEND für jedes Meme (sofern passend) abwechslungsreiche Templates nutzen. Um dies zu garantieren, wurde für diesen Artikel das folgende Template ausgewählt: '${forcedMeme}'. Du MUSST EXAKT DIESES TEMPLATE NUTZEN. Versuche nicht, es durch ein Standard-Template wie 'drake' zu ersetzen!
- Zitat-Varianten: Wenn du Organisationen oder Studien zitierst, nutze ArticleQuote (mit isCompany=true für Firmen). Für Personen lass isCompany weg.
- Füge zwingend ein prägnantes 'TL;DR' ganz am Anfang ein.
- Füge ein sauberes TableOfContents ein.
- Verwende unsere Komponenten stilvoll für Visualisierungen.
- Agiere als hochprofessioneller Digital Architect und entferne alte MDX-Metadaten im Body.
- Fazit: Schließe JEDEN Artikel ZWINGEND mit einem starken, klaren 'Fazit' ab.

View File

@@ -2,7 +2,7 @@
"name": "customer-manager",
"description": "Custom High-Fidelity Management for Directus",
"icon": "extension",
"version": "1.8.15",
"version": "1.8.21",
"type": "module",
"keywords": [
"directus",

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/directus-extension-toolkit",
"version": "1.8.15",
"version": "1.8.21",
"description": "Shared toolkit for Directus extensions in the Mintel ecosystem",
"type": "module",
"main": "./dist/index.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/eslint-config",
"version": "1.8.15",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -0,0 +1,46 @@
{
"name": "@mintel/estimation-engine",
"version": "1.8.21",
"private": true,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"estimate": "./dist/cli.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
"dev": "tsup src/index.ts src/cli.ts --format esm --watch --dts",
"lint": "eslint src",
"estimate": "tsx src/cli.ts"
},
"dependencies": {
"@mintel/concept-engine": "workspace:*",
"axios": "^1.6.0",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"dotenv": "^17.3.1",
"ink": "^5.1.0",
"ink-spinner": "^5.0.0",
"ink-select-input": "^6.0.0",
"ink-text-input": "^6.0.0",
"react": "^18.2.0",
"openai": "^4.82.0"
},
"devDependencies": {
"@mintel/eslint-config": "workspace:*",
"@mintel/tsconfig": "workspace:*",
"@types/node": "^20.0.0",
"@types/react": "^18.2.0",
"tsup": "^8.3.5",
"tsx": "^4.7.0",
"typescript": "^5.0.0"
}
}

View File

@@ -2,7 +2,7 @@
"name": "feedback-commander",
"description": "Custom High-Fidelity Management for Directus",
"icon": "extension",
"version": "1.8.15",
"version": "1.8.21",
"type": "module",
"keywords": [
"directus",

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/gatekeeper",
"version": "1.8.15",
"version": "1.8.21",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/husky-config",
"version": "1.8.15",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -0,0 +1 @@
404: Not Found

View File

@@ -0,0 +1,30 @@
[
{
"weights":
[
{"name":"conv0/filters","shape":[3,3,3,16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009007044399485869,"min":-1.2069439495311063}},
{"name":"conv0/bias","shape":[16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005263455241334205,"min":-0.9211046672334858}},
{"name":"conv1/depthwise_filter","shape":[3,3,16,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004001977630690033,"min":-0.5042491814669441}},
{"name":"conv1/pointwise_filter","shape":[1,1,16,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013836609615999109,"min":-1.411334180831909}},
{"name":"conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0015159862590771096,"min":-0.30926119685173037}},
{"name":"conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002666276225856706,"min":-0.317286870876948}},
{"name":"conv2/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015265831292844286,"min":-1.6792414422128714}},
{"name":"conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0020280554598453,"min":-0.37113414915168985}},
{"name":"conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006100742489683862,"min":-0.8907084034938438}},
{"name":"conv3/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016276211832083907,"min":-2.0508026908425725}},
{"name":"conv3/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003394414279975143,"min":-0.7637432129944072}},
{"name":"conv4/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006716050119961009,"min":-0.8059260143953211}},
{"name":"conv4/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021875603993733724,"min":-2.8875797271728514}},
{"name":"conv4/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0041141652009066415,"min":-0.8187188749804216}},
{"name":"conv5/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008423839597141042,"min":-0.9013508368940915}},
{"name":"conv5/pointwise_filter","shape":[1,1,256,512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.030007277283014035,"min":-3.8709387695088107}},
{"name":"conv5/bias","shape":[512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008402082966823203,"min":-1.4871686851277068}},
{"name":"conv8/filters","shape":[1,1,512,25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.028336129469030042,"min":-4.675461362389957}},
{"name":"conv8/bias","shape":[25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002268134028303857,"min":-0.41053225912299807}}
],
"paths":
[
"tiny_face_detector_model.bin"
]
}
]

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/image-processor",
"version": "1.8.15",
"version": "1.8.21",
"private": true,
"type": "module",
"main": "./dist/index.js",
@@ -13,14 +13,14 @@
}
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts --clean",
"dev": "tsup src/index.ts --format esm --watch --dts",
"build": "tsup",
"dev": "tsup --watch",
"lint": "eslint src"
},
"dependencies": {
"@tensorflow/tfjs-node": "^4.22.0",
"@vladmandic/face-api": "^1.7.13",
"canvas": "^2.11.2",
"@tensorflow/tfjs": "^4.22.0",
"@vladmandic/face-api": "^1.7.15",
"canvas": "^3.2.1",
"sharp": "^0.33.2"
},
"devDependencies": {

View File

@@ -1,55 +0,0 @@
import * as fs from "node:fs";
import * as path from "node:path";
import * as https from "node:https";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const MODELS_DIR = path.join(__dirname, "..", "models");
const BASE_URL =
"https://raw.githubusercontent.com/vladmandic/face-api/master/model/";
const models = [
"tiny_face_detector_model-weights_manifest.json",
"tiny_face_detector_model-shard1",
];
async function downloadModel(filename: string) {
const destPath = path.join(MODELS_DIR, filename);
if (fs.existsSync(destPath)) {
console.log(`Model ${filename} already exists.`);
return;
}
return new Promise((resolve, reject) => {
console.log(`Downloading ${filename}...`);
const file = fs.createWriteStream(destPath);
https
.get(BASE_URL + filename, (response) => {
response.pipe(file);
file.on("finish", () => {
file.close();
resolve(true);
});
})
.on("error", (err) => {
fs.unlinkSync(destPath);
reject(err);
});
});
}
async function main() {
if (!fs.existsSync(MODELS_DIR)) {
fs.mkdirSync(MODELS_DIR, { recursive: true });
}
for (const model of models) {
await downloadModel(model);
}
console.log("All models downloaded successfully!");
}
main().catch(console.error);

View File

@@ -1,25 +1,33 @@
import * as faceapi from "@vladmandic/face-api";
// Provide Canvas fallback for face-api in Node.js
import { Canvas, Image, ImageData } from "canvas";
import sharp from "sharp";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { Canvas, Image, ImageData } from "canvas";
// Use the ESM no-bundle build to avoid the default Node entrypoint
// which hardcodes require('@tensorflow/tfjs-node') and crashes in Docker.
// This build uses pure @tensorflow/tfjs (JS-only, no native C++ bindings).
// @ts-ignore - direct path import has no type declarations
import * as faceapi from "@vladmandic/face-api/dist/face-api.esm-nobundle.js";
import * as tf from "@tensorflow/tfjs";
import path from "path";
import { fileURLToPath } from "url";
// @ts-expect-error FaceAPI does not have type definitions for monkeyPatch
faceapi.env.monkeyPatch({ Canvas, Image, ImageData });
// Polyfill required by face-api for Node.js
faceapi.env.monkeyPatch({ Canvas, Image, ImageData } as any);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const MODEL_URL = path.join(__dirname, "../models");
// Path to the downloaded models
const MODELS_PATH = path.join(__dirname, "..", "models");
// State flag to ensure we only load weights once
let modelsLoaded = false;
let isModelsLoaded = false;
async function loadModelsOnce() {
if (modelsLoaded) return;
// Initialize pure JS CPU backend (no native bindings needed)
await tf.setBackend("cpu");
await tf.ready();
async function loadModels() {
if (isModelsLoaded) return;
await faceapi.nets.tinyFaceDetector.loadFromDisk(MODELS_PATH);
isModelsLoaded = true;
// Load the microscopic TinyFaceDetector (~190KB)
await faceapi.nets.tinyFaceDetector.loadFromDisk(MODEL_URL);
modelsLoaded = true;
}
export interface ProcessImageOptions {
@@ -29,23 +37,82 @@ export interface ProcessImageOptions {
quality?: number;
}
/**
* Maps a URL based on the IMGPROXY_URL_MAPPING environment variable.
* Format: "match1:replace1,match2:replace2"
*/
export function mapUrl(url: string, mappingString?: string): string {
if (!mappingString) return url;
const mappings = mappingString.split(",").map((m) => {
if (m.includes("|")) {
return m.split("|");
}
// Legacy support for simple "host:target" or cases where one side might have a protocol
// We try to find the split point that isn't part of a protocol "://"
const colonIndices = [];
for (let i = 0; i < m.length; i++) {
if (m[i] === ":") {
// Check if this colon is part of "://"
if (!(m[i + 1] === "/" && m[i + 2] === "/")) {
colonIndices.push(i);
}
}
}
if (colonIndices.length === 0) return [m];
// In legacy mode with colons, we take the LAST non-protocol colon as the separator
// This handles "http://host:port" or "host:http://target" better
const lastColon = colonIndices[colonIndices.length - 1];
return [m.substring(0, lastColon), m.substring(lastColon + 1)];
});
let mappedUrl = url;
for (const [match, replace] of mappings) {
if (match && replace && url.includes(match)) {
mappedUrl = url.replace(match, replace);
}
}
return mappedUrl;
}
/**
* Parses legacy imgproxy options string.
* Example: rs:fill:300:400/q:80
*/
export function parseImgproxyOptions(
optionsStr: string,
): Partial<ProcessImageOptions> {
const parts = optionsStr.split("/");
const options: Partial<ProcessImageOptions> = {};
for (const part of parts) {
if (part.startsWith("rs:")) {
const [, , w, h] = part.split(":");
if (w) options.width = parseInt(w, 10);
if (h) options.height = parseInt(h, 10);
} else if (part.startsWith("q:")) {
const q = part.split(":")[1];
if (q) options.quality = parseInt(q, 10);
} else if (part.startsWith("ext:")) {
const ext = part.split(":")[1] as any;
if (["webp", "jpeg", "png", "avif"].includes(ext)) {
options.format = ext;
}
}
}
return options;
}
export async function processImageWithSmartCrop(
inputBuffer: Buffer,
options: ProcessImageOptions,
): Promise<Buffer> {
await loadModels();
// Load image via Canvas for face-api
const img = new Image();
img.src = inputBuffer;
// Detect faces
const detections = await faceapi.detectAllFaces(
// @ts-expect-error FaceAPI does not have type definitions for monkeyPatch
img,
new faceapi.TinyFaceDetectorOptions(),
);
const sharpImage = sharp(inputBuffer);
const metadata = await sharpImage.metadata();
@@ -53,35 +120,45 @@ export async function processImageWithSmartCrop(
throw new Error("Could not read image metadata");
}
// If faces are found, calculate the bounding box containing all faces
// Load ML models (noop if already loaded)
await loadModelsOnce();
// Convert sharp image to a Node-compatible canvas Image for face-api
const jpegBuffer = await sharpImage.jpeg().toBuffer();
const img = new Image();
img.src = jpegBuffer;
const canvas = new Canvas(img.width, img.height);
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, img.width, img.height);
// Detect faces locally using the tiny model
// Requires explicit any cast since the types expect HTML elements in browser contexts
const detections = await faceapi.detectAllFaces(
canvas as any,
new faceapi.TinyFaceDetectorOptions(),
);
let cropPosition: "center" | "attention" | number = "attention"; // Fallback to sharp's attention if no faces
if (detections.length > 0) {
// We have faces! Calculate the bounding box that contains all of them
let minX = metadata.width;
let minY = metadata.height;
let maxX = 0;
let maxY = 0;
for (const det of detections) {
const { x, y, width, height } = det.box;
if (x < minX) minX = Math.max(0, x);
if (y < minY) minY = Math.max(0, y);
if (x + width > maxX) maxX = Math.min(metadata.width, x + width);
if (y + height > maxY) maxY = Math.min(metadata.height, y + height);
const box = det.box;
if (box.x < minX) minX = Math.max(0, box.x);
if (box.y < minY) minY = Math.max(0, box.y);
if (box.x + box.width > maxX)
maxX = Math.min(metadata.width, box.x + box.width);
if (box.y + box.height > maxY)
maxY = Math.min(metadata.height, box.y + box.height);
}
const faceBoxWidth = maxX - minX;
const faceBoxHeight = maxY - minY;
// Calculate center of the faces
const centerX = Math.floor(minX + faceBoxWidth / 2);
const centerY = Math.floor(minY + faceBoxHeight / 2);
// Provide this as a focus point for sharp's extract or resize
// We can use sharp's resize with `position` focusing on crop options,
// or calculate an exact bounding box. However, extracting an exact bounding box
// and then resizing usually yields the best results when focusing on a specific coordinate.
// A simpler approach is to crop a rectangle with the target aspect ratio
// centered on the faces, then resize. Let's calculate the crop box.
const centerX = Math.floor(minX + (maxX - minX) / 2);
const centerY = Math.floor(minY + (maxY - minY) / 2);
const targetRatio = options.width / options.height;
const currentRatio = metadata.width / metadata.height;
@@ -89,38 +166,39 @@ export async function processImageWithSmartCrop(
let cropWidth = metadata.width;
let cropHeight = metadata.height;
// Determine the maximal crop window that maintains aspect ratio
if (currentRatio > targetRatio) {
// Image is wider than target, calculate new width
cropWidth = Math.floor(metadata.height * targetRatio);
} else {
// Image is taller than target, calculate new height
cropHeight = Math.floor(metadata.width / targetRatio);
}
// Try to center the crop box around the faces
// Center the crop window over the center of the faces
let cropX = Math.floor(centerX - cropWidth / 2);
let cropY = Math.floor(centerY - cropHeight / 2);
// Keep crop box within image bounds
// Keep crop window inside image bounds
if (cropX < 0) cropX = 0;
if (cropY < 0) cropY = 0;
if (cropX + cropWidth > metadata.width) cropX = metadata.width - cropWidth;
if (cropY + cropHeight > metadata.height)
cropY = metadata.height - cropHeight;
// Pre-crop the image to isolate the faces before resizing
sharpImage.extract({
left: cropX,
top: cropY,
width: cropWidth,
height: cropHeight,
});
// As we manually calculated the exact focal box, we can now just center it
cropPosition = "center";
}
// Finally, resize to the requested dimensions and format
let finalImage = sharpImage.resize(options.width, options.height, {
// If faces weren't found, default to entropy/attention based cropping as fallback
fit: "cover",
position: detections.length > 0 ? "center" : "attention",
position: cropPosition,
});
const format = options.format || "webp";

View File

@@ -0,0 +1,19 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
clean: true,
// Bundle face-api and tensorflow inline (they're pure JS).
// Keep sharp and canvas external (they have native C++ bindings).
noExternal: [
"@vladmandic/face-api",
"@tensorflow/tfjs",
"@tensorflow/tfjs-backend-wasm"
],
external: [
"sharp",
"canvas"
],
});

View File

@@ -1,6 +1,6 @@
# Step 1: Builder stage
FROM node:20-alpine AS builder
RUN apk add --no-cache libc6-compat curl
RUN apk add --no-cache libc6-compat curl python3 make g++ pkgconfig pixman-dev cairo-dev pango-dev
WORKDIR /app
RUN corepack enable pnpm
ENV CI=true
@@ -25,7 +25,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
--mount=type=secret,id=NPM_TOKEN \
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) && \
pnpm config set store-dir /pnpm/store && \
pnpm i --frozen-lockfile
pnpm i --no-frozen-lockfile
# Copy the rest of the source
COPY . .

View File

@@ -189,7 +189,7 @@ jobs:
with:
context: .
file: packages/infra/docker/Dockerfile.nextjs
platforms: linux/arm64
platforms: linux/amd64
pull: true
provenance: false
build-args: |

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/infra",
"version": "1.8.15",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/journaling",
"version": "1.8.15",
"version": "1.8.21",
"private": true,
"type": "module",
"main": "./dist/index.js",

View File

@@ -176,6 +176,7 @@ Return JSON: { "facts": [ { "statement": "...", "source": "Organization Name Onl
*/
async fetchRealSocialPosts(
topic: string,
customSources?: string[],
retries = 1,
): Promise<SocialPost[]> {
console.log(
@@ -220,7 +221,7 @@ Return a JSON object with a single string field "query". Example: {"query": "cor
if (!videos || videos.length === 0) {
console.warn(`⚠️ [Serper] No videos found for query: "${queryStr}"`);
if (retries > 0) return this.fetchRealSocialPosts(topic, retries - 1);
if (retries > 0) return this.fetchRealSocialPosts(topic, customSources, retries - 1);
return [];
}
@@ -237,11 +238,16 @@ Return a JSON object with a single string field "query". Example: {"query": "cor
if (ytVideos.length === 0) {
console.warn(`⚠️ [Serper] No YouTube videos in search results.`);
if (retries > 0) return this.fetchRealSocialPosts(topic, retries - 1);
if (retries > 0) return this.fetchRealSocialPosts(topic, customSources, retries - 1);
return [];
}
// Step 3: Ask the LLM to evaluate the relevance of the found videos
const sourceExamples = customSources && customSources.length > 0
? `Specifically prioritize content from: ${customSources.join(", ")}.`
: `(e.g., Google Developers, Vercel, Theo - t3.gg, Fireship, Syntax, ByteByteGo, IBM Technology, McKinsey, Gartner, Deloitte).`;
const evalPrompt = `You are a strict technical evaluator. You must select the MOST RELEVANT educational tech video from the list below based on this core article context: "${topic.slice(0, 800)}..."
Videos:
@@ -249,7 +255,7 @@ ${ytVideos.map((v, i) => `[ID: ${i}] Title: "${v.title}" | Channel: "${v.channel
RULES:
1. The video MUST be highly relevant to the EXACT technical topic of the context.
2. The channel SHOULD be a high-quality tech, development, or professional B2B channel (e.g., Google Developers, Vercel, Theo - t3.gg, Fireship, Syntax, ByteByteGo, IBM Technology, McKinsey, Gartner, Deloitte). AVOID gaming, generic vlogs, clickbait, off-topic podcasts, or unrelated topics.
2. The channel SHOULD be a high-quality tech, development, or professional B2B channel ${sourceExamples} AVOID gaming, generic vlogs, clickbait, off-topic podcasts, or unrelated topics.
3. If none of the videos are strictly relevant to the core technical or business subject (e.g. they are just casually mentioning the word), YOU MUST RETURN -1. Be extremely critical. Do not just pick the "best of the worst".
4. If one is highly relevant, return its ID number.
@@ -273,7 +279,7 @@ Return ONLY a JSON object: {"bestVideoId": number}`;
if (bestIdx < 0 || bestIdx >= ytVideos.length) {
console.warn(`⚠️ [Serper] LLM rejected all videos as irrelevant.`);
if (retries > 0) return this.fetchRealSocialPosts(topic, retries - 1);
if (retries > 0) return this.fetchRealSocialPosts(topic, customSources, retries - 1);
return [];
}
@@ -342,7 +348,7 @@ CRITICAL: Do NOT provide more than 2 trendsKeywords. Keep it extremely focused.`
try {
let parsed = JSON.parse(
response.choices[0].message.content ||
'{"trendsKeywords": [], "dcVariables": []}',
'{"trendsKeywords": [], "dcVariables": []}',
);
if (Array.isArray(parsed)) {
parsed = parsed[0] || { trendsKeywords: [], dcVariables: [] };

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/mail",
"version": "1.8.15",
"version": "1.8.21",
"private": false,
"publishConfig": {
"access": "public",

View File

@@ -1,7 +1,7 @@
{
"name": "@mintel/meme-generator",
"version": "1.8.15",
"private": true,
"version": "1.8.21",
"private": false,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/next-config",
"version": "1.8.15",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/next-feedback",
"version": "1.8.15",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/next-observability",
"version": "1.8.15",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/next-utils",
"version": "1.8.15",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/observability",
"version": "1.8.15",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -0,0 +1,39 @@
{
"name": "@mintel/page-audit",
"version": "1.8.21",
"private": true,
"description": "AI-powered website IST-analysis using DataForSEO and Gemini",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"page-audit": "./dist/cli.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
"dev": "tsup src/index.ts src/cli.ts --format esm --watch --dts",
"audit": "tsx src/cli.ts"
},
"dependencies": {
"chalk": "^5.3.0",
"cheerio": "^1.0.0",
"commander": "^12.0.0",
"dotenv": "^17.3.1",
"openai": "^4.82.0"
},
"devDependencies": {
"@mintel/eslint-config": "workspace:*",
"@mintel/tsconfig": "workspace:*",
"@types/node": "^20.0.0",
"tsup": "^8.3.5",
"tsx": "^4.7.0",
"typescript": "^5.0.0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/pdf",
"version": "1.8.15",
"version": "1.8.21",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",

View File

@@ -13,6 +13,8 @@ import {
Footer,
FoldingMarks,
DocumentTitle,
COLORS,
FONT_SIZES,
} from "./pdf/SharedUI.js";
import { SimpleLayout } from "./pdf/SimpleLayout.js";
@@ -29,23 +31,23 @@ const localStyles = PDFStyleSheet.create({
marginBottom: 6,
},
monoNumber: {
fontSize: 7,
fontSize: FONT_SIZES.TINY,
fontWeight: "bold",
color: "#94a3b8",
color: COLORS.TEXT_LIGHT,
letterSpacing: 2,
width: 25,
},
sectionTitle: {
fontSize: 9,
fontSize: FONT_SIZES.LABEL,
fontWeight: "bold",
color: "#000000",
color: COLORS.CHARCOAL,
textTransform: "uppercase",
letterSpacing: 0.5,
},
officialText: {
fontSize: 8,
fontSize: FONT_SIZES.BODY,
lineHeight: 1.5,
color: "#334155",
color: COLORS.TEXT_MAIN,
textAlign: "justify",
paddingLeft: 25,
},
@@ -100,7 +102,7 @@ export const AgbsPDF = ({
};
const content = (
<>
<PDFView>
<DocumentTitle
title="Allgemeine Geschäftsbedingungen"
subLines={[`Stand: ${date}`]}
@@ -142,7 +144,7 @@ export const AgbsPDF = ({
<AGBSection index="05" title="Abnahme">
Die Leistung gilt als abgenommen, wenn der Auftraggeber sie produktiv
nutzt oder innerhalb von 7 Tagen nach Bereitstellung keine
nutzt oder innerhalb von 30 Tagen nach Bereitstellung keine
wesentlichen Mängel angezeigt werden. Optische Abweichungen,
Geschmacksfragen oder subjektive Einschätzungen stellen keine Mängel
dar.
@@ -206,7 +208,7 @@ export const AgbsPDF = ({
bleibt die Wirksamkeit der übrigen Regelungen unberührt.
</AGBSection>
</PDFView>
</>
</PDFView>
);
if (mode === "full") {
@@ -214,9 +216,8 @@ export const AgbsPDF = ({
<SimpleLayout
companyData={companyData}
bankData={bankData}
headerIcon={headerIcon}
footerLogo={footerLogo}
icon={headerIcon}
pageNumber="10"
showPageNumber={false}
>
{content}
@@ -232,7 +233,7 @@ export const AgbsPDF = ({
<Footer
logo={footerLogo}
companyData={companyData}
bankData={bankData}
_bankData={bankData}
showDetails={false}
showPageNumber={false}
/>

View File

@@ -46,7 +46,7 @@ export const CombinedQuotePDF = ({
const layoutProps = {
date,
icon: estimationProps.headerIcon,
headerIcon: estimationProps.headerIcon,
footerLogo: estimationProps.footerLogo,
companyData,
bankData,
@@ -71,7 +71,7 @@ export const CombinedQuotePDF = ({
footerLogo={estimationProps.footerLogo}
/>
)}
<SimpleLayout {...layoutProps} pageNumber="END" showPageNumber={false}>
<SimpleLayout {...layoutProps} showPageNumber={false}>
<ClosingModule />
</SimpleLayout>
</PDFDocument>

View File

@@ -50,7 +50,7 @@ export const EstimationPDF = ({
const commonProps = {
state,
date,
icon: headerIcon,
headerIcon,
footerLogo,
companyData,
};
@@ -64,17 +64,17 @@ export const EstimationPDF = ({
<FrontPageModule state={state} headerIcon={headerIcon} date={date} />
</PDFPage>
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<SimpleLayout {...commonProps}>
<BriefingModule state={state} />
</SimpleLayout>
{state.sitemap && state.sitemap.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<SimpleLayout {...commonProps}>
<SitemapModule state={state} />
</SimpleLayout>
)}
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<SimpleLayout {...commonProps}>
<EstimationModule
state={state}
positions={positions}
@@ -83,11 +83,11 @@ export const EstimationPDF = ({
/>
</SimpleLayout>
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<SimpleLayout {...commonProps}>
<TransparenzModule pricing={pricing} />
</SimpleLayout>
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<SimpleLayout {...commonProps}>
<ClosingModule />
</SimpleLayout>
</PDFDocument>

View File

@@ -8,58 +8,48 @@ const simpleStyles = StyleSheet.create({
industrialPage: {
padding: 30,
paddingTop: 20,
backgroundColor: '#ffffff',
flexDirection: 'column',
backgroundColor: '#FFFFFF',
fontFamily: 'Outfit',
},
industrialNumber: {
fontSize: 60,
fontWeight: 'bold',
color: '#f1f5f9',
position: 'absolute',
top: -10,
right: 0,
zIndex: -1,
},
industrialSection: {
marginTop: 16,
paddingTop: 12,
flexDirection: 'row',
position: 'relative',
contentView: {
flex: 1,
marginTop: 20,
},
});
interface SimpleLayoutProps {
children: React.ReactNode;
pageNumber?: string;
icon?: string;
headerIcon?: string;
footerLogo?: string;
companyData: any;
bankData?: any;
showDetails?: boolean;
showPageNumber?: boolean;
children: React.ReactNode;
}
export const SimpleLayout = ({
children,
pageNumber,
icon,
export const SimpleLayout: React.FC<SimpleLayoutProps> = ({
headerIcon,
footerLogo,
companyData,
bankData,
showPageNumber = true
}: SimpleLayoutProps) => {
showDetails = false,
showPageNumber = true,
children,
}) => {
return (
<PDFPage size="A4" style={[pdfStyles.page, simpleStyles.industrialPage]}>
<Header icon={icon} showAddress={false} />
{pageNumber && <PDFText style={simpleStyles.industrialNumber}>{pageNumber}</PDFText>}
<PDFView style={simpleStyles.industrialSection}>
<PDFView style={{ width: '100%' }}>
{children}
</PDFView>
<PDFPage size="A4" style={simpleStyles.industrialPage}>
<Header icon={headerIcon} sender={companyData.name} showAddress={false} />
<PDFView style={simpleStyles.contentView}>
{children}
</PDFView>
<Footer
logo={footerLogo}
companyData={companyData}
bankData={bankData}
showDetails={false}
_bankData={bankData}
showDetails={showDetails}
showPageNumber={showPageNumber}
/>
</PDFPage>

View File

@@ -0,0 +1,53 @@
import { renderToFile, Document as PDFDocument, Font } from "@react-pdf/renderer";
import { createElement } from "react";
import { AgbsPDF } from "./components/AgbsPDF.js";
import path from "path";
import fs from "fs";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Standard Font Registration
Font.register({
family: 'Outfit',
fonts: [
{ src: 'Helvetica' },
{ src: 'Helvetica-Bold', fontWeight: 'bold' },
],
});
Font.register({
family: 'Helvetica',
fonts: [
{ src: 'Helvetica' },
{ src: 'Helvetica-Bold', fontWeight: 'bold' },
],
});
async function generate() {
const outDir = path.join(__dirname, "../../../out");
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
const outputPath = path.resolve(outDir, "AGB_Mintel.pdf");
console.log("Generating High-Fidelity AGB PDF...");
const headerIcon = "/Users/marcmintel/Projects/mintel.me/apps/web/src/assets/logo/Icon-White-Transparent.png";
const footerLogo = "/Users/marcmintel/Projects/mintel.me/apps/web/src/assets/logo/Logo-Black-Transparent.png";
// WRAP IN DOCUMENT - MANDATORY FOR standalone rendering
const document = createElement(PDFDocument, {
title: "Allgemeine Geschäftsbedingungen - Marc Mintel",
author: "Marc Mintel",
},
createElement(AgbsPDF, { mode: "full", headerIcon, footerLogo })
);
await renderToFile(document, outputPath);
console.log(`Generated: ${outputPath}`);
}
generate().catch(console.error);

View File

@@ -2,7 +2,7 @@
"name": "people-manager",
"description": "Custom High-Fidelity Management for Directus",
"icon": "extension",
"version": "1.8.15",
"version": "1.8.21",
"type": "module",
"keywords": [
"directus",

View File

@@ -1,7 +1,7 @@
{
"name": "@mintel/thumbnail-generator",
"version": "1.8.15",
"private": true,
"version": "1.8.21",
"private": false,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",

View File

@@ -56,8 +56,8 @@ export class ThumbnailGenerator {
}
}
// Default to the requested nano-banana-pro model unless explicitly provided
const model = options?.model || "google/nano-banana-pro";
// Default to the requested flux-1.1-pro model unless explicitly provided
const model = options?.model || "black-forest-labs/flux-1.1-pro";
const output = await this.replicate.run(model as `${string}/${string}`, {
input: inputPayload,

View File

@@ -1,6 +1,6 @@
{
"name": "@mintel/tsconfig",
"version": "1.8.15",
"version": "1.8.21",
"publishConfig": {
"access": "public",
"registry": "https://npm.infra.mintel.me"

View File

@@ -2,7 +2,7 @@
"name": "unified-dashboard",
"description": "Custom High-Fidelity Management for Directus",
"icon": "extension",
"version": "1.8.15",
"version": "1.8.21",
"type": "module",
"keywords": [
"directus",

14
plan_free.hbs Normal file
View File

@@ -0,0 +1,14 @@
<div class="membership-card free">
<div class="membership-card-content">
<h2 class="membership-card-title">{{t "Free"}}</h2>
<h3 class="membership-card-price"><sup>$</sup>0</h3>
<div class="membership-card-options">
<ul>
<li>Full access to posts for subscribers</li>
<li>Weekly email newsletter</li>
<li>No advertising</li>
</ul>
</div>
</div>
<a href="{{@site.url}}/signup/" class="global-button">{{t "Subscribe now"}}</a>
</div>

16
plan_monthly.hbs Normal file
View File

@@ -0,0 +1,16 @@
<div class="membership-card monthly">
<div class="membership-card-content">
<h2 class="membership-card-title">{{t "Monthly"}}</h2>
<h3 class="membership-card-price">{{price monthly_price currency=currency}}</h3>
<div class="membership-card-options">
<ul>
<li>Full access to all premium posts</li>
<li>Weekly email newsletter</li>
<li>Support independent publishing</li>
<li>Simple, secure card payment</li>
<li>No advertising</li>
</ul>
</div>
</div>
<a href="#" class="global-button" data-members-plan="Monthly">{{t "Subscribe now"}}</a>
</div>

17
plan_yearly.hbs Normal file
View File

@@ -0,0 +1,17 @@
<div class="membership-card yearly">
<div class="membership-card-content">
<h2 class="membership-card-title">{{t "Yearly"}}</h2>
<h3 class="membership-card-price">{{price yearly_price currency=currency}}</h3>
<div class="membership-card-options">
<ul>
<li>Full access to all premium posts</li>
<li>Weekly email newsletter</li>
<li>Support independent publishing</li>
<li>Simple, secure card payment</li>
<li>One easy payment instead of 12!</li>
<li>No advertising</li>
</ul>
</div>
</div>
<a href="#" class="global-button" data-members-plan="Yearly">{{t "Subscribe now"}}</a>
</div>

611
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -116,7 +116,6 @@ if (fs.existsSync(appsDir)) {
}
// Update .env files
updateEnv(".env");
updateEnv(".env.example");
console.log("✨ All versions synced!");