diff --git a/reproduce_issue.py b/reproduce_issue.py deleted file mode 100644 index 51d3066..0000000 --- a/reproduce_issue.py +++ /dev/null @@ -1,99 +0,0 @@ -# Root Cause Analysis -# The issue was a bidirectional CRLF incompatibility between Windows Python and the MCP Client (Antigravity/Claude). - -# Input (Client -> Server): Windows terminals send \r\n (CRLF) as a newline. The MCP server's JSON parser choked on the \r character, causing the initial "invalid trailing data" error. -# Output (Server -> Client): Even after fixing the input, the server (running on Windows) was printing \r\n to stdout by default. The MCP Client also choked on the \r in the response, causing the same error message to persist. -# Comparison with main branch: The main branch uses standard sys.stdin and sys.stdout, which on Windows automatically translate \n to \r\n. The fix intercepts both streams at the binary buffer level to enforce Unix-style \n (LF) only, ensuring compatibility regardless of the platform. - -# Cleanup & Integration -# I have: - -# Updated tests/test_windows_compat.py to include regression tests for both Input (stripping \r) and Output (enforcing \n). -# Deleted all temporary reproduction scripts (reproduce_issue.py, test_anyio.py, etc.). -# Pushed the final clean code to the fix/windows-crlf-issue branch. -# You can now merge this branch into main. - -import subprocess -import sys -import json -import time - -def run_reproduction(): - # Command to run the server as a module - cmd = [sys.executable, "-m", "code_trajectory.server"] - - # Start the server process - process = subprocess.Popen( - cmd, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=False, # Binary mode - bufsize=0 # Unbuffered - ) - - # JSON-RPC initialization message - init_msg = { - "jsonrpc": "2.0", - "id": 1, - "method": "initialize", - "params": { - "protocolVersion": "2024-11-05", - "capabilities": {}, - "clientInfo": {"name": "test-client", "version": "1.0"} - } - } - - # Serialize to JSON and append CRLF explicitly - json_str = json.dumps(init_msg) - # Windows style line ending encoded to utf-8 - input_bytes = (json_str + "\r\n").encode('utf-8') - - print(f"Sending bytes: {input_bytes}") - - try: - # Assert streams are available for type checkers - assert process.stdin is not None - assert process.stdout is not None - assert process.stderr is not None - - process.stdin.write(input_bytes) - process.stdin.flush() - - # Read response with timeout - start_time = time.time() - while time.time() - start_time < 5: - if process.poll() is not None: - print(f"Process exited prematurely with code {process.returncode}") - stdout, stderr = process.communicate() - print("STDOUT:", stdout.decode('utf-8', errors='replace')) - print("STDERR:", stderr.decode('utf-8', errors='replace')) - return - - line = process.stdout.readline() - if line: - print(f"Received bytes: {line}") - print(f"Received: {line.decode('utf-8', errors='replace').strip()}") - if b"jsonrpc" in line: - if b'\r\n' in line: - print("WARNING: Response contains CRLF (\\r\\n) - FIX NOT WORKING") - else: - print("INFO: Response contains LF (\\n) only - FIX WORKING") - - print("SUCCESS: Received JSON-RPC response.") - process.terminate() - return - time.sleep(0.1) - - print("TIMEOUT: No response received within 5 seconds.") - process.terminate() - stdout, stderr = process.communicate() - print("STDOUT:", stdout.decode('utf-8', errors='replace')) - print("STDERR:", stderr.decode('utf-8', errors='replace')) - - except Exception as e: - print(f"Exception: {e}") - process.kill() - -if __name__ == "__main__": - run_reproduction() diff --git a/src/code_trajectory/server.py b/src/code_trajectory/server.py index 5ebbb70..4f73a02 100644 --- a/src/code_trajectory/server.py +++ b/src/code_trajectory/server.py @@ -65,6 +65,9 @@ def _initialize_components(path: str) -> str: if state.watcher: state.watcher.stop() + # Check if this is a new initialization before creating the recorder (which creates the repo) + is_new_initialization = not os.path.exists(shadow_repo_path) + try: state.recorder = Recorder(target_path) state.watcher = Watcher(target_path, state.recorder) @@ -73,6 +76,12 @@ def _initialize_components(path: str) -> str: state.watcher.start() logger.info(f"Initialized components for {target_path}") + + if is_new_initialization: + return ( + "New project initialized. No history available yet. " + "Do NOT call get_session_summary." + ) return f"Successfully configured to track: {target_path}" except Exception as e: logger.error(f"Failed to initialize components: {e}") diff --git a/src/code_trajectory/trajectory.py b/src/code_trajectory/trajectory.py index 391407a..a6b0052 100644 --- a/src/code_trajectory/trajectory.py +++ b/src/code_trajectory/trajectory.py @@ -126,6 +126,20 @@ def get_global_trajectory(self, limit: int = 20, since_consolidate: bool = False commits = list(self.recorder.repo.iter_commits(max_count=limit)) except Exception as e: + # Check for empty repo error (gitpython usually raises ValueError or GitCommandError) + error_msg = str(e) + # More specific checks for empty repository states + if "Reference at 'refs/heads/master' does not exist" in error_msg: + return "No history available" + + # Check for GitCommandError that indicates no commits (git log fails) + if hasattr(e, 'stderr') and "does not have any commits yet" in str(e.stderr): + return "No history available" + + # Check for BadObject (happens when HEAD is invalid) + if "BadObject" in error_msg and "HEAD" in error_msg: + return "No history available" + logger.error(f"Failed to fetch global trajectory: {e}") return f"Error fetching global trajectory: {e}" @@ -166,6 +180,10 @@ def get_session_summary(self) -> str: timestamps = [int(ts) for ts in timestamps_output.splitlines()] except Exception as e: + error_msg = str(e) + if "does not have any commits yet" in error_msg: + return "No history available" + logger.error(f"Failed to fetch commit timestamps: {e}") return f"Error analyzing session history: {e}" diff --git a/tests/test_server_config.py b/tests/test_server_config.py index 783934e..dd553ee 100644 --- a/tests/test_server_config.py +++ b/tests/test_server_config.py @@ -12,7 +12,8 @@ def test_explicit_configuration(temp_project_dir): result = configure_project(temp_project_dir) - assert "Successfully configured" in result + # Check for either success message (existing) or new init message + assert "Successfully configured" in result or "New project initialized" in result assert state.project_path == temp_project_dir assert state.recorder is not None assert state.recorder.project_root == temp_project_dir @@ -47,7 +48,7 @@ def test_reconfiguration(temp_project_dir): # Configure second project result = configure_project(second_dir) - assert "Successfully configured" in result + assert "Successfully configured" in result or "New project initialized" in result assert state.project_path == second_dir assert state.watcher != old_watcher