Skip to main content
Glama
session-core.ts12.2 kB
/** * Shared session management utilities * Used by both MCP server and Web GUI */ import { Effect } from 'effect' import * as fs from 'node:fs/promises' import * as path from 'node:path' import * as os from 'node:os' // Get Claude sessions directory export const getSessionsDir = (): string => path.join(os.homedir(), '.claude', 'projects') // Get Claude todos directory export const getTodosDir = (): string => path.join(os.homedir(), '.claude', 'todos') // Find agent files linked to a session export const findLinkedAgents = (projectName: string, sessionId: string) => Effect.gen(function* () { const projectPath = path.join(getSessionsDir(), projectName) const files = yield* Effect.tryPromise(() => fs.readdir(projectPath)) const agentFiles = files.filter((f) => f.startsWith('agent-') && f.endsWith('.jsonl')) const linkedAgents: string[] = [] for (const agentFile of agentFiles) { const filePath = path.join(projectPath, agentFile) const content = yield* Effect.tryPromise(() => fs.readFile(filePath, 'utf-8')) const firstLine = content.split('\n')[0] if (firstLine) { try { const parsed = JSON.parse(firstLine) as { sessionId?: string } if (parsed.sessionId === sessionId) { linkedAgents.push(agentFile.replace('.jsonl', '')) } } catch { // Skip invalid JSON } } } return linkedAgents }) // Find orphan agent files (agents whose parent session no longer exists) export const findOrphanAgents = (projectName: string) => Effect.gen(function* () { const projectPath = path.join(getSessionsDir(), projectName) const files = yield* Effect.tryPromise(() => fs.readdir(projectPath)) const sessionIds = new Set( files .filter((f) => !f.startsWith('agent-') && f.endsWith('.jsonl')) .map((f) => f.replace('.jsonl', '')) ) const agentFiles = files.filter((f) => f.startsWith('agent-') && f.endsWith('.jsonl')) const orphanAgents: Array<{ agentId: string; sessionId: string }> = [] for (const agentFile of agentFiles) { const filePath = path.join(projectPath, agentFile) const content = yield* Effect.tryPromise(() => fs.readFile(filePath, 'utf-8')) const firstLine = content.split('\n')[0] if (firstLine) { try { const parsed = JSON.parse(firstLine) as { sessionId?: string } if (parsed.sessionId && !sessionIds.has(parsed.sessionId)) { orphanAgents.push({ agentId: agentFile.replace('.jsonl', ''), sessionId: parsed.sessionId, }) } } catch { // Skip invalid JSON } } } return orphanAgents }) // Delete orphan agent files (move to .bak) export const deleteOrphanAgents = (projectName: string) => Effect.gen(function* () { const projectPath = path.join(getSessionsDir(), projectName) const orphans = yield* findOrphanAgents(projectName) // Create backup directory const backupDir = path.join(projectPath, '.bak') yield* Effect.tryPromise(() => fs.mkdir(backupDir, { recursive: true })) const deletedAgents: string[] = [] for (const orphan of orphans) { const agentPath = path.join(projectPath, `${orphan.agentId}.jsonl`) const agentBackupPath = path.join(backupDir, `${orphan.agentId}.jsonl`) yield* Effect.tryPromise(() => fs.rename(agentPath, agentBackupPath)) deletedAgents.push(orphan.agentId) } return { success: true, deletedAgents, count: deletedAgents.length } }) // Todo item interface export interface TodoItem { content: string status: 'pending' | 'in_progress' | 'completed' activeForm?: string } export interface SessionTodos { sessionId: string sessionTodos: TodoItem[] agentTodos: { agentId: string; todos: TodoItem[] }[] hasTodos: boolean } // Find linked todo files for a session and its agents export const findLinkedTodos = (sessionId: string, agentIds: string[]) => Effect.gen(function* () { const todosDir = getTodosDir() // Check if todos directory exists const exists = yield* Effect.tryPromise(() => fs .access(todosDir) .then(() => true) .catch(() => false) ) if (!exists) { return { sessionId, sessionTodos: [], agentTodos: [], hasTodos: false, } satisfies SessionTodos } // Read session's own todo file const sessionTodoPath = path.join(todosDir, `${sessionId}.json`) let sessionTodos: TodoItem[] = [] const sessionTodoExists = yield* Effect.tryPromise(() => fs .access(sessionTodoPath) .then(() => true) .catch(() => false) ) if (sessionTodoExists) { const content = yield* Effect.tryPromise(() => fs.readFile(sessionTodoPath, 'utf-8')) try { sessionTodos = JSON.parse(content) as TodoItem[] } catch { // Invalid JSON, treat as empty } } // Read agent todo files const agentTodos: { agentId: string; todos: TodoItem[] }[] = [] for (const agentId of agentIds) { // Agent todo files are named: {sessionId}-{agentId}.json const shortAgentId = agentId.replace('agent-', '') const agentTodoPath = path.join(todosDir, `${sessionId}-agent-${shortAgentId}.json`) const agentTodoExists = yield* Effect.tryPromise(() => fs .access(agentTodoPath) .then(() => true) .catch(() => false) ) if (agentTodoExists) { const content = yield* Effect.tryPromise(() => fs.readFile(agentTodoPath, 'utf-8')) try { const todos = JSON.parse(content) as TodoItem[] agentTodos.push({ agentId, todos }) } catch { // Invalid JSON, skip } } } const hasTodos = sessionTodos.length > 0 || agentTodos.some((at) => at.todos.length > 0) return { sessionId, sessionTodos, agentTodos, hasTodos, } satisfies SessionTodos }) // Check if session has any todos (quick check) export const sessionHasTodos = (sessionId: string, agentIds: string[]) => Effect.gen(function* () { const todosDir = getTodosDir() // Check if todos directory exists const exists = yield* Effect.tryPromise(() => fs .access(todosDir) .then(() => true) .catch(() => false) ) if (!exists) return false // Check session's own todo file const sessionTodoPath = path.join(todosDir, `${sessionId}.json`) const sessionTodoExists = yield* Effect.tryPromise(() => fs .access(sessionTodoPath) .then(() => true) .catch(() => false) ) if (sessionTodoExists) { const content = yield* Effect.tryPromise(() => fs.readFile(sessionTodoPath, 'utf-8')) try { const todos = JSON.parse(content) as TodoItem[] if (todos.length > 0) return true } catch { // Invalid JSON, continue } } // Check agent todo files for (const agentId of agentIds) { const shortAgentId = agentId.replace('agent-', '') const agentTodoPath = path.join(todosDir, `${sessionId}-agent-${shortAgentId}.json`) const agentTodoExists = yield* Effect.tryPromise(() => fs .access(agentTodoPath) .then(() => true) .catch(() => false) ) if (agentTodoExists) { const content = yield* Effect.tryPromise(() => fs.readFile(agentTodoPath, 'utf-8')) try { const todos = JSON.parse(content) as TodoItem[] if (todos.length > 0) return true } catch { // Invalid JSON, continue } } } return false }) // Delete linked todo files for a session (move to .bak) export const deleteLinkedTodos = (sessionId: string, agentIds: string[]) => Effect.gen(function* () { const todosDir = getTodosDir() // Check if todos directory exists const exists = yield* Effect.tryPromise(() => fs .access(todosDir) .then(() => true) .catch(() => false) ) if (!exists) return { deletedCount: 0 } // Create backup directory const backupDir = path.join(todosDir, '.bak') yield* Effect.tryPromise(() => fs.mkdir(backupDir, { recursive: true })) let deletedCount = 0 // Delete session's own todo file const sessionTodoPath = path.join(todosDir, `${sessionId}.json`) const sessionTodoExists = yield* Effect.tryPromise(() => fs .access(sessionTodoPath) .then(() => true) .catch(() => false) ) if (sessionTodoExists) { const backupPath = path.join(backupDir, `${sessionId}.json`) yield* Effect.tryPromise(() => fs.rename(sessionTodoPath, backupPath)) deletedCount++ } // Delete agent todo files for (const agentId of agentIds) { const shortAgentId = agentId.replace('agent-', '') const agentTodoPath = path.join(todosDir, `${sessionId}-agent-${shortAgentId}.json`) const agentTodoExists = yield* Effect.tryPromise(() => fs .access(agentTodoPath) .then(() => true) .catch(() => false) ) if (agentTodoExists) { const backupPath = path.join(backupDir, `${sessionId}-agent-${shortAgentId}.json`) yield* Effect.tryPromise(() => fs.rename(agentTodoPath, backupPath)) deletedCount++ } } return { deletedCount } }) // Find all orphan todo files (session no longer exists) export const findOrphanTodos = () => Effect.gen(function* () { const todosDir = getTodosDir() const sessionsDir = getSessionsDir() // Check if directories exist const [todosExists, sessionsExists] = yield* Effect.all([ Effect.tryPromise(() => fs .access(todosDir) .then(() => true) .catch(() => false) ), Effect.tryPromise(() => fs .access(sessionsDir) .then(() => true) .catch(() => false) ), ]) if (!todosExists || !sessionsExists) return [] // Get all todo files const todoFiles = yield* Effect.tryPromise(() => fs.readdir(todosDir)) const jsonFiles = todoFiles.filter((f) => f.endsWith('.json')) // Build set of all valid session IDs across all projects const validSessionIds = new Set<string>() const projectEntries = yield* Effect.tryPromise(() => fs.readdir(sessionsDir, { withFileTypes: true }) ) for (const entry of projectEntries) { if (!entry.isDirectory() || entry.name.startsWith('.')) continue const projectPath = path.join(sessionsDir, entry.name) const files = yield* Effect.tryPromise(() => fs.readdir(projectPath)) for (const f of files) { if (f.endsWith('.jsonl') && !f.startsWith('agent-')) { validSessionIds.add(f.replace('.jsonl', '')) } } } // Find orphan todo files const orphans: string[] = [] for (const todoFile of jsonFiles) { // Parse session ID from todo filename // Format: {sessionId}.json or {sessionId}-agent-{agentId}.json const match = todoFile.match(/^([a-f0-9-]+)(?:-agent-[a-f0-9]+)?\.json$/) if (match) { const sessionId = match[1] if (!validSessionIds.has(sessionId)) { orphans.push(todoFile) } } } return orphans }) // Delete orphan todo files export const deleteOrphanTodos = () => Effect.gen(function* () { const todosDir = getTodosDir() const orphans = yield* findOrphanTodos() if (orphans.length === 0) return { success: true, deletedCount: 0 } // Create backup directory const backupDir = path.join(todosDir, '.bak') yield* Effect.tryPromise(() => fs.mkdir(backupDir, { recursive: true })) let deletedCount = 0 for (const orphan of orphans) { const filePath = path.join(todosDir, orphan) const backupPath = path.join(backupDir, orphan) yield* Effect.tryPromise(() => fs.rename(filePath, backupPath)) deletedCount++ } return { success: true, deletedCount } })

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DrumRobot/claude-sessions-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server