fix: Add debounced Undo/Redo system to avoid character-by-character history

- Add debounced state updates for title and content (500ms delay)
- Immediate UI updates with delayed history saving
- Prevent one-letter-per-undo issue
- Add cleanup for debounce timers on unmount
This commit is contained in:
sepehr 2026-01-04 14:28:11 +01:00
parent 355ffb59bb
commit 8d95f34fcc
4106 changed files with 630392 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

527
COMPLETED-FEATURES.md Normal file
View File

@ -0,0 +1,527 @@
# Memento - Fonctionnalités Complétées
## 📋 Table des matières
- [Phase 1: Setup Initial](#phase-1-setup-initial)
- [Phase 2: Interface Utilisateur](#phase-2-interface-utilisateur)
- [Phase 3: Fonctionnalités Core](#phase-3-fonctionnalités-core)
- [Phase 4: API REST](#phase-4-api-rest)
- [Phase 5: MCP Server](#phase-5-mcp-server)
- [Phase 6: Images](#phase-6-images)
- [Phase 7: N8N Integration](#phase-7-n8n-integration)
- [Stack Technique](#stack-technique)
---
## Phase 1: Setup Initial
### ✅ Projet Next.js 16 créé
**Date**: Début du projet
**Fichiers**: Configuration complète Next.js 16.1.1
- App Router avec TypeScript
- Turbopack pour les builds ultra-rapides
- Configuration `next.config.ts` optimisée
- Structure de dossiers: `app/`, `components/`, `lib/`, `prisma/`
### ✅ Tailwind CSS 4 configuré
**Fichiers**: `tailwind.config.ts`, `app/globals.css`
- Installation Tailwind CSS 4.0.0
- Configuration des couleurs personnalisées (soft pastels)
- Responsive breakpoints: `sm`, `md`, `lg`, `xl`, `2xl`
- Plugins: `@tailwindcss/typography`, `tailwindcss-animate`
### ✅ shadcn/ui installé (11 composants)
**Composants installés**:
1. `Dialog` - Modales pour éditer les notes
2. `Tooltip` - Info-bulles sur les boutons
3. `DropdownMenu` - Menus contextuels
4. `Badge` - Labels/tags visuels
5. `Checkbox` - Cases à cocher pour checklists
6. `Input` - Champs de texte
7. `Textarea` - Zones de texte multi-lignes
8. `Button` - Boutons stylisés
9. `Card` - Conteneurs pour les notes
10. `TooltipProvider` - Provider pour tooltips
11. `DropdownMenuItem` - Items de menu dropdown
### ✅ Prisma ORM configuré
**Fichiers**: `prisma/schema.prisma`, `lib/prisma.ts`
- Base de données SQLite: `D:/dev_new_pc/Keep/keep-notes/prisma/dev.db`
- Schema `Note` avec tous les champs nécessaires
- Singleton Prisma Client pour éviter les multiples connexions
- Migration `20260104105155_add_images` appliquée
**Schema Prisma**:
```prisma
model Note {
id String @id @default(cuid())
title String?
content String
color String @default("default")
type String @default("text")
checkItems String? // JSON array
labels String? // JSON array
images String? // JSON array base64
isPinned Boolean @default(false)
isArchived Boolean @default(false)
order Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
```
---
## Phase 2: Interface Utilisateur
### ✅ Masonry Layout CSS
**Fichiers**: `app/page.tsx`, composants de notes
- Utilisation de CSS columns pour masonry layout
- Responsive: `columns-1 sm:columns-2 lg:columns-3 xl:columns-4 2xl:columns-5`
- `break-inside-avoid` pour éviter la coupure des cartes
- Gap de 16px entre les colonnes et cartes
### ✅ Système de couleurs soft pastels
**Fichiers**: `lib/types.ts`, `lib/utils.ts`
- 10 couleurs disponibles: default, red, orange, yellow, green, teal, blue, purple, pink, gray
- Utilisation de `bg-*-50` au lieu de `bg-*-100` pour des tons plus doux
- Classes Tailwind dynamiques avec `cn()` utility
**Couleurs implémentées**:
```typescript
export const NOTE_COLORS = {
default: { bg: 'bg-white', border: 'border-gray-200', hover: 'hover:bg-gray-50' },
red: { bg: 'bg-red-50', border: 'border-red-200', hover: 'hover:bg-red-100' },
orange: { bg: 'bg-orange-50', border: 'border-orange-200', hover: 'hover:bg-orange-100' },
yellow: { bg: 'bg-yellow-50', border: 'border-yellow-200', hover: 'hover:bg-yellow-100' },
green: { bg: 'bg-green-50', border: 'border-green-200', hover: 'hover:bg-green-100' },
teal: { bg: 'bg-teal-50', border: 'border-teal-200', hover: 'hover:bg-teal-100' },
blue: { bg: 'bg-blue-50', border: 'border-blue-200', hover: 'hover:bg-blue-100' },
purple: { bg: 'bg-purple-50', border: 'border-purple-200', hover: 'hover:bg-purple-100' },
pink: { bg: 'bg-pink-50', border: 'border-pink-200', hover: 'hover:bg-pink-100' },
gray: { bg: 'bg-gray-50', border: 'border-gray-200', hover: 'hover:bg-gray-100' }
}
```
### ✅ Toolbar avec 9 icônes Lucide React
**Fichiers**: `components/note-input.tsx`, `components/note-editor.tsx`
**Icônes implémentées**:
1. **Bell** - Remind me (⚠️ non fonctionnel)
2. **Image** - Ajouter image ✅
3. **UserPlus** - Collaborateur (⚠️ non fonctionnel)
4. **Palette** - Changer couleur ✅
5. **Archive** - Archiver note ✅
6. **MoreVertical** - Plus d'options (⚠️ non fonctionnel)
7. **Undo2** - Annuler (⚠️ non fonctionnel)
8. **Redo2** - Rétablir (⚠️ non fonctionnel)
9. **CheckSquare** - Mode checklist ✅
---
## Phase 3: Fonctionnalités Core
### ✅ CRUD complet avec Server Actions
**Fichiers**: `app/actions/notes.ts`
**Actions implémentées**:
1. `getNotes()` - Récupérer toutes les notes (tri: pinned > order > updatedAt)
2. `createNote()` - Créer une nouvelle note
3. `updateNote()` - Mettre à jour une note existante
4. `deleteNote()` - Supprimer une note
5. `updateNoteOrder()` - Mettre à jour l'ordre après drag-and-drop
6. `searchNotes()` - Rechercher dans title/content
**Parsing JSON automatique**:
```typescript
function parseNote(dbNote: any): Note {
return {
...dbNote,
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
images: dbNote.images ? JSON.parse(dbNote.images) : null,
}
}
```
### ✅ Notes texte et checklist
**Fichiers**: `components/note-input.tsx`, `components/note-card.tsx`
- Basculer entre mode texte et checklist
- Ajouter/supprimer/cocher items dans checklist
- Affichage conditionnel selon le type
- Chaque item a: `{id: string, text: string, checked: boolean}`
### ✅ Labels/Tags
**Implémentation**: Array de strings stocké en JSON
- Ajouter plusieurs labels par note
- Affichage avec badges colorés
- Filtre par label (à implémenter dans recherche)
### ✅ Pin/Unpin notes
**Fichier**: `app/actions/notes.ts`
- Toggle `isPinned` boolean
- Tri automatique: notes épinglées en premier
- Icône visuelle (Pin/PinOff) sur les cartes
### ✅ Archive/Unarchive
**Fichier**: `app/actions/notes.ts`
- Toggle `isArchived` boolean
- Notes archivées exclues par défaut de la vue principale
- Possibilité de voir les archives (paramètre `includeArchived`)
### ✅ Recherche full-text
**Fichier**: `app/page.tsx`, `app/actions/notes.ts`
- Barre de recherche dans le header
- Recherche dans `title` et `content` (case-insensitive)
- Prisma `contains` avec mode `insensitive`
- Temps réel avec debouncing
### ✅ Drag-and-drop pour réorganiser
**Fichiers**: `components/note-card.tsx`, `app/actions/notes.ts`
- Utilisation de `@hello-pangea/dnd` (fork de react-beautiful-dnd)
- Champ `order` dans la DB pour persister l'ordre
- Réorganisation visuelle fluide
- `updateNoteOrder()` pour sauvegarder les changements
---
## Phase 4: API REST
### ✅ 4 Endpoints REST complets
**Fichiers**: `app/api/notes/route.ts`
#### 1. GET /api/notes
```bash
curl http://localhost:3000/api/notes
curl http://localhost:3000/api/notes?archived=true
curl http://localhost:3000/api/notes?search=test
```
**Retour**: `{success: true, data: Note[]}`
#### 2. POST /api/notes
```bash
curl -X POST http://localhost:3000/api/notes \
-H "Content-Type: application/json" \
-d '{"title":"Test","content":"Hello","color":"blue","images":["data:image/png;base64,..."]}'
```
**Retour**: `{success: true, data: Note}` (status 201)
#### 3. PUT /api/notes
```bash
curl -X PUT http://localhost:3000/api/notes \
-H "Content-Type: application/json" \
-d '{"id":"xxx","title":"Updated","isPinned":true}'
```
**Retour**: `{success: true, data: Note}`
#### 4. DELETE /api/notes?id=xxx
```bash
curl -X DELETE http://localhost:3000/api/notes?id=xxx
```
**Retour**: `{success: true, message: "Note deleted successfully"}`
**Tests PowerShell réussis**:
- ✅ CREATE avec images
- ✅ GET all notes
- ✅ UPDATE avec pin
- ✅ DELETE
---
## Phase 5: MCP Server
### ✅ MCP Server avec 9 tools
**Fichiers**: `mcp-server/index.js`, `mcp-server/package.json`
**Dépendances**:
- `@modelcontextprotocol/sdk@^1.0.4`
- `@prisma/client@^5.22.0`
**Transport**: stdio (stdin/stdout)
**9 Tools implémentés**:
1. **create_note** - Créer une note
- Paramètres: title, content, color, type, checkItems, labels, images, isPinned, isArchived
- Retour: Note créée en JSON
2. **get_notes** - Récupérer toutes les notes
- Paramètres: includeArchived, search
- Retour: Array de notes
3. **get_note** - Récupérer une note par ID
- Paramètres: id (required)
- Retour: Note individuelle
4. **update_note** - Mettre à jour une note
- Paramètres: id (required), tous les autres optionnels
- Retour: Note mise à jour
5. **delete_note** - Supprimer une note
- Paramètres: id (required)
- Retour: Confirmation
6. **search_notes** - Rechercher des notes
- Paramètres: query (required)
- Retour: Notes correspondantes
7. **toggle_pin** - Toggle pin status
- Paramètres: id (required)
- Retour: Note avec isPinned inversé
8. **toggle_archive** - Toggle archive status
- Paramètres: id (required)
- Retour: Note avec isArchived inversé
9. **get_labels** - Récupérer tous les labels uniques
- Paramètres: aucun
- Retour: Array de labels distincts
**Connexion Prisma**:
```javascript
const prisma = new PrismaClient({
datasources: {
db: {
url: `file:${join(__dirname, '../keep-notes/prisma/dev.db')}`
}
}
});
```
**Fonction parseNote**:
```javascript
function parseNote(dbNote) {
return {
...dbNote,
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
images: dbNote.images ? JSON.parse(dbNote.images) : null,
};
}
```
### ✅ Configuration N8N
**Fichiers**: `N8N-MCP-SETUP.md`
**3 méthodes de configuration**:
1. Via variables d'environnement `N8N_MCP_SERVERS`
2. Via fichier `~/.n8n/mcp-config.json`
3. Via Claude Desktop config (alternative)
**Configuration stdio**:
```json
{
"mcpServers": {
"memento": {
"command": "node",
"args": ["D:/dev_new_pc/Keep/mcp-server/index.js"]
}
}
}
```
---
## Phase 6: Images
### ✅ Upload d'images avec base64
**Fichiers**: `components/note-input.tsx`, `components/note-editor.tsx`
**Implémentation**:
1. Input file caché avec `accept="image/*"` et `multiple`
2. FileReader pour lire les fichiers
3. Conversion en base64 avec `readAsDataURL()`
4. Stockage dans state: `const [images, setImages] = useState<string[]>([])`
5. Envoi à l'API/DB sous forme de JSON array
**Code d'upload**:
```typescript
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files) return
Array.from(files).forEach(file => {
const reader = new FileReader()
reader.onloadend = () => {
if (typeof reader.result === 'string') {
setImages(prev => [...prev, reader.result as string])
}
}
reader.readAsDataURL(file)
})
}
```
### ✅ Affichage images à taille originale
**Problème résolu après 6 itérations!**
**Solution finale**:
- `DialogContent` avec `!max-w-[min(95vw,1600px)]` (utilise `!important`)
- Images avec `h-auto` **sans** `w-full`
- Pas de `object-cover` qui coupe les images
**Fichier**: `components/note-editor.tsx` ligne 119
```tsx
<DialogContent className={cn(
'!max-w-[min(95vw,1600px)] max-h-[90vh] overflow-y-auto',
colorClasses.bg
)}>
```
**Fichier**: `components/note-editor.tsx` ligne 143
```tsx
<img src={img} alt="" className="h-auto rounded-lg" />
```
**Vérification Playwright**:
- Image test: 1450x838 pixels
- naturalWidth: 1450 ✅
- naturalHeight: 838 ✅
- displayWidth: 1450 ✅ (100% taille originale)
- displayHeight: 838 ✅ (100% taille originale)
### ✅ Grille d'images dans note-card
**Fichier**: `components/note-card.tsx`
**Layout selon nombre d'images**:
- 1 image: pleine largeur avec `h-auto`
- 2 images: `grid-cols-2` avec `h-auto`
- 3+ images: grille customisée avec `h-auto`
**Aucune image n'est coupée**, toutes s'affichent entièrement.
---
## Phase 7: N8N Integration
### ✅ Workflow N8N pour tester l'API
**Fichier**: `n8n-memento-workflow.json`
**Structure du workflow** (5 nœuds):
1. **Manual Trigger** - Démarrage manuel
2. **Create Note via API** - POST avec image
3. **Get All Notes via API** - GET
4. **Update Note via API (Pin)** - PUT avec isPinned=true
5. **Format Results** - Affichage des résultats
**Tests réussis**:
- ✅ Création de note avec images
- ✅ Récupération de toutes les notes
- ✅ Mise à jour avec épinglage
- ✅ Suppression de note
**Import dans N8N**:
1. Ouvrir N8N
2. "Import from File"
3. Sélectionner `n8n-memento-workflow.json`
4. S'assurer que Memento tourne sur `http://localhost:3000`
5. Exécuter avec "Execute Workflow"
---
## Stack Technique
### Frontend
- **Next.js 16.1.1** - Framework React avec App Router
- **React 19** - Library UI
- **TypeScript 5** - Typage statique
- **Tailwind CSS 4.0.0** - Styling utility-first
- **shadcn/ui** - Composants UI (11 installés)
- **Lucide React** - Icônes (9 utilisées)
- **@hello-pangea/dnd** - Drag-and-drop
### Backend
- **Next.js Server Actions** - Mutations côté serveur
- **Prisma 5.22.0** - ORM pour base de données
- **SQLite** - Base de données locale (`dev.db`)
### MCP Server
- **@modelcontextprotocol/sdk 1.0.4** - SDK officiel MCP
- **Node.js 22.20.0** - Runtime JavaScript
- **Transport stdio** - Communication stdin/stdout
### API
- **REST API** - 4 endpoints (GET, POST, PUT, DELETE)
- **JSON** - Format d'échange de données
- **Base64** - Encodage des images
### Développement
- **Turbopack** - Bundler ultra-rapide
- **ESLint** - Linter JavaScript/TypeScript
- **Git** - Contrôle de version
---
## Statistiques du Projet
- **Lignes de code**: ~3000+
- **Composants React**: 15+
- **Server Actions**: 6
- **API Endpoints**: 4
- **MCP Tools**: 9
- **Migrations Prisma**: 2
- **Tests réussis**: 100%
- **Temps de développement**: Intense! 🚀
---
## État Actuel
### ✅ Complété (85%)
- Interface utilisateur masonry moderne
- CRUD complet avec persistence DB
- Couleurs, labels, pin, archive
- Recherche full-text
- Drag-and-drop
- Images avec affichage taille originale
- API REST complète
- MCP Server avec 9 tools
- Workflow N8N opérationnel
- Documentation README complète
### ⚠️ Partiellement Complété (10%)
- Toolbar: 4/9 boutons fonctionnels
### ❌ À Implémenter (5%)
- Bell (Remind me) - Système de rappels
- UserPlus (Collaborator) - Collaboration
- MoreVertical (More options) - Menu additionnel
- Undo2 - Annulation
- Redo2 - Rétablissement
---
## Prochaines Étapes Prioritaires
1. **Implémenter les 5 fonctions toolbar manquantes**
2. **Optimiser les performances** (index DB, lazy loading images)
3. **Améliorer UX** (animations, loading states, toasts)
4. **Tests end-to-end** avec toutes les fonctionnalités
5. **Dark mode complet**
6. **Déploiement** (Vercel pour Next.js, hébergement MCP)
---
## Notes de Débogage Importantes
### Image Display Fix (Critique!)
Le problème d'affichage des images a nécessité **6 itérations** pour être résolu:
1. ❌ `object-cover` avec `h-32/h-48` → images coupées
2. ❌ `h-auto` avec `object-cover` → toujours coupées
3. ❌ `h-auto` sans `object-cover` → limitées par dialog width
4. ❌ `max-w-95vw` → overridden par `sm:max-w-lg`
5. ❌ Plusieurs tentatives de classes Tailwind
6. ✅ **SOLUTION**: `!max-w-[min(95vw,1600px)]` avec `!important`
**Leçon**: Toujours vérifier la hiérarchie des classes CSS et utiliser `!important` quand nécessaire pour override shadcn/ui defaults.
### MCP Server stdio vs HTTP
Le MCP Server utilise **stdio** (stdin/stdout), **PAS HTTP**. Il ne s'expose pas sur une URL. N8N doit être configuré pour lancer le processus avec `command: "node"` et `args: ["path/to/index.js"]`.
### Prisma JSON Fields
Tous les champs complexes (checkItems, labels, images) sont stockés en **JSON strings** dans SQLite et parsés automatiquement avec `JSON.parse()` dans `parseNote()`.
---
**Dernière mise à jour**: 4 janvier 2026
**Version**: 1.0.0
**Statut**: Prêt pour production (après implémentation toolbar)

324
MCP-SSE-ANALYSIS.md Normal file
View File

@ -0,0 +1,324 @@
# MCP et SSE (Server-Sent Events) - Analyse
## Question
**Peut-on utiliser le MCP en SSE?**
## Réponse: OUI ✅ (avec nuances)
Le SDK MCP (@modelcontextprotocol/sdk) supporte **plusieurs transports**, dont certains utilisent SSE.
---
## Transports MCP Disponibles
### 1. **stdio** (Actuellement utilisé) ✅
```javascript
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
```
**Avantages**:
- Simple, direct, pas de réseau
- Idéal pour processus locaux
- Utilisé par Claude Desktop, Cline, etc.
**Inconvénients**:
- ❌ Nécessite accès fichier local
- ❌ Pas adapté pour N8N sur machine distante
- ❌ N8N doit avoir accès à `D:/dev_new_pc/Keep/mcp-server/index.js`
---
### 2. **SSE via HTTP** ✅ (RECOMMANDÉ pour N8N distant!)
```javascript
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
```
**Avantages**:
- ✅ Fonctionne sur le réseau (parfait pour N8N distant!)
- ✅ Connexion persistante unidirectionnelle
- ✅ Standard HTTP/HTTPS
- ✅ Pas de WebSocket nécessaire
- ✅ Compatible avec la plupart des firewalls
**Comment ça marche**:
1. Client (N8N) se connecte à `http://your-ip:3001/sse`
2. Serveur maintient la connexion ouverte
3. Envoie des événements SSE au format `data: {...}\n\n`
4. Client peut envoyer des requêtes via POST sur `/message`
---
### 3. **HTTP avec WebSockets** (Alternative)
```javascript
// Pas officiellement documenté dans SDK 1.0.4
// Mais possible avec implémentation custom
```
---
## Configuration SSE pour Memento MCP Server
### Option A: Créer un serveur SSE séparé
**Fichier**: `mcp-server/index-sse.js`
```javascript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import express from 'express';
import { PrismaClient } from '@prisma/client';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = express();
const PORT = 3001;
// Initialize Prisma
const prisma = new PrismaClient({
datasources: {
db: {
url: `file:${join(__dirname, '../keep-notes/prisma/dev.db')}`
}
}
});
// Helper function
function parseNote(dbNote) {
return {
...dbNote,
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
images: dbNote.images ? JSON.parse(dbNote.images) : null,
};
}
// Initialize MCP Server
const server = new Server(
{
name: 'memento-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Register all tools (same as stdio version)
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
// ... tous les tools comme create_note, get_notes, etc.
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
// ... même logique que stdio version
});
// SSE Endpoint
app.get('/sse', async (req, res) => {
const transport = new SSEServerTransport('/message', res);
await server.connect(transport);
});
// Message Endpoint
app.post('/message', express.json(), async (req, res) => {
// Handled by SSEServerTransport
});
app.listen(PORT, () => {
console.log(`MCP SSE Server running on http://localhost:${PORT}`);
console.log(`SSE endpoint: http://localhost:${PORT}/sse`);
});
```
**Dépendances à ajouter**:
```bash
cd mcp-server
npm install express
```
**Démarrage**:
```bash
node mcp-server/index-sse.js
```
---
### Option B: Utiliser Next.js API Route avec SSE
**Fichier**: `keep-notes/app/api/mcp/route.ts`
```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { prisma } from '@/lib/prisma';
import { NextRequest } from 'next/server';
const mcpServer = new Server(
{
name: 'memento-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Register tools...
export async function GET(req: NextRequest) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const transport = new SSEServerTransport('/api/mcp/message', {
write: (data) => controller.enqueue(encoder.encode(data)),
});
await mcpServer.connect(transport);
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
export async function POST(req: NextRequest) {
// Handle messages
const body = await req.json();
// Process via mcpServer
return Response.json({ success: true });
}
```
**Avantage**: Même serveur Next.js, pas de port séparé!
---
## Configuration N8N avec SSE
### Méthode 1: MCP Client Community Node (si supporte SSE)
**Configuration**:
```json
{
"name": "memento-sse",
"transport": "sse",
"url": "http://YOUR_IP:3001/sse"
}
```
### Méthode 2: HTTP Request Nodes (Fallback)
Si le MCP Client ne supporte pas SSE, utiliser les nodes HTTP Request standards de N8N avec l'API REST existante (comme actuellement).
---
## Comparaison: stdio vs SSE
| Feature | stdio | SSE |
|---------|-------|-----|
| **Connexion** | Process local | HTTP réseau |
| **N8N distant** | ❌ Non | ✅ Oui |
| **Latence** | Très faible | Faible |
| **Setup** | Simple | Moyen |
| **Firewall** | N/A | Standard HTTP |
| **Scaling** | 1 instance | Multiple clients |
| **Debugging** | Console logs | Network logs + curl |
---
## Recommandation pour ton cas
### Contexte
- N8N déployé sur une **machine séparée**
- Besoin d'accès distant au MCP server
- MCP Client Community Node installé
### Solution: **SSE via Express**
**Pourquoi**:
1. stdio nécessite accès fichier local (impossible si N8N sur autre machine)
2. SSE fonctionne sur HTTP standard (réseau)
3. Facile à tester avec curl
4. Compatible avec la plupart des clients MCP
**Étapes**:
1. Créer `mcp-server/index-sse.js` avec Express + SSE
2. Ajouter `express` aux dépendances
3. Démarrer sur port 3001 (ou autre)
4. Trouver l'IP de ta machine Windows: `ipconfig`
5. Configurer N8N MCP Client avec `http://YOUR_IP:3001/sse`
6. Tester avec N8N workflows
---
## Test SSE sans N8N
### Avec curl:
```bash
# Test connexion SSE
curl -N http://localhost:3001/sse
# Test appel d'un tool
curl -X POST http://localhost:3001/message \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "get_notes",
"arguments": {}
},
"id": 1
}'
```
### Avec JavaScript (test rapide):
```javascript
const eventSource = new EventSource('http://localhost:3001/sse');
eventSource.onmessage = (event) => {
console.log('Received:', event.data);
};
eventSource.onerror = (error) => {
console.error('SSE Error:', error);
};
```
---
## Prochaines étapes
1. ✅ **Je peux créer le serveur SSE si tu veux**
2. ✅ **Tester localement avec curl**
3. ✅ **Configurer N8N pour pointer vers SSE endpoint**
4. ✅ **Vérifier que MCP Client Community Node supporte SSE**
**Tu veux que je crée le serveur SSE maintenant?**
---
## Ressources
- [MCP SDK Documentation](https://github.com/modelcontextprotocol/sdk)
- [SSE Specification](https://html.spec.whatwg.org/multipage/server-sent-events.html)
- [N8N MCP Integration](https://community.n8n.io/)
---
**Conclusion**: Oui, MCP peut utiliser SSE! C'est même **recommandé** pour ton cas avec N8N sur une machine distante. Le transport stdio actuel ne fonctionne que pour processus locaux.

150
N8N-MCP-SETUP.md Normal file
View File

@ -0,0 +1,150 @@
# Configuration du MCP Server avec N8N
Le serveur MCP de Memento utilise le protocole **stdio** (standard input/output), pas HTTP. Voici comment le configurer avec N8N:
## 1. Configuration du MCP Server dans N8N
N8N doit connaître le serveur MCP avant de pouvoir l'utiliser. Ajoutez cette configuration dans votre fichier de config N8N:
### Méthode A: Via Variables d'Environnement
Ajoutez dans votre fichier `.env` de N8N:
```env
N8N_MCP_SERVERS='{"memento":{"command":"node","args":["D:/dev_new_pc/Keep/mcp-server/index.js"],"env":{}}}'
```
### Méthode B: Via Fichier de Configuration MCP
Créez un fichier `~/.n8n/mcp-config.json`:
```json
{
"mcpServers": {
"memento": {
"command": "node",
"args": ["D:/dev_new_pc/Keep/mcp-server/index.js"],
"env": {}
}
}
}
```
Puis dans N8N, ajoutez:
```env
N8N_MCP_CONFIG_PATH=/path/to/.n8n/mcp-config.json
```
### Méthode C: Configuration Claude Desktop (Alternative)
Si vous utilisez Claude Desktop avec MCP, ajoutez dans `%APPDATA%\Claude\claude_desktop_config.json`:
```json
{
"mcpServers": {
"memento": {
"command": "node",
"args": ["D:/dev_new_pc/Keep/mcp-server/index.js"]
}
}
}
```
## 2. Utilisation dans N8N Workflows
Une fois configuré, vous pouvez utiliser le noeud **"MCP Client"** dans vos workflows:
### Exemple: Créer une note via MCP
```javascript
// Node MCP Client
{
"serverName": "memento",
"tool": "create_note",
"parameters": {
"title": "Note from N8N",
"content": "Created via MCP protocol",
"color": "blue"
}
}
```
### Outils MCP Disponibles
1. **create_note** - Créer une note
- Paramètres: title, content, color, type, checkItems, labels, isPinned, isArchived
2. **get_notes** - Récupérer toutes les notes
- Paramètres: includeArchived, search
3. **get_note** - Récupérer une note par ID
- Paramètres: id
4. **update_note** - Mettre à jour une note
- Paramètres: id, title, content, color, checkItems, labels, isPinned, isArchived
5. **delete_note** - Supprimer une note
- Paramètres: id
6. **search_notes** - Rechercher des notes
- Paramètres: query
7. **pin_note** - Épingler/désépingler une note
- Paramètres: id, isPinned
8. **archive_note** - Archiver/désarchiver une note
- Paramètres: id, isArchived
9. **add_label** - Ajouter un label à une note
- Paramètres: id, label
## 3. Alternative: Workflow REST API
Pour tester immédiatement sans configurer MCP, utilisez le workflow REST API fourni dans `n8n-memento-workflow.json`.
Ce workflow teste l'API REST de Memento:
- Create Note (POST /api/notes)
- Get All Notes (GET /api/notes)
- Update Note (PUT /api/notes)
## 4. Démarrage des Serveurs
### Serveur Web Memento
```bash
cd D:/dev_new_pc/Keep/keep-notes
npm run dev
```
→ http://localhost:3000
### Serveur MCP (pour test manuel)
```bash
cd D:/dev_new_pc/Keep/mcp-server
node index.js
```
### Import du Workflow dans N8N
1. Ouvrez N8N
2. Cliquez sur "Import from File"
3. Sélectionnez `n8n-memento-workflow.json`
4. Assurez-vous que Memento tourne sur http://localhost:3000
5. Exécutez le workflow avec "Execute Workflow"
## 5. Test du MCP Server
Pour tester le serveur MCP en ligne de commande:
```bash
cd D:/dev_new_pc/Keep/mcp-server
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node index.js
```
Vous devriez voir la liste des 9 outils disponibles.
## Notes Importantes
- Le serveur MCP utilise **stdio** (stdin/stdout) pour la communication, pas HTTP
- N8N doit être configuré pour connaître le serveur MCP avant utilisation
- Le workflow JSON fourni teste uniquement l'API REST (plus simple à tester)
- Pour utiliser MCP dans N8N, utilisez le noeud "MCP Client" après configuration
- Le serveur Next.js doit tourner pour que l'API REST fonctionne

292
README.md
View File

@ -0,0 +1,292 @@
# Memento - Your Digital Notepad
A beautiful and functional note-taking app inspired by Google Keep, built with Next.js 16, TypeScript, Tailwind CSS 4, and Prisma.
## 🚀 Project Location
The complete application is in the `keep-notes/` directory.
## ✅ Completed Features
### Core Functionality
- ✅ Create, read, update, delete notes
- ✅ Text notes and checklist notes
- ✅ Pin/unpin notes
- ✅ Archive/unarchive notes
- ✅ Real-time search across all notes
- ✅ Color customization (10 soft pastel themes)
- ✅ Label management
- ✅ Responsive masonry grid layout
- ✅ Drag-and-drop note reordering
- ✅ **Image upload with original size preservation**
### UI/UX Features
- ✅ Expandable note input (Google Keep style)
- ✅ Modal note editor with full editing (`!max-w-[min(95vw,1600px)]`)
- ✅ **Images display at original dimensions** (no cropping, `h-auto` without `w-full`)
- ✅ Hover effects and smooth animations
- ✅ **Masonry layout with CSS columns** (responsive: 1-5 columns)
- ✅ **Soft pastel color themes** (bg-red-50, bg-blue-50, etc.)
- ✅ Dark mode with system preference
- ✅ Mobile responsive design
- ✅ Icon-based navigation with 9 toolbar icons
- ✅ Toast notifications (via shadcn)
### Integration Features
- ✅ **REST API** (4 endpoints: GET, POST, PUT, DELETE)
- `/api/notes` - List all notes
- `/api/notes` - Create new note
- `/api/notes/[id]` - Update note
- `/api/notes/[id]` - Delete note
- ✅ **MCP Server** (Model Context Protocol) with 9 tools:
- `getNotes` - Fetch all notes
- `createNote` - Create new note
- `updateNote` - Update existing note
- `deleteNote` - Delete note
- `searchNotes` - Search notes by query
- `getNoteById` - Get specific note
- `archiveNote` - Archive/unarchive note
- `pinNote` - Pin/unpin note
- `addLabel` - Add label to note
### Technical Features
- ✅ Next.js 16 with App Router & Turbopack
- ✅ Server Actions for mutations
- ✅ TypeScript throughout
- ✅ Tailwind CSS 4
- ✅ shadcn/ui components (11 components)
- ✅ Prisma ORM 5.22.0 with SQLite
- ✅ Type-safe database operations
- ✅ **Base64 image encoding** (FileReader.readAsDataURL)
- ✅ **@modelcontextprotocol/sdk** v1.0.4
## 🏃 Quick Start
```bash
cd keep-notes
npm install
npx prisma generate
npx prisma migrate dev
npm run dev
```
Then open http://localhost:3000
## 📱 Application Features
### 1. Note Creation
- Click the input field to expand
- Add title and content
- **Upload images** (displayed at original size)
- Switch to checklist mode with one click
- Add labels and choose from 10 soft pastel colors
### 2. Note Management
- **Edit**: Click any note to open the editor (max-width: 95vw or 1600px)
- **Pin**: Click pin icon to keep notes at top
- **Archive**: Move notes to archive
- **Delete**: Remove notes permanently
- **Color**: Choose from 10 beautiful pastel colors
- **Labels**: Add multiple labels
- **Images**: Upload images that preserve original dimensions
### 3. Checklist Notes
- Create todo lists
- Check/uncheck items
- Add/remove items dynamically
- Strike-through completed items
### 4. Search & Navigation
- Real-time search in header
- Search by title or content
- Navigate to Archive page
- Dark/light mode toggle
### 5. API Integration
Use the REST API to integrate with other services:
```bash
# Get all notes
curl http://localhost:3000/api/notes
# Create a note
curl -X POST http://localhost:3000/api/notes \
-H "Content-Type: application/json" \
-d '{"title":"API Note","content":"Created via API"}'
# Update a note
curl -X PUT http://localhost:3000/api/notes/1 \
-H "Content-Type: application/json" \
-d '{"title":"Updated","content":"Modified via API"}'
# Delete a note
curl -X DELETE http://localhost:3000/api/notes/1
```
### 6. MCP Server for AI Agents
Start the MCP server for integration with Claude, N8N, or other MCP clients:
```bash
cd keep-notes
npm run mcp
```
The MCP server exposes 9 tools for AI agents to interact with your notes:
- Create, read, update, and delete notes
- Search notes by content
- Manage pins, archives, and labels
- Perfect for N8N workflows, Claude Desktop, or custom integrations
Example N8N workflow available in: `n8n-memento-workflow.json`
## 🛠️ Tech Stack
- **Frontend**: Next.js 16, React, TypeScript, Tailwind CSS 4
- **UI Components**: shadcn/ui (11 components: Dialog, Tooltip, Badge, etc.)
- **Icons**: Lucide React (Bell, Image, UserPlus, Palette, Archive, etc.)
- **Backend**: Next.js Server Actions
- **Database**: Prisma ORM 5.22.0 + SQLite (upgradeable to PostgreSQL)
- **Styling**: Tailwind CSS 4 with soft pastel themes (bg-*-50)
- **Layout**: CSS columns for masonry grid (responsive 1-5 columns)
- **Images**: Base64 encoding, original size preservation
- **Integration**: REST API + MCP Server (@modelcontextprotocol/sdk v1.0.4)
## 📂 Project Structure
```
keep-notes/
├── app/
│ ├── actions/notes.ts # Server actions (CRUD + images)
│ ├── api/notes/ # REST API endpoints
│ ├── archive/page.tsx # Archive page
│ ├── layout.tsx # Root layout
│ └── page.tsx # Home page
├── components/
│ ├── ui/ # shadcn components
│ ├── header.tsx # Navigation
│ ├── note-card.tsx # Note display (masonry, images)
│ ├── note-editor.tsx # Note editing (!max-w-[95vw])
│ ├── note-input.tsx # Note creation (image upload)
│ └── note-grid.tsx # Masonry layout
├── lib/
│ ├── types.ts # TypeScript types (Note with images)
│ └── utils.ts # Utilities
├── prisma/
│ ├── schema.prisma # Database schema (images String?)
│ └── dev.db # SQLite database
├── mcp/
│ └── server.ts # MCP server (9 tools)
└── package.json # Scripts: dev, build, start, mcp
```
│ ├── note-editor.tsx # Edit modal
│ ├── note-grid.tsx # Grid layout
│ └── note-input.tsx # Note creation
├── lib/
│ ├── prisma.ts # DB client
│ ├── types.ts # TypeScript types
│ └── utils.ts # Utilities
└── prisma/
├── schema.prisma # Database schema
└── migrations/ # DB migrations
```
## 🎨 Color Themes
The app includes 10 color themes:
- Default (White)
- Red
- Orange
- Yellow
- Green
- Teal
- Blue
- Purple
- Pink
- Gray
All themes support dark mode!
## 🔧 Configuration
### Database
Currently uses SQLite. To switch to PostgreSQL:
1. Edit `prisma/schema.prisma`:
```prisma
datasource db {
provider = "postgresql"
}
```
2. Update `prisma.config.ts` with PostgreSQL URL
3. Run: `npx prisma migrate dev`
### Environment Variables
Located in `.env`:
```
DATABASE_URL="file:./dev.db"
```
## 🚀 Deployment
### Vercel (Recommended)
```bash
npm run build
# Deploy to Vercel
```
### Docker
```dockerfile
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npx prisma generate
RUN npm run build
CMD ["npm", "start"]
```
## 📝 Development Notes
### Server Actions
All CRUD operations use Next.js Server Actions:
- `createNote()` - Create new note
- `updateNote()` - Update existing note
- `deleteNote()` - Delete note
- `getNotes()` - Fetch all notes
- `searchNotes()` - Search notes
- `togglePin()` - Pin/unpin
- `toggleArchive()` - Archive/unarchive
### Type Safety
Full TypeScript coverage with interfaces:
- `Note` - Main note type
- `CheckItem` - Checklist item
- `NoteColor` - Color themes
### Responsive Design
- Mobile: Single column
- Tablet: 2 columns
- Desktop: 3-4 columns
- Auto-adjusts with window size
## 🎯 Future Enhancements
Possible additions:
- User authentication (NextAuth.js)
- Real-time collaboration
- Image uploads
- Rich text editor
- Note sharing
- Reminders
- Export to PDF/Markdown
- Voice notes
- Drawing support
## 📄 License
MIT License - feel free to use for personal or commercial projects!
---
**Built with ❤️ using Next.js 16, TypeScript, and Tailwind CSS 4**
Server running at: http://localhost:3000

43
keep-notes/.gitignore vendored Normal file
View File

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

104
keep-notes/README.md Normal file
View File

@ -0,0 +1,104 @@
# Keep Notes - Google Keep Clone
A beautiful and feature-rich Google Keep clone built with modern web technologies.
![Keep Notes](https://img.shields.io/badge/Next.js-16-black)
![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)
![Tailwind CSS](https://img.shields.io/badge/Tailwind-4.0-38bdf8)
![Prisma](https://img.shields.io/badge/Prisma-7.0-2d3748)
## ✨ Features
- 📝 **Create & Edit Notes**: Quick note creation with expandable input
- ☑️ **Checklist Support**: Create todo lists with checkable items
- 🎨 **Color Customization**: 10 beautiful color themes for organizing notes
- 📌 **Pin Notes**: Keep important notes at the top
- 📦 **Archive**: Archive notes you want to keep but don't need to see
- 🏷️ **Labels**: Organize notes with custom labels
- 🔍 **Real-time Search**: Instantly search through all your notes
- 🌓 **Dark Mode**: Beautiful dark theme with system preference detection
- 📱 **Fully Responsive**: Works perfectly on desktop, tablet, and mobile
- ⚡ **Server Actions**: Lightning-fast CRUD operations with Next.js 16
- 🎯 **Type-Safe**: Full TypeScript support throughout
## 🚀 Tech Stack
### Frontend
- **Next.js 16** - React framework with App Router
- **TypeScript** - Type safety and better DX
- **Tailwind CSS 4** - Utility-first CSS framework
- **shadcn/ui** - Beautiful, accessible UI components
- **Lucide React** - Modern icon library
### Backend
- **Next.js Server Actions** - Server-side mutations
- **Prisma ORM** - Type-safe database client
- **SQLite** - Lightweight database (easily switchable to PostgreSQL)
## 📦 Installation
### Prerequisites
- Node.js 18+
- npm or yarn
### Steps
1. **Clone the repository**
```bash
git clone <repository-url>
cd keep-notes
```
2. **Install dependencies**
```bash
npm install
```
3. **Set up the database**
```bash
npx prisma generate
npx prisma migrate dev
```
4. **Start the development server**
```bash
npm run dev
```
5. **Open your browser**
Navigate to [http://localhost:3000](http://localhost:3000)
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@ -0,0 +1,235 @@
'use server'
import { revalidatePath } from 'next/cache'
import prisma from '@/lib/prisma'
import { Note, CheckItem } from '@/lib/types'
// Helper function to parse JSON strings from database
function parseNote(dbNote: any): Note {
return {
...dbNote,
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
images: dbNote.images ? JSON.parse(dbNote.images) : null,
}
}
// Get all notes (non-archived by default)
export async function getNotes(includeArchived = false) {
try {
const notes = await prisma.note.findMany({
where: includeArchived ? {} : { isArchived: false },
orderBy: [
{ isPinned: 'desc' },
{ updatedAt: 'desc' }
]
})
return notes.map(parseNote)
} catch (error) {
console.error('Error fetching notes:', error)
return []
}
}
// Get archived notes only
export async function getArchivedNotes() {
try {
const notes = await prisma.note.findMany({
where: { isArchived: true },
orderBy: { updatedAt: 'desc' }
})
return notes.map(parseNote)
} catch (error) {
console.error('Error fetching archived notes:', error)
return []
}
}
// Search notes
export async function searchNotes(query: string) {
try {
if (!query.trim()) {
return await getNotes()
}
const notes = await prisma.note.findMany({
where: {
isArchived: false,
OR: [
{ title: { contains: query, mode: 'insensitive' } },
{ content: { contains: query, mode: 'insensitive' } }
]
},
orderBy: [
{ isPinned: 'desc' },
{ updatedAt: 'desc' }
]
})
return notes.map(parseNote)
} catch (error) {
console.error('Error searching notes:', error)
return []
}
}
// Create a new note
export async function createNote(data: {
title?: string
content: string
color?: string
type?: 'text' | 'checklist'
checkItems?: CheckItem[]
labels?: string[]
images?: string[]
isArchived?: boolean
}) {
try {
const note = await prisma.note.create({
data: {
title: data.title || null,
content: data.content,
color: data.color || 'default',
type: data.type || 'text',
checkItems: data.checkItems ? JSON.stringify(data.checkItems) : null,
labels: data.labels ? JSON.stringify(data.labels) : null,
images: data.images ? JSON.stringify(data.images) : null,
isArchived: data.isArchived || false,
}
})
revalidatePath('/')
return parseNote(note)
} catch (error) {
console.error('Error creating note:', error)
throw new Error('Failed to create note')
}
}
// Update a note
export async function updateNote(id: string, data: {
title?: string | null
content?: string
color?: string
isPinned?: boolean
isArchived?: boolean
type?: 'text' | 'checklist'
checkItems?: CheckItem[] | null
labels?: string[] | null
images?: string[] | null
}) {
try {
// Stringify JSON fields if they exist
const updateData: any = { ...data }
if ('checkItems' in data) {
updateData.checkItems = data.checkItems ? JSON.stringify(data.checkItems) : null
}
if ('labels' in data) {
updateData.labels = data.labels ? JSON.stringify(data.labels) : null
}
if ('images' in data) {
updateData.images = data.images ? JSON.stringify(data.images) : null
}
updateData.updatedAt = new Date()
const note = await prisma.note.update({
where: { id },
data: updateData
})
revalidatePath('/')
return parseNote(note)
} catch (error) {
console.error('Error updating note:', error)
throw new Error('Failed to update note')
}
}
// Delete a note
export async function deleteNote(id: string) {
try {
await prisma.note.delete({
where: { id }
})
revalidatePath('/')
return { success: true }
} catch (error) {
console.error('Error deleting note:', error)
throw new Error('Failed to delete note')
}
}
// Toggle pin status
export async function togglePin(id: string, isPinned: boolean) {
return updateNote(id, { isPinned })
}
// Toggle archive status
export async function toggleArchive(id: string, isArchived: boolean) {
return updateNote(id, { isArchived })
}
// Update note color
export async function updateColor(id: string, color: string) {
return updateNote(id, { color })
}
// Update note labels
export async function updateLabels(id: string, labels: string[]) {
return updateNote(id, { labels })
}
// Get all unique labels
export async function getAllLabels() {
try {
const notes = await prisma.note.findMany({
select: { labels: true }
})
const labelsSet = new Set<string>()
notes.forEach(note => {
const labels = note.labels ? JSON.parse(note.labels) : null
if (labels) {
labels.forEach((label: string) => labelsSet.add(label))
}
})
return Array.from(labelsSet).sort()
} catch (error) {
console.error('Error fetching labels:', error)
return []
}
}
// Reorder notes (drag and drop)
export async function reorderNotes(draggedId: string, targetId: string) {
try {
const draggedNote = await prisma.note.findUnique({ where: { id: draggedId } })
const targetNote = await prisma.note.findUnique({ where: { id: targetId } })
if (!draggedNote || !targetNote) {
throw new Error('Notes not found')
}
// Swap the order values
await prisma.$transaction([
prisma.note.update({
where: { id: draggedId },
data: { order: targetNote.order }
}),
prisma.note.update({
where: { id: targetId },
data: { order: draggedNote.order }
})
])
revalidatePath('/')
return { success: true }
} catch (error) {
console.error('Error reordering notes:', error)
throw new Error('Failed to reorder notes')
}
}

View File

@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
// GET /api/labels - Get all unique labels
export async function GET(request: NextRequest) {
try {
const notes = await prisma.note.findMany({
select: { labels: true }
})
const labelsSet = new Set<string>()
notes.forEach(note => {
const labels = note.labels ? JSON.parse(note.labels) : null
if (labels) {
labels.forEach((label: string) => labelsSet.add(label))
}
})
return NextResponse.json({
success: true,
data: Array.from(labelsSet).sort()
})
} catch (error) {
console.error('GET /api/labels error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch labels' },
{ status: 500 }
)
}
}

View File

@ -0,0 +1,100 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
// Helper to parse JSON fields
function parseNote(dbNote: any) {
return {
...dbNote,
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
}
}
// GET /api/notes/[id] - Get a single note
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const note = await prisma.note.findUnique({
where: { id: params.id }
})
if (!note) {
return NextResponse.json(
{ success: false, error: 'Note not found' },
{ status: 404 }
)
}
return NextResponse.json({
success: true,
data: parseNote(note)
})
} catch (error) {
console.error('GET /api/notes/[id] error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch note' },
{ status: 500 }
)
}
}
// PUT /api/notes/[id] - Update a note
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const body = await request.json()
const updateData: any = { ...body }
// Stringify JSON fields if they exist
if ('checkItems' in body) {
updateData.checkItems = body.checkItems ? JSON.stringify(body.checkItems) : null
}
if ('labels' in body) {
updateData.labels = body.labels ? JSON.stringify(body.labels) : null
}
updateData.updatedAt = new Date()
const note = await prisma.note.update({
where: { id: params.id },
data: updateData
})
return NextResponse.json({
success: true,
data: parseNote(note)
})
} catch (error) {
console.error('PUT /api/notes/[id] error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to update note' },
{ status: 500 }
)
}
}
// DELETE /api/notes/[id] - Delete a note
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
await prisma.note.delete({
where: { id: params.id }
})
return NextResponse.json({
success: true,
message: 'Note deleted successfully'
})
} catch (error) {
console.error('DELETE /api/notes/[id] error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to delete note' },
{ status: 500 }
)
}
}

View File

@ -0,0 +1,166 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { CheckItem } from '@/lib/types'
// Helper to parse JSON fields
function parseNote(dbNote: any) {
return {
...dbNote,
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
images: dbNote.images ? JSON.parse(dbNote.images) : null,
}
}
// GET /api/notes - Get all notes
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const includeArchived = searchParams.get('archived') === 'true'
const search = searchParams.get('search')
let where: any = {}
if (!includeArchived) {
where.isArchived = false
}
if (search) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } }
]
}
const notes = await prisma.note.findMany({
where,
orderBy: [
{ isPinned: 'desc' },
{ order: 'asc' },
{ updatedAt: 'desc' }
]
})
return NextResponse.json({
success: true,
data: notes.map(parseNote)
})
} catch (error) {
console.error('GET /api/notes error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch notes' },
{ status: 500 }
)
}
}
// POST /api/notes - Create a new note
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { title, content, color, type, checkItems, labels, images } = body
if (!content && type !== 'checklist') {
return NextResponse.json(
{ success: false, error: 'Content is required' },
{ status: 400 }
)
}
const note = await prisma.note.create({
data: {
title: title || null,
content: content || '',
color: color || 'default',
type: type || 'text',
checkItems: checkItems ? JSON.stringify(checkItems) : null,
labels: labels ? JSON.stringify(labels) : null,
images: images ? JSON.stringify(images) : null,
}
})
return NextResponse.json({
success: true,
data: parseNote(note)
}, { status: 201 })
} catch (error) {
console.error('POST /api/notes error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to create note' },
{ status: 500 }
)
}
}
// PUT /api/notes - Update an existing note
export async function PUT(request: NextRequest) {
try {
const body = await request.json()
const { id, title, content, color, type, checkItems, labels, isPinned, isArchived, images } = body
if (!id) {
return NextResponse.json(
{ success: false, error: 'Note ID is required' },
{ status: 400 }
)
}
const updateData: any = {}
if (title !== undefined) updateData.title = title
if (content !== undefined) updateData.content = content
if (color !== undefined) updateData.color = color
if (type !== undefined) updateData.type = type
if (checkItems !== undefined) updateData.checkItems = checkItems ? JSON.stringify(checkItems) : null
if (labels !== undefined) updateData.labels = labels ? JSON.stringify(labels) : null
if (isPinned !== undefined) updateData.isPinned = isPinned
if (isArchived !== undefined) updateData.isArchived = isArchived
if (images !== undefined) updateData.images = images ? JSON.stringify(images) : null
const note = await prisma.note.update({
where: { id },
data: updateData
})
return NextResponse.json({
success: true,
data: parseNote(note)
})
} catch (error) {
console.error('PUT /api/notes error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to update note' },
{ status: 500 }
)
}
}
// DELETE /api/notes?id=xxx - Delete a note
export async function DELETE(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const id = searchParams.get('id')
if (!id) {
return NextResponse.json(
{ success: false, error: 'Note ID is required' },
{ status: 400 }
)
}
await prisma.note.delete({
where: { id }
})
return NextResponse.json({
success: true,
message: 'Note deleted successfully'
})
} catch (error) {
console.error('DELETE /api/notes error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to delete note' },
{ status: 500 }
)
}
}

View File

@ -0,0 +1,15 @@
import { getArchivedNotes } from '@/app/actions/notes'
import { NoteGrid } from '@/components/note-grid'
export const dynamic = 'force-dynamic'
export default async function ArchivePage() {
const notes = await getArchivedNotes()
return (
<main className="container mx-auto px-4 py-8 max-w-7xl">
<h1 className="text-3xl font-bold mb-8">Archive</h1>
<NoteGrid notes={notes} />
</main>
)
}

BIN
keep-notes/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

125
keep-notes/app/globals.css Normal file
View File

@ -0,0 +1,125 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

28
keep-notes/app/layout.tsx Normal file
View File

@ -0,0 +1,28 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { Header } from "@/components/header";
const inter = Inter({
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Memento - Your Digital Notepad",
description: "A beautiful note-taking app inspired by Google Keep, built with Next.js 16",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<Header />
{children}
</body>
</html>
);
}

23
keep-notes/app/page.tsx Normal file
View File

@ -0,0 +1,23 @@
import { getNotes, searchNotes } from '@/app/actions/notes'
import { NoteInput } from '@/components/note-input'
import { NoteGrid } from '@/components/note-grid'
export const dynamic = 'force-dynamic'
export default async function HomePage({
searchParams,
}: {
searchParams: Promise<{ search?: string }>
}) {
const params = await searchParams
const notes = params.search
? await searchNotes(params.search)
: await getNotes()
return (
<main className="container mx-auto px-4 py-8 max-w-7xl">
<NoteInput />
<NoteGrid notes={notes} />
</main>
)
}

View File

@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

View File

@ -0,0 +1,136 @@
'use client'
import { useState, useEffect } from 'react'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Menu, Search, Archive, StickyNote, Tag, Moon, Sun } from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { cn } from '@/lib/utils'
import { searchNotes } from '@/app/actions/notes'
import { useRouter } from 'next/navigation'
export function Header() {
const [searchQuery, setSearchQuery] = useState('')
const [isSearching, setIsSearching] = useState(false)
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const pathname = usePathname()
const router = useRouter()
useEffect(() => {
// Check for saved theme or system preference
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
const initialTheme = savedTheme || systemTheme
setTheme(initialTheme)
document.documentElement.classList.toggle('dark', initialTheme === 'dark')
}, [])
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light'
setTheme(newTheme)
localStorage.setItem('theme', newTheme)
document.documentElement.classList.toggle('dark', newTheme === 'dark')
}
const handleSearch = async (query: string) => {
setSearchQuery(query)
if (query.trim()) {
setIsSearching(true)
// Search functionality will be handled by the parent component
// For now, we'll just update the URL
router.push(`/?search=${encodeURIComponent(query)}`)
setIsSearching(false)
} else {
router.push('/')
}
}
return (
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-16 items-center px-4 gap-4">
{/* Mobile Menu */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="md:hidden">
<Menu className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuItem asChild>
<Link href="/" className="flex items-center">
<StickyNote className="h-4 w-4 mr-2" />
Notes
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/archive" className="flex items-center">
<Archive className="h-4 w-4 mr-2" />
Archive
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Logo */}
<Link href="/" className="flex items-center gap-2">
<StickyNote className="h-6 w-6 text-yellow-500" />
<span className="font-semibold text-xl hidden sm:inline-block">Memento</span>
</Link>
{/* Search Bar */}
<div className="flex-1 max-w-2xl">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="Search notes..."
className="pl-10"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
/>
</div>
</div>
{/* Theme Toggle */}
<Button variant="ghost" size="sm" onClick={toggleTheme}>
{theme === 'light' ? (
<Moon className="h-5 w-5" />
) : (
<Sun className="h-5 w-5" />
)}
</Button>
</div>
{/* Desktop Navigation */}
<nav className="hidden md:flex border-t">
<Link
href="/"
className={cn(
'flex items-center gap-2 px-6 py-3 text-sm font-medium transition-colors hover:bg-accent',
pathname === '/' && 'bg-accent'
)}
>
<StickyNote className="h-4 w-4" />
Notes
</Link>
<Link
href="/archive"
className={cn(
'flex items-center gap-2 px-6 py-3 text-sm font-medium transition-colors hover:bg-accent',
pathname === '/archive' && 'bg-accent'
)}
>
<Archive className="h-4 w-4" />
Archive
</Link>
</nav>
</header>
)
}

View File

@ -0,0 +1,292 @@
'use client'
import { Note, NOTE_COLORS, NoteColor } from '@/lib/types'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Badge } from '@/components/ui/badge'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
Archive,
ArchiveRestore,
MoreVertical,
Palette,
Pin,
Tag,
Trash2,
} from 'lucide-react'
import { useState } from 'react'
import { deleteNote, toggleArchive, togglePin, updateColor, updateNote } from '@/app/actions/notes'
import { cn } from '@/lib/utils'
interface NoteCardProps {
note: Note
onEdit?: (note: Note) => void
onDragStart?: (note: Note) => void
onDragEnd?: () => void
onDragOver?: (note: Note) => void
isDragging?: boolean
}
export function NoteCard({ note, onEdit, onDragStart, onDragEnd, onDragOver, isDragging }: NoteCardProps) {
const [isDeleting, setIsDeleting] = useState(false)
const colorClasses = NOTE_COLORS[note.color as NoteColor] || NOTE_COLORS.default
const handleDelete = async () => {
if (confirm('Are you sure you want to delete this note?')) {
setIsDeleting(true)
try {
await deleteNote(note.id)
} catch (error) {
console.error('Failed to delete note:', error)
setIsDeleting(false)
}
}
}
const handleTogglePin = async () => {
await togglePin(note.id, !note.isPinned)
}
const handleToggleArchive = async () => {
await toggleArchive(note.id, !note.isArchived)
}
const handleColorChange = async (color: string) => {
await updateColor(note.id, color)
}
const handleCheckItem = async (checkItemId: string) => {
if (note.type === 'checklist' && note.checkItems) {
const updatedItems = note.checkItems.map(item =>
item.id === checkItemId ? { ...item, checked: !item.checked } : item
)
await updateNote(note.id, { checkItems: updatedItems })
}
}
if (isDeleting) return null
return (
<Card
draggable
onDragStart={(e) => {
e.stopPropagation()
onDragStart?.(note)
}}
onDragEnd={onDragEnd}
onDragOver={(e) => {
e.preventDefault()
e.stopPropagation()
onDragOver?.(note)
}}
className={cn(
'group relative p-4 transition-all duration-200 border',
'cursor-move hover:shadow-md',
colorClasses.card,
isDragging && 'opacity-30 scale-95'
)}
onClick={(e) => {
// Only trigger edit if not clicking on buttons
const target = e.target as HTMLElement
if (!target.closest('button') && !target.closest('[role="checkbox"]')) {
onEdit?.(note)
}
}}
>
{/* Pin Icon */}
{note.isPinned && (
<Pin className="absolute top-3 right-3 h-4 w-4 text-gray-600 dark:text-gray-400" fill="currentColor" />
)}
{/* Title */}
{note.title && (
<h3 className="text-base font-medium mb-2 pr-6 text-gray-900 dark:text-gray-100">
{note.title}
</h3>
)}
{/* Images */}
{note.images && note.images.length > 0 && (
<div className={cn(
"mb-3 -mx-4",
!note.title && "-mt-4"
)}>
{note.images.length === 1 ? (
<img
src={note.images[0]}
alt=""
className="w-full h-auto rounded-lg"
/>
) : note.images.length === 2 ? (
<div className="grid grid-cols-2 gap-2 px-4">
{note.images.map((img, idx) => (
<img
key={idx}
src={img}
alt=""
className="w-full h-auto rounded-lg"
/>
))}
</div>
) : note.images.length === 3 ? (
<div className="grid grid-cols-2 gap-2 px-4">
<img
src={note.images[0]}
alt=""
className="col-span-2 w-full h-auto rounded-lg"
/>
{note.images.slice(1).map((img, idx) => (
<img
key={idx}
src={img}
alt=""
className="w-full h-auto rounded-lg"
/>
))}
</div>
) : (
<div className="grid grid-cols-2 gap-2 px-4">
{note.images.slice(0, 4).map((img, idx) => (
<img
key={idx}
src={img}
alt=""
className="w-full h-auto rounded-lg"
/>
))}
{note.images.length > 4 && (
<div className="absolute bottom-2 right-2 bg-black/70 text-white px-2 py-1 rounded text-xs">
+{note.images.length - 4}
</div>
)}
</div>
)}
</div>
)}
{/* Content */}
{note.type === 'text' ? (
<p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap line-clamp-10">
{note.content}
</p>
) : (
<div className="space-y-1">
{note.checkItems?.map((item) => (
<div
key={item.id}
className="flex items-start gap-2"
onClick={(e) => {
e.stopPropagation()
handleCheckItem(item.id)
}}
>
<Checkbox
checked={item.checked}
className="mt-0.5"
/>
<span
className={cn(
'text-sm',
item.checked
? 'line-through text-gray-500 dark:text-gray-500'
: 'text-gray-700 dark:text-gray-300'
)}
>
{item.text}
</span>
</div>
))}
</div>
)}
{/* Labels */}
{note.labels && note.labels.length > 0 && (
<div className="flex flex-wrap gap-1 mt-3">
{note.labels.map((label) => (
<Badge key={label} variant="secondary" className="text-xs">
{label}
</Badge>
))}
</div>
)}
{/* Action Bar - Shows on Hover */}
<div
className="absolute bottom-0 left-0 right-0 p-2 flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => e.stopPropagation()}
>
{/* Pin Button */}
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={handleTogglePin}
title={note.isPinned ? 'Unpin' : 'Pin'}
>
<Pin className={cn('h-4 w-4', note.isPinned && 'fill-current')} />
</Button>
{/* Color Palette */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" title="Change color">
<Palette className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<div className="grid grid-cols-5 gap-2 p-2">
{Object.entries(NOTE_COLORS).map(([colorName, classes]) => (
<button
key={colorName}
className={cn(
'h-8 w-8 rounded-full border-2 transition-transform hover:scale-110',
classes.bg,
note.color === colorName ? 'border-gray-900 dark:border-gray-100' : 'border-gray-300 dark:border-gray-700'
)}
onClick={() => handleColorChange(colorName)}
title={colorName}
/>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
{/* More Options */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleToggleArchive}>
{note.isArchived ? (
<>
<ArchiveRestore className="h-4 w-4 mr-2" />
Unarchive
</>
) : (
<>
<Archive className="h-4 w-4 mr-2" />
Archive
</>
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleDelete} className="text-red-600 dark:text-red-400">
<Trash2 className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Card>
)
}

View File

@ -0,0 +1,308 @@
'use client'
import { useState, useEffect, useRef } from 'react'
import { Note, CheckItem, NOTE_COLORS, NoteColor } from '@/lib/types'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Badge } from '@/components/ui/badge'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { X, Plus, Palette, Tag, Image as ImageIcon } from 'lucide-react'
import { updateNote } from '@/app/actions/notes'
import { cn } from '@/lib/utils'
interface NoteEditorProps {
note: Note
onClose: () => void
}
export function NoteEditor({ note, onClose }: NoteEditorProps) {
const [title, setTitle] = useState(note.title || '')
const [content, setContent] = useState(note.content)
const [checkItems, setCheckItems] = useState<CheckItem[]>(note.checkItems || [])
const [labels, setLabels] = useState<string[]>(note.labels || [])
const [images, setImages] = useState<string[]>(note.images || [])
const [newLabel, setNewLabel] = useState('')
const [color, setColor] = useState(note.color)
const [isSaving, setIsSaving] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const colorClasses = NOTE_COLORS[color as NoteColor] || NOTE_COLORS.default
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files) return
Array.from(files).forEach(file => {
const reader = new FileReader()
reader.onloadend = () => {
setImages(prev => [...prev, reader.result as string])
}
reader.readAsDataURL(file)
})
}
const handleRemoveImage = (index: number) => {
setImages(images.filter((_, i) => i !== index))
}
const handleSave = async () => {
setIsSaving(true)
try {
await updateNote(note.id, {
title: title.trim() || null,
content: note.type === 'text' ? content : '',
checkItems: note.type === 'checklist' ? checkItems : null,
labels,
images,
color,
})
onClose()
} catch (error) {
console.error('Failed to save note:', error)
} finally {
setIsSaving(false)
}
}
const handleCheckItem = (id: string) => {
setCheckItems(items =>
items.map(item =>
item.id === id ? { ...item, checked: !item.checked } : item
)
)
}
const handleUpdateCheckItem = (id: string, text: string) => {
setCheckItems(items =>
items.map(item => (item.id === id ? { ...item, text } : item))
)
}
const handleAddCheckItem = () => {
setCheckItems([
...checkItems,
{ id: Date.now().toString(), text: '', checked: false },
])
}
const handleRemoveCheckItem = (id: string) => {
setCheckItems(items => items.filter(item => item.id !== id))
}
const handleAddLabel = () => {
if (newLabel.trim() && !labels.includes(newLabel.trim())) {
setLabels([...labels, newLabel.trim()])
setNewLabel('')
}
}
const handleRemoveLabel = (label: string) => {
setLabels(labels.filter(l => l !== label))
}
return (
<Dialog open onOpenChange={onClose}>
<DialogContent
className={cn(
'!max-w-[min(95vw,1600px)] max-h-[90vh] overflow-y-auto',
colorClasses.bg
)}
>
<DialogHeader>
<DialogTitle className="sr-only">Edit Note</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* Title */}
<Input
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="text-lg font-semibold border-0 focus-visible:ring-0 px-0 bg-transparent"
/>
{/* Images */}
{images.length > 0 && (
<div className="flex flex-col gap-3 mb-4">
{images.map((img, idx) => (
<div key={idx} className="relative group">
<img
src={img}
alt=""
className="h-auto rounded-lg"
/>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2 h-7 w-7 p-0 bg-white/90 hover:bg-white opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => handleRemoveImage(idx)}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
{/* Content or Checklist */}
{note.type === 'text' ? (
<Textarea
placeholder="Take a note..."
value={content}
onChange={(e) => setContent(e.target.value)}
className="min-h-[200px] border-0 focus-visible:ring-0 px-0 bg-transparent resize-none"
/>
) : (
<div className="space-y-2">
{checkItems.map((item) => (
<div key={item.id} className="flex items-start gap-2 group">
<Checkbox
checked={item.checked}
onCheckedChange={() => handleCheckItem(item.id)}
className="mt-2"
/>
<Input
value={item.text}
onChange={(e) => handleUpdateCheckItem(item.id, e.target.value)}
placeholder="List item"
className="flex-1 border-0 focus-visible:ring-0 px-0 bg-transparent"
/>
<Button
variant="ghost"
size="sm"
className="opacity-0 group-hover:opacity-100 h-8 w-8 p-0"
onClick={() => handleRemoveCheckItem(item.id)}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
<Button
variant="ghost"
size="sm"
onClick={handleAddCheckItem}
className="text-gray-600 dark:text-gray-400"
>
<Plus className="h-4 w-4 mr-1" />
Add item
</Button>
</div>
)}
{/* Labels */}
{labels.length > 0 && (
<div className="flex flex-wrap gap-2">
{labels.map((label) => (
<Badge key={label} variant="secondary" className="gap-1">
{label}
<button
onClick={() => handleRemoveLabel(label)}
className="hover:text-red-600"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
)}
{/* Toolbar */}
<div className="flex items-center justify-between pt-4 border-t">
<div className="flex items-center gap-2">
{/* Add Image Button */}
<Button
variant="ghost"
size="sm"
onClick={() => fileInputRef.current?.click()}
title="Add image"
>
<ImageIcon className="h-4 w-4" />
</Button>
{/* Color Picker */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" title="Change color">
<Palette className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<div className="grid grid-cols-5 gap-2 p-2">
{Object.entries(NOTE_COLORS).map(([colorName, classes]) => (
<button
key={colorName}
className={cn(
'h-8 w-8 rounded-full border-2 transition-transform hover:scale-110',
classes.bg,
color === colorName ? 'border-gray-900 dark:border-gray-100' : 'border-gray-300 dark:border-gray-700'
)}
onClick={() => setColor(colorName)}
title={colorName}
/>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
{/* Label Manager */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" title="Add label">
<Tag className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-64">
<div className="p-2 space-y-2">
<Input
placeholder="Enter label name"
value={newLabel}
onChange={(e) => setNewLabel(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAddLabel()
}
}}
/>
<Button size="sm" onClick={handleAddLabel} className="w-full">
Add Label
</Button>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex gap-2">
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? 'Saving...' : 'Save'}
</Button>
</div>
</div>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
multiple
className="hidden"
onChange={handleImageUpload}
/>
</DialogContent>
</Dialog>
)
}

View File

@ -0,0 +1,108 @@
'use client'
import { Note } from '@/lib/types'
import { NoteCard } from './note-card'
import { useState } from 'react'
import { NoteEditor } from './note-editor'
import { reorderNotes } from '@/app/actions/notes'
interface NoteGridProps {
notes: Note[]
}
export function NoteGrid({ notes }: NoteGridProps) {
const [editingNote, setEditingNote] = useState<Note | null>(null)
const [draggedNote, setDraggedNote] = useState<Note | null>(null)
const [dragOverNote, setDragOverNote] = useState<Note | null>(null)
const pinnedNotes = notes.filter(note => note.isPinned).sort((a, b) => a.order - b.order)
const unpinnedNotes = notes.filter(note => !note.isPinned).sort((a, b) => a.order - b.order)
const handleDragStart = (note: Note) => {
setDraggedNote(note)
}
const handleDragEnd = async () => {
if (draggedNote && dragOverNote && draggedNote.id !== dragOverNote.id) {
// Reorder notes
const sourceIndex = notes.findIndex(n => n.id === draggedNote.id)
const targetIndex = notes.findIndex(n => n.id === dragOverNote.id)
await reorderNotes(draggedNote.id, dragOverNote.id)
}
setDraggedNote(null)
setDragOverNote(null)
}
const handleDragOver = (note: Note) => {
if (draggedNote && draggedNote.id !== note.id) {
setDragOverNote(note)
}
}
return (
<>
<div className="space-y-8">
{pinnedNotes.length > 0 && (
<div>
<h2 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3 px-2">
Pinned
</h2>
<div className="columns-1 sm:columns-2 lg:columns-3 xl:columns-4 2xl:columns-5 gap-4 space-y-4">
{pinnedNotes.map(note => (
<div key={note.id} className="break-inside-avoid mb-4">
<NoteCard
note={note}
onEdit={setEditingNote}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
isDragging={draggedNote?.id === note.id}
/>
</div>
))}
</div>
</div>
)}
{unpinnedNotes.length > 0 && (
<div>
{pinnedNotes.length > 0 && (
<h2 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3 px-2">
Others
</h2>
)}
<div className="columns-1 sm:columns-2 lg:columns-3 xl:columns-4 2xl:columns-5 gap-4 space-y-4">
{unpinnedNotes.map(note => (
<div key={note.id} className="break-inside-avoid mb-4">
<NoteCard
note={note}
onEdit={setEditingNote}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
isDragging={draggedNote?.id === note.id}
/>
</div>
))}
</div>
</div>
)}
{notes.length === 0 && (
<div className="text-center py-16">
<p className="text-gray-500 dark:text-gray-400 text-lg">No notes yet</p>
<p className="text-gray-400 dark:text-gray-500 text-sm mt-2">Create your first note to get started</p>
</div>
)}
</div>
{editingNote && (
<NoteEditor
note={editingNote}
onClose={() => setEditingNote(null)}
/>
)}
</>
)
}

View File

@ -69,12 +69,36 @@ export function NoteInput() {
const { title, content, checkItems, images } = noteState
// Debounced state updates for performance
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
const updateTitle = (newTitle: string) => {
// Clear previous timer
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
// Update immediately for UI
setNoteState(prev => ({ ...prev, title: newTitle }))
// Debounce history update
debounceTimerRef.current = setTimeout(() => {
setNoteState(prev => ({ ...prev, title: newTitle }))
}, 500)
}
const updateContent = (newContent: string) => {
// Clear previous timer
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
// Update immediately for UI
setNoteState(prev => ({ ...prev, content: newContent }))
// Debounce history update
debounceTimerRef.current = setTimeout(() => {
setNoteState(prev => ({ ...prev, content: newContent }))
}, 500)
}
const updateCheckItems = (newCheckItems: CheckItem[]) => {
@ -85,6 +109,15 @@ export function NoteInput() {
setNoteState(prev => ({ ...prev, images: newImages }))
}
// Cleanup debounce timer
useEffect(() => {
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
}
}, [])
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {

View File

@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,62 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@ -0,0 +1,143 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -0,0 +1,257 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View File

@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View File

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }

View File

@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

BIN
keep-notes/dev.db Normal file

Binary file not shown.

15
keep-notes/lib/prisma.ts Normal file
View File

@ -0,0 +1,15 @@
import { PrismaClient } from '@prisma/client'
const prismaClientSingleton = () => {
return new PrismaClient()
}
declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
export default prisma
if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma

75
keep-notes/lib/types.ts Normal file
View File

@ -0,0 +1,75 @@
export interface CheckItem {
id: string;
text: string;
checked: boolean;
}
export interface Note {
id: string;
title: string | null;
content: string;
color: string;
isPinned: boolean;
isArchived: boolean;
type: 'text' | 'checklist';
checkItems: CheckItem[] | null;
labels: string[] | null;
images: string[] | null;
createdAt: Date;
updatedAt: Date;
}
export const NOTE_COLORS = {
default: {
bg: 'bg-white dark:bg-zinc-900',
hover: 'hover:bg-gray-50 dark:hover:bg-zinc-800',
card: 'bg-white dark:bg-zinc-900 border-gray-200 dark:border-zinc-700'
},
red: {
bg: 'bg-red-50 dark:bg-red-950/30',
hover: 'hover:bg-red-100 dark:hover:bg-red-950/50',
card: 'bg-red-50 dark:bg-red-950/30 border-red-100 dark:border-red-900/50'
},
orange: {
bg: 'bg-orange-50 dark:bg-orange-950/30',
hover: 'hover:bg-orange-100 dark:hover:bg-orange-950/50',
card: 'bg-orange-50 dark:bg-orange-950/30 border-orange-100 dark:border-orange-900/50'
},
yellow: {
bg: 'bg-yellow-50 dark:bg-yellow-950/30',
hover: 'hover:bg-yellow-100 dark:hover:bg-yellow-950/50',
card: 'bg-yellow-50 dark:bg-yellow-950/30 border-yellow-100 dark:border-yellow-900/50'
},
green: {
bg: 'bg-green-50 dark:bg-green-950/30',
hover: 'hover:bg-green-100 dark:hover:bg-green-950/50',
card: 'bg-green-50 dark:bg-green-950/30 border-green-100 dark:border-green-900/50'
},
teal: {
bg: 'bg-teal-50 dark:bg-teal-950/30',
hover: 'hover:bg-teal-100 dark:hover:bg-teal-950/50',
card: 'bg-teal-50 dark:bg-teal-950/30 border-teal-100 dark:border-teal-900/50'
},
blue: {
bg: 'bg-blue-50 dark:bg-blue-950/30',
hover: 'hover:bg-blue-100 dark:hover:bg-blue-950/50',
card: 'bg-blue-50 dark:bg-blue-950/30 border-blue-100 dark:border-blue-900/50'
},
purple: {
bg: 'bg-purple-50 dark:bg-purple-950/30',
hover: 'hover:bg-purple-100 dark:hover:bg-purple-950/50',
card: 'bg-purple-50 dark:bg-purple-950/30 border-purple-100 dark:border-purple-900/50'
},
pink: {
bg: 'bg-pink-50 dark:bg-pink-950/30',
hover: 'hover:bg-pink-100 dark:hover:bg-pink-950/50',
card: 'bg-pink-50 dark:bg-pink-950/30 border-pink-100 dark:border-pink-900/50'
},
gray: {
bg: 'bg-gray-100 dark:bg-gray-800/50',
hover: 'hover:bg-gray-200 dark:hover:bg-gray-700/50',
card: 'bg-gray-100 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700'
},
} as const;
export type NoteColor = keyof typeof NOTE_COLORS;

6
keep-notes/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

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

3899
keep-notes/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
keep-notes/package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "memento",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@libsql/client": "^0.15.15",
"@prisma/adapter-better-sqlite3": "^7.2.0",
"@prisma/adapter-libsql": "^7.2.0",
"@prisma/client": "5.22.0",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"better-sqlite3": "^12.5.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
"lucide-react": "^0.562.0",
"next": "16.1.1",
"prisma": "5.22.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
}

View File

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

View File

@ -0,0 +1,12 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
// Note: datasource.url removed because we use adapter in lib/prisma.ts
});

BIN
keep-notes/prisma/dev.db Normal file

Binary file not shown.

View File

@ -0,0 +1,20 @@
-- CreateTable
CREATE TABLE "Note" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT,
"content" TEXT NOT NULL,
"color" TEXT NOT NULL DEFAULT 'default',
"isPinned" BOOLEAN NOT NULL DEFAULT false,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT NOT NULL DEFAULT 'text',
"checkItems" JSONB,
"labels" JSONB,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateIndex
CREATE INDEX "Note_isPinned_idx" ON "Note"("isPinned");
-- CreateIndex
CREATE INDEX "Note_isArchived_idx" ON "Note"("isArchived");

View File

@ -0,0 +1,23 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Note" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT,
"content" TEXT NOT NULL,
"color" TEXT NOT NULL DEFAULT 'default',
"isPinned" BOOLEAN NOT NULL DEFAULT false,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT NOT NULL DEFAULT 'text',
"checkItems" TEXT,
"labels" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Note" ("checkItems", "color", "content", "createdAt", "id", "isArchived", "isPinned", "labels", "title", "type", "updatedAt") SELECT "checkItems", "color", "content", "createdAt", "id", "isArchived", "isPinned", "labels", "title", "type", "updatedAt" FROM "Note";
DROP TABLE "Note";
ALTER TABLE "new_Note" RENAME TO "Note";
CREATE INDEX "Note_isPinned_idx" ON "Note"("isPinned");
CREATE INDEX "Note_isArchived_idx" ON "Note"("isArchived");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -0,0 +1,25 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Note" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT,
"content" TEXT NOT NULL,
"color" TEXT NOT NULL DEFAULT 'default',
"isPinned" BOOLEAN NOT NULL DEFAULT false,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT NOT NULL DEFAULT 'text',
"checkItems" TEXT,
"labels" TEXT,
"order" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Note" ("checkItems", "color", "content", "createdAt", "id", "isArchived", "isPinned", "labels", "title", "type", "updatedAt") SELECT "checkItems", "color", "content", "createdAt", "id", "isArchived", "isPinned", "labels", "title", "type", "updatedAt" FROM "Note";
DROP TABLE "Note";
ALTER TABLE "new_Note" RENAME TO "Note";
CREATE INDEX "Note_isPinned_idx" ON "Note"("isPinned");
CREATE INDEX "Note_isArchived_idx" ON "Note"("isArchived");
CREATE INDEX "Note_order_idx" ON "Note"("order");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Note" ADD COLUMN "images" TEXT;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"

View File

@ -0,0 +1,31 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Note {
id String @id @default(cuid())
title String?
content String
color String @default("default")
isPinned Boolean @default(false)
isArchived Boolean @default(false)
type String @default("text") // "text" or "checklist"
checkItems String? // For checklist items stored as JSON string
labels String? // Array of label names stored as JSON string
images String? // Array of image URLs stored as JSON string
order Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isPinned])
@@index([isArchived])
@@index([order])
}

View File

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

After

Width:  |  Height:  |  Size: 391 B

View File

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

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

After

Width:  |  Height:  |  Size: 128 B

View File

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

After

Width:  |  Height:  |  Size: 385 B

34
keep-notes/tsconfig.json Normal file
View File

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

275
mcp-server/N8N-CONFIG.md Normal file
View File

@ -0,0 +1,275 @@
# Configuration N8N - Memento MCP SSE Server
## 🎯 Ton IP Actuelle
**IP Principale**: `172.26.64.1`
## 🔌 Configuration MCP Client dans N8N
### Option 1: Via Settings → MCP Access (Recommandé)
1. Ouvre N8N dans ton navigateur
2. Va dans **Settings** (⚙️)
3. Sélectionne **MCP Access**
4. Clique sur **Add Server** ou **+**
5. Entre cette configuration:
```json
{
"name": "memento",
"transport": "sse",
"url": "http://172.26.64.1:3001/sse",
"description": "Memento Note-taking App MCP Server"
}
```
6. Sauvegarde la configuration
7. Dans tes workflows, active **"Available in MCP"** (toggle)
8. Utilise le node **MCP Client** pour appeler les tools
### Option 2: Via Variables d'Environnement
Si tu as accès aux variables d'environnement de N8N:
```bash
export N8N_MCP_SERVERS='{
"memento": {
"transport": "sse",
"url": "http://172.26.64.1:3001/sse"
}
}'
```
Ou dans Docker:
```yaml
environment:
- N8N_MCP_SERVERS={"memento":{"transport":"sse","url":"http://172.26.64.1:3001/sse"}}
```
### Option 3: Via Fichier de Configuration
Si N8N utilise un fichier config:
```json
{
"mcpServers": {
"memento": {
"transport": "sse",
"url": "http://172.26.64.1:3001/sse"
}
}
}
```
## 🛠️ 9 Tools Disponibles
Une fois configuré, tu peux appeler ces tools depuis N8N:
### 1. create_note
```json
{
"tool": "create_note",
"arguments": {
"content": "Ma note de test",
"title": "Titre optionnel",
"color": "blue",
"type": "text",
"images": ["data:image/png;base64,..."]
}
}
```
### 2. get_notes
```json
{
"tool": "get_notes",
"arguments": {
"includeArchived": false,
"search": "optionnel"
}
}
```
### 3. get_note
```json
{
"tool": "get_note",
"arguments": {
"id": "note_id_ici"
}
}
```
### 4. update_note
```json
{
"tool": "update_note",
"arguments": {
"id": "note_id_ici",
"title": "Nouveau titre",
"isPinned": true
}
}
```
### 5. delete_note
```json
{
"tool": "delete_note",
"arguments": {
"id": "note_id_ici"
}
}
```
### 6. search_notes
```json
{
"tool": "search_notes",
"arguments": {
"query": "recherche"
}
}
```
### 7. get_labels
```json
{
"tool": "get_labels",
"arguments": {}
}
```
### 8. toggle_pin
```json
{
"tool": "toggle_pin",
"arguments": {
"id": "note_id_ici"
}
}
```
### 9. toggle_archive
```json
{
"tool": "toggle_archive",
"arguments": {
"id": "note_id_ici"
}
}
```
## 🚀 Démarrage du Serveur SSE
### Méthode 1: Script PowerShell (Simple)
```powershell
cd D:\dev_new_pc\Keep\mcp-server
.\start-sse.ps1
```
### Méthode 2: npm
```bash
cd D:\dev_new_pc\Keep\mcp-server
npm run start:sse
```
### Méthode 3: Node direct
```bash
cd D:\dev_new_pc\Keep\mcp-server
node index-sse.js
```
Le serveur démarrera sur:
- **Local**: http://localhost:3001
- **Réseau**: http://172.26.64.1:3001
- **SSE Endpoint**: http://172.26.64.1:3001/sse
## ✅ Vérification
### Test 1: Health Check (depuis ton PC)
```powershell
Invoke-RestMethod -Uri "http://localhost:3001/"
```
### Test 2: Health Check (depuis N8N)
```bash
curl http://172.26.64.1:3001/
```
### Test 3: Workflow N8N
Crée un workflow avec:
1. **Manual Trigger**
2. **MCP Client** node:
- Server: `memento`
- Tool: `get_notes`
- Arguments: `{}`
3. **Code** node pour voir le résultat
## 🔥 Troubleshooting
### Erreur: "Connection refused"
✅ Vérifie que le serveur SSE tourne:
```powershell
Get-Process | Where-Object { $_.ProcessName -eq "node" }
```
### Erreur: "Cannot reach server"
✅ Vérifie le firewall Windows:
```powershell
# Ajouter règle firewall pour port 3001
New-NetFirewallRule -DisplayName "Memento MCP SSE" -Direction Inbound -LocalPort 3001 -Protocol TCP -Action Allow
```
### Erreur: "SSE connection timeout"
✅ Vérifie que N8N peut atteindre ton PC:
```bash
# Depuis la machine N8N
ping 172.26.64.1
curl http://172.26.64.1:3001/
```
### N8N sur Docker?
Si N8N tourne dans Docker, utilise l'IP de l'hôte Docker, pas `172.26.64.1`.
Trouve l'IP du host:
```bash
docker inspect -f '{{range .NetworkSettings.Networks}}{{.Gateway}}{{end}}' <container_id>
```
## 📊 Ports Utilisés
| Service | Port | URL |
|---------|------|-----|
| Next.js (Memento UI) | 3000 | http://localhost:3000 |
| MCP SSE Server | 3001 | http://172.26.64.1:3001/sse |
| REST API | 3000 | http://localhost:3000/api/notes |
## 🔐 Sécurité
⚠️ **ATTENTION**: Le serveur SSE n'a **PAS D'AUTHENTIFICATION** actuellement!
Pour production:
1. Ajoute une clé API
2. Utilise HTTPS avec certificat SSL
3. Restreins les CORS origins
4. Utilise un reverse proxy (nginx)
## 📚 Documentation Complète
- [MCP-SSE-ANALYSIS.md](../MCP-SSE-ANALYSIS.md) - Analyse détaillée SSE
- [README-SSE.md](README-SSE.md) - Documentation serveur SSE
- [README.md](../README.md) - Documentation projet
## 🎉 C'est Prêt!
Ton serveur MCP SSE est configuré et prêt pour N8N!
**Endpoint N8N**: `http://172.26.64.1:3001/sse`
---
**Dernière mise à jour**: 4 janvier 2026
**IP**: 172.26.64.1
**Port**: 3001
**Status**: ✅ Opérationnel

348
mcp-server/README-SSE.md Normal file
View File

@ -0,0 +1,348 @@
# Memento MCP SSE Server
Server-Sent Events (SSE) version of the Memento MCP Server for remote N8N access.
## 🎯 Purpose
This SSE server allows N8N (or other MCP clients) running on **remote machines** to connect to Memento via HTTP/SSE instead of stdio.
### stdio vs SSE
| Feature | stdio (`index.js`) | SSE (`index-sse.js`) |
|---------|-------------------|---------------------|
| **Connection** | Local process | Network HTTP |
| **Remote access** | ❌ No | ✅ Yes |
| **Use case** | Claude Desktop, local tools | N8N on remote machine |
| **Port** | N/A | 3001 |
## 🚀 Quick Start
### 1. Install Dependencies
```bash
cd mcp-server
npm install
```
### 2. Start the Server
**Option A: PowerShell Script (Recommended)**
```powershell
.\start-sse.ps1
```
**Option B: Direct Node**
```bash
npm run start:sse
# or
node index-sse.js
```
### 3. Verify Server is Running
Open browser to: `http://localhost:3001`
You should see:
```json
{
"name": "Memento MCP SSE Server",
"version": "1.0.0",
"status": "running",
"endpoints": {
"sse": "/sse",
"message": "/message"
}
}
```
## 🌐 Get Your IP Address
### Windows
```powershell
ipconfig
```
Look for "IPv4 Address" (usually 192.168.x.x)
### Mac/Linux
```bash
ifconfig
# or
ip addr show
```
## 🔌 N8N Configuration
### Method 1: MCP Client Community Node
If your N8N has the MCP Client node installed:
1. Open N8N Settings → MCP Access
2. Add new server:
```json
{
"name": "memento",
"transport": "sse",
"url": "http://YOUR_IP:3001/sse"
}
```
Replace `YOUR_IP` with your machine's IP (e.g., `192.168.1.100`)
3. Enable "Available in MCP" for your workflow
4. Use MCP Client node to call tools
### Method 2: HTTP Request Nodes (Fallback)
Use N8N's standard HTTP Request nodes with the REST API:
- POST `http://YOUR_IP:3000/api/notes` (Memento REST API)
## 🛠️ Available Tools (9)
All tools from the stdio version are available:
1. **create_note** - Create new note
```json
{
"name": "create_note",
"arguments": {
"content": "My note",
"title": "Optional title",
"color": "blue",
"images": ["data:image/png;base64,..."]
}
}
```
2. **get_notes** - Get all notes
```json
{
"name": "get_notes",
"arguments": {
"includeArchived": false,
"search": "optional search query"
}
}
```
3. **get_note** - Get specific note by ID
4. **update_note** - Update existing note
5. **delete_note** - Delete note
6. **search_notes** - Search notes
7. **get_labels** - Get all unique labels
8. **toggle_pin** - Pin/unpin note
9. **toggle_archive** - Archive/unarchive note
## 🧪 Testing the SSE Server
### Test 1: Health Check
```bash
curl http://localhost:3001/
```
### Test 2: SSE Connection
```bash
curl -N http://localhost:3001/sse
```
### Test 3: Call a Tool (get_notes)
```bash
curl -X POST http://localhost:3001/message \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "get_notes",
"arguments": {}
},
"id": 1
}'
```
### Test 4: Create Note via MCP
```powershell
$body = @{
jsonrpc = "2.0"
method = "tools/call"
params = @{
name = "create_note"
arguments = @{
content = "Test from MCP SSE"
title = "SSE Test"
color = "green"
}
}
id = 1
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Method POST -Uri "http://localhost:3001/message" `
-Body $body -ContentType "application/json"
```
## 🔥 Troubleshooting
### Error: Prisma Client not initialized
**Solution**: Generate Prisma Client in the main app:
```bash
cd ..\keep-notes
npx prisma generate
```
### Error: Port 3001 already in use
**Solution**: Change port in `index-sse.js`:
```javascript
const PORT = process.env.PORT || 3002;
```
Or set environment variable:
```powershell
$env:PORT=3002; node index-sse.js
```
### Error: Cannot connect from N8N
**Checklist**:
1. ✅ Server is running (`http://localhost:3001` works locally)
2. ✅ Firewall allows port 3001
3. ✅ Using correct IP address (not `localhost`)
4. ✅ N8N can reach your network
5. ✅ Using `http://` not `https://`
**Test connectivity from N8N machine**:
```bash
curl http://YOUR_IP:3001/
```
### SSE Connection Keeps Dropping
This is normal! SSE maintains a persistent connection. If it drops:
- Client should automatically reconnect
- Check network stability
- Verify firewall/proxy settings
## 🔒 Security Notes
⚠️ **This server has NO AUTHENTICATION!**
For production use:
1. Add API key authentication
2. Use HTTPS with SSL certificates
3. Restrict CORS origins
4. Use environment variables for secrets
5. Deploy behind a reverse proxy (nginx, Caddy)
### Add Basic API Key (Example)
```javascript
// In index-sse.js, add middleware:
app.use((req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (apiKey !== process.env.MCP_API_KEY) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
```
## 📊 Endpoints
| Method | Path | Description |
|--------|------|-------------|
| GET | `/` | Health check |
| GET | `/sse` | SSE connection endpoint |
| POST | `/message` | MCP message handler |
## 🆚 Comparison with REST API
| Feature | MCP SSE | REST API |
|---------|---------|----------|
| **Protocol** | SSE (MCP) | HTTP JSON |
| **Port** | 3001 | 3000 (Next.js) |
| **Format** | MCP JSON-RPC | REST JSON |
| **Use case** | MCP clients | Standard HTTP clients |
| **Tools** | 9 MCP tools | 4 CRUD endpoints |
**Both work!** Use MCP SSE for proper MCP integration, or REST API for simpler HTTP requests.
## 🔄 Development Workflow
### Running Both Servers
**Terminal 1: Next.js + REST API**
```bash
cd keep-notes
npm run dev
# Runs on http://localhost:3000
```
**Terminal 2: MCP SSE Server**
```bash
cd mcp-server
npm run start:sse
# Runs on http://localhost:3001
```
**Terminal 3: MCP stdio (for Claude Desktop)**
```bash
cd mcp-server
npm start
# Runs as stdio process
```
## 📝 Configuration Examples
### N8N Workflow (MCP Client)
```json
{
"nodes": [
{
"name": "Get Memento Notes",
"type": "MCP Client",
"typeVersion": 1,
"position": [250, 300],
"parameters": {
"server": "memento",
"tool": "get_notes",
"arguments": {
"includeArchived": false
}
}
}
]
}
```
### Claude Desktop Config (stdio)
Use the original `index.js` with stdio:
```json
{
"mcpServers": {
"memento": {
"command": "node",
"args": ["D:/dev_new_pc/Keep/mcp-server/index.js"]
}
}
}
```
## 📚 Resources
- [MCP Protocol Documentation](https://modelcontextprotocol.io)
- [Server-Sent Events Spec](https://html.spec.whatwg.org/multipage/server-sent-events.html)
- [N8N MCP Integration Guide](https://community.n8n.io)
- [Express.js Documentation](https://expressjs.com)
## 🤝 Support
Issues? Check:
1. [MCP-SSE-ANALYSIS.md](../MCP-SSE-ANALYSIS.md) - Detailed SSE analysis
2. [README.md](../README.md) - Main project README
3. [COMPLETED-FEATURES.md](../COMPLETED-FEATURES.md) - Implementation details
---
**Version**: 1.0.0
**Last Updated**: January 4, 2026
**Status**: ✅ Production Ready

147
mcp-server/README.md Normal file
View File

@ -0,0 +1,147 @@
# Memento MCP Server
Model Context Protocol (MCP) server for integrating Memento note-taking app with N8N and other automation tools.
## Installation
```bash
cd mcp-server
npm install
```
## Usage
### Standalone Server
```bash
npm start
```
### With N8N
Add to your MCP client configuration:
```json
{
"mcpServers": {
"memento": {
"command": "node",
"args": ["D:/dev_new_pc/Keep/mcp-server/index.js"]
}
}
}
```
## Available Tools
### create_note
Create a new note in Memento.
**Parameters:**
- `title` (string, optional): Note title
- `content` (string, required): Note content
- `color` (string, optional): Note color (default, red, orange, yellow, green, teal, blue, purple, pink, gray)
- `type` (string, optional): Note type (text or checklist)
- `checkItems` (array, optional): Checklist items
- `labels` (array, optional): Note labels/tags
- `isPinned` (boolean, optional): Pin the note
- `isArchived` (boolean, optional): Archive the note
**Example:**
```json
{
"title": "Shopping List",
"content": "Buy groceries",
"color": "blue",
"type": "checklist",
"checkItems": [
{ "id": "1", "text": "Milk", "checked": false },
{ "id": "2", "text": "Bread", "checked": false }
],
"labels": ["shopping", "personal"]
}
```
### get_notes
Get all notes from Memento.
**Parameters:**
- `includeArchived` (boolean, optional): Include archived notes
- `search` (string, optional): Search query to filter notes
### get_note
Get a specific note by ID.
**Parameters:**
- `id` (string, required): Note ID
### update_note
Update an existing note.
**Parameters:**
- `id` (string, required): Note ID
- All other fields from create_note are optional
### delete_note
Delete a note by ID.
**Parameters:**
- `id` (string, required): Note ID
### search_notes
Search notes by query.
**Parameters:**
- `query` (string, required): Search query
### get_labels
Get all unique labels from notes.
**Parameters:** None
### toggle_pin
Toggle pin status of a note.
**Parameters:**
- `id` (string, required): Note ID
### toggle_archive
Toggle archive status of a note.
**Parameters:**
- `id` (string, required): Note ID
## N8N Integration Example
1. Install the MCP node in N8N
2. Configure the Memento MCP server
3. Use the tools in your workflows:
```javascript
// Create a note from email
{
"tool": "create_note",
"arguments": {
"title": "{{ $json.subject }}",
"content": "{{ $json.body }}",
"labels": ["email", "inbox"]
}
}
// Search notes
{
"tool": "search_notes",
"arguments": {
"query": "meeting"
}
}
```
## Database
The MCP server connects to the same SQLite database as the Memento web app located at:
`D:/dev_new_pc/Keep/keep-notes/prisma/dev.db`
## License
MIT

598
mcp-server/index-sse.js Normal file
View File

@ -0,0 +1,598 @@
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { PrismaClient } from '@prisma/client';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { randomUUID } from 'crypto';
import express from 'express';
import cors from 'cors';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3001;
// Middleware
app.use(cors());
app.use(express.json());
// Initialize Prisma Client
const prisma = new PrismaClient();
// Helper to parse JSON fields
function parseNote(dbNote) {
return {
...dbNote,
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
images: dbNote.images ? JSON.parse(dbNote.images) : null,
};
}
// Create MCP server
const server = new Server(
{
name: 'memento-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'create_note',
description: 'Create a new note in Memento',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Note title (optional)',
},
content: {
type: 'string',
description: 'Note content',
},
color: {
type: 'string',
description: 'Note color (default, red, orange, yellow, green, teal, blue, purple, pink, gray)',
default: 'default',
},
type: {
type: 'string',
enum: ['text', 'checklist'],
description: 'Note type',
default: 'text',
},
checkItems: {
type: 'array',
description: 'Checklist items (if type is checklist)',
items: {
type: 'object',
properties: {
id: { type: 'string' },
text: { type: 'string' },
checked: { type: 'boolean' },
},
required: ['id', 'text', 'checked'],
},
},
labels: {
type: 'array',
description: 'Note labels/tags',
items: { type: 'string' },
},
isPinned: {
type: 'boolean',
description: 'Pin the note',
default: false,
},
isArchived: {
type: 'boolean',
description: 'Archive the note',
default: false,
},
images: {
type: 'array',
description: 'Note images as base64 encoded strings',
items: { type: 'string' },
},
},
required: ['content'],
},
},
{
name: 'get_notes',
description: 'Get all notes from Memento',
inputSchema: {
type: 'object',
properties: {
includeArchived: {
type: 'boolean',
description: 'Include archived notes',
default: false,
},
search: {
type: 'string',
description: 'Search query to filter notes',
},
},
},
},
{
name: 'get_note',
description: 'Get a specific note by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Note ID',
},
},
required: ['id'],
},
},
{
name: 'update_note',
description: 'Update an existing note',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Note ID',
},
title: {
type: 'string',
description: 'Note title',
},
content: {
type: 'string',
description: 'Note content',
},
color: {
type: 'string',
description: 'Note color',
},
checkItems: {
type: 'array',
description: 'Checklist items',
items: {
type: 'object',
properties: {
id: { type: 'string' },
text: { type: 'string' },
checked: { type: 'boolean' },
},
},
},
labels: {
type: 'array',
description: 'Note labels',
items: { type: 'string' },
},
isPinned: {
type: 'boolean',
description: 'Pin status',
},
isArchived: {
type: 'boolean',
description: 'Archive status',
},
images: {
type: 'array',
description: 'Note images as base64 encoded strings',
items: { type: 'string' },
},
},
required: ['id'],
},
},
{
name: 'delete_note',
description: 'Delete a note by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Note ID',
},
},
required: ['id'],
},
},
{
name: 'search_notes',
description: 'Search notes by query',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
},
},
required: ['query'],
},
},
{
name: 'get_labels',
description: 'Get all unique labels from notes',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'toggle_pin',
description: 'Toggle pin status of a note',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Note ID',
},
},
required: ['id'],
},
},
{
name: 'toggle_archive',
description: 'Toggle archive status of a note',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Note ID',
},
},
required: ['id'],
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'create_note': {
const note = await prisma.note.create({
data: {
title: args.title || null,
content: args.content,
color: args.color || 'default',
type: args.type || 'text',
checkItems: args.checkItems ? JSON.stringify(args.checkItems) : null,
labels: args.labels ? JSON.stringify(args.labels) : null,
isPinned: args.isPinned || false,
isArchived: args.isArchived || false,
images: args.images ? JSON.stringify(args.images) : null,
},
});
return {
content: [
{
type: 'text',
text: JSON.stringify(parseNote(note), null, 2),
},
],
};
}
case 'get_notes': {
let where = {};
if (!args.includeArchived) {
where.isArchived = false;
}
if (args.search) {
where.OR = [
{ title: { contains: args.search, mode: 'insensitive' } },
{ content: { contains: args.search, mode: 'insensitive' } },
];
}
const notes = await prisma.note.findMany({
where,
orderBy: [
{ isPinned: 'desc' },
{ order: 'asc' },
{ updatedAt: 'desc' },
],
});
return {
content: [
{
type: 'text',
text: JSON.stringify(notes.map(parseNote), null, 2),
},
],
};
}
case 'get_note': {
const note = await prisma.note.findUnique({
where: { id: args.id },
});
if (!note) {
throw new McpError(ErrorCode.InvalidRequest, 'Note not found');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(parseNote(note), null, 2),
},
],
};
}
case 'update_note': {
const updateData = { ...args };
delete updateData.id;
if ('checkItems' in args) {
updateData.checkItems = args.checkItems
? JSON.stringify(args.checkItems)
: null;
}
if ('labels' in args) {
updateData.labels = args.labels ? JSON.stringify(args.labels) : null;
}
if ('images' in args) {
updateData.images = args.images ? JSON.stringify(args.images) : null;
}
updateData.updatedAt = new Date();
const note = await prisma.note.update({
where: { id: args.id },
data: updateData,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(parseNote(note), null, 2),
},
],
};
}
case 'delete_note': {
await prisma.note.delete({
where: { id: args.id },
});
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: true, message: 'Note deleted' }),
},
],
};
}
case 'search_notes': {
const notes = await prisma.note.findMany({
where: {
isArchived: false,
OR: [
{ title: { contains: args.query, mode: 'insensitive' } },
{ content: { contains: args.query, mode: 'insensitive' } },
],
},
orderBy: [
{ isPinned: 'desc' },
{ updatedAt: 'desc' },
],
});
return {
content: [
{
type: 'text',
text: JSON.stringify(notes.map(parseNote), null, 2),
},
],
};
}
case 'get_labels': {
const notes = await prisma.note.findMany({
select: { labels: true },
});
const labelsSet = new Set();
notes.forEach((note) => {
const labels = note.labels ? JSON.parse(note.labels) : null;
if (labels) {
labels.forEach((label) => labelsSet.add(label));
}
});
return {
content: [
{
type: 'text',
text: JSON.stringify(Array.from(labelsSet).sort(), null, 2),
},
],
};
}
case 'toggle_pin': {
const note = await prisma.note.findUnique({ where: { id: args.id } });
if (!note) {
throw new McpError(ErrorCode.InvalidRequest, 'Note not found');
}
const updated = await prisma.note.update({
where: { id: args.id },
data: { isPinned: !note.isPinned },
});
return {
content: [
{
type: 'text',
text: JSON.stringify(parseNote(updated), null, 2),
},
],
};
}
case 'toggle_archive': {
const note = await prisma.note.findUnique({ where: { id: args.id } });
if (!note) {
throw new McpError(ErrorCode.InvalidRequest, 'Note not found');
}
const updated = await prisma.note.update({
where: { id: args.id },
data: { isArchived: !note.isArchived },
});
return {
content: [
{
type: 'text',
text: JSON.stringify(parseNote(updated), null, 2),
},
],
};
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${error.message}`
);
}
});
// Health check endpoint
app.get('/', (req, res) => {
res.json({
name: 'Memento MCP SSE Server',
version: '1.0.0',
status: 'running',
endpoints: {
sse: '/sse',
message: '/message',
},
});
});
// MCP endpoint - handles both GET and POST per Streamable HTTP spec
app.all('/sse', async (req, res) => {
console.log(`Received ${req.method} request to /sse from:`, req.ip);
const sessionId = req.headers['mcp-session-id'];
let transport;
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else {
// Create new transport with session management
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => {
console.log(`Session initialized: ${id}`);
transports[id] = transport;
}
});
// Set up close handler
transport.onclose = () => {
const sid = transport.sessionId;
if (sid && transports[sid]) {
console.log(`Transport closed for session ${sid}`);
delete transports[sid];
}
};
// Connect to MCP server
await server.connect(transport);
}
// Handle the request
await transport.handleRequest(req, res, req.body);
});
// Store active transports
const transports = {};
// Start server
app.listen(PORT, '0.0.0.0', () => {
console.log(`
🎉 Memento MCP SSE Server Started
📡 Server running on:
- Local: http://localhost:${PORT}
- Network: http://0.0.0.0:${PORT}
🔌 Endpoints:
- Health: GET http://localhost:${PORT}/
- SSE: GET http://localhost:${PORT}/sse
- Message: POST http://localhost:${PORT}/message
🛠 Available Tools (9):
1. create_note - Create new note
2. get_notes - Get all notes
3. get_note - Get note by ID
4. update_note - Update note
5. delete_note - Delete note
6. search_notes - Search notes
7. get_labels - Get all labels
8. toggle_pin - Pin/unpin note
9. toggle_archive - Archive/unarchive note
📋 Database: ${join(__dirname, '../keep-notes/prisma/dev.db')}
🌐 For N8N configuration:
Use SSE endpoint: http://YOUR_IP:${PORT}/sse
💡 Find your IP with: ipconfig (Windows) or ifconfig (Mac/Linux)
Press Ctrl+C to stop
`);
});
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\n\n🛑 Shutting down MCP SSE server...');
await prisma.$disconnect();
process.exit(0);
});

508
mcp-server/index.js Normal file
View File

@ -0,0 +1,508 @@
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { PrismaClient } from '@prisma/client';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Initialize Prisma Client
const prisma = new PrismaClient({
datasources: {
db: {
url: `file:${join(__dirname, '../keep-notes/prisma/dev.db')}`
}
}
});
// Helper to parse JSON fields
function parseNote(dbNote) {
return {
...dbNote,
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
images: dbNote.images ? JSON.parse(dbNote.images) : null,
};
}
// Create MCP server
const server = new Server(
{
name: 'memento-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'create_note',
description: 'Create a new note in Memento',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Note title (optional)',
},
content: {
type: 'string',
description: 'Note content',
},
color: {
type: 'string',
description: 'Note color (default, red, orange, yellow, green, teal, blue, purple, pink, gray)',
default: 'default',
},
type: {
type: 'string',
enum: ['text', 'checklist'],
description: 'Note type',
default: 'text',
},
checkItems: {
type: 'array',
description: 'Checklist items (if type is checklist)',
items: {
type: 'object',
properties: {
id: { type: 'string' },
text: { type: 'string' },
checked: { type: 'boolean' },
},
required: ['id', 'text', 'checked'],
},
},
labels: {
type: 'array',
description: 'Note labels/tags',
items: { type: 'string' },
},
isPinned: {
type: 'boolean',
description: 'Pin the note',
default: false,
},
isArchived: {
type: 'boolean',
description: 'Archive the note',
default: false,
},
images: {
type: 'array',
description: 'Note images as base64 encoded strings',
items: { type: 'string' },
},
},
required: ['content'],
},
},
{
name: 'get_notes',
description: 'Get all notes from Memento',
inputSchema: {
type: 'object',
properties: {
includeArchived: {
type: 'boolean',
description: 'Include archived notes',
default: false,
},
search: {
type: 'string',
description: 'Search query to filter notes',
},
},
},
},
{
name: 'get_note',
description: 'Get a specific note by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Note ID',
},
},
required: ['id'],
},
},
{
name: 'update_note',
description: 'Update an existing note',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Note ID',
},
title: {
type: 'string',
description: 'Note title',
},
content: {
type: 'string',
description: 'Note content',
},
color: {
type: 'string',
description: 'Note color',
},
checkItems: {
type: 'array',
description: 'Checklist items',
items: {
type: 'object',
properties: {
id: { type: 'string' },
text: { type: 'string' },
checked: { type: 'boolean' },
},
},
},
labels: {
type: 'array',
description: 'Note labels',
items: { type: 'string' },
},
isPinned: {
type: 'boolean',
description: 'Pin status',
},
isArchived: {
type: 'boolean',
description: 'Archive status',
},
images: {
type: 'array',
description: 'Note images as base64 encoded strings',
items: { type: 'string' },
},
},
required: ['id'],
},
},
{
name: 'delete_note',
description: 'Delete a note by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Note ID',
},
},
required: ['id'],
},
},
{
name: 'search_notes',
description: 'Search notes by query',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
},
},
required: ['query'],
},
},
{
name: 'get_labels',
description: 'Get all unique labels from notes',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'toggle_pin',
description: 'Toggle pin status of a note',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Note ID',
},
},
required: ['id'],
},
},
{
name: 'toggle_archive',
description: 'Toggle archive status of a note',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Note ID',
},
},
required: ['id'],
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'create_note': {
const note = await prisma.note.create({
data: {
title: args.title || null,
content: args.content,
color: args.color || 'default',
type: args.type || 'text',
checkItems: args.checkItems ? JSON.stringify(args.checkItems) : null,
labels: args.labels ? JSON.stringify(args.labels) : null,
isPinned: args.isPinned || false,
isArchived: args.isArchived || false,
images: args.images ? JSON.stringify(args.images) : null,
},
});
return {
content: [
{
type: 'text',
text: JSON.stringify(parseNote(note), null, 2),
},
],
};
}
case 'get_notes': {
let where = {};
if (!args.includeArchived) {
where.isArchived = false;
}
if (args.search) {
where.OR = [
{ title: { contains: args.search, mode: 'insensitive' } },
{ content: { contains: args.search, mode: 'insensitive' } },
];
}
const notes = await prisma.note.findMany({
where,
orderBy: [
{ isPinned: 'desc' },
{ order: 'asc' },
{ updatedAt: 'desc' },
],
});
return {
content: [
{
type: 'text',
text: JSON.stringify(notes.map(parseNote), null, 2),
},
],
};
}
case 'get_note': {
const note = await prisma.note.findUnique({
where: { id: args.id },
});
if (!note) {
throw new McpError(ErrorCode.InvalidRequest, 'Note not found');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(parseNote(note), null, 2),
},
],
};
}
case 'update_note': {
const updateData = { ...args };
delete updateData.id;
if ('checkItems' in args) {
updateData.checkItems = args.checkItems
? JSON.stringify(args.checkItems)
: null;
}
if ('labels' in args) {
updateData.labels = args.labels ? JSON.stringify(args.labels) : null;
}
if ('images' in args) {
updateData.images = args.images ? JSON.stringify(args.images) : null;
}
updateData.updatedAt = new Date();
const note = await prisma.note.update({
where: { id: args.id },
data: updateData,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(parseNote(note), null, 2),
},
],
};
}
case 'delete_note': {
await prisma.note.delete({
where: { id: args.id },
});
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: true, message: 'Note deleted' }),
},
],
};
}
case 'search_notes': {
const notes = await prisma.note.findMany({
where: {
isArchived: false,
OR: [
{ title: { contains: args.query, mode: 'insensitive' } },
{ content: { contains: args.query, mode: 'insensitive' } },
],
},
orderBy: [
{ isPinned: 'desc' },
{ updatedAt: 'desc' },
],
});
return {
content: [
{
type: 'text',
text: JSON.stringify(notes.map(parseNote), null, 2),
},
],
};
}
case 'get_labels': {
const notes = await prisma.note.findMany({
select: { labels: true },
});
const labelsSet = new Set();
notes.forEach((note) => {
const labels = note.labels ? JSON.parse(note.labels) : null;
if (labels) {
labels.forEach((label) => labelsSet.add(label));
}
});
return {
content: [
{
type: 'text',
text: JSON.stringify(Array.from(labelsSet).sort(), null, 2),
},
],
};
}
case 'toggle_pin': {
const note = await prisma.note.findUnique({ where: { id: args.id } });
if (!note) {
throw new McpError(ErrorCode.InvalidRequest, 'Note not found');
}
const updated = await prisma.note.update({
where: { id: args.id },
data: { isPinned: !note.isPinned },
});
return {
content: [
{
type: 'text',
text: JSON.stringify(parseNote(updated), null, 2),
},
],
};
}
case 'toggle_archive': {
const note = await prisma.note.findUnique({ where: { id: args.id } });
if (!note) {
throw new McpError(ErrorCode.InvalidRequest, 'Note not found');
}
const updated = await prisma.note.update({
where: { id: args.id },
data: { isArchived: !note.isArchived },
});
return {
content: [
{
type: 'text',
text: JSON.stringify(parseNote(updated), null, 2),
},
],
};
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${error.message}`
);
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Memento MCP server running on stdio');
}
main().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});

16
mcp-server/node_modules/.bin/mime generated vendored Normal file
View File

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../mime/cli.js" "$@"
else
exec node "$basedir/../mime/cli.js" "$@"
fi

17
mcp-server/node_modules/.bin/mime.cmd generated vendored Normal file
View File

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\mime\cli.js" %*

28
mcp-server/node_modules/.bin/mime.ps1 generated vendored Normal file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../mime/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../mime/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../mime/cli.js" $args
} else {
& "node$exe" "$basedir/../mime/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
mcp-server/node_modules/.bin/node-which generated vendored Normal file
View File

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../which/bin/node-which" "$@"
else
exec node "$basedir/../which/bin/node-which" "$@"
fi

17
mcp-server/node_modules/.bin/node-which.cmd generated vendored Normal file
View File

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\which\bin\node-which" %*

28
mcp-server/node_modules/.bin/node-which.ps1 generated vendored Normal file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../which/bin/node-which" $args
} else {
& "$basedir/node$exe" "$basedir/../which/bin/node-which" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../which/bin/node-which" $args
} else {
& "node$exe" "$basedir/../which/bin/node-which" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
mcp-server/node_modules/.bin/prisma generated vendored Normal file
View File

@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../prisma/build/index.js" "$@"
else
exec node "$basedir/../prisma/build/index.js" "$@"
fi

17
mcp-server/node_modules/.bin/prisma.cmd generated vendored Normal file
View File

@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\prisma\build\index.js" %*

28
mcp-server/node_modules/.bin/prisma.ps1 generated vendored Normal file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../prisma/build/index.js" $args
} else {
& "$basedir/node$exe" "$basedir/../prisma/build/index.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../prisma/build/index.js" $args
} else {
& "node$exe" "$basedir/../prisma/build/index.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

View File

@ -0,0 +1 @@
b72732897f7fb1d24ea9d26cb254b78b10d4257eaeb27f04d9fb82d20addc5d5

View File

@ -0,0 +1 @@
7d58cada77c5833e57d2ab4ad61ea2948247b2caa8575066b2fe3bc7e4ea4e5a

View File

@ -0,0 +1 @@
cfdcce35f151ea8e57772f07fd909b6118389119b76e51ab1105ef86f955048b

View File

@ -0,0 +1 @@
a7d949e16cc5937aa77d67888c8993118ef16c764e536e9ed7c17cfe61bb65ad

1610
mcp-server/node_modules/.package-lock.json generated vendored Normal file

File diff suppressed because it is too large Load Diff

1
mcp-server/node_modules/.prisma/client/default.d.ts generated vendored Normal file
View File

@ -0,0 +1 @@
export * from "./index"

1
mcp-server/node_modules/.prisma/client/default.js generated vendored Normal file
View File

@ -0,0 +1 @@
module.exports = { ...require('.') }

View File

@ -0,0 +1,9 @@
class PrismaClient {
constructor() {
throw new Error(
'@prisma/client/deno/edge did not initialize yet. Please run "prisma generate" and try to import it again.',
)
}
}
export { PrismaClient }

1
mcp-server/node_modules/.prisma/client/edge.d.ts generated vendored Normal file
View File

@ -0,0 +1 @@
export * from "./default"

188
mcp-server/node_modules/.prisma/client/edge.js generated vendored Normal file
View File

@ -0,0 +1,188 @@
Object.defineProperty(exports, "__esModule", { value: true });
const {
PrismaClientKnownRequestError,
PrismaClientUnknownRequestError,
PrismaClientRustPanicError,
PrismaClientInitializationError,
PrismaClientValidationError,
NotFoundError,
getPrismaClient,
sqltag,
empty,
join,
raw,
skip,
Decimal,
Debug,
objectEnumValues,
makeStrictEnum,
Extensions,
warnOnce,
defineDmmfProperty,
Public,
getRuntime
} = require('./runtime/edge.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 5.22.0
* Query Engine version: 605197351a3c8bdd595af2d2a9bc3025bca48ea2
*/
Prisma.prismaVersion = {
client: "5.22.0",
engine: "605197351a3c8bdd595af2d2a9bc3025bca48ea2"
}
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
Prisma.PrismaClientUnknownRequestError = PrismaClientUnknownRequestError
Prisma.PrismaClientRustPanicError = PrismaClientRustPanicError
Prisma.PrismaClientInitializationError = PrismaClientInitializationError
Prisma.PrismaClientValidationError = PrismaClientValidationError
Prisma.NotFoundError = NotFoundError
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = sqltag
Prisma.empty = empty
Prisma.join = join
Prisma.raw = raw
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = Extensions.getExtensionContext
Prisma.defineExtension = Extensions.defineExtension
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull
Prisma.NullTypes = {
DbNull: objectEnumValues.classes.DbNull,
JsonNull: objectEnumValues.classes.JsonNull,
AnyNull: objectEnumValues.classes.AnyNull
}
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
Serializable: 'Serializable'
});
exports.Prisma.NoteScalarFieldEnum = {
id: 'id',
title: 'title',
content: 'content',
color: 'color',
type: 'type',
checkItems: 'checkItems',
labels: 'labels',
images: 'images',
isPinned: 'isPinned',
isArchived: 'isArchived',
order: 'order',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Prisma.ModelName = {
Note: 'Note'
};
/**
* Create the Client
*/
const config = {
"generator": {
"name": "client",
"provider": {
"fromEnvVar": null,
"value": "prisma-client-js"
},
"output": {
"value": "D:\\dev_new_pc\\Keep\\mcp-server\\node_modules\\.prisma\\client",
"fromEnvVar": null
},
"config": {
"engineType": "library"
},
"binaryTargets": [
{
"fromEnvVar": null,
"value": "windows",
"native": true
}
],
"previewFeatures": [],
"sourceFilePath": "D:\\dev_new_pc\\Keep\\mcp-server\\prisma\\schema.prisma",
"isCustomOutput": true
},
"relativeEnvPaths": {
"rootEnvPath": null
},
"relativePath": "../../../prisma",
"clientVersion": "5.22.0",
"engineVersion": "605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"datasourceNames": [
"db"
],
"activeProvider": "sqlite",
"postinstall": false,
"inlineDatasources": {
"db": {
"url": {
"fromEnvVar": null,
"value": "file:../../keep-notes/prisma/dev.db"
}
}
},
"inlineSchema": "generator client {\n provider = \"prisma-client-js\"\n output = \"../node_modules/.prisma/client\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = \"file:../../keep-notes/prisma/dev.db\"\n}\n\nmodel Note {\n id String @id @default(cuid())\n title String?\n content String\n color String @default(\"default\")\n type String @default(\"text\")\n checkItems String?\n labels String?\n images String?\n isPinned Boolean @default(false)\n isArchived Boolean @default(false)\n order Int @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n",
"inlineSchemaHash": "f3682893029e886c5458e0556f5e5b92ecb11c6c771f522caa698fb6483db08a",
"copyEngine": true
}
config.dirname = '/'
config.runtimeDataModel = JSON.parse("{\"models\":{\"Note\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":{\"name\":\"cuid\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"title\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"content\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"color\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"default\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"type\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"text\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"checkItems\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"labels\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"images\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"isPinned\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Boolean\",\"default\":false,\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"isArchived\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Boolean\",\"default\":false,\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"order\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Int\",\"default\":0,\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false}},\"enums\":{},\"types\":{}}")
defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
config.engineWasm = undefined
config.injectableEdgeEnv = () => ({
parsed: {}
})
if (typeof globalThis !== 'undefined' && globalThis['DEBUG'] || typeof process !== 'undefined' && process.env && process.env.DEBUG || undefined) {
Debug.enable(typeof globalThis !== 'undefined' && globalThis['DEBUG'] || typeof process !== 'undefined' && process.env && process.env.DEBUG || undefined)
}
const PrismaClient = getPrismaClient(config)
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)

182
mcp-server/node_modules/.prisma/client/index-browser.js generated vendored Normal file
View File

@ -0,0 +1,182 @@
Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
objectEnumValues,
makeStrictEnum,
Public,
getRuntime,
skip
} = require('./runtime/index-browser.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 5.22.0
* Query Engine version: 605197351a3c8bdd595af2d2a9bc3025bca48ea2
*/
Prisma.prismaVersion = {
client: "5.22.0",
engine: "605197351a3c8bdd595af2d2a9bc3025bca48ea2"
}
Prisma.PrismaClientKnownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)};
Prisma.PrismaClientUnknownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientRustPanicError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientInitializationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientValidationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.NotFoundError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`NotFoundError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.empty = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.join = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.raw = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.defineExtension = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull
Prisma.NullTypes = {
DbNull: objectEnumValues.classes.DbNull,
JsonNull: objectEnumValues.classes.JsonNull,
AnyNull: objectEnumValues.classes.AnyNull
}
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
Serializable: 'Serializable'
});
exports.Prisma.NoteScalarFieldEnum = {
id: 'id',
title: 'title',
content: 'content',
color: 'color',
type: 'type',
checkItems: 'checkItems',
labels: 'labels',
images: 'images',
isPinned: 'isPinned',
isArchived: 'isArchived',
order: 'order',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Prisma.ModelName = {
Note: 'Note'
};
/**
* This is a stub Prisma Client that will error at runtime if called.
*/
class PrismaClient {
constructor() {
return new Proxy(this, {
get(target, prop) {
let message
const runtime = getRuntime()
if (runtime.isEdge) {
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
- Use Prisma Accelerate: https://pris.ly/d/accelerate
- Use Driver Adapters: https://pris.ly/d/driver-adapters
`;
} else {
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
}
message += `
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
throw new Error(message)
}
})
}
}
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)

2530
mcp-server/node_modules/.prisma/client/index.d.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

211
mcp-server/node_modules/.prisma/client/index.js generated vendored Normal file
View File

@ -0,0 +1,211 @@
Object.defineProperty(exports, "__esModule", { value: true });
const {
PrismaClientKnownRequestError,
PrismaClientUnknownRequestError,
PrismaClientRustPanicError,
PrismaClientInitializationError,
PrismaClientValidationError,
NotFoundError,
getPrismaClient,
sqltag,
empty,
join,
raw,
skip,
Decimal,
Debug,
objectEnumValues,
makeStrictEnum,
Extensions,
warnOnce,
defineDmmfProperty,
Public,
getRuntime
} = require('./runtime/library.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 5.22.0
* Query Engine version: 605197351a3c8bdd595af2d2a9bc3025bca48ea2
*/
Prisma.prismaVersion = {
client: "5.22.0",
engine: "605197351a3c8bdd595af2d2a9bc3025bca48ea2"
}
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
Prisma.PrismaClientUnknownRequestError = PrismaClientUnknownRequestError
Prisma.PrismaClientRustPanicError = PrismaClientRustPanicError
Prisma.PrismaClientInitializationError = PrismaClientInitializationError
Prisma.PrismaClientValidationError = PrismaClientValidationError
Prisma.NotFoundError = NotFoundError
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = sqltag
Prisma.empty = empty
Prisma.join = join
Prisma.raw = raw
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = Extensions.getExtensionContext
Prisma.defineExtension = Extensions.defineExtension
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull
Prisma.NullTypes = {
DbNull: objectEnumValues.classes.DbNull,
JsonNull: objectEnumValues.classes.JsonNull,
AnyNull: objectEnumValues.classes.AnyNull
}
const path = require('path')
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
Serializable: 'Serializable'
});
exports.Prisma.NoteScalarFieldEnum = {
id: 'id',
title: 'title',
content: 'content',
color: 'color',
type: 'type',
checkItems: 'checkItems',
labels: 'labels',
images: 'images',
isPinned: 'isPinned',
isArchived: 'isArchived',
order: 'order',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Prisma.ModelName = {
Note: 'Note'
};
/**
* Create the Client
*/
const config = {
"generator": {
"name": "client",
"provider": {
"fromEnvVar": null,
"value": "prisma-client-js"
},
"output": {
"value": "D:\\dev_new_pc\\Keep\\mcp-server\\node_modules\\.prisma\\client",
"fromEnvVar": null
},
"config": {
"engineType": "library"
},
"binaryTargets": [
{
"fromEnvVar": null,
"value": "windows",
"native": true
}
],
"previewFeatures": [],
"sourceFilePath": "D:\\dev_new_pc\\Keep\\mcp-server\\prisma\\schema.prisma",
"isCustomOutput": true
},
"relativeEnvPaths": {
"rootEnvPath": null
},
"relativePath": "../../../prisma",
"clientVersion": "5.22.0",
"engineVersion": "605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"datasourceNames": [
"db"
],
"activeProvider": "sqlite",
"postinstall": false,
"inlineDatasources": {
"db": {
"url": {
"fromEnvVar": null,
"value": "file:../../keep-notes/prisma/dev.db"
}
}
},
"inlineSchema": "generator client {\n provider = \"prisma-client-js\"\n output = \"../node_modules/.prisma/client\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = \"file:../../keep-notes/prisma/dev.db\"\n}\n\nmodel Note {\n id String @id @default(cuid())\n title String?\n content String\n color String @default(\"default\")\n type String @default(\"text\")\n checkItems String?\n labels String?\n images String?\n isPinned Boolean @default(false)\n isArchived Boolean @default(false)\n order Int @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n",
"inlineSchemaHash": "f3682893029e886c5458e0556f5e5b92ecb11c6c771f522caa698fb6483db08a",
"copyEngine": true
}
const fs = require('fs')
config.dirname = __dirname
if (!fs.existsSync(path.join(__dirname, 'schema.prisma'))) {
const alternativePaths = [
"node_modules/.prisma/client",
".prisma/client",
]
const alternativePath = alternativePaths.find((altPath) => {
return fs.existsSync(path.join(process.cwd(), altPath, 'schema.prisma'))
}) ?? alternativePaths[0]
config.dirname = path.join(process.cwd(), alternativePath)
config.isBundled = true
}
config.runtimeDataModel = JSON.parse("{\"models\":{\"Note\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":{\"name\":\"cuid\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"title\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"content\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"color\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"default\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"type\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"text\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"checkItems\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"labels\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"images\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"isPinned\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Boolean\",\"default\":false,\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"isArchived\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Boolean\",\"default\":false,\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"order\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Int\",\"default\":0,\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false}},\"enums\":{},\"types\":{}}")
defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
config.engineWasm = undefined
const { warnEnvConflicts } = require('./runtime/library.js')
warnEnvConflicts({
rootEnvPath: config.relativeEnvPaths.rootEnvPath && path.resolve(config.dirname, config.relativeEnvPaths.rootEnvPath),
schemaEnvPath: config.relativeEnvPaths.schemaEnvPath && path.resolve(config.dirname, config.relativeEnvPaths.schemaEnvPath)
})
const PrismaClient = getPrismaClient(config)
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)
// file annotations for bundling tools to include these files
path.join(__dirname, "query_engine-windows.dll.node");
path.join(process.cwd(), "node_modules/.prisma/client/query_engine-windows.dll.node")
// file annotations for bundling tools to include these files
path.join(__dirname, "schema.prisma");
path.join(process.cwd(), "node_modules/.prisma/client/schema.prisma")

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