diff --git a/architecture_boundaries.md b/architecture_boundaries.md new file mode 100644 index 0000000..d7a41d3 --- /dev/null +++ b/architecture_boundaries.md @@ -0,0 +1,146 @@ +# FileKitty Architecture Boundaries: Python vs Swift + +## Overview +This document defines the clear separation of responsibilities between Python backend and Swift UI for FileKitty's hybrid architecture. + +## Python Backend Responsibilities + +### Core Logic (āœ… Stays in Python) +- **File Processing**: Reading, parsing, and content extraction +- **Tree Generation**: Directory traversal and markdown tree creation +- **Python Code Analysis**: AST parsing for classes/functions +- **Content Aggregation**: Combining files into markdown output +- **Hash Calculation**: File integrity and staleness detection +- **Language Detection**: File type and syntax highlighting determination +- **Project Root Detection**: Finding repository/project boundaries + +### Data Management (āœ… Stays in Python) +- **Session Serialization**: JSON conversion and file I/O +- **History Management**: State persistence and navigation +- **Settings Storage**: Configuration and preferences +- **File Metadata**: Size, modification dates, permissions +- **Error Handling**: File access and processing errors + +### Business Logic (āœ… Stays in Python) +- **Output Filtering**: Ignore patterns and file exclusion +- **Content Formatting**: Markdown generation and styling +- **Selection Logic**: Class/function filtering +- **Path Normalization**: Display paths and project-relative paths +- **Validation**: Input sanitization and security checks + +## Swift UI Responsibilities + +### User Interface (šŸ”„ Migrates to Swift) +- **File List Display**: Tree view of selected files +- **File Selection**: Drag & drop, file picker dialogs +- **Text Display**: Markdown rendering and syntax highlighting +- **Navigation**: History back/forward buttons +- **Settings UI**: Preferences and configuration panels +- **Progress Indicators**: Loading states and operation feedback + +### User Interactions (šŸ”„ Migrates to Swift) +- **Drag & Drop**: File and folder handling +- **Context Menus**: Right-click actions +- **Keyboard Shortcuts**: Navigation and actions +- **Copy/Paste**: Clipboard operations +- **Export/Save**: File output operations +- **Search/Filter**: UI-level filtering and navigation + +### Platform Integration (šŸ”„ Migrates to Swift) +- **macOS Integration**: Dock, menu bar, notifications +- **File System Access**: Sandboxed file operations +- **Window Management**: Sizing, positioning, multi-window +- **Accessibility**: VoiceOver and assistive technologies +- **Dark Mode**: Theme and appearance handling + +## Shared/Bridge Layer + +### Data Exchange (šŸ”„ New Implementation) +- **JSON Communication**: Request/response handling +- **Session Management**: State synchronization +- **Error Propagation**: Python errors to Swift UI +- **Progress Updates**: Long-running operation status +- **Validation**: Input/output data verification + +### CLI Interface (šŸ”„ New Implementation) +- **Command Processing**: Python CLI entry point +- **Argument Parsing**: Swift → Python parameter passing +- **Output Formatting**: Structured JSON responses +- **Error Handling**: Consistent error format +- **Process Management**: Python subprocess lifecycle + +## Migration Strategy + +### Phase 1: Foundation +1. **Keep Python Logic Intact**: No changes to core processing +2. **Create CLI Interface**: New command-line entry point +3. **Implement Data Models**: PromptSession and supporting classes +4. **Define JSON Schema**: Request/response contracts + +### Phase 2: Bridge Implementation +1. **Python CLI Module**: Handle Swift → Python communication +2. **Swift HTTP/Process Client**: Handle Python subprocess calls +3. **Data Serialization**: JSON encoding/decoding on both sides +4. **Error Handling**: Consistent error propagation + +### Phase 3: UI Migration +1. **File List View**: Replace PyQt5 QListWidget +2. **Text Display**: Replace PyQt5 QTextEdit +3. **Menu/Toolbar**: Replace PyQt5 menus +4. **Settings Dialog**: Replace PyQt5 preferences + +### Phase 4: Polish +1. **Platform Features**: macOS-specific integration +2. **Performance**: Optimize Swift ↔ Python communication +3. **Testing**: Comprehensive integration testing +4. **Documentation**: User guides and API docs + +### Phase 5: Packaging & Release +1. **Swift Build**: `swift build -c release` for optimized binary +2. **Code Signing**: Sign SwiftUI app with developer certificate +3. **DMG Creation**: `create-dmg` with custom background and layout +4. **Notarization**: Submit to Apple for security approval +5. **GitHub Release**: `gh release create v0-swift-alpha` with signed DMG +6. **Release Notes**: Document new SwiftUI features and migration notes + +## Key Principles + +### 1. Minimal Python Changes +- Keep existing Python logic unchanged where possible +- Add new CLI interface without breaking existing code +- Maintain backward compatibility with current features + +### 2. Clear Separation +- Python: Data processing, business logic, file operations +- Swift: UI, user interaction, platform integration +- Bridge: Communication, serialization, error handling + +### 3. Incremental Migration +- Start with basic file processing +- Add features incrementally +- Maintain working application at each step + +### 4. Data Integrity +- Consistent data models between Python and Swift +- Comprehensive validation and error handling +- Atomic operations where possible + +## Benefits of This Architecture + +### For Python +- **Leverage Existing Code**: Minimal rewrite required +- **Maintain Complexity**: Keep sophisticated logic in Python +- **Cross-Platform**: Python backend remains portable +- **Testing**: Existing Python tests continue to work + +### For Swift +- **Native Performance**: Fast, responsive UI +- **Platform Integration**: Full macOS feature support +- **Modern UX**: SwiftUI declarative interface +- **Maintainability**: Type-safe, compiled UI layer + +### For Users +- **Better Performance**: Native Swift UI responsiveness +- **Native Feel**: macOS-standard interface patterns +- **Enhanced Features**: Platform-specific capabilities +- **Reliability**: Compiled UI with runtime Python backend \ No newline at end of file diff --git a/example_payloads.json b/example_payloads.json new file mode 100644 index 0000000..93cfed4 --- /dev/null +++ b/example_payloads.json @@ -0,0 +1,242 @@ +{ + "examples": { + "process_files_request": { + "action": "process_files", + "session_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2025-07-05T14:30:00Z", + "payload": { + "files": [ + "/Users/rob/code/FileKitty/src/filekitty/models.py", + "/Users/rob/code/FileKitty/src/filekitty/app_logic.py" + ], + "selection_state": { + "mode": "All Files", + "selected_file": null, + "selected_items": [] + }, + "settings": { + "include_tree": true, + "tree_base_dir": "/Users/rob/code/FileKitty", + "tree_ignore_regex": "\\.git|\\.pyc|\\.DS_Store", + "include_date_modified": true, + "auto_copy": false, + "use_llm_timestamp": false + } + } + }, + "process_files_response": { + "action": "process_files", + "session_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2025-07-05T14:30:02Z", + "success": true, + "payload": { + "prompt_session": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2025-07-05T14:30:00Z", + "files": [ + "/Users/rob/code/FileKitty/src/filekitty/models.py", + "/Users/rob/code/FileKitty/src/filekitty/app_logic.py" + ], + "file_metadata": [ + { + "path": "/Users/rob/code/FileKitty/src/filekitty/models.py", + "display_path": "src/filekitty/models.py", + "is_text_file": true, + "language": "python", + "last_modified": "2025-07-05T14:25:00Z", + "file_hash": "abc123def456789012345678901234567890abcdef123456789012345678901234", + "size_bytes": 5432 + }, + { + "path": "/Users/rob/code/FileKitty/src/filekitty/app_logic.py", + "display_path": "src/filekitty/app_logic.py", + "is_text_file": true, + "language": "python", + "last_modified": "2025-07-05T12:00:00Z", + "file_hash": "def456789012345678901234567890abcdef123456789012345678901234567890", + "size_bytes": 2048 + } + ], + "selection_state": { + "mode": "All Files", + "selected_file": null, + "selected_items": [] + }, + "project_root": "/Users/rob/code/FileKitty", + "tree_snapshot": { + "base_path": "/Users/rob/code/FileKitty", + "base_path_display": "FileKitty/", + "ignore_regex": "\\.git|\\.pyc|\\.DS_Store", + "rendered": "# Folder Tree of FileKitty/\\n\\n```text\\nFileKitty/\\nā”œā”€ā”€ src/\\n│ └── filekitty/\\n│ ā”œā”€ā”€ models.py\\n│ ā”œā”€ā”€ app_logic.py\\n│ └── __init__.py\\nā”œā”€ā”€ README.md\\n└── pyproject.toml\\n```" + }, + "output_text": "# Folder Tree of FileKitty/\\n\\n```text\\nFileKitty/\\nā”œā”€ā”€ src/\\n│ └── filekitty/\\n│ ā”œā”€ā”€ models.py\\n│ ā”œā”€ā”€ app_logic.py\\n│ └── __init__.py\\nā”œā”€ā”€ README.md\\n└── pyproject.toml\\n```\\n\\n# src/filekitty/models.py\\n**Last modified: Jul 05, 2025 2:25 PM PST**\\n\\n```python\\n\"\"\"Data models for FileKitty Swift integration.\"\"\"\\n\\nimport json\\nfrom dataclasses import dataclass, field\\nfrom datetime import datetime\\nfrom typing import Any, Dict, List, Optional\\n\\n\\n@dataclass\\nclass FileMetadata:\\n \"\"\"Metadata for a single file in the session.\"\"\"\\n path: str\\n display_path: str\\n is_text_file: bool\\n # ... rest of file content\\n```\\n\\n# src/filekitty/app_logic.py\\n**Last modified: Jul 05, 2025 12:00 PM PST**\\n\\n```python\\nimport sys\\nfrom pathlib import Path\\n\\nfrom PyQt5.QtCore import QEvent, Qt\\nfrom PyQt5.QtWidgets import QApplication\\n\\nfrom filekitty.ui.main_window import FilePicker\\n\\n\\nclass FileKittyApp(QApplication):\\n # ... rest of file content\\n```", + "settings": { + "include_tree": true, + "tree_base_dir": "/Users/rob/code/FileKitty", + "tree_ignore_regex": "\\.git|\\.pyc|\\.DS_Store", + "include_date_modified": true, + "auto_copy": false, + "use_llm_timestamp": false + } + } + } + }, + "get_python_symbols_request": { + "action": "get_python_symbols", + "session_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2025-07-05T14:31:00Z", + "payload": { + "files": [ + "/Users/rob/code/FileKitty/src/filekitty/models.py", + "/Users/rob/code/FileKitty/src/filekitty/app_logic.py" + ] + } + }, + "get_python_symbols_response": { + "action": "get_python_symbols", + "session_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2025-07-05T14:31:01Z", + "success": true, + "payload": { + "symbols": { + "/Users/rob/code/FileKitty/src/filekitty/models.py": { + "classes": ["FileMetadata", "TreeSnapshot", "SelectionState", "PromptSession"], + "functions": [] + }, + "/Users/rob/code/FileKitty/src/filekitty/app_logic.py": { + "classes": ["FileKittyApp"], + "functions": ["main"] + } + }, + "errors": [] + } + }, + "update_selection_request": { + "action": "update_selection", + "session_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2025-07-05T14:32:00Z", + "payload": { + "selection_state": { + "mode": "Single File", + "selected_file": "/Users/rob/code/FileKitty/src/filekitty/models.py", + "selected_items": ["PromptSession", "FileMetadata"] + } + } + }, + "update_selection_response": { + "action": "update_selection", + "session_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2025-07-05T14:32:01Z", + "success": true, + "payload": { + "output_text": "# Folder Tree of FileKitty/\\n\\n```text\\nFileKitty/\\nā”œā”€ā”€ src/\\n│ └── filekitty/\\n│ ā”œā”€ā”€ models.py\\n│ ā”œā”€ā”€ app_logic.py\\n│ └── __init__.py\\nā”œā”€ā”€ README.md\\n└── pyproject.toml\\n```\\n\\n# src/filekitty/models.py\\n**Last modified: Jul 05, 2025 2:25 PM PST**\\n\\n```python\\n@dataclass\\nclass FileMetadata:\\n \"\"\"Metadata for a single file in the session.\"\"\"\\n path: str\\n display_path: str\\n is_text_file: bool\\n language: Optional[str] = None\\n last_modified: Optional[datetime] = None\\n file_hash: Optional[str] = None\\n size_bytes: Optional[int] = None\\n\\n\\n@dataclass\\nclass PromptSession:\\n \"\"\"Complete session state for FileKitty.\"\"\"\\n id: str\\n timestamp: datetime\\n files: List[str]\\n file_metadata: List[FileMetadata]\\n selection_state: SelectionState\\n project_root: Optional[str] = None\\n tree_snapshot: Optional[TreeSnapshot] = None\\n output_text: Optional[str] = None\\n settings: Dict[str, Any] = field(default_factory=dict)\\n # ... methods omitted for brevity\\n```" + } + }, + "save_session_request": { + "action": "save_session", + "session_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2025-07-05T14:33:00Z", + "payload": { + "file_path": "/Users/rob/Documents/filekitty_session.json" + } + }, + "save_session_response": { + "action": "save_session", + "session_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2025-07-05T14:33:01Z", + "success": true, + "payload": { + "saved_path": "/Users/rob/Documents/filekitty_session.json" + } + }, + "load_session_request": { + "action": "load_session", + "session_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2025-07-05T14:34:00Z", + "payload": { + "file_path": "/Users/rob/Documents/filekitty_session.json" + } + }, + "load_session_response": { + "action": "load_session", + "session_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2025-07-05T14:34:01Z", + "success": true, + "payload": { + "prompt_session": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2025-07-05T14:30:00Z", + "files": [ + "/Users/rob/code/FileKitty/src/filekitty/models.py", + "/Users/rob/code/FileKitty/src/filekitty/app_logic.py" + ], + "file_metadata": [ + { + "path": "/Users/rob/code/FileKitty/src/filekitty/models.py", + "display_path": "src/filekitty/models.py", + "is_text_file": true, + "language": "python", + "last_modified": "2025-07-05T14:25:00Z", + "file_hash": "abc123def456789012345678901234567890abcdef123456789012345678901234", + "size_bytes": 5432 + } + ], + "selection_state": { + "mode": "Single File", + "selected_file": "/Users/rob/code/FileKitty/src/filekitty/models.py", + "selected_items": ["PromptSession", "FileMetadata"] + }, + "project_root": "/Users/rob/code/FileKitty", + "tree_snapshot": { + "base_path": "/Users/rob/code/FileKitty", + "base_path_display": "FileKitty/", + "ignore_regex": "\\.git|\\.pyc|\\.DS_Store", + "rendered": "# Folder Tree of FileKitty/\\n\\n```text\\nFileKitty/\\nā”œā”€ā”€ src/\\n│ └── filekitty/\\n│ ā”œā”€ā”€ models.py\\n│ ā”œā”€ā”€ app_logic.py\\n│ └── __init__.py\\nā”œā”€ā”€ README.md\\n└── pyproject.toml\\n```" + }, + "output_text": "# Filtered content based on selection...", + "settings": { + "include_tree": true, + "tree_base_dir": "/Users/rob/code/FileKitty", + "tree_ignore_regex": "\\.git|\\.pyc|\\.DS_Store", + "include_date_modified": true, + "auto_copy": false, + "use_llm_timestamp": false + } + } + } + }, + "error_response": { + "action": "process_files", + "session_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2025-07-05T14:35:00Z", + "success": false, + "error": { + "type": "FileNotFoundError", + "message": "One or more files could not be found", + "details": { + "missing_files": [ + "/Users/rob/code/FileKitty/nonexistent.py" + ], + "accessible_files": [ + "/Users/rob/code/FileKitty/src/filekitty/models.py" + ] + } + } + }, + "validation_error_response": { + "action": "update_selection", + "session_id": "invalid-session-id", + "timestamp": "2025-07-05T14:36:00Z", + "success": false, + "error": { + "type": "ValidationError", + "message": "Invalid session ID format", + "details": { + "field": "session_id", + "expected": "Valid UUID string", + "received": "invalid-session-id" + } + } + } + } +} \ No newline at end of file diff --git a/src/filekitty/models.py b/src/filekitty/models.py new file mode 100644 index 0000000..0a98088 --- /dev/null +++ b/src/filekitty/models.py @@ -0,0 +1,163 @@ +"""Data models for FileKitty Swift integration.""" + +import json +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any + + +@dataclass +class FileMetadata: + """Metadata for a single file in the session.""" + + path: str + display_path: str + is_text_file: bool + language: str | None = None + last_modified: datetime | None = None + file_hash: str | None = None + size_bytes: int | None = None + + +@dataclass +class TreeSnapshot: + """File tree snapshot data.""" + + base_path: str + base_path_display: str + ignore_regex: str + rendered: str + + def to_dict(self) -> dict[str, Any]: + return { + "base_path": self.base_path, + "base_path_display": self.base_path_display, + "ignore_regex": self.ignore_regex, + "rendered": self.rendered, + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "TreeSnapshot": + return cls( + base_path=data["base_path"], + base_path_display=data["base_path_display"], + ignore_regex=data["ignore_regex"], + rendered=data["rendered"], + ) + + +@dataclass +class SelectionState: + """Current file and code selection state.""" + + mode: str = "All Files" # "All Files" or "Single File" + selected_file: str | None = None + selected_items: list[str] = field(default_factory=list) # Classes/functions + + +@dataclass +class PromptSession: + """Complete session state for FileKitty.""" + + id: str + timestamp: datetime + files: list[str] + file_metadata: list[FileMetadata] + selection_state: SelectionState + project_root: str | None = None + tree_snapshot: TreeSnapshot | None = None + output_text: str | None = None + settings: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for JSON serialization.""" + return { + "id": self.id, + "timestamp": self.timestamp.isoformat(), + "files": self.files, + "file_metadata": [ + { + "path": fm.path, + "display_path": fm.display_path, + "is_text_file": fm.is_text_file, + "language": fm.language, + "last_modified": fm.last_modified.isoformat() if fm.last_modified else None, + "file_hash": fm.file_hash, + "size_bytes": fm.size_bytes, + } + for fm in self.file_metadata + ], + "selection_state": { + "mode": self.selection_state.mode, + "selected_file": self.selection_state.selected_file, + "selected_items": self.selection_state.selected_items, + }, + "project_root": self.project_root, + "tree_snapshot": self.tree_snapshot.to_dict() if self.tree_snapshot else None, + "output_text": self.output_text, + "settings": self.settings, + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "PromptSession": + """Create from dictionary (JSON deserialization).""" + file_metadata = [] + for fm_data in data.get("file_metadata", []): + last_modified = None + if fm_data.get("last_modified"): + last_modified = datetime.fromisoformat(fm_data["last_modified"]) + + file_metadata.append( + FileMetadata( + path=fm_data["path"], + display_path=fm_data["display_path"], + is_text_file=fm_data["is_text_file"], + language=fm_data.get("language"), + last_modified=last_modified, + file_hash=fm_data.get("file_hash"), + size_bytes=fm_data.get("size_bytes"), + ) + ) + + selection_data = data.get("selection_state", {}) + selection_state = SelectionState( + mode=selection_data.get("mode", "All Files"), + selected_file=selection_data.get("selected_file"), + selected_items=selection_data.get("selected_items", []), + ) + + tree_snapshot = None + if data.get("tree_snapshot"): + tree_snapshot = TreeSnapshot.from_dict(data["tree_snapshot"]) + + return cls( + id=data["id"], + timestamp=datetime.fromisoformat(data["timestamp"]), + files=data["files"], + file_metadata=file_metadata, + selection_state=selection_state, + project_root=data.get("project_root"), + tree_snapshot=tree_snapshot, + output_text=data.get("output_text"), + settings=data.get("settings", {}), + ) + + def to_json(self) -> str: + """Convert to JSON string.""" + return json.dumps(self.to_dict(), indent=2) + + @classmethod + def from_json(cls, json_str: str) -> "PromptSession": + """Create from JSON string.""" + return cls.from_dict(json.loads(json_str)) + + def save_to_file(self, file_path: str) -> None: + """Save session to JSON file.""" + with open(file_path, "w", encoding="utf-8") as f: + f.write(self.to_json()) + + @classmethod + def load_from_file(cls, file_path: str) -> "PromptSession": + """Load session from JSON file.""" + with open(file_path, encoding="utf-8") as f: + return cls.from_json(f.read()) diff --git a/src/tests/test_models.py b/src/tests/test_models.py new file mode 100644 index 0000000..4337419 --- /dev/null +++ b/src/tests/test_models.py @@ -0,0 +1,99 @@ +"""Unit tests for FileKitty data models - ensures JSON schema stability.""" +# ruff: noqa: UP017 + +import json +import uuid +from datetime import datetime, timezone + +from src.filekitty.models import FileMetadata, PromptSession, SelectionState, TreeSnapshot + + +def test_prompt_session_json_round_trip(): + """Test PromptSession to_json → from_json round-trip maintains data integrity.""" + # Create test data with UTC timestamps + session = PromptSession( + id=str(uuid.uuid4()), + timestamp=datetime.now(timezone.utc), + files=["/path/to/file1.py", "/path/to/file2.js"], + file_metadata=[ + FileMetadata( + path="/path/to/file1.py", + display_path="file1.py", + is_text_file=True, + language="python", + last_modified=datetime.now(timezone.utc), + file_hash="abc123", + size_bytes=1024, + ), + FileMetadata( + path="/path/to/file2.js", + display_path="file2.js", + is_text_file=True, + language="javascript", + last_modified=datetime.now(timezone.utc), + file_hash="def456", + size_bytes=512, + ), + ], + selection_state=SelectionState( + mode="Single File", selected_file="/path/to/file1.py", selected_items=["MyClass", "my_function"] + ), + project_root="/path/to", + tree_snapshot=TreeSnapshot( + base_path="/path/to", + base_path_display="project/", + ignore_regex=r"\.git|\.pyc", + rendered="# Tree\n```\nproject/\nā”œā”€ā”€ file1.py\n└── file2.js\n```", + ), + output_text="# Generated output...", + settings={"include_tree": True, "auto_copy": False}, + ) + + # Convert to JSON and back + json_str = session.to_json() + parsed_session = PromptSession.from_json(json_str) + + # Verify round-trip integrity + assert parsed_session.id == session.id + assert parsed_session.timestamp == session.timestamp + assert parsed_session.files == session.files + assert len(parsed_session.file_metadata) == len(session.file_metadata) + assert parsed_session.file_metadata[0].path == session.file_metadata[0].path + assert parsed_session.file_metadata[0].language == session.file_metadata[0].language + assert parsed_session.selection_state.mode == session.selection_state.mode + assert parsed_session.selection_state.selected_items == session.selection_state.selected_items + assert parsed_session.project_root == session.project_root + assert parsed_session.tree_snapshot.base_path == session.tree_snapshot.base_path + assert parsed_session.output_text == session.output_text + assert parsed_session.settings == session.settings + + # Verify JSON contains expected UTC timestamp format + parsed_json = json.loads(json_str) + assert parsed_json["timestamp"].endswith("Z") or "+" in parsed_json["timestamp"] + + print("āœ… PromptSession JSON round-trip test passed") + + +def test_timestamp_utc_format(): + """Test that timestamps are properly formatted with UTC timezone.""" + session = PromptSession( + id="test-id", timestamp=datetime.now(timezone.utc), files=[], file_metadata=[], selection_state=SelectionState() + ) + + json_data = session.to_dict() + timestamp_str = json_data["timestamp"] + + # Should end with Z (UTC) or have timezone offset + assert timestamp_str.endswith("Z") or ("+" in timestamp_str) or ("-" in timestamp_str) + + # Should be parseable by Swift ISO8601DateFormatter + parsed_back = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00")) + assert parsed_back.tzinfo is not None + + print(f"āœ… UTC timestamp format test passed: {timestamp_str}") + + +if __name__ == "__main__": + test_prompt_session_json_round_trip() + test_timestamp_utc_format() + print("šŸŽ‰ All model tests passed - JSON schema is stable!") diff --git a/swift_integration_spec.md b/swift_integration_spec.md new file mode 100644 index 0000000..1cd63ea --- /dev/null +++ b/swift_integration_spec.md @@ -0,0 +1,278 @@ +# FileKitty Swift Integration Specification + +## Overview +This document defines the data exchange format and API contract between the Swift UI layer and Python backend for FileKitty. + +## Communication Method +- **Primary**: JSON over subprocess CLI +- **Alternative**: PythonKit integration (TBD) + +## Data Models + +### Core Request/Response Structure + +```json +{ + "action": "string", + "session_id": "string", + "payload": {}, + "timestamp": "ISO8601" +} +``` + +### Actions + +#### 1. Process Files (`process_files`) +**Request:** +```json +{ + "action": "process_files", + "session_id": "uuid", + "payload": { + "files": ["path1", "path2"], + "selection_state": { + "mode": "All Files", + "selected_file": null, + "selected_items": [] + }, + "settings": { + "include_tree": true, + "tree_base_dir": "", + "tree_ignore_regex": "", + "include_date_modified": true, + "auto_copy": false + } + } +} +``` + +**Response:** +```json +{ + "action": "process_files", + "session_id": "uuid", + "success": true, + "payload": { + "prompt_session": { + "id": "uuid", + "timestamp": "2025-07-05T12:00:00Z", + "files": ["path1", "path2"], + "file_metadata": [ + { + "path": "path1", + "display_path": "file1.py", + "is_text_file": true, + "language": "python", + "last_modified": "2025-07-05T11:30:00Z", + "file_hash": "abc123", + "size_bytes": 1024 + } + ], + "selection_state": { + "mode": "All Files", + "selected_file": null, + "selected_items": [] + }, + "project_root": "/path/to/project", + "tree_snapshot": { + "base_path": "/path/to/project", + "base_path_display": "project/", + "ignore_regex": "\\.git|\\.pyc", + "rendered": "# Folder Tree\\n\\n```text\\nproject/\\nā”œā”€ā”€ file1.py\\n└── file2.py\\n```" + }, + "output_text": "# Combined markdown output...", + "settings": {} + } + } +} +``` + +#### 2. Get Python Classes/Functions (`get_python_symbols`) +**Request:** +```json +{ + "action": "get_python_symbols", + "session_id": "uuid", + "payload": { + "files": ["file1.py", "file2.py"] + } +} +``` + +**Response:** +```json +{ + "action": "get_python_symbols", + "session_id": "uuid", + "success": true, + "payload": { + "symbols": { + "file1.py": { + "classes": ["MyClass", "AnotherClass"], + "functions": ["my_function", "helper_func"] + }, + "file2.py": { + "classes": [], + "functions": ["main", "process"] + } + }, + "errors": [] + } +} +``` + +#### 3. Update Selection (`update_selection`) +**Request:** +```json +{ + "action": "update_selection", + "session_id": "uuid", + "payload": { + "selection_state": { + "mode": "Single File", + "selected_file": "file1.py", + "selected_items": ["MyClass", "my_function"] + } + } +} +``` + +**Response:** +```json +{ + "action": "update_selection", + "session_id": "uuid", + "success": true, + "payload": { + "output_text": "# Updated markdown output with filtered content..." + } +} +``` + +#### 4. Save Session (`save_session`) +**Request:** +```json +{ + "action": "save_session", + "session_id": "uuid", + "payload": { + "file_path": "/path/to/session.json" + } +} +``` + +**Response:** +```json +{ + "action": "save_session", + "session_id": "uuid", + "success": true, + "payload": { + "saved_path": "/path/to/session.json" + } +} +``` + +#### 5. Load Session (`load_session`) +**Request:** +```json +{ + "action": "load_session", + "session_id": "uuid", + "payload": { + "file_path": "/path/to/session.json" + } +} +``` + +**Response:** +```json +{ + "action": "load_session", + "session_id": "uuid", + "success": true, + "payload": { + "prompt_session": { + // Full PromptSession object + } + } +} +``` + +### Error Response Format +```json +{ + "action": "action_name", + "session_id": "uuid", + "success": false, + "error": { + "type": "ValidationError", + "message": "Invalid file path provided", + "details": {}, + "debug": "Traceback (most recent call last):\n File...\nValueError: Invalid path" + } +} +``` + +**Note**: The `debug` field is optional and only included when `--debug` flag is used. + +## CLI Interface + +### Command Structure +```bash +filekitty [options] +``` + +### Global Options +- `--json`: Output responses as newline-delimited JSON (NDJSON) for streaming +- `--debug`: Include Python tracebacks in error responses for debugging + +### Examples +```bash +# Process files with JSON output +filekitty process_files --files file1.py file2.py --session-id abc123 --json + +# Get symbols +filekitty get_python_symbols --files file1.py --session-id abc123 + +# Update selection with debug output +filekitty update_selection --session-id abc123 --mode "Single File" --selected-file file1.py --debug + +# Stream processing with NDJSON +filekitty process_files --files *.py --json | while read line; do + echo "Received: $line" +done +``` + +## Data Validation + +### Required Fields +- `action`: Must be valid action name +- `session_id`: Must be valid UUID +- `payload`: Must contain required fields for action + +### File Path Validation +- All file paths must be absolute +- Files must exist and be readable +- Paths must be within allowed directories (security) + +## Error Handling + +### Error Types +- `ValidationError`: Invalid input data +- `FileNotFoundError`: Requested file doesn't exist +- `PermissionError`: Cannot read file +- `ProcessingError`: Error during file processing +- `SessionError`: Session management error + +## Security Considerations + +### File Access +- Only allow access to files within project boundaries +- Validate all file paths to prevent directory traversal +- Implement file size limits + +### Command Injection +- Sanitize all command line arguments +- Use subprocess with shell=False +- Validate JSON input structure \ No newline at end of file