#
tokens: 43060/50000 38/38 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── DEPLOYMENT.md
├── esbuild.config.js
├── jest.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── core
│   │   ├── git-executor.ts
│   │   ├── repository-manager.ts
│   │   ├── security-constants.ts
│   │   └── session-client.ts
│   ├── handlers
│   │   ├── checkpoint-handler.ts
│   │   ├── git-handler.ts
│   │   ├── list-repos-handler.ts
│   │   └── session-handler.ts
│   ├── shadowgit-mcp-server.ts
│   ├── types.ts
│   └── utils
│       ├── constants.ts
│       ├── file-utils.ts
│       ├── logger.ts
│       └── response-utils.ts
├── test-package.js
├── TESTING.md
├── tests
│   ├── __mocks__
│   │   └── @modelcontextprotocol
│   │       └── sdk
│   │           ├── server
│   │           │   ├── index.js
│   │           │   └── stdio.js
│   │           └── types.js
│   ├── core
│   │   ├── git-executor.test.ts
│   │   ├── repository-manager.test.ts
│   │   └── session-client.test.ts
│   ├── handlers
│   │   ├── checkpoint-handler.test.ts
│   │   ├── git-handler.test.ts
│   │   ├── list-repos-handler.test.ts
│   │   └── session-handler.test.ts
│   ├── integration
│   │   └── workflow.test.ts
│   ├── shadowgit-mcp-server-logic.test.ts
│   └── shadowgit-mcp-server.test.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
# Source files
src/
tests/
tsconfig.json

# Development files
*.test.ts
*.test.js
.git/
.gitignore

# Build artifacts
node_modules/
*.tgz

# IDE files
.vscode/
.idea/
*.swp
*.swo

# OS files
.DS_Store
Thumbs.db

# Logs
*.log
npm-debug.log*

# Only include built dist files and essential docs
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Dependencies
node_modules/

# Build output
dist/

# Logs
*.log
npm-debug.log*

# Runtime data
*.pid
*.seed
*.pid.lock

# Coverage directory used by tools like istanbul
coverage/

# IDE files
.vscode/
.idea/
*.swp
*.swo

# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.shadowgit.git
.claude
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# ShadowGit MCP Server

[![npm version](https://badge.fury.io/js/shadowgit-mcp-server.svg)](https://www.npmjs.com/package/shadowgit-mcp-server)

A Model Context Protocol (MCP) server that provides AI assistants with secure git access to your ShadowGit repositories, including the ability to create organized commits through the Session API. This enables powerful debugging, code analysis, and clean commit management by giving AI controlled access to your project's git history.

## What is ShadowGit?

[ShadowGit](https://shadowgit.com) automatically captures every save as a git commit while also providing a Session API that allows AI assistants to pause auto-commits and create clean, organized commits. The MCP server provides both read access to your detailed development history and the ability to manage AI-assisted changes properly.

## Installation

```bash
npm install -g shadowgit-mcp-server
```

## Setup with Claude Code

```bash
# Add to Claude Code
claude mcp add shadowgit -- shadowgit-mcp-server

# Restart Claude Code to load the server
```

## Setup with Claude Desktop

Add to your Claude Desktop MCP configuration:

**macOS/Linux:** `~/.config/Claude/claude_desktop_config.json`  
**Windows:** `%APPDATA%\\Claude\\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "shadowgit": {
      "command": "shadowgit-mcp-server"
    }
  }
}
```

## Requirements

- **Node.js 18+**
- **ShadowGit app** installed and running with tracked repositories
  - Session API requires ShadowGit version >= 0.3.0
- **Git** available in PATH

## How It Works

**MCP servers are stateless and use stdio transport:**
- The server runs on-demand when AI tools (Claude, Cursor) invoke it
- Communication happens via stdin/stdout, not HTTP
- The server starts when needed and exits when done
- No persistent daemon or background process

## Environment Variables

You can configure the server behavior using these optional environment variables:

- `SHADOWGIT_TIMEOUT` - Command execution timeout in milliseconds (default: 10000)
- `SHADOWGIT_SESSION_API` - Session API URL (default: http://localhost:45289/api)
- `SHADOWGIT_LOG_LEVEL` - Log level: debug, info, warn, error (default: info)
- `SHADOWGIT_HINTS` - Set to `0` to disable workflow hints in git command outputs (default: enabled)

Example:
```bash
export SHADOWGIT_TIMEOUT=30000  # 30 second timeout
export SHADOWGIT_LOG_LEVEL=debug  # Enable debug logging
export SHADOWGIT_HINTS=0  # Disable workflow banners for cleaner output
```

## Available Commands

### Session Management

**The Session API** (requires ShadowGit >= 0.3.0) allows AI assistants to temporarily pause ShadowGit's auto-commit feature and create clean, organized commits instead of having fragmented auto-commits during AI work.

**IMPORTANT**: AI assistants MUST follow this four-step workflow when making changes:

1. **`start_session({repo, description})`** - Start work session BEFORE making changes (pauses auto-commits)
2. **Make your changes** - Edit code, fix bugs, add features
3. **`checkpoint({repo, title, message?, author?})`** - Create a clean commit AFTER completing work  
4. **`end_session({sessionId, commitHash?})`** - End session when done (resumes auto-commits)

This workflow ensures AI-assisted changes result in clean, reviewable commits instead of fragmented auto-saves.

### `list_repos()`
Lists all ShadowGit-tracked repositories.

```javascript
await shadowgit.list_repos()
```

### `git_command({repo, command})`
Executes read-only git commands on a specific repository.

```javascript
// View recent commits
await shadowgit.git_command({
  repo: "my-project",
  command: "log --oneline -10"
})

// Check what changed recently
await shadowgit.git_command({
  repo: "my-project", 
  command: "diff HEAD~5 HEAD --stat"
})

// Find who changed a specific line
await shadowgit.git_command({
  repo: "my-project",
  command: "blame src/auth.ts"
})
```

### `start_session({repo, description})`
Starts an AI work session using the Session API. This pauses ShadowGit's auto-commit feature, allowing you to make multiple changes that will be grouped into a single clean commit.

```javascript
const result = await shadowgit.start_session({
  repo: "my-app",
  description: "Fixing authentication bug"
})
// Returns: Session ID (e.g., "mcp-client-1234567890")
```

### `checkpoint({repo, title, message?, author?})`
Creates a checkpoint commit to save your work.

```javascript
// After fixing a bug
const result = await shadowgit.checkpoint({
  repo: "my-app",
  title: "Fix null pointer exception in auth",
  message: "Added null check before accessing user object",
  author: "Claude"
})
// Returns formatted commit details including the commit hash

// After adding a feature
await shadowgit.checkpoint({
  repo: "my-app",
  title: "Add dark mode toggle",
  message: "Implemented theme switching using CSS variables and localStorage persistence",
  author: "GPT-4"
})

// Minimal usage (author defaults to "AI Assistant")
await shadowgit.checkpoint({
  repo: "my-app",
  title: "Update dependencies"
})
```

### `end_session({sessionId, commitHash?})`
Ends the AI work session via the Session API. This resumes ShadowGit's auto-commit functionality for regular development.

```javascript
await shadowgit.end_session({
  sessionId: "mcp-client-1234567890",
  commitHash: "abc1234"  // Optional: from checkpoint result
})
```

**Parameters:**
- `repo` (required): Repository name or full path
- `title` (required): Short commit title (max 50 characters)
- `message` (optional): Detailed description of changes
- `author` (optional): Your identifier (e.g., "Claude", "GPT-4", "Gemini") - defaults to "AI Assistant"

**Notes:**
- Sessions prevent auto-commits from interfering with AI work
- Automatically respects `.gitignore` patterns
- Creates a timestamped commit with author identification
- Will report if there are no changes to commit

## Security

- **Read-only access**: Only safe git commands are allowed
- **No write operations**: Commands like `commit`, `push`, `merge` are blocked
- **No destructive operations**: Commands like `branch`, `tag`, `reflog` are blocked to prevent deletions
- **Repository validation**: Only ShadowGit repositories can be accessed  
- **Path traversal protection**: Attempts to access files outside repositories are blocked
- **Command injection prevention**: Uses `execFileSync` with array arguments for secure execution
- **Dangerous flag blocking**: Blocks `--git-dir`, `--work-tree`, `--exec`, `-c`, `--config`, `-C` and other risky flags
- **Timeout protection**: Commands are limited to prevent hanging
- **Enhanced error reporting**: Git errors now include stderr/stdout for better debugging

## Best Practices for AI Assistants

When using ShadowGit MCP Server, AI assistants should:

1. **Follow the workflow**: Always: `start_session()` → make changes → `checkpoint()` → `end_session()`
2. **Use descriptive titles**: Keep titles under 50 characters but make them meaningful
3. **Always create checkpoints**: Call `checkpoint()` after completing each task
4. **Identify yourself**: Use the `author` parameter to identify which AI created the checkpoint
5. **Document changes**: Use the `message` parameter to explain what was changed and why
6. **End sessions properly**: Always call `end_session()` to resume auto-commits

### Complete Example Workflow
```javascript
// 1. First, check available repositories
const repos = await shadowgit.list_repos()

// 2. Start session BEFORE making changes
const sessionId = await shadowgit.start_session({
  repo: "my-app",
  description: "Refactoring authentication module"
})

// 3. Examine recent history
await shadowgit.git_command({
  repo: "my-app",
  command: "log --oneline -5"
})

// 4. Make your changes to the code...
// ... (edit files, fix bugs, etc.) ...

// 5. IMPORTANT: Create a checkpoint after completing the task
const commitHash = await shadowgit.checkpoint({
  repo: "my-app",
  title: "Refactor authentication module",
  message: "Simplified login flow and added better error handling",
  author: "Claude"
})

// 6. End the session when done
await shadowgit.end_session({
  sessionId: sessionId,
  commitHash: commitHash  // Optional but recommended
})
```

## Example Use Cases

### Debug Recent Changes
```javascript
// Find what broke in the last hour
await shadowgit.git_command({
  repo: "my-app",
  command: "log --since='1 hour ago' --oneline"
})
```

### Trace Code Evolution
```javascript
// See how a function evolved
await shadowgit.git_command({
  repo: "my-app", 
  command: "log -L :functionName:src/file.ts"
})
```

### Cross-Repository Analysis
```javascript
// Compare activity across projects
const repos = await shadowgit.list_repos()
for (const repo of repos) {
  await shadowgit.git_command({
    repo: repo.name,
    command: "log --since='1 day ago' --oneline"
  })
}
```

## Troubleshooting

### No repositories found
- Ensure ShadowGit app is installed and has tracked repositories
- Check that `~/.shadowgit/repos.json` exists

### Repository not found
- Use `list_repos()` to see exact repository names
- Ensure the repository has a `.shadowgit.git` directory

### Git commands fail
- Verify git is installed: `git --version`
- Only read-only commands are allowed
- Use absolute paths or repository names from `list_repos()`
- Check error output which now includes stderr details for debugging

### Workflow hints are too verbose
- Set `SHADOWGIT_HINTS=0` environment variable to disable workflow banners
- This provides cleaner output for programmatic use

### Session API offline
If you see "Session API is offline. Proceeding without session tracking":
- The ShadowGit app may not be running
- Sessions won't be tracked but git commands will still work
- Auto-commits won't be paused (may cause fragmented commits)
- Make sure ShadowGit app is running
- Go in ShadowGit settings and check that the Session API is healthy

## Development

For contributors who want to modify or extend the MCP server:

```bash
# Clone the repository (private GitHub repo)
git clone https://github.com/shadowgit/shadowgit-mcp-server.git
cd shadowgit-mcp-server
npm install

# Build
npm run build

# Test
npm test

# Run locally for development
npm run dev

# Test the built version locally
node dist/shadowgit-mcp-server.js
```

### Publishing Updates

```bash
# Update version
npm version patch  # or minor/major

# Build and test
npm run build
npm test

# Publish to npm (public registry)
npm publish
```

## License

MIT License - see [LICENSE](LICENSE) file for details.

## Related Projects

- [ShadowGit](https://shadowgit.com) - Automatic code snapshot tool
- [MCP SDK](https://github.com/modelcontextprotocol/typescript-sdk) - Model Context Protocol TypeScript SDK

---


Transform your development history into a powerful AI debugging assistant! 🚀

[![MCP Badge](https://lobehub.com/badge/mcp/shadowgit-shadowgit-mcp-server)](https://lobehub.com/mcp/shadowgit-shadowgit-mcp-server)

```

--------------------------------------------------------------------------------
/tests/__mocks__/@modelcontextprotocol/sdk/server/stdio.js:
--------------------------------------------------------------------------------

```javascript
module.exports = {
  StdioServerTransport: jest.fn(),
};
```

--------------------------------------------------------------------------------
/tests/__mocks__/@modelcontextprotocol/sdk/types.js:
--------------------------------------------------------------------------------

```javascript
module.exports = {
  CallToolRequestSchema: {
    parse: jest.fn((data) => data),
  },
  ListToolsRequestSchema: {
    parse: jest.fn((data) => data),
  },
};
```

--------------------------------------------------------------------------------
/tests/__mocks__/@modelcontextprotocol/sdk/server/index.js:
--------------------------------------------------------------------------------

```javascript
module.exports = {
  Server: jest.fn().mockImplementation(() => ({
    setRequestHandler: jest.fn(),
    connect: jest.fn().mockResolvedValue(undefined),
    close: jest.fn().mockResolvedValue(undefined),
  })),
};
```

--------------------------------------------------------------------------------
/src/utils/constants.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Shared constants for ShadowGit MCP Server
 */

export const SHADOWGIT_DIR = '.shadowgit.git';
export const TIMEOUT_MS = parseInt(process.env.SHADOWGIT_TIMEOUT || '10000', 10); // Default 10 seconds
export const MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB
export const MAX_COMMAND_LENGTH = 1000; // Maximum git command length
export const VERSION = '1.1.2';

// Session API configuration
export const SESSION_API_URL = process.env.SHADOWGIT_SESSION_API || 'http://localhost:45289/api';
export const SESSION_API_TIMEOUT = 3000; // 3 seconds timeout for session API calls
```

--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Simple logging utility for ShadowGit MCP Server
 */

export type LogLevel = 'debug' | 'info' | 'warn' | 'error';

const LOG_LEVELS: Record<LogLevel, number> = {
  debug: 0,
  info: 1,
  warn: 2,
  error: 3
};

const CURRENT_LOG_LEVEL = LOG_LEVELS[process.env.SHADOWGIT_LOG_LEVEL as LogLevel] ?? LOG_LEVELS.info;

export const log = (level: LogLevel, message: string): void => {
  if (LOG_LEVELS[level] >= CURRENT_LOG_LEVEL) {
    const timestamp = new Date().toISOString();
    process.stderr.write(`[${timestamp}] [shadowgit-mcp] [${level.toUpperCase()}] ${message}\n`);
  }
};
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": false,
    "sourceMap": false,
    "allowJs": true,
    "downlevelIteration": true,
    "allowSyntheticDefaultImports": true
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist",
    "**/*.test.ts"
  ]
}
```

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Type definitions for ShadowGit MCP Server
 */

export interface Repository {
  name: string;
  path: string;
}

export interface GitCommandArgs {
  repo: string;
  command: string;
}

export interface ManualCheckpointArgs {
  repo: string;
  title: string;
  message?: string;
  author?: string;
}

// MCP Tool Response format
export type MCPToolResponse = {
  content: Array<{
    type: string;
    text: string;
  }>;
  success?: boolean;  // Optional flag to indicate if the operation was successful
};

// Session API types
export interface SessionStartRequest {
  repoPath: string;
  aiTool: string;
  description: string;
}

export interface SessionStartResponse {
  success: boolean;
  sessionId?: string;
  error?: string;
}

export interface SessionEndRequest {
  sessionId: string;
  commitHash?: string;
}

export interface SessionEndResponse {
  success: boolean;
  error?: string;
}
```

--------------------------------------------------------------------------------
/src/core/security-constants.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Security constants for Git command validation
 */

// Read-only commands allowed for AI assistants
export const SAFE_COMMANDS = new Set([
  'log', 'show', 'diff', 'status',
  'describe', 'rev-parse', 'ls-files',
  'ls-tree', 'cat-file', 'show-branch', 'shortlog',
  'rev-list', 'blame'
]);

// Dangerous arguments that should always be blocked
export const DANGEROUS_PATTERNS = [
  '--upload-pack',
  '--receive-pack', 
  '--exec',
  '-c',              // Block config overrides
  '--config',
  '-e',              // Block -e flag
  '--git-dir',       // Block repository override
  '--work-tree',     // Block work tree override
  '-C'               // Block directory change
];

// Check if an argument is dangerous
export function isDangerousArg(arg: string): boolean {
  const lowerArg = arg.toLowerCase();
  return DANGEROUS_PATTERNS.some(pattern => 
    lowerArg === pattern || lowerArg.startsWith(pattern + '=')
  );
}
```

--------------------------------------------------------------------------------
/src/utils/file-utils.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * File system utility functions
 */

import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

export function getStorageLocation(): string {
  const platform = process.platform;
  const homeDir = os.homedir();
  
  switch (platform) {
    case 'darwin':
      return path.join(homeDir, '.shadowgit');
    case 'win32':
      return path.join(
        process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'),
        'shadowgit'
      );
    default:
      return path.join(
        process.env.XDG_DATA_HOME || path.join(homeDir, '.local', 'share'),
        'shadowgit'
      );
  }
}

export function fileExists(filePath: string): boolean {
  try {
    return fs.existsSync(filePath);
  } catch {
    return false;
  }
}

export function readJsonFile<T>(filePath: string, defaultValue: T): T {
  try {
    if (!fileExists(filePath)) {
      return defaultValue;
    }
    
    const content = fs.readFileSync(filePath, 'utf-8');
    return JSON.parse(content) as T;
  } catch (error) {
    return defaultValue;
  }
}
```

--------------------------------------------------------------------------------
/src/utils/response-utils.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Utility functions for creating consistent MCPToolResponse objects
 */

import type { MCPToolResponse } from '../types';

/**
 * Create a text response for MCP tools
 */
export function createTextResponse(text: string): MCPToolResponse {
  return {
    content: [
      {
        type: 'text',
        text
      }
    ]
  };
}

/**
 * Create an error response for MCP tools
 */
export function createErrorResponse(error: string, details?: string): MCPToolResponse {
  const message = details ? `${error}\n\n${details}` : error;
  return {
    content: [
      {
        type: 'text',
        text: message
      }
    ],
    success: false
  };
}

/**
 * Format a list of repositories for display
 */
export function formatRepositoryList(repos: Array<{ name: string; path: string }>): string {
  if (repos.length === 0) {
    return 'No repositories available.';
  }
  return repos.map(r => `  ${r.name}:\n    Path: ${r.path}`).join('\n\n');
}

/**
 * Create repository not found error response
 */
export function createRepoNotFoundResponse(repoName: string, availableRepos: Array<{ name: string; path: string }>): MCPToolResponse {
  let errorMsg = `Error: Repository '${repoName}' not found.`;
  
  if (availableRepos.length > 0) {
    errorMsg += `\n\nAvailable repositories:\n${formatRepositoryList(availableRepos)}`;
  } else {
    errorMsg += '\n\nNo repositories found. Please add repositories to ShadowGit first.';
  }
  
  return createErrorResponse(errorMsg);
}
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "shadowgit-mcp-server",
  "version": "1.1.2",
  "description": "MCP server providing AI assistants with read-only access to ShadowGit repositories",
  "author": "Alessandro Afloarei",
  "license": "MIT",
  "homepage": "https://shadowgit.com",
  "keywords": [
    "mcp",
    "model-context-protocol",
    "git",
    "ai",
    "claude",
    "cursor",
    "debugging",
    "shadowgit"
  ],
  "bin": {
    "shadowgit-mcp-server": "./dist/shadowgit-mcp-server.js"
  },
  "main": "./dist/shadowgit-mcp-server.js",
  "types": "./dist/shadowgit-mcp-server.d.ts",
  "files": [
    "dist/**/*",
    "README.md",
    "LICENSE"
  ],
  "scripts": {
    "build": "npm run build:prod",
    "build:dev": "tsc",
    "build:prod": "node esbuild.config.js",
    "dev": "ts-node src/shadowgit-mcp-server.ts",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "lint": "eslint src --ext .ts",
    "prepublishOnly": "npm run build:prod",
    "clean": "rimraf dist"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.5.0"
  },
  "devDependencies": {
    "@jest/globals": "^29.7.0",
    "@types/jest": "^29.5.0",
    "@types/node": "^20.0.0",
    "@typescript-eslint/eslint-plugin": "^7.1.1",
    "@typescript-eslint/parser": "^7.1.1",
    "esbuild": "^0.25.9",
    "eslint": "^8.57.0",
    "jest": "^29.7.0",
    "rimraf": "^6.0.1",
    "ts-jest": "^29.4.1",
    "ts-node": "^10.9.0",
    "typescript": "^5.0.0"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}

```

--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------

```javascript
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/tests'],
  testMatch: ['**/*.test.ts'],  // Run all test files
  transform: {
    '^.+\\.ts$': ['ts-jest', {
      useESM: false,
      tsconfig: {
        moduleResolution: 'node',
        allowSyntheticDefaultImports: true,
        esModuleInterop: true
      }
    }]
  },
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
    // Map both with and without .js extension
    '^@modelcontextprotocol/sdk/server/index.js$': '<rootDir>/tests/__mocks__/@modelcontextprotocol/sdk/server/index.js',
    '^@modelcontextprotocol/sdk/server/stdio.js$': '<rootDir>/tests/__mocks__/@modelcontextprotocol/sdk/server/stdio.js',
    '^@modelcontextprotocol/sdk/types.js$': '<rootDir>/tests/__mocks__/@modelcontextprotocol/sdk/types.js',
    '^@modelcontextprotocol/sdk/server/index$': '<rootDir>/tests/__mocks__/@modelcontextprotocol/sdk/server/index.js',
    '^@modelcontextprotocol/sdk/server/stdio$': '<rootDir>/tests/__mocks__/@modelcontextprotocol/sdk/server/stdio.js',
    '^@modelcontextprotocol/sdk/types$': '<rootDir>/tests/__mocks__/@modelcontextprotocol/sdk/types.js'
  },
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/**/*.test.ts',
    '!src/shadowgit-mcp-server.ts', // Main entry point
  ],
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html'],
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  testTimeout: 10000,
  setupFilesAfterEnv: [],
  clearMocks: true,
  restoreMocks: true,
};
```

--------------------------------------------------------------------------------
/esbuild.config.js:
--------------------------------------------------------------------------------

```javascript
const esbuild = require('esbuild');
const fs = require('fs');
const path = require('path');

async function build() {
  try {
    // Clean dist directory
    const distPath = path.join(__dirname, 'dist');
    if (fs.existsSync(distPath)) {
      fs.rmSync(distPath, { recursive: true });
    }
    fs.mkdirSync(distPath, { recursive: true });

    // Build the bundled and minified version
    const result = await esbuild.build({
      entryPoints: ['src/shadowgit-mcp-server.ts'],
      bundle: true,
      minify: true,
      platform: 'node',
      target: 'node18',
      outfile: 'dist/shadowgit-mcp-server.js',
      external: [
        // Don't bundle node built-ins
      ],
      format: 'cjs',
      sourcemap: false,
      treeShaking: true,
      metafile: true,
      banner: {
        js: '#!/usr/bin/env node'
      },
      define: {
        'process.env.NODE_ENV': '"production"'
      }
    });

    // Print build stats
    const text = await esbuild.analyzeMetafile(result.metafile);
    console.log('Build analysis:');
    console.log(text);

    // Also build TypeScript declarations using tsc
    console.log('\nGenerating TypeScript declarations...');
    const { execSync } = require('child_process');
    execSync('tsc --emitDeclarationOnly', { stdio: 'inherit' });

    console.log('\n✅ Build completed successfully!');
    
    // Check final size
    const stats = fs.statSync('dist/shadowgit-mcp-server.js');
    console.log(`\n📦 Bundle size: ${(stats.size / 1024).toFixed(2)}KB`);
    
  } catch (error) {
    console.error('Build failed:', error);
    process.exit(1);
  }
}

// Run build if called directly
if (require.main === module) {
  build();
}

module.exports = { build };
```

--------------------------------------------------------------------------------
/test-package.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

/**
 * Simple test to verify the package works
 */

const { spawn } = require('child_process');
const path = require('path');

async function testPackage() {
  console.log('🧪 Testing shadowgit-mcp-server package...\n');

  // Test 1: Check built file exists
  const builtFile = path.join(__dirname, 'dist', 'shadowgit-mcp-server.js');
  try {
    require('fs').accessSync(builtFile);
    console.log('✅ Built file exists:', builtFile);
  } catch (error) {
    console.log('❌ Built file missing:', builtFile);
    return false;
  }

  // Test 2: Check package.json is valid
  try {
    const pkg = require('./package.json');
    console.log('✅ Package.json valid');
    console.log('   Name:', pkg.name);
    console.log('   Version:', pkg.version);
    console.log('   Bin:', pkg.bin);
  } catch (error) {
    console.log('❌ Package.json invalid:', error.message);
    return false;
  }

  // Test 3: Try running the server (should wait for input)
  try {
    console.log('✅ Testing server startup...');
    const child = spawn('node', [builtFile], { stdio: 'pipe' });
    
    // Give it a moment to start
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    if (!child.killed) {
      console.log('✅ Server starts successfully (PID:', child.pid + ')');
      child.kill();
    } else {
      console.log('❌ Server failed to start');
      return false;
    }
  } catch (error) {
    console.log('❌ Server test failed:', error.message);
    return false;
  }

  console.log('\n🎉 Package test completed successfully!');
  console.log('\nNext steps:');
  console.log('1. npm publish (when ready)');  
  console.log('2. npm install -g shadowgit-mcp-server');
  console.log('3. claude mcp add shadowgit -- shadowgit-mcp-server');
  
  return true;
}

testPackage().catch(console.error);
```

--------------------------------------------------------------------------------
/src/handlers/list-repos-handler.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Handler for list_repos tool
 */

import { RepositoryManager } from '../core/repository-manager';
import { createTextResponse, formatRepositoryList } from '../utils/response-utils';
import type { MCPToolResponse } from '../types';

export class ListReposHandler {
  constructor(private repositoryManager: RepositoryManager) {}

  /**
   * Handle list_repos tool execution
   */
  async handle(): Promise<MCPToolResponse> {
    const repos = this.repositoryManager.getRepositories();
    
    if (repos.length === 0) {
      return createTextResponse(
        `No repositories found in ShadowGit.

To add repositories:
1. Open the ShadowGit application
2. Click "Add Repository" 
3. Select the repository you want to track

ShadowGit will automatically create shadow repositories (.shadowgit.git) to track changes.`
      );
    }
    
    const repoList = formatRepositoryList(repos);
    const firstRepo = repos[0].name;
    
    return createTextResponse(
      `🚀 **ShadowGit MCP Server Connected**
${'='.repeat(50)}

📁 **Available Repositories (${repos.length})**
${repoList}

${'='.repeat(50)}
⚠️ **CRITICAL: Required Workflow for ALL Changes**
${'='.repeat(50)}

**You MUST follow this 4-step workflow:**

1️⃣ **START SESSION** (before ANY edits)
   \`start_session({repo: "${firstRepo}", description: "your task"})\`

2️⃣ **MAKE YOUR CHANGES**
   Edit code, fix bugs, add features

3️⃣ **CREATE CHECKPOINT** (after changes complete)
   \`checkpoint({repo: "${firstRepo}", title: "Clear commit message"})\`

4️⃣ **END SESSION** (to resume auto-commits)
   \`end_session({sessionId: "...", commitHash: "..."})\`

${'='.repeat(50)}

💡 **Quick Start Examples:**
\`\`\`javascript
// Check recent history
git_command({repo: "${firstRepo}", command: "log -5"})

// Start your work session
start_session({repo: "${firstRepo}", description: "Fixing authentication bug"})
\`\`\`

📖 **NEXT STEP:** Call \`start_session()\` before making any changes!`
    );
  }
}
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
# Changelog

All notable changes to the ShadowGit MCP Server will be documented in this file.

## [1.1.2] - 2025-09-05

### Security Improvements
- **Critical**: Removed `branch`, `tag`, `reflog` commands to prevent destructive operations
- Added `-C` flag to blocked arguments to prevent directory changes
- Enhanced repository validation to check for .shadowgit.git on raw paths

### Bug Fixes
- Fixed remaining error responses in SessionHandler to use createErrorResponse
- Aligned email domains to consistently use @shadowgit.local

## [1.1.1] - 2025-09-05

### Security Improvements
- **Critical**: Block `--git-dir` and `--work-tree` flags to prevent repository escape attacks
- Switched internal commands to array-based execution, eliminating command injection risks
- Enhanced Git error reporting to include stderr/stdout for better debugging
- Fixed command length validation to only apply to external commands

### Features
- Added `SHADOWGIT_HINTS` environment variable to toggle workflow hints (set to `0` to disable)
- Standardized all error responses with consistent `success: false` flag

### Bug Fixes
- Fixed string command parser to handle all whitespace characters (tabs, spaces, etc.)
- Fixed Jest configuration for extensionless imports
- Removed .js extensions from TypeScript imports for better compatibility
- Improved error handling for Git commands with exit codes

### Developer Experience
- Added comprehensive test coverage for security features
- Improved documentation with security updates and troubleshooting tips
- All 175 tests passing with improved coverage

## [1.1.0] - 2025-09-04

### Features
- Added session management with start_session and end_session
- Added checkpoint command for creating AI-authored commits
- Integrated with ShadowGit Session API for auto-commit control
- Added workflow reminders in git command outputs

### Security
- Implemented comprehensive command validation
- Added dangerous argument blocking
- Path traversal protection
- Repository validation

## [1.0.0] - 2025-09-03

### Initial Release
- MCP server implementation for ShadowGit
- Support for read-only git commands
- Repository listing functionality
- Integration with Claude Code and Claude Desktop
- Basic security restrictions
```

--------------------------------------------------------------------------------
/src/handlers/git-handler.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Handler for git_command tool
 */

import { RepositoryManager } from '../core/repository-manager';
import { GitExecutor } from '../core/git-executor';
import { createErrorResponse, createTextResponse, createRepoNotFoundResponse } from '../utils/response-utils';
import type { MCPToolResponse, GitCommandArgs } from '../types';

export class GitHandler {
  constructor(
    private repositoryManager: RepositoryManager,
    private gitExecutor: GitExecutor
  ) {}

  /**
   * Validate git command arguments
   */
  private isGitCommandArgs(args: unknown): args is GitCommandArgs {
    return (
      typeof args === 'object' &&
      args !== null &&
      'repo' in args &&
      'command' in args &&
      typeof (args as GitCommandArgs).repo === 'string' &&
      typeof (args as GitCommandArgs).command === 'string'
    );
  }

  /**
   * Handle git_command tool execution
   */
  async handle(args: unknown): Promise<MCPToolResponse> {
    if (!this.isGitCommandArgs(args)) {
      return createErrorResponse(
        "Error: Both 'repo' and 'command' parameters are required.",
        `Example usage:
  git_command({repo: "my-project", command: "log --oneline -10"})
  git_command({repo: "my-project", command: "diff HEAD~1"})
  
Use list_repos() to see available repositories.`
      );
    }

    const repoPath = this.repositoryManager.resolveRepoPath(args.repo);
    
    if (!repoPath) {
      const repos = this.repositoryManager.getRepositories();
      return createRepoNotFoundResponse(args.repo, repos);
    }

    const output = await this.gitExecutor.execute(args.command, repoPath);
    
    // Add workflow reminder for common commands that suggest changes are being planned
    // Show workflow hints unless disabled
    const showHints = process.env.SHADOWGIT_HINTS !== '0';
    const reminderCommands = ['diff', 'status', 'log', 'blame'];
    const needsReminder = showHints && reminderCommands.some(cmd => args.command.toLowerCase().includes(cmd));
    
    if (needsReminder) {
      return createTextResponse(
        `${output}

${'='.repeat(50)}
📝 **Planning to Make Changes?**
${'='.repeat(50)}

**Required Workflow:**
1️⃣ \`start_session({repo: "${args.repo}", description: "your task"})\`
2️⃣ Make your changes
3️⃣ \`checkpoint({repo: "${args.repo}", title: "commit message"})\`
4️⃣ \`end_session({sessionId: "...", commitHash: "..."})\`

💡 **NEXT STEP:** Call \`start_session()\` before editing any files!`
      );
    }
    
    return createTextResponse(output);
  }
}
```

--------------------------------------------------------------------------------
/DEPLOYMENT.md:
--------------------------------------------------------------------------------

```markdown
# Deployment Guide

## Current Version
- **Package**: `shadowgit-mcp-server`
- **Version**: 1.1.2
- **npm Registry**: https://www.npmjs.com/package/shadowgit-mcp-server

## Installation

### For Users
```bash
# Install globally from npm
npm install -g shadowgit-mcp-server

# The command will be available globally
shadowgit-mcp-server --version
```

### For Development
```bash
# Clone and build locally
git clone https://github.com/shadowgit/shadowgit-mcp-server.git
cd shadowgit-mcp-server
npm install
npm run build
npm link  # Makes it available globally for testing
```

## Build System

### Production Build
```bash
npm run build
```
- Creates a single optimized bundle (`dist/shadowgit-mcp-server.js`)
- Size: ~93KB (includes all dependencies)
- Uses esbuild for fast bundling and minification
- Cross-platform: Works on macOS, Windows, and Linux

### File Structure
```
dist/
├── shadowgit-mcp-server.js    # Main bundled executable (93KB)
├── shadowgit-mcp-server.d.ts  # TypeScript declarations
└── [other .d.ts files]        # Additional type definitions
```

## Publishing Updates

### 1. Update Version
```bash
npm version patch  # Bug fixes (1.1.2 -> 1.1.3)
npm version minor  # New features (1.1.2 -> 1.2.0)
npm version major  # Breaking changes (1.1.2 -> 2.0.0)
```

### 2. Build and Test
```bash
npm run build
npm test
```

### 3. Publish to npm
```bash
npm publish
```

## MCP Configuration

### Claude Desktop
Add to `claude_desktop_config.json`:
```json
{
  "mcpServers": {
    "shadowgit": {
      "command": "shadowgit-mcp-server"
    }
  }
}
```

### Direct Execution
```json
{
  "mcpServers": {
    "shadowgit": {
      "command": "node",
      "args": ["/path/to/shadowgit-mcp-server/dist/shadowgit-mcp-server.js"]
    }
  }
}
```

## Cross-Platform Support

The bundled JavaScript file works identically across all platforms:
- **macOS/Linux**: Direct execution with shebang support
- **Windows**: npm creates `.cmd` wrapper for global installs
- **Node.js Requirement**: Version 18 or higher

## Quick Commands

```bash
# Check version
shadowgit-mcp-server --version

# Build locally
npm run build

# Run tests
npm test

# Clean build artifacts
npm run clean

# Development mode (TypeScript directly)
npm run dev
```

## Troubleshooting

### Module Not Found
- Run `npm install` to ensure all dependencies are installed
- For global install issues, check npm prefix: `npm config get prefix`

### Permission Denied (Unix)
```bash
chmod +x dist/shadowgit-mcp-server.js
```

### Windows Execution
Use `node dist/shadowgit-mcp-server.js` if the global command doesn't work

## Support

- GitHub Issues: https://github.com/shadowgit/shadowgit-mcp-server
- npm Package: https://www.npmjs.com/package/shadowgit-mcp-server
```

--------------------------------------------------------------------------------
/tests/shadowgit-mcp-server.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { ShadowGitMCPServer } from '../src/shadowgit-mcp-server';

// Mock child_process
jest.mock('child_process', () => ({
  execFileSync: jest.fn()
}));

// Mock fs
jest.mock('fs', () => ({
  existsSync: jest.fn(),
  readFileSync: jest.fn()
}));

// Mock os
jest.mock('os', () => ({
  homedir: jest.fn(() => '/home/testuser')
}));

describe('ShadowGitMCPServer', () => {
  let server: ShadowGitMCPServer;
  let mockExecFileSync: jest.Mock;
  let mockExistsSync: jest.Mock;
  let mockReadFileSync: jest.Mock;

  beforeEach(() => {
    // Reset all mocks
    jest.clearAllMocks();
    
    // Get mock references
    const childProcess = require('child_process');
    const fs = require('fs');
    mockExecFileSync = childProcess.execFileSync as jest.Mock;
    mockExistsSync = fs.existsSync as jest.Mock;
    mockReadFileSync = fs.readFileSync as jest.Mock;
    
    // Setup default mock behaviors
    mockExistsSync.mockReturnValue(true);
    mockReadFileSync.mockReturnValue(JSON.stringify([
      { name: 'test-repo', path: '/test/repo' },
      { name: 'another-repo', path: '/another/repo' }
    ]));
    
    // Create server instance
    server = new ShadowGitMCPServer();
  });

  describe('Server Initialization', () => {
    it('should create server instance successfully', () => {
      expect(server).toBeDefined();
      expect(server).toBeInstanceOf(ShadowGitMCPServer);
    });

    it('should initialize with required handlers', () => {
      // Server should be initialized with all required components
      expect(server).toBeDefined();
      // The actual handlers are private, but we can verify the server exists
    });
  });

  describe('Configuration Loading', () => {
    it('should load repositories from config file', () => {
      const testRepos = [
        { name: 'repo1', path: '/path/to/repo1' },
        { name: 'repo2', path: '/path/to/repo2' }
      ];
      
      mockReadFileSync.mockReturnValue(JSON.stringify(testRepos));
      
      // Create a new instance to trigger config loading
      const newServer = new ShadowGitMCPServer();
      
      expect(newServer).toBeDefined();
      expect(mockReadFileSync).toHaveBeenCalled();
    });

    it('should handle missing config file gracefully', () => {
      mockExistsSync.mockReturnValue(false);
      mockReadFileSync.mockImplementation(() => {
        throw new Error('File not found');
      });
      
      // Should not throw when config is missing
      expect(() => new ShadowGitMCPServer()).not.toThrow();
    });
  });

  describe('Server Lifecycle', () => {
    it('should handle server start', async () => {
      // Server should be properly initialized
      expect(server).toBeDefined();
    });

    it('should handle server shutdown gracefully', async () => {
      // Server should clean up resources on shutdown
      // This is typically handled by the Server class from MCP SDK
      expect(server).toBeDefined();
    });
  });
});
```

--------------------------------------------------------------------------------
/src/handlers/session-handler.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Handler for session management - start and end sessions
 */

import { RepositoryManager } from '../core/repository-manager';
import { SessionClient } from '../core/session-client';
import { log } from '../utils/logger';
import { createErrorResponse } from '../utils/response-utils';
import type { MCPToolResponse } from '../types';

interface StartSessionArgs {
  repo: string;
  description: string;
}

interface EndSessionArgs {
  sessionId: string;
  commitHash?: string;
}

export class SessionHandler {
  constructor(
    private repositoryManager: RepositoryManager,
    private sessionClient: SessionClient
  ) {}

  /**
   * Start a new work session
   */
  async startSession(args: unknown): Promise<MCPToolResponse> {
    // Validate args
    if (!this.isStartSessionArgs(args)) {
      return createErrorResponse(
        'Error: Both "repo" and "description" are required for start_session.'
      );
    }

    // Resolve repository
    const repoPath = this.repositoryManager.resolveRepoPath(args.repo);
    if (!repoPath) {
      return createErrorResponse(
        `Error: Repository '${args.repo}' not found. Use list_repos() to see available repositories.`
      );
    }

    // Start session
    const sessionId = await this.sessionClient.startSession({
      repoPath,
      aiTool: 'MCP Client',
      description: args.description
    });

    if (sessionId) {
      log('info', `Session started: ${sessionId}`);
      return {
        content: [{
          type: 'text',
          text: `Session started successfully.
Session ID: ${sessionId}

📋 **Your Workflow Checklist:**
1. Make your changes
2. Call checkpoint() to commit
3. Call end_session() with this session ID`
        }]
      };
    }

    // Fallback if Session API is offline
    return createErrorResponse(
      'Session API is offline. Proceeding without session tracking.'
    );
  }

  /**
   * End an active session
   */
  async endSession(args: unknown): Promise<MCPToolResponse> {
    // Validate args
    if (!this.isEndSessionArgs(args)) {
      return createErrorResponse(
        'Error: "sessionId" is required for end_session.'
      );
    }

    // End session
    const success = await this.sessionClient.endSession(
      args.sessionId,
      args.commitHash
    );

    if (success) {
      log('info', `Session ended: ${args.sessionId}`);
      return {
        content: [{
          type: 'text',
          text: `Session ${args.sessionId} ended successfully.`
        }]
      };
    }

    return createErrorResponse(
      `❌ **Failed to End Session**
${'='.repeat(50)}

⚠️ The session may have already ended or expired.

**Note:** Auto-commits may have already resumed.

💡 **NEXT STEP:** You can continue working or start a new session.`
    );
  }

  private isStartSessionArgs(args: unknown): args is StartSessionArgs {
    return (
      typeof args === 'object' &&
      args !== null &&
      'repo' in args &&
      'description' in args &&
      typeof (args as StartSessionArgs).repo === 'string' &&
      typeof (args as StartSessionArgs).description === 'string'
    );
  }

  private isEndSessionArgs(args: unknown): args is EndSessionArgs {
    return (
      typeof args === 'object' &&
      args !== null &&
      'sessionId' in args &&
      typeof (args as EndSessionArgs).sessionId === 'string'
    );
  }
}
```

--------------------------------------------------------------------------------
/src/core/session-client.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * HTTP client for ShadowGit Session API
 * Provides session lifecycle management for AI tools
 */

import { log } from '../utils/logger';
import { SESSION_API_URL, SESSION_API_TIMEOUT } from '../utils/constants';
import type { 
  SessionStartRequest, 
  SessionStartResponse, 
  SessionEndRequest, 
  SessionEndResponse 
} from '../types';

export class SessionClient {
  private baseUrl: string;
  private timeout: number;

  constructor(baseUrl = SESSION_API_URL, timeout = SESSION_API_TIMEOUT) {
    this.baseUrl = baseUrl;
    this.timeout = timeout;
  }

  /**
   * Start a new AI session for a repository
   */
  async startSession(data: SessionStartRequest): Promise<string | null> {
    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), this.timeout);

      const response = await fetch(`${this.baseUrl}/session/start`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      if (response.ok) {
        const result = await response.json() as SessionStartResponse;
        if (result.success && result.sessionId) {
          log('info', `Session started: ${result.sessionId} for ${data.repoPath}`);
          return result.sessionId;
        }
      }
      
      log('warn', `Failed to start session: ${response.status} ${response.statusText}`);
    } catch (error) {
      // Silently fail - don't break MCP if Session API is down
      if (error instanceof Error && error.name !== 'AbortError') {
        log('debug', `Session API unavailable: ${error.message}`);
      }
    }
    return null;
  }

  /**
   * End an AI session with optional commit hash
   */
  async endSession(sessionId: string, commitHash?: string): Promise<boolean> {
    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), this.timeout);

      const data: SessionEndRequest = { sessionId };
      if (commitHash) {
        data.commitHash = commitHash;
      }

      const response = await fetch(`${this.baseUrl}/session/end`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      if (response.ok) {
        const result = await response.json() as SessionEndResponse;
        if (result.success) {
          log('info', `Session ended: ${sessionId}`);
          return true;
        }
      }
      
      log('warn', `Failed to end session: ${response.status} ${response.statusText}`);
    } catch (error) {
      if (error instanceof Error && error.name !== 'AbortError') {
        log('debug', `Failed to end session: ${error.message}`);
      }
    }
    return false;
  }

  /**
   * Check if Session API is healthy
   */
  async isHealthy(): Promise<boolean> {
    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 1000); // Quick health check

      const response = await fetch(`${this.baseUrl}/health`, {
        signal: controller.signal,
      });

      clearTimeout(timeoutId);
      return response.ok;
    } catch {
      return false;
    }
  }
}
```

--------------------------------------------------------------------------------
/src/core/repository-manager.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Repository discovery and management
 */

import * as path from 'path';
import * as os from 'os';
import { getStorageLocation, fileExists, readJsonFile } from '../utils/file-utils';
import { log } from '../utils/logger';
import { SHADOWGIT_DIR } from '../utils/constants';
import type { Repository } from '../types';

export class RepositoryManager {
  private repositories: Repository[] = [];

  constructor() {
    this.loadRepositories();
  }

  /**
   * Load repositories from ShadowGit's configuration
   */
  private loadRepositories(): void {
    const storageLocation = getStorageLocation();
    const repositoryPath = path.join(storageLocation, 'repos.json');
    
    log('info', `Loading repositories from ${repositoryPath}`);
    
    this.repositories = readJsonFile<Repository[]>(repositoryPath, []);
    
    log('info', `Loaded ${this.repositories.length} repositories`);
    
    if (this.repositories.length === 0) {
      log('warn', 'No repositories found. Please add repositories via ShadowGit app.');
    }
  }

  /**
   * Get all loaded repositories
   */
  getRepositories(): Repository[] {
    return this.repositories;
  }

  /**
   * Find a repository by name
   */
  findRepository(name: string): Repository | undefined {
    return this.repositories.find(r => r.name === name);
  }

  /**
   * Resolve a repository name or path to an absolute path
   */
  resolveRepoPath(repoNameOrPath: string): string | null {
    // Handle null/undefined inputs
    if (!repoNameOrPath) {
      log('warn', 'No repository name or path provided');
      return null;
    }

    // First, check if it's a known repository name
    const knownRepo = this.findRepository(repoNameOrPath);
    if (knownRepo) {
      // Expand tilde in repository path if present
      let repoPath = knownRepo.path;
      if (repoPath.startsWith('~')) {
        const homeDir = os.homedir();
        if (repoPath === '~') {
          repoPath = homeDir;
        } else if (repoPath.startsWith('~/')) {
          repoPath = path.join(homeDir, repoPath.slice(2));
        }
      }
      
      // Validate that the repository exists and has a .shadowgit.git directory
      const shadowgitPath = path.join(repoPath, SHADOWGIT_DIR);
      
      if (fileExists(shadowgitPath)) {
        log('debug', `Resolved repository '${repoNameOrPath}' to path: ${repoPath}`);
        return repoPath;
      } else {
        log('warn', `Repository '${repoNameOrPath}' exists but .shadowgit.git directory not found at: ${shadowgitPath}`);
        log('warn', 'Please ensure ShadowGit is monitoring this repository.');
        return null;
      }
    }
    
    // Support Unix-style paths and Windows paths
    const isPath = repoNameOrPath.startsWith('/') || 
                   repoNameOrPath.startsWith('~') ||
                   repoNameOrPath.includes(':') || // Windows drive letter
                   repoNameOrPath.startsWith('\\\\'); // UNC path
                   
    if (isPath) {
      // Properly handle tilde expansion
      let resolvedPath = repoNameOrPath;
      if (repoNameOrPath.startsWith('~')) {
        const homeDir = os.homedir();
        if (repoNameOrPath === '~') {
          resolvedPath = homeDir;
        } else if (repoNameOrPath.startsWith('~/')) {
          resolvedPath = path.join(homeDir, repoNameOrPath.slice(2));
        } else {
          // ~username not supported, return null
          log('warn', `Unsupported tilde expansion: ${repoNameOrPath}`);
          return null;
        }
      }
      
      resolvedPath = path.normalize(resolvedPath);
      
      // Ensure the resolved path is absolute and doesn't escape
      if (!path.isAbsolute(resolvedPath)) {
        log('warn', `Invalid path provided: ${repoNameOrPath}`);
        return null;
      }
      
      if (fileExists(resolvedPath)) {
        // Validate that the path has a .shadowgit.git directory
        const shadowgitPath = path.join(resolvedPath, SHADOWGIT_DIR);
        if (fileExists(shadowgitPath)) {
          return resolvedPath;
        } else {
          log('warn', `Path exists but .shadowgit.git directory not found at: ${shadowgitPath}`);
          return null;
        }
      }
    }
    
    return null;
  }
}
```

--------------------------------------------------------------------------------
/src/core/git-executor.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Git command execution with security and safety checks
 */

import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { log } from '../utils/logger';
import { 
  SHADOWGIT_DIR, 
  TIMEOUT_MS, 
  MAX_BUFFER_SIZE, 
  MAX_COMMAND_LENGTH 
} from '../utils/constants';
import { SAFE_COMMANDS, isDangerousArg } from './security-constants';

export class GitExecutor {

  /**
   * Execute a git command with security checks
   * @param command - Either a string command or array of arguments
   */
  async execute(
    command: string | string[], 
    repoPath: string, 
    isInternal = false,
    additionalEnv?: NodeJS.ProcessEnv
  ): Promise<string> {
    // Parse command into arguments
    let args: string[];
    
    if (Array.isArray(command)) {
      // Array-based command (safer for internal use)
      args = command;
    } else {
      // String command - check length only for external calls
      if (!isInternal && command.length > MAX_COMMAND_LENGTH) {
        return `Error: Command too long (max ${MAX_COMMAND_LENGTH} characters).`;
      }
    
      // Remove control characters
      const sanitizedCommand = command.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
      
      // Simple argument parsing that handles quotes and all whitespace
      args = [];
      let current = '';
      let inQuotes = false;
      let quoteChar = '';
      
      for (let i = 0; i < sanitizedCommand.length; i++) {
        const char = sanitizedCommand[i];
        const nextChar = sanitizedCommand[i + 1];
        
        if (!inQuotes && (char === '"' || char === "'")) {
          inQuotes = true;
          quoteChar = char;
        } else if (inQuotes && char === '\\' && nextChar === quoteChar) {
          // Handle escaped quote
          current += quoteChar;
          i++; // Skip the quote
        } else if (inQuotes && char === quoteChar) {
          inQuotes = false;
          quoteChar = '';
        } else if (!inQuotes && /\s/.test(char)) {
          // Split on any whitespace (space, tab, etc.)
          if (current) {
            args.push(current);
            current = '';
          }
        } else {
          current += char;
        }
      }
      if (current) {
        args.push(current);
      }
    }
    
    if (args.length === 0) {
      return 'Error: No command provided.';
    }
    
    const gitCommand = args[0];
    
    // Safety check 1: ALWAYS block dangerous arguments
    for (const arg of args) {
      if (isDangerousArg(arg)) {
        return 'Error: Command contains potentially dangerous arguments.';
      }
    }
    
    // Safety check 2: Only check command whitelist for external calls
    if (!isInternal && !SAFE_COMMANDS.has(gitCommand)) {
      return `Error: Command '${gitCommand}' is not allowed. Only read-only commands are permitted.

Allowed commands: ${Array.from(SAFE_COMMANDS).join(', ')}`;
    }
    
    // Safety check 3: Ensure we're operating on a .shadowgit.git repository
    const gitDir = path.join(repoPath, SHADOWGIT_DIR);
    
    if (!fs.existsSync(gitDir)) {
      return `Error: Not a ShadowGit repository. The .shadowgit.git directory was not found at ${gitDir}`;
    }
    
    log('debug', `Executing git ${gitCommand} in ${repoPath}`);
    
    try {
      const output = execFileSync('git', [
        `--git-dir=${gitDir}`,
        `--work-tree=${repoPath}`,
        ...args
      ], {
        cwd: repoPath,
        encoding: 'utf-8',
        timeout: TIMEOUT_MS,
        maxBuffer: MAX_BUFFER_SIZE,
        env: {
          ...process.env,
          GIT_TERMINAL_PROMPT: '0', // Disable interactive prompts
          GIT_SSH_COMMAND: 'ssh -o BatchMode=yes', // Disable SSH prompts
          GIT_PAGER: 'cat', // Disable pager
          PAGER: 'cat', // Fallback pager disable
          ...additionalEnv
        }
      });
      
      return output || '(empty output)';
    } catch (error: unknown) {
      if (error && typeof error === 'object') {
        const execError = error as any;
        
        // Check for timeout
        if (execError.code === 'ETIMEDOUT' || execError.signal === 'SIGTERM') {
          return `Error: Command timed out after ${TIMEOUT_MS}ms.`;
        }
        
        // Check for detailed error info (has stderr/stdout or status code)
        if ('stderr' in execError || 'stdout' in execError || 'status' in execError) {
          const stderr = execError.stderr?.toString() || '';
          const stdout = execError.stdout?.toString() || '';
          const message = execError.message || 'Unknown error';
          
          return `Error executing git command:
${message}
${stderr ? `\nError output:\n${stderr}` : ''}
${stdout ? `\nPartial output:\n${stdout}` : ''}`;
        }
      }
      
      return `Error: ${error}`;
    }
  }
}
```

--------------------------------------------------------------------------------
/src/handlers/checkpoint-handler.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Handler for checkpoint tool - creates git commits
 */

import { RepositoryManager } from '../core/repository-manager';
import { GitExecutor } from '../core/git-executor';
import { createErrorResponse } from '../utils/response-utils';
import type { MCPToolResponse, ManualCheckpointArgs } from '../types';

export class CheckpointHandler {
  constructor(
    private repositoryManager: RepositoryManager,
    private gitExecutor: GitExecutor
  ) {}

  /**
   * Validate checkpoint arguments
   */
  private isValidArgs(args: unknown): args is ManualCheckpointArgs {
    return (
      typeof args === 'object' &&
      args !== null &&
      'repo' in args &&
      'title' in args &&
      typeof (args as ManualCheckpointArgs).repo === 'string' &&
      typeof (args as ManualCheckpointArgs).title === 'string'
    );
  }

  /**
   * Handle checkpoint tool execution
   */
  async handle(args: unknown): Promise<MCPToolResponse> {
    if (!this.isValidArgs(args)) {
      return createErrorResponse(
        "Error: Both 'repo' and 'title' parameters are required.",
        `Example usage:
  checkpoint({
    repo: "my-project",
    title: "Fix authentication bug",
    author: "Claude"
  })

Use list_repos() to see available repositories.`
      );
    }
    
    // Validate title length
    if (args.title.length > 50) {
      return createErrorResponse(
        `Error: Title must be 50 characters or less (current: ${args.title.length} chars).`
      );
    }
    
    // Validate message length if provided
    if (args.message && args.message.length > 1000) {
      return createErrorResponse(
        `Error: Message must be 1000 characters or less (current: ${args.message.length} chars).`
      );
    }
    
    const repoPath = this.repositoryManager.resolveRepoPath(args.repo);
    
    if (!repoPath) {
      const repos = this.repositoryManager.getRepositories();
      
      if (repos.length === 0) {
        return createErrorResponse(
          'Error: No repositories found. Please add repositories to ShadowGit first.'
        );
      }
      
      return createErrorResponse(
        `Error: Repository '${args.repo}' not found.`,
        `Available repositories:
${repos.map(r => `  - ${r.name}: ${r.path}`).join('\n')}`
      );
    }


    // Check for changes
    const statusOutput = await this.gitExecutor.execute(['status', '--porcelain'], repoPath, true);
    
    if (!statusOutput || statusOutput.trim() === '' || statusOutput === '(empty output)') {
      return createErrorResponse(
        `❌ **No Changes Detected**
${'='.repeat(50)}

📁 Repository has no changes to commit.

⚠️ **Important:** Do NOT call end_session() - no commit was created.

💡 **NEXT STEP:** Make some changes first, then call checkpoint() again.`
      );
    }
    
    // Build commit message
    const commitTitle = args.title;
    const commitBody = args.message || '';
    const author = args.author || 'AI Assistant';
    
    // Add all changes
    const addOutput = await this.gitExecutor.execute(['add', '-A'], repoPath, true);
    
    if (addOutput.startsWith('Error:')) {
      return createErrorResponse(
        `❌ **Failed to Stage Changes**
${'='.repeat(50)}

🚨 **Error:** ${addOutput}

⚠️ **Important:** Do NOT call end_session() - commit was not created.

💡 **NEXT STEP:** Check the error and try again.`
      );
    }
    
    // Build full commit message
    let fullMessage = commitTitle;
    if (commitBody) {
      fullMessage += `\n\n${commitBody}`;
    }
    fullMessage += `\n\nAuthor: ${author} (via ShadowGit MCP)`;
    
    // Create commit with author information
    const commitEnv = {
      GIT_AUTHOR_NAME: author,
      GIT_AUTHOR_EMAIL: `${author.toLowerCase().replace(/\s+/g, '-')}@shadowgit.local`,
      GIT_COMMITTER_NAME: 'ShadowGit MCP',
      GIT_COMMITTER_EMAIL: '[email protected]'
    };
    
    // Use array-based command to avoid parsing issues
    const commitOutput = await this.gitExecutor.execute(
      ['commit', '-m', fullMessage],
      repoPath,
      true,
      commitEnv
    );
    
    if (commitOutput.startsWith('Error:')) {
      return createErrorResponse(
        `❌ **Failed to Create Commit**
${'='.repeat(50)}

🚨 **Error:** ${commitOutput}

⚠️ **Important:** Do NOT call end_session() - commit was not created.

💡 **NEXT STEP:** Check the error message and try checkpoint() again.`
      );
    }
    
    // Extract commit hash from output
    let commitHash: string | undefined;
    const hashMatch = commitOutput.match(/\[[\w\-]+ ([a-f0-9]+)\]/);
    if (hashMatch) {
      commitHash = hashMatch[1];
    }
    
    
    // Get summary of changes
    const showOutput = await this.gitExecutor.execute(['show', '--stat', '--format=short', 'HEAD'], repoPath, true);
    
    return {
      content: [
        {
          type: 'text',
          text: `✅ **Checkpoint Created Successfully!**
${'='.repeat(50)}

📦 **Commit Details:**
${commitOutput}

📊 **Changes Summary:**
${showOutput}

🔑 **Commit Hash:** \`${commitHash || 'unknown'}\`

${'='.repeat(50)}

📋 **Workflow Progress:**
- [x] Session started
- [x] Changes made
- [x] Checkpoint created ✨
- [ ] Session ended

🚨 **REQUIRED NEXT STEP:**
You MUST now call \`end_session()\` to resume auto-commits:

\`\`\`javascript
end_session({
  sessionId: "your-session-id",
  commitHash: "${commitHash || 'unknown'}"
})
\`\`\`

⚠️ **Important:** Only call end_session() because the commit was SUCCESSFUL.`
        }
      ],
      success: true
    };
  }
}
```

--------------------------------------------------------------------------------
/TESTING.md:
--------------------------------------------------------------------------------

```markdown
# Testing Guide for ShadowGit MCP Server

This guide walks you through testing the ShadowGit MCP server with Claude Code and Claude Desktop after installing from npm.

## Prerequisites

Before testing, ensure you have:

1. **ShadowGit app** installed and running with at least one repository tracked
2. **shadowgit-mcp-server** installed globally from npm
3. **Claude Code CLI** or **Claude Desktop** installed
4. **Git** available in your PATH

## Installation

```bash
# Install the MCP server globally from npm
npm install -g shadowgit-mcp-server

# Verify installation
shadowgit-mcp-server --version
# or test it starts correctly (Ctrl+C to exit)
shadowgit-mcp-server
```

## Testing with Claude Code

### 1. Configure MCP Server

```bash
# Add the ShadowGit MCP server to Claude Code
claude mcp add shadowgit -- shadowgit-mcp-server

# Verify configuration
claude mcp list
# Should show: shadowgit

# Get details
claude mcp get shadowgit
```

### 2. Restart Claude Code

```bash
# Exit current session
exit

# Start new session
claude
```

### 3. Test Basic Commands

In Claude Code, try these commands:

```
"Can you list my ShadowGit repositories?"
```

Expected: Claude uses `shadowgit.list_repos()` and shows your repositories.

```
"Show me the last 5 commits in [your-repo-name]"
```

Expected: Claude uses `shadowgit.git({repo: "your-repo", command: "log --oneline -5"})`.

## Testing with Claude Desktop

### 1. Configure MCP Server

Add to your Claude Desktop configuration file:

**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`  
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`  
**Linux:** `~/.config/Claude/claude_desktop_config.json`

```json
{
  "mcpServers": {
    "shadowgit": {
      "command": "shadowgit-mcp-server"
    }
  }
}
```

### 2. Restart Claude Desktop

Completely quit and restart Claude Desktop to load the MCP server.

### 3. Test in Claude Desktop

Ask Claude questions like:
- "What ShadowGit repositories do I have?"
- "Show me recent commits in my project"
- "What changed in the last hour?"

## Test Scenarios

### Basic Discovery
```
User: "List my ShadowGit repositories"
```
Expected: Claude shows all your tracked repositories.

### Recent Changes
```
User: "What changed in [repo-name] today?"
```
Expected: Claude queries commits from today.

### Debugging Help
```
User: "Something broke in the last hour, can you help?"
```
Expected: Claude examines recent commits to identify potential issues.

### Code Evolution
```
User: "How has [filename] evolved over time?"
```
Expected: Claude traces the file's history.

## Expected MCP Commands

During testing, you should see Claude using:

```javascript
// List repositories
shadowgit.list_repos()

// Query git history
shadowgit.git({
  repo: "repository-name",
  command: "log --oneline -10"
})

// Check status
shadowgit.git({
  repo: "repository-name",
  command: "status"
})

// View diffs
shadowgit.git({
  repo: "repository-name",
  command: "diff HEAD~1 HEAD"
})
```

## Troubleshooting

### MCP Server Not Found

**Problem:** Claude says it doesn't have access to shadowgit commands.

**Solutions:**
1. Verify global installation: `which shadowgit-mcp-server`
2. Check MCP configuration: `claude mcp list`
3. Restart Claude Code/Desktop completely
4. Try removing and re-adding: 
   ```bash
   claude mcp remove shadowgit
   claude mcp add shadowgit -- shadowgit-mcp-server
   ```

### No Repositories Found

**Problem:** `list_repos()` returns empty.

**Solutions:**
1. Check ShadowGit app has repositories tracked
2. Verify `~/.shadowgit/repos.json` exists and has content
3. Test MCP server manually: `shadowgit-mcp-server` (should show loading message)

### Command Not Allowed

**Problem:** Git commands return "not allowed" error.

**Solutions:**
1. Only read-only commands are permitted
2. Check the command doesn't contain blocked arguments
3. See README for list of allowed commands

### Permission Errors

**Problem:** "EACCES" or permission denied errors.

**Solutions:**
1. Check npm global installation permissions
2. Verify `~/.shadowgit/` directory is readable
3. Try reinstalling with proper permissions:
   ```bash
   npm uninstall -g shadowgit-mcp-server
   sudo npm install -g shadowgit-mcp-server
   ```

## Verifying Success

Your setup is working correctly when:

✅ Claude can list your ShadowGit repositories  
✅ Claude can execute git commands on your repos  
✅ Claude blocks write operations appropriately  
✅ Claude can query multiple repositories  
✅ Error messages are clear and helpful  

## Advanced Testing

### Performance Testing
```
User: "Show me all commits from the last week with statistics"
```
Should complete within 10 seconds.

### Multi-Repository Testing
```
User: "Compare activity across all my projects today"
```
Should query each repository efficiently.

### Security Testing
```
User: "Can you commit these changes?"
```
Should be rejected with explanation about read-only access.

## Getting Help

If you encounter issues:

1. Check the npm package version: `npm list -g shadowgit-mcp-server`
2. Update to latest: `npm update -g shadowgit-mcp-server`
3. Review server output when running manually
4. Check Claude Code MCP documentation
5. File issues on the GitHub repository

## Summary

The ShadowGit MCP server transforms your development history into a powerful debugging tool. Once properly configured, it provides seamless integration between your ShadowGit repositories and AI assistants, enabling advanced code analysis and debugging workflows.
```

--------------------------------------------------------------------------------
/tests/handlers/list-repos-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { ListReposHandler } from '../../src/handlers/list-repos-handler';
import { RepositoryManager } from '../../src/core/repository-manager';

// Mock the dependencies
jest.mock('../../src/core/repository-manager');
jest.mock('../../src/utils/logger', () => ({
  log: jest.fn(),
}));

describe('ListReposHandler', () => {
  let handler: ListReposHandler;
  let mockRepositoryManager: jest.Mocked<RepositoryManager>;

  beforeEach(() => {
    jest.clearAllMocks();
    
    mockRepositoryManager = new RepositoryManager() as jest.Mocked<RepositoryManager>;
    handler = new ListReposHandler(mockRepositoryManager);
  });

  describe('handle', () => {
    it('should list repositories when available', async () => {
      const mockRepos = [
        { name: 'project-alpha', path: '/home/user/projects/alpha' },
        { name: 'project-beta', path: '/home/user/projects/beta' },
        { name: 'my-app', path: '/Users/dev/workspace/my-app' },
      ];
      
      (mockRepositoryManager.getRepositories as any) = jest.fn().mockReturnValue(mockRepos);

      const result = await handler.handle();

      expect(result.content[0].text).toContain('Available Repositories (3)');
      expect(result.content[0].text).toContain('project-alpha:\n    Path: /home/user/projects/alpha');
      expect(result.content[0].text).toContain('project-beta:\n    Path: /home/user/projects/beta');
      expect(result.content[0].text).toContain('my-app:\n    Path: /Users/dev/workspace/my-app');
      expect(result.content[0].text).toContain('CRITICAL: Required Workflow for ALL Changes');
      expect(result.content[0].text).toContain('start_session');
    });

    it('should handle no repositories configured', async () => {
      (mockRepositoryManager.getRepositories as any) = jest.fn().mockReturnValue([]);

      const result = await handler.handle();

      expect(result.content[0].text).toContain('No repositories found in ShadowGit');
      expect(result.content[0].text).toContain('To add repositories:');
      expect(result.content[0].text).toContain('Open the ShadowGit application');
      expect(result.content[0].text).not.toContain('Available Repositories');
    });

    it('should handle single repository', async () => {
      const mockRepos = [
        { name: 'solo-project', path: '/workspace/solo' },
      ];
      
      (mockRepositoryManager.getRepositories as any) = jest.fn().mockReturnValue(mockRepos);

      const result = await handler.handle();

      expect(result.content[0].text).toContain('Available Repositories (1)');
      expect(result.content[0].text).toContain('solo-project:\n    Path: /workspace/solo');
      expect(result.content[0].text).toContain('git_command({repo: "solo-project"');
    });

    it('should handle repositories with special characters in names', async () => {
      const mockRepos = [
        { name: 'project-with-dashes', path: '/path/to/project' },
        { name: 'project_with_underscores', path: '/another/path' },
        { name: 'project.with.dots', path: '/dotted/path' },
      ];
      
      (mockRepositoryManager.getRepositories as any) = jest.fn().mockReturnValue(mockRepos);

      const result = await handler.handle();

      expect(result.content[0].text).toContain('project-with-dashes:\n    Path: /path/to/project');
      expect(result.content[0].text).toContain('project_with_underscores:\n    Path: /another/path');
      expect(result.content[0].text).toContain('project.with.dots:\n    Path: /dotted/path');
    });

    it('should handle repositories with long paths', async () => {
      const mockRepos = [
        { 
          name: 'deep-project', 
          path: '/very/long/path/to/deeply/nested/project/directory/structure/here' 
        },
      ];
      
      (mockRepositoryManager.getRepositories as any) = jest.fn().mockReturnValue(mockRepos);

      const result = await handler.handle();

      expect(result.content[0].text).toContain(
        'deep-project:\n    Path: /very/long/path/to/deeply/nested/project/directory/structure/here'
      );
    });

    it('should handle Windows-style paths', async () => {
      const mockRepos = [
        { name: 'windows-project', path: 'C:\\Users\\Developer\\Projects\\MyApp' },
        { name: 'network-project', path: '\\\\server\\share\\project' },
      ];
      
      (mockRepositoryManager.getRepositories as any) = jest.fn().mockReturnValue(mockRepos);

      const result = await handler.handle();

      expect(result.content[0].text).toContain('windows-project:\n    Path: C:\\Users\\Developer\\Projects\\MyApp');
      expect(result.content[0].text).toContain('network-project:\n    Path: \\\\server\\share\\project');
    });

    it('should handle many repositories', async () => {
      const mockRepos = Array.from({ length: 20 }, (_, i) => ({
        name: `project-${i + 1}`,
        path: `/path/to/project${i + 1}`,
      }));
      
      (mockRepositoryManager.getRepositories as any) = jest.fn().mockReturnValue(mockRepos);

      const result = await handler.handle();

      expect(result.content[0].text).toContain('Available Repositories (20)');
      expect(result.content[0].text).toContain('project-1:\n    Path: /path/to/project1');
      expect(result.content[0].text).toContain('project-20:\n    Path: /path/to/project20');
    });

    it('should always return MCPToolResponse with text content', async () => {
      (mockRepositoryManager.getRepositories as any) = jest.fn().mockReturnValue([]);

      const result = await handler.handle();

      expect(result).toHaveProperty('content');
      expect(Array.isArray(result.content)).toBe(true);
      expect(result.content).toHaveLength(1);
      expect(result.content[0]).toHaveProperty('type', 'text');
      expect(result.content[0]).toHaveProperty('text');
      expect(typeof result.content[0].text).toBe('string');
    });

    it('should throw if getRepositories throws', async () => {
      (mockRepositoryManager.getRepositories as any) = jest.fn().mockImplementation(() => {
        throw new Error('Failed to read repositories');
      });

      // Should propagate the error
      await expect(handler.handle()).rejects.toThrow('Failed to read repositories');
    });

    it('should handle null return from getRepositories', async () => {
      (mockRepositoryManager.getRepositories as any) = jest.fn().mockReturnValue(null as any);

      // This will cause an error when trying to check length
      await expect(handler.handle()).rejects.toThrow();
    });

    it('should handle undefined return from getRepositories', async () => {
      (mockRepositoryManager.getRepositories as any) = jest.fn().mockReturnValue(undefined as any);

      // This will cause an error when trying to check length
      await expect(handler.handle()).rejects.toThrow();
    });
  });
});
```

--------------------------------------------------------------------------------
/src/shadowgit-mcp-server.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * ShadowGit MCP Server - Main Entry Point
 * Provides read-only Git access, session management and checkpoint creation for AI assistants
 */

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { 
  CallToolRequestSchema, 
  ListToolsRequestSchema 
} from '@modelcontextprotocol/sdk/types.js';

import { log } from './utils/logger';
import { VERSION } from './utils/constants';
import { RepositoryManager } from './core/repository-manager';
import { GitExecutor } from './core/git-executor';
import { SessionClient } from './core/session-client';
import { GitHandler } from './handlers/git-handler';
import { ListReposHandler } from './handlers/list-repos-handler';
import { CheckpointHandler } from './handlers/checkpoint-handler';
import { SessionHandler } from './handlers/session-handler';

export class ShadowGitMCPServer {
  private server: Server;
  private repositoryManager: RepositoryManager;
  private gitExecutor: GitExecutor;
  private sessionClient: SessionClient;
  private gitHandler: GitHandler;
  private listReposHandler: ListReposHandler;
  private checkpointHandler: CheckpointHandler;
  private sessionHandler: SessionHandler;

  constructor() {
    // Initialize core services
    this.repositoryManager = new RepositoryManager();
    this.gitExecutor = new GitExecutor();
    this.sessionClient = new SessionClient();
    
    // Initialize handlers
    this.gitHandler = new GitHandler(this.repositoryManager, this.gitExecutor);
    this.listReposHandler = new ListReposHandler(this.repositoryManager);
    this.checkpointHandler = new CheckpointHandler(
      this.repositoryManager, 
      this.gitExecutor
    );
    this.sessionHandler = new SessionHandler(
      this.repositoryManager,
      this.sessionClient
    );
    
    // Initialize MCP server
    this.server = new Server(
      {
        name: 'shadowgit-mcp-server',
        version: VERSION,
      },
      {
        capabilities: {
          tools: {},
        },
      }
    );
    
    this.setupHandlers();
  }

  private setupHandlers(): void {
    // List available tools
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        {
          name: 'list_repos',
          description: 'List all available ShadowGit repositories. Use this first to discover which repositories you can work with.',
          inputSchema: {
            type: 'object',
            properties: {},
          },
        },
        {
          name: 'git_command',
          description: 'Execute a read-only git command on a ShadowGit repository. Only safe, read-only commands are allowed.',
          inputSchema: {
            type: 'object',
            properties: {
              repo: {
                type: 'string',
                description: 'Repository name (use list_repos to see available repositories)',
              },
              command: {
                type: 'string',
                description: 'Git command to execute (e.g., "log -10", "diff HEAD~1", "status")',
              },
            },
            required: ['repo', 'command'],
          },
        },
        {
          name: 'start_session',
          description: 'Start a work session. MUST be called BEFORE making any changes. Without this, ShadowGit will create fragmented auto-commits during your work!',
          inputSchema: {
            type: 'object',
            properties: {
              repo: {
                type: 'string',
                description: 'Repository name',
              },
              description: {
                type: 'string',
                description: 'What you plan to do in this session',
              },
            },
            required: ['repo', 'description'],
          },
        },
        {
          name: 'checkpoint',
          description: 'Create a git commit with your changes. Call this AFTER completing your work but BEFORE end_session. Creates a clean commit for the user to review.',
          inputSchema: {
            type: 'object',
            properties: {
              repo: {
                type: 'string',
                description: 'Repository name',
              },
              title: {
                type: 'string',
                description: 'Commit title (max 50 chars) - REQUIRED. Be specific about what was changed.',
              },
              message: {
                type: 'string',
                description: 'Detailed commit message (optional, max 1000 chars)',
              },
              author: {
                type: 'string',
                description: 'Author name (e.g., "Claude", "GPT-4"). Defaults to "AI Assistant"',
              },
            },
            required: ['repo', 'title'],
          },
        },
        {
          name: 'end_session',
          description: 'End your work session to resume ShadowGit auto-commits. MUST be called AFTER checkpoint to properly close your work session.',
          inputSchema: {
            type: 'object',
            properties: {
              sessionId: {
                type: 'string',
                description: 'Session ID from start_session',
              },
              commitHash: {
                type: 'string',
                description: 'Commit hash from checkpoint (optional)',
              },
            },
            required: ['sessionId'],
          },
        }
      ],
    }));

    // Handle tool execution
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;
      
      log('info', `Tool called: ${name}`);
      
      try {
        switch (name) {
          case 'list_repos':
            return await this.listReposHandler.handle();
            
          case 'git_command':
            return await this.gitHandler.handle(args);
            
          case 'start_session':
            return await this.sessionHandler.startSession(args);
            
          case 'checkpoint':
            return await this.checkpointHandler.handle(args);
            
          case 'end_session':
            return await this.sessionHandler.endSession(args);
            
          default:
            return {
              content: [
                {
                  type: 'text',
                  text: `Unknown tool: ${name}. Available tools: list_repos, git_command, start_session, checkpoint, end_session`,
                },
              ],
            };
        }
      } catch (error) {
        log('error', `Tool execution error: ${error}`);
        return {
          content: [
            {
              type: 'text',
              text: `Error executing ${name}: ${error}`,
            },
          ],
        };
      }
    });
  }

  async start(): Promise<void> {
    log('info', `Starting ShadowGit MCP Server v${VERSION}`);
    
    // Check Session API health
    const isSessionApiHealthy = await this.sessionClient.isHealthy();
    if (isSessionApiHealthy) {
      log('info', 'Session API is available - session tracking enabled');
    } else {
      log('warn', 'Session API is not available - proceeding without session tracking');
    }
    
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    
    log('info', 'ShadowGit MCP Server is running');
  }

  shutdown(signal: string): void {
    log('info', `Received ${signal}, shutting down gracefully...`);
    process.exit(0);
  }
}

// Main entry point
async function main(): Promise<void> {
  // Handle CLI arguments
  if (process.argv.includes('--version')) {
    console.log(VERSION);
    process.exit(0);
  }
  
  try {
    const server = new ShadowGitMCPServer();
    
    // Handle shutdown signals
    process.on('SIGINT', () => server.shutdown('SIGINT'));
    process.on('SIGTERM', () => server.shutdown('SIGTERM'));
    
    await server.start();
  } catch (error) {
    log('error', `Failed to start server: ${error}`);
    process.exit(1);
  }
}

// Start the server
main().catch((error) => {
  log('error', `Unhandled error: ${error}`);
  process.exit(1);
});
```

--------------------------------------------------------------------------------
/tests/core/session-client.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, jest, afterEach } from '@jest/globals';
import { SessionClient } from '../../src/core/session-client';

// Mock the global fetch
global.fetch = jest.fn() as jest.MockedFunction<typeof fetch>;

describe('SessionClient', () => {
  let client: SessionClient;
  let mockFetch: jest.MockedFunction<typeof fetch>;

  beforeEach(() => {
    jest.clearAllMocks();
    client = new SessionClient();
    mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('isHealthy', () => {
    it('should return true when API responds successfully', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
      } as Response);

      const result = await client.isHealthy();
      
      expect(result).toBe(true);
      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining('/health'),
        expect.objectContaining({
          signal: expect.any(Object),
        })
      );
    });

    it('should return false when API returns non-200 status', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 404,
      } as Response);

      const result = await client.isHealthy();
      
      expect(result).toBe(false);
    });

    it('should return false when fetch throws an error', async () => {
      mockFetch.mockRejectedValueOnce(new Error('Network error'));

      const result = await client.isHealthy();
      
      expect(result).toBe(false);
    });

    it('should return false when request times out', async () => {
      mockFetch.mockImplementationOnce(() => 
        new Promise((_, reject) => 
          setTimeout(() => reject(new Error('AbortError')), 100)
        )
      );

      const result = await client.isHealthy();
      
      expect(result).toBe(false);
    });
  });

  describe('startSession', () => {
    it('should successfully start a session and return sessionId', async () => {
      const mockSessionId = 'test-session-123';
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: (jest.fn() as any).mockResolvedValue({
          success: true,
          sessionId: mockSessionId,
        }),
      } as unknown as Response);

      const result = await client.startSession({
        repoPath: '/test/repo',
        aiTool: 'Claude',
        description: 'Testing session',
      });
      
      expect(result).toBe(mockSessionId);
      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining('/session/start'),
        expect.objectContaining({
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            repoPath: '/test/repo',
            aiTool: 'Claude',
            description: 'Testing session',
          }),
          signal: expect.any(AbortSignal),
        })
      );
    });

    it('should return null when API returns failure', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: (jest.fn() as any).mockResolvedValue({
          success: false,
          error: 'Repository not found',
        }),
      } as unknown as Response);

      const result = await client.startSession({
        repoPath: '/test/repo',
        aiTool: 'Claude',
        description: 'Testing session',
      });
      
      expect(result).toBeNull();
    });

    it('should return null when API returns non-200 status', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 404,
        json: (jest.fn() as any).mockResolvedValue({
          error: 'Not found',
        }),
      } as unknown as Response);

      const result = await client.startSession({
        repoPath: '/test/repo',
        aiTool: 'Claude',
        description: 'Testing session',
      });
      
      expect(result).toBeNull();
    });

    it('should return null when fetch throws an error', async () => {
      mockFetch.mockRejectedValueOnce(new Error('Network error'));

      const result = await client.startSession({
        repoPath: '/test/repo',
        aiTool: 'Claude',
        description: 'Testing session',
      });
      
      expect(result).toBeNull();
    });

    it('should return null when response is not valid JSON', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: (jest.fn() as any).mockRejectedValue(new Error('Invalid JSON')),
      } as unknown as Response);

      const result = await client.startSession({
        repoPath: '/test/repo',
        aiTool: 'Claude',
        description: 'Testing session',
      });
      
      expect(result).toBeNull();
    });
  });

  describe('endSession', () => {
    it('should successfully end a session', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: (jest.fn() as any).mockResolvedValue({
          success: true,
        }),
      } as unknown as Response);

      const result = await client.endSession('test-session-123', 'abc1234');
      
      expect(result).toBe(true);
      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining('/session/end'),
        expect.objectContaining({
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            sessionId: 'test-session-123',
            commitHash: 'abc1234',
          }),
          signal: expect.any(AbortSignal),
        })
      );
    });

    it('should successfully end a session without commit hash', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: (jest.fn() as any).mockResolvedValue({
          success: true,
        }),
      } as unknown as Response);

      const result = await client.endSession('test-session-123');
      
      expect(result).toBe(true);
      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining('/session/end'),
        expect.objectContaining({
          body: JSON.stringify({
            sessionId: 'test-session-123',
          }),
        })
      );
    });

    it('should return false when API returns failure', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: (jest.fn() as any).mockResolvedValue({
          success: false,
          error: 'Session not found',
        }),
      } as unknown as Response);

      const result = await client.endSession('invalid-session');
      
      expect(result).toBe(false);
    });

    it('should return false when API returns non-200 status', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 404,
      } as unknown as Response);

      const result = await client.endSession('test-session-123');
      
      expect(result).toBe(false);
    });

    it('should return false when fetch throws an error', async () => {
      mockFetch.mockRejectedValueOnce(new Error('Network error'));

      const result = await client.endSession('test-session-123');
      
      expect(result).toBe(false);
    });
  });

  describe('Environment Variables', () => {
    it('should use custom SESSION_API_URL from environment', () => {
      const originalEnv = process.env.SHADOWGIT_SESSION_API;
      process.env.SHADOWGIT_SESSION_API = 'http://custom-api:5000/api';
      
      // Create new client to pick up env var
      const customClient = new SessionClient();
      
      // Reset environment
      if (originalEnv) {
        process.env.SHADOWGIT_SESSION_API = originalEnv;
      } else {
        delete process.env.SHADOWGIT_SESSION_API;
      }
      
      // We can't directly test the URL without exposing it, but we can verify
      // the client was created without errors
      expect(customClient).toBeDefined();
    });
  });

  describe('Timeout Handling', () => {
    it('should timeout health check after 3 seconds', async () => {
      let timeoutCalled = false;
      
      mockFetch.mockImplementationOnce((_, options) => {
        const signal = (options as any).signal;
        
        // Simulate timeout
        return new Promise((_, reject) => {
          signal.addEventListener('abort', () => {
            timeoutCalled = true;
            reject(new Error('AbortError'));
          });
          
          // Wait longer than timeout
          setTimeout(() => {}, 5000);
        });
      });

      const result = await client.isHealthy();
      
      expect(result).toBe(false);
      // The timeout should have been triggered
      expect(mockFetch).toHaveBeenCalled();
    });

    it('should timeout startSession after 3 seconds', async () => {
      mockFetch.mockImplementationOnce(() => 
        new Promise((_, reject) => 
          setTimeout(() => reject(new Error('AbortError')), 5000)
        )
      );

      const result = await client.startSession({
        repoPath: '/test/repo',
        aiTool: 'Claude',
        description: 'Test',
      });
      
      expect(result).toBeNull();
    });

    it('should timeout endSession after 3 seconds', async () => {
      mockFetch.mockImplementationOnce(() => 
        new Promise((_, reject) => 
          setTimeout(() => reject(new Error('AbortError')), 5000)
        )
      );

      const result = await client.endSession('test-session-123');
      
      expect(result).toBe(false);
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/handlers/session-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { SessionHandler } from '../../src/handlers/session-handler';
import { RepositoryManager } from '../../src/core/repository-manager';
import { SessionClient } from '../../src/core/session-client';

// Mock the dependencies
jest.mock('../../src/core/repository-manager');
jest.mock('../../src/core/session-client');
jest.mock('../../src/utils/logger', () => ({
  log: jest.fn(),
}));

describe('SessionHandler', () => {
  let handler: SessionHandler;
  let mockRepositoryManager: jest.Mocked<RepositoryManager>;
  let mockSessionClient: jest.Mocked<SessionClient>;

  beforeEach(() => {
    jest.clearAllMocks();
    jest.resetAllMocks();
    
    mockRepositoryManager = new RepositoryManager() as jest.Mocked<RepositoryManager>;
    mockSessionClient = new SessionClient() as jest.Mocked<SessionClient>;
    
    handler = new SessionHandler(mockRepositoryManager, mockSessionClient);
  });

  describe('startSession', () => {
    it('should successfully start a session with valid arguments', async () => {
      const testSessionId = 'test-session-123';
      (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue('/test/repo');
      (mockSessionClient as any).startSession = (jest.fn() as any).mockResolvedValue(testSessionId);

      const result = await handler.startSession({
        repo: 'test-repo',
        description: 'Testing session',
      });

      expect(result.content[0].text).toContain('Session started successfully');
      expect(result.content[0].text).toContain(testSessionId);
      expect(mockRepositoryManager.resolveRepoPath).toHaveBeenCalledWith('test-repo');
      expect(mockSessionClient.startSession).toHaveBeenCalledWith({
        repoPath: '/test/repo',
        aiTool: 'MCP Client',
        description: 'Testing session',
      });
    });

    it('should return error when repo is missing', async () => {
      const result = await handler.startSession({
        description: 'Testing session',
      });

      expect(result.content[0].text).toContain('Error: Both "repo" and "description" are required');
      expect(mockRepositoryManager.resolveRepoPath).not.toHaveBeenCalled();
      expect(mockSessionClient.startSession).not.toHaveBeenCalled();
    });

    it('should return error when description is missing', async () => {
      const result = await handler.startSession({
        repo: 'test-repo',
      });

      expect(result.content[0].text).toContain('Error: Both "repo" and "description" are required');
      expect(mockRepositoryManager.resolveRepoPath).not.toHaveBeenCalled();
      expect(mockSessionClient.startSession).not.toHaveBeenCalled();
    });

    it('should return error when both parameters are missing', async () => {
      const result = await handler.startSession({});

      expect(result.content[0].text).toContain('Error: Both "repo" and "description" are required');
    });

    it('should return error when null is passed', async () => {
      const result = await handler.startSession(null);

      expect(result.content[0].text).toContain('Error: Both "repo" and "description" are required');
    });

    it('should return error when repository is not found', async () => {
      (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue(null);

      const result = await handler.startSession({
        repo: 'non-existent',
        description: 'Testing session',
      });

      expect(result.content[0].text).toContain("Error: Repository 'non-existent' not found");
      expect(result.content[0].text).toContain('Use list_repos()');
      expect(mockSessionClient.startSession).not.toHaveBeenCalled();
    });

    it('should handle Session API being offline gracefully', async () => {
      (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue('/test/repo');
      (mockSessionClient as any).startSession = (jest.fn() as any).mockResolvedValue(null);

      const result = await handler.startSession({
        repo: 'test-repo',
        description: 'Testing session',
      });

      expect(result.content[0].text).toContain('Session API is offline');
      expect(result.content[0].text).toContain('Proceeding without session tracking');
    });

    it('should include helpful instructions in success message', async () => {
      const testSessionId = 'test-session-456';
      (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue('/test/repo');
      (mockSessionClient as any).startSession = (jest.fn() as any).mockResolvedValue(testSessionId);

      const result = await handler.startSession({
        repo: 'test-repo',
        description: 'Testing session',
      });

      expect(result.content[0].text).toContain('📋 **Your Workflow Checklist:**');
      expect(result.content[0].text).toContain('Session started successfully');
      expect(result.content[0].text).toContain('checkpoint()');
      expect(result.content[0].text).toContain('end_session()');
    });

    it('should handle non-string repo parameter', async () => {
      const result = await handler.startSession({
        repo: 123 as any,
        description: 'Testing session',
      });

      expect(result.content[0].text).toContain('Error: Both "repo" and "description" are required');
    });

    it('should handle non-string description parameter', async () => {
      const result = await handler.startSession({
        repo: 'test-repo',
        description: true as any,
      });

      expect(result.content[0].text).toContain('Error: Both "repo" and "description" are required');
    });
  });

  describe('endSession', () => {
    it('should successfully end a session with sessionId and commitHash', async () => {
      (mockSessionClient as any).endSession = (jest.fn() as any).mockResolvedValue(true);

      const result = await handler.endSession({
        sessionId: 'test-session-123',
        commitHash: 'abc1234',
      });

      expect(result.content[0].text).toContain('Session test-session-123 ended successfully');
      expect(mockSessionClient.endSession).toHaveBeenCalledWith('test-session-123', 'abc1234');
    });

    it('should successfully end a session with only sessionId', async () => {
      // Create a fresh mock to avoid pollution from previous tests
      const freshMockClient = new SessionClient() as jest.Mocked<SessionClient>;
      (freshMockClient as any).endSession = (jest.fn() as any).mockResolvedValue(true);
      const freshHandler = new SessionHandler(mockRepositoryManager, freshMockClient);

      const result = await freshHandler.endSession({
        sessionId: 'test-session-456',
      });

      expect(result.content[0].text).toContain('Session test-session-456 ended successfully');
      expect(freshMockClient.endSession).toHaveBeenCalledWith('test-session-456', undefined);
    });

    it('should return error when sessionId is missing', async () => {
      const result = await handler.endSession({
        commitHash: 'abc1234',
      });

      expect(result.content[0].text).toContain('Error: "sessionId" is required');
      expect(mockSessionClient.endSession).not.toHaveBeenCalled();
    });

    it('should return error when arguments are missing', async () => {
      const result = await handler.endSession({});

      expect(result.content[0].text).toContain('Error: "sessionId" is required');
    });

    it('should return error when null is passed', async () => {
      const result = await handler.endSession(null);

      expect(result.content[0].text).toContain('Error: "sessionId" is required');
    });

    it('should handle session not found or already ended', async () => {
      (mockSessionClient as any).endSession = (jest.fn() as any).mockResolvedValue(false);

      const result = await handler.endSession({
        sessionId: 'invalid-session',
      });

      expect(result.content[0].text).toContain('Failed to End Session');
      expect(result.content[0].text).toContain('may have already ended or expired');
    });

    it('should handle non-string sessionId parameter', async () => {
      const result = await handler.endSession({
        sessionId: 123 as any,
      });

      expect(result.content[0].text).toContain('Error: "sessionId" is required');
    });

    it('should handle Session API error gracefully', async () => {
      // SessionClient.endSession returns false on error, not a rejected promise
      const errorMockClient = new SessionClient() as jest.Mocked<SessionClient>;
      (errorMockClient as any).endSession = (jest.fn() as any).mockResolvedValue(false);
      const errorHandler = new SessionHandler(mockRepositoryManager, errorMockClient);

      const result = await errorHandler.endSession({
        sessionId: 'test-session-789',
      });

      // Should handle the error and return false
      expect(result.content[0].text).toContain('Failed to End Session');
      expect(result.content[0].text).toContain('may have already ended or expired');
    });

    it('should pass optional commitHash to SessionClient', async () => {
      (mockSessionClient as any).endSession = (jest.fn() as any).mockResolvedValue(true);

      await handler.endSession({
        sessionId: 'test-session-999',
        commitHash: 'def5678',
      });

      expect(mockSessionClient.endSession).toHaveBeenCalledWith('test-session-999', 'def5678');
    });

    it('should not pass commitHash when not provided', async () => {
      (mockSessionClient as any).endSession = (jest.fn() as any).mockResolvedValue(true);

      await handler.endSession({
        sessionId: 'test-session-888',
      });

      expect(mockSessionClient.endSession).toHaveBeenCalledWith('test-session-888', undefined);
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/handlers/git-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { GitHandler } from '../../src/handlers/git-handler';
import { RepositoryManager } from '../../src/core/repository-manager';
import { GitExecutor } from '../../src/core/git-executor';

// Mock the dependencies
jest.mock('../../src/core/repository-manager');
jest.mock('../../src/core/git-executor');
jest.mock('../../src/utils/logger', () => ({
  log: jest.fn(),
}));
jest.mock('fs', () => ({
  existsSync: jest.fn(),
}));

import * as fs from 'fs';

describe('GitHandler', () => {
  let handler: GitHandler;
  let mockRepositoryManager: jest.Mocked<RepositoryManager>;
  let mockGitExecutor: jest.Mocked<GitExecutor>;
  let mockExistsSync: jest.MockedFunction<typeof fs.existsSync>;

  beforeEach(() => {
    jest.clearAllMocks();
    
    mockRepositoryManager = new RepositoryManager() as jest.Mocked<RepositoryManager>;
    mockGitExecutor = new GitExecutor() as jest.Mocked<GitExecutor>;
    mockExistsSync = fs.existsSync as jest.MockedFunction<typeof fs.existsSync>;
    
    handler = new GitHandler(mockRepositoryManager, mockGitExecutor);
  });

  describe('handle', () => {
    describe('Validation', () => {
      it('should require both repo and command parameters', async () => {
        // Missing repo
        let result = await handler.handle({ command: 'log' });
        expect(result.content[0].text).toContain("Error: Both 'repo' and 'command' parameters are required");
        
        // Missing command
        result = await handler.handle({ repo: 'test-repo' });
        expect(result.content[0].text).toContain("Error: Both 'repo' and 'command' parameters are required");
        
        // Missing both
        result = await handler.handle({});
        expect(result.content[0].text).toContain("Error: Both 'repo' and 'command' parameters are required");
        
        // Null
        result = await handler.handle(null);
        expect(result.content[0].text).toContain("Error: Both 'repo' and 'command' parameters are required");
      });

      it('should handle non-string repo parameter', async () => {
        const result = await handler.handle({
          repo: 123 as any,
          command: 'log',
        });
        
        expect(result.content[0].text).toContain("Error: Both 'repo' and 'command' parameters are required");
      });

      it('should handle non-string command parameter', async () => {
        const result = await handler.handle({
          repo: 'test-repo',
          command: true as any,
        });
        
        expect(result.content[0].text).toContain("Error: Both 'repo' and 'command' parameters are required");
      });
    });

    describe('Repository Resolution', () => {
      it('should handle repository not found', async () => {
        (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue(null);
        (mockRepositoryManager as any).getRepositories = jest.fn().mockReturnValue([
          { name: 'repo1', path: '/path/to/repo1' },
          { name: 'repo2', path: '/path/to/repo2' },
        ]);

        const result = await handler.handle({
          repo: 'non-existent',
          command: 'log',
        });
        
        expect(result.content[0].text).toContain("Error: Repository 'non-existent' not found");
        expect(result.content[0].text).toContain('Available repositories:');
        expect(result.content[0].text).toContain('repo1:');
        expect(result.content[0].text).toContain('Path: /path/to/repo1');
        expect(result.content[0].text).toContain('repo2:');
        expect(result.content[0].text).toContain('Path: /path/to/repo2');
      });

      it('should handle no repositories configured', async () => {
        (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue(null);
        (mockRepositoryManager as any).getRepositories = jest.fn().mockReturnValue([]);

        const result = await handler.handle({
          repo: 'test-repo',
          command: 'log',
        });
        
        expect(result.content[0].text).toContain('No repositories found');
        expect(result.content[0].text).toContain('Please add repositories to ShadowGit first');
      });
    });

    describe('ShadowGit Directory Check', () => {
      beforeEach(() => {
        (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue('/test/repo');
      });

      it('should handle when GitExecutor returns error for missing .shadowgit.git', async () => {
        mockExistsSync.mockReturnValue(false);
        (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue('Error: No ShadowGit repository found at /test/repo');

        const result = await handler.handle({
          repo: 'test-repo',
          command: 'log',
        });
        
        expect(result).toBeDefined();
        expect(result.content).toBeDefined();
        expect(result.content[0]).toBeDefined();
        expect(result.content[0].text).toContain('Error');
        expect(mockGitExecutor.execute).toHaveBeenCalledWith('log', '/test/repo');
      });

      it('should proceed when .shadowgit.git directory exists', async () => {
        mockExistsSync.mockReturnValue(true);
        (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue('commit abc1234\nAuthor: Test');

        const result = await handler.handle({
          repo: 'test-repo',
          command: 'log -1',
        });
        
        expect(result).toBeDefined();
        expect(result.content).toBeDefined();
        expect(result.content[0]).toBeDefined();
        expect(result.content[0].text).toContain('commit abc1234');
        expect(mockGitExecutor.execute).toHaveBeenCalledWith('log -1', '/test/repo');
      });
    });

    describe('Git Command Execution', () => {
      beforeEach(() => {
        (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue('/test/repo');
        mockExistsSync.mockReturnValue(true);
      });

      it('should execute valid git commands', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue('commit abc1234\ncommit def5678');

        const result = await handler.handle({
          repo: 'test-repo',
          command: 'log --oneline -2',
        });
        
        expect(result.content[0].text).toContain('commit abc1234');
        expect(result.content[0].text).toContain('commit def5678');
        expect(mockGitExecutor.execute).toHaveBeenCalledWith(
          'log --oneline -2',
          '/test/repo'
        );
      });

      it('should handle empty output', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue('');

        const result = await handler.handle({
          repo: 'test-repo',
          command: 'status',
        });
        
        // Now includes workflow reminder for status command
        expect(result.content[0].text).toContain('Planning to Make Changes?');
      });

      it('should trim whitespace from output', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue('  \n  output with spaces  \n  ');

        const result = await handler.handle({
          repo: 'test-repo',
          command: 'log',
        });
        
        // Now includes workflow reminder for log command
        expect(result.content[0].text).toContain('output with spaces');
        expect(result.content[0].text).toContain('Planning to Make Changes?');
      });

      it('should handle error output from GitExecutor', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue('Error: Command not allowed');

        const result = await handler.handle({
          repo: 'test-repo',
          command: 'commit -m "test"',
        });
        
        expect(result.content[0].text).toContain('Error: Command not allowed');
      });

      it('should handle various git commands', async () => {
        const commands = [
          { cmd: 'log -10', output: 'log output', hasReminder: true },
          { cmd: 'diff HEAD~1', output: 'diff output', hasReminder: true },
          { cmd: 'show abc123', output: 'show output', hasReminder: false },
          { cmd: 'blame file.txt', output: 'blame output', hasReminder: true },
          { cmd: 'status', output: 'status output', hasReminder: true },
          { cmd: 'branch --list', output: 'branch output', hasReminder: false },
        ];

        for (const { cmd, output, hasReminder } of commands) {
          jest.clearAllMocks();
          (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue(output);

          const result = await handler.handle({
            repo: 'test-repo',
            command: cmd,
          });

          expect(result.content[0].text).toContain(output);
          if (hasReminder) {
            expect(result.content[0].text).toContain('Planning to Make Changes?');
          } else {
            expect(result.content[0].text).toBe(output);
          }
          expect(mockGitExecutor.execute).toHaveBeenCalledWith(cmd, '/test/repo');
        }
      });

      it('should pass correct parameters for regular commands', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue('output');

        const result = await handler.handle({
          repo: 'test-repo',
          command: 'log',
        });

        expect(mockGitExecutor.execute).toHaveBeenCalledWith(
          'log',
          '/test/repo'
        );
        // Verify workflow reminder is included
        expect(result.content[0].text).toContain('Planning to Make Changes?');
      });

      it('should handle multi-line output correctly', async () => {
        const multiLineOutput = `commit abc1234
Author: Test User
Date: Mon Jan 1 2024

    First commit
    
commit def5678
Author: Another User
Date: Mon Jan 2 2024

    Second commit`;

        (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue(multiLineOutput);

        const result = await handler.handle({
          repo: 'test-repo',
          command: 'log -2',
        });
        
        // Now includes workflow reminder for log command
        expect(result.content[0].text).toContain(multiLineOutput);
        expect(result.content[0].text).toContain('Planning to Make Changes?');
      });

      it('should handle special characters in output', async () => {
        const specialOutput = 'Output with $pecial "chars" `backticks` & symbols';
        (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue(specialOutput);

        const result = await handler.handle({
          repo: 'test-repo',
          command: 'show',
        });
        
        expect(result.content[0].text).toBe(specialOutput);
      });

      it('should handle error responses from GitExecutor', async () => {
        // GitExecutor returns error messages as strings, not rejected promises
        (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue('Error: Execution failed');

        const result = await handler.handle({
          repo: 'test-repo',
          command: 'log',
        });
        
        // The error message is returned as-is
        expect(result.content[0].text).toContain('Error: Execution failed');
      });
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/core/repository-manager.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { RepositoryManager } from '../../src/core/repository-manager';
import * as os from 'os';
import * as path from 'path';
import { getStorageLocation, fileExists, readJsonFile } from '../../src/utils/file-utils';

// Mock the dependencies
jest.mock('os');
jest.mock('../../src/utils/logger', () => ({
  log: jest.fn(),
}));
jest.mock('../../src/utils/file-utils', () => ({
  getStorageLocation: jest.fn(),
  fileExists: jest.fn(),
  readJsonFile: jest.fn(),
}));

describe('RepositoryManager', () => {
  let manager: RepositoryManager;
  let mockGetStorageLocation: jest.MockedFunction<typeof getStorageLocation>;
  let mockFileExists: jest.MockedFunction<typeof fileExists>;
  let mockReadJsonFile: jest.MockedFunction<typeof readJsonFile>;
  let mockHomedir: jest.MockedFunction<typeof os.homedir>;

  beforeEach(() => {
    jest.clearAllMocks();
    
    mockGetStorageLocation = getStorageLocation as jest.MockedFunction<typeof getStorageLocation>;
    mockFileExists = fileExists as jest.MockedFunction<typeof fileExists>;
    mockReadJsonFile = readJsonFile as jest.MockedFunction<typeof readJsonFile>;
    mockHomedir = os.homedir as jest.MockedFunction<typeof os.homedir>;
    
    // Default mock behaviors
    mockHomedir.mockReturnValue('/home/testuser');
    mockGetStorageLocation.mockReturnValue('/home/testuser/.shadowgit');
    mockFileExists.mockReturnValue(true);
    mockReadJsonFile.mockReturnValue([
      { name: 'test-repo', path: '/test/repo' },
      { name: 'another-repo', path: '/another/repo' },
    ]);
    
    manager = new RepositoryManager();
  });

  describe('getRepositories', () => {
    it('should load repositories from config file', () => {
      const repos = manager.getRepositories();
      
      expect(repos).toHaveLength(2);
      expect(repos[0]).toEqual({ name: 'test-repo', path: '/test/repo' });
      expect(repos[1]).toEqual({ name: 'another-repo', path: '/another/repo' });
      expect(mockReadJsonFile).toHaveBeenCalledWith(
        path.join('/home/testuser/.shadowgit', 'repos.json'),
        []
      );
    });

    it('should return empty array when config file does not exist', () => {
      mockReadJsonFile.mockReturnValue([]);
      manager = new RepositoryManager();
      
      const repos = manager.getRepositories();
      
      expect(repos).toEqual([]);
    });

    it('should return empty array when config file is empty', () => {
      mockReadJsonFile.mockReturnValue([]);
      manager = new RepositoryManager();
      
      const repos = manager.getRepositories();
      
      expect(repos).toEqual([]);
    });

    it('should return empty array when config file contains invalid JSON', () => {
      // readJsonFile handles invalid JSON and returns default value
      mockReadJsonFile.mockReturnValue([]);
      manager = new RepositoryManager();
      
      const repos = manager.getRepositories();
      
      expect(repos).toEqual([]);
    });

    it('should handle config file with empty array', () => {
      mockReadJsonFile.mockReturnValue([]);
      manager = new RepositoryManager();
      
      const repos = manager.getRepositories();
      
      expect(repos).toEqual([]);
    });

    it('should cache repositories after first load', () => {
      const repos1 = manager.getRepositories();
      const repos2 = manager.getRepositories();
      
      expect(repos1).toBe(repos2); // Same reference
      expect(mockReadJsonFile).toHaveBeenCalledTimes(1); // Only called once
    });

    it('should use getStorageLocation from file-utils', () => {
      mockGetStorageLocation.mockReturnValue('/custom/shadowgit');
      mockReadJsonFile.mockReturnValue([]);
      
      const customManager = new RepositoryManager();
      customManager.getRepositories();
      
      expect(mockReadJsonFile).toHaveBeenCalledWith(
        path.join('/custom/shadowgit', 'repos.json'),
        []
      );
    });

    it('should handle repositories with Windows paths', () => {
      mockReadJsonFile.mockReturnValue([
        { name: 'windows-project', path: 'C:\\Users\\Dev\\Project' },
        { name: 'network-project', path: '\\\\server\\share\\repo' },
      ]);
      manager = new RepositoryManager();
      
      const repos = manager.getRepositories();
      
      expect(repos[0].path).toBe('C:\\Users\\Dev\\Project');
      expect(repos[1].path).toBe('\\\\server\\share\\repo');
    });

    it('should handle malformed repository objects', () => {
      mockReadJsonFile.mockReturnValue([
        { name: 'valid-repo', path: '/valid/path' },
        { name: 'missing-path' }, // Missing path
        { path: '/missing/name' }, // Missing name
        null, // Null entry
        'string-entry', // String instead of object
        { name: 'another-valid', path: '/another/valid' },
      ] as any);
      manager = new RepositoryManager();
      
      const repos = manager.getRepositories();
      
      // The implementation doesn't filter out invalid entries
      expect(repos).toHaveLength(6);
      expect(repos[0]).toEqual({ name: 'valid-repo', path: '/valid/path' });
      expect(repos[5]).toEqual({ name: 'another-valid', path: '/another/valid' });
    });
  });

  describe('resolveRepoPath', () => {
    beforeEach(() => {
      mockReadJsonFile.mockReturnValue([
        { name: 'test-repo', path: '/test/repo' },
        { name: 'another-repo', path: '/another/repo' },
        { name: 'home-repo', path: '~/projects/home' },
      ]);
      manager = new RepositoryManager();
    });

    it('should resolve repository by exact name', () => {
      mockFileExists.mockImplementation((p: any) => p === path.join('/test/repo', '.shadowgit.git'));
      const resolvedPath = manager.resolveRepoPath('test-repo');
      expect(resolvedPath).toBe('/test/repo');
    });

    it('should resolve repository by another name', () => {
      mockFileExists.mockImplementation((p: any) => p === path.join('/another/repo', '.shadowgit.git'));
      const resolvedPath = manager.resolveRepoPath('another-repo');
      expect(resolvedPath).toBe('/another/repo');
    });

    it('should return null for non-existent repository name', () => {
      const resolvedPath = manager.resolveRepoPath('non-existent');
      expect(resolvedPath).toBeNull();
    });

    it('should resolve absolute path directly if it exists with .shadowgit.git', () => {
      mockFileExists.mockImplementation((p: any) => 
        p === '/direct/path' || p === path.join('/direct/path', '.shadowgit.git')
      );
      
      const resolvedPath = manager.resolveRepoPath('/direct/path');
      expect(resolvedPath).toBe('/direct/path');
    });

    it('should return null for non-existent absolute path', () => {
      mockFileExists.mockReturnValue(false);
      
      const resolvedPath = manager.resolveRepoPath('/non/existent/path');
      expect(resolvedPath).toBeNull();
    });

    it('should resolve repository with tilde path', () => {
      mockHomedir.mockReturnValue('/home/testuser');
      // The repository path contains ~/projects/home which needs to be resolved to /home/testuser/projects/home
      // resolveRepoPath will check for .shadowgit.git in the resolved path
      mockFileExists.mockImplementation((p: any) => {
        // When checking if .shadowgit.git exists in the resolved path
        const resolvedPath = p.replace('~', '/home/testuser');
        return resolvedPath === path.join('/home/testuser/projects/home', '.shadowgit.git');
      });
      
      const resolvedPath = manager.resolveRepoPath('home-repo');
      expect(resolvedPath).toBe('/home/testuser/projects/home');
    });

    it('should handle empty input', () => {
      const resolvedPath = manager.resolveRepoPath('');
      expect(resolvedPath).toBeNull();
    });

    it('should handle null input', () => {
      const resolvedPath = manager.resolveRepoPath(null as any);
      expect(resolvedPath).toBeNull();
    });

    it('should handle undefined input', () => {
      const resolvedPath = manager.resolveRepoPath(undefined as any);
      expect(resolvedPath).toBeNull();
    });

    it('should be case-sensitive for repository names', () => {
      mockFileExists.mockImplementation((p: any) => p === path.join('/test/repo', '.shadowgit.git'));
      const path1 = manager.resolveRepoPath('test-repo');
      const path2 = manager.resolveRepoPath('Test-Repo');
      const path3 = manager.resolveRepoPath('TEST-REPO');
      
      expect(path1).toBe('/test/repo');
      expect(path2).toBeNull();
      expect(path3).toBeNull();
    });

    it('should handle Windows absolute paths', () => {
      mockFileExists.mockImplementation((p: any) => {
        // path.isAbsolute on Windows will recognize C:\ paths
        return p === 'C:\\Windows\\Path';
      });
      
      const resolvedPath = manager.resolveRepoPath('C:\\Windows\\Path');
      // On non-Windows systems, path.isAbsolute may not recognize C:\ as absolute
      // So this test may return null on Unix systems
      if (process.platform === 'win32') {
        expect(resolvedPath).toBe('C:\\Windows\\Path');
      } else {
        // On Unix, C:\ is not recognized as an absolute path
        expect(resolvedPath).toBeNull();
      }
    });

    it('should handle UNC paths', () => {
      mockFileExists.mockImplementation((p: any) => p === '\\\\server\\share');
      
      const resolvedPath = manager.resolveRepoPath('\\\\server\\share');
      // UNC paths are Windows-specific
      if (process.platform === 'win32') {
        expect(resolvedPath).toBe('\\\\server\\share');
      } else {
        // On Unix, \\\\ is not recognized as a path
        expect(resolvedPath).toBeNull();
      }
    });

    it('should handle relative paths as repository names', () => {
      // Relative paths should be treated as repo names, not paths
      const resolvedPath = manager.resolveRepoPath('./relative/path');
      expect(resolvedPath).toBeNull();
    });

    it('should handle repository names with special characters', () => {
      mockReadJsonFile.mockReturnValue([
        { name: 'repo-with-dash', path: '/dash/repo' },
        { name: 'repo_with_underscore', path: '/underscore/repo' },
        { name: 'repo.with.dots', path: '/dots/repo' },
      ]);
      mockFileExists.mockImplementation((p: any) => 
        p === path.join('/dash/repo', '.shadowgit.git') ||
        p === path.join('/underscore/repo', '.shadowgit.git') ||
        p === path.join('/dots/repo', '.shadowgit.git')
      );
      manager = new RepositoryManager();
      
      expect(manager.resolveRepoPath('repo-with-dash')).toBe('/dash/repo');
      expect(manager.resolveRepoPath('repo_with_underscore')).toBe('/underscore/repo');
      expect(manager.resolveRepoPath('repo.with.dots')).toBe('/dots/repo');
    });

    it('should check if path is absolute using path.isAbsolute', () => {
      // Mock a path that path.isAbsolute would recognize
      const unixPath = '/absolute/unix/path';
      mockFileExists.mockImplementation((p: any) => 
        p === unixPath || p === path.join(unixPath, '.shadowgit.git')
      );
      
      const resolvedPath = manager.resolveRepoPath(unixPath);
      
      if (path.isAbsolute(unixPath)) {
        expect(resolvedPath).toBe(unixPath);
      } else {
        expect(resolvedPath).toBeNull();
      }
    });

    it('should normalize tilde in repository paths during resolution', () => {
      mockHomedir.mockReturnValue('/home/user');
      mockReadJsonFile.mockReturnValue([
        { name: 'tilde-repo', path: '~/my/project' },
      ]);
      // Now the implementation expands tilde before checking fileExists
      mockFileExists.mockImplementation((p: any) => p === path.join('/home/user/my/project', '.shadowgit.git'));
      manager = new RepositoryManager();
      
      const resolvedPath = manager.resolveRepoPath('tilde-repo');
      expect(resolvedPath).toBe('/home/user/my/project');
    });

    it('should handle tilde at different positions', () => {
      mockHomedir.mockReturnValue('/home/user');
      mockReadJsonFile.mockReturnValue([
        { name: 'repo1', path: '~/project' },
        { name: 'repo2', path: '/path/~/invalid' }, // Tilde not at start
      ]);
      mockFileExists.mockImplementation((p: any) => 
        p === path.join('/home/user/project', '.shadowgit.git') ||
        p === path.join('/path/~/invalid', '.shadowgit.git')
      );
      manager = new RepositoryManager();
      
      expect(manager.resolveRepoPath('repo1')).toBe('/home/user/project');
      expect(manager.resolveRepoPath('repo2')).toBe('/path/~/invalid'); // Not expanded since tilde is not at start
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/handlers/checkpoint-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { CheckpointHandler } from '../../src/handlers/checkpoint-handler';
import { RepositoryManager } from '../../src/core/repository-manager';
import { GitExecutor } from '../../src/core/git-executor';

// Mock the dependencies
jest.mock('../../src/core/repository-manager');
jest.mock('../../src/core/git-executor');
jest.mock('../../src/utils/logger', () => ({
  log: jest.fn(),
}));

describe('CheckpointHandler', () => {
  let handler: CheckpointHandler;
  let mockRepositoryManager: jest.Mocked<RepositoryManager>;
  let mockGitExecutor: jest.Mocked<GitExecutor>;

  beforeEach(() => {
    jest.clearAllMocks();
    
    mockRepositoryManager = new RepositoryManager() as jest.Mocked<RepositoryManager>;
    mockGitExecutor = new GitExecutor() as jest.Mocked<GitExecutor>;
    
    handler = new CheckpointHandler(mockRepositoryManager, mockGitExecutor);
  });

  describe('handle', () => {
    describe('Validation', () => {
      it('should require both repo and title parameters', async () => {
        // Missing repo
        let result = await handler.handle({ title: 'Test' });
        expect(result.content[0].text).toContain("Error: Both 'repo' and 'title' parameters are required");
        
        // Missing title
        result = await handler.handle({ repo: 'test-repo' });
        expect(result.content[0].text).toContain("Error: Both 'repo' and 'title' parameters are required");
        
        // Missing both
        result = await handler.handle({});
        expect(result.content[0].text).toContain("Error: Both 'repo' and 'title' parameters are required");
        
        // Null
        result = await handler.handle(null);
        expect(result.content[0].text).toContain("Error: Both 'repo' and 'title' parameters are required");
      });

      it('should validate title length (max 50 chars)', async () => {
        const longTitle = 'a'.repeat(51);
        const result = await handler.handle({
          repo: 'test-repo',
          title: longTitle,
        });
        
        expect(result.content[0].text).toContain('Error: Title must be 50 characters or less');
        expect(result.content[0].text).toContain('(current: 51 chars)');
      });

      it('should validate message length (max 1000 chars)', async () => {
        const longMessage = 'a'.repeat(1001);
        const result = await handler.handle({
          repo: 'test-repo',
          title: 'Test checkpoint',
          message: longMessage,
        });
        
        expect(result.content[0].text).toContain('Error: Message must be 1000 characters or less');
        expect(result.content[0].text).toContain('(current: 1001 chars)');
      });

      it('should handle non-string repo parameter', async () => {
        const result = await handler.handle({
          repo: 123 as any,
          title: 'Test',
        });
        
        expect(result.content[0].text).toContain("Error: Both 'repo' and 'title' parameters are required");
      });

      it('should handle non-string title parameter', async () => {
        const result = await handler.handle({
          repo: 'test-repo',
          title: true as any,
        });
        
        expect(result.content[0].text).toContain("Error: Both 'repo' and 'title' parameters are required");
      });
    });

    describe('Repository Resolution', () => {
      it('should handle repository not found', async () => {
        (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue(null);
        (mockRepositoryManager as any).getRepositories = jest.fn().mockReturnValue([
          { name: 'repo1', path: '/path/to/repo1' },
          { name: 'repo2', path: '/path/to/repo2' },
        ]);

        const result = await handler.handle({
          repo: 'non-existent',
          title: 'Test checkpoint',
        });
        
        expect(result.content[0].text).toContain("Error: Repository 'non-existent' not found");
        expect(result.content[0].text).toContain('Available repositories:');
        expect(result.content[0].text).toContain('repo1: /path/to/repo1');
        expect(result.content[0].text).toContain('repo2: /path/to/repo2');
      });

      it('should handle no repositories configured', async () => {
        (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue(null);
        (mockRepositoryManager as any).getRepositories = jest.fn().mockReturnValue([]);

        const result = await handler.handle({
          repo: 'test-repo',
          title: 'Test checkpoint',
        });
        
        expect(result.content[0].text).toContain('Error: No repositories found');
        expect(result.content[0].text).toContain('Please add repositories to ShadowGit first');
      });
    });

    describe('Git Operations', () => {
      beforeEach(() => {
        (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue('/test/repo');
      });

      it('should handle no changes to commit', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any)
          .mockResolvedValueOnce(''); // status --porcelain returns empty

        const result = await handler.handle({
          repo: 'test-repo',
          title: 'Test checkpoint',
        });
        
        expect(result.content[0].text).toContain('No Changes Detected');
        expect(result.content[0].text).toContain('Repository has no changes to commit');
        expect(mockGitExecutor.execute).toHaveBeenCalledWith(
          ['status', '--porcelain'],
          '/test/repo',
          true
        );
      });

      it('should handle empty output from status', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any)
          .mockResolvedValueOnce('(empty output)');

        const result = await handler.handle({
          repo: 'test-repo',
          title: 'Test checkpoint',
        });
        
        expect(result.content[0].text).toContain('No Changes Detected');
      });

      it('should create checkpoint with minimal parameters', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any)
          .mockResolvedValueOnce('M file.txt\nA new.txt') // status --porcelain
          .mockResolvedValueOnce('') // add -A
          .mockResolvedValueOnce('[main abc1234] Test checkpoint\n2 files changed') // commit
          .mockResolvedValueOnce('commit abc1234\nAuthor: AI Assistant'); // show --stat

        const result = await handler.handle({
          repo: 'test-repo',
          title: 'Test checkpoint',
        });
        
        expect(result.content[0].text).toContain('Checkpoint Created Successfully!');
        expect(result.content[0].text).toContain('[main abc1234] Test checkpoint');
        expect(result.content[0].text).toContain('Commit Hash:** `abc1234`');
        expect(mockGitExecutor.execute).toHaveBeenCalledTimes(4);
      });

      it('should create checkpoint with all parameters', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any)
          .mockResolvedValueOnce('M file.txt') // status --porcelain
          .mockResolvedValueOnce('') // add -A
          .mockResolvedValueOnce('[main def5678] Fix bug') // commit
          .mockResolvedValueOnce('commit def5678\nAuthor: Claude'); // show --stat

        const result = await handler.handle({
          repo: 'test-repo',
          title: 'Fix bug',
          message: 'Fixed null pointer exception',
          author: 'Claude',
        });
        
        expect(result.content[0].text).toContain('Checkpoint Created Successfully!');
        expect(result.content[0].text).toContain('Commit Hash:** `def5678`');
      });

      it('should properly escape special characters in commit message', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any)
          .mockResolvedValueOnce('M file.txt') // status
          .mockResolvedValueOnce('') // add
          .mockResolvedValueOnce('[main xyz789] Escaped') // commit
          .mockResolvedValueOnce('commit xyz789'); // show

        await handler.handle({
          repo: 'test-repo',
          title: 'Test with $pecial "quotes" and `backticks`',
          message: 'Message with $vars and `commands`',
          author: 'Test',
        });

        // Check that commit was called with array args
        const commitCall = mockGitExecutor.execute.mock.calls.find(
          call => Array.isArray(call[0]) && call[0][0] === 'commit'
        );
        expect(commitCall).toBeDefined();
        // Message is passed as a separate argument
        expect(commitCall![0]).toEqual(['commit', '-m', expect.any(String)]);
        const message = commitCall![0][2];
        // Special characters should be preserved
        expect(message).toContain('$pecial');
        expect(message).toContain('"quotes"');
        expect(message).toContain('`backticks`');
        expect(message).toContain('`commands`');
      });

      it('should set correct Git author environment', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any)
          .mockResolvedValueOnce('M file.txt')
          .mockResolvedValueOnce('')
          .mockResolvedValueOnce('[main abc1234] Test')
          .mockResolvedValueOnce('commit abc1234');

        await handler.handle({
          repo: 'test-repo',
          title: 'Test',
          author: 'GPT-4',
        });

        // Check the commit call
        const commitCall = mockGitExecutor.execute.mock.calls.find(
          call => Array.isArray(call[0]) && call[0][0] === 'commit'
        );
        expect(commitCall).toBeDefined();
        expect(commitCall![3]).toMatchObject({
          GIT_AUTHOR_NAME: 'GPT-4',
          GIT_AUTHOR_EMAIL: '[email protected]',
          GIT_COMMITTER_NAME: 'ShadowGit MCP',
          GIT_COMMITTER_EMAIL: '[email protected]',
        });
      });

      it('should use default author when not specified', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any)
          .mockResolvedValueOnce('M file.txt')
          .mockResolvedValueOnce('')
          .mockResolvedValueOnce('[main abc1234] Test')
          .mockResolvedValueOnce('commit abc1234');

        await handler.handle({
          repo: 'test-repo',
          title: 'Test',
        });

        const commitCall = mockGitExecutor.execute.mock.calls.find(
          call => Array.isArray(call[0]) && call[0][0] === 'commit'
        );
        expect(commitCall![3]).toMatchObject({
          GIT_AUTHOR_NAME: 'AI Assistant',
          GIT_AUTHOR_EMAIL: '[email protected]',
        });
      });

      it('should handle git add failure', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any)
          .mockResolvedValueOnce('M file.txt') // status
          .mockResolvedValueOnce('Error: Failed to add files'); // add fails

        const result = await handler.handle({
          repo: 'test-repo',
          title: 'Test checkpoint',
        });
        
        expect(result.content[0].text).toContain('Failed to Stage Changes');
        expect(result.content[0].text).toContain('Error: Failed to add files');
      });

      it('should handle git commit failure', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any)
          .mockResolvedValueOnce('M file.txt') // status
          .mockResolvedValueOnce('') // add
          .mockResolvedValueOnce('Error: Cannot commit'); // commit fails

        const result = await handler.handle({
          repo: 'test-repo',
          title: 'Test checkpoint',
        });
        
        expect(result.content[0].text).toContain('Failed to Create Commit');
        expect(result.content[0].text).toContain('Error: Cannot commit');
      });

      it('should handle commit output without hash', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any)
          .mockResolvedValueOnce('M file.txt')
          .mockResolvedValueOnce('')
          .mockResolvedValueOnce('Commit created successfully') // No hash in output
          .mockResolvedValueOnce('commit details');

        const result = await handler.handle({
          repo: 'test-repo',
          title: 'Test checkpoint',
        });
        
        expect(result.content[0].text).toContain('Checkpoint Created Successfully!');
        expect(result.content[0].text).toContain('Commit Hash:** `unknown`');
      });

      it('should extract commit hash from various formats', async () => {
        const hashFormats = [
          '[main abc1234] Message',
          '[feature-branch def5678] Message',
          '[develop 1a2b3c4d5e6f] Message',
        ];

        for (const format of hashFormats) {
          jest.clearAllMocks();
          (mockGitExecutor as any).execute = (jest.fn() as any)
            .mockResolvedValueOnce('M file.txt')
            .mockResolvedValueOnce('')
            .mockResolvedValueOnce(format)
            .mockResolvedValueOnce('details');

          const result = await handler.handle({
            repo: 'test-repo',
            title: 'Test',
          });

          const match = format.match(/\[[\w-]+ ([a-f0-9]+)\]/);
          expect(result.content[0].text).toContain(`Commit Hash:** \`${match![1]}\``);
        }
      });

      it('should include commit message body when provided', async () => {
        (mockGitExecutor as any).execute = (jest.fn() as any)
          .mockResolvedValueOnce('M file.txt')
          .mockResolvedValueOnce('')
          .mockResolvedValueOnce('[main abc1234] Title')
          .mockResolvedValueOnce('commit abc1234');

        await handler.handle({
          repo: 'test-repo',
          title: 'Fix critical bug',
          message: 'Added null check to prevent crash',
          author: 'Claude',
        });

        const commitCall = mockGitExecutor.execute.mock.calls.find(
          call => Array.isArray(call[0]) && call[0][0] === 'commit'
        );
        // Check that commit message includes all parts
        const message = commitCall![0][2];
        expect(message).toContain('Fix critical bug');
        expect(message).toContain('Added null check to prevent crash');
        expect(message).toContain('Claude');
        expect(message).toContain('(via ShadowGit MCP)');
      });
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/core/git-executor.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { GitExecutor } from '../../src/core/git-executor';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';

// Mock the dependencies
jest.mock('child_process');
jest.mock('fs');
jest.mock('../../src/utils/logger', () => ({
  log: jest.fn(),
}));

describe('GitExecutor', () => {
  let executor: GitExecutor;
  let mockExecFileSync: jest.MockedFunction<typeof execFileSync>;
  let mockExistsSync: jest.MockedFunction<typeof fs.existsSync>;

  beforeEach(() => {
    jest.clearAllMocks();
    executor = new GitExecutor();
    mockExecFileSync = execFileSync as jest.MockedFunction<typeof execFileSync>;
    mockExistsSync = fs.existsSync as jest.MockedFunction<typeof fs.existsSync>;
    mockExistsSync.mockReturnValue(true); // Default: .shadowgit.git exists
  });

  describe('execute', () => {
    describe('Security Validation', () => {
      it('should block write commands when isInternal is false', async () => {
        const dangerousCommands = [
          'commit -m "test"',
          'push origin main',
          'pull origin main',
          'merge feature-branch',
          'rebase main',
          'reset --hard HEAD~1',
          'clean -fd',
          'checkout -b new-branch',
          'add .',
          'rm file.txt',
          'mv old.txt new.txt',
        ];

        for (const cmd of dangerousCommands) {
          const result = await executor.execute(cmd, '/test/repo', false);
          const gitCommand = cmd.split(' ')[0];
          expect(result).toContain(`Error: Command '${gitCommand}' is not allowed`);
          expect(result).toContain('Only read-only commands are permitted');
          expect(mockExecFileSync).not.toHaveBeenCalled();
        }
      });

      it('should allow write commands when isInternal is true', async () => {
        mockExecFileSync.mockReturnValue('Success');

        const commands = ['commit -m "test"', 'add .', 'push origin main'];
        
        for (const cmd of commands) {
          jest.clearAllMocks();
          const result = await executor.execute(cmd, '/test/repo', true);
          expect(result).not.toContain('Error: Command not allowed');
          expect(mockExecFileSync).toHaveBeenCalled();
        }
      });

      it('should block dangerous arguments even in read commands', async () => {
        const dangerousArgs = [
          'log --exec=rm -rf /',
          'diff --upload-pack=evil',
          'show --receive-pack=bad',
          'log -e rm',  // -e followed by space
        ];

        for (const cmd of dangerousArgs) {
          const result = await executor.execute(cmd, '/test/repo', false);
          expect(result).toContain('Error: Command contains potentially dangerous arguments');
          expect(mockExecFileSync).not.toHaveBeenCalled();
        }

        // -c flag should now be blocked
        const blockedConfigArgs = [
          'log -c core.editor=vim',
          'diff --config user.name=evil',
        ];
        
        for (const cmd of blockedConfigArgs) {
          jest.clearAllMocks();
          const result = await executor.execute(cmd, '/test/repo', false);
          expect(result).toContain('Error: Command contains potentially dangerous arguments');
          expect(mockExecFileSync).not.toHaveBeenCalled();
        }
      });

      it('should allow safe read-only commands', async () => {
        const safeCommands = [
          'log --oneline -5',
          'diff HEAD~1 HEAD',
          'show abc123',
          'blame file.txt',
          'status',
          'rev-parse HEAD',
          'ls-files',
          'cat-file -p HEAD',
          'describe --tags',
        ];

        mockExecFileSync.mockReturnValue('output');

        for (const cmd of safeCommands) {
          jest.clearAllMocks();
          const result = await executor.execute(cmd, '/test/repo', false);
          expect(result).not.toContain('Error');
          expect(result).toBe('output');
          expect(mockExecFileSync).toHaveBeenCalled();
        }
      });

      it('should detect command injection attempts', async () => {
        const injectionAttempts = [
          '; rm -rf /',
          '&& malicious-command',
          '| evil-pipe',
          '& background-job',
          '|| fallback-command',
          '$(dangerous-subshell)',
          '`backtick-execution`',
        ];

        for (const cmd of injectionAttempts) {
          const result = await executor.execute(cmd, '/test/repo', false);
          // These shell operators become the command name
          const firstToken = cmd.trim().split(/\s+/)[0];
          expect(result).toContain(`Error: Command '${firstToken}' is not allowed`);
          expect(mockExecFileSync).not.toHaveBeenCalled();
        }
      });

      it('should handle path arguments in commands', async () => {
        mockExecFileSync.mockReturnValue('output');
        const pathCommands = [
          'show ../../../etc/passwd',
          'diff ..\\..\\windows\\system32',
          'log %2e%2e%2fetc%2fpasswd',
          'blame ..%2f..%2f..%2fsensitive',
        ];

        // The implementation doesn't block path traversal in arguments
        // Git itself would handle these paths
        for (const cmd of pathCommands) {
          jest.clearAllMocks();
          const result = await executor.execute(cmd, '/test/repo', false);
          expect(result).toBe('output');
          expect(mockExecFileSync).toHaveBeenCalled();
        }
      });

      it('should sanitize control characters', async () => {
        mockExecFileSync.mockReturnValue('output');
        
        const dirtyCommand = 'log\x00\x01\x02\x1F --oneline';
        const result = await executor.execute(dirtyCommand, '/test/repo', false);
        
        // Should execute with cleaned command
        expect(mockExecFileSync).toHaveBeenCalledWith(
          'git',
          expect.arrayContaining(['log', '--oneline']),
          expect.any(Object)
        );
      });

      it('should enforce command length limit', async () => {
        const longCommand = 'log ' + 'a'.repeat(2000);
        const result = await executor.execute(longCommand, '/test/repo', false);
        
        expect(result).toContain('Error: Command too long');
        expect(result).toContain('max 1000 characters');
        expect(mockExecFileSync).not.toHaveBeenCalled();
      });
    });

    describe('Git Execution', () => {
      it('should set correct environment variables', async () => {
        mockExecFileSync.mockReturnValue('output');
        
        await executor.execute('log', '/test/repo', false);
        
        expect(mockExecFileSync).toHaveBeenCalledWith(
          'git',
          [
            `--git-dir=${path.join('/test/repo', '.shadowgit.git')}`,
            '--work-tree=/test/repo',
            'log'
          ],
          expect.objectContaining({
            cwd: '/test/repo',
            encoding: 'utf-8',
            timeout: 10000,
            maxBuffer: 10 * 1024 * 1024,
            env: expect.objectContaining({
              GIT_TERMINAL_PROMPT: '0',
              GIT_SSH_COMMAND: 'ssh -o BatchMode=yes',
              GIT_PAGER: 'cat',
              PAGER: 'cat'
            })
          })
        );
      });

      it('should pass custom environment variables', async () => {
        mockExecFileSync.mockReturnValue('output');
        
        const customEnv = {
          GIT_AUTHOR_NAME: 'Test User',
          GIT_AUTHOR_EMAIL: '[email protected]',
        };
        
        await executor.execute('commit -m "test"', '/test/repo', true, customEnv);
        
        expect(mockExecFileSync).toHaveBeenCalledWith(
          'git',
          [
            `--git-dir=${path.join('/test/repo', '.shadowgit.git')}`,
            '--work-tree=/test/repo',
            'commit', '-m', 'test'
          ],
          expect.objectContaining({
            env: expect.objectContaining({
              GIT_AUTHOR_NAME: 'Test User',
              GIT_AUTHOR_EMAIL: '[email protected]',
            }),
          })
        );
      });

      it('should handle successful command execution', async () => {
        const expectedOutput = 'commit abc1234\nAuthor: Test';
        mockExecFileSync.mockReturnValue(expectedOutput);
        
        const result = await executor.execute('log -1', '/test/repo', false);
        
        expect(result).toBe(expectedOutput);
      });

      it('should handle empty output', async () => {
        mockExecFileSync.mockReturnValue('');
        
        const result = await executor.execute('status', '/test/repo', false);
        
        expect(result).toBe('(empty output)');
      });

      it('should handle multi-line output', async () => {
        const multiLine = 'line1\nline2\nline3\n';
        mockExecFileSync.mockReturnValue(multiLine);
        
        const result = await executor.execute('log', '/test/repo', false);
        
        expect(result).toBe(multiLine);
      });
    });

    describe('Error Handling', () => {
      it('should handle git not installed (ENOENT)', async () => {
        const error: any = new Error('Command not found');
        error.code = 'ENOENT';
        mockExecFileSync.mockImplementation(() => {
          throw error;
        });
        
        const result = await executor.execute('log', '/test/repo', false);
        
        // ENOENT won't have stderr/stdout, falls to generic error
        expect(result).toBe('Error: Error: Command not found');
      });

      it('should handle timeout (ETIMEDOUT)', async () => {
        const error: any = new Error('Command timeout');
        error.code = 'ETIMEDOUT';
        mockExecFileSync.mockImplementation(() => {
          throw error;
        });
        
        const result = await executor.execute('log', '/test/repo', false);
        
        expect(result).toContain('Error: Command timed out after');
        expect(result).toContain('ms');
      });

      it('should handle buffer overflow (ENOBUFS)', async () => {
        const error: any = new Error('Buffer overflow');
        error.code = 'ENOBUFS';
        mockExecFileSync.mockImplementation(() => {
          throw error;
        });
        
        const result = await executor.execute('log', '/test/repo', false);
        
        // ENOBUFS won't have stderr/stdout, falls to generic error
        expect(result).toBe('Error: Error: Buffer overflow');
      });

      it('should handle git errors (exit code 128)', async () => {
        const error: any = new Error('Git error');
        error.status = 128;
        error.code = 'GITERROR';
        error.stderr = Buffer.from('fatal: bad revision');
        mockExecFileSync.mockImplementation(() => {
          throw error;
        });
        
        const result = await executor.execute('log bad-ref', '/test/repo', false);
        
        expect(result).toContain('Error executing git command');
        expect(result).toContain('fatal: bad revision');
      });

      it('should handle git errors with status but no stderr', async () => {
        const error: any = new Error('Git failed');
        error.status = 1;
        // Need 'code' property for it to go through the detailed error path
        error.code = 'GITERROR';
        mockExecFileSync.mockImplementation(() => {
          throw error;
        });
        
        const result = await executor.execute('log', '/test/repo', false);
        
        expect(result).toContain('Error executing git command');
        expect(result).toContain('Git failed');
      });

      it('should handle generic errors', async () => {
        mockExecFileSync.mockImplementation(() => {
          throw new Error('Unexpected error');
        });
        
        const result = await executor.execute('log', '/test/repo', false);
        
        // Generic errors without code property go through the fallback
        expect(result).toBe('Error: Error: Unexpected error');
      });

      it('should handle non-Error objects thrown', async () => {
        mockExecFileSync.mockImplementation(() => {
          throw 'String error';
        });
        
        const result = await executor.execute('log', '/test/repo', false);
        
        expect(result).toContain('Error: String error');
      });
    });

    describe('Special Cases', () => {
      it('should handle Windows-style paths', async () => {
        mockExecFileSync.mockReturnValue('output');
        
        const windowsPath = 'C:\\Users\\Test\\Project';
        await executor.execute('log', windowsPath, false);
        
        expect(mockExecFileSync).toHaveBeenCalledWith(
          'git',
          [
            `--git-dir=${path.join(windowsPath, '.shadowgit.git')}`,
            `--work-tree=${windowsPath}`,
            'log'
          ],
          expect.objectContaining({
            cwd: windowsPath,
          })
        );
      });

      it('should handle paths with spaces', async () => {
        mockExecFileSync.mockReturnValue('output');
        
        const pathWithSpaces = '/path/with spaces/project';
        await executor.execute('log', pathWithSpaces, false);
        
        expect(mockExecFileSync).toHaveBeenCalledWith(
          'git',
          [
            `--git-dir=${path.join(pathWithSpaces, '.shadowgit.git')}`,
            `--work-tree=${pathWithSpaces}`,
            'log'
          ],
          expect.objectContaining({
            cwd: pathWithSpaces,
          })
        );
      });

      it('should handle Unicode in output', async () => {
        const unicodeOutput = 'commit with emoji 🎉 and 中文';
        mockExecFileSync.mockReturnValue(unicodeOutput);
        
        const result = await executor.execute('log', '/test/repo', false);
        
        expect(result).toBe(unicodeOutput);
      });

      it('should handle binary output gracefully', async () => {
        // When encoding is specified, execFileSync returns a string even for binary data
        // It will be garbled but still a string
        const garbledString = '\uFFFD\uFFFD\u0000\u0001';
        mockExecFileSync.mockReturnValue(garbledString);
        
        const result = await executor.execute('cat-file -p HEAD:binary', '/test/repo', false);
        
        // Should return a string
        expect(typeof result).toBe('string');
        expect(result).toBe(garbledString);
      });
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/shadowgit-mcp-server-logic.test.ts:
--------------------------------------------------------------------------------

```typescript
// Tests for ShadowGit MCP Server logic without importing MCP SDK
// This avoids ESM/CommonJS conflicts while still testing core functionality

import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

// Mock modules
jest.mock('child_process');
jest.mock('fs');
jest.mock('os');

describe('ShadowGitMCPServer Logic Tests', () => {
  let mockExecFileSync: jest.MockedFunction<typeof execFileSync>;
  let mockExistsSync: jest.MockedFunction<typeof fs.existsSync>;
  let mockReadFileSync: jest.MockedFunction<typeof fs.readFileSync>;
  let mockHomedir: jest.MockedFunction<typeof os.homedir>;

  beforeEach(() => {
    jest.clearAllMocks();
    
    mockExecFileSync = execFileSync as jest.MockedFunction<typeof execFileSync>;
    mockExistsSync = fs.existsSync as jest.MockedFunction<typeof fs.existsSync>;
    mockReadFileSync = fs.readFileSync as jest.MockedFunction<typeof fs.readFileSync>;
    mockHomedir = os.homedir as jest.MockedFunction<typeof os.homedir>;
    
    // Default mock behaviors
    mockHomedir.mockReturnValue('/home/testuser');
    mockExistsSync.mockReturnValue(true);
    mockReadFileSync.mockReturnValue(JSON.stringify([
      { name: 'test-repo', path: '/test/repo' },
      { name: 'another-repo', path: '/another/repo' }
    ]));
  });

  describe('Security Validation', () => {
    const SAFE_COMMANDS = new Set([
      'log', 'diff', 'show', 'blame', 'grep', 'status',
      'rev-parse', 'rev-list', 'ls-files', 'cat-file',
      'diff-tree', 'shortlog', 'reflog', 'describe',
      'branch', 'tag', 'for-each-ref', 'ls-tree',
      'merge-base', 'cherry', 'count-objects'
    ]);

    const BLOCKED_ARGS = [
      '--exec', '--upload-pack', '--receive-pack',
      '-c', '--config', '--work-tree', '--git-dir',
      'push', 'pull', 'fetch', 'commit', 'merge',
      'rebase', 'reset', 'clean', 'checkout', 'add',
      'rm', 'mv', 'restore', 'stash', 'remote',
      'submodule', 'worktree', 'filter-branch',
      'repack', 'gc', 'prune', 'fsck'
    ];

    it('should only allow safe read-only commands', () => {
      const testCommands = [
        { cmd: 'log', expected: true },
        { cmd: 'diff', expected: true },
        { cmd: 'commit', expected: false },
        { cmd: 'push', expected: false },
        { cmd: 'merge', expected: false },
        { cmd: 'rebase', expected: false }
      ];

      testCommands.forEach(({ cmd, expected }) => {
        expect(SAFE_COMMANDS.has(cmd)).toBe(expected);
      });
    });

    it('should block dangerous arguments', () => {
      const dangerousCommands = [
        'log --exec=rm -rf /',
        'log -c core.editor=vim',
        'log --work-tree=/other/path',
        'diff push origin',
        'show && commit -m "test"'
      ];

      dangerousCommands.forEach(cmd => {
        const hasBlockedArg = BLOCKED_ARGS.some(arg => cmd.includes(arg));
        expect(hasBlockedArg).toBe(true);
      });
    });

    it('should detect path traversal attempts', () => {
      const PATH_TRAVERSAL_PATTERNS = [
        '../',
        '..\\',
        '%2e%2e',
        '..%2f',
        '..%5c'
      ];

      const maliciousPaths = [
        '../etc/passwd',
        '..\\windows\\system32',
        '%2e%2e%2fetc%2fpasswd',
        'test/../../sensitive'
      ];

      maliciousPaths.forEach(malPath => {
        const hasTraversal = PATH_TRAVERSAL_PATTERNS.some(pattern => 
          malPath.toLowerCase().includes(pattern)
        );
        expect(hasTraversal).toBe(true);
      });
    });
  });

  describe('Repository Path Resolution', () => {
    it('should normalize paths correctly', () => {
      const testPath = '~/projects/test';
      const normalized = testPath.replace('~', '/home/testuser');
      expect(normalized).toBe('/home/testuser/projects/test');
    });

    it('should handle Windows paths', () => {
      const windowsPaths = [
        'C:\\Users\\test\\project',
        'D:\\repos\\myrepo',
        '\\\\server\\share\\repo'
      ];

      windowsPaths.forEach(winPath => {
        const isWindowsPath = winPath.includes(':') || winPath.startsWith('\\\\');
        expect(isWindowsPath).toBe(true);
      });
    });

    it('should validate absolute paths', () => {
      const paths = [
        { path: '/absolute/path', isAbsolute: true },
        { path: 'relative/path', isAbsolute: false },
        { path: './relative', isAbsolute: false }
      ];
      
      // Test Windows path separately on Windows platform
      if (process.platform === 'win32') {
        paths.push({ path: 'C:\\Windows', isAbsolute: true });
      }

      paths.forEach(({ path: testPath, isAbsolute: expected }) => {
        expect(path.isAbsolute(testPath)).toBe(expected);
      });
    });
  });

  describe('Git Environment Configuration', () => {
    it('should set correct environment variables', () => {
      const repoPath = '/test/repo';
      const shadowGitDir = path.join(repoPath, '.shadowgit.git');
      
      const gitEnv = {
        ...process.env,
        GIT_DIR: shadowGitDir,
        GIT_WORK_TREE: repoPath
      };

      expect(gitEnv.GIT_DIR).toBe('/test/repo/.shadowgit.git');
      expect(gitEnv.GIT_WORK_TREE).toBe('/test/repo');
    });

    it('should enforce timeout and buffer limits', () => {
      const TIMEOUT_MS = 10000; // 10 seconds
      const MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB

      expect(TIMEOUT_MS).toBe(10000);
      expect(MAX_BUFFER_SIZE).toBe(10485760);
    });
  });

  describe('Command Sanitization', () => {
    it('should remove control characters', () => {
      const dirtyCommand = 'log\x00\x01\x02\x1F --oneline';
      const sanitized = dirtyCommand.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
      expect(sanitized).toBe('log --oneline');
    });

    it('should enforce command length limit', () => {
      const MAX_COMMAND_LENGTH = 1000;
      const longCommand = 'log ' + 'a'.repeat(2000);
      expect(longCommand.length).toBeGreaterThan(MAX_COMMAND_LENGTH);
    });
  });

  describe('Error Handling', () => {
    it('should handle git not installed error', () => {
      const error: any = new Error('Command not found');
      error.code = 'ENOENT';
      expect(error.code).toBe('ENOENT');
    });

    it('should handle timeout error', () => {
      const error: any = new Error('Timeout');
      error.signal = 'SIGTERM';
      expect(error.signal).toBe('SIGTERM');
    });

    it('should handle buffer overflow error', () => {
      const error: any = new Error('Buffer overflow');
      error.code = 'ENOBUFS';
      expect(error.code).toBe('ENOBUFS');
    });

    it('should handle git error (exit code 128)', () => {
      const error: any = new Error('Git error');
      error.status = 128;
      error.stderr = 'fatal: bad revision';
      expect(error.status).toBe(128);
      expect(error.stderr).toContain('fatal');
    });
  });

  describe('Logging System', () => {
    it('should support multiple log levels', () => {
      const LOG_LEVELS = {
        debug: 0,
        info: 1,
        warn: 2,
        error: 3
      };

      expect(LOG_LEVELS.debug).toBeLessThan(LOG_LEVELS.info);
      expect(LOG_LEVELS.info).toBeLessThan(LOG_LEVELS.warn);
      expect(LOG_LEVELS.warn).toBeLessThan(LOG_LEVELS.error);
    });

    it('should include timestamp in logs', () => {
      const timestamp = new Date().toISOString();
      expect(timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
    });
  });

  describe('Configuration', () => {
    it('should read timeout from environment', () => {
      const customTimeout = '30000';
      const timeout = parseInt(customTimeout || '10000', 10);
      expect(timeout).toBe(30000);
    });

    it('should use default timeout if not specified', () => {
      const envTimeout: string | undefined = undefined;
      const timeout = parseInt(envTimeout || '10000', 10);
      expect(timeout).toBe(10000);
    });

    it('should read log level from environment', () => {
      const logLevel = 'debug';
      expect(['debug', 'info', 'warn', 'error']).toContain(logLevel);
    });
  });

  describe('Manual Checkpoint Functionality', () => {
    it('should validate required parameters', () => {
      // Test that repo and title are required
      const validateArgs = (args: any): boolean => {
        return (
          typeof args === 'object' &&
          args !== null &&
          'repo' in args &&
          'title' in args &&
          typeof args.repo === 'string' &&
          typeof args.title === 'string'
        );
      };

      expect(validateArgs({ repo: 'test', title: 'Test' })).toBe(true);
      expect(validateArgs({ repo: 'test' })).toBe(false);
      expect(validateArgs({ title: 'Test' })).toBe(false);
      expect(validateArgs({})).toBe(false);
      expect(validateArgs(null)).toBe(false);
    });

    it('should validate title length', () => {
      const MAX_TITLE_LENGTH = 50;
      const validateTitleLength = (title: string): boolean => {
        return title.length <= MAX_TITLE_LENGTH;
      };

      expect(validateTitleLength('Normal title')).toBe(true);
      expect(validateTitleLength('a'.repeat(50))).toBe(true);
      expect(validateTitleLength('a'.repeat(51))).toBe(false);
    });

    it('should generate correct author email from name', () => {
      const generateAuthorEmail = (author: string): string => {
        return `${author.toLowerCase().replace(/\s+/g, '-')}@shadowgit.local`;
      };

      expect(generateAuthorEmail('Claude')).toBe('[email protected]');
      expect(generateAuthorEmail('GPT-4')).toBe('[email protected]');
      expect(generateAuthorEmail('AI Assistant')).toBe('[email protected]');
      expect(generateAuthorEmail('Gemini Pro')).toBe('[email protected]');
    });

    it('should properly escape shell special characters', () => {
      const escapeShellString = (str: string): string => {
        return str
          .replace(/\\/g, '\\\\')  // Escape backslashes first
          .replace(/"/g, '\\"')    // Escape double quotes
          .replace(/\$/g, '\\$')   // Escape dollar signs
          .replace(/`/g, '\\`')    // Escape backticks
          .replace(/'/g, "\\'");   // Escape single quotes
      };

      expect(escapeShellString('normal text')).toBe('normal text');
      expect(escapeShellString('text with $var')).toBe('text with \\$var');
      expect(escapeShellString('text with "quotes"')).toBe('text with \\"quotes\\"');
      expect(escapeShellString('text with `backticks`')).toBe('text with \\`backticks\\`');
      expect(escapeShellString('text with \\backslash')).toBe('text with \\\\backslash');
      expect(escapeShellString("text with 'single'")).toBe("text with \\'single\\'");
      expect(escapeShellString('$var "quote" `tick` \\slash')).toBe('\\$var \\"quote\\" \\`tick\\` \\\\slash');
    });

    it('should format commit message correctly', () => {
      const formatCommitMessage = (
        title: string,
        message: string | undefined,
        author: string,
        timestamp: string
      ): string => {
        let commitMessage = `✋ [${author}] Manual Checkpoint: ${title}`;
        if (message) {
          commitMessage += `\n\n${message}`;
        }
        commitMessage += `\n\nCreated by: ${author}\nTimestamp: ${timestamp}`;
        return commitMessage;
      };

      const timestamp = '2024-01-01T12:00:00Z';
      
      // Test with minimal parameters
      const msg1 = formatCommitMessage('Fix bug', undefined, 'AI Assistant', timestamp);
      expect(msg1).toContain('✋ [AI Assistant] Manual Checkpoint: Fix bug');
      expect(msg1).toContain('Created by: AI Assistant');
      expect(msg1).toContain('Timestamp: 2024-01-01T12:00:00Z');
      expect(msg1).not.toContain('undefined');

      // Test with all parameters
      const msg2 = formatCommitMessage('Add feature', 'Detailed description', 'Claude', timestamp);
      expect(msg2).toContain('✋ [Claude] Manual Checkpoint: Add feature');
      expect(msg2).toContain('Detailed description');
      expect(msg2).toContain('Created by: Claude');
    });

    it('should extract commit hash from git output', () => {
      const extractCommitHash = (output: string): string => {
        const match = output.match(/\[[\w\s-]+\s+([a-f0-9]{7,})\]/);
        return match ? match[1] : 'unknown';
      };

      expect(extractCommitHash('[main abc1234] Test commit')).toBe('abc1234');
      expect(extractCommitHash('[feature-branch def56789] Another commit')).toBe('def56789');
      expect(extractCommitHash('[develop 1a2b3c4d5e6f] Long hash')).toBe('1a2b3c4d5e6f');
      expect(extractCommitHash('No match here')).toBe('unknown');
    });

    it('should set correct Git environment variables', () => {
      const createGitEnv = (author: string) => {
        const authorEmail = `${author.toLowerCase().replace(/\s+/g, '-')}@shadowgit.local`;
        return {
          GIT_AUTHOR_NAME: author,
          GIT_AUTHOR_EMAIL: authorEmail,
          GIT_COMMITTER_NAME: author,
          GIT_COMMITTER_EMAIL: authorEmail
        };
      };

      const env1 = createGitEnv('Claude');
      expect(env1.GIT_AUTHOR_NAME).toBe('Claude');
      expect(env1.GIT_AUTHOR_EMAIL).toBe('[email protected]');
      expect(env1.GIT_COMMITTER_NAME).toBe('Claude');
      expect(env1.GIT_COMMITTER_EMAIL).toBe('[email protected]');

      const env2 = createGitEnv('GPT-4');
      expect(env2.GIT_AUTHOR_NAME).toBe('GPT-4');
      expect(env2.GIT_AUTHOR_EMAIL).toBe('[email protected]');
    });

    it('should handle isInternal flag for bypassing security', () => {
      // Test that internal flag allows normally blocked commands
      const isCommandAllowed = (command: string, isInternal: boolean): boolean => {
        const SAFE_COMMANDS = new Set(['log', 'diff', 'show', 'status']);
        const parts = command.trim().split(/\s+/);
        const gitCommand = parts[0];
        
        if (isInternal) {
          return true; // Bypass all checks for internal operations
        }
        
        return SAFE_COMMANDS.has(gitCommand);
      };

      // Normal security checks
      expect(isCommandAllowed('log', false)).toBe(true);
      expect(isCommandAllowed('commit', false)).toBe(false);
      expect(isCommandAllowed('add', false)).toBe(false);
      
      // Internal bypass
      expect(isCommandAllowed('commit', true)).toBe(true);
      expect(isCommandAllowed('add', true)).toBe(true);
      expect(isCommandAllowed('anything', true)).toBe(true);
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/integration/workflow.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { RepositoryManager } from '../../src/core/repository-manager';
import { GitExecutor } from '../../src/core/git-executor';
import { SessionClient } from '../../src/core/session-client';
import { GitHandler } from '../../src/handlers/git-handler';
import { ListReposHandler } from '../../src/handlers/list-repos-handler';
import { CheckpointHandler } from '../../src/handlers/checkpoint-handler';
import { SessionHandler } from '../../src/handlers/session-handler';
import * as fs from 'fs';
import * as os from 'os';
import { execFileSync } from 'child_process';

// Mock all external dependencies
jest.mock('fs');
jest.mock('os');
jest.mock('child_process');
jest.mock('../../src/utils/logger', () => ({
  log: jest.fn(),
}));

// Mock fetch for SessionClient
global.fetch = jest.fn() as jest.MockedFunction<typeof fetch>;

describe('Integration: Complete Workflow', () => {
  let repositoryManager: RepositoryManager;
  let gitExecutor: GitExecutor;
  let sessionClient: SessionClient;
  let gitHandler: GitHandler;
  let listReposHandler: ListReposHandler;
  let checkpointHandler: CheckpointHandler;
  let sessionHandler: SessionHandler;
  
  let mockExistsSync: jest.MockedFunction<typeof fs.existsSync>;
  let mockReadFileSync: jest.MockedFunction<typeof fs.readFileSync>;
  let mockHomedir: jest.MockedFunction<typeof os.homedir>;
  let mockExecFileSync: jest.MockedFunction<typeof execFileSync>;
  let mockFetch: jest.MockedFunction<typeof fetch>;

  beforeEach(() => {
    jest.clearAllMocks();
    
    // Get mock references
    mockExistsSync = fs.existsSync as jest.MockedFunction<typeof fs.existsSync>;
    mockReadFileSync = fs.readFileSync as jest.MockedFunction<typeof fs.readFileSync>;
    mockHomedir = os.homedir as jest.MockedFunction<typeof os.homedir>;
    mockExecFileSync = execFileSync as jest.MockedFunction<typeof execFileSync>;
    mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
    
    // Setup default mocks
    mockHomedir.mockReturnValue('/home/testuser');
    mockExistsSync.mockReturnValue(true);
    mockReadFileSync.mockReturnValue(JSON.stringify([
      { name: 'my-project', path: '/workspace/my-project' },
      { name: 'another-project', path: '/workspace/another-project' },
    ]));
    
    // Initialize services
    repositoryManager = new RepositoryManager();
    gitExecutor = new GitExecutor();
    sessionClient = new SessionClient();
    
    // Initialize handlers
    gitHandler = new GitHandler(repositoryManager, gitExecutor);
    listReposHandler = new ListReposHandler(repositoryManager);
    checkpointHandler = new CheckpointHandler(repositoryManager, gitExecutor);
    sessionHandler = new SessionHandler(repositoryManager, sessionClient);
  });

  describe('Scenario: Complete AI Work Session with Session API Available', () => {
    it('should complete full workflow: list → start_session → git_command → checkpoint → end_session', async () => {
      const sessionId = 'session-123-abc';
      const commitHash = 'abc1234';
      
      // Step 1: List repositories
      const listResult = await listReposHandler.handle();
      expect(listResult.content[0].text).toContain('my-project:');
      expect(listResult.content[0].text).toContain('Path: /workspace/my-project');
      expect(listResult.content[0].text).toContain('another-project:');
      expect(listResult.content[0].text).toContain('Path: /workspace/another-project');
      
      // Step 2: Start session
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: (jest.fn() as any).mockResolvedValue({
          success: true,
          sessionId,
        }),
      } as unknown as Response);
      
      const startResult = await sessionHandler.startSession({
        repo: 'my-project',
        description: 'Implementing new feature X',
      });
      
      expect(startResult.content[0].text).toContain('Session started successfully');
      expect(startResult.content[0].text).toContain(sessionId);
      
      // Step 3: Execute git commands
      mockExecFileSync.mockReturnValue('On branch main\nYour branch is up to date');
      
      const statusResult = await gitHandler.handle({
        repo: 'my-project',
        command: 'status',
      });
      
      expect(statusResult.content[0].text).toContain('On branch main');
      
      // Step 4: Simulate some changes and check diff
      mockExecFileSync.mockReturnValue('diff --git a/file.txt b/file.txt\n+new line');
      
      const diffResult = await gitHandler.handle({
        repo: 'my-project',
        command: 'diff',
      });
      
      expect(diffResult.content[0].text).toContain('diff --git');
      
      // Step 5: Create checkpoint
      mockExecFileSync
        .mockReturnValueOnce('M file.txt\nA newfile.js') // status --porcelain
        .mockReturnValueOnce('') // add -A
        .mockReturnValueOnce(`[main ${commitHash}] Add feature X`) // commit
        .mockReturnValueOnce('commit abc1234\nAuthor: Claude'); // show --stat
      
      const checkpointResult = await checkpointHandler.handle({
        repo: 'my-project',
        title: 'Add feature X',
        message: 'Implemented new feature X with comprehensive tests',
        author: 'Claude',
      });
      
      expect(checkpointResult.content[0].text).toContain('Checkpoint Created Successfully!');
      expect(checkpointResult.content[0].text).toContain(commitHash);
      
      // Step 6: End session
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: (jest.fn() as any).mockResolvedValue({
          success: true,
        }),
      } as unknown as Response);
      
      const endResult = await sessionHandler.endSession({
        sessionId,
        commitHash,
      });
      
      expect(endResult.content[0].text).toContain(`Session ${sessionId} ended successfully`);
    });
  });

  describe('Scenario: Session API Offline Fallback', () => {
    it('should handle workflow when Session API is unavailable', async () => {
      // Session API is offline
      mockFetch.mockRejectedValue(new Error('Connection refused'));
      
      // Step 1: Try to start session (should fallback gracefully)
      const startResult = await sessionHandler.startSession({
        repo: 'my-project',
        description: 'Fixing bug in authentication',
      });
      
      expect(startResult.content[0].text).toContain('Session API is offline');
      expect(startResult.content[0].text).toContain('Proceeding without session tracking');
      
      // Step 2: Continue with git operations
      mockExecFileSync.mockReturnValue('file.txt | 2 +-');
      
      const diffStatResult = await gitHandler.handle({
        repo: 'my-project',
        command: 'diff --stat',
      });
      
      expect(diffStatResult.content[0].text).toContain('file.txt | 2 +-');
      
      // Step 3: Create checkpoint (should work without session)
      mockExecFileSync
        .mockReturnValueOnce('M file.txt') // status
        .mockReturnValueOnce('') // add
        .mockReturnValueOnce('[main def5678] Fix auth bug') // commit
        .mockReturnValueOnce('commit def5678'); // show
      
      const checkpointResult = await checkpointHandler.handle({
        repo: 'my-project',
        title: 'Fix auth bug',
        author: 'GPT-4',
      });
      
      expect(checkpointResult.content[0].text).toContain('Checkpoint Created Successfully!');
      
      // Step 4: Try to end session (should handle gracefully)
      const endResult = await sessionHandler.endSession({
        sessionId: 'non-existent-session',
      });
      
      expect(endResult.content[0].text).toContain('Failed to End Session');
    });
  });

  describe('Scenario: Multiple AI Agents Collaboration', () => {
    it('should handle multiple agents working on different repositories', async () => {
      const sessions = [
        { id: 'claude-session-1', repo: 'my-project', agent: 'Claude' },
        { id: 'gpt4-session-2', repo: 'another-project', agent: 'GPT-4' },
      ];
      
      // Both agents start sessions
      for (const session of sessions) {
        mockFetch.mockResolvedValueOnce({
          ok: true,
          status: 200,
          json: (jest.fn() as any).mockResolvedValue({
            success: true,
            sessionId: session.id,
          }),
        } as unknown as Response);
        
        const result = await sessionHandler.startSession({
          repo: session.repo,
          description: `${session.agent} working on ${session.repo}`,
        });
        
        expect(result.content[0].text).toContain(session.id);
      }
      
      // Each agent makes changes and creates checkpoints
      for (const session of sessions) {
        mockExecFileSync
          .mockReturnValueOnce('M file.txt') // status
          .mockReturnValueOnce('') // add
          .mockReturnValueOnce(`[main abc${session.id.slice(0, 4)}] ${session.agent} changes`) // commit
          .mockReturnValueOnce('commit details'); // show
        
        const checkpointResult = await checkpointHandler.handle({
          repo: session.repo,
          title: `${session.agent} changes`,
          author: session.agent,
        });
        
        expect(checkpointResult.content[0].text).toContain('Checkpoint Created Successfully!');
      }
      
      // Both agents end their sessions
      for (const session of sessions) {
        mockFetch.mockResolvedValueOnce({
          ok: true,
          status: 200,
          json: (jest.fn() as any).mockResolvedValue({
            success: true,
          }),
        } as unknown as Response);
        
        const result = await sessionHandler.endSession({
          sessionId: session.id,
        });
        
        expect(result.content[0].text).toContain(`Session ${session.id} ended successfully`);
      }
    });
  });

  describe('Scenario: Error Recovery', () => {
    it('should handle errors at each stage gracefully', async () => {
      // Repository not found
      const invalidRepoResult = await gitHandler.handle({
        repo: 'non-existent-repo',
        command: 'log',
      });
      
      expect(invalidRepoResult.content[0].text).toContain("Error: Repository 'non-existent-repo' not found");
      
      // No .shadowgit.git directory
      mockExistsSync.mockImplementation(p => {
        if (typeof p === 'string' && p.includes('.shadowgit.git')) return false;
        if (typeof p === 'string' && p.includes('repos.json')) return true;
        return true;
      });
      
      const noShadowGitResult = await gitHandler.handle({
        repo: 'my-project',
        command: 'log',
      });
      
      expect(noShadowGitResult.content[0].text).toContain('not found');
      
      // Reset mock for next tests
      mockExistsSync.mockReturnValue(true);
      
      // Invalid git command
      mockExecFileSync.mockReturnValue('Error: Command not allowed');
      
      const invalidCommandResult = await gitHandler.handle({
        repo: 'my-project',
        command: 'push origin main',
      });
      
      expect(invalidCommandResult.content[0].text).toContain('not allowed');
      
      // No changes to commit
      mockExecFileSync.mockReturnValueOnce(''); // empty status
      
      const noChangesResult = await checkpointHandler.handle({
        repo: 'my-project',
        title: 'No changes',
        author: 'Claude',
      });
      
      expect(noChangesResult.content[0].text).toContain('No Changes Detected');
      
      // Git commit failure
      mockExecFileSync
        .mockReturnValueOnce('M file.txt') // status
        .mockReturnValueOnce('') // add
        .mockReturnValueOnce('Error: Cannot create commit'); // commit fails
      
      const commitFailResult = await checkpointHandler.handle({
        repo: 'my-project',
        title: 'Test',
        author: 'Claude',
      });
      
      expect(commitFailResult.content[0].text).toContain('Failed to Create Commit');
    });
  });

  describe('Scenario: Validation and Edge Cases', () => {
    it('should validate all required parameters', async () => {
      // Missing parameters for start_session
      let result = await sessionHandler.startSession({
        repo: 'my-project',
        // missing description
      });
      expect(result.content[0].text).toContain('Error');
      
      // Missing parameters for checkpoint
      result = await checkpointHandler.handle({
        repo: 'my-project',
        // missing title
      });
      expect(result.content[0].text).toContain('Error');
      
      // Title too long
      result = await checkpointHandler.handle({
        repo: 'my-project',
        title: 'a'.repeat(51),
      });
      expect(result.content[0].text).toContain('50 characters or less');
      
      // Message too long
      result = await checkpointHandler.handle({
        repo: 'my-project',
        title: 'Valid title',
        message: 'a'.repeat(1001),
      });
      expect(result.content[0].text).toContain('1000 characters or less');
    });
    
    it('should handle special characters in commit messages', async () => {
      mockExecFileSync
        .mockReturnValueOnce('M file.txt') // status
        .mockReturnValueOnce('') // add
        .mockReturnValueOnce('[main xyz789] Special') // commit
        .mockReturnValueOnce('commit xyz789'); // show
      
      const result = await checkpointHandler.handle({
        repo: 'my-project',
        title: 'Fix $pecial "bug" with `quotes`',
        message: 'Message with $var and backslash',
        author: 'AI-Agent',
      });
      
      expect(result.content[0].text).toContain('Checkpoint Created Successfully!');
      
      // Verify commit was called with correct arguments
      const commitCall = mockExecFileSync.mock.calls.find(
        call => Array.isArray(call[1]) && call[1].includes('commit')
      );
      expect(commitCall).toBeDefined();
      // With execFileSync, first arg is 'git', second is array of args
      expect(commitCall![0]).toBe('git');
      // Find the commit message in the arguments array
      const args = commitCall![1] as string[];
      const messageIndex = args.indexOf('-m') + 1;
      const commitMessage = args[messageIndex];
      // Special characters should be preserved in the message
      expect(commitMessage).toContain('$pecial');
      expect(commitMessage).toContain('"bug"');
      expect(commitMessage).toContain('`quotes`');
    });
  });

  describe('Scenario: Cross-Platform Compatibility', () => {
    it('should handle Windows paths correctly', async () => {
      mockReadFileSync.mockReturnValue(JSON.stringify([
        { name: 'windows-project', path: 'C:\\Projects\\MyApp' },
      ]));
      
      // Reinitialize to load Windows paths
      repositoryManager = new RepositoryManager();
      gitHandler = new GitHandler(repositoryManager, gitExecutor);
      
      mockExecFileSync.mockReturnValue('Windows output');
      
      const result = await gitHandler.handle({
        repo: 'windows-project',
        command: 'status',
      });
      
      // Now includes workflow reminder for status command
      expect(result.content[0].text).toContain('Windows output');
      expect(result.content[0].text).toContain('Planning to Make Changes?');
      expect(mockExecFileSync).toHaveBeenCalledWith(
        'git',
        expect.arrayContaining([
          expect.stringContaining('--git-dir='),
          expect.stringContaining('--work-tree='),
        ]),
        expect.objectContaining({
          cwd: 'C:\\Projects\\MyApp',
        })
      );
    });
    
    it('should handle paths with spaces', async () => {
      mockReadFileSync.mockReturnValue(JSON.stringify([
        { name: 'space-project', path: '/path/with spaces/project' },
      ]));
      
      repositoryManager = new RepositoryManager();
      gitHandler = new GitHandler(repositoryManager, gitExecutor);
      
      mockExecFileSync.mockReturnValue('Output');
      
      const result = await gitHandler.handle({
        repo: 'space-project',
        command: 'log',
      });
      
      // Now includes workflow reminder for log command
      expect(result.content[0].text).toContain('Output');
      expect(result.content[0].text).toContain('Planning to Make Changes?');
      expect(mockExecFileSync).toHaveBeenCalledWith(
        'git',
        expect.arrayContaining([
          expect.stringContaining('--git-dir='),
          expect.stringContaining('--work-tree='),
        ]),
        expect.objectContaining({
          cwd: '/path/with spaces/project',
        })
      );
    });
  });
});
```