├── LICENSE ├── README.md ├── claude-tw └── sync.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Claude Code-TaskWarrior Integration 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Claude Code-TaskWarrior Integration 2 | 3 | A comprehensive integration system that bridges Claude Code's TodoWrite tool with TaskWarrior, providing persistent task management across sessions. 4 | 5 | ## Features 6 | 7 | - **Persistent Tasks**: Tasks survive Claude Code session restarts 8 | - **Bidirectional Sync**: Import from TaskWarrior, export to TaskWarrior 9 | - **Session Management**: Organized project-based sessions 10 | - **Automatic Backups**: Every sync creates timestamped backups 11 | - **Priority Mapping**: Claude Code priorities (high/medium/low) ↔ TaskWarrior (H/M/L) 12 | - **Status Tracking**: pending, in_progress, completed status sync 13 | - **Multi-Project Support**: Organize tasks by project 14 | - **Data Validation**: Ensures data integrity during sync 15 | - **Rich CLI**: Full-featured command-line interface 16 | - **Global Access**: Works from any directory with any Claude Code session 17 | 18 | ## Platform Compatibility 19 | 20 | - **Tested on**: Ubuntu-based Linux systems (Ubuntu, Debian, Pop!_OS, Linux Mint) 21 | - **Should work on**: Most Linux distributions with bash shell 22 | - **Not tested on**: 23 | - macOS (may work with modifications to paths and commands) 24 | - Windows WSL (untested but likely compatible) 25 | - Native Windows (requires significant modifications) 26 | 27 | **Note**: The installation instructions use `apt` package manager. For other systems: 28 | - macOS: Use `brew install task node jq` 29 | - Fedora/RHEL: Use `dnf install task nodejs jq` 30 | - Arch: Use `pacman -S task nodejs jq` 31 | 32 | ## Quick Start 33 | 34 | ### 1. Installation 35 | 36 | ```bash 37 | # Install dependencies 38 | sudo apt install taskwarrior nodejs jq 39 | 40 | # Global installation (recommended) 41 | mkdir -p ~/bin 42 | cp -r claude-taskwarrior ~/bin/ 43 | chmod +x ~/bin/claude-taskwarrior/claude-tw 44 | chmod +x ~/bin/claude-taskwarrior/sync.js 45 | 46 | # Add to PATH 47 | echo 'export PATH="$PATH:$HOME/bin/claude-taskwarrior"' >> ~/.bashrc 48 | source ~/.bashrc 49 | ``` 50 | 51 | ### 2. Initialize TaskWarrior (First time only) 52 | 53 | ```bash 54 | # Initialize TaskWarrior 55 | task add "test task" 56 | task 1 delete 57 | ``` 58 | 59 | ### 3. Start a Session 60 | 61 | ```bash 62 | # Start with default project (works from any directory) 63 | claude-tw start 64 | 65 | # Or start with custom project 66 | claude-tw start my-project-name 67 | ``` 68 | 69 | ### 4. Sync with Claude Code 70 | 71 | When Claude Code updates todos, sync them to TaskWarrior: 72 | 73 | ```bash 74 | # Example: sync current todos 75 | claude-tw sync '[{"id":"1","content":"Task 1","status":"pending","priority":"high"}]' 76 | ``` 77 | 78 | ### 5. Export for Claude Code 79 | 80 | Import existing TaskWarrior tasks into Claude Code: 81 | 82 | ```bash 83 | # Export tasks for Claude Code import 84 | claude-tw export 85 | ``` 86 | 87 | ## Usage 88 | 89 | ### Session Management 90 | 91 | ```bash 92 | # Start new session 93 | claude-tw start [project-name] 94 | 95 | # Check session status 96 | claude-tw status 97 | 98 | # End current session 99 | claude-tw end 100 | ``` 101 | 102 | ### Task Synchronization 103 | 104 | ```bash 105 | # Sync Claude Code todos to TaskWarrior 106 | claude-tw sync '' 107 | 108 | # Export TaskWarrior tasks for Claude Code 109 | claude-tw export 110 | 111 | # Create manual backup 112 | claude-tw backup 113 | ``` 114 | 115 | ### TaskWarrior Commands 116 | 117 | View your tasks in TaskWarrior: 118 | 119 | ```bash 120 | # List all project tasks 121 | task project:claude-session list 122 | 123 | # Show only pending tasks 124 | task project:claude-session status:pending 125 | 126 | # Show task summary 127 | task project:claude-session summary 128 | 129 | # Mark task complete 130 | task done 131 | 132 | # Start working on task 133 | task start 134 | ``` 135 | 136 | ## Data Format 137 | 138 | ### Claude Code Todo Format 139 | ```json 140 | [ 141 | { 142 | "id": "unique-id", 143 | "content": "Task description", 144 | "status": "pending|in_progress|completed", 145 | "priority": "high|medium|low", 146 | "due": "2024-01-15", 147 | "tags": ["tag1", "tag2"] 148 | } 149 | ] 150 | ``` 151 | 152 | ### TaskWarrior Export Format 153 | ```json 154 | [ 155 | { 156 | "id": "tw-12345678", 157 | "content": "Task description", 158 | "status": "pending", 159 | "priority": "high", 160 | "created": "2024-01-01T10:00:00Z", 161 | "modified": "2024-01-01T10:00:00Z", 162 | "due": "2024-01-15", 163 | "tags": ["tag1", "tag2"] 164 | } 165 | ] 166 | ``` 167 | 168 | ## Configuration 169 | 170 | Configuration file: `~/.claude-taskwarrior.conf` 171 | 172 | ```bash 173 | # Default project name 174 | PROJECT=claude-session 175 | 176 | # Enable automatic sync 177 | AUTO_SYNC=true 178 | 179 | # Backup directory 180 | BACKUP_DIR=/home/user/.claude-taskwarrior-backups 181 | 182 | # Creation timestamp 183 | CREATED=2024-01-01T10:00:00Z 184 | ``` 185 | 186 | ## Directory Structure 187 | 188 | ``` 189 | claude-taskwarrior/ 190 | ├── claude-tw # Main CLI script 191 | ├── sync.js # Core sync engine 192 | ├── README.md # This file 193 | └── examples/ # Usage examples 194 | ├── basic-workflow.md 195 | └── advanced-usage.md 196 | ``` 197 | 198 | ## Backup System 199 | 200 | - **Automatic**: Backups created before each sync operation 201 | - **Location**: `~/.claude-taskwarrior-backups/` 202 | - **Format**: `backup-project-timestamp-operation.json` 203 | - **Manual**: `claude-tw backup` creates manual backup 204 | 205 | Example backup filename: 206 | ``` 207 | backup-claude-session-2024-01-01T10-00-00Z-pre-import.json 208 | ``` 209 | 210 | ## Error Handling 211 | 212 | The system includes comprehensive error handling: 213 | 214 | - **Validation**: Ensures todo structure integrity 215 | - **Graceful Degradation**: Continues processing if some tasks fail 216 | - **Detailed Logging**: Clear error messages with context 217 | - **Rollback**: Backups enable easy recovery 218 | 219 | ## Integration with Claude Code 220 | 221 | ### Automatic Workflow 222 | 223 | 1. **Session Start**: `claude-tw start` 224 | 2. **Import Existing**: Shows existing TaskWarrior tasks for Claude Code import 225 | 3. **Work in Claude Code**: Use TodoWrite as normal 226 | 4. **Sync Changes**: `claude-tw sync "$(claude-todos-json)"` 227 | 5. **Persistent Storage**: Tasks persist in TaskWarrior 228 | 6. **Session End**: `claude-tw end` for summary 229 | 230 | ### Manual Workflow 231 | 232 | 1. Export TaskWarrior tasks: `claude-tw export` 233 | 2. Copy output to Claude Code: `TodoWrite ` 234 | 3. Work with todos in Claude Code 235 | 4. Sync back to TaskWarrior: `claude-tw sync ''` 236 | 237 | ## Global Installation 238 | 239 | The integration works from any directory and with any Claude Code session: 240 | 241 | ```bash 242 | # Install globally 243 | mkdir -p ~/bin 244 | cp -r claude-taskwarrior ~/bin/ 245 | echo 'export PATH="$PATH:$HOME/bin/claude-taskwarrior"' >> ~/.bashrc 246 | source ~/.bashrc 247 | 248 | # Now works from anywhere 249 | cd /any/directory 250 | claude-tw start my-project 251 | ``` 252 | 253 | ## Development 254 | 255 | ### Testing 256 | 257 | ```bash 258 | # Test basic functionality 259 | node sync.js import test-project '[{"id":"test","content":"Test task","status":"pending","priority":"medium"}]' 260 | node sync.js export test-project 261 | node sync.js summary test-project 262 | 263 | # Test CLI 264 | ./claude-tw start test-session 265 | ./claude-tw status 266 | ./claude-tw end 267 | ``` 268 | 269 | ### Extension 270 | 271 | The sync engine is modular and extensible: 272 | 273 | ```javascript 274 | const ClaudeTaskWarriorSync = require('./sync.js'); 275 | const sync = new ClaudeTaskWarriorSync(); 276 | 277 | // Custom usage 278 | const todos = sync.exportTodos('my-project'); 279 | sync.importTodos('my-project', customTodos); 280 | ``` 281 | 282 | ## Troubleshooting 283 | 284 | ### Common Issues 285 | 286 | 1. **TaskWarrior not initialized** 287 | ```bash 288 | task add "init task" 289 | task 1 delete 290 | ``` 291 | 292 | 2. **Permission denied** 293 | ```bash 294 | chmod +x ~/bin/claude-taskwarrior/claude-tw 295 | ``` 296 | 297 | 3. **Node.js not found** 298 | ```bash 299 | sudo apt install nodejs npm 300 | ``` 301 | 302 | 4. **jq not found** 303 | ```bash 304 | sudo apt install jq 305 | ``` 306 | 307 | ### Debug Mode 308 | 309 | Enable verbose output: 310 | ```bash 311 | export CLAUDE_TW_DEBUG=1 312 | claude-tw status 313 | ``` 314 | 315 | ### Log Files 316 | 317 | Session logs: `/tmp/claude-tw-session.json` 318 | Error logs: Check terminal output during sync operations 319 | 320 | ## Contributing 321 | 322 | This project is open source. Contributions welcome: 323 | 324 | 1. Fork the repository 325 | 2. Create feature branch 326 | 3. Add tests for new functionality 327 | 4. Submit pull request 328 | 329 | ## License 330 | 331 | MIT License - see LICENSE file for details. 332 | 333 | ## Credits 334 | 335 | Built for integration with: 336 | - [Claude Code](https://claude.ai/code) - Anthropic's AI coding assistant 337 | - [TaskWarrior](https://taskwarrior.org/) - Command-line task management 338 | 339 | --- -------------------------------------------------------------------------------- /claude-tw: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Claude-TaskWarrior Integration 4 | # Main CLI interface for TaskWarrior-Claude Code integration 5 | # Usage: claude-tw [command] [options] 6 | 7 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 8 | CONFIG_FILE="$HOME/.claude-taskwarrior.conf" 9 | SYNC_SCRIPT="$SCRIPT_DIR/sync.js" 10 | SESSION_FILE="/tmp/claude-tw-session.json" 11 | 12 | # Default configuration 13 | DEFAULT_PROJECT="claude-session" 14 | DEFAULT_AUTO_SYNC="true" 15 | DEFAULT_BACKUP_DIR="$HOME/.claude-taskwarrior-backups" 16 | 17 | # Colors for output 18 | RED='\033[0;31m' 19 | GREEN='\033[0;32m' 20 | YELLOW='\033[1;33m' 21 | BLUE='\033[0;34m' 22 | NC='\033[0m' # No Color 23 | 24 | # Initialize configuration if it doesn't exist 25 | init_config() { 26 | if [[ ! -f "$CONFIG_FILE" ]]; then 27 | echo "# Claude-TaskWarrior Configuration" > "$CONFIG_FILE" 28 | echo "PROJECT=$DEFAULT_PROJECT" >> "$CONFIG_FILE" 29 | echo "AUTO_SYNC=$DEFAULT_AUTO_SYNC" >> "$CONFIG_FILE" 30 | echo "BACKUP_DIR=$DEFAULT_BACKUP_DIR" >> "$CONFIG_FILE" 31 | echo "CREATED=$(date)" >> "$CONFIG_FILE" 32 | 33 | mkdir -p "$DEFAULT_BACKUP_DIR" 34 | echo -e "${GREEN}✓${NC} Configuration initialized at $CONFIG_FILE" 35 | fi 36 | 37 | # Load configuration 38 | source "$CONFIG_FILE" 39 | } 40 | 41 | # Load current session state 42 | load_session() { 43 | if [[ -f "$SESSION_FILE" ]]; then 44 | SESSION_ACTIVE=$(jq -r '.active // false' "$SESSION_FILE" 2>/dev/null || echo "false") 45 | SESSION_PROJECT=$(jq -r '.project // "claude-session"' "$SESSION_FILE" 2>/dev/null || echo "claude-session") 46 | SESSION_START=$(jq -r '.start_time // ""' "$SESSION_FILE" 2>/dev/null || echo "") 47 | else 48 | SESSION_ACTIVE="false" 49 | SESSION_PROJECT="$PROJECT" 50 | SESSION_START="" 51 | fi 52 | } 53 | 54 | # Save session state 55 | save_session() { 56 | local active="$1" 57 | local project="$2" 58 | local start_time="$3" 59 | 60 | cat > "$SESSION_FILE" << EOF 61 | { 62 | "active": $active, 63 | "project": "$project", 64 | "start_time": "$start_time", 65 | "last_sync": "$(date -Iseconds)" 66 | } 67 | EOF 68 | } 69 | 70 | # Start a new session 71 | start_session() { 72 | local project_name="${1:-$PROJECT}" 73 | 74 | init_config 75 | 76 | echo -e "${BLUE}🚀 Starting Claude-TaskWarrior session...${NC}" 77 | 78 | # Create backup 79 | backup_tasks "$project_name" 80 | 81 | # Import existing TaskWarrior tasks 82 | if task project:"$project_name" export &>/dev/null; then 83 | local task_count=$(task project:"$project_name" count 2>/dev/null || echo "0") 84 | if [[ "$task_count" -gt 0 ]]; then 85 | echo -e "${YELLOW}📥 Found $task_count existing tasks in TaskWarrior${NC}" 86 | node "$SYNC_SCRIPT" export "$project_name" > "/tmp/claude-import-$$.json" 87 | echo -e "${GREEN}✓${NC} Exported existing tasks for Claude import" 88 | echo -e "${BLUE}💡 Claude can import these with: TodoWrite$(cat /tmp/claude-import-$$.json)${NC}" 89 | fi 90 | fi 91 | 92 | # Save session state 93 | save_session "true" "$project_name" "$(date -Iseconds)" 94 | 95 | echo -e "${GREEN}✓${NC} Session started for project: $project_name" 96 | echo -e "${BLUE}💡 Use 'claude-tw sync' to sync Claude todos to TaskWarrior${NC}" 97 | echo -e "${BLUE}💡 Use 'claude-tw status' to check session status${NC}" 98 | } 99 | 100 | # Sync Claude todos to TaskWarrior 101 | sync_todos() { 102 | load_session 103 | 104 | if [[ "$SESSION_ACTIVE" != "true" ]]; then 105 | echo -e "${RED}❌ No active session. Start one with: claude-tw start${NC}" 106 | exit 1 107 | fi 108 | 109 | local todos_json="$1" 110 | 111 | if [[ -z "$todos_json" ]]; then 112 | echo -e "${YELLOW}⚠️ No todos provided. Usage: claude-tw sync '[{\"id\":\"...\"}]'${NC}" 113 | exit 1 114 | fi 115 | 116 | echo -e "${BLUE}🔄 Syncing todos to TaskWarrior...${NC}" 117 | 118 | # Backup before sync 119 | backup_tasks "$SESSION_PROJECT" 120 | 121 | # Clear existing project tasks and import new ones 122 | if task project:"$SESSION_PROJECT" count &>/dev/null && [[ $(task project:"$SESSION_PROJECT" count) -gt 0 ]]; then 123 | echo "yes" | task project:"$SESSION_PROJECT" delete &>/dev/null 124 | fi 125 | 126 | # Import new todos 127 | node "$SYNC_SCRIPT" import "$SESSION_PROJECT" "$todos_json" 128 | 129 | local task_count=$(task project:"$SESSION_PROJECT" count 2>/dev/null || echo "0") 130 | echo -e "${GREEN}✓${NC} Synced $task_count tasks to TaskWarrior project: $SESSION_PROJECT" 131 | 132 | # Update session 133 | save_session "true" "$SESSION_PROJECT" "$SESSION_START" 134 | } 135 | 136 | # Export TaskWarrior tasks for Claude 137 | export_todos() { 138 | load_session 139 | 140 | if [[ "$SESSION_ACTIVE" != "true" ]]; then 141 | echo -e "${RED}❌ No active session. Start one with: claude-tw start${NC}" 142 | exit 1 143 | fi 144 | 145 | local task_count=$(task project:"$SESSION_PROJECT" count 2>/dev/null || echo "0") 146 | 147 | if [[ "$task_count" -eq 0 ]]; then 148 | echo -e "${YELLOW}⚠️ No tasks found in project: $SESSION_PROJECT${NC}" 149 | echo "[]" 150 | exit 0 151 | fi 152 | 153 | echo -e "${BLUE}📤 Exporting $task_count tasks from TaskWarrior...${NC}" >&2 154 | node "$SYNC_SCRIPT" export "$SESSION_PROJECT" 155 | } 156 | 157 | # Show session status 158 | show_status() { 159 | load_session 160 | 161 | echo -e "${BLUE}📊 Claude-TaskWarrior Status${NC}" 162 | echo "==================================" 163 | 164 | if [[ "$SESSION_ACTIVE" == "true" ]]; then 165 | echo -e "Status: ${GREEN}Active${NC}" 166 | echo "Project: $SESSION_PROJECT" 167 | echo "Started: $SESSION_START" 168 | 169 | local task_count=$(task project:"$SESSION_PROJECT" count 2>/dev/null || echo "0") 170 | local pending_count=$(task project:"$SESSION_PROJECT" status:pending count 2>/dev/null || echo "0") 171 | local completed_count=$(task project:"$SESSION_PROJECT" status:completed count 2>/dev/null || echo "0") 172 | 173 | echo "Tasks: $task_count total, $pending_count pending, $completed_count completed" 174 | 175 | if [[ "$task_count" -gt 0 ]]; then 176 | echo "" 177 | echo -e "${BLUE}Recent tasks:${NC}" 178 | task project:"$SESSION_PROJECT" limit:5 2>/dev/null || echo "No tasks found" 179 | fi 180 | else 181 | echo -e "Status: ${YELLOW}Inactive${NC}" 182 | echo "Use 'claude-tw start' to begin a new session" 183 | fi 184 | } 185 | 186 | # Create backup of current tasks 187 | backup_tasks() { 188 | local project_name="$1" 189 | local backup_file="$BACKUP_DIR/backup-$(date +%Y%m%d-%H%M%S).json" 190 | 191 | if task project:"$project_name" export &>/dev/null; then 192 | task project:"$project_name" export > "$backup_file" 2>/dev/null 193 | echo -e "${GREEN}✓${NC} Backup created: $backup_file" 194 | fi 195 | } 196 | 197 | # End current session 198 | end_session() { 199 | load_session 200 | 201 | if [[ "$SESSION_ACTIVE" != "true" ]]; then 202 | echo -e "${YELLOW}⚠️ No active session to end${NC}" 203 | exit 0 204 | fi 205 | 206 | echo -e "${BLUE}🏁 Ending Claude-TaskWarrior session...${NC}" 207 | 208 | # Final backup 209 | backup_tasks "$SESSION_PROJECT" 210 | 211 | # Show final summary 212 | local task_count=$(task project:"$SESSION_PROJECT" count 2>/dev/null || echo "0") 213 | local completed_count=$(task project:"$SESSION_PROJECT" status:completed count 2>/dev/null || echo "0") 214 | 215 | echo -e "${GREEN}✓${NC} Session completed" 216 | echo "Final stats: $completed_count of $task_count tasks completed" 217 | 218 | # Clear session 219 | rm -f "$SESSION_FILE" 220 | } 221 | 222 | # Show help 223 | show_help() { 224 | echo "Claude-TaskWarrior Integration CLI" 225 | echo "" 226 | echo "USAGE:" 227 | echo " claude-tw [options]" 228 | echo "" 229 | echo "COMMANDS:" 230 | echo " start [project] Start new session (default project: claude-session)" 231 | echo " sync '' Sync Claude todos to TaskWarrior" 232 | echo " export Export TaskWarrior tasks for Claude import" 233 | echo " status Show current session status" 234 | echo " end End current session" 235 | echo " backup Create manual backup" 236 | echo " config Show configuration" 237 | echo " help Show this help" 238 | echo "" 239 | echo "EXAMPLES:" 240 | echo " claude-tw start # Start session with default project" 241 | echo " claude-tw start chrome-launch # Start session with custom project" 242 | echo " claude-tw sync '[{\"id\":\"1\",\"content\":\"Task\"}]' # Sync todos" 243 | echo " claude-tw export # Export for Claude" 244 | echo "" 245 | echo "FILES:" 246 | echo " Config: $CONFIG_FILE" 247 | echo " Session: $SESSION_FILE" 248 | echo " Backups: $BACKUP_DIR" 249 | } 250 | 251 | # Main command dispatcher 252 | main() { 253 | case "$1" in 254 | start) 255 | start_session "$2" 256 | ;; 257 | sync) 258 | sync_todos "$2" 259 | ;; 260 | export) 261 | export_todos 262 | ;; 263 | status) 264 | show_status 265 | ;; 266 | end) 267 | end_session 268 | ;; 269 | backup) 270 | load_session 271 | backup_tasks "${SESSION_PROJECT:-$DEFAULT_PROJECT}" 272 | ;; 273 | config) 274 | init_config 275 | echo "Configuration file: $CONFIG_FILE" 276 | cat "$CONFIG_FILE" 277 | ;; 278 | help|--help|-h) 279 | show_help 280 | ;; 281 | *) 282 | echo -e "${RED}❌ Unknown command: $1${NC}" 283 | echo "Use 'claude-tw help' for usage information" 284 | exit 1 285 | ;; 286 | esac 287 | } 288 | 289 | # Check dependencies 290 | check_dependencies() { 291 | local missing_deps=() 292 | 293 | if ! command -v task &> /dev/null; then 294 | missing_deps+=("taskwarrior") 295 | fi 296 | 297 | if ! command -v node &> /dev/null; then 298 | missing_deps+=("nodejs") 299 | fi 300 | 301 | if ! command -v jq &> /dev/null; then 302 | missing_deps+=("jq") 303 | fi 304 | 305 | if [[ ${#missing_deps[@]} -gt 0 ]]; then 306 | echo -e "${RED}❌ Missing dependencies: ${missing_deps[*]}${NC}" 307 | echo "Install them with: sudo apt install ${missing_deps[*]}" 308 | exit 1 309 | fi 310 | } 311 | 312 | # Initialize 313 | check_dependencies 314 | init_config 315 | 316 | # Run main function 317 | main "$@" -------------------------------------------------------------------------------- /sync.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Claude-TaskWarrior Sync Engine 5 | * Advanced synchronization between Claude's TodoWrite tool and TaskWarrior 6 | * 7 | * Features: 8 | * - Bidirectional sync 9 | * - Conflict resolution 10 | * - Data validation 11 | * - Backup integration 12 | * - Multi-project support 13 | */ 14 | 15 | const { execSync } = require('child_process'); 16 | const fs = require('fs'); 17 | const path = require('path'); 18 | const crypto = require('crypto'); 19 | 20 | class ClaudeTaskWarriorSync { 21 | constructor() { 22 | this.defaultProject = 'claude-session'; 23 | this.backupDir = process.env.HOME + '/.claude-taskwarrior-backups'; 24 | this.configFile = process.env.HOME + '/.claude-taskwarrior.conf'; 25 | 26 | this.ensureBackupDir(); 27 | this.loadConfig(); 28 | } 29 | 30 | ensureBackupDir() { 31 | if (!fs.existsSync(this.backupDir)) { 32 | fs.mkdirSync(this.backupDir, { recursive: true }); 33 | } 34 | } 35 | 36 | loadConfig() { 37 | if (fs.existsSync(this.configFile)) { 38 | const config = fs.readFileSync(this.configFile, 'utf8'); 39 | const lines = config.split('\n'); 40 | 41 | for (const line of lines) { 42 | if (line.startsWith('PROJECT=')) { 43 | this.defaultProject = line.split('=')[1]; 44 | } 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * Import Claude todos to TaskWarrior 51 | */ 52 | importTodos(projectName, todosJson) { 53 | const project = projectName || this.defaultProject; 54 | 55 | try { 56 | const todos = JSON.parse(todosJson); 57 | console.log(`🔄 Importing ${todos.length} todos to TaskWarrior project: ${project}`); 58 | 59 | // Validate todos structure 60 | this.validateTodos(todos); 61 | 62 | // Create backup before import 63 | this.createBackup(project, 'pre-import'); 64 | 65 | let importedCount = 0; 66 | let errorCount = 0; 67 | 68 | for (const todo of todos) { 69 | try { 70 | const taskId = this.addTaskToWarrior(todo, project); 71 | 72 | if (taskId && todo.status === 'completed') { 73 | this.markTaskCompleted(taskId); 74 | } else if (taskId && todo.status === 'in_progress') { 75 | this.markTaskStarted(taskId); 76 | } 77 | 78 | importedCount++; 79 | console.log(` ✓ ${todo.content}`); 80 | 81 | } catch (error) { 82 | errorCount++; 83 | console.error(` ✗ Failed: ${todo.content} (${error.message})`); 84 | } 85 | } 86 | 87 | console.log(`\n📊 Import Summary:`); 88 | console.log(` Imported: ${importedCount}`); 89 | console.log(` Errors: ${errorCount}`); 90 | console.log(` Project: ${project}`); 91 | 92 | return { imported: importedCount, errors: errorCount }; 93 | 94 | } catch (error) { 95 | console.error(`❌ Import failed: ${error.message}`); 96 | throw error; 97 | } 98 | } 99 | 100 | /** 101 | * Export TaskWarrior tasks to Claude format 102 | */ 103 | exportTodos(projectName) { 104 | const project = projectName || this.defaultProject; 105 | 106 | try { 107 | // Check if project has tasks 108 | const taskCount = this.getTaskCount(project); 109 | 110 | if (taskCount === 0) { 111 | return JSON.stringify([], null, 2); 112 | } 113 | 114 | // Export tasks from TaskWarrior 115 | const result = execSync(`task project:${project} export`, { encoding: 'utf8' }); 116 | const tasks = JSON.parse(result); 117 | 118 | // Convert to Claude format 119 | const todos = tasks.map(task => this.convertTaskToTodo(task)); 120 | 121 | // Sort by priority and status 122 | todos.sort((a, b) => { 123 | const priorityOrder = { 'high': 0, 'medium': 1, 'low': 2 }; 124 | const statusOrder = { 'in_progress': 0, 'pending': 1, 'completed': 2 }; 125 | 126 | // First by status, then by priority 127 | const statusDiff = statusOrder[a.status] - statusOrder[b.status]; 128 | if (statusDiff !== 0) return statusDiff; 129 | 130 | return priorityOrder[a.priority] - priorityOrder[b.priority]; 131 | }); 132 | 133 | console.error(`📤 Exported ${todos.length} tasks from project: ${project}`); 134 | return JSON.stringify(todos, null, 2); 135 | 136 | } catch (error) { 137 | console.error(`❌ Export failed: ${error.message}`); 138 | return JSON.stringify([], null, 2); 139 | } 140 | } 141 | 142 | /** 143 | * Add a single task to TaskWarrior 144 | */ 145 | addTaskToWarrior(todo, project) { 146 | const priority = this.mapPriority(todo.priority); 147 | const description = todo.content.replace(/"/g, '\\"'); 148 | 149 | // Build task command 150 | let cmd = `task add project:${project} priority:${priority}`; 151 | 152 | // Add tags if any 153 | if (todo.tags && Array.isArray(todo.tags)) { 154 | cmd += ` +${todo.tags.join(' +')}`; 155 | } 156 | 157 | // Add due date if specified 158 | if (todo.due) { 159 | cmd += ` due:${todo.due}`; 160 | } 161 | 162 | cmd += ` "${description}"`; 163 | 164 | try { 165 | const result = execSync(cmd, { encoding: 'utf8' }); 166 | const match = result.match(/Created task (\d+)/); 167 | return match ? match[1] : null; 168 | } catch (error) { 169 | throw new Error(`TaskWarrior add failed: ${error.message}`); 170 | } 171 | } 172 | 173 | /** 174 | * Mark task as completed 175 | */ 176 | markTaskCompleted(taskId) { 177 | try { 178 | execSync(`echo "yes" | task ${taskId} done`, { encoding: 'utf8' }); 179 | } catch (error) { 180 | console.warn(`Could not mark task ${taskId} as completed: ${error.message}`); 181 | } 182 | } 183 | 184 | /** 185 | * Mark task as started 186 | */ 187 | markTaskStarted(taskId) { 188 | try { 189 | execSync(`task ${taskId} start`, { encoding: 'utf8' }); 190 | } catch (error) { 191 | console.warn(`Could not start task ${taskId}: ${error.message}`); 192 | } 193 | } 194 | 195 | /** 196 | * Convert TaskWarrior task to Claude todo format 197 | */ 198 | convertTaskToTodo(task) { 199 | return { 200 | id: `tw-${task.uuid.slice(0, 8)}`, 201 | content: task.description, 202 | status: this.mapTaskStatus(task.status, task.start), 203 | priority: this.unmapPriority(task.priority), 204 | created: task.entry ? new Date(task.entry).toISOString() : undefined, 205 | modified: task.modified ? new Date(task.modified).toISOString() : undefined, 206 | due: task.due ? new Date(task.due).toISOString().split('T')[0] : undefined, 207 | tags: task.tags || undefined 208 | }; 209 | } 210 | 211 | /** 212 | * Map Claude priority to TaskWarrior priority 213 | */ 214 | mapPriority(claudePriority) { 215 | const mapping = { 216 | 'high': 'H', 217 | 'medium': 'M', 218 | 'low': 'L' 219 | }; 220 | return mapping[claudePriority] || 'M'; 221 | } 222 | 223 | /** 224 | * Map TaskWarrior priority to Claude priority 225 | */ 226 | unmapPriority(taskPriority) { 227 | const mapping = { 228 | 'H': 'high', 229 | 'M': 'medium', 230 | 'L': 'low' 231 | }; 232 | return mapping[taskPriority] || 'medium'; 233 | } 234 | 235 | /** 236 | * Map TaskWarrior status to Claude status 237 | */ 238 | mapTaskStatus(taskStatus, startTime) { 239 | if (taskStatus === 'completed') return 'completed'; 240 | if (taskStatus === 'pending' && startTime) return 'in_progress'; 241 | return 'pending'; 242 | } 243 | 244 | /** 245 | * Get task count for a project 246 | */ 247 | getTaskCount(project) { 248 | try { 249 | const result = execSync(`task project:${project} count`, { encoding: 'utf8' }); 250 | return parseInt(result.trim()) || 0; 251 | } catch (error) { 252 | return 0; 253 | } 254 | } 255 | 256 | /** 257 | * Create backup of current tasks 258 | */ 259 | createBackup(project, suffix = '') { 260 | try { 261 | const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); 262 | const filename = `backup-${project}-${timestamp}${suffix ? '-' + suffix : ''}.json`; 263 | const backupPath = path.join(this.backupDir, filename); 264 | 265 | const result = execSync(`task project:${project} export`, { encoding: 'utf8' }); 266 | fs.writeFileSync(backupPath, result); 267 | 268 | console.log(`💾 Backup created: ${filename}`); 269 | return backupPath; 270 | } catch (error) { 271 | console.warn(`⚠️ Backup failed: ${error.message}`); 272 | return null; 273 | } 274 | } 275 | 276 | /** 277 | * Validate todos structure 278 | */ 279 | validateTodos(todos) { 280 | if (!Array.isArray(todos)) { 281 | throw new Error('Todos must be an array'); 282 | } 283 | 284 | for (const [index, todo] of todos.entries()) { 285 | if (!todo.content || typeof todo.content !== 'string') { 286 | throw new Error(`Todo ${index}: content is required and must be a string`); 287 | } 288 | 289 | if (!todo.status || !['pending', 'in_progress', 'completed'].includes(todo.status)) { 290 | throw new Error(`Todo ${index}: status must be pending, in_progress, or completed`); 291 | } 292 | 293 | if (!todo.priority || !['high', 'medium', 'low'].includes(todo.priority)) { 294 | throw new Error(`Todo ${index}: priority must be high, medium, or low`); 295 | } 296 | } 297 | } 298 | 299 | /** 300 | * Get summary statistics 301 | */ 302 | getSummary(projectName) { 303 | const project = projectName || this.defaultProject; 304 | 305 | try { 306 | const pending = this.getTaskCount(project + ' status:pending'); 307 | const completed = this.getTaskCount(project + ' status:completed'); 308 | const inProgress = execSync(`task project:${project} +ACTIVE count`, { encoding: 'utf8' }).trim(); 309 | 310 | return { 311 | project, 312 | total: pending + completed + parseInt(inProgress || 0), 313 | pending, 314 | completed, 315 | in_progress: parseInt(inProgress || 0) 316 | }; 317 | } catch (error) { 318 | return { 319 | project, 320 | total: 0, 321 | pending: 0, 322 | completed: 0, 323 | in_progress: 0, 324 | error: error.message 325 | }; 326 | } 327 | } 328 | } 329 | 330 | // CLI interface 331 | if (require.main === module) { 332 | const sync = new ClaudeTaskWarriorSync(); 333 | const command = process.argv[2]; 334 | const projectName = process.argv[3]; 335 | const data = process.argv[4] || process.argv[3]; // Support both project+data and just data 336 | 337 | try { 338 | switch (command) { 339 | case 'import': 340 | if (!data || (!projectName && !data.startsWith('['))) { 341 | console.error('Usage: node sync.js import [project] \'[{"id":"...","content":"..."}]\''); 342 | process.exit(1); 343 | } 344 | 345 | const importProject = projectName && !projectName.startsWith('[') ? projectName : null; 346 | const importData = importProject ? data : projectName; 347 | 348 | sync.importTodos(importProject, importData); 349 | break; 350 | 351 | case 'export': 352 | console.log(sync.exportTodos(projectName)); 353 | break; 354 | 355 | case 'summary': 356 | const summary = sync.getSummary(projectName); 357 | console.log(JSON.stringify(summary, null, 2)); 358 | break; 359 | 360 | case 'backup': 361 | sync.createBackup(projectName || sync.defaultProject, 'manual'); 362 | break; 363 | 364 | default: 365 | console.log('Claude-TaskWarrior Sync Engine'); 366 | console.log('Usage: node sync.js [import|export|summary|backup] [project] [data]'); 367 | console.log(''); 368 | console.log('Commands:'); 369 | console.log(' import [project] \'[json]\' Import Claude todos to TaskWarrior'); 370 | console.log(' export [project] Export TaskWarrior tasks to Claude format'); 371 | console.log(' summary [project] Show project statistics'); 372 | console.log(' backup [project] Create manual backup'); 373 | process.exit(1); 374 | } 375 | } catch (error) { 376 | console.error(`❌ Error: ${error.message}`); 377 | process.exit(1); 378 | } 379 | } 380 | 381 | module.exports = ClaudeTaskWarriorSync; --------------------------------------------------------------------------------