Add session documentation system (migration + MCP tools)
Phase 1-2 complete: Database schema + 9 MCP tools for session docs Database Changes (migration 016): - session_notes table (accomplishments, decisions, gotchas, etc.) - session_plans table (plan mode plans with lifecycle tracking) - project_documentation table (persistent project docs) - sessions.documentation column (auto-generated markdown) - HNSW indexes for semantic search across all doc types MCP Tools Added (session-docs.ts): 1. session_note_add - Add structured notes to session 2. session_notes_list - List notes by type 3. session_plan_save - Save plan with embedding 4. session_plan_update_status - Track plan lifecycle 5. session_plan_list - List session plans 6. project_doc_upsert - Create/update project docs 7. project_doc_get - Get specific doc by type 8. project_doc_list - List all project docs 9. session_documentation_generate - Auto-generate markdown Replaces: CLAUDE.md files, ~/.claude/plans/ directory Next: Update session-start/end scripts for temp file management Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
424
src/tools/session-docs.ts
Normal file
424
src/tools/session-docs.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
// Session documentation operations - notes, plans, and project documentation
|
||||
// Replaces file-based CLAUDE.md and plan files with database storage
|
||||
|
||||
import { query, queryOne, execute } from '../db.js';
|
||||
import { getEmbedding, formatEmbedding } from '../embeddings.js';
|
||||
|
||||
// ============================================================================
|
||||
// SESSION NOTES
|
||||
// ============================================================================
|
||||
|
||||
interface SessionNoteAddArgs {
|
||||
session_id: string;
|
||||
note_type: 'accomplishment' | 'decision' | 'gotcha' | 'next_steps' | 'context';
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface SessionNotesListArgs {
|
||||
session_id: string;
|
||||
note_type?: 'accomplishment' | 'decision' | 'gotcha' | 'next_steps' | 'context';
|
||||
}
|
||||
|
||||
interface SessionNote {
|
||||
id: number;
|
||||
session_id: string;
|
||||
note_type: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a note to current session
|
||||
* Auto-generates embedding for semantic search
|
||||
*/
|
||||
export async function sessionNoteAdd(args: SessionNoteAddArgs): Promise<string> {
|
||||
const { session_id, note_type, content } = args;
|
||||
|
||||
// Generate embedding for semantic search
|
||||
const embedding = await getEmbedding(content);
|
||||
const embeddingFormatted = embedding ? formatEmbedding(embedding) : null;
|
||||
|
||||
await execute(
|
||||
`INSERT INTO session_notes (session_id, note_type, content, embedding)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
[session_id, note_type, content, embeddingFormatted]
|
||||
);
|
||||
|
||||
return `Note added to session ${session_id} (type: ${note_type})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all notes for a session, optionally filtered by type
|
||||
*/
|
||||
export async function sessionNotesList(args: SessionNotesListArgs): Promise<SessionNote[]> {
|
||||
const { session_id, note_type } = args;
|
||||
|
||||
let sql = `
|
||||
SELECT id, session_id, note_type, content, created_at
|
||||
FROM session_notes
|
||||
WHERE session_id = $1
|
||||
`;
|
||||
|
||||
const params: unknown[] = [session_id];
|
||||
|
||||
if (note_type) {
|
||||
sql += ` AND note_type = $2`;
|
||||
params.push(note_type);
|
||||
}
|
||||
|
||||
sql += ` ORDER BY created_at ASC`;
|
||||
|
||||
const notes = await query<SessionNote>(sql, params);
|
||||
return notes;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SESSION PLANS
|
||||
// ============================================================================
|
||||
|
||||
interface SessionPlanSaveArgs {
|
||||
session_id: string;
|
||||
plan_content: string;
|
||||
plan_file_name?: string;
|
||||
status?: 'draft' | 'approved' | 'executed' | 'abandoned';
|
||||
}
|
||||
|
||||
interface SessionPlanUpdateStatusArgs {
|
||||
plan_id: number;
|
||||
status: 'draft' | 'approved' | 'executed' | 'abandoned';
|
||||
}
|
||||
|
||||
interface SessionPlanListArgs {
|
||||
session_id: string;
|
||||
status?: 'draft' | 'approved' | 'executed' | 'abandoned';
|
||||
}
|
||||
|
||||
interface SessionPlan {
|
||||
id: number;
|
||||
session_id: string;
|
||||
plan_file_name: string | null;
|
||||
plan_content: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
approved_at: string | null;
|
||||
completed_at: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a plan to database
|
||||
* Extracts embedding from plan content for semantic search
|
||||
*/
|
||||
export async function sessionPlanSave(args: SessionPlanSaveArgs): Promise<string> {
|
||||
const { session_id, plan_content, plan_file_name, status = 'draft' } = args;
|
||||
|
||||
// Generate embedding for semantic search
|
||||
const embedding = await getEmbedding(plan_content);
|
||||
const embeddingFormatted = embedding ? formatEmbedding(embedding) : null;
|
||||
|
||||
const result = await queryOne<{ id: number }>(
|
||||
`INSERT INTO session_plans (session_id, plan_file_name, plan_content, status, embedding)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id`,
|
||||
[session_id, plan_file_name || null, plan_content, status, embeddingFormatted]
|
||||
);
|
||||
|
||||
const planId = result?.id || 0;
|
||||
|
||||
return `Plan saved to session ${session_id} (plan_id: ${planId}, status: ${status})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update plan status (draft → approved → executed)
|
||||
*/
|
||||
export async function sessionPlanUpdateStatus(args: SessionPlanUpdateStatusArgs): Promise<string> {
|
||||
const { plan_id, status } = args;
|
||||
|
||||
// Set timestamps based on status
|
||||
let sql = 'UPDATE session_plans SET status = $1';
|
||||
const params: unknown[] = [status, plan_id];
|
||||
|
||||
if (status === 'approved') {
|
||||
sql += ', approved_at = NOW()';
|
||||
} else if (status === 'executed' || status === 'abandoned') {
|
||||
sql += ', completed_at = NOW()';
|
||||
}
|
||||
|
||||
sql += ' WHERE id = $2';
|
||||
|
||||
await execute(sql, params);
|
||||
|
||||
return `Plan ${plan_id} status updated to: ${status}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* List plans for a session
|
||||
*/
|
||||
export async function sessionPlanList(args: SessionPlanListArgs): Promise<SessionPlan[]> {
|
||||
const { session_id, status } = args;
|
||||
|
||||
let sql = `
|
||||
SELECT id, session_id, plan_file_name, plan_content, status,
|
||||
created_at, approved_at, completed_at
|
||||
FROM session_plans
|
||||
WHERE session_id = $1
|
||||
`;
|
||||
|
||||
const params: unknown[] = [session_id];
|
||||
|
||||
if (status) {
|
||||
sql += ` AND status = $2`;
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
sql += ` ORDER BY created_at DESC`;
|
||||
|
||||
const plans = await query<SessionPlan>(sql, params);
|
||||
return plans;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PROJECT DOCUMENTATION
|
||||
// ============================================================================
|
||||
|
||||
interface ProjectDocUpsertArgs {
|
||||
project: string;
|
||||
doc_type: 'overview' | 'architecture' | 'guidelines' | 'history';
|
||||
title: string;
|
||||
content: string;
|
||||
session_id?: string;
|
||||
}
|
||||
|
||||
interface ProjectDocGetArgs {
|
||||
project: string;
|
||||
doc_type: 'overview' | 'architecture' | 'guidelines' | 'history';
|
||||
}
|
||||
|
||||
interface ProjectDocListArgs {
|
||||
project: string;
|
||||
}
|
||||
|
||||
interface ProjectDoc {
|
||||
id: number;
|
||||
project: string;
|
||||
doc_type: string;
|
||||
title: string;
|
||||
content: string;
|
||||
last_updated_session: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update project documentation
|
||||
* Links to current session
|
||||
*/
|
||||
export async function projectDocUpsert(args: ProjectDocUpsertArgs): Promise<string> {
|
||||
const { project, doc_type, title, content, session_id } = args;
|
||||
|
||||
await execute(
|
||||
`INSERT INTO project_documentation (project, doc_type, title, content, last_updated_session, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||
ON CONFLICT (project, doc_type)
|
||||
DO UPDATE SET
|
||||
title = EXCLUDED.title,
|
||||
content = EXCLUDED.content,
|
||||
last_updated_session = EXCLUDED.last_updated_session,
|
||||
updated_at = NOW()`,
|
||||
[project, doc_type, title, content, session_id || null]
|
||||
);
|
||||
|
||||
return `Project documentation updated: ${project} (${doc_type})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific project documentation by type
|
||||
*/
|
||||
export async function projectDocGet(args: ProjectDocGetArgs): Promise<ProjectDoc | null> {
|
||||
const { project, doc_type } = args;
|
||||
|
||||
const doc = await queryOne<ProjectDoc>(
|
||||
`SELECT id, project, doc_type, title, content, last_updated_session, created_at, updated_at
|
||||
FROM project_documentation
|
||||
WHERE project = $1 AND doc_type = $2`,
|
||||
[project, doc_type]
|
||||
);
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all documentation for a project
|
||||
*/
|
||||
export async function projectDocList(args: ProjectDocListArgs): Promise<ProjectDoc[]> {
|
||||
const { project } = args;
|
||||
|
||||
const docs = await query<ProjectDoc>(
|
||||
`SELECT id, project, doc_type, title, content, last_updated_session, created_at, updated_at
|
||||
FROM project_documentation
|
||||
WHERE project = $1
|
||||
ORDER BY doc_type`,
|
||||
[project]
|
||||
);
|
||||
|
||||
return docs;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SESSION DOCUMENTATION GENERATION
|
||||
// ============================================================================
|
||||
|
||||
interface SessionDocumentationGenerateArgs {
|
||||
session_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-generate full markdown documentation for a session
|
||||
* Aggregates: tasks, commits, notes, decisions, plans
|
||||
* Stores in sessions.documentation field
|
||||
*/
|
||||
export async function sessionDocumentationGenerate(args: SessionDocumentationGenerateArgs): Promise<string> {
|
||||
const { session_id } = args;
|
||||
|
||||
// Fetch session details
|
||||
const session = await queryOne<{
|
||||
id: string;
|
||||
project: string;
|
||||
session_number: number;
|
||||
started_at: string;
|
||||
ended_at: string;
|
||||
duration_minutes: number;
|
||||
summary: string;
|
||||
git_branch: string;
|
||||
}>(
|
||||
`SELECT id, project, session_number, started_at, ended_at, duration_minutes, summary, git_branch
|
||||
FROM sessions
|
||||
WHERE id = $1`,
|
||||
[session_id]
|
||||
);
|
||||
|
||||
if (!session) {
|
||||
throw new Error(`Session not found: ${session_id}`);
|
||||
}
|
||||
|
||||
// Fetch tasks worked on
|
||||
const tasks = await query<{ task_id: string; title: string; status: string }>(
|
||||
`SELECT DISTINCT t.id as task_id, t.title, t.status
|
||||
FROM task_activity ta
|
||||
JOIN tasks t ON ta.task_id = t.id
|
||||
WHERE ta.session_id = $1
|
||||
ORDER BY t.id`,
|
||||
[session_id]
|
||||
);
|
||||
|
||||
// Fetch commits made
|
||||
const commits = await query<{ commit_sha: string; commit_message: string }>(
|
||||
`SELECT commit_sha, commit_message
|
||||
FROM session_commits
|
||||
WHERE session_id = $1
|
||||
ORDER BY committed_at`,
|
||||
[session_id]
|
||||
);
|
||||
|
||||
// Fetch notes
|
||||
const notes = await query<{ note_type: string; content: string }>(
|
||||
`SELECT note_type, content
|
||||
FROM session_notes
|
||||
WHERE session_id = $1
|
||||
ORDER BY created_at`,
|
||||
[session_id]
|
||||
);
|
||||
|
||||
// Fetch plans
|
||||
const plans = await query<{ plan_content: string; status: string }>(
|
||||
`SELECT plan_content, status
|
||||
FROM session_plans
|
||||
WHERE session_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1`,
|
||||
[session_id]
|
||||
);
|
||||
|
||||
// Build markdown documentation
|
||||
let doc = `# Session ${session.session_number} - ${session.project}\n\n`;
|
||||
doc += `**Date:** ${new Date(session.started_at).toLocaleDateString()}\n`;
|
||||
doc += `**Duration:** ${session.duration_minutes || 0} minutes\n`;
|
||||
doc += `**Status:** ${session.summary ? '✅ Complete' : '⚠️ In Progress'}\n`;
|
||||
if (session.git_branch) {
|
||||
doc += `**Branch:** ${session.git_branch}\n`;
|
||||
}
|
||||
doc += `\n---\n\n`;
|
||||
|
||||
// Summary
|
||||
if (session.summary) {
|
||||
doc += `## Summary\n\n${session.summary}\n\n`;
|
||||
}
|
||||
|
||||
// Tasks
|
||||
if (tasks.length > 0) {
|
||||
doc += `## Tasks Completed\n\n`;
|
||||
tasks.forEach(task => {
|
||||
doc += `- **${task.task_id}**: ${task.title} (${task.status})\n`;
|
||||
});
|
||||
doc += `\n`;
|
||||
}
|
||||
|
||||
// Commits
|
||||
if (commits.length > 0) {
|
||||
doc += `## Commits\n\n`;
|
||||
commits.forEach(commit => {
|
||||
const shortSha = commit.commit_sha.substring(0, 7);
|
||||
doc += `- \`${shortSha}\` - ${commit.commit_message || 'No message'}\n`;
|
||||
});
|
||||
doc += `\n`;
|
||||
}
|
||||
|
||||
// Notes by type
|
||||
const notesByType: Record<string, string[]> = {};
|
||||
notes.forEach(note => {
|
||||
if (!notesByType[note.note_type]) {
|
||||
notesByType[note.note_type] = [];
|
||||
}
|
||||
notesByType[note.note_type].push(note.content);
|
||||
});
|
||||
|
||||
if (notesByType.accomplishment && notesByType.accomplishment.length > 0) {
|
||||
doc += `## Accomplishments\n\n`;
|
||||
notesByType.accomplishment.forEach(content => {
|
||||
doc += `${content}\n\n`;
|
||||
});
|
||||
}
|
||||
|
||||
if (notesByType.decision && notesByType.decision.length > 0) {
|
||||
doc += `## Key Decisions\n\n`;
|
||||
notesByType.decision.forEach(content => {
|
||||
doc += `${content}\n\n`;
|
||||
});
|
||||
}
|
||||
|
||||
if (notesByType.gotcha && notesByType.gotcha.length > 0) {
|
||||
doc += `## Gotchas & Learnings\n\n`;
|
||||
notesByType.gotcha.forEach(content => {
|
||||
doc += `${content}\n\n`;
|
||||
});
|
||||
}
|
||||
|
||||
if (notesByType.next_steps && notesByType.next_steps.length > 0) {
|
||||
doc += `## Next Steps\n\n`;
|
||||
notesByType.next_steps.forEach(content => {
|
||||
doc += `${content}\n\n`;
|
||||
});
|
||||
}
|
||||
|
||||
// Plan (if exists)
|
||||
if (plans.length > 0 && plans[0].status === 'executed') {
|
||||
doc += `## Plan\n\n${plans[0].plan_content}\n\n`;
|
||||
}
|
||||
|
||||
// Store documentation in sessions table
|
||||
await execute(
|
||||
`UPDATE sessions SET documentation = $1 WHERE id = $2`,
|
||||
[doc, session_id]
|
||||
);
|
||||
|
||||
return `Documentation generated for session ${session_id} (${doc.length} characters)`;
|
||||
}
|
||||
Reference in New Issue
Block a user