fix: unify theme system - fix theme switching persistence

- Unified localStorage key to 'theme-preference' across all components
- Fixed header.tsx using wrong localStorage key ('theme' instead of 'theme-preference')
- Added localStorage hybrid persistence for instant theme changes
- Removed router.refresh() which was causing stale data revert
- Replaced Blue theme with Sepia
- Consolidated auth() calls to prevent race conditions
- Updated UserSettingsData types to include all themes
This commit is contained in:
2026-01-18 22:33:41 +01:00
parent ef60dafd73
commit ddb67ba9e5
306 changed files with 59580 additions and 6063 deletions

View File

@@ -14,11 +14,11 @@ import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Initialize Prisma Client
// Initialize Prisma Client with correct database path
const prisma = new PrismaClient({
datasources: {
db: {
url: `file:${join(__dirname, '../keep-notes/prisma/dev.db')}`
url: 'file:D:/dev_new_pc/Keep/keep-notes/prisma/dev.db'
}
}
});
@@ -30,14 +30,23 @@ function parseNote(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,
links: dbNote.links ? JSON.parse(dbNote.links) : null,
};
}
// Helper to parse Notebook fields
function parseNotebook(dbNotebook) {
return {
...dbNotebook,
labels: dbNotebook.labels || [],
};
}
// Create MCP server
const server = new Server(
{
name: 'memento-mcp-server',
version: '1.0.0',
name: 'keep-notes-mcp-server',
version: '2.0.0',
},
{
capabilities: {
@@ -50,9 +59,10 @@ const server = new Server(
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
// Note Tools
{
name: 'create_note',
description: 'Create a new note in Memento',
description: 'Create a new note in Keep Notes',
inputSchema: {
type: 'object',
properties: {
@@ -108,13 +118,50 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
description: 'Note images as base64 encoded strings',
items: { type: 'string' },
},
links: {
type: 'array',
description: 'Note links',
items: { type: 'string' },
},
reminder: {
type: 'string',
description: 'Reminder date/time (ISO 8601 format)',
},
isReminderDone: {
type: 'boolean',
description: 'Mark reminder as done',
default: false,
},
reminderRecurrence: {
type: 'string',
description: 'Reminder recurrence (daily, weekly, monthly, yearly)',
},
reminderLocation: {
type: 'string',
description: 'Reminder location',
},
isMarkdown: {
type: 'boolean',
description: 'Enable markdown support',
default: false,
},
size: {
type: 'string',
enum: ['small', 'medium', 'large'],
description: 'Note size',
default: 'small',
},
notebookId: {
type: 'string',
description: 'Notebook ID to associate the note with',
},
},
required: ['content'],
},
},
{
name: 'get_notes',
description: 'Get all notes from Memento',
description: 'Get all notes from Keep Notes',
inputSchema: {
type: 'object',
properties: {
@@ -127,6 +174,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
type: 'string',
description: 'Search query to filter notes',
},
notebookId: {
type: 'string',
description: 'Filter notes by notebook ID',
},
},
},
},
@@ -166,6 +217,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
type: 'string',
description: 'Note color',
},
type: {
type: 'string',
enum: ['text', 'checklist'],
description: 'Note type',
},
checkItems: {
type: 'array',
description: 'Checklist items',
@@ -196,6 +252,40 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
description: 'Note images as base64 encoded strings',
items: { type: 'string' },
},
links: {
type: 'array',
description: 'Note links',
items: { type: 'string' },
},
reminder: {
type: 'string',
description: 'Reminder date/time (ISO 8601 format)',
},
isReminderDone: {
type: 'boolean',
description: 'Mark reminder as done',
},
reminderRecurrence: {
type: 'string',
description: 'Reminder recurrence',
},
reminderLocation: {
type: 'string',
description: 'Reminder location',
},
isMarkdown: {
type: 'boolean',
description: 'Enable markdown support',
},
size: {
type: 'string',
enum: ['small', 'medium', 'large'],
description: 'Note size',
},
notebookId: {
type: 'string',
description: 'Notebook ID to move the note to',
},
},
required: ['id'],
},
@@ -224,6 +314,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
type: 'string',
description: 'Search query',
},
notebookId: {
type: 'string',
description: 'Filter search by notebook ID',
},
},
required: ['query'],
},
@@ -264,6 +358,173 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
required: ['id'],
},
},
// Notebook Tools
{
name: 'create_notebook',
description: 'Create a new notebook',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Notebook name',
},
icon: {
type: 'string',
description: 'Notebook icon (emoji)',
},
color: {
type: 'string',
description: 'Notebook color (hex code)',
},
order: {
type: 'number',
description: 'Notebook order',
},
},
required: ['name'],
},
},
{
name: 'get_notebooks',
description: 'Get all notebooks',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_notebook',
description: 'Get a specific notebook by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Notebook ID',
},
},
required: ['id'],
},
},
{
name: 'update_notebook',
description: 'Update an existing notebook',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Notebook ID',
},
name: {
type: 'string',
description: 'Notebook name',
},
icon: {
type: 'string',
description: 'Notebook icon',
},
color: {
type: 'string',
description: 'Notebook color',
},
order: {
type: 'number',
description: 'Notebook order',
},
},
required: ['id'],
},
},
{
name: 'delete_notebook',
description: 'Delete a notebook by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Notebook ID',
},
},
required: ['id'],
},
},
// Label Tools
{
name: 'create_label',
description: 'Create a new label',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Label name',
},
color: {
type: 'string',
description: 'Label color (red, orange, yellow, green, teal, blue, purple, pink, gray)',
},
notebookId: {
type: 'string',
description: 'Notebook ID to associate the label with',
},
},
required: ['name', 'notebookId'],
},
},
{
name: 'get_labels_detailed',
description: 'Get all labels with details',
inputSchema: {
type: 'object',
properties: {
notebookId: {
type: 'string',
description: 'Filter labels by notebook ID',
},
},
},
},
{
name: 'update_label',
description: 'Update an existing label',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Label ID',
},
name: {
type: 'string',
description: 'Label name',
},
color: {
type: 'string',
description: 'Label color',
},
},
required: ['id'],
},
},
{
name: 'delete_label',
description: 'Delete a label by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Label ID',
},
},
required: ['id'],
},
},
],
};
});
@@ -273,6 +534,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
// === NOTE TOOLS ===
switch (name) {
case 'create_note': {
const note = await prisma.note.create({
@@ -286,6 +548,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
isPinned: args.isPinned || false,
isArchived: args.isArchived || false,
images: args.images ? JSON.stringify(args.images) : null,
links: args.links ? JSON.stringify(args.links) : null,
reminder: args.reminder ? new Date(args.reminder) : null,
isReminderDone: args.isReminderDone || false,
reminderRecurrence: args.reminderRecurrence || null,
reminderLocation: args.reminderLocation || null,
isMarkdown: args.isMarkdown || false,
size: args.size || 'small',
notebookId: args.notebookId || null,
},
});
return {
@@ -309,6 +579,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
{ content: { contains: args.search, mode: 'insensitive' } },
];
}
if (args.notebookId) {
where.notebookId = args.notebookId;
}
const notes = await prisma.note.findMany({
where,
@@ -361,6 +634,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
if ('images' in args) {
updateData.images = args.images ? JSON.stringify(args.images) : null;
}
if ('links' in args) {
updateData.links = args.links ? JSON.stringify(args.links) : null;
}
if ('reminder' in args) {
updateData.reminder = args.reminder ? new Date(args.reminder) : null;
}
updateData.updatedAt = new Date();
const note = await prisma.note.update({
@@ -393,14 +672,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
case 'search_notes': {
const where = {
isArchived: false,
OR: [
{ title: { contains: args.query, mode: 'insensitive' } },
{ content: { contains: args.query, mode: 'insensitive' } },
],
};
if (args.notebookId) {
where.notebookId = args.notebookId;
}
const notes = await prisma.note.findMany({
where: {
isArchived: false,
OR: [
{ title: { contains: args.query, mode: 'insensitive' } },
{ content: { contains: args.query, mode: 'insensitive' } },
],
},
where,
orderBy: [
{ isPinned: 'desc' },
{ updatedAt: 'desc' },
@@ -478,6 +762,238 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
};
}
// === NOTEBOOK TOOLS ===
case 'create_notebook': {
// Get the highest order value
const highestOrder = await prisma.notebook.findFirst({
orderBy: { order: 'desc' },
select: { order: true }
});
const nextOrder = args.order !== undefined ? args.order : (highestOrder?.order ?? -1) + 1;
const notebook = await prisma.notebook.create({
data: {
name: args.name.trim(),
icon: args.icon || '📁',
color: args.color || '#3B82F6',
order: nextOrder,
},
include: {
labels: true,
_count: {
select: { notes: true }
}
}
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
...notebook,
notesCount: notebook._count.notes
}, null, 2),
},
],
};
}
case 'get_notebooks': {
const notebooks = await prisma.notebook.findMany({
include: {
labels: {
orderBy: { name: 'asc' }
},
_count: {
select: { notes: true }
}
},
orderBy: { order: 'asc' }
});
return {
content: [
{
type: 'text',
text: JSON.stringify(
notebooks.map(nb => ({
...nb,
notesCount: nb._count.notes
})),
null, 2
),
},
],
};
}
case 'get_notebook': {
const notebook = await prisma.notebook.findUnique({
where: { id: args.id },
include: {
labels: true,
notes: true,
_count: {
select: { notes: true }
}
}
});
if (!notebook) {
throw new McpError(ErrorCode.InvalidRequest, 'Notebook not found');
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
...notebook,
notes: notebook.notes.map(parseNote),
notesCount: notebook._count.notes
}, null, 2),
},
],
};
}
case 'update_notebook': {
const updateData = { ...args };
delete updateData.id;
const notebook = await prisma.notebook.update({
where: { id: args.id },
data: updateData,
include: {
labels: true,
_count: {
select: { notes: true }
}
}
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
...notebook,
notesCount: notebook._count.notes
}, null, 2),
},
],
};
}
case 'delete_notebook': {
await prisma.notebook.delete({
where: { id: args.id },
});
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: true, message: 'Notebook deleted' }),
},
],
};
}
// === LABEL TOOLS ===
case 'create_label': {
const COLORS = ['red', 'orange', 'yellow', 'green', 'teal', 'blue', 'purple', 'pink', 'gray'];
// Check if label already exists in this notebook
const existing = await prisma.label.findFirst({
where: {
name: args.name.trim(),
notebookId: args.notebookId
}
});
if (existing) {
throw new McpError(ErrorCode.InvalidRequest, 'Label already exists in this notebook');
}
const label = await prisma.label.create({
data: {
name: args.name.trim(),
color: args.color || COLORS[Math.floor(Math.random() * COLORS.length)],
notebookId: args.notebookId,
}
});
return {
content: [
{
type: 'text',
text: JSON.stringify(label, null, 2),
},
],
};
}
case 'get_labels_detailed': {
const where = {};
if (args.notebookId) {
where.notebookId = args.notebookId;
}
const labels = await prisma.label.findMany({
where,
include: {
notebook: {
select: { id: true, name: true }
}
},
orderBy: { name: 'asc' }
});
return {
content: [
{
type: 'text',
text: JSON.stringify(labels, null, 2),
},
],
};
}
case 'update_label': {
const updateData = { ...args };
delete updateData.id;
const label = await prisma.label.update({
where: { id: args.id },
data: updateData,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(label, null, 2),
},
],
};
}
case 'delete_label': {
await prisma.label.delete({
where: { id: args.id },
});
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: true, message: 'Label deleted' }),
},
],
};
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
@@ -499,7 +1015,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Memento MCP server running on stdio');
console.error('Keep Notes MCP server running on stdio');
}
main().catch((error) => {