Files
session-mcp/src/db.ts
Christian Gick 99f5828f60 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>
2026-01-10 09:26:33 +02:00

186 lines
4.5 KiB
TypeScript

import pg from 'pg';
const { Pool } = pg;
// Configuration from environment variables
const config = {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'agiliton',
user: process.env.DB_USER || 'agiliton',
password: process.env.DB_PASSWORD || 'QtqiwCOAUpQNF6pjzOMAREzUny2bY8V1',
max: 5,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
};
// Create connection pool
const pool = new Pool(config);
// Log connection errors
pool.on('error', (err) => {
console.error('Unexpected database error:', err);
});
/**
* Execute a query and return all rows
*/
export async function query<T = Record<string, unknown>>(
text: string,
params?: unknown[]
): Promise<T[]> {
const result = await pool.query(text, params);
return result.rows as T[];
}
/**
* Execute a query and return the first row
*/
export async function queryOne<T = Record<string, unknown>>(
text: string,
params?: unknown[]
): Promise<T | null> {
const result = await pool.query(text, params);
return (result.rows[0] as T) || null;
}
/**
* Execute a query without returning results (INSERT, UPDATE, DELETE)
*/
export async function execute(
text: string,
params?: unknown[]
): Promise<number> {
const result = await pool.query(text, params);
return result.rowCount || 0;
}
/**
* Get the next task ID for a project
*/
export async function getNextTaskId(projectKey: string): Promise<string> {
const result = await queryOne<{ next_id: number }>(
`INSERT INTO task_sequences (project, next_id) VALUES ($1, 1)
ON CONFLICT (project) DO UPDATE SET next_id = task_sequences.next_id + 1
RETURNING next_id`,
[projectKey]
);
return `${projectKey}-${result?.next_id || 1}`;
}
/**
* Detect project from current working directory
*/
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
const existing = await queryOne<{ key: string }>(
`SELECT key FROM projects WHERE name = $1 LIMIT 1`,
[projectName]
);
if (existing) {
return existing.key;
}
// Generate a key from the name (uppercase first letters)
let generated = projectName.replace(/[a-z]/g, '').slice(0, 4);
if (!generated) {
generated = projectName.slice(0, 3).toUpperCase();
}
// Register the new project
await execute(
`INSERT INTO projects (key, name) VALUES ($1, $2)
ON CONFLICT (key) DO NOTHING`,
[generated, projectName]
);
return generated;
}
/**
* Test database connection
*/
export async function testConnection(): Promise<boolean> {
try {
await query('SELECT 1');
return true;
} catch (error) {
console.error('Database connection failed:', error);
return false;
}
}
/**
* Close the connection pool
*/
export async function close(): Promise<void> {
await pool.end();
}
export default pool;