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:
69
src/db.ts
69
src/db.ts
@@ -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`,
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user