feat: 8 AI providers, rich text editor, agent notifications, UI contrast & font settings
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m25s

- Add DeepSeek, OpenRouter, Mistral, Z.AI, LM Studio as AI providers
  with editable model names via Combobox in admin settings
- Fix OpenRouter broken by normalizeProvider bug in config.ts
- Convert agent-created notes from Markdown to HTML (TipTap rich text)
- Add Notification model + in-app notifications for agent results
- Agent notification click opens the created note directly
- Add note count display on notebook and inbox headers
- Fix checklist toggle in card view (persist state via localCheckItems)
- Add checklist creation option in tabs/list view (dropdown on + button)
- Fix image description ENOENT error with HTTP fallback
- Improve UI contrast across all themes (input, border, checkbox visibility)
- Add font family setting (Inter vs System Default) in Appearance settings
- Fix CSS font-sans variable conflict (removed dead Geist references)
- Update README with new features and 8 providers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-05-01 16:14:07 +02:00
parent 1345403a31
commit dbd49d6fcb
64 changed files with 4124 additions and 1392 deletions

510
scripts/deploy-local.ps1 Normal file
View File

@@ -0,0 +1,510 @@
# ============================================================
# Memento - Local Deploy Script (Windows PowerShell)
# ============================================================
# Usage:
# .\scripts\deploy-local.ps1 # Full setup
# .\scripts\deploy-local.ps1 -EnvOnly # Generate .env only
# .\scripts\deploy-local.ps1 -Start # Start the app (dev or prod)
# .\scripts\deploy-local.ps1 -Migrate # Run database migrations
# .\scripts\deploy-local.ps1 -Install # Install npm dependencies
# ============================================================
[CmdletBinding()]
param(
[switch]$EnvOnly = $false,
[switch]$Start = $false,
[switch]$Migrate = $false,
[switch]$Install = $false,
[switch]$Full = $false
)
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ProjectDir = Split-Path -Parent $ScriptDir
$AppDir = Join-Path $ProjectDir "memento-note"
$McpDir = Join-Path $ProjectDir "mcp-server"
$EnvFile = Join-Path $AppDir ".env"
$McpEnvFile = Join-Path $McpDir ".env"
# -----------------------------------------------------------
# Helpers
# -----------------------------------------------------------
function Write-Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Blue }
function Write-Ok($msg) { Write-Host "[OK] $msg" -ForegroundColor Green }
function Write-Warn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow }
function Write-Err($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red; exit 1 }
function Write-Step($msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan }
function Get-RandomSecret {
$bytes = New-Object byte[] 32
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($bytes)
[Convert]::ToBase64String($bytes)
}
function Ask-Input {
param([string]$Prompt, [string]$Default = "")
if ($Default) {
Write-Host " ? $Prompt [$Default]: " -ForegroundColor Cyan -NoNewline
} else {
Write-Host " ? $Prompt: " -ForegroundColor Cyan -NoNewline
}
$result = Read-Host
if ([string]::IsNullOrWhiteSpace($result)) { $result = $Default }
return $result
}
function Ask-Required {
param([string]$Prompt)
while ($true) {
Write-Host " ? $Prompt (required): " -ForegroundColor Cyan -NoNewline
$result = Read-Host
if (-not [string]::IsNullOrWhiteSpace($result)) { return $result }
Write-Host " This field is required." -ForegroundColor Red
}
}
function Ask-Email {
param([string]$Prompt)
while ($true) {
Write-Host " ? $Prompt (required): " -ForegroundColor Cyan -NoNewline
$result = Read-Host
if (-not [string]::IsNullOrWhiteSpace($result) -and $result -match '^[^@]+@[^@]+\.[^@]+$') {
return $result
}
Write-Host " Please enter a valid email address." -ForegroundColor Red
}
}
# -----------------------------------------------------------
# Check dependencies
# -----------------------------------------------------------
function Check-Deps {
Write-Step "Checking dependencies..."
$missing = @()
# Node.js
$node = Get-Command node -ErrorAction SilentlyContinue
if ($node) {
$nodeVersion = & node -v 2>$null
Write-Ok "Node.js $nodeVersion"
} else {
$missing += "node"
}
# npm
$npm = Get-Command npm -ErrorAction SilentlyContinue
if ($npm) {
$npmVersion = & npm -v 2>$null
Write-Ok "npm $npmVersion"
} else {
$missing += "npm"
}
# PostgreSQL
$psql = Get-Command psql -ErrorAction SilentlyContinue
if ($psql) {
$pgVersion = & psql --version 2>$null | Select-Object -First 1
Write-Ok "PostgreSQL client $pgVersion"
} else {
$missing += "psql"
}
if ($missing.Count -gt 0) {
Write-Host ""
Write-Warn "Missing dependencies: $($missing -join ', ')"
Write-Host ""
Write-Host " Installation instructions:" -ForegroundColor White
Write-Host ""
foreach ($dep in $missing) {
switch ($dep) {
"node" {
Write-Host " Node.js + npm:"
Write-Host " Download: https://nodejs.org/"
Write-Host " Winget: winget install OpenJS.NodeJS.LTS"
Write-Host ""
}
"npm" {
Write-Host " npm:"
Write-Host " Usually included with Node.js installer"
Write-Host ""
}
"psql" {
Write-Host " PostgreSQL:"
Write-Host " Download: https://www.postgresql.org/download/windows/"
Write-Host " Winget: winget install PostgreSQL.PostgreSQL"
Write-Host ""
}
}
}
$cont = Ask-Input "Continue anyway?" "N"
if ($cont -notmatch "^[Yy]") { exit 1 }
}
}
# -----------------------------------------------------------
# Generate .env
# -----------------------------------------------------------
function Generate-Env {
Write-Step "Configuring Memento for local development"
if (Test-Path $EnvFile) {
Write-Warn ".env already exists at $EnvFile"
$confirm = Ask-Input "Overwrite?" "N"
if ($confirm -notmatch "^[Yy]") {
Write-Info "Keeping existing .env"
return
}
}
Write-Host ""
Write-Host " This wizard will guide you through the configuration." -ForegroundColor White
Write-Host " Press Enter to accept defaults in [brackets]." -ForegroundColor White
Write-Host ""
# ---- Database ----
Write-Step "Database configuration"
$dbHost = Ask-Input "PostgreSQL host" "localhost"
$dbPort = Ask-Input "PostgreSQL port" "5432"
$dbName = Ask-Input "PostgreSQL database name" "memento"
$dbUser = Ask-Input "PostgreSQL username" "memento"
$dbPass = Ask-Input "PostgreSQL password" "memento"
$dbUrl = "postgresql://${dbUser}:${dbPass}@${dbHost}:${dbPort}/${dbName}"
# Check DB connectivity
if (Get-Command psql -ErrorAction SilentlyContinue) {
Write-Step "Testing database connection..."
$env:PGPASSWORD = $dbPass
try {
& psql -h $dbHost -p $dbPort -U $dbUser -d $dbName -c "SELECT 1" 2>&1 | Out-Null
Write-Ok "Database connection successful"
} catch {
Write-Warn "Could not connect to database. It may not exist yet."
Write-Host ""
Write-Host " Create it with:"
Write-Host " createdb $dbName"
Write-Host " psql -c `"CREATE USER $dbUser WITH PASSWORD '$dbPass';`""
Write-Host " psql -c `"GRANT ALL PRIVILEGES ON DATABASE $dbName TO $dbUser;`""
Write-Host ""
$cont = Ask-Input "Continue anyway?" "Y"
if ($cont -match "^[Nn]") { exit 1 }
}
Remove-Item Env:PGPASSWORD
}
# ---- Core ----
Write-Step "Core configuration"
$url = Ask-Input "App URL (NEXTAUTH_URL)" "http://localhost:3000"
$secret = Get-RandomSecret
Write-Info "Auto-generated NEXTAUTH_SECRET"
$adminEmail = Ask-Email "Admin email (first user with this email becomes ADMIN)"
$allowReg = Ask-Input "Allow public registration" "true"
if ($allowReg -match "^[Yy]|^[Yy]es|^true|^1") { $allowReg = "true" } else { $allowReg = "false" }
# ---- AI Provider ----
Write-Step "AI Provider configuration"
Write-Host " Choose your AI provider:"
Write-Host " 1) OpenAI"
Write-Host " 2) Ollama (local)"
Write-Host " 3) Custom OpenAI-compatible (OpenRouter, Groq, Together, etc.)"
Write-Host " 4) Skip AI configuration"
Write-Host ""
$aiChoice = Ask-Input "Choice" "4"
$aiTagsProvider = $aiTagsModel = $aiEmbedProvider = $aiEmbedModel = ""
$aiChatProvider = $aiChatModel = $openaiKey = $customKey = $customUrl = $ollamaUrl = ""
switch ($aiChoice) {
"1" {
$aiTagsProvider = "openai"; $aiTagsModel = "gpt-4o-mini"
$aiEmbedProvider = "openai"; $aiEmbedModel = "text-embedding-3-small"
$aiChatProvider = "openai"; $aiChatModel = "gpt-4o-mini"
$openaiKey = Ask-Required "OpenAI API Key"
}
"2" {
$aiTagsProvider = "ollama"; $aiTagsModel = "granite4:latest"
$aiEmbedProvider = "ollama"; $aiEmbedModel = "embeddinggemma:latest"
$aiChatProvider = "ollama"; $aiChatModel = "granite4:latest"
$ollamaUrl = Ask-Input "Ollama base URL" "http://localhost:11434"
}
"3" {
$aiTagsProvider = "custom"
$aiEmbedProvider = "custom"
$aiChatProvider = "custom"
$customKey = Ask-Required "Custom provider API Key"
$customUrl = Ask-Required "Custom provider base URL"
$aiTagsModel = Ask-Input "Model for tags" "gpt-4o-mini"
$aiEmbedModel = Ask-Input "Model for embeddings" "text-embedding-3-small"
$aiChatModel = Ask-Input "Model for chat" "gpt-4o-mini"
}
"4" {
Write-Info "Skipping AI configuration. You can configure it later in the admin panel."
}
default { Write-Err "Invalid choice" }
}
# ---- MCP ----
Write-Step "MCP Server configuration (optional)"
$mcpEnable = Ask-Input "Configure MCP server?" "no"
$mcpEnable = ($mcpEnable -match "^[Yy]|^[Yy]es|^true|^1")
$mcpMode = "sse"; $mcpPort = "3001"; $mcpServerUrl = ""
if ($mcpEnable) {
$mcpMode = Ask-Input "MCP mode (sse or stdio)" "sse"
$mcpPort = Ask-Input "MCP port" "3001"
$mcpServerUrl = "http://localhost:${mcpPort}"
}
# ---- Email ----
Write-Step "Email configuration (optional, needed for password reset)"
Write-Host " Choose an email provider:"
Write-Host " 1) Resend"
Write-Host " 2) SMTP"
Write-Host " 3) Skip"
Write-Host ""
$emailChoice = Ask-Input "Choice" "3"
$resendKey = $smtpHost = $smtpPort = $smtpUser = $smtpPass = $smtpFrom = ""
switch ($emailChoice) {
"1" { $resendKey = Ask-Required "Resend API Key" }
"2" {
$smtpHost = Ask-Required "SMTP Host"
$smtpPort = Ask-Input "SMTP Port" "587"
$smtpUser = Ask-Required "SMTP Username"
$smtpPass = Ask-Required "SMTP Password"
$smtpFrom = Ask-Required "SMTP From email"
}
}
# ---- Write .env ----
Write-Step "Writing .env"
$envContent = @"
# =============================================================================
# Memento - Local Environment (auto-generated by deploy-local.ps1)
# =============================================================================
# Generated on $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
# =============================================================================
# Core
DATABASE_URL="$dbUrl"
NEXTAUTH_SECRET="$secret"
NEXTAUTH_URL="$url"
ADMIN_EMAIL="$adminEmail"
ALLOW_REGISTRATION="$allowReg"
"@
if ($aiChoice -ne "4") {
$envContent += @"
# AI - Tags
AI_PROVIDER_TAGS=$aiTagsProvider
AI_MODEL_TAGS="$aiTagsModel"
# AI - Embeddings
AI_PROVIDER_EMBEDDING=$aiEmbedProvider
AI_MODEL_EMBEDDING="$aiEmbedModel"
# AI - Chat
AI_PROVIDER_CHAT=$aiChatProvider
AI_MODEL_CHAT="$aiChatModel"
"@
if ($openaiKey) { $envContent += "`nOPENAI_API_KEY=`"$openaiKey`"" }
if ($customKey) { $envContent += "`nCUSTOM_OPENAI_API_KEY=`"$customKey`"`nCUSTOM_OPENAI_BASE_URL=`"$customUrl`"" }
if ($ollamaUrl) { $envContent += "`nOLLAMA_BASE_URL=`"$ollamaUrl`"" }
}
if ($mcpEnable) {
$envContent += @"
# MCP Server
MCP_SERVER_MODE="$mcpMode"
MCP_SERVER_URL="$mcpServerUrl"
"@
} else {
$envContent += @"
# MCP Server (disabled)
MCP_SERVER_MODE="disabled"
"@
}
if ($resendKey) { $envContent += "`n`n# Email - Resend`nRESEND_API_KEY=`"$resendKey`"" }
if ($smtpHost) {
$envContent += @"
# Email - SMTP
SMTP_HOST="$smtpHost"
SMTP_PORT="$smtpPort"
SMTP_USER="$smtpUser"
SMTP_PASS="$smtpPass"
SMTP_FROM="$smtpFrom"
"@
}
$envContent | Set-Content -Path $EnvFile -Encoding UTF8
Write-Ok ".env created at $EnvFile"
# MCP server .env
if ($mcpEnable) {
$mcpContent = @"
# =============================================================================
# MCP Server - Local Environment (auto-generated by deploy-local.ps1)
# =============================================================================
DATABASE_URL="$dbUrl"
MCP_MODE="$mcpMode"
PORT="$mcpPort"
APP_BASE_URL="$url"
"@
$mcpContent | Set-Content -Path $McpEnvFile -Encoding UTF8
Write-Ok ".env created at $McpEnvFile"
}
Write-Host ""
Write-Host " Configuration summary:" -ForegroundColor White
Write-Host " URL: $url"
Write-Host " Admin email: $adminEmail"
Write-Host " Database: $dbHost`:$dbPort/$dbName"
Write-Host " AI provider: $(if ($aiChoice -eq '4') { 'skipped' } else { $aiTagsProvider })"
Write-Host " MCP server: $(if ($mcpEnable) { "enabled ($mcpMode)" } else { 'disabled' })"
Write-Host " Email: $(if ($emailChoice -eq '3') { 'skipped' } else { 'configured' })"
}
# -----------------------------------------------------------
# Install dependencies
# -----------------------------------------------------------
function Install-Deps {
Write-Step "Installing dependencies..."
Push-Location $AppDir
if (-not (Test-Path "node_modules")) {
Write-Info "Running npm install..."
& npm install
Write-Ok "Dependencies installed"
} else {
Write-Info "node_modules exists, checking for updates..."
& npm install
}
Pop-Location
}
# -----------------------------------------------------------
# Run migrations
# -----------------------------------------------------------
function Run-Migrations {
if (-not (Test-Path $EnvFile)) {
Write-Err ".env not found. Run: .\scripts\deploy-local.ps1 -EnvOnly"
}
Write-Step "Running database migrations..."
Push-Location $AppDir
try {
& npx prisma migrate deploy 2>&1
} catch {
Write-Warn "migrate deploy failed, trying db push..."
try {
& npx prisma db push --skip-generate 2>&1
} catch {
Write-Err "Database migration failed"
}
}
Write-Ok "Database migrations complete"
Pop-Location
}
# -----------------------------------------------------------
# Start app
# -----------------------------------------------------------
function Start-App {
if (-not (Test-Path $EnvFile)) {
Write-Err ".env not found. Run: .\scripts\deploy-local.ps1 -EnvOnly"
}
Push-Location $AppDir
Write-Host ""
Write-Host " Choose mode:"
Write-Host " 1) Development (npm run dev, with hot reload)"
Write-Host " 2) Production (npm run build + npm start)"
Write-Host ""
$mode = Ask-Input "Choice" "1"
switch ($mode) {
"2" {
Write-Step "Building for production..."
& npm run build
Write-Step "Starting in production mode..."
Write-Info "Starting server on http://localhost:3000"
Write-Info "Press Ctrl+C to stop"
Write-Host ""
& npm start
}
default {
Write-Step "Starting in development mode..."
Write-Info "Starting dev server on http://localhost:3000"
Write-Info "Press Ctrl+C to stop"
Write-Host ""
& npm run dev
}
}
Pop-Location
}
# -----------------------------------------------------------
# Full setup
# -----------------------------------------------------------
function Full-Setup {
Check-Deps
Generate-Env
Install-Deps
Run-Migrations
Write-Host ""
Write-Host "============================================" -ForegroundColor Green
Write-Host " Setup complete!" -ForegroundColor Green
Write-Host "============================================" -ForegroundColor Green
Write-Host ""
$admEmail = (Select-String -Path $EnvFile -Pattern '^ADMIN_EMAIL=(.+)$').Matches[0].Groups[1].Value -replace '"',''
$appUrl = (Select-String -Path $EnvFile -Pattern '^NEXTAUTH_URL=(.+)$').Matches[0].Groups[1].Value -replace '"',''
Write-Host " Next steps:" -ForegroundColor White
Write-Host " 1. Start the app: .\scripts\deploy-local.ps1 -Start"
Write-Host " 2. Open: $appUrl"
Write-Host " 3. Register with: $admEmail" -ForegroundColor White
Write-Host " 4. That account will automatically get ADMIN role"
Write-Host ""
}
# -----------------------------------------------------------
# Main
# -----------------------------------------------------------
if ($EnvOnly) {
Check-Deps
Generate-Env
} elseif ($Start) {
Start-App
} elseif ($Migrate) {
Run-Migrations
} elseif ($Install) {
Install-Deps
} else {
# Default: full setup
Full-Setup
}