chore: clean up repo for public release

- Remove BMAD framework, IDE configs, dev screenshots, test files,
  internal docs, and backup files
- Rename keep-notes/ to memento-note/
- Update all references from keep-notes to memento-note
- Add Apache 2.0 license with Commons Clause (non-commercial restriction)
- Add clean .gitignore and .env.docker.example
This commit is contained in:
Sepehr Ramezani
2026-04-20 22:48:06 +02:00
parent 402e88b788
commit e4d4e23dc7
3981 changed files with 407 additions and 530622 deletions

View File

@@ -0,0 +1,85 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { memoryEchoService } from '@/lib/ai/services/memory-echo.service'
/**
* GET /api/ai/echo/connections?noteId={id}&page={page}&limit={limit}
* Fetch all connections for a specific note
*/
export async function GET(req: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
// Get query parameters
const { searchParams } = new URL(req.url)
const noteId = searchParams.get('noteId')
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
// Validate noteId
if (!noteId) {
return NextResponse.json(
{ error: 'noteId parameter is required' },
{ status: 400 }
)
}
// Validate pagination parameters
if (page < 1 || limit < 1 || limit > 50) {
return NextResponse.json(
{ error: 'Invalid pagination parameters. page >= 1, limit between 1 and 50' },
{ status: 400 }
)
}
// Get all connections for the note
const allConnections = await memoryEchoService.getConnectionsForNote(noteId, session.user.id)
// Calculate pagination
const total = allConnections.length
const startIndex = (page - 1) * limit
const endIndex = startIndex + limit
const paginatedConnections = allConnections.slice(startIndex, endIndex)
// Format connections for response
const connections = paginatedConnections.map(conn => {
// Determine which note is the "other" note (not the target note)
const isNote1Target = conn.note1.id === noteId
const otherNote = isNote1Target ? conn.note2 : conn.note1
return {
noteId: otherNote.id,
title: otherNote.title,
content: otherNote.content,
createdAt: otherNote.createdAt,
similarity: conn.similarityScore,
daysApart: conn.daysApart
}
})
return NextResponse.json({
connections,
pagination: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
hasNext: endIndex < total,
hasPrev: page > 1
}
})
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch connections' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,60 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
/**
* POST /api/ai/echo/dismiss
* Dismiss a connection for a specific note
* Body: { noteId, connectedNoteId }
*/
export async function POST(req: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const body = await req.json()
const { noteId, connectedNoteId } = body
if (!noteId || !connectedNoteId) {
return NextResponse.json(
{ error: 'noteId and connectedNoteId are required' },
{ status: 400 }
)
}
// Find and mark matching insights as dismissed
// We need to find insights where (note1Id = noteId AND note2Id = connectedNoteId) OR (note1Id = connectedNoteId AND note2Id = noteId)
await prisma.memoryEchoInsight.updateMany({
where: {
userId: session.user.id,
OR: [
{
note1Id: noteId,
note2Id: connectedNoteId
},
{
note1Id: connectedNoteId,
note2Id: noteId
}
]
},
data: {
dismissed: true
}
})
return NextResponse.json({ success: true })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to dismiss connection' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,108 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { getChatProvider } from '@/lib/ai/factory'
import { getSystemConfig } from '@/lib/config'
import prisma from '@/lib/prisma'
/**
* POST /api/ai/echo/fusion
* Generate intelligent fusion of multiple notes
*/
export async function POST(req: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const body = await req.json()
const { noteIds, prompt } = body
if (!noteIds || !Array.isArray(noteIds) || noteIds.length < 2) {
return NextResponse.json(
{ error: 'At least 2 note IDs are required' },
{ status: 400 }
)
}
// Fetch the notes
const notes = await prisma.note.findMany({
where: {
id: { in: noteIds },
userId: session.user.id
},
select: {
id: true,
title: true,
content: true,
createdAt: true
}
})
if (notes.length !== noteIds.length) {
return NextResponse.json(
{ error: 'Some notes not found or access denied' },
{ status: 404 }
)
}
// Get AI provider
const config = await getSystemConfig()
const provider = getChatProvider(config)
// Build fusion prompt
const notesDescriptions = notes.map((note, index) => {
return `Note ${index + 1}: "${note.title || 'Untitled'}"
${note.content}`
}).join('\n\n')
const fusionPrompt = `You are an expert at synthesizing and merging information from multiple sources.
TASK: Create a unified, well-structured note by intelligently combining the following notes.
${prompt ? `ADDITIONAL INSTRUCTIONS: ${prompt}\n` : ''}
NOTES TO MERGE:
${notesDescriptions}
REQUIREMENTS:
1. Create a clear, descriptive title that captures the essence of all notes
2. Merge and consolidate related information
3. Remove duplicates while preserving unique details from each note
4. Organize the content logically (use headers, bullet points, etc.)
5. Maintain the important details and context from all notes
6. Keep the tone and style consistent
7. Use markdown formatting for better readability
Output format:
# [Fused Title]
[Merged and organized content...]
Begin:`
try {
const fusedContent = await provider.generateText(fusionPrompt)
return NextResponse.json({
fusedNote: fusedContent,
notesCount: notes.length
})
} catch (error) {
return NextResponse.json(
{ error: 'Failed to generate fusion' },
{ status: 500 }
)
}
} catch (error) {
return NextResponse.json(
{ error: 'Failed to process fusion request' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,92 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { memoryEchoService } from '@/lib/ai/services/memory-echo.service'
/**
* GET /api/ai/echo
* Fetch next Memory Echo insight for current user
*/
export async function GET(req: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
// Get next insight (respects frequency limits)
const insight = await memoryEchoService.getNextInsight(session.user.id)
if (!insight) {
return NextResponse.json(
{
insight: null,
message: 'No new insights available at the moment. Memory Echo will notify you when we discover connections between your notes.'
}
)
}
return NextResponse.json({ insight })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch Memory Echo insight' },
{ status: 500 }
)
}
}
/**
* POST /api/ai/echo
* Submit feedback or mark as viewed
*/
export async function POST(req: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const body = await req.json()
const { action, insightId, feedback } = body
if (action === 'view') {
// Mark insight as viewed
await memoryEchoService.markAsViewed(insightId)
return NextResponse.json({ success: true })
} else if (action === 'feedback') {
// Submit feedback (thumbs_up or thumbs_down)
if (!feedback || !['thumbs_up', 'thumbs_down'].includes(feedback)) {
return NextResponse.json(
{ error: 'Invalid feedback. Must be thumbs_up or thumbs_down' },
{ status: 400 }
)
}
await memoryEchoService.submitFeedback(insightId, feedback)
return NextResponse.json({ success: true })
} else {
return NextResponse.json(
{ error: 'Invalid action. Must be "view" or "feedback"' },
{ status: 400 }
)
}
} catch (error) {
return NextResponse.json(
{ error: 'Failed to process request' },
{ status: 500 }
)
}
}