session.tsโข16.2 kB
/**
* Session management tools - restore and summarize sessions
*/
import { Storage } from '../core/storage.js';
import { SessionManager } from '../core/session-manager.js';
import { SearchEngine } from '../core/search.js';
import { GoldfishMemory } from '../types/index.js';
import { SessionRestoreResponse, SessionSummaryResponse } from '../types/responses.js';
import { buildToolContent, OutputMode } from '../core/output-utils.js';
export class SessionTools {
private storage: Storage;
private sessionManager: SessionManager;
private searchEngine: SearchEngine;
constructor(storage: Storage, sessionManager: SessionManager) {
this.storage = storage;
this.sessionManager = sessionManager;
this.searchEngine = new SearchEngine(storage);
}
/**
* Restore session with progressive depth
*/
async restoreSession(args: {
sessionId?: string;
depth?: 'minimal' | 'highlights' | 'full';
workspace?: string;
format?: OutputMode;
} = {}) {
const {
sessionId,
depth = 'highlights',
workspace,
format
} = args;
try {
let targetMemories: GoldfishMemory[] = [];
if (sessionId) {
// Restore specific session
targetMemories = await this.getSessionMemories(sessionId, workspace);
} else {
// Get latest checkpoint
const recentMemories = await this.searchEngine.searchMemories({
type: 'checkpoint',
workspace,
scope: 'current',
limit: 1
});
if (recentMemories.length === 0) {
return {
content: [
{
type: 'text',
text: 'โ No recent checkpoints found. Create your first checkpoint to establish session state!'
}
]
};
}
targetMemories = recentMemories;
// Using latest checkpoint
}
if (targetMemories.length === 0) {
return {
content: [
{
type: 'text',
text: `โ No session found${sessionId ? ` with ID "${sessionId}"` : ''}. It may have expired or was never saved.`
}
]
};
}
// Format output based on depth
const output = [
'โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ',
'๐ RESUMING FROM CHECKPOINT',
'โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ',
''
];
if (depth === 'minimal') {
// Just the latest checkpoint
const latest = targetMemories[0];
if (latest) {
output.push(this.formatCheckpoint(latest, true));
}
} else if (depth === 'highlights') {
// Latest checkpoint + session highlights
const latest = targetMemories[0];
if (latest) {
output.push(this.formatCheckpoint(latest, true));
// Get session highlights
if (typeof latest.content === 'object' && latest.content && 'highlights' in latest.content) {
const contentObj = latest.content as { highlights?: string[] };
if (Array.isArray(contentObj.highlights) && contentObj.highlights.length > 0) {
output.push('\n๐ **Session Highlights:**');
contentObj.highlights.slice(-5).forEach((highlight: string) => {
output.push(` โจ ${highlight}`);
});
}
}
}
} else if (depth === 'full') {
// All checkpoints from session
output.push(`๐ Found ${targetMemories.length} checkpoints:\n`);
targetMemories.slice(0, 10).forEach((memory, index) => {
output.push(`**Checkpoint ${index + 1}** (${this.formatAge(memory.timestamp)})`);
output.push(this.formatCheckpoint(memory, false));
output.push('');
});
if (targetMemories.length > 10) {
output.push(`... and ${targetMemories.length - 10} more checkpoints`);
}
}
output.push('');
output.push('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
output.push('โ
Session restored successfully');
output.push('๐ Ready to continue where you left off!');
output.push('๐ What would you like to work on?');
output.push('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
const formatted = output.join('\n');
const data = {
sessionId: sessionId || 'latest',
depth,
checkpointsFound: targetMemories.length,
highlightsFound: targetMemories.filter((m: GoldfishMemory) =>
typeof m.content === 'object' && m.content && 'highlights' in m.content &&
Array.isArray((m.content as { highlights?: string[] }).highlights) &&
(m.content as { highlights?: string[] }).highlights!.length > 0
).length,
workspace,
sample: targetMemories.slice(0, 3) as unknown as Record<string, unknown>
} as const;
return buildToolContent('session-restore', formatted, data as any, format);
} catch (error) {
return {
content: [
{
type: 'text',
text: `โ Session restoration failed: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
/**
* Summarize session using AI-like condensation
*/
async summarizeSession(args: {
sessionId?: string;
depth?: 'highlights' | 'full';
workspace?: string;
since?: string;
format?: OutputMode;
}) {
const {
sessionId,
depth = 'highlights',
workspace,
since = '1d',
format
} = args;
try {
let memories: GoldfishMemory[] = [];
let summaryTitle = '';
if (sessionId) {
memories = await this.getSessionMemories(sessionId, workspace);
summaryTitle = `Session ${sessionId} Summary`;
} else {
// Summarize recent work
memories = await this.searchEngine.searchMemories({
type: 'checkpoint',
since,
workspace,
scope: 'current',
limit: 50
});
summaryTitle = `Work Summary (${since})`;
}
if (memories.length === 0) {
return {
content: [
{
type: 'text',
text: '๐ No checkpoints found for summary. Create checkpoints as you work to enable session summaries.'
}
]
};
}
// Extract key information
const workAreas = new Set<string>();
const allHighlights: string[] = [];
const gitBranches = new Set<string>();
const activeFiles = new Set<string>();
for (const memory of memories) {
if (typeof memory.content === 'object' && memory.content) {
const content = memory.content as {
description?: string;
workContext?: string;
gitBranch?: string;
activeFiles?: string[];
highlights?: string[]
};
// Collect work areas from descriptions
if (content.description) {
const workArea = this.extractWorkArea(content.description);
if (workArea) workAreas.add(workArea);
}
// Collect highlights
if (Array.isArray(content.highlights)) {
allHighlights.push(...content.highlights);
}
// Collect git info
if (content.gitBranch) {
gitBranches.add(content.gitBranch);
}
// Collect files
if (Array.isArray(content.activeFiles)) {
content.activeFiles.forEach((file: string) => activeFiles.add(file));
}
}
}
// Build summary
const output = [`๐ **${summaryTitle}**\n`];
output.push(`๐ **Overview:**`);
output.push(` โข ${memories.length} checkpoints`);
output.push(` โข ${workAreas.size} work areas`);
if (gitBranches.size > 0) {
output.push(` โข Branches: ${Array.from(gitBranches).join(', ')}`);
}
output.push('');
// Work areas
if (workAreas.size > 0) {
output.push('๐ฏ **Work Areas:**');
Array.from(workAreas).forEach(area => {
output.push(` โข ${area}`);
});
output.push('');
}
// Key highlights
const uniqueHighlights = [...new Set(allHighlights)];
if (uniqueHighlights.length > 0) {
output.push('โจ **Key Accomplishments:**');
uniqueHighlights.slice(-8).forEach(highlight => {
output.push(` โข ${highlight}`);
});
output.push('');
}
// Recent progress (last few checkpoints)
if (depth === 'full' && memories.length > 1) {
output.push('๐ **Recent Progress:**');
memories.slice(0, 5).forEach(memory => {
const age = this.formatAge(memory.timestamp);
if (typeof memory.content === 'object' && memory.content && 'description' in memory.content) {
const contentObj = memory.content as { description?: string };
output.push(` โข ${age}: ${contentObj.description}`);
}
});
output.push('');
}
// Active files
if (activeFiles.size > 0) {
output.push('๐ **Files Involved:**');
Array.from(activeFiles).slice(0, 10).forEach(file => {
output.push(` โข ${file}`);
});
if (activeFiles.size > 10) {
output.push(` ... and ${activeFiles.size - 10} more files`);
}
}
const formatted = output.join('\n');
const data = {
sessionId: sessionId || undefined,
timeRange: since,
workspace,
achievements: uniqueHighlights.slice(-5),
nextSteps: [],
stats: {
checkpoints: memories.length,
workAreas: Array.from(workAreas),
branches: Array.from(gitBranches),
files: Array.from(activeFiles).slice(0, 10)
}
} as const;
return buildToolContent('session-summary', formatted, data as any, format);
} catch (error) {
return {
content: [
{
type: 'text',
text: `โ Summary failed: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
/**
* Get memories for a specific session
*/
private async getSessionMemories(sessionId: string, workspace?: string): Promise<GoldfishMemory[]> {
const memories = await this.searchEngine.searchMemories({
workspace,
scope: workspace ? 'current' : 'all',
limit: 100
});
return memories.filter(m =>
m.sessionId === sessionId ||
m.id === sessionId ||
m.id.startsWith(sessionId) ||
(m.metadata?.sessionId === sessionId)
).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}
/**
* Format a checkpoint for display
*/
private formatCheckpoint(memory: GoldfishMemory, detailed: boolean): string {
const output: string[] = [];
if (typeof memory.content === 'object' && memory.content) {
const content = memory.content as {
description?: string;
workContext?: string;
gitBranch?: string;
activeFiles?: string[];
highlights?: string[]
};
output.push(`๐ **Description:** ${content.description || 'No description'}`);
if (detailed) {
if (content.workContext) {
output.push(`๐ฏ **Context:** ${content.workContext}`);
}
if (content.gitBranch) {
output.push(`๐ฟ **Branch:** ${content.gitBranch}`);
}
if (Array.isArray(content.activeFiles) && content.activeFiles.length > 0) {
output.push(`๐ **Files:** ${content.activeFiles.slice(0, 5).join(', ')}`);
}
}
} else {
output.push(`๐ ${memory.content}`);
}
return output.join('\n');
}
/**
* Extract work area from description
*/
private extractWorkArea(description: string): string | null {
// Simple heuristics to extract work areas
const patterns = [
/(?:working on|implementing|fixing|updating|refactoring)\s+(.+?)(?:\s|$)/i,
/(?:^|\s)(auth|api|database|ui|test|deploy|bug|feature)(?:\s|$)/i
];
for (const pattern of patterns) {
const match = description.match(pattern);
if (match) {
return match[1] || match[0];
}
}
return null;
}
/**
* Format age for display
*/
private formatAge(timestamp: Date): string {
const now = new Date();
const diffMs = now.getTime() - timestamp.getTime();
const diffHours = diffMs / (1000 * 60 * 60);
if (diffHours < 1) {
return 'just now';
} else if (diffHours < 24) {
return `${Math.floor(diffHours)}h ago`;
} else {
const diffDays = Math.floor(diffHours / 24);
return `${diffDays}d ago`;
}
}
/**
* Get tool schemas for MCP
*/
static getToolSchemas() {
return [
{
name: 'restore_session',
description: 'IMMEDIATELY restore context after any break or /clear. ALWAYS use at conversation start if continuing previous work. Critical for continuity. Use depth: "full" when returning after days away.',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'Specific session ID to restore (optional - defaults to latest)'
},
depth: {
type: 'string',
enum: ['minimal', 'highlights', 'full'],
description: 'Restoration depth: minimal=last checkpoint only, highlights=last+key points, full=entire session'
},
workspace: {
type: 'string',
description: 'Workspace name or path (e.g., "coa-goldfish-mcp" or "C:\\source\\COA Goldfish MCP"). Will be normalized automatically.'
},
format: {
type: 'string',
enum: ['plain', 'emoji', 'json', 'dual'],
description: 'Output format override (defaults to env GOLDFISH_OUTPUT_MODE or dual)'
}
}
}
},
{
name: 'summarize_session',
description: 'ALWAYS summarize before ending work sessions. Use when user says "done for today" or asks about accomplishments. Creates shareable progress reports. Essential for handoffs and documentation.',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'Specific session to summarize (optional)'
},
depth: {
type: 'string',
enum: ['highlights', 'full'],
description: 'Summary depth: highlights=key points only, full=detailed timeline'
},
workspace: {
type: 'string',
description: 'Workspace name or path (e.g., "coa-goldfish-mcp" or "C:\\source\\COA Goldfish MCP"). Will be normalized automatically.'
},
since: {
type: 'string',
description: 'Time range for summary when no sessionId (default: "1d")'
},
format: {
type: 'string',
enum: ['plain', 'emoji', 'json', 'dual'],
description: 'Output format override (defaults to env GOLDFISH_OUTPUT_MODE or dual)'
}
}
}
}
];
}
}