mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +00:00
210 lines
5.9 KiB
Go
210 lines
5.9 KiB
Go
// Package sqlite provides SQLite database operations for claude-mnemonic.
|
|
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
|
|
)
|
|
|
|
// PromptCleanupFunc is a callback for when prompts are cleaned up.
|
|
// Receives the IDs of deleted prompts for downstream cleanup (e.g., vector DB).
|
|
type PromptCleanupFunc func(ctx context.Context, deletedIDs []int64)
|
|
|
|
// MaxPromptsGlobal is the hard limit of prompts across all projects.
|
|
const MaxPromptsGlobal = 500
|
|
|
|
// PromptStore provides user prompt-related database operations.
|
|
type PromptStore struct {
|
|
store *Store
|
|
cleanupFunc PromptCleanupFunc
|
|
}
|
|
|
|
// NewPromptStore creates a new prompt store.
|
|
func NewPromptStore(store *Store) *PromptStore {
|
|
return &PromptStore{store: store}
|
|
}
|
|
|
|
// SetCleanupFunc sets the callback for when prompts are deleted during cleanup.
|
|
func (s *PromptStore) SetCleanupFunc(fn PromptCleanupFunc) {
|
|
s.cleanupFunc = fn
|
|
}
|
|
|
|
// SaveUserPromptWithMatches saves a user prompt with matched observation count.
|
|
func (s *PromptStore) SaveUserPromptWithMatches(ctx context.Context, claudeSessionID string, promptNumber int, promptText string, matchedObservations int) (int64, error) {
|
|
now := time.Now()
|
|
|
|
const query = `
|
|
INSERT INTO user_prompts
|
|
(claude_session_id, prompt_number, prompt_text, matched_observations, created_at, created_at_epoch)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`
|
|
|
|
result, err := s.store.ExecContext(ctx, query,
|
|
claudeSessionID, promptNumber, promptText, matchedObservations,
|
|
now.Format(time.RFC3339), now.UnixMilli(),
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
id, _ := result.LastInsertId()
|
|
|
|
// Cleanup old prompts beyond the global limit (async to not block handler)
|
|
go func() {
|
|
cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
deletedIDs, _ := s.CleanupOldPrompts(cleanupCtx)
|
|
if len(deletedIDs) > 0 && s.cleanupFunc != nil {
|
|
s.cleanupFunc(cleanupCtx, deletedIDs)
|
|
}
|
|
}()
|
|
|
|
return id, nil
|
|
}
|
|
|
|
// CleanupOldPrompts deletes prompts beyond the global limit.
|
|
// Keeps the most recent MaxPromptsGlobal prompts.
|
|
// Returns the IDs of deleted prompts for downstream cleanup (e.g., vector DB).
|
|
func (s *PromptStore) CleanupOldPrompts(ctx context.Context) ([]int64, error) {
|
|
// First, find IDs that will be deleted
|
|
const selectQuery = `
|
|
SELECT id FROM user_prompts
|
|
WHERE id NOT IN (
|
|
SELECT id FROM user_prompts
|
|
ORDER BY created_at_epoch DESC
|
|
LIMIT ?
|
|
)
|
|
`
|
|
|
|
rows, err := s.store.QueryContext(ctx, selectQuery, MaxPromptsGlobal)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var toDelete []int64
|
|
for rows.Next() {
|
|
var id int64
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
toDelete = append(toDelete, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(toDelete) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Delete the prompts
|
|
const deleteQuery = `
|
|
DELETE FROM user_prompts
|
|
WHERE id NOT IN (
|
|
SELECT id FROM user_prompts
|
|
ORDER BY created_at_epoch DESC
|
|
LIMIT ?
|
|
)
|
|
`
|
|
|
|
_, err = s.store.ExecContext(ctx, deleteQuery, MaxPromptsGlobal)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return toDelete, nil
|
|
}
|
|
|
|
// GetPromptsByIDs retrieves user prompts by a list of IDs.
|
|
func (s *PromptStore) GetPromptsByIDs(ctx context.Context, ids []int64, orderBy string, limit int) ([]*models.UserPromptWithSession, error) {
|
|
if len(ids) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Build query with placeholders
|
|
// #nosec G202 -- query uses parameterized placeholders, not user input
|
|
query := `
|
|
SELECT up.id, up.claude_session_id, up.prompt_number, up.prompt_text,
|
|
COALESCE(up.matched_observations, 0) as matched_observations,
|
|
up.created_at, up.created_at_epoch,
|
|
COALESCE(s.project, '') as project,
|
|
COALESCE(s.sdk_session_id, '') as sdk_session_id
|
|
FROM user_prompts up
|
|
LEFT JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
|
WHERE up.id IN (?` + repeatPlaceholders(len(ids)-1) + `)
|
|
ORDER BY up.created_at_epoch `
|
|
|
|
if orderBy == "date_asc" {
|
|
query += "ASC"
|
|
} else {
|
|
query += "DESC"
|
|
}
|
|
|
|
if limit > 0 {
|
|
query += " LIMIT ?"
|
|
}
|
|
|
|
args := int64SliceToInterface(ids)
|
|
if limit > 0 {
|
|
args = append(args, limit)
|
|
}
|
|
|
|
rows, err := s.store.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
return scanPromptWithSessionRows(rows)
|
|
}
|
|
|
|
// GetAllRecentUserPrompts retrieves recent user prompts across all sessions.
|
|
func (s *PromptStore) GetAllRecentUserPrompts(ctx context.Context, limit int) ([]*models.UserPromptWithSession, error) {
|
|
const query = `
|
|
SELECT up.id, up.claude_session_id, up.prompt_number, up.prompt_text,
|
|
COALESCE(up.matched_observations, 0) as matched_observations,
|
|
up.created_at, up.created_at_epoch,
|
|
COALESCE(s.project, '') as project,
|
|
COALESCE(s.sdk_session_id, '') as sdk_session_id
|
|
FROM user_prompts up
|
|
LEFT JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
|
ORDER BY up.created_at_epoch DESC
|
|
LIMIT ?
|
|
`
|
|
|
|
rows, err := s.store.QueryContext(ctx, query, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
return scanPromptWithSessionRows(rows)
|
|
}
|
|
|
|
// GetRecentUserPromptsByProject retrieves recent user prompts for a specific project.
|
|
func (s *PromptStore) GetRecentUserPromptsByProject(ctx context.Context, project string, limit int) ([]*models.UserPromptWithSession, error) {
|
|
const query = `
|
|
SELECT up.id, up.claude_session_id, up.prompt_number, up.prompt_text,
|
|
COALESCE(up.matched_observations, 0) as matched_observations,
|
|
up.created_at, up.created_at_epoch,
|
|
COALESCE(s.project, '') as project,
|
|
COALESCE(s.sdk_session_id, '') as sdk_session_id
|
|
FROM user_prompts up
|
|
LEFT JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
|
WHERE s.project = ?
|
|
ORDER BY up.created_at_epoch DESC
|
|
LIMIT ?
|
|
`
|
|
|
|
rows, err := s.store.QueryContext(ctx, query, project, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
return scanPromptWithSessionRows(rows)
|
|
}
|