diff --git a/src/db.ts b/src/db.ts index dd01c92..888f81a 100644 --- a/src/db.ts +++ b/src/db.ts @@ -68,9 +68,74 @@ export async function getNextTaskId(projectKey: string): Promise { } /** - * Get project key from name, or generate one + * Detect project from current working directory */ -export async function getProjectKey(projectName: string): Promise { +export function detectProjectFromCwd(): string | null { + const cwd = process.cwd(); + + // Known project path patterns + const patterns: Record = { + 'SmartTranslate': 'ST', + 'VPN': 'VPN', + 'BestGPT': 'BGPT', + 'AssistOWUI': 'OWUI', + 'AssistForOpenWebUI': 'OWUI', + 'AssistForJira': 'AFJ', + 'eToroGridbot': 'GB', + 'WildFiles': 'WF', + 'Circles': 'CIR', + 'ClaudeFramework': 'CF', + 'AgilitonScripts': 'AS', + 'WHMCS': 'WHMCS', + 'Cardscanner': 'CS', + 'ZorkiOS': 'ZORK', + 'MeetingMind': 'MM', + 'PropertyMap': 'PM', + 'Rubic': 'RUB', + 'Socialguard': 'SG', + }; + + // Check each pattern + for (const [name, key] of Object.entries(patterns)) { + if (cwd.includes(`/${name}/`) || cwd.endsWith(`/${name}`)) { + return key; + } + } + + // Try to extract from Apps/X or Libraries/X path + const appsMatch = cwd.match(/\/Apps\/([^/]+)/); + if (appsMatch) { + return appsMatch[1].replace(/[a-z]/g, '').slice(0, 4) || appsMatch[1].slice(0, 3).toUpperCase(); + } + + const infraMatch = cwd.match(/\/Infrastructure\/([^/]+)/); + if (infraMatch) { + return infraMatch[1].replace(/[a-z]/g, '').slice(0, 4) || infraMatch[1].slice(0, 3).toUpperCase(); + } + + return null; +} + +/** + * Get project key from name, or generate one + * If projectName is empty/undefined, tries to detect from CWD + */ +export async function getProjectKey(projectName?: string): Promise { + // Auto-detect if not provided + if (!projectName) { + const detected = detectProjectFromCwd(); + if (detected) { + // Ensure project exists in DB + await execute( + `INSERT INTO projects (key, name) VALUES ($1, $1) + ON CONFLICT (key) DO NOTHING`, + [detected] + ); + return detected; + } + return 'MISC'; // Fallback for unknown projects + } + // First check if already registered const existing = await queryOne<{ key: string }>( `SELECT key FROM projects WHERE name = $1 LIMIT 1`, diff --git a/src/index.ts b/src/index.ts index 7395ad4..41179ac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,7 @@ import { taskSimilar, taskContext } from './tools/search.js'; import { taskLink, checklistAdd, checklistToggle, taskResolveDuplicate } from './tools/relations.js'; import { epicAdd, epicList, epicShow, epicAssign, epicClose } from './tools/epics.js'; import { taskDelegations, taskDelegationQuery } from './tools/delegations.js'; -import { projectLock, projectUnlock, projectLockStatus } from './tools/locks.js'; +import { projectLock, projectUnlock, projectLockStatus, projectContext } from './tools/locks.js'; // Create MCP server const server = new Server( @@ -185,6 +185,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { project: a.project, }); break; + case 'project_context': + result = await projectContext(); + break; default: throw new Error(`Unknown tool: ${name}`); diff --git a/src/tools/index.ts b/src/tools/index.ts index 4d6f906..a20f788 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -272,4 +272,12 @@ export const toolDefinitions = [ }, }, }, + { + name: 'project_context', + description: 'Get project context from current directory - returns detected project, open tasks, epics, and lock status. Use at session start.', + inputSchema: { + type: 'object', + properties: {}, + }, + }, ]; diff --git a/src/tools/locks.ts b/src/tools/locks.ts index 5f86de6..f3d78c8 100644 --- a/src/tools/locks.ts +++ b/src/tools/locks.ts @@ -1,6 +1,6 @@ // Project lock operations for session exclusivity -import { query, queryOne, execute, getProjectKey } from '../db.js'; +import { query, queryOne, execute, getProjectKey, detectProjectFromCwd } from '../db.js'; interface ProjectLock { project: string; @@ -150,3 +150,93 @@ export async function projectLockStatus(args: LockStatusArgs): Promise { return lines.join('\n'); } + +interface Task { + id: string; + title: string; + type: string; + status: string; + priority: string; +} + +interface Epic { + id: string; + title: string; + status: string; +} + +/** + * Get project context from CWD - returns project key, open tasks, and epics + */ +export async function projectContext(): Promise { + const projectKey = detectProjectFromCwd(); + + if (!projectKey) { + return 'No project detected from current directory'; + } + + // Ensure project exists + await execute( + `INSERT INTO projects (key, name) VALUES ($1, $1) ON CONFLICT (key) DO NOTHING`, + [projectKey] + ); + + // Get open tasks for this project + const tasks = await query( + `SELECT id, title, type, status, priority FROM tasks + WHERE project = $1 AND status != 'completed' + ORDER BY + CASE priority WHEN 'P0' THEN 0 WHEN 'P1' THEN 1 WHEN 'P2' THEN 2 ELSE 3 END, + created_at DESC + LIMIT 10`, + [projectKey] + ); + + // Get open epics for this project + const epics = await query( + `SELECT id, title, status FROM epics + WHERE project = $1 AND status != 'completed' + ORDER BY created_at DESC + LIMIT 5`, + [projectKey] + ); + + // Check lock status + const lock = await queryOne( + `SELECT * FROM project_locks WHERE project = $1`, + [projectKey] + ); + + let result = `Project: ${projectKey}\n`; + result += `CWD: ${process.cwd()}\n`; + + if (lock) { + const now = new Date(); + const expired = lock.expires_at && new Date(lock.expires_at) < now; + result += `Lock: ${expired ? 'EXPIRED' : 'ACTIVE'} (session: ${lock.session_id.substring(0, 8)}...)\n`; + } else { + result += `Lock: None\n`; + } + + result += `\n`; + + if (epics.length > 0) { + result += `Epics (${epics.length}):\n`; + epics.forEach(e => { + result += ` ${e.id}: ${e.title} [${e.status}]\n`; + }); + result += `\n`; + } + + if (tasks.length > 0) { + result += `Tasks (${tasks.length}):\n`; + tasks.forEach(t => { + const icon = t.status === 'in_progress' ? '>' : ' '; + result += `${icon} ${t.priority} ${t.id}: ${t.title} [${t.type}]\n`; + }); + } else { + result += `No open tasks\n`; + } + + return result; +}