feat: Add version management, session tracking, and commit linking
Phase 1: Version Management - Migration 006: Add git_tag, git_sha columns to versions - New tools: version_add, version_list, version_show, version_update, version_release, version_assign_task - Links versions to git tags for release tracking Phase 2: Session/Task Activity Tracking - Migration 007: Create task_activity table - Track task changes per session (created, updated, status_change, closed) - Session ID from env/cache file (usage-stats format) - New tool: session_tasks to query tasks by session Phase 3: Commit-Task Linking - Migration 008: Create task_commits junction table - New tools: task_commit_add, task_commit_remove, task_commits_list, task_link_commits (batch parsing) - Stores SHA references only (Gitea MCP owns full data) - Commits shown in task_show output 17 new MCP tools total. All migrations applied to docker-host postgres. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,56 @@ import { query, queryOne, execute, getNextTaskId, getProjectKey } from '../db.js
|
||||
import { getEmbedding, formatEmbedding } from '../embeddings.js';
|
||||
import type { Task, ChecklistItem, TaskLink } from '../types.js';
|
||||
import { getRecentDelegations } from './delegations.js';
|
||||
import { getTaskCommits } from './commits.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
/**
|
||||
* Get current session ID from environment or cache file
|
||||
*/
|
||||
function getSessionId(): string {
|
||||
// Check environment first
|
||||
if (process.env.CLAUDE_SESSION_ID) {
|
||||
return process.env.CLAUDE_SESSION_ID;
|
||||
}
|
||||
|
||||
// Try to read from cache file (usage-stats format)
|
||||
const cacheFile = path.join(os.homedir(), '.claude', 'cache', '.current_session_id');
|
||||
try {
|
||||
const sessionId = fs.readFileSync(cacheFile, 'utf-8').trim();
|
||||
if (sessionId) return sessionId;
|
||||
} catch {
|
||||
// File doesn't exist or can't be read
|
||||
}
|
||||
|
||||
// Generate a new session ID
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString().replace(/[-:T]/g, '').slice(0, 15);
|
||||
return `session_${timestamp}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record task activity for session tracking
|
||||
*/
|
||||
async function recordActivity(
|
||||
task_id: string,
|
||||
activity_type: 'created' | 'updated' | 'status_change' | 'closed',
|
||||
old_value?: string,
|
||||
new_value?: string
|
||||
): Promise<void> {
|
||||
const session_id = getSessionId();
|
||||
try {
|
||||
await execute(
|
||||
`INSERT INTO task_activity (task_id, session_id, activity_type, old_value, new_value)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[task_id, session_id, activity_type, old_value || null, new_value || null]
|
||||
);
|
||||
} catch {
|
||||
// Don't fail the main operation if activity tracking fails
|
||||
console.error('Failed to record task activity');
|
||||
}
|
||||
}
|
||||
|
||||
interface TaskAddArgs {
|
||||
title: string;
|
||||
@@ -61,6 +111,9 @@ export async function taskAdd(args: TaskAddArgs): Promise<string> {
|
||||
);
|
||||
}
|
||||
|
||||
// Record activity for session tracking
|
||||
await recordActivity(taskId, 'created', undefined, 'open');
|
||||
|
||||
return `Created: ${taskId}\n Title: ${title}\n Type: ${type}\n Priority: ${priority}\n Project: ${projectKey}${embedding ? '\n (embedded for semantic search)' : ''}`;
|
||||
}
|
||||
|
||||
@@ -226,6 +279,12 @@ export async function taskShow(id: string): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
// Get commits
|
||||
const commitHistory = await getTaskCommits(id);
|
||||
if (commitHistory) {
|
||||
output += commitHistory;
|
||||
}
|
||||
|
||||
// Get recent delegations
|
||||
const delegationHistory = await getRecentDelegations(id);
|
||||
if (delegationHistory) {
|
||||
@@ -239,6 +298,9 @@ export async function taskShow(id: string): Promise<string> {
|
||||
* Close a task
|
||||
*/
|
||||
export async function taskClose(id: string): Promise<string> {
|
||||
// Get current status for activity tracking
|
||||
const task = await queryOne<{ status: string }>(`SELECT status FROM tasks WHERE id = $1`, [id]);
|
||||
|
||||
const result = await execute(
|
||||
`UPDATE tasks
|
||||
SET status = 'completed', completed_at = NOW(), updated_at = NOW()
|
||||
@@ -250,6 +312,9 @@ export async function taskClose(id: string): Promise<string> {
|
||||
return `Task not found: ${id}`;
|
||||
}
|
||||
|
||||
// Record activity
|
||||
await recordActivity(id, 'closed', task?.status, 'completed');
|
||||
|
||||
return `Closed: ${id}`;
|
||||
}
|
||||
|
||||
@@ -259,6 +324,12 @@ export async function taskClose(id: string): Promise<string> {
|
||||
export async function taskUpdate(args: TaskUpdateArgs): Promise<string> {
|
||||
const { id, status, priority, type, title } = args;
|
||||
|
||||
// Get current values for activity tracking
|
||||
const task = await queryOne<{ status: string }>(`SELECT status FROM tasks WHERE id = $1`, [id]);
|
||||
if (!task) {
|
||||
return `Task not found: ${id}`;
|
||||
}
|
||||
|
||||
const updates: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
let paramIndex = 1;
|
||||
@@ -299,5 +370,12 @@ export async function taskUpdate(args: TaskUpdateArgs): Promise<string> {
|
||||
return `Task not found: ${id}`;
|
||||
}
|
||||
|
||||
// Record activity
|
||||
if (status && status !== task.status) {
|
||||
await recordActivity(id, 'status_change', task.status, status);
|
||||
} else {
|
||||
await recordActivity(id, 'updated');
|
||||
}
|
||||
|
||||
return `Updated: ${id}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user