refactor(src): ♻️ Restructure src/index.ts exports and initialization logic for improved maintainability
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
962bab4485
commit
0ad3205dc8
1 changed files with 136 additions and 368 deletions
504
src/index.ts
504
src/index.ts
|
|
@ -3,52 +3,41 @@
|
|||
* MCP Task Persistence Server
|
||||
*
|
||||
* Persists user prompts across Claude Code sessions for task recovery.
|
||||
* Tasks are stored in ~/.local/claude/tasks/ with depth-first path naming.
|
||||
* Tasks are stored in ~/.local/claude/tasks/ with timestamp_slug_project naming.
|
||||
*/
|
||||
|
||||
import { homedir } from 'os';
|
||||
import { join, sep } from 'path';
|
||||
import { existsSync, readdirSync, statSync, utimesSync } from 'fs';
|
||||
import { mkdir, writeFile, readFile, unlink, appendFile } from 'fs/promises';
|
||||
import { existsSync, utimesSync } from 'fs';
|
||||
import { mkdir, writeFile, readFile, readdir, unlink, appendFile, stat } from 'fs/promises';
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { Server } from '@modelcontextprotocol/sdk/server';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio';
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types';
|
||||
|
||||
const TASKS_DIR = join(homedir(), '.local', 'claude', 'tasks');
|
||||
const SLUG_MAX_LENGTH = 30;
|
||||
|
||||
/**
|
||||
* Slugify a string for use in filenames
|
||||
* - Lowercase
|
||||
* - Replace spaces and underscores with dashes
|
||||
* - Remove special chars except alphanumeric and dashes
|
||||
* - Truncate to max length
|
||||
* - Remove trailing dashes
|
||||
*/
|
||||
function slugify(text: string, maxLength = SLUG_MAX_LENGTH): string {
|
||||
if (!text || !text.trim()) {
|
||||
return 'task';
|
||||
}
|
||||
if (!text?.trim()) return 'task';
|
||||
|
||||
let slug = text
|
||||
.toLowerCase()
|
||||
.replace(/[\s_]+/g, '-') // spaces/underscores → dashes
|
||||
.replace(/[^a-z0-9-]/g, '') // remove non-alphanumeric except dashes
|
||||
.replace(/-+/g, '-') // collapse multiple dashes
|
||||
.replace(/^-|-$/g, ''); // trim leading/trailing dashes
|
||||
.replace(/[\s_]+/g, '-')
|
||||
.replace(/[^a-z0-9-]/g, '')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
if (slug.length > maxLength) {
|
||||
slug = slug.substring(0, maxLength);
|
||||
// Don't cut in the middle of a word if possible
|
||||
const lastDash = slug.lastIndexOf('-');
|
||||
if (lastDash > maxLength * 0.6) {
|
||||
slug = slug.substring(0, lastDash);
|
||||
}
|
||||
slug = slug.replace(/-$/g, ''); // trim trailing dash after truncation
|
||||
slug = slug.replace(/-$/g, '');
|
||||
}
|
||||
|
||||
return slug || 'task';
|
||||
|
|
@ -56,13 +45,10 @@ function slugify(text: string, maxLength = SLUG_MAX_LENGTH): string {
|
|||
|
||||
/**
|
||||
* Extract project name from working directory path
|
||||
* /var/home/lilith/Code/@applications/@lilith/lilith-platform → lilith-platform
|
||||
*/
|
||||
function extractProjectName(workingDir: string): string {
|
||||
const parts = workingDir.split(sep).filter(Boolean);
|
||||
const lastPart = parts[parts.length - 1] || 'unknown';
|
||||
// Slugify the project name (handles @ symbols etc.)
|
||||
return slugify(lastPart, 30);
|
||||
return slugify(parts[parts.length - 1] || 'unknown', 30);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -74,69 +60,35 @@ async function ensureTasksDir(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a path to depth-first filename format (legacy format)
|
||||
* /var/home/lilith/Code/packages/ -> packages-Code-lilith-home-var
|
||||
*/
|
||||
function pathToDepthFirst(workingDir: string): string {
|
||||
const parts = workingDir.split(sep).filter(Boolean);
|
||||
return parts.reverse().join('-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a task filename from timestamp, prompt, and working directory
|
||||
* New format: {timestamp}_{prompt-slug}_{project-name}.txt
|
||||
*/
|
||||
function generateTaskFilename(timestamp: number, prompt: string, workingDir: string): string {
|
||||
const promptSlug = slugify(prompt);
|
||||
const projectName = extractProjectName(workingDir);
|
||||
return `${timestamp}_${promptSlug}_${projectName}.txt`;
|
||||
}
|
||||
|
||||
interface ParsedTaskFilename {
|
||||
timestamp: number;
|
||||
slug: string;
|
||||
project: string;
|
||||
// For backward compatibility
|
||||
pathInfo?: string;
|
||||
isLegacyFormat: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse task filename to extract components
|
||||
* Supports both new format: {timestamp}_{slug}_{project}.txt
|
||||
* And legacy format: {timestamp}_{reversed-path}.txt
|
||||
* Parse task filename: {timestamp}_{slug}_{project}.txt
|
||||
*/
|
||||
function parseTaskFilename(filename: string): ParsedTaskFilename | null {
|
||||
// Try new format first: {timestamp}_{slug}_{project}.txt
|
||||
// New format has exactly 2 underscores separating 3 parts
|
||||
const newMatch = filename.match(/^(\d+)_([a-z0-9-]+)_([a-z0-9-]+)\.txt$/);
|
||||
if (newMatch) {
|
||||
return {
|
||||
timestamp: parseInt(newMatch[1], 10),
|
||||
slug: newMatch[2],
|
||||
project: newMatch[3],
|
||||
isLegacyFormat: false,
|
||||
};
|
||||
}
|
||||
const match = filename.match(/^(\d+)_([a-z0-9-]+)_([a-z0-9-]+)\.txt$/);
|
||||
if (!match) return null;
|
||||
|
||||
// Fall back to legacy format: {timestamp}_{anything}.txt
|
||||
const legacyMatch = filename.match(/^(\d+)_(.+)\.txt$/);
|
||||
if (legacyMatch) {
|
||||
return {
|
||||
timestamp: parseInt(legacyMatch[1], 10),
|
||||
slug: '',
|
||||
project: '',
|
||||
pathInfo: legacyMatch[2],
|
||||
isLegacyFormat: true,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
return {
|
||||
timestamp: parseInt(match[1], 10),
|
||||
slug: match[2],
|
||||
project: match[3],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new task with structured content format
|
||||
* Generate a task filename
|
||||
*/
|
||||
function generateTaskFilename(timestamp: number, prompt: string, workingDir: string): string {
|
||||
return `${timestamp}_${slugify(prompt)}_${extractProjectName(workingDir)}.txt`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new task
|
||||
*/
|
||||
async function saveTask(prompt: string, workingDir: string): Promise<{ taskId: string; filename: string }> {
|
||||
await ensureTasksDir();
|
||||
|
|
@ -145,25 +97,54 @@ async function saveTask(prompt: string, workingDir: string): Promise<{ taskId: s
|
|||
const filename = generateTaskFilename(timestamp, prompt, workingDir);
|
||||
const filepath = join(TASKS_DIR, filename);
|
||||
|
||||
// Write structured content with header
|
||||
const createdDate = new Date(timestamp).toISOString();
|
||||
const content = `[Task: ${timestamp}]
|
||||
[Directory: ${workingDir}]
|
||||
[Created: ${createdDate}]
|
||||
[Created: ${new Date(timestamp).toISOString()}]
|
||||
|
||||
${prompt}
|
||||
`;
|
||||
|
||||
await writeFile(filepath, content, 'utf-8');
|
||||
|
||||
return {
|
||||
taskId: timestamp.toString(),
|
||||
filename,
|
||||
};
|
||||
return { taskId: timestamp.toString(), filename };
|
||||
}
|
||||
|
||||
/**
|
||||
* Append content to an existing task (for conversation history)
|
||||
* Find task file by working directory (most recently modified)
|
||||
*/
|
||||
async function findTaskByWorkingDir(workingDir: string): Promise<string | null> {
|
||||
if (!existsSync(TASKS_DIR)) return null;
|
||||
|
||||
const projectName = extractProjectName(workingDir);
|
||||
const suffix = `_${projectName}.txt`;
|
||||
const files = await readdir(TASKS_DIR);
|
||||
const matching = files.filter((f) => f.endsWith(suffix));
|
||||
|
||||
if (matching.length === 0) return null;
|
||||
|
||||
const withMtime = await Promise.all(
|
||||
matching.map(async (f) => ({
|
||||
file: f,
|
||||
mtime: (await stat(join(TASKS_DIR, f))).mtime.getTime(),
|
||||
}))
|
||||
);
|
||||
|
||||
const newest = withMtime.reduce((a, b) => (a.mtime > b.mtime ? a : b));
|
||||
return join(TASKS_DIR, newest.file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find task file by timestamp prefix
|
||||
*/
|
||||
async function findTaskByTimestamp(taskId: string): Promise<string | null> {
|
||||
if (!existsSync(TASKS_DIR)) return null;
|
||||
|
||||
const files = await readdir(TASKS_DIR);
|
||||
const match = files.find((f) => f.startsWith(`${taskId}_`));
|
||||
return match ? join(TASKS_DIR, match) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append content to an existing task
|
||||
*/
|
||||
async function appendToTask(
|
||||
content: string,
|
||||
|
|
@ -174,66 +155,22 @@ async function appendToTask(
|
|||
let filepath: string | null = null;
|
||||
|
||||
if (taskId) {
|
||||
filepath = findTaskByTimestamp(taskId);
|
||||
filepath = await findTaskByTimestamp(taskId);
|
||||
} else if (workingDir) {
|
||||
filepath = findTaskByWorkingDir(workingDir);
|
||||
filepath = await findTaskByWorkingDir(workingDir);
|
||||
}
|
||||
|
||||
if (!filepath || !existsSync(filepath)) {
|
||||
throw new Error('Task not found');
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
const marker = type === 'user' ? '--- USER ---' : '--- SUMMARY ---';
|
||||
const entry = `\n\n${marker} [${timestamp}]\n${content}`;
|
||||
const entry = `\n\n${marker} [${new Date().toISOString()}]\n${content}`;
|
||||
|
||||
await appendFile(filepath, entry, 'utf-8');
|
||||
|
||||
return `Appended ${type} to task`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find task file by timestamp prefix
|
||||
*/
|
||||
function findTaskByTimestamp(taskId: string): string | null {
|
||||
if (!existsSync(TASKS_DIR)) return null;
|
||||
|
||||
const files = readdirSync(TASKS_DIR);
|
||||
const match = files.find((f) => f.startsWith(`${taskId}_`));
|
||||
return match ? join(TASKS_DIR, match) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find task file by working directory (for current session)
|
||||
* Supports both new format (project name) and legacy format (reversed path)
|
||||
* Returns the most recently modified matching file
|
||||
*/
|
||||
function findTaskByWorkingDir(workingDir: string): string | null {
|
||||
if (!existsSync(TASKS_DIR)) return null;
|
||||
|
||||
const projectName = extractProjectName(workingDir);
|
||||
const depthFirst = pathToDepthFirst(workingDir);
|
||||
const files = readdirSync(TASKS_DIR);
|
||||
|
||||
// Find all matching files (both formats)
|
||||
const matchingFiles = files.filter((f) => {
|
||||
// New format: ends with _{project-name}.txt
|
||||
if (f.endsWith(`_${projectName}.txt`)) return true;
|
||||
// Legacy format: ends with _{reversed-path}.txt
|
||||
if (f.endsWith(`_${depthFirst}.txt`)) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (matchingFiles.length === 0) return null;
|
||||
|
||||
// Return most recently modified
|
||||
const sorted = matchingFiles
|
||||
.map((f) => ({ file: f, mtime: statSync(join(TASKS_DIR, f)).mtime }))
|
||||
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
|
||||
return join(TASKS_DIR, sorted[0].file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete (delete) a task
|
||||
*/
|
||||
|
|
@ -241,9 +178,9 @@ async function completeTask(taskId?: string, workingDir?: string): Promise<strin
|
|||
let filepath: string | null = null;
|
||||
|
||||
if (taskId) {
|
||||
filepath = findTaskByTimestamp(taskId);
|
||||
filepath = await findTaskByTimestamp(taskId);
|
||||
} else if (workingDir) {
|
||||
filepath = findTaskByWorkingDir(workingDir);
|
||||
filepath = await findTaskByWorkingDir(workingDir);
|
||||
}
|
||||
|
||||
if (!filepath || !existsSync(filepath)) {
|
||||
|
|
@ -258,125 +195,93 @@ async function completeTask(taskId?: string, workingDir?: string): Promise<strin
|
|||
* Load a task by timestamp
|
||||
*/
|
||||
async function loadTask(taskId: string): Promise<{ prompt: string; recoveryMessage: string }> {
|
||||
const filepath = findTaskByTimestamp(taskId);
|
||||
const filepath = await findTaskByTimestamp(taskId);
|
||||
|
||||
if (!filepath || !existsSync(filepath)) {
|
||||
throw new Error(`Task not found: ${taskId}`);
|
||||
}
|
||||
|
||||
// Touch the file (update mtime)
|
||||
const now = new Date();
|
||||
utimesSync(filepath, now, now);
|
||||
|
||||
const content = await readFile(filepath, 'utf-8');
|
||||
|
||||
const recoveryMessage = `The session that was working on the following task was lost. Figure out how to continue it.
|
||||
return {
|
||||
prompt: content,
|
||||
recoveryMessage: `The session working on the following task was lost. Continue it.\n\n<task_context>\n${content}\n</task_context>`,
|
||||
};
|
||||
}
|
||||
|
||||
<task_context>
|
||||
${content}
|
||||
</task_context>`;
|
||||
|
||||
return { prompt: content, recoveryMessage };
|
||||
interface ResumeResult {
|
||||
taskId: string;
|
||||
content: string;
|
||||
recoveryMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the most recent task for a working directory (for post-reboot recovery)
|
||||
* Supports both new format (project name) and legacy format (reversed path)
|
||||
* Resume the most recent task for a working directory
|
||||
*/
|
||||
async function resumeTask(workingDir: string): Promise<{ taskId: string; content: string; recoveryMessage: string }> {
|
||||
if (!existsSync(TASKS_DIR)) {
|
||||
throw new Error('No tasks found');
|
||||
}
|
||||
async function resumeTask(workingDir: string): Promise<ResumeResult> {
|
||||
const filepath = await findTaskByWorkingDir(workingDir);
|
||||
|
||||
const projectName = extractProjectName(workingDir);
|
||||
const depthFirst = pathToDepthFirst(workingDir);
|
||||
const files = readdirSync(TASKS_DIR);
|
||||
|
||||
// Find all tasks matching this directory (both formats)
|
||||
const matchingFiles = files.filter((f) => {
|
||||
// New format: ends with _{project-name}.txt
|
||||
if (f.endsWith(`_${projectName}.txt`)) return true;
|
||||
// Legacy format: ends with _{reversed-path}.txt
|
||||
if (f.endsWith(`_${depthFirst}.txt`)) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (matchingFiles.length === 0) {
|
||||
if (!filepath) {
|
||||
throw new Error(`No task found for directory: ${workingDir}`);
|
||||
}
|
||||
|
||||
// Sort by mtime (most recent first) and pick the latest
|
||||
const sorted = matchingFiles
|
||||
.map((f) => ({ file: f, mtime: statSync(join(TASKS_DIR, f)).mtime }))
|
||||
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
const content = await readFile(filepath, 'utf-8');
|
||||
const filename = filepath.split(sep).pop()!;
|
||||
const parsed = parseTaskFilename(filename);
|
||||
|
||||
const latest = sorted[0];
|
||||
const filepath = join(TASKS_DIR, latest.file);
|
||||
const parsed = parseTaskFilename(latest.file);
|
||||
|
||||
// Touch the file
|
||||
const now = new Date();
|
||||
utimesSync(filepath, now, now);
|
||||
|
||||
const content = await readFile(filepath, 'utf-8');
|
||||
|
||||
const recoveryMessage = `The session that was working on the following task was lost (likely due to host reboot/crash). Figure out how to continue it.
|
||||
|
||||
<task_context>
|
||||
${content}
|
||||
</task_context>`;
|
||||
|
||||
return {
|
||||
taskId: parsed?.timestamp.toString() || '',
|
||||
content,
|
||||
recoveryMessage,
|
||||
recoveryMessage: `The session working on the following task was lost. Continue it.\n\n<task_context>\n${content}\n</task_context>`,
|
||||
};
|
||||
}
|
||||
|
||||
interface TaskListItem {
|
||||
taskId: string;
|
||||
slug: string; // Task description (new format) or empty (legacy)
|
||||
project: string; // Project name (new format) or empty (legacy)
|
||||
pathInfo?: string; // Full path info (legacy format only)
|
||||
isLegacy: boolean;
|
||||
slug: string;
|
||||
project: string;
|
||||
created: Date;
|
||||
modified: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all tasks
|
||||
* Returns both new format and legacy format tasks
|
||||
*/
|
||||
function listTasks(): TaskListItem[] {
|
||||
async function listTasks(): Promise<TaskListItem[]> {
|
||||
if (!existsSync(TASKS_DIR)) return [];
|
||||
|
||||
const files = readdirSync(TASKS_DIR);
|
||||
const tasks: TaskListItem[] = [];
|
||||
const files = await readdir(TASKS_DIR);
|
||||
const validFiles: Array<{ file: string; parsed: ParsedTaskFilename }> = [];
|
||||
|
||||
for (const file of files) {
|
||||
const parsed = parseTaskFilename(file);
|
||||
if (parsed) {
|
||||
const filepath = join(TASKS_DIR, file);
|
||||
const stats = statSync(filepath);
|
||||
tasks.push({
|
||||
taskId: parsed.timestamp.toString(),
|
||||
slug: parsed.slug,
|
||||
project: parsed.project,
|
||||
pathInfo: parsed.pathInfo,
|
||||
isLegacy: parsed.isLegacyFormat,
|
||||
created: new Date(parsed.timestamp),
|
||||
modified: stats.mtime,
|
||||
});
|
||||
validFiles.push({ file, parsed });
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by creation time (newest first)
|
||||
const tasks = await Promise.all(
|
||||
validFiles.map(async ({ file, parsed }) => {
|
||||
const stats = await stat(join(TASKS_DIR, file));
|
||||
return {
|
||||
taskId: parsed.timestamp.toString(),
|
||||
slug: parsed.slug,
|
||||
project: parsed.project,
|
||||
created: new Date(parsed.timestamp),
|
||||
modified: stats.mtime,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return tasks.sort((a, b) => b.created.getTime() - a.created.getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool definitions
|
||||
*/
|
||||
const TOOLS = [
|
||||
{
|
||||
name: 'save_task',
|
||||
|
|
@ -384,14 +289,8 @@ const TOOLS = [
|
|||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
prompt: {
|
||||
type: 'string',
|
||||
description: 'The user prompt to save',
|
||||
},
|
||||
working_directory: {
|
||||
type: 'string',
|
||||
description: 'Current working directory (used for task identification)',
|
||||
},
|
||||
prompt: { type: 'string', description: 'The user prompt to save' },
|
||||
working_directory: { type: 'string', description: 'Current working directory (used for task identification)' },
|
||||
},
|
||||
required: ['prompt', 'working_directory'],
|
||||
},
|
||||
|
|
@ -402,14 +301,8 @@ const TOOLS = [
|
|||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
task_id: {
|
||||
type: 'string',
|
||||
description: 'Task ID (timestamp) to complete. If omitted, finds task by working_directory.',
|
||||
},
|
||||
working_directory: {
|
||||
type: 'string',
|
||||
description: 'Current working directory (used to find task if task_id not provided)',
|
||||
},
|
||||
task_id: { type: 'string', description: 'Task ID (timestamp) to complete. If omitted, finds task by working_directory.' },
|
||||
working_directory: { type: 'string', description: 'Current working directory (used to find task if task_id not provided)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -419,10 +312,7 @@ const TOOLS = [
|
|||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
task_id: {
|
||||
type: 'string',
|
||||
description: 'Task ID (timestamp) to load',
|
||||
},
|
||||
task_id: { type: 'string', description: 'Task ID (timestamp) to load' },
|
||||
},
|
||||
required: ['task_id'],
|
||||
},
|
||||
|
|
@ -433,10 +323,7 @@ const TOOLS = [
|
|||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
working_directory: {
|
||||
type: 'string',
|
||||
description: 'Current working directory to find task for',
|
||||
},
|
||||
working_directory: { type: 'string', description: 'Current working directory to find task for' },
|
||||
},
|
||||
required: ['working_directory'],
|
||||
},
|
||||
|
|
@ -455,51 +342,24 @@ const TOOLS = [
|
|||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'The summary or update to append',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['user', 'summary'],
|
||||
description: 'Type of content: "summary" for Claude summaries, "user" for user responses',
|
||||
},
|
||||
task_id: {
|
||||
type: 'string',
|
||||
description: 'Task ID (timestamp). If omitted, finds task by working_directory.',
|
||||
},
|
||||
working_directory: {
|
||||
type: 'string',
|
||||
description: 'Current working directory (used to find task if task_id not provided)',
|
||||
},
|
||||
content: { type: 'string', description: 'The summary or update to append' },
|
||||
type: { type: 'string', enum: ['user', 'summary'], description: 'Type of content: "summary" for Claude summaries, "user" for user responses' },
|
||||
task_id: { type: 'string', description: 'Task ID (timestamp). If omitted, finds task by working_directory.' },
|
||||
working_directory: { type: 'string', description: 'Current working directory (used to find task if task_id not provided)' },
|
||||
},
|
||||
required: ['content', 'type'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Initialize MCP server
|
||||
*/
|
||||
async function main() {
|
||||
const server = new Server(
|
||||
{
|
||||
name: 'task-persistence-server',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
{ name: 'task-persistence-server', version: '1.0.0' },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
// List available tools
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: TOOLS,
|
||||
}));
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
||||
|
||||
// Handle tool calls
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
|
|
@ -507,109 +367,40 @@ async function main() {
|
|||
switch (name) {
|
||||
case 'save_task': {
|
||||
const { prompt, working_directory } = args as { prompt?: string; working_directory?: string };
|
||||
if (!prompt || !working_directory) {
|
||||
throw new Error('prompt and working_directory are required');
|
||||
}
|
||||
|
||||
if (!prompt || !working_directory) throw new Error('prompt and working_directory are required');
|
||||
const result = await saveTask(prompt, working_directory);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Task saved. ID: ${result.taskId}\nFile: ${result.filename}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
return { content: [{ type: 'text', text: `Task saved. ID: ${result.taskId}\nFile: ${result.filename}` }] };
|
||||
}
|
||||
|
||||
case 'complete_task': {
|
||||
const { task_id, working_directory } = args as { task_id?: string; working_directory?: string };
|
||||
|
||||
const result = await completeTask(task_id, working_directory);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: result,
|
||||
},
|
||||
],
|
||||
};
|
||||
return { content: [{ type: 'text', text: result }] };
|
||||
}
|
||||
|
||||
case 'load_task': {
|
||||
const { task_id } = args as { task_id?: string };
|
||||
if (!task_id) {
|
||||
throw new Error('task_id is required');
|
||||
}
|
||||
|
||||
if (!task_id) throw new Error('task_id is required');
|
||||
const result = await loadTask(task_id);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: result.recoveryMessage,
|
||||
},
|
||||
],
|
||||
};
|
||||
return { content: [{ type: 'text', text: result.recoveryMessage }] };
|
||||
}
|
||||
|
||||
case 'resume_task': {
|
||||
const { working_directory } = args as { working_directory?: string };
|
||||
if (!working_directory) {
|
||||
throw new Error('working_directory is required');
|
||||
}
|
||||
|
||||
if (!working_directory) throw new Error('working_directory is required');
|
||||
const result = await resumeTask(working_directory);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Task ID: ${result.taskId}\n\n${result.recoveryMessage}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
return { content: [{ type: 'text', text: `Task ID: ${result.taskId}\n\n${result.recoveryMessage}` }] };
|
||||
}
|
||||
|
||||
case 'list_tasks': {
|
||||
const tasks = listTasks();
|
||||
|
||||
const tasks = await listTasks();
|
||||
if (tasks.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'No saved tasks found.',
|
||||
},
|
||||
],
|
||||
};
|
||||
return { content: [{ type: 'text', text: 'No saved tasks found.' }] };
|
||||
}
|
||||
|
||||
const taskList = tasks
|
||||
.map((t) => {
|
||||
// New format: show slug and project
|
||||
if (!t.isLegacy) {
|
||||
return `- ${t.taskId} | ${t.slug} | ${t.project} | Created: ${t.created.toISOString()}`;
|
||||
}
|
||||
// Legacy format: show pathInfo (truncated if too long)
|
||||
const pathInfo = t.pathInfo && t.pathInfo.length > 50
|
||||
? t.pathInfo.substring(0, 47) + '...'
|
||||
: t.pathInfo;
|
||||
return `- ${t.taskId} | (legacy) ${pathInfo} | Created: ${t.created.toISOString()}`;
|
||||
})
|
||||
.map((t) => `- ${t.taskId} | ${t.slug} | ${t.project} | Created: ${t.created.toISOString()}`)
|
||||
.join('\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Saved tasks:\n${taskList}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
return { content: [{ type: 'text', text: `Saved tasks:\n${taskList}` }] };
|
||||
}
|
||||
|
||||
case 'append_to_task': {
|
||||
|
|
@ -619,21 +410,9 @@ async function main() {
|
|||
task_id?: string;
|
||||
working_directory?: string;
|
||||
};
|
||||
|
||||
if (!content || !type) {
|
||||
throw new Error('content and type are required');
|
||||
}
|
||||
|
||||
if (!content || !type) throw new Error('content and type are required');
|
||||
const result = await appendToTask(content, type, task_id, working_directory);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: result,
|
||||
},
|
||||
],
|
||||
};
|
||||
return { content: [{ type: 'text', text: result }] };
|
||||
}
|
||||
|
||||
default:
|
||||
|
|
@ -641,23 +420,12 @@ async function main() {
|
|||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
||||
}
|
||||
});
|
||||
|
||||
// Start server
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
|
||||
// Keep process alive
|
||||
process.stdin.resume();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue