feat: Add project_context tool and CWD auto-detection

New features:
- detectProjectFromCwd(): Maps directory paths to project keys
- project_context: Returns project, tasks, epics, lock status
- Auto-detection for 18 known projects (ST, VPN, OWUI, etc.)
- Falls back to extracting from Apps/X or Infrastructure/X paths

Use project_context at session start to see only relevant tasks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-01-10 09:26:33 +02:00
parent 837fb8060c
commit 99f5828f60
4 changed files with 170 additions and 4 deletions

View File

@@ -68,9 +68,74 @@ export async function getNextTaskId(projectKey: string): Promise<string> {
} }
/** /**
* Get project key from name, or generate one * Detect project from current working directory
*/ */
export async function getProjectKey(projectName: string): Promise<string> { export function detectProjectFromCwd(): string | null {
const cwd = process.cwd();
// Known project path patterns
const patterns: Record<string, string> = {
'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<string> {
// 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 // First check if already registered
const existing = await queryOne<{ key: string }>( const existing = await queryOne<{ key: string }>(
`SELECT key FROM projects WHERE name = $1 LIMIT 1`, `SELECT key FROM projects WHERE name = $1 LIMIT 1`,

View File

@@ -23,7 +23,7 @@ import { taskSimilar, taskContext } from './tools/search.js';
import { taskLink, checklistAdd, checklistToggle, taskResolveDuplicate } from './tools/relations.js'; import { taskLink, checklistAdd, checklistToggle, taskResolveDuplicate } from './tools/relations.js';
import { epicAdd, epicList, epicShow, epicAssign, epicClose } from './tools/epics.js'; import { epicAdd, epicList, epicShow, epicAssign, epicClose } from './tools/epics.js';
import { taskDelegations, taskDelegationQuery } from './tools/delegations.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 // Create MCP server
const server = new Server( const server = new Server(
@@ -185,6 +185,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
project: a.project, project: a.project,
}); });
break; break;
case 'project_context':
result = await projectContext();
break;
default: default:
throw new Error(`Unknown tool: ${name}`); throw new Error(`Unknown tool: ${name}`);

View File

@@ -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: {},
},
},
]; ];

View File

@@ -1,6 +1,6 @@
// Project lock operations for session exclusivity // 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 { interface ProjectLock {
project: string; project: string;
@@ -150,3 +150,93 @@ export async function projectLockStatus(args: LockStatusArgs): Promise<string> {
return lines.join('\n'); 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<string> {
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<Task>(
`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<Epic>(
`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<ProjectLock>(
`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;
}