/**
* Git History Tool
*
* Comprehensive change tracking tool that records all repository modifications
* with detailed timestamps, storing history locally in JSON and syncing to remote providers.
*
* Operations: log, track, sync, export, auto
*/
import { GitCommandExecutor, GitCommandResult } from '../utils/git-command-executor.js';
import { ParameterValidator, ToolParams } from '../utils/parameter-validator.js';
import { OperationErrorHandler, ToolResult } from '../utils/operation-error-handler.js';
import { ProviderOperationHandler } from '../providers/provider-operation-handler.js';
import { ProviderConfig, ProviderOperation } from '../providers/types.js';
import { configManager } from '../config.js';
import * as fs from 'fs';
import * as path from 'path';
export interface GitHistoryParams extends ToolParams {
action: 'log' | 'track' | 'sync' | 'export' | 'auto';
// Log parameters
limit?: number; // Number of entries to show (default: 50)
since?: string; // Start date (ISO 8601)
until?: string; // End date (ISO 8601)
author?: string; // Filter by author
filePath?: string; // Filter by file path
branch?: string; // Filter by branch
format?: 'json' | 'markdown'; // Output format for log
// Track parameters
message: string; // Change description
timestamp?: string; // Custom timestamp (ISO 8601)
files?: string[]; // Files affected by this change
additions?: number; // Lines added
deletions?: number; // Lines deleted
// Sync parameters
provider?: 'github' | 'gitea' | 'both';
syncMethod?: 'file' | 'api'; // File commit or API upload
repo?: string; // Repository name for remote sync
// Export parameters
outputPath?: string; // Export file path
includeDiffs?: boolean; // Include full diffs in export
// Auto parameters
enabled?: boolean; // Enable/disable auto-tracking
}
export interface HistoryEntry {
id: string;
timestamp: string;
commitHash?: string;
author: string;
authorEmail: string;
message: string;
filesChanged: FileChange[];
additions: number;
deletions: number;
branch: string;
tags?: string[];
diff?: string;
synced?: boolean;
manual?: boolean;
}
export interface FileChange {
path: string;
status: 'added' | 'modified' | 'deleted' | 'renamed';
additions: number;
deletions: number;
}
export interface HistoryConfig {
autoTracking: boolean;
lastCommitHash?: string;
lastSyncTimestamp?: string;
}
export class GitHistoryTool {
private gitExecutor: GitCommandExecutor;
private providerHandler?: ProviderOperationHandler;
private historyPath: string;
private configPath: string;
constructor(providerConfig?: ProviderConfig) {
this.gitExecutor = new GitCommandExecutor();
this.historyPath = path.join('.git-history', 'history.json');
this.configPath = path.join('.git-history', 'config.json');
if (providerConfig) {
this.providerHandler = new ProviderOperationHandler(providerConfig);
}
this.ensureHistoryDirectory();
}
/**
* Execute git-history operation
*/
async execute(params: GitHistoryParams): Promise<ToolResult> {
const startTime = Date.now();
try {
// Validate basic parameters
const validation = ParameterValidator.validateToolParams('git-history', params);
if (!validation.isValid) {
return OperationErrorHandler.createToolError(
'VALIDATION_ERROR',
`Parameter validation failed: ${validation.errors.join(', ')}`,
params.action,
{ validationErrors: validation.errors },
validation.suggestions
);
}
// Validate operation-specific parameters
const operationValidation = this.validateOperationParams(params);
if (!operationValidation.isValid) {
return OperationErrorHandler.createToolError(
'VALIDATION_ERROR',
`Operation validation failed: ${operationValidation.errors.join(', ')}`,
params.action,
{ validationErrors: operationValidation.errors },
operationValidation.suggestions
);
}
// Route to appropriate handler
switch (params.action) {
case 'log':
return await this.handleLog(params, startTime);
case 'track':
return await this.handleTrack(params, startTime);
case 'sync':
return await this.handleSync(params, startTime);
case 'export':
return await this.handleExport(params, startTime);
case 'auto':
return await this.handleAuto(params, startTime);
default:
return OperationErrorHandler.createToolError(
'UNSUPPORTED_OPERATION',
`Unsupported operation: ${params.action}`,
params.action,
{ supportedOperations: ['log', 'track', 'sync', 'export', 'auto'] },
['Use one of the supported operations: log, track, sync, export, auto']
);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'EXECUTION_ERROR',
`Failed to execute git-history operation: ${errorMessage}`,
params.action,
{ error: errorMessage }
);
}
}
/**
* Validate operation-specific parameters
*/
private validateOperationParams(params: GitHistoryParams): { isValid: boolean; errors: string[]; suggestions: string[] } {
const errors: string[] = [];
const suggestions: string[] = [];
switch (params.action) {
case 'track':
if (!params.message) {
errors.push('Message is required for track operation');
suggestions.push('Provide a message describing the change');
}
break;
case 'sync':
if (!this.providerHandler && params.syncMethod === 'api') {
errors.push('Provider configuration required for API sync');
suggestions.push('Configure GitHub/Gitea provider or use file sync method');
}
break;
case 'export':
if (params.outputPath && !path.isAbsolute(params.outputPath)) {
errors.push('Output path must be absolute');
suggestions.push('Provide an absolute path for export file');
}
break;
}
return {
isValid: errors.length === 0,
errors,
suggestions
};
}
/**
* Handle log operation - View history entries
*/
private async handleLog(params: GitHistoryParams, startTime: number): Promise<ToolResult> {
try {
const history = await this.loadHistory();
let entries = [...history.entries];
// Apply filters
if (params.since) {
const sinceDate = new Date(params.since);
entries = entries.filter(entry => new Date(entry.timestamp) >= sinceDate);
}
if (params.until) {
const untilDate = new Date(params.until);
entries = entries.filter(entry => new Date(entry.timestamp) <= untilDate);
}
if (params.author) {
entries = entries.filter(entry =>
entry.author.toLowerCase().includes(params.author!.toLowerCase()) ||
entry.authorEmail.toLowerCase().includes(params.author!.toLowerCase())
);
}
if (params.filePath) {
entries = entries.filter(entry =>
entry.filesChanged.some(file => file.path.includes(params.filePath!))
);
}
if (params.branch) {
entries = entries.filter(entry => entry.branch === params.branch);
}
// Sort by timestamp (newest first)
entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
// Apply limit
const limit = params.limit || 50;
entries = entries.slice(0, limit);
// Format output
const format = params.format || 'json';
let formattedData: any;
if (format === 'markdown') {
formattedData = this.formatHistoryAsMarkdown(entries);
} else {
formattedData = {
totalEntries: history.entries.length,
filteredEntries: entries.length,
entries: entries
};
}
return {
success: true,
data: {
...formattedData,
filters: {
since: params.since,
until: params.until,
author: params.author,
filePath: params.filePath,
branch: params.branch,
limit: limit
}
},
metadata: {
operation: params.action,
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'LOG_FAILED',
`Failed to retrieve history: ${errorMessage}`,
params.action,
{ error: errorMessage }
);
}
}
/**
* Handle track operation - Manually record a change
*/
private async handleTrack(params: GitHistoryParams, startTime: number): Promise<ToolResult> {
try {
const currentBranch = await this.getCurrentBranch(params.projectPath);
const timestamp = params.timestamp || new Date().toISOString();
const entry: HistoryEntry = {
id: `manual-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp,
author: await this.getProviderUserName() || 'Unknown',
authorEmail: await this.getGitUserEmail() || '',
message: params.message,
filesChanged: params.files ? params.files.map(file => ({
path: file,
status: 'modified' as const,
additions: params.additions || 0,
deletions: params.deletions || 0
})) : [],
additions: params.additions || 0,
deletions: params.deletions || 0,
branch: currentBranch || 'main',
manual: true,
synced: false
};
await this.addHistoryEntry(entry);
return {
success: true,
data: {
entry: entry,
message: 'Change tracked successfully'
},
metadata: {
operation: params.action,
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'TRACK_FAILED',
`Failed to track change: ${errorMessage}`,
params.action,
{ error: errorMessage }
);
}
}
/**
* Handle sync operation - Sync local history to remote
*/
private async handleSync(params: GitHistoryParams, startTime: number): Promise<ToolResult> {
try {
const history = await this.loadHistory();
const unsyncedEntries = history.entries.filter(entry => !entry.synced);
if (unsyncedEntries.length === 0) {
return {
success: true,
data: {
message: 'All entries are already synced',
totalEntries: history.entries.length,
syncedEntries: history.entries.length - unsyncedEntries.length,
unsyncedEntries: 0
},
metadata: {
operation: params.action,
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
}
const syncMethod = params.syncMethod || 'file';
let syncResult: any = {};
if (syncMethod === 'file') {
syncResult = await this.syncViaFile(params, unsyncedEntries);
} else if (syncMethod === 'api') {
syncResult = await this.syncViaApi(params, unsyncedEntries);
} else {
return OperationErrorHandler.createToolError(
'INVALID_SYNC_METHOD',
`Invalid sync method: ${syncMethod}`,
params.action,
{ supportedMethods: ['file', 'api'] },
['Use "file" to commit history.json or "api" to use provider API']
);
}
// Mark entries as synced
const updatedHistory = await this.loadHistory();
updatedHistory.entries.forEach(entry => {
if (!entry.synced && unsyncedEntries.some(u => u.id === entry.id)) {
entry.synced = true;
}
});
await this.saveHistory(updatedHistory);
return {
success: true,
data: {
...syncResult,
syncedEntries: unsyncedEntries.length,
totalEntries: history.entries.length,
syncMethod
},
metadata: {
operation: params.action,
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'SYNC_FAILED',
`Failed to sync history: ${errorMessage}`,
params.action,
{ error: errorMessage }
);
}
}
/**
* Handle export operation - Export history to file
*/
private async handleExport(params: GitHistoryParams, startTime: number): Promise<ToolResult> {
try {
const history = await this.loadHistory();
const outputPath = params.outputPath || path.join(params.projectPath, 'HISTORY.json');
let exportData: any;
if (path.extname(outputPath).toLowerCase() === '.md') {
// Export as Markdown
exportData = this.formatHistoryAsMarkdown(history.entries);
} else {
// Export as JSON
exportData = {
exportedAt: new Date().toISOString(),
totalEntries: history.entries.length,
includeDiffs: params.includeDiffs || false,
entries: params.includeDiffs ? history.entries : history.entries.map(entry => {
const { diff, ...entryWithoutDiff } = entry;
return entryWithoutDiff;
})
};
}
// Write to file
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
if (typeof exportData === 'string') {
fs.writeFileSync(outputPath, exportData, 'utf8');
} else {
fs.writeFileSync(outputPath, JSON.stringify(exportData, null, 2), 'utf8');
}
return {
success: true,
data: {
exportedPath: outputPath,
format: path.extname(outputPath).toLowerCase() === '.md' ? 'markdown' : 'json',
totalEntries: history.entries.length,
includeDiffs: params.includeDiffs || false
},
metadata: {
operation: params.action,
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'EXPORT_FAILED',
`Failed to export history: ${errorMessage}`,
params.action,
{ error: errorMessage }
);
}
}
/**
* Handle auto operation - Enable/disable automatic tracking
*/
private async handleAuto(params: GitHistoryParams, startTime: number): Promise<ToolResult> {
try {
const config = await this.loadConfig();
const wasEnabled = config.autoTracking;
if (params.enabled !== undefined) {
config.autoTracking = params.enabled;
await this.saveConfig(config);
if (params.enabled && !wasEnabled) {
// Enable auto-tracking - sync current commits
await this.syncRecentCommits();
}
}
return {
success: true,
data: {
autoTrackingEnabled: config.autoTracking,
message: `Auto-tracking ${config.autoTracking ? 'enabled' : 'disabled'}`
},
metadata: {
operation: params.action,
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'AUTO_CONFIG_FAILED',
`Failed to configure auto-tracking: ${errorMessage}`,
params.action,
{ error: errorMessage }
);
}
}
/**
* Sync via file commit
*/
private async syncViaFile(params: GitHistoryParams, entries: HistoryEntry[]): Promise<any> {
try {
const historyFile = path.join(params.projectPath, 'HISTORY.json');
// Write history to repository
const historyData = {
lastUpdated: new Date().toISOString(),
entries: entries
};
fs.writeFileSync(historyFile, JSON.stringify(historyData, null, 2), 'utf8');
// Commit the file
const addResult = await this.gitExecutor.executeGitCommand('add', ['HISTORY.json'], params.projectPath);
if (!addResult.success) {
throw new Error(`Failed to add HISTORY.json: ${addResult.stderr}`);
}
const commitResult = await this.gitExecutor.executeGitCommand(
'commit',
['-m', `Update history: ${entries.length} new entries`],
params.projectPath
);
if (!commitResult.success && !commitResult.stderr.includes('nothing to commit')) {
throw new Error(`Failed to commit HISTORY.json: ${commitResult.stderr}`);
}
return {
method: 'file',
committed: commitResult.success,
message: 'History synced via file commit'
};
} catch (error) {
throw new Error(`File sync failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Sync via provider API
*/
private async syncViaApi(params: GitHistoryParams, entries: HistoryEntry[]): Promise<any> {
if (!this.providerHandler) {
throw new Error('Provider handler not configured');
}
try {
const provider = params.provider || 'github';
const repo = params.repo || await this.getRepoName();
// Create a summary entry for the API
const summary = {
period: `${entries[0]?.timestamp} to ${entries[entries.length - 1]?.timestamp}`,
totalEntries: entries.length,
authors: [...new Set(entries.map(e => e.author))],
filesChanged: entries.reduce((sum, e) => sum + e.filesChanged.length, 0),
totalAdditions: entries.reduce((sum, e) => sum + e.additions, 0),
totalDeletions: entries.reduce((sum, e) => sum + e.deletions, 0),
entries: entries.slice(0, 10) // Include first 10 entries in detail
};
const operation: ProviderOperation = {
provider,
operation: 'create',
parameters: {
repo,
title: `History Update: ${entries.length} changes`,
body: JSON.stringify(summary, null, 2),
type: 'issue' // Create as issue for tracking
},
requiresAuth: true,
isRemoteOperation: true
};
const result = await this.providerHandler.executeOperation(operation);
return {
method: 'api',
provider,
success: result.success,
message: result.success ? 'History synced via API' : 'API sync failed'
};
} catch (error) {
throw new Error(`API sync failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Format history as Markdown
*/
private formatHistoryAsMarkdown(entries: HistoryEntry[]): string {
let markdown = '# Repository History\n\n';
markdown += `Generated on: ${new Date().toISOString()}\n\n`;
markdown += `Total entries: ${entries.length}\n\n`;
for (const entry of entries) {
markdown += `## ${entry.message}\n\n`;
markdown += `- **Date:** ${new Date(entry.timestamp).toLocaleString()}\n`;
markdown += `- **Author:** ${entry.author} <${entry.authorEmail}>\n`;
markdown += `- **Branch:** ${entry.branch}\n`;
markdown += `- **Commit:** ${entry.commitHash || 'Manual entry'}\n`;
markdown += `- **Changes:** +${entry.additions} -${entry.deletions}\n`;
if (entry.filesChanged.length > 0) {
markdown += `- **Files changed:**\n`;
for (const file of entry.filesChanged) {
markdown += ` - ${file.status.toUpperCase()}: ${file.path} (+${file.additions} -${file.deletions})\n`;
}
}
if (entry.tags && entry.tags.length > 0) {
markdown += `- **Tags:** ${entry.tags.join(', ')}\n`;
}
markdown += '\n---\n\n';
}
return markdown;
}
/**
* Load history from file
*/
private async loadHistory(): Promise<{ entries: HistoryEntry[] }> {
try {
if (fs.existsSync(this.historyPath)) {
const data = fs.readFileSync(this.historyPath, 'utf8');
return JSON.parse(data);
}
} catch (error) {
console.warn('Failed to load history file:', error);
}
return { entries: [] };
}
/**
* Save history to file
*/
private async saveHistory(history: { entries: HistoryEntry[] }): Promise<void> {
try {
fs.writeFileSync(this.historyPath, JSON.stringify(history, null, 2), 'utf8');
} catch (error) {
throw new Error(`Failed to save history: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Add entry to history
*/
private async addHistoryEntry(entry: HistoryEntry): Promise<void> {
const history = await this.loadHistory();
history.entries.push(entry);
await this.saveHistory(history);
}
/**
* Load configuration
*/
private async loadConfig(): Promise<HistoryConfig> {
try {
if (fs.existsSync(this.configPath)) {
const data = fs.readFileSync(this.configPath, 'utf8');
return JSON.parse(data);
}
} catch (error) {
console.warn('Failed to load config file:', error);
}
return { autoTracking: false };
}
/**
* Save configuration
*/
private async saveConfig(config: HistoryConfig): Promise<void> {
try {
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf8');
} catch (error) {
throw new Error(`Failed to save config: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Sync recent commits to history
*/
private async syncRecentCommits(): Promise<void> {
try {
const config = await this.loadConfig();
const lastHash = config.lastCommitHash;
// Get commits since last sync
const logArgs = ['--oneline', '--numstat', '--format=format:%H|%an|%ae|%ad|%s'];
if (lastHash) {
logArgs.push(`${lastHash}..HEAD`);
}
const logResult = await this.gitExecutor.executeGitCommand('log', logArgs);
if (!logResult.success) {
console.warn('Failed to get git log:', logResult.stderr);
return;
}
const lines = logResult.stdout.split('\n').filter(line => line.trim());
let currentEntry: Partial<HistoryEntry> | null = null;
for (const line of lines) {
if (line.includes('|')) {
// Commit header line
if (currentEntry) {
await this.addHistoryEntry(currentEntry as HistoryEntry);
}
const [hash, gitAuthor, email, date, ...messageParts] = line.split('|');
const message = messageParts.join('|');
currentEntry = {
id: `commit-${hash}`,
timestamp: new Date(date).toISOString(),
commitHash: hash,
author: await this.getProviderUserName() || gitAuthor, // Use provider username, fallback to git author
authorEmail: email,
message,
filesChanged: [],
additions: 0,
deletions: 0,
branch: await this.getCurrentBranch() || 'main',
synced: false
};
} else if (line.trim() && currentEntry) {
// File change line (from --numstat)
const parts = line.trim().split('\t');
if (parts.length >= 3) {
const additions = parseInt(parts[0]) || 0;
const deletions = parseInt(parts[1]) || 0;
const filePath = parts[2];
currentEntry.filesChanged!.push({
path: filePath,
status: additions === 0 && deletions === 0 ? 'modified' : (additions > 0 ? 'added' : 'deleted'),
additions,
deletions
});
currentEntry.additions! += additions;
currentEntry.deletions! += deletions;
}
}
}
// Add the last entry
if (currentEntry) {
await this.addHistoryEntry(currentEntry as HistoryEntry);
}
// Update last commit hash
const latestHash = await this.getLatestCommitHash();
if (latestHash) {
config.lastCommitHash = latestHash;
config.lastSyncTimestamp = new Date().toISOString();
await this.saveConfig(config);
}
} catch (error) {
console.warn('Failed to sync recent commits:', error);
}
}
/**
* Get current branch
*/
private async getCurrentBranch(projectPath?: string): Promise<string | null> {
try {
const result = await this.gitExecutor.executeGitCommand('branch', ['--show-current'], projectPath);
return result.success ? result.stdout.trim() : null;
} catch {
return null;
}
}
/**
* Get provider username (GitHub or Gitea username from env)
*/
private async getProviderUserName(): Promise<string | null> {
// Try GitHub username first
const githubUser = process.env.GITHUB_USERNAME;
if (githubUser) return githubUser;
// Try Gitea username
const giteaUser = process.env.GITEA_USERNAME;
if (giteaUser) return giteaUser;
// Fallback to git config user name
try {
const result = await this.gitExecutor.executeGitCommand('config', ['user.name']);
return result.success ? result.stdout.trim() : null;
} catch {
return null;
}
}
/**
* Get git user email
*/
private async getGitUserEmail(): Promise<string | null> {
try {
const result = await this.gitExecutor.executeGitCommand('config', ['user.email']);
return result.success ? result.stdout.trim() : null;
} catch {
return null;
}
}
/**
* Get latest commit hash
*/
private async getLatestCommitHash(): Promise<string | null> {
try {
const result = await this.gitExecutor.executeGitCommand('rev-parse', ['HEAD']);
return result.success ? result.stdout.trim() : null;
} catch {
return null;
}
}
/**
* Get repository name from git config
*/
private async getRepoName(): Promise<string | null> {
try {
const result = await this.gitExecutor.executeGitCommand('config', ['--get', 'remote.origin.url']);
if (result.success) {
const url = result.stdout.trim();
// Extract repo name from URL (supports GitHub and Gitea formats)
const match = url.match(/\/([^\/]+?)(?:\.git)?$/);
return match ? match[1] : null;
}
} catch {
// Ignore errors
}
return null;
}
/**
* Ensure history directory exists
*/
private ensureHistoryDirectory(): void {
try {
const dir = path.dirname(this.historyPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
} catch (error) {
console.warn('Failed to create history directory:', error);
}
}
/**
* Get tool schema for MCP registration
*/
static getToolSchema() {
return {
name: 'git-history',
description: 'Comprehensive change tracking tool that records all repository modifications with detailed timestamps, storing history locally in JSON and syncing to remote providers.',
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['log', 'track', 'sync', 'export', 'auto'],
description: 'The git-history operation to perform'
},
projectPath: {
type: 'string',
description: 'Absolute path to the project directory'
},
limit: {
type: 'number',
description: 'Number of entries to show (default: 50)',
minimum: 1,
maximum: 1000
},
since: {
type: 'string',
description: 'Start date filter (ISO 8601 format)'
},
until: {
type: 'string',
description: 'End date filter (ISO 8601 format)'
},
author: {
type: 'string',
description: 'Filter by author name or email'
},
filePath: {
type: 'string',
description: 'Filter by file path'
},
branch: {
type: 'string',
description: 'Filter by branch name'
},
format: {
type: 'string',
enum: ['json', 'markdown'],
description: 'Output format for log operation'
},
message: {
type: 'string',
description: 'Change description (required for track operation)'
},
timestamp: {
type: 'string',
description: 'Custom timestamp for track operation (ISO 8601 format)'
},
files: {
type: 'array',
items: { type: 'string' },
description: 'Files affected by the change (for track operation)'
},
additions: {
type: 'number',
description: 'Lines added (for track operation)',
minimum: 0
},
deletions: {
type: 'number',
description: 'Lines deleted (for track operation)',
minimum: 0
},
provider: {
type: 'string',
enum: ['github', 'gitea', 'both'],
description: 'Provider for remote sync operations'
},
syncMethod: {
type: 'string',
enum: ['file', 'api'],
description: 'Sync method: file (commit to repo) or api (use provider API)'
},
repo: {
type: 'string',
description: 'Repository name for remote sync'
},
outputPath: {
type: 'string',
description: 'Export file path (absolute path required)'
},
includeDiffs: {
type: 'boolean',
description: 'Include full diffs in export (default: false)'
},
enabled: {
type: 'boolean',
description: 'Enable/disable auto-tracking (for auto operation)'
}
},
required: ['action', 'projectPath']
}
};
}
}