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:
211
migrations/016_session_documentation.sql
Normal file
211
migrations/016_session_documentation.sql
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
-- Migration 016: Session Documentation System
|
||||||
|
-- Purpose: Migrate from file-based documentation (CLAUDE.md, plan files) to database
|
||||||
|
-- Dependencies: 010_sessions.sql (sessions table), 001_base_schema.sql (pgvector)
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- SESSION NOTES: Structured notes within sessions
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS session_notes (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
||||||
|
note_type TEXT NOT NULL CHECK (note_type IN (
|
||||||
|
'accomplishment',
|
||||||
|
'decision',
|
||||||
|
'gotcha',
|
||||||
|
'next_steps',
|
||||||
|
'context'
|
||||||
|
)),
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
embedding vector(1024) -- For semantic search of notes
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for efficient querying
|
||||||
|
CREATE INDEX idx_session_notes_session ON session_notes(session_id);
|
||||||
|
CREATE INDEX idx_session_notes_type ON session_notes(note_type);
|
||||||
|
CREATE INDEX idx_session_notes_created ON session_notes(created_at DESC);
|
||||||
|
|
||||||
|
-- HNSW index for semantic similarity search
|
||||||
|
CREATE INDEX idx_session_notes_embedding ON session_notes
|
||||||
|
USING hnsw (embedding vector_cosine_ops);
|
||||||
|
|
||||||
|
COMMENT ON TABLE session_notes IS 'Structured notes within sessions (replaces ad-hoc note files)';
|
||||||
|
COMMENT ON COLUMN session_notes.note_type IS 'Category of note: accomplishment, decision, gotcha, next_steps, context';
|
||||||
|
COMMENT ON COLUMN session_notes.embedding IS 'Vector embedding for semantic search across notes';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- SESSION PLANS: Plan mode plans with lifecycle tracking
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS session_plans (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
||||||
|
plan_file_name TEXT, -- Original filename (e.g., "transient-forging-reddy.md")
|
||||||
|
plan_content TEXT NOT NULL,
|
||||||
|
status TEXT DEFAULT 'draft' CHECK (status IN (
|
||||||
|
'draft',
|
||||||
|
'approved',
|
||||||
|
'executed',
|
||||||
|
'abandoned'
|
||||||
|
)),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
approved_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
completed_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
embedding vector(1024) -- For semantic search of plans
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for efficient querying
|
||||||
|
CREATE INDEX idx_session_plans_session ON session_plans(session_id);
|
||||||
|
CREATE INDEX idx_session_plans_status ON session_plans(status);
|
||||||
|
CREATE INDEX idx_session_plans_created ON session_plans(created_at DESC);
|
||||||
|
|
||||||
|
-- HNSW index for semantic similarity search
|
||||||
|
CREATE INDEX idx_session_plans_embedding ON session_plans
|
||||||
|
USING hnsw (embedding vector_cosine_ops);
|
||||||
|
|
||||||
|
COMMENT ON TABLE session_plans IS 'Plan mode plans with lifecycle tracking (replaces ~/.claude/plans/)';
|
||||||
|
COMMENT ON COLUMN session_plans.plan_file_name IS 'Original filename from plan mode (e.g., eloquent-yellow-cat.md)';
|
||||||
|
COMMENT ON COLUMN session_plans.status IS 'Plan lifecycle: draft → approved → executed/abandoned';
|
||||||
|
COMMENT ON COLUMN session_plans.embedding IS 'Vector embedding for semantic search across plans';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- PROJECT DOCUMENTATION: Persistent project-level docs
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS project_documentation (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
project TEXT NOT NULL REFERENCES projects(key) ON DELETE CASCADE,
|
||||||
|
doc_type TEXT NOT NULL CHECK (doc_type IN (
|
||||||
|
'overview',
|
||||||
|
'architecture',
|
||||||
|
'guidelines',
|
||||||
|
'history'
|
||||||
|
)),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
last_updated_session TEXT REFERENCES sessions(id) ON DELETE SET NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
UNIQUE(project, doc_type) -- One doc of each type per project
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for efficient querying
|
||||||
|
CREATE INDEX idx_project_docs_project ON project_documentation(project);
|
||||||
|
CREATE INDEX idx_project_docs_type ON project_documentation(doc_type);
|
||||||
|
CREATE INDEX idx_project_docs_updated ON project_documentation(updated_at DESC);
|
||||||
|
|
||||||
|
COMMENT ON TABLE project_documentation IS 'Persistent project documentation (replaces CLAUDE.md sections)';
|
||||||
|
COMMENT ON COLUMN project_documentation.doc_type IS 'Type of documentation: overview, architecture, guidelines, history';
|
||||||
|
COMMENT ON COLUMN project_documentation.last_updated_session IS 'Session that last updated this documentation';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- SESSIONS TABLE ENHANCEMENTS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Add documentation column for full markdown documentation
|
||||||
|
ALTER TABLE sessions
|
||||||
|
ADD COLUMN IF NOT EXISTS documentation TEXT;
|
||||||
|
|
||||||
|
-- Add migration flag to track CLAUDE.md migration status
|
||||||
|
ALTER TABLE sessions
|
||||||
|
ADD COLUMN IF NOT EXISTS claude_md_migrated BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN sessions.documentation IS 'Full markdown documentation for session (auto-generated at end)';
|
||||||
|
COMMENT ON COLUMN sessions.claude_md_migrated IS 'Flag indicating this session migrated from CLAUDE.md file';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- HELPER VIEWS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- View: Session with note counts
|
||||||
|
CREATE OR REPLACE VIEW session_documentation_summary AS
|
||||||
|
SELECT
|
||||||
|
s.id,
|
||||||
|
s.project,
|
||||||
|
s.session_number,
|
||||||
|
s.started_at,
|
||||||
|
s.ended_at,
|
||||||
|
s.status,
|
||||||
|
s.summary,
|
||||||
|
COUNT(DISTINCT sn.id) as note_count,
|
||||||
|
COUNT(DISTINCT sp.id) as plan_count,
|
||||||
|
ARRAY_AGG(DISTINCT sn.note_type) FILTER (WHERE sn.note_type IS NOT NULL) as note_types,
|
||||||
|
MAX(sn.created_at) as last_note_at,
|
||||||
|
MAX(sp.created_at) as last_plan_at
|
||||||
|
FROM sessions s
|
||||||
|
LEFT JOIN session_notes sn ON s.id = sn.session_id
|
||||||
|
LEFT JOIN session_plans sp ON s.id = sp.session_id
|
||||||
|
GROUP BY s.id, s.project, s.session_number, s.started_at, s.ended_at, s.status, s.summary;
|
||||||
|
|
||||||
|
COMMENT ON VIEW session_documentation_summary IS 'Sessions with note and plan counts for quick overview';
|
||||||
|
|
||||||
|
-- View: Latest project documentation
|
||||||
|
CREATE OR REPLACE VIEW latest_project_docs AS
|
||||||
|
SELECT
|
||||||
|
pd.project,
|
||||||
|
pd.doc_type,
|
||||||
|
pd.title,
|
||||||
|
LEFT(pd.content, 200) as content_preview,
|
||||||
|
pd.updated_at,
|
||||||
|
s.session_number as last_updated_session_number,
|
||||||
|
s.started_at as last_updated_session_date
|
||||||
|
FROM project_documentation pd
|
||||||
|
LEFT JOIN sessions s ON pd.last_updated_session = s.id
|
||||||
|
ORDER BY pd.project, pd.doc_type;
|
||||||
|
|
||||||
|
COMMENT ON VIEW latest_project_docs IS 'Latest project documentation with session context';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- MAINTENANCE FUNCTIONS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Function to clean up old session notes (optional retention policy)
|
||||||
|
CREATE OR REPLACE FUNCTION cleanup_old_session_notes(retention_days INTEGER DEFAULT 180)
|
||||||
|
RETURNS INTEGER AS $$
|
||||||
|
DECLARE
|
||||||
|
deleted_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM session_notes
|
||||||
|
WHERE created_at < NOW() - (retention_days || ' days')::INTERVAL
|
||||||
|
AND session_id IN (
|
||||||
|
SELECT id FROM sessions WHERE status = 'completed'
|
||||||
|
);
|
||||||
|
|
||||||
|
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
||||||
|
RETURN deleted_count;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION cleanup_old_session_notes IS 'Clean up notes from completed sessions older than retention period (default 180 days)';
|
||||||
|
|
||||||
|
-- Function to generate session documentation stats
|
||||||
|
CREATE OR REPLACE FUNCTION session_documentation_stats(p_project TEXT DEFAULT NULL)
|
||||||
|
RETURNS TABLE(
|
||||||
|
total_sessions BIGINT,
|
||||||
|
sessions_with_notes BIGINT,
|
||||||
|
sessions_with_plans BIGINT,
|
||||||
|
total_notes BIGINT,
|
||||||
|
total_plans BIGINT,
|
||||||
|
avg_notes_per_session NUMERIC,
|
||||||
|
avg_plans_per_session NUMERIC
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
COUNT(DISTINCT s.id) as total_sessions,
|
||||||
|
COUNT(DISTINCT CASE WHEN sn.id IS NOT NULL THEN s.id END) as sessions_with_notes,
|
||||||
|
COUNT(DISTINCT CASE WHEN sp.id IS NOT NULL THEN s.id END) as sessions_with_plans,
|
||||||
|
COUNT(sn.id) as total_notes,
|
||||||
|
COUNT(sp.id) as total_plans,
|
||||||
|
ROUND(COUNT(sn.id)::NUMERIC / NULLIF(COUNT(DISTINCT s.id), 0), 2) as avg_notes_per_session,
|
||||||
|
ROUND(COUNT(sp.id)::NUMERIC / NULLIF(COUNT(DISTINCT s.id), 0), 2) as avg_plans_per_session
|
||||||
|
FROM sessions s
|
||||||
|
LEFT JOIN session_notes sn ON s.id = sn.session_id
|
||||||
|
LEFT JOIN session_plans sp ON s.id = sp.session_id
|
||||||
|
WHERE (p_project IS NULL OR s.project = p_project)
|
||||||
|
AND s.status = 'completed';
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION session_documentation_stats IS 'Generate statistics about session documentation usage';
|
||||||
90
regenerate_tool_docs_embeddings.mjs
Normal file
90
regenerate_tool_docs_embeddings.mjs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import pg from 'pg';
|
||||||
|
const { Pool } = pg;
|
||||||
|
|
||||||
|
// Database configuration
|
||||||
|
const pool = new Pool({
|
||||||
|
host: 'infra.agiliton.internal',
|
||||||
|
port: 5432,
|
||||||
|
database: 'agiliton',
|
||||||
|
user: 'agiliton',
|
||||||
|
password: 'QtqiwCOAUpQNF6pjzOMAREzUny2bY8V1',
|
||||||
|
max: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
// LiteLLM API configuration
|
||||||
|
const LLM_API_URL = 'https://api.agiliton.cloud/llm';
|
||||||
|
const LLM_API_KEY = 'sk-litellm-master-key';
|
||||||
|
|
||||||
|
async function getEmbedding(text) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${LLM_API_URL}/v1/embeddings`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${LLM_API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'mxbai-embed-large',
|
||||||
|
input: text,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Embedding API error:', response.status, await response.text());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data?.[0]?.embedding || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Embedding generation failed:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function regenerateEmbeddings() {
|
||||||
|
try {
|
||||||
|
// Fetch all tool_docs
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT id, tool_name, title, description, notes, tags FROM tool_docs WHERE embedding IS NULL'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Found ${result.rows.length} tool_docs without embeddings`);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
for (const row of result.rows) {
|
||||||
|
const embedText = `${row.title}. ${row.description}. ${row.tags?.join(' ') || ''}. ${row.notes || ''}`;
|
||||||
|
|
||||||
|
console.log(`Generating embedding for: ${row.tool_name}`);
|
||||||
|
const embedding = await getEmbedding(embedText);
|
||||||
|
|
||||||
|
if (embedding) {
|
||||||
|
const embeddingStr = `[${embedding.join(',')}]`;
|
||||||
|
await pool.query(
|
||||||
|
'UPDATE tool_docs SET embedding = $1 WHERE id = $2',
|
||||||
|
[embeddingStr, row.id]
|
||||||
|
);
|
||||||
|
successCount++;
|
||||||
|
console.log(` ✓ ${row.tool_name} (${successCount}/${result.rows.length})`);
|
||||||
|
} else {
|
||||||
|
failCount++;
|
||||||
|
console.log(` ✗ ${row.tool_name} - Failed to generate embedding`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to avoid rate limiting
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n✅ Complete: ${successCount} successful, ${failCount} failed`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
regenerateEmbeddings();
|
||||||
87
src/index.ts
87
src/index.ts
@@ -49,6 +49,17 @@ import {
|
|||||||
buildRecord,
|
buildRecord,
|
||||||
sessionCommitLink,
|
sessionCommitLink,
|
||||||
} from './tools/sessions.js';
|
} from './tools/sessions.js';
|
||||||
|
import {
|
||||||
|
sessionNoteAdd,
|
||||||
|
sessionNotesList,
|
||||||
|
sessionPlanSave,
|
||||||
|
sessionPlanUpdateStatus,
|
||||||
|
sessionPlanList,
|
||||||
|
projectDocUpsert,
|
||||||
|
projectDocGet,
|
||||||
|
projectDocList,
|
||||||
|
sessionDocumentationGenerate,
|
||||||
|
} from './tools/session-docs.js';
|
||||||
|
|
||||||
// Create MCP server
|
// Create MCP server
|
||||||
const server = new Server(
|
const server = new Server(
|
||||||
@@ -476,6 +487,82 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// Session Documentation
|
||||||
|
case 'session_note_add':
|
||||||
|
result = await sessionNoteAdd({
|
||||||
|
session_id: a.session_id,
|
||||||
|
note_type: a.note_type,
|
||||||
|
content: a.content,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'session_notes_list':
|
||||||
|
result = JSON.stringify(
|
||||||
|
await sessionNotesList({
|
||||||
|
session_id: a.session_id,
|
||||||
|
note_type: a.note_type,
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'session_plan_save':
|
||||||
|
result = await sessionPlanSave({
|
||||||
|
session_id: a.session_id,
|
||||||
|
plan_content: a.plan_content,
|
||||||
|
plan_file_name: a.plan_file_name,
|
||||||
|
status: a.status,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'session_plan_update_status':
|
||||||
|
result = await sessionPlanUpdateStatus({
|
||||||
|
plan_id: a.plan_id,
|
||||||
|
status: a.status,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'session_plan_list':
|
||||||
|
result = JSON.stringify(
|
||||||
|
await sessionPlanList({
|
||||||
|
session_id: a.session_id,
|
||||||
|
status: a.status,
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'project_doc_upsert':
|
||||||
|
result = await projectDocUpsert({
|
||||||
|
project: a.project,
|
||||||
|
doc_type: a.doc_type,
|
||||||
|
title: a.title,
|
||||||
|
content: a.content,
|
||||||
|
session_id: a.session_id,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'project_doc_get':
|
||||||
|
result = JSON.stringify(
|
||||||
|
await projectDocGet({
|
||||||
|
project: a.project,
|
||||||
|
doc_type: a.doc_type,
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'project_doc_list':
|
||||||
|
result = JSON.stringify(
|
||||||
|
await projectDocList({
|
||||||
|
project: a.project,
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'session_documentation_generate':
|
||||||
|
result = await sessionDocumentationGenerate({
|
||||||
|
session_id: a.session_id,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tool: ${name}`);
|
throw new Error(`Unknown tool: ${name}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -825,4 +825,118 @@ export const toolDefinitions = [
|
|||||||
required: ['session_id', 'commit_sha', 'repo'],
|
required: ['session_id', 'commit_sha', 'repo'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Session Documentation Tools
|
||||||
|
{
|
||||||
|
name: 'session_note_add',
|
||||||
|
description: 'Add a note to current session with auto-generated embedding',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
session_id: { type: 'string', description: 'Session ID' },
|
||||||
|
note_type: { type: 'string', enum: ['accomplishment', 'decision', 'gotcha', 'next_steps', 'context'], description: 'Category of note' },
|
||||||
|
content: { type: 'string', description: 'Note content' },
|
||||||
|
},
|
||||||
|
required: ['session_id', 'note_type', 'content'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'session_notes_list',
|
||||||
|
description: 'List all notes for a session, optionally filtered by type',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
session_id: { type: 'string', description: 'Session ID' },
|
||||||
|
note_type: { type: 'string', enum: ['accomplishment', 'decision', 'gotcha', 'next_steps', 'context'], description: 'Filter by note type (optional)' },
|
||||||
|
},
|
||||||
|
required: ['session_id'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'session_plan_save',
|
||||||
|
description: 'Save a plan to database with semantic embedding',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
session_id: { type: 'string', description: 'Session ID' },
|
||||||
|
plan_content: { type: 'string', description: 'Plan content in markdown' },
|
||||||
|
plan_file_name: { type: 'string', description: 'Original filename (e.g., eloquent-yellow-cat.md) - optional' },
|
||||||
|
status: { type: 'string', enum: ['draft', 'approved', 'executed', 'abandoned'], description: 'Plan status (default: draft)' },
|
||||||
|
},
|
||||||
|
required: ['session_id', 'plan_content'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'session_plan_update_status',
|
||||||
|
description: 'Update plan status (draft → approved → executed)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
plan_id: { type: 'number', description: 'Plan ID to update' },
|
||||||
|
status: { type: 'string', enum: ['draft', 'approved', 'executed', 'abandoned'], description: 'New status' },
|
||||||
|
},
|
||||||
|
required: ['plan_id', 'status'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'session_plan_list',
|
||||||
|
description: 'List plans for a session',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
session_id: { type: 'string', description: 'Session ID' },
|
||||||
|
status: { type: 'string', enum: ['draft', 'approved', 'executed', 'abandoned'], description: 'Filter by status (optional)' },
|
||||||
|
},
|
||||||
|
required: ['session_id'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'project_doc_upsert',
|
||||||
|
description: 'Create or update project documentation (replaces CLAUDE.md sections)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
project: { type: 'string', description: 'Project key (e.g., CF, VPN)' },
|
||||||
|
doc_type: { type: 'string', enum: ['overview', 'architecture', 'guidelines', 'history'], description: 'Documentation type' },
|
||||||
|
title: { type: 'string', description: 'Document title' },
|
||||||
|
content: { type: 'string', description: 'Document content in markdown' },
|
||||||
|
session_id: { type: 'string', description: 'Session ID (optional)' },
|
||||||
|
},
|
||||||
|
required: ['project', 'doc_type', 'title', 'content'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'project_doc_get',
|
||||||
|
description: 'Get specific project documentation by type',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
project: { type: 'string', description: 'Project key' },
|
||||||
|
doc_type: { type: 'string', enum: ['overview', 'architecture', 'guidelines', 'history'], description: 'Documentation type' },
|
||||||
|
},
|
||||||
|
required: ['project', 'doc_type'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'project_doc_list',
|
||||||
|
description: 'List all documentation for a project',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
project: { type: 'string', description: 'Project key' },
|
||||||
|
},
|
||||||
|
required: ['project'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'session_documentation_generate',
|
||||||
|
description: 'Auto-generate full markdown documentation for a session (tasks, commits, notes, plans)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
session_id: { type: 'string', description: 'Session ID' },
|
||||||
|
},
|
||||||
|
required: ['session_id'],
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
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